Files
rui-docs/superpowers/plans/2026-06-07-user-management-api-adaptation-plan.md

502 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 用户管理接口适配实施计划
> **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 执行