14 KiB
用户管理接口适配实施计划
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 方法后添加:
/**
* 聚合查询用户完整信息(基础信息 + 部门列表 + 角色列表)
*/
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
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> 顶部添加导入:
import { ref, onMounted } from 'vue'
import { deptService } from '@/service/system/deptService'
import { roleService } from '@/service/system/roleService'
- Step 2: 添加部门和角色列到表格配置
在 columns 数组中,在 createdAt 列之前添加:
{
prop: 'depts',
label: '所属部门',
minWidth: 150,
slot: true,
},
{
prop: 'roles',
label: '角色',
minWidth: 150,
slot: true,
},
- Step 3: 添加树形数据状态
在 const { t } = useI18n() 后添加:
/**
* 部门树数据
*/
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 中,在状态筛选后添加:
<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 后添加:
<!-- 自定义列:所属部门 -->
<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
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> 顶部修改:
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?.:
<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
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) => { ... }) 中,修改编辑时的数据处理:
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
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: 最终提交(可选,如果需要合并)
# 如果需要,可以创建一个合并提交
git log --oneline -5
回滚计划
如果出现问题,可以按以下顺序回滚:
- 回滚 Task 4:
git revert <task4-commit> - 回滚 Task 3:
git revert <task3-commit> - 回滚 Task 2:
git revert <task2-commit> - 回滚 Task 1:
git revert <task1-commit>
测试清单
- 用户列表页显示部门列(主部门高亮)
- 用户列表页显示角色列
- 部门树形筛选正常工作
- 角色树形筛选正常工作
- 同时筛选部门和角色正常工作
- 用户详情弹窗显示完整部门列表
- 用户详情弹窗显示完整角色列表
- 编辑用户时正确加载已选部门
- 编辑用户时正确加载已选角色
- 保存用户后数据正确刷新
计划状态: 待评审
下一步: 用户评审通过后,使用 superpowers-subagent-driven-development 或 superpowers-executing-plans 执行