fix(worker-platform): 优化工单号精准检索与跨状态流转展示,修复状态更新后工单消失问题

1. 后端查询重构:新增 isWorkOrderNoLookupKeyword 工单号前缀与忽略大小写精准索引匹配,优化后端 work-order-query-repo 的工单号检索路径;
2. 前端单号智能穿透:管理员输入 WO... 等精准单号查询时,自动放宽细粒度排他状态/阶段过滤,避免加好友/验收等状态流转后工单因旧状态筛选脱靶消失;
3. 搜索栏交互升级:输入框清空即时重置关键词与页码,支持回车/按钮/清除全方位无缝触发;
4. 详情抽屉与多面板联动:增加 ordersQuery 数据更新对 detailOrder 的自动同步,并将 refreshAll 广播范围覆盖至寄售仓库与统计指标。
This commit is contained in:
yml2213
2026-09-18 19:36:17 +08:00
parent 68cd309bdd
commit d6523635ea
12 changed files with 526 additions and 93 deletions
@@ -41,6 +41,8 @@ export type ConsignmentInventoryRow = {
gift_evidence_json?: string | Record<string, unknown> | null
gift_work_order_no?: string | null
gift_work_order_status?: string | null
gift_phase?: string | null
gift_friend_added_at?: string | null
bound_work_order_material_json?: string | Record<string, unknown> | null
bound_work_order_requirement_json?: string | Record<string, unknown> | null
worker_id?: string | number | null
@@ -197,6 +199,8 @@ export async function listConsignmentInventories(filters: {
wo.acceptance_json AS gift_evidence_json,
wo.work_order_no AS gift_work_order_no,
wo.status AS gift_work_order_status,
wo.gift_phase AS gift_phase,
wo.friend_added_at AS gift_friend_added_at,
wo.material_json AS bound_work_order_material_json,
wo.requirement_json AS bound_work_order_requirement_json
FROM consignment_inventories ci
@@ -314,6 +318,8 @@ export async function getConsignmentInventoryById(
wo.acceptance_json AS gift_evidence_json,
wo.work_order_no AS gift_work_order_no,
wo.status AS gift_work_order_status,
wo.gift_phase AS gift_phase,
wo.friend_added_at AS gift_friend_added_at,
wo.material_json AS bound_work_order_material_json,
wo.requirement_json AS bound_work_order_requirement_json
FROM consignment_inventories ci
@@ -3,9 +3,14 @@ import type { ListInput, WorkOrderRow, WorkOrderStatistics } from './types.js'
import type { PoolClient } from 'pg'
import {
normalizeWorkOrderAcceptanceMode,
WORK_ORDER_ACCEPTANCE_MODE,
WORK_ORDER_GIFT_PHASE,
} from '../../domain/work-order-acceptance-mode.js'
import { isPlatformOrderIdLookupKeyword } from '../../utils/platform-order-id.js'
import {
isPlatformOrderIdLookupKeyword,
isWorkOrderNoLookupKeyword,
} from '../../utils/platform-order-id.js'
export const WORK_ORDER_SELECT = `
SELECT
wo.*,
@@ -843,6 +848,12 @@ export function buildWorkOrderWhere({
filters.push(
`(wo.platform_order_id = ${paramIndex} OR wo.platform_order_id LIKE ${paramIndex} || '%')`,
)
} else if (isWorkOrderNoLookupKeyword(normalizedKeyword)) {
params.push(normalizedKeyword)
const paramIndex = `$${params.length}`
filters.push(
`(wo.work_order_no = ${paramIndex} OR wo.work_order_no ILIKE ${paramIndex} || '%' OR LOWER(wo.work_order_no) = LOWER(${paramIndex}))`,
)
} else {
params.push(`%${normalizedKeyword}%`)
filters.push(`${workOrderSearchExpression()} LIKE LOWER($${params.length})`)
@@ -887,11 +898,37 @@ export function buildWorkOrderWhere({
)
}
if (acceptanceMode) {
params.push(normalizeWorkOrderAcceptanceMode(acceptanceMode))
filters.push(`wo.acceptance_mode = $${params.length}`)
const rawMode = String(acceptanceMode).trim()
if (rawMode === 'internal_consignment') {
filters.push(`(
wo.material_json->'consignment'->>'inventoryId' IS NOT NULL
AND (wo.material_json->'consignment'->>'fulfillmentType' IS NULL OR wo.material_json->'consignment'->>'fulfillmentType' != 'worker_deposit')
AND (wo.material_json->'consignment'->>'workerId' IS NULL OR wo.material_json->'consignment'->>'workerId' = '0')
AND (wo.assigned_worker_id IS NULL OR wo.assigned_worker_id = 0)
)`)
} else if (rawMode === WORK_ORDER_ACCEPTANCE_MODE.FRIEND_GIFT) {
filters.push(`(
wo.acceptance_mode = 'friend_gift'
AND NOT (
wo.material_json->'consignment'->>'inventoryId' IS NOT NULL
AND (wo.material_json->'consignment'->>'fulfillmentType' IS NULL OR wo.material_json->'consignment'->>'fulfillmentType' != 'worker_deposit')
AND (wo.material_json->'consignment'->>'workerId' IS NULL OR wo.material_json->'consignment'->>'workerId' = '0')
AND (wo.assigned_worker_id IS NULL OR wo.assigned_worker_id = 0)
)
)`)
} else if (rawMode === WORK_ORDER_ACCEPTANCE_MODE.STANDARD) {
filters.push(`(
wo.acceptance_mode = 'standard'
AND wo.material_json->'consignment'->>'inventoryId' IS NULL
)`)
} else {
params.push(normalizeWorkOrderAcceptanceMode(acceptanceMode))
filters.push(`wo.acceptance_mode = $${params.length}`)
}
}
if (giftPhase) {
const normalizedGiftPhase = String(giftPhase).trim()
const activeGiftStatusSql = `wo.status NOT IN ('accepted', 'cancelled')`
if (normalizedGiftPhase === WORK_ORDER_GIFT_PHASE.GIFT_READY) {
// gift_ready 是冷却到点后的实时阶段,由时间条件计算而不是落库状态。
filters.push(`(
@@ -899,10 +936,18 @@ export function buildWorkOrderWhere({
AND wo.gift_phase = 'friend_countdown'
AND wo.gift_available_at IS NOT NULL
AND wo.gift_available_at <= NOW()
AND ${activeGiftStatusSql}
)`)
} else if (normalizedGiftPhase === WORK_ORDER_GIFT_PHASE.FRIEND_COUNTDOWN) {
filters.push(`(
wo.acceptance_mode = 'friend_gift'
AND wo.gift_phase = 'friend_countdown'
AND (wo.gift_available_at IS NULL OR wo.gift_available_at > NOW())
AND ${activeGiftStatusSql}
)`)
} else if (Object.values(WORK_ORDER_GIFT_PHASE).includes(normalizedGiftPhase as never)) {
params.push(normalizedGiftPhase)
const giftPhaseFilter = `wo.acceptance_mode = 'friend_gift' AND wo.gift_phase = $${params.length}`
const giftPhaseFilter = `wo.acceptance_mode = 'friend_gift' AND wo.gift_phase = $${params.length} AND ${activeGiftStatusSql}`
// 待打手提交资料仅代表已接单后的赠送流程;待抢单/未分配模板单不应混入该筛选。
filters.push(
normalizedGiftPhase === WORK_ORDER_GIFT_PHASE.MATERIAL_REQUIRED
@@ -110,6 +110,8 @@ function mapConsignmentEvidence(source: {
gift_evidence_json?: string | Record<string, unknown> | null
gift_work_order_no?: string | null
gift_work_order_status?: string | null
gift_phase?: string | null
gift_friend_added_at?: string | null
}) {
const acceptance = toRecord(source.gift_evidence_json)
const files = Array.isArray(acceptance?.files) ? (acceptance.files as unknown[]) : []
@@ -146,6 +148,8 @@ function mapConsignmentEvidence(source: {
giftEvidenceSubmittedAt: String(acceptance?.submittedAt || ''),
giftWorkOrderNo: source.gift_work_order_no || '',
giftWorkOrderStatus: workOrderStatus,
giftPhase: source.gift_phase || '',
giftFriendAddedAt: source.gift_friend_added_at || '',
/** none=还没交 / pending=交了待客服验收 / accepted=客服已验收 / submitted=其他在途状态 */
giftEvidenceReviewStatus: reviewStatus,
}
@@ -1635,6 +1635,25 @@ test('好友赠送待打手提交资料筛选排除待抢单订单', () => {
assert.match(whereClause, /wo\.status IN \('in_progress', 'problem'\)/)
})
test('工单筛选支持内部寄售匹配与打手好友赠送隔离,且赠送冷却中排除已验收已取消工单', () => {
const internalQuery = buildWorkOrderWhere({ acceptanceMode: 'internal_consignment' })
assert.match(internalQuery.whereClause, /wo\.material_json->'consignment'->>'inventoryId' IS NOT NULL/)
assert.match(internalQuery.whereClause, /wo\.assigned_worker_id IS NULL OR wo\.assigned_worker_id = 0/)
const friendGiftQuery = buildWorkOrderWhere({ acceptanceMode: 'friend_gift' })
assert.match(friendGiftQuery.whereClause, /wo\.acceptance_mode = 'friend_gift'/)
assert.match(friendGiftQuery.whereClause, /NOT \(/)
const countdownQuery = buildWorkOrderWhere({ giftPhase: 'friend_countdown' })
assert.match(countdownQuery.whereClause, /wo\.gift_phase = 'friend_countdown'/)
assert.match(countdownQuery.whereClause, /wo\.gift_available_at > NOW\(\)/)
assert.match(countdownQuery.whereClause, /wo\.status NOT IN \('accepted', 'cancelled'\)/)
const readyQuery = buildWorkOrderWhere({ giftPhase: 'gift_ready' })
assert.match(readyQuery.whereClause, /wo\.gift_available_at <= NOW\(\)/)
assert.match(readyQuery.whereClause, /wo\.status NOT IN \('accepted', 'cancelled'\)/)
})
test('好友赠送冷却中才允许客服手动结束冷却', () => {
const cooling = mapWorkOrderAdmin(
buildWorkOrderRow({
@@ -1,7 +1,10 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { isPlatformOrderIdLookupKeyword } from './platform-order-id.js'
import {
isPlatformOrderIdLookupKeyword,
isWorkOrderNoLookupKeyword,
} from './platform-order-id.js'
test('识别纯数字平台订单号', () => {
assert.equal(isPlatformOrderIdLookupKeyword('20247230034021572'), true)
@@ -14,9 +17,19 @@ test('识别拼多多带连字符的 order_sn', () => {
assert.equal(isPlatformOrderIdLookupKeyword('260912-123456789012345'), true)
})
test('识别统一内部工单号 (WO...)', () => {
assert.equal(isWorkOrderNoLookupKeyword('WOb320ee0b39b9'), true)
assert.equal(isWorkOrderNoLookupKeyword('wob320ee0b39b9'), true)
assert.equal(isWorkOrderNoLookupKeyword('WO26091216358821'), true)
assert.equal(isWorkOrderNoLookupKeyword('WO12'), false)
assert.equal(isWorkOrderNoLookupKeyword('20247230034021572'), false)
})
test('拒绝普通关键词', () => {
assert.equal(isPlatformOrderIdLookupKeyword('皮肤店'), false)
assert.equal(isPlatformOrderIdLookupKeyword('安卓怒嚎3钻'), false)
assert.equal(isPlatformOrderIdLookupKeyword('DT881aa66294cc'), false)
assert.equal(isPlatformOrderIdLookupKeyword(''), false)
assert.equal(isWorkOrderNoLookupKeyword('皮肤店'), false)
})
@@ -6,3 +6,12 @@ export function isPlatformOrderIdLookupKeyword(value: unknown): boolean {
const trimmed = String(value || '').trim()
return /^\d{6,}(-\d{6,})?$/.test(trimmed)
}
/**
* 识别内部统一工单号 (以 WO 开头且至少 4 位字符,如 WOb320ee0b39b9、WO26091216358821)。
*/
export function isWorkOrderNoLookupKeyword(value: unknown): boolean {
const trimmed = String(value || '').trim()
return /^WO[0-9a-zA-Z]{4,}$/i.test(trimmed)
}
@@ -1,4 +1,4 @@
import { useQuery } from '@tanstack/react-query'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Card, Space, Tabs } from 'antd'
import { useEffect, useRef, useState } from 'react'
import { useLocation, useSearchParams } from 'react-router'
@@ -27,7 +27,30 @@ type AfterSalesLocationState = {
createOrder?: WorkOrder
}
const TAB_QUERY_KEYS: Record<string, string[][]> = {
'consignment-inventory': [['admin-consignment-inventories'], ['admin-consignment-stats']],
'consignment-submissions': [
['admin-consignment-submissions'],
['admin-consignment-submissions-pending-total'],
],
orders: [['admin-work-orders'], ['admin-work-orders-stats']],
'cancel-requests': [['admin-work-order-cancel-requests']],
'after-sales': [['admin-after-sales-orders'], ['admin-after-sales-stats']],
feedbacks: [['admin-feedbacks']],
rules: [['admin-product-rules']],
'product-match': [['admin-product-match']],
categories: [['admin-categories']],
workers: [['admin-workers'], ['admin-worker-users']],
leaderboard: [['admin-leaderboard']],
finance: [['admin-finance-settlements'], ['admin-finance-withdrawals']],
levels: [['admin-worker-levels']],
notifications: [['admin-notifications']],
announcement: [['admin-announcements']],
'hall-config': [['admin-hall-config']],
}
export default function AdminWorkerPlatformPage() {
const queryClient = useQueryClient()
const [searchParams, setSearchParams] = useSearchParams()
const location = useLocation()
const [activeTab, setActiveTab] = useState(() => loadActiveTab(searchParams.get('tab')))
@@ -49,6 +72,18 @@ export default function AdminWorkerPlatformPage() {
? 'orders'
: activeTab
// 切换标签页时自动刷新对应标签页的查询数据,确保切入即为最新状态
useEffect(() => {
if (!visibleActiveTab) return
const queries = TAB_QUERY_KEYS[visibleActiveTab]
if (queries) {
queries.forEach((queryKey) => {
void queryClient.invalidateQueries({ queryKey })
})
}
void queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-summary'] })
}, [visibleActiveTab, queryClient])
useEffect(() => {
const tab = searchParams.get('tab')
if (isWorkerPlatformTab(tab)) {
@@ -331,6 +331,31 @@ export default function ConsignmentInventoryPanel() {
{resolveConsignmentNo(record)}
</Text>
</Space>
{record.giftWorkOrderNo ? (
<div style={{ marginTop: 2 }}>
<Space size={4} align="center" wrap>
<Text type="secondary" style={{ fontSize: 12 }}>
关联工单
</Text>
<Text copyable style={{ fontSize: 12, fontWeight: 600, color: '#1677ff' }}>
{record.giftWorkOrderNo}
</Text>
{record.giftWorkOrderStatus ? (
<Tag
color="cyan"
style={{
fontSize: 10,
lineHeight: '16px',
padding: '0 4px',
marginInlineEnd: 0,
}}
>
{formatGiftWorkOrderStatus(record.giftWorkOrderStatus)}
</Tag>
) : null}
</Space>
</div>
) : null}
<div style={{ marginTop: 2 }}>
{record.sourceType === 'worker_portal' ? (
<Tag color="purple" style={{ fontSize: 11, padding: '0 4px' }}>
@@ -510,8 +535,18 @@ export default function ConsignmentInventoryPanel() {
return (
<Space direction="vertical" size={2}>
<Badge status="success" text="已履约出库" />
{record.giftWorkOrderNo ? (
<Space size={2} align="center">
<Text type="secondary" style={{ fontSize: 11 }}>
工单:
</Text>
<Text copyable style={{ fontSize: 11, color: '#1677ff', fontWeight: 600 }}>
{record.giftWorkOrderNo}
</Text>
</Space>
) : null}
{record.boundPlatformOrderId ? (
<Text copyable style={{ fontSize: 11, color: '#1890ff' }}>
<Text copyable style={{ fontSize: 11, color: '#8c8c8c' }}>
{record.boundPlatformOrderId}
</Text>
) : null}
@@ -601,6 +636,8 @@ export default function ConsignmentInventoryPanel() {
) : null}
{record.status === 'consumed' &&
canManageConsignment &&
!record.giftFriendAddedAt &&
(!record.giftPhase || record.giftPhase === 'material_required') &&
record.giftEvidenceReviewStatus !== 'accepted' &&
record.giftWorkOrderStatus !== 'accepted' ? (
<Button
@@ -30,6 +30,7 @@ import type { TableColumnsType } from 'antd'
import { useState } from 'react'
import ConsignmentEvidenceGallery from '@/components/admin/ConsignmentEvidenceGallery'
import ImagePreviewList from '@/components/files/ImagePreviewList'
import {
approveAdminConsignmentSubmission,
fetchAdminConsignmentSubmissions,
@@ -468,9 +469,16 @@ export default function ConsignmentSubmissionsPanel() {
</Card>
<Modal
title="审核寄卖申请并正式入库"
title={
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<CheckCircleOutlined style={{ color: '#52c41a' }} />
<span>审核寄卖申请并正式入库</span>
</div>
}
open={Boolean(approveModalItem)}
width={680}
width={760}
style={{ top: 24 }}
bodyStyle={{ maxHeight: 'calc(86vh - 120px)', overflowY: 'auto', paddingRight: 8 }}
onCancel={() => setApproveModalItem(null)}
onOk={handleApproveSubmit}
confirmLoading={approving}
@@ -478,73 +486,186 @@ export default function ConsignmentSubmissionsPanel() {
okType="primary"
destroyOnClose
>
<div style={{ marginBottom: 16 }}>
<Text type="secondary">
提报人联系方式:<strong>{approveModalItem?.contactInfo}</strong> | 游戏ID:
<strong>{approveModalItem?.sellerGameId || '-'}</strong> | 卖家要价:
<strong style={{ color: '#cf1322' }}>
¥{((approveModalItem?.askingPriceFen || 0) / 100).toFixed(2)}
</strong>
</Text>
{/* 顶部提报概览卡片 */}
<div
style={{
background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
border: '1px solid #e2e8f0',
borderRadius: 8,
padding: '12px 16px',
marginBottom: 16,
}}
>
<div
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}
>
<div>
<Space wrap size={8} style={{ marginBottom: 6 }}>
<Text strong style={{ fontSize: 14 }}>
单号:{approveModalItem?.submissionNo}
</Text>
{approveModalItem?.workerName ? (
<Tag color="cyan">打手提报: {approveModalItem.workerName}</Tag>
) : (
<Tag color="purple">卖家自主提报</Tag>
)}
{approveModalItem?.createdAt ? (
<Text type="secondary" style={{ fontSize: 12 }}>
提报于 {formatDateTime(approveModalItem.createdAt)}
</Text>
) : null}
</Space>
<div
style={{
fontSize: 12,
color: '#475569',
display: 'flex',
flexWrap: 'wrap',
gap: '12px 20px',
}}
>
<span>
联系方式:<Text strong>{approveModalItem?.contactInfo || '-'}</Text>
</span>
<span>
游戏ID:<Text strong>{approveModalItem?.sellerGameId || '-'}</Text>
</span>
{approveModalItem?.sellerPaymentInfo ? (
<span>
提报收款:<Text strong>{approveModalItem.sellerPaymentInfo}</Text>
</span>
) : null}
</div>
</div>
<div style={{ textAlign: 'right', flexShrink: 0, paddingLeft: 16 }}>
<div style={{ fontSize: 12, color: '#64748b' }}>卖家提报要价</div>
<div style={{ fontSize: 22, fontWeight: 700, color: '#f5222d', lineHeight: 1.2 }}>
¥{((approveModalItem?.askingPriceFen || 0) / 100).toFixed(2)}
</div>
</div>
</div>
{/* 提报截图快速直览 */}
{approveModalItem?.screenshotsJson && approveModalItem.screenshotsJson.length > 0 ? (
<div
style={{
marginTop: 10,
paddingTop: 8,
borderTop: '1px dashed #cbd5e1',
display: 'flex',
alignItems: 'center',
gap: 12,
}}
>
<Text type="secondary" style={{ fontSize: 12, flexShrink: 0 }}>
商品截图 ({approveModalItem.screenshotsJson.length}张):
</Text>
<ImagePreviewList
imageUrls={approveModalItem.screenshotsJson}
size={38}
maxVisible={6}
/>
</div>
) : null}
</div>
<Form form={approveForm} layout="vertical">
<Row gutter={16}>
<Col span={14}>
{/* 分组一:物品与规格 */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
marginBottom: 10,
fontWeight: 600,
fontSize: 13,
color: '#1e293b',
}}
>
<div style={{ width: 3, height: 14, background: '#1677ff', borderRadius: 2 }} />
物品属性与收购成本
</div>
<Row gutter={12}>
<Col span={15}>
<Form.Item
name="itemName"
label="正式入库物品名"
rules={[{ required: true, message: '请输入入库物品名' }]}
style={{ marginBottom: 12 }}
>
<Input />
<Input placeholder="如:安卓暗潮1钻1璀璨 苏尔南" />
</Form.Item>
</Col>
<Col span={10}>
<Col span={9}>
<Form.Item
name="costPrice"
label="实际收购成本 (元)"
label="实际收购成本"
rules={[{ required: true, message: '请输入实际收购成本' }]}
style={{ marginBottom: 12 }}
>
<InputNumber min={0} step={1} style={{ width: '100%' }} />
<InputNumber
min={0}
step={1}
prefix="¥"
addonAfter="元"
style={{ width: '100%' }}
/>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Row gutter={12}>
<Col span={5}>
<Form.Item name="system" label="系统">
{/* 审核可修正规格:与打手端发布同一套选项(苹果/安卓;微信/QQ/互通) */}
<Form.Item name="system" label="系统" style={{ marginBottom: 16 }}>
<Select options={[...CONSIGNMENT_SYSTEM_OPTIONS]} />
</Form.Item>
</Col>
<Col span={5}>
<Form.Item name="serverZone" label="区服">
<Form.Item name="serverZone" label="区服" style={{ marginBottom: 16 }}>
<Select options={[...CONSIGNMENT_ZONE_OPTIONS]} />
</Form.Item>
</Col>
<Col span={5}>
<Form.Item name="series" label="系列">
<Input />
</Form.Item>
</Col>
<Col span={5}>
<Form.Item name="diamondLevel" label="钻石等级 (1-7)">
<InputNumber min={1} max={7} style={{ width: '100%' }} />
<Form.Item name="series" label="系列" style={{ marginBottom: 16 }}>
<Input placeholder="如 暗潮" />
</Form.Item>
</Col>
<Col span={4}>
<Form.Item name="brillianceCount" label="璀璨数 (0-7)">
<Form.Item name="diamondLevel" label="钻石等级" style={{ marginBottom: 16 }}>
<InputNumber min={1} max={7} style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col span={5}>
<Form.Item name="brillianceCount" label="璀璨数" style={{ marginBottom: 16 }}>
<InputNumber min={0} max={7} style={{ width: '100%' }} />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
{/* 分组二:履约与打手 */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
marginBottom: 10,
fontWeight: 600,
fontSize: 13,
color: '#1e293b',
}}
>
<div style={{ width: 3, height: 14, background: '#52c41a', borderRadius: 2 }} />
履约模式与打手关联 (选填)
</div>
<Row gutter={12}>
<Col span={14}>
<Form.Item
name="workerId"
label="走打手接单平台 (关联履约打手)"
tooltip="指派该打手接单丢枪交货时,接单平台将自动冻结其押金并担保交付"
tooltip="指派该打手接单丢枪交货时,接单平台将自动冻结其押金并担保交付;不选则为客服/内部匹配"
style={{ marginBottom: 16 }}
>
<Select
allowClear
@@ -554,7 +675,7 @@ export default function ConsignmentSubmissionsPanel() {
.toLowerCase()
.includes(input.toLowerCase())
}
placeholder="可选择关联的接单打手 (选填)"
placeholder="未选择则为客服直接入库/内部现货匹配"
options={workerOptions}
/>
</Form.Item>
@@ -562,13 +683,15 @@ export default function ConsignmentSubmissionsPanel() {
<Col span={10}>
<Form.Item
name="depositAmount"
label="履约押金金额 (元)"
label="履约押金金额"
tooltip="指派该打手交枪履约时,系统冻结的押金金额"
style={{ marginBottom: 16 }}
>
<InputNumber
min={0}
step={10}
addonBefore="¥"
prefix="¥"
addonAfter="元"
style={{ width: '100%' }}
placeholder="如 100"
/>
@@ -576,52 +699,69 @@ export default function ConsignmentSubmissionsPanel() {
</Col>
</Row>
<Row gutter={16}>
{/* 分组三:卖家信息与入库登记 */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
marginBottom: 10,
fontWeight: 600,
fontSize: 13,
color: '#1e293b',
}}
>
<div style={{ width: 3, height: 14, background: '#722ed1', borderRadius: 2 }} />
卖家联络与入库登记
</div>
<Row gutter={12}>
<Col span={8}>
<Form.Item name="sellerContact" label="卖家微信号" style={{ marginBottom: 12 }}>
<Input placeholder="微信号" />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="sellerPhone" label="卖家手机" style={{ marginBottom: 12 }}>
<Input placeholder="手机号" />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="sellerGameId" label="卖家游戏ID" style={{ marginBottom: 12 }}>
<Input placeholder="游戏昵称/ID" />
</Form.Item>
</Col>
</Row>
<Row gutter={12}>
<Col span={10}>
<Form.Item name="sellerPaymentInfo" label="卖家收款信息" style={{ marginBottom: 12 }}>
<Input placeholder="如 走打手接单平台钱包 或 支付宝账号" />
</Form.Item>
</Col>
<Col span={7}>
<Form.Item
name="buyerName"
label="收购人 (客服)"
rules={[{ required: true, message: '请填写收购人' }]}
style={{ marginBottom: 12 }}
>
<Input />
<Input placeholder="如 客服" />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="shopName" label="关联店铺">
<Input placeholder="店铺名" />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="inboundBatch" label="收购批次">
<Input placeholder="批次" />
<Col span={7}>
<Form.Item name="inboundBatch" label="收购批次" style={{ marginBottom: 12 }}>
<Input placeholder="如 2026-09" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={8}>
<Form.Item name="sellerContact" label="卖家微信号">
<Input />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="sellerPhone" label="卖家手机">
<Input />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="sellerGameId" label="卖家游戏ID">
<Input />
</Form.Item>
</Col>
</Row>
<Form.Item name="sellerPaymentInfo" label="卖家收款信息">
<Input />
</Form.Item>
<Form.Item name="reviewNote" label="审核备注">
<Input.TextArea rows={2} placeholder="审核核准说明" />
{/* 分组四:审核备注 */}
<Form.Item name="reviewNote" label="审核备注" style={{ marginTop: 4, marginBottom: 0 }}>
<Input.TextArea
rows={2}
placeholder="可选填写核准或沟通说明(如:已与打手核对主页截图无误)"
/>
</Form.Item>
</Form>
</Modal>
@@ -415,6 +415,17 @@ export default function WorkOrdersPanel() {
}),
})
const ordersPagination = ordersQuery.data?.data.pagination
// 当列表重新拉取最新数据后,若详情抽屉正处于打开状态,自动用最新状态同步更新 detailOrder
useEffect(() => {
if (!detailOrder) return
const items = ordersQuery.data?.data.items || []
const updated = items.find((item) => item.workOrderId === detailOrder.workOrderId)
if (updated) {
setDetailOrder(updated)
}
}, [ordersQuery.data])
const categoriesQuery = useQuery({
queryKey: ['admin-worker-platform-categories'],
queryFn: () => fetchAdminWorkCategories(),
@@ -518,7 +529,9 @@ export default function WorkOrdersPanel() {
}, [showWorkerLevelFilter])
useEffect(() => {
if (acceptanceMode !== 'friend_gift') setGiftPhase(undefined)
if (acceptanceMode !== 'friend_gift' && acceptanceMode !== 'internal_consignment') {
setGiftPhase(undefined)
}
}, [acceptanceMode])
function openAssignModal(row: WorkOrder) {
@@ -680,6 +693,12 @@ export default function WorkOrdersPanel() {
queryClient.invalidateQueries({
queryKey: ['admin-worker-platform-summary'],
}),
queryClient.invalidateQueries({
queryKey: ['admin-consignment-inventories'],
}),
queryClient.invalidateQueries({
queryKey: ['admin-consignment-stats'],
}),
])
}
@@ -1008,9 +1027,26 @@ export default function WorkOrdersPanel() {
setMaterialScreenshots([])
}
function isExplicitOrderNoSearch(text: string) {
const trimmed = text.trim()
if (!trimmed) return false
return (
/^WO[0-9a-zA-Z]{4,}$/i.test(trimmed) ||
/^\d{6,}(-\d{6,})?$/.test(trimmed) ||
/^OD[0-9a-zA-Z]{4,}$/i.test(trimmed)
)
}
function searchOrders() {
setKeyword(keywordInput.trim())
const trimmed = keywordInput.trim()
setKeyword(trimmed)
setPage(1)
// 管理员精准搜索工单号/平台单号时,自动清除排他的状态与阶段限制,避免状态流转后工单从视野中消失
if (isExplicitOrderNoSearch(trimmed)) {
if (giftPhase) setGiftPhase(undefined)
if (statuses.length > 0) setStatuses([])
if (acceptanceMode) setAcceptanceMode(undefined)
}
}
function clearOrderFilters() {
@@ -1395,8 +1431,14 @@ export default function WorkOrdersPanel() {
置顶
</Tag>
) : null}
{row.acceptanceMode === 'friend_gift' ? <Tag color="purple">好友赠送</Tag> : null}
{row.acceptanceMode === 'friend_gift' &&
{isInternalConsignmentOrder(row) ? (
<Tag color="cyan">内部匹配</Tag>
) : row.acceptanceMode === 'friend_gift' ? (
<Tag color="purple">好友赠送</Tag>
) : null}
{row.status !== 'accepted' &&
row.status !== 'cancelled' &&
row.acceptanceMode === 'friend_gift' &&
row.gift &&
(!['pending_material', 'unassigned', 'open'].includes(row.rawStatus || row.status) ||
isInternalConsignmentOrder(row)) ? (
@@ -1682,9 +1724,16 @@ export default function WorkOrdersPanel() {
<Input
allowClear
value={keywordInput}
placeholder="订单号 / 商品名 / 游戏ID / 游戏昵称"
placeholder="工单号 / 平台单号 / 商品名 / 游戏ID / 游戏昵称"
style={{ width: 280 }}
onChange={(event) => setKeywordInput(event.target.value)}
onChange={(event) => {
const nextVal = event.target.value
setKeywordInput(nextVal)
if (!nextVal.trim()) {
setKeyword('')
setPage(1)
}
}}
onPressEnter={searchOrders}
/>
<Select
@@ -1763,17 +1812,18 @@ export default function WorkOrdersPanel() {
allowClear
value={acceptanceMode}
placeholder="全部验收模式"
style={{ width: 150 }}
style={{ width: 160 }}
options={[
{ value: 'standard', label: '标准验收' },
{ value: 'friend_gift', label: '好友赠送' },
{ value: 'friend_gift', label: '打手好友赠送' },
{ value: 'internal_consignment', label: '内部寄售匹配' },
]}
onChange={(nextMode) => {
setAcceptanceMode(nextMode)
setPage(1)
}}
/>
{acceptanceMode === 'friend_gift' ? (
{acceptanceMode === 'friend_gift' || acceptanceMode === 'internal_consignment' ? (
<Select
allowClear
value={giftPhase}
@@ -1781,7 +1831,11 @@ export default function WorkOrdersPanel() {
style={{ width: 170 }}
options={GIFT_PHASE_FILTER_OPTIONS.map((phase) => ({
value: phase,
label: <Tag color={resolveGiftPhaseColor(phase)}>{formatGiftPhase(phase)}</Tag>,
label: (
<Tag color={resolveGiftPhaseColor(phase)}>
{formatGiftPhase(phase, acceptanceMode === 'internal_consignment')}
</Tag>
),
}))}
onChange={(nextPhase) => {
setGiftPhase(nextPhase)
@@ -2041,11 +2095,17 @@ export default function WorkOrdersPanel() {
{detailOrder.acceptanceMode === 'friend_gift' && detailOrder.gift ? (
<Card
title="好友赠送进度"
title={
isInternalConsignmentOrder(detailOrder) ? '内部寄售赠送进度' : '好友赠送进度'
}
size="small"
extra={
<Space>
<Tag color="purple">{formatAcceptanceMode(detailOrder.acceptanceMode)}</Tag>
{isInternalConsignmentOrder(detailOrder) ? (
<Tag color="cyan">内部寄售匹配</Tag>
) : (
<Tag color="purple">{formatAcceptanceMode(detailOrder.acceptanceMode)}</Tag>
)}
<Tag color={resolveGiftPhaseColor(detailOrder.gift.phase)}>
{formatGiftPhase(
detailOrder.gift.phase,
@@ -12,7 +12,7 @@ import {
SafetyCertificateOutlined,
UserOutlined,
} from '@ant-design/icons'
import { useQuery } from '@tanstack/react-query'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import {
Alert,
App,
@@ -101,6 +101,8 @@ export default function WorkerConsignmentPage() {
const priceFetchTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
const lastPriceQueryKeyRef = useRef('')
const queryClient = useQueryClient()
const profileQuery = useQuery({
queryKey: ['worker-profile'],
queryFn: () => fetchWorkerProfile(),
@@ -114,6 +116,14 @@ export default function WorkerConsignmentPage() {
queryFn: () => fetchWorkerConsignments(),
})
// 切换标签页时自动刷新最新数据
useEffect(() => {
if (activeTab === 'records') {
void queryClient.invalidateQueries({ queryKey: ['worker-my-consignments'] })
}
void queryClient.invalidateQueries({ queryKey: ['worker-profile'] })
}, [activeTab, queryClient])
// 后端已按「一件货一行」聚合,前端不再合并提报单与库存
const records = myConsignmentsQuery.data?.data.items || []
const stageCounts = myConsignmentsQuery.data?.data.stageCounts || {}
@@ -1104,10 +1114,37 @@ function ConsignmentRecordCard({
</Space>
) : null}
{shots.length > 0 || giftShots.length > 0 ? (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 10, marginTop: 8, alignItems: 'center' }}>
<div
style={{
display: 'flex',
flexWrap: 'wrap',
gap: 10,
marginTop: 8,
alignItems: 'center',
}}
>
{shots.length > 0 ? (
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 8, background: '#f8fafc', padding: '4px 10px', borderRadius: 6, border: '1px solid #e2e8f0' }}>
<Typography.Text type="secondary" style={{ fontSize: 12, display: 'flex', alignItems: 'center', gap: 4, whiteSpace: 'nowrap' }}>
<div
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 8,
background: '#f8fafc',
padding: '4px 10px',
borderRadius: 6,
border: '1px solid #e2e8f0',
}}
>
<Typography.Text
type="secondary"
style={{
fontSize: 12,
display: 'flex',
alignItems: 'center',
gap: 4,
whiteSpace: 'nowrap',
}}
>
<PictureOutlined />
商品截图({shots.length})
</Typography.Text>
@@ -1127,8 +1164,28 @@ function ConsignmentRecordCard({
</div>
) : null}
{giftShots.length > 0 ? (
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 8, background: '#f0fdf4', padding: '4px 10px', borderRadius: 6, border: '1px solid #bbf7d0' }}>
<Typography.Text strong style={{ fontSize: 12, color: '#15803d', display: 'flex', alignItems: 'center', gap: 4, whiteSpace: 'nowrap' }}>
<div
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 8,
background: '#f0fdf4',
padding: '4px 10px',
borderRadius: 6,
border: '1px solid #bbf7d0',
}}
>
<Typography.Text
strong
style={{
fontSize: 12,
color: '#15803d',
display: 'flex',
alignItems: 'center',
gap: 4,
whiteSpace: 'nowrap',
}}
>
<CheckCircleOutlined />
赠送凭证({giftShots.length})
</Typography.Text>
@@ -1146,7 +1203,11 @@ function ConsignmentRecordCard({
</Space>
</Image.PreviewGroup>
{record.giftEvidenceNote ? (
<Typography.Text type="secondary" style={{ fontSize: 11, color: '#4b5563', maxWidth: 160 }} ellipsis={{ tooltip: record.giftEvidenceNote }}>
<Typography.Text
type="secondary"
style={{ fontSize: 11, color: '#4b5563', maxWidth: 160 }}
ellipsis={{ tooltip: record.giftEvidenceNote }}
>
{record.giftEvidenceNote}
</Typography.Text>
) : null}
+4
View File
@@ -26,6 +26,10 @@ export type ConsignmentEvidenceFields = {
giftWorkOrderNo?: string
/** 关联工单状态:in_progress / pending_acceptance / accepted 等 */
giftWorkOrderStatus?: string
/** 关联工单赠送阶段:material_required / material_submitted / friend_countdown / gift_ready 等 */
giftPhase?: string
/** 关联工单确认加好友时间(ISO 字符串,为空代表尚未确认加好友) */
giftFriendAddedAt?: string
/** 赠送凭证的客服验收状态 */
giftEvidenceReviewStatus?: GiftEvidenceReviewStatus
}