front/src/utils/warehouseListMapper.ts
zzh 68b85dadf9
All checks were successful
Build and Deploy Front / deploy (push) Successful in 10m0s
feat: 仓储表单体验、轨迹 SPA 代理修复与打印预览增强
修复 /track/pull|push 被代理误伤 404;Warning/ProviderService/Dictionary 路径改写;SkuImage 入库归一化。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-12 12:30:35 +08:00

256 lines
9.1 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)
}
/** 申报图入库:去掉签名参数,避免 SkuImage VARCHAR 超长 */
export function normalizeSkuImageForStorage(value?: string | null): string {
const raw = String(value ?? '').trim()
if (!raw) return ''
if (raw.startsWith('data:') || raw.startsWith('blob:')) return raw
try {
if (/^https?:\/\//i.test(raw)) {
const url = new URL(raw)
// 托管资源只保留路径 key;外链去掉 query/hash 再存
const path = decodeURIComponent(url.pathname || '').replace(/^\/+/, '')
if (path && /(?:uploads\/|sku\/|parcel\/)/i.test(path)) {
return path
}
return `${url.origin}${url.pathname}`
}
} catch {
// ignore
}
const q = raw.indexOf('?')
const h = raw.indexOf('#')
let end = raw.length
if (q >= 0) end = Math.min(end, q)
if (h >= 0) end = Math.min(end, h)
return raw.slice(0, end)
}
/** 仓库 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 = normalizeSkuImageForStorage(
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'),
}
}