From cf02c164250d8dad9c61e7c49a71871dc777c1b3 Mon Sep 17 00:00:00 2001 From: hanyuqing <1106611654@qq.com> Date: Thu, 15 Jan 2026 16:53:17 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8F=98=E8=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vue/src/system/GraphQA.vue | 446 +++++++++++++----------------------------- vue/src/system/GraphStyle.vue | 42 +++- 2 files changed, 180 insertions(+), 308 deletions(-) diff --git a/vue/src/system/GraphQA.vue b/vue/src/system/GraphQA.vue index 82c90f4..183255a 100644 --- a/vue/src/system/GraphQA.vue +++ b/vue/src/system/GraphQA.vue @@ -94,7 +94,7 @@ export default { query:"", answers:[], selected:0, - // 节点样式 + // 默认节点样式 nodeShowLabel: true, nodeFontSize: 12, nodeFontColor: '#fff', @@ -105,7 +105,7 @@ export default { nodeLineWidth: 2, nodeFontFamily: 'Microsoft YaHei, sans-serif', - // 边样式 + // 默认边样式 edgeShowLabel: true, edgeFontSize: 10, edgeFontColor: '#666666', @@ -115,7 +115,6 @@ export default { edgeEndArrow: true, edgeFontFamily: 'Microsoft YaHei, sans-serif', - queryRecord:"", isSending:false, configs:[], @@ -127,41 +126,37 @@ export default { Symptom: '症状', Other: '其他' } - }; }, + // =============== 👇【新增】组件内路由守卫:离开当前路由时触发 =============== beforeRouteLeave(to, from, next) { this.saveDataToLocalStorage(); - next(); // 允许导航 + next();// 允许导航 + }, - // ======================================================================= async mounted() { - await this.getDefault() + await this.getDefault(); // =============== 👇【新增】页面加载时从 localStorage 恢复数据 =============== this.restoreDataFromLocalStorage(); - // ======================================================================= - // this.answers=[] - // 如果有初始数据,可以初始化图谱(可选) if (this.answers.length > 0) { - this.initGraph(this.answers[0].result); - // console.log(this.answers[0].result) + this.initGraph(this.answers[this.selected].result); } }, + beforeUnmount() { // =============== 👇【新增】组件销毁前也保存一次(兼容非路由跳转场景)============== this.saveDataToLocalStorage(); // 移除页面卸载监听(如果用了 beforeunload) window.removeEventListener('beforeunload', this.handleBeforeUnload); - // ======================================================================= }, created() { // =============== 👇【新增】监听页面刷新/关闭事件 =============== window.addEventListener('beforeunload', this.handleBeforeUnload); - // ======================================================================= }, + methods: { safeParseStyles(stylesStr) { try { @@ -171,6 +166,7 @@ export default { return {}; } }, + async getDefault(){ const response = await getGraphStyleActive(); const data = response.data; @@ -179,7 +175,6 @@ export default { this.parsedStyles = {}; return; } - // 只取第一个(即 is_active=1 的组) const activeGroup = data[0]; this.configs = Array.isArray(activeGroup.configs) ? activeGroup.configs : []; @@ -187,91 +182,76 @@ export default { // 构建 label -> style 映射 const styleMap = {}; this.configs.forEach(config => { + // 重要:这里的 current_label 通常是中文(疾病、药品) const label = config.current_label; styleMap[label] = this.safeParseStyles(config.styles); }); this.parsedStyles = styleMap; - console.log(this.parsedStyles) }, + buildNodeLabelMap(nodes) { this._nodeLabelMap = new Map(); nodes.forEach(node => { this._nodeLabelMap.set(node.id, node.data?.type || 'default'); }); }, -// =============== 👇【新增】统一的数据保存方法 =============== + + // =============== 👇【新增】统一的数据保存方法 =============== saveDataToLocalStorage() { try { localStorage.setItem('graphQA_queryRecord', this.queryRecord); localStorage.setItem('graphQA_answers', JSON.stringify(this.answers)); - console.log('✅ 数据已保存到 localStorage'); } catch (e) { console.warn('⚠️ 无法保存到 localStorage:', e); } }, - // ======================================================================= - // =============== 👇【新增】统一的数据恢复方法 =============== restoreDataFromLocalStorage() { try { const savedQuery = localStorage.getItem('graphQA_queryRecord'); const savedAnswers = localStorage.getItem('graphQA_answers'); - - if (savedQuery !== null) { - this.queryRecord = savedQuery; - } + if (savedQuery !== null) this.queryRecord = savedQuery; if (savedAnswers !== null) { this.answers = JSON.parse(savedAnswers); - // 确保 selected 不越界 if (this.answers.length > 0) { this.selected = Math.min(this.selected, this.answers.length - 1); } } - console.log('✅ 数据已从 localStorage 恢复'); } catch (e) { console.warn('⚠️ 无法从 localStorage 恢复数据:', e); - // 出错时清空(避免脏数据) localStorage.removeItem('graphQA_queryRecord'); localStorage.removeItem('graphQA_answers'); } }, - // ======================================================================= // =============== 👇【新增】处理页面关闭/刷新的兜底保存 =============== handleBeforeUnload(event) { this.saveDataToLocalStorage(); - // 注意:现代浏览器通常不显示自定义消息 event.preventDefault(); - event.returnValue = ''; // 必须设置才能触发提示(但实际可能不显示) + event.returnValue = '';// 必须设置才能触发提示(但实际可能不显示) }, + selectGraph(index){ - this.selected=index - if(this.answers.length>0){ - this.formatData(this.answers[index].result) + this.selected = index; + if(this.answers.length > 0){ + 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 = "" - // 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) + 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 + this.answers = res; if (this.answers.length > 0) { - this.initGraph(this.answers[0].result) + this.initGraph(this.answers[0].result); } - this.isSending = false + this.isSending = false; }).catch(err => { console.error('接口失败,启动保底方案', err); const mockData = { @@ -296,36 +276,36 @@ export default { this.isSending = false; }); }, - formatData(data){ - // this._graph.stopLayout(); - // this.clearGraphState(); - const typeMap = { '疾病': 'Disease', '药品': 'Drug', '药物': 'Drug', '症状': 'Symptom', '检查': 'Check', '病因': 'Cause' }; - const getStandardLabel = (rawType) => typeMap[rawType] || rawType; + // 抽离统一的样式处理函数,供 initGraph 和 formatData 使用 + processGraphData(data) { + // 1. 获取节点类型映射 const nodeIdToData = {}; data.nodes.forEach(node => { - nodeIdToData[node.id] = { - enLabel: getStandardLabel(node.data.type), - rawType: node.data.type - }; + nodeIdToData[node.id] = { rawType: node.data.type || '其他' }; }); - // === 2. 处理节点:根据自身 label 设置样式 === + + // 2. 处理节点 const updatedNodes = data.nodes.map(node => { - const enLabel = getStandardLabel(node.data.type); - const styleConf = this.parsedStyles[enLabel] || {}; - // 💡 颜色映射逻辑:如果 styleConf 没给颜色,则按类型分配 + const zhLabel = node.data.type || '其他'; + // 直接用中文 Label 去匹配样式配置 + const styleConf = this.parsedStyles[zhLabel] || {}; + + // 颜色优先级:配置 > 硬编码 > 默认 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'; + if (zhLabel === '疾病') fColor = '#EF4444'; + else if (zhLabel === '药品' || zhLabel === '药物') fColor = '#91cc75'; + else if (zhLabel === '症状') fColor = '#fac858'; + else if (zhLabel === '检查') fColor = '#336eee'; else fColor = this.nodeFill; } + return { ...node, type: styleConf.nodeShape || this.nodeShape, - data: { ...node.data, label: enLabel, name: node.label }, + // 保持 data 原样,但确保 G6 渲染需要的字段 + data: { ...node.data, name: node.label }, style: { ...node.style, size: styleConf.nodeSize || this.nodeSize, @@ -335,17 +315,17 @@ export default { label: styleConf.nodeShowLabel !== undefined ? styleConf.nodeShowLabel : true, labelFontSize: styleConf.nodeFontSize || this.nodeFontSize, labelFontFamily: styleConf.nodeFontFamily || this.nodeFontFamily, - labelFill: styleConf.nodeFontColor || this.nodeFontColor + labelFill: styleConf.nodeFontColor || this.nodeFontColor, + labelText: node.label // 显式传入文本防止丢失 } }; }); - // === 3. 处理边:根据 source 节点的 label 设置样式 === + // 3. 处理边 const updatedEdges = data.edges.map(edge => { - const sourceInfo = nodeIdToData[edge.source]; - const styleConf = this.parsedStyles[sourceInfo.enLabel] || {}; + const sourceInfo = nodeIdToData[edge.source] || { rawType: '其他' }; + const styleConf = this.parsedStyles[sourceInfo.rawType] || {}; - // 💡 边颜色逻辑:跟随源节点类型 let eStroke = styleConf.edgeStroke; if (!eStroke) { if (sourceInfo.rawType === '疾病') eStroke = 'rgba(239, 68, 68, 0.4)'; @@ -357,267 +337,121 @@ export default { return { ...edge, id: edge.data?.relationship?.id || edge.id, - type: styleConf.edgeType ||this.edgeType, - data: { ...edge.data, label: edge.data?.label || "" }, + type: styleConf.edgeType || this.edgeType, 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, labelFontFamily: styleConf.edgeFontFamily || this.edgeFontFamily, - labelFill: styleConf.edgeFontColor || this.edgeFontColor + labelFill: styleConf.edgeFontColor || this.edgeFontColor, + labelText: edge.data?.label || "" } }; }); - // === 4. 更新图数据 === - const pureData = JSON.parse(JSON.stringify({ nodes: updatedNodes, edges: updatedEdges })); - this.updateGraph(pureData); + return { nodes: updatedNodes, edges: updatedEdges }; + }, + + formatData(data){ + const processed = this.processGraphData(data); + this.buildNodeLabelMap(processed.nodes); + this.updateGraph(processed); }, + updateGraph(data) { 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() + if (this._graph != null){ + this._graph.destroy(); this._graph = null; } - // === 1. 构建 nodeId → label 映射 === - const nodeIdToData = {}; - data.nodes.forEach(node => { - nodeIdToData[node.id] = { - enLabel: getStandardLabel(node.data.type), - rawType: node.data.type - }; - }); - // === 2. 处理节点:根据自身 label 设置样式 === - const updatedNodes = data.nodes.map(node => { - 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: fColor, - 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 => { - 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; - } + const updatedData = this.processGraphData(data); + this.buildNodeLabelMap(updatedData.nodes); - 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: eStroke, - 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); 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, - 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: '导出图谱' }, - ], + if (!container) return; + + const width = container.clientWidth || 800; + const height = container.clientHeight || 600; + + const graph = new Graph({ + container, + width, + height, + plugins: [ + { + type: 'toolbar', + key: 'g6-toolbar', + onClick: (id) => { + if (this.$refs.toolbarRef) this.$refs.toolbarRef.handleToolbarAction(id); }, - ], - layout: { - type: 'force', // 力导向布局 - gravity: 0.3, // 重力系数,控制节点聚集程度 - repulsion: 500, // 排斥力 - attraction: 20, // 吸引力 - preventOverlap: true // 防止节点重叠 + 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: '导出图谱' }, + ], }, - 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?.type; - // 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, - 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 - } - - - }, + ], + 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: { + labelPlacement: 'center', + labelWordWrap: true, + labelMaxWidth: '150%', + labelMaxLines: 3, + labelTextOverflow: 'ellipsis', + labelTextAlign: 'center', + opacity: 1 }, - - edge: { - style: { - labelText: (d) => { - return d.data.label}, - // stroke: (d) => { - // const targetLabel = this._nodeLabelMap.get(d.source); // d.target 是目标节点 ID - // if (targetLabel === '疾病') return 'rgba(239,68,68,0.5)'; - // 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: { - 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 - } - + state: { + active: { lineWidth: 2, shadowColor: '#ffffff', shadowBlur: 10, opacity: 1 }, + inactive: { opacity: 0.3 }, + normal: { opacity: 1 } + }, + }, + edge: { + style: { + // 默认配置会在 updatedData 的 style 里被覆盖 + }, + 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() - } - - - }, - - }, + }, + data: updatedData, + }); + graph.render(); + this._graph = graph; + this._graph.fitView(); + } + } };