Files
order_site/apps/frontend/src/components/admin/ConsignmentWorkOrderMatchModal.tsx
T

162 lines
5.5 KiB
TypeScript

import { App, Alert, Button, Card, Empty, Modal, Space, Tag, Typography } from 'antd'
import { useQuery } from '@tanstack/react-query'
import { useEffect, useMemo, useState } from 'react'
import PercentTable from '@/components/common/PercentTable'
import {
fetchAdminConsignmentMatchCandidates,
matchAdminConsignmentWorkOrder,
} from '@/services/admin/consignment'
import type { ConsignmentMatchCandidate } from '@/types/consignment'
function money(fen: number) {
return `¥${((Number(fen || 0) || 0) / 100).toFixed(2)}`
}
export default function ConsignmentWorkOrderMatchModal({
open,
workOrderId,
onClose,
onMatched,
}: {
open: boolean
workOrderId: number | null
onClose: () => void
onMatched?: () => void
}) {
const { message } = App.useApp()
const [selectedIds, setSelectedIds] = useState<number[]>([])
const [submitting, setSubmitting] = useState(false)
const candidatesQuery = useQuery({
queryKey: ['admin-consignment-match-candidates', workOrderId],
queryFn: () => fetchAdminConsignmentMatchCandidates(Number(workOrderId)),
enabled: open && Boolean(workOrderId),
})
const data = candidatesQuery.data?.data
const options = data?.options || []
const needed = Math.max(1, Number(data?.totalQuantity || 1) - Number(data?.activeQuantity || 0))
useEffect(() => {
if (!open) {
setSelectedIds([])
return
}
if (options.length === 0) return
// 默认按缺口选中价格最低的几条,客服可自行调整。
setSelectedIds(options.slice(0, needed).map((item) => item.inventoryId))
}, [open, options, needed])
const totalPrice = useMemo(
() =>
selectedIds.reduce((sum, id) => {
const item = options.find((option) => option.inventoryId === id)
return sum + Number(item?.salePriceFen || 0)
}, 0),
[options, selectedIds],
)
return (
<Modal
title="寄卖库存匹配"
open={open}
onCancel={onClose}
width={860}
destroyOnHidden
footer={
<Space>
<Button onClick={onClose}>取消</Button>
<Button
type="primary"
loading={submitting}
disabled={selectedIds.length === 0}
onClick={async () => {
if (!workOrderId) return
setSubmitting(true)
try {
const result = await matchAdminConsignmentWorkOrder({
workOrderId,
inventoryIds: selectedIds,
})
message.success(
result.data?.matched
? `已匹配 ${result.data.matchedCount || selectedIds.length} 条寄卖库存`
: '未匹配到可用库存',
)
onMatched?.()
onClose()
} catch (error) {
message.error(error instanceof Error ? error.message : '匹配失败')
} finally {
setSubmitting(false)
}
}}
>
确认匹配
</Button>
</Space>
}
>
{candidatesQuery.isLoading ? <Card loading /> : null}
{!candidatesQuery.isLoading && options.length === 0 ? (
<Empty description="没有精确匹配的在库寄卖库存" />
) : null}
{options.length > 0 ? (
<Space direction="vertical" size={12} style={{ width: '100%' }}>
<Alert
type="info"
showIcon
message={`该工单${data?.sharingEnabled ? `拼单 ${data.totalQuantity} 份,已匹配 ${data.activeQuantity} 份` : '为整单'},本次还需 ${needed} 条库存。`}
description="自动匹配只做精确规格;手动匹配可选多条,拼单工单每选一条生成一个份额。"
/>
<PercentTable<ConsignmentMatchCandidate>
rowKey="inventoryId"
size="small"
pagination={false}
dataSource={options}
rowSelection={{
selectedRowKeys: selectedIds,
onChange: (keys) => setSelectedIds(keys.map((key) => Number(key))),
}}
columns={[
{
title: '寄卖库存',
render: (_, item) => (
<Space direction="vertical" size={0}>
<Typography.Text strong>{item.itemName}</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{item.inventoryNo} · {item.workerName || `打手#${item.workerId || '-'}`}
</Typography.Text>
</Space>
),
},
{
title: '规格',
render: (_, item) =>
`${item.system}/${item.serverZone}/${item.series}/${item.diamondLevel}钻/${item.brillianceCount}璀`,
},
{
title: '库存',
width: 90,
render: (_, item) => (
<Tag color={item.remainingQuantity > 1 ? 'blue' : 'default'}>
剩 {item.remainingQuantity}/{item.quantity}
</Tag>
),
},
{
title: '售价',
width: 110,
render: (_, item) => money(item.salePriceFen),
},
]}
/>
<Typography.Text type="secondary">
已选 {selectedIds.length} 条,合计成本 {money(totalPrice)}
</Typography.Text>
</Space>
) : null}
</Modal>
)
}