添加docker版drawio配置
This commit is contained in:
1 parent
a5d9198bba
commit
f73806cfb0
1297 files changed
+240603
No files matched your search
@@ -0,0 +1 @@
|
||||
|
||||
+193
@@ -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]
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user