diff --git a/apps/frontend/src/pages/admin/panels/work-order-panel-utils.tsx b/apps/frontend/src/pages/admin/panels/work-order-panel-utils.tsx
index 9acaa539..b31d7668 100644
--- a/apps/frontend/src/pages/admin/panels/work-order-panel-utils.tsx
+++ b/apps/frontend/src/pages/admin/panels/work-order-panel-utils.tsx
@@ -1,6 +1,7 @@
-import { Typography } from 'antd'
+import { Space, Tag, Typography } from 'antd'
import type { CollectField, WorkOrder } from '@/types/worker-platform'
+import { resolveConsignmentWorkerDisplay } from '@/utils/consignment-display'
import { asRecord, formatMoney } from './shared'
@@ -27,25 +28,58 @@ export function formatUserPaymentAmount(order: WorkOrder): string {
return paymentFen === null ? '-' : formatMoney(paymentFen)
}
-/** 拼单母单没有单一 assigned worker,改为展示当前参与打手及其承接份数。 */
+/** 渲染工单关联打手:普通打手 / 拼单参与打手 / 寄卖供货打手与现货资料 */
export function renderWorkOrderWorkers(order: WorkOrder) {
if (order.worker) return order.worker.displayName || order.worker.username || '-'
- if (!order.sharing?.enabled || !order.sharingWorkers?.length) return '-'
- const names = order.sharingWorkers
- .map((worker) => worker.displayName || worker.username || `打手#${worker.workerId}`)
- .join('、')
- const quantity = order.sharingWorkers.reduce(
- (sum, worker) => sum + Number(worker.quantity || 0),
- 0,
- )
- return (
-
- {names}
-
- {order.sharingWorkers.length} 人 · {quantity} 份
-
-
- )
+
+ if (order.sharing?.enabled && order.sharingWorkers?.length) {
+ const names = order.sharingWorkers
+ .map((worker) => worker.displayName || worker.username || `打手#${worker.workerId}`)
+ .join('、')
+ const quantity = order.sharingWorkers.reduce(
+ (sum, worker) => sum + Number(worker.quantity || 0),
+ 0,
+ )
+ return (
+
+ {names}
+
+ {order.sharingWorkers.length} 人 · {quantity} 份
+
+
+ )
+ }
+
+ // 寄卖库存 / 内部现货履约单:回显供货打手或卖家现货资料
+ const consignmentDisplay = resolveConsignmentWorkerDisplay(order)
+ if (consignmentDisplay) {
+ return (
+
+
+
+ {consignmentDisplay.tagText}
+
+
+ {consignmentDisplay.primaryName}
+
+
+ {consignmentDisplay.secondaryInfo ? (
+
+ {consignmentDisplay.secondaryInfo}
+
+ ) : null}
+
+ )
+ }
+
+ return '-'
}
export function getDisplayOrderNo(
diff --git a/apps/frontend/src/utils/consignment-display.ts b/apps/frontend/src/utils/consignment-display.ts
new file mode 100644
index 00000000..00b6de84
--- /dev/null
+++ b/apps/frontend/src/utils/consignment-display.ts
@@ -0,0 +1,58 @@
+import type { WorkOrder } from '@/types/worker-platform'
+
+function asRecord(value: unknown): Record {
+ return value && typeof value === 'object' && !Array.isArray(value)
+ ? (value as Record)
+ : {}
+}
+
+export type ConsignmentWorkerDisplayInfo = {
+ tagText: '供货打手' | '内部现货'
+ primaryName: string
+ secondaryInfo: string
+}
+
+/** 解析寄卖库存/内部现货履约单的供货人与现货资料信息 */
+export function resolveConsignmentWorkerDisplay(
+ order: WorkOrder,
+): ConsignmentWorkerDisplayInfo | null {
+ const material = asRecord(order.material)
+ const consignment = asRecord(material.consignment)
+ const boosterFields = asRecord(order.gift?.boosterMaterial?.fields)
+ const hasConsignment =
+ Object.keys(consignment).length > 0 ||
+ Boolean(boosterFields.sourceType === 'consignment' || boosterFields.inventoryNo)
+
+ if (!hasConsignment) return null
+
+ const workerName = String(consignment.workerName || consignment.worker_name || '').trim()
+ const sellerNickname = String(
+ boosterFields.gameNickname || consignment.buyerName || consignment.sellerGameNickname || '',
+ ).trim()
+ const sellerGameId = String(
+ boosterFields.gameId || consignment.sellerGameId || consignment.seller_game_id || '',
+ ).trim()
+ const sellerContact = String(
+ boosterFields.sellerPhone ||
+ consignment.sellerPhone ||
+ consignment.workerPhone ||
+ boosterFields.sellerContact ||
+ consignment.sellerContact ||
+ '',
+ ).trim()
+
+ const primaryName = workerName || sellerNickname || sellerGameId || '内部现货'
+ const secondaryInfo = [
+ workerName && sellerNickname && sellerNickname !== workerName ? sellerNickname : '',
+ sellerGameId ? `ID:${sellerGameId}` : '',
+ !sellerGameId && sellerContact ? sellerContact : '',
+ ]
+ .filter(Boolean)
+ .join(' · ')
+
+ return {
+ tagText: workerName ? '供货打手' : '内部现货',
+ primaryName,
+ secondaryInfo,
+ }
+}
diff --git a/apps/frontend/tests/consignment-worker-display.test.ts b/apps/frontend/tests/consignment-worker-display.test.ts
new file mode 100644
index 00000000..5b9fb66e
--- /dev/null
+++ b/apps/frontend/tests/consignment-worker-display.test.ts
@@ -0,0 +1,55 @@
+import assert from 'node:assert/strict'
+import test from 'node:test'
+
+import { resolveConsignmentWorkerDisplay } from '../src/utils/consignment-display.ts'
+
+test('普通工单无寄售信息时 resolveConsignmentWorkerDisplay 返回 null', () => {
+ const order = {
+ material: {},
+ } as never
+ assert.equal(resolveConsignmentWorkerDisplay(order), null)
+})
+
+test('寄售工单有供货打手时优先展示供货打手与角色ID', () => {
+ const order = {
+ material: {
+ consignment: {
+ workerName: '01邀请的',
+ buyerName: '客服',
+ sellerGameId: '草莓酱板鸭 (1234551)',
+ sellerPhone: '16677778888',
+ },
+ },
+ gift: {
+ boosterMaterial: {
+ fields: {
+ gameNickname: '客服',
+ gameId: '草莓酱板鸭 (1234551)',
+ },
+ },
+ },
+ } as never
+
+ const result = resolveConsignmentWorkerDisplay(order)
+ assert.ok(result)
+ assert.equal(result.tagText, '供货打手')
+ assert.equal(result.primaryName, '01邀请的')
+ assert.equal(result.secondaryInfo, '客服 · ID:草莓酱板鸭 (1234551)')
+})
+
+test('寄售工单无供货打手名时回退展示角色昵称与现货标签', () => {
+ const order = {
+ material: {
+ consignment: {
+ buyerName: '客服角色',
+ sellerGameId: '1234551',
+ },
+ },
+ } as never
+
+ const result = resolveConsignmentWorkerDisplay(order)
+ assert.ok(result)
+ assert.equal(result.tagText, '内部现货')
+ assert.equal(result.primaryName, '客服角色')
+ assert.equal(result.secondaryInfo, 'ID:1234551')
+})
diff --git a/docs/寄卖库存优化现状与进度追踪.md b/docs/寄卖库存优化现状与进度追踪.md
index be3db0f7..74ec43f8 100644
--- a/docs/寄卖库存优化现状与进度追踪.md
+++ b/docs/寄卖库存优化现状与进度追踪.md
@@ -140,7 +140,7 @@
1. **手动匹配弹窗收敛为单一【内部现货直接履约】**:
- 在 `ConsignmentManualMatchModal.tsx` 中彻底废除 `assignWorkOrderWithConsignmentWorker`,切换为调用 `fulfillWorkOrderWithConsignment`;
- 彻底解除 `selected.workerId` 强依赖与无法指派错误提示,详情卡片调整为展示供货来源(“供货打手”或“内部现货”);
- - 文案统一调整为“确认内部现货履约”,明确告知“免外派佣金、不冻结重复押金、自动跳过打手资料”。
+ - 文案统一调整为“确认内部现货履约”,明确告知“免外派佣金、不冻结重复押金、自动跳过打手资料”。
2. **工单发布拦截弹窗候选操作收敛为【内部现货直接履约】**:
- 在 `WorkOrdersPanel.tsx` 中将卡片操作按钮替换为【内部现货直接履约】高亮主按钮(绿色),副标题标注“免外派佣金 · 不扣打手押金 · 自动进冷静期”;
- 杜绝客服误点走旧指派流程。
@@ -148,4 +148,25 @@
- `consignment-manual-match-mount.test.ts` 更新为断言手动匹配走内部现货直接履约;
- 前端 54 测试全绿、后端 501 测试全绿;两侧 `typecheck`、`format:check`、`build` 均 100% 通过。
+---
+## 六、 专项优化:接单工单表格「打手」列回显寄售供货打手与现货资料(2026-09-21 已闭环)
+
+### 1. 现象与用户反馈
+- **问题现象**:接单工单在走内部现货直接履约后,工单列表表格的【打手】列以及工单详情基本信息中的【打手】字段显示为冰冷的一横杠 `-`,客服/运营在列表视图下无法直观查看是谁供的货或对应哪个游戏角色。
+
+### 2. 代码级根因定位
+- `apps/frontend/src/pages/admin/panels/work-order-panel-utils.tsx:31`:
+ 原 `renderWorkOrderWorkers(order)` 仅针对 `order.worker`(传统指派/抢单打手)与 `order.sharingWorkers`(拼单打手)进行了渲染,而内部现货履约单没有 `order.worker`,直接走到底部返回了 `'-'`。
+
+### 3. 解决对策与落地成果
+1. **提炼寄售供货展示纯函数**:
+ - 在 `apps/frontend/src/utils/consignment-display.ts` 中建立 `resolveConsignmentWorkerDisplay(order: WorkOrder)`;
+ - 自动解析 `material.consignment` 与 `gift.boosterMaterial.fields`:
+ - 当存在 `workerName` 时,主标题展示供货打手名称,打上 `供货打手`,副标题自动拼接游戏角色昵称与游戏 ID;
+ - 当无 `workerName`(纯仓库现货)时,主标题展示卖家角色昵称/游戏 ID,打上 `内部现货`。
+2. **列表与详情无缝集成**:
+ - `renderWorkOrderWorkers` 统一接入 `resolveConsignmentWorkerDisplay`,使工单列表表格中的【打手】列和工单详情抽屉基本信息中的【打手】栏均能清晰、美观地回显供货人。
+3. **自动化测试与样式达标**:
+ - 新增前端单元测试 `apps/frontend/tests/consignment-worker-display.test.ts`,验证普通工单、有供货打手寄售单、无打手纯现货单的解析分支;
+ - 前端 57 个测试全绿,`format:check`、`lint:check`、`typecheck` 全部通过。