w-flowchart 1.0.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/.editorconfig +9 -0
- package/.eslintignore +3 -0
- package/.eslintrc.js +55 -0
- package/.jsdoc +25 -0
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/SECURITY.md +5 -0
- package/babel.config.js +16 -0
- package/dist/w-flowchart.umd.js +7 -0
- package/dist/w-flowchart.umd.js.map +1 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
- package/docs/index.html +84 -0
- package/docs/scripts/collapse.js +39 -0
- package/docs/scripts/commonNav.js +28 -0
- package/docs/scripts/linenumber.js +25 -0
- package/docs/scripts/nav.js +12 -0
- package/docs/scripts/polyfill.js +4 -0
- package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
- package/docs/scripts/prettify/lang-css.js +2 -0
- package/docs/scripts/prettify/prettify.js +28 -0
- package/docs/scripts/search.js +99 -0
- package/docs/styles/jsdoc.css +776 -0
- package/docs/styles/prettify.css +80 -0
- package/package.json +49 -0
- package/script.txt +17 -0
- package/src/WFlowchart.mjs +0 -0
- package/src/common/derive.mjs +7 -0
- package/src/common/palette.mjs +34 -0
- package/src/common/pkg.mjs +31 -0
- package/src/p1/README.md +65 -0
- package/src/p1/gen.mjs +203 -0
- package/src/p10/README.md +31 -0
- package/src/p10/gen.mjs +228 -0
- package/src/p2/README.md +87 -0
- package/src/p2/gen.mjs +210 -0
- package/src/p3/README.md +99 -0
- package/src/p3/gen.mjs +188 -0
- package/src/p4/README.md +84 -0
- package/src/p4/gen.mjs +82 -0
- package/src/p5/README.md +90 -0
- package/src/p5/gen.mjs +182 -0
- package/src/p6/README.md +87 -0
- package/src/p6/gen.mjs +186 -0
- package/src/p7/README.md +84 -0
- package/src/p7/gen.mjs +386 -0
- package/src/p8/README.md +92 -0
- package/src/p8/gen.mjs +416 -0
- package/src/p9/README.md +90 -0
- package/src/p9/gen.mjs +63 -0
- package/src/p9/page9.html +420 -0
- package/test/cases.mjs +349 -0
- package/test/data//344/275/234/346/245/255/346/265/201/347/250/213/345/234/226.json +92 -0
- package/test/data//345/211/215/347/253/257/346/236/266/346/247/213/350/210/207/350/263/207/346/226/231/346/265/201/345/234/226.json +112 -0
- package/test/data//345/276/214/347/253/257/346/234/215/345/213/231/347/265/204/346/210/220/345/234/226.json +116 -0
- package/test/data//346/211/271/346/254/241/344/270/212/345/202/263/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.json +251 -0
- package/test/data//347/250/213/345/274/217/345/205/203/344/273/266/347/265/204/346/210/220/345/234/226.json +126 -0
- package/test/data//347/250/213/345/274/217/345/210/206/345/261/244/346/236/266/346/247/213/345/234/226.json +133 -0
- package/test/data//347/263/273/347/265/261/346/236/266/346/247/213/345/234/226.json +102 -0
- package/test/data//347/263/273/347/265/261/351/227/234/350/201/257/345/234/226.json +117 -0
- package/test/data//347/267/232/344/270/212/347/225/253/351/235/242/351/227/234/350/201/257/345/234/226.json +162 -0
- package/test/figures.mjs +10 -0
- package/test/lib.mjs +116 -0
- package/test/p10.test.mjs +89 -0
- package/test/pics/cycle.png +0 -0
- package/test/pics/cycle.svg +1 -0
- package/test/pics/deep-nest.png +0 -0
- package/test/pics/deep-nest.svg +1 -0
- package/test/pics/diamond.png +0 -0
- package/test/pics/diamond.svg +1 -0
- package/test/pics/edges-mix.png +0 -0
- package/test/pics/edges-mix.svg +1 -0
- package/test/pics/english.png +0 -0
- package/test/pics/english.svg +1 -0
- package/test/pics/flat-tb.png +0 -0
- package/test/pics/flat-tb.svg +1 -0
- package/test/pics/fold-tall.png +0 -0
- package/test/pics/fold-tall.svg +1 -0
- package/test/pics/fold-wide.png +0 -0
- package/test/pics/fold-wide.svg +1 -0
- package/test/pics/group-edge.png +0 -0
- package/test/pics/group-edge.svg +1 -0
- package/test/pics/items-node.png +0 -0
- package/test/pics/items-node.svg +1 -0
- package/test/pics/long-label.png +0 -0
- package/test/pics/long-label.svg +1 -0
- package/test/pics/lr-flat.png +0 -0
- package/test/pics/lr-flat.svg +1 -0
- package/test/pics/nested-group.png +0 -0
- package/test/pics/nested-group.svg +1 -0
- package/test/pics/no-edges.png +0 -0
- package/test/pics/no-edges.svg +1 -0
- package/test/pics/title-overflow.png +0 -0
- package/test/pics/title-overflow.svg +1 -0
- package/test/pics/valign.png +0 -0
- package/test/pics/valign.svg +1 -0
- package/test/pics//344/275/234/346/245/255/346/265/201/347/250/213/345/234/226.png +0 -0
- package/test/pics//344/275/234/346/245/255/346/265/201/347/250/213/345/234/226.svg +1 -0
- package/test/pics//345/211/215/347/253/257/346/236/266/346/247/213/350/210/207/350/263/207/346/226/231/346/265/201/345/234/226.png +0 -0
- package/test/pics//345/211/215/347/253/257/346/236/266/346/247/213/350/210/207/350/263/207/346/226/231/346/265/201/345/234/226.svg +1 -0
- package/test/pics//345/276/214/347/253/257/346/234/215/345/213/231/347/265/204/346/210/220/345/234/226.png +0 -0
- package/test/pics//345/276/214/347/253/257/346/234/215/345/213/231/347/265/204/346/210/220/345/234/226.svg +1 -0
- package/test/pics//346/211/271/346/254/241/344/270/212/345/202/263/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.png +0 -0
- package/test/pics//346/211/271/346/254/241/344/270/212/345/202/263/350/231/225/347/220/206/346/265/201/347/250/213/345/234/226.svg +1 -0
- package/test/pics//347/250/213/345/274/217/345/205/203/344/273/266/347/265/204/346/210/220/345/234/226.png +0 -0
- package/test/pics//347/250/213/345/274/217/345/205/203/344/273/266/347/265/204/346/210/220/345/234/226.svg +1 -0
- package/test/pics//347/250/213/345/274/217/345/210/206/345/261/244/346/236/266/346/247/213/345/234/226.png +0 -0
- package/test/pics//347/250/213/345/274/217/345/210/206/345/261/244/346/236/266/346/247/213/345/234/226.svg +1 -0
- package/test/pics//347/263/273/347/265/261/346/236/266/346/247/213/345/234/226.png +0 -0
- package/test/pics//347/263/273/347/265/261/346/236/266/346/247/213/345/234/226.svg +1 -0
- package/test/pics//347/263/273/347/265/261/351/227/234/350/201/257/345/234/226.png +0 -0
- package/test/pics//347/263/273/347/265/261/351/227/234/350/201/257/345/234/226.svg +1 -0
- package/test/pics//347/267/232/344/270/212/347/225/253/351/235/242/351/227/234/350/201/257/345/234/226.png +0 -0
- package/test/pics//347/267/232/344/270/212/347/225/253/351/235/242/351/227/234/350/201/257/345/234/226.svg +1 -0
- package/test/wflowchart.test.mjs +41 -0
- package/toolg/addVersion.mjs +4 -0
- package/toolg/cleanFolder.mjs +4 -0
- package/toolg/gDistRollup.mjs +51 -0
- package/toolg/modifyReadme.mjs +4 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
{
|
|
2
|
+
"dir": "LR",
|
|
3
|
+
"nodes": [
|
|
4
|
+
{
|
|
5
|
+
"id": "APP",
|
|
6
|
+
"label": "應用系統",
|
|
7
|
+
"cls": "purpleG"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"id": "a1",
|
|
11
|
+
"label": "GIS2D",
|
|
12
|
+
"cls": "purple",
|
|
13
|
+
"group": "APP"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "a2",
|
|
17
|
+
"label": "GIS3D",
|
|
18
|
+
"cls": "purple",
|
|
19
|
+
"group": "APP"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": "a3",
|
|
23
|
+
"label": "需求管理",
|
|
24
|
+
"cls": "purple",
|
|
25
|
+
"group": "APP"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "a4",
|
|
29
|
+
"label": "知識管理",
|
|
30
|
+
"cls": "purple",
|
|
31
|
+
"group": "APP"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "a5",
|
|
35
|
+
"label": "特徵事件作用",
|
|
36
|
+
"cls": "purple",
|
|
37
|
+
"group": "APP"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"id": "a6",
|
|
41
|
+
"label": "評估模式流程",
|
|
42
|
+
"cls": "purple",
|
|
43
|
+
"group": "APP"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "a7",
|
|
47
|
+
"label": "源項",
|
|
48
|
+
"cls": "purple",
|
|
49
|
+
"group": "APP"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "API",
|
|
53
|
+
"label": "API 管理系統",
|
|
54
|
+
"cls": "blue"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"id": "PDM",
|
|
58
|
+
"label": "計畫資料管理系統",
|
|
59
|
+
"cls": "blue"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"id": "BAT",
|
|
63
|
+
"label": "批次上傳系統",
|
|
64
|
+
"cls": "orange"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"id": "AUTH",
|
|
68
|
+
"label": "單一登入+權限管理",
|
|
69
|
+
"cls": "red"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"id": "URLC",
|
|
73
|
+
"label": "輔助網址控制系統",
|
|
74
|
+
"cls": "orange"
|
|
75
|
+
}
|
|
76
|
+
],
|
|
77
|
+
"edges": [
|
|
78
|
+
{
|
|
79
|
+
"from": "APP",
|
|
80
|
+
"to": "API",
|
|
81
|
+
"label": "取用詮釋資料"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"from": "API",
|
|
85
|
+
"to": "PDM"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"from": "BAT",
|
|
89
|
+
"to": "PDM",
|
|
90
|
+
"label": "匯入"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"from": "AUTH",
|
|
94
|
+
"to": "APP",
|
|
95
|
+
"label": "共用驗證",
|
|
96
|
+
"kind": "dashed"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"from": "AUTH",
|
|
100
|
+
"to": "PDM",
|
|
101
|
+
"label": "共用驗證",
|
|
102
|
+
"kind": "dashed"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"from": "AUTH",
|
|
106
|
+
"to": "API",
|
|
107
|
+
"label": "共用驗證",
|
|
108
|
+
"kind": "dashed"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"from": "URLC",
|
|
112
|
+
"to": "APP",
|
|
113
|
+
"label": "跨系統調閱",
|
|
114
|
+
"kind": "dashed"
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
{
|
|
2
|
+
"dir": "LR",
|
|
3
|
+
"nodes": [
|
|
4
|
+
{
|
|
5
|
+
"id": "L",
|
|
6
|
+
"title": "登入",
|
|
7
|
+
"items": [
|
|
8
|
+
"SSO"
|
|
9
|
+
],
|
|
10
|
+
"cls": "red"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"id": "M",
|
|
14
|
+
"title": "主畫面",
|
|
15
|
+
"items": [
|
|
16
|
+
"選單"
|
|
17
|
+
],
|
|
18
|
+
"cls": "blue"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "p0",
|
|
22
|
+
"label": "主要資訊",
|
|
23
|
+
"cls": "purple"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "p1",
|
|
27
|
+
"label": "計畫資料管理",
|
|
28
|
+
"cls": "purple"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "p2",
|
|
32
|
+
"label": "輔助參考文獻",
|
|
33
|
+
"cls": "purple"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "p3",
|
|
37
|
+
"label": "需求管理",
|
|
38
|
+
"cls": "purple"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "p4",
|
|
42
|
+
"label": "知識管理",
|
|
43
|
+
"cls": "purple"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "p5",
|
|
47
|
+
"label": "特徵事件作用",
|
|
48
|
+
"cls": "purple"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"id": "p6",
|
|
52
|
+
"label": "評估模式流程",
|
|
53
|
+
"cls": "purple"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"id": "p7",
|
|
57
|
+
"label": "源項",
|
|
58
|
+
"cls": "purple"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"id": "p8",
|
|
62
|
+
"label": "GIS2D",
|
|
63
|
+
"cls": "purple"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"id": "p9",
|
|
67
|
+
"label": "GIS3D",
|
|
68
|
+
"cls": "purple"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"id": "p10",
|
|
72
|
+
"label": "模板下載",
|
|
73
|
+
"cls": "purple"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"id": "p11",
|
|
77
|
+
"label": "輔助工具",
|
|
78
|
+
"cls": "purple"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"id": "p12",
|
|
82
|
+
"label": "批次上傳",
|
|
83
|
+
"cls": "purple"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"id": "p13",
|
|
87
|
+
"label": "API 管理",
|
|
88
|
+
"cls": "purple"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"id": "p14",
|
|
92
|
+
"label": "後台管理",
|
|
93
|
+
"cls": "purple"
|
|
94
|
+
}
|
|
95
|
+
],
|
|
96
|
+
"edges": [
|
|
97
|
+
{
|
|
98
|
+
"from": "L",
|
|
99
|
+
"to": "M"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"from": "M",
|
|
103
|
+
"to": "p0"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"from": "M",
|
|
107
|
+
"to": "p1"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"from": "M",
|
|
111
|
+
"to": "p2"
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"from": "M",
|
|
115
|
+
"to": "p3"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"from": "M",
|
|
119
|
+
"to": "p4"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"from": "M",
|
|
123
|
+
"to": "p5"
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"from": "M",
|
|
127
|
+
"to": "p6"
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"from": "M",
|
|
131
|
+
"to": "p7"
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"from": "M",
|
|
135
|
+
"to": "p8"
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"from": "M",
|
|
139
|
+
"to": "p9"
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"from": "M",
|
|
143
|
+
"to": "p10"
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"from": "M",
|
|
147
|
+
"to": "p11"
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"from": "M",
|
|
151
|
+
"to": "p12"
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"from": "M",
|
|
155
|
+
"to": "p13"
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"from": "M",
|
|
159
|
+
"to": "p14"
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
}
|
package/test/figures.mjs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// 9 張真圖正規化數據索引(供回歸測試): 讀 test/data/*.json, 衍生 rich 節點 label
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import { fileURLToPath } from 'url'
|
|
4
|
+
import { dirname, resolve } from 'path'
|
|
5
|
+
import { deriveLabels } from '../src/common/derive.mjs'
|
|
6
|
+
|
|
7
|
+
const __dir = dirname(fileURLToPath(import.meta.url))
|
|
8
|
+
const KEYS = ['系統架構圖', '系統關聯圖', '作業流程圖', '線上畫面關聯圖', '程式分層架構圖', '程式元件組成圖', '後端服務組成圖', '批次上傳處理流程圖', '前端架構與資料流圖']
|
|
9
|
+
|
|
10
|
+
export const FIGURES = KEYS.map(key => ({ key, data: deriveLabels(JSON.parse(fs.readFileSync(resolve(__dir, 'data', key + '.json'), 'utf8'))) }))
|
package/test/lib.mjs
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// p10 測試共用 — 重用 gen.mjs 之「真實」管線(translate + PAGE_HTML + window.renderFig), 不複製渲染器(複製會與 production 分叉, 失去測試意義)。
|
|
2
|
+
// openStage(): 啟一個 headless 舞台頁(elkjs + renderFig 就緒)
|
|
3
|
+
// renderCase(page, data): 跑單案真實管線 → { spec, res(含 geom 幾何), svg(字串), png(Buffer) }
|
|
4
|
+
// runInvariants(spec, geom): 對「任何正確 p10 輸出皆須成立」之結構不變量做斷言(spec 翻譯, 非現狀指紋)
|
|
5
|
+
import { chromium } from 'playwright'
|
|
6
|
+
import { translate, PAGE_HTML } from '../src/p10/gen.mjs'
|
|
7
|
+
|
|
8
|
+
export { translate }
|
|
9
|
+
|
|
10
|
+
export async function openStage() {
|
|
11
|
+
const browser = await chromium.launch()
|
|
12
|
+
const page = await browser.newPage({ deviceScaleFactor: 2 })
|
|
13
|
+
const errs = []
|
|
14
|
+
page.on('pageerror', e => errs.push(e.message))
|
|
15
|
+
page.on('console', m => {
|
|
16
|
+
if (m.type() === 'error') errs.push('c:' + m.text().slice(0, 200))
|
|
17
|
+
})
|
|
18
|
+
await page.setContent(PAGE_HTML, { waitUntil: 'load' })
|
|
19
|
+
await page.waitForFunction(() => window.__ready, { timeout: 60000 }).catch(() => {})
|
|
20
|
+
await page.evaluate(() => document.fonts && document.fonts.ready)
|
|
21
|
+
return { browser, page, errs }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 跑單一案例之真實管線 → { spec, res, svg, png }
|
|
25
|
+
export async function renderCase(page, data) {
|
|
26
|
+
const spec = translate(data)
|
|
27
|
+
const res = await page.evaluate(s => window.renderFig(s), spec)
|
|
28
|
+
if (!res.ok) return { spec, res, svg: null, png: null }
|
|
29
|
+
await page.evaluate(() => document.fonts && document.fonts.ready)
|
|
30
|
+
await page.waitForTimeout(120)
|
|
31
|
+
const svg = await page.evaluate(() => document.getElementById('stage').innerHTML)
|
|
32
|
+
const png = await page.locator('#stage svg').screenshot()
|
|
33
|
+
return { spec, res, svg, png }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 幾何便捷查詢
|
|
37
|
+
export function helpers(geom) {
|
|
38
|
+
return {
|
|
39
|
+
node: id => geom.nodes.find(n => n.id === id),
|
|
40
|
+
group: id => geom.groups.find(g => g.id === id),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const within = (c, p, tol) => c.x >= p.x - tol && c.y >= p.y - tol && c.x + c.w <= p.x + p.w + tol && c.y + c.h <= p.y + p.h + tol
|
|
45
|
+
const overlap = (a, b, tol) => a.x < b.x + b.w - tol && b.x < a.x + a.w - tol && a.y < b.y + b.h - tol && b.y < a.y + a.h - tol
|
|
46
|
+
const near = (p, box, tol) => p.x >= box.x - tol && p.x <= box.x + box.w + tol && p.y >= box.y - tol && p.y <= box.y + box.h + tol
|
|
47
|
+
|
|
48
|
+
// 通用結構不變量(對所有案例 + 9 真圖皆套用)。每項對應「正確 p10 輸出必成立」之一句 spec, 非「跟上次一樣」之指紋。
|
|
49
|
+
// 回 [{ name, pass, detail }]
|
|
50
|
+
export function runInvariants(spec, geom) {
|
|
51
|
+
const out = []
|
|
52
|
+
const bbox = {}; geom.groups.forEach(g => bbox[g.id] = g); geom.nodes.forEach(n => bbox[n.id] = n)
|
|
53
|
+
|
|
54
|
+
// 1) 存在性: 每個邏輯節點/群組都被畫出(重構不可漏掉元素)
|
|
55
|
+
{
|
|
56
|
+
const gNodes = new Set(geom.nodes.map(n => n.id)); const gGroups = new Set(geom.groups.map(g => g.id))
|
|
57
|
+
const missN = spec.nodes.filter(n => !gNodes.has(n.id)).map(n => n.id)
|
|
58
|
+
const missG = spec.groups.filter(g => !gGroups.has(g.id)).map(g => g.id)
|
|
59
|
+
out.push({ name: '存在性: 節點/群組無遺漏', pass: !missN.length && !missG.length, detail: (missN.length || missG.length) ? `缺節點[${missN}] 缺群組[${missG}]` : `${geom.nodes.length} 節點 / ${geom.groups.length} 群組` })
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 2) 包含: 有父者其外框 ⊆ 父外框(容器須包住成員, ELK INCLUDE_CHILDREN 合約)
|
|
63
|
+
{
|
|
64
|
+
const bad = []
|
|
65
|
+
;[...geom.groups, ...geom.nodes].forEach(c => {
|
|
66
|
+
if (c.parent && bbox[c.parent] && !within(c, bbox[c.parent], 1.5)) bad.push(c.id + '⊄' + c.parent)
|
|
67
|
+
})
|
|
68
|
+
out.push({ name: '包含: 成員不逃出容器', pass: !bad.length, detail: bad.length ? bad.slice(0, 4).join(', ') : '全部成員在容器內' })
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 3) 標題不溢出: 每個容器標題寬 ≤ 容器寬(2-pass padding 撐寬機制之合約)
|
|
72
|
+
{
|
|
73
|
+
const bad = geom.groups.filter(g => g.labelW > g.w + 2).map(g => `${g.id}(標題${g.labelW}>框${g.w})`)
|
|
74
|
+
out.push({ name: '標題不溢出容器', pass: !bad.length, detail: bad.length ? bad.slice(0, 4).join(', ') : '全部標題在框內' })
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 4) 不重疊: 同一父之葉節點互不重疊(佈局健全性)
|
|
78
|
+
{
|
|
79
|
+
const byParent = {}; geom.nodes.forEach(n => {
|
|
80
|
+
(byParent[n.parent || '_root'] = byParent[n.parent || '_root'] || []).push(n)
|
|
81
|
+
})
|
|
82
|
+
const bad = []
|
|
83
|
+
Object.values(byParent).forEach(arr => {
|
|
84
|
+
for (let i = 0; i < arr.length; i++) for (let j = i + 1; j < arr.length; j++) if (overlap(arr[i], arr[j], 1.5)) bad.push(arr[i].id + '∩' + arr[j].id)
|
|
85
|
+
})
|
|
86
|
+
out.push({ name: '不重疊: 同層節點分離', pass: !bad.length, detail: bad.length ? bad.slice(0, 4).join(', ') : '節點無重疊' })
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 5) items 框: 標題+items 節點須有標題列(headH>0)且本體高於標題列
|
|
90
|
+
{
|
|
91
|
+
const its = geom.nodes.filter(n => n.kind === 'items')
|
|
92
|
+
const bad = its.filter(n => !(n.headH > 0 && n.h > n.headH + 4)).map(n => `${n.id}(h=${n.h} headH=${n.headH})`)
|
|
93
|
+
out.push({ name: 'items 框: 標題列+本體完整', pass: !bad.length, detail: its.length ? (bad.length ? bad.join(', ') : `${its.length} 個 items 框正常`) : '(無 items 框)' })
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 6) 邊: 每條邏輯邊都有路由(≥1 段, ≥2 點), 且兩端點落在來源/目標節點邊界(LCA 容器座標正確 → 不會跑到左上角)
|
|
97
|
+
{
|
|
98
|
+
const idset = new Set(geom.edges.map(e => e.id))
|
|
99
|
+
const missing = spec.edges.filter(e => !idset.has(e.id)).map(e => e.id)
|
|
100
|
+
const noRoute = []; const offTarget = []
|
|
101
|
+
spec.edges.forEach(se => {
|
|
102
|
+
const ge = geom.edges.find(e => e.id === se.id); if (!ge) return
|
|
103
|
+
const seg = ge.segs && ge.segs.find(s => s.length >= 2)
|
|
104
|
+
if (!seg) {
|
|
105
|
+
noRoute.push(se.id); return
|
|
106
|
+
}
|
|
107
|
+
const s0 = seg[0]; const s1 = seg[seg.length - 1]
|
|
108
|
+
const src = bbox[se.from]; const tgt = bbox[se.to]
|
|
109
|
+
if (src && tgt && !(near(s0, src, 6) && near(s1, tgt, 6))) offTarget.push(`${se.id}(${se.from}→${se.to})`)
|
|
110
|
+
})
|
|
111
|
+
const pass = !missing.length && !noRoute.length && !offTarget.length
|
|
112
|
+
out.push({ name: '邊: 皆有路由且兩端接節點', pass, detail: pass ? `${geom.edges.length} 條邊正常` : `缺[${missing}] 無路由[${noRoute}] 端點脫離[${offTarget.slice(0, 4)}]` })
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return out
|
|
116
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// p10 渲染回歸(mocha) — 10 合成案 + 9 真圖, 兩層把關:
|
|
2
|
+
// A. 結構不變量(spec 翻譯, 硬失敗): 存在性/包含/標題不溢出/不重疊/items框/邊路由
|
|
3
|
+
// B. 快照基準(回歸偵測): SVG 逐字比對 + PNG 像素(pixelmatch includeAA:false, 容差 MAXDIFF)
|
|
4
|
+
// 用法:
|
|
5
|
+
// npm test 全部測試
|
|
6
|
+
// npx mocha "test/p10.test.mjs" --timeout 60000 只跑本回歸
|
|
7
|
+
// npx mocha "test/p10.test.mjs" --timeout 60000 --grep fold 只跑名稱含 fold 之案
|
|
8
|
+
// 重建基準(確認變動為預期後才做): 設環境變數 UPDATE_BASELINE=1 再跑
|
|
9
|
+
import assert from 'assert'
|
|
10
|
+
import fs from 'fs'
|
|
11
|
+
import { fileURLToPath } from 'url'
|
|
12
|
+
import { dirname, resolve } from 'path'
|
|
13
|
+
import pixelmatch from 'pixelmatch'
|
|
14
|
+
import { PNG } from 'pngjs'
|
|
15
|
+
import { openStage, renderCase, runInvariants, helpers } from './lib.mjs'
|
|
16
|
+
import { CASES } from './cases.mjs'
|
|
17
|
+
import { FIGURES } from './figures.mjs'
|
|
18
|
+
|
|
19
|
+
const __dir = dirname(fileURLToPath(import.meta.url))
|
|
20
|
+
const SNAP = resolve(__dir, 'pics')
|
|
21
|
+
const MAXDIFF = 60 // PNG 容許之反鋸齒雜訊像素上限(真 regression 量級遠超此)
|
|
22
|
+
const UPDATE = process.env.UPDATE_BASELINE === '1' || process.env.UPDATE_BASELINE === 'true'
|
|
23
|
+
|
|
24
|
+
// 待測清單: 合成案 + 9 真圖(真圖為最重要之回歸案)
|
|
25
|
+
const items = []
|
|
26
|
+
for (const c of CASES) items.push({ name: c.name, data: c.data, check: c.check, expect: c.expect, kind: '合成' })
|
|
27
|
+
for (const f of FIGURES) items.push({ name: f.key, data: f.data, kind: '真圖' })
|
|
28
|
+
|
|
29
|
+
// PNG 像素比對(反鋸齒容差): 回 { same, detail }
|
|
30
|
+
function pngCompare(baseBuf, curBuf, diffPath) {
|
|
31
|
+
const a = PNG.sync.read(baseBuf), b = PNG.sync.read(curBuf)
|
|
32
|
+
if (a.width !== b.width || a.height !== b.height) return { same: false, detail: `尺寸 ${a.width}x${a.height} → ${b.width}x${b.height}` }
|
|
33
|
+
const diff = new PNG({ width: a.width, height: a.height })
|
|
34
|
+
const n = pixelmatch(a.data, b.data, diff.data, a.width, a.height, { includeAA: false, threshold: 0.1 })
|
|
35
|
+
if (n > MAXDIFF) { fs.writeFileSync(diffPath, PNG.sync.write(diff)); return { same: false, detail: `${n} 像素差 (>${MAXDIFF}) → ${diffPath}` } }
|
|
36
|
+
return { same: true, detail: `${n} 像素差` }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('p10 渲染回歸(結構不變量 + 快照基準)', function() {
|
|
40
|
+
let browser = null
|
|
41
|
+
let page = null
|
|
42
|
+
|
|
43
|
+
before(async function() {
|
|
44
|
+
this.timeout(120000)
|
|
45
|
+
fs.mkdirSync(SNAP, { recursive: true })
|
|
46
|
+
const s = await openStage()
|
|
47
|
+
browser = s.browser
|
|
48
|
+
page = s.page
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
after(async function() {
|
|
52
|
+
if (browser) await browser.close()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
for (const tc of items) {
|
|
56
|
+
it(`[${tc.kind}] ${tc.name}`, async function() {
|
|
57
|
+
const r = await renderCase(page, tc.data)
|
|
58
|
+
assert.ok(r.res && r.res.ok, '渲染失敗: ' + (r.res && r.res.err))
|
|
59
|
+
const geom = r.res.geom
|
|
60
|
+
const h = helpers(geom)
|
|
61
|
+
|
|
62
|
+
// A. 不變量 + 案例專屬斷言 + 比例期望
|
|
63
|
+
const checks = runInvariants(r.spec, geom)
|
|
64
|
+
if (tc.check) checks.push(...tc.check(r.spec, geom, h).filter(Boolean))
|
|
65
|
+
if (tc.expect) {
|
|
66
|
+
const R = geom.H / geom.W
|
|
67
|
+
if (tc.expect.ratioMax != null) checks.push({ name: `比例 H/W ≤ ${tc.expect.ratioMax}`, pass: R <= tc.expect.ratioMax + 0.02, detail: `R=${R.toFixed(3)}` })
|
|
68
|
+
if (tc.expect.ratioMin != null) checks.push({ name: `比例 H/W ≥ ${tc.expect.ratioMin}`, pass: R >= tc.expect.ratioMin - 0.02, detail: `R=${R.toFixed(3)}` })
|
|
69
|
+
}
|
|
70
|
+
const failed = checks.filter(c => !c.pass)
|
|
71
|
+
assert.ok(!failed.length, '不變量失敗: ' + failed.map(c => `${c.name}(${c.detail || ''})`).join('; '))
|
|
72
|
+
|
|
73
|
+
// B. 快照基準(無基準或 UPDATE_BASELINE=1 時建立/重建)
|
|
74
|
+
const svgP = resolve(SNAP, tc.name + '.svg')
|
|
75
|
+
const pngP = resolve(SNAP, tc.name + '.png')
|
|
76
|
+
if (UPDATE || !fs.existsSync(svgP)) fs.writeFileSync(svgP, r.svg)
|
|
77
|
+
else {
|
|
78
|
+
const base = fs.readFileSync(svgP, 'utf8')
|
|
79
|
+
if (base !== r.svg) fs.writeFileSync(resolve(SNAP, tc.name + '.new.svg'), r.svg)
|
|
80
|
+
assert.ok(base === r.svg, `SVG 與基準不同(長 ${base.length}→${r.svg.length}; 已輸出 ${tc.name}.new.svg, 確認為預期後以 UPDATE_BASELINE=1 重建)`)
|
|
81
|
+
}
|
|
82
|
+
if (UPDATE || !fs.existsSync(pngP)) fs.writeFileSync(pngP, r.png)
|
|
83
|
+
else {
|
|
84
|
+
const c = pngCompare(fs.readFileSync(pngP), r.png, resolve(SNAP, tc.name + '.diff.png'))
|
|
85
|
+
assert.ok(c.same, `PNG 與基準不同: ${c.detail}(確認為預期後以 UPDATE_BASELINE=1 重建)`)
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
})
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="147" height="497" viewBox="-18 -18 147 497" style="background:#fff"><path d="M 49.66666666666667 48 L 49.66666666666667 82" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 49.66666666666667 82 L 53.66666666666667 73 L 45.66666666666667 73 Z" fill="#44505a"></path><path d="M 58.33333333333333 118 L 58.33333333333333 144 L 66 144 L 66 214 L 70 214 L 70 240" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 70 240 L 74 231 L 66 231 Z" fill="#44505a"></path><path d="M 55.5 327 L 55.5 413" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 55.5 413 L 59.5 404 L 51.5 404 Z" fill="#44505a"></path><foreignObject x="28.5" y="361" width="31" height="22"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:12.5px;color:#4a5560;text-shadow:0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff;white-space:nowrap">通過</div></foreignObject><path d="M 41 240 L 41 118" fill="none" stroke="#44505a" stroke-width="2" stroke-dasharray="6 4"></path><path d="M 41 118 L 37 127 L 45 127 Z" fill="#44505a"></path><foreignObject x="14" y="170" width="31" height="22"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:12.5px;color:#4a5560;text-shadow:0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff;white-space:nowrap">退回</div></foreignObject><rect x="23.666666666666668" y="12" width="52" height="36" rx="7" fill="#eef4fb" stroke="#3f6fb0" stroke-width="1.6"></rect><foreignObject x="23.666666666666668" y="12" width="52" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">開始</div></foreignObject><rect x="23.666666666666668" y="82" width="52" height="36" rx="7" fill="#eaf4ef" stroke="#348a5c" stroke-width="1.6"></rect><foreignObject x="23.666666666666668" y="82" width="52" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">處理</div></foreignObject><polygon points="55.5,240 99,283.5 55.5,327 12,283.5" fill="#fcf0e2" stroke="#c46e1a" stroke-width="1.8"></polygon><foreignObject x="12" y="240" width="87" height="87"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#23303a;line-height:1.4;padding:0 4px;box-sizing:border-box">審核?</div></foreignObject><rect x="29.5" y="413" width="52" height="36" rx="7" fill="#eaf4ef" stroke="#256046" stroke-width="1.6"></rect><foreignObject x="29.5" y="413" width="52" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#256046;line-height:1.4;padding:0 4px;box-sizing:border-box">完成</div></foreignObject></svg>
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="224" height="419" viewBox="-18 -18 224 419" style="background:#fff"><rect x="12" y="12" width="164" height="226" rx="8" fill="#f5f9fe" fill-opacity="0.38" stroke="#3f6fb0" stroke-width="2"></rect><foreignObject x="59.5" y="17" width="69" height="33"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="text-align:center;font-weight:bold;font-size:15px;color:#3f6fb0;line-height:1.3">第一層</div></foreignObject><rect x="26" y="54" width="136" height="159" rx="8" fill="#f0f7f3" fill-opacity="0.38" stroke="#348a5c" stroke-width="2"></rect><foreignObject x="59.5" y="59" width="69" height="33"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="text-align:center;font-weight:bold;font-size:15px;color:#348a5c;line-height:1.3">第二層</div></foreignObject><rect x="40" y="96" width="108" height="92" rx="8" fill="#fdf6ec" fill-opacity="0.38" stroke="#c46e1a" stroke-width="2"></rect><foreignObject x="59.5" y="101" width="69" height="33"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="text-align:center;font-weight:bold;font-size:15px;color:#c46e1a;line-height:1.3">第三層</div></foreignObject><path d="M 94 174 L 94 335" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 94 335 L 98 326 L 90 326 Z" fill="#44505a"></path><foreignObject x="54" y="283" width="44" height="22"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:12.5px;color:#4a5560;text-shadow:0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff;white-space:nowrap">跨三層</div></foreignObject><rect x="54" y="138" width="80" height="36" rx="7" fill="#fcf0e2" stroke="#c46e1a" stroke-width="1.6"></rect><foreignObject x="54" y="138" width="80" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">最深節點</div></foreignObject><rect x="54" y="335" width="80" height="36" rx="7" fill="#eef4fb" stroke="#3f6fb0" stroke-width="1.6"></rect><foreignObject x="54" y="335" width="80" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">頂層節點</div></foreignObject></svg>
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="197" height="357" viewBox="-18 -18 197 357" style="background:#fff"><path d="M 55.5 48 L 55.5 82" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 55.5 82 L 59.5 73 L 51.5 73 Z" fill="#44505a"></path><path d="M 41 169 L 41 273" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 41 273 L 45 264 L 37 264 Z" fill="#44505a"></path><foreignObject x="26" y="221" width="19" height="22"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:12.5px;color:#4a5560;text-shadow:0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff;white-space:nowrap">是</div></foreignObject><path d="M 70 169 L 70 195 L 123 195 L 123 273" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 123 273 L 127 264 L 119 264 Z" fill="#44505a"></path><foreignObject x="126" y="221" width="19" height="22"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:12.5px;color:#4a5560;text-shadow:0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff;white-space:nowrap">否</div></foreignObject><rect x="29.5" y="12" width="52" height="36" rx="7" fill="#eef4fb" stroke="#3f6fb0" stroke-width="1.6"></rect><foreignObject x="29.5" y="12" width="52" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">輸入</div></foreignObject><polygon points="55.5,82 99,125.5 55.5,169 12,125.5" fill="#fcf0e2" stroke="#c46e1a" stroke-width="1.8"></polygon><foreignObject x="12" y="82" width="87" height="87"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#23303a;line-height:1.4;padding:0 4px;box-sizing:border-box">判斷?</div></foreignObject><rect x="15" y="273" width="52" height="36" rx="7" fill="#eaf4ef" stroke="#348a5c" stroke-width="1.6"></rect><foreignObject x="15" y="273" width="52" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">通過</div></foreignObject><rect x="97" y="273" width="52" height="36" rx="7" fill="#fbecea" stroke="#c0392b" stroke-width="1.6"></rect><foreignObject x="97" y="273" width="52" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#23303a;line-height:1.4;padding:0 4px;box-sizing:border-box">退回</div></foreignObject></svg>
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="222" height="324" viewBox="-18 -18 222 324" style="background:#fff"><path d="M 64 48 L 64 152" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 64 152 L 68 143 L 60 143 Z" fill="#44505a"></path><foreignObject x="12" y="100" width="56" height="22"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:12.5px;color:#4a5560;text-shadow:0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff;white-space:nowrap">實線標籤</div></foreignObject><path d="M 81.33333333333334 48 L 81.33333333333334 74 L 121 74 L 121 214 L 86 214 L 86 240" fill="none" stroke="#44505a" stroke-width="2" stroke-dasharray="6 4"></path><path d="M 86 240 L 90 231 L 82 231 Z" fill="#44505a"></path><foreignObject x="124" y="100" width="56" height="22"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:12.5px;color:#4a5560;text-shadow:0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff;white-space:nowrap">虛線標籤</div></foreignObject><path d="M 64 188 L 64 240" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 64 240 L 68 231 L 60 231 Z" fill="#44505a"></path><rect x="46.66666666666667" y="12" width="52" height="36" rx="7" fill="#eef4fb" stroke="#3f6fb0" stroke-width="1.6"></rect><foreignObject x="46.66666666666667" y="12" width="52" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">來源</div></foreignObject><rect x="31" y="152" width="66" height="36" rx="7" fill="#eaf4ef" stroke="#348a5c" stroke-width="1.6"></rect><foreignObject x="31" y="152" width="66" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">目標一</div></foreignObject><rect x="42" y="240" width="66" height="36" rx="7" fill="#fcf0e2" stroke="#c46e1a" stroke-width="1.6"></rect><foreignObject x="42" y="240" width="66" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">目標二</div></foreignObject></svg>
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="284" height="375" viewBox="-18 -18 284 375" style="background:#fff"><path d="M 124 48 L 124 134" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 124 134 L 128 125 L 120 125 Z" fill="#44505a"></path><foreignObject x="83" y="82" width="45" height="22"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:12.5px;color:#4a5560;text-shadow:0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff;white-space:nowrap">HTTPS</div></foreignObject><path d="M 124 237 L 124 271" fill="none" stroke="#44505a" stroke-width="2" stroke-dasharray="6 4"></path><path d="M 124 271 L 128 262 L 120 262 Z" fill="#44505a"></path><rect x="70.5" y="12" width="107" height="36" rx="7" fill="#eef4fb" stroke="#3f6fb0" stroke-width="1.6"></rect><foreignObject x="70.5" y="12" width="107" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">API Gateway</div></foreignObject><rect x="45.5" y="134" width="157" height="103" rx="7" fill="#eaf4ef"></rect><path d="M 52.5 134 H 195.5 A 7 7 0 0 1 202.5 141 V 166 H 45.5 V 141 A 7 7 0 0 1 52.5 134 Z" fill="#cde3d7"></path><rect x="45.5" y="134" width="157" height="103" rx="7" fill="none" stroke="#348a5c" stroke-width="1.6"></rect><line x1="45.5" y1="166" x2="202.5" y2="166" stroke="#348a5c" stroke-width="1"></line><foreignObject x="45.5" y="134" width="157" height="32"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-weight:bold;font-size:14px;color:#1c2b36">Auth Service</div></foreignObject><foreignObject x="45.5" y="166" width="157" height="71"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:13px;line-height:1.55;padding:6px 9px 4px;text-align:left;color:#1c2b36"><div style="white-space:nowrap">• login/logout</div><div style="white-space:nowrap">• token refresh (JWT)</div><div style="white-space:nowrap">• session mgmt</div></div></foreignObject><rect x="12" y="271" width="224" height="56" rx="7" fill="#fcf0e2" stroke="#c46e1a" stroke-width="1.6"></rect><foreignObject x="12" y="271" width="224" height="56"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">InternationalizationConfigurationManager</div></foreignObject></svg>
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="126" height="236" viewBox="-18 -18 126 236" style="background:#fff"><path d="M 45 48 L 45 82" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 45 82 L 49 73 L 41 73 Z" fill="#44505a"></path><path d="M 45 118 L 45 152" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 45 152 L 49 143 L 41 143 Z" fill="#44505a"></path><rect x="12" y="12" width="66" height="36" rx="7" fill="#eef4fb" stroke="#3f6fb0" stroke-width="1.6"></rect><foreignObject x="12" y="12" width="66" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">節點 A</div></foreignObject><rect x="12.5" y="82" width="65" height="36" rx="7" fill="#eaf4ef" stroke="#348a5c" stroke-width="1.6"></rect><foreignObject x="12.5" y="82" width="65" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">節點 B</div></foreignObject><rect x="12.5" y="152" width="65" height="36" rx="7" fill="#fcf0e2" stroke="#c46e1a" stroke-width="1.6"></rect><foreignObject x="12.5" y="152" width="65" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">節點 C</div></foreignObject></svg>
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="384" height="304" viewBox="-18 -18 384 304" style="background:#fff"><rect x="12" y="12" width="324" height="244" rx="8" fill="#f5f9fe" fill-opacity="0.38" stroke="#3f6fb0" stroke-width="2"></rect><foreignObject x="117" y="17" width="114" height="33"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="text-align:center;font-weight:bold;font-size:15px;color:#3f6fb0;line-height:1.3">垂直長鏈容器</div></foreignObject><path d="M 69 100 L 69 130" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 69 130 L 73 121 L 65 121 Z" fill="#44505a"></path><path d="M 69 166 L 69 196" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 69 196 L 73 187 L 65 187 Z" fill="#44505a"></path><path d="M 69 232 L 69 250 L 121 250 L 121 46 L 174 46 L 174 64" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 174 64 L 178 55 L 170 55 Z" fill="#44505a"></path><path d="M 174 100 L 174 130" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 174 130 L 178 121 L 170 121 Z" fill="#44505a"></path><path d="M 174 166 L 174 196" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 174 196 L 178 187 L 170 187 Z" fill="#44505a"></path><path d="M 174 232 L 174 250 L 226 250 L 226 46 L 279 46 L 279 64" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 279 64 L 283 55 L 275 55 Z" fill="#44505a"></path><rect x="37" y="64" width="64" height="36" rx="7" fill="#eef4fb" stroke="#3f6fb0" stroke-width="1.6"></rect><foreignObject x="37" y="64" width="64" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">步驟 1</div></foreignObject><rect x="37" y="130" width="64" height="36" rx="7" fill="#eef4fb" stroke="#3f6fb0" stroke-width="1.6"></rect><foreignObject x="37" y="130" width="64" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">步驟 2</div></foreignObject><rect x="37" y="196" width="64" height="36" rx="7" fill="#eef4fb" stroke="#3f6fb0" stroke-width="1.6"></rect><foreignObject x="37" y="196" width="64" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">步驟 3</div></foreignObject><rect x="142" y="64" width="64" height="36" rx="7" fill="#eef4fb" stroke="#3f6fb0" stroke-width="1.6"></rect><foreignObject x="142" y="64" width="64" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">步驟 4</div></foreignObject><rect x="142" y="130" width="64" height="36" rx="7" fill="#eef4fb" stroke="#3f6fb0" stroke-width="1.6"></rect><foreignObject x="142" y="130" width="64" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">步驟 5</div></foreignObject><rect x="142" y="196" width="64" height="36" rx="7" fill="#eef4fb" stroke="#3f6fb0" stroke-width="1.6"></rect><foreignObject x="142" y="196" width="64" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">步驟 6</div></foreignObject><rect x="247" y="64" width="64" height="36" rx="7" fill="#eef4fb" stroke="#3f6fb0" stroke-width="1.6"></rect><foreignObject x="247" y="64" width="64" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">步驟 7</div></foreignObject></svg>
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="306" viewBox="-18 -18 360 306" style="background:#fff"><rect x="12" y="12" width="300" height="246" rx="8" fill="#fdf6ec" fill-opacity="0.38" stroke="#c46e1a" stroke-width="2"></rect><foreignObject x="105" y="17" width="114" height="33"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="text-align:center;font-weight:bold;font-size:15px;color:#c46e1a;line-height:1.3">水平長鏈容器</div></foreignObject><path d="M 100 72 L 130 72" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 130 72 L 121 68 L 121 76 Z" fill="#44505a"></path><path d="M 194 72 L 224 72" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 224 72 L 215 68 L 215 76 Z" fill="#44505a"></path><path d="M 288 72 L 306 72 L 306 110 L 18 110 L 18 149 L 36 149" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 36 149 L 27 145 L 27 153 Z" fill="#44505a"></path><path d="M 100 149 L 130 149" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 130 149 L 121 145 L 121 153 Z" fill="#44505a"></path><path d="M 194 149 L 224 149" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 224 149 L 215 145 L 215 153 Z" fill="#44505a"></path><path d="M 288 149 L 306 149 L 306 187 L 18 187 L 18 226 L 36 226" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 36 226 L 27 222 L 27 230 Z" fill="#44505a"></path><rect x="36" y="54" width="64" height="36" rx="7" fill="#fcf0e2" stroke="#c46e1a" stroke-width="1.6"></rect><foreignObject x="36" y="54" width="64" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">階段 1</div></foreignObject><rect x="130" y="54" width="64" height="36" rx="7" fill="#fcf0e2" stroke="#c46e1a" stroke-width="1.6"></rect><foreignObject x="130" y="54" width="64" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">階段 2</div></foreignObject><rect x="224" y="54" width="64" height="36" rx="7" fill="#fcf0e2" stroke="#c46e1a" stroke-width="1.6"></rect><foreignObject x="224" y="54" width="64" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">階段 3</div></foreignObject><rect x="36" y="131" width="64" height="36" rx="7" fill="#fcf0e2" stroke="#c46e1a" stroke-width="1.6"></rect><foreignObject x="36" y="131" width="64" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">階段 4</div></foreignObject><rect x="130" y="131" width="64" height="36" rx="7" fill="#fcf0e2" stroke="#c46e1a" stroke-width="1.6"></rect><foreignObject x="130" y="131" width="64" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">階段 5</div></foreignObject><rect x="224" y="131" width="64" height="36" rx="7" fill="#fcf0e2" stroke="#c46e1a" stroke-width="1.6"></rect><foreignObject x="224" y="131" width="64" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">階段 6</div></foreignObject><rect x="36" y="208" width="64" height="36" rx="7" fill="#fcf0e2" stroke="#c46e1a" stroke-width="1.6"></rect><foreignObject x="36" y="208" width="64" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">階段 7</div></foreignObject></svg>
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="223" height="366" viewBox="-18 -18 223 366" style="background:#fff"><rect x="79.5" y="12" width="85" height="92" rx="8" fill="#f5f9fe" fill-opacity="0.38" stroke="#3f6fb0" stroke-width="2"></rect><foreignObject x="80" y="17" width="84" height="33"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="text-align:center;font-weight:bold;font-size:15px;color:#3f6fb0;line-height:1.3">來源群組</div></foreignObject><rect x="24" y="226" width="84" height="92" rx="8" fill="#fdf6ec" fill-opacity="0.38" stroke="#c46e1a" stroke-width="2"></rect><foreignObject x="24" y="231" width="84" height="33"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="text-align:center;font-weight:bold;font-size:15px;color:#c46e1a;line-height:1.3">目標群組</div></foreignObject><path d="M 122 104 L 122 200 L 80 200 L 80 226" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 80 226 L 84 217 L 76 217 Z" fill="#44505a"></path><foreignObject x="125" y="147" width="56" height="22"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:12.5px;color:#4a5560;text-shadow:0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff;white-space:nowrap">群組相依</div></foreignObject><path d="M 52 174 L 52 226" fill="none" stroke="#44505a" stroke-width="2" stroke-dasharray="6 4"></path><path d="M 52 226 L 56 217 L 48 217 Z" fill="#44505a"></path><rect x="106.5" y="54" width="31" height="36" rx="7" fill="#eef4fb" stroke="#3f6fb0" stroke-width="1.6"></rect><foreignObject x="106.5" y="54" width="31" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">x</div></foreignObject><rect x="50" y="268" width="32" height="36" rx="7" fill="#fcf0e2" stroke="#c46e1a" stroke-width="1.6"></rect><foreignObject x="50" y="268" width="32" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">y</div></foreignObject><rect x="12" y="138" width="80" height="36" rx="7" fill="#eaf4ef" stroke="#348a5c" stroke-width="1.6"></rect><foreignObject x="12" y="138" width="80" height="36"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-size:14px;color:#1c2b36;line-height:1.4;padding:0 4px;box-sizing:border-box">外部節點</div></foreignObject></svg>
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="152" height="352" viewBox="-18 -18 152 352" style="background:#fff"><path d="M 58 155 L 58 241" fill="none" stroke="#44505a" stroke-width="2"></path><path d="M 58 241 L 62 232 L 54 232 Z" fill="#44505a"></path><foreignObject x="31" y="189" width="31" height="22"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:12.5px;color:#4a5560;text-shadow:0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff,0 0 3px #ffffff;white-space:nowrap">呼叫</div></foreignObject><rect x="18.5" y="12" width="79" height="143" rx="7" fill="#fcf0e2"></rect><path d="M 25.5 12 H 90.5 A 7 7 0 0 1 97.5 19 V 44 H 18.5 V 19 A 7 7 0 0 1 25.5 12 Z" fill="#f3dbc2"></path><rect x="18.5" y="12" width="79" height="143" rx="7" fill="none" stroke="#c46e1a" stroke-width="1.6"></rect><line x1="18.5" y1="44" x2="97.5" y2="44" stroke="#c46e1a" stroke-width="1"></line><foreignObject x="18.5" y="12" width="79" height="32"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-weight:bold;font-size:14px;color:#1c2b36">服務 X</div></foreignObject><foreignObject x="18.5" y="44" width="79" height="111"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:13px;line-height:1.55;padding:6px 9px 4px;text-align:left;color:#1c2b36"><div style="white-space:nowrap">• 項目一</div><div style="white-space:nowrap">• 項目二</div><div style="white-space:nowrap">• 項目三</div><div style="white-space:nowrap">• 項目四</div><div style="white-space:nowrap">• 項目五</div></div></foreignObject><rect x="12" y="241" width="92" height="63" rx="7" fill="#fcf0e2"></rect><path d="M 19 241 H 97 A 7 7 0 0 1 104 248 V 273 H 12 V 248 A 7 7 0 0 1 19 241 Z" fill="#f3dbc2"></path><rect x="12" y="241" width="92" height="63" rx="7" fill="none" stroke="#c46e1a" stroke-width="1.6"></rect><line x1="12" y1="273" x2="104" y2="273" stroke="#c46e1a" stroke-width="1"></line><foreignObject x="12" y="241" width="92" height="32"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center;font-weight:bold;font-size:14px;color:#1c2b36">服務 Y</div></foreignObject><foreignObject x="12" y="273" width="92" height="31"><div xmlns="http://www.w3.org/1999/xhtml" class="lbl" style="font-size:13px;line-height:1.55;padding:6px 9px 4px;text-align:left;color:#1c2b36"><div style="white-space:nowrap">• 只有一項</div></div></foreignObject></svg>
|
|
Binary file
|