feat(consignment): 后台寄卖规格按分类模板动态渲染

This commit is contained in:
2026-11-08 11:50:51 +08:00
parent caa2358bbd
commit e03abc3a8a
5 changed files with 483 additions and 142 deletions
@@ -62,6 +62,11 @@ import { fetchAdminSession } from '@/services/admin/auth'
import { resolveAdminPermission } from '@/utils/admin-auth' import { resolveAdminPermission } from '@/utils/admin-auth'
import { formatConsignmentContactSupporter } from '@/utils/consignment-contact' import { formatConsignmentContactSupporter } from '@/utils/consignment-contact'
import { describeGiftEvidence, resolveGiftEvidenceBadge } from '@/utils/consignment-evidence' import { describeGiftEvidence, resolveGiftEvidenceBadge } from '@/utils/consignment-evidence'
import {
CONSIGNMENT_SPEC_TAG_COLORS,
resolveConsignmentSpecDisplayItems,
resolveConsignmentSpecText,
} from '@/utils/consignment-spec-display'
import { CONSIGNMENT_SYSTEM_OPTIONS, CONSIGNMENT_ZONE_OPTIONS } from '@/utils/consignment-spec' import { CONSIGNMENT_SYSTEM_OPTIONS, CONSIGNMENT_ZONE_OPTIONS } from '@/utils/consignment-spec'
import { formatDateTime } from '@/utils/date-time' import { formatDateTime } from '@/utils/date-time'
@@ -301,14 +306,21 @@ export default function ConsignmentInventoryPanel() {
? Math.round(Number(values.depositAmount) * 100) ? Math.round(Number(values.depositAmount) * 100)
: undefined : undefined
const selectedWorker = workerOptions.find((w) => w.value === values.workerId) const selectedWorker = workerOptions.find((w) => w.value === values.workerId)
// 有动态规格时规格值走 specValues(后端据此重算 spec_text),旧列不再写,
// 否则密钥这类只有「商品」的分类会被顺手写成 1钻0璀璨。
const hasDynamicSpecs = Boolean(editingItem.specs?.length)
await updateAdminConsignmentInventory(editingItem.id, { await updateAdminConsignmentInventory(editingItem.id, {
itemName: values.itemName, itemName: values.itemName,
system: values.system, ...(hasDynamicSpecs
serverZone: values.serverZone, ? {}
series: values.series, : {
diamondLevel: Number(values.diamondLevel || 1), system: values.system,
brillianceCount: Number(values.brillianceCount || 0), serverZone: values.serverZone,
series: values.series,
diamondLevel: Number(values.diamondLevel || 1),
brillianceCount: Number(values.brillianceCount || 0),
}),
costPriceFen, costPriceFen,
salePriceFen: salePriceFen:
values.salePrice !== undefined values.salePrice !== undefined
@@ -471,28 +483,46 @@ export default function ConsignmentInventoryPanel() {
), ),
}, },
{ {
title: '和平精英规格', title: '寄卖规格',
key: 'specs', key: 'specs',
width: 200, width: 220,
render: (_, record) => ( render: (_, record) => {
<Space direction="vertical" size={2}> // 规格按该分类已发布的寄卖模板渲染;没有规格配置的历史库存才回退旧字段。
<div> const specItems = resolveConsignmentSpecDisplayItems(record.specs, record.specValues)
<Tag color={record.system === '苹果' ? 'geekblue' : 'green'}> if (specItems.length > 0) {
{record.system || '通用'} return (
</Tag> <Space size={4} wrap>
<Tag color={record.serverZone === '微信区' ? 'success' : 'processing'}> {specItems.map((item, index) => (
{record.serverZone || '全区'} <Tag
</Tag> key={item.key}
</div> color={CONSIGNMENT_SPEC_TAG_COLORS[index % CONSIGNMENT_SPEC_TAG_COLORS.length]}
<div> >
<Tag color="orange">{record.series || '通用'}</Tag> {item.label} {item.value}
<Tag color="gold">{record.diamondLevel} 钻</Tag> </Tag>
{record.brillianceCount > 0 ? ( ))}
<Tag color="magenta">{record.brillianceCount} 璀璨</Tag> </Space>
) : null} )
</div> }
</Space> return (
), <Space direction="vertical" size={2}>
<div>
<Tag color={record.system === '苹果' ? 'geekblue' : 'green'}>
{record.system || '通用'}
</Tag>
<Tag color={record.serverZone === '微信区' ? 'success' : 'processing'}>
{record.serverZone || '全区'}
</Tag>
</div>
<div>
<Tag color="orange">{record.series || '通用'}</Tag>
<Tag color="gold">{record.diamondLevel} 钻</Tag>
{record.brillianceCount > 0 ? (
<Tag color="magenta">{record.brillianceCount} 璀璨</Tag>
) : null}
</div>
</Space>
)
},
}, },
{ {
title: '收购价', title: '收购价',
@@ -1047,34 +1077,36 @@ export default function ConsignmentInventoryPanel() {
</Col> </Col>
</Row> </Row>
<Row gutter={16}> {editingItem?.specs?.length ? null : (
<Col span={5}> <Row gutter={16}>
<Form.Item name="system" label="系统"> <Col span={5}>
{/* 后台全量选项:系统 苹果/安卓/通用,区服 微信/QQ/互通(互通=QQ/微信都能收) */} <Form.Item name="system" label="系统">
<Select options={[...CONSIGNMENT_SYSTEM_OPTIONS]} /> {/* 后台全量选项:系统 苹果/安卓/通用,区服 微信/QQ/互通(互通=QQ/微信都能收) */}
</Form.Item> <Select options={[...CONSIGNMENT_SYSTEM_OPTIONS]} />
</Col> </Form.Item>
<Col span={5}> </Col>
<Form.Item name="serverZone" label="区服"> <Col span={5}>
<Select options={[...CONSIGNMENT_ZONE_OPTIONS]} /> <Form.Item name="serverZone" label="区服">
</Form.Item> <Select options={[...CONSIGNMENT_ZONE_OPTIONS]} />
</Col> </Form.Item>
<Col span={5}> </Col>
<Form.Item name="series" label="系列"> <Col span={5}>
<Input /> <Form.Item name="series" label="系列">
</Form.Item> <Input />
</Col> </Form.Item>
<Col span={5}> </Col>
<Form.Item name="diamondLevel" label="钻石等级 (1-7)"> <Col span={5}>
<InputNumber min={1} max={7} style={{ width: '100%' }} /> <Form.Item name="diamondLevel" label="钻石等级 (1-7)">
</Form.Item> <InputNumber min={1} max={7} style={{ width: '100%' }} />
</Col> </Form.Item>
<Col span={4}> </Col>
<Form.Item name="brillianceCount" label="璀璨数 (0-7)"> <Col span={4}>
<InputNumber min={0} max={7} style={{ width: '100%' }} /> <Form.Item name="brillianceCount" label="璀璨数 (0-7)">
</Form.Item> <InputNumber min={0} max={7} style={{ width: '100%' }} />
</Col> </Form.Item>
</Row> </Col>
</Row>
)}
{editingItem?.specs?.length ? ( {editingItem?.specs?.length ? (
<> <>
@@ -1082,7 +1114,15 @@ export default function ConsignmentInventoryPanel() {
<Row gutter={16}> <Row gutter={16}>
{editingItem.specs.map((field) => ( {editingItem.specs.map((field) => (
<Col span={8} key={field.key}> <Col span={8} key={field.key}>
<Form.Item name={['specValues', field.key]} label={field.label}> <Form.Item
name={['specValues', field.key]}
label={field.label}
rules={
field.required
? [{ required: true, message: `请填写「${field.label}」` }]
: undefined
}
>
{field.type === 'select' || field.type === 'radio' ? ( {field.type === 'select' || field.type === 'radio' ? (
field.type === 'radio' ? ( field.type === 'radio' ? (
<Radio.Group <Radio.Group
@@ -1254,8 +1294,7 @@ export default function ConsignmentInventoryPanel() {
)} )}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="规格"> <Descriptions.Item label="规格">
{viewingItem.system} / {viewingItem.serverZone} / {viewingItem.series} /{' '} {resolveConsignmentSpecText(viewingItem)}
{viewingItem.diamondLevel}钻 ({viewingItem.brillianceCount}璀璨)
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="收购成本"> <Descriptions.Item label="收购成本">
<Text strong style={{ color: '#cf1322' }}> <Text strong style={{ color: '#cf1322' }}>
@@ -12,6 +12,7 @@ import {
Card, Card,
Col, Col,
Descriptions, Descriptions,
Divider,
Form, Form,
Input, Input,
InputNumber, InputNumber,
@@ -43,6 +44,11 @@ import type { ConsignmentSubmission, GiftEvidenceReviewStatus } from '@/types/co
import { resolveAdminPermission } from '@/utils/admin-auth' import { resolveAdminPermission } from '@/utils/admin-auth'
import { formatConsignmentContactSupporter } from '@/utils/consignment-contact' import { formatConsignmentContactSupporter } from '@/utils/consignment-contact'
import { describeGiftEvidence, resolveGiftEvidenceBadge } from '@/utils/consignment-evidence' import { describeGiftEvidence, resolveGiftEvidenceBadge } from '@/utils/consignment-evidence'
import {
CONSIGNMENT_SPEC_TAG_COLORS,
resolveConsignmentSpecDisplayItems,
resolveConsignmentSpecText,
} from '@/utils/consignment-spec-display'
import { CONSIGNMENT_SYSTEM_OPTIONS, CONSIGNMENT_ZONE_OPTIONS } from '@/utils/consignment-spec' import { CONSIGNMENT_SYSTEM_OPTIONS, CONSIGNMENT_ZONE_OPTIONS } from '@/utils/consignment-spec'
import { formatDateTime } from '@/utils/date-time' import { formatDateTime } from '@/utils/date-time'
@@ -154,6 +160,9 @@ export default function ConsignmentSubmissionsPanel() {
? Math.round(Number(values.depositAmount) * 100) ? Math.round(Number(values.depositAmount) * 100)
: undefined : undefined
const selectedWorker = workerOptions.find((w) => w.value === values.workerId) const selectedWorker = workerOptions.find((w) => w.value === values.workerId)
// 有动态规格时规格值由 specValues 下发给后端推导 spec_text;
// 旧的 系统/区服/系列/钻级/璀璨 列只是历史兼容,不能再写(否则密钥类目会被写成 1钻0璀璨)。
const hasDynamicSpecs = Boolean(approveModalItem.specs?.length)
await approveAdminConsignmentSubmission(approveModalItem.id, { await approveAdminConsignmentSubmission(approveModalItem.id, {
costPriceFen, costPriceFen,
@@ -161,11 +170,15 @@ export default function ConsignmentSubmissionsPanel() {
shopName: values.shopName, shopName: values.shopName,
inboundBatch: values.inboundBatch, inboundBatch: values.inboundBatch,
itemName: values.itemName, itemName: values.itemName,
system: values.system, ...(hasDynamicSpecs
serverZone: values.serverZone, ? {}
series: values.series, : {
diamondLevel: Number(values.diamondLevel || 1), system: values.system,
brillianceCount: Number(values.brillianceCount || 0), serverZone: values.serverZone,
series: values.series,
diamondLevel: Number(values.diamondLevel || 1),
brillianceCount: Number(values.brillianceCount || 0),
}),
specValues: specValues:
values.specValues && Object.keys(values.specValues).length values.specValues && Object.keys(values.specValues).length
? values.specValues ? values.specValues
@@ -229,21 +242,38 @@ export default function ConsignmentSubmissionsPanel() {
{ {
title: '提报物品规格', title: '提报物品规格',
key: 'specs', key: 'specs',
width: 210, width: 230,
render: (_, record) => ( render: (_, record) => {
<Space direction="vertical" size={2}> // 规格按该分类已发布的寄卖模板渲染;没配置规格的历史提报才回退旧字段。
<div style={{ fontWeight: 600 }}>{record.itemName}</div> const specItems = resolveConsignmentSpecDisplayItems(record.specs, record.specValues)
<Space size={4} wrap> return (
<Tag color={record.system === '苹果' ? 'geekblue' : 'green'}>{record.system}</Tag> <Space direction="vertical" size={2}>
<Tag color="cyan">{record.serverZone}</Tag> <div style={{ fontWeight: 600 }}>{record.itemName}</div>
<Tag color="orange">{record.series}</Tag> <Space size={4} wrap>
<Tag color="gold">{record.diamondLevel} 钻</Tag> {specItems.length > 0 ? (
{record.brillianceCount > 0 ? ( specItems.map((item, index) => (
<Tag color="magenta">{record.brillianceCount} 璀璨</Tag> <Tag
) : null} key={item.key}
color={CONSIGNMENT_SPEC_TAG_COLORS[index % CONSIGNMENT_SPEC_TAG_COLORS.length]}
>
{item.label} {item.value}
</Tag>
))
) : (
<>
<Tag color={record.system === '苹果' ? 'geekblue' : 'green'}>{record.system}</Tag>
<Tag color="cyan">{record.serverZone}</Tag>
<Tag color="orange">{record.series}</Tag>
<Tag color="gold">{record.diamondLevel} 钻</Tag>
{record.brillianceCount > 0 ? (
<Tag color="magenta">{record.brillianceCount} 璀璨</Tag>
) : null}
</>
)}
</Space>
</Space> </Space>
</Space> )
), },
}, },
{ {
title: '卖家报价', title: '卖家报价',
@@ -647,73 +677,85 @@ export default function ConsignmentSubmissionsPanel() {
</Col> </Col>
</Row> </Row>
<Row gutter={12}> {approveModalItem?.specs?.length ? null : (
<Col span={5}> <Row gutter={12}>
<Form.Item name="system" label="系统" style={{ marginBottom: 16 }}> <Col span={5}>
<Select options={[...CONSIGNMENT_SYSTEM_OPTIONS]} /> <Form.Item name="system" label="系统" style={{ marginBottom: 16 }}>
</Form.Item> <Select options={[...CONSIGNMENT_SYSTEM_OPTIONS]} />
</Col> </Form.Item>
<Col span={5}> </Col>
<Form.Item name="serverZone" label="区服" style={{ marginBottom: 16 }}> <Col span={5}>
<Select options={[...CONSIGNMENT_ZONE_OPTIONS]} /> <Form.Item name="serverZone" label="区服" style={{ marginBottom: 16 }}>
</Form.Item> <Select options={[...CONSIGNMENT_ZONE_OPTIONS]} />
</Col> </Form.Item>
<Col span={5}> </Col>
<Form.Item name="series" label="系列" style={{ marginBottom: 16 }}> <Col span={5}>
<Input placeholder="如 暗潮" /> <Form.Item name="series" label="系列" style={{ marginBottom: 16 }}>
</Form.Item> <Input placeholder="如 暗潮" />
</Col> </Form.Item>
<Col span={4}> </Col>
<Form.Item name="diamondLevel" label="钻石等级" style={{ marginBottom: 16 }}> <Col span={4}>
<InputNumber min={1} max={7} style={{ width: '100%' }} /> <Form.Item name="diamondLevel" label="钻石等级" style={{ marginBottom: 16 }}>
</Form.Item> <InputNumber min={1} max={7} style={{ width: '100%' }} />
</Col> </Form.Item>
<Col span={5}> </Col>
<Form.Item name="brillianceCount" label="璀璨数" style={{ marginBottom: 16 }}> <Col span={5}>
<InputNumber min={0} max={7} style={{ width: '100%' }} /> <Form.Item name="brillianceCount" label="璀璨数" style={{ marginBottom: 16 }}>
</Form.Item> <InputNumber min={0} max={7} style={{ width: '100%' }} />
</Col> </Form.Item>
</Row> </Col>
</Row>
)}
{approveModalItem?.specs?.length ? ( {approveModalItem?.specs?.length ? (
<Row gutter={12}> <>
{approveModalItem.specs.map((field) => ( <Divider plain style={{ fontSize: 12, margin: '0 0 12px' }}>
<Col span={8} key={field.key}> 规格(按寄卖分类模板)
<Form.Item </Divider>
name={['specValues', field.key]} <Row gutter={12}>
label={field.label} {approveModalItem.specs.map((field) => (
style={{ marginBottom: 16 }} <Col span={8} key={field.key}>
> <Form.Item
{field.type === 'select' || field.type === 'radio' ? ( name={['specValues', field.key]}
field.type === 'radio' ? ( label={field.label}
<Radio.Group rules={
options={field.options field.required
.filter((option) => option.enabled !== false) ? [{ required: true, message: `请填写「${field.label}」` }]
.map((option) => ({ label: option.label, value: option.value }))} : undefined
}
style={{ marginBottom: 16 }}
>
{field.type === 'select' || field.type === 'radio' ? (
field.type === 'radio' ? (
<Radio.Group
options={field.options
.filter((option) => option.enabled !== false)
.map((option) => ({ label: option.label, value: option.value }))}
/>
) : (
<Select
options={field.options
.filter((option) => option.enabled !== false)
.map((option) => ({ label: option.label, value: option.value }))}
/>
)
) : field.type === 'number' ? (
<InputNumber style={{ width: '100%' }} />
) : field.type === 'date' ? (
<Input type="date" />
) : field.type === 'textarea' ? (
<Input.TextArea
placeholder={field.placeholder || undefined}
autoSize={{ minRows: 2, maxRows: 4 }}
/> />
) : ( ) : (
<Select <Input placeholder={field.placeholder || undefined} />
options={field.options )}
.filter((option) => option.enabled !== false) </Form.Item>
.map((option) => ({ label: option.label, value: option.value }))} </Col>
/> ))}
) </Row>
) : field.type === 'number' ? ( </>
<InputNumber style={{ width: '100%' }} />
) : field.type === 'date' ? (
<Input type="date" />
) : field.type === 'textarea' ? (
<Input.TextArea
placeholder={field.placeholder || undefined}
autoSize={{ minRows: 2, maxRows: 4 }}
/>
) : (
<Input placeholder={field.placeholder || undefined} />
)}
</Form.Item>
</Col>
))}
</Row>
) : null} ) : null}
{/* 分组二:履约与打手 */} {/* 分组二:履约与打手 */}
@@ -888,9 +930,7 @@ export default function ConsignmentSubmissionsPanel() {
{viewingSubmission.itemName} {viewingSubmission.itemName}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="规格"> <Descriptions.Item label="规格">
{viewingSubmission.system} / {viewingSubmission.serverZone} /{' '} {resolveConsignmentSpecText(viewingSubmission)}
{viewingSubmission.series} / {viewingSubmission.diamondLevel}钻 (
{viewingSubmission.brillianceCount}璀璨)
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="卖家报价"> <Descriptions.Item label="卖家报价">
<Text strong style={{ color: '#cf1322' }}> <Text strong style={{ color: '#cf1322' }}>
@@ -0,0 +1,90 @@
import type { ConsignmentSpecField, ConsignmentSpecValues } from '../types/consignment'
export type ConsignmentSpecDisplayItem = {
key: string
label: string
value: string
}
/** 旧版内置规格字段(没有动态规格配置的历史分类才会用到)。 */
export type ConsignmentLegacySpecRecord = {
system?: string
serverZone?: string
series?: string
diamondLevel?: number | null
brillianceCount?: number | null
}
/** 动态规格字段按模板顺序展示时的标签配色。 */
export const CONSIGNMENT_SPEC_TAG_COLORS = [
'geekblue',
'cyan',
'orange',
'gold',
'magenta',
'green',
]
/** 选项值统一按字符串匹配(配置里 value 是字符串,历史数据可能是数字)。 */
function formatSpecValue(field: ConsignmentSpecField, raw: unknown): string {
if (raw === undefined || raw === null) return ''
const text = String(raw).trim()
if (!text) return ''
if (field.type === 'select' || field.type === 'radio') {
const option = field.options?.find((item) => String(item.value) === text)
return option?.label || text
}
return text
}
/**
* 按寄卖分类模板把 specValues 转成可展示的 {label, value}。
*
* 顺序与后端 buildConsignmentSpecText 保持一致(sortOrder 升序,其次 key),
* 选项型字段把存储的选项值映射回选项名称(如 product=15 → 碎梦浮光珍宝礼盒)。
*/
export function resolveConsignmentSpecDisplayItems(
specs: ConsignmentSpecField[] | null | undefined,
values: ConsignmentSpecValues | null | undefined,
): ConsignmentSpecDisplayItem[] {
if (!specs || specs.length === 0) return []
return [...specs]
.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0) || a.key.localeCompare(b.key))
.map((field) => ({
key: field.key,
label: field.label,
value: formatSpecValue(field, values?.[field.key]),
}))
.filter((item) => item.value !== '')
}
/** 旧版内置字段拼成一行(历史分类 & 历史数据兜底)。 */
export function formatLegacyConsignmentSpec(record: ConsignmentLegacySpecRecord): string {
const parts = [
record.system,
record.serverZone,
record.series,
record.diamondLevel ? `${record.diamondLevel}钻` : '',
record.brillianceCount ? `${record.brillianceCount}璀璨` : '',
]
.map((part) => String(part ?? '').trim())
.filter(Boolean)
return parts.join(' / ')
}
/**
* 详情页规格文本:后端 specText 优先(已含选项名称),其次按模板现拼,最后回退旧字段。
*/
export function resolveConsignmentSpecText(
input: {
specText?: string
specs?: ConsignmentSpecField[] | null
specValues?: ConsignmentSpecValues | null
} & ConsignmentLegacySpecRecord,
): string {
const specText = String(input.specText ?? '').trim()
if (specText) return specText
const items = resolveConsignmentSpecDisplayItems(input.specs, input.specValues)
if (items.length > 0) return items.map((item) => `${item.label} ${item.value}`).join(' / ')
return formatLegacyConsignmentSpec(input) || '-'
}
@@ -0,0 +1,60 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import test from 'node:test'
function readPanel(name: string) {
return readFileSync(
fileURLToPath(new URL(`../src/pages/admin/panels/${name}`, import.meta.url)),
'utf8',
)
}
test('提报审核:规格按寄卖模板渲染,有模板时不再写死旧字段', () => {
const page = readPanel('ConsignmentSubmissionsPanel.tsx')
assert.ok(
page.includes('resolveConsignmentSpecDisplayItems(record.specs, record.specValues)'),
'提报列表必须按模板字段渲染规格',
)
assert.ok(
page.includes('resolveConsignmentSpecText(viewingSubmission)'),
'提报详情必须使用模板规格文本',
)
assert.ok(
page.includes('{approveModalItem?.specs?.length ? null : ('),
'有模板规格时审核弹窗必须隐藏系统/区服/系列/钻级/璀璨旧字段',
)
assert.ok(
page.includes('const hasDynamicSpecs = Boolean(approveModalItem.specs?.length)'),
'提交前必须判断该分类是否有模板规格',
)
assert.ok(page.includes('...(hasDynamicSpecs'), '有模板规格时不得再提交旧规格字段')
assert.ok(page.includes('规格(按寄卖分类模板)'), '审核弹窗需要标明规格来自寄卖模板')
assert.ok(page.includes('请填写「'), '动态必填字段必须在表单层校验')
})
test('寄售仓库:列表/详情按模板渲染,编辑不再写死旧字段', () => {
const page = readPanel('ConsignmentInventoryPanel.tsx')
assert.ok(!page.includes("title: '和平精英规格'"), '仓库列表列名不得写死和平精英')
assert.ok(page.includes("title: '寄卖规格'"), '仓库列表列名应为通用「寄卖规格」')
assert.ok(
page.includes('resolveConsignmentSpecDisplayItems(record.specs, record.specValues)'),
'仓库列表必须按模板字段渲染规格',
)
assert.ok(
page.includes('resolveConsignmentSpecText(viewingItem)'),
'仓库详情必须使用模板规格文本',
)
assert.ok(
page.includes('{editingItem?.specs?.length ? null : ('),
'有模板规格时编辑弹窗必须隐藏旧规格字段',
)
assert.ok(
page.includes('const hasDynamicSpecs = Boolean(editingItem.specs?.length)'),
'保存前必须判断该分类是否有模板规格',
)
assert.ok(page.includes('...(hasDynamicSpecs'), '有模板规格时不得再提交旧规格字段')
assert.ok(page.includes('请填写「'), '动态必填字段必须在表单层校验')
})
@@ -0,0 +1,112 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import type { ConsignmentSpecField } from '../src/types/consignment.ts'
import {
formatLegacyConsignmentSpec,
resolveConsignmentSpecDisplayItems,
resolveConsignmentSpecText,
} from '../src/utils/consignment-spec-display.ts'
function makeSpec(
overrides: Partial<ConsignmentSpecField> & Pick<ConsignmentSpecField, 'key' | 'label'>,
): ConsignmentSpecField {
return {
type: 'text',
required: false,
options: [],
placeholder: '',
helpText: '',
matchMode: 'exact',
wildcardValues: [],
showInItemName: false,
sortOrder: 0,
...overrides,
}
}
const productSpec = makeSpec({
key: 'product',
label: '商品',
type: 'select',
required: true,
sortOrder: 10,
options: [
{ label: '碎梦浮光珍宝礼盒', value: '15', enabled: true },
{ label: '指挥官密钥', value: '9', enabled: true },
],
})
const systemSpec = makeSpec({
key: 'system',
label: '系统',
type: 'select',
sortOrder: 1,
options: [
{ label: '苹果', value: 'ios', enabled: true },
{ label: '安卓', value: 'android', enabled: true },
],
})
const levelSpec = makeSpec({
key: 'diamondLevel',
label: '钻石等级',
type: 'number',
sortOrder: 2,
})
test('动态规格:选项值映射成选项名称(密钥分类 product=15)', () => {
const items = resolveConsignmentSpecDisplayItems([productSpec], { product: '15' })
assert.deepEqual(items, [{ key: 'product', label: '商品', value: '碎梦浮光珍宝礼盒' }])
})
test('动态规格:按模板 sortOrder 排序,空值不展示', () => {
const items = resolveConsignmentSpecDisplayItems([productSpec, levelSpec, systemSpec], {
product: '15',
system: 'ios',
diamondLevel: '',
})
assert.deepEqual(
items.map((item) => item.key),
['system', 'product'],
)
assert.equal(items[0]?.value, '苹果')
})
test('动态规格:没有模板字段时返回空数组', () => {
assert.deepEqual(resolveConsignmentSpecDisplayItems([], { product: '15' }), [])
assert.deepEqual(resolveConsignmentSpecDisplayItems(undefined, undefined), [])
})
test('规格文本:后端 specText 优先', () => {
assert.equal(
resolveConsignmentSpecText({
specText: '商品 碎梦浮光珍宝礼盒',
specs: [productSpec],
specValues: { product: '9' },
}),
'商品 碎梦浮光珍宝礼盒',
)
})
test('规格文本:无 specText 时按模板现拼', () => {
assert.equal(
resolveConsignmentSpecText({ specs: [productSpec], specValues: { product: '15' } }),
'商品 碎梦浮光珍宝礼盒',
)
})
test('规格文本:没有模板规格时回退旧列,0 璀璨不展示', () => {
assert.equal(
resolveConsignmentSpecText({
system: '苹果',
serverZone: '微信区',
series: '暗潮',
diamondLevel: 3,
brillianceCount: 2,
}),
'苹果 / 微信区 / 暗潮 / 3钻 / 2璀璨',
)
assert.equal(formatLegacyConsignmentSpec({ diamondLevel: 1, brillianceCount: 0 }), '1钻')
assert.equal(resolveConsignmentSpecText({}), '-')
})