完善保险前后端、养殖端小程序
This commit is contained in:
@@ -189,37 +189,37 @@ const fetchMenus = async () => {
|
||||
menus.value = [
|
||||
{
|
||||
key: 'Dashboard',
|
||||
icon: () => h(DashboardOutlined),
|
||||
icon: DashboardOutlined,
|
||||
label: '仪表板',
|
||||
path: '/dashboard'
|
||||
},
|
||||
{
|
||||
key: 'DataWarehouse',
|
||||
icon: () => h(DatabaseOutlined),
|
||||
icon: DatabaseOutlined,
|
||||
label: '数据览仓',
|
||||
path: '/dashboard' // 重定向到仪表板
|
||||
},
|
||||
{
|
||||
key: 'SupervisionTask',
|
||||
icon: () => h(CheckCircleOutlined),
|
||||
icon: CheckCircleOutlined,
|
||||
label: '监管任务',
|
||||
path: '/supervision-tasks' // 使用正确的复数路径
|
||||
},
|
||||
{
|
||||
key: 'PendingInstallationTask',
|
||||
icon: () => h(ExclamationCircleOutlined),
|
||||
icon: ExclamationCircleOutlined,
|
||||
label: '待安装任务',
|
||||
path: '/pending-installation' // 使用正确的路径
|
||||
},
|
||||
{
|
||||
key: 'CompletedTask',
|
||||
icon: () => h(FileDoneOutlined),
|
||||
icon: FileDoneOutlined,
|
||||
label: '监管任务已结项',
|
||||
path: '/completed-tasks'
|
||||
},
|
||||
{
|
||||
key: 'InsuredCustomers',
|
||||
icon: () => h(ShopOutlined),
|
||||
icon: ShopOutlined,
|
||||
label: '投保客户单',
|
||||
children: [
|
||||
{
|
||||
@@ -231,7 +231,7 @@ const fetchMenus = async () => {
|
||||
},
|
||||
{
|
||||
key: 'AgriculturalInsurance',
|
||||
icon: () => h(FileProtectOutlined),
|
||||
icon: FileProtectOutlined,
|
||||
label: '生资保单',
|
||||
children: [
|
||||
{
|
||||
@@ -248,7 +248,7 @@ const fetchMenus = async () => {
|
||||
},
|
||||
{
|
||||
key: 'InsuranceTypeManagement',
|
||||
icon: () => h(MedicineBoxOutlined),
|
||||
icon: MedicineBoxOutlined,
|
||||
label: '险种管理',
|
||||
children: [
|
||||
{
|
||||
@@ -260,7 +260,7 @@ const fetchMenus = async () => {
|
||||
},
|
||||
{
|
||||
key: 'CustomerClaims',
|
||||
icon: () => h(ExclamationCircleOutlined),
|
||||
icon: ExclamationCircleOutlined,
|
||||
label: '客户理赔',
|
||||
children: [
|
||||
{
|
||||
@@ -272,25 +272,25 @@ const fetchMenus = async () => {
|
||||
},
|
||||
{
|
||||
key: 'Notifications',
|
||||
icon: () => h(BellOutlined),
|
||||
icon: BellOutlined,
|
||||
label: '消息通知',
|
||||
path: '/notifications'
|
||||
},
|
||||
{
|
||||
key: 'UserManagement',
|
||||
icon: () => h(UserAddOutlined),
|
||||
icon: UserAddOutlined,
|
||||
label: '子账号管理',
|
||||
path: '/users'
|
||||
},
|
||||
{
|
||||
key: 'SystemSettings',
|
||||
icon: () => h(SettingOutlined),
|
||||
icon: SettingOutlined,
|
||||
label: '系统设置',
|
||||
path: '/system-settings'
|
||||
},
|
||||
{
|
||||
key: 'UserProfile',
|
||||
icon: () => h(UserSwitchOutlined),
|
||||
icon: UserSwitchOutlined,
|
||||
label: '个人中心',
|
||||
path: '/dashboard' // 重定向到仪表板
|
||||
}
|
||||
|
||||
527
insurance_admin-system/src/components/PermissionManagement.vue
Normal file
527
insurance_admin-system/src/components/PermissionManagement.vue
Normal file
@@ -0,0 +1,527 @@
|
||||
<template>
|
||||
<div class="permission-management">
|
||||
<!-- 权限树和表格布局 -->
|
||||
<a-row :gutter="16">
|
||||
<!-- 左侧权限树 -->
|
||||
<a-col :span="8">
|
||||
<a-card title="权限树" size="small">
|
||||
<template #extra>
|
||||
<a-button type="primary" size="small" @click="refreshPermissionTree">
|
||||
刷新
|
||||
</a-button>
|
||||
</template>
|
||||
<a-tree
|
||||
v-model:selectedKeys="selectedTreeKeys"
|
||||
:tree-data="permissionTree"
|
||||
:field-names="{ children: 'children', title: 'name', key: 'id' }"
|
||||
@select="onTreeSelect"
|
||||
show-line
|
||||
>
|
||||
<template #title="{ name, type }">
|
||||
<span>
|
||||
<a-tag :color="type === 'menu' ? 'blue' : 'green'" size="small">
|
||||
{{ type === 'menu' ? '菜单' : '操作' }}
|
||||
</a-tag>
|
||||
{{ name }}
|
||||
</span>
|
||||
</template>
|
||||
</a-tree>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<!-- 右侧权限列表 -->
|
||||
<a-col :span="16">
|
||||
<a-card title="权限管理" size="small">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="showAddModal">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增权限
|
||||
</a-button>
|
||||
<a-button @click="refreshPermissionList">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
刷新
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<!-- 搜索表单 -->
|
||||
<a-form layout="inline" style="margin-bottom: 16px">
|
||||
<a-form-item label="权限名称">
|
||||
<a-input
|
||||
v-model:value="searchForm.name"
|
||||
placeholder="请输入权限名称"
|
||||
style="width: 150px"
|
||||
@pressEnter="searchPermissions"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="权限代码">
|
||||
<a-input
|
||||
v-model:value="searchForm.code"
|
||||
placeholder="请输入权限代码"
|
||||
style="width: 150px"
|
||||
@pressEnter="searchPermissions"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="模块">
|
||||
<a-select
|
||||
v-model:value="searchForm.module"
|
||||
placeholder="请选择模块"
|
||||
style="width: 120px"
|
||||
allowClear
|
||||
>
|
||||
<a-select-option value="user">用户管理</a-select-option>
|
||||
<a-select-option value="insurance">保险管理</a-select-option>
|
||||
<a-select-option value="application">申请管理</a-select-option>
|
||||
<a-select-option value="policy">保单管理</a-select-option>
|
||||
<a-select-option value="claim">理赔管理</a-select-option>
|
||||
<a-select-option value="system">系统管理</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="类型">
|
||||
<a-select
|
||||
v-model:value="searchForm.type"
|
||||
placeholder="请选择类型"
|
||||
style="width: 100px"
|
||||
allowClear
|
||||
>
|
||||
<a-select-option value="menu">菜单</a-select-option>
|
||||
<a-select-option value="operation">操作</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="searchPermissions" :loading="loading">
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button style="margin-left: 8px" @click="resetSearch">
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 权限表格 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="permissionList"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
@change="handleTableChange"
|
||||
size="small"
|
||||
:scroll="{ x: 1000 }"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'type'">
|
||||
<a-tag :color="record.type === 'menu' ? 'blue' : 'green'">
|
||||
{{ record.type === 'menu' ? '菜单' : '操作' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 'active' ? 'green' : 'red'">
|
||||
{{ record.status === 'active' ? '启用' : '禁用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="editPermission(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
title="确定要删除这个权限吗?"
|
||||
@confirm="deletePermission(record.id)"
|
||||
>
|
||||
<a-button type="link" size="small" danger>
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 新增/编辑权限模态框 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="isEdit ? '编辑权限' : '新增权限'"
|
||||
width="600px"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
:confirm-loading="submitLoading"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<a-form-item label="权限名称" name="name">
|
||||
<a-input v-model:value="formData.name" placeholder="请输入权限名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="权限代码" name="code">
|
||||
<a-input v-model:value="formData.code" placeholder="请输入权限代码,如:user:create" />
|
||||
</a-form-item>
|
||||
<a-form-item label="权限描述" name="description">
|
||||
<a-textarea v-model:value="formData.description" placeholder="请输入权限描述" :rows="3" />
|
||||
</a-form-item>
|
||||
<a-form-item label="所属模块" name="module">
|
||||
<a-select v-model:value="formData.module" placeholder="请选择所属模块">
|
||||
<a-select-option value="user">用户管理</a-select-option>
|
||||
<a-select-option value="insurance">保险管理</a-select-option>
|
||||
<a-select-option value="application">申请管理</a-select-option>
|
||||
<a-select-option value="policy">保单管理</a-select-option>
|
||||
<a-select-option value="claim">理赔管理</a-select-option>
|
||||
<a-select-option value="system">系统管理</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="权限类型" name="type">
|
||||
<a-select v-model:value="formData.type" placeholder="请选择权限类型">
|
||||
<a-select-option value="menu">菜单</a-select-option>
|
||||
<a-select-option value="operation">操作</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="父级权限" name="parent_id">
|
||||
<a-tree-select
|
||||
v-model:value="formData.parent_id"
|
||||
:tree-data="parentPermissionOptions"
|
||||
:field-names="{ children: 'children', label: 'name', value: 'id' }"
|
||||
placeholder="请选择父级权限(可选)"
|
||||
allowClear
|
||||
tree-default-expand-all
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-select v-model:value="formData.status" placeholder="请选择状态">
|
||||
<a-select-option value="active">启用</a-select-option>
|
||||
<a-select-option value="inactive">禁用</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="排序" name="sort_order">
|
||||
<a-input-number v-model:value="formData.sort_order" :min="0" placeholder="请输入排序值" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { PlusOutlined, ReloadOutlined } from '@ant-design/icons-vue'
|
||||
import { permissionAPI } from '@/utils/api'
|
||||
|
||||
// 数据定义
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const permissionList = ref([])
|
||||
const permissionTree = ref([])
|
||||
const selectedTreeKeys = ref([])
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
name: '',
|
||||
code: '',
|
||||
module: '',
|
||||
type: ''
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条,共 ${total} 条`
|
||||
})
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 60
|
||||
},
|
||||
{
|
||||
title: '权限名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '权限代码',
|
||||
dataIndex: 'code',
|
||||
key: 'code',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '模块',
|
||||
dataIndex: 'module',
|
||||
key: 'module',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
dataIndex: 'sort_order',
|
||||
key: 'sort_order',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 120,
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
// 模态框相关
|
||||
const modalVisible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const formRef = ref()
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
code: '',
|
||||
description: '',
|
||||
module: '',
|
||||
type: '',
|
||||
parent_id: null,
|
||||
status: 'active',
|
||||
sort_order: 0
|
||||
})
|
||||
|
||||
const parentPermissionOptions = ref([])
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入权限名称', trigger: 'blur' }
|
||||
],
|
||||
code: [
|
||||
{ required: true, message: '请输入权限代码', trigger: 'blur' },
|
||||
{ pattern: /^[a-zA-Z0-9:_-]+$/, message: '权限代码只能包含字母、数字、冒号、下划线和横线', trigger: 'blur' }
|
||||
],
|
||||
module: [
|
||||
{ required: true, message: '请选择所属模块', trigger: 'change' }
|
||||
],
|
||||
type: [
|
||||
{ required: true, message: '请选择权限类型', trigger: 'change' }
|
||||
],
|
||||
status: [
|
||||
{ required: true, message: '请选择状态', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
// 方法定义
|
||||
const loadPermissions = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
...searchForm
|
||||
}
|
||||
|
||||
const response = await permissionAPI.getList(params)
|
||||
const responseData = response.data || response
|
||||
|
||||
permissionList.value = (responseData.permissions || []).map(permission => ({
|
||||
...permission,
|
||||
key: permission.id
|
||||
}))
|
||||
|
||||
pagination.total = responseData.total || 0
|
||||
} catch (error) {
|
||||
console.error('加载权限列表失败:', error)
|
||||
message.error('加载权限列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadPermissionTree = async () => {
|
||||
try {
|
||||
const response = await permissionAPI.getTree()
|
||||
const responseData = response.data || response
|
||||
permissionTree.value = responseData.tree || []
|
||||
|
||||
// 同时更新父级权限选项
|
||||
parentPermissionOptions.value = buildParentOptions(permissionTree.value)
|
||||
} catch (error) {
|
||||
console.error('加载权限树失败:', error)
|
||||
message.error('加载权限树失败')
|
||||
}
|
||||
}
|
||||
|
||||
const buildParentOptions = (tree) => {
|
||||
const options = []
|
||||
const traverse = (nodes) => {
|
||||
nodes.forEach(node => {
|
||||
options.push({
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
children: node.children ? buildParentOptions(node.children) : []
|
||||
})
|
||||
if (node.children && node.children.length > 0) {
|
||||
traverse(node.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
traverse(tree)
|
||||
return options
|
||||
}
|
||||
|
||||
const searchPermissions = () => {
|
||||
pagination.current = 1
|
||||
loadPermissions()
|
||||
}
|
||||
|
||||
const resetSearch = () => {
|
||||
Object.assign(searchForm, {
|
||||
name: '',
|
||||
code: '',
|
||||
module: '',
|
||||
type: ''
|
||||
})
|
||||
pagination.current = 1
|
||||
loadPermissions()
|
||||
}
|
||||
|
||||
const handleTableChange = (pag) => {
|
||||
pagination.current = pag.current
|
||||
pagination.pageSize = pag.pageSize
|
||||
loadPermissions()
|
||||
}
|
||||
|
||||
const refreshPermissionList = () => {
|
||||
loadPermissions()
|
||||
}
|
||||
|
||||
const refreshPermissionTree = () => {
|
||||
loadPermissionTree()
|
||||
}
|
||||
|
||||
const onTreeSelect = (selectedKeys, info) => {
|
||||
console.log('选中的权限:', info.selectedNodes)
|
||||
}
|
||||
|
||||
const showAddModal = () => {
|
||||
isEdit.value = false
|
||||
modalVisible.value = true
|
||||
resetForm()
|
||||
}
|
||||
|
||||
const editPermission = (record) => {
|
||||
isEdit.value = true
|
||||
modalVisible.value = true
|
||||
Object.assign(formData, {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
code: record.code,
|
||||
description: record.description,
|
||||
module: record.module,
|
||||
type: record.type,
|
||||
parent_id: record.parent_id,
|
||||
status: record.status,
|
||||
sort_order: record.sort_order
|
||||
})
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(formData, {
|
||||
name: '',
|
||||
code: '',
|
||||
description: '',
|
||||
module: '',
|
||||
type: '',
|
||||
parent_id: null,
|
||||
status: 'active',
|
||||
sort_order: 0
|
||||
})
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
submitLoading.value = true
|
||||
|
||||
if (isEdit.value) {
|
||||
await permissionAPI.update(formData.id, formData)
|
||||
message.success('更新权限成功')
|
||||
} else {
|
||||
await permissionAPI.create(formData)
|
||||
message.success('创建权限成功')
|
||||
}
|
||||
|
||||
modalVisible.value = false
|
||||
loadPermissions()
|
||||
loadPermissionTree()
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
message.error(isEdit.value ? '更新权限失败' : '创建权限失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
modalVisible.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
const deletePermission = async (id) => {
|
||||
try {
|
||||
await permissionAPI.delete(id)
|
||||
message.success('删除权限成功')
|
||||
loadPermissions()
|
||||
loadPermissionTree()
|
||||
} catch (error) {
|
||||
console.error('删除权限失败:', error)
|
||||
message.error('删除权限失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadPermissions()
|
||||
loadPermissionTree()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.permission-management {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-card {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-tree {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,525 @@
|
||||
<template>
|
||||
<div class="role-permission-management">
|
||||
<!-- 角色权限分配区域 -->
|
||||
<a-card title="角色权限管理" style="margin-bottom: 16px">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleRefresh">
|
||||
刷新数据
|
||||
</a-button>
|
||||
<a-button @click="handleCopyPermissions">
|
||||
复制权限
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<!-- 角色选择和权限统计 -->
|
||||
<a-row :gutter="16" style="margin-bottom: 16px">
|
||||
<a-col :span="8">
|
||||
<a-card size="small" title="选择角色">
|
||||
<a-select
|
||||
v-model:value="selectedRoleId"
|
||||
placeholder="请选择角色"
|
||||
style="width: 100%"
|
||||
@change="handleRoleChange"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="role in roles"
|
||||
:key="role.id"
|
||||
:value="role.id"
|
||||
>
|
||||
{{ role.name }} ({{ role.description }})
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="16">
|
||||
<a-row :gutter="8">
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic
|
||||
title="总权限数"
|
||||
:value="permissionStats.total"
|
||||
:value-style="{ color: '#1890ff' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic
|
||||
title="已分配"
|
||||
:value="permissionStats.assigned"
|
||||
:value-style="{ color: '#52c41a' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic
|
||||
title="未分配"
|
||||
:value="permissionStats.unassigned"
|
||||
:value-style="{ color: '#faad14' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic
|
||||
title="分配率"
|
||||
:value="permissionStats.percentage"
|
||||
suffix="%"
|
||||
:value-style="{ color: '#722ed1' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 权限树形结构 -->
|
||||
<div v-if="selectedRoleId">
|
||||
<a-divider>权限分配</a-divider>
|
||||
|
||||
<!-- 批量操作按钮 -->
|
||||
<div style="margin-bottom: 16px">
|
||||
<a-space>
|
||||
<a-button @click="handleSelectAll">全选</a-button>
|
||||
<a-button @click="handleSelectNone">全不选</a-button>
|
||||
<a-button @click="handleSelectByModule">按模块选择</a-button>
|
||||
<a-button type="primary" @click="handleSavePermissions" :loading="saveLoading">
|
||||
保存权限设置
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<!-- 按模块分组的权限列表 -->
|
||||
<a-collapse v-model:activeKey="activeModules" ghost>
|
||||
<a-collapse-panel
|
||||
v-for="module in permissionModules"
|
||||
:key="module.module"
|
||||
:header="`${module.module} (${module.permissions.length}个权限)`"
|
||||
>
|
||||
<template #extra>
|
||||
<a-checkbox
|
||||
:checked="isModuleAllSelected(module.module)"
|
||||
:indeterminate="isModuleIndeterminate(module.module)"
|
||||
@change="(e) => handleModuleCheckChange(e, module.module)"
|
||||
@click.stop
|
||||
>
|
||||
全选
|
||||
</a-checkbox>
|
||||
</template>
|
||||
|
||||
<a-row :gutter="[16, 8]">
|
||||
<a-col
|
||||
v-for="permission in module.permissions"
|
||||
:key="permission.id"
|
||||
:span="8"
|
||||
>
|
||||
<a-checkbox
|
||||
v-model:checked="selectedPermissions[permission.id]"
|
||||
@change="handlePermissionChange"
|
||||
>
|
||||
<a-tooltip :title="permission.description">
|
||||
<span>{{ permission.name }}</span>
|
||||
<a-tag
|
||||
:color="getPermissionTypeColor(permission.type)"
|
||||
size="small"
|
||||
style="margin-left: 4px"
|
||||
>
|
||||
{{ permission.type }}
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
</a-checkbox>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</div>
|
||||
|
||||
<!-- 未选择角色时的提示 -->
|
||||
<div v-else class="empty-state">
|
||||
<a-empty description="请先选择一个角色来管理权限" />
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<!-- 权限复制模态框 -->
|
||||
<a-modal
|
||||
v-model:open="copyModalVisible"
|
||||
title="复制权限"
|
||||
@ok="handleConfirmCopy"
|
||||
@cancel="copyModalVisible = false"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="源角色">
|
||||
<a-select
|
||||
v-model:value="copyForm.sourceRoleId"
|
||||
placeholder="请选择源角色"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="role in roles"
|
||||
:key="role.id"
|
||||
:value="role.id"
|
||||
>
|
||||
{{ role.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="目标角色">
|
||||
<a-select
|
||||
v-model:value="copyForm.targetRoleId"
|
||||
placeholder="请选择目标角色"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="role in roles"
|
||||
:key="role.id"
|
||||
:value="role.id"
|
||||
>
|
||||
{{ role.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="复制方式">
|
||||
<a-radio-group v-model:value="copyForm.mode">
|
||||
<a-radio value="replace">替换(清空目标角色权限后复制)</a-radio>
|
||||
<a-radio value="merge">合并(保留目标角色原有权限)</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 按模块选择模态框 -->
|
||||
<a-modal
|
||||
v-model:open="moduleSelectModalVisible"
|
||||
title="按模块选择权限"
|
||||
@ok="handleConfirmModuleSelect"
|
||||
@cancel="moduleSelectModalVisible = false"
|
||||
>
|
||||
<a-checkbox-group v-model:value="selectedModules" style="width: 100%">
|
||||
<a-row>
|
||||
<a-col
|
||||
v-for="module in permissionModules"
|
||||
:key="module.module"
|
||||
:span="12"
|
||||
style="margin-bottom: 8px"
|
||||
>
|
||||
<a-checkbox :value="module.module">
|
||||
{{ module.module }} ({{ module.permissions.length }}个)
|
||||
</a-checkbox>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-checkbox-group>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { rolePermissionAPI } from '@/utils/api'
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const saveLoading = ref(false)
|
||||
const roles = ref([])
|
||||
const permissions = ref([])
|
||||
const selectedRoleId = ref(null)
|
||||
const selectedPermissions = reactive({})
|
||||
const activeModules = ref([])
|
||||
|
||||
// 模态框状态
|
||||
const copyModalVisible = ref(false)
|
||||
const moduleSelectModalVisible = ref(false)
|
||||
const selectedModules = ref([])
|
||||
|
||||
// 复制表单
|
||||
const copyForm = reactive({
|
||||
sourceRoleId: null,
|
||||
targetRoleId: null,
|
||||
mode: 'replace'
|
||||
})
|
||||
|
||||
// 计算属性 - 权限模块分组
|
||||
const permissionModules = computed(() => {
|
||||
const modules = {}
|
||||
permissions.value.forEach(permission => {
|
||||
if (!modules[permission.module]) {
|
||||
modules[permission.module] = {
|
||||
module: permission.module,
|
||||
permissions: []
|
||||
}
|
||||
}
|
||||
modules[permission.module].permissions.push(permission)
|
||||
})
|
||||
return Object.values(modules)
|
||||
})
|
||||
|
||||
// 计算属性 - 权限统计
|
||||
const permissionStats = computed(() => {
|
||||
const total = permissions.value.length
|
||||
const assigned = Object.values(selectedPermissions).filter(Boolean).length
|
||||
const unassigned = total - assigned
|
||||
const percentage = total > 0 ? Math.round((assigned / total) * 100) : 0
|
||||
|
||||
return {
|
||||
total,
|
||||
assigned,
|
||||
unassigned,
|
||||
percentage
|
||||
}
|
||||
})
|
||||
|
||||
// 获取权限类型颜色
|
||||
const getPermissionTypeColor = (type) => {
|
||||
const colorMap = {
|
||||
'view': 'blue',
|
||||
'create': 'green',
|
||||
'update': 'orange',
|
||||
'delete': 'red',
|
||||
'manage': 'purple',
|
||||
'admin': 'magenta'
|
||||
}
|
||||
return colorMap[type] || 'default'
|
||||
}
|
||||
|
||||
// 检查模块是否全选
|
||||
const isModuleAllSelected = (module) => {
|
||||
const modulePermissions = permissionModules.value.find(m => m.module === module)?.permissions || []
|
||||
return modulePermissions.length > 0 && modulePermissions.every(p => selectedPermissions[p.id])
|
||||
}
|
||||
|
||||
// 检查模块是否部分选择
|
||||
const isModuleIndeterminate = (module) => {
|
||||
const modulePermissions = permissionModules.value.find(m => m.module === module)?.permissions || []
|
||||
const selectedCount = modulePermissions.filter(p => selectedPermissions[p.id]).length
|
||||
return selectedCount > 0 && selectedCount < modulePermissions.length
|
||||
}
|
||||
|
||||
// 获取所有角色和权限数据
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const [rolesResponse, permissionsResponse] = await Promise.all([
|
||||
rolePermissionAPI.getAllRolesWithPermissions(),
|
||||
rolePermissionAPI.getAllPermissions()
|
||||
])
|
||||
|
||||
if (rolesResponse.success) {
|
||||
roles.value = rolesResponse.data
|
||||
}
|
||||
|
||||
if (permissionsResponse.success) {
|
||||
permissions.value = permissionsResponse.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error)
|
||||
message.error('获取数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 角色变化处理
|
||||
const handleRoleChange = async (roleId) => {
|
||||
if (!roleId) return
|
||||
|
||||
try {
|
||||
const response = await rolePermissionAPI.getRolePermissions(roleId)
|
||||
if (response.success) {
|
||||
// 重置选择状态
|
||||
Object.keys(selectedPermissions).forEach(key => {
|
||||
selectedPermissions[key] = false
|
||||
})
|
||||
|
||||
// 设置当前角色的权限
|
||||
response.data.permissions.forEach(permission => {
|
||||
selectedPermissions[permission.id] = true
|
||||
})
|
||||
|
||||
// 默认展开所有模块
|
||||
activeModules.value = permissionModules.value.map(m => m.module)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取角色权限失败:', error)
|
||||
message.error('获取角色权限失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 权限变化处理
|
||||
const handlePermissionChange = () => {
|
||||
// 这里可以添加实时保存逻辑,或者只在点击保存时处理
|
||||
}
|
||||
|
||||
// 模块复选框变化处理
|
||||
const handleModuleCheckChange = (e, module) => {
|
||||
const checked = e.target.checked
|
||||
const modulePermissions = permissionModules.value.find(m => m.module === module)?.permissions || []
|
||||
|
||||
modulePermissions.forEach(permission => {
|
||||
selectedPermissions[permission.id] = checked
|
||||
})
|
||||
}
|
||||
|
||||
// 全选处理
|
||||
const handleSelectAll = () => {
|
||||
permissions.value.forEach(permission => {
|
||||
selectedPermissions[permission.id] = true
|
||||
})
|
||||
}
|
||||
|
||||
// 全不选处理
|
||||
const handleSelectNone = () => {
|
||||
permissions.value.forEach(permission => {
|
||||
selectedPermissions[permission.id] = false
|
||||
})
|
||||
}
|
||||
|
||||
// 按模块选择处理
|
||||
const handleSelectByModule = () => {
|
||||
selectedModules.value = []
|
||||
moduleSelectModalVisible.value = true
|
||||
}
|
||||
|
||||
// 确认按模块选择
|
||||
const handleConfirmModuleSelect = () => {
|
||||
// 先清空所有选择
|
||||
handleSelectNone()
|
||||
|
||||
// 选择指定模块的权限
|
||||
selectedModules.value.forEach(module => {
|
||||
const modulePermissions = permissionModules.value.find(m => m.module === module)?.permissions || []
|
||||
modulePermissions.forEach(permission => {
|
||||
selectedPermissions[permission.id] = true
|
||||
})
|
||||
})
|
||||
|
||||
moduleSelectModalVisible.value = false
|
||||
}
|
||||
|
||||
// 保存权限设置
|
||||
const handleSavePermissions = async () => {
|
||||
if (!selectedRoleId.value) {
|
||||
message.warning('请先选择角色')
|
||||
return
|
||||
}
|
||||
|
||||
saveLoading.value = true
|
||||
try {
|
||||
const permissionIds = Object.keys(selectedPermissions)
|
||||
.filter(id => selectedPermissions[id])
|
||||
.map(id => parseInt(id))
|
||||
|
||||
const response = await rolePermissionAPI.assignRolePermissions(selectedRoleId.value, {
|
||||
permissionIds,
|
||||
mode: 'replace'
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
message.success('权限设置保存成功')
|
||||
} else {
|
||||
message.error(response.message || '保存失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存权限设置失败:', error)
|
||||
message.error('保存权限设置失败')
|
||||
} finally {
|
||||
saveLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 复制权限处理
|
||||
const handleCopyPermissions = () => {
|
||||
copyForm.sourceRoleId = null
|
||||
copyForm.targetRoleId = null
|
||||
copyForm.mode = 'replace'
|
||||
copyModalVisible.value = true
|
||||
}
|
||||
|
||||
// 确认复制权限
|
||||
const handleConfirmCopy = async () => {
|
||||
if (!copyForm.sourceRoleId || !copyForm.targetRoleId) {
|
||||
message.warning('请选择源角色和目标角色')
|
||||
return
|
||||
}
|
||||
|
||||
if (copyForm.sourceRoleId === copyForm.targetRoleId) {
|
||||
message.warning('源角色和目标角色不能相同')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await rolePermissionAPI.copyRolePermissions(
|
||||
copyForm.sourceRoleId,
|
||||
copyForm.targetRoleId,
|
||||
copyForm.mode
|
||||
)
|
||||
|
||||
if (response.success) {
|
||||
message.success('权限复制成功')
|
||||
copyModalVisible.value = false
|
||||
|
||||
// 如果当前选择的是目标角色,刷新权限显示
|
||||
if (selectedRoleId.value === copyForm.targetRoleId) {
|
||||
handleRoleChange(selectedRoleId.value)
|
||||
}
|
||||
} else {
|
||||
message.error(response.message || '复制失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('复制权限失败:', error)
|
||||
message.error('复制权限失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
const handleRefresh = () => {
|
||||
fetchData()
|
||||
if (selectedRoleId.value) {
|
||||
handleRoleChange(selectedRoleId.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听权限数据变化,初始化选择状态
|
||||
watch(permissions, (newPermissions) => {
|
||||
newPermissions.forEach(permission => {
|
||||
if (!(permission.id in selectedPermissions)) {
|
||||
selectedPermissions[permission.id] = false
|
||||
}
|
||||
})
|
||||
}, { immediate: true })
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.role-permission-management {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.ant-statistic-content {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ant-collapse-header {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ant-checkbox-wrapper {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.ant-tag {
|
||||
margin-left: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,213 +1,207 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import axios from 'axios'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
// 兼容旧版本的token存储
|
||||
const accessToken = ref(localStorage.getItem('accessToken') || localStorage.getItem('token'))
|
||||
const refreshToken = ref(localStorage.getItem('refreshToken'))
|
||||
const userInfo = ref(JSON.parse(localStorage.getItem('userInfo') || '{}'))
|
||||
const tokenExpiresAt = ref(localStorage.getItem('tokenExpiresAt'))
|
||||
// 状态
|
||||
const accessToken = ref(localStorage.getItem('accessToken') || '')
|
||||
const refreshToken = ref(localStorage.getItem('refreshToken') || '')
|
||||
const userInfo = ref(JSON.parse(localStorage.getItem('userInfo') || 'null'))
|
||||
const tokenExpiresAt = ref(parseInt(localStorage.getItem('tokenExpiresAt') || '0'))
|
||||
|
||||
// 防抖更新localStorage
|
||||
let updateTimer = null
|
||||
const debouncedUpdateStorage = (key, value) => {
|
||||
clearTimeout(updateTimer)
|
||||
updateTimer = setTimeout(() => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
localStorage.removeItem(key)
|
||||
} else {
|
||||
localStorage.setItem(key, typeof value === 'object' ? JSON.stringify(value) : value)
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// 计算属性 - 使用缓存避免重复计算
|
||||
const isTokenExpired = computed(() => {
|
||||
if (!tokenExpiresAt.value) return true
|
||||
return Date.now() >= tokenExpiresAt.value
|
||||
})
|
||||
|
||||
// 计算属性:检查token是否即将过期(提前5分钟刷新)
|
||||
const isTokenExpiringSoon = computed(() => {
|
||||
if (!tokenExpiresAt.value) return false
|
||||
const expiresAt = new Date(tokenExpiresAt.value)
|
||||
const now = new Date()
|
||||
const fiveMinutesFromNow = new Date(now.getTime() + 5 * 60 * 1000)
|
||||
return expiresAt <= fiveMinutesFromNow
|
||||
const fiveMinutes = 5 * 60 * 1000
|
||||
return Date.now() >= (tokenExpiresAt.value - fiveMinutes)
|
||||
})
|
||||
|
||||
// 计算属性:检查token是否已过期
|
||||
const isTokenExpired = computed(() => {
|
||||
if (!tokenExpiresAt.value) return false
|
||||
const expiresAt = new Date(tokenExpiresAt.value)
|
||||
const now = new Date()
|
||||
return expiresAt <= now
|
||||
const isLoggedIn = computed(() => {
|
||||
return !!accessToken.value && !isTokenExpired.value
|
||||
})
|
||||
|
||||
// 设置访问令牌
|
||||
const setAccessToken = (newToken) => {
|
||||
accessToken.value = newToken
|
||||
localStorage.setItem('accessToken', newToken)
|
||||
// 兼容旧版本
|
||||
localStorage.setItem('token', newToken)
|
||||
}
|
||||
|
||||
// 设置刷新令牌
|
||||
const setRefreshToken = (newRefreshToken) => {
|
||||
refreshToken.value = newRefreshToken
|
||||
localStorage.setItem('refreshToken', newRefreshToken)
|
||||
}
|
||||
|
||||
// 设置令牌过期时间
|
||||
const setTokenExpiresAt = (expiresIn) => {
|
||||
const expiresAt = new Date(Date.now() + expiresIn * 1000)
|
||||
tokenExpiresAt.value = expiresAt.toISOString()
|
||||
localStorage.setItem('tokenExpiresAt', expiresAt.toISOString())
|
||||
}
|
||||
|
||||
// 设置完整的认证信息
|
||||
|
||||
// Actions
|
||||
const setAuthData = (authData) => {
|
||||
if (authData.accessToken) {
|
||||
setAccessToken(authData.accessToken)
|
||||
if (!authData) return
|
||||
|
||||
const { accessToken: newAccessToken, refreshToken: newRefreshToken, user, expiresIn } = authData
|
||||
|
||||
// 批量更新状态,避免多次触发响应式更新
|
||||
const updates = []
|
||||
|
||||
if (newAccessToken && newAccessToken !== accessToken.value) {
|
||||
accessToken.value = newAccessToken
|
||||
updates.push(['accessToken', newAccessToken])
|
||||
}
|
||||
if (authData.refreshToken) {
|
||||
setRefreshToken(authData.refreshToken)
|
||||
|
||||
if (newRefreshToken && newRefreshToken !== refreshToken.value) {
|
||||
refreshToken.value = newRefreshToken
|
||||
updates.push(['refreshToken', newRefreshToken])
|
||||
}
|
||||
if (authData.accessTokenExpiresIn) {
|
||||
setTokenExpiresAt(authData.accessTokenExpiresIn)
|
||||
|
||||
if (user && JSON.stringify(user) !== JSON.stringify(userInfo.value)) {
|
||||
userInfo.value = user
|
||||
updates.push(['userInfo', user])
|
||||
}
|
||||
if (authData.user) {
|
||||
setUserInfo(authData.user)
|
||||
}
|
||||
}
|
||||
|
||||
const setUserInfo = (info) => {
|
||||
userInfo.value = info
|
||||
localStorage.setItem('userInfo', JSON.stringify(info))
|
||||
}
|
||||
|
||||
// Token刷新方法
|
||||
const refreshAccessToken = async () => {
|
||||
try {
|
||||
if (!refreshToken.value) {
|
||||
throw new Error('没有刷新令牌')
|
||||
|
||||
if (expiresIn) {
|
||||
const newExpiresAt = Date.now() + (expiresIn * 1000)
|
||||
if (newExpiresAt !== tokenExpiresAt.value) {
|
||||
tokenExpiresAt.value = newExpiresAt
|
||||
updates.push(['tokenExpiresAt', newExpiresAt])
|
||||
}
|
||||
|
||||
const response = await axios.post('/api/auth/refresh', {
|
||||
refreshToken: refreshToken.value
|
||||
}
|
||||
|
||||
// 批量更新localStorage
|
||||
updates.forEach(([key, value]) => {
|
||||
debouncedUpdateStorage(key, value)
|
||||
})
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
// 清除状态
|
||||
accessToken.value = ''
|
||||
refreshToken.value = ''
|
||||
userInfo.value = null
|
||||
tokenExpiresAt.value = 0
|
||||
|
||||
// 清除localStorage
|
||||
const keysToRemove = ['accessToken', 'refreshToken', 'userInfo', 'tokenExpiresAt']
|
||||
keysToRemove.forEach(key => localStorage.removeItem(key))
|
||||
}
|
||||
|
||||
// 刷新访问令牌
|
||||
const refreshAccessToken = async () => {
|
||||
if (!refreshToken.value) {
|
||||
throw new Error('没有刷新令牌')
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:3000/api/auth/refresh', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
refreshToken: refreshToken.value
|
||||
})
|
||||
})
|
||||
|
||||
if (response.data.success) {
|
||||
const authData = response.data.data
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('刷新令牌失败')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// 修复:检查响应格式,支持多种成功状态
|
||||
if (data.status === 'success' || data.code === 200 || data.success) {
|
||||
const authData = data.data || data
|
||||
setAuthData(authData)
|
||||
console.log('令牌刷新成功')
|
||||
return authData.accessToken
|
||||
} else {
|
||||
throw new Error(response.data.message || '刷新令牌失败')
|
||||
throw new Error(data.message || '刷新令牌失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('刷新令牌失败:', error)
|
||||
// 刷新失败,清除所有认证信息
|
||||
console.error('刷新访问令牌失败:', error)
|
||||
logout()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 自动刷新令牌(如果需要的话)
|
||||
const ensureValidToken = async () => {
|
||||
if (!accessToken.value) {
|
||||
throw new Error('没有访问令牌')
|
||||
}
|
||||
|
||||
if (isTokenExpired.value) {
|
||||
// Token已过期,尝试刷新
|
||||
return await refreshAccessToken()
|
||||
} else if (isTokenExpiringSoon.value) {
|
||||
// Token即将过期,主动刷新
|
||||
try {
|
||||
return await refreshAccessToken()
|
||||
} catch (error) {
|
||||
// 刷新失败但当前token还未过期,继续使用当前token
|
||||
console.warn('主动刷新失败,继续使用当前token:', error)
|
||||
return accessToken.value
|
||||
}
|
||||
}
|
||||
|
||||
return accessToken.value
|
||||
}
|
||||
|
||||
// 自动重新登录(委托给认证服务)
|
||||
// 自动重新登录方法
|
||||
const autoRelogin = async () => {
|
||||
// 导入认证服务(避免循环依赖)
|
||||
const { default: authService } = await import('@/services/authService')
|
||||
return authService.autoRelogin()
|
||||
}
|
||||
|
||||
// 获取保存的登录凭据
|
||||
const getSavedCredentials = () => {
|
||||
try {
|
||||
// 检查是否有有效的refresh token
|
||||
// 首先尝试使用refresh token刷新
|
||||
if (refreshToken.value) {
|
||||
return {
|
||||
refreshToken: refreshToken.value
|
||||
try {
|
||||
await refreshAccessToken()
|
||||
return true
|
||||
} catch (error) {
|
||||
console.warn('使用refresh token刷新失败,尝试其他方式:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 可以在这里添加其他类型的保存凭据检查
|
||||
// 例如:记住的用户名、设备指纹等
|
||||
// 检查是否有记住的登录信息
|
||||
const rememberedUsername = localStorage.getItem('rememberedUsername')
|
||||
const rememberLogin = localStorage.getItem('rememberLogin') === 'true'
|
||||
|
||||
return null
|
||||
} catch (error) {
|
||||
console.error('获取保存的登录凭据失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
const fetchUserInfo = async () => {
|
||||
try {
|
||||
// 动态导入API以避免循环依赖
|
||||
const { authAPI } = await import('@/utils/api')
|
||||
const response = await authAPI.getProfile()
|
||||
|
||||
if (response.data && response.data.status === 'success') {
|
||||
const userData = response.data.data
|
||||
setUserInfo(userData)
|
||||
return userData
|
||||
} else {
|
||||
throw new Error(response.data?.message || '获取用户信息失败')
|
||||
if (rememberedUsername && rememberLogin) {
|
||||
// 这里可以实现使用记住的凭据自动登录
|
||||
// 但出于安全考虑,通常不会保存密码
|
||||
console.log('检测到记住的用户名,但需要用户重新输入密码')
|
||||
}
|
||||
|
||||
return false
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
throw error
|
||||
console.error('自动重新登录失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
accessToken.value = null
|
||||
refreshToken.value = null
|
||||
userInfo.value = {}
|
||||
tokenExpiresAt.value = null
|
||||
localStorage.removeItem('accessToken')
|
||||
localStorage.removeItem('refreshToken')
|
||||
localStorage.removeItem('userInfo')
|
||||
localStorage.removeItem('tokenExpiresAt')
|
||||
// 兼容旧版本
|
||||
localStorage.removeItem('token')
|
||||
|
||||
// 确保token有效
|
||||
const ensureValidToken = async () => {
|
||||
if (!accessToken.value) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 如果token已过期,尝试刷新
|
||||
if (isTokenExpired.value) {
|
||||
try {
|
||||
return await refreshAccessToken()
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 如果token即将过期(5分钟内),提前刷新
|
||||
if (isTokenExpiringSoon.value) {
|
||||
try {
|
||||
return await refreshAccessToken()
|
||||
} catch (error) {
|
||||
// 刷新失败,但当前token仍然有效,继续使用
|
||||
console.warn('提前刷新token失败,继续使用当前token:', error)
|
||||
return accessToken.value
|
||||
}
|
||||
}
|
||||
|
||||
return accessToken.value
|
||||
}
|
||||
|
||||
// 兼容旧版本的方法
|
||||
const setToken = (newToken) => {
|
||||
setAccessToken(newToken)
|
||||
}
|
||||
|
||||
const token = computed(() => accessToken.value)
|
||||
|
||||
|
||||
return {
|
||||
// 新的双Token属性
|
||||
// 状态
|
||||
accessToken,
|
||||
refreshToken,
|
||||
userInfo,
|
||||
tokenExpiresAt,
|
||||
isTokenExpiringSoon,
|
||||
isTokenExpired,
|
||||
|
||||
// 新的方法
|
||||
setAccessToken,
|
||||
setRefreshToken,
|
||||
setTokenExpiresAt,
|
||||
// 计算属性
|
||||
isTokenExpired,
|
||||
isTokenExpiringSoon,
|
||||
isLoggedIn,
|
||||
|
||||
// 方法
|
||||
setAuthData,
|
||||
logout,
|
||||
refreshAccessToken,
|
||||
ensureValidToken,
|
||||
autoRelogin,
|
||||
getSavedCredentials,
|
||||
fetchUserInfo,
|
||||
|
||||
// 兼容旧版本的属性和方法
|
||||
token,
|
||||
setToken,
|
||||
setUserInfo,
|
||||
logout
|
||||
autoRelogin
|
||||
}
|
||||
})
|
||||
@@ -25,7 +25,7 @@ export const userAPI = {
|
||||
|
||||
export const menuAPI = {
|
||||
getMenus: async () => {
|
||||
const response = await api.get('/menus/public');
|
||||
const response = await api.get('/menus');
|
||||
return response.data; // 返回响应的data部分
|
||||
},
|
||||
getAllMenus: async () => {
|
||||
@@ -70,8 +70,9 @@ export const claimAPI = {
|
||||
}
|
||||
|
||||
export const dashboardAPI = {
|
||||
getStats: () => api.get('/system/stats'),
|
||||
getRecentActivities: () => api.get('/system/logs?limit=10')
|
||||
getStats: () => api.get('/dashboard/stats'),
|
||||
getRecentActivities: () => api.get('/dashboard/recent-activities'),
|
||||
getChartData: (params) => api.get('/dashboard/chart-data', { params })
|
||||
}
|
||||
|
||||
// 设备预警API
|
||||
@@ -160,4 +161,40 @@ export const operationLogAPI = {
|
||||
export: (params) => api.get('/operation-logs/export', { params })
|
||||
}
|
||||
|
||||
// 权限管理API
|
||||
export const permissionAPI = {
|
||||
getList: (params) => api.get('/permissions', { params }),
|
||||
create: (data) => api.post('/permissions', data),
|
||||
update: (id, data) => api.put(`/permissions/${id}`, data),
|
||||
delete: (id) => api.delete(`/permissions/${id}`),
|
||||
getTree: () => api.get('/permissions/tree'),
|
||||
getRolePermissions: (roleId) => api.get(`/permissions/roles/${roleId}`)
|
||||
}
|
||||
|
||||
// 角色权限管理API
|
||||
export const rolePermissionAPI = {
|
||||
// 获取所有角色及其权限
|
||||
getAllRolesWithPermissions: () => api.get('/role-permissions/roles'),
|
||||
|
||||
// 获取所有权限
|
||||
getAllPermissions: () => api.get('/role-permissions/permissions'),
|
||||
|
||||
// 获取指定角色的详细权限信息
|
||||
getRolePermissions: (roleId) => api.get(`/role-permissions/roles/${roleId}`),
|
||||
|
||||
// 批量分配角色权限
|
||||
assignRolePermissions: (roleId, data) => api.post(`/role-permissions/roles/${roleId}/assign`, data),
|
||||
|
||||
// 复制角色权限
|
||||
copyRolePermissions: (sourceRoleId, targetRoleId, mode) =>
|
||||
api.post(`/role-permissions/roles/${sourceRoleId}/copy/${targetRoleId}`, { mode }),
|
||||
|
||||
// 检查用户权限
|
||||
checkUserPermission: (userId, permissionCode) =>
|
||||
api.get(`/role-permissions/users/${userId}/check/${permissionCode}`),
|
||||
|
||||
// 获取权限统计
|
||||
getPermissionStats: () => api.get('/role-permissions/stats')
|
||||
}
|
||||
|
||||
export default api
|
||||
119
insurance_admin-system/src/utils/dataValidator.js
Normal file
119
insurance_admin-system/src/utils/dataValidator.js
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* 数据验证工具
|
||||
* 用于确保 API 响应数据的一致性和安全性
|
||||
*/
|
||||
|
||||
/**
|
||||
* 验证并确保数据是数组
|
||||
* @param {any} data - 需要验证的数据
|
||||
* @param {string} context - 上下文信息,用于日志记录
|
||||
* @returns {Array} 确保返回数组
|
||||
*/
|
||||
export const ensureArray = (data, context = 'unknown') => {
|
||||
if (Array.isArray(data)) {
|
||||
return data
|
||||
}
|
||||
|
||||
console.warn(`[DataValidator] ${context}: 期望数组但收到`, typeof data, data)
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 API 响应格式
|
||||
* @param {Object} response - API 响应对象
|
||||
* @param {string} context - 上下文信息
|
||||
* @returns {Object} 标准化的响应对象
|
||||
*/
|
||||
export const validateApiResponse = (response, context = 'API') => {
|
||||
if (!response) {
|
||||
console.warn(`[DataValidator] ${context}: 响应为空`)
|
||||
return { data: [], pagination: null }
|
||||
}
|
||||
|
||||
// 检查响应是否有 data 字段
|
||||
if (!response.hasOwnProperty('data')) {
|
||||
console.warn(`[DataValidator] ${context}: 响应缺少 data 字段`, response)
|
||||
return { data: [], pagination: null }
|
||||
}
|
||||
|
||||
return {
|
||||
data: response.data,
|
||||
pagination: response.pagination || null,
|
||||
message: response.message || '',
|
||||
code: response.code || 200
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证并处理列表数据
|
||||
* @param {Object} response - API 响应
|
||||
* @param {string} context - 上下文信息
|
||||
* @returns {Object} 包含验证后的数据和分页信息
|
||||
*/
|
||||
export const validateListResponse = (response, context = 'List API') => {
|
||||
const validatedResponse = validateApiResponse(response, context)
|
||||
|
||||
return {
|
||||
data: ensureArray(validatedResponse.data, context),
|
||||
pagination: validatedResponse.pagination,
|
||||
message: validatedResponse.message,
|
||||
code: validatedResponse.code
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证分页信息
|
||||
* @param {Object} pagination - 分页对象
|
||||
* @returns {Object} 标准化的分页对象
|
||||
*/
|
||||
export const validatePagination = (pagination) => {
|
||||
if (!pagination || typeof pagination !== 'object') {
|
||||
return {
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0,
|
||||
totalPages: 0
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
page: parseInt(pagination.page) || 1,
|
||||
limit: parseInt(pagination.limit) || 10,
|
||||
total: parseInt(pagination.total) || 0,
|
||||
totalPages: parseInt(pagination.totalPages) || 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全的数据访问器
|
||||
* @param {Object} obj - 对象
|
||||
* @param {string} path - 属性路径,如 'data.list'
|
||||
* @param {any} defaultValue - 默认值
|
||||
* @returns {any} 安全访问的值
|
||||
*/
|
||||
export const safeGet = (obj, path, defaultValue = null) => {
|
||||
try {
|
||||
const keys = path.split('.')
|
||||
let result = obj
|
||||
|
||||
for (const key of keys) {
|
||||
if (result === null || result === undefined) {
|
||||
return defaultValue
|
||||
}
|
||||
result = result[key]
|
||||
}
|
||||
|
||||
return result !== undefined ? result : defaultValue
|
||||
} catch (error) {
|
||||
console.warn(`[DataValidator] safeGet 访问路径 "${path}" 失败:`, error)
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
ensureArray,
|
||||
validateApiResponse,
|
||||
validateListResponse,
|
||||
validatePagination,
|
||||
safeGet
|
||||
}
|
||||
@@ -1,19 +1,22 @@
|
||||
import axios from 'axios'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import router from '@/router'
|
||||
|
||||
// 创建axios实例
|
||||
const request = axios.create({
|
||||
// API基础配置
|
||||
const API_CONFIG = {
|
||||
baseURL: 'http://localhost:3000/api',
|
||||
timeout: 10000
|
||||
})
|
||||
}
|
||||
|
||||
// 是否正在刷新token的标志
|
||||
let isRefreshing = false
|
||||
// 存储待重试的请求
|
||||
let failedQueue = []
|
||||
|
||||
// 请求缓存
|
||||
const requestCache = new Map()
|
||||
const CACHE_DURATION = 5 * 60 * 1000 // 5分钟缓存
|
||||
|
||||
// 处理队列中的请求
|
||||
const processQueue = (error, token = null) => {
|
||||
failedQueue.forEach(({ resolve, reject }) => {
|
||||
@@ -27,49 +30,134 @@ const processQueue = (error, token = null) => {
|
||||
failedQueue = []
|
||||
}
|
||||
|
||||
// 请求拦截器
|
||||
request.interceptors.request.use(
|
||||
async (config) => {
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 对于登录、刷新token和公开接口,跳过token检查
|
||||
const skipTokenCheck = config.url?.includes('/auth/login') ||
|
||||
config.url?.includes('/auth/refresh') ||
|
||||
config.url?.includes('/auth/register') ||
|
||||
config.url?.includes('/menus/public')
|
||||
|
||||
if (!skipTokenCheck) {
|
||||
try {
|
||||
// 确保token有效(自动刷新如果需要)
|
||||
const validToken = await userStore.ensureValidToken()
|
||||
|
||||
if (validToken) {
|
||||
config.headers.Authorization = `Bearer ${validToken}`
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取有效token失败:', error)
|
||||
// 如果无法获取有效token,继续发送请求,让响应拦截器处理
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
// 生成缓存键
|
||||
const generateCacheKey = (url, options) => {
|
||||
return `${url}_${JSON.stringify(options)}`
|
||||
}
|
||||
|
||||
// 响应拦截器
|
||||
request.interceptors.response.use(
|
||||
(response) => {
|
||||
return response
|
||||
},
|
||||
async (error) => {
|
||||
const userStore = useUserStore()
|
||||
const originalRequest = error.config
|
||||
// 检查缓存
|
||||
const checkCache = (cacheKey) => {
|
||||
const cached = requestCache.get(cacheKey)
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
|
||||
return cached.data
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 设置缓存
|
||||
const setCache = (cacheKey, data) => {
|
||||
requestCache.set(cacheKey, {
|
||||
data,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建请求头
|
||||
* @param {Object} customHeaders - 自定义请求头
|
||||
* @returns {Object} 请求头对象
|
||||
*/
|
||||
const createHeaders = (customHeaders = {}) => {
|
||||
const userStore = useUserStore()
|
||||
const token = userStore.accessToken || localStorage.getItem('accessToken')
|
||||
|
||||
const defaultHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache'
|
||||
}
|
||||
|
||||
if (token) {
|
||||
defaultHeaders['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
return { ...defaultHeaders, ...customHeaders }
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理fetch响应
|
||||
* @param {Response} response - fetch响应对象
|
||||
* @returns {Promise} 处理后的响应数据
|
||||
*/
|
||||
const handleResponse = async (response) => {
|
||||
let data
|
||||
|
||||
try {
|
||||
const contentType = response.headers.get('content-type')
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
data = await response.json()
|
||||
} else {
|
||||
data = await response.text()
|
||||
}
|
||||
} catch (error) {
|
||||
data = null
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const error = new Error(data?.message || `HTTP ${response.status}: ${response.statusText}`)
|
||||
error.response = {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
data: data
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
return { data, status: response.status, statusText: response.statusText }
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于fetch的请求方法
|
||||
* @param {string} url - 请求URL
|
||||
* @param {Object} options - 请求选项
|
||||
* @returns {Promise} 请求结果
|
||||
*/
|
||||
const fetchRequest = async (url, options = {}) => {
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 构建完整URL
|
||||
const fullUrl = url.startsWith('http') ? url : `${API_CONFIG.baseURL}${url}`
|
||||
|
||||
// 对于登录、刷新token接口,跳过token检查
|
||||
const skipTokenCheck = url.includes('/auth/login') ||
|
||||
url.includes('/auth/refresh') ||
|
||||
url.includes('/auth/register')
|
||||
|
||||
if (!skipTokenCheck) {
|
||||
try {
|
||||
// 确保token有效(自动刷新如果需要)
|
||||
const validToken = await userStore.ensureValidToken()
|
||||
|
||||
if (validToken) {
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
'Authorization': `Bearer ${validToken}`
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取有效token失败:', error)
|
||||
// 如果无法获取有效token,继续发送请求,让响应处理器处理
|
||||
}
|
||||
}
|
||||
|
||||
// 设置默认请求头
|
||||
options.headers = createHeaders(options.headers)
|
||||
|
||||
// 设置超时
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), API_CONFIG.timeout)
|
||||
options.signal = controller.signal
|
||||
|
||||
try {
|
||||
const response = await fetch(fullUrl, options)
|
||||
clearTimeout(timeoutId)
|
||||
return await handleResponse(response)
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
// 如果是401错误且不是刷新token的请求
|
||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||
// 处理401错误
|
||||
if (error.response?.status === 401) {
|
||||
const errorCode = error.response?.data?.code
|
||||
|
||||
// 如果是token过期错误
|
||||
@@ -79,14 +167,13 @@ request.interceptors.response.use(
|
||||
return new Promise((resolve, reject) => {
|
||||
failedQueue.push({ resolve, reject })
|
||||
}).then(token => {
|
||||
originalRequest.headers.Authorization = `Bearer ${token}`
|
||||
return request(originalRequest)
|
||||
options.headers['Authorization'] = `Bearer ${token}`
|
||||
return fetchRequest(url, options)
|
||||
}).catch(err => {
|
||||
return Promise.reject(err)
|
||||
})
|
||||
}
|
||||
|
||||
originalRequest._retry = true
|
||||
isRefreshing = true
|
||||
|
||||
try {
|
||||
@@ -97,8 +184,8 @@ request.interceptors.response.use(
|
||||
processQueue(null, newToken)
|
||||
|
||||
// 重试原始请求
|
||||
originalRequest.headers.Authorization = `Bearer ${newToken}`
|
||||
return request(originalRequest)
|
||||
options.headers['Authorization'] = `Bearer ${newToken}`
|
||||
return fetchRequest(url, options)
|
||||
} catch (refreshError) {
|
||||
// 刷新失败,处理队列并跳转到登录页
|
||||
processQueue(refreshError, null)
|
||||
@@ -131,18 +218,61 @@ request.interceptors.response.use(
|
||||
// 处理其他错误
|
||||
if (error.response?.data?.message) {
|
||||
message.error(error.response.data.message)
|
||||
} else if (error.message) {
|
||||
} else if (error.message && !error.message.includes('aborted')) {
|
||||
message.error(error.message)
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
throw error
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建类似axios的API接口
|
||||
*/
|
||||
const request = {
|
||||
get: (url, config = {}) => {
|
||||
return fetchRequest(url, {
|
||||
method: 'GET',
|
||||
...config
|
||||
})
|
||||
},
|
||||
|
||||
post: (url, data = null, config = {}) => {
|
||||
return fetchRequest(url, {
|
||||
method: 'POST',
|
||||
body: data ? JSON.stringify(data) : null,
|
||||
...config
|
||||
})
|
||||
},
|
||||
|
||||
put: (url, data = null, config = {}) => {
|
||||
return fetchRequest(url, {
|
||||
method: 'PUT',
|
||||
body: data ? JSON.stringify(data) : null,
|
||||
...config
|
||||
})
|
||||
},
|
||||
|
||||
delete: (url, config = {}) => {
|
||||
return fetchRequest(url, {
|
||||
method: 'DELETE',
|
||||
...config
|
||||
})
|
||||
},
|
||||
|
||||
patch: (url, data = null, config = {}) => {
|
||||
return fetchRequest(url, {
|
||||
method: 'PATCH',
|
||||
body: data ? JSON.stringify(data) : null,
|
||||
...config
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 自动重新登录功能
|
||||
export const autoRelogin = async (username, password) => {
|
||||
try {
|
||||
const response = await axios.post('http://localhost:3000/api/auth/login', {
|
||||
const response = await request.post('/auth/login', {
|
||||
username,
|
||||
password
|
||||
})
|
||||
@@ -194,6 +324,6 @@ export const setupTokenExpirationWarning = () => {
|
||||
}, 60000) // 每分钟检查一次
|
||||
}
|
||||
|
||||
// 导出默认的axios实例和别名
|
||||
// 导出默认的请求实例和别名
|
||||
export default request
|
||||
export const apiClient = request
|
||||
@@ -325,9 +325,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { applicationAPI, insuranceTypeAPI } from '@/utils/api'
|
||||
import { validateListResponse, validatePagination } from '@/utils/dataValidator'
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
@@ -468,11 +469,19 @@ const loadApplications = async () => {
|
||||
...searchForm
|
||||
}
|
||||
const response = await applicationAPI.getList(params)
|
||||
applications.value = response.data || []
|
||||
pagination.total = response.pagination?.total || 0
|
||||
|
||||
// 使用数据验证工具处理响应
|
||||
const validatedResponse = validateListResponse(response, '保险申请列表')
|
||||
applications.value = validatedResponse.data
|
||||
|
||||
// 设置分页信息
|
||||
const validatedPagination = validatePagination(validatedResponse.pagination)
|
||||
pagination.total = validatedPagination.total
|
||||
} catch (error) {
|
||||
console.error('加载申请数据失败:', error)
|
||||
message.error('加载申请数据失败')
|
||||
// 确保在错误情况下 applications 也是数组
|
||||
applications.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -481,10 +490,15 @@ const loadApplications = async () => {
|
||||
const loadInsuranceTypes = async () => {
|
||||
try {
|
||||
const response = await insuranceTypeAPI.getList()
|
||||
insuranceTypes.value = response.data || []
|
||||
|
||||
// 使用数据验证工具处理响应
|
||||
const validatedResponse = validateListResponse(response, '险种列表')
|
||||
insuranceTypes.value = validatedResponse.data
|
||||
} catch (error) {
|
||||
console.error('加载险种数据失败:', error)
|
||||
message.error('加载险种数据失败')
|
||||
// 确保在错误情况下 insuranceTypes 也是数组
|
||||
insuranceTypes.value = []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,11 +61,11 @@
|
||||
<a-col :span="12">
|
||||
<a-card title="保险申请趋势" :bordered="false">
|
||||
<div style="height: 300px">
|
||||
<!-- 这里可以放置ECharts图表 -->
|
||||
<div style="text-align: center; padding: 60px 0; color: #999">
|
||||
<bar-chart-outlined style="font-size: 48px" />
|
||||
<p>图表区域 - 申请趋势</p>
|
||||
</div>
|
||||
<v-chart
|
||||
:option="applicationTrendOption"
|
||||
style="height: 100%; width: 100%"
|
||||
:loading="chartLoading"
|
||||
/>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
@@ -73,11 +73,11 @@
|
||||
<a-col :span="12">
|
||||
<a-card title="保单状态分布" :bordered="false">
|
||||
<div style="height: 300px">
|
||||
<!-- 这里可以放置ECharts饼图 -->
|
||||
<div style="text-align: center; padding: 60px 0; color: #999">
|
||||
<pie-chart-outlined style="font-size: 48px" />
|
||||
<p>图表区域 - 状态分布</p>
|
||||
</div>
|
||||
<v-chart
|
||||
:option="policyDistributionOption"
|
||||
style="height: 100%; width: 100%"
|
||||
:loading="chartLoading"
|
||||
/>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
@@ -124,10 +124,34 @@ import {
|
||||
} from '@ant-design/icons-vue'
|
||||
import { dashboardAPI } from '@/utils/api'
|
||||
import { message } from 'ant-design-vue'
|
||||
import VChart from 'vue-echarts'
|
||||
import { use } from 'echarts/core'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
import { BarChart, PieChart } from 'echarts/charts'
|
||||
import {
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
GridComponent
|
||||
} from 'echarts/components'
|
||||
|
||||
// 注册必要的组件
|
||||
use([
|
||||
CanvasRenderer,
|
||||
BarChart,
|
||||
PieChart,
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
GridComponent
|
||||
])
|
||||
|
||||
const loading = ref(false)
|
||||
const chartLoading = ref(false)
|
||||
const stats = ref({})
|
||||
const recentActivities = ref([])
|
||||
const applicationTrendOption = ref({})
|
||||
const policyDistributionOption = ref({})
|
||||
|
||||
const getActivityColor = (type) => {
|
||||
const colors = {
|
||||
@@ -187,22 +211,22 @@ const loadDashboardData = async () => {
|
||||
const statsResponse = await dashboardAPI.getStats()
|
||||
if (statsResponse.status === 'success') {
|
||||
stats.value = {
|
||||
totalUsers: statsResponse.data.overview?.users || 0,
|
||||
totalApplications: statsResponse.data.overview?.applications || 0,
|
||||
totalPolicies: statsResponse.data.overview?.policies || 0,
|
||||
totalClaims: statsResponse.data.overview?.claims || 0
|
||||
totalUsers: statsResponse.data.totalUsers || 0,
|
||||
totalApplications: statsResponse.data.totalApplications || 0,
|
||||
totalPolicies: statsResponse.data.totalPolicies || 0,
|
||||
totalClaims: statsResponse.data.totalClaims || 0
|
||||
}
|
||||
}
|
||||
|
||||
// 获取最近活动(使用系统日志作为活动记录)
|
||||
// 获取最近活动
|
||||
const activitiesResponse = await dashboardAPI.getRecentActivities()
|
||||
if (activitiesResponse.status === 'success') {
|
||||
recentActivities.value = activitiesResponse.data.logs?.slice(0, 10).map(log => ({
|
||||
id: log.id,
|
||||
type: getLogType(log.message),
|
||||
title: getLogTitle(log.level, log.message),
|
||||
description: log.message,
|
||||
created_at: log.timestamp
|
||||
recentActivities.value = activitiesResponse.data.map(activity => ({
|
||||
id: activity.id,
|
||||
type: getLogType(activity.action),
|
||||
title: getLogTitle('info', activity.action),
|
||||
description: activity.action,
|
||||
created_at: activity.createdAt
|
||||
})) || []
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -247,11 +271,163 @@ const loadDashboardData = async () => {
|
||||
created_at: new Date(Date.now() - 10800000).toISOString()
|
||||
}
|
||||
]
|
||||
|
||||
// 加载图表数据
|
||||
await loadChartData()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载图表数据
|
||||
const loadChartData = async () => {
|
||||
chartLoading.value = true
|
||||
try {
|
||||
// 获取申请趋势数据
|
||||
const applicationTrendResponse = await dashboardAPI.getChartData({
|
||||
type: 'applications',
|
||||
period: '7d'
|
||||
})
|
||||
|
||||
if (applicationTrendResponse.status === 'success') {
|
||||
setupApplicationTrendChart(applicationTrendResponse.data)
|
||||
}
|
||||
|
||||
// 获取保单分布数据
|
||||
const policyDistributionResponse = await dashboardAPI.getChartData({
|
||||
type: 'policies',
|
||||
period: '30d'
|
||||
})
|
||||
|
||||
if (policyDistributionResponse.status === 'success') {
|
||||
setupPolicyDistributionChart(policyDistributionResponse.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载图表数据失败:', error)
|
||||
// 使用模拟数据
|
||||
setupApplicationTrendChart([
|
||||
{ date: '2024-01-01', count: 5 },
|
||||
{ date: '2024-01-02', count: 8 },
|
||||
{ date: '2024-01-03', count: 12 },
|
||||
{ date: '2024-01-04', count: 7 },
|
||||
{ date: '2024-01-05', count: 15 },
|
||||
{ date: '2024-01-06', count: 10 },
|
||||
{ date: '2024-01-07', count: 18 }
|
||||
])
|
||||
|
||||
setupPolicyDistributionChart([
|
||||
{ date: '2024-01-01', count: 3 },
|
||||
{ date: '2024-01-02', count: 5 },
|
||||
{ date: '2024-01-03', count: 8 },
|
||||
{ date: '2024-01-04', count: 4 },
|
||||
{ date: '2024-01-05', count: 12 }
|
||||
])
|
||||
} finally {
|
||||
chartLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 设置申请趋势图表
|
||||
const setupApplicationTrendChart = (data) => {
|
||||
const dates = data.map(item => item.date)
|
||||
const counts = data.map(item => item.count)
|
||||
|
||||
applicationTrendOption.value = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: dates,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#8c8c8c'
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#8c8c8c'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '申请数量',
|
||||
type: 'bar',
|
||||
data: counts,
|
||||
itemStyle: {
|
||||
color: '#1890ff'
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: '#40a9ff'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// 设置保单分布图表
|
||||
const setupPolicyDistributionChart = (data) => {
|
||||
const chartData = data.map(item => ({
|
||||
name: item.date,
|
||||
value: item.count
|
||||
}))
|
||||
|
||||
policyDistributionOption.value = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '保单数量',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '18',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: chartData,
|
||||
itemStyle: {
|
||||
color: function(params) {
|
||||
const colors = ['#1890ff', '#52c41a', '#faad14', '#f5222d', '#722ed1']
|
||||
return colors[params.dataIndex % colors.length]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadDashboardData()
|
||||
})
|
||||
|
||||
@@ -78,24 +78,29 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { ref, onMounted, onUnmounted, getCurrentInstance } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { dataWarehouseAPI } from '@/utils/api';
|
||||
import dayjsLib from 'dayjs';
|
||||
|
||||
// 安全获取dayjs实例
|
||||
const getSafeDayjs = () => {
|
||||
try {
|
||||
// 尝试使用Ant Design Vue提供的dayjs实例
|
||||
if (window.dayjs && typeof window.dayjs === 'function') {
|
||||
return window.dayjs;
|
||||
// 首先尝试使用导入的dayjs
|
||||
if (dayjsLib && typeof dayjsLib === 'function') {
|
||||
return dayjsLib;
|
||||
}
|
||||
|
||||
// 如果Ant Design Vue没有提供,尝试导入我们自己的dayjs
|
||||
// 但需要确保版本兼容性
|
||||
const dayjsModule = require('dayjs');
|
||||
if (dayjsModule && typeof dayjsModule === 'function') {
|
||||
return dayjsModule;
|
||||
// 尝试从Vue实例中获取全局配置的dayjs
|
||||
const instance = getCurrentInstance();
|
||||
if (instance && instance.appContext.config.globalProperties.$dayjs) {
|
||||
return instance.appContext.config.globalProperties.$dayjs;
|
||||
}
|
||||
|
||||
// 尝试使用window上的dayjs实例
|
||||
if (window.dayjs && typeof window.dayjs === 'function') {
|
||||
return window.dayjs;
|
||||
}
|
||||
|
||||
// 如果都失败,使用简单的兼容实现
|
||||
@@ -134,8 +139,6 @@ const createSimpleDayjs = () => {
|
||||
return simpleDayjs;
|
||||
};
|
||||
|
||||
const dayjs = getSafeDayjs();
|
||||
|
||||
// 数据状态
|
||||
const overview = ref({
|
||||
totalUsers: 0,
|
||||
@@ -148,9 +151,10 @@ const overview = ref({
|
||||
});
|
||||
|
||||
// 初始化日期范围为字符串格式
|
||||
const dayjsInstance = getSafeDayjs();
|
||||
const dateRange = ref([
|
||||
dayjs().subtract(7, 'day').format('YYYY-MM-DD'),
|
||||
dayjs().format('YYYY-MM-DD')
|
||||
dayjsInstance().subtract(7, 'day').format('YYYY-MM-DD'),
|
||||
dayjsInstance().format('YYYY-MM-DD')
|
||||
]);
|
||||
const loading = ref(false);
|
||||
|
||||
@@ -169,9 +173,10 @@ let claimChartInstance = null;
|
||||
// 获取概览数据
|
||||
const fetchOverview = async () => {
|
||||
try {
|
||||
const result = await dataWarehouseAPI.getOverview();
|
||||
if (result.status === 'success') {
|
||||
overview.value = result.data;
|
||||
const response = await dataWarehouseAPI.getOverview();
|
||||
console.log('概览数据响应:', response);
|
||||
if (response.data && response.data.success) {
|
||||
overview.value = response.data.data;
|
||||
} else {
|
||||
message.error('获取概览数据失败');
|
||||
}
|
||||
@@ -184,9 +189,10 @@ const fetchOverview = async () => {
|
||||
// 获取保险类型分布数据
|
||||
const fetchTypeDistribution = async () => {
|
||||
try {
|
||||
const result = await dataWarehouseAPI.getInsuranceTypeDistribution();
|
||||
if (result.status === 'success') {
|
||||
renderTypeDistributionChart(result.data);
|
||||
const response = await dataWarehouseAPI.getInsuranceTypeDistribution();
|
||||
console.log('保险类型分布响应:', response);
|
||||
if (response.data && response.data.success) {
|
||||
renderTypeDistributionChart(response.data.data);
|
||||
} else {
|
||||
message.error('获取保险类型分布数据失败');
|
||||
}
|
||||
@@ -199,9 +205,10 @@ const fetchTypeDistribution = async () => {
|
||||
// 获取申请状态分布数据
|
||||
const fetchStatusDistribution = async () => {
|
||||
try {
|
||||
const result = await dataWarehouseAPI.getApplicationStatusDistribution();
|
||||
if (result.status === 'success') {
|
||||
renderStatusDistributionChart(result.data);
|
||||
const response = await dataWarehouseAPI.getApplicationStatusDistribution();
|
||||
console.log('申请状态分布响应:', response);
|
||||
if (response.data && response.data.success) {
|
||||
renderStatusDistributionChart(response.data.data);
|
||||
} else {
|
||||
message.error('获取申请状态分布数据失败');
|
||||
}
|
||||
@@ -214,9 +221,10 @@ const fetchStatusDistribution = async () => {
|
||||
// 获取趋势数据
|
||||
const fetchTrendData = async () => {
|
||||
try {
|
||||
const result = await dataWarehouseAPI.getTrendData();
|
||||
if (result.status === 'success') {
|
||||
renderTrendChart(result.data);
|
||||
const response = await dataWarehouseAPI.getTrendData();
|
||||
console.log('趋势数据响应:', response);
|
||||
if (response.data && response.data.success) {
|
||||
renderTrendChart(response.data.data);
|
||||
} else {
|
||||
message.error('获取趋势数据失败');
|
||||
}
|
||||
@@ -229,15 +237,21 @@ const fetchTrendData = async () => {
|
||||
// 获取赔付统计数据
|
||||
const fetchClaimStats = async () => {
|
||||
try {
|
||||
const result = await dataWarehouseAPI.getClaimStats();
|
||||
if (result.status === 'success') {
|
||||
renderClaimStatsChart(result.data);
|
||||
const response = await dataWarehouseAPI.getClaimStats();
|
||||
console.log('赔付统计响应:', response);
|
||||
if (response && response.data && response.data.success) {
|
||||
renderClaimStatsChart(response.data.data || { statusDistribution: [], monthlyTrend: [] });
|
||||
} else {
|
||||
console.warn('赔付统计数据响应格式不正确:', response);
|
||||
message.error('获取赔付统计数据失败');
|
||||
// 使用空数据渲染图表,避免显示错误
|
||||
renderClaimStatsChart({ statusDistribution: [], monthlyTrend: [] });
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('获取赔付统计数据失败');
|
||||
console.error('获取赔付统计数据错误:', error);
|
||||
// 出错时也使用空数据渲染图表
|
||||
renderClaimStatsChart({ statusDistribution: [], monthlyTrend: [] });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -271,6 +285,8 @@ const renderTypeDistributionChart = (data) => {
|
||||
typeChartInstance = echarts.init(typeDistributionChart.value);
|
||||
}
|
||||
|
||||
console.log('保险类型分布数据:', data);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
@@ -279,7 +295,7 @@ const renderTypeDistributionChart = (data) => {
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
data: data.map(item => item.name)
|
||||
data: data.map(item => item.type)
|
||||
},
|
||||
series: [
|
||||
{
|
||||
@@ -287,7 +303,7 @@ const renderTypeDistributionChart = (data) => {
|
||||
type: 'pie',
|
||||
radius: '50%',
|
||||
center: ['50%', '50%'],
|
||||
data: data.map(item => ({ value: item.count, name: item.name })),
|
||||
data: data.map(item => ({ value: item.count, name: item.type })),
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
@@ -311,6 +327,8 @@ const renderStatusDistributionChart = (data) => {
|
||||
statusChartInstance = echarts.init(statusDistributionChart.value);
|
||||
}
|
||||
|
||||
console.log('申请状态分布数据:', data);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
@@ -319,7 +337,7 @@ const renderStatusDistributionChart = (data) => {
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
data: data.map(item => item.name)
|
||||
data: data.map(item => item.label || item.status)
|
||||
},
|
||||
series: [
|
||||
{
|
||||
@@ -327,7 +345,7 @@ const renderStatusDistributionChart = (data) => {
|
||||
type: 'pie',
|
||||
radius: '50%',
|
||||
center: ['50%', '50%'],
|
||||
data: data.map(item => ({ value: item.count, name: item.name })),
|
||||
data: data.map(item => ({ value: item.count, name: item.label || item.status })),
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
@@ -351,10 +369,35 @@ const renderTrendChart = (data) => {
|
||||
trendChartInstance = echarts.init(trendChart.value);
|
||||
}
|
||||
|
||||
const dates = data.map(item => item.date);
|
||||
const newApplications = data.map(item => item.newApplications);
|
||||
const newPolicies = data.map(item => item.newPolicies);
|
||||
const newClaims = data.map(item => item.newClaims);
|
||||
console.log('趋势数据:', data);
|
||||
|
||||
// 确保data是对象
|
||||
if (!data || typeof data !== 'object') {
|
||||
console.warn('趋势数据格式错误:', data);
|
||||
data = { applications: [], policies: [], claims: [] };
|
||||
}
|
||||
|
||||
// 处理数据结构,合并三个数组的数据
|
||||
const applications = data.applications || [];
|
||||
const policies = data.policies || [];
|
||||
const claims = data.claims || [];
|
||||
|
||||
// 获取所有日期并排序
|
||||
const allDates = [...new Set([
|
||||
...applications.map(item => item.date),
|
||||
...policies.map(item => item.date),
|
||||
...claims.map(item => item.date)
|
||||
])].sort();
|
||||
|
||||
// 为每个日期创建数据映射
|
||||
const applicationMap = new Map(applications.map(item => [item.date, item.count]));
|
||||
const policyMap = new Map(policies.map(item => [item.date, item.count]));
|
||||
const claimMap = new Map(claims.map(item => [item.date, item.count]));
|
||||
|
||||
const dates = allDates;
|
||||
const newApplications = allDates.map(date => applicationMap.get(date) || 0);
|
||||
const newPolicies = allDates.map(date => policyMap.get(date) || 0);
|
||||
const newClaims = allDates.map(date => claimMap.get(date) || 0);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
@@ -426,6 +469,17 @@ const renderClaimStatsChart = (data) => {
|
||||
claimChartInstance = echarts.init(claimStatsChart.value);
|
||||
}
|
||||
|
||||
console.log('赔付统计数据:', data);
|
||||
|
||||
// 确保data是对象且有statusDistribution属性
|
||||
if (!data || typeof data !== 'object') {
|
||||
console.warn('赔付统计数据格式错误:', data);
|
||||
data = { statusDistribution: [], monthlyTrend: [] };
|
||||
}
|
||||
|
||||
// 处理数据结构,使用statusDistribution数组
|
||||
const chartData = data.statusDistribution || [];
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
@@ -444,7 +498,7 @@ const renderClaimStatsChart = (data) => {
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: data.map(item => item.name),
|
||||
data: chartData.map(item => item.label || item.status),
|
||||
axisLabel: {
|
||||
rotate: 45
|
||||
}
|
||||
@@ -460,8 +514,8 @@ const renderClaimStatsChart = (data) => {
|
||||
{
|
||||
name: '赔付金额',
|
||||
type: 'bar',
|
||||
data: data.map(item => ({
|
||||
value: item.totalAmount,
|
||||
data: chartData.map(item => ({
|
||||
value: item.totalAmount || 0,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
|
||||
@@ -2,194 +2,182 @@
|
||||
<div class="system-settings">
|
||||
<a-page-header
|
||||
title="系统设置"
|
||||
sub-title="操作日志管理"
|
||||
sub-title="系统配置与权限管理"
|
||||
/>
|
||||
|
||||
<!-- 操作日志 -->
|
||||
<a-card title="操作日志">
|
||||
<!-- 统计卡片 -->
|
||||
<a-row :gutter="16" style="margin-bottom: 16px">
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic
|
||||
title="今日操作"
|
||||
:value="logStats.todayCount || 0"
|
||||
:value-style="{ color: '#3f8600' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic
|
||||
title="本周操作"
|
||||
:value="logStats.weekCount || 0"
|
||||
:value-style="{ color: '#1890ff' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic
|
||||
title="本月操作"
|
||||
:value="logStats.monthCount || 0"
|
||||
:value-style="{ color: '#722ed1' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic
|
||||
title="总操作数"
|
||||
:value="logStats.totalCount || 0"
|
||||
:value-style="{ color: '#cf1322' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<!-- 标签页 -->
|
||||
<a-tabs v-model:activeKey="activeTab" type="card">
|
||||
<!-- 权限设置标签页 -->
|
||||
<a-tab-pane key="permissions" tab="权限设置">
|
||||
<PermissionManagement />
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 搜索表单 -->
|
||||
<a-form layout="inline" style="margin-bottom: 16px">
|
||||
<a-form-item label="用户名">
|
||||
<a-input
|
||||
v-model:value="logFilters.username"
|
||||
placeholder="请输入用户名"
|
||||
style="width: 150px"
|
||||
@pressEnter="searchLogs"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="操作类型">
|
||||
<a-select
|
||||
v-model:value="logFilters.operation_type"
|
||||
placeholder="请选择操作类型"
|
||||
style="width: 120px"
|
||||
allowClear
|
||||
>
|
||||
<a-select-option value="CREATE">创建</a-select-option>
|
||||
<a-select-option value="UPDATE">更新</a-select-option>
|
||||
<a-select-option value="DELETE">删除</a-select-option>
|
||||
<a-select-option value="LOGIN">登录</a-select-option>
|
||||
<a-select-option value="LOGOUT">登出</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="操作模块">
|
||||
<a-select
|
||||
v-model:value="logFilters.operation_module"
|
||||
placeholder="请选择操作模块"
|
||||
style="width: 120px"
|
||||
allowClear
|
||||
>
|
||||
<a-select-option value="user">用户管理</a-select-option>
|
||||
<a-select-option value="insurance">保险管理</a-select-option>
|
||||
<a-select-option value="application">申请管理</a-select-option>
|
||||
<a-select-option value="policy">保单管理</a-select-option>
|
||||
<a-select-option value="claim">理赔管理</a-select-option>
|
||||
<a-select-option value="system">系统管理</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态">
|
||||
<a-select
|
||||
v-model:value="logFilters.status"
|
||||
placeholder="请选择状态"
|
||||
style="width: 100px"
|
||||
allowClear
|
||||
>
|
||||
<a-select-option value="success">成功</a-select-option>
|
||||
<a-select-option value="failed">失败</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="日期范围">
|
||||
<a-range-picker
|
||||
v-model:value="logFilters.dateRange"
|
||||
style="width: 240px"
|
||||
format="YYYY-MM-DD"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="searchLogs" :loading="logLoading">
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button style="margin-left: 8px" @click="resetLogFilters">
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<!-- 角色权限管理标签页 -->
|
||||
<a-tab-pane key="role-permissions" tab="角色权限管理">
|
||||
<RolePermissionManagement />
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 操作日志表格 -->
|
||||
<a-table
|
||||
:columns="logColumns"
|
||||
:data-source="logList"
|
||||
:loading="logLoading"
|
||||
:pagination="logPagination"
|
||||
@change="handleLogTableChange"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'operation_type'">
|
||||
<a-tag :color="getOperationTypeColor(record.operation_type)">
|
||||
{{ getOperationTypeText(record.operation_type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 'success' ? 'green' : 'red'">
|
||||
{{ record.status === 'success' ? '成功' : '失败' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'operation_time'">
|
||||
{{ formatDateTime(record.operation_time) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-button type="link" size="small" @click="viewLogDetail(record)">
|
||||
查看详情
|
||||
</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
<!-- 操作日志标签页 -->
|
||||
<a-tab-pane key="logs" tab="操作日志">
|
||||
<div>
|
||||
<!-- 统计卡片 -->
|
||||
<a-row :gutter="16" style="margin-bottom: 16px">
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic
|
||||
title="今日操作"
|
||||
:value="logStats.todayCount || 0"
|
||||
:value-style="{ color: '#3f8600' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic
|
||||
title="本周操作"
|
||||
:value="logStats.weekCount || 0"
|
||||
:value-style="{ color: '#1890ff' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic
|
||||
title="本月操作"
|
||||
:value="logStats.monthCount || 0"
|
||||
:value-style="{ color: '#722ed1' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic
|
||||
title="总操作数"
|
||||
:value="logStats.totalCount || 0"
|
||||
:value-style="{ color: '#cf1322' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 日志详情模态框 -->
|
||||
<a-modal
|
||||
v-model:open="logDetailVisible"
|
||||
title="操作日志详情"
|
||||
width="800px"
|
||||
:footer="null"
|
||||
>
|
||||
<a-descriptions v-if="selectedLog" bordered :column="2">
|
||||
<a-descriptions-item label="操作ID">{{ selectedLog.id }}</a-descriptions-item>
|
||||
<a-descriptions-item label="用户名">{{ selectedLog.username }}</a-descriptions-item>
|
||||
<a-descriptions-item label="操作类型">
|
||||
<a-tag :color="getOperationTypeColor(selectedLog.operation_type)">
|
||||
{{ getOperationTypeText(selectedLog.operation_type) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="操作模块">{{ selectedLog.operation_module }}</a-descriptions-item>
|
||||
<a-descriptions-item label="操作描述" :span="2">{{ selectedLog.operation_description }}</a-descriptions-item>
|
||||
<a-descriptions-item label="IP地址">{{ selectedLog.ip_address }}</a-descriptions-item>
|
||||
<a-descriptions-item label="用户代理" :span="2">{{ selectedLog.user_agent }}</a-descriptions-item>
|
||||
<a-descriptions-item label="操作时间">{{ formatDateTime(selectedLog.operation_time) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag :color="selectedLog.status === 'success' ? 'green' : 'red'">
|
||||
{{ selectedLog.status === 'success' ? '成功' : '失败' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item v-if="selectedLog.error_message" label="错误信息" :span="2">
|
||||
<a-typography-text type="danger">{{ selectedLog.error_message }}</a-typography-text>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item v-if="selectedLog.request_data" label="请求数据" :span="2">
|
||||
<pre style="background: #f5f5f5; padding: 8px; border-radius: 4px; max-height: 200px; overflow-y: auto;">{{ formatJSON(selectedLog.request_data) }}</pre>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item v-if="selectedLog.response_data" label="响应数据" :span="2">
|
||||
<pre style="background: #f5f5f5; padding: 8px; border-radius: 4px; max-height: 200px; overflow-y: auto;">{{ formatJSON(selectedLog.response_data) }}</pre>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-modal>
|
||||
<!-- 搜索表单 -->
|
||||
<a-card style="margin-bottom: 16px">
|
||||
<a-form layout="inline" :model="logFilters">
|
||||
<a-form-item label="用户名">
|
||||
<a-input
|
||||
v-model:value="logFilters.username"
|
||||
placeholder="请输入用户名"
|
||||
style="width: 150px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="操作类型">
|
||||
<a-select
|
||||
v-model:value="logFilters.action"
|
||||
placeholder="请选择操作类型"
|
||||
style="width: 150px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="login">登录</a-select-option>
|
||||
<a-select-option value="logout">登出</a-select-option>
|
||||
<a-select-option value="create">创建</a-select-option>
|
||||
<a-select-option value="update">更新</a-select-option>
|
||||
<a-select-option value="delete">删除</a-select-option>
|
||||
<a-select-option value="view">查看</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="时间范围">
|
||||
<a-range-picker
|
||||
v-model:value="logFilters.dateRange"
|
||||
style="width: 250px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="searchLogs">
|
||||
<SearchOutlined />
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button style="margin-left: 8px" @click="resetLogFilters">
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<!-- 操作日志表格 -->
|
||||
<a-card>
|
||||
<a-table
|
||||
:columns="logColumns"
|
||||
:data-source="logList"
|
||||
:loading="logLoading"
|
||||
:pagination="logPagination"
|
||||
@change="handleLogTableChange"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-tag :color="getActionColor(record.action)">
|
||||
{{ getActionText(record.action) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'operation'">
|
||||
<a-button type="link" @click="showLogDetail(record)">
|
||||
查看详情
|
||||
</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 日志详情模态框 -->
|
||||
<a-modal
|
||||
v-model:open="logDetailVisible"
|
||||
title="操作日志详情"
|
||||
:footer="null"
|
||||
width="800px"
|
||||
>
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="用户名">
|
||||
{{ selectedLog.username }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="操作类型">
|
||||
<a-tag :color="getActionColor(selectedLog.action)">
|
||||
{{ getActionText(selectedLog.action) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="操作时间">
|
||||
{{ dayjs(selectedLog.created_at).format('YYYY-MM-DD HH:mm:ss') }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="IP地址">
|
||||
{{ selectedLog.ip_address }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="用户代理" :span="2">
|
||||
{{ selectedLog.user_agent }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="操作描述" :span="2">
|
||||
{{ selectedLog.description }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="请求详情" :span="2">
|
||||
<pre style="white-space: pre-wrap; word-break: break-all;">{{ selectedLog.details }}</pre>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-modal>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { SearchOutlined } from '@ant-design/icons-vue'
|
||||
import { operationLogAPI } from '@/utils/api'
|
||||
import dayjs from 'dayjs'
|
||||
import PermissionManagement from '@/components/PermissionManagement.vue'
|
||||
import RolePermissionManagement from '@/components/RolePermissionManagement.vue'
|
||||
|
||||
// 标签页状态
|
||||
const activeTab = ref('permissions')
|
||||
|
||||
// 操作日志相关数据
|
||||
const logLoading = ref(false)
|
||||
@@ -203,10 +191,8 @@ const logStats = reactive({
|
||||
|
||||
const logFilters = reactive({
|
||||
username: '',
|
||||
operation_type: '',
|
||||
operation_module: '',
|
||||
status: '',
|
||||
dateRange: null
|
||||
action: undefined,
|
||||
dateRange: undefined
|
||||
})
|
||||
|
||||
const logPagination = reactive({
|
||||
@@ -215,71 +201,92 @@ const logPagination = reactive({
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条,共 ${total} 条`
|
||||
showTotal: (total) => `共 ${total} 条记录`
|
||||
})
|
||||
|
||||
const logDetailVisible = ref(false)
|
||||
const selectedLog = ref({})
|
||||
|
||||
// 表格列定义
|
||||
const logColumns = [
|
||||
{
|
||||
title: '操作ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
key: 'username',
|
||||
width: 100
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '操作类型',
|
||||
dataIndex: 'operation_type',
|
||||
key: 'operation_type',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '操作模块',
|
||||
dataIndex: 'operation_module',
|
||||
key: 'operation_module',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '操作描述',
|
||||
dataIndex: 'operation_description',
|
||||
key: 'operation_description',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: 'IP地址',
|
||||
dataIndex: 'ip_address',
|
||||
key: 'ip_address',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 80
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
title: '操作时间',
|
||||
dataIndex: 'operation_time',
|
||||
key: 'operation_time',
|
||||
width: 160
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
width: 180,
|
||||
customRender: ({ text }) => dayjs(text).format('YYYY-MM-DD HH:mm:ss')
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
key: 'operation',
|
||||
width: 100
|
||||
}
|
||||
]
|
||||
|
||||
// 日志详情
|
||||
const logDetailVisible = ref(false)
|
||||
const selectedLog = ref(null)
|
||||
// 获取操作类型颜色
|
||||
const getActionColor = (action) => {
|
||||
const colors = {
|
||||
login: 'green',
|
||||
logout: 'blue',
|
||||
create: 'cyan',
|
||||
update: 'orange',
|
||||
delete: 'red',
|
||||
view: 'purple'
|
||||
}
|
||||
return colors[action] || 'default'
|
||||
}
|
||||
|
||||
// 操作日志相关方法
|
||||
const searchLogs = async () => {
|
||||
// 获取操作类型文本
|
||||
const getActionText = (action) => {
|
||||
const texts = {
|
||||
login: '登录',
|
||||
logout: '登出',
|
||||
create: '创建',
|
||||
update: '更新',
|
||||
delete: '删除',
|
||||
view: '查看'
|
||||
}
|
||||
return texts[action] || action
|
||||
}
|
||||
|
||||
// 获取操作日志统计
|
||||
const getLogStats = async () => {
|
||||
try {
|
||||
const response = await operationLogAPI.getStats()
|
||||
if (response.code === 200) {
|
||||
Object.assign(logStats, response.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取日志统计失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取操作日志列表
|
||||
const getLogList = async () => {
|
||||
logLoading.value = true
|
||||
try {
|
||||
const params = {
|
||||
@@ -288,106 +295,57 @@ const searchLogs = async () => {
|
||||
...logFilters
|
||||
}
|
||||
|
||||
// 处理日期范围
|
||||
if (logFilters.dateRange && logFilters.dateRange.length === 2) {
|
||||
params.start_date = logFilters.dateRange[0].format('YYYY-MM-DD')
|
||||
params.end_date = logFilters.dateRange[1].format('YYYY-MM-DD')
|
||||
params.startDate = dayjs(logFilters.dateRange[0]).format('YYYY-MM-DD')
|
||||
params.endDate = dayjs(logFilters.dateRange[1]).format('YYYY-MM-DD')
|
||||
}
|
||||
|
||||
const response = await operationLogAPI.getList(params)
|
||||
|
||||
// 根据后端API响应结构调整数据处理
|
||||
const responseData = response.data || response
|
||||
logList.value = (responseData.logs || []).map(log => ({
|
||||
...log,
|
||||
key: log.id,
|
||||
username: log.User?.username || '未知用户'
|
||||
}))
|
||||
|
||||
logPagination.total = responseData.total || 0
|
||||
|
||||
// 加载统计数据
|
||||
await loadLogStats()
|
||||
if (response.code === 200) {
|
||||
logList.value = response.data.list
|
||||
logPagination.total = response.data.total
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载操作日志失败:', error)
|
||||
message.error('加载操作日志失败')
|
||||
message.error('获取操作日志失败')
|
||||
console.error('获取操作日志失败:', error)
|
||||
} finally {
|
||||
logLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadLogStats = async () => {
|
||||
try {
|
||||
const response = await operationLogAPI.getStats()
|
||||
const responseData = response.data || response
|
||||
Object.assign(logStats, responseData)
|
||||
} catch (error) {
|
||||
console.error('加载统计数据失败:', error)
|
||||
}
|
||||
// 搜索日志
|
||||
const searchLogs = () => {
|
||||
logPagination.current = 1
|
||||
getLogList()
|
||||
}
|
||||
|
||||
// 重置搜索条件
|
||||
const resetLogFilters = () => {
|
||||
Object.assign(logFilters, {
|
||||
username: '',
|
||||
operation_type: '',
|
||||
operation_module: '',
|
||||
status: '',
|
||||
dateRange: null
|
||||
action: undefined,
|
||||
dateRange: undefined
|
||||
})
|
||||
logPagination.current = 1
|
||||
searchLogs()
|
||||
}
|
||||
|
||||
// 表格变化处理
|
||||
const handleLogTableChange = (pagination) => {
|
||||
logPagination.current = pagination.current
|
||||
logPagination.pageSize = pagination.pageSize
|
||||
searchLogs()
|
||||
getLogList()
|
||||
}
|
||||
|
||||
const getOperationTypeColor = (type) => {
|
||||
const colors = {
|
||||
CREATE: 'green',
|
||||
UPDATE: 'blue',
|
||||
DELETE: 'red',
|
||||
LOGIN: 'cyan',
|
||||
LOGOUT: 'orange'
|
||||
}
|
||||
return colors[type] || 'default'
|
||||
}
|
||||
|
||||
const getOperationTypeText = (type) => {
|
||||
const texts = {
|
||||
CREATE: '创建',
|
||||
UPDATE: '更新',
|
||||
DELETE: '删除',
|
||||
LOGIN: '登录',
|
||||
LOGOUT: '登出'
|
||||
}
|
||||
return texts[type] || type
|
||||
}
|
||||
|
||||
const viewLogDetail = (log) => {
|
||||
selectedLog.value = log
|
||||
// 显示日志详情
|
||||
const showLogDetail = (record) => {
|
||||
selectedLog.value = record
|
||||
logDetailVisible.value = true
|
||||
}
|
||||
|
||||
const formatDateTime = (dateTime) => {
|
||||
return dayjs(dateTime).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
const formatJSON = (data) => {
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(data), null, 2)
|
||||
} catch {
|
||||
return data
|
||||
}
|
||||
}
|
||||
return JSON.stringify(data, null, 2)
|
||||
}
|
||||
|
||||
// 页面加载时获取数据
|
||||
onMounted(() => {
|
||||
searchLogs()
|
||||
getLogStats()
|
||||
getLogList()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -398,11 +356,17 @@ onMounted(() => {
|
||||
|
||||
.ant-statistic-content {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ant-descriptions-item-content pre {
|
||||
margin: 0;
|
||||
.ant-descriptions-item-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: #f5f5f5;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user