feat(worker-platform): 接单工单表格「打手」列支持展示寄售供货打手与现货资料

1. 新增 resolveConsignmentWorkerDisplay 工具函数,自动解析工单寄售现货与资料字段(供货打手、游戏角色昵称、游戏ID及联系方式);
2. 优化 renderWorkOrderWorkers 渲染逻辑:当工单为内部寄售现货履约时,打手列带「供货打手」或「内部现货」专属角标,主标题展示打手名/角色名,副标题拼接游戏ID与联系方式,解决原先显示为破折号 - 的问题;
3. 新增 consignment-worker-display 单元测试,更新寄卖库存优化现状与进度追踪文档,前端 57 个测试全绿并通过类型检查。
This commit is contained in:
yml2213
2026-09-21 11:40:25 +08:00
parent c80bd0e337
commit 29589bb9af
4 changed files with 187 additions and 19 deletions
@@ -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 (
<div className="cell-stack">
<Typography.Text ellipsis={{ tooltip: names }}>{names}</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{order.sharingWorkers.length} 人 · {quantity} 份
</Typography.Text>
</div>
)
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 (
<div className="cell-stack">
<Typography.Text ellipsis={{ tooltip: names }}>{names}</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{order.sharingWorkers.length} 人 · {quantity} 份
</Typography.Text>
</div>
)
}
// 寄卖库存 / 内部现货履约单:回显供货打手或卖家现货资料
const consignmentDisplay = resolveConsignmentWorkerDisplay(order)
if (consignmentDisplay) {
return (
<div className="cell-stack">
<Space size={4} align="center" wrap>
<Tag
color="cyan"
style={{ margin: 0, fontSize: 11, padding: '0 4px', lineHeight: '18px' }}
>
{consignmentDisplay.tagText}
</Tag>
<Typography.Text ellipsis={{ tooltip: consignmentDisplay.primaryName }}>
{consignmentDisplay.primaryName}
</Typography.Text>
</Space>
{consignmentDisplay.secondaryInfo ? (
<Typography.Text
type="secondary"
style={{ fontSize: 12 }}
ellipsis={{ tooltip: consignmentDisplay.secondaryInfo }}
>
{consignmentDisplay.secondaryInfo}
</Typography.Text>
) : null}
</div>
)
}
return '-'
}
export function getDisplayOrderNo(
@@ -0,0 +1,58 @@
import type { WorkOrder } from '@/types/worker-platform'
function asRecord(value: unknown): Record<string, unknown> {
return value && typeof value === 'object' && !Array.isArray(value)
? (value as Record<string, unknown>)
: {}
}
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,
}
}
@@ -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')
})
@@ -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` 时,主标题展示供货打手名称,打上 `<Tag color="cyan">供货打手</Tag>`,副标题自动拼接游戏角色昵称与游戏 ID;
- 当无 `workerName`(纯仓库现货)时,主标题展示卖家角色昵称/游戏 ID,打上 `<Tag color="cyan">内部现货</Tag>`。
2. **列表与详情无缝集成**:
- `renderWorkOrderWorkers` 统一接入 `resolveConsignmentWorkerDisplay`,使工单列表表格中的【打手】列和工单详情抽屉基本信息中的【打手】栏均能清晰、美观地回显供货人。
3. **自动化测试与样式达标**:
- 新增前端单元测试 `apps/frontend/tests/consignment-worker-display.test.ts`,验证普通工单、有供货打手寄售单、无打手纯现货单的解析分支;
- 前端 57 个测试全绿,`format:check`、`lint:check`、`typecheck` 全部通过。