docs: 更新分页功能设计文档,扩展到所有管理表格页面
This commit is contained in:
parent
54230b6cfd
commit
cc1a6459bd
|
|
@ -0,0 +1,389 @@
|
|||
# 管理后台分页功能设计文档
|
||||
|
||||
**日期**: 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
|
||||
- 支持自定义每页行数选项
|
||||
|
||||
Loading…
Reference in New Issue