const endpoints = { orders: "/api/orders", risks: "/api/risks", flow: "/api/flow", nodeTimes: "/api/node-times", cutoffs: "/api/order-cutoffs", exceptionMap: "/api/exception-map", }; const labels = { orderNo: "订单号", customerName: "客户", status: "状态", currentNodeName: "当前节点", ownerName: "责任人", riskName: "风险", exceptionName: "异常", affectedNodeName: "影响节点", sequence: "顺序", nodeName: "节点", nextNodeName: "下一节点", deadlineName: "关键时限", plannedTime: "计划时间", cutoffTime: "截止时间", actualTime: "实际完成", isCompleted: "完成", isDelayed: "超时", customsCutoffTime: "截关", terminalCutoffTime: "截港", vgmCutoffTime: "VGM截止", estimatedSailingTime: "预计开船", }; let currentRunId = null; let operationsLoaded = false; let marketOptions = null; let mockCatalog = null; let activeCatalog = "routes"; document.querySelectorAll(".view-tab").forEach(button => { button.addEventListener("click", () => switchView(button.dataset.view)); }); document.getElementById("inquiryForm").addEventListener("submit", submitInquiry); document.getElementById("recommendBtn").addEventListener("click", submitInquiry); document.getElementById("refreshBtn").addEventListener("click", loadOperations); document.getElementById("auditBtn").addEventListener("click", openAudit); document.getElementById("generateMockBtn").addEventListener("click", generateMockBatch); document.getElementById("resetMockBtn").addEventListener("click", resetMockData); document.getElementById("origin").addEventListener("change", () => syncRouteControls(true)); document.getElementById("destination").addEventListener("change", () => applyRouteDefaults(true)); document.querySelectorAll(".catalog-tab").forEach(button => { button.addEventListener("click", () => selectCatalog(button.dataset.catalog)); }); document.getElementById("closeAuditBtn").addEventListener("click", () => { document.getElementById("auditDialog").close(); }); initialize(); async function initialize() { try { const [health, options] = await Promise.all([ fetchJson("/health"), fetchJson("/api/market/form-options"), ]); document.getElementById("systemStatus").textContent = `本地知识图谱 · ${numberFormat(health.triple_count)} triples`; marketOptions = options; populateInquiryForm(options); await requestRecommendation(options.defaults); } catch (error) { renderMarketError(error.message); } } function switchView(view) { document.querySelectorAll(".view-tab").forEach(button => { button.classList.toggle("is-active", button.dataset.view === view); }); document.querySelectorAll(".app-view").forEach(section => { section.classList.toggle("is-active", section.id === `${view}View`); }); if (view === "operations" && !operationsLoaded) loadOperations(); if (view === "mock" && !mockCatalog) loadMockCatalog(); } function populateInquiryForm(options) { populateSelect("customer", options.customers, "id", "name", options.defaults.customer); populateSelect("origin", options.origins, null, null, options.defaults.origin); syncRouteControls(false, options.defaults.destination); const defaultRoute = options.routes.find(route => route.origin === options.defaults.origin && route.destination === options.defaults.destination ); if (defaultRoute) { populateSelect( "routeType", defaultRoute.route_types, null, null, options.defaults.preferred_route_type, ); } document.getElementById("cargoName").value = options.defaults.cargo_name; document.getElementById("cargoVolume").value = options.defaults.cargo_volume; document.getElementById("evaluationDate").value = options.defaults.evaluation_date; document.getElementById("departureDate").value = options.defaults.requested_departure_date; document.getElementById("maxTransitDays").value = options.defaults.max_transit_days; document.getElementById("targetAmount").value = options.defaults.target_amount; document.getElementById("dangerousGoods").checked = options.defaults.dangerous_goods; populateMockRouteSelect(); } function syncRouteControls(updateDefaults, selectedDestination) { if (!marketOptions) return; const origin = document.getElementById("origin").value; const destinations = marketOptions.routes .filter(route => route.origin === origin) .map(route => route.destination); const preferredDestination = selectedDestination && destinations.includes(selectedDestination) ? selectedDestination : destinations[0]; populateSelect("destination", destinations, null, null, preferredDestination); applyRouteDefaults(updateDefaults); } function applyRouteDefaults(updateValues) { if (!marketOptions) return; const origin = document.getElementById("origin").value; const destination = document.getElementById("destination").value; const route = marketOptions.routes.find(item => item.origin === origin && item.destination === destination); if (!route) return; const currentRouteType = document.getElementById("routeType").value; const preferredRouteType = route.route_types.includes(currentRouteType) ? currentRouteType : route.route_types[0]; populateSelect("routeType", route.route_types, null, null, preferredRouteType); if (updateValues) { document.getElementById("targetAmount").value = route.target_amount; document.getElementById("departureDate").value = route.requested_departure_date; document.getElementById("maxTransitDays").value = Math.ceil(route.max_transit_days); } } function populateMockRouteSelect() { if (!marketOptions) return; const allRoutes = marketOptions.routes.map((route, index) => ({ id: String(index), name: `${route.origin} → ${route.destination}`, })); const indiaRoutes = allRoutes.filter(route => route.name.includes("印度")); const routes = indiaRoutes.length ? indiaRoutes : allRoutes; populateSelect("mockRoute", routes, "id", "name", routes[0]?.id || ""); } function populateSelect(id, values, valueKey, labelKey, selectedValue) { const select = document.getElementById(id); select.innerHTML = values.map(item => { const value = valueKey ? item[valueKey] : item; const label = labelKey ? item[labelKey] : item; return ``; }).join(""); } async function submitInquiry(event) { event.preventDefault(); const form = new FormData(document.getElementById("inquiryForm")); const inquiry = { customer: form.get("customer"), origin: form.get("origin"), destination: form.get("destination"), cargo_name: form.get("cargo_name"), cargo_volume: Number(form.get("cargo_volume")), evaluation_date: form.get("evaluation_date"), requested_departure_date: form.get("requested_departure_date"), max_transit_days: Number(form.get("max_transit_days")), target_amount: Number(form.get("target_amount")), preferred_route_type: form.get("preferred_route_type"), dangerous_goods: document.getElementById("dangerousGoods").checked, salesperson_adjustment: 0, }; await requestRecommendation(inquiry); } async function requestRecommendation(inquiry) { setRecommendationLoading(true); try { const result = await fetchJson("/api/market/recommend", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(inquiry), }); currentRunId = result.run_id; renderMarketResult(result); } catch (error) { currentRunId = null; renderMarketError(error.message); } finally { setRecommendationLoading(false); } } function setRecommendationLoading(loading) { const button = document.getElementById("recommendBtn"); button.disabled = loading; button.querySelector("span:first-child").textContent = loading ? "正在计算" : "生成市场方案"; document.getElementById("inquiryState").textContent = loading ? "决策计算中" : "已标准化"; if (loading) { document.getElementById("recommendationBand").className = "recommendation-band is-loading"; document.getElementById("recommendationBand").innerHTML = `
`; } } function renderMarketResult(result) { const inquiry = result.normalized_inquiry; const recommendation = result.recommendation; document.getElementById("decisionTitle").textContent = `${inquiry.customer_name} · ${inquiry.route}`; document.getElementById("inquiryState").textContent = `任务 ${result.run_id}`; document.getElementById("auditBtn").disabled = false; renderPipeline(result.pipeline); renderRecommendation(result); renderCandidates(result.candidates); renderRejected(result.rejected_candidates); } function renderPipeline(pipeline) { const steps = [ ["DISCOVER", pipeline.discovered, "发现候选", ""], ["REJECT", pipeline.rejected, "硬条件淘汰", "is-rejected"], ["FEASIBLE", pipeline.feasible, "通过可执行校验", "is-accent"], ["SOLUTIONS", pipeline.generated_solutions, "生成市场方案", "is-accent"], ]; document.getElementById("pipeline").innerHTML = steps.map(([code, value, label, className]) => `
${code}${escapeHtml(value)}${label}
`).join(""); } function renderRecommendation(result) { const recommendation = result.recommendation; const top = result.candidates.find(item => item.id === recommendation.candidate_id); const pricing = top.pricing; const context = result.market_context; const confidence = Math.round(recommendation.confidence_score * 100); const band = document.getElementById("recommendationBand"); band.className = "recommendation-band"; band.innerHTML = `
AI MARKET RECOMMENDATION

${escapeHtml(recommendation.solution_type)} · ${escapeHtml(recommendation.supplier_name)}

${escapeHtml(recommendation.summary)}

${escapeHtml(top.route_type)} ${escapeHtml(top.transit_days)} 天 与 ${escapeHtml(top.supplier_name)} ${escapeHtml(top.relationship.tier)} ${escapeHtml(top.spot_space.summary)} 低价机会 ${percent(top.relationship.low_price_access_probability)} ${escapeHtml(context.strategy_name)}
客户最终建议报价
${money(pricing.final_quote_amount)}${escapeHtml(pricing.currency)}
授权区间 ${money(pricing.allowed_quote_lower_amount)} – ${money(pricing.allowed_quote_upper_amount)}
置信度 ${confidence}%
`; } function renderCandidates(candidates) { document.getElementById("candidateMeta").textContent = `${candidates.length} 套可执行方案 · 方案、供应商合作与现舱优先`; document.getElementById("candidateGrid").innerHTML = candidates.map((candidate, index) => { const typeClass = candidate.solution_type === "关系优选" ? "type-relationship" : candidate.solution_type === "价格参考" ? "type-price" : candidate.solution_type === "时效参考" ? "type-speed" : "type-primary"; const riskClass = candidate.pricing.cost_risk_status.includes("底线") ? "is-danger" : candidate.pricing.cost_risk_status.includes("安全") ? "is-safe" : ""; return `
${escapeHtml(candidate.solution_type)} RANK ${candidate.rank}

${escapeHtml(candidate.product_name)}

${escapeHtml(candidate.supplier_name)} · ${escapeHtml(candidate.offering_channel)}

预计时效${escapeHtml(candidate.transit_days)} 天
方案保障${candidate.solution_assurance_score}
供应商合作等级${escapeHtml(candidate.relationship.tier)}
现舱${escapeHtml(candidate.spot_space.status)}${candidate.spot_space.teu ? ` · ${escapeHtml(candidate.spot_space.teu)} TEU` : ""}
${scoreRow("方案保障", candidate.solution_assurance_score)} ${scoreRow("关系优势", candidate.relationship_advantage_score)} ${scoreRow("现舱保障", candidate.spot_space_score)} ${scoreRow("供应履约", candidate.supplier_execution_score)} ${scoreRow("客户适配", candidate.customer_fit_score)} ${scoreRow("运输时效", candidate.transit_score)} ${scoreRow("价格参考", candidate.price_score)}
我方与供应商 ${escapeHtml(candidate.relationship.supplier_name)}(${escapeHtml(candidate.relationship.supplier_channel)})的合作关系${escapeHtml(candidate.relationship.tier)} · 供应商合作亲密度 ${percent(candidate.relationship.strength)}
低价获取概率${percent(candidate.relationship.low_price_access_probability)}
优先舱位概率${percent(candidate.relationship.priority_space_probability)}
现舱状态${escapeHtml(candidate.spot_space.status)}

${escapeHtml(candidate.relationship.evidence)}

${candidate.reasons.slice(0, 4).map(escapeHtml).join(";")}
建议客户报价${money(candidate.pricing.final_quote_amount)}
${escapeHtml(candidate.pricing.cost_risk_status)}
`; }).join(""); } function scoreRow(label, value) { const width = Math.max(0, Math.min(100, Number(value))); return `
${label}
${value}
`; } function renderRejected(rejected) { document.getElementById("rejectedMeta").textContent = `${rejected.length} 个淘汰候选`; document.getElementById("rejectedList").innerHTML = rejected.length ? rejected.map(item => `
${escapeHtml(item.product_name)} ${escapeHtml(item.supplier_name)} · ${escapeHtml(item.quote_no)}
${item.reasons.map(reason => `${escapeHtml(reason)}`).join("")}
`).join("") : `
本次没有候选被硬条件淘汰
`; } function renderMarketError(message) { document.getElementById("decisionTitle").textContent = "当前询价无法生成可执行方案"; document.getElementById("auditBtn").disabled = true; document.getElementById("pipeline").innerHTML = ""; const band = document.getElementById("recommendationBand"); band.className = "recommendation-band"; band.innerHTML = `
DECISION STOPPED

${escapeHtml(message)}

`; document.getElementById("candidateGrid").innerHTML = ""; document.getElementById("candidateMeta").textContent = "0 套可执行方案"; } async function openAudit() { if (!currentRunId) return; const dialog = document.getElementById("auditDialog"); const target = document.getElementById("auditContent"); target.innerHTML = `
正在读取审计记录
`; dialog.showModal(); try { const audit = await fetchJson(`/api/market/recommendations/${encodeURIComponent(currentRunId)}/audit`); const explanation = audit.business_explanation; target.innerHTML = `
本次怎么选

${escapeHtml(explanation.decision)}

为什么优先推荐

    ${explanation.why_recommended.map(reason => `
  1. ${escapeHtml(reason)}
  2. `).join("")}

其他方案为什么保留

${explanation.alternatives.map(item => `
${escapeHtml(item.title)}

${escapeHtml(item.detail)}

`).join("") || "

本次没有其他可执行备选方案。

"}

没有采用的方案

判断口径

${escapeHtml(explanation.selection_basis)}

`; } catch (error) { target.innerHTML = `
${escapeHtml(error.message)}
`; } } function auditItem(label, value) { return `
${escapeHtml(label)}${escapeHtml(value)}
`; } async function loadMockCatalog() { const target = document.getElementById("mockCatalogTable"); target.innerHTML = `
正在读取本地 RDF 数据
`; try { mockCatalog = await fetchJson("/api/mock/catalog"); renderMockCatalog(); } catch (error) { target.innerHTML = `
${escapeHtml(error.message)}
`; } } function renderMockCatalog() { if (!mockCatalog) return; const summary = mockCatalog.summary; const stats = [ ["RDF 三元组", summary.triple_count, summary.in_memory_triple_count ? `+${summary.in_memory_triple_count} 内存` : "基础图", summary.in_memory_triple_count > 0], ["客户", summary.customer_count, "Customer", false], ["供应商", summary.supplier_count, "Supplier", false], ["产品", summary.product_count, "ServiceProduct", false], ["报价", summary.quote_count, "SupplierQuote", false], ["路线", summary.route_count, "Route", false], ["启用策略", summary.strategy_count, "Strategy", false], ]; document.getElementById("mockSummary").innerHTML = stats.map(([label, value, note, hasMemory]) => `
${escapeHtml(label)}${numberFormat(value)}${escapeHtml(note)}
`).join(""); document.getElementById("mockSources").innerHTML = mockCatalog.loaded_files .map(source => `${escapeHtml(source)}`).join(""); document.getElementById("systemStatus").textContent = summary.in_memory_triple_count ? `本地知识图谱 · ${numberFormat(summary.triple_count)} triples · +${summary.in_memory_triple_count} 内存` : `本地知识图谱 · ${numberFormat(summary.triple_count)} triples`; renderActiveCatalogTable(); } function selectCatalog(catalog) { activeCatalog = catalog; document.querySelectorAll(".catalog-tab").forEach(button => { button.classList.toggle("is-active", button.dataset.catalog === catalog); }); renderActiveCatalogTable(); } function renderActiveCatalogTable() { if (!mockCatalog) return; const definitions = { routes: { columns: [ ["origin", "起运地"], ["destination", "目的地"], ["transport_modes", "运输方式"], ["product_count", "产品"], ["supplier_count", "供应商"], ["quote_count", "全部报价"], ["current_quote_count", "当前有效"], ], rows: mockCatalog.routes, }, customers: { columns: [ ["name", "客户"], ["industry", "行业"], ["preferred_route", "主要路线"], ["repeat_purchase_count", "复购"], ["price_sensitivity", "价格敏感"], ["time_sensitivity", "时效敏感"], ["reliability_sensitivity", "可靠性敏感"], ], rows: mockCatalog.customers, }, quotes: { columns: [ ["quote_no", "报价编号"], ["route", "路线"], ["transport_mode", "方式"], ["product_name", "产品"], ["supplier_name", "供应商"], ["relationship_tier", "供应商合作等级"], ["relationship_strength", "供应商合作亲密度"], ["low_price_access_probability", "低价获取"], ["priority_space_probability", "优先舱位"], ["spot_space_status", "现舱状态"], ["spot_space_teu", "现舱 TEU"], ["relationship_evidence", "合作依据"], ["supplier_cost_amount", "采购金额"], ["valid_to", "有效至"], ["status", "状态"], ], rows: mockCatalog.quotes, }, strategies: { columns: [ ["name", "策略"], ["route", "适用路线"], ["objective", "目标"], ["adjustment_rate", "价格调整"], ["valid_to", "有效至"], ], rows: mockCatalog.strategies, }, }; const definition = definitions[activeCatalog]; document.getElementById("mockCatalogTable").innerHTML = renderCatalogTable( definition.rows, definition.columns, ); } function renderCatalogTable(rows, columns) { if (!rows.length) return `
当前分类没有数据
`; const head = columns.map(([, label]) => `${escapeHtml(label)}`).join(""); const body = rows.map(row => ` ${columns.map(([key]) => `${formatCatalogCell(key, row[key], row)}`).join("")}` ).join(""); return `${head}${body}
`; } function formatCatalogCell(key, value, row) { if ([ "price_sensitivity", "time_sensitivity", "reliability_sensitivity", "relationship_strength", "low_price_access_probability", "priority_space_probability", ].includes(key)) { const width = Math.round(Number(value || 0) * 100); return `
${width}%
`; } if (key === "supplier_cost_amount") { return `${money(value)} ${escapeHtml(row.currency || "CNY")}${row.runtime_mock ? ` 内存` : ""}`; } if (key === "spot_space_teu") return value ? `${Number(value)} TEU` : "待确认"; if (key === "status") { const className = value === "当前有效" ? "status-current" : value === "已过期" ? "status-expired" : ""; return `${escapeHtml(value)}`; } if (key === "adjustment_rate") return percent(value); return escapeHtml(value ?? ""); } async function generateMockBatch() { if (!marketOptions?.routes?.length) return; const button = document.getElementById("generateMockBtn"); const route = marketOptions.routes[Number(document.getElementById("mockRoute").value)]; if (!route) return; button.disabled = true; button.querySelector("span:first-child").textContent = "正在写入内存图"; document.getElementById("mockStatus").textContent = "正在生成供应商、产品、班期、报价和客户画像"; try { const result = await fetchJson("/api/mock/generate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ origin: route.origin, destination: route.destination, supplier_count: Number(document.getElementById("mockSupplierCount").value), customer_count: Number(document.getElementById("mockCustomerCount").value), evaluation_date: document.getElementById("evaluationDate").value || "2026-07-20", }), }); mockCatalog = result.catalog; document.getElementById("mockStatus").textContent = `批次 ${result.batch_id} 已增加 ${result.added_triples} 条内存三元组`; await refreshMarketOptions(); renderMockCatalog(); } catch (error) { document.getElementById("mockStatus").textContent = error.message; } finally { button.disabled = false; button.querySelector("span:first-child").textContent = "添加一批 Mock 数据"; } } async function resetMockData() { const button = document.getElementById("resetMockBtn"); button.disabled = true; try { mockCatalog = await fetchJson("/api/mock/reset", { method: "POST" }); document.getElementById("mockStatus").textContent = "动态数据已清空,已恢复本地 TTL 基础图"; await refreshMarketOptions(); await requestRecommendation(marketOptions.defaults); renderMockCatalog(); } catch (error) { document.getElementById("mockStatus").textContent = error.message; } finally { button.disabled = false; } } async function refreshMarketOptions() { marketOptions = await fetchJson("/api/market/form-options"); populateInquiryForm(marketOptions); } async function loadOperations() { await Promise.all([ loadTable("orders", endpoints.orders, ["orderNo", "customerName", "status", "currentNodeName", "ownerName"]), loadTable("risks", endpoints.risks, ["orderNo", "riskName", "exceptionName", "affectedNodeName"]), loadFlow(), loadTable("nodeTimes", endpoints.nodeTimes, ["sequence", "nodeName", "deadlineName", "plannedTime", "cutoffTime", "actualTime", "isCompleted", "isDelayed"]), loadTable("cutoffs", endpoints.cutoffs, ["orderNo", "customsCutoffTime", "terminalCutoffTime", "vgmCutoffTime", "estimatedSailingTime"]), loadTable("exceptionMap", endpoints.exceptionMap, ["exceptionName", "affectedNodeName", "riskName"]), ]); operationsLoaded = true; } async function fetchJson(url, options) { const response = await fetch(url, options); if (!response.ok) { let detail = `请求失败:HTTP ${response.status}`; try { const payload = await response.json(); detail = payload.detail || detail; } catch (_) { // Preserve the HTTP message when the response is not JSON. } throw new Error(detail); } return response.json(); } async function fetchRows(url) { const data = await fetchJson(url); return data.rows || []; } async function loadTable(id, url, columns) { const target = document.getElementById(id); const counter = document.getElementById(`${id}Count`); try { const rows = await fetchRows(url); if (counter) counter.textContent = rows.length; target.innerHTML = renderTable(rows, columns); } catch (error) { target.innerHTML = `
本地RDF数据读取失败
`; } } async function loadFlow() { const target = document.getElementById("flow"); try { const rows = await fetchRows(endpoints.flow); if (!rows.length) { target.innerHTML = `
暂无流程数据
`; return; } target.innerHTML = rows.map(row => `
${escapeHtml(row.sequence)}. ${escapeHtml(row.nodeName)} ${row.deadlineName ? `时限:${escapeHtml(row.deadlineName)}` : "无关键时限"} ${row.nextNodeName ? `下一步:${escapeHtml(row.nextNodeName)}` : "流程结束"}
`).join(""); } catch (error) { target.innerHTML = `
流程读取失败
`; } } function renderTable(rows, columns) { if (!rows.length) return `
暂无数据
`; const head = columns.map(column => `${labels[column] || column}`).join(""); const body = rows.map(row => ` ${columns.map(column => `${formatCell(column, row[column])}`).join("")}`).join(""); return `${head}${body}
`; } function formatCell(column, value) { if (!value) return ""; if (column === "isDelayed" && value === "true") return ``; if (column === "isDelayed" && value === "false") return "否"; if (column === "isCompleted" && value === "true") return "是"; if (column === "isCompleted" && value === "false") return ``; if (column.endsWith("Time")) return escapeHtml(value.replace("T", " ").replace("+08:00", "")); return escapeHtml(value); } function money(value) { return new Intl.NumberFormat("zh-CN", { maximumFractionDigits: 0 }).format(Number(value)); } function numberFormat(value) { return new Intl.NumberFormat("zh-CN").format(Number(value)); } function percent(value) { return `${Math.round(Number(value) * 100)}%`; } function shortDate(value) { return value ? value.slice(5).replace("-", "/") : "待确认"; } function escapeHtml(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); }