feat(consignment): 客服手动指定现货匹配的界面入口

后端批次已提供判定/差异/强制留痕与双向读取接口,这里补界面,让客服真的能用上。

- 新增共享弹窗 ConsignmentManualMatchModal:订单侧(匹配弹窗「手动指定现货匹配」)与
  库存侧(寄售仓库行内「手动匹配订单」)共用同一套交互与落地逻辑,避免两套 UI 漂移
- 交互:搜索 → 选中(精确/非精确/规格冲突三档标签 + 字段级差异说明)→ 选交付方式
  (内部现货履约 / 指派供货打手)→ 非精确需勾选确认、冲突需勾选「确认规格不一致」并必填原因
  → 落地;并在落地前提示"现货立刻被占用、点错需撤销寄售履约"
- 判定规则收敛成纯函数 consignment-manual-match.ts(需不需要确认/填原因、能否提交、
  关键词过滤、标签文案)并加 4 例单测
- 服务与类型补齐:fetchWorkOrderConsignmentMatchOptions / fetchInventoryMatchableOrders,
  fulfill/assign 入参增加 allowSpecMismatch 与 matchNote
- 凭证核销失败的提示沿用既有规则(核销失败不能报成功)

验证:前端 typecheck/lint/format/44 例测试/build 全过;两个新接口未登录访问均 401
(路由已挂载、权限位生效)。
This commit is contained in:
yml2213
2026-09-17 12:23:30 +08:00
parent 94328ac643
commit a7a55d6c16
8 changed files with 771 additions and 0 deletions
@@ -0,0 +1,72 @@
/**
* 「手动指定现货」的判定辅助(纯函数,便于单测)。
*
* 业务规则(用户口径):
* - 精确匹配:直接落地;
* - 非精确匹配(订单规格没写全 / 高配充低配):必须勾选确认;
* - 规格冲突(系统/区服/系列/钻级不符):必须勾选「确认规格不一致」,且**必填原因**,
* 原因会写进工单事件与 material 留痕,方便事后核对是谁指定了这件货。
*/
export type ManualMatchType = 'exact' | 'partial' | 'conflict'
export type ManualMatchRequirement = {
needsPartialConfirm: boolean
needsMismatchConfirm: boolean
needsNote: boolean
}
export function resolveManualMatchRequirement(matchType: ManualMatchType): ManualMatchRequirement {
return {
needsPartialConfirm: matchType === 'partial',
needsMismatchConfirm: matchType === 'conflict',
needsNote: matchType === 'conflict',
}
}
export function describeManualMatchType(matchType: ManualMatchType): {
label: string
color: string
tone: 'ok' | 'warning' | 'danger'
} {
if (matchType === 'exact') {
return { label: '精确匹配', color: 'green', tone: 'ok' }
}
if (matchType === 'partial') {
return { label: '非精确匹配', color: 'orange', tone: 'warning' }
}
return { label: '规格冲突', color: 'red', tone: 'danger' }
}
/** 提交按钮是否可用:该确认的都要确认,冲突还要填原因 */
export function canSubmitManualMatch(input: {
matchType: ManualMatchType
confirmedPartial?: boolean
confirmedMismatch?: boolean
matchNote?: string
}): boolean {
const requirement = resolveManualMatchRequirement(input.matchType)
if (requirement.needsPartialConfirm && input.confirmedPartial !== true) return false
if (requirement.needsMismatchConfirm && input.confirmedMismatch !== true) return false
if (requirement.needsNote && !String(input.matchNote || '').trim()) return false
return true
}
/** 关键词过滤:现货按物品名/寄卖单号/仓库号搜,订单按单号/商品名/买家搜 */
export function filterManualMatchOptions<T>(
list: T[],
keyword: string,
pickFields: (item: T) => Array<string | number | null | undefined>,
): T[] {
const trimmed = String(keyword || '')
.trim()
.toLowerCase()
if (!trimmed) return list
return list.filter((item) =>
pickFields(item).some((value) =>
String(value ?? '')
.toLowerCase()
.includes(trimmed),
),
)
}