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
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>
|