参考(图谱)
<!-- ==================== 知识图谱入口 ==================== -->
<div class="graph-entry-section">
<div class="graph-entry-card">
<div class="graph-entry-icon">🌌</div>
<div class="graph-entry-text">
<h3>探索知识宇宙</h3>
<p>可视化浏览全站 <strong id="total-nodes-count">-</strong> 篇内容的关联关系</p>
</div>
<div class="graph-entry-arrow">→</div>
</div>
</div>
<!-- ==================== 图谱弹窗 ==================== -->
<div id="graphModal" class="graph-modal">
<!-- 头部工具栏 -->
<div class="graph-header">
<div class="graph-header-left">
<span class="graph-title">🌌 知识关联图谱</span>
<span class="graph-subtitle">以「首页」为中心展开</span>
</div>
<div class="graph-header-right">
<!-- 搜索框 -->
<div class="graph-search-box">
<input type="text" id="graph-search-input" placeholder="搜索节点..." />
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.35-4.35"></path>
</svg>
</div>
<!-- 控制按钮 -->
<button class="graph-ctrl-btn" id="graph-reset-btn" title="重置视图">
🎯
</button>
<button class="graph-ctrl-btn" id="graph-fullscreen-btn" title="全屏">
⛶
</button>
<button class="graph-close-btn" title="关闭">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 6 6 18M6 6l12 12"/>
</svg>
</button>
</div>
</div>
<!-- 图谱容器 -->
<div id="knowledge-graph-container"></div>
<!-- 节点信息面板 -->
<div id="node-info-panel" class="node-info-panel">
<div class="node-info-title" id="node-info-title">-</div>
<div class="node-info-meta">
<span id="node-info-children">0 个子节点</span>
<span id="node-info-depth">深度: 0</span>
</div>
<button class="node-info-btn" id="node-goto-btn">前往该页面 →</button>
</div>
<!-- 底部提示 -->
<div class="graph-tips">
<span>🖱️ 滚轮缩放</span>
<span>✋ 拖拽画布</span>
<span>👆 点击节点查看详情</span>
<span>🔍 双击节点跳转</span>
</div>
<!-- 图例 -->
<div class="graph-legend">
<div class="legend-item"><span class="legend-dot root"></span> 首页(根节点)</div>
<div class="legend-item"><span class="legend-dot category"></span> 分类目录</div>
<div class="legend-item"><span class="legend-dot article"></span> 文章页面</div>
</div>
</div>
<!-- ==================== 样式 ==================== -->
/* 入口卡片 */
.graph-entry-section {
margin: 30px auto;
max-width: 500px;
}
.graph-entry-card {
display: flex;
align-items: center;
gap: 15px;
padding: 20px 25px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3);
}
.graph-entry-card:hover {
transform: translateY(-3px) scale(1.02);
box-shadow: 0 15px 40px rgba(102, 126, 234, 0.4);
}
.graph-entry-icon {
font-size: 2.5em;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
.graph-entry-text {
flex: 1;
color: white;
}
.graph-entry-text h3 {
margin: 0 0 5px 0;
font-size: 1.2em;
}
.graph-entry-text p {
margin: 0;
font-size: 0.9em;
opacity: 0.9;
}
.graph-entry-arrow {
font-size: 1.5em;
color: white;
opacity: 0.7;
transition: transform 0.3s;
}
.graph-entry-card:hover .graph-entry-arrow {
transform: translateX(5px);
opacity: 1;
}
/* 弹窗 */
.graph-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #0a0a1a;
z-index: 100000;
flex-direction: column;
animation: modalFadeIn 0.3s ease;
}
.graph-modal.show {
display: flex;
}
@keyframes modalFadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* 头部 */
.graph-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
background: rgba(255, 255, 255, 0.05);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
}
.graph-header-left {
display: flex;
align-items: baseline;
gap: 12px;
}
.graph-title {
font-size: 1.1em;
font-weight: bold;
color: #fff;
}
.graph-subtitle {
font-size: 0.85em;
color: rgba(255, 255, 255, 0.5);
}
.graph-header-right {
display: flex;
align-items: center;
gap: 10px;
}
/* 搜索框 */
.graph-search-box {
position: relative;
display: flex;
align-items: center;
}
.graph-search-box input {
width: 180px;
padding: 8px 12px 8px 35px;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 20px;
background: rgba(255, 255, 255, 0.1);
color: #fff;
font-size: 13px;
outline: none;
transition: all 0.2s;
}
.graph-search-box input:focus {
border-color: #5986D1;
background: rgba(255, 255, 255, 0.15);
width: 220px;
}
.graph-search-box input::placeholder {
color: rgba(255, 255, 255, 0.4);
}
.graph-search-box svg {
position: absolute;
left: 12px;
color: rgba(255, 255, 255, 0.4);
pointer-events: none;
}
/* 控制按钮 */
.graph-ctrl-btn {
width: 36px;
height: 36px;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
background: rgba(255, 255, 255, 0.1);
color: #fff;
font-size: 16px;
cursor: pointer;
transition: all 0.2s;
}
.graph-ctrl-btn:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.3);
}
.graph-close-btn {
width: 36px;
height: 36px;
border: none;
border-radius: 8px;
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.graph-close-btn:hover {
background: #ef4444;
color: #fff;
}
/* 图谱容器 */
#knowledge-graph-container {
flex: 1;
width: 100%;
overflow: hidden;
background: radial-gradient(ellipse at center, #1a1a2e 0%, #0a0a1a 100%);
}
/* 节点信息面板 */
.node-info-panel {
position: absolute;
bottom: 80px;
right: 20px;
width: 250px;
padding: 15px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 12px;
color: #fff;
opacity: 0;
transform: translateY(10px);
transition: all 0.3s ease;
pointer-events: none;
}
.node-info-panel.show {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.node-info-title {
font-size: 1.1em;
font-weight: bold;
margin-bottom: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.node-info-meta {
display: flex;
gap: 15px;
font-size: 0.85em;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 12px;
}
.node-info-btn {
width: 100%;
padding: 10px;
border: none;
border-radius: 8px;
background: linear-gradient(135deg, #5986D1, #764ba2);
color: #fff;
font-size: 0.9em;
cursor: pointer;
transition: all 0.2s;
}
.node-info-btn:hover {
transform: scale(1.02);
box-shadow: 0 5px 20px rgba(89, 134, 209, 0.4);
}
/* 底部提示 */
.graph-tips {
position: absolute;
bottom: 20px;
left: 20px;
display: flex;
gap: 20px;
font-size: 12px;
color: rgba(255, 255, 255, 0.4);
}
/* 图例 */
.graph-legend {
position: absolute;
top: 80px;
left: 20px;
display: flex;
flex-direction: column;
gap: 8px;
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
}
.legend-dot {
width: 12px;
height: 12px;
border-radius: 50%;
}
.legend-dot.root { background: #ff6b6b; box-shadow: 0 0 10px #ff6b6b; }
.legend-dot.category { background: #4ecdc4; }
.legend-dot.article { background: #a8e6cf; }
/* 移动端适配 */
@media (max-width: 768px) {
.graph-header {
flex-direction: column;
gap: 10px;
padding: 10px 15px;
}
.graph-search-box input {
width: 150px;
}
.graph-tips {
flex-wrap: wrap;
gap: 10px;
font-size: 11px;
}
.node-info-panel {
left: 10px;
right: 10px;
width: auto;
bottom: 60px;
}
.graph-legend {
top: auto;
bottom: 60px;
left: 10px;
flex-direction: row;
gap: 15px;
}
}
<!-- ==================== 数据生成 (服务端) ==================== -->
<%
const graphData = { nodes: [], links: [] };
const nodeDepthMap = new Map(); // 记录节点深度
/**
* 递归生成图谱数据
* @param {Note} currentNote - 当前笔记
* @param {string|null} parentId - 父节点ID
* @param {number} depth - 当前深度
* @param {boolean} isRoot - 是否为根节点
*/
function generateForceGraphData(currentNote, parentId, depth = 0, isRoot = false) {
// 避免重复添加(处理可能的循环引用)
if (nodeDepthMap.has(currentNote.noteId)) {
// 只添加连线,不重复添加节点
if (parentId) {
graphData.links.push({
source: parentId,
target: currentNote.noteId
});
}
return;
}
nodeDepthMap.set(currentNote.noteId, depth);
const childNotes = currentNote.getVisibleChildNotes();
const categoryName = currentNote.getLabelValue("categoryName");
// 判断节点类型
let nodeType = 'article';
if (isRoot) {
nodeType = 'root';
} else if (childNotes.length > 3) {
nodeType = 'category'; // 子节点多的视为分类
}
// 添加节点
graphData.nodes.push({
id: currentNote.noteId,
shareId: currentNote.shareId,
name: currentNote.title,
childCount: childNotes.length,
depth: depth,
type: nodeType,
category: categoryName || ''
});
// 添加连线
if (parentId) {
graphData.links.push({
source: parentId,
target: currentNote.noteId
});
}
// 递归处理子节点
childNotes.forEach(child => {
generateForceGraphData(child, currentNote.noteId, depth + 1, false);
});
}
/**
* 查找首页笔记
* 遍历 subRoot 的子节点,找到 categoryName === "首页" 的笔记
*/
function findHomePage() {
if (!subRoot || !subRoot.note) return null;
const children = subRoot.note.getVisibleChildNotes();
for (const child of children) {
if (child.getLabelValue("categoryName") === "首页") {
return child;
}
}
// 如果没找到,尝试用索引 [5](根据你的模板结构)
return children[5] || children[0] || null;
}
// 从首页开始构建图谱
const homePage = findHomePage();
if (homePage) {
generateForceGraphData(homePage, null, 0, true);
}
// 统计信息
const totalNodes = graphData.nodes.length;
const maxDepth = Math.max(...Array.from(nodeDepthMap.values()), 0);
%>
<!-- ==================== 脚本 ==================== -->
(function() {
'use strict';
// ==================== 数据 ====================
const graphData = <%- JSON.stringify(graphData) %>;
const totalNodes = <%= totalNodes %>;
const maxDepth = <%= maxDepth %>;
// 更新入口卡片的统计
const countEl = document.getElementById('total-nodes-count');
if (countEl) countEl.textContent = totalNodes;
// ==================== 状态 ====================
let Graph = null;
let graphInitialized = false;
let selectedNode = null;
// ==================== DOM ====================
const modal = document.getElementById('graphModal');
const container = document.getElementById('knowledge-graph-container');
const searchInput = document.getElementById('graph-search-input');
const resetBtn = document.getElementById('graph-reset-btn');
const fullscreenBtn = document.getElementById('graph-fullscreen-btn');
const infoPanel = document.getElementById('node-info-panel');
const infoTitle = document.getElementById('node-info-title');
const infoChildren = document.getElementById('node-info-children');
const infoDepth = document.getElementById('node-info-depth');
const gotoBtn = document.getElementById('node-goto-btn');
// ==================== 颜色配置 ====================
const colors = {
root: '#ff6b6b',
category: '#4ecdc4',
article: '#a8e6cf',
link: 'rgba(255, 255, 255, 0.15)',
linkHighlight: 'rgba(89, 134, 209, 0.6)',
text: '#ffffff'
};
// ==================== 公开函数 ====================
window.openGraphModal = function() {
modal.classList.add('show');
document.body.style.overflow = 'hidden';
if (!graphInitialized) {
// 延迟初始化,确保容器已渲染
setTimeout(initGraph, 100);
graphInitialized = true;
}
};
window.closeGraphModal = function() {
modal.classList.remove('show');
document.body.style.overflow = 'auto';
hideInfoPanel();
};
// ==================== 图谱初始化 ====================
function initGraph() {
if (!graphData.nodes || graphData.nodes.length === 0) {
container.innerHTML = '<div style="color:#666;text-align:center;padding:50px;">暂无数据</div>';
return;
}
// 预处理节点
graphData.nodes.forEach(node => {
// 根据类型设置颜色
node.color = colors[node.type] || colors.article;
// 根据子节点数量和深度计算大小
const baseSize = 4;
const childBonus = Math.min(node.childCount * 0.8, 8);
const depthPenalty = node.depth * 0.3;
node.radius = Math.max(baseSize + childBonus - depthPenalty, 3);
// 根节点特殊处理
if (node.type === 'root') {
node.radius = 15;
}
});
// 创建图谱
Graph = ForceGraph()(container)
.graphData(graphData)
.width(container.clientWidth)
.height(container.clientHeight)
.backgroundColor('transparent')
// 节点配置
.nodeColor(d => d.color)
.nodeVal(d => d.radius)
.nodeRelSize(5)
.nodeLabel('') // 禁用默认 tooltip
// 连线配置
.linkWidth(1)
.linkColor(() => colors.link)
.linkDirectionalParticles(2)
.linkDirectionalParticleWidth(1.5)
.linkDirectionalParticleSpeed(0.005)
.linkDirectionalParticleColor(() => 'rgba(255,255,255,0.3)')
// 交互
.onNodeClick(handleNodeClick)
.onNodeHover(handleNodeHover)
.onBackgroundClick(hideInfoPanel)
// 自定义渲染
.nodeCanvasObject((node, ctx, globalScale) => {
// 画圆
ctx.beginPath();
ctx.arc(node.x, node.y, node.radius, 0, 2 * Math.PI, false);
ctx.fillStyle = node.color;
ctx.fill();
// 根节点发光效果
if (node.type === 'root') {
ctx.shadowColor = node.color;
ctx.shadowBlur = 15;
ctx.fill();
ctx.shadowBlur = 0;
}
// 文字(放大时显示,或者根节点/分类节点始终显示)
const showLabel = globalScale > 1.5 || node.type === 'root' ||
(node.type === 'category' && globalScale > 0.8);
if (showLabel) {
const fontSize = Math.max(12 / globalScale, 2);
ctx.font = `${node.type === 'root' ? 'bold ' : ''}${fontSize}px "LXGW WenKai Screen", sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillStyle = colors.text;
// 文字背景
const textWidth = ctx.measureText(node.name).width;
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(
node.x - textWidth / 2 - 2,
node.y + node.radius + 2,
textWidth + 4,
fontSize + 2
);
ctx.fillStyle = colors.text;
ctx.fillText(node.name, node.x, node.y + node.radius + 3);
}
})
// 物理引擎配置
.d3AlphaDecay(0.02)
.d3VelocityDecay(0.3)
.warmupTicks(100)
.cooldownTicks(200);
// 调整力学参数
Graph.d3Force('charge').strength(-80);
Graph.d3Force('link').distance(link => {
// 根据深度调整连线长度
const sourceNode = graphData.nodes.find(n => n.id === link.source.id || n.id === link.source);
return sourceNode && sourceNode.depth < 2 ? 80 : 50;
});
// 初始缩放到合适位置
setTimeout(() => {
Graph.zoomToFit(500, 50);
}, 500);
// 窗口大小变化
window.addEventListener('resize', handleResize);
}
// ==================== 事件处理 ====================
function handleNodeClick(node, event) {
if (!node) return;
selectedNode = node;
// 更新信息面板
infoTitle.textContent = node.name;
infoChildren.textContent = `${node.childCount} 个子节点`;
infoDepth.textContent = `深度: ${node.depth}`;
// 显示面板
infoPanel.classList.add('show');
// 高亮连接的边
highlightConnections(node);
// 居中显示该节点
Graph.centerAt(node.x, node.y, 500);
Graph.zoom(2, 500);
}
function handleNodeHover(node) {
container.style.cursor = node ? 'pointer' : 'grab';
}
function hideInfoPanel() {
infoPanel.classList.remove('show');
selectedNode = null;
// 重置高亮
if (Graph) {
Graph.linkColor(() => colors.link);
}
}
function highlightConnections(node) {
Graph.linkColor(link => {
const sourceId = link.source.id || link.source;
const targetId = link.target.id || link.target;
if (sourceId === node.id || targetId === node.id) {
return colors.linkHighlight;
}
return colors.link;
});
}
function handleResize() {
if (Graph && modal.classList.contains('show')) {
Graph.width(container.clientWidth);
Graph.height(container.clientHeight);
}
}
// ==================== 按钮事件 ====================
// 跳转按钮
gotoBtn?.addEventListener('click', () => {
if (selectedNode && selectedNode.shareId) {
window.location.href = './' + selectedNode.shareId;
} else if (selectedNode && selectedNode.id) {
window.location.href = './' + selectedNode.id;
}
});
// 重置视图
resetBtn?.addEventListener('click', () => {
if (Graph) {
Graph.zoomToFit(500, 50);
hideInfoPanel();
}
});
// 全屏切换
fullscreenBtn?.addEventListener('click', () => {
if (document.fullscreenElement) {
document.exitFullscreen();
fullscreenBtn.textContent = '⛶';
} else {
modal.requestFullscreen();
fullscreenBtn.textContent = '⛶';
}
});
// 搜索
let searchTimeout;
searchInput?.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
const term = e.target.value.trim().toLowerCase();
if (!term || !Graph) return;
// 查找匹配的节点
const match = graphData.nodes.find(n =>
n.name.toLowerCase().includes(term)
);
if (match) {
// 定位到该节点
Graph.centerAt(match.x, match.y, 500);
Graph.zoom(2.5, 500);
// 模拟点击
setTimeout(() => handleNodeClick(match), 600);
}
}, 300);
});
// ESC 关闭
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && modal.classList.contains('show')) {
closeGraphModal();
}
});
// 双击节点跳转
container?.addEventListener('dblclick', () => {
if (selectedNode) {
gotoBtn.click();
}
});
console.log(`🌌 知识图谱已加载: ${totalNodes} 个节点, 最大深度 ${maxDepth}`);
})();
项目分区导航:⬅️ 01-design - “筑迹” | 02-参考(图谱) | ➡️ 03-参考(地图)
💬 评论