@yss123/cage-cli 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +18 -2
- package/bin/cage.js +6 -0
- package/bin/create-cage-app.js +6 -0
- package/dist/analyze.d.ts +18 -0
- package/dist/analyze.js +26 -0
- package/dist/analyze.js.map +1 -0
- package/dist/baseline.d.ts +5 -0
- package/dist/baseline.js +50 -0
- package/dist/baseline.js.map +1 -0
- package/dist/budget.d.ts +7 -0
- package/dist/budget.js +52 -0
- package/dist/budget.js.map +1 -0
- package/dist/config.d.ts +30 -0
- package/dist/config.js +47 -0
- package/dist/config.js.map +1 -0
- package/dist/create.d.ts +1 -0
- package/dist/create.js +47 -0
- package/dist/create.js.map +1 -0
- package/dist/database.d.ts +5 -0
- package/dist/database.js +70 -0
- package/dist/database.js.map +1 -0
- package/dist/derive.d.ts +14 -0
- package/dist/derive.js +163 -0
- package/dist/derive.js.map +1 -0
- package/dist/dev.d.ts +2 -0
- package/dist/dev.js +155 -0
- package/dist/dev.js.map +1 -0
- package/dist/env.d.ts +1 -0
- package/dist/env.js +15 -0
- package/dist/env.js.map +1 -0
- package/dist/findings.d.ts +11 -0
- package/dist/findings.js +13 -0
- package/dist/findings.js.map +1 -0
- package/dist/gen/contracts.d.ts +6 -0
- package/dist/gen/contracts.js +167 -0
- package/dist/gen/contracts.js.map +1 -0
- package/dist/gen/index.d.ts +19 -0
- package/dist/gen/index.js +137 -0
- package/dist/gen/index.js.map +1 -0
- package/dist/gen/zod-code.d.ts +7 -0
- package/dist/gen/zod-code.js +161 -0
- package/dist/gen/zod-code.js.map +1 -0
- package/dist/graph-snapshot.d.ts +13 -0
- package/dist/graph-snapshot.js +46 -0
- package/dist/graph-snapshot.js.map +1 -0
- package/dist/graph.d.ts +11 -0
- package/dist/graph.js +87 -0
- package/dist/graph.js.map +1 -0
- package/dist/impact.d.ts +28 -0
- package/dist/impact.js +223 -0
- package/dist/impact.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/load.d.ts +6 -0
- package/dist/load.js +17 -0
- package/dist/load.js.map +1 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +176 -0
- package/dist/main.js.map +1 -0
- package/dist/migrate.d.ts +7 -0
- package/dist/migrate.js +69 -0
- package/dist/migrate.js.map +1 -0
- package/dist/ontology.d.ts +41 -0
- package/dist/ontology.js +270 -0
- package/dist/ontology.js.map +1 -0
- package/dist/process.d.ts +6 -0
- package/dist/process.js +39 -0
- package/dist/process.js.map +1 -0
- package/dist/rules.d.ts +23 -0
- package/dist/rules.js +251 -0
- package/dist/rules.js.map +1 -0
- package/dist/static-check.d.ts +23 -0
- package/dist/static-check.js +225 -0
- package/dist/static-check.js.map +1 -0
- package/dist/sync.d.ts +5 -0
- package/dist/sync.js +46 -0
- package/dist/sync.js.map +1 -0
- package/dist/upgrade.d.ts +10 -0
- package/dist/upgrade.js +81 -0
- package/dist/upgrade.js.map +1 -0
- package/dist/verify.d.ts +7 -0
- package/dist/verify.js +88 -0
- package/dist/verify.js.map +1 -0
- package/dist/version.d.ts +4 -0
- package/dist/version.js +6 -0
- package/dist/version.js.map +1 -0
- package/graph-ui/app.js +1065 -0
- package/graph-ui/index.html +197 -0
- package/graph-ui/model.js +141 -0
- package/graph-ui/relations.js +125 -0
- package/graph-ui/structure.js +74 -0
- package/graph-ui/style.css +1698 -0
- package/managed/errors/CAGE-APPR-001.md +32 -0
- package/managed/errors/CAGE-APPR-002.md +32 -0
- package/managed/errors/CAGE-APPR-003.md +32 -0
- package/managed/errors/CAGE-APPR-004.md +32 -0
- package/managed/errors/CAGE-APPR-005.md +32 -0
- package/managed/errors/CAGE-APPR-006.md +32 -0
- package/managed/errors/CAGE-APPR-007.md +32 -0
- package/managed/errors/CAGE-APPR-010.md +32 -0
- package/managed/errors/CAGE-AUTH-001.md +32 -0
- package/managed/errors/CAGE-BUDGET-001.md +32 -0
- package/managed/errors/CAGE-BUDGET-002.md +32 -0
- package/managed/errors/CAGE-BUDGET-003.md +32 -0
- package/managed/errors/CAGE-CMD-002.md +32 -0
- package/managed/errors/CAGE-CMD-003.md +34 -0
- package/managed/errors/CAGE-CONN-001.md +32 -0
- package/managed/errors/CAGE-CONN-002.md +32 -0
- package/managed/errors/CAGE-CONN-003.md +32 -0
- package/managed/errors/CAGE-CONN-004.md +32 -0
- package/managed/errors/CAGE-CONN-005.md +32 -0
- package/managed/errors/CAGE-CONN-006.md +32 -0
- package/managed/errors/CAGE-CONN-007.md +32 -0
- package/managed/errors/CAGE-CONN-008.md +32 -0
- package/managed/errors/CAGE-CONN-009.md +32 -0
- package/managed/errors/CAGE-CONN-010.md +32 -0
- package/managed/errors/CAGE-CONN-011.md +32 -0
- package/managed/errors/CAGE-CONN-012.md +32 -0
- package/managed/errors/CAGE-CREATE-001.md +32 -0
- package/managed/errors/CAGE-DECL-001.md +34 -0
- package/managed/errors/CAGE-DECL-002.md +34 -0
- package/managed/errors/CAGE-DECL-003.md +34 -0
- package/managed/errors/CAGE-DECL-004.md +34 -0
- package/managed/errors/CAGE-DEP-001.md +34 -0
- package/managed/errors/CAGE-DEP-002.md +34 -0
- package/managed/errors/CAGE-DEP-003.md +34 -0
- package/managed/errors/CAGE-DEP-004.md +34 -0
- package/managed/errors/CAGE-DEP-005.md +34 -0
- package/managed/errors/CAGE-DLQ-001.md +32 -0
- package/managed/errors/CAGE-ENV-001.md +32 -0
- package/managed/errors/CAGE-EVENT-001.md +32 -0
- package/managed/errors/CAGE-EVENT-002.md +32 -0
- package/managed/errors/CAGE-FIELD-001.md +32 -0
- package/managed/errors/CAGE-FIELD-002.md +32 -0
- package/managed/errors/CAGE-FIELD-003.md +32 -0
- package/managed/errors/CAGE-FIELD-004.md +32 -0
- package/managed/errors/CAGE-GEN-001.md +33 -0
- package/managed/errors/CAGE-GEN-002.md +33 -0
- package/managed/errors/CAGE-GRANT-001.md +32 -0
- package/managed/errors/CAGE-GRANT-002.md +32 -0
- package/managed/errors/CAGE-GRANT-003.md +32 -0
- package/managed/errors/CAGE-IMPACT-001.md +32 -0
- package/managed/errors/CAGE-JOB-001.md +32 -0
- package/managed/errors/CAGE-JOB-002.md +32 -0
- package/managed/errors/CAGE-JOB-003.md +32 -0
- package/managed/errors/CAGE-LINT-001.md +32 -0
- package/managed/errors/CAGE-MIG-001.md +32 -0
- package/managed/errors/CAGE-MIG-002.md +32 -0
- package/managed/errors/CAGE-MIG-003.md +32 -0
- package/managed/errors/CAGE-MIG-004.md +32 -0
- package/managed/errors/CAGE-MIG-005.md +32 -0
- package/managed/errors/CAGE-NAME-001.md +32 -0
- package/managed/errors/CAGE-NAME-002.md +32 -0
- package/managed/errors/CAGE-NAME-003.md +32 -0
- package/managed/errors/CAGE-NAME-004.md +32 -0
- package/managed/errors/CAGE-NAME-005.md +32 -0
- package/managed/errors/CAGE-NAME-006.md +32 -0
- package/managed/errors/CAGE-ORG-001.md +32 -0
- package/managed/errors/CAGE-PAIR-002.md +33 -0
- package/managed/errors/CAGE-QUERY-001.md +32 -0
- package/managed/errors/CAGE-QUERY-002.md +32 -0
- package/managed/errors/CAGE-QUERY-003.md +32 -0
- package/managed/errors/CAGE-REACT-001.md +32 -0
- package/managed/errors/CAGE-REACT-002.md +32 -0
- package/managed/errors/CAGE-READS-001.md +32 -0
- package/managed/errors/CAGE-REG-001.md +32 -0
- package/managed/errors/CAGE-REG-002.md +32 -0
- package/managed/errors/CAGE-REG-003.md +32 -0
- package/managed/errors/CAGE-ROUTE-001.md +32 -0
- package/managed/errors/CAGE-SCOPE-001.md +32 -0
- package/managed/errors/CAGE-SCOPE-002.md +32 -0
- package/managed/errors/CAGE-SECRET-001.md +32 -0
- package/managed/errors/CAGE-SECRET-002.md +32 -0
- package/managed/errors/CAGE-SENS-001.md +32 -0
- package/managed/errors/CAGE-SQL-001.md +32 -0
- package/managed/errors/CAGE-SQL-002.md +32 -0
- package/managed/errors/CAGE-STATE-001.md +32 -0
- package/managed/errors/CAGE-STATE-002.md +32 -0
- package/managed/errors/CAGE-STATE-003.md +32 -0
- package/managed/errors/CAGE-STATE-004.md +32 -0
- package/managed/errors/CAGE-STATE-005.md +32 -0
- package/managed/errors/CAGE-STATE-010.md +32 -0
- package/managed/errors/CAGE-STATE-011.md +32 -0
- package/managed/errors/CAGE-SYNC-001.md +35 -0
- package/managed/errors/CAGE-TEST-001.md +33 -0
- package/managed/errors/CAGE-UNSAFE-001.md +32 -0
- package/managed/errors/CAGE-UPGRADE-001.md +32 -0
- package/managed/errors/README.md +99 -0
- package/package.json +60 -3
- package/templates/app/.dockerignore +10 -0
- package/templates/app/.env.example +17 -0
- package/templates/app/.github/workflows/cage.yml +44 -0
- package/templates/app/AGENTS.md +13 -0
- package/templates/app/Dockerfile +23 -0
- package/templates/app/README.md +58 -0
- package/templates/app/apps/api/package.json +16 -0
- package/templates/app/apps/api/src/main.ts +10 -0
- package/templates/app/apps/api/src/worker.ts +14 -0
- package/templates/app/apps/api/tsconfig.json +5 -0
- package/templates/app/apps/web/index.html +13 -0
- package/templates/app/apps/web/package.json +22 -0
- package/templates/app/apps/web/src/main.tsx +552 -0
- package/templates/app/apps/web/src/style.css +663 -0
- package/templates/app/apps/web/tsconfig.json +11 -0
- package/templates/app/apps/web/vite.config.ts +16 -0
- package/templates/app/cage.config.json +114 -0
- package/templates/app/compose.yaml +39 -0
- package/templates/app/db/migrations/0001_init.sql +112 -0
- package/templates/app/package.json +25 -0
- package/templates/app/packages/contracts/package.json +16 -0
- package/templates/app/packages/contracts/src/dept.ts +74 -0
- package/templates/app/packages/contracts/src/index.ts +81 -0
- package/templates/app/packages/contracts/src/inventory-lock.ts +52 -0
- package/templates/app/packages/contracts/src/inventory.ts +57 -0
- package/templates/app/packages/contracts/src/purchase-order.ts +72 -0
- package/templates/app/packages/contracts/src/queries.ts +17 -0
- package/templates/app/packages/contracts/src/role.ts +85 -0
- package/templates/app/packages/contracts/src/user.ts +89 -0
- package/templates/app/packages/contracts/tsconfig.json +5 -0
- package/templates/app/packages/domain/package.json +23 -0
- package/templates/app/packages/domain/src/approvals.ts +18 -0
- package/templates/app/packages/domain/src/erp.ts +36 -0
- package/templates/app/packages/domain/src/index.ts +9 -0
- package/templates/app/packages/domain/src/inventory.ts +43 -0
- package/templates/app/packages/domain/src/jobs.ts +13 -0
- package/templates/app/packages/domain/src/purchase-order.test.ts +137 -0
- package/templates/app/packages/domain/src/purchase-order.ts +122 -0
- package/templates/app/packages/domain/src/queries.ts +10 -0
- package/templates/app/packages/domain/src/reactions.ts +24 -0
- package/templates/app/packages/domain/tsconfig.json +5 -0
- package/templates/app/pnpm-workspace.yaml +10 -0
- package/templates/app/renovate.json +5 -0
- package/templates/app/tsconfig.base.json +8 -0
|
@@ -0,0 +1,197 @@
|
|
|
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
|
+
<meta name="theme-color" content="#f6f7f3" />
|
|
7
|
+
<title>Cage Atlas · 本体图谱</title>
|
|
8
|
+
<link
|
|
9
|
+
rel="icon"
|
|
10
|
+
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Crect width='40' height='40' rx='10' fill='%232d665a'/%3E%3Cpath d='M11 12h18v16H11zm0 8h18m-9-8v16' stroke='%23fff' fill='none' stroke-width='2'/%3E%3C/svg%3E"
|
|
11
|
+
/>
|
|
12
|
+
<link rel="stylesheet" href="/style.css" />
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<div class="app-shell">
|
|
16
|
+
<header class="topbar">
|
|
17
|
+
<a class="brand" href="/" aria-label="Cage Atlas 首页"
|
|
18
|
+
><span class="brand-mark"><i></i><i></i><i></i><i></i></span
|
|
19
|
+
><strong>CAGE<span>ATLAS</span></strong></a
|
|
20
|
+
>
|
|
21
|
+
<div class="breadcrumb">
|
|
22
|
+
<span>开发者工作台</span><b>/</b><span id="project-name">正在读取项目</span>
|
|
23
|
+
</div>
|
|
24
|
+
<span class="local-badge"><i></i>本地声明</span>
|
|
25
|
+
<button
|
|
26
|
+
class="icon-button mobile-filters"
|
|
27
|
+
id="toggle-sidebar"
|
|
28
|
+
aria-label="展开筛选"
|
|
29
|
+
aria-expanded="false"
|
|
30
|
+
>
|
|
31
|
+
☰
|
|
32
|
+
</button>
|
|
33
|
+
</header>
|
|
34
|
+
|
|
35
|
+
<aside class="sidebar" id="sidebar">
|
|
36
|
+
<div class="section-label">项目工作区 <span>01</span></div>
|
|
37
|
+
<div class="nav-active"><span class="nav-symbol">◇</span> 本体图谱 <span class="nav-dot"></span></div>
|
|
38
|
+
<div class="sidebar-heading">业务对象 <span id="aggregate-total"></span></div>
|
|
39
|
+
<div id="aggregate-list" class="aggregate-list"></div>
|
|
40
|
+
<div class="sidebar-heading" id="types-heading">
|
|
41
|
+
节点类型 <button id="reset-types" class="text-button">重置</button>
|
|
42
|
+
</div>
|
|
43
|
+
<div id="type-filters" class="type-filters"></div>
|
|
44
|
+
<div id="structure-note" class="structure-note">
|
|
45
|
+
<strong>一张卡片,一个业务对象</strong>
|
|
46
|
+
<p>字段收在卡片内,引用从字段连出。点击字段可查看业务用途。</p>
|
|
47
|
+
<small>* 必填 ↗ 引用字段</small>
|
|
48
|
+
</div>
|
|
49
|
+
<label class="system-toggle"
|
|
50
|
+
><input id="system-toggle" type="checkbox" /><span>包含系统组织模型</span></label
|
|
51
|
+
>
|
|
52
|
+
<div class="sidebar-note">
|
|
53
|
+
<span class="note-icon">⌘</span>
|
|
54
|
+
<div>
|
|
55
|
+
<strong>声明即地图</strong>
|
|
56
|
+
<p>修改 domain 后重新抽取,<br />让关系与代码保持同步。</p>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
<a class="sidebar-footer" href="/api/graph" target="_blank" rel="noreferrer"
|
|
60
|
+
>查看原始图谱数据 <span>↗</span></a
|
|
61
|
+
>
|
|
62
|
+
</aside>
|
|
63
|
+
|
|
64
|
+
<main class="main">
|
|
65
|
+
<section class="page-heading">
|
|
66
|
+
<div>
|
|
67
|
+
<div class="eyebrow">业务关系探索</div>
|
|
68
|
+
<h1>本体图谱<span class="version-tag">v0.1</span></h1>
|
|
69
|
+
<p>从声明出发,看见业务如何连接。</p>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="heading-actions">
|
|
72
|
+
<button id="download" class="button secondary"><span>↓</span> 导出 JSON</button
|
|
73
|
+
><button id="refresh" class="button primary"><span class="refresh-icon">↻</span> 重新抽取</button>
|
|
74
|
+
</div>
|
|
75
|
+
</section>
|
|
76
|
+
<div id="notice" class="notice" role="status" hidden></div>
|
|
77
|
+
<section class="overview" aria-label="图谱统计">
|
|
78
|
+
<div class="overview-item">
|
|
79
|
+
<span class="metric-icon aggregate-icon">◇</span>
|
|
80
|
+
<div><small>业务对象</small><strong id="metric-aggregates">—</strong></div>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="overview-item">
|
|
83
|
+
<span class="metric-icon node-icon">⠿</span>
|
|
84
|
+
<div><small>声明节点</small><strong id="metric-nodes">—</strong></div>
|
|
85
|
+
</div>
|
|
86
|
+
<div class="overview-item">
|
|
87
|
+
<span class="metric-icon edge-icon">↗</span>
|
|
88
|
+
<div><small>声明关系</small><strong id="metric-edges">—</strong></div>
|
|
89
|
+
</div>
|
|
90
|
+
<div class="overview-health">
|
|
91
|
+
<span id="health-dot" class="health-dot"></span>
|
|
92
|
+
<div><strong id="health">正在抽取声明</strong><small id="updated">稍候即可探索图谱</small></div>
|
|
93
|
+
<button id="show-findings" class="text-button" hidden>查看问题</button>
|
|
94
|
+
</div>
|
|
95
|
+
</section>
|
|
96
|
+
|
|
97
|
+
<section class="explorer" aria-label="交互式本体图谱">
|
|
98
|
+
<div class="graph-panel">
|
|
99
|
+
<div class="graph-toolbar">
|
|
100
|
+
<div class="tabs" role="group" aria-label="图谱视图">
|
|
101
|
+
<button data-mode="structure" class="active" aria-pressed="true">对象与字段</button
|
|
102
|
+
><button data-mode="business" aria-pressed="false">业务流程</button
|
|
103
|
+
><button data-mode="all" aria-pressed="false">完整本体</button>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="search-wrap">
|
|
106
|
+
<span aria-hidden="true">⌕</span
|
|
107
|
+
><input
|
|
108
|
+
id="search"
|
|
109
|
+
type="search"
|
|
110
|
+
placeholder="搜索中文名称或代码标识"
|
|
111
|
+
aria-label="搜索节点"
|
|
112
|
+
autocomplete="off"
|
|
113
|
+
/><kbd>/</kbd>
|
|
114
|
+
<div id="search-results" class="search-results" hidden></div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
<div class="graph-subbar">
|
|
118
|
+
<span
|
|
119
|
+
><i class="tiny-dot"></i><strong id="view-title">全部业务</strong
|
|
120
|
+
><span id="view-count"></span></span
|
|
121
|
+
><button id="exit-focus" class="text-button" hidden>退出聚焦 ×</button
|
|
122
|
+
><span class="drag-hint">点击连线看解释 · 拖拽节点 · 滚轮缩放</span>
|
|
123
|
+
</div>
|
|
124
|
+
<div class="relation-guide">
|
|
125
|
+
<span>↗</span><span id="relation-guide">箭头按线上的文字阅读;点击连线查看原因和用途。</span>
|
|
126
|
+
</div>
|
|
127
|
+
<div id="canvas" class="canvas">
|
|
128
|
+
<svg id="graph" role="group" aria-label="本体节点与依赖关系" xmlns="http://www.w3.org/2000/svg">
|
|
129
|
+
<defs>
|
|
130
|
+
<marker
|
|
131
|
+
id="arrow"
|
|
132
|
+
viewBox="0 0 10 10"
|
|
133
|
+
refX="9"
|
|
134
|
+
refY="5"
|
|
135
|
+
markerWidth="5"
|
|
136
|
+
markerHeight="5"
|
|
137
|
+
orient="auto-start-reverse"
|
|
138
|
+
>
|
|
139
|
+
<path d="M0 0L10 5L0 10z" fill="#a4b4ad" />
|
|
140
|
+
</marker>
|
|
141
|
+
<marker
|
|
142
|
+
id="arrow-active"
|
|
143
|
+
viewBox="0 0 10 10"
|
|
144
|
+
refX="9"
|
|
145
|
+
refY="5"
|
|
146
|
+
markerWidth="5"
|
|
147
|
+
markerHeight="5"
|
|
148
|
+
orient="auto-start-reverse"
|
|
149
|
+
>
|
|
150
|
+
<path d="M0 0L10 5L0 10z" fill="#357c6c" />
|
|
151
|
+
</marker>
|
|
152
|
+
</defs>
|
|
153
|
+
<g id="viewport">
|
|
154
|
+
<g id="edges"></g>
|
|
155
|
+
<g id="nodes"></g>
|
|
156
|
+
</g>
|
|
157
|
+
</svg>
|
|
158
|
+
<div id="empty" class="empty-state" hidden>
|
|
159
|
+
<span>◇</span><strong>当前筛选下没有节点</strong>
|
|
160
|
+
<p>调整左侧类型,或恢复默认业务视图。</p>
|
|
161
|
+
<button id="reset-view" class="button secondary">恢复视图</button>
|
|
162
|
+
</div>
|
|
163
|
+
<div id="loading" class="loading-state">
|
|
164
|
+
<span class="loader"></span><strong>正在展开业务地图</strong>
|
|
165
|
+
</div>
|
|
166
|
+
<div class="canvas-signature">CAGE <span>/</span> 业务声明关系图</div>
|
|
167
|
+
<div class="zoom-controls" aria-label="画布操作">
|
|
168
|
+
<button id="zoom-out" aria-label="缩小">−</button><span id="zoom-level">100%</span
|
|
169
|
+
><button id="zoom-in" aria-label="放大">+</button><i></i
|
|
170
|
+
><button id="fit" aria-label="适应画布" title="适应画布">⛶</button
|
|
171
|
+
><button id="relayout" aria-label="重置布局" title="重置布局">↺</button>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
<footer class="graph-footer">
|
|
175
|
+
<div id="edge-legend">
|
|
176
|
+
<span class="legend-line"></span> 声明关系 <span class="legend-line dashed"></span> 视图归属
|
|
177
|
+
</div>
|
|
178
|
+
<span id="canvas-status">读取声明中</span>
|
|
179
|
+
</footer>
|
|
180
|
+
</div>
|
|
181
|
+
<aside id="inspector" class="inspector" aria-label="节点与关系详情" aria-live="polite"></aside>
|
|
182
|
+
</section>
|
|
183
|
+
<footer class="page-footer">
|
|
184
|
+
<span id="domain-path">packages/domain/src/index.ts</span><span>只读探索 · 不连接业务数据库</span>
|
|
185
|
+
</footer>
|
|
186
|
+
</main>
|
|
187
|
+
</div>
|
|
188
|
+
<dialog id="findings-dialog">
|
|
189
|
+
<div class="dialog-heading">
|
|
190
|
+
<h2>声明检查</h2>
|
|
191
|
+
<button id="close-findings" class="icon-button" aria-label="关闭检查结果">×</button>
|
|
192
|
+
</div>
|
|
193
|
+
<div id="findings-list"></div>
|
|
194
|
+
</dialog>
|
|
195
|
+
<script type="module" src="/app.js"></script>
|
|
196
|
+
</body>
|
|
197
|
+
</html>
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
export const kinds = {
|
|
2
|
+
aggregate: { label: '业务对象', color: '#197c72', short: '表', column: 0 },
|
|
3
|
+
command: { label: '命令', color: '#547db2', short: '令', column: 1 },
|
|
4
|
+
event: { label: '事件', color: '#c08c36', short: '事', column: 2 },
|
|
5
|
+
reaction: { label: '反应', color: '#aa7288', short: '应', column: 3 },
|
|
6
|
+
query: { label: '查询', color: '#648c5b', short: '查', column: 1 },
|
|
7
|
+
approval: { label: '审批', color: '#9c739f', short: '审', column: 3 },
|
|
8
|
+
route: { label: '审批路由', color: '#9c739f', short: '路', column: 3 },
|
|
9
|
+
job: { label: '定时任务', color: '#7f82a0', short: '时', column: 0 },
|
|
10
|
+
connector: { label: '连接器', color: '#b57452', short: '连', column: 4 },
|
|
11
|
+
outbound: { label: '出站映射', color: '#b57452', short: '出', column: 3 },
|
|
12
|
+
inbound: { label: '入站回调', color: '#b57452', short: '入', column: 4 },
|
|
13
|
+
field: { label: '字段', color: '#67998f', short: '字', column: 1 },
|
|
14
|
+
state: { label: '状态', color: '#898b69', short: '态', column: 2 },
|
|
15
|
+
capability: { label: '能力点', color: '#81949f', short: '权', column: 1 },
|
|
16
|
+
entry: { label: '新建入口', color: '#89938e', short: '+', column: 0 },
|
|
17
|
+
};
|
|
18
|
+
export const edgeLabels = {
|
|
19
|
+
transition: '状态迁移',
|
|
20
|
+
reads: '读取',
|
|
21
|
+
emits: '发出',
|
|
22
|
+
uses: '使用字段',
|
|
23
|
+
triggers: '触发',
|
|
24
|
+
targets: '调用目标',
|
|
25
|
+
queries: '查询',
|
|
26
|
+
refs: '引用',
|
|
27
|
+
calls: '调用',
|
|
28
|
+
binds: '审批绑定',
|
|
29
|
+
field: '声明字段',
|
|
30
|
+
ownership: '归属(视图推导)',
|
|
31
|
+
};
|
|
32
|
+
/** 中文用于展示,英文仍作为复制、搜索和查源码的稳定标识。
|
|
33
|
+
* 没有配置业务名称的未知节点保留原标识,不能猜测业务含义后悄悄改名。 */
|
|
34
|
+
export function nodeLabel(node, labels = {}) {
|
|
35
|
+
if (node.kind === 'entry')
|
|
36
|
+
return '新建入口';
|
|
37
|
+
return labels[node.key] || String(node.attrs.description || node.id);
|
|
38
|
+
}
|
|
39
|
+
export function matchesNode(node, query, labels) {
|
|
40
|
+
return `${nodeLabel(node, labels)} ${node.id} ${kinds[node.kind].label}`
|
|
41
|
+
.toLowerCase()
|
|
42
|
+
.includes(query.toLowerCase());
|
|
43
|
+
}
|
|
44
|
+
export function projectGraph(graph) {
|
|
45
|
+
const nodes = graph.nodes.map((n) => ({ ...n, key: `${n.kind}:${n.id}` }));
|
|
46
|
+
const edges = [];
|
|
47
|
+
// 节点身份包含 kind。能力点和命令可能同名,不能单用显示名作为 SVG 的主键。
|
|
48
|
+
const find = (id) => nodes.find((n) => n.id === id && n.kind !== 'capability') ?? nodes.find((n) => n.id === id);
|
|
49
|
+
for (const e of graph.edges) {
|
|
50
|
+
let from = find(e.from);
|
|
51
|
+
const to = find(e.to);
|
|
52
|
+
// [new] 是迁移的起点,并不属于声明状态;在视图中显式标为虚拟入口。
|
|
53
|
+
if (!from && e.kind === 'transition' && e.from.endsWith('.[new]')) {
|
|
54
|
+
from = { key: `entry:${e.from}`, id: e.from, kind: 'entry', hash: '', attrs: {} };
|
|
55
|
+
nodes.push(from);
|
|
56
|
+
}
|
|
57
|
+
if (from && to)
|
|
58
|
+
edges.push({ ...e, from: from.key, to: to.key, key: `edge:${edges.length}`, derived: false });
|
|
59
|
+
}
|
|
60
|
+
for (const n of nodes) {
|
|
61
|
+
if (!['command', 'state', 'capability', 'entry'].includes(n.kind))
|
|
62
|
+
continue;
|
|
63
|
+
const owner = nodes.find((a) => a.kind === 'aggregate' && n.id.startsWith(`${a.id}.`));
|
|
64
|
+
if (owner)
|
|
65
|
+
edges.push({ key: `owner:${n.key}`, kind: 'ownership', from: owner.key, to: n.key, derived: true });
|
|
66
|
+
}
|
|
67
|
+
return { nodes, edges };
|
|
68
|
+
}
|
|
69
|
+
export const isSystem = (n) => /^(User|Role|Dept)(\.|$)/.test(n.id);
|
|
70
|
+
export function neighborhood(model, key) {
|
|
71
|
+
const connected = new Set([key]);
|
|
72
|
+
for (const e of model.edges) {
|
|
73
|
+
if (e.from === key)
|
|
74
|
+
connected.add(e.to);
|
|
75
|
+
if (e.to === key)
|
|
76
|
+
connected.add(e.from);
|
|
77
|
+
}
|
|
78
|
+
return connected;
|
|
79
|
+
}
|
|
80
|
+
export const NODE_WIDTH = 190;
|
|
81
|
+
export const NODE_HEIGHT = 56;
|
|
82
|
+
/** 按类型分组的平衡列 + 邻接排序。相同声明每次打开位置相同,没有持续运动的力导向
|
|
83
|
+
* 动画;读长名称、比较刷新前后的关系时不会被随机布局打断。 */
|
|
84
|
+
export function layoutGraph(nodes, edges, focusKey) {
|
|
85
|
+
const positions = new Map();
|
|
86
|
+
// 小范围聚焦按关系方向排布:依赖方在左、当前节点居中、目标在右。
|
|
87
|
+
// 大图继续使用平衡列,避免高连接度聚合把所有邻居挤成一条长列。
|
|
88
|
+
if (focusKey && nodes.some((n) => n.key === focusKey) && nodes.length <= 12) {
|
|
89
|
+
positions.set(focusKey, { x: 330, y: 0 });
|
|
90
|
+
const incoming = new Set(edges.filter((e) => e.to === focusKey).map((e) => e.from));
|
|
91
|
+
const neighbors = nodes.filter((n) => n.key !== focusKey);
|
|
92
|
+
for (const [column, items] of [
|
|
93
|
+
neighbors.filter((n) => incoming.has(n.key)),
|
|
94
|
+
neighbors.filter((n) => !incoming.has(n.key)),
|
|
95
|
+
].entries()) {
|
|
96
|
+
items.forEach((n, row) => positions.set(n.key, { x: column * 660, y: (row - (items.length - 1) / 2) * 105 }));
|
|
97
|
+
}
|
|
98
|
+
return positions;
|
|
99
|
+
}
|
|
100
|
+
// 命令通常远多于其他节点;强制“一类一列”会形成极长列,适应画布后字体
|
|
101
|
+
// 小到无法阅读。保持语义排序,再均衡分列,让概览尽量利用二维空间。
|
|
102
|
+
const ordered = [...nodes].sort((a, b) => kinds[a.kind].column - kinds[b.kind].column || a.id.localeCompare(b.id));
|
|
103
|
+
const columnCount = Math.min(4, Math.max(1, Math.ceil(Math.sqrt(nodes.length / 1.6))));
|
|
104
|
+
const perColumn = Math.ceil(nodes.length / columnCount);
|
|
105
|
+
const columns = Array.from({ length: columnCount }, (_, i) => ordered.slice(i * perColumn, (i + 1) * perColumn));
|
|
106
|
+
const place = () => columns.forEach((items, column) => items.forEach((n, row) => {
|
|
107
|
+
positions.set(n.key, { x: column * 330, y: (row - (items.length - 1) / 2) * 105 });
|
|
108
|
+
}));
|
|
109
|
+
place();
|
|
110
|
+
// 只做排序,节点仍各占一行;邻居平均纵坐标让同一业务链尽量接近。
|
|
111
|
+
for (let pass = 0; pass < 4; pass++) {
|
|
112
|
+
for (const items of columns) {
|
|
113
|
+
const score = (node) => {
|
|
114
|
+
const ys = edges
|
|
115
|
+
.filter((e) => e.from === node.key || e.to === node.key)
|
|
116
|
+
.map((e) => positions.get(e.from === node.key ? e.to : e.from)?.y)
|
|
117
|
+
.filter((y) => y !== undefined);
|
|
118
|
+
return ys.length ? ys.reduce((a, b) => a + b, 0) / ys.length : 0;
|
|
119
|
+
};
|
|
120
|
+
items.sort((a, b) => score(a) - score(b) || a.id.localeCompare(b.id));
|
|
121
|
+
place();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return positions;
|
|
125
|
+
}
|
|
126
|
+
export function edgePath(from, to, self) {
|
|
127
|
+
if (self)
|
|
128
|
+
return `M${from.x + NODE_WIDTH - 25},${from.y} C${from.x + NODE_WIDTH + 50},${from.y - 65} ${from.x + NODE_WIDTH + 50},${from.y + 100} ${from.x + NODE_WIDTH - 25},${from.y + NODE_HEIGHT}`;
|
|
129
|
+
// 同列关系从右侧绕行,给标签留出空间,也避免穿过中间节点。
|
|
130
|
+
if (from.x === to.x) {
|
|
131
|
+
const x = from.x + NODE_WIDTH;
|
|
132
|
+
const y1 = from.y + NODE_HEIGHT / 2, y2 = to.y + NODE_HEIGHT / 2;
|
|
133
|
+
const bend = 110 + Math.abs(y2 - y1) * 0.12;
|
|
134
|
+
return `M${x},${y1} C${x + bend},${y1} ${x + bend},${y2} ${x},${y2}`;
|
|
135
|
+
}
|
|
136
|
+
const forward = to.x >= from.x;
|
|
137
|
+
const x1 = from.x + (forward ? NODE_WIDTH : 0), x2 = to.x + (forward ? 0 : NODE_WIDTH);
|
|
138
|
+
const y1 = from.y + NODE_HEIGHT / 2, y2 = to.y + NODE_HEIGHT / 2;
|
|
139
|
+
const bend = Math.max(60, Math.abs(x2 - x1) / 2) * (forward ? 1 : -1);
|
|
140
|
+
return `M${x1},${y1} C${x1 + bend},${y1} ${x2 - bend},${y2} ${x2},${y2}`;
|
|
141
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { kinds, nodeLabel } from './model.js';
|
|
2
|
+
export function explainEdge(edge, model, labels = {}) {
|
|
3
|
+
const from = model.nodes.find((n) => n.key === edge.from);
|
|
4
|
+
const to = model.nodes.find((n) => n.key === edge.to);
|
|
5
|
+
const a = nodeLabel(from, labels), b = nodeLabel(to, labels);
|
|
6
|
+
const field = to.id.slice(to.id.indexOf('.') + 1);
|
|
7
|
+
const resource = (id) => labels[`resource:${id}`] ?? id;
|
|
8
|
+
const result = (label, category, why, purpose, evidence, source = from) => ({
|
|
9
|
+
label,
|
|
10
|
+
category,
|
|
11
|
+
why,
|
|
12
|
+
purpose,
|
|
13
|
+
evidence,
|
|
14
|
+
source,
|
|
15
|
+
direction: `“${a}” → “${b}”:${label}。`,
|
|
16
|
+
});
|
|
17
|
+
switch (edge.kind) {
|
|
18
|
+
case 'field':
|
|
19
|
+
return result('包含字段', '数据结构', `“${a}”的字段声明中包含“${b}”。`, '说明业务对象由哪些数据组成;调整字段时,可以继续沿线查看受影响的查询、审批和事件。', [`${from.id}.fields.${field}`]);
|
|
20
|
+
case 'refs':
|
|
21
|
+
return result('引用对象', '数据关联', `“${a}”声明为引用“${b}”的字段,保存所关联对象的标识。`, `用于理解这条数据关联到哪个${kinds[to.kind].label}。例如部门字段关联部门对象;具体查询仍受数据范围约束。这条线本身不执行查询。`, [`${from.id}: ref(${JSON.stringify(to.id)})`]);
|
|
22
|
+
case 'ownership':
|
|
23
|
+
return result(`拥有${kinds[to.kind].label}`, '视图归属 · 虚线', `“${b}”的标识位于“${from.id}.”之下,页面据此将它归入“${a}”。`, '帮助整理同一业务对象的命令、状态和能力点。这是页面推导的归属,不是抽取器声明的依赖,也不会触发命令。', [`${to.id} → 所属聚合 ${from.id}`]);
|
|
24
|
+
case 'transition': {
|
|
25
|
+
const command = model.nodes.find((n) => n.kind === 'command' && n.id === edge.via);
|
|
26
|
+
const name = nodeLabel(command, labels);
|
|
27
|
+
const stays = edge.from === edge.to;
|
|
28
|
+
return result(stays ? '保持状态' : '迁移状态', '状态变化', `“${name}”声明${stays ? `在“${a}”状态执行后保持原状态` : `从“${a}”进入“${b}”`}。`, `用于检查业务生命周期是否完整。只有命令通过权限、约束并成功提交,${stays ? '操作才生效' : '状态才改变'}。`, [
|
|
29
|
+
`命令:${command.id}`,
|
|
30
|
+
`from: ${JSON.stringify(command.attrs.from)}`,
|
|
31
|
+
`to: ${JSON.stringify(command.attrs.to)}`,
|
|
32
|
+
], command);
|
|
33
|
+
}
|
|
34
|
+
case 'reads':
|
|
35
|
+
case 'queries':
|
|
36
|
+
return result('读取数据', '读取依赖', `“${a}”在 reads 中声明读取“${b}”${edge.alias ? `,使用别名 ${edge.alias}` : ''}。`, '明确可读取的业务对象,并让影响分析找到依赖方;实际读取通过安全视图,受权限和数据范围约束。箭头指向数据来源,不表示先执行它。', [`${from.id}.reads${edge.alias ? `.${edge.alias}` : ''} → ${to.id}`]);
|
|
37
|
+
case 'emits':
|
|
38
|
+
return result('产生事件', '事件协作', `“${a}”的 emits 中声明了“${b}”事件。`, '命令成功执行时按声明生成事件,连接后续反应、审批或外部同步;事务失败时不能当作已经发生。', [`${from.id}.emits → ${to.id}`]);
|
|
39
|
+
case 'uses': {
|
|
40
|
+
const as = edge.as ?? '';
|
|
41
|
+
if (as === '可路由字段')
|
|
42
|
+
return result('允许审批读取', '审批可用字段', `“${a}”把“${b}”列入 routable,允许该字段作为审批流程变量。`, '明确审批可以拿到哪些业务数据。这里只表示允许使用;是否真的参与分支或审批选人,要查看审批路由的字段关系。', [`${from.id}.routable 包含 ${field}`]);
|
|
43
|
+
if (as === '路由条件') {
|
|
44
|
+
// fieldsUsed 同时包含 when 条件和 deptLeader 选人字段,不能一律解释成条件判断。
|
|
45
|
+
const nodes = from.attrs.nodes;
|
|
46
|
+
const conditions = nodes.filter((n) => n.kind === 'when' && n.cond.field === field);
|
|
47
|
+
const steps = nodes
|
|
48
|
+
.flatMap((n) => (n.kind === 'step' ? [n] : n.steps))
|
|
49
|
+
.filter((s) => s.approver.kind === 'deptLeader' && s.approver.field === field);
|
|
50
|
+
const operators = {
|
|
51
|
+
gt: '大于',
|
|
52
|
+
gte: '大于等于',
|
|
53
|
+
lt: '小于',
|
|
54
|
+
lte: '小于等于',
|
|
55
|
+
eq: '等于',
|
|
56
|
+
ne: '不等于',
|
|
57
|
+
in: '属于',
|
|
58
|
+
};
|
|
59
|
+
const reasons = [
|
|
60
|
+
...steps.map((s) => `“${s.title}”步骤根据“${b}”找到对应部门的负责人`),
|
|
61
|
+
...conditions.map((n) => `当“${b}”${operators[n.cond.op]} ${JSON.stringify(n.cond.value)} 时,加入${n.steps.map((s) => `“${s.title}”`).join('、')}步骤`),
|
|
62
|
+
];
|
|
63
|
+
return result(steps.length && conditions.length ? '选人和分支依据' : steps.length ? '确定审批人' : '判断审批分支', '审批路由依赖', `${reasons.join(';')}。`, '字段值会影响审批人或需要经过的审批步骤;修改字段含义时,应一起检查这些路由配置。', [
|
|
64
|
+
...steps.map((s) => `${s.title}: approver.deptLeader(${JSON.stringify(field)})`),
|
|
65
|
+
...conditions.map((n) => `when(${n.cond.op}(${JSON.stringify(field)}, ${JSON.stringify(n.cond.value)}))`),
|
|
66
|
+
]);
|
|
67
|
+
}
|
|
68
|
+
if (as === '事件载荷')
|
|
69
|
+
return result('携带字段', '事件数据依赖', `“${a}”的默认事件载荷按同名字段从业务对象取“${b}”。`, '订阅方可以用事件中的这份数据处理后续业务;字段变更需要检查事件及订阅方的数据约定。箭头指向载荷的数据来源。', [`事件 ${from.id} 的同名字段 ${field} ← ${to.id}`]);
|
|
70
|
+
if (as === '查询')
|
|
71
|
+
return result('查询使用字段', '查询字段线索', `抽取器在“${a}”的 SQL 或查询构造函数文本中匹配到了“${b}”的字段名。`, '提供检查查询依赖的线索。这里基于字段名文本匹配,可能匹配到注释或同名内容;实际筛选、分组或计算用途请核对查询声明。', [`${from.id}: SQL / build 文本匹配 ${field}`]);
|
|
72
|
+
if (as === '出站映射')
|
|
73
|
+
return result('映射外发字段', '外部同步数据', `“${a}”的 fields 声明包含“${b}”对应的字段名;抽取器将它关联到发出事件的聚合中同名字段。`, '用于核对外发字段范围和字段变更的同步影响;具体取值及转换由事件载荷与 map 函数决定。', [`${from.id}.fields 包含 ${field}`, `同名聚合字段:${to.id}`]);
|
|
74
|
+
if (as.startsWith('约束 ')) {
|
|
75
|
+
const rule = as.slice(3);
|
|
76
|
+
const purpose = rule.startsWith('ceiling(')
|
|
77
|
+
? `用于校验“${b}”是否超出调用人的审批额度。`
|
|
78
|
+
: rule.startsWith('sod.notSameAs(')
|
|
79
|
+
? `用于校验调用人与“${b}”记录的人不同,避免同一人完成需要分离的职责。`
|
|
80
|
+
: `该约束读取“${b}”执行命令前的业务检查;字段变更时应一起复核约束。`;
|
|
81
|
+
return result('执行约束检查', '命令校验依赖', `“${a}”声明的约束 ${rule} 使用“${b}”。`, purpose, [
|
|
82
|
+
`${from.id}.constraints → ${rule}`,
|
|
83
|
+
`约束 fields 包含 ${field}`,
|
|
84
|
+
]);
|
|
85
|
+
}
|
|
86
|
+
return result('使用字段', '字段依赖', `“${a}”声明使用“${b}”。`, '用于追踪字段变更的影响范围。', [
|
|
87
|
+
as || edge.kind,
|
|
88
|
+
]);
|
|
89
|
+
}
|
|
90
|
+
case 'triggers': {
|
|
91
|
+
const label = to.kind === 'approval' ? '启动审批' : to.kind === 'outbound' ? '触发外部同步' : '触发反应';
|
|
92
|
+
return result(label, '事件触发', `“${b}”订阅“${a}”作为${to.kind === 'approval' ? '启动' : '触发'}事件。`, to.kind === 'approval'
|
|
93
|
+
? '事件发生后启动对应审批流程,生成流程中的审批任务。'
|
|
94
|
+
: to.kind === 'outbound'
|
|
95
|
+
? '事件发生后由 worker 处理外部推送,失败按投递策略重试。'
|
|
96
|
+
: `事件发生后执行订阅的反应${to.attrs.tx === 'same' ? ',该反应与原命令处于同一事务' : ';执行方式由反应的事务声明决定'}。`, [`${to.id}.${to.kind === 'approval' ? 'start' : 'event'} → ${from.id}`], to);
|
|
97
|
+
}
|
|
98
|
+
case 'targets': {
|
|
99
|
+
if (to.kind === 'connector')
|
|
100
|
+
return result('调用外部系统', '连接器调用', `“${a}”声明通过“${b}”执行操作 ${labels[`operation:${from.attrs.operation}`] ?? from.attrs.operation}。`, '由连接器统一处理地址与字段白名单、密钥、限流和超时,让外部同步保持可管控。', [`${from.id}.connector → ${to.id}`, `operation: ${from.attrs.operation}`]);
|
|
101
|
+
if (from.kind === 'outbound')
|
|
102
|
+
return result('成功后回写', '同步成功回调', `“${a}”的 onSuccess 绑定“${b}”。`, '外部操作成功后调用该命令,将返回结果按映射写回业务;失败时不执行成功回写。', [`${from.id}.onSuccess → ${to.id}`]);
|
|
103
|
+
if (from.kind === 'inbound')
|
|
104
|
+
return result('回调转为命令', '外部回调', `“${a}”将接收的外部回调映射到“${b}”。`, '外部回调通过验签和数据校验后,使用业务命令处理,统一遵守状态和业务约束。', [`${from.id}.command → ${to.id}`]);
|
|
105
|
+
const resources = [
|
|
106
|
+
edge.acquires ? `获取“${resource(edge.acquires)}”` : '',
|
|
107
|
+
edge.releases ? `释放“${resource(edge.releases)}”` : '',
|
|
108
|
+
].filter(Boolean);
|
|
109
|
+
return result('调用业务命令', '反应调用', `“${a}”把事件映射后交给“${b}”处理。`, `将后续业务操作交给受约束的命令执行。${resources.length ? `此声明还标记${resources.join('、')},用于检查资源获取与释放是否成对。` : ''}`, [`${from.id}.command → ${to.id}`, ...resources]);
|
|
110
|
+
}
|
|
111
|
+
case 'binds': {
|
|
112
|
+
if (from.kind === 'route')
|
|
113
|
+
return result('定义审批步骤', '流程配置', `“${a}”与“${b}”使用同一流程标识 ${from.attrs.process}。`, '这份路由定义流程里的审批步骤、选人方式和条件分支,供代码内审批引擎使用。', [`process: ${from.attrs.process}`]);
|
|
114
|
+
const approved = from.attrs.onApproved === to.id;
|
|
115
|
+
const rejected = from.attrs.onRejected === to.id;
|
|
116
|
+
const outcome = approved && rejected ? '结束' : approved ? '通过' : '驳回';
|
|
117
|
+
return result(`${outcome}后执行`, '审批结论回写', `“${a}”${outcome}时绑定执行“${b}”。`, '将审批结论交给业务命令推进单据状态,并统一执行权限和业务约束。', [
|
|
118
|
+
approved ? `${from.id}.onApproved → ${to.id}` : '',
|
|
119
|
+
rejected ? `${from.id}.onRejected → ${to.id}` : '',
|
|
120
|
+
].filter(Boolean));
|
|
121
|
+
}
|
|
122
|
+
case 'calls':
|
|
123
|
+
return result('允许调用命令', '任务调用范围', `“${a}”在 calls 中声明可以调用“${b}”。`, '明确任务能执行哪些业务写入,并支持影响分析。每次任务是否实际调用该命令,取决于 run 中的业务逻辑。', [`${from.id}.calls → ${to.id}`]);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export const TABLE_WIDTH = 336;
|
|
2
|
+
export const TABLE_HEADER = 58;
|
|
3
|
+
export const TABLE_ROW = 38;
|
|
4
|
+
export const TABLE_FOOTER = 34;
|
|
5
|
+
/** 对象卡片展示声明字段,而不是猜测完整数据库结构。系统自动添加的列不在
|
|
6
|
+
* 抽取结果中,因此这里不补造 id、主键、外键约束或一对多基数。
|
|
7
|
+
* 字段仍保留原节点身份,搜索、点击和关系解释都能回到同一份声明。 */
|
|
8
|
+
export function tableCards(model) {
|
|
9
|
+
return model.nodes
|
|
10
|
+
.filter((n) => n.kind === 'aggregate')
|
|
11
|
+
.map((node) => {
|
|
12
|
+
const fieldKeys = model.edges.filter((e) => e.kind === 'field' && e.from === node.key).map((e) => e.to);
|
|
13
|
+
const fields = fieldKeys.map((key) => model.nodes.find((n) => n.key === key));
|
|
14
|
+
return { node, fields, height: TABLE_HEADER + fields.length * TABLE_ROW + TABLE_FOOTER };
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/** 用真实字段归属解析端点;不能把两个同名字段合并,也不能把命令算成列。 */
|
|
18
|
+
export function tableOwner(cards, key) {
|
|
19
|
+
return cards.find((card) => card.node.key === key || card.fields.some((field) => field.key === key));
|
|
20
|
+
}
|
|
21
|
+
/** 关联较多的对象优先放置,再按实际卡片高度分配到最短列。
|
|
22
|
+
* 两列上限保持字段字可读;卡片高度由字段数决定,避免固定网格裁掉长表。 */
|
|
23
|
+
export function layoutTables(cards, edges, columns) {
|
|
24
|
+
const positions = new Map();
|
|
25
|
+
const heights = Array.from({ length: Math.min(columns, cards.length) }, () => 0);
|
|
26
|
+
const degree = (card) => edges.filter((e) => e.to === card.node.key || card.fields.some((f) => f.key === e.from)).length;
|
|
27
|
+
const ordered = [...cards].sort((a, b) => degree(b) - degree(a) || b.height - a.height || a.node.id.localeCompare(b.node.id));
|
|
28
|
+
for (const card of ordered) {
|
|
29
|
+
const column = heights.indexOf(Math.min(...heights));
|
|
30
|
+
positions.set(card.node.key, { x: column * (TABLE_WIDTH + 155), y: heights[column] });
|
|
31
|
+
heights[column] += card.height + 84;
|
|
32
|
+
}
|
|
33
|
+
return positions;
|
|
34
|
+
}
|
|
35
|
+
/** 引用线从具体字段行连接到目标对象表头。端点随卡片拖动一起计算,
|
|
36
|
+
* 不把整张卡片的中心当成字段位置;同列时从侧边绕开表体。 */
|
|
37
|
+
export function tableEdgePath(edge, cards, positions) {
|
|
38
|
+
const source = tableOwner(cards, edge.from);
|
|
39
|
+
const target = tableOwner(cards, edge.to);
|
|
40
|
+
const from = positions.get(source.node.key);
|
|
41
|
+
const to = positions.get(target.node.key);
|
|
42
|
+
const row = source.fields.findIndex((f) => f.key === edge.from);
|
|
43
|
+
const y1 = from.y + TABLE_HEADER + row * TABLE_ROW + TABLE_ROW / 2;
|
|
44
|
+
const y2 = to.y + TABLE_HEADER / 2;
|
|
45
|
+
if (from.x === to.x) {
|
|
46
|
+
const x = from.x + TABLE_WIDTH;
|
|
47
|
+
const bend = 88 + row * 14;
|
|
48
|
+
return `M${x},${y1} C${x + bend},${y1} ${x + bend},${y2} ${x},${y2}`;
|
|
49
|
+
}
|
|
50
|
+
const forward = to.x > from.x;
|
|
51
|
+
const x1 = from.x + (forward ? TABLE_WIDTH : 0);
|
|
52
|
+
const x2 = to.x + (forward ? 0 : TABLE_WIDTH);
|
|
53
|
+
const bend = Math.max(65, Math.abs(x2 - x1) / 2) * (forward ? 1 : -1);
|
|
54
|
+
return `M${x1},${y1} C${x1 + bend},${y1} ${x2 - bend},${y2} ${x2},${y2}`;
|
|
55
|
+
}
|
|
56
|
+
export function fieldType(field) {
|
|
57
|
+
const types = {
|
|
58
|
+
text: '文本',
|
|
59
|
+
int: '整数',
|
|
60
|
+
money: '金额',
|
|
61
|
+
decimal: '小数',
|
|
62
|
+
float: '浮点数',
|
|
63
|
+
bool: '布尔',
|
|
64
|
+
boolean: '布尔',
|
|
65
|
+
id: '标识',
|
|
66
|
+
ref: '引用',
|
|
67
|
+
list: '列表',
|
|
68
|
+
json: 'JSON',
|
|
69
|
+
date: '日期',
|
|
70
|
+
datetime: '时间',
|
|
71
|
+
enum: '枚举',
|
|
72
|
+
};
|
|
73
|
+
return types[String(field.attrs.kind)] ?? String(field.attrs.kind);
|
|
74
|
+
}
|