feat(consignment): 苏尔南手枪按枪型/钻石/璀璨分层匹配并过滤跨枪型
排序:完美匹配 > 同钻高璀 > 高钻同璀 > 高钻高璀 > 低配;跨枪型不展示不参与;高配仍需填人工原因。
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
} from '@/services/admin/consignment'
|
||||
import type { PendingPistolMatchPair } from '@/types/consignment'
|
||||
import { formatDateTime } from '@/utils/date-time'
|
||||
import { resolveConsignmentPriorityTagColor } from '@/utils/consignment-match-priority'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
@@ -140,6 +141,14 @@ export default function ConsignmentInventoryMatchModal({
|
||||
key: 'reason',
|
||||
render: (_, record) => (
|
||||
<Space direction="vertical" size={2}>
|
||||
{record.priorityLabel ? (
|
||||
<Tag
|
||||
color={resolveConsignmentPriorityTagColor(record.priorityLabel)}
|
||||
style={{ marginInlineEnd: 0, fontSize: 11 }}
|
||||
>
|
||||
{record.priorityLabel}
|
||||
</Tag>
|
||||
) : null}
|
||||
<Text style={{ fontSize: 12 }}>{record.reason || '-'}</Text>
|
||||
{record.conflicts.length > 0 ? (
|
||||
<Text type="danger" style={{ fontSize: 11 }}>
|
||||
@@ -205,7 +214,7 @@ export default function ConsignmentInventoryMatchModal({
|
||||
type="info"
|
||||
showIcon
|
||||
message="按寄卖配置与数量判定候选工单;多数量库存会按剩余量扣减,不会被整批消耗。"
|
||||
description="精确匹配可直接提交;非精确或规格冲突必须勾选强制匹配并填写原因。拼单工单必须一次拼满,单条库存不够时请到工单侧匹配。"
|
||||
description="候选按 枪型 → 钻石 → 璀璨 排序,完美匹配在前;跨枪型不展示。精确匹配可直接提交;非精确或规格冲突必须勾选强制匹配并填写原因。拼单工单必须一次拼满,单条库存不够时请到工单侧匹配。"
|
||||
/>
|
||||
<Space wrap size={12}>
|
||||
<Checkbox
|
||||
|
||||
@@ -8,6 +8,7 @@ import PercentTable from '@/components/common/PercentTable'
|
||||
import { fetchPendingPistolMatches } from '@/services/admin/consignment'
|
||||
import type { PendingPistolMatchPair } from '@/types/consignment'
|
||||
import { formatDateTime } from '@/utils/date-time'
|
||||
import { resolveConsignmentPriorityTagColor } from '@/utils/consignment-match-priority'
|
||||
|
||||
import ConsignmentWorkOrderMatchModal from './ConsignmentWorkOrderMatchModal'
|
||||
|
||||
@@ -221,6 +222,14 @@ export default function ConsignmentPendingMatchModal({
|
||||
width: 300,
|
||||
render: (_, record) => (
|
||||
<Space direction="vertical" size={2}>
|
||||
{record.priorityLabel ? (
|
||||
<Tag
|
||||
color={resolveConsignmentPriorityTagColor(record.priorityLabel)}
|
||||
style={{ marginInlineEnd: 0, fontSize: 11 }}
|
||||
>
|
||||
{record.priorityLabel}
|
||||
</Tag>
|
||||
) : null}
|
||||
<Text style={{ fontSize: 12 }}>{record.reason || '-'}</Text>
|
||||
{record.conflicts.length > 0 ? (
|
||||
<Text type="danger" style={{ fontSize: 11 }}>
|
||||
@@ -289,8 +298,8 @@ export default function ConsignmentPendingMatchModal({
|
||||
}
|
||||
description={
|
||||
canMatch
|
||||
? '默认展示全部配对(含不匹配项),按打分从高到低排列;点「选择」会打开手动匹配并预选该库存,确认后直接占用库存。数量不足或规格不匹配时需要人工补选。'
|
||||
: '默认展示全部配对(含不匹配项),按打分从高到低排列;当前账号没有履约权限,只能查看。'
|
||||
? '默认展示全部配对,按 枪型 → 钻石 → 璀璨 排序:完美匹配在前,同钻高璀/高钻同璀/高钻高璀次之;跨枪型不展示。点「选择」会打开手动匹配并预选该库存,确认后直接占用库存。数量不足或规格不匹配时需要人工补选。'
|
||||
: '默认展示全部配对,按 枪型 → 钻石 → 璀璨 排序;当前账号没有履约权限,只能查看。'
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
describeConsignmentMatch,
|
||||
resolveMatchCoverage,
|
||||
} from '@/utils/consignment-match-coverage'
|
||||
import { resolveConsignmentPriorityTagColor } from '@/utils/consignment-match-priority'
|
||||
|
||||
function money(fen: number) {
|
||||
return `¥${((Number(fen || 0) || 0) / 100).toFixed(2)}`
|
||||
@@ -93,16 +94,11 @@ export default function ConsignmentWorkOrderMatchModal({
|
||||
: totalQuantity
|
||||
|
||||
const normalizedKeyword = keyword.trim().toLowerCase()
|
||||
// 默认展示全部候选,按匹配打分降序;关键词只做过滤,不改变排序口径。
|
||||
// 后端已按「枪型 → 钻石 → 璀璨」排好序;这里只做关键词过滤,必须保持原顺序。
|
||||
const visibleOptions = useMemo(() => {
|
||||
const filtered = normalizedKeyword
|
||||
return normalizedKeyword
|
||||
? options.filter((item) => buildCandidateHaystack(item).includes(normalizedKeyword))
|
||||
: options
|
||||
return [...filtered].sort((left, right) => {
|
||||
const scoreDiff = computeConsignmentMatchScore(right) - computeConsignmentMatchScore(left)
|
||||
if (scoreDiff !== 0) return scoreDiff
|
||||
return Number(right.remainingQuantity || 0) - Number(left.remainingQuantity || 0)
|
||||
})
|
||||
}, [normalizedKeyword, options])
|
||||
|
||||
const selectedOptions = useMemo(
|
||||
@@ -314,7 +310,7 @@ export default function ConsignmentWorkOrderMatchModal({
|
||||
type={remainingNeeded <= 0 ? 'warning' : 'info'}
|
||||
showIcon
|
||||
message={infoMessage}
|
||||
description="默认按匹配打分从高到低展示全部候选;非精确/冲突必须填写人工原因;拼单必须一次拼满,失败不会占用任何库存。"
|
||||
description="默认按 枪型 → 钻石 → 璀璨 排序:完美匹配在前,同钻高璀/高钻同璀/高钻高璀次之,低配靠后;非精确/冲突必须填写人工原因;拼单必须一次拼满,失败不会占用任何库存。"
|
||||
/>
|
||||
<Space wrap size={8}>
|
||||
<Tag color="blue">
|
||||
@@ -356,6 +352,22 @@ export default function ConsignmentWorkOrderMatchModal({
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '匹配层级',
|
||||
key: 'priority',
|
||||
width: 105,
|
||||
render: (_, item) =>
|
||||
item.priorityLabel ? (
|
||||
<Tag
|
||||
color={resolveConsignmentPriorityTagColor(item.priorityLabel)}
|
||||
style={{ marginInlineEnd: 0 }}
|
||||
>
|
||||
{item.priorityLabel}
|
||||
</Tag>
|
||||
) : (
|
||||
<Typography.Text type="secondary">-</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '寄卖单号',
|
||||
key: 'consignmentNo',
|
||||
|
||||
@@ -349,6 +349,12 @@ export type PendingPistolMatchPair = {
|
||||
score: number
|
||||
reason: string
|
||||
conflicts: string[]
|
||||
/** 枪型 → 钻石 → 璀璨 的排序层级文案 */
|
||||
priorityLabel?: string
|
||||
seriesMatched?: boolean
|
||||
seriesSpecified?: boolean
|
||||
diamondGap?: number
|
||||
brillianceGap?: number
|
||||
/** 自定义规格逐字段对比;内置五字段为空,沿用旧评分原因 */
|
||||
matchDetails?: Array<{
|
||||
key: string
|
||||
@@ -560,6 +566,12 @@ export type ConsignmentMatchCandidate = {
|
||||
specText?: string
|
||||
matchType?: 'exact' | 'partial' | 'conflict' | 'unknown'
|
||||
matchReasons?: string[]
|
||||
/** 枪型 → 钻石 → 璀璨 的排序层级文案 */
|
||||
priorityLabel?: string
|
||||
seriesMatched?: boolean | null
|
||||
seriesSpecified?: boolean
|
||||
diamondGap?: number | null
|
||||
brillianceGap?: number | null
|
||||
matchDetails?: Array<{
|
||||
key: string
|
||||
label: string
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/** 匹配层级标签颜色:完美绿、高配蓝系、低配橙、冲突/缺失红。 */
|
||||
export function resolveConsignmentPriorityTagColor(label: string | null | undefined): string {
|
||||
const value = String(label || '').trim()
|
||||
if (!value) return 'default'
|
||||
if (value === '完美匹配') return 'green'
|
||||
if (value === '同钻高璀') return 'blue'
|
||||
if (value === '高钻同璀') return 'cyan'
|
||||
if (value === '高钻高璀') return 'geekblue'
|
||||
if (value === '低配') return 'orange'
|
||||
if (value.includes('冲突') || value.includes('缺失') || value.includes('不符')) {
|
||||
return 'red'
|
||||
}
|
||||
return 'default'
|
||||
}
|
||||
@@ -73,17 +73,17 @@ test('后端:手动候选、手动复检、待匹配看板都使用手动口
|
||||
assert.ok(pending.includes("matchScope: 'manual'"), '一键查询待匹配订单必须按手动口径')
|
||||
})
|
||||
|
||||
test('内部库存匹配弹窗:默认展示全部候选,按打分降序,并显示寄卖单号', () => {
|
||||
test('内部库存匹配弹窗:默认展示全部候选,按枪型/钻石/璀璨排序,并显示寄卖单号', () => {
|
||||
const source = readSource('../src/components/admin/ConsignmentWorkOrderMatchModal.tsx')
|
||||
|
||||
assert.ok(source.includes('默认按匹配打分从高到低展示全部候选'), '默认展示口径必须在界面上说明')
|
||||
assert.ok(source.includes('默认按 枪型 → 钻石 → 璀璨 排序'), '默认展示口径必须在界面上说明')
|
||||
assert.ok(
|
||||
!source.includes("if (!searchable) return (item.matchType || 'exact') === 'exact'"),
|
||||
'不能再默认只过滤精确匹配',
|
||||
)
|
||||
assert.ok(
|
||||
source.includes('computeConsignmentMatchScore(right) - computeConsignmentMatchScore(left)'),
|
||||
'列表必须按匹配打分降序排列',
|
||||
!source.includes('computeConsignmentMatchScore(right) - computeConsignmentMatchScore(left)'),
|
||||
'列表必须保持后端排序,不能再按打分重排',
|
||||
)
|
||||
assert.ok(
|
||||
source.includes('buildCandidateHaystack(item).includes(normalizedKeyword)'),
|
||||
|
||||
@@ -42,15 +42,15 @@ test('一键查询待匹配订单:说明文案不再声称「不直接改单
|
||||
assert.ok(source.includes('「手动匹配方式」') || source.includes('手动匹配'))
|
||||
})
|
||||
|
||||
test('一键查询待匹配订单:默认展示全部配对并按打分降序', () => {
|
||||
test('一键查询待匹配订单:默认展示全部配对并按枪型/钻石/璀璨排序', () => {
|
||||
const source = readSource('../src/components/admin/ConsignmentPendingMatchModal.tsx')
|
||||
|
||||
assert.ok(source.includes('includeConflicts: true'), '默认必须包含冲突(0 分)配对')
|
||||
assert.ok(!source.includes('显示 0 分不匹配项'), '旧的勾选框必须删除')
|
||||
assert.ok(
|
||||
!source.includes('right.score - left.score'),
|
||||
'不得在前端只按分数重排,否则会覆盖后端的「系列/钻石/璀璨」优先级',
|
||||
'不得在前端只按分数重排,否则会覆盖后端的「枪型/钻石/璀璨」优先级',
|
||||
)
|
||||
assert.ok(source.includes('不匹配 {data?.conflictCount ?? 0}'), '必须展示不匹配数量')
|
||||
assert.ok(source.includes('默认展示全部配对(含不匹配项)'), '界面必须说明默认口径')
|
||||
assert.ok(source.includes('默认展示全部配对,按 枪型 → 钻石 → 璀璨 排序'), '界面必须说明默认口径')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user