feat(consignment): 新增和平精英寄卖库存管理、定向收集审核与工单智能匹配履约
This commit is contained in:
@@ -38,6 +38,7 @@ const AdminDevMockPage = lazy(() => import('@/pages/admin/AdminDevMockPage'))
|
||||
const AdminWorkerPlatformPage = lazy(() => import('@/pages/admin/AdminWorkerPlatformPage'))
|
||||
const ClaimPage = lazy(() => import('@/pages/claim/ClaimPage'))
|
||||
const CollectPage = lazy(() => import('@/pages/collect/CollectPage'))
|
||||
const ConsignmentCollectPage = lazy(() => import('@/pages/collect/ConsignmentCollectPage'))
|
||||
const NotFoundPage = lazy(() => import('@/pages/NotFoundPage'))
|
||||
const WorkerHallPage = lazy(() => import('@/pages/worker/WorkerHallPage'))
|
||||
const WorkerLoginPage = lazy(() => import('@/pages/worker/WorkerLoginPage'))
|
||||
@@ -133,6 +134,7 @@ function AdminApplication() {
|
||||
</Route>
|
||||
<Route path="/claim/:token" element={<ClaimPage />} />
|
||||
<Route path="/collect" element={<CollectPage />} />
|
||||
<Route path="/consignment-collect/:token" element={<ConsignmentCollectPage />} />
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
|
||||
@@ -581,12 +581,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
}
|
||||
|
||||
type NotificationAlertKind =
|
||||
| 'withdraw'
|
||||
| 'recharge'
|
||||
| 'acceptance'
|
||||
| 'material'
|
||||
| 'reminder'
|
||||
| 'default'
|
||||
'withdraw' | 'recharge' | 'acceptance' | 'material' | 'reminder' | 'default'
|
||||
|
||||
function playNotificationSound(kind: NotificationAlertKind) {
|
||||
try {
|
||||
|
||||
@@ -178,6 +178,14 @@ export function apiPut<T>(url: string, data?: unknown) {
|
||||
})
|
||||
}
|
||||
|
||||
export function apiPatch<T>(url: string, data?: unknown) {
|
||||
return request<T>({
|
||||
method: 'PATCH',
|
||||
url,
|
||||
data: data as Record<string, unknown>,
|
||||
})
|
||||
}
|
||||
|
||||
export function apiPostForm<T>(url: string, data: FormData) {
|
||||
return request<T>({
|
||||
method: 'POST',
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,453 @@
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
CloudUploadOutlined,
|
||||
LockOutlined,
|
||||
TrophyOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import {
|
||||
Alert,
|
||||
App,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Descriptions,
|
||||
Form,
|
||||
Image,
|
||||
Input,
|
||||
InputNumber,
|
||||
Radio,
|
||||
Result,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Spin,
|
||||
Tag,
|
||||
Typography,
|
||||
Upload,
|
||||
} from 'antd'
|
||||
import type { UploadFile } from 'antd'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useParams } from 'react-router'
|
||||
|
||||
import {
|
||||
fetchPublicConsignmentLink,
|
||||
submitPublicConsignment,
|
||||
uploadConsignmentImage,
|
||||
} from '@/services/consignment-public'
|
||||
import type { ConsignmentCollectLink, ConsignmentSubmission } from '@/types/consignment'
|
||||
|
||||
const { Title, Paragraph, Text } = Typography
|
||||
|
||||
export default function ConsignmentCollectPage() {
|
||||
const { message } = App.useApp()
|
||||
const { token } = useParams<{ token: string }>()
|
||||
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [errorMsg, setErrorMsg] = useState('')
|
||||
const [linkInfo, setLinkInfo] = useState<ConsignmentCollectLink | null>(null)
|
||||
const [submittedResult, setSubmittedResult] = useState<ConsignmentSubmission | null>(null)
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([])
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
const [form] = Form.useForm()
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
setErrorMsg('缺少收集凭证')
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
fetchPublicConsignmentLink(token)
|
||||
.then((res) => {
|
||||
const data = res.data
|
||||
setLinkInfo(data)
|
||||
const briStr = data.brillianceCount > 0 ? `${data.brillianceCount}璀璨` : ''
|
||||
form.setFieldsValue({
|
||||
itemName:
|
||||
data.title ||
|
||||
`${data.system}${data.serverZone}${data.series}${data.diamondLevel}钻${briStr}`,
|
||||
system: data.system || '苹果',
|
||||
serverZone: data.serverZone || '微信区',
|
||||
series: data.series || '暗潮',
|
||||
diamondLevel: data.diamondLevel || 1,
|
||||
brillianceCount: data.brillianceCount || 0,
|
||||
askingPrice: data.presetPriceFen > 0 ? data.presetPriceFen / 100 : undefined,
|
||||
loginMethod: 'wechat_qrcode',
|
||||
})
|
||||
})
|
||||
.catch((err: any) => {
|
||||
setErrorMsg(err.message || '获取收集信息失败或链接已失效')
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}, [token, form])
|
||||
|
||||
async function handleSubmit(values: any) {
|
||||
if (!token) return
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const askingPriceFen = Math.round(Number(values.askingPrice || 0) * 100)
|
||||
const screenshots = fileList
|
||||
.filter((f) => f.status === 'done' && (f.response?.url || f.url))
|
||||
.map((f) => f.response?.url || f.url)
|
||||
|
||||
const payload = {
|
||||
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,
|
||||
contactInfo: values.sellerContact,
|
||||
sellerPhone: values.sellerPhone,
|
||||
sellerGameId: values.sellerGameId,
|
||||
sellerPaymentInfo: values.sellerPaymentInfo,
|
||||
askingPriceFen,
|
||||
screenshotsJson: screenshots,
|
||||
contentJson: {
|
||||
loginMethod: values.loginMethod,
|
||||
account: values.account,
|
||||
password: values.password,
|
||||
authNote: values.authNote,
|
||||
sellerNote: values.sellerNote,
|
||||
},
|
||||
}
|
||||
|
||||
const res = await submitPublicConsignment(token, payload)
|
||||
setSubmittedResult(res.data)
|
||||
message.success('寄卖申请提交成功!')
|
||||
} catch (err: any) {
|
||||
message.error(err.message || '提交失败,请重试')
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ padding: '60px 20px', textAlign: 'center' }}>
|
||||
<Spin size="large" />
|
||||
<div style={{ marginTop: 16, color: '#8c8c8c' }}>正在加载寄卖登记信息...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (errorMsg) {
|
||||
return (
|
||||
<div style={{ maxWidth: 600, margin: '40px auto', padding: '0 16px' }}>
|
||||
<Result
|
||||
status="warning"
|
||||
title="链接无法访问"
|
||||
subTitle={errorMsg}
|
||||
extra={<Text type="secondary">如有疑问,请联系客服重新获取有效链接</Text>}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (submittedResult) {
|
||||
return (
|
||||
<div style={{ maxWidth: 600, margin: '40px auto', padding: '0 16px' }}>
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{ boxShadow: '0 4px 12px rgba(0,0,0,0.05)', borderRadius: 12 }}
|
||||
>
|
||||
<Result
|
||||
status="success"
|
||||
title="寄卖登记提交成功!"
|
||||
subTitle={
|
||||
<div>
|
||||
<div>
|
||||
您的登记单号:
|
||||
<Text strong copyable>
|
||||
{submittedResult.submissionNo}
|
||||
</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
客服将在核实账号及物品品质后,通过您填写的联系方式与您确认交接并打款。
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
extra={[
|
||||
<Button key="back" type="primary" onClick={() => window.location.reload()}>
|
||||
返回重新填写
|
||||
</Button>,
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
minHeight: '100vh',
|
||||
background: '#f5f7fa',
|
||||
padding: '24px 16px 60px',
|
||||
}}
|
||||
>
|
||||
<div style={{ maxWidth: 680, margin: '0 auto' }}>
|
||||
{/* 顶部标题区 */}
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
marginBottom: 16,
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.04)',
|
||||
background: 'linear-gradient(135deg, #1890ff 0%, #096dd9 100%)',
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
<Space orientation="vertical" size={6} style={{ width: '100%' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<TrophyOutlined style={{ fontSize: 24, color: '#ffd666' }} />
|
||||
<Title level={4} style={{ color: '#fff', margin: 0 }}>
|
||||
{linkInfo?.title || '和平精英 装备/账号 寄卖登记'}
|
||||
</Title>
|
||||
</div>
|
||||
<Paragraph style={{ color: 'rgba(255,255,255,0.85)', margin: 0, fontSize: 13 }}>
|
||||
平台保障 · 极速审核 · 验货即打款 · 请如实填写以下信息
|
||||
</Paragraph>
|
||||
|
||||
{linkInfo ? (
|
||||
<div style={{ marginTop: 8, display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
||||
{linkInfo.system ? <Tag color="blue">{linkInfo.system}</Tag> : null}
|
||||
{linkInfo.serverZone ? <Tag color="cyan">{linkInfo.serverZone}</Tag> : null}
|
||||
{linkInfo.series ? <Tag color="orange">{linkInfo.series}</Tag> : null}
|
||||
{linkInfo.diamondLevel ? <Tag color="gold">{linkInfo.diamondLevel} 钻</Tag> : null}
|
||||
{linkInfo.brillianceCount > 0 ? (
|
||||
<Tag color="magenta">{linkInfo.brillianceCount} 璀璨</Tag>
|
||||
) : null}
|
||||
{linkInfo.presetPriceFen > 0 ? (
|
||||
<Tag color="volcano" style={{ fontWeight: 600 }}>
|
||||
指导收购价 ¥{(linkInfo.presetPriceFen / 100).toFixed(0)}
|
||||
</Tag>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
{/* 表单填写区 */}
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.04)',
|
||||
}}
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
||||
<Title level={5} style={{ marginBottom: 16 }}>
|
||||
1. 物品与规格确认
|
||||
</Title>
|
||||
|
||||
<Row gutter={12}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="system" label="操作系统" rules={[{ required: true }]}>
|
||||
<Select
|
||||
options={[
|
||||
{ label: '苹果 (iOS)', value: '苹果' },
|
||||
{ label: '安卓 (Android)', value: '安卓' },
|
||||
{ label: '通用/双端', value: '通用' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="serverZone" label="游戏区服" rules={[{ required: true }]}>
|
||||
<Select
|
||||
options={[
|
||||
{ label: '微信区', value: '微信区' },
|
||||
{ label: 'QQ区', value: 'QQ区' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={12}>
|
||||
<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>
|
||||
<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>
|
||||
</Row>
|
||||
|
||||
<Form.Item
|
||||
name="itemName"
|
||||
label="物品/账号名称"
|
||||
rules={[{ required: true, message: '请输入物品名称' }]}
|
||||
>
|
||||
<Input placeholder="如:苹果微信暗潮1钻" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="askingPrice"
|
||||
label="期望转让/收购价格 (元)"
|
||||
rules={[{ required: true, message: '请填写出价' }]}
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
step={1}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="请输入您的心理价位 (元)"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Title level={5} style={{ margin: '24px 0 16px' }}>
|
||||
2. 卖家联系与收款方式
|
||||
</Title>
|
||||
|
||||
<Row gutter={12}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="sellerContact"
|
||||
label="您的微信号/QQ号"
|
||||
rules={[{ required: true, message: '请填写联系微信号或QQ' }]}
|
||||
>
|
||||
<Input placeholder="方便客服核实联系" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="sellerPhone" label="联系电话">
|
||||
<Input placeholder="手机号 (选填)" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item
|
||||
name="sellerGameId"
|
||||
label="游戏内角色名 / 角色数字ID"
|
||||
rules={[{ required: true, message: '请填写游戏内角色名称或ID' }]}
|
||||
>
|
||||
<Input placeholder="如:和平精英-玩家张三 (用于核实号主)" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="sellerPaymentInfo"
|
||||
label="收款账户 (支付宝账号及姓名 或 微信收款卡号)"
|
||||
rules={[{ required: true, message: '请填写收款信息' }]}
|
||||
>
|
||||
<Input placeholder="如:支付宝 13800000000 (张三) / 工商银行 6222... (张三)" />
|
||||
</Form.Item>
|
||||
|
||||
<Title level={5} style={{ margin: '24px 0 16px' }}>
|
||||
3. 凭据截图与账号交接 (仅内部审核验号可见)
|
||||
</Title>
|
||||
|
||||
<Alert
|
||||
showIcon
|
||||
icon={<LockOutlined />}
|
||||
type="info"
|
||||
message="信息安全保障"
|
||||
description="您填写的账号凭据与截图受平台严格加密保护,仅由官方指定客服在验货核实时可见,绝不泄露给任何无关第三方。"
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
<Form.Item label="游戏内物品/品质/仓库截图证明 (最多4张)">
|
||||
<Upload
|
||||
listType="picture-card"
|
||||
fileList={fileList}
|
||||
maxCount={4}
|
||||
customRequest={async ({ file, onSuccess, onError }) => {
|
||||
try {
|
||||
const res = await uploadConsignmentImage(file as File)
|
||||
onSuccess?.(res)
|
||||
} catch (err: any) {
|
||||
onError?.(err)
|
||||
message.error(err.message || '上传失败')
|
||||
}
|
||||
}}
|
||||
onChange={({ fileList: nextList }) => setFileList(nextList)}
|
||||
>
|
||||
{fileList.length < 4 ? (
|
||||
<div>
|
||||
<CloudUploadOutlined style={{ fontSize: 20, color: '#1890ff' }} />
|
||||
<div style={{ marginTop: 6, fontSize: 12 }}>上传截图</div>
|
||||
</div>
|
||||
) : null}
|
||||
</Upload>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="loginMethod" label="交付/上号方式">
|
||||
<Radio.Group>
|
||||
<Radio value="wechat_qrcode">微信扫码登录</Radio>
|
||||
<Radio value="qq_qrcode">QQ扫码登录</Radio>
|
||||
<Radio value="account_pwd">账号密码交付</Radio>
|
||||
<Radio value="in_game_transfer">游戏内转赠/投喂</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Row gutter={12}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="account" label="游戏账号/上号手机号">
|
||||
<Input placeholder="如账号密码交付请填写" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="password" label="上号密码">
|
||||
<Input.Password placeholder="如需密码登录请填写" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item name="authNote" label="上号与二次验证说明">
|
||||
<Input placeholder="如:需发微信确认登录、备用安全码、在线时间段等" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="sellerNote" label="其他留言与补充">
|
||||
<Input.TextArea rows={2} placeholder="如有其他装备搭配或说明可在此补充" />
|
||||
</Form.Item>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
size="large"
|
||||
block
|
||||
loading={submitting}
|
||||
style={{
|
||||
marginTop: 16,
|
||||
height: 48,
|
||||
fontSize: 16,
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
提交寄卖申请并登记
|
||||
</Button>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -41,11 +41,7 @@ type AfterSalesResponseFormValues = {
|
||||
}
|
||||
|
||||
type StatusFilter =
|
||||
| 'all'
|
||||
| 'awaiting_worker_response'
|
||||
| 'under_review'
|
||||
| 'recovery_pending'
|
||||
| 'closed'
|
||||
'all' | 'awaiting_worker_response' | 'under_review' | 'recovery_pending' | 'closed'
|
||||
|
||||
/** 待办优先:待说明 > 处理中 > 追缴中 > 已关闭,同状态按发起时间倒序。 */
|
||||
const STATUS_WEIGHT: Record<string, number> = {
|
||||
|
||||
@@ -306,17 +306,15 @@ export function getUniqueAcceptanceFiles(source?: WorkerAcceptance | null): Uplo
|
||||
if (!source) return []
|
||||
const files = [
|
||||
...(Array.isArray(source.files) ? source.files : []),
|
||||
...(Array.isArray(source.imageUrls) ? source.imageUrls : []).map(
|
||||
(url): UploadedFile => ({
|
||||
objectKey: '',
|
||||
url: String(url || ''),
|
||||
thumbnailUrl: String(url || ''),
|
||||
mediumUrl: String(url || ''),
|
||||
filename: '图片',
|
||||
contentType: '',
|
||||
size: 0,
|
||||
}),
|
||||
),
|
||||
...(Array.isArray(source.imageUrls) ? source.imageUrls : []).map((url): UploadedFile => ({
|
||||
objectKey: '',
|
||||
url: String(url || ''),
|
||||
thumbnailUrl: String(url || ''),
|
||||
mediumUrl: String(url || ''),
|
||||
filename: '图片',
|
||||
contentType: '',
|
||||
size: 0,
|
||||
})),
|
||||
]
|
||||
const identities = new Set<string>()
|
||||
return files.filter((file) => {
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import { apiGet, apiPatch, apiPost, apiPut } from '@/lib/http'
|
||||
import type {
|
||||
ConsignmentCollectLink,
|
||||
ConsignmentInventory,
|
||||
ConsignmentStatsSummary,
|
||||
ConsignmentSubmission,
|
||||
WorkOrderInventoryMatchCheck,
|
||||
} from '@/types/consignment'
|
||||
|
||||
// --- 库存管理 ---
|
||||
|
||||
export function fetchAdminConsignmentInventories(params?: Record<string, unknown>) {
|
||||
return apiGet<{ items: ConsignmentInventory[]; total: number }>(
|
||||
'/api/v1/admin/worker-platform/consignment/inventories',
|
||||
params,
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminConsignmentStats() {
|
||||
return apiGet<ConsignmentStatsSummary>('/api/v1/admin/worker-platform/consignment/stats')
|
||||
}
|
||||
|
||||
export function getAdminConsignmentInventory(id: number | string) {
|
||||
return apiGet<ConsignmentInventory>(`/api/v1/admin/worker-platform/consignment/inventories/${id}`)
|
||||
}
|
||||
|
||||
export function createAdminConsignmentInventory(payload: Record<string, unknown>) {
|
||||
return apiPost<ConsignmentInventory>(
|
||||
'/api/v1/admin/worker-platform/consignment/inventories',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function updateAdminConsignmentInventory(
|
||||
id: number | string,
|
||||
payload: Record<string, unknown>,
|
||||
) {
|
||||
return apiPut<ConsignmentInventory>(
|
||||
`/api/v1/admin/worker-platform/consignment/inventories/${id}`,
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function toggleAdminConsignmentMatching(id: number | string, allowMatching: boolean) {
|
||||
return apiPatch<ConsignmentInventory>(
|
||||
`/api/v1/admin/worker-platform/consignment/inventories/${id}/matching`,
|
||||
{ allowMatching },
|
||||
)
|
||||
}
|
||||
|
||||
export function archiveAdminConsignmentInventory(id: number | string) {
|
||||
return apiPost<ConsignmentInventory>(
|
||||
`/api/v1/admin/worker-platform/consignment/inventories/${id}/archive`,
|
||||
{},
|
||||
)
|
||||
}
|
||||
|
||||
export function returnAdminConsignmentInventory(id: number | string, payload: { reason?: string }) {
|
||||
return apiPost<ConsignmentInventory>(
|
||||
`/api/v1/admin/worker-platform/consignment/inventories/${id}/return`,
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
// --- 收集链接 ---
|
||||
|
||||
export function fetchAdminConsignmentLinks() {
|
||||
return apiGet<{ items: ConsignmentCollectLink[] }>(
|
||||
'/api/v1/admin/worker-platform/consignment/links',
|
||||
)
|
||||
}
|
||||
|
||||
export function createAdminConsignmentLink(payload: Record<string, unknown>) {
|
||||
return apiPost<ConsignmentCollectLink>('/api/v1/admin/worker-platform/consignment/links', payload)
|
||||
}
|
||||
|
||||
export function updateAdminConsignmentLink(id: number | string, payload: Record<string, unknown>) {
|
||||
return apiPut<ConsignmentCollectLink>(
|
||||
`/api/v1/admin/worker-platform/consignment/links/${id}`,
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
// --- 提报审核 ---
|
||||
|
||||
export function fetchAdminConsignmentSubmissions(params?: Record<string, unknown>) {
|
||||
return apiGet<{ items: ConsignmentSubmission[]; total: number }>(
|
||||
'/api/v1/admin/worker-platform/consignment/submissions',
|
||||
params,
|
||||
)
|
||||
}
|
||||
|
||||
export function getAdminConsignmentSubmission(id: number | string) {
|
||||
return apiGet<ConsignmentSubmission>(
|
||||
`/api/v1/admin/worker-platform/consignment/submissions/${id}`,
|
||||
)
|
||||
}
|
||||
|
||||
export function approveAdminConsignmentSubmission(
|
||||
id: number | string,
|
||||
payload: Record<string, unknown>,
|
||||
) {
|
||||
return apiPost<{ submission: ConsignmentSubmission; inventory: ConsignmentInventory }>(
|
||||
`/api/v1/admin/worker-platform/consignment/submissions/${id}/approve`,
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function rejectAdminConsignmentSubmission(
|
||||
id: number | string,
|
||||
payload: { reviewNote?: string },
|
||||
) {
|
||||
return apiPost<ConsignmentSubmission>(
|
||||
`/api/v1/admin/worker-platform/consignment/submissions/${id}/reject`,
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
// --- 匹配与履约 ---
|
||||
|
||||
export function checkWorkOrderConsignmentMatches(workOrderId: number | string) {
|
||||
return apiGet<WorkOrderInventoryMatchCheck>(
|
||||
`/api/v1/admin/worker-platform/consignment/matches/${workOrderId}`,
|
||||
)
|
||||
}
|
||||
|
||||
export function fulfillWorkOrderWithConsignment(payload: {
|
||||
workOrderId: number | string
|
||||
inventoryId: number | string
|
||||
}) {
|
||||
return apiPost<{ workOrder: unknown; inventory: ConsignmentInventory }>(
|
||||
'/api/v1/admin/worker-platform/consignment/fulfill',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function revokeWorkOrderConsignmentFulfillment(payload: {
|
||||
workOrderId: number | string
|
||||
action?: 'reopen' | 'cancel_order' | 'archive_inventory'
|
||||
reason?: string
|
||||
}) {
|
||||
return apiPost<{ workOrder: unknown; inventory?: ConsignmentInventory | null }>(
|
||||
'/api/v1/admin/worker-platform/consignment/revoke',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { apiGet, apiPost } from '@/lib/http'
|
||||
import type { ConsignmentCollectLink, ConsignmentSubmission } from '@/types/consignment'
|
||||
|
||||
export function fetchPublicConsignmentLink(token: string) {
|
||||
return apiGet<ConsignmentCollectLink>(`/api/v1/consignment/${encodeURIComponent(token)}`)
|
||||
}
|
||||
|
||||
export function submitPublicConsignment(token: string, payload: Record<string, unknown>) {
|
||||
return apiPost<ConsignmentSubmission>(
|
||||
`/api/v1/consignment/${encodeURIComponent(token)}/submit`,
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export async function uploadConsignmentImage(
|
||||
file: File,
|
||||
): Promise<{ url: string; assetId?: string }> {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
formData.append('scene', 'consignment-material')
|
||||
|
||||
const response = await fetch('/api/v1/consignment/upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errJson = await response.json().catch(() => ({}))
|
||||
throw new Error(errJson?.message || '图片上传失败')
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
return result?.data || result
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
export type AdminKuaishouIndustryAccessTokenStatus =
|
||||
| 'missing'
|
||||
| 'unknown'
|
||||
| 'expired'
|
||||
| 'expiring'
|
||||
| 'valid'
|
||||
'missing' | 'unknown' | 'expired' | 'expiring' | 'valid'
|
||||
|
||||
export interface AdminKuaishouIndustryShopConfig {
|
||||
enabled: boolean
|
||||
|
||||
@@ -193,11 +193,7 @@ export interface ClaimDetailData {
|
||||
tokenStatus: ClaimTokenStatus
|
||||
claimUrl: string
|
||||
flowType:
|
||||
| 'kuaishou_cloud'
|
||||
| 'kuaishou_ct_assisted'
|
||||
| 'kuaishou_feifei'
|
||||
| 'affiliate_dash'
|
||||
| (string & {})
|
||||
'kuaishou_cloud' | 'kuaishou_ct_assisted' | 'kuaishou_feifei' | 'affiliate_dash' | (string & {})
|
||||
claimIdentity: ClaimIdentityInfo | null
|
||||
task: ClaimTaskInfo
|
||||
order: ClaimOrderInfo
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
export type ConsignmentInventory = {
|
||||
id: number
|
||||
inventoryNo: string
|
||||
gameName: string
|
||||
categoryId: number | null
|
||||
categoryName?: string
|
||||
itemName: string
|
||||
system: string
|
||||
serverZone: string
|
||||
series: string
|
||||
diamondLevel: number
|
||||
brillianceCount: number
|
||||
costPriceFen: number
|
||||
buyerName: string
|
||||
shopName: string
|
||||
inboundBatch: string
|
||||
sellerContact: string
|
||||
sellerGameId: string
|
||||
sellerPaymentInfo: string
|
||||
sellerPhone: string
|
||||
credentialsJson: Record<string, unknown>
|
||||
screenshotsJson: string[]
|
||||
remark: string
|
||||
allowMatching: boolean
|
||||
status: 'in_stock' | 'consumed' | 'archived'
|
||||
allocationStatus: string
|
||||
boundWorkOrderId: number | null
|
||||
boundPlatformOrderId: string
|
||||
sourceType: 'manual' | 'collection_link' | 'recycle'
|
||||
sourceSubmissionId: number | null
|
||||
inboundAt: string
|
||||
consumedAt: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export type ConsignmentCollectLink = {
|
||||
id: number
|
||||
token: string
|
||||
title: string
|
||||
gameName: string
|
||||
categoryId: number | null
|
||||
categoryName?: string
|
||||
system: string
|
||||
serverZone: string
|
||||
series: string
|
||||
diamondLevel: number
|
||||
brillianceCount: number
|
||||
presetPriceFen: number
|
||||
inboundBatch: string
|
||||
buyerName: string
|
||||
templateJson: Record<string, unknown>
|
||||
isMultiUse: boolean
|
||||
expiresAt: string | null
|
||||
enabled: boolean
|
||||
createdBy: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
submissionCount?: number
|
||||
}
|
||||
|
||||
export type ConsignmentSubmission = {
|
||||
id: number
|
||||
submissionNo: string
|
||||
linkId: number | null
|
||||
linkTitle?: string
|
||||
gameName: string
|
||||
categoryId: number | null
|
||||
categoryName?: string
|
||||
itemName: string
|
||||
system: string
|
||||
serverZone: string
|
||||
series: string
|
||||
diamondLevel: number
|
||||
brillianceCount: number
|
||||
sellerGameId: string
|
||||
sellerPaymentInfo: string
|
||||
sellerPhone: string
|
||||
contactInfo: string
|
||||
contentJson: Record<string, unknown>
|
||||
screenshotsJson: string[]
|
||||
askingPriceFen: number
|
||||
status: 'pending_review' | 'approved' | 'rejected'
|
||||
reviewNote: string
|
||||
reviewerName: string
|
||||
reviewedAt: string | null
|
||||
resultInventoryId: number | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export type ConsignmentStatsSummary = {
|
||||
totalInStockCount: number
|
||||
totalInStockCostFen: number
|
||||
totalConsumedCount: number
|
||||
totalConsumedCostFen: number
|
||||
byBuyer: Array<{
|
||||
buyer_name: string
|
||||
count: number
|
||||
total_cost_fen: number
|
||||
consumed_count: number
|
||||
}>
|
||||
byShop: Array<{ shop_name: string; count: number }>
|
||||
byBatch: Array<{ inbound_batch: string; count: number; total_cost_fen: number }>
|
||||
bySeries: Array<{ series: string; diamond_level: number; count: number }>
|
||||
}
|
||||
|
||||
export type ConsignmentMatchResult = {
|
||||
inventory: ConsignmentInventory
|
||||
matchType: 'exact' | 'partial'
|
||||
matchScore: number
|
||||
matchReasons: string[]
|
||||
}
|
||||
|
||||
export type WorkOrderInventoryMatchCheck = {
|
||||
hasMatch: boolean
|
||||
exactCount: number
|
||||
partialCount: number
|
||||
matches: ConsignmentMatchResult[]
|
||||
}
|
||||
@@ -531,11 +531,7 @@ export type WorkOrderShare = {
|
||||
}
|
||||
|
||||
export type WorkOrderGiftPhase =
|
||||
| 'material_required'
|
||||
| 'material_submitted'
|
||||
| 'friend_countdown'
|
||||
| 'gift_ready'
|
||||
| string
|
||||
'material_required' | 'material_submitted' | 'friend_countdown' | 'gift_ready' | string
|
||||
|
||||
/** 好友赠送验收模式的打手本人资料 */
|
||||
export type BoosterMaterial = {
|
||||
|
||||
Reference in New Issue
Block a user