Recruitment_site/docs/superpowers/specs/2026-03-25-organization-pag...

12 KiB
Raw Blame History

管理后台分页功能设计文档

日期: 2026-03-25 功能: 为管理后台所有表格页面(职位管理、投递管理、组织架构、用户管理)添加分页功能 优先级: 标准 涉及页面:

  • 职位管理 (JobManageView.vue)
  • 投递管理 (ApplicationManageView.vue)
  • 组织架构 (OrganizationManageView.vue)
  • 用户管理 (UserManageView.vue)

概述

当前所有管理后台表格一次性加载所有数据到表格中。为了改善用户体验和减少初始加载时间,为所有管理表格页面实现统一的标准分页功能。后端已配置 PageNumberPagination(每页 20 条),前端只需统一接入分页参数和 UI 组件。

背景

  • 现状: 所有管理表格页面(职位、投递、部门、用户)都直接加载所有数据到表格,无分页
  • 问题: 数据量增多时,页面加载缓慢,表格拥挤,用户体验差
  • 后端支持: Django REST Framework 已全局配置分页(DEFAULT_PAGE_SIZE: 20),自动处理 ?page=1 参数
  • 目标: 为所有管理表格统一添加分页功能,提升用户体验

设计方案

1. 后端 API 层

无需改动OrganizationManageViewSet 继承 DRF 的 ModelViewSet,已自动支持 PageNumberPagination

API 响应格式(现有):

{
  "count": 50,
  "next": "http://.../?page=2",
  "previous": null,
  "results": [
    { "id": 1, "name": "集团", "email": "...", "is_active": true, "parent": null },
    ...
  ]
}

2. 前端 API 层修改

文件: offer_frontend/src/api/organizations.js

修改 manageOrganizations 函数,支持可选的页码参数:

export const manageOrganizations = (page = 1) =>
  client.get(`/organizations/manage/?page=${page}`)

调用示例:

// 获取第 1 页
const { data } = await manageOrganizations(1)
// 获取第 2 页
const { data } = await manageOrganizations(2)

3. 前端组件修改

文件: offer_frontend/src/views/admin/OrganizationManageView.vue

3.1 状态管理

新增和保留响应式状态:

const orgs = ref([])            // 当前页部门列表
const allOrgs = ref([])         // 所有部门列表(用于下拉框)
const loading = ref(false)      // 当前页加载状态
const currentPage = ref(1)      // 当前页码
const pageSize = ref(20)        // 每页行数(与后端 PAGE_SIZE 一致)
const total = ref(0)            // 总记录数

3.2 修改 fetchOrgs 函数

用于加载分页数据:

const fetchOrgs = async (page = 1) => {
  loading.value = true
  try {
    const { data } = await manageOrganizations(page)
    orgs.value = data.results
    total.value = data.count
    currentPage.value = page
  } catch (error) {
    ElMessage.error('加载部门列表失败,请重试')
  } finally {
    loading.value = false
  }
}

3.3 新增 fetchAllOrgs 函数(用于下拉框)

为了支持下拉框选择任意部门,需要单独加载所有部门(不分页):

const fetchAllOrgs = async () => {
  try {
    // 调用同一个 API但不传 page 参数时默认返回第 1 页
    // 实际需要:修改后端 API 或调用一个无分页的端点
    // 临时方案:调用 getOrganizations() 获取公开的组织(如果有)
    // 或者:预加载足够多的页(假设部门不超过 500调用 3-4 次)
    const { data } = await manageOrganizations(1)
    // 如果只有少量部门,可以一次性加载最多 5 页
    let allResults = data.results
    for (let i = 2; i <= Math.ceil(data.count / 20) && i <= 5; i++) {
      const { data: nextData } = await manageOrganizations(i)
      allResults = allResults.concat(nextData.results)
    }
    allOrgs.value = allResults
  } catch (error) {
    ElMessage.error('加载部门列表失败')
  }
}

注意: 这是一个临时方案。如果部门数量可能超过 100建议后端新增一个 list_all 参数或单独的无分页 API 端点。

3.4 添加分页事件处理

function handlePageChange(newPage) {
  fetchOrgs(newPage)
}

3.5 修改保存后的逻辑

无论新增还是编辑,保存成功后都重置分页状态并返回第 1 页(保持用户体验一致):

async function save() {
  saving.value = true
  try {
    if (editing.value) await updateOrganization(editing.value.id, form)
    else await createOrganization(form)
    ElMessage.success('保存成功')
    dialogVisible.value = false
    currentPage.value = 1  // 重置到第 1 页
    fetchOrgs(1)           // 重新加载第 1 页(明确传入页码)
    fetchAllOrgs()         // 刷新下拉框数据
  } catch (error) {
    ElMessage.error('保存失败')
  } finally {
    saving.value = false
  }
}

3.6 新增删除功能

添加 handleDelete() 函数:

async function handleDelete(id) {
  try {
    await ElMessageBox.confirm('确认删除该部门?', '提示', {
      type: 'warning',
      confirmButtonText: '确认',
      cancelButtonText: '取消'
    })
    await deleteOrganization(id)
    ElMessage.success('已删除')
    currentPage.value = 1  // 删除后回到第 1 页
    fetchOrgs(1)           // 重新加载第 1 页(明确传入页码)
    fetchAllOrgs()         // 刷新下拉框数据
  } catch (error) {
    // ElMessageBox.confirm 取消时会抛出 ElMessageBoxCancel 异常,需要判断
    if (error.name !== 'ElMessageBoxCancel') {
      ElMessage.error('删除失败')
    }
  }
}

3.7 初始化时加载所有数据

修改 onMounted 钩子:

onMounted(() => {
  fetchOrgs()    // 加载分页数据(第 1 页)
  fetchAllOrgs() // 加载所有部门用于下拉框
})

4. UI 层修改

4.1 表格加载状态

修改表格标签,添加 v-loading 指令:

<el-table :data="orgs" v-loading="loading" border>

4.2 操作列添加删除按钮

修改操作列,添加删除按钮:

<el-table-column label="操作" width="150">
  <template #default="{ row }">
    <el-button size="small" @click="openDialog(row)">编辑</el-button>
    <el-button size="small" type="danger" @click="handleDelete(row.id)">删除</el-button>
  </template>
</el-table-column>

4.3 下拉框使用所有部门列表

修改编辑对话框中的"上级公司"选择,使用 allOrgs 而非 orgs

<el-form-item label="上级公司">
  <el-select v-model="form.parent" clearable placeholder="不选则为集团顶级">
    <el-option v-for="o in allOrgs" :key="o.id" :value="o.id" :label="o.name" />
  </el-select>
</el-form-item>

4.4 表格下方添加分页控件

<div style="margin-top: 16px; text-align: right;">
  <el-pagination
    v-model:current-page="currentPage"
    :page-size="pageSize"
    :total="total"
    @current-change="handlePageChange"
    layout="total, prev, pager, next, jumper"
  />
</div>

样式说明:

  • layout="total, prev, pager, next, jumper" - 显示总数、前一页、页码、后一页、跳转输入框
  • 右对齐,与表格宽度对称
  • margin-top: 16px 与表格距离保持一致

5. 数据流

用户交互
    ↓
点击分页按钮 → handlePageChange(newPage)
    ↓
fetchOrgs(newPage) → manageOrganizations(newPage)
    ↓
API 返回 { count, results }
    ↓
更新 orgs、total、currentPage
    ↓
表格重新渲染

6. 错误处理

  • 加载失败: 显示 ElMessage.error(),保留当前页状态
  • 删除部门: 删除后调用 currentPage.value = 1; fetchOrgs() 回到第 1 页(避免最后一条删除后页码超出范围)
  • 网络异常: 用户可点击分页按钮重试

7. 边界情况

场景 处理方式
初始化(无数据) currentPage=1, total=0, orgs=[],分页组件自动隐藏
新增部门后 重置到第 1 页,重新加载分页数据和下拉框数据
编辑部门后 重置到第 1 页,重新加载分页数据和下拉框数据(保持一致体验)
删除部门后 重置到第 1 页,重新加载分页数据和下拉框数据
当前页无数据 自动回到第 1 页(由分页组件自动处理)
用户取消删除确认 不执行任何操作,保留当前状态

8. 下拉框数据一致性

问题: 分页后,表格显示当前页的 20 条部门,但"父公司"下拉框需要显示所有部门(因为用户可能需要将某个部门的父级设置为其他页的部门)

解决方案:

  • 维护单独的 allOrgs 数组,通过 fetchAllOrgs() 函数加载
  • 表格显示分页数据 orgs(当前页)
  • 下拉框显示所有数据 allOrgs(所有页)
  • 新增/编辑/删除后同时刷新两个列表

临时方案说明: 当前方案通过预加载前几页(最多 5 页、共 100 条)来获取所有部门。若部门数量可能超过 100建议后续优化为

  1. 后端添加 ?list_all=true 参数,返回无分页的完整列表
  2. 或单独新增一个无分页的 API 端点(如 /organizations/manage/all/

后续优化: 若需要搜索功能,改为搜索型下拉框(el-select + filterable

实现清单

后端 (无需改动):

  • Django REST Framework 已支持 PageNumberPagination

前端 - API 层 (各修改一次):

  • organizations.js - manageOrganizations() 添加 page 参数
  • jobs.js - manageJobs() 添加 page 参数
  • applications.js - getManageApplications() 添加 page 参数
  • /auth/users/ 调用位置 - 传入 page 参数

前端 - 组件层 (共 4 个页面):

  • OrganizationManageView.vue (部门管理)

    • 添加分页状态和函数
    • 修改表格和操作列
    • 添加分页控件
  • JobManageView.vue (职位管理)

    • 添加分页状态和函数
    • 修改表格和操作列
    • 添加分页控件
  • ApplicationManageView.vue (投递管理)

    • 添加分页状态和函数
    • 修改表格
    • 添加分页控件
  • UserManageView.vue (用户管理)

    • 添加分页状态和函数
    • 修改表格和操作列
    • 添加分页控件

测试 (每个页面):

  • 验证分页功能(第 1 页、中间页、最后一页)
  • 验证新增/编辑/删除后的分页重置
  • 验证网络错误恢复

性能考量

  • API 调用: 每次页码变更触发一次 API 调用(频率不高,通常用户不会频繁翻页)
  • 数据量: 每次最多 20 条记录,前端内存占用低
  • 首屏加载: 从一次性加载 N 条改为加载 20 条,性能改善明显

向后兼容性

无破坏性改动:

  • API 默认参数 page=1,未传参时返回第 1 页
  • 现有其他功能不受影响

测试场景

  1. 初次加载

    • 验证第 1 页正确显示(最多 20 条)
    • 验证总数、页码正确
    • 验证下拉框加载所有部门
  2. 翻页

    • 点击下一页、上一页、特定页码,数据正确更新
    • 验证 loading 状态在加载期间显示
  3. 跳转输入

    • 直接输入页码(如第 5 页),正确跳转
    • 输入超出范围的页码(如第 100 页),无报错
  4. 新增部门

    • 新增后,回到第 1 页并显示新部门
    • 验证下拉框立即更新,新部门可被选为父级
  5. 编辑部门

    • 编辑后,回到第 1 页,验证修改内容生效
    • 下拉框同时更新
  6. 删除部门

    • 点击删除,出现确认对话框
    • 确认删除后,回到第 1 页,验证被删除部门消失
    • 下拉框同时移除该部门
    • 取消删除,页面无变化
  7. 边界情况

    • 当前为最后一页,删除该页最后一条,自动回到第 1 页
    • 删除所有部门,表格显示空,分页组件隐藏
  8. 错误恢复

    • 网络超时,显示错误提示,保留当前页状态
    • 点击分页按钮重试,正确加载
  9. 下拉框完整性

    • 在下拉框中能选到所有部门(不仅仅是当前页)
    • 搜索或滚动下拉框,能找到目标部门

后续扩展

  • 为其他管理页面(职位、用户)添加相同的分页逻辑
  • 考虑将分页逻辑抽象为可复用的 Composable
  • 支持自定义每页行数选项