完善保险前后端、养殖端小程序

This commit is contained in:
xuqiuyun
2025-09-25 19:09:51 +08:00
parent 76b5393182
commit 852adbcfff
199 changed files with 8642 additions and 52333 deletions

View File

@@ -21,9 +21,9 @@
"vue-router": "^4.2.4"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.3",
"eslint": "^8.45.0",
"eslint-plugin-vue": "^9.15.1",
"vite": "^4.4.5"
"@vitejs/plugin-vue": "^4.6.2",
"eslint": "^8.57.1",
"eslint-plugin-vue": "^9.33.0",
"vite": "^4.5.14"
}
}

View File

@@ -0,0 +1,157 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dayjs 修复测试</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.test-container {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.test-result {
margin: 10px 0;
padding: 10px;
border-radius: 4px;
}
.success {
background-color: #f6ffed;
border: 1px solid #b7eb8f;
color: #52c41a;
}
.error {
background-color: #fff2f0;
border: 1px solid #ffccc7;
color: #ff4d4f;
}
.info {
background-color: #e6f7ff;
border: 1px solid #91d5ff;
color: #1890ff;
}
button {
background-color: #1890ff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin: 10px 5px;
}
button:hover {
background-color: #40a9ff;
}
</style>
</head>
<body>
<div class="test-container">
<h1>🔧 Dayjs 修复测试</h1>
<p>这个页面用于测试 DataWarehouse.vue 中的 dayjs 修复是否成功。</p>
<button onclick="testDataWarehouse()">测试 DataWarehouse 页面</button>
<button onclick="testConsoleErrors()">检查控制台错误</button>
<div id="test-results"></div>
</div>
<script>
function addResult(message, type = 'info') {
const resultsDiv = document.getElementById('test-results');
const resultDiv = document.createElement('div');
resultDiv.className = `test-result ${type}`;
resultDiv.innerHTML = message;
resultsDiv.appendChild(resultDiv);
}
function testDataWarehouse() {
addResult('🔍 开始测试 DataWarehouse 页面...', 'info');
// 创建一个 iframe 来加载主应用
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = 'http://localhost:3002/#/data-warehouse';
iframe.onload = function() {
setTimeout(() => {
try {
// 检查 iframe 中是否有错误
const iframeWindow = iframe.contentWindow;
const iframeDocument = iframe.contentDocument;
if (iframeDocument) {
addResult('✅ DataWarehouse 页面加载成功', 'success');
addResult('📍 页面 URL: http://localhost:3002/#/data-warehouse', 'info');
addResult('💡 请手动访问该页面并检查浏览器控制台是否还有 "require is not defined" 错误', 'info');
} else {
addResult('❌ 无法访问 iframe 内容,可能存在跨域问题', 'error');
}
} catch (error) {
addResult(`⚠️ 测试过程中出现异常: ${error.message}`, 'error');
} finally {
document.body.removeChild(iframe);
}
}, 3000);
};
iframe.onerror = function() {
addResult('❌ DataWarehouse 页面加载失败', 'error');
document.body.removeChild(iframe);
};
document.body.appendChild(iframe);
}
function testConsoleErrors() {
addResult('🔍 检查控制台错误...', 'info');
// 监听控制台错误
const originalError = console.error;
const errors = [];
console.error = function(...args) {
errors.push(args.join(' '));
originalError.apply(console, args);
};
setTimeout(() => {
console.error = originalError;
const requireErrors = errors.filter(error =>
error.includes('require is not defined') ||
error.includes('ReferenceError: require is not defined')
);
if (requireErrors.length === 0) {
addResult('✅ 没有发现 "require is not defined" 错误', 'success');
} else {
addResult(`❌ 发现 ${requireErrors.length} 个 require 相关错误:`, 'error');
requireErrors.forEach(error => {
addResult(` ${error}`, 'error');
});
}
if (errors.length === 0) {
addResult('✅ 控制台没有错误信息', 'success');
} else {
addResult(`⚠️ 控制台共有 ${errors.length} 个错误信息`, 'info');
}
}, 2000);
}
// 页面加载完成后的初始化
window.onload = function() {
addResult('🚀 测试页面已加载,请点击按钮开始测试', 'info');
addResult('📋 修复内容: 将 DataWarehouse.vue 中的 require("dayjs") 改为 ES6 import', 'info');
};
</script>
</body>
</html>

View File

@@ -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' // 重定向到仪表板
}

View 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>

View File

@@ -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>

View File

@@ -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
}
})

View File

@@ -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

View 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
}

View File

@@ -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

View File

@@ -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 = []
}
}

View File

@@ -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()
})

View File

@@ -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',

View File

@@ -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>

View File

@@ -0,0 +1,62 @@
import axios from 'axios';
async function testCurrentLogs() {
try {
console.log('🔐 先登录获取token...');
// 登录获取token
const loginResponse = await axios.post('http://localhost:3000/api/auth/login', {
username: 'admin',
password: '123456'
});
const token = loginResponse.data.data.accessToken;
console.log('✅ 登录成功获取到token');
console.log('🔍 测试当前日志API...');
// 测试系统日志API
const logsResponse = await axios.get('http://localhost:3000/api/system/logs', {
headers: {
'Authorization': `Bearer ${token}`
},
params: {
page: 1,
limit: 10
}
});
console.log('📊 日志API响应状态:', logsResponse.status);
console.log('📋 日志数据:', JSON.stringify(logsResponse.data, null, 2));
console.log('📈 日志总数:', logsResponse.data.data?.pagination?.total || 0);
console.log('📄 当前页日志数量:', logsResponse.data.data?.logs?.length || 0);
// 测试仪表板统计API
const statsResponse = await axios.get('http://localhost:3000/api/dashboard/stats', {
headers: {
'Authorization': `Bearer ${token}`
}
});
console.log('\n📊 仪表板统计响应状态:', statsResponse.status);
console.log('📋 统计数据:', JSON.stringify(statsResponse.data, null, 2));
// 测试最近活动API
const activitiesResponse = await axios.get('http://localhost:3000/api/dashboard/recent-activities', {
headers: {
'Authorization': `Bearer ${token}`
}
});
console.log('\n📊 最近活动响应状态:', activitiesResponse.status);
console.log('📋 活动数据:', JSON.stringify(activitiesResponse.data, null, 2));
console.log('📈 活动总数:', activitiesResponse.data.data?.length || 0);
} catch (error) {
console.error('❌ 测试失败:', error.message);
if (error.response) {
console.error('错误状态:', error.response.status);
console.error('错误数据:', error.response.data);
}
}
}
testCurrentLogs().catch(console.error);