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. 123
      vue/src/components/GraphToolbar.vue
  2. 73
      vue/src/system/GraphDemo.vue
  3. 383
      vue/src/system/GraphStyle.vue

123
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('导出失败');
} }

73
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,
@ -1201,10 +1212,67 @@ export default {
${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) {
@ -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;

383
vue/src/system/GraphStyle.vue

@ -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';
@ -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;
@ -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