import dayjs from 'dayjs' type Row = Record export function pickField(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(row, 'lengthCm', 'LengthCm') const width = pickField(row, 'widthCm', 'WidthCm') const height = pickField(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 { return new Map(options.map((item) => [item.value, item.label])) } export function resolveWarehouseLabel( warehouseId: number | undefined, map: Map, ): 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(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'), } }