从 Neo4j Aura 到 Vue 可视化:非遗知识图谱全流程(附完整项目包 + 零报错复现)
前言
做非遗文化传承项目时,我踩过一个典型坑:在 Neo4j 里建好图谱后,不知道怎么快速做成可交互的 Web 可视化页面。后来用Neo4j Aura(在线图数据库)+ Vue + Tailwind CSS + Vis-Network 搭了一套流程,从图谱构建到导出数据,再到本地可视化,全程零复杂配置,新手也能 10 分钟跑通。
现在把完整流程整理成教程,附免费的可直接下载的项目包 —— 你不仅能复现非遗知识图谱,还能替换成自己的数据集(比如中医药、历史人物图谱),轻松实现 “Neo4j 数据→Web 可视化” 的闭环。
(教程全程从 “用户视角” 出发,跟着步骤点鼠标就能成功运行。不堆砌技术术语,重点说清 “你需要做什么”“怎么做”“遇到问题怎么解决”,哪怕是刚接触 Vue 的新手也能轻松搞定。)
完整项目包下载链接:
https://download.csdn.net/download/2301_79876346/92064569
目录
3. 工具 3:Neo4j Aura 账号(可选,仅换数据时用)
2. src/App.vue(根组件,只做一件事:渲染图谱)
3. src/assets/main.css(Tailwind 入口,3 行核心代码)
4. src/views/KnowledgeGraph.vue(核心中的核心)
五、关键说明:项目里的 CSV 数据怎么来的?(换数据必看)
问题 2:图谱空白,控制台报 “404 Not Found”(CSV 加载失败)
一、先搞懂:你拿到的项目包是什么?
在开始操作前,先简单了解项目包的核心作用 —— 这是一个 “开箱即用” 的知识图谱可视化项目,核心功能是:读取public文件夹里的 CSV 数据(节点 + 关系),用 Vue+Tailwind+Vis-Network 渲染成可交互的图谱(支持拖拽、缩放、节点详情查看)。
项目包里已经包含了:
- 现成的非遗数据(
node-export1.csv节点文件、relationship-export1.csv关系文件); - 所有配置好的代码(布局、样式、交互逻辑全写好);
- 无需额外安装的依赖清单(
package.json已配置)。
二、前置准备:3 个必装工具(5 分钟搞定)
在运行项目前,必须先装 3 个基础工具,少一个都跑不起来,按顺序装就行:
1. 工具 1:Node.js(Vue 项目运行环境)
- 作用:相当于 “Vue 项目的发动机”,没有它无法启动项目;
- 怎么装:
- 打开Node.js 官网,下载 “LTS 长期支持版”(推荐 16.x 或 18.x,别下最新的 “Current 版”,可能有兼容性问题);
- 安装时 务必勾选 “Add to PATH”(自动配置环境变量,否则终端找不到 Node);
- 验证是否装好:打开电脑 “命令提示符”(Win+R 输入
cmd),输入node -v,能显示版本号(如 v18.17.0)就是成功。
2. 工具 2:VS Code(查看项目 + 执行命令)
- 作用:轻量的代码编辑器,用来打开项目文件夹、执行安装 / 启动命令;
- 怎么装:
- 打开VS Code 官网,下载对应系统的版本(Win/Mac 都有);
- 安装时可勾选 “添加到右键菜单”(后续右键文件夹就能用 VS Code 打开,方便)。
3. 工具 3:Neo4j Aura 账号(可选,仅换数据时用)
- 作用:如果你想替换成自己的图谱数据(比如中医药、历史人物),需要用它在线建图谱、导出 CSV;
- 怎么弄:
- 打开Neo4j Aura 官网,用邮箱注册账号;
- 创建 “Free 免费版” 数据库,保存好自动生成的 “用户名(默认 neo4j)” 和 “密码”(丢了会很麻烦)。
三、Vue 项目结构与核心代码
我已经把项目精简到极致,只保留知识图谱相关文件,没有任何冗余代码。先看项目结构,再逐个解析核心文件。
3.1 项目目录(精简版)
下载项目包后,目录长这样(标★的是核心文件,不能删):
非遗知识图谱项目/
├─ public/ # 静态资源文件夹
│ ├─ node-export1.csv # ★ Neo4j导出的节点数据
│ ├─ relationship-export1.csv # ★ Neo4j导出的关系数据
│ └─ index.html # 项目入口HTML(无需修改)
├─ src/
│ ├─ assets/
│ │ └─ main.css # ★ Tailwind CSS入口(已配置好)
│ ├─ views/
│ │ └─ KnowledgeGraph.vue # ★ 知识图谱核心页面(含布局+交互)
│ ├─ App.vue # ★ 根组件(直接渲染图谱)
│ └─ main.js # ★ 项目入口(挂载App+引入样式)
├─ tailwind.config.js # Tailwind配置(已适配项目)
├─ postcss.config.js # Tailwind编译依赖(无需修改)
├─ package.json # 项目依赖清单(已配置好)
└─ README.md # 简单说明(可忽略)
3.2 核心文件代码解析(关键部分标注释)
1. src/main.js(项目入口,1 行关键代码)
作用:创建 Vue 实例,引入 Tailwind CSS(确保样式生效)
import { createApp } from 'vue';
import App from './App.vue';
import './assets/main.css'; // 关键:引入Tailwind样式,没这行布局会乱
// 直接挂载App,无需路由(单页面更简洁)
createApp(App).mount('#app');
2. src/App.vue(根组件,只做一件事:渲染图谱)
作用:不写多余代码,直接引入KnowledgeGraph组件,避免导航栏等冗余元素
<template>
<div id="app">
<!-- 直接渲染知识图谱,无任何多余内容 -->
<KnowledgeGraph />
</div>
</template>
<script>
// 仅引入图谱组件,其他组件全删
import KnowledgeGraph from './views/KnowledgeGraph.vue';
export default {
name: 'App',
components: {
KnowledgeGraph // 注册图谱组件
}
};
</script>
<style>
/* 重置页面边距,避免顶部空白 */
body {
margin: 0;
padding: 0;
}
</style>
3. src/assets/main.css(Tailwind 入口,3 行核心代码)
作用:导入 Tailwind 的基础样式、组件和工具类(页面布局全靠它)
/* Tailwind三要素:基础样式+组件+工具类,缺一不可 */
@tailwind base;
@tailwind components;
@tailwind utilities;
4. src/views/KnowledgeGraph.vue(核心中的核心)
这个文件包含 3 部分:页面布局(Tailwind 实现)、CSV 数据加载、Vis-Network 图谱初始化。关键代码已标注释,重点看这 3 处:
- 布局:用
flex/container/lg:flex-row实现 “图谱 + 侧边栏” 的响应式布局; - 数据加载:
loadCSV函数读取public下的 CSV 文件,路径不用改; - 图谱初始化:
initNetwork配置 Vis-Network,支持拖拽、缩放、节点高亮。
<template>
<!-- 最外层容器:新增专属类heritage-graph-app -->
<div class="heritage-graph-app">
<!-- 顶部装饰纹样 -->
<div class="decorative-pattern top-pattern"></div>
<!-- 主内容区 -->
<main class="container mx-auto p-6">
<!-- 标题区域 -->
<div class="text-center mb-8">
<h1 class="text-3xl md:text-4xl font-serif font-bold text-[#8B5A2B] relative inline-block">
非物质文化遗产知识图谱
<div class="absolute -bottom-2 left-0 right-0 h-px bg-[#D2B48C]"></div>
</h1>
<p class="mt-3 text-[#8B5A2B] font-serif">探索非遗项目的传承与关联</p>
</div>
<!-- 介绍卡片 -->
<div class="bg-[#FFFCF7] rounded-2xl shadow-md p-8 mb-6 border border-[#F5F5DC] mx-auto max-w-4xl px-6 intro-card">
<p class="text-lg font-serif leading-relaxed">
此可视化展示了不同非物质文化遗产遗产项目之间的关系,包括世界遗产、国家级级非遗等类别,
帮助您探索非遗项目之间的关联与传承脉络。
</p>
</div>
<!-- 图谱和侧边栏水平排列 -->
<div class="flex flex-col lg:flex-row px-8 gap-6">
<!-- 知识图谱区域 -->
<div class="lg:w-[1400px] graph-visualization-container" style="margin-bottom: 2rem;">
<div style="padding-bottom: 16px;" class="p-4 border-b border-[#F5F5DC] flex justify-between items-center">
<h2 class="font-serif font-semibold text-lg text-[#8B5A2B]">知识图谱可视化</h2>
<div class="flex space-x-2">
<button @click="resetGraph" style="margin-right: 16px;" class="text-sm px-3 py-1 bg-[#FFF8E1] text-[#8B5A2B] rounded border border-[#F5F5DC] hover:bg-[#F5F5DC] transition-colors">
<i class="fa fa-refresh mr-1"></i>重置视图
</button>
<button @click="togglePhysics" class="text-sm px-3 py-1 bg-[#CD853F] text-white rounded border border-[#8B5A2B] hover:bg-[#8B5A2B] transition-colors">
<i class="fa fa-magic mr-1"></i>{{ physicsEnabled ? '固定布局' : '启用布局' }}
</button>
</div>
</div>
<!-- 图谱容器 -->
<div class="network-wrapper">
<div ref="networkContainer" class="network-container"></div>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner">
<i class="fa fa-circle-o-notch fa-spin mr-2 text-[#CD853F]"></i>
<span>正在生成知识图谱...</span>
</div>
</div>
</div>
<!-- 侧边栏:右侧布局 -->
<div class="lg:w-96 flex flex-col gap-6 sidebar-right">
<!-- 节点详情 -->
<div class="bg-[#FFFCF7] rounded-xl shadow-md border border-[#F5F5DC] p-5">
<h3 class="text-lg font-serif font-semibold mb-4 border-b border-[#F5F5DC]" style="padding-bottom: 12px;">
<i class="fa fa-info-circle text-[#CD853F] mr-2"></i>节点详情
</h3>
<div v-if="selectedNode" class="space-y-3">
<div>
<h4 class="font-medium text-[#8B5A2B] text-lg font-serif">{{ selectedNode.name }}</h4>
<p class="text-sm text-gray-600 mt-1">{{ selectedNode.description || '暂无详细描述' }}</p>
</div>
<div class="pt-2 border-t border-[#F5F5F5]">
<span class="text-xs bg-[#FFF8E1] px-2 py-1 rounded border border-[#F5F5DC]">
类型: {{ formatNodeType(selectedNode.nodeLabel) }}
</span>
</div>
</div>
<div v-else class="text-center text-gray-500 py-6">
<p>点击图谱中的节点查看详情</p>
</div>
</div>
<!-- 图例说明 -->
<div class="bg-[#FFFCF7] rounded-xl shadow-md border border-[#F5F5DC] p-5">
<h3 class="text-lg font-serif font-semibold mb-4 border-b border-[#F5F5DC]" style="padding-bottom: 12px;">
<i class="fa fa-map-signs text-[#CD853F] mr-2"></i>图例说明
</h3>
<div class="space-y-3">
<div class="flex items-center">
<div class="w-5 h-5 rounded-full bg-[#CD853F] border border-[#8B5A2B] mr-3"></div>
<span>核心节点(Heritage)</span>
</div>
<div class="flex items-center">
<div class="w-5 h-5 rounded-full bg-[#F5F5DC] border border-[#D2B48C] mr-3"></div>
<span>其他节点(项目/人物/机构等)</span>
</div>
<div class="flex items-center">
<div class="w-10 h-0.5 bg-[#CD853F] mr-3"></div>
<span>关系连线</span>
</div>
</div>
</div>
<!-- 操作指南 -->
<div class="bg-[#FFFCF7] rounded-xl shadow-md border border-[#F5F5DC] p-5">
<h3 class="text-lg font-serif font-semibold mb-4 border-b border-[#F5F5DC]" style="padding-bottom: 12px;">
<i class="fa fa-hand-pointer-o text-[#CD853F] mr-2"></i>操作指南
</h3>
<ul class="space-y-2 text-sm">
<li class="flex items-start">
<i class="fa fa-arrows text-[#CD853F] mt-1 mr-2"></i>
<span>拖拽节点可重新布局</span>
</li>
<li class="flex items-start">
<i class="fa fa-mouse-pointer text-[#CD853F] mt-1 mr-2"></i>
<span>点击节点可查看详细信息</span>
</li>
<li class="flex items-start">
<i class="fa fa-search-plus text-[#CD853F] mt-1 mr-2"></i>
<span>鼠标滚轮可缩放视图</span>
</li>
<li class="flex items-start">
<i class="fa fa-expand text-[#CD853F] mt-1 mr-2"></i>
<span>双击节点可聚焦相关连接</span>
</li>
<li class="flex items-start">
<i class="fa fa-lightbulb-o text-[#CD853F] mt-1 mr-2"></i>
<span>悬停节点可查看发光效果</span>
</li>
</ul>
</div>
<!-- 统计信息 -->
<div class="bg-[#FFFCF7] rounded-xl shadow-md border border-[#F5F5DC] p-5">
<h3 class="text-lg font-serif font-semibold mb-4 border-b border-[#F5F5DC]" style="padding-bottom: 12px;">
<i class="fa fa-bar-chart text-[#CD853F] mr-2"></i>统计信息
</h3>
<div class="space-y-3">
<div class="flex justify-between">
<span>核心节点数量</span>
<span class="font-semibold">{{ heritageCount }}</span>
</div>
<div class="flex justify-between">
<span>其他节点数量</span>
<span class="font-semibold">{{ otherCount }}</span>
</div>
<div class="flex justify-between">
<span>关系总数</span>
<span class="font-semibold">{{ relCount }}</span>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- 底部装饰纹样 -->
<div class="decorative-pattern bottom-pattern"></div>
<!-- 页脚 -->
<footer class="mt-12 py-6 border-t border-[#F5F5DC] bg-[#FFFCF7]">
<div class="container mx-auto px-6 text-center text-[#8B5A2B]">
<p class="font-serif">非物质文化遗产知识图谱可视化工具 © {{ new Date().getFullYear() }}</p>
</div>
</footer>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
import { Network } from 'vis-network/standalone';
import Papa from 'papaparse';
// 状态管理
const loading = ref(true);
const network = ref(null);
const networkContainer = ref(null);
const physicsEnabled = ref(true);
const selectedNode = ref(null);
const heritageCount = ref(0);
const otherCount = ref(0);
const relCount = ref(0);
// 统一的明亮古风主题配色 - 使用更亮眼的暖棕色系
const THEME_COLORS = {
primary: "#CD853F", // 主色调:秘鲁棕色,温暖亮眼
secondary: "#8B5A2B", // 次要色: saddle棕色,稍深但不暗沉
accent: "#D2B48C", // 强调色:秘鲁棕色浅色调
background: "#FFFCF7", // 极浅米色背景
paper: "#FFFFFF", // 白色纸张效果
border: "#F5F5DC", // 边框色:米色,更柔和
lightBeige: "#FFF8E1" // 浅米色,用于高亮元素
};
// 节点配色:与整体色调统一且更亮眼
const NODE_COLORS = {
"Heritage": { // 核心节点 - 使用主色调,更亮眼
background: THEME_COLORS.border, // #F5F5DC
border: THEME_COLORS.accent, // #D2B48C
font: THEME_COLORS.secondary,
highlight: {
background: THEME_COLORS.secondary,
border: "#6d4c41"
},
hover: {
background: THEME_COLORS.border, // #F5F5DC
border: THEME_COLORS.accent
}
},
"Item": { // 项目节点 - 明亮的米色,与主色调协调
background: THEME_COLORS.border, // #F5F5DC
border: THEME_COLORS.accent, // #D2B48C
font: THEME_COLORS.secondary, // #8B5A2B
highlight: {
background: THEME_COLORS.accent,
border: THEME_COLORS.secondary
},
hover: {
background: THEME_COLORS.border,
border: THEME_COLORS.accent
}
}
};
// 关系样式:与节点颜色统一
const RELATION_STYLE = {
color: {
color: THEME_COLORS.primary, // 关系线条使用主色调,与节点统一
highlight: THEME_COLORS.secondary,
opacity: 0.9 // 稍提高不透明度,更清晰可见
},
width: 1, // 线条稍粗,更明显
font: {
size: 10,
face: "SimSun",
color: THEME_COLORS.secondary,
strokeWidth: 2,
strokeColor: THEME_COLORS.background
},
smooth: { enabled: false },
selectionWidth: 2,
hoverWidth: 2,
arrows: { to: { enabled: true, scaleFactor: 0.7 } }
};
// 修复ResizeObserver循环错误
const handleResizeObserverError = () => {
const originalErrorHandler = window.onerror;
window.onerror = function(message) {
if (message.includes('ResizeObserver loop completed with undelivered notifications')) {
return true; // 标记为已处理
}
return originalErrorHandler ? originalErrorHandler.apply(this, arguments) : false;
};
const originalRejectionHandler = window.onunhandledrejection;
window.onunhandledrejection = function(event) {
if (event.reason && event.reason.message &&
event.reason.message.includes('ResizeObserver loop completed with undelivered notifications')) {
event.preventDefault();
return true;
}
return originalRejectionHandler ? originalRejectionHandler.apply(this, arguments) : false;
};
if (window.ResizeObserver) {
const OriginalResizeObserver = window.ResizeObserver;
window.ResizeObserver = class ResizeObserver extends OriginalResizeObserver {
constructor(callback) {
super((entries, observer) => {
requestAnimationFrame(() => callback(entries, observer));
});
}
};
}
};
// 加载CSV文件
const loadCSV = async (filename) => {
try {
const response = await fetch(filename);
if (!response.ok) throw new Error(`文件加载失败: ${response.statusText}`);
const text = await response.text();
return new Promise((resolve) => {
Papa.parse(text, {
header: true,
skipEmptyLines: true,
complete: (results) => resolve(results.data)
});
});
} catch (error) {
console.error(`加载${filename}错误:`, error);
alert(`无法加载${filename},请确认文件在Vue文件同一层级`);
return [];
}
};
// 格式化节点类型显示
const formatNodeType = (label) => {
const typeMap = {
"Heritage": "核心节点(非遗分类)",
"Item": "项目节点(戏曲/曲艺等)",
"Person": "人物节点(传承人/艺术家)",
"Event": "活动节点(艺术节/文化节)",
"Institution": "机构节点(高校/院校)",
"Organization": "组织节点(剧院/剧团)",
"ArtForm": "艺术形式节点(绘画/工艺等)"
};
return typeMap[label] || `其他节点(${label})`;
};
// 初始化网络图谱
const initNetwork = async () => {
loading.value = true;
await nextTick();
// 确保容器尺寸固定
if (networkContainer.value) {
networkContainer.value.style.width = '100%';
networkContainer.value.style.height = '600px';
}
// 加载节点和关系数据
const nodesData = await loadCSV('node-export1.csv');
const relsData = await loadCSV('relationship-export1.csv');
if (nodesData.length === 0 || relsData.length === 0) {
loading.value = false;
return;
}
// 处理节点数据
const nodes = [];
const nodesMap = {};
nodesData.forEach(row => {
const nodeId = row["~id"];
const nodeName = row["name"] || "未命名节点";
const nodeDescription = row["description"] || "";
const nodeLabel = row["~labels"] || "Item";
// 节点配置:区分核心/非核心
const nodeConfig = NODE_COLORS[nodeLabel] || NODE_COLORS["Item"];
nodes.push({
id: nodeId,
label: nodeName,
name: nodeName,
description: nodeDescription,
nodeLabel: nodeLabel,
color: {
background: nodeConfig.background,
border: nodeConfig.border,
highlight: nodeConfig.highlight,
hover: nodeConfig.hover
},
borderWidth: 2,
font: {
size: nodeLabel === "Heritage" ? 12 : 10,
face: "SimSun",
color: nodeConfig.font,
strokeWidth: 1,
strokeColor: "rgba(0,0,0,0.1)"
},
shape: "circle",
size: nodeLabel === "Heritage" ? 40 : 35,
title: `描述: ${nodeDescription || '暂无描述'}`
});
nodesMap[nodeId] = { label: nodeLabel };
});
// 处理关系数据
const edges = [];
relsData.forEach(row => {
const sourceId = row["~start_node_id"];
const targetId = row["~end_node_id"];
const relType = row["~relationship_type"] || "关联";
// 只添加存在的节点关系
if (nodesMap[sourceId] && nodesMap[targetId]) {
edges.push({
from: sourceId,
to: targetId,
label: relType,
color: RELATION_STYLE.color,
width: RELATION_STYLE.width,
font: RELATION_STYLE.font,
smooth: RELATION_STYLE.smooth,
selectionWidth: RELATION_STYLE.selectionWidth,
hoverWidth: RELATION_STYLE.hoverWidth,
arrows: RELATION_STYLE.arrows
});
}
});
// 更新统计信息
heritageCount.value = nodes.filter(n => n.nodeLabel === "Heritage").length;
otherCount.value = nodes.length - heritageCount.value;
relCount.value = edges.length;
// 网络配置 - 导航按钮颜色设置
const container = networkContainer.value;
const data = { nodes, edges };
const options = {
physics: {
enabled: physicsEnabled.value,
stabilization: {
iterations: 500,
updateInterval: 100,
onlyDynamicEdges: true,
fit: true
},
barnesHut: {
gravitationalConstant: -8000,
centralGravity: 0.3,
springLength: 150,
springConstant: 0.1,
damping: 0.9,
avoidOverlap: 0.8
},
maxVelocity: 30,
solver: "barnesHut",
timestep: 0.5
},
interaction: {
hover: true,
hoverConnectedEdges: true,
selectConnectedEdges: true,
navigationButtons: true,
keyboard: true,
tooltipDelay: 200
},
nodes: {
scaling: {
min: 35,
max: 40,
label: {
enabled: true,
min: 10,
max: 14,
maxVisible: 30,
drawThreshold: 5
}
},
shapeProperties: { interpolation: false },
shadow: {
enabled: true,
color: `rgba(${hexToRgb(THEME_COLORS.primary).r}, ${hexToRgb(THEME_COLORS.primary).g}, ${hexToRgb(THEME_COLORS.primary).b}, 0.2)`,
size: 10,
x: 0,
y: 0
}
},
configure: {
enabled: false
},
edges: {
smooth: { enabled: false },
scaling: {
min: 0.5,
max: 2,
label: {
enabled: true,
min: 10,
max: 12,
maxVisible: 20,
drawThreshold: 1
}
},
font: {
align: "middle",
strokeWidth: 2,
strokeColor: THEME_COLORS.background
},
color: { inherit: false },
selectionWidth: 2,
arrowStrikethrough: false
},
layout: {
improvedLayout: true,
hierarchical: { enabled: false }
},
// 导航按钮样式设置 - 与主色调统一
manipulation: {
enabled: false
},
navigation: {
enabled: true,
buttonColor: THEME_COLORS.background, // 按钮背景色
iconColor: THEME_COLORS.primary, // 按钮图标色(放大缩小等)
hoverButtonColor: THEME_COLORS.lightBeige // 悬停时按钮背景色
}
};
// 销毁旧实例避免内存泄漏
if (network.value) {
network.value.destroy();
}
// 创建网络实例
requestAnimationFrame(() => {
// 安全检查:确保容器存在
if (!container) {
loading.value = false;
return;
}
network.value = new Network(container, data, options);
// 节点点击事件:显示详情
network.value.on('click', (params) => {
if (params.nodes.length > 0) {
const nodeId = params.nodes[0];
selectedNode.value = nodes.find(n => n.id === nodeId) || null;
}
});
// 布局稳定后自动禁用物理引擎
network.value.once('stabilizationIterationsDone', () => {
// 添加安全检查,确保network实例仍然存在
if (network.value && physicsEnabled.value) {
setTimeout(() => {
// 再次检查network实例是否存在
if (network.value) {
network.value.setOptions({ physics: { enabled: false } });
physicsEnabled.value = false;
}
}, 1000);
}
});
loading.value = false;
});
};
// 辅助函数:十六进制颜色转RGB
function hexToRgb(hex) {
// 移除#号
hex = hex.replace(/^#/, '');
// 解析RGB值
const bigint = parseInt(hex, 16);
const r = (bigint >> 16) & 255;
const g = (bigint >> 8) & 255;
const b = bigint & 255;
return { r, g, b };
}
// 重置图谱视图
const resetGraph = () => {
// 添加安全检查
if (!network.value) return;
network.value.fit();
selectedNode.value = null;
// 临时启用物理引擎重新布局
network.value.setOptions({
physics: {
enabled: true,
stabilization: { iterations: 500 }
}
});
physicsEnabled.value = true;
// 布局完成后禁用
setTimeout(() => {
if (network.value) { // 安全检查
network.value.setOptions({ physics: { enabled: false } });
physicsEnabled.value = false;
}
}, 1500);
};
// 切换物理引擎状态
const togglePhysics = () => {
// 添加安全检查
if (!network.value) return;
physicsEnabled.value = !physicsEnabled.value;
network.value.setOptions({
physics: {
enabled: physicsEnabled.value,
stabilization: { iterations: physicsEnabled.value ? 500 : 0 }
}
});
};
// 组件挂载初始化
onMounted(() => {
// 提前设置容器基础尺寸
if (networkContainer.value) {
networkContainer.value.style.width = '100%';
networkContainer.value.style.height = '600px';
}
// 错误处理初始化
handleResizeObserverError();
nextTick(() => {
initNetwork();
});
});
// 组件卸载清理
onUnmounted(() => {
if (network.value) {
network.value.destroy();
network.value = null;
}
// 恢复原生错误处理
window.onerror = null;
window.onunhandledrejection = null;
});
</script>
<style scoped>
/* 全局样式重置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
main {
flex-grow: 1;
}
/* 页面整体样式 - 明亮浅色古风基调 */
.heritage-graph-app {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: #FFFCF7; /* 极浅米色背景,类似宣纸但更亮 */
color: #8B5A2B; /* 使用主题次要色 */
background-image:
linear-gradient(rgba(210, 180, 140, 0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(210, 180, 140, 0.1) 1px, transparent 1px);
background-size: 20px 20px; /* 更淡的宣纸纹理效果 */
}
/* 装饰性纹样 - 与主色调统一 */
.decorative-pattern {
height: 30px;
background-image: url("data:image/svg+xml,%3Csvg width='100' height='20' viewBox='0 0 100 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M21.184 20c.357-.13.72-.264.888-.14 1.24.916 1.8 2.376 1.8 3.82 0 3.87-3.42 7-7.628 7C13.282 27 10 23.87 10 20c0-1.444.56-2.904 1.8-3.82.168-.124.53-.015.888.14C16.145 15.248 18.135 15 20 15c1.864 0 3.854.248 5.184 5zM82.5 10c.69 0 1.37-.055 2-.16v1.16c0 4.418-3.582 8-8 8-4.418 0-8-3.582-8-8s3.582-8 8-8zm0-2c-5.523 0-10 4.477-10 10s4.477 10 10 10 10-4.477 10-10-4.477-10-10-10z' fill='%23cd853f' fill-opacity='0.2' fill-rule='evenodd'/%3E%3C/svg%3E");
opacity: 0.8;
}
.top-pattern {
margin-bottom: 20px;
}
.bottom-pattern {
margin-top: 20px;
}
/* 介绍卡片样式 - 明亮浅色 */
.intro-card {
border-radius: 16px !important;
margin-left: 2rem !important;
margin-right: 2rem !important;
padding-top: 1.5rem !important;
padding-bottom: 1.5rem !important;
box-shadow: 0 4px 6px rgba(205, 133, 63, 0.1) !important;
background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%238b5a2b' fill-opacity='0.05' fill-rule='evenodd'/%3E%3C/svg%3E");
}
.bg-\[\#FFFCF7\] {
margin-bottom: 2rem;
margin-top: 2rem;
}
/* 图谱区域样式 - 更明亮 */
.graph-visualization-container {
background-color: #FFFCF7;
border: 1px solid #F5F5DC;
box-shadow: 0 4px 12px rgba(205, 133, 63, 0.1) !important;
overflow: hidden;
max-width: 1400px;
margin-left: 2rem;
margin-right: 2rem;
padding: 1rem;
border-radius: 8px;
}
.graph-visualization-container button {
border-radius: 4px !important;
padding: 6px 12px !important;
font-size: 14px !important;
transition: all 0.3s ease;
}
/* 侧边栏样式 - 明亮浅色 */
.sidebar-right {
margin-right: 2rem;
}
.sidebar-right > div {
padding: 1.25rem !important;
border-radius: 8px;
background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%238b5a2b' fill-opacity='0.05' fill-rule='evenodd'/%3E%3C/svg%3E");
}
/* 自定义滚动条 - 与主题色统一 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f9f9f9;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: #D2B48C;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #CD853F;
}
/* 图谱容器样式 - 更明亮的背景 */
.network-wrapper {
position: relative;
width: 100%;
height: 600px;
overflow: hidden;
border: 1px solid #F5F5DC;
border-radius: 4px;
background-color: rgba(255, 252, 247, 0.95);
}
.network-container {
width: 100%;
height: 100%;
position: relative;
min-width: 600px;
min-height: 600px;
}
/* 加载状态样式 - 与主题色统一 */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 252, 247, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 10;
}
.loading-spinner {
background-color: #FFFCF7;
padding: 12px 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
display: flex;
align-items: center;
border: 1px solid #F5F5DC;
}
/* 悬停提示样式 - 与主题色统一 */
.vis-tooltip {
border-radius: 8px !important;
padding: 10px !important;
box-shadow: 0 4px 8px rgba(158, 201, 209, 0.05) !important;
background-color: #FFFCF7 !important;
border: 1px solid #F5F5DC !important;
color: #44bac0 !important;
font-size: 12px !important;
font-family: "SimSun", serif !important;
}
/* 节点交互样式 - 更亮眼的效果 */
.vis-network:focus {
outline: none !important;
}
.vis-active {
box-shadow: 0 0 15px rgba(205, 133, 63, 0.5) !important;
}
.vis-node:hover {
filter: brightness(1.15) !important;
}
/* 导航按钮样式 - 与主题色统一 */
.vis-navigation {
border-color: #F5F5DC !important;
}
.vis-button {
background-color: #FFFCF7 !important;
border-color: #F5F5DC !important;
color: #CD853F !important;
}
.vis-button:hover {
background-color: #FFF8E1 !important;
color: #8B5A2B !important;
}
footer {
margin-bottom: 2rem;
}
footer .container {
padding-top: 2rem;
}
/* 响应式调整 */
@media (min-width: 1024px) {
.flex-col.lg\:flex-row {
flex-direction: row;
}
}
/* 标题样式 */
h1, h2, h3, h4 {
font-family: "STZhongsong", "SimSun", serif;
}
/* 中文字体优化 */
body {
font-family: "SimSun", "STSong", serif;
}
</style>
四、核心步骤:拿到项目包后,4 步跑通(零代码)
这部分是重点!跟着做,10 分钟内必能运行成功。
步骤 1:解压项目包,用 VS Code 打开
- 下载我提供的完整项目包,右键 “解压到当前文件夹”(记住解压后的路径,比如
D:\非遗知识图谱项目); - 打开 VS Code,点击顶部菜单栏「文件」→「打开文件夹」,选择解压后的项目文件夹(比如选 “非遗知识图谱项目”);
- 此时 VS Code 左侧会显示项目目录,重点确认 2 个文件是否存在(这是图谱的数据来源):
public文件夹下的node-export1.csv(节点数据);public文件夹下的relationship-export1.csv(关系数据)。
步骤 2:安装项目依赖(仅需执行 1 条命令)
- 在 VS Code 里打开 “终端”:点击顶部菜单栏「终端」→「新建终端」,底部会出现黑色命令框;
- 在终端里输入命令:
npm install(复制粘贴就行,不用手敲); - 等待安装:过程需要 2-3 分钟(取决于网速),看到终端显示 “added XXX packages”(XXX 是数字),没有红色报错,就是安装成功;
- ❗ 如果报错:比如 “npm 不是内部命令”,是 Node.js 没装好或没勾选 “Add to PATH”,重新装 Node.js 并重启 VS Code;
- ❗ 如果卡住:按
Ctrl+C终止,重新执行npm install。
步骤 3:启动项目(仅需执行 1 条命令)
- 在终端里继续输入命令:
npm run serve(复制粘贴); - 等待启动:看到终端显示类似以下内容,就是启动成功:
plaintext
App running at: - Local: http://localhost:8080/ - Network: http://192.168.1.100:8080/ - 访问图谱:在地址栏输入上述链接(二选一),按回车。
步骤 4:验证是否成功(看 3 个标志)
启动后如果看到以下 3 点,说明运行成功:
- 页面直接显示 “非物质文化遗产知识图谱”,没有多余的导航栏、按钮;
- 左侧有圆形节点和连线(图谱主体),右侧有 “节点详情”“图例说明” 等侧边栏;
- 交互正常:
- 鼠标拖拽节点能移动;
- 点击节点,右侧 “节点详情” 会显示节点名称和描述;
- 鼠标滚轮能缩放图谱;
- 点击 “重置视图”“固定布局” 按钮,功能能正常触发。
项目实际运行效果:

五、关键说明:项目里的 CSV 数据怎么来的?(换数据必看)
很多小伙伴会问:“我想换成自己的图谱数据,该怎么做?” 先搞懂现有数据的来源,再换数据就简单了。
现有 CSV 数据的来源(不用你操作,仅了解)
项目包里的node-export1.csv和relationship-export1.csv,是我从 Neo4j Aura 导出的,步骤如下:
- 在 Neo4j Aura 的「Neo4j Query」里,用Cypher 语句创建非遗相关的节点(如 “黄梅戏”“邢绣娘”)和关系(如 “属于”“借鉴”);
- 创建完成后,在 Neo4j Explore里可以查看图谱效果,并且找到Export按钮导出 CSV文件;
- 把导出的 CSV 文件改名(保持
node-export1.csv和relationship-export1.csv),放到项目的public文件夹里。
替换成自己的数据(3 步操作,不用改 Vue 代码)
如果你想做自己的图谱(比如 “中医药知识图谱”“历史人物图谱”),按以下步骤来,Vue 代码不用动:
第一步:用 Neo4j Aura 建你的图谱
- 登录 Neo4j Aura,打开「Neo4j Browser」;
- 用Cypher 语句创建你的节点和关系(比如 “中药 - 功效 - 病症” 节点,具体语法可查 Neo4j 官方文档,很简单);
- 确认创建成功:左侧「Database Information」能看到节点数和关系数,没有报错。
第二步:导出你的 CSV 文件
- 创建完成后,在 Neo4j Explore里可以查看图谱效果,并且找到Export按钮导出 CSV文件;
- 注意:文件名必须和项目代码里的一致(
node-export1.csv和relationship-export1.csv),否则 Vue 读不到数据。
第三步:替换项目里的 CSV 并重启
- 找到你导出的 2 个 CSV 文件,复制粘贴到项目的
public文件夹里,选择 “替换”(覆盖原来的非遗数据); - 重启 Vue 项目:在 VS Code 终端按
Ctrl+C终止当前运行,再执行npm run serve; - 打开链接,就能看到你的图谱了!
六、常见问题排查(新手必看,少踩坑)
运行中遇到问题别慌,90% 的问题都是以下几种,按步骤解决就行:
问题 1:页面样式错乱(比如文字挤在一起、没有布局)
- 原因:Tailwind CSS 样式没生效(项目的布局全靠它);
- 解决:
- 检查
src/main.js文件:是否有import './assets/main.css'这行代码(没的话加上,再重启); - 检查
src/assets/main.css文件:是否有 3 行 Tailwind 代码(@tailwind base; @tailwind components; @tailwind utilities;),没的话加上,再重启。
- 检查
问题 2:图谱空白,控制台报 “404 Not Found”(CSV 加载失败)
- 原因:CSV 文件没放在
public文件夹里,或文件名错了; - 解决:
- 确认
public文件夹下有node-export1.csv和relationship-export1.csv; - 确认文件名和代码里的一致(不能多空格、不能改后缀,比如
node-export1.csv不能写成node-export.csv); - 重启项目,刷新浏览器。
- 确认
问题 3:图谱节点重叠在一起,看不清
- 原因:物理引擎的 “避免重叠” 参数设得太小;
- 解决:
- 打开
src/views/KnowledgeGraph.vue文件; - 找到
initNetwork函数里的options.physics.barnesHut.avoidOverlap,把值从0.8改成1.0; - 重启项目,节点就会自动分开。
- 打开
七、总结
这套项目的核心优势就是 “零门槛”:
- 不用装复杂的本地数据库(Neo4j Aura 在线用);
- 不用写 Vue 代码(项目包全配置好);
- 换数据只需改 Neo4j 和 CSV,不用动前端逻辑。
如果在操作中遇到其他问题,欢迎在评论区留言,我会把常见问题补充到教程里,让更多人少走弯路~ 最后,希望大家都能轻松做出自己的知识图谱可视化页面!
更多推荐


所有评论(0)