落地报价模板主账与后台:合同映射、灌种 SQL/脚本、智能体填表,并补测服运维探针与合同文档对齐。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-09-14 14:09:45 +08:00
co-authored by Cursor
parent 8bdf4990a2
commit a70bce3c8f
138 changed files with 7239 additions and 1085 deletions
@@ -0,0 +1,224 @@
/**
* 报价模板预览用:合同样例事实 + 字段中文名。
* 与 contracts/quote-template-mapping-v1.json 的 samplePreviewFacts / systemFields 对齐。
* 仅用于管理端「用户端预览」弹窗回填一览;金额不由模型发明。
*/
/** 系统字段 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: "报价合计",
};
/** 弹窗优先展示的字段顺序(接近原型) */
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 PreviewFeeRow = { name: string; amount: string; remark?: string };
export type PreviewFieldRow = {
code: string;
label: string;
value: string;
cell: string;
};
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[];
mappedCount: number;
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") {
try {
obj = JSON.parse(raw);
} catch {
return { fields: {}, unmapped: [] };
}
}
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 };
}
/**
* 根据 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 ?? ""),
}))
: [];
const currency = String(facts.currency || "USD");
const total = String(facts.total_amount || "");
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,
unmappedRequired: unmapped,
};
}
export function emptyPreviewViewModel(): PreviewViewModel {
return {
loading: false,
error: "",
quoteNo: "",
quoteDate: "",
currency: "USD",
totalAmount: "",
validUntil: "",
routeText: "",
fields: [],
fees: [],
mappedCount: 0,
unmappedRequired: [],
};
}