网页内docker版drawio内嵌和xml生成
This commit is contained in:
1 parent
f73806cfb0
commit
6b7be41e02
2036 files changed
+373597
-63
No files matched your search
Binary file not shown.
Binary file not shown.
+141
-4
@@ -3,10 +3,12 @@ from __future__ import annotations
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
from urllib.parse import unquote
|
||||
from urllib.parse import unquote, urljoin
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Request, Response
|
||||
@@ -16,6 +18,54 @@ from pydantic import BaseModel
|
||||
|
||||
app = FastAPI(title="System Simulation App")
|
||||
INDEX_TEMPLATE = Path(__file__).parent / "static" / "index.html"
|
||||
DRAWIO_UPSTREAM_URL = (
|
||||
os.getenv("DRAWIO_UPSTREAM_URL")
|
||||
or os.getenv("DRAWIO_BASE_URL")
|
||||
or "http://127.0.0.1:8081/"
|
||||
)
|
||||
COMPONENT_LIBRARY = [
|
||||
{
|
||||
"title": "\u8d2e\u7bb1",
|
||||
"w": 80,
|
||||
"h": 80,
|
||||
"xml": (
|
||||
'<mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/>'
|
||||
'<object id="tank" label="\u8d2e\u7bb1" 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": "\u9600\u95e8",
|
||||
"w": 72,
|
||||
"h": 56,
|
||||
"xml": (
|
||||
'<mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/>'
|
||||
'<object id="valve" label="\u9600\u95e8" 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": "\u7ba1\u6bb5",
|
||||
"w": 96,
|
||||
"h": 36,
|
||||
"xml": (
|
||||
'<mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/>'
|
||||
'<object id="pipe" label="\u7ba1\u6bb5" 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>"
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class DiagramPayload(BaseModel):
|
||||
@@ -25,9 +75,68 @@ class DiagramPayload(BaseModel):
|
||||
@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)
|
||||
drawio_base_url = default_drawio_base_url(request)
|
||||
html = html.replace("__DRAWIO_BASE_URL__", json.dumps(drawio_base_url))
|
||||
return HTMLResponse(html)
|
||||
return HTMLResponse(html, headers={"Cache-Control": "no-store"})
|
||||
|
||||
|
||||
@app.get("/api/component-library.drawiolib")
|
||||
def component_library() -> Response:
|
||||
library = ET.Element("mxlibrary", {"title": "System Components"})
|
||||
library.text = json.dumps(COMPONENT_LIBRARY, ensure_ascii=False)
|
||||
return Response(
|
||||
content=ET.tostring(library, encoding="utf-8"),
|
||||
media_type="application/xml",
|
||||
headers={
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@app.api_route("/drawio", methods=["GET", "POST", "HEAD", "OPTIONS"])
|
||||
@app.api_route("/drawio/{path:path}", methods=["GET", "POST", "HEAD", "OPTIONS"])
|
||||
async def drawio_proxy(request: Request, path: str = "") -> Response:
|
||||
upstream_url = build_drawio_upstream_url(path, request.url.query)
|
||||
body = await request.body()
|
||||
headers = {
|
||||
key: value
|
||||
for key, value in request.headers.items()
|
||||
if key.lower()
|
||||
not in {
|
||||
"host",
|
||||
"connection",
|
||||
"content-length",
|
||||
"accept-encoding",
|
||||
}
|
||||
}
|
||||
|
||||
upstream_request = urllib.request.Request(
|
||||
upstream_url,
|
||||
data=body if body else None,
|
||||
headers=headers,
|
||||
method=request.method,
|
||||
)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(upstream_request, timeout=30) as upstream_response:
|
||||
content = upstream_response.read()
|
||||
return Response(
|
||||
content=content,
|
||||
status_code=upstream_response.status,
|
||||
headers=filter_proxy_headers(upstream_response.headers),
|
||||
)
|
||||
except urllib.error.HTTPError as exc:
|
||||
return Response(
|
||||
content=exc.read(),
|
||||
status_code=exc.code,
|
||||
headers=filter_proxy_headers(exc.headers),
|
||||
)
|
||||
except urllib.error.URLError as exc:
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail=f"Unable to reach draw.io upstream: {exc.reason}",
|
||||
) from exc
|
||||
|
||||
|
||||
@app.post("/api/system-xml")
|
||||
@@ -46,7 +155,35 @@ def default_drawio_base_url(request: Request) -> str:
|
||||
if ":" in hostname and not hostname.startswith("["):
|
||||
hostname = f"[{hostname}]"
|
||||
|
||||
return f"{request.url.scheme}://{hostname}:8080/"
|
||||
port = f":{request.url.port}" if request.url.port else ""
|
||||
return f"{request.url.scheme}://{hostname}{port}/drawio/"
|
||||
|
||||
|
||||
def build_drawio_upstream_url(path: str, query: str) -> str:
|
||||
target = urljoin(DRAWIO_UPSTREAM_URL, path)
|
||||
if query:
|
||||
target = f"{target}?{query}"
|
||||
return target
|
||||
|
||||
|
||||
def filter_proxy_headers(headers) -> dict[str, str]:
|
||||
excluded = {
|
||||
"connection",
|
||||
"content-encoding",
|
||||
"content-length",
|
||||
"keep-alive",
|
||||
"proxy-authenticate",
|
||||
"proxy-authorization",
|
||||
"te",
|
||||
"trailer",
|
||||
"transfer-encoding",
|
||||
"upgrade",
|
||||
}
|
||||
return {
|
||||
key: value
|
||||
for key, value in headers.items()
|
||||
if key.lower() not in excluded
|
||||
}
|
||||
|
||||
|
||||
def read_graph_model(xml_text: str) -> ET.Element:
|
||||
|
||||
+52
-59
@@ -107,10 +107,19 @@
|
||||
const exportButton = document.getElementById("export-button");
|
||||
const statusNode = document.getElementById("status");
|
||||
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("configure", "1");
|
||||
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");
|
||||
@@ -118,38 +127,19 @@
|
||||
frame.src = drawioUrl.toString();
|
||||
|
||||
let pendingExport = false;
|
||||
let exportTimeoutId = null;
|
||||
|
||||
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 escapeRegExp(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function sendToDrawio(message) {
|
||||
frame.contentWindow.postMessage(JSON.stringify(message), drawioUrl.origin);
|
||||
}
|
||||
@@ -166,6 +156,32 @@
|
||||
}
|
||||
}
|
||||
|
||||
function clearExportTimeout() {
|
||||
if (exportTimeoutId !== null) {
|
||||
window.clearTimeout(exportTimeoutId);
|
||||
exportTimeoutId = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function finishExport(diagramXml) {
|
||||
clearExportTimeout();
|
||||
pendingExport = false;
|
||||
await exportSystemXml(diagramXml || initialXml);
|
||||
}
|
||||
|
||||
function scheduleExportTimeout() {
|
||||
clearExportTimeout();
|
||||
exportTimeoutId = window.setTimeout(() => {
|
||||
if (!pendingExport) {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingExport = false;
|
||||
exportButton.disabled = false;
|
||||
setStatus("生成超时");
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
window.addEventListener("message", async (event) => {
|
||||
if (event.origin !== drawioUrl.origin) {
|
||||
return;
|
||||
@@ -176,35 +192,6 @@
|
||||
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",
|
||||
@@ -218,21 +205,27 @@
|
||||
}
|
||||
|
||||
if (message.event === "autosave") {
|
||||
setStatus("已更新");
|
||||
if (!pendingExport) {
|
||||
setStatus("已更新");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.event === "save" && pendingExport) {
|
||||
pendingExport = false;
|
||||
await exportSystemXml(message.xml);
|
||||
if (message.event === "export" && pendingExport) {
|
||||
await finishExport(message.xml);
|
||||
}
|
||||
});
|
||||
|
||||
exportButton.addEventListener("click", () => {
|
||||
if (pendingExport) {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingExport = true;
|
||||
exportButton.disabled = true;
|
||||
setStatus("生成中");
|
||||
sendToDrawio({ action: "save" });
|
||||
sendToDrawio({ action: "export", format: "xml" });
|
||||
scheduleExportTimeout();
|
||||
});
|
||||
|
||||
async function exportSystemXml(diagramXml) {
|
||||
|
||||
Reference in new issue
Block a user