4 Commits
Author SHA1 Message Date
yml2213 e5cc7cadef style: 格式化后端工单查询与展示映射模块代码风格 2026-09-19 14:41:16 +08:00
yml2213 f08a221e15 feat(worker-platform): 接单平台工单详情抽屉支持卖家与买家资料一键复制与单项复制
1. 寄售赠送进度卡片右上角新增【一键复制卖家资料】按钮,一键聚合复制卖家角色昵称、游戏ID、区服、系统、联系方式(微信/QQ/手机)、提报/仓库单号及备注;
2. 资料信息卡片右上角新增【一键复制买家资料】按钮,一键复制买家提交的全部表单字段;
3. 卖家资料与买家资料的各项字段值(角色名、数字ID、区服、系统、联系方式、单号等)右侧均补充内联一键复制小图标,支持快捷单项提取。
2026-09-19 14:39:43 +08:00
yml2213 2c120bd3e5 fix(worker-platform): 接单平台工单详情抽屉完整展示寄售手动匹配与指派订单的卖家资料
1. 修复管理后台「接单平台 -> 接单工单」抽屉中,手动匹配或指派寄售库存履约的工单因 booster_material_json 为空导致卖家资料全部显示 '-' 的问题;
2. 后端在履约及指派时将卖家角色昵称、数字ID、联系方式、手机号、备注及上架凭证截图完整写入工单 material.consignment;
3. 后端工单查询及展示映射层增加自动回退与批量库存补充解析机制,确保历史与新增的寄售工单均能准确解析出卖家角色名、ID、区服、系统与截图;
4. 前端工单抽屉卡片优化寄售订单的资料展示与阶段标签,清晰展示卖家角色名、游戏ID、联系方式(微信/QQ/手机)、提报/仓库单号及上架截图。
2026-09-19 14:33:56 +08:00
yml2213 13a9028c66 feat(worker-platform): 发布寄卖与我的寄卖界面支持停留状态与筛选持久化记忆
1. 在 WorkerConsignmentPage 中通过 localStorage 与 URL 查询参数实时双向同步 activeTab 与 recordFilter;
2. 打手在切换发布寄卖/我的寄卖或筛选状态后,刷新页面或切换路由返回时自动恢复上次停留界面;
3. 提交寄卖成功后自动切换并记住跳转到我的寄卖-待审核列表。
2026-09-19 14:23:51 +08:00
8 changed files with 593 additions and 131 deletions
@@ -333,6 +333,27 @@ export async function getConsignmentInventoryById(
return res.rows[0] || null
}
export async function listConsignmentInventoriesByIds(
ids: Array<number | string>,
): Promise<ConsignmentInventoryRow[]> {
const numericIds = [...new Set(ids.map(Number).filter((n) => Number.isFinite(n) && n > 0))]
if (numericIds.length === 0) return []
const sql = `
SELECT
ci.*,
wc.name AS category_name,
cs.submission_no AS source_submission_no,
cs.screenshots_json AS submission_screenshots_json
FROM consignment_inventories ci
LEFT JOIN work_categories wc ON wc.id = ci.category_id
LEFT JOIN consignment_submissions cs ON cs.id = ci.source_submission_id
WHERE ci.id = ANY($1::bigint[])
`
const res = await query<ConsignmentInventoryRow>(sql, [numericIds])
return res.rows
}
export async function updateConsignmentInventory(
id: number | string,
input: {
@@ -25,3 +25,4 @@ export * from './worker-cancel-request-repo.js'
export * from './after-sales-repo.js'
export * from './product-match-repo.js'
export * from './sms-code-repo.js'
export * from './consignment-repo.js'
@@ -1,15 +1,18 @@
import {
getWorkOrderStatistics,
listConsignmentInventoriesByIds,
listWorkOrderEventsByOrderId,
listWorkOrderSharesByOrderIds,
listWorkOrders,
sumPendingUnfreezeByOrderIds,
type ConsignmentInventoryRow,
} from '../../repositories/worker-platform/index.js'
import type { JsonObject } from '../../types/json.js'
import {
normalizeDateString,
normalizePage,
normalizePageSize,
safeParseJson,
} from '../admin/admin-query-utils.js'
import {
mapWorkOrderAdmin,
@@ -60,10 +63,29 @@ export async function listAdminWorkOrders(query: JsonObject = {}) {
getWorkOrderStatistics(filters),
])
const itemIds = items.map((item) => Number(item.id))
const [pendingUnfreezeByOrderId, shares] = await Promise.all([
const inventoryIds = new Set<number>()
for (const item of items) {
const mat = safeParseJson(item.material_json)
const invId = Number(mat.consignment?.inventoryId || 0)
if (invId > 0) {
inventoryIds.add(invId)
}
}
const [pendingUnfreezeByOrderId, shares, inventories] = await Promise.all([
sumPendingUnfreezeByOrderIds(itemIds),
listWorkOrderSharesByOrderIds(itemIds),
inventoryIds.size > 0
? listConsignmentInventoriesByIds([...inventoryIds])
: Promise.resolve([]),
])
const inventoriesById = new Map<number, ConsignmentInventoryRow>()
for (const inv of inventories) {
inventoriesById.set(Number(inv.id), inv)
}
const sharesByOrderId = new Map<number, typeof shares>()
for (const share of shares) {
const orderId = Number(share.work_order_id)
@@ -72,10 +94,15 @@ export async function listAdminWorkOrders(query: JsonObject = {}) {
sharesByOrderId.set(orderId, current)
}
return {
items: items.map((item) => ({
...mapWorkOrderAdmin(item, sharesByOrderId.get(Number(item.id)) || []),
pendingUnfreezeAmount: pendingUnfreezeByOrderId.get(Number(item.id)) || 0,
})),
items: items.map((item) => {
const mat = safeParseJson(item.material_json)
const invId = Number(mat.consignment?.inventoryId || 0)
const inv = invId > 0 ? inventoriesById.get(invId) || null : null
return {
...mapWorkOrderAdmin(item, sharesByOrderId.get(Number(item.id)) || [], inv),
pendingUnfreezeAmount: pendingUnfreezeByOrderId.get(Number(item.id)) || 0,
}
}),
pagination: { page, pageSize, total: statistics.totalCount },
statistics,
}
@@ -462,8 +462,15 @@ export async function fulfillWorkOrderWithConsignment(input: {
diamondLevel: inv.diamond_level,
brillianceCount: inv.brilliance_count,
buyerName: inv.buyer_name,
sellerGameId: inv.seller_game_id || '',
sellerContact: inv.seller_contact || '',
sellerPhone: inv.seller_phone || '',
workerName: inv.worker_name || '',
workerPhone: inv.worker_phone || '',
remark: inv.remark || '',
inboundBatch: inv.inbound_batch,
credentials: safeParseJson(inv.credentials_json),
screenshots: safeParseJson(inv.screenshots_json || inv.submission_screenshots_json || []),
costPriceFen: inv.cost_price_fen,
sourceSubmissionId: inv.source_submission_id ? Number(inv.source_submission_id) : null,
sourceSubmissionNo: inv.source_submission_no || '',
@@ -694,11 +701,22 @@ export async function assignWorkOrderWithConsignmentWorker(input: {
diamondLevel: claimed.diamond_level,
brillianceCount: claimed.brilliance_count,
costPriceFen: claimed.cost_price_fen,
buyerName: claimed.buyer_name,
sellerGameId: claimed.seller_game_id || '',
sellerContact: claimed.seller_contact || '',
sellerPhone: claimed.seller_phone || '',
workerId: Number(targetWorkerId),
workerName: claimed.worker_name || '',
workerPhone: claimed.worker_phone || '',
remark: claimed.remark || '',
credentials: safeParseJson(claimed.credentials_json),
screenshots: safeParseJson(
claimed.screenshots_json || claimed.submission_screenshots_json || [],
),
sourceSubmissionId: claimed.source_submission_id
? Number(claimed.source_submission_id)
: null,
sourceSubmissionNo: claimed.source_submission_no || '',
fulfillmentType: 'worker_deposit',
assignedAt: now,
assignedBy: input.actorName,
@@ -10,6 +10,7 @@ import {
WORK_ORDER_GIFT_PHASE,
} from '../../domain/work-order-acceptance-mode.js'
import {
type ConsignmentInventoryRow,
type WorkCategoryRow,
type WorkOrderCapabilities,
type WorkOrderRow,
@@ -76,9 +77,101 @@ export function mapWorkOrderBoosterMaterial(value: unknown) {
screenshots: normalizeUploadedFiles(material.screenshots).map(refreshUploadedFileUrls),
note: String(material.note || ''),
submittedAt: String(material.submittedAt || ''),
isConsignmentSellerMaterial: false,
}
}
/** 读取好友赠送模式的打手本人资料或寄售卖家资料;截图访问链接需要读取时重签。 */
export function resolveWorkOrderEffectiveBoosterMaterial(
workOrder: WorkOrderRow,
consignmentInventory?: ConsignmentInventoryRow | null,
) {
const existingBooster = safeParseJson(workOrder.booster_material_json)
const existingFields =
existingBooster &&
typeof existingBooster.fields === 'object' &&
!Array.isArray(existingBooster.fields)
? (existingBooster.fields as Record<string, string>)
: {}
const hasExistingFields = Object.values(existingFields).some(
(v) => String(v || '').trim().length > 0,
)
const existingScreenshots = normalizeUploadedFiles(existingBooster.screenshots).map(
refreshUploadedFileUrls,
)
const hasExistingScreenshots = existingScreenshots.length > 0
if (hasExistingFields || hasExistingScreenshots) {
return {
fields: existingFields,
screenshots: existingScreenshots,
note: String(existingBooster.note || ''),
submittedAt: String(existingBooster.submittedAt || ''),
isConsignmentSellerMaterial: false,
}
}
// Fallback to consignment seller information if this is a consignment-fulfilled order
const material = safeParseJson(workOrder.material_json)
const consignment = safeParseJson(material.consignment)
const hasConsignment =
(consignment && typeof consignment === 'object' && Object.keys(consignment).length > 0) ||
Boolean(consignmentInventory)
if (hasConsignment) {
const rawScreenshots =
consignment?.screenshots ||
consignmentInventory?.screenshots_json ||
consignmentInventory?.submission_screenshots_json ||
[]
const fields: Record<string, string> = {
gameNickname: String(
consignment?.buyerName ||
consignmentInventory?.buyer_name ||
consignment?.sellerGameNickname ||
'',
).trim(),
gameId: String(
consignment?.sellerGameId || consignmentInventory?.seller_game_id || '',
).trim(),
serverZone: String(consignment?.serverZone || consignmentInventory?.server_zone || '').trim(),
system: String(consignment?.system || consignmentInventory?.system || '').trim(),
sellerContact: String(
consignment?.sellerContact || consignmentInventory?.seller_contact || '',
).trim(),
sellerPhone: String(
consignment?.sellerPhone || consignmentInventory?.seller_phone || '',
).trim(),
sourceSubmissionNo: String(
consignment?.sourceSubmissionNo || consignmentInventory?.source_submission_no || '',
).trim(),
inventoryNo: String(
consignment?.inventoryNo || consignmentInventory?.inventory_no || '',
).trim(),
sourceType: 'consignment',
}
return {
fields,
screenshots: normalizeUploadedFiles(rawScreenshots).map(refreshUploadedFileUrls),
note: String(
consignment?.remark || consignmentInventory?.remark || consignment?.matchNote || '',
).trim(),
submittedAt: String(
consignment?.fulfilledAt ||
consignment?.assignedAt ||
consignmentInventory?.inbound_at ||
consignmentInventory?.created_at ||
workOrder.created_at ||
'',
),
isConsignmentSellerMaterial: true,
}
}
return mapWorkOrderBoosterMaterial(workOrder.booster_material_json)
}
/** 统一计算后台使用的工单业务状态,避免拼单订单继续直接暴露母工单 open。 */
export function resolveWorkOrderOperationalStatus(workOrder: WorkOrderRow): string {
const activeShareQuantity = Math.max(0, Number(workOrder.active_share_quantity || 0))
@@ -131,7 +224,11 @@ function resolveWorkOrderCancelSource(cancelEventType: string | null | undefined
return eventType || '未知来源'
}
export function mapWorkOrderAdmin(workOrder: WorkOrderRow, shares?: WorkOrderShareRow[]) {
export function mapWorkOrderAdmin(
workOrder: WorkOrderRow,
shares?: WorkOrderShareRow[],
consignmentInventory?: ConsignmentInventoryRow | null,
) {
const activeShareQuantity = Math.max(0, Number(workOrder.active_share_quantity || 0))
const pendingShareCount = Math.max(0, Number(workOrder.pending_share_count || 0))
const sharingTotalQuantity = Math.max(1, Number(workOrder.sharing_total_quantity || 1))
@@ -245,7 +342,10 @@ export function mapWorkOrderAdmin(workOrder: WorkOrderRow, shares?: WorkOrderSha
cooldownMinutes: normalizeWorkOrderGiftCooldownMinutes(workOrder.gift_cooldown_minutes),
friendAddedAt: workOrder.friend_added_at,
giftAvailableAt: workOrder.gift_available_at,
boosterMaterial: mapWorkOrderBoosterMaterial(workOrder.booster_material_json),
boosterMaterial: resolveWorkOrderEffectiveBoosterMaterial(
workOrder,
consignmentInventory,
),
},
}
: {}),
@@ -1,5 +1,6 @@
import {
CheckOutlined,
CopyOutlined,
DeleteOutlined,
EditOutlined,
FormOutlined,
@@ -177,6 +178,16 @@ function isInternalConsignmentOrder(row: WorkOrder) {
return Boolean(consignment.inventoryId) && !row.worker
}
function isConsignmentOrder(row: WorkOrder) {
const consignment = asRecord((row.material as Record<string, unknown> | undefined)?.consignment)
return Boolean(
consignment.inventoryId ||
consignment.inventoryNo ||
consignment.fulfillmentType ||
row.gift?.boosterMaterial?.fields?.inventoryNo,
)
}
/** 工单 `material.consignment`(寄售履约信息);没有寄售履约信息时返回 null。 */
function getConsignmentFulfillment(row: WorkOrder): ConsignmentFulfillmentInfo | null {
const consignment = asRecord((row.material as Record<string, unknown> | undefined)?.consignment)
@@ -1285,6 +1296,63 @@ export default function WorkOrdersPanel() {
})
}
function copyBuyerMaterialInfo(order: WorkOrder) {
const items = getMaterialDetailItems(order).filter((item) => String(item.value || '').trim())
if (items.length === 0) {
message.warning('当前工单暂无买家资料')
return
}
const lines = items.map((item) => `${item.label}: ${item.value}`)
navigator.clipboard
.writeText(lines.join('\n'))
.then(() => message.success('买家资料已复制到剪贴板'))
.catch(() => message.error('复制失败,请手动复制'))
}
function copyConsignmentSellerInfo(order: WorkOrder) {
const consignmentInfo = getConsignmentFulfillment(order)
const sellerFields = order.gift?.boosterMaterial?.fields || {}
const gameNickname = String(
sellerFields.gameNickname || consignmentInfo?.buyerName || '',
).trim()
const gameId = String(sellerFields.gameId || consignmentInfo?.sellerGameId || '').trim()
const serverZone = String(sellerFields.serverZone || consignmentInfo?.serverZone || '').trim()
const system = String(sellerFields.system || consignmentInfo?.system || '').trim()
const sellerContact = String(
sellerFields.sellerContact || consignmentInfo?.sellerContact || '',
).trim()
const sellerPhone = String(
sellerFields.sellerPhone || consignmentInfo?.sellerPhone || '',
).trim()
const sourceSubmissionNo = String(
sellerFields.sourceSubmissionNo || consignmentInfo?.sourceSubmissionNo || '',
).trim()
const inventoryNo = String(
sellerFields.inventoryNo || consignmentInfo?.inventoryNo || '',
).trim()
const note = String(order.gift?.boosterMaterial?.note || consignmentInfo?.remark || '').trim()
const lines: string[] = []
if (gameNickname) lines.push(`卖家角色昵称: ${gameNickname}`)
if (gameId) lines.push(`卖家游戏ID: ${gameId}`)
if (serverZone) lines.push(`区服: ${serverZone}`)
if (system) lines.push(`系统: ${system}`)
if (sellerContact) lines.push(`卖家微信/QQ: ${sellerContact}`)
if (sellerPhone) lines.push(`卖家手机: ${sellerPhone}`)
if (sourceSubmissionNo) lines.push(`提报单号: ${sourceSubmissionNo}`)
if (inventoryNo) lines.push(`仓库单号: ${inventoryNo}`)
if (note) lines.push(`卖家备注: ${note}`)
if (lines.length === 0) {
message.warning('当前工单暂无可复制的卖家资料')
return
}
navigator.clipboard
.writeText(lines.join('\n'))
.then(() => message.success('卖家资料已复制到剪贴板'))
.catch(() => message.error('复制失败,请手动复制'))
}
const columns: TableColumnsType<WorkOrder> = [
{
title: '订单',
@@ -2083,7 +2151,19 @@ export default function WorkOrdersPanel() {
</Descriptions>
</Card>
<Card title="资料信息" size="small">
<Card
title="资料信息"
size="small"
extra={
<Button
size="small"
icon={<CopyOutlined />}
onClick={() => copyBuyerMaterialInfo(detailOrder)}
>
一键复制买家资料
</Button>
}
>
{renderDetailFieldValues(getMaterialDetailItems(detailOrder), '当前还没有填写资料')}
<div style={{ marginTop: 12 }}>
<Typography.Text type="secondary">买家主页截图</Typography.Text>
@@ -2093,112 +2173,256 @@ export default function WorkOrdersPanel() {
</div>
</Card>
{detailOrder.acceptanceMode === 'friend_gift' && detailOrder.gift ? (
<Card
title={
isInternalConsignmentOrder(detailOrder) ? '内部寄售赠送进度' : '好友赠送进度'
}
size="small"
extra={
<Space>
{isInternalConsignmentOrder(detailOrder) ? (
<Tag color="cyan">内部寄售匹配</Tag>
) : (
<Tag color="purple">{formatAcceptanceMode(detailOrder.acceptanceMode)}</Tag>
)}
<Tag color={resolveGiftPhaseColor(detailOrder.gift.phase)}>
{formatGiftPhase(
detailOrder.gift.phase,
isInternalConsignmentOrder(detailOrder),
)}
</Tag>
{detailOrder.capabilities?.canConfirmFriend && canManageProblem ? (
<Button
type="primary"
size="small"
onClick={() => confirmFriendAdded(detailOrder)}
>
确认加好友
</Button>
) : null}
{detailOrder.capabilities?.canCompleteGiftCooldown && canManageProblem ? (
<Button
type="primary"
size="small"
onClick={() => completeGiftCooldown(detailOrder)}
>
结束冷却
</Button>
) : null}
{detailOrder.capabilities?.canResetGiftFlow && canManageProblem ? (
<Button
size="small"
danger
onClick={() => requestResetGiftMaterial(detailOrder)}
>
重置打手资料
</Button>
) : null}
</Space>
}
>
<div className="worker-order-detail-stack">
<Descriptions column={{ xs: 1, md: 2 }} bordered size="small">
<Descriptions.Item label="冷却时长">
{detailOrder.gift.cooldownMinutes > 0
? `${detailOrder.gift.cooldownMinutes} 分钟`
: `${detailOrder.gift.cooldownHours} 小时`}
</Descriptions.Item>
<Descriptions.Item label="打手资料提交">
{formatAdminDateTime(detailOrder.gift.boosterMaterial?.submittedAt)}
</Descriptions.Item>
<Descriptions.Item label="已加好友">
{formatAdminDateTime(detailOrder.gift.friendAddedAt)}
</Descriptions.Item>
<Descriptions.Item label="可赠送时间">
{detailOrder.gift.giftAvailableAt
? formatAdminDateTime(detailOrder.gift.giftAvailableAt)
: '-'}
</Descriptions.Item>
</Descriptions>
<Descriptions column={{ xs: 1, md: 2 }} bordered size="small">
{(
[
['gameNickname', '游戏昵称'],
['gameId', '游戏ID'],
['serverZone', '区服'],
['system', '系统'],
] as const
).map(([key, label]) => (
<Descriptions.Item key={key} label={label}>
<span className="worker-order-detail-value">
{String(detailOrder.gift?.boosterMaterial?.fields?.[key] || '').trim() ||
'-'}
</span>
</Descriptions.Item>
))}
<Descriptions.Item label="打手备注" span={2}>
<span className="worker-order-detail-value">
{String(detailOrder.gift?.boosterMaterial?.note || '').trim() || '-'}
</span>
</Descriptions.Item>
</Descriptions>
<div>
<Typography.Text type="secondary">打手主页截图</Typography.Text>
<div style={{ marginTop: 8 }}>
{detailOrder.gift.boosterMaterial?.screenshots?.length ? (
<ImagePreviewList
files={detailOrder.gift.boosterMaterial.screenshots}
size={72}
/>
) : (
<Typography.Text type="secondary">未提交</Typography.Text>
)}
</div>
</div>
</div>
</Card>
) : null}
{detailOrder.acceptanceMode === 'friend_gift' && detailOrder.gift
? (() => {
const isConsignment =
isInternalConsignmentOrder(detailOrder) || isConsignmentOrder(detailOrder)
const consignmentInfo = getConsignmentFulfillment(detailOrder)
const sellerFields = detailOrder.gift.boosterMaterial?.fields || {}
const sellerNickname = String(
sellerFields.gameNickname || consignmentInfo?.buyerName || '',
).trim()
const sellerGameId = String(
sellerFields.gameId || consignmentInfo?.sellerGameId || '',
).trim()
const sellerServerZone = String(
sellerFields.serverZone || consignmentInfo?.serverZone || '',
).trim()
const sellerSystem = String(
sellerFields.system || consignmentInfo?.system || '',
).trim()
const sellerContact = String(
sellerFields.sellerContact || consignmentInfo?.sellerContact || '',
).trim()
const sellerPhone = String(
sellerFields.sellerPhone || consignmentInfo?.sellerPhone || '',
).trim()
const inventoryNo = String(
sellerFields.inventoryNo || consignmentInfo?.inventoryNo || '',
).trim()
const sourceSubmissionNo = String(
sellerFields.sourceSubmissionNo || consignmentInfo?.sourceSubmissionNo || '',
).trim()
const effectiveNote = String(
detailOrder.gift?.boosterMaterial?.note || consignmentInfo?.remark || '',
).trim()
return (
<Card
title={isConsignment ? '寄售赠送进度' : '好友赠送进度'}
size="small"
extra={
<Space wrap>
{isInternalConsignmentOrder(detailOrder) ? (
<Tag color="cyan">内部寄售匹配</Tag>
) : isConsignment ? (
<Tag color="blue">寄售指派匹配</Tag>
) : (
<Tag color="purple">
{formatAcceptanceMode(detailOrder.acceptanceMode)}
</Tag>
)}
<Tag color={resolveGiftPhaseColor(detailOrder.gift.phase)}>
{formatGiftPhase(detailOrder.gift.phase, isConsignment)}
</Tag>
{isConsignment ? (
<Button
size="small"
icon={<CopyOutlined />}
onClick={() => copyConsignmentSellerInfo(detailOrder)}
>
一键复制卖家资料
</Button>
) : null}
{detailOrder.capabilities?.canConfirmFriend && canManageProblem ? (
<Button
type="primary"
size="small"
onClick={() => confirmFriendAdded(detailOrder)}
>
确认加好友
</Button>
) : null}
{detailOrder.capabilities?.canCompleteGiftCooldown && canManageProblem ? (
<Button
type="primary"
size="small"
onClick={() => completeGiftCooldown(detailOrder)}
>
结束冷却
</Button>
) : null}
{detailOrder.capabilities?.canResetGiftFlow && canManageProblem ? (
<Button
size="small"
danger
onClick={() => requestResetGiftMaterial(detailOrder)}
>
重置打手资料
</Button>
) : null}
</Space>
}
>
<div className="worker-order-detail-stack">
<Descriptions column={{ xs: 1, md: 2 }} bordered size="small">
<Descriptions.Item label="冷却时长">
{detailOrder.gift.cooldownMinutes > 0
? `${detailOrder.gift.cooldownMinutes} 分钟`
: `${detailOrder.gift.cooldownHours} 小时`}
</Descriptions.Item>
<Descriptions.Item
label={isConsignment ? '卖家资料/提报时间' : '打手资料提交'}
>
{formatAdminDateTime(detailOrder.gift.boosterMaterial?.submittedAt) ||
(isConsignment ? '已自动带入卖家现货资料' : '-')}
</Descriptions.Item>
<Descriptions.Item label="已加好友">
{formatAdminDateTime(detailOrder.gift.friendAddedAt)}
</Descriptions.Item>
<Descriptions.Item label="可赠送时间">
{detailOrder.gift.giftAvailableAt
? formatAdminDateTime(detailOrder.gift.giftAvailableAt)
: '-'}
</Descriptions.Item>
</Descriptions>
<Descriptions column={{ xs: 1, md: 2 }} bordered size="small">
<Descriptions.Item label={isConsignment ? '卖家角色昵称' : '游戏昵称'}>
{sellerNickname ? (
<Space size={6} align="center">
<span className="worker-order-detail-value">{sellerNickname}</span>
<Typography.Text
copyable={{
text: sellerNickname,
tooltips: ['复制昵称', '已复制'],
}}
/>
</Space>
) : (
<span className="worker-order-detail-value">-</span>
)}
</Descriptions.Item>
<Descriptions.Item label={isConsignment ? '卖家游戏ID' : '游戏ID'}>
{sellerGameId ? (
<Space size={6} align="center">
<span className="worker-order-detail-value">{sellerGameId}</span>
<Typography.Text
copyable={{
text: sellerGameId,
tooltips: ['复制游戏ID', '已复制'],
}}
/>
</Space>
) : (
<span className="worker-order-detail-value">-</span>
)}
</Descriptions.Item>
<Descriptions.Item label="区服">
{sellerServerZone ? (
<Space size={6} align="center">
<span className="worker-order-detail-value">
{sellerServerZone}
</span>
<Typography.Text
copyable={{
text: sellerServerZone,
tooltips: ['复制区服', '已复制'],
}}
/>
</Space>
) : (
<span className="worker-order-detail-value">-</span>
)}
</Descriptions.Item>
<Descriptions.Item label="系统">
{sellerSystem ? (
<Space size={6} align="center">
<span className="worker-order-detail-value">{sellerSystem}</span>
<Typography.Text
copyable={{
text: sellerSystem,
tooltips: ['复制系统', '已复制'],
}}
/>
</Space>
) : (
<span className="worker-order-detail-value">-</span>
)}
</Descriptions.Item>
{isConsignment && (sellerContact || sellerPhone) ? (
<Descriptions.Item label="卖家联系方式" span={2}>
<Space size={6} align="center" wrap>
<span className="worker-order-detail-value">
{[
sellerContact ? `微信/QQ: ${sellerContact}` : '',
sellerPhone ? `手机: ${sellerPhone}` : '',
]
.filter(Boolean)
.join(' / ')}
</span>
<Typography.Text
copyable={{
text: [
sellerContact ? `微信/QQ: ${sellerContact}` : '',
sellerPhone ? `手机: ${sellerPhone}` : '',
]
.filter(Boolean)
.join(' '),
tooltips: ['复制联系方式', '已复制'],
}}
/>
</Space>
</Descriptions.Item>
) : null}
{isConsignment && (inventoryNo || sourceSubmissionNo) ? (
<Descriptions.Item label="寄售单号" span={2}>
<Space size={6} align="center" wrap>
<span className="worker-order-detail-value">
{[
sourceSubmissionNo ? `提报单: ${sourceSubmissionNo}` : '',
inventoryNo ? `仓库单: ${inventoryNo}` : '',
]
.filter(Boolean)
.join(' / ')}
</span>
<Typography.Text
copyable={{
text: sourceSubmissionNo || inventoryNo,
tooltips: ['复制单号', '已复制'],
}}
/>
</Space>
</Descriptions.Item>
) : null}
<Descriptions.Item
label={isConsignment ? '卖家备注' : '打手备注'}
span={2}
>
<span className="worker-order-detail-value">
{effectiveNote || '-'}
</span>
</Descriptions.Item>
</Descriptions>
<div>
<Typography.Text type="secondary">
{isConsignment ? '卖家上架/主页截图' : '打手主页截图'}
</Typography.Text>
<div style={{ marginTop: 8 }}>
{detailOrder.gift.boosterMaterial?.screenshots?.length ? (
<ImagePreviewList
files={detailOrder.gift.boosterMaterial.screenshots}
size={72}
/>
) : (
<Typography.Text type="secondary">未提交</Typography.Text>
)}
</div>
</div>
</div>
</Card>
)
})()
: null}
<Card title="验收信息" size="small">
<div className="worker-order-detail-stack">
+23 -15
View File
@@ -84,21 +84,29 @@ export function renderDetailFieldValues(items: DetailFieldItem[], emptyText: str
return (
<Descriptions column={1} bordered size="small">
{items.map((item) => (
<Descriptions.Item
key={item.key}
label={
<Space size={6} wrap>
<span>{item.label}</span>
{item.required ? <Tag color="orange">必填</Tag> : null}
</Space>
}
>
<span className="worker-order-detail-value">
{String(item.value || '').trim() || '-'}
</span>
</Descriptions.Item>
))}
{items.map((item) => {
const val = String(item.value || '').trim()
return (
<Descriptions.Item
key={item.key}
label={
<Space size={6} wrap>
<span>{item.label}</span>
{item.required ? <Tag color="orange">必填</Tag> : null}
</Space>
}
>
{val ? (
<Space size={6} align="center">
<span className="worker-order-detail-value">{val}</span>
<Typography.Text copyable={{ text: val, tooltips: ['复制', '已复制'] }} />
</Space>
) : (
<span className="worker-order-detail-value">-</span>
)}
</Descriptions.Item>
)
})}
</Descriptions>
)
}
@@ -74,6 +74,41 @@ const BRILLIANCE_OPTIONS = [
type RecordFilter = 'all' | 'reviewing' | 'active' | 'finished'
const DEPOSIT_TIP_KEY = 'worker-consignment-deposit-tip-seen'
const ACTIVE_TAB_STORAGE_KEY = 'worker_consignment_active_tab'
const RECORD_FILTER_STORAGE_KEY = 'worker_consignment_record_filter'
function getInitialActiveTab(): 'publish' | 'records' {
if (typeof window === 'undefined') return 'publish'
try {
const params = new URLSearchParams(window.location.search)
const tabParam = params.get('tab')
if (tabParam === 'records' || tabParam === 'publish') {
return tabParam
}
const saved = window.localStorage.getItem(ACTIVE_TAB_STORAGE_KEY)
return saved === 'records' ? 'records' : 'publish'
} catch {
return 'publish'
}
}
function getInitialRecordFilter(): RecordFilter {
if (typeof window === 'undefined') return 'all'
try {
const params = new URLSearchParams(window.location.search)
const filterParam = params.get('filter') as RecordFilter | null
if (filterParam && ['all', 'reviewing', 'active', 'finished'].includes(filterParam)) {
return filterParam
}
const saved = window.localStorage.getItem(RECORD_FILTER_STORAGE_KEY) as RecordFilter | null
if (saved && ['all', 'reviewing', 'active', 'finished'].includes(saved)) {
return saved
}
} catch {
// ignore
}
return 'all'
}
/** 赠送冷却倒计时刷新间隔:30 秒足够展示,不需要秒级跳动 */
const COOLDOWN_TICK_MS = 30_000
@@ -81,8 +116,36 @@ const COOLDOWN_TICK_MS = 30_000
export default function WorkerConsignmentPage() {
const isMobile = useIsMobile()
const { message, modal } = App.useApp()
const [activeTab, setActiveTab] = useState<'publish' | 'records'>('publish')
const [recordFilter, setRecordFilter] = useState<RecordFilter>('all')
const [activeTab, setActiveTabState] = useState<'publish' | 'records'>(getInitialActiveTab)
const [recordFilter, setRecordFilterState] = useState<RecordFilter>(getInitialRecordFilter)
const setActiveTab = (tab: 'publish' | 'records') => {
setActiveTabState(tab)
if (typeof window !== 'undefined') {
try {
window.localStorage.setItem(ACTIVE_TAB_STORAGE_KEY, tab)
const url = new URL(window.location.href)
url.searchParams.set('tab', tab)
window.history.replaceState({}, '', url.toString())
} catch {
// ignore
}
}
}
const setRecordFilter = (filter: RecordFilter) => {
setRecordFilterState(filter)
if (typeof window !== 'undefined') {
try {
window.localStorage.setItem(RECORD_FILTER_STORAGE_KEY, filter)
const url = new URL(window.location.href)
url.searchParams.set('filter', filter)
window.history.replaceState({}, '', url.toString())
} catch {
// ignore
}
}
}
const [screenshots, setScreenshots] = useState<UploadedFile[]>([])
const [submitting, setSubmitting] = useState(false)
const [evidenceItem, setEvidenceItem] = useState<WorkerConsignmentRecord | null>(null)