You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

1694 lines
50 KiB

<template>
<div class="knowledge-graph-container">
<Menu
:initial-active="0"
/>
<main class="main-body">
<header class="top-nav">
<div class="search-container">
<input v-model="searchKeyword" type="text" placeholder="搜索......" @keydown.enter="search"/>
<img src="@/assets/搜索.png" style="cursor: pointer" class="search-btn" @click="search"/>
</div>
<div class="legend-box">
<div v-for="(item, index) in legendItems" :key="index" class="legend-item">
<div
:style="{
backgroundColor: item.color,
opacity: visibleCategories.has(item.key) ? 1 : 0.3 // ✅ 关键:根据状态调整透明度
}"
class="color-block"
@click="toggleCategory(item.key)"
></div>
<span style=" font-size: 12px;color: rgb(0, 0, 0);font-weight: 600;">{{ item.label }}</span>
</div>
</div>
</header>
<section class="main-content">
<div class="disease-container">
<div class="disease-header" :style="headerStyle">
<div style="display: flex;align-items: center;">
<div class="d-title"><img :src="iconSrc" class="d-icon"/>
<span v-if="typeRadio=== 'Disease'">疾病信息</span>
<span v-if="typeRadio=== 'Drug'">药品信息</span>
<span v-if="typeRadio=== 'Check'">检查信息</span>
</div>
<div v-if="typeRadio=== 'Disease'" class="d-count" :style="headerLabel">{{diseaseCount.toLocaleString()}}种</div>
<div v-if="typeRadio=== 'Drug'" class="d-count" :style="headerLabel">{{ drugCount.toLocaleString() }}种</div>
<div v-if="typeRadio=== 'Check'" class="d-count" :style="headerLabel">{{checkCount.toLocaleString()}}</div>
</div>
<div>
<!-- 替换开始自定义下拉选择器使用 div 实现 -->
<div class="select-container" @click="toggleDropdown">
<!-- 当前选中项显示 -->
<div class="select-display">
<span style="color: #000; background: rgb(255 255 255 / 64%);
padding: 2px 13px;
border-radius: 15px;">{{ currentTypeLabel }}</span>
<img src="../assets/下拉11.png" class="dropdown-icon" />
</div>
<!-- 下拉菜单 -->
<div v-show="isDropdownOpen" class="select-dropdown">
<div
v-for="item in typeOptions"
:key="item.value"
class="select-option"
@click.stop="selectType(item.value)"
>
{{ item.label }}
</div>
</div>
</div>
<!-- ✅ 替换结束 -->
</div>
</div>
<!-- <div>-->
<!-- <el-radio-group v-model="typeRadio" @change="changeTree">-->
<!-- <el-radio-->
<!-- value="Disease"-->
<!-- :class="{'radio-disease': typeRadio === 'Disease'}"-->
<!-- >疾病</el-radio>-->
<!-- <el-radio-->
<!-- value="Drug"-->
<!-- :class="{'radio-drug': typeRadio === 'Drug'}"-->
<!-- >药品</el-radio>-->
<!-- <el-radio-->
<!-- value="Check"-->
<!-- :class="{'radio-check': typeRadio === 'Check'}"-->
<!-- >检查</el-radio>-->
<!-- </el-radio-group>-->
<!-- </div>-->
<div v-if="typeRadio === 'Disease'">
<el-radio-group v-model="DiseaseRadio" @change="changeTree">
<el-radio
value="ICD10"
:class="{'radio-disease': typeRadio === 'Disease'}"
>ICD10</el-radio>
<el-radio
value="Department"
:class="{'radio-disease': typeRadio === 'Disease'}"
>科室</el-radio>
<el-radio
value="SZM"
:class="{'radio-disease': typeRadio === 'Disease'}"
>首字母</el-radio>
</el-radio-group>
</div>
<div v-if="typeRadio === 'Drug'">
<el-radio-group v-model="DrugRadio" @change="changeTree">
<el-radio
value="Subject"
:class="{'radio-drug': typeRadio === 'Drug'}"
>药物分类</el-radio>
<el-radio
value="SZM"
:class="{'radio-drug': typeRadio === 'Drug'}"
>首字母</el-radio>
</el-radio-group>
</div>
<div class="disease-body">
<div class="tree">
<el-tree
:data="treeData"
:props="treeProps"
accordion
expand-on-click-node
:expand-on-click-node="false"
@node-click="handleNodeClick"
>
<template #default="{ node, data }">
<span class="custom-tree-node">
<span class="code">{{ data.code }}</span>
<el-tooltip :content="data.label" placement="top">
<span class="label">{{ data.label }}</span>
</el-tooltip>
</span>
</template>
</el-tree>
</div>
</div>
</div>
<div class="graph-viewport">
<div ref="graphContainer" class="graph-container" id="container"></div>
</div>
</section>
</main>
<!-- 图谱容器 -->
</div>
</template>
<script>
import {
getCheckTree,
getCount,
getDiseaseDepartTree,
getDiseaseTree, getDrugSubjectTree,
getDrugTree,
getGraph,
getTestGraphData
} from "@/api/graph"
import {Graph, Tooltip} from '@antv/g6';
import Menu from "@/components/Menu.vue";
import {a} from "vue-router/dist/devtools-EWN81iOl.mjs";
import Fuse from 'fuse.js';
import {getGraphStyleActive} from "@/api/style";
export default {
name: 'Display',
components: {Menu},
data() {
return {
G6: null, // 添加这个
// 节点样式
nodeShowLabel: true,
nodeFontSize: 12,
nodeFontColor: '#fff',
nodeShape: 'circle',
nodeSize: 50,
nodeFill: '#9FD5FF',
nodeStroke: '#5B8FF9',
nodeLineWidth: 2,
nodeFontFamily: 'Microsoft YaHei, sans-serif',
// 边样式
edgeShowLabel: true,
edgeFontSize: 10,
edgeFontColor: '#666666',
edgeType: 'quadratic',
edgeLineWidth: 2,
edgeStroke: '#b6b2b2',
edgeEndArrow: true,
edgeFontFamily: 'Microsoft YaHei, sans-serif',
defaultData: {
nodes: [
{id: 'A', label: '人工智能'},
{id: 'B', label: '机器学习'},
{id: 'C', label: '深度学习'}
],
edges: [
{source: 'A', target: 'B', label: '包含'},
{source: 'B', target: 'C', label: '子领域'}
]
},
treeData: [],
treeProps: {
children: 'children',
label: 'title' // 虽然用插槽,但 el-tree 内部仍会读取,可留空或任意
},
typeRadio:"Disease",
DiseaseRadio:"ICD10",
drugTree:[],
diseaseDepartTree:[],
diseaseICD10Tree:[],
diseaseSZMTree:[],
checkTree:[],
legendItems: [
{ key: 'Disease', label: '疾病', color: '#EF4444' },
{ key: 'Drug', label: '药品', color: '#91cc75' },
{ key: 'Check', label: '检查', color: '#336eee' },
{ key: 'Symptom', label: '症状', color: '#fac858' },
{ key: 'Other', label: '其他', color: '#59d1d4' }
],
visibleCategories: new Set(), // 先空着
diseaseCount:0,
drugCount:0,
checkCount:0,
originalNodeStyles: new Map(), // 保存原始节点样式
originalEdgeStyles: new Map(), // 保存原始边样式
searchResults: {
nodes: [],
edges: []
},
searchKeyword:"",
drugSubjectTree:[],
DrugRadio:"Subject",
isDropdownOpen: false,
typeOptions: [
{ value: 'Disease', label: '疾病' },
{ value: 'Drug', label: '药品' },
{ value: 'Check', label: '检查' }
],
configs:[],
parsedStyles:{},
enToZhLabelMap: {
Disease: '疾病',
Drug: '药品',
Check: '检查',
Symptom: '症状',
Other: '其他'
}
}
},
computed: {
currentTypeLabel() {
const option = this.typeOptions.find(opt => opt.value === this.typeRadio);
return option ? option.label : '请选择';
},
// 根据当前选择的类型返回不同的背景颜色
headerStyle() {
let backgroundColor = '#fff'; // 默认颜色
switch (this.typeRadio) {
case 'Disease':
backgroundColor = '#990A00'; // 疾病选中时的颜色
break;
case 'Drug':
backgroundColor = '#9ECA7F'; // 药品选中时的颜色
break;
case 'Check':
backgroundColor = '#2265f4'; // 检查选中时的颜色
break;
default:
break;
}
return {
backgroundColor, // 动态设置背景色
};
},
iconSrc() {
switch (this.typeRadio) {
case 'Disease':
return require('@/assets/red.png'); // 疾病的图标
case 'Drug':
return require('@/assets/green.png'); // 药品的图标
case 'Check':
return require('@/assets/blue.png'); // 检查的图标
default:
return require('@/assets/blue.png'); // 默认图标
}
},
headerLabel() {
let backgroundColor = '#fff'; // 默认颜色
switch (this.typeRadio) {
case 'Disease':
backgroundColor = '#790800'; // 疾病选中时的颜色
break;
case 'Drug':
backgroundColor = '#638C0B'; // 药品选中时的颜色
break;
case 'Check':
backgroundColor = '#5384EF'; // 检查选中时的颜色
break;
default:
break;
}
return {
backgroundColor, // 动态设置背景色
};
},
// #790800
},
async mounted() {
this.visibleCategories = new Set(this.legendItems.map(i => i.key));
await this.loadDiseaseICD10TreeData()
this.loadDiseaseDepartTreeData()
this.loadDiseaseSZMTreeData()
this.getCount()
this.loadDrugTreeData()
this.loadCheckTreeData()
this.loadDrugSubjectTreeData()
this.treeData=this.diseaseICD10Tree
await this.$nextTick();
try {
await this.getDefault()
const response = await getTestGraphData(); // 等待 Promise 解析
// === 1. 构建 nodeId → label 映射 ===
const nodeIdToEnLabel = {};
response.nodes.forEach(node => {
nodeIdToEnLabel[node.id] = node.data.label; // e.g. "Disease"
});
// === 2. 处理节点:根据自身 label 设置样式 ===
const updatedNodes = response.nodes.map(node => {
const enLabel = node.data.label;
const zhLabel = this.enToZhLabelMap[enLabel] || '其他'; // 默认回退到“其他”
const styleConf = this.parsedStyles[zhLabel] || {};
return {
...node,
type: styleConf.nodeShape || this.nodeShape,
style: {
size: styleConf.nodeSize || this.nodeSize,
fill: styleConf.nodeFill || this.nodeFill,
stroke: styleConf.nodeStroke || this.nodeStroke,
lineWidth: styleConf.nodeLineWidth || this.nodeLineWidth,
label: styleConf.nodeShowLabel !== undefined ? styleConf.nodeShowLabel : true,
labelFontSize: styleConf.nodeFontSize || this.nodeFontSize,
labelFontFamily: styleConf.nodeFontFamily || this.nodeFontFamily,
labelFill: styleConf.nodeFontColor || this.nodeFontColor
}
};
});
// === 3. 处理边:根据 source 节点的 label 设置样式 ===
const updatedEdges = response.edges.map(edge => {
console.log(edge)
const sourceEnLabel = nodeIdToEnLabel[edge.source]; // e.g. "Disease"
const sourceZhLabel = this.enToZhLabelMap[sourceEnLabel] || '其他';
const styleConf = this.parsedStyles[sourceZhLabel] || {};
return {
...edge,
id: edge.data?.relationship?.id || edge.id,
type: styleConf.edgeType ||this.edgeType,
style: {
endArrow: styleConf.edgeEndArrow !== undefined ? styleConf.edgeEndArrow : true,
stroke: styleConf.edgeStroke || this.edgeStroke,
lineWidth: styleConf.edgeLineWidth || this.edgeLineWidth,
label: styleConf.edgeShowLabel !== undefined ? styleConf.edgeShowLabel : false,
labelFontSize: styleConf.edgeFontSize || this.edgeFontSize,
labelFontFamily: styleConf.edgeFontFamily || this.edgeFontFamily,
labelFill: styleConf.edgeFontColor || this.edgeFontColor
}
};
});
// === 4. 更新图数据 ===
let updatedData = {
nodes: updatedNodes,
edges: updatedEdges
};
console.log(updatedData)
this.defaultData = updatedData
setTimeout(() => {
this.initGraph();
this.buildCategoryIndex();
window.addEventListener('resize', this.handleResize);
}, 1000);
} catch (error) {
console.error('加载图谱数据失败:', error);
}
// try {
// await this.getDefault()
// const response = await getTestGraphData(); // 等待 Promise 解析
// const updatedNodes = response.nodes.map(node => ({
// ...node,
// type: this.nodeShape,
// style:{
// size: this.nodeSize,
// fill: this.nodeFill,
// stroke: this.nodeStroke,
// lineWidth: this.nodeLineWidth,
// label: this.nodeShowLabel,
// labelFontSize: this.nodeFontSize,
// labelFontFamily: this.nodeFontFamily,
// labelFill: this.nodeFontColor,
// }
// }))
// const updatedEdges = response.edges.map(edge => ({
// ...edge,
// id: edge.data.relationship.id,
// type: this.edgeType,
// style: {
// endArrow: this.edgeEndArrow,
// stroke: this.edgeStroke,
// lineWidth: this.edgeLineWidth,
// label: this.edgeShowLabel,
// labelFontSize: this.edgeFontSize,
// labelFontFamily: this.edgeFontFamily,
// labelFill: this.edgeFontColor,
// },
// }))
// const updatedData = {
// nodes: updatedNodes,
// edges: updatedEdges
// }
// this.defaultData = updatedData
// setTimeout(() => {
// this.initGraph();
// this.buildCategoryIndex();
// window.addEventListener('resize', this.handleResize);
// }, 1000);
// } catch (error) {
// console.error('加载图谱数据失败:', error);
// }
},
beforeUnmount() {
if (this._graph!=null){
this._graph.stopLayout();
this.clearGraphState();
this._graph.destroy()
this._graph = null;
}
window.removeEventListener('resize', this.handleResize);
},
watch: {
// 节点相关
nodeShowLabel: 'updateAllNodes',
nodeFontSize: 'updateAllNodes',
nodeFontColor: 'updateAllNodes',
nodeShape: 'updateAllNodes',
nodeSize: 'updateAllNodes',
nodeFill: 'updateAllNodes',
nodeStroke: 'updateAllNodes',
nodeLineWidth: 'updateAllNodes',
nodeFontFamily: 'updateAllNodes',
// 边相关
edgeShowLabel: 'updateAllEdges',
edgeFontSize: 'updateAllEdges',
edgeFontColor: 'updateAllEdges',
edgeType: 'updateAllEdges',
edgeLineWidth: 'updateAllEdges',
edgeStroke: 'updateAllEdges',
edgeEndArrow: 'updateAllEdges',
edgeFontFamily: 'updateAllEdges',
},
methods: {
safeParseStyles(stylesStr) {
try {
return JSON.parse(stylesStr || '{}');
} catch (e) {
console.warn('Failed to parse styles:', stylesStr);
return {};
}
},
async getDefault(){
const response = await getGraphStyleActive();
const data = response.data;
if (!Array.isArray(data) || data.length === 0) {
this.configs = [];
this.parsedStyles = {};
return;
}
// 只取第一个(即 is_active=1 的组)
const activeGroup = data[0];
this.configs = Array.isArray(activeGroup.configs) ? activeGroup.configs : [];
// 构建 label -> style 映射
const styleMap = {};
this.configs.forEach(config => {
const label = config.current_label;
styleMap[label] = this.safeParseStyles(config.styles);
});
this.parsedStyles = styleMap;
console.log(this.parsedStyles)
},
// 切换下拉菜单显隐
toggleDropdown() {
this.isDropdownOpen = !this.isDropdownOpen;
},
// 选择类型并关闭下拉
selectType(value) {
if (this.typeRadio !== value) {
this.typeRadio = value;
this.changeTree(); // 触发数据更新
}
this.isDropdownOpen = false;
},
search() {
const keyword = this.searchKeyword?.trim();
if (!keyword) {
this.clearSearchHighlight();
return;
}
const nodeData = this._graph?.getNodeData() || [];
const edgeData = this._graph?.getEdgeData() || [];
// === 1. 节点搜索项 ===
const nodeSearchItems = nodeData.map(node => ({
id: node.id,
name: node.data?.name || '',
label: node.data?.label || ''
}));
// === 2. 边搜索项(用于模糊匹配)===
const edgeSearchItems = edgeData.map(edge => {
const sourceName = this._nodeLabelMap.get(edge.source) || '';
const targetName = this._nodeLabelMap.get(edge.target) || '';
const relLabel = edge.data?.relationship?.properties?.label || '';
const fullText = [sourceName, relLabel, targetName].filter(Boolean).join(' ');
return {
id: edge.id,
sourceId: edge.source,
targetId: edge.target,
fullText
};
});
// === 3. Fuse 搜索(启用中文分词)===
const fuseOptions = {
includeScore: true,
threshold: 0.4,
ignoreLocation: true,
tokenize: true, // ✅ 支持中文
keys: ['name', 'label']
};
const edgeFuseOptions = {
includeScore: true,
threshold: 0.5,
ignoreLocation: true,
tokenize: true,
keys: ['fullText']
};
const nodeFuse = new Fuse(nodeSearchItems, fuseOptions);
const edgeFuse = new Fuse(edgeSearchItems, edgeFuseOptions);
// 初步结果
const directNodeIds = nodeFuse.search(keyword).map(r => r.item.id);
const matchedEdgesFromSearch = edgeFuse.search(keyword).map(r => r.item);
const matchedEdgeIdsFromSearch = matchedEdgesFromSearch.map(e => e.id);
// === 4. 关键:从命中的节点,反查“两端都被命中的边” ===
const nodeSet = new Set(directNodeIds);
const additionalEdgeIds = [];
edgeData.forEach(edge => {
// 如果边的 source 和 target 都在命中的节点集合中
if (nodeSet.has(edge.source) && nodeSet.has(edge.target)) {
additionalEdgeIds.push(edge.id);
}
});
// 合并边 ID(去重)
const allEdgeIds = Array.from(new Set([
...matchedEdgeIdsFromSearch,
...additionalEdgeIds
]));
// === 5. 节点:直接命中的 + 从边补的(保持你原有逻辑)===
const finalNodeIds = new Set(directNodeIds);
matchedEdgesFromSearch.forEach(edge => {
finalNodeIds.add(edge.sourceId);
finalNodeIds.add(edge.targetId);
});
// 注意:additionalEdge 对应的节点已在 directNodeIds 中,无需再加
// === 6. 调用高亮(不改此函数)===
this.highlightSearchResults(
Array.from(finalNodeIds),
allEdgeIds
);
},
highlightSearchResults(nodeIds, edgeIds) {
if (!this._graph) return;
const allNodes = this._graph.getNodeData().map(n => n.id);
const allEdges = this._graph.getEdgeData().map(e => e.id);
// 1️⃣ 先清除所有 search 相关状态(避免残留)
allNodes.forEach(id => {
this._graph.setElementState(id, 'active', false);
this._graph.setElementState(id, 'inactive', false);
});
allEdges.forEach(id => {
this._graph.setElementState(id, 'active', false);
this._graph.setElementState(id, 'inactive', false);
});
// 2️⃣ 高亮匹配项
nodeIds.forEach(id => {
if (this._graph.hasNode(id)) {
this._graph.setElementState(id, 'active', true);
}
});
edgeIds.forEach(id => {
if (this._graph.hasEdge(id)) {
this._graph.setElementState(id, 'active', true);
}
});
// 3️⃣ 淡化未匹配项
const matchedNodeSet = new Set(nodeIds);
const matchedEdgeSet = new Set(edgeIds);
allNodes.forEach(id => {
if (!matchedNodeSet.has(id)) {
this._graph.setElementState(id, 'inactive', true);
}
});
allEdges.forEach(id => {
if (!matchedEdgeSet.has(id)) {
this._graph.setElementState(id, 'inactive', true);
}
});
// 4️⃣ 聚焦第一个匹配项
const firstMatch = nodeIds[0] || edgeIds[0];
if (firstMatch && (this._graph.hasNode(firstMatch) || this._graph.hasEdge(firstMatch))) {
this._graph.focusElement(firstMatch, { animation: true, duration: 600 });
}
},
clearSearchHighlight() {
if (!this._graph) return;
const allNodes = this._graph.getNodeData().map(n => n.id);
const allEdges = this._graph.getEdgeData().map(e => e.id);
// 关闭所有 search 状态
[...allNodes, ...allEdges].forEach(id => {
this._graph.setElementState(id, 'normal', true);
});
},
getCount(){
getCount().then(res=>{
this.diseaseCount=res.Disease
this.drugCount=res.Drug
this.checkCount=res.Check
console.log(res)
})
},
buildCategoryIndex() {
const index = {};
if (this._graph!=null){
const nodes = this._graph.getNodeData() // 获取所有节点
nodes.forEach(node => {
console.log(node.data.label)
const category = node.data.label; // 假设 label 是类别
if(category=='Drug'||category=='Symptom'||
category=='Disease'||category=='Check'){
if (!index[category]) index[category] = [];
index[category].push(node.id);
}else{
if (!index["Other"]) index["Other"] = [];
index["Other"].push(node.id);
}
});
this.categoryToNodeIds = index;
}
},
// 切换某个类别的显示状态
toggleCategory (key){
if (this.visibleCategories.has(key)) {
this.visibleCategories.delete(key)
} else {
this.visibleCategories.add(key)
}
this.updateGraphFilter()
},
// 更新 G6 图的过滤规则
updateGraphFilter() {
console.log('visibleCategories:', this.visibleCategories);
// Step 1: 收集所有当前应该显示的节点 ID(扁平化)
const visibleNodeIds = new Set();
for (const [category, nodeIds] of Object.entries(this.categoryToNodeIds)) {
if (this.visibleCategories.has(category)) {
nodeIds.forEach(id => visibleNodeIds.add(id));
}
}
// Step 2: 更新节点的显示状态
for (const [category, nodeIds] of Object.entries(this.categoryToNodeIds)) {
if (this.visibleCategories.has(category)) {
this._graph.showElement(nodeIds, true);
} else {
this._graph.hideElement(nodeIds, true);
}
}
// Step 3: 处理边的显示/隐藏
const edges = this._graph.getEdgeData(); // 获取所有边数据
const edgesToShow = [];
const edgesToHide = [];
edges.forEach(edge => {
const sourceVisible = visibleNodeIds.has(edge.source);
const targetVisible = visibleNodeIds.has(edge.target);
// 只有当 source 和 target 都可见时,才显示这条边
if (sourceVisible && targetVisible) {
edgesToShow.push(edge.id);
} else {
edgesToHide.push(edge.id);
}
});
if (edgesToShow.length > 0) {
this._graph.showElement(edgesToShow, true);
}
if (edgesToHide.length > 0) {
this._graph.hideElement(edgesToHide, true);
}
},
changeTree(){
if(this.typeRadio=="Disease"){
if(this.DiseaseRadio=="ICD10") {
this.treeData=this.diseaseICD10Tree
}
if(this.DiseaseRadio=="Department") {
this.treeData=this.diseaseDepartTree
}
if(this.DiseaseRadio=="SZM") {
this.treeData=this.diseaseSZMTree
}
}
if(this.typeRadio=="Drug") {
if(this.DrugRadio=="Subject") {
this.treeData=this.drugSubjectTree
}
if(this.DrugRadio=="SZM") {
this.treeData=this.drugTree
}
}
if(this.typeRadio=="Check") {
this.treeData=this.checkTree
}
},
async loadDiseaseICD10TreeData() {
try {
const res = await fetch('/icd10.json')
if (!res.ok) throw new Error('Failed to load JSON')
this.diseaseICD10Tree = await res.json()
} catch (error) {
console.error('加载 ICD-10 数据失败:', error)
this.$message.error('加载编码数据失败,请检查文件路径')
}
},
async loadDiseaseDepartTreeData() {
try {
const res = await getDiseaseDepartTree()
this.diseaseDepartTree = res
} catch (error) {
}
},
async loadDiseaseSZMTreeData() {
try {
const res = await getDiseaseTree()
this.diseaseSZMTree = res
} catch (error) {
}
},
async loadDrugSubjectTreeData() {
try {
const res = await getDrugSubjectTree()
this.drugSubjectTree = res
} catch (error) {
}
},
async loadDrugTreeData() {
try {
const res = await getDrugTree()
this.drugTree = res
console.log(this.drugTree)
} catch (error) {
}
},
async loadCheckTreeData() {
try {
const res = await getCheckTree()
this.checkTree = res
console.log(this.checkTree)
} catch (error) {
}
},
async handleNodeClick(data) {
console.log('点击节点:', data)
// 可用于显示详情、复制 code 等
if (
(data.type === "Drug" || data.type === "Check") &&
!data.children // 确保没有 children 属性(或 children 为 undefined / null / 空数组)
) {
const response = await getGraph(data); // 等待 Promise 解析
this.formatData(response)
}
if(data.level=="category"||
data.level=="subcategory"||
data.level=="diagnosis"){
data.type="Disease"
const response = await getGraph(data); // 等待 Promise 解析
this.formatData(response)
}
if(data.type === "Disease"){
data.type="Disease"
const response = await getGraph(data); // 等待 Promise 解析
this.formatData(response)
}
},
buildNodeLabelMap(nodes) {
this._nodeLabelMap = new Map();
nodes.forEach(node => {
this._nodeLabelMap.set(node.id, node.data?.label || 'default');
});
},
clearGraphState() {
if (!this._graph) return;
// 1. 清除所有节点和边的状态
this._graph.getNodeData().forEach(node => {
this._graph.setElementState(node.id,[]);
});
this._graph.getEdgeData().forEach(edge => {
this._graph.setElementState(edge.id,[]);
});
// 2. (可选)取消所有 pending 的交互
// 比如如果你有高亮定时器,这里 clearTimeout
},
formatData(data){
this._graph.stopLayout();
this.clearGraphState();
// === 1. 构建 nodeId → label 映射 ===
const nodeIdToEnLabel = {};
data.nodes.forEach(node => {
nodeIdToEnLabel[node.id] = node.data.label; // e.g. "Disease"
});
// === 2. 处理节点:根据自身 label 设置样式 ===
const updatedNodes = data.nodes.map(node => {
const enLabel = node.data.label;
const zhLabel = this.enToZhLabelMap[enLabel] || '其他'; // 默认回退到“其他”
const styleConf = this.parsedStyles[zhLabel] || {};
return {
...node,
type: styleConf.nodeShape || this.nodeShape,
style: {
size: styleConf.nodeSize || this.nodeSize,
fill: styleConf.nodeFill || this.nodeFill,
stroke: styleConf.nodeStroke || this.nodeStroke,
lineWidth: styleConf.nodeLineWidth || this.nodeLineWidth,
label: styleConf.nodeShowLabel !== undefined ? styleConf.nodeShowLabel : true,
labelFontSize: styleConf.nodeFontSize || this.nodeFontSize,
labelFontFamily: styleConf.nodeFontFamily || this.nodeFontFamily,
labelFill: styleConf.nodeFontColor || this.nodeFontColor
}
};
});
// === 3. 处理边:根据 source 节点的 label 设置样式 ===
const updatedEdges = data.edges.map(edge => {
console.log(edge)
const sourceEnLabel = nodeIdToEnLabel[edge.source]; // e.g. "Disease"
const sourceZhLabel = this.enToZhLabelMap[sourceEnLabel] || '其他';
const styleConf = this.parsedStyles[sourceZhLabel] || {};
return {
...edge,
id: edge.data?.relationship?.id || edge.id,
type: styleConf.edgeType ||this.edgeType,
style: {
endArrow: styleConf.edgeEndArrow !== undefined ? styleConf.edgeEndArrow : true,
stroke: styleConf.edgeStroke || this.edgeStroke,
lineWidth: styleConf.edgeLineWidth || this.edgeLineWidth,
label: styleConf.edgeShowLabel !== undefined ? styleConf.edgeShowLabel : false,
labelFontSize: styleConf.edgeFontSize || this.edgeFontSize,
labelFontFamily: styleConf.edgeFontFamily || this.edgeFontFamily,
labelFill: styleConf.edgeFontColor || this.edgeFontColor
}
};
});
// === 4. 更新图数据 ===
let updatedData = {
nodes: updatedNodes,
edges: updatedEdges
};
this.buildNodeLabelMap(updatedNodes);
this.updateGraph(updatedData)
this.buildCategoryIndex();
},
getLevelLabel(level) {
const map = {
chapter: '章',
section: '节',
category: '类目',
subcategory: '亚目',
diagnosis: '条目'
}
return map[level] || level
},
getTagType(level) {
const map = {
chapter: 'primary',
section: 'success',
category: 'warning',
subcategory: 'info',
diagnosis: 'info'
}
return map[level] || ''
},
initGraph() {
if (this._graph!=null){
this._graph.destroy()
this._graph = null;
}
if (!this._nodeLabelMap) {
this.buildNodeLabelMap(this.defaultData.nodes);
}
console.log(this.defaultData)
console.log(this._nodeLabelMap)
const container = this.$refs.graphContainer;
if (!container) return;
if (container!=null){
const width = container.clientWidth || 800;
const height = container.clientHeight || 600;
console.log(width)
console.log(height)
const graph = new Graph({
container,
width,
height,
layout: {
type: 'force', // 力导向布局
gravity: 0.3, // 重力系数,控制节点聚集程度
repulsion: 500, // 排斥力
attraction: 20, // 吸引力
preventOverlap: true, // 防止节点重叠
// center: [width / 2, height / 2]
// type: 'radial',
// preventOverlap: true,
// unitRadius: 200,
// maxPreventOverlapIteration:100
// type: 'force-atlas2',
// preventOverlap: true,
// kr: 1000,
// center: [250, 250],
// barnesHut:true,
},
autoFit: {
type: 'center', // 自适应类型:'view' 或 'center'
options: {
// 仅适用于 'view' 类型
when: 'always', // 何时适配:'overflow'(仅当内容溢出时) 或 'always'(总是适配)
direction: 'both', // 适配方向:'x'、'y' 或 'both'
},
animation: {
// 自适应动画效果
duration: 1000, // 动画持续时间(毫秒)
easing: 'ease-in-out', // 动画缓动函数
},
},
behaviors: [ 'zoom-canvas', 'drag-element',
'click-select','focus-element', {
type: 'hover-activate',
degree: 1,
},
{
type: 'drag-canvas',
enable: (event) => event.shiftKey === false,
},
{
type: 'brush-select',
},
],
node: {
style: {
// fill: (d) => {
// const label = d.data?.label;
// if (label === 'Disease') return '#EF4444'; // 红
// if (label === 'Drug') return '#91cc75'; // 绿
// if (label === 'Symptom') return '#fac858'; // 橙
// if (label === 'Check') return '#336eee'; // 橙
// return '#59d1d4'; // 默认灰蓝
// },
// stroke: (d) => {
// const label = d.data?.label;
// if (label === 'Disease') return '#B91C1C';
// if (label === 'Drug') return '#047857';
// if (label === 'Check') return '#1D4ED8'; // 橙
// if (label === 'Symptom') return '#B45309';
// return '#40999b';
// },
labelText: (d) => d.data.name,
labelPlacement: 'center',
labelWordWrap: true,
labelMaxWidth: '100%',
labelMaxLines: 2,
labelTextOverflow: 'ellipsis',
labelTextAlign: 'center',
opacity: 1
},
state: {
active: {
lineWidth: 2,
shadowColor: '#ffffff',
shadowBlur: 10,
opacity: 1
},
highlight:{
stroke: '#FF5722',
lineWidth: 4,
opacity: 1
},
inactive: {
opacity: 0.8
},
normal:{
opacity: 1
}
},
},
edge: {
style: {
labelText: (d) => d.data.relationship.properties.label,
// stroke: (d) => {
// // 获取 target 节点的 label
// const targetLabel = this._nodeLabelMap.get(d.source); // d.target 是目标节点 ID
// // 根据 target 节点类型返回对应浅色
// if (targetLabel === 'Disease') return 'rgba(239,68,68,0.5)';
// if (targetLabel === 'Drug') return 'rgba(145,204,117,0.5)';
// if (targetLabel === 'Symptom') return 'rgba(250,200,88,0.5)';
// if (targetLabel === 'Check') return 'rgba(51,110,238,0.5)'; // 橙
// return 'rgba(89,209,212,0.5)'; // default
// },
// labelFill: (d) => {
// // 获取 target 节点的 label
// const targetLabel = this._nodeLabelMap.get(d.target); // d.target 是目标节点 ID
// // 根据 target 节点类型返回对应浅色
//
// if (targetLabel === 'Disease') return '#ff4444';
// if (targetLabel === 'Drug') return '#2f9b70';
// if (targetLabel === 'Symptom') return '#f89775';
// return '#6b91ff'; // default
// }
},
state: {
selected: {
stroke: '#1890FF',
lineWidth: 2,
},
highlight: {
halo: true,
haloStroke: '#1890FF',
haloLineWidth: 6,
haloStrokeOpacity: 0.3,
lineWidth: 3,
opacity: 1
},
inactive: {
opacity: 0.8
},
normal:{
opacity: 1
}
},
},
data:this.defaultData,
});
this.$nextTick(() => {
graph.render();
// graph.fitView()
// graph.fitView({ padding: 30, duration: 1000, easing: 'ease-in' });
});
console.log("Container scroll position after:", container.scrollTop, container.scrollLeft);
// graph.fitView();
// graph.on('node:pointerover', (evt) => {
// const nodeItem = evt.target; // 获取当前鼠标进入的节点元素
// const relatedEdges = graph.getRelatedEdgesData(nodeItem.id);
// const relatedEdgeIds = relatedEdges.map(edge => edge.id);
//
// // 3. 高亮这些边(比如用 'highlight' 状态)
// relatedEdgeIds.forEach(edgeId => {
// graph.setElementState(edgeId, 'highlight', true);
// });
// graph.setElementState(nodeItem.id, 'active',true);
//
// // 2. 获取邻居节点 ID(去重)
// const neighborNodeIds = new Set();
// relatedEdges.forEach(edge => {
// if (edge.source !== nodeItem.id) neighborNodeIds.add(edge.source);
// if (edge.target !== nodeItem.id) neighborNodeIds.add(edge.target);
// });
//
// neighborNodeIds.forEach(id => {
// graph.setElementState(id, 'active', true);
// });
//
// graph.getEdgeData().forEach(edge => {
// if (!relatedEdgeIds.includes(edge.id)) {
// graph.setElementState(edge.id, 'inactive', true);
// }
// });
// graph.getNodeData().forEach(node => {
// if (node.id !== nodeItem.id && !neighborNodeIds.has(node.id)) {
// graph.setElementState(node.id, 'inactive',true);
// }
// });
// });
// graph.on('node:pointerleave', (evt) => {
// graph.getEdgeData().forEach(edge => {
// graph.setElementState(edge.id, 'highlight', false);
// graph.setElementState(edge.id, 'inactive', false);
// graph.setElementState(edge.id, 'normal', true);
// });
// graph.getNodeData().forEach(node => {
// graph.setElementState(node.id, 'active', false);
// graph.setElementState(node.id, 'inactive', false);
// graph.setElementState(node.id, 'normal', true);
// });
// });
graph.on('node:click', (evt) => {
const nodeItem = evt.target.id; // 获取当前鼠标进入的节点元素
let node=graph.getNodeData(nodeItem).data
let data={
label:node.name,
type:node.label
}
getGraph(data).then(response=>{
console.log(response)
this.formatData(response)
}); // 等待 Promise 解析
});
graph.once('afterlayout', () => {
if (!graph.destroyed) {
graph.fitCenter({ padding: 40, duration: 1000 });
}
});
this._graph = graph
this._graph.setPlugins([ {
type: 'tooltip',
// 只对节点启用,边不显示tooltip
enable: (e) => e.targetType === 'edge',
getContent: (e, items) => {
console.log(items)
console.log(e)
const edge = items[0]; // 当前悬停的边
if (!edge) return '';
const data=items[0].data
const sourceId = edge.source;
const targetId = edge.target;
// 获取节点数据(通过图实例)
const sourceNode = this._graph.getNodeData(sourceId);
console.log(sourceNode)
const targetNode = this._graph.getNodeData(targetId);
const sourceName = sourceNode?.data.name || sourceId;
const targetName = targetNode?.data.name || targetId;
const rel = data.relationship.properties.label || '关联';
return `<div style="padding: 4px 8px; color: #b6b2b2; border-radius: 4px; font-size: 12px;">
${sourceName} ${rel} > ${targetName}
</div>`;
},
},])
}
},
updateGraph(data) {
if (!this._graph) return
this._graph.setData(data)
this._graph.render();
},
updateAllNodes() {
if (!this._graph) return
const updatedNodes = this.defaultData.nodes.map(node => ({
...node,
type: this.nodeShape,
style:{
size: this.nodeSize,
fill: this.nodeFill,
stroke: this.nodeStroke,
lineWidth: this.nodeLineWidth,
label: this.nodeShowLabel,
labelFontSize: this.nodeFontSize,
labelFontFamily: this.nodeFontFamily,
labelFontColor: this.nodeFontColor,
}
}))
const updatedData = {
nodes: updatedNodes,
edges: this.defaultData.edges
}
this.defaultData = updatedData
this.updateGraph(updatedData)
},
updateAllEdges() {
if (!this._graph) return
const updatedEdges = this.defaultData.edges.map(edge => ({
...edge,
type:this.edgeType,
style: {
endArrow: this.edgeEndArrow,
stroke: this.edgeStroke,
lineWidth: this.edgeLineWidth,
label: this.edgeShowLabel,
labelFontSize: this.edgeFontSize,
labelFontFamily: this.edgeFontFamily,
labelFill: this.edgeFontColor,
},
}))
const updatedData = {
nodes: this.defaultData.nodes,
edges: updatedEdges
}
this.defaultData = updatedData
this.updateGraph(updatedData)
},
clearAllStates() {
if (!this._graph) return
// 清除所有节点的状态
this._graph.getNodes().forEach(node => {
this._graph.clearItemStates(node)
})
// 清除所有边的状态
this._graph.getEdges().forEach(edge => {
this._graph.clearItemStates(edge)
})
},
handleResize() {
const container = this.$refs.graphContainer
if (this._graph && container) {
this._graph.resize(container.offsetWidth, container.offsetHeight)
}
},
resetView() {
this._graph?.fitView()
},
resetStyle() {
Object.assign(this, {
nodeShowLabel: true,
nodeFontSize: 12,
nodeFontColor: '#000000',
nodeShape: 'circle',
nodeSize: 60,
nodeFill: '#9FD5FF',
nodeStroke: '#5B8FF9',
nodeLineWidth: 2,
nodeFontFamily: 'Microsoft YaHei, sans-serif',
edgeShowLabel: true,
edgeFontSize: 10,
edgeFontColor: '#666666',
edgeType: 'line',
edgeLineWidth: 2,
edgeStroke: '#F00',
edgeEndArrow: true,
edgeFontFamily: 'Microsoft YaHei, sans-serif'
})
}
}
}
</script>
<style scoped>
.knowledge-graph-container {
display: flex;
height: 100vh;
}
.control-panel {
width: 280px;
background: #f5f7fa;
padding: 16px;
border-right: 1px solid #ddd;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.1);
font-family: 'Microsoft YaHei';
}
.section {
margin-bottom: 10px;
padding: 10px;
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.section h5 {
margin-top: 0;
color: #333;
font-size: 14px;
}
.slider-group {
margin: 10px 0;
display: flex;
align-items: center;
}
.slider-group span {
margin-right: 10px;
font-size: 12px;
}
.form-group {
margin: 10px 0;
display: flex;
align-items: center;
}
.form-group label {
margin-right: 8px;
font-size: 12px;
}
.form-group select,
.form-group input[type="number"] {
padding: 4px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 12px;
}
.color-picker {
margin: 10px 0;
display: flex;
align-items: center;
}
.color-picker label {
margin-right: 8px;
font-size: 12px;
}
.color-picker input[type="color"] {
width: 40px;
height: 20px;
border: none;
cursor: pointer;
}
button {
width: 45%;
padding: 10px;
background: #007bff;
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
margin-right: 2%;
}
button:hover {
background: #0056b3;
}
.graph-container {
flex: 1;
background: #fff;
width: 100%;
height: 100%;
border: 1px dashed #e2e8f0;
border-radius: 12px;
}
.legend-item {
display: flex;
align-items: center;
gap: 7px;
cursor: pointer;
user-select: none;
}
.color-block {
width: 30px;
height: 15px;
border-radius: 18px;
transition: all 0.2s ease;
}
.legend-item:hover {
opacity: 0.8;
}
/* --- 主体区域 --- */
.main-body {
flex: 1;
display: flex;
flex-direction: column;
background: #fff;
overflow: auto;
}
.top-nav {
height: 7vw;
padding: 0 30px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #f0f0f0;
}
.search-container {
width: 330px;
height: 30px;
background: #fff;
border-radius: 18px;
display: flex;
align-items: center;
padding: 0 15px;
box-shadow: 4px 2px 9px 1px rgb(97 99 100 / 19%);
}
.search-container input {
border: none;
outline: none;
background: transparent;
width: 100%;
font-size: 13px;
}
.search-btn {
width: 16px;
}
/*.search-btn:hover {
background-color: rgba(34, 101, 244, 0.64);
border-radius: 50%;
padding: 2px 0;
}*/
.legend-box {
display: flex;
gap: 20px;
}
.leg-item {
display: flex;
align-items: center;
font-size: 13px;
}
.leg-item i {
width: 24px;
height: 8px;
border-radius: 4px;
margin-right: 8px;
}
.main-content {
flex: 1;
padding: 20px 30px;
display: flex;
gap: 20px;
height: 50vw;
}
.disease-container {
width: 360px;
border: 1px solid #f0f0f0;
border-radius: 12px;
overflow: hidden;
max-height: 85vh;
}
.disease-header {
height: 50px;
background: #2265f4;
color: #fff;
display: flex;
align-items: center;
padding: 0 2px 0px 15px;
justify-content: space-between;
}
.d-title{
display: flex;
align-items: center;
font-size: 13px;
}
.d-count{
font-size: 9px;
background-color: #5989F0;
border-radius: 7px;
padding: 0px 4px;
margin-left: 7px;
}
.d-icon {
width: 21px;
margin-right: 8px;
}
.graph-viewport {
flex: 1;
border: 1px dashed #e2e8f0;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: #ccc;
}
.custom-tree-node {
display: flex;
align-items: center;
width: 100%;
overflow: hidden;
font-weight: 500;
}
.code {
flex-shrink: 0; /* 不允许 code 被压缩 */
margin-right: 8px;
font-size: 12px;
color: #666;
}
.label {
flex: 1; /* 占据剩余空间 */
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-align: left;
font-size: 12px;
}
.disease-body{
width: 360px;
overflow: scroll;
height: 74vh;
}
/* 隐藏滚动条,但允许滚动 */
.disease-body {
/* Firefox */
scrollbar-width: none; /* 'auto' | 'thin' | 'none' */
/* Webkit (Chrome, Safari, Edge) */
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
padding-bottom: 9px;
}
.disease-body::-webkit-scrollbar {
display: none; /* Webkit browsers */
}
/* 动态给每个 el-radio 设置不同的背景颜色 */
/deep/ .radio-disease .el-radio__input.is-checked .el-radio__inner {
background: rgb(153, 10, 0); /* 疾病的颜色 */
border-color: rgb(153, 10, 0);
}
/deep/ .radio-drug .el-radio__input.is-checked .el-radio__inner {
background: #52c41a; /* 检查的颜色 */
border-color:#52c41a;
}
/deep/ .radio-check .el-radio__input.is-checked .el-radio__inner {
background: #1890ff; /* 药品的颜色 */
border-color:#1890ff;
}
/* 动态给每个 el-radio 设置不同的背景颜色 */
/deep/ .radio-disease .el-radio__input.is-checked .el-radio__inner:hover {
background: rgb(153, 10, 0); /* 疾病的颜色 */
border-color: rgb(153, 10, 0);
}
/deep/ .radio-drug .el-radio__input.is-checked .el-radio__inner:hover {
background: #52c41a; /* 检查的颜色 */
border-color:#52c41a;
}
/deep/ .radio-check .el-radio__input.is-checked .el-radio__inner:hover {
background: #1890ff; /* 药品的颜色 */
border-color:#1890ff;
}
/* 自定义选中后的样式 */
/deep/ .radio-disease .el-radio__input.is-checked+.el-radio__label {
color: rgb(153, 10, 0);
}
/deep/ .radio-drug .el-radio__input.is-checked+.el-radio__label {
color: #52c41a;
}
/deep/ .radio-check .el-radio__input.is-checked+.el-radio__label {
color: #1890ff;
}
/* 自定义下拉样式 */
.select-container {
position: relative;
cursor: pointer;
user-select: none;
}
.select-display {
display: flex;
align-items: center;
padding: 0 12px;
height: 25px;
border-radius: 20px;
font-size: 12px;
}
.dropdown-icon {
width: 23px;
margin-left: 6px;
}
.select-dropdown {
position: absolute;
top: 100%;
right: 0;
margin-top: 4px;
background: rgba(255, 255, 255, 0.64);
border: 1px solid #e0e0e0;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 10;
min-width: 80px;
color: #000;
padding: 3px 0px;
}
.select-option {
padding: 7px 12px;
font-size: 13px;
text-align: center;
}
.select-option:hover {
background-color: #f5f7fa;
}
</style>