|
|
@ -52,7 +52,8 @@ |
|
|
<div class="color-picker-item"> |
|
|
<div class="color-picker-item"> |
|
|
<label>字体颜色</label> |
|
|
<label>字体颜色</label> |
|
|
<div class="color-picker-border"> |
|
|
<div class="color-picker-border"> |
|
|
<input v-model="nodeFontColor" type="color" class="square-picker"/> |
|
|
<el-color-picker v-model="nodeFontColor" show-alpha class="square-picker"/> |
|
|
|
|
|
<!-- <input v-model="nodeFontColor" type="color" class="square-picker"/>--> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
@ -80,14 +81,16 @@ |
|
|
<div class="color-picker-item"> |
|
|
<div class="color-picker-item"> |
|
|
<label>填充颜色</label> |
|
|
<label>填充颜色</label> |
|
|
<div class="color-picker-border"> |
|
|
<div class="color-picker-border"> |
|
|
<input v-model="nodeFill" type="color" class="square-picker"/> |
|
|
<el-color-picker v-model="nodeFill" show-alpha class="square-picker"/> |
|
|
|
|
|
<!-- <input v-model="nodeFill" type="color" class="square-picker"/>--> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="color-picker-item"> |
|
|
<div class="color-picker-item"> |
|
|
<label>边框颜色</label> |
|
|
<label>边框颜色</label> |
|
|
<div class="color-picker-border"> |
|
|
<div class="color-picker-border"> |
|
|
<input v-model="nodeStroke" type="color" class="square-picker"/> |
|
|
<el-color-picker v-model="nodeStroke" show-alpha class="square-picker"/> |
|
|
|
|
|
<!-- <input v-model="nodeStroke" type="color" class="square-picker"/>--> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
@ -136,7 +139,8 @@ |
|
|
<div class="color-picker-item"> |
|
|
<div class="color-picker-item"> |
|
|
<label>字体颜色</label> |
|
|
<label>字体颜色</label> |
|
|
<div class="color-picker-border"> |
|
|
<div class="color-picker-border"> |
|
|
<input v-model="edgeFontColor" type="color" class="square-picker"/> |
|
|
<el-color-picker v-model="edgeFontColor" class="square-picker" show-alpha /> |
|
|
|
|
|
<!-- <input v-model="edgeFontColor" type="color" class="square-picker"/>--> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
@ -164,7 +168,8 @@ |
|
|
<div class="color-picker-item"> |
|
|
<div class="color-picker-item"> |
|
|
<label>线条颜色</label> |
|
|
<label>线条颜色</label> |
|
|
<div class="color-picker-border"> |
|
|
<div class="color-picker-border"> |
|
|
<input v-model="edgeStroke" type="color" class="square-picker"/> |
|
|
<el-color-picker v-model="edgeStroke" class="square-picker" show-alpha /> |
|
|
|
|
|
<!-- <input v-model="edgeStroke" type="color" class="square-picker"/>--> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
@ -221,33 +226,51 @@ |
|
|
</div> |
|
|
</div> |
|
|
</template> |
|
|
</template> |
|
|
|
|
|
|
|
|
<div |
|
|
<el-dropdown |
|
|
|
|
|
trigger="contextmenu" |
|
|
|
|
|
placement="bottom-start" |
|
|
v-for="item in group.configs" |
|
|
v-for="item in group.configs" |
|
|
:key="item.id" |
|
|
:key="item.id" |
|
|
class="config-card" |
|
|
style="width: 100%; display: block;" |
|
|
:class="{ |
|
|
|
|
|
'card-using': usingConfigIds.includes(item.id), |
|
|
|
|
|
'card-checked': checkedConfigIds.includes(item.id) |
|
|
|
|
|
}" |
|
|
|
|
|
@click="toggleApplyConfig(item)" |
|
|
|
|
|
> |
|
|
> |
|
|
<div class="card-left"> |
|
|
<div |
|
|
<div class="checkbox-wrapper"> |
|
|
class="config-card" |
|
|
<input type="checkbox" :value="item.id" v-model="checkedConfigIds" @click.stop |
|
|
:class="{ |
|
|
class="config-checkbox"/> |
|
|
'card-using': usingConfigIds.includes(item.id), |
|
|
</div> |
|
|
'card-checked': checkedConfigIds.includes(item.id) |
|
|
<div class="card-info"> |
|
|
}" |
|
|
<div class="card-title-row"> |
|
|
@click="toggleApplyConfig(item)" |
|
|
<span class="card-name">{{ item.canvas_name }}</span> |
|
|
> |
|
|
<span v-if="usingConfigIds.includes(item.id)" class="status-badge">已应用</span> |
|
|
<div class="card-left"> |
|
|
|
|
|
<div class="checkbox-wrapper"> |
|
|
|
|
|
<input type="checkbox" :value="item.id" v-model="checkedConfigIds" @click.stop class="config-checkbox"/> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div class="card-info"> |
|
|
|
|
|
<div class="card-title-row"> |
|
|
|
|
|
<span class="card-name">{{ item.canvas_name }}</span> |
|
|
|
|
|
<span v-if="usingConfigIds.includes(item.id)" class="status-badge">已应用</span> |
|
|
|
|
|
</div> |
|
|
|
|
|
<span class="card-tag">标签: {{ item.current_label }}</span> |
|
|
</div> |
|
|
</div> |
|
|
<span class="card-tag">标签: {{ item.current_label }}</span> |
|
|
</div> |
|
|
|
|
|
<div class="card-right"> |
|
|
|
|
|
<i class="el-icon-delete delete-icon" @click.stop="deleteSingleConfig(item.id)"></i> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="card-right"> |
|
|
|
|
|
<i class="el-icon-delete delete-icon" @click.stop="deleteSingleConfig(item.id)"></i> |
|
|
<template #dropdown> |
|
|
</div> |
|
|
<el-dropdown-menu> |
|
|
</div> |
|
|
<el-dropdown-item disabled style="font-weight: bold; color: #333;">移动至方案:</el-dropdown-item> |
|
|
|
|
|
<el-dropdown-item |
|
|
|
|
|
v-for="targetGroup in styleGroups.filter(g => g.id !== group.id)" |
|
|
|
|
|
:key="targetGroup.id" |
|
|
|
|
|
@click="moveConfigToGroup(item, targetGroup)" |
|
|
|
|
|
> |
|
|
|
|
|
{{ targetGroup.group_name }} |
|
|
|
|
|
</el-dropdown-item> |
|
|
|
|
|
</el-dropdown-menu> |
|
|
|
|
|
</template> |
|
|
|
|
|
</el-dropdown> |
|
|
</el-collapse-item> |
|
|
</el-collapse-item> |
|
|
</el-collapse> |
|
|
</el-collapse> |
|
|
</div> |
|
|
</div> |
|
|
@ -362,7 +385,7 @@ export default { |
|
|
edgeFontFamily: 'Microsoft YaHei, sans-serif', |
|
|
edgeFontFamily: 'Microsoft YaHei, sans-serif', |
|
|
edgeFontSize: 10, |
|
|
edgeFontSize: 10, |
|
|
edgeFontColor: '#666666', |
|
|
edgeFontColor: '#666666', |
|
|
edgeType: 'line', |
|
|
edgeType: 'quadratic', |
|
|
edgeLineWidth: 2, |
|
|
edgeLineWidth: 2, |
|
|
edgeStroke: '#EF4444', |
|
|
edgeStroke: '#EF4444', |
|
|
defaultData: { |
|
|
defaultData: { |
|
|
@ -489,6 +512,7 @@ export default { |
|
|
}, |
|
|
}, |
|
|
syncAndRefresh() { |
|
|
syncAndRefresh() { |
|
|
const label = tagToLabelMap[this.activeTags]; |
|
|
const label = tagToLabelMap[this.activeTags]; |
|
|
|
|
|
console.log(this.nodeFill) |
|
|
if (label) { |
|
|
if (label) { |
|
|
this.tagStyles[label] = { |
|
|
this.tagStyles[label] = { |
|
|
nodeShowLabel: this.nodeShowLabel, nodeFontFamily: this.nodeFontFamily, nodeFontSize: this.nodeFontSize, |
|
|
nodeShowLabel: this.nodeShowLabel, nodeFontFamily: this.nodeFontFamily, nodeFontSize: this.nodeFontSize, |
|
|
@ -589,31 +613,66 @@ export default { |
|
|
try { |
|
|
try { |
|
|
const res = await getGroupedGraphStyleList(); |
|
|
const res = await getGroupedGraphStyleList(); |
|
|
if (res.code === 200) { |
|
|
if (res.code === 200) { |
|
|
this.styleGroups = res.data.map(group => ({ |
|
|
// 1. 数据处理与【强制去重】 |
|
|
...group, |
|
|
// 这里的逻辑确保了同一个配置 ID 在同一个分组内不会出现两次 |
|
|
configs: group.configs.map(conf => ({ |
|
|
this.styleGroups = res.data.map(group => { |
|
|
...conf, |
|
|
const idSet = new Set(); |
|
|
styles: typeof conf.styles === 'string' ? JSON.parse(conf.styles) : conf.styles |
|
|
const uniqueConfigs = []; |
|
|
})) |
|
|
|
|
|
})); |
|
|
(group.configs || []).forEach(conf => { |
|
|
|
|
|
if (!idSet.has(conf.id)) { |
|
|
// 仅在初始加载且没有选中项时执行自动同步逻辑 |
|
|
idSet.add(conf.id); |
|
|
|
|
|
uniqueConfigs.push({ |
|
|
|
|
|
...conf, |
|
|
|
|
|
// 安全解析 styles 字符串 |
|
|
|
|
|
styles: typeof conf.styles === 'string' ? JSON.parse(conf.styles) : conf.styles |
|
|
|
|
|
}); |
|
|
|
|
|
} else { |
|
|
|
|
|
console.warn(`检测到重复 ID: ${conf.id},已在前端过滤`); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
return { |
|
|
|
|
|
...group, |
|
|
|
|
|
configs: uniqueConfigs |
|
|
|
|
|
}; |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// 2. 初始加载时的应用逻辑(带排他性同步) |
|
|
if (this.usingConfigIds.length === 0) { |
|
|
if (this.usingConfigIds.length === 0) { |
|
|
|
|
|
const tempUsingIds = []; |
|
|
|
|
|
const seenLabels = new Set(); // 记录已处理的标签(如:疾病、药品) |
|
|
|
|
|
|
|
|
this.styleGroups.forEach(group => { |
|
|
this.styleGroups.forEach(group => { |
|
|
|
|
|
// 只处理后端标记为 active 的方案组 |
|
|
if (group.is_active) { |
|
|
if (group.is_active) { |
|
|
|
|
|
// 自动展开激活的折叠面板 |
|
|
if (!this.activeCollapseNames.includes(group.id)) { |
|
|
if (!this.activeCollapseNames.includes(group.id)) { |
|
|
this.activeCollapseNames.push(group.id); |
|
|
this.activeCollapseNames.push(group.id); |
|
|
} |
|
|
} |
|
|
const ids = group.configs.map(c => c.id); |
|
|
|
|
|
this.usingConfigIds = [...this.usingConfigIds, ...ids]; |
|
|
// 遍历组内配置进行标签去重 |
|
|
|
|
|
group.configs.forEach(conf => { |
|
|
|
|
|
if (!seenLabels.has(conf.current_label)) { |
|
|
|
|
|
tempUsingIds.push(conf.id); |
|
|
|
|
|
seenLabels.add(conf.current_label); |
|
|
|
|
|
} else { |
|
|
|
|
|
console.warn(`标签冲突拦截:方案【${group.group_name}】中的配置【${conf.canvas_name}】因标签【${conf.current_label}】已存在而被忽略`); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
} |
|
|
} |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// 最终赋值 |
|
|
|
|
|
this.usingConfigIds = tempUsingIds; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 3. 驱动图谱重新渲染 |
|
|
this.updateAllElements(); |
|
|
this.updateAllElements(); |
|
|
} |
|
|
} |
|
|
} catch (err) { |
|
|
} catch (err) { |
|
|
console.error("加载配置失败:", err); |
|
|
console.error("加载配置失败:", err); |
|
|
|
|
|
ElMessage.error("获取方案列表失败"); |
|
|
} |
|
|
} |
|
|
}, |
|
|
}, |
|
|
async fetchGroupNames() { |
|
|
async fetchGroupNames() { |
|
|
@ -637,40 +696,100 @@ export default { |
|
|
} |
|
|
} |
|
|
this.updateAllElements(); |
|
|
this.updateAllElements(); |
|
|
}, |
|
|
}, |
|
|
|
|
|
/** |
|
|
|
|
|
* 新增功能:将配置移动到另一个方案 |
|
|
|
|
|
*/ |
|
|
|
|
|
async moveConfigToGroup(config, targetGroup) { |
|
|
|
|
|
try { |
|
|
|
|
|
// 1. 构建更新负载 |
|
|
|
|
|
const payload = { |
|
|
|
|
|
id: config.id, // 确保有 ID,后端才会执行更新而非新增 |
|
|
|
|
|
canvas_name: config.canvas_name, |
|
|
|
|
|
group_name: targetGroup.group_name, // 目标方案名 |
|
|
|
|
|
current_label: config.current_label, |
|
|
|
|
|
styles: config.styles |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const res = await saveGraphStyle(payload); |
|
|
|
|
|
|
|
|
|
|
|
if (res.code === 200) { |
|
|
|
|
|
ElMessage.success(`已成功移动至【${targetGroup.group_name}】`); |
|
|
|
|
|
|
|
|
|
|
|
// 2. 【核心修复】手动清理本地 styleGroups 中的旧引用,避免 fetchConfigs 响应延迟导致的视觉重复 |
|
|
|
|
|
this.styleGroups = this.styleGroups.map(group => { |
|
|
|
|
|
return { |
|
|
|
|
|
...group, |
|
|
|
|
|
// 过滤掉所有组里 ID 等于当前移动配置 ID 的项 |
|
|
|
|
|
configs: group.configs.filter(c => c.id !== config.id) |
|
|
|
|
|
}; |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// 3. 重新获取最新的服务器状态 |
|
|
|
|
|
await this.fetchConfigs(); |
|
|
|
|
|
} |
|
|
|
|
|
} catch (err) { |
|
|
|
|
|
console.error("移动配置失败:", err); |
|
|
|
|
|
ElMessage.error("移动操作失败,请重试"); |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
/** |
|
|
/** |
|
|
* 修改后的应用全案方法 |
|
|
* 修改后的应用全案方法 |
|
|
* 逻辑:保留当前已手动选中的配置,新方案中冲突的配置不予应用 |
|
|
* 逻辑:保留当前已手动选中的配置,新方案中冲突的配置不予应用 |
|
|
*/ |
|
|
*/ |
|
|
async applyWholeGroup(group) { |
|
|
async applyWholeGroup(group) { |
|
|
try { |
|
|
try { |
|
|
// 1. 获取当前正在使用的所有配置项对象 |
|
|
// 1. 【新增】定义必须包含的 5 个核心标签 |
|
|
const currentlyUsingConfigs = []; |
|
|
const REQUIRED_TAGS = ['疾病', '症状', '药品', '检查', '其他']; |
|
|
|
|
|
|
|
|
|
|
|
// 2. 【新增】获取当前方案组里已有的标签 |
|
|
|
|
|
const currentLabels = group.configs.map(conf => conf.current_label); |
|
|
|
|
|
const hasTags = new Set(currentLabels); |
|
|
|
|
|
|
|
|
|
|
|
// 3. 【新增】找出缺失的标签 |
|
|
|
|
|
const missingTags = REQUIRED_TAGS.filter(tag => !hasTags.has(tag)); |
|
|
|
|
|
|
|
|
|
|
|
// 4. 【新增】拦截逻辑:如果标签不全,弹出提示并返回 |
|
|
|
|
|
if (missingTags.length > 0) { |
|
|
|
|
|
return ElMessage.warning('该方案配置不完整,无法应用。目前缺少:'+missingTags.join('、')); |
|
|
|
|
|
} |
|
|
|
|
|
// 获取当前正在使用的所有标签名(用于外部排他) |
|
|
|
|
|
const currentlyUsingLabels = []; |
|
|
this.styleGroups.forEach(g => { |
|
|
this.styleGroups.forEach(g => { |
|
|
g.configs.forEach(c => { |
|
|
g.configs.forEach(c => { |
|
|
if (this.usingConfigIds.includes(c.id)) { |
|
|
if (this.usingConfigIds.includes(c.id)) { |
|
|
currentlyUsingConfigs.push(c); |
|
|
currentlyUsingLabels.push(c.current_label); |
|
|
} |
|
|
} |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
// 2. 提取出当前已选中的标签列表 (例如: ['疾病', '药品']) |
|
|
// 对新方案内部进行去重:如果方案内有多个同标签配置,只取第一个 |
|
|
const currentlySelectedLabels = currentlyUsingConfigs.map(c => c.current_label); |
|
|
const uniqueNewConfigs = []; |
|
|
|
|
|
const seenLabelsInNewGroup = new Set(); |
|
|
|
|
|
|
|
|
|
|
|
group.configs.forEach(conf => { |
|
|
|
|
|
if (!seenLabelsInNewGroup.has(conf.current_label)) { |
|
|
|
|
|
uniqueNewConfigs.push(conf); |
|
|
|
|
|
seenLabelsInNewGroup.add(conf.current_label); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
// 3. 过滤新方案:如果新方案里的配置标签 已经存在于当前已选列表中,则剔除 |
|
|
// 过滤掉与当前已选标签冲突的配置(外部排他) |
|
|
const filteredNewConfigIds = group.configs |
|
|
const filteredNewConfigIds = uniqueNewConfigs |
|
|
.filter(newConf => !currentlySelectedLabels.includes(newConf.current_label)) |
|
|
.filter(newConf => !currentlyUsingLabels.includes(newConf.current_label)) |
|
|
.map(newConf => newConf.id); |
|
|
.map(newConf => newConf.id); |
|
|
|
|
|
|
|
|
// 4. 将过滤后的新 ID 追加到现有的选中列表中 |
|
|
if (filteredNewConfigIds.length === 0) { |
|
|
|
|
|
return ElMessage.info("该方案中的标签配置已存在,无需重复应用"); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 追加 ID |
|
|
this.usingConfigIds = [...this.usingConfigIds, ...filteredNewConfigIds]; |
|
|
this.usingConfigIds = [...this.usingConfigIds, ...filteredNewConfigIds]; |
|
|
|
|
|
|
|
|
// 5. 调用后端接口更新激活状态(保持后端数据同步) |
|
|
// 调用后端接口更新状态 |
|
|
const res = await applyGraphStyleGroup(group.id); |
|
|
const res = await applyGraphStyleGroup(group.id); |
|
|
if (res.code === 200) { |
|
|
if (res.code === 200) { |
|
|
// 重新获取列表以刷新 UI 状态(如“已应用”按钮状态) |
|
|
|
|
|
await this.fetchConfigs(); |
|
|
await this.fetchConfigs(); |
|
|
ElMessage.success(`方案【${group.group_name}】已应用,已保留您手动选择的标签`); |
|
|
ElMessage.success(`方案【${group.group_name}】已应用,已自动过滤重复标签`); |
|
|
} |
|
|
} |
|
|
} catch (err) { |
|
|
} catch (err) { |
|
|
console.error(err); |
|
|
console.error(err); |
|
|
@ -780,11 +899,7 @@ export default { |
|
|
.some(g => g.configs.some(c => this.usingConfigIds.includes(c.id))); |
|
|
.some(g => g.configs.some(c => this.usingConfigIds.includes(c.id))); |
|
|
|
|
|
|
|
|
if (isAnyCheckedConfigUsing || isAnyCheckedGroupUsing) { |
|
|
if (isAnyCheckedConfigUsing || isAnyCheckedGroupUsing) { |
|
|
return ElMessageBox.alert( |
|
|
return ElMessage.warning('选中的项目中包含“正在应用”的配置,请先取消应用后再执行删除操作。'); |
|
|
'选中的项目中包含“正在应用”的配置,请先取消应用后再执行删除操作。', |
|
|
|
|
|
'无法执行删除', |
|
|
|
|
|
{ type: 'error', confirmButtonText: '我知道了' } |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 3. 最小保留数判断 (针对方案组) |
|
|
// 3. 最小保留数判断 (针对方案组) |
|
|
@ -1022,9 +1137,11 @@ export default { |
|
|
.form-group select, .form-group input[type="number"] { |
|
|
.form-group select, .form-group input[type="number"] { |
|
|
flex: 1; |
|
|
flex: 1; |
|
|
padding: 5px; |
|
|
padding: 5px; |
|
|
border: 1px solid #e2e8f0; |
|
|
border: none; |
|
|
border-radius: 4px; |
|
|
border-radius: 4px; |
|
|
width: 100px; |
|
|
width: 100px; |
|
|
|
|
|
box-shadow: 0 0 0 2px #EBF0FF; |
|
|
|
|
|
outline: none; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.slider-wrapper { |
|
|
.slider-wrapper { |
|
|
@ -1056,14 +1173,11 @@ export default { |
|
|
|
|
|
|
|
|
.val-text-black { |
|
|
.val-text-black { |
|
|
color: #000; |
|
|
color: #000; |
|
|
font-weight: bold; |
|
|
|
|
|
font-size: 13px; |
|
|
font-size: 13px; |
|
|
min-width: 35px; |
|
|
min-width: 35px; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.color-picker-border { |
|
|
.color-picker-border { |
|
|
padding: 3px; |
|
|
|
|
|
border: 1px solid #e2e8f0; |
|
|
|
|
|
border-radius: 4px; |
|
|
border-radius: 4px; |
|
|
display: flex; |
|
|
display: flex; |
|
|
} |
|
|
} |
|
|
@ -1367,7 +1481,6 @@ export default { |
|
|
<style> |
|
|
<style> |
|
|
.el-message-box__header { |
|
|
.el-message-box__header { |
|
|
text-align: left !important; |
|
|
text-align: left !important; |
|
|
padding-top: 15px !important; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.el-message-box__title { |
|
|
.el-message-box__title { |
|
|
@ -1380,4 +1493,5 @@ export default { |
|
|
background-color: #1559f3 !important; |
|
|
background-color: #1559f3 !important; |
|
|
border-color: #1559f3 !important; |
|
|
border-color: #1559f3 !important; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
</style> |
|
|
</style> |