Skip to content

Commit 5ec4faa

Browse files
committed
feat: svg viewer
1 parent 1974c1f commit 5ec4faa

16 files changed

Lines changed: 697 additions & 32 deletions

File tree

.vscode/settings.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,5 +111,7 @@
111111
"sonarlint.connectedMode.project": {
112112
"connectionId": "http-localhost-9000-",
113113
"projectKey": "pro-react-admin"
114-
}
114+
},
115+
"codegraphy.connectionType": "Interaction",
116+
"codegraphy.nodeSize": "Lines"
115117
}

docs/svg_tools.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# svg_tools 使用说明
2+
3+
合并后的脚本位于 `scripts/svg_tools.cjs`,用于检查和修复 `src/pages/svgViewer/index.jsx` 中内联 `SVG_SOURCE` 的常见问题(标签未闭合、`<br>` 标签缺斜杠等),以减少手动维护多个小脚本的开销。
4+
5+
前提
6+
- 需要在项目根目录下运行,且系统上安装有 Node.js。
7+
8+
默认目标文件
9+
- `src/pages/svgViewer/index.jsx`
10+
11+
用法
12+
13+
基本格式:
14+
15+
```bash
16+
node scripts/svg_tools.cjs <command> [--file=PATH]
17+
```
18+
19+
命令
20+
- `check`:分析 `SVG_SOURCE` 中常见标签计数,定位最后的 `<g>``</g>``</svg>` 出现位置,并尝试检测未闭合的 `<g>`。仅输出信息,不修改文件。
21+
22+
- `replace-br`:将 `SVG_SOURCE` 内的所有 `<br>` 替换为自闭合的 `<br/>`,会直接修改文件。
23+
24+
- `fix-unclosed-g`:统计 `<g>` 的开闭差异,并在 `</svg>` 之前插入缺失数量的 `</g>` 来尝试补齐。此命令会修改文件,建议先备份或先运行 `check` 查看差异。
25+
26+
示例
27+
28+
检查(默认文件):
29+
30+
```bash
31+
node scripts/svg_tools.cjs check
32+
```
33+
34+
对指定文件做替换 `<br>`
35+
36+
```bash
37+
node scripts/svg_tools.cjs replace-br --file=src/pages/svgViewer/index.jsx
38+
```
39+
40+
自动插入缺失 `</g>`
41+
42+
```bash
43+
node scripts/svg_tools.cjs fix-unclosed-g
44+
```
45+
46+
注意事项
47+
- 修改类命令会直接写回文件,请在执行前确认 Git 工作区干净,或手动创建备份。
48+
- `fix-unclosed-g` 是一个启发式修复:它通过统计差异并在 `</svg>` 前插入缺失的 `</g>`,适用于常见的“少一个闭合标签”场景,但不能保证修复所有结构性错误。建议先运行 `check` 并人工确认上下文后再执行自动插入。
49+
- 如果你已将内联 SVG 移出源码并替换为静态资源(例如放到 `public/`),可以不再使用此脚本。
50+
51+
示例工作流建议
52+
53+
```bash
54+
# 查看问题
55+
node scripts/svg_tools.cjs check
56+
57+
# 如果只有 <br> 问题:
58+
node scripts/svg_tools.cjs replace-br
59+
60+
# 如果缺少闭合 <g> 且你已确认:
61+
node scripts/svg_tools.cjs fix-unclosed-g
62+
63+
# 查看修改并提交
64+
git add -p
65+
git commit
66+
```
67+
68+
问题反馈
69+
- 如果脚本没有覆盖到你遇到的问题,请把 `src/pages/svgViewer/index.jsx` 的小片段(敏感信息打码)贴出来,我可以帮你改进脚本或手动修复。

scripts/svg_tools.cjs

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
#!/usr/bin/env node
2+
const fs = require('fs');
3+
const path = require('path');
4+
5+
function resolveTargetFile(provided) {
6+
if (provided) return path.resolve(provided);
7+
return path.join(__dirname, '..', 'src', 'pages', 'svgViewer', 'index.jsx');
8+
}
9+
10+
function readFile(fp) {
11+
try {
12+
return fs.readFileSync(fp, 'utf8');
13+
} catch (e) {
14+
console.error('Cannot read file:', fp);
15+
process.exit(2);
16+
}
17+
}
18+
19+
function writeFile(fp, content) {
20+
fs.writeFileSync(fp, content, 'utf8');
21+
}
22+
23+
function findSvgSource(content) {
24+
const m = content.match(/const\s+SVG_SOURCE\s*=\s*`([\s\S]*?)`;/m);
25+
return m ? { fullMatch: m[0], svg: m[1], index: m.index } : null;
26+
}
27+
28+
function showContext(svg, idx, label) {
29+
if (idx < 0) {
30+
console.log(`${label}: not found`);
31+
return;
32+
}
33+
const start = Math.max(0, idx - 200);
34+
const end = Math.min(svg.length, idx + 200);
35+
console.log(`\n${label} context (around index ${idx}):\n---START---\n${svg.slice(start, end)}\n---END---`);
36+
}
37+
38+
function cmd_check(fp) {
39+
const content = readFile(fp);
40+
const found = findSvgSource(content);
41+
if (!found) {
42+
console.error('Cannot find SVG_SOURCE in file');
43+
process.exit(2);
44+
}
45+
const svg = found.svg;
46+
function countTag(tag) {
47+
const openRe = new RegExp('<' + tag + '[\\s>]', 'gi');
48+
const closeRe = new RegExp('</' + tag + '>', 'gi');
49+
const opens = (svg.match(openRe) || []).length;
50+
const closes = (svg.match(closeRe) || []).length;
51+
return { opens, closes };
52+
}
53+
const tags = ['svg','g','foreignObject','div','span','p','br'];
54+
const counts = {};
55+
for (const t of tags) counts[t] = countTag(t);
56+
console.log('Tag counts:');
57+
for (const t of tags) console.log(` ${t}: open=${counts[t].opens} close=${counts[t].closes}`);
58+
59+
const lastG = svg.lastIndexOf('<g');
60+
const lastCloseG = svg.lastIndexOf('</g>');
61+
const lastCloseSvg = svg.lastIndexOf('</svg>');
62+
console.log('\nPositions (char index):');
63+
console.log(' last <g at', lastG);
64+
console.log(' last </g> at', lastCloseG);
65+
console.log(' last </svg> at', lastCloseSvg);
66+
67+
showContext(svg, lastG, '<g (last)');
68+
showContext(svg, lastCloseG, '</g> (last)');
69+
showContext(svg, lastCloseSvg, '</svg> (last)');
70+
71+
// stack-based scan for unclosed <g>
72+
const tagRegex = /<\/?g[\s>]/gi;
73+
let stack = [];
74+
let m;
75+
while ((m = tagRegex.exec(svg)) !== null) {
76+
if (m[0].startsWith('</')) {
77+
if (stack.length === 0) {
78+
console.log('\nStray </g> at', m.index);
79+
showContext(svg, m.index, 'stray </g>');
80+
break;
81+
} else {
82+
stack.pop();
83+
}
84+
} else {
85+
stack.push(m.index);
86+
}
87+
}
88+
if (stack.length > 0) {
89+
const lastUnclosed = stack[stack.length - 1];
90+
console.log('\nFound unclosed <g> (last open at index', lastUnclosed + ')');
91+
showContext(svg, lastUnclosed, 'unclosed <g>');
92+
} else {
93+
console.log('\nAll <g> tags appear closed (no unclosed <g> found)');
94+
}
95+
}
96+
97+
function cmd_replace_br(fp) {
98+
const content = readFile(fp);
99+
const found = findSvgSource(content);
100+
if (!found) {
101+
console.error('Cannot find SVG_SOURCE');
102+
process.exit(2);
103+
}
104+
const svg = found.svg;
105+
const replaced = svg.replace(/<br>/g, '<br/>');
106+
if (replaced === svg) {
107+
console.log('No <br> occurrences found to replace');
108+
process.exit(0);
109+
}
110+
const newContent = content.replace(found.fullMatch, `const SVG_SOURCE = ` + '`' + replaced + '`;');
111+
writeFile(fp, newContent);
112+
console.log('Replaced <br> with <br/> in SVG_SOURCE');
113+
}
114+
115+
function cmd_fix_unclosed_g(fp) {
116+
const content = readFile(fp);
117+
const found = findSvgSource(content);
118+
if (!found) {
119+
console.error('Cannot find SVG_SOURCE');
120+
process.exit(2);
121+
}
122+
let svg = found.svg;
123+
const openCount = (svg.match(/<g[\s>]/gi) || []).length;
124+
const closeCount = (svg.match(/<\/g>/gi) || []).length;
125+
const missing = openCount - closeCount;
126+
if (missing <= 0) {
127+
console.log('No missing </g> tags detected (open:', openCount, 'close:', closeCount, ')');
128+
process.exit(0);
129+
}
130+
// insert missing number of </g> before the last </svg>
131+
const lastCloseSvgIdx = svg.lastIndexOf('</svg>');
132+
if (lastCloseSvgIdx === -1) {
133+
console.error('No </svg> found, aborting');
134+
process.exit(2);
135+
}
136+
const inserts = '</g>'.repeat(missing);
137+
const newSvg = svg.slice(0, lastCloseSvgIdx) + inserts + svg.slice(lastCloseSvgIdx);
138+
const newContent = content.replace(found.fullMatch, `const SVG_SOURCE = ` + '`' + newSvg + '`;');
139+
writeFile(fp, newContent);
140+
console.log(`Inserted ${missing} </g> before closing </svg>`);
141+
}
142+
143+
function usage() {
144+
console.log('Usage: node scripts/svg_tools.cjs <command> [--file=path]');
145+
console.log('Commands:');
146+
console.log(' check : analyze tag counts and find unclosed <g>');
147+
console.log(' replace-br : replace <br> with <br/> inside SVG_SOURCE');
148+
console.log(' fix-unclosed-g : insert missing </g> before </svg> to balance <g> tags');
149+
console.log('\nOptions:');
150+
console.log(' --file=PATH : specify target file (defaults to src/pages/svgViewer/index.jsx)');
151+
}
152+
153+
function main() {
154+
const rawArgs = process.argv.slice(2);
155+
if (rawArgs.length === 0) return usage();
156+
let cmd = rawArgs[0];
157+
let fileArg = null;
158+
for (let i = 1; i < rawArgs.length; i++) {
159+
const a = rawArgs[i];
160+
if (a.startsWith('--file=')) fileArg = a.split('=')[1];
161+
else if (a === '--file' && rawArgs[i+1]) { fileArg = rawArgs[i+1]; i++; }
162+
}
163+
const fp = resolveTargetFile(fileArg);
164+
if (!fs.existsSync(fp)) {
165+
console.error('Target file does not exist:', fp);
166+
process.exit(2);
167+
}
168+
if (cmd === 'check') return cmd_check(fp);
169+
if (cmd === 'replace-br') return cmd_replace_br(fp);
170+
if (cmd === 'fix-unclosed-g') return cmd_fix_unclosed_g(fp);
171+
usage();
172+
}
173+
174+
main();

src/assets/svg/architecture-diagram-rcba.svg

Lines changed: 2 additions & 0 deletions
Loading

src/assets/svg/architecture-diagram.svg

Lines changed: 1 addition & 0 deletions
Loading

src/assets/svg/mermaid-diagram-pro.svg

Lines changed: 1 addition & 0 deletions
Loading

src/components/stateless/ScrollLayout/index.module.less

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@
1515
.content {
1616
flex: 1;
1717
width: 100%;
18-
overflow-y: auto;
19-
overflow-x: hidden;
18+
overflow: hidden auto;
2019
position: relative;
2120
}
2221

src/config/menu.config.jsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
HeatMapOutlined,
66
ApartmentOutlined,
77
QuestionCircleOutlined,
8-
FireOutlined,
98
GlobalOutlined,
109
QrcodeOutlined,
1110
RocketOutlined,
@@ -21,7 +20,6 @@ import {
2120
RobotOutlined,
2221
SendOutlined,
2322
EnvironmentOutlined,
24-
DotChartOutlined,
2523
PrinterOutlined,
2624
UserOutlined,
2725
ContactsOutlined,
@@ -40,6 +38,12 @@ import {
4038
const rawMainLayoutMenu = [
4139
{ label: 'home', i18nKey: 'home', key: '/', icon: <HomeOutlined /> },
4240
{ label: 'demo', i18nKey: 'demo', key: '/demo', icon: <GlobalOutlined /> },
41+
{
42+
label: 'SVG Viewer',
43+
i18nKey: 'menu.svgViewer',
44+
key: '/svg-viewer',
45+
icon: <HeatMapOutlined />,
46+
},
4347
{
4448
label: 'Zustand演示',
4549
i18nKey: 'menu.zustand',
@@ -280,7 +284,7 @@ const rawMainLayoutMenu = [
280284
function normalizeMenu(items) {
281285
return items.map((it) => {
282286
const { children, ...rest } = it
283-
const normalized = { ...rest, path: (it && it.path) || it.key }
287+
const normalized = { ...rest, path: it?.path || it.key }
284288
if (children && Array.isArray(children)) {
285289
normalized.children = normalizeMenu(children)
286290
}

src/locales/en/translation.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ const en = {
7373
motion: 'Motion',
7474
mermaid: 'Mermaid',
7575
topology: 'Topology',
76+
svgViewer: 'SVG Viewer',
7677
permissionExample: 'Permission',
7778
phBar: 'PH Bar',
7879
chatgpt: 'ChatGPT',

src/locales/zh/translation.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ const zh = {
7373
motion: '动效',
7474
mermaid: 'Mermaid',
7575
topology: '拓扑图',
76+
svgViewer: 'SVG 查看器',
7677
permissionExample: '权限示例',
7778
phBar: 'PH Bar',
7879
chatgpt: 'ChatGPT',

0 commit comments

Comments
 (0)