日期:2026-09-08
状态:待审阅
交互 Demo:docs/crm-date-column-sort-demo.html(样式 2 · 双三角)
前置能力:列头漏斗整库筛选(CrmColFilterHeader + useServerColumnFilters)
CRM 客户、报价、合同等列表已具备列头漏斗过滤,但日期列无法在列头切换正序/倒序。分页场景下不能只排当前页,必须服务端排序。
ColumnFilterDef.type === 'datetime')列头增加双三角,支持升序 / 降序 / 取消。total / 换页 / 导出与当前排序一致。create_time DESC。el-table 内置 sortable 替换自定义列头| # | 决策 |
|---|---|
| D1 | 交互:双三角(上下箭头);再点同一方向取消排序 |
| D2 | 范围首批:客户管理 + 报价单管理/审批 + 合同管理/审批 |
| D3 | 审批与管理共用 QuoteListPage / ContractListPage,一并支持(无需单独隐藏) |
| D4 | 仅日期列(datetime)显示排序三角;其它有漏斗的列不加 |
| D5 | 空值:升序时空值在最前,降序时空值在最后(与 MySQL 对 NULL 的默认次序一致) |
| D6 | 架构:公共列头 + 查询参数 sortField/sortOrder + 后端字段白名单(方案 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 排序分支 |
客户公海:本批产品范围写的是「客户」主列表;不改公海,除非实现时发现与客户共用同一排序模块且零成本——默认不做公海。
以各页现有 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。
从左到右(居中列同样水平排列):
datetime)视觉对齐 Fresh Mint:未激活三角 #cbd5e1;激活 #059669;可点区域约 14×10px×2,避免挤爆列头。参考 Demo「样式 2」。
| 操作 | 结果 |
|---|---|
| 点上三角 | 该列 asc;若已是该列 asc → 清除排序 |
| 点下三角 | 该列 desc;若已是该列 desc → 清除排序 |
| 点另一日期列三角 | 替换为新列的方向 |
| 清除排序 | 不传 sortField/sortOrder,后端默认 create_time DESC |
| 藏列 | 若隐藏列正是当前排序列 → 清除排序并重查(与藏列清过滤对称) |
| 漏斗过滤变更 | 保留当前排序(AND) |
不在工具栏强制展示「当前排序」徽章(Demo 仅示意);列头高亮三角即为状态。
MySQL:NULL 小于非空 → ASC 时 NULL 在前,DESC 时 NULL 在后。本批直接 ORDER BY <col> ASC|DESC,不额外写 IS NULL 表达式,除非实测与预期不符再补。
导出请求与列表同一套:现有列筛参数 + 可选 sortField/sortOrder。无排序时导出顺序与现网默认一致。
| 参数 | 类型 | 说明 |
|---|---|---|
sortField |
string | 前端列 prop(camelCase),须在白名单内 |
sortOrder |
asc | desc |
与 sortField 成对出现;缺一则忽略 |
与 useServerColumnFilters.toQueryParams() 合并进 list/export 请求体(POST body 或现网同等位置)。
建议 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()。
在客户 / 报价 / 合同的 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。
| 单元 | 职责 | 依赖 |
|---|---|---|
CrmColFilterHeader |
展示漏斗 + 条件渲染双三角,转发 sort 事件 | inject filters + sort |
useColumnSort(名可调整) |
单列 sort 状态与 query 片段 | 无后端 |
| 各 ListPage | 合并 filter+sort 参数、藏列联动、拉数 | composable + API |
| CRUD list | 白名单 order_by | Schema 字段 |
createTime 升/降/取消,换页后顺序仍正确;与漏斗日期段同时生效。quoteDate/expireDate 排序;审批模式同样可见三角。signDate/endDate 排序。sortField(抓包篡改)→ 回退默认,不报错。dealTime):升序空在前、降序空在后。docs/superpowers/specs/2026-09-08-crm-date-column-sort-design.mddocs/crm-date-column-sort-demo.html