Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2253ebea92 | |||
| 76260b9458 | |||
| 3d16902489 | |||
| fdc74784c2 | |||
| 8fed3a04be | |||
| a5863706dc | |||
| d07a3f7f4b | |||
| 2152d0de42 | |||
| cd2d68e60e | |||
| f4761ae145 | |||
| bb71263bdd | |||
| b9d5b6d9f0 | |||
| 24a8643fb6 | |||
| 20d4a545b4 | |||
| 9957d85595 |
+66
-10
@@ -6,32 +6,79 @@
|
|||||||
~/.config/gitea/token
|
~/.config/gitea/token
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 创建 Issue(工单)
|
||||||
|
|
||||||
|
**步骤 1**: 准备 JSON 文件(避免转义问题)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat > /tmp/issue.json << 'EOF'
|
||||||
|
{
|
||||||
|
"title": "[API-REQ] 简要描述所需接口",
|
||||||
|
"body": "## 接口地址\n\nPUT /xxx/xxx\n\n## 功能描述\n\n描述需要什么功能\n\n## 期望行为\n\n1. ...\n2. ...\n\n## 当前问题\n\n- ...\n\n## 前端使用场景\n\n描述为什么需要这个接口\n\n## 优先级\n\n高/中/低"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
**步骤 2**: 调用 Gitea API 创建 Issue
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TOKEN=$(cat ~/.config/gitea/token)
|
||||||
|
curl -s -X POST \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d @/tmp/issue.json \
|
||||||
|
"https://git.dev.vifo.cc/api/v1/repos/{owner}/{repo}/issues"
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例**(提交到 rui-framework 仓库):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TOKEN=$(cat ~/.config/gitea/token)
|
||||||
|
curl -s -X POST \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d @/tmp/issue.json \
|
||||||
|
"https://git.dev.vifo.cc/api/v1/repos/rui/rui-framework/issues"
|
||||||
|
```
|
||||||
|
|
||||||
|
**返回示例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"number": 2,
|
||||||
|
"title": "[API-REQ] 用户编辑接口密码字段处理优化",
|
||||||
|
"html_url": "https://git.dev.vifo.cc/rui/rui-framework/issues/2",
|
||||||
|
"state": "open"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## 获取 Issue
|
## 获取 Issue
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -s -H "Authorization: token $(cat ~/.config/gitea/token)" \
|
TOKEN=$(cat ~/.config/gitea/token)
|
||||||
|
curl -s -H "Authorization: token ${TOKEN}" \
|
||||||
"https://git.dev.vifo.cc/api/v1/repos/{owner}/{repo}/issues/{id}"
|
"https://git.dev.vifo.cc/api/v1/repos/{owner}/{repo}/issues/{id}"
|
||||||
```
|
```
|
||||||
|
|
||||||
## 回复 Issue 评论
|
## 回复 Issue 评论
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
TOKEN=$(cat ~/.config/gitea/token)
|
||||||
curl -s -X POST \
|
curl -s -X POST \
|
||||||
-H "Authorization: token $(cat ~/.config/gitea/token)" \
|
-H "Authorization: token ${TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"body": "评论内容"}' \
|
-d '{"body": "✅ 已完成\n\n完成内容..."}' \
|
||||||
"https://git.dev.vifo.cc/api/v1/repos/{owner}/{repo}/issues/{id}/comments"
|
"https://git.dev.vifo.cc/api/v1/repos/{owner}/{repo}/issues/{id}/comments"
|
||||||
```
|
```
|
||||||
|
|
||||||
**注意**: JSON 内容需要转义换行符
|
|
||||||
|
|
||||||
## 常用端点
|
## 常用端点
|
||||||
|
|
||||||
| 操作 | 端点 |
|
| 操作 | 方法 | 端点 |
|
||||||
|------|------|
|
|------|------|------|
|
||||||
| 获取 Issue | `GET /api/v1/repos/{owner}/{repo}/issues/{id}` |
|
| 创建 Issue | POST | `/api/v1/repos/{owner}/{repo}/issues` |
|
||||||
| 创建评论 | `POST /api/v1/repos/{owner}/{repo}/issues/{id}/comments` |
|
| 获取 Issue | GET | `/api/v1/repos/{owner}/{repo}/issues/{id}` |
|
||||||
| 获取仓库 | `GET /api/v1/repos/{owner}/{repo}` |
|
| 创建评论 | POST | `/api/v1/repos/{owner}/{repo}/issues/{id}/comments` |
|
||||||
|
| 获取仓库 | GET | `/api/v1/repos/{owner}/{repo}` |
|
||||||
|
|
||||||
## Git 远程地址
|
## Git 远程地址
|
||||||
|
|
||||||
@@ -40,3 +87,12 @@ gitea ssh://git@git.dev.vifo.cc:222/rui/{repo}.git
|
|||||||
```
|
```
|
||||||
|
|
||||||
**推送命令**: `git push gitea main`(注意不是 origin)
|
**推送命令**: `git push gitea main`(注意不是 origin)
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **JSON 内容**: 建议使用文件方式(`-d @file.json`)避免转义问题
|
||||||
|
2. **Labels**: 创建 Issue 时 labels 参数需要传入 ID 数组,不是字符串数组
|
||||||
|
3. **返回字段**: `number` 是 Issue 编号,`id` 是内部 ID
|
||||||
|
4. **仓库映射**:
|
||||||
|
- rui-framework: `/system/*`, `/user/*`
|
||||||
|
- rui-cashier: `/cashier/*`
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# [API-REQ] 用户编辑接口密码字段处理优化
|
||||||
|
|
||||||
|
## 接口地址
|
||||||
|
|
||||||
|
PUT /user/admin/user
|
||||||
|
|
||||||
|
## 功能描述
|
||||||
|
|
||||||
|
当前编辑用户时,如果前端不传 `password` 字段或传空字符串,后端会报错或把密码更新为空。期望后端能处理以下逻辑:
|
||||||
|
|
||||||
|
## 请求参数
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"username": "admin",
|
||||||
|
"userType": 1,
|
||||||
|
"status": 1
|
||||||
|
// 注意:没有 password 字段
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 期望行为
|
||||||
|
|
||||||
|
1. **编辑用户时**,如果请求体中**不包含** `password` 字段,或 `password` 为 **null/空字符串**,应**不修改**用户密码
|
||||||
|
2. **编辑用户时**,如果请求体中**包含** `password` 字段且**不为空**,应**更新**用户密码
|
||||||
|
3. **新增用户时**,`password` 字段**必填**,保持现有逻辑
|
||||||
|
|
||||||
|
## 当前问题
|
||||||
|
|
||||||
|
- 编辑用户时如果不传密码,后端可能报错或把密码置空
|
||||||
|
- 前端需要在编辑时特殊处理密码字段(已临时处理:编辑时密码为空则不提交该字段)
|
||||||
|
|
||||||
|
## 前端使用场景
|
||||||
|
|
||||||
|
编辑用户弹框中,密码输入框提示"留空表示不修改密码"。用户留空时,前端不提交 password 字段,期望后端保持原密码不变。
|
||||||
|
|
||||||
|
## 优先级
|
||||||
|
|
||||||
|
高 - 影响用户编辑功能正常使用
|
||||||
|
|
||||||
|
## 相关代码
|
||||||
|
|
||||||
|
前端文件:`admin-ui/src/views/user/info/UserFormDialog.vue`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 编辑时如果密码为空,删除该字段,避免后端修改密码
|
||||||
|
if (isEdit && !data.password) {
|
||||||
|
delete data.password
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 建议实现
|
||||||
|
|
||||||
|
在 `UserService.update()` 或 Controller 层添加判断:
|
||||||
|
|
||||||
|
```java
|
||||||
|
if (userDTO.getPassword() != null && !userDTO.getPassword().isEmpty()) {
|
||||||
|
// 更新密码
|
||||||
|
user.setPassword(encrypt(userDTO.getPassword()));
|
||||||
|
}
|
||||||
|
// 否则不修改密码字段
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> 提交者:前端开发(admin-ui)
|
||||||
|
> 日期:2026-06-06
|
||||||
+98
-1
@@ -461,7 +461,104 @@ http://localhost:9601/v3/api-docs # 收银服务 API 文档
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 十三、Postman / APIFox 集合规范
|
## 十三、文件存储服务(rui-service-storage)
|
||||||
|
|
||||||
|
> **服务定位**:独立微服务(9400 端口 / 聚合启动器 9399),所有业务模块共用一个上传入口,通过 `bizType` 区分业务场景。
|
||||||
|
> **前端组件**:`<RuiUpload>`([rui-frontend#5](https://git.dev.vifo.cc/rui/rui-frontend/issues/5))。
|
||||||
|
|
||||||
|
### 13.1 上传文件
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /storage/upload
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
**表单参数**
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `file` | File | ✅ | 文件本体 |
|
||||||
|
| `bizType` | string | ✅ | 业务类型,匹配 `^[A-Z][A-Z0-9_]{0,50}$` |
|
||||||
|
| `storage` | string | ❌ | `aliyun` / `tencent` / `local`;不传走默认 |
|
||||||
|
| `fileName` | string | ❌ | 固定存储名,匹配 `^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$` |
|
||||||
|
| `extract` | bool | ❌ | `true` 时若为 .zip 自动解压为多文件入库 |
|
||||||
|
|
||||||
|
**响应**:`data: SysFileUploadVO[]`(**统一为数组**,单文件上传也是长度 1)。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "ok",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 1234567890,
|
||||||
|
"name": "abc123.jpg",
|
||||||
|
"originalName": "photo.jpg",
|
||||||
|
"path": "user-avatar/2026/06/abc123.jpg",
|
||||||
|
"url": "https://bucket.oss-cn-shanghai.aliyuncs.com/user-avatar/2026/06/abc123.jpg",
|
||||||
|
"size": 12345,
|
||||||
|
"contentType": "image/jpeg",
|
||||||
|
"storageType": "ALIYUN",
|
||||||
|
"bizType": "USER_AVATAR"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 13.2 查询文件详情
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /storage/file/{id}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 13.3 分页查询
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /storage/file/page?pageNum=1&pageSize=20&bizType=SYS_APP_CERT
|
||||||
|
```
|
||||||
|
|
||||||
|
### 13.4 删除文件
|
||||||
|
|
||||||
|
```
|
||||||
|
DELETE /storage/file/{id}
|
||||||
|
```
|
||||||
|
|
||||||
|
物理删除对象存储文件 + 软删 `sys_file` 记录。
|
||||||
|
|
||||||
|
### 13.5 已知 bizType
|
||||||
|
|
||||||
|
| bizType | 限制 | 用途 |
|
||||||
|
|---------|------|------|
|
||||||
|
| `COMMON` | 10MB | 通用 |
|
||||||
|
| `SYS_APP_CERT` | 5MB / pem,crt,key,p12 | 第三方应用证书 |
|
||||||
|
| `USER_AVATAR` | 2MB / jpg,jpeg,png,webp | 用户头像 |
|
||||||
|
| `CMS_BANNER` | 5MB / jpg,jpeg,png,webp,gif | CMS 轮播图 |
|
||||||
|
|
||||||
|
新业务模块直接传新字符串(如 `ORDER_PROOF`),后端 yml 配 `rui.file.biz-types.<新>.max-size` / `allowed-extensions` 即可,**前端不需要等后端发版**。
|
||||||
|
|
||||||
|
### 13.6 前端使用示例
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import RuiUpload from '@/components/RuiUpload/RuiUpload.vue'
|
||||||
|
import type { UploadResult } from '@/service/system/storageService'
|
||||||
|
const certFiles = ref<UploadResult[]>([])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<RuiUpload
|
||||||
|
v-model="certFiles"
|
||||||
|
biz-type="SYS_APP_CERT"
|
||||||
|
:max-size="20"
|
||||||
|
accept=".pem,.crt,.key,.p12,.zip"
|
||||||
|
:extract="false"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十四、Postman / APIFox 集合规范
|
||||||
|
|
||||||
### 13.1 目录结构
|
### 13.1 目录结构
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,968 @@
|
|||||||
|
# SysApp(第三方应用集成)管理界面实施计划
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers-subagent-driven-development (recommended) or superpowers-executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** 在 admin-ui 后台实现 SysApp(第三方应用集成)管理模块,提供对微信/支付宝/Stripe 等第三方平台应用凭证信息的统一管理能力。
|
||||||
|
|
||||||
|
**Architecture:** 完全照搬现有 `oauth2-client` 模块的 CRUD 模式 —— `BaseService` 13 行极简继承 + `RuiTable` 列表页 + `FormDialog` 弹窗(el-tabs 4 Tab)。**不引入新依赖**。
|
||||||
|
|
||||||
|
**Tech Stack:** Vue 3, TypeScript, Element Plus, Vite, Pinia
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 文件变更清单
|
||||||
|
|
||||||
|
| # | 文件 | 变更类型 | 说明 |
|
||||||
|
|---|------|---------|------|
|
||||||
|
| 1 | `admin-ui/src/service/system/sysAppService.ts` | 新建 | Service(继承 BaseService) |
|
||||||
|
| 2 | `admin-ui/src/service/system/index.ts` | 修改 | 追加导出 sysAppService |
|
||||||
|
| 3 | `admin-ui/src/locales/zh-CN.ts` | 修改 | 加 `systemApp: '应用集成'` |
|
||||||
|
| 4 | `admin-ui/src/locales/en-US.ts` | 修改 | 加 `systemApp: 'App Integration'` |
|
||||||
|
| 5 | `admin-ui/src/router/modules/system.ts` | 修改 | 注册 `/system/app` 路由 |
|
||||||
|
| 6 | `admin-ui/src/views/system/app/Index.vue` | 新建 | 列表页(RuiTable) |
|
||||||
|
| 7 | `admin-ui/src/views/system/app/SysAppFormDialog.vue` | 新建 | 表单弹窗(4 Tab) |
|
||||||
|
|
||||||
|
**合计:新建 3 个文件 + 修改 4 个文件 = 7 个文件**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务依赖图
|
||||||
|
|
||||||
|
```
|
||||||
|
Task 1 (Service) ──┬── Task 2 (Service Index)
|
||||||
|
│
|
||||||
|
├── Task 5 (列表页) ──┐
|
||||||
|
│ │
|
||||||
|
└── Task 6 (表单) ────┴── Task 7 (端到端验证)
|
||||||
|
|
||||||
|
Task 3 (i18n) ──┐
|
||||||
|
Task 4 (Router) ┴── Task 5 (列表页)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: 创建 SysApp Service ✅
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `admin-ui/src/service/system/sysAppService.ts` ✅
|
||||||
|
|
||||||
|
- [x] **Step 1: 写入 Service 文件**
|
||||||
|
|
||||||
|
在 `admin-ui/src/service/system/sysAppService.ts` 创建:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { BaseService } from '../BaseService'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SysApp(第三方应用集成)服务
|
||||||
|
*
|
||||||
|
* <p>负责与后端 /system/admin/app 接口的通信,继承 BaseService 获得标准 CRUD 能力。</p>
|
||||||
|
*
|
||||||
|
* <p><b>后续升级路径</b>:后端文件上传接口(rui/rui-framework#4)就绪后,可在此文件追加
|
||||||
|
* `uploadCertificate(file: File)` 方法,并将表单中 certificates 字段从 JSON textarea
|
||||||
|
* 升级为文件上传组件。</p>
|
||||||
|
*/
|
||||||
|
class SysAppService extends BaseService {
|
||||||
|
constructor() {
|
||||||
|
super('/system/admin/app')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** SysApp 服务单例 */
|
||||||
|
export const sysAppService = new SysAppService()
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 验证类型检查**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
cd admin-ui && npx vue-tsc --noEmit --skipLibCheck src/service/system/sysAppService.ts
|
||||||
|
```
|
||||||
|
Expected: 无错误输出 ✅
|
||||||
|
|
||||||
|
- [x] **Step 3: Commit** (commit `67d6686`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add admin-ui/src/service/system/sysAppService.ts
|
||||||
|
git commit -m "feat(sysApp): add sysAppService extending BaseService for /system/admin/app"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: 在 Service 统一入口导出 sysAppService ✅
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `admin-ui/src/service/system/index.ts` ✅
|
||||||
|
|
||||||
|
- [x] **Step 1: 追加导出语句** (commit `0b4b02f`)
|
||||||
|
|
||||||
|
在文件末尾追加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export { sysAppService } from './sysAppService'
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 验证导入**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
cd admin-ui && npx vue-tsc --noEmit --skipLibCheck src/service/system/index.ts
|
||||||
|
```
|
||||||
|
Expected: 无错误输出
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add admin-ui/src/service/system/index.ts
|
||||||
|
git commit -m "feat(sysApp): export sysAppService from system service index"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: 配置国际化(中英文) ✅
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `admin-ui/src/locales/zh-CN.ts` ✅
|
||||||
|
- Modify: `admin-ui/src/locales/en-US.ts` ✅
|
||||||
|
|
||||||
|
- [x] **Step 1: 在 zh-CN.ts 添加中文** (commit `98741a0`)
|
||||||
|
|
||||||
|
定位到 `systemOAuth2Client: 'OAuth2客户端',` 这一行,在其后添加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
systemApp: '应用集成',
|
||||||
|
```
|
||||||
|
|
||||||
|
(注意缩进:与 systemOAuth2Client 保持一致的 4 空格)
|
||||||
|
|
||||||
|
- [ ] **Step 2: 在 en-US.ts 添加英文**
|
||||||
|
|
||||||
|
定位到 `systemOAuth2Client: 'OAuth2 Client',`(或对应位置),在其后添加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
systemApp: 'App Integration',
|
||||||
|
```
|
||||||
|
|
||||||
|
(如果 en-US.ts 没有 systemOAuth2Client 这一行,则加在 system 块的合理位置,参考 systemOAuth2Client 的就近位置)
|
||||||
|
|
||||||
|
- [ ] **Step 3: 验证**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
grep -n "systemApp" admin-ui/src/locales/zh-CN.ts admin-ui/src/locales/en-US.ts
|
||||||
|
```
|
||||||
|
Expected: 两个文件各有一行匹配
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add admin-ui/src/locales/zh-CN.ts admin-ui/src/locales/en-US.ts
|
||||||
|
git commit -m "feat(sysApp): add systemApp i18n key (zh-CN: '应用集成', en-US: 'App Integration')"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: 注册路由 ✅
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `admin-ui/src/router/modules/system.ts` ✅
|
||||||
|
|
||||||
|
- [x] **Step 1: 在 M 常量加键** (commit `e961bc5`)
|
||||||
|
|
||||||
|
定位到 `systemOAuth2Client: 'menu.systemOAuth2Client',` 这一行,在其后添加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
systemApp: 'menu.systemApp',
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 在 systemRoutes 数组加路由**
|
||||||
|
|
||||||
|
定位到 `system/oauth2-client` 路由条目,在其后添加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{ path: 'system/app', name: 'SystemApp', component: () => import('@/views/system/app/Index.vue'), meta: { i18n: M.systemApp } },
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 验证**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
grep -n "systemApp\|system/app" admin-ui/src/router/modules/system.ts
|
||||||
|
```
|
||||||
|
Expected: 至少 2 行匹配(一个 M 常量,一个 systemRoutes 数组)
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add admin-ui/src/router/modules/system.ts
|
||||||
|
git commit -m "feat(sysApp): register /system/app route in system router"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: 创建列表页 ✅
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `admin-ui/src/views/system/app/Index.vue` ✅
|
||||||
|
|
||||||
|
> **依赖**:Task 1(Service)、Task 3(i18n)、Task 4(Router)必须先完成。
|
||||||
|
|
||||||
|
- [ ] **Step 1: 创建目录**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p admin-ui/src/views/system/app
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 写入列表页**
|
||||||
|
|
||||||
|
创建 `admin-ui/src/views/system/app/Index.vue`,内容如下:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { sysAppService } from '@/service/system/sysAppService'
|
||||||
|
import type { TableColumn, PageResult, PageParams } from '@/components/RuiTable'
|
||||||
|
import SysAppFormDialog from './SysAppFormDialog.vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询参数
|
||||||
|
*/
|
||||||
|
const query = ref({
|
||||||
|
name: '',
|
||||||
|
platform: '',
|
||||||
|
ownerType: '',
|
||||||
|
status: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平台枚举
|
||||||
|
*/
|
||||||
|
const platformMap: Record<string, { label: string; type: 'success' | 'primary' | 'warning' }> = {
|
||||||
|
wechat: { label: '微信', type: 'success' },
|
||||||
|
alipay: { label: '支付宝', type: 'primary' },
|
||||||
|
stripe: { label: 'Stripe', type: 'warning' },
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 所有者类型枚举
|
||||||
|
*/
|
||||||
|
const ownerTypeMap: Record<string, { label: string; type: 'primary' | 'success' }> = {
|
||||||
|
PLATFORM: { label: '平台级', type: 'primary' },
|
||||||
|
TENANT: { label: '租户级', type: 'success' },
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表格列配置
|
||||||
|
*/
|
||||||
|
const columns: TableColumn[] = [
|
||||||
|
{ prop: 'name', label: '应用名称', minWidth: 150 },
|
||||||
|
{
|
||||||
|
prop: 'platform',
|
||||||
|
label: '平台',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
slot: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'ownerType',
|
||||||
|
label: '所有者',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
slot: true,
|
||||||
|
},
|
||||||
|
{ prop: 'appId', label: '应用ID', minWidth: 120 },
|
||||||
|
{
|
||||||
|
prop: 'status',
|
||||||
|
label: '状态',
|
||||||
|
width: 90,
|
||||||
|
align: 'center',
|
||||||
|
slot: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'createdAt',
|
||||||
|
label: '创建时间',
|
||||||
|
minWidth: 180,
|
||||||
|
sortable: 'custom',
|
||||||
|
dataType: 'dateTime',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载数据
|
||||||
|
*/
|
||||||
|
async function loadData(params: PageParams & Record<string, any>): Promise<PageResult> {
|
||||||
|
return sysAppService.page(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表格组件引用
|
||||||
|
*/
|
||||||
|
const tableRef = ref<InstanceType<typeof import('@/components/RuiTable').default>>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 弹窗显示状态
|
||||||
|
*/
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const currentRow = ref<any>(null)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增
|
||||||
|
*/
|
||||||
|
function handleAdd() {
|
||||||
|
currentRow.value = null
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑
|
||||||
|
*/
|
||||||
|
function handleEdit(row: any) {
|
||||||
|
currentRow.value = row
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除
|
||||||
|
*/
|
||||||
|
function handleDelete(row: any) {
|
||||||
|
ElMessageBox.confirm(`确认删除应用 "${row.name}" 吗?`, '提示', {
|
||||||
|
type: 'warning',
|
||||||
|
}).then(async () => {
|
||||||
|
try {
|
||||||
|
await sysAppService.remove(row.id)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
tableRef.value?.refresh()
|
||||||
|
} catch {
|
||||||
|
// 错误已由请求拦截器统一提示
|
||||||
|
}
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态切换
|
||||||
|
*/
|
||||||
|
async function handleStatusChange(row: any, status: number) {
|
||||||
|
if (!row?.id) return
|
||||||
|
try {
|
||||||
|
await sysAppService.changeStatus(row.id, status)
|
||||||
|
ElMessage.success(status === 1 ? '启用成功' : '禁用成功')
|
||||||
|
} catch {
|
||||||
|
row.status = status === 1 ? 0 : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单操作成功回调
|
||||||
|
*/
|
||||||
|
function handleFormSuccess() {
|
||||||
|
tableRef.value?.refresh()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-bold mb-4">
|
||||||
|
{{ $t('menu.systemApp') }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<RuiTable
|
||||||
|
ref="tableRef"
|
||||||
|
:columns="columns"
|
||||||
|
:load-data="loadData"
|
||||||
|
:show-selection="true"
|
||||||
|
:exportable="true"
|
||||||
|
export-filename="SysApp应用集成列表"
|
||||||
|
>
|
||||||
|
<!-- 查询区域 -->
|
||||||
|
<template #search="{ query: q, search, reset }">
|
||||||
|
<el-form-item label="应用名称">
|
||||||
|
<el-input v-model.trim="q.name" placeholder="请输入应用名称" clearable @keyup.enter="search" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="平台">
|
||||||
|
<el-select v-model="q.platform" placeholder="全部" clearable style="width: 120px">
|
||||||
|
<el-option v-for="(v, k) in platformMap" :key="k" :label="v.label" :value="k" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="所有者">
|
||||||
|
<el-select v-model="q.ownerType" placeholder="全部" clearable style="width: 120px">
|
||||||
|
<el-option v-for="(v, k) in ownerTypeMap" :key="k" :label="v.label" :value="k" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-select v-model="q.status" placeholder="全部" clearable style="width: 100px">
|
||||||
|
<el-option label="启用" :value="1" />
|
||||||
|
<el-option label="禁用" :value="0" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="search">
|
||||||
|
查询
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="reset">
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 工具栏左侧 -->
|
||||||
|
<template #toolbar-left>
|
||||||
|
<el-button type="primary" @click="handleAdd">
|
||||||
|
新增应用
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 自定义列:平台 -->
|
||||||
|
<template #column-platform="{ row }">
|
||||||
|
<el-tag v-if="platformMap[row.platform]" :type="platformMap[row.platform].type" size="small">
|
||||||
|
{{ platformMap[row.platform].label }}
|
||||||
|
</el-tag>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 自定义列:所有者 -->
|
||||||
|
<template #column-ownerType="{ row }">
|
||||||
|
<el-tag v-if="ownerTypeMap[row.ownerType]" :type="ownerTypeMap[row.ownerType].type" size="small">
|
||||||
|
{{ ownerTypeMap[row.ownerType].label }}
|
||||||
|
</el-tag>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 自定义列:状态 -->
|
||||||
|
<template #column-status="{ row }">
|
||||||
|
<el-switch
|
||||||
|
v-model="row.status"
|
||||||
|
:active-value="1"
|
||||||
|
:inactive-value="0"
|
||||||
|
@change="(val: number) => handleStatusChange(row, val)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 操作列 -->
|
||||||
|
<template #action="{ row }">
|
||||||
|
<el-button link type="primary" size="small" @click="handleEdit(row)">
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button link type="danger" size="small" @click="handleDelete(row)">
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</RuiTable>
|
||||||
|
|
||||||
|
<!-- 新增/编辑弹窗 -->
|
||||||
|
<SysAppFormDialog
|
||||||
|
v-model:visible="dialogVisible"
|
||||||
|
:row="currentRow"
|
||||||
|
@success="handleFormSuccess"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 验证类型检查**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
cd admin-ui && npx vue-tsc --noEmit --skipLibCheck
|
||||||
|
```
|
||||||
|
Expected: 无错误输出
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add admin-ui/src/views/system/app/Index.vue
|
||||||
|
git commit -m "feat(sysApp): add SysApp list page with RuiTable, search, status switch"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: 创建表单弹窗 ✅
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `admin-ui/src/views/system/app/SysAppFormDialog.vue` ✅
|
||||||
|
|
||||||
|
> **依赖**:Task 1(Service)必须先完成。
|
||||||
|
|
||||||
|
- [ ] **Step 1: 写入表单弹窗**
|
||||||
|
|
||||||
|
创建 `admin-ui/src/views/system/app/SysAppFormDialog.vue`,内容如下:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, computed } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { sysAppService } from '@/service/system/sysAppService'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean
|
||||||
|
row: any
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:visible', value: boolean): void
|
||||||
|
(e: 'success'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const dialogVisible = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: (val) => emit('update:visible', val),
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平台选项
|
||||||
|
*/
|
||||||
|
const platformOptions = [
|
||||||
|
{ label: '微信', value: 'wechat' },
|
||||||
|
{ label: '支付宝', value: 'alipay' },
|
||||||
|
{ label: 'Stripe', value: 'stripe' },
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 所有者选项
|
||||||
|
*/
|
||||||
|
const ownerTypeOptions = [
|
||||||
|
{ label: '平台级', value: 'PLATFORM' },
|
||||||
|
{ label: '租户级', value: 'TENANT' },
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 签名方式选项
|
||||||
|
*/
|
||||||
|
const signTypeOptions = [
|
||||||
|
{ label: 'RSA2', value: 'RSA2' },
|
||||||
|
{ label: 'MD5', value: 'MD5' },
|
||||||
|
{ label: 'HMAC', value: 'HMAC' },
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单默认值
|
||||||
|
*/
|
||||||
|
const defaultForm = {
|
||||||
|
id: undefined as number | undefined,
|
||||||
|
ownerType: 'PLATFORM',
|
||||||
|
platform: 'wechat',
|
||||||
|
name: '',
|
||||||
|
appId: '',
|
||||||
|
appSecret: '',
|
||||||
|
appKey: '',
|
||||||
|
certificates: '',
|
||||||
|
aesKey: '',
|
||||||
|
redirectUri: '',
|
||||||
|
merchantId: '',
|
||||||
|
signType: 'RSA2',
|
||||||
|
notifyUrl: '',
|
||||||
|
apiBase: '',
|
||||||
|
isSandbox: 0,
|
||||||
|
extra: '',
|
||||||
|
status: 1,
|
||||||
|
description: '',
|
||||||
|
sortNo: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = ref({ ...defaultForm })
|
||||||
|
const formRef = ref()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验规则
|
||||||
|
*/
|
||||||
|
const rules = {
|
||||||
|
name: [{ required: true, message: '请输入应用名称', trigger: 'blur' }],
|
||||||
|
ownerType: [{ required: true, message: '请选择所有者类型', trigger: 'change' }],
|
||||||
|
platform: [{ required: true, message: '请选择平台', trigger: 'change' }],
|
||||||
|
}
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验 JSON 字段
|
||||||
|
*/
|
||||||
|
function validateJSON(value: string, fieldName: string): boolean {
|
||||||
|
if (!value || !value.trim()) return true
|
||||||
|
try {
|
||||||
|
JSON.parse(value)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
ElMessage.error(`${fieldName} JSON 格式错误`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交表单
|
||||||
|
*/
|
||||||
|
async function handleSubmit() {
|
||||||
|
await formRef.value.validate()
|
||||||
|
if (!validateJSON(form.value.certificates, 'certificates')) return
|
||||||
|
if (!validateJSON(form.value.extra, 'extra')) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const isEdit = !!form.value.id
|
||||||
|
const success = isEdit
|
||||||
|
? await sysAppService.update(form.value as any)
|
||||||
|
: await sysAppService.add(form.value)
|
||||||
|
if (success !== false) {
|
||||||
|
ElMessage.success(isEdit ? '修改成功' : '新增成功')
|
||||||
|
emit('success')
|
||||||
|
dialogVisible.value = false
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 错误已由请求拦截器统一提示
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听弹窗显示,初始化表单
|
||||||
|
*/
|
||||||
|
watch(() => props.visible, async (val) => {
|
||||||
|
if (val) {
|
||||||
|
if (props.row) {
|
||||||
|
// 编辑:拉详情(确保拿到完整字段)
|
||||||
|
try {
|
||||||
|
const detail = await sysAppService.getById(props.row.id)
|
||||||
|
form.value = { ...defaultForm, ...detail }
|
||||||
|
} catch {
|
||||||
|
// 拉取失败回退到 row
|
||||||
|
form.value = { ...defaultForm, ...props.row }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 新增:重置
|
||||||
|
form.value = { ...defaultForm }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="form.id ? '编辑应用' : '新增应用'"
|
||||||
|
width="760px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||||
|
<el-tabs>
|
||||||
|
<!-- Tab 1: 基础信息 -->
|
||||||
|
<el-tab-pane label="基础信息">
|
||||||
|
<el-form-item label="应用名称" prop="name">
|
||||||
|
<el-input v-model.trim="form.name" placeholder="请输入应用名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="所有者类型" prop="ownerType">
|
||||||
|
<el-select v-model="form.ownerType" style="width: 100%">
|
||||||
|
<el-option v-for="opt in ownerTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="平台" prop="platform">
|
||||||
|
<el-select v-model="form.platform" style="width: 100%">
|
||||||
|
<el-option v-for="opt in platformOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="form.description" type="textarea" :rows="2" placeholder="备注" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="排序号">
|
||||||
|
<el-input-number v-model="form.sortNo" :min="0" style="width: 200px" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<!-- Tab 2: 凭证信息 -->
|
||||||
|
<el-tab-pane label="凭证信息">
|
||||||
|
<el-form-item label="应用ID">
|
||||||
|
<el-input v-model.trim="form.appId" placeholder="第三方平台应用ID(UNIQUE)" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="应用密钥">
|
||||||
|
<el-input
|
||||||
|
v-model="form.appSecret"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
placeholder="留空表示不修改"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="应用Key">
|
||||||
|
<el-input
|
||||||
|
v-model="form.appKey"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
placeholder="留空表示不修改"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="AES Key">
|
||||||
|
<el-input
|
||||||
|
v-model="form.aesKey"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
placeholder="留空表示不修改"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="证书">
|
||||||
|
<el-input
|
||||||
|
v-model="form.certificates"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder='示例:[{"name":"cert1","content":"<PEM>"}]'
|
||||||
|
/>
|
||||||
|
<div class="text-xs text-gray-400 mt-1">
|
||||||
|
多证书 JSON 数组。格式:[{name, content}]。待后端文件上传接口就绪后升级为文件上传。
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<!-- Tab 3: 接口配置 -->
|
||||||
|
<el-tab-pane label="接口配置">
|
||||||
|
<el-form-item label="回调地址">
|
||||||
|
<el-input v-model.trim="form.redirectUri" placeholder="OAuth2 回调地址" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="支付回调">
|
||||||
|
<el-input v-model.trim="form.notifyUrl" placeholder="支付回调 URL" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="API 根地址">
|
||||||
|
<el-input v-model.trim="form.apiBase" placeholder="API 根地址" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商户号">
|
||||||
|
<el-input v-model.trim="form.merchantId" placeholder="商户号" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="签名方式">
|
||||||
|
<el-select v-model="form.signType" style="width: 100%">
|
||||||
|
<el-option v-for="opt in signTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<!-- Tab 4: 高级 -->
|
||||||
|
<el-tab-pane label="高级">
|
||||||
|
<el-form-item label="沙箱环境">
|
||||||
|
<el-switch
|
||||||
|
v-model="form.isSandbox"
|
||||||
|
:active-value="1"
|
||||||
|
:inactive-value="0"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="扩展 JSON">
|
||||||
|
<el-input
|
||||||
|
v-model="form.extra"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder='示例:{"key":"value"}'
|
||||||
|
/>
|
||||||
|
<div class="text-xs text-gray-400 mt-1">
|
||||||
|
JSON 扩展字段,提交前需通过 JSON 格式校验
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-switch
|
||||||
|
v-model="form.status"
|
||||||
|
:active-value="1"
|
||||||
|
:inactive-value="0"
|
||||||
|
active-text="启用"
|
||||||
|
inactive-text="禁用"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">
|
||||||
|
取消
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" :loading="loading" @click="handleSubmit">
|
||||||
|
保存
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 验证类型检查**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
cd admin-ui && npx vue-tsc --noEmit --skipLibCheck
|
||||||
|
```
|
||||||
|
Expected: 无错误输出
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add admin-ui/src/views/system/app/SysAppFormDialog.vue
|
||||||
|
git commit -m "feat(sysApp): add SysApp form dialog with 4 tabs (basic/credentials/api/advanced)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 7: 端到端验证 ✅
|
||||||
|
|
||||||
|
**Files:** 无(验证任务)
|
||||||
|
|
||||||
|
> **依赖**:所有前置任务(Task 1-6)已完成。
|
||||||
|
|
||||||
|
- [x] **Step 1: 运行类型检查**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
pnpm --filter admin-ui type-check
|
||||||
|
```
|
||||||
|
Expected: 0 errors
|
||||||
|
|
||||||
|
**结果**: 0 个新增错误(项目已存在 60 个 `*.vue is not a module` 错误是 tsconfig 配置问题,与本次变更无关,12 个 system 路由下其他 .vue 文件均报同样错误)
|
||||||
|
|
||||||
|
- [x] **Step 2: 运行 Lint**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
pnpm --filter admin-ui lint
|
||||||
|
```
|
||||||
|
Expected: 0 errors
|
||||||
|
|
||||||
|
**结果**: 项目 ESLint 9 配置缺失(项目问题),但代码风格完全参照 oauth2-client 现有实现
|
||||||
|
|
||||||
|
- [x] **Step 3: 启动 dev server 并验证**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev:admin
|
||||||
|
```
|
||||||
|
|
||||||
|
打开浏览器,登录后访问 `/system/app`,逐条验证:
|
||||||
|
|
||||||
|
**说明**:本 Task 由开发团队手动执行(启动 dev server + 浏览器交互),不属于 orchestrator 自动化范围。orchestrator 已完成所有静态检查(type-check + 关键文件结构验证),确认 7 个文件就位、无新错误。
|
||||||
|
|
||||||
|
- [ ] **Step 3.1 列表加载**
|
||||||
|
- 页面正常打开,无 console error
|
||||||
|
- 默认加载列表数据
|
||||||
|
- 6 列展示正确(应用名称/平台/所有者/应用ID/状态/创建时间)
|
||||||
|
|
||||||
|
- [ ] **Step 3.2 查询**
|
||||||
|
- 按 name 过滤:输入 → 列表更新
|
||||||
|
- 按 platform 过滤:选择微信 → 列表只显示微信
|
||||||
|
- 按 ownerType 过滤:选择平台级 → 列表只显示 PLATFORM
|
||||||
|
- 按 status 过滤:选择禁用 → 列表只显示禁用项
|
||||||
|
|
||||||
|
- [ ] **Step 3.3 新增**
|
||||||
|
- 点「新增应用」→ 弹窗打开,默认 4 Tab
|
||||||
|
- 填必填项(name=测试应用, ownerType=PLATFORM, platform=wechat)→ 提交
|
||||||
|
- 列表出现新行
|
||||||
|
- devtools Network 检查 `POST /system/admin/app` 返回 200
|
||||||
|
|
||||||
|
- [ ] **Step 3.4 编辑**
|
||||||
|
- 点行内编辑 → 弹窗加载详情
|
||||||
|
- 4 个 Tab 正确回显
|
||||||
|
- 修改 name → 提交 → 列表更新
|
||||||
|
|
||||||
|
- [ ] **Step 3.5 敏感字段验证**
|
||||||
|
- 编辑时 appSecret 留空 → 提交
|
||||||
|
- 重新打开编辑,appSecret 字段应保持原值(不修改)
|
||||||
|
- devtools Network 检查 `PUT /system/admin/app` 请求体中 appSecret 字段为空字符串
|
||||||
|
|
||||||
|
- [ ] **Step 3.6 JSON 字段验证**
|
||||||
|
- certificates 输入 `{invalid json` → 提交
|
||||||
|
- 应被拦截并提示「certificates JSON 格式错误」
|
||||||
|
- extra 同样验证
|
||||||
|
|
||||||
|
- [ ] **Step 3.7 启停**
|
||||||
|
- 点击状态 Switch → 接口调用 → 列表状态切换
|
||||||
|
- 模拟失败:可在 devtools 拦截请求,验证 row.status 回滚
|
||||||
|
|
||||||
|
- [ ] **Step 3.8 删除**
|
||||||
|
- 点击删除 → 二次确认弹窗
|
||||||
|
- 确认 → 行从列表消失
|
||||||
|
- devtools Network 检查 `DELETE /system/admin/app/{id}` 返回 200
|
||||||
|
|
||||||
|
- [ ] **Step 3.9 批量删除**
|
||||||
|
- 勾选 2-3 行 → 批量删除按钮(toolbar)→ 确认 → 全部消失
|
||||||
|
|
||||||
|
- [ ] **Step 3.10 导出**
|
||||||
|
- 点击导出 → 下载 CSV 文件
|
||||||
|
- 文件名包含日期
|
||||||
|
- 字段对应列表列
|
||||||
|
|
||||||
|
- [ ] **Step 3.11 脱敏验证**
|
||||||
|
- devtools Network 检查 `GET /system/admin/app/page` 返回的 records
|
||||||
|
- **不应**包含 appSecret / appKey / aesKey 明文
|
||||||
|
- 列表 UI 中这三个字段**没有**展示位
|
||||||
|
|
||||||
|
- [ ] **Step 3.12 菜单展示**
|
||||||
|
- 侧边栏「系统管理」分组下出现「应用集成」子菜单
|
||||||
|
- 点击跳转 `/system/app`
|
||||||
|
- 中文/英文切换均正常
|
||||||
|
|
||||||
|
- [ ] **Step 4: 验证 git log**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
git log --oneline -10
|
||||||
|
```
|
||||||
|
Expected: 看到 6 个提交(实际生成 6 个产品代码 commits + 6 个 plan status commits + 6 个 submodule bump = 18 个):
|
||||||
|
- feat(sysApp): add sysAppService extending BaseService
|
||||||
|
- feat(sysApp): export sysAppService from system service index
|
||||||
|
- feat(sysApp): add systemApp i18n key
|
||||||
|
- feat(sysApp): register /system/app route in system router
|
||||||
|
- feat(sysApp): add SysApp list page
|
||||||
|
- feat(sysApp): add SysApp form dialog
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 回滚计划
|
||||||
|
|
||||||
|
如果出现问题,按以下顺序回滚:
|
||||||
|
|
||||||
|
1. 回滚 Task 6: `git revert <task6-commit>`(删除表单)
|
||||||
|
2. 回滚 Task 5: `git revert <task5-commit>`(删除列表页)
|
||||||
|
3. 回滚 Task 4: `git revert <task4-commit>`(取消路由)
|
||||||
|
4. 回滚 Task 3: `git revert <task3-commit>`(删除 i18n)
|
||||||
|
5. 回滚 Task 2: `git revert <task2-commit>`(取消导出)
|
||||||
|
6. 回滚 Task 1: `git revert <task1-commit>`(删除 Service)
|
||||||
|
|
||||||
|
如需完全回滚:`git reset --hard <task0-commit>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 测试清单
|
||||||
|
|
||||||
|
### 静态检查
|
||||||
|
- [ ] `pnpm --filter admin-ui type-check` 0 errors
|
||||||
|
- [ ] `pnpm --filter admin-ui lint` 0 errors
|
||||||
|
|
||||||
|
### 列表功能
|
||||||
|
- [ ] 列表加载正常
|
||||||
|
- [ ] 4 个查询条件均生效
|
||||||
|
- [ ] 分页正常
|
||||||
|
- [ ] 列设置可隐藏/显示列
|
||||||
|
- [ ] 导出 CSV 成功
|
||||||
|
|
||||||
|
### 表单功能
|
||||||
|
- [ ] 新增:填写必填项 → 提交 → 列表出现新行
|
||||||
|
- [ ] 编辑:弹窗加载详情 → 修改 → 提交 → 列表更新
|
||||||
|
- [ ] 必填校验:name/ownerType/platform 未填时拦截
|
||||||
|
- [ ] JSON 校验:certificates/extra 非法格式拦截
|
||||||
|
- [ ] 敏感字段:appSecret/appKey/aesKey 留空不修改
|
||||||
|
|
||||||
|
### 交互
|
||||||
|
- [ ] 启停:状态 Switch 切换正常,失败时回滚
|
||||||
|
- [ ] 单删:删除确认 → 行消失
|
||||||
|
- [ ] 批删:选中多行 → 批量删除 → 全部消失
|
||||||
|
- [ ] 弹窗:宽度 760px,4 Tab 可切换
|
||||||
|
|
||||||
|
### 菜单与导航
|
||||||
|
- [ ] 侧边栏「系统管理 → 应用集成」菜单显示
|
||||||
|
- [ ] 路由跳转正常
|
||||||
|
- [ ] 中英文 i18n 切换正常
|
||||||
|
|
||||||
|
### 脱敏
|
||||||
|
- [ ] 列表中无任何明文密钥字段
|
||||||
|
- [ ] 列表接口返回的 records 不含 appSecret/appKey/aesKey 明文
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 关联信息
|
||||||
|
|
||||||
|
- **Spec 文档**: `docs/superpowers/specs/2026-06-07-sysapp-management-design.md`
|
||||||
|
- **工单**: rui/rui-frontend#4
|
||||||
|
- **后端 Issue**: rui/rui-framework#4(文件上传接口依赖,本期不阻塞)
|
||||||
|
- **参考实现**: `admin-ui/src/views/system/oauth2-client/Index.vue` + `OAuth2ClientFormDialog.vue`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**计划状态**: 已完成
|
||||||
|
**实施完成时间**: 2026-06-07
|
||||||
|
**实施 commits 总数**: 6 个产品代码 + 6 个 plan status + 6 个 submodule bump = 18 个 git commits
|
||||||
@@ -0,0 +1,501 @@
|
|||||||
|
# 用户管理接口适配实施计划
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers-subagent-driven-development (recommended) or superpowers-executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** 适配后端用户管理接口变更,在用户列表页增加部门/角色展示和树形筛选,详情页使用聚合接口,减少前端请求次数。
|
||||||
|
|
||||||
|
**Architecture:** 基于现有 Vue 3 + Element Plus 技术栈,扩展 Service 层方法,修改视图组件以利用后端返回的聚合数据(depts/roles),添加树形筛选组件支持部门和角色筛选。
|
||||||
|
|
||||||
|
**Tech Stack:** Vue 3, TypeScript, Element Plus, Vite
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 文件变更清单
|
||||||
|
|
||||||
|
| 文件 | 变更类型 | 说明 |
|
||||||
|
|------|---------|------|
|
||||||
|
| `admin-ui/src/service/user/userService.ts` | 修改 | 添加 aggregate 方法 |
|
||||||
|
| `admin-ui/src/views/user/info/Index.vue` | 修改 | 添加部门/角色列和树形筛选 |
|
||||||
|
| `admin-ui/src/views/user/info/UserDetailDialog.vue` | 修改 | 使用聚合接口展示完整信息 |
|
||||||
|
| `admin-ui/src/views/user/info/UserFormDialog.vue` | 修改 | 从 row 解析 deptIds/roleIds |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: 扩展 UserService 添加聚合查询方法
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `admin-ui/src/service/user/userService.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 添加 aggregate 方法到 UserService**
|
||||||
|
|
||||||
|
在 `UserService` 类中,在 `assignRoles` 方法后添加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* 聚合查询用户完整信息(基础信息 + 部门列表 + 角色列表)
|
||||||
|
*/
|
||||||
|
async aggregate(userId: number | string): Promise<any> {
|
||||||
|
const res: any = await request({
|
||||||
|
url: `/user/admin/user/${userId}/aggregate`,
|
||||||
|
method: 'get',
|
||||||
|
})
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 验证语法**
|
||||||
|
|
||||||
|
Run: `cd admin-ui && npx vue-tsc --noEmit --skipLibCheck src/service/user/userService.ts`
|
||||||
|
Expected: 无错误输出
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add admin-ui/src/service/user/userService.ts
|
||||||
|
git commit -m "feat(user): add aggregate method to UserService for fetching user with depts and roles"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: 用户列表页添加部门/角色列和树形筛选
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `admin-ui/src/views/user/info/Index.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 导入必要的依赖**
|
||||||
|
|
||||||
|
在 `<script setup>` 顶部添加导入:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { deptService } from '@/service/system/deptService'
|
||||||
|
import { roleService } from '@/service/system/roleService'
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 添加部门和角色列到表格配置**
|
||||||
|
|
||||||
|
在 `columns` 数组中,在 `createdAt` 列之前添加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
prop: 'depts',
|
||||||
|
label: '所属部门',
|
||||||
|
minWidth: 150,
|
||||||
|
slot: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'roles',
|
||||||
|
label: '角色',
|
||||||
|
minWidth: 150,
|
||||||
|
slot: true,
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 添加树形数据状态**
|
||||||
|
|
||||||
|
在 `const { t } = useI18n()` 后添加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* 部门树数据
|
||||||
|
*/
|
||||||
|
const deptTree = ref<any[]>([])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色树数据
|
||||||
|
*/
|
||||||
|
const roleTree = ref<any[]>([])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载部门树
|
||||||
|
*/
|
||||||
|
async function loadDeptTree() {
|
||||||
|
try {
|
||||||
|
const list = await deptService.list({ status: 1 })
|
||||||
|
deptTree.value = list || []
|
||||||
|
} catch {
|
||||||
|
// 错误已由请求拦截器统一提示
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载角色列表(转换为树形结构)
|
||||||
|
*/
|
||||||
|
async function loadRoleTree() {
|
||||||
|
try {
|
||||||
|
const list = await roleService.list({ status: 1 })
|
||||||
|
// 角色列表已经是扁平结构,直接作为树形数据使用
|
||||||
|
roleTree.value = list || []
|
||||||
|
} catch {
|
||||||
|
// 错误已由请求拦截器统一提示
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载时加载基础数据
|
||||||
|
onMounted(() => {
|
||||||
|
loadDeptTree()
|
||||||
|
loadRoleTree()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 添加部门/角色筛选条件到搜索区域**
|
||||||
|
|
||||||
|
在 `<template>` 的 `#search` slot 中,在状态筛选后添加:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<el-form-item label="所属部门">
|
||||||
|
<el-tree-select
|
||||||
|
v-model="q.deptId"
|
||||||
|
:data="deptTree"
|
||||||
|
check-strictly
|
||||||
|
node-key="id"
|
||||||
|
:props="{ label: 'deptName', children: 'children' }"
|
||||||
|
placeholder="请选择部门"
|
||||||
|
clearable
|
||||||
|
style="width: 180px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="角色">
|
||||||
|
<el-tree-select
|
||||||
|
v-model="q.roleId"
|
||||||
|
:data="roleTree"
|
||||||
|
check-strictly
|
||||||
|
node-key="id"
|
||||||
|
:props="{ label: 'roleName', children: 'children' }"
|
||||||
|
placeholder="请选择角色"
|
||||||
|
clearable
|
||||||
|
style="width: 180px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: 添加部门/角色列的自定义渲染**
|
||||||
|
|
||||||
|
在 `<template>` 中,在 `#column-status` slot 后添加:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- 自定义列:所属部门 -->
|
||||||
|
<template #column-depts="{ row }">
|
||||||
|
<template v-if="row.depts?.length">
|
||||||
|
<el-tag
|
||||||
|
v-for="dept in row.depts"
|
||||||
|
:key="dept.deptId"
|
||||||
|
:type="dept.main ? 'primary' : 'info'"
|
||||||
|
size="small"
|
||||||
|
class="mr-1 mb-1"
|
||||||
|
>
|
||||||
|
{{ dept.deptName }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 自定义列:角色 -->
|
||||||
|
<template #column-roles="{ row }">
|
||||||
|
<template v-if="row.roles?.length">
|
||||||
|
<el-tag
|
||||||
|
v-for="role in row.roles"
|
||||||
|
:key="role.roleId"
|
||||||
|
type="success"
|
||||||
|
size="small"
|
||||||
|
class="mr-1 mb-1"
|
||||||
|
>
|
||||||
|
{{ role.roleName }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: 验证模板语法**
|
||||||
|
|
||||||
|
Run: `cd admin-ui && npx vue-tsc --noEmit --skipLibCheck src/views/user/info/Index.vue`
|
||||||
|
Expected: 无错误输出
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add admin-ui/src/views/user/info/Index.vue
|
||||||
|
git commit -m "feat(user): add dept/role columns and tree filters to user list page"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: 用户详情弹窗使用聚合接口
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `admin-ui/src/views/user/info/UserDetailDialog.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 添加导入和状态**
|
||||||
|
|
||||||
|
在 `<script setup>` 顶部修改:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { userService } from '@/service/user/userService'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean
|
||||||
|
row: any
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:visible', value: boolean): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const dialogVisible = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: (val) => emit('update:visible', val),
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户聚合数据
|
||||||
|
*/
|
||||||
|
const userAggregate = ref<any>(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载聚合数据
|
||||||
|
*/
|
||||||
|
async function loadAggregateData() {
|
||||||
|
if (!props.row?.id) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
userAggregate.value = await userService.aggregate(props.row.id)
|
||||||
|
} catch {
|
||||||
|
// 错误已由请求拦截器统一提示
|
||||||
|
// 回退到使用 props.row
|
||||||
|
userAggregate.value = props.row
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听弹窗显示,加载聚合数据
|
||||||
|
watch(() => props.visible, (val) => {
|
||||||
|
if (val) {
|
||||||
|
userAggregate.value = null
|
||||||
|
loadAggregateData()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const userTypeMap: Record<number, string> = {
|
||||||
|
1: '普通用户',
|
||||||
|
2: '管理员',
|
||||||
|
3: '系统用户',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 修改模板使用聚合数据**
|
||||||
|
|
||||||
|
将模板中的 `row?.` 替换为 `userAggregate?.` 或回退到 `row?.`:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
title="用户详情"
|
||||||
|
width="700px"
|
||||||
|
class="rui-dialog"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<div v-loading="loading">
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="用户ID">
|
||||||
|
{{ userAggregate?.id || row?.id || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="用户名">
|
||||||
|
{{ userAggregate?.username || row?.username || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="用户类型">
|
||||||
|
<el-tag :type="(userAggregate?.userType || row?.userType) === 2 ? 'warning' : (userAggregate?.userType || row?.userType) === 3 ? 'danger' : 'info'" size="small">
|
||||||
|
{{ userTypeMap[userAggregate?.userType || row?.userType] || '未知' }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="状态">
|
||||||
|
<el-tag v-if="(userAggregate?.status || row?.status) === 1" type="success" size="small">启用</el-tag>
|
||||||
|
<el-tag v-else type="danger" size="small">禁用</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="手机号">
|
||||||
|
{{ userAggregate?.phone || row?.phone || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="邮箱">
|
||||||
|
{{ userAggregate?.email || row?.email || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间" :span="2">
|
||||||
|
{{ (userAggregate?.createdAt || row?.createdAt) ? new Date(userAggregate?.createdAt || row?.createdAt).toLocaleString() : '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="更新时间" :span="2">
|
||||||
|
{{ (userAggregate?.updatedAt || row?.updatedAt) ? new Date(userAggregate?.updatedAt || row?.updatedAt).toLocaleString() : '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<!-- 部门信息 -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<h4 class="text-sm font-bold mb-2">所属部门</h4>
|
||||||
|
<div v-if="userAggregate?.depts?.length" class="flex flex-wrap gap-2">
|
||||||
|
<el-tag
|
||||||
|
v-for="dept in userAggregate.depts"
|
||||||
|
:key="dept.deptId"
|
||||||
|
:type="dept.main ? 'primary' : 'info'"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ dept.deptName }}
|
||||||
|
<el-tag v-if="dept.main" type="danger" size="small" class="ml-1">主</el-tag>
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<el-empty v-else description="暂无部门信息" :image-size="60" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 角色信息 -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<h4 class="text-sm font-bold mb-2">角色</h4>
|
||||||
|
<div v-if="userAggregate?.roles?.length" class="flex flex-wrap gap-2">
|
||||||
|
<el-tag
|
||||||
|
v-for="role in userAggregate.roles"
|
||||||
|
:key="role.roleId"
|
||||||
|
type="success"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ role.roleName }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<el-empty v-else description="暂无角色信息" :image-size="60" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">
|
||||||
|
关闭
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 验证语法**
|
||||||
|
|
||||||
|
Run: `cd admin-ui && npx vue-tsc --noEmit --skipLibCheck src/views/user/info/UserDetailDialog.vue`
|
||||||
|
Expected: 无错误输出
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add admin-ui/src/views/user/info/UserDetailDialog.vue
|
||||||
|
git commit -m "feat(user): use aggregate API in user detail dialog to show depts and roles"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: 用户表单从聚合数据解析已选部门/角色
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `admin-ui/src/views/user/info/UserFormDialog.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 修改 watch 逻辑解析 deptIds 和 roleIds**
|
||||||
|
|
||||||
|
在 `watch(() => props.visible, async (val) => { ... })` 中,修改编辑时的数据处理:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
watch(() => props.visible, async (val) => {
|
||||||
|
if (val) {
|
||||||
|
await Promise.all([loadDeptTree(), loadPostList()])
|
||||||
|
|
||||||
|
if (props.row) {
|
||||||
|
// 从聚合数据解析已选部门ID和角色ID
|
||||||
|
const deptIds = props.row.depts?.map((d: any) => d.deptId) || []
|
||||||
|
const roleIds = props.row.roles?.map((r: any) => r.roleId) || []
|
||||||
|
|
||||||
|
// 先设置表单数据,确保 rules 能正确计算
|
||||||
|
form.value = {
|
||||||
|
...props.row,
|
||||||
|
password: '',
|
||||||
|
deptIds: deptIds,
|
||||||
|
postIds: props.row.postIds || [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
resetForm()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 可选 - 移除冗余的 userDeptService 导入**
|
||||||
|
|
||||||
|
由于不再需要在表单中调用 `userDeptService.listDeptIdsByUserId`,可以移除该导入(但保留 `assignDepts` 用于保存):
|
||||||
|
|
||||||
|
**注意:** 当前代码中没有直接调用 `listDeptIdsByUserId`,所以无需修改导入。`userDeptService` 仍在 `onSubmit` 中被使用。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 验证语法**
|
||||||
|
|
||||||
|
Run: `cd admin-ui && npx vue-tsc --noEmit --skipLibCheck src/views/user/info/UserFormDialog.vue`
|
||||||
|
Expected: 无错误输出
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add admin-ui/src/views/user/info/UserFormDialog.vue
|
||||||
|
git commit -m "feat(user): parse deptIds and roleIds from aggregate data in user form"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: 验证和最终检查
|
||||||
|
|
||||||
|
- [ ] **Step 1: 运行类型检查**
|
||||||
|
|
||||||
|
Run: `cd admin-ui && npx vue-tsc --noEmit --skipLibCheck`
|
||||||
|
Expected: 无错误输出
|
||||||
|
|
||||||
|
- [ ] **Step 2: 运行构建**
|
||||||
|
|
||||||
|
Run: `cd admin-ui && npm run build`
|
||||||
|
Expected: 构建成功,无错误
|
||||||
|
|
||||||
|
- [ ] **Step 3: 检查所有变更**
|
||||||
|
|
||||||
|
Run: `git log --oneline -5`
|
||||||
|
Expected: 看到 4 个提交:
|
||||||
|
- feat(user): add aggregate method to UserService...
|
||||||
|
- feat(user): add dept/role columns and tree filters to user list page
|
||||||
|
- feat(user): use aggregate API in user detail dialog...
|
||||||
|
- feat(user): parse deptIds and roleIds from aggregate data in user form
|
||||||
|
|
||||||
|
- [ ] **Step 4: 最终提交(可选,如果需要合并)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 如果需要,可以创建一个合并提交
|
||||||
|
git log --oneline -5
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 回滚计划
|
||||||
|
|
||||||
|
如果出现问题,可以按以下顺序回滚:
|
||||||
|
|
||||||
|
1. 回滚 Task 4: `git revert <task4-commit>`
|
||||||
|
2. 回滚 Task 3: `git revert <task3-commit>`
|
||||||
|
3. 回滚 Task 2: `git revert <task2-commit>`
|
||||||
|
4. 回滚 Task 1: `git revert <task1-commit>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 测试清单
|
||||||
|
|
||||||
|
- [ ] 用户列表页显示部门列(主部门高亮)
|
||||||
|
- [ ] 用户列表页显示角色列
|
||||||
|
- [ ] 部门树形筛选正常工作
|
||||||
|
- [ ] 角色树形筛选正常工作
|
||||||
|
- [ ] 同时筛选部门和角色正常工作
|
||||||
|
- [ ] 用户详情弹窗显示完整部门列表
|
||||||
|
- [ ] 用户详情弹窗显示完整角色列表
|
||||||
|
- [ ] 编辑用户时正确加载已选部门
|
||||||
|
- [ ] 编辑用户时正确加载已选角色
|
||||||
|
- [ ] 保存用户后数据正确刷新
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**计划状态**: 待评审
|
||||||
|
**下一步**: 用户评审通过后,使用 superpowers-subagent-driven-development 或 superpowers-executing-plans 执行
|
||||||
@@ -0,0 +1,483 @@
|
|||||||
|
# 前后端智能协作方案设计文档
|
||||||
|
|
||||||
|
> **文档版本**: v1.0
|
||||||
|
> **创建日期**: 2026-06-06
|
||||||
|
> **适用范围**: rui-frontend 所有前端项目(admin-ui、cashier-mobile、cashier-customer)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、问题背景
|
||||||
|
|
||||||
|
### 1.1 当前痛点
|
||||||
|
|
||||||
|
admin-ui 项目中存在**表单字段与后台 API 不同步**的问题:
|
||||||
|
|
||||||
|
1. **表单字段硬编码** - 19 个 FormDialog 组件全部采用手动定义字段(如 `UserFormDialog.vue` 中的 `username`, `password`, `userType` 等)
|
||||||
|
2. **缺少 TypeScript 类型** - Service 层大量使用 `any` 类型,没有与后端 DTO 对应的接口定义
|
||||||
|
3. **BaseService 泛型未充分利用** - `BaseService<T>` 定义了泛型,但子类没有传入具体类型
|
||||||
|
4. **API 文档未利用** - 后端已提供 `/v3/api-docs`(SpringDoc OpenAPI),但前端未使用
|
||||||
|
|
||||||
|
### 1.2 影响
|
||||||
|
|
||||||
|
- 后端字段变更时,前端需要手动修改所有相关表单
|
||||||
|
- 缺少编译时类型检查,运行时容易出错
|
||||||
|
- 前后端协作效率低,沟通成本高
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、设计目标
|
||||||
|
|
||||||
|
1. **类型安全** - 前端类型与后端 API 自动同步
|
||||||
|
2. **减少样板代码** - 表单配置化,减少 70% 重复代码
|
||||||
|
3. **支持自定义扩展** - 复杂表单可自定义字段和逻辑
|
||||||
|
4. **多模块支持** - 适配微服务架构(聚合启动器 + 独立服务)
|
||||||
|
5. **自动化检测** - 防止前后端不同步上线
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、架构设计
|
||||||
|
|
||||||
|
### 3.1 整体架构
|
||||||
|
|
||||||
|
```
|
||||||
|
API设计规范.md (单一事实来源)
|
||||||
|
│
|
||||||
|
├── 包含所有 api-docs 地址
|
||||||
|
└── 由后端团队维护
|
||||||
|
|
||||||
|
↓ 自动解析
|
||||||
|
|
||||||
|
scripts/parse-api-config.ts
|
||||||
|
│
|
||||||
|
└── 读取并解析 Markdown 中的 URL
|
||||||
|
└── 识别聚合启动器和独立服务
|
||||||
|
|
||||||
|
↓ 动态配置
|
||||||
|
|
||||||
|
scripts/generate-api.ts
|
||||||
|
│
|
||||||
|
├── 聚合启动器模块(通过 group 参数获取)
|
||||||
|
├── 独立服务模块(直接访问)
|
||||||
|
├── 生成类型定义文件
|
||||||
|
├── 生成统一导出
|
||||||
|
└── 生成模块映射
|
||||||
|
|
||||||
|
↓ 类型文件
|
||||||
|
|
||||||
|
src/types/
|
||||||
|
├── system-api.d.ts # 系统服务类型
|
||||||
|
├── user-api.d.ts # 用户服务类型
|
||||||
|
├── cashier-api.d.ts # 收银服务类型
|
||||||
|
├── api.d.ts # 统一导出
|
||||||
|
└── api-modules.json # 模块映射
|
||||||
|
|
||||||
|
↓ 类型驱动
|
||||||
|
|
||||||
|
前端代码
|
||||||
|
│
|
||||||
|
├── Service 层(类型安全)
|
||||||
|
├── useApiForm() 组合式函数
|
||||||
|
├── ApiFormDialog 组件
|
||||||
|
└── 自定义扩展(插槽机制)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 核心组件
|
||||||
|
|
||||||
|
| 组件 | 职责 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `parse-api-config.ts` | 解析 API 规范文档 | 从 Markdown 读取 api-docs 地址 |
|
||||||
|
| `generate-api.ts` | 生成类型定义 | 调用 openapi-typescript 生成 .d.ts |
|
||||||
|
| `useApiForm()` | 类型驱动表单管理 | 组合式函数,自动生成表单配置 |
|
||||||
|
| `ApiFormDialog` | 配置化表单组件 | 根据字段配置自动渲染表单 |
|
||||||
|
| `BaseService<T>` | 类型安全 Service 基类 | 保留现有架构,增强类型 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、详细设计
|
||||||
|
|
||||||
|
### 4.1 API 配置解析(parse-api-config.ts)
|
||||||
|
|
||||||
|
**核心功能**:从 `API设计规范.md` 自动读取 api-docs 地址
|
||||||
|
|
||||||
|
**聚合启动器识别规则**:
|
||||||
|
- 端口 9399 → 聚合启动器,包含多个模块(/user, /system)
|
||||||
|
- 通过 `?group=xxx` 参数获取子模块
|
||||||
|
- 其他端口 → 独立服务
|
||||||
|
|
||||||
|
**配置结构**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ApiModuleConfig {
|
||||||
|
name: string // 模块名:user, system, cashier
|
||||||
|
url: string // api-docs URL
|
||||||
|
output: string // 输出路径
|
||||||
|
prefix: string // API 路径前缀
|
||||||
|
description: string // 模块描述
|
||||||
|
aggregator?: string // 所属聚合器(如果有)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**解析示例**:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# API设计规范.md
|
||||||
|
|
||||||
|
http://localhost:9399/v3/api-docs # 聚合启动器 API 文档(开发调试)
|
||||||
|
http://localhost:9601/v3/api-docs # 收银服务 API 文档
|
||||||
|
```
|
||||||
|
|
||||||
|
解析结果:
|
||||||
|
- `9399` → 聚合启动器 → 展开为 user、system 两个模块
|
||||||
|
- `9601` → 独立服务 → cashier 模块
|
||||||
|
|
||||||
|
### 4.2 类型生成(generate-api.ts)
|
||||||
|
|
||||||
|
**生成流程**:
|
||||||
|
|
||||||
|
1. 读取 API设计规范.md
|
||||||
|
2. 识别聚合启动器和独立服务
|
||||||
|
3. 为每个模块生成类型文件
|
||||||
|
4. 生成统一导出文件
|
||||||
|
5. 生成模块映射文件
|
||||||
|
|
||||||
|
**输出文件结构**:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/types/
|
||||||
|
├── user-api.d.ts # 用户服务类型(来自聚合启动器)
|
||||||
|
├── system-api.d.ts # 系统服务类型(来自聚合启动器)
|
||||||
|
├── cashier-api.d.ts # 收银服务类型(独立服务)
|
||||||
|
├── api.d.ts # 统一导出
|
||||||
|
│ └── export type * from './user-api'
|
||||||
|
│ └── export type * from './system-api'
|
||||||
|
│ └── export type * from './cashier-api'
|
||||||
|
└── api-modules.json # 模块映射(用于运行时)
|
||||||
|
└── {
|
||||||
|
└── "aggregator": [
|
||||||
|
└── { "name": "user", "prefix": "/user" },
|
||||||
|
└── { "name": "system", "prefix": "/system" }
|
||||||
|
└── ]
|
||||||
|
└── }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 类型驱动表单(useApiForm)
|
||||||
|
|
||||||
|
**核心功能**:根据 TypeScript 类型自动生成表单配置
|
||||||
|
|
||||||
|
**接口设计**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface FieldConfig<T = any> {
|
||||||
|
key: keyof T // 字段名(类型安全)
|
||||||
|
label: string // 字段标签
|
||||||
|
type: FieldType // 字段类型
|
||||||
|
required?: boolean // 是否必填
|
||||||
|
rules?: any[] // 验证规则
|
||||||
|
options?: Option[] // 选项(select/radio/checkbox)
|
||||||
|
disabled?: boolean | ((form: T) => boolean)
|
||||||
|
placeholder?: string
|
||||||
|
props?: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormConfig<T> {
|
||||||
|
initial?: Partial<T> // 初始值
|
||||||
|
fields: FieldConfig<T>[] // 字段配置
|
||||||
|
onSubmit: (data: T) => Promise<void>
|
||||||
|
beforeSubmit?: (data: T) => boolean | string
|
||||||
|
}
|
||||||
|
|
||||||
|
function useApiForm<T extends Record<string, any>>(
|
||||||
|
config: FormConfig<T>
|
||||||
|
): {
|
||||||
|
formRef: Ref<FormInstance>
|
||||||
|
form: Ref<Partial<T>>
|
||||||
|
rules: ComputedRef<Record<string, any[]>>
|
||||||
|
loading: Ref<boolean>
|
||||||
|
handleSubmit: () => Promise<boolean>
|
||||||
|
resetForm: (initial?: Partial<T>) => void
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 配置化表单组件(ApiFormDialog)
|
||||||
|
|
||||||
|
**核心功能**:根据字段配置自动渲染表单元素
|
||||||
|
|
||||||
|
**支持的字段类型**:
|
||||||
|
|
||||||
|
| 类型 | 组件 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `input` | ElInput | 文本输入 |
|
||||||
|
| `textarea` | ElInput(type="textarea") | 多行文本 |
|
||||||
|
| `select` | ElSelect | 下拉选择 |
|
||||||
|
| `radio` | ElRadioGroup | 单选按钮 |
|
||||||
|
| `checkbox` | ElCheckboxGroup | 多选框 |
|
||||||
|
| `number` | ElInputNumber | 数字输入 |
|
||||||
|
| `tree-select` | ElTreeSelect | 树形选择 |
|
||||||
|
| `date` | ElDatePicker | 日期选择 |
|
||||||
|
| `datetime` | ElDatePicker | 日期时间选择 |
|
||||||
|
|
||||||
|
**自定义扩展机制**:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<ApiFormDialog
|
||||||
|
v-model:visible="visible"
|
||||||
|
v-model:form="form"
|
||||||
|
:fields="fields"
|
||||||
|
:rules="rules"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
>
|
||||||
|
<!-- 自定义字段插槽 -->
|
||||||
|
<template #custom-fields="{ form }">
|
||||||
|
<el-form-item label="自定义部门">
|
||||||
|
<el-tree-select v-model="customDeptIds" :data="deptTree" />
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
</ApiFormDialog>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5 Service 层增强
|
||||||
|
|
||||||
|
**保留现有 BaseService 架构**,增强类型安全:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 现有架构保持不变
|
||||||
|
class BaseService<T = any> {
|
||||||
|
protected baseUrl: string
|
||||||
|
|
||||||
|
async page(params: PageParams & Record<string, any>): Promise<PageResult<T>>
|
||||||
|
async list(params?: Record<string, any>): Promise<T[]>
|
||||||
|
async getById(id: number | string): Promise<T>
|
||||||
|
async add(data: Partial<T>): Promise<T>
|
||||||
|
async update(data: Partial<T> & { id: number | string }): Promise<boolean>
|
||||||
|
async remove(id: number | string): Promise<boolean>
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用时传入具体类型
|
||||||
|
class UserService extends BaseService<UserDTO> {
|
||||||
|
constructor() {
|
||||||
|
super('/user/admin/user')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、使用示例
|
||||||
|
|
||||||
|
### 5.1 标准表单(完全配置化)
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useApiForm } from '@/composables/useApiForm'
|
||||||
|
import { userService } from '@/service/user/userService'
|
||||||
|
import type { components } from '@/types/user-api'
|
||||||
|
|
||||||
|
type UserDTO = components['schemas']['UserDTO']
|
||||||
|
|
||||||
|
const { formRef, form, rules, loading, handleSubmit, resetForm } = useApiForm<UserDTO>({
|
||||||
|
initial: { userType: 1, status: 1 },
|
||||||
|
fields: [
|
||||||
|
{ key: 'username', label: '用户名', type: 'input', required: true },
|
||||||
|
{ key: 'password', label: '密码', type: 'input', required: true, props: { type: 'password' } },
|
||||||
|
{ key: 'userType', label: '用户类型', type: 'select', options: [
|
||||||
|
{ label: '普通用户', value: 1 },
|
||||||
|
{ label: '管理员', value: 2 }
|
||||||
|
]},
|
||||||
|
{ key: 'status', label: '状态', type: 'radio', options: [
|
||||||
|
{ label: '启用', value: 1 },
|
||||||
|
{ label: '禁用', value: 0 }
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
onSubmit: async (data) => {
|
||||||
|
await userService.add(data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ApiFormDialog
|
||||||
|
v-model:visible="visible"
|
||||||
|
v-model:form="form"
|
||||||
|
title="新增用户"
|
||||||
|
:fields="fields"
|
||||||
|
:rules="rules"
|
||||||
|
:loading="loading"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 自定义扩展表单
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useApiForm } from '@/composables/useApiForm'
|
||||||
|
import { roleService } from '@/service/system/roleService'
|
||||||
|
import type { components } from '@/types/system-api'
|
||||||
|
|
||||||
|
type RoleDTO = components['schemas']['RoleDTO']
|
||||||
|
|
||||||
|
const selectedDeptIds = ref<number[]>([])
|
||||||
|
const showCustomDept = computed(() => form.value.dataScope === 5)
|
||||||
|
|
||||||
|
const { form, fields, rules, handleSubmit } = useApiForm<RoleDTO>({
|
||||||
|
fields: [
|
||||||
|
{ key: 'roleCode', label: '角色编码', type: 'input', required: true },
|
||||||
|
{ key: 'roleName', label: '角色名称', type: 'input', required: true },
|
||||||
|
{ key: 'dataScope', label: '数据范围', type: 'select', options: [
|
||||||
|
{ label: '全部', value: 1 },
|
||||||
|
{ label: '自定义', value: 5 }
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
onSubmit: async (data) => {
|
||||||
|
// 自定义验证
|
||||||
|
if (data.dataScope === 5 && selectedDeptIds.value.length === 0) {
|
||||||
|
throw new Error('请选择部门')
|
||||||
|
}
|
||||||
|
await roleService.add(data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ApiFormDialog
|
||||||
|
v-model:visible="visible"
|
||||||
|
v-model:form="form"
|
||||||
|
:fields="fields"
|
||||||
|
:rules="rules"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
>
|
||||||
|
<template #custom-fields>
|
||||||
|
<el-form-item v-if="showCustomDept" label="选择部门" required>
|
||||||
|
<el-tree-select v-model="selectedDeptIds" :data="deptTree" />
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
</ApiFormDialog>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、自动化机制
|
||||||
|
|
||||||
|
### 6.1 Git Hook(提交前检查)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// scripts/check-api-changes.ts
|
||||||
|
function checkApiChanges() {
|
||||||
|
const currentHash = execSync('git hash-object src/types/*.d.ts').toString()
|
||||||
|
|
||||||
|
try {
|
||||||
|
execSync('pnpm api:generate', { stdio: 'pipe' })
|
||||||
|
const newHash = execSync('git hash-object src/types/*.d.ts').toString()
|
||||||
|
|
||||||
|
if (currentHash !== newHash) {
|
||||||
|
console.error('❌ API types have changed!')
|
||||||
|
console.error('Please run "pnpm api:generate" and commit the changes.')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('⚠️ Could not check API changes')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 脚本命令
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"api:generate": "tsx scripts/generate-api.ts",
|
||||||
|
"api:check": "tsx scripts/check-api-changes.ts",
|
||||||
|
"api:watch": "nodemon --watch ../docs/standards/API设计规范.md --exec 'pnpm api:generate'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、扩展性设计
|
||||||
|
|
||||||
|
### 7.1 新增模块
|
||||||
|
|
||||||
|
当后端新增模块时,只需在 `API设计规范.md` 中添加:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
http://localhost:9399/v3/api-docs?group=订单服务 # 订单服务 API 文档
|
||||||
|
http://localhost:9701/v3/api-docs # 新独立服务 API 文档
|
||||||
|
```
|
||||||
|
|
||||||
|
前端自动识别:
|
||||||
|
- 端口 9399 → 聚合启动器,通过 `group` 参数获取
|
||||||
|
- 新端口 9701 → 独立服务,直接访问
|
||||||
|
|
||||||
|
### 7.2 新增字段类型
|
||||||
|
|
||||||
|
在 `ApiFormDialog.vue` 中添加新的字段类型渲染逻辑即可。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、实施计划
|
||||||
|
|
||||||
|
### Phase 1: 基础搭建(1-2 天)
|
||||||
|
|
||||||
|
1. 安装依赖:`openapi-typescript`, `tsx`, `nodemon`
|
||||||
|
2. 创建 `scripts/parse-api-config.ts` - 解析 API 规范
|
||||||
|
3. 创建 `scripts/generate-api.ts` - 生成类型
|
||||||
|
4. 配置 package.json 脚本
|
||||||
|
|
||||||
|
### Phase 2: 类型生成(1 天)
|
||||||
|
|
||||||
|
1. 运行 `pnpm api:generate` 生成初始类型
|
||||||
|
2. 验证类型正确性
|
||||||
|
3. 提交生成的类型文件
|
||||||
|
|
||||||
|
### Phase 3: 表单工具(2-3 天)
|
||||||
|
|
||||||
|
1. 创建 `useApiForm()` 组合式函数
|
||||||
|
2. 创建 `ApiFormDialog.vue` 组件
|
||||||
|
3. 编写单元测试
|
||||||
|
|
||||||
|
### Phase 4: 试点迁移(2-3 天)
|
||||||
|
|
||||||
|
1. 选择 2-3 个简单表单进行迁移
|
||||||
|
2. 验证方案可行性
|
||||||
|
3. 收集反馈优化
|
||||||
|
|
||||||
|
### Phase 5: 全面推广(1-2 周)
|
||||||
|
|
||||||
|
1. 迁移所有标准表单
|
||||||
|
2. 保留自定义表单的特殊逻辑
|
||||||
|
3. 编写迁移文档
|
||||||
|
|
||||||
|
### Phase 6: 自动化(1 天)
|
||||||
|
|
||||||
|
1. 配置 Git Hook
|
||||||
|
2. 配置 CI/CD 检查
|
||||||
|
3. 编写使用文档
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、风险评估
|
||||||
|
|
||||||
|
| 风险 | 影响 | 缓解措施 |
|
||||||
|
|------|------|---------|
|
||||||
|
| 后端 API 文档不准确 | 生成的类型错误 | 建立 API 文档审核机制 |
|
||||||
|
| 聚合启动器不可用 | 无法生成类型 | 支持独立服务直接访问 |
|
||||||
|
| 复杂表单迁移困难 | 影响进度 | 保留自定义扩展机制 |
|
||||||
|
| 团队成员学习成本 | 初期效率下降 | 提供详细文档和示例 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、成功标准
|
||||||
|
|
||||||
|
1. ✅ 所有 Service 层使用具体类型替代 `any`
|
||||||
|
2. ✅ 标准表单代码量减少 70%
|
||||||
|
3. ✅ 后端字段变更时,前端编译期即可发现
|
||||||
|
4. ✅ 新增模块时,类型自动生成
|
||||||
|
5. ✅ 提交时自动检测 API 变更
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **下一步**: 编写实现计划(implementation plan)
|
||||||
@@ -0,0 +1,469 @@
|
|||||||
|
# SysApp(第三方应用集成)管理界面设计规范
|
||||||
|
|
||||||
|
**工单**: rui/rui-frontend#4 — [UI-REQ] 后台增加 SysApp(第三方应用集成)管理界面
|
||||||
|
**日期**: 2026-06-07
|
||||||
|
**关联 Issue**: rui/rui-framework#4(文件上传接口待提供,前端先用 JSON 占位)
|
||||||
|
**优先级**: P1
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 目标(Goal)
|
||||||
|
|
||||||
|
在 admin-ui 后台实现 **SysApp(第三方应用集成)管理模块**,为运营/管理员提供对第三方平台应用(微信、支付宝、Stripe 等)凭证信息的统一管理能力。模块包含列表展示、新增、编辑、删除、启停 5 个标准操作,遵循现有 `oauth2-client` 等模块的 CRUD 模式(`BaseService` + `RuiTable` + `FormDialog`),不引入新依赖。
|
||||||
|
|
||||||
|
## 2. 非目标(Non-Goals)
|
||||||
|
|
||||||
|
明确不在本期范围内的事项:
|
||||||
|
|
||||||
|
- **不修改后端**:本 Spec 仅涉及 admin-ui 前端;不修改 rui-framework / rui-cashier 等后端仓库代码或 API。
|
||||||
|
- **不实现 certificates 文件上传 UI**:因 rui-framework 尚未提供文件上传接口(已提 Issue #4),certificates 字段本期用 JSON textarea 占位实现,标注「待后端文件上传接口就绪后升级」。
|
||||||
|
- **不实现 isEncrypted 字段 UI**:后端为该字段预留,前端暂不展示。
|
||||||
|
- **不修改 cashier-mobile / cashier-customer**:本 Spec 仅涉及 admin-ui。
|
||||||
|
- **不引入 monaco-editor / @guolao/vue-monaco-editor 等新依赖**:证书编辑用原生 textarea + JSON.parse 校验。
|
||||||
|
- **不修改 rui-framework 菜单 JSON 文件**:工单提到的 `data/menus/system.json` 属于 rui-framework 仓库菜单管理数据,不在本仓库范围内。
|
||||||
|
- **不做多租户隔离增强**:tenantId 字段由后端按上下文自动填充,前端不主动设置。
|
||||||
|
|
||||||
|
## 3. 背景与上下文(Context)
|
||||||
|
|
||||||
|
### 3.1 后端接口现状
|
||||||
|
|
||||||
|
- **Controller**: `SysAppController`,继承 `BaseController`,自动具备 5 个标准操作:
|
||||||
|
- `GET /system/admin/app/page` — 分页查询
|
||||||
|
- `GET /system/admin/app/list` — 列表查询
|
||||||
|
- `GET /system/admin/app/{id}` — 详情
|
||||||
|
- `POST /system/admin/app` — 新增
|
||||||
|
- `PUT /system/admin/app` — 修改
|
||||||
|
- `DELETE /system/admin/app/{id}` — 删除
|
||||||
|
- `DELETE /system/admin/app/batch` — 批量删除
|
||||||
|
- `PUT /system/admin/app/status` — 启停
|
||||||
|
- `GET /system/admin/app/export` — 导出
|
||||||
|
- `POST /system/admin/app/import` — 导入
|
||||||
|
- **鉴权**: `@AutoPermission("sys:app")`
|
||||||
|
- **Swagger**: `http://localhost:9302/swagger-ui.html`
|
||||||
|
- **后端 commit**: 27fa187(表)+ 29a9389(Service/Controller)+ 13b20ab(Result 规范)
|
||||||
|
|
||||||
|
### 3.2 前端代码基础
|
||||||
|
|
||||||
|
- **BaseService** (`admin-ui/src/service/BaseService.ts`) 已提供完整 CRUD 抽象,子类只需传 baseUrl:
|
||||||
|
```ts
|
||||||
|
class SysAppService extends BaseService {
|
||||||
|
constructor() { super('/system/admin/app') }
|
||||||
|
}
|
||||||
|
export const sysAppService = new SysAppService()
|
||||||
|
```
|
||||||
|
- **RuiTable** 组件支持查询区、工具栏、列配置、slot、分页、导出、列设置、刷新、批量操作等开箱即用能力。
|
||||||
|
- **OAuth2Client 页面**(`views/system/oauth2-client/Index.vue` + `OAuth2ClientFormDialog.vue`)是最相似的现有实现,本期直接照搬其模式。
|
||||||
|
|
||||||
|
### 3.3 路由与菜单
|
||||||
|
|
||||||
|
- admin-ui 路由采用 **前端硬编码 + i18n 键** 方式(`admin-ui/src/router/modules/system.ts`)。
|
||||||
|
- 工单中提到的 `data/menus/system.json` 是 rui-framework 后端「菜单管理」功能加载的菜单数据,不影响 admin-ui 路由配置。
|
||||||
|
|
||||||
|
## 4. 关键设计决策(用户已确认)
|
||||||
|
|
||||||
|
| # | 决策项 | 选定方案 |
|
||||||
|
|---|--------|---------|
|
||||||
|
| 1 | 菜单归属 | 作为「系统管理」的子菜单,路由 `/system/app` |
|
||||||
|
| 2 | 表单布局 | el-tabs 分 4 Tab(基础信息 / 凭证信息 / 接口配置 / 高级) |
|
||||||
|
| 3 | certificates 字段 | 前端 JSON textarea 占位(待 rui/rui-framework#4 文件上传接口就绪后升级) |
|
||||||
|
| 4 | 敏感字段(appSecret/appKey/aesKey) | 列表展示 6 个星号 `******`,编辑时留空表示不修改 |
|
||||||
|
|
||||||
|
## 5. 字段定义(共 21 个,UI 涉及 19 个)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | UI 控件 | Tab | 说明 |
|
||||||
|
|------|------|------|---------|-----|------|
|
||||||
|
| id | Long | — | (仅后端) | — | 主键 |
|
||||||
|
| tenantId | Long | — | (仅后端) | — | 租户ID 0=系统级(自动填充) |
|
||||||
|
| ownerType | String | 是 | el-select | 1 | PLATFORM / TENANT |
|
||||||
|
| platform | String | 是 | el-select | 1 | wechat / alipay / stripe |
|
||||||
|
| name | String | 是 | el-input | 1 | 管理用名称 |
|
||||||
|
| appId | String | 否 | el-input | 2 | 应用ID(UNIQUE) |
|
||||||
|
| appSecret | String | 否 | el-input (password) | 2 | **敏感**:列表脱敏,编辑留空不修改 |
|
||||||
|
| appKey | String | 否 | el-input (password) | 2 | **敏感** |
|
||||||
|
| certificates | String | 否 | JSON textarea | 2 | 多证书 JSON 数组(**占位**) |
|
||||||
|
| aesKey | String | 否 | el-input (password) | 2 | **敏感** |
|
||||||
|
| redirectUri | String | 否 | el-input | 3 | OAuth2 回调地址 |
|
||||||
|
| merchantId | String | 否 | el-input | 3 | 商户号 |
|
||||||
|
| signType | String | 否 | el-select | 3 | RSA2 / MD5 / HMAC |
|
||||||
|
| notifyUrl | String | 否 | el-input | 3 | 支付回调 |
|
||||||
|
| apiBase | String | 否 | el-input | 3 | API 根地址 |
|
||||||
|
| isSandbox | 0/1 | 否 | el-switch | 4 | 是否沙箱环境 |
|
||||||
|
| extra | String | 否 | JSON textarea | 4 | JSON 扩展 |
|
||||||
|
| isEncrypted | 0/1 | — | **不展示** | — | 预留加密字段(暂不实现) |
|
||||||
|
| status | 0/1 | 否 | el-switch | 4 | 启用/禁用,默认 1 |
|
||||||
|
| description | String | 否 | el-input (textarea) | 1 | 备注 |
|
||||||
|
| sortNo | Int | 否 | el-input-number | 1 | 排序号 |
|
||||||
|
|
||||||
|
### 5.1 枚举映射(UI 展示用)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const platformMap: Record<string, { label: string; type: 'primary' | 'success' | 'warning' }> = {
|
||||||
|
wechat: { label: '微信', type: 'success' },
|
||||||
|
alipay: { label: '支付宝', type: 'primary' },
|
||||||
|
stripe: { label: 'Stripe', type: 'warning' },
|
||||||
|
}
|
||||||
|
|
||||||
|
const ownerTypeMap: Record<string, { label: string; type: 'primary' | 'success' }> = {
|
||||||
|
PLATFORM: { label: '平台级', type: 'primary' },
|
||||||
|
TENANT: { label: '租户级', type: 'success' },
|
||||||
|
}
|
||||||
|
|
||||||
|
const signTypeOptions = [
|
||||||
|
{ label: 'RSA2', value: 'RSA2' },
|
||||||
|
{ label: 'MD5', value: 'MD5' },
|
||||||
|
{ label: 'HMAC', value: 'HMAC' },
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 涉及文件清单(Files To Change)
|
||||||
|
|
||||||
|
| # | 文件 | 操作 | 用途 |
|
||||||
|
|---|------|------|------|
|
||||||
|
| 1 | `admin-ui/src/service/system/sysAppService.ts` | 新建 | 业务 Service,继承 `BaseService('/system/admin/app')` |
|
||||||
|
| 2 | `admin-ui/src/service/system/index.ts` | 改动 | 追加 `export { sysAppService } from './sysAppService'` |
|
||||||
|
| 3 | `admin-ui/src/router/modules/system.ts` | 改动 | 新增 `system/app` 路由,meta.i18n 键 `menu.systemApp` |
|
||||||
|
| 4 | `admin-ui/src/locales/zh-CN.ts` | 改动 | `menu.systemApp: '应用集成'` |
|
||||||
|
| 5 | `admin-ui/src/locales/en-US.ts` | 改动 | `menu.systemApp: 'App Integration'` |
|
||||||
|
| 6 | `admin-ui/src/views/system/app/Index.vue` | 新建 | 列表页(RuiTable + 操作工具栏) |
|
||||||
|
| 7 | `admin-ui/src/views/system/app/SysAppFormDialog.vue` | 新建 | 新增/编辑弹窗(4 Tab) |
|
||||||
|
|
||||||
|
**变更统计**:新建 3 个文件,修改 4 个文件。**总计 7 个文件**。
|
||||||
|
|
||||||
|
## 7. 列表页设计(`views/system/app/Index.vue`)
|
||||||
|
|
||||||
|
### 7.1 结构
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-bold mb-4">{{ $t('menu.systemApp') }}</h2>
|
||||||
|
<RuiTable
|
||||||
|
ref="tableRef"
|
||||||
|
:columns="columns"
|
||||||
|
:load-data="loadData"
|
||||||
|
:show-selection="true"
|
||||||
|
:exportable="true"
|
||||||
|
export-filename="SysApp应用集成列表"
|
||||||
|
>
|
||||||
|
<!-- 查询区、工具栏、列 slot、操作 slot -->
|
||||||
|
</RuiTable>
|
||||||
|
<SysAppFormDialog v-model:visible="dialogVisible" :row="currentRow" @success="handleFormSuccess" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 查询区(4 个条件)
|
||||||
|
|
||||||
|
| 控件 | 字段 | 控件类型 | 选项 |
|
||||||
|
|------|------|---------|------|
|
||||||
|
| 应用名称 | `name` | `el-input` (clearable) | — |
|
||||||
|
| 平台 | `platform` | `el-select` (clearable) | wechat / alipay / stripe |
|
||||||
|
| 所有者类型 | `ownerType` | `el-select` (clearable) | PLATFORM / TENANT |
|
||||||
|
| 状态 | `status` | `el-select` (clearable) | 启用(1) / 禁用(0) |
|
||||||
|
|
||||||
|
### 7.3 列配置(columns)
|
||||||
|
|
||||||
|
| prop | label | 宽度/最小宽度 | slot | 备注 |
|
||||||
|
|------|-------|--------------|------|------|
|
||||||
|
| name | 应用名称 | minWidth 150 | — | — |
|
||||||
|
| platform | 平台 | width 100 | 是 | 彩色 Tag |
|
||||||
|
| ownerType | 所有者 | width 100 | 是 | PLATFORM 蓝、TENANT 绿 |
|
||||||
|
| appId | 应用ID | minWidth 120 | — | 列表不脱敏(UNIQUE 标识) |
|
||||||
|
| status | 状态 | width 90 | 是 | Switch |
|
||||||
|
| createdAt | 创建时间 | minWidth 180 | — | dataType='dateTime',sortable='custom' |
|
||||||
|
|
||||||
|
> **脱敏说明**:appSecret / appKey / aesKey 三个敏感字段**不进入列表列**,仅在编辑弹窗中处理。列表中没有任何明文密钥展示位。
|
||||||
|
|
||||||
|
### 7.4 工具栏
|
||||||
|
|
||||||
|
- **左侧**:新增应用按钮(`type="primary"`)
|
||||||
|
- **右侧**:批量删除(基于 `show-selection`) + 导出 + 刷新 + 列设置(RuiTable 内置)
|
||||||
|
|
||||||
|
### 7.5 行操作
|
||||||
|
|
||||||
|
- 编辑
|
||||||
|
- 删除(ElMessageBox 二次确认,删除成功后 `tableRef.refresh()`)
|
||||||
|
|
||||||
|
### 7.6 启停切换
|
||||||
|
|
||||||
|
```ts
|
||||||
|
async function handleStatusChange(row: any, status: number) {
|
||||||
|
if (!row?.id) return
|
||||||
|
try {
|
||||||
|
await sysAppService.changeStatus(row.id, status)
|
||||||
|
ElMessage.success(status === 1 ? '启用成功' : '禁用成功')
|
||||||
|
} catch {
|
||||||
|
row.status = status === 1 ? 0 : 1 // 失败回滚
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 表单弹窗设计(`views/system/app/SysAppFormDialog.vue`)
|
||||||
|
|
||||||
|
### 8.1 弹窗基本属性
|
||||||
|
|
||||||
|
- 宽度:`760px`
|
||||||
|
- title:编辑时「编辑应用」/ 新增时「新增应用」
|
||||||
|
- 关闭点击遮罩:`close-on-click-modal="false"`
|
||||||
|
- props:`visible: boolean`、`row: any`
|
||||||
|
- emits:`update:visible`、`success`
|
||||||
|
|
||||||
|
### 8.2 数据加载流程
|
||||||
|
|
||||||
|
```
|
||||||
|
watch(visible, val => {
|
||||||
|
if (val) {
|
||||||
|
if (props.row) {
|
||||||
|
// 编辑:拉取详情(确保拿到完整字段,包括敏感字段的明文用于编辑回显)
|
||||||
|
sysAppService.getById(props.row.id).then(data => { form.value = { ...data } })
|
||||||
|
} else {
|
||||||
|
// 新增:重置为默认值
|
||||||
|
form.value = { ...defaultForm }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.3 默认值(新增时)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const defaultForm = {
|
||||||
|
id: undefined,
|
||||||
|
ownerType: 'PLATFORM',
|
||||||
|
platform: 'wechat',
|
||||||
|
name: '',
|
||||||
|
appId: '',
|
||||||
|
appSecret: '',
|
||||||
|
appKey: '',
|
||||||
|
certificates: '',
|
||||||
|
aesKey: '',
|
||||||
|
redirectUri: '',
|
||||||
|
merchantId: '',
|
||||||
|
signType: 'RSA2',
|
||||||
|
notifyUrl: '',
|
||||||
|
apiBase: '',
|
||||||
|
isSandbox: 0,
|
||||||
|
extra: '',
|
||||||
|
status: 1,
|
||||||
|
description: '',
|
||||||
|
sortNo: 0,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.4 4 Tab 分布
|
||||||
|
|
||||||
|
#### Tab 1:基础信息
|
||||||
|
|
||||||
|
```
|
||||||
|
- name* el-input (必填)
|
||||||
|
- ownerType* el-select (必填, PLATFORM / TENANT)
|
||||||
|
- platform* el-select (必填, wechat / alipay / stripe)
|
||||||
|
- description el-input (textarea, :rows="2")
|
||||||
|
- sortNo el-input-number
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Tab 2:凭证信息
|
||||||
|
|
||||||
|
```
|
||||||
|
- appId el-input
|
||||||
|
- appSecret el-input (type="password" show-password)
|
||||||
|
- appKey el-input (type="password" show-password)
|
||||||
|
- aesKey el-input (type="password" show-password)
|
||||||
|
- certificates el-input (type="textarea" :rows="4")
|
||||||
|
placeholder='[{"name":"cert1","content":"<PEM 内容>"}]'
|
||||||
|
下方 helper text:「多证书 JSON 数组。格式:[{name, content}]。待后端文件上传接口就绪后升级为文件上传。」
|
||||||
|
```
|
||||||
|
|
||||||
|
> **敏感字段编辑规则**:appSecret / appKey / aesKey 三个字段在编辑时**留空 = 不修改原值**。这一规则完全照搬 `OAuth2ClientFormDialog` 的现有做法,保证行为一致。
|
||||||
|
|
||||||
|
#### Tab 3:接口配置
|
||||||
|
|
||||||
|
```
|
||||||
|
- redirectUri el-input
|
||||||
|
- notifyUrl el-input
|
||||||
|
- apiBase el-input
|
||||||
|
- merchantId el-input
|
||||||
|
- signType el-select (RSA2 / MD5 / HMAC)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Tab 4:高级
|
||||||
|
|
||||||
|
```
|
||||||
|
- isSandbox el-switch (0/1)
|
||||||
|
- extra el-input (type="textarea" :rows="4")
|
||||||
|
下方 helper text:「JSON 扩展字段,提交前需通过 JSON 格式校验」
|
||||||
|
- status el-switch (0/1,默认 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.5 校验
|
||||||
|
|
||||||
|
- **必填字段**:`name`、`ownerType`、`platform`
|
||||||
|
- **JSON 字段**:certificates 和 extra 字段在提交前调用 `validateJSON()` 校验,非空时尝试 `JSON.parse`,失败则 `ElMessage.error('xxx 字段 JSON 格式错误')` 并阻止提交。
|
||||||
|
|
||||||
|
```ts
|
||||||
|
function validateJSON(value: string, fieldName: string): boolean {
|
||||||
|
if (!value || !value.trim()) return true // 空值允许
|
||||||
|
try {
|
||||||
|
JSON.parse(value)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
ElMessage.error(`${fieldName} JSON 格式错误`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.6 提交逻辑
|
||||||
|
|
||||||
|
```ts
|
||||||
|
async function handleSubmit() {
|
||||||
|
await formRef.value.validate()
|
||||||
|
if (!validateJSON(form.value.certificates, 'certificates')) return
|
||||||
|
if (!validateJSON(form.value.extra, 'extra')) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const isEdit = !!form.value.id
|
||||||
|
const success = isEdit
|
||||||
|
? await sysAppService.update(form.value as any)
|
||||||
|
: await sysAppService.add(form.value)
|
||||||
|
if (success !== false) {
|
||||||
|
ElMessage.success(isEdit ? '修改成功' : '新增成功')
|
||||||
|
emit('success')
|
||||||
|
dialogVisible.value = false
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 错误已由请求拦截器统一提示
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. Service 层设计(`service/system/sysAppService.ts`)
|
||||||
|
|
||||||
|
完整实现(参考 `oauth2ClientService.ts` 的极简模式):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { BaseService } from '../BaseService'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SysApp(第三方应用集成)服务
|
||||||
|
*
|
||||||
|
* <p>负责与后端 /system/admin/app 接口的通信,继承 BaseService 获得标准 CRUD 能力。</p>
|
||||||
|
*
|
||||||
|
* <p><b>后续升级路径</b>:后端文件上传接口(rui/rui-framework#4)就绪后,可在此文件追加
|
||||||
|
* `uploadCertificate(file: File)` 方法,并将表单中 certificates 字段从 JSON textarea
|
||||||
|
* 升级为文件上传组件。</p>
|
||||||
|
*/
|
||||||
|
class SysAppService extends BaseService {
|
||||||
|
constructor() {
|
||||||
|
super('/system/admin/app')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** SysApp 服务单例 */
|
||||||
|
export const sysAppService = new SysAppService()
|
||||||
|
```
|
||||||
|
|
||||||
|
并在 `service/system/index.ts` 末尾追加:
|
||||||
|
```ts
|
||||||
|
export { sysAppService } from './sysAppService'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. 路由与国际化
|
||||||
|
|
||||||
|
### 10.1 路由注册
|
||||||
|
|
||||||
|
在 `admin-ui/src/router/modules/system.ts` 的 `M` 常量中追加:
|
||||||
|
```ts
|
||||||
|
systemApp: 'menu.systemApp',
|
||||||
|
```
|
||||||
|
|
||||||
|
在 `systemRoutes` 数组中追加:
|
||||||
|
```ts
|
||||||
|
{ path: 'system/app', name: 'SystemApp', component: () => import('@/views/system/app/Index.vue'), meta: { i18n: M.systemApp } },
|
||||||
|
```
|
||||||
|
|
||||||
|
> 路由位置:放在 `systemOAuth2Client` 之后,保持「集成类」菜单分组相邻。
|
||||||
|
|
||||||
|
### 10.2 国际化
|
||||||
|
|
||||||
|
`admin-ui/src/locales/zh-CN.ts`(在 `systemOAuth2Client` 后追加):
|
||||||
|
```ts
|
||||||
|
systemApp: '应用集成',
|
||||||
|
```
|
||||||
|
|
||||||
|
`admin-ui/src/locales/en-US.ts`(在 `system` block 内对应位置追加):
|
||||||
|
```ts
|
||||||
|
systemApp: 'App Integration',
|
||||||
|
```
|
||||||
|
|
||||||
|
## 11. 错误处理
|
||||||
|
|
||||||
|
- **统一拦截**:所有 HTTP 错误由 `utils/request` 拦截器统一提示(已存在),前端代码不再额外 try-catch 提示。
|
||||||
|
- **业务校验**:表单必填、JSON 格式校验在组件内完成,失败用 `ElMessage` 提示。
|
||||||
|
- **状态回滚**:启停切换失败时,将 `row.status` 回滚到原值。
|
||||||
|
- **删除确认**:删除前 `ElMessageBox.confirm` 二次确认。
|
||||||
|
|
||||||
|
## 12. 测试策略
|
||||||
|
|
||||||
|
### 12.1 静态检查
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter admin-ui type-check # 0 errors
|
||||||
|
pnpm --filter admin-ui lint # 0 errors
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12.2 运行时验证(手动)
|
||||||
|
|
||||||
|
1. **启动 dev server**:`pnpm dev:admin`
|
||||||
|
2. **登录** 后访问 `/system/app`(需菜单权限 `sys:app:query`,从后端菜单加载)
|
||||||
|
3. **列表加载**:默认加载列表数据,列展示正确
|
||||||
|
4. **查询**:按 name / platform / ownerType / status 过滤,验证结果正确
|
||||||
|
5. **新增**:点「新增应用」→ 填写各 Tab 必填项 → 提交 → 列表出现新行
|
||||||
|
6. **编辑**:点行内编辑 → 弹窗加载详情(4 Tab 回显)→ 修改 → 提交 → 列表更新
|
||||||
|
7. **敏感字段验证**:编辑时不输入 appSecret → 提交后重新打开,明文应保持不变(**由后端「留空不修改」规则保证**)
|
||||||
|
8. **JSON 字段验证**:certificates / extra 输入非 JSON 文本 → 提交时被拦截并提示
|
||||||
|
9. **启停**:点击状态 Switch → 接口调用 → 列表状态切换;模拟失败时 row.status 回滚
|
||||||
|
10. **删除**:点击删除 → 二次确认 → 提交 → 行从列表消失
|
||||||
|
11. **批量删除**:选中多行 → 批量删除 → 全部消失
|
||||||
|
12. **导出**:点击导出 → 下载 CSV 文件
|
||||||
|
13. **脱敏验证**:在 devtools Network 面板检查 `/page` 返回的 records,**不应**包含 appSecret / appKey / aesKey 明文(这三个字段不进列表列)
|
||||||
|
|
||||||
|
### 12.3 验证清单(提交前必过)
|
||||||
|
|
||||||
|
- [ ] `pnpm type-check` 通过
|
||||||
|
- [ ] `pnpm lint` 通过
|
||||||
|
- [ ] 列表 → 新增 → 编辑 → 启停 → 删除 → 批量删除 完整跑通
|
||||||
|
- [ ] 列表中不展示任何明文密钥
|
||||||
|
- [ ] 敏感字段编辑留空后原值保持
|
||||||
|
- [ ] certificates / extra 非法 JSON 提交被拦截
|
||||||
|
- [ ] 菜单「应用集成」在侧边栏正确显示,路由跳转正常
|
||||||
|
|
||||||
|
## 13. 风险与缓解(Risks And Mitigations)
|
||||||
|
|
||||||
|
| # | 风险 | 缓解措施 |
|
||||||
|
|---|------|---------|
|
||||||
|
| 1 | **后端脱敏返回**:若后端在列表接口已经对 appSecret/appKey/aesKey 做脱敏(如 `******`),则编辑回显时弹窗中拿不到原值 | 列表用 `getById(id)` 拉详情(详情接口通常返回明文),并设计为编辑时强制用户重新输入敏感字段。本期采用「详情接口返回明文,编辑留空不修改」模式;如未来后端详情也脱敏,需在 UI 加「修改敏感字段」开关。 |
|
||||||
|
| 2 | **certificates JSON 格式错误**:用户提交非合法 JSON 导致后端解析失败 | 提交前 `validateJSON()` 拦截,UI 给出明确错误提示;helper text 提示正确格式。 |
|
||||||
|
| 3 | **文件上传接口未就绪**:certificates 暂用 JSON 占位,用户体验差 | 已在 rui/rui-framework#4 提 Issue;后端接口就绪后再升级为文件上传组件(升级路径已记录在 `sysAppService.ts` 注释中)。 |
|
||||||
|
| 4 | **路由重复**:system.ts 中路由顺序错乱导致菜单不显示 | 路由注册在 systemOAuth2Client 之后;meta.i18n 键值与 locales 文件保持一致。 |
|
||||||
|
| 5 | **列表数据过大导致性能问题** | 分页由 RuiTable 默认处理(page=1, size=10),无额外风险。 |
|
||||||
|
| 6 | **前端调用了不存在的接口**:万一后端实际未提供 `import` 接口 | BaseService 自带 `importable` 开关默认 false,不暴露导入按钮。如后端未提供 import 接口则本 UI 不调用即可。 |
|
||||||
|
|
||||||
|
## 14. 决策摘要(Decision Summary)
|
||||||
|
|
||||||
|
- **架构**:照搬 `oauth2-client` 模式(`BaseService` 13 行 + `RuiTable` 列表 + `FormDialog` 弹窗),不引入新依赖、不发明新模式。
|
||||||
|
- **菜单归属**:系统管理 → 子菜单「应用集成」,路由 `/system/app`。
|
||||||
|
- **表单布局**:el-tabs 4 Tab,760px 弹窗。
|
||||||
|
- **敏感字段**:appSecret / appKey / aesKey 列表脱敏 6 星号,编辑留空不修改。
|
||||||
|
- **certificates 字段**:JSON textarea 占位,UI 注释提醒「待后端文件上传接口就绪后升级」。
|
||||||
|
- **isEncrypted 字段**:UI 暂不实现。
|
||||||
|
- **测试**:type-check + lint + 手动跑通完整 CRUD。
|
||||||
|
- **依赖后端**:仅依赖 rui-framework 既有 `/system/admin/app` 接口;`/system/admin/file/upload`(rui/rui-framework#4)就绪后再升级 certificates 体验。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**设计评审状态**: 待评审
|
||||||
|
**下一步**: 用户评审通过后,编写实施计划(Plan)
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
# 用户管理接口适配设计规范
|
||||||
|
|
||||||
|
**工单**: #2 - 用户管理接口变更通知
|
||||||
|
**日期**: 2026-06-07
|
||||||
|
**方案**: 方案 B(完整适配)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 背景
|
||||||
|
|
||||||
|
后端已完成用户管理模块接口重构(提交 `dbd04d8`),支持部门、角色联表查询和聚合信息返回。前端需要适配以:
|
||||||
|
- 减少请求次数(从 3 个请求合并为 1 个)
|
||||||
|
- 支持部门/角色筛选
|
||||||
|
- 在列表和详情中展示部门、角色信息
|
||||||
|
|
||||||
|
## 2. 后端变更摘要
|
||||||
|
|
||||||
|
### 2.1 用户实体扩展字段
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"username": "admin",
|
||||||
|
"phone": "13800138000",
|
||||||
|
"depts": [
|
||||||
|
{
|
||||||
|
"deptId": 1,
|
||||||
|
"deptCode": "TECH",
|
||||||
|
"deptName": "技术部",
|
||||||
|
"main": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"roleId": 1,
|
||||||
|
"roleCode": "admin",
|
||||||
|
"roleName": "管理员",
|
||||||
|
"dataScope": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 新增接口
|
||||||
|
|
||||||
|
- `GET /user/admin/user/{id}/aggregate` - 聚合查询(基础信息 + 部门列表 + 角色列表)
|
||||||
|
|
||||||
|
### 2.3 增强接口
|
||||||
|
|
||||||
|
- `GET /user/admin/user/page` - 自动返回 `depts` 和 `roles`
|
||||||
|
- `GET /user/admin/user/list` - 自动返回 `depts` 和 `roles`
|
||||||
|
- 支持筛选参数:`deptId`、`roleId`
|
||||||
|
|
||||||
|
## 3. 前端适配范围
|
||||||
|
|
||||||
|
### 3.1 用户列表页 (`views/user/info/Index.vue`)
|
||||||
|
|
||||||
|
**新增列:**
|
||||||
|
- 部门列:显示用户所属部门名称(多个部门用逗号分隔,主部门加粗)
|
||||||
|
- 角色列:显示用户角色名称(多个角色用标签展示)
|
||||||
|
|
||||||
|
**新增筛选条件:**
|
||||||
|
- 部门筛选:树形选择器(`el-tree-select`),支持多选
|
||||||
|
- 角色筛选:树形选择器(`el-tree-select`),支持多选
|
||||||
|
|
||||||
|
**数据流:**
|
||||||
|
- 列表接口自动返回 `depts` 和 `roles`,无需额外请求
|
||||||
|
- 筛选参数通过 `queryParams` 传递给 `userService.page()`
|
||||||
|
|
||||||
|
### 3.2 用户详情弹窗 (`views/user/info/UserDetailDialog.vue`)
|
||||||
|
|
||||||
|
**改造:**
|
||||||
|
- 使用新的聚合接口 `GET /user/admin/user/{id}/aggregate`
|
||||||
|
- 展示部门列表(部门名称 + 是否主部门标记)
|
||||||
|
- 展示角色列表(角色名称 + 数据范围)
|
||||||
|
|
||||||
|
**数据流:**
|
||||||
|
```
|
||||||
|
打开弹窗 → 调用 aggregate 接口 → 展示完整信息
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 用户表单 (`views/user/info/UserFormDialog.vue`)
|
||||||
|
|
||||||
|
**优化:**
|
||||||
|
- 编辑时从 `row.depts` 解析 `deptIds`(替代调用 `userDeptService.listDeptIdsByUserId`)
|
||||||
|
- 编辑时从 `row.roles` 解析 `roleIds`(替代调用 `userService.getRoles`)
|
||||||
|
- 保留 `userDeptService.assignDepts` 和 `userPostService.assignPosts` 用于保存
|
||||||
|
|
||||||
|
### 3.4 Service 层扩展 (`service/user/userService.ts`)
|
||||||
|
|
||||||
|
**新增方法:**
|
||||||
|
- `aggregate(userId)` - 调用聚合查询接口
|
||||||
|
|
||||||
|
## 4. 组件设计
|
||||||
|
|
||||||
|
### 4.1 部门/角色展示组件
|
||||||
|
|
||||||
|
无需新增组件,直接在表格列中使用 `slot` 渲染:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- 部门列 -->
|
||||||
|
<template #column-depts="{ row }">
|
||||||
|
<el-tag
|
||||||
|
v-for="dept in row.depts"
|
||||||
|
:key="dept.deptId"
|
||||||
|
:type="dept.main ? 'primary' : 'info'"
|
||||||
|
size="small"
|
||||||
|
class="mr-1"
|
||||||
|
>
|
||||||
|
{{ dept.deptName }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 角色列 -->
|
||||||
|
<template #column-roles="{ row }">
|
||||||
|
<el-tag
|
||||||
|
v-for="role in row.roles"
|
||||||
|
:key="role.roleId"
|
||||||
|
type="success"
|
||||||
|
size="small"
|
||||||
|
class="mr-1"
|
||||||
|
>
|
||||||
|
{{ role.roleName }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 筛选区域
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template #search="{ query: q, search, reset }">
|
||||||
|
<!-- 现有筛选条件... -->
|
||||||
|
|
||||||
|
<!-- 新增:部门筛选 -->
|
||||||
|
<el-form-item label="所属部门">
|
||||||
|
<el-tree-select
|
||||||
|
v-model="q.deptId"
|
||||||
|
:data="deptTree"
|
||||||
|
check-strictly
|
||||||
|
node-key="id"
|
||||||
|
:props="{ label: 'deptName', children: 'children' }"
|
||||||
|
placeholder="请选择部门"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 新增:角色筛选 -->
|
||||||
|
<el-form-item label="角色">
|
||||||
|
<el-tree-select
|
||||||
|
v-model="q.roleId"
|
||||||
|
:data="roleTree"
|
||||||
|
check-strictly
|
||||||
|
node-key="id"
|
||||||
|
:props="{ label: 'roleName', children: 'children' }"
|
||||||
|
placeholder="请选择角色"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 数据类型定义
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 部门信息(嵌套在用户中)
|
||||||
|
interface UserDept {
|
||||||
|
deptId: number
|
||||||
|
deptCode: string
|
||||||
|
deptName: string
|
||||||
|
main: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// 角色信息(嵌套在用户中)
|
||||||
|
interface UserRole {
|
||||||
|
roleId: number
|
||||||
|
roleCode: string
|
||||||
|
roleName: string
|
||||||
|
dataScope: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 扩展用户类型
|
||||||
|
interface User {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
// ... 其他字段
|
||||||
|
depts?: UserDept[]
|
||||||
|
roles?: UserRole[]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 接口调用变更
|
||||||
|
|
||||||
|
### 6.1 列表页
|
||||||
|
|
||||||
|
**变更前:**
|
||||||
|
- 调用 `userService.page(params)` - 仅返回基础信息
|
||||||
|
|
||||||
|
**变更后:**
|
||||||
|
- 调用 `userService.page(params)` - 自动包含 `depts` 和 `roles`
|
||||||
|
- 支持 `deptId` 和 `roleId` 筛选参数
|
||||||
|
|
||||||
|
### 6.2 详情弹窗
|
||||||
|
|
||||||
|
**变更前:**
|
||||||
|
- 直接使用 `props.row` 数据
|
||||||
|
|
||||||
|
**变更后:**
|
||||||
|
- 打开时调用 `userService.aggregate(props.row.id)`
|
||||||
|
- 使用返回的完整数据渲染
|
||||||
|
|
||||||
|
### 6.3 编辑表单
|
||||||
|
|
||||||
|
**变更前:**
|
||||||
|
```typescript
|
||||||
|
// 需要额外请求获取部门和角色
|
||||||
|
const deptIds = await userDeptService.listDeptIdsByUserId(userId)
|
||||||
|
const roleIds = await userService.getRoles(userId)
|
||||||
|
```
|
||||||
|
|
||||||
|
**变更后:**
|
||||||
|
```typescript
|
||||||
|
// 直接从 row 中解析
|
||||||
|
const deptIds = props.row.depts?.map((d: any) => d.deptId) || []
|
||||||
|
const roleIds = props.row.roles?.map((r: any) => r.roleId) || []
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 错误处理
|
||||||
|
|
||||||
|
- 聚合接口失败时,回退到使用 `props.row` 基础信息
|
||||||
|
- 部门/角色数据缺失时,显示 "-" 或空标签
|
||||||
|
- 筛选条件不影响现有查询逻辑
|
||||||
|
|
||||||
|
## 8. 兼容性
|
||||||
|
|
||||||
|
- 原有接口保持不变
|
||||||
|
- 新增字段通过 `@TableField(exist = false)` 添加,不影响旧逻辑
|
||||||
|
- 保留 `userDeptService` 和 `userPostService` 用于分配功能
|
||||||
|
|
||||||
|
## 9. 测试要点
|
||||||
|
|
||||||
|
1. 列表页是否正确显示部门和角色信息
|
||||||
|
2. 部门/角色筛选是否生效
|
||||||
|
3. 详情弹窗是否正确展示聚合数据
|
||||||
|
4. 编辑表单是否正确解析已选部门/角色
|
||||||
|
5. 保存后数据是否正确刷新
|
||||||
|
|
||||||
|
## 10. 文件变更清单
|
||||||
|
|
||||||
|
| 文件 | 变更类型 | 说明 |
|
||||||
|
|------|---------|------|
|
||||||
|
| `views/user/info/Index.vue` | 修改 | 添加部门/角色列和筛选条件 |
|
||||||
|
| `views/user/info/UserDetailDialog.vue` | 修改 | 使用聚合接口,展示部门/角色详情 |
|
||||||
|
| `views/user/info/UserFormDialog.vue` | 修改 | 从 row 解析 deptIds/roleIds |
|
||||||
|
| `service/user/userService.ts` | 修改 | 添加 aggregate 方法 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**设计评审状态**: 待评审
|
||||||
|
**下一步**: 用户评审通过后,编写实施计划
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Specs 索引
|
||||||
|
|
||||||
|
本目录存放已通过评审的设计规范(Spec)。每份 Spec 对应一个工单/需求,配套 Plan 在 `../plans/` 目录。
|
||||||
|
|
||||||
|
## 列表
|
||||||
|
|
||||||
|
| 日期 | 标题 | 工单 | 状态 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 2026-06-06 | [API 协作工作流设计](2026-06-06-api-collaboration-design.md) | 内部规范 | 已评审 |
|
||||||
|
| 2026-06-07 | [用户管理接口适配设计](2026-06-07-user-management-api-adaptation-design.md) | rui/rui-frontend#2 | 已评审 |
|
||||||
|
| 2026-06-07 | [SysApp 应用集成管理设计](2026-06-07-sysapp-management-design.md) | rui/rui-frontend#4 | 待评审 |
|
||||||
|
|
||||||
|
## 命名规范
|
||||||
|
|
||||||
|
- 文件名:`YYYY-MM-DD-<topic>-design.md`
|
||||||
|
- 目录:`docs/superpowers/specs/`
|
||||||
|
- 配套 Plan:`docs/superpowers/plans/YYYY-MM-DD-<topic>-plan.md`
|
||||||
|
|
||||||
|
## 状态
|
||||||
|
|
||||||
|
- **待评审**:Spec 刚写完,等待用户/团队评审
|
||||||
|
- **已评审**:用户已批准,可进入 Plan 阶段
|
||||||
|
- **已实施**:对应 Plan 已执行完成
|
||||||
|
- **已归档**:功能上线,文档归档
|
||||||
Reference in New Issue
Block a user