docs: add superpowers design docs and plans
This commit is contained in:
@@ -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 执行
|
||||
Reference in New Issue
Block a user