<template>
  <div class="workflow-canvas-editor" :class="{ 'is-fullscreen': isFullscreen }" v-loading="loading">
    <!-- 顶部工具栏 -->
    <div class="canvas-toolbar" v-show="!isFullscreen">
      <div class="toolbar-left">
        <el-button @click="goBack" icon="el-icon-arrow-left" size="small">返回</el-button>
        <el-divider direction="vertical"></el-divider>
        <span class="template-name">{{ templateData.templateName || '加载中...' }}</span>
        <el-tag v-if="templateData.industryType" size="mini" type="info">{{ templateData.industryType }}</el-tag>
        <el-tag v-if="isPublished" size="mini" type="success">已发布（只读）</el-tag>
      </div>
      <div class="toolbar-right">
        <el-radio-group v-model="viewMode" size="small" class="view-switch-group">
          <el-radio-button label="canvas">画布</el-radio-button>
          <el-radio-button label="list">列表</el-radio-button>
        </el-radio-group>
        <el-button size="small" icon="el-icon-connection" @click="autoLayoutNodes" title="自动排列">自动排列</el-button>
        <el-divider direction="vertical"></el-divider>
        <el-button size="small" icon="el-icon-zoom-in" @click="zoomIn">放大</el-button>
        <el-button size="small" icon="el-icon-zoom-out" @click="zoomOut">缩小</el-button>
        <el-button size="small" icon="el-icon-rank" @click="fitView">适应</el-button>
        <el-divider direction="vertical"></el-divider>
        <el-button size="small" icon="el-icon-full-screen" @click="toggleFullscreen" title="全屏">全屏</el-button>
        <el-divider direction="vertical"></el-divider>
        <el-button type="primary" size="small" :loading="saving" :disabled="isPublished" @click="saveCanvas">保存</el-button>
      </div>
    </div>

    <!-- 全屏模式浮动工具栏 -->
    <div class="fullscreen-toolbar" v-if="isFullscreen">
      <el-button @click="toggleFullscreen" icon="el-icon-close" type="info" plain size="small" title="退出全屏">退出全屏</el-button>
      <span class="fullscreen-title" v-if="templateData">{{ templateData.templateName }}</span>
      <el-button type="primary" size="small" :loading="saving" :disabled="isPublished" @click="saveCanvas">保存</el-button>
    </div>

    <!-- 基本信息卡片 -->
    <el-card class="info-card" shadow="never" v-show="!isFullscreen && templateData">
      <el-descriptions :column="4" size="small" border>
        <el-descriptions-item label="模板名称">{{ templateData.templateName }}</el-descriptions-item>
        <el-descriptions-item label="行业类型">{{ templateData.industryType || '通用' }}</el-descriptions-item>
        <el-descriptions-item label="模板编码">{{ templateData.templateCode || '-' }}</el-descriptions-item>
        <el-descriptions-item label="节点数量">{{ nodes.length }} 个</el-descriptions-item>
      </el-descriptions>
    </el-card>

    <!-- 画布模式 -->
    <div v-if="viewMode === 'canvas'" class="canvas-mode">
      <div class="canvas-meta">
        <div class="meta-item">
          <span class="meta-label">节点数</span>
          <span class="meta-value">{{ nodes.length }}</span>
        </div>
        <div class="meta-item">
          <span class="meta-label">连线数</span>
          <span class="meta-value">{{ edges.length }}</span>
        </div>
        <div class="meta-item">
          <span class="meta-label">缩放</span>
          <span class="meta-value">{{ Math.round(scale * 100) }}%</span>
        </div>
      </div>

    <div class="canvas-workspace" v-loading="loading">
      <!-- 左侧节点面板 -->
      <div class="node-panel">
        <div class="panel-title">节点类型</div>
        <div class="node-category" v-for="category in nodeCategories" :key="category.key">
          <div class="category-title">{{ category.name }}</div>
          <div class="node-list">
            <div
              class="node-item"
              v-for="nodeType in category.types"
              :key="nodeType.type"
              :draggable="!isPublished"
              :class="{ disabled: isPublished }"
              @dragstart="onDragStart($event, nodeType)"
            >
              <i :class="nodeType.icon" :style="{ color: nodeType.color }"></i>
              <span>{{ nodeType.label }}</span>
            </div>
          </div>
        </div>
      </div>

      <!-- 中间画布区域 -->
      <div
        class="canvas-container"
        ref="canvasContainer"
        tabindex="0"
        @drop="onDrop"
        @dragover.prevent
        @click="onCanvasClick"
        @mousedown="onCanvasMouseDown"
        @mousemove="onCanvasMouseMove"
        @mouseup="onCanvasMouseUp"
        @wheel.prevent="onMouseWheel"
        @keydown.delete="handleDelete"
        @keydown.backspace="handleDelete"
      >
        <svg
          class="canvas-svg"
          ref="canvasSvg"
          :width="canvasSize.width"
          :height="canvasSize.height"
          :class="{ 'dragging-canvas': isDraggingCanvas }"
        >
          <defs>
            <!-- 阴影滤镜 -->
            <filter id="node-shadow" x="-5%" y="-5%" width="115%" height="120%">
              <feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#000000" flood-opacity="0.08" />
            </filter>
            <filter id="node-shadow-hover" x="-5%" y="-5%" width="115%" height="120%">
              <feDropShadow dx="0" dy="4" stdDeviation="8" flood-color="#000000" flood-opacity="0.12" />
            </filter>
            <filter id="node-shadow-selected" x="-10%" y="-10%" width="130%" height="140%">
              <feDropShadow dx="0" dy="3" stdDeviation="6" flood-color="#409eff" flood-opacity="0.35" />
            </filter>
            <filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
              <feGaussianBlur stdDeviation="3" result="blur" />
              <feMerge>
                <feMergeNode in="blur" />
                <feMergeNode in="SourceGraphic" />
              </feMerge>
            </filter>
            <!-- 点阵网格 -->
            <pattern id="dot-grid" width="24" height="24" patternUnits="userSpaceOnUse">
              <circle cx="12" cy="12" r="1.2" fill="#dce0e6" />
            </pattern>
            <!-- 节点渐变背景 -->
            <linearGradient id="node-bg-gradient" x1="0%" y1="0%" x2="0%" y2="100%">
              <stop offset="0%" stop-color="#ffffff" />
              <stop offset="100%" stop-color="#f8f9fb" />
            </linearGradient>
            <!-- 箭头标记 -->
            <marker id="arrowhead" markerWidth="10" markerHeight="8" refX="8" refY="4" orient="auto">
              <path d="M0,1 L6,4 L0,7 Q0,4 0,1" fill="#b0b8c1" />
            </marker>
            <marker id="arrowhead-selected" markerWidth="10" markerHeight="8" refX="8" refY="4" orient="auto">
              <path d="M0,1 L6,4 L0,7 Q0,4 0,1" fill="#409eff" />
            </marker>
          </defs>

          <!-- 网格背景 -->
          <rect :width="canvasSize.width" :height="canvasSize.height" fill="url(#dot-grid)" />

          <!-- 连线 -->
          <g class="edges-layer" :transform="`translate(${canvasOffset.x}, ${canvasOffset.y}) scale(${scale})`">
            <g
              v-for="edge in edges"
              :key="edge.edgeKey"
              class="edge-group"
              :class="{ selected: selectedEdge === edge }"
              @click.stop="selectEdge(edge)"
            >
              <path
                :d="getEdgePath(edge)"
                stroke="transparent"
                stroke-width="16"
                fill="none"
                class="edge-interaction"
                style="cursor: pointer"
              />
              <path
                :d="getEdgePath(edge)"
                :stroke="edge.edgeColor || '#b0b8c1'"
                stroke-width="2"
                fill="none"
                :marker-end="selectedEdge === edge ? 'url(#arrowhead-selected)' : 'url(#arrowhead)'"
                class="edge-path"
              />
              <text
                v-if="edge.edgeLabel"
                :x="getEdgeLabelPos(edge).x"
                :y="getEdgeLabelPos(edge).y"
                text-anchor="middle"
                font-size="11"
                font-weight="500"
                fill="#86909c"
              >{{ edge.edgeLabel }}</text>
            </g>

            <!-- 正在绘制的临时连线 -->
            <path
              v-if="drawingEdge"
              :d="getDrawingEdgePath()"
              stroke="#409eff"
              stroke-width="2"
              stroke-dasharray="6,4"
              fill="none"
              opacity="0.7"
              marker-end="url(#arrowhead-selected)"
            />
          </g>

          <!-- 节点 -->
          <g class="nodes-layer" :transform="`translate(${canvasOffset.x}, ${canvasOffset.y}) scale(${scale})`">
            <g
              v-for="node in nodes"
              :key="node.nodeCode"
              class="node-group"
              :class="{ selected: selectedNode === node, dragging: isDraggingNode && draggingNode === node }"
              :transform="`translate(${node.positionX || 0}, ${node.positionY || 0})`"
              @click.stop="selectNode(node)"
              :filter="selectedNode === node ? 'url(#node-shadow-selected)' : 'url(#node-shadow)'"
            >
              <!-- 节点背景 -->
              <rect
                x="0"
                y="0"
                :width="node.width || 200"
                :height="node.height || 80"
                rx="10"
                fill="url(#node-bg-gradient)"
                :stroke="selectedNode === node ? '#409eff' : '#e8ecf1'"
                stroke-width="2"
                class="node-rect"
                @mousedown="onNodeMouseDown($event, node)"
                style="cursor: move; pointer-events: all"
              />

              <!-- 左侧色彩装饰条 -->
              <rect
                x="0"
                y="0"
                width="5"
                :height="node.height || 80"
                rx="10"
                class="node-accent"
                :fill="getNodeAccentColor(node.nodeType)"
                style="pointer-events: none"
              />

              <!-- 拖拽指示器 -->
              <text
                :x="(node.width || 200) - 14"
                :y="14"
                font-size="9"
                fill="#c8cdd4"
                class="drag-indicator"
                style="cursor: move"
              >⋮⋮</text>

              <!-- 图标背景圆角方形 -->
              <rect
                x="14"
                y="12"
                rx="6"
                width="36"
                height="36"
                :fill="getNodeAccentBg(node.nodeType)"
                class="node-icon-bg"
                style="pointer-events: none"
              />

              <!-- 节点类型图标 -->
              <text
                x="32"
                y="37"
                font-size="20"
                text-anchor="middle"
                fill="#ffffff"
                style="pointer-events: none"
              >{{ getNodeIcon(node.nodeType) }}</text>

              <!-- 节点名称 -->
              <text
                x="60"
                y="26"
                font-size="13"
                font-weight="700"
                fill="#1d2129"
                style="pointer-events: none"
              >{{ node.nodeName }}</text>

              <!-- 节点类型标签 -->
              <rect
                x="60"
                y="33"
                :width="getTypeTagWidth(node) + 8"
                height="17"
                rx="4"
                :fill="getNodeAccentBg(node.nodeType)"
                opacity="0.15"
                style="pointer-events: none"
              />
              <text
                x="64"
                y="45"
                font-size="10"
                font-weight="600"
                :fill="getNodeAccentColor(node.nodeType)"
                style="pointer-events: none"
              >{{ getNodeTypeName(node.nodeType) }}</text>

              <!-- 底部描述 -->
              <text
                x="14"
                y="68"
                font-size="11"
                fill="#86909c"
                style="pointer-events: none"
              >{{ getNodeBottomDesc(node) }}</text>

              <!-- 连接点 - 上 -->
              <circle
                :cx="(node.width || 200) / 2"
                cy="0"
                r="5"
                fill="#ffffff"
                stroke="#c0c8d4"
                stroke-width="1.5"
                class="port port-top"
                @mousedown.stop="startDrawEdge($event, node, 'top')"
                style="cursor: crosshair; pointer-events: all"
              />
              <!-- 连接点 - 下 -->
              <circle
                :cx="(node.width || 200) / 2"
                :cy="node.height || 80"
                r="5"
                fill="#ffffff"
                stroke="#c0c8d4"
                stroke-width="1.5"
                class="port port-bottom"
                @mousedown.stop="startDrawEdge($event, node, 'bottom')"
                style="cursor: crosshair; pointer-events: all"
              />
              <!-- 连接点 - 左 -->
              <circle
                cx="0"
                :cy="(node.height || 80) / 2"
                r="5"
                fill="#ffffff"
                stroke="#c0c8d4"
                stroke-width="1.5"
                class="port port-left"
                @mousedown.stop="startDrawEdge($event, node, 'left')"
                style="cursor: crosshair; pointer-events: all"
              />
              <!-- 连接点 - 右 -->
              <circle
                :cx="node.width || 200"
                :cy="(node.height || 80) / 2"
                r="5"
                fill="#ffffff"
                stroke="#c0c8d4"
                stroke-width="1.5"
                class="port port-right"
                @mousedown.stop="startDrawEdge($event, node, 'right')"
                style="cursor: crosshair; pointer-events: all"
              />
            </g>
          </g>
        </svg>
      </div>

      <!-- 右侧属性面板（固定预留宽度） -->
      <div class="property-panel">
        <div class="panel-title">
          {{ selectedNode ? '节点属性' : (selectedEdge ? '连线属性' : '属性面板') }}
          <el-button size="mini" icon="el-icon-close" @click="clearSelection" style="float: right"></el-button>
        </div>

        <div v-if="!selectedNode && !selectedEdge" class="property-empty">
          请先在画布中选择节点或连线
        </div>

        <!-- 节点属性表单 -->
        <div class="panel-subtitle" v-if="selectedNode">基础配置</div>
        <el-form v-if="selectedNode" label-width="80px" size="small" class="property-form">
          <el-form-item label="节点名称">
            <el-input v-model="selectedNode.nodeName" :disabled="isPublished" />
          </el-form-item>
          <el-form-item label="节点类型">
            <el-select v-model="selectedNode.nodeType" style="width: 100%" :disabled="isPublished">
              <el-option label="开始节点" :value="1" />
              <el-option label="消息节点" :value="2" />
              <el-option label="判断节点" :value="3" />
              <el-option label="等待节点" :value="4" />
              <el-option label="结束节点" :value="5" />
              <el-option label="API节点" :value="6" />
              <el-option label="购物车节点" :value="7" />
              <el-option label="优惠券节点" :value="8" />
              <el-option label="标签节点" :value="9" />
              <el-option label="赠礼节点" :value="10" />
              <el-option label="文档节点" :value="11" />
              <el-option label="用户节点" :value="12" />
              <el-option label="数据分析节点" :value="13" />
              <el-option label="AI节点" :value="14" />
            </el-select>
          </el-form-item>
          <el-form-item label="节点编码">
            <el-input v-model="selectedNode.nodeCode" disabled />
          </el-form-item>
          <el-form-item label="消息模板" v-if="selectedNode.nodeType === 2">
            <el-input v-model="selectedNode.messageTemplate" type="textarea" :rows="3" :disabled="isPublished" />
          </el-form-item>
          <el-form-item label="发送时间" v-if="selectedNode.nodeType === 2">
            <el-input v-model="selectedNode.sendTime" :disabled="isPublished" placeholder="例如：09:00" />
          </el-form-item>
          <el-form-item label="条件表达式" v-if="selectedNode.nodeType === 3">
            <el-input v-model="selectedNode.conditionExpr" type="textarea" :rows="3" :disabled="isPublished" />
          </el-form-item>
          <el-form-item label="AI 场景" v-if="aiSceneOptions.length">
            <el-select v-model="selectedNode.sceneCode" :disabled="isPublished" placeholder="按场景选模型" clearable size="small" style="width:100%">
              <el-option v-for="s in aiSceneOptions" :key="s.sceneCode" :label="`${s.sceneName} (${s.sceneCode})`" :value="s.sceneCode" />
            </el-select>
          </el-form-item>
          <el-form-item label="指定模型" v-if="aiModelOptions.length">
            <el-select v-model="selectedNode.modelName" :disabled="isPublished" placeholder="覆盖场景，强制指定" clearable size="small" style="width:100%">
              <el-option v-for="m in aiModelOptions" :key="m.modelName" :label="`${m.modelName} [${m.providerName||m.provider}]`" :value="m.modelName" />
            </el-select>
          </el-form-item>
          <el-form-item label="节点配置">
            <el-input v-model="selectedNode.nodeConfig" type="textarea" :rows="4" placeholder="JSON格式" :disabled="isPublished" />
          </el-form-item>
          <el-form-item>
            <el-button type="danger" size="small" :disabled="isPublished" @click="deleteSelectedNode">删除节点</el-button>
          </el-form-item>
        </el-form>

        <!-- 连线属性表单 -->
        <div class="panel-subtitle" v-if="selectedEdge">连线配置</div>
        <el-form v-if="selectedEdge" label-width="80px" size="small" class="property-form">
          <el-form-item label="连线标签">
            <el-input v-model="selectedEdge.edgeLabel" :disabled="isPublished" />
          </el-form-item>
          <el-form-item label="连线颜色">
            <el-color-picker v-model="selectedEdge.edgeColor" :disabled="isPublished" />
          </el-form-item>
          <el-form-item label="条件表达式">
            <el-input v-model="selectedEdge.conditionExpr" type="textarea" :rows="3" :disabled="isPublished" />
          </el-form-item>
          <el-form-item>
            <el-button type="danger" size="small" :disabled="isPublished" @click="deleteSelectedEdge">删除连线</el-button>
          </el-form-item>
        </el-form>
      </div>
    </div>
    </div>

    <!-- 列表模式 -->
    <div v-if="viewMode === 'list'" class="list-container" v-show="!isFullscreen">
      <el-card shadow="never">
        <div slot="header" class="card-header">
          <span>节点列表（{{ nodes.length }}个）</span>
          <el-button type="primary" size="small" icon="el-icon-plus" @click="showAddNodeDialog">添加节点</el-button>
        </div>
        <el-timeline v-if="nodes.length > 0">
          <el-timeline-item
            v-for="(node, index) in sortedNodes"
            :key="node.nodeCode"
            :type="getNodeTimelineType(node.nodeType)"
            :icon="getNodeTimelineIcon(node.nodeType)"
          >
            <el-card shadow="hover" class="timeline-node-card">
              <div class="node-header">
                <div class="node-title">
                  <span class="node-name">{{ node.nodeName }}</span>
                  <el-tag size="mini" :type="getNodeTimelineType(node.nodeType)">{{ getNodeTypeName(node.nodeType) }}</el-tag>
                  <el-tag size="mini" type="info">{{ node.nodeCode }}</el-tag>
                </div>
                <div class="node-actions">
                  <el-button type="text" size="small" icon="el-icon-edit" @click="selectNode(node); viewMode = 'canvas'">编辑</el-button>
                  <el-button type="text" size="small" icon="el-icon-delete" style="color: #f56c6c" :disabled="isPublished" @click="deleteListNode(node)">删除</el-button>
                </div>
              </div>
              <div class="node-body">
                <p v-if="node.messageTemplate" class="node-preview">{{ truncateText(node.messageTemplate, 100) }}</p>
                <p class="node-next">
                  <i class="el-icon-right"></i>
                  下一步: {{ node.nextNodeCode || '无' }}
                </p>
              </div>
            </el-card>
          </el-timeline-item>
        </el-timeline>
        <el-empty v-else description="暂无节点数据" />
      </el-card>
    </div>
  </div>
</template>

<script>
import { getWorkflowTemplateDetail, saveWorkflowCanvas } from '@/api/company/workflowLobster'
import request from '@/utils/request'

export default {
  name: 'WorkflowCanvasEditor',
  components: {},
  data() {
    return {
      loading: false,
      saving: false,
      viewMode: 'canvas',
      templateId: null,
      templateData: {}, // 初始化为空对象，避免null访问错误
      nodes: [],
      edges: [],
      variables: [],
      editorForm: {
        templateName: '',
        industryType: '',
        description: '',
        variables: [],
        nodes: [],
        edges: [] // 添加edges字段
      },
      // 画布控制
      scale: 1,
      canvasOffset: { x: 0, y: 0 },
      canvasSize: { width: 5000, height: 5000 },
      isDraggingCanvas: false,
      dragStart: { x: 0, y: 0 },
      // 节点拖拽
      isDraggingNode: false,
      draggingNode: null,
      dragNodeOffset: { x: 0, y: 0 }, // 节点内的偏移量，避免跳动
      canvasRect: null, // 缓存画布位置，提升拖动性能
      dragAnimationFrame: null, // requestAnimationFrame ID，避免频繁更新
      pendingDragClient: null, // 最新鼠标位置，供requestAnimationFrame消费
      hasDragged: false, // 标记是否发生了拖动，用于区分click和drag
      // 连线绘制
      drawingEdge: null,
      edgeStartNode: null,
      edgeStartPort: null,
      mousePos: { x: 0, y: 0 },
      // 选中状态
      selectedNode: null,
      selectedEdge: null,
      routeStatus: null,
      // AI 场景与模型列表（用于节点配置抽屉的选择器）
      aiSceneOptions: [],
      aiModelOptions: [],
      // 全屏状态
      isFullscreen: false,
      // 网格配置
      gridSize: 20, // 网格大小，用于对齐
      // 节点类型配置
      nodeCategories: [
        {
          key: 'basic',
          name: '基础节点',
          types: [
            { type: 1, label: '开始节点', icon: 'el-icon-video-play', color: '#22c55e' },
            { type: 5, label: '结束节点', icon: 'el-icon-video-pause', color: '#ef4444' }
          ]
        },
        {
          key: 'action',
          name: '动作节点',
          types: [
            { type: 2, label: '消息节点', icon: 'el-icon-message', color: '#3b82f6' },
            { type: 4, label: '等待节点', icon: 'el-icon-time', color: '#8b5cf6' },
            { type: 6, label: 'API节点', icon: 'el-icon-link', color: '#6366f1' }
          ]
        },
        {
          key: 'logic',
          name: '逻辑节点',
          types: [
            { type: 3, label: '判断节点', icon: 'el-icon-question', color: '#f59e0b' }
          ]
        },
        {
          key: 'ecommerce',
          name: '电商节点',
          types: [
            { type: 7, label: '购物车节点', icon: 'el-icon-goods', color: '#22c55e' },
            { type: 8, label: '优惠券节点', icon: 'el-icon-tickets', color: '#f59e0b' },
            { type: 9, label: '标签节点', icon: 'el-icon-price-tag', color: '#ec4899' },
            { type: 10, label: '赠礼节点', icon: 'el-icon-present', color: '#ec4899' }
          ]
        },
        {
          key: 'crm',
          name: '客户节点',
          types: [
            { type: 11, label: '文档节点', icon: 'el-icon-document', color: '#6366f1' },
            { type: 12, label: '用户节点', icon: 'el-icon-user-solid', color: '#8b5cf6' },
            { type: 13, label: '复购节点', icon: 'el-icon-refresh-right', color: '#14b8a6' },
            { type: 14, label: '智能API节点', icon: 'el-icon-magic-stick', color: '#3b82f6' }
          ]
        },
        {
          key: 'analysis',
          name: '分析节点',
          types: [
            { type: 13, label: '数据分析节点', icon: 'el-icon-data-line', color: '#14b8a6' },
            { type: 14, label: 'AI节点', icon: 'el-icon-magic-stick', color: '#ec4899' }
          ]
        }
      ]
    }
  },
  created() {
    this.templateId = this.$route.params.id
    this.routeStatus = this.$route.query && this.$route.query.status !== undefined
      ? Number(this.$route.query.status)
      : null
    this.fetchTemplateDetail()
  },
  computed: {
    isPublished() {
      const detailStatus = this.templateData && this.templateData.status !== undefined
        ? Number(this.templateData.status)
        : null
      const status = detailStatus !== null && !Number.isNaN(detailStatus) ? detailStatus : this.routeStatus
      return Number(status) === 1
    },
    sortedNodes() {
      return [...this.nodes].sort((a, b) => (a.sortNo || 0) - (b.sortNo || 0))
    }
  },
  mounted() {
    // 自动聚焦到画布以接收键盘事件
    this.$nextTick(() => {
      if (this.$refs.canvasContainer) {
        this.$refs.canvasContainer.focus()
      }
    })
    document.addEventListener('fullscreenchange', this.onFullscreenChange)
    document.addEventListener('webkitfullscreenchange', this.onFullscreenChange)
    // 加载 AI 场景与模型供节点配置选择
    this.loadAiSceneAndModelOptions()
  },
  beforeDestroy() {
    // 清理事件监听器，防止内存泄漏
    document.removeEventListener('mousemove', this.onNodeDragMove)
    document.removeEventListener('mouseup', this.onNodeDragEnd)
    document.removeEventListener('mousemove', this.onEdgeDrawMove)
    document.removeEventListener('mouseup', this.onEdgeDrawEnd)
    document.removeEventListener('fullscreenchange', this.onFullscreenChange)
    document.removeEventListener('webkitfullscreenchange', this.onFullscreenChange)
    if (this.dragAnimationFrame) {
      cancelAnimationFrame(this.dragAnimationFrame)
      this.dragAnimationFrame = null
    }
  },
  methods: {
    ensureEditable() {
      if (!this.isPublished) return true
      this.$message.warning('模板已发布，无法修改')
      return false
    },
    async fetchTemplateDetail() {
      this.loading = true
      try {
        const res = await getWorkflowTemplateDetail(this.templateId)
        const data = res && res.data ? res.data : {}
        if (data && data.status !== undefined && data.status !== null && !Number.isNaN(Number(data.status))) {
          this.routeStatus = Number(data.status)
        }
        this.templateData = data
        this.nodes = Array.isArray(data.nodes) ? data.nodes : []
        this.edges = Array.isArray(data.edges) ? data.edges : []
        this.variables = Array.isArray(data.variables) ? data.variables : []
        
        // 自动布局节点，避免堆积
        this.autoLayoutNodes()
        
        this.editorForm = {
          templateName: data.templateName || '',
          industryType: data.industryType || 'general',
          description: data.description || '',
          variables: JSON.parse(JSON.stringify(this.variables)),
          nodes: JSON.parse(JSON.stringify(this.nodes)),
          edges: JSON.parse(JSON.stringify(this.edges)) // 添加edges数据
        }
      } catch (e) {
        this.$message.error(e.message || '获取模板详情失败')
      } finally {
        this.loading = false
      }
    },
    
    /**
     * 自动布局节点
     * 根据节点的sortNo和连接关系，自动计算位置，避免堆积
     * 每5个节点进行换行排列
     */
    autoLayoutNodes() {
      if (!this.nodes || this.nodes.length === 0) {
        this.$message.warning('没有可布局的节点')
        return
      }
      
      // 配置参数
      const nodeWidth = 200
      const nodeHeight = 80
      const horizontalGap = 56   // 水平间距
      const verticalGap = 72     // 垂直间距（行间距）
      const startX = 100         // 起始X坐标
      const startY = 100         // 起始Y坐标
      const nodesPerRow = 4      // 每行节点数
      
      // 按照连接关系和sortNo排序节点
      const sortedNodes = this.sortNodesForLayout()
      
      // 按照每行4个节点进行布局
      sortedNodes.forEach((node, index) => {
        // 计算行号和列号
        const row = Math.floor(index / nodesPerRow)
        const col = index % nodesPerRow
        
        // 计算位置
        node.positionX = startX + col * (nodeWidth + horizontalGap)
        node.positionY = startY + row * (nodeHeight + verticalGap)
        
        // 设置节点尺寸
        if (!node.width) node.width = nodeWidth
        if (!node.height) node.height = nodeHeight
      })
      
      this.$message.success(`节点已自动排列（每行${nodesPerRow}个）`)
      console.log('节点自动布局完成', sortedNodes.map(n => ({
        code: n.nodeCode,
        name: n.nodeName,
        x: n.positionX,
        y: n.positionY
      })))
    },
    
    /**
     * 对节点进行排序，确保连线关系合理的节点排列在一起
     */
    sortNodesForLayout() {
      if (!this.nodes || this.nodes.length === 0) return []
      
      // 构建节点映射表
      const nodeMap = {}
      this.nodes.forEach(node => {
        nodeMap[node.nodeCode] = node
      })
      
      // 使用BFS遍历，确保连接的节点相邻
      const ordered = []
      const visited = new Set()
      
      // 从START节点开始
      const startNode = this.nodes.find(n => n.nodeType === 1)
      if (startNode) {
        const queue = [startNode.nodeCode]
        visited.add(startNode.nodeCode)
        
        while (queue.length > 0) {
          const code = queue.shift()
          ordered.push(nodeMap[code])
          
          // 查找所有从当前节点出发的边
          this.edges.forEach(edge => {
            if (edge.sourceNodeCode === code && !visited.has(edge.targetNodeCode)) {
              visited.add(edge.targetNodeCode)
              queue.push(edge.targetNodeCode)
            }
          })
        }
      }
      
      // 添加未访问的节点（根据sortNo排序）
      const unvisitedNodes = this.nodes
        .filter(n => !visited.has(n.nodeCode))
        .sort((a, b) => (a.sortNo || 0) - (b.sortNo || 0))
      
      return [...ordered, ...unvisitedNodes]
    },
    goBack() {
      this.$router.back()
    },
    // 全屏切换
    toggleFullscreen() {
      if (!this.isFullscreen) {
        document.body.classList.add('is-visual-fullscreen')
        const el = document.documentElement
        if (el.requestFullscreen) {
          el.requestFullscreen()
        } else if (el.webkitRequestFullscreen) {
          el.webkitRequestFullscreen()
        } else if (el.msRequestFullscreen) {
          el.msRequestFullscreen()
        }
      } else {
        document.body.classList.remove('is-visual-fullscreen')
        if (document.exitFullscreen) {
          document.exitFullscreen()
        } else if (document.webkitExitFullscreen) {
          document.webkitExitFullscreen()
        } else if (document.msExitFullscreen) {
          document.msExitFullscreen()
        }
      }
    },
    onFullscreenChange() {
      this.isFullscreen = !!document.fullscreenElement
      if (!document.fullscreenElement) {
        document.body.classList.remove('is-visual-fullscreen')
      }
    },
    // 加载 AI 场景与模型清单（用于节点配置抽屉的下拉选择器）
    async loadAiSceneAndModelOptions() {
      try {
        const scenesResp = await request({ url: '/admin/aiScene/list', method: 'get', params: { pageNum: 1, pageSize: 100 } })
        const sceneRows = (scenesResp && (scenesResp.rows || scenesResp.data || scenesResp.list)) || []
        this.aiSceneOptions = sceneRows.map(s => ({
          sceneCode: s.sceneCode || s.code,
          sceneName: s.sceneName || s.name
        })).filter(s => s.sceneCode)
      } catch (e) {
        console.warn('[visual.vue] 加载 AI 场景失败', e && e.message)
        this.aiSceneOptions = []
      }
      try {
        const modelsResp = await request({ url: '/admin/aiModel/list', method: 'get', params: { pageNum: 1, pageSize: 200, enabled: 1 } })
        const modelRows = (modelsResp && (modelsResp.rows || modelsResp.data || modelsResp.list)) || []
        this.aiModelOptions = modelRows.map(m => ({
          modelName: m.modelName,
          provider: m.provider,
          providerName: m.providerName
        })).filter(m => m.modelName)
      } catch (e) {
        console.warn('[visual.vue] 加载 AI 模型失败', e && e.message)
        this.aiModelOptions = []
      }
    },
    // 时间线节点类型颜色
    getNodeTimelineType(type) {
      const map = {
        1: 'success', 2: 'primary', 3: 'warning', 4: 'info', 5: 'danger', 6: '',
        7: 'success', 8: 'warning', 9: '', 10: 'primary',
        11: 'info', 12: '', 13: 'success', 14: 'primary'
      }
      return map[type] || 'info'
    },
    getNodeTimelineIcon(type) {
      const map = {
        1: 'el-icon-video-play', 2: 'el-icon-message', 3: 'el-icon-question',
        4: 'el-icon-time', 5: 'el-icon-video-pause', 6: 'el-icon-link',
        7: 'el-icon-goods', 8: 'el-icon-tickets', 9: 'el-icon-price-tag', 10: 'el-icon-present',
        11: 'el-icon-document', 12: 'el-icon-user-solid', 13: 'el-icon-data-line', 14: 'el-icon-magic-stick'
      }
      return map[type] || 'el-icon-connection'
    },
    // 列表视图添加节点
    showAddNodeDialog() {
      this.$message.info('请切换到画布视图拖拽添加节点')
      this.viewMode = 'canvas'
    },
    // 列表视图删除节点
    deleteListNode(node) {
      if (this.isPublished) {
        this.$message.warning('模板已发布，无法修改')
        return
      }
      this.$confirm('确定删除该节点吗？', '提示', { type: 'warning' }).then(() => {
        const index = this.nodes.findIndex(n => n.nodeCode === node.nodeCode)
        if (index > -1) {
          this.nodes.splice(index, 1)
          this.edges = this.edges.filter(e =>
            e.sourceNodeCode !== node.nodeCode && e.targetNodeCode !== node.nodeCode
          )
        }
      }).catch(() => {})
    },
    // 文本截断
    truncateText(str, len) {
      if (!str) return ''
      return str.length > len ? str.substring(0, len) + '...' : str
    },
    // 缩放控制
    zoomIn() {
      this.scale = Math.min(this.scale + 0.1, 2)
    },
    zoomOut() {
      this.scale = Math.max(this.scale - 0.1, 0.5)
    },
    fitView() {
      this.scale = 1
      this.canvasOffset = { x: 0, y: 0 }
    },
    // 鼠标滚轮缩放
    onMouseWheel(event) {
      const delta = event.deltaY > 0 ? -0.05 : 0.05
      this.scale = Math.max(0.5, Math.min(2, this.scale + delta))
    },
    // 节点类型辅助方法 - 现代配色
    getNodeAccentColor(type) {
      const map = {
        1: '#22c55e', 2: '#3b82f6', 3: '#f59e0b',
        4: '#8b5cf6', 5: '#ef4444', 6: '#6366f1',
        7: '#22c55e', 8: '#f59e0b', 9: '#ec4899', 10: '#ec4899',
        11: '#6366f1', 12: '#8b5cf6', 13: '#14b8a6', 14: '#ec4899'
      }
      return map[type] || '#6b7280'
    },
    getNodeAccentBg(type) {
      const map = {
        1: '#22c55e', 2: '#3b82f6', 3: '#f59e0b',
        4: '#8b5cf6', 5: '#ef4444', 6: '#6366f1',
        7: '#22c55e', 8: '#f59e0b', 9: '#ec4899', 10: '#ec4899',
        11: '#6366f1', 12: '#8b5cf6', 13: '#14b8a6', 14: '#ec4899'
      }
      return map[type] || '#6b7280'
    },
    getNodeBottomDesc(node) {
      const typeName = this.getNodeTypeName(node.nodeType)
      if (node.nodeType === 2 && node.sendTime) {
        return typeName + ' · ' + this.formatSendTime(node.sendTime)
      }
      const descMap = {
        1: '工作流起始节点',
        2: '发送消息给客户',
        3: '条件判断分支',
        4: '等待指定时长',
        5: '工作流结束节点',
        6: '调用外部API接口',
        7: '购物车营销动作',
        8: '优惠券发放与核销',
        9: '客户标签管理',
        10: '赠礼营销动作',
        11: '文档处理与生成',
        12: '用户信息管理',
        13: '数据分析与报表',
        14: 'AI智能处理'
      }
      return descMap[node.nodeType] || typeName
    },
    getTypeTagWidth(node) {
      const name = this.getNodeTypeName(node.nodeType)
      return name.replace(/[^\x00-\xff]/g, 'aa').length * 5.5
    },
    getNodeIcon(type) {
      const icons = {
        1: '\u25b6',  2: '\u2709',  3: '\u2699',
        4: '\u23f3',  5: '\u25a0',  6: '\u21c4',
        7: '\u266a',  8: '\u2726',  9: '\u2665', 10: '\u2605',
        11: '\u270e', 12: '\u263a', 13: '\u2191', 14: '\u2603'
      }
      return icons[type] || '\u25cf'
    },
    getNodeTypeName(type) {
      const names = {
        1: '开始节点', 2: '消息节点', 3: '判断节点',
        4: '等待节点', 5: '结束节点', 6: 'API节点',
        7: '购物车节点', 8: '优惠券节点', 9: '标签节点', 10: '赠礼节点',
        11: '文档节点', 12: '用户节点', 13: '数据分析节点', 14: 'AI节点'
      }
      return names[type] || '未知类型'
    },
    formatSendTime(sendTime) {
      if (!sendTime) return ''
      if (typeof sendTime === 'string') return sendTime
      return String(sendTime)
    },
    // 拖拽添加节点
    onDragStart(event, nodeType) {
      if (!this.ensureEditable()) {
        event.preventDefault()
        return
      }
      event.dataTransfer.setData('nodeType', JSON.stringify(nodeType))
      event.dataTransfer.effectAllowed = 'copy'
    },
    onDrop(event) {
      if (!this.ensureEditable()) return
      event.preventDefault()
      const nodeType = JSON.parse(event.dataTransfer.getData('nodeType'))
      const rect = this.$refs.canvasContainer.getBoundingClientRect()
      const x = (event.clientX - rect.left - this.canvasOffset.x) / this.scale
      const y = (event.clientY - rect.top - this.canvasOffset.y) / this.scale

      // 对齐到网格
      const snappedX = Math.round((x - 100) / this.gridSize) * this.gridSize
      const snappedY = Math.round((y - 40) / this.gridSize) * this.gridSize

      const nodeCode = 'NODE_' + Date.now()
      const newNode = {
        nodeCode: nodeCode,
        nodeName: nodeType.label,
        nodeType: nodeType.type,
        positionX: snappedX,
        positionY: snappedY,
        width: 200,
        height: 80,
        sortNo: this.nodes.length + 1,
        nodeConfig: '{}',
        messageTemplate: '',
        sendTime: '',
        conditionExpr: '',
        nextNodeCode: '',
        sceneCode: '',
        modelName: '',
        delFlag: 0
      }
      this.nodes.push(newNode)
      this.$message.success('已添加节点：' + nodeType.label)
    },
    // 画布拖拽 - 优化版
    onCanvasMouseDown(event) {
      // 只有在空白区域点击时才拖拽画布
      if (event.target === this.$refs.canvasSvg || event.target.tagName === 'rect') {
        this.isDraggingCanvas = true
        this.dragStart = { 
          x: event.clientX - this.canvasOffset.x, 
          y: event.clientY - this.canvasOffset.y 
        }
        this.clearSelection()
        // 改变鼠标样式
        this.$refs.canvasContainer.style.cursor = 'grabbing'
      }
    },
    onCanvasMouseMove(event) {
      // 更新鼠标位置（用于连线预览）
      if (this.drawingEdge) {
        this.mousePos = {
          x: event.offsetX,
          y: event.offsetY
        }
      }
      
      // 画布拖动
      if (this.isDraggingCanvas) {
        this.canvasOffset.x = event.clientX - this.dragStart.x
        this.canvasOffset.y = event.clientY - this.dragStart.y
      }
    },
    onCanvasMouseUp(event) {
      this.isDraggingCanvas = false
      this.isDraggingNode = false
      this.draggingNode = null
      // 恢复鼠标样式
      if (this.$refs.canvasContainer) {
        this.$refs.canvasContainer.style.cursor = 'grab'
      }
      // 恢复文本选择
      document.body.style.userSelect = ''
      document.body.style.webkitUserSelect = ''
      // 完成连线绘制
      if (this.drawingEdge && this.edgeStartNode) {
        // 查找鼠标位置下的节点作为目标节点
        const targetNode = this.findNodeAtPosition(event)
        if (targetNode && targetNode.nodeCode !== this.edgeStartNode.nodeCode) {
          this.createEdge(this.edgeStartNode, this.edgeStartPort, targetNode)
        }
        this.drawingEdge = null
        this.edgeStartNode = null
        this.edgeStartPort = null
      }
    },
    // 节点拖动开始
    onNodeMouseDown(event, node) {
      if (!this.ensureEditable()) return
      event.preventDefault()
      event.stopPropagation()
      
      this.isDraggingNode = true
      this.draggingNode = node
      this.selectedNode = node
      this.hasDragged = false // 重置拖动标记
      
      // 计算鼠标相对于节点左上角的偏移
      const nodeX = node.positionX || 0
      const nodeY = node.positionY || 0
      
      // 缓存画布位置，避免重复计算
      this.canvasRect = this.$refs.canvasContainer.getBoundingClientRect()
      const mouseX = (event.clientX - this.canvasRect.left - this.canvasOffset.x) / this.scale
      const mouseY = (event.clientY - this.canvasRect.top - this.canvasOffset.y) / this.scale
      
      this.dragNodeOffset = {
        x: mouseX - nodeX,
        y: mouseY - nodeY
      }
      this.pendingDragClient = null
      
      // 添加全局事件监听，使用passive提升性能
      document.addEventListener('mousemove', this.onNodeDragMove, { passive: true })
      document.addEventListener('mouseup', this.onNodeDragEnd)
      document.body.style.cursor = 'move'
      document.body.style.userSelect = 'none'
    },
    
    // 节点拖动中：仅记录坐标，实际更新交给requestAnimationFrame
    onNodeDragMove(event) {
      if (!this.isDraggingNode || !this.draggingNode) return
      this.pendingDragClient = { x: event.clientX, y: event.clientY }
      this.scheduleNodeDragUpdate()
    },

    scheduleNodeDragUpdate() {
      if (this.dragAnimationFrame) return
      this.dragAnimationFrame = requestAnimationFrame(() => {
        this.dragAnimationFrame = null
        this.applyNodeDragPosition()
      })
    },

    applyNodeDragPosition() {
      if (!this.isDraggingNode || !this.draggingNode || !this.pendingDragClient) return
      const canvasRect = this.canvasRect || this.$refs.canvasContainer.getBoundingClientRect()
      const { x: clientX, y: clientY } = this.pendingDragClient
      this.pendingDragClient = null

      // 计算新的节点位置（光标位置 - 偏移量）
      let newX = (clientX - canvasRect.left - this.canvasOffset.x) / this.scale - this.dragNodeOffset.x
      let newY = (clientY - canvasRect.top - this.canvasOffset.y) / this.scale - this.dragNodeOffset.y

      // 边界限制
      const nodeWidth = this.draggingNode.width || 200
      const nodeHeight = this.draggingNode.height || 80
      newX = Math.max(0, Math.min(newX, this.canvasSize.width - nodeWidth))
      newY = Math.max(0, Math.min(newY, this.canvasSize.height - nodeHeight))

      this.draggingNode.positionX = newX
      this.draggingNode.positionY = newY
      this.hasDragged = true
    },
    
    // 节点拖动结束
    onNodeDragEnd() {
      // 结束前应用最后一次鼠标位置，避免“慢一拍”
      this.applyNodeDragPosition()
      if (this.dragAnimationFrame) {
        cancelAnimationFrame(this.dragAnimationFrame)
        this.dragAnimationFrame = null
      }

      // 拖动结束时进行网格对齐
      if (this.draggingNode) {
        this.draggingNode.positionX = Math.round(this.draggingNode.positionX / this.gridSize) * this.gridSize
        this.draggingNode.positionY = Math.round(this.draggingNode.positionY / this.gridSize) * this.gridSize
      }
      
      this.isDraggingNode = false
      this.draggingNode = null
      this.pendingDragClient = null
      
      // 移除全局事件监听
      document.removeEventListener('mousemove', this.onNodeDragMove)
      document.removeEventListener('mouseup', this.onNodeDragEnd)
      document.body.style.cursor = ''
      document.body.style.userSelect = ''
    },
    selectNode(node) {
      // 如果刚发生了拖动，不处理click事件
      if (this.hasDragged) {
        this.hasDragged = false
        return
      }
      
      this.selectedNode = node
      this.selectedEdge = null
    },
    clearSelection() {
      this.selectedNode = null
      this.selectedEdge = null
    },
    deleteSelectedNode() {
      if (!this.ensureEditable()) return
      if (!this.selectedNode) return
      const index = this.nodes.findIndex(n => n.nodeCode === this.selectedNode.nodeCode)
      if (index > -1) {
        this.nodes.splice(index, 1)
        // 删除相关连线
        this.edges = this.edges.filter(e =>
          e.sourceNodeCode !== this.selectedNode.nodeCode &&
          e.targetNodeCode !== this.selectedNode.nodeCode
        )
        this.selectedNode = null
      }
    },
    // 开始绘制连线
    startDrawEdge(event, node, port) {
      if (!this.ensureEditable()) return
      console.log('开始连线:', node.nodeName, port)
      event.preventDefault()
      event.stopPropagation()
      
      this.drawingEdge = true
      this.edgeStartNode = node
      this.edgeStartPort = port
      
      // 记录鼠标位置用于绘制预览线
      const canvasRect = this.$refs.canvasContainer.getBoundingClientRect()
      this.mousePos = {
        x: event.clientX - canvasRect.left,
        y: event.clientY - canvasRect.top
      }
      
      // 添加全局事件监听
      document.addEventListener('mousemove', this.onEdgeDrawMove)
      document.addEventListener('mouseup', this.onEdgeDrawEnd)
      document.body.style.cursor = 'crosshair'
      document.body.style.userSelect = 'none'
    },
    
    // 连线绘制中
    onEdgeDrawMove(event) {
      if (!this.drawingEdge) return
      
      const canvasRect = this.$refs.canvasContainer.getBoundingClientRect()
      this.mousePos = {
        x: event.clientX - canvasRect.left,
        y: event.clientY - canvasRect.top
      }
    },
    
    // 连线绘制结束
    onEdgeDrawEnd(event) {
      console.log('结束连线')
      
      if (this.drawingEdge && this.edgeStartNode) {
        // 查找目标节点
        const targetNode = this.findNodeAtPosition(event)
        if (targetNode && targetNode.nodeCode !== this.edgeStartNode.nodeCode) {
          this.createEdge(this.edgeStartNode, this.edgeStartPort, targetNode)
          this.$message.success('连线创建成功')
        }
      }
      
      // 清理状态
      this.drawingEdge = false
      this.edgeStartNode = null
      this.edgeStartPort = null
      
      // 移除全局事件监听
      document.removeEventListener('mousemove', this.onEdgeDrawMove)
      document.removeEventListener('mouseup', this.onEdgeDrawEnd)
      document.body.style.cursor = ''
      document.body.style.userSelect = ''
    },
    selectEdge(edge) {
      this.selectedEdge = edge
      this.selectedNode = null
    },
    deleteSelectedEdge() {
      if (!this.ensureEditable()) return
      if (!this.selectedEdge) return
      const index = this.edges.findIndex(e => e.edgeKey === this.selectedEdge.edgeKey)
      if (index > -1) {
        this.edges.splice(index, 1)
        this.selectedEdge = null
      }
    },
    getEdgePath(edge) {
      const sourceNode = this.nodes.find(n => n.nodeCode === edge.sourceNodeCode)
      const targetNode = this.nodes.find(n => n.nodeCode === edge.targetNodeCode)
      if (!sourceNode || !targetNode) return ''

      // 根据连接点计算起点和终点
      const { x: sx, y: sy } = this.getPortPosition(sourceNode, edge.sourcePort || 'right')
      const { x: tx, y: ty } = this.getPortPosition(targetNode, edge.targetPort || 'left')

      // 使用贝塞尔曲线，根据连接点方向计算控制点
      const dx = Math.abs(tx - sx)
      const dy = Math.abs(ty - sy)
      
      // 控制点偏移量，使曲线更平滑
      const offset = Math.max(50, Math.min(dx * 0.5, dy * 0.5))
      
      let cx1, cy1, cx2, cy2
      
      // 根据源连接点方向设置控制点
      switch(edge.sourcePort) {
        case 'right':
          cx1 = sx + offset
          cy1 = sy
          break
        case 'left':
          cx1 = sx - offset
          cy1 = sy
          break
        case 'bottom':
          cx1 = sx
          cy1 = sy + offset
          break
        case 'top':
        default:
          cx1 = sx
          cy1 = sy - offset
          break
      }
      
      // 根据目标连接点方向设置控制点
      switch(edge.targetPort) {
        case 'right':
          cx2 = tx + offset
          cy2 = ty
          break
        case 'left':
          cx2 = tx - offset
          cy2 = ty
          break
        case 'bottom':
          cx2 = tx
          cy2 = ty + offset
          break
        case 'top':
        default:
          cx2 = tx
          cy2 = ty - offset
          break
      }

      // 返回贝塞尔曲线路径
      return `M ${sx} ${sy} C ${cx1} ${cy1}, ${cx2} ${cy2}, ${tx} ${ty}`
    },
    // 获取连接点的位置
    getPortPosition(node, port) {
      const x = node.positionX || 0
      const y = node.positionY || 0
      const w = node.width || 200
      const h = node.height || 80
      
      switch(port) {
        case 'top':
          return { x: x + w / 2, y: y }
        case 'bottom':
          return { x: x + w / 2, y: y + h }
        case 'left':
          return { x: x, y: y + h / 2 }
        case 'right':
        default:
          return { x: x + w, y: y + h / 2 }
      }
    },
    getEdgeLabelPos(edge) {
      const sourceNode = this.nodes.find(n => n.nodeCode === edge.sourceNodeCode)
      const targetNode = this.nodes.find(n => n.nodeCode === edge.targetNodeCode)
      if (!sourceNode || !targetNode) return { x: 0, y: 0 }

      // 根据连接点计算起点和终点
      const { x: sx, y: sy } = this.getPortPosition(sourceNode, edge.sourcePort || 'right')
      const { x: tx, y: ty } = this.getPortPosition(targetNode, edge.targetPort || 'left')

      // 标签位置在曲线中点上方
      return {
        x: (sx + tx) / 2,
        y: (sy + ty) / 2 - 10
      }
    },
    getDrawingEdgePath() {
      if (!this.edgeStartNode) return ''
      const startPos = this.getPortPosition(this.edgeStartNode, this.edgeStartPort)
      const mx = (this.mousePos.x - this.canvasOffset.x) / this.scale
      const my = (this.mousePos.y - this.canvasOffset.y) / this.scale
      
      // 使用简单的直线作为预览，贝塞尔曲线计算较复杂
      return `M ${startPos.x} ${startPos.y} L ${mx} ${my}`
    },
    handleDelete() {
      if (!this.ensureEditable()) return
      if (this.selectedNode) {
        this.deleteSelectedNode()
      } else if (this.selectedEdge) {
        this.deleteSelectedEdge()
      }
    },
    onCanvasClick(event) {
      // 如果刚发生了拖动，不处理click事件
      if (this.hasDragged) {
        this.hasDragged = false
        return
      }
      
      // 点击空白区域取消选中
      // 检查点击的是否是SVG背景或网格矩形
      const target = event.target
      const isSvgBackground = target === this.$refs.canvasSvg || 
                             (target.tagName === 'rect' && target.getAttribute('fill') === 'url(#dot-grid)')
      
      if (isSvgBackground) {
        this.clearSelection()
      }
    },
    // 辅助方法：查找鼠标位置下的节点
    findNodeAtPosition(event) {
      const rect = this.$refs.canvasContainer.getBoundingClientRect()
      const x = (event.clientX - rect.left - this.canvasOffset.x) / this.scale
      const y = (event.clientY - rect.top - this.canvasOffset.y) / this.scale

      for (const node of this.nodes) {
        const nx = node.positionX || 0
        const ny = node.positionY || 0
        const nw = node.width || 200
        const nh = node.height || 80
        if (x >= nx && x <= nx + nw && y >= ny && y <= ny + nh) {
          return node
        }
      }
      return null
    },
    // 创建连线
    createEdge(sourceNode, sourcePort, targetNode) {
      const edgeKey = 'EDGE_' + Date.now()
      const newEdge = {
        edgeKey: edgeKey,
        sourceNodeCode: sourceNode.nodeCode,
        targetNodeCode: targetNode.nodeCode,
        sourcePort: sourcePort || 'right',
        targetPort: 'left',
        edgeLabel: '',
        edgeColor: '#b0b8c1',
        conditionExpr: '',
        sortNo: this.edges.length + 1,
        delFlag: 0
      }
      this.edges.push(newEdge)
    },
    // 保存画布
    async saveCanvas() {
      if (!this.ensureEditable()) return
      if (!this.templateData || !this.templateData.templateName) {
        this.$message.warning('模板数据未加载完成')
        return
      }
      
      this.saving = true
      try {
        const payload = {
          templateName: this.templateData.templateName,
          industryType: this.templateData.industryType,
          description: this.templateData.description,
          canvasData: JSON.stringify({
            scale: this.scale,
            offset: this.canvasOffset
          }),
          variables: this.variables,
          nodes: this.nodes,
          edges: this.edges
        }
        await saveWorkflowCanvas(this.templateId, payload)
        this.$message.success('保存成功')
        await this.fetchTemplateDetail()
      } catch (e) {
        this.$message.error(e.message || '保存失败')
      } finally {
        this.saving = false
      }
    },
  }
}
</script>

<style scoped lang="scss">
.workflow-canvas-editor {
  height: calc(100vh - 84px);
  display: flex;
  flex-direction: column;
  background: #f0f2f5;
  overflow: hidden;
  padding: 16px;
  box-sizing: border-box;

  .canvas-toolbar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 14px 18px;
    background: #fff;
    border-radius: 12px;
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
    margin-bottom: 10px;

    .toolbar-left {
      display: flex;
      align-items: center;
      gap: 10px;

      .template-name {
        font-size: 16px;
        font-weight: 600;
        color: #303133;
        letter-spacing: 0.2px;
      }
    }

    .toolbar-right {
      display: flex;
      align-items: center;
      gap: 6px;

      .view-switch-group {
        margin-right: 10px;
      }

      ::v-deep .el-button {
        border-radius: 8px;
        padding: 8px 12px;
        min-height: 32px;
      }

      ::v-deep .el-radio-button__inner {
        border-radius: 8px;
        padding: 8px 12px;
        min-height: 32px;
        line-height: 14px;
      }

      ::v-deep .el-divider--vertical {
        margin: 0 4px;
        height: 18px;
        background-color: #e5e7eb;
      }
    }
  }

  // 基本信息卡片
  .info-card {
    margin-bottom: 12px;
    border-radius: 12px;
    border: none;
    box-shadow: 0 1px 8px rgba(0, 0, 0, 0.04);
  }

  // 全屏浮动工具栏
  .fullscreen-toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 8px 16px;
    background: #fff;
    border-bottom: 1px solid #ebeef5;
    height: 48px;
    box-sizing: border-box;
    flex-shrink: 0;

    .fullscreen-title {
      font-size: 15px;
      font-weight: 600;
      color: #303133;
    }
  }

  .canvas-mode {
    flex: 1;
    display: flex;
    flex-direction: column;
    min-height: 0;
  }

  .canvas-meta {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 8px;

    .meta-item {
      min-width: 96px;
      padding: 7px 12px;
      border-radius: 12px;
      background: #fff;
      border: none;
      box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
    }

    .meta-label {
      font-size: 12px;
      color: #909399;
      margin-right: 8px;
    }

    .meta-value {
      font-size: 16px;
      font-weight: 700;
      color: #303133;
    }
  }

  .canvas-workspace {
    flex: 1;
    display: flex;
    overflow: hidden;
    gap: 12px;
    min-height: 0;

    .node-panel {
      width: 240px;
      background: #fff;
      border: none;
      border-radius: 12px;
      padding: 14px;
      overflow-y: auto;
      box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);

      .panel-title {
        font-size: 14px;
        font-weight: 600;
        margin-bottom: 14px;
        color: #303133;
      }

      .node-category {
        margin-bottom: 20px;

        .category-title {
          font-size: 12px;
          color: #909399;
          margin-bottom: 6px;
        }

        .node-list {
          display: flex;
          flex-direction: column;
          gap: 8px;

          .node-item {
            display: flex;
            align-items: center;
            gap: 8px;
            padding: 10px 12px;
            background: #f7f8fa;
            border: 1px solid #ebeef5;
            border-radius: 8px;
            cursor: move;
            transition: all 0.2s ease;

            &.disabled {
              cursor: not-allowed;
              opacity: 0.55;
            }

            &:hover {
              background: #eef5ff;
              border-color: #c6ddf7;
              box-shadow: 0 2px 8px rgba(59, 130, 246, 0.12);
              transform: translateY(-1px);
            }

            &.disabled:hover {
              background: #f7f8fa;
              border-color: #ebeef5;
              box-shadow: none;
            }

            i {
              font-size: 15px;
            }

            span {
              font-size: 12px;
              color: #606266;
            }
          }
        }
      }
    }

    .canvas-container {
      flex: 1;
      position: relative;
      overflow: hidden;
      background: #fff;
      outline: none;
      border-radius: 12px;
      border: none;
      box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);

      .canvas-svg {
        min-width: 100%;
        min-height: 100%;
        cursor: grab;
        background: #f7f8fa;
        user-select: none;
        -webkit-user-select: none;

        &.dragging-canvas {
          cursor: grabbing;
        }

        .edge-group {
          will-change: transform;
          transition: opacity 0.2s;

          .edge-path {
            transition: stroke 0.2s ease, stroke-width 0.2s ease;
          }

          &.selected {
            .edge-path {
              stroke: #409eff !important;
              stroke-width: 2.5;
            }
          }

          &:hover:not(.selected) {
            .edge-path {
              stroke: #86909c !important;
              stroke-width: 2.5;
            }
          }

          .edge-interaction {
            &:hover {
              stroke: rgba(64, 158, 255, 0.06);
            }
          }
        }

        .node-group {
          cursor: move;
          pointer-events: all;
          transition: filter 0.2s, opacity 0.2s;

          .node-rect {
            transition: stroke 0.2s ease, stroke-width 0.2s ease;
          }

          .node-accent {
            transition: opacity 0.2s;
          }

          &.selected {
            .node-rect {
              stroke: #409eff !important;
              stroke-width: 2.5;
            }
          }

          &.dragging {
            transition: none !important;
            .node-rect {
              stroke: #409eff !important;
              stroke-width: 2.5;
            }
            opacity: 0.92;
          }

          &:hover:not(.selected):not(.dragging) {
            filter: url(#node-shadow-hover) !important;
            .node-rect {
              stroke: #c8cdd4;
            }
          }

          .drag-indicator {
            opacity: 0;
            transition: opacity 0.15s;
            user-select: none;
          }

          &:hover .drag-indicator {
            opacity: 1;
          }

          .port {
            opacity: 0;
            transition: all 0.2s ease;
            cursor: crosshair;

            &:hover {
              r: 7;
              fill: #409eff;
              stroke: #409eff;
              stroke-width: 2;
              opacity: 1;
              filter: url(#glow);
            }
          }

          &:hover .port {
            opacity: 0.85;
          }
        }
      }
    }

    .property-panel {
      width: 320px;
      min-width: 320px;
      background: #fff;
      border: none;
      border-radius: 12px;
      overflow-y: auto;
      box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);

      .panel-title {
        padding: 11px 14px;
        font-size: 14px;
        font-weight: 600;
        border-bottom: 1px solid #f0f2f5;
        background: #f8fafc;
        border-radius: 12px 12px 0 0;
      }

      .panel-subtitle {
        margin: 10px 12px 4px;
        font-size: 12px;
        color: #909399;
        font-weight: 600;
      }

      .property-empty {
        margin: 14px 12px;
        padding: 14px 12px;
        border: 1px dashed #dcdfe6;
        border-radius: 8px;
        color: #909399;
        font-size: 13px;
        background: #fafafa;
        text-align: center;
      }

      .property-form {
        padding: 8px 12px 12px;
      }

      ::v-deep .el-form-item__label {
        color: #606266;
        font-weight: 500;
      }

      ::v-deep .el-input__inner,
      ::v-deep .el-textarea__inner,
      ::v-deep .el-select .el-input__inner {
        border-radius: 8px;
      }
    }
  }

  // 列表视图
  .list-container {
    flex: 1;
    overflow-y: auto;
    padding: 0;

    ::v-deep .el-card {
      border-radius: 12px;
      border: none;
      box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
    }

    .timeline-node-card {
      margin-bottom: 8px;
      border-radius: 8px;
      transition: box-shadow 0.2s;

      &:hover {
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
      }

      .node-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 8px;

        .node-title {
          display: flex;
          align-items: center;
          gap: 8px;

          .node-name {
            font-weight: 700;
            font-size: 15px;
            color: #303133;
          }
        }

        .node-actions {
          display: flex;
          gap: 4px;
        }
      }

      .node-body {
        color: #606266;
        font-size: 13px;

        .node-preview {
          margin: 0 0 8px 0;
          line-height: 1.5;
          color: #606266;
        }

        .node-next {
          margin: 0;
          color: #909399;
          font-size: 12px;
          display: flex;
          align-items: center;
          gap: 4px;
        }
      }
    }
  }

  .card-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
}

// 全屏模式样式
.workflow-canvas-editor.is-fullscreen {
  padding: 0;
  height: 100vh;
  overflow: hidden;
  background: #f8f9fb;

  .canvas-workspace {
    gap: 8px;
  }

  .canvas-workspace .canvas-container {
    border-radius: 0;
    box-shadow: none;
  }

  .canvas-workspace .node-panel {
    border-radius: 0 12px 12px 0;
    margin-left: 0;
  }

  .canvas-workspace .property-panel {
    border-radius: 12px 0 0 12px;
    margin-right: 0;
  }
}
</style>

<style lang="scss">
.is-visual-fullscreen {
  .sidebar,
  .header,
  .el-aside,
  .ant-layout-sider {
    display: none !important;
  }

  .main-content,
  .ant-layout-content {
    padding: 0 !important;
    overflow: hidden !important;
  }

  .layout-container,
  .ant-layout {
    height: 100vh !important;
  }
}
</style>
