502 lines
14 KiB
Markdown
502 lines
14 KiB
Markdown
# 用户管理接口适配实施计划
|
||
|
||
> **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 执行
|