寄卖手动匹配改走内部现货并统一展示与超时口径
- 手动匹配不再指派供货打手:停在待加好友,清空截止时间与超时分钟 - 物品名模板去掉「0 璀璨」残留;寄卖单号无提报号时回退库存编号 - 状态列按寄卖库存显示内部匹配,打手列保留供货打手标签 - 超时配置增加启用开关,默认关闭(不限时) - 补充前后端回归测试
This commit is contained in:
@@ -101,6 +101,7 @@ type ConfigFormValues = {
|
||||
orderEffectiveFrom: Dayjs
|
||||
}
|
||||
timeout: {
|
||||
enabled: boolean
|
||||
hours: number
|
||||
policy: 'reopen' | 'cancel_release' | 'cancel_deduct' | 'rematch_release' | 'rematch_deduct'
|
||||
}
|
||||
@@ -216,7 +217,8 @@ function toFormValues(profile: ConsignmentProfile): ConfigFormValues {
|
||||
orderEffectiveFrom: dayjs(config.matching.orderEffectiveFrom || undefined),
|
||||
},
|
||||
timeout: {
|
||||
hours: Math.max(0, Number(config.timeout.minutes ?? 30) / 60),
|
||||
enabled: Number(config.timeout.minutes ?? 0) > 0,
|
||||
hours: Number(config.timeout.minutes ?? 0) > 0 ? Number(config.timeout.minutes) / 60 : 0.5,
|
||||
policy: config.timeout.policy,
|
||||
},
|
||||
priceDrop: { ...config.priceDrop },
|
||||
@@ -255,7 +257,7 @@ function emptyFormValues(): ConfigFormValues {
|
||||
preferLowReward: true,
|
||||
orderEffectiveFrom: dayjs(),
|
||||
},
|
||||
timeout: { hours: 0.5, policy: 'reopen' },
|
||||
timeout: { enabled: false, hours: 0.5, policy: 'reopen' },
|
||||
priceDrop: { enabled: true, hours: 24, mode: 'percent', value: 10 },
|
||||
}
|
||||
}
|
||||
@@ -338,7 +340,10 @@ function buildPayload(
|
||||
values.matching.orderEffectiveFrom?.toISOString() || new Date().toISOString(),
|
||||
},
|
||||
timeout: {
|
||||
minutes: Math.max(0, Math.round(Number(values.timeout.hours || 0) * 60)),
|
||||
// 开关关闭 = 不限时(minutes=0),后端不会生成截止时间,也不会触发超时处置。
|
||||
minutes: values.timeout.enabled
|
||||
? Math.max(1, Math.round(Number(values.timeout.hours || 0) * 60))
|
||||
: 0,
|
||||
policy: values.timeout.policy,
|
||||
},
|
||||
priceDrop: {
|
||||
@@ -1027,6 +1032,7 @@ export default function ConsignmentConfigPanel() {
|
||||
const serviceFeeMode = Form.useWatch(['serviceFee', 'mode'], form)
|
||||
const inventoryLimitEnabled = Form.useWatch(['inventoryLimit', 'enabled'], form)
|
||||
const autoMatchEnabled = Form.useWatch(['matching', 'autoMatchEnabled'], form)
|
||||
const timeoutEnabled = Form.useWatch(['timeout', 'enabled'], form)
|
||||
const priceDropEnabled = Form.useWatch(['priceDrop', 'enabled'], form)
|
||||
const priceDropMode = Form.useWatch(['priceDrop', 'mode'], form)
|
||||
const headerStatus: ProfileStatusInfo = selectedProfile
|
||||
@@ -1977,47 +1983,62 @@ export default function ConsignmentConfigPanel() {
|
||||
<ConsignmentSectionTitle
|
||||
index="09"
|
||||
title="超时设置"
|
||||
description="匹配成功进入代练中后的时限与超时处置动作"
|
||||
description="默认关闭(不限时);开启后按小时计时并执行超时动作"
|
||||
/>
|
||||
</div>
|
||||
<div style={gridStyle}>
|
||||
<Form.Item
|
||||
label="代练超时(小时)"
|
||||
name={['timeout', 'hours']}
|
||||
rules={[{ type: 'number', min: 0 }]}
|
||||
extra="默认 0.5 小时;到点后按下方超时动作处置"
|
||||
style={{ marginBottom: 0 }}
|
||||
label="启用超时"
|
||||
name={['timeout', 'enabled']}
|
||||
valuePropName="checked"
|
||||
extra="关闭后订单进入代练中不设截止时间(不限时),也不会自动超时处置"
|
||||
style={{ marginBottom: 0, gridColumn: '1 / -1' }}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
step={0.5}
|
||||
precision={1}
|
||||
style={{ width: '100%' }}
|
||||
addonAfter="小时"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="超时动作"
|
||||
name={['timeout', 'policy']}
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'reopen', label: '退回大厅(退还押金)' },
|
||||
{ value: 'cancel_release', label: '取消并释放押金' },
|
||||
{ value: 'cancel_deduct', label: '取消并扣除押金' },
|
||||
{ value: 'rematch_release', label: '重新匹配并退还押金' },
|
||||
{ value: 'rematch_deduct', label: '重新匹配并扣除押金' },
|
||||
]}
|
||||
/>
|
||||
<Switch checkedChildren="开启" unCheckedChildren="关闭" />
|
||||
</Form.Item>
|
||||
{timeoutEnabled ? (
|
||||
<>
|
||||
<Form.Item
|
||||
label="代练超时(小时)"
|
||||
name={['timeout', 'hours']}
|
||||
rules={[{ type: 'number', min: 0.1, message: '请填写大于 0 的小时数' }]}
|
||||
extra="开启后按此小时数生成截止时间"
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<InputNumber
|
||||
min={0.1}
|
||||
step={0.5}
|
||||
precision={1}
|
||||
style={{ width: '100%' }}
|
||||
addonAfter="小时"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="超时动作"
|
||||
name={['timeout', 'policy']}
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'reopen', label: '退回大厅(退还押金)' },
|
||||
{ value: 'cancel_release', label: '取消并释放押金' },
|
||||
{ value: 'cancel_deduct', label: '取消并扣除押金' },
|
||||
{ value: 'rematch_release', label: '重新匹配并退还押金' },
|
||||
{ value: 'rematch_deduct', label: '重新匹配并扣除押金' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginTop: 12 }}
|
||||
message="重新匹配:订单回到内部匹配池,并排除原卖家与原库存,避免同一卖家立刻接回同一单。"
|
||||
/>
|
||||
{timeoutEnabled ? (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginTop: 12 }}
|
||||
message="重新匹配:订单回到内部匹配池,并排除原卖家与原库存,避免同一卖家立刻接回同一单。"
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<section className="order-service-section">
|
||||
|
||||
@@ -1428,7 +1428,7 @@ export default function ConsignmentInventoryPanel() {
|
||||
) : null}
|
||||
{viewingItem.sourceType === 'worker_portal' && viewingItem.createdAt ? (
|
||||
<Descriptions.Item label="打手提报时间">
|
||||
{formatDateTime(viewingItem.createdAt)}
|
||||
{formatDateTime(viewingItem.sourceSubmissionCreatedAt || viewingItem.createdAt)}
|
||||
</Descriptions.Item>
|
||||
) : null}
|
||||
<Descriptions.Item label="卖家微信号">
|
||||
|
||||
@@ -98,6 +98,7 @@ import type { UploadedFile, WorkOrder, WorkOrderShare } from '@/types/worker-pla
|
||||
import { formatConsignmentContactSupporter } from '@/utils/consignment-contact'
|
||||
import {
|
||||
isConsignmentOrder,
|
||||
isConsignmentStockOrder,
|
||||
isInternalConsignmentOrder,
|
||||
resolveConsignmentScreenshotUrls,
|
||||
} from '@/utils/consignment-display'
|
||||
@@ -245,13 +246,11 @@ async function copyTextToClipboard(text: string): Promise<boolean> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 寄卖单号 = 来源提报单号(提报单号即寄卖单号),客服界面只认这一个号。
|
||||
*
|
||||
* 内部仓库号 INV… 仍留在数据里(`info.inventoryNo`,撤销/解绑链路还在用),
|
||||
* 只是不再展示;历史老单没有 sourceSubmissionNo 时明确写出来,不给内部编号。
|
||||
* 寄卖单号:优先来源提报单号(提报即寄卖);历史老单没有提报号时回退库存编号 INV…,
|
||||
* 保证客服手里始终是一个可用于定位的真实单号。
|
||||
*/
|
||||
function resolveConsignmentNo(info: ConsignmentFulfillmentInfo) {
|
||||
return info.sourceSubmissionNo || (info.inventoryNo ? '老单无提报单号' : '')
|
||||
return info.sourceSubmissionNo || info.inventoryNo || ''
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1885,7 +1884,7 @@ export default function WorkOrdersPanel() {
|
||||
置顶
|
||||
</Tag>
|
||||
) : null}
|
||||
{isInternalConsignmentOrder(row) ? (
|
||||
{isConsignmentStockOrder(row) ? (
|
||||
<Tag color="cyan">内部匹配</Tag>
|
||||
) : row.acceptanceMode === 'friend_gift' ? (
|
||||
<Tag color="purple">好友赠送</Tag>
|
||||
|
||||
@@ -29,7 +29,36 @@ export function setSkipPublishConfirm() {
|
||||
|
||||
/** 渲染工单关联打手:普通打手 / 拼单参与打手 / 寄卖供货打手与现货资料 */
|
||||
export function renderWorkOrderWorkers(order: WorkOrder) {
|
||||
if (order.worker) return order.worker.displayName || order.worker.username || '-'
|
||||
const consignmentDisplay = resolveConsignmentWorkerDisplay(order)
|
||||
const assignedName = order.worker
|
||||
? order.worker.displayName || order.worker.username || `打手#${order.worker.workerId}`
|
||||
: ''
|
||||
|
||||
if (order.worker) {
|
||||
const supplierId = consignmentDisplay?.workerId || 0
|
||||
const assignedId = Number(order.worker.workerId || 0)
|
||||
// 押金履约:接单人就是库存供货打手,仍按「供货打手」口径展示。
|
||||
if (consignmentDisplay?.tagText === '供货打手' && supplierId > 0 && supplierId === assignedId) {
|
||||
return renderConsignmentWorkerBlock(consignmentDisplay)
|
||||
}
|
||||
// 指派了别的打手:显示实际接单人,并补一行供货打手,避免丢掉货源信息。
|
||||
if (consignmentDisplay?.tagText === '供货打手') {
|
||||
const supplierText = `供货打手:${consignmentDisplay.primaryName}`
|
||||
return (
|
||||
<div className="cell-stack">
|
||||
<Typography.Text ellipsis={{ tooltip: assignedName }}>{assignedName}</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
style={{ fontSize: 12 }}
|
||||
ellipsis={{ tooltip: supplierText }}
|
||||
>
|
||||
{supplierText}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return assignedName
|
||||
}
|
||||
|
||||
if (order.sharing?.enabled && order.sharingWorkers?.length) {
|
||||
const names = order.sharingWorkers
|
||||
@@ -49,38 +78,40 @@ export function renderWorkOrderWorkers(order: WorkOrder) {
|
||||
)
|
||||
}
|
||||
|
||||
// 寄卖库存 / 内部现货履约单:回显供货打手或卖家现货资料
|
||||
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 renderConsignmentWorkerBlock(consignmentDisplay)
|
||||
}
|
||||
|
||||
return '-'
|
||||
}
|
||||
|
||||
/** 寄卖库存履约:供货打手标签 + 姓名 + 卖家资料 */
|
||||
function renderConsignmentWorkerBlock(
|
||||
display: NonNullable<ReturnType<typeof resolveConsignmentWorkerDisplay>>,
|
||||
) {
|
||||
return (
|
||||
<div className="cell-stack">
|
||||
<Space size={4} align="center" wrap>
|
||||
<Tag color="cyan" style={{ margin: 0, fontSize: 11, padding: '0 4px', lineHeight: '18px' }}>
|
||||
{display.tagText}
|
||||
</Tag>
|
||||
<Typography.Text ellipsis={{ tooltip: display.primaryName }}>
|
||||
{display.primaryName}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
{display.secondaryInfo ? (
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
style={{ fontSize: 12 }}
|
||||
ellipsis={{ tooltip: display.secondaryInfo }}
|
||||
>
|
||||
{display.secondaryInfo}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function getDisplayOrderNo(
|
||||
order: Pick<WorkOrder, 'workOrderNo' | 'platformOrderId'> | null | undefined,
|
||||
): string {
|
||||
|
||||
@@ -139,6 +139,8 @@ export type ConsignmentInventory = ConsignmentEvidenceFields &
|
||||
sourceSubmissionId: number | null
|
||||
/** 来源提报单号(提报单号即寄卖单号);客服手工入库的现货为空字符串。 */
|
||||
sourceSubmissionNo?: string
|
||||
/** 来源提报单创建时间(打手提报时间) */
|
||||
sourceSubmissionCreatedAt?: string | null
|
||||
/** 分类级寄卖配置 */
|
||||
profileId?: number | null
|
||||
profileVersionId?: number | null
|
||||
|
||||
@@ -31,6 +31,8 @@ export type ConsignmentWorkerDisplayInfo = {
|
||||
tagText: '供货打手' | '内部现货'
|
||||
primaryName: string
|
||||
secondaryInfo: string
|
||||
/** 库存登记的供货打手 id;只用于判断「接单人是不是供货本人」 */
|
||||
workerId: number | null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,6 +67,20 @@ export function isConsignmentOrder(row: { material?: unknown; gift?: unknown }):
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否由寄卖库存匹配产生的工单(纯展示口径)。
|
||||
*
|
||||
* 只看工单物料里有没有库存在场,不要求「没有指派打手」:
|
||||
* 押金履约(worker_deposit)同样是寄卖库存匹配,后台状态列也要显示「内部匹配」。
|
||||
* 操作能力判断仍用 isInternalConsignmentOrder,不要混用。
|
||||
*/
|
||||
export function isConsignmentStockOrder(row: { material?: unknown; gift?: unknown }): boolean {
|
||||
const consignment = asRecord(asRecord(row.material).consignment)
|
||||
if (consignment.inventoryId || consignment.inventoryNo) return true
|
||||
const boosterFields = asRecord(asRecord(asRecord(row.gift).boosterMaterial).fields)
|
||||
return Boolean(boosterFields.inventoryNo || boosterFields.sourceType === 'consignment')
|
||||
}
|
||||
|
||||
/** 解析寄卖库存/内部现货履约单的供货人与现货资料信息 */
|
||||
export function resolveConsignmentWorkerDisplay(
|
||||
order: WorkOrder,
|
||||
@@ -107,5 +123,6 @@ export function resolveConsignmentWorkerDisplay(
|
||||
tagText: workerName ? '供货打手' : '内部现货',
|
||||
primaryName,
|
||||
secondaryInfo,
|
||||
workerId: Number(consignment.workerId || 0) > 0 ? Number(consignment.workerId) : null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +169,27 @@ export function buildSpecText(
|
||||
.join(' / ')
|
||||
}
|
||||
|
||||
/**
|
||||
* 物品名模板里跟在占位符后面的固定单位。
|
||||
* 数值字段为 0 时 formatSpecValue 返回空串,单位必须一并省略,
|
||||
* 否则会留下「2钻璀璨」这种没有数字的残句。
|
||||
*/
|
||||
const ITEM_NAME_UNIT_SUFFIX: Record<string, string> = {
|
||||
brillianceCount: '璀璨',
|
||||
diamondLevel: '钻',
|
||||
}
|
||||
|
||||
function fillItemNameTemplate(template: string, tokens: Map<string, string>): string {
|
||||
return template.replace(
|
||||
/\{([a-zA-Z][a-zA-Z0-9_]*)\}(璀璨|钻)?/g,
|
||||
(_, key: string, unit?: string) => {
|
||||
const value = tokens.get(key) || ''
|
||||
if (value) return unit ? `${value}${unit}` : value
|
||||
return ITEM_NAME_UNIT_SUFFIX[key] === unit ? '' : unit || ''
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export function buildSpecItemName(
|
||||
template: string,
|
||||
specs: ConsignmentSpecField[],
|
||||
@@ -183,10 +204,7 @@ export function buildSpecItemName(
|
||||
tokens.set('category', categoryName)
|
||||
let result = String(template || '').trim()
|
||||
if (result) {
|
||||
result = result.replace(
|
||||
/\{([a-zA-Z][a-zA-Z0-9_]*)\}/g,
|
||||
(_, key: string) => tokens.get(key) || '',
|
||||
)
|
||||
result = fillItemNameTemplate(result, tokens)
|
||||
} else {
|
||||
result = specs
|
||||
.filter((field) => field.showInItemName)
|
||||
|
||||
@@ -58,3 +58,25 @@ test('寄售仓库:列表/详情按模板渲染,编辑不再写死旧字段'
|
||||
assert.ok(page.includes('...(hasDynamicSpecs'), '有模板规格时不得再提交旧规格字段')
|
||||
assert.ok(page.includes('请填写「'), '动态必填字段必须在表单层校验')
|
||||
})
|
||||
|
||||
test('物品详情的打手提报时间取来源提报创建时间', () => {
|
||||
const inventoryPage = readPanel('ConsignmentInventoryPanel.tsx')
|
||||
assert.ok(
|
||||
inventoryPage.includes('viewingItem.sourceSubmissionCreatedAt || viewingItem.createdAt'),
|
||||
'打手提报时间必须优先取来源提报创建时间',
|
||||
)
|
||||
|
||||
const repo = readFileSync(
|
||||
fileURLToPath(
|
||||
new URL(
|
||||
'../../backend/src/repositories/worker-platform/consignment-repo.ts',
|
||||
import.meta.url,
|
||||
),
|
||||
),
|
||||
'utf8',
|
||||
)
|
||||
assert.ok(
|
||||
repo.includes('cs.created_at AS source_submission_created_at'),
|
||||
'库存查询必须带来源提报创建时间',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import test from 'node:test'
|
||||
|
||||
import type { WorkOrder } from '../src/types/worker-platform.ts'
|
||||
import {
|
||||
isConsignmentStockOrder,
|
||||
isInternalConsignmentOrder,
|
||||
resolveConsignmentWorkerDisplay,
|
||||
} from '../src/utils/consignment-display.ts'
|
||||
|
||||
function readPanel(name: string) {
|
||||
return readFileSync(
|
||||
fileURLToPath(new URL(`../src/pages/admin/panels/${name}`, import.meta.url)),
|
||||
'utf8',
|
||||
)
|
||||
}
|
||||
|
||||
test('内部匹配展示:押金履约也算寄卖库存匹配', () => {
|
||||
const deposit = {
|
||||
material: {
|
||||
consignment: { inventoryId: 12, fulfillmentType: 'worker_deposit', workerId: 7 },
|
||||
},
|
||||
}
|
||||
assert.equal(isConsignmentStockOrder(deposit), true)
|
||||
assert.equal(
|
||||
isConsignmentStockOrder({ material: { consignment: { inventoryNo: 'INV1' } } }),
|
||||
true,
|
||||
)
|
||||
assert.equal(isConsignmentStockOrder({ material: {} }), false)
|
||||
assert.equal(
|
||||
isInternalConsignmentOrder(deposit),
|
||||
false,
|
||||
'操作判定仍按原口径,不影响加好友文案与撤销入口',
|
||||
)
|
||||
})
|
||||
|
||||
test('内部匹配展示:供货打手 id 会随展示信息返回', () => {
|
||||
const display = resolveConsignmentWorkerDisplay({
|
||||
material: { consignment: { inventoryId: 1, workerId: 7, workerName: '苏尔南2号' } },
|
||||
} as unknown as WorkOrder)
|
||||
assert.equal(display?.tagText, '供货打手')
|
||||
assert.equal(display?.workerId, 7)
|
||||
assert.equal(display?.primaryName, '苏尔南2号')
|
||||
})
|
||||
|
||||
test('手动匹配内部现货:workerName 只是供货人快照,不算接单打手', () => {
|
||||
const manualMatched = {
|
||||
material: {
|
||||
consignment: {
|
||||
inventoryId: 88,
|
||||
inventoryNo: 'INV20261113001',
|
||||
sourceSubmissionNo: 'SUB20261113001',
|
||||
fulfillmentType: 'internal_stock',
|
||||
workerName: '苏尔南2号小号',
|
||||
},
|
||||
},
|
||||
}
|
||||
// 撤销寄卖入口依赖「没有接单打手」这一口径,供货人快照不能把它挡掉
|
||||
assert.equal(isInternalConsignmentOrder(manualMatched), true)
|
||||
assert.equal(isConsignmentStockOrder(manualMatched), true)
|
||||
const display = resolveConsignmentWorkerDisplay(manualMatched as unknown as WorkOrder)
|
||||
assert.equal(display?.tagText, '供货打手')
|
||||
assert.equal(display?.primaryName, '苏尔南2号小号')
|
||||
assert.equal(display?.workerId, null)
|
||||
})
|
||||
|
||||
test('工单列表:内部匹配标签用展示口径,打手列保留供货打手', () => {
|
||||
const orders = readPanel('WorkOrdersPanel.tsx')
|
||||
assert.ok(orders.includes('isConsignmentStockOrder(row)'), '状态列必须用寄卖库存展示判定')
|
||||
assert.ok(
|
||||
orders.includes("return info.sourceSubmissionNo || info.inventoryNo || ''"),
|
||||
'寄卖单号必须回退库存编号',
|
||||
)
|
||||
|
||||
const utils = readFileSync(
|
||||
fileURLToPath(new URL('../src/pages/admin/panels/work-order-panel-utils.tsx', import.meta.url)),
|
||||
'utf8',
|
||||
)
|
||||
assert.ok(utils.includes('supplierId === assignedId'), '接单人就是供货打手时必须保留供货打手标签')
|
||||
assert.ok(
|
||||
utils.includes('供货打手:${consignmentDisplay.primaryName}'),
|
||||
'指派其他打手时必须补一行供货打手信息',
|
||||
)
|
||||
})
|
||||
|
||||
test('配置面板:超时开关默认关闭,关闭即不限时', () => {
|
||||
const panel = readPanel('ConsignmentConfigPanel.tsx')
|
||||
assert.ok(panel.includes("name={['timeout', 'enabled']}"), '超时设置必须有启用开关')
|
||||
assert.ok(
|
||||
panel.includes("timeout: { enabled: false, hours: 0.5, policy: 'reopen' }"),
|
||||
'新建配置默认不限时',
|
||||
)
|
||||
assert.ok(panel.includes('minutes: values.timeout.enabled'), '保存时必须按开关决定 0 或分钟')
|
||||
assert.ok(panel.includes('默认关闭(不限时)'), '配置区文案必须说明默认不限时')
|
||||
})
|
||||
@@ -0,0 +1,67 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import type { ConsignmentSpecField } from '../src/types/consignment.ts'
|
||||
import { buildSpecItemName } from '../src/utils/consignment-spec.ts'
|
||||
|
||||
function makeSpec(
|
||||
overrides: Partial<ConsignmentSpecField> & Pick<ConsignmentSpecField, 'key' | 'label'>,
|
||||
): ConsignmentSpecField {
|
||||
return {
|
||||
type: 'text',
|
||||
required: false,
|
||||
options: [],
|
||||
placeholder: '',
|
||||
helpText: '',
|
||||
matchMode: 'exact',
|
||||
wildcardValues: [],
|
||||
showInItemName: true,
|
||||
sortOrder: 0,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
const surnanSpecs = [
|
||||
makeSpec({ key: 'system', label: '操作系统', sortOrder: 10 }),
|
||||
makeSpec({ key: 'series', label: '系列', sortOrder: 20 }),
|
||||
makeSpec({ key: 'diamondLevel', label: '钻石等级', type: 'number', sortOrder: 30 }),
|
||||
makeSpec({ key: 'brillianceCount', label: '璀璨数', type: 'number', sortOrder: 40 }),
|
||||
]
|
||||
|
||||
const surnanTemplate = '{system}{series}{diamondLevel}钻{brillianceCount}璀璨 苏尔南'
|
||||
|
||||
test('物品名模板:璀璨数为 0 时不残留「璀璨」单位', () => {
|
||||
assert.equal(
|
||||
buildSpecItemName(surnanTemplate, surnanSpecs, {
|
||||
system: '安卓',
|
||||
series: '怒嚎',
|
||||
diamondLevel: '2',
|
||||
brillianceCount: '0',
|
||||
}),
|
||||
'安卓怒嚎2钻 苏尔南',
|
||||
)
|
||||
})
|
||||
|
||||
test('物品名模板:璀璨数大于 0 时单位正常保留', () => {
|
||||
assert.equal(
|
||||
buildSpecItemName(surnanTemplate, surnanSpecs, {
|
||||
system: '安卓',
|
||||
series: '怒嚎',
|
||||
diamondLevel: '2',
|
||||
brillianceCount: '3',
|
||||
}),
|
||||
'安卓怒嚎2钻3璀璨 苏尔南',
|
||||
)
|
||||
})
|
||||
|
||||
test('物品名模板:钻级为空时「钻」也不会单独残留', () => {
|
||||
assert.equal(
|
||||
buildSpecItemName(surnanTemplate, surnanSpecs, {
|
||||
system: '安卓',
|
||||
series: '怒嚎',
|
||||
diamondLevel: '',
|
||||
brillianceCount: '0',
|
||||
}),
|
||||
'安卓怒嚎 苏尔南',
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user