feat(consignment): 新增和平精英寄卖库存管理、定向收集审核与工单智能匹配履约

This commit is contained in:
yml2213
2026-09-14 11:39:15 +08:00
parent 38c0cf1223
commit 1aa6dba931
30 changed files with 6350 additions and 44 deletions
@@ -11,6 +11,8 @@ import AfterSalesPanel, { type AfterSalesPanelHandle } from './panels/AfterSales
import AnnouncementPanel from './panels/AnnouncementPanel'
import CancelRequestsPanel from './panels/CancelRequestsPanel'
import CategoriesPanel from './panels/CategoriesPanel'
import ConsignmentInventoryPanel from './panels/ConsignmentInventoryPanel'
import ConsignmentSubmissionsPanel from './panels/ConsignmentSubmissionsPanel'
import FeedbacksPanel from './panels/FeedbacksPanel'
import FinancePanel from './panels/FinancePanel'
import LeaderboardPanel from './panels/LeaderboardPanel'
@@ -94,6 +96,16 @@ export default function AdminWorkerPlatformPage() {
: isSupport
? [
{ key: 'orders', label: '接单工单', children: <WorkOrdersPanel /> },
{
key: 'consignment-inventory',
label: '寄售仓库',
children: <ConsignmentInventoryPanel />,
},
{
key: 'consignment-submissions',
label: '寄卖收集与审核',
children: <ConsignmentSubmissionsPanel />,
},
{
key: 'cancel-requests',
label: '打手退单审核',
@@ -104,6 +116,16 @@ export default function AdminWorkerPlatformPage() {
]
: [
{ key: 'orders', label: '接单工单', children: <WorkOrdersPanel /> },
{
key: 'consignment-inventory',
label: '寄售仓库',
children: <ConsignmentInventoryPanel />,
},
{
key: 'consignment-submissions',
label: '寄卖收集与审核',
children: <ConsignmentSubmissionsPanel />,
},
{
key: 'cancel-requests',
label: '打手退单审核',
@@ -142,6 +164,8 @@ const ACTIVE_TAB_STORAGE_KEY = 'admin-worker-platform-tab'
function loadActiveTabPreference(): string {
const saved = localStorage.getItem(ACTIVE_TAB_STORAGE_KEY)
return saved === 'orders' ||
saved === 'consignment-inventory' ||
saved === 'consignment-submissions' ||
saved === 'leaderboard' ||
saved === 'cancel-requests' ||
saved === 'feedbacks' ||
@@ -165,6 +189,8 @@ function loadActiveTab(tab: string | null): string {
function isWorkerPlatformTab(tab: string | null): tab is string {
return [
'orders',
'consignment-inventory',
'consignment-submissions',
'leaderboard',
'cancel-requests',
'feedbacks',
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,947 @@
import {
CheckCircleOutlined,
CloseCircleOutlined,
CopyOutlined,
EyeOutlined,
LinkOutlined,
PlusOutlined,
ReloadOutlined,
} from '@ant-design/icons'
import { useQuery } from '@tanstack/react-query'
import {
Alert,
App,
Badge,
Button,
Card,
Col,
Descriptions,
Form,
Image,
Input,
InputNumber,
Modal,
Radio,
Row,
Select,
Space,
Switch,
Table,
Tabs,
Tag,
Typography,
} from 'antd'
import type { TableColumnsType } from 'antd'
import { useState } from 'react'
import {
approveAdminConsignmentSubmission,
createAdminConsignmentLink,
fetchAdminConsignmentLinks,
fetchAdminConsignmentSubmissions,
rejectAdminConsignmentSubmission,
updateAdminConsignmentLink,
} from '@/services/admin/consignment'
import type { ConsignmentCollectLink, ConsignmentSubmission } from '@/types/consignment'
const { Text } = Typography
export default function ConsignmentSubmissionsPanel() {
const { message } = App.useApp()
const [activeSubTab, setActiveSubTab] = useState<'submissions' | 'links'>('submissions')
// 提报审核状态
const [submissionStatus, setSubmissionStatus] = useState<string>('pending_review')
const [submissionKeyword, setSubmissionKeyword] = useState<string>('')
const [submissionPage, setSubmissionPage] = useState<number>(1)
const [submissionPageSize, setSubmissionPageSize] = useState<number>(20)
// 弹窗状态
const [createLinkModalOpen, setCreateLinkModalOpen] = useState(false)
const [createdLinkUrl, setCreatedLinkUrl] = useState<string>('')
const [approveModalItem, setApproveModalItem] = useState<ConsignmentSubmission | null>(null)
const [rejectModalItem, setRejectModalItem] = useState<ConsignmentSubmission | null>(null)
const [viewingSubmission, setViewingSubmission] = useState<ConsignmentSubmission | null>(null)
const [rejectNote, setRejectNote] = useState('')
const [linkForm] = Form.useForm()
const [approveForm] = Form.useForm()
// 查询提报列表
const submissionsQuery = useQuery({
queryKey: [
'admin-consignment-submissions',
submissionStatus,
submissionKeyword,
submissionPage,
submissionPageSize,
],
queryFn: () =>
fetchAdminConsignmentSubmissions({
status: submissionStatus === 'all' ? undefined : submissionStatus,
keyword: submissionKeyword || undefined,
page: submissionPage,
pageSize: submissionPageSize,
}),
})
// 查询链接列表
const linksQuery = useQuery({
queryKey: ['admin-consignment-links'],
queryFn: () => fetchAdminConsignmentLinks(),
})
const submissions = submissionsQuery.data?.data.items || []
const submissionTotal = submissionsQuery.data?.data.total || 0
const links = Array.isArray(linksQuery.data?.data)
? linksQuery.data?.data
: linksQuery.data?.data?.items || []
// 复制收集公开链接
function copyPublicLink(token: string) {
const origin = window.location.origin
const url = `${origin}/#/consignment-collect/${token}`
navigator.clipboard.writeText(url)
message.success('收集链接已复制到剪贴板,可直接发给客户!')
}
// 切换链接启用/关闭
async function handleToggleLink(record: ConsignmentCollectLink, enabled: boolean) {
try {
await updateAdminConsignmentLink(record.id, { enabled })
message.success(enabled ? '链接已开启' : '链接已关闭')
linksQuery.refetch()
} catch (err: any) {
message.error(err.message || '更新失败')
}
}
// 创建收集链接
async function handleCreateLinkSubmit() {
try {
const values = await linkForm.validateFields()
const presetPriceFen = Math.round(Number(values.presetPrice || 0) * 100)
const briStr = values.brillianceCount > 0 ? `${values.brillianceCount}璀璨` : ''
const defaultTitle = `${values.system}${values.serverZone}${values.series}${values.diamondLevel}钻${briStr} 寄卖征集`
const res = await createAdminConsignmentLink({
title: values.title || defaultTitle,
system: values.system,
serverZone: values.serverZone,
series: values.series,
diamondLevel: Number(values.diamondLevel || 1),
brillianceCount: Number(values.brillianceCount || 0),
presetPriceFen,
buyerName: values.buyerName,
inboundBatch: values.inboundBatch,
isMultiUse: values.isMultiUse !== false,
})
const token = res.data?.token || ''
const url = `${window.location.origin}/#/consignment-collect/${token}`
setCreatedLinkUrl(url)
try {
navigator.clipboard.writeText(url)
} catch {}
message.success('收集链接生成成功!')
setCreateLinkModalOpen(false)
linkForm.resetFields()
await linksQuery.refetch()
} catch (err: any) {
if (err.errorFields) return
message.error(err.message || '创建收集链接失败')
}
}
// 审核通过并入库
async function handleApproveSubmit() {
if (!approveModalItem) return
try {
const values = await approveForm.validateFields()
const costPriceFen = Math.round(Number(values.costPrice || 0) * 100)
await approveAdminConsignmentSubmission(approveModalItem.id, {
costPriceFen,
buyerName: values.buyerName,
shopName: values.shopName,
inboundBatch: values.inboundBatch,
itemName: values.itemName,
system: values.system,
serverZone: values.serverZone,
series: values.series,
diamondLevel: Number(values.diamondLevel || 1),
brillianceCount: Number(values.brillianceCount || 0),
sellerContact: values.sellerContact,
sellerGameId: values.sellerGameId,
sellerPaymentInfo: values.sellerPaymentInfo,
sellerPhone: values.sellerPhone,
reviewNote: values.reviewNote,
})
message.success('审核通过!已正式存入寄售库存待匹配!')
setApproveModalItem(null)
submissionsQuery.refetch()
} catch (err: any) {
if (err.errorFields) return
message.error(err.message || '审核入库失败')
}
}
// 驳回提报
async function handleRejectConfirm() {
if (!rejectModalItem) return
try {
await rejectAdminConsignmentSubmission(rejectModalItem.id, { reviewNote: rejectNote })
message.success('已驳回该提报申请')
setRejectModalItem(null)
setRejectNote('')
submissionsQuery.refetch()
} catch (err: any) {
message.error(err.message || '驳回失败')
}
}
// 提报列表列定义
const submissionColumns: TableColumnsType<ConsignmentSubmission> = [
{
title: '提报单号',
key: 'submissionNo',
width: 170,
render: (_, record) => (
<div>
<Text strong>{record.submissionNo}</Text>
{record.linkTitle ? (
<div style={{ fontSize: 11, color: '#8c8c8c' }}>来自: {record.linkTitle}</div>
) : null}
</div>
),
},
{
title: '提报物品规格',
key: 'specs',
width: 210,
render: (_, record) => (
<Space direction="vertical" size={2}>
<div style={{ fontWeight: 600 }}>{record.itemName}</div>
<Space size={4} wrap>
<Tag color={record.system === '苹果' ? 'geekblue' : 'green'}>{record.system}</Tag>
<Tag color="cyan">{record.serverZone}</Tag>
<Tag color="orange">{record.series}</Tag>
<Tag color="gold">{record.diamondLevel} 钻</Tag>
{record.brillianceCount > 0 ? (
<Tag color="magenta">{record.brillianceCount} 璀璨</Tag>
) : null}
</Space>
</Space>
),
},
{
title: '卖家报价',
key: 'price',
width: 110,
render: (_, record) => (
<Text strong style={{ color: '#cf1322', fontSize: 15 }}>
¥{(record.askingPriceFen / 100).toFixed(2)}
</Text>
),
},
{
title: '卖家联系方式',
key: 'contact',
width: 180,
render: (_, record) => (
<div style={{ fontSize: 12 }}>
<div>联系: {record.contactInfo}</div>
{record.sellerGameId ? <div>游戏ID: {record.sellerGameId}</div> : null}
{record.sellerPhone ? <div>电话: {record.sellerPhone}</div> : null}
</div>
),
},
{
title: '凭据截图',
key: 'screens',
width: 110,
render: (_, record) => {
const screens = Array.isArray(record.screenshotsJson) ? record.screenshotsJson : []
if (screens.length === 0) return <Text type="secondary">无截图</Text>
return (
<Image.PreviewGroup>
<Image
src={screens[0]}
width={48}
height={48}
style={{ objectFit: 'cover', borderRadius: 4 }}
/>
</Image.PreviewGroup>
)
},
},
{
title: '状态',
key: 'status',
width: 100,
render: (_, record) => {
if (record.status === 'pending_review') return <Tag color="gold">待客服审核</Tag>
if (record.status === 'approved') return <Tag color="green">已入库</Tag>
return <Tag color="red">已驳回</Tag>
},
},
{
title: '提报时间',
dataIndex: 'createdAt',
width: 150,
render: (v) => <Text style={{ fontSize: 12 }}>{v}</Text>,
},
{
title: '操作',
key: 'actions',
width: 160,
render: (_, record) => (
<Space size={8}>
<Button
size="small"
type="text"
icon={<EyeOutlined />}
onClick={() => setViewingSubmission(record)}
>
详情
</Button>
{record.status === 'pending_review' ? (
<>
<Button
size="small"
type="primary"
onClick={() => {
setApproveModalItem(record)
approveForm.setFieldsValue({
...record,
costPrice: record.askingPriceFen / 100,
sellerContact: record.contactInfo,
buyerName: '客服',
inboundBatch: new Date().toISOString().slice(0, 7),
})
}}
>
审核入库
</Button>
<Button
size="small"
danger
type="text"
onClick={() => {
setRejectModalItem(record)
setRejectNote('')
}}
>
驳回
</Button>
</>
) : null}
</Space>
),
},
]
// 链接列表列定义
const linkColumns: TableColumnsType<ConsignmentCollectLink> = [
{
title: '收集标题 / 规格',
key: 'title',
width: 280,
render: (_, record) => {
const fullUrl = `${window.location.origin}/#/consignment-collect/${record.token}`
return (
<div>
<div style={{ fontWeight: 600 }}>{record.title}</div>
<Space size={4} wrap style={{ marginTop: 4 }}>
<Tag color={record.system === '苹果' ? 'geekblue' : 'green'}>
{record.system || '通用'}
</Tag>
<Tag color="cyan">{record.serverZone || '全区'}</Tag>
<Tag color="orange">{record.series || '通用'}</Tag>
<Tag color="gold">{record.diamondLevel || 1} 钻</Tag>
{record.brillianceCount > 0 ? (
<Tag color="magenta">{record.brillianceCount} 璀璨</Tag>
) : null}
</Space>
<div style={{ marginTop: 6 }}>
<Text
copyable={{ text: fullUrl }}
style={{ fontSize: 11, color: '#1890ff', wordBreak: 'break-all' }}
>
{fullUrl}
</Text>
</div>
</div>
)
},
},
{
title: '指导参考价',
key: 'price',
width: 110,
render: (_, record) => (
<Text strong style={{ color: '#cf1322' }}>
¥{(record.presetPriceFen / 100).toFixed(2)}
</Text>
),
},
{
title: '创建人/批次',
key: 'creator',
width: 140,
render: (_, record) => (
<div>
<div>{record.createdBy || '-'}</div>
{record.inboundBatch ? (
<div style={{ fontSize: 11, color: '#8c8c8c' }}>批次: {record.inboundBatch}</div>
) : null}
</div>
),
},
{
title: '提报数量',
key: 'count',
width: 100,
render: (_, record) => (
<Badge
count={record.submissionCount || 0}
showZero
style={{ backgroundColor: (record.submissionCount || 0) > 0 ? '#52c41a' : '#d9d9d9' }}
/>
),
},
{
title: '状态开关',
key: 'enabled',
width: 100,
render: (_, record) => (
<Switch
checked={record.enabled}
checkedChildren="开启"
unCheckedChildren="关闭"
onChange={(checked) => handleToggleLink(record, checked)}
/>
),
},
{
title: '操作',
key: 'actions',
width: 180,
render: (_, record) => (
<Space size={8}>
<Button
size="small"
type="primary"
ghost
icon={<CopyOutlined />}
onClick={() => copyPublicLink(record.token)}
>
复制公开链接
</Button>
</Space>
),
},
]
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<Tabs
activeKey={activeSubTab}
onChange={(k) => setActiveSubTab(k as any)}
items={[
{
key: 'submissions',
label: (
<span>
玩家提报审核{' '}
{submissions.filter((s) => s.status === 'pending_review').length > 0 ? (
<Badge
count={submissions.filter((s) => s.status === 'pending_review').length}
offset={[6, -2]}
/>
) : null}
</span>
),
children: (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* 过滤条 */}
<Card size="small">
<Space wrap size={12}>
<Radio.Group
value={submissionStatus}
onChange={(e) => {
setSubmissionStatus(e.target.value)
setSubmissionPage(1)
}}
buttonStyle="solid"
>
<Radio.Button value="pending_review">待客服审核</Radio.Button>
<Radio.Button value="approved">已审核入库</Radio.Button>
<Radio.Button value="rejected">已驳回</Radio.Button>
<Radio.Button value="all">全部</Radio.Button>
</Radio.Group>
<Input.Search
placeholder="搜索提报单号/微信号/游戏ID"
allowClear
style={{ width: 240 }}
onSearch={(v) => {
setSubmissionKeyword(v)
setSubmissionPage(1)
}}
/>
<Button icon={<ReloadOutlined />} onClick={() => submissionsQuery.refetch()}>
刷新
</Button>
</Space>
</Card>
{/* 提报审核表格 */}
<Card size="small">
<Table
rowKey="id"
columns={submissionColumns}
dataSource={submissions}
loading={submissionsQuery.isLoading}
pagination={{
current: submissionPage,
pageSize: submissionPageSize,
total: submissionTotal,
showSizeChanger: true,
showTotal: (t) => `共 ${t} 条提报记录`,
onChange: (p, ps) => {
setSubmissionPage(p)
setSubmissionPageSize(ps)
},
}}
/>
</Card>
</div>
),
},
{
key: 'links',
label: '收集链接生成与管理',
children: (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<Card size="small">
<Space wrap size={12}>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => {
linkForm.resetFields()
linkForm.setFieldsValue({
system: '苹果',
serverZone: '微信区',
series: '暗潮',
diamondLevel: 1,
brillianceCount: 0,
isMultiUse: true,
buyerName: '客服',
})
setCreateLinkModalOpen(true)
}}
>
生成定向收集链接
</Button>
<Button icon={<ReloadOutlined />} onClick={() => linksQuery.refetch()}>
刷新
</Button>
</Space>
</Card>
<Card size="small">
<Table
rowKey="id"
columns={linkColumns}
dataSource={links}
loading={linksQuery.isLoading}
pagination={false}
/>
</Card>
</div>
),
},
]}
/>
{/* 生成收集链接弹窗 */}
<Modal
title="生成寄卖线索收集链接"
open={createLinkModalOpen}
width={600}
onCancel={() => setCreateLinkModalOpen(false)}
onOk={handleCreateLinkSubmit}
destroyOnClose
>
<Form form={linkForm} layout="vertical">
<Row gutter={16}>
<Col span={8}>
<Form.Item name="system" label="指定系统" rules={[{ required: true }]}>
<Select
options={[
{ label: '苹果 (iOS)', value: '苹果' },
{ label: '安卓 (Android)', value: '安卓' },
{ label: '通用', value: '通用' },
]}
/>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="serverZone" label="指定区服" rules={[{ required: true }]}>
<Select
options={[
{ label: '微信区', value: '微信区' },
{ label: 'QQ区', value: 'QQ区' },
]}
/>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="series" label="枪系/系列" rules={[{ required: true }]}>
<Select
options={[
{ label: '暗潮', value: '暗潮' },
{ label: '怒嚎', value: '怒嚎' },
{ label: '血宴', value: '血宴' },
{ label: '矩阵', value: '矩阵' },
{ label: '其他', value: '其他' },
]}
/>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={8}>
<Form.Item name="diamondLevel" label="钻石等级" rules={[{ required: true }]}>
<Select
options={[
{ label: '1 钻', value: 1 },
{ label: '2 钻', value: 2 },
{ label: '3 钻', value: 3 },
{ label: '4 钻', value: 4 },
{ label: '5 钻', value: 5 },
{ label: '6 钻', value: 6 },
]}
/>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="brillianceCount" label="璀璨数">
<InputNumber min={0} max={6} style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="presetPrice" label="指导收购价 (元)">
<InputNumber min={0} style={{ width: '100%' }} placeholder="如 80" />
</Form.Item>
</Col>
</Row>
<Form.Item
name="title"
label="链接标题 (留空则根据规格自动生成)"
tooltip="玩家打开链接时看到的标题"
>
<Input placeholder="如:【高价收】苹果微信暗潮1钻 极速审核" />
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="buyerName" label="默认收购人">
<Input placeholder="客服姓名" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="inboundBatch" label="归属批次">
<Input placeholder="如 202609-01" />
</Form.Item>
</Col>
</Row>
<Form.Item name="isMultiUse" label="允许多次提报" valuePropName="checked">
<Switch defaultChecked checkedChildren="多人可填" unCheckedChildren="单次失效" />
</Form.Item>
</Form>
</Modal>
{/* 审核入库弹窗 */}
<Modal
title="审核寄卖申请并正式入库"
open={Boolean(approveModalItem)}
width={680}
onCancel={() => setApproveModalItem(null)}
onOk={handleApproveSubmit}
okText="审核通过并正式入库"
okType="primary"
destroyOnClose
>
<div style={{ marginBottom: 16 }}>
<Text type="secondary">
提报人联系方式:<strong>{approveModalItem?.contactInfo}</strong> | 游戏ID:
<strong>{approveModalItem?.sellerGameId || '-'}</strong> | 卖家要价:
<strong style={{ color: '#cf1322' }}>
¥{((approveModalItem?.askingPriceFen || 0) / 100).toFixed(2)}
</strong>
</Text>
</div>
<Form form={approveForm} layout="vertical">
<Row gutter={16}>
<Col span={14}>
<Form.Item
name="itemName"
label="正式入库物品名"
rules={[{ required: true, message: '请输入入库物品名' }]}
>
<Input />
</Form.Item>
</Col>
<Col span={10}>
<Form.Item
name="costPrice"
label="实际收购成本 (元)"
rules={[{ required: true, message: '请输入实际收购成本' }]}
>
<InputNumber min={0} step={1} style={{ width: '100%' }} />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={6}>
<Form.Item name="system" label="系统">
<Input />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item name="serverZone" label="区服">
<Input />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item name="series" label="系列">
<Input />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item name="diamondLevel" label="钻石等级">
<InputNumber min={1} max={6} style={{ width: '100%' }} />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={8}>
<Form.Item
name="buyerName"
label="收购人 (客服)"
rules={[{ required: true, message: '请填写收购人' }]}
>
<Input />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="shopName" label="关联店铺">
<Input placeholder="店铺名" />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="inboundBatch" label="收购批次">
<Input placeholder="批次" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={8}>
<Form.Item name="sellerContact" label="卖家微信号">
<Input />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="sellerPhone" label="卖家手机">
<Input />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="sellerGameId" label="卖家游戏ID">
<Input />
</Form.Item>
</Col>
</Row>
<Form.Item name="sellerPaymentInfo" label="卖家收款信息">
<Input />
</Form.Item>
<Form.Item name="reviewNote" label="审核备注">
<Input.TextArea rows={2} placeholder="审核核准说明" />
</Form.Item>
</Form>
</Modal>
{/* 驳回弹窗 */}
<Modal
title="驳回寄卖提报"
open={Boolean(rejectModalItem)}
onCancel={() => setRejectModalItem(null)}
onOk={handleRejectConfirm}
okText="确认驳回"
okType="danger"
>
<div style={{ marginBottom: 12 }}>
<Text>
确认驳回 <strong>[{rejectModalItem?.submissionNo}]</strong> 的寄卖申请吗?
</Text>
</div>
<Form layout="vertical">
<Form.Item label="驳回原因说明">
<Input.TextArea
rows={3}
value={rejectNote}
onChange={(e) => setRejectNote(e.target.value)}
placeholder="如:要价过高、物品规格不符、账号存在实名绑定限制等"
/>
</Form.Item>
</Form>
</Modal>
{/* 提报详情查看弹窗 */}
<Modal
title={`提报详情 [${viewingSubmission?.submissionNo}]`}
open={Boolean(viewingSubmission)}
width={680}
footer={[
<Button key="close" onClick={() => setViewingSubmission(null)}>
关闭
</Button>,
]}
onCancel={() => setViewingSubmission(null)}
>
{viewingSubmission ? (
<Descriptions bordered size="small" column={2}>
<Descriptions.Item label="提报单号" span={2}>
<Text copyable>{viewingSubmission.submissionNo}</Text>
</Descriptions.Item>
<Descriptions.Item label="物品名称" span={2}>
{viewingSubmission.itemName}
</Descriptions.Item>
<Descriptions.Item label="规格">
{viewingSubmission.system} / {viewingSubmission.serverZone} /{' '}
{viewingSubmission.series} / {viewingSubmission.diamondLevel}钻 (
{viewingSubmission.brillianceCount}璀璨)
</Descriptions.Item>
<Descriptions.Item label="卖家报价">
<Text strong style={{ color: '#cf1322' }}>
¥{(viewingSubmission.askingPriceFen / 100).toFixed(2)}
</Text>
</Descriptions.Item>
<Descriptions.Item label="卖家微信号">
<Text copyable>{viewingSubmission.contactInfo}</Text>
</Descriptions.Item>
<Descriptions.Item label="卖家电话">
<Text copyable>{viewingSubmission.sellerPhone || '-'}</Text>
</Descriptions.Item>
<Descriptions.Item label="卖家游戏ID" span={2}>
<Text copyable>{viewingSubmission.sellerGameId || '-'}</Text>
</Descriptions.Item>
<Descriptions.Item label="卖家收款方式" span={2}>
<Text copyable>{viewingSubmission.sellerPaymentInfo || '-'}</Text>
</Descriptions.Item>
<Descriptions.Item label="凭据截图" span={2}>
{Array.isArray(viewingSubmission.screenshotsJson) &&
viewingSubmission.screenshotsJson.length > 0 ? (
<Image.PreviewGroup>
<Space wrap>
{viewingSubmission.screenshotsJson.map((src, i) => (
<Image
key={i}
src={src}
width={80}
height={80}
style={{ objectFit: 'cover', borderRadius: 4 }}
/>
))}
</Space>
</Image.PreviewGroup>
) : (
<Text type="secondary">无截图</Text>
)}
</Descriptions.Item>
<Descriptions.Item label="审核状态">
{viewingSubmission.status === 'pending_review' ? (
<Tag color="gold">待审核</Tag>
) : viewingSubmission.status === 'approved' ? (
<Tag color="green">已入库</Tag>
) : (
<Tag color="red">已驳回</Tag>
)}
</Descriptions.Item>
<Descriptions.Item label="审核人">
{viewingSubmission.reviewerName || '-'}
</Descriptions.Item>
<Descriptions.Item label="审核备注" span={2}>
{viewingSubmission.reviewNote || '-'}
</Descriptions.Item>
</Descriptions>
) : null}
</Modal>
{/* 链接生成成功提示弹窗 */}
<Modal
title="收集链接生成成功"
open={Boolean(createdLinkUrl)}
onCancel={() => setCreatedLinkUrl('')}
footer={[
<Button
key="copy"
type="primary"
icon={<CopyOutlined />}
onClick={() => {
navigator.clipboard?.writeText(createdLinkUrl)
message.success('链接已复制到剪贴板!')
}}
>
复制公开链接
</Button>,
<Button key="close" onClick={() => setCreatedLinkUrl('')}>
完成
</Button>,
]}
>
<Alert
type="success"
showIcon
message="定向收集链接已就绪"
description="该链接已自动复制到剪贴板。您可以直接发送给玩家卖家,玩家免登录打开即可登记账号与装备信息并上传凭据截图。"
style={{ marginBottom: 16 }}
/>
<div style={{ marginBottom: 8 }}>
<Text strong>公开收集链接:</Text>
</div>
<div
style={{
padding: '12px 16px',
background: '#f6ffed',
border: '1px solid #b7eb8f',
borderRadius: 6,
wordBreak: 'break-all',
}}
>
<Text
copyable={{ text: createdLinkUrl }}
style={{ fontSize: 13, color: '#389e0d', fontWeight: 500 }}
>
{createdLinkUrl}
</Text>
</div>
</Modal>
</div>
)
}
@@ -11,6 +11,7 @@ import {
UndoOutlined,
UserAddOutlined,
WarningOutlined,
RollbackOutlined,
} from '@ant-design/icons'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import {
@@ -72,6 +73,12 @@ import {
updateAdminWorkOrderNote,
updateAdminWorkOrderSharing,
} from '@/services/admin'
import {
checkWorkOrderConsignmentMatches,
fulfillWorkOrderWithConsignment,
revokeWorkOrderConsignmentFulfillment,
} from '@/services/admin/consignment'
import type { WorkOrderInventoryMatchCheck } from '@/types/consignment'
import type { UploadedFile, WorkOrder, WorkOrderShare } from '@/types/worker-platform'
import { AdminRangePicker } from '@/components/admin/AdminDatePicker'
import dayjs, { ADMIN_DATE_FORMAT } from '@/lib/dayjs'
@@ -259,6 +266,35 @@ export default function WorkOrdersPanel() {
unitReward?: number
totalAmount?: number
}>()
// 寄售库存匹配拦截弹窗状态
const [consignmentInterceptModal, setConsignmentInterceptModal] = useState<{
open: boolean
order: WorkOrder | null
matchData: WorkOrderInventoryMatchCheck | null
submitting: boolean
}>({
open: false,
order: null,
matchData: null,
submitting: false,
})
// 寄售履约撤单弹窗状态
const [revokeConsignmentModal, setRevokeConsignmentModal] = useState<{
open: boolean
order: WorkOrder | null
action: 'reopen' | 'cancel_order' | 'archive_inventory'
reason: string
submitting: boolean
}>({
open: false,
order: null,
action: 'reopen',
reason: '',
submitting: false,
})
// 仅单独筛选「待验收」时提供批量验收入口(多选状态下不启用勾选,避免误操作)
const isPendingAcceptanceOnly = statuses.length === 1 && statuses[0] === 'pending_acceptance'
const selectedWorkOrderStatuses = statuses.filter(
@@ -652,6 +688,75 @@ export default function WorkOrdersPanel() {
})
}
async function handlePublishClick(row: WorkOrder) {
try {
const matchRes = await checkWorkOrderConsignmentMatches(row.workOrderId)
if (matchRes.data?.hasMatch && (matchRes.data.matches?.length || 0) > 0) {
setConsignmentInterceptModal({
open: true,
order: row,
matchData: matchRes.data,
submitting: false,
})
return
}
} catch {
// 检查匹配异常降级走正常发布
}
confirmPublishWorkOrder(row)
}
async function handleFulfillWithConsignment(inventoryId: number) {
if (!consignmentInterceptModal.order) return
setConsignmentInterceptModal((prev) => ({ ...prev, submitting: true }))
try {
await fulfillWorkOrderWithConsignment({
workOrderId: consignmentInterceptModal.order.workOrderId,
inventoryId,
})
message.success('已使用内部寄售库存直接履约!工单已自动完成,无需打手接单佣金')
setConsignmentInterceptModal({ open: false, order: null, matchData: null, submitting: false })
await refreshAll()
} catch (err: any) {
message.error(err.message || '内部履约失败')
setConsignmentInterceptModal((prev) => ({ ...prev, submitting: false }))
}
}
function openRevokeConsignmentModal(row: WorkOrder) {
setRevokeConsignmentModal({
open: true,
order: row,
action: 'reopen',
reason: '',
submitting: false,
})
}
async function handleRevokeConsignmentSubmit() {
if (!revokeConsignmentModal.order) return
setRevokeConsignmentModal((prev) => ({ ...prev, submitting: true }))
try {
await revokeWorkOrderConsignmentFulfillment({
workOrderId: revokeConsignmentModal.order.workOrderId,
action: revokeConsignmentModal.action,
reason: revokeConsignmentModal.reason,
})
message.success('寄售履约已撤销!物品已退单回库,工单已更新')
setRevokeConsignmentModal({
open: false,
order: null,
action: 'reopen',
reason: '',
submitting: false,
})
await refreshAll()
} catch (err: any) {
message.error(err.message || '撤销履约失败')
setRevokeConsignmentModal((prev) => ({ ...prev, submitting: false }))
}
}
function confirmBatchAcceptance() {
const workOrderIds = selectedAcceptanceOrderIds
if (workOrderIds.length === 0) {
@@ -1018,6 +1123,12 @@ export default function WorkOrdersPanel() {
{shopName ? `店铺:${shopName}` : ''}
</Typography.Text>
) : null}
{(row.material as any)?.consignment ? (
<Tag color="purple" style={{ marginTop: 2, width: 'fit-content' }}>
寄售履约: {(row.material as any).consignment.inventoryNo} (成本: ¥
{(((row.material as any).consignment.costPriceFen || 0) / 100).toFixed(2)})
</Tag>
) : null}
</div>
)
},
@@ -1249,7 +1360,7 @@ export default function WorkOrdersPanel() {
title={
Number(row.rewardAmount || 0) <= 0 ? '接单金额为 0,不能发布到大厅' : undefined
}
onClick={() => confirmPublishWorkOrder(row)}
onClick={() => handlePublishClick(row)}
>
发布并核销
</Button>
@@ -1301,6 +1412,15 @@ export default function WorkOrdersPanel() {
扣押金
</Button>
) : null}
{(row.material as any)?.consignment ? (
<Button
icon={<RollbackOutlined />}
danger
onClick={() => openRevokeConsignmentModal(row)}
>
撤单/撤销寄售
</Button>
) : null}
{row.status === 'accepted' ? (
<Button
icon={<WarningOutlined />}
@@ -2618,6 +2738,249 @@ export default function WorkOrdersPanel() {
</Form>
) : null}
</Modal>
{/* 内部寄售库存匹配拦截弹窗 */}
<Modal
title={
<Space>
<span>内部寄售库存匹配提醒</span>
<Tag color="purple">和平精英内部仓</Tag>
</Space>
}
open={consignmentInterceptModal.open}
width={760}
onCancel={() => {
setConsignmentInterceptModal({
open: false,
order: null,
matchData: null,
submitting: false,
})
}}
footer={[
<Button
key="ignore-and-publish"
onClick={() => {
const currentOrder = consignmentInterceptModal.order
setConsignmentInterceptModal({
open: false,
order: null,
matchData: null,
submitting: false,
})
if (currentOrder) confirmPublishWorkOrder(currentOrder)
}}
>
忽略内部库存,继续发布到打手抢单大厅
</Button>,
<Button
key="close"
onClick={() => {
setConsignmentInterceptModal({
open: false,
order: null,
matchData: null,
submitting: false,
})
}}
>
取消
</Button>,
]}
>
<Alert
showIcon
type="success"
message="检测到内部寄售仓库中有可直接匹配履约的在库物品!"
description={
<div>
<div>
当前工单原定接单赏金:
<strong>
¥{Number(consignmentInterceptModal.order?.rewardAmount || 0).toFixed(2)}
</strong>
</div>
<div style={{ color: '#389e0d', marginTop: 4 }}>
若使用内部寄售库存履约,<strong>无需向外部打手支付佣金(打手支出 ¥0.00)</strong>
,系统将自动核销工单并归档!
</div>
</div>
}
style={{ marginBottom: 16 }}
/>
<div style={{ marginBottom: 12 }}>
<Space size={8}>
<Typography.Text strong>匹配结果:</Typography.Text>
<Tag color="green">
全部匹配: {consignmentInterceptModal.matchData?.exactCount || 0} 件
</Tag>
<Tag color="gold">
部分匹配(高配充低配): {consignmentInterceptModal.matchData?.partialCount || 0} 件
</Tag>
</Space>
</div>
<div
style={{
maxHeight: 400,
overflowY: 'auto',
display: 'flex',
flexDirection: 'column',
gap: 10,
}}
>
{(consignmentInterceptModal.matchData?.matches || []).map((m) => {
const inv = m.inventory
const isExact = m.matchType === 'exact'
return (
<Card
key={inv.id}
size="small"
style={{
borderLeft: isExact ? '4px solid #52c41a' : '4px solid #faad14',
backgroundColor: isExact ? '#f6ffed' : '#fffbe6',
}}
>
<div
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}
>
<div style={{ flex: 1 }}>
<Space size={8} wrap>
<strong style={{ fontSize: 15 }}>{inv.itemName}</strong>
{isExact ? (
<Tag color="green">【全部匹配】</Tag>
) : (
<Tag color="gold">【部分匹配 / 高配充低配】</Tag>
)}
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{inv.inventoryNo}
</Typography.Text>
</Space>
<div style={{ marginTop: 4 }}>
<Space size={4} wrap>
<Tag color={inv.system === '苹果' ? 'geekblue' : 'green'}>{inv.system}</Tag>
<Tag color="cyan">{inv.serverZone}</Tag>
<Tag color="orange">{inv.series}</Tag>
<Tag color="gold">{inv.diamondLevel} 钻</Tag>
{inv.brillianceCount > 0 ? (
<Tag color="magenta">{inv.brillianceCount} 璀璨</Tag>
) : null}
</Space>
</div>
<div style={{ marginTop: 6, fontSize: 12, color: '#595959' }}>
<Space size={16} wrap>
<span>
内部收购成本:{' '}
<strong style={{ color: '#cf1322' }}>
¥{(inv.costPriceFen / 100).toFixed(2)}
</strong>
</span>
<span>收购人: {inv.buyerName || '-'}</span>
{inv.shopName ? <span>店铺: {inv.shopName}</span> : null}
{inv.inboundBatch ? <span>批次: {inv.inboundBatch}</span> : null}
</Space>
</div>
<div style={{ marginTop: 4, fontSize: 11, color: '#8c8c8c' }}>
匹配分析: {m.matchReasons.join(' · ')}
</div>
</div>
<div style={{ marginLeft: 16 }}>
<Button
type="primary"
loading={consignmentInterceptModal.submitting}
onClick={() => handleFulfillWithConsignment(inv.id)}
>
使用此库存直接履约
</Button>
</div>
</div>
</Card>
)
})}
</div>
</Modal>
{/* 撤单 / 撤销寄售履约弹窗 */}
<Modal
title={`撤单 / 撤销寄售履约 · ${getDisplayOrderNo(revokeConsignmentModal.order)}`}
open={revokeConsignmentModal.open}
onCancel={() => {
setRevokeConsignmentModal({
open: false,
order: null,
action: 'reopen',
reason: '',
submitting: false,
})
}}
onOk={handleRevokeConsignmentSubmit}
confirmLoading={revokeConsignmentModal.submitting}
okText="确认撤单"
okType="danger"
destroyOnClose
>
<Alert
showIcon
type="warning"
message="撤销后,关联寄售物品将退单回库并重新进入待匹配池"
description="适用场景:买家退款、客服误选库存、账号密码错误失效或规格不符等情况。"
style={{ marginBottom: 16 }}
/>
<Form layout="vertical">
<Form.Item label="处理流转方式" required>
<Radio.Group
value={revokeConsignmentModal.action}
onChange={(e) =>
setRevokeConsignmentModal((prev) => ({ ...prev, action: e.target.value }))
}
>
<Space direction="vertical">
<Radio value="reopen">
<div>
<strong>退单回库并恢复工单为未指派 (推荐)</strong>
<div style={{ fontSize: 12, color: '#8c8c8c' }}>
物品重新变为【在库】待匹配,工单恢复为【未指派】,可重新选择其他库存或发布到打手大厅。
</div>
</div>
</Radio>
<Radio value="cancel_order">
<div>
<strong>退单回库并取消整单</strong>
<div style={{ fontSize: 12, color: '#8c8c8c' }}>
买家退款时使用。物品退回复库,工单状态变更为【已取消】。
</div>
</div>
</Radio>
<Radio value="archive_inventory">
<div>
<strong>报废该物品并恢复工单</strong>
<div style={{ fontSize: 12, color: '#8c8c8c' }}>
如果该寄售物品无法登录/被找回已彻底失效,将该物品标记为【作废】,工单恢复为【未指派】重选。
</div>
</div>
</Radio>
</Space>
</Radio.Group>
</Form.Item>
<Form.Item label="撤单原因说明" required>
<Input.TextArea
rows={3}
value={revokeConsignmentModal.reason}
onChange={(e) =>
setRevokeConsignmentModal((prev) => ({ ...prev, reason: e.target.value }))
}
placeholder="请输入撤单详细原因,如:买家申请退款、选错账号规格、账号密码错误等..."
/>
</Form.Item>
</Form>
</Modal>
</section>
)
}