254 lines
7.0 KiB
TypeScript
254 lines
7.0 KiB
TypeScript
/**
|
||
* 报价模板预览:Excel 样式网格(上传原文),辅以映射摘要。
|
||
* 主数据来自 /previewSheet;mappingJson 用于已映射/未映射清单。
|
||
*/
|
||
|
||
/** 系统字段 code → 中文标签 */
|
||
export const FIELD_LABELS: Record<string, string> = {
|
||
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<string, any> | null {
|
||
if (!mappingJson) return null;
|
||
if (typeof mappingJson === "object") return mappingJson as Record<string, any>;
|
||
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();
|
||
}
|