/** * 报价模板预览:Excel 样式网格(上传原文),辅以映射摘要。 * 主数据来自 /previewSheet;mappingJson 用于已映射/未映射清单。 */ /** 系统字段 code → 中文标签 */ export const FIELD_LABELS: Record = { customer_name: "客户名称", contact_name: "联系人", contact_phone: "电话", quote_no: "报价单编号", quote_date: "报价日期", valid_until: "有效期至", currency: "币别", sales_name: "业务员", pol: "起运港", pod: "目的港", origin: "始发地", destination: "目的地", commodity: "品名", quantity: "货物数量", container_type_qty: "箱型箱量", fcl_or_lcl: "整柜或拼柜", trade_terms: "贸易条款", cargo_ready_date: "货好时间", pieces: "件数", weight_kg: "重量", volume_cbm: "体积", total_amount: "报价合计", chargeable_weight_kg: "计费重", delivery_address: "送货地址", pickup_address: "提货地址", transit_time: "运输时效", cargo_value: "货值", hs_code: "HS编码", oil_elec_mag: "油电磁", has_battery: "是否带电", on_board_date: "开船日", special_remarks: "特殊备注", eta_request: "要求到港", sailing_schedule: "船期", shipping_line: "船公司", flight_no: "航班号", transfer_airport: "中转机场", effective_date: "生效日期", fuel_surcharge_rate: "燃油附加费率", tax_rate: "税率", has_oil: "是否含油", has_magnet: "是否含磁", vehicle_qty: "车型/数量", }; /** 解析结果摘要:供预览弹窗展示「已解析 / 未解析」 */ export type MappingFieldItem = { code: string; label: string; cell: string; }; export type MappingRegionItem = { kind: "fee" | "route"; id: string; title: string; }; export type MappingParseSummary = { fields: MappingFieldItem[]; regions: MappingRegionItem[]; gaps: string[]; unmappedRequired: string[]; unmappedLabels: string[]; pointCount: number; }; function asMappingObject(mappingJson: unknown): Record | null { if (!mappingJson) return null; if (typeof mappingJson === "object") return mappingJson as Record; if (typeof mappingJson === "string") { try { const obj = JSON.parse(mappingJson); return obj && typeof obj === "object" ? obj : null; } catch { return null; } } return null; } /** * 从 mappingJson 拆出运营可读的已映射 / 未映射清单。 * 不打接口;mappingGaps 为空时(旧版解析)仍可看 fields 与 unmapped*。 */ export function summarizeMappingParse(mappingJson: unknown): MappingParseSummary { const obj = asMappingObject(mappingJson); const empty: MappingParseSummary = { fields: [], regions: [], gaps: [], unmappedRequired: [], unmappedLabels: [], pointCount: 0, }; if (!obj) return empty; const fieldsObj = obj.fields && typeof obj.fields === "object" ? obj.fields : {}; const fields: MappingFieldItem[] = Object.keys(fieldsObj) .filter((k) => fieldsObj[k]) .map((code) => ({ code, label: FIELD_LABELS[code] || code, cell: String(fieldsObj[code] || ""), })) .sort((a, b) => a.label.localeCompare(b.label, "zh")); const regions: MappingRegionItem[] = []; const feeRegions = Array.isArray(obj.feeRegions) ? obj.feeRegions : []; for (const r of feeRegions) { if (!r || typeof r !== "object") continue; regions.push({ kind: "fee", id: String(r.id || ""), title: String(r.title || r.id || "费用区"), }); } const routeRegions = Array.isArray(obj.routeRegions) ? obj.routeRegions : []; for (const r of routeRegions) { if (!r || typeof r !== "object") continue; regions.push({ kind: "route", id: String(r.id || ""), title: String(r.title || r.id || "线路区"), }); } const gaps = Array.isArray(obj.mappingGaps) ? obj.mappingGaps.map((x: any) => String(x)).filter(Boolean) : []; const unmappedRequired = Array.isArray(obj.unmappedRequired) ? obj.unmappedRequired.map((x: any) => String(x)).filter(Boolean) : []; const unmappedLabels = Array.isArray(obj.unmappedLabels) ? obj.unmappedLabels.map((x: any) => String(x)).filter(Boolean) : []; return { fields, regions, gaps, unmappedRequired, unmappedLabels, pointCount: fields.length + regions.length, }; } export type SheetPreviewCell = { addr: string; text: string; yellow: boolean; mappedCode?: string; mappedLabel?: string; }; export type SheetPreviewRow = { rowNo: number; cells: SheetPreviewCell[]; }; export type PreviewViewModel = { loading: boolean; error: string; sheet: string; fileName: string; colHeaders: string[]; rows: SheetPreviewRow[]; mappedCount: number; yellowCount: number; unmappedLabels: string[]; unmappedRequired: string[]; }; function parseUnmapped(mappingJson: unknown): string[] { if (!mappingJson) return []; let obj: any = mappingJson; if (typeof mappingJson === "string") { try { obj = JSON.parse(mappingJson); } catch { return []; } } return Array.isArray(obj?.unmappedRequired) ? obj.unmappedRequired.map((x: any) => String(x)) : []; } /** 把主账 previewSheet 结果转成弹窗视图 */ export function buildSheetPreviewViewModel(sheetRes: any, mappingJson?: unknown): PreviewViewModel { const colHeaders = Array.isArray(sheetRes?.colHeaders) ? sheetRes.colHeaders.map((x: any) => String(x)) : []; const rowsRaw = Array.isArray(sheetRes?.rows) ? sheetRes.rows : []; const rows: SheetPreviewRow[] = rowsRaw.map((r: any) => ({ rowNo: Number(r.rowNo || 0), cells: Array.isArray(r.cells) ? r.cells.map((c: any) => ({ addr: String(c.addr || ""), text: String(c.text || ""), yellow: Boolean(c.yellow), mappedCode: c.mappedCode ? String(c.mappedCode) : undefined, mappedLabel: c.mappedLabel ? String(c.mappedLabel) : undefined, })) : [], })); let yellowCount = 0; let mappedHit = 0; for (const row of rows) { for (const c of row.cells) { if (c.yellow) yellowCount += 1; if (c.mappedCode) mappedHit += 1; } } const unmapped = parseUnmapped(mappingJson); return { loading: false, error: String(sheetRes?.error || ""), sheet: String(sheetRes?.sheet || ""), fileName: String(sheetRes?.fileName || ""), colHeaders, rows, mappedCount: Number(sheetRes?.mappedCount || mappedHit || 0), yellowCount, unmappedLabels: unmapped.map((c) => FIELD_LABELS[c] || c), unmappedRequired: unmapped, }; } export function emptyPreviewViewModel(): PreviewViewModel { return { loading: false, error: "", sheet: "", fileName: "", colHeaders: [], rows: [], mappedCount: 0, yellowCount: 0, unmappedLabels: [], unmappedRequired: [], }; } /** @deprecated 兼容旧名:请用 buildSheetPreviewViewModel */ export function buildPreviewViewModel(_mappingJson: unknown, _pointCountHint = 0): PreviewViewModel { return emptyPreviewViewModel(); }