feat(consignment): 新增待抢单手枪一键查询匹配看板
- 寄售仓库刷新前新增「一键查询待匹配订单」入口 - 待抢单非拼单工单 × 在库待匹配现货,按系统/区服/系列/钻级/璀璨细粒度评分 - 支持关键字搜索、冲突项开关、刷新与分页 - 只读查询,不自动履约、不改工单
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { ReloadOutlined } from '@ant-design/icons'
|
||||
import { Alert, Button, Checkbox, Empty, Input, Modal, Space, Tag, Typography } from 'antd'
|
||||
import type { TableColumnsType } from 'antd'
|
||||
import { useState } from 'react'
|
||||
|
||||
import PercentTable from '@/components/common/PercentTable'
|
||||
import { fetchPendingPistolMatches } from '@/services/admin/consignment'
|
||||
import type { PendingPistolMatchPair } from '@/types/consignment'
|
||||
import { formatDateTime } from '@/utils/date-time'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
function resolveScoreTag(pair: PendingPistolMatchPair) {
|
||||
if (pair.matchType === 'exact') return { color: 'green', label: '精确' }
|
||||
if (pair.matchType === 'conflict') return { color: 'default', label: '不匹配' }
|
||||
if (pair.score >= 90) return { color: 'blue', label: '非精确' }
|
||||
if (pair.score >= 70) return { color: 'orange', label: '非精确' }
|
||||
return { color: 'gold', label: '需核对' }
|
||||
}
|
||||
|
||||
export default function ConsignmentPendingMatchModal({
|
||||
open,
|
||||
onClose,
|
||||
}: {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
}) {
|
||||
const [keyword, setKeyword] = useState('')
|
||||
const [includeConflicts, setIncludeConflicts] = useState(false)
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ['admin-pending-pistol-matches', keyword, includeConflicts],
|
||||
queryFn: () => fetchPendingPistolMatches({ keyword, includeConflicts, limit: 300 }),
|
||||
enabled: open,
|
||||
refetchOnWindowFocus: false,
|
||||
retry: false,
|
||||
})
|
||||
const data = query.data?.data
|
||||
const pairs = data?.pairs || []
|
||||
|
||||
const columns: TableColumnsType<PendingPistolMatchPair> = [
|
||||
{
|
||||
title: '匹配打分',
|
||||
key: 'score',
|
||||
width: 90,
|
||||
render: (_, record) => {
|
||||
const tag = resolveScoreTag(record)
|
||||
return (
|
||||
<Space direction="vertical" size={3} style={{ width: '100%' }}>
|
||||
<span style={{ fontSize: 18, fontWeight: 700, lineHeight: 1 }}>{record.score}</span>
|
||||
<Tag color={tag.color} style={{ marginInlineEnd: 0 }}>
|
||||
{tag.label}
|
||||
</Tag>
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '订单信息',
|
||||
key: 'order',
|
||||
width: 280,
|
||||
render: (_, record) => (
|
||||
<Space direction="vertical" size={2} style={{ width: '100%' }}>
|
||||
<Space size={4} align="center">
|
||||
<Text type="secondary" style={{ fontSize: 11 }}>
|
||||
工单
|
||||
</Text>
|
||||
{record.workOrderNo ? (
|
||||
<Text
|
||||
copyable={{ text: record.workOrderNo }}
|
||||
style={{ fontSize: 12, fontWeight: 600, whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{record.workOrderNo}
|
||||
</Text>
|
||||
) : (
|
||||
<Text type="secondary">-</Text>
|
||||
)}
|
||||
</Space>
|
||||
<Space size={4} align="center">
|
||||
<Text type="secondary" style={{ fontSize: 11 }}>
|
||||
平台
|
||||
</Text>
|
||||
{record.platformOrderId ? (
|
||||
<Text
|
||||
copyable={{ text: record.platformOrderId }}
|
||||
style={{ fontSize: 11, whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{record.platformOrderId}
|
||||
</Text>
|
||||
) : (
|
||||
<Text type="secondary">-</Text>
|
||||
)}
|
||||
</Space>
|
||||
<Text type="secondary" style={{ fontSize: 11 }}>
|
||||
录单 {record.workOrderCreatedAt ? formatDateTime(record.workOrderCreatedAt) : '-'}
|
||||
</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '商品名',
|
||||
key: 'productName',
|
||||
width: 220,
|
||||
render: (_, record) => (
|
||||
<Text ellipsis={{ tooltip: record.productName }} style={{ maxWidth: '100%' }}>
|
||||
{record.productName || '-'}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '寄售信息',
|
||||
key: 'consignment',
|
||||
width: 280,
|
||||
render: (_, record) => (
|
||||
<Space direction="vertical" size={2} style={{ width: '100%' }}>
|
||||
<Space size={4} align="center">
|
||||
<Text type="secondary" style={{ fontSize: 11 }}>
|
||||
寄售
|
||||
</Text>
|
||||
<Text
|
||||
copyable={{ text: record.consignmentNo || record.inventoryNo }}
|
||||
style={{ fontSize: 12, fontWeight: 600, whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{record.consignmentNo || record.inventoryNo || '-'}
|
||||
</Text>
|
||||
</Space>
|
||||
{record.inventoryNo && record.inventoryNo !== record.consignmentNo ? (
|
||||
<Space size={4} align="center">
|
||||
<Text type="secondary" style={{ fontSize: 11 }}>
|
||||
仓库
|
||||
</Text>
|
||||
<Text
|
||||
copyable={{ text: record.inventoryNo }}
|
||||
style={{ fontSize: 11, whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{record.inventoryNo}
|
||||
</Text>
|
||||
</Space>
|
||||
) : null}
|
||||
<Text type="secondary" style={{ fontSize: 11 }}>
|
||||
入库 {record.inventoryInboundAt ? formatDateTime(record.inventoryInboundAt) : '-'}
|
||||
</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '寄售物品',
|
||||
key: 'itemName',
|
||||
width: 220,
|
||||
render: (_, record) => (
|
||||
<Text ellipsis={{ tooltip: record.itemName }} style={{ maxWidth: '100%' }}>
|
||||
{record.itemName || '-'}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '匹配说明',
|
||||
key: 'reason',
|
||||
width: 300,
|
||||
render: (_, record) => (
|
||||
<Space direction="vertical" size={2}>
|
||||
<Text style={{ fontSize: 12 }}>{record.reason || '-'}</Text>
|
||||
{record.conflicts.length > 0 ? (
|
||||
<Text type="danger" style={{ fontSize: 11 }}>
|
||||
{record.conflicts.join(';')}
|
||||
</Text>
|
||||
) : null}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="匹配 · 待抢单手枪 × 在库现货"
|
||||
open={open}
|
||||
width="min(1320px, 94vw)"
|
||||
footer={null}
|
||||
destroyOnHidden
|
||||
onCancel={onClose}
|
||||
>
|
||||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="只查询非拼单的待抢单手枪单 × 在库待匹配现货,不直接改单。"
|
||||
description="分数 0-100:系统 20 / 区服 20 / 系列 20 / 钻级 25 / 璀璨 15。非精确项仍需按现有规则确认;不按录单、入库时间先后过滤。"
|
||||
/>
|
||||
|
||||
<Space wrap size={12}>
|
||||
<Tag>待抢单 {data?.pendingOrderCount ?? 0} 单</Tag>
|
||||
<Tag>在库待匹配 {data?.inStockInventoryCount ?? 0} 件</Tag>
|
||||
<Tag color="blue">匹配组合 {data?.pairCount ?? 0} 条</Tag>
|
||||
<Tag color="green">精确 {data?.exactCount ?? 0}</Tag>
|
||||
<Tag color="orange">非精确 {data?.partialCount ?? 0}</Tag>
|
||||
{includeConflicts ? <Tag>不匹配 {data?.conflictCount ?? 0}</Tag> : null}
|
||||
</Space>
|
||||
|
||||
<Space wrap size={12}>
|
||||
<Input.Search
|
||||
allowClear
|
||||
placeholder="搜索平台号 / 工单号 / 商品名 / 寄售单号 / 寄售物品"
|
||||
style={{ width: 420 }}
|
||||
onSearch={(value) => setKeyword(value.trim())}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={includeConflicts}
|
||||
onChange={(event) => setIncludeConflicts(event.target.checked)}
|
||||
>
|
||||
显示 0 分不匹配项
|
||||
</Checkbox>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={query.isFetching}
|
||||
onClick={() => void query.refetch()}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
{query.isError ? (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
message="读取匹配结果失败"
|
||||
description={query.error instanceof Error ? query.error.message : '请稍后重试'}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{data?.truncated ? (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message={`结果较多,仅展示前 ${pairs.length} 条,请用搜索缩小范围`}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<PercentTable<PendingPistolMatchPair>
|
||||
rowKey={(record) => `${record.workOrderId}-${record.inventoryId}`}
|
||||
size="small"
|
||||
loading={query.isFetching}
|
||||
dataSource={pairs}
|
||||
columns={columns}
|
||||
scroll={{ y: 460 }}
|
||||
pagination={{
|
||||
pageSize: 20,
|
||||
showSizeChanger: false,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
}}
|
||||
locale={{
|
||||
emptyText: (
|
||||
<Empty
|
||||
description={
|
||||
includeConflicts
|
||||
? '没有可展示的配对'
|
||||
: '没有有分匹配;可勾选「显示 0 分不匹配项」查看冲突'
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,10 @@
|
||||
import { EyeOutlined, ReloadOutlined, RollbackOutlined, StopOutlined } from '@ant-design/icons'
|
||||
import {
|
||||
EyeOutlined,
|
||||
ReloadOutlined,
|
||||
RollbackOutlined,
|
||||
StopOutlined,
|
||||
ThunderboltOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import {
|
||||
Alert,
|
||||
@@ -27,6 +33,7 @@ import type { TableColumnsType } from 'antd'
|
||||
import { useState } from 'react'
|
||||
|
||||
import ConsignmentEvidenceGallery from '@/components/admin/ConsignmentEvidenceGallery'
|
||||
import ConsignmentPendingMatchModal from '@/components/admin/ConsignmentPendingMatchModal'
|
||||
import {
|
||||
archiveAdminConsignmentInventory,
|
||||
fetchAdminConsignmentContacts,
|
||||
@@ -149,6 +156,7 @@ export default function ConsignmentInventoryPanel() {
|
||||
const [viewingItem, setViewingItem] = useState<ConsignmentInventory | null>(null)
|
||||
const [returnItem, setReturnItem] = useState<ConsignmentInventory | null>(null)
|
||||
const [manualMatchItem, setManualMatchItem] = useState<ConsignmentInventory | null>(null)
|
||||
const [pendingMatchOpen, setPendingMatchOpen] = useState(false)
|
||||
const [returnReason, setReturnReason] = useState<string>('')
|
||||
|
||||
const [editForm] = Form.useForm()
|
||||
@@ -836,6 +844,10 @@ export default function ConsignmentInventoryPanel() {
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button icon={<ThunderboltOutlined />} onClick={() => setPendingMatchOpen(true)}>
|
||||
一键查询待匹配订单
|
||||
</Button>
|
||||
|
||||
<Button icon={<ReloadOutlined />} onClick={refresh}>
|
||||
刷新
|
||||
</Button>
|
||||
@@ -1305,6 +1317,12 @@ export default function ConsignmentInventoryPanel() {
|
||||
}
|
||||
onMatched={() => void inventoriesQuery.refetch()}
|
||||
/>
|
||||
|
||||
{/* 全局匹配看板:非拼单待抢单手枪 × 在库待匹配现货 */}
|
||||
<ConsignmentPendingMatchModal
|
||||
open={pendingMatchOpen}
|
||||
onClose={() => setPendingMatchOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
ConsignmentStatsSummary,
|
||||
ConsignmentSubmission,
|
||||
InventoryMatchableOrders,
|
||||
PendingPistolMatchResult,
|
||||
WorkOrderInventoryMatchCheck,
|
||||
WorkOrderInventoryMatchOptions,
|
||||
} from '@/types/consignment'
|
||||
@@ -158,6 +159,22 @@ export function fetchInventoryMatchableOrders(
|
||||
)
|
||||
}
|
||||
|
||||
/** 全局匹配看板:非拼单待抢单手枪 × 在库待匹配现货,按细粒度评分排序。 */
|
||||
export function fetchPendingPistolMatches(params?: {
|
||||
keyword?: string
|
||||
includeConflicts?: boolean
|
||||
limit?: number
|
||||
}) {
|
||||
return apiGet<PendingPistolMatchResult>(
|
||||
'/api/v1/admin/worker-platform/consignment/pending-pistol-matches',
|
||||
{
|
||||
keyword: params?.keyword?.trim() || undefined,
|
||||
includeConflicts: params?.includeConflicts ? 'true' : undefined,
|
||||
limit: params?.limit,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/** 履约/指派返回的电子凭证核销结果:ok=false 时必须提示运营人工核销,不能报成功 */
|
||||
export type WorkOrderVoucherConsumeResult = {
|
||||
ok: boolean
|
||||
|
||||
@@ -240,3 +240,32 @@ export type WorkOrderInventoryMatchCheck = {
|
||||
partialCount: number
|
||||
matches: ConsignmentMatchResult[]
|
||||
}
|
||||
|
||||
/** 后台「寄售仓库 → 匹配」:待抢单手枪 × 在库待匹配现货的配对明细 */
|
||||
export type PendingPistolMatchPair = {
|
||||
workOrderId: number
|
||||
workOrderNo: string
|
||||
platformOrderId: string
|
||||
productName: string
|
||||
workOrderCreatedAt: string | null
|
||||
inventoryId: number
|
||||
consignmentNo: string
|
||||
inventoryNo: string
|
||||
itemName: string
|
||||
inventoryInboundAt: string | null
|
||||
matchType: 'exact' | 'partial' | 'conflict'
|
||||
score: number
|
||||
reason: string
|
||||
conflicts: string[]
|
||||
}
|
||||
|
||||
export type PendingPistolMatchResult = {
|
||||
pendingOrderCount: number
|
||||
inStockInventoryCount: number
|
||||
pairCount: number
|
||||
exactCount: number
|
||||
partialCount: number
|
||||
conflictCount: number
|
||||
truncated: boolean
|
||||
pairs: PendingPistolMatchPair[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user