From a61036e5dcd284f9feaea346e57c1f0e7efaaae5 Mon Sep 17 00:00:00 2001 From: sheng <905537351@qq.com> Date: Thu, 28 May 2026 19:16:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E8=A7=92=E8=89=B2?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?API=E4=B8=8E=E4=BA=A4=E4=BA=92=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增角色管理后台页面、路由与国际化文案 2. 重构API请求错误处理逻辑,统一拦截业务与HTTP错误 3. 新增确认弹窗组合式函数,区分取消与请求错误场景 4. 完善表格按钮权限与显示控制逻辑 5. 更新API参数规范与文档说明 6. 修复部分页面分页数据解析问题 --- docs/表格组件使用说明.md | 213 +++++++++- docs/迁移提示词.md | 12 + src/api/_service.js | 15 +- src/api/menu.js | 2 + src/api/system-administration/role.js | 75 ++++ src/components/page-table/index.vue | 24 +- src/composables/useConfirmHandle.js | 60 +++ src/composables/useTableButtons.js | 3 +- src/locales/en.json | 37 +- src/locales/zh-chs.json | 37 +- src/router/modules/system-administration.js | 6 + .../factory-model/factory-area/index.vue | 44 +-- .../role/components/PermDrawer/index.vue | 151 ++++++++ .../user-management/role/index.vue | 365 ++++++++++++++++++ 14 files changed, 999 insertions(+), 45 deletions(-) create mode 100644 src/api/system-administration/role.js create mode 100644 src/composables/useConfirmHandle.js create mode 100644 src/views/system-administration/user-management/role/components/PermDrawer/index.vue create mode 100644 src/views/system-administration/user-management/role/index.vue diff --git a/docs/表格组件使用说明.md b/docs/表格组件使用说明.md index 19d10bca..b65f5fce 100644 --- a/docs/表格组件使用说明.md +++ b/docs/表格组件使用说明.md @@ -17,7 +17,8 @@ 7. [路由配置](#7-路由配置) 8. [API 文件写法](#8-api-文件写法) 9. [旧代码迁移对照](#9-旧代码迁移对照) -10. [常见问题排查](#10-常见问题排查) +10. [接口请求错误处理规范](#10-接口请求错误处理规范) +11. [常见问题排查](#11-常见问题排查) --- @@ -963,7 +964,215 @@ export function deleteFactoryArea (data) { --- -## 10. 常见问题排查 +## 10. 接口请求错误处理规范 + +> 错误处理采用 **三层架构**:拦截器全局兜底 → confirmMixin 区分取消/错误 → 页面标准模式。 + +### 10.1 错误处理总流程 + +``` + ┌─────────────────────┐ + │ 页面发起 API 请求 │ + └──────────┬──────────┘ + │ + ┌──────────▼──────────┐ + │ 是否需 confirm ? │ + └─────┬────────┬──────┘ + │ 否 │ 是 + │ │ + │ ┌────▼───────────┐ + │ │ $confirmAction() │ + │ │ (confirmMixin) │ + │ └────┬──────┬─────┘ + │ 取消 │ │ 确认 + │ ┌────▼──┐ │ + │ │return │ │ + │ │ true │ │ + │ └───────┘ │ + │ │ + ┌──────────▼────────────────▼──┐ + │ axios 请求 │ + └──────────┬───────────────────┘ + │ + ┌───────────▼────────────┐ + │ HTTP 状态码是什么? │ + └──┬──────────────┬──────┘ + 200 │ │ 非 200 + │ │ + ┌───────────▼──────┐ ┌───▼─────────────┐ + │ response.data. │ │ HTTP 拦截器 │ + │ code 是什么? │ │ error.message = │ + └──┬───────────┬───┘ │ '未授权/超时/...' │ + 0 │ │≠0 └───┬──────────────┘ + │ │ │ + ┌────▼──┐ ┌─────▼──────┐ │ + │resolve│ │handleError()│◄─┘ + │ data │ │Message.error│ + └───────┘ └─────┬──────┘ + │ + ┌────▼────┐ + │ throw │ + │ error │ + └────┬────┘ + │ + ┌─────────▼─────────┐ + │ 页面层 catch 到了吗?│ + └────┬──────────┬───┘ + 没写 │ │ 写了 + │ │ + ┌─────────▼──┐ ┌────▼─────────────┐ + │ 什么都不发生 │ │ finally 关锁 │ + │(依赖拦截器 │ │ loading/submitting│ + │ Message) │ │ = false │ + └────────────┘ └──────────────────┘ +``` + +### 10.2 三层架构详图 + +``` +┌─────────────────────────────────────────────────┐ +│ 第一层:全局拦截器 │ +│ src/api/_service.js │ +│ │ +│ 拦截所有 axios 响应,统一处理两类错误: │ +│ │ +│ HTTP 错误 (4xx/5xx) │ +│ ├─ 400 → error.message = '请求错误' │ +│ ├─ 401 → error.message = '未授权,请登录' │ +│ ├─ 403 → error.message = '拒绝访问' │ +│ ├─ 404 → error.message = '请求地址出错' │ +│ ├─ 408 → error.message = '请求超时' │ +│ ├─ 500 → error.message = '服务器内部错误' │ +│ ├─ 502 → error.message = '网关错误' │ +│ ├─ ... 其它状态码 │ +│ │ │ +│ └─ handleError(error) │ +│ ├─ store.dispatch('d2admin/log/push', ...) │ +│ ├─ console.log(error) (dev only) │ +│ └─ Message.error(error.message) ← 弹红色提示 │ +│ │ +│ 业务错误 (code ≠ 0, 如 code=500 "参数错误") │ +│ ├─ 取出 response.data.msg 或 '请求失败' │ +│ ├─ const err = new Error(`${msg}: ${url}`) │ +│ ├─ handleError(err) │ +│ └─ throw err ← 继续向上抛给页面层 │ +└──────────────────────┬──────────────────────────┘ + │ +┌──────────────────────▼──────────────────────────┐ +│ 第二层:confirmMixin │ +│ src/composables/useConfirmHandle.js │ +│ │ +│ $confirmAction(confirmOpts, action) │ +│ ├─ 第一层 try/catch │ +│ │ await this.$confirm(...) │ +│ │ ├─ 用户点"确定" → 继续 │ +│ │ └─ 用户点"取消" → catch → return true │ +│ │ │ +│ └─ 第二层 try/catch │ +│ await action() ← 执行 API 调用 │ +│ ├─ 成功 → return false │ +│ └─ 失败 → catch → return false │ +│ (拦截器已弹出 Message,此处静默吞掉) │ +└──────────────────────┬──────────────────────────┘ + │ +┌──────────────────────▼──────────────────────────┐ +│ 第三层:页面标准模式 │ +│ │ +│ 场景A — 查询 (fetchData) │ +│ ┌─────────────────────────────────────────────┐ │ +│ │ async fetchData () { │ │ +│ │ this.loading = true │ │ +│ │ try { │ │ +│ │ const res = await getList(...) │ │ +│ │ this.tableData = res.data │ │ +│ │ } finally { this.loading = false } │ │ +│ │ } │ │ +│ └─────────────────────────────────────────────┘ │ +│ • try 内无 catch:拦截器已弹错误提示 │ +│ • finally 始终执行:loading 无论成败都关闭 │ +│ │ +│ 场景B — 提交 (onDialogSubmit) │ +│ ┌─────────────────────────────────────────────┐ │ +│ │ async onDialogSubmit () { │ │ +│ │ this.submitting = true │ │ +│ │ try { │ │ +│ │ await createApi(this.formData) │ │ +│ │ this.$message.success(...) ← 成功才执行 │ │ +│ │ this.dialogVisible = false │ │ +│ │ this.fetchData() │ │ +│ │ } finally { this.submitting = false } │ │ +│ │ } │ │ +│ └─────────────────────────────────────────────┘ │ +│ • 出错时弹窗不关,用户可修改后重试 │ +│ │ +│ 场景C — 删除 (handleDelete) │ +│ ┌─────────────────────────────────────────────┐ │ +│ │ async handleDelete (row) { │ │ +│ │ const cancelled = await this.$confirmAction(│ │ +│ │ { message: this.key('confirm_delete'), │ │ +│ │ title: this.key('tip') }, │ │ +│ │ () => deleteApi({ id: row.id }) │ │ +│ │ ) │ │ +│ │ if (cancelled) return ← 用户取消,静默 │ │ +│ │ this.$message.success(...) ← 成功才执行 │ │ +│ │ this.fetchData() │ │ +│ │ } │ │ +│ └─────────────────────────────────────────────┘ │ +│ • $confirmAction 内已处理错误 Message,不额外 catch│ +│ • 没有 try/catch 包裹 → 代码更简洁直观 │ +└─────────────────────────────────────────────────┘ +``` + +### 10.3 关键原则 + +| 原则 | 说明 | +|------|------| +| **拦截器兜底** | 所有 API 错误(HTTP + 业务)统一在 `_service.js` 中弹出 `Message.error` | +| **finally 关锁** | `loading` / `submitting` 用 `try { ... } finally { lock = false }`,不论成败都关闭 | +| **只用 catch 不动** | 成功后的 `$message.success` / `dialogVisible = false` / `fetchData` 不放在 `finally`,只有成功才执行 | +| **$confirmAction 包装** | 删除/批量操作类的 confirm + API 用 `$confirmAction()` 区分取消和错误 | +| **不吞业务错误** | `fetchData` / `onDialogSubmit` 的 try 只写 `finally`,不写 `catch`,错误由拦截器处理 | + +### 10.4 confirmMixin 使用 + +页面中引入: + +```js +import { confirmMixin } from '@/composables/useConfirmHandle' + +export default { + mixins: [ + i18nMixin('page.xxx.xxx'), + confirmMixin // ← 新增 + ] +} +``` + +API: + +```js +// $confirmAction(confirmOpts, actionFn) → Promise +// 返回 true = 用户点了取消 +// 返回 false = action 已执行完成(无论成功失败) + +const cancelled = await this.$confirmAction( + { + message: this.key('confirm_delete'), // confirm 正文 + title: this.key('tip'), // confirm 标题 + // type: 'warning', // 可选,默认 'warning' + // confirmButtonText / cancelButtonText 可选,默认用 page.common 的 + }, + () => deleteApi({ id: row.id }) // 确认后执行的异步函数 +) +if (cancelled) return +// 以下只在成功时执行 +this.$message.success(...) +this.fetchData() +``` + +--- + +## 11. 常见问题排查 ### Q1:弹框打开后不显示内容? diff --git a/docs/迁移提示词.md b/docs/迁移提示词.md index ff392078..11fa9516 100644 --- a/docs/迁移提示词.md +++ b/docs/迁移提示词.md @@ -41,6 +41,18 @@ ``` - 后续统一修改路由后再批量替换 +#### 4.1 API 参数名必须对齐旧项目(重要) +- **API 函数入参的 key 名称必须与旧项目保持一致**,不能自行发明参数名 +- 例如:旧项目角色菜单接口参数是 `role_id`,不能写成 `id` + ```js + // ❌ 错误 — 自行发明参数名 + getRoleMenu({ id: this.roleId }) + // ✅ 正确 — 与旧项目参数名一致 + getRoleMenu({ role_id: this.roleId }) + ``` +- **迁移 API 时必须仔细阅读旧项目接口代码**,逐个核对每个接口的入参 key 名称 +- 如果后端报「xxx 不能为空」,首先检查参数 key 是否与旧项目一致 + #### 5. 旧 key → 新 key 映射 如果用户提供的是旧页面代码,需要根据对照表做 key 映射。已知映射如下(持续补充): diff --git a/src/api/_service.js b/src/api/_service.js index dc0fa44f..6fd22d7c 100644 --- a/src/api/_service.js +++ b/src/api/_service.js @@ -80,14 +80,19 @@ function createService () { } // 有 code 判断为项目接口请求 + let errorMessage = '' switch (response.data.code) { - // 返回响应内容 case 0: return response.data.data - // 例如在 code 401 情况下退回到登录页面 - case 401: throw new Error('请重新登录') - // 根据需要添加其它判断 - default: throw new Error(`${response.data.msg}: ${response.config.url}`) + case 401: + errorMessage = '请重新登录' + break + default: + errorMessage = response.data.msg || '请求失败' + break } + const businessError = new Error(`${errorMessage}: ${response.config.url}`) + handleError(businessError) + throw businessError }, error => { const status = get(error, 'response.status') diff --git a/src/api/menu.js b/src/api/menu.js index 955c1195..1cb95262 100644 --- a/src/api/menu.js +++ b/src/api/menu.js @@ -7,6 +7,8 @@ export function getMenuAll (data) { url: urls + 'all', method: 'get', params: { + method: 'system_settings_menu_configuration_menu_all', + platform: 'background', ...data } }) diff --git a/src/api/system-administration/role.js b/src/api/system-administration/role.js new file mode 100644 index 00000000..ded27261 --- /dev/null +++ b/src/api/system-administration/role.js @@ -0,0 +1,75 @@ +import { request } from '@/api/_service' + +const BASE = 'system_settings/user_management/role/' + +function apiParams (method, data = {}) { + return { + method: `system_settings_user_management_role_${method}`, + platform: 'background', + ...data + } +} + +export function getRoleList (data) { + return request({ + url: BASE + 'list', + method: 'get', + params: apiParams('list', data) + }) +} + +export function createRole (data) { + return request({ + url: BASE + 'create', + method: 'post', + data: apiParams('create', data) + }) +} + +export function editRole (data) { + return request({ + url: BASE + 'edit', + method: 'put', + data: apiParams('edit', data) + }) +} + +export function deleteRole (data) { + return request({ + url: BASE + 'delete', + method: 'delete', + data: apiParams('delete', data) + }) +} + +export function updateRoleStatus (data) { + return request({ + url: BASE + 'update_status', + method: 'put', + data: apiParams('update_status', data) + }) +} + +export function giveRoleMenu (data) { + return request({ + url: BASE + 'give', + method: 'put', + data: { + method: 'system_settings_user_management_give_role_menu', + platform: 'background', + ...data + } + }) +} + +export function getRoleMenu (data) { + return request({ + url: BASE + 'menu', + method: 'get', + params: { + method: 'system_settings_user_management_role_menu', + platform: 'background', + ...data + } + }) +} diff --git a/src/components/page-table/index.vue b/src/components/page-table/index.vue index 8733109c..208f7888 100644 --- a/src/components/page-table/index.vue +++ b/src/components/page-table/index.vue @@ -105,17 +105,19 @@ v-bind="colAttrs(col)" > diff --git a/src/composables/useConfirmHandle.js b/src/composables/useConfirmHandle.js new file mode 100644 index 00000000..094cd5fc --- /dev/null +++ b/src/composables/useConfirmHandle.js @@ -0,0 +1,60 @@ +/** + * $confirm + API 调用的标准包装 mixin + * + * 解决问题:$confirm 取消也会 reject,和 API 失败混在同一个 catch 里无法区分 + * + * 调用方无需 try/catch,只需判断返回值: + * - 返回 true → 用户取消,静默返回 + * - 返回 false → action 已执行(成功或失败),拦截器已处理 Message + * + * @example + * import { confirmMixin } from '@/composables/useConfirmHandle' + * export default { + * mixins: [confirmMixin, i18nMixin('page.xxx')], + * methods: { + * async handleDelete (row) { + * const cancelled = await this.$confirmAction( + * { message: this.key('confirm_delete'), title: this.key('tip') }, + * () => deleteApi({ id: row.id }) + * ) + * if (cancelled) return + * this.$message.success(this.$t(this.key('operation_success'))) + * this.fetchData() + * } + * } + * } + */ +export const confirmMixin = { + methods: { + /** + * @param {Object} confirmOpts - { message, title, ...$confirm 其余参数 } + * @param {Function} action - 确认后执行的 async 函数 + * @returns {Promise} true = 已取消, false = 已执行 + */ + async $confirmAction (confirmOpts, action) { + try { + await this.$confirm( + confirmOpts.message ? this.$t(confirmOpts.message) : '', + confirmOpts.title ? this.$t(confirmOpts.title) : '', + { + confirmButtonText: this.$t(confirmOpts.confirmButtonText || 'page.common.confirm'), + cancelButtonText: this.$t(confirmOpts.cancelButtonText || 'page.common.cancel'), + type: confirmOpts.type || 'warning', + closeOnClickModal: confirmOpts.closeOnClickModal !== false + } + ) + } catch { + return true + } + + try { + await action() + } catch { + // 拦截器已弹出 Message + } + return false + } + } +} + +export default confirmMixin diff --git a/src/composables/useTableButtons.js b/src/composables/useTableButtons.js index 9af94f9f..72a1dd7b 100644 --- a/src/composables/useTableButtons.js +++ b/src/composables/useTableButtons.js @@ -39,7 +39,8 @@ export function useTableButtons (options = {}, permissionCheck) { auth: btn.auth, confirm: btn.confirm || false, onClick: btn.onClick, - hasPermission: btn.auth ? check(btn.auth) : true + hasPermission: btn.auth ? check(btn.auth) : true, + visible: btn.visible || (() => true) })) return { toolbarButtons, rowButtons } diff --git a/src/locales/en.json b/src/locales/en.json index f594f182..208eec1e 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -4,7 +4,10 @@ "page": { "common": { "help": "Help", - "use": "Apply" + "use": "Apply", + "confirm": "Confirm", + "cancel": "Cancel", + "tip": "Tip" }, "demo": { "playground": { @@ -45,6 +48,38 @@ } } }, + "system_administration": { + "user_management": { + "role": { + "search": "Search", + "reset": "Reset", + "index": "No.", + "name": "Role Name", + "status": "Status", + "enabled": "Enabled", + "disabled": "Disabled", + "description": "Description", + "actions": "Actions", + "add": "Add", + "edit": "Edit", + "delete": "Delete", + "add_title": "Add Role", + "edit_title": "Edit Role", + "enter_name": "Please enter role name", + "enter_description": "Please enter description", + "select_status": "Please select status", + "select_rows_first": "Please select rows first", + "operation_success": "Operation succeeded", + "confirm_delete": "Are you sure to delete this role?", + "length_limit": "Length should be 1 to 100 characters", + "assign_permissions": "Assign Permissions", + "permission_assignment": "Permission Assignment", + "confirm": "Confirm", + "cancel": "Cancel", + "tip": "Tip" + } + } + }, "system": { "login": { "time_is_most_precious": "Time is the most precious of all wealth", diff --git a/src/locales/zh-chs.json b/src/locales/zh-chs.json index 61920423..21f516f8 100644 --- a/src/locales/zh-chs.json +++ b/src/locales/zh-chs.json @@ -4,7 +4,10 @@ "page": { "common": { "help": "帮 助", - "use": "使用" + "use": "使用", + "confirm": "确定", + "cancel": "取消", + "tip": "提示" }, "demo": { "playground": { @@ -45,6 +48,38 @@ } } }, + "system_administration": { + "user_management": { + "role": { + "search": "查询", + "reset": "重置", + "index": "序号", + "name": "角色名称", + "status": "状态", + "enabled": "启用", + "disabled": "禁用", + "description": "描述", + "actions": "操作", + "add": "新 增", + "edit": "编 辑", + "delete": "删 除", + "add_title": "新增角色", + "edit_title": "编辑角色", + "enter_name": "请输入角色名称", + "enter_description": "请输入描述", + "select_status": "请选择状态", + "select_rows_first": "请先勾选要操作的数据", + "operation_success": "操作成功", + "confirm_delete": "确定要删除该角色吗?", + "length_limit": "长度在 1 到 100 个字符", + "assign_permissions": "分配权限", + "permission_assignment": "权限分配", + "confirm": "确定", + "cancel": "取消", + "tip": "提示" + } + } + }, "system": { "login": { "time_is_most_precious": "时间是一切财富中最宝贵的财富", diff --git a/src/router/modules/system-administration.js b/src/router/modules/system-administration.js index 4c553b25..77b6c574 100644 --- a/src/router/modules/system-administration.js +++ b/src/router/modules/system-administration.js @@ -13,6 +13,12 @@ export default { name: `${pre}index`, meta: { ...meta, title: '系统设置', root: '/system_settings' }, component: _import('system/function/module-index') + }, + { + path: 'user_management/role', + name: `${pre}user_management-role`, + meta: { ...meta, cache: true, title: '角色' }, + component: _import('system-administration/user-management/role') } ])('system_settings-') } diff --git a/src/views/production-master-data/factory-model/factory-area/index.vue b/src/views/production-master-data/factory-model/factory-area/index.vue index 13b60dca..ecd83472 100644 --- a/src/views/production-master-data/factory-model/factory-area/index.vue +++ b/src/views/production-master-data/factory-model/factory-area/index.vue @@ -70,6 +70,7 @@ import { useTableColumns } from '@/composables/useTableColumns' import { useTableButtons } from '@/composables/useTableButtons' import { i18nMixin } from '@/composables/useI18n' +import { confirmMixin } from '@/composables/useConfirmHandle' import { getFactoryAreaList, createFactoryArea, @@ -82,7 +83,7 @@ import PageDialogForm from '@/components/page-dialog-form' export default { name: 'production-master-data-factory-area', components: { PageTable, PageDialogForm }, - mixins: [i18nMixin('page.production_master_data.factory_model.factory_area')], + mixins: [i18nMixin('page.production_master_data.factory_model.factory_area'), confirmMixin], data () { return { loading: false, @@ -195,8 +196,10 @@ export default { page_no: this.pagination.current, page_size: this.pagination.size }) - this.tableData = res.data || [] - this.pagination.total = res.count || 0 + const list = Array.isArray(res) ? res : (res.data || []) + const total = Array.isArray(res) ? res.length : (res.count || 0) + this.tableData = list + this.pagination.total = total } finally { this.loading = false } @@ -261,27 +264,20 @@ export default { this.resetForm() }, async handleDelete (row) { - try { - await this.$confirm( - this.$t(this.key('confirm_delete')), - this.$t(this.key('tip')), - { - confirmButtonText: this.$t(this.key('confirm')), - cancelButtonText: this.$t(this.key('cancel')), - type: 'warning', - closeOnClickModal: false - } - ) - await deleteFactoryArea({ id: [row.id] }) - this.$message.success(this.$t(this.key('operation_success'))) - this.pagination.current = Math.min( - this.pagination.current, - Math.ceil((this.pagination.total - 1) / this.pagination.size) || 1 - ) - this.fetchData() - } catch (e) { - // 取消删除 / 请求失败时不处理 - } + const cancelled = await this.$confirmAction( + { + message: this.key('confirm_delete'), + title: this.key('tip') + }, + () => deleteFactoryArea({ id: [row.id] }) + ) + if (cancelled) return + this.$message.success(this.$t(this.key('operation_success'))) + this.pagination.current = Math.min( + this.pagination.current, + Math.ceil((this.pagination.total - 1) / this.pagination.size) || 1 + ) + this.fetchData() } } } diff --git a/src/views/system-administration/user-management/role/components/PermDrawer/index.vue b/src/views/system-administration/user-management/role/components/PermDrawer/index.vue new file mode 100644 index 00000000..055099f8 --- /dev/null +++ b/src/views/system-administration/user-management/role/components/PermDrawer/index.vue @@ -0,0 +1,151 @@ + + + + + diff --git a/src/views/system-administration/user-management/role/index.vue b/src/views/system-administration/user-management/role/index.vue new file mode 100644 index 00000000..399059c1 --- /dev/null +++ b/src/views/system-administration/user-management/role/index.vue @@ -0,0 +1,365 @@ + + + + +