Files
SystemSimulationApp/app/static/index.html
T

1414 lines
41 KiB
HTML

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>System Simulation App</title>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
height: 100vh;
color: #1f2933;
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
background: #f4f7fb;
}
.app {
display: grid;
grid-template-rows: 48px minmax(0, 1fr);
height: 100vh;
}
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 0 14px;
border-bottom: 1px solid #d6dde8;
background: #ffffff;
}
.title {
min-width: 0;
overflow: hidden;
font-size: 15px;
font-weight: 650;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
.actions {
display: flex;
align-items: center;
gap: 8px;
}
.status {
max-width: 260px;
overflow: hidden;
color: #5d6877;
font-size: 13px;
text-overflow: ellipsis;
white-space: nowrap;
}
button {
height: 32px;
padding: 0 12px;
border: 1px solid #1d6fb8;
border-radius: 6px;
color: #ffffff;
font: inherit;
font-size: 14px;
background: #1d6fb8;
cursor: pointer;
}
button.secondary {
border-color: #c7d0dc;
color: #273646;
background: #ffffff;
}
button:disabled {
border-color: #a7b0bd;
color: #ffffff;
background: #a7b0bd;
cursor: wait;
}
.workbench {
display: grid;
grid-template-columns: minmax(0, 1fr) 340px;
min-height: 0;
}
.canvas {
min-width: 0;
min-height: 0;
}
iframe {
display: block;
width: 100%;
height: 100%;
border: 0;
}
.side-panel {
display: grid;
grid-template-rows: auto minmax(140px, 32%) minmax(0, 1fr);
min-height: 0;
border-left: 1px solid #d6dde8;
background: #ffffff;
}
.panel-header,
.panel-section {
padding: 12px;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
border-bottom: 1px solid #e3e8ef;
}
.panel-title {
font-size: 14px;
font-weight: 650;
}
.component-list {
min-height: 0;
overflow: auto;
border-bottom: 1px solid #e3e8ef;
}
.component-row {
display: block;
width: 100%;
height: auto;
min-height: 54px;
padding: 9px 12px;
border: 0;
border-bottom: 1px solid #edf1f5;
border-radius: 0;
color: #1f2933;
text-align: left;
background: #ffffff;
}
.component-row:hover,
.component-row.active {
background: #eef6ff;
}
.component-name {
overflow: hidden;
font-size: 14px;
font-weight: 620;
text-overflow: ellipsis;
white-space: nowrap;
}
.component-meta {
margin-top: 3px;
overflow: hidden;
color: #667387;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.empty-state {
padding: 18px 12px;
color: #667387;
font-size: 13px;
}
.parameter-panel {
min-height: 0;
overflow: auto;
}
.selected-heading {
margin: 0;
overflow-wrap: anywhere;
font-size: 15px;
font-weight: 650;
}
.selected-meta {
margin-top: 4px;
overflow-wrap: anywhere;
color: #667387;
font-size: 12px;
}
.field-grid {
display: grid;
gap: 12px;
margin-top: 14px;
}
.field {
display: grid;
gap: 5px;
}
.name-field {
margin-top: 14px;
}
.field label {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
color: #334155;
font-size: 13px;
font-weight: 590;
}
.field-key {
color: #7b8797;
font-size: 12px;
font-weight: 400;
}
.input-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 8px;
}
input {
width: 100%;
height: 34px;
padding: 0 9px;
border: 1px solid #c7d0dc;
border-radius: 6px;
color: #1f2933;
font: inherit;
font-size: 14px;
background: #ffffff;
}
input:focus {
border-color: #1d6fb8;
outline: 2px solid rgba(29, 111, 184, 0.16);
}
.unit {
min-width: 26px;
color: #667387;
font-size: 12px;
}
.form-actions {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
@media (max-width: 980px) {
.workbench {
grid-template-columns: minmax(0, 1fr);
grid-template-rows: minmax(0, 1fr) 360px;
}
.side-panel {
grid-template-rows: auto minmax(120px, 42%) minmax(0, 1fr);
border-top: 1px solid #d6dde8;
border-left: 0;
}
}
</style>
</head>
<body>
<main class="app">
<header class="toolbar">
<div class="title">System Simulation App</div>
<div class="actions">
<span class="status" id="status">加载中</span>
<button id="refresh-button" class="secondary" type="button" disabled>
刷新组件
</button>
<button id="export-button" type="button" disabled>生成 XML</button>
</div>
</header>
<section class="workbench">
<div class="canvas">
<iframe id="drawio-frame" title="draw.io"></iframe>
</div>
<aside class="side-panel">
<div class="panel-header">
<div class="panel-title">组件参数</div>
</div>
<div class="component-list" id="component-list">
<div class="empty-state">暂无组件</div>
</div>
<div class="parameter-panel">
<div class="panel-section">
<h2 class="selected-heading" id="selected-heading">未选择组件</h2>
<div class="selected-meta" id="selected-meta"></div>
<form id="parameter-form">
<div class="field name-field">
<label for="component-name-input">
<span>组件名称</span>
<span class="field-key">name</span>
</label>
<input
id="component-name-input"
name="component_name"
type="text"
disabled
/>
</div>
<div class="field-grid" id="field-grid"></div>
<div class="form-actions">
<button id="apply-button" type="submit" disabled>
应用参数
</button>
</div>
</form>
</div>
</div>
</aside>
</section>
</main>
<script>
const DRAWIO_BASE_URL = __DRAWIO_BASE_URL__;
const frame = document.getElementById("drawio-frame");
const exportButton = document.getElementById("export-button");
const refreshButton = document.getElementById("refresh-button");
const applyButton = document.getElementById("apply-button");
const statusNode = document.getElementById("status");
const componentListNode = document.getElementById("component-list");
const selectedHeadingNode = document.getElementById("selected-heading");
const selectedMetaNode = document.getElementById("selected-meta");
const parameterForm = document.getElementById("parameter-form");
const fieldGridNode = document.getElementById("field-grid");
const componentNameInput = document.getElementById("component-name-input");
const drawioUrl = new URL(DRAWIO_BASE_URL, window.location.href);
const componentLibraryUrl = new URL(
"/api/component-library.drawiolib",
window.location.href,
);
const componentLibraryKey = `U${componentLibraryUrl.href}`;
drawioUrl.searchParams.set("embed", "1");
drawioUrl.searchParams.set("proto", "json");
drawioUrl.searchParams.set("libraries", "1");
drawioUrl.searchParams.set("clibs", componentLibraryKey);
drawioUrl.searchParams.set(
"cors",
`^${escapeRegExp(window.location.origin)}/`,
);
drawioUrl.searchParams.set("spin", "1");
drawioUrl.searchParams.set("noSaveBtn", "1");
drawioUrl.searchParams.set("noExitBtn", "1");
drawioUrl.searchParams.set("saveAndExit", "0");
const initialXml =
'<mxGraphModel dx="1200" dy="800" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="827" math="0" shadow="0"><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel>';
let componentSchemas = {};
let components = [];
let selectedComponentId = null;
let currentDiagramXml = initialXml;
let drawioReady = false;
let pendingDiagramRequest = null;
let selectedCanApply = false;
let autoRefreshTimeoutId = null;
let autoRefreshNeedsExport = false;
let selectionBridgeInstalled = false;
let selectionBridgeRetryCount = 0;
let selectionSyncInFlight = false;
const focusHighlightPrefix = "__component_focus_";
loadComponentSchemas();
function setStatus(text) {
statusNode.textContent = text;
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function sendToDrawio(message) {
frame.contentWindow.postMessage(JSON.stringify(message), drawioUrl.origin);
}
frame.addEventListener("load", () => {
selectionBridgeInstalled = false;
selectionBridgeRetryCount = 0;
scheduleSelectionBridgeInstall();
});
async function focusComponentInDrawio(componentId) {
if (!drawioReady || !componentId) {
return;
}
const cells = [componentId];
sendToDrawio({ action: "select", cells });
sendToDrawio({
action: "highlight",
cells,
color: "#1d6fb8",
opacity: 80,
duration: 1200,
});
try {
const diagramXml = await requestDiagramXml("定位组件");
const highlightedXml = buildFocusHighlightedXml(diagramXml, componentId);
if (!highlightedXml) {
setStatus("未找到组件图元");
return;
}
sendToDrawio({
action: "load",
xml: highlightedXml,
title: "system.drawio",
autosave: 1,
});
setStatus("已定位组件");
} catch (error) {
console.warn(error);
setStatus("定位组件失败");
}
}
function parseMessage(data) {
if (typeof data !== "string") {
return null;
}
try {
return JSON.parse(data);
} catch {
return null;
}
}
function scheduleSelectionBridgeInstall() {
if (selectionBridgeInstalled || selectionBridgeRetryCount > 20) {
return;
}
window.setTimeout(() => {
selectionBridgeRetryCount += 1;
if (!installSelectionBridge()) {
scheduleSelectionBridgeInstall();
}
}, 250);
}
function installSelectionBridge() {
try {
const frameDocument = frame.contentDocument || frame.contentWindow?.document;
if (!frameDocument || frameDocument.readyState === "loading") {
return false;
}
frameDocument.addEventListener(
"mouseup",
(event) => {
const componentId = componentIdFromDomTarget(event.target);
if (componentId) {
void selectComponentFromCanvas(componentId);
return;
}
window.setTimeout(() => {
const selectedComponentIdFromGraph = componentIdFromDrawioSelection();
if (selectedComponentIdFromGraph) {
void selectComponentFromCanvas(selectedComponentIdFromGraph);
}
}, 80);
},
true,
);
frameDocument.addEventListener(
"keyup",
() => {
const componentId = componentIdFromDrawioSelection();
if (componentId) {
void selectComponentFromCanvas(componentId);
}
},
true,
);
selectionBridgeInstalled = true;
return true;
} catch (error) {
console.warn(error);
return false;
}
}
async function selectComponentFromCanvas(componentId) {
if (!componentId) {
return false;
}
if (
componentId === selectedComponentId &&
components.some((component) => component.id === componentId)
) {
return true;
}
if (!components.some((component) => component.id === componentId)) {
await loadComponentsFromXml(currentDiagramXml);
}
if (!components.some((component) => component.id === componentId)) {
return false;
}
selectedComponentId = componentId;
renderComponentList();
renderParameterForm();
componentListNode
.querySelector(".component-row.active")
?.scrollIntoView({ block: "nearest" });
setStatus("已选择组件");
return true;
}
function isSelectionMessage(message) {
const eventName = String(message.event || "").toLowerCase();
return [
"select",
"selection",
"selected",
"cellselected",
"cellsselected",
"systemsimulationselection",
].includes(eventName);
}
async function handleSelectionMessage(message) {
if (!isSelectionMessage(message)) {
return false;
}
const candidateIds = collectSelectionIds(message);
if (candidateIds.length === 0) {
setStatus("收到选择事件,但没有 cell id");
return true;
}
const componentId = componentIdFromCandidateIds(candidateIds);
if (componentId) {
const selected = await selectComponentFromCanvas(componentId);
if (selected) {
return true;
}
}
setStatus(`收到选择,正在匹配 ${candidateIds.length} 个 id`);
await syncSelectionFromDrawio(candidateIds);
return true;
}
async function syncSelectionFromDrawio(candidateIds) {
if (
!drawioReady ||
selectionSyncInFlight ||
pendingDiagramRequest
) {
setStatus(
!drawioReady
? "收到选择,但 draw.io 尚未就绪"
: "收到选择,等待当前导出完成",
);
return;
}
if (candidateIds.length === 0) {
setStatus("收到选择事件,但没有 cell id");
return;
}
selectionSyncInFlight = true;
try {
const diagramXml = await requestDiagramXml("同步选择");
await loadComponentsFromXml(diagramXml, { selectFirst: false });
const componentId = componentIdFromCandidateIds(candidateIds);
if (componentId) {
await selectComponentFromCanvas(componentId);
} else {
setStatus(`未匹配到组件:${candidateIds.slice(0, 3).join(", ")}`);
}
} catch (error) {
console.warn(error);
setStatus("选择同步失败");
} finally {
selectionSyncInFlight = false;
}
}
function collectSelectionIds(value, ids = []) {
if (value === null || value === undefined) {
return ids;
}
if (typeof value === "string" || typeof value === "number") {
ids.push(String(value));
return ids;
}
if (Array.isArray(value)) {
for (const item of value) {
collectSelectionIds(item, ids);
}
return ids;
}
if (typeof value === "object") {
for (const key of ["id", "parentId", "ancestorId", "cell", "cellId", "value"]) {
if (value[key] !== undefined) {
collectSelectionIds(value[key], ids);
}
}
for (const key of ["cells", "ids", "ancestorIds", "selection", "selected"]) {
if (value[key] !== undefined) {
collectSelectionIds(value[key], ids);
}
}
}
return ids;
}
function componentIdFromDrawioSelection() {
const graph = findDrawioGraph();
if (!graph || typeof graph.getSelectionCells !== "function") {
return null;
}
try {
return componentIdFromCandidateIds(
collectCellIds(graph.getSelectionCells()),
);
} catch (error) {
console.warn(error);
return null;
}
}
function collectCellIds(cells) {
const ids = [];
for (const cell of cells || []) {
if (!cell) {
continue;
}
if (cell.id !== undefined) {
ids.push(String(cell.id));
}
if (typeof cell.getId === "function") {
ids.push(String(cell.getId()));
}
const parent =
typeof cell.getParent === "function" ? cell.getParent() : cell.parent;
if (parent?.id !== undefined) {
ids.push(String(parent.id));
}
if (typeof parent?.getId === "function") {
ids.push(String(parent.getId()));
}
}
return ids;
}
function findDrawioGraph() {
const frameWindow = frame.contentWindow;
if (!frameWindow) {
return null;
}
const directCandidates = [
frameWindow.editorUi,
frameWindow.ui,
frameWindow.app,
frameWindow.App,
frameWindow.graph,
];
for (const candidate of directCandidates) {
const graph = graphFromCandidate(candidate);
if (graph) {
return graph;
}
}
try {
for (const key of Object.keys(frameWindow)) {
if (!/(editor|graph|draw|app|ui)/i.test(key)) {
continue;
}
let candidate = null;
try {
candidate = frameWindow[key];
} catch {
continue;
}
const graph = graphFromCandidate(candidate);
if (graph) {
return graph;
}
}
} catch {
return null;
}
return null;
}
function graphFromCandidate(candidate) {
if (!candidate || typeof candidate !== "object") {
return null;
}
const possibleGraphs = [
candidate,
candidate.graph,
candidate.editor?.graph,
candidate.editorUi?.editor?.graph,
candidate.ui?.editor?.graph,
];
return (
possibleGraphs.find(
(graph) =>
graph &&
typeof graph === "object" &&
typeof graph.getSelectionCells === "function",
) || null
);
}
function componentIdFromDomTarget(target) {
const ids = [];
let element = target;
let depth = 0;
while (element && depth < 10) {
if (typeof element.getAttribute === "function") {
for (const name of ["data-cell-id", "data-id", "id"]) {
const value = element.getAttribute(name);
if (value) {
ids.push(value);
}
}
}
element = element.parentElement;
depth += 1;
}
return componentIdFromCandidateIds(ids);
}
function componentIdFromCandidateIds(ids) {
const idMap = buildCellToComponentMap(currentDiagramXml);
for (const id of ids || []) {
if (!id || id.startsWith(focusHighlightPrefix)) {
continue;
}
const componentId = idMap.get(id);
if (componentId) {
return componentId;
}
}
return null;
}
function buildCellToComponentMap(xmlText) {
const map = new Map();
const documentNode = parseXmlDocument(xmlText);
const graph = documentNode ? graphRootFromDocument(documentNode) : null;
const componentIds = new Set();
for (const component of components) {
componentIds.add(component.id);
map.set(component.id, component.id);
}
if (!graph) {
return map;
}
for (const element of Array.from(graph.root.children)) {
if (element.localName !== "object") {
continue;
}
if (element.hasAttribute("portName")) {
continue;
}
const componentId = element.getAttribute("id");
if (!componentId) {
continue;
}
componentIds.add(componentId);
map.set(componentId, componentId);
}
for (const element of Array.from(graph.root.children)) {
if (element.localName === "object") {
const objectId = element.getAttribute("id");
const cell = Array.from(element.children).find(
(child) => child.localName === "mxCell",
);
const parentId = cell?.getAttribute("parent");
if (objectId && componentIds.has(objectId)) {
map.set(objectId, objectId);
} else if (objectId && parentId && componentIds.has(parentId)) {
map.set(objectId, parentId);
}
const innerCellId = cell?.getAttribute("id");
if (innerCellId && parentId && componentIds.has(parentId)) {
map.set(innerCellId, parentId);
}
}
if (element.localName === "mxCell") {
const cellId = element.getAttribute("id");
const parentId = element.getAttribute("parent");
if (cellId && parentId && componentIds.has(parentId)) {
map.set(cellId, parentId);
}
}
}
return map;
}
function updateControls() {
const busy = Boolean(pendingDiagramRequest);
exportButton.disabled = !drawioReady || busy;
refreshButton.disabled = !drawioReady || busy;
applyButton.disabled =
!drawioReady || busy || !selectedComponentId || !selectedCanApply;
}
function requestDiagramXml(statusText) {
if (!drawioReady) {
return Promise.reject(new Error("draw.io 尚未就绪"));
}
if (pendingDiagramRequest) {
return Promise.reject(new Error("已有导出任务正在执行"));
}
setStatus(statusText);
updateControls();
return new Promise((resolve, reject) => {
const timeoutId = window.setTimeout(() => {
pendingDiagramRequest = null;
updateControls();
reject(new Error("draw.io 导出超时"));
}, 6000);
pendingDiagramRequest = { resolve, reject, timeoutId };
sendToDrawio({ action: "export", format: "xml" });
updateControls();
});
}
window.addEventListener("message", async (event) => {
if (event.origin !== drawioUrl.origin) {
return;
}
const message = parseMessage(event.data);
if (!message) {
return;
}
if (message.event === "systemSimulationPluginReady") {
selectionBridgeInstalled = true;
setStatus("选择联动就绪");
return;
}
if (message.event === "systemSimulationPluginStatus") {
if (message.status === "ready") {
setStatus("选择联动就绪");
} else if (message.status === "prototype-ready") {
setStatus("选择捕获就绪");
} else if (message.status === "registered") {
setStatus("选择插件已注册");
} else if (message.status === "graph-missing") {
setStatus("选择插件未找到画布实例");
} else if (message.status === "waiting-for-draw-loadplugin") {
setStatus("等待 draw.io 插件接口");
}
return;
}
if (isSelectionMessage(message)) {
void handleSelectionMessage(message);
return;
}
if (message.event === "init") {
sendToDrawio({
action: "load",
xml: currentDiagramXml,
title: "system.drawio",
autosave: 1,
});
drawioReady = true;
setStatus("就绪");
updateControls();
scheduleSelectionBridgeInstall();
return;
}
if (message.event === "autosave") {
if (message.xml) {
currentDiagramXml = stripFocusHighlights(message.xml);
}
if (!pendingDiagramRequest) {
setStatus("已更新");
scheduleComponentAutoRefresh(message.xml || null);
}
return;
}
if (message.event === "export" && pendingDiagramRequest) {
window.clearTimeout(pendingDiagramRequest.timeoutId);
const diagramXml = stripFocusHighlights(
message.xml || currentDiagramXml || initialXml,
);
currentDiagramXml = diagramXml;
pendingDiagramRequest.resolve(diagramXml);
pendingDiagramRequest = null;
updateControls();
}
});
refreshButton.addEventListener("click", async () => {
try {
const diagramXml = await requestDiagramXml("读取组件");
await loadComponentsFromXml(diagramXml);
setStatus("组件已刷新");
} catch (error) {
handleError(error, "刷新失败");
}
});
exportButton.addEventListener("click", async () => {
try {
const diagramXml = await requestDiagramXml("生成 XML");
await exportSystemXml(diagramXml);
setStatus("已生成");
} catch (error) {
handleError(error, "生成失败");
}
});
parameterForm.addEventListener("submit", async (event) => {
event.preventDefault();
await applySelectedParameters();
});
async function loadComponentSchemas() {
try {
const response = await fetch("/api/component-schemas");
if (!response.ok) {
throw new Error(await readResponseError(response));
}
componentSchemas = await response.json();
} catch (error) {
handleError(error, "参数模板加载失败");
}
}
async function loadComponentsFromXml(diagramXml, options = {}) {
const { selectFirst = true } = options;
const response = await fetch("/api/diagram-components", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ xml: diagramXml }),
});
if (!response.ok) {
throw new Error(await readResponseError(response));
}
const data = await response.json();
components = data.components || [];
if (!components.some((component) => component.id === selectedComponentId)) {
selectedComponentId = selectFirst ? components[0]?.id || null : null;
}
renderComponentList();
renderParameterForm();
}
function scheduleComponentAutoRefresh(diagramXml) {
if (arguments.length > 0) {
if (diagramXml) {
currentDiagramXml = diagramXml;
} else {
autoRefreshNeedsExport = true;
}
}
if (!drawioReady) {
return;
}
if (autoRefreshTimeoutId !== null) {
window.clearTimeout(autoRefreshTimeoutId);
}
autoRefreshTimeoutId = window.setTimeout(() => {
autoRefreshTimeoutId = null;
void autoRefreshComponents();
}, 800);
}
async function autoRefreshComponents() {
if (!drawioReady) {
return;
}
if (pendingDiagramRequest || isEditingParameters()) {
scheduleComponentAutoRefresh();
return;
}
try {
let diagramXml = currentDiagramXml;
if (autoRefreshNeedsExport || !diagramXml) {
diagramXml = await requestDiagramXml("同步组件");
autoRefreshNeedsExport = false;
}
await loadComponentsFromXml(diagramXml);
setStatus("组件已同步");
} catch (error) {
handleError(error, "同步失败");
}
}
function isEditingParameters() {
return parameterForm.contains(document.activeElement);
}
async function applySelectedParameters() {
const component = selectedComponent();
if (!component) {
return;
}
const schema = schemaForComponent(component);
const parameterNames = Object.keys(schema.parameters || {});
const formData = new FormData(parameterForm);
const parameters = {};
for (const name of parameterNames) {
parameters[name] = formData.get(name);
}
try {
const diagramXml = await requestDiagramXml("应用参数");
const response = await fetch("/api/update-component-parameters", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
xml: diagramXml,
component_id: component.id,
name: componentNameInput.value,
parameters,
}),
});
if (!response.ok) {
throw new Error(await readResponseError(response));
}
currentDiagramXml = await response.text();
sendToDrawio({
action: "load",
xml: currentDiagramXml,
title: "system.drawio",
autosave: 1,
});
await loadComponentsFromXml(currentDiagramXml);
setStatus("参数已应用");
} catch (error) {
handleError(error, "应用失败");
}
}
async function exportSystemXml(diagramXml) {
const response = await fetch("/api/system-xml", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ xml: diagramXml }),
});
if (!response.ok) {
throw new Error(await readResponseError(response));
}
const systemXml = await response.text();
downloadText(systemXml, "system.xml", "application/xml");
}
function renderComponentList() {
componentListNode.textContent = "";
if (components.length === 0) {
const emptyNode = document.createElement("div");
emptyNode.className = "empty-state";
emptyNode.textContent = "暂无组件";
componentListNode.append(emptyNode);
return;
}
for (const component of components) {
const row = document.createElement("button");
row.type = "button";
row.className = "component-row";
if (component.id === selectedComponentId) {
row.classList.add("active");
}
row.addEventListener("click", () => {
selectedComponentId = component.id;
renderComponentList();
renderParameterForm();
void focusComponentInDrawio(component.id);
});
const name = document.createElement("div");
name.className = "component-name";
name.textContent = displayNameForComponent(component);
const meta = document.createElement("div");
meta.className = "component-meta";
meta.textContent = `${schemaKeyForComponent(component)} · ${component.id}`;
row.append(name, meta);
componentListNode.append(row);
}
}
function renderParameterForm() {
fieldGridNode.textContent = "";
const component = selectedComponent();
if (!component) {
selectedCanApply = false;
componentNameInput.value = "";
componentNameInput.disabled = true;
selectedHeadingNode.textContent = "未选择组件";
selectedMetaNode.textContent = "";
updateControls();
return;
}
const schema = schemaForComponent(component);
const parameters = schema.parameters || {};
const displayName = displayNameForComponent(component);
selectedCanApply = true;
componentNameInput.disabled = false;
componentNameInput.value = displayName;
selectedHeadingNode.textContent = displayName;
selectedMetaNode.textContent = `${schemaKeyForComponent(component)} · ${component.id}`;
const entries = Object.entries(parameters);
if (entries.length === 0) {
const emptyNode = document.createElement("div");
emptyNode.className = "empty-state";
emptyNode.textContent = "该组件暂无可调参数";
fieldGridNode.append(emptyNode);
updateControls();
return;
}
for (const [name, definition] of entries) {
fieldGridNode.append(createParameterField(component, name, definition));
}
updateControls();
}
function createParameterField(component, name, definition) {
const field = document.createElement("div");
field.className = "field";
const label = document.createElement("label");
label.htmlFor = `param-${name}`;
const labelText = document.createElement("span");
labelText.textContent = definition.label || name;
const key = document.createElement("span");
key.className = "field-key";
key.textContent = name;
label.append(labelText, key);
const row = document.createElement("div");
row.className = "input-row";
const input = document.createElement("input");
input.id = `param-${name}`;
input.name = name;
input.type =
definition.type === "float" || definition.type === "int" ? "number" : "text";
input.step = definition.type === "int" ? "1" : "any";
if (definition.min !== undefined) {
input.min = definition.min;
}
if (definition.max !== undefined) {
input.max = definition.max;
}
input.value =
component.properties?.[name] ?? definition.default ?? "";
const unit = document.createElement("span");
unit.className = "unit";
unit.textContent = definition.unit || "";
row.append(input, unit);
field.append(label, row);
return field;
}
function selectedComponent() {
return components.find((component) => component.id === selectedComponentId);
}
function schemaKeyForComponent(component) {
return component.properties?.modelType || component.type || "component";
}
function schemaForComponent(component) {
const key = schemaKeyForComponent(component);
return (
componentSchemas[key] ||
componentSchemas[component.type] || {
label: component.label || component.type || "组件",
parameters: {},
}
);
}
function buildFocusHighlightedXml(xmlText, componentId) {
const documentNode = parseXmlDocument(xmlText);
if (!documentNode) {
return null;
}
removeFocusHighlights(documentNode);
const graph = graphRootFromDocument(documentNode);
if (!graph) {
return null;
}
const component = Array.from(graph.root.children).find(
(element) =>
element.localName === "object" &&
element.getAttribute("id") === componentId,
);
if (!component) {
return null;
}
const cell = Array.from(component.children).find(
(element) => element.localName === "mxCell",
);
const geometry = cell
? Array.from(cell.children).find(
(element) => element.localName === "mxGeometry",
)
: null;
if (!cell || !geometry) {
return null;
}
const x = Number(geometry.getAttribute("x") || 0);
const y = Number(geometry.getAttribute("y") || 0);
const width = Number(geometry.getAttribute("width") || 0);
const height = Number(geometry.getAttribute("height") || 0);
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
return null;
}
const inset = 8;
const highlight = documentNode.createElement("mxCell");
highlight.setAttribute("id", `${focusHighlightPrefix}${componentId}`);
highlight.setAttribute("value", "");
highlight.setAttribute(
"style",
"rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#1d6fb8;strokeWidth=3;dashed=1;dashPattern=8 4;pointerEvents=0;editable=0;movable=0;resizable=0;rotatable=0;deletable=0;connectable=0;",
);
highlight.setAttribute("vertex", "1");
highlight.setAttribute("connectable", "0");
highlight.setAttribute("parent", cell.getAttribute("parent") || "1");
const highlightGeometry = documentNode.createElement("mxGeometry");
highlightGeometry.setAttribute("x", String(x - inset));
highlightGeometry.setAttribute("y", String(y - inset));
highlightGeometry.setAttribute("width", String(width + inset * 2));
highlightGeometry.setAttribute("height", String(height + inset * 2));
highlightGeometry.setAttribute("as", "geometry");
highlight.append(highlightGeometry);
graph.root.append(highlight);
return serializeXmlDocument(documentNode);
}
function stripFocusHighlights(xmlText) {
const documentNode = parseXmlDocument(xmlText);
if (!documentNode) {
return xmlText;
}
removeFocusHighlights(documentNode);
return serializeXmlDocument(documentNode);
}
function parseXmlDocument(xmlText) {
const documentNode = new DOMParser().parseFromString(
xmlText,
"application/xml",
);
if (documentNode.getElementsByTagName("parsererror").length > 0) {
return null;
}
return documentNode;
}
function serializeXmlDocument(documentNode) {
return new XMLSerializer().serializeToString(documentNode);
}
function graphRootFromDocument(documentNode) {
const model =
documentNode.documentElement.localName === "mxGraphModel"
? documentNode.documentElement
: Array.from(documentNode.getElementsByTagName("mxGraphModel"))[0];
if (!model) {
return null;
}
const root = Array.from(model.children).find(
(element) => element.localName === "root",
);
if (!root) {
return null;
}
return { model, root };
}
function removeFocusHighlights(documentNode) {
for (const cell of Array.from(documentNode.getElementsByTagName("mxCell"))) {
if (cell.getAttribute("id")?.startsWith(focusHighlightPrefix)) {
cell.remove();
}
}
}
function displayNameForComponent(component) {
return (
component.properties?.name ||
component.label ||
schemaForComponent(component).label ||
component.type ||
"组件"
);
}
async function readResponseError(response) {
const text = await response.text();
try {
const payload = JSON.parse(text);
return payload.detail || text;
} catch {
return text || response.statusText;
}
}
function handleError(error, fallback) {
console.error(error);
setStatus(error?.message || fallback);
updateControls();
}
function downloadText(text, filename, mimeType) {
const blob = new Blob([text], { type: mimeType });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
}
window.setTimeout(() => {
frame.src = drawioUrl.toString();
}, 0);
</script>
</body>
</html>