|
|
@ -33,7 +33,7 @@ |
|
|
<div class="answer-item" style=" background-color: #165DFF; |
|
|
<div class="answer-item" style=" background-color: #165DFF; |
|
|
border-color: #0066cc; |
|
|
border-color: #0066cc; |
|
|
color: #fff; height: auto;max-height: 3vw"> |
|
|
color: #fff; height: auto;max-height: 3vw"> |
|
|
{{queryRecord}} |
|
|
{{queryRecord}} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<h2 class="section-title">问答结果</h2> |
|
|
<h2 class="section-title">问答结果</h2> |
|
|
@ -63,12 +63,6 @@ |
|
|
|
|
|
|
|
|
<!-- 右侧:知识图谱 --> |
|
|
<!-- 右侧:知识图谱 --> |
|
|
<div class="knowledge-graph"> |
|
|
<div class="knowledge-graph"> |
|
|
<GraphToolbar |
|
|
|
|
|
v-if="_graph" |
|
|
|
|
|
:graph="_graph" |
|
|
|
|
|
ref="toolbarRef" |
|
|
|
|
|
/> |
|
|
|
|
|
|
|
|
|
|
|
<div ref="graphContainer" class="graph-container" id="container"></div> |
|
|
<div ref="graphContainer" class="graph-container" id="container"></div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
@ -79,21 +73,21 @@ |
|
|
|
|
|
|
|
|
<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 {Graph} from "@antv/g6"; |
|
|
import { getGraphStyleActive } from "@/api/style"; |
|
|
import {getGraph} from "@/api/graph"; |
|
|
import GraphToolbar from '@/components/GraphToolbar.vue'; |
|
|
import {getGraphStyleActive} from "@/api/style"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export default { |
|
|
export default { |
|
|
name: 'GraghQA', |
|
|
name: 'GraghQA', |
|
|
components: { Menu, GraphToolbar }, |
|
|
components: {Menu}, |
|
|
data() { |
|
|
data() { |
|
|
return { |
|
|
return { |
|
|
_graph: null, |
|
|
query:"", |
|
|
query: "", |
|
|
answers:[], |
|
|
answers: [], |
|
|
selected:0, |
|
|
selected: 0, |
|
|
// 节点样式 |
|
|
// 节点基础样式 |
|
|
|
|
|
nodeShowLabel: true, |
|
|
nodeShowLabel: true, |
|
|
nodeFontSize: 12, |
|
|
nodeFontSize: 12, |
|
|
nodeFontColor: '#fff', |
|
|
nodeFontColor: '#fff', |
|
|
@ -104,7 +98,7 @@ export default { |
|
|
nodeLineWidth: 2, |
|
|
nodeLineWidth: 2, |
|
|
nodeFontFamily: 'Microsoft YaHei, sans-serif', |
|
|
nodeFontFamily: 'Microsoft YaHei, sans-serif', |
|
|
|
|
|
|
|
|
// 边基础样式 |
|
|
// 边样式 |
|
|
edgeShowLabel: true, |
|
|
edgeShowLabel: true, |
|
|
edgeFontSize: 10, |
|
|
edgeFontSize: 10, |
|
|
edgeFontColor: '#666666', |
|
|
edgeFontColor: '#666666', |
|
|
@ -114,10 +108,11 @@ export default { |
|
|
edgeEndArrow: true, |
|
|
edgeEndArrow: true, |
|
|
edgeFontFamily: 'Microsoft YaHei, sans-serif', |
|
|
edgeFontFamily: 'Microsoft YaHei, sans-serif', |
|
|
|
|
|
|
|
|
queryRecord: "", |
|
|
|
|
|
isSending: false, |
|
|
queryRecord:"", |
|
|
configs: [], |
|
|
isSending:false, |
|
|
parsedStyles: {}, |
|
|
configs:[], |
|
|
|
|
|
parsedStyles:{}, |
|
|
enToZhLabelMap: { |
|
|
enToZhLabelMap: { |
|
|
Disease: '疾病', |
|
|
Disease: '疾病', |
|
|
Drug: '药品', |
|
|
Drug: '药品', |
|
|
@ -125,31 +120,41 @@ export default { |
|
|
Symptom: '症状', |
|
|
Symptom: '症状', |
|
|
Other: '其他' |
|
|
Other: '其他' |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
}; |
|
|
}; |
|
|
}, |
|
|
}, |
|
|
|
|
|
// =============== 👇【新增】组件内路由守卫:离开当前路由时触发 =============== |
|
|
beforeRouteLeave(to, from, next) { |
|
|
beforeRouteLeave(to, from, next) { |
|
|
this.saveDataToLocalStorage(); |
|
|
this.saveDataToLocalStorage(); |
|
|
next(); |
|
|
next(); // 允许导航 |
|
|
}, |
|
|
}, |
|
|
|
|
|
// ======================================================================= |
|
|
|
|
|
|
|
|
async mounted() { |
|
|
async mounted() { |
|
|
await this.getDefault(); |
|
|
await this.getDefault() |
|
|
|
|
|
// =============== 👇【新增】页面加载时从 localStorage 恢复数据 =============== |
|
|
this.restoreDataFromLocalStorage(); |
|
|
this.restoreDataFromLocalStorage(); |
|
|
|
|
|
// ======================================================================= |
|
|
|
|
|
// this.answers=[] |
|
|
|
|
|
// 如果有初始数据,可以初始化图谱(可选) |
|
|
if (this.answers.length > 0) { |
|
|
if (this.answers.length > 0) { |
|
|
this.initGraph(this.answers[this.selected].result); |
|
|
this.initGraph(this.answers[0].result); |
|
|
|
|
|
// console.log(this.answers[0].result) |
|
|
} |
|
|
} |
|
|
}, |
|
|
}, |
|
|
|
|
|
|
|
|
beforeUnmount() { |
|
|
beforeUnmount() { |
|
|
|
|
|
// =============== 👇【新增】组件销毁前也保存一次(兼容非路由跳转场景)============== |
|
|
this.saveDataToLocalStorage(); |
|
|
this.saveDataToLocalStorage(); |
|
|
|
|
|
// 移除页面卸载监听(如果用了 beforeunload) |
|
|
window.removeEventListener('beforeunload', this.handleBeforeUnload); |
|
|
window.removeEventListener('beforeunload', this.handleBeforeUnload); |
|
|
|
|
|
// ======================================================================= |
|
|
}, |
|
|
}, |
|
|
|
|
|
|
|
|
created() { |
|
|
created() { |
|
|
|
|
|
// =============== 👇【新增】监听页面刷新/关闭事件 =============== |
|
|
window.addEventListener('beforeunload', this.handleBeforeUnload); |
|
|
window.addEventListener('beforeunload', this.handleBeforeUnload); |
|
|
|
|
|
// ======================================================================= |
|
|
}, |
|
|
}, |
|
|
|
|
|
|
|
|
methods: { |
|
|
methods: { |
|
|
safeParseStyles(stylesStr) { |
|
|
safeParseStyles(stylesStr) { |
|
|
try { |
|
|
try { |
|
|
@ -159,8 +164,7 @@ export default { |
|
|
return {}; |
|
|
return {}; |
|
|
} |
|
|
} |
|
|
}, |
|
|
}, |
|
|
|
|
|
async getDefault(){ |
|
|
async getDefault() { |
|
|
|
|
|
const response = await getGraphStyleActive(); |
|
|
const response = await getGraphStyleActive(); |
|
|
const data = response.data; |
|
|
const data = response.data; |
|
|
if (!Array.isArray(data) || data.length === 0) { |
|
|
if (!Array.isArray(data) || data.length === 0) { |
|
|
@ -168,23 +172,27 @@ export default { |
|
|
this.parsedStyles = {}; |
|
|
this.parsedStyles = {}; |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 只取第一个(即 is_active=1 的组) |
|
|
const activeGroup = data[0]; |
|
|
const activeGroup = data[0]; |
|
|
this.configs = Array.isArray(activeGroup.configs) ? activeGroup.configs : []; |
|
|
this.configs = Array.isArray(activeGroup.configs) ? activeGroup.configs : []; |
|
|
|
|
|
|
|
|
|
|
|
// 构建 label -> style 映射 |
|
|
const styleMap = {}; |
|
|
const styleMap = {}; |
|
|
this.configs.forEach(config => { |
|
|
this.configs.forEach(config => { |
|
|
const label = config.current_label; |
|
|
const label = config.current_label; |
|
|
styleMap[label] = this.safeParseStyles(config.styles); |
|
|
styleMap[label] = this.safeParseStyles(config.styles); |
|
|
}); |
|
|
}); |
|
|
this.parsedStyles = styleMap; |
|
|
this.parsedStyles = styleMap; |
|
|
|
|
|
console.log(this.parsedStyles) |
|
|
}, |
|
|
}, |
|
|
|
|
|
|
|
|
buildNodeLabelMap(nodes) { |
|
|
buildNodeLabelMap(nodes) { |
|
|
this._nodeLabelMap = new Map(); |
|
|
this._nodeLabelMap = new Map(); |
|
|
nodes.forEach(node => { |
|
|
nodes.forEach(node => { |
|
|
this._nodeLabelMap.set(node.id, node.data?.type || 'default'); |
|
|
this._nodeLabelMap.set(node.id, node.data?.type || 'default'); |
|
|
}); |
|
|
}); |
|
|
}, |
|
|
}, |
|
|
|
|
|
// =============== 👇【新增】统一的数据保存方法 =============== |
|
|
saveDataToLocalStorage() { |
|
|
saveDataToLocalStorage() { |
|
|
try { |
|
|
try { |
|
|
localStorage.setItem('graphQA_queryRecord', this.queryRecord); |
|
|
localStorage.setItem('graphQA_queryRecord', this.queryRecord); |
|
|
@ -194,110 +202,89 @@ export default { |
|
|
console.warn('⚠️ 无法保存到 localStorage:', e); |
|
|
console.warn('⚠️ 无法保存到 localStorage:', e); |
|
|
} |
|
|
} |
|
|
}, |
|
|
}, |
|
|
|
|
|
// ======================================================================= |
|
|
|
|
|
|
|
|
|
|
|
// =============== 👇【新增】统一的数据恢复方法 =============== |
|
|
restoreDataFromLocalStorage() { |
|
|
restoreDataFromLocalStorage() { |
|
|
try { |
|
|
try { |
|
|
const savedQuery = localStorage.getItem('graphQA_queryRecord'); |
|
|
const savedQuery = localStorage.getItem('graphQA_queryRecord'); |
|
|
const savedAnswers = localStorage.getItem('graphQA_answers'); |
|
|
const savedAnswers = localStorage.getItem('graphQA_answers'); |
|
|
if (savedQuery !== null) this.queryRecord = savedQuery; |
|
|
|
|
|
|
|
|
if (savedQuery !== null) { |
|
|
|
|
|
this.queryRecord = savedQuery; |
|
|
|
|
|
} |
|
|
if (savedAnswers !== null) { |
|
|
if (savedAnswers !== null) { |
|
|
this.answers = JSON.parse(savedAnswers); |
|
|
this.answers = JSON.parse(savedAnswers); |
|
|
|
|
|
// 确保 selected 不越界 |
|
|
if (this.answers.length > 0) { |
|
|
if (this.answers.length > 0) { |
|
|
this.selected = Math.min(this.selected, this.answers.length - 1); |
|
|
this.selected = Math.min(this.selected, this.answers.length - 1); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
console.log('✅ 数据已从 localStorage 恢复'); |
|
|
} catch (e) { |
|
|
} catch (e) { |
|
|
console.warn('⚠️ 无法从 localStorage 恢复数据:', e); |
|
|
console.warn('⚠️ 无法从 localStorage 恢复数据:', e); |
|
|
|
|
|
// 出错时清空(避免脏数据) |
|
|
|
|
|
localStorage.removeItem('graphQA_queryRecord'); |
|
|
|
|
|
localStorage.removeItem('graphQA_answers'); |
|
|
} |
|
|
} |
|
|
}, |
|
|
}, |
|
|
|
|
|
// ======================================================================= |
|
|
|
|
|
|
|
|
|
|
|
// =============== 👇【新增】处理页面关闭/刷新的兜底保存 =============== |
|
|
handleBeforeUnload(event) { |
|
|
handleBeforeUnload(event) { |
|
|
this.saveDataToLocalStorage(); |
|
|
this.saveDataToLocalStorage(); |
|
|
|
|
|
// 注意:现代浏览器通常不显示自定义消息 |
|
|
event.preventDefault(); |
|
|
event.preventDefault(); |
|
|
event.returnValue = ''; |
|
|
event.returnValue = ''; // 必须设置才能触发提示(但实际可能不显示) |
|
|
}, |
|
|
}, |
|
|
|
|
|
selectGraph(index){ |
|
|
selectGraph(index) { |
|
|
this.selected=index |
|
|
this.selected = index; |
|
|
if(this.answers.length>0){ |
|
|
if (this.answers.length > 0) { |
|
|
this.formatData(this.answers[index].result) |
|
|
this.formatData(this.answers[index].result); |
|
|
|
|
|
} |
|
|
} |
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
handleSearch() { |
|
|
|
|
|
this.isSending = true; |
|
|
|
|
|
this.answers = []; |
|
|
|
|
|
if (this._graph) this._graph.clear(); |
|
|
|
|
|
|
|
|
|
|
|
let data = { text: this.query }; |
|
|
|
|
|
this.queryRecord = this.query; |
|
|
|
|
|
this.query = ""; |
|
|
|
|
|
|
|
|
|
|
|
qaAnalyze(data).then(res => { |
|
|
|
|
|
this.answers = res; |
|
|
|
|
|
if (this.answers && this.answers.length > 0) { |
|
|
|
|
|
this.initGraph(this.answers[0].result); |
|
|
|
|
|
} |
|
|
|
|
|
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; |
|
|
|
|
|
}); |
|
|
|
|
|
}, |
|
|
}, |
|
|
|
|
|
handleSearch(){ |
|
|
formatData(data) { |
|
|
this.isSending=true |
|
|
const typeMap = { '疾病': 'Disease', '药品': 'Drug', '药物': 'Drug', '症状': 'Symptom', '检查': 'Check', '病因': 'Cause' }; |
|
|
this.answers=[] |
|
|
const getStandardLabel = (rawType) => typeMap[rawType] || rawType; |
|
|
if (this._graph){ |
|
|
|
|
|
this._graph.clear() |
|
|
const nodeIdToData = {}; |
|
|
} |
|
|
|
|
|
let data={ |
|
|
|
|
|
text: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){ |
|
|
|
|
|
this.initGraph(this.answers[0].result) |
|
|
|
|
|
} |
|
|
|
|
|
this.isSending=false |
|
|
|
|
|
}) |
|
|
|
|
|
}, |
|
|
|
|
|
formatData(data){ |
|
|
|
|
|
// this._graph.stopLayout(); |
|
|
|
|
|
// this.clearGraphState(); |
|
|
|
|
|
// === 1. 构建 nodeId → label 映射 === |
|
|
|
|
|
const nodeIdToEnLabel = {}; |
|
|
data.nodes.forEach(node => { |
|
|
data.nodes.forEach(node => { |
|
|
nodeIdToData[node.id] = { |
|
|
nodeIdToEnLabel[node.id] = node.data.type; // e.g. "Disease" |
|
|
enLabel: getStandardLabel(node.data.type), |
|
|
|
|
|
rawType: node.data.type |
|
|
|
|
|
}; |
|
|
|
|
|
}); |
|
|
}); |
|
|
|
|
|
// === 2. 处理节点:根据自身 label 设置样式 === |
|
|
const updatedNodes = data.nodes.map(node => { |
|
|
const updatedNodes = data.nodes.map(node => { |
|
|
const enLabel = getStandardLabel(node.data.type); |
|
|
const enLabel = node.data.type; |
|
|
const styleConf = this.parsedStyles[enLabel] || {}; |
|
|
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 { |
|
|
return { |
|
|
...node, |
|
|
...node, |
|
|
type: styleConf.nodeShape || this.nodeShape, |
|
|
type: styleConf.nodeShape || this.nodeShape, |
|
|
data: { ...node.data, label: enLabel, name: node.label }, |
|
|
|
|
|
style: { |
|
|
style: { |
|
|
...node.style, |
|
|
|
|
|
size: styleConf.nodeSize || this.nodeSize, |
|
|
size: styleConf.nodeSize || this.nodeSize, |
|
|
fill: fColor, |
|
|
fill: styleConf.nodeFill || this.nodeFill, |
|
|
stroke: styleConf.nodeStroke || this.nodeStroke, |
|
|
stroke: styleConf.nodeStroke || this.nodeStroke, |
|
|
lineWidth: styleConf.nodeLineWidth || this.nodeLineWidth, |
|
|
lineWidth: styleConf.nodeLineWidth || this.nodeLineWidth, |
|
|
label: styleConf.nodeShowLabel !== undefined ? styleConf.nodeShowLabel : true, |
|
|
label: styleConf.nodeShowLabel !== undefined ? styleConf.nodeShowLabel : true, |
|
|
@ -308,97 +295,64 @@ export default { |
|
|
}; |
|
|
}; |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// === 3. 处理边:根据 source 节点的 label 设置样式 === |
|
|
const updatedEdges = data.edges.map(edge => { |
|
|
const updatedEdges = data.edges.map(edge => { |
|
|
const sourceInfo = nodeIdToData[edge.source]; |
|
|
console.log(edge) |
|
|
const styleConf = this.parsedStyles[sourceInfo.enLabel] || {}; |
|
|
const sourceEnLabel = nodeIdToEnLabel[edge.source]; // e.g. "Disease" |
|
|
|
|
|
const styleConf = this.parsedStyles[sourceEnLabel] || {}; |
|
|
// 💡 边颜色逻辑:跟随源节点类型 |
|
|
|
|
|
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 { |
|
|
return { |
|
|
...edge, |
|
|
...edge, |
|
|
id: edge.data?.relationship?.id || edge.id, |
|
|
id: edge.data?.relationship?.id || edge.id, |
|
|
type: styleConf.edgeType || this.edgeType, |
|
|
type: styleConf.edgeType ||this.edgeType, |
|
|
style: { |
|
|
style: { |
|
|
...edge.style, |
|
|
|
|
|
endArrow: styleConf.edgeEndArrow !== undefined ? styleConf.edgeEndArrow : true, |
|
|
endArrow: styleConf.edgeEndArrow !== undefined ? styleConf.edgeEndArrow : true, |
|
|
stroke: eStroke, |
|
|
stroke: styleConf.edgeStroke || this.edgeStroke, |
|
|
lineWidth: styleConf.edgeLineWidth || this.edgeLineWidth, |
|
|
lineWidth: styleConf.edgeLineWidth || this.edgeLineWidth, |
|
|
label: styleConf.edgeShowLabel !== undefined ? styleConf.edgeShowLabel : false, |
|
|
label: styleConf.edgeShowLabel !== undefined ? styleConf.edgeShowLabel : false, |
|
|
labelFontSize: styleConf.edgeFontSize || this.edgeFontSize, |
|
|
labelFontSize: styleConf.edgeFontSize || this.edgeFontSize, |
|
|
labelFontFamily: styleConf.edgeFontFamily || this.edgeFontFamily, |
|
|
labelFontFamily: styleConf.edgeFontFamily || this.edgeFontFamily, |
|
|
labelFill: styleConf.edgeFontColor || this.edgeFontColor |
|
|
labelFill: styleConf.edgeFontColor || this.edgeFontColor |
|
|
}, |
|
|
} |
|
|
data: { ...edge.data, label: edge.data?.label || "" }, |
|
|
|
|
|
}; |
|
|
}; |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
const pureData = JSON.parse(JSON.stringify({ nodes: updatedNodes, edges: updatedEdges })); |
|
|
// === 4. 更新图数据 === |
|
|
this.updateGraph(pureData); |
|
|
let updatedData = { |
|
|
}, |
|
|
nodes: updatedNodes, |
|
|
|
|
|
edges: updatedEdges |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
updateGraph(data) { |
|
|
this.updateGraph(updatedData) |
|
|
if (!this._graph) return; |
|
|
|
|
|
this._graph.setData(data); |
|
|
|
|
|
this._graph.render(); |
|
|
|
|
|
}, |
|
|
}, |
|
|
|
|
|
updateGraph(data) { |
|
|
|
|
|
if (!this._graph) return |
|
|
|
|
|
|
|
|
localResetGraph() { |
|
|
this._graph.setData(data) |
|
|
if (!this._graph) return; |
|
|
this._graph.render() |
|
|
const currentResult = this.answers[this.selected]?.result; |
|
|
|
|
|
if (!currentResult) return; |
|
|
|
|
|
this._graph.destroy(); |
|
|
|
|
|
this._graph = null; |
|
|
|
|
|
this.$nextTick(() => { |
|
|
|
|
|
this.initGraph(currentResult); |
|
|
|
|
|
this.$message.success("图谱已重置"); |
|
|
|
|
|
}); |
|
|
|
|
|
}, |
|
|
}, |
|
|
|
|
|
|
|
|
initGraph(data) { |
|
|
initGraph(data) { |
|
|
const typeMap = { '疾病': 'Disease', '药品': 'Drug', '药物': 'Drug', '症状': 'Symptom', '检查': 'Check', '病因': 'Cause' }; |
|
|
if (this._graph!=null){ |
|
|
const getStandardLabel = (rawType) => typeMap[rawType] || rawType; |
|
|
this._graph.destroy() |
|
|
|
|
|
|
|
|
if (this._graph != null) { |
|
|
|
|
|
this._graph.destroy(); |
|
|
|
|
|
this._graph = null; |
|
|
this._graph = null; |
|
|
} |
|
|
} |
|
|
|
|
|
console.log(data) |
|
|
const nodeIdToData = {}; |
|
|
// === 1. 构建 nodeId → label 映射 === |
|
|
|
|
|
const nodeIdToEnLabel = {}; |
|
|
data.nodes.forEach(node => { |
|
|
data.nodes.forEach(node => { |
|
|
nodeIdToData[node.id] = { |
|
|
nodeIdToEnLabel[node.id] = node.data.type; // e.g. "Disease" |
|
|
enLabel: getStandardLabel(node.data.type), |
|
|
|
|
|
rawType: node.data.type |
|
|
|
|
|
}; |
|
|
|
|
|
}); |
|
|
}); |
|
|
|
|
|
console.log(nodeIdToEnLabel) |
|
|
|
|
|
// === 2. 处理节点:根据自身 label 设置样式 === |
|
|
const updatedNodes = data.nodes.map(node => { |
|
|
const updatedNodes = data.nodes.map(node => { |
|
|
const enLabel = getStandardLabel(node.data.type); |
|
|
const enLabel = node.data.type; |
|
|
const styleConf = this.parsedStyles[enLabel] || {}; |
|
|
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 { |
|
|
return { |
|
|
...node, |
|
|
...node, |
|
|
type: styleConf.nodeShape || this.nodeShape, |
|
|
type: styleConf.nodeShape || this.nodeShape, |
|
|
data: { ...node.data, label: enLabel, name: node.label }, |
|
|
|
|
|
style: { |
|
|
style: { |
|
|
...node.style, |
|
|
|
|
|
size: styleConf.nodeSize || this.nodeSize, |
|
|
size: styleConf.nodeSize || this.nodeSize, |
|
|
fill: fColor, |
|
|
fill: styleConf.nodeFill || this.nodeFill, |
|
|
stroke: styleConf.nodeStroke || this.nodeStroke, |
|
|
stroke: styleConf.nodeStroke || this.nodeStroke, |
|
|
lineWidth: styleConf.nodeLineWidth || this.nodeLineWidth, |
|
|
lineWidth: styleConf.nodeLineWidth || this.nodeLineWidth, |
|
|
label: styleConf.nodeShowLabel !== undefined ? styleConf.nodeShowLabel : true, |
|
|
label: styleConf.nodeShowLabel !== undefined ? styleConf.nodeShowLabel : true, |
|
|
@ -409,27 +363,19 @@ export default { |
|
|
}; |
|
|
}; |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// === 3. 处理边:根据 source 节点的 label 设置样式 === |
|
|
const updatedEdges = data.edges.map(edge => { |
|
|
const updatedEdges = data.edges.map(edge => { |
|
|
const sourceInfo = nodeIdToData[edge.source]; |
|
|
console.log(edge) |
|
|
const styleConf = this.parsedStyles[sourceInfo.enLabel] || {}; |
|
|
const sourceEnLabel = nodeIdToEnLabel[edge.source]; // e.g. "Disease" |
|
|
|
|
|
const styleConf = this.parsedStyles[sourceEnLabel] || {}; |
|
|
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 { |
|
|
return { |
|
|
...edge, |
|
|
...edge, |
|
|
id: edge.data?.relationship?.id || edge.id, |
|
|
id: edge.data?.relationship?.id || edge.id, |
|
|
type: styleConf.edgeType || this.edgeType, |
|
|
type: styleConf.edgeType ||this.edgeType, |
|
|
data: { ...edge.data, label: edge.data?.label || "" }, |
|
|
|
|
|
style: { |
|
|
style: { |
|
|
...edge.style, |
|
|
|
|
|
endArrow: styleConf.edgeEndArrow !== undefined ? styleConf.edgeEndArrow : true, |
|
|
endArrow: styleConf.edgeEndArrow !== undefined ? styleConf.edgeEndArrow : true, |
|
|
stroke: eStroke, |
|
|
stroke: styleConf.edgeStroke || this.edgeStroke, |
|
|
lineWidth: styleConf.edgeLineWidth || this.edgeLineWidth, |
|
|
lineWidth: styleConf.edgeLineWidth || this.edgeLineWidth, |
|
|
label: styleConf.edgeShowLabel !== undefined ? styleConf.edgeShowLabel : false, |
|
|
label: styleConf.edgeShowLabel !== undefined ? styleConf.edgeShowLabel : false, |
|
|
labelFontSize: styleConf.edgeFontSize || this.edgeFontSize, |
|
|
labelFontSize: styleConf.edgeFontSize || this.edgeFontSize, |
|
|
@ -439,68 +385,65 @@ export default { |
|
|
}; |
|
|
}; |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
const finalData = JSON.parse(JSON.stringify({ |
|
|
// === 4. 更新图数据 === |
|
|
|
|
|
let updatedData = { |
|
|
nodes: updatedNodes, |
|
|
nodes: updatedNodes, |
|
|
edges: updatedEdges |
|
|
edges: updatedEdges |
|
|
})); |
|
|
}; |
|
|
|
|
|
this.buildNodeLabelMap(updatedNodes); |
|
|
this.buildNodeLabelMap(finalData.nodes); |
|
|
|
|
|
const container = this.$refs.graphContainer; |
|
|
const container = this.$refs.graphContainer; |
|
|
|
|
|
console.log(container) |
|
|
if (container != null) { |
|
|
if (container!=null){ |
|
|
const width = container.clientWidth || 800; |
|
|
const width = container.clientWidth || 800; |
|
|
const height = container.clientHeight || 600; |
|
|
const height = container.clientHeight || 600; |
|
|
|
|
|
console.log(width) |
|
|
|
|
|
console.log(height) |
|
|
const graph = new Graph({ |
|
|
const graph = new Graph({ |
|
|
container, |
|
|
container, |
|
|
width, |
|
|
width, |
|
|
height, |
|
|
height, |
|
|
plugins: [ |
|
|
|
|
|
{ |
|
|
|
|
|
type: 'toolbar', |
|
|
|
|
|
key: 'g6-toolbar', |
|
|
|
|
|
onClick: (id) => { |
|
|
|
|
|
if (id === 'reset') this.localResetGraph(); |
|
|
|
|
|
else 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: 'undo', value: 'undo', title: '撤销' }, |
|
|
|
|
|
{ id: 'redo', value: 'redo', title: '重做' }, |
|
|
|
|
|
{ id: 'auto-fit', value: 'auto-fit', title: '聚焦' }, |
|
|
|
|
|
{ id: 'reset', value: 'reset', title: '重置' }, |
|
|
|
|
|
{ id: 'export', value: 'export', title: '导出图谱' }, |
|
|
|
|
|
], |
|
|
|
|
|
}, |
|
|
|
|
|
{ type: 'history', key: 'history' }, |
|
|
|
|
|
], |
|
|
|
|
|
layout: { |
|
|
layout: { |
|
|
type: 'force', |
|
|
type: 'force', // 力导向布局 |
|
|
gravity: 0.3, |
|
|
gravity: 0.3, // 重力系数,控制节点聚集程度 |
|
|
repulsion: 500, |
|
|
repulsion: 500, // 排斥力 |
|
|
attraction: 20, |
|
|
attraction: 20, // 吸引力 |
|
|
preventOverlap: true |
|
|
preventOverlap: true // 防止节点重叠 |
|
|
}, |
|
|
|
|
|
behaviors: [ |
|
|
|
|
|
'zoom-canvas', 'drag-element', 'click-select', 'focus-element', |
|
|
}, |
|
|
{ |
|
|
behaviors: [ 'zoom-canvas', 'drag-element', |
|
|
|
|
|
'click-select','focus-element', { |
|
|
type: 'hover-activate', |
|
|
type: 'hover-activate', |
|
|
degree: 1, |
|
|
degree: 1, |
|
|
enable: (e) => e.target && e.target.id && e.action !== 'drag' |
|
|
|
|
|
}, |
|
|
}, |
|
|
{ |
|
|
{ |
|
|
type: 'drag-canvas', |
|
|
type: 'drag-canvas', |
|
|
enable: (event) => event.shiftKey === false, |
|
|
enable: (event) => event.shiftKey === false, |
|
|
}, |
|
|
}, |
|
|
{ type: 'brush-select' }, |
|
|
{ |
|
|
|
|
|
type: 'brush-select', |
|
|
|
|
|
}, |
|
|
], |
|
|
], |
|
|
|
|
|
|
|
|
node: { |
|
|
node: { |
|
|
style: { |
|
|
style: { |
|
|
fill: (d) => d.style?.fill, |
|
|
// fill: (d) => { |
|
|
stroke: (d) => d.style?.stroke, |
|
|
// |
|
|
size: (d) => d.style?.size, |
|
|
// const label = d.data?.type; |
|
|
lineWidth: (d) => d.style?.lineWidth, |
|
|
// if (label === '疾病') return '#EF4444'; // 红 |
|
|
|
|
|
// if (label === '药品'||label === '药物') return '#91cc75'; // 绿 |
|
|
|
|
|
// if (label === '症状') return '#fac858'; // 橙 |
|
|
|
|
|
// if (label === '检查') return '#336eee'; // 橙 |
|
|
|
|
|
// return '#59d1d4'; // 默认灰蓝 |
|
|
|
|
|
// }, |
|
|
|
|
|
// stroke: (d) => { |
|
|
|
|
|
// const label = d.data?.type; |
|
|
|
|
|
// if (label === '疾病') return '#B91C1C'; |
|
|
|
|
|
// if (label === '药品'||label === '药物') return '#047857'; |
|
|
|
|
|
// if (label === '检查') return '#1D4ED8'; // 橙 |
|
|
|
|
|
// if (label === '症状') return '#B45309'; |
|
|
|
|
|
// return '#40999b'; |
|
|
|
|
|
// }, |
|
|
labelText: (d) => d.label, |
|
|
labelText: (d) => d.label, |
|
|
labelPlacement: 'center', |
|
|
labelPlacement: 'center', |
|
|
labelWordWrap: true, |
|
|
labelWordWrap: true, |
|
|
@ -508,37 +451,55 @@ export default { |
|
|
labelMaxLines: 3, |
|
|
labelMaxLines: 3, |
|
|
labelTextOverflow: 'ellipsis', |
|
|
labelTextOverflow: 'ellipsis', |
|
|
labelTextAlign: 'center', |
|
|
labelTextAlign: 'center', |
|
|
labelFill: (d) => d.style?.labelFill, |
|
|
|
|
|
labelFontSize: (d) => d.style?.labelFontSize, |
|
|
|
|
|
labelFontFamily: (d) => d.style?.labelFontFamily, |
|
|
|
|
|
opacity: 1 |
|
|
opacity: 1 |
|
|
}, |
|
|
}, |
|
|
state: { |
|
|
state: { |
|
|
active: { |
|
|
active: { |
|
|
fill: (d) => d.style?.fill, |
|
|
lineWidth: 2, |
|
|
stroke: (d) => d.style?.stroke, |
|
|
shadowColor: '#ffffff', |
|
|
lineWidth: 3, |
|
|
shadowBlur: 10, |
|
|
opacity: 1 |
|
|
opacity: 1 |
|
|
}, |
|
|
}, |
|
|
inactive: { opacity: 0.3 }, |
|
|
inactive: { |
|
|
normal: { |
|
|
opacity: 0.3 |
|
|
fill: (d) => d.style?.fill, |
|
|
}, |
|
|
stroke: (d) => d.style?.stroke, |
|
|
normal:{ |
|
|
opacity: 1 |
|
|
opacity: 1 |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}, |
|
|
}, |
|
|
}, |
|
|
}, |
|
|
|
|
|
|
|
|
edge: { |
|
|
edge: { |
|
|
style: { |
|
|
style: { |
|
|
stroke: (d) => d.style?.stroke, |
|
|
labelText: (d) => { |
|
|
lineWidth: (d) => d.style?.lineWidth, |
|
|
return d.data.label}, |
|
|
endArrow: (d) => d.style?.endArrow, |
|
|
// stroke: (d) => { |
|
|
labelText: (d) => d.data?.label, |
|
|
// const targetLabel = this._nodeLabelMap.get(d.source); // d.target 是目标节点 ID |
|
|
labelFill: (d) => d.style?.labelFill, |
|
|
// if (targetLabel === '疾病') return 'rgba(239,68,68,0.5)'; |
|
|
labelFontSize: (d) => d.style?.labelFontSize, |
|
|
// if (targetLabel === '药品'||targetLabel === '药物') return 'rgba(145,204,117,0.5)'; |
|
|
|
|
|
// if (targetLabel === '症状') return 'rgba(250,200,88,0.5)'; |
|
|
|
|
|
// if (targetLabel === '检查') 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: { |
|
|
state: { |
|
|
selected: { stroke: '#1890FF', lineWidth: 2 }, |
|
|
selected: { |
|
|
|
|
|
stroke: '#1890FF', |
|
|
|
|
|
lineWidth: 2, |
|
|
|
|
|
}, |
|
|
highlight: { |
|
|
highlight: { |
|
|
halo: true, |
|
|
halo: true, |
|
|
haloStroke: '#1890FF', |
|
|
haloStroke: '#1890FF', |
|
|
@ -547,22 +508,36 @@ export default { |
|
|
lineWidth: 3, |
|
|
lineWidth: 3, |
|
|
opacity: 1 |
|
|
opacity: 1 |
|
|
}, |
|
|
}, |
|
|
inactive: { opacity: 0.3 }, |
|
|
inactive: { |
|
|
normal: { opacity: 1 } |
|
|
opacity: 0.3 |
|
|
}, |
|
|
}, |
|
|
|
|
|
normal:{ |
|
|
|
|
|
opacity: 1 |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
}, |
|
|
}, |
|
|
data: finalData, |
|
|
|
|
|
|
|
|
}, |
|
|
|
|
|
data:updatedData, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
graph.render(); |
|
|
graph.render(); |
|
|
this._graph = graph; |
|
|
this._graph = graph |
|
|
this._graph?.fitView(); |
|
|
this._graph?.fitView() |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}, |
|
|
}, |
|
|
|
|
|
|
|
|
}, |
|
|
}, |
|
|
|
|
|
|
|
|
}; |
|
|
}; |
|
|
</script> |
|
|
</script> |
|
|
<style scoped> |
|
|
<style scoped> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.medical-qa-container { |
|
|
.medical-qa-container { |
|
|
padding: 25px; |
|
|
padding: 25px; |
|
|
background-color: #F6F9FF; |
|
|
background-color: #F6F9FF; |
|
|
@ -749,29 +724,4 @@ export default { |
|
|
.dot-2 { animation-delay: -0.2s; } |
|
|
.dot-2 { animation-delay: -0.2s; } |
|
|
.dot-3 { animation-delay: 0s; } |
|
|
.dot-3 { animation-delay: 0s; } |
|
|
.dot-4 { animation-delay: 0.2s; } |
|
|
.dot-4 { animation-delay: 0.2s; } |
|
|
|
|
|
|
|
|
.knowledge-graph { |
|
|
|
|
|
position: relative; /* 必须!否则工具栏会飘走 */ |
|
|
|
|
|
flex: 1; |
|
|
|
|
|
height: 100%; |
|
|
|
|
|
background-color: white; |
|
|
|
|
|
border-radius: 16px; |
|
|
|
|
|
padding: 20px; |
|
|
|
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.05); |
|
|
|
|
|
overflow: hidden; /* 保证工具栏不超出边界 */ |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* 确保图谱容器铺满 */ |
|
|
|
|
|
.graph-container { |
|
|
|
|
|
width: 100%; |
|
|
|
|
|
height: 100%; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
:deep(.custom-graph-toolbar) { |
|
|
|
|
|
position: absolute !important; /* 强制绝对定位 */ |
|
|
|
|
|
top: 30px !important; |
|
|
|
|
|
right: 30px !important; |
|
|
|
|
|
z-index: 9999 !important; /* 确保层级最高 */ |
|
|
|
|
|
display: inline-flex !important; |
|
|
|
|
|
} |
|
|
|
|
|
</style> |
|
|
</style> |