|
|
@ -7,7 +7,11 @@ |
|
|
/> |
|
|
/> |
|
|
<div class="medical-qa-container"> |
|
|
<div class="medical-qa-container"> |
|
|
<!-- 标题 --> |
|
|
<!-- 标题 --> |
|
|
|
|
|
<div style="display: flex;align-items: center;"> |
|
|
|
|
|
<div class="title-before"> |
|
|
|
|
|
</div> |
|
|
<h2 class="title">医疗问答</h2> |
|
|
<h2 class="title">医疗问答</h2> |
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
<!-- 搜索框 --> |
|
|
<!-- 搜索框 --> |
|
|
<div class="search-box"> |
|
|
<div class="search-box"> |
|
|
@ -23,7 +27,15 @@ |
|
|
<div class="content-wrapper"> |
|
|
<div class="content-wrapper"> |
|
|
<!-- 左侧:问答结果 --> |
|
|
<!-- 左侧:问答结果 --> |
|
|
<div class="answer-list"> |
|
|
<div class="answer-list"> |
|
|
<div class="section-title">问答结果</div> |
|
|
<div v-if="queryRecord"> |
|
|
|
|
|
<h2 class="section-title" style="margin-bottom: 10px;margin-top: 0px">问答提问</h2> |
|
|
|
|
|
<div class="answer-item" style=" background-color: #165DFF; |
|
|
|
|
|
border-color: #0066cc; |
|
|
|
|
|
color: #fff; height: auto;max-height: 3vw"> |
|
|
|
|
|
{{queryRecord}} |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
<h2 class="section-title">问答结果</h2> |
|
|
<div v-if="answers.length === 0" class="empty-state"> |
|
|
<div v-if="answers.length === 0" class="empty-state"> |
|
|
请输入问题进行查询... |
|
|
请输入问题进行查询... |
|
|
</div> |
|
|
</div> |
|
|
@ -35,14 +47,14 @@ |
|
|
@click="selectGraph(index)" |
|
|
@click="selectGraph(index)" |
|
|
:class="{ 'highlight': selected === index }" |
|
|
:class="{ 'highlight': selected === index }" |
|
|
> |
|
|
> |
|
|
{{ item }} |
|
|
{{ item.answer }} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<!-- 右侧:知识图谱 --> |
|
|
<!-- 右侧:知识图谱 --> |
|
|
<div class="knowledge-graph"> |
|
|
<div class="knowledge-graph"> |
|
|
<KnowledgeGraph /> |
|
|
<div ref="graphContainer" class="graph-container" id="container"></div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
@ -53,6 +65,8 @@ |
|
|
<script> |
|
|
<script> |
|
|
import Menu from "@/components/Menu.vue"; |
|
|
import Menu from "@/components/Menu.vue"; |
|
|
import {qaAnalyze} from "@/api/qa"; |
|
|
import {qaAnalyze} from "@/api/qa"; |
|
|
|
|
|
import {Graph} from "@antv/g6"; |
|
|
|
|
|
import {getGraph} from "@/api/graph"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export default { |
|
|
export default { |
|
|
@ -61,24 +75,273 @@ export default { |
|
|
data() { |
|
|
data() { |
|
|
return { |
|
|
return { |
|
|
query:"", |
|
|
query:"", |
|
|
answers:['糖尿病不能吃什么','糖尿病不能吃什么','糖尿病不能吃什么','糖尿病不能吃什么','糖尿病不能吃什么',], |
|
|
answers:[], |
|
|
selected:0, |
|
|
selected:0, |
|
|
|
|
|
// 节点样式 |
|
|
|
|
|
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', |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
queryRecord:"" |
|
|
|
|
|
|
|
|
}; |
|
|
}; |
|
|
}, |
|
|
}, |
|
|
methods: { |
|
|
methods: { |
|
|
selectGraph(index){ |
|
|
selectGraph(index){ |
|
|
this.selected=index |
|
|
this.selected=index |
|
|
|
|
|
this.formatData(this.answers[index].result) |
|
|
}, |
|
|
}, |
|
|
handleSearch(){ |
|
|
handleSearch(){ |
|
|
|
|
|
this.answers=[] |
|
|
let data={ |
|
|
let data={ |
|
|
text:this.query |
|
|
text:this.query |
|
|
} |
|
|
} |
|
|
|
|
|
this.queryRecord=this.query |
|
|
this.query="" |
|
|
this.query="" |
|
|
|
|
|
// this.answers=[{"answer":"糖尿病患者应避免高糖食物,如糖果、甜点、含糖饮料等,以防止血糖波动。", |
|
|
|
|
|
// "result":{"nodes":[{"id":"e0a8410b-c5ee-47d4-acbe-b7ae5f111fd4","label":"糖尿病","type":"疾病"},{"id":"89f9498b-7a83-4361-889b-f96dfdfb802c","label":"血糖波动","type":"症状"},{"id":"03201620-ba9f-4957-b7d3-f89c5a115e37","label":"高糖食物","type":"其他类型"},{"id":"b0bace0a-eedc-485c-90d3-0a5378dc5556","label":"糖果","type":"其他类型"},{"id":"ffc12d7b-60e5-4ffa-a945-a0769f6e1047","label":"甜点","type":"其他类型"},{"id":"a7e94ee7-072b-456f-bc0c-354545851c38","label":"含糖饮料","type":"其他类型"}],"edges":[{"source":"e0a8410b-c5ee-47d4-acbe-b7ae5f111fd4","target":"03201620-ba9f-4957-b7d3-f89c5a115e37","label":"关联"},{"source":"e0a8410b-c5ee-47d4-acbe-b7ae5f111fd4","target":"b0bace0a-eedc-485c-90d3-0a5378dc5556","label":"关联"},{"source":"e0a8410b-c5ee-47d4-acbe-b7ae5f111fd4","target":"ffc12d7b-60e5-4ffa-a945-a0769f6e1047","label":"关联"},{"source":"e0a8410b-c5ee-47d4-acbe-b7ae5f111fd4","target":"a7e94ee7-072b-456f-bc0c-354545851c38","label":"关联"},{"source":"e0a8410b-c5ee-47d4-acbe-b7ae5f111fd4","target":"89f9498b-7a83-4361-889b-f96dfdfb802c","label":"导致"}]}}] |
|
|
|
|
|
// this.initGraph(this.answers[0].result) |
|
|
|
|
|
// this.formatData(this.answers[0].result) |
|
|
qaAnalyze(data).then(res=>{ |
|
|
qaAnalyze(data).then(res=>{ |
|
|
|
|
|
this.answers=res |
|
|
|
|
|
if(this.answers.length>0){ |
|
|
|
|
|
this.initGraph(this.answers[0].result) |
|
|
|
|
|
} |
|
|
}) |
|
|
}) |
|
|
|
|
|
}, |
|
|
|
|
|
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 = { |
|
|
|
|
|
nodes: updatedNodes, |
|
|
|
|
|
edges: updatedEdges |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
this.updateGraph(updatedData) |
|
|
|
|
|
}, |
|
|
|
|
|
updateGraph(data) { |
|
|
|
|
|
if (!this._graph) return |
|
|
|
|
|
this._graph.setData(data) |
|
|
|
|
|
this._graph.render() |
|
|
|
|
|
}, |
|
|
|
|
|
initGraph(data) { |
|
|
|
|
|
if (this._graph!=null){ |
|
|
|
|
|
this._graph.destroy() |
|
|
|
|
|
this._graph = null; |
|
|
|
|
|
} |
|
|
|
|
|
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 = { |
|
|
|
|
|
nodes: updatedNodes, |
|
|
|
|
|
edges: updatedEdges |
|
|
|
|
|
} |
|
|
|
|
|
const container = this.$refs.graphContainer; |
|
|
|
|
|
console.log(container) |
|
|
|
|
|
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 // 防止节点重叠 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}, |
|
|
|
|
|
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?.type; |
|
|
|
|
|
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?.type; |
|
|
|
|
|
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.label, |
|
|
|
|
|
labelPlacement: 'center', |
|
|
|
|
|
labelWordWrap: true, |
|
|
|
|
|
labelMaxWidth: '150%', |
|
|
|
|
|
labelMaxLines: 3, |
|
|
|
|
|
labelTextOverflow: 'ellipsis', |
|
|
|
|
|
labelTextAlign: 'center', |
|
|
|
|
|
opacity: 1 |
|
|
|
|
|
}, |
|
|
|
|
|
state: { |
|
|
|
|
|
active: { |
|
|
|
|
|
lineWidth: 2, |
|
|
|
|
|
shadowColor: '#ffffff', |
|
|
|
|
|
shadowBlur: 10, |
|
|
|
|
|
opacity: 1 |
|
|
|
|
|
}, |
|
|
|
|
|
inactive: { |
|
|
|
|
|
opacity: 0.3 |
|
|
|
|
|
}, |
|
|
|
|
|
normal:{ |
|
|
|
|
|
opacity: 1 |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}, |
|
|
|
|
|
}, |
|
|
|
|
|
edge: { |
|
|
|
|
|
style: { |
|
|
|
|
|
labelText: (d) => d.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.3 |
|
|
|
|
|
}, |
|
|
|
|
|
normal:{ |
|
|
|
|
|
opacity: 1 |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
}, |
|
|
|
|
|
data:updatedData, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
graph.render(); |
|
|
|
|
|
this._graph = graph |
|
|
|
|
|
this._graph?.fitView() |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}, |
|
|
}, |
|
|
}, |
|
|
|
|
|
|
|
|
}; |
|
|
}; |
|
|
@ -96,26 +359,21 @@ export default { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.title { |
|
|
.title { |
|
|
font-size: 20px; |
|
|
font-size: 22px; |
|
|
margin-bottom: 20px; |
|
|
margin-bottom: 20px; |
|
|
padding-left: 12px; |
|
|
|
|
|
position: relative; |
|
|
position: relative; |
|
|
display: flex; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
align-items: center; |
|
|
color: #165DFF; |
|
|
color: #165DFF; |
|
|
margin-left: 15px; |
|
|
margin-left: 11px; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.title::before { |
|
|
.title-before { |
|
|
content: ''; |
|
|
content: ''; |
|
|
width: 6px; |
|
|
width: 8px; |
|
|
height: 16px; |
|
|
height: 22px; |
|
|
background-color: #165DFF; |
|
|
background-color: #165DFF; |
|
|
position: absolute; |
|
|
border-radius: 5px; |
|
|
left: 0; |
|
|
|
|
|
top: 50%; |
|
|
|
|
|
transform: translateY(-50%); |
|
|
|
|
|
border-radius: 2px; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.search-box { |
|
|
.search-box { |
|
|
@ -161,17 +419,17 @@ export default { |
|
|
.section-title::before { |
|
|
.section-title::before { |
|
|
content: ''; |
|
|
content: ''; |
|
|
width: 6px; |
|
|
width: 6px; |
|
|
height: 12px; |
|
|
height: 16px; |
|
|
background-color: #165DFF; |
|
|
background-color: #165DFF; |
|
|
position: absolute; |
|
|
position: absolute; |
|
|
left: 0; |
|
|
left: 0; |
|
|
top: 50%; |
|
|
top: 50%; |
|
|
transform: translateY(-50%); |
|
|
transform: translateY(-50%); |
|
|
border-radius: 2px; |
|
|
border-radius: 5px; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.answer-items { |
|
|
.answer-items { |
|
|
height: 30vw; |
|
|
max-height: 25vw; |
|
|
overflow-y: auto; |
|
|
overflow-y: auto; |
|
|
padding-right: 10px; |
|
|
padding-right: 10px; |
|
|
cursor: pointer; |
|
|
cursor: pointer; |
|
|
|