import { useEffect, useMemo, useState } from 'react' import { Alert, Button, Checkbox, Descriptions, Empty, Input, Modal, Space, Spin, Tag, Typography, message, } from 'antd' import { SearchOutlined, TeamOutlined } from '@ant-design/icons' import { fetchInventoryMatchableOrders, fetchWorkOrderConsignmentMatchOptions, fulfillWorkOrderWithConsignment, } from '@/services/admin/consignment' import type { ConsignmentMatchOptionView, ConsignmentMatchableOrder } from '@/types/consignment' import { canSubmitManualMatch, describeManualMatchType, filterManualMatchOptions, resolveManualMatchRequirement, type ManualMatchType, } from '@/utils/consignment-manual-match' import { notifyVoucherConsume } from '@/utils/voucher-consume' const { Text, Paragraph } = Typography type Selected = { workOrderId: number | string workOrderLabel: string inventoryId: number | string inventoryLabel: string matchType: ManualMatchType conflicts: string[] /** 供货打手(用于回显供货来源) */ workerId?: number | null workerName?: string } export type ConsignmentManualMatchModalProps = { open: boolean onClose: () => void /** 订单侧入口:从订单出发选现货 */ workOrder?: { workOrderId: number | string; label: string } | null /** 库存侧入口:从现货出发选订单 */ inventory?: { inventoryId: number | string; label: string } | null onMatched?: () => void } /** * 「手动指定现货」弹窗:订单侧与库存侧共用一套交互与落地逻辑。 * * 统一走内部现货直接履约(原子占库、免外派佣金、不冻结重复押金、跳过打手资料直接进入待加好友)。 */ export default function ConsignmentManualMatchModal({ open, onClose, workOrder, inventory, onMatched, }: ConsignmentManualMatchModalProps) { const [keyword, setKeyword] = useState('') const [loading, setLoading] = useState(false) const [options, setOptions] = useState([]) const [orders, setOrders] = useState([]) const [inventoryLabel, setInventoryLabel] = useState('') const [inventoryWorker, setInventoryWorker] = useState<{ workerId: number | null workerName: string } | null>(null) const [selected, setSelected] = useState(null) const [confirmedPartial, setConfirmedPartial] = useState(false) const [confirmedMismatch, setConfirmedMismatch] = useState(false) const [matchNote, setMatchNote] = useState('') const [submitting, setSubmitting] = useState(false) const mode: 'order' | 'inventory' = inventory ? 'inventory' : 'order' const resetState = () => { setKeyword('') setSelected(null) setConfirmedPartial(false) setConfirmedMismatch(false) setMatchNote('') } const load = async () => { setLoading(true) try { if (inventory) { // 关键字与分页透传后端:库存侧默认只拉 50 条时,第 51 单以后永远搜不到 const res = await fetchInventoryMatchableOrders(inventory.inventoryId, { keyword: keyword.trim() || undefined, pageSize: 100, }) setOrders(res.data?.orders || []) setInventoryLabel( res.data?.inventory ? `${res.data.inventory.itemName}(${res.data.inventory.sourceSubmissionNo || res.data.inventory.inventoryNo})` : inventory.label, ) setInventoryWorker({ workerId: res.data?.inventory?.workerId ?? null, workerName: res.data?.inventory?.workerName || '', }) } else if (workOrder) { const res = await fetchWorkOrderConsignmentMatchOptions(workOrder.workOrderId) setOptions(res.data?.options || []) } } catch (err: any) { message.error(err.message || '读取可匹配数据失败') } finally { setLoading(false) } } // 库存侧:关键字防抖透传后端搜索(本地过滤只作用在已拉取的 100 条内) useEffect(() => { if (!open || !inventory) return const timer = setTimeout(() => { void load() }, 300) return () => clearTimeout(timer) // eslint-disable-next-line react-hooks/exhaustive-deps }, [keyword]) const filteredOptions = useMemo( () => filterManualMatchOptions(options, keyword, (item) => [ item.inventory.itemName, item.inventory.sourceSubmissionNo, item.inventory.inventoryNo, item.inventory.system, item.inventory.serverZone, item.inventory.series, item.inventory.workerName, item.inventory.buyerName, item.inventory.sellerGameId, item.inventory.id, ]), [options, keyword], ) const filteredOrders = useMemo( () => filterManualMatchOptions(orders, keyword, (item) => [ item.workOrderNo, item.workOrderId, item.productName, item.requiredSpec.system, item.requiredSpec.serverZone, item.requiredSpec.series, item.status, ]), [orders, keyword], ) const requirement = resolveManualMatchRequirement(selected?.matchType || 'exact') const canSubmit = Boolean(selected) && canSubmitManualMatch({ matchType: selected?.matchType || 'exact', confirmedPartial, confirmedMismatch, matchNote, }) async function handleSubmit() { if (!selected) return setSubmitting(true) try { const res = await fulfillWorkOrderWithConsignment({ workOrderId: selected.workOrderId, inventoryId: selected.inventoryId, allowPartial: selected.matchType !== 'exact' || confirmedPartial, allowSpecMismatch: selected.matchType === 'conflict' || confirmedMismatch, matchNote: matchNote.trim(), }) // 核销失败时共享工具会弹警告(状态已变更但凭证没消耗),此时不能再弹"成功"掩盖 const voucher = (res.data as any)?.voucherConsume notifyVoucherConsume(voucher) if (!voucher || voucher.ok) { message.success('已使用内部寄售现货直接履约,已进入待加好友阶段') } onMatched?.() resetState() onClose() } catch (err: any) { // 列表数据过期(并发占用 / 打开弹窗后订单规格被改):刷新列表按最新判定重来, // 否则运营会对着已失效的候选反复重试、循环撞同一个错 const code = String(err?.errorCode || '') if ( code === 'consignment_inventory_race_condition' || code === 'consignment_inventory_unavailable' ) { message.error('该现货刚被其他操作占用或已下架,已刷新列表,请重新选择') setSelected(null) void load() return } if ( code === 'consignment_spec_mismatch' || code === 'consignment_spec_partial_requires_confirm' ) { message.error('订单或现货的规格已变化(列表数据过期),已刷新,请按最新判定重新确认') setSelected(null) void load() return } message.error(err.message || '内部现货履约失败') } finally { setSubmitting(false) } } return ( void handleSubmit()} onCancel={() => { resetState() onClose() }} afterOpenChange={(visible) => { if (visible) { void load() } else { resetState() } }} > } value={keyword} onChange={(e) => setKeyword(e.target.value)} allowClear />
{mode === 'order' ? ( filteredOptions.length === 0 ? ( ) : ( filteredOptions.map((option) => { const meta = describeManualMatchType(option.matchType) const isSelected = selected?.inventoryId != null && String(selected.inventoryId) === String(option.inventory.id) return (
{option.inventory.sourceSubmissionNo || option.inventory.inventoryNo} {option.inventory.itemName} {meta.label} 寄卖单号{' '} {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 ? ( 差异:{option.conflicts.join(';')} ) : ( {option.reason || '规格完全一致'} )}
) }) ) ) : filteredOrders.length === 0 ? ( ) : ( filteredOrders.map((order) => { const meta = describeManualMatchType(order.matchType) const isSelected = selected?.workOrderId != null && String(selected.workOrderId) === String(order.workOrderId) return (
{order.workOrderNo || `#${order.workOrderId}`} {order.productName} {meta.label} {order.status === 'open' ? ( 大厅待抢 ) : order.status === 'unassigned' ? ( 待指派 ) : order.status === 'pending_material' ? ( 待补资料 ) : null} 要求:{order.requiredSpec.system || '未指定'}/ {order.requiredSpec.serverZone || '未指定'}/ {order.requiredSpec.series || '未指定'}{' '} {order.requiredSpec.diamondLevel ? `${order.requiredSpec.diamondLevel}钻` : ''}{' '} · 报酬 ¥{(order.rewardAmountFen / 100).toFixed(2)} {order.conflicts.length > 0 ? ( 差异:{order.conflicts.join(';')} ) : ( {order.reason || '规格相符'} )}
) }) )}
{selected ? (
{selected.workOrderLabel || '—'} {selected.inventoryLabel || '—'} {selected.workerId ? ( } color="blue"> {selected.workerName || `供货打手 #${selected.workerId}`} (验收后将自动为该打手发放货款) ) : ( 内部库存现货(无打手关联) )} {describeManualMatchType(selected.matchType).label} {selected.conflicts.length > 0 ? ( {selected.conflicts.join(';')} ) : null} {requirement.needsPartialConfirm ? ( setConfirmedPartial(e.target.checked)} > 允许非精确匹配(订单规格未写全或高配充低配) ) : null} {requirement.needsMismatchConfirm ? ( <> setConfirmedMismatch(e.target.checked)} > 确认规格不一致,仍要手动匹配 ) : null} {requirement.needsNote ? ( setMatchNote(e.target.value)} /> ) : null} 确认后现货立即被占用并进入「待加好友」,双方加好买家好友后点确认加好友进入赠送冷却期。 如需退回请使用「撤销寄售履约」。
) : null}
) }