机器人显示名与站点标题可配;多段联运列表投影;协同群监听对账;工单附件落存;模板与工单后台补强
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -27,6 +27,7 @@ const LAND_LABEL: Record<string, string> = {
|
||||
/**
|
||||
* JSON 字符串 / 对象都解开成扁平键值。坏 JSON 当空。
|
||||
* 协同快照里的车型等也要能展示,调用方自己决定合并顺序。
|
||||
* segments_json 若已是数组/对象,序列化成字符串留给 parseSegments,禁止直接丢掉。
|
||||
*/
|
||||
export function parseFactsMap(raw: unknown): Record<string, string> {
|
||||
if (raw == null) return {};
|
||||
@@ -43,13 +44,97 @@ export function parseFactsMap(raw: unknown): Record<string, string> {
|
||||
for (const key of Object.keys(obj)) {
|
||||
const val = obj[key];
|
||||
if (val == null) continue;
|
||||
if (typeof val === "object") continue;
|
||||
if (typeof val === "object") {
|
||||
// 多段 segments_json 常被反序列成数组;保留才能在详情里画出段1/段2。
|
||||
if (
|
||||
(key === "segments_json" || key === "segmentsJson") &&
|
||||
(Array.isArray(val) || typeof val === "object")
|
||||
) {
|
||||
try {
|
||||
out[key] = JSON.stringify(val);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const text = String(val).trim();
|
||||
if (text) out[key] = text;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 列表子工单行:一段的运输方式码 + 展示文案。 */
|
||||
export type MultiSegmentView = {
|
||||
index: number;
|
||||
/** AIR / SEA / LAND */
|
||||
mode: string;
|
||||
modeWord: string;
|
||||
/** 子工单左侧详情:海运 · 上海 → 汉堡 · … */
|
||||
detailLine: string;
|
||||
/** 旧列表兼容:段1陆运 北京 → 上海 茶叶 */
|
||||
summaryLine: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 多段各段一行摘要(段1陆运 北京 → 上海)。无段返回空数组。
|
||||
*/
|
||||
export function multiSegmentSummaryLines(ticket: any): string[] {
|
||||
return multiSegmentViews(ticket).map((seg) => seg.summaryLine);
|
||||
}
|
||||
|
||||
/**
|
||||
* 多段子工单视图:列表父行下灰底子行用。
|
||||
* 详情串尽量贴近原型「方式 · 路线 · 货量条款」;缺字段就跳过,不编造。
|
||||
*/
|
||||
export function multiSegmentViews(ticket: any): MultiSegmentView[] {
|
||||
const facts = mergeInquiryFacts(ticket);
|
||||
const segs = parseSegments(facts);
|
||||
return segs.map((seg) => {
|
||||
const sf = seg.facts;
|
||||
const o = pick(sf, "起运港", "起运地", "始发站");
|
||||
const d = pick(sf, "目的港", "目的地", "目的站");
|
||||
const g = pick(sf, "品名", "货物品名");
|
||||
const route = o || d ? `${o || "-"} → ${d || "-"}` : "";
|
||||
const bits: string[] = [seg.modeWord];
|
||||
if (route) bits.push(route);
|
||||
const extras = [
|
||||
pick(sf, "运输条款", "贸易条款"),
|
||||
pick(sf, "整柜或拼柜"),
|
||||
pick(sf, "箱型箱量"),
|
||||
pick(sf, "包装方式", "包装类型"),
|
||||
pick(sf, "运输类型"),
|
||||
pick(sf, "客户"),
|
||||
volumeWeightText(sf),
|
||||
pick(sf, "车型/数量", "车型"),
|
||||
readyDateText(sf),
|
||||
g ? `品名 ${g}` : "",
|
||||
].filter(Boolean);
|
||||
bits.push(...extras);
|
||||
return {
|
||||
index: seg.index,
|
||||
mode: seg.mode,
|
||||
modeWord: seg.modeWord,
|
||||
detailLine: bits.join(" · "),
|
||||
summaryLine: [`段${seg.index}${seg.modeWord}`, route, g].filter(Boolean).join(" "),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function volumeWeightText(sf: Record<string, string>): string {
|
||||
const vol = pick(sf, "体积");
|
||||
const w = pick(sf, "毛重", "重量");
|
||||
if (vol && w) return `${vol} / ${w}`;
|
||||
if (vol) return vol;
|
||||
if (w) return w;
|
||||
return "";
|
||||
}
|
||||
|
||||
function readyDateText(sf: Record<string, string>): string {
|
||||
const d = pick(sf, "货好时间", "cargo_ready_date");
|
||||
return d ? `货好 ${d}` : "";
|
||||
}
|
||||
|
||||
/** 询价事实优先,协同只补空键(中港整车车型常在协同里)。 */
|
||||
export function mergeInquiryFacts(ticket: any): Record<string, string> {
|
||||
const inquiry = parseFactsMap(ticket?.factsJson ?? ticket?.facts);
|
||||
@@ -69,6 +154,40 @@ function pick(facts: Record<string, string>, ...keys: string[]): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
const MODE_WORD: Record<string, string> = { AIR: "空运", SEA: "海运", LAND: "陆运" };
|
||||
|
||||
/** 从 facts.segments_json 解出各段;坏 JSON 当没有段。 */
|
||||
function parseSegments(facts: Record<string, string>): Array<{
|
||||
index: number;
|
||||
mode: string;
|
||||
modeWord: string;
|
||||
facts: Record<string, string>;
|
||||
}> {
|
||||
const raw = (facts.segments_json || facts.segmentsJson || "").trim();
|
||||
if (!raw) return [];
|
||||
try {
|
||||
const arr = JSON.parse(raw);
|
||||
if (!Array.isArray(arr)) return [];
|
||||
return arr
|
||||
.filter((x) => x && typeof x === "object")
|
||||
.map((seg: any, i: number) => {
|
||||
const mode = String(seg.mode || "").toUpperCase();
|
||||
const nested =
|
||||
seg.facts && typeof seg.facts === "object" && !Array.isArray(seg.facts)
|
||||
? parseFactsMap(seg.facts)
|
||||
: {};
|
||||
return {
|
||||
index: Number(seg.index) > 0 ? Number(seg.index) : i + 1,
|
||||
mode,
|
||||
modeWord: MODE_WORD[mode] || mode || "段",
|
||||
facts: nested,
|
||||
};
|
||||
});
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function dash(v: unknown): string {
|
||||
const s = (v ?? "").toString().trim();
|
||||
return s ? s : "-";
|
||||
@@ -183,8 +302,34 @@ export function buildDemandFields(ticket: any): DemandField[] {
|
||||
push(fields, "增值服务", added);
|
||||
}
|
||||
push(fields, "报价日期", quoteDate);
|
||||
} else if (line === "MULTI") {
|
||||
// 多段:按 segments_json 逐段展示,并带整票工单号。
|
||||
push(fields, "工单号", columnText(ticket?.workOrderNo) || columnText(ticket?.work_order_no));
|
||||
const segs = parseSegments(facts);
|
||||
if (segs.length) {
|
||||
for (const seg of segs) {
|
||||
const sf = seg.facts;
|
||||
const o = pick(sf, "起运港", "起运地", "始发站");
|
||||
const d = pick(sf, "目的港", "目的地", "目的站");
|
||||
const g = pick(sf, "品名", "货物品名");
|
||||
push(
|
||||
fields,
|
||||
`段${seg.index}${seg.modeWord}`,
|
||||
[o || d ? `${o || "-"} → ${d || "-"}` : "", g, pick(sf, "件数"), pick(sf, "毛重", "重量"), pick(sf, "体积")]
|
||||
.filter(Boolean)
|
||||
.join(" · "),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
push(fields, "起运 / 目的", origin || dest ? `${origin || "-"} → ${dest || "-"}` : "");
|
||||
push(fields, "品名", goods);
|
||||
}
|
||||
push(fields, "件数", pick(facts, "件数") || columnText(ticket?.pieces));
|
||||
push(fields, "重量(KG)", pick(facts, "毛重", "重量") || columnText(ticket?.weightKg));
|
||||
push(fields, "体积(CBM)", pick(facts, "体积") || columnText(ticket?.volumeCbm));
|
||||
push(fields, "报价日期", quoteDate);
|
||||
} else {
|
||||
// 多段或未识别:至少把路线和货量列出来,避免整块空白。
|
||||
// 未识别:至少把路线和货量列出来,避免整块空白。
|
||||
push(fields, "起运 / 目的", origin || dest ? `${origin || "-"} → ${dest || "-"}` : "");
|
||||
push(fields, "品名", goods);
|
||||
push(fields, "件数", pick(facts, "件数") || columnText(ticket?.pieces));
|
||||
|
||||
Reference in New Issue
Block a user