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

390 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 管理后台分页功能设计文档
**日期**: 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 响应格式**(现有):
```json
{
"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` 函数,支持可选的页码参数:
```javascript
export const manageOrganizations = (page = 1) =>
client.get(`/organizations/manage/?page=${page}`)
```
**调用示例**:
```javascript
// 获取第 1 页
const { data } = await manageOrganizations(1)
// 获取第 2 页
const { data } = await manageOrganizations(2)
```
### 3. 前端组件修改
**文件**: `offer_frontend/src/views/admin/OrganizationManageView.vue`
#### 3.1 状态管理
新增和保留响应式状态:
```javascript
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 函数
用于加载分页数据:
```javascript
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 函数(用于下拉框)
为了支持下拉框选择任意部门,需要单独加载**所有部门**(不分页):
```javascript
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 添加分页事件处理
```javascript
function handlePageChange(newPage) {
fetchOrgs(newPage)
}
```
#### 3.5 修改保存后的逻辑
无论新增还是编辑,保存成功后都重置分页状态并返回第 1 页(保持用户体验一致):
```javascript
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()` 函数:
```javascript
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` 钩子:
```javascript
onMounted(() => {
fetchOrgs() // 加载分页数据(第 1 页)
fetchAllOrgs() // 加载所有部门用于下拉框
})
```
### 4. UI 层修改
#### 4.1 表格加载状态
修改表格标签,添加 `v-loading` 指令:
```vue
<el-table :data="orgs" v-loading="loading" border>
```
#### 4.2 操作列添加删除按钮
修改操作列,添加删除按钮:
```vue
<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`
```vue
<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 表格下方添加分页控件
```vue
<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`
## 实现清单
**后端** (无需改动):
- [x] 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
- 支持自定义每页行数选项