2026-09-08-crm-date-column-sort-design.md 8.0 KB

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 <col> 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 并列:

{
  sortField: Ref<string | null>
  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 处:

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