Files
lserp_cs_6.0/插件库/Lskj.AgentPet/pet-shell.js
2026-08-14 14:28:28 +08:00

899 lines
33 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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();
})();