fix(admin-worker): 实时补丁同步筛选统计,并锁住"按工单号搜索结果被删行"的回归

线上现象:按工单号搜到的那一单,状态一变(实时 SSE),列表就变成「暂无数据」,而上方
筛选统计仍显示「共 1 单」;整页刷新后才恢复。

成因在前端实时快照补丁:旧逻辑里的 matchesWorkOrderKeyword 只比对「商品名 + 平台单号」,
不比对「工单号」,所以按 WO… 搜出来的行一旦有实时变更就被判成不匹配,直接从缓存里删掉;
而 statistics 是首次响应里带出来的、没被同步,于是出现"统计说有、列表没有"的自相矛盾。
(删行本身已由上一条提交改为:关键词筛选归入「不可证真」→ 只原地更新。)

本条补齐剩下的一致性:
1. 实时补丁增删/原地更新行时,同步维护 data.statistics 的 totalCount 与 statusCounts
   (statusCounts 按运营态口径记账,与后端 getWorkOrderStatistics 一致),列表与统计不再打架;
2. 补两个回归测试:按工单号搜到的行在状态变化后必须原地保留;插入/移除/原地改状态三种
   情况下统计都要跟得上。

验证:前端 80 项单测全绿(新增 2 项),lint / typecheck / build 通过。
This commit is contained in:
yml2213
2026-10-01 12:35:59 +08:00
parent 9573acc59d
commit d262b22a41
2 changed files with 148 additions and 9 deletions
@@ -178,7 +178,45 @@ export function shouldPrependAdminWorkOrder(items: readonly unknown[], snapshot:
}
/**
* 按工单号原地增删列表行,并同步分页总数(结构与后端 `{ data: { items, pagination } }` 一致)。
* 同步筛选统计卡(`data.statistics`)。
*
* 统计与列表是**同一个响应**里下发的,两边必须一起改:只改 items 不改 statistics,
* 就会出现"统计说还有 1 单、列表却是暂无数据"的自相矛盾界面(线上就是这样暴露的)。
* 注意 statusCounts 用的是**运营态**口径(后端 statistics 走 adminOperationalStatusExpression),
* 所以要按快照的 status 而非 rawStatus 记账。
*/
function adjustWorkOrderStatistics(
statistics: unknown,
change: { previousRow?: unknown; nextRow?: unknown; totalDelta: number },
): unknown {
if (!isRecord(statistics)) return statistics
const previousStatus = String(
(change.previousRow as { status?: unknown } | null | undefined)?.status || '',
)
const nextStatus = String(
(change.nextRow as { status?: unknown } | null | undefined)?.status || '',
)
const statusCounts = isRecord(statistics.statusCounts) ? { ...statistics.statusCounts } : null
if (statusCounts && previousStatus !== nextStatus) {
if (previousStatus) {
statusCounts[previousStatus] = Math.max(0, toNumber(statusCounts[previousStatus]) - 1)
}
if (nextStatus) {
statusCounts[nextStatus] = toNumber(statusCounts[nextStatus]) + 1
}
}
return {
...statistics,
totalCount: Math.max(0, toNumber(statistics.totalCount) + change.totalDelta),
...(statusCounts ? { statusCounts } : {}),
}
}
/**
* 按工单号原地增删列表行,并同步分页总数与筛选统计
* (结构与后端 `{ data: { items, pagination, statistics } }` 一致)。
* 行已存在时永远只做原地替换;只有 insert 为真、且列表里确实没有这一行时才插到顶部。
*/
export function patchAdminWorkOrderListItems(
@@ -194,7 +232,12 @@ export function patchAdminWorkOrderListItems(
if (id <= 0) return current
let nextItems: unknown[]
let previousRow: unknown = null
let nextRow: unknown = null
if (options.remove) {
previousRow = items.find(
(item) => toNumber((item as { workOrderId?: unknown })?.workOrderId) === id,
)
nextItems = items.filter(
(item) => toNumber((item as { workOrderId?: unknown })?.workOrderId) !== id,
)
@@ -203,12 +246,14 @@ export function patchAdminWorkOrderListItems(
(item) => toNumber((item as { workOrderId?: unknown })?.workOrderId) === id,
)
if (index >= 0) {
previousRow = items[index]
nextItems = items.map((item, itemIndex) => (itemIndex === index ? snapshot : item))
} else if (options.insert) {
nextItems = [snapshot, ...items]
} else {
return current
}
nextRow = snapshot
}
const delta = nextItems.length - items.length
@@ -226,6 +271,15 @@ export function patchAdminWorkOrderListItems(
},
}
: {}),
...(isRecord(current.data.statistics)
? {
statistics: adjustWorkOrderStatistics(current.data.statistics, {
previousRow,
nextRow,
totalDelta: delta,
}),
}
: {}),
},
}
}