Browse Source

Merge branch 'mh' of http://124.70.32.114:3100/hanyuqing/KGPython into hanyuqing

# Conflicts:
#	vue/src/system/GraphDemo.vue
#	vue/src/system/GraphStyle.vue
hanyuqing
hanyuqing 3 months ago
parent
commit
7296db1d76
  1. 125
      vue/src/components/GraphToolbar.vue
  2. 229
      vue/src/system/GraphDemo.vue
  3. 403
      vue/src/system/GraphStyle.vue

125
vue/src/components/GraphToolbar.vue

@ -127,9 +127,14 @@ export default {
}, },
exportToSVGManual() { exportToSVGManual() {
const graph = this.graph; const graph = this._graph || this.graph;
if (!graph) return; if (!graph) {
this.$message.error("未找到图表实例");
return;
}
const { nodes, edges } = graph.getData(); const { nodes, edges } = graph.getData();
// 1.
const bBox = graph.getCanvas().getRoot().getRenderBounds(); const bBox = graph.getCanvas().getRoot().getRenderBounds();
const padding = 60; const padding = 60;
const minX = bBox.min[0] - padding; const minX = bBox.min[0] - padding;
@ -139,7 +144,6 @@ export default {
const vWidth = maxX - minX; const vWidth = maxX - minX;
const vHeight = maxY - minY; const vHeight = maxY - minY;
// 1.
const colorMap = { const colorMap = {
'Disease': { fill: '#EF4444', stroke: '#B91C1C', edge: '#EF4444' }, 'Disease': { fill: '#EF4444', stroke: '#B91C1C', edge: '#EF4444' },
'Drug': { fill: '#91cc75', stroke: '#047857', edge: '#91cc75' }, 'Drug': { fill: '#91cc75', stroke: '#047857', edge: '#91cc75' },
@ -148,39 +152,101 @@ export default {
'Other': { fill: '#59d1d4', stroke: '#40999b', edge: '#59d1d4' } 'Other': { fill: '#59d1d4', stroke: '#40999b', edge: '#59d1d4' }
}; };
// 2.
const usedColors = new Set();
edges.forEach(edge => {
const s = nodes.find(n => n.id === edge.source);
const sourceType = s?.data?.label || 'Other';
const color = edge.style?.stroke || colorMap[sourceType]?.edge || '#cccccc';
usedColors.add(color);
});
let svgContent = `<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="${vWidth}" height="${vHeight}" viewBox="${minX} ${minY} ${vWidth} ${vHeight}">`; let svgContent = `<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="${vWidth}" height="${vHeight}" viewBox="${minX} ${minY} ${vWidth} ${vHeight}">`;
// 3. Marker
svgContent += `<defs>`;
const colorList = Array.from(usedColors);
colorList.forEach((color, index) => {
svgContent += `
<marker id="arrow-${index}" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="${color}" />
</marker>`;
});
svgContent += `</defs>`;
svgContent += `<rect x="${minX}" y="${minY}" width="${vWidth}" height="${vHeight}" fill="#ffffff" />`; svgContent += `<rect x="${minX}" y="${minY}" width="${vWidth}" height="${vHeight}" fill="#ffffff" />`;
// 2. // 4.
edges.forEach(edge => { edges.forEach(edge => {
const s = nodes.find(n => n.id === edge.source); const s = nodes.find(n => n.id === edge.source);
const t = nodes.find(n => n.id === edge.target); const t = nodes.find(n => n.id === edge.target);
if (!s || !t) return; if (!s || !t) return;
// ( style G6 ) const x1 = s.style?.x || 0, y1 = s.style?.y || 0;
const x1 = s.style?.x || 0; const x2 = t.style?.x || 0, y2 = t.style?.y || 0;
const y1 = s.style?.y || 0; const nodeRadius = (t.style?.size || 50) / 2;
const x2 = t.style?.x || 0;
const y2 = t.style?.y || 0;
// --- --- const sourceType = s.data?.label || 'Other';
// edge: { style: { stroke: (d) => ... } } const strokeColor = edge.style?.stroke || colorMap[sourceType]?.edge || '#cccccc';
const sourceType = s.data?.label || 'Other'; // (Disease/Drug) const markerId = `arrow-${colorList.indexOf(strokeColor)}`;
const strokeColor = colorMap[sourceType]?.edge || '#cccccc';
let pathD = "";
let tx, ty;
const dx = x2 - x1, dy = y2 - y1;
const dist = Math.sqrt(dx * dx + dy * dy);
const nx = -dy / dist, ny = dx / dist;
const curveOffset = edge.style?.curveOffset || 40;
// --- 线 ---
if (edge.type === 'cubic' && dist > 0) {
//
const cp1X = x1 + dx / 3 + nx * curveOffset;
const cp1Y = y1 + dy / 3 + ny * curveOffset;
const cp2X = x1 + (dx * 2) / 3 + nx * curveOffset;
const cp2Y = y1 + (dy * 2) / 3 + ny * curveOffset;
const angle = Math.atan2(y2 - cp2Y, x2 - cp2X);
const realX2 = x2 - Math.cos(angle) * (nodeRadius + 1);
const realY2 = y2 - Math.sin(angle) * (nodeRadius + 1);
pathD = `M ${x1} ${y1} C ${cp1X} ${cp1Y} ${cp2X} ${cp2Y} ${realX2} ${realY2}`;
tx = (x1 + cp1X + cp2X + x2) / 4;
ty = (y1 + cp1Y + cp2Y + y2) / 4;
} else if (edge.type === 'quadratic' && dist > 0) {
//
const cpX = (x1 + x2) / 2 + nx * curveOffset;
const cpY = (y1 + y2) / 2 + ny * curveOffset;
const angle = Math.atan2(y2 - cpY, x2 - cpX);
const realX2 = x2 - Math.cos(angle) * (nodeRadius + 1);
const realY2 = y2 - Math.sin(angle) * (nodeRadius + 1);
pathD = `M ${x1} ${y1} Q ${cpX} ${cpY} ${realX2} ${realY2}`;
tx = (x1 + cpX * 2 + x2) / 4;
ty = (y1 + cpY * 2 + y2) / 4;
} else {
// 线
const angle = Math.atan2(y2 - y1, x2 - x1);
const realX2 = x2 - Math.cos(angle) * nodeRadius;
const realY2 = y2 - Math.sin(angle) * nodeRadius;
pathD = `M ${x1} ${y1} L ${realX2} ${realY2}`;
tx = (x1 + x2) / 2;
ty = (y1 + y2) / 2;
}
// 线 ( stroke-width ) svgContent += `<path d="${pathD}" stroke="${strokeColor}" stroke-width="2" fill="none" opacity="0.6" marker-end="url(#${markerId})" />`;
svgContent += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${strokeColor}" stroke-width="2" opacity="0.4" />`;
// //
const labelText = edge.data?.relationship?.properties?.label || edge.data?.label || ""; const labelText = edge.data?.label || edge.label || "";
if (labelText) { if (labelText) {
const mx = (x1 + x2) / 2; svgContent += `<text x="${tx}" y="${ty - 8}" fill="#666666" font-size="10" font-family="Microsoft YaHei" text-anchor="middle">${labelText}</text>`;
const my = (y1 + y2) / 2;
svgContent += `<text x="${mx}" y="${my - 4}" fill="#666666" font-size="10" font-family="Microsoft YaHei" text-anchor="middle">${labelText}</text>`;
} }
}); });
// 3. // 5.
nodes.forEach(node => { nodes.forEach(node => {
const type = node.data?.label || 'Other'; const type = node.data?.label || 'Other';
const colors = colorMap[type] || colorMap['Other']; const colors = colorMap[type] || colorMap['Other'];
@ -188,25 +254,28 @@ export default {
const x = node.style?.x || 0; const x = node.style?.x || 0;
const y = node.style?.y || 0; const y = node.style?.y || 0;
svgContent += `<circle cx="${x}" cy="${y}" r="${radius}" fill="${colors.fill}" stroke="${colors.stroke}" stroke-width="1.5" />`; svgContent += `<circle cx="${x}" cy="${y}" r="${radius}" fill="${node.style?.fill || colors.fill}" stroke="${node.style?.stroke || colors.stroke}" stroke-width="1.5" />`;
if (node.data?.name) { const name = node.label || node.data?.name || "";
svgContent += `<text x="${x}" y="${y}" fill="#ffffff" font-size="12" font-family="Microsoft YaHei" text-anchor="middle" dominant-baseline="middle">${node.data.name}</text>`; if (name) {
svgContent += `<text x="${x}" y="${y}" fill="#ffffff" font-size="11" font-family="Microsoft YaHei" text-anchor="middle" dominant-baseline="middle">${name}</text>`;
} }
}); });
svgContent += `</svg>`; svgContent += `</svg>`;
// ... // 6.
try { try {
const blob = new Blob([svgContent], {type: 'image/svg+xml;charset=utf-8'}); const blob = new Blob([svgContent], { type: 'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const link = document.createElement('a'); const link = document.createElement('a');
link.href = url; link.href = url;
link.download = `矢量图谱_${Date.now()}.svg`; link.download = `医疗图谱导出_${Date.now()}.svg`;
document.body.appendChild(link);
link.click(); link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
this.$message.success('全量矢量图导出成功'); this.$message.success('高精度矢量图导出成功');
} catch (err) { } catch (err) {
this.$message.error('导出失败'); this.$message.error('导出失败');
} }

229
vue/src/system/GraphDemo.vue

@ -134,6 +134,13 @@
</div> </div>
</div> </div>
<div class="graph-viewport"> <div class="graph-viewport">
<GraphToolbar
ref="toolbarRef"
v-if="_graph"
:graph="_graph"
class="toolbar-position"
style="position: absolute; top: 40px; left: 750px; z-index: 1000; width: auto;"
/>
<div ref="graphContainer" class="graph-container" id="container"></div> <div ref="graphContainer" class="graph-container" id="container"></div>
</div> </div>
</section> </section>
@ -155,14 +162,18 @@ import {
import {Graph, Tooltip} from '@antv/g6'; import {Graph, Tooltip} from '@antv/g6';
import Menu from "@/components/Menu.vue"; import Menu from "@/components/Menu.vue";
import {a} from "vue-router/dist/devtools-EWN81iOl.mjs"; import {a} from "vue-router/dist/devtools-EWN81iOl.mjs";
import GraphToolbar from "@/components/GraphToolbar.vue";
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
import {ElMessage} from "element-plus";
import {getGraphStyleActive} from "@/api/style"; import {getGraphStyleActive} from "@/api/style";
export default { export default {
name: 'Display', name: 'Display',
components: {Menu}, components: {Menu, GraphToolbar},
data() { data() {
return { return {
_graph: null,
G6: null, // G6: null, //
// //
nodeShowLabel: true, nodeShowLabel: true,
@ -201,41 +212,41 @@ export default {
children: 'children', children: 'children',
label: 'title' // el-tree label: 'title' // el-tree
}, },
typeRadio:"Disease", typeRadio: "Disease",
DiseaseRadio:"ICD10", DiseaseRadio: "ICD10",
drugTree:[], drugTree: [],
diseaseDepartTree:[], diseaseDepartTree: [],
diseaseICD10Tree:[], diseaseICD10Tree: [],
diseaseSZMTree:[], diseaseSZMTree: [],
checkTree:[], checkTree: [],
legendItems: [ legendItems: [
{ key: 'Disease', label: '疾病', color: '#EF4444' }, {key: 'Disease', label: '疾病', color: '#EF4444'},
{ key: 'Drug', label: '药品', color: '#91cc75' }, {key: 'Drug', label: '药品', color: '#91cc75'},
{ key: 'Check', label: '检查', color: '#336eee' }, {key: 'Check', label: '检查', color: '#336eee'},
{ key: 'Symptom', label: '症状', color: '#fac858' }, {key: 'Symptom', label: '症状', color: '#fac858'},
{ key: 'Other', label: '其他', color: '#59d1d4' } {key: 'Other', label: '其他', color: '#59d1d4'}
], ],
visibleCategories: new Set(), // visibleCategories: new Set(), //
diseaseCount:0, diseaseCount: 0,
drugCount:0, drugCount: 0,
checkCount:0, checkCount: 0,
originalNodeStyles: new Map(), // originalNodeStyles: new Map(), //
originalEdgeStyles: new Map(), // originalEdgeStyles: new Map(), //
searchResults: { searchResults: {
nodes: [], nodes: [],
edges: [] edges: []
}, },
searchKeyword:"", searchKeyword: "",
drugSubjectTree:[], drugSubjectTree: [],
DrugRadio:"Subject", DrugRadio: "Subject",
isDropdownOpen: false, isDropdownOpen: false,
typeOptions: [ typeOptions: [
{ value: 'Disease', label: '疾病' }, {value: 'Disease', label: '疾病'},
{ value: 'Drug', label: '药品' }, {value: 'Drug', label: '药品'},
{ value: 'Check', label: '检查' } {value: 'Check', label: '检查'}
], ],
configs:[], configs: [],
parsedStyles:{}, parsedStyles: {},
enToZhLabelMap: { enToZhLabelMap: {
Disease: '疾病', Disease: '疾病',
Drug: '药品', Drug: '药品',
@ -314,7 +325,7 @@ export default {
this.loadDrugTreeData() this.loadDrugTreeData()
this.loadCheckTreeData() this.loadCheckTreeData()
this.loadDrugSubjectTreeData() this.loadDrugSubjectTreeData()
this.treeData=this.diseaseICD10Tree this.treeData = this.diseaseICD10Tree
await this.$nextTick(); await this.$nextTick();
try { try {
await this.getDefault() await this.getDefault()
@ -356,7 +367,7 @@ export default {
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: {
endArrow: styleConf.edgeEndArrow !== undefined ? styleConf.edgeEndArrow : true, endArrow: styleConf.edgeEndArrow !== undefined ? styleConf.edgeEndArrow : true,
stroke: styleConf.edgeStroke || this.edgeStroke, stroke: styleConf.edgeStroke || this.edgeStroke,
@ -432,7 +443,7 @@ export default {
}, },
beforeUnmount() { beforeUnmount() {
if (this._graph!=null){ if (this._graph != null) {
this._graph.stopLayout(); this._graph.stopLayout();
this.clearGraphState(); this.clearGraphState();
this._graph.destroy() this._graph.destroy()
@ -471,7 +482,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) {
@ -639,7 +650,7 @@ export default {
// 4 // 4
const firstMatch = nodeIds[0] || edgeIds[0]; const firstMatch = nodeIds[0] || edgeIds[0];
if (firstMatch && (this._graph.hasNode(firstMatch) || this._graph.hasEdge(firstMatch))) { if (firstMatch && (this._graph.hasNode(firstMatch) || this._graph.hasEdge(firstMatch))) {
this._graph.focusElement(firstMatch, { animation: true, duration: 600 }); this._graph.focusElement(firstMatch, {animation: true, duration: 600});
} }
}, },
clearSearchHighlight() { clearSearchHighlight() {
@ -653,26 +664,26 @@ export default {
this._graph.setElementState(id, 'normal', true); this._graph.setElementState(id, 'normal', true);
}); });
}, },
getCount(){ getCount() {
getCount().then(res=>{ getCount().then(res => {
this.diseaseCount=res.Disease this.diseaseCount = res.Disease
this.drugCount=res.Drug this.drugCount = res.Drug
this.checkCount=res.Check this.checkCount = res.Check
console.log(res) console.log(res)
}) })
}, },
buildCategoryIndex() { buildCategoryIndex() {
const index = {}; const index = {};
if (this._graph!=null){ if (this._graph != null) {
const nodes = this._graph.getNodeData() // const nodes = this._graph.getNodeData() //
nodes.forEach(node => { nodes.forEach(node => {
console.log(node.data.label) console.log(node.data.label)
const category = node.data.label; // label const category = node.data.label; // label
if(category=='Drug'||category=='Symptom'|| if (category == 'Drug' || category == 'Symptom' ||
category=='Disease'||category=='Check'){ category == 'Disease' || category == 'Check') {
if (!index[category]) index[category] = []; if (!index[category]) index[category] = [];
index[category].push(node.id); index[category].push(node.id);
}else{ } else {
if (!index["Other"]) index["Other"] = []; if (!index["Other"]) index["Other"] = [];
index["Other"].push(node.id); index["Other"].push(node.id);
} }
@ -682,7 +693,7 @@ export default {
}, },
// //
toggleCategory (key){ toggleCategory(key) {
if (this.visibleCategories.has(key)) { if (this.visibleCategories.has(key)) {
this.visibleCategories.delete(key) this.visibleCategories.delete(key)
} else { } else {
@ -737,28 +748,28 @@ export default {
} }
}, },
changeTree(){ changeTree() {
if(this.typeRadio=="Disease"){ if (this.typeRadio == "Disease") {
if(this.DiseaseRadio=="ICD10") { if (this.DiseaseRadio == "ICD10") {
this.treeData=this.diseaseICD10Tree this.treeData = this.diseaseICD10Tree
} }
if(this.DiseaseRadio=="Department") { if (this.DiseaseRadio == "Department") {
this.treeData=this.diseaseDepartTree this.treeData = this.diseaseDepartTree
} }
if(this.DiseaseRadio=="SZM") { if (this.DiseaseRadio == "SZM") {
this.treeData=this.diseaseSZMTree this.treeData = this.diseaseSZMTree
} }
} }
if(this.typeRadio=="Drug") { if (this.typeRadio == "Drug") {
if(this.DrugRadio=="Subject") { if (this.DrugRadio == "Subject") {
this.treeData=this.drugSubjectTree this.treeData = this.drugSubjectTree
} }
if(this.DrugRadio=="SZM") { if (this.DrugRadio == "SZM") {
this.treeData=this.drugTree this.treeData = this.drugTree
} }
} }
if(this.typeRadio=="Check") { if (this.typeRadio == "Check") {
this.treeData=this.checkTree this.treeData = this.checkTree
} }
}, },
async loadDiseaseICD10TreeData() { async loadDiseaseICD10TreeData() {
@ -818,15 +829,15 @@ export default {
const response = await getGraph(data); // Promise const response = await getGraph(data); // Promise
this.formatData(response) this.formatData(response)
} }
if(data.level=="category"|| if (data.level == "category" ||
data.level=="subcategory"|| data.level == "subcategory" ||
data.level=="diagnosis"){ data.level == "diagnosis") {
data.type="Disease" data.type = "Disease"
const response = await getGraph(data); // Promise const response = await getGraph(data); // Promise
this.formatData(response) this.formatData(response)
} }
if(data.type === "Disease"){ if (data.type === "Disease") {
data.type="Disease" data.type = "Disease"
const response = await getGraph(data); // Promise const response = await getGraph(data); // Promise
this.formatData(response) this.formatData(response)
} }
@ -842,17 +853,17 @@ export default {
// 1. // 1.
this._graph.getNodeData().forEach(node => { this._graph.getNodeData().forEach(node => {
this._graph.setElementState(node.id,[]); this._graph.setElementState(node.id, []);
}); });
this._graph.getEdgeData().forEach(edge => { this._graph.getEdgeData().forEach(edge => {
this._graph.setElementState(edge.id,[]); this._graph.setElementState(edge.id, []);
}); });
// 2. pending // 2. pending
// clearTimeout // clearTimeout
}, },
formatData(data){ formatData(data) {
this._graph.stopLayout(); this._graph.stopLayout();
this.clearGraphState(); this.clearGraphState();
@ -893,7 +904,7 @@ export default {
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: {
endArrow: styleConf.edgeEndArrow !== undefined ? styleConf.edgeEndArrow : true, endArrow: styleConf.edgeEndArrow !== undefined ? styleConf.edgeEndArrow : true,
stroke: styleConf.edgeStroke || this.edgeStroke, stroke: styleConf.edgeStroke || this.edgeStroke,
@ -939,7 +950,7 @@ export default {
}, },
initGraph() { initGraph() {
if (this._graph!=null){ if (this._graph != null) {
this._graph.destroy() this._graph.destroy()
this._graph = null; this._graph = null;
} }
@ -950,7 +961,7 @@ export default {
console.log(this._nodeLabelMap) console.log(this._nodeLabelMap)
const container = this.$refs.graphContainer; const container = this.$refs.graphContainer;
if (!container) return; if (!container) return;
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(width)
@ -990,8 +1001,8 @@ export default {
easing: 'ease-in-out', // easing: 'ease-in-out', //
}, },
}, },
behaviors: [ 'zoom-canvas', 'drag-element', behaviors: ['zoom-canvas', 'drag-element',
'click-select','focus-element', { 'click-select', 'focus-element', {
type: 'hover-activate', type: 'hover-activate',
degree: 1, degree: 1,
}, },
@ -1038,7 +1049,7 @@ export default {
shadowBlur: 10, shadowBlur: 10,
opacity: 1 opacity: 1
}, },
highlight:{ highlight: {
stroke: '#FF5722', stroke: '#FF5722',
lineWidth: 4, lineWidth: 4,
opacity: 1 opacity: 1
@ -1046,7 +1057,7 @@ export default {
inactive: { inactive: {
opacity: 0.8 opacity: 0.8
}, },
normal:{ normal: {
opacity: 1 opacity: 1
} }
@ -1094,14 +1105,14 @@ export default {
inactive: { inactive: {
opacity: 0.8 opacity: 0.8
}, },
normal:{ normal: {
opacity: 1 opacity: 1
} }
}, },
}, },
data:this.defaultData, data: this.defaultData,
}); });
this.$nextTick(() => { this.$nextTick(() => {
@ -1159,10 +1170,10 @@ export default {
// }); // });
graph.on('node:click', (evt) => { graph.on('node:click', (evt) => {
const nodeItem = evt.target.id; // const nodeItem = evt.target.id; //
let node=graph.getNodeData(nodeItem).data let node = graph.getNodeData(nodeItem).data
let data={ let data = {
label:node.name, label: node.name,
type:node.label type: node.label
} }
getGraph(data).then(response=>{ getGraph(data).then(response=>{
console.log(response) console.log(response)
@ -1195,16 +1206,73 @@ export default {
const sourceName = sourceNode?.data.name || sourceId; const sourceName = sourceNode?.data.name || sourceId;
const targetName = targetNode?.data.name || targetId; const targetName = targetNode?.data.name || targetId;
const rel = data.relationship.properties.label || '关联'; const rel = data.relationship.properties.label || '关联';
return `<div style="padding: 4px 8px; color: #b6b2b2; border-radius: 4px; font-size: 12px;"> return `<div style="padding: 4px 8px; color: #b6b2b2; border-radius: 4px; font-size: 12px;">
${sourceName} ${rel} > ${targetName} ${sourceName} ${rel} > ${targetName}
</div>`; </div>`;
}, },
},]) },
{
type: 'toolbar',
onClick: (id) => {
if (id === 'reset') {
// 1.
this.localResetGraph();
} else if (this.$refs.toolbarRef) {
// 2.
this.$refs.toolbarRef.handleToolbarAction(id);
}
},
getItems: () => {
return [
{id: 'zoom-in', value: 'zoom-in', title: '放大'},
{id: 'zoom-out', value: 'zoom-out', title: '缩小'},
{id: 'auto-fit', value: 'auto-fit', title: '聚焦'},
{id: 'reset', value: 'reset', title: '重置'},
{id: 'export', value: 'export', title: '导出图谱'},
];
},
},
])
} }
},
localResetGraph() {
// 1.
if (!this._graph) return;
if (!this.defaultData || !this.defaultData.nodes) {
this.$message.warning("未找到可重置的数据源");
return;
}
try {
// 2.
const canvas = this._graph.getCanvas();
if (canvas && typeof canvas.setCursor === 'function') {
canvas.setCursor('default');
}
// 3.
// DOM EventBoundary
this._graph.destroy();
this._graph = null;
// 4.
this.$nextTick(() => {
// initGraph this.defaultData
this.initGraph();
// 5. destroy
this.buildCategoryIndex();
this.$message.success("图谱已重置");
});
} catch (err) {
console.error('重置图谱失败:', err);
//
// location.reload();
}
}, },
updateGraph(data) { updateGraph(data) {
@ -1218,7 +1286,7 @@ export default {
const updatedNodes = this.defaultData.nodes.map(node => ({ const updatedNodes = this.defaultData.nodes.map(node => ({
...node, ...node,
type: this.nodeShape, type: this.nodeShape,
style:{ style: {
size: this.nodeSize, size: this.nodeSize,
fill: this.nodeFill, fill: this.nodeFill,
stroke: this.nodeStroke, stroke: this.nodeStroke,
@ -1645,6 +1713,7 @@ button:hover {
/deep/ .radio-check .el-radio__input.is-checked+.el-radio__label { /deep/ .radio-check .el-radio__input.is-checked+.el-radio__label {
color: #1890ff; color: #1890ff;
} }
/* 自定义下拉样式 */ /* 自定义下拉样式 */
.select-container { .select-container {
position: relative; position: relative;
@ -1674,7 +1743,7 @@ button:hover {
background: rgba(255, 255, 255, 0.64); background: rgba(255, 255, 255, 0.64);
border: 1px solid #e0e0e0; border: 1px solid #e0e0e0;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 10; z-index: 10;
min-width: 80px; min-width: 80px;
color: #000; color: #000;

403
vue/src/system/GraphStyle.vue

@ -53,7 +53,7 @@
<label>字体颜色</label> <label>字体颜色</label>
<div class="color-picker-border"> <div class="color-picker-border">
<el-color-picker v-model="nodeFontColor" show-alpha class="square-picker"/> <el-color-picker v-model="nodeFontColor" show-alpha class="square-picker"/>
<!-- <input v-model="nodeFontColor" type="color" class="square-picker"/>--> <!-- <input v-model="nodeFontColor" type="color" class="square-picker"/>-->
</div> </div>
</div> </div>
@ -82,7 +82,7 @@
<label>填充颜色</label> <label>填充颜色</label>
<div class="color-picker-border"> <div class="color-picker-border">
<el-color-picker v-model="nodeFill" show-alpha class="square-picker"/> <el-color-picker v-model="nodeFill" show-alpha class="square-picker"/>
<!-- <input v-model="nodeFill" type="color" class="square-picker"/>--> <!-- <input v-model="nodeFill" type="color" class="square-picker"/>-->
</div> </div>
</div> </div>
@ -90,7 +90,7 @@
<label>边框颜色</label> <label>边框颜色</label>
<div class="color-picker-border"> <div class="color-picker-border">
<el-color-picker v-model="nodeStroke" show-alpha class="square-picker"/> <el-color-picker v-model="nodeStroke" show-alpha class="square-picker"/>
<!-- <input v-model="nodeStroke" type="color" class="square-picker"/>--> <!-- <input v-model="nodeStroke" type="color" class="square-picker"/>-->
</div> </div>
</div> </div>
@ -140,7 +140,7 @@
<label>字体颜色</label> <label>字体颜色</label>
<div class="color-picker-border"> <div class="color-picker-border">
<el-color-picker v-model="edgeFontColor" class="square-picker" show-alpha /> <el-color-picker v-model="edgeFontColor" class="square-picker" show-alpha />
<!-- <input v-model="edgeFontColor" type="color" class="square-picker"/>--> <!-- <input v-model="edgeFontColor" type="color" class="square-picker"/>-->
</div> </div>
</div> </div>
@ -169,7 +169,7 @@
<label>线条颜色</label> <label>线条颜色</label>
<div class="color-picker-border"> <div class="color-picker-border">
<el-color-picker v-model="edgeStroke" class="square-picker" show-alpha /> <el-color-picker v-model="edgeStroke" class="square-picker" show-alpha />
<!-- <input v-model="edgeStroke" type="color" class="square-picker"/>--> <!-- <input v-model="edgeStroke" type="color" class="square-picker"/>-->
</div> </div>
</div> </div>
</div> </div>
@ -571,9 +571,10 @@ export default {
this.tagStyles[labelEn] = currentStyle; this.tagStyles[labelEn] = currentStyle;
this.updateAllElements(); this.updateAllElements();
// //
if (this.editingConfigId) { if (this.editingConfigId) {
if (this.saveTimer) clearTimeout(this.saveTimer); if (this.saveTimer) clearTimeout(this.saveTimer);
const currentEditId = this.editingConfigId; const currentEditId = this.editingConfigId;
this.saveTimer = setTimeout(async () => { this.saveTimer = setTimeout(async () => {
try { try {
@ -679,19 +680,280 @@ export default {
this.updateAllElements(); this.updateAllElements();
}, },
// --- XML ---
async handleExportClick() {
if (!this._graph) return;
ElMessageBox.confirm(
'请选择您要导出的图片格式:',
'导出图谱',
{
confirmButtonText: '导出为 PNG',
cancelButtonText: '导出为 SVG',
distinguishCancelAndClose: true,
type: 'info',
draggable: true,
}
).then(async () => {
try {
// PNG 姿
const dataURL = await this._graph.toDataURL({
type: 'image/png',
backgroundColor: '#ffffff'
});
const link = document.createElement('a');
link.href = dataURL;
link.download = `图谱_${Date.now()}.png`;
link.click();
ElMessage.success('PNG 导出成功');
} catch (e) {
ElMessage.error('PNG 导出失败');
}
}).catch((action) => {
if (action === 'cancel') {
this.exportToSVGManual();
}
});
},
exportToSVGManual() {
const graph = this._graph;
if (!graph) return;
const { nodes, edges } = graph.getData();
const width = graph.getSize()[0];
const height = graph.getSize()[1];
const usedColors = new Set();
edges.forEach(edge => { if (edge.style?.stroke) usedColors.add(edge.style.stroke); });
if (usedColors.size === 0) usedColors.add('#cccccc');
let svgContent = `<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`;
// 1.
svgContent += `<defs>`;
Array.from(usedColors).forEach((color, index) => {
const hexColor = color.includes('rgba') ? color.replace(/rgba?\((\d+),\s*(\d+),\s*(\d+).*/, 'rgb($1,$2,$3)') : color;
svgContent += `
<marker id="arrow-${index}" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="${hexColor}" />
</marker>`;
});
svgContent += `</defs>`;
svgContent += `<rect width="100%" height="100%" fill="#ffffff" />`;
// 2.
edges.forEach(edge => {
const s = nodes.find(n => n.id === edge.source);
const t = nodes.find(n => n.id === edge.target);
if (!s || !t) return;
const x1 = s.style?.x || 0, y1 = s.style?.y || 0;
const x2 = t.style?.x || 0, y2 = t.style?.y || 0;
const nodeRadius = (t.style?.size || 60) / 2;
const style = edge.style || {};
const stroke = style.stroke || '#ccc';
const colorIdx = Array.from(usedColors).indexOf(stroke);
const markerId = `arrow-${colorIdx !== -1 ? colorIdx : 0}`;
let pathD = "";
let tx, ty; //
const dx = x2 - x1, dy = y2 - y1;
const dist = Math.sqrt(dx * dx + dy * dy);
const nx = -dy / dist, ny = dx / dist;
// --- ---
if (edge.type === 'cubic') {
// 💡 线 (Cubic Bezier)
const offset = style.curveOffset || 40;
// G6
const cp1X = x1 + dx / 3 + nx * offset;
const cp1Y = y1 + dy / 3 + ny * offset;
const cp2X = x1 + (dx * 2) / 3 + nx * offset;
const cp2Y = y1 + (dy * 2) / 3 + ny * offset;
// cp2
const angle = Math.atan2(y2 - cp2Y, x2 - cp2X);
const realX2 = x2 - Math.cos(angle) * (nodeRadius + 1);
const realY2 = y2 - Math.sin(angle) * (nodeRadius + 1);
pathD = `M ${x1} ${y1} C ${cp1X} ${cp1Y} ${cp2X} ${cp2Y} ${realX2} ${realY2}`;
//
tx = (x1 + cp1X + cp2X + x2) / 4;
ty = (y1 + cp1Y + cp2Y + y2) / 4;
} else if (edge.type === 'quadratic') {
// 💡 线 (Quadratic Bezier)
const offset = style.curveOffset || 40;
const cpX = (x1 + x2) / 2 + nx * offset;
const cpY = (y1 + y2) / 2 + ny * offset;
const angle = Math.atan2(y2 - cpY, x2 - cpX);
const realX2 = x2 - Math.cos(angle) * (nodeRadius + 1);
const realY2 = y2 - Math.sin(angle) * (nodeRadius + 1);
pathD = `M ${x1} ${y1} Q ${cpX} ${cpY} ${realX2} ${realY2}`;
tx = (x1 + cpX * 2 + x2) / 4;
ty = (y1 + cpY * 2 + y2) / 4;
} else {
// 💡 线 (Line)
const angle = Math.atan2(y2 - y1, x2 - x1);
const realX2 = x2 - Math.cos(angle) * nodeRadius;
const realY2 = y2 - Math.sin(angle) * nodeRadius;
pathD = `M ${x1} ${y1} L ${realX2} ${realY2}`;
tx = (x1 + x2) / 2;
ty = (y1 + y2) / 2;
}
// --- ---
const markerAttr = style.endArrow ? `marker-end="url(#${markerId})"` : "";
svgContent += `<path d="${pathD}" stroke="${stroke}" stroke-width="${style.lineWidth || 2}" fill="none" opacity="0.6" ${markerAttr} />`;
const label = edge.data?.relationship?.properties?.label || style.labelText || "";
if (style.edgeShowLabel !== false && label) {
svgContent += `<text x="${tx}" y="${ty - 8}" fill="${style.labelFill || '#666'}" font-size="${style.labelFontSize || 10}" font-family="Microsoft YaHei" text-anchor="middle">${label}</text>`;
}
});
// 3.
nodes.forEach(node => {
const style = node.style || {};
const r = (style.size || 60) / 2;
svgContent += `<circle cx="${style.x}" cy="${style.y}" r="${r}" fill="${style.fill}" stroke="${style.stroke}" stroke-width="${style.lineWidth || 2}" />`;
if (style.labelText) {
svgContent += `<text x="${style.x}" y="${style.y}" fill="${style.labelFill || '#fff'}" font-size="${style.labelFontSize || 12}" font-family="Microsoft YaHei" text-anchor="middle" dominant-baseline="middle">${style.labelText}</text>`;
}
});
svgContent += `</svg>`;
// 4.
const blob = new Blob([svgContent], { type: 'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `医疗图谱导出_${Date.now()}.svg`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
},
initDraggableToolbar() {
const toolbar = document.querySelector('.draggable-toolbar');
if (!toolbar) return;
let isDragging = false;
let startPos = { x: 0, y: 0 };
let offset = { x: 0, y: 0 };
const onMouseDown = (e) => {
if (e.target.closest('.g6-toolbar-item')) return;
isDragging = true;
startPos = { x: e.clientX, y: e.clientY };
offset.x = e.clientX - toolbar.offsetLeft;
offset.y = e.clientY - toolbar.offsetTop;
toolbar.style.transition = 'none';
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
e.preventDefault();
};
const onMouseMove = (e) => {
if (!isDragging) return;
let left = e.clientX - offset.x;
let top = e.clientY - offset.y;
left = Math.max(10, Math.min(window.innerWidth - toolbar.offsetWidth - 10, left));
top = Math.max(10, Math.min(window.innerHeight - toolbar.offsetHeight - 10, top));
toolbar.style.left = left + 'px';
toolbar.style.top = top + 'px';
toolbar.style.right = 'auto';
};
const onMouseUp = () => {
isDragging = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
toolbar.addEventListener('mousedown', onMouseDown);
},
initGraph() { initGraph() {
const container = this.$refs.graphContainer; const container = this.$refs.graphContainer;
if (!container || container.clientWidth === 0) return; if (!container || container.clientWidth === 0) return;
this.defaultData.nodes.forEach(n => this._nodeLabelMap.set(n.id, n.data?.label)); this.defaultData.nodes.forEach(n => this._nodeLabelMap.set(n.id, n.data?.label));
if (this._graph) this._graph.destroy(); if (this._graph) this._graph.destroy();
const graph = new Graph({ const graph = new Graph({
container, width: container.clientWidth, height: container.clientHeight || 600, container,
width: container.clientWidth,
height: container.clientHeight || 600,
layout: { type: 'radial', unitRadius: 100, preventOverlap: true, nodeSpacing: 50 }, layout: { type: 'radial', unitRadius: 100, preventOverlap: true, nodeSpacing: 50 },
behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element', 'hover-activate'], behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element', 'hover-activate'],
autoFit: 'center', animation: false autoFit: 'center',
animation: false,
plugins: [
{ type: 'history', key: 'history' },
{
type: 'toolbar',
position: 'top-right',
className: 'g6-toolbar draggable-toolbar',
style: { marginRight: '320px', marginTop: '10px', cursor: 'move', zIndex: 999 },
onClick: (id) => {
const historyPlugin = this._graph.getPluginInstance('history');
switch (id) {
case 'zoom-in': this._graph.zoomBy(1.2); break;
case 'zoom-out': this._graph.zoomBy(0.8); break;
case 'undo':
if (historyPlugin && historyPlugin.canUndo()) historyPlugin.undo();
break;
case 'redo':
if (historyPlugin && historyPlugin.canRedo()) historyPlugin.redo();
break;
case 'auto-fit': this._graph.fitView(); break;
case 'reset':
const currentData = this._graph.getData();
const cleanNodes = currentData.nodes.map(node => {
const { x, y, ...rest } = node;
if (rest.style) {
delete rest.style.x;
delete rest.style.y;
}
return rest;
});
this._graph.zoomTo(1);
this._graph.translateTo([container.clientWidth / 2, container.clientHeight / 2]);
this._graph.setData({
nodes: cleanNodes,
edges: currentData.edges
});
this._graph.layout().then(() => {
this._graph.fitCenter();
});
ElMessage.success("已重置图谱位置");
break;
case 'export': this.handleExportClick(); break;
}
},
getItems: () => {
return [
{ 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: '导出图谱' },
];
}
}
]
}); });
this._graph = markRaw(graph); this._graph = markRaw(graph);
this.updateAllElements(); this.updateAllElements();
this.$nextTick(() => {
setTimeout(this.initDraggableToolbar, 800);
});
}, },
getEffectiveStyleKey(label) { getEffectiveStyleKey(label) {
return CORE_LABELS.includes(label) ? label : 'Other'; return CORE_LABELS.includes(label) ? label : 'Other';
@ -772,7 +1034,7 @@ export default {
} }
}; };
}); });
this._graph.setData({ nodes, edges }); this._graph.setData({nodes, edges});
this._graph.render(); this._graph.render();
}, },
safeNum(val, defaultVal = 1) { safeNum(val, defaultVal = 1) {
@ -788,7 +1050,7 @@ export default {
...conf, ...conf,
styles: typeof conf.styles === 'string' ? JSON.parse(conf.styles) : conf.styles styles: typeof conf.styles === 'string' ? JSON.parse(conf.styles) : conf.styles
})); }));
return { ...group, configs: uniqueConfigs }; return {...group, configs: uniqueConfigs};
}); });
const activeGroup = this.styleGroups.find(g => g.is_active); const activeGroup = this.styleGroups.find(g => g.is_active);
@ -807,7 +1069,9 @@ export default {
this.applyStylesToPanel(currentActiveConf.styles); this.applyStylesToPanel(currentActiveConf.styles);
this.editingConfigId = currentActiveConf.id; this.editingConfigId = currentActiveConf.id;
this.editingConfigLabel = currentActiveConf.current_label; this.editingConfigLabel = currentActiveConf.current_label;
this.$nextTick(() => { this.isInitialEcho = false; }); this.$nextTick(() => {
this.isInitialEcho = false;
});
} }
} }
this.updateAllElements(); this.updateAllElements();
@ -839,35 +1103,53 @@ export default {
const isLabelExist = group.configs.some(c => c.current_label === labelName && c.id !== excludeId); const isLabelExist = group.configs.some(c => c.current_label === labelName && c.id !== excludeId);
if (isLabelExist) { if (isLabelExist) {
ElMessageBox.alert(`方案【${groupName}】中已存在【${labelName}】标签的配置,请先删除旧配置或选择其他方案。`, '校验失败', { type: 'error' }).catch(() => {}); ElMessageBox.alert(`方案【${groupName}】中已存在【${labelName}】标签的配置。`, '校验失败', {type: 'error'}).catch(() => {
});
return false; return false;
} }
if (group.configs.length >= 5 && !group.configs.some(c => c.id === excludeId)) { if (group.configs.length >= 5 && !group.configs.some(c => c.id === excludeId)) {
ElMessageBox.alert(`方案【${groupName}】的配置已满(上限5个),无法添加。`, '校验失败', { type: 'error' }).catch(() => {}); ElMessageBox.alert(`方案【${groupName}】已满(上限5个)。`, '校验失败', {type: 'error'}).catch(() => {
});
return false; return false;
} }
return true; return true;
}, },
async moveConfigToGroup(config, targetGroup) { async moveConfigToGroup(config, targetGroup) {
// 1.
if (config.group_id === targetGroup.id) {
return ElMessage.info("该配置已在该方案中");
}
if (!this.validateGroupConstraint(targetGroup.group_name, config.current_label, config.id)) return; if (!this.validateGroupConstraint(targetGroup.group_name, config.current_label, config.id)) return;
try { try {
// 2. Payload target_group_id
const payload = { const payload = {
id: config.id, canvas_name: config.canvas_name, group_name: targetGroup.group_name, id: config.id,
current_label: config.current_label, styles: config.styles, is_auto_save: false target_group_id: targetGroup.id, // IDD
canvas_name: config.canvas_name,
current_label: config.current_label,
styles: config.styles,
is_auto_save: false
}; };
const res = await saveGraphStyle(payload); const res = await saveGraphStyle(payload);
if (res.code === 200) { if (res.code === 200) {
ElMessage.success(`已移动至【${targetGroup.group_name}`); ElMessage.success(`已移动至【${targetGroup.group_name}`);
await this.fetchConfigs(); await this.fetchConfigs(); //
} else {
ElMessage.error(res.msg || "移动失败");
} }
} catch (err) { ElMessage.error("操作失败"); } } catch (err) {
ElMessage.error("移动操作异常");
}
}, },
handleSaveClick() { handleSaveClick() {
this.fetchGroupNames(); this.fetchGroupNames();
this.saveForm.canvas_name = `${this.activeTags}_${Date.now()}`; this.saveForm.canvas_name = `${this.activeTags}_${Date.now()}`;
this.saveForm.group_name = '';
this.saveDialogVisible = true; this.saveDialogVisible = true;
}, },
@ -876,13 +1158,19 @@ export default {
if (!this.validateGroupConstraint(this.saveForm.group_name.trim(), this.activeTags)) return; if (!this.validateGroupConstraint(this.saveForm.group_name.trim(), this.activeTags)) return;
const labelEn = tagToLabelMap[this.activeTags]; const labelEn = tagToLabelMap[this.activeTags];
const payload = { const payload = {
canvas_name: this.saveForm.canvas_name.trim(), group_name: this.saveForm.group_name.trim(), canvas_name: this.saveForm.canvas_name.trim(),
current_label: this.activeTags, styles: { ...this.tagStyles[labelEn] }, is_auto_save: false group_name: this.saveForm.group_name.trim(),
current_label: this.activeTags,
styles: {...this.tagStyles[labelEn]},
is_auto_save: false //
}; };
const res = await saveGraphStyle(payload); const res = await saveGraphStyle(payload);
if (res.code === 200) { if (res.code === 200) {
ElMessage.success("保存成功"); ElMessage.success("保存成功");
this.saveDialogVisible = false; this.saveDialogVisible = false;
this.editingConfigId = null;
this.editingConfigLabel = '';
await this.fetchConfigs(); await this.fetchConfigs();
} }
}, },
@ -898,7 +1186,8 @@ export default {
const missingTags = REQUIRED_TAGS.filter(tag => !new Set(currentLabels).has(tag)); const missingTags = REQUIRED_TAGS.filter(tag => !new Set(currentLabels).has(tag));
if (missingTags.length > 0) { if (missingTags.length > 0) {
this.isInitialEcho = false; this.isInitialEcho = false;
return ElMessageBox.alert(`该方案配置不完整,缺失:${missingTags.join('、')}`, '提示', { type: 'warning' }).catch(() => {}); return ElMessageBox.alert(`该方案配置不完整,缺失:${missingTags.join('、')}`, '提示', {type: 'warning'}).catch(() => {
});
} }
this.usingConfigIds = group.configs.map(c => c.id); this.usingConfigIds = group.configs.map(c => c.id);
const res = await applyGraphStyleGroup(group.id); const res = await applyGraphStyleGroup(group.id);
@ -908,7 +1197,10 @@ export default {
if (group.configs.length > 0) this.handleEditConfig(group.configs[0]); if (group.configs.length > 0) this.handleEditConfig(group.configs[0]);
ElMessage.success(`已应用方案【${group.group_name}`); ElMessage.success(`已应用方案【${group.group_name}`);
} }
} catch (err) { this.isInitialEcho = false; ElMessage.error("切换失败"); } } catch (err) {
this.isInitialEcho = false;
ElMessage.error("切换失败");
}
}, },
resetStyle() { resetStyle() {
@ -928,8 +1220,12 @@ export default {
try { try {
await ElMessageBox.confirm('确定删除吗?', '提示'); await ElMessageBox.confirm('确定删除吗?', '提示');
const res = await deleteGraphStyle(id); const res = await deleteGraphStyle(id);
if (res.code === 200) { ElMessage.success("删除成功"); this.fetchConfigs(); } if (res.code === 200) {
} catch (err) { } ElMessage.success("删除成功");
this.fetchConfigs();
}
} catch (err) {
}
}, },
async deleteGroup(groupId) { async deleteGroup(groupId) {
@ -938,21 +1234,35 @@ export default {
try { try {
await ElMessageBox.confirm('确定删除全案吗?', '提示'); await ElMessageBox.confirm('确定删除全案吗?', '提示');
const res = await deleteGraphStyleGroup(groupId); const res = await deleteGraphStyleGroup(groupId);
if (res.code === 200) { ElMessage.success("已删除"); this.fetchConfigs(); } if (res.code === 200) {
} catch (err) { } ElMessage.success("已删除");
this.fetchConfigs();
}
} catch (err) {
}
}, },
async handleUnifiedBatchDelete() { async handleUnifiedBatchDelete() {
if (this.checkedConfigIds.length === 0 && this.checkedGroupIds.length === 0) return; if (this.checkedConfigIds.length === 0 && this.checkedGroupIds.length === 0) return;
try { try {
await ElMessageBox.confirm('确定批量删除吗?', '提示'); await ElMessageBox.confirm('确定批量删除吗?', '提示');
if (this.checkedGroupIds.length > 0) { for (const gid of this.checkedGroupIds) await deleteGraphStyleGroup(gid); } if (this.checkedGroupIds.length > 0) {
if (this.checkedConfigIds.length > 0) { await batchDeleteGraphStyle({ ids: this.checkedConfigIds }); } for (const gid of this.checkedGroupIds) await deleteGraphStyleGroup(gid);
ElMessage.success("成功"); this.clearSelection(); this.fetchConfigs(); }
} catch (e) { } if (this.checkedConfigIds.length > 0) {
await batchDeleteGraphStyle({ids: this.checkedConfigIds});
}
ElMessage.success("成功");
this.clearSelection();
this.fetchConfigs();
} catch (e) {
}
}, },
clearSelection() { this.checkedConfigIds = []; this.checkedGroupIds = []; }, clearSelection() {
this.checkedConfigIds = [];
this.checkedGroupIds = [];
},
handleResize() { handleResize() {
if (this._graph && this.$refs.graphContainer) { if (this._graph && this.$refs.graphContainer) {
this._graph.setSize(this.$refs.graphContainer.clientWidth, this.$refs.graphContainer.clientHeight); this._graph.setSize(this.$refs.graphContainer.clientWidth, this.$refs.graphContainer.clientHeight);
@ -961,6 +1271,7 @@ export default {
} }
} }
</script> </script>
<style scoped> <style scoped>
/* 精准控制“应用全案”按钮 */ /* 精准控制“应用全案”按钮 */
@ -1063,7 +1374,11 @@ export default {
padding-bottom: 10px; padding-bottom: 10px;
margin-top: 10px; margin-top: 10px;
} }
:deep(.el-collapse){ --el-collapse-border-color: transparent;}
:deep(.el-collapse) {
--el-collapse-border-color: transparent;
}
.tag-pill { .tag-pill {
flex-shrink: 0; flex-shrink: 0;
padding: 1px 10px; padding: 1px 10px;
@ -1238,8 +1553,8 @@ export default {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding-bottom: 100px; padding-bottom: 100px;
scrollbar-width: none; /* 针对 Firefox */ scrollbar-width: none; /* 针对 Firefox */
-ms-overflow-style: none; /* 针对 IE 和 Edge */ -ms-overflow-style: none; /* 针对 IE 和 Edge */
} }
:deep(.el-collapse-item__content) { :deep(.el-collapse-item__content) {
@ -1254,7 +1569,7 @@ export default {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
background-color:#f6f9fc !important; background-color: #f6f9fc !important;
padding: 7px 15px; padding: 7px 15px;
margin-bottom: 10px; margin-bottom: 10px;
border-radius: 6px; border-radius: 6px;
@ -1296,6 +1611,7 @@ export default {
height: auto; height: auto;
margin-top: 4px; margin-top: 4px;
} }
.config-checkbox { .config-checkbox {
margin: 0; margin: 0;
cursor: pointer; cursor: pointer;
@ -1458,6 +1774,7 @@ export default {
border-color: #e6e6e6 !important; border-color: #e6e6e6 !important;
color: #333 !important; color: #333 !important;
} }
:deep(.el-message-box__btns .el-button--primary) { :deep(.el-message-box__btns .el-button--primary) {
background-color: #1559f3 !important; background-color: #1559f3 !important;
border-color: #1559f3 !important; border-color: #1559f3 !important;
@ -1487,6 +1804,22 @@ export default {
:deep(.el-dialog .el-input__inner) { :deep(.el-dialog .el-input__inner) {
outline: none !important; outline: none !important;
} }
/* 强制覆盖 G6 工具栏样式 */
:deep(.g6-toolbar) {
height: 35px !important;
display: flex !important;
align-items: center !important;
padding: 0 10px !important;
}
:deep(.g6-toolbar-item) {
width: 20px !important;
height: 35px !important;
font-size: 15px !important;
margin: 0 4px !important;
}
</style> </style>
<style> <style>

Loading…
Cancel
Save