新增代发订单列表、扫码反查与拣货页,完善绑定/进度展示与 nginx/vite SPA 回退;同步 SKU 申报、价格发布跳过、字典与 H5 代发体验优化。 Co-authored-by: Cursor <cursoragent@cursor.com>
228 lines
8.2 KiB
TypeScript
228 lines
8.2 KiB
TypeScript
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'),
|
||
}
|
||
}
|