front/src/utils/warehouseListMapper.ts
zzh 8155ba93d9 feat: 代发订单分轨与运营页,补 SPA 回退与 H5/下单优化
新增代发订单列表、扫码反查与拣货页,完善绑定/进度展示与 nginx/vite SPA 回退;同步 SKU 申报、价格发布跳过、字典与 H5 代发体验优化。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 10:27:37 +08:00

228 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import dayjs from 'dayjs'
type Row = Record<string, unknown>
export function pickField<T = unknown>(row: Row, ...keys: string[]): T | undefined {
for (const key of keys) {
const value = row[key]
if (value !== undefined && value !== null && value !== '') {
return value as T
}
}
return undefined
}
export function formatDateTime(value: unknown): string {
if (!value) return '-'
const formatted = dayjs(String(value))
return formatted.isValid() ? formatted.format('YYYY-MM-DD HH:mm') : String(value)
}
export function formatDimensions(row: Row): string {
const length = pickField<number | string>(row, 'lengthCm', 'LengthCm')
const width = pickField<number | string>(row, 'widthCm', 'WidthCm')
const height = pickField<number | string>(row, 'heightCm', 'HeightCm')
if (length == null && width == null && height == null) return '-'
return `${length ?? '-'}×${width ?? '-'}×${height ?? '-'}`
}
export function buildWarehouseNameMap(
options: Array<{ label: string; value: number }>,
): Map<number, string> {
return new Map(options.map((item) => [item.value, item.label]))
}
export function resolveWarehouseLabel(
warehouseId: number | undefined,
map: Map<number, string>,
): string {
if (!warehouseId) return '-'
return map.get(warehouseId) || `#${warehouseId}`
}
/** 商品信息:首个品名;多项时后缀 +N(其余条数) */
export function summarizeParcelItems(items: unknown): string {
const list = Array.isArray(items) ? items : []
if (!list.length) return '-'
const first = list[0] as Row
const name = pickField<string>(first, 'productName', 'ProductName') || '-'
if (list.length === 1) return name
return `${name} +${list.length - 1}`
}
/** 属性:只取第一条明细的属性(不加 +N) */
export function summarizeParcelAttributes(items: unknown): string {
const list = Array.isArray(items) ? items : []
if (!list.length) return '-'
const first = list[0] as Row
const text = String(pickField(first, 'attributeText', 'AttributeText') ?? '').trim()
return text || '-'
}
/** 数量:合计件数 */
export function summarizeParcelQuantity(items: unknown): string {
const list = Array.isArray(items) ? items : []
if (!list.length) return '-'
const total = list.reduce((sum, item) => {
const q = Number(pickField(item as Row, 'quantity', 'Quantity') ?? 0)
return sum + (q > 0 ? q : 1)
}, 0)
return String(total)
}
export type ParcelItemLine = {
productName: string
attributeText: string
imageUrl: string
quantity: number
availableQty: number
frozenQty: number
remark: string
}
/** 取首条有图的商品图 URL */
export function pickFirstParcelItemImageUrl(items: unknown): string {
const list = Array.isArray(items) ? items : []
for (const item of list) {
const url = String(
pickField(item as Row, 'imageUrl', 'ImageUrl', 'image', 'Image') ?? '',
).trim()
if (url) return url
}
return ''
}
/** 规范化包裹明细行(列表浮层 / 编辑共用) */
export function normalizeParcelItemLines(items: unknown): ParcelItemLine[] {
const list = Array.isArray(items) ? items : []
return list.map((item) => {
const row = item as Row
const q = Number(pickField(row, 'quantity', 'Quantity') ?? 1)
const qty = q > 0 ? q : 1
const frozen = Math.max(0, Number(pickField(row, 'frozenQty', 'FrozenQty') ?? 0))
const availableRaw = pickField(row, 'availableQty', 'AvailableQty')
const available =
availableRaw != null && availableRaw !== ''
? Number(availableRaw)
: Math.max(0, qty - frozen)
return {
productName: String(pickField(row, 'productName', 'ProductName') ?? '').trim() || '-',
attributeText: String(pickField(row, 'attributeText', 'AttributeText') ?? '').trim() || '-',
imageUrl: String(pickField(row, 'imageUrl', 'ImageUrl', 'image', 'Image') ?? '').trim(),
quantity: qty,
availableQty: Number.isFinite(available) ? available : 0,
frozenQty: frozen,
remark: String(pickField(row, 'remark', 'Remark') ?? '').trim() || '-',
}
})
}
/** 悬停提示:品名 ×数量,最多 maxLines 行 */
export function summarizeParcelItemsTooltip(items: unknown, maxLines = 5): string {
const lines = normalizeParcelItemLines(items)
if (!lines.length) return ''
const shown = lines.slice(0, maxLines).map((l) => `${l.productName} ×${l.quantity}`)
if (lines.length > maxLines) {
shown.push(`共 ${lines.length} 项`)
}
return shown.join('\n')
}
export function formatMoney(amount: unknown, currency?: string | null): string {
if (amount == null || amount === '') return '-'
const code = currency?.trim()
return code ? `${amount} ${code}` : String(amount)
}
export function normalizeListRow(row: Row): Row {
const normalized: Row = { ...row }
Object.entries(row).forEach(([key, value]) => {
if (!key) return
const camel = key.charAt(0).toLowerCase() + key.slice(1)
if (normalized[camel] === undefined) normalized[camel] = value
const pascal = key.charAt(0).toUpperCase() + key.slice(1)
if (normalized[pascal] === undefined) normalized[pascal] = value
})
return normalized
}
/** SKU 英文名称(兼容多种 API 字段名) */
export function pickSkuEnglishName(row: Row): string {
return String(
pickField(row, 'skuNameEn', 'SkuNameEn', 'enName', 'EnName', 'englishName', 'EnglishName') ?? '',
).trim()
}
function looksLikeChinese(text: string) {
return /[\u4e00-\u9fff]/.test(text)
}
/** 仓库 SKU → 我要下单申报行字段 */
export function mapWarehouseSkuToDeclarationFields(raw: Row) {
const row = normalizeListRow(raw)
const skuCode = String(pickField(row, 'skuCode', 'SkuCode') ?? '')
const skuName = String(pickField(row, 'skuName', 'SkuName') ?? '')
const skuNameEn = pickSkuEnglishName(row)
const weightG = Number(pickField(row, 'weightG', 'WeightG') ?? 0)
const unitPrice = pickField(row, 'unitPrice', 'UnitPrice')
const exHsCode = String(pickField(row, 'exHsCode', 'ExHsCode') ?? '')
const imHsCode = String(pickField(row, 'imHsCode', 'ImHsCode') ?? '')
const image = String(pickField(row, 'skuImageUrl', 'SkuImageUrl') ?? '')
const remark = String(pickField(row, 'remark', 'Remark') ?? '')
const formatNum = (value: number) => {
if (Number.isInteger(value)) return String(value)
return value.toFixed(4).replace(/(\.\d*?)0+$/, '$1').replace(/\.$/, '')
}
const unitWeight = weightG > 0 ? formatNum(weightG / 1000) : ''
const enName = skuNameEn || (skuName && !looksLikeChinese(skuName) ? skuName : '')
const priceText = unitPrice != null && unitPrice !== ''
? formatNum(Number(unitPrice))
: ''
return {
sku: skuCode,
cnName: skuName,
enName,
unitPrice: priceText,
unitWeight,
quantity: '1',
exHsCode,
imHsCode,
skuImage: image,
comment: remark,
}
}
/** 选择 SKU 弹窗回传:合并原始行与已解析字段,避免英文名称丢失 */
export function buildWarehouseSkuSelectPayload(raw: Row, mapped?: Row): Row {
const normalized = normalizeListRow(raw)
const merged = mapped ? normalizeListRow(mapped) : {}
return {
...normalized,
...raw,
skuCode: merged.skuCode ?? normalized.skuCode ?? raw.skuCode ?? raw.SkuCode,
skuName: merged.skuName ?? normalized.skuName ?? raw.skuName ?? raw.SkuName,
skuNameEn: pickSkuEnglishName(merged) || pickSkuEnglishName(normalized) || pickSkuEnglishName(raw),
unitPrice: pickField(merged, 'unitPrice', 'UnitPrice')
?? pickField(normalized, 'unitPrice', 'UnitPrice')
?? pickField(raw, 'unitPrice', 'UnitPrice'),
exHsCode: pickField(merged, 'exHsCode', 'ExHsCode')
?? pickField(normalized, 'exHsCode', 'ExHsCode')
?? pickField(raw, 'exHsCode', 'ExHsCode'),
imHsCode: pickField(merged, 'imHsCode', 'ImHsCode')
?? pickField(normalized, 'imHsCode', 'ImHsCode')
?? pickField(raw, 'imHsCode', 'ImHsCode'),
skuImageUrl: pickField(merged, 'skuImageUrl', 'SkuImageUrl')
?? pickField(normalized, 'skuImageUrl', 'SkuImageUrl')
?? pickField(raw, 'skuImageUrl', 'SkuImageUrl'),
weightG: pickField(merged, 'weightG', 'WeightG')
?? pickField(normalized, 'weightG', 'WeightG')
?? pickField(raw, 'weightG', 'WeightG'),
remark: pickField(merged, 'remark', 'Remark')
?? pickField(normalized, 'remark', 'Remark')
?? pickField(raw, 'remark', 'Remark'),
}
}