feat(consignment): 寄卖配置、内部匹配与打手端升级
This commit is contained in:
@@ -0,0 +1,568 @@
|
||||
import { ReloadOutlined, SaveOutlined, SendOutlined, StopOutlined } from '@ant-design/icons'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
App,
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
Empty,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import {
|
||||
archiveAdminConsignmentProfile,
|
||||
fetchAdminConsignmentProfiles,
|
||||
publishAdminConsignmentProfile,
|
||||
saveAdminConsignmentProfile,
|
||||
} from '@/services/admin/consignment'
|
||||
import type { ConsignmentProfile } from '@/types/consignment'
|
||||
|
||||
const { Text, Title } = Typography
|
||||
|
||||
type ConfigFormValues = {
|
||||
name: string
|
||||
title: string
|
||||
description: string
|
||||
tabTitle: string
|
||||
fields: {
|
||||
system: boolean
|
||||
serverZone: boolean
|
||||
series: boolean
|
||||
diamondLevel: boolean
|
||||
brillianceCount: boolean
|
||||
quantity: boolean
|
||||
}
|
||||
pricing: { useTemplatePrice: boolean; allowWorkerPrice: boolean; minPriceYuan: number }
|
||||
deposit: {
|
||||
enabled: boolean
|
||||
mode: 'fixed' | 'per_quantity'
|
||||
fixedAmountYuan: number
|
||||
unitAmountYuan: number
|
||||
}
|
||||
serviceFee: { enabled: boolean; mode: 'percent' | 'fixed'; percent: number; amountYuan: number }
|
||||
inventoryLimit: { enabled: boolean; maxActiveQuantity: number }
|
||||
matching: {
|
||||
mode: 'manual' | 'auto'
|
||||
autoMatchEnabled: boolean
|
||||
exactOnly: boolean
|
||||
preferLowReward: boolean
|
||||
}
|
||||
internalMatch: { delayMinutes: number; fallbackToHall: boolean }
|
||||
timeout: { minutes: number; policy: 'reopen' | 'cancel_release' | 'cancel_deduct' }
|
||||
priceDrop: { enabled: boolean; hours: number; mode: 'percent' | 'fixed'; value: number }
|
||||
}
|
||||
|
||||
function yuan(fen: number | undefined): number {
|
||||
return Number(((Number(fen || 0) || 0) / 100).toFixed(2))
|
||||
}
|
||||
|
||||
function fen(value: number | undefined): number {
|
||||
return Math.max(0, Math.round(Number(value || 0) * 100))
|
||||
}
|
||||
|
||||
function toFormValues(profile: ConsignmentProfile): ConfigFormValues {
|
||||
const config = profile.config
|
||||
return {
|
||||
name: profile.name,
|
||||
title: profile.title,
|
||||
description: profile.description,
|
||||
tabTitle: profile.tabTitle,
|
||||
fields: { ...config.fields },
|
||||
pricing: {
|
||||
useTemplatePrice: config.pricing.useTemplatePrice,
|
||||
allowWorkerPrice: config.pricing.allowWorkerPrice,
|
||||
minPriceYuan: yuan(config.pricing.minPriceFen),
|
||||
},
|
||||
deposit: {
|
||||
enabled: config.deposit.enabled,
|
||||
mode: config.deposit.mode,
|
||||
fixedAmountYuan: yuan(config.deposit.fixedAmountFen),
|
||||
unitAmountYuan: yuan(config.deposit.unitAmountFen),
|
||||
},
|
||||
serviceFee: {
|
||||
enabled: config.serviceFee.enabled,
|
||||
mode: config.serviceFee.mode,
|
||||
percent: config.serviceFee.percent,
|
||||
amountYuan: yuan(config.serviceFee.amountFen),
|
||||
},
|
||||
inventoryLimit: { ...config.inventoryLimit },
|
||||
matching: { ...config.matching },
|
||||
internalMatch: { ...config.internalMatch },
|
||||
timeout: { ...config.timeout },
|
||||
priceDrop: { ...config.priceDrop },
|
||||
}
|
||||
}
|
||||
|
||||
function emptyFormValues(): ConfigFormValues {
|
||||
return {
|
||||
name: '寄卖配置',
|
||||
title: '',
|
||||
description: '',
|
||||
tabTitle: '',
|
||||
fields: {
|
||||
system: true,
|
||||
serverZone: true,
|
||||
series: true,
|
||||
diamondLevel: true,
|
||||
brillianceCount: true,
|
||||
quantity: true,
|
||||
},
|
||||
pricing: { useTemplatePrice: true, allowWorkerPrice: false, minPriceYuan: 0 },
|
||||
deposit: { enabled: false, mode: 'fixed', fixedAmountYuan: 0, unitAmountYuan: 0 },
|
||||
serviceFee: { enabled: false, mode: 'percent', percent: 0, amountYuan: 0 },
|
||||
inventoryLimit: { enabled: false, maxActiveQuantity: 0 },
|
||||
matching: {
|
||||
mode: 'manual',
|
||||
autoMatchEnabled: false,
|
||||
exactOnly: true,
|
||||
preferLowReward: true,
|
||||
},
|
||||
internalMatch: { delayMinutes: 30, fallbackToHall: true },
|
||||
timeout: { minutes: 30, policy: 'reopen' },
|
||||
priceDrop: { enabled: false, hours: 72, mode: 'percent', value: 10 },
|
||||
}
|
||||
}
|
||||
|
||||
function buildPayload(categoryId: number, values: ConfigFormValues) {
|
||||
return {
|
||||
categoryId,
|
||||
name: values.name,
|
||||
title: values.title,
|
||||
description: values.description,
|
||||
tabTitle: values.tabTitle,
|
||||
config: {
|
||||
version: 1,
|
||||
tabTitle: values.tabTitle,
|
||||
tabDescription: values.description,
|
||||
fields: values.fields,
|
||||
pricing: {
|
||||
useTemplatePrice: values.pricing.useTemplatePrice,
|
||||
allowWorkerPrice: values.pricing.allowWorkerPrice,
|
||||
minPriceFen: fen(values.pricing.minPriceYuan),
|
||||
},
|
||||
deposit: {
|
||||
enabled: values.deposit.enabled,
|
||||
mode: values.deposit.mode,
|
||||
fixedAmountFen: fen(values.deposit.fixedAmountYuan),
|
||||
unitAmountFen: fen(values.deposit.unitAmountYuan),
|
||||
},
|
||||
serviceFee: {
|
||||
enabled: values.serviceFee.enabled,
|
||||
mode: values.serviceFee.mode,
|
||||
percent: values.serviceFee.percent,
|
||||
amountFen: fen(values.serviceFee.amountYuan),
|
||||
},
|
||||
inventoryLimit: values.inventoryLimit,
|
||||
matching: values.matching,
|
||||
internalMatch: values.internalMatch,
|
||||
timeout: values.timeout,
|
||||
priceDrop: values.priceDrop,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default function ConsignmentConfigPanel() {
|
||||
const { message } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
const [selectedCategoryId, setSelectedCategoryId] = useState<number | null>(null)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [form] = Form.useForm<ConfigFormValues>()
|
||||
|
||||
const profilesQuery = useQuery({
|
||||
queryKey: ['admin-consignment-profiles'],
|
||||
queryFn: () => fetchAdminConsignmentProfiles(),
|
||||
})
|
||||
const data = profilesQuery.data?.data
|
||||
const categories = useMemo(
|
||||
() => (data?.categories || []).filter((category) => category.status === 'active'),
|
||||
[data?.categories],
|
||||
)
|
||||
const profiles = data?.profiles || []
|
||||
const profileByCategoryId = useMemo(
|
||||
() => new Map(profiles.map((profile) => [profile.categoryId, profile])),
|
||||
[profiles],
|
||||
)
|
||||
const selectedCategory = categories.find((category) => category.categoryId === selectedCategoryId)
|
||||
const selectedProfile = selectedCategoryId
|
||||
? profileByCategoryId.get(selectedCategoryId)
|
||||
: undefined
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedCategoryId === null && categories.length > 0) {
|
||||
setSelectedCategoryId(categories[0]!.categoryId)
|
||||
}
|
||||
}, [categories, selectedCategoryId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedCategoryId) return
|
||||
if (selectedProfile) {
|
||||
form.setFieldsValue(toFormValues(selectedProfile))
|
||||
} else {
|
||||
form.setFieldsValue({
|
||||
...emptyFormValues(),
|
||||
name: selectedCategory?.name ? `${selectedCategory.name}寄卖` : '寄卖配置',
|
||||
tabTitle: selectedCategory?.name || '',
|
||||
})
|
||||
}
|
||||
}, [selectedCategoryId, selectedProfile, selectedCategory?.name, form])
|
||||
|
||||
async function refresh() {
|
||||
await queryClient.invalidateQueries({ queryKey: ['admin-consignment-profiles'] })
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!selectedCategoryId) return
|
||||
const values = await form.validateFields()
|
||||
setSaving(true)
|
||||
try {
|
||||
await saveAdminConsignmentProfile(buildPayload(selectedCategoryId, values))
|
||||
message.success('寄卖配置已保存')
|
||||
await refresh()
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '保存失败')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const publishMutation = useMutation({
|
||||
mutationFn: (profileId: number) => publishAdminConsignmentProfile(profileId),
|
||||
onSuccess: async () => {
|
||||
message.success('已发布新版本,打手端将按新配置展示')
|
||||
await refresh()
|
||||
},
|
||||
onError: (error) => message.error(error instanceof Error ? error.message : '发布失败'),
|
||||
})
|
||||
const archiveMutation = useMutation({
|
||||
mutationFn: (profileId: number) => archiveAdminConsignmentProfile(profileId),
|
||||
onSuccess: async () => {
|
||||
message.success('寄卖配置已归档')
|
||||
await refresh()
|
||||
},
|
||||
onError: (error) => message.error(error instanceof Error ? error.message : '归档失败'),
|
||||
})
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={
|
||||
<Space size={8}>
|
||||
<span>寄卖配置</span>
|
||||
<Tag color="blue">{profiles.filter((p) => p.status === 'published').length} 个已发布</Tag>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={profilesQuery.isFetching}
|
||||
onClick={() => void refresh()}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className="order-service-layout">
|
||||
<aside className="order-service-sidebar">
|
||||
<div className="order-service-sidebar-head">
|
||||
<Text strong>寄卖分类</Text>
|
||||
</div>
|
||||
<div className="order-service-category-list">
|
||||
{categories.map((category) => {
|
||||
const rowProfile = profileByCategoryId.get(category.categoryId)
|
||||
return (
|
||||
<button
|
||||
key={category.categoryId}
|
||||
type="button"
|
||||
className={`order-service-category-item${
|
||||
category.categoryId === selectedCategoryId ? ' is-active' : ''
|
||||
}`}
|
||||
onClick={() => setSelectedCategoryId(category.categoryId)}
|
||||
>
|
||||
<span className="order-service-category-main">
|
||||
<span className="order-service-category-name">
|
||||
{category.parentId ? ' └ ' : ''}
|
||||
{category.name}
|
||||
</span>
|
||||
</span>
|
||||
<span className="order-service-category-meta">
|
||||
<span
|
||||
className={`order-service-status-pill is-${rowProfile?.status || 'draft'}`}
|
||||
>
|
||||
{rowProfile?.status === 'published'
|
||||
? `已发布 v${rowProfile.currentVersionNo || 1}`
|
||||
: rowProfile?.status === 'archived'
|
||||
? '已归档'
|
||||
: '未配置'}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
{categories.length === 0 ? (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无启用分类" />
|
||||
) : null}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section className="order-service-editor">
|
||||
{selectedCategory ? (
|
||||
<>
|
||||
<div className="order-service-editor-head">
|
||||
<div>
|
||||
<Space size={8} wrap>
|
||||
<Title level={5}>{selectedCategory.name}</Title>
|
||||
<Tag color={selectedProfile?.status === 'published' ? 'green' : 'orange'}>
|
||||
{selectedProfile?.status === 'published'
|
||||
? `线上 v${selectedProfile.currentVersionNo || 1}`
|
||||
: selectedProfile?.status === 'archived'
|
||||
? '已归档'
|
||||
: '草稿'}
|
||||
</Tag>
|
||||
</Space>
|
||||
<Text type="secondary">分类标识:{selectedCategory.categoryKey}</Text>
|
||||
</div>
|
||||
<Space size={8} wrap>
|
||||
<Button
|
||||
icon={<SaveOutlined />}
|
||||
loading={saving}
|
||||
onClick={() => void handleSave()}
|
||||
>
|
||||
保存草稿
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SendOutlined />}
|
||||
disabled={!selectedProfile}
|
||||
loading={publishMutation.isPending}
|
||||
onClick={() => {
|
||||
if (selectedProfile) publishMutation.mutate(selectedProfile.profileId)
|
||||
}}
|
||||
>
|
||||
发布新版本
|
||||
</Button>
|
||||
{selectedProfile ? (
|
||||
<Button
|
||||
danger
|
||||
icon={<StopOutlined />}
|
||||
loading={archiveMutation.isPending}
|
||||
onClick={() => archiveMutation.mutate(selectedProfile.profileId)}
|
||||
>
|
||||
归档
|
||||
</Button>
|
||||
) : null}
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Form form={form} layout="vertical">
|
||||
<Divider plain>基础与打手端展示</Divider>
|
||||
<Form.Item label="配置名称" name="name" rules={[{ required: true }]}>
|
||||
<Input placeholder="后台识别名称" />
|
||||
</Form.Item>
|
||||
<Form.Item label="打手端 tab 名称" name="tabTitle">
|
||||
<Input placeholder="留空则使用配置名称" />
|
||||
</Form.Item>
|
||||
<Form.Item label="打手端说明" name="description">
|
||||
<Input.TextArea autoSize={{ minRows: 1, maxRows: 3 }} />
|
||||
</Form.Item>
|
||||
|
||||
<Divider plain>发布字段</Divider>
|
||||
<Space wrap size={24}>
|
||||
<Form.Item label="操作系统" name={['fields', 'system']} valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item label="区服" name={['fields', 'serverZone']} valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item label="系列" name={['fields', 'series']} valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="钻石等级"
|
||||
name={['fields', 'diamondLevel']}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="璀璨数"
|
||||
name={['fields', 'brillianceCount']}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item label="数量" name={['fields', 'quantity']} valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
|
||||
<Divider plain>价格 / 保证金 / 服务费</Divider>
|
||||
<Space wrap size={24}>
|
||||
<Form.Item
|
||||
label="使用接单模板价"
|
||||
name={['pricing', 'useTemplatePrice']}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="允许打手改价"
|
||||
name={['pricing', 'allowWorkerPrice']}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item label="最低售价(元)" name={['pricing', 'minPriceYuan']}>
|
||||
<InputNumber min={0} precision={2} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Space wrap size={24}>
|
||||
<Form.Item
|
||||
label="开启保证金"
|
||||
name={['deposit', 'enabled']}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item label="保证金模式" name={['deposit', 'mode']}>
|
||||
<Select
|
||||
style={{ width: 140 }}
|
||||
options={[
|
||||
{ value: 'fixed', label: '固定金额' },
|
||||
{ value: 'per_quantity', label: '按数量' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="固定金额(元)" name={['deposit', 'fixedAmountYuan']}>
|
||||
<InputNumber min={0} precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item label="每件金额(元)" name={['deposit', 'unitAmountYuan']}>
|
||||
<InputNumber min={0} precision={2} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Space wrap size={24}>
|
||||
<Form.Item
|
||||
label="开启服务费"
|
||||
name={['serviceFee', 'enabled']}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item label="服务费模式" name={['serviceFee', 'mode']}>
|
||||
<Select
|
||||
style={{ width: 140 }}
|
||||
options={[
|
||||
{ value: 'percent', label: '按比例 %' },
|
||||
{ value: 'fixed', label: '固定金额' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="比例(%)" name={['serviceFee', 'percent']}>
|
||||
<InputNumber min={0} max={100} precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item label="固定金额(元)" name={['serviceFee', 'amountYuan']}>
|
||||
<InputNumber min={0} precision={2} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
|
||||
<Divider plain>数量限制 / 匹配 / 超时 / 降价</Divider>
|
||||
<Space wrap size={24}>
|
||||
<Form.Item
|
||||
label="开启单人寄售上限"
|
||||
name={['inventoryLimit', 'enabled']}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item label="最多在售数量" name={['inventoryLimit', 'maxActiveQuantity']}>
|
||||
<InputNumber min={0} precision={0} />
|
||||
</Form.Item>
|
||||
<Form.Item label="匹配模式" name={['matching', 'mode']}>
|
||||
<Select
|
||||
style={{ width: 140 }}
|
||||
options={[
|
||||
{ value: 'manual', label: '手动匹配' },
|
||||
{ value: 'auto', label: '自动匹配' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="自动匹配开关"
|
||||
name={['matching', 'autoMatchEnabled']}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="低价订单优先"
|
||||
name={['matching', 'preferLowReward']}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Space wrap size={24}>
|
||||
<Form.Item label="内部匹配等待(分钟)" name={['internalMatch', 'delayMinutes']}>
|
||||
<InputNumber min={0} precision={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="超时发回大厅"
|
||||
name={['internalMatch', 'fallbackToHall']}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item label="代练超时(分钟)" name={['timeout', 'minutes']}>
|
||||
<InputNumber min={0} precision={0} />
|
||||
</Form.Item>
|
||||
<Form.Item label="超时动作" name={['timeout', 'policy']}>
|
||||
<Select
|
||||
style={{ width: 160 }}
|
||||
options={[
|
||||
{ value: 'reopen', label: '退回大厅' },
|
||||
{ value: 'cancel_release', label: '取消并释放' },
|
||||
{ value: 'cancel_deduct', label: '取消并扣押金' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Space wrap size={24}>
|
||||
<Form.Item
|
||||
label="开启降价提示"
|
||||
name={['priceDrop', 'enabled']}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item label="未售出时长(小时)" name={['priceDrop', 'hours']}>
|
||||
<InputNumber min={1} precision={0} />
|
||||
</Form.Item>
|
||||
<Form.Item label="降幅模式" name={['priceDrop', 'mode']}>
|
||||
<Select
|
||||
style={{ width: 140 }}
|
||||
options={[
|
||||
{ value: 'percent', label: '按比例 %' },
|
||||
{ value: 'fixed', label: '固定金额' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="降幅数值" name={['priceDrop', 'value']}>
|
||||
<InputNumber min={0} precision={0} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Form>
|
||||
</>
|
||||
) : (
|
||||
<Empty description="请选择分类" />
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -266,6 +266,10 @@ export default function ConsignmentInventoryPanel() {
|
||||
diamondLevel: Number(values.diamondLevel || 1),
|
||||
brillianceCount: Number(values.brillianceCount || 0),
|
||||
costPriceFen,
|
||||
salePriceFen:
|
||||
values.salePrice !== undefined
|
||||
? Math.max(0, Math.round(Number(values.salePrice) * 100))
|
||||
: undefined,
|
||||
buyerName: values.buyerName,
|
||||
shopName: values.shopName,
|
||||
inboundBatch: values.inboundBatch,
|
||||
@@ -663,6 +667,7 @@ export default function ConsignmentInventoryPanel() {
|
||||
editForm.setFieldsValue({
|
||||
...record,
|
||||
costPrice: record.costPriceFen / 100,
|
||||
salePrice: (record.salePriceFen ?? record.costPriceFen) / 100,
|
||||
workerId: record.workerId || undefined,
|
||||
// 没有押金就留空,不能兜底成 100:保存会把"无押金"悄悄写成 ¥100
|
||||
depositAmount: record.depositAmountFen
|
||||
@@ -915,6 +920,11 @@ export default function ConsignmentInventoryPanel() {
|
||||
<InputNumber min={0} step={1} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item name="salePrice" label="寄卖售价 (元)">
|
||||
<InputNumber min={0} step={1} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item name="allowMatching" label="允许匹配" valuePropName="checked">
|
||||
<Switch checkedChildren="开" unCheckedChildren="关" />
|
||||
|
||||
@@ -102,6 +102,7 @@ import {
|
||||
resolveConsignmentScreenshotUrls,
|
||||
} from '@/utils/consignment-display'
|
||||
import ConsignmentManualMatchModal from '@/components/admin/ConsignmentManualMatchModal'
|
||||
import ConsignmentWorkOrderMatchModal from '@/components/admin/ConsignmentWorkOrderMatchModal'
|
||||
import { AdminRangePicker } from '@/components/admin/AdminDatePicker'
|
||||
import dayjs, { ADMIN_DATE_FORMAT } from '@/lib/dayjs'
|
||||
import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination'
|
||||
@@ -161,6 +162,7 @@ const ADMIN_NOTE_QUICK_TAGS = ['已通知加好友'] as const
|
||||
const FILTERABLE_WORK_ORDER_STATUSES = [
|
||||
'pending_material',
|
||||
'unassigned',
|
||||
'internal_matching',
|
||||
'open',
|
||||
'in_progress',
|
||||
'pending_acceptance',
|
||||
@@ -413,6 +415,7 @@ export default function WorkOrdersPanel() {
|
||||
|
||||
// 寄售库存匹配拦截弹窗状态
|
||||
const [manualMatchOrder, setManualMatchOrder] = useState<WorkOrder | null>(null)
|
||||
const [consignmentMatchOrder, setConsignmentMatchOrder] = useState<WorkOrder | null>(null)
|
||||
const [consignmentInterceptModal, setConsignmentInterceptModal] = useState<{
|
||||
open: boolean
|
||||
order: WorkOrder | null
|
||||
@@ -1988,6 +1991,16 @@ export default function WorkOrdersPanel() {
|
||||
手动匹配寄售
|
||||
</Button>
|
||||
) : null}
|
||||
{canFulfillConsignment &&
|
||||
['internal_matching', 'unassigned', 'open'].includes(String(row.status || '')) ? (
|
||||
<Button
|
||||
icon={<ThunderboltOutlined />}
|
||||
onClick={() => setConsignmentMatchOrder(row)}
|
||||
title="按寄卖配置精确匹配库存;拼单工单可选多条库存一起拼"
|
||||
>
|
||||
内部库存匹配
|
||||
</Button>
|
||||
) : null}
|
||||
{row.capabilities?.canCancel && (canReturnToHall || canCancelOrder) ? (
|
||||
<Button danger icon={<UndoOutlined />} onClick={() => openCancelModal(row)}>
|
||||
撤单
|
||||
@@ -4351,6 +4364,17 @@ export default function WorkOrdersPanel() {
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<ConsignmentWorkOrderMatchModal
|
||||
open={Boolean(consignmentMatchOrder)}
|
||||
workOrderId={consignmentMatchOrder?.workOrderId ?? null}
|
||||
onClose={() => setConsignmentMatchOrder(null)}
|
||||
onMatched={() => {
|
||||
void queryClient.invalidateQueries({ queryKey: ['admin-work-orders'] })
|
||||
void queryClient.invalidateQueries({ queryKey: ['admin-work-orders-stats'] })
|
||||
void queryClient.invalidateQueries({ queryKey: ['admin-consignment-inventories'] })
|
||||
}}
|
||||
/>
|
||||
|
||||
<ConsignmentManualMatchModal
|
||||
open={Boolean(manualMatchOrder)}
|
||||
onClose={() => setManualMatchOrder(null)}
|
||||
|
||||
@@ -208,6 +208,7 @@ export function formatStatus(status: string) {
|
||||
const labels: Record<string, string> = {
|
||||
pending_material: '待完善',
|
||||
unassigned: '待分配',
|
||||
internal_matching: '内部匹配中',
|
||||
open: '待抢单',
|
||||
in_progress: '代练中',
|
||||
pending_acceptance: '待验收',
|
||||
@@ -223,6 +224,7 @@ export function resolveStatusColor(status: string) {
|
||||
const colors: Record<string, string> = {
|
||||
pending_material: 'orange',
|
||||
unassigned: 'cyan',
|
||||
internal_matching: 'magenta',
|
||||
open: 'blue',
|
||||
in_progress: 'geekblue',
|
||||
pending_acceptance: 'purple',
|
||||
|
||||
Reference in New Issue
Block a user