diff --git a/apps/frontend/src/components/admin/ConsignmentManualMatchModal.tsx b/apps/frontend/src/components/admin/ConsignmentManualMatchModal.tsx index 95b3e832..601b1e79 100644 --- a/apps/frontend/src/components/admin/ConsignmentManualMatchModal.tsx +++ b/apps/frontend/src/components/admin/ConsignmentManualMatchModal.tsx @@ -7,20 +7,18 @@ import { Empty, Input, Modal, - Radio, Space, Spin, Tag, Typography, message, } from 'antd' -import { SearchOutlined } from '@ant-design/icons' +import { SearchOutlined, TeamOutlined } from '@ant-design/icons' import { assignWorkOrderWithConsignmentWorker, fetchInventoryMatchableOrders, fetchWorkOrderConsignmentMatchOptions, - fulfillWorkOrderWithConsignment, } from '@/services/admin/consignment' import type { ConsignmentMatchOptionView, ConsignmentMatchableOrder } from '@/types/consignment' import { @@ -41,7 +39,7 @@ type Selected = { inventoryLabel: string matchType: ManualMatchType conflicts: string[] - /** 指派打手线要用的供货卖家(现货的供货打手);内部现货履约不需要 */ + /** 供货打手(接单平台指派履约对象) */ workerId?: number | null workerName?: string } @@ -59,9 +57,7 @@ export type ConsignmentManualMatchModalProps = { /** * 「手动指定现货」弹窗:订单侧与库存侧共用一套交互与落地逻辑。 * - * 为什么单独做:系统候选只覆盖"规格完全对得上"的现货,客服经常遇到 - * "订单规格没写全/订单录错/需要高配充低配"的情况,必须能人工指定; - * 同时要把"哪里不一致"讲清楚,并把原因留痕,避免变成无痕操作。 + * 统一走接单平台指派供货打手履约(自动冻结押金、开启交付倒计时与履约监控)。 */ export default function ConsignmentManualMatchModal({ open, @@ -80,9 +76,6 @@ export default function ConsignmentManualMatchModal({ workerName: string } | null>(null) const [selected, setSelected] = useState(null) - const [deliveryKind, setDeliveryKind] = useState<'internal_stock' | 'assign_worker'>( - 'internal_stock', - ) const [confirmedPartial, setConfirmedPartial] = useState(false) const [confirmedMismatch, setConfirmedMismatch] = useState(false) const [matchNote, setMatchNote] = useState('') @@ -93,7 +86,6 @@ export default function ConsignmentManualMatchModal({ const resetState = () => { setKeyword('') setSelected(null) - setDeliveryKind('internal_stock') setConfirmedPartial(false) setConfirmedMismatch(false) setMatchNote('') @@ -167,6 +159,7 @@ export default function ConsignmentManualMatchModal({ const requirement = resolveManualMatchRequirement(selected?.matchType || 'exact') const canSubmit = Boolean(selected) && + Boolean(selected?.workerId) && canSubmitManualMatch({ matchType: selected?.matchType || 'exact', confirmedPartial, @@ -176,36 +169,26 @@ export default function ConsignmentManualMatchModal({ async function handleSubmit() { if (!selected) return - if (deliveryKind === 'assign_worker' && !selected.workerId) { - message.error('该现货未关联供货打手,不能走指派履约;请改用内部现货履约') + if (!selected.workerId) { + message.error('该现货未关联供货打手,无法走平台指派履约;请先关联打手') return } setSubmitting(true) try { - const payload = { + const res = await assignWorkOrderWithConsignmentWorker({ workOrderId: selected.workOrderId, inventoryId: selected.inventoryId, + workerId: selected.workerId, allowPartial: selected.matchType !== 'exact', allowSpecMismatch: selected.matchType === 'conflict', matchNote: matchNote.trim(), - } - const res = - deliveryKind === 'assign_worker' - ? await assignWorkOrderWithConsignmentWorker({ - ...payload, - workerId: selected.workerId ?? undefined, - }) - : await fulfillWorkOrderWithConsignment(payload) + }) // 核销失败时共享工具会弹警告(状态已变更但凭证没消耗),此时不能再弹"成功"掩盖 const voucher = (res.data as any)?.voucherConsume notifyVoucherConsume(voucher) if (!voucher || voucher.ok) { - message.success( - deliveryKind === 'assign_worker' - ? '已手动匹配:指派供货打手履约,押金已冻结' - : '已手动匹配:使用内部现货履约,工单进入待加好友', - ) + message.success('已手动匹配并指派供货打手履约,履约押金已冻结') } onMatched?.() resetState() @@ -232,7 +215,7 @@ export default function ConsignmentManualMatchModal({ void load() return } - message.error(err.message || '手动匹配失败') + message.error(err.message || '手动匹配指派失败') } finally { setSubmitting(false) } @@ -243,59 +226,53 @@ export default function ConsignmentManualMatchModal({ title={mode === 'order' ? `手动指定现货 · ${workOrder?.label || ''}` : '手动匹配订单'} open={open} width={860} + okText="确认指派履约" + okButtonProps={{ + disabled: !canSubmit, + loading: submitting, + }} + onOk={() => void handleSubmit()} onCancel={() => { resetState() onClose() }} afterOpenChange={(visible) => { - if (visible) void load() + if (visible) { + void load() + } else { + resetState() + } }} - footer={ - - - - - } - destroyOnHidden > } placeholder={ mode === 'order' ? '搜索物品名 / 寄卖单号 / 仓库号 / 规格' - : '搜索订单号 / 商品名 / 规格' + : '搜索工单号 / 平台单号 / 标题 / 规格' } + prefix={} value={keyword} onChange={(e) => setKeyword(e.target.value)} + allowClear /> -
+
{mode === 'order' ? ( filteredOptions.length === 0 ? ( - + ) : ( filteredOptions.map((option) => { const meta = describeManualMatchType(option.matchType) @@ -323,11 +300,13 @@ export default function ConsignmentManualMatchModal({ {meta.label} - 寄卖单号 {option.inventory.sourceSubmissionNo || '—'} ·{' '} - {option.inventory.system}/{option.inventory.serverZone}/ - {option.inventory.series} {option.inventory.diamondLevel}钻 · 货源{' '} - {option.inventory.workerName || '—'} · 结算价 ¥ - {(option.inventory.costPriceFen / 100).toFixed(2)} + 寄卖单号 {option.inventory.sourceSubmissionNo || option.inventory.inventoryNo} ·{' '} + {option.inventory.system || '未指定'}/ + {option.inventory.serverZone || '未指定'}/ + {option.inventory.series || '未指定'}{' '} + {option.inventory.diamondLevel ? `${option.inventory.diamondLevel}钻` : ''} · 货源{' '} + {option.inventory.workerName || '未关联打手'} · 结算价 ¥ + {(Number(option.inventory.costPriceFen || 0) / 100).toFixed(2)} {option.conflicts.length > 0 ? ( @@ -335,7 +314,7 @@ export default function ConsignmentManualMatchModal({ ) : ( - {option.reason || '规格相符'} + {option.reason || '规格完全一致'} )} @@ -356,10 +335,6 @@ export default function ConsignmentManualMatchModal({ setConfirmedPartial(false) setConfirmedMismatch(false) setMatchNote('') - // 选了无打手的现货时指派不可用,兜底切回内部履约 - if (!option.inventory.workerId && deliveryKind === 'assign_worker') { - setDeliveryKind('internal_stock') - } }} > {isSelected ? '已选' : '选它'} @@ -425,17 +400,12 @@ export default function ConsignmentManualMatchModal({ inventoryLabel, matchType: order.matchType, conflicts: order.conflicts, - // 指派线要用现货的供货打手:库存侧入口从接口响应的 inventory 里带过来 workerId: inventoryWorker?.workerId ?? null, workerName: inventoryWorker?.workerName || '', }) setConfirmedPartial(false) setConfirmedMismatch(false) setMatchNote('') - // 选了无打手的现货时指派不可用,兜底切回内部履约 - if (!inventoryWorker?.workerId && deliveryKind === 'assign_worker') { - setDeliveryKind('internal_stock') - } }} > {isSelected ? '已选' : '选它'} @@ -453,6 +423,20 @@ export default function ConsignmentManualMatchModal({ {selected.workOrderLabel || '—'} {selected.inventoryLabel || '—'} + + {selected.workerId ? ( + + } color="blue"> + {selected.workerName || `打手 #${selected.workerId}`} + + + (工单将进入该打手“我的接单”并冻结履约押金) + + + ) : ( + 该现货未关联供货打手,无法指派 + )} + {describeManualMatchType(selected.matchType).label} @@ -466,26 +450,14 @@ export default function ConsignmentManualMatchModal({ - setDeliveryKind(e.target.value)} - options={[ - { - value: 'internal_stock', - label: '内部现货履约(客服/内部仓代交付,工单停在待加好友)', - }, - { - value: 'assign_worker', - // 没关联打手的现货走指派必 400 consignment_worker_required,直接禁用并说明,别给死路 - disabled: !selected.workerId, - label: `指派供货打手履约(冻结押金、工单进行中)${ - selected.workerName - ? ` · 打手:${selected.workerName}` - : ' · 该现货未关联打手,不可指派' - }`, - }, - ]} - /> + {!selected.workerId ? ( + + ) : null} {requirement.needsPartialConfirm ? ( -
- 选择履约方式(都会立刻占用该现货) -
{inv.workerId ? ( <> -
- 由该供货打手接单,需冻结履约押金 +
+ 指派供货打手履约 · 自动冻结履约押金
- ) : null} - -
- 不外派打手,直接用库存账号赠送 -
+ ) : ( +
+ 该现货未关联供货打手,无法走平台指派;请先关联打手 +
+ )}
diff --git a/apps/frontend/tests/consignment-manual-match-mount.test.ts b/apps/frontend/tests/consignment-manual-match-mount.test.ts index 25df48cb..52d9f58b 100644 --- a/apps/frontend/tests/consignment-manual-match-mount.test.ts +++ b/apps/frontend/tests/consignment-manual-match-mount.test.ts @@ -87,11 +87,11 @@ test('手动匹配弹窗:数据过期的 409 要清选择并刷新,无打手 assert.ok(source.includes('consignment_inventory_race_condition'), '要处理并发占用错误码') assert.ok(source.includes('consignment_spec_mismatch'), '要处理规格变化错误码') assert.ok(source.includes('setSelected(null)'), '数据过期时要清空已选') - // 没关联打手的现货走指派必 400 consignment_worker_required:选项禁用 + 提交前再拦一次 - assert.ok(source.includes('disabled: !selected.workerId'), '无打手时指派选项必须禁用') + // 统一平台履约:没关联打手的现货禁用提交,提交前严格校验 workerId + assert.ok(source.includes('Boolean(selected?.workerId)'), '无打手时必须禁用提交') assert.ok( - source.includes("deliveryKind === 'assign_worker' && !selected.workerId"), - '提交前要对无打手的指派再拦一道', + source.includes("if (!selected.workerId)"), + '提交前要对无打手的现货严格拦截', ) // 凭证核销反馈与关键字搜索:用共享出口 / 透传后端,不内联复制 assert.ok(source.includes("from '@/utils/voucher-consume'"), '核销反馈必须用共享出口')