diff --git a/data/wiki/guanghan_topology_demo.html b/data/wiki/guanghan_topology_demo.html index da0d4d5..0fa4df0 100644 --- a/data/wiki/guanghan_topology_demo.html +++ b/data/wiki/guanghan_topology_demo.html @@ -3,243 +3,83 @@ -广寒基地拓扑结构 · GHC-05 任务后 +广寒基地拓扑结构 @@ -248,174 +88,152 @@
-
+
+ + + + + + +
+ +
结构直连 加压走道 燃料 / 供电 大型折叠散热面板
-
- - 广寒基地 GHC-05 任务后拓扑 +
正在从 API 加载拓扑数据...
+ + + -

模块与车辆支持详情查看;走道、管线和散热面板仅用于表达拓扑关系。

@@ -437,217 +255,289 @@ diff --git a/topology_api/app.py b/topology_api/app.py index 43d3db7..855a507 100644 --- a/topology_api/app.py +++ b/topology_api/app.py @@ -1,11 +1,13 @@ # topology_api/app.py from flask import Flask +from flask_cors import CORS from topology_api.config import API_HOST, API_PORT, API_DEBUG from topology_api.routes.snapshot import snapshot_bp def create_app(): app = Flask(__name__) + CORS(app) app.register_blueprint(snapshot_bp) @app.route("/health") diff --git a/topology_api/migrations/004_layout.sql b/topology_api/migrations/004_layout.sql new file mode 100644 index 0000000..1d27899 --- /dev/null +++ b/topology_api/migrations/004_layout.sql @@ -0,0 +1,34 @@ +-- topology_api/migrations/004_layout.sql +-- Diagram layout tables for topology visualization + +CREATE TABLE topology.diagram_layouts ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + base_id uuid NOT NULL REFERENCES topology.bases(id), + layout_key text NOT NULL, + name text NOT NULL, + canvas_width numeric NOT NULL DEFAULT 1500, + canvas_height numeric NOT NULL DEFAULT 1900, + is_default boolean NOT NULL DEFAULT false, + created_at timestamptz NOT NULL DEFAULT now(), + CONSTRAINT uq_layout_base_key UNIQUE (base_id, layout_key) +); + +CREATE TABLE topology.diagram_nodes ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + layout_id uuid NOT NULL REFERENCES topology.diagram_layouts(id), + component_id uuid REFERENCES topology.components(id), + node_key text NOT NULL, + x numeric NOT NULL, + y numeric NOT NULL, + width numeric NOT NULL DEFAULT 200, + height numeric NOT NULL DEFAULT 120, + rotation_deg numeric NOT NULL DEFAULT 0, + style_key text NOT NULL DEFAULT 'node-card', + label_override text, + z_index integer NOT NULL DEFAULT 0, + created_at timestamptz NOT NULL DEFAULT now(), + CONSTRAINT uq_diagram_node UNIQUE (layout_id, node_key) +); + +CREATE INDEX IF NOT EXISTS idx_diagram_nodes_layout + ON topology.diagram_nodes(layout_id); diff --git a/topology_api/queries/snapshot_queries.py b/topology_api/queries/snapshot_queries.py index 5366dba..3ae946b 100644 --- a/topology_api/queries/snapshot_queries.py +++ b/topology_api/queries/snapshot_queries.py @@ -201,3 +201,48 @@ def fetch_snapshot(snapshot_at): }) return result + + +def fetch_layout(): + """Fetch the default diagram layout with node positions.""" + with read_cursor() as cur: + cur.execute(""" + SELECT + dl.layout_key, + dl.name AS layout_name, + dl.canvas_width, + dl.canvas_height, + dn.node_key, + dn.component_id, + c.component_key, + dn.x, dn.y, dn.width, dn.height, + dn.style_key, dn.label_override, dn.z_index + FROM diagram_layouts dl + JOIN diagram_nodes dn ON dn.layout_id = dl.id + LEFT JOIN components c ON c.id = dn.component_id + WHERE dl.base_id = (SELECT id FROM bases WHERE code = 'guanghan') + AND dl.is_default = true + ORDER BY dn.z_index, dn.node_key + """) + nodes = [] + layout_info = None + for row in cur.fetchall(): + if layout_info is None: + layout_info = { + "key": row["layout_key"], + "name": row["layout_name"], + "canvasWidth": float(row["canvas_width"]), + "canvasHeight": float(row["canvas_height"]), + } + nodes.append({ + "nodeKey": row["node_key"], + "componentKey": row["component_key"], + "x": float(row["x"]), + "y": float(row["y"]), + "width": float(row["width"]), + "height": float(row["height"]), + "styleKey": row["style_key"], + "labelOverride": row["label_override"], + "zIndex": row["z_index"], + }) + return {"layout": layout_info, "nodes": nodes} if layout_info else None diff --git a/topology_api/routes/snapshot.py b/topology_api/routes/snapshot.py index 85674bc..5a2d474 100644 --- a/topology_api/routes/snapshot.py +++ b/topology_api/routes/snapshot.py @@ -1,6 +1,8 @@ # topology_api/routes/snapshot.py from flask import Blueprint, request, jsonify -from topology_api.queries.snapshot_queries import resolve_snapshot_time, fetch_snapshot +from topology_api.queries.snapshot_queries import ( + resolve_snapshot_time, fetch_snapshot, fetch_layout, +) snapshot_bp = Blueprint("snapshot", __name__) @@ -47,6 +49,7 @@ def get_snapshot(base_code): }) data = fetch_snapshot(snapshot_at) + layout = fetch_layout() return jsonify({ "base": data["base"], @@ -58,4 +61,5 @@ def get_snapshot(base_code): }, "components": data["components"], "connections": data["connections"], + "layout": layout, }) diff --git a/topology_api/seed/seed_layout.py b/topology_api/seed/seed_layout.py new file mode 100644 index 0000000..95096f2 --- /dev/null +++ b/topology_api/seed/seed_layout.py @@ -0,0 +1,102 @@ +# topology_api/seed/seed_layout.py +"""Seed diagram layout and node positions for the engineering overview SVG.""" +import json +from topology_api.db import write_cursor + + +def seed(): + with write_cursor() as cur: + # Get base + cur.execute("SELECT id FROM bases WHERE code = 'guanghan'") + base_id = cur.fetchone()["id"] + + # Upsert layout + cur.execute(""" + INSERT INTO diagram_layouts (id, base_id, layout_key, name, + canvas_width, canvas_height, is_default) + VALUES (gen_random_uuid(), %(base)s, 'engineering_overview_v1', + '工程总图 v1', 1500, 1900, true) + ON CONFLICT (base_id, layout_key) DO UPDATE + SET name = EXCLUDED.name, is_default = true + RETURNING id + """, {"base": base_id}) + layout_id = cur.fetchone()["id"] + print(f"Layout: {layout_id}") + + # Component key → component_id mapping + cur.execute("SELECT component_key, id FROM components WHERE base_id = %(base)s", + {"base": base_id}) + comp_map = {r["component_key"]: r["id"] for r in cur.fetchall()} + + # SVG node positions extracted from guanghan_topology_demo.html + # node_key → (x, y, width, height, style_key, label_override, z_index) + nodes = [ + # Core cross + ("core_cabin", comp_map.get("core_cabin"), 630, 470, 240, 200, + "core-card", None, 10), + ("habitat_2", comp_map.get("habitat_2"), 630, 220, 240, 140, + "node-card", None, 5), + ("habitat_1", comp_map.get("habitat_1"), 630, 760, 240, 120, + "node-card", None, 5), + ("greenhouse", comp_map.get("greenhouse"), 330, 490, 220, 160, + "node-card", None, 5), + ("lab_module", comp_map.get("lab_module"), 950, 490, 220, 160, + "node-card", None, 5), + ("wugang_2", comp_map.get("wugang_2"), 40, 495, 210, 150, + "vehicle-card", None, 3), + + # Power cross + ("power_module_hub", comp_map.get("power_module_hub"), 650, 1260, 200, 180, + "power-node-card", None, 8), + ("power_storage_rack", comp_map.get("power_storage_rack"), 600, 990, 300, 160, + "power-card", "北部模块", 5), + ("nuclear_reactor", comp_map.get("nuclear_reactor"), 620, 1540, 260, 150, + "power-card", "南部模块", 5), + + # Power west/east groups (multiple DB components per SVG node) + # power-west groups: isru_module, mining_module, ore_fuel_storage + ("power_west_group", None, 280, 1250, 260, 200, + "power-card", "西部模块", 5), + # power-east groups: fuel_storage_mod, centrifuge_mod + ("power_east_group", None, 960, 1250, 260, 200, + "power-card", "东部模块", 5), + + # Bingluns + ("binglun_1", comp_map.get("binglun_1"), 30, 1280, 210, 140, + "vehicle-card", None, 2), + ("binglun_2", comp_map.get("binglun_2"), 645, 1740, 210, 120, + "vehicle-card", None, 2), + ("binglun_3", comp_map.get("binglun_3"), 1260, 1280, 210, 140, + "vehicle-card", None, 2), + ] + + # Delete existing nodes and re-insert + cur.execute("DELETE FROM diagram_nodes WHERE layout_id = %(lid)s", + {"lid": layout_id}) + + for node_key, comp_id, x, y, w, h, style, label, z in nodes: + cur.execute(""" + INSERT INTO diagram_nodes + (id, layout_id, component_id, node_key, x, y, + width, height, style_key, label_override, z_index) + VALUES (gen_random_uuid(), %(lid)s, %(cid)s, %(nkey)s, + %(x)s, %(y)s, %(w)s, %(h)s, + %(style)s, %(label)s, %(z)s) + """, { + "lid": layout_id, "cid": comp_id, "nkey": node_key, + "x": x, "y": y, "w": w, "h": h, + "style": style, "label": label, "z": z, + }) + + print(f"Nodes inserted: {len(nodes)}") + + # Node group mappings (for modal details aggregation) + groups = { + "power_west_group": ["isru_module", "mining_module", "ore_fuel_storage"], + "power_east_group": ["fuel_storage_mod", "centrifuge_mod"], + } + print(f"Node groups: {json.dumps(groups, ensure_ascii=False)}") + + +if __name__ == "__main__": + seed()