Files
shanxia2.0/docs/superpowers/plans/2026-09-08-bx-date-column-sort.md
T

15 KiB
Raw Blame History

报销域日期列头双三角排序 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 在有日期列的报销相关列表上,为 datetime 列头增加与 CRM 相同的双三角整库排序(视图合并页为拉全量后的客户端排序)。

Architecture: 扩展 ColumnHeaderFilter:存在 crmColumnSort inject 且 type==='datetime' 时渲染双三角。页面接入已有 useColumnSort。后端复用 resolve_datetime_order_by;applyTime 映射 create_time。视图 mine/todo/all 已 fetchAllPagedRecords 合并双边数据,改 sortMergedRows 按当前 sort 状态排。

Tech Stack: Vue 3、ColumnHeaderFilter、useColumnSort、FastAPI、SQLAlchemy、resolve_datetime_order_by

Spec: docs/superpowers/specs/2026-09-08-bx-date-column-sort-design.md


文件结构

操作 文件 职责
Modify web-admin/src/components/business/column-header-filter/index.vue datetime + inject 时显示双三角
Modify backend/app/services/bx/base.py get_paged 支持可选 order_by
Create backend/app/services/bx/list_sort.py 各实体白名单封装(调用 crm resolve_datetime_order_by)
Create backend/tests/test_bx_list_sort.py 白名单映射单测
Modify backend/app/api/bx/advance.py + services/bx/advance.py 透传 sort → order
Modify backend/app/api/bx/reimburse.py + services/bx/reimburse.py 同上
Modify backend/app/api/bx/payment.py + 付款 list 路径 applyTime/payTime
Modify backend/app/api/bx/invoice.py + services/bx/invoice.py 我的发票/流水 createTime
Modify backend/app/api/bx/export_common.py BxApplyListExportBody 显式 sortField/sortOrder(可选,extra=allow 已可透传)
Modify 各 views/bx/**/index.vue(见 Task 6–10) provide + merge query + 藏列

不做: expense-item、workflow、invoice-wallet/admin;非日期列。


Task 1: ColumnHeaderFilter 双三角

Files:

  • Modify: web-admin/src/components/business/column-header-filter/index.vue

  • Step 1: 改模板根结构

当前根节点是单个 el-popover。改为外包一层,在 popover 之前插入三角(与 CRM 列头「标题 | 三角 | 漏斗」一致;标题仍由页面 label 提供):

<template>
  <span class="col-header-filter-root">
    <span
      v-if="showSort"
      class="col-header-sort-pair"
      @click.stop
    >
      <button
        type="button"
        class="col-header-sort-btn"
        :class="{ on: sortApi!.isSortActive(prop, 'asc') }"
        title="升序"
        aria-label="升序"
        @click="sortApi!.setSort(prop, 'asc')"
      >▲</button>
      <button
        type="button"
        class="col-header-sort-btn"
        :class="{ on: sortApi!.isSortActive(prop, 'desc') }"
        title="降序"
        aria-label="降序"
        @click="sortApi!.setSort(prop, 'desc')"
      >▼</button>
    </span>
    <el-popover ...现有内容不变...>
      ...
    </el-popover>
  </span>
</template>
  • Step 2: script 增加 inject
import { computed, inject, ref, watch } from 'vue'
// ...

const sortApi = inject<{
  setSort: (prop: string, order: 'asc' | 'desc') => void
  isSortActive: (prop: string, order?: 'asc' | 'desc') => boolean
} | null>('crmColumnSort', null)

const showSort = computed(() => Boolean(sortApi && props.type === 'datetime'))

样式 .col-header-sort-pair 已在同文件全局 style 中(CRM Task 6 已加),无需重复。可补:

.col-header-filter-root {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  flex-shrink: 0;
}
  • Step 3: 确认无 provide 时不显示三角;CRM 的 CrmColFilterHeader 仍自带三角(两套并存可接受)

  • Step 4: Commit

git add web-admin/src/components/business/column-header-filter/index.vue
git commit -m "feat(web-admin): show dual carets on ColumnHeaderFilter datetime columns."

Task 2: BX 排序白名单 helper + get_paged 可选 order

Files:

  • Create: backend/app/services/bx/list_sort.py

  • Create: backend/tests/test_bx_list_sort.py

  • Modify: backend/app/services/bx/base.py

  • Step 1: 写失败测试

# backend/tests/test_bx_list_sort.py
from app.models.bx_models import BxAdvanceApply, BxReimburseApply, BxInvoice, BxInvoiceLedger
from app.services.bx.list_sort import (
    order_advance,
    order_reimburse,
    order_invoice,
    order_ledger,
)


def test_advance_apply_time_asc():
    expr = order_advance("applyTime", "asc")
    assert "create_time" in str(expr).lower()
    assert "asc" in str(expr).lower()


def test_advance_issue_time():
    expr = order_advance("issueTime", "desc")
    assert "issue_time" in str(expr).lower()


def test_advance_invalid_falls_back():
    default = BxAdvanceApply.create_time.desc()
    assert order_advance("hack", "asc", default=default) is default


def test_reimburse_pay_time():
    expr = order_reimburse("payTime", "desc")
    assert "pay_time" in str(expr).lower()


def test_invoice_create_time():
    expr = order_invoice("createTime", "asc")
    assert "create_time" in str(expr).lower()

(若模型 import 路径不同,以仓库 app.models 实际模块为准。)

  • Step 2: pytest 应 FAIL
cd backend && source venv/bin/activate && PYTHONPATH=. pytest tests/test_bx_list_sort.py -v
  • Step 3: 实现 list_sort.py
# backend/app/services/bx/list_sort.py
from __future__ import annotations
from typing import Any, Optional

from app.models.bx_models import (
    BxAdvanceApply,
    BxInvoice,
    BxInvoiceLedger,
    BxReimburseApply,
)
from app.services.crm.list_sort import resolve_datetime_order_by

ADVANCE_WHITELIST = {
    "applyTime": BxAdvanceApply.create_time,
    "issueTime": BxAdvanceApply.issue_time,
}
REIMBURSE_WHITELIST = {
    "applyTime": BxReimburseApply.create_time,
    "payTime": BxReimburseApply.pay_time,
}
INVOICE_WHITELIST = {"createTime": BxInvoice.create_time}
LEDGER_WHITELIST = {"createTime": BxInvoiceLedger.create_time}


def order_advance(sort_field: Optional[str], sort_order: Optional[str], default=None):
    if default is None:
        default = BxAdvanceApply.create_time.desc()
    return resolve_datetime_order_by(sort_field, sort_order, ADVANCE_WHITELIST, default)


def order_reimburse(sort_field: Optional[str], sort_order: Optional[str], default=None):
    if default is None:
        default = BxReimburseApply.create_time.desc()
    return resolve_datetime_order_by(sort_field, sort_order, REIMBURSE_WHITELIST, default)


def order_invoice(sort_field: Optional[str], sort_order: Optional[str], default=None):
    if default is None:
        default = BxInvoice.create_time.desc()
    return resolve_datetime_order_by(sort_field, sort_order, INVOICE_WHITELIST, default)


def order_ledger(sort_field: Optional[str], sort_order: Optional[str], default=None):
    if default is None:
        default = BxInvoiceLedger.create_time.desc()
    return resolve_datetime_order_by(sort_field, sort_order, LEDGER_WHITELIST, default)


def parse_sort_params(params: dict) -> tuple:
    """从 request/body dict 取 sortField/sortOrder(兼容 snake)。"""
    sf = params.get("sortField") or params.get("sort_field")
    so = params.get("sortOrder") or params.get("sort_order")
    return (str(sf).strip() if sf else None), (str(so).strip() if so else None)
  • Step 4: BxCrudService.get_paged 增加可选 order_by=None

在现有默认 order_by(sort_order, create_time) 分支前:

order_by: Optional[Any] = None,
# ...
if order_by is not None:
    query = query.order_by(order_by)
elif hasattr(model, "sort_order"):
    ...
else:
    query = query.order_by(model.create_time.desc())
  • Step 5: 测试通过并 Commit
git add backend/app/services/bx/list_sort.py backend/tests/test_bx_list_sort.py backend/app/services/bx/base.py
git commit -m "feat(bx): add datetime list sort helpers and get_paged order_by."

Task 3: 备用金 API/Service 接排序

Files:

  • Modify: backend/app/api/bx/advance.py

  • Modify: backend/app/services/bx/advance.py

  • Modify: backend/app/api/bx/export_common.py(可选显式字段)

  • Step 1: BxApplyListExportBody 增加

sort_field: Optional[str] = Field(default=None, alias="sortField")
sort_order: Optional[str] = Field(default=None, alias="sortOrder")

(extra=allow 已有则 GET query 的 sortField 进 _merge_query 即可;显式字段便于导出 body。)

  • Step 2: 所有走 AdvanceService.get_paged 的 list/export

在调用前:

from app.services.bx.list_sort import order_advance, parse_sort_params

sf, so = parse_sort_params(params)
order = order_advance(sf, so)
rows, total = AdvanceService.get_paged(..., order_by=order)

确认 AdvanceService.get_paged 把 order_by 传给 BxCrudService.get_paged(若当前是薄封装,补参数)。

  • Step 3: pending_approval / approved_by_me

将硬编码 .order_by(BxAdvanceApply.create_time.desc()) 改为 order_by(order_advance(sf, so));sf/so 从该接口的 params/_merge_query 读取。

  • Step 4: Commit
git commit -m "feat(bx): support datetime column sort on advance lists."

Task 4: 报销 API/Service 接排序

Files:

  • Modify: backend/app/api/bx/reimburse.py
  • Modify: backend/app/services/bx/reimburse.py

镜像 Task 3:get_paged、pending_approval、approved_by_me 及对应 export 使用 order_reimburse。白名单含 applyTime、payTime。

git commit -m "feat(bx): support datetime column sort on reimburse lists."

Task 5: 支付发放冲抵接排序

Files:

  • Modify: backend/app/api/bx/payment.py(及实际 list/export 调用的 service)

待处理/已处理列表对 BxReimburseApply 排序时用 order_reimburse(applyTime/payTime)。冲抵记录子表(BxReimburseOffset.create_time)本批不改(非主列表漏斗列)。

git commit -m "feat(bx): support datetime column sort on payment lists."

Task 6: 我的发票夹后端

Files:

  • Modify: backend/app/api/bx/invoice.py

  • Modify: backend/app/services/bx/invoice.py

  • list_invoices:经 get_paged 传入 order_by=order_invoice(sf, so);API _param_get / merge 取出 sort。

  • list_ledgers:将 order_by(BxInvoiceLedger.create_time.desc()) 换为 order_ledger(sf, so)。

git commit -m "feat(bx): support datetime column sort on my invoice wallet lists."

Task 7: 接线备用金三页前端

Files:

  • web-admin/src/views/bx/advance/apply/index.vue
  • web-admin/src/views/bx/advance/approve/index.vue
  • web-admin/src/views/bx/advance/issue/index.vue

每页:

import { useColumnSort } from '@/composables/useColumnSort'

const {
  setSort, clearSort, isSortActive, sortField, sortOrder,
  toQueryParams: toSortQueryParams,
  watchVisibleColumns: watchSortVisibleColumns
} = useColumnSort({ onChange: () => { /* 现有 reset page + load */ } })

provide('crmColumnSort', { setSort, clearSort, isSortActive, sortField, sortOrder })

列表/导出参数合并 ...toSortQueryParams();watchSortVisibleColumns(visibleFilterProps)。

审批/发放若双 Tab 共用一套 filters,共用一套 sort 即可。

git commit -m "feat(bx): wire date column sort on advance apply/approve/issue pages."

Task 8: 接线报销申请/审批前端

Files:

  • web-admin/src/views/bx/reimburse/apply/index.vue
  • web-admin/src/views/bx/reimburse/approve/index.vue

同 Task 7 模式。

git commit -m "feat(bx): wire date column sort on reimburse apply/approve pages."

Task 9: 接线支付页前端

Files:

  • web-admin/src/views/bx/payment/index.vue

同模式;payTime 仅已处理 Tab 有漏斗,藏列时 watchSortVisibleColumns 清排序。

git commit -m "feat(bx): wire date column sort on payment page."

Task 10: 接线我的发票夹前端

Files:

  • web-admin/src/views/bx/invoice-wallet/mine/index.vue

发票表与流水表各有 useServerColumnFilters。推荐:

  • 两套 useColumnSort(invoiceSort / ledgerSort)
  • provide 时按当前 Tab 提供,或给 ColumnHeaderFilter 增加可选 injectKey(若改组件成本高:切换 Tab 时 provide 无法动态——改用 同一套 sort,切 Tab 时 clearSort,更简单)

选定更简单:共用一套 useColumnSort,切 Tab 时 clearSort(),请求分别带 toSortQueryParams()。

git commit -m "feat(bx): wire date column sort on my invoice wallet page."

Task 11: 视图三页(合并列表 · 客户端排序)

Files:

  • web-admin/src/views/bx/view/mine/index.vue
  • web-admin/src/views/bx/view/todo/index.vue
  • web-admin/src/views/bx/view/all/index.vue

现状:sortMergedRows 固定 applyTime 倒序。这些页拉全量后合并,以客户端排序为准:

function sortMergedRows(rows: any[]) {
  const field = sortField.value || 'applyTime'
  const order = sortOrder.value || 'desc'
  return [...rows].sort((a, b) => {
    const av = String(a[field] || '')
    const bv = String(b[field] || '')
    if (!av && !bv) return 0
    if (!av) return order === 'asc' ? -1 : 1  // 空值:升序前、降序后
    if (!bv) return order === 'asc' ? 1 : -1
    const cmp = av.localeCompare(bv)
    return order === 'asc' ? cmp : -cmp
  })
}

仍 provide('crmColumnSort') 驱动列头三角;onChange → loadData()。
可选:请求时仍带 sortField 给单边 API(非必须,合并后会再排)。

git commit -m "feat(bx): wire date column sort on bx view mine/todo/all pages."

Task 12: 回归与图谱

  • Step 1: PYTHONPATH=. pytest tests/test_bx_list_sort.py tests/test_crm_list_sort.py -v
  • Step 2: pnpm dlx vitest run src/composables/useColumnSort.test.ts
  • Step 3: 手工:备用金申请 applyTime;发放 issueTime;报销申请;支付 payTime;我的发票夹;我的申请单合并列表;费用项目无三角。
  • Step 4: ./scripts/update-graph.sh all 后仅提交图谱变更:
git commit -m "chore: update graphify after BX date column sort."

Spec 覆盖自检

Spec 项 Task
ColumnHeaderFilter 扩展 1
applyTime→create_time 白名单 2–5
备用金/报销/支付/发票夹 3–6, 7–10
视图合并页 11(客户端排序,符合现网拉全量架构)
跳过费用项目/工作流/发票夹管理 文件结构「不做」
空值升降序 2 helper + 11 sortMergedRows
与 CRM 同 inject key 1, 7–11

无 TBD;视图页明确为客户端排序,避免与「整库服务端」表述冲突——spec §7 已提示 union/合并风险,本计划按现网 fetchAllPagedRecords 落地。