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

14 KiB
Raw Permalink Blame History

用户管理接口适配实施计划

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

回滚计划

如果出现问题,可以按以下顺序回滚:

  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 执行