|
|
|
@ -53,7 +53,7 @@ |
|
|
|
<label>字体颜色</label> |
|
|
|
<div class="color-picker-border"> |
|
|
|
<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> |
|
|
|
|
|
|
|
@ -82,7 +82,7 @@ |
|
|
|
<label>填充颜色</label> |
|
|
|
<div class="color-picker-border"> |
|
|
|
<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> |
|
|
|
|
|
|
|
@ -90,7 +90,7 @@ |
|
|
|
<label>边框颜色</label> |
|
|
|
<div class="color-picker-border"> |
|
|
|
<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> |
|
|
|
|
|
|
|
@ -140,7 +140,7 @@ |
|
|
|
<label>字体颜色</label> |
|
|
|
<div class="color-picker-border"> |
|
|
|
<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> |
|
|
|
|
|
|
|
@ -169,7 +169,7 @@ |
|
|
|
<label>线条颜色</label> |
|
|
|
<div class="color-picker-border"> |
|
|
|
<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> |
|
|
|
@ -571,9 +571,10 @@ export default { |
|
|
|
this.tagStyles[labelEn] = currentStyle; |
|
|
|
this.updateAllElements(); |
|
|
|
|
|
|
|
// 只有在编辑模式下才同步到数据库 |
|
|
|
// 只有在“编辑模式”下才触发后端自动同步 |
|
|
|
if (this.editingConfigId) { |
|
|
|
if (this.saveTimer) clearTimeout(this.saveTimer); |
|
|
|
|
|
|
|
const currentEditId = this.editingConfigId; |
|
|
|
this.saveTimer = setTimeout(async () => { |
|
|
|
try { |
|
|
|
@ -679,19 +680,280 @@ export default { |
|
|
|
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() { |
|
|
|
const container = this.$refs.graphContainer; |
|
|
|
if (!container || container.clientWidth === 0) return; |
|
|
|
this.defaultData.nodes.forEach(n => this._nodeLabelMap.set(n.id, n.data?.label)); |
|
|
|
if (this._graph) this._graph.destroy(); |
|
|
|
|
|
|
|
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 }, |
|
|
|
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.updateAllElements(); |
|
|
|
this.$nextTick(() => { |
|
|
|
setTimeout(this.initDraggableToolbar, 800); |
|
|
|
}); |
|
|
|
}, |
|
|
|
getEffectiveStyleKey(label) { |
|
|
|
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(); |
|
|
|
}, |
|
|
|
safeNum(val, defaultVal = 1) { |
|
|
|
@ -788,7 +1050,7 @@ export default { |
|
|
|
...conf, |
|
|
|
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); |
|
|
|
@ -807,7 +1069,9 @@ export default { |
|
|
|
this.applyStylesToPanel(currentActiveConf.styles); |
|
|
|
this.editingConfigId = currentActiveConf.id; |
|
|
|
this.editingConfigLabel = currentActiveConf.current_label; |
|
|
|
this.$nextTick(() => { this.isInitialEcho = false; }); |
|
|
|
this.$nextTick(() => { |
|
|
|
this.isInitialEcho = false; |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
this.updateAllElements(); |
|
|
|
@ -839,35 +1103,53 @@ export default { |
|
|
|
|
|
|
|
const isLabelExist = group.configs.some(c => c.current_label === labelName && c.id !== excludeId); |
|
|
|
if (isLabelExist) { |
|
|
|
ElMessageBox.alert(`方案【${groupName}】中已存在【${labelName}】标签的配置,请先删除旧配置或选择其他方案。`, '校验失败', { type: 'error' }).catch(() => {}); |
|
|
|
ElMessageBox.alert(`方案【${groupName}】中已存在【${labelName}】标签的配置。`, '校验失败', {type: 'error'}).catch(() => { |
|
|
|
}); |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
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 true; |
|
|
|
}, |
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
try { |
|
|
|
// 2. 构造 Payload:直接传入 target_group_id |
|
|
|
const payload = { |
|
|
|
id: config.id, canvas_name: config.canvas_name, group_name: targetGroup.group_name, |
|
|
|
current_label: config.current_label, styles: config.styles, is_auto_save: false |
|
|
|
id: config.id, |
|
|
|
target_group_id: targetGroup.id, // 新增:直接传 ID,最准确D |
|
|
|
canvas_name: config.canvas_name, |
|
|
|
current_label: config.current_label, |
|
|
|
styles: config.styles, |
|
|
|
is_auto_save: false |
|
|
|
}; |
|
|
|
|
|
|
|
const res = await saveGraphStyle(payload); |
|
|
|
if (res.code === 200) { |
|
|
|
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() { |
|
|
|
this.fetchGroupNames(); |
|
|
|
this.saveForm.canvas_name = `${this.activeTags}_${Date.now()}`; |
|
|
|
this.saveForm.group_name = ''; |
|
|
|
this.saveDialogVisible = true; |
|
|
|
}, |
|
|
|
|
|
|
|
@ -876,13 +1158,19 @@ export default { |
|
|
|
if (!this.validateGroupConstraint(this.saveForm.group_name.trim(), this.activeTags)) return; |
|
|
|
const labelEn = tagToLabelMap[this.activeTags]; |
|
|
|
const payload = { |
|
|
|
canvas_name: this.saveForm.canvas_name.trim(), group_name: this.saveForm.group_name.trim(), |
|
|
|
current_label: this.activeTags, styles: { ...this.tagStyles[labelEn] }, is_auto_save: false |
|
|
|
canvas_name: this.saveForm.canvas_name.trim(), |
|
|
|
group_name: this.saveForm.group_name.trim(), |
|
|
|
current_label: this.activeTags, |
|
|
|
styles: {...this.tagStyles[labelEn]}, |
|
|
|
is_auto_save: false // 标记为手动保存 |
|
|
|
}; |
|
|
|
const res = await saveGraphStyle(payload); |
|
|
|
if (res.code === 200) { |
|
|
|
ElMessage.success("保存成功"); |
|
|
|
this.saveDialogVisible = false; |
|
|
|
this.editingConfigId = null; |
|
|
|
this.editingConfigLabel = ''; |
|
|
|
|
|
|
|
await this.fetchConfigs(); |
|
|
|
} |
|
|
|
}, |
|
|
|
@ -898,7 +1186,8 @@ export default { |
|
|
|
const missingTags = REQUIRED_TAGS.filter(tag => !new Set(currentLabels).has(tag)); |
|
|
|
if (missingTags.length > 0) { |
|
|
|
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); |
|
|
|
const res = await applyGraphStyleGroup(group.id); |
|
|
|
@ -908,7 +1197,10 @@ export default { |
|
|
|
if (group.configs.length > 0) this.handleEditConfig(group.configs[0]); |
|
|
|
ElMessage.success(`已应用方案【${group.group_name}】`); |
|
|
|
} |
|
|
|
} catch (err) { this.isInitialEcho = false; ElMessage.error("切换失败"); } |
|
|
|
} catch (err) { |
|
|
|
this.isInitialEcho = false; |
|
|
|
ElMessage.error("切换失败"); |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
resetStyle() { |
|
|
|
@ -928,8 +1220,12 @@ export default { |
|
|
|
try { |
|
|
|
await ElMessageBox.confirm('确定删除吗?', '提示'); |
|
|
|
const res = await deleteGraphStyle(id); |
|
|
|
if (res.code === 200) { ElMessage.success("删除成功"); this.fetchConfigs(); } |
|
|
|
} catch (err) { } |
|
|
|
if (res.code === 200) { |
|
|
|
ElMessage.success("删除成功"); |
|
|
|
this.fetchConfigs(); |
|
|
|
} |
|
|
|
} catch (err) { |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
async deleteGroup(groupId) { |
|
|
|
@ -938,21 +1234,35 @@ export default { |
|
|
|
try { |
|
|
|
await ElMessageBox.confirm('确定删除全案吗?', '提示'); |
|
|
|
const res = await deleteGraphStyleGroup(groupId); |
|
|
|
if (res.code === 200) { ElMessage.success("已删除"); this.fetchConfigs(); } |
|
|
|
} catch (err) { } |
|
|
|
if (res.code === 200) { |
|
|
|
ElMessage.success("已删除"); |
|
|
|
this.fetchConfigs(); |
|
|
|
} |
|
|
|
} catch (err) { |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
async handleUnifiedBatchDelete() { |
|
|
|
if (this.checkedConfigIds.length === 0 && this.checkedGroupIds.length === 0) return; |
|
|
|
try { |
|
|
|
await ElMessageBox.confirm('确定批量删除吗?', '提示'); |
|
|
|
if (this.checkedGroupIds.length > 0) { for (const gid of this.checkedGroupIds) await deleteGraphStyleGroup(gid); } |
|
|
|
if (this.checkedConfigIds.length > 0) { await batchDeleteGraphStyle({ ids: this.checkedConfigIds }); } |
|
|
|
ElMessage.success("成功"); this.clearSelection(); this.fetchConfigs(); |
|
|
|
} catch (e) { } |
|
|
|
if (this.checkedGroupIds.length > 0) { |
|
|
|
for (const gid of this.checkedGroupIds) await deleteGraphStyleGroup(gid); |
|
|
|
} |
|
|
|
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() { |
|
|
|
if (this._graph && this.$refs.graphContainer) { |
|
|
|
this._graph.setSize(this.$refs.graphContainer.clientWidth, this.$refs.graphContainer.clientHeight); |
|
|
|
@ -961,6 +1271,7 @@ export default { |
|
|
|
} |
|
|
|
} |
|
|
|
</script> |
|
|
|
|
|
|
|
<style scoped> |
|
|
|
|
|
|
|
/* 精准控制“应用全案”按钮 */ |
|
|
|
@ -1063,7 +1374,11 @@ export default { |
|
|
|
padding-bottom: 10px; |
|
|
|
margin-top: 10px; |
|
|
|
} |
|
|
|
:deep(.el-collapse){ --el-collapse-border-color: transparent;} |
|
|
|
|
|
|
|
:deep(.el-collapse) { |
|
|
|
--el-collapse-border-color: transparent; |
|
|
|
} |
|
|
|
|
|
|
|
.tag-pill { |
|
|
|
flex-shrink: 0; |
|
|
|
padding: 1px 10px; |
|
|
|
@ -1238,8 +1553,8 @@ export default { |
|
|
|
flex: 1; |
|
|
|
overflow-y: auto; |
|
|
|
padding-bottom: 100px; |
|
|
|
scrollbar-width: none; /* 针对 Firefox */ |
|
|
|
-ms-overflow-style: none; /* 针对 IE 和 Edge */ |
|
|
|
scrollbar-width: none; /* 针对 Firefox */ |
|
|
|
-ms-overflow-style: none; /* 针对 IE 和 Edge */ |
|
|
|
} |
|
|
|
|
|
|
|
:deep(.el-collapse-item__content) { |
|
|
|
@ -1254,7 +1569,7 @@ export default { |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
justify-content: space-between; |
|
|
|
background-color:#f6f9fc !important; |
|
|
|
background-color: #f6f9fc !important; |
|
|
|
padding: 7px 15px; |
|
|
|
margin-bottom: 10px; |
|
|
|
border-radius: 6px; |
|
|
|
@ -1296,6 +1611,7 @@ export default { |
|
|
|
height: auto; |
|
|
|
margin-top: 4px; |
|
|
|
} |
|
|
|
|
|
|
|
.config-checkbox { |
|
|
|
margin: 0; |
|
|
|
cursor: pointer; |
|
|
|
@ -1458,6 +1774,7 @@ export default { |
|
|
|
border-color: #e6e6e6 !important; |
|
|
|
color: #333 !important; |
|
|
|
} |
|
|
|
|
|
|
|
:deep(.el-message-box__btns .el-button--primary) { |
|
|
|
background-color: #1559f3 !important; |
|
|
|
border-color: #1559f3 !important; |
|
|
|
@ -1487,6 +1804,22 @@ export default { |
|
|
|
:deep(.el-dialog .el-input__inner) { |
|
|
|
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> |
|
|
|
|