fix(worker): 打手端实时补丁不再误删"按工单号搜到的行",并同步分页总数

这是后台那个"搜工单号搜不到、必须刷新整页"在打手端的同源副本:
WorkerLayout 的 operation = event.operation === 'remove' || !matches ? 'remove' : 'upsert'
只要前端判"不匹配"就删行,而前端关键词匹配器**没有比对工单号**——
后端搜索表达式的第一项恰恰就是 work_order_no(还有 material_json 里更多前端不知道的字段)。
于是按工单号搜索时,该单一旦有实时变更,行就被从缓存删掉、列表变空。

修法与后台侧对齐:
1. 抽出 utils/worker-realtime-work-orders.ts(可单测、无运行时依赖),关键词匹配补上
   workOrderNo,并改成三态判定——状态/分类等可证伪维度不符才删行;关键词能找到=match
   (允许插入),找不到=unknown(不删不插,只就地更新)。
2. patchWorkerWorkOrderListEnvelope 增删行时同步 data.pagination.total,
   修掉"列表空了、分页器还显示有 N 条"。
3. 大厅无限下拉只在第 1 页允许插入,其余页只就地更新。

验证:前端 86 项单测全绿(新增 6 项,覆盖"按工单号搜索+实时变更不得删行"、
unknown 只就地更新、删/插同步总数、无限下拉页边界)。
This commit is contained in:
yml2213
2026-10-01 13:48:05 +08:00
parent 2d75be702d
commit f4c405d51a
3 changed files with 271 additions and 59 deletions
@@ -0,0 +1,134 @@
/**
* 打手端列表的实时快照补丁(与后台的 admin-realtime-work-orders.ts 同源同策)。
*
* 这里修的是和后台"搜工单号搜不到、必须整页刷新"完全同一个坑:
* 旧逻辑 `operation = event.operation === 'remove' || !matches ? 'remove' : 'upsert'`
* 只要前端认为"不匹配"就把行从缓存里删掉,而**前端的关键词匹配天生不完整**——
* 后端查询串的第一项是工单号(work_order_no,见 work-order-query-repo 的搜索表达式),
* 前端匹配器里却没有它;而且后端还会搜 material_json 里更多字段。
* 于是"按工单号搜索 → 该单有实时变更 → 行被删 → 列表变空",只能刷新整页恢复。
*
* 所以判定改成三态:
* - 状态 / 分类等**可证伪**的维度不符 → mismatch,才允许删行;
* - 关键词能找到 → match,允许插入到第 1 页顶部;
* - 有关键词但在前端已知字段里找不到 → unknown,**既不移除也不插入,只就地更新**。
*
* 另外补上分页总数同步:旧实现只改 items,删行后会出现"列表空了、分页器还显示有 N 条"。
*/
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
function toNumber(value: unknown): number {
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : 0
}
type WorkerListEvent = {
operation?: unknown
entityId?: unknown
data?: unknown
}
/** 前端能自己核对的关键词字段(后端搜索范围更广,所以不一致时只判 unknown)。 */
function collectKeywordFieldValues(item: Record<string, unknown>): unknown[] {
const material = isRecord(item.material) ? item.material : {}
const collect = isRecord(material.collect) ? material.collect : {}
const fields = isRecord(collect.fields) ? collect.fields : {}
return [
// 关键补充:工单号。后端搜索表达式的第一项就是它,缺了它"按工单号搜索"必然误判。
item.workOrderNo,
item.productName,
item.platformOrderId,
fields.gameId,
fields.gameNickname,
fields.gameAccount,
fields.roleName,
material.gameId,
material.gameNickname,
]
}
export function matchesWorkerWorkOrderKeyword(item: unknown, keyword: string): boolean {
const normalizedKeyword = String(keyword || '')
.trim()
.toLowerCase()
if (!normalizedKeyword) return true
if (!isRecord(item)) return false
return collectKeywordFieldValues(item)
.map((value) => String(value ?? '').toLowerCase())
.some((value) => value.includes(normalizedKeyword))
}
/** 关键词维度的三态判定:能找到= match;有关键词但找不到= unknown(不可证伪)。 */
export function evaluateWorkerKeywordVerdict(item: unknown, keyword: string): 'match' | 'unknown' {
return matchesWorkerWorkOrderKeyword(item, keyword) ? 'match' : 'unknown'
}
/** 按工单号原地增删行,并同步分页总数(结构同后端 `{ data: { items, pagination } }`)。 */
export function patchWorkerWorkOrderListEnvelope(
current: unknown,
event: WorkerListEvent,
insert = false,
): unknown {
if (!isRecord(current) || !isRecord(current.data) || !Array.isArray(current.data.items)) {
return current
}
const items = current.data.items as unknown[]
const data = isRecord(event.data) ? event.data : {}
const id = toNumber(data.workOrderId) || toNumber(event.entityId)
if (id <= 0) return current
let nextItems: unknown[]
if (event.operation === 'remove') {
nextItems = items.filter(
(entry) => toNumber((entry as { workOrderId?: unknown })?.workOrderId) !== id,
)
} else {
const index = items.findIndex(
(entry) => toNumber((entry as { workOrderId?: unknown })?.workOrderId) === id,
)
if (index >= 0) {
nextItems = items.map((entry, itemIndex) => (itemIndex === index ? event.data : entry))
} else if (insert) {
nextItems = [event.data, ...items]
} else {
return current
}
}
const delta = nextItems.length - items.length
const pagination = isRecord(current.data.pagination) ? current.data.pagination : null
return {
...current,
data: {
...current.data,
items: nextItems,
// 删行/插行都要同步总数,否则出现"列表空着、分页器还有 N 条"
...(pagination && delta !== 0
? {
pagination: {
...pagination,
total: Math.max(0, toNumber(pagination.total) + delta),
},
}
: {}),
},
}
}
/** 无限下拉(大厅):第 1 页允许插入,其余页只做就地更新。 */
export function patchWorkerWorkOrderInfiniteEnvelope(
current: unknown,
event: WorkerListEvent,
insert = false,
): unknown {
if (!isRecord(current) || !Array.isArray(current.pages)) return current
return {
...current,
pages: current.pages.map((page: unknown, index: number) =>
patchWorkerWorkOrderListEnvelope(page, event, insert && index === 0),
),
}
}