|
|
|
@ -63,6 +63,12 @@ |
|
|
|
|
|
|
|
<!-- 右侧:知识图谱 --> |
|
|
|
<div class="knowledge-graph"> |
|
|
|
<GraphToolbar |
|
|
|
v-if="_graph" |
|
|
|
:graph="_graph" |
|
|
|
ref="toolbarRef" |
|
|
|
class="custom-graph-toolbar" |
|
|
|
/> |
|
|
|
<div ref="graphContainer" class="graph-container" id="container"></div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
@ -77,11 +83,12 @@ import {qaAnalyze} from "@/api/qa"; |
|
|
|
import {Graph} from "@antv/g6"; |
|
|
|
import {getGraph} from "@/api/graph"; |
|
|
|
import {getGraphStyleActive} from "@/api/style"; |
|
|
|
import GraphToolbar from '@/components/GraphToolbar.vue'; |
|
|
|
|
|
|
|
|
|
|
|
export default { |
|
|
|
name: 'GraghQA', |
|
|
|
components: {Menu}, |
|
|
|
components: {Menu,GraphToolbar}, |
|
|
|
data() { |
|
|
|
return { |
|
|
|
query:"", |
|
|
|
@ -244,47 +251,85 @@ export default { |
|
|
|
} |
|
|
|
|
|
|
|
}, |
|
|
|
handleSearch(){ |
|
|
|
this.isSending=true |
|
|
|
this.answers=[] |
|
|
|
if (this._graph){ |
|
|
|
handleSearch() { |
|
|
|
this.isSending = true |
|
|
|
this.answers = [] |
|
|
|
if (this._graph) { |
|
|
|
this._graph.clear() |
|
|
|
} |
|
|
|
let data={ |
|
|
|
text:this.query |
|
|
|
let data = { |
|
|
|
text: this.query |
|
|
|
} |
|
|
|
this.queryRecord=this.query |
|
|
|
this.query="" |
|
|
|
this.queryRecord = 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=>{ |
|
|
|
this.answers=res |
|
|
|
if(this.answers.length>0){ |
|
|
|
qaAnalyze(data).then(res => { |
|
|
|
this.answers = res |
|
|
|
if (this.answers.length > 0) { |
|
|
|
this.initGraph(this.answers[0].result) |
|
|
|
} |
|
|
|
this.isSending=false |
|
|
|
}) |
|
|
|
this.isSending = false |
|
|
|
}).catch(err => { |
|
|
|
console.error('接口失败,启动保底方案', err); |
|
|
|
const mockData = { |
|
|
|
nodes: [ |
|
|
|
{id: "node1", label: "霍乱", data: {type: "疾病"}}, |
|
|
|
{id: "node2", label: "腹泻", data: {type: "症状"}}, |
|
|
|
{id: "node3", label: "脱水", data: {type: "疾病"}}, |
|
|
|
{id: "node4", label: "呕吐", data: {type: "疾病"}}, |
|
|
|
{id: "node5", label: "霍乱弧菌", data: {type: "病因"}}, |
|
|
|
{id: "node6", label: "复方磺胺", data: {type: "药品"}}, |
|
|
|
], |
|
|
|
edges: [ |
|
|
|
{id: "e1", source: "node1", target: "node2", data: {label: "典型症状"}}, |
|
|
|
{id: "e2", source: "node1", target: "node3", data: {label: "并发症"}}, |
|
|
|
{id: "e3", source: "node1", target: "node4", data: {label: "并发症"}}, |
|
|
|
{id: "e4", source: "node1", target: "node5", data: {label: "致病菌"}}, |
|
|
|
{id: "e5", source: "node1", target: "node6", data: {label: "推荐用药"}}, |
|
|
|
] |
|
|
|
}; |
|
|
|
this.answers = [{answer: "连接失败,显示预览版。", result: mockData}]; |
|
|
|
this.initGraph(this.answers[0].result); |
|
|
|
this.isSending = false; |
|
|
|
}); |
|
|
|
}, |
|
|
|
formatData(data){ |
|
|
|
// this._graph.stopLayout(); |
|
|
|
// this.clearGraphState(); |
|
|
|
// === 1. 构建 nodeId → label 映射 === |
|
|
|
const nodeIdToEnLabel = {}; |
|
|
|
const typeMap = { '疾病': 'Disease', '药品': 'Drug', '药物': 'Drug', '症状': 'Symptom', '检查': 'Check', '病因': 'Cause' }; |
|
|
|
const getStandardLabel = (rawType) => typeMap[rawType] || rawType; |
|
|
|
|
|
|
|
const nodeIdToData = {}; |
|
|
|
data.nodes.forEach(node => { |
|
|
|
nodeIdToEnLabel[node.id] = node.data.type; // e.g. "Disease" |
|
|
|
nodeIdToData[node.id] = { |
|
|
|
enLabel: getStandardLabel(node.data.type), |
|
|
|
rawType: node.data.type |
|
|
|
}; |
|
|
|
}); |
|
|
|
// === 2. 处理节点:根据自身 label 设置样式 === |
|
|
|
const updatedNodes = data.nodes.map(node => { |
|
|
|
const enLabel = node.data.type; |
|
|
|
const enLabel = getStandardLabel(node.data.type); |
|
|
|
const styleConf = this.parsedStyles[enLabel] || {}; |
|
|
|
// 💡 颜色映射逻辑:如果 styleConf 没给颜色,则按类型分配 |
|
|
|
let fColor = styleConf.nodeFill; |
|
|
|
if (!fColor) { |
|
|
|
if (node.data.type === '疾病') fColor = '#EF4444'; |
|
|
|
else if (node.data.type === '药品' || node.data.type === '药物') fColor = '#91cc75'; |
|
|
|
else if (node.data.type === '症状') fColor = '#fac858'; |
|
|
|
else if (node.data.type === '检查') fColor = '#336eee'; |
|
|
|
else fColor = this.nodeFill; |
|
|
|
} |
|
|
|
return { |
|
|
|
...node, |
|
|
|
type: styleConf.nodeShape || this.nodeShape, |
|
|
|
data: { ...node.data, label: enLabel, name: node.label }, |
|
|
|
style: { |
|
|
|
...node.style, |
|
|
|
size: styleConf.nodeSize || this.nodeSize, |
|
|
|
fill: styleConf.nodeFill || this.nodeFill, |
|
|
|
fill: fColor, |
|
|
|
stroke: styleConf.nodeStroke || this.nodeStroke, |
|
|
|
lineWidth: styleConf.nodeLineWidth || this.nodeLineWidth, |
|
|
|
label: styleConf.nodeShowLabel !== undefined ? styleConf.nodeShowLabel : true, |
|
|
|
@ -297,15 +342,25 @@ export default { |
|
|
|
|
|
|
|
// === 3. 处理边:根据 source 节点的 label 设置样式 === |
|
|
|
const updatedEdges = data.edges.map(edge => { |
|
|
|
console.log(edge) |
|
|
|
const sourceEnLabel = nodeIdToEnLabel[edge.source]; // e.g. "Disease" |
|
|
|
const styleConf = this.parsedStyles[sourceEnLabel] || {}; |
|
|
|
const sourceInfo = nodeIdToData[edge.source]; |
|
|
|
const styleConf = this.parsedStyles[sourceInfo.enLabel] || {}; |
|
|
|
|
|
|
|
// 💡 边颜色逻辑:跟随源节点类型 |
|
|
|
let eStroke = styleConf.edgeStroke; |
|
|
|
if (!eStroke) { |
|
|
|
if (sourceInfo.rawType === '疾病') eStroke = 'rgba(239, 68, 68, 0.4)'; |
|
|
|
else if (sourceInfo.rawType === '药品' || sourceInfo.rawType === '药物') eStroke = 'rgba(145, 204, 117, 0.4)'; |
|
|
|
else if (sourceInfo.rawType === '症状') eStroke = 'rgba(250, 200, 88, 0.4)'; |
|
|
|
else eStroke = this.edgeStroke; |
|
|
|
} |
|
|
|
|
|
|
|
return { |
|
|
|
...edge, |
|
|
|
id: edge.data?.relationship?.id || edge.id, |
|
|
|
type: styleConf.edgeType ||this.edgeType, |
|
|
|
data: { ...edge.data, label: edge.data?.label || "" }, |
|
|
|
style: { |
|
|
|
...edge.style, |
|
|
|
endArrow: styleConf.edgeEndArrow !== undefined ? styleConf.edgeEndArrow : true, |
|
|
|
stroke: styleConf.edgeStroke || this.edgeStroke, |
|
|
|
lineWidth: styleConf.edgeLineWidth || this.edgeLineWidth, |
|
|
|
@ -318,41 +373,49 @@ export default { |
|
|
|
}); |
|
|
|
|
|
|
|
// === 4. 更新图数据 === |
|
|
|
let updatedData = { |
|
|
|
nodes: updatedNodes, |
|
|
|
edges: updatedEdges |
|
|
|
}; |
|
|
|
|
|
|
|
this.updateGraph(updatedData) |
|
|
|
const pureData = JSON.parse(JSON.stringify({ nodes: updatedNodes, edges: updatedEdges })); |
|
|
|
this.updateGraph(pureData); |
|
|
|
}, |
|
|
|
updateGraph(data) { |
|
|
|
if (!this._graph) return |
|
|
|
|
|
|
|
this._graph.setData(data) |
|
|
|
this._graph.render() |
|
|
|
if (!this._graph) return; |
|
|
|
this._graph.setData(data); |
|
|
|
this._graph.render(); |
|
|
|
}, |
|
|
|
initGraph(data) { |
|
|
|
const typeMap = { '疾病': 'Disease', '药品': 'Drug', '药物': 'Drug', '症状': 'Symptom', '检查': 'Check', '病因': 'Cause' }; |
|
|
|
const getStandardLabel = (rawType) => typeMap[rawType] || rawType; |
|
|
|
if (this._graph!=null){ |
|
|
|
this._graph.destroy() |
|
|
|
this._graph = null; |
|
|
|
} |
|
|
|
console.log(data) |
|
|
|
// === 1. 构建 nodeId → label 映射 === |
|
|
|
const nodeIdToEnLabel = {}; |
|
|
|
const nodeIdToData = {}; |
|
|
|
data.nodes.forEach(node => { |
|
|
|
nodeIdToEnLabel[node.id] = node.data.type; // e.g. "Disease" |
|
|
|
nodeIdToData[node.id] = { |
|
|
|
enLabel: getStandardLabel(node.data.type), |
|
|
|
rawType: node.data.type |
|
|
|
}; |
|
|
|
}); |
|
|
|
console.log(nodeIdToEnLabel) |
|
|
|
// === 2. 处理节点:根据自身 label 设置样式 === |
|
|
|
const updatedNodes = data.nodes.map(node => { |
|
|
|
const enLabel = node.data.type; |
|
|
|
const enLabel = getStandardLabel(node.data.type); |
|
|
|
const styleConf = this.parsedStyles[enLabel] || {}; |
|
|
|
let fColor = styleConf.nodeFill; |
|
|
|
if (!fColor) { |
|
|
|
if (node.data.type === '疾病') fColor = '#EF4444'; |
|
|
|
else if (node.data.type === '药品' || node.data.type === '药物') fColor = '#91cc75'; |
|
|
|
else if (node.data.type === '症状') fColor = '#fac858'; |
|
|
|
else if (node.data.type === '检查') fColor = '#336eee'; |
|
|
|
else fColor = this.nodeFill; |
|
|
|
} |
|
|
|
return { |
|
|
|
...node, |
|
|
|
type: styleConf.nodeShape || this.nodeShape, |
|
|
|
data: { ...node.data, label: enLabel, name: node.label }, |
|
|
|
style: { |
|
|
|
...node.style, |
|
|
|
size: styleConf.nodeSize || this.nodeSize, |
|
|
|
fill: styleConf.nodeFill || this.nodeFill, |
|
|
|
fill: fColor, |
|
|
|
stroke: styleConf.nodeStroke || this.nodeStroke, |
|
|
|
lineWidth: styleConf.nodeLineWidth || this.nodeLineWidth, |
|
|
|
label: styleConf.nodeShowLabel !== undefined ? styleConf.nodeShowLabel : true, |
|
|
|
@ -365,17 +428,26 @@ export default { |
|
|
|
|
|
|
|
// === 3. 处理边:根据 source 节点的 label 设置样式 === |
|
|
|
const updatedEdges = data.edges.map(edge => { |
|
|
|
console.log(edge) |
|
|
|
const sourceEnLabel = nodeIdToEnLabel[edge.source]; // e.g. "Disease" |
|
|
|
const styleConf = this.parsedStyles[sourceEnLabel] || {}; |
|
|
|
const sourceInfo = nodeIdToData[edge.source]; |
|
|
|
const styleConf = this.parsedStyles[sourceInfo.enLabel] || {}; |
|
|
|
|
|
|
|
let eStroke = styleConf.edgeStroke; |
|
|
|
if (!eStroke) { |
|
|
|
if (sourceInfo.rawType === '疾病') eStroke = 'rgba(239, 68, 68, 0.4)'; |
|
|
|
else if (sourceInfo.rawType === '药品' || sourceInfo.rawType === '药物') eStroke = 'rgba(145, 204, 117, 0.4)'; |
|
|
|
else if (sourceInfo.rawType === '症状') eStroke = 'rgba(250, 200, 88, 0.4)'; |
|
|
|
else eStroke = this.edgeStroke; |
|
|
|
} |
|
|
|
|
|
|
|
return { |
|
|
|
...edge, |
|
|
|
id: edge.data?.relationship?.id || edge.id, |
|
|
|
type: styleConf.edgeType ||this.edgeType, |
|
|
|
data: { ...edge.data, label: edge.data?.label || "" }, |
|
|
|
style: { |
|
|
|
...edge.style, |
|
|
|
endArrow: styleConf.edgeEndArrow !== undefined ? styleConf.edgeEndArrow : true, |
|
|
|
stroke: styleConf.edgeStroke || this.edgeStroke, |
|
|
|
stroke: eStroke, |
|
|
|
lineWidth: styleConf.edgeLineWidth || this.edgeLineWidth, |
|
|
|
label: styleConf.edgeShowLabel !== undefined ? styleConf.edgeShowLabel : false, |
|
|
|
labelFontSize: styleConf.edgeFontSize || this.edgeFontSize, |
|
|
|
@ -402,14 +474,27 @@ export default { |
|
|
|
container, |
|
|
|
width, |
|
|
|
height, |
|
|
|
plugins: [ |
|
|
|
{ |
|
|
|
type: 'toolbar', |
|
|
|
key: 'g6-toolbar', |
|
|
|
onClick: (id) => { |
|
|
|
if (this.$refs.toolbarRef) this.$refs.toolbarRef.handleToolbarAction(id); |
|
|
|
}, |
|
|
|
getItems: () => [ |
|
|
|
{ id: 'zoom-in', value: 'zoom-in', title: '放大' }, |
|
|
|
{ id: 'zoom-out', value: 'zoom-out', title: '缩小' }, |
|
|
|
{ id: 'auto-fit', value: 'auto-fit', title: '聚焦' }, |
|
|
|
{ id: 'export', value: 'export', title: '导出图谱' }, |
|
|
|
], |
|
|
|
}, |
|
|
|
], |
|
|
|
layout: { |
|
|
|
type: 'force', // 力导向布局 |
|
|
|
gravity: 0.3, // 重力系数,控制节点聚集程度 |
|
|
|
repulsion: 500, // 排斥力 |
|
|
|
attraction: 20, // 吸引力 |
|
|
|
preventOverlap: true // 防止节点重叠 |
|
|
|
|
|
|
|
|
|
|
|
}, |
|
|
|
behaviors: [ 'zoom-canvas', 'drag-element', |
|
|
|
'click-select','focus-element', { |
|
|
|
@ -724,4 +809,39 @@ export default { |
|
|
|
.dot-2 { animation-delay: -0.2s; } |
|
|
|
.dot-3 { animation-delay: 0s; } |
|
|
|
.dot-4 { animation-delay: 0.2s; } |
|
|
|
|
|
|
|
/* 右侧图谱外层容器 */ |
|
|
|
.knowledge-graph { |
|
|
|
position: relative !important; /* 保持相对定位,作为工具栏的参照物 */ |
|
|
|
flex: 1; |
|
|
|
height: 100%; |
|
|
|
background-color: white; |
|
|
|
border-radius: 16px; |
|
|
|
padding: 0 !important; |
|
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.05); |
|
|
|
overflow: hidden; |
|
|
|
display: flex; |
|
|
|
} |
|
|
|
|
|
|
|
/* 图谱画布容器 */ |
|
|
|
.graph-container { |
|
|
|
width: 100% !important; |
|
|
|
height: 100% !important; |
|
|
|
margin: 0 !important; |
|
|
|
padding: 0 !important; |
|
|
|
} |
|
|
|
|
|
|
|
/* 工具栏浮动修饰 */ |
|
|
|
:deep(.custom-graph-toolbar) { |
|
|
|
position: absolute !important; /* 必须绝对定位,不占物理高度 */ |
|
|
|
top: 0 !important; |
|
|
|
right: 0 !important; |
|
|
|
z-index: 9999 !important; |
|
|
|
display: inline-flex !important; |
|
|
|
background: rgba(255, 255, 255, 0.9) !important; |
|
|
|
padding: 5px !important; |
|
|
|
border-radius: 8px !important; |
|
|
|
border: 1px solid #ebeef5 !important; |
|
|
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1) !important; |
|
|
|
} |
|
|
|
</style> |