<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>