gitlink-cli/skills/gitlink-research-hotspot/scripts/_output/knowledge-graph-gitlink.html

643 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>