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>
This commit is contained in:
zlwuandCursor committed 2026-08-21 16:42:40 +08:00
1 parent e9ad26cb48
commit d8f10b9efe
1 file changed
+140
@@ -0,0 +1,140 @@
# 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(新接入页)
- 异步导出任务、导出历史、邮件通知
- 自定义列模板编辑器、导入
- 小程序端导出