|
|
|
@ -4,56 +4,113 @@ |
|
|
|
<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" class="search-btn" @click="search"/> |
|
|
|
</div> |
|
|
|
<div class="legend-box"> |
|
|
|
<div v-for="(item, index) in legendItems" :key="index" class="legend-item"> |
|
|
|
<div |
|
|
|
:style="{ |
|
|
|
<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 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> |
|
|
|
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> |
|
|
|
<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> |
|
|
|
<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 class="disease-body"> |
|
|
|
</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 |
|
|
|
@ -93,7 +150,15 @@ |
|
|
|
</template> |
|
|
|
|
|
|
|
<script> |
|
|
|
import {getCheckTree, getCount, getDrugTree, getGraph, getTestGraphData} from "@/api/graph" |
|
|
|
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"; |
|
|
|
@ -101,6 +166,7 @@ import GraphToolbar from "@/components/GraphToolbar.vue"; |
|
|
|
|
|
|
|
import Fuse from 'fuse.js'; |
|
|
|
import {ElMessage} from "element-plus"; |
|
|
|
import {getGraphStyleActive} from "@/api/style"; |
|
|
|
export default { |
|
|
|
name: 'Display', |
|
|
|
components: {Menu,GraphToolbar}, |
|
|
|
@ -146,8 +212,11 @@ export default { |
|
|
|
label: 'title' // 虽然用插槽,但 el-tree 内部仍会读取,可留空或任意 |
|
|
|
}, |
|
|
|
typeRadio:"Disease", |
|
|
|
DiseaseRadio:"ICD10", |
|
|
|
drugTree:[], |
|
|
|
diseaseTree:[], |
|
|
|
diseaseDepartTree:[], |
|
|
|
diseaseICD10Tree:[], |
|
|
|
diseaseSZMTree:[], |
|
|
|
checkTree:[], |
|
|
|
legendItems: [ |
|
|
|
{ key: 'Disease', label: '疾病', color: '#EF4444' }, |
|
|
|
@ -166,10 +235,31 @@ export default { |
|
|
|
nodes: [], |
|
|
|
edges: [] |
|
|
|
}, |
|
|
|
searchKeyword:"" |
|
|
|
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'; // 默认颜色 |
|
|
|
@ -225,47 +315,76 @@ export default { |
|
|
|
// #790800 |
|
|
|
}, |
|
|
|
async mounted() { |
|
|
|
|
|
|
|
this.visibleCategories = new Set(this.legendItems.map(i => i.key)); |
|
|
|
await this.loadDiseaseTreeData() |
|
|
|
await this.loadDiseaseICD10TreeData() |
|
|
|
this.loadDiseaseDepartTreeData() |
|
|
|
this.loadDiseaseSZMTreeData() |
|
|
|
this.getCount() |
|
|
|
this.loadDrugTreeData() |
|
|
|
this.loadCheckTreeData() |
|
|
|
this.treeData=this.diseaseTree |
|
|
|
this.loadDrugSubjectTreeData() |
|
|
|
this.treeData=this.diseaseICD10Tree |
|
|
|
await this.$nextTick(); |
|
|
|
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 = { |
|
|
|
// === 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(); |
|
|
|
@ -275,6 +394,50 @@ export default { |
|
|
|
} 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); |
|
|
|
// } |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
@ -310,6 +473,49 @@ export default { |
|
|
|
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) { |
|
|
|
@ -543,53 +749,59 @@ export default { |
|
|
|
|
|
|
|
changeTree(){ |
|
|
|
if(this.typeRadio=="Disease"){ |
|
|
|
this.treeData=this.diseaseTree |
|
|
|
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") { |
|
|
|
this.treeData=this.drugTree |
|
|
|
if(this.DrugRadio=="Subject") { |
|
|
|
this.treeData=this.drugSubjectTree |
|
|
|
} |
|
|
|
if(this.DrugRadio=="SZM") { |
|
|
|
this.treeData=this.drugTree |
|
|
|
} |
|
|
|
} |
|
|
|
if(this.typeRadio=="Check") { |
|
|
|
this.treeData=this.checkTree |
|
|
|
} |
|
|
|
}, |
|
|
|
loadTreeNode(node, resolve, data) { |
|
|
|
// 根据当前选中的类型决定调用哪个接口 |
|
|
|
let apiCall; |
|
|
|
if (this.typeRadio === 'Disease') { |
|
|
|
// 假设你的 ICD-10 节点有 level 字段(如 chapter, section 等) |
|
|
|
// 并且子节点需要根据 code 或 id 请求 |
|
|
|
apiCall = () => this.loadDiseaseTreeData(); |
|
|
|
} else if (this.typeRadio === 'Drug') { |
|
|
|
apiCall = () => this.loadDrugTreeData(); // 你需要实现这个 API |
|
|
|
} else if (this.typeRadio === 'Check') { |
|
|
|
apiCall = () => this.loadCheckTreeData(); |
|
|
|
} |
|
|
|
if (apiCall) { |
|
|
|
apiCall() |
|
|
|
.then(children => { |
|
|
|
// children 必须是数组,每个元素要有 label、code/id 等字段 |
|
|
|
// 如果没有子节点,返回空数组 [] |
|
|
|
resolve(children || []); |
|
|
|
}) |
|
|
|
.catch(err => { |
|
|
|
console.error('加载子节点失败:', err); |
|
|
|
resolve([]); // 防止卡住 |
|
|
|
}); |
|
|
|
} else { |
|
|
|
resolve([]); |
|
|
|
} |
|
|
|
}, |
|
|
|
async loadDiseaseTreeData() { |
|
|
|
async loadDiseaseICD10TreeData() { |
|
|
|
try { |
|
|
|
const res = await fetch('/icd10.json') |
|
|
|
if (!res.ok) throw new Error('Failed to load JSON') |
|
|
|
this.diseaseTree = await res.json() |
|
|
|
console.log(this.treeData) |
|
|
|
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() |
|
|
|
@ -623,6 +835,11 @@ export default { |
|
|
|
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(); |
|
|
|
@ -648,37 +865,63 @@ export default { |
|
|
|
formatData(data){ |
|
|
|
this._graph.stopLayout(); |
|
|
|
this.clearGraphState(); |
|
|
|
const updatedEdges = data.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 updatedNodes = data.nodes.map(node => ({ |
|
|
|
...node, |
|
|
|
type: this.nodeShape, |
|
|
|
style:{ |
|
|
|
size: this.nodeSize, |
|
|
|
lineWidth: this.nodeLineWidth, |
|
|
|
label: this.nodeShowLabel, |
|
|
|
labelFontSize: this.nodeFontSize, |
|
|
|
labelFontFamily: this.nodeFontFamily, |
|
|
|
labelFill: this.nodeFontColor, |
|
|
|
opacity: 1, |
|
|
|
} |
|
|
|
})) |
|
|
|
const updatedData = { |
|
|
|
|
|
|
|
// === 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(); |
|
|
|
@ -745,10 +988,11 @@ export default { |
|
|
|
|
|
|
|
}, |
|
|
|
autoFit: { |
|
|
|
type: 'center', |
|
|
|
type: 'center', // 自适应类型:'view' 或 'center' |
|
|
|
options: { |
|
|
|
when: 'first', |
|
|
|
direction: 'both', |
|
|
|
// 仅适用于 'view' 类型 |
|
|
|
when: 'always', // 何时适配:'overflow'(仅当内容溢出时) 或 'always'(总是适配) |
|
|
|
direction: 'both', // 适配方向:'x'、'y' 或 'both' |
|
|
|
}, |
|
|
|
animation: { |
|
|
|
// 自适应动画效果 |
|
|
|
@ -756,8 +1000,8 @@ export default { |
|
|
|
easing: 'ease-in-out', // 动画缓动函数 |
|
|
|
}, |
|
|
|
}, |
|
|
|
behaviors: ['zoom-canvas', 'drag-element', |
|
|
|
'click-select', 'focus-element', { |
|
|
|
behaviors: [ 'zoom-canvas', 'drag-element', |
|
|
|
'click-select','focus-element', { |
|
|
|
type: 'hover-activate', |
|
|
|
degree: 1, |
|
|
|
}, |
|
|
|
@ -772,27 +1016,27 @@ export default { |
|
|
|
|
|
|
|
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'; |
|
|
|
}, |
|
|
|
// 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: '150%', |
|
|
|
labelMaxLines: 3, |
|
|
|
labelMaxWidth: '100%', |
|
|
|
labelMaxLines: 2, |
|
|
|
labelTextOverflow: 'ellipsis', |
|
|
|
labelTextAlign: 'center', |
|
|
|
opacity: 1 |
|
|
|
@ -804,7 +1048,7 @@ export default { |
|
|
|
shadowBlur: 10, |
|
|
|
opacity: 1 |
|
|
|
}, |
|
|
|
highlight: { |
|
|
|
highlight:{ |
|
|
|
stroke: '#FF5722', |
|
|
|
lineWidth: 4, |
|
|
|
opacity: 1 |
|
|
|
@ -812,7 +1056,7 @@ export default { |
|
|
|
inactive: { |
|
|
|
opacity: 0.8 |
|
|
|
}, |
|
|
|
normal: { |
|
|
|
normal:{ |
|
|
|
opacity: 1 |
|
|
|
} |
|
|
|
|
|
|
|
@ -822,16 +1066,16 @@ export default { |
|
|
|
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 |
|
|
|
}, |
|
|
|
// 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 |
|
|
|
@ -860,14 +1104,14 @@ export default { |
|
|
|
inactive: { |
|
|
|
opacity: 0.8 |
|
|
|
}, |
|
|
|
normal: { |
|
|
|
normal:{ |
|
|
|
opacity: 1 |
|
|
|
} |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
}, |
|
|
|
data: this.defaultData, |
|
|
|
data:this.defaultData, |
|
|
|
|
|
|
|
}); |
|
|
|
this.$nextTick(() => { |
|
|
|
@ -925,10 +1169,10 @@ export default { |
|
|
|
// }); |
|
|
|
graph.on('node:click', (evt) => { |
|
|
|
const nodeItem = evt.target.id; // 获取当前鼠标进入的节点元素 |
|
|
|
let node = graph.getNodeData(nodeItem).data |
|
|
|
let data = { |
|
|
|
label: node.name, |
|
|
|
type: node.label |
|
|
|
let node=graph.getNodeData(nodeItem).data |
|
|
|
let data={ |
|
|
|
label:node.name, |
|
|
|
type:node.label |
|
|
|
} |
|
|
|
getGraph(data).then(response => { |
|
|
|
console.log(response) |
|
|
|
@ -961,7 +1205,7 @@ export default { |
|
|
|
|
|
|
|
const sourceName = sourceNode?.data.name || sourceId; |
|
|
|
const targetName = targetNode?.data.name || targetId; |
|
|
|
const rel = data.relationship.properties.label || '关联'; |
|
|
|
const rel = data.relationship.properties.label || '关联'; |
|
|
|
|
|
|
|
return `<div style="padding: 4px 8px; color: #b6b2b2; border-radius: 4px; font-size: 12px;"> |
|
|
|
${sourceName} — ${rel} —> ${targetName} |
|
|
|
@ -1046,7 +1290,7 @@ export default { |
|
|
|
const updatedNodes = this.defaultData.nodes.map(node => ({ |
|
|
|
...node, |
|
|
|
type: this.nodeShape, |
|
|
|
style: { |
|
|
|
style:{ |
|
|
|
size: this.nodeSize, |
|
|
|
fill: this.nodeFill, |
|
|
|
stroke: this.nodeStroke, |
|
|
|
@ -1302,8 +1546,13 @@ button:hover { |
|
|
|
|
|
|
|
.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; |
|
|
|
@ -1335,7 +1584,7 @@ button:hover { |
|
|
|
border: 1px solid #f0f0f0; |
|
|
|
border-radius: 12px; |
|
|
|
overflow: hidden; |
|
|
|
|
|
|
|
max-height: 85vh; |
|
|
|
} |
|
|
|
|
|
|
|
.disease-header { |
|
|
|
@ -1344,8 +1593,8 @@ button:hover { |
|
|
|
color: #fff; |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
padding: 0 15px; |
|
|
|
justify-content: flex-start; |
|
|
|
padding: 0 2px 0px 15px; |
|
|
|
justify-content: space-between; |
|
|
|
} |
|
|
|
|
|
|
|
.d-title { |
|
|
|
@ -1460,6 +1709,7 @@ button:hover { |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* 自定义选中后的样式 */ |
|
|
|
/deep/ .radio-disease .el-radio__input.is-checked + .el-radio__label { |
|
|
|
color: rgb(153, 10, 0); |
|
|
|
@ -1472,4 +1722,50 @@ button:hover { |
|
|
|
/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> |