feat(topology): add square light canvas

Co-Authored-By: OpenAI Codex <codex@openai.com>
This commit is contained in:
2026-07-15 12:21:34 +08:00
co-authored by OpenAI Codex
parent 8d057dbfa3
commit c99c910aa3
8 changed files with 101 additions and 68 deletions
+7 -7
View File
@@ -42,17 +42,17 @@
.wiki-article .topo-connector { color: #8af; font-weight: bold; font-size: 0.85em; text-align: center; padding: 2px 0; }
.wiki-article .topo-power-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 6px; text-align: center; margin-top: 8px; }
.wiki-article .topo-power-inner { border: 1px dashed #666; padding: 12px 8px; border-radius: 4px; background: rgba(255,255,255,0.05); margin-top: 12px; }
.wiki-article .topology-embed-shell { margin: 1em 0 1.4em; border: 1px solid #6f7f96; border-radius: 8px; overflow: hidden; background: #07111f; box-shadow: 0 2px 10px rgba(0,0,0,0.18); }
.wiki-article .topology-embed-shell { margin: 1em 0 1.4em; border: 1px solid #b8c5d4; border-radius: 8px; overflow: hidden; background: #f4f7fb; box-shadow: 0 2px 10px rgba(50,72,96,0.14); }
.wiki-article #topology { scroll-margin-top: 88px; }
.wiki-article .topology-embed-host { min-height: 420px; overflow: hidden; background: #07111f; color: #a8b8cd; }
.wiki-article .topology-embed-frame { display: block; width: 100%; height: 1760px; border: 0; background: #07111f; }
.wiki-article .topology-embed-fallback { margin: 0; padding: 12px 16px; border-top: 1px solid #31425a; background: #0d1929; color: #d7e3f4; font-size: 0.9em; }
.wiki-article .topology-embed-fallback a { color: #70b7ff; }
.wiki-article .topology-embed-host { min-height: 420px; overflow: hidden; background: #f4f7fb; color: #5e6f85; }
.wiki-article .topology-embed-frame { display: block; width: 100%; height: 1450px; border: 0; background: #f4f7fb; }
.wiki-article .topology-embed-fallback { margin: 0; padding: 12px 16px; border-top: 1px solid #c7d2de; background: #eef3f8; color: #152033; font-size: 0.9em; }
.wiki-article .topology-embed-fallback a { color: #0645ad; }
@media (min-width: 821px) and (max-width: 1279px) {
.wiki-article .topology-embed-frame { width: 125%; height: 2200px; margin-bottom: -440px; transform: scale(0.8); transform-origin: top left; }
.wiki-article .topology-embed-frame { width: 125%; height: 1450px; margin-bottom: -290px; transform: scale(0.8); transform-origin: top left; }
}
@media (min-width: 1280px) and (max-width: 1500px) {
.wiki-article .topology-embed-frame { width: 170%; height: 1700px; margin-bottom: -700px; transform: scale(0.588235); transform-origin: top left; }
.wiki-article .topology-embed-frame { width: 170%; height: 1450px; margin-bottom: -597px; transform: scale(0.588235); transform-origin: top left; }
}
@media (max-width: 820px) { .wiki-article { padding: 18px 16px 36px; } .wiki-article .infobox { float: none; width: 100%; margin: 0.75em 0 1em; } .wiki-article .topology-grid { grid-template-columns: 1fr; } .wiki-article .topo-node.core { grid-column: 1; grid-row: auto; } }
+29 -28
View File
@@ -3,20 +3,21 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="data:,">
<title>广寒基地拓扑结构</title>
<style>
:root {
color-scheme: dark;
--bg: #070d18; --panel: #0d1728; --panel-2: #101c30;
--text: #eaf2ff; --muted: #91a3bd; --border: #263750;
--structural: #8fa1b8; --pressurized: #51e89c; --utility: #b178e6;
--core: #ffad42; --habitat: #52baff; --power: #d5b642;
--vehicle: #c28cdf; --radiator: #f0d869;
color-scheme: light;
--bg: #f4f7fb; --panel: #ffffff; --panel-2: #eef3f8;
--text: #152033; --muted: #5e6f85; --border: #b8c5d4;
--structural: #66788e; --pressurized: #168a55; --utility: #7a4fb3;
--core: #c76b00; --habitat: #1677b8; --power: #9b7900;
--vehicle: #8755a4; --radiator: #9b7900;
}
* { box-sizing: border-box; }
body {
margin: 0; min-height: 100vh; padding: 30px;
background: radial-gradient(circle at 50% 0%, rgba(51,88,140,0.18), transparent 36rem), var(--bg);
background: radial-gradient(circle at 50% 0%, rgba(89,132,181,0.14), transparent 36rem), var(--bg);
color: var(--text);
font-family: "Microsoft YaHei", "PingFang SC", system-ui, sans-serif;
}
@@ -27,42 +28,42 @@
.toolbar { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 12px; margin: 16px 0; }
.toolbar label { color: var(--muted); font-size: 0.88rem; }
.toolbar select, .toolbar input { padding: 6px 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--panel); color: var(--text); font-size: 0.88rem; }
.toolbar button { padding: 6px 16px; border: 1px solid var(--pressurized); border-radius: 6px; background: #0a2e16; color: var(--pressurized); cursor: pointer; font-size: 0.88rem; font-weight: 600; }
.toolbar button:hover { background: #0f3d1f; }
.toolbar button { padding: 6px 16px; border: 1px solid var(--pressurized); border-radius: 6px; background: #e7f7ee; color: #0e6840; cursor: pointer; font-size: 0.88rem; font-weight: 600; }
.toolbar button:hover { background: #d6f0e1; }
.loading-msg { text-align: center; color: var(--muted); padding: 40px; }
.error-msg { text-align: center; color: #f66; padding: 40px; }
.legend { display: flex; flex-wrap: wrap; justify-content: center; gap: 13px 24px; margin: 20px 0; padding: 13px 18px; border: 1px solid var(--border); border-radius: 12px; background: rgba(13,23,40,0.84); }
.legend-item { display: inline-flex; align-items: center; gap: 9px; color: #c8d5e7; font-size: 0.86rem; }
.error-msg { text-align: center; color: #b42318; padding: 40px; }
.legend { display: flex; flex-wrap: wrap; justify-content: center; gap: 13px 24px; margin: 20px 0; padding: 13px 18px; border: 1px solid var(--border); border-radius: 12px; background: rgba(255,255,255,0.88); }
.legend-item { display: inline-flex; align-items: center; gap: 9px; color: var(--text); font-size: 0.86rem; }
.legend-line { display: inline-block; width: 46px; border-top: 4px solid; }
.legend-chip { width: 19px; height: 13px; border: 2px solid var(--radiator); background: #665511; border-radius: 3px; }
.map-shell { overflow: auto; border: 1px solid var(--border); border-radius: 18px; background: #08101e; box-shadow: 0 24px 70px rgba(0,0,0,0.34); }
.topology-map { display: block; width: 100%; min-width: 1080px; height: auto; background: linear-gradient(rgba(18,36,60,0.21) 1px, transparent 1px), linear-gradient(90deg, rgba(18,36,60,0.21) 1px, transparent 1px), #08101e; background-size: 48px 48px; }
.legend-chip { width: 19px; height: 13px; border: 2px solid var(--radiator); background: #fff0a8; border-radius: 3px; }
.map-shell { overflow: auto; border: 1px solid var(--border); border-radius: 18px; background: #f8fbff; box-shadow: 0 18px 45px rgba(50,72,96,0.16); }
.topology-map { display: block; width: 100%; min-width: 1080px; height: auto; background: linear-gradient(rgba(78,105,135,0.12) 1px, transparent 1px), linear-gradient(90deg, rgba(78,105,135,0.12) 1px, transparent 1px), #f8fbff; background-size: 48px 48px; }
.connection { fill: none; vector-effect: non-scaling-stroke; stroke-linecap: square; }
.topology-node { cursor: pointer; outline: none; }
.topology-node rect { vector-effect: non-scaling-stroke; transition: filter 150ms ease, stroke-width 150ms ease; }
.topology-node:hover rect, .topology-node:focus-visible rect { filter: brightness(1.25); stroke-width: 5; }
.topology-node:hover rect, .topology-node:focus-visible rect { filter: brightness(0.97) drop-shadow(0 6px 8px rgba(45,66,88,0.18)); stroke-width: 5; }
.node-title { fill: var(--text); font-size: 25px; font-weight: 700; text-anchor: middle; }
.node-meta { fill: #a6b5c9; font-size: 17px; text-anchor: middle; }
.node-hint { fill: #71849e; font-size: 14px; text-anchor: middle; }
.node-meta { fill: #435a72; font-size: 17px; text-anchor: middle; }
.node-hint { fill: #6f8296; font-size: 14px; text-anchor: middle; }
.radiator { pointer-events: none; }
.radiator rect { vector-effect: non-scaling-stroke; }
.radiator line { stroke-width: 1.5; vector-effect: non-scaling-stroke; }
.radiator-label { font-size: 15px; font-weight: 650; text-anchor: middle; }
.click-note { margin: 13px 0 0; color: var(--muted); font-size: 0.84rem; text-align: center; }
body.modal-open { overflow: hidden; }
.modal-backdrop { position: fixed; inset: 0; z-index: 20; display: none; align-items: center; justify-content: center; padding: 24px; background: rgba(2,7,14,0.82); backdrop-filter: blur(8px); }
.modal-backdrop { position: fixed; inset: 0; z-index: 20; display: none; align-items: center; justify-content: center; padding: 24px; background: rgba(21,32,51,0.38); backdrop-filter: blur(8px); }
.modal-backdrop[aria-hidden="false"] { display: flex; }
.detail-dialog { position: relative; width: min(760px, 100%); max-height: min(820px, calc(100vh - 48px)); overflow: auto; padding: 30px; border: 1px solid #405572; border-radius: 18px; outline: none; background: linear-gradient(145deg, #13233a, #0b1424 72%); box-shadow: 0 30px 100px rgba(0,0,0,0.64); }
.detail-dialog:focus-visible { box-shadow: 0 0 0 3px var(--pressurized), 0 30px 100px rgba(0,0,0,0.64); }
.modal-close { position: absolute; top: 14px; right: 14px; width: 40px; height: 40px; border: 1px solid #4e627e; border-radius: 50%; background: #182a43; color: var(--text); font: 500 1.65rem/1 system-ui, sans-serif; cursor: pointer; }
.modal-close:hover, .modal-close:focus-visible { border-color: var(--pressurized); outline: none; background: #203754; }
.detail-dialog { position: relative; width: min(760px, 100%); max-height: min(820px, calc(100vh - 48px)); overflow: auto; padding: 30px; border: 1px solid #a8b7c8; border-radius: 18px; outline: none; background: linear-gradient(145deg, #ffffff, #f2f6fa 72%); box-shadow: 0 30px 80px rgba(38,55,74,0.28); }
.detail-dialog:focus-visible { box-shadow: 0 0 0 3px var(--pressurized), 0 30px 80px rgba(38,55,74,0.28); }
.modal-close { position: absolute; top: 14px; right: 14px; width: 40px; height: 40px; border: 1px solid #9aabbd; border-radius: 50%; background: #edf2f7; color: var(--text); font: 500 1.65rem/1 system-ui, sans-serif; cursor: pointer; }
.modal-close:hover, .modal-close:focus-visible { border-color: var(--pressurized); outline: none; background: #e2eaf2; }
.detail-kicker { margin: 0 46px 6px 0; color: var(--pressurized); font-size: 0.77rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; }
#detail-title { margin: 0 50px 10px 0; font-size: 1.85rem; }
#detail-role { margin: 0 0 22px; color: #c4d2e4; line-height: 1.7; }
#detail-role { margin: 0 0 22px; color: #42566e; line-height: 1.7; }
.detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin: 0; }
.detail-grid > div { min-width: 0; padding: 14px; border: 1px solid #293c57; border-radius: 10px; background: rgba(8,18,33,0.7); }
.detail-grid dt { margin-bottom: 7px; color: #7f94b1; font-size: 0.75rem; }
.detail-grid dd { margin: 0; color: #e1ebf8; line-height: 1.55; overflow-wrap: anywhere; }
.detail-grid > div { min-width: 0; padding: 14px; border: 1px solid #c7d2de; border-radius: 10px; background: rgba(247,249,252,0.9); }
.detail-grid dt { margin-bottom: 7px; color: #61758c; font-size: 0.75rem; }
.detail-grid dd { margin: 0; color: #1e2d40; line-height: 1.55; overflow-wrap: anywhere; }
.detail-grid dd span { display: block; }
.detail-grid dd span + span { margin-top: 4px; }
@media (max-width: 760px) {
@@ -96,7 +97,7 @@
<div id="error-msg" class="error-msg" style="display:none;"></div>
<div class="map-shell" id="map-shell" style="display:none;">
<svg class="topology-map" viewBox="0 0 1500 1900" role="img" aria-labelledby="topology-title" id="topology-svg">
<svg class="topology-map" viewBox="0 0 1900 1900" role="img" aria-labelledby="topology-title" id="topology-svg">
<title id="topology-title">广寒基地拓扑</title>
<desc id="topology-desc">根据所选任务或时间展示广寒基地当时的工程拓扑。</desc>
<!-- FUTURE_EXPANSION_ANCHOR:north -->
Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

+7
View File
@@ -49,6 +49,13 @@ class GuanghanTopologySourceTests(unittest.TestCase):
self.assertIn("decorationStyles", self.html)
self.assertIn("legendItems", self.html)
def test_canvas_is_square_and_page_defaults_to_light(self) -> None:
self.assertIn('viewBox="0 0 1900 1900"', self.html)
self.assertIn("payload.layout.canvasWidth", self.html)
self.assertIn("payload.layout.canvasHeight", self.html)
self.assertIn("color-scheme: light", self.html)
self.assertNotIn("color-scheme: dark", self.html)
def test_business_visual_semantics_are_not_css_hardcoded(self) -> None:
forbidden = [
".connection.structural",
+19 -9
View File
@@ -211,8 +211,10 @@ class TopologyApiQueryTests(unittest.TestCase):
self.assertEqual(
presentation["theme"]["key"],
"guanghan_dark_engineering_v1",
"guanghan_light_engineering_v1",
)
self.assertEqual(layout["layout"]["canvasWidth"], 1900.0)
self.assertEqual(layout["layout"]["canvasHeight"], 1900.0)
self.assertIn("nodeStyles", presentation)
self.assertIn("edgeStyles", presentation)
self.assertIn("decorationStyles", presentation)
@@ -239,7 +241,7 @@ class TopologyApiQueryTests(unittest.TestCase):
presentation["edgeStyles"]["pressurized_passage"],
{
"displayName": "加压走道",
"strokeColor": "#51e89c",
"strokeColor": "#168a55",
"strokeWidth": 9.0,
"dashArray": None,
"lineCap": "square",
@@ -265,15 +267,20 @@ class TopologyApiQueryTests(unittest.TestCase):
conn.execute("SET search_path TO topology, public")
with conn.cursor() as cur:
cur.execute("""
SELECT stroke_color
FROM diagram_edge_styles
WHERE connection_type = 'pressurized_passage'
SELECT edge.stroke_color
FROM diagram_edge_styles AS edge
JOIN diagram_themes AS theme ON theme.id = edge.theme_id
WHERE edge.connection_type = 'pressurized_passage'
AND theme.theme_key = 'guanghan_light_engineering_v1'
""")
original_color = cur.fetchone()["stroke_color"]
cur.execute("""
UPDATE diagram_edge_styles
UPDATE diagram_edge_styles AS edge
SET stroke_color = '#00ffaa'
WHERE connection_type = 'pressurized_passage'
FROM diagram_themes AS theme
WHERE theme.id = edge.theme_id
AND edge.connection_type = 'pressurized_passage'
AND theme.theme_key = 'guanghan_light_engineering_v1'
""")
conn.commit()
try:
@@ -290,9 +297,12 @@ class TopologyApiQueryTests(unittest.TestCase):
finally:
with conn.cursor() as cur:
cur.execute("""
UPDATE diagram_edge_styles
UPDATE diagram_edge_styles AS edge
SET stroke_color = %(original)s
WHERE connection_type = 'pressurized_passage'
FROM diagram_themes AS theme
WHERE theme.id = edge.theme_id
AND edge.connection_type = 'pressurized_passage'
AND theme.theme_key = 'guanghan_light_engineering_v1'
""", {"original": original_color})
conn.commit()
+39 -24
View File
@@ -3,41 +3,44 @@
import json
from topology_api.db import write_cursor
THEME_KEY = "guanghan_dark_engineering_v1"
THEME_KEY = "guanghan_light_engineering_v1"
CANVAS_WIDTH = 1900
CANVAS_HEIGHT = 1900
CONTENT_X_OFFSET = 200
THEME_TOKENS = {
"textColor": "#eaf2ff",
"mutedTextColor": "#91a3bd",
"metaColor": "#a6b5c9",
"hintColor": "#71849e",
"textColor": "#152033",
"mutedTextColor": "#5e6f85",
"metaColor": "#435a72",
"hintColor": "#6f8296",
}
NODE_STYLES = [
("node-card", "普通舱段", "#10263a", "#52baff", 3, 16),
("core-card", "核心舱", "#3b250d", "#ffad42", 5, 16),
("power-card", "电力模块分支", "#2a2810", "#d5b642", 3, 16),
("power-node-card", "电力模块中心", "#393412", "#d5b642", 5, 16),
("vehicle-card", "车辆", "#291c34", "#c28cdf", 3, 16),
("node-card", "普通舱段", "#edf7ff", "#1677b8", 3, 16),
("core-card", "核心舱", "#fff3e2", "#c76b00", 5, 16),
("power-card", "电力模块分支", "#fff9d9", "#9b7900", 3, 16),
("power-node-card", "电力模块中心", "#fff2b8", "#9b7900", 5, 16),
("vehicle-card", "车辆", "#f7effb", "#8755a4", 3, 16),
]
EDGE_STYLES = [
("structural_mount", "结构直连", "#8fa1b8", 6, None, "square", "structural", 10),
("pressurized_passage", "加压走道", "#51e89c", 9, None, "square", "pressurized", 20),
("fuel", "燃料 / 供电", "#b178e6", 4, "13 10", "square", "utility", 30),
("power", "燃料 / 供电", "#b178e6", 4, "13 10", "square", "utility", 30),
("cooling", "冷却", "#78d6e6", 4, "10 8", "square", "cooling", 40),
("docking", "对接口", "#c28cdf", 4, "10 8", "square", "docking", 50),
("structural_mount", "结构直连", "#66788e", 6, None, "square", "structural", 10),
("pressurized_passage", "加压走道", "#168a55", 9, None, "square", "pressurized", 20),
("fuel", "燃料 / 供电", "#7a4fb3", 4, "13 10", "square", "utility", 30),
("power", "燃料 / 供电", "#7a4fb3", 4, "13 10", "square", "utility", 30),
("cooling", "冷却", "#27809a", 4, "10 8", "square", "cooling", 40),
("docking", "对接口", "#8755a4", 4, "10 8", "square", "docking", 50),
]
DECORATION_STYLES = [
(
"radiator",
"大型折叠散热面板",
"#6b5814",
"#f0d869",
"#fff0a8",
"#9b7900",
2,
"散热器",
"#f4df7b",
"#6d5500",
{
"widthRatio": 0.55,
"maxWidth": 130,
@@ -64,6 +67,11 @@ def seed():
cur.execute("SELECT id FROM bases WHERE code = 'guanghan'")
base_id = cur.fetchone()["id"]
cur.execute(
"UPDATE diagram_themes SET is_default = false WHERE base_id = %(base)s",
{"base": base_id},
)
cur.execute("""
INSERT INTO diagram_themes
(id, base_id, theme_key, name, description, tokens, is_default)
@@ -71,8 +79,8 @@ def seed():
gen_random_uuid(),
%(base)s,
%(theme_key)s,
'广寒色工程图',
'广寒基地工程总图默认表现层主题',
'广寒色工程图',
'适配 Wiki.js 与 Dark Reader 的浅色工程总图主题',
%(tokens)s,
true
)
@@ -96,13 +104,20 @@ def seed():
INSERT INTO diagram_layouts (id, base_id, layout_key, name,
canvas_width, canvas_height, is_default, theme_id)
VALUES (gen_random_uuid(), %(base)s, 'engineering_overview_v1',
'工程总图 v1', 1500, 1900, true, %(theme)s)
'工程总图 v1', %(canvas_width)s, %(canvas_height)s, true, %(theme)s)
ON CONFLICT (base_id, layout_key) DO UPDATE
SET name = EXCLUDED.name,
canvas_width = EXCLUDED.canvas_width,
canvas_height = EXCLUDED.canvas_height,
is_default = true,
theme_id = EXCLUDED.theme_id
RETURNING id
""", {"base": base_id, "theme": theme_id})
""", {
"base": base_id,
"canvas_width": CANVAS_WIDTH,
"canvas_height": CANVAS_HEIGHT,
"theme": theme_id,
})
layout_id = cur.fetchone()["id"]
print(f"Layout: {layout_id}")
@@ -278,7 +293,7 @@ def seed():
""", {
"lid": layout_id, "cid": primary_component_id,
"nkey": node_key,
"x": x, "y": y, "w": w, "h": h,
"x": x + CONTENT_X_OFFSET, "y": y, "w": w, "h": h,
"style": style, "label": label, "z": z,
"meta": display_meta, "hint": display_hint,
"decorations": json.dumps(decorations),