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
@@ -36,6 +36,7 @@ import {
updateAdminConsignmentInventory,
} from '@/services/admin/consignment'
import { fetchAdminWorkerUsers } from '@/services/admin/worker-platform'
import ConsignmentManualMatchModal from '@/components/admin/ConsignmentManualMatchModal'
import type { ConsignmentInventory, GiftEvidenceReviewStatus } from '@/types/consignment'
import { fetchAdminSession } from '@/services/admin/auth'
import { resolveAdminPermission } from '@/utils/admin-auth'
@@ -143,6 +144,7 @@ export default function ConsignmentInventoryPanel() {
const [editingItem, setEditingItem] = useState<ConsignmentInventory | null>(null)
const [viewingItem, setViewingItem] = useState<ConsignmentInventory | null>(null)
const [returnItem, setReturnItem] = useState<ConsignmentInventory | null>(null)
const [manualMatchItem, setManualMatchItem] = useState<ConsignmentInventory | null>(null)
const [returnReason, setReturnReason] = useState<string>('')
const [editForm] = Form.useForm()
@@ -552,6 +554,9 @@ export default function ConsignmentInventoryPanel() {
</Button>
{record.status === 'in_stock' && canManageConsignment ? (
<>
<Button size="small" type="link" onClick={() => setManualMatchItem(record)}>
手动匹配订单
</Button>
<Button
size="small"
type="link"
@@ -939,6 +944,20 @@ export default function ConsignmentInventoryPanel() {
</Form.Item>
</Card>
<ConsignmentManualMatchModal
open={Boolean(manualMatchItem)}
onClose={() => setManualMatchItem(null)}
inventory={
manualMatchItem
? {
inventoryId: manualMatchItem.id,
label: manualMatchItem.itemName,
}
: null
}
onMatched={() => void inventoriesQuery.refetch()}
/>
<Form.Item name="remark" label="备注">
<Input.TextArea rows={2} />
</Form.Item>
@@ -82,6 +82,7 @@ import {
} from '@/services/admin/consignment'
import type { ConsignmentFulfillmentInfo, WorkOrderInventoryMatchCheck } from '@/types/consignment'
import type { UploadedFile, WorkOrder, WorkOrderShare } from '@/types/worker-platform'
import ConsignmentManualMatchModal from '@/components/admin/ConsignmentManualMatchModal'
import { AdminRangePicker } from '@/components/admin/AdminDatePicker'
import dayjs, { ADMIN_DATE_FORMAT } from '@/lib/dayjs'
import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination'
@@ -313,6 +314,7 @@ export default function WorkOrdersPanel() {
}>()
// 寄售库存匹配拦截弹窗状态
const [manualMatchOrder, setManualMatchOrder] = useState<WorkOrder | null>(null)
const [consignmentInterceptModal, setConsignmentInterceptModal] = useState<{
open: boolean
order: WorkOrder | null
@@ -2970,6 +2972,23 @@ export default function WorkOrdersPanel() {
})
}}
footer={[
<Button
key="manual-match"
type="primary"
onClick={() => {
const currentOrder = consignmentInterceptModal.order
if (!currentOrder) return
setConsignmentInterceptModal({
open: false,
order: null,
matchData: null,
submitting: false,
})
setManualMatchOrder(currentOrder)
}}
>
手动指定现货匹配
</Button>,
<Button
key="ignore-and-publish"
onClick={() => {
@@ -3189,6 +3208,22 @@ export default function WorkOrdersPanel() {
</div>
</Modal>
<ConsignmentManualMatchModal
open={Boolean(manualMatchOrder)}
onClose={() => setManualMatchOrder(null)}
workOrder={
manualMatchOrder
? {
workOrderId: manualMatchOrder.workOrderId,
label: `#${manualMatchOrder.workOrderId} ${manualMatchOrder.productName || ''}`,
}
: null
}
onMatched={() => {
void ordersQuery.refetch()
}}
/>
{/* 撤单 / 撤销寄售履约弹窗 */}
<Modal
title={`撤单 / 撤销寄售履约 · ${getDisplayOrderNo(revokeConsignmentModal.order)}`}