feat(consignment): 寄卖规格与价格动态配置重构
This commit is contained in:
@@ -1,16 +1,13 @@
|
||||
import type {
|
||||
ConsignmentPricingConfig,
|
||||
ConsignmentSpecField,
|
||||
ConsignmentSpecFieldType,
|
||||
ConsignmentSpecValues,
|
||||
} from '@/types/consignment'
|
||||
|
||||
/**
|
||||
* 寄卖规格选项(单一来源)。
|
||||
*
|
||||
* 两套口径:
|
||||
* 1. 后台全量(CONSIGNMENT_SYSTEM_OPTIONS / CONSIGNMENT_ZONE_OPTIONS):给客服端
|
||||
* 提报审核、库存编辑用 —— 系统 苹果/安卓/通用,区服 微信区/QQ区/互通。
|
||||
* 2. 发布端按物品类型展示(SURNAN_*):打手端「我要寄卖」按物品类型给选项 ——
|
||||
* 苏尔南必须区分 苹果/安卓(不给通用),区服固定「互通」(QQ/微信互通,默认选中)。
|
||||
*
|
||||
* 「互通」在匹配引擎里是区服通配值:互通现货 = 微信/QQ 订单都匹配,反之亦然;
|
||||
* 订单文案里的「通用/全区/不限」会归一化成通配(见后端 ZONE_WILDCARDS)。
|
||||
* 系统侧的「通用」同理是系统通配(订单文案里的「双端/全端」也归一化过来)。
|
||||
* 后续其他品类规则不同(有的不分端),在这里按品类加条目即可。
|
||||
* 旧版客服手工入库表单的选项常量。
|
||||
* 新链路以「每个寄卖配置的 specs」为准,这里只给没有配置快照的旧数据兜底。
|
||||
*/
|
||||
export const CONSIGNMENT_SYSTEM_OPTIONS = [
|
||||
{ label: '苹果', value: '苹果' },
|
||||
@@ -24,13 +21,124 @@ export const CONSIGNMENT_ZONE_OPTIONS = [
|
||||
{ label: '互通(QQ/微信)', value: '互通' },
|
||||
] as const
|
||||
|
||||
/** 打手端发布 · 苏尔南:系统必须二选一,区服固定「互通」 */
|
||||
export const SURNAN_PUBLISH_SYSTEM_OPTIONS = [
|
||||
{ label: '苹果', value: '苹果' },
|
||||
{ label: '安卓', value: '安卓' },
|
||||
] as const
|
||||
export const SPEC_FIELD_TYPE_OPTIONS: Array<{ label: string; value: ConsignmentSpecFieldType }> = [
|
||||
{ label: '文本', value: 'text' },
|
||||
{ label: '长文本', value: 'textarea' },
|
||||
{ label: '下拉', value: 'select' },
|
||||
{ label: '单选', value: 'radio' },
|
||||
{ label: '数字', value: 'number' },
|
||||
{ label: '日期', value: 'date' },
|
||||
]
|
||||
|
||||
export const SURNAN_PUBLISH_ZONE_OPTIONS = [{ label: '互通(QQ/微信)', value: '互通' }] as const
|
||||
export function specFieldTypeLabel(type: ConsignmentSpecFieldType): string {
|
||||
return SPEC_FIELD_TYPE_OPTIONS.find((item) => item.value === type)?.label || '文本'
|
||||
}
|
||||
|
||||
/** 苏尔南发布时区服的默认值(唯一选项,自动选中) */
|
||||
export const SURNAN_DEFAULT_ZONE = '互通'
|
||||
export function isOptionSpecField(field: Pick<ConsignmentSpecField, 'type'>): boolean {
|
||||
return field.type === 'select' || field.type === 'radio'
|
||||
}
|
||||
|
||||
export function formatSpecValue(
|
||||
field: ConsignmentSpecField,
|
||||
value: unknown,
|
||||
options: { forItemName?: boolean } = {},
|
||||
): string {
|
||||
const raw = String(value ?? '').trim()
|
||||
if (!raw) return ''
|
||||
if (options.forItemName && (field.key === 'diamondLevel' || field.key === 'brillianceCount')) {
|
||||
const numeric = Number(raw)
|
||||
if (!Number.isFinite(numeric)) return raw
|
||||
if (field.key === 'brillianceCount' && numeric <= 0) return ''
|
||||
return String(Math.round(numeric))
|
||||
}
|
||||
if (isOptionSpecField(field)) {
|
||||
const option = field.options.find((item) => item.value === raw)
|
||||
if (options.forItemName && field.key === 'serverZone' && field.wildcardValues.includes(raw)) {
|
||||
return ''
|
||||
}
|
||||
return option?.label || raw
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
export function buildSpecText(
|
||||
specs: ConsignmentSpecField[],
|
||||
values: ConsignmentSpecValues | undefined,
|
||||
): string {
|
||||
if (!values) return ''
|
||||
return specs
|
||||
.slice()
|
||||
.sort((a, b) => a.sortOrder - b.sortOrder || a.key.localeCompare(b.key))
|
||||
.map((field) => {
|
||||
const display = formatSpecValue(field, values[field.key])
|
||||
return display ? `${field.label} ${display}` : ''
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(' / ')
|
||||
}
|
||||
|
||||
export function buildSpecItemName(
|
||||
template: string,
|
||||
specs: ConsignmentSpecField[],
|
||||
values: ConsignmentSpecValues | undefined,
|
||||
categoryName = '',
|
||||
): string {
|
||||
if (!values) return categoryName
|
||||
const tokens = new Map<string, string>()
|
||||
for (const field of specs) {
|
||||
tokens.set(field.key, formatSpecValue(field, values[field.key], { forItemName: true }))
|
||||
}
|
||||
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) || '',
|
||||
)
|
||||
} else {
|
||||
result = specs
|
||||
.filter((field) => field.showInItemName)
|
||||
.map((field) => formatSpecValue(field, values[field.key], { forItemName: true }))
|
||||
.filter(Boolean)
|
||||
.join('')
|
||||
}
|
||||
const normalized = result.replace(/\s+/g, ' ').trim()
|
||||
return normalized || categoryName || '寄卖物品'
|
||||
}
|
||||
|
||||
export type SpecPriceResult = {
|
||||
priceFen: number
|
||||
capFen: number
|
||||
templatePriceFen: number
|
||||
breakdown: Array<{ label: string; amountFen: number }>
|
||||
}
|
||||
|
||||
/** 与后端 computeConsignmentPrice 保持同口径,仅用于配置面板预览。 */
|
||||
export function computeSpecPrice(
|
||||
pricing: ConsignmentPricingConfig,
|
||||
templatePriceFen = 0,
|
||||
): SpecPriceResult {
|
||||
const template = Math.max(0, Math.round(Number(templatePriceFen || 0)))
|
||||
const breakdown: Array<{ label: string; amountFen: number }> = []
|
||||
let priceFen = 0
|
||||
|
||||
if (pricing.priceType === 'fixed') {
|
||||
priceFen = Math.max(0, Math.round(Number(pricing.fixedPriceFen || 0)))
|
||||
if (priceFen > 0) breakdown.push({ label: '固定价格', amountFen: priceFen })
|
||||
} else if (template > 0) {
|
||||
const discount = Math.min(10, Math.max(0.1, Number(pricing.discount || 10)))
|
||||
priceFen = Math.round((template * discount) / 10)
|
||||
breakdown.push({ label: '接单模板价', amountFen: template })
|
||||
if (discount !== 10) {
|
||||
breakdown.push({ label: `${discount} 折`, amountFen: priceFen - template })
|
||||
}
|
||||
}
|
||||
|
||||
priceFen = Math.max(0, Math.round(priceFen))
|
||||
return {
|
||||
priceFen,
|
||||
capFen: priceFen,
|
||||
templatePriceFen: template,
|
||||
breakdown,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user