From 9480b426dca1573f240a5c91c50791ffbdd748eb Mon Sep 17 00:00:00 2001 From: "Ray.Hao" <1490493387@qq.com> Date: Thu, 12 Feb 2026 21:01:48 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=88=86=E9=A1=B5=E5=93=8D?= =?UTF-8?q?=E5=BA=94=E6=95=B0=E6=8D=AE=E7=BB=93=E6=9E=84=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mock/ai.mock.ts | 120 ------ mock/dict.mock.ts | 82 ++-- mock/log.mock.ts | 308 ++++++++------- mock/notice.mock.ts | 366 +++++++++--------- mock/role.mock.ts | 190 +++++---- mock/user.mock.ts | 56 ++- src/api/system/dict.ts | 6 +- src/api/system/role.ts | 6 +- src/components/Breadcrumb/index.vue | 18 +- src/components/CURD/PageContent.vue | 72 ++-- src/components/CURD/types.ts | 3 +- src/components/CopyButton/index.vue | 29 +- src/components/TableSelect/index.vue | 6 +- src/components/Upload/FileUpload.vue | 13 +- src/components/Upload/MultiImageUpload.vue | 11 +- src/components/Upload/SingleImageUpload.vue | 11 +- src/components/WangEditor/index.vue | 4 +- src/enums/api.ts | 5 + src/layouts/components/LayoutToolbar.vue | 12 +- src/store/modules/permission.ts | 57 +++ src/store/modules/user.ts | 18 + src/types/api/common.ts | 19 +- src/types/api/role.ts | 4 +- src/utils/request.ts | 107 +++-- src/views/codegen/index.vue | 92 ++--- src/views/demo/curd-single.vue | 27 +- src/views/demo/curd/config/content.ts | 6 +- src/views/demo/curd/config/options.ts | 16 +- src/views/demo/curd/config2/content.ts | 8 +- src/views/demo/curd/index.vue | 11 +- src/views/demo/dict-sync.vue | 18 +- src/views/login/components/Login.vue | 39 +- src/views/profile/index.vue | 19 +- src/views/profile/notice/index.vue | 21 +- src/views/system/config/index.vue | 6 +- src/views/system/dict/dict-item.vue | 6 +- src/views/system/dict/index.vue | 6 +- src/views/system/log/index.vue | 6 +- src/views/system/notice/index.vue | 6 +- src/views/system/role/index.vue | 74 +++- src/views/system/tenant/index.vue | 38 +- src/views/system/tenant/plan.vue | 11 +- .../user/components/UserImportDialog.vue | 27 +- src/views/system/user/index.vue | 78 ++-- types/env.d.ts | 1 - 45 files changed, 1013 insertions(+), 1026 deletions(-) delete mode 100644 mock/ai.mock.ts diff --git a/mock/ai.mock.ts b/mock/ai.mock.ts deleted file mode 100644 index 8f54b83f..00000000 --- a/mock/ai.mock.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { defineMock } from "./base"; - -export default defineMock([ - { - url: "ai/assistant/parse", - method: ["POST"], - body: ({ body }) => { - return { - code: "00000", - data: { - parseLogId: "10001", - success: true, - functionCalls: [ - { - name: "navigate", - arguments: { - path: "/system/user", - }, - }, - ], - explanation: `Mock: 已解析命令:${body?.command ?? ""}`, - confidence: 0.92, - }, - msg: "一切ok", - }; - }, - }, - { - url: "ai/assistant/execute", - method: ["POST"], - body: { - code: "00000", - data: { - success: true, - message: "Mock: 执行成功", - }, - msg: "一切ok", - }, - }, - { - url: "ai/assistant/records", - method: ["GET"], - body: ({ query }) => { - const pageNum = Number(query?.pageNum ?? 1); - const pageSize = Number(query?.pageSize ?? 10); - const total = 2; - - return { - code: "00000", - data: [ - { - id: "10001", - userId: 1, - username: "admin", - originalCommand: "跳转到用户管理", - aiProvider: "qwen", - aiModel: "qwen-plus", - parseStatus: 1, - functionCalls: JSON.stringify( - [ - { - name: "navigate", - arguments: { path: "/system/user" }, - }, - ], - null, - 0 - ), - explanation: "Mock: 识别到跳转用户管理", - confidence: 0.92, - parseDurationMs: 128, - functionName: "navigate", - functionArguments: JSON.stringify({ path: "/system/user" }), - executeStatus: 1, - ipAddress: "127.0.0.1", - createTime: "2025-12-17 15:00:00", - updateTime: "2025-12-17 15:00:00", - }, - { - id: "10002", - userId: 1, - username: "admin", - originalCommand: "获取姓名为张三的用户信息", - aiProvider: "qwen", - aiModel: "qwen-plus", - parseStatus: 0, - functionCalls: "[]", - explanation: "Mock: 解析失败示例", - confidence: 0.2, - parseErrorMessage: "Mock: 无法匹配函数", - parseDurationMs: 256, - executeStatus: 0, - ipAddress: "127.0.0.1", - createTime: "2025-12-17 15:01:00", - updateTime: "2025-12-17 15:01:00", - }, - ].slice((pageNum - 1) * pageSize, pageNum * pageSize), - page: { - pageNum, - pageSize, - total, - }, - msg: "一切ok", - }; - }, - }, - { - url: "ai/assistant/records/:ids", - method: ["DELETE"], - body: ({ params }) => { - return { - code: "00000", - data: { - ids: params?.ids, - }, - msg: "一切ok", - }; - }, - }, -]); diff --git a/mock/dict.mock.ts b/mock/dict.mock.ts index 17ec1325..41b8f909 100644 --- a/mock/dict.mock.ts +++ b/mock/dict.mock.ts @@ -6,17 +6,15 @@ export default defineMock([ method: ["GET"], body: { code: "00000", - data: [ - { - id: 1, - name: "性别", - dictCode: "gender", - status: 1, - }, - ], - page: { - pageNum: 1, - pageSize: 10, + data: { + list: [ + { + id: 1, + name: "性别", + dictCode: "gender", + status: 1, + }, + ], total: 1, }, msg: "一切ok", @@ -103,38 +101,36 @@ export default defineMock([ method: ["GET"], body: { code: "00000", - data: [ - { - id: 1, - dictCode: "gender", - label: "男", - value: "1", - sort: 1, - status: 1, - tagType: "P", - }, - { - id: 2, - dictCode: "gender", - label: "女", - value: "2", - sort: 2, - status: 1, - tagType: "D", - }, - { - id: 3, - dictCode: "gender", - label: "保密", - value: "0", - sort: 3, - status: 1, - tagType: "I", - }, - ], - page: { - pageNum: 1, - pageSize: 10, + data: { + list: [ + { + id: 1, + dictCode: "gender", + label: "男", + value: "1", + sort: 1, + status: 1, + tagType: "P", + }, + { + id: 2, + dictCode: "gender", + label: "女", + value: "2", + sort: 2, + status: 1, + tagType: "D", + }, + { + id: 3, + dictCode: "gender", + label: "保密", + value: "0", + sort: 3, + status: 1, + tagType: "I", + }, + ], total: 3, }, msg: "一切ok", diff --git a/mock/log.mock.ts b/mock/log.mock.ts index 2583e8dd..5943b6e9 100644 --- a/mock/log.mock.ts +++ b/mock/log.mock.ts @@ -6,161 +6,159 @@ export default defineMock([ method: ["GET"], body: { code: "00000", - data: [ - { - id: 36192, - module: "菜单", - content: "菜单列表", - requestUri: "/api/v1/menus", - method: null, - ip: "183.156.148.241", - region: "浙江省 杭州市", - browser: "Chrome 109.0.0.0", - os: "OSX", - executionTime: 5, - createBy: null, - createTime: "2024-07-07 20:38:47", - operator: "系统管理员", - }, - { - id: 36190, - module: "字典", - content: "字典分页列表", - requestUri: "/api/v1/dicts", - method: null, - ip: "183.156.148.241", - region: "浙江省 杭州市", - browser: "Chrome 109.0.0.0", - os: "OSX", - executionTime: 9, - createBy: null, - createTime: "2024-07-07 20:38:45", - operator: "系统管理员", - }, - { - id: 36193, - module: "部门", - content: "部门列表", - requestUri: "/api/v1/depts", - method: null, - ip: "192.168.31.134", - region: "0 内网IP", - browser: "Chrome 125.0.0.0", - os: "Windows 10 or Windows Server 2016", - executionTime: 27, - createBy: null, - createTime: "2024-07-07 20:38:45", - operator: "系统管理员", - }, - { - id: 36191, - module: "菜单", - content: "菜单列表", - requestUri: "/api/v1/menus", - method: null, - ip: "192.168.31.134", - region: "0 内网IP", - browser: "Chrome 125.0.0.0", - os: "Windows 10 or Windows Server 2016", - executionTime: 39, - createBy: null, - createTime: "2024-07-07 20:38:44", - operator: "系统管理员", - }, - { - id: 36189, - module: "角色", - content: "角色分页列表", - requestUri: "/api/v1/roles", - method: null, - ip: "192.168.31.134", - region: "0 内网IP", - browser: "Chrome 125.0.0.0", - os: "Windows 10 or Windows Server 2016", - executionTime: 55, - createBy: null, - createTime: "2024-07-07 20:38:43", - operator: "系统管理员", - }, - { - id: 36188, - module: "用户", - content: "用户分页列表", - requestUri: "/api/v1/users", - method: null, - ip: "192.168.31.134", - region: "0 内网IP", - browser: "Chrome 125.0.0.0", - os: "Windows 10 or Windows Server 2016", - executionTime: 92, - createBy: null, - createTime: "2024-07-07 20:38:42", - operator: "系统管理员", - }, - { - id: 36187, - module: "登录", - content: "登录", - requestUri: "/api/v1/auth/login", - method: null, - ip: "192.168.31.134", - region: "0 内网IP", - browser: "Chrome 125.0.0.0", - os: "Windows 10 or Windows Server 2016", - executionTime: 19340, - createBy: null, - createTime: "2024-07-07 20:38:09", - operator: "系统管理员", - }, - { - id: 36186, - module: "登录", - content: "登录", - requestUri: "/api/v1/auth/login", - method: null, - ip: "192.168.31.134", - region: "0 内网IP", - browser: "Chrome 125.0.0.0", - os: "Windows 10 or Windows Server 2016", - executionTime: 19869, - createBy: null, - createTime: "2024-07-07 20:37:59", - operator: "系统管理员", - }, - { - id: 36185, - module: "登录", - content: "登录", - requestUri: "/api/v1/auth/login", - method: null, - ip: "112.103.111.59", - region: "黑龙江省 哈尔滨市", - browser: "Chrome 97.0.4692.98", - os: "Android", - executionTime: 96, - createBy: null, - createTime: "2024-07-07 20:37:21", - operator: "系统管理员", - }, - { - id: 36184, - module: "登录", - content: "登录", - requestUri: "/api/v1/auth/login", - method: null, - ip: "114.86.204.190", - region: "上海 上海市", - browser: "Chrome 125.0.0.0", - os: "Windows 10 or Windows Server 2016", - executionTime: 89, - createBy: null, - createTime: "2024-07-07 20:29:37", - operator: "系统管理员", - }, - ], - page: { - pageNum: 1, - pageSize: 10, + data: { + list: [ + { + id: 36192, + module: "菜单", + content: "菜单列表", + requestUri: "/api/v1/menus", + method: null, + ip: "183.156.148.241", + region: "浙江省 杭州市", + browser: "Chrome 109.0.0.0", + os: "OSX", + executionTime: 5, + createBy: null, + createTime: "2024-07-07 20:38:47", + operator: "系统管理员", + }, + { + id: 36190, + module: "字典", + content: "字典分页列表", + requestUri: "/api/v1/dicts", + method: null, + ip: "183.156.148.241", + region: "浙江省 杭州市", + browser: "Chrome 109.0.0.0", + os: "OSX", + executionTime: 9, + createBy: null, + createTime: "2024-07-07 20:38:45", + operator: "系统管理员", + }, + { + id: 36193, + module: "部门", + content: "部门列表", + requestUri: "/api/v1/depts", + method: null, + ip: "192.168.31.134", + region: "0 内网IP", + browser: "Chrome 125.0.0.0", + os: "Windows 10 or Windows Server 2016", + executionTime: 27, + createBy: null, + createTime: "2024-07-07 20:38:45", + operator: "系统管理员", + }, + { + id: 36191, + module: "菜单", + content: "菜单列表", + requestUri: "/api/v1/menus", + method: null, + ip: "192.168.31.134", + region: "0 内网IP", + browser: "Chrome 125.0.0.0", + os: "Windows 10 or Windows Server 2016", + executionTime: 39, + createBy: null, + createTime: "2024-07-07 20:38:44", + operator: "系统管理员", + }, + { + id: 36189, + module: "角色", + content: "角色分页列表", + requestUri: "/api/v1/roles", + method: null, + ip: "192.168.31.134", + region: "0 内网IP", + browser: "Chrome 125.0.0.0", + os: "Windows 10 or Windows Server 2016", + executionTime: 55, + createBy: null, + createTime: "2024-07-07 20:38:43", + operator: "系统管理员", + }, + { + id: 36188, + module: "用户", + content: "用户分页列表", + requestUri: "/api/v1/users", + method: null, + ip: "192.168.31.134", + region: "0 内网IP", + browser: "Chrome 125.0.0.0", + os: "Windows 10 or Windows Server 2016", + executionTime: 92, + createBy: null, + createTime: "2024-07-07 20:38:42", + operator: "系统管理员", + }, + { + id: 36187, + module: "登录", + content: "登录", + requestUri: "/api/v1/auth/login", + method: null, + ip: "192.168.31.134", + region: "0 内网IP", + browser: "Chrome 125.0.0.0", + os: "Windows 10 or Windows Server 2016", + executionTime: 19340, + createBy: null, + createTime: "2024-07-07 20:38:09", + operator: "系统管理员", + }, + { + id: 36186, + module: "登录", + content: "登录", + requestUri: "/api/v1/auth/login", + method: null, + ip: "192.168.31.134", + region: "0 内网IP", + browser: "Chrome 125.0.0.0", + os: "Windows 10 or Windows Server 2016", + executionTime: 19869, + createBy: null, + createTime: "2024-07-07 20:37:59", + operator: "系统管理员", + }, + { + id: 36185, + module: "登录", + content: "登录", + requestUri: "/api/v1/auth/login", + method: null, + ip: "112.103.111.59", + region: "黑龙江省 哈尔滨市", + browser: "Chrome 97.0.4692.98", + os: "Android", + executionTime: 96, + createBy: null, + createTime: "2024-07-07 20:37:21", + operator: "系统管理员", + }, + { + id: 36184, + module: "登录", + content: "登录", + requestUri: "/api/v1/auth/login", + method: null, + ip: "114.86.204.190", + region: "上海 上海市", + browser: "Chrome 125.0.0.0", + os: "Windows 10 or Windows Server 2016", + executionTime: 89, + createBy: null, + createTime: "2024-07-07 20:29:37", + operator: "系统管理员", + }, + ], total: 36188, }, msg: "一切ok", diff --git a/mock/notice.mock.ts b/mock/notice.mock.ts index 276c211e..43be43dd 100644 --- a/mock/notice.mock.ts +++ b/mock/notice.mock.ts @@ -6,141 +6,139 @@ export default defineMock([ method: ["GET"], body: { code: "00000", - data: [ - { - id: 1, - title: "v2.12.0 新增系统日志,访问趋势统计功能。", - publishStatus: 1, - type: 1, - publisherName: "系统管理员", - level: "L", - publishTime: "2024-09-30 17:21", - isRead: null, - targetType: 1, - createTime: "2024-09-28 11:21", - revokeTime: "2024-09-30 17:21", - }, - { - id: 2, - title: "v2.13.0 新增菜单搜索。", - publishStatus: 1, - type: 1, - publisherName: "系统管理员", - level: "L", - publishTime: "2024-09-30 17:22", - isRead: null, - targetType: 1, - createTime: "2024-09-28 11:21", - revokeTime: "2024-09-30 17:21", - }, - { - id: 3, - title: "\r\nv2.14.0 新增个人中心。", - publishStatus: 1, - type: 1, - publisherName: "系统管理员", - level: "L", - publishTime: "2024-09-30 17:23", - isRead: null, - targetType: 1, - createTime: "2024-09-28 11:21", - revokeTime: "2024-09-30 17:21", - }, - { - id: 4, - title: "v2.15.0 登录页面改造。", - publishStatus: 1, - type: 1, - publisherName: "系统管理员", - level: "L", - publishTime: "2024-09-30 17:24", - isRead: null, - targetType: 1, - createTime: "2024-09-28 11:21", - revokeTime: "2024-09-30 17:21", - }, - { - id: 5, - title: "v2.16.0 通知公告、字典翻译组件。", - publishStatus: 1, - type: 1, - publisherName: "系统管理员", - level: "L", - publishTime: "2024-09-30 17:25", - isRead: null, - targetType: 1, - createTime: "2024-09-28 11:21", - revokeTime: "2024-09-30 17:21", - }, - { - id: 6, - title: "系统将于本周六凌晨 2 点进行维护,预计维护时间为 2 小时。", - publishStatus: 1, - type: 2, - publisherName: "系统管理员", - level: "L", - publishTime: "2024-09-30 17:26", - isRead: null, - targetType: 1, - createTime: "2024-09-28 11:21", - revokeTime: "2024-09-30 17:21", - }, - { - id: 7, - title: "最近发现一些钓鱼邮件,请大家提高警惕,不要点击陌生链接。", - publishStatus: 1, - type: 3, - publisherName: "系统管理员", - level: "L", - publishTime: "2024-09-30 17:27", - isRead: null, - targetType: 1, - createTime: "2024-09-28 11:21", - revokeTime: "2024-09-30 17:21", - }, - { - id: 8, - title: "国庆假期从 10 月 1 日至 10 月 7 日放假,共 7 天。", - publishStatus: 1, - type: 4, - publisherName: "系统管理员", - level: "L", - publishTime: "2024-09-30 17:28", - isRead: null, - targetType: 1, - createTime: "2024-09-28 11:21", - revokeTime: "2024-09-30 17:21", - }, - { - id: 9, - title: "公司将在 10 月 15 日举办新产品发布会,敬请期待。", - publishStatus: 1, - type: 5, - publisherName: "系统管理员", - level: "L", - publishTime: "2024-09-30 17:29", - isRead: null, - targetType: 1, - createTime: "2024-09-28 11:21", - revokeTime: "2024-09-30 17:21", - }, - { - id: 10, - title: "v2.16.1 版本修复了 WebSocket 重复连接导致的后台线程阻塞问题,优化了通知公告。", - publishStatus: 1, - type: 1, - publisherName: "系统管理员", - level: "L", - publishTime: "2024-09-30 17:30", - isRead: null, - targetType: 1, - createTime: "2024-09-28 11:21", - revokeTime: "2024-09-30 17:21", - }, - ], - page: { - pageNum: 1, - pageSize: 10, + data: { + list: [ + { + id: 1, + title: "v2.12.0 新增系统日志,访问趋势统计功能。", + publishStatus: 1, + type: 1, + publisherName: "系统管理员", + level: "L", + publishTime: "2024-09-30 17:21", + isRead: null, + targetType: 1, + createTime: "2024-09-28 11:21", + revokeTime: "2024-09-30 17:21", + }, + { + id: 2, + title: "v2.13.0 新增菜单搜索。", + publishStatus: 1, + type: 1, + publisherName: "系统管理员", + level: "L", + publishTime: "2024-09-30 17:22", + isRead: null, + targetType: 1, + createTime: "2024-09-28 11:21", + revokeTime: "2024-09-30 17:21", + }, + { + id: 3, + title: "\r\nv2.14.0 新增个人中心。", + publishStatus: 1, + type: 1, + publisherName: "系统管理员", + level: "L", + publishTime: "2024-09-30 17:23", + isRead: null, + targetType: 1, + createTime: "2024-09-28 11:21", + revokeTime: "2024-09-30 17:21", + }, + { + id: 4, + title: "v2.15.0 登录页面改造。", + publishStatus: 1, + type: 1, + publisherName: "系统管理员", + level: "L", + publishTime: "2024-09-30 17:24", + isRead: null, + targetType: 1, + createTime: "2024-09-28 11:21", + revokeTime: "2024-09-30 17:21", + }, + { + id: 5, + title: "v2.16.0 通知公告、字典翻译组件。", + publishStatus: 1, + type: 1, + publisherName: "系统管理员", + level: "L", + publishTime: "2024-09-30 17:25", + isRead: null, + targetType: 1, + createTime: "2024-09-28 11:21", + revokeTime: "2024-09-30 17:21", + }, + { + id: 6, + title: "系统将于本周六凌晨 2 点进行维护,预计维护时间为 2 小时。", + publishStatus: 1, + type: 2, + publisherName: "系统管理员", + level: "L", + publishTime: "2024-09-30 17:26", + isRead: null, + targetType: 1, + createTime: "2024-09-28 11:21", + revokeTime: "2024-09-30 17:21", + }, + { + id: 7, + title: "最近发现一些钓鱼邮件,请大家提高警惕,不要点击陌生链接。", + publishStatus: 1, + type: 3, + publisherName: "系统管理员", + level: "L", + publishTime: "2024-09-30 17:27", + isRead: null, + targetType: 1, + createTime: "2024-09-28 11:21", + revokeTime: "2024-09-30 17:21", + }, + { + id: 8, + title: "国庆假期从 10 月 1 日至 10 月 7 日放假,共 7 天。", + publishStatus: 1, + type: 4, + publisherName: "系统管理员", + level: "L", + publishTime: "2024-09-30 17:28", + isRead: null, + targetType: 1, + createTime: "2024-09-28 11:21", + revokeTime: "2024-09-30 17:21", + }, + { + id: 9, + title: "公司将在 10 月 15 日举办新产品发布会,敬请期待。", + publishStatus: 1, + type: 5, + publisherName: "系统管理员", + level: "L", + publishTime: "2024-09-30 17:29", + isRead: null, + targetType: 1, + createTime: "2024-09-28 11:21", + revokeTime: "2024-09-30 17:21", + }, + { + id: 10, + title: "v2.16.1 版本修复了 WebSocket 重复连接导致的后台线程阻塞问题,优化了通知公告。", + publishStatus: 1, + type: 1, + publisherName: "系统管理员", + level: "L", + publishTime: "2024-09-30 17:30", + isRead: null, + targetType: 1, + createTime: "2024-09-28 11:21", + revokeTime: "2024-09-30 17:21", + }, + ], total: 10, }, msg: "一切ok", @@ -217,56 +215,54 @@ export default defineMock([ method: ["GET"], body: { code: "00000", - data: [ - { - id: 10, - title: "v2.16.1 版本修复了 WebSocket 重复连接导致的后台线程阻塞问题,优化了通知公告。", - type: 1, - level: "L", - publisherName: "系统管理员", - publishTime: "2024-09-30 17:30", - isRead: 0, - }, - { - id: 9, - title: "公司将在 10 月 15 日举办新产品发布会,敬请期待。", - type: 5, - level: "L", - publisherName: "系统管理员", - publishTime: "2024-09-30 17:29", - isRead: 0, - }, - { - id: 8, - title: "国庆假期从 10 月 1 日至 10 月 7 日放假,共 7 天。", - type: 4, - level: "L", - publisherName: "系统管理员", - publishTime: "2024-09-30 17:28", - isRead: 0, - }, - { - id: 7, - title: "最近发现一些钓鱼邮件,请大家提高警惕,不要点击陌生链接。", - type: 3, - level: "L", - publisherName: "系统管理员", - publishTime: "2024-09-30 17:27", - isRead: 0, - }, - { - id: 6, - title: "系统将于本周六凌晨 2 点进行维护,预计维护时间为 2 小时。", - type: 2, - level: "L", - publisherName: "系统管理员", - publishTime: "2024-09-30 17:26", - isRead: 0, - }, - ], - page: { - pageNum: 1, - pageSize: 10, + data: { + list: [ + { + id: 10, + title: "v2.16.1 版本修复了 WebSocket 重复连接导致的后台线程阻塞问题,优化了通知公告。", + type: 1, + level: "L", + publisherName: "系统管理员", + publishTime: "2024-09-30 17:30", + isRead: 0, + }, + { + id: 9, + title: "公司将在 10 月 15 日举办新产品发布会,敬请期待。", + type: 5, + level: "L", + publisherName: "系统管理员", + publishTime: "2024-09-30 17:29", + isRead: 0, + }, + { + id: 8, + title: "国庆假期从 10 月 1 日至 10 月 7 日放假,共 7 天。", + type: 4, + level: "L", + publisherName: "系统管理员", + publishTime: "2024-09-30 17:28", + isRead: 0, + }, + { + id: 7, + title: "最近发现一些钓鱼邮件,请大家提高警惕,不要点击陌生链接。", + type: 3, + level: "L", + publisherName: "系统管理员", + publishTime: "2024-09-30 17:27", + isRead: 0, + }, + { + id: 6, + title: "系统将于本周六凌晨 2 点进行维护,预计维护时间为 2 小时。", + type: 2, + level: "L", + publisherName: "系统管理员", + publishTime: "2024-09-30 17:26", + isRead: 0, + }, + ], total: 10, }, msg: "一切ok", diff --git a/mock/role.mock.ts b/mock/role.mock.ts index cad20852..42d1c356 100644 --- a/mock/role.mock.ts +++ b/mock/role.mock.ts @@ -61,101 +61,99 @@ export default defineMock([ method: ["GET"], body: { code: "00000", - data: [ - { - id: 2, - name: "系统管理员", - code: "ADMIN", - status: 1, - sort: 2, - createTime: "2021-03-25 12:39:54", - updateTime: null, - }, - { - id: 3, - name: "访问游客", - code: "GUEST", - status: 1, - sort: 3, - createTime: "2021-05-26 15:49:05", - updateTime: "2019-05-05 16:00:00", - }, - { - id: 4, - name: "系统管理员1", - code: "ADMIN1", - status: 1, - sort: 2, - createTime: "2021-03-25 12:39:54", - updateTime: null, - }, - { - id: 5, - name: "系统管理员2", - code: "ADMIN2", - status: 1, - sort: 2, - createTime: "2021-03-25 12:39:54", - updateTime: null, - }, - { - id: 6, - name: "系统管理员3", - code: "ADMIN3", - status: 1, - sort: 2, - createTime: "2021-03-25 12:39:54", - updateTime: null, - }, - { - id: 7, - name: "系统管理员4", - code: "ADMIN4", - status: 1, - sort: 2, - createTime: "2021-03-25 12:39:54", - updateTime: null, - }, - { - id: 8, - name: "系统管理员5", - code: "ADMIN5", - status: 1, - sort: 2, - createTime: "2021-03-25 12:39:54", - updateTime: null, - }, - { - id: 9, - name: "系统管理员6", - code: "ADMIN6", - status: 1, - sort: 2, - createTime: "2021-03-25 12:39:54", - updateTime: "2023-12-04 11:43:15", - }, - { - id: 10, - name: "系统管理员7", - code: "ADMIN7", - status: 1, - sort: 2, - createTime: "2021-03-25 12:39:54", - updateTime: null, - }, - { - id: 11, - name: "系统管理员8", - code: "ADMIN8", - status: 1, - sort: 2, - createTime: "2021-03-25 12:39:54", - updateTime: null, - }, - ], - page: { - pageNum: 1, - pageSize: 10, + data: { + list: [ + { + id: 2, + name: "系统管理员", + code: "ADMIN", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + { + id: 3, + name: "访问游客", + code: "GUEST", + status: 1, + sort: 3, + createTime: "2021-05-26 15:49:05", + updateTime: "2019-05-05 16:00:00", + }, + { + id: 4, + name: "系统管理员1", + code: "ADMIN1", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + { + id: 5, + name: "系统管理员2", + code: "ADMIN2", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + { + id: 6, + name: "系统管理员3", + code: "ADMIN3", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + { + id: 7, + name: "系统管理员4", + code: "ADMIN4", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + { + id: 8, + name: "系统管理员5", + code: "ADMIN5", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + { + id: 9, + name: "系统管理员6", + code: "ADMIN6", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: "2023-12-04 11:43:15", + }, + { + id: 10, + name: "系统管理员7", + code: "ADMIN7", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + { + id: 11, + name: "系统管理员8", + code: "ADMIN8", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + ], total: 10, }, msg: "一切ok", @@ -214,7 +212,7 @@ export default defineMock([ }, // 获取角色拥有的菜单ID { - url: "roles/:id/menuIds", + url: "roles/:id/menu-ids", method: ["GET"], body: () => { return { diff --git a/mock/user.mock.ts b/mock/user.mock.ts index ce95c1a6..baacfa6f 100644 --- a/mock/user.mock.ts +++ b/mock/user.mock.ts @@ -70,35 +70,33 @@ export default defineMock([ method: ["GET"], body: { code: "00000", - data: [ - { - id: 2, - username: "admin", - nickname: "系统管理员", - mobile: "17621210366", - gender: 1, - avatar: "https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif", - email: "", - status: 1, - deptId: 1, - roleIds: [2], - }, - { - id: 3, - username: "test", - nickname: "测试小用户", - mobile: "17621210366", - gender: 1, - avatar: "https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif", - email: "youlaitech@163.com", - status: 1, - deptId: 3, - roleIds: [3], - }, - ], - page: { - pageNum: 1, - pageSize: 10, + data: { + list: [ + { + id: 2, + username: "admin", + nickname: "系统管理员", + mobile: "17621210366", + gender: 1, + avatar: "https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif", + email: "", + status: 1, + deptId: 1, + roleIds: [2], + }, + { + id: 3, + username: "test", + nickname: "测试小用户", + mobile: "17621210366", + gender: 1, + avatar: "https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif", + email: "youlaitech@163.com", + status: 1, + deptId: 3, + roleIds: [3], + }, + ], total: 2, }, msg: "一切ok", diff --git a/src/api/system/dict.ts b/src/api/system/dict.ts index ae8803c4..9c4fc104 100644 --- a/src/api/system/dict.ts +++ b/src/api/system/dict.ts @@ -94,9 +94,9 @@ const DictAPI = { url: `${DICT_BASE_URL}/${dictCode}/items`, method: "get", params: queryParams, - }).then((res) => ({ - ...res, - data: (res.data ?? []).map((item) => ({ + }).then((data) => ({ + ...data, + list: (data.list ?? []).map((item) => ({ ...item, tagType: decodeDictTagType((item as any).tagType), })), diff --git a/src/api/system/role.ts b/src/api/system/role.ts index c9e86d6c..f0387d45 100644 --- a/src/api/system/role.ts +++ b/src/api/system/role.ts @@ -18,7 +18,7 @@ const RoleAPI = { }, /** 获取角色的菜单ID集合 */ getRoleMenuIds(roleId: string) { - return request({ url: `${ROLE_BASE_URL}/${roleId}/menuIds`, method: "get" }); + return request({ url: `${ROLE_BASE_URL}/${roleId}/menu-ids`, method: "get" }); }, /** 分配菜单权限 */ updateRoleMenus(roleId: string, data: number[]) { @@ -28,6 +28,10 @@ const RoleAPI = { getFormData(id: string) { return request({ url: `${ROLE_BASE_URL}/${id}/form`, method: "get" }); }, + /** 获取角色的部门ID集合(自定义数据权限) */ + getRoleDeptIds(roleId: string) { + return request({ url: `${ROLE_BASE_URL}/${roleId}/dept-ids`, method: "get" }); + }, /** 新增角色 */ create(data: RoleForm) { return request({ url: `${ROLE_BASE_URL}`, method: "post", data }); diff --git a/src/components/Breadcrumb/index.vue b/src/components/Breadcrumb/index.vue index d981d546..48bd4ec2 100644 --- a/src/components/Breadcrumb/index.vue +++ b/src/components/Breadcrumb/index.vue @@ -51,14 +51,20 @@ function isDashboard(route: RouteLocationMatched) { function handleLink(item: any) { const { redirect, path } = item; if (redirect) { - router.push(redirect).catch((err) => { - console.warn(err); - }); + router.push(redirect).then( + () => {}, + (err) => { + console.warn(err); + } + ); return; } - router.push(pathCompile(path)).catch((err) => { - console.warn(err); - }); + router.push(pathCompile(path)).then( + () => {}, + (err) => { + console.warn(err); + } + ); } watch( diff --git a/src/components/CURD/PageContent.vue b/src/components/CURD/PageContent.vue index 9db6c91e..b0b0a9c1 100644 --- a/src/components/CURD/PageContent.vue +++ b/src/components/CURD/PageContent.vue @@ -509,24 +509,29 @@ function handleDelete(id?: number | string) { confirmButtonText: "确定", cancelButtonText: "取消", type: "warning", - }) - .then(function () { + }).then( + function () { if (props.contentConfig.deleteAction) { - props.contentConfig - .deleteAction(ids) - .then(() => { + props.contentConfig.deleteAction(ids).then( + () => { ElMessage.success("删除成功"); removeIds.value = []; // 清空选中项 tableRef.value?.clearSelection(); handleRefresh(true); - }) - .catch(() => {}); + }, + () => { + // 交由全局错误处理 + } + ); } else { ElMessage.error("未配置deleteAction"); } - }) - .catch(() => {}); + }, + () => { + // 用户取消 + } + ); } // 导出表单 @@ -591,14 +596,14 @@ function handleExports() { worksheet.columns = columns; if (exportsFormData.origin === ExportsOriginEnum.REMOTE) { if (props.contentConfig.exportsAction) { - props.contentConfig.exportsAction(lastFormData).then((res) => { - worksheet.addRows(res); - workbook.xlsx - .writeBuffer() - .then((buffer) => { + props.contentConfig.exportsAction(lastFormData).then((data) => { + worksheet.addRows(data); + workbook.xlsx.writeBuffer().then( + (buffer) => { saveXlsx(buffer, filename as string); - }) - .catch((error) => console.log(error)); + }, + (error) => console.log(error) + ); }); } else { ElMessage.error("未配置exportsAction"); @@ -607,12 +612,12 @@ function handleExports() { worksheet.addRows( exportsFormData.origin === ExportsOriginEnum.SELECTED ? selectionData.value : pageData.value ); - workbook.xlsx - .writeBuffer() - .then((buffer) => { + workbook.xlsx.writeBuffer().then( + (buffer) => { saveXlsx(buffer, filename as string); - }) - .catch((error) => console.log(error)); + }, + (error) => console.log(error) + ); } } @@ -710,9 +715,8 @@ function handleImports() { if (ev.target !== null && ev.target.result !== null) { const result = ev.target.result as ArrayBuffer; // 从 buffer 中加载并解析数据 - workbook.xlsx - .load(result) - .then((workbook) => { + workbook.xlsx.load(result).then( + (workbook) => { // 解析后的数据 const data = []; // 获取第一个worksheet内容 @@ -745,14 +749,14 @@ function handleImports() { handleCloseImportModal(); handleRefresh(true); }); - }) - .catch((error) => console.log(error)); + }, + (error) => console.log(error) + ); } else { ElMessage.error("读取文件失败"); } }; } - // 操作人" function handleToolbar(name: string) { switch (name) { @@ -864,17 +868,21 @@ function fetchPageData(formData: IObject = {}, isRestart = false) { ? { [request.pageName]: pagination.currentPage, [request.limitName]: pagination.pageSize, + ...getFilterParams(), + ...formData, + } + : { + ...getFilterParams(), ...formData, } - : formData ) .then((data) => { if (showPagination) { - const pageResult = Array.isArray(data) ? { data, page: null } : data; - pagination.total = pageResult.page?.total ?? 0; - pageData.value = pageResult.data ?? []; + const pageResult = Array.isArray(data) ? { list: data, total: 0 } : data; + pagination.total = pageResult?.total ?? 0; + pageData.value = pageResult?.list ?? []; } else { - pageData.value = Array.isArray(data) ? data : (data.data ?? []); + pageData.value = Array.isArray(data) ? data : (data?.list ?? data?.data ?? []); } }) .finally(() => { diff --git a/src/components/CURD/types.ts b/src/components/CURD/types.ts index 152d89c8..3b3f7fbd 100644 --- a/src/components/CURD/types.ts +++ b/src/components/CURD/types.ts @@ -4,6 +4,7 @@ import type PageContent from "./PageContent.vue"; import type PageModal from "./PageModal.vue"; import type PageSearch from "./PageSearch.vue"; import type { CSSProperties } from "vue"; +import type { PageResult } from "@/types/api/common"; export type PageSearchInstance = InstanceType; export type PageContentInstance = InstanceType; @@ -78,7 +79,7 @@ export interface IContentConfig { pageName: string; limitName: string; }; - // 分页接口统一返回 PageResult { data, page } + // 分页接口统一返回 PageResult { list, total } // 修改属性的网络请求函数(需返回promise) modifyAction?: (data: { [key: string]: any; diff --git a/src/components/CopyButton/index.vue b/src/components/CopyButton/index.vue index 5179633b..1bd1bb8c 100644 --- a/src/components/CopyButton/index.vue +++ b/src/components/CopyButton/index.vue @@ -27,15 +27,15 @@ const props = defineProps({ function handleClipboard() { if (navigator.clipboard && navigator.clipboard.writeText) { // 使用 Clipboard API - navigator.clipboard - .writeText(props.text) - .then(() => { + navigator.clipboard.writeText(props.text).then( + () => { ElMessage.success("Copy successfully"); - }) - .catch((error) => { + }, + (error) => { ElMessage.warning("Copy failed"); console.log("[CopyButton] Copy failed", error); - }); + } + ); } else { // 兼容性处理(useClipboard 有兼容性问题) const input = document.createElement("input"); @@ -44,19 +44,18 @@ function handleClipboard() { input.setAttribute("value", props.text); document.body.appendChild(input); input.select(); + let successful = false; try { - const successful = document.execCommand("copy"); - if (successful) { - ElMessage.success("Copy successfully!"); - } else { - ElMessage.warning("Copy failed!"); - } - } catch (err) { - ElMessage.error("Copy failed."); - console.log("[CopyButton] Copy failed.", err); + successful = document.execCommand("copy"); } finally { document.body.removeChild(input); } + + if (successful) { + ElMessage.success("Copy successfully!"); + } else { + ElMessage.warning("Copy failed!"); + } } } diff --git a/src/components/TableSelect/index.vue b/src/components/TableSelect/index.vue index 0b314d59..6a6e5a71 100644 --- a/src/components/TableSelect/index.vue +++ b/src/components/TableSelect/index.vue @@ -261,9 +261,9 @@ function fetchPageData(isRestart = false) { } props.selectConfig .indexAction(queryParams) - .then((res) => { - total.value = res.page?.total ?? 0; - pageData.value = res.data ?? []; + .then((data) => { + total.value = data.total ?? 0; + pageData.value = data.list ?? []; }) .finally(() => { loading.value = false; diff --git a/src/components/Upload/FileUpload.vue b/src/components/Upload/FileUpload.vue index 903c7946..74925791 100644 --- a/src/components/Upload/FileUpload.vue +++ b/src/components/Upload/FileUpload.vue @@ -172,13 +172,14 @@ function handleUpload(options: UploadRequestOptions) { if (fileItem) { fileItem.percentage = percent; } - }) - .then((res) => { - resolve(res); - }) - .catch((err) => { + }).then( + (data) => { + resolve(data); + }, + (err) => { reject(err); - }); + } + ); }); } diff --git a/src/components/Upload/MultiImageUpload.vue b/src/components/Upload/MultiImageUpload.vue index 83d6591c..d63403cf 100644 --- a/src/components/Upload/MultiImageUpload.vue +++ b/src/components/Upload/MultiImageUpload.vue @@ -156,13 +156,14 @@ function handleUpload(options: UploadRequestOptions) { formData.append(key, props.data[key]); }); - FileAPI.upload(formData) - .then((data) => { + FileAPI.upload(formData).then( + (data) => { resolve(data); - }) - .catch((error) => { + }, + (error) => { reject(error); - }); + } + ); }); } diff --git a/src/components/Upload/SingleImageUpload.vue b/src/components/Upload/SingleImageUpload.vue index 49e9939a..e4e5942b 100644 --- a/src/components/Upload/SingleImageUpload.vue +++ b/src/components/Upload/SingleImageUpload.vue @@ -137,13 +137,14 @@ function handleUpload(options: UploadRequestOptions) { formData.append(key, props.data[key]); }); - FileAPI.upload(formData) - .then((data) => { + FileAPI.upload(formData).then( + (data) => { resolve(data); - }) - .catch((error) => { + }, + (error) => { reject(error); - }); + } + ); }); } diff --git a/src/components/WangEditor/index.vue b/src/components/WangEditor/index.vue index cea0258d..1e8d16b0 100644 --- a/src/components/WangEditor/index.vue +++ b/src/components/WangEditor/index.vue @@ -64,9 +64,9 @@ const editorConfig = ref>({ uploadImage: { customUpload(file: File, insertFn: InsertFnType) { // 上传图片 - FileAPI.uploadFile(file).then((res) => { + FileAPI.uploadFile(file).then((data) => { // 插入图片 - insertFn(res.url, res.name, res.url); + insertFn(data.url, data.name, data.url); }); }, } as any, diff --git a/src/enums/api.ts b/src/enums/api.ts index 1754ffab..951ba228 100644 --- a/src/enums/api.ts +++ b/src/enums/api.ts @@ -24,6 +24,11 @@ export const enum ApiCodeEnum { */ REFRESH_TOKEN_INVALID = "A0231", + /** + * 权限不足 + */ + PERMISSION_DENIED = "A0301", + /** * 需要选择租户 */ diff --git a/src/layouts/components/LayoutToolbar.vue b/src/layouts/components/LayoutToolbar.vue index 23454c8c..711a52ba 100644 --- a/src/layouts/components/LayoutToolbar.vue +++ b/src/layouts/components/LayoutToolbar.vue @@ -105,15 +105,15 @@ const showTenantSwitcher = computed(() => { }); function handleTenantChange(tenantId: number) { - tenantStore - .switchTenant(tenantId) - .then(() => { + tenantStore.switchTenant(tenantId).then( + () => { ElMessage.success("切换租户成功"); window.location.href = "/"; - }) - .catch((error: any) => { + }, + (error: any) => { ElMessage.error(error.message || "切换租户失败"); - }); + } + ); } /** diff --git a/src/store/modules/permission.ts b/src/store/modules/permission.ts index 2c584658..ccc325d0 100644 --- a/src/store/modules/permission.ts +++ b/src/store/modules/permission.ts @@ -2,6 +2,7 @@ import type { RouteRecordRaw } from "vue-router"; import { constantRoutes } from "@/router"; import { store } from "@/store"; import router from "@/router"; +import { useUserStoreHook } from "@/store/modules/user"; import MenuAPI from "@/api/system/menu"; import { RouteItem } from "@/types"; @@ -67,6 +68,60 @@ export const usePermissionStore = defineStore("permission", () => { isRouteGenerated.value = false; }; + let reloadPromise: Promise | null = null; + + /** + * 重新加载动态路由(单飞)。 + * + * 典型场景:后端权限变更导致接口返回权限不足(A0301),前端需要刷新路由和菜单以同步最新权限。 + * + * - 会先清理已注册的动态路由(resetRouter) + * - 重新从后端拉取路由(generateRoutes) + * - 将动态路由注册到 vue-router(router.addRoute) + */ + async function reloadDynamicRoutesOnce(): Promise { + if (reloadPromise) return reloadPromise; + + reloadPromise = (async () => { + try { + resetRouter(); + const dynamicRoutes = await generateRoutes(); + dynamicRoutes.forEach((route: RouteRecordRaw) => { + router.addRoute(route); + }); + return dynamicRoutes; + } finally { + reloadPromise = null; + } + })(); + + return reloadPromise; + } + + let snapshotPromise: Promise | null = null; + + /** + * 刷新权限快照(单飞)。 + * + * - 刷新用户信息(包含 perms/roles 等) + * - 重新加载动态路由 + */ + async function reloadPermissionSnapshotOnce(): Promise { + if (snapshotPromise) return snapshotPromise; + + snapshotPromise = (async () => { + try { + const userStore = useUserStoreHook(); + await userStore.getUserInfo(); + await reloadDynamicRoutesOnce(); + } finally { + snapshotPromise = null; + } + })(); + + return snapshotPromise; + } + return { routes, mixLayoutSideMenus, @@ -74,6 +129,8 @@ export const usePermissionStore = defineStore("permission", () => { generateRoutes, setMixLayoutSideMenus, resetRouter, + reloadDynamicRoutesOnce, + reloadPermissionSnapshotOnce, }; }); diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts index b91fa096..6e56edb4 100644 --- a/src/store/modules/user.ts +++ b/src/store/modules/user.ts @@ -38,6 +38,23 @@ export const useUserStore = defineStore("user", () => { }); } + let refreshPromise: Promise | null = null; + + /** + * 刷新 token(单飞)。 + * + * 多个并发请求遇到 token 过期时,共享同一次 refresh 请求。 + */ + function refreshTokenOnce() { + if (refreshPromise) return refreshPromise; + + refreshPromise = refreshToken().finally(() => { + refreshPromise = null; + }); + + return refreshPromise; + } + /** * 获取用户信息 * @@ -146,6 +163,7 @@ export const useUserStore = defineStore("user", () => { resetAllState, resetUserState, refreshToken, + refreshTokenOnce, }; }); diff --git a/src/types/api/common.ts b/src/types/api/common.ts index fff58239..12a8740f 100644 --- a/src/types/api/common.ts +++ b/src/types/api/common.ts @@ -10,9 +10,6 @@ export interface ApiResponse { data: T; /** 响应消息 */ msg: string; - - /** 分页信息(非列表接口通常不存在该字段) */ - page?: PageMeta | null; } /** 基础查询参数 */ @@ -29,20 +26,12 @@ export interface BaseQueryParams { order?: string; } -/** 分页元信息 */ -export interface PageMeta { - pageNum: number; - pageSize: number; - total: number; -} - -/** 列表响应结构(统一) */ +/** 分页数据结构(仅分页接口) */ export interface PageResult { /** 数据列表 */ - data: T[]; - - /** 分页信息,不分页时为 null */ - page: PageMeta | null; + list: T[]; + /** 总记录数 */ + total: number; } /** 下拉选项 */ diff --git a/src/types/api/role.ts b/src/types/api/role.ts index 7458b760..26ead5ee 100644 --- a/src/types/api/role.ts +++ b/src/types/api/role.ts @@ -38,8 +38,10 @@ export interface RoleForm { name?: string; /** 排序 */ sort?: number; - /** 数据权限 */ + /** 数据权限(1-所有数据 2-部门及子部门数据 3-本部门数据 4-本人数据 5-自定义部门数据) */ dataScope?: number; + /** 自定义数据权限部门ID列表(当dataScope=5时有效) */ + deptIds?: number[]; /** 角色状态 */ status?: number; /** 备注 */ diff --git a/src/utils/request.ts b/src/utils/request.ts index 58deb517..7fe7e974 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -2,6 +2,7 @@ import axios, { type InternalAxiosRequestConfig, type AxiosResponse } from "axio import qs from "qs"; import { ApiCodeEnum } from "@/enums/api"; import { useUserStoreHook } from "@/store/modules/user"; +import { usePermissionStoreHook } from "@/store/modules/permission"; import { AuthStorage, redirectToLogin } from "@/utils/auth"; // ============================================ @@ -49,13 +50,9 @@ http.interceptors.response.use( const { code, data, msg } = response.data; if (code === ApiCodeEnum.SUCCESS) { - // 分页接口需要同时返回 data 与 page 元信息 - const page = (response.data as any)?.page; - if (page != null) return { data, page }; return data; } - ElMessage.error(msg || "系统出错"); - return Promise.reject(new Error(msg || "Error")); + return rejectWithMessage(msg, "系统出错"); }, async (error) => { @@ -68,54 +65,78 @@ http.interceptors.response.use( const { code, msg } = response.data as ApiResponse; - // Token 过期处理 + // Token 过期:尝试刷新 token 后自动重试一次 if (code === ApiCodeEnum.ACCESS_TOKEN_INVALID) { return retryWithRefresh(config); } + // Refresh token 失效:无法续期,跳转登录 if (code === ApiCodeEnum.REFRESH_TOKEN_INVALID) { await redirectToLogin("登录已过期,请重新登录"); return Promise.reject(new Error(msg || "Token Invalid")); } - ElMessage.error(msg || "请求失败"); - return Promise.reject(new Error(msg || "Error")); + // 权限不足:刷新权限快照(用户信息 + 动态路由)后提示 + if (code === ApiCodeEnum.PERMISSION_DENIED) { + return handlePermissionDenied(msg); + } + + return rejectWithMessage(msg, "请求失败"); } ); -export default http; - -// ============================================ -// Token 刷新重试 -// ============================================ - -type Pending = { resolve: (v: unknown) => void; reject: (e: Error) => void }; - -let refreshing = false; -const queue: Pending[] = []; - -async function retryWithRefresh(config: InternalAxiosRequestConfig): Promise { - return new Promise((resolve, reject) => { - queue.push({ resolve, reject }); - - if (refreshing) return; - refreshing = true; - - useUserStoreHook() - .refreshToken() - .then(() => { - const token = AuthStorage.getAccessToken(); - if (token) config.headers.Authorization = `Bearer ${token}`; - - queue.forEach(({ resolve }) => http(config).then(resolve).catch(reject)); - }) - .catch(async () => { - queue.forEach(({ reject }) => reject(new Error("Token refresh failed"))); - await redirectToLogin("登录已过期,请重新登录"); - }) - .finally(() => { - queue.length = 0; - refreshing = false; - }); - }); +/** + * 权限不足处理:刷新权限快照(用户信息 + 动态路由),并给出提示 + * + * 刷新完成后仍然按失败处理,交由调用方的错误流处理 + */ +async function handlePermissionDenied(msg?: string): Promise { + const permissionStore = usePermissionStoreHook(); + await permissionStore.reloadPermissionSnapshotOnce(); + return rejectWithMessage(msg, "权限不足"); } + +/** + * access token 过期后的自动续期与重试 + * + * - 刷新 token 走单飞(userStore.refreshTokenOnce) + * - 当前请求最多重试一次(__isTokenRetry 标记) + */ +async function retryWithRefresh(config: InternalAxiosRequestConfig): Promise { + const retryConfig = config as InternalAxiosRequestConfig & { __isTokenRetry?: boolean }; + if (retryConfig.__isTokenRetry) { + await redirectToLogin("登录已过期,请重新登录"); + return Promise.reject(new Error("Token Invalid")); + } + retryConfig.__isTokenRetry = true; + + try { + const userStore = useUserStoreHook(); + await userStore.refreshTokenOnce(); + + const token = AuthStorage.getAccessToken(); + if (token) { + retryConfig.headers = retryConfig.headers || ({} as any); + (retryConfig.headers as any).Authorization = `Bearer ${token}`; + } + + return http(retryConfig); + } catch { + await redirectToLogin("登录已过期,请重新登录"); + return Promise.reject(new Error("Token refresh failed")); + } +} + +/** + * 统一处理业务错误提示并拒绝 Promise + * + * @param msg 错误消息内容 + * @param fallback 默认兜底消息 + */ +function rejectWithMessage(msg: string | undefined, fallback: string): Promise { + const message = msg || fallback; + ElMessage.error(message); + return Promise.reject(new Error(message)); +} + +export default http; diff --git a/src/views/codegen/index.vue b/src/views/codegen/index.vue index d3084e1b..a302e15f 100644 --- a/src/views/codegen/index.vue +++ b/src/views/codegen/index.vue @@ -823,9 +823,9 @@ function handleNextClick() { function handleQuery() { loading.value = true; GeneratorAPI.getTablePage(queryParams) - .then((res) => { - pageData.value = res.data; - total.value = res.page?.total ?? 0; + .then((data) => { + pageData.value = data.list; + total.value = data.total ?? 0; }) .finally(() => { loading.value = false; @@ -1071,18 +1071,16 @@ async function writeFile(dirHandle: any, filePath: string, content: string) { const folderSegments = parts; const targetDir = await ensureDir(dirHandle, folderSegments, true); // @ts-ignore - let fileHandle; - try { - // @ts-ignore - fileHandle = await targetDir.getFileHandle(fileName, { create: true }); - } catch (err: any) { - if (err?.name === "TypeMismatchError") { - // 存在同名目录(或其它类型冲突),为安全起见不自动删除 - throw err; - } else { + const fileHandle = await targetDir.getFileHandle(fileName, { create: true }).then( + (handle: any) => handle, + (err: any) => { + if (err?.name === "TypeMismatchError") { + // 存在同名目录(或其它类型冲突),为安全起见不自动删除 + throw err; + } throw err; } - } + ); // @ts-ignore const writable = await fileHandle.createWritable(); await writable.write(content ?? ""); @@ -1190,41 +1188,43 @@ const writeGeneratedCode = async () => { while (queue.length) { const item = queue.shift()!; try { - const root = resolveRootForItem(item); - const relativePath = stripProjectRoot(`${item.path}/${item.fileName}`); - writeProgress.current = relativePath; - if (overwriteMode.value === "ifChanged") { - // 简单差异:已有文件内容与待写内容相同则跳过 - // @ts-ignore - const targetRoot = root === "frontend" ? frontendDirHandle.value : backendDirHandle.value; - const existsSame = await isSameFile(targetRoot, relativePath, item.content || ""); - if (existsSame) { - // 视作成功但不处理 - writeProgress.done++; - writeProgress.percent = Math.round((writeProgress.done / writeProgress.total) * 100); - continue; + await (async () => { + const root = resolveRootForItem(item); + const relativePath = stripProjectRoot(`${item.path}/${item.fileName}`); + writeProgress.current = relativePath; + if (overwriteMode.value === "ifChanged") { + // 简单差异:已有文件内容与待写内容相同则跳过 + // @ts-ignore + const targetRoot = + root === "frontend" ? frontendDirHandle.value : backendDirHandle.value; + const existsSame = await isSameFile(targetRoot, relativePath, item.content || ""); + if (existsSame) { + return; + } } - } - if (overwriteMode.value === "skip") { - // @ts-ignore - const targetRoot = root === "frontend" ? frontendDirHandle.value : backendDirHandle.value; - const exists = await pathExists(targetRoot, relativePath); - if (exists) { - writeProgress.done++; - writeProgress.percent = Math.round((writeProgress.done / writeProgress.total) * 100); - continue; + if (overwriteMode.value === "skip") { + // @ts-ignore + const targetRoot = + root === "frontend" ? frontendDirHandle.value : backendDirHandle.value; + const exists = await pathExists(targetRoot, relativePath); + if (exists) { + return; + } } - } - if (root === "frontend") { - await writeFile(frontendDirHandle.value, relativePath, item.content || ""); - frontCount++; - } else { - await writeFile(backendDirHandle.value, relativePath, item.content || ""); - backCount++; - } - } catch (err) { - console.error("写入失败:", item.path, err); - failed.push(item.path); + if (root === "frontend") { + await writeFile(frontendDirHandle.value, relativePath, item.content || ""); + frontCount++; + } else { + await writeFile(backendDirHandle.value, relativePath, item.content || ""); + backCount++; + } + })().then( + () => {}, + (err) => { + console.error("写入失败:", item.path, err); + failed.push(item.path); + } + ); } finally { writeProgress.done++; writeProgress.percent = Math.round((writeProgress.done / writeProgress.total) * 100); diff --git a/src/views/demo/curd-single.vue b/src/views/demo/curd-single.vue index d0916850..caf4ddf7 100644 --- a/src/views/demo/curd-single.vue +++ b/src/views/demo/curd-single.vue @@ -99,13 +99,9 @@ const stateArr = ref([ // 初始化选项数据 const initOptions = async () => { - try { - const [dept, roles] = await Promise.all([DeptAPI.getOptions(), RoleAPI.getOptions()]); - deptArr.value = dept; - roleArr.value = roles; - } catch (error) { - console.error("初始化选项失败:", error); - } + const [dept, roles] = await Promise.all([DeptAPI.getOptions(), RoleAPI.getOptions()]); + deptArr.value = dept; + roleArr.value = roles; }; // ========================= 搜索配置 ========================= @@ -191,9 +187,9 @@ const contentConfig: IContentConfig = reactive({ return Promise.resolve(); }, async exportsAction(params: any) { - const res = await UserAPI.getPage(params); - console.log("exportsAction", res.data); - return res.data; + const data = await UserAPI.getPage(params); + console.log("exportsAction", data.list); + return data.list; }, pk: "id", toolbar: [ @@ -560,8 +556,8 @@ const handleOperateClick = (data: IObject) => { ElMessageBox.prompt("请输入用户名" + data.row.username + "」的新密码", "重置密码", { confirmButtonText: "确定", cancelButtonText: "取消", - }) - .then(({ value }: any) => { + }).then( + ({ value }: any) => { if (!value || value.length < 6) { ElMessage.warning("密码至少需6位字符,请重新输入"); return false; @@ -569,8 +565,11 @@ const handleOperateClick = (data: IObject) => { UserAPI.resetPassword(data.row.id, value).then(() => { ElMessage.success("密码重置成功,新密码是:" + value); }); - }) - .catch(() => {}); + }, + () => { + // 用户取消 + } + ); } }; diff --git a/src/views/demo/curd/config/content.ts b/src/views/demo/curd/config/content.ts index 5e453d34..e5d8b05e 100644 --- a/src/views/demo/curd/config/content.ts +++ b/src/views/demo/curd/config/content.ts @@ -31,9 +31,9 @@ const contentConfig: IContentConfig = { }, async exportsAction(params) { // 模拟获取到的是全量数据 - const res = await UserAPI.getPage(params); - console.log("exportsAction", res.data); - return res.data; + const data = await UserAPI.getPage(params); + console.log("exportsAction", data.list); + return data.list; }, pk: "id", toolbar: [ diff --git a/src/views/demo/curd/config/options.ts b/src/views/demo/curd/config/options.ts index 63e22402..0e0baa8b 100644 --- a/src/views/demo/curd/config/options.ts +++ b/src/views/demo/curd/config/options.ts @@ -18,14 +18,10 @@ export const stateArr = ref([ // 初始化逻辑,在 onMounted 钩子中调用 export const initOptions = async () => { - try { - // 使用Promise.all并行请求 - const [dept, roles] = await Promise.all([DeptAPI.getOptions(), RoleAPI.getOptions()]); - // 获取部门选项并赋值 - deptArr.value = dept; - // 获取角色选项并赋值 - roleArr.value = roles; - } catch (error) { - console.error("初始化选项失败:", error); - } + // 使用Promise.all并行请求 + const [dept, roles] = await Promise.all([DeptAPI.getOptions(), RoleAPI.getOptions()]); + // 获取部门选项并赋值 + deptArr.value = dept; + // 获取角色选项并赋值 + roleArr.value = roles; }; diff --git a/src/views/demo/curd/config2/content.ts b/src/views/demo/curd/config2/content.ts index 1ed0cd13..72d6f5f5 100644 --- a/src/views/demo/curd/config2/content.ts +++ b/src/views/demo/curd/config2/content.ts @@ -68,12 +68,8 @@ const contentConfig: IContentConfig = { const end = start + pageSize; return Promise.resolve({ - data: list.slice(start, end), - page: { - pageNum, - pageSize, - total: list.length, - }, + list: list.slice(start, end), + total: list.length, }); }, modifyAction(data) { diff --git a/src/views/demo/curd/index.vue b/src/views/demo/curd/index.vue index dfc99e56..16a760f4 100644 --- a/src/views/demo/curd/index.vue +++ b/src/views/demo/curd/index.vue @@ -169,8 +169,8 @@ const handleOperateClick = (data: IObject) => { ElMessageBox.prompt("请输入用户名" + data.row.username + "」的新密码", "重置密码", { confirmButtonText: "确定", cancelButtonText: "取消", - }) - .then(({ value }) => { + }).then( + ({ value }) => { if (!value || value.length < 6) { ElMessage.warning("密码至少需6位字符,请重新输入"); return false; @@ -178,8 +178,11 @@ const handleOperateClick = (data: IObject) => { UserAPI.resetPassword(data.row.id, value).then(() => { ElMessage.success("密码重置成功,新密码是:" + value); }); - }) - .catch(() => {}); + }, + () => { + // 用户取消 + } + ); } }; const handleOperateClick2 = (data: IOperateData) => { diff --git a/src/views/demo/dict-sync.vue b/src/views/demo/dict-sync.vue index dbe5723d..41213f3a 100644 --- a/src/views/demo/dict-sync.vue +++ b/src/views/demo/dict-sync.vue @@ -218,20 +218,14 @@ const saveDict = async () => { if (!dictForm.value) return; saving.value = true; - try { - // dictForm的类型已经是DictItemForm,直接传递 - await DictAPI.updateDictItem(DICT_CODE, MALE_ITEM_ID, dictForm.value); + // dictForm的类型已经是DictItemForm,直接传递 + await DictAPI.updateDictItem(DICT_CODE, MALE_ITEM_ID, dictForm.value); - // 更新时间 - lastUpdateTime.value = useDateFormat(new Date(), "YYYY-MM-DD HH:mm:ss").value; + // 更新时间 + lastUpdateTime.value = useDateFormat(new Date(), "YYYY-MM-DD HH:mm:ss").value; - ElMessage.success("保存成功,后端将通过WebSocket通知所有客户端"); - } catch (error) { - console.error("保存字典项失败", error); - ElMessage.error("保存失败"); - } finally { - saving.value = false; - } + ElMessage.success("保存成功,后端将通过WebSocket通知所有客户端"); + saving.value = false; }; // 组件挂载时加载性别字典 diff --git a/src/views/login/components/Login.vue b/src/views/login/components/Login.vue index 44c5f817..8405a025 100644 --- a/src/views/login/components/Login.vue +++ b/src/views/login/components/Login.vue @@ -192,27 +192,28 @@ function getCaptcha() { * 登录提交 */ async function handleLoginSubmit() { + // 1. 表单验证 + const valid = await loginFormRef.value?.validate().then( + () => true, + () => false + ); + if (!valid) return; + + loading.value = true; try { - // 1. 表单验证 - const valid = await loginFormRef.value?.validate(); - if (!valid) return; - - loading.value = true; - // 2. 执行登录 - try { - await userStore.login(loginFormData.value); - // 登录成功,跳转到目标页面 - const redirectPath = (route.query.redirect as string) || "/"; - await router.push(decodeURIComponent(redirectPath)); - } catch (error) { - // 登录失败,刷新验证码 - getCaptcha(); - throw error; - } - } catch (error) { - // 统一错误处理 - console.error("登录失败:", error); + await userStore.login(loginFormData.value).then( + async () => { + // 登录成功,跳转到目标页面 + const redirectPath = (route.query.redirect as string) || "/"; + await router.push(decodeURIComponent(redirectPath)); + }, + (error) => { + // 登录失败,刷新验证码 + getCaptcha(); + throw error; + } + ); } finally { loading.value = false; } diff --git a/src/views/profile/index.vue b/src/views/profile/index.vue index d9e7eb6d..628dabb1 100644 --- a/src/views/profile/index.vue +++ b/src/views/profile/index.vue @@ -586,18 +586,13 @@ const handleFileChange = async (event: Event) => { const file = target.files ? target.files[0] : null; if (file) { // 调用文件上传API - try { - const data = await FileAPI.uploadFile(file); - // 更新用户信息 - await UserAPI.updateProfile({ - avatar: data.url, - }); - // 更新用户头像 - userStore.userInfo.avatar = data.url; - } catch (error) { - console.error("头像上传失败:" + error); - ElMessage.error("头像上传失败"); - } + const data = await FileAPI.uploadFile(file); + // 更新用户信息 + await UserAPI.updateProfile({ + avatar: data.url, + }); + // 更新用户头像 + userStore.userInfo.avatar = data.url; } }; diff --git a/src/views/profile/notice/index.vue b/src/views/profile/notice/index.vue index 51b1fd9e..3429050f 100644 --- a/src/views/profile/notice/index.vue +++ b/src/views/profile/notice/index.vue @@ -114,7 +114,6 @@ defineOptions({ }); import { onMounted, reactive, ref } from "vue"; -import { ElMessage } from "element-plus"; import NoticeAPI from "@/api/system/notice"; import type { NoticeDetail, NoticeItem, NoticeQueryParams } from "@/types/api"; @@ -134,12 +133,9 @@ const noticeDetail = ref(null); async function handleQuery() { loading.value = true; try { - const res = await NoticeAPI.getMyNoticePage(queryParams); - pageData.value = res.data; - total.value = res.page?.total ?? 0; - } catch (error) { - ElMessage.error("获取通知列表失败"); - console.error("获取我的通知失败", error); + const data = await NoticeAPI.getMyNoticePage(queryParams); + pageData.value = data.list; + total.value = data.total ?? 0; } finally { loading.value = false; } @@ -152,14 +148,9 @@ function handleResetQuery() { } async function handleReadNotice(id: string) { - try { - const data = await NoticeAPI.getDetail(id); - noticeDetail.value = data; - noticeDialogVisible.value = true; - } catch (error) { - ElMessage.error("获取通知详情失败"); - console.error("获取通知详情失败", error); - } + const data = await NoticeAPI.getDetail(id); + noticeDetail.value = data; + noticeDialogVisible.value = true; } onMounted(() => { diff --git a/src/views/system/config/index.vue b/src/views/system/config/index.vue index 48623da4..86c6a699 100644 --- a/src/views/system/config/index.vue +++ b/src/views/system/config/index.vue @@ -185,9 +185,9 @@ const rules = reactive({ function fetchData() { loading.value = true; ConfigAPI.getPage(queryParams) - .then((res) => { - pageData.value = res.data; - total.value = res.page?.total ?? 0; + .then((data) => { + pageData.value = data.list; + total.value = data.total ?? 0; }) .finally(() => { loading.value = false; diff --git a/src/views/system/dict/dict-item.vue b/src/views/system/dict/dict-item.vue index bb9d5b2a..1866123d 100644 --- a/src/views/system/dict/dict-item.vue +++ b/src/views/system/dict/dict-item.vue @@ -199,9 +199,9 @@ const computedRules = computed(() => { function fetchData() { loading.value = true; DictAPI.getDictItemPage(dictCode.value, queryParams) - .then((res) => { - tableData.value = res.data; - total.value = res.page?.total ?? 0; + .then((data) => { + tableData.value = data.list; + total.value = data.total ?? 0; }) .finally(() => { loading.value = false; diff --git a/src/views/system/dict/index.vue b/src/views/system/dict/index.vue index e55a5ba9..a9ec2a4e 100644 --- a/src/views/system/dict/index.vue +++ b/src/views/system/dict/index.vue @@ -176,9 +176,9 @@ const computedRules = computed(() => { function fetchData() { loading.value = true; DictAPI.getPage(queryParams) - .then((res) => { - tableData.value = res.data; - total.value = res.page?.total ?? 0; + .then((data) => { + tableData.value = data.list; + total.value = data.total ?? 0; }) .finally(() => { loading.value = false; diff --git a/src/views/system/log/index.vue b/src/views/system/log/index.vue index 65ded950..f5a03541 100644 --- a/src/views/system/log/index.vue +++ b/src/views/system/log/index.vue @@ -90,9 +90,9 @@ const pageData = ref(); function fetchData() { loading.value = true; LogAPI.getPage(queryParams) - .then((res) => { - pageData.value = res.data; - total.value = res.page?.total ?? 0; + .then((data) => { + pageData.value = data.list; + total.value = data.total ?? 0; }) .finally(() => { loading.value = false; diff --git a/src/views/system/notice/index.vue b/src/views/system/notice/index.vue index 1fbe47c7..11ceb558 100644 --- a/src/views/system/notice/index.vue +++ b/src/views/system/notice/index.vue @@ -349,9 +349,9 @@ function handleQuery() { function fetchData() { loading.value = true; NoticeAPI.getPage(queryParams) - .then((res) => { - pageData.value = res.data; - total.value = res.page?.total ?? 0; + .then((data) => { + pageData.value = data.list; + total.value = data.total ?? 0; }) .finally(() => { loading.value = false; diff --git a/src/views/system/role/index.vue b/src/views/system/role/index.vue index 3ee0ba75..3a1971b1 100644 --- a/src/views/system/role/index.vue +++ b/src/views/system/role/index.vue @@ -47,6 +47,14 @@ + + + +