feat(consignment): 后台寄卖规格按分类模板动态渲染
This commit is contained in:
@@ -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({}), '-')
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user