You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1522 lines
46 KiB
1522 lines
46 KiB
<template>
|
|
<div class="knowledge-graph-container">
|
|
<Menu :initial-active="4"/>
|
|
|
|
<div class="control-panel">
|
|
<div class="panel-header-container">
|
|
<div class="panel-header">
|
|
<span class="header-title custom-title-style">图谱样式控制</span>
|
|
<i class="el-icon-setting setting-icon" style="color: #94a3b8; cursor: pointer;"></i>
|
|
</div>
|
|
<div class="header-line"></div>
|
|
</div>
|
|
|
|
<div class="tag-filters">
|
|
<div
|
|
v-for="tag in ['疾病','药品','检查','症状','其他']"
|
|
:key="tag"
|
|
class="tag-pill"
|
|
:class="{ 'is-active': activeTags === tag }"
|
|
@click="handleTagClick(tag)"
|
|
>
|
|
{{ tag }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-title">节点控制</div>
|
|
|
|
<div class="form-group-flex">
|
|
<label class="checkbox-label">显示节点标签</label>
|
|
<input v-model="nodeShowLabel" type="checkbox" class="theme-checkbox"/>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>字体名称</label>
|
|
<select v-model="nodeFontFamily">
|
|
<option value="Microsoft YaHei, sans-serif">微软雅黑</option>
|
|
<option value="SimSun, serif">宋体(SimSun)</option>
|
|
<option value="SimHei, sans-serif">黑体(SimHei)</option>
|
|
<option value="Arial, sans-serif">Arial</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>字体大小</label>
|
|
<div class="slider-wrapper">
|
|
<input v-model.number="nodeFontSize" type="range" min="10" max="24" step="1" class="theme-slider"/>
|
|
<span class="val-text-black">{{ nodeFontSize }}px</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="color-picker-item">
|
|
<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"/>-->
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>图形</label>
|
|
<select v-model="nodeShape">
|
|
<option value="circle">圆形</option>
|
|
<option value="diamond">菱形</option>
|
|
<option value="triangle">三角形</option>
|
|
<option value="rect">矩形</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>尺寸</label>
|
|
<input
|
|
:value="nodeSize"
|
|
type="number"
|
|
placeholder="30-100"
|
|
@blur="validateNodeSize($event)"
|
|
@keyup.enter="$event.target.blur()"
|
|
/>
|
|
</div>
|
|
|
|
<div class="color-picker-item">
|
|
<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"/>-->
|
|
</div>
|
|
</div>
|
|
|
|
<div class="color-picker-item">
|
|
<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"/>-->
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>边框尺寸</label>
|
|
<input
|
|
:value="nodeLineWidth"
|
|
type="number"
|
|
placeholder="1-5"
|
|
@blur="validateNodeLineWidth($event)"
|
|
@keyup.enter="$event.target.blur()"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-title">连边控制</div>
|
|
|
|
<div class="form-group-flex">
|
|
<label class="checkbox-label">显示连边标签</label>
|
|
<input v-model="edgeShowLabel" type="checkbox" class="theme-checkbox"/>
|
|
</div>
|
|
<div class="form-group-flex">
|
|
<label class="checkbox-label">显示端点箭头</label>
|
|
<input v-model="edgeEndArrow" type="checkbox" class="theme-checkbox"/>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>字体名称</label>
|
|
<select v-model="edgeFontFamily">
|
|
<option value="Microsoft YaHei, sans-serif">微软雅黑</option>
|
|
<option value="SimSun, serif">宋体</option>
|
|
<option value="SimHei, sans-serif">黑体</option>
|
|
<option value="Arial, sans-serif">Arial</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>字体大小</label>
|
|
<div class="slider-wrapper">
|
|
<input v-model.number="edgeFontSize" type="range" min="8" max="16" step="1" class="theme-slider"/>
|
|
<span class="val-text-black">{{ edgeFontSize }}px</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="color-picker-item">
|
|
<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"/>-->
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>连边类型</label>
|
|
<select v-model="edgeType">
|
|
<option value="line">直线</option>
|
|
<option value="polyline">折线</option>
|
|
<option value="cubic">贝塞尔曲线</option>
|
|
<option value="quadratic">二次贝塞尔曲线</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>线粗细</label>
|
|
<input
|
|
:value="edgeLineWidth"
|
|
type="number"
|
|
placeholder="1-5"
|
|
@blur="validateEdgeLineWidth($event)"
|
|
@keyup.enter="$event.target.blur()"
|
|
/>
|
|
</div>
|
|
|
|
<div class="color-picker-item">
|
|
<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"/>-->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="button-footer">
|
|
<button class="btn-confirm-save" @click="handleSaveClick">保存当前配置</button>
|
|
<button class="btn-reset-style" @click="resetStyle">重置样式</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div ref="graphContainer" class="graph-container" id="container"></div>
|
|
|
|
<div class="config-list-panel">
|
|
<div class="panel-header-container">
|
|
<div class="panel-header">
|
|
<span class="header-title custom-title-style">图谱方案管理</span>
|
|
<div class="header-actions">
|
|
<i class="el-icon-refresh refresh-icon" title="刷新列表" @click="fetchConfigs"></i>
|
|
</div>
|
|
</div>
|
|
<div class="header-line"></div>
|
|
</div>
|
|
|
|
<div class="config-list">
|
|
<div v-if="styleGroups.length === 0" class="empty-text">暂无保存的方案</div>
|
|
|
|
<el-collapse v-model="activeCollapseNames">
|
|
<el-collapse-item v-for="group in styleGroups" :key="group.id" :name="group.id">
|
|
<template #title>
|
|
<div class="group-header-slot">
|
|
<input type="checkbox" :value="group.id" v-model="checkedGroupIds" @click.stop class="config-checkbox"
|
|
style="margin-right: 8px;"/>
|
|
<span class="group-name-text">
|
|
{{ group.group_name }}
|
|
</span>
|
|
|
|
<el-button
|
|
v-if="!group.is_active"
|
|
size="small"
|
|
type="primary"
|
|
plain
|
|
@click.stop="applyWholeGroup(group)">应用全案
|
|
</el-button>
|
|
<el-button
|
|
v-else
|
|
size="small"
|
|
type="info"
|
|
disabled
|
|
plain>已应用
|
|
</el-button>
|
|
|
|
<i class="el-icon-delete group-del" @click.stop="deleteGroup(group.id)"></i>
|
|
</div>
|
|
</template>
|
|
|
|
<el-dropdown
|
|
trigger="contextmenu"
|
|
placement="bottom-start"
|
|
v-for="item in group.configs"
|
|
:key="item.id"
|
|
style="width: 100%; display: block;"
|
|
>
|
|
<div
|
|
class="config-card"
|
|
:class="{
|
|
'card-using': item.id === editingConfigId,
|
|
'card-checked': checkedConfigIds.includes(item.id)
|
|
}"
|
|
@click="handleEditConfig(item)"
|
|
>
|
|
<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="item.id === editingConfigId" class="status-badge">编辑中</span>
|
|
</div>
|
|
<span class="card-tag">标签: {{ item.current_label }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="card-right">
|
|
<i class="el-icon-delete delete-icon" @click.stop="deleteSingleConfig(item.id)"></i>
|
|
</div>
|
|
</div>
|
|
|
|
<template #dropdown>
|
|
<el-dropdown-menu>
|
|
<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>
|
|
</div>
|
|
|
|
<div class="batch-actions-fixed">
|
|
<div class="selection-info" v-if="checkedConfigIds.length > 0 || checkedGroupIds.length > 0">
|
|
已选中 {{ checkedConfigIds.length }} 项配置 / {{ checkedGroupIds.length }} 个方案
|
|
</div>
|
|
<div class="batch-button-group">
|
|
<button class="btn-batch-delete-final"
|
|
:disabled="checkedConfigIds.length === 0 && checkedGroupIds.length === 0"
|
|
@click="handleUnifiedBatchDelete">批量删除所选
|
|
</button>
|
|
<button v-if="checkedConfigIds.length > 0 || checkedGroupIds.length > 0" class="btn-clear-selection"
|
|
@click="clearSelection">清空已选
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<el-dialog v-model="saveDialogVisible" title="保存样式至方案" width="400px">
|
|
<el-form label-width="80px">
|
|
<el-form-item label="所属方案" required>
|
|
<el-select
|
|
v-model="saveForm.group_name"
|
|
filterable
|
|
allow-create
|
|
default-first-option
|
|
placeholder="请选择或输入新方案名"
|
|
style="width: 100%"
|
|
>
|
|
<el-option v-for="g in existingGroups" :key="g.id" :label="g.group_name" :value="g.group_name"/>
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="配置名称" required>
|
|
<el-input v-model="saveForm.canvas_name" placeholder="请输入配置名称"/>
|
|
</el-form-item>
|
|
</el-form>
|
|
<template #footer>
|
|
<el-button @click="saveDialogVisible = false">取消</el-button>
|
|
<el-button type="primary" @click="confirmSave">确定保存</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { Graph } from '@antv/g6';
|
|
import Menu from "@/components/Menu.vue";
|
|
import {
|
|
saveGraphStyle,
|
|
getGroupedGraphStyleList,
|
|
getGraphStyleGroups,
|
|
deleteGraphStyle,
|
|
batchDeleteGraphStyle,
|
|
deleteGraphStyleGroup,
|
|
applyGraphStyleGroup, getGraphStyleActive
|
|
} from '@/api/style';
|
|
import { ElMessageBox, ElMessage } from 'element-plus';
|
|
import { markRaw } from 'vue';
|
|
|
|
const tagToLabelMap = {
|
|
'疾病': 'Disease', '症状': 'Symptom', '病因': 'Cause', '药品': 'Drug', '科室': 'Department', '检查': 'Check', '其他': 'Other'
|
|
};
|
|
|
|
const CORE_LABELS = ['Disease', 'Symptom', 'Drug', 'Check'];
|
|
|
|
const INITIAL_FILL_MAP = {
|
|
'Disease': '#EF4444', 'Drug': '#91cc75', 'Symptom': '#fac858', 'Check': '#336eee',
|
|
'Cause': '#59d1d4', 'Department': '#59d1d4', 'Other': '#59d1d4'
|
|
};
|
|
const INITIAL_STROKE_MAP = {
|
|
'Disease': '#B91C1C', 'Drug': '#047857', 'Symptom': '#B45309', 'Check': '#1D4ED8',
|
|
'Cause': '#40999b', 'Department': '#40999b', 'Other': '#40999b'
|
|
};
|
|
|
|
export default {
|
|
name: 'GraphDemo',
|
|
components: { Menu },
|
|
data() {
|
|
return {
|
|
activeTags: '疾病',
|
|
styleGroups: [],
|
|
existingGroups: [],
|
|
activeCollapseNames: [],
|
|
checkedConfigIds: [],
|
|
checkedGroupIds: [],
|
|
usingConfigIds: [],
|
|
editingConfigId: null,
|
|
editingConfigLabel: '',
|
|
saveDialogVisible: false,
|
|
saveForm: { group_name: '', canvas_name: '' },
|
|
tagStyles: {
|
|
'Disease': this.getInitialTagParams('Disease'),
|
|
'Symptom': this.getInitialTagParams('Symptom'),
|
|
'Cause': this.getInitialTagParams('Cause'),
|
|
'Drug': this.getInitialTagParams('Drug'),
|
|
'Department': this.getInitialTagParams('Department'),
|
|
'DiseaseSite': this.getInitialTagParams('DiseaseSite'),
|
|
'Check': this.getInitialTagParams('Check'),
|
|
'Other': this.getInitialTagParams('Other'),
|
|
},
|
|
nodeShowLabel: true,
|
|
nodeFontFamily: 'Microsoft YaHei, sans-serif',
|
|
nodeFontSize: 12,
|
|
nodeFontColor: '#ffffff',
|
|
nodeShape: 'circle',
|
|
nodeSize: 60,
|
|
nodeFill: '#EF4444',
|
|
nodeStroke: '#B91C1C',
|
|
nodeLineWidth: 2,
|
|
edgeShowLabel: true,
|
|
edgeEndArrow: true,
|
|
edgeFontFamily: 'Microsoft YaHei, sans-serif',
|
|
edgeFontSize: 10,
|
|
edgeFontColor: '#666666',
|
|
edgeType: 'quadratic',
|
|
edgeLineWidth: 2,
|
|
edgeStroke: '#EF4444',
|
|
defaultData: {
|
|
nodes: [
|
|
{ id: "node1", data: { name: "霍乱", label: "Disease" } },
|
|
{ id: "node2", data: { name: "腹泻", label: "Symptom" } },
|
|
{ id: "node3", data: { name: "脱水", label: "Disease" } },
|
|
{ id: "node4", data: { name: "呕吐", label: "Disease" } },
|
|
{ id: "node5", data: { name: "由霍乱弧菌感染所致", label: "Cause" } },
|
|
{ id: "node6", data: { name: "复方磺胺甲噁唑", label: "Drug" } },
|
|
{ id: "node7", data: { name: "消化系统", label: "DiseaseSite" } },
|
|
{ id: "node8", data: { name: "传染科", label: "Department" } },
|
|
{ id: "node9", data: { name: "代谢性 酸中毒", label: "Disease" } },
|
|
{ id: "node10", data: { name: "急性肾衰竭", label: "Disease" } },
|
|
{ id: "node11", data: { name: "检查项目", label: "Check" } }
|
|
],
|
|
edges: [
|
|
{ id: "e1", source: "node1", target: "node2", data: { relationship: { properties: { label: "症状与体征" } } } },
|
|
{ id: "e2", source: "node1", target: "node3", data: { relationship: { properties: { label: "并发症" } } } },
|
|
{ id: "e3", source: "node1", target: "node4", data: { relationship: { properties: { label: "并发症" } } } },
|
|
{ id: "e4", source: "node1", target: "node5", data: { relationship: { properties: { label: "病因" } } } },
|
|
{ id: "e5", source: "node1", target: "node6", data: { relationship: { properties: { label: "治疗方案" } } } },
|
|
{ id: "e6", source: "node1", target: "node7", data: { relationship: { properties: { label: "病变部位" } } } },
|
|
{ id: "e7", source: "node1", target: "node8", data: { relationship: { properties: { label: "科室" } } } },
|
|
{ id: "e8", source: "node1", target: "node9", data: { relationship: { properties: { label: "并发症" } } } },
|
|
{ id: "e9", source: "node1", target: "node10", data: { relationship: { properties: { label: "并发症" } } } },
|
|
{ id: "e10", source: "node1", target: "node11", data: { relationship: { properties: { label: "检查" } } } }
|
|
]
|
|
},
|
|
saveTimer: null,
|
|
isInitialEcho: false
|
|
}
|
|
},
|
|
watch: {
|
|
checkedGroupIds(newGroupIds) {
|
|
this.styleGroups.forEach(group => {
|
|
const isGroupChecked = newGroupIds.includes(group.id);
|
|
const childIds = group.configs.map(c => c.id);
|
|
if (isGroupChecked) {
|
|
this.checkedConfigIds = Array.from(new Set([...this.checkedConfigIds, ...childIds]));
|
|
} else {
|
|
this.checkedConfigIds = this.checkedConfigIds.filter(id => !childIds.includes(id));
|
|
}
|
|
});
|
|
},
|
|
nodeFill(newVal) {
|
|
this.edgeStroke = newVal;
|
|
},
|
|
nodeShowLabel: 'syncAndRefresh', nodeFontFamily: 'syncAndRefresh', nodeFontSize: 'syncAndRefresh',
|
|
nodeFontColor: 'syncAndRefresh', nodeShape: 'syncAndRefresh', nodeSize: 'syncAndRefresh',
|
|
nodeStroke: 'syncAndRefresh', nodeLineWidth: 'syncAndRefresh',
|
|
edgeShowLabel: 'syncAndRefresh', edgeEndArrow: 'syncAndRefresh', edgeFontFamily: 'syncAndRefresh',
|
|
edgeFontSize: 'syncAndRefresh', edgeFontColor: 'syncAndRefresh', edgeType: 'syncAndRefresh',
|
|
edgeLineWidth: 'syncAndRefresh', edgeStroke: 'syncAndRefresh'
|
|
},
|
|
|
|
created() {
|
|
this._graph = null;
|
|
this._nodeLabelMap = new Map();
|
|
},
|
|
mounted() {
|
|
this.$nextTick(() => {
|
|
setTimeout(() => {
|
|
this.initGraph();
|
|
this.fetchConfigs();
|
|
window.addEventListener('resize', this.handleResize);
|
|
}, 300);
|
|
});
|
|
},
|
|
beforeUnmount() {
|
|
if (this._graph) {
|
|
this._graph.destroy();
|
|
this._graph = null;
|
|
}
|
|
window.removeEventListener('resize', this.handleResize);
|
|
if (this.saveTimer) clearTimeout(this.saveTimer);
|
|
},
|
|
methods: {
|
|
handleEditConfig(item) {
|
|
if (this.saveTimer) clearTimeout(this.saveTimer);
|
|
this.isInitialEcho = true;
|
|
|
|
this.editingConfigId = item.id;
|
|
this.editingConfigLabel = item.current_label;
|
|
this.activeTags = item.current_label;
|
|
|
|
const s = item.styles;
|
|
if (!s) {
|
|
this.isInitialEcho = false;
|
|
return;
|
|
}
|
|
|
|
this.nodeShowLabel = s.nodeShowLabel;
|
|
this.nodeFontFamily = s.nodeFontFamily;
|
|
this.nodeFontSize = s.nodeFontSize;
|
|
this.nodeFontColor = s.nodeFontColor;
|
|
this.nodeShape = s.nodeShape;
|
|
this.nodeSize = s.nodeSize;
|
|
this.nodeFill = s.nodeFill;
|
|
this.nodeStroke = s.nodeStroke;
|
|
this.nodeLineWidth = s.nodeLineWidth;
|
|
this.edgeShowLabel = s.edgeShowLabel;
|
|
this.edgeEndArrow = s.edgeEndArrow;
|
|
this.edgeFontFamily = s.edgeFontFamily;
|
|
this.edgeFontSize = s.edgeFontSize;
|
|
this.edgeFontColor = s.edgeFontColor;
|
|
this.edgeType = s.edgeType;
|
|
this.edgeLineWidth = s.edgeLineWidth;
|
|
this.edgeStroke = s.edgeStroke;
|
|
|
|
const labelEn = tagToLabelMap[item.current_label];
|
|
if (labelEn) this.tagStyles[labelEn] = JSON.parse(JSON.stringify(s));
|
|
|
|
if (!this.usingConfigIds.includes(item.id)) {
|
|
this.styleGroups.forEach(g => {
|
|
g.configs.forEach(c => {
|
|
if (this.usingConfigIds.includes(c.id) && c.current_label === item.current_label) {
|
|
this.usingConfigIds = this.usingConfigIds.filter(id => id !== c.id);
|
|
}
|
|
});
|
|
});
|
|
this.usingConfigIds.push(item.id);
|
|
}
|
|
|
|
this.updateAllElements();
|
|
this.$nextTick(() => {
|
|
setTimeout(() => {
|
|
this.isInitialEcho = false;
|
|
}, 100);
|
|
});
|
|
},
|
|
|
|
syncAndRefresh() {
|
|
if (this.isInitialEcho) return;
|
|
|
|
const labelEn = tagToLabelMap[this.activeTags];
|
|
if (!labelEn) return;
|
|
|
|
const currentStyle = {
|
|
nodeShowLabel: this.nodeShowLabel, nodeFontFamily: this.nodeFontFamily, nodeFontSize: this.nodeFontSize,
|
|
nodeFontColor: this.nodeFontColor, nodeShape: this.nodeShape, nodeSize: this.nodeSize,
|
|
nodeFill: this.nodeFill, nodeStroke: this.nodeStroke, nodeLineWidth: this.nodeLineWidth,
|
|
edgeShowLabel: this.edgeShowLabel, edgeEndArrow: this.edgeEndArrow, edgeFontFamily: this.edgeFontFamily,
|
|
edgeFontSize: this.edgeFontSize, edgeFontColor: this.edgeFontColor, edgeType: this.edgeType,
|
|
edgeLineWidth: this.edgeLineWidth, edgeStroke: this.edgeStroke
|
|
};
|
|
|
|
this.tagStyles[labelEn] = currentStyle;
|
|
this.updateAllElements();
|
|
|
|
if (this.editingConfigId) {
|
|
if (this.saveTimer) clearTimeout(this.saveTimer);
|
|
const currentEditId = this.editingConfigId;
|
|
this.saveTimer = setTimeout(async () => {
|
|
try {
|
|
let targetConf = null;
|
|
let groupName = null;
|
|
for (const group of this.styleGroups) {
|
|
targetConf = group.configs.find(c => c.id === currentEditId);
|
|
if (targetConf) {
|
|
groupName = group.group_name;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (targetConf) {
|
|
const payload = {
|
|
id: currentEditId,
|
|
canvas_name: targetConf.canvas_name,
|
|
current_label: targetConf.current_label,
|
|
group_name: groupName,
|
|
styles: currentStyle,
|
|
is_auto_save: true
|
|
};
|
|
await saveGraphStyle(payload);
|
|
targetConf.styles = JSON.parse(JSON.stringify(currentStyle));
|
|
}
|
|
} catch (err) {
|
|
console.error("同步失败:", err);
|
|
}
|
|
}, 800);
|
|
}
|
|
},
|
|
|
|
validateNodeSize(event) {
|
|
const val = parseInt(event.target.value);
|
|
if (isNaN(val) || val < 30 || val > 100) {
|
|
ElMessage({ message: `请输入 30 到 100`, type: 'warning' });
|
|
event.target.value = this.nodeSize;
|
|
return;
|
|
}
|
|
this.nodeSize = val;
|
|
},
|
|
validateNodeLineWidth(event) {
|
|
const val = parseInt(event.target.value);
|
|
if (isNaN(val) || val < 1 || val > 5) {
|
|
ElMessage({ message: `请输入 1 到 5`, type: 'warning' });
|
|
event.target.value = this.nodeLineWidth;
|
|
return;
|
|
}
|
|
this.nodeLineWidth = val;
|
|
},
|
|
validateEdgeLineWidth(event) {
|
|
const val = parseInt(event.target.value);
|
|
if (isNaN(val) || val < 1 || val > 5) {
|
|
ElMessage({ message: `请输入 1 到 5`, type: 'warning' });
|
|
event.target.value = this.edgeLineWidth;
|
|
return;
|
|
}
|
|
this.edgeLineWidth = val;
|
|
},
|
|
getInitialTagParams(label) {
|
|
const fill = INITIAL_FILL_MAP[label] || '#59d1d4';
|
|
return {
|
|
nodeShowLabel: true, nodeFontFamily: 'Microsoft YaHei, sans-serif', nodeFontSize: 12, nodeFontColor: '#ffffff',
|
|
nodeShape: 'circle', nodeSize: 60, nodeFill: fill,
|
|
nodeStroke: INITIAL_STROKE_MAP[label] || '#40999b', nodeLineWidth: 2, edgeShowLabel: true, edgeEndArrow: true,
|
|
edgeFontFamily: 'Microsoft YaHei, sans-serif', edgeFontSize: 10, edgeFontColor: '#666666', edgeType: 'line',
|
|
edgeLineWidth: 2, edgeStroke: fill
|
|
};
|
|
},
|
|
|
|
handleTagClick(tag) {
|
|
if (this.editingConfigId && this.editingConfigLabel !== tag) {
|
|
ElMessageBox.confirm(
|
|
`当前正在编辑【${this.editingConfigLabel}】的存储配置,切换到【${tag}】将退出编辑模式,未保存的修改可能丢失。`,
|
|
'提示',
|
|
{ confirmButtonText: '确定切换', cancelButtonText: '取消', type: 'warning' }
|
|
).then(() => {
|
|
this.editingConfigId = null;
|
|
this.editingConfigLabel = '';
|
|
this.performTagSwitch(tag);
|
|
}).catch(() => {});
|
|
} else {
|
|
this.performTagSwitch(tag);
|
|
}
|
|
},
|
|
|
|
performTagSwitch(tag) {
|
|
this.activeTags = tag;
|
|
const labelEn = tagToLabelMap[tag];
|
|
const style = this.tagStyles[labelEn];
|
|
if (style) {
|
|
this.isInitialEcho = true;
|
|
Object.assign(this, style);
|
|
this.$nextTick(() => { this.isInitialEcho = false; });
|
|
}
|
|
this.updateAllElements();
|
|
},
|
|
|
|
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,
|
|
layout: { type: 'radial', unitRadius: 100, preventOverlap: true, nodeSpacing: 50 },
|
|
behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element', 'hover-activate'],
|
|
autoFit: 'center', animation: false
|
|
});
|
|
this._graph = markRaw(graph);
|
|
this.updateAllElements();
|
|
},
|
|
getEffectiveStyleKey(label) {
|
|
return CORE_LABELS.includes(label) ? label : 'Other';
|
|
},
|
|
updateAllElements() {
|
|
if (!this._graph) return;
|
|
|
|
const currentActiveLabelEn = tagToLabelMap[this.activeTags];
|
|
const labelToAppliedConfigMap = {};
|
|
this.styleGroups.forEach(group => {
|
|
group.configs.forEach(conf => {
|
|
if (this.usingConfigIds.includes(conf.id)) {
|
|
const labelEn = tagToLabelMap[conf.current_label];
|
|
if (labelEn) labelToAppliedConfigMap[labelEn] = conf.styles;
|
|
}
|
|
});
|
|
});
|
|
|
|
const hexToRgba = (hex, opacity) => {
|
|
if (!hex) return 'rgba(182, 178, 178, 0.5)';
|
|
if (hex.startsWith('rgba')) return hex;
|
|
let r = parseInt(hex.slice(1, 3), 16), g = parseInt(hex.slice(3, 5), 16), b = parseInt(hex.slice(5, 7), 16);
|
|
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
|
};
|
|
|
|
const nodes = this.defaultData.nodes.map(node => {
|
|
const rawLabel = node.data?.label || '';
|
|
const effectiveKey = this.getEffectiveStyleKey(rawLabel);
|
|
|
|
let s;
|
|
if (effectiveKey === currentActiveLabelEn) {
|
|
s = {
|
|
nodeShape: this.nodeShape, nodeSize: this.nodeSize, nodeFill: this.nodeFill,
|
|
nodeStroke: this.nodeStroke, nodeLineWidth: this.nodeLineWidth,
|
|
nodeShowLabel: this.nodeShowLabel, nodeFontColor: this.nodeFontColor,
|
|
nodeFontSize: this.nodeFontSize, nodeFontFamily: this.nodeFontFamily
|
|
};
|
|
} else {
|
|
s = labelToAppliedConfigMap[effectiveKey] || this.tagStyles[effectiveKey];
|
|
}
|
|
|
|
return {
|
|
...node, type: s?.nodeShape || 'circle',
|
|
style: {
|
|
size: this.safeNum(s?.nodeSize, 60), fill: s?.nodeFill, stroke: s?.nodeStroke,
|
|
lineWidth: this.safeNum(s?.nodeLineWidth, 2), labelText: s?.nodeShowLabel ? (node.data?.name || '') : '',
|
|
labelFill: s?.nodeFontColor || '#ffffff', labelFontSize: this.safeNum(s?.nodeFontSize, 12),
|
|
labelFontFamily: s?.nodeFontFamily || 'Microsoft YaHei', labelPlacement: 'center', labelWordWrap: true,
|
|
labelMaxWidth: '150%', labelMaxLines: 3, labelTextOverflow: 'ellipsis', labelTextAlign: 'center',
|
|
}
|
|
};
|
|
});
|
|
|
|
const edges = this.defaultData.edges.map(edge => {
|
|
const sRawLabel = this._nodeLabelMap.get(edge.source);
|
|
const effectiveKey = this.getEffectiveStyleKey(sRawLabel);
|
|
|
|
let s;
|
|
if (effectiveKey === currentActiveLabelEn) {
|
|
s = {
|
|
edgeType: this.edgeType, edgeStroke: this.edgeStroke, edgeLineWidth: this.edgeLineWidth,
|
|
edgeEndArrow: this.edgeEndArrow, edgeShowLabel: this.edgeShowLabel,
|
|
edgeFontColor: this.edgeFontColor, edgeFontSize: this.edgeFontSize, edgeFontFamily: this.edgeFontFamily
|
|
};
|
|
} else {
|
|
s = labelToAppliedConfigMap[effectiveKey] || this.tagStyles[effectiveKey];
|
|
}
|
|
|
|
const strokeColor = hexToRgba(s?.edgeStroke || '#EF4444', 0.6);
|
|
return {
|
|
...edge, type: s?.edgeType || 'line',
|
|
style: {
|
|
stroke: strokeColor, lineWidth: this.safeNum(s?.edgeLineWidth, 2), endArrow: s?.edgeEndArrow,
|
|
labelText: s?.edgeShowLabel ? (edge.data?.relationship?.properties?.label || '') : '',
|
|
labelFill: s?.edgeFontColor || '#666', labelFontSize: this.safeNum(s?.edgeFontSize, 10),
|
|
labelFontFamily: s?.edgeFontFamily || 'Microsoft YaHei', labelBackground: true,
|
|
labelBackgroundFill: '#fff', labelBackgroundOpacity: 0.7
|
|
}
|
|
};
|
|
});
|
|
this._graph.setData({ nodes, edges });
|
|
this._graph.render();
|
|
},
|
|
safeNum(val, defaultVal = 1) {
|
|
const n = Number(val);
|
|
return isNaN(n) ? defaultVal : n;
|
|
},
|
|
async fetchConfigs() {
|
|
try {
|
|
const res = await getGroupedGraphStyleList();
|
|
if (res.code === 200) {
|
|
this.styleGroups = res.data.map(group => {
|
|
const uniqueConfigs = (group.configs || []).map(conf => ({
|
|
...conf,
|
|
styles: typeof conf.styles === 'string' ? JSON.parse(conf.styles) : conf.styles
|
|
}));
|
|
return { ...group, configs: uniqueConfigs };
|
|
});
|
|
|
|
const activeGroup = this.styleGroups.find(g => g.is_active);
|
|
if (activeGroup) {
|
|
this.usingConfigIds = activeGroup.configs.map(c => c.id);
|
|
activeGroup.configs.forEach(conf => {
|
|
const labelEn = tagToLabelMap[conf.current_label];
|
|
if (labelEn) {
|
|
this.tagStyles[labelEn] = JSON.parse(JSON.stringify(conf.styles));
|
|
}
|
|
});
|
|
|
|
const currentActiveConf = activeGroup.configs.find(c => c.current_label === this.activeTags);
|
|
if (currentActiveConf) {
|
|
this.isInitialEcho = true;
|
|
Object.assign(this, currentActiveConf.styles);
|
|
this.editingConfigId = currentActiveConf.id;
|
|
this.editingConfigLabel = currentActiveConf.current_label;
|
|
this.$nextTick(() => { this.isInitialEcho = false; });
|
|
}
|
|
}
|
|
this.updateAllElements();
|
|
}
|
|
} catch (err) {
|
|
console.error("加载配置失败:", err);
|
|
}
|
|
},
|
|
async fetchGroupNames() {
|
|
const res = await getGraphStyleGroups();
|
|
if (res.code === 200) this.existingGroups = res.data;
|
|
},
|
|
toggleApplyConfig(item) {
|
|
const idx = this.usingConfigIds.indexOf(item.id);
|
|
if (idx > -1) {
|
|
this.usingConfigIds.splice(idx, 1);
|
|
if (this.editingConfigId === item.id) {
|
|
this.editingConfigId = null;
|
|
this.editingConfigLabel = '';
|
|
}
|
|
} else {
|
|
this.handleEditConfig(item);
|
|
}
|
|
this.updateAllElements();
|
|
},
|
|
|
|
// 修改 validateGroupConstraint 函数
|
|
validateGroupConstraint(groupName, labelName, excludeId = null) {
|
|
const group = this.styleGroups.find(g => g.group_name === groupName);
|
|
if (!group) return true;
|
|
|
|
// 1. 检查标签是否重复
|
|
const isLabelExist = group.configs.some(c => c.current_label === labelName && c.id !== excludeId);
|
|
if (isLabelExist) {
|
|
// 使用 alert 而不是 confirm,因为它不需要处理“取消”逻辑,且必须加 .catch() 规避报错
|
|
ElMessageBox.alert(
|
|
`方案【${groupName}】中已存在【${labelName}】标签的配置,请先删除旧配置或选择其他方案。`,
|
|
'校验失败',
|
|
{ type: 'error' }
|
|
).catch(() => {}); // 捕获关闭弹窗时的异常
|
|
return false;
|
|
}
|
|
|
|
// 2. 检查总量是否已达5个
|
|
if (group.configs.length >= 5 && !group.configs.some(c => c.id === excludeId)) {
|
|
ElMessageBox.alert(
|
|
`方案【${groupName}】的配置已满(上限5个),无法添加。`,
|
|
'校验失败',
|
|
{ type: 'error' }
|
|
).catch(() => {});
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
async moveConfigToGroup(config, targetGroup) {
|
|
// 移动时的校验
|
|
if (!this.validateGroupConstraint(targetGroup.group_name, config.current_label, config.id)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
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
|
|
};
|
|
const res = await saveGraphStyle(payload);
|
|
if (res.code === 200) {
|
|
ElMessage.success(`已移动至【${targetGroup.group_name}】`);
|
|
await this.fetchConfigs();
|
|
}
|
|
} catch (err) {
|
|
ElMessage.error("操作失败");
|
|
}
|
|
},
|
|
|
|
handleSaveClick() {
|
|
this.fetchGroupNames();
|
|
this.saveForm.canvas_name = `${this.activeTags}_${Date.now()}`;
|
|
this.saveDialogVisible = true;
|
|
},
|
|
|
|
async confirmSave() {
|
|
if (!this.saveForm.group_name?.trim() || !this.saveForm.canvas_name?.trim()) {
|
|
return ElMessage.warning("请完善名称");
|
|
}
|
|
|
|
// 保存时的校验
|
|
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
|
|
};
|
|
const res = await saveGraphStyle(payload);
|
|
if (res.code === 200) {
|
|
ElMessage.success("保存成功");
|
|
this.saveDialogVisible = false;
|
|
await this.fetchConfigs();
|
|
}
|
|
},
|
|
// ===========================================================================
|
|
|
|
async applyWholeGroup(group) {
|
|
if (this.saveTimer) clearTimeout(this.saveTimer);
|
|
this.isInitialEcho = true;
|
|
this.editingConfigId = null;
|
|
this.editingConfigLabel = '';
|
|
|
|
try {
|
|
const REQUIRED_TAGS = ['疾病', '症状', '药品', '检查', '其他'];
|
|
const currentLabels = group.configs.map(conf => conf.current_label);
|
|
const hasTags = new Set(currentLabels);
|
|
const missingTags = REQUIRED_TAGS.filter(tag => !hasTags.has(tag));
|
|
|
|
if (missingTags.length > 0) {
|
|
this.isInitialEcho = false;
|
|
return ElMessageBox.alert(
|
|
`该方案配置不完整,无法应用。<br/>缺失:<b style="color: #f56c6c">${missingTags.join('、')}</b>`,
|
|
'提示', { dangerouslyUseHTMLString: true, type: 'warning' }
|
|
);
|
|
}
|
|
|
|
this.usingConfigIds = group.configs.map(c => c.id);
|
|
const res = await applyGraphStyleGroup(group.id);
|
|
if (res.code === 200) {
|
|
await this.fetchConfigs();
|
|
if (group.configs.length > 0) {
|
|
this.handleEditConfig(group.configs[0]);
|
|
}
|
|
ElMessage.success(`已应用方案【${group.group_name}】`);
|
|
}
|
|
} catch (err) {
|
|
this.isInitialEcho = false;
|
|
ElMessage.error("切换失败");
|
|
}
|
|
},
|
|
resetStyle() {
|
|
const labelEn = tagToLabelMap[this.activeTags];
|
|
const initial = this.getInitialTagParams(labelEn);
|
|
this.tagStyles[labelEn] = initial;
|
|
this.isInitialEcho = true;
|
|
Object.assign(this, initial);
|
|
this.$nextTick(() => {
|
|
this.isInitialEcho = false;
|
|
this.syncAndRefresh();
|
|
});
|
|
},
|
|
async deleteSingleConfig(id) {
|
|
if (this.usingConfigIds.includes(id)) return ElMessage.error("应用中无法删除");
|
|
try {
|
|
await ElMessageBox.confirm('确定删除吗?', '提示');
|
|
const res = await deleteGraphStyle(id);
|
|
if (res.code === 200) {
|
|
ElMessage.success("删除成功");
|
|
this.fetchConfigs();
|
|
}
|
|
} catch (err) { }
|
|
},
|
|
async deleteGroup(groupId) {
|
|
const group = this.styleGroups.find(g => g.id === groupId);
|
|
if (!group || group.is_active) return ElMessage.error("应用中无法删除");
|
|
try {
|
|
await ElMessageBox.confirm('确定删除全案吗?', '提示');
|
|
const res = await deleteGraphStyleGroup(groupId);
|
|
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) { }
|
|
},
|
|
clearSelection() {
|
|
this.checkedConfigIds = [];
|
|
this.checkedGroupIds = [];
|
|
},
|
|
handleResize() {
|
|
if (this._graph && this.$refs.graphContainer) {
|
|
this._graph.setSize(this.$refs.graphContainer.clientWidth, this.$refs.graphContainer.clientHeight);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
<style scoped>
|
|
|
|
/* 精准控制“应用全案”按钮 */
|
|
.group-header-slot .el-button--primary.is-plain {
|
|
background-color: #1559f3 !important;
|
|
color: #ffffff !important;
|
|
border: none !important;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.group-header-slot .el-button--primary.is-plain:hover {
|
|
opacity: 0.8;
|
|
background-color: #1559f3 !important;
|
|
color: #ffffff !important;
|
|
}
|
|
|
|
/* 精准控制弹窗中的“确定保存”按钮 */
|
|
.el-dialog__footer .el-button--primary {
|
|
background-color: #1559f3 !important;
|
|
border-color: #1559f3 !important;
|
|
color: #ffffff !important;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.el-dialog__footer .el-button--primary:hover {
|
|
opacity: 0.8;
|
|
background-color: #1559f3 !important;
|
|
border-color: #1559f3 !important;
|
|
}
|
|
|
|
.control-panel::-webkit-scrollbar {
|
|
display: none !important;
|
|
}
|
|
|
|
.control-panel {
|
|
scrollbar-width: none !important;
|
|
-ms-overflow-style: none !important;
|
|
}
|
|
|
|
.knowledge-graph-container {
|
|
display: flex;
|
|
height: 100vh;
|
|
background-color: #f3f3f3;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.control-panel {
|
|
width: 250px;
|
|
background: #ffffff;
|
|
box-shadow: 4px 0 12px rgba(0, 0, 0, 0.08);
|
|
padding: 10px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow-y: auto;
|
|
flex-shrink: 0;
|
|
z-index: 5;
|
|
}
|
|
|
|
.config-list-panel {
|
|
width: 250px;
|
|
background: #ffffff;
|
|
box-shadow: -4px 0 12px rgba(0, 0, 0, 0.08);
|
|
padding: 18px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex-shrink: 0;
|
|
position: relative;
|
|
z-index: 5;
|
|
}
|
|
|
|
.panel-header-container {
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.panel-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding-bottom: 10px;
|
|
border-bottom: 1px solid #E2E6F3;
|
|
}
|
|
|
|
/*.header-line {
|
|
height: 1px;
|
|
background-color: #e2e8f0;
|
|
width: 100%;
|
|
}*/
|
|
|
|
.custom-title-style {
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
color: #1157f3;
|
|
}
|
|
|
|
/* 标签过滤样式 */
|
|
.tag-filters {
|
|
display: flex;
|
|
flex-wrap: nowrap;
|
|
gap: 6px;
|
|
margin-bottom: 10px;
|
|
padding-bottom: 10px;
|
|
}
|
|
|
|
.tag-pill {
|
|
flex-shrink: 0;
|
|
padding: 1px 10px;
|
|
border-radius: 20px;
|
|
font-size: 12px;
|
|
font-weight: bold;
|
|
color: #ffffff;
|
|
background-color: #959390;
|
|
cursor: pointer;
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
user-select: none;
|
|
border: 1px solid transparent;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.tag-pill.is-active {
|
|
background-color: #4a68db;
|
|
}
|
|
|
|
|
|
.section-title {
|
|
display: flex;
|
|
align-items: center;
|
|
font-size: 14px;
|
|
font-weight: bold;
|
|
color: #334155;
|
|
margin-bottom: 12px;
|
|
padding-left: 12px;
|
|
position: relative;
|
|
line-height: 1;
|
|
}
|
|
|
|
.section-title::before {
|
|
content: "";
|
|
position: absolute;
|
|
left: 0;
|
|
width: 4px;
|
|
height: 15px;
|
|
background-color: #1559f3;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.form-group-flex {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-start;
|
|
font-size: 13px;
|
|
margin-bottom: 10px;
|
|
color: #475569;
|
|
}
|
|
|
|
.checkbox-label {
|
|
width: 80px;
|
|
flex-shrink: 0;
|
|
text-align: left;
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.theme-checkbox {
|
|
accent-color: #1559f3;
|
|
width: 16px;
|
|
height: 16px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.form-group, .color-picker-item {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 10px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.form-group label, .color-picker-item label {
|
|
width: 80px;
|
|
flex-shrink: 0;
|
|
text-align: right;
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.form-group select, .form-group input[type="number"] {
|
|
flex: 1;
|
|
padding: 5px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
width: 100px;
|
|
box-shadow: 0 0 0 2px #EBF0FF;
|
|
outline: none;
|
|
}
|
|
|
|
.slider-wrapper {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.theme-slider {
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
width: 100%;
|
|
height: 6px;
|
|
background: #dde5f3;
|
|
border-radius: 10px;
|
|
outline: none;
|
|
}
|
|
|
|
.theme-slider::-webkit-slider-thumb {
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
width: 14px;
|
|
height: 14px;
|
|
background: #1559f3;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.val-text-black {
|
|
color: #000;
|
|
font-size: 13px;
|
|
min-width: 35px;
|
|
}
|
|
|
|
.color-picker-border {
|
|
border-radius: 4px;
|
|
display: flex;
|
|
}
|
|
|
|
.square-picker {
|
|
width: 24px;
|
|
height: 24px;
|
|
cursor: pointer;
|
|
border: none;
|
|
padding: 0;
|
|
}
|
|
|
|
.button-footer {
|
|
}
|
|
|
|
.btn-confirm-save {
|
|
background: #1559f3;
|
|
color: #fff;
|
|
border: none;
|
|
flex: 1;
|
|
padding: 5px 14px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
width: 100px;
|
|
font-size: 12px;
|
|
margin-right: 15px;
|
|
}
|
|
|
|
.btn-reset-style {
|
|
background: #fff;
|
|
color: #1559f3;
|
|
border: 1px solid #1559f3;
|
|
flex: 1;
|
|
padding: 5px 14px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
width: 100px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.graph-container {
|
|
flex: 1;
|
|
background: #fff;
|
|
}
|
|
|
|
.config-list {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding-bottom: 100px;
|
|
scrollbar-width: none; /* 针对 Firefox */
|
|
-ms-overflow-style: none; /* 针对 IE 和 Edge */
|
|
}
|
|
|
|
:deep(.el-collapse-item__content) {
|
|
padding-left: 32px !important;
|
|
padding-right: 10px !important;
|
|
padding-bottom: 12px !important;
|
|
padding-top: 2px !important;
|
|
background-color: transparent !important;
|
|
}
|
|
|
|
.config-card {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
background-color:#f6f9fc !important;
|
|
padding: 7px 15px;
|
|
margin-bottom: 10px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
border: 1px solid #E2E5EA;
|
|
}
|
|
|
|
.config-card:hover {
|
|
background-color: #eff6ff !important;
|
|
outline: 1.5px solid #1559f3;
|
|
}
|
|
|
|
.card-using {
|
|
background-color: #eff6ff !important;
|
|
}
|
|
|
|
.card-checked {
|
|
border-left: 4px solid rgb(239, 68, 68) !important;
|
|
}
|
|
|
|
|
|
.card-left {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
flex: 1;
|
|
min-width: 0;
|
|
gap: 8px;
|
|
}
|
|
|
|
.checkbox-wrapper {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-start;
|
|
margin-right: 0;
|
|
padding-right: 0;
|
|
border-right: none;
|
|
flex-shrink: 0;
|
|
height: auto;
|
|
margin-top: 4px;
|
|
}
|
|
.config-checkbox {
|
|
margin: 0;
|
|
cursor: pointer;
|
|
}
|
|
|
|
|
|
.card-info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
flex: 1;
|
|
min-width: 0;
|
|
gap: 4px;
|
|
}
|
|
|
|
.card-title-row {
|
|
display: flex;
|
|
align-items: center;
|
|
line-height: 20px;
|
|
}
|
|
|
|
.card-name {
|
|
font-weight: 600;
|
|
color: #1e293b;
|
|
font-size: 14px;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
margin-right: 4px;
|
|
}
|
|
|
|
.status-badge {
|
|
font-size: 10px;
|
|
background: #1559f3;
|
|
color: white;
|
|
padding: 0px 6px;
|
|
border-radius: 4px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.card-right {
|
|
display: flex;
|
|
align-items: center;
|
|
height: 20px;
|
|
margin-left: 10px;
|
|
}
|
|
|
|
.delete-icon {
|
|
color: #94a3b8;
|
|
padding: 4px;
|
|
}
|
|
|
|
.delete-icon:hover {
|
|
color: #ef4444;
|
|
background: #fee2e2;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.batch-actions-fixed {
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
background: #ffffff;
|
|
padding: 15px;
|
|
z-index: 10;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
.selection-info {
|
|
font-size: 13px;
|
|
color: #ef4444;
|
|
font-weight: bold;
|
|
text-align: center;
|
|
}
|
|
|
|
.batch-button-group {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.btn-batch-delete-final {
|
|
flex: 2;
|
|
padding: 10px;
|
|
background: #ef4444;
|
|
color: #fff;
|
|
border: none;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.btn-clear-selection {
|
|
flex: 1;
|
|
padding: 10px;
|
|
background: #fff;
|
|
color: #64748b;
|
|
border: 1px solid #cbd5e1;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.refresh-icon {
|
|
cursor: pointer;
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.group-header-slot {
|
|
display: flex;
|
|
align-items: center;
|
|
width: 100%;
|
|
padding-right: 12px;
|
|
flex-wrap: nowrap;
|
|
}
|
|
|
|
.group-name-text {
|
|
flex: 1;
|
|
font-weight: 900;
|
|
font-size: 16px;
|
|
color: #0f172a;
|
|
margin-left: 4px;
|
|
text-align: left;
|
|
}
|
|
|
|
.group-del {
|
|
margin-left: 10px;
|
|
color: #94a3b8;
|
|
}
|
|
|
|
:deep(.el-collapse-item__header) {
|
|
padding: 0 10px;
|
|
height: 54px;
|
|
line-height: 54px;
|
|
background-color: #ffffff !important;
|
|
border-top: none !important;
|
|
}
|
|
|
|
:deep(.el-collapse-item__wrap) {
|
|
border-bottom: none;
|
|
background-color: transparent !important;
|
|
}
|
|
|
|
/* 统一对话框/消息框 */
|
|
:deep(.el-dialog__title),
|
|
:deep(.el-message-box__title) {
|
|
color: #000000 !important;
|
|
font-weight: 600 !important;
|
|
}
|
|
|
|
:deep(.el-dialog__header),
|
|
:deep(.el-message-box__header) {
|
|
text-align: left !important;
|
|
}
|
|
|
|
:deep(.el-dialog__footer .el-button:first-child),
|
|
:deep(.el-message-box__btns .el-button:first-child) {
|
|
background-color: #e6e6e6 !important;
|
|
border-color: #e6e6e6 !important;
|
|
color: #333 !important;
|
|
}
|
|
:deep(.el-message-box__btns .el-button--primary) {
|
|
background-color: #1559f3 !important;
|
|
border-color: #1559f3 !important;
|
|
}
|
|
|
|
|
|
:deep(.el-dialog) {
|
|
--el-color-primary: #ebf0ff !important;
|
|
--el-input-hover-border-color: #ebf0ff !important;
|
|
--el-input-focus-border-color: #ebf0ff !important;
|
|
--el-border-color-hover: #ebf0ff !important;
|
|
}
|
|
|
|
:deep(.el-dialog .el-input__wrapper) {
|
|
box-shadow: 0 0 0 1px #ebf0ff inset !important;
|
|
background-color: #ffffff !important;
|
|
}
|
|
|
|
:deep(.el-dialog .el-input.is-focus .el-input__wrapper),
|
|
:deep(.el-dialog .el-input__wrapper.is-focus),
|
|
:deep(.el-dialog .el-select .el-input__wrapper.is-focus),
|
|
:deep(.el-dialog .el-select:hover .el-input__wrapper) {
|
|
box-shadow: 0 0 0 1px #ebf0ff inset !important;
|
|
}
|
|
|
|
|
|
:deep(.el-dialog .el-input__inner) {
|
|
outline: none !important;
|
|
}
|
|
</style>
|
|
|
|
<style>
|
|
.el-message-box__header {
|
|
text-align: left !important;
|
|
}
|
|
|
|
.el-message-box__title {
|
|
color: #000000 !important;
|
|
font-weight: 500 !important;
|
|
font-size: 18px !important;
|
|
}
|
|
|
|
.el-message-box__btns .el-button--primary {
|
|
background-color: #1559f3 !important;
|
|
border-color: #1559f3 !important;
|
|
}
|
|
|
|
</style>
|