(function () { "use strict"; const runtime = window.LskjPetRuntime; const bridgeApi = window.LskjBridgeClient; const shell = document.getElementById("pet-shell"); const anchor = document.getElementById("pet-anchor"); const panel = document.getElementById("assistant-panel"); const sprite = document.getElementById("pet-sprite"); const status = document.getElementById("pet-status"); const connection = document.getElementById("connection-state"); const closePetButton = document.getElementById("close-pet"); const moduleTitle = document.getElementById("module-title"); const contextScope = document.getElementById("context-scope"); const conversation = document.getElementById("conversation"); const composer = document.getElementById("composer"); const input = document.getElementById("message-input"); const attachButton = document.getElementById("attach-files"); const sendButton = document.getElementById("send-message"); const attachmentList = document.getElementById("attachment-list"); const planPreview = document.getElementById("plan-preview"); const planTitle = document.getElementById("plan-title"); const planDetail = document.getElementById("plan-detail"); const executeButton = document.getElementById("execute-plan"); const quickActionButtons = Array.from( document.querySelectorAll("[data-quick-action]")); const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; const previewParameters = new URLSearchParams(window.location.search); const previewMode = previewParameters.get("preview") === "1"; if (previewMode) shell.dataset.preview = "enabled"; const state = new runtime.PetStateMachine("offline"); const animator = new runtime.AtlasAnimator(sprite, { reducedMotion }); let bridge = null; let activePlan = null; let activeAssistantMessage = null; let attachments = []; let uploading = false; let chatBusy = false; let assetReady = false; let bridgeConnected = false; let assetLoadGeneration = 0; let contextRefreshGeneration = 0; let hoverOpenTimer = null; let panelOpenedByHover = false; let planReviewRequired = false; let planReviewSatisfied = true; state.subscribe(event => { animator.playSemantic(event.state); const labels = { idle: "可以问我", listening: "我在听", thinking: "正在理解", planning: "正在生成预览", awaiting_confirmation: "等待确认", executing: "ERP 正在执行", success: "操作完成", error: "需要检查", offline: "等待 ERP" }; status.textContent = labels[event.state] || "处理中"; }); animator.playSemantic("offline"); anchor.addEventListener("mouseenter", scheduleHoverOpen); anchor.addEventListener("mouseleave", cancelHoverOpen); anchor.addEventListener("click", () => { cancelHoverOpen(); const open = runtime.panelOpenAfterAnchorClick( shell.dataset.panel === "open", panelOpenedByHover); panelOpenedByHover = false; setPanel(open, true); }); composer.addEventListener("submit", onSubmit); input.addEventListener("keydown", event => { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); composer.requestSubmit(); } }); executeButton.addEventListener("click", executeActivePlan); planDetail.addEventListener("scroll", updatePlanReviewFromScroll); attachButton.addEventListener("click", pickAttachments); closePetButton.addEventListener("click", closePet); quickActionButtons.forEach(button => { button.addEventListener("click", () => { if (button.disabled) return; let prompt; try { prompt = runtime.quickActionPrompt(button.dataset.quickAction); } catch (_) { return; } sendChat(prompt, button.textContent.trim()); }); }); if (!previewMode && window.chrome && window.chrome.webview) { window.chrome.webview.addEventListener("message", onHostMessage); bridge = new bridgeApi.BridgeClient(new bridgeApi.WebViewTransport(window.chrome.webview)); connection.textContent = "正在验证素材"; connection.dataset.state = "offline"; } else { appendMessage("system", "当前是浏览器预览模式;Windows 宿主启动后会连接 ERP 命令桥和 AstrBot。 "); configureLocalPreview(); } function configureLocalPreview() { const localPreviewOrigin = window.location.protocol === "file:" || window.location.hostname === "127.0.0.1" || window.location.hostname === "localhost"; if (!localPreviewOrigin || previewParameters.get("preview") !== "1") return; const previewSprite = previewParameters.get("previewSprite"); if (previewSprite) animator.setAsset(previewSprite); applyContext({ userId: "DEMO-USER", userName: "演示用户", accountBook: "演示账套", subSystemId: "DEMO-SCM", subSystemName: "采购管理", databaseScopeFingerprint: "a".repeat(64), isAdministrator: false, activeModule: { moduleCode: "CGDD", navigationCode: "NAV-CGDD", moduleName: "采购订单" } }); connection.textContent = "原型预览"; connection.dataset.state = "online"; state.set("awaiting_confirmation"); setPanel(true); const lineMatches = [ previewPurchaseLine("L1", "MAT-1001", 12, 320, 0.13, "PO-20260811-018", "PO-L1", 30), previewPurchaseLine("L2", "MAT-2048", 5, 680, 0.13, "PO-20260811-018", "PO-L2", 8), previewPurchaseLine("L3", "MAT-3106", 18, 190, 0.06, "PO-20260809-006", "PO-L7", 40) ]; const preview = { "供应商": "示例供应商", "发票号码": "INV-20260811-018", "发票日期": "2026-08-11", "币种": "CNY", "不含税金额": 10660, "税额": 1146.4, "价税合计": 11806.4, "来源附件": ["invoice.pdf (0123456789ab…)"], "明细汇总不含税": 10660, "明细汇总税额": 1146.4, "明细汇总价税": 11806.4, "发票行数": 3, "确定匹配行数": 3, "来源采购单": ["PO-20260811-018", "PO-20260809-006"], "来源汇率": [1], "重复发票": false }; showPlan({ planId: "0123456789abcdef0123456789abcdef", commandName: "purchase.invoice.create", moduleCode: "CGDD", risk: "write", valid: true, executionAllowed: true, title: "采购发票识别结果", preview, data: { preview, lineMatches }, warnings: [] }); } function previewPurchaseLine( invoiceLineId, materialCode, invoiceQuantity, invoiceUnitPrice, invoiceTaxRate, sourceOrderNumber, sourceLineId, sourceRemainingQuantity) { const invoiceLineAmount = invoiceQuantity * invoiceUnitPrice; const invoiceTaxAmount = Number( (invoiceLineAmount * invoiceTaxRate).toFixed(2)); return { invoiceLineId, materialCode, invoiceUnit: "件", invoiceQuantity, invoiceUnitPrice, invoiceTaxRate, invoiceTaxAmount, invoiceLineAmount, status: "exact", sourceOrderNumber, sourceLineId, unit: "件", remainingQuantity: sourceRemainingQuantity, unitPrice: invoiceUnitPrice, taxRate: invoiceTaxRate, exchangeRate: 1, sourceUnit: "件", sourceRemainingQuantity, sourceUnitPrice: invoiceUnitPrice, sourceTaxRate: invoiceTaxRate, sourceExchangeRate: 1, candidateCount: 1, issues: [] }; } function setPanel(open, focusInput) { shell.dataset.panel = open ? "open" : "closed"; anchor.setAttribute("aria-expanded", String(open)); panel.setAttribute("aria-hidden", String(!open)); if (!open) panelOpenedByHover = false; if (open) { refreshActiveContext(); if (focusInput === true) setTimeout(() => input.focus(), 180); } } function scheduleHoverOpen() { if (shell.dataset.panel === "open" || hoverOpenTimer !== null) return; hoverOpenTimer = setTimeout(() => { hoverOpenTimer = null; // 悬停只展开,不抢走 ERP 当前控件的键盘焦点;点击才聚焦输入框。 panelOpenedByHover = true; setPanel(true, false); }, 480); } function cancelHoverOpen() { if (hoverOpenTimer === null) return; clearTimeout(hoverOpenTimer); hoverOpenTimer = null; } async function refreshActiveContext() { if (previewMode) return true; if (!bridge || !bridgeConnected) return false; const generation = ++contextRefreshGeneration; try { const context = await bridge.context(); if (generation !== contextRefreshGeneration) return false; return applyContext(context); } catch (_) { // 展开动作不能制造错误气泡;真正发送问题时 AstrBot 会再次实时取上下文, // 桥不可用则沿既有错误通道给出关联 ID。 if (generation === contextRefreshGeneration) applyContext(null); return false; } } function applyContext(context) { const scope = runtime.erpSessionScopeSummary(context); quickActionButtons.forEach(button => { button.hidden = !runtime.quickActionVisible( button.dataset.quickAction, context); }); const active = context && context.activeModule; moduleTitle.textContent = active && active.moduleName ? active.moduleName : context && context.subSystemName ? context.subSystemName : "ERP 会话未确认"; if (!scope.complete) { contextScope.textContent = "会话范围未确认,执行前必须重新验证"; contextScope.title = ""; contextScope.dataset.state = "error"; return false; } const administrator = scope.isAdministrator ? " · 管理员" : ""; contextScope.textContent = `账套 ${scope.accountBook} · ${scope.subSystemName} · ${scope.userName}${administrator} · 库 ${scope.databaseEvidence}`; contextScope.title = `用户 ${scope.userId};子系统 ${scope.subSystemId};数据库作用域证据 ${scope.databaseEvidence}`; contextScope.dataset.state = "bound"; return true; } function closePet() { if (!previewMode && window.chrome && window.chrome.webview) { window.chrome.webview.postMessage({ type: "lserp.window.close" }); return; } setPanel(false); } async function connectBridge() { if (!assetReady || !bridge) return; try { await bridge.health(); const context = await bridge.context(); if (!applyContext(context)) { const scopeError = new Error("当前 ERP 会话范围不完整或格式无效。"); scopeError.code = "erp_session_scope_invalid"; throw scopeError; } connection.textContent = "已连接"; connection.dataset.state = "online"; bridgeConnected = true; state.set("idle"); updateComposerState(); } catch (error) { bridgeConnected = false; applyContext(null); connection.textContent = "连接失败"; connection.dataset.state = "error"; state.set("offline"); appendMessage("error", safeError(error)); updateComposerState(); } } async function configureSprite(assetUrl) { const generation = ++assetLoadGeneration; assetReady = false; bridgeConnected = false; animator.setAsset(""); connection.textContent = "正在验证素材"; connection.dataset.state = "offline"; updateComposerState(); try { const loaded = await runtime.loadAtlasAsset(assetUrl); if (generation !== assetLoadGeneration) return; animator.setAsset(loaded.url); assetReady = true; await connectBridge(); } catch (error) { if (generation !== assetLoadGeneration) return; connection.textContent = "素材无效"; connection.dataset.state = "error"; state.set("error"); appendMessage("error", "guga 图集无法完整解码或尺寸不符合 1536×1872,桌宠已停止连接 ERP。"); updateComposerState(); } } function onHostMessage(event) { const message = event && event.data; if (!message) return; if (message.type === "lserp.pet.configure" && message.spriteUrl) { configureSprite(message.spriteUrl); return; } if (message.type === "lserp.chat.delta") { appendAssistantDelta(message.text || ""); return; } if (message.type === "lserp.chat.result") { chatBusy = false; updateComposerState(); state.set("idle"); const streamed = finishAssistantStream(); if (message.text && !streamed) appendMessage("assistant", message.text); if (message.commandPlan) { const correlatedPlan = Object.assign({}, message.commandPlan); if (typeof message.bridgeCorrelationId === "string" && /^[A-Za-z0-9_.:-]{8,128}$/.test(message.bridgeCorrelationId)) { correlatedPlan.bridgeCorrelationId = message.bridgeCorrelationId; } showPlan(correlatedPlan); } return; } if (message.type === "lserp.chat.error") { chatBusy = false; updateComposerState(); finishAssistantStream(); state.set("error"); appendMessage("error", appendCorrelation( message.message || "对话服务暂时不可用。", message.correlationId)); return; } if (message.type === "lserp.attachments.uploading") { uploading = message.uploading === true; updateComposerState(); if (uploading) state.set("thinking"); else if (!chatBusy && state.state === "thinking") state.set("idle"); return; } if (message.type === "lserp.attachments.changed") { attachments = Array.isArray(message.attachments) ? message.attachments.filter(item => item && typeof item.attachmentId === "string") : []; renderAttachments(); updateComposerState(); return; } if (message.type === "lserp.attachments.error") { appendMessage("error", appendCorrelation( message.message || "附件处理失败。", message.correlationId)); state.set("error"); } } function onSubmit(event) { event.preventDefault(); const text = input.value.trim(); if ((!text && attachments.length === 0) || uploading || chatBusy || !bridgeConnected) return; sendChat( text, text || `已发送 ${attachments.length} 个附件,请识别并生成业务预览。`); } function sendChat(text, visibleText) { if ((!text && attachments.length === 0) || uploading || chatBusy || !bridgeConnected) return false; appendMessage("user", visibleText || text); finishAssistantStream(); activePlan = null; planReviewRequired = false; planReviewSatisfied = true; planPreview.hidden = true; executeButton.disabled = true; input.value = ""; resizeInput(); chatBusy = true; updateComposerState(); state.set("thinking"); if (window.chrome && window.chrome.webview) { window.chrome.webview.postMessage({ type: "lserp.chat.send", text }); } else { setTimeout(() => { chatBusy = false; updateComposerState(); state.set("idle"); appendMessage("assistant", "浏览器预览没有连接 AstrBot。正式宿主会把这条消息连同当前 ERP 上下文发送给机器人。"); }, 320); } return true; } function pickAttachments() { if (uploading || chatBusy || !bridgeConnected) return; if (window.chrome && window.chrome.webview) { window.chrome.webview.postMessage({ type: "lserp.attachment.pick" }); return; } appendMessage("system", "浏览器预览不能读取本地附件;Windows 宿主会打开原生选件窗口。"); } function renderAttachments() { attachmentList.replaceChildren(); attachmentList.hidden = attachments.length === 0; attachments.forEach(item => { const chip = document.createElement("span"); chip.className = "attachment-chip"; const name = document.createElement("span"); name.className = "attachment-name"; name.textContent = item.fileName || "附件"; name.title = item.fileName || "附件"; const remove = document.createElement("button"); remove.type = "button"; remove.className = "attachment-remove"; remove.setAttribute("aria-label", "移除附件 " + (item.fileName || "")); remove.textContent = "×"; remove.disabled = uploading || chatBusy; remove.addEventListener("click", () => { if (!window.chrome || !window.chrome.webview) return; window.chrome.webview.postMessage({ type: "lserp.attachment.remove", attachmentId: item.attachmentId }); }); chip.append(name, remove); attachmentList.appendChild(chip); }); } function updateComposerState() { const unavailable = !previewMode && !bridgeConnected; attachButton.disabled = uploading || chatBusy || unavailable; input.disabled = uploading || chatBusy || unavailable; sendButton.disabled = uploading || chatBusy || unavailable || (!input.value.trim() && attachments.length === 0); attachButton.textContent = uploading ? "上传中" : "+附件"; attachmentList.querySelectorAll(".attachment-remove").forEach(button => { button.disabled = uploading || chatBusy || unavailable; }); quickActionButtons.forEach(button => { // 快捷问题永不隐式携带待处理发票;用户必须先发送或移除附件。 button.disabled = uploading || chatBusy || unavailable || attachments.length > 0; }); } function showPlan(plan) { if (!plan || !plan.planId) return; const purchasePreview = runtime.purchaseLinePreview(plan); const leavePreview = runtime.leaveConfirmationPreview(plan); const tracePreview = runtime.initializationTracePreview(plan); const dynamicPreview = runtime.dynamicModuleConfirmationPreview(plan); const correlationBound = plan && typeof plan.bridgeCorrelationId === "string" && /^[A-Za-z0-9_.:-]{8,128}$/.test(plan.bridgeCorrelationId); const executable = runtime.isExecutablePlan(plan) && (previewMode || correlationBound); activePlan = executable ? plan : null; planReviewRequired = executable && ((purchasePreview.required && purchasePreview.complete) || (dynamicPreview.required && dynamicPreview.complete)); planReviewSatisfied = !planReviewRequired; planTitle.textContent = plan.title || plan.commandName || "业务操作预览"; const preview = dynamicPreview.required ? { "命令": plan.commandName, "模块": plan.moduleCode, "风险": plan.risk, "计划号": plan.planId, "动态配置": dynamicPreview.complete ? dynamicPreview.mode === "create" ? `${dynamicPreview.totalValues} 个参数,${dynamicPreview.detailRows.length} 行明细` : `${dynamicPreview.changes.length} 个字段将修改` : "确认结构未通过安全校验" } : Object.assign({}, plan.preview || (plan.data && plan.data.preview) || { "命令": plan.commandName, "模块": plan.moduleCode, "风险": plan.risk, "计划号": plan.planId }); if (Array.isArray(plan.warnings) && plan.warnings.length) { preview["需处理"] = plan.warnings.slice(0, 5).join(";"); } if (purchasePreview.required && !purchasePreview.complete) { preview["安全检查"] = "逐行匹配证据不完整,已禁止执行;请重新生成采购预览。"; } if (leavePreview.required && !leavePreview.complete) { preview["安全检查"] = "请假确认信息不完整,已禁止执行;请重新生成请假预览。"; } if (tracePreview.required && !tracePreview.complete) { preview["安全检查"] = "初始化诊断的模块范围或风险说明不完整,已禁止执行。"; } if (dynamicPreview.required && !dynamicPreview.complete) { preview["安全检查"] = "低代码动态参数、并发快照或适配器证据不完整,已禁止执行。"; } const detailNodes = [buildDefinitionList(preview, 24)]; if (purchasePreview.complete && purchasePreview.lines.length) { detailNodes.push(buildPurchaseLineList(purchasePreview.lines)); } if (dynamicPreview.required && dynamicPreview.complete) { detailNodes.push(buildDynamicModulePreview(dynamicPreview)); } planDetail.replaceChildren(...detailNodes); planDetail.scrollTop = 0; planPreview.hidden = false; executeButton.disabled = !executable || (!previewMode && !bridgeConnected) || !planReviewSatisfied; executeButton.textContent = executable && !planReviewSatisfied ? "请先向下滚动核对全部内容" : executable ? "在 ERP 中确认并执行" : purchasePreview.required && !purchasePreview.complete ? "逐行匹配证据不完整,不能执行" : leavePreview.required && !leavePreview.complete ? "请假确认信息不完整,不能执行" : tracePreview.required && !tracePreview.complete ? "诊断范围或风险说明不完整,不能执行" : dynamicPreview.required && !dynamicPreview.complete ? "动态参数或并发证据不完整,不能执行" : plan.valid === false ? "预览未通过,不能执行" : plan.risk === "read" ? "只读结果,无需执行" : "解析预览,无需执行"; state.set(executable ? "awaiting_confirmation" : "idle"); if (planReviewRequired) { requestAnimationFrame(updatePlanReviewFromScroll); } if (!previewMode) void refreshActiveContext(); } function updatePlanReviewFromScroll() { if (!planReviewRequired || planReviewSatisfied || !activePlan) return; planReviewSatisfied = runtime.previewReviewComplete( planDetail.scrollTop, planDetail.clientHeight, planDetail.scrollHeight); if (planReviewSatisfied) syncActivePlanButton(); } function syncActivePlanButton() { if (!activePlan) { executeButton.disabled = true; return; } executeButton.disabled = (!previewMode && !bridgeConnected) || !planReviewSatisfied; executeButton.textContent = planReviewSatisfied ? "在 ERP 中确认并执行" : "请先向下滚动核对全部内容"; } async function executeActivePlan() { if (!activePlan || !bridge || !bridgeConnected || !planReviewSatisfied) return; executeButton.disabled = true; if (!previewMode && !await refreshActiveContext()) { activePlan = null; planReviewRequired = false; planReviewSatisfied = false; executeButton.disabled = true; executeButton.textContent = "ERP 会话范围已变化,请重新生成预览"; appendMessage( "error", "当前 ERP 数据库、用户、账套或子系统范围无法重新确认;旧计划已作废,请确认登录范围后重新发起。" ); state.set("error"); return; } state.set("executing"); try { const data = await bridge.execute( activePlan.planId, activePlan.bridgeCorrelationId); const result = data.result || {}; const receipt = result.recordId ? (result.message || "ERP 操作已完成。") + "(业务记录:" + result.recordId + ")" : result.message || "ERP 操作已完成。"; appendMessage("assistant", appendCorrelation( receipt, data.bridgeCorrelationId)); const diagnosticSummary = runtime.diagnosticExecutionSummary(result); if (diagnosticSummary) appendMessage("system", diagnosticSummary); executeButton.textContent = "在 ERP 中确认并执行"; if (data.followupPlan) { showPlan(data.followupPlan); appendMessage("system", "下一步仍是独立操作,请核对新预览后再次确认。"); } else { if (data.followupCode) { appendMessage( "system", "ERP 操作已完成,但后续操作预览未生成(" + data.followupCode + ")。后续操作尚未执行,请重新询问桌宠。" ); } planPreview.hidden = true; activePlan = null; planReviewRequired = false; planReviewSatisfied = true; state.set("success"); setTimeout(() => state.set("idle"), 1600); } } catch (error) { const failure = runtime.bridgeFailureSummary(error); appendMessage( failure.code === "user_cancelled" ? "system" : "error", failure.text); if (failure.planInvalidated) { activePlan = null; planReviewRequired = false; planReviewSatisfied = false; planPreview.hidden = true; executeButton.textContent = "请重新生成预览"; } state.set(failure.code === "user_cancelled" ? "idle" : "error"); } finally { syncActivePlanButton(); } } function appendMessage(kind, text) { if (!text) return; const paragraph = document.createElement("p"); paragraph.className = kind + "-message"; paragraph.textContent = text; conversation.appendChild(paragraph); conversation.scrollTop = conversation.scrollHeight; } function appendAssistantDelta(text) { if (!text) return; if (!activeAssistantMessage) { activeAssistantMessage = document.createElement("p"); activeAssistantMessage.className = "assistant-message"; conversation.appendChild(activeAssistantMessage); } activeAssistantMessage.appendChild(document.createTextNode(text)); conversation.scrollTop = conversation.scrollHeight; } function finishAssistantStream() { const hadStream = !!activeAssistantMessage; activeAssistantMessage = null; return hadStream; } function buildDefinitionList(values, maximumEntries) { const list = document.createElement("dl"); const entries = Object.entries(values || {}); const limit = Number.isInteger(maximumEntries) ? maximumEntries : 24; entries.slice(0, limit).forEach(([key, value]) => { const term = document.createElement("dt"); const description = document.createElement("dd"); term.textContent = key; description.textContent = value === null || value === undefined ? "-" : typeof value === "object" ? JSON.stringify(value) : String(value); list.append(term, description); }); if (entries.length > limit) { const term = document.createElement("dt"); const description = document.createElement("dd"); term.textContent = "更多字段"; description.textContent = `还有 ${entries.length - limit} 项,请重新生成更小的操作预览。`; list.append(term, description); } return list; } function buildPurchaseLineList(lines) { const section = document.createElement("section"); section.className = "purchase-line-preview"; const heading = document.createElement("h3"); heading.textContent = `逐行来源匹配(${lines.length} 行)`; section.appendChild(heading); lines.forEach((line, index) => { const card = document.createElement("article"); card.className = "purchase-line-card"; const title = document.createElement("h4"); title.textContent = `${line.invoiceLineId || `第 ${index + 1} 行`} · ${line.materialCode}`; const status = document.createElement("span"); status.className = "purchase-match-status"; status.textContent = "唯一匹配"; const header = document.createElement("div"); header.className = "purchase-line-header"; header.append(title, status); card.appendChild(header); card.appendChild(buildDefinitionList({ "本次数量": `${displayNumber(line.invoiceQuantity)} ${line.invoiceUnit}`, "本次单价": displayNumber(line.invoiceUnitPrice), "本次税率": displayPercent(line.invoiceTaxRate), "本次税额": displayNumber(line.invoiceTaxAmount), "本次行金额": displayNumber(line.invoiceLineAmount), "来源采购单": line.sourceOrderNumber, "来源明细": line.sourceLineId, "来源剩余": `${displayNumber(line.sourceRemainingQuantity)} ${line.sourceUnit}`, "来源单价": displayNumber(line.sourceUnitPrice), "来源税率": displayPercent(line.sourceTaxRate), "来源汇率": displayNumber(line.sourceExchangeRate) }, 16)); section.appendChild(card); }); return section; } function buildDynamicModulePreview(preview) { const section = document.createElement("section"); section.className = "dynamic-module-preview"; const heading = document.createElement("h3"); heading.textContent = preview.mode === "create" ? "低代码动态参数 · 新增" : "低代码动态参数 · 并发修改"; section.appendChild(heading); if (preview.mode === "update") { section.appendChild(buildDefinitionList({ "目标记录": preview.recordDisplay, "修改字段": preview.changes.length }, 4)); const changes = document.createElement("div"); changes.className = "dynamic-change-list"; preview.changes.forEach(change => { const card = dynamicFieldCard( change.label, change.parameterId, change.valueType); const comparison = document.createElement("div"); comparison.className = "dynamic-change-values"; comparison.append( dynamicChangeValue("修改前", change.previousValue), dynamicChangeValue("修改后", change.newValue)); card.appendChild(comparison); changes.appendChild(card); }); section.appendChild(changes); return section; } const master = document.createElement("section"); master.className = "dynamic-value-group"; const masterHeading = document.createElement("h4"); masterHeading.textContent = `主表字段(${preview.masterValues.length} 项)`; master.append(masterHeading, buildDynamicValueCards(preview.masterValues)); section.appendChild(master); const details = document.createElement("section"); details.className = "dynamic-detail-preview"; const detailHeading = document.createElement("h4"); detailHeading.textContent = `明细数据(${preview.detailRows.length} 行)`; details.appendChild(detailHeading); if (preview.detailRows.length === 0) { const empty = document.createElement("p"); empty.className = "dynamic-empty-value"; empty.textContent = "本次新增没有明细行。"; details.appendChild(empty); } else { preview.detailRows.forEach(row => { const card = document.createElement("article"); card.className = "dynamic-detail-row"; const title = document.createElement("h5"); title.textContent = `第 ${row.rowNumber} 行 · ${row.values.length} 个字段`; card.append(title, buildDynamicValueCards(row.values)); details.appendChild(card); }); } section.appendChild(details); return section; } function buildDynamicValueCards(values) { const list = document.createElement("div"); list.className = "dynamic-value-list"; if (values.length === 0) { const empty = document.createElement("p"); empty.className = "dynamic-empty-value"; empty.textContent = "没有需要写入的字段。"; list.appendChild(empty); return list; } values.forEach(value => { const card = dynamicFieldCard( value.label, value.parameterId, value.valueType); const content = document.createElement("div"); content.className = "dynamic-field-value"; content.textContent = value.value === "" ? "(空值)" : value.value; card.appendChild(content); list.appendChild(card); }); return list; } function dynamicFieldCard(label, parameterId, valueType) { const card = document.createElement("article"); card.className = "dynamic-field-card"; const header = document.createElement("div"); header.className = "dynamic-field-header"; const title = document.createElement("h5"); title.textContent = label; const type = document.createElement("span"); type.className = "dynamic-value-type"; type.textContent = valueType; header.append(title, type); const identifier = document.createElement("code"); identifier.className = "dynamic-parameter-id"; identifier.textContent = parameterId; card.append(header, identifier); return card; } function dynamicChangeValue(label, value) { const item = document.createElement("div"); const heading = document.createElement("span"); heading.textContent = label; const content = document.createElement("div"); content.textContent = value === "" ? "(空值)" : value; item.append(heading, content); return item; } function displayNumber(value) { return new Intl.NumberFormat("zh-CN", { maximumFractionDigits: 8, useGrouping: true }).format(value); } function displayPercent(value) { return new Intl.NumberFormat("zh-CN", { style: "percent", maximumFractionDigits: 6 }).format(value); } function safeError(error) { return runtime.bridgeFailureSummary(error).text; } function appendCorrelation(message, correlationId) { return correlationId && /^[A-Za-z0-9_.:-]{8,128}$/.test(correlationId) ? message + "(关联 ID:" + correlationId + ")" : message; } function resizeInput() { input.style.height = "auto"; input.style.height = Math.min(input.scrollHeight, 90) + "px"; } input.addEventListener("input", () => { resizeInput(); updateComposerState(); }); updateComposerState(); })();