fix(consignment): 下线 H5 收集链接渠道并封闭寄卖履约状态机
下线定向收集链接(H5 填报)整条线,提报统一走接单平台打手端: - 删除前端 ConsignmentCollectPage、consignment-public 服务与 /consignment-collect/:token 路由 - 删除后端公开路由 routes/consignment.ts、consignment-public-service 与管理端 /consignment/links 三接口 - 迁移 079 删表 consignment_collect_links、删列 consignment_submissions.link_id, source_type 的 collection_link 归并为 worker_portal 并替换 CHECK 修复手工入库/编辑的凭据与截图静默丢失:后端统一按 credentialsJson ?? credentials、 screenshotsJson ?? screenshots 读取(入库/编辑/审核入库三个入口)。 按"已验收一律禁止撤销"封闭状态机: - 撤销寄售履约加状态白名单(in_progress|pending_acceptance|problem)+ 事务内 FOR UPDATE 锁单 + UPDATE 带 status 条件(0 行 409)+ 原因必填 + 清空指派/时限/验收草稿痕迹 - 库存改状态一律带条件:撤销要求 bound_work_order_id 命中本工单;作废/回库要求未被 进行中工单占用(被挡时提示具体工单号);079 加部分唯一索引并清理历史重复绑定 - 工单终止统一解绑回库:打手撤单/取消指派/退回大厅/撤单/打手退单申请/超时处置接入 releaseConsignmentInventoryBindingForWorkOrderWithClient(售后不改工单状态,无需接入) - 回库路径保留运营的 allow_matching 开关设置(不再强制开启),前端文案同步更正 顺带修掉资金与归属的同源缺陷: - 押金归属与重冻金额改取不可变的冻结核销行,审核改派不再"退给错人/向错人扣款" - 新增 assigned_worker_id 拆开"卖家"与"被指派履约人",占库不再覆盖卖家 - PUT /inventories/:id 禁止直接改 status;发布接口补 requireActiveWorker - 押金冻结/释放后补推钱包实时事件;迁移 079 给发布冻结加台账幂等唯一索引 验证:后端 401 测试/398 通过/3 skip(新增 5 例回归),前端 16 通过; 两侧 typecheck、lint:check、format:check 与 check:sql、build 全绿。 迁移 079 未经真实数据库执行(本环境无 Postgres),上库前请先 dry-run。
This commit is contained in:
@@ -35,7 +35,6 @@ 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'))
|
||||
@@ -131,7 +130,6 @@ 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>
|
||||
|
||||
@@ -305,7 +305,7 @@ export default function ConsignmentInventoryPanel() {
|
||||
if (!returnItem) return
|
||||
try {
|
||||
await returnAdminConsignmentInventory(returnItem.id, { reason: returnReason })
|
||||
message.success('物品已退单回库,重新进入待匹配池!')
|
||||
message.success('物品已退单回库,可在列表中查看匹配开关状态')
|
||||
setReturnItem(null)
|
||||
setReturnReason('')
|
||||
await refresh()
|
||||
@@ -339,9 +339,13 @@ export default function ConsignmentInventoryPanel() {
|
||||
<Text type="secondary" copyable style={{ fontSize: 12 }}>
|
||||
{record.inventoryNo}
|
||||
</Text>
|
||||
{record.sourceType === 'collection_link' ? (
|
||||
{record.sourceType === 'worker_portal' ? (
|
||||
<Tag color="purple" style={{ fontSize: 11, padding: '0 4px' }}>
|
||||
链接收集
|
||||
打手端提报
|
||||
</Tag>
|
||||
) : record.sourceType === 'recycle' ? (
|
||||
<Tag color="green" style={{ fontSize: 11, padding: '0 4px' }}>
|
||||
回收
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag color="blue" style={{ fontSize: 11, padding: '0 4px' }}>
|
||||
@@ -1249,7 +1253,7 @@ export default function ConsignmentInventoryPanel() {
|
||||
退回复库吗?
|
||||
</Text>
|
||||
<div style={{ marginTop: 8, color: '#8c8c8c', fontSize: 12 }}>
|
||||
回库后,物品状态将重新变为【在库】,允许匹配开关将自动开启,可重新匹配履约其他工单。
|
||||
回库后,物品状态将重新变为【在库】。允许匹配开关保持原设置:若该物品此前被手动暂停匹配,回库后需在列表里手动开启才会重新参与匹配。
|
||||
</div>
|
||||
</div>
|
||||
<Form layout="vertical">
|
||||
|
||||
@@ -141,7 +141,6 @@ export default function ConsignmentSubmissionsPanel() {
|
||||
function resolveSubmissionSource(record: ConsignmentSubmission) {
|
||||
const channel = (record.contentJson as { channel?: string } | undefined)?.channel
|
||||
if (channel === 'worker_portal' || record.workerId) return '打手端提报'
|
||||
if (record.linkTitle) return `旧收集链接: ${record.linkTitle}`
|
||||
return '其他渠道'
|
||||
}
|
||||
|
||||
|
||||
@@ -167,6 +167,18 @@ function isInternalConsignmentOrder(row: WorkOrder) {
|
||||
return Boolean(consignment.inventoryId) && !row.worker
|
||||
}
|
||||
|
||||
/**
|
||||
* 只有「未验收且未取消」的工单可以撤销寄售履约。
|
||||
* 已验收工单撤销会造成同一件现货二次出货、并重新冻结已退还的押金,后端一律拒绝。
|
||||
*/
|
||||
const CONSIGNMENT_REVOCABLE_ORDER_STATUSES = ['in_progress', 'pending_acceptance', 'problem']
|
||||
|
||||
function isConsignmentRevocableOrder(row: WorkOrder) {
|
||||
return (
|
||||
isInternalConsignmentOrder(row) && CONSIGNMENT_REVOCABLE_ORDER_STATUSES.includes(row.status)
|
||||
)
|
||||
}
|
||||
|
||||
export default function WorkOrdersPanel() {
|
||||
const { message, modal } = App.useApp()
|
||||
const navigate = useNavigate()
|
||||
@@ -216,6 +228,9 @@ export default function WorkOrdersPanel() {
|
||||
const canDeductDeposit = sessionPermissions
|
||||
? sessionPermissions.includes('worker_order.deduct_deposit')
|
||||
: hasAdminPermission('worker_order.deduct_deposit')
|
||||
const canFulfillConsignment = sessionPermissions
|
||||
? sessionPermissions.includes('consignment.fulfill')
|
||||
: hasAdminPermission('consignment.fulfill')
|
||||
const canConfigureSharing = sessionPermissions
|
||||
? sessionPermissions.includes('worker_order.configure_share')
|
||||
: hasAdminPermission('worker_order.configure_share')
|
||||
@@ -760,6 +775,10 @@ export default function WorkOrdersPanel() {
|
||||
|
||||
async function handleRevokeConsignmentSubmit() {
|
||||
if (!revokeConsignmentModal.order) return
|
||||
if (!revokeConsignmentModal.reason.trim()) {
|
||||
message.warning('请填写撤单原因,便于后续对账与追溯')
|
||||
return
|
||||
}
|
||||
setRevokeConsignmentModal((prev) => ({ ...prev, submitting: true }))
|
||||
try {
|
||||
await revokeWorkOrderConsignmentFulfillment({
|
||||
@@ -1454,7 +1473,7 @@ export default function WorkOrdersPanel() {
|
||||
扣押金
|
||||
</Button>
|
||||
) : null}
|
||||
{isInternalConsignmentOrder(row) ? (
|
||||
{isConsignmentRevocableOrder(row) && canFulfillConsignment ? (
|
||||
<Button
|
||||
icon={<RollbackOutlined />}
|
||||
danger
|
||||
@@ -2997,8 +3016,8 @@ export default function WorkOrdersPanel() {
|
||||
<Alert
|
||||
showIcon
|
||||
type="warning"
|
||||
message="撤销后,关联寄售物品将退单回库并重新进入待匹配池"
|
||||
description="适用场景:买家退款、客服误选库存、账号密码错误失效或规格不符等情况。"
|
||||
message="撤销后,关联寄售物品将退单回库;物品的【允许匹配】开关保持原设置不变"
|
||||
description="只适用于【进行中 / 待验收 / 问题单】。已验收订单无法撤销(现货已交付、押金已结清),买家退款或纠纷请走售后流程。"
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,577 +0,0 @@
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
CloudUploadOutlined,
|
||||
DollarOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
TrophyOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import {
|
||||
Alert,
|
||||
App,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Descriptions,
|
||||
Form,
|
||||
Image,
|
||||
Input,
|
||||
InputNumber,
|
||||
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 [depositTouched, setDepositTouched] = useState(false)
|
||||
|
||||
const [form] = Form.useForm()
|
||||
|
||||
function updateAutoItemName(changedValues?: Record<string, unknown>) {
|
||||
const sys = form.getFieldValue('system') || '苹果'
|
||||
const zone = form.getFieldValue('serverZone') || '微信区'
|
||||
const ser = form.getFieldValue('series') || '暗潮'
|
||||
const dia = form.getFieldValue('diamondLevel') || 1
|
||||
const bri = form.getFieldValue('brillianceCount') || 0
|
||||
const briStr = bri > 0 ? `${bri}璀璨` : ''
|
||||
const next: Record<string, unknown> = {
|
||||
itemName: `${sys}${zone}${ser}${dia}钻${briStr} 苏尔南`,
|
||||
}
|
||||
|
||||
if (changedValues && Object.prototype.hasOwnProperty.call(changedValues, 'depositAmount')) {
|
||||
setDepositTouched(true)
|
||||
}
|
||||
|
||||
if (
|
||||
changedValues &&
|
||||
Object.prototype.hasOwnProperty.call(changedValues, 'askingPrice') &&
|
||||
!depositTouched &&
|
||||
!Object.prototype.hasOwnProperty.call(changedValues, 'depositAmount')
|
||||
) {
|
||||
const price = Number(changedValues.askingPrice || 0)
|
||||
if (price > 0) next.depositAmount = price
|
||||
}
|
||||
|
||||
form.setFieldsValue(next)
|
||||
}
|
||||
|
||||
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 || 1}钻${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,
|
||||
depositAmount: data.presetPriceFen > 0 ? data.presetPriceFen / 100 : 100,
|
||||
})
|
||||
setDepositTouched(false)
|
||||
})
|
||||
.catch((err: any) => {
|
||||
setErrorMsg(err.message || '获取收集信息失败或链接已失效')
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}, [token, form])
|
||||
|
||||
async function handleSubmit(values: any) {
|
||||
if (!token) return
|
||||
|
||||
const screenshots = fileList
|
||||
.filter((f) => f.status === 'done' && (f.response?.url || f.url))
|
||||
.map((f) => f.response?.url || f.url)
|
||||
|
||||
if (screenshots.length === 0) {
|
||||
message.error('请上传游戏主页截图以供核验!')
|
||||
return
|
||||
}
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const askingPriceFen = Math.round(Number(values.askingPrice || 0) * 100)
|
||||
const depositAmountFen = Math.round(
|
||||
Number(values.depositAmount || values.askingPrice || 100) * 100,
|
||||
)
|
||||
|
||||
if (askingPriceFen < 100) {
|
||||
message.error('寄售价格不能少于1元')
|
||||
return
|
||||
}
|
||||
if (depositAmountFen < 100) {
|
||||
message.error('履约押金不能少于1元')
|
||||
return
|
||||
}
|
||||
|
||||
const payload = {
|
||||
itemName:
|
||||
values.itemName ||
|
||||
`${values.system}${values.serverZone}${values.series}${values.diamondLevel}钻苏尔南`,
|
||||
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.characterName} (${values.characterId})`,
|
||||
sellerPaymentInfo: '走接单平台钱包结算',
|
||||
askingPriceFen,
|
||||
depositAmountFen,
|
||||
screenshotsJson: screenshots,
|
||||
contentJson: {
|
||||
characterName: values.characterName,
|
||||
characterId: values.characterId,
|
||||
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: '80px 20px', textAlign: 'center' }}>
|
||||
<Spin size="large" />
|
||||
<div style={{ marginTop: 16, color: '#8c8c8c' }}>正在加载苏尔南手枪寄卖登记信息...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (errorMsg) {
|
||||
return (
|
||||
<div style={{ maxWidth: 600, margin: '60px auto', padding: '0 16px' }}>
|
||||
<Result
|
||||
status="warning"
|
||||
title="链接无法访问"
|
||||
subTitle={errorMsg}
|
||||
extra={<Text type="secondary">如有疑问,请联系客服重新获取有效链接</Text>}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (submittedResult) {
|
||||
return (
|
||||
<div style={{ maxWidth: 640, margin: '40px auto', padding: '0 16px' }}>
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{ boxShadow: '0 4px 16px rgba(0,0,0,0.06)', borderRadius: 16 }}
|
||||
>
|
||||
<Result
|
||||
status="success"
|
||||
title="苏尔南手枪寄售登记成功!"
|
||||
subTitle={
|
||||
<div>
|
||||
<div style={{ fontSize: 15, margin: '8px 0' }}>
|
||||
登记单号:
|
||||
<Text strong copyable style={{ color: '#1890ff' }}>
|
||||
{submittedResult.submissionNo}
|
||||
</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: 12, color: '#595959', lineHeight: 1.7 }}>
|
||||
您的寄售手枪已进入<strong>打手接单平台寄售池</strong>。
|
||||
<br />
|
||||
匹配到买家工单后,系统将通过接单平台指派您进行局内交付,并自动执行
|
||||
<strong>押金保障机制</strong>。交付验收后货款将直达您的账户。
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
extra={[
|
||||
<Button key="back" type="primary" onClick={() => window.location.reload()}>
|
||||
返回重新填写
|
||||
</Button>,
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
minHeight: '100vh',
|
||||
background: '#f0f2f5',
|
||||
padding: '24px 16px 60px',
|
||||
}}
|
||||
>
|
||||
<div style={{ maxWidth: 680, margin: '0 auto' }}>
|
||||
{/* 顶部标题区 */}
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{
|
||||
borderRadius: 16,
|
||||
marginBottom: 16,
|
||||
boxShadow: '0 4px 14px rgba(24, 144, 255, 0.15)',
|
||||
background: 'linear-gradient(135deg, #096dd9 0%, #1890ff 50%, #40a9ff 100%)',
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<TrophyOutlined style={{ fontSize: 26, color: '#ffd666' }} />
|
||||
<Title level={4} style={{ color: '#fff', margin: 0 }}>
|
||||
{linkInfo?.title || '苏尔南手枪 · 走打手接单平台寄卖'}
|
||||
</Title>
|
||||
</div>
|
||||
<Paragraph style={{ color: 'rgba(255,255,255,0.92)', margin: 0, fontSize: 13.5 }}>
|
||||
平台保障 · 走接单平台押金履约 · 验货即打款 · 杜绝私下跑单
|
||||
</Paragraph>
|
||||
|
||||
{linkInfo ? (
|
||||
<div style={{ marginTop: 6, 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>
|
||||
|
||||
{/* 押金保障卡片 */}
|
||||
<Alert
|
||||
showIcon
|
||||
icon={<SafetyCertificateOutlined style={{ fontSize: 20, color: '#52c41a' }} />}
|
||||
type="success"
|
||||
message="走打手接单平台 · 押金保障机制"
|
||||
description="寄售手枪统一通过接单平台派单交付。登记时可填写履约押金(默认跟寄售价一致);指派交枪时冻结押金,买家验货确认后解冻并结算货款。"
|
||||
style={{ marginBottom: 16, borderRadius: 12, border: '1px solid #b7eb8f' }}
|
||||
/>
|
||||
|
||||
{/* 表单填写区 */}
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{
|
||||
borderRadius: 16,
|
||||
boxShadow: '0 2px 10px rgba(0,0,0,0.04)',
|
||||
}}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{ depositAmount: 100 }}
|
||||
onFinish={handleSubmit}
|
||||
onValuesChange={(changed) => updateAutoItemName(changed)}
|
||||
>
|
||||
{/* 1. 枪系与规格 */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
marginBottom: 16,
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
paddingBottom: 8,
|
||||
}}
|
||||
>
|
||||
<TrophyOutlined style={{ color: '#1890ff', fontSize: 18 }} />
|
||||
<Title level={5} style={{ margin: 0 }}>
|
||||
1. 苏尔南手枪规格与价格
|
||||
</Title>
|
||||
</div>
|
||||
|
||||
<Row gutter={12}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="system"
|
||||
label="操作系统"
|
||||
rules={[{ required: true, message: '请选择操作系统' }]}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ label: '苹果 (iOS)', value: '苹果' },
|
||||
{ label: '安卓 (Android)', value: '安卓' },
|
||||
{ label: '通用/双端', value: '通用' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="serverZone"
|
||||
label="游戏区服"
|
||||
rules={[{ required: true, message: '请选择游戏区服' }]}
|
||||
>
|
||||
<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, message: '请选择枪系' }]}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ label: '暗潮', value: '暗潮' },
|
||||
{ label: '怒嚎', value: '怒嚎' },
|
||||
{ label: '血宴', value: '血宴' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item
|
||||
name="diamondLevel"
|
||||
label="钻石等级 (1-7)"
|
||||
rules={[{ required: true, message: '请选择钻石等级' }]}
|
||||
>
|
||||
<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 },
|
||||
{ label: '7 钻', value: 7 },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name="brillianceCount" label="璀璨数 (0-7)">
|
||||
<Select
|
||||
options={[
|
||||
{ label: '无璀璨 (0)', value: 0 },
|
||||
{ label: '1 璀璨', value: 1 },
|
||||
{ label: '2 璀璨', value: 2 },
|
||||
{ label: '3 璀璨', value: 3 },
|
||||
{ label: '4 璀璨', value: 4 },
|
||||
{ label: '5 璀璨', value: 5 },
|
||||
{ label: '6 璀璨', value: 6 },
|
||||
{ label: '7 璀璨', value: 7 },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={12}>
|
||||
<Col span={10}>
|
||||
<Form.Item
|
||||
name="itemName"
|
||||
label="物品名称"
|
||||
rules={[{ required: true, message: '请输入物品名称' }]}
|
||||
>
|
||||
<Input placeholder="如:苹果微信暗潮1钻 苏尔南" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={7}>
|
||||
<Form.Item
|
||||
name="askingPrice"
|
||||
label="寄售价格 (元)"
|
||||
rules={[{ required: true, message: '请填写寄售价格' }]}
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
step={1}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="如 80"
|
||||
addonBefore="¥"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={7}>
|
||||
<Form.Item
|
||||
name="depositAmount"
|
||||
label="履约押金 (元)"
|
||||
rules={[{ required: true, message: '请填写履约押金' }]}
|
||||
tooltip="指派打手交枪时冻结;验货通过后解冻。建议不低于寄售价。"
|
||||
extra="默认跟寄售价一致,也可手动调整"
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
step={10}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="如 100"
|
||||
addonBefore="¥"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 2. 卖家联系方式与游戏凭据 */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
margin: '24px 0 16px',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
paddingBottom: 8,
|
||||
}}
|
||||
>
|
||||
<SafetyCertificateOutlined style={{ color: '#52c41a', fontSize: 18 }} />
|
||||
<Title level={5} style={{ margin: 0 }}>
|
||||
2. 卖家联系方式与游戏凭据
|
||||
</Title>
|
||||
</div>
|
||||
|
||||
<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="联系电话 (用于打手平台关联)"
|
||||
rules={[{ required: true, message: '请填写联系电话' }]}
|
||||
tooltip="如您已是接单平台打手,请填写注册手机号,系统将自动识别并关联您的打手账户与押金额度"
|
||||
>
|
||||
<Input placeholder="手机号 (用于匹配打手)" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={12}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="characterName"
|
||||
label="游戏内角色名"
|
||||
rules={[{ required: true, message: '请填写游戏内角色名' }]}
|
||||
>
|
||||
<Input placeholder="如:战神张三 (用于局内加好友丢枪)" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="characterId"
|
||||
label="角色数字ID"
|
||||
rules={[{ required: true, message: '请填写角色数字ID' }]}
|
||||
>
|
||||
<Input placeholder="如:1089238472 (核准号主身份)" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item
|
||||
label="游戏主页截图 (必须包含角色名、ID及仓库/手枪详情)"
|
||||
required
|
||||
tooltip="请上传游戏个人主页或背包手枪品质截图,供客服审核真实性"
|
||||
>
|
||||
<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: 22, color: '#1890ff' }} />
|
||||
<div style={{ marginTop: 6, fontSize: 12 }}>上传主页截图</div>
|
||||
</div>
|
||||
) : null}
|
||||
</Upload>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="sellerNote" label="其他留言与补充">
|
||||
<Input.TextArea
|
||||
rows={2}
|
||||
placeholder="如有手枪配件、子弹、涂装或在线时间段说明,可在此备注"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
size="large"
|
||||
block
|
||||
loading={submitting}
|
||||
icon={<DollarOutlined />}
|
||||
style={{
|
||||
marginTop: 16,
|
||||
height: 48,
|
||||
fontSize: 16,
|
||||
borderRadius: 10,
|
||||
background: 'linear-gradient(135deg, #1890ff 0%, #096dd9 100%)',
|
||||
}}
|
||||
>
|
||||
提交苏尔南手枪寄售登记
|
||||
</Button>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { apiGet, apiPatch, apiPost, apiPut } from '@/lib/http'
|
||||
import type {
|
||||
ConsignmentCollectLink,
|
||||
ConsignmentInventory,
|
||||
ConsignmentStatsSummary,
|
||||
ConsignmentSubmission,
|
||||
@@ -62,25 +61,6 @@ export function returnAdminConsignmentInventory(id: number | string, 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>) {
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -26,11 +26,13 @@ export type ConsignmentInventory = {
|
||||
allocationStatus: string
|
||||
boundWorkOrderId: number | null
|
||||
boundPlatformOrderId: string
|
||||
sourceType: 'manual' | 'collection_link' | 'recycle'
|
||||
sourceType: 'manual' | 'worker_portal' | 'recycle'
|
||||
sourceSubmissionId: number | null
|
||||
workerId?: number | null
|
||||
workerName?: string
|
||||
workerPhone?: string
|
||||
/** 被指派执行本次履约的打手(卖家之外的第三人);workerId 始终是卖家。 */
|
||||
assignedWorkerId?: number | null
|
||||
depositAmountFen?: number
|
||||
inboundAt: string
|
||||
consumedAt: string | null
|
||||
@@ -38,36 +40,9 @@ export type ConsignmentInventory = {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user