diff --git a/docs/crm-date-column-sort-demo.html b/docs/crm-date-column-sort-demo.html new file mode 100644 index 000000000..3820a6aa0 --- /dev/null +++ b/docs/crm-date-column-sort-demo.html @@ -0,0 +1,638 @@ + + + + + + CRM 日期列头排序 Demo · 正序 / 倒序 + + + +
+

CRM 日期列头排序 Demo

+

+ 在现有漏斗过滤旁增加日期列排序(正序 / 倒序)。样式对齐 Fresh Mint。 + 打开本文件即可交互:点击切换排序,表格会即时重排。 +

+ +
+ 工作量结论: + 只给 type: 'datetime' 的列加排序 —— 改公共列头组件 + 列表查询加一个排序参数,改动集中、可控。 + 若所有有漏斗的列(文本/枚举/数字/日期)都加 —— 前端交互可复用,但后端几乎每个 CRM 列表接口都要做白名单排序字段,工作量大约是「日期方案」的 2~3 倍,且数字/文本排序语义更啰嗦。 + 建议先只做日期列。 +
+ +
+
+

方案范围 A · 仅日期列 推荐

+
    +
  • 前端:列头组件识别 datetime → 显示排序控件
  • +
  • 后端:列表支持 sortField + sortOrder(白名单仅日期字段)
  • +
  • 默认仍是 create_time DESC;点列头后覆盖
  • +
+
+
+

方案范围 B · 所有漏斗列

+
    +
  • 交互可做成通用,但文本/枚举排序价值一般
  • +
  • 数字列还要处理空值、精度
  • +
  • 每个列表 API 白名单字段更多,联调面大
  • +
+
+
+ +

交互样式对比(选一种即可)

+
+
+

样式 1 · 点击列名循环 推荐

+

无排序 → 升序 → 降序 → 无。漏斗只管过滤,互不干扰。

+
+ 创建时间 ↑ + +
+
+
+

样式 2 · 双三角(Element 风格)

+

上下三角独立点选;与漏斗并排,更贴近表格库默认习惯。

+
+ 创建时间 + + + + +
+
+
+

样式 3 · 放进漏斗面板

+

排序入口藏在过滤弹层里;列头更干净,但多一步点击。

+
+ 创建时间 + + + +
+
+
+ +
+ + + +
+ +
+
+

客户列表(示意)

+ 日期列:点击列名循环 无 → 升序 → 降序 +
+
+ 已选 0 项 + + 非日期列仅有漏斗,无排序 +
+ + + + + + + + + + + + +
+
+ 客户名称 + +
+
+
+ 状态 + +
+
+
+
+
+
+
+
+
+ 负责人 + +
+
+
+ + +
+ + + + diff --git a/docs/superpowers/specs/2026-09-08-crm-date-column-sort-design.md b/docs/superpowers/specs/2026-09-08-crm-date-column-sort-design.md new file mode 100644 index 000000000..96d9b6465 --- /dev/null +++ b/docs/superpowers/specs/2026-09-08-crm-date-column-sort-design.md @@ -0,0 +1,192 @@ +# CRM 日期列头双三角排序(客户 / 报价 / 合同) + +**日期**:2026-09-08 +**状态**:待审阅 +**交互 Demo**:`docs/crm-date-column-sort-demo.html`(样式 2 · 双三角) +**前置能力**:列头漏斗整库筛选(`CrmColFilterHeader` + `useServerColumnFilters`) + +--- + +## 1. 背景与目标 + +CRM 客户、报价、合同等列表已具备列头漏斗过滤,但日期列无法在列头切换正序/倒序。分页场景下不能只排当前页,必须服务端排序。 + +### 目标 + +1. 日期列(`ColumnFilterDef.type === 'datetime'`)列头增加双三角,支持升序 / 降序 / 取消。 +2. 排序打整库;与现有列过滤、Tab、快捷搜索 **AND**;`total` / 换页 / 导出与当前排序一致。 +3. 同一时刻仅一列生效;取消后恢复默认 `create_time DESC`。 +4. 首批只改客户 + 报价 + 合同(含管理与审批共用列表页);其它 CRM 页后续再加。 + +### 非目标(本批) + +- 文本 / 枚举 / 数字列排序 +- 产品、调价函、线索、商机、合同变更/模板/审批记录等列表 +- 多列组合排序、排序状态持久化(URL / localStorage) +- 用 `el-table` 内置 `sortable` 替换自定义列头 + +--- + +## 2. 已确认产品决策 + +| # | 决策 | +|---|------| +| D1 | 交互:**双三角**(上下箭头);再点**同一方向**取消排序 | +| D2 | 范围首批:**客户管理** + **报价单管理/审批** + **合同管理/审批** | +| D3 | 审批与管理共用 `QuoteListPage` / `ContractListPage`,**一并支持**(无需单独隐藏) | +| D4 | **仅日期列**(`datetime`)显示排序三角;其它有漏斗的列不加 | +| D5 | 空值:升序时空值在最前,降序时空值在最后(与 MySQL 对 NULL 的默认次序一致) | +| D6 | 架构:公共列头 + 查询参数 `sortField`/`sortOrder` + 后端字段白名单(方案 1);不用纯前端当前页排序 | + +--- + +## 3. 首批范围 + +### 3.1 改造 + +| 层 | 路径 / 说明 | +|----|-------------| +| 列头 UI | `web-admin/src/components/business/crm-col-filter-header/index.vue`(及必要样式) | +| 排序状态 | 新建小型 composable 或扩展 provide(如 `useColumnSort` / `crmColumnSort`),由列表页 `provide` | +| 客户列表 | `web-admin/src/views/crm/customer/index.vue` | +| 报价列表 | `web-admin/src/views/crm/quote/QuoteListPage.vue`(`mode=manage\|approve`) | +| 合同列表 | `web-admin/src/views/crm/contract/ContractListPage.vue`(`mode=manage\|approve`) | +| 后端客户 | list / export:`CustomerListBody`(及公海若本批不改则不动公海)+ `list_customers` 排序分支 | +| 后端报价 | `QuoteListBody` + quote list/export CRUD 排序分支 | +| 后端合同 | `ContractListQuery` + contract list/export CRUD 排序分支 | + +**客户公海**:本批产品范围写的是「客户」主列表;**不改公海**,除非实现时发现与客户共用同一排序模块且零成本——默认不做公海。 + +### 3.2 日期列白名单(前端 prop → 后端仅允许这些) + +以各页现有 `columnFilterDefs` 中 `type: 'datetime'` 为准: + +**客户**(`customer/columnFilterDefs.ts`) +`lastTime`、`nextTime`、`dealTime`、`intoPoolTime`、`claimTime`、`protectTime`、`createTime`、`updateTime` + +**报价**(`quote/columnFilterDefs.ts`) +`quoteDate`、`expireDate`、`lastViewTime`、`createTime`、`updateTime` + +**合同**(`contract/columnFilterDefs.ts`) +`signDate`、`startDate`、`endDate`、`createTime`、`updateTime` + +映射到 ORM 字段时使用既有 snake_case 列(如 `createTime` → `create_time`)。非法 `sortField` 忽略并回退默认排序,不 500。 + +--- + +## 4. 产品与交互 + +### 4.1 列头布局 + +从左到右(居中列同样水平排列): + +1. 列标题文案 +2. **双三角**(仅 `datetime`) +3. 漏斗过滤按钮(已有) + +视觉对齐 Fresh Mint:未激活三角 `#cbd5e1`;激活 `#059669`;可点区域约 14×10px×2,避免挤爆列头。参考 Demo「样式 2」。 + +### 4.2 交互规则 + +| 操作 | 结果 | +|------|------| +| 点上三角 | 该列 `asc`;若已是该列 `asc` → 清除排序 | +| 点下三角 | 该列 `desc`;若已是该列 `desc` → 清除排序 | +| 点另一日期列三角 | 替换为新列的方向 | +| 清除排序 | 不传 `sortField`/`sortOrder`,后端默认 `create_time DESC` | +| 藏列 | 若隐藏列正是当前排序列 → **清除排序**并重查(与藏列清过滤对称) | +| 漏斗过滤变更 | 保留当前排序(AND) | + +不在工具栏强制展示「当前排序」徽章(Demo 仅示意);列头高亮三角即为状态。 + +### 4.3 空值 + +MySQL:`NULL` 小于非空 → `ASC` 时 NULL 在前,`DESC` 时 NULL 在后。本批直接 `ORDER BY ASC|DESC`,**不**额外写 `IS NULL` 表达式,除非实测与预期不符再补。 + +### 4.4 导出 + +导出请求与列表同一套:现有列筛参数 + 可选 `sortField`/`sortOrder`。无排序时导出顺序与现网默认一致。 + +--- + +## 5. 架构 + +### 5.1 查询参数 + +| 参数 | 类型 | 说明 | +|------|------|------| +| `sortField` | string | 前端列 `prop`(camelCase),须在白名单内 | +| `sortOrder` | `asc` \| `desc` | 与 `sortField` 成对出现;缺一则忽略 | + +与 `useServerColumnFilters.toQueryParams()` 合并进 list/export 请求体(POST body 或现网同等位置)。 + +### 5.2 前端状态 + +建议 `provide('crmColumnSort', api)`,与 `crmColumnFilters` 并列: + +```ts +{ + sortField: Ref + sortOrder: Ref<'asc' | 'desc' | null> + setSort(prop: string, order: 'asc' | 'desc'): void // 同向再点 → 清空 + clearSort(): void + isSortActive(prop: string, order?: 'asc' | 'desc'): boolean + toQueryParams(): { sortField?: string; sortOrder?: string } + onColumnHidden(prop: string): void // 若 prop 为当前排序列则 clear +} +``` + +`CrmColFilterHeader`:`def.type === 'datetime'` 且 inject 到 sort api 时渲染双三角;无 sort provide 的页面(产品等)行为不变,仅漏斗。 + +列表页:`setSort` / 过滤 `onChange` → `pageNum = 1` → `getList()`。 + +### 5.3 后端 + +在客户 / 报价 / 合同的 list(及对应 export)组装 query 处: + +```text +if sortField in WHITELIST and sortOrder in {asc, desc}: + order_by(mapped_column.asc() | .desc()) +else: + order_by(create_time.desc()) # 现网默认 +``` + +白名单为 dict:`prop → Column`;禁止动态拼接任意字符串进 SQL。 + +Schema:`CustomerListBody` / `QuoteListBody` / `ContractListQuery`(及 export 若独立 body)增加可选 `sortField`、`sortOrder`。 + +### 5.4 单元边界 + +| 单元 | 职责 | 依赖 | +|------|------|------| +| `CrmColFilterHeader` | 展示漏斗 + 条件渲染双三角,转发 sort 事件 | inject filters + sort | +| `useColumnSort`(名可调整) | 单列 sort 状态与 query 片段 | 无后端 | +| 各 ListPage | 合并 filter+sort 参数、藏列联动、拉数 | composable + API | +| CRUD list | 白名单 order_by | Schema 字段 | + +--- + +## 6. 测试要点 + +1. 客户:对 `createTime` 升/降/取消,换页后顺序仍正确;与漏斗日期段同时生效。 +2. 报价管理 + 报价审批:对 `quoteDate`/`expireDate` 排序;审批模式同样可见三角。 +3. 合同管理 + 合同审批:对 `signDate`/`endDate` 排序。 +4. 非法 `sortField`(抓包篡改)→ 回退默认,不报错。 +5. 含空日期列(如客户 `dealTime`):升序空在前、降序空在后。 +6. 隐藏当前排序列 → 排序清除,列表恢复默认序。 +7. 导出顺序与当前列表排序一致(在导出支持 body 的前提下)。 +8. 产品/调价函等未改页面:列头无三角、行为无回归。 + +--- + +## 7. 风险与后续 + +- **风险**:三套 list/export 漏接排序导致「列表排了导出没排」——实现时 list 与 export 共用同一 order 辅助函数。 +- **后续**:产品、调价函、线索、商机等复制同一 provide + 后端白名单即可;数字列排序另开需求。 + +--- + +## 8. 文档与 Demo + +- 本设计:`docs/superpowers/specs/2026-09-08-crm-date-column-sort-design.md` +- 交互 Demo:`docs/crm-date-column-sort-demo.html`