feat: 新增角色管理模块,优化API与交互体验
Some checks are pending
Release pipeline / publish (push) Waiting to run
Release pipeline / Always run job (push) Waiting to run

1.  新增角色管理后台页面、路由与国际化文案
2.  重构API请求错误处理逻辑,统一拦截业务与HTTP错误
3.  新增确认弹窗组合式函数,区分取消与请求错误场景
4.  完善表格按钮权限与显示控制逻辑
5.  更新API参数规范与文档说明
6.  修复部分页面分页数据解析问题
This commit is contained in:
sheng
2026-05-28 19:16:05 +08:00
parent ba43de8f4b
commit a61036e5dc
14 changed files with 999 additions and 45 deletions

View File

@@ -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<boolean>
// 返回 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弹框打开后不显示内容

View File

@@ -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 映射。已知映射如下(持续补充):