落地报价模板主账与后台:合同映射、灌种 SQL/脚本、智能体填表,并补测服运维探针与合同文档对齐。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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: [],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user