添加docker版drawio配置

This commit is contained in:
lujingze committed 2026-07-03 09:23:24 +00:00
1 parent a5d9198bba
commit f73806cfb0
1297 files changed
+240603

No files matched your search

+1
View File
@@ -0,0 +1 @@
+193
View File
@@ -0,0 +1,193 @@
from __future__ import annotations
import base64
import json
import os
import zlib
from pathlib import Path
from typing import Iterable
from urllib.parse import unquote
from xml.etree import ElementTree as ET
from fastapi import FastAPI, HTTPException, Request, Response
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
app = FastAPI(title="System Simulation App")
INDEX_TEMPLATE = Path(__file__).parent / "static" / "index.html"
class DiagramPayload(BaseModel):
xml: str
@app.get("/")
def index(request: Request) -> HTMLResponse:
html = INDEX_TEMPLATE.read_text(encoding="utf-8")
drawio_base_url = os.getenv("DRAWIO_BASE_URL") or default_drawio_base_url(request)
html = html.replace("__DRAWIO_BASE_URL__", json.dumps(drawio_base_url))
return HTMLResponse(html)
@app.post("/api/system-xml")
def export_system_xml(payload: DiagramPayload) -> Response:
try:
model = read_graph_model(payload.xml)
system_xml = build_system_xml(model)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return Response(content=system_xml, media_type="application/xml")
def default_drawio_base_url(request: Request) -> str:
hostname = request.url.hostname or "127.0.0.1"
if ":" in hostname and not hostname.startswith("["):
hostname = f"[{hostname}]"
return f"{request.url.scheme}://{hostname}:8080/"
def read_graph_model(xml_text: str) -> ET.Element:
xml_text = xml_text.strip()
if not xml_text:
raise ValueError("Diagram XML is empty.")
try:
root = ET.fromstring(xml_text)
except ET.ParseError as exc:
raise ValueError("Diagram XML is invalid.") from exc
if tag_name(root) == "mxGraphModel":
return root
if tag_name(root) != "mxfile":
raise ValueError("Expected mxGraphModel or mxfile XML.")
diagram = next((child for child in root if tag_name(child) == "diagram"), None)
if diagram is None:
raise ValueError("mxfile does not contain a diagram.")
embedded_model = next(
(child for child in diagram if tag_name(child) == "mxGraphModel"),
None,
)
if embedded_model is not None:
return embedded_model
if not diagram.text:
raise ValueError("diagram node is empty.")
return decode_diagram_payload(diagram.text.strip())
def decode_diagram_payload(payload: str) -> ET.Element:
if payload.startswith("<"):
return parse_graph_model(payload)
if payload.startswith("%3C"):
return parse_graph_model(unquote(payload))
try:
padded = payload + ("=" * (-len(payload) % 4))
compressed = base64.b64decode(padded)
try:
decoded = zlib.decompress(compressed, -15)
except zlib.error:
decoded = zlib.decompress(compressed)
return parse_graph_model(unquote(decoded.decode("utf-8")))
except Exception as exc: # noqa: BLE001 - keep API error small and stable.
raise ValueError("Unable to decode draw.io diagram payload.") from exc
def parse_graph_model(xml_text: str) -> ET.Element:
try:
model = ET.fromstring(xml_text)
except ET.ParseError as exc:
raise ValueError("Decoded diagram XML is invalid.") from exc
if tag_name(model) != "mxGraphModel":
raise ValueError("Decoded diagram is not an mxGraphModel.")
return model
def build_system_xml(model: ET.Element) -> bytes:
root = next((child for child in model if tag_name(child) == "root"), None)
if root is None:
raise ValueError("mxGraphModel does not contain a root node.")
system = ET.Element("System")
components_node = ET.SubElement(system, "Components")
connections_node = ET.SubElement(system, "Connections")
for component in read_components(root):
component_node = ET.SubElement(
components_node,
"Component",
{
"id": component["id"],
"type": component["type"],
"label": component["label"],
},
)
for name, value in component["properties"].items():
ET.SubElement(
component_node,
"Property",
{"name": name, "value": value},
)
for edge in read_connections(root):
ET.SubElement(connections_node, "Connection", edge)
ET.indent(system, space=" ")
return ET.tostring(system, encoding="utf-8", xml_declaration=True)
def read_components(root: ET.Element) -> Iterable[dict]:
for element in root:
if tag_name(element) != "object":
continue
cell = next((child for child in element if tag_name(child) == "mxCell"), None)
if cell is None or cell.attrib.get("vertex") != "1":
continue
component_id = element.attrib.get("id") or cell.attrib.get("id")
if not component_id:
continue
properties = {
key: value
for key, value in element.attrib.items()
if key not in {"id", "label", "componentType"}
}
yield {
"id": component_id,
"type": element.attrib.get("componentType", "component"),
"label": element.attrib.get("label", ""),
"properties": properties,
}
def read_connections(root: ET.Element) -> Iterable[dict[str, str]]:
for cell in root.iter():
if tag_name(cell) != "mxCell" or cell.attrib.get("edge") != "1":
continue
source = cell.attrib.get("source")
target = cell.attrib.get("target")
if not source or not target:
continue
edge = {"source": source, "target": target}
if "id" in cell.attrib:
edge["id"] = cell.attrib["id"]
yield edge
def tag_name(element: ET.Element) -> str:
return element.tag.rsplit("}", 1)[-1]
+272
View File
@@ -0,0 +1,272 @@
<!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: #18202a;
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
background: #f6f8fb;
}
.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 #d8dee8;
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: 10px;
}
.status {
max-width: 220px;
overflow: hidden;
color: #5d6877;
font-size: 13px;
text-overflow: ellipsis;
white-space: nowrap;
}
button {
height: 32px;
padding: 0 12px;
border: 1px solid #1b6fb8;
border-radius: 6px;
color: #ffffff;
font: inherit;
font-size: 14px;
background: #1b6fb8;
cursor: pointer;
}
button:disabled {
border-color: #98a2af;
background: #98a2af;
cursor: wait;
}
iframe {
display: block;
width: 100%;
height: 100%;
border: 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="export-button" type="button" disabled>生成 XML</button>
</div>
</header>
<iframe id="drawio-frame" title="draw.io"></iframe>
</main>
<script>
const DRAWIO_BASE_URL = __DRAWIO_BASE_URL__;
const frame = document.getElementById("drawio-frame");
const exportButton = document.getElementById("export-button");
const statusNode = document.getElementById("status");
const drawioUrl = new URL(DRAWIO_BASE_URL, window.location.href);
drawioUrl.searchParams.set("embed", "1");
drawioUrl.searchParams.set("proto", "json");
drawioUrl.searchParams.set("configure", "1");
drawioUrl.searchParams.set("libraries", "1");
drawioUrl.searchParams.set("spin", "1");
drawioUrl.searchParams.set("noSaveBtn", "1");
drawioUrl.searchParams.set("noExitBtn", "1");
drawioUrl.searchParams.set("saveAndExit", "0");
frame.src = drawioUrl.toString();
let pendingExport = false;
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>';
const componentLibrary = [
{
title: "贮箱",
w: 80,
h: 80,
xml:
'<mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/><object id="tank" label="贮箱" componentType="tank" medium="N2" volume="1.0"><mxCell style="shape=cylinder;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry width="80" height="80" as="geometry"/></mxCell></object></root></mxGraphModel>',
},
{
title: "阀门",
w: 72,
h: 56,
xml:
'<mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/><object id="valve" label="阀门" componentType="valve" valveType="manual" nominalDiameter="10"><mxCell style="rhombus;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="1"><mxGeometry width="72" height="56" as="geometry"/></mxCell></object></root></mxGraphModel>',
},
{
title: "管段",
w: 96,
h: 36,
xml:
'<mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/><object id="pipe" label="管段" componentType="pipe" length="1.0" diameter="0.01"><mxCell style="rounded=1;whiteSpace=wrap;html=1;arcSize=12;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry width="96" height="36" as="geometry"/></mxCell></object></root></mxGraphModel>',
},
];
function setStatus(text) {
statusNode.textContent = text;
}
function sendToDrawio(message) {
frame.contentWindow.postMessage(JSON.stringify(message), drawioUrl.origin);
}
function parseMessage(data) {
if (typeof data !== "string") {
return null;
}
try {
return JSON.parse(data);
} catch {
return null;
}
}
window.addEventListener("message", async (event) => {
if (event.origin !== drawioUrl.origin) {
return;
}
const message = parseMessage(event.data);
if (!message) {
return;
}
if (message.event === "configure") {
sendToDrawio({
action: "configure",
config: {
defaultLibraries: "system-components",
enabledLibraries: [],
enableCustomLibraries: false,
libraries: [
{
title: { main: "System" },
entries: [
{
id: "system-components",
title: { main: "Components" },
libs: [
{
title: { main: "Components" },
data: componentLibrary,
},
],
},
],
},
],
},
});
return;
}
if (message.event === "init") {
sendToDrawio({
action: "load",
xml: initialXml,
title: "system.drawio",
autosave: 1,
});
exportButton.disabled = false;
setStatus("已就绪");
return;
}
if (message.event === "autosave") {
setStatus("已更新");
return;
}
if (message.event === "save" && pendingExport) {
pendingExport = false;
await exportSystemXml(message.xml);
}
});
exportButton.addEventListener("click", () => {
pendingExport = true;
exportButton.disabled = true;
setStatus("生成中");
sendToDrawio({ action: "save" });
});
async function exportSystemXml(diagramXml) {
try {
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 response.text());
}
const systemXml = await response.text();
downloadText(systemXml, "system.xml", "application/xml");
setStatus("已生成");
} catch (error) {
console.error(error);
setStatus("生成失败");
} finally {
exportButton.disabled = false;
}
}
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);
}
</script>
</body>
</html>