Files
shanxia2.0/docs/superpowers/specs/2026-08-21-web-admin-list-export-design.md
T
zlwuandCursor d8f10b9efe docs: add design for web-admin unified list Excel export
Capture backend export infrastructure, ListExportDropdown UX, and
per-resource /export conventions for filtered and selected rows.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-21 16:42:40 +08:00

141 lines
6.3 KiB
Markdown
Raw 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.
# web-admin 列表统一后端导出
**日期:** 2026-08-21
**状态:** 已确认
**范围:** `web-admin` 所有带筛选/分页的业务列表(系统管理、物流、报销、CRM、地磅等);小程序不在本期。
## 背景
多数列表页缺少导出;少数页(CRM 等)已有后端 Excel 导出,Demo 商品等则用前端 `ArtExcelExport` 分页拉全量再生成。筛选结果达数万行时,前端全量拉取不合理。需要统一为**后端生成 xlsx、前端下载**,并在工具栏提供一致交互。
## 决策摘要
| 项 | 结论 |
|----|------|
| 覆盖范围 | 所有带筛选/分页的业务列表;已有导出逻辑保留,仅对齐交互 |
| 实现方式 | 后端导出(方案 2:共享基建 + 各页注册) |
| 导出列 | 与当前列表可见列一致;中文表头;状态等为文案非 code |
| 主操作 | 导出筛选结果 |
| 次操作 | 导出已选(仅有多选勾选的列表;下拉菜单) |
| 无多选列表 | 仅单个「导出」按钮(等同导出筛选结果) |
| 大数据 | 同步流式下载;硬上限 50000 行 |
| 非目标 | 异步任务、导出历史、邮件、导入、小程序 |
## 架构
### 后端
- 复用并扩展 `backend/app/utils/excel_export.py`(`ExportColumn`、`DictLabelResolver`、`export_rows_to_xlsx_response`)。
- 各资源仍暴露独立 `POST …/export`(已有 GET 的保持兼容);**不做**全局万能导出 API。
- 共享约定:
- 查询复用对应列表(租户隔离、数据权限、软删、筛选条件一致)
- 可选 `ids`:有则只导出这些行(仍叠加权限过滤)
- 可选 `columns`:前端传入当前可见列 `prop`;缺省用服务端默认列(对齐列表默认可见列)
- 硬上限 **50000**;0 条返回业务错误,不生成空文件
- 二进制响应路径须在 `API_ENCRYPT_EXCLUDE_PATHS` 中(与现有 CRM 导出一致)。
### 前端
- 新组件 `ListExportDropdown`(中性 `btn-ghost` + `Download` 图标):
- `hasSelection=true`:下拉——主按钮「导出筛选结果」,菜单项「导出已选」(无选中则禁用)
- `hasSelection=false`:单按钮「导出」
- 位置:列表工具栏主操作之后、`ListToolbarEndActions` 之前。
- 统一 blob 下载工具(解析 `Content-Disposition`、loading、错误提示)。
- 有列显隐时,请求携带当前 `visibleColumns`。
### 数据流
1. **导出筛选结果**:当前筛选参数 + 可选 `columns`,不传 `ids` → 后端查全量筛选结果 → xlsx → 下载。
2. **导出已选**:同上 + `ids: selectedRowIds`;未选中时前端禁用,不发请求。
```text
[工具栏 ListExportDropdown]
│ filterParams + columns? + ids?
▼
[POST /api/.../export] ──同列表权限──► [查询行] ──ExportColumn──► [openpyxl] ──► StreamingResponse
▲ │
└──────────────────────── blob download + Message ──────────────────────────────┘
```
## 接口约定
### 请求
```ts
{
// 与对应列表相同的筛选字段
ids?: string[] // 导出已选
columns?: string[] // 当前可见列 prop;省略则服务端默认列
}
```
### 响应
- 成功:xlsx 流;`Content-Disposition` 含中文文件名(沿用 `workbook_to_streaming_response`)。
- 失败(建议):
| 场景 | 处理 |
|------|------|
| 0 条可导出 | 400「没有可导出的数据」 |
| 超过 50000 | 400「导出超过 50000 条,请缩小筛选范围」 |
| 无权限 | 与列表一致 401/403 |
| `columns` 含未知 prop | 忽略未知项;仅导出已登记字段 |
| `ids` 权限过滤后为空 | 同 0 条 |
### 列定义
- 使用 `ExportColumn(title, field, dict_type?, formatter?)`。
- 树形路径等展示规则与列表/规范一致(全路径、` / ` 分隔)。
- 「可见列」:有 `usePersistedListColumns` 时跟当前勾选;否则跟页面表格列定义(排除 selection/index/操作列)。
### 已有导出
- **已有后端导出**(CRM 等):保留既有列与查询;按需补 `ids` / `columns`;前端改为 `ListExportDropdown`(或等价交互)。
- **现有前端 `ArtExcelExport` 页**(如 Demo 商品):批处理时迁为后端 `/export` + `ListExportDropdown`,不再用浏览器拉全量生成。
## 前端组件草案
**`ListExportDropdown` props**
| Prop | 说明 |
|------|------|
| `exportFn` | `(payload) => Promise<Blob>` |
| `getFilterParams` | `() => Record<string, any>` |
| `selectedIds` | 已选 id;无多选可不传 |
| `hasSelection` | 是否支持多选 UI |
| `visibleColumns` | 可选可见列 |
| `filename` | 兜底文件名(优先响应头) |
| `disabled` / `loading` | 外部控制 |
页面接入:声明导出 API、列与表格对齐、挂组件;有勾选则传 `selectedIds`。
## 落地节奏
1. **基建**:后端上限/空数据约定文档化进工具函数;前端组件 + 下载工具。
2. **样板页**:选一标准列表(如 `system/user` 或 `demo/unit`)端到端打通。
3. **批处理**:按模块为缺导出页补 `/export` + 挂组件;已有导出页对齐下拉。
4. **回归**:筛选一致性、可见列、已选、空数据、超限、加密排除路径。
## 权限与安全
- 导出 SHALL 与列表同一鉴权、租户、数据权限;MUST NOT 因导出绕过范围。
- `ids` MUST 仍叠加租户 + 数据权限,禁止仅凭 id 跨范围导出。
- `columns` 仅允许该资源已登记 field。
## 验收标准
- [ ] 目标列表工具栏均有导出入口(中性按钮 + 图标)。
- [ ] 有多选:下拉含「导出筛选结果」「导出已选」;未选中时后者禁用。
- [ ] 无多选:仅「导出」单按钮。
- [ ] 导出结果与当前筛选一致;可见列与 Excel 列一致;状态为中文文案。
- [ ] 0 条 / 超 50000 有明确提示且不下载损坏文件。
- [ ] 已有导出页交互对齐,业务列含义不回退。
- [ ] 导出路径不受 API 加密中间件破坏二进制。
## 明确不做
- 前端拉全量再生成 Excel(新接入页)
- 异步导出任务、导出历史、邮件通知
- 自定义列模板编辑器、导入
- 小程序端导出