hanyuqing 3 months ago
parent
commit
cf02c16425
  1. 446
      vue/src/system/GraphQA.vue
  2. 42
      vue/src/system/GraphStyle.vue

446
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();
}
}
};
</script>
<style scoped>

42
vue/src/system/GraphStyle.vue

@ -517,7 +517,7 @@ export default {
this.nodeFontColor = s.nodeFontColor;
this.nodeShape = s.nodeShape;
this.nodeSize = s.nodeSize;
this.nodeFill = s.nodeFill;
this.nodeFill = this.convertToRgba(s.nodeFill, 1);
this.nodeStroke = s.nodeStroke;
this.nodeLineWidth = s.nodeLineWidth;
this.edgeShowLabel = s.edgeShowLabel;
@ -527,7 +527,45 @@ export default {
this.edgeFontColor = s.edgeFontColor;
this.edgeType = s.edgeType;
this.edgeLineWidth = s.edgeLineWidth;
this.edgeStroke = s.edgeStroke;
this.edgeStroke = this.convertToRgba(s.edgeStroke, 0.6);
},
convertToRgba(color, defaultOpacity = 1) {
if (!color) return `rgba(182, 178, 178, ${defaultOpacity})`;
const strColor = String(color).trim();
const safeOpacity = (opacity) => {
const n = parseFloat(opacity);
return n >= 1 ? 0.999 : n;
};
// 1. rgba
if (strColor.startsWith('rgba')) {
const parts = strColor.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)/);
if (parts) {
return `rgba(${parts[1]}, ${parts[2]}, ${parts[3]}, ${safeOpacity(parts[4])})`;
}
return strColor;
}
// 2. rgb
if (strColor.startsWith('rgb')) {
return strColor.replace('rgb', 'rgba').replace(')', `, ${safeOpacity(defaultOpacity)})`);
}
// 3. Hex
if (strColor.startsWith('#')) {
let r, g, b;
if (strColor.length === 4) {
r = parseInt(strColor[1] + strColor[1], 16);
g = parseInt(strColor[2] + strColor[2], 16);
b = parseInt(strColor[3] + strColor[3], 16);
} else {
r = parseInt(strColor.slice(1, 3), 16);
g = parseInt(strColor.slice(3, 5), 16);
b = parseInt(strColor.slice(5, 7), 16);
}
return `rgba(${r}, ${g}, ${b}, ${safeOpacity(defaultOpacity)})`;
}
return strColor;
},
// 退

Loading…
Cancel
Save