feat(consignment): 内部现货占库进入赠送冷静期并闭环发布押金

打手发布冻结履约押金,驳回/作废退还,验收释放;内部匹配履约立刻占用库存并跳过打手资料进入冷静期,撤销回库时补冻押金。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yml2213
2026-09-15 20:03:11 +08:00
co-authored by Cursor
parent 4a6215e816
commit fd1eb5815b
24 changed files with 1662 additions and 462 deletions
@@ -22,8 +22,8 @@ import {
Image,
Input,
InputNumber,
Radio,
Row,
Select,
Space,
Tabs,
Tag,
@@ -59,14 +59,15 @@ const BRILLIANCE_OPTIONS = [
type RecordFilter = 'all' | 'pending' | 'in_stock' | 'done'
const DEPOSIT_TIP_KEY = 'worker-consignment-deposit-tip-seen'
export default function WorkerConsignmentPage() {
const isMobile = useIsMobile()
const { message } = App.useApp()
const { message, modal } = App.useApp()
const [activeTab, setActiveTab] = useState<'publish' | 'records'>('publish')
const [recordFilter, setRecordFilter] = useState<RecordFilter>('all')
const [screenshots, setScreenshots] = useState<UploadedFile[]>([])
const [submitting, setSubmitting] = useState(false)
const [depositTouched, setDepositTouched] = useState(false)
const [form] = Form.useForm()
const profileQuery = useQuery({
@@ -75,6 +76,7 @@ export default function WorkerConsignmentPage() {
staleTime: 60_000,
})
const worker = profileQuery.data?.data.worker
const financeConfig = profileQuery.data?.data.financeConfig
const myConsignmentsQuery = useQuery({
queryKey: ['worker-my-consignments'],
@@ -83,6 +85,14 @@ export default function WorkerConsignmentPage() {
const submissions = myConsignmentsQuery.data?.data.submissions || []
const inventories = myConsignmentsQuery.data?.data.inventories || []
const publishDepositFen = Number(
myConsignmentsQuery.data?.data.publishDepositFen ??
financeConfig?.consignmentPublishDepositFen ??
2000,
)
const publishDepositYuan = publishDepositFen / 100
const availableAmount = Number(worker?.wallet?.availableAmount || 0)
const canPublish = availableAmount >= publishDepositFen
const pendingCount = submissions.filter((s) => s.status === 'pending_review').length
const inStockCount = inventories.filter((i) => i.status === 'in_stock').length
@@ -95,39 +105,53 @@ export default function WorkerConsignmentPage() {
}
}, [worker, form])
function handleFormValuesChange(changedValues: Record<string, unknown>) {
useEffect(() => {
if (profileQuery.isLoading) return
if (typeof window === 'undefined') return
if (window.localStorage.getItem(DEPOSIT_TIP_KEY)) return
window.localStorage.setItem(DEPOSIT_TIP_KEY, '1')
modal.info({
title: '发布寄售需先充值履约押金',
content: (
<div>
<p>
首次发布苏尔南寄售前,请先联系客服充值至少{' '}
<Typography.Text strong type="danger">
¥{publishDepositYuan.toFixed(0)}
</Typography.Text>{' '}
履约押金到接单钱包。
</p>
<p style={{ marginBottom: 0 }}>
发布成功后系统将暂扣该押金;审核驳回或履约完成后自动释放回可用余额。
</p>
</div>
),
okText: '我知道了',
centered: true,
})
}, [profileQuery.isLoading, modal, publishDepositYuan])
function handleFormValuesChange() {
const sys = form.getFieldValue('system')
const zone = form.getFieldValue('serverZone')
const ser = form.getFieldValue('series')
const dia = form.getFieldValue('diamondLevel')
const bri = form.getFieldValue('brillianceCount')
const next: Record<string, unknown> = {}
if (sys && zone && ser && dia != null && bri != null) {
const briNum = Number(bri)
const briStr = briNum > 0 ? `${briNum}璀璨` : ''
next.itemName = `${sys}${zone}${ser}${dia}钻${briStr} 苏尔南`
}
if (Object.prototype.hasOwnProperty.call(changedValues, 'depositAmount')) {
setDepositTouched(true)
}
if (
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
}
if (Object.keys(next).length > 0) {
form.setFieldsValue(next)
form.setFieldsValue({
itemName: `${sys}${zone}${ser}${dia}钻${briStr} 苏尔南`,
})
}
}
async function handlePublishSubmit(values: Record<string, any>) {
if (!canPublish) {
message.warning(`可用余额不足 ¥${publishDepositYuan.toFixed(0)},请先联系客服充值履约押金`)
return
}
const urls = screenshots.map((f) => f.url).filter(Boolean)
if (urls.length === 0) {
message.error('请上传游戏主页截图以供核验')
@@ -140,18 +164,16 @@ export default function WorkerConsignmentPage() {
return
}
const depositAmountFen = Math.round(
Number(values.depositAmount || values.askingPrice || 100) * 100,
)
if (depositAmountFen < 100) {
message.error('履约押金不能少于1元')
return
}
const briNum = Number(values.brillianceCount || 0)
const briStr = briNum > 0 ? `${briNum}璀璨` : ''
const itemName =
String(values.itemName || '').trim() ||
`${values.system}${values.serverZone}${values.series}${values.diamondLevel}钻${briStr} 苏尔南`
setSubmitting(true)
try {
await publishWorkerConsignment({
itemName: values.itemName,
itemName,
system: values.system,
serverZone: values.serverZone,
series: values.series,
@@ -164,14 +186,13 @@ export default function WorkerConsignmentPage() {
characterId: values.characterId,
screenshotsJson: urls,
sellerNote: values.sellerNote,
depositAmountFen,
depositAmountFen: publishDepositFen,
})
message.success('寄卖提报成功,已进入审核')
message.success(`寄卖提报成功,已暂扣履约押金 ¥${publishDepositYuan.toFixed(0)}`)
form.resetFields()
setDepositTouched(false)
setScreenshots([])
await myConsignmentsQuery.refetch()
await Promise.all([myConsignmentsQuery.refetch(), profileQuery.refetch()])
setRecordFilter('pending')
setActiveTab('records')
} catch (err: any) {
@@ -189,7 +210,10 @@ export default function WorkerConsignmentPage() {
const done = inventories.filter((i) => i.status === 'consumed' || i.status === 'archived')
if (recordFilter === 'pending') {
return { submissions: [...pendingSubs, ...rejectedSubs], inventories: [] as WorkerInventoryItem[] }
return {
submissions: [...pendingSubs, ...rejectedSubs],
inventories: [] as WorkerInventoryItem[],
}
}
if (recordFilter === 'in_stock') {
return { submissions: [] as WorkerConsignmentItem[], inventories: inStock }
@@ -203,8 +227,7 @@ export default function WorkerConsignmentPage() {
}
}, [submissions, inventories, recordFilter])
const recordTotal =
filteredRecords.submissions.length + filteredRecords.inventories.length
const recordTotal = filteredRecords.submissions.length + filteredRecords.inventories.length
return (
<section className="page-stack worker-consignment-page">
@@ -347,92 +370,112 @@ export default function WorkerConsignmentPage() {
onValuesChange={handleFormValuesChange}
>
<Alert
type="info"
type={canPublish ? 'info' : 'warning'}
showIcon
icon={<SafetyCertificateOutlined />}
className="worker-consignment-tip"
message="走接单平台押金履约:审核入库后匹配买家工单,系统按登记押金冻结钱包;局内交枪验货通过后解冻并结算货款。"
message={
canPublish
? `履约押金 ¥${publishDepositYuan.toFixed(0)}:发布成功后暂扣,驳回或履约完成后释放。当前可用余额 ¥${(availableAmount / 100).toFixed(2)}`
: `可用余额不足 ¥${publishDepositYuan.toFixed(0)}。请先联系客服充值履约押金后,才能发布寄售。当前可用 ¥${(availableAmount / 100).toFixed(2)}`
}
/>
<Divider orientation="left" plain className="worker-consignment-divider">
<Divider plain className="worker-consignment-divider">
规格与价格
</Divider>
<Row gutter={[12, 0]}>
<Col xs={12} sm={8} md={6}>
<div className="worker-consignment-specs">
<div className="worker-consignment-spec-grid">
<Form.Item
name="system"
label="操作系统"
rules={[{ required: true, message: '请选择系统' }]}
className="worker-consignment-spec-card"
>
<Select
placeholder="请选择"
<Radio.Group
className="worker-consignment-chips"
optionType="button"
buttonStyle="solid"
options={[
{ label: '苹果 (iOS)', value: '苹果' },
{ label: '安卓 (Android)', value: '安卓' },
{ label: '通用/双端', value: '通用' },
{ label: '苹果', value: '苹果' },
{ label: '安卓', value: '安卓' },
{ label: '通用', value: '通用' },
]}
/>
</Form.Item>
</Col>
<Col xs={12} sm={8} md={6}>
<Form.Item
name="serverZone"
label="游戏区服"
rules={[{ required: true, message: '请选择区服' }]}
className="worker-consignment-spec-card"
>
<Select
placeholder="请选择"
<Radio.Group
className="worker-consignment-chips"
optionType="button"
buttonStyle="solid"
options={[
{ label: '微信区', value: '微信区' },
{ label: 'QQ区', value: 'QQ区' },
]}
/>
</Form.Item>
</Col>
<Col xs={12} sm={8} md={6}>
<Form.Item
name="series"
label="系列/枪系"
label="系列 / 枪系"
rules={[{ required: true, message: '请选择枪系' }]}
className="worker-consignment-spec-card"
>
<Select placeholder="请选择" options={SERIES_OPTIONS} />
<Radio.Group
className="worker-consignment-chips"
optionType="button"
buttonStyle="solid"
options={SERIES_OPTIONS}
/>
</Form.Item>
</Col>
<Col xs={12} sm={8} md={6}>
<Form.Item
name="diamondLevel"
label="钻石等级"
rules={[{ required: true, message: '请选择钻级' }]}
className="worker-consignment-spec-card is-count"
>
<Select placeholder="请选择" options={DIAMOND_OPTIONS} />
<Radio.Group
className="worker-consignment-chips is-count"
optionType="button"
buttonStyle="solid"
options={DIAMOND_OPTIONS.map((o) => ({
label: `${o.value}钻`,
value: o.value,
}))}
/>
</Form.Item>
</Col>
<Col xs={12} sm={8} md={6}>
<Form.Item
name="brillianceCount"
label="璀璨数"
rules={[{ required: true, message: '请选择璀璨数' }]}
className="worker-consignment-spec-card is-count"
>
<Select placeholder="请选择" options={BRILLIANCE_OPTIONS} />
<Radio.Group
className="worker-consignment-chips is-count"
optionType="button"
buttonStyle="solid"
options={BRILLIANCE_OPTIONS.map((o) => ({
label: o.value === 0 ? '无' : `${o.value}璀`,
value: o.value,
}))}
/>
</Form.Item>
</Col>
<Col xs={24} sm={16} md={12}>
<Form.Item
name="itemName"
label="物品名称"
rules={[{ required: true, message: '请输入名称' }]}
>
<Input placeholder="选择规格后自动生成,也可手动修改" />
</Form.Item>
</Col>
<Col xs={12} sm={8} md={6}>
</div>
<div className="worker-consignment-price-grid">
<Form.Item
name="askingPrice"
label="寄售价格 (元)"
label="寄售价格"
rules={[{ required: true, message: '请填写寄售价格' }]}
className="worker-consignment-spec-card is-price"
>
<InputNumber
size="large"
min={1}
step={1}
style={{ width: '100%' }}
@@ -440,26 +483,26 @@ export default function WorkerConsignmentPage() {
addonBefore="¥"
/>
</Form.Item>
</Col>
<Col xs={12} sm={8} md={6}>
<Form.Item
name="depositAmount"
label="履约押金 (元)"
rules={[{ required: true, message: '请填写履约押金' }]}
tooltip="派单交枪时冻结,验货后解冻。填写寄售价后可自动带出,也可手动改。"
label="履约押金"
tooltip="平台统一配置。发布成功后暂扣,驳回或履约完成后释放。"
className="worker-consignment-spec-card is-price"
>
<InputNumber
min={1}
step={10}
size="large"
disabled
value={publishDepositYuan}
style={{ width: '100%' }}
placeholder="请输入"
addonBefore="¥"
/>
</Form.Item>
</Col>
</Row>
<Form.Item name="itemName" hidden>
<Input />
</Form.Item>
</div>
</div>
<Divider orientation="left" plain className="worker-consignment-divider">
<Divider plain className="worker-consignment-divider">
联系方式与游戏凭据
</Divider>
@@ -532,12 +575,15 @@ export default function WorkerConsignmentPage() {
htmlType="submit"
size="large"
loading={submitting}
disabled={!canPublish}
icon={<CloudUploadOutlined />}
>
提交寄卖提报
{canPublish
? `提交寄卖(暂扣押金 ¥${publishDepositYuan.toFixed(0)})`
: '余额不足,请先充值押金'}
</Button>
<Typography.Text type="secondary">
提交后进入客服审核,通过后入库匹配
提交后进入客服审核;押金在驳回或履约完成后释放
</Typography.Text>
</div>
</Form>