落地报价模板 AI 解析与对象存储,并增加回复超时系统设置页。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
/**
|
||||
* 报价模板预览用:合同样例事实 + 字段中文名。
|
||||
* 与 contracts/quote-template-mapping-v1.json 的 samplePreviewFacts / systemFields 对齐。
|
||||
* 仅用于管理端「用户端预览」弹窗回填一览;金额不由模型发明。
|
||||
* 报价模板预览:Excel 样式网格(上传原文),辅以映射摘要。
|
||||
* 主数据来自 /previewSheet;mappingJson 仅用于未映射必填提示。
|
||||
*/
|
||||
|
||||
/** 系统字段 code → 中文标签 */
|
||||
@@ -30,178 +29,82 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
total_amount: "报价合计",
|
||||
};
|
||||
|
||||
/** 弹窗优先展示的字段顺序(接近原型) */
|
||||
export const PREVIEW_FIELD_ORDER = [
|
||||
"customer_name",
|
||||
"container_type_qty",
|
||||
"pol",
|
||||
"pod",
|
||||
"origin",
|
||||
"destination",
|
||||
"sales_name",
|
||||
"commodity",
|
||||
"fcl_or_lcl",
|
||||
"trade_terms",
|
||||
"contact_name",
|
||||
"contact_phone",
|
||||
"quantity",
|
||||
"pieces",
|
||||
"weight_kg",
|
||||
"volume_cbm",
|
||||
"cargo_ready_date",
|
||||
"currency",
|
||||
] as const;
|
||||
|
||||
/** 与主账 previewSample 同源的合同样例事实 */
|
||||
export const SAMPLE_PREVIEW_FACTS: Record<string, any> = {
|
||||
customer_name: "ABC Trading Co.",
|
||||
contact_name: "张三",
|
||||
contact_phone: "13800000000",
|
||||
quote_no: "QT-202607020001",
|
||||
quote_date: "2026-07-02",
|
||||
valid_until: "2026-07-05",
|
||||
currency: "USD",
|
||||
sales_name: "张敏",
|
||||
pol: "CNSHA",
|
||||
pod: "USLAX",
|
||||
origin: "深圳",
|
||||
destination: "洛杉矶",
|
||||
commodity: "通用货物",
|
||||
quantity: "2",
|
||||
container_type_qty: "40HQ × 2",
|
||||
fcl_or_lcl: "FCL",
|
||||
trade_terms: "FOB",
|
||||
cargo_ready_date: "2026-07-10",
|
||||
pieces: "10",
|
||||
weight_kg: "1200",
|
||||
volume_cbm: "8.5",
|
||||
total_amount: "3680.00",
|
||||
fee_rows: [
|
||||
{ name: "O/F 海运费", amount: "2900", remark: "" },
|
||||
{ name: "THC + DOC + SEAL", amount: "780", remark: "" },
|
||||
],
|
||||
export type SheetPreviewCell = {
|
||||
addr: string;
|
||||
text: string;
|
||||
yellow: boolean;
|
||||
mappedCode?: string;
|
||||
mappedLabel?: string;
|
||||
};
|
||||
|
||||
export type PreviewFeeRow = { name: string; amount: string; remark?: string };
|
||||
|
||||
export type PreviewFieldRow = {
|
||||
code: string;
|
||||
label: string;
|
||||
value: string;
|
||||
cell: string;
|
||||
export type SheetPreviewRow = {
|
||||
rowNo: number;
|
||||
cells: SheetPreviewCell[];
|
||||
};
|
||||
|
||||
export type PreviewViewModel = {
|
||||
loading: boolean;
|
||||
error: string;
|
||||
quoteNo: string;
|
||||
quoteDate: string;
|
||||
currency: string;
|
||||
totalAmount: string;
|
||||
validUntil: string;
|
||||
/** 航线摘要:有 pol/pod 或 origin/destination 时拼出 */
|
||||
routeText: string;
|
||||
fields: PreviewFieldRow[];
|
||||
fees: PreviewFeeRow[];
|
||||
sheet: string;
|
||||
fileName: string;
|
||||
colHeaders: string[];
|
||||
rows: SheetPreviewRow[];
|
||||
mappedCount: number;
|
||||
yellowCount: number;
|
||||
unmappedLabels: string[];
|
||||
unmappedRequired: string[];
|
||||
};
|
||||
|
||||
function parseMapping(raw: unknown): { fields: Record<string, string>; unmapped: string[] } {
|
||||
if (!raw) return { fields: {}, unmapped: [] };
|
||||
let obj: any = raw;
|
||||
if (typeof raw === "string") {
|
||||
function parseUnmapped(mappingJson: unknown): string[] {
|
||||
if (!mappingJson) return [];
|
||||
let obj: any = mappingJson;
|
||||
if (typeof mappingJson === "string") {
|
||||
try {
|
||||
obj = JSON.parse(raw);
|
||||
obj = JSON.parse(mappingJson);
|
||||
} catch {
|
||||
return { fields: {}, unmapped: [] };
|
||||
return [];
|
||||
}
|
||||
}
|
||||
const fields: Record<string, string> = {};
|
||||
const src = obj?.fields || {};
|
||||
if (src && typeof src === "object") {
|
||||
for (const [k, v] of Object.entries(src)) {
|
||||
if (k === "fee_rows") continue;
|
||||
const cell = String(v ?? "").trim();
|
||||
if (cell) fields[k] = cell;
|
||||
}
|
||||
}
|
||||
const unmapped = Array.isArray(obj?.unmappedRequired)
|
||||
? obj.unmappedRequired.map((x: any) => String(x))
|
||||
: [];
|
||||
return { fields, unmapped };
|
||||
return Array.isArray(obj?.unmappedRequired) ? obj.unmappedRequired.map((x: any) => String(x)) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 mappingJson 与样例事实组装弹窗视图。
|
||||
* 只展示「已映射」字段的样例值;费用行在有 fee_rows 映射或样例费用时展示。
|
||||
*/
|
||||
export function buildPreviewViewModel(mappingJson: unknown, pointCountHint = 0): PreviewViewModel {
|
||||
const { fields: mapped, unmapped } = parseMapping(mappingJson);
|
||||
const facts = SAMPLE_PREVIEW_FACTS;
|
||||
const fieldRows: PreviewFieldRow[] = [];
|
||||
|
||||
const ordered = [
|
||||
...PREVIEW_FIELD_ORDER.filter((c) => mapped[c]),
|
||||
...Object.keys(mapped).filter((c) => !(PREVIEW_FIELD_ORDER as readonly string[]).includes(c)),
|
||||
];
|
||||
|
||||
// 航线合并展示,避免 grid 里重复 pol/pod
|
||||
const hasRoute = Boolean(mapped.pol || mapped.pod || mapped.origin || mapped.destination);
|
||||
let routeText = "";
|
||||
if (hasRoute) {
|
||||
if (mapped.pol || mapped.pod) {
|
||||
routeText = `${facts.pol || "—"} → ${facts.pod || "—"}`;
|
||||
} else {
|
||||
routeText = `${facts.origin || "—"} → ${facts.destination || "—"}`;
|
||||
}
|
||||
}
|
||||
|
||||
for (const code of ordered) {
|
||||
if (code === "pol" || code === "pod" || code === "origin" || code === "destination") {
|
||||
continue;
|
||||
}
|
||||
const rawVal = facts[code];
|
||||
if (rawVal == null || rawVal === "") continue;
|
||||
fieldRows.push({
|
||||
code,
|
||||
label: FIELD_LABELS[code] || code,
|
||||
value: String(rawVal),
|
||||
cell: mapped[code],
|
||||
});
|
||||
}
|
||||
if (routeText) {
|
||||
fieldRows.splice(Math.min(2, fieldRows.length), 0, {
|
||||
code: "route",
|
||||
label: "航线",
|
||||
value: routeText,
|
||||
cell: [mapped.pol, mapped.pod, mapped.origin, mapped.destination].filter(Boolean).join(" / "),
|
||||
});
|
||||
}
|
||||
|
||||
const fees: PreviewFeeRow[] = Array.isArray(facts.fee_rows)
|
||||
? facts.fee_rows.map((r: any) => ({
|
||||
name: String(r.name ?? ""),
|
||||
amount: String(r.amount ?? ""),
|
||||
remark: String(r.remark ?? ""),
|
||||
}))
|
||||
/** 把主账 previewSheet 结果转成弹窗视图 */
|
||||
export function buildSheetPreviewViewModel(sheetRes: any, mappingJson?: unknown): PreviewViewModel {
|
||||
const colHeaders = Array.isArray(sheetRes?.colHeaders)
|
||||
? sheetRes.colHeaders.map((x: any) => String(x))
|
||||
: [];
|
||||
|
||||
const currency = String(facts.currency || "USD");
|
||||
const total = String(facts.total_amount || "");
|
||||
|
||||
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: "",
|
||||
quoteNo: String(facts.quote_no || ""),
|
||||
quoteDate: String(facts.quote_date || ""),
|
||||
currency,
|
||||
totalAmount: total,
|
||||
validUntil: String(facts.valid_until || ""),
|
||||
routeText,
|
||||
fields: fieldRows,
|
||||
fees,
|
||||
mappedCount: Object.keys(mapped).length || pointCountHint,
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -210,15 +113,18 @@ export function emptyPreviewViewModel(): PreviewViewModel {
|
||||
return {
|
||||
loading: false,
|
||||
error: "",
|
||||
quoteNo: "",
|
||||
quoteDate: "",
|
||||
currency: "USD",
|
||||
totalAmount: "",
|
||||
validUntil: "",
|
||||
routeText: "",
|
||||
fields: [],
|
||||
fees: [],
|
||||
sheet: "",
|
||||
fileName: "",
|
||||
colHeaders: [],
|
||||
rows: [],
|
||||
mappedCount: 0,
|
||||
yellowCount: 0,
|
||||
unmappedLabels: [],
|
||||
unmappedRequired: [],
|
||||
};
|
||||
}
|
||||
|
||||
/** @deprecated 兼容旧名:请用 buildSheetPreviewViewModel */
|
||||
export function buildPreviewViewModel(_mappingJson: unknown, _pointCountHint = 0): PreviewViewModel {
|
||||
return emptyPreviewViewModel();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user