Skip to content
Clone

chore(knowledge-graph): remove obsolete HTML file

ZhenYi committed
parent ffc8ef9 commit 6a5c56d Browse files

1 file changed

+0 −437

Deleted knowledge-graph.html
@@ -1,437 +0,0 @@
1<!doctype html>
2<html lang="zh-CN">
3<head>
4 <meta charset="utf-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <title>GitData Knowledge Graph</title>
7 <style>
8 :root {
9 color-scheme: light;
10 --bg: #f4f7fb;
11 --surface: rgba(255, 255, 255, .88);
12 --surface-strong: #ffffff;
13 --text: #172033;
14 --muted: #667085;
15 --line: #d9e0ec;
16 --line-strong: #aebbd0;
17 --accent: #315efb;
18 --accent-soft: #e7edff;
19 --shadow: 0 16px 42px rgba(30, 53, 93, .11);
20 --app: #e8f0ff;
21 --domain: #e9f8f1;
22 --foundation: #fff3dc;
23 --resource: #f2eafd;
24 --app-line: #6f92e8;
25 --domain-line: #4fae83;
26 --foundation-line: #d69c3a;
27 --resource-line: #9b79d4;
28 }
29
30 [data-theme="dark"] {
31 color-scheme: dark;
32 --bg: #111827;
33 --surface: rgba(24, 34, 52, .9);
34 --surface-strong: #1a2437;
35 --text: #edf2ff;
36 --muted: #a6b2c8;
37 --line: #334156;
38 --line-strong: #55657f;
39 --accent: #8ca7ff;
40 --accent-soft: #25345f;
41 --shadow: 0 18px 42px rgba(0, 0, 0, .24);
42 --app: #1d2b4d;
43 --domain: #193d35;
44 --foundation: #4a3820;
45 --resource: #362851;
46 --app-line: #7e9eff;
47 --domain-line: #70cfaa;
48 --foundation-line: #f0bb61;
49 --resource-line: #b69ae7;
50 }
51
52 * { box-sizing: border-box; }
53 body {
54 margin: 0;
55 min-width: 320px;
56 background:
57 radial-gradient(circle at 0 0, rgba(49, 94, 251, .12), transparent 30rem),
58 var(--bg);
59 color: var(--text);
60 font: 14px/1.5 Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
61 }
62
63 button, input { font: inherit; }
64 button { color: inherit; }
65 .shell { max-width: 1680px; margin: 0 auto; padding: 28px; }
66 .topbar { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; margin-bottom: 22px; }
67 .eyebrow { color: var(--accent); font-weight: 700; letter-spacing: .08em; text-transform: uppercase; font-size: 12px; }
68 h1 { margin: 4px 0 5px; font-size: clamp(25px, 3vw, 38px); letter-spacing: -.035em; line-height: 1.1; }
69 .subtitle { margin: 0; color: var(--muted); max-width: 760px; }
70 .top-actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
71 .button, .filter-button {
72 border: 1px solid var(--line);
73 background: var(--surface-strong);
74 border-radius: 10px;
75 padding: 9px 13px;
76 cursor: pointer;
77 transition: border-color .15s ease, background .15s ease, transform .15s ease;
78 }
79 .button:hover, .filter-button:hover { border-color: var(--accent); transform: translateY(-1px); }
80 .button:focus-visible, .filter-button:focus-visible, input:focus-visible, .node:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 42%, transparent); outline-offset: 2px; }
81 .button.primary { border-color: var(--accent); background: var(--accent); color: #fff; }
82 .stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; }
83 .stat { border: 1px solid var(--line); background: var(--surface); box-shadow: var(--shadow); border-radius: 14px; padding: 14px 16px; }
84 .stat-label { color: var(--muted); font-size: 12px; }
85 .stat-value { margin-top: 2px; font-size: 26px; font-weight: 700; letter-spacing: -.03em; }
86 .workspace { display: grid; grid-template-columns: minmax(0, 1fr) 330px; gap: 16px; align-items: stretch; }
87 .graph-card, .detail-card { border: 1px solid var(--line); background: var(--surface); box-shadow: var(--shadow); border-radius: 16px; overflow: hidden; }
88 .toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 13px 14px; border-bottom: 1px solid var(--line); }
89 .search { min-width: 230px; flex: 1 1 260px; position: relative; }
90 .search input { width: 100%; border: 1px solid var(--line); background: var(--surface-strong); color: var(--text); border-radius: 10px; padding: 9px 12px 9px 36px; }
91 .search span { position: absolute; left: 12px; top: 8px; color: var(--muted); }
92 .filters { display: flex; gap: 6px; flex-wrap: wrap; }
93 .filter-button { padding: 7px 10px; font-size: 12px; }
94 .filter-button[aria-pressed="false"] { opacity: .48; }
95 .canvas-wrap { position: relative; min-height: 680px; background: linear-gradient(180deg, color-mix(in srgb, var(--surface-strong) 90%, transparent), transparent); }
96 svg { display: block; width: 100%; height: 680px; touch-action: none; user-select: none; }
97 .layer-band { fill: color-mix(in srgb, var(--band) 55%, transparent); stroke: color-mix(in srgb, var(--band-line) 65%, transparent); stroke-width: 1; stroke-dasharray: 4 5; }
98 .layer-title { fill: var(--muted); font-size: 13px; font-weight: 700; letter-spacing: .04em; }
99 .edge { fill: none; stroke: var(--line-strong); stroke-width: 1.3; opacity: .68; transition: opacity .15s ease, stroke .15s ease, stroke-width .15s ease; }
100 .edge[data-type="connects"] { stroke-dasharray: 5 5; }
101 .edge.is-related { stroke: var(--accent); stroke-width: 2.6; opacity: 1; }
102 .edge.is-dim { opacity: .08; }
103 .node { cursor: pointer; transition: opacity .15s ease; }
104 .node.is-dim { opacity: .13; }
105 .node rect { stroke: var(--node-line); stroke-width: 1.3; fill: var(--node-fill); }
106 .node.is-selected rect { stroke: var(--accent); stroke-width: 3; filter: drop-shadow(0 5px 8px color-mix(in srgb, var(--accent) 28%, transparent)); }
107 .node.is-match rect { stroke: var(--accent); stroke-width: 2.4; }
108 .node-title { fill: var(--text); font-size: 14px; font-weight: 700; pointer-events: none; }
109 .node-type { fill: var(--muted); font-size: 11px; pointer-events: none; }
110 .node-dot { fill: var(--node-line); pointer-events: none; }
111 .legend { display: flex; flex-wrap: wrap; gap: 11px; padding: 10px 15px 14px; color: var(--muted); font-size: 12px; border-top: 1px solid var(--line); }
112 .legend span { display: inline-flex; align-items: center; gap: 6px; }
113 .legend i { width: 9px; height: 9px; border-radius: 50%; background: var(--legend); }
114 .canvas-hint { position: absolute; bottom: 12px; left: 15px; color: var(--muted); font-size: 11px; pointer-events: none; }
115 .detail-card { min-height: 756px; padding: 19px; }
116 .detail-kicker { color: var(--accent); font-size: 11px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
117 .detail-card h2 { margin: 5px 0 6px; font-size: 24px; letter-spacing: -.025em; }
118 .detail-summary { margin: 0 0 17px; color: var(--muted); }
119 .detail-section { border-top: 1px solid var(--line); padding-top: 14px; margin-top: 14px; }
120 .detail-section h3 { margin: 0 0 8px; font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: .07em; }
121 .file-list, .link-list { display: grid; gap: 7px; margin: 0; padding: 0; list-style: none; }
122 .file-list code { display: block; padding: 6px 8px; border-radius: 7px; background: color-mix(in srgb, var(--accent-soft) 62%, var(--surface-strong)); color: var(--text); font-size: 11px; overflow-wrap: anywhere; }
123 .link-list button { display: block; width: 100%; text-align: left; padding: 6px 0; border: 0; background: transparent; color: var(--accent); cursor: pointer; }
124 .link-list button:hover { text-decoration: underline; }
125 .empty-detail { display: grid; place-items: center; height: 680px; color: var(--muted); text-align: center; }
126 .footer { display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-top: 15px; color: var(--muted); font-size: 12px; }
127 .tabular { font-variant-numeric: tabular-nums; }
128 @media (max-width: 1080px) {
129 .workspace { grid-template-columns: 1fr; }
130 .detail-card { min-height: 0; }
131 .empty-detail { height: auto; min-height: 180px; }
132 }
133 @media (max-width: 640px) {
134 .shell { padding: 17px; }
135 .topbar { flex-direction: column; }
136 .top-actions { justify-content: flex-start; }
137 .stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
138 .canvas-wrap, svg { min-height: 560px; height: 560px; }
139 }
140 </style>
141</head>
142<body>
143 <main class="shell">
144 <header class="topbar">
145 <div>
146 <div class="eyebrow">GitData / architecture map</div>
147 <h1>项目知识图谱</h1>
148 <p class="subtitle">以当前源码为准的架构级视图:应用入口、领域服务、基础设施与运行时资源之间的依赖和数据流。</p>
149 </div>
150 <div class="top-actions">
151 <button class="button" id="resetView" type="button">重置视图</button>
152 <button class="button" id="themeToggle" type="button">切换主题</button>
153 </div>
154 </header>
155
156 <section class="stats" aria-label="图谱统计">
157 <div class="stat"><div class="stat-label">Workspace packages</div><div class="stat-value tabular">23</div></div>
158 <div class="stat"><div class="stat-label">Rust files</div><div class="stat-value tabular">918</div></div>
159 <div class="stat"><div class="stat-label">Rust lines</div><div class="stat-value tabular">81.8k</div></div>
160 <div class="stat"><div class="stat-label">OpenAPI paths / operations</div><div class="stat-value tabular">283 / 409</div></div>
161 </section>
162
163 <section class="workspace">
164 <div class="graph-card">
165 <div class="toolbar">
166 <label class="search"><span aria-hidden="true">⌕</span><input id="search" type="search" placeholder="搜索 crate、服务或文件…" aria-label="搜索图谱"></label>
167 <div class="filters" id="filters" aria-label="显示分层"></div>
168 <button class="button" id="zoomOut" type="button" aria-label="缩小">−</button>
169 <button class="button" id="zoomIn" type="button" aria-label="放大">+</button>
170 </div>
171 <div class="canvas-wrap">
172 <svg id="graph" viewBox="0 0 1280 820" role="img" aria-labelledby="graphTitle graphDesc">
173 <title id="graphTitle">GitData 架构知识图谱</title>
174 <desc id="graphDesc">展示应用、领域、基础设施和外部资源之间的依赖关系。点击节点查看详情。</desc>
175 <defs>
176 <marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="var(--line-strong)"></path></marker>
177 </defs>
178 <g id="viewport">
179 <g id="bands"></g>
180 <g id="edges"></g>
181 <g id="nodes"></g>
182 </g>
183 </svg>
184 <div class="canvas-hint">滚轮缩放 · 拖动节点 · 点击节点查看调用边界</div>
185 </div>
186 <div class="legend" aria-label="分层图例">
187 <span><i style="--legend:var(--app-line)"></i>应用入口</span>
188 <span><i style="--legend:var(--domain-line)"></i>业务领域</span>
189 <span><i style="--legend:var(--foundation-line)"></i>基础设施</span>
190 <span><i style="--legend:var(--resource-line)"></i>运行时资源</span>
191 <span>实线:依赖 · 虚线:连接运行时资源</span>
192 </div>
193 </div>
194
195 <aside class="detail-card" id="detail" aria-live="polite"></aside>
196 </section>
197
198 <footer class="footer">
199 <span>数据来源:当前工作区源码、Cargo metadata、config/default.toml、openapi.json。</span>
200 <span>生成时间:2026-09-14 · commit <code>06505aa8</code></span>
201 </footer>
202 </main>
203
204 <script>
205 const graphData = {
206 layers: [
207 { id: "apps", name: "应用入口", band: "var(--app)", line: "var(--app-line)" },
208 { id: "domains", name: "业务领域", band: "var(--domain)", line: "var(--domain-line)" },
209 { id: "foundation", name: "基础设施", band: "var(--foundation)", line: "var(--foundation-line)" },
210 { id: "resources", name: "运行时资源", band: "var(--resource)", line: "var(--resource-line)" }
211 ],
212 nodes: [
213 { id: "apihost", name: "apihost", layer: "apps", type: "binary", summary: "主 API 进程:聚合 REST、GraphQL、页面、Swagger 和健康检查,并启动 Chat 与注销后台任务。", files: ["app/apihost/src/main.rs"] },
214 { id: "githost", name: "githost", layer: "apps", type: "binary", summary: "Git 数据面进程:Smart HTTP 与 SSH 双监听,接收 clone/fetch/push。", files: ["app/githost/src/main.rs", "lib/git/src/smart/handler.rs"] },
215 { id: "synchost", name: "synchost", layer: "apps", type: "binary", summary: "Git 元数据同步 worker,从 Redis LIST 消费 post-receive 任务。", files: ["app/synchost/src/main.rs", "lib/git/src/sync/worker.rs"] },
216 { id: "im", name: "im", layer: "apps", type: "binary", summary: "独立即时通信网关,负责 HTTP/1 upgrade 和 WebSocket 会话。", files: ["app/im/src/main.rs", "app/im/src/gateway.rs", "app/im/src/socket.rs"] },
217 { id: "mails", name: "mails", layer: "apps", type: "binary", summary: "邮件队列消费者,连接 Redis Pub/Sub 和 SMTP。", files: ["app/mails/src/main.rs", "lib/email/src/runtime.rs"] },
218 { id: "migrate", name: "migrate", layer: "apps", type: "binary", summary: "一次性数据库 schema 同步入口。", files: ["app/migrate/src/main.rs", "lib/models/src/migrate/mod.rs"] },
219 { id: "openapi", name: "openapi", layer: "apps", type: "binary", summary: "生成或校验仓库根目录的 openapi.json。", files: ["app/openapi/src/main.rs", "lib/webapi/src/openapi.rs"] },
220
221 { id: "webapi", name: "webapi", layer: "domains", type: "assembly", summary: "HTTP 装配层:合并所有领域路由,安装 Session、CORS、body limit、trace、metrics 和 SPA fallback。", files: ["lib/webapi/src/lib.rs", "lib/webapi/src/openapi.rs", "lib/webapi/src/metrics.rs"] },
222 { id: "auth", name: "auth", layer: "domains", type: "domain", summary: "注册、登录、密码、邮箱验证、2FA、访问密钥、SSH key、通知和账户注销。", files: ["lib/auth/src/routes.rs", "lib/auth/src/services/login.rs", "lib/auth/src/services/two_factor.rs"] },
223 { id: "user", name: "user", layer: "domains", type: "domain", summary: "用户资料、邮箱、头像、偏好、设备、会话和用户侧资源。", files: ["lib/user/src/routes.rs", "lib/user/src/services/service.rs"] },
224 { id: "space", name: "space", layer: "domains", type: "domain", summary: "多租户 Space、成员、组、邀请、封禁、目录树和 ACL。", files: ["lib/space/src/routes.rs", "lib/space/src/share/authz.rs", "lib/space/src/services/service.rs"] },
225 { id: "issues", name: "issues", layer: "domains", type: "domain", summary: "Board、Issue、评论、标签、里程碑、依赖、事件线和工时。", files: ["lib/issues/src/routes.rs", "lib/issues/src/services/issues.rs", "lib/issues/src/services/events.rs"] },
226 { id: "pulls", name: "pulls", layer: "domains", type: "domain", summary: "Pull Request、Review、评论、检查、分支更新、锁定和合并。", files: ["lib/pulls/src/routes.rs", "lib/pulls/src/services/merge.rs", "lib/pulls/src/services/reviews.rs"] },
227 { id: "git", name: "git", layer: "domains", type: "domain", summary: "仓库、分支、提交、Smart HTTP、SSH、LFS、GraphQL、保护规则和同步。", files: ["lib/git/src/routes.rs", "lib/git/src/service/mod.rs", "lib/git/src/smart/handler.rs", "lib/git/src/sync/worker.rs"] },
228 { id: "chat", name: "chat", layer: "domains", type: "domain", summary: "房间、消息、线程、DM、附件、提醒、Webhook、实时事件和后台任务。", files: ["lib/chat/src/routes.rs", "lib/chat/src/services/messages.rs", "lib/chat/src/share/realtime.rs"] },
229
230 { id: "config", name: "config", layer: "foundation", type: "foundation", summary: "AppConfig、Figment/TOML/APP_ 环境变量加载,以及各进程的 scoped config。", files: ["lib/config/src/lib.rs", "lib/config/src/loader.rs", "lib/config/src/scoped.rs", "config/default.toml"] },
231 { id: "models", name: "models", layer: "foundation", type: "foundation", summary: "SeaORM 领域实体、schema sync 和用户、Space、Repo、Issue、PR、Chat 等持久化模型。", files: ["lib/models/src/lib.rs", "lib/models/src/migrate/mod.rs", "lib/models/schema_pre_sync.sql", "lib/models/schema_extras.sql"] },
232 { id: "export", name: "export", layer: "foundation", type: "foundation", summary: "共享 AppState、数据库读写路由、Redis、S3、统一响应、健康检查、通知和 telemetry。", files: ["lib/export/src/lib.rs", "lib/export/src/database.rs", "lib/export/src/redis.rs", "lib/export/src/rest.rs"] },
233 { id: "session", name: "session", layer: "foundation", type: "foundation", summary: "Redis-backed axum session、用户上下文、密码版本和通用 SessionGuard。", files: ["lib/session/src/store.rs", "lib/session/src/share/guard.rs", "lib/session/src/share/session.rs"] },
234 { id: "storage", name: "storage", layer: "foundation", type: "foundation", summary: "S3 兼容对象存储封装,支持流式传输、multipart 和 presigned URL。", files: ["lib/storage/src/lib.rs", "lib/storage/src/s3.rs"] },
235 { id: "email", name: "email", layer: "foundation", type: "foundation", summary: "SMTP transport、模板渲染、Redis 邮件队列和 subscriber 生命周期。", files: ["lib/email/src/service.rs", "lib/email/src/queue.rs", "lib/email/src/runtime.rs"] },
236 { id: "cache", name: "cache", layer: "foundation", type: "foundation", summary: "oxcache 两级缓存:本地 L1 + Redis L2。主要服务 Git 和 synchost。", files: ["lib/cache/src/lib.rs", "lib/cache/src/tiered.rs"] },
237 { id: "pow-solver", name: "pow-solver", layer: "foundation", type: "foundation", summary: "SHA-256 PoW challenge 的生成、校验和 WASM 求解导出。", files: ["lib/pow-solver/src/challenge.rs", "lib/pow-solver/src/wasm.rs"] },
238
239 { id: "postgres", name: "PostgreSQL", layer: "resources", type: "runtime", summary: "业务事实源:用户、权限、Git 元数据、Issue/PR/Chat 和 session 影子记录。", files: ["config/default.toml", "lib/export/src/database.rs"] },
240 { id: "redis", name: "Redis / Valkey", layer: "resources", type: "runtime", summary: "Session、配额、Pub/Sub、邮件队列、Git 同步 LIST、锁和实时事件。", files: ["config/default.toml", "lib/export/src/redis.rs", "lib/git/src/sync/queue.rs"] },
241 { id: "s3", name: "S3 compatible", layer: "resources", type: "runtime", summary: "头像、空间图片、Chat 附件、LFS 等二进制对象。", files: ["config/default.toml", "lib/storage/src/s3.rs"] },
242 { id: "repo-fs", name: "bare repository FS", layer: "resources", type: "runtime", summary: "Git 裸仓库和对象池所在文件系统;githost 与 synchost 共享挂载。", files: ["config/default.toml", "chart/templates/repos-pvc.yaml", "lib/git/src/context/facade.rs"] },
243 { id: "smtp", name: "SMTP", layer: "resources", type: "runtime", summary: "验证码、邀请、密码重置和通知邮件的外部传输端。", files: ["config/default.toml", "lib/email/src/transport.rs"] },
244 { id: "web-assets", name: "web crate assets", layer: "resources", type: "runtime", summary: "外部固定 commit 的 web crate 提供内嵌前端静态资源,webapi 负责 SPA fallback 和缓存。", files: ["lib/webapi/Cargo.toml", "lib/webapi/src/spa.rs"] }
245 ],
246 edges: [
247 ["apihost", "webapi", "depends"], ["apihost", "auth", "starts"], ["apihost", "chat", "starts"], ["apihost", "config", "depends"], ["apihost", "export", "depends"], ["apihost", "session", "depends"],
248 ["githost", "git", "depends"], ["githost", "export", "depends"], ["githost", "config", "depends"], ["githost", "repo-fs", "connects"], ["synchost", "git", "depends"], ["synchost", "cache", "depends"], ["synchost", "export", "depends"], ["synchost", "config", "depends"], ["synchost", "repo-fs", "connects"],
249 ["im", "chat", "depends"], ["im", "session", "depends"], ["im", "export", "depends"], ["im", "config", "depends"], ["mails", "email", "depends"], ["mails", "export", "depends"], ["mails", "config", "depends"], ["mails", "smtp", "connects"], ["migrate", "models", "depends"], ["migrate", "config", "depends"], ["openapi", "webapi", "depends"],
250 ["webapi", "auth", "depends"], ["webapi", "chat", "depends"], ["webapi", "issues", "depends"], ["webapi", "pulls", "depends"], ["webapi", "space", "depends"], ["webapi", "user", "depends"], ["webapi", "git", "depends"], ["webapi", "session", "depends"], ["webapi", "export", "depends"], ["webapi", "web-assets", "connects"],
251 ["auth", "session", "depends"], ["auth", "export", "depends"], ["auth", "models", "depends"], ["auth", "config", "depends"], ["auth", "email", "depends"], ["auth", "space", "depends"], ["auth", "pow-solver", "depends"],
252 ["user", "session", "depends"], ["user", "export", "depends"], ["user", "models", "depends"], ["user", "space", "depends"], ["user", "storage", "depends"], ["space", "session", "depends"], ["space", "export", "depends"], ["space", "models", "depends"], ["space", "email", "depends"], ["space", "storage", "depends"],
253 ["issues", "session", "depends"], ["issues", "export", "depends"], ["issues", "models", "depends"], ["issues", "space", "depends"], ["issues", "email", "depends"], ["pulls", "git", "depends"], ["pulls", "issues", "depends"], ["pulls", "space", "depends"], ["pulls", "session", "depends"], ["pulls", "export", "depends"], ["pulls", "models", "depends"],
254 ["git", "export", "depends"], ["git", "config", "depends"], ["git", "models", "depends"], ["git", "space", "depends"], ["git", "session", "depends"], ["git", "cache", "depends"], ["chat", "session", "depends"], ["chat", "export", "depends"], ["chat", "models", "depends"], ["chat", "space", "depends"], ["chat", "storage", "depends"],
255 ["export", "config", "depends"], ["export", "email", "depends"], ["export", "storage", "depends"], ["export", "postgres", "connects"], ["export", "redis", "connects"], ["session", "redis", "connects"], ["models", "postgres", "connects"], ["storage", "s3", "connects"], ["email", "redis", "connects"], ["email", "smtp", "connects"], ["cache", "redis", "connects"]
256 ]
257 };
258
259 const svg = document.getElementById("graph");
260 const viewport = document.getElementById("viewport");
261 const bands = document.getElementById("bands");
262 const edgesLayer = document.getElementById("edges");
263 const nodesLayer = document.getElementById("nodes");
264 const detail = document.getElementById("detail");
265 const search = document.getElementById("search");
266 const nodeMap = new Map(graphData.nodes.map(node => [node.id, node]));
267 const layerMap = new Map(graphData.layers.map(layer => [layer.id, layer]));
268 const edgeData = graphData.edges.map(([source, target, type]) => ({ source, target, type }));
269 const positions = new Map();
270 const columnX = { apps: 155, domains: 455, foundation: 755, resources: 1055 };
271 const bandWidth = 245;
272 let selectedId = null;
273 let scale = 1;
274 let panX = 0;
275 let panY = 0;
276 let dragState = null;
277 let enabledLayers = new Set(graphData.layers.map(layer => layer.id));
278
279 for (const layer of graphData.layers) {
280 const nodes = graphData.nodes.filter(node => node.layer === layer.id);
281 nodes.forEach((node, index) => positions.set(node.id, { x: columnX[layer.id], y: 92 + index * 82 }));
282 const band = document.createElementNS("http://www.w3.org/2000/svg", "rect");
283 band.setAttribute("x", columnX[layer.id] - bandWidth / 2);
284 band.setAttribute("y", 42);
285 band.setAttribute("width", bandWidth);
286 band.setAttribute("height", 708);
287 band.setAttribute("rx", 18);
288 band.classList.add("layer-band");
289 band.style.setProperty("--band", layer.band);
290 band.style.setProperty("--band-line", layer.line);
291 bands.appendChild(band);
292 const title = document.createElementNS("http://www.w3.org/2000/svg", "text");
293 title.setAttribute("x", columnX[layer.id] - bandWidth / 2 + 18);
294 title.setAttribute("y", 70);
295 title.classList.add("layer-title");
296 title.textContent = layer.name;
297 bands.appendChild(title);
298 }
299
300 function nodeStyle(node) {
301 const layer = layerMap.get(node.layer);
302 return { fill: layer.band, line: layer.line };
303 }
304
305 function makeNode(node) {
306 const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
307 group.classList.add("node");
308 group.dataset.id = node.id;
309 group.setAttribute("tabindex", "0");
310 group.setAttribute("role", "button");
311 group.setAttribute("aria-label", `${node.name}:${node.summary}`);
312 const style = nodeStyle(node);
313 group.style.setProperty("--node-fill", style.fill);
314 group.style.setProperty("--node-line", style.line);
315 const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
316 rect.setAttribute("x", -105); rect.setAttribute("y", -27); rect.setAttribute("width", 210); rect.setAttribute("height", 54); rect.setAttribute("rx", 12);
317 group.appendChild(rect);
318 const dot = document.createElementNS("http://www.w3.org/2000/svg", "circle");
319 dot.setAttribute("cx", -88); dot.setAttribute("cy", -9); dot.setAttribute("r", 5); dot.classList.add("node-dot"); group.appendChild(dot);
320 const title = document.createElementNS("http://www.w3.org/2000/svg", "text");
321 title.setAttribute("x", -75); title.setAttribute("y", -5); title.classList.add("node-title"); title.textContent = node.name; group.appendChild(title);
322 const type = document.createElementNS("http://www.w3.org/2000/svg", "text");
323 type.setAttribute("x", -75); type.setAttribute("y", 15); type.classList.add("node-type"); type.textContent = node.type; group.appendChild(type);
324 group.addEventListener("click", event => { if (!dragState || !dragState.moved) selectNode(node.id); event.stopPropagation(); });
325 group.addEventListener("keydown", event => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); selectNode(node.id); } });
326 group.addEventListener("pointerdown", event => startDrag(event, node.id, group));
327 nodesLayer.appendChild(group);
328 return group;
329 }
330
331 const nodeElements = new Map(graphData.nodes.map(node => [node.id, makeNode(node)]));
332 const edgeElements = edgeData.map(edge => {
333 const line = document.createElementNS("http://www.w3.org/2000/svg", "path");
334 line.classList.add("edge"); line.dataset.source = edge.source; line.dataset.target = edge.target; line.dataset.type = edge.type; line.setAttribute("marker-end", "url(#arrow)");
335 edgesLayer.appendChild(line); return line;
336 });
337
338 function edgePath(source, target) {
339 const a = positions.get(source), b = positions.get(target);
340 const dx = b.x - a.x;
341 const bend = Math.max(38, Math.abs(dx) * .28);
342 const sign = dx >= 0 ? 1 : -1;
343 return `M ${a.x + sign * 105} ${a.y} C ${a.x + sign * bend} ${a.y}, ${b.x - sign * bend} ${b.y}, ${b.x - sign * 105} ${b.y}`;
344 }
345
346 function redraw() {
347 for (const node of graphData.nodes) nodeElements.get(node.id).setAttribute("transform", `translate(${positions.get(node.id).x} ${positions.get(node.id).y})`);
348 edgeElements.forEach((element, index) => element.setAttribute("d", edgePath(edgeData[index].source, edgeData[index].target)));
349 viewport.setAttribute("transform", `translate(${panX} ${panY}) scale(${scale})`);
350 applyState();
351 }
352
353 function connectedTo(id) {
354 const ids = new Set([id]);
355 edgeData.forEach(edge => { if (edge.source === id) ids.add(edge.target); if (edge.target === id) ids.add(edge.source); });
356 return ids;
357 }
358
359 function applyState() {
360 const query = search.value.trim().toLowerCase();
361 const matches = new Set(graphData.nodes.filter(node => `${node.name} ${node.type} ${node.summary} ${node.files.join(" ")}`.toLowerCase().includes(query)).map(node => node.id));
362 const related = selectedId ? connectedTo(selectedId) : null;
363 graphData.nodes.forEach(node => {
364 const element = nodeElements.get(node.id);
365 const visible = enabledLayers.has(node.layer);
366 element.classList.toggle("is-dim", !visible || (query.length > 0 && !matches.has(node.id)) || (related && !related.has(node.id)));
367 element.classList.toggle("is-match", query.length > 0 && matches.has(node.id));
368 element.classList.toggle("is-selected", selectedId === node.id);
369 element.style.pointerEvents = visible ? "auto" : "none";
370 });
371 edgeData.forEach((edge, index) => {
372 const element = edgeElements[index];
373 const visible = enabledLayers.has(nodeMap.get(edge.source).layer) && enabledLayers.has(nodeMap.get(edge.target).layer);
374 element.classList.toggle("is-dim", !visible || (related && !(related.has(edge.source) && related.has(edge.target))));
375 });
376 const count = matches.size;
377 search.setAttribute("aria-label", query ? `搜索图谱,匹配 ${count} 个节点` : "搜索图谱");
378 }
379
380 function detailHtml(node) {
381 const incoming = edgeData.filter(edge => edge.target === node.id).map(edge => nodeMap.get(edge.source));
382 const outgoing = edgeData.filter(edge => edge.source === node.id).map(edge => nodeMap.get(edge.target));
383 const unique = list => [...new Map(list.map(item => [item.id, item])).values()];
384 const links = (title, list) => list.length ? `<div class="detail-section"><h3>${title}</h3><ul class="link-list">${unique(list).map(item => `<li><button type="button" data-jump="${item.id}">${item.name}<span class="text-muted"> · ${item.type}</span></button></li>`).join("")}</ul></div>` : "";
385 return `<div class="detail-kicker">${layerMap.get(node.layer).name} · ${node.type}</div><h2>${node.name}</h2><p class="detail-summary">${node.summary}</p><div class="detail-section"><h3>关键文件</h3><ul class="file-list">${node.files.map(file => `<li><code>${file}</code></li>`).join("")}</ul></div>${links("依赖 / 下游", outgoing)}${links("被使用 / 上游", incoming)}`;
386 }
387
388 function selectNode(id) {
389 selectedId = id;
390 const node = nodeMap.get(id);
391 detail.innerHTML = detailHtml(node);
392 detail.querySelectorAll("[data-jump]").forEach(button => button.addEventListener("click", () => selectNode(button.dataset.jump)));
393 applyState();
394 }
395
396 function resetView() {
397 selectedId = null; search.value = ""; scale = 1; panX = 0; panY = 0; enabledLayers = new Set(graphData.layers.map(layer => layer.id));
398 document.querySelectorAll(".filter-button").forEach(button => { button.setAttribute("aria-pressed", "true"); });
399 detail.innerHTML = `<div class="empty-detail"><div><strong>选择一个节点</strong><br>查看职责、关键文件和上下游关系。</div></div>`;
400 redraw();
401 }
402
403 function makeFilters() {
404 const container = document.getElementById("filters");
405 graphData.layers.forEach(layer => {
406 const button = document.createElement("button"); button.type = "button"; button.className = "filter-button"; button.textContent = layer.name; button.setAttribute("aria-pressed", "true");
407 button.addEventListener("click", () => { const enabled = enabledLayers.has(layer.id); enabled ? enabledLayers.delete(layer.id) : enabledLayers.add(layer.id); button.setAttribute("aria-pressed", String(!enabled)); applyState(); });
408 container.appendChild(button);
409 });
410 }
411
412 function zoomAt(next, cx = 640, cy = 410) {
413 const beforeX = (cx - panX) / scale, beforeY = (cy - panY) / scale;
414 scale = Math.max(.55, Math.min(1.8, next)); panX = cx - beforeX * scale; panY = cy - beforeY * scale; redraw();
415 }
416
417 function svgPoint(event) { const rect = svg.getBoundingClientRect(); return { x: (event.clientX - rect.left) * 1280 / rect.width, y: (event.clientY - rect.top) * 820 / rect.height }; }
418 function startDrag(event, id, element) { element.setPointerCapture(event.pointerId); const point = svgPoint(event), pos = positions.get(id); dragState = { id, element, startX: point.x, startY: point.y, originalX: pos.x, originalY: pos.y, moved: false }; }
419 function moveDrag(event) { if (!dragState) return; const point = svgPoint(event), dx = (point.x - dragState.startX) / scale, dy = (point.y - dragState.startY) / scale; if (Math.abs(dx) + Math.abs(dy) > 3) dragState.moved = true; positions.set(dragState.id, { x: dragState.originalX + dx, y: dragState.originalY + dy }); redraw(); }
420 function endDrag() { dragState = null; }
421
422 search.addEventListener("input", applyState);
423 document.getElementById("zoomIn").addEventListener("click", () => zoomAt(scale + .15));
424 document.getElementById("zoomOut").addEventListener("click", () => zoomAt(scale - .15));
425 document.getElementById("resetView").addEventListener("click", resetView);
426 document.getElementById("themeToggle").addEventListener("click", () => { document.body.dataset.theme = document.body.dataset.theme === "dark" ? "light" : "dark"; });
427 svg.addEventListener("wheel", event => { event.preventDefault(); const point = svgPoint(event); zoomAt(scale + (event.deltaY < 0 ? .1 : -.1), point.x, point.y); }, { passive: false });
428 svg.addEventListener("pointermove", moveDrag);
429 svg.addEventListener("pointerup", endDrag);
430 svg.addEventListener("pointercancel", endDrag);
431 svg.addEventListener("click", event => { if (event.target === svg) { selectedId = null; detail.innerHTML = `<div class="empty-detail"><div><strong>选择一个节点</strong><br>查看职责、关键文件和上下游关系。</div></div>`; applyState(); } });
432
433 makeFilters();
434 resetView();
435 </script>
436</body>
437</html>