Przeglądaj źródła

feat:多维 度知识图谱数据挖掘分析系统功能提交,包含节点过滤,溯源,树形结构网状结构切换 等,出题模式出题,导出word等,上传pptx和video播放预览等

韩洋 1 miesiąc temu
commit
a634a779f3
45 zmienionych plików z 10067 dodań i 0 usunięć
  1. 24 0
      .gitignore
  2. 3 0
      .vscode/extensions.json
  3. 5 0
      README.md
  4. BIN
      dist.zip
  5. 253 0
      docs/功能和操作描述.docx
  6. 358 0
      docs/功能和操作描述.md
  7. BIN
      docs/多维度知识图谱数据挖掘分析系统功能和操作描述.docx
  8. BIN
      docs/知识图谱分析系统功能和操作描述.docx
  9. 14 0
      index.html
  10. 2301 0
      package-lock.json
  11. 28 0
      package.json
  12. 7 0
      public/config.js
  13. 0 0
      public/favicon.svg
  14. 24 0
      public/icons.svg
  15. 13 0
      src/App.vue
  16. 1 0
      src/assets/fit.svg
  17. BIN
      src/assets/hero.png
  18. 0 0
      src/assets/ppt.svg
  19. 0 0
      src/assets/video.svg
  20. 0 0
      src/assets/vite.svg
  21. 1 0
      src/assets/vue.svg
  22. 22 0
      src/base/store.js
  23. 0 0
      src/components/graph-view/WordSource.vue
  24. 100 0
      src/layout/APPLayout.vue
  25. 32 0
      src/main.js
  26. 54 0
      src/mock/graph.js
  27. 106 0
      src/mock/mockData.js
  28. 62 0
      src/plugins/neo4j.js
  29. 60 0
      src/reset.css
  30. 20 0
      src/router/index.js
  31. 302 0
      src/style.css
  32. 276 0
      src/utils/fileStorage.js
  33. 39 0
      src/utils/util.js
  34. 2 0
      src/views/graph-view/1.js
  35. 1587 0
      src/views/graph-view/ComposeConfig.vue
  36. 1989 0
      src/views/graph-view/GraphView.vue
  37. 143 0
      src/views/graph-view/HeaderView.vue
  38. 183 0
      src/views/graph-view/LableSelected.vue
  39. 394 0
      src/views/graph-view/NodeCountStatisticize.vue
  40. 604 0
      src/views/graph-view/PPTView.vue
  41. 135 0
      src/views/graph-view/VideoView.vue
  42. 167 0
      src/views/graph-view/WeightSelected.vue
  43. 625 0
      src/views/graph-view/graph.js
  44. 117 0
      src/views/graph-view/mock.js
  45. 16 0
      vite.config.js

+ 24 - 0
.gitignore

@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 3 - 0
.vscode/extensions.json

@@ -0,0 +1,3 @@
+{
+  "recommendations": ["Vue.volar"]
+}

+ 5 - 0
README.md

@@ -0,0 +1,5 @@
+# Vue 3 + Vite
+
+This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
+
+Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).

BIN
dist.zip


+ 253 - 0
docs/功能和操作描述.docx

@@ -0,0 +1,253 @@
+知识图谱分析系统 - 功能和操作描述
+文档版本:v1.0
+生成日期:2026年6月
+
+一、系统概述
+1.1 系统简介
+知识图谱分析系统是一款基于 Vue3 + AntV G6 构建的知识图谱可视化分析平台,旨在帮助用户直观地浏览、分析和探索知识图谱数据。系统通过图形化方式展示节点与关系,支持多种交互操作,提供强大的数据探索能力。
+
+1.2 技术架构
+层级	技术栈	说明
+前端框架	Vue 3 + Composition API	渐进式 JavaScript 框架
+图可视化	AntV G6	专业图可视化引擎
+UI 组件	Arco Design Vue	企业级组件库
+数据库	Neo4j	图数据库
+
+1.3 核心功能模块
+模块	功能描述
+图可视化	节点与边的图形化渲染
+节点筛选	按标签类型筛选节点
+节点统计	节点类型统计与高亮
+搜索功能	节点名称模糊搜索
+溯源详情	查看节点关联原文数据
+右键菜单	节点展开/收起、查看详情
+
+二、界面布局
+2.1 整体布局
+系统采用三栏布局结构:
+┌─────────────────────────────────────────────────────────────────┐
+│                        HeaderView (顶部导航)                    │
+├────────────┬──────────────────────────────────────┬─────────────┤
+│            │                                      │             │
+│ Label      │          GraphView                   │ NodeCount   │
+│ Selected   │          (主图可视化区域)             │ Statisticize│
+│ (左侧筛选) │                                      │ (右侧统计)   │
+│            │                                      │             │
+│            │                                      │             │
+└────────────┴──────────────────────────────────────┴─────────────┘
+
+2.2 组件说明
+组件	文件路径	功能定位
+HeaderView	src/views/graph-view/HeaderView.vue	顶部导航栏
+GraphView	src/views/graph-view/GraphView.vue	主图可视化区域
+LabelSelected	src/views/graph-view/LableSelected.vue	左侧标签筛选面板
+NodeCountStatisticize	src/views/graph-view/NodeCountStatisticize.vue	右侧节点统计面板
+
+三、功能模块详解
+3.1 顶部导航栏 (HeaderView)
+3.1.1 功能说明
+- Logo 展示:显示系统 logo 和标题"知识图谱分析系统"
+- 刷新按钮:点击可刷新整个图谱数据
+
+3.1.2 操作说明
+1. 点击右上角刷新图标,系统将重新加载图谱数据
+
+3.1.3 界面元素
+元素	样式特征	交互方式
+Logo 图标	紫色渐变背景,网络图标	静态展示
+标题	白色文字,18px,字间距 1.5px	静态展示
+刷新按钮	白色图标,悬停高亮	点击刷新
+
+3.2 主图可视化区域 (GraphView)
+3.2.1 功能说明
+(1)图渲染功能
+- 节点展示:圆形节点,根据类型显示不同颜色
+- 边展示:带箭头的连接线,展示节点间关系
+- 布局算法:采用 d3-force 力导向布局,自动计算节点位置
+
+(2)节点类型与颜色映射
+节点类型	标签名称	颜色
+Book	书籍	#E3A3B8 (粉色)
+Part	部分	#F9C351 (黄色)
+Chapter	章节	#F58B55 (橙色)
+EntityType	实体类型	#F15D5D (红色)
+Entity	实体	#CD96CD (紫色)
+
+(3)工具栏功能
+工具栏位于右上角,提供以下操作:
+按钮	功能	快捷键
+🔍 放大	放大视图 1.2 倍	-
+🔍 缩小	缩小视图 0.8 倍	-
+📷 导出	导出当前图谱为 PNG 图片	-
+🔄 重置	重置视图到初始状态	-
+⬜ 全屏	切换全屏模式	F11
+
+(4)小地图 (Minimap)
+- 位于右下角
+- 显示图谱缩略图
+- 红色框表示当前视口位置
+- 支持点击快速定位
+
+3.2.2 操作说明
+(1)基础操作
+操作	方式	说明
+平移画布	拖拽空白区域	移动整个图谱视图
+缩放视图	鼠标滚轮	放大/缩小当前视图
+选中节点	左键点击节点	节点边框变粗表示选中
+拖拽节点	拖拽节点	移动单个节点位置
+
+(2)右键菜单操作
+节点右键菜单:
+1.展开:点击已选中节点的右键菜单"展开",加载该节点的子节点和关联边
+2.收起:点击已展开节点的右键菜单"收起",隐藏该节点的所有子节点
+3.查看原文数据:打开抽屉展示该节点关联的原文数据
+
+边右键菜单:
+1.查看原文数据:打开抽屉展示该关系关联的原文数据
+
+(3)搜索功能
+1.在顶部搜索框输入节点名称
+2.按 Enter 键或点击搜索按钮执行搜索
+3.系统将高亮显示匹配的节点及其关联关系
+4.点击搜索框清除按钮恢复初始视图
+
+3.2.3 界面元素
+元素	位置	样式特征
+搜索框	左上角	白色背景,圆角 6px,带阴影
+画布	中央区域	白色背景,带网格线
+工具栏	右上角	白色背景,圆角 4px
+小地图	右下角	白色背景,带边框
+
+3.3 标签筛选面板 (LabelSelected)
+3.3.1 功能说明
+- 提供节点类型筛选功能
+- 支持展开/收起面板
+- 点击标签按钮筛选对应类型节点
+
+3.3.2 筛选标签
+标签名称	对应类型	颜色
+书籍	Book	#E3A3B8
+部分	Part	#F9C351
+章节	Chapter	#F58B55
+
+3.3.3 操作说明
+1.点击标签按钮筛选该类型的节点
+2.点击面板左侧箭头展开/收起面板
+
+3.3.4 界面元素
+元素	样式特征
+面板标题	紫色渐变背景,白色文字
+标签按钮	圆角 20px,带颜色圆点
+展开/收起箭头	灰色图标,悬停变蓝
+
+3.4 节点统计面板 (NodeCountStatisticize)
+3.4.1 功能说明
+- 统计当前图谱中各类型节点数量
+- 显示各类型节点占比
+- 支持点击统计项高亮对应节点
+- 支持取消高亮和刷新统计
+
+3.4.2 操作说明
+(1)高亮节点
+1.查看统计面板中的节点类型分布
+2.点击任意统计项,该类型节点高亮显示(发光效果),其他节点变暗
+
+(2)取消高亮
+1.点击面板底部"取消高亮"按钮
+2.所有节点恢复正常显示
+
+(3)刷新统计
+1.点击面板底部"刷新"按钮
+2.重新计算节点统计数据
+
+3.4.3 界面元素
+元素	样式特征
+面板标题	紫色渐变背景,白色文字
+总数标签	白色背景,圆角 20px
+统计项	进度条展示占比,悬停高亮
+操作按钮	圆角按钮,带图标
+
+四、数据溯源功能
+4.1 功能说明
+当用户需要查看节点或边的原始数据来源时,可通过右键菜单打开溯源详情抽屉,展示关联的原文数据片段。
+
+4.2 溯源卡片结构
+区域	内容	样式
+卡片头部	切片ID标签	蓝色背景,白色文字
+卡片主体	章节引用	左侧蓝色边框,灰色引用图标
+完整上下文	原文片段	灰色背景,圆角 6px
+
+4.3 操作说明
+1.右键点击节点或边
+2.选择"查看原文数据"
+3.在右侧抽屉中查看关联的原文片段
+4.点击抽屉关闭按钮或点击遮罩层关闭抽屉
+
+五、快捷键与快捷操作
+5.1 画布操作
+操作	快捷键/方式
+平移画布	拖拽空白区域
+缩放视图	鼠标滚轮
+选中节点	左键点击
+拖拽节点	拖拽节点
+右键菜单	右键点击节点/边
+
+5.2 工具栏操作
+操作	按钮位置
+放大	工具栏第一个按钮
+缩小	工具栏第二个按钮
+导出图片	工具栏第三个按钮
+重置视图	工具栏第四个按钮
+全屏模式	工具栏第五个按钮
+
+5.3 搜索操作
+操作	方式
+执行搜索	Enter 键 / 点击搜索按钮
+清除搜索	点击清除按钮
+
+六、系统状态与提示
+6.1 消息提示
+系统使用 Arco Design 的 Message 组件展示操作反馈:
+场景	提示内容	类型
+搜索成功	"成功渲染 X 个节点,并建立 Y 条关联线!"	success
+搜索无结果	"未找到相关的节点"	warning
+取消高亮	"已取消高亮"	info
+刷新统计	"统计数据已刷新"	info
+查询失败	"节点级联查询失败"	error
+
+6.2 节点状态
+状态	视觉表现	触发方式
+选中	边框加粗(3px)	左键点击
+高亮	发光效果(shadowBlur 20)	统计面板点击
+变暗	透明度降低(0.3)	其他类型高亮时
+默认	正常显示	初始状态
+
+七、数据加载流程
+7.1 初始化流程
+页面加载 → 查找根节点 → 渲染初始图谱 → 布局完成 → 自动放大居中
+
+7.2 节点展开流程
+右键点击节点 → 选择"展开" → 查询子节点数据 → 更新图谱数据 → 重新布局渲染
+
+7.3 节点收起流程
+右键点击节点 → 选择"收起" → 递归删除子节点 → 更新图谱数据 → 重新渲染
+
+7.4 搜索流程
+输入关键词 → 执行搜索 → 获取匹配节点 → 更新图谱数据 → 布局居中 → 显示结果提示
+
+八、注意事项
+8.1 性能优化建议
+1.节点数量控制:建议单次展示节点数不超过 500 个,避免性能下降
+2.布局等待:大规模图谱布局时请耐心等待,布局完成后会自动居中
+3.导出图片:导出前建议调整视图到合适位置,确保所有关键节点可见
+
+8.2 操作提示
+1.节点拖拽:拖拽节点时会自动清除其他节点的选中状态
+2.右键菜单:点击画布空白区域可关闭右键菜单
+3.全屏模式:全屏状态下按 Esc 键或点击工具栏按钮退出
+
+九、版本信息
+项目	版本
+Vue	3.x
+AntV G6	最新稳定版
+Arco Design Vue	最新稳定版

+ 358 - 0
docs/功能和操作描述.md

@@ -0,0 +1,358 @@
+
+# 知识图谱分析系统 - 功能和操作描述
+
+---
+
+## 一、系统概述
+
+### 1.1 系统简介
+
+**知识图谱分析系统**是一款基于 Vue3 + AntV G6 构建的知识图谱可视化分析平台,旨在帮助用户直观地浏览、分析和探索知识图谱数据。系统通过图形化方式展示节点与关系,支持多种交互操作,提供强大的数据探索能力。
+
+### 1.2 技术架构
+
+| 层级 | 技术栈 | 说明 |
+| :--- | :--- | :--- |
+| 前端框架 | Vue 3 + Composition API | 渐进式 JavaScript 框架 |
+| 图可视化 | AntV G6 | 专业图可视化引擎 |
+| UI 组件 | Arco Design Vue | 企业级组件库 |
+| 数据库 | Neo4j | 图数据库 |
+
+### 1.3 核心功能模块
+
+| 模块 | 功能描述 |
+| :--- | :--- |
+| 图可视化 | 节点与边的图形化渲染 |
+| 节点筛选 | 按标签类型筛选节点 |
+| 节点统计 | 节点类型统计与高亮 |
+| 搜索功能 | 节点名称模糊搜索 |
+| 溯源详情 | 查看节点关联原文数据 |
+| 右键菜单 | 节点展开/收起、查看详情 |
+
+---
+
+## 二、界面布局
+
+### 2.1 整体布局
+
+系统采用三栏布局结构:
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│                        HeaderView (顶部导航)                    │
+├────────────┬──────────────────────────────────────┬─────────────┤
+│            │                                      │             │
+│ Label      │          GraphView                   │ NodeCount   │
+│ Selected   │          (主图可视化区域)             │ Statisticize│
+│ (左侧筛选) │                                      │ (右侧统计)   │
+│            │                                      │             │
+│            │                                      │             │
+└────────────┴──────────────────────────────────────┴─────────────┘
+```
+
+### 2.2 组件说明
+
+| 组件 | 文件路径 | 功能定位 |
+| :--- | :--- | :--- |
+| HeaderView | `src/views/graph-view/HeaderView.vue` | 顶部导航栏 |
+| GraphView | `src/views/graph-view/GraphView.vue` | 主图可视化区域 |
+| LabelSelected | `src/views/graph-view/LableSelected.vue` | 左侧标签筛选面板 |
+| NodeCountStatisticize | `src/views/graph-view/NodeCountStatisticize.vue` | 右侧节点统计面板 |
+
+---
+
+## 三、功能模块详解
+
+### 3.1 顶部导航栏 (HeaderView)
+
+#### 功能说明
+- **Logo 展示**:显示系统 logo 和标题"知识图谱分析系统"
+- **刷新按钮**:点击可刷新整个图谱数据
+
+#### 操作说明
+1. 点击右上角刷新图标,系统将重新加载图谱数据
+
+#### 界面元素
+| 元素 | 样式特征 | 交互方式 |
+| :--- | :--- | :--- |
+| Logo 图标 | 紫色渐变背景,网络图标 | 静态展示 |
+| 标题 | 白色文字,18px,字间距 1.5px | 静态展示 |
+| 刷新按钮 | 白色图标,悬停高亮 | 点击刷新 |
+
+---
+
+### 3.2 主图可视化区域 (GraphView)
+
+#### 功能说明
+
+##### 3.2.1 图渲染功能
+- **节点展示**:圆形节点,根据类型显示不同颜色
+- **边展示**:带箭头的连接线,展示节点间关系
+- **布局算法**:采用 d3-force 力导向布局,自动计算节点位置
+
+##### 3.2.2 节点类型与颜色映射
+
+| 节点类型 | 标签名称 | 颜色 |
+| :--- | :--- | :--- |
+| Book | 书籍 | #E3A3B8 (粉色) |
+| Part | 部分 | #F9C351 (黄色) |
+| Chapter | 章节 | #F58B55 (橙色) |
+| EntityType | 实体类型 | #F15D5D (红色) |
+| Entity | 实体 | #CD96CD (紫色) |
+
+##### 3.2.3 工具栏功能
+
+工具栏位于右上角,提供以下操作:
+
+| 按钮 | 功能 | 快捷键 |
+| :--- | :--- | :--- |
+| 🔍 放大 | 放大视图 1.2 倍 | - |
+| 🔍 缩小 | 缩小视图 0.8 倍 | - |
+| 📷 导出 | 导出当前图谱为 PNG 图片 | - |
+| 🔄 重置 | 重置视图到初始状态 | - |
+| ⬜ 全屏 | 切换全屏模式 | F11 |
+
+##### 3.2.4 小地图 (Minimap)
+
+- 位于右下角
+- 显示图谱缩略图
+- 红色框表示当前视口位置
+- 支持点击快速定位
+
+#### 操作说明
+
+##### 基础操作
+
+| 操作 | 方式 | 说明 |
+| :--- | :--- | :--- |
+| 平移画布 | 拖拽空白区域 | 移动整个图谱视图 |
+| 缩放视图 | 鼠标滚轮 | 放大/缩小当前视图 |
+| 选中节点 | 左键点击节点 | 节点边框变粗表示选中 |
+| 拖拽节点 | 拖拽节点 | 移动单个节点位置 |
+
+##### 右键菜单操作
+
+**节点右键菜单**:
+1. **展开**:点击已选中节点的右键菜单"展开",加载该节点的子节点和关联边
+2. **收起**:点击已展开节点的右键菜单"收起",隐藏该节点的所有子节点
+3. **查看原文数据**:打开抽屉展示该节点关联的原文数据
+
+**边右键菜单**:
+1. **查看原文数据**:打开抽屉展示该关系关联的原文数据
+
+##### 搜索功能
+
+1. 在顶部搜索框输入节点名称
+2. 按 Enter 键或点击搜索按钮执行搜索
+3. 系统将高亮显示匹配的节点及其关联关系
+4. 点击搜索框清除按钮恢复初始视图
+
+#### 界面元素
+
+| 元素 | 位置 | 样式特征 |
+| :--- | :--- | :--- |
+| 搜索框 | 左上角 | 白色背景,圆角 6px,带阴影 |
+| 画布 | 中央区域 | 白色背景,带网格线 |
+| 工具栏 | 右上角 | 白色背景,圆角 4px |
+| 小地图 | 右下角 | 白色背景,带边框 |
+
+---
+
+### 3.3 标签筛选面板 (LabelSelected)
+
+#### 功能说明
+- 提供节点类型筛选功能
+- 支持展开/收起面板
+- 点击标签按钮筛选对应类型节点
+
+#### 筛选标签
+
+| 标签名称 | 对应类型 | 颜色 |
+| :--- | :--- | :--- |
+| 书籍 | Book | #E3A3B8 |
+| 部分 | Part | #F9C351 |
+| 章节 | Chapter | #F58B55 |
+
+#### 操作说明
+1. 点击标签按钮筛选该类型的节点
+2. 点击面板左侧箭头展开/收起面板
+
+#### 界面元素
+
+| 元素 | 样式特征 |
+| :--- | :--- |
+| 面板标题 | 紫色渐变背景,白色文字 |
+| 标签按钮 | 圆角 20px,带颜色圆点 |
+| 展开/收起箭头 | 灰色图标,悬停变蓝 |
+
+---
+
+### 3.4 节点统计面板 (NodeCountStatisticize)
+
+#### 功能说明
+- 统计当前图谱中各类型节点数量
+- 显示各类型节点占比
+- 支持点击统计项高亮对应节点
+- 支持取消高亮和刷新统计
+
+#### 操作说明
+
+##### 高亮节点
+1. 查看统计面板中的节点类型分布
+2. 点击任意统计项,该类型节点高亮显示(发光效果),其他节点变暗
+
+##### 取消高亮
+1. 点击面板底部"取消高亮"按钮
+2. 所有节点恢复正常显示
+
+##### 刷新统计
+1. 点击面板底部"刷新"按钮
+2. 重新计算节点统计数据
+
+#### 界面元素
+
+| 元素 | 样式特征 |
+| :--- | :--- |
+| 面板标题 | 紫色渐变背景,白色文字 |
+| 总数标签 | 白色背景,圆角 20px |
+| 统计项 | 进度条展示占比,悬停高亮 |
+| 操作按钮 | 圆角按钮,带图标 |
+
+---
+
+## 四、数据溯源功能
+
+### 4.1 功能说明
+
+当用户需要查看节点或边的原始数据来源时,可通过右键菜单打开溯源详情抽屉,展示关联的原文数据片段。
+
+### 4.2 溯源卡片结构
+
+| 区域 | 内容 | 样式 |
+| :--- | :--- | :--- |
+| 卡片头部 | 切片ID标签 | 蓝色背景,白色文字 |
+| 卡片主体 | 章节引用 | 左侧蓝色边框,灰色引用图标 |
+| 完整上下文 | 原文片段 | 灰色背景,圆角 6px |
+
+### 4.3 操作说明
+1. 右键点击节点或边
+2. 选择"查看原文数据"
+3. 在右侧抽屉中查看关联的原文片段
+4. 点击抽屉关闭按钮或点击遮罩层关闭抽屉
+
+---
+
+## 五、快捷键与快捷操作
+
+### 5.1 画布操作
+
+| 操作 | 快捷键/方式 |
+| :--- | :--- |
+| 平移画布 | 拖拽空白区域 |
+| 缩放视图 | 鼠标滚轮 |
+| 选中节点 | 左键点击 |
+| 拖拽节点 | 拖拽节点 |
+| 右键菜单 | 右键点击节点/边 |
+
+### 5.2 工具栏操作
+
+| 操作 | 按钮位置 |
+| :--- | :--- |
+| 放大 | 工具栏第一个按钮 |
+| 缩小 | 工具栏第二个按钮 |
+| 导出图片 | 工具栏第三个按钮 |
+| 重置视图 | 工具栏第四个按钮 |
+| 全屏模式 | 工具栏第五个按钮 |
+
+### 5.3 搜索操作
+
+| 操作 | 方式 |
+| :--- | :--- |
+| 执行搜索 | Enter 键 / 点击搜索按钮 |
+| 清除搜索 | 点击清除按钮 |
+
+---
+
+## 六、系统状态与提示
+
+### 6.1 消息提示
+
+系统使用 Arco Design 的 Message 组件展示操作反馈:
+
+| 场景 | 提示内容 | 类型 |
+| :--- | :--- | :--- |
+| 搜索成功 | "成功渲染 X 个节点,并建立 Y 条关联线!" | success |
+| 搜索无结果 | "未找到相关的节点" | warning |
+| 取消高亮 | "已取消高亮" | info |
+| 刷新统计 | "统计数据已刷新" | info |
+| 查询失败 | "节点级联查询失败" | error |
+
+### 6.2 节点状态
+
+| 状态 | 视觉表现 | 触发方式 |
+| :--- | :--- | :--- |
+| 选中 | 边框加粗(3px) | 左键点击 |
+| 高亮 | 发光效果(shadowBlur 20) | 统计面板点击 |
+| 变暗 | 透明度降低(0.3) | 其他类型高亮时 |
+| 默认 | 正常显示 | 初始状态 |
+
+---
+
+## 七、数据加载流程
+
+### 7.1 初始化流程
+
+```
+页面加载 → 查找根节点 → 渲染初始图谱 → 布局完成 → 自动放大居中
+```
+
+### 7.2 节点展开流程
+
+```
+右键点击节点 → 选择"展开" → 查询子节点数据 → 更新图谱数据 → 重新布局渲染
+```
+
+### 7.3 节点收起流程
+
+```
+右键点击节点 → 选择"收起" → 递归删除子节点 → 更新图谱数据 → 重新渲染
+```
+
+### 7.4 搜索流程
+
+```
+输入关键词 → 执行搜索 → 获取匹配节点 → 更新图谱数据 → 布局居中 → 显示结果提示
+```
+
+---
+
+## 八、注意事项
+
+### 8.1 性能优化建议
+
+1. **节点数量控制**:建议单次展示节点数不超过 500 个,避免性能下降
+2. **布局等待**:大规模图谱布局时请耐心等待,布局完成后会自动居中
+3. **导出图片**:导出前建议调整视图到合适位置,确保所有关键节点可见
+
+### 8.2 操作提示
+
+1. **节点拖拽**:拖拽节点时会自动清除其他节点的选中状态
+2. **右键菜单**:点击画布空白区域可关闭右键菜单
+3. **全屏模式**:全屏状态下按 Esc 键或点击工具栏按钮退出
+
+---
+
+## 九、版本信息
+
+| 项目 | 版本 |
+| :--- | :--- |
+| Vue | 3.x |
+| AntV G6 | 最新稳定版 |
+| Arco Design Vue | 最新稳定版 |
+
+---
+
+**文档版本**: v1.0  
+**生成日期**: 2026年6月  
+**适用系统**: 知识图谱分析系统
+

BIN
docs/多维度知识图谱数据挖掘分析系统功能和操作描述.docx


BIN
docs/知识图谱分析系统功能和操作描述.docx


+ 14 - 0
index.html

@@ -0,0 +1,14 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>linwit-ai-graph</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script src="/config.js"></script>
+    <script type="module" src="/src/main.js"></script>
+  </body>
+</html>

+ 2301 - 0
package-lock.json

@@ -0,0 +1,2301 @@
+{
+  "name": "linwit-ai-graph",
+  "version": "0.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "linwit-ai-graph",
+      "version": "0.0.0",
+      "dependencies": {
+        "@antv/g6": "5.1.0",
+        "@arco-design/web-vue": "2.58.0",
+        "@vue-office/pptx": "^1.0.1",
+        "docx": "^9.7.1",
+        "file-saver": "^2.0.5",
+        "neo4j-driver": "^6.0.1",
+        "pinia": "3.0.4",
+        "vue": "3.5.32",
+        "vue-demi": "^0.14.10",
+        "vue-router": "4.6.4",
+        "wa-sqlite": "^1.0.0"
+      },
+      "devDependencies": {
+        "@vitejs/plugin-vue": "6.0.5",
+        "vite": "8.0.4"
+      }
+    },
+    "node_modules/@antv/algorithm": {
+      "version": "0.1.26",
+      "resolved": "https://registry.npmmirror.com/@antv/algorithm/-/algorithm-0.1.26.tgz",
+      "integrity": "sha512-DVhcFSQ8YQnMNW34Mk8BSsfc61iC1sAnmcfYoXTAshYHuU50p/6b7x3QYaGctDNKWGvi1ub7mPcSY0bK+aN0qg==",
+      "license": "MIT",
+      "dependencies": {
+        "@antv/util": "^2.0.13",
+        "tslib": "^2.0.0"
+      }
+    },
+    "node_modules/@antv/algorithm/node_modules/@antv/util": {
+      "version": "2.0.17",
+      "resolved": "https://registry.npmmirror.com/@antv/util/-/util-2.0.17.tgz",
+      "integrity": "sha512-o6I9hi5CIUvLGDhth0RxNSFDRwXeywmt6ExR4+RmVAzIi48ps6HUy+svxOCayvrPBN37uE6TAc2KDofRo0nK9Q==",
+      "license": "ISC",
+      "dependencies": {
+        "csstype": "^3.0.8",
+        "tslib": "^2.0.3"
+      }
+    },
+    "node_modules/@antv/component": {
+      "version": "2.1.11",
+      "resolved": "https://registry.npmmirror.com/@antv/component/-/component-2.1.11.tgz",
+      "integrity": "sha512-dTdz8VAd3rpjOaGEZTluz82mtzrP4XCtNlNQyrxY7VNRNcjtvpTLDn57bUL2lRu1T+iklKvgbE2llMriWkq9vQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@antv/g": "^6.1.11",
+        "@antv/scale": "^0.4.16",
+        "@antv/util": "^3.3.10",
+        "svg-path-parser": "^1.1.0"
+      }
+    },
+    "node_modules/@antv/event-emitter": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmmirror.com/@antv/event-emitter/-/event-emitter-0.1.3.tgz",
+      "integrity": "sha512-4ddpsiHN9Pd4UIlWuKVK1C4IiZIdbwQvy9i7DUSI3xNJ89FPUFt8lxDYj8GzzfdllV0NkJTRxnG+FvLk0llidg==",
+      "license": "MIT"
+    },
+    "node_modules/@antv/expr": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/@antv/expr/-/expr-1.0.2.tgz",
+      "integrity": "sha512-vrfdmPHkTuiS5voVutKl2l06w1ihBh9A8SFdQPEE+2KMVpkymzGOF1eWpfkbGZ7tiFE15GodVdhhHomD/hdIwg==",
+      "license": "MIT"
+    },
+    "node_modules/@antv/g": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmmirror.com/@antv/g/-/g-6.3.1.tgz",
+      "integrity": "sha512-WYEKqy86LHB2PzTmrZXrIsIe+3Epeds2f68zceQ+BJtRoGki7Sy4IhlC8LrUMztgfT1t3d/0L745NWZwITroKA==",
+      "license": "MIT",
+      "dependencies": {
+        "@antv/g-lite": "2.7.0",
+        "@antv/util": "^3.3.5",
+        "@babel/runtime": "^7.25.6",
+        "gl-matrix": "^3.4.3",
+        "html2canvas": "^1.4.1"
+      }
+    },
+    "node_modules/@antv/g-canvas": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmmirror.com/@antv/g-canvas/-/g-canvas-2.2.0.tgz",
+      "integrity": "sha512-h7zVBBo2aO64DuGKvq9sG+yTU3sCUb9DALCVm7nz8qGPs8hhLuFOkKPEzUDNfNYZGJUGzY8UDtJ3QRGRFcvEQg==",
+      "license": "MIT",
+      "dependencies": {
+        "@antv/g-lite": "2.7.0",
+        "@antv/g-math": "3.1.0",
+        "@antv/util": "^3.3.5",
+        "@babel/runtime": "^7.25.6",
+        "gl-matrix": "^3.4.3",
+        "tslib": "^2.5.3"
+      }
+    },
+    "node_modules/@antv/g-lite": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmmirror.com/@antv/g-lite/-/g-lite-2.7.0.tgz",
+      "integrity": "sha512-uSzgHYa5bwR5L2Au7/5tsOhFmXKZKLPBH90+Q9bP9teVs5VT4kOAi0isPSpDI8uhdDC2/VrfTWu5K9HhWI6FWw==",
+      "license": "MIT",
+      "dependencies": {
+        "@antv/g-math": "3.1.0",
+        "@antv/util": "^3.3.5",
+        "@antv/vendor": "^1.0.3",
+        "@babel/runtime": "^7.25.6",
+        "eventemitter3": "^5.0.1",
+        "gl-matrix": "^3.4.3",
+        "tslib": "^2.5.3"
+      }
+    },
+    "node_modules/@antv/g-math": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmmirror.com/@antv/g-math/-/g-math-3.1.0.tgz",
+      "integrity": "sha512-DtN1Gj/yI0UiK18nSBsZX8RK0LszGwqfb+cBYWgE+ddyTm8dZnW4tPUhV7QXePsS6/A5hHC+JFpAAK7OEGo5ZQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@antv/util": "^3.3.5",
+        "@babel/runtime": "^7.25.6",
+        "gl-matrix": "^3.4.3",
+        "tslib": "^2.5.3"
+      }
+    },
+    "node_modules/@antv/g-plugin-dragndrop": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmmirror.com/@antv/g-plugin-dragndrop/-/g-plugin-dragndrop-2.1.1.tgz",
+      "integrity": "sha512-+aesDUJVQDs6UJ2bOBbDlaGAPCfHmU0MbrMTlQlfpwNplWueqtgVAZ3L57oZ2ZGHRWUHiRwZGPjXMBM3O2LELw==",
+      "license": "MIT",
+      "dependencies": {
+        "@antv/g-lite": "2.7.0",
+        "@antv/util": "^3.3.5",
+        "@babel/runtime": "^7.25.6",
+        "tslib": "^2.5.3"
+      }
+    },
+    "node_modules/@antv/g6": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmmirror.com/@antv/g6/-/g6-5.1.0.tgz",
+      "integrity": "sha512-tvoBDKypL/zWEG99pgwGJLWr2CKA+6zVixYxaVzDOp0+TrPY2cxB1jevxFGPjbTOLBIMYt/vKCh1jnmDtfvtpg==",
+      "license": "MIT",
+      "dependencies": {
+        "@antv/algorithm": "^0.1.26",
+        "@antv/component": "^2.1.7",
+        "@antv/event-emitter": "^0.1.3",
+        "@antv/g": "^6.1.28",
+        "@antv/g-canvas": "^2.0.48",
+        "@antv/g-plugin-dragndrop": "^2.0.38",
+        "@antv/graphlib": "^2.0.4",
+        "@antv/hierarchy": "^0.7.1",
+        "@antv/layout": "^2.0.0",
+        "@antv/util": "^3.3.11",
+        "bubblesets-js": "^2.3.4"
+      }
+    },
+    "node_modules/@antv/graphlib": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmmirror.com/@antv/graphlib/-/graphlib-2.0.4.tgz",
+      "integrity": "sha512-zc/5oQlsdk42Z0ib1mGklwzhJ5vczLFiPa1v7DgJkTbgJ2YxRh9xdarf86zI49sKVJmgbweRpJs7Nu5bIiwv4w==",
+      "license": "MIT",
+      "dependencies": {
+        "@antv/event-emitter": "^0.1.3"
+      }
+    },
+    "node_modules/@antv/hierarchy": {
+      "version": "0.7.1",
+      "resolved": "https://registry.npmmirror.com/@antv/hierarchy/-/hierarchy-0.7.1.tgz",
+      "integrity": "sha512-7r22r+HxfcRZp79ZjGmsn97zgC1Iajrv0Mm9DIgx3lPfk+Kme2MG/+EKdZj1iEBsN0rJRzjWVPGL5YrBdVHchw==",
+      "license": "MIT"
+    },
+    "node_modules/@antv/layout": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/@antv/layout/-/layout-2.0.0.tgz",
+      "integrity": "sha512-aCZ3UdNc40SfT7meFV7QTADY2HCnc0DShVw56CJNTI6oExUIVU736grPuL5Dhb8/JrVaU4Y83QPN/P7KafBzlw==",
+      "license": "MIT",
+      "dependencies": {
+        "@antv/event-emitter": "^0.1.3",
+        "@antv/expr": "^1.0.2",
+        "@antv/graphlib": "^2.0.0",
+        "@antv/util": "^3.3.2",
+        "comlink": "^4.4.1",
+        "d3-force": "^3.0.0",
+        "d3-force-3d": "^3.0.5",
+        "d3-octree": "^1.0.2",
+        "d3-quadtree": "^3.0.1",
+        "dagre": "^0.8.5",
+        "ml-matrix": "^6.10.4",
+        "tslib": "^2.8.1"
+      }
+    },
+    "node_modules/@antv/scale": {
+      "version": "0.4.16",
+      "resolved": "https://registry.npmmirror.com/@antv/scale/-/scale-0.4.16.tgz",
+      "integrity": "sha512-5wg/zB5kXHxpTV5OYwJD3ja6R8yTiqIOkjOhmpEJiowkzRlbEC/BOyMvNUq5fqFIHnMCE9woO7+c3zxEQCKPjw==",
+      "license": "MIT",
+      "dependencies": {
+        "@antv/util": "^3.3.7",
+        "color-string": "^1.5.5",
+        "fecha": "^4.2.1"
+      }
+    },
+    "node_modules/@antv/util": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmmirror.com/@antv/util/-/util-3.3.11.tgz",
+      "integrity": "sha512-FII08DFM4ABh2q5rPYdr0hMtKXRgeZazvXaFYCs7J7uTcWDHUhczab2qOCJLNDugoj8jFag1djb7wS9ehaRYBg==",
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.3",
+        "gl-matrix": "^3.3.0",
+        "tslib": "^2.3.1"
+      }
+    },
+    "node_modules/@antv/vendor": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmmirror.com/@antv/vendor/-/vendor-1.0.11.tgz",
+      "integrity": "sha512-LmhPEQ+aapk3barntaiIxJ5VHno/Tyab2JnfdcPzp5xONh/8VSfed4bo/9xKo5HcUAEydko38vYLfj6lJliLiw==",
+      "license": "MIT AND ISC",
+      "dependencies": {
+        "@types/d3-array": "^3.2.1",
+        "@types/d3-color": "^3.1.3",
+        "@types/d3-dispatch": "^3.0.6",
+        "@types/d3-dsv": "^3.0.7",
+        "@types/d3-ease": "^3.0.2",
+        "@types/d3-fetch": "^3.0.7",
+        "@types/d3-force": "^3.0.10",
+        "@types/d3-format": "^3.0.4",
+        "@types/d3-geo": "^3.1.0",
+        "@types/d3-hierarchy": "^3.1.7",
+        "@types/d3-interpolate": "^3.0.4",
+        "@types/d3-path": "^3.1.0",
+        "@types/d3-quadtree": "^3.0.6",
+        "@types/d3-random": "^3.0.3",
+        "@types/d3-scale": "^4.0.9",
+        "@types/d3-scale-chromatic": "^3.1.0",
+        "@types/d3-shape": "^3.1.7",
+        "@types/d3-time": "^3.0.4",
+        "@types/d3-timer": "^3.0.2",
+        "d3-array": "^3.2.4",
+        "d3-color": "^3.1.0",
+        "d3-dispatch": "^3.0.1",
+        "d3-dsv": "^3.0.1",
+        "d3-ease": "^3.0.1",
+        "d3-fetch": "^3.0.1",
+        "d3-force": "^3.0.0",
+        "d3-force-3d": "^3.0.5",
+        "d3-format": "^3.1.0",
+        "d3-geo": "^3.1.1",
+        "d3-geo-projection": "^4.0.0",
+        "d3-hierarchy": "^3.1.2",
+        "d3-interpolate": "^3.0.1",
+        "d3-path": "^3.1.0",
+        "d3-quadtree": "^3.0.1",
+        "d3-random": "^3.0.1",
+        "d3-regression": "^1.3.10",
+        "d3-scale": "^4.0.2",
+        "d3-scale-chromatic": "^3.1.0",
+        "d3-shape": "^3.2.0",
+        "d3-time": "^3.1.0",
+        "d3-timer": "^3.0.1"
+      }
+    },
+    "node_modules/@arco-design/color": {
+      "version": "0.4.0",
+      "license": "MIT",
+      "dependencies": {
+        "color": "^3.1.3"
+      }
+    },
+    "node_modules/@arco-design/web-vue": {
+      "version": "2.58.0",
+      "license": "MIT",
+      "dependencies": {
+        "@arco-design/color": "^0.4.0",
+        "b-tween": "^0.3.3",
+        "b-validate": "^1.5.3",
+        "compute-scroll-into-view": "^1.0.20",
+        "dayjs": "^1.11.13",
+        "number-precision": "^1.6.0",
+        "resize-observer-polyfill": "^1.5.1",
+        "scroll-into-view-if-needed": "^2.2.31",
+        "vue": "^3.1.0"
+      },
+      "peerDependencies": {
+        "vue": ">=3.1.0"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.27.1",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.28.5",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.29.2",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.29.0"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/runtime": {
+      "version": "7.29.2",
+      "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.29.2.tgz",
+      "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.29.0",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.27.1",
+        "@babel/helper-validator-identifier": "^7.28.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "license": "MIT"
+    },
+    "node_modules/@napi-rs/wasm-runtime": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
+      "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@tybys/wasm-util": "^0.10.1"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/Brooooooklyn"
+      },
+      "peerDependencies": {
+        "@emnapi/core": "^1.7.1",
+        "@emnapi/runtime": "^1.7.1"
+      }
+    },
+    "node_modules/@oxc-project/types": {
+      "version": "0.122.0",
+      "resolved": "https://registry.npmmirror.com/@oxc-project/types/-/types-0.122.0.tgz",
+      "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/Boshen"
+      }
+    },
+    "node_modules/@rolldown/binding-android-arm64": {
+      "version": "1.0.0-rc.12",
+      "resolved": "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz",
+      "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-darwin-arm64": {
+      "version": "1.0.0-rc.12",
+      "resolved": "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz",
+      "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-darwin-x64": {
+      "version": "1.0.0-rc.12",
+      "resolved": "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz",
+      "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-freebsd-x64": {
+      "version": "1.0.0-rc.12",
+      "resolved": "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz",
+      "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+      "version": "1.0.0-rc.12",
+      "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz",
+      "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-arm64-gnu": {
+      "version": "1.0.0-rc.12",
+      "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz",
+      "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-arm64-musl": {
+      "version": "1.0.0-rc.12",
+      "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz",
+      "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+      "version": "1.0.0-rc.12",
+      "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz",
+      "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-s390x-gnu": {
+      "version": "1.0.0-rc.12",
+      "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz",
+      "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-x64-gnu": {
+      "version": "1.0.0-rc.12",
+      "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz",
+      "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-x64-musl": {
+      "version": "1.0.0-rc.12",
+      "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz",
+      "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-openharmony-arm64": {
+      "version": "1.0.0-rc.12",
+      "resolved": "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz",
+      "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-wasm32-wasi": {
+      "version": "1.0.0-rc.12",
+      "resolved": "https://registry.npmmirror.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz",
+      "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==",
+      "cpu": [
+        "wasm32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@napi-rs/wasm-runtime": "^1.1.1"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/@rolldown/binding-win32-arm64-msvc": {
+      "version": "1.0.0-rc.12",
+      "resolved": "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz",
+      "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-win32-x64-msvc": {
+      "version": "1.0.0-rc.12",
+      "resolved": "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz",
+      "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/pluginutils": {
+      "version": "1.0.0-rc.2",
+      "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz",
+      "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@tybys/wasm-util": {
+      "version": "0.10.1",
+      "resolved": "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
+      "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@types/d3-array": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmmirror.com/@types/d3-array/-/d3-array-3.2.2.tgz",
+      "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-color": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmmirror.com/@types/d3-color/-/d3-color-3.1.3.tgz",
+      "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-dispatch": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmmirror.com/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
+      "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-dsv": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmmirror.com/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
+      "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-ease": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmmirror.com/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+      "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-fetch": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmmirror.com/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
+      "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-dsv": "*"
+      }
+    },
+    "node_modules/@types/d3-force": {
+      "version": "3.0.10",
+      "resolved": "https://registry.npmmirror.com/@types/d3-force/-/d3-force-3.0.10.tgz",
+      "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-format": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmmirror.com/@types/d3-format/-/d3-format-3.0.4.tgz",
+      "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-geo": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmmirror.com/@types/d3-geo/-/d3-geo-3.1.0.tgz",
+      "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/geojson": "*"
+      }
+    },
+    "node_modules/@types/d3-hierarchy": {
+      "version": "3.1.7",
+      "resolved": "https://registry.npmmirror.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
+      "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-interpolate": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmmirror.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+      "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-color": "*"
+      }
+    },
+    "node_modules/@types/d3-path": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmmirror.com/@types/d3-path/-/d3-path-3.1.1.tgz",
+      "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-quadtree": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmmirror.com/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
+      "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-random": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmmirror.com/@types/d3-random/-/d3-random-3.0.3.tgz",
+      "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-scale": {
+      "version": "4.0.9",
+      "resolved": "https://registry.npmmirror.com/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+      "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-time": "*"
+      }
+    },
+    "node_modules/@types/d3-scale-chromatic": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmmirror.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+      "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-shape": {
+      "version": "3.1.8",
+      "resolved": "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.8.tgz",
+      "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-path": "*"
+      }
+    },
+    "node_modules/@types/d3-time": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmmirror.com/@types/d3-time/-/d3-time-3.0.4.tgz",
+      "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-timer": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmmirror.com/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+      "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+      "license": "MIT"
+    },
+    "node_modules/@types/geojson": {
+      "version": "7946.0.16",
+      "resolved": "https://registry.npmmirror.com/@types/geojson/-/geojson-7946.0.16.tgz",
+      "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+      "license": "MIT"
+    },
+    "node_modules/@types/node": {
+      "version": "25.9.4",
+      "resolved": "https://registry.npmmirror.com/@types/node/-/node-25.9.4.tgz",
+      "integrity": "sha512-dszCsrKb5U7ZsVZBWiHFklTloVl0mSEnWH/iZXfZUlI4rzCUnsvGmgqfuVRHL54ugE7/wRuxEIXRa2iMZ+BG6g==",
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": ">=7.24.0 <7.24.7"
+      }
+    },
+    "node_modules/@vitejs/plugin-vue": {
+      "version": "6.0.5",
+      "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.5.tgz",
+      "integrity": "sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@rolldown/pluginutils": "1.0.0-rc.2"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "peerDependencies": {
+        "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
+        "vue": "^3.2.25"
+      }
+    },
+    "node_modules/@vue-office/pptx": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/@vue-office/pptx/-/pptx-1.0.1.tgz",
+      "integrity": "sha512-+V7Kctzl6f6+Yk4NaD/wQGRIkqLWcowe0jEhPexWQb8Oilbzt1OyhWRWcMsxNDTdrgm6aMLP+0/tmw27cxddMg==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "@vue/composition-api": "^1.7.1",
+        "vue": "^2.0.0 || >=3.0.0",
+        "vue-demi": "^0.14.6"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vue/compiler-core": {
+      "version": "3.5.32",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.2",
+        "@vue/shared": "3.5.32",
+        "entities": "^7.0.1",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-dom": {
+      "version": "3.5.32",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-core": "3.5.32",
+        "@vue/shared": "3.5.32"
+      }
+    },
+    "node_modules/@vue/compiler-sfc": {
+      "version": "3.5.32",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.2",
+        "@vue/compiler-core": "3.5.32",
+        "@vue/compiler-dom": "3.5.32",
+        "@vue/compiler-ssr": "3.5.32",
+        "@vue/shared": "3.5.32",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.21",
+        "postcss": "^8.5.8",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-ssr": {
+      "version": "3.5.32",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.32",
+        "@vue/shared": "3.5.32"
+      }
+    },
+    "node_modules/@vue/devtools-api": {
+      "version": "6.6.4",
+      "license": "MIT"
+    },
+    "node_modules/@vue/devtools-kit": {
+      "version": "7.7.9",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz",
+      "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-shared": "^7.7.9",
+        "birpc": "^2.3.0",
+        "hookable": "^5.5.3",
+        "mitt": "^3.0.1",
+        "perfect-debounce": "^1.0.0",
+        "speakingurl": "^14.0.1",
+        "superjson": "^2.2.2"
+      }
+    },
+    "node_modules/@vue/devtools-shared": {
+      "version": "7.7.9",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz",
+      "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==",
+      "license": "MIT",
+      "dependencies": {
+        "rfdc": "^1.4.1"
+      }
+    },
+    "node_modules/@vue/reactivity": {
+      "version": "3.5.32",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/shared": "3.5.32"
+      }
+    },
+    "node_modules/@vue/runtime-core": {
+      "version": "3.5.32",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.32",
+        "@vue/shared": "3.5.32"
+      }
+    },
+    "node_modules/@vue/runtime-dom": {
+      "version": "3.5.32",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.32",
+        "@vue/runtime-core": "3.5.32",
+        "@vue/shared": "3.5.32",
+        "csstype": "^3.2.3"
+      }
+    },
+    "node_modules/@vue/server-renderer": {
+      "version": "3.5.32",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-ssr": "3.5.32",
+        "@vue/shared": "3.5.32"
+      },
+      "peerDependencies": {
+        "vue": "3.5.32"
+      }
+    },
+    "node_modules/@vue/shared": {
+      "version": "3.5.32",
+      "license": "MIT"
+    },
+    "node_modules/b-tween": {
+      "version": "0.3.3",
+      "license": "MIT"
+    },
+    "node_modules/b-validate": {
+      "version": "1.5.3",
+      "license": "MIT"
+    },
+    "node_modules/base64-arraybuffer": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
+      "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6.0"
+      }
+    },
+    "node_modules/base64-js": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz",
+      "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/birpc": {
+      "version": "2.9.0",
+      "resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.9.0.tgz",
+      "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/bubblesets-js": {
+      "version": "2.3.4",
+      "resolved": "https://registry.npmmirror.com/bubblesets-js/-/bubblesets-js-2.3.4.tgz",
+      "integrity": "sha512-DyMjHmpkS2+xcFNtyN00apJYL3ESdp9fTrkDr5+9Qg/GPqFmcWgGsK1akZnttE1XFxJ/VMy4DNNGMGYtmFp1Sg==",
+      "license": "MIT"
+    },
+    "node_modules/buffer": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmmirror.com/buffer/-/buffer-6.0.3.tgz",
+      "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "base64-js": "^1.3.1",
+        "ieee754": "^1.2.1"
+      }
+    },
+    "node_modules/color": {
+      "version": "3.2.1",
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^1.9.3",
+        "color-string": "^1.6.0"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "1.9.3",
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "1.1.3"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.3",
+      "license": "MIT"
+    },
+    "node_modules/color-string": {
+      "version": "1.9.1",
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "^1.0.0",
+        "simple-swizzle": "^0.2.2"
+      }
+    },
+    "node_modules/comlink": {
+      "version": "4.4.2",
+      "resolved": "https://registry.npmmirror.com/comlink/-/comlink-4.4.2.tgz",
+      "integrity": "sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g==",
+      "license": "Apache-2.0"
+    },
+    "node_modules/commander": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmmirror.com/commander/-/commander-7.2.0.tgz",
+      "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/compute-scroll-into-view": {
+      "version": "1.0.20",
+      "license": "MIT"
+    },
+    "node_modules/copy-anything": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmmirror.com/copy-anything/-/copy-anything-4.0.5.tgz",
+      "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==",
+      "license": "MIT",
+      "dependencies": {
+        "is-what": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mesqueeb"
+      }
+    },
+    "node_modules/core-util-is": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz",
+      "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+      "license": "MIT"
+    },
+    "node_modules/css-line-break": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/css-line-break/-/css-line-break-2.1.0.tgz",
+      "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
+      "license": "MIT",
+      "dependencies": {
+        "utrie": "^1.0.2"
+      }
+    },
+    "node_modules/csstype": {
+      "version": "3.2.3",
+      "license": "MIT"
+    },
+    "node_modules/d3-array": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.4.tgz",
+      "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+      "license": "ISC",
+      "dependencies": {
+        "internmap": "1 - 2"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-binarytree": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/d3-binarytree/-/d3-binarytree-1.0.2.tgz",
+      "integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==",
+      "license": "MIT"
+    },
+    "node_modules/d3-color": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz",
+      "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-dispatch": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+      "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-dsv": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/d3-dsv/-/d3-dsv-3.0.1.tgz",
+      "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
+      "license": "ISC",
+      "dependencies": {
+        "commander": "7",
+        "iconv-lite": "0.6",
+        "rw": "1"
+      },
+      "bin": {
+        "csv2json": "bin/dsv2json.js",
+        "csv2tsv": "bin/dsv2dsv.js",
+        "dsv2dsv": "bin/dsv2dsv.js",
+        "dsv2json": "bin/dsv2json.js",
+        "json2csv": "bin/json2dsv.js",
+        "json2dsv": "bin/json2dsv.js",
+        "json2tsv": "bin/json2dsv.js",
+        "tsv2csv": "bin/dsv2dsv.js",
+        "tsv2json": "bin/dsv2json.js"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-ease": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz",
+      "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-fetch": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/d3-fetch/-/d3-fetch-3.0.1.tgz",
+      "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-dsv": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-force": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/d3-force/-/d3-force-3.0.0.tgz",
+      "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-dispatch": "1 - 3",
+        "d3-quadtree": "1 - 3",
+        "d3-timer": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-force-3d": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmmirror.com/d3-force-3d/-/d3-force-3d-3.0.6.tgz",
+      "integrity": "sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==",
+      "license": "MIT",
+      "dependencies": {
+        "d3-binarytree": "1",
+        "d3-dispatch": "1 - 3",
+        "d3-octree": "1",
+        "d3-quadtree": "1 - 3",
+        "d3-timer": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-format": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.2.tgz",
+      "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-geo": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmmirror.com/d3-geo/-/d3-geo-3.1.1.tgz",
+      "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-array": "2.5.0 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-geo-projection": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/d3-geo-projection/-/d3-geo-projection-4.0.0.tgz",
+      "integrity": "sha512-p0bK60CEzph1iqmnxut7d/1kyTmm3UWtPlwdkM31AU+LW+BXazd5zJdoCn7VFxNCHXRngPHRnsNn5uGjLRGndg==",
+      "license": "ISC",
+      "dependencies": {
+        "commander": "7",
+        "d3-array": "1 - 3",
+        "d3-geo": "1.12.0 - 3"
+      },
+      "bin": {
+        "geo2svg": "bin/geo2svg.js",
+        "geograticule": "bin/geograticule.js",
+        "geoproject": "bin/geoproject.js",
+        "geoquantize": "bin/geoquantize.js",
+        "geostitch": "bin/geostitch.js"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-hierarchy": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmmirror.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
+      "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-interpolate": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+      "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-color": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-octree": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/d3-octree/-/d3-octree-1.1.0.tgz",
+      "integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==",
+      "license": "MIT"
+    },
+    "node_modules/d3-path": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmmirror.com/d3-path/-/d3-path-3.1.0.tgz",
+      "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-quadtree": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
+      "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-random": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/d3-random/-/d3-random-3.0.1.tgz",
+      "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-regression": {
+      "version": "1.3.10",
+      "resolved": "https://registry.npmmirror.com/d3-regression/-/d3-regression-1.3.10.tgz",
+      "integrity": "sha512-PF8GWEL70cHHWpx2jUQXc68r1pyPHIA+St16muk/XRokETzlegj5LriNKg7o4LR0TySug4nHYPJNNRz/W+/Niw==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/d3-scale": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmmirror.com/d3-scale/-/d3-scale-4.0.2.tgz",
+      "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-array": "2.10.0 - 3",
+        "d3-format": "1 - 3",
+        "d3-interpolate": "1.2.0 - 3",
+        "d3-time": "2.1.1 - 3",
+        "d3-time-format": "2 - 4"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-scale-chromatic": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmmirror.com/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+      "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-color": "1 - 3",
+        "d3-interpolate": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-shape": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz",
+      "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-path": "^3.1.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-time": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmmirror.com/d3-time/-/d3-time-3.1.0.tgz",
+      "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-array": "2 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-time-format": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/d3-time-format/-/d3-time-format-4.1.0.tgz",
+      "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-time": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-timer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz",
+      "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/dagre": {
+      "version": "0.8.5",
+      "resolved": "https://registry.npmmirror.com/dagre/-/dagre-0.8.5.tgz",
+      "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==",
+      "license": "MIT",
+      "dependencies": {
+        "graphlib": "^2.1.8",
+        "lodash": "^4.17.15"
+      }
+    },
+    "node_modules/dayjs": {
+      "version": "1.11.20",
+      "license": "MIT"
+    },
+    "node_modules/detect-libc": {
+      "version": "2.1.2",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/docx": {
+      "version": "9.7.1",
+      "resolved": "https://registry.npmmirror.com/docx/-/docx-9.7.1.tgz",
+      "integrity": "sha512-ilXFf9Moz47ABjFpDiA5s1w9lpb4EFSp7+5iiJSbfyYDM+bpZdAgLlSr7fW4aXhVe/E+F6QCv0EvRVFEd5CsWg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "^25.2.3",
+        "hash.js": "^1.1.7",
+        "jszip": "^3.10.1",
+        "nanoid": "^5.1.3",
+        "xml": "^1.0.1",
+        "xml-js": "^1.6.8"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/docx/node_modules/nanoid": {
+      "version": "5.1.15",
+      "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-5.1.15.tgz",
+      "integrity": "sha512-kBg3RpGtIe+RpTbyXwoI6pk5yD7KUiI3sygUqgeBMRst42KmhB4RZC7eiO9Wa1HIpaCCtpE2DJ6OI4Wi5ebwFw==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.js"
+      },
+      "engines": {
+        "node": "^18 || >=20"
+      }
+    },
+    "node_modules/entities": {
+      "version": "7.0.1",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/estree-walker": {
+      "version": "2.0.2",
+      "license": "MIT"
+    },
+    "node_modules/eventemitter3": {
+      "version": "5.0.4",
+      "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.4.tgz",
+      "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
+      "license": "MIT"
+    },
+    "node_modules/fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+      "license": "MIT"
+    },
+    "node_modules/fdir": {
+      "version": "6.5.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/fecha": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmmirror.com/fecha/-/fecha-4.2.3.tgz",
+      "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==",
+      "license": "MIT"
+    },
+    "node_modules/file-saver": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmmirror.com/file-saver/-/file-saver-2.0.5.tgz",
+      "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==",
+      "license": "MIT"
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/gl-matrix": {
+      "version": "3.4.4",
+      "resolved": "https://registry.npmmirror.com/gl-matrix/-/gl-matrix-3.4.4.tgz",
+      "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
+      "license": "MIT"
+    },
+    "node_modules/graphlib": {
+      "version": "2.1.8",
+      "resolved": "https://registry.npmmirror.com/graphlib/-/graphlib-2.1.8.tgz",
+      "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==",
+      "license": "MIT",
+      "dependencies": {
+        "lodash": "^4.17.15"
+      }
+    },
+    "node_modules/hash.js": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmmirror.com/hash.js/-/hash.js-1.1.7.tgz",
+      "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
+      "license": "MIT",
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "minimalistic-assert": "^1.0.1"
+      }
+    },
+    "node_modules/hookable": {
+      "version": "5.5.3",
+      "resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
+      "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
+      "license": "MIT"
+    },
+    "node_modules/html2canvas": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz",
+      "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
+      "license": "MIT",
+      "dependencies": {
+        "css-line-break": "^2.1.0",
+        "text-segmentation": "^1.0.3"
+      },
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz",
+      "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/ieee754": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz",
+      "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/immediate": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz",
+      "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
+      "license": "MIT"
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "license": "ISC"
+    },
+    "node_modules/internmap": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz",
+      "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/is-any-array": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/is-any-array/-/is-any-array-3.0.0.tgz",
+      "integrity": "sha512-o4h+tylWykC4BD1vaejp6gDxoM13bwW8FGuNs4yIKpj8xbBJcRxJx8vZpq0dCr7ZDEfeKjmsi/euolKhX6f/ww==",
+      "license": "MIT"
+    },
+    "node_modules/is-arrayish": {
+      "version": "0.3.4",
+      "license": "MIT"
+    },
+    "node_modules/is-what": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmmirror.com/is-what/-/is-what-5.5.0.tgz",
+      "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mesqueeb"
+      }
+    },
+    "node_modules/isarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz",
+      "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+      "license": "MIT"
+    },
+    "node_modules/jszip": {
+      "version": "3.10.1",
+      "resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz",
+      "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
+      "license": "(MIT OR GPL-3.0-or-later)",
+      "dependencies": {
+        "lie": "~3.3.0",
+        "pako": "~1.0.2",
+        "readable-stream": "~2.3.6",
+        "setimmediate": "^1.0.5"
+      }
+    },
+    "node_modules/lie": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz",
+      "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+      "license": "MIT",
+      "dependencies": {
+        "immediate": "~3.0.5"
+      }
+    },
+    "node_modules/lightningcss": {
+      "version": "1.32.0",
+      "dev": true,
+      "license": "MPL-2.0",
+      "dependencies": {
+        "detect-libc": "^2.0.3"
+      },
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      },
+      "optionalDependencies": {
+        "lightningcss-android-arm64": "1.32.0",
+        "lightningcss-darwin-arm64": "1.32.0",
+        "lightningcss-darwin-x64": "1.32.0",
+        "lightningcss-freebsd-x64": "1.32.0",
+        "lightningcss-linux-arm-gnueabihf": "1.32.0",
+        "lightningcss-linux-arm64-gnu": "1.32.0",
+        "lightningcss-linux-arm64-musl": "1.32.0",
+        "lightningcss-linux-x64-gnu": "1.32.0",
+        "lightningcss-linux-x64-musl": "1.32.0",
+        "lightningcss-win32-arm64-msvc": "1.32.0",
+        "lightningcss-win32-x64-msvc": "1.32.0"
+      }
+    },
+    "node_modules/lightningcss-win32-x64-msvc": {
+      "version": "1.32.0",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lodash": {
+      "version": "4.18.1",
+      "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz",
+      "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
+      "license": "MIT"
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.21",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.5"
+      }
+    },
+    "node_modules/minimalistic-assert": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+      "license": "ISC"
+    },
+    "node_modules/mitt": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
+      "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
+      "license": "MIT"
+    },
+    "node_modules/ml-array-max": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/ml-array-max/-/ml-array-max-2.0.0.tgz",
+      "integrity": "sha512-QQZ4kENwpWmyNb98UXRDFXrmtIXuXtt1+bSbda/2KA85+F+rrJP8hZk6QOkCQXM2Th9mUDYdq/PNByPdT9ID4A==",
+      "license": "MIT",
+      "dependencies": {
+        "is-any-array": "^3.0.0"
+      }
+    },
+    "node_modules/ml-array-min": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/ml-array-min/-/ml-array-min-2.0.0.tgz",
+      "integrity": "sha512-GRj6Ky6sW9vGL6yIjgsHmXZ9YgrdmcQ8nCxPqEGeKc6dkfYg1XDYxGFxADUjNuZyoCd5PUscWAS4N+cFaX6hFg==",
+      "license": "MIT",
+      "dependencies": {
+        "is-any-array": "^3.0.0"
+      }
+    },
+    "node_modules/ml-array-rescale": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/ml-array-rescale/-/ml-array-rescale-2.0.0.tgz",
+      "integrity": "sha512-2GGtKfSno94/kIloWGvpp/U5Q5vLvLrza+SAaGsLeo6Xj4mEbA6Gqx+oTfZFkxnd1grT2X007HfJNs3T5BsiVg==",
+      "license": "MIT",
+      "dependencies": {
+        "is-any-array": "^3.0.0",
+        "ml-array-max": "^2.0.0",
+        "ml-array-min": "^2.0.0"
+      }
+    },
+    "node_modules/ml-matrix": {
+      "version": "6.12.2",
+      "resolved": "https://registry.npmmirror.com/ml-matrix/-/ml-matrix-6.12.2.tgz",
+      "integrity": "sha512-GC+BnW+pBh8Auap8goAxY0senAmF0IEoc3HNVSfnfbvGw0buuDIYb9kAKMS1l+GiwJ1rfK2bzJ8IHhwjzATSFA==",
+      "license": "MIT",
+      "dependencies": {
+        "is-any-array": "^3.0.0",
+        "ml-array-rescale": "^2.0.0"
+      }
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.11",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/neo4j-driver": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmmirror.com/neo4j-driver/-/neo4j-driver-6.0.1.tgz",
+      "integrity": "sha512-8DDF2MwEJNz7y7cp97x4u8fmVIP4CWS8qNBxdwxTG0fWtsS+2NdeC+7uXwmmuFOpHvkfXqv63uWY73bfDtOH8Q==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "neo4j-driver-bolt-connection": "6.0.1",
+        "neo4j-driver-core": "6.0.1",
+        "rxjs": "^7.8.2"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "node_modules/neo4j-driver-bolt-connection": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmmirror.com/neo4j-driver-bolt-connection/-/neo4j-driver-bolt-connection-6.0.1.tgz",
+      "integrity": "sha512-1KyG73TO+CwnYJisdHD0sjUw9yR+P5q3JFcmVPzsHT4/whzCjuXSMpmY4jZcHH2PdY2cBUq4l/6WcDiPMxW2UA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "buffer": "^6.0.3",
+        "neo4j-driver-core": "6.0.1",
+        "string_decoder": "^1.3.0"
+      }
+    },
+    "node_modules/neo4j-driver-core": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmmirror.com/neo4j-driver-core/-/neo4j-driver-core-6.0.1.tgz",
+      "integrity": "sha512-5I2KxICAvcHxnWdJyDqwu8PBAQvWVTlQH2ve3VQmtVdJScPqWhpXN1PiX5IIl+cRF3pFpz9GQF53B5n6s0QQUQ==",
+      "license": "Apache-2.0"
+    },
+    "node_modules/number-precision": {
+      "version": "1.6.0",
+      "license": "MIT"
+    },
+    "node_modules/pako": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz",
+      "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
+      "license": "(MIT AND Zlib)"
+    },
+    "node_modules/perfect-debounce": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
+      "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
+      "license": "MIT"
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "4.0.4",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/pinia": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmmirror.com/pinia/-/pinia-3.0.4.tgz",
+      "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^7.7.7"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.5.0",
+        "vue": "^3.5.11"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/pinia/node_modules/@vue/devtools-api": {
+      "version": "7.7.9",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-7.7.9.tgz",
+      "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-kit": "^7.7.9"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.10",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.11",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/process-nextick-args": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+      "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+      "license": "MIT"
+    },
+    "node_modules/readable-stream": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz",
+      "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+      "license": "MIT",
+      "dependencies": {
+        "core-util-is": "~1.0.0",
+        "inherits": "~2.0.3",
+        "isarray": "~1.0.0",
+        "process-nextick-args": "~2.0.0",
+        "safe-buffer": "~5.1.1",
+        "string_decoder": "~1.1.1",
+        "util-deprecate": "~1.0.1"
+      }
+    },
+    "node_modules/readable-stream/node_modules/safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+      "license": "MIT"
+    },
+    "node_modules/readable-stream/node_modules/string_decoder": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz",
+      "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "~5.1.0"
+      }
+    },
+    "node_modules/resize-observer-polyfill": {
+      "version": "1.5.1",
+      "license": "MIT"
+    },
+    "node_modules/rfdc": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz",
+      "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
+      "license": "MIT"
+    },
+    "node_modules/rolldown": {
+      "version": "1.0.0-rc.12",
+      "resolved": "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.0-rc.12.tgz",
+      "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@oxc-project/types": "=0.122.0",
+        "@rolldown/pluginutils": "1.0.0-rc.12"
+      },
+      "bin": {
+        "rolldown": "bin/cli.mjs"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "optionalDependencies": {
+        "@rolldown/binding-android-arm64": "1.0.0-rc.12",
+        "@rolldown/binding-darwin-arm64": "1.0.0-rc.12",
+        "@rolldown/binding-darwin-x64": "1.0.0-rc.12",
+        "@rolldown/binding-freebsd-x64": "1.0.0-rc.12",
+        "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12",
+        "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12",
+        "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12",
+        "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12",
+        "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12",
+        "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12",
+        "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12",
+        "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12",
+        "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12",
+        "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12",
+        "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12"
+      }
+    },
+    "node_modules/rolldown/node_modules/@rolldown/pluginutils": {
+      "version": "1.0.0-rc.12",
+      "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz",
+      "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/rw": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmmirror.com/rw/-/rw-1.3.3.tgz",
+      "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/rxjs": {
+      "version": "7.8.2",
+      "resolved": "https://registry.npmmirror.com/rxjs/-/rxjs-7.8.2.tgz",
+      "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "tslib": "^2.1.0"
+      }
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "license": "MIT"
+    },
+    "node_modules/sax": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmmirror.com/sax/-/sax-1.6.0.tgz",
+      "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": ">=11.0.0"
+      }
+    },
+    "node_modules/scroll-into-view-if-needed": {
+      "version": "2.2.31",
+      "license": "MIT",
+      "dependencies": {
+        "compute-scroll-into-view": "^1.0.20"
+      }
+    },
+    "node_modules/setimmediate": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz",
+      "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
+      "license": "MIT"
+    },
+    "node_modules/simple-swizzle": {
+      "version": "0.2.4",
+      "license": "MIT",
+      "dependencies": {
+        "is-arrayish": "^0.3.1"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/speakingurl": {
+      "version": "14.0.1",
+      "resolved": "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz",
+      "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/string_decoder": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz",
+      "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "~5.2.0"
+      }
+    },
+    "node_modules/superjson": {
+      "version": "2.2.6",
+      "resolved": "https://registry.npmmirror.com/superjson/-/superjson-2.2.6.tgz",
+      "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==",
+      "license": "MIT",
+      "dependencies": {
+        "copy-anything": "^4"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/svg-path-parser": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/svg-path-parser/-/svg-path-parser-1.1.0.tgz",
+      "integrity": "sha512-jGCUqcQyXpfe38R7RFfhrMyfXcBmpMNJI/B+4CE9/Unkh98UporAc461GTthv+TVDuZXsBx7/WiwJb1Oh4tt4A==",
+      "license": "MIT"
+    },
+    "node_modules/text-segmentation": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/text-segmentation/-/text-segmentation-1.0.3.tgz",
+      "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
+      "license": "MIT",
+      "dependencies": {
+        "utrie": "^1.0.2"
+      }
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.16",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.4"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/tslib": {
+      "version": "2.8.1",
+      "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+      "license": "0BSD"
+    },
+    "node_modules/undici-types": {
+      "version": "7.24.6",
+      "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.24.6.tgz",
+      "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
+      "license": "MIT"
+    },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+      "license": "MIT"
+    },
+    "node_modules/utrie": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/utrie/-/utrie-1.0.2.tgz",
+      "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
+      "license": "MIT",
+      "dependencies": {
+        "base64-arraybuffer": "^1.0.2"
+      }
+    },
+    "node_modules/vite": {
+      "version": "8.0.4",
+      "resolved": "https://registry.npmmirror.com/vite/-/vite-8.0.4.tgz",
+      "integrity": "sha512-baBr4jUVSLJ0RPyZ2nK0zS2+W8hNHbM4hEzfvllukmRPVS3xDG5ATTNtbRXrKIOE2b8/FsPWJAOnuIxcs7g3cw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "lightningcss": "^1.32.0",
+        "picomatch": "^4.0.4",
+        "postcss": "^8.5.8",
+        "rolldown": "1.0.0-rc.12",
+        "tinyglobby": "^0.2.15"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^20.19.0 || >=22.12.0",
+        "@vitejs/devtools": "^0.1.0",
+        "esbuild": "^0.27.0 || ^0.28.0",
+        "jiti": ">=1.21.0",
+        "less": "^4.0.0",
+        "sass": "^1.70.0",
+        "sass-embedded": "^1.70.0",
+        "stylus": ">=0.54.8",
+        "sugarss": "^5.0.0",
+        "terser": "^5.16.0",
+        "tsx": "^4.8.1",
+        "yaml": "^2.4.2"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "@vitejs/devtools": {
+          "optional": true
+        },
+        "esbuild": {
+          "optional": true
+        },
+        "jiti": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        },
+        "tsx": {
+          "optional": true
+        },
+        "yaml": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue": {
+      "version": "3.5.32",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.32",
+        "@vue/compiler-sfc": "3.5.32",
+        "@vue/runtime-dom": "3.5.32",
+        "@vue/server-renderer": "3.5.32",
+        "@vue/shared": "3.5.32"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue-demi": {
+      "version": "0.14.10",
+      "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
+      "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "vue-demi-fix": "bin/vue-demi-fix.js",
+        "vue-demi-switch": "bin/vue-demi-switch.js"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "@vue/composition-api": "^1.0.0-rc.1",
+        "vue": "^3.0.0-0 || ^2.6.0"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue-router": {
+      "version": "4.6.4",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^6.6.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "vue": "^3.5.0"
+      }
+    },
+    "node_modules/wa-sqlite": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/wa-sqlite/-/wa-sqlite-1.0.0.tgz",
+      "integrity": "sha512-Kyybo5/BaJp76z7gDWGk2J6Hthl4NIPsE+swgraEjy3IY6r5zIR02wAs1OJH4XtJp1y3puj3Onp5eMGS0z7nUA=="
+    },
+    "node_modules/xml": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/xml/-/xml-1.0.1.tgz",
+      "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==",
+      "license": "MIT"
+    },
+    "node_modules/xml-js": {
+      "version": "1.6.11",
+      "resolved": "https://registry.npmmirror.com/xml-js/-/xml-js-1.6.11.tgz",
+      "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
+      "license": "MIT",
+      "dependencies": {
+        "sax": "^1.2.4"
+      },
+      "bin": {
+        "xml-js": "bin/cli.js"
+      }
+    }
+  }
+}

+ 28 - 0
package.json

@@ -0,0 +1,28 @@
+{
+  "name": "linwit-ai-graph",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "@antv/g6": "5.1.0",
+    "@arco-design/web-vue": "2.58.0",
+    "@vue-office/pptx": "^1.0.1",
+    "docx": "^9.7.1",
+    "file-saver": "^2.0.5",
+    "neo4j-driver": "^6.0.1",
+    "pinia": "3.0.4",
+    "vue": "3.5.32",
+    "vue-demi": "^0.14.10",
+    "vue-router": "4.6.4",
+    "wa-sqlite": "^1.0.0"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-vue": "6.0.5",
+    "vite": "8.0.4"
+  }
+}

+ 7 - 0
public/config.js

@@ -0,0 +1,7 @@
+window.APP_CONFIG = {
+  neo4j: {
+    url: 'neo4j://192.168.20.70:7687',
+    username: 'neo4j',
+    password: 'psw123456'
+  }
+};

Plik diff jest za duży
+ 0 - 0
public/favicon.svg


+ 24 - 0
public/icons.svg

@@ -0,0 +1,24 @@
+<svg xmlns="http://www.w3.org/2000/svg">
+  <symbol id="bluesky-icon" viewBox="0 0 16 17">
+    <g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
+    <defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
+  </symbol>
+  <symbol id="discord-icon" viewBox="0 0 20 19">
+    <path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
+  </symbol>
+  <symbol id="documentation-icon" viewBox="0 0 21 20">
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
+  </symbol>
+  <symbol id="github-icon" viewBox="0 0 19 19">
+    <path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
+  </symbol>
+  <symbol id="social-icon" viewBox="0 0 20 20">
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
+  </symbol>
+  <symbol id="x-icon" viewBox="0 0 19 19">
+    <path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
+  </symbol>
+</svg>

+ 13 - 0
src/App.vue

@@ -0,0 +1,13 @@
+<template>
+  <keep-alive>
+    <router-view />
+  </keep-alive>
+</template>
+<script setup>
+import { onMounted } from 'vue';
+// import DevicePixelRatio from './utils/devicePixelRatio'
+
+onMounted(() => {
+  // new DevicePixelRatio().init()
+});
+</script>

+ 1 - 0
src/assets/fit.svg

@@ -0,0 +1 @@
+<svg t="1783321433527" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1755" width="200" height="200"><path d="M921.6 64c20.48 0 38.4 17.92 38.4 38.4v819.2c0 20.48-17.92 38.4-38.4 38.4H102.4c-20.48 0-38.4-17.92-38.4-38.4V102.4c0-20.48 17.92-38.4 38.4-38.4h819.2z m-38.4 76.8H140.8v742.4h742.4V140.8z m-640 478.72c17.92 0 33.28 15.36 33.28 33.28v94.72H371.2c17.92 0 33.28 15.36 33.28 33.28s-15.36 33.28-33.28 33.28h-128a33.92 33.92 0 0 1-33.28-33.28v-128c0-17.92 15.36-33.28 33.28-33.28z m537.6 0c17.92 0 33.28 15.36 33.28 33.28v128c0 17.92-15.36 33.28-33.28 33.28h-128a33.92 33.92 0 0 1-33.28-33.28c0-17.92 15.36-33.28 33.28-33.28h94.72V652.8c0-17.92 15.36-33.28 33.28-33.28z m0-409.6c17.92 0 33.28 15.36 33.28 33.28v128c0 17.92-15.36 33.28-33.28 33.28a33.92 33.92 0 0 1-33.28-33.28V276.48H652.8a33.92 33.92 0 0 1-33.28-33.28c0-17.92 15.36-33.28 33.28-33.28h128z m-409.6 0c17.92 0 33.28 15.36 33.28 33.28s-15.36 33.28-33.28 33.28H276.48V371.2c0 17.92-15.36 33.28-33.28 33.28a33.92 33.92 0 0 1-33.28-33.28v-128c0-17.92 15.36-33.28 33.28-33.28h128z" fill="#595959" p-id="1756"></path></svg>

BIN
src/assets/hero.png


Plik diff jest za duży
+ 0 - 0
src/assets/ppt.svg


Plik diff jest za duży
+ 0 - 0
src/assets/video.svg


Plik diff jest za duży
+ 0 - 0
src/assets/vite.svg


+ 1 - 0
src/assets/vue.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

+ 22 - 0
src/base/store.js

@@ -0,0 +1,22 @@
+import { defineStore } from 'pinia';
+
+
+/**
+ * 图谱模式
+ */
+export const useModeStore = defineStore('mode', {
+  state: () => {
+    return {
+      // value: THEME_GREEN
+      value: 'analysis'
+    };
+  },
+  actions: {
+    updateMode(mode) {
+      this.value = mode;
+    }
+  },
+  getters: {
+    getMode: (state) => state.value
+  }
+});

+ 0 - 0
src/components/graph-view/WordSource.vue


+ 100 - 0
src/layout/APPLayout.vue

@@ -0,0 +1,100 @@
+<!-- APPLayoutNew.vue -->
+<template>
+  <div class="layout-graph">
+    <HeaderView />
+    <div class="graph-body">
+      <!-- 智能组卷模式:侧边栏 + 画布 -->
+      <template v-if="target_mode === 'compose'">
+        <!-- <div class="compose-sidebar"> -->
+          <ComposeConfig 
+            :selectedNodes="composeSelectedNodes" 
+            @remove-node="handleRemoveNode"
+          />
+        <!-- </div> -->
+        <div class="graph-content">
+          <GraphView 
+            ref="graphViewRef"
+            @update-selected="handleUpdateSelected"
+          />
+        </div>
+      </template>
+      <!-- 其他模式:只有画布 -->
+      <template v-else>
+        <GraphView />
+      </template>
+    </div>
+  </div>
+</template>
+
+<script setup lang="js">
+import { computed, ref, watch } from 'vue';
+import { useModeStore } from '../base/store';
+import HeaderView from '../views/graph-view/HeaderView.vue'
+import GraphView from '../views/graph-view/GraphView.vue'
+import ComposeConfig from '../views/graph-view/ComposeConfig.vue'
+
+const modeStore = useModeStore();
+const target_mode = computed(() => modeStore.getMode);
+
+// 引用 GraphView 组件
+const graphViewRef = ref(null);
+
+// 选中的节点数组
+const composeSelectedNodes = ref([]);
+
+// 处理选中更新
+const handleUpdateSelected = (nodes) => {
+  composeSelectedNodes.value = nodes;
+};
+
+// 处理删除节点
+const handleRemoveNode = (nodeId) => {
+  // 通知 GraphView 取消选中状态
+  if (graphViewRef.value && graphViewRef.value.handleExternalRemove) {
+    graphViewRef.value.handleExternalRemove(nodeId);
+  }
+  // 同时更新本地数组
+  const index = composeSelectedNodes.value.findIndex(n => n.id === nodeId);
+  if (index > -1) {
+    composeSelectedNodes.value.splice(index, 1);
+  }
+};
+
+// 监听模式变化,切换出题模式和智能分析模式时清空选中节点
+watch(target_mode, (newMode, oldMode) => {
+  if ((newMode === 'compose' && oldMode === 'analysis') || 
+      (newMode === 'analysis' && oldMode === 'compose')) {
+    composeSelectedNodes.value = [];
+  }
+});
+</script>
+
+<style scoped>
+.layout-graph {
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+  width: 100%;
+  background: #f5f5f5;
+}
+
+.graph-body {
+  flex: 1;
+  overflow: hidden;
+  display: flex;
+}
+
+/* 智能组卷模式的布局 */
+.compose-sidebar {
+  width: 420px;
+  flex-shrink: 0;
+  background: #ffffff;
+  border-right: 1px solid #e5e6eb;
+  overflow-y: auto;
+}
+
+.graph-content {
+  flex: 1;
+  overflow: hidden;
+}
+</style>

+ 32 - 0
src/main.js

@@ -0,0 +1,32 @@
+/*
+ * @Author: 韩洋 1528276425@qq.com
+ * @Date: 2026-04-17 15:07:01
+ * @LastEditors: 韩洋 1528276425@qq.com
+ * @LastEditTime: 2026-04-17 15:38:46
+ * @FilePath: \linwit-ai-graph\src\main.js
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+ */
+import { createApp } from 'vue'
+import { createPinia } from 'pinia'
+import "./reset.css"
+import './style.css'
+import App from './App.vue'
+import ArcoVue from '@arco-design/web-vue'
+import ArcoVueIcon from '@arco-design/web-vue/es/icon';
+import '@arco-design/web-vue/dist/arco.css'
+import Neo4jPlugin from './plugins/neo4j.js'
+import router from './router'  // 新增:导入路由
+
+const app = createApp(App)
+const pinia = createPinia()
+
+const neo4jConfig = window.APP_CONFIG?.neo4j 
+
+app.use(Neo4jPlugin, neo4jConfig);
+app.use(ArcoVue)
+app.use(ArcoVueIcon)
+
+app.use(router)  // 新增:注册路由
+app.use(pinia) 
+
+app.mount('#app')

+ 54 - 0
src/mock/graph.js

@@ -0,0 +1,54 @@
+export const mockGraphData = {
+  nodes: [
+    { id: 'u:admin', label: '管理员', group: 'person' },
+    { id: 'u:dev_01', label: '前端开发', group: 'person' },
+    { id: 'o:linwit', label: 'Linwit科技', group: 'org' },
+    { id: 'o:antv', label: 'AntV团队', group: 'org' },
+    { id: 'o:alibaba', label: '阿里巴巴', group: 'org' },
+    { id: 't:g6', label: 'AntV G6', group: 'tech' },
+    { id: 't:x6', label: 'AntV X6', group: 'tech' },
+    { id: 't:vue', label: 'Vue 3', group: 'tech' },
+    { id: 't:react', label: 'React 18', group: 'tech' },
+    { id: 't:ts', label: 'TypeScript', group: 'tech' },
+    { id: 't:arco', label: 'Arco Design', group: 'tech' },
+    { id: 't:antd', label: 'Ant Design', group: 'tech' },
+    { id: 't:d3', label: 'D3.js', group: 'tech' },
+    { id: 't:vite', label: 'Vite', group: 'tool' },
+    { id: 'p:graph_editor', label: '图编辑器', group: 'project' },
+    { id: 'p:dashboard', label: '监控大屏', group: 'project' },
+    { id: 'c:visualization', label: '数据可视化', group: 'concept' },
+    { id: 'c:lowcode', label: '低代码', group: 'concept' },
+    { id: 'c:frontend', label: '前端工程', group: 'concept' },
+    { id: 'd:api', label: 'API文档', group: 'doc' },
+    { id: 'd:guide', label: '使用指南', group: 'doc' }
+  ],
+  edges: [
+    { source: 'o:alibaba', target: 'o:antv', label: '孵化' },
+    { source: 'o:linwit', target: 'o:antv', label: '战略合作' },
+    { source: 'u:admin', target: 'o:linwit', label: '任职' },
+    { source: 'u:dev_01', target: 'o:linwit', label: '任职' },
+    { source: 'u:dev_01', target: 'u:admin', label: '汇报' },
+    { source: 'p:graph_editor', target: 't:g6', label: '核心依赖' },
+    { source: 'p:graph_editor', target: 't:vue', label: '框架' },
+    { source: 'p:graph_editor', target: 't:ts', label: '语言' },
+    { source: 'p:graph_editor', target: 't:arco', label: 'UI库' },
+    { source: 'p:dashboard', target: 't:g6', label: '核心依赖' },
+    { source: 'p:dashboard', target: 't:react', label: '框架' },
+    { source: 'p:dashboard', target: 't:antd', label: 'UI库' },
+    { source: 't:g6', target: 't:d3', label: '底层参考' },
+    { source: 't:arco', target: 't:react', label: '基于' },
+    { source: 't:antd', target: 't:react', label: '基于' },
+    { source: 't:vite', target: 't:vue', label: '优化' },
+    { source: 't:vite', target: 't:ts', label: '支持' },
+    { source: 't:g6', target: 'c:visualization', label: '属于领域' },
+    { source: 'p:graph_editor', target: 'c:lowcode', label: '属于领域' },
+    { source: 'c:lowcode', target: 'c:frontend', label: '子集' },
+    { source: 'c:visualization', target: 'c:frontend', label: '子集' },
+    { source: 't:g6', target: 'd:api', label: '包含' },
+    { source: 't:g6', target: 'd:guide', label: '包含' },
+    { source: 'u:dev_01', target: 'd:guide', label: '编写' },
+    { source: 't:ts', target: 't:ts', label: '自类型检查' },
+    { source: 't:g6', target: 't:x6', label: '竞品/兄弟' },
+    { source: 't:x6', target: 't:g6', label: '竞品/兄弟' }
+  ]
+}

+ 106 - 0
src/mock/mockData.js

@@ -0,0 +1,106 @@
+/**
+ * 模拟相对完善的知识切片
+ */
+const generateChunks = (label, type) => {
+  const baseChunks = [
+    {
+      content: `[技术规约] 关于${label}的定义:在系统设计说明书v2.0中,${label}被定义为${type}层级的关键实体,负责处理核心业务逻辑流转。`,
+      source: "架构设计说明书.pdf"
+    },
+    {
+      content: `[运维记录] 2026-04-15:${label}节点完成高可用架构升级,当前冗余系数为 N+2。`,
+      source: "运维周报_Q2.docx"
+    }
+  ];
+
+  // 为特定类型的节点增加更多切片
+  if (type === 'server' || type === 'db_sub') {
+    baseChunks.push({
+      content: `[监控指标] ${label} 实时负载:CPU 15%, Memory 42%。连接池配置参数为 max_active=200, wait_timeout=30s。`,
+      source: "Prometheus_Export.log"
+    });
+    baseChunks.push({
+      content: `[故障预案] 若${label}发生不可恢复异常,系统将自动触发熔断,流量切换至备用可用区(AZ-2)。`,
+      source: "灾备手册_2026.pdf"
+    });
+  }
+
+  return baseChunks;
+};
+
+// 辅助函数:快速构建节点数据
+const createNode = (id, label, group, hasChildren = false) => ({
+  id,
+  label,
+  data: {
+    group,
+    hasChildren,
+    sourceChunks: generateChunks(label, group)
+  }
+});
+
+/**
+ * 初始展示的 1-2 级数据
+ */
+export const initialData = {
+  nodes: [
+    createNode("node1", "AI 核心引擎", "org"),
+    createNode("node2", "鉴权网关", "server"),
+    createNode("node3", "语义解析模块", "server"),
+    createNode("node4", "安全审计员", "person"),
+    createNode("node5", "数据持久层", "server", true), // 可展开
+    createNode("node10", "分布式缓存", "server", true), // 可展开
+  ],
+  edges: [
+    { source: "node2", target: "node1", label: "请求路由" },
+    { source: "node3", target: "node1", label: "语义输入" },
+    { source: "node4", target: "node1", label: "策略下发" },
+    { source: "node1", target: "node5", label: "IO 读写" },
+    { source: "node1", target: "node10", label: "缓存查询" },
+    { source: "node2", target: "node4", label: "鉴权上报" },
+  ],
+};
+
+/**
+ * 待扩展的详细 3 级数据
+ */
+export const detailData = {
+  // 数据库展开
+  node5: {
+    nodes: [
+      createNode("db_master", "PostgreSQL 主库", "db_sub", true),
+      createNode("db_slave", "ReadOnly 从库", "db_sub"),
+      createNode("db_vector", "Milvus 向量库", "db_sub"),
+    ],
+    edges: [
+      { source: "node5", target: "db_master", label: "主节点连接" },
+      { source: "node5", target: "db_slave", label: "只读分离" },
+      { source: "node5", target: "db_vector", label: "向量存储" },
+      { source: "db_master", target: "db_slave", label: "WAL 同步" },
+    ],
+  },
+  // 缓存展开
+  node10: {
+    nodes: [
+      createNode("redis_shard_1", "Redis 分片 01", "cache_sub"),
+      createNode("redis_shard_2", "Redis 分片 02", "cache_sub"),
+    ],
+    edges: [
+      { source: "node10", target: "redis_shard_1", label: "分片映射" },
+      { source: "node10", target: "redis_shard_2", label: "分片映射" },
+    ],
+  },
+  // 数据库主库进一步展开(监控信息)
+  db_master: {
+    nodes: [
+      createNode("mon_cpu", "CPU 占用率: 12%", "tech"),
+      createNode("mon_disk", "磁盘剩余: 850G", "tech"),
+      createNode("mon_tps", "当前 TPS: 1200", "tech"),
+    ],
+    edges: [
+      { source: "db_master", target: "mon_cpu", label: "健康度" },
+      { source: "db_master", target: "mon_disk", label: "容量" },
+      { source: "db_master", target: "mon_tps", label: "性能" },
+    ],
+  }
+};

+ 62 - 0
src/plugins/neo4j.js

@@ -0,0 +1,62 @@
+import neo4j from 'neo4j-driver';
+
+class Neo4jManager {
+  constructor(config) {
+    this.driver = neo4j.driver(
+      config.url,
+      neo4j.auth.basic(config.username, config.password)
+    );
+  }
+
+  // 统一的查询执行逻辑
+  async execute(cypher, params = {}, mode = 'read') {
+    const session = this.driver.session();
+    try {
+      const result = mode === 'write' 
+        ? await session.executeWrite(tx => tx.run(cypher, params))
+        : await session.executeRead(tx => tx.run(cypher, params));
+      
+      // 自动转换 Neo4j 特有的 Integer 类型
+      return result.records.map(record => this.parseRecord(record));
+    } finally {
+      await session.close();
+    }
+  }
+
+  // 解析记录,处理 Neo4j 数据类型转换
+  parseRecord(record) {
+    const data = {};
+    record.keys.forEach((key) => {
+      let value = record.get(key);
+      
+      // 处理 Neo4j 节点对象
+      if (value && typeof value === 'object' && value.properties) {
+        value = value.properties;
+      }
+      
+      // 递归处理数字精度(Integer)
+      data[key] = this.convertToNative(value);
+    });
+    return data;
+  }
+
+  convertToNative(value) {
+    if (neo4j.isInt(value)) return value.toNumber();
+    if (Array.isArray(value)) return value.map(v => this.convertToNative(v));
+    if (typeof value === 'object' && value !== null) {
+      return Object.fromEntries(
+        Object.entries(value).map(([k, v]) => [k, this.convertToNative(v)])
+      );
+    }
+    return value;
+  }
+}
+
+// Vue 插件导出
+export default {
+  install: (app, options) => {
+    const instance = new Neo4jManager(options);
+    app.provide('neo4j', instance);
+    app.config.globalProperties.$neo4j = instance;
+  }
+};

+ 60 - 0
src/reset.css

@@ -0,0 +1,60 @@
+/* 1. 全局盒模型设置:让 padding 不增加元素总宽度 */
+*,
+*::before,
+*::after {
+  box-sizing: border-box;
+}
+
+/* 2. 移除默认边距,提供平滑滚动 */
+* {
+  margin: 0;
+}
+
+html {
+  /* 优化移动端点击高亮 */
+  -webkit-tap-highlight-color: transparent;
+  /* 平滑滚动效果 */
+  scroll-behavior: smooth;
+}
+
+body {
+  /* 设置基础行高,防止文字太挤 */
+  line-height: 1.5;
+  /* 优化字体渲染 */
+  -webkit-font-smoothing: antialiased;
+  /* 防止 body 高度塌陷,确保占满视口 */
+  min-height: 100vh;
+}
+
+/* 3. 图片与媒体元素优化 */
+img, picture, video, canvas, svg {
+  display: block; /* 移除图片底部的幽灵空隙 */
+  max-width: 100%; /* 响应式图片,防止溢出 */
+}
+
+/* 4. 表单元素继承字体 */
+input, button, textarea, select {
+  font: inherit; /* 继承父元素字体大小和样式 */
+}
+
+/* 5. 文本排版优化 */
+p, h1, h2, h3, h4, h5, h6 {
+  overflow-wrap: break-word; /* 长单词自动换行,防止撑破布局 */
+}
+
+/* 6. 列表重置 */
+ul, ol {
+  list-style: none;
+}
+
+/* 7. 链接重置 */
+a {
+  text-decoration: none;
+  color: inherit;
+}
+
+/* 8. 表格重置 */
+table {
+  border-collapse: collapse;
+  border-spacing: 0;
+}

+ 20 - 0
src/router/index.js

@@ -0,0 +1,20 @@
+import { createRouter, createWebHistory,createWebHashHistory } from 'vue-router'
+
+const routes = [
+  {
+    path: '/',
+    name: 'home',
+    component: () => import('../layout/APPLayout.vue')
+  },
+  {
+    path: '/:pathMatch(.*)*',
+    redirect: '/'
+  }
+]
+
+const router = createRouter({
+  history: createWebHashHistory(),
+  routes
+})
+
+export default router

+ 302 - 0
src/style.css

@@ -0,0 +1,302 @@
+:root {
+  --text: #6b6375;
+  --text-h: #08060d;
+  --bg: #fff;
+  --panel: #fbfbfc;
+  --border: #e5e4e7;
+  --code-bg: #f4f3ec;
+  --accent: #aa3bff;
+  --accent-bg: rgba(170, 59, 255, 0.1);
+  --accent-border: rgba(170, 59, 255, 0.5);
+  --social-bg: rgba(244, 243, 236, 0.5);
+  --shadow:
+    rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
+
+  --sans: system-ui, 'Segoe UI', Roboto, sans-serif;
+  --heading: system-ui, 'Segoe UI', Roboto, sans-serif;
+  --mono: ui-monospace, Consolas, monospace;
+
+  font: 18px/145% var(--sans);
+  letter-spacing: 0.18px;
+  color-scheme: light dark;
+  color: var(--text);
+  background: var(--bg);
+  font-synthesis: none;
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+
+  @media (max-width: 1024px) {
+    font-size: 16px;
+  }
+}
+
+@media (prefers-color-scheme: dark) {
+  :root {
+    --text: #9ca3af;
+    --text-h: #f3f4f6;
+    --bg: #16171d;
+    --panel: #1b1d25;
+    --border: #2e303a;
+    --code-bg: #1f2028;
+    --accent: #c084fc;
+    --accent-bg: rgba(192, 132, 252, 0.15);
+    --accent-border: rgba(192, 132, 252, 0.5);
+    --social-bg: rgba(47, 48, 58, 0.5);
+    --shadow:
+      rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
+  }
+
+  #social .button-icon {
+    filter: invert(1) brightness(2);
+  }
+}
+
+body {
+  margin: 0;
+}
+
+h1,
+h2 {
+  font-family: var(--heading);
+  font-weight: 500;
+  color: var(--text-h);
+}
+
+h1 {
+  font-size: 56px;
+  letter-spacing: -1.68px;
+  margin: 32px 0;
+  @media (max-width: 1024px) {
+    font-size: 36px;
+    margin: 20px 0;
+  }
+}
+h2 {
+  font-size: 24px;
+  line-height: 118%;
+  letter-spacing: -0.24px;
+  margin: 0 0 8px;
+  @media (max-width: 1024px) {
+    font-size: 20px;
+  }
+}
+p {
+  margin: 0;
+}
+
+code,
+.counter {
+  font-family: var(--mono);
+  display: inline-flex;
+  border-radius: 4px;
+  color: var(--text-h);
+}
+
+code {
+  font-size: 15px;
+  line-height: 135%;
+  padding: 4px 8px;
+  background: var(--code-bg);
+}
+
+.counter {
+  font-size: 16px;
+  padding: 5px 10px;
+  border-radius: 5px;
+  color: var(--accent);
+  background: var(--accent-bg);
+  border: 2px solid transparent;
+  transition: border-color 0.3s;
+  margin-bottom: 24px;
+
+  &:hover {
+    border-color: var(--accent-border);
+  }
+  &:focus-visible {
+    outline: 2px solid var(--accent);
+    outline-offset: 2px;
+  }
+}
+
+.hero {
+  position: relative;
+
+  .base,
+  .framework,
+  .vite {
+    inset-inline: 0;
+    margin: 0 auto;
+  }
+
+  .base {
+    width: 170px;
+    position: relative;
+    z-index: 0;
+  }
+
+  .framework,
+  .vite {
+    position: absolute;
+  }
+
+  .framework {
+    z-index: 1;
+    top: 34px;
+    height: 28px;
+    transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
+      scale(1.4);
+  }
+
+  .vite {
+    z-index: 0;
+    top: 107px;
+    height: 26px;
+    width: auto;
+    transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
+      scale(0.8);
+  }
+}
+
+#app {
+  min-height: 100svh;
+  width: 100%;
+  background: radial-gradient(
+      1200px 600px at 10% 0%,
+      color-mix(in srgb, var(--accent) 8%, transparent),
+      transparent 60%
+    ),
+    radial-gradient(
+      1200px 600px at 90% 0%,
+      color-mix(in srgb, #1890ff 8%, transparent),
+      transparent 60%
+    ),
+    var(--bg);
+}
+
+#center {
+  display: flex;
+  flex-direction: column;
+  gap: 25px;
+  place-content: center;
+  place-items: center;
+  flex-grow: 1;
+
+  @media (max-width: 1024px) {
+    padding: 32px 20px 24px;
+    gap: 18px;
+  }
+}
+
+#next-steps {
+  display: flex;
+  border-top: 1px solid var(--border);
+  text-align: left;
+
+  & > div {
+    flex: 1 1 0;
+    padding: 32px;
+    @media (max-width: 1024px) {
+      padding: 24px 20px;
+    }
+  }
+
+  .icon {
+    margin-bottom: 16px;
+    width: 22px;
+    height: 22px;
+  }
+
+  @media (max-width: 1024px) {
+    flex-direction: column;
+    text-align: center;
+  }
+}
+
+#docs {
+  border-right: 1px solid var(--border);
+
+  @media (max-width: 1024px) {
+    border-right: none;
+    border-bottom: 1px solid var(--border);
+  }
+}
+
+#next-steps ul {
+  list-style: none;
+  padding: 0;
+  display: flex;
+  gap: 8px;
+  margin: 32px 0 0;
+
+  .logo {
+    height: 18px;
+  }
+
+  a {
+    color: var(--text-h);
+    font-size: 16px;
+    border-radius: 6px;
+    background: var(--social-bg);
+    display: flex;
+    padding: 6px 12px;
+    align-items: center;
+    gap: 8px;
+    text-decoration: none;
+    transition: box-shadow 0.3s;
+
+    &:hover {
+      box-shadow: var(--shadow);
+    }
+    .button-icon {
+      height: 18px;
+      width: 18px;
+    }
+  }
+
+  @media (max-width: 1024px) {
+    margin-top: 20px;
+    flex-wrap: wrap;
+    justify-content: center;
+
+    li {
+      flex: 1 1 calc(50% - 8px);
+    }
+
+    a {
+      width: 100%;
+      justify-content: center;
+      box-sizing: border-box;
+    }
+  }
+}
+
+#spacer {
+  height: 88px;
+  border-top: 1px solid var(--border);
+  @media (max-width: 1024px) {
+    height: 48px;
+  }
+}
+
+.ticks {
+  position: relative;
+  width: 100%;
+
+  &::before,
+  &::after {
+    content: '';
+    position: absolute;
+    top: -4.5px;
+    border: 5px solid transparent;
+  }
+
+  &::before {
+    left: 0;
+    border-left-color: var(--border);
+  }
+  &::after {
+    right: 0;
+    border-right-color: var(--border);
+  }
+}

+ 276 - 0
src/utils/fileStorage.js

@@ -0,0 +1,276 @@
+let opfsRoot = null;
+
+const DB_NAME = 'graph_files_db';
+const DB_VERSION = 2;
+const STORE_NAME = 'file_records';
+
+const openDB = () => {
+  return new Promise((resolve, reject) => {
+    const request = indexedDB.open(DB_NAME, DB_VERSION);
+    
+    request.onerror = () => reject(request.error);
+    
+    request.onsuccess = () => resolve(request.result);
+    
+    request.onupgradeneeded = (event) => {
+      const db = event.target.result;
+      if (!db.objectStoreNames.contains(STORE_NAME)) {
+        const store = db.createObjectStore(STORE_NAME, { keyPath: 'id', autoIncrement: true });
+        store.createIndex('node_id', 'node_id', { unique: false });
+        store.createIndex('file_type', 'file_type', { unique: false });
+        store.createIndex('node_type', ['node_id', 'file_type'], { unique: false });
+      } else {
+        const store = event.target.transaction.objectStore(STORE_NAME);
+        if (store.indexNames.contains('node_type')) {
+          store.deleteIndex('node_type');
+        }
+        store.createIndex('node_type', ['node_id', 'file_type'], { unique: false });
+      }
+    };
+  });
+};
+
+const saveToIndexedDB = async (record) => {
+  const db = await openDB();
+  return new Promise((resolve, reject) => {
+    const transaction = db.transaction([STORE_NAME], 'readwrite');
+    const store = transaction.objectStore(STORE_NAME);
+    const request = store.put(record);
+    request.onerror = () => reject(request.error);
+    request.onsuccess = () => resolve(request.result);
+  });
+};
+
+const getFromIndexedDB = async (nodeId, fileType) => {
+  const db = await openDB();
+  return new Promise((resolve, reject) => {
+    const transaction = db.transaction([STORE_NAME], 'readonly');
+    const store = transaction.objectStore(STORE_NAME);
+    const index = store.index('node_type');
+    const request = index.get([nodeId, fileType]);
+    request.onerror = () => reject(request.error);
+    request.onsuccess = () => resolve(request.result);
+  });
+};
+
+const getAllByNodeId = async (nodeId) => {
+  const db = await openDB();
+  return new Promise((resolve, reject) => {
+    const transaction = db.transaction([STORE_NAME], 'readonly');
+    const store = transaction.objectStore(STORE_NAME);
+    const index = store.index('node_id');
+    const request = index.getAll(nodeId);
+    request.onerror = () => reject(request.error);
+    request.onsuccess = () => resolve(request.result || []);
+  });
+};
+
+const deleteFromIndexedDB = async (nodeId, fileType) => {
+  const db = await openDB();
+  return new Promise((resolve, reject) => {
+    const transaction = db.transaction([STORE_NAME], 'readwrite');
+    const store = transaction.objectStore(STORE_NAME);
+    const index = store.index('node_type');
+    const getRequest = index.get([nodeId, fileType]);
+    
+    getRequest.onsuccess = () => {
+      const record = getRequest.result;
+      if (record) {
+        const deleteRequest = store.delete(record.id);
+        deleteRequest.onerror = () => reject(deleteRequest.error);
+        deleteRequest.onsuccess = () => resolve();
+      } else {
+        resolve();
+      }
+    };
+    
+    getRequest.onerror = () => reject(getRequest.error);
+  });
+};
+
+const deleteFromIndexedDBById = async (recordId) => {
+  const db = await openDB();
+  return new Promise((resolve, reject) => {
+    const transaction = db.transaction([STORE_NAME], 'readwrite');
+    const store = transaction.objectStore(STORE_NAME);
+    const deleteRequest = store.delete(recordId);
+    deleteRequest.onerror = () => reject(deleteRequest.error);
+    deleteRequest.onsuccess = () => resolve();
+  });
+};
+
+export const fileStorageService = {
+  async init() {
+    if (opfsRoot) return;
+    
+    try {
+      if ('storage' in navigator && 'getDirectory' in navigator.storage) {
+        opfsRoot = await navigator.storage.getDirectory();
+        console.log('OPFS initialized');
+      } else {
+        console.warn('OPFS not supported, falling back to memory storage');
+      }
+      
+      await openDB();
+      console.log('IndexedDB initialized');
+    } catch (error) {
+      console.error('Failed to initialize file storage:', error);
+      throw error;
+    }
+  },
+  
+  async saveFileToOPFS(file, nodeId) {
+    if (!opfsRoot) {
+      throw new Error('OPFS not supported');
+    }
+    
+    const fileType = file.name.toLowerCase().endsWith('.pptx') ? 'pptx' : 'video';
+    const timestamp = Date.now();
+    const ext = file.name.slice(file.name.lastIndexOf('.'));
+    const fileName = `${nodeId}_${fileType}_${timestamp}${ext}`;
+    
+    try {
+      const fileHandle = await opfsRoot.getFileHandle(fileName, { create: true });
+      const writable = await fileHandle.createWritable();
+      await writable.write(await file.arrayBuffer());
+      await writable.close();
+      
+      return {
+        fileName,
+        filePath: fileName,
+        fileType,
+        fileSize: file.size
+      };
+    } catch (error) {
+      console.error('Failed to save file to OPFS:', error);
+      throw error;
+    }
+  },
+  
+  async uploadFile(nodeId, file) {
+    await this.init();
+    
+    const fileType = file.name.toLowerCase().endsWith('.pptx') ? 'pptx' : 'video';
+    
+    const existingRecords = await this.getFileRecords(nodeId);
+    const hasDuplicateName = existingRecords.some(r => r.original_name === file.name);
+    if (hasDuplicateName) {
+      throw new Error(`文件 "${file.name}" 已存在`);
+    }
+    
+    let opfsResult;
+    if (opfsRoot) {
+      opfsResult = await this.saveFileToOPFS(file, nodeId);
+    } else {
+      opfsResult = {
+        fileName: file.name,
+        filePath: file.name,
+        fileType,
+        fileSize: file.size
+      };
+    }
+    
+    const now = new Date().toISOString();
+    const record = {
+      node_id: nodeId,
+      file_name: opfsResult.fileName,
+      original_name: file.name,
+      file_type: opfsResult.fileType,
+      file_path: opfsResult.filePath,
+      file_size: opfsResult.fileSize,
+      created_at: now,
+      updated_at: now
+    };
+    
+    await saveToIndexedDB(record);
+    console.log('File record saved:', record);
+    
+    return opfsResult;
+  },
+  
+  async getFileRecord(nodeId, fileType) {
+    await this.init();
+    return await getFromIndexedDB(nodeId, fileType);
+  },
+  
+  async getFileRecords(nodeId) {
+    await this.init();
+    return await getAllByNodeId(nodeId);
+  },
+  
+  async readFileFromOPFS(filePath) {
+    if (!opfsRoot) {
+      throw new Error('OPFS not supported');
+    }
+    
+    try {
+      const fileHandle = await opfsRoot.getFileHandle(filePath);
+      const file = await fileHandle.getFile();
+      return file;
+    } catch (error) {
+      console.error('Failed to read file from OPFS:', error);
+      throw error;
+    }
+  },
+  
+  async updateFileRecord(recordId, updates) {
+    await this.init();
+    const db = await openDB();
+    return new Promise((resolve, reject) => {
+      const transaction = db.transaction([STORE_NAME], 'readwrite');
+      const store = transaction.objectStore(STORE_NAME);
+      const getRequest = store.get(recordId);
+      
+      getRequest.onsuccess = () => {
+        const record = getRequest.result;
+        if (record) {
+          Object.assign(record, updates, { updated_at: new Date().toISOString() });
+          const putRequest = store.put(record);
+          putRequest.onerror = () => reject(putRequest.error);
+          putRequest.onsuccess = () => resolve(record);
+        } else {
+          reject(new Error('Record not found'));
+        }
+      };
+      
+      getRequest.onerror = () => reject(getRequest.error);
+    });
+  },
+  
+  async deleteFile(nodeId, fileType) {
+    await this.init();
+    
+    const record = await getFromIndexedDB(nodeId, fileType);
+    if (!record) return;
+    
+    try {
+      if (opfsRoot && record.file_path) {
+        await opfsRoot.removeEntry(record.file_path);
+      }
+      
+      await deleteFromIndexedDB(nodeId, fileType);
+      console.log('File deleted:', record);
+    } catch (error) {
+      console.error('Failed to delete file:', error);
+      throw error;
+    }
+  },
+  
+  async deleteFileById(record) {
+    await this.init();
+    
+    if (!record) return;
+    
+    try {
+      if (opfsRoot && record.file_path) {
+        await opfsRoot.removeEntry(record.file_path);
+      }
+      
+      await deleteFromIndexedDBById(record.id);
+      console.log('File deleted:', record);
+    } catch (error) {
+      console.error('Failed to delete file:', error);
+      throw error;
+    }
+  }
+};

+ 39 - 0
src/utils/util.js

@@ -0,0 +1,39 @@
+/**
+ * 根据指定字段对对象数组进行去重(默认保留先出现的项)
+ * @param {Array<Object>} array - 需要去重的对象数组
+ * @param {string} key - 依据去重的字段名(如 'id', 'name')
+ * @returns {Array<Object>} - 去重后的新数组
+ */
+export const uniqueByField = (array = [], key) => {
+    // 边界安全检查:如果不是数组,直接返回空数组
+    if (!Array.isArray(array)) return [];
+    // 如果没有传去重字段,直接返回原数组
+    if (!key) return [...array];
+
+    const seen = new Set();
+    
+    return array.filter(item => {
+        // 获取当前项对应字段的值
+        const fieldValue = item?.[key];
+        
+        // 如果该项没有这个字段,或者该值已经存在过,则过滤掉
+        if (fieldValue === undefined || seen.has(fieldValue)) {
+            return false;
+        }
+        
+        // 第一次出现,记录到 Set 中并保留
+        seen.add(fieldValue);
+        return true;
+    });
+};
+
+/**
+ * 时间格式化 xxxx-xx-xx xx:xx:xx 格式
+ * @param {string} timeStr - 时间字符串
+ * @returns {string} - 格式化后的时间字符串
+ */
+export const formatTime = (timeStr) => {
+    if (!timeStr) return '';
+    const date = new Date(timeStr);
+    return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
+}

+ 2 - 0
src/views/graph-view/1.js

@@ -0,0 +1,2 @@
+entity_names=["过程流程图","功能框图","质量管理标准","质量管理创新","Web系统","执行","实施","处理","检查","药物","竞争优势","财务战略","人力资源战略","帕累托最优","随机前沿分析","成本考核","新药物","新技术","按知分配","法律顾问","机制调整","体制改革","基本药物制度","社会资本办医","医疗保障体系","基层医疗机构","CT操作人员","大型医用设备管理","4E原则","信息平台","科研平台","医疗资源共享平台","人才后勤保障机制","人才梯队","SWOT法","科技兴院,特色兴院","发展规划","临床医学技术","技术开发","直接人工工资率差异","直接人工效率差异","间接费用的生产能力差异","间接费用效率差异","间接费用预算差异","专业风险管理人员","损后目标","损前目标","组织学习","管理创新理论","知识共享理论","战略规划理论","组织发展理论","医院信息科","公共卫生服务体系","网络化管理模式","面上项目","医院素质分析","技术技能优势","细胞治疗","医院信息管理能力","基因治疗","城市医疗集团","县域医共体","医疗救助制度","城乡居民基本医疗保险","城镇职工基本医疗保险","AI智能辅诊系统","直接材料数量差异","配置理论","设备维护保养的信息化平台","卫生经济学原理","全生命周期管理流程","后起之秀","中青年技术骨干","医院学科战略规划","弹性预算","视频应用","设备管理","深圳罗湖医院集团","二级医院","村卫生室","县级医院","乡镇卫生院","筹资标准","新型农村合作医疗","城镇居民基本医疗保险","待遇保障","《社会保险法》","灵活就业人员","民办非企业单位","事业单位","机关","社会团体","企业","县域常见病诊疗","大病保险","社会力量举办医院"]
+question_types=["填空题","选择题","判断题","简答题","多选题"]

+ 1587 - 0
src/views/graph-view/ComposeConfig.vue

@@ -0,0 +1,1587 @@
+<template>
+  <div class="compose-config">
+    <!-- 头部标题 -->
+    <div class="config-header">
+      <!-- <icon-pencil class="header-icon" /> -->
+      <span class="header-title">智能组卷配置面板</span>
+    </div>
+
+    <div class="config-content">
+      <!-- 1. 已选知识点 -->
+      <div class="section">
+        <div class="section-header">
+          <icon-check-square class="section-icon" />
+          <span class="section-title"
+            >已选知识点({{ selectedNodes.length }}个)</span
+          >
+        </div>
+
+        <div class="knowledge-list">
+          <div
+            v-for="item in selectedNodes"
+            :key="item.id"
+            class="knowledge-item"
+          >
+            <div
+              class="knowledge-color"
+              :style="{ backgroundColor: item.data.color || '#3B82F6' }"
+            ></div>
+            <span class="knowledge-name">{{ item.data.text || item.id }}</span>
+            <a-checkbox
+              v-model="item.includeChildren"
+              class="knowledge-checkbox"
+            >
+              包含子节点
+            </a-checkbox>
+            <a-button
+              type="text"
+              size="small"
+              class="remove-btn"
+              @click="handleRemoveNode(item.id)"
+            >
+              <icon-close />
+            </a-button>
+          </div>
+          <div class="empty" v-if="!selectedNodes.length">
+            <a-empty description="在图谱中点击节点添加到已选知识点"></a-empty>
+          </div>
+        </div>
+      </div>
+
+      <!-- 2. 题型筛选 -->
+      <div class="section">
+        <div class="section-header">
+          <icon-filter class="section-icon" />
+          <span class="section-title">题型筛选</span>
+        </div>
+
+        <div class="question-types">
+          <a-checkbox
+            :model-value="isAllSelected"
+            :indeterminate="isIndeterminate"
+            class="type-checkbox"
+            @change="handleSelectAll"
+          >
+            全选
+          </a-checkbox>
+
+          <a-checkbox
+            v-for="(type, index) in questionTypes"
+            :key="index"
+            v-model="type.selected"
+            class="type-checkbox"
+          >
+            {{ type.label }}
+          </a-checkbox>
+        </div>
+      </div>
+
+      <!-- 3. 组卷设置 -->
+      <div class="section">
+        <div class="section-header">
+          <icon-settings class="section-icon" />
+          <span class="section-title">组卷设置</span>
+        </div>
+
+        <div class="form-items">
+          <div class="form-row">
+            <span class="item">试卷名称:</span>
+            <a-input
+              v-model="paperConfig.name"
+              placeholder="请输入试卷名称"
+              class="form-input"
+            >
+              <template #prefix>
+                <icon-file />
+              </template>
+            </a-input>
+          </div>
+          <div class="form-row">
+            <span class="item">参考答案:</span>
+            <a-switch v-model="paperConfig.includeAnswers"></a-switch>
+          </div>
+          <div class="form-row">
+            <span class="item">单选题:</span
+            ><a-input-number
+              v-model="paperConfig.choiceCount"
+              :min="0"
+              class="form-input-number"
+            >
+              <template #prefix>
+                <icon-check-circle />
+              </template>
+            </a-input-number>
+            <span class="input-hint">道</span>
+            <span class="input-hint"
+              >(共{{ choiceQuestions.length }}道)</span
+            >
+          </div>
+          <div class="form-row">
+            <span class="item">多选题:</span
+            ><a-input-number
+              v-model="paperConfig.multiChoiceCount"
+              :min="0"
+              class="form-input-number"
+            >
+              <template #prefix>
+                <icon-check-circle />
+              </template>
+            </a-input-number>
+            <span class="input-hint">道</span>
+            <span class="input-hint"
+              >(共{{ multiChoiceQuestions.length }}道)</span
+            >
+          </div>
+          <div class="form-row">
+            <span class="item">判断题:</span
+            ><a-input-number
+              v-model="paperConfig.judgeCount"
+              :min="0"
+              class="form-input-number"
+            >
+              <template #prefix>
+                <icon-check-circle />
+              </template>
+            </a-input-number>
+            <span class="input-hint">道</span>
+            <span class="input-hint"
+              >(共{{ judgeQuestions.length }}道)</span
+            >
+          </div>
+          <div class="form-row">
+            <span class="item">简答题:</span
+            ><a-input-number
+              v-model="paperConfig.answerCount"
+              :min="0"
+              class="form-input-number"
+            >
+              <template #prefix>
+                <icon-check-circle />
+              </template>
+            </a-input-number>
+            <span class="input-hint">道</span>
+            <span class="input-hint">(共{{ answerQuestions.length }}道)</span>
+          </div>
+          <div class="form-row">
+            <span class="item">案例分析题:</span
+            ><a-input-number
+              v-model="paperConfig.caseAnalysisQuestionCount"
+              :min="0"
+              class="form-input-number"
+            >
+              <template #prefix>
+                <icon-check-circle />
+              </template>
+            </a-input-number>
+            <span class="input-hint">道</span>
+            <span class="input-hint"
+              >(共{{ caseAnalysisQuestionQuestions.length }}道)</span
+            >
+          </div>
+        </div>
+      </div>
+
+      <!-- 操作按钮 -->
+      <div class="action-buttons">
+        <a-button
+          type="primary"
+          size="large"
+          class="generate-btn"
+          v-if="!is_loading"
+          @click="handleGenerate"
+        >
+          <icon-send /> 开始自动生成试卷
+        </a-button>
+        <a-button
+          type="primary"
+          size="large"
+          class="generate-btn"
+          v-if="is_loading"
+          :loading="is_loading"
+        >
+          正在自动生成试卷
+        </a-button>
+
+        <a-button
+          size="large"
+          class="export-btn"
+          @click="exportToDocx"
+          :disabled="totalQuestions === 0"
+        >
+          <icon-download /> 导出为Word文档
+        </a-button>
+      </div>
+      <!-- 出题预览与修改 -->
+      <div class="section">
+        <div class="section-header">
+          <icon-file class="section-icon" />
+          <span class="section-title"
+            >预览与修改(共{{ totalQuestions }}题)</span
+          >
+        </div>
+        <div class="preview-container">
+          <div v-if="totalQuestions === 0" class="empty-preview">
+            <a-empty description="点击生成按钮生成试卷" />
+          </div>
+
+          <div v-else class="question-list">
+            <!-- 选择题 -->
+            <template v-if="targetChoiceQuestions.length > 0">
+              <div class="question-type-header">
+                <span
+                  >{{ getQuestionPrefix("选择题") }}选择题(共{{
+                    targetChoiceQuestions.length
+                  }}题)</span
+                >
+              </div>
+              <template v-for="(question, index) in targetChoiceQuestions" :key="question.question_id">
+                <transition name="card-slide">
+                  <a-card
+                    v-if="changingQuestion['选择题'] !== index"
+                    class="question-card"
+                    :title="`${index + 1}、${question.entity_name}`"
+                  >
+                    <div class="question-content">
+                      <div class="question-body">
+                        <span class="question-text">{{ question.question }}</span>
+                        <div class="options-list">
+                          <div
+                            v-for="(text, label) in question.options"
+                            :key="label"
+                            class="option-item"
+                          >
+                            <span class="option-label">{{ label }}</span>
+                            <span class="option-text">{{ text }}</span>
+                          </div>
+                        </div>
+                        <div
+                          class="answer-section"
+                          v-if="paperConfig.includeAnswers"
+                        >
+                          <span class="answer-label">参考答案:</span>
+                          <span class="answer-value">{{ question.answer }}</span>
+                        </div>
+                      </div>
+                      <div class="question-actions">
+                        <a-button
+                          v-if="canChangeQuestion('选择题')"
+                          type="text"
+                          size="small"
+                          @click="handleChangeQuestion(index, '选择题')"
+                        >换一题</a-button>
+                        <a-button
+                          type="text"
+                          size="small"
+                          @click="handleEditQuestion(question, '选择题', index)"
+                          >修改</a-button
+                        >
+                        <a-button
+                          type="text"
+                          size="small"
+                          status="danger"
+                          @click="handleDeleteQuestion(question.question_id, '选择题')"
+                          >删除</a-button
+                        >
+                      </div>
+                    </div>
+                  </a-card>
+                </transition>
+              </template>
+            </template>
+
+            <!-- 多选题 -->
+            <template v-if="targetMultiChoiceQuestions.length > 0">
+              <div class="question-type-header">
+                <span
+                  >{{ getQuestionPrefix("多选题") }}多选题(共{{
+                    targetMultiChoiceQuestions.length
+                  }}题)</span
+                >
+              </div>
+              <template v-for="(question, index) in targetMultiChoiceQuestions" :key="question.question_id">
+                <transition name="card-slide">
+                  <a-card
+                    v-if="changingQuestion['多选题'] !== index"
+                    class="question-card"
+                    :title="`${index + 1}、${question.entity_name}`"
+                  >
+                    <div class="question-content">
+                      <div class="question-body">
+                        <span class="question-text">{{ question.question }}</span>
+                        <div class="options-list">
+                          <div
+                            v-for="(text, label) in question.options"
+                            :key="label"
+                            class="option-item"
+                          >
+                            <span class="option-label">{{ label }}</span>
+                            <span class="option-text">{{ text }}</span>
+                          </div>
+                        </div>
+                        <div
+                          class="answer-section"
+                          v-if="paperConfig.includeAnswers"
+                        >
+                          <span class="answer-label">参考答案:</span>
+                          <span class="answer-value">{{
+                            Array.isArray(question.answer)
+                              ? question.answer.join("、")
+                              : question.answer
+                          }}</span>
+                        </div>
+                      </div>
+                      <div class="question-actions">
+                        <a-button
+                          v-if="canChangeQuestion('多选题')"
+                          type="text"
+                          size="small"
+                          @click="handleChangeQuestion(index, '多选题')"
+                        >换一题</a-button>
+                        <a-button
+                          type="text"
+                          size="small"
+                          @click="handleEditQuestion(question, '多选题', index)"
+                          >修改</a-button
+                        >
+                        <a-button
+                          type="text"
+                          size="small"
+                          status="danger"
+                          @click="handleDeleteQuestion(question.question_id, '多选题')"
+                          >删除</a-button
+                        >
+                      </div>
+                    </div>
+                  </a-card>
+                </transition>
+              </template>
+            </template>
+
+            <!-- 判断题 -->
+            <template v-if="targetJudgeQuestions.length > 0">
+              <div class="question-type-header">
+                <span
+                  >{{ getQuestionPrefix("判断题") }}判断题(共{{
+                    targetJudgeQuestions.length
+                  }}题)</span
+                >
+              </div>
+              <template v-for="(question, index) in targetJudgeQuestions" :key="question.question_id">
+                <transition name="card-slide">
+                  <a-card
+                    v-if="changingQuestion['判断题'] !== index"
+                    class="question-card"
+                    :title="`${index + 1}、${question.entity_name}`"
+                  >
+                    <div class="question-content">
+                      <div class="question-body">
+                        <span class="question-text">{{ question.question }}</span>
+                        <div
+                          class="answer-section"
+                          v-if="paperConfig.includeAnswers"
+                        >
+                          <span class="answer-label">参考答案:</span>
+                          <span class="answer-value">{{ question.answer }}</span>
+                        </div>
+                      </div>
+                      <div class="question-actions">
+                        <a-button
+                          v-if="canChangeQuestion('判断题')"
+                          type="text"
+                          size="small"
+                          @click="handleChangeQuestion(index, '判断题')"
+                        >换一题</a-button>
+                        <a-button
+                          type="text"
+                          size="small"
+                          @click="handleEditQuestion(question, '判断题', index)"
+                          >修改</a-button
+                        >
+                        <a-button
+                          type="text"
+                          size="small"
+                          status="danger"
+                          @click="handleDeleteQuestion(question.question_id, '判断题')"
+                          >删除</a-button
+                        >
+                      </div>
+                    </div>
+                  </a-card>
+                </transition>
+              </template>
+            </template>
+
+            <!-- 简答题 -->
+            <template v-if="targetAnswerQuestions.length > 0">
+              <div class="question-type-header">
+                <span
+                  >{{ getQuestionPrefix("简答题") }}简答题(共{{
+                    targetAnswerQuestions.length
+                  }}题)</span
+                >
+              </div>
+              <template v-for="(question, index) in targetAnswerQuestions" :key="question.question_id">
+                <transition name="card-slide">
+                  <a-card
+                    v-if="changingQuestion['简答题'] !== index"
+                    class="question-card"
+                    :title="`${index + 1}、${question.entity_name}`"
+                  >
+                    <div class="question-content">
+                      <div class="question-body">
+                        <span class="question-text">{{ question.question }}</span>
+                        <div
+                          class="answer-section answer-long"
+                          v-if="paperConfig.includeAnswers"
+                        >
+                          <span class="answer-label">参考答案:</span>
+                          <div class="answer-text">{{ question.answer }}</div>
+                        </div>
+                      </div>
+                      <div class="question-actions">
+                        <a-button
+                          v-if="canChangeQuestion('简答题')"
+                          type="text"
+                          size="small"
+                          @click="handleChangeQuestion(index, '简答题')"
+                        >换一题</a-button>
+                        <a-button
+                          type="text"
+                          size="small"
+                          @click="handleEditQuestion(question, '简答题', index)"
+                          >修改</a-button
+                        >
+                        <a-button
+                          type="text"
+                          size="small"
+                          status="danger"
+                          @click="handleDeleteQuestion(question.question_id, '简答题')"
+                          >删除</a-button
+                        >
+                      </div>
+                    </div>
+                  </a-card>
+                </transition>
+              </template>
+            </template>
+
+            <!-- 案例分析题 -->
+            <template v-if="targetCaseAnalysisQuestionQuestions.length > 0">
+              <div class="question-type-header">
+                <span
+                  >{{ getQuestionPrefix("案例分析题") }}案例分析题(共{{
+                    targetCaseAnalysisQuestionQuestions.length
+                  }}题)</span
+                >
+              </div>
+              <template v-for="(question, index) in targetCaseAnalysisQuestionQuestions" :key="question.question_id">
+                <transition name="card-slide">
+                  <a-card
+                    v-if="changingQuestion['案例分析题'] !== index"
+                    class="question-card"
+                    :title="`${index + 1}、${question.entity_name}`"
+                  >
+                    <div class="question-content">
+                      <div class="question-body">
+                        <span class="question-text">{{ question.question }}</span>
+                        <div
+                          class="answer-section answer-long"
+                          v-if="paperConfig.includeAnswers"
+                        >
+                          <span class="answer-label">参考答案:</span>
+                          <div class="answer-text">{{ question.answer }}</div>
+                        </div>
+                      </div>
+                      <div class="question-actions">
+                        <a-button
+                          v-if="canChangeQuestion('案例分析题')"
+                          type="text"
+                          size="small"
+                          @click="handleChangeQuestion(index, '案例分析题')"
+                        >换一题</a-button>
+                        <a-button
+                          type="text"
+                          size="small"
+                          @click="handleEditQuestion(question, '案例分析题', index)"
+                          >修改</a-button
+                        >
+                        <a-button
+                          type="text"
+                          size="small"
+                          status="danger"
+                          @click="handleDeleteQuestion(question.question_id, '案例分析题')"
+                          >删除</a-button
+                        >
+                      </div>
+                    </div>
+                  </a-card>
+                </transition>
+              </template>
+            </template>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+  <!-- 修改题目弹窗 -->
+  <a-modal
+    v-model:visible="editModalVisible"
+    title="修改题目"
+    @cancel="cancelEdit"
+    @ok="saveEditQuestion"
+    width="500px"
+  >
+    <div v-if="editingQuestion" class="edit-form">
+      <!-- 题目内容 -->
+      <div class="form-item">
+        <label class="form-label">题目内容:</label>
+        <a-textarea
+          v-model="editingQuestion.question"
+          placeholder="请输入题目内容"
+          :rows="3"
+          class="form-textarea"
+        />
+      </div>
+
+      <!-- 选项(单选/多选) -->
+      <div v-if="editingQuestion.options" class="form-item">
+        <label class="form-label">选项:</label>
+        <div class="options-edit">
+          <div
+            v-for="(text, label) in editingQuestion.options"
+            :key="label"
+            class="option-edit-row"
+          >
+            <span class="option-label">{{ label }}</span>
+            <a-input
+              v-model="editingQuestion.options[label]"
+              class="option-input"
+            />
+          </div>
+        </div>
+      </div>
+
+      <!-- 答案 -->
+      <div class="form-item">
+        <label class="form-label">答案:</label>
+        <a-textarea
+          v-model="editingQuestion.answer"
+          placeholder="请输入答案"
+          :rows="3"
+          class="form-textarea"
+        />
+      </div>
+    </div>
+  </a-modal>
+</template>
+
+<script setup lang="js">
+import { ref, reactive, watch, computed,inject } from 'vue';
+import { Document, Packer, Paragraph, TextRun, HeadingLevel, AlignmentType } from 'docx';
+import { saveAs } from 'file-saver';
+import { graphService } from './graph'
+import { Message, Modal } from '@arco-design/web-vue';
+const neo4j = inject('neo4j'); // 注入插件实例
+
+// 定义 props
+const props = defineProps({
+  selectedNodes: {
+    type: Array,
+    default: () => []
+  }
+});
+
+// 定义事件
+const emit = defineEmits(['remove-node']);
+
+// 处理删除节点
+const handleRemoveNode = (nodeId) => {
+  emit('remove-node', nodeId);
+};
+
+// 题型数据
+const questionTypes = ref([
+  { name: '选择题', label: '单选题', selected: true },
+  { name: '多选题', label: '多选题', selected: true },
+  { name: '判断题', label: '判断题', selected: true },
+  { name: '简答题', label: '简答题', selected: true },
+  { name: '案例分析题', label: '案例分析题', selected: true },
+
+]);
+
+// 加载状态
+const is_loading = ref(false)
+
+// 组卷配置
+const paperConfig = reactive({
+  name: '',
+  includeAnswers: true,
+  choiceCount: 20,
+  multiChoiceCount: 20,
+  judgeCount: 20,
+  answerCount: 20,
+  caseAnalysisQuestionCount: 20,
+  totalCount: 0,
+});
+
+// 是否全选
+const isAllSelected = computed(() => {
+  return questionTypes.value.length > 0 && questionTypes.value.every(type => type.selected);
+});
+
+// 是否半选(部分选中)
+const isIndeterminate = computed(() => {
+  const selectedCount = questionTypes.value.filter(type => type.selected).length;
+  return selectedCount > 0 && selectedCount < questionTypes.value.length;
+});
+
+// 处理全选
+const handleSelectAll = (checked) => {
+  questionTypes.value.forEach(type => {
+    type.selected = checked;
+  });
+};
+
+
+// 模拟题目数据
+const generatedQuestions = ref([
+  // 单选题
+  // {
+  //   question_id: 'q_80ee9d2057a6',
+  //   question_type: '单选题',
+  //   question: '在虚拟医院的星型模式中,起主导协调作用的组织被称为什么?',
+  //   options: {'A': '协调指挥委员会', 'B': '盟主', 'C': '核心枢纽', 'D': '战略伙伴'},
+  //   answer: 'B',
+  //   entity_name: '星型模式',
+  //   entity_type: '其他类[第六章]',
+  //   chapter: '第一部分_理念与战略_第六章 医院核心竞争力',
+  //   slice_id: '切片15',
+  //   explanation: '原文明确表述:一是星型模式(Star-like Mode)或有盟主的虚拟医院组织模式。这一模式一般由一个占主导地位的组织(盟主)和一些相对固定的伙伴组成。盟主负责协调和管理整个联盟的运作。',
+  //   source_text: '(二)拓宽虚拟功能\n所谓虚拟功能是指医院借助外部力量来改善自己的弱势部门的功能,使其与其他部门相配合,避免因局部功能弱化而影响和阻碍医院的发展。例如'
+  // },
+  // 名词解释
+  // {
+  //   question_id: 'q_2a683a9a0c93',
+  //   question_type: '名词解释',
+  //   question: '请解释"虚拟医院"',
+  //   answer: '虚拟医院是由2个以上的组织(医院、企业、院校等)为了共同的利益而组成的相互依赖、信任、合作的联盟,以便以最少的投资、最短的时间应对医疗市场的机遇做出快速反应。',
+  //   entity_name: '虚拟医院',
+  //   entity_type: '其他类[第六章]',
+  //   chapter: '第一部分_理念与战略_第六章 医院核心竞争力',
+  //   slice_id: '切片15',
+  //   explanation: '原文可归纳出完整定义:虚拟医院是由2个以上的组织(医院、企业、院校等)为了共同的利益而组成的相互依赖、信任、合作的联盟,以便以最少的投资、最短的时间应对医疗市场的机遇做出快速反应。',
+  //   source_text: '(二)拓宽虚拟功能\n所谓虚拟功能是指医院借助外部力量来改善自己的弱势部门的功能,使其与其他部门相配合,避免因局部功能弱化而影响和阻碍医院的发展。例如'
+  // },
+  // 多选题
+  // {
+  //   question_id: 'q_1048ff2e2968',
+  //   question_type: '多选题',
+  //   question: '根据原文,虚拟创新模式的典型特征包括哪些?',
+  //   options: {'A': '以信息技术实现快速组合', 'B': '具有医院边界和长期稳定性', 'C': '联盟基础是成员间技术互补性和核心竞争力', 'D': '目标实现后联盟即解体'},
+  //   answer: ['A', 'C', 'D'],
+  //   entity_name: '虚拟创新模式',
+  //   entity_type: '其他类[第六章]',
+  //   chapter: '第一部分_理念与战略_第六章 医院核心竞争力',
+  //   slice_id: '切片16',
+  //   explanation: '原文依据:(A)以信息技术实现快速组合;(C)联盟的基础在于医院间是否具有技术的互补性和核心竞争力;(D)目标实现后,联盟即解体;(B)错误,虚拟创新模式不具有长期稳定性。',
+  //   source_text: '3. 模仿创新模式 模仿创新是医院在率先创新的示范影响和利益诱导之下模仿创新者的创新思路和创新行为,引进和破译率先者的核心技术和技术秘密,然后通过利用'
+  // },
+  // 简答题
+  // {
+  //   question_id: 'q_a10b386f86e5',
+  //   question_type: '简答题',
+  //   question: '简述"坚持文化创新"的基本理由和要求。',
+  //   answer: '基本理由:医院文化不是一成不变的,它随医院发展而变化;医院的竞争是创新的竞争,其中包含医院文化的创新。基本要求:医院文化创建要有前瞻性眼光,着眼于未来和社会发展趋势。',
+  //   entity_name: '文化创新',
+  //   entity_type: '其他类[第六章]',
+  //   chapter: '第一部分_理念与战略_第六章 医院核心竞争力',
+  //   slice_id: '切片16',
+  //   explanation: '原文依据:医院文化不是一成不变的,它随着医院的发展而变化的……医院的竞争是创新的竞争,这种创新涉及医院各个方面,其中也包含医院文化的创新。',
+  //   source_text: '5. 坚决注重个性特质 个性特质或差异性是医院文化的一个重要特征,医院文化的活力就在于鲜明的个性。医院文化的创建切忌雷同,即与其他医院相似相识。'
+  // }
+]);
+
+/**
+ * 各类题目数量
+ * */ 
+// 选择题
+const choiceQuestions = computed(() => generatedQuestions.value.filter(item => item.question_type === '选择题'))
+// 多选题
+const multiChoiceQuestions = computed(() => generatedQuestions.value.filter(item => item.question_type === '多选题'))
+// 判断题
+const judgeQuestions = computed(() => generatedQuestions.value.filter(item => item.question_type === '判断题'))
+// 简答题
+const answerQuestions = computed(() => generatedQuestions.value.filter(item => item.question_type === '简答题'))
+// 案例分析题
+const caseAnalysisQuestionQuestions = computed(() => generatedQuestions.value.filter(item => item.question_type === '案例分析题'))
+
+/**
+ * 当前展示的每类题目数据
+ * */ 
+const targetChoiceQuestions = ref([])
+const targetMultiChoiceQuestions = ref([])
+const targetJudgeQuestions = ref([])
+const targetAnswerQuestions = ref([])
+const targetCaseAnalysisQuestionQuestions = ref([])
+
+// 换题动画状态
+const changingQuestion = reactive({
+  '选择题': null,
+  '多选题': null,
+  '判断题': null,
+  '简答题': null,
+  '案例分析题': null
+});
+
+// 总题数
+const totalQuestions = computed(() => 
+  targetChoiceQuestions.value.length + 
+  targetMultiChoiceQuestions.value.length + 
+  targetJudgeQuestions.value.length + 
+  targetAnswerQuestions.value.length + 
+  targetCaseAnalysisQuestionQuestions.value.length
+);
+
+const getQuestionPrefix = (type) => {
+  const typeOrder = ['选择题', '多选题', '判断题', '简答题', '案例分析题'];
+  const prefixList = ['(一)', '(二)', '(三)', '(四)', '(五)'];
+  
+  let activeIndex = 0;
+  for (let i = 0; i < typeOrder.length; i++) {
+    const currentType = typeOrder[i];
+    const targetArray = {
+      '选择题': targetChoiceQuestions,
+      '多选题': targetMultiChoiceQuestions,
+      '判断题': targetJudgeQuestions,
+      '简答题': targetAnswerQuestions,
+      '案例分析题': targetCaseAnalysisQuestionQuestions
+    }[currentType];
+    
+    if (targetArray && targetArray.value.length > 0) {
+      if (currentType === type) {
+        return prefixList[activeIndex];
+      }
+      activeIndex++;
+    }
+  }
+  return '';
+};
+
+// 判断是否可以换题
+const canChangeQuestion = (questionType) => {
+  const allQuestions = {
+    '选择题': choiceQuestions.value,
+    '多选题': multiChoiceQuestions.value,
+    '判断题': judgeQuestions.value,
+    '简答题': answerQuestions.value,
+    '案例分析题': caseAnalysisQuestionQuestions.value
+  };
+  
+  const targetQuestions = {
+    '选择题': targetChoiceQuestions.value,
+    '多选题': targetMultiChoiceQuestions.value,
+    '判断题': targetJudgeQuestions.value,
+    '简答题': targetAnswerQuestions.value,
+    '案例分析题': targetCaseAnalysisQuestionQuestions.value
+  };
+  
+  return allQuestions[questionType].length > targetQuestions[questionType].length;
+};
+
+// 换一题
+const handleChangeQuestion = (index, questionType) => {
+  if (changingQuestion[questionType] !== null) return;
+  
+  const allQuestions = {
+    '选择题': choiceQuestions.value,
+    '多选题': multiChoiceQuestions.value,
+    '判断题': judgeQuestions.value,
+    '简答题': answerQuestions.value,
+    '案例分析题': caseAnalysisQuestionQuestions.value
+  };
+
+  const targetQuestions = {
+    '选择题': targetChoiceQuestions.value,
+    '多选题': targetMultiChoiceQuestions.value,
+    '判断题': targetJudgeQuestions.value,
+    '简答题': targetAnswerQuestions.value,
+    '案例分析题': targetCaseAnalysisQuestionQuestions.value
+  };
+
+  // 获取当前展示的所有题目ID
+  const targetIds = new Set(targetQuestions[questionType].map(q => q.question_id));
+
+  // 找出未展示的题目
+  const availableQuestions = allQuestions[questionType].filter(q => !targetIds.has(q.question_id));
+
+  if (availableQuestions.length > 0) {
+    // 设置换题状态,触发离场动画
+    changingQuestion[questionType] = index;
+    
+    // 等待离场动画完成(300ms)
+    setTimeout(() => {
+      // 随机选择一道题
+      const randomIndex = Math.floor(Math.random() * availableQuestions.length);  
+      targetQuestions[questionType][index] = availableQuestions[randomIndex];     
+      
+      // 重置状态,触发入场动画
+      changingQuestion[questionType] = null;
+      Message.success('已更换题目');
+    }, 300);
+  }
+};
+
+// 获取选项标签
+const getOptionLabel = (index) => {
+  const labels = ['A', 'B', 'C', 'D', 'E', 'F'];
+  return labels[index] || '';
+};
+
+function randomPick(arr, count) {
+  const shuffled = [...arr].sort(() => Math.random() - 0.5);
+  return shuffled.slice(0, count);
+}
+
+// 获取所有的需要出题的节点
+const getNeedNode = async () => {
+  const entitys_names = props.selectedNodes.map(item=>item.data.text)
+  for(const item of props.selectedNodes){
+    const res = await graphService.getEntityNamesWithChildren(neo4j,item.data.text,item.includeChildren)
+      entitys_names.push(...res)
+  }
+  return Array.from(new Set(entitys_names))
+}
+// 查询题目
+const  getQuestions = async (entitys_names,question_types) =>{
+  // const entitys_names = props.selectedNodes.map(item=>item.data.text)
+  // const question_types =  questionTypes.value
+  // .filter(item => item.selected)
+  // .map(item => item.name);
+  // console.log(entitys_names,question_types,999)
+  return await graphService.findQuestionsNodesByNameAndType(neo4j,entitys_names,question_types)
+
+}
+
+function compareArrayById(arr1, arr2) {
+  // 1. 用Map存储arr2的id映射,查询效率O(1)
+  const map2 = new Map();
+  arr2.forEach(item => {
+    map2.set(item.id, item);
+  });
+
+  const same = [];    // id相同的项
+  const onlyA = [];   // 仅arr1存在
+  // 遍历arr1,拆分same和onlyA
+  arr1.forEach(item => {
+    if (map2.has(item.id)) {
+      same.push({ a: item, b: map2.get(item.id) });
+      map2.delete(item.id); // 已匹配,从map删除,剩下的就是onlyB
+    } else {
+      onlyA.push(item);
+    }
+  });
+
+  // map剩余内容 = 仅arr2存在
+  const onlyB = Array.from(map2.values());
+
+  return {
+    same,
+    onlyA,
+    onlyB
+  };
+}
+// 生成试卷
+const handleGenerate = async () => {
+
+  const d5 = await graphService.findNodesByQuestionType(neo4j, "简答题") // 395
+  console.log(d5)
+
+  is_loading.value = true
+  const entity_names = await getNeedNode()
+  const question_types = questionTypes.value
+  .filter(item => item.selected)
+  .map(item => item.name);
+  // console.log(JSON.stringify(entity_names))
+  const res = await getQuestions(entity_names,question_types)
+  // console.log(111,res)
+
+  const {same,onlyA,onlyB} = compareArrayById(res,d5)
+  console.log(same,onlyA,onlyB)
+  console.log(onlyB.filter(item=>item.entity_type!==''))
+  res.forEach(item =>{
+    if(item.options){
+      item.options = JSON.parse(item.options)
+    }
+  })
+  generatedQuestions.value = [...res]
+
+  const grouped = {
+    '选择题': generatedQuestions.value.filter(q => q.question_type === '选择题'),
+    '多选题': generatedQuestions.value.filter(q => q.question_type === '多选题'),
+    '判断题': generatedQuestions.value.filter(q => q.question_type === '判断题'),
+    '简答题': generatedQuestions.value.filter(q => q.question_type === '简答题'),
+    '案例分析题': generatedQuestions.value.filter(q => q.question_type === '案例分析题'),
+  };
+
+  if (paperConfig.choiceCount > grouped['选择题'].length) {
+    paperConfig.choiceCount = grouped['选择题'].length;
+  }
+  if (paperConfig.multiChoiceCount > grouped['多选题'].length) {
+    paperConfig.multiChoiceCount = grouped['多选题'].length;
+  }
+  if (paperConfig.judgeCount > grouped['判断题'].length) {
+    paperConfig.judgeCount = grouped['判断题'].length;
+  }
+  if (paperConfig.answerCount > grouped['简答题'].length) {
+    paperConfig.answerCount = grouped['简答题'].length;
+  }
+  if (paperConfig.caseAnalysisQuestionCount > grouped['案例分析题'].length) {
+    paperConfig.caseAnalysisQuestionCount = grouped['案例分析题'].length;
+  }
+
+  targetChoiceQuestions.value = randomPick(grouped['选择题'], paperConfig.choiceCount);
+  targetMultiChoiceQuestions.value = randomPick(grouped['多选题'], paperConfig.multiChoiceCount);
+  targetJudgeQuestions.value = randomPick(grouped['判断题'], paperConfig.judgeCount);
+  targetAnswerQuestions.value = randomPick(grouped['简答题'], paperConfig.answerCount);
+  targetCaseAnalysisQuestionQuestions.value = randomPick(grouped['案例分析题'], paperConfig.caseAnalysisQuestionCount);
+
+  is_loading.value = false
+};
+
+// 自动生成试卷
+const handleAuto = () => {
+  console.log('开始自动生成试卷:', {
+    knowledge: selectedKnowledge.value,
+    types: questionTypes.value.filter(t => t.selected),
+    config: paperConfig
+  });
+};
+
+// 删除题目
+const handleDeleteQuestion = (questionId, questionType) => {
+  const targetMap = {
+    '选择题': targetChoiceQuestions,
+    '多选题': targetMultiChoiceQuestions,
+    '判断题': targetJudgeQuestions,
+    '简答题': targetAnswerQuestions,
+    '案例分析题': targetCaseAnalysisQuestionQuestions
+  };
+  
+  const targetArray = targetMap[questionType];
+  if (!targetArray) return;
+  
+  const index = targetArray.value.findIndex(q => q.question_id === questionId);
+  if (index > -1) {
+    const question = targetArray.value[index];
+    Modal.confirm({
+      title: '确认删除',
+      content: `确定要删除题目"${question.question}"吗?`,
+      okText: '确定',
+      cancelText: '取消',
+      okButtonProps: { status: 'danger' },
+      onOk: () => {
+        targetArray.value.splice(index, 1);
+        Message.success('题目已删除');
+      },
+    });
+  }
+};
+
+// 修改题目弹窗状态
+const editModalVisible = ref(false);
+const editingQuestion = ref(null);
+const editingIndex = ref(-1);
+const editingType = ref('');
+
+// 修改题目
+const handleEditQuestion = (question, questionType, index) => {
+  editingQuestion.value = { ...question };
+  editingIndex.value = index;
+  editingType.value = questionType;
+  editModalVisible.value = true;
+};
+
+// 保存修改
+const saveEditQuestion = () => {
+  if (editingIndex.value > -1 && editingQuestion.value && editingType.value) {
+    const targetMap = {
+      '选择题': targetChoiceQuestions,
+      '多选题': targetMultiChoiceQuestions,
+      '判断题': targetJudgeQuestions,
+      '简答题': targetAnswerQuestions,
+      '案例分析题': targetCaseAnalysisQuestionQuestions
+    };
+    
+    const targetArray = targetMap[editingType.value];
+    if (targetArray) {
+      targetArray.value[editingIndex.value] = { ...editingQuestion.value };
+    }
+    
+    editModalVisible.value = false;
+    Message.success('题目已修改');
+  }
+};
+
+// 关闭修改弹窗
+const cancelEdit = () => {
+  editModalVisible.value = false;
+  editingQuestion.value = null;
+  editingIndex.value = -1;
+};
+
+// 导出为Word文档
+const exportToDocx = async () => {
+  if (totalQuestions.value === 0) {
+    return;
+  }
+  if(!paperConfig.name.trim().length){
+    return Message.warning('请填写试卷名称')
+  }
+
+  // 使用目标数组进行导出
+  const groupedQuestions = {
+    '选择题': targetChoiceQuestions.value,
+    '多选题': targetMultiChoiceQuestions.value,
+    '判断题': targetJudgeQuestions.value,
+    '简答题': targetAnswerQuestions.value,
+    '案例分析题': targetCaseAnalysisQuestionQuestions.value,
+  };
+
+  const paragraphs = [];
+
+  // 试卷标题
+  paragraphs.push(
+    new Paragraph({
+      children: [new TextRun({ text: paperConfig.name, bold: true, size: 32 })],
+      heading: HeadingLevel.HEADING_1,
+      alignment: AlignmentType.CENTER,
+      spacing: { after: 400 },
+    })
+  );
+
+  // 题型顺序
+  const typeOrder = ['选择题', '多选题', '判断题', '简答题', '案例分析题'];
+  const prefixList = ['(一)', '(二)', '(三)', '(四)', '(五)'];
+
+  let questionIndex = 1;
+  let prefixIndex = 0;
+
+  // 遍历每种题型
+  typeOrder.forEach(type => {
+    const questions = groupedQuestions[type];
+    if (questions && questions.length > 0) {
+      // 题型标题(包含前缀)
+      paragraphs.push(
+        new Paragraph({
+          children: [new TextRun({ text: `${prefixList[prefixIndex]}${type}(共${questions.length}题)`, bold: true, size: 24 })],
+          heading: HeadingLevel.HEADING_2,
+          spacing: { before: 300, after: 200 },
+        })
+      );
+      prefixIndex++;
+
+      // 每个题目
+      questions.forEach((question, idx) => {
+        // 题目序号和内容(使用新的数据结构)
+        paragraphs.push(
+          new Paragraph({
+            children: [
+              new TextRun({ text: `${questionIndex}、`, bold: true }),
+              new TextRun({ text: question.question })
+            ],
+            spacing: { before: 150, after: 100 },
+            indent: { firstLine: 420 },
+          })
+        );
+
+        // 选项(单选和多选)- 选项是对象格式
+        if (question.options && typeof question.options === 'object') {
+          Object.entries(question.options).forEach(([label, text]) => {
+            paragraphs.push(
+              new Paragraph({
+                children: [
+                  new TextRun({ text: `${label}. `, bold: true }),
+                  new TextRun({ text: text })
+                ],
+                indent: { firstLine: 840 },
+                spacing: { after: 50 },
+              })
+            );
+          });
+        }
+
+        // 参考答案(根据配置决定是否显示)
+        if (paperConfig.includeAnswers && question.answer !== undefined) {
+          let answerText = '';
+          
+          if (type === '简答题' || type === '名词解释' || type === '填空题') {
+            // 简答题、名词解释和填空题直接显示答案文本
+            answerText = question.answer;
+          } else if (Array.isArray(question.answer)) {
+            // 多选题答案是数组
+            answerText = question.answer.join('、');
+          } else {
+            // 单选题答案是字符串
+            answerText = question.answer;
+          }
+          
+          paragraphs.push(
+            new Paragraph({
+              children: [
+                new TextRun({ text: '【参考答案】', bold: true, color: 'FF0000' }),
+                new TextRun({ text: answerText, color: 'FF0000' })
+              ],
+              spacing: { before: 100, after: 150 },
+              indent: { firstLine: 420 },
+            })
+          );
+        }
+
+        questionIndex++;
+      });
+    }
+  });
+
+  // 创建文档
+  const doc = new Document({
+    sections: [{
+      properties: {},
+      children: paragraphs,
+    }],
+  });
+
+  // 生成并下载
+  const blob = await Packer.toBlob(doc);
+  const fileName = `${paperConfig.name}.docx`;
+  saveAs(blob, fileName);
+};
+watch(()=>questionTypes.value,()=>{
+  paperConfig.choiceCount=20
+  paperConfig.multiChoiceCount=20
+  paperConfig.judgeCount=20
+  paperConfig.answerCount=20
+  paperConfig.caseAnalysisQuestionCount=20
+
+},{
+  deep:true
+})
+watch(()=>props.selectedNodes,()=>{
+    paperConfig.choiceCount=20
+  paperConfig.multiChoiceCount=20
+  paperConfig.judgeCount=20
+  paperConfig.answerCount=20
+  paperConfig.caseAnalysisQuestionCount=20
+
+},{deep:true})
+</script>
+
+<style scoped lang="css">
+.compose-config {
+  position: absolute;
+  top: 80px;
+  right: 16px;
+  z-index: 20;
+  width: 520px;
+  height: calc(100vh - 100px);
+  background: #ffffff;
+  border-radius: 12px;
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+  overflow: hidden;
+}
+
+/* 头部 */
+.config-header {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  padding: 16px 20px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+}
+
+.header-icon {
+  color: #ffffff;
+  font-size: 18px;
+}
+
+.header-title {
+  color: #ffffff;
+  font-size: 15px;
+  font-weight: 600;
+}
+
+/* 内容区域 */
+.config-content {
+  padding: 16px 20px;
+  height: calc(100% - 74px);
+  overflow-x: auto;
+}
+
+/* 区块 */
+.section {
+  margin-bottom: 20px;
+  padding-bottom: 16px;
+  border-bottom: 1px dashed #e5e6eb;
+}
+
+.section:last-of-type {
+  border-bottom: none;
+}
+
+.section-header {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-bottom: 12px;
+}
+
+.section-icon {
+  color: #667eea;
+  font-size: 14px;
+}
+
+.section-title {
+  font-size: 14px;
+  font-weight: 600;
+  color: #1d2129;
+}
+
+/* 知识点列表 */
+.knowledge-list {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  max-height: 300px;
+  overflow-y: auto;
+}
+
+.knowledge-item {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  padding: 10px 12px;
+  background: #f7f8fa;
+  border-radius: 8px;
+  transition: all 0.2s ease;
+}
+
+.knowledge-item:hover {
+  background: #f0f1f5;
+}
+
+.knowledge-color {
+  width: 8px;
+  height: 24px;
+  border-radius: 4px;
+  flex-shrink: 0;
+}
+
+.knowledge-name {
+  flex: 1;
+  font-size: 13px;
+  color: #4e5969;
+}
+
+.knowledge-checkbox {
+  flex-shrink: 0;
+}
+
+.remove-btn {
+  color: #86909c;
+}
+
+.remove-btn:hover {
+  color: #f53f3f;
+}
+
+/* 题型筛选 */
+.question-types {
+  display: flex;
+  /* flex-direction: column; */
+  gap: 10px;
+}
+
+.type-checkbox {
+  font-size: 13px;
+  color: #4e5969;
+}
+
+/* 表单项目 */
+.form-items {
+  display: flex;
+  flex-direction: column;
+  gap: 14px;
+  padding: 8px 0;
+}
+
+.form-input {
+  flex: 1;
+  min-width: 0;
+}
+
+.form-row {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 4px 0;
+}
+
+.form-row .item {
+  width: 90px;
+  text-align: right;
+  font-size: 14px;
+  color: #4e5969;
+  flex-shrink: 0;
+  font-weight: 500;
+}
+
+.form-input-number {
+  width: 120px;
+  flex-shrink: 0;
+}
+
+.input-hint {
+  font-size: 12px;
+  color: #86909c;
+  flex-shrink: 0;
+}
+
+/* 操作按钮 */
+.action-buttons {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+  margin: 16px 0;
+}
+
+.generate-btn {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+}
+
+.auto-btn {
+  background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+  border: none;
+  color: #ffffff;
+}
+
+.auto-btn:hover {
+  background: linear-gradient(135deg, #ee76e2 0%, #f13d53 100%);
+}
+
+.export-btn {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
+  border: none;
+  color: #ffffff;
+}
+
+.export-btn:hover:not(:disabled) {
+  background: linear-gradient(135deg, #0e8074 0%, #2ed66d 100%);
+}
+
+.export-btn:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
+/* 预览区域 */
+.preview-container {
+  min-height: 200px;
+}
+
+.empty-preview {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 150px;
+}
+
+.question-list {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.question-type-header {
+  font-size: 16px;
+  font-weight: 600;
+  color: #1d2129;
+  padding: 8px 0;
+  border-bottom: 2px solid #667eea;
+  margin-top: 16px;
+}
+
+.question-type-header:first-child {
+  margin-top: 0;
+}
+
+.question-card {
+  border-radius: 8px;
+  border: 1px solid #e5e6eb;
+}
+
+/* 卡片中心弹出动画 */
+.card-slide-enter-active {
+  transition: all 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
+}
+
+.card-slide-leave-active {
+  transition: all 0.3s ease-out;
+}
+
+.card-slide-enter-from {
+  opacity: 0;
+  transform: scale(0.6) translateY(-20px);
+}
+
+.card-slide-leave-to {
+  opacity: 0;
+  transform: scale(0.95) translateY(10px);
+}
+
+.question-card .arco-card-title {
+  font-size: 14px;
+  font-weight: 600;
+  color: #1d2129;
+}
+
+.question-content {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.question-body {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+}
+
+.question-text {
+  font-size: 14px;
+  color: #4e5969;
+  line-height: 1.6;
+}
+
+.options-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+}
+
+.option-item {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 6px 12px;
+  background: #f7f8fa;
+  border-radius: 4px;
+}
+
+.option-label {
+  font-weight: 600;
+  color: #667eea;
+  font-size: 13px;
+}
+
+.option-text {
+  font-size: 13px;
+  color: #4e5969;
+}
+
+.answer-section {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 10px 12px;
+  background: #f0f5ff;
+  border-radius: 4px;
+}
+
+.answer-section.answer-long {
+  flex-direction: column;
+  align-items: flex-start;
+}
+
+.answer-label {
+  font-size: 13px;
+  color: #667eea;
+  font-weight: 500;
+}
+
+.answer-value {
+  font-size: 13px;
+  color: #1d2129;
+  font-weight: 500;
+}
+
+.answer-text {
+  font-size: 13px;
+  color: #4e5969;
+  line-height: 1.6;
+  margin-top: 4px;
+}
+
+.question-actions {
+  display: flex;
+  gap: 10px;
+  justify-content: flex-end;
+}
+
+.question-actions .arco-btn-text {
+  font-size: 12px;
+  padding: 4px 12px;
+}
+
+/* 修改弹窗样式 */
+.edit-form {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.form-item {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.form-label {
+  font-size: 14px;
+  font-weight: 500;
+  color: #4e5969;
+}
+
+.form-textarea {
+  width: 100%;
+}
+
+.options-edit {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.option-edit-row {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.option-edit-row .option-label {
+  width: 24px;
+  font-weight: 600;
+  color: #1d2129;
+}
+
+.option-input {
+  flex: 1;
+}
+</style>

+ 1989 - 0
src/views/graph-view/GraphView.vue

@@ -0,0 +1,1989 @@
+<template>
+  <div class="graph-wrapper" ref="graphWrapper">
+    <div id="graph-container" ref="graphContainer"></div>
+
+    <!-- 左侧悬浮窗 - 标签筛选 -->
+    <LabelSelected   :neo4j="neo4j" @filterNode="handleLabelFilter" />
+    
+    <!-- 左侧悬浮窗 - 权重筛选 -->
+    <WeightSelected v-if="isNetMode" :neo4j="neo4j" @filterWeight="handleWeightFilter" />
+
+    <!-- 右侧悬浮窗 - 节点统计 -->
+    <NodeCountStatisticize
+      v-if="target_mode!=='compose'"
+      :nodes="graphData.nodes"
+      @refresh="handleRefresh"
+      @highlight="handleHighlightByLabel"
+      @cancel-highlight="cancelHighlight"
+    />
+
+    <div class="tools">
+      <a-space>
+        <a-input-search
+          v-model="searchText"
+          placeholder="搜索节点名称"
+          allow-clear
+          @keyup.enter="handleSearch(searchText)"
+          @search="handleSearch"
+          @clear="handleClearSearch"
+          class="search-input"
+        />
+        <a-select
+          v-model="selectedLayout"
+          placeholder="布局方式"
+          @change="handleLayoutChange"
+          class="layout-select"
+        > 
+          <a-option
+            v-for="item in layoutOptions"
+            :key="item.value"
+            :value="item.value"
+            >{{ item.label }}</a-option
+          >
+        </a-select>
+        <a-switch
+          v-model="isNetMode"
+          @change="handleNetModeChange"
+          class="net-mode-switch"
+        >
+            <template #checked>
+        网状结构
+      </template>
+      <template #unchecked>
+       层级结构
+      </template></a-switch>
+      </a-space>
+    </div>
+
+    <div v-if="menuVisible" :style="menuStyle" class="custom-context-menu">
+      <template v-if="target_type === 'node'">
+        <div class="menu-item" @click="handleMenuExpandCollapse">
+          <component :is="hasChildren ? 'icon-minus' : 'icon-plus'" />
+          {{ hasChildren ? "收起" : "展开" }}
+        </div>
+        <a-divider v-if="hasChildren" style="margin: 4px 0" />
+      </template>
+
+      <div class="menu-item" v-if="target_mode === 'analysis'" @click="openSourceDrawer">
+        <icon-file /> 查看原文数据
+      </div>
+      
+      <a-divider v-if="target_mode === 'analysis'" style="margin: 4px 0" />
+      
+      <div class="menu-item" v-if="target_mode === 'analysis'" @click="handleUploadVideo">
+        <icon-video /> 上传视频
+      </div>
+      <div class="menu-item" v-if="target_mode === 'analysis'" @click="handleUploadPPTX">
+        <icon-presentation /> 上传PPTX
+      </div>
+    </div>
+
+    <a-drawer
+      :width="500"
+      :visible="drawerVisible"
+      @cancel="drawerVisible = false"
+      :footer="false"
+    >
+      <template #title>
+        <div style="display: flex; align-items: center; gap: 8px">
+          <icon-file :style="{ color: '#165dff' }" />
+          <span
+            >溯源详情:{{ activeNodeInfo?.text || activeNodeInfo?.label }}</span
+          >
+        </div>
+      </template>
+
+      <div class="source-container">
+        <a-empty
+          v-if="target_node_slices.length === 0"
+          description="暂无关联原文数据"
+          style="margin-top: 40px"
+        />
+
+        <div
+          v-for="(item, index) in target_node_slices"
+          :key="index"
+          class="source-card"
+        >
+          <div class="card-header">
+            <span class="location-tag">{{ item.mu_id }}</span>
+            <span class="node-name">{{ item.name }}</span>
+          </div>
+
+          <div class="card-body">
+            <div class="path-info">
+              {{ item.path }}
+            </div>
+            <div class="full-text">
+              <div class="text-label">完整上下文:</div>
+              {{ item.text }}
+            </div>
+          </div>
+        </div>
+        
+        <div class="file-preview-section">
+          <div class="section-title">关联文件</div>
+          
+          <div v-if="fileRecords.length > 0">
+            <div 
+              class="preview-card" 
+              v-for="(record, index) in fileRecords" 
+              :key="index"
+            >
+              <icon-video v-if="record.file_type === 'video'" class="preview-icon video-icon" />
+              <icon-presentation v-else class="preview-icon ppt-icon" />
+              <div class="preview-info">
+                <div class="preview-name">{{ record.original_name || record.file_name }}</div>
+                <div class="preview-size">{{ formatFileSize(record.file_size) }} · {{ formatTime(record.created_at) }}</div>
+              </div>
+              <div class="preview-actions">
+                <icon-play-circle v-if="record.file_type === 'video'" class="preview-action" @click.stop="handlePreviewFile(record)" />
+                <icon-eye v-else class="preview-action" @click.stop="handlePreviewFile(record)" />
+                <icon-edit class="preview-action edit-action" @click.stop="handleEditFileName(record)" />
+                <icon-delete class="preview-action delete-action" @click.stop="handleDeleteFile(record)" />
+              </div>
+            </div>
+          </div>
+          
+          <div v-else class="preview-card disabled-card">
+            <icon-file class="preview-icon" />
+            <div class="preview-info">
+              <div class="preview-name">暂无关联文件</div>
+              <div class="preview-size">右键节点上传视频或PPTX</div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </a-drawer>
+    
+    <VideoView 
+      :visible="videoPreviewVisible"
+      :file-record="currentVideoRecord"
+      @update:visible="videoPreviewVisible = $event"
+    />
+    <PPTView 
+      :visible="pptPreviewVisible"
+      :file-record="currentPPTRecord"
+      @update:visible="pptPreviewVisible = $event"
+    />
+    
+    <a-modal
+      v-model:visible="editFileNameVisible"
+      title="编辑文件名"
+      @ok="handleSaveFileName"
+      @cancel="editFileNameVisible = false"
+    >
+      <a-input
+        v-model="editFileName"
+        placeholder="请输入新的文件名"
+        class="edit-file-name-input"
+      />
+      <div class="edit-file-suffix">{{ getFileExtension(editRecord?.original_name || editRecord?.file_name || '') }}</div>
+    </a-modal>
+    
+    <a-modal
+      v-model:visible="deleteFileVisible"
+      title="确认删除"
+      ok-text="删除"
+      cancel-text="取消"
+      @ok="handleConfirmDelete"
+      @cancel="deleteFileVisible = false"
+    >
+      <p>确定要删除文件「{{ deleteRecord?.original_name || deleteRecord?.file_name }}」吗?</p>
+      <p class="delete-warning">此操作不可恢复,请谨慎操作。</p>
+    </a-modal>
+    <!-- <div></div> -->
+    <!-- <ComposeConfig v-if="target_mode=='compose'"></ComposeConfig> -->
+  </div>
+</template>
+
+<script setup lang="js">
+import { ref, reactive, shallowRef, onMounted, onBeforeUnmount, inject, toRaw, computed, watch, nextTick } from 'vue';
+import { Graph } from '@antv/g6';
+import { mockBookData } from './mock';
+import { Message } from '@arco-design/web-vue';
+import { graphService } from './graph'
+import { uniqueByField } from '../../utils/util'
+import { useModeStore } from '../../base/store'
+import { fileStorageService } from '../../utils/fileStorage'
+import LabelSelected from './LableSelected.vue'
+import WeightSelected from './WeightSelected.vue'
+import NodeCountStatisticize from './NodeCountStatisticize.vue'
+import ComposeConfig from './ComposeConfig.vue'
+import VideoView from './VideoView.vue'
+import PPTView from './PPTView.vue'
+import PPTICON from '../../assets/ppt.svg'
+import VideoICON from '../../assets/video.svg'
+import { formatTime } from '../../utils/util'
+
+const modeStore = useModeStore();
+const neo4j = inject('neo4j'); // 注入插件实例
+const graphContainer = ref(null);
+
+// 定义事件
+const emit = defineEmits(['update-selected']);
+
+// 更新选中状态并通知父组件
+const updateSelectedNodes = () => {
+  emit('update-selected', [...composeSelectedNodes.value]);
+};
+
+// 从外部删除节点(由父组件调用)
+const handleExternalRemove = (nodeId) => {
+  // 取消画布中的选中状态
+  if (graphInstance.value) {
+    try {
+      graphInstance.value.setElementState(String(nodeId), 'default');
+    } catch (e) {
+      console.warn(`取消节点 ${nodeId} 选中状态失败:`, e);
+    }
+  }
+  // 从数组中移除
+  const index = composeSelectedNodes.value.findIndex(n => n.id === nodeId);
+  if (index > -1) {
+    composeSelectedNodes.value.splice(index, 1);
+    updateSelectedNodes();
+  }
+};
+const graphWrapper = ref(null);
+const graphInstance = shallowRef(null);
+const target_mode = computed(()=>{
+  return modeStore.getMode
+})
+
+let dragChildren = [];
+let startParentPos = { x: 0, y: 0 };
+
+
+const searchText = ref("")
+const selectedLayout = ref("d3-force")
+const layoutOptions = ref([
+  { label: "树形图布局", value: "CompactBox" },
+  { label: "力导向图布局", value: "d3-force" },
+])
+const graphData = ref({ nodes: [], edges: [] }); // 💡 初始化为标准对象结构
+const isNetMode = ref(false); // 网状结构模式开关
+
+// 抽屉与菜单状态
+const drawerVisible = ref(false);
+const activeNodeInfo = ref(null);
+const target_node_slices = ref([])
+
+const menuVisible = ref(false);
+const menuStyle = reactive({ left: "0px", top: "0px" });
+const target_type = ref("node"); // 'node' 或 'edge'
+const activeItemId = ref(null);  // 记录当前操作的元素 ID
+const hasChildren = ref(false);  // 当前节点是否有子节点
+const isExpanded = ref(false);   // 当前节点子节点是否已展开
+const expandedNodeIds = ref(new Set()); // 记录已展开的节点ID集合
+const expandedChildrenMap = ref(new Map()); // 记录每个节点展开时新增的子节点ID集合
+
+const isOperating = ref(false); // 操作锁,防止并发异步操作
+
+// compose模式下选中的节点数组
+const composeSelectedNodes = ref([]);
+
+// 预览窗口状态
+const videoPreviewVisible = ref(false);
+const pptPreviewVisible = ref(false);
+const currentVideoRecord = ref(null);
+const currentPPTRecord = ref(null);
+const fileRecords = ref([]);
+
+// 编辑文件名弹窗状态
+const editFileNameVisible = ref(false);
+const editFileName = ref('');
+const editRecord = ref(null);
+
+// 删除文件确认弹窗状态
+const deleteFileVisible = ref(false);
+const deleteRecord = ref(null);
+
+// 监听模式变化,模式切换时清除所有选中状态
+watch(target_mode, (newMode, oldMode) => {
+  // 切换出题模式和智能分析模式时清空选中节点
+  if ((newMode === 'compose' && oldMode === 'analysis') || 
+      (newMode === 'analysis' && oldMode === 'compose')) {
+    // 清除画布中所有节点选中状态
+    if (graphInstance.value) {
+      try {
+        if (typeof graphInstance.value.clearElementStates === 'function') {
+          graphInstance.value.clearElementStates();
+        }
+      } catch (e) {
+        console.warn(`清除节点状态失败:`, e);
+      }
+    }
+    // 清空选中数组
+    composeSelectedNodes.value = [];
+    updateSelectedNodes();
+  }
+});
+
+// 从compose选中数组中删除节点并同步取消画布选中状态
+const removeComposeNode = (nodeId) => {
+  // 取消画布中的选中状态
+  if (graphInstance.value) {
+    try {
+      graphInstance.value.setElementState(String(nodeId), 'default');
+    } catch (e) {
+      console.warn(`取消节点 ${nodeId} 选中状态失败:`, e);
+    }
+  }
+  // 从数组中移除
+  const index = composeSelectedNodes.value.findIndex(n => n.id === nodeId);
+  if (index > -1) {
+    composeSelectedNodes.value.splice(index, 1);
+  }
+};
+
+// ✨ 7阶渐变带透明度色卡
+const nodeLevelStyles = [
+  { fill: 'rgba(22, 93, 255, 0.65)', stroke: '#165DFF' },
+  { fill: 'rgba(0, 153, 255, 0.62)', stroke: '#0099FF' },
+  { fill: 'rgba(20, 201, 201, 0.60)', stroke: '#14C9C9' },
+  { fill: 'rgba(0, 180, 42, 0.68)', stroke: '#00B42A' },
+  { fill: 'rgba(250, 220, 25, 0.68)', stroke: '#FADC19' },
+  { fill: 'rgba(255, 125, 0, 0.65)', stroke: '#FF7D00' },
+  { fill: 'rgba(114, 46, 209, 0.65)', stroke: '#722ED1' }
+];
+const node_label_maps = {
+  "Book": "rgba(227, 163, 184, 0.8)",
+  "Part": "rgba(249, 195, 81, 0.8)",
+  "Chapter": "rgba(245, 139, 85, 0.8)",
+  "Section": "rgba(102, 191, 102, 0.8)",
+  "Mu": "rgba(99, 179, 237, 0.8)",
+  "EntityType": "rgba(241, 93, 93, 0.8)",
+  "Entity": "rgba(205, 150, 205, 0.8)"
+}
+const node_label_maps_stoke = {
+  "rgba(227, 163, 184, 0.8)": "#E3A3B8",
+  "rgba(249, 195, 81, 0.8)": "#F9C351",
+  "rgba(245, 139, 85, 0.8)": "#F58B55",
+  "rgba(102, 191, 102, 0.8)": "#66BF66",
+  "rgba(99, 179, 237, 0.8)": "#63B3ED",
+  "rgba(241, 93, 93, 0.8)": "#F15D5D",
+  "rgba(205, 150, 205, 0.8)": "#CD96CD"
+}
+
+const defaultNodeStyle = { fill: 'rgba(134, 144, 156, 0.15)', stroke: '#86909c' };
+
+// 安全工具函数:强行刹车并清洗全图动画流与状态
+const stopAllAnimationsAndStates = () => {
+  try {
+    if (graphInstance.value?.layoutInstance?.stop) {
+      graphInstance.value.layoutInstance.stop();
+    }
+    if (graphInstance.value?.stopLayout) {
+      graphInstance.value.stopLayout();
+    }
+    if (typeof graphInstance.value?.clearElementStates === 'function') {
+      graphInstance.value.clearElementStates();
+    }
+  } catch (e) {
+    console.log('叫停动画静默捕获:', e);
+  }
+};
+
+// 安全工具函数:将响应式数据清洗为最纯净的原始 JSON 拍给 G6
+const syncDataToG6 = (animated = false, applyLayout = true) => {
+  if (!graphInstance.value) return;
+  // ✨【致命破案点】:必须通过 toRaw 彻底扒掉 Vue 的 Proxy 外衣,防止 getPorts 报错
+  const rawData = toRaw(graphData.value);
+  const pureData = JSON.parse(JSON.stringify(rawData));
+  
+  const nodeIdSet = new Set(pureData.nodes.map(n => String(n.id)))
+  pureData.edges = pureData.edges.filter(edge => {
+    const s = String(edge.source)
+    const t = String(edge.target)
+    return nodeIdSet.has(s) && nodeIdSet.has(t)
+  })
+  
+  // 处理树形布局的多根节点问题
+  let processedData = pureData;
+  if (selectedLayout.value === 'CompactBox') {
+    processedData = handleMultipleRootNodes(pureData);
+    // 清除节点已有的 x/y 坐标,让树形布局重新计算位置
+    processedData.nodes.forEach(node => {
+      delete node.x;
+      delete node.y;
+      delete node.fx;
+      delete node.fy;
+    });
+  }
+  
+  // 更新响应式数据源,保持数据一致
+    graphData.value = { nodes: processedData.nodes, edges: processedData.edges };
+    // 使用处理后的纯净数据传给 G6,避免响应式 Proxy 导致 getPorts 报错
+    graphInstance.value.setData(processedData);
+  
+  // 根据参数决定是否应用布局配置
+  if (applyLayout) {
+    const layoutConfig = getLayoutConfig();
+    graphInstance.value.layout({ ...layoutConfig, animated });
+  }
+  
+  graphInstance.value.render();
+};
+
+const rebuildGraph = (newData) => {
+  if (!newData || !graphContainer.value) return;
+  
+  try {
+    if (graphInstance.value?.layoutInstance?.stop) {
+      graphInstance.value.layoutInstance.stop();
+    }
+    if (graphInstance.value?.stopLayout) {
+      graphInstance.value.stopLayout();
+    }
+    
+    graphInstance.value.destroy();
+    graphInstance.value = null;
+  } catch (e) {
+    console.warn('销毁旧图实例时发生错误:', e);
+  }
+  
+  initGraph();
+  
+  const rawData = toRaw(newData);
+  const pureData = JSON.parse(JSON.stringify(rawData));
+  
+  const nodeIdSet = new Set(pureData.nodes.map(n => String(n.id)));
+  pureData.edges = pureData.edges.filter(edge => {
+    const s = String(edge.source);
+    const t = String(edge.target);
+    return nodeIdSet.has(s) && nodeIdSet.has(t);
+  });
+  
+  let processedData = pureData;
+  if (selectedLayout.value === 'CompactBox') {
+    processedData = handleMultipleRootNodes(pureData);
+    processedData.nodes.forEach(node => {
+      delete node.x;
+      delete node.y;
+      delete node.fx;
+      delete node.fy;
+    });
+  }
+  
+  graphData.value = { nodes: processedData.nodes, edges: processedData.edges };
+  graphInstance.value.setData(processedData);
+  
+  const layoutConfig = getLayoutConfig();
+  graphInstance.value.layout({ ...layoutConfig, animated: false });
+  
+  graphInstance.value.render();
+  
+  graphInstance.value.once('layoutend', () => {
+    graphInstance.value?.fitCenter({ duration: 300 });
+  });
+  setTimeout(() => {
+    graphInstance.value?.fitCenter({ duration: 300 });
+  }, 500);
+};
+
+onMounted(async () => {
+  const r =   await graphService.findQuestionsNodes(neo4j)
+  const a = r.map(item=>item.question_type)
+  console.log(r,a)
+  const d1 = await graphService.findNodesByQuestionType(neo4j, "简答题") //358
+  const d2 = await graphService.findNodesByQuestionType(neo4j, "名词解释")  //  360
+  const d3 = await graphService.findNodesByQuestionType(neo4j, "单选题") // 403
+  const d4 = await graphService.findNodesByQuestionType(neo4j, "多选题") // 395
+  const d5 = await graphService.findNodesByQuestionType(neo4j, "填空题") // 395
+  console.log(d1)
+  console.log(d2)
+  console.log(d3)
+  console.log(d4) 
+  console.log(d5)
+  const res = await findRootNodes(neo4j)
+  console.log("根据根节点获取根节点信息:", res)
+  if (res && res.length) {
+    graphData.value = {
+      nodes: res.map((item) => ({
+        id: String(item.id),
+        data: {
+          labels: item.labels,
+          color: node_label_maps[item.labels[0]] || defaultNodeStyle.fill,
+          text: item.name,
+          elementId: item.elementId,
+          ...(item.star !== undefined && { star: item.star })
+        }
+      })),
+      edges: []
+    };
+  }
+
+  initGraph();
+
+  // 渲染完成后,安全等待布局结束自动放大居中
+  graphInstance.value.once('layoutend', () => {
+    // graphInstance.value?.zoomTo(1.3, { duration: 300 });
+    graphInstance.value?.fitCenter({ duration: 300 });
+  });
+});
+
+onBeforeUnmount(() => {
+  if (graphInstance.value) {
+    graphInstance.value.destroy();
+  }
+});
+
+
+const findRootNodes = async () => {
+  return await graphService.findRootNodes(neo4j)
+}
+const findNodeById = async (nodeId) => {
+  return await graphService.findNodeById(neo4j, nodeId)
+}
+
+const findSubgraphByParentId = async (parentId) => {
+  return await graphService.findSubgraphByParentId(neo4j, parentId)
+}
+
+const findNodeRelations = async (nodeId) => {
+  return await graphService.findNodeRelations(neo4j, nodeId)
+}
+
+const initGraph = () => {
+  if (!graphContainer.value) return;
+
+  graphInstance.value = new Graph({
+    container: graphContainer.value,
+    data: { nodes: [], edges: [] }, // 初始给空,随后通过逻辑同步
+    layout: {
+      // type: "radial",
+      type: "d3-force",
+      animated: false,
+      iterations: 150,
+      linkDistance: 220,
+      preventOverlap: true,
+      manyBody: {
+        strength: -400,
+      },
+        // link: { strength: 0.5 },     // 减小连线弹簧拉力(最关键,不拉邻居)
+      randomize: true
+    },
+
+
+    node: {
+      type: 'circle',
+      style: (model) => {
+        // 处理虚拟根节点 - 完全隐藏
+        if (model.id === '__virtual_root__' || model?.data?.labels?.includes('VirtualRoot')) {
+          return {
+            size: 0,
+            fill: 'transparent',
+            stroke: 'transparent',
+            opacity: 0,
+            labelText: '',
+            visible: false,
+          };
+        }
+        
+        const isHighlight = model?.data?._highlight;
+        const isDimmed = model?.data?._dimmed;
+        const nodeColor = model?.data?.color || defaultNodeStyle.fill;
+        const strokeColor = node_label_maps_stoke[nodeColor] || defaultNodeStyle.stroke;
+        
+        return {
+          size: 60,
+          fill: nodeColor,
+          labelText: model.data.text,
+          labelPlacement: 'bottom',
+          labelFill: '#1d2129',
+          labelFontSize: 14,
+          cursor: 'pointer',
+          // 根据数据属性动态设置样式
+          stroke: isHighlight ? strokeColor : strokeColor,
+          lineWidth: isHighlight ? 4 : 1,
+          shadowColor: isHighlight ? strokeColor : 'transparent',
+          shadowBlur: isHighlight ? 20 : 0,
+          opacity: isDimmed ? 0.3 : 1,
+        };
+      },
+      state: {
+        selected: {
+          stroke: (model) => node_label_maps_stoke[model?.data?.color] || defaultNodeStyle.stroke,
+          lineWidth: 3, // 加粗边框以突出显示
+        }
+      }
+
+    },
+    edge: {
+      style: (model) => {
+        // 处理虚拟边 - 完全隐藏
+        if (model.id?.startsWith('__virtual_edge_') || model.source === '__virtual_root__') {
+          return {
+            stroke: 'transparent',
+            lineWidth: 0,
+            endArrow: false,
+            opacity: 0,
+            visible: false,
+          };
+        }
+        
+        return {
+          stroke: '#C9CDD4',
+          lineWidth: 1,
+          endArrow: true,
+          labelText: model.label || '',
+          labelFontSize: 12,
+          labelFill: '#1d2129',
+          labelBackground: {
+            fill: '#fff',
+            padding: [2, 4, 2, 4],
+            radius: 2
+          },
+          cursor: 'pointer'
+        };
+      },
+    },
+    behaviors: [
+      'drag-canvas', 
+      'zoom-canvas', 
+      {
+        type: "drag-element",
+        key: "drag-element",
+        enable: true
+      }
+    ],
+    plugins: [
+      { type: "background", key: "bg", background: "#ffffff" },
+      { type: "grid-line", key: "grid", size: 12, stroke: "rgba(15, 23, 42, 0.05)" },
+      { type: 'minimap', size: [150, 100],delay:1000 },
+      {
+        type: "toolbar",
+        key: "toolbar",
+        position: "top-left",
+        onClick: (item) => {
+          const actionId = typeof item === "string" ? item : item.id;
+          switch (actionId) {
+            case "zoom-in": zoomIn(); break;
+            case "zoom-out": zoomOut(); break;
+            case "reset": resetView(); break;
+            case "export": exportImage(); break;
+            case "request-fullscreen": toggleFullscreen(); break;
+          }
+        },
+        getItems: () => [
+          { id: "zoom-in", value: "zoom-in" },
+          { id: "zoom-out", value: "zoom-out" },
+          { id: "export", value: "export" },
+
+          { id: "auto-fit", value: "reset" },
+          { id: "request-fullscreen", value: "request-fullscreen" },
+        ],
+      },
+    ]
+  });
+
+  // 初始化首次喂数
+  const rawData = toRaw(graphData.value);
+  graphInstance.value.setData(JSON.parse(JSON.stringify(rawData)));
+  graphInstance.value.render();
+
+  // 画布点击
+  graphInstance.value.on("canvas:click", (evt) => {
+    // 只有在非compose模式下才清除选中状态
+    if (target_mode.value !== 'compose') {
+      // 获取所有处于 'selected' 状态的节点数据
+      const selectedNodes = graphInstance.value.getElementDataByState('node', 'selected');
+
+      if (selectedNodes.length > 0) {
+        selectedNodes.forEach(node => {
+          try {
+            graphInstance.value.setElementState(String(node.id), 'default');
+          } catch (e) {
+            console.warn(`清除节点 ${node.id} 状态失败:`, e);
+          }
+        });
+      }
+    }
+    menuVisible.value = false;
+  });
+
+  // 节点单击 - compose模式下选中节点
+  graphInstance.value.on("node:click", (evt) => {
+    if (target_mode.value === 'compose') {
+      const id = evt.target.config.id;
+      const nodeData = graphInstance.value.getNodeData(id);
+      
+      // 获取当前节点状态
+      const currentState = graphInstance.value.getElementState(id);
+      const isSelected = currentState.includes('selected');
+      
+      if (isSelected) {
+        // 如果已选中,取消选中
+        graphInstance.value.setElementState(id, 'default');
+        // 从选中数组中移除
+        const index = composeSelectedNodes.value.findIndex(n => n.id === id);
+        if (index > -1) {
+          composeSelectedNodes.value.splice(index, 1);
+        }
+      } else {
+        // 如果未选中,选中它
+        graphInstance.value.setElementState(id, 'selected');
+        // 添加到选中数组
+        if (!composeSelectedNodes.value.find(n => n.id === id)) {
+          composeSelectedNodes.value.push({
+            id: id,
+            data: nodeData?.data || {}
+          });
+        }
+      }
+      
+      // 通知父组件选中状态变化
+      updateSelectedNodes();
+      console.log('compose模式选中节点:', composeSelectedNodes.value);
+    }
+  });
+
+  // 节点双击 - 展开/收起功能(仅非compose模式)
+  graphInstance.value.on("node:dblclick", (evt) => {
+    if (target_mode.value !== 'compose') {
+      const id = evt.target.config.id;
+      activeItemId.value = id;
+      
+      // 清除其他节点选中状态,选中当前节点
+      clearAllSelectedStates();
+      graphInstance.value.setElementState(id, 'selected');
+      
+      // 检查该节点是否已展开
+      const isNodeExpanded = expandedNodeIds.value.has(id);
+      
+      if (isNodeExpanded) {
+        // 已展开,执行收起操作
+        handleExpandCollapse(true);
+        expandedNodeIds.value.delete(id);
+      } else {
+        // 未展开,执行展开操作
+        handleExpandCollapse(false);
+        expandedNodeIds.value.add(id);
+      }
+    }
+  });
+
+  // 节点右键
+  graphInstance.value.on("node:contextmenu", (evt) => {
+    evt.preventDefault();
+    const id = evt.target.config.id;
+    // 节点选中
+    const currentState = graphInstance.value.getElementState(id);
+    const isSelected = currentState.includes('selected');
+
+    // 先清除所有已选中的节点
+    clearAllSelectedStates();
+    // 先找到所有当前处于 'selected' 状态的节点
+    const selectedNodes = graphInstance.value.getElementDataByState('node', 'selected');
+    const selectedIds = selectedNodes.map(node => node.id);
+
+    // 如果有选中的节点,将它们重置为 default 状态
+    if (selectedIds.length > 0) {
+      graphInstance.value.setElementState(selectedIds, 'default');
+    }
+
+    // 4. 如果刚才点击的节点之前没被选中,则现在选中它
+    // 如果之前已经选中,上面的步骤已经把它取消了,所以这里不需要再操作
+    // 如果之前没选中,现在需要选中它
+    if (!isSelected) {
+      graphInstance.value.setElementState(id, 'selected');
+    }
+
+    activeItemId.value = id;
+    target_type.value = "node";
+    let outEdges = graphInstance.value.getRelatedEdgesData(id, 'out') || [];
+    hasChildren.value = outEdges.length > 0;
+    menuStyle.left = `${evt.client.x}px`;
+    menuStyle.top = `${evt.client.y}px`;
+    menuVisible.value = true;
+  });
+
+  // 关系右键
+  graphInstance.value.on("edge:contextmenu", async (evt) => {
+    evt.preventDefault();
+    const id = evt.target.config.id;
+    activeItemId.value = id;
+    target_type.value = "edge";
+    // console.log(id, graphInstance.value.getEdgeData(String(id)))
+
+    activeNodeInfo.value = graphInstance.value.getEdgeData(String(id)) || { text: '未命名关系' };
+    const sources = await graphService.searchEdgeSources(neo4j, String(id))
+    if (sources) {
+      target_node_slices.value = JSON.parse(sources)
+    }
+    menuStyle.left = `${evt.client.x}px`;
+    menuStyle.top = `${evt.client.y}px`;
+    menuVisible.value = true;
+  });
+
+  graphInstance.value.on("canvas:click", () => { menuVisible.value = false; });
+  graphInstance.value.on("canvas:dragstart", () => { menuVisible.value = false; });
+
+  // 节点拖拽开始时,清除其他节点的选中状态,只保留被拖拽节点
+  graphInstance.value.on("node:dragstart", (evt) => {
+    const draggedNodeId = evt.target.config.id;
+    
+    // 获取所有选中的节点
+    const selectedNodes = graphInstance.value.getElementDataByState('node', 'selected');
+    const selectedIds = selectedNodes.map(node => String(node.id));
+    
+    // 取消除被拖拽节点外其他节点的选中状态
+    selectedIds.forEach(id => {
+      if (id !== String(draggedNodeId)) {
+        graphInstance.value.setElementState(id, 'default');
+      }
+    });
+  });
+  graphInstance.value.on("viewport:change", () => { menuVisible.value = false; });
+};
+const clearAllSelectedStates = () => {
+  const selectedNodes = graphInstance.value.getElementDataByState('node', 'selected');
+  // console.log(selectedNodes)
+  if (selectedNodes.length > 0) {
+    // 💡 核心修复点:使用 String() 确保所有 ID 都转为字符串
+    // const selectedIds = selectedNodes.map(node => String(node.id)); 
+    // console.log(selectedIds)
+    selectedNodes.forEach(node => {
+      try {
+        graphInstance.value.setElementState(String(node.id), 'default');
+      } catch (e) {
+        console.warn(`清除节点 ${node.id} 状态失败:`, e);
+      }
+    });
+  }
+};
+const resetView = () => {
+  graphInstance.value?.zoomTo(1, { duration: 500 });
+  graphInstance.value?.fitCenter({ duration: 500 });
+};
+const zoomIn = () => graphInstance.value?.zoomBy(1.2, { duration: 300 });
+const zoomOut = () => graphInstance.value?.zoomBy(0.8, { duration: 300 });
+
+const toggleFullscreen = () => {
+  const el = graphWrapper.value;
+  if (!document.fullscreenElement && el) {
+    el.requestFullscreen().then(() => {
+      setTimeout(() => {
+        if (graphInstance.value && graphContainer.value) {
+          graphInstance.value.setSize(graphContainer.value.clientWidth, graphContainer.value.clientHeight);
+          graphInstance.value.fitView();
+        }
+      }, 100);
+    });
+  } else {
+    document.exitFullscreen();
+    setTimeout(() => {
+      if (graphInstance.value && graphContainer.value) {
+        graphInstance.value.setSize(graphContainer.value.clientWidth, graphContainer.value.clientHeight);
+      }
+    }, 100);
+  }
+};
+
+const exportImage = async () => {
+  if (graphInstance.value) {
+    const dataURL = await graphInstance.value.toDataURL();
+    const link = document.createElement("a");
+    link.download = `graph-${Date.now()}.png`;
+    link.href = dataURL;
+    link.click();
+  }
+};
+
+// ================= 布局切换功能 =================
+// 获取当前布局配置
+const getLayoutConfig = () => {
+  if (selectedLayout.value === 'd3-force') {
+    return {
+      type: 'd3-force',
+      animated: false,
+      iterations: 150,
+      linkDistance: 220,
+      preventOverlap: true,
+      manyBody: {
+        strength: -400,
+      },
+      randomize: true
+    };
+  } else if (selectedLayout.value === 'CompactBox') {
+    return {
+      type: 'compact-box',
+      direction: 'LR',
+      getHeight: () => 60,
+      getWidth: () => 120,
+      getVGap: () => 40,
+      getHGap: () => 120,
+      nodeSep: 50,
+      rankSep: 150,
+      align: 'DL',
+      compactBox: true,
+    };
+  }
+  // 默认力导向图
+  return {
+    type: 'd3-force',
+    animated: false,
+    iterations: 150,
+    linkDistance: 220,
+    preventOverlap: true,
+    manyBody: {
+      strength: -400,
+    },
+    randomize: true
+  };
+};
+
+// 处理布局切换
+const handleLayoutChange = async (value) => {
+  if (isOperating.value) return;
+  isOperating.value = true;
+  
+  if (!value || !graphInstance.value) {
+    isOperating.value = false;
+    return;
+  }
+  
+  if (isNetMode.value && value !== 'd3-force') {
+    Message.warning('网状结构模式下只能使用力导向图布局');
+    selectedLayout.value = 'd3-force';
+    isOperating.value = false;
+    return;
+  }
+  
+  try {
+    rebuildGraph(graphData.value);
+    
+    const layoutName = value === 'd3-force' ? '力导向图' : '树形图';
+    Message.success(`已切换为${layoutName}布局`);
+  } catch (err) {
+    console.error('布局切换失败:', err);
+    Message.error('布局切换失败');
+  } finally {
+    isOperating.value = false;
+  }
+};
+
+// 处理树形布局中的多根节点问题
+const handleMultipleRootNodes = (data) => {
+  if (!data || !data.nodes || !data.edges) {
+    return data;
+  }
+  
+  const virtualRootId = '__virtual_root__';
+  
+  // 先移除已存在的虚拟根节点和虚拟边,避免重复创建
+  const cleanNodes = data.nodes.filter(node => String(node.id) !== virtualRootId);
+  const cleanEdges = data.edges.filter(edge => 
+    String(edge.source) !== virtualRootId && !String(edge.id).startsWith('__virtual_edge_')
+  );
+  
+  // 找出所有根节点(没有入边的节点)
+  const targetNodes = new Set(cleanEdges.map(e => String(e.target)));
+  const rootNodes = cleanNodes.filter(node => !targetNodes.has(String(node.id)));
+  
+  // 如果只有一个或没有根节点,不需要处理
+  if (rootNodes.length <= 1) {
+    return { nodes: cleanNodes, edges: cleanEdges };
+  }
+  
+  // 创建虚拟根节点
+  const virtualRoot = {
+    id: virtualRootId,
+    data: {
+      labels: ['VirtualRoot'],
+      color: 'rgba(255, 255, 255, 0)',
+      text: '',
+      elementId: virtualRootId
+    }
+  };
+  
+  // 创建从虚拟根节点到所有根节点的边
+  const newEdges = rootNodes.map(rootNode => ({
+    id: `__virtual_edge_${rootNode.id}`,
+    source: virtualRootId,
+    target: String(rootNode.id),
+    data: {
+      label: ''
+    }
+  }));
+  
+  return {
+    nodes: [virtualRoot, ...cleanNodes],
+    edges: [...cleanEdges, ...newEdges]
+  };
+};
+
+const openSourceDrawer = async () => {
+  menuVisible.value = false;
+  drawerVisible.value = true;
+  const node_id = activeItemId.value;
+  if (!graphData.value.nodes) return;
+
+  graphData.value?.nodes.forEach(async node => {
+    if (node.id === node_id) {
+      activeNodeInfo.value = node.data;
+      
+      let sources;
+      if (node.data?.labels?.includes('Entity')) {
+        sources = await graphService.searchMuByEntitySources(neo4j, node_id);
+      } else {
+        sources = await graphService.searchNodeSources(neo4j, node_id);
+      }
+      
+      if (sources) {
+        target_node_slices.value = JSON.parse(sources);
+      }
+      
+      fileRecords.value = await fileStorageService.getFileRecords(node_id);
+    }
+  })
+};
+
+const formatFileSize = (size) => {
+  if (!size) return '0 B';
+  const units = ['B', 'KB', 'MB', 'GB'];
+  let i = 0;
+  let num = size;
+  while (num >= 1024 && i < units.length - 1) {
+    num /= 1024;
+    i++;
+  }
+  return num.toFixed(2) + ' ' + units[i];
+};
+
+const handlePreviewFile = (record) => {
+  if (record.file_type === 'video') {
+    currentVideoRecord.value = record;
+    videoPreviewVisible.value = true;
+  } else {
+    currentPPTRecord.value = record;
+    pptPreviewVisible.value = true;
+  }
+};
+
+const getFileExtension = (fileName) => {
+  const lastDotIndex = fileName.lastIndexOf('.');
+  if (lastDotIndex === -1) return '';
+  return fileName.substring(lastDotIndex);
+};
+
+const getFileNameWithoutExtension = (fileName) => {
+  const lastDotIndex = fileName.lastIndexOf('.');
+  if (lastDotIndex === -1) return fileName;
+  return fileName.substring(0, lastDotIndex);
+};
+
+const handleEditFileName = (record) => {
+  editRecord.value = record;
+  const fullName = record.original_name || record.file_name;
+  editFileName.value = getFileNameWithoutExtension(fullName);
+  editFileNameVisible.value = true;
+};
+
+const handleSaveFileName = async () => {
+  if (!editFileName.value.trim()) {
+    Message.warning('请输入文件名');
+    return;
+  }
+  
+  try {
+    const fullName = editRecord.value.original_name || editRecord.value.file_name;
+    const extension = getFileExtension(fullName);
+    const newFileName = editFileName.value.trim() + extension;
+    
+    await fileStorageService.updateFileRecord(editRecord.value.id, {
+      original_name: newFileName
+    });
+    
+    const index = fileRecords.value.findIndex(r => r.id === editRecord.value.id);
+    if (index > -1) {
+      fileRecords.value[index].original_name = newFileName;
+    }
+    
+    Message.success('文件名修改成功');
+    editFileNameVisible.value = false;
+  } catch (error) {
+    console.error('修改文件名失败:', error);
+    Message.error('修改文件名失败');
+  }
+};
+
+const handleDeleteFile = (record) => {
+  deleteRecord.value = record;
+  deleteFileVisible.value = true;
+};
+
+const handleConfirmDelete = async () => {
+  try {
+    await fileStorageService.deleteFileById(deleteRecord.value);
+    
+    fileRecords.value = fileRecords.value.filter(r => r.id !== deleteRecord.value.id);
+    
+    Message.success('文件删除成功');
+    deleteFileVisible.value = false;
+  } catch (error) {
+    console.error('删除文件失败:', error);
+    Message.error('删除文件失败');
+  }
+};
+
+// ==================== 文件上传处理 ====================
+const handleUploadVideo = () => {
+  menuVisible.value = false;
+  const input = document.createElement('input');
+  input.type = 'file';
+  input.accept = 'video/*';
+  input.onchange = async (e) => {
+    const file = e.target.files[0];
+    if (!file) return;
+    
+    try {
+      await fileStorageService.uploadFile(activeItemId.value, file);
+      Message.success('视频上传成功');
+    } catch (error) {
+      console.error('视频上传失败:', error);
+      if (error.message && error.message.includes('已存在')) {
+        Message.warning(error.message);
+      } else {
+        Message.error('视频上传失败');
+      }
+    }
+  };
+  input.click();
+};
+
+const handleUploadPPTX = () => {
+  menuVisible.value = false;
+  const input = document.createElement('input');
+  input.type = 'file';
+  input.accept = '.pptx';
+  input.onchange = async (e) => {
+    const file = e.target.files[0];
+    if (!file) return;
+    
+    try {
+      await fileStorageService.uploadFile(activeItemId.value, file);
+      Message.success('PPTX上传成功');
+    } catch (error) {
+      console.error('PPTX上传失败:', error);
+      if (error.message && error.message.includes('已存在')) {
+        Message.warning(error.message);
+      } else {
+        Message.error('PPTX上传失败');
+      }
+    }
+  };
+  input.click();
+};
+
+// ==================== 右键菜单展开/收起处理 ====================
+const handleMenuExpandCollapse = async () => {
+  const isNodeExpanded = expandedNodeIds.value.has(activeItemId.value);
+  
+  if (isNodeExpanded) {
+    // 已展开,执行收起操作
+    await handleExpandCollapse(true);
+    expandedNodeIds.value.delete(activeItemId.value);
+  } else {
+    // 未展开,执行展开操作
+    await handleExpandCollapse(false);
+    expandedNodeIds.value.add(activeItemId.value);
+  }
+};
+
+// ==================== 【终极加固:展开与收起核心控制】 ====================
+const handleExpandCollapse = async (hasChildrenParam) => {
+  if (isOperating.value) return;
+  isOperating.value = true;
+  
+  menuVisible.value = false;
+
+  stopAllAnimationsAndStates();
+
+  try {
+    if (hasChildrenParam) {
+      // -------------------- 【级联收起分支】 --------------------
+      const allDeleteNodeIds = new Set();
+      const queue = [activeItemId.value];
+
+      if (isNetMode.value && expandedChildrenMap.value.has(activeItemId.value)) {
+        const directChildren = expandedChildrenMap.value.get(activeItemId.value);
+        const childQueue = [...directChildren];
+        
+        while (childQueue.length > 0) {
+          const childId = childQueue.shift();
+          allDeleteNodeIds.add(childId);
+          
+          if (expandedChildrenMap.value.has(childId)) {
+            const grandchildren = expandedChildrenMap.value.get(childId);
+            grandchildren.forEach(gc => {
+              if (!allDeleteNodeIds.has(gc)) {
+                childQueue.push(gc);
+              }
+            });
+          }
+        }
+      } else {
+        while (queue.length > 0) {
+          const currentId = queue.shift();
+          
+          const outEdges = graphInstance.value.getRelatedEdgesData(currentId, 'out') || [];
+          const inEdges = isNetMode.value ? graphInstance.value.getRelatedEdgesData(currentId, 'in') || [] : [];
+
+          outEdges.forEach(edge => {
+            if (!allDeleteNodeIds.has(edge.target)) {
+              allDeleteNodeIds.add(edge.target);
+              queue.push(edge.target);
+            }
+          });
+
+          if (isNetMode.value) {
+            inEdges.forEach(edge => {
+              if (!allDeleteNodeIds.has(edge.source)) {
+                allDeleteNodeIds.add(edge.source);
+                queue.push(edge.source);
+              }
+            });
+          }
+        }
+      }
+
+      allDeleteNodeIds.forEach(id => {
+        try { graphInstance.value.setState(id, 'selected', false); } catch (e) { }
+      });
+
+      allDeleteNodeIds.forEach(id => {
+        expandedChildrenMap.value.delete(id);
+      });
+      expandedChildrenMap.value.delete(activeItemId.value);
+
+      let survivingNodes = [];
+      if (graphData.value.nodes?.length) {
+        survivingNodes = graphData.value.nodes.filter(node => !allDeleteNodeIds.has(node.id));
+      }
+
+      const survivingNodeIds = new Set(survivingNodes.map(n => String(n.id)));
+
+      let survivingEdges = [];
+      if (graphData.value.edges?.length) {
+        survivingEdges = graphData.value.edges.filter(edge => {
+          return survivingNodeIds.has(String(edge.source)) && survivingNodeIds.has(String(edge.target));
+        });
+      }
+
+      graphData.value = {
+        nodes: survivingNodes,
+        edges: survivingEdges
+      };
+
+      syncDataToG6(false);
+    }
+    else {
+      // -------------------- 【大集群展开分支】 --------------------
+      const res = isNetMode.value 
+        ? await findNodeRelations(activeItemId.value)
+        : await findNodeById(activeItemId.value);
+
+      let currentNodes = graphData.value.nodes ? [...toRaw(graphData.value.nodes)] : [];
+      let currentEdges = graphData.value.edges ? [...toRaw(graphData.value.edges)] : [];
+
+      const existingNodeIds = new Set(currentNodes.map(node => String(node.id)));
+
+      if (res.nodes && res.nodes.length) {
+        console.log(res.nodes,89);
+        res.nodes.forEach(item => {
+          item.id = String(item.id);
+          if (item.data && item.data.labels) {
+            item.data.color = node_label_maps[item.data.labels[0]] || defaultNodeStyle.fill;
+          }
+        });
+        currentNodes = uniqueByField([...currentNodes, ...res.nodes], 'id');
+      }
+
+      if (res.edges && res.edges.length) {
+        res.edges.forEach(edge => {
+          edge.id = String(edge.id);
+          edge.source = String(edge.source);
+          edge.target = String(edge.target);
+        });
+        currentEdges = uniqueByField([...currentEdges, ...res.edges], 'id');
+      }
+
+      const newNodeIds = new Set();
+      currentNodes.forEach(node => {
+        if (!existingNodeIds.has(String(node.id))) {
+          newNodeIds.add(String(node.id));
+        }
+      });
+      expandedChildrenMap.value.set(activeItemId.value, newNodeIds);
+
+      const validNodeIds = new Set(currentNodes.map(node => String(node.id)));
+
+      const safeEdges = currentEdges.filter(edge => {
+        const hasSource = validNodeIds.has(String(edge.source));
+        const hasTarget = validNodeIds.has(String(edge.target));
+        if (!hasSource || !hasTarget) {
+          console.warn(`[数据预警] 发现了一条悬空的垃圾边被拦截,ID: ${edge.id}, source: ${edge.source}(存在:${hasSource}), target: ${edge.target}(存在:${hasTarget})`);
+        }
+        return hasSource && hasTarget;
+      });
+
+      graphData.value = {
+        nodes: currentNodes,
+        edges: safeEdges
+      };
+
+      syncDataToG6(false);
+    }
+  } catch (err) {
+    console.error(err);
+    Message.error("节点级联查询失败");
+  } finally {
+    isOperating.value = false;
+  }
+};
+
+// ==================== 网状结构模式切换处理 ====================
+const handleNetModeChange = async (checked) => {
+  if (isOperating.value) return;
+  isOperating.value = true;
+
+  try {
+    if (checked) {
+      selectedLayout.value = 'd3-force';
+    }
+
+    expandedNodeIds.value.clear();
+    expandedChildrenMap.value.clear();
+
+    const res = await findRootNodes(neo4j);
+    console.log(res,89);
+    if (res && res.length) {
+      const newData = {
+        nodes: res.map((item) => ({
+          id: String(item.id),
+          data: {
+            labels: item.labels,
+            color: node_label_maps[item.labels[0]] || defaultNodeStyle.fill,
+            text: item.name,
+            elementId: item.elementId,
+            ...(item.star !== undefined && { star: item.star })
+          }
+        })),
+        edges: []
+      };
+
+      rebuildGraph(newData);
+    }
+
+    const modeName = checked ? '网状结构' : '层级结构';
+    Message.success(`已切换为${modeName}视图,请重新展开节点`);
+  } catch (err) {
+    console.error(err);
+    Message.error('模式切换失败');
+  } finally {
+    isOperating.value = false;
+  }
+};
+
+// ================= 搜索与重置功能 =================
+const handleSearch = async (value) => {
+  if (isOperating.value) return;
+  isOperating.value = true;
+  
+  if (!value) {
+    handleClearSearch();
+    isOperating.value = false;
+    return;
+  }
+
+  expandedNodeIds.value.clear();
+  expandedChildrenMap.value.clear();
+  
+  try {
+    const res = await graphService.searchNodesByName(neo4j, value);
+    if (!res.nodes || res.nodes.length === 0) {
+      Message.warning('未找到相关的节点');
+      return;
+    }
+
+    res.nodes.forEach(item => {
+      const mainLabel = item.data.labels?.[0];
+      item.data.color = node_label_maps[mainLabel] || defaultNodeStyle.fill;
+    });
+
+    const newData = {
+      nodes: res.nodes,
+      edges: res.edges
+    };
+
+    rebuildGraph(newData);
+
+    if (res.edges.length > 0) {
+      Message.success(`成功渲染 ${res.nodes.length} 个节点,并建立 ${res.edges.length} 条关联线!`);
+    } else {
+      Message.success(`成功渲染 ${res.nodes.length} 个节点`);
+    }
+  } catch (err) {
+    console.error(err);
+    Message.error('数据库模糊搜索失败');
+  } finally {
+    isOperating.value = false;
+  }
+};
+const handleClearSearch = async () => {
+  if (isOperating.value) return;
+  isOperating.value = true;
+  
+  searchText.value = '';
+  expandedNodeIds.value.clear();
+  expandedChildrenMap.value.clear();
+
+  try {
+    const res = await findRootNodes(neo4j);
+    if (res && res.length) {
+      const newData = {
+        nodes: res.map((item) => ({
+          id: String(item.id),
+          data: {
+            labels: item.labels,
+            color: node_label_maps[item.labels[0]] || defaultNodeStyle.fill,
+            text: item.name,
+            elementId: item.elementId,
+            ...(item.star !== undefined && { star: item.star })
+          }
+        })),
+        edges: []
+      };
+
+      rebuildGraph(newData);
+    }
+  } catch (err) {
+    console.error(err);
+    Message.error('清除搜索失败');
+  } finally {
+    isOperating.value = false;
+  }
+};
+
+// ================= 标签筛选回调方法 =================
+const handleLabelFilter = async (label) => {
+  if (isOperating.value) return;
+  isOperating.value = true;
+  
+  try {
+    searchText.value = '';
+    expandedNodeIds.value.clear();
+    expandedChildrenMap.value.clear();
+    
+    const res = await graphService.findNodesByLabel(neo4j, label);
+    if(res && res.length){
+      const newData = {
+        nodes: res.map((item) => ({
+          id: String(item.id),
+          data: {
+            labels: item.labels,
+            color: node_label_maps[item.labels[0]] || defaultNodeStyle.fill,
+            text: item.name,
+            elementId: item.elementId,
+            ...(item.star !== undefined && { star: item.star })
+          }
+        })),
+        edges: []
+      };
+
+      rebuildGraph(newData);
+    }
+  } catch (err) {
+    console.error(err);
+    Message.error('标签筛选失败');
+  } finally {
+    isOperating.value = false;
+  }
+};
+
+// ================= 根据权重高亮节点 =================
+const handleWeightFilter = (weight) => {
+  if (!graphInstance.value) return;
+
+  if (currentHighlightWeight.value === weight) {
+    cancelHighlight();
+    return;
+  }
+
+  graphData.value.nodes.forEach(node => {
+    node.data._highlight = false;
+    node.data._dimmed = false;
+  });
+
+  const nodesWithWeight = graphData.value.nodes.filter(node => {
+    return node.data?.star === weight;
+  });
+
+  nodesWithWeight.forEach(node => {
+    node.data._highlight = true;
+  });
+
+  graphData.value.nodes.forEach(node => {
+    if (node.data?.star !== weight) {
+      node.data._dimmed = true;
+    }
+  });
+
+  currentHighlightWeight.value = weight;
+
+  const rawData = toRaw(graphData.value);
+  const pureData = JSON.parse(JSON.stringify(rawData));
+  graphInstance.value.setData(pureData);
+  graphInstance.value.render();
+};
+
+// ================= 刷新统计数据 =================
+const handleRefresh = () => {
+  Message.info('统计数据已刷新');
+};
+
+// 当前高亮的标签
+const currentHighlightLabel = ref(null);
+// 当前高亮的权重
+const currentHighlightWeight = ref(null);
+
+// ================= 根据标签高亮节点 =================
+const handleHighlightByLabel = (labelName) => {
+  if (!graphInstance.value) return;
+
+  // 如果点击的是当前已高亮的标签,则取消高亮
+  if (currentHighlightLabel.value === labelName) {
+    cancelHighlight();
+    return;
+  }
+
+  // 清除所有节点的高亮状态
+  graphData.value.nodes.forEach(node => {
+    node.data._highlight = false;
+    node.data._dimmed = false;
+  });
+
+  // 获取具有指定标签的节点
+  const nodesWithLabel = graphData.value.nodes.filter(node => {
+    const labels = node.data?.labels || [];
+    return labels.includes(labelName);
+  });
+
+  // 设置高亮状态
+  nodesWithLabel.forEach(node => {
+    node.data._highlight = true;
+  });
+
+  // 设置其他节点变暗状态
+  graphData.value.nodes.forEach(node => {
+    const labels = node.data?.labels || [];
+    if (!labels.includes(labelName)) {
+      node.data._dimmed = true;
+    }
+  });
+
+  // 记录当前高亮的标签
+  currentHighlightLabel.value = labelName;
+
+  // 使用 setData 更新数据,确保样式重新计算
+  const rawData = toRaw(graphData.value);
+  const pureData = JSON.parse(JSON.stringify(rawData));
+  graphInstance.value.setData(pureData);
+  graphInstance.value.render();
+
+  // Message.info(`已高亮 ${nodesWithLabel.length} 个 "${labelName}" 类型节点(再次点击取消高亮)`);
+};
+
+// ================= 取消高亮 =================
+const cancelHighlight = () => {
+  // 清除所有节点的高亮状态
+  graphData.value.nodes.forEach(node => {
+    node.data._highlight = false;
+    node.data._dimmed = false;
+  });
+
+  // 重置当前高亮标签和权重
+  currentHighlightLabel.value = null;
+  currentHighlightWeight.value = null;
+
+  // 使用 setData 更新数据,确保样式重新计算
+  const rawData = toRaw(graphData.value);
+  const pureData = JSON.parse(JSON.stringify(rawData));
+  graphInstance.value.setData(pureData);
+  graphInstance.value.render();
+
+  Message.info('已取消高亮');
+};
+
+// 暴露方法供父组件调用
+defineExpose({
+  handleExternalRemove
+});
+</script>
+
+<style scoped>
+.graph-wrapper {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  background: #ffffff;
+  display: flex;
+}
+
+.tools {
+  position: absolute;
+  top: 16px;
+  left: 16px;
+  z-index: 10;
+  display: flex;
+  align-items: center;
+}
+
+.search-input {
+  width: 260px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+  border-radius: 6px;
+  overflow: hidden;
+}
+
+.layout-select {
+  width: 160px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+  border-radius: 6px;
+  overflow: hidden;
+}
+
+.net-mode-switch {
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+  border-radius: 6px;
+}
+
+.layout-select :deep(.arco-select-view-single) {
+  background: #fff;
+  border: 1px solid #e5e6eb;
+  border-radius: 6px;
+  transition: all 0.2s ease;
+}
+
+.layout-select :deep(.arco-select-view-single:hover) {
+  border-color: #165dff;
+}
+
+.layout-select :deep(.arco-select-view-focused) {
+  border-color: #165dff;
+  box-shadow: 0 0 0 3px rgba(22, 93, 255, 0.1);
+}
+
+.search-input :deep(.arco-input-wrapper) {
+  background: #fff;
+  border: 1px solid #e5e6eb;
+  border-radius: 6px;
+  transition: all 0.2s ease;
+}
+
+.search-input :deep(.arco-input-wrapper:hover) {
+  border-color: #165dff;
+}
+
+.search-input :deep(.arco-input-wrapper:focus-within) {
+  border-color: #165dff;
+  box-shadow: 0 0 0 3px rgba(22, 93, 255, 0.1);
+}
+
+.search-input :deep(.arco-input) {
+  font-size: 14px;
+}
+
+.search-input :deep(.arco-btn) {
+  background: #165dff;
+  border-color: #165dff;
+}
+
+.search-input :deep(.arco-btn:hover) {
+  background: #4080ff;
+  border-color: #4080ff;
+}
+
+#graph-container {
+  width: 100%;
+  height: 100%;
+}
+
+.layout-btn {
+  position: absolute;
+  top: 16px;
+  left: 16px;
+  z-index: 10;
+}
+
+/* 自定义右键菜单样式 */
+.custom-context-menu {
+  position: fixed;
+  z-index: 1000;
+  background: #fff;
+  border: 1px solid #e5e6eb;
+  border-radius: 6px;
+  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
+  padding: 5px 0;
+  min-width: 160px;
+}
+
+.menu-item {
+  padding: 10px 16px;
+  cursor: pointer;
+  font-size: 13px;
+  color: #1d2129;
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.menu-item:hover {
+  background: #f2f3f5;
+  color: #165dff;
+}
+
+/* 抽屉溯源卡片样式 */
+.source-container {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+  padding: 4px;
+}
+
+.source-card {
+  background: #ffffff;
+  border: 1px solid #eef0f2;
+  border-radius: 12px;
+  padding: 0;
+  transition: all 0.3s ease;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
+  overflow: hidden;
+}
+
+.source-card:hover {
+  border-color: #165dff;
+  box-shadow: 0 4px 12px rgba(22, 93, 255, 0.1);
+  transform: translateY(-2px);
+}
+
+.card-header {
+  background: #f8f9fb;
+  padding: 10px 16px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  border-bottom: 1px solid #f2f3f5;
+}
+
+.location-tag {
+  font-size: 12px;
+  font-weight: bold;
+  color: #165dff;
+  background: #e8f3ff;
+  padding: 2px 8px;
+  border-radius: 4px;
+}
+
+.relation-badge {
+  font-size: 12px;
+  color: #86909c;
+}
+
+.card-body {
+  padding: 16px;
+}
+
+.sentence-quote {
+  font-size: 14px;
+  font-weight: 500;
+  color: #1d2129;
+  margin-bottom: 12px;
+  line-height: 1.6;
+  border-left: 3px solid #165dff;
+  padding-left: 12px;
+}
+
+.full-text {
+  font-size: 13px;
+  color: #4e5969;
+  line-height: 1.8;
+  background: #fdfdfd;
+  padding: 10px;
+  border-radius: 6px;
+}
+
+.text-label {
+  font-size: 11px;
+  color: #86909c;
+  margin-bottom: 4px;
+  text-transform: uppercase;
+}
+
+/* 覆盖层组件定位挂载修正 */
+:deep(.g6-minimap) {
+  position: absolute !important;
+  bottom: 24px !important;
+  right: 24px !important;
+  top: auto !important;
+  left: auto !important;
+  background: #fff;
+  border: 1px solid #e5e6eb;
+  border-radius: 4px;
+  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
+  z-index: 10;
+}
+
+:deep(.g6-toolbar) {
+  position: absolute !important;
+  top: 16px !important;
+  right: 24px !important;
+  left: auto !important;
+  z-index: 10;
+  background: #fff;
+  padding: 4px;
+  border-radius: 4px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.node-name {
+  font-size: 14px;
+  font-weight: 600;
+  color: #1d2129;
+  margin-left: 8px;
+}
+
+.path-info {
+  font-size: 13px;
+  color: #86909c;
+  margin-bottom: 8px;
+  padding: 8px;
+  background: #f7f8fa;
+  border-radius: 4px;
+  word-break: break-all;
+}
+
+.file-preview-section {
+  margin-top: 24px;
+  padding-top: 20px;
+  border-top: 1px solid #e5e6eb;
+}
+
+.section-title {
+  font-size: 14px;
+  font-weight: 600;
+  color: #1d2129;
+  margin-bottom: 12px;
+}
+
+.preview-card {
+  display: flex;
+  align-items: center;
+  padding: 12px;
+  background: #f7f8fa;
+  border-radius: 8px;
+  margin-bottom: 10px;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+
+.preview-card:hover {
+  background: #f0f1f5;
+  transform: translateX(4px);
+}
+
+.preview-icon {
+  font-size: 28px;
+  margin-right: 12px;
+}
+
+.video-icon {
+  color: #165dff;
+}
+
+.ppt-icon {
+  color: #f58b55;
+}
+
+.preview-icon.disabled {
+  color: #c9cdd4;
+}
+
+.preview-info {
+  flex: 1;
+}
+
+.preview-name {
+  font-size: 14px;
+  font-weight: 500;
+  color: #1d2129;
+  margin-bottom: 2px;
+}
+
+.preview-size {
+  font-size: 12px;
+  color: #86909c;
+}
+
+.preview-action {
+  font-size: 20px;
+  color: #165dff;
+  margin-left: 8px;
+  cursor: pointer;
+  transition: color 0.2s;
+}
+
+.preview-action:hover {
+  color: #4080ff;
+}
+
+.edit-action {
+  color: #86909c;
+}
+
+.edit-action:hover {
+  color: #165dff;
+}
+
+.delete-action {
+  color: #86909c;
+}
+
+.delete-action:hover {
+  color: #f53f3f;
+}
+
+.preview-actions {
+  display: flex;
+  align-items: center;
+}
+
+.disabled-card {
+  cursor: default;
+  opacity: 0.6;
+}
+
+.disabled-card:hover {
+  background: #f7f8fa;
+  transform: none;
+}
+
+.delete-warning {
+  color: #f53f3f;
+  font-size: 12px;
+  margin-top: 8px;
+}
+
+.edit-file-name-input {
+  width: 100%;
+}
+
+.edit-file-suffix {
+  margin-top: 8px;
+  font-size: 13px;
+  color: #86909c;
+}
+
+</style>

+ 143 - 0
src/views/graph-view/HeaderView.vue

@@ -0,0 +1,143 @@
+<!-- HeaderView.vue -->
+<template>
+  <div class="header-view">
+    <div class="logo-area">
+      <icon-network class="logo-icon" />
+      <span class="title">多维度知识图谱数据挖掘分析系统</span>
+    </div>
+    
+    <!-- 模式切换按钮组 -->
+    <div class="mode-switch">
+      <a-radio-group v-model="currentMode" type="button" @change="handleModeChange">
+        <a-radio value="analysis">
+          <icon-bar-chart class="mode-icon" />
+          智能分析模式
+        </a-radio>
+        <a-radio value="compose">
+          <icon-file class="mode-icon" />
+          智能组卷模式
+        </a-radio>
+      </a-radio-group>
+    </div>
+    
+    <div class="actions">
+      <a-button type="text" @click="$emit('refresh')" class="action-btn">
+        <icon-refresh-cw />
+      </a-button>
+    </div>
+  </div>
+</template>
+
+<script setup lang="js">
+import { ref,watch } from 'vue';
+import { useModeStore } from '../../base/store';
+
+const emit = defineEmits(['refresh', 'modeChange']);
+const modeStore = useModeStore();
+// 当前模式,默认智能分析模式
+const currentMode = ref('analysis');
+
+// 模式切换处理
+const handleModeChange = (value) => {
+  
+  modeStore.updateMode(value);
+  // emit('modeChange', value);
+};
+// watch(()=>currentMode.value, (newVal, oldVal) => {
+//   // emit('modeChange', newVal);
+//   sessionStorage.setItem('currentMode', newVal);
+// },{
+//   immediate: true
+// })
+</script>
+
+<style scoped>
+.header-view {
+  height: 64px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: #fff;
+  display: flex;
+  align-items: center;
+  padding: 0 24px;
+  justify-content: space-between;
+  box-shadow: 0 4px 20px rgba(102, 126, 234, 0.3);
+  z-index: 20;
+}
+
+.logo-area {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.logo-icon {
+  font-size: 24px;
+  opacity: 0.9;
+}
+
+.title {
+  font-size: 18px;
+  font-weight: 600;
+  letter-spacing: 1.5px;
+}
+
+/* 模式切换样式 */
+.mode-switch {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+}
+
+.mode-switch :deep(.arco-radio-group-button) {
+  background: rgba(255, 255, 255, 0.1);
+  padding: 4px;
+  border-radius: 8px;
+  border: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+.mode-switch :deep(.arco-radio-button) {
+  color: rgba(255, 255, 255, 0.8);
+  background: transparent;
+  border: none;
+  padding: 8px 16px;
+  border-radius: 6px;
+  font-size: 14px;
+  font-weight: 500;
+  transition: all 0.3s ease;
+}
+
+.mode-switch :deep(.arco-radio-button:hover) {
+  color: #fff;
+  background: rgba(255, 255, 255, 0.1);
+}
+
+.mode-switch :deep(.arco-radio-button.arco-radio-checked) {
+  color: #667eea;
+  background: rgba(255, 255, 255, 0.95);
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+}
+
+.mode-switch :deep(.arco-radio-button.arco-radio-checked .mode-icon) {
+  color: #667eea;
+}
+
+.mode-icon {
+  margin-right: 6px;
+  font-size: 16px;
+}
+
+.actions {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.action-btn {
+  color: rgba(255, 255, 255, 0.85);
+}
+
+.action-btn:hover {
+  color: #fff;
+  background: rgba(255, 255, 255, 0.15);
+}
+</style>

+ 183 - 0
src/views/graph-view/LableSelected.vue

@@ -0,0 +1,183 @@
+<template>
+  <div class="sidebar-wrapper" :class="{ collapsed: sidebarCollapsed }">
+    <div class="sidebar-toggle" @click="sidebarCollapsed = !sidebarCollapsed">
+      <!-- <icon-chevron-left v-if="!sidebarCollapsed" />
+      <icon-chevron-right v-else /> -->
+      <span title="收起"><icon-import v-if="!sidebarCollapsed" /></span>
+      <span title="展开"><icon-export v-if="sidebarCollapsed" /></span>
+    </div>
+    
+    <div class="sidebar-content" v-show="!sidebarCollapsed">
+      <div class="sidebar-header">
+        <icon-filter class="header-icon" />
+        <span class="header-title">节点筛选</span>
+      </div>
+      
+      <div class="sidebar-body">
+        <div class="label-tags">
+          <button 
+            v-for="label in labelFilters" 
+            :key="label.name"
+            class="label-tag"
+            :style="{ backgroundColor: label.color + '20', borderColor: label.color, color: label.color }"
+            @click="toggleLabelFilter(label.name)"
+          >
+            <div class="color-dot" :style="{ backgroundColor: label.color }"></div>
+            <span>{{ label.label }}</span>
+          </button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="js">
+import { ref, watch } from 'vue';
+import { Message } from '@arco-design/web-vue';
+import { graphService } from './graph'
+
+const props = defineProps({
+  neo4j: {
+    type: Object,
+    required: true
+  },
+  onFilter: {
+    type: Function,
+    default: () => {}
+  }
+});
+
+const emit = defineEmits(['filterNode']);
+
+// 侧边栏状态
+const sidebarCollapsed = ref(false);
+const activeLabels = ref([]);
+const labelFilters = ref([
+  { name: 'Book', label: '书籍', color: '#E3A3B8' },
+  { name: 'Part', label: '部分', color: '#F9C351' },
+  { name: 'Chapter', label: '章节', color: '#F58B55'},
+  // { name: 'EntityType', label: '实体类型', color: '#F15D5D', count: 0 },
+  // { name: 'Entity', label: '实体', color: '#CD96CD', count: 0 },
+]);
+
+// 切换标签筛选
+const toggleLabelFilter = (labelName) => {
+  emit('filterNode', labelName);
+};
+
+
+</script>
+
+<style scoped lang="css">
+/* 悬浮窗容器 */
+.sidebar-wrapper {
+  position: absolute;
+  top: 80px;
+  left: 16px;
+  z-index: 20;
+  display: flex;
+  transition: all 0.3s ease;
+}
+
+/* 收起/展开按钮 */
+.sidebar-toggle {
+  position: absolute;
+  right: -24px;
+  top: 50%;
+  transform: translateY(-50%);
+  width: 24px;
+  height: 48px;
+  background: #fff;
+  border: 1px solid #e5e6eb;
+  border-left: none;
+  border-radius: 0 4px 4px 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  color: #86909c;
+  transition: all 0.2s ease;
+  z-index: 1;
+}
+
+.sidebar-toggle:hover {
+  background: #f2f3f5;
+  color: #165dff;
+}
+
+/* 侧边栏内容面板 */
+.sidebar-content {
+  background: #fff;
+  border-radius: 12px;
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+  overflow: hidden;
+  min-width: 240px;
+  transition: all 0.3s ease;
+}
+
+.sidebar-wrapper.collapsed .sidebar-content {
+  width: 0;
+  padding: 0;
+  overflow: hidden;
+}
+
+/* 头部 */
+.sidebar-header {
+  padding: 16px 20px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.header-icon {
+  color: #fff;
+  font-size: 18px;
+}
+
+.header-title {
+  color: #fff;
+  font-size: 15px;
+  font-weight: 600;
+}
+
+/* 主体内容 */
+.sidebar-body {
+  padding: 16px 20px;
+}
+
+/* 标签容器 */
+.label-tags {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+}
+
+/* 标签按钮 */
+.label-tag {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 8px 14px;
+  border: 1px solid;
+  border-radius: 20px;
+  background: transparent;
+  font-size: 13px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.2s ease;
+}
+
+.label-tag:hover {
+  opacity: 0.7;
+  transform: translateY(-1px);
+}
+
+/* 颜色圆点 */
+.color-dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  flex-shrink: 0;
+}
+</style>

+ 394 - 0
src/views/graph-view/NodeCountStatisticize.vue

@@ -0,0 +1,394 @@
+<template>
+  <div class="statistics-wrapper" :class="{ collapsed: isCollapsed }">
+    <div class="statistics-toggle" @click="isCollapsed = !isCollapsed">
+
+      <span title="收起"><icon-export v-if="!isCollapsed" /></span>
+      <span title="展开"><icon-import v-if="isCollapsed" /></span>
+    </div>
+    
+    <div class="statistics-panel" v-show="!isCollapsed">
+      <div class="panel-header">
+        <icon-bar-chart class="header-icon" />
+        <span class="header-title">节点统计</span>
+        <span class="total-count">共 {{ totalCount }} 个节点</span>
+      </div>
+      
+      <!-- 操作提示 -->
+      <div class="action-tip">
+        <icon-bulb size="16"/>
+        <span>点击统计项高亮对应节点</span>
+      </div>
+      
+      <div class="statistics-content">
+        <div 
+          v-for="(stat, label) in statistics" 
+          :key="label"
+          class="stat-item"
+          @click="handleStatClick(stat.name)"
+        >
+          <div class="stat-header">
+            <div class="color-dot" :style="{ backgroundColor: stat.color }"></div>
+            <span class="stat-label">{{ stat.label }}</span>
+            <span class="stat-count">{{ stat.count }}</span>
+          </div>
+          <div class="progress-bar">
+            <div 
+              class="progress-fill" 
+              :style="{ 
+                width: stat.percentage + '%', 
+                backgroundColor: stat.color 
+              }"
+            ></div>
+          </div>
+          <span class="stat-percentage">{{ stat.percentage.toFixed(1) }}%</span>
+        </div>
+        
+        <div v-if="statistics.length === 0" class="empty-state">
+          <icon-inbox />
+          <span>暂无数据</span>
+        </div>
+      </div>
+      
+      <div class="panel-footer">
+        <button class="cancel-highlight-btn" @click="$emit('cancelHighlight')">
+          <icon-highlight-off /> 取消高亮
+        </button>
+        <button class="refresh-btn" @click="$emit('refresh')">
+          <icon-refresh-cw /> 刷新
+        </button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="js">
+import { ref, computed } from 'vue';
+
+const props = defineProps({
+  nodes: {
+    type: Array,
+    default: () => []
+  }
+});
+
+const emit = defineEmits(['refresh', 'highlight', 'cancelHighlight']);
+
+const isCollapsed = ref(false);
+
+// 点击统计项时触发高亮
+const handleStatClick = (labelName) => {
+  // console.log(labelName)
+  emit('highlight', labelName);
+};
+
+const node_label_maps = {
+  'Book': '#E3A3B8',
+  'Part': '#F9C351',
+  'Chapter': '#F58B55',
+  "Section": "rgba(102, 191, 102, 0.8)",
+  "Mu": "rgba(99, 179, 237, 0.8)",
+  'EntityType': '#F15D5D',
+  'Entity': '#CD96CD'
+};
+
+const label_name_maps = {
+  'Book': '书籍',
+  'Part': '部分',
+  'Chapter': '章节',
+  'EntityType': '实体类型',
+  'Entity': '实体'
+};
+
+const totalCount = computed(() => props.nodes.length);
+
+const statistics = computed(() => {
+  if (!props.nodes || props.nodes.length === 0) {
+    return [];
+  }
+
+  const labelCounts = {};
+  
+  props.nodes.forEach(node => {
+    const labels = node.data?.labels || [];
+    const mainLabel = labels[0];
+    if (mainLabel) {
+      labelCounts[mainLabel] = (labelCounts[mainLabel] || 0) + 1;
+    }
+  });
+
+  return Object.entries(labelCounts)
+    .map(([name, count]) => ({
+      label: label_name_maps[name] || name,
+      name: name,
+      count: count,
+      color: node_label_maps[name] || '#999999',
+      percentage: (count / props.nodes.length) * 100
+    }))
+    .sort((a, b) => b.count - a.count);
+});
+</script>
+
+<style scoped lang="css">
+/* 悬浮窗容器 */
+.statistics-wrapper {
+  position: absolute;
+  top: 80px;
+  right: 16px;
+  z-index: 20;
+  display: flex;
+  transition: all 0.3s ease;
+}
+
+/* 收起/展开按钮 */
+.statistics-toggle {
+  position: absolute;
+   left: -24px;  /* 箭头方向调整 */
+  right: auto;
+  top: 50%;
+  transform: translateY(-50%);
+  width: 24px;
+  height: 48px;
+  background: #fff;
+  border: 1px solid #e5e6eb;
+  /* border-left: none; */
+  border-radius: 0 4px 4px 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  color: #86909c;
+  transition: all 0.2s ease;
+  z-index: 1;
+
+  border-left: 1px solid #e5e6eb;
+  border-right: none;
+  border-radius: 4px 0 0 4px;
+}
+
+.statistics-toggle:hover {
+  background: #f2f3f5;
+  color: #165dff;
+}
+
+/* 统计面板 */
+.statistics-panel {
+  background: #fff;
+  border-radius: 12px;
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+  overflow: hidden;
+  min-width: 240px;
+  transition: all 0.3s ease;
+}
+
+.statistics-wrapper.collapsed .statistics-panel {
+  width: 0;
+  padding: 0;
+  overflow: hidden;
+}
+
+.panel-header {
+  padding: 16px 20px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.header-icon {
+  color: #fff;
+  font-size: 18px;
+}
+
+.header-title {
+  color: #fff;
+  font-size: 15px;
+  font-weight: 600;
+}
+
+.total-count {
+  margin-left: auto;
+  color: rgba(255, 255, 255, 0.85);
+  font-size: 13px;
+  background: rgba(255, 255, 255, 0.2);
+  padding: 4px 12px;
+  border-radius: 20px;
+}
+
+/* 操作提示 */
+.action-tip {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 10px 16px;
+  background: #fffbe6;
+  border-bottom: 1px solid #ffe58f;
+  font-size: 12px;
+  color: #d48806;
+}
+
+.action-tip .tip-icon {
+  font-size: 14px;
+}
+
+.statistics-content {
+  padding: 16px 20px;
+  max-height: 390px;
+  overflow-y: auto;
+}
+
+.stat-item {
+  margin-bottom: 16px;
+  cursor: pointer;
+  /* padding: 8px; */
+  border-radius: 8px;
+  transition: all 0.2s ease;
+  position: relative;
+}
+
+.stat-item:hover {
+  background: #f7f8fa;
+  transform: translateX(4px);
+}
+
+.stat-item::after {
+  content: '';
+  position: absolute;
+  right: 0;
+  top: 50%;
+  transform: translateY(-50%);
+  width: 6px;
+  height: 6px;
+  border-top: 2px solid #c0c4cc;
+  border-right: 2px solid #c0c4cc;
+  transform: translateY(-50%) rotate(45deg);
+  opacity: 0.5;
+  transition: all 0.2s ease;
+}
+
+.stat-item:hover::after {
+  opacity: 1;
+  right: 8px;
+}
+
+.stat-item:last-child {
+  margin-bottom: 0;
+}
+
+.stat-header {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-bottom: 8px;
+}
+
+.color-dot {
+  width: 10px;
+  height: 10px;
+  border-radius: 50%;
+  flex-shrink: 0;
+}
+
+.stat-label {
+  font-size: 13px;
+  color: #4e5969;
+  flex: 1;
+}
+
+.stat-count {
+  font-size: 14px;
+  font-weight: 600;
+  color: #1d2129;
+}
+
+.progress-bar {
+  height: 6px;
+  background: #f2f3f5;
+  border-radius: 3px;
+  overflow: hidden;
+}
+
+.progress-fill {
+  height: 100%;
+  border-radius: 3px;
+  transition: width 0.3s ease;
+}
+
+.stat-percentage {
+  display: block;
+  text-align: right;
+  font-size: 12px;
+  color: #86909c;
+  margin-top: 4px;
+}
+
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 40px 20px;
+  color: #86909c;
+}
+
+.empty-state icon {
+  font-size: 40px;
+  margin-bottom: 12px;
+  opacity: 0.5;
+}
+
+.empty-state span {
+  font-size: 14px;
+}
+
+.panel-footer {
+  padding: 12px 20px;
+  border-top: 1px solid #f2f3f5;
+  display: flex;
+  justify-content: flex-end;
+  gap: 8px;
+}
+
+.cancel-highlight-btn {
+  display: flex;
+  align-items: center;
+  gap: 5px;
+  padding: 6px 12px;
+  border: 1px solid #e5e6eb;
+  border-radius: 20px;
+  background: #f7f8fa;
+  font-size: 12px;
+  color: #86909c;
+  cursor: pointer;
+  transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
+  opacity: 0.8;
+}
+
+.cancel-highlight-btn:hover {
+  background: #fff;
+  border-color: #c0c4cc;
+  color: #4e5969;
+  opacity: 1;
+  transform: translateY(-1px);
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+}
+
+.refresh-btn {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 6px 14px;
+  border: 1px solid #e5e6eb;
+  border-radius: 6px;
+  background: #fff;
+  font-size: 12px;
+  color: #4e5969;
+  cursor: pointer;
+  transition: all 0.2s ease;
+}
+
+.refresh-btn:hover {
+  background: #f2f3f5;
+  border-color: #165dff;
+  color: #165dff;
+}
+</style>

+ 604 - 0
src/views/graph-view/PPTView.vue

@@ -0,0 +1,604 @@
+<template>
+  <a-modal
+    :visible="isVisible"
+    :title="'PPT预览:' + fileName"
+    :footer="null"
+    :width="modalWidth"
+    :closable="true"
+    :bodyStyle="{ padding: '0', height: 'calc(100vh - 80px)', overflow: 'hidden' }"
+    :maskClosable="false"
+    @cancel="handleClose"
+    @update:visible="handleUpdateVisible"
+    class="ppt-modal"
+  >
+    <div 
+      class="ppt-container" 
+      :class="{ 'ppt-fullscreen': isFullscreen }" 
+      ref="pptContainerRef"
+      @mousemove="handleMouseMove"
+    >
+      <div class="ppt-toolbar" :class="{ 'toolbar-hidden': isFullscreen && !showToolbar }">
+        <div class="toolbar-left">
+          <span class="file-info">{{ fileName }}</span>
+        </div>
+        <div class="toolbar-center">
+          <!-- <a-button type="text" :disabled="currentSlide <= 1" @click="prevSlide" class="nav-btn">
+            <icon-left />
+          </a-button>
+          <span class="page-indicator">
+            {{ currentSlide }} / {{ totalSlides }}
+          </span>
+          <a-button type="text" :disabled="currentSlide >= totalSlides" @click="nextSlide" class="nav-btn">
+            <icon-right />
+          </a-button> -->
+        </div>
+        <div class="toolbar-right">
+          <a-button  @click="zoomOut" class="zoom-btn">
+            <span style="font-size: 20px;">-</span>
+          </a-button>
+          <span class="zoom-value">{{ Math.round(zoomLevel * 100) }}%</span>
+          <a-button  @click="zoomIn" class="zoom-btn">
+            <span style="font-size: 20px;">+</span>
+          </a-button>
+          <!-- <a-button type="primary" @click="fitToScreen" class="zoom-btn"> -->
+            <img :src="fitIcon" alt="适配屏幕" style="width: 20px; height: 20px;" @click="fitToScreen" class="zoom-btn zoom-img-btn" />
+          <!-- </a-button> -->
+          <a-button type="primary" @click="toggleFullscreen" class="fullscreen-btn">
+            <icon-fullscreen-exit v-if="isFullscreen" />
+            <icon-fullscreen v-else />
+            {{ isFullscreen ? '退出全屏' : '全屏' }}
+          </a-button>
+          <a-button type="text" @click="handleDownload" class="download-btn">
+            <icon-download /> 下载
+          </a-button>
+        </div>
+      </div>
+
+      <div class="ppt-main-area" ref="mainAreaRef">
+        <div v-if="loading" class="ppt-loading">
+          <a-spin size="large" tip="加载中..." />
+        </div>
+        <div v-else-if="previewError" class="ppt-preview-error">
+          <icon-warning-circle class="error-icon" />
+          <p class="error-text">{{ previewError }}</p>
+          <a-button type="primary" @click="handleDownload" class="retry-btn">
+            <icon-download /> 下载查看
+          </a-button>
+        </div>
+        <div v-else class="ppt-preview-wrapper">
+          <div class="ppt-scroll-container">
+            <div 
+              class="ppt-viewport" 
+              :style="{ transform: `scale(${zoomLevel})`, transformOrigin: 'center center' }"
+            >
+              <vue-office-pptx 
+                :src="pptData" 
+                @error="handlePreviewError"
+                @rendered="handleRendered"
+                class="ppt-component"
+              />
+            </div>
+          </div>
+          
+          <!-- <div class="ppt-nav-overlay">
+            <div class="nav-arrow nav-prev" :class="{ disabled: currentSlide <= 1 }" @click="prevSlide">
+              <icon-left />
+            </div>
+            <div class="nav-arrow nav-next" :class="{ disabled: currentSlide >= totalSlides }" @click="nextSlide">
+              <icon-right />
+            </div>
+          </div> -->
+        </div>
+      </div>
+
+      <div class="ppt-footer" :class="{ 'footer-hidden': isFullscreen }">
+        <div class="footer-info">
+          <span class="info-item">
+            <icon-file-text /> {{ fileName }}
+          </span>
+          <span class="info-item">
+            <icon-database /> {{ formatFileSize(fileSize) }}
+          </span>
+          <span class="info-item">
+            <icon-clock /> {{ formatTime(createdTime) }}
+          </span>
+        </div>
+        <div class="footer-tips">
+          <span>快捷键:ESC 退出全屏 | + - 缩放</span>
+        </div>
+      </div>
+    </div>
+  </a-modal>
+</template>
+
+<script setup lang="js">
+import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue';
+import VueOfficePptx from '@vue-office/pptx';
+import fitIcon from '../../assets/fit.svg';
+import { fileStorageService } from '../../utils/fileStorage'; // 请确保路径正确
+import { formatTime } from '../../utils/util'
+
+const props = defineProps({
+  visible: Boolean,
+  nodeId: String,
+  fileRecord: Object
+});
+
+const emit = defineEmits(['update:visible']);
+
+const isVisible = computed(() => props.visible);
+const fileName = ref('');
+const fileSize = ref(0);
+const createdTime = ref('');
+const pptData = ref(null);
+const loading = ref(false);
+const previewError = ref('');
+const isFullscreen = ref(false);
+const pptContainerRef = ref(null);
+const mainAreaRef = ref(null); // 新增:用于计算内容区高宽
+const showToolbar = ref(true);
+const currentSlide = ref(1);
+const totalSlides = ref(1);
+const zoomLevel = ref(1);
+const modalWidth = ref('80vw');
+
+let toolbarTimer = null;
+let resizeObserver = null;
+
+const formatFileSize = (size) => {
+  if (!size) return '0 B';
+  const units = ['B', 'KB', 'MB', 'GB'];
+  let i = 0;
+  let num = size;
+  while (num >= 1024 && i < units.length - 1) {
+    num /= 1024;
+    i++;
+  }
+  return num.toFixed(2) + ' ' + units[i];
+};
+
+const loadPPTInfo = async () => {
+  if (!props.visible || !props.fileRecord) return;
+  
+  fileName.value = props.fileRecord.original_name || props.fileRecord.file_name;
+  fileSize.value = props.fileRecord.file_size;
+  createdTime.value = props.fileRecord.created_at;
+  
+  loading.value = true;
+  previewError.value = '';
+  currentSlide.value = 1;
+  totalSlides.value = 1;
+  zoomLevel.value = 1;
+  
+  try {
+    const file = await fileStorageService.readFileFromOPFS(props.fileRecord.file_path);
+    pptData.value = await file.arrayBuffer();
+  } catch (error) {
+    console.error('加载PPT文件失败:', error);
+    previewError.value = '无法加载PPT文件,请尝试下载查看';
+  } finally {
+    loading.value = false;
+  }
+};
+
+const handleRendered = () => {
+  detectSlides();
+};
+
+// 探测幻灯片并初始化样式
+const detectSlides = () => {
+  setTimeout(() => {
+    const slideContainer = document.querySelector('.vue-office-pptx .wrapper'); 
+    // 注意:@vue-office/pptx 最新版内部结构可能是 .wrapper 或 .docx-wrapper,这里兼顾处理
+    const targetContainer = slideContainer || document.querySelector('.vue-office-pptx');
+    
+    if (targetContainer) {
+      // 提取所有单页 slide 元素
+      const slides = targetContainer.children;
+      totalSlides.value = slides.length || 1;
+      
+      // 初始化控制单页显示
+      updateSlideVisibility();
+      // 自动缩放自适应当前屏幕
+      fitToScreen();
+    }
+  }, 300);
+};
+
+// 关键控制:只显示当前页,隐藏其他页(模拟课件翻页)
+const updateSlideVisibility = () => {
+  const slideContainer = document.querySelector('.vue-office-pptx .wrapper') || document.querySelector('.vue-office-pptx');
+  if (slideContainer && slideContainer.children.length > 0) {
+    Array.from(slideContainer.children).forEach((slide, index) => {
+      if (index === currentSlide.value - 1) {
+        slide.style.display = 'block';
+        slide.style.margin = '0 auto';
+      } else {
+        slide.style.display = 'none';
+      }
+    });
+  }
+};
+
+const handlePreviewError = (error) => {
+  console.error('PPT预览错误:', error);
+  previewError.value = 'PPT预览失败,请尝试下载查看';
+};
+
+const handleDownload = async () => {
+  if (!props.fileRecord) return;
+  try {
+    const file = await fileStorageService.readFileFromOPFS(props.fileRecord.file_path);
+    const url = URL.createObjectURL(file);
+    const link = document.createElement('a');
+    link.href = url;
+    link.download = props.fileRecord.original_name || props.fileRecord.file_name;
+    link.click();
+    URL.revokeObjectURL(url);
+  } catch (error) {
+    console.error('下载文件失败:', error);
+  }
+};
+
+const prevSlide = () => {
+  if (currentSlide.value > 1) {
+    currentSlide.value--;
+    updateSlideVisibility();
+    nextTick(() => fitToScreen());
+  }
+};
+
+const nextSlide = () => {
+  if (currentSlide.value < totalSlides.value) {
+    currentSlide.value++;
+    updateSlideVisibility();
+    nextTick(() => fitToScreen());
+  }
+};
+
+const zoomIn = () => {
+  if (zoomLevel.value < 3) {
+    zoomLevel.value = Math.round((zoomLevel.value + 0.1) * 10) / 10;
+  }
+};
+
+const zoomOut = () => {
+  if (zoomLevel.value > 0.3) {
+    zoomLevel.value = Math.round((zoomLevel.value - 0.1) * 10) / 10;
+  }
+};
+
+// 核心自适应逻辑:计算最佳缩放比,让PPT完美塞满当前容器
+const fitToScreen = () => {
+  if (!mainAreaRef.value) return;
+  
+  // 获取单页 Slide 的原生尺寸
+  const activeSlide = document.querySelector('.vue-office-pptx .wrapper > div[style*="display: block"]') 
+                     || document.querySelector('.vue-office-pptx > div');
+                     
+  if (!activeSlide) return;
+
+  // 获取外层容器的高宽(扣除 padding 后的可用大小)
+  const containerWidth = mainAreaRef.value.clientWidth - 40;
+  const containerHeight = mainAreaRef.value.clientHeight - 40;
+
+  // 获取 PPT 单页的实际高宽
+  const slideWidth = activeSlide.offsetWidth || 960; 
+  const slideHeight = activeSlide.offsetHeight || 540;
+
+  // 计算宽和高的缩放比例,取较小值以保证整页都能完整显示
+  const scaleX = containerWidth / slideWidth;
+  const scaleY = containerHeight / slideHeight;
+  
+  zoomLevel.value = Math.min(scaleX, scaleY, 1.5); // 最大不放大超过 1.5 倍以免失真
+};
+
+const toggleFullscreen = async () => {
+  if (!pptContainerRef.value) return;
+  try {
+    if (!document.fullscreenElement) {
+      await pptContainerRef.value.requestFullscreen();
+    } else {
+      await document.exitFullscreen();
+    }
+  } catch (error) {
+    console.error('全屏切换失败:', error);
+  }
+};
+
+const handleFullscreenChange = () => {
+  isFullscreen.value = !!document.fullscreenElement;
+  modalWidth.value = isFullscreen.value ? '100vw' : '80vw';
+  showToolbar.value = true;
+  if (isFullscreen.value) {
+    scheduleToolbarHide();
+    setTimeout(() => {
+      zoomLevel.value = 1.9;
+    }, 100);
+  } else {
+    setTimeout(() => {
+      fitToScreen();
+    }, 100);
+  }
+};
+
+const scheduleToolbarHide = () => {
+  if (toolbarTimer) clearTimeout(toolbarTimer);
+  toolbarTimer = setTimeout(() => {
+    showToolbar.value = false;
+  }, 3000);
+};
+
+const handleMouseMove = () => {
+  if (isFullscreen.value) {
+    showToolbar.value = true;
+    scheduleToolbarHide();
+  }
+};
+
+const handleKeydown = (event) => {
+  if (!isVisible.value) return;
+  switch (event.key) {
+    case 'ArrowLeft':
+      event.preventDefault();
+      prevSlide();
+      break;
+    case 'ArrowRight':
+      event.preventDefault();
+      nextSlide();
+      break;
+    case 'Escape':
+      if (document.fullscreenElement) {
+        event.preventDefault();
+        document.exitFullscreen().catch(() => {});
+      }
+      break;
+    case '+':
+    case '=':
+      event.preventDefault();
+      zoomIn();
+      break;
+    case '-':
+      event.preventDefault();
+      zoomOut();
+      break;
+    case '0':
+      event.preventDefault();
+      fitToScreen();
+      break;
+  }
+};
+
+const handleClose = () => {
+  if (document.fullscreenElement) {
+    document.exitFullscreen().catch(() => {});
+  }
+  emit('update:visible', false);
+};
+
+const handleUpdateVisible = (val) => {
+  if (!val && document.fullscreenElement) {
+    document.exitFullscreen().catch(() => {});
+  }
+  emit('update:visible', val);
+};
+
+watch(() => props.visible, (newVal) => {
+  if (newVal) {
+    loadPPTInfo();
+  }
+});
+
+onMounted(() => {
+  document.addEventListener('fullscreenchange', handleFullscreenChange);
+  document.addEventListener('keydown', handleKeydown);
+  
+  // 监听窗口或容器大小变化,自动重新计算 PPT 大小
+  if (mainAreaRef.value) {
+    resizeObserver = new ResizeObserver(() => {
+      fitToScreen();
+    });
+    resizeObserver.observe(mainAreaRef.value);
+  }
+});
+
+onUnmounted(() => {
+  document.removeEventListener('fullscreenchange', handleFullscreenChange);
+  document.removeEventListener('keydown', handleKeydown);
+  if (document.fullscreenElement) {
+    document.exitFullscreen().catch(() => {});
+  }
+  if (toolbarTimer) clearTimeout(toolbarTimer);
+  if (resizeObserver) resizeObserver.disconnect();
+});
+</script>
+
+<style scoped lang="css">
+/* 保留你原有的主题样式... */
+.ppt-modal :deep(.ant-modal-header) {
+  padding: 12px 20px;
+  background: linear-gradient(135deg, #1677ff 0%, #0958d9 100%);
+  border-radius: 8px 8px 0 0;
+}
+.ppt-modal :deep(.ant-modal-title) { color: #fff; font-size: 16px; font-weight: 600; }
+.ppt-modal :deep(.ant-modal-close-x) { color: rgba(255, 255, 255, 0.8); }
+.ppt-modal :deep(.ant-modal-close-x:hover) { color: #fff; }
+
+.ppt-container {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  background: #f0f2f5;
+  position: relative;
+}
+.ppt-container.ppt-fullscreen {
+  width: 100vw;
+  height: 100vh;
+  background: #000;
+}
+
+.ppt-toolbar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 10px 20px;
+  background: #fff;
+  border-bottom: 1px solid #e8e8e8;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+  transition: opacity 0.3s ease, transform 0.3s ease;
+  z-index: 100;
+}
+.ppt-toolbar.toolbar-hidden {
+  opacity: 0;
+  transform: translateY(-100%);
+}
+
+.toolbar-left { flex: 1; }
+.file-info {
+  font-size: 14px;
+  font-weight: 500;
+  color: #333;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  max-width: 300px;
+}
+.toolbar-center { display: flex; align-items: center; gap: 16px; }
+.nav-btn {
+  width: 36px;
+  height: 36px;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 18px;
+  color: #595959;
+}
+.nav-btn:hover:not(:disabled) { background: #e6f7ff; color: #1890ff; }
+.nav-btn:disabled { color: #d9d9d9; cursor: not-allowed; }
+.page-indicator { font-size: 16px; font-weight: 600; color: #1890ff; min-width: 80px; text-align: center; }
+.toolbar-right { display: flex; align-items: center; gap: 8px; flex: 1; justify-content: flex-end; }
+.zoom-btn { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; color: #000; }
+.zoom-btn:hover { background: #f0f0f0; color: #1890ff; }
+.zoom-img-btn { width: 20px; height: 20px; cursor: pointer; }
+.zoom-img-btn:hover {opacity: 0.8; }
+.zoom-value { font-size: 13px; color: #666; min-width: 50px; text-align: center; }
+.fullscreen-btn { margin-left: 8px; }
+.download-btn { margin-left: 8px; color: #52c41a; }
+.download-btn:hover { color: #389e0d; background: #f6ffed; }
+
+/* 🌟 改动最大的区域:容器布局优化 */
+.ppt-main-area {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 20px;
+  overflow: hidden; /* 防止出现双重滚动条 */
+}
+
+.ppt-loading, .ppt-preview-error {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 100%;
+}
+.ppt-preview-error { background: #fff; border-radius: 8px; }
+.error-icon { font-size: 64px; color: #ff4d4f; margin-bottom: 20px; }
+.error-text { font-size: 16px; color: #666; margin-bottom: 24px; }
+.retry-btn { display: flex; align-items: center; gap: 8px; }
+
+.ppt-preview-wrapper {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  position: relative;
+}
+
+.ppt-scroll-container {
+  width: 100%;
+  height: 100%;
+  overflow: auto; /* 仅在手动放大(>Fit尺寸)时允许滚动条 */
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+/* 视口样式:课件模式核心 */
+.ppt-viewport {
+  transition: transform 0.2s ease-out;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15);
+  background: #fff;
+}
+
+/* 覆盖组件默认的宽高度配置 */
+.ppt-component {
+  width: auto !important;
+  height: auto !important;
+}
+
+/* 左右浮动翻页箭头 */
+.ppt-nav-overlay {
+  position: absolute;
+  top: 0; left: 0; right: 0; bottom: 0;
+  pointer-events: none;
+  z-index: 10;
+}
+.nav-arrow {
+  position: absolute;
+  top: 50%;
+  transform: translateY(-50%);
+  width: 50px;
+  height: 50px;
+  border-radius: 50%;
+  background: rgba(0, 0, 0, 0.3);
+  color: #fff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 24px;
+  cursor: pointer;
+  pointer-events: auto;
+  transition: all 0.2s;
+  opacity: 0;
+}
+.ppt-preview-wrapper:hover .nav-arrow { opacity: 1; }
+.nav-arrow:hover:not(.disabled) { background: rgba(24, 144, 255, 0.9); transform: translateY(-50%) scale(1.1); }
+.nav-arrow.disabled { opacity: 0.1; cursor: not-allowed; }
+.nav-prev { left: 20px; }
+.nav-next { right: 20px; }
+
+.ppt-footer {
+  padding: 12px 20px;
+  background: #fff;
+  border-top: 1px solid #e8e8e8;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  transition: opacity 0.3s ease, transform 0.3s ease;
+}
+.ppt-footer.footer-hidden { opacity: 0; transform: translateY(100%); }
+.footer-info { display: flex; gap: 24px; }
+.info-item { display: flex; align-items: center; gap: 6px; font-size: 12px; color: #666; }
+.footer-tips { font-size: 12px; color: #999; }
+
+/* 🌟 强行穿透组件库内置样式,统一抹平 */
+:deep(.vue-office-pptx) {
+  padding: 0 !important;
+  background: transparent !important;
+}
+:deep(.vue-office-pptx .wrapper) {
+  padding: 0 !important;
+  background: transparent !important;
+}
+:deep(.pptx-preview-wrapper){
+  overflow-x: hidden
+}
+</style>

+ 135 - 0
src/views/graph-view/VideoView.vue

@@ -0,0 +1,135 @@
+<template>
+  <!-- <div class="video-view"> -->
+    <a-modal
+      :visible="isVisible"
+      :title="'视频预览:' + fileName"
+      :footer="null"
+      :width="800"
+      :closable="true"
+      @cancel="handleClose"
+      @update:visible="handleUpdateVisible"
+    >
+      <div class="video-container">
+        <video
+          ref="videoRef"
+          :src="videoUrl"
+          controls
+          autoplay
+          class="preview-video"
+        ></video>
+      </div>
+      <div class="video-info">
+        <p>文件名:{{ fileName }}</p>
+        <p>文件大小:{{ formatFileSize(fileSize) }}</p>
+        <p>上传时间:{{ formatTime(createdTime) }}</p>
+      </div>
+    </a-modal>
+  <!-- </div> -->
+</template>
+
+<script setup lang="js">
+import { ref, computed, watch, onUnmounted } from 'vue';
+import { fileStorageService } from '../../utils/fileStorage';
+import { formatTime } from '../../utils/util'
+
+const props = defineProps({
+  visible: Boolean,
+  nodeId: String,
+  fileRecord: Object
+});
+
+const emit = defineEmits(['update:visible']);
+
+const isVisible = computed(() => props.visible);
+const videoRef = ref(null);
+const videoUrl = ref('');
+const fileName = ref('');
+const fileSize = ref(0);
+const createdTime = ref('');
+
+const formatFileSize = (size) => {
+  if (!size) return '0 B';
+  const units = ['B', 'KB', 'MB', 'GB'];
+  let i = 0;
+  let num = size;
+  while (num >= 1024 && i < units.length - 1) {
+    num /= 1024;
+    i++;
+  }
+  return num.toFixed(2) + ' ' + units[i];
+};
+
+const loadVideo = async () => {
+  if (!props.visible || !props.fileRecord) return;
+  
+  try {
+    fileName.value = props.fileRecord.original_name || props.fileRecord.file_name;
+    fileSize.value = props.fileRecord.file_size;
+    createdTime.value = props.fileRecord.created_at;
+    
+    const file = await fileStorageService.readFileFromOPFS(props.fileRecord.file_path);
+    videoUrl.value = URL.createObjectURL(file);
+  } catch (error) {
+    console.error('加载视频失败:', error);
+  }
+};
+
+const handleClose = () => {
+  emit('update:visible', false);
+};
+
+const handleUpdateVisible = (val) => {
+  emit('update:visible', val);
+};
+
+watch(() => props.visible, (newVal) => {
+  if (newVal) {
+    loadVideo();
+  } else {
+    if (videoUrl.value) {
+      URL.revokeObjectURL(videoUrl.value);
+      videoUrl.value = '';
+    }
+  }
+});
+
+onUnmounted(() => {
+  if (videoUrl.value) {
+    URL.revokeObjectURL(videoUrl.value);
+  }
+});
+</script>
+
+<style scoped lang="css">
+.video-view {
+  width: 100%;
+  height: 100%;
+}
+
+.video-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background: #000;
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+.preview-video {
+  width: 100%;
+  max-height: 500px;
+}
+
+.video-info {
+  margin-top: 16px;
+  padding: 12px;
+  background: #f7f8fa;
+  border-radius: 8px;
+}
+
+.video-info p {
+  margin: 4px 0;
+  font-size: 13px;
+  color: #646a73;
+}
+</style>

+ 167 - 0
src/views/graph-view/WeightSelected.vue

@@ -0,0 +1,167 @@
+<template>
+  <div class="sidebar-wrapper" :class="{ collapsed: sidebarCollapsed }">
+    <div class="sidebar-toggle" @click="sidebarCollapsed = !sidebarCollapsed">
+      <span title="收起"><icon-import v-if="!sidebarCollapsed" /></span>
+      <span title="展开"><icon-export v-if="sidebarCollapsed" /></span>
+    </div>
+    
+    <div class="sidebar-content" v-show="!sidebarCollapsed">
+      <div class="sidebar-header">
+        <icon-filter class="header-icon" />
+        <span class="header-title">实体权重筛选</span>
+      </div>
+      
+      <div class="sidebar-body">
+        <div class="label-tags">
+          <button 
+            v-for="weight in weightFilters" 
+            :key="weight.name"
+            class="label-tag"
+            :style="{ backgroundColor: weight.color + '20', borderColor: weight.color, color: weight.color }"
+            @click="toggleWeightFilter(weight.name)"
+          >
+            <div class="color-dot" :style="{ backgroundColor: weight.color }"></div>
+            <span>{{ weight.label }}</span>
+          </button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="js">
+import { ref } from 'vue';
+
+defineProps({
+  neo4j: {
+    type: Object,
+    required: true
+  },
+  onFilter: {
+    type: Function,
+    default: () => {}
+  }
+});
+
+const emit = defineEmits(['filterWeight']);
+
+const sidebarCollapsed = ref(false);
+const activeWeights = ref([]);
+const weightFilters = ref([
+  { name: 3, label: '高', color: '#F15D5D' },
+  { name: 2, label: '中', color: '#F9C351' },
+  { name: 1, label: '底', color: '#86909c' },
+]);
+
+const toggleWeightFilter = (weightName) => {
+  emit('filterWeight', weightName);
+};
+
+</script>
+
+<style scoped lang="css">
+.sidebar-wrapper {
+  position: absolute;
+  top: 220px;
+  left: 16px;
+  z-index: 20;
+  width: 266px;
+  display: flex;
+  transition: all 0.3s ease;
+}
+
+.sidebar-toggle {
+  position: absolute;
+  right: -24px;
+  top: 50%;
+  transform: translateY(-50%);
+  width: 24px;
+  height: 48px;
+  background: #fff;
+  border: 1px solid #e5e6eb;
+  border-left: none;
+  border-radius: 0 4px 4px 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  color: #86909c;
+  transition: all 0.2s ease;
+  z-index: 1;
+}
+
+.sidebar-toggle:hover {
+  background: #f2f3f5;
+  color: #165dff;
+}
+
+.sidebar-content {
+  background: #fff;
+  border-radius: 12px;
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+  overflow: hidden;
+  min-width: 266px;
+  transition: all 0.3s ease;
+}
+
+.sidebar-wrapper.collapsed .sidebar-content {
+  width: 0;
+  padding: 0;
+  overflow: hidden;
+}
+
+.sidebar-header {
+  padding: 16px 20px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.header-icon {
+  color: #fff;
+  font-size: 18px;
+}
+
+.header-title {
+  color: #fff;
+  font-size: 15px;
+  font-weight: 600;
+}
+
+.sidebar-body {
+  padding: 16px 20px;
+}
+
+.label-tags {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+}
+
+.label-tag {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 8px 14px;
+  border: 1px solid;
+  border-radius: 20px;
+  background: transparent;
+  font-size: 13px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.2s ease;
+}
+
+.label-tag:hover {
+  opacity: 0.7;
+  transform: translateY(-1px);
+}
+
+.color-dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  flex-shrink: 0;
+}
+</style>

+ 625 - 0
src/views/graph-view/graph.js

@@ -0,0 +1,625 @@
+export const graphService = {
+  /**
+   * 获取图谱的根节点
+   */
+  async findRootNodes(neo4jInstance) {
+    const cypher = `
+    MATCH (n)
+    WHERE NOT ()-[]->(n) AND NOT n:Question
+    RETURN id(n) AS id, elementId(n) AS elementId, labels(n) as labels, properties(n) AS props
+    `;
+
+    const records = await neo4jInstance.execute(cypher);
+    // console.log(records)
+    // 将结果整理为清晰的数组格式
+    return records.map(record => ({
+      id: record.id,
+      elementId: record.elementId,
+      labels: record.labels,
+      ...record.props // 展开所有属性,比如 name, group 等
+    }));
+  },
+  /**
+   * 根据节点ID获取节点信息   WITH n,r,m LIMIT 50 
+   * */
+  async findNodeById(neo4jInstance, nodeId) {
+    const cypher = `
+    MATCH (n)-[r]->(m) 
+    WHERE id(n) = ${nodeId}
+      AND type(r) <> 'HAS_QUESTION'
+  
+    RETURN 
+      {id: id(n), elementId: elementId(n), labels: labels(n), props: properties(n)} AS n_info,
+      {id: id(r), elementId: elementId(r), type: type(r), props: properties(r)} AS r_info,
+      {id: id(m), elementId: elementId(m), labels: labels(m), props: properties(m)} AS m_info
+    `;
+    const records = await neo4jInstance.execute(cypher);
+
+    // 将 records 转换为 G6 需要的 {nodes:[], edges:[]} 格式
+    const nodeMap = new Map();
+    const edgeMap = new Map();
+
+    records.forEach(record => {
+      const {
+        n_info,
+        r_info,
+        m_info
+      } = record;
+
+      // 处理起始节点 n
+      if (n_info && !nodeMap.has(n_info.id)) {
+        // console.log(n_info.props.sources)
+        nodeMap.set(n_info.id, {
+          id: String(n_info.id),
+          data: {
+            text: n_info.props.name || '',
+            elementId: n_info.elementId,
+            labels: n_info.labels,
+            ...(n_info.props.star !== undefined && { star: n_info.props.star })
+          }
+        });
+      }
+
+      // 处理目标节点 m
+      if (m_info && !nodeMap.has(m_info.id)) {
+        nodeMap.set(m_info.id, {
+          id: String(m_info.id),
+          data: {
+            text: m_info.props.name || '',
+            elementId: m_info.elementId,
+            labels: m_info.labels,
+            ...(m_info.props.star !== undefined && { star: m_info.props.star })
+          }
+        });
+      }
+
+      // 处理关系 r
+      if (r_info && !edgeMap.has(r_info.id)) {
+        edgeMap.set(r_info.id, {
+          id: String(r_info.elementId),
+          source: String(n_info.id),
+          target: String(m_info.id),
+          label: r_info.type || r_info.props.label || '相关',
+          // sources: r_info.props?.sources?r_info.props?.sources:"[]",
+        });
+      }
+    });
+    // console.log("根据节点ID获取节点信息:",nodeMap.values(), edgeMap.values())
+
+    const result = {
+      nodes: Array.from(nodeMap.values()),
+      edges: Array.from(edgeMap.values())
+    };
+
+    // console.log('转换后的数据:', result);
+    return result;
+  },
+  /**
+   * 根据节点ID查询节点的双向关系(网状结构)
+   * 包括入边和出边,展示节点的完整关联路径
+   */
+  async findNodeRelations(neo4jInstance, nodeId) {
+    const cypher = `
+      MATCH (n)
+      WHERE elementId(n) = $nodeId OR id(n) = toInteger($nodeId)
+      
+      OPTIONAL MATCH (n)-[r1]->(m)
+      WHERE type(r1) <> 'HAS_QUESTION'
+      
+      OPTIONAL MATCH (p)-[r2]->(n)
+      WHERE type(r2) <> 'HAS_QUESTION'
+      
+      WITH n, m, r1, p, r2
+      
+      WITH collect(DISTINCT n) + collect(DISTINCT m) + collect(DISTINCT p) AS allNodes,
+           collect(DISTINCT r1) + collect(DISTINCT r2) AS allRelations
+      
+      UNWIND allNodes AS n1
+      UNWIND allNodes AS n2
+      MATCH (n1)-[r]->(n2)
+      WHERE type(r) <> 'HAS_QUESTION' AND r IS NOT NULL
+      
+      RETURN 
+        {id: id(n1), elementId: elementId(n1), labels: labels(n1), props: properties(n1)} AS n_info,
+        {id: id(r), elementId: elementId(r), type: type(r), props: properties(r)} AS r_info,
+        {id: id(n2), elementId: elementId(n2), labels: labels(n2), props: properties(n2)} AS m_info
+    `;
+    
+    const records = await neo4jInstance.execute(cypher, { nodeId });
+
+    const nodeMap = new Map();
+    const edgeMap = new Map();
+
+    records.forEach(record => {
+      const { n_info, r_info, m_info } = record;
+
+      if (n_info && !nodeMap.has(n_info.id)) {
+        nodeMap.set(n_info.id, {
+          id: String(n_info.id),
+          data: { 
+            text: n_info.props.name || '', 
+            elementId: n_info.elementId, 
+            labels: n_info.labels,
+            ...(n_info.props.star !== undefined && { star: n_info.props.star })
+          }
+        });
+      }
+
+      if (m_info && !nodeMap.has(m_info.id)) {
+        nodeMap.set(m_info.id, {
+          id: String(m_info.id),
+          data: { 
+            text: m_info.props.name || '', 
+            elementId: m_info.elementId, 
+            labels: m_info.labels,
+            ...(m_info.props.star !== undefined && { star: m_info.props.star })
+          }
+        });
+      }
+
+      if (r_info && !edgeMap.has(r_info.elementId)) {
+        edgeMap.set(r_info.elementId, {
+          id: String(r_info.elementId),
+          source: String(n_info.id),
+          target: String(m_info.id),
+          label: r_info.type || r_info.props.label || '相关',
+        });
+      }
+    });
+
+    if (nodeMap.size === 0) {
+      const fallbackCypher = `
+        MATCH (n) WHERE elementId(n) = $nodeId OR id(n) = toInteger($nodeId)
+        RETURN {id: id(n), elementId: elementId(n), labels: labels(n), props: properties(n)} AS n_info
+      `;
+      const fallbackRecords = await neo4jInstance.execute(fallbackCypher, { nodeId });
+      if (fallbackRecords.length > 0) {
+        const { n_info } = fallbackRecords[0];
+        nodeMap.set(n_info.id, {
+          id: String(n_info.id),
+          data: { 
+            text: n_info.props.name || '', 
+            elementId: n_info.elementId, 
+            labels: n_info.labels,
+            ...(n_info.props.star !== undefined && { star: n_info.props.star })
+          }
+        });
+      }
+    }
+
+    return {
+      nodes: Array.from(nodeMap.values()),
+      edges: Array.from(edgeMap.values())
+    };
+  },
+
+  /**
+   * 根据父节点ID,获取其下属的子图
+   * (包含父节点、所有子节点,以及这个家族圈子内部的所有交叉关系)
+   */
+  async findSubgraphByParentId(neo4jInstance, parentNodeId) {
+    // 1. MATCH 找到指定的父节点 n
+    // 2. OPTIONAL MATCH (n)-[r1]->(m) 顺着箭头找出所有的子节点 m
+    // 3. 限制数量,并将父节点与所有子节点合并为 allNodes 节点池
+    // 4. 在节点池内部,寻找任意两点之间的所有连线 r2(从而把子节点之间的关系也查出来)
+    const cypher = `
+      MATCH (n)
+      WHERE elementId(n) = $parentNodeId OR id(n) = toInteger($parentNodeId)
+      
+      // 明确单向箭头,只找父节点指向的子节点
+      OPTIONAL MATCH (n)-[r1]->(m)
+      WHERE type(r1) <> 'HAS_QUESTION'
+      
+      WITH n, m LIMIT 60
+      
+      // 将父节点和它所有的子节点打包成一个“家族圈子”
+      WITH collect(distinct n) + collect(distinct m) AS allNodes
+      
+      // 展开圈子,寻找圈子内部成员之间的所有关系(包括子节点与子节点之间的关系)
+      UNWIND allNodes AS n1
+      UNWIND allNodes AS n2
+      MATCH (n1)-[r2]->(n2)
+      WHERE type(r2) <> 'HAS_QUESTION'
+      
+      RETURN 
+        {id: id(n1), elementId: elementId(n1), labels: labels(n1), props: properties(n1)} AS n_info,
+        {id: id(r2), elementId: elementId(r2), type: type(r2), props: properties(r2)} AS r_info,
+        {id: id(n2), elementId: elementId(n2), labels: labels(n2), props: properties(n2)} AS m_info
+    `;
+    
+    const records = await neo4jInstance.execute(cypher, { parentNodeId: parentNodeId });
+
+    const nodeMap = new Map();
+    const edgeMap = new Map();
+
+    records.forEach(record => {
+      const { n_info, r_info, m_info } = record;
+
+      // 1. 处理源节点
+      if (n_info && !nodeMap.has(n_info.elementId)) {
+        nodeMap.set(n_info.elementId, {
+          id: String(n_info.elementId),
+          data: { 
+            text: n_info.props.name || '', 
+            labels: n_info.labels,
+            ...(n_info.props.star !== undefined && { star: n_info.props.star })
+          }
+        });
+      }
+
+      // 2. 处理目标节点
+      if (m_info && !nodeMap.has(m_info.elementId)) {
+        nodeMap.set(m_info.elementId, {
+          id: String(m_info.elementId),
+          data: { 
+            text: m_info.props.name || '', 
+            labels: m_info.labels,
+            ...(m_info.props.star !== undefined && { star: m_info.props.star })
+          }
+        });
+      }
+
+      // 3. 处理关系(此时 r_info 包含了:父->子、子->子 的所有关系)
+      if (r_info && !edgeMap.has(r_info.elementId)) {
+        edgeMap.set(r_info.elementId, {
+          id: String(r_info.elementId),
+          source: String(n_info.elementId),
+          target: String(m_info.elementId),
+          label: r_info.type || r_info.props.label || '相关',
+        });
+      }
+    });
+
+    // 兜底机制:如果该父节点没有任何子节点,至少把父节点自身返回,避免前端画布空白
+    if (nodeMap.size === 0) {
+      const fallbackCypher = `
+        MATCH (n) WHERE elementId(n) = $parentNodeId OR id(n) = toInteger($parentNodeId)
+        RETURN {id: id(n), elementId: elementId(n), labels: labels(n), props: properties(n)} AS n_info
+      `;
+      const fallbackRecords = await neo4jInstance.execute(fallbackCypher, { parentNodeId: parentNodeId });
+      if (fallbackRecords.length > 0) {
+        const { n_info } = fallbackRecords[0];
+        nodeMap.set(n_info.elementId, {
+          id: String(n_info.elementId),
+          data: { 
+            text: n_info.props.name || '', 
+            labels: n_info.labels,
+            ...(n_info.props.star !== undefined && { star: n_info.props.star })
+          }
+        });
+      }
+    }
+
+    return {
+      nodes: Array.from(nodeMap.values()),
+      edges: Array.from(edgeMap.values())
+    };
+  },
+
+  /**
+   * 根据节点名称模糊查询节点信息
+   */
+  async searchNodesByName(neo4jInstance, keyword) {
+    // 1. 同款 Cypher 改造:在数据库直接把数据组装成干净的 JSON 对象
+    const cypher = `
+    MATCH (node)
+    WHERE (node:Book OR node:Part OR node:Chapter OR node:EntityType OR node:Entity)
+      AND node.name CONTAINS $keyword
+    WITH collect(node) AS items
+    WITH items[0..100] AS nodesList
+    UNWIND nodesList AS n
+    
+    // 寻找这些节点之间的内部上下级/横向关系
+    OPTIONAL MATCH (n)-[r]->(m)
+    WHERE m IN nodesList
+    
+    // 同款 RETURN 打包语法糖,把所有需要的字段一口气全部暴露出来
+    RETURN 
+      {id: id(n), elementId: elementId(n), labels: labels(n), props: properties(n)} AS n_info,
+      CASE WHEN r IS NOT NULL 
+        THEN {id: id(r), elementId: elementId(r), type: type(r), props: properties(r)}
+        ELSE null 
+      END AS r_info,
+      CASE WHEN m IS NOT NULL 
+        THEN {id: id(m), elementId: elementId(m), labels: labels(m), props: properties(m)}
+        ELSE null 
+      END AS m_info
+  `;
+
+    // 2. 使用你的自定义注入插件执行查询
+    const records = await neo4jInstance.execute(cypher, {
+      keyword
+    });
+    // console.log("模糊搜索原始 records:", records);
+
+    // 3. 采用跟你一模一样的 Map 去重组装逻辑
+    const nodeMap = new Map();
+    const edgeMap = new Map();
+
+    records.forEach(record => {
+      // 解构出每一行的 JSON 数据
+      const {
+        n_info,
+        r_info,
+        m_info
+      } = record;
+
+      // ================= 处理主节点 n (匹配到名字的医院等节点) =================
+      if (n_info && !nodeMap.has(n_info.id)) {
+        nodeMap.set(n_info.id, {
+          id: String(n_info.id),
+          data: {
+            text: n_info.props.name || '',
+            elementId: n_info.elementId,
+            labels: n_info.labels,
+            ...(n_info.props.star !== undefined && { star: n_info.props.star })
+          }
+        });
+      }
+
+      // ================= 处理目标节点 m (存在内部关系的另一个医院节点) =================
+      if (m_info && !nodeMap.has(m_info.id)) {
+        nodeMap.set(m_info.id, {
+          id: String(m_info.id),
+          data: {
+            text: m_info.props.name || '',
+            elementId: m_info.elementId,
+            labels: m_info.labels,
+            ...(m_info.props.star !== undefined && { star: m_info.props.star })
+          }
+        });
+      }
+
+      // ================= 处理关系连线 r =================
+      if (r_info && !edgeMap.has(r_info.id)) {
+        edgeMap.set(r_info.elementId, {
+          id: String(r_info.elementId),
+          source: String(n_info.id), // 连线起点
+          target: String(m_info.id), // 连线终点
+          label: r_info.type || r_info.props.label || '相关',
+          // sources: r_info.props?.sources ? r_info.props.sources : "[]",
+        });
+      }
+    });
+
+    const result = {
+      nodes: Array.from(nodeMap.values()),
+      edges: Array.from(edgeMap.values())
+    };
+
+    // console.log("模糊搜索内连最终结果:", result);
+    return result;
+  },
+  /**
+   * 根据节点id查询节点中的sources,返回新的结构格式
+   * */
+  async searchNodeSources(neo4jInstance, nodeId) {
+    const cypher = `
+      MATCH (node)
+      WHERE id(node) = ${nodeId}
+      RETURN properties(node) AS props
+    `;
+    const records = await neo4jInstance.execute(cypher);
+    const props = records[0]?.props || {};
+    
+    const result = [{
+      mu_id: props.mu_id || '',
+      name: props.name || '',
+      path: props.path || '',
+      text: props.description || props.text || '',
+      menuVisible: false,
+      menuStyle: {}
+    }];
+    
+    return JSON.stringify(result);
+  },
+  /**
+   * 根据关系id查询关系中的sources
+   * */
+  async searchEdgeSources(neo4jInstance, edgeId) {
+    const cypher = `
+    MATCH (n)-[r]->(m)
+    WHERE elementId(r) = $edgeId
+    RETURN   r.source_sentence as source_sentence, r.slice_id as slice_id,r.chapter as chapter,r.slice_text as slice_text
+  `;
+    const sources = []
+    const records = await neo4jInstance.execute(cypher, {
+      edgeId
+    });
+    // console.log(records,999)
+    if (!records[0].source_sentence || !records[0].slice_id || !records[0].chapter || !records[0].slice_text) {
+      return "[]";
+    } else {
+      sources.push(records[0]);
+      return JSON.stringify(sources);
+    }
+    // if(records[0])
+    // return records[0].props.sources|| "[]";
+  },
+  /**
+   * 根据实体id查询MU中所有信息
+   * */
+  async searchMuByEntitySources(neo4jInstance, nodId) {
+    const cypher = `
+      MATCH (mu)-[:包含实体]->(node)
+      WHERE id(node) = ${nodId}
+      RETURN properties(mu) AS muProps
+    `;
+    const records = await neo4jInstance.execute(cypher, {
+      nodId
+    });
+    
+    const result = records.map(record => {
+      const muProps = record.muProps || {};
+      return {
+        mu_id: muProps.mu_id || '',
+        name: muProps.name || '',
+        path: muProps.path || '',
+        text: muProps.description || muProps.text || '',
+        menuVisible: false,
+        menuStyle: {}
+      };
+    });
+    
+    return JSON.stringify(result);
+  },
+
+  /**
+   * 根据标签查询所有label的节点
+   * */
+  async findNodesByLabel(neo4jInstance, label) {
+    const cypher = `
+    MATCH(n:${label}) 
+    RETURN id(n) AS id, elementId(n) AS elementId, labels(n) as labels, properties(n) AS props
+  `;
+
+    const records = await neo4jInstance.execute(cypher);
+    // console.log("根据标签查询节点11:", records);
+
+    // 将结果整理为清晰的数组格式
+    return records.map(record => ({
+      id: record.id,
+      elementId: record.elementId,
+      labels: record.labels,
+      ...record.props // 展开所有属性,比如 name, group 等
+    }));
+  },
+
+  /**
+   * 根据权重查询节点
+   * */
+  async findNodesByWeight(neo4jInstance, weight) {
+    const cypher = `
+    MATCH(n) 
+    WHERE n.weight = $weight
+    RETURN id(n) AS id, elementId(n) AS elementId, labels(n) as labels, properties(n) AS props
+  `;
+
+    const records = await neo4jInstance.execute(cypher, { weight });
+
+    return records.map(record => ({
+      id: record.id,
+      elementId: record.elementId,
+      labels: record.labels,
+      ...record.props
+    }));
+  },
+
+  /**
+   * 查询所有的Question节点
+   * */
+  async findQuestionsNodes(neo4jInstance) {
+    const cypher = `
+    MATCH (n)
+    WHERE NOT ()-[]->(n) OR n:Question
+    RETURN id(n) AS id, elementId(n) AS elementId, labels(n) as labels, properties(n) AS props
+    `;
+
+    const records = await neo4jInstance.execute(cypher);
+
+    // 将结果整理为清晰的数组格式
+    return records.map(record => ({
+      id: record.id,
+      elementId: record.elementId,
+      labels: record.labels,
+      ...record.props // 展开所有属性
+    }));
+  },
+
+
+  async findNodesByQuestionType(neo4jInstance, questionType) {
+    // 1. 修改 Cypher 语句,直接比对节点的属性 n.questionType
+    const cypher = `
+    MATCH (n)
+    WHERE n.question_type = $targetType
+    RETURN id(n) AS id, elementId(n) AS elementId, labels(n) as labels, properties(n) AS props
+    `;
+
+    // 2. 将动态的 questionType 作为参数传入,防止注入并提高性能
+    const records = await neo4jInstance.execute(cypher, {
+      targetType: questionType
+    });
+
+    // 3. 保持原有的数据格式化逻辑
+    return records.map(record => ({
+      id: record.id,
+      elementId: record.elementId,
+      labels: record.labels,
+      ...record.props // 展开后,questionType 也会平铺在这个对象的顶层
+    }));
+  },
+  /**
+   * 根据entity_name和question_type查询Question节点
+   * @param {Object} neo4jInstance Neo4j 驱动实例
+   * @param {string[]} entity_names 要查询的实体名称数组
+   * @param {string[]} question_types 要查询的题目类型数组
+   * @returns {Promise<QuestionNode[]>} 符合条件的Question节点数组
+   * */ 
+  async findQuestionsNodesByNameAndType(neo4jInstance, entity_names, question_types) {
+    const cypher = `
+    MATCH (q:Question)
+    WHERE q.entity_name IN $entity_names
+      AND ($question_types = [] OR q.question_type IN $question_types)
+    RETURN 
+      id(q) AS node_db_id,  // Neo4j数据库内置节点ID
+      q.elementId AS elementId, // 节点自身属性elementId
+      q AS node
+    ORDER BY q.chapter ASC, q.question_type ASC
+  `.trim();
+
+    const records = await neo4jInstance.execute(cypher, {
+      entity_names,
+      question_types: question_types || [] // 确保如果是 null/undefined 时传空数组
+    });
+    console.log(records, 111)
+    return records.map(record => {
+      return {
+        id: record.node_db_id,
+        elementId: record.elementId,
+        ...record.node
+      }
+      // 替换 record.get('q') 为普通的 JS 对象取值
+      const node = record.q;
+
+      if (!node) return null; // 增强健壮性,防止查空
+
+      // console.log(node);
+      // return {
+      //    id: nodeDbId,
+      //   elementId: elementId,
+      //   ...node
+      // };
+    }) // 过滤掉可能为 null 的数据
+  },
+  /**
+   * 根据单个节点名称,获取其自身及所有子节点的名称列表
+   * @param {Object} neo4jInstance Neo4j 驱动实例
+   * @param {string} nodeName 当前选中的节点名称
+   * @param {boolean} includeChildren 是否勾选了包含子节点
+   * @returns {Promise<string[]>} 节点名称数组
+   */
+  async getEntityNamesWithChildren(neo4jInstance, nodeName, includeChildren = false) {
+    if (!nodeName) return [];
+
+    // 如果没有勾选包含子节点,直接返回当前节点名称
+    if (!includeChildren) {
+      return [nodeName];
+    }
+
+    // 如果勾选了,去图谱中查询自身及所有后代节点
+    const cypher = `
+    MATCH (p{name: $nodeName})-[*1..]->(descendant)
+    RETURN collect(DISTINCT descendant.name) AS name_list
+  `
+
+    const records = await neo4jInstance.execute(cypher, {
+      nodeName
+    });
+    // console.log(records)
+    if (records.length) {
+      return records[0].name_list
+    } else {
+      return []
+    }
+  }
+}

+ 117 - 0
src/views/graph-view/mock.js

@@ -0,0 +1,117 @@
+// mock.js
+export const mockBookData = {
+  nodes: [
+    { 
+      id: 'root', 
+      data: { 
+        label: '《AI大模型工程架构》', 
+        level: 0, 
+        text: '本书主要介绍AI工程化的核心架构,从底层图数据库到Agent上层应用,旨在帮助开发者打通从数据底座到智能体业务全链路。' 
+      } 
+    },
+    { 
+      id: 'chap1', 
+      data: { 
+        label: '第一章:基础环境', 
+        level: 1, 
+        text: '本章涵盖前置知识与开发环境搭建,重点在于图数据库与向量数据库的本地及容器化部署。' 
+      } 
+    },
+    { 
+      id: 'chap2', 
+      data: { 
+        label: '第二章:核心大脑', 
+        level: 1, 
+        text: '本章探讨智能体的核心认知能力,涵盖向量检索增强生成(RAG)技术与长短期记忆机制。' 
+      } 
+    },
+    { 
+      id: 'sec1-1', 
+      data: { 
+        label: '1.1 图数据库', 
+        level: 2, 
+        text: 'Neo4j图数据库基础与Cypher查询语言。通过图结构存储实体间复杂的语义关联,为知识图谱可视化奠定基础。' 
+      } 
+    },
+    { 
+      id: 'sec1-2', 
+      data: { 
+        label: '1.2 向量检索', 
+        level: 2, 
+        text: '介绍 ChromaDB 等轻量级向量数据库的使用,涵盖文本向量化(Embedding)及高维向量相似度检索。' 
+      } 
+    },
+    { 
+      id: 'sec2-1', 
+      data: { 
+        label: '2.1 Agent基础', 
+        level: 2, 
+        text: '智能体的工作流设计与Prompt调优,探讨单智能体在 ReAct 框架下的思考与行动循环。' 
+      } 
+    },
+    { 
+      id: 'sec2-2', 
+      data: { 
+        label: '2.2 LangGraph', 
+        level: 2, 
+        text: '使用 LangGraph 管理复杂的多智能体流转,利用图结构精细控制状态机流转与人工介入(Interrupt)中断。' 
+      } 
+    }
+  ],
+  edges: [
+    { 
+      id: 'e-root-chap1', 
+      source: 'root', 
+      target: 'chap1', 
+      data: { 
+        label: '包含', 
+        text: '在《AI大模型工程架构》全书大纲规划中,明确将第一章定义为“基础环境建设”,属于总分架构。' 
+      } 
+    },
+    { 
+      id: 'e-root-chap2', 
+      source: 'root', 
+      target: 'chap2', 
+      data: { 
+        label: '包含', 
+        text: '作为贯穿全书的核心骨架,前言部分明确指出第二章“核心大脑”是本书的核心算法篇章。' 
+      } 
+    },
+    { 
+      id: 'e-chap1-sec1-1', 
+      source: 'chap1', 
+      target: 'sec1-1', 
+      data: { 
+        label: '包含', 
+        text: '在第一章基础环境的1.1节中,主要讲解了图数据库Neo4j的技术选型与环境安装。' 
+      } 
+    },
+    { 
+      id: 'e-chap1-sec1-2', 
+      source: 'chap1', 
+      target: 'sec1-2', 
+      data: { 
+        label: '包含', 
+        text: '为了对比传统关联查询,第一章1.2节引入了向量检索底座ChromaDB的搭建。' 
+      } 
+    },
+    { 
+      id: 'e-chap2-sec2-1', 
+      source: 'chap2', 
+      target: 'sec2-1', 
+      data: { 
+        label: '包含', 
+        text: '第二章核心大脑深度解析了Agent智能体的基本原理,以此衍生出后续的复杂工作流。' 
+      } 
+    },
+    { 
+      id: 'e-chap2-sec2-2', 
+      source: 'chap2', 
+      target: 'sec2-2', 
+      data: { 
+        label: '包含', 
+        text: '在第二章高级演进部分,2.2节着重探讨了利用LangGraph管理复杂的多节点状态中断。' 
+      } 
+    }
+  ]
+};

+ 16 - 0
vite.config.js

@@ -0,0 +1,16 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+// https://vite.dev/config/
+export default defineConfig({
+  plugins: [vue()],
+  base: './', // 设置打包时使用相对路径
+  server: {
+    host: '0.0.0.0', // 或者设置为 true
+  },
+  build: {
+    outDir: 'dist',
+    assetsDir: 'assets',
+    emptyOutDir: true
+  }
+})

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików