12 changed files with 2702 additions and 722 deletions
@ -1,191 +1,218 @@ |
|||
# service/GraphStyleService.py |
|||
import json |
|||
import logging |
|||
from util.mysql_utils import mysql_client |
|||
|
|||
# 配置日志 |
|||
logger = logging.getLogger(__name__) |
|||
|
|||
|
|||
class GraphStyleService: |
|||
@staticmethod |
|||
def save_config(canvas_name: str, current_label: str, styles_dict: dict, group_name: str = None, config_id: int = None) -> bool: |
|||
def _get_or_create_group(group_name: str) -> int: |
|||
"""内部辅助方法:获取或创建方案组 ID""" |
|||
if not group_name or group_name.strip() == "": |
|||
group_name = "默认方案" |
|||
|
|||
group_name = group_name.strip() |
|||
|
|||
# 1. 查询是否存在 |
|||
check_sql = "SELECT id FROM graph_style_groups WHERE group_name = %s LIMIT 1" |
|||
existing = mysql_client.execute_query(check_sql, (group_name,)) |
|||
if existing: |
|||
return int(existing[0]['id']) |
|||
|
|||
# 2. 不存在则插入 |
|||
insert_sql = "INSERT INTO graph_style_groups (group_name, is_active, is_default) VALUES (%s, %s, %s)" |
|||
mysql_client.execute_update(insert_sql, (group_name, False, False)) |
|||
|
|||
# 3. 获取新生成的 ID |
|||
final_check = mysql_client.execute_query(check_sql, (group_name,)) |
|||
return int(final_check[0]['id']) if final_check else 1 |
|||
|
|||
@staticmethod |
|||
def create_config(canvas_name: str, current_label: str, styles_dict: dict, group_name: str = None) -> bool: |
|||
"""【纯新增】用于另存为或初始保存""" |
|||
config_json = json.dumps(styles_dict, ensure_ascii=False) |
|||
target_group_id = GraphStyleService._get_or_create_group(group_name) |
|||
|
|||
sql = """ |
|||
INSERT INTO graph_configs (canvas_name, current_label, config_json, group_id) |
|||
VALUES (%s, %s, %s, %s) |
|||
""" |
|||
保存图谱样式配置(修复版:防止自动保存导致的分组乱跑) |
|||
affected_rows = mysql_client.execute_update(sql, (canvas_name, current_label, config_json, target_group_id)) |
|||
return affected_rows > 0 |
|||
|
|||
@staticmethod |
|||
def update_config(config_id: int, canvas_name: str, current_label: str, styles_dict: dict, |
|||
group_name: str = None, is_auto_save: bool = False, target_group_id: int = None) -> bool: |
|||
""" |
|||
# 2. 转换样式 JSON |
|||
config_json = json.dumps(styles_dict, ensure_ascii=False) |
|||
核心更新逻辑:支持精准 ID 移动,优化了逻辑优先级判断 |
|||
""" |
|||
if not config_id: |
|||
logger.error("更新失败:缺少 config_id") |
|||
return False |
|||
|
|||
# 3. 【核心修改点】:区分 更新 还是 新建 |
|||
if config_id: |
|||
# --- 更新逻辑 --- |
|||
# 如果带了 ID,我们要极其谨慎地处理 group_id,防止在自动保存时被误改 |
|||
|
|||
# A. 如果调用者明确传了 group_name,说明是“移动”或“初次保存到某组” |
|||
if group_name and group_name.strip() != "": |
|||
# 检查/创建 目标方案组 |
|||
check_group_sql = "SELECT id FROM graph_style_groups WHERE group_name = %s LIMIT 1" |
|||
existing_group = mysql_client.execute_query(check_group_sql, (group_name,)) |
|||
|
|||
if existing_group: |
|||
target_group_id = existing_group[0]['id'] |
|||
else: |
|||
create_group_sql = "INSERT INTO graph_style_groups (group_name, is_active, is_default) VALUES (%s, %s, %s)" |
|||
mysql_client.execute_update(create_group_sql, (group_name, False, False)) |
|||
target_group_id = mysql_client.execute_query("SELECT LAST_INSERT_ID() as last_id")[0]['last_id'] |
|||
|
|||
# 执行带分组更新的 SQL |
|||
sql = """ |
|||
UPDATE graph_configs |
|||
SET canvas_name = %s, current_label = %s, config_json = %s, group_id = %s |
|||
WHERE id = %s |
|||
""" |
|||
affected_rows = mysql_client.execute_update(sql, (canvas_name, current_label, config_json, target_group_id, config_id)) |
|||
config_json_str = json.dumps(styles_dict, ensure_ascii=False) |
|||
|
|||
try: |
|||
# --- 步骤 1:查询当前数据库状态 --- |
|||
curr_sql = "SELECT group_id, canvas_name, current_label, config_json FROM graph_configs WHERE id = %s" |
|||
current_data = mysql_client.execute_query(curr_sql, (config_id,)) |
|||
|
|||
if not current_data: |
|||
logger.warning(f"更新失败:找不到 ID 为 {config_id} 的配置") |
|||
return False |
|||
|
|||
curr_row = current_data[0] |
|||
old_group_id = int(curr_row['group_id']) |
|||
|
|||
# --- 步骤 2:确定目标组 ID (调整优先级) --- |
|||
# 优先级 1: 只要传了 target_group_id,就说明是移动操作,优先级最高 |
|||
if target_group_id is not None: |
|||
final_group_id = int(target_group_id) |
|||
logger.info(f"【移动模式】配置 {config_id}: 强制设定目标组 ID 为 {final_group_id}") |
|||
|
|||
# 优先级 2: 自动保存模式下,锁定 group_id 不允许变动 |
|||
elif is_auto_save: |
|||
final_group_id = old_group_id |
|||
logger.debug(f"【自保模式】配置 {config_id}: 锁定原组 ID {final_group_id}") |
|||
|
|||
# 优先级 3: 传了 group_name 但没传 target_group_id (旧版移动逻辑) |
|||
elif group_name: |
|||
final_group_id = GraphStyleService._get_or_create_group(group_name) |
|||
logger.info(f"【名称模式】配置 {config_id}: 根据名称 [{group_name}] 获得 ID {final_group_id}") |
|||
|
|||
# 兜底:保持不变 |
|||
else: |
|||
# B. 如果没有传 group_name,说明是“实时自动保存”,严禁修改 group_id |
|||
# 这样即使前端变量乱了,数据库的分组也不会变 |
|||
sql = """ |
|||
final_group_id = old_group_id |
|||
|
|||
# --- 步骤 3:差异比对 --- |
|||
# 增加对数据一致性的判定 |
|||
has_changed = ( |
|||
int(final_group_id) != old_group_id or |
|||
canvas_name != curr_row['canvas_name'] or |
|||
current_label != curr_row['current_label'] or |
|||
config_json_str != curr_row['config_json'] |
|||
) |
|||
|
|||
if not has_changed: |
|||
logger.info( |
|||
f"配置 {config_id} 内容无变化 (最终目标ID:{final_group_id}, 原ID:{old_group_id}),跳过数据库更新") |
|||
return True |
|||
|
|||
# --- 步骤 4:执行更新 --- |
|||
sql = """ |
|||
UPDATE graph_configs |
|||
SET canvas_name = %s, current_label = %s, config_json = %s |
|||
SET group_id = %s, canvas_name = %s, current_label = %s, config_json = %s |
|||
WHERE id = %s |
|||
""" |
|||
affected_rows = mysql_client.execute_update(sql, (canvas_name, current_label, config_json, config_id)) |
|||
else: |
|||
# --- 新建逻辑 --- |
|||
# 新建时必须有组名,默认“默认方案” |
|||
if not group_name or group_name.strip() == "": |
|||
group_name = "默认方案" |
|||
|
|||
check_group_sql = "SELECT id FROM graph_style_groups WHERE group_name = %s LIMIT 1" |
|||
existing_group = mysql_client.execute_query(check_group_sql, (group_name,)) |
|||
if existing_group: |
|||
target_group_id = existing_group[0]['id'] |
|||
else: |
|||
create_group_sql = "INSERT INTO graph_style_groups (group_name, is_active, is_default) VALUES (%s, %s, %s)" |
|||
mysql_client.execute_update(create_group_sql, (group_name, False, False)) |
|||
target_group_id = mysql_client.execute_query("SELECT LAST_INSERT_ID() as last_id")[0]['last_id'] |
|||
params = (final_group_id, canvas_name, current_label, config_json_str, config_id) |
|||
|
|||
sql = """ |
|||
INSERT INTO graph_configs (canvas_name, current_label, config_json, group_id) |
|||
VALUES (%s, %s, %s, %s) |
|||
""" |
|||
affected_rows = mysql_client.execute_update(sql, (canvas_name, current_label, config_json, target_group_id)) |
|||
affected_rows = mysql_client.execute_update(sql, params) |
|||
|
|||
return affected_rows > 0 |
|||
if affected_rows > 0: |
|||
logger.info(f"更新成功,ID: {config_id}, 归属组已变更为: {final_group_id}") |
|||
return True |
|||
else: |
|||
logger.error(f"数据库更新执行成功但受影响行数为 0,ID: {config_id}") |
|||
return False |
|||
|
|||
except Exception as e: |
|||
logger.error(f"Service 层更新异常: {str(e)}", exc_info=True) |
|||
return False |
|||
|
|||
@staticmethod |
|||
def get_grouped_configs() -> list: |
|||
""" |
|||
获取嵌套结构的方案列表,按默认/激活状态排序 |
|||
""" |
|||
groups_sql = """ |
|||
SELECT id, group_name, is_active, is_default |
|||
FROM graph_style_groups |
|||
ORDER BY is_default DESC, id ASC |
|||
""" |
|||
"""获取嵌套结构的方案列表""" |
|||
groups_sql = "SELECT id, group_name, is_active, is_default FROM graph_style_groups ORDER BY is_default DESC, id ASC" |
|||
groups = mysql_client.execute_query(groups_sql) or [] |
|||
|
|||
configs_sql = "SELECT id, group_id, canvas_name, current_label, config_json, create_time FROM graph_configs" |
|||
configs = mysql_client.execute_query(configs_sql) or [] |
|||
|
|||
# 格式化配置数据 |
|||
for conf in configs: |
|||
if conf.get('config_json'): |
|||
try: |
|||
conf['styles'] = json.loads(conf['config_json']) |
|||
except: |
|||
conf['styles'] = {} |
|||
conf['styles'] = json.loads(conf['config_json']) if conf.get('config_json') else {} |
|||
# 保持 key 简洁 |
|||
if 'config_json' in conf: |
|||
del conf['config_json'] |
|||
|
|||
if conf.get('create_time') and not isinstance(conf['create_time'], str): |
|||
conf['create_time'] = conf['create_time'].strftime('%Y-%m-%d %H:%M:%S') |
|||
|
|||
result = [] |
|||
# 组装嵌套结构 |
|||
for g in groups: |
|||
g['is_active'] = bool(g['is_active']) |
|||
g['is_default'] = bool(g['is_default']) |
|||
g_children = [c for c in configs if c['group_id'] == g['id']] |
|||
g['configs'] = g_children |
|||
g['configs'] = [c for c in configs if c['group_id'] == g['id']] |
|||
g['expanded'] = g['is_active'] |
|||
result.append(g) |
|||
|
|||
return result |
|||
return groups |
|||
|
|||
@staticmethod |
|||
def apply_group_all(group_id: int) -> bool: |
|||
"""应用全案:设置激活状态""" |
|||
"""切换当前激活的方案组""" |
|||
try: |
|||
reset_sql = "UPDATE graph_style_groups SET is_active = %s" |
|||
mysql_client.execute_update(reset_sql, (False,)) |
|||
apply_sql = "UPDATE graph_style_groups SET is_active = %s WHERE id = %s" |
|||
affected_rows = mysql_client.execute_update(apply_sql, (True, group_id)) |
|||
# 重置所有组的激活状态 |
|||
mysql_client.execute_update("UPDATE graph_style_groups SET is_active = %s", (False,)) |
|||
# 激活目标组 |
|||
affected_rows = mysql_client.execute_update( |
|||
"UPDATE graph_style_groups SET is_active = %s WHERE id = %s", |
|||
(True, group_id) |
|||
) |
|||
return affected_rows > 0 |
|||
except Exception as e: |
|||
print(f"Apply group error: {e}") |
|||
logger.error(f"应用全案异常: {str(e)}") |
|||
return False |
|||
|
|||
@staticmethod |
|||
def set_default_group(group_id: int) -> bool: |
|||
"""设为系统初始默认方案""" |
|||
"""设为默认方案组""" |
|||
try: |
|||
reset_sql = "UPDATE graph_style_groups SET is_default = %s" |
|||
mysql_client.execute_update(reset_sql, (False,)) |
|||
set_sql = "UPDATE graph_style_groups SET is_default = %s WHERE id = %s" |
|||
affected_rows = mysql_client.execute_update(set_sql, (True, group_id)) |
|||
mysql_client.execute_update("UPDATE graph_style_groups SET is_default = %s", (False,)) |
|||
affected_rows = mysql_client.execute_update( |
|||
"UPDATE graph_style_groups SET is_default = %s WHERE id = %s", |
|||
(True, group_id) |
|||
) |
|||
return affected_rows > 0 |
|||
except Exception as e: |
|||
print(f"Set default error: {e}") |
|||
logger.error(f"设置默认方案异常: {str(e)}") |
|||
return False |
|||
|
|||
@staticmethod |
|||
def get_all_configs() -> list: |
|||
"""获取扁平查询""" |
|||
sql = """ |
|||
SELECT c.id, c.group_id, c.canvas_name, c.current_label, c.config_json, c.create_time, g.is_active |
|||
FROM graph_configs c |
|||
LEFT JOIN graph_style_groups g ON c.group_id = g.id |
|||
ORDER BY c.create_time DESC |
|||
""" |
|||
rows = mysql_client.execute_query(sql) |
|||
if not rows: return [] |
|||
|
|||
for row in rows: |
|||
if row.get('config_json'): |
|||
try: |
|||
row['styles'] = json.loads(row['config_json']) |
|||
except: |
|||
row['styles'] = {} |
|||
del row['config_json'] |
|||
if row.get('create_time') and not isinstance(row['create_time'], str): |
|||
row['create_time'] = row['create_time'].strftime('%Y-%m-%d %H:%M:%S') |
|||
row['is_active'] = bool(row.get('is_active', False)) |
|||
return rows |
|||
|
|||
@staticmethod |
|||
def delete_group(group_id: int) -> bool: |
|||
"""级联删除""" |
|||
del_configs_sql = "DELETE FROM graph_configs WHERE group_id = %s" |
|||
mysql_client.execute_update(del_configs_sql, (group_id,)) |
|||
del_group_sql = "DELETE FROM graph_style_groups WHERE id = %s" |
|||
affected_rows = mysql_client.execute_update(del_group_sql, (group_id,)) |
|||
return affected_rows > 0 |
|||
"""级联删除组及其下的所有配置""" |
|||
try: |
|||
# 先删配置,再删组(如果没设外键级联) |
|||
mysql_client.execute_update("DELETE FROM graph_configs WHERE group_id = %s", (group_id,)) |
|||
affected_rows = mysql_client.execute_update("DELETE FROM graph_style_groups WHERE id = %s", (group_id,)) |
|||
return affected_rows > 0 |
|||
except Exception as e: |
|||
logger.error(f"删除方案组异常: {str(e)}") |
|||
return False |
|||
|
|||
@staticmethod |
|||
def delete_config(config_id: int) -> bool: |
|||
"""删除配置""" |
|||
sql = "DELETE FROM graph_configs WHERE id = %s" |
|||
affected_rows = mysql_client.execute_update(sql, (config_id,)) |
|||
return affected_rows > 0 |
|||
"""删除单个配置""" |
|||
try: |
|||
affected_rows = mysql_client.execute_update("DELETE FROM graph_configs WHERE id = %s", (config_id,)) |
|||
return affected_rows > 0 |
|||
except Exception as e: |
|||
logger.error(f"删除配置异常: {str(e)}") |
|||
return False |
|||
|
|||
@staticmethod |
|||
def batch_delete_configs(config_ids: list) -> int: |
|||
"""批量删除""" |
|||
if not config_ids: return 0 |
|||
try: |
|||
clean_ids = [int(cid) for cid in config_ids if str(cid).isdigit()] |
|||
except: return 0 |
|||
if not clean_ids: return 0 |
|||
placeholders = ', '.join(['%s'] * len(clean_ids)) |
|||
sql = f"DELETE FROM graph_configs WHERE id IN ({placeholders})" |
|||
return mysql_client.execute_update(sql, tuple(clean_ids)) |
|||
placeholders = ', '.join(['%s'] * len(config_ids)) |
|||
sql = f"DELETE FROM graph_configs WHERE id IN ({placeholders})" |
|||
return mysql_client.execute_update(sql, tuple(config_ids)) |
|||
except Exception as e: |
|||
logger.error(f"批量删除异常: {str(e)}") |
|||
return 0 |
|||
|
|||
@staticmethod |
|||
def get_group_list() -> list: |
|||
"""获取方案列表""" |
|||
"""简单的方案名称列表""" |
|||
sql = "SELECT id, group_name, is_active, is_default FROM graph_style_groups ORDER BY is_default DESC, id DESC" |
|||
return mysql_client.execute_query(sql) or [] |
|||
@ -0,0 +1,260 @@ |
|||
<template> |
|||
<div ref="toolbarContainer" class="custom-graph-toolbar"></div> |
|||
</template> |
|||
|
|||
<script> |
|||
import { ElMessageBox, ElMessage } from 'element-plus'; |
|||
|
|||
export default { |
|||
name: 'GraphToolbar', |
|||
props: ['graph'], |
|||
data() { |
|||
return { |
|||
observer: null // 增加样式监听器 |
|||
}; |
|||
}, |
|||
watch: { |
|||
graph: { |
|||
handler(newGraph) { |
|||
if (newGraph) { |
|||
this.$nextTick(() => { |
|||
// 给插件一点点初始化时间,确保 DOM 已存在 |
|||
setTimeout(() => this.mountG6Toolbar(), 200); |
|||
}); |
|||
} |
|||
}, |
|||
immediate: true |
|||
} |
|||
}, |
|||
beforeUnmount() { |
|||
// 组件销毁时断开监听,防止内存泄漏 |
|||
if (this.observer) this.observer.disconnect(); |
|||
}, |
|||
methods: { |
|||
mountG6Toolbar() { |
|||
const container = this.$refs.toolbarContainer; |
|||
const g6ToolbarElement = document.querySelector('.g6-toolbar'); |
|||
|
|||
// 【日志 1:输入状态检查】 |
|||
console.log('===[Toolbar Check Start]==='); |
|||
console.log('1. Graph 实例状态:', !!this.graph); |
|||
console.log('2. Vue 容器 (ref):', !!container); |
|||
console.log('3. 是否抓取到原生 G6 DOM:', !!g6ToolbarElement); |
|||
|
|||
if (!this.graph || !container || !g6ToolbarElement) { |
|||
console.warn('!!! [Toolbar] 挂载终止:由于缺少以上必要条件,搬家逻辑未执行。'); |
|||
return; |
|||
} |
|||
|
|||
// 1. 【搬家】将 G6 插件生成的 DOM 移动到 Vue 容器内 |
|||
container.appendChild(g6ToolbarElement); |
|||
|
|||
// 2. 【核心逻辑】锁死样式函数 |
|||
const lockStyle = () => { |
|||
g6ToolbarElement.style.setProperty('position', 'static', 'important'); |
|||
g6ToolbarElement.style.setProperty('inset', 'unset', 'important'); |
|||
g6ToolbarElement.style.setProperty('display', 'flex', 'important'); |
|||
g6ToolbarElement.style.setProperty('height', '100%', 'important'); |
|||
g6ToolbarElement.style.setProperty('background', 'transparent', 'important'); |
|||
g6ToolbarElement.style.setProperty('box-shadow', 'none', 'important'); |
|||
g6ToolbarElement.style.setProperty('border', 'none', 'important'); |
|||
g6ToolbarElement.style.setProperty('width', 'auto', 'important'); |
|||
}; |
|||
|
|||
// 3. 【强效监控】防止刷新或 Resize 时 G6 自动改回样式 |
|||
if (this.observer) this.observer.disconnect(); |
|||
this.observer = new MutationObserver(() => lockStyle()); |
|||
this.observer.observe(g6ToolbarElement, {attributes: true, attributeFilter: ['style']}); |
|||
|
|||
// 执行初始锁死 |
|||
lockStyle(); |
|||
|
|||
// 4. 【修复报错】阻止冒泡,不使用 capture |
|||
const stopEvent = (e) => e.stopPropagation(); |
|||
['mousedown', 'click', 'dblclick', 'contextmenu', 'wheel'].forEach(type => { |
|||
g6ToolbarElement.addEventListener(type, stopEvent); |
|||
}); |
|||
}, |
|||
|
|||
handleToolbarAction(id) { |
|||
if (!this.graph) return; |
|||
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?.canUndo()) historyPlugin.undo(); |
|||
break; |
|||
case 'redo': |
|||
if (historyPlugin?.canRedo()) historyPlugin.redo(); |
|||
break; |
|||
case 'auto-fit': |
|||
this.graph.fitView(); |
|||
break; |
|||
case 'reset': |
|||
break; |
|||
case 'export': |
|||
this.handleExportClick(); |
|||
break; |
|||
} |
|||
}, |
|||
|
|||
async handleExportClick() { |
|||
ElMessageBox.confirm('请选择您要导出的图片格式:', '导出图谱', { |
|||
confirmButtonText: '导出为 PNG', |
|||
cancelButtonText: '导出为 SVG', |
|||
distinguishCancelAndClose: true, |
|||
type: 'info', |
|||
draggable: true, |
|||
}).then(async () => { |
|||
try { |
|||
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 bBox = graph.getCanvas().getRoot().getRenderBounds(); |
|||
const padding = 60; |
|||
const minX = bBox.min[0] - padding; |
|||
const minY = bBox.min[1] - padding; |
|||
const maxX = bBox.max[0] + padding; |
|||
const maxY = bBox.max[1] + padding; |
|||
const vWidth = maxX - minX; |
|||
const vHeight = maxY - minY; |
|||
|
|||
// 1. 定义与问答页面一致的颜色映射(用于兜底和逻辑计算) |
|||
const colorMap = { |
|||
'Disease': { fill: '#EF4444', stroke: '#B91C1C', edge: '#EF4444' }, |
|||
'Drug': { fill: '#91cc75', stroke: '#047857', edge: '#91cc75' }, |
|||
'Check': { fill: '#336eee', stroke: '#1D4ED8', edge: '#336eee' }, |
|||
'Symptom': { fill: '#fac858', stroke: '#B45309', edge: '#fac858' }, |
|||
'Other': { fill: '#59d1d4', stroke: '#40999b', edge: '#59d1d4' } |
|||
}; |
|||
|
|||
let svgContent = `<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="${vWidth}" height="${vHeight}" viewBox="${minX} ${minY} ${vWidth} ${vHeight}">`; |
|||
svgContent += `<rect x="${minX}" y="${minY}" width="${vWidth}" height="${vHeight}" 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; |
|||
|
|||
// 获取坐标 (从 style 中取,G6 会把布局后的坐标存这) |
|||
const x1 = s.style?.x || 0; |
|||
const y1 = s.style?.y || 0; |
|||
const x2 = t.style?.x || 0; |
|||
const y2 = t.style?.y || 0; |
|||
|
|||
// --- 核心修正:根据起点节点的类型来确定边的颜色 --- |
|||
// 这对应了你问答页面里 edge: { style: { stroke: (d) => ... } } 的逻辑 |
|||
const sourceType = s.data?.label || 'Other'; // 获取起点类型 (Disease/Drug等) |
|||
const strokeColor = colorMap[sourceType]?.edge || '#cccccc'; |
|||
|
|||
// 绘制线条 (增加 stroke-width 确保可见) |
|||
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 || ""; |
|||
if (labelText) { |
|||
const mx = (x1 + x2) / 2; |
|||
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. 渲染节点 |
|||
nodes.forEach(node => { |
|||
const type = node.data?.label || 'Other'; |
|||
const colors = colorMap[type] || colorMap['Other']; |
|||
const radius = (node.style?.size || 50) / 2; |
|||
const x = node.style?.x || 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" />`; |
|||
|
|||
if (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>`; |
|||
} |
|||
}); |
|||
|
|||
svgContent += `</svg>`; |
|||
|
|||
// 保存文件逻辑保持不变... |
|||
try { |
|||
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`; |
|||
link.click(); |
|||
URL.revokeObjectURL(url); |
|||
this.$message.success('全量矢量图导出成功'); |
|||
} catch (err) { |
|||
this.$message.error('导出失败'); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.custom-graph-toolbar { |
|||
display: inline-flex !important; |
|||
width: auto !important; |
|||
min-width: 100px; |
|||
background: #ffffff; |
|||
border: 1px solid #e2e8f0; |
|||
border-radius: 8px; |
|||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); |
|||
align-items: center; |
|||
height: 42px !important; |
|||
padding: 0 4px; |
|||
z-index: 1001; |
|||
pointer-events: auto; |
|||
} |
|||
|
|||
:deep(.g6-toolbar) { |
|||
display: inline-flex !important; |
|||
height: 35px !important; |
|||
width: auto !important; |
|||
align-items: center !important; |
|||
background: transparent !important; |
|||
} |
|||
|
|||
:deep(.g6-toolbar-item) { |
|||
width: 20px !important; |
|||
height: 35px !important; |
|||
font-size: 15px !important; |
|||
margin: 0 2px !important; |
|||
display: flex !important; |
|||
align-items: center !important; |
|||
justify-content: center !important; |
|||
border-radius: 6px; |
|||
cursor: pointer; |
|||
color: #838383 !important; |
|||
transition: all 0.2s; |
|||
} |
|||
|
|||
:deep(.g6-toolbar-item:hover) { |
|||
background: #f1f5f9 !important; |
|||
color: #1e293b !important; |
|||
} |
|||
</style> |
|||
File diff suppressed because it is too large
Loading…
Reference in new issue