feat(consignment): 苏尔南寄卖接入打手端押金履约
打手可直接提报寄卖,关联 worker 与履约押金后指派交枪;后台去掉旧收集链接管理,提报审核与库存/工单流程对齐接单平台。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
-- 078_consignment_worker_deposit.sql —— 寄卖库存支持关联接单平台打手账号与押金履约模式
|
||||
|
||||
ALTER TABLE consignment_inventories
|
||||
ADD COLUMN IF NOT EXISTS worker_id BIGINT REFERENCES worker_users(id) ON DELETE SET NULL,
|
||||
ADD COLUMN IF NOT EXISTS worker_name TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS worker_phone TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS deposit_amount_fen INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE consignment_submissions
|
||||
ADD COLUMN IF NOT EXISTS worker_id BIGINT REFERENCES worker_users(id) ON DELETE SET NULL,
|
||||
ADD COLUMN IF NOT EXISTS worker_name TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS worker_phone TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS deposit_amount_fen INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_consignment_inventories_worker_id
|
||||
ON consignment_inventories(worker_id)
|
||||
WHERE worker_id IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_consignment_submissions_worker_id
|
||||
ON consignment_submissions(worker_id)
|
||||
WHERE worker_id IS NOT NULL;
|
||||
|
||||
COMMENT ON COLUMN consignment_inventories.worker_id IS '关联接单平台的打手ID(用于押金冻结与履约指派)';
|
||||
COMMENT ON COLUMN consignment_inventories.deposit_amount_fen IS '该寄卖手枪建议/约定的履约押金金额(分)';
|
||||
COMMENT ON COLUMN consignment_submissions.worker_id IS '提交人或关联的接单平台打手ID';
|
||||
@@ -32,6 +32,10 @@ export type ConsignmentInventoryRow = {
|
||||
shop_name: string
|
||||
source_type: 'manual' | 'collection_link' | 'recycle'
|
||||
source_submission_id: string | number | null
|
||||
worker_id?: string | number | null
|
||||
worker_name?: string
|
||||
worker_phone?: string
|
||||
deposit_amount_fen?: number
|
||||
inbound_at: string
|
||||
consumed_at: string | null
|
||||
created_at: string
|
||||
@@ -83,6 +87,10 @@ export type ConsignmentSubmissionRow = {
|
||||
content_json: string | Record<string, unknown>
|
||||
screenshots_json: string | unknown[]
|
||||
asking_price_fen: number
|
||||
worker_id?: string | number | null
|
||||
worker_name?: string
|
||||
worker_phone?: string
|
||||
deposit_amount_fen?: number
|
||||
status: 'pending_review' | 'approved' | 'rejected'
|
||||
review_note: string
|
||||
reviewer_name: string
|
||||
@@ -121,6 +129,7 @@ export async function listConsignmentInventories(filters: {
|
||||
series?: string | undefined
|
||||
diamondLevel?: number | undefined
|
||||
allowMatching?: boolean | undefined
|
||||
workerId?: number | undefined
|
||||
keyword?: string | undefined
|
||||
page?: number | undefined
|
||||
pageSize?: number | undefined
|
||||
@@ -132,6 +141,10 @@ export async function listConsignmentInventories(filters: {
|
||||
values.push(filters.status)
|
||||
conditions.push(`ci.status = $${values.length}`)
|
||||
}
|
||||
if (filters.workerId) {
|
||||
values.push(filters.workerId)
|
||||
conditions.push(`ci.worker_id = $${values.length}`)
|
||||
}
|
||||
if (filters.buyerName) {
|
||||
values.push(filters.buyerName)
|
||||
conditions.push(`ci.buyer_name = $${values.length}`)
|
||||
@@ -329,6 +342,10 @@ export async function createConsignmentInventory(input: {
|
||||
allowMatching?: boolean
|
||||
sourceType?: 'manual' | 'collection_link' | 'recycle'
|
||||
sourceSubmissionId?: number | null
|
||||
workerId?: number | null
|
||||
workerName?: string
|
||||
workerPhone?: string
|
||||
depositAmountFen?: number
|
||||
now: string
|
||||
}): Promise<ConsignmentInventoryRow> {
|
||||
const sql = `
|
||||
@@ -338,14 +355,16 @@ export async function createConsignmentInventory(input: {
|
||||
inbound_batch, seller_contact, seller_game_id, seller_payment_info,
|
||||
seller_phone, credentials_json, screenshots_json, remark,
|
||||
allow_matching, status, allocation_status, source_type,
|
||||
source_submission_id, inbound_at, created_at, updated_at
|
||||
source_submission_id, worker_id, worker_name, worker_phone, deposit_amount_fen,
|
||||
inbound_at, created_at, updated_at
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6,
|
||||
$7, $8, $9, $10, $11,
|
||||
$12, $13, $14, $15,
|
||||
$16, $17::jsonb, $18::jsonb, $19,
|
||||
$20, 'in_stock', '待安排', $21,
|
||||
$22, $23, $23, $23
|
||||
$22, $23, $24, $25, $26,
|
||||
$27, $27, $27
|
||||
)
|
||||
RETURNING *
|
||||
`
|
||||
@@ -372,6 +391,10 @@ export async function createConsignmentInventory(input: {
|
||||
input.allowMatching !== false,
|
||||
input.sourceType || 'manual',
|
||||
input.sourceSubmissionId || null,
|
||||
input.workerId || null,
|
||||
input.workerName || '',
|
||||
input.workerPhone || '',
|
||||
input.depositAmountFen || 0,
|
||||
input.now,
|
||||
]
|
||||
const res = await query<ConsignmentInventoryRow>(sql, values)
|
||||
@@ -400,6 +423,10 @@ export async function updateConsignmentInventory(
|
||||
allowMatching?: boolean | undefined
|
||||
status?: 'in_stock' | 'consumed' | 'archived' | undefined
|
||||
allocationStatus?: string | undefined
|
||||
workerId?: number | null | undefined
|
||||
workerName?: string | undefined
|
||||
workerPhone?: string | undefined
|
||||
depositAmountFen?: number | undefined
|
||||
now: string
|
||||
},
|
||||
): Promise<ConsignmentInventoryRow | null> {
|
||||
@@ -482,6 +509,22 @@ export async function updateConsignmentInventory(
|
||||
values.push(input.allocationStatus)
|
||||
updates.push(`allocation_status = $${values.length}`)
|
||||
}
|
||||
if (input.workerId !== undefined) {
|
||||
values.push(input.workerId)
|
||||
updates.push(`worker_id = $${values.length}`)
|
||||
}
|
||||
if (input.workerName !== undefined) {
|
||||
values.push(input.workerName)
|
||||
updates.push(`worker_name = $${values.length}`)
|
||||
}
|
||||
if (input.workerPhone !== undefined) {
|
||||
values.push(input.workerPhone)
|
||||
updates.push(`worker_phone = $${values.length}`)
|
||||
}
|
||||
if (input.depositAmountFen !== undefined) {
|
||||
values.push(input.depositAmountFen)
|
||||
updates.push(`deposit_amount_fen = $${values.length}`)
|
||||
}
|
||||
|
||||
if (updates.length === 0) return getConsignmentInventoryById(id)
|
||||
|
||||
@@ -681,6 +724,7 @@ export async function updateConsignmentCollectLink(
|
||||
export async function listConsignmentSubmissions(filters: {
|
||||
status?: string | undefined
|
||||
linkId?: number | undefined
|
||||
workerId?: number | undefined
|
||||
keyword?: string | undefined
|
||||
page?: number | undefined
|
||||
pageSize?: number | undefined
|
||||
@@ -696,6 +740,10 @@ export async function listConsignmentSubmissions(filters: {
|
||||
values.push(filters.linkId)
|
||||
conditions.push(`cs.link_id = $${values.length}`)
|
||||
}
|
||||
if (filters.workerId) {
|
||||
values.push(filters.workerId)
|
||||
conditions.push(`cs.worker_id = $${values.length}`)
|
||||
}
|
||||
if (filters.keyword) {
|
||||
values.push(`%${filters.keyword.trim()}%`)
|
||||
conditions.push(
|
||||
@@ -765,6 +813,10 @@ export async function createConsignmentSubmission(input: {
|
||||
contentJson?: Record<string, unknown>
|
||||
screenshotsJson?: unknown[]
|
||||
askingPriceFen?: number
|
||||
workerId?: number | null
|
||||
workerName?: string
|
||||
workerPhone?: string
|
||||
depositAmountFen?: number
|
||||
now: string
|
||||
}): Promise<ConsignmentSubmissionRow> {
|
||||
const sql = `
|
||||
@@ -773,13 +825,15 @@ export async function createConsignmentSubmission(input: {
|
||||
system, server_zone, series, diamond_level, brilliance_count,
|
||||
seller_game_id, seller_payment_info, seller_phone, contact_info,
|
||||
content_json, screenshots_json, asking_price_fen,
|
||||
worker_id, worker_name, worker_phone, deposit_amount_fen,
|
||||
status, created_at, updated_at
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5,
|
||||
$6, $7, $8, $9, $10,
|
||||
$11, $12, $13, $14,
|
||||
$15::jsonb, $16::jsonb, $17,
|
||||
'pending_review', $18, $18
|
||||
$18, $19, $20, $21,
|
||||
'pending_review', $22, $22
|
||||
)
|
||||
RETURNING *
|
||||
`
|
||||
@@ -801,6 +855,10 @@ export async function createConsignmentSubmission(input: {
|
||||
toJsonString(input.contentJson || {}),
|
||||
toJsonString(input.screenshotsJson || []),
|
||||
input.askingPriceFen || 0,
|
||||
input.workerId || null,
|
||||
input.workerName || '',
|
||||
input.workerPhone || '',
|
||||
input.depositAmountFen || 0,
|
||||
input.now,
|
||||
])
|
||||
return res.rows[0]!
|
||||
@@ -852,6 +910,10 @@ export async function approveConsignmentSubmissionWithInbound(input: {
|
||||
credentialsJson?: Record<string, unknown> | undefined
|
||||
screenshotsJson?: unknown[] | undefined
|
||||
reviewNote?: string | undefined
|
||||
workerId?: number | null | undefined
|
||||
workerName?: string | undefined
|
||||
workerPhone?: string | undefined
|
||||
depositAmountFen?: number | undefined
|
||||
now: string
|
||||
}): Promise<{ submission: ConsignmentSubmissionRow; inventory: ConsignmentInventoryRow }> {
|
||||
return withTransaction(async (client: PoolClient) => {
|
||||
@@ -863,6 +925,16 @@ export async function approveConsignmentSubmissionWithInbound(input: {
|
||||
if (!submission) throw new Error('提报单不存在')
|
||||
if (submission.status !== 'pending_review') throw new Error('提报单已被处理')
|
||||
|
||||
const workerId = input.workerId !== undefined ? input.workerId : submission.worker_id || null
|
||||
const workerName =
|
||||
input.workerName !== undefined ? input.workerName : submission.worker_name || ''
|
||||
const workerPhone =
|
||||
input.workerPhone !== undefined ? input.workerPhone : submission.worker_phone || ''
|
||||
const depositAmountFen =
|
||||
input.depositAmountFen !== undefined
|
||||
? input.depositAmountFen
|
||||
: submission.deposit_amount_fen || 0
|
||||
|
||||
const invSql = `
|
||||
INSERT INTO consignment_inventories (
|
||||
inventory_no, game_name, category_id, item_name, system, server_zone,
|
||||
@@ -870,14 +942,16 @@ export async function approveConsignmentSubmissionWithInbound(input: {
|
||||
inbound_batch, seller_contact, seller_game_id, seller_payment_info,
|
||||
seller_phone, credentials_json, screenshots_json, remark,
|
||||
allow_matching, status, allocation_status, source_type,
|
||||
source_submission_id, inbound_at, created_at, updated_at
|
||||
source_submission_id, worker_id, worker_name, worker_phone, deposit_amount_fen,
|
||||
inbound_at, created_at, updated_at
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6,
|
||||
$7, $8, $9, $10, $11,
|
||||
$12, $13, $14, $15,
|
||||
$16, $17::jsonb, $18::jsonb, $19,
|
||||
TRUE, 'in_stock', '待安排', 'collection_link',
|
||||
$20, $21, $21, $21
|
||||
$20, $21, $22, $23, $24,
|
||||
$25, $25, $25
|
||||
)
|
||||
RETURNING *
|
||||
`
|
||||
@@ -902,6 +976,10 @@ export async function approveConsignmentSubmissionWithInbound(input: {
|
||||
toJsonString(input.screenshotsJson || submission.screenshots_json || []),
|
||||
input.reviewNote || '',
|
||||
submission.id,
|
||||
workerId,
|
||||
workerName,
|
||||
workerPhone,
|
||||
depositAmountFen,
|
||||
input.now,
|
||||
]
|
||||
const invRes = await client.query<ConsignmentInventoryRow>(invSql, invValues)
|
||||
@@ -914,15 +992,23 @@ export async function approveConsignmentSubmissionWithInbound(input: {
|
||||
result_inventory_id = $1,
|
||||
reviewer_name = $2,
|
||||
review_note = $3,
|
||||
reviewed_at = $4,
|
||||
updated_at = $4
|
||||
WHERE id = $5
|
||||
worker_id = COALESCE($4, worker_id),
|
||||
worker_name = COALESCE($5, worker_name),
|
||||
worker_phone = COALESCE($6, worker_phone),
|
||||
deposit_amount_fen = COALESCE($7, deposit_amount_fen),
|
||||
reviewed_at = $8,
|
||||
updated_at = $8
|
||||
WHERE id = $9
|
||||
RETURNING *
|
||||
`
|
||||
const updateSubRes = await client.query<ConsignmentSubmissionRow>(updateSubSql, [
|
||||
inventory.id,
|
||||
input.buyerName,
|
||||
input.reviewNote || '',
|
||||
workerId,
|
||||
workerName,
|
||||
workerPhone,
|
||||
depositAmountFen,
|
||||
input.now,
|
||||
submission.id,
|
||||
])
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
updateAdminConsignmentLink,
|
||||
} from '../../../services/worker-platform/consignment-admin-service.js'
|
||||
import {
|
||||
assignWorkOrderWithConsignmentWorker,
|
||||
checkWorkOrderInventoryMatches,
|
||||
fulfillWorkOrderWithConsignment,
|
||||
revokeWorkOrderConsignmentFulfillment,
|
||||
@@ -253,6 +254,25 @@ router.post(
|
||||
),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/worker-platform/consignment/assign-worker',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
assignWorkOrderWithConsignmentWorker({
|
||||
workOrderId: String(req.body?.workOrderId || ''),
|
||||
inventoryId: String(req.body?.inventoryId || ''),
|
||||
workerId: req.body?.workerId ? Number(req.body.workerId) : null,
|
||||
actorName: req.adminSession?.username || 'admin',
|
||||
}),
|
||||
{
|
||||
successMessage: '已指派打手,押金已冻结,工单流转进行中!',
|
||||
errorMessage: '指派打手履约失败',
|
||||
scope: '[admin/consignment/assign-worker]',
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/worker-platform/consignment/revoke',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
|
||||
@@ -5,6 +5,7 @@ import workerAuthSessionRoutes from './worker/auth-session.js'
|
||||
import workerProfileRoutes from './worker/profile.js'
|
||||
import workerHallRoutes from './worker/hall.js'
|
||||
import workerOrdersRoutes from './worker/orders.js'
|
||||
import workerConsignmentRoutes from './worker/consignment.js'
|
||||
import { requireWorkerSession } from './worker/session.js'
|
||||
import { buildNotFoundPayload } from '../utils/http.js'
|
||||
|
||||
@@ -22,6 +23,8 @@ router.use(workerHallRoutes)
|
||||
|
||||
router.use(workerOrdersRoutes)
|
||||
|
||||
router.use(workerConsignmentRoutes)
|
||||
|
||||
router.use((req, res) => {
|
||||
res.status(404).json(buildNotFoundPayload(req))
|
||||
})
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Router } from 'express'
|
||||
|
||||
import {
|
||||
listWorkerConsignments,
|
||||
publishWorkerConsignment,
|
||||
} from '../../services/worker-platform/consignment-worker-service.js'
|
||||
import { sendRouteError } from '../../utils/http.js'
|
||||
import { getRequiredWorkerSession } from './session.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
/**
|
||||
* POST /api/v1/worker/consignment/publish
|
||||
* 打手在接单平台直接提报苏尔南手枪寄卖
|
||||
*/
|
||||
router.post('/consignment/publish', async (req, res) => {
|
||||
try {
|
||||
const session = getRequiredWorkerSession(req)
|
||||
const result = await publishWorkerConsignment(session.workerId, req.body || {})
|
||||
res.json({ ok: true, data: result })
|
||||
} catch (error) {
|
||||
sendRouteError(res, error, '提交寄卖失败', '[worker/consignment/publish]')
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* GET /api/v1/worker/consignment/my-items
|
||||
* 打手查看自己在接单平台寄卖的手枪列表与状态
|
||||
*/
|
||||
router.get('/consignment/my-items', async (req, res) => {
|
||||
try {
|
||||
const session = getRequiredWorkerSession(req)
|
||||
const result = await listWorkerConsignments(session.workerId)
|
||||
res.json({ ok: true, data: result })
|
||||
} catch (error) {
|
||||
sendRouteError(res, error, '获取我的寄卖列表失败', '[worker/consignment/my-items]')
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -16,6 +16,10 @@ import {
|
||||
updateConsignmentCollectLink,
|
||||
updateConsignmentInventory,
|
||||
} from '../../repositories/worker-platform/consignment-repo.js'
|
||||
import {
|
||||
getWorkerUserById,
|
||||
getWorkerUserByPhone,
|
||||
} from '../../repositories/worker-platform/index.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { generateConsignmentInventoryNo, randomToken } from '../../utils/random.js'
|
||||
@@ -60,6 +64,10 @@ export function mapConsignmentInventory(row: ConsignmentInventoryRow) {
|
||||
boundPlatformOrderId: row.bound_platform_order_id || '',
|
||||
sourceType: row.source_type,
|
||||
sourceSubmissionId: row.source_submission_id ? Number(row.source_submission_id) : null,
|
||||
workerId: row.worker_id ? Number(row.worker_id) : null,
|
||||
workerName: row.worker_name || '',
|
||||
workerPhone: row.worker_phone || '',
|
||||
depositAmountFen: Number(row.deposit_amount_fen || 0),
|
||||
inboundAt: row.inbound_at || row.created_at,
|
||||
consumedAt: row.consumed_at,
|
||||
createdAt: row.created_at,
|
||||
@@ -126,6 +134,10 @@ export function mapConsignmentSubmission(row: ConsignmentSubmissionRow) {
|
||||
? (safeParseJson(row.screenshots_json) as unknown[])
|
||||
: [],
|
||||
askingPriceFen: Number(row.asking_price_fen || 0),
|
||||
workerId: row.worker_id ? Number(row.worker_id) : null,
|
||||
workerName: row.worker_name || '',
|
||||
workerPhone: row.worker_phone || '',
|
||||
depositAmountFen: Number(row.deposit_amount_fen || 0),
|
||||
status: row.status,
|
||||
reviewNote: row.review_note || '',
|
||||
reviewerName: row.reviewer_name || '',
|
||||
@@ -204,8 +216,8 @@ export async function createAdminConsignmentInventory(payload: JsonObject, actor
|
||||
system: String(payload.system || '').trim(),
|
||||
serverZone: String(payload.serverZone || '').trim(),
|
||||
series: String(payload.series || '').trim(),
|
||||
diamondLevel: Math.max(1, Math.min(6, Number(payload.diamondLevel || 1))),
|
||||
brillianceCount: Math.max(0, Math.min(6, Number(payload.brillianceCount || 0))),
|
||||
diamondLevel: Math.max(1, Math.min(7, Number(payload.diamondLevel || 1))),
|
||||
brillianceCount: Math.max(0, Math.min(7, Number(payload.brillianceCount || 0))),
|
||||
costPriceFen,
|
||||
buyerName: String(payload.buyerName || actorName || '').trim(),
|
||||
inboundBatch: String(payload.inboundBatch || '').trim(),
|
||||
@@ -221,6 +233,10 @@ export async function createAdminConsignmentInventory(payload: JsonObject, actor
|
||||
remark: String(payload.remark || '').trim(),
|
||||
allowMatching: payload.allowMatching !== false,
|
||||
sourceType: 'manual',
|
||||
workerId: payload.workerId ? Number(payload.workerId) : null,
|
||||
workerName: String(payload.workerName || '').trim(),
|
||||
workerPhone: String(payload.workerPhone || '').trim(),
|
||||
depositAmountFen: payload.depositAmountFen ? Number(payload.depositAmountFen) : 0,
|
||||
now,
|
||||
})
|
||||
|
||||
@@ -238,9 +254,14 @@ export async function updateAdminConsignmentInventory(
|
||||
system: payload.system !== undefined ? String(payload.system).trim() : undefined,
|
||||
serverZone: payload.serverZone !== undefined ? String(payload.serverZone).trim() : undefined,
|
||||
series: payload.series !== undefined ? String(payload.series).trim() : undefined,
|
||||
diamondLevel: payload.diamondLevel !== undefined ? Number(payload.diamondLevel) : undefined,
|
||||
diamondLevel:
|
||||
payload.diamondLevel !== undefined
|
||||
? Math.max(1, Math.min(7, Number(payload.diamondLevel)))
|
||||
: undefined,
|
||||
brillianceCount:
|
||||
payload.brillianceCount !== undefined ? Number(payload.brillianceCount) : undefined,
|
||||
payload.brillianceCount !== undefined
|
||||
? Math.max(0, Math.min(7, Number(payload.brillianceCount)))
|
||||
: undefined,
|
||||
costPriceFen:
|
||||
payload.costPriceFen !== undefined
|
||||
? Math.max(0, Math.round(Number(payload.costPriceFen)))
|
||||
@@ -265,6 +286,16 @@ export async function updateAdminConsignmentInventory(
|
||||
remark: payload.remark !== undefined ? String(payload.remark).trim() : undefined,
|
||||
allowMatching: payload.allowMatching !== undefined ? Boolean(payload.allowMatching) : undefined,
|
||||
status: payload.status !== undefined ? (payload.status as any) : undefined,
|
||||
workerId:
|
||||
payload.workerId !== undefined
|
||||
? payload.workerId
|
||||
? Number(payload.workerId)
|
||||
: null
|
||||
: undefined,
|
||||
workerName: payload.workerName !== undefined ? String(payload.workerName).trim() : undefined,
|
||||
workerPhone: payload.workerPhone !== undefined ? String(payload.workerPhone).trim() : undefined,
|
||||
depositAmountFen:
|
||||
payload.depositAmountFen !== undefined ? Number(payload.depositAmountFen) : undefined,
|
||||
now,
|
||||
})
|
||||
|
||||
@@ -428,6 +459,29 @@ export async function approveAdminConsignmentSubmission(
|
||||
Math.round(Number(payload.costPriceFen ?? submission.asking_price_fen ?? 0)),
|
||||
)
|
||||
|
||||
let workerId = payload.workerId
|
||||
? Number(payload.workerId)
|
||||
: submission.worker_id
|
||||
? Number(submission.worker_id)
|
||||
: null
|
||||
let workerName = String(payload.workerName || submission.worker_name || '').trim()
|
||||
let workerPhone = String(payload.workerPhone || submission.worker_phone || '').trim()
|
||||
|
||||
const phoneToLookup = String(payload.sellerPhone || submission.seller_phone || '').trim()
|
||||
if (!workerId && phoneToLookup) {
|
||||
const matchedWorker = await getWorkerUserByPhone(phoneToLookup)
|
||||
if (matchedWorker) {
|
||||
workerId = Number(matchedWorker.id)
|
||||
workerName = matchedWorker.display_name || matchedWorker.username || ''
|
||||
workerPhone = matchedWorker.phone || phoneToLookup
|
||||
}
|
||||
}
|
||||
|
||||
const depositAmountFen =
|
||||
payload.depositAmountFen !== undefined
|
||||
? Number(payload.depositAmountFen)
|
||||
: Number(submission.deposit_amount_fen || 0)
|
||||
|
||||
const res = await approveConsignmentSubmissionWithInbound({
|
||||
submissionId: id,
|
||||
inventoryNo,
|
||||
@@ -439,23 +493,27 @@ export async function approveAdminConsignmentSubmission(
|
||||
series: String(payload.series || submission.series).trim(),
|
||||
diamondLevel: Math.max(
|
||||
1,
|
||||
Math.min(6, Number(payload.diamondLevel ?? submission.diamond_level ?? 1)),
|
||||
Math.min(7, Number(payload.diamondLevel ?? submission.diamond_level ?? 1)),
|
||||
),
|
||||
brillianceCount: Math.max(
|
||||
0,
|
||||
Math.min(6, Number(payload.brillianceCount ?? submission.brilliance_count ?? 0)),
|
||||
Math.min(7, Number(payload.brillianceCount ?? submission.brilliance_count ?? 0)),
|
||||
),
|
||||
costPriceFen,
|
||||
sellerContact: String(payload.sellerContact || submission.contact_info).trim(),
|
||||
sellerGameId: String(payload.sellerGameId || submission.seller_game_id).trim(),
|
||||
sellerPaymentInfo: String(payload.sellerPaymentInfo || submission.seller_payment_info).trim(),
|
||||
sellerPhone: String(payload.sellerPhone || submission.seller_phone).trim(),
|
||||
sellerPhone: phoneToLookup,
|
||||
credentialsJson:
|
||||
payload.credentials && typeof payload.credentials === 'object'
|
||||
? (payload.credentials as Record<string, unknown>)
|
||||
: undefined,
|
||||
screenshotsJson: Array.isArray(payload.screenshots) ? payload.screenshots : undefined,
|
||||
reviewNote: String(payload.reviewNote || '').trim(),
|
||||
workerId,
|
||||
workerName,
|
||||
workerPhone,
|
||||
depositAmountFen,
|
||||
now,
|
||||
})
|
||||
|
||||
|
||||
@@ -5,16 +5,19 @@ import {
|
||||
type ConsignmentInventoryRow,
|
||||
getConsignmentInventoryById,
|
||||
listActiveConsignmentInventoriesForMatching,
|
||||
updateConsignmentInventory,
|
||||
} from '../../repositories/worker-platform/consignment-repo.js'
|
||||
import {
|
||||
createWorkOrderEvent,
|
||||
getWorkOrderById,
|
||||
updateWorkOrder,
|
||||
type WorkOrderRow,
|
||||
} from '../../repositories/worker-platform/index.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import { safeParseJson } from '../admin/admin-query-utils.js'
|
||||
import { publishWorkOrderRealtimeChange } from '../realtime/realtime-event-service.js'
|
||||
import { assignAdminWorkOrderToWorker } from './admin-work-order-assignment-service.js'
|
||||
import { mapWorkOrderAdmin } from './mappers.js'
|
||||
import {
|
||||
consumeIndustryVouchersBeforeWorkOrderPublish,
|
||||
@@ -246,6 +249,122 @@ export async function fulfillWorkOrderWithConsignment(input: {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 选用内部寄售库存并走接单平台指派打手(冻结押金担保履约)
|
||||
*/
|
||||
export async function assignWorkOrderWithConsignmentWorker(input: {
|
||||
workOrderId: number | string
|
||||
inventoryId: number | string
|
||||
workerId?: number | string | null
|
||||
actorName: string
|
||||
}): Promise<{
|
||||
order: ReturnType<typeof mapWorkOrderAdmin>
|
||||
inventory: ReturnType<typeof mapConsignmentInventory>
|
||||
voucherConsume: WorkOrderVoucherConsumeResult
|
||||
}> {
|
||||
const workOrder = await getWorkOrderById(input.workOrderId)
|
||||
if (!workOrder) {
|
||||
throw createHttpError('工单不存在', { statusCode: 404, errorCode: 'work_order_not_found' })
|
||||
}
|
||||
|
||||
const inventory = await getConsignmentInventoryById(input.inventoryId)
|
||||
if (!inventory) {
|
||||
throw createHttpError('寄卖物品不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'consignment_inventory_not_found',
|
||||
})
|
||||
}
|
||||
|
||||
if (inventory.status !== 'in_stock' || !inventory.allow_matching) {
|
||||
throw createHttpError('该寄卖物品不可用或已被暂停匹配', {
|
||||
statusCode: 409,
|
||||
errorCode: 'consignment_inventory_unavailable',
|
||||
})
|
||||
}
|
||||
|
||||
const targetWorkerId = input.workerId || inventory.worker_id
|
||||
if (!targetWorkerId) {
|
||||
throw createHttpError('该寄卖物品未关联打手,请在指派弹窗中选择打手账号', {
|
||||
statusCode: 400,
|
||||
errorCode: 'consignment_worker_required',
|
||||
})
|
||||
}
|
||||
|
||||
// 1. 调用接单平台统一指派流程(进行打手可用性校验、余额与押金冻结、电子凭证核销、工单状态流转到 in_progress)
|
||||
const assignResult = await assignAdminWorkOrderToWorker(
|
||||
workOrder.id,
|
||||
{ workerId: targetWorkerId },
|
||||
input.actorName,
|
||||
)
|
||||
|
||||
const now = nowIso()
|
||||
const material = safeParseJson(workOrder.material_json)
|
||||
|
||||
// 2. 将寄售手枪信息写入工单 material
|
||||
const updatedMaterial = {
|
||||
...material,
|
||||
consignment: {
|
||||
inventoryId: Number(inventory.id),
|
||||
inventoryNo: inventory.inventory_no,
|
||||
itemName: inventory.item_name,
|
||||
system: inventory.system,
|
||||
serverZone: inventory.server_zone,
|
||||
series: inventory.series,
|
||||
diamondLevel: inventory.diamond_level,
|
||||
brillianceCount: inventory.brilliance_count,
|
||||
costPriceFen: inventory.cost_price_fen,
|
||||
workerId: Number(targetWorkerId),
|
||||
workerName: inventory.worker_name || '',
|
||||
fulfillmentType: 'worker_deposit',
|
||||
assignedAt: now,
|
||||
assignedBy: input.actorName,
|
||||
},
|
||||
}
|
||||
|
||||
const rewardAmountYuan =
|
||||
Number(inventory.cost_price_fen || 0) > 0
|
||||
? Number((Number(inventory.cost_price_fen) / 100).toFixed(2))
|
||||
: Number(workOrder.reward_amount || 0)
|
||||
|
||||
await updateWorkOrder(workOrder.id, {
|
||||
material_json: updatedMaterial,
|
||||
reward_amount: rewardAmountYuan,
|
||||
updated_at: now,
|
||||
})
|
||||
|
||||
// 3. 将寄卖库存置为 consumed 已出库/已安排打手
|
||||
const updatedInventory = await updateConsignmentInventory(inventory.id, {
|
||||
status: 'consumed',
|
||||
allocationStatus: '已指派打手(押金履约中)',
|
||||
workerId: Number(targetWorkerId),
|
||||
now,
|
||||
})
|
||||
|
||||
await createWorkOrderEvent({
|
||||
workOrderId: workOrder.id,
|
||||
actorType: 'admin',
|
||||
actorId: input.actorName,
|
||||
eventType: 'consignment_worker_assigned',
|
||||
fromStatus: workOrder.status,
|
||||
toStatus: WORK_ORDER_STATUS.IN_PROGRESS,
|
||||
payloadJson: JSON.stringify({
|
||||
inventoryId: inventory.id,
|
||||
inventoryNo: inventory.inventory_no,
|
||||
workerId: targetWorkerId,
|
||||
costPriceFen: inventory.cost_price_fen,
|
||||
}),
|
||||
now,
|
||||
})
|
||||
|
||||
const refreshedOrder = await getWorkOrderById(workOrder.id)
|
||||
|
||||
return {
|
||||
order: mapWorkOrderAdmin(refreshedOrder || (assignResult.order as any)),
|
||||
inventory: mapConsignmentInventory(updatedInventory!),
|
||||
voucherConsume: assignResult.voucherConsume,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销寄售履约(用于选错重选、买家退款或物品异常)
|
||||
*/
|
||||
|
||||
@@ -61,6 +61,12 @@ test('extractPeacekeeperSpecs extracts specs accurately', () => {
|
||||
assert.equal(spec2.series, '怒嚎')
|
||||
assert.equal(spec2.diamondLevel, 2)
|
||||
assert.equal(spec2.brillianceCount, 1)
|
||||
|
||||
const spec3 = extractPeacekeeperSpecs('安卓区 血宴 7钻 7璀璨')
|
||||
assert.equal(spec3.system, '安卓')
|
||||
assert.equal(spec3.series, '血宴')
|
||||
assert.equal(spec3.diamondLevel, 7)
|
||||
assert.equal(spec3.brillianceCount, 7)
|
||||
})
|
||||
|
||||
test('matchPeacekeeperOrderWithInventory matches exact spec', () => {
|
||||
|
||||
@@ -62,16 +62,16 @@ export function extractPeacekeeperSpecs(
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 钻级识别 (1~6钻)
|
||||
// 4. 钻级识别 (1~7钻)
|
||||
let diamondLevel = extraMeta?.diamondLevel || 1
|
||||
const diamondMatch = text.match(/([1-6])\s*钻/)
|
||||
const diamondMatch = text.match(/([1-7])\s*钻/)
|
||||
if (diamondMatch?.[1]) {
|
||||
diamondLevel = Number(diamondMatch[1])
|
||||
}
|
||||
|
||||
// 5. 璀璨数量识别 (0~6璀璨)
|
||||
// 5. 璀璨数量识别 (0~7璀璨)
|
||||
let brillianceCount = 0
|
||||
const brillianceMatch = text.match(/([1-6])\s*璀璨/)
|
||||
const brillianceMatch = text.match(/([1-7])\s*璀璨/)
|
||||
if (brillianceMatch?.[1]) {
|
||||
brillianceCount = Number(brillianceMatch[1])
|
||||
} else if (text.includes('璀璨')) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
createConsignmentSubmission,
|
||||
getConsignmentCollectLinkByToken,
|
||||
} from '../../repositories/worker-platform/consignment-repo.js'
|
||||
import { getWorkerUserByPhone } from '../../repositories/worker-platform/index.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { generateConsignmentSubmissionNo } from '../../utils/random.js'
|
||||
@@ -65,19 +66,89 @@ export async function submitPublicConsignment(token: string, payload: JsonObject
|
||||
).trim()
|
||||
const contactInfo = String(payload.contactInfo || payload.sellerContact || '').trim()
|
||||
if (!contactInfo) {
|
||||
throw createHttpError('请填写联系方式(微信号或手机号)', {
|
||||
throw createHttpError('请填写微信号或QQ号以便联系与核实', {
|
||||
statusCode: 400,
|
||||
errorCode: 'contact_info_required',
|
||||
})
|
||||
}
|
||||
|
||||
const contentPayload =
|
||||
payload.contentJson && typeof payload.contentJson === 'object'
|
||||
? (payload.contentJson as Record<string, unknown>)
|
||||
: payload.content && typeof payload.content === 'object'
|
||||
? (payload.content as Record<string, unknown>)
|
||||
: {}
|
||||
|
||||
const characterName = String(
|
||||
contentPayload.characterName || payload.characterName || '',
|
||||
).trim()
|
||||
const characterId = String(contentPayload.characterId || payload.characterId || '').trim()
|
||||
if (!characterName) {
|
||||
throw createHttpError('请填写游戏内角色名', {
|
||||
statusCode: 400,
|
||||
errorCode: 'character_name_required',
|
||||
})
|
||||
}
|
||||
if (!characterId) {
|
||||
throw createHttpError('请填写角色数字ID', {
|
||||
statusCode: 400,
|
||||
errorCode: 'character_id_required',
|
||||
})
|
||||
}
|
||||
|
||||
const screenshots = Array.isArray(payload.screenshotsJson)
|
||||
? payload.screenshotsJson
|
||||
: Array.isArray(payload.screenshots)
|
||||
? payload.screenshots
|
||||
: []
|
||||
if (screenshots.length === 0) {
|
||||
throw createHttpError('请上传游戏主页截图以供核验', {
|
||||
statusCode: 400,
|
||||
errorCode: 'screenshot_required',
|
||||
})
|
||||
}
|
||||
|
||||
const now = nowIso()
|
||||
const submissionNo = generateConsignmentSubmissionNo()
|
||||
const askingPriceFen = Math.max(
|
||||
0,
|
||||
100,
|
||||
Math.round(Number(payload.askingPriceFen || link.preset_price_fen || 0)),
|
||||
)
|
||||
|
||||
const sellerPhone = String(payload.sellerPhone || '').trim()
|
||||
if (!sellerPhone) {
|
||||
throw createHttpError('请填写联系电话', {
|
||||
statusCode: 400,
|
||||
errorCode: 'seller_phone_required',
|
||||
})
|
||||
}
|
||||
|
||||
let workerId: number | null = null
|
||||
let workerName = ''
|
||||
let workerPhone = ''
|
||||
|
||||
if (sellerPhone) {
|
||||
const matchedWorker = await getWorkerUserByPhone(sellerPhone)
|
||||
if (matchedWorker) {
|
||||
workerId = Number(matchedWorker.id)
|
||||
workerName = matchedWorker.display_name || matchedWorker.username || ''
|
||||
workerPhone = matchedWorker.phone || sellerPhone
|
||||
}
|
||||
}
|
||||
|
||||
const diamondLevel = Math.max(
|
||||
1,
|
||||
Math.min(7, Number(payload.diamondLevel || link.diamond_level || 1)),
|
||||
)
|
||||
const brillianceCount = Math.max(
|
||||
0,
|
||||
Math.min(7, Number(payload.brillianceCount ?? link.brilliance_count ?? 0)),
|
||||
)
|
||||
|
||||
const sellerGameId = String(
|
||||
payload.sellerGameId || `${characterName} (${characterId})`,
|
||||
).trim()
|
||||
|
||||
const submission = await createConsignmentSubmission({
|
||||
submissionNo,
|
||||
linkId: Number(link.id),
|
||||
@@ -87,18 +158,28 @@ export async function submitPublicConsignment(token: string, payload: JsonObject
|
||||
system: String(payload.system || link.system || '').trim(),
|
||||
serverZone: String(payload.serverZone || link.server_zone || '').trim(),
|
||||
series: String(payload.series || link.series || '').trim(),
|
||||
diamondLevel: Number(payload.diamondLevel || link.diamond_level || 1),
|
||||
brillianceCount: Number(payload.brillianceCount ?? link.brilliance_count ?? 0),
|
||||
sellerGameId: String(payload.sellerGameId || '').trim(),
|
||||
sellerPaymentInfo: String(payload.sellerPaymentInfo || '').trim(),
|
||||
sellerPhone: String(payload.sellerPhone || '').trim(),
|
||||
diamondLevel,
|
||||
brillianceCount,
|
||||
sellerGameId,
|
||||
sellerPaymentInfo: String(payload.sellerPaymentInfo || '走接单平台钱包结算').trim(),
|
||||
sellerPhone,
|
||||
contactInfo,
|
||||
contentJson:
|
||||
payload.content && typeof payload.content === 'object'
|
||||
? (payload.content as Record<string, unknown>)
|
||||
: (payload as Record<string, unknown>),
|
||||
screenshotsJson: Array.isArray(payload.screenshots) ? payload.screenshots : [],
|
||||
contentJson: {
|
||||
...contentPayload,
|
||||
characterName,
|
||||
characterId,
|
||||
sellerNote: String(contentPayload.sellerNote || payload.sellerNote || '').trim(),
|
||||
channel: 'collect_link',
|
||||
},
|
||||
screenshotsJson: screenshots,
|
||||
askingPriceFen,
|
||||
workerId,
|
||||
workerName,
|
||||
workerPhone,
|
||||
depositAmountFen: Math.max(
|
||||
100,
|
||||
Math.round(Number(payload.depositAmountFen || askingPriceFen || 10000)),
|
||||
),
|
||||
now,
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import {
|
||||
createConsignmentSubmission,
|
||||
listConsignmentInventories,
|
||||
listConsignmentSubmissions,
|
||||
} from '../../repositories/worker-platform/consignment-repo.js'
|
||||
import { getWorkerUserById } from '../../repositories/worker-platform/index.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { generateConsignmentSubmissionNo } from '../../utils/random.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
|
||||
export async function publishWorkerConsignment(workerId: number, payload: JsonObject) {
|
||||
const worker = await getWorkerUserById(workerId)
|
||||
if (!worker) {
|
||||
throw createHttpError('打手账户不存在', { statusCode: 404, errorCode: 'worker_not_found' })
|
||||
}
|
||||
|
||||
const system = String(payload.system || '苹果').trim()
|
||||
const serverZone = String(payload.serverZone || '微信区').trim()
|
||||
const series = String(payload.series || '暗潮').trim()
|
||||
const diamondLevel = Math.max(1, Math.min(7, Number(payload.diamondLevel || 1)))
|
||||
const brillianceCount = Math.max(0, Math.min(7, Number(payload.brillianceCount || 0)))
|
||||
|
||||
const contactInfo = String(payload.sellerContact || payload.contactInfo || '').trim()
|
||||
if (!contactInfo) {
|
||||
throw createHttpError('请填写微信号或QQ号以便联系与核实', {
|
||||
statusCode: 400,
|
||||
errorCode: 'contact_required',
|
||||
})
|
||||
}
|
||||
|
||||
const characterName = String(payload.characterName || '').trim()
|
||||
const characterId = String(payload.characterId || '').trim()
|
||||
if (!characterName) {
|
||||
throw createHttpError('请填写游戏内角色名', {
|
||||
statusCode: 400,
|
||||
errorCode: 'character_name_required',
|
||||
})
|
||||
}
|
||||
if (!characterId) {
|
||||
throw createHttpError('请填写角色数字ID', {
|
||||
statusCode: 400,
|
||||
errorCode: 'character_id_required',
|
||||
})
|
||||
}
|
||||
|
||||
const screenshots = Array.isArray(payload.screenshotsJson) ? payload.screenshotsJson : []
|
||||
if (screenshots.length === 0) {
|
||||
throw createHttpError('请上传游戏主页截图以供核验', {
|
||||
statusCode: 400,
|
||||
errorCode: 'screenshot_required',
|
||||
})
|
||||
}
|
||||
|
||||
const askingPriceFen = Math.max(100, Math.round(Number(payload.askingPriceFen || 0)))
|
||||
if (askingPriceFen < 100) {
|
||||
throw createHttpError('寄售价格不能少于1元', {
|
||||
statusCode: 400,
|
||||
errorCode: 'invalid_asking_price',
|
||||
})
|
||||
}
|
||||
|
||||
const depositAmountFen = Math.max(
|
||||
100,
|
||||
Math.round(Number(payload.depositAmountFen || askingPriceFen || 10000)),
|
||||
)
|
||||
|
||||
const sellerPhone = String(payload.sellerPhone || worker.phone || '').trim()
|
||||
const briStr = brillianceCount > 0 ? `${brillianceCount}璀璨` : ''
|
||||
const defaultItemName = `${system}${serverZone}${series}${diamondLevel}钻${briStr} 苏尔南`
|
||||
const itemName = String(payload.itemName || defaultItemName).trim()
|
||||
|
||||
const submissionNo = generateConsignmentSubmissionNo()
|
||||
const now = nowIso()
|
||||
|
||||
const submission = await createConsignmentSubmission({
|
||||
submissionNo,
|
||||
linkId: null,
|
||||
gameName: '和平精英',
|
||||
categoryId: null,
|
||||
itemName,
|
||||
system,
|
||||
serverZone,
|
||||
series,
|
||||
diamondLevel,
|
||||
brillianceCount,
|
||||
sellerGameId: characterId ? `${characterName} (${characterId})` : characterName,
|
||||
sellerPaymentInfo: '走打手接单平台钱包',
|
||||
sellerPhone,
|
||||
contactInfo,
|
||||
contentJson: {
|
||||
characterName,
|
||||
characterId,
|
||||
sellerNote: String(payload.sellerNote || '').trim(),
|
||||
channel: 'worker_portal',
|
||||
},
|
||||
screenshotsJson: screenshots,
|
||||
askingPriceFen,
|
||||
workerId: Number(worker.id),
|
||||
workerName: worker.display_name || worker.username,
|
||||
workerPhone: sellerPhone || worker.phone || '',
|
||||
depositAmountFen,
|
||||
now,
|
||||
})
|
||||
|
||||
return {
|
||||
id: submission.id,
|
||||
submissionNo: submission.submission_no,
|
||||
itemName: submission.item_name,
|
||||
system: submission.system,
|
||||
serverZone: submission.server_zone,
|
||||
series: submission.series,
|
||||
diamondLevel: submission.diamond_level,
|
||||
brillianceCount: submission.brilliance_count,
|
||||
askingPriceFen: submission.asking_price_fen,
|
||||
status: submission.status,
|
||||
workerId: submission.worker_id,
|
||||
workerName: submission.worker_name,
|
||||
workerPhone: submission.worker_phone,
|
||||
depositAmountFen: submission.deposit_amount_fen,
|
||||
createdAt: submission.created_at,
|
||||
}
|
||||
}
|
||||
|
||||
export async function listWorkerConsignments(workerId: number) {
|
||||
const [submissionsRes, inventoriesRes] = await Promise.all([
|
||||
listConsignmentSubmissions({ workerId, pageSize: 100 }),
|
||||
listConsignmentInventories({ workerId, pageSize: 100 }),
|
||||
])
|
||||
|
||||
return {
|
||||
submissions: submissionsRes.items.map((sub) => ({
|
||||
id: sub.id,
|
||||
submissionNo: sub.submission_no,
|
||||
itemName: sub.item_name,
|
||||
system: sub.system,
|
||||
serverZone: sub.server_zone,
|
||||
series: sub.series,
|
||||
diamondLevel: sub.diamond_level,
|
||||
brillianceCount: sub.brilliance_count,
|
||||
askingPriceFen: sub.asking_price_fen,
|
||||
status: sub.status,
|
||||
reviewNote: sub.review_note,
|
||||
resultInventoryId: sub.result_inventory_id,
|
||||
screenshotsJson: sub.screenshots_json,
|
||||
contentJson: sub.content_json,
|
||||
createdAt: sub.created_at,
|
||||
})),
|
||||
inventories: inventoriesRes.items.map((inv) => ({
|
||||
id: inv.id,
|
||||
inventoryNo: inv.inventory_no,
|
||||
itemName: inv.item_name,
|
||||
system: inv.system,
|
||||
serverZone: inv.server_zone,
|
||||
series: inv.series,
|
||||
diamondLevel: inv.diamond_level,
|
||||
brillianceCount: inv.brilliance_count,
|
||||
costPriceFen: inv.cost_price_fen,
|
||||
status: inv.status,
|
||||
allocationStatus: inv.allocation_status,
|
||||
boundPlatformOrderId: inv.bound_platform_order_id,
|
||||
depositAmountFen: inv.deposit_amount_fen,
|
||||
createdAt: inv.created_at,
|
||||
})),
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@ const WorkerHallPage = lazy(() => import('@/pages/worker/WorkerHallPage'))
|
||||
const WorkerLoginPage = lazy(() => import('@/pages/worker/WorkerLoginPage'))
|
||||
const WorkerOrdersPage = lazy(() => import('@/pages/worker/WorkerOrdersPage'))
|
||||
const WorkerAfterSalesPage = lazy(() => import('@/pages/worker/WorkerAfterSalesPage'))
|
||||
const WorkerConsignmentPage = lazy(() => import('@/pages/worker/WorkerConsignmentPage'))
|
||||
const WorkerProfilePage = lazy(() => import('@/pages/worker/WorkerProfilePage'))
|
||||
|
||||
function RequireAdmin() {
|
||||
@@ -149,6 +150,7 @@ function WorkerApplication() {
|
||||
<Route path="hall" element={<WorkerHallPage />} />
|
||||
<Route path="orders" element={<WorkerOrdersPage />} />
|
||||
<Route path="orders/after-sales" element={<WorkerAfterSalesPage />} />
|
||||
<Route path="consignment" element={<WorkerConsignmentPage />} />
|
||||
<Route path="profile" element={<WorkerProfilePage />} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
ContainerOutlined,
|
||||
DollarCircleOutlined,
|
||||
LogoutOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
ShoppingOutlined,
|
||||
@@ -31,12 +32,14 @@ const menuItems: MenuProps['items'] = [
|
||||
{ key: '/hall', icon: <ShoppingOutlined />, label: '抢单大厅' },
|
||||
{ key: '/orders', icon: <ContainerOutlined />, label: '我的订单' },
|
||||
{ key: '/orders/after-sales', icon: <SafetyCertificateOutlined />, label: '售后问题单' },
|
||||
{ key: '/consignment', icon: <DollarCircleOutlined />, label: '我要寄卖' },
|
||||
{ key: '/profile', icon: <UserOutlined />, label: '个人中心' },
|
||||
]
|
||||
|
||||
const tabItems = [
|
||||
{ key: '/hall', icon: ShoppingOutlined, label: '抢单大厅' },
|
||||
{ key: '/orders', icon: ContainerOutlined, label: '我的订单' },
|
||||
{ key: '/consignment', icon: DollarCircleOutlined, label: '我要寄卖' },
|
||||
{ key: '/profile', icon: UserOutlined, label: '个人中心' },
|
||||
]
|
||||
|
||||
@@ -84,6 +87,7 @@ export default function WorkerLayout() {
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{ key: '/consignment', icon: <DollarCircleOutlined />, label: '我要寄卖' },
|
||||
{ key: '/profile', icon: <UserOutlined />, label: '个人中心' },
|
||||
],
|
||||
[afterSalesPendingCount],
|
||||
@@ -552,6 +556,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
function resolveSelectedKey(pathname: string) {
|
||||
if (pathname.startsWith('/orders/after-sales')) return '/orders/after-sales'
|
||||
if (pathname.startsWith('/orders')) return '/orders'
|
||||
if (pathname.startsWith('/consignment')) return '/consignment'
|
||||
if (pathname.startsWith('/profile')) return '/profile'
|
||||
return '/hall'
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ export default function AdminWorkerPlatformPage() {
|
||||
},
|
||||
{
|
||||
key: 'consignment-submissions',
|
||||
label: '寄卖收集与审核',
|
||||
label: '寄卖提报审核',
|
||||
children: <ConsignmentSubmissionsPanel />,
|
||||
},
|
||||
{
|
||||
@@ -123,7 +123,7 @@ export default function AdminWorkerPlatformPage() {
|
||||
},
|
||||
{
|
||||
key: 'consignment-submissions',
|
||||
label: '寄卖收集与审核',
|
||||
label: '寄卖提报审核',
|
||||
children: <ConsignmentSubmissionsPanel />,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
toggleAdminConsignmentMatching,
|
||||
updateAdminConsignmentInventory,
|
||||
} from '@/services/admin/consignment'
|
||||
import { fetchAdminWorkerUsers } from '@/services/admin/worker-platform'
|
||||
import type { ConsignmentInventory } from '@/types/consignment'
|
||||
|
||||
const { Text } = Typography
|
||||
@@ -66,8 +67,6 @@ const SERIES_OPTIONS = [
|
||||
{ label: '暗潮', value: '暗潮' },
|
||||
{ label: '怒嚎', value: '怒嚎' },
|
||||
{ label: '血宴', value: '血宴' },
|
||||
{ label: '矩阵', value: '矩阵' },
|
||||
{ label: '其他', value: '其他' },
|
||||
]
|
||||
|
||||
const DIAMOND_OPTIONS = [
|
||||
@@ -78,6 +77,7 @@ const DIAMOND_OPTIONS = [
|
||||
{ label: '4 钻', value: 4 },
|
||||
{ label: '5 钻', value: 5 },
|
||||
{ label: '6 钻', value: 6 },
|
||||
{ label: '7 钻', value: 7 },
|
||||
]
|
||||
|
||||
const BUYER_COLORS: Record<string, string> = {
|
||||
@@ -113,6 +113,18 @@ export default function ConsignmentInventoryPanel() {
|
||||
const [inboundForm] = Form.useForm()
|
||||
const [editForm] = Form.useForm()
|
||||
|
||||
// 查询活跃打手列表用于指派与关联
|
||||
const workersQuery = useQuery({
|
||||
queryKey: ['admin-worker-users-consignment-options'],
|
||||
queryFn: () => fetchAdminWorkerUsers({ page: 1, pageSize: 200, status: 'active' }),
|
||||
})
|
||||
const workerOptions = (workersQuery.data?.data.items || []).map((w) => ({
|
||||
label: `${w.displayName || w.username} (${w.phone || '无手机号'}) [ID:${w.workerId}]`,
|
||||
value: w.workerId,
|
||||
phone: w.phone,
|
||||
name: w.displayName || w.username,
|
||||
}))
|
||||
|
||||
// 数据查询
|
||||
const inventoriesQuery = useQuery({
|
||||
queryKey: [
|
||||
@@ -171,6 +183,10 @@ export default function ConsignmentInventoryPanel() {
|
||||
const values = await inboundForm.validateFields()
|
||||
const count = Number(values.count || 1)
|
||||
const costPriceFen = Math.round(Number(values.costPrice || 0) * 100)
|
||||
const depositAmountFen = values.depositAmount
|
||||
? Math.round(Number(values.depositAmount) * 100)
|
||||
: undefined
|
||||
const selectedWorker = workerOptions.find((w) => w.value === values.workerId)
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
await createAdminConsignmentInventory({
|
||||
@@ -188,6 +204,10 @@ export default function ConsignmentInventoryPanel() {
|
||||
sellerGameId: values.sellerGameId,
|
||||
sellerPaymentInfo: values.sellerPaymentInfo,
|
||||
sellerPhone: values.sellerPhone,
|
||||
workerId: values.workerId || undefined,
|
||||
workerName: selectedWorker?.name || undefined,
|
||||
workerPhone: selectedWorker?.phone || undefined,
|
||||
depositAmountFen,
|
||||
credentialsJson: {
|
||||
loginMethod: values.loginMethod,
|
||||
account: values.account,
|
||||
@@ -217,6 +237,10 @@ export default function ConsignmentInventoryPanel() {
|
||||
try {
|
||||
const values = await editForm.validateFields()
|
||||
const costPriceFen = Math.round(Number(values.costPrice || 0) * 100)
|
||||
const depositAmountFen = values.depositAmount
|
||||
? Math.round(Number(values.depositAmount) * 100)
|
||||
: undefined
|
||||
const selectedWorker = workerOptions.find((w) => w.value === values.workerId)
|
||||
|
||||
await updateAdminConsignmentInventory(editingItem.id, {
|
||||
itemName: values.itemName,
|
||||
@@ -233,6 +257,10 @@ export default function ConsignmentInventoryPanel() {
|
||||
sellerGameId: values.sellerGameId,
|
||||
sellerPaymentInfo: values.sellerPaymentInfo,
|
||||
sellerPhone: values.sellerPhone,
|
||||
workerId: values.workerId || undefined,
|
||||
workerName: selectedWorker?.name || editingItem.workerName || undefined,
|
||||
workerPhone: selectedWorker?.phone || editingItem.workerPhone || undefined,
|
||||
depositAmountFen,
|
||||
credentialsJson: {
|
||||
loginMethod: values.loginMethod,
|
||||
account: values.account,
|
||||
@@ -381,18 +409,28 @@ export default function ConsignmentInventoryPanel() {
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '卖家信息',
|
||||
title: '卖家与供货打手',
|
||||
key: 'seller',
|
||||
width: 180,
|
||||
width: 200,
|
||||
render: (_, record) => (
|
||||
<div style={{ fontSize: 12 }}>
|
||||
{record.sellerContact ? <div>联系: {record.sellerContact}</div> : null}
|
||||
{record.sellerGameId ? <div>游戏ID: {record.sellerGameId}</div> : null}
|
||||
{record.sellerPaymentInfo ? (
|
||||
<Text type="secondary" ellipsis style={{ maxWidth: 160, display: 'inline-block' }}>
|
||||
收款: {record.sellerPaymentInfo}
|
||||
</Text>
|
||||
) : null}
|
||||
{record.sellerPhone ? <div>电话: {record.sellerPhone}</div> : null}
|
||||
{record.workerId ? (
|
||||
<div style={{ marginTop: 4 }}>
|
||||
<Tag color="blue">
|
||||
打手: {record.workerName || `ID:${record.workerId}`}
|
||||
{record.depositAmountFen
|
||||
? ` (押¥${(record.depositAmountFen / 100).toFixed(0)})`
|
||||
: ''}
|
||||
</Tag>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ marginTop: 4 }}>
|
||||
<Tag color="default">内部现货</Tag>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -471,6 +509,8 @@ export default function ConsignmentInventoryPanel() {
|
||||
editForm.setFieldsValue({
|
||||
...record,
|
||||
costPrice: record.costPriceFen / 100,
|
||||
workerId: record.workerId || undefined,
|
||||
depositAmount: record.depositAmountFen ? record.depositAmountFen / 100 : 100,
|
||||
loginMethod: (record.credentialsJson as any)?.loginMethod,
|
||||
account: (record.credentialsJson as any)?.account,
|
||||
password: (record.credentialsJson as any)?.password,
|
||||
@@ -734,8 +774,6 @@ export default function ConsignmentInventoryPanel() {
|
||||
{ label: '暗潮', value: '暗潮' },
|
||||
{ label: '怒嚎', value: '怒嚎' },
|
||||
{ label: '血宴', value: '血宴' },
|
||||
{ label: '矩阵', value: '矩阵' },
|
||||
{ label: '其他', value: '其他' },
|
||||
]}
|
||||
onChange={() => autoFillItemName(inboundForm)}
|
||||
/>
|
||||
@@ -751,6 +789,7 @@ export default function ConsignmentInventoryPanel() {
|
||||
{ label: '4 钻', value: 4 },
|
||||
{ label: '5 钻', value: 5 },
|
||||
{ label: '6 钻', value: 6 },
|
||||
{ label: '7 钻', value: 7 },
|
||||
]}
|
||||
onChange={() => autoFillItemName(inboundForm)}
|
||||
/>
|
||||
@@ -763,7 +802,7 @@ export default function ConsignmentInventoryPanel() {
|
||||
<Form.Item name="brillianceCount" label="璀璨数">
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={6}
|
||||
max={7}
|
||||
style={{ width: '100%' }}
|
||||
onChange={() => autoFillItemName(inboundForm)}
|
||||
/>
|
||||
@@ -829,6 +868,43 @@ export default function ConsignmentInventoryPanel() {
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={14}>
|
||||
<Form.Item
|
||||
name="workerId"
|
||||
label="走打手接单平台 (供货/履约打手)"
|
||||
tooltip="关联打手账号后,工单匹配推荐时将直接指派该打手接单丢枪并冻结押金"
|
||||
>
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
String(option?.label ?? '')
|
||||
.toLowerCase()
|
||||
.includes(input.toLowerCase())
|
||||
}
|
||||
placeholder="选择关联的接单平台打手账号 (选填)"
|
||||
options={workerOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={10}>
|
||||
<Form.Item
|
||||
name="depositAmount"
|
||||
label="履约押金金额 (元)"
|
||||
tooltip="指派该打手接单交付手枪时,平台自动冻结的押金金额(默认100元)"
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
step={10}
|
||||
addonBefore="¥"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="如 100"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item name="sellerPaymentInfo" label="卖家收款信息 (支付宝/微信号/银行卡)">
|
||||
<Input placeholder="如:支付宝 13800000000 (张三)" />
|
||||
</Form.Item>
|
||||
@@ -925,24 +1001,29 @@ export default function ConsignmentInventoryPanel() {
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={6}>
|
||||
<Col span={5}>
|
||||
<Form.Item name="system" label="系统">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Col span={5}>
|
||||
<Form.Item name="serverZone" label="区服">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Col span={5}>
|
||||
<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%' }} />
|
||||
<Col span={5}>
|
||||
<Form.Item name="diamondLevel" label="钻石等级 (1-7)">
|
||||
<InputNumber min={1} max={7} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Form.Item name="brillianceCount" label="璀璨数 (0-7)">
|
||||
<InputNumber min={0} max={7} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -983,6 +1064,43 @@ export default function ConsignmentInventoryPanel() {
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={14}>
|
||||
<Form.Item
|
||||
name="workerId"
|
||||
label="走打手接单平台 (供货/履约打手)"
|
||||
tooltip="关联后,工单匹配推荐时将直接指派该打手接单丢枪并冻结押金"
|
||||
>
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
String(option?.label ?? '')
|
||||
.toLowerCase()
|
||||
.includes(input.toLowerCase())
|
||||
}
|
||||
placeholder="选择关联的接单平台打手账号 (选填)"
|
||||
options={workerOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={10}>
|
||||
<Form.Item
|
||||
name="depositAmount"
|
||||
label="履约押金金额 (元)"
|
||||
tooltip="指派该打手接单交付手枪时,平台自动冻结的押金金额(默认100元)"
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
step={10}
|
||||
addonBefore="¥"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="如 100"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item name="sellerPaymentInfo" label="卖家收款信息">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
@@ -1064,6 +1182,23 @@ export default function ConsignmentInventoryPanel() {
|
||||
<Descriptions.Item label="卖家游戏ID" span={2}>
|
||||
<Text copyable>{viewingItem.sellerGameId || '-'}</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="供货/接单打手">
|
||||
{viewingItem.workerId ? (
|
||||
<Tag color="blue">
|
||||
{viewingItem.workerName || `ID:${viewingItem.workerId}`} (
|
||||
{viewingItem.workerPhone || '无电话'})
|
||||
</Tag>
|
||||
) : (
|
||||
<Text type="secondary">内部自营现货</Text>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="履约押金要求">
|
||||
{viewingItem.depositAmountFen ? (
|
||||
<Tag color="gold">¥{(viewingItem.depositAmountFen / 100).toFixed(0)}</Tag>
|
||||
) : (
|
||||
<Text type="secondary">按平台默认规则</Text>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="卖家收款信息" span={2}>
|
||||
<Text copyable>{viewingItem.sellerPaymentInfo || '-'}</Text>
|
||||
</Descriptions.Item>
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
CopyOutlined,
|
||||
EyeOutlined,
|
||||
LinkOutlined,
|
||||
PlusOutlined,
|
||||
ReloadOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { CheckCircleOutlined, CloseCircleOutlined, EyeOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import {
|
||||
Alert,
|
||||
App,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
@@ -25,49 +15,49 @@ import {
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Table,
|
||||
Tabs,
|
||||
Tag,
|
||||
Typography,
|
||||
App,
|
||||
} 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'
|
||||
import { fetchAdminWorkerUsers } from '@/services/admin/worker-platform'
|
||||
import type { 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 workersQuery = useQuery({
|
||||
queryKey: ['admin-worker-users-consignment-options'],
|
||||
queryFn: () => fetchAdminWorkerUsers({ page: 1, pageSize: 200, status: 'active' }),
|
||||
})
|
||||
const workerOptions = (workersQuery.data?.data.items || []).map((w) => ({
|
||||
label: `${w.displayName || w.username} (${w.phone || '无手机号'}) [ID:${w.workerId}]`,
|
||||
value: w.workerId,
|
||||
phone: w.phone,
|
||||
name: w.displayName || w.username,
|
||||
}))
|
||||
|
||||
const submissionsQuery = useQuery({
|
||||
queryKey: [
|
||||
'admin-consignment-submissions',
|
||||
@@ -85,83 +75,19 @@ export default function ConsignmentSubmissionsPanel() {
|
||||
}),
|
||||
})
|
||||
|
||||
// 查询链接列表
|
||||
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 || []
|
||||
const pendingCount = submissions.filter((s) => s.status === 'pending_review').length
|
||||
|
||||
// 复制收集公开链接
|
||||
function copyPublicLink(token: string) {
|
||||
const origin = window.location.origin
|
||||
const url = `${origin}/#/consignment-collect/${token}`
|
||||
void navigator.clipboard.writeText(url)
|
||||
message.success('收集链接已复制到剪贴板,可直接发给客户!')
|
||||
}
|
||||
|
||||
// 切换链接启用/关闭
|
||||
async function handleToggleLink(record: ConsignmentCollectLink, enabled: boolean) {
|
||||
try {
|
||||
await updateAdminConsignmentLink(record.id, { enabled })
|
||||
message.success(enabled ? '链接已开启' : '链接已关闭')
|
||||
await 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 {
|
||||
await navigator.clipboard.writeText(url)
|
||||
} catch {
|
||||
// clipboard access might be unavailable
|
||||
}
|
||||
|
||||
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)
|
||||
const depositAmountFen = values.depositAmount
|
||||
? Math.round(Number(values.depositAmount) * 100)
|
||||
: undefined
|
||||
const selectedWorker = workerOptions.find((w) => w.value === values.workerId)
|
||||
|
||||
await approveAdminConsignmentSubmission(approveModalItem.id, {
|
||||
costPriceFen,
|
||||
@@ -178,6 +104,10 @@ export default function ConsignmentSubmissionsPanel() {
|
||||
sellerGameId: values.sellerGameId,
|
||||
sellerPaymentInfo: values.sellerPaymentInfo,
|
||||
sellerPhone: values.sellerPhone,
|
||||
workerId: values.workerId || undefined,
|
||||
workerName: selectedWorker?.name || approveModalItem.workerName || undefined,
|
||||
workerPhone: selectedWorker?.phone || approveModalItem.workerPhone || undefined,
|
||||
depositAmountFen,
|
||||
reviewNote: values.reviewNote,
|
||||
})
|
||||
|
||||
@@ -190,7 +120,6 @@ export default function ConsignmentSubmissionsPanel() {
|
||||
}
|
||||
}
|
||||
|
||||
// 驳回提报
|
||||
async function handleRejectConfirm() {
|
||||
if (!rejectModalItem) return
|
||||
try {
|
||||
@@ -204,18 +133,22 @@ 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 '其他渠道'
|
||||
}
|
||||
|
||||
const submissionColumns: TableColumnsType<ConsignmentSubmission> = [
|
||||
{
|
||||
title: '提报单号',
|
||||
key: 'submissionNo',
|
||||
width: 170,
|
||||
width: 180,
|
||||
render: (_, record) => (
|
||||
<div>
|
||||
<Text strong>{record.submissionNo}</Text>
|
||||
{record.linkTitle ? (
|
||||
<div style={{ fontSize: 11, color: '#8c8c8c' }}>来自: {record.linkTitle}</div>
|
||||
) : null}
|
||||
<div style={{ fontSize: 11, color: '#8c8c8c' }}>{resolveSubmissionSource(record)}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -249,14 +182,24 @@ export default function ConsignmentSubmissionsPanel() {
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '卖家联系方式',
|
||||
title: '卖家与关联打手',
|
||||
key: 'contact',
|
||||
width: 180,
|
||||
width: 200,
|
||||
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}
|
||||
{record.workerId ? (
|
||||
<div style={{ marginTop: 4 }}>
|
||||
<Tag color="blue">
|
||||
打手: {record.workerName || `ID:${record.workerId}`}
|
||||
{record.depositAmountFen
|
||||
? ` (押¥${(record.depositAmountFen / 100).toFixed(0)})`
|
||||
: ''}
|
||||
</Tag>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -298,7 +241,7 @@ export default function ConsignmentSubmissionsPanel() {
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 160,
|
||||
width: 200,
|
||||
render: (_, record) => (
|
||||
<Space size={8}>
|
||||
<Button
|
||||
@@ -314,6 +257,7 @@ export default function ConsignmentSubmissionsPanel() {
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<CheckCircleOutlined />}
|
||||
onClick={() => {
|
||||
setApproveModalItem(record)
|
||||
approveForm.setFieldsValue({
|
||||
@@ -322,6 +266,10 @@ export default function ConsignmentSubmissionsPanel() {
|
||||
sellerContact: record.contactInfo,
|
||||
buyerName: '客服',
|
||||
inboundBatch: new Date().toISOString().slice(0, 7),
|
||||
workerId: record.workerId || undefined,
|
||||
depositAmount: record.depositAmountFen
|
||||
? record.depositAmountFen / 100
|
||||
: record.askingPriceFen / 100,
|
||||
})
|
||||
}}
|
||||
>
|
||||
@@ -331,6 +279,7 @@ export default function ConsignmentSubmissionsPanel() {
|
||||
size="small"
|
||||
danger
|
||||
type="text"
|
||||
icon={<CloseCircleOutlined />}
|
||||
onClick={() => {
|
||||
setRejectModalItem(record)
|
||||
setRejectNote('')
|
||||
@@ -345,334 +294,65 @@ export default function ConsignmentSubmissionsPanel() {
|
||||
},
|
||||
]
|
||||
|
||||
// 链接列表列定义
|
||||
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="玩家打开链接时看到的标题"
|
||||
<Card size="small">
|
||||
<Space wrap size={12}>
|
||||
<Radio.Group
|
||||
value={submissionStatus}
|
||||
onChange={(e) => {
|
||||
setSubmissionStatus(e.target.value)
|
||||
setSubmissionPage(1)
|
||||
}}
|
||||
buttonStyle="solid"
|
||||
>
|
||||
<Input placeholder="如:【高价收】苹果微信暗潮1钻 极速审核" />
|
||||
</Form.Item>
|
||||
<Radio.Button value="pending_review">
|
||||
待客服审核
|
||||
{pendingCount > 0 ? (
|
||||
<Badge count={pendingCount} size="small" offset={[6, -2]} />
|
||||
) : null}
|
||||
</Radio.Button>
|
||||
<Radio.Button value="approved">已审核入库</Radio.Button>
|
||||
<Radio.Button value="rejected">已驳回</Radio.Button>
|
||||
<Radio.Button value="all">全部</Radio.Button>
|
||||
</Radio.Group>
|
||||
|
||||
<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>
|
||||
<Input.Search
|
||||
placeholder="搜索提报单号/微信号/游戏ID/打手"
|
||||
allowClear
|
||||
style={{ width: 260 }}
|
||||
onSearch={(v) => {
|
||||
setSubmissionKeyword(v)
|
||||
setSubmissionPage(1)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Form.Item name="isMultiUse" label="允许多次提报" valuePropName="checked">
|
||||
<Switch defaultChecked checkedChildren="多人可填" unCheckedChildren="单次失效" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
<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>
|
||||
|
||||
{/* 审核入库弹窗 */}
|
||||
<Modal
|
||||
title="审核寄卖申请并正式入库"
|
||||
open={Boolean(approveModalItem)}
|
||||
@@ -716,24 +396,66 @@ export default function ConsignmentSubmissionsPanel() {
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={6}>
|
||||
<Col span={5}>
|
||||
<Form.Item name="system" label="系统">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Col span={5}>
|
||||
<Form.Item name="serverZone" label="区服">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Col span={5}>
|
||||
<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%' }} />
|
||||
<Col span={5}>
|
||||
<Form.Item name="diamondLevel" label="钻石等级 (1-7)">
|
||||
<InputNumber min={1} max={7} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Form.Item name="brillianceCount" label="璀璨数 (0-7)">
|
||||
<InputNumber min={0} max={7} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={14}>
|
||||
<Form.Item
|
||||
name="workerId"
|
||||
label="走打手接单平台 (关联履约打手)"
|
||||
tooltip="指派该打手接单丢枪交货时,接单平台将自动冻结其押金并担保交付"
|
||||
>
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
String(option?.label ?? '')
|
||||
.toLowerCase()
|
||||
.includes(input.toLowerCase())
|
||||
}
|
||||
placeholder="可选择关联的接单打手 (选填)"
|
||||
options={workerOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={10}>
|
||||
<Form.Item
|
||||
name="depositAmount"
|
||||
label="履约押金金额 (元)"
|
||||
tooltip="指派该打手交枪履约时,系统冻结的押金金额"
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
step={10}
|
||||
addonBefore="¥"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="如 100"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -788,7 +510,6 @@ export default function ConsignmentSubmissionsPanel() {
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 驳回弹窗 */}
|
||||
<Modal
|
||||
title="驳回寄卖提报"
|
||||
open={Boolean(rejectModalItem)}
|
||||
@@ -814,7 +535,6 @@ export default function ConsignmentSubmissionsPanel() {
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 提报详情查看弹窗 */}
|
||||
<Modal
|
||||
title={`提报详情 [${viewingSubmission?.submissionNo}]`}
|
||||
open={Boolean(viewingSubmission)}
|
||||
@@ -831,6 +551,9 @@ export default function ConsignmentSubmissionsPanel() {
|
||||
<Descriptions.Item label="提报单号" span={2}>
|
||||
<Text copyable>{viewingSubmission.submissionNo}</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="来源" span={2}>
|
||||
{resolveSubmissionSource(viewingSubmission)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="物品名称" span={2}>
|
||||
{viewingSubmission.itemName}
|
||||
</Descriptions.Item>
|
||||
@@ -853,6 +576,23 @@ export default function ConsignmentSubmissionsPanel() {
|
||||
<Descriptions.Item label="卖家游戏ID" span={2}>
|
||||
<Text copyable>{viewingSubmission.sellerGameId || '-'}</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="关联接单打手">
|
||||
{viewingSubmission.workerId ? (
|
||||
<Tag color="blue">
|
||||
{viewingSubmission.workerName || `ID:${viewingSubmission.workerId}`} (
|
||||
{viewingSubmission.workerPhone || '无手机号'})
|
||||
</Tag>
|
||||
) : (
|
||||
<Text type="secondary">未关联打手账号</Text>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="履约押金要求">
|
||||
{viewingSubmission.depositAmountFen ? (
|
||||
<Tag color="gold">¥{(viewingSubmission.depositAmountFen / 100).toFixed(0)}</Tag>
|
||||
) : (
|
||||
<Text type="secondary">按平台默认规则</Text>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="卖家收款方式" span={2}>
|
||||
<Text copyable>{viewingSubmission.sellerPaymentInfo || '-'}</Text>
|
||||
</Descriptions.Item>
|
||||
@@ -894,56 +634,6 @@ export default function ConsignmentSubmissionsPanel() {
|
||||
</Descriptions>
|
||||
) : null}
|
||||
</Modal>
|
||||
|
||||
{/* 链接生成成功提示弹窗 */}
|
||||
<Modal
|
||||
title="收集链接生成成功"
|
||||
open={Boolean(createdLinkUrl)}
|
||||
onCancel={() => setCreatedLinkUrl('')}
|
||||
footer={[
|
||||
<Button
|
||||
key="copy"
|
||||
type="primary"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => {
|
||||
void 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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ import {
|
||||
updateAdminWorkOrderSharing,
|
||||
} from '@/services/admin'
|
||||
import {
|
||||
assignWorkOrderWithConsignmentWorker,
|
||||
checkWorkOrderConsignmentMatches,
|
||||
fulfillWorkOrderWithConsignment,
|
||||
revokeWorkOrderConsignmentFulfillment,
|
||||
@@ -723,6 +724,24 @@ export default function WorkOrdersPanel() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAssignWithConsignmentWorker(inventoryId: number, workerId?: number | null) {
|
||||
if (!consignmentInterceptModal.order) return
|
||||
setConsignmentInterceptModal((prev) => ({ ...prev, submitting: true }))
|
||||
try {
|
||||
await assignWorkOrderWithConsignmentWorker({
|
||||
workOrderId: consignmentInterceptModal.order.workOrderId,
|
||||
inventoryId,
|
||||
workerId,
|
||||
})
|
||||
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,
|
||||
@@ -2879,6 +2898,16 @@ export default function WorkOrdersPanel() {
|
||||
</strong>
|
||||
</span>
|
||||
<span>收购人: {inv.buyerName || '-'}</span>
|
||||
{inv.workerName ? (
|
||||
<Tag color="purple">
|
||||
供货打手: {inv.workerName} ({inv.workerPhone || '无手机号'})
|
||||
</Tag>
|
||||
) : null}
|
||||
{inv.depositAmountFen != null ? (
|
||||
<Tag color="warning">
|
||||
履约押金: ¥{(inv.depositAmountFen / 100).toFixed(2)}
|
||||
</Tag>
|
||||
) : null}
|
||||
{inv.shopName ? <span>店铺: {inv.shopName}</span> : null}
|
||||
{inv.inboundBatch ? <span>批次: {inv.inboundBatch}</span> : null}
|
||||
</Space>
|
||||
@@ -2890,13 +2919,25 @@ export default function WorkOrdersPanel() {
|
||||
</div>
|
||||
|
||||
<div style={{ marginLeft: 16 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={consignmentInterceptModal.submitting}
|
||||
onClick={() => handleFulfillWithConsignment(inv.id)}
|
||||
>
|
||||
使用此库存直接履约
|
||||
</Button>
|
||||
<Space direction="vertical" align="end" size={8}>
|
||||
{inv.workerId ? (
|
||||
<Button
|
||||
type="primary"
|
||||
style={{ backgroundColor: '#722ed1', borderColor: '#722ed1' }}
|
||||
loading={consignmentInterceptModal.submitting}
|
||||
onClick={() => handleAssignWithConsignmentWorker(inv.id, inv.workerId)}
|
||||
>
|
||||
走接单平台指派打手(冻结押金交付)
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
type={inv.workerId ? 'default' : 'primary'}
|
||||
loading={consignmentInterceptModal.submitting}
|
||||
onClick={() => handleFulfillWithConsignment(inv.id)}
|
||||
>
|
||||
内部现货直接核销
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
CloudUploadOutlined,
|
||||
LockOutlined,
|
||||
DollarOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
TrophyOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import {
|
||||
@@ -15,7 +16,6 @@ import {
|
||||
Image,
|
||||
Input,
|
||||
InputNumber,
|
||||
Radio,
|
||||
Result,
|
||||
Row,
|
||||
Select,
|
||||
@@ -48,9 +48,38 @@ export default function ConsignmentCollectPage() {
|
||||
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('缺少收集凭证')
|
||||
@@ -66,15 +95,16 @@ export default function ConsignmentCollectPage() {
|
||||
form.setFieldsValue({
|
||||
itemName:
|
||||
data.title ||
|
||||
`${data.system}${data.serverZone}${data.series}${data.diamondLevel}钻${briStr}`,
|
||||
`${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,
|
||||
loginMethod: 'wechat_qrcode',
|
||||
depositAmount: data.presetPriceFen > 0 ? data.presetPriceFen / 100 : 100,
|
||||
})
|
||||
setDepositTouched(false)
|
||||
})
|
||||
.catch((err: any) => {
|
||||
setErrorMsg(err.message || '获取收集信息失败或链接已失效')
|
||||
@@ -86,15 +116,36 @@ export default function ConsignmentCollectPage() {
|
||||
|
||||
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 screenshots = fileList
|
||||
.filter((f) => f.status === 'done' && (f.response?.url || f.url))
|
||||
.map((f) => f.response?.url || f.url)
|
||||
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,
|
||||
itemName:
|
||||
values.itemName ||
|
||||
`${values.system}${values.serverZone}${values.series}${values.diamondLevel}钻苏尔南`,
|
||||
system: values.system,
|
||||
serverZone: values.serverZone,
|
||||
series: values.series,
|
||||
@@ -103,22 +154,21 @@ export default function ConsignmentCollectPage() {
|
||||
sellerContact: values.sellerContact,
|
||||
contactInfo: values.sellerContact,
|
||||
sellerPhone: values.sellerPhone,
|
||||
sellerGameId: values.sellerGameId,
|
||||
sellerPaymentInfo: values.sellerPaymentInfo,
|
||||
sellerGameId: `${values.characterName} (${values.characterId})`,
|
||||
sellerPaymentInfo: '走接单平台钱包结算',
|
||||
askingPriceFen,
|
||||
depositAmountFen,
|
||||
screenshotsJson: screenshots,
|
||||
contentJson: {
|
||||
loginMethod: values.loginMethod,
|
||||
account: values.account,
|
||||
password: values.password,
|
||||
authNote: values.authNote,
|
||||
characterName: values.characterName,
|
||||
characterId: values.characterId,
|
||||
sellerNote: values.sellerNote,
|
||||
},
|
||||
}
|
||||
|
||||
const res = await submitPublicConsignment(token, payload)
|
||||
setSubmittedResult(res.data)
|
||||
message.success('寄卖申请提交成功!')
|
||||
message.success('苏尔南手枪寄卖登记提交成功!')
|
||||
} catch (err: any) {
|
||||
message.error(err.message || '提交失败,请重试')
|
||||
} finally {
|
||||
@@ -128,16 +178,16 @@ export default function ConsignmentCollectPage() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ padding: '60px 20px', textAlign: 'center' }}>
|
||||
<div style={{ padding: '80px 20px', textAlign: 'center' }}>
|
||||
<Spin size="large" />
|
||||
<div style={{ marginTop: 16, color: '#8c8c8c' }}>正在加载寄卖登记信息...</div>
|
||||
<div style={{ marginTop: 16, color: '#8c8c8c' }}>正在加载苏尔南手枪寄卖登记信息...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (errorMsg) {
|
||||
return (
|
||||
<div style={{ maxWidth: 600, margin: '40px auto', padding: '0 16px' }}>
|
||||
<div style={{ maxWidth: 600, margin: '60px auto', padding: '0 16px' }}>
|
||||
<Result
|
||||
status="warning"
|
||||
title="链接无法访问"
|
||||
@@ -150,24 +200,27 @@ export default function ConsignmentCollectPage() {
|
||||
|
||||
if (submittedResult) {
|
||||
return (
|
||||
<div style={{ maxWidth: 600, margin: '40px auto', padding: '0 16px' }}>
|
||||
<div style={{ maxWidth: 640, margin: '40px auto', padding: '0 16px' }}>
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{ boxShadow: '0 4px 12px rgba(0,0,0,0.05)', borderRadius: 12 }}
|
||||
style={{ boxShadow: '0 4px 16px rgba(0,0,0,0.06)', borderRadius: 16 }}
|
||||
>
|
||||
<Result
|
||||
status="success"
|
||||
title="寄卖登记提交成功!"
|
||||
title="苏尔南手枪寄售登记成功!"
|
||||
subTitle={
|
||||
<div>
|
||||
<div>
|
||||
您的登记单号:
|
||||
<Text strong copyable>
|
||||
<div style={{ fontSize: 15, margin: '8px 0' }}>
|
||||
登记单号:
|
||||
<Text strong copyable style={{ color: '#1890ff' }}>
|
||||
{submittedResult.submissionNo}
|
||||
</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
客服将在核实账号及物品品质后,通过您填写的联系方式与您确认交接并打款。
|
||||
<div style={{ marginTop: 12, color: '#595959', lineHeight: 1.7 }}>
|
||||
您的寄售手枪已进入<strong>打手接单平台寄售池</strong>。
|
||||
<br />
|
||||
匹配到买家工单后,系统将通过接单平台指派您进行局内交付,并自动执行
|
||||
<strong>押金保障机制</strong>。交付验收后货款将直达您的账户。
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -186,7 +239,7 @@ export default function ConsignmentCollectPage() {
|
||||
<div
|
||||
style={{
|
||||
minHeight: '100vh',
|
||||
background: '#f5f7fa',
|
||||
background: '#f0f2f5',
|
||||
padding: '24px 16px 60px',
|
||||
}}
|
||||
>
|
||||
@@ -195,26 +248,26 @@ export default function ConsignmentCollectPage() {
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
borderRadius: 16,
|
||||
marginBottom: 16,
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.04)',
|
||||
background: 'linear-gradient(135deg, #1890ff 0%, #096dd9 100%)',
|
||||
boxShadow: '0 4px 14px rgba(24, 144, 255, 0.15)',
|
||||
background: 'linear-gradient(135deg, #096dd9 0%, #1890ff 50%, #40a9ff 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' }} />
|
||||
<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 || '和平精英 装备/账号 寄卖登记'}
|
||||
{linkInfo?.title || '苏尔南手枪 · 走打手接单平台寄卖'}
|
||||
</Title>
|
||||
</div>
|
||||
<Paragraph style={{ color: 'rgba(255,255,255,0.85)', margin: 0, fontSize: 13 }}>
|
||||
平台保障 · 极速审核 · 验货即打款 · 请如实填写以下信息
|
||||
<Paragraph style={{ color: 'rgba(255,255,255,0.92)', margin: 0, fontSize: 13.5 }}>
|
||||
平台保障 · 走接单平台押金履约 · 验货即打款 · 杜绝私下跑单
|
||||
</Paragraph>
|
||||
|
||||
{linkInfo ? (
|
||||
<div style={{ marginTop: 8, display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
||||
<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}
|
||||
@@ -232,22 +285,55 @@ export default function ConsignmentCollectPage() {
|
||||
</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: 12,
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.04)',
|
||||
borderRadius: 16,
|
||||
boxShadow: '0 2px 10px rgba(0,0,0,0.04)',
|
||||
}}
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
||||
<Title level={5} style={{ marginBottom: 16 }}>
|
||||
1. 物品与规格确认
|
||||
</Title>
|
||||
<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 }]}>
|
||||
<Form.Item
|
||||
name="system"
|
||||
label="操作系统"
|
||||
rules={[{ required: true, message: '请选择操作系统' }]}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ label: '苹果 (iOS)', value: '苹果' },
|
||||
@@ -258,7 +344,11 @@ export default function ConsignmentCollectPage() {
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="serverZone" label="游戏区服" rules={[{ required: true }]}>
|
||||
<Form.Item
|
||||
name="serverZone"
|
||||
label="游戏区服"
|
||||
rules={[{ required: true, message: '请选择游戏区服' }]}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ label: '微信区', value: '微信区' },
|
||||
@@ -271,20 +361,26 @@ export default function ConsignmentCollectPage() {
|
||||
|
||||
<Row gutter={12}>
|
||||
<Col span={8}>
|
||||
<Form.Item name="series" label="系列/枪系" rules={[{ required: true }]}>
|
||||
<Form.Item
|
||||
name="series"
|
||||
label="系列/枪系"
|
||||
rules={[{ required: true, message: '请选择枪系' }]}
|
||||
>
|
||||
<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 }]}>
|
||||
<Form.Item
|
||||
name="diamondLevel"
|
||||
label="钻石等级 (1-7)"
|
||||
rules={[{ required: true, message: '请选择钻石等级' }]}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ label: '1 钻', value: 1 },
|
||||
@@ -293,89 +389,138 @@ export default function ConsignmentCollectPage() {
|
||||
{ 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="璀璨数">
|
||||
<InputNumber min={0} max={6} style={{ width: '100%' }} />
|
||||
<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>
|
||||
|
||||
<Form.Item
|
||||
name="itemName"
|
||||
label="物品/账号名称"
|
||||
rules={[{ required: true, message: '请输入物品名称' }]}
|
||||
>
|
||||
<Input placeholder="如:苹果微信暗潮1钻" />
|
||||
</Form.Item>
|
||||
<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>
|
||||
|
||||
<Form.Item
|
||||
name="askingPrice"
|
||||
label="期望转让/收购价格 (元)"
|
||||
rules={[{ required: true, message: '请填写出价' }]}
|
||||
{/* 2. 卖家联系方式与游戏凭据 */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
margin: '24px 0 16px',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
paddingBottom: 8,
|
||||
}}
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
step={1}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="请输入您的心理价位 (元)"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Title level={5} style={{ margin: '24px 0 16px' }}>
|
||||
2. 卖家联系与收款方式
|
||||
</Title>
|
||||
<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' }]}
|
||||
rules={[{ required: true, message: '请填写微信号或QQ' }]}
|
||||
>
|
||||
<Input placeholder="方便客服核实联系" />
|
||||
<Input placeholder="方便客服核实与通知" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="sellerPhone" label="联系电话">
|
||||
<Input placeholder="手机号 (选填)" />
|
||||
<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
|
||||
name="sellerGameId"
|
||||
label="游戏内角色名 / 角色数字ID"
|
||||
rules={[{ required: true, message: '请填写游戏内角色名称或ID' }]}
|
||||
label="游戏主页截图 (必须包含角色名、ID及仓库/手枪详情)"
|
||||
required
|
||||
tooltip="请上传游戏个人主页或背包手枪品质截图,供客服审核真实性"
|
||||
>
|
||||
<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}
|
||||
@@ -393,41 +538,18 @@ export default function ConsignmentCollectPage() {
|
||||
>
|
||||
{fileList.length < 4 ? (
|
||||
<div>
|
||||
<CloudUploadOutlined style={{ fontSize: 20, color: '#1890ff' }} />
|
||||
<div style={{ marginTop: 6, fontSize: 12 }}>上传截图</div>
|
||||
<CloudUploadOutlined style={{ fontSize: 22, 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="如有其他装备搭配或说明可在此补充" />
|
||||
<Input.TextArea
|
||||
rows={2}
|
||||
placeholder="如有手枪配件、子弹、涂装或在线时间段说明,可在此备注"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Button
|
||||
@@ -436,14 +558,16 @@ export default function ConsignmentCollectPage() {
|
||||
size="large"
|
||||
block
|
||||
loading={submitting}
|
||||
icon={<DollarOutlined />}
|
||||
style={{
|
||||
marginTop: 16,
|
||||
height: 48,
|
||||
fontSize: 16,
|
||||
borderRadius: 8,
|
||||
borderRadius: 10,
|
||||
background: 'linear-gradient(135deg, #1890ff 0%, #096dd9 100%)',
|
||||
}}
|
||||
>
|
||||
提交寄卖申请并登记
|
||||
提交苏尔南手枪寄售登记
|
||||
</Button>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
@@ -0,0 +1,739 @@
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
CloudUploadOutlined,
|
||||
DollarCircleOutlined,
|
||||
HistoryOutlined,
|
||||
InboxOutlined,
|
||||
PlusOutlined,
|
||||
ReloadOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import {
|
||||
Alert,
|
||||
App,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Divider,
|
||||
Empty,
|
||||
Form,
|
||||
Image,
|
||||
Input,
|
||||
InputNumber,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Tabs,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import ImageUpload from '@/components/files/ImageUpload'
|
||||
import {
|
||||
fetchWorkerConsignments,
|
||||
fetchWorkerProfile,
|
||||
publishWorkerConsignment,
|
||||
} from '@/services/worker'
|
||||
import type { WorkerConsignmentItem, WorkerInventoryItem } from '@/services/worker'
|
||||
import type { UploadedFile } from '@/types/worker-platform'
|
||||
import { useIsMobile } from '@/utils/use-is-mobile'
|
||||
|
||||
const SERIES_OPTIONS = [
|
||||
{ label: '暗潮', value: '暗潮' },
|
||||
{ label: '怒嚎', value: '怒嚎' },
|
||||
{ label: '血宴', value: '血宴' },
|
||||
]
|
||||
|
||||
const DIAMOND_OPTIONS = [1, 2, 3, 4, 5, 6, 7].map((v) => ({
|
||||
label: `${v} 钻`,
|
||||
value: v,
|
||||
}))
|
||||
|
||||
const BRILLIANCE_OPTIONS = [
|
||||
{ label: '无璀璨 (0)', value: 0 },
|
||||
...[1, 2, 3, 4, 5, 6, 7].map((v) => ({ label: `${v} 璀璨`, value: v })),
|
||||
]
|
||||
|
||||
type RecordFilter = 'all' | 'pending' | 'in_stock' | 'done'
|
||||
|
||||
export default function WorkerConsignmentPage() {
|
||||
const isMobile = useIsMobile()
|
||||
const { message } = 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({
|
||||
queryKey: ['worker-profile'],
|
||||
queryFn: () => fetchWorkerProfile(),
|
||||
staleTime: 60_000,
|
||||
})
|
||||
const worker = profileQuery.data?.data.worker
|
||||
|
||||
const myConsignmentsQuery = useQuery({
|
||||
queryKey: ['worker-my-consignments'],
|
||||
queryFn: () => fetchWorkerConsignments(),
|
||||
})
|
||||
|
||||
const submissions = myConsignmentsQuery.data?.data.submissions || []
|
||||
const inventories = myConsignmentsQuery.data?.data.inventories || []
|
||||
|
||||
const pendingCount = submissions.filter((s) => s.status === 'pending_review').length
|
||||
const inStockCount = inventories.filter((i) => i.status === 'in_stock').length
|
||||
const doneCount = inventories.filter((i) => i.status === 'consumed').length
|
||||
const rejectedCount = submissions.filter((s) => s.status === 'rejected').length
|
||||
|
||||
useEffect(() => {
|
||||
if (worker?.phone) {
|
||||
form.setFieldsValue({ sellerPhone: worker.phone })
|
||||
}
|
||||
}, [worker, form])
|
||||
|
||||
function handleFormValuesChange(changedValues: Record<string, unknown>) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePublishSubmit(values: Record<string, any>) {
|
||||
const urls = screenshots.map((f) => f.url).filter(Boolean)
|
||||
if (urls.length === 0) {
|
||||
message.error('请上传游戏主页截图以供核验')
|
||||
return
|
||||
}
|
||||
|
||||
const askingPriceFen = Math.round(Number(values.askingPrice || 0) * 100)
|
||||
if (askingPriceFen < 100) {
|
||||
message.error('寄售价格不能少于1元')
|
||||
return
|
||||
}
|
||||
|
||||
const depositAmountFen = Math.round(
|
||||
Number(values.depositAmount || values.askingPrice || 100) * 100,
|
||||
)
|
||||
if (depositAmountFen < 100) {
|
||||
message.error('履约押金不能少于1元')
|
||||
return
|
||||
}
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await publishWorkerConsignment({
|
||||
itemName: values.itemName,
|
||||
system: values.system,
|
||||
serverZone: values.serverZone,
|
||||
series: values.series,
|
||||
diamondLevel: Number(values.diamondLevel || 1),
|
||||
brillianceCount: Number(values.brillianceCount || 0),
|
||||
askingPriceFen,
|
||||
sellerContact: values.sellerContact,
|
||||
sellerPhone: values.sellerPhone || worker?.phone || '',
|
||||
characterName: values.characterName,
|
||||
characterId: values.characterId,
|
||||
screenshotsJson: urls,
|
||||
sellerNote: values.sellerNote,
|
||||
depositAmountFen,
|
||||
})
|
||||
|
||||
message.success('寄卖提报成功,已进入审核')
|
||||
form.resetFields()
|
||||
setDepositTouched(false)
|
||||
setScreenshots([])
|
||||
await myConsignmentsQuery.refetch()
|
||||
setRecordFilter('pending')
|
||||
setActiveTab('records')
|
||||
} catch (err: any) {
|
||||
message.error(err.message || '发布寄卖失败,请重试')
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const filteredRecords = useMemo(() => {
|
||||
const pendingSubs = submissions.filter((s) => s.status === 'pending_review')
|
||||
const rejectedSubs = submissions.filter((s) => s.status === 'rejected')
|
||||
const approvedSubs = submissions.filter((s) => s.status === 'approved')
|
||||
const inStock = inventories.filter((i) => i.status === 'in_stock')
|
||||
const done = inventories.filter((i) => i.status === 'consumed' || i.status === 'archived')
|
||||
|
||||
if (recordFilter === 'pending') {
|
||||
return { submissions: [...pendingSubs, ...rejectedSubs], inventories: [] as WorkerInventoryItem[] }
|
||||
}
|
||||
if (recordFilter === 'in_stock') {
|
||||
return { submissions: [] as WorkerConsignmentItem[], inventories: inStock }
|
||||
}
|
||||
if (recordFilter === 'done') {
|
||||
return { submissions: [] as WorkerConsignmentItem[], inventories: done }
|
||||
}
|
||||
return {
|
||||
submissions: [...pendingSubs, ...rejectedSubs, ...approvedSubs],
|
||||
inventories,
|
||||
}
|
||||
}, [submissions, inventories, recordFilter])
|
||||
|
||||
const recordTotal =
|
||||
filteredRecords.submissions.length + filteredRecords.inventories.length
|
||||
|
||||
return (
|
||||
<section className="page-stack worker-consignment-page">
|
||||
<div className="worker-orders-desktop-header">
|
||||
<div className="worker-orders-header-left">
|
||||
<Typography.Title level={3} className="worker-orders-heading">
|
||||
苏尔南寄卖
|
||||
</Typography.Title>
|
||||
<span className="worker-orders-live-badge">
|
||||
<span className="worker-orders-pulse-dot" />
|
||||
接单平台押金履约
|
||||
</span>
|
||||
</div>
|
||||
<div className="worker-orders-header-right">
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={myConsignmentsQuery.isFetching}
|
||||
onClick={() => myConsignmentsQuery.refetch()}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="worker-orders-kpi-grid">
|
||||
<div
|
||||
className={`worker-orders-kpi-card ${recordFilter === 'pending' && activeTab === 'records' ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
setRecordFilter('pending')
|
||||
setActiveTab('records')
|
||||
}}
|
||||
>
|
||||
<div className="worker-orders-kpi-top">
|
||||
<div className="worker-orders-kpi-icon-wrap pending">
|
||||
<ClockCircleOutlined />
|
||||
</div>
|
||||
<div className="worker-orders-kpi-info">
|
||||
<span className="worker-orders-kpi-title">审核中 / 驳回</span>
|
||||
<strong className="worker-orders-kpi-val">
|
||||
{pendingCount + rejectedCount}
|
||||
<span className="worker-orders-kpi-unit"> 单</span>
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="worker-orders-kpi-bottom">
|
||||
<span className="worker-orders-kpi-sub-label">
|
||||
{pendingCount > 0 ? `${pendingCount} 单待客服审核` : '暂无待审提报'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`worker-orders-kpi-card ${recordFilter === 'in_stock' && activeTab === 'records' ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
setRecordFilter('in_stock')
|
||||
setActiveTab('records')
|
||||
}}
|
||||
>
|
||||
<div className="worker-orders-kpi-top">
|
||||
<div className="worker-orders-kpi-icon-wrap in-progress">
|
||||
<InboxOutlined />
|
||||
</div>
|
||||
<div className="worker-orders-kpi-info">
|
||||
<span className="worker-orders-kpi-title">在库待派单</span>
|
||||
<strong className="worker-orders-kpi-val">
|
||||
{inStockCount}
|
||||
<span className="worker-orders-kpi-unit"> 把</span>
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="worker-orders-kpi-bottom">
|
||||
<span className="worker-orders-kpi-sub-label">已入库,等待匹配买家工单</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`worker-orders-kpi-card ${recordFilter === 'done' && activeTab === 'records' ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
setRecordFilter('done')
|
||||
setActiveTab('records')
|
||||
}}
|
||||
>
|
||||
<div className="worker-orders-kpi-top">
|
||||
<div className="worker-orders-kpi-icon-wrap accepted">
|
||||
<CheckCircleOutlined />
|
||||
</div>
|
||||
<div className="worker-orders-kpi-info">
|
||||
<span className="worker-orders-kpi-title">已履约出库</span>
|
||||
<strong className="worker-orders-kpi-val">
|
||||
{doneCount}
|
||||
<span className="worker-orders-kpi-unit"> 把</span>
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="worker-orders-kpi-bottom">
|
||||
<span className="worker-orders-kpi-sub-label">押金履约交付完成</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`worker-orders-kpi-card ${activeTab === 'publish' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('publish')}
|
||||
>
|
||||
<div className="worker-orders-kpi-top">
|
||||
<div className="worker-orders-kpi-icon-wrap problem">
|
||||
<PlusOutlined />
|
||||
</div>
|
||||
<div className="worker-orders-kpi-info">
|
||||
<span className="worker-orders-kpi-title">发布寄卖</span>
|
||||
<strong className="worker-orders-kpi-val" style={{ fontSize: 18 }}>
|
||||
苏尔南
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="worker-orders-kpi-bottom">
|
||||
<span className="worker-orders-kpi-sub-label">暗潮 / 怒嚎 / 血宴 · 押金履约</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card bordered={false} className="worker-orders-card">
|
||||
<Tabs
|
||||
className="worker-order-tabs"
|
||||
activeKey={activeTab}
|
||||
onChange={(key) => setActiveTab(key as 'publish' | 'records')}
|
||||
items={[
|
||||
{
|
||||
key: 'publish',
|
||||
label: (
|
||||
<span>
|
||||
<DollarCircleOutlined /> 发布寄卖
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
className="worker-consignment-form"
|
||||
onFinish={handlePublishSubmit}
|
||||
onValuesChange={handleFormValuesChange}
|
||||
>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
icon={<SafetyCertificateOutlined />}
|
||||
className="worker-consignment-tip"
|
||||
message="走接单平台押金履约:审核入库后匹配买家工单,系统按登记押金冻结钱包;局内交枪验货通过后解冻并结算货款。"
|
||||
/>
|
||||
|
||||
<Divider orientation="left" plain className="worker-consignment-divider">
|
||||
规格与价格
|
||||
</Divider>
|
||||
|
||||
<Row gutter={[12, 0]}>
|
||||
<Col xs={12} sm={8} md={6}>
|
||||
<Form.Item
|
||||
name="system"
|
||||
label="操作系统"
|
||||
rules={[{ required: true, message: '请选择系统' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择"
|
||||
options={[
|
||||
{ label: '苹果 (iOS)', value: '苹果' },
|
||||
{ label: '安卓 (Android)', value: '安卓' },
|
||||
{ label: '通用/双端', value: '通用' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={12} sm={8} md={6}>
|
||||
<Form.Item
|
||||
name="serverZone"
|
||||
label="游戏区服"
|
||||
rules={[{ required: true, message: '请选择区服' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择"
|
||||
options={[
|
||||
{ label: '微信区', value: '微信区' },
|
||||
{ label: 'QQ区', value: 'QQ区' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={12} sm={8} md={6}>
|
||||
<Form.Item
|
||||
name="series"
|
||||
label="系列/枪系"
|
||||
rules={[{ required: true, message: '请选择枪系' }]}
|
||||
>
|
||||
<Select placeholder="请选择" options={SERIES_OPTIONS} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={12} sm={8} md={6}>
|
||||
<Form.Item
|
||||
name="diamondLevel"
|
||||
label="钻石等级"
|
||||
rules={[{ required: true, message: '请选择钻级' }]}
|
||||
>
|
||||
<Select placeholder="请选择" options={DIAMOND_OPTIONS} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={12} sm={8} md={6}>
|
||||
<Form.Item
|
||||
name="brillianceCount"
|
||||
label="璀璨数"
|
||||
rules={[{ required: true, message: '请选择璀璨数' }]}
|
||||
>
|
||||
<Select placeholder="请选择" options={BRILLIANCE_OPTIONS} />
|
||||
</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}>
|
||||
<Form.Item
|
||||
name="askingPrice"
|
||||
label="寄售价格 (元)"
|
||||
rules={[{ required: true, message: '请填写寄售价格' }]}
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
step={1}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="请输入"
|
||||
addonBefore="¥"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={12} sm={8} md={6}>
|
||||
<Form.Item
|
||||
name="depositAmount"
|
||||
label="履约押金 (元)"
|
||||
rules={[{ required: true, message: '请填写履约押金' }]}
|
||||
tooltip="派单交枪时冻结,验货后解冻。填写寄售价后可自动带出,也可手动改。"
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
step={10}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="请输入"
|
||||
addonBefore="¥"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Divider orientation="left" plain className="worker-consignment-divider">
|
||||
联系方式与游戏凭据
|
||||
</Divider>
|
||||
|
||||
<Row gutter={[12, 0]}>
|
||||
<Col xs={24} sm={12}>
|
||||
<Form.Item
|
||||
name="sellerContact"
|
||||
label="微信号 / QQ号"
|
||||
rules={[{ required: true, message: '请填写微信号或QQ' }]}
|
||||
>
|
||||
<Input placeholder="方便派单与紧急联系" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} sm={12}>
|
||||
<Form.Item
|
||||
name="sellerPhone"
|
||||
label="联系电话"
|
||||
tooltip="默认使用当前接单账号绑定手机号"
|
||||
>
|
||||
<Input placeholder="手机号" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} sm={12}>
|
||||
<Form.Item
|
||||
name="characterName"
|
||||
label="游戏内角色名"
|
||||
rules={[{ required: true, message: '请填写角色名' }]}
|
||||
>
|
||||
<Input placeholder="局内加好友丢枪用" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} sm={12}>
|
||||
<Form.Item
|
||||
name="characterId"
|
||||
label="角色数字ID"
|
||||
rules={[{ required: true, message: '请填写数字ID' }]}
|
||||
>
|
||||
<Input placeholder="如 1089238472" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
label="游戏主页截图"
|
||||
required
|
||||
extra="须包含角色名、ID 及仓库/手枪详情,最多 4 张"
|
||||
>
|
||||
<ImageUpload
|
||||
value={screenshots}
|
||||
onChange={setScreenshots}
|
||||
scene="consignment_screenshot"
|
||||
scope="worker"
|
||||
maxCount={4}
|
||||
showPasteHint={!isMobile}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item name="sellerNote" label="其他留言与补充">
|
||||
<Input.TextArea
|
||||
rows={2}
|
||||
placeholder="配件、子弹、涂装或在线时段等(选填)"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div className="worker-consignment-actions">
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
size="large"
|
||||
loading={submitting}
|
||||
icon={<CloudUploadOutlined />}
|
||||
>
|
||||
提交寄卖提报
|
||||
</Button>
|
||||
<Typography.Text type="secondary">
|
||||
提交后进入客服审核,通过后入库匹配
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</Form>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'records',
|
||||
label: (
|
||||
<span>
|
||||
<HistoryOutlined /> 我的寄卖
|
||||
{submissions.length + inventories.length > 0 ? (
|
||||
<span className="worker-order-tab-count">
|
||||
{submissions.length + inventories.length}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<Space direction="vertical" size={14} style={{ width: '100%' }}>
|
||||
<div className="worker-consignment-filter-row">
|
||||
{(
|
||||
[
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: 'pending', label: '审核中' },
|
||||
{ key: 'in_stock', label: '在库' },
|
||||
{ key: 'done', label: '已出库' },
|
||||
] as const
|
||||
).map((item) => (
|
||||
<Button
|
||||
key={item.key}
|
||||
size="small"
|
||||
type={recordFilter === item.key ? 'primary' : 'default'}
|
||||
onClick={() => setRecordFilter(item.key)}
|
||||
>
|
||||
{item.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{myConsignmentsQuery.isLoading ? (
|
||||
<Typography.Text type="secondary">正在加载寄卖记录…</Typography.Text>
|
||||
) : recordTotal === 0 ? (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description="暂无寄卖记录,去发布一单苏尔南吧"
|
||||
>
|
||||
<Button type="primary" onClick={() => setActiveTab('publish')}>
|
||||
发布寄卖
|
||||
</Button>
|
||||
</Empty>
|
||||
) : (
|
||||
<div className="worker-consignment-record-list">
|
||||
{filteredRecords.inventories.map((inv) => (
|
||||
<InventoryRecordCard key={`inv-${inv.id}`} inv={inv} />
|
||||
))}
|
||||
{filteredRecords.submissions.map((sub) => (
|
||||
<SubmissionRecordCard key={`sub-${sub.id}`} sub={sub} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function SpecTags({
|
||||
system,
|
||||
serverZone,
|
||||
series,
|
||||
diamondLevel,
|
||||
brillianceCount,
|
||||
}: {
|
||||
system: string
|
||||
serverZone: string
|
||||
series: string
|
||||
diamondLevel: number
|
||||
brillianceCount: number
|
||||
}) {
|
||||
return (
|
||||
<Space size={4} wrap>
|
||||
<Tag>{system}</Tag>
|
||||
<Tag>{serverZone}</Tag>
|
||||
<Tag color="orange">{series}</Tag>
|
||||
<Tag color="gold">{diamondLevel} 钻</Tag>
|
||||
{brillianceCount > 0 ? <Tag color="magenta">{brillianceCount} 璀璨</Tag> : null}
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
|
||||
function InventoryRecordCard({ inv }: { inv: WorkerInventoryItem }) {
|
||||
return (
|
||||
<article className="worker-consignment-record-card">
|
||||
<div className="worker-consignment-record-main">
|
||||
<div className="worker-consignment-record-title-row">
|
||||
<Typography.Text strong>{inv.itemName}</Typography.Text>
|
||||
<Typography.Text type="secondary" className="worker-consignment-record-no">
|
||||
{inv.inventoryNo}
|
||||
</Typography.Text>
|
||||
{inv.status === 'in_stock' ? (
|
||||
<Tag color="green">在库待派</Tag>
|
||||
) : inv.status === 'consumed' ? (
|
||||
<Tag color="blue">已履约</Tag>
|
||||
) : (
|
||||
<Tag>已下架</Tag>
|
||||
)}
|
||||
</div>
|
||||
<SpecTags
|
||||
system={inv.system}
|
||||
serverZone={inv.serverZone}
|
||||
series={inv.series}
|
||||
diamondLevel={inv.diamondLevel}
|
||||
brillianceCount={inv.brillianceCount}
|
||||
/>
|
||||
<div className="worker-consignment-record-meta">
|
||||
<span>
|
||||
货款 <strong>¥{(inv.costPriceFen / 100).toFixed(2)}</strong>
|
||||
</span>
|
||||
<span>
|
||||
押金 <strong>¥{((inv.depositAmountFen || 0) / 100).toFixed(0)}</strong>
|
||||
</span>
|
||||
{inv.boundPlatformOrderId ? (
|
||||
<Typography.Text copyable={{ text: inv.boundPlatformOrderId }}>
|
||||
履约单 {inv.boundPlatformOrderId}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<Typography.Text type="secondary" className="worker-consignment-record-time">
|
||||
{inv.createdAt}
|
||||
</Typography.Text>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
function SubmissionRecordCard({ sub }: { sub: WorkerConsignmentItem }) {
|
||||
const shots = Array.isArray(sub.screenshotsJson)
|
||||
? sub.screenshotsJson.filter((s): s is string => typeof s === 'string')
|
||||
: []
|
||||
|
||||
return (
|
||||
<article className="worker-consignment-record-card">
|
||||
<div className="worker-consignment-record-main">
|
||||
<div className="worker-consignment-record-title-row">
|
||||
<Typography.Text strong>{sub.itemName}</Typography.Text>
|
||||
<Typography.Text type="secondary" className="worker-consignment-record-no">
|
||||
{sub.submissionNo}
|
||||
</Typography.Text>
|
||||
{sub.status === 'pending_review' ? (
|
||||
<Tag color="gold">审核中</Tag>
|
||||
) : sub.status === 'approved' ? (
|
||||
<Tag color="green">已通过</Tag>
|
||||
) : (
|
||||
<Tag color="red">已驳回</Tag>
|
||||
)}
|
||||
</div>
|
||||
<SpecTags
|
||||
system={sub.system}
|
||||
serverZone={sub.serverZone}
|
||||
series={sub.series}
|
||||
diamondLevel={sub.diamondLevel}
|
||||
brillianceCount={sub.brillianceCount}
|
||||
/>
|
||||
<div className="worker-consignment-record-meta">
|
||||
<span>
|
||||
期望价 <strong>¥{(sub.askingPriceFen / 100).toFixed(2)}</strong>
|
||||
</span>
|
||||
{sub.reviewNote ? (
|
||||
<Typography.Text type="danger">审核说明:{sub.reviewNote}</Typography.Text>
|
||||
) : null}
|
||||
</div>
|
||||
{shots.length > 0 ? (
|
||||
<div className="worker-consignment-record-shots">
|
||||
<Image.PreviewGroup>
|
||||
<Space size={8}>
|
||||
{shots.map((src, i) => (
|
||||
<Image
|
||||
key={`${src}-${i}`}
|
||||
src={src}
|
||||
width={44}
|
||||
height={44}
|
||||
style={{ objectFit: 'cover', borderRadius: 6 }}
|
||||
/>
|
||||
))}
|
||||
</Space>
|
||||
</Image.PreviewGroup>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<Typography.Text type="secondary" className="worker-consignment-record-time">
|
||||
{sub.createdAt}
|
||||
</Typography.Text>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
@@ -134,6 +134,17 @@ export function fulfillWorkOrderWithConsignment(payload: {
|
||||
)
|
||||
}
|
||||
|
||||
export function assignWorkOrderWithConsignmentWorker(payload: {
|
||||
workOrderId: number | string
|
||||
inventoryId: number | string
|
||||
workerId?: number | null
|
||||
}) {
|
||||
return apiPost<{ workOrder: unknown; inventory: ConsignmentInventory }>(
|
||||
'/api/v1/admin/worker-platform/consignment/assign-worker',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function revokeWorkOrderConsignmentFulfillment(payload: {
|
||||
workOrderId: number | string
|
||||
action?: 'reopen' | 'cancel_order' | 'archive_inventory'
|
||||
|
||||
@@ -352,3 +352,67 @@ export function submitCollectOrder(payload: {
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------- 打手端:我要寄卖与手枪库存 ----------------
|
||||
|
||||
export type WorkerConsignmentPublishPayload = {
|
||||
itemName?: string
|
||||
system: string
|
||||
serverZone: string
|
||||
series: string
|
||||
diamondLevel: number
|
||||
brillianceCount: number
|
||||
askingPriceFen: number
|
||||
sellerContact: string
|
||||
sellerPhone?: string
|
||||
characterName: string
|
||||
characterId: string
|
||||
screenshotsJson: string[]
|
||||
sellerNote?: string
|
||||
depositAmountFen?: number
|
||||
}
|
||||
|
||||
export type WorkerConsignmentItem = {
|
||||
id: number | string
|
||||
submissionNo: string
|
||||
itemName: string
|
||||
system: string
|
||||
serverZone: string
|
||||
series: string
|
||||
diamondLevel: number
|
||||
brillianceCount: number
|
||||
askingPriceFen: number
|
||||
status: 'pending_review' | 'approved' | 'rejected'
|
||||
reviewNote?: string
|
||||
resultInventoryId?: number | string | null
|
||||
screenshotsJson: string[]
|
||||
contentJson: Record<string, unknown>
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export type WorkerInventoryItem = {
|
||||
id: number | string
|
||||
inventoryNo: string
|
||||
itemName: string
|
||||
system: string
|
||||
serverZone: string
|
||||
series: string
|
||||
diamondLevel: number
|
||||
brillianceCount: number
|
||||
costPriceFen: number
|
||||
status: 'in_stock' | 'consumed' | 'archived'
|
||||
allocationStatus: string
|
||||
boundPlatformOrderId: string
|
||||
depositAmountFen?: number
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export function publishWorkerConsignment(payload: WorkerConsignmentPublishPayload) {
|
||||
return apiPost<WorkerConsignmentItem>('/api/v1/worker/consignment/publish', payload)
|
||||
}
|
||||
|
||||
export function fetchWorkerConsignments() {
|
||||
return apiGet<{ submissions: WorkerConsignmentItem[]; inventories: WorkerInventoryItem[] }>(
|
||||
'/api/v1/worker/consignment/my-items',
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4210,3 +4210,121 @@ body {
|
||||
.worker-profile-details-card {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
/* ===== 苏尔南寄卖页(对齐订单/售后视觉) ===== */
|
||||
.worker-consignment-page .worker-orders-card {
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.worker-consignment-tip {
|
||||
margin-bottom: 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.worker-consignment-divider.ant-divider {
|
||||
margin: 8px 0 16px;
|
||||
border-color: #e2e8f0;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.worker-consignment-form .ant-form-item {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.worker-consignment-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 4px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.worker-consignment-filter-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.worker-consignment-record-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.worker-consignment-record-card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.worker-consignment-record-main {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.worker-consignment-record-title-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.worker-consignment-record-no {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.worker-consignment-record-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px 16px;
|
||||
font-size: 13px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.worker-consignment-record-meta strong {
|
||||
color: #cf1322;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.worker-consignment-record-time {
|
||||
flex: 0 0 auto;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.worker-consignment-record-shots {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.worker-consignment-record-card {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.worker-consignment-record-time {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.worker-consignment-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.worker-consignment-actions .ant-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,10 @@ export type ConsignmentInventory = {
|
||||
boundPlatformOrderId: string
|
||||
sourceType: 'manual' | 'collection_link' | 'recycle'
|
||||
sourceSubmissionId: number | null
|
||||
workerId?: number | null
|
||||
workerName?: string
|
||||
workerPhone?: string
|
||||
depositAmountFen?: number
|
||||
inboundAt: string
|
||||
consumedAt: string | null
|
||||
createdAt: string
|
||||
@@ -80,6 +84,10 @@ export type ConsignmentSubmission = {
|
||||
contentJson: Record<string, unknown>
|
||||
screenshotsJson: string[]
|
||||
askingPriceFen: number
|
||||
workerId?: number | null
|
||||
workerName?: string
|
||||
workerPhone?: string
|
||||
depositAmountFen?: number
|
||||
status: 'pending_review' | 'approved' | 'rejected'
|
||||
reviewNote: string
|
||||
reviewerName: string
|
||||
|
||||
Reference in New Issue
Block a user