forked from Gitlink/gitlink-cli
643 lines
23 KiB
HTML
643 lines
23 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>知识图谱 - gitlink</title>
|
||
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body {
|
||
font-family: 'Segoe UI', -apple-system, sans-serif;
|
||
background: #f8fafc;
|
||
color: #1e293b;
|
||
height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
.toolbar {
|
||
background: #ffffff;
|
||
border-bottom: 1px solid #e2e8f0;
|
||
padding: 12px 24px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
flex-shrink: 0;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
z-index: 10;
|
||
}
|
||
.toolbar h1 {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.toolbar h1 .badge {
|
||
font-size: 11px;
|
||
font-weight: 400;
|
||
background: #e2e8f0;
|
||
padding: 2px 8px;
|
||
border-radius: 10px;
|
||
color: #64748b;
|
||
}
|
||
.toolbar .actions {
|
||
display: flex;
|
||
gap: 6px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.toolbar .actions button {
|
||
background: #f1f5f9;
|
||
border: 1px solid #e2e8f0;
|
||
color: #475569;
|
||
padding: 6px 14px;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
transition: all 0.15s;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
.toolbar .actions button:hover {
|
||
background: #e2e8f0;
|
||
border-color: #94a3b8;
|
||
}
|
||
.toolbar .actions button.primary {
|
||
background: #3b82f6;
|
||
border-color: #3b82f6;
|
||
color: white;
|
||
}
|
||
.toolbar .actions button.primary:hover {
|
||
background: #2563eb;
|
||
}
|
||
.main {
|
||
display: flex;
|
||
flex: 1;
|
||
min-height: 0;
|
||
position: relative;
|
||
}
|
||
.graph-area {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
position: relative;
|
||
background: #ffffff;
|
||
cursor: grab;
|
||
}
|
||
.graph-area:active { cursor: grabbing; }
|
||
.graph-area .mermaid-wrapper {
|
||
position: absolute;
|
||
transform-origin: 0 0;
|
||
transition: transform 0.1s ease;
|
||
}
|
||
.graph-area svg {
|
||
max-width: none !important;
|
||
display: block;
|
||
}
|
||
.sidebar {
|
||
width: 300px;
|
||
background: #ffffff;
|
||
border-left: 1px solid #e2e8f0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex-shrink: 0;
|
||
overflow: hidden;
|
||
}
|
||
.sidebar .sidebar-header {
|
||
padding: 14px 16px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
border-bottom: 1px solid #e2e8f0;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
.sidebar .sidebar-header .count {
|
||
font-weight: 400;
|
||
color: #94a3b8;
|
||
font-size: 12px;
|
||
}
|
||
.sidebar .project-list {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 8px;
|
||
}
|
||
.sidebar .project-item {
|
||
padding: 8px 10px;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
border: 1px solid transparent;
|
||
margin-bottom: 4px;
|
||
transition: all 0.15s;
|
||
}
|
||
.sidebar .project-item:hover {
|
||
background: #f1f5f9;
|
||
border-color: #e2e8f0;
|
||
}
|
||
.sidebar .project-item .proj-name {
|
||
font-weight: 500;
|
||
color: #1e293b;
|
||
}
|
||
.sidebar .project-item .proj-desc {
|
||
color: #64748b;
|
||
font-size: 11px;
|
||
margin-top: 2px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.sidebar .project-item .proj-topics {
|
||
display: flex;
|
||
gap: 4px;
|
||
flex-wrap: wrap;
|
||
margin-top: 4px;
|
||
}
|
||
.sidebar .project-item .proj-topics span {
|
||
background: #f0f9ff;
|
||
color: #0369a1;
|
||
font-size: 10px;
|
||
padding: 1px 6px;
|
||
border-radius: 4px;
|
||
}
|
||
.sidebar .project-item.highlight {
|
||
background: #eff6ff;
|
||
border-color: #3b82f6;
|
||
}
|
||
.status-bar {
|
||
position: absolute;
|
||
bottom: 12px;
|
||
left: 12px;
|
||
background: rgba(15,23,42,0.85);
|
||
color: #e2e8f0;
|
||
padding: 6px 14px;
|
||
border-radius: 8px;
|
||
font-size: 12px;
|
||
pointer-events: none;
|
||
opacity: 0;
|
||
transition: opacity 0.3s;
|
||
}
|
||
.status-bar.visible { opacity: 1; }
|
||
.legend-panel {
|
||
position: absolute;
|
||
bottom: 12px;
|
||
right: 12px;
|
||
background: rgba(255,255,255,0.95);
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 8px;
|
||
padding: 10px 14px;
|
||
font-size: 11px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 5px;
|
||
}
|
||
.legend-panel .row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.legend-panel .color-box {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 3px;
|
||
}
|
||
.legend-panel .line-sample {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
.legend-panel .line-sample .line {
|
||
width: 20px;
|
||
height: 2px;
|
||
border-radius: 1px;
|
||
}
|
||
@media (max-width: 768px) {
|
||
.sidebar { display: none; }
|
||
.toolbar { padding: 8px 12px; }
|
||
.toolbar h1 { font-size: 14px; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="toolbar">
|
||
<h1>
|
||
🔬 知识图谱:gitlink
|
||
<span class="badge">24 个项目</span>
|
||
</h1>
|
||
<div class="actions">
|
||
<button onclick="fitToScreen()">⊞ 适应屏幕</button>
|
||
<button onclick="zoomIn()">+ 放大</button>
|
||
<button onclick="zoomOut()">− 缩小</button>
|
||
<button onclick="resetView()">⟲ 重置</button>
|
||
<button class="primary" onclick="downloadSVG()">📥 导出 SVG</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="main">
|
||
<div class="graph-area" id="graphArea">
|
||
<div class="mermaid-wrapper" id="mermaidWrapper">
|
||
<pre class="mermaid" id="mermaidGraph">
|
||
flowchart LR
|
||
subgraph Projects["🔬 研究项目"]
|
||
P1["Gitlink/gitlink-notification-system\n⭐9 · Gitlink Notification System 源码以及说明"]
|
||
P2["Gitlink/gitlink-cli\n⭐5 · GitLink CLI - GitLink 平台命令行工具"]
|
||
P3["chroe/gitlink-cli\n⭐3 · GitLink CLI - GitLink 平台命令行工具"]
|
||
P4["wangtao/gitlink-cli\n⭐0 · GitLink CLI - GitLink 平台命令行工具"]
|
||
P5["otto/gitlink-cli\n⭐0 · GitLink CLI - GitLink 平台命令行工具"]
|
||
P6["mozilong/gitlink-cli\n⭐0 · GitLink CLI - GitLink 平台命令行工具"]
|
||
P7["topshare/gitlink-cli\n⭐0 · GitLink CLI - GitLink 平台命令行工具"]
|
||
P8["yingjie/gitlink-cli\n⭐0 · GitLink CLI - GitLink 平台命令行工具"]
|
||
P9["floraachy/gitlink-cli\n⭐0 · GitLink CLI - GitLink 平台命令行工具"]
|
||
P10["wangyue111/gitlink-cli\n⭐0 · GitLink CLI - GitLink 平台命令行工具"]
|
||
end
|
||
|
||
subgraph Topics["📚 热门主题"]
|
||
T1["java"]
|
||
T2["dockerfile"]
|
||
T3["batchfile"]
|
||
T4["swift"]
|
||
end
|
||
|
||
subgraph Techs["💻 技术栈"]
|
||
L1["Go"]
|
||
L2["Java"]
|
||
end
|
||
|
||
subgraph Orgs["🏛️ 组织机构"]
|
||
O1["Gitlink"]
|
||
O2["wangtao"]
|
||
O3["otto"]
|
||
O4["mozilong"]
|
||
end
|
||
|
||
P1 -->|topic| T1
|
||
P1 -->|topic| T2
|
||
P1 -->|topic| T3
|
||
P1 -->|belongs_to| O1
|
||
P2 -->|belongs_to| O1
|
||
P4 -->|belongs_to| O2
|
||
P5 -->|belongs_to| O3
|
||
P6 -->|belongs_to| O4
|
||
P2 -->|uses| L1
|
||
P3 -->|uses| L1
|
||
P9 -->|uses| L1
|
||
P6 -->|uses| L1
|
||
P5 -->|uses| L1
|
||
P7 -->|uses| L1
|
||
P4 -->|uses| L1
|
||
P10 -->|uses| L1
|
||
P8 -->|uses| L1
|
||
|
||
classDef project fill:#e1f5fe,stroke:#01579b,color:#01579b
|
||
classDef topic fill:#f3e5f5,stroke:#7b1fa2,color:#7b1fa2
|
||
classDef tech fill:#e8f5e9,stroke:#2e7d32,color:#2e7d32
|
||
classDef org fill:#fff3e0,stroke:#e65100,color:#e65100
|
||
|
||
class P1,P2,P3,P4,P5,P6,P7,P8,P9,P10 project
|
||
class T1,T2,T3,T4 topic
|
||
class L1,L2 tech
|
||
class O1,O2,O3,O4 org
|
||
</pre>
|
||
</div>
|
||
<div class="status-bar" id="statusBar">就绪</div>
|
||
<div class="legend-panel">
|
||
<div class="row"><div class="color-box" style="background:#3b82f6"></div> 项目</div>
|
||
<div class="row"><div class="color-box" style="background:#a78bfa"></div> 主题</div>
|
||
<div class="row"><div class="color-box" style="background:#22c55e"></div> 技术栈</div>
|
||
<div class="row"><div class="color-box" style="background:#f97316"></div> 组织</div>
|
||
<div class="row" style="margin-top:2px;color:#94a3b8;font-size:10px;">🖱 滚轮缩放 · 拖拽平移</div>
|
||
</div>
|
||
</div>
|
||
<div class="sidebar">
|
||
<div class="sidebar-header">
|
||
项目列表
|
||
<span class="count">24</span>
|
||
</div>
|
||
<div class="project-list" id="projectList">
|
||
<div style="color:#94a3b8;font-size:12px;padding:8px;text-align:center;">等待图谱加载...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let scale = 0.8, panX = 0, panY = 0;
|
||
let isPanning = false, startX, startY, startPanX, startPanY;
|
||
let svgElement = null;
|
||
let currentHighlight = null;
|
||
|
||
mermaid.initialize({
|
||
startOnLoad: true,
|
||
theme: 'default',
|
||
themeVariables: {
|
||
primaryColor: '#eff6ff',
|
||
primaryBorderColor: '#3b82f6',
|
||
primaryTextColor: '#1e293b',
|
||
lineColor: '#94a3b8',
|
||
secondaryColor: '#f5f3ff',
|
||
tertiaryColor: '#f0fdf4',
|
||
fontFamily: 'Segoe UI, sans-serif',
|
||
fontSize: '13px',
|
||
mainBkg: '#ffffff',
|
||
nodeBorder: '#e2e8f0',
|
||
clusterBkg: '#f8fafc',
|
||
clusterBorder: '#e2e8f0',
|
||
titleColor: '#1e293b',
|
||
edgeLabelBackground: '#ffffff',
|
||
nodeTextColor: '#1e293b',
|
||
},
|
||
flowchart: {
|
||
useMaxWidth: false,
|
||
htmlLabels: true,
|
||
curve: 'basis',
|
||
padding: 16,
|
||
nodeSpacing: 60,
|
||
rankSpacing: 80,
|
||
}
|
||
});
|
||
|
||
// 等 Mermaid 渲染完成后设置交互
|
||
setTimeout(() => {
|
||
svgElement = document.querySelector('#mermaidGraph svg');
|
||
if (!svgElement) {
|
||
document.getElementById('statusBar').textContent = '⚠️ 图谱渲染失败,请检查 Mermaid 语法';
|
||
document.getElementById('statusBar').classList.add('visible');
|
||
return;
|
||
}
|
||
|
||
// 使 SVG 响应式
|
||
svgElement.style.maxWidth = 'none';
|
||
const box = svgElement.getBBox();
|
||
svgElement.setAttribute('viewBox', `${box.x} ${box.y} ${box.width} ${box.height}`);
|
||
|
||
const wrapper = document.getElementById('mermaidWrapper');
|
||
wrapper.style.width = box.width + 'px';
|
||
wrapper.style.height = box.height + 'px';
|
||
|
||
// 适应屏幕
|
||
fitToScreen();
|
||
|
||
// 构建项目列表
|
||
buildProjectList();
|
||
setupNodeInteractions();
|
||
setupPanZoom();
|
||
|
||
setStatus('✅ 图谱已加载 — 滚轮缩放 · 拖拽平移 · 点击节点高亮');
|
||
}, 800);
|
||
|
||
function fitToScreen() {
|
||
if (!svgElement) return;
|
||
const area = document.getElementById('graphArea');
|
||
const box = svgElement.getBBox();
|
||
const areaW = area.clientWidth - 40;
|
||
const areaH = area.clientHeight - 40;
|
||
const s = Math.min(areaW / (box.width || 800), areaH / (box.height || 600), 1.2);
|
||
scale = Math.max(0.2, Math.min(s, 2));
|
||
panX = (areaW - box.width * scale) / 2;
|
||
panY = (areaH - box.height * scale) / 2;
|
||
applyTransform();
|
||
setStatus('⊞ 已适应屏幕');
|
||
}
|
||
|
||
function zoomIn() {
|
||
scale = Math.min(3, scale + 0.15);
|
||
applyTransform();
|
||
setStatus('🔍 ' + Math.round(scale * 100) + '%');
|
||
}
|
||
|
||
function zoomOut() {
|
||
scale = Math.max(0.2, scale - 0.15);
|
||
applyTransform();
|
||
setStatus('🔍 ' + Math.round(scale * 100) + '%');
|
||
}
|
||
|
||
function resetView() {
|
||
scale = 0.8; panX = 0; panY = 0;
|
||
applyTransform();
|
||
fitToScreen();
|
||
}
|
||
|
||
function applyTransform() {
|
||
const wrapper = document.getElementById('mermaidWrapper');
|
||
wrapper.style.transform = `translate(${panX}px, ${panY}px) scale(${scale})`;
|
||
}
|
||
|
||
function setupPanZoom() {
|
||
const area = document.getElementById('graphArea');
|
||
|
||
area.addEventListener('wheel', (e) => {
|
||
e.preventDefault();
|
||
const rect = area.getBoundingClientRect();
|
||
const mx = e.clientX - rect.left;
|
||
const my = e.clientY - rect.top;
|
||
const delta = e.deltaY > 0 ? -0.08 : 0.08;
|
||
const newScale = Math.max(0.2, Math.min(3, scale + delta));
|
||
const ratio = newScale / scale;
|
||
panX = mx - (mx - panX) * ratio;
|
||
panY = my - (my - panY) * ratio;
|
||
scale = newScale;
|
||
applyTransform();
|
||
}, { passive: false });
|
||
|
||
area.addEventListener('mousedown', (e) => {
|
||
if (e.target.closest('.node') || e.target.closest('.edgeLabel') || e.target.closest('button')) return;
|
||
isPanning = true;
|
||
startX = e.clientX;
|
||
startY = e.clientY;
|
||
startPanX = panX;
|
||
startPanY = panY;
|
||
area.style.cursor = 'grabbing';
|
||
});
|
||
|
||
window.addEventListener('mousemove', (e) => {
|
||
if (!isPanning) return;
|
||
panX = startPanX + (e.clientX - startX);
|
||
panY = startPanY + (e.clientY - startY);
|
||
applyTransform();
|
||
});
|
||
|
||
window.addEventListener('mouseup', () => {
|
||
if (isPanning) {
|
||
isPanning = false;
|
||
document.getElementById('graphArea').style.cursor = 'grab';
|
||
}
|
||
});
|
||
|
||
// 触摸支持
|
||
let lastTouchDist = 0, lastTouchX, lastTouchY;
|
||
area.addEventListener('touchstart', (e) => {
|
||
if (e.touches.length === 2) {
|
||
lastTouchDist = Math.hypot(
|
||
e.touches[0].clientX - e.touches[1].clientX,
|
||
e.touches[0].clientY - e.touches[1].clientY
|
||
);
|
||
} else if (e.touches.length === 1) {
|
||
isPanning = true;
|
||
startX = e.touches[0].clientX;
|
||
startY = e.touches[0].clientY;
|
||
startPanX = panX;
|
||
startPanY = panY;
|
||
}
|
||
}, { passive: true });
|
||
|
||
area.addEventListener('touchmove', (e) => {
|
||
e.preventDefault();
|
||
if (e.touches.length === 2) {
|
||
const dist = Math.hypot(
|
||
e.touches[0].clientX - e.touches[1].clientX,
|
||
e.touches[0].clientY - e.touches[1].clientY
|
||
);
|
||
if (lastTouchDist > 0) {
|
||
const delta = (dist - lastTouchDist) / 200;
|
||
scale = Math.max(0.2, Math.min(3, scale + delta));
|
||
applyTransform();
|
||
}
|
||
lastTouchDist = dist;
|
||
} else if (e.touches.length === 1 && isPanning) {
|
||
panX = startPanX + (e.touches[0].clientX - startX);
|
||
panY = startPanY + (e.touches[0].clientY - startY);
|
||
applyTransform();
|
||
}
|
||
}, { passive: false });
|
||
|
||
area.addEventListener('touchend', () => {
|
||
isPanning = false;
|
||
lastTouchDist = 0;
|
||
}, { passive: true });
|
||
}
|
||
|
||
function setupNodeInteractions() {
|
||
document.querySelectorAll('.node').forEach(node => {
|
||
node.style.cursor = 'pointer';
|
||
|
||
node.addEventListener('click', () => {
|
||
const text = node.querySelector('text')?.textContent || '';
|
||
const title = text.split('⭐')[0].trim();
|
||
|
||
// 高亮侧边栏
|
||
document.querySelectorAll('.project-item').forEach(item => {
|
||
item.classList.toggle('highlight', item.dataset.name === title);
|
||
});
|
||
|
||
// 高亮图中节点
|
||
document.querySelectorAll('.node').forEach(n => {
|
||
const t = (n.querySelector('text')?.textContent || '').split('⭐')[0].trim();
|
||
const opacity = (t === title) ? '1' : '0.3';
|
||
n.querySelectorAll('*').forEach(el => {
|
||
if (!el.closest('a')) el.style.opacity = opacity;
|
||
});
|
||
});
|
||
|
||
// 重置其它标签
|
||
setTimeout(() => {
|
||
document.querySelectorAll('.node').forEach(n => {
|
||
const t = (n.querySelector('text')?.textContent || '').split('⭐')[0].trim();
|
||
if (t !== title) {
|
||
n.querySelectorAll('*').forEach(el => {
|
||
el.style.opacity = '';
|
||
});
|
||
}
|
||
});
|
||
}, 3000);
|
||
|
||
setStatus('📌 ' + title);
|
||
});
|
||
});
|
||
}
|
||
|
||
function buildProjectList() {
|
||
const container = document.getElementById('projectList');
|
||
const nodes = document.querySelectorAll('.node rect, .node polygon');
|
||
const projects = [];
|
||
const seen = new Set();
|
||
|
||
nodes.forEach(rect => {
|
||
const textEl = rect.parentElement?.querySelector('text');
|
||
const text = textEl?.textContent || '';
|
||
const title = text.split('⭐')[0].trim();
|
||
const starMatch = text.match(/⭐(d+)/);
|
||
const stars = starMatch ? parseInt(starMatch[1]) : 0;
|
||
|
||
// 判断是否在 Projects 子图中
|
||
const cluster = rect.closest('.cluster');
|
||
const isProject = cluster && (cluster.textContent || '').includes('研究项目');
|
||
if (isProject && title && !seen.has(title)) {
|
||
seen.add(title);
|
||
projects.push({ name: title, stars, desc: text.split('·')[1]?.trim() || '' });
|
||
}
|
||
});
|
||
|
||
if (projects.length === 0) {
|
||
container.innerHTML = '<div style="color:#94a3b8;font-size:12px;padding:8px;">未找到项目节点</div>';
|
||
return;
|
||
}
|
||
|
||
projects.sort((a, b) => b.stars - a.stars);
|
||
|
||
container.innerHTML = projects.map(p => `
|
||
<div class="project-item" data-name="${p.name}" onclick="highlightProject('${p.name}')">
|
||
<div class="proj-name">${p.name} ${p.stars > 0 ? '⭐'.repeat(Math.min(p.stars, 5)) : ''}</div>
|
||
${p.desc ? '<div class="proj-desc">' + p.desc + '</div>' : ''}
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
function highlightProject(name) {
|
||
const node = Array.from(document.querySelectorAll('.node')).find(n => {
|
||
return (n.querySelector('text')?.textContent || '').includes(name);
|
||
});
|
||
if (node) {
|
||
node.dispatchEvent(new Event('click'));
|
||
// 滚动到节点位置
|
||
const svg = svgElement;
|
||
if (svg) {
|
||
const ctm = node.getScreenCTM();
|
||
if (ctm) {
|
||
const area = document.getElementById('graphArea');
|
||
const cx = area.clientWidth / 2 - ctm.e;
|
||
const cy = area.clientHeight / 2 - ctm.f;
|
||
panX = cx;
|
||
panY = cy;
|
||
applyTransform();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function setStatus(msg) {
|
||
const bar = document.getElementById('statusBar');
|
||
bar.textContent = msg;
|
||
bar.classList.add('visible');
|
||
clearTimeout(window.statusTimer);
|
||
window.statusTimer = setTimeout(() => bar.classList.remove('visible'), 3000);
|
||
}
|
||
|
||
function downloadSVG() {
|
||
if (!svgElement) return;
|
||
const clone = svgElement.cloneNode(true);
|
||
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
||
const style = document.createElement('style');
|
||
style.textContent = `
|
||
.node rect, .node polygon { fill: #ffffff; stroke: #e2e8f0; }
|
||
.edgePath .path { stroke: #94a3b8; stroke-width: 1.5; }
|
||
.edgeLabel { background: #ffffff; }
|
||
.cluster rect { fill: #f8fafc; stroke: #e2e8f0; }
|
||
`;
|
||
clone.insertBefore(style, clone.firstChild);
|
||
|
||
const serializer = new XMLSerializer();
|
||
const svgStr = serializer.serializeToString(clone);
|
||
const blob = new Blob([svgStr], {type: 'image/svg+xml;charset=utf-8'});
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = 'knowledge-graph-gitlink.svg';
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
setStatus('📥 SVG 已导出');
|
||
}
|
||
</script>
|
||
</body>
|
||
</html> |