feat(consignment): 寄卖配置、内部匹配与打手端升级
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user