feat(consignment): 分类家族口径支持二级分类匹配上级寄卖库存

- 新增 consignment-category-scope 作为分类范围单一事实来源(自身 + 祖先 + 子孙,二级分类对祖先库存按系列收窄)
- 寄卖库存列表/统计/筛选项按分类范围取数,二级分类(血宴/暗潮/怒嚎)可正常进入下拉并看到挂在一级分类下的货
- 一键查询待匹配看板:订单侧与库存侧都改为分类范围过滤,二级分类页签不再空白
- 手动匹配候选与提交校验统一按分类家族取数(工单分类 + 其一级分类),跨枪型仍不参与自动匹配,人工强制匹配需填原因
- 仓库搜索关键字增加系列、规格文本与当前/上级分类名,可直接搜「血宴」「苏尔南手枪」
- 新增分类范围单测,并补充看板跨层级用例
This commit is contained in:
2026-11-15 19:47:30 +08:00
parent d7606060a4
commit 8181ef50a5
11 changed files with 882 additions and 53 deletions
@@ -39,6 +39,7 @@ export default function ConsignmentPendingMatchModal({
onClose,
categoryId,
categoryName,
categoryParentName,
canMatch = false,
onMatched,
}: {
@@ -47,6 +48,8 @@ export default function ConsignmentPendingMatchModal({
/** 只统计该寄卖分类;不传 = 全部 */
categoryId?: number | null
categoryName?: string
/** 二级分类的上级分类名:用于说明「会带上上级同系列库存」 */
categoryParentName?: string
/** 有 consignment.fulfill 权限时才显示「选择」列 */
canMatch?: boolean
/** 快速匹配成功后通知外层刷新库存 */
@@ -297,9 +300,15 @@ export default function ConsignmentPendingMatchModal({
: '按寄卖配置匹配全部待匹配订单 × 在库待匹配现货。'
}
description={
canMatch
? '默认展示全部配对,按 枪型 → 钻石 → 璀璨 排序:完美匹配在前,同钻高璀/高钻同璀/高钻高璀次之;跨枪型不展示。点「选择」会打开手动匹配并预选该库存,确认后直接占用库存。数量不足或规格不匹配时需要人工补选。'
: '默认展示全部配对,按 枪型 → 钻石 → 璀璨 排序;当前账号没有履约权限,只能查看。'
categoryParentName
? `「${categoryName}」是二级分类:会连同上级「${categoryParentName}」中同系列的库存一起匹配(跨枪型不参与)。${
canMatch
? '点「选择」会打开手动匹配并预选该库存,确认后直接占用库存。'
: '当前账号没有履约权限,只能查看。'
}`
: canMatch
? '默认展示全部配对,按 枪型 → 钻石 → 璀璨 排序:完美匹配在前,同钻高璀/高钻同璀/高钻高璀次之;跨枪型不展示。点「选择」会打开手动匹配并预选该库存,确认后直接占用库存。数量不足或规格不匹配时需要人工补选。'
: '默认展示全部配对,按 枪型 → 钻石 → 璀璨 排序;当前账号没有履约权限,只能查看。'
}
/>
@@ -260,16 +260,21 @@ export default function ConsignmentWorkOrderMatchModal({
/>
) : null}
{!candidatesQuery.isLoading && !candidatesQuery.isError ? (
<Space wrap size={8} style={{ marginBottom: 12 }}>
<Input.Search
allowClear
placeholder="搜索寄卖单号 / 库存号 / 物品 / 规格 / 打手"
style={{ width: 320 }}
value={keyword}
onChange={(event) => setKeyword(event.target.value)}
/>
<Typography.Text type="secondary">
显示 {visibleOptions.length} / 候选 {options.length} 条
<Space direction="vertical" size={8} style={{ width: '100%', marginBottom: 12 }}>
<Space wrap size={8}>
<Input.Search
allowClear
placeholder="搜索寄卖单号 / 库存号 / 物品 / 规格 / 打手"
style={{ width: 320 }}
value={keyword}
onChange={(event) => setKeyword(event.target.value)}
/>
<Typography.Text type="secondary">
显示 {visibleOptions.length} / 候选 {options.length} 条
</Typography.Text>
</Space>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
候选范围:本工单分类及其上级分类下的全部在库寄卖单(跨枪型会标为不匹配,需填原因才能强制匹配)。
</Typography.Text>
</Space>
) : null}
@@ -1026,6 +1026,12 @@ export default function ConsignmentInventoryPanel() {
}}
/>
{activeFilterCategory?.parentId ? (
<Text type="secondary" style={{ fontSize: 12 }}>
含上级「{activeFilterCategory.parentName || '一级分类'}」同系列
{activeFilterCategory.name} 库存
</Text>
) : null}
{dynamicFilters.map((field) => {
const value = specFilters[field.key]
if (field.type === 'select' || field.type === 'radio') {
@@ -1640,6 +1646,9 @@ export default function ConsignmentInventoryPanel() {
open={pendingMatchOpen}
categoryId={categoryId}
categoryName={activeFilterCategory?.name}
categoryParentName={
activeFilterCategory?.parentId ? activeFilterCategory.parentName : undefined
}
canMatch={canFulfillConsignment}
onClose={() => setPendingMatchOpen(false)}
onMatched={() => void inventoriesQuery.refetch()}