Procházet zdrojové kódy

feat:能力画像模式新增,修改之前一些错误,新增节点上限配置项

韩洋 před 2 týdny
rodič
revize
895345ae05

binární
dist.zip


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

@@ -1,253 +0,0 @@
-知识图谱分析系统 - 功能和操作描述
-文档版本: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	最新稳定版

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

@@ -1,358 +0,0 @@
-
-# 知识图谱分析系统 - 功能和操作描述
-
----
-
-## 一、系统概述
-
-### 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ární
docs/多维度知识图谱数据挖掘分析系统功能和操作描述.docx


binární
docs/知识图谱分析系统功能和操作描述.docx


+ 6 - 0
public/config.js

@@ -3,5 +3,11 @@ window.APP_CONFIG = {
     url: 'neo4j://192.168.20.70:7687',
     username: 'neo4j',
     password: 'psw123456'
+  },
+  // 顶部模式切换按钮名称
+  modes: {
+    analysis: '智能分析模式',
+    compose: '智能组卷模式',
+    ability: '能力画像模式'
   }
 };

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 667 - 82
src/views/graph-view/GraphView.vue


+ 12 - 7
src/views/graph-view/HeaderView.vue

@@ -9,13 +9,9 @@
     <!-- 模式切换按钮组 -->
     <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 v-for="item in modeItems" :key="item.value" :value="item.value">
+          <component :is="item.icon" class="mode-icon" />
+          {{ item.label }}
         </a-radio>
       </a-radio-group>
     </div>
@@ -37,6 +33,15 @@ const modeStore = useModeStore();
 // 当前模式,默认智能分析模式
 const currentMode = ref('analysis');
 
+// 模式按钮名称从 public/config.js 读取(config 未加载时回退到内置默认值);
+// 图标为全局注册的 Arco 图标组件,字符串名通过 <component :is> 解析
+const modeIcons = { analysis: 'icon-bar-chart', compose: 'icon-file', ability: 'icon-radar' };
+const modeItems = Object.keys(modeIcons).map(value => ({
+  value,
+  icon: modeIcons[value],
+  label: window.APP_CONFIG?.modes?.[value] ?? { analysis: '智能分析模式', compose: '智能组卷模式', ability: '能力画像模式' }[value]
+}));
+
 // 模式切换处理
 const handleModeChange = (value) => {
   

+ 54 - 6
src/views/graph-view/LableSelected.vue

@@ -10,13 +10,31 @@
     <div class="sidebar-content" v-show="!sidebarCollapsed">
       <div class="sidebar-header">
         <icon-filter class="header-icon" />
-        <span class="header-title">节点筛选</span>
+        <span class="header-title">{{ mode === 'ability' ? '能力层次' : '节点筛选' }}</span>
       </div>
-      
+
       <div class="sidebar-body">
-        <div class="label-tags">
-          <button 
-            v-for="label in labelFilters" 
+        <!-- 能力画像模式:能力层次(点击高亮画布中已打开的对应层次能力项) -->
+        <div class="label-tags" v-if="mode === 'ability'">
+          <button
+            v-for="lv in levelFilters"
+            :key="lv.name"
+            class="label-tag"
+            :class="{ 'label-tag-active': activeLevel === lv.name }"
+            :style="activeLevel === lv.name
+              ? { backgroundColor: lv.color, borderColor: lv.color, color: '#fff' }
+              : { backgroundColor: lv.color + '20', borderColor: lv.color, color: lv.color }"
+            @click="toggleLevelFilter(lv.name)"
+          >
+            <div class="color-dot" :style="{ backgroundColor: lv.color }"></div>
+            <span>{{ lv.name }} {{ lv.bloom }}</span>
+          </button>
+        </div>
+
+        <!-- 普通模式:原有标签筛选 -->
+        <div class="label-tags" v-else>
+          <button
+            v-for="label in labelFilters"
             :key="label.name"
             class="label-tag"
             :style="{ backgroundColor: label.color + '20', borderColor: label.color, color: label.color }"
@@ -41,13 +59,17 @@ const props = defineProps({
     type: Object,
     required: true
   },
+  mode: {
+    type: String,
+    default: 'analysis'
+  },
   onFilter: {
     type: Function,
     default: () => {}
   }
 });
 
-const emit = defineEmits(['filterNode']);
+const emit = defineEmits(['filterNode', 'filterLevel']);
 
 // 侧边栏状态
 const sidebarCollapsed = ref(false);
@@ -60,12 +82,34 @@ const labelFilters = ref([
   // { name: 'Entity', label: '实体', color: '#CD96CD', count: 0 },
 ]);
 
+// 能力画像:层次筛选(与画布 Ability 节点配色一致)
+const activeLevel = ref(null);
+const levelFilters = ref([
+  { name: '懂', bloom: 'L1-L2', color: '#165DFF' },
+  { name: '会', bloom: 'L3',    color: '#00B42A' },
+  { name: '通', bloom: 'L4-L5', color: '#FF7D00' },
+  { name: '创', bloom: 'L6',    color: '#F53F3F' },
+]);
+
 // 切换标签筛选
 const toggleLabelFilter = (labelName) => {
   emit('filterNode', labelName);
 };
 
+// 切换层次高亮:点击已选中项 → 取消选中并通知父组件(父组件靠 currentHighlightLevel 判重取消)
+const toggleLevelFilter = (levelName) => {
+  if (activeLevel.value === levelName) {
+    activeLevel.value = null;
+  } else {
+    activeLevel.value = levelName;
+  }
+  emit('filterLevel', levelName);
+};
 
+// 模式切换时清空层次选中态
+watch(() => props.mode, () => {
+  activeLevel.value = null;
+});
 </script>
 
 <style scoped lang="css">
@@ -173,6 +217,10 @@ const toggleLabelFilter = (labelName) => {
   transform: translateY(-1px);
 }
 
+.label-tag-active {
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+}
+
 /* 颜色圆点 */
 .color-dot {
   width: 8px;

+ 382 - 0
src/views/graph-view/MaxNodesConfig.vue

@@ -0,0 +1,382 @@
+<template>
+  <div class="config-wrapper" :class="{ collapsed: isCollapsed }">
+    <div class="config-toggle" @click="isCollapsed = !isCollapsed">
+      <span title="收起"><icon-export v-if="!isCollapsed" /></span>
+      <span title="展开"><icon-import v-if="isCollapsed" /></span>
+    </div>
+    <div class="config-panel" v-show="!isCollapsed">
+      <div class="config-header">
+        <icon-settings class="header-icon" />
+        <span class="header-title">节点数量配置</span>
+      </div>
+      <div class="config-body">
+        <!-- 三个统计指标 -->
+        <div class="stats-row">
+          <div class="stat-box current">
+            <div class="stat-num">{{ currentNodes }}</div>
+            <div class="stat-name">当前节点</div>
+          </div>
+          <div class="stat-box limit">
+            <div class="stat-num">{{ maxExpandNodes }}</div>
+            <div class="stat-name">展示上限</div>
+          </div>
+          <div class="stat-box total">
+            <div class="stat-num">{{ totalNodes }}</div>
+            <div class="stat-name">库内总数</div>
+          </div>
+        </div>
+
+        <!-- 滑块 -->
+        <div class="slider-section">
+          <div class="slider-label">页面展示节点上限</div>
+          <div class="slider-with-btns">
+            <button class="step-btn" :disabled="sliderDisabled" @click="handleStep(-10)">
+              <icon-minus />
+            </button>
+            <a-slider
+              v-model="maxExpandNodes"
+              :min="10"
+              :max="totalNodes || 10"
+              :step="10"
+              :disabled="sliderDisabled"
+              @change="handleMaxNodesChange"
+            />
+            <button class="step-btn" :disabled="sliderDisabled" @click="handleStep(10)">
+              <icon-plus />
+            </button>
+          </div>
+          <div class="slider-scale">
+            <span>10</span>
+            <span>{{ totalNodes }}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="js">
+import { ref, inject, onMounted, watch } from 'vue';
+import { Modal } from '@arco-design/web-vue';
+import { graphService } from './graph'
+
+const props = defineProps({
+  currentNodes: {
+    type: Number,
+    default: 0
+  },
+  // 当前模式(ability=能力画像),库内总数按模式区分统计口径
+  mode: {
+    type: String,
+    default: 'analysis'
+  }
+});
+
+const emit = defineEmits(['updateMaxNodes', 'updateNoLimit']);
+
+const neo4j = inject('neo4j');
+const isCollapsed = ref(false);
+// 确认框是否已打开,防止重复弹出
+const confirming = ref(false);
+// 滑块是否禁用(确认框打开期间禁用,防止继续拖动)
+const sliderDisabled = ref(false);
+
+// 库内可展开节点总数(作为滑块上限)
+const totalNodes = ref(0);
+// 最大展开节点数(画布全局节点总数上限),默认 300
+const maxExpandNodes = ref(300);
+// 是否已解除节点数量限制(用户在达到 1000 时确认继续后置为 true,后续不再控制)
+const noLimit = ref(false);
+
+const handleMaxNodesChange = (val) => {
+  if (val == null) return;
+  // 达到 1000 且当前仍受限、且确认框未打开 → 弹确认
+  if (val >= 1000 && !noLimit.value && !confirming.value) {
+    confirming.value = true;
+    sliderDisabled.value = true;
+    Modal.confirm({
+      title: '节点数量提示',
+      content: '节点数量太多可能导致页面卡顿,是否继续?',
+      okText: '继续',
+      cancelText: '取消',
+      onOk: () => {
+        confirming.value = false;
+        sliderDisabled.value = false;
+        noLimit.value = true;
+        localStorage.setItem('max_expand_no_limit', '1');
+        localStorage.setItem('max_expand_nodes', String(val));
+        emit('updateNoLimit', true);
+        emit('updateMaxNodes', val);
+      },
+      onCancel: () => {
+        confirming.value = false;
+        sliderDisabled.value = false;
+        maxExpandNodes.value = 990;
+        localStorage.setItem('max_expand_nodes', '990');
+        emit('updateMaxNodes', 990);
+      },
+      onClose: () => {
+        confirming.value = false;
+        sliderDisabled.value = false;
+      }
+    });
+    return;
+  }
+  // 确认框已打开时,直接忽略本次变化
+  if (confirming.value) return;
+  // 正常设值
+  localStorage.setItem('max_expand_nodes', String(val));
+  emit('updateMaxNodes', val);
+  // 从超量状态调回 1000 以内,恢复限制
+  if (val < 1000 && noLimit.value) {
+    noLimit.value = false;
+    localStorage.removeItem('max_expand_no_limit');
+    emit('updateNoLimit', false);
+  }
+};
+
+// 步进按钮:每次 ±10,对齐滑块吸附点,复用 handleMaxNodesChange 的确认弹窗/持久化逻辑
+const handleStep = (delta) => {
+  if (sliderDisabled.value) return;
+  const max = totalNodes.value || 10;
+  // 对齐到 10 的倍数,避免与滑块吸附点错位
+  let next = Math.round((maxExpandNodes.value + delta) / 10) * 10;
+  next = Math.min(Math.max(next, 10), max);
+  if (next === maxExpandNodes.value) return;
+  maxExpandNodes.value = next;
+  handleMaxNodesChange(next);
+};
+
+// 按模式查询库内总数:能力画像=维度+能力项+实体;普通=结构节点(Book/Part/Chapter/Section/Mu)+实体
+const loadTotalNodes = async () => {
+  try {
+    totalNodes.value = await graphService.countAllNodes(neo4j, props.mode === 'ability');
+  } catch (e) {
+    console.error('查询节点总数失败:', e);
+  }
+};
+
+onMounted(async () => {
+  await loadTotalNodes();
+  // 初始化时同步 noLimit 状态给父组件
+  emit('updateNoLimit', noLimit.value);
+  emit('updateMaxNodes', maxExpandNodes.value);
+});
+
+// 模式切换时重新统计库内总数
+watch(() => props.mode, () => {
+  loadTotalNodes();
+});
+</script>
+
+<style scoped lang="css">
+/* 外层只负责定位,不承载卡片样式,收起后无残留白底 */
+.config-wrapper {
+  position: relative;
+}
+
+/* 收起时只保留 toggle 按钮的高度 */
+.config-wrapper.collapsed {
+  height: 48px;
+}
+
+/* 收起/展开按钮(与节点统计悬浮窗一致) */
+.config-toggle {
+  position: absolute;
+  left: -24px;
+  top: 50%;
+  transform: translateY(-50%);
+  width: 24px;
+  height: 48px;
+  background: #fff;
+  border: 1px solid #e5e6eb;
+  border-right: none;
+  border-radius: 4px 0 0 4px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  color: #86909c;
+  transition: all 0.2s ease;
+  z-index: 21;
+}
+
+.config-toggle:hover {
+  background: #f2f3f5;
+  color: #165dff;
+}
+
+/* 卡片面板(标题栏 + 内容),收起时整体隐藏 */
+.config-panel {
+  width: 280px;
+  background: #fff;
+  border-radius: 12px;
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+  overflow: hidden;
+}
+
+.config-header {
+  padding: 14px 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;
+}
+
+.config-body {
+  padding: 16px 20px 18px;
+}
+
+/* 三个统计指标 */
+.stats-row {
+  display: flex;
+  gap: 8px;
+  margin-bottom: 16px;
+}
+
+.stat-box {
+  flex: 1;
+  text-align: center;
+  padding: 10px 4px 8px;
+  border-radius: 10px;
+  background: #f7f8fa;
+}
+
+.stat-box.current {
+  background: #e8f3ff;
+}
+
+.stat-box.limit {
+  background: #fff3e8;
+}
+
+.stat-box.total {
+  background: #f2f0fb;
+}
+
+.stat-num {
+  font-size: 22px;
+  font-weight: 700;
+  line-height: 1.1;
+}
+
+.stat-box.current .stat-num {
+  color: #165dff;
+}
+
+.stat-box.limit .stat-num {
+  color: #ff7d00;
+}
+
+.stat-box.total .stat-num {
+  color: #722ed1;
+}
+
+.stat-name {
+  font-size: 11px;
+  color: #86909c;
+  margin-top: 4px;
+}
+
+/* 滑块区域 */
+.slider-section {
+  padding-top: 2px;
+}
+
+.slider-label {
+  font-size: 13px;
+  color: #4e5969;
+  margin-bottom: 2px;
+  font-weight: 500;
+}
+
+.slider-section :deep(.arco-slider) {
+  padding: 10px 0 4px;
+}
+
+/* 滑块 + 左右步进按钮 */
+.slider-with-btns {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.slider-with-btns :deep(.arco-slider) {
+  flex: 1;
+  min-width: 0;
+}
+
+.step-btn {
+  width: 24px;
+  height: 24px;
+  padding: 0;
+  border-radius: 50%;
+  border: 1px solid #e5e6eb;
+  background: #fff;
+  color: #4e5969;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  flex-shrink: 0;
+  font-size: 14px;
+  transition: all 0.2s ease;
+}
+
+.step-btn:hover:not(:disabled) {
+  border-color: #667eea;
+  color: #667eea;
+  background: #f0f2ff;
+}
+
+.step-btn:disabled {
+  cursor: not-allowed;
+  opacity: 0.4;
+}
+
+.slider-section :deep(.arco-slider-track) {
+  height: 8px;
+  background: #f2f3f5;
+  border-radius: 4px;
+}
+
+.slider-section :deep(.arco-slider-track::before) {
+  background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
+  border-radius: 4px;
+}
+
+.slider-section :deep(.arco-slider-handle) {
+  width: 20px;
+  height: 20px;
+  background: #fff;
+  border: 3px solid #667eea;
+  box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4);
+  margin-top: -6px;
+}
+
+.slider-section :deep(.arco-slider-handle:hover) {
+  border-color: #764ba2;
+  box-shadow: 0 2px 12px rgba(118, 75, 162, 0.5);
+  transform: scale(1.1);
+}
+
+.slider-scale {
+  display: flex;
+  justify-content: space-between;
+  font-size: 13px;
+  color: #4e5969;
+  font-weight: 500;
+  margin-top: 4px;
+}
+</style>

+ 72 - 32
src/views/graph-view/NodeCountStatisticize.vue

@@ -20,11 +20,11 @@
       </div>
       
       <div class="statistics-content">
-        <div 
-          v-for="(stat, label) in statistics" 
-          :key="label"
+        <div
+          v-for="stat in statistics"
+          :key="stat.key"
           class="stat-item"
-          @click="handleStatClick(stat.name)"
+          @click="handleStatClick(stat)"
         >
           <div class="stat-header">
             <div class="color-dot" :style="{ backgroundColor: stat.color }"></div>
@@ -32,11 +32,11 @@
             <span class="stat-count">{{ stat.count }}</span>
           </div>
           <div class="progress-bar">
-            <div 
-              class="progress-fill" 
-              :style="{ 
-                width: stat.percentage + '%', 
-                backgroundColor: stat.color 
+            <div
+              class="progress-fill"
+              :style="{
+                width: stat.percentage + '%',
+                backgroundColor: stat.color
               }"
             ></div>
           </div>
@@ -48,7 +48,7 @@
           <span>暂无数据</span>
         </div>
       </div>
-      
+
       <div class="panel-footer">
         <button class="cancel-highlight-btn" @click="$emit('cancelHighlight')">
           <icon-highlight-off /> 取消高亮
@@ -71,14 +71,17 @@ const props = defineProps({
   }
 });
 
-const emit = defineEmits(['refresh', 'highlight', 'cancelHighlight']);
+const emit = defineEmits(['refresh', 'highlight', 'highlightLevel', 'cancelHighlight']);
 
 const isCollapsed = ref(false);
 
-// 点击统计项时触发高亮
-const handleStatClick = (labelName) => {
-  // console.log(labelName)
-  emit('highlight', labelName);
+// 点击统计项时触发高亮:能力项按层次分组 → 层次高亮;其余 → 标签高亮
+const handleStatClick = (stat) => {
+  if (stat.level) {
+    emit('highlightLevel', stat.level);
+  } else {
+    emit('highlight', stat.name);
+  }
 };
 
 const node_label_maps = {
@@ -88,7 +91,10 @@ const node_label_maps = {
   "Section": "rgba(102, 191, 102, 0.8)",
   "Mu": "rgba(99, 179, 237, 0.8)",
   'EntityType': '#F15D5D',
-  'Entity': '#CD96CD'
+  'Entity': '#CD96CD',
+  // 能力画像节点(与画布配色一致)
+  'AbilityDimension': '#722ED1',
+  'Question': '#FADC19'
 };
 
 const label_name_maps = {
@@ -96,7 +102,17 @@ const label_name_maps = {
   'Part': '部分',
   'Chapter': '章节',
   'EntityType': '实体类型',
-  'Entity': '实体'
+  'Entity': '实体',
+  'AbilityDimension': '维度',
+  'Question': '题目'
+};
+
+// 能力项层次色(与画布 Ability 节点配色一致:懂蓝/会绿/通橙/创红)
+const ability_level_colors = {
+  '懂': '#165DFF',
+  '会': '#00B42A',
+  '通': '#FF7D00',
+  '创': '#F53F3F'
 };
 
 const totalCount = computed(() => props.nodes.length);
@@ -107,23 +123,45 @@ const statistics = computed(() => {
   }
 
   const labelCounts = {};
-  
+
   props.nodes.forEach(node => {
     const labels = node.data?.labels || [];
     const mainLabel = labels[0];
-    if (mainLabel) {
+    if (!mainLabel) return;
+    // 能力项按层次分组(画布上按层次上色,统计口径保持一致)
+    if (mainLabel === 'Ability') {
+      const level = node.data?.level || '未分层';
+      const key = `Ability:${level}`;
+      labelCounts[key] = (labelCounts[key] || 0) + 1;
+    } else {
       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
-    }))
+    .map(([key, count]) => {
+      // 能力项·层次 分组项:颜色与画布层次色一致,点击走层次高亮
+      if (key.startsWith('Ability:')) {
+        const level = key.slice('Ability:'.length);
+        return {
+          key,
+          name: key,
+          level: level === '未分层' ? null : level,
+          label: `能力项·${level}`,
+          count,
+          color: ability_level_colors[level] || '#23B8D5',
+          percentage: (count / props.nodes.length) * 100
+        };
+      }
+      return {
+        key,
+        label: label_name_maps[key] || key,
+        name: key,
+        count,
+        color: node_label_maps[key] || '#999999',
+        percentage: (count / props.nodes.length) * 100
+      };
+    })
     .sort((a, b) => b.count - a.count);
 });
 </script>
@@ -131,14 +169,16 @@ const statistics = computed(() => {
 <style scoped lang="css">
 /* 悬浮窗容器 */
 .statistics-wrapper {
-  position: absolute;
-  top: 80px;
-  right: 16px;
-  z-index: 20;
+  position: relative;
   display: flex;
   transition: all 0.3s ease;
 }
 
+/* 收起时只保留 toggle 按钮的高度 */
+.statistics-wrapper.collapsed {
+  height: 48px;
+}
+
 /* 收起/展开按钮 */
 .statistics-toggle {
   position: absolute;
@@ -176,7 +216,7 @@ const statistics = computed(() => {
   border-radius: 12px;
   box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
   overflow: hidden;
-  min-width: 240px;
+  width: 280px;
   transition: all 0.3s ease;
 }
 
@@ -232,7 +272,7 @@ const statistics = computed(() => {
 
 .statistics-content {
   padding: 16px 20px;
-  max-height: 390px;
+  max-height: 350px;
   overflow-y: auto;
 }
 

+ 429 - 62
src/views/graph-view/graph.js

@@ -1,39 +1,104 @@
+// C2 防护:边类型黑名单(模块级常量,参数化传入,勿拼字符串)
+// 普通模式:HAS_QUESTION + 能力画像全部边类型(支撑能力/考查能力/支撑维度/属于层次/对应模块)
+const HIDDEN_REL_TYPES = ['HAS_QUESTION', '支撑能力', '考查能力', '支撑维度', '属于层次', '对应模块'];
+// 能力画像模式:支撑能力仅由抽屉"展开支撑子图"专用查询(Q3a)消费,通用展开路径同样屏蔽
+const HIDDEN_REL_TYPES_ABILITY = ['HAS_QUESTION', '考查能力', '支撑维度', '属于层次', '对应模块'];
+
+// 边显示名:能力画像的"支撑能力/考查能力"边附加置信度(confidence 是过滤的第一口径)
+const formatEdgeLabel = (r_info) => {
+  const type = r_info.type || r_info.props?.label || '相关';
+  const c = r_info.props?.confidence;
+  return c ? `${type}·${c}` : type;
+};
+
+// 节点 data 组装:能力画像属性(ability_id/level/definition 等)一并带上,
+// 供画布层次上色与能力项抽屉使用
+const toNodeData = (info) => {
+  const p = info.props || {};
+  return {
+    text: p.name || '',
+    elementId: info.elementId,
+    labels: info.labels,
+    ...(p.ability_id !== undefined && { ability_id: p.ability_id }),
+    ...(p.level !== undefined && { level: p.level }),
+    ...(p.definition !== undefined && { definition: p.definition }),
+    ...(p.knowledge_support !== undefined && { knowledge_support: p.knowledge_support }),
+    ...(p.star !== undefined && { star: p.star })
+  };
+};
+
 export const graphService = {
   /**
    * 获取图谱的根节点
+   * 能力画像模式:以 6 个维度(AbilityDimension)为根(维度有"支撑维度"入边,
+   * 不能用 NOT ()-[]->(n) 判根,必须按标签直查)
    */
-  async findRootNodes(neo4jInstance) {
-    const cypher = `
+  async findRootNodes(neo4jInstance, includeAbility = false) {
+    // 能力画像模式:6 个维度为根,节点大小按支撑知识点总数编码(如 877/818/672/653/637/419);
+    // 不走 NOT ()-[]->(n) 判根(维度有"支撑维度"入边),按标签直查
+    const cypher = includeAbility ? `
+    MATCH (n:AbilityDimension)
+    OPTIONAL MATCH ()-[r:支撑能力]->( )-[:包含]->(n)
+    WITH n, count(r) AS supportCount
+    RETURN id(n) AS id, elementId(n) AS elementId, labels(n) as labels,
+           properties(n) AS props, supportCount
+    ` : `
     MATCH (n)
     WHERE NOT ()-[]->(n) AND NOT n:Question
+      AND NOT n:AbilityDimension AND NOT n:Ability AND NOT n:AbilityLevel
     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,
+      supportCount: record.supportCount ?? 0,
       ...record.props // 展开所有属性,比如 name, group 等
     }));
   },
   /**
    * 根据节点ID获取节点信息   WITH n,r,m LIMIT 50 
    * */
-  async findNodeById(neo4jInstance, nodeId) {
-    const cypher = `
+  async findNodeById(neo4jInstance, nodeId, includeAbility = false) {
+    // 能力画像模式:双向展开(Entity─支撑能力→Ability 等入边仅由 Q3a 专用查询消费,
+    // 通用展开路径屏蔽全部能力画像边,画布只出 维度─包含→能力项);
+    // AbilityLevel 不进画布(层次降维为 Ability 颜色属性)
+    const cypher = includeAbility ? `
+    MATCH (n)
+    WHERE id(n) = ${nodeId}
+    OPTIONAL MATCH (n)-[r1]->(m1)
+      WHERE NOT type(r1) IN $hidden AND NOT m1:AbilityLevel
+        AND (NOT n:Entity OR m1:Entity)
+    OPTIONAL MATCH (m2)-[r2]->(n)
+      WHERE NOT type(r2) IN $hidden AND NOT m2:AbilityLevel
+        AND (NOT n:Entity OR m2:Entity)
+    WITH n,
+         CASE WHEN r1 IS NOT NULL AND m1 IS NOT NULL THEN {rel: r1, other: m1} ELSE null END AS outRel,
+         CASE WHEN r2 IS NOT NULL AND m2 IS NOT NULL THEN {rel: r2, other: m2} ELSE null END AS inRel
+    WITH n, [x IN [outRel, inRel] WHERE x IS NOT NULL] AS rels
+    UNWIND rels AS rel
+    WITH n, rel.rel AS r, rel.other AS m
+    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
+    ` : `
     MATCH (n)-[r]->(m) 
     WHERE id(n) = ${nodeId}
-      AND type(r) <> 'HAS_QUESTION'
-  
+      AND NOT type(r) IN $hidden
+      AND NOT m:AbilityDimension AND NOT m:Ability AND NOT m:AbilityLevel
+    
     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);
+    const records = await neo4jInstance.execute(cypher, {
+      hidden: includeAbility ? HIDDEN_REL_TYPES_ABILITY : HIDDEN_REL_TYPES
+    });
 
     // 将 records 转换为 G6 需要的 {nodes:[], edges:[]} 格式
     const nodeMap = new Map();
@@ -51,12 +116,7 @@ export const graphService = {
         // 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 })
-          }
+          data: toNodeData(n_info)
         });
       }
 
@@ -64,12 +124,7 @@ export const graphService = {
       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 })
-          }
+          data: toNodeData(m_info)
         });
       }
 
@@ -79,7 +134,7 @@ export const graphService = {
           id: String(r_info.elementId),
           source: String(n_info.id),
           target: String(m_info.id),
-          label: r_info.type || r_info.props.label || '相关',
+          label: formatEdgeLabel(r_info),
           // sources: r_info.props?.sources?r_info.props?.sources:"[]",
         });
       }
@@ -98,16 +153,22 @@ export const graphService = {
    * 根据节点ID查询节点的双向关系(网状结构)
    * 包括入边和出边,展示节点的完整关联路径
    */
-  async findNodeRelations(neo4jInstance, nodeId) {
+  async findNodeRelations(neo4jInstance, nodeId, includeAbility = false) {
+    // 普通模式剔除能力画像三类节点;能力画像模式保留但排除 AbilityLevel(层次不进画布);
+    // 能力画像模式下展开实体时仅保留实体邻居(过滤 Mu/Section 等结构节点)
+    const filterM = includeAbility ? ' AND NOT m:AbilityLevel AND (NOT n:Entity OR m:Entity)'
+      : ' AND NOT m:AbilityDimension AND NOT m:Ability AND NOT m:AbilityLevel';
+    const filterP = includeAbility ? ' AND NOT p:AbilityLevel AND (NOT n:Entity OR p:Entity)'
+      : ' AND NOT p:AbilityDimension AND NOT p:Ability AND NOT p:AbilityLevel';
     const cypher = `
       MATCH (n)
       WHERE elementId(n) = $nodeId OR id(n) = toInteger($nodeId)
       
       OPTIONAL MATCH (n)-[r1]->(m)
-      WHERE type(r1) <> 'HAS_QUESTION'
+      WHERE NOT type(r1) IN $hidden${filterM}
       
       OPTIONAL MATCH (p)-[r2]->(n)
-      WHERE type(r2) <> 'HAS_QUESTION'
+      WHERE NOT type(r2) IN $hidden${filterP}
       
       WITH n, m, r1, p, r2
       
@@ -117,7 +178,7 @@ export const graphService = {
       UNWIND allNodes AS n1
       UNWIND allNodes AS n2
       MATCH (n1)-[r]->(n2)
-      WHERE type(r) <> 'HAS_QUESTION' AND r IS NOT NULL
+      WHERE NOT type(r) IN $hidden AND r IS NOT NULL
       
       RETURN 
         {id: id(n1), elementId: elementId(n1), labels: labels(n1), props: properties(n1)} AS n_info,
@@ -125,7 +186,10 @@ export const graphService = {
         {id: id(n2), elementId: elementId(n2), labels: labels(n2), props: properties(n2)} AS m_info
     `;
     
-    const records = await neo4jInstance.execute(cypher, { nodeId });
+    const records = await neo4jInstance.execute(cypher, {
+      nodeId,
+      hidden: includeAbility ? HIDDEN_REL_TYPES_ABILITY : HIDDEN_REL_TYPES
+    });
 
     const nodeMap = new Map();
     const edgeMap = new Map();
@@ -136,24 +200,14 @@ export const graphService = {
       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 })
-          }
+          data: toNodeData(n_info)
         });
       }
 
       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 })
-          }
+          data: toNodeData(m_info)
         });
       }
 
@@ -162,7 +216,7 @@ export const graphService = {
           id: String(r_info.elementId),
           source: String(n_info.id),
           target: String(m_info.id),
-          label: r_info.type || r_info.props.label || '相关',
+          label: formatEdgeLabel(r_info),
         });
       }
     });
@@ -197,18 +251,22 @@ export const graphService = {
    * 根据父节点ID,获取其下属的子图
    * (包含父节点、所有子节点,以及这个家族圈子内部的所有交叉关系)
    */
-  async findSubgraphByParentId(neo4jInstance, parentNodeId) {
+  async findSubgraphByParentId(neo4jInstance, parentNodeId, includeAbility = false) {
     // 1. MATCH 找到指定的父节点 n
     // 2. OPTIONAL MATCH (n)-[r1]->(m) 顺着箭头找出所有的子节点 m
     // 3. 限制数量,并将父节点与所有子节点合并为 allNodes 节点池
     // 4. 在节点池内部,寻找任意两点之间的所有连线 r2(从而把子节点之间的关系也查出来)
+    // 能力画像模式排除 AbilityLevel(层次降维为颜色属性,不进画布);
+    // 展开实体时仅保留实体邻居(过滤 Mu/Section 等结构节点及其他类型)
+    const abilityFilter = includeAbility ? ' AND NOT m:AbilityLevel AND (NOT n:Entity OR m:Entity)'
+      : ' AND NOT m:AbilityDimension AND NOT m:Ability AND NOT m:AbilityLevel';
     const cypher = `
       MATCH (n)
       WHERE elementId(n) = $parentNodeId OR id(n) = toInteger($parentNodeId)
       
       // 明确单向箭头,只找父节点指向的子节点
       OPTIONAL MATCH (n)-[r1]->(m)
-      WHERE type(r1) <> 'HAS_QUESTION'
+      WHERE NOT type(r1) IN $hidden${abilityFilter}
       
       WITH n, m LIMIT 60
       
@@ -219,7 +277,7 @@ export const graphService = {
       UNWIND allNodes AS n1
       UNWIND allNodes AS n2
       MATCH (n1)-[r2]->(n2)
-      WHERE type(r2) <> 'HAS_QUESTION'
+      WHERE NOT type(r2) IN $hidden
       
       RETURN 
         {id: id(n1), elementId: elementId(n1), labels: labels(n1), props: properties(n1)} AS n_info,
@@ -227,7 +285,10 @@ export const graphService = {
         {id: id(n2), elementId: elementId(n2), labels: labels(n2), props: properties(n2)} AS m_info
     `;
     
-    const records = await neo4jInstance.execute(cypher, { parentNodeId: parentNodeId });
+    const records = await neo4jInstance.execute(cypher, {
+      parentNodeId: parentNodeId,
+      hidden: includeAbility ? HIDDEN_REL_TYPES_ABILITY : HIDDEN_REL_TYPES
+    });
 
     const nodeMap = new Map();
     const edgeMap = new Map();
@@ -265,7 +326,7 @@ export const graphService = {
           id: String(r_info.elementId),
           source: String(n_info.elementId),
           target: String(m_info.elementId),
-          label: r_info.type || r_info.props.label || '相关',
+          label: formatEdgeLabel(r_info),
         });
       }
     });
@@ -296,14 +357,273 @@ export const graphService = {
     };
   },
 
+  /**
+   * 查询某个父节点的直接子节点总数
+   */
+  async countChildren(neo4jInstance, parentNodeId, includeAbility = false) {
+    // 能力画像模式:双向计数(出边邻居 + 入边邻居),排除 AbilityLevel;
+    // 支撑能力等边在黑名单内,此计数反映"画布可见邻居数"
+    const cypher = includeAbility ? `
+      MATCH (n)
+      WHERE elementId(n) = $parentNodeId OR id(n) = toInteger($parentNodeId)
+      OPTIONAL MATCH (n)-[r1]->(m1)
+        WHERE NOT type(r1) IN $hidden AND NOT m1:AbilityLevel
+          AND (NOT n:Entity OR m1:Entity)
+      OPTIONAL MATCH (m2)-[r2]->(n)
+        WHERE NOT type(r2) IN $hidden AND NOT m2:AbilityLevel
+          AND (NOT n:Entity OR m2:Entity)
+      WITH collect(DISTINCT m1) + collect(DISTINCT m2) AS allM
+      RETURN size([x IN allM WHERE x IS NOT NULL]) AS total
+    ` : `
+      MATCH (n)-[r]->(m)
+      WHERE (elementId(n) = $parentNodeId OR id(n) = toInteger($parentNodeId))
+      AND NOT type(r) IN $hidden
+      AND NOT m:AbilityDimension AND NOT m:Ability AND NOT m:AbilityLevel
+      RETURN count(DISTINCT m) AS total
+    `;
+
+    const records = await neo4jInstance.execute(cypher, {
+      parentNodeId,
+      hidden: includeAbility ? HIDDEN_REL_TYPES_ABILITY : HIDDEN_REL_TYPES
+    });
+    return records[0]?.total || 0;
+  },
+
+  /**
+   * 能力画像:查询能力项的层次详情(唯一需要查 AbilityLevel 节点的场景)
+   * 取 bloom 区间映射与 definition,用于抽屉展示
+   */
+  async findAbilityLevelDetail(neo4jInstance, abilityId) {
+    const cypher = `
+      MATCH (a:Ability {ability_id: $abilityId})-[:属于层次]->(l:AbilityLevel)
+      RETURN properties(l) AS levelProps
+    `;
+    const records = await neo4jInstance.execute(cypher, { abilityId });
+    return records[0]?.levelProps || null;
+  },
+
+  /**
+   * 能力画像:本维度的能力层次分布(聚合统计小字,如"会 3 · 通 2")
+   */
+  async findDimensionLevelDistribution(neo4jInstance, dimensionId) {
+    const cypher = `
+      MATCH (d:AbilityDimension {dimension_id: $dimensionId})-[:包含]->(a:Ability)
+      RETURN a.level AS level, count(a) AS cnt
+      ORDER BY cnt DESC
+    `;
+    const records = await neo4jInstance.execute(cypher, { dimensionId });
+    return records.map(r => ({ level: r.level, count: r.cnt }));
+  },
+
+  /**
+   * Q1 能力全景(首页态 + 一级展开)
+   * 每行一个能力项,带支撑统计(total / high_cnt);
+   * 首页态按维度聚合 supportCount(节点大小编码),展开态取该维度的能力项行
+   */
+  async findAbilityOverview(neo4jInstance) {
+    const cypher = `
+      MATCH (d:AbilityDimension)-[:包含]->(a:Ability)-[:属于层次]->(l:AbilityLevel)
+      OPTIONAL MATCH (e:Entity)-[s:支撑能力]->(a)
+      RETURN id(d) AS d_id, elementId(d) AS d_elementId,
+             d.dimension_id AS dimension_id, d.name AS dimension_name,
+             id(a) AS a_id, elementId(a) AS a_elementId,
+             a.ability_id AS ability_id, a.name AS ability_name,
+             a.level AS level, a.definition AS definition, a.knowledge_support AS knowledge_support,
+             l.code AS level_code, l.bloom AS level_bloom, l.definition AS level_definition,
+             count(s) AS total,
+             sum(CASE WHEN s.confidence='高' THEN 1 ELSE 0 END) AS high_cnt
+      ORDER BY d.dimension_id, a.ability_id
+    `;
+    return await neo4jInstance.execute(cypher);
+  },
+
+  /**
+   * Q2 能力详情:支撑知识点列表(置信度分组排序:高→中→低,star 优先)
+   * 供能力项抽屉展示(name/type/star/confidence/reasoning)
+   */
+  async findAbilitySupportList(neo4jInstance, abilityId) {
+    const cypher = `
+      MATCH (e:Entity)-[s:支撑能力]->(a:Ability {ability_id: $abilityId})
+      RETURN e.name AS name, e.type AS type, e.star AS star,
+             s.confidence AS confidence, s.reasoning AS reasoning
+      ORDER BY CASE s.confidence WHEN '高' THEN 1 WHEN '中' THEN 2 ELSE 3 END,
+               e.star DESC
+    `;
+    return await neo4jInstance.execute(cypher, { abilityId });
+  },
+
+  /**
+   * Q3a 支撑子图·节点:Entity─支撑能力→Ability,高置信优先 + star 优先,LIMIT 默认 200
+   */
+  async findAbilitySupportSubgraph(neo4jInstance, abilityId, limit = 200) {
+    const cypher = `
+      MATCH (e:Entity)-[r:支撑能力]->(a:Ability {ability_id: $abilityId})
+      WITH e, r, a
+      ORDER BY CASE r.confidence WHEN '高' THEN 1 WHEN '中' THEN 2 WHEN '低' THEN 3 ELSE 4 END, e.star DESC
+      LIMIT toInteger($limit)
+      RETURN
+        {id: id(a), elementId: elementId(a), labels: labels(a), props: properties(a)} AS n_info,
+        {id: id(r), elementId: elementId(r), type: type(r), props: properties(r)} AS r_info,
+        {id: id(e), elementId: elementId(e), labels: labels(e), props: properties(e)} AS m_info
+    `;
+    const records = await neo4jInstance.execute(cypher, { abilityId, limit });
+
+    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: toNodeData(n_info)
+        });
+      }
+      if (m_info && !nodeMap.has(m_info.id)) {
+        nodeMap.set(m_info.id, {
+          id: String(m_info.id),
+          data: toNodeData(m_info)
+        });
+      }
+      if (r_info && !edgeMap.has(r_info.id)) {
+        edgeMap.set(r_info.id, {
+          id: String(r_info.elementId),
+          source: String(m_info.id),
+          target: String(n_info.id),
+          label: formatEdgeLabel(r_info),
+        });
+      }
+    });
+
+    return {
+      nodes: Array.from(nodeMap.values()),
+      edges: Array.from(edgeMap.values())
+    };
+  },
+
+  /**
+   * Q3b 支撑子图·实体间语义边:仅在 Q3a 返回的实体名列表内,
+   * 找 前序/关联/因果/对比 四类语义关系($names 为实体名列表)
+   */
+  async findEntitySemanticEdges(neo4jInstance, names) {
+    if (!names || names.length < 2) return [];
+    const cypher = `
+      UNWIND $names AS n1
+      UNWIND $names AS n2
+      MATCH (a:Entity {name: n1})-[r]->(b:Entity {name: n2})
+      WHERE type(r) IN ['前序', '关联', '因果', '对比']
+      RETURN n1, n2, type(r) AS rel
+    `;
+    return await neo4jInstance.execute(cypher, { names });
+  },
+
+  /**
+   * Q4 能力可选题量:考查该能力项的题目数(本阶段仅数字展示,不做跳转)
+   */
+  async findAbilityQuestionCount(neo4jInstance, abilityId) {
+    const cypher = `
+      MATCH (a:Ability {ability_id: $abilityId})<-[:考查能力]-(q:Question)
+      RETURN count(q) AS q_cnt
+    `;
+    const records = await neo4jInstance.execute(cypher, { abilityId });
+    return records[0]?.q_cnt || 0;
+  },
+
+  /**
+   * 分页查询父节点的子图(用于子节点超过阈值时分批加载)
+   * 行为与 findNodeById 一致(单向父子边,数字id),仅加分页
+   */
+  async findSubgraphByParentIdPaged(neo4jInstance, parentNodeId, skip, limit, includeAbility = false) {
+    // 分页查询(普通模式单向父子边);能力画像模式为双向(入边邻居)
+    const cypher = includeAbility ? `
+      MATCH (n)
+      WHERE (elementId(n) = $parentNodeId OR id(n) = toInteger($parentNodeId))
+      OPTIONAL MATCH (n)-[r1]->(m1)
+        WHERE NOT type(r1) IN $hidden AND NOT m1:AbilityLevel
+          AND (NOT n:Entity OR m1:Entity)
+      OPTIONAL MATCH (m2)-[r2]->(n)
+        WHERE NOT type(r2) IN $hidden AND NOT m2:AbilityLevel
+          AND (NOT n:Entity OR m2:Entity)
+      WITH n,
+           CASE WHEN r1 IS NOT NULL AND m1 IS NOT NULL THEN {rel: r1, other: m1} ELSE null END AS outRel,
+           CASE WHEN r2 IS NOT NULL AND m2 IS NOT NULL THEN {rel: r2, other: m2} ELSE null END AS inRel
+      WITH n, [x IN [outRel, inRel] WHERE x IS NOT NULL] AS rels
+      UNWIND rels AS rel
+      WITH n, rel.rel AS r, rel.other AS m
+      ORDER BY elementId(m), elementId(r)
+      SKIP toInteger($skip) LIMIT toInteger($limit)
+      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
+    ` : `
+      MATCH (n)-[r]->(m)
+      WHERE (elementId(n) = $parentNodeId OR id(n) = toInteger($parentNodeId))
+        AND NOT type(r) IN $hidden
+        AND NOT m:AbilityDimension AND NOT m:Ability AND NOT m:AbilityLevel
+      WITH n, r, m
+      ORDER BY elementId(m)
+      SKIP toInteger($skip) LIMIT toInteger($limit)
+      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, {
+      parentNodeId, skip, limit,
+      hidden: includeAbility ? HIDDEN_REL_TYPES_ABILITY : HIDDEN_REL_TYPES
+    });
+
+    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)) {
+        nodeMap.set(n_info.id, {
+          id: String(n_info.id),
+          data: toNodeData(n_info)
+        });
+      }
+      // 目标节点 m
+      if (m_info && !nodeMap.has(m_info.id)) {
+        nodeMap.set(m_info.id, {
+          id: String(m_info.id),
+          data: toNodeData(m_info)
+        });
+      }
+      // 关系 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: formatEdgeLabel(r_info),
+        });
+      }
+    });
+
+    return {
+      nodes: Array.from(nodeMap.values()),
+      edges: Array.from(edgeMap.values())
+    };
+  },
+
   /**
    * 根据节点名称模糊查询节点信息
+   * 按模式区分检索范围:
+   * - 普通模式(智能分析/智能组卷):Book/Part/Chapter/Section/Mu 结构节点 + 全部实体节点
+   * - 能力画像模式:维度 + 能力项(懂/会/通/创)+ 全部实体节点
    */
-  async searchNodesByName(neo4jInstance, keyword) {
+  async searchNodesByName(neo4jInstance, keyword, includeAbility = false) {
+    const typeFilter = includeAbility
+      ? `(node:AbilityDimension OR node:Ability OR node:Entity)`
+      : `(node:Book OR node:Part OR node:Chapter OR node:Section OR node:Mu OR node:Entity)`;
     // 1. 同款 Cypher 改造:在数据库直接把数据组装成干净的 JSON 对象
     const cypher = `
     MATCH (node)
-    WHERE (node:Book OR node:Part OR node:Chapter OR node:EntityType OR node:Entity)
+    WHERE ${typeFilter}
       AND node.name CONTAINS $keyword
     WITH collect(node) AS items
     WITH items[0..100] AS nodesList
@@ -311,7 +631,7 @@ export const graphService = {
     
     // 寻找这些节点之间的内部上下级/横向关系
     OPTIONAL MATCH (n)-[r]->(m)
-    WHERE m IN nodesList
+    WHERE m IN nodesList AND NOT type(r) IN $hidden
     
     // 同款 RETURN 打包语法糖,把所有需要的字段一口气全部暴露出来
     RETURN 
@@ -328,7 +648,8 @@ export const graphService = {
 
     // 2. 使用你的自定义注入插件执行查询
     const records = await neo4jInstance.execute(cypher, {
-      keyword
+      keyword,
+      hidden: HIDDEN_REL_TYPES
     });
     // console.log("模糊搜索原始 records:", records);
 
@@ -376,7 +697,7 @@ export const graphService = {
           id: String(r_info.elementId),
           source: String(n_info.id), // 连线起点
           target: String(m_info.id), // 连线终点
-          label: r_info.type || r_info.props.label || '相关',
+          label: formatEdgeLabel(r_info),
           // sources: r_info.props?.sources ? r_info.props.sources : "[]",
         });
       }
@@ -401,16 +722,21 @@ export const graphService = {
     `;
     const records = await neo4jInstance.execute(cypher);
     const props = records[0]?.props || {};
-    
+
+    // 能力画像节点(维度/能力项/层次)用 definition/knowledge_support 等属性展示
     const result = [{
-      mu_id: props.mu_id || '',
+      mu_id: props.mu_id || props.dimension_id || props.ability_id || props.code || '',
       name: props.name || '',
-      path: props.path || '',
-      text: props.description || props.text || '',
+      path: props.path
+        || [props.core_position, props.knowledge_module, props.domain_tag].filter(Boolean).join(' · ')
+        || '',
+      text: props.description || props.text || props.definition
+        || [props.level && `层次:${props.level}`, props.bloom && `Bloom:${props.bloom}`, props.knowledge_support].filter(Boolean).join('\n')
+        || '',
       menuVisible: false,
       menuStyle: {}
     }];
-    
+
     return JSON.stringify(result);
   },
   /**
@@ -420,21 +746,41 @@ export const graphService = {
     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
+    RETURN r.source_sentence as source_sentence, r.slice_id as slice_id, r.chapter as chapter, r.slice_text as slice_text,
+           properties(r) AS props, type(r) AS rel_type
   `;
     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]);
+    const rec = records[0];
+    if (!rec) return "[]";
+
+    // 知识图谱边:有原文切片属性,直接返回
+    if (rec.source_sentence || rec.slice_id || rec.chapter || rec.slice_text) {
+      sources.push(rec);
+      return JSON.stringify(sources);
+    }
+
+    // 能力画像边(支撑能力/考查能力等):展示边属性
+    // 样例: {confidence:"高", level:"会", bloom_level:"L3", knowledge_domain:"质量", reasoning:"..."}
+    const p = rec.props || {};
+    if (Object.keys(p).length > 0) {
+      sources.push({
+        mu_id: rec.rel_type,
+        name: p.confidence ? `置信度:${p.confidence}` : '关系属性',
+        path: [
+          p.level && `层次:${p.level}`,
+          p.bloom_level && `Bloom:${p.bloom_level}`,
+          p.knowledge_domain && `领域:${p.knowledge_domain}`,
+          p.derived === true ? 'derived' : null,
+          p.via_entity && `经由实体:${p.via_entity}`
+        ].filter(Boolean).join(' · '),
+        text: p.reasoning || JSON.stringify(p)
+      });
       return JSON.stringify(sources);
     }
-    // if(records[0])
-    // return records[0].props.sources|| "[]";
+    return "[]";
   },
   /**
    * 根据实体id查询MU中所有信息
@@ -621,5 +967,26 @@ export const graphService = {
     } else {
       return []
     }
+  },
+
+  /**
+   * 查询库内可展开节点总数
+   * 按模式区分统计口径:
+   * - 普通模式(智能分析/智能组卷):Book/Part/Chapter/Section/Mu 结构节点 + 全部实体节点
+   * - 能力画像模式:维度 + 能力项(懂/会/通/创)+ 全部实体节点
+   * @param {Object} neo4jInstance Neo4j 驱动实例
+   * @param {boolean} includeAbility 是否能力画像模式
+   * @returns {Promise<number>} 节点总数
+   */
+  async countAllNodes(neo4jInstance, includeAbility = false) {
+    const cypher = includeAbility
+      ? `MATCH (n)
+         WHERE (n:AbilityDimension OR n:Ability OR n:Entity)
+         RETURN count(n) AS total`
+      : `MATCH (n)
+         WHERE (n:Book OR n:Part OR n:Chapter OR n:Section OR n:Mu OR n:Entity)
+         RETURN count(n) AS total`;
+    const records = await neo4jInstance.execute(cypher);
+    return records[0]?.total || 0;
   }
 }

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů