docs: add superpowers design docs and plans

This commit is contained in:
2026-06-07 09:37:53 +08:00
parent b9d5b6d9f0
commit bb71263bdd
2 changed files with 762 additions and 0 deletions
@@ -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,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 方法 |
---
**设计评审状态**: 待评审
**下一步**: 用户评审通过后,编写实施计划