使用 Trae 实现一个简单可视化拖拽组件的demo

478 阅读14分钟

使用 Claude-4-Sonnet 这个模型来实现功能,用Vue2实现前端内容,下面给出实现代码和具体功能描述,还有实现效果,做一下简单代码记录

实现效果

image.png

实现代码

<template>
  <div id="app">
    <!-- 顶部工具栏:包含撤销、重做、清空、保存功能以及画布尺寸控制 -->
    <div class="toolbar">
      <!-- 历史操作按钮 -->
      <button @click="undo" :disabled="!canUndo">撤销</button>
      <button @click="redo" :disabled="!canRedo">重做</button>
      <button @click="clearCanvas">清空</button>
      <button @click="saveProject">保存</button>
      <button @click="$refs.fileInput.click()">加载</button>
      <button @click="resetCanvasView">重置视图</button>
      <button @click="clearAllGuidelines" :disabled="guidelines.length === 0">清除参考线</button>
      
      <!-- 隐藏的文件输入框 -->
      <input 
        ref="fileInput" 
        type="file" 
        accept=".json" 
        @change="loadProject" 
        style="display: none;"
      />
      
      <!-- 画布尺寸调整控件 -->
      <div class="canvas-size-controls">
        <label>宽度: <input v-model.number="canvasWidth" type="number" min="300" max="2000" /></label>
        <label>高度: <input v-model.number="canvasHeight" type="number" min="200" max="1500" /></label>
      </div>
      
      <!-- 缩放倍率显示 -->
      <div class="zoom-info">
        缩放: {{ Math.round(canvasScale * 100) }}%
      </div>
      
      <!-- 鼠标位置信息显示 -->
      <div class="mouse-position-info">
        位置: X{{ mousePosition.x }}, Y{{ mousePosition.y }}
      </div>
    </div>

    <div class="main-content">
      <!-- 左侧组件面板:展示可拖拽的组件列表 -->
      <div class="component-panel">
        <h3>组件列表</h3>
        <div class="component-list">
          <!-- 遍历所有可用组件类型,每个组件都可以拖拽到画布 -->
          <div 
            v-for="component in componentTypes" 
            :key="component.type"
            class="component-item"
            draggable="true"
            @dragstart="onDragStart($event, component)"
          >
            <!-- 组件预览区域,显示组件样式和名称 -->
            <div class="component-preview" :style="component.style">
              {{ component.name }}
            </div>
          </div>
        </div>
      </div>

      <!-- 右侧编辑区域:主要的设计画布 -->
      <div 
        class="canvas-container"
        @mousedown="onCanvasMouseDown"
        @wheel="onCanvasWheel"
        @mousemove="updateRulerIndicators"
      >
        <!-- 水平标尺 -->
        <div class="ruler horizontal-ruler" ref="horizontalRuler" @click="onHorizontalRulerClick">
          <div class="ruler-indicator horizontal-indicator" :style="{ left: rulerIndicators.x + 'px' }"></div>
          <div 
            v-for="tick in horizontalTicks" 
            :key="'h-' + tick.position"
            class="ruler-tick"
            :class="{ 'major-tick': tick.isMajor }"
            :style="{ left: tick.position + 'px' }"
          >
            <span v-if="tick.isMajor" class="tick-label">{{ tick.label }}</span>
          </div>
          <!-- 水平标尺上的垂直参考线标记 -->
          <div 
            v-for="guideline in verticalGuidelines" 
            :key="'hr-' + guideline.id"
            class="guideline-marker vertical-marker"
            :style="{ left: guideline.rulerPosition + 'px' }"
          ></div>
        </div>
        
        <!-- 垂直标尺 -->
        <div class="ruler vertical-ruler" ref="verticalRuler" @click="onVerticalRulerClick">
          <div class="ruler-indicator vertical-indicator" :style="{ top: rulerIndicators.y + 'px' }"></div>
          <div 
            v-for="tick in verticalTicks" 
            :key="'v-' + tick.position"
            class="ruler-tick"
            :class="{ 'major-tick': tick.isMajor }"
            :style="{ top: tick.position + 'px' }"
          >
            <span v-if="tick.isMajor" class="tick-label">{{ tick.label }}</span>
          </div>
          <!-- 垂直标尺上的水平参考线标记 -->
          <div 
            v-for="guideline in horizontalGuidelines" 
            :key="'vr-' + guideline.id"
            class="guideline-marker horizontal-marker"
            :style="{ top: guideline.rulerPosition + 'px' }"
          ></div>
        </div>
        
        <!-- 标尺角落 -->
        <div class="ruler-corner"></div>
        
        <div 
          class="canvas"
          :style="{ 
            width: canvasWidth + 'px', 
            height: canvasHeight + 'px',
            transform: `translate(${canvasOffset.x}px, ${canvasOffset.y}px) scale(${canvasScale})`,
            cursor: isCanvasDragging ? 'grabbing' : 'grab'
          }"
          @drop="onDrop"
          @dragover="onDragOver"
          @click="selectComponent(null)"
        >
          <!-- 对齐辅助线:拖拽组件时显示的红色对齐线 -->
          <div 
            v-for="line in alignmentLines" 
            :key="line.id"
            class="alignment-line"
            :class="line.type"
            :style="line.style"
          ></div>
          
          <!-- 参考线 -->
          <div 
            v-for="guideline in guidelines" 
            :key="guideline.id"
            class="guideline"
            :class="guideline.type"
            :style="getGuidelineStyle(guideline)"
            @mousedown="onGuidelineMouseDown($event, guideline)"
            @contextmenu.prevent="showGuidelineContextMenu($event, guideline)"
          >
            <!-- 参考线位置信息 -->
            <div class="guideline-label start" @contextmenu.prevent.stop="deleteGuideline(guideline)">
              {{ Math.round(guideline.position) }}
            </div>
            <div class="guideline-label end" @contextmenu.prevent.stop="deleteGuideline(guideline)">
              {{ Math.round(guideline.position) }}
            </div>
          </div>

          <!-- 画布中的所有组件 -->
          <div 
            v-for="component in components" 
            :key="component.id"
            class="canvas-component"
            :class="{ selected: selectedComponent && selectedComponent.id === component.id }"
            :style="{
              left: component.x + 'px',
              top: component.y + 'px',
              width: component.width + 'px',
              height: component.height + 'px',
              zIndex: component.zIndex,
              ...component.style
            }"
            @mousedown="onComponentMouseDown($event, component)"
            @click.stop="selectComponent(component)"
            @contextmenu.prevent="showContextMenu($event, component)"
          >
            <!-- 可以将左侧组件改为自定义组件列表,且左侧组件已经在components中注册,从而根据组件类型渲染不同的组件 -->
            <!-- <component :is="component.type" /> -->
            {{ component.content }}
            
            <!-- 选中组件时显示的8个调整大小控制点 -->
            <div 
              v-if="selectedComponent && selectedComponent.id === component.id"
              class="resize-handles"
            >
              <div 
                v-for="handle in resizeHandles" 
                :key="handle.position"
                class="resize-handle"
                :class="handle.position"
                @mousedown.stop="onResizeStart($event, component, handle.position)"
              ></div>
            </div>
          </div>
        </div>
      </div>
    </div>

    <!-- 右键菜单:组件右键时显示的操作菜单 -->
    <div 
      v-if="contextMenu.show"
      class="context-menu"
      :style="{ left: contextMenu.x + 'px', top: contextMenu.y + 'px' }"
    >
      <div @click="deleteComponent(contextMenu.component)">删除组件</div>
      <div @click="bringToFront(contextMenu.component)">置顶</div>
      <div @click="sendToBack(contextMenu.component)">置底</div>
      <div @click="bringForward(contextMenu.component)">上移</div>
      <div @click="sendBackward(contextMenu.component)">下移</div>
    </div>
  </div>
</template>

<script>
/**
 * 低代码编辑器主组件
 * 功能包括:组件拖拽、调整大小、对齐辅助线、历史记录、右键菜单等
 */

export default {
  name: 'App',
  data() {
    return {
      // 画布尺寸
      canvasWidth: 1920,
      canvasHeight: 1080,
      
      // 组件相关状态
      components: [], // 画布中的所有组件
      selectedComponent: null, // 当前选中的组件
      draggedComponent: null, // 正在拖拽的组件
      
      // 交互状态
      isDragging: false, // 是否正在拖拽组件
      isResizing: false, // 是否正在调整大小
      isCanvasDragging: false, // 是否正在拖拽画布
      resizeHandle: '', // 当前调整大小的控制点
      
      // 拖拽和调整大小的辅助数据
      dragOffset: { x: 0, y: 0 }, // 拖拽时的偏移量
      resizeStartPos: { x: 0, y: 0 }, // 调整大小开始时的鼠标位置
      resizeStartSize: { width: 0, height: 0 }, // 调整大小开始时的组件尺寸
      
      // 画布拖拽相关
      canvasOffset: { x: 0, y: 0 }, // 画布偏移量
      canvasDragStart: { x: 0, y: 0 }, // 画布拖拽开始位置
      canvasStartOffset: { x: 0, y: 0 }, // 画布拖拽开始时的偏移量
      
      // 画布缩放相关
      canvasScale: 1, // 画布缩放比例
      minScale: 0.1, // 最小缩放比例
      maxScale: 3, // 最大缩放比例
      
      // 对齐辅助线
      alignmentLines: [],
      
      // 右键菜单状态
      contextMenu: {
        show: false,
        x: 0,
        y: 0,
        component: null
      },
      
      // 历史记录
      history: [],
      historyIndex: -1,
      
      // 标尺相关
      rulerUnit: 20, // 标尺单位大小(像素)
      rulerSize: 20, // 标尺宽度/高度
      horizontalTicks: [], // 水平标尺刻度
      verticalTicks: [], // 垂直标尺刻度
      rulerIndicators: { x: 0, y: 0 }, // 标尺指示器位置
      
      // 参考线相关
      guidelines: [], // 参考线数组
      isDraggingGuideline: false, // 是否正在拖拽参考线
      draggedGuideline: null, // 正在拖拽的参考线
      guidelineOffset: 0, // 拖拽参考线时的偏移量
      
      // 窗口大小变化处理
      resizeTimer: null, // 窗口大小变化防抖定时器
      
      // 鼠标位置信息
      mousePosition: { x: 0, y: 0 }, // 鼠标在画布中的位置
      
      // 可用的组件类型定义
      componentTypes: [
        {
          type: 'button',
          name: '按钮',
          style: {
            backgroundColor: '#409eff',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            padding: '8px 16px',
            cursor: 'pointer'
          },
          defaultProps: {
            width: 80,
            height: 32,
            content: '按钮'
          }
        },
        {
          type: 'input',
          name: '输入框',
          style: {
            border: '1px solid #dcdfe6',
            borderRadius: '4px',
            padding: '8px',
            backgroundColor: 'white'
          },
          defaultProps: {
            width: 200,
            height: 32,
            content: '输入框'
          }
        },
        {
          type: 'text',
          name: '文本',
          style: {
            color: '#333',
            fontSize: '14px',
            lineHeight: '1.5'
          },
          defaultProps: {
            width: 100,
            height: 24,
            content: '文本内容'
          }
        },
        {
          type: 'div',
          name: '容器',
          style: {
            border: '1px solid #ddd',
            backgroundColor: '#f5f5f5',
            borderRadius: '4px'
          },
          defaultProps: {
            width: 200,
            height: 100,
            content: '容器'
          }
        }
      ],
      // 调整大小的8个控制点位置定义
      resizeHandles: [
        { position: 'nw' }, // 西北角
        { position: 'n' },  // 北边
        { position: 'ne' }, // 东北角
        { position: 'e' },  // 东边
        { position: 'se' }, // 东南角
        { position: 's' },  // 南边
        { position: 'sw' }, // 西南角
        { position: 'w' }   // 西边
      ]
    }
  },
  computed: {
    // 是否可以撤销
    canUndo() {
      return this.historyIndex > 0
    },
    // 是否可以重做
    canRedo() {
      return this.historyIndex < this.history.length - 1
    },
    // 垂直参考线(在水平标尺上显示)
    verticalGuidelines() {
      return this.guidelines.filter(g => g.type === 'vertical').map(g => ({
        ...g,
        rulerPosition: this.getGuidelineRulerPosition(g)
      }))
    },
    // 水平参考线(在垂直标尺上显示)
    horizontalGuidelines() {
      return this.guidelines.filter(g => g.type === 'horizontal').map(g => ({
        ...g,
        rulerPosition: this.getGuidelineRulerPosition(g)
      }))
    }
  },
  watch: {
    // 监听画布尺寸变化,自动重新初始化视图
    canvasWidth() {
      this.$nextTick(() => {
        this.initializeCanvasView()
      })
    },
    canvasHeight() {
      this.$nextTick(() => {
        this.initializeCanvasView()
      })
    }
  },
  mounted() {
    // 添加全局事件监听器
    document.addEventListener('mousemove', this.onMouseMove)
    document.addEventListener('mouseup', this.onMouseUp)
    document.addEventListener('click', this.hideContextMenu)
    // 添加窗口大小变化监听器
    window.addEventListener('resize', this.onWindowResize)
    // 初始化画布视图
    this.$nextTick(() => {
      this.initializeCanvasView()
    })
    // 保存初始状态
    this.saveState()
  },
  beforeDestroy() {
    // 移除全局事件监听器,防止内存泄漏
    document.removeEventListener('mousemove', this.onMouseMove)
    document.removeEventListener('mouseup', this.onMouseUp)
    document.removeEventListener('click', this.hideContextMenu)
    // 移除窗口大小变化监听器
    window.removeEventListener('resize', this.onWindowResize)
  },
  methods: {
    // ==================== 拖拽相关方法 ====================
    
    /**
     * 开始拖拽组件
     * @param {Event} event - 拖拽事件
     * @param {Object} componentType - 组件类型数据
     */
    onDragStart(event, componentType) {
      event.dataTransfer.setData('componentType', JSON.stringify(componentType))
    },
    
    /**
     * 拖拽经过画布时的处理
     * @param {Event} event - 拖拽事件
     */
    onDragOver(event) {
      event.preventDefault()
    },
    
    /**
     * 在画布上放置组件
     * @param {Event} event - 放置事件
     */
    onDrop(event) {
      event.preventDefault()
      const componentTypeData = event.dataTransfer.getData('componentType')
      if (componentTypeData) {
        const componentType = JSON.parse(componentTypeData)
        const rect = event.currentTarget.getBoundingClientRect()
        
        // 将鼠标坐标转换为画布内的实际坐标,考虑缩放
        const x = (event.clientX - rect.left) / this.canvasScale
        const y = (event.clientY - rect.top) / this.canvasScale
        
        this.addComponent(componentType, x, y)
      }
    },
    
    // ==================== 组件管理方法 ====================
    
    /**
     * 添加新组件到画布
     * @param {Object} componentType - 组件类型
     * @param {number} x - X坐标
     * @param {number} y - Y坐标
     */
    addComponent(componentType, x, y) {
      // 计算组件的初始位置(居中放置)
      let componentX = x - componentType.defaultProps.width / 2
      let componentY = y - componentType.defaultProps.height / 2
      
      // 边界检查:确保组件完全在画布范围内
      // 限制X坐标不能小于0,且组件右边不能超出画布宽度
      componentX = Math.max(0, Math.min(componentX, this.canvasWidth - componentType.defaultProps.width))
      // 限制Y坐标不能小于0,且组件底部不能超出画布高度
      componentY = Math.max(0, Math.min(componentY, this.canvasHeight - componentType.defaultProps.height))
      
      const newComponent = {
        id: Date.now() + Math.random(), // 生成唯一ID
        type: componentType.type,
        x: componentX,
        y: componentY,
        width: componentType.defaultProps.width,
        height: componentType.defaultProps.height,
        content: componentType.defaultProps.content,
        style: { ...componentType.style },
        zIndex: this.getMaxZIndex() + 1 // 设置为最高层级
      }
      
      this.components.push(newComponent)
      this.selectComponent(newComponent)
      this.saveState()
    },
    
    /**
     * 选中组件
     * @param {Object|null} component - 要选中的组件,null表示取消选中
     */
    selectComponent(component) {
      this.selectedComponent = component
      this.hideContextMenu()
    },
    
    /**
     * 删除组件
     * @param {Object} component - 要删除的组件
     */
    deleteComponent(component) {
      const index = this.components.findIndex(c => c.id === component.id)
      if (index > -1) {
        this.components.splice(index, 1)
        if (this.selectedComponent && this.selectedComponent.id === component.id) {
          this.selectedComponent = null
        }
        this.saveState()
      }
      this.hideContextMenu()
    },
    
    // ==================== 层级管理方法 ====================
    
    /**
     * 获取当前最大的z-index值
     * @returns {number} 最大z-index值
     */
    getMaxZIndex() {
      return this.components.length > 0 ? Math.max(...this.components.map(c => c.zIndex || 0)) : 0
    },
    
    /**
     * 将组件置顶
     * @param {Object} component - 要置顶的组件
     */
    bringToFront(component) {
      component.zIndex = this.getMaxZIndex() + 1
      this.saveState()
      this.hideContextMenu()
    },
    
    /**
     * 将组件置底
     * @param {Object} component - 要置底的组件
     */
    sendToBack(component) {
      const minZ = Math.min(...this.components.map(c => c.zIndex || 0))
      component.zIndex = minZ - 1
      this.saveState()
      this.hideContextMenu()
    },
    
    /**
     * 将组件上移一层
     * @param {Object} component - 要上移的组件
     */
    bringForward(component) {
      const currentZ = component.zIndex || 0
      const higherComponents = this.components.filter(c => (c.zIndex || 0) > currentZ)
      if (higherComponents.length > 0) {
        const nextZ = Math.min(...higherComponents.map(c => c.zIndex || 0))
        component.zIndex = nextZ + 1
      }
      this.saveState()
      this.hideContextMenu()
    },
    
    /**
     * 将组件下移一层
     * @param {Object} component - 要下移的组件
     */
    sendBackward(component) {
      const currentZ = component.zIndex || 0
      const lowerComponents = this.components.filter(c => (c.zIndex || 0) < currentZ)
      if (lowerComponents.length > 0) {
        const prevZ = Math.max(...lowerComponents.map(c => c.zIndex || 0))
        component.zIndex = prevZ - 1
      }
      this.saveState()
      this.hideContextMenu()
    },
    
    // ==================== 鼠标事件处理方法 ====================
    
    /**
     * 组件鼠标按下事件处理
     * @param {Event} event - 鼠标事件
     * @param {Object} component - 被点击的组件
     */
    onComponentMouseDown(event, component) {
      if (event.button !== 0) return // 只处理左键点击
      
      this.selectComponent(component)
      this.isDragging = true
      this.draggedComponent = component
      
      // 计算鼠标相对于组件的偏移量,考虑画布缩放
      const canvas = document.querySelector('.canvas')
      const canvasRect = canvas.getBoundingClientRect()
      const mouseXInCanvas = (event.clientX - canvasRect.left) / this.canvasScale
      const mouseYInCanvas = (event.clientY - canvasRect.top) / this.canvasScale
      
      this.dragOffset = {
        x: mouseXInCanvas - component.x,
        y: mouseYInCanvas - component.y
      }
      
      event.preventDefault()
    },
    
    /**
     * 全局鼠标移动事件处理
     * @param {Event} event - 鼠标事件
     */
    onMouseMove(event) {
      if (this.isDragging && this.draggedComponent) {
        // 处理组件拖拽,考虑画布缩放
        const canvas = document.querySelector('.canvas')
        const canvasRect = canvas.getBoundingClientRect()
        
        // 将鼠标坐标转换为画布内的实际坐标
        const mouseXInCanvas = (event.clientX - canvasRect.left) / this.canvasScale
        const mouseYInCanvas = (event.clientY - canvasRect.top) / this.canvasScale
        
        let newX = mouseXInCanvas - this.dragOffset.x
        let newY = mouseYInCanvas - this.dragOffset.y
        
        // 边界检查:确保组件不会超出画布区域
        // 限制X坐标不能小于0,且组件右边不能超出画布宽度
        newX = Math.max(0, Math.min(newX, this.canvasWidth - this.draggedComponent.width))
        // 限制Y坐标不能小于0,且组件底部不能超出画布高度
        newY = Math.max(0, Math.min(newY, this.canvasHeight - this.draggedComponent.height))
        
        this.draggedComponent.x = newX
        this.draggedComponent.y = newY
        
        // 显示对齐辅助线
        this.showAlignmentLines(this.draggedComponent)
        
        // 更新标尺指示器位置
        this.updateRulerIndicators(event)
      } else if (this.isResizing && this.selectedComponent) {
        // 处理组件大小调整
        this.handleResize(event)
        
        // 更新标尺指示器位置
        this.updateRulerIndicators(event)
      } else if (this.isCanvasDragging) {
        // 处理画布拖拽
        const deltaX = event.clientX - this.canvasDragStart.x
        const deltaY = event.clientY - this.canvasDragStart.y
        
        this.canvasOffset.x = this.canvasStartOffset.x + deltaX
        this.canvasOffset.y = this.canvasStartOffset.y + deltaY
        
        // 更新标尺刻度
        this.updateRulerTicks()
        // 更新标尺指示器位置
        this.updateRulerIndicators(event)
      } else if (this.isDraggingGuideline && this.draggedGuideline) {
        // 处理参考线拖拽
        this.handleGuidelineDrag(event)
      } else {
        // 即使没有拖拽操作,也更新标尺指示器位置
        this.updateRulerIndicators(event)
      }
    },
    
    /**
     * 全局鼠标释放事件处理
     */
    onMouseUp() {
      if (this.isDragging) {
        this.isDragging = false
        this.draggedComponent = null
        this.alignmentLines = [] // 清除对齐线
        this.saveState()
      }
      if (this.isResizing) {
        this.isResizing = false
        this.saveState()
      }
      if (this.isCanvasDragging) {
        this.isCanvasDragging = false
      }
      if (this.isDraggingGuideline) {
        this.isDraggingGuideline = false
        this.draggedGuideline = null
      }
    },
    
    // ==================== 调整大小相关方法 ====================
    
    /**
     * 开始调整组件大小
     * @param {Event} event - 鼠标事件
     * @param {Object} component - 要调整的组件
     * @param {string} handle - 调整控制点位置
     */
    onResizeStart(event, component, handle) {
      this.isResizing = true
      this.resizeHandle = handle
      
      // 将鼠标坐标转换为画布内的实际坐标,考虑缩放
      const canvas = document.querySelector('.canvas')
      const canvasRect = canvas.getBoundingClientRect()
      const mouseXInCanvas = (event.clientX - canvasRect.left) / this.canvasScale
      const mouseYInCanvas = (event.clientY - canvasRect.top) / this.canvasScale
      
      this.resizeStartPos = { x: mouseXInCanvas, y: mouseYInCanvas }
      this.resizeStartSize = { width: component.width, height: component.height }
      this.resizeStartPosition = { x: component.x, y: component.y }
      event.preventDefault()
    },
    
    /**
     * 处理组件大小调整
     * @param {Event} event - 鼠标事件
     */
    handleResize(event) {
      // 将当前鼠标坐标转换为画布内的实际坐标,考虑缩放
      const canvas = document.querySelector('.canvas')
      const canvasRect = canvas.getBoundingClientRect()
      const mouseXInCanvas = (event.clientX - canvasRect.left) / this.canvasScale
      const mouseYInCanvas = (event.clientY - canvasRect.top) / this.canvasScale
      
      const deltaX = mouseXInCanvas - this.resizeStartPos.x
      const deltaY = mouseYInCanvas - this.resizeStartPos.y
      const component = this.selectedComponent
      
      let newWidth = this.resizeStartSize.width
      let newHeight = this.resizeStartSize.height
      let newX = this.resizeStartPosition.x
      let newY = this.resizeStartPosition.y
      
      // 根据不同的控制点处理调整逻辑
      switch (this.resizeHandle) {
        case 'nw': // 西北角:同时调整宽高和位置
          newWidth = this.resizeStartSize.width - deltaX
          newHeight = this.resizeStartSize.height - deltaY
          newX = this.resizeStartPosition.x + deltaX
          newY = this.resizeStartPosition.y + deltaY
          break
        case 'n': // 北边:只调整高度和Y位置
          newHeight = this.resizeStartSize.height - deltaY
          newY = this.resizeStartPosition.y + deltaY
          break
        case 'ne': // 东北角:调整宽高,只改变Y位置
          newWidth = this.resizeStartSize.width + deltaX
          newHeight = this.resizeStartSize.height - deltaY
          newY = this.resizeStartPosition.y + deltaY
          break
        case 'e': // 东边:只调整宽度
          newWidth = this.resizeStartSize.width + deltaX
          break
        case 'se': // 东南角:只调整宽高
          newWidth = this.resizeStartSize.width + deltaX
          newHeight = this.resizeStartSize.height + deltaY
          break
        case 's': // 南边:只调整高度
          newHeight = this.resizeStartSize.height + deltaY
          break
        case 'sw': // 西南角:调整宽高,只改变X位置
          newWidth = this.resizeStartSize.width - deltaX
          newHeight = this.resizeStartSize.height + deltaY
          newX = this.resizeStartPosition.x + deltaX
          break
        case 'w': // 西边:只调整宽度和X位置
          newWidth = this.resizeStartSize.width - deltaX
          newX = this.resizeStartPosition.x + deltaX
          break
      }
      
      // 边界检查,确保组件不会超出画布
      // 限制位置不能小于0
      newX = Math.max(0, newX)
      newY = Math.max(0, newY)
      
      // 限制位置+尺寸不能超出画布
      if (newX + newWidth > this.canvasWidth) {
        if (this.resizeHandle.includes('w')) {
          // 如果是从左边调整,限制X位置
          newX = this.canvasWidth - newWidth
        } else {
          // 如果是从右边调整,限制宽度
          newWidth = this.canvasWidth - newX
        }
      }
      
      if (newY + newHeight > this.canvasHeight) {
        if (this.resizeHandle.includes('n')) {
          // 如果是从上边调整,限制Y位置
          newY = this.canvasHeight - newHeight
        } else {
          // 如果是从下边调整,限制高度
          newHeight = this.canvasHeight - newY
        }
      }
      
      // 最小尺寸限制(在边界检查之后)
      newWidth = Math.max(20, newWidth)
      newHeight = Math.max(20, newHeight)
      
      // 再次检查边界,确保最小尺寸限制后仍在画布内
      if (newX + newWidth > this.canvasWidth) {
        newX = this.canvasWidth - newWidth
      }
      if (newY + newHeight > this.canvasHeight) {
        newY = this.canvasHeight - newHeight
      }
      
      // 确保位置不为负数
      newX = Math.max(0, newX)
      newY = Math.max(0, newY)
      
      // 应用新的尺寸和位置
      component.width = newWidth
      component.height = newHeight
      component.x = newX
      component.y = newY
    },
    
    // 对齐辅助线
    showAlignmentLines(draggedComponent) {
      this.alignmentLines = []
      const threshold = 5 // 对齐阈值
      
      this.components.forEach(component => {
        if (component.id === draggedComponent.id) return
        
        const comp = component
        const drag = draggedComponent
        
        // 垂直对齐线
        if (Math.abs(comp.x - drag.x) < threshold) {
          this.alignmentLines.push({
            id: `v-${comp.id}-left`,
            type: 'vertical',
            style: {
              left: comp.x + 'px',
              top: '0px',
              height: this.canvasHeight + 'px'
            }
          })
        }
        if (Math.abs((comp.x + comp.width) - (drag.x + drag.width)) < threshold) {
          this.alignmentLines.push({
            id: `v-${comp.id}-right`,
            type: 'vertical',
            style: {
              left: (comp.x + comp.width) + 'px',
              top: '0px',
              height: this.canvasHeight + 'px'
            }
          })
        }
        if (Math.abs((comp.x + comp.width / 2) - (drag.x + drag.width / 2)) < threshold) {
          this.alignmentLines.push({
            id: `v-${comp.id}-center`,
            type: 'vertical',
            style: {
              left: (comp.x + comp.width / 2) + 'px',
              top: '0px',
              height: this.canvasHeight + 'px'
            }
          })
        }
        
        // 水平对齐线
        if (Math.abs(comp.y - drag.y) < threshold) {
          this.alignmentLines.push({
            id: `h-${comp.id}-top`,
            type: 'horizontal',
            style: {
              left: '0px',
              top: comp.y + 'px',
              width: this.canvasWidth + 'px'
            }
          })
        }
        if (Math.abs((comp.y + comp.height) - (drag.y + drag.height)) < threshold) {
          this.alignmentLines.push({
            id: `h-${comp.id}-bottom`,
            type: 'horizontal',
            style: {
              left: '0px',
              top: (comp.y + comp.height) + 'px',
              width: this.canvasWidth + 'px'
            }
          })
        }
        if (Math.abs((comp.y + comp.height / 2) - (drag.y + drag.height / 2)) < threshold) {
          this.alignmentLines.push({
            id: `h-${comp.id}-middle`,
            type: 'horizontal',
            style: {
              left: '0px',
              top: (comp.y + comp.height / 2) + 'px',
              width: this.canvasWidth + 'px'
            }
          })
        }
      })
    },
    
    // 右键菜单
    showContextMenu(event, component) {
      this.contextMenu = {
        show: true,
        x: event.clientX,
        y: event.clientY,
        component: component
      }
    },
    
    hideContextMenu() {
      this.contextMenu.show = false
    },
    
    // 历史记录
    saveState() {
      const state = JSON.parse(JSON.stringify(this.components))
      this.history = this.history.slice(0, this.historyIndex + 1)
      this.history.push(state)
      this.historyIndex = this.history.length - 1
      
      // 限制历史记录数量
      if (this.history.length > 50) {
        this.history.shift()
        this.historyIndex--
      }
    },
    
    undo() {
      if (this.canUndo) {
        this.historyIndex--
        this.components = JSON.parse(JSON.stringify(this.history[this.historyIndex]))
        this.selectedComponent = null
      }
    },
    
    redo() {
      if (this.canRedo) {
        this.historyIndex++
        this.components = JSON.parse(JSON.stringify(this.history[this.historyIndex]))
        this.selectedComponent = null
      }
    },
    
    clearCanvas() {
      if (confirm('确定要清空画布吗?')) {
        this.components = []
        this.selectedComponent = null
        this.saveState()
      }
    },
    
    saveProject() {
      // 对组件信息进行归一化处理
      const normalizedComponents = this.components.map(component => {
        return {
          ...component,
          // 保留原始像素值
          x: component.x,
          y: component.y,
          width: component.width,
          height: component.height,
          // 添加归一化值(相对于画布尺寸的比例,0-1之间)
          normalizedX: component.x / this.canvasWidth,
          normalizedY: component.y / this.canvasHeight,
          normalizedWidth: component.width / this.canvasWidth,
          normalizedHeight: component.height / this.canvasHeight
        }
      })
      
      const projectData = {
        components: normalizedComponents,
        canvasWidth: this.canvasWidth,
        canvasHeight: this.canvasHeight
      }
      
      const dataStr = JSON.stringify(projectData, null, 2)
      const dataBlob = new Blob([dataStr], { type: 'application/json' })
      const url = URL.createObjectURL(dataBlob)
      
      const link = document.createElement('a')
      link.href = url
      link.download = 'lowcode-project.json'
      link.click()
      
      URL.revokeObjectURL(url)
    },
    
    /**
     * 加载项目文件
     * @param {Event} event - 文件输入事件
     */
    loadProject(event) {
      const file = event.target.files[0]
      if (!file) return
      
      const reader = new FileReader()
      reader.onload = (e) => {
        try {
          const projectData = JSON.parse(e.target.result)
          
          // 恢复画布尺寸
          if (projectData.canvasWidth && projectData.canvasHeight) {
            this.canvasWidth = projectData.canvasWidth
            this.canvasHeight = projectData.canvasHeight
          }
          
          // 恢复组件,优先使用归一化数据
          this.components = projectData.components.map(component => {
            let restoredComponent = { ...component }
            
            // 如果存在归一化数据,根据当前画布尺寸还原位置和大小
            if (component.normalizedX !== undefined) {
              restoredComponent.x = component.normalizedX * this.canvasWidth
              restoredComponent.y = component.normalizedY * this.canvasHeight
              restoredComponent.width = component.normalizedWidth * this.canvasWidth
              restoredComponent.height = component.normalizedHeight * this.canvasHeight
              
              // 清除归一化数据,避免在运行时保留
              delete restoredComponent.normalizedX
              delete restoredComponent.normalizedY
              delete restoredComponent.normalizedWidth
              delete restoredComponent.normalizedHeight
            }
            
            return restoredComponent
          })
          
          this.selectedComponent = null
          this.saveState()
          
          alert('项目加载成功!')
        } catch (error) {
          alert('项目文件格式错误!')
          console.error('加载项目失败:', error)
        }
      }
      
      reader.readAsText(file)
      // 清空文件输入,允许重复选择同一文件
      event.target.value = ''
    },
    
    // ==================== 画布拖拽相关方法 ====================
    
    /**
     * 画布鼠标按下事件处理
     * @param {Event} event - 鼠标事件
     */
    onCanvasMouseDown(event) {
      // 只有在点击画布空白区域或画布容器时才开始拖拽画布
      // 排除点击组件、调整大小控制点等元素
      if (event.target.classList.contains('canvas') || 
          event.target.classList.contains('canvas-container')) {
        this.isCanvasDragging = true
        this.canvasDragStart = {
          x: event.clientX,
          y: event.clientY
        }
        this.canvasStartOffset = {
          x: this.canvasOffset.x,
          y: this.canvasOffset.y
        }
        event.preventDefault()
      }
    },
    
    /**
     * 画布鼠标滚轮事件处理(缩放功能)
     * @param {Event} event - 滚轮事件
     */
    onCanvasWheel(event) {
      event.preventDefault()
      
      // 计算缩放增量
      const delta = event.deltaY > 0 ? -0.1 : 0.1
      const oldScale = this.canvasScale
      const newScale = Math.max(this.minScale, Math.min(this.maxScale, oldScale + delta))
      
      // 如果缩放比例没有变化,直接返回
      if (newScale === oldScale) return
      
      // 获取鼠标在画布容器中的位置
      const container = event.currentTarget
      const containerRect = container.getBoundingClientRect()
      const mouseX = event.clientX - containerRect.left
      const mouseY = event.clientY - containerRect.top
      
      // 计算鼠标相对于画布中心的偏移量
      const containerCenterX = container.clientWidth / 2
      const containerCenterY = container.clientHeight / 2
      
      // 计算缩放前鼠标在画布坐标系中的位置
      const canvasMouseX = (mouseX - containerCenterX - this.canvasOffset.x) / oldScale
      const canvasMouseY = (mouseY - containerCenterY - this.canvasOffset.y) / oldScale
      
      // 更新缩放比例
      this.canvasScale = newScale
      
      // 计算新的偏移量,使鼠标位置在画布中保持不变
      this.canvasOffset.x = mouseX - containerCenterX - canvasMouseX * newScale
      this.canvasOffset.y = mouseY - containerCenterY - canvasMouseY * newScale
      
      // 更新标尺刻度
      this.updateRulerTicks()
      // 更新标尺指示器位置
      this.updateRulerIndicators(event)
    },
    
    /**
     * 重置画布视图(缩放和偏移)
     */
    resetCanvasView() {
      this.canvasScale = 1
      this.canvasOffset.x = 0
      this.canvasOffset.y = 0
      
      // 更新标尺刻度
      this.updateRulerTicks()
    },
    
    /**
     * 初始化画布视图,根据画布尺寸自动缩放居中
     */
    initializeCanvasView() {
      const container = document.querySelector('.canvas-container')
      if (!container) return
      
      // 获取容器的可用空间(减去padding和标尺大小)
      const containerRect = container.getBoundingClientRect()
      const availableWidth = containerRect.width - 40 - this.rulerSize // 减去左右padding和垂直标尺宽度
      const availableHeight = containerRect.height - 40 - this.rulerSize // 减去上下padding和水平标尺高度
      
      // 计算缩放比例,使画布能完整显示在容器中
      const scaleX = availableWidth / this.canvasWidth
      const scaleY = availableHeight / this.canvasHeight
      const optimalScale = Math.min(scaleX, scaleY, 1) // 不超过1倍缩放
      
      // 限制缩放比例在允许范围内
      this.canvasScale = Math.max(this.minScale, Math.min(this.maxScale, optimalScale))
      
      // 重置画布偏移量为0,让CSS的flex布局来处理居中
      this.canvasOffset.x = 0
      this.canvasOffset.y = 0
      
      // 延迟初始化标尺,确保DOM更新完成
      this.$nextTick(() => {
        this.initializeRulers()
      })
    },
    
    /**
     * 初始化标尺刻度
     */
    initializeRulers() {
      this.updateRulerTicks()
      this.$nextTick(() => {
        this.updateRulerIndicators()
      })
    },
    
    /**
     * 更新标尺刻度
     */
    updateRulerTicks() {
      // 清空现有刻度
      this.horizontalTicks = []
      this.verticalTicks = []
      
      // 计算可见区域的起始和结束位置(考虑缩放和偏移)
      const container = document.querySelector('.canvas-container')
      if (!container) return
      
      const containerRect = container.getBoundingClientRect()
      const availableWidth = containerRect.width - this.rulerSize
      const availableHeight = containerRect.height - this.rulerSize
      
      // 计算标尺上的刻度间隔
      const tickInterval = this.rulerUnit
      
      // 计算画布在容器中的理论居中位置
      const scaledCanvasWidth = this.canvasWidth * this.canvasScale
      const scaledCanvasHeight = this.canvasHeight * this.canvasScale
      
      // 计算画布左上角在标尺坐标系中的位置
      // 考虑容器padding(20px)、标尺大小、flex居中和transform偏移
      const containerCenterX = (containerRect.width - this.rulerSize) / 2
      const containerCenterY = (containerRect.height - this.rulerSize) / 2
      
      const canvasCenterX = scaledCanvasWidth / 2
      const canvasCenterY = scaledCanvasHeight / 2
      
      // 画布左上角在标尺坐标系中的位置
      // 需要减去标尺自身的偏移量:水平标尺left: 20px,垂直标尺top: 20px
      const originX = containerCenterX - canvasCenterX + this.canvasOffset.x - this.rulerSize / 2
      const originY = containerCenterY - canvasCenterY + this.canvasOffset.y - this.rulerSize / 2
      
      // 计算可见区域内的刻度范围
      const startX = Math.floor(-originX / (tickInterval * this.canvasScale)) - 5
      const endX = Math.ceil((availableWidth - originX) / (tickInterval * this.canvasScale)) + 5
      
      const startY = Math.floor(-originY / (tickInterval * this.canvasScale)) - 5
      const endY = Math.ceil((availableHeight - originY) / (tickInterval * this.canvasScale)) + 5
      
      // 生成水平标尺刻度
      for (let i = startX; i <= endX; i++) {
        const canvasPos = i * tickInterval // 画布上的位置
        const pixelPosition = originX + canvasPos * this.canvasScale // 标尺上的像素位置
        
        // 确保刻度在可见区域内
        if (pixelPosition >= 0 && pixelPosition <= availableWidth) {
          const isMajor = i % 5 === 0 // 每5个刻度为主刻度
          const label = Math.round(i * tickInterval) // 实际像素值
          
          this.horizontalTicks.push({
            position: pixelPosition,
            isMajor,
            label
          })
        }
      }
      
      // 生成垂直标尺刻度
      for (let i = startY; i <= endY; i++) {
        const canvasPos = i * tickInterval // 画布上的位置
        const pixelPosition = originY + canvasPos * this.canvasScale // 标尺上的像素位置
        
        // 确保刻度在可见区域内
        if (pixelPosition >= 0 && pixelPosition <= availableHeight) {
          const isMajor = i % 5 === 0 // 每5个刻度为主刻度
          const label = Math.round(i * tickInterval) // 实际像素值
          
          this.verticalTicks.push({
            position: pixelPosition,
            isMajor,
            label
          })
        }
      }
    },
    
    /**
     * 更新标尺指示器位置
     * @param {Event} event - 鼠标事件(可选)
     */
    updateRulerIndicators(event) {
      const container = document.querySelector('.canvas-container')
      if (!container) return
      
      if (event) {
        const containerRect = container.getBoundingClientRect()
        
        // 检查鼠标是否在canvas-container区域内
        const mouseInContainer = event.clientX >= containerRect.left && 
                                event.clientX <= containerRect.right && 
                                event.clientY >= containerRect.top && 
                                event.clientY <= containerRect.bottom
        
        // 只有在容器内时才更新标尺指示器
        if (mouseInContainer) {
          // 计算鼠标相对于容器的位置
          const mouseX = event.clientX - containerRect.left
          const mouseY = event.clientY - containerRect.top
          
          // 直接使用鼠标在容器中的位置作为指示器位置
          // 水平标尺指示器:鼠标的X坐标减去垂直标尺的宽度
          const indicatorX = mouseX - this.rulerSize
          
          // 垂直标尺指示器:鼠标的Y坐标减去水平标尺的高度
          const indicatorY = mouseY - this.rulerSize
          
          // 确保指示器不会超出标尺范围
          const maxX = containerRect.width - this.rulerSize
          const maxY = containerRect.height - this.rulerSize
          
          // 更新指示器位置,限制在有效范围内
          this.rulerIndicators.x = Math.max(0, Math.min(indicatorX, maxX))
          this.rulerIndicators.y = Math.max(0, Math.min(indicatorY, maxY))
        }
        
        // 鼠标位置信息只在容器内时更新
        if (mouseInContainer) {
          const canvas = document.querySelector('.canvas')
          if (canvas) {
            const canvasRect = canvas.getBoundingClientRect()
            const mouseXInCanvas = (event.clientX - canvasRect.left) / this.canvasScale
            const mouseYInCanvas = (event.clientY - canvasRect.top) / this.canvasScale
            
            // 更新鼠标位置信息,允许负数和超出画布范围的坐标
            this.mousePosition.x = Math.round(mouseXInCanvas)
            this.mousePosition.y = Math.round(mouseYInCanvas)
          }
        }
      }
    },
    
    // ==================== 窗口大小变化处理 ====================
    
    /**
     * 窗口大小变化时的处理
     * 重新初始化画布视图以适应新的窗口尺寸
     */
    onWindowResize() {
      // 使用防抖处理,避免频繁触发
      clearTimeout(this.resizeTimer)
      this.resizeTimer = setTimeout(() => {
        this.$nextTick(() => {
          this.initializeCanvasView()
        })
      }, 100)
    },
    
    // ==================== 参考线相关方法 ====================
    
    /**
     * 点击水平标尺创建垂直参考线
     * @param {Event} event - 点击事件
     */
    onHorizontalRulerClick(event) {
      const rect = event.currentTarget.getBoundingClientRect()
      const clickX = event.clientX - rect.left
      
      // 将点击位置转换为画布坐标
      const canvasPosition = this.rulerPositionToCanvasPosition(clickX, 'vertical')
      
      // 只有在画布范围内才创建参考线
      if (canvasPosition >= 0 && canvasPosition <= this.canvasWidth) {
        this.createGuideline('vertical', canvasPosition)
      }
    },
    
    /**
     * 点击垂直标尺创建水平参考线
     * @param {Event} event - 点击事件
     */
    onVerticalRulerClick(event) {
      const rect = event.currentTarget.getBoundingClientRect()
      const clickY = event.clientY - rect.top
      
      // 将点击位置转换为画布坐标
      const canvasPosition = this.rulerPositionToCanvasPosition(clickY, 'horizontal')
      
      // 只有在画布范围内才创建参考线
      if (canvasPosition >= 0 && canvasPosition <= this.canvasHeight) {
        this.createGuideline('horizontal', canvasPosition)
      }
    },
    
    /**
     * 创建参考线
     * @param {string} type - 参考线类型:'vertical' 或 'horizontal'
     * @param {number} position - 参考线在画布上的位置
     */
    createGuideline(type, position) {
      const guideline = {
        id: Date.now() + Math.random(),
        type,
        position: Math.max(0, Math.min(position, type === 'vertical' ? this.canvasWidth : this.canvasHeight))
      }
      
      this.guidelines.push(guideline)
    },
    
    /**
     * 将标尺位置转换为画布坐标
     * @param {number} rulerPos - 标尺上的位置
     * @param {string} type - 参考线类型
     * @returns {number} 画布坐标
     */
    rulerPositionToCanvasPosition(rulerPos, type) {
      const container = document.querySelector('.canvas-container')
      if (!container) return 0
      
      const containerRect = container.getBoundingClientRect()
      const scaledCanvasWidth = this.canvasWidth * this.canvasScale
      const scaledCanvasHeight = this.canvasHeight * this.canvasScale
      
      if (type === 'vertical') {
        const containerCenterX = (containerRect.width - this.rulerSize) / 2
        const canvasCenterX = scaledCanvasWidth / 2
        const originX = containerCenterX - canvasCenterX + this.canvasOffset.x - this.rulerSize / 2
        
        return (rulerPos - originX) / this.canvasScale
      } else {
        const containerCenterY = (containerRect.height - this.rulerSize) / 2
        const canvasCenterY = scaledCanvasHeight / 2
        const originY = containerCenterY - canvasCenterY + this.canvasOffset.y - this.rulerSize / 2
        
        return (rulerPos - originY) / this.canvasScale
      }
    },
    
    /**
     * 获取参考线在标尺上的位置
     * @param {Object} guideline - 参考线对象
     * @returns {number} 标尺位置
     */
    getGuidelineRulerPosition(guideline) {
      const container = document.querySelector('.canvas-container')
      if (!container) return 0
      
      const containerRect = container.getBoundingClientRect()
      const scaledCanvasWidth = this.canvasWidth * this.canvasScale
      const scaledCanvasHeight = this.canvasHeight * this.canvasScale
      
      if (guideline.type === 'vertical') {
        const containerCenterX = (containerRect.width - this.rulerSize) / 2
        const canvasCenterX = scaledCanvasWidth / 2
        const originX = containerCenterX - canvasCenterX + this.canvasOffset.x - this.rulerSize / 2
        
        return originX + guideline.position * this.canvasScale
      } else {
        const containerCenterY = (containerRect.height - this.rulerSize) / 2
        const canvasCenterY = scaledCanvasHeight / 2
        const originY = containerCenterY - canvasCenterY + this.canvasOffset.y - this.rulerSize / 2
        
        return originY + guideline.position * this.canvasScale
      }
    },
    
    /**
     * 获取参考线样式
     * @param {Object} guideline - 参考线对象
     * @returns {Object} 样式对象
     */
    getGuidelineStyle(guideline) {
      if (guideline.type === 'vertical') {
        return {
          left: guideline.position + 'px',
          top: '0px',
          width: '1px',
          height: '100%'
        }
      } else {
        return {
          left: '0px',
          top: guideline.position + 'px',
          width: '100%',
          height: '1px'
        }
      }
    },
    
    /**
     * 参考线鼠标按下事件
     * @param {Event} event - 鼠标事件
     * @param {Object} guideline - 参考线对象
     */
    onGuidelineMouseDown(event, guideline) {
      if (event.button !== 0) return
      
      this.isDraggingGuideline = true
      this.draggedGuideline = guideline
      
      // 计算拖拽偏移量
      const canvas = document.querySelector('.canvas')
      const canvasRect = canvas.getBoundingClientRect()
      
      if (guideline.type === 'vertical') {
        const mouseXInCanvas = (event.clientX - canvasRect.left) / this.canvasScale
        this.guidelineOffset = mouseXInCanvas - guideline.position
      } else {
        const mouseYInCanvas = (event.clientY - canvasRect.top) / this.canvasScale
        this.guidelineOffset = mouseYInCanvas - guideline.position
      }
      
      event.preventDefault()
      event.stopPropagation()
    },
    
    /**
     * 处理参考线拖拽
     * @param {Event} event - 鼠标事件
     */
    handleGuidelineDrag(event) {
      if (!this.draggedGuideline) return
      
      const canvas = document.querySelector('.canvas')
      const canvasRect = canvas.getBoundingClientRect()
      
      if (this.draggedGuideline.type === 'vertical') {
        const mouseXInCanvas = (event.clientX - canvasRect.left) / this.canvasScale
        let newPosition = mouseXInCanvas - this.guidelineOffset
        
        // 限制在画布范围内
        newPosition = Math.max(0, Math.min(newPosition, this.canvasWidth))
        this.draggedGuideline.position = newPosition
      } else {
        const mouseYInCanvas = (event.clientY - canvasRect.top) / this.canvasScale
        let newPosition = mouseYInCanvas - this.guidelineOffset
        
        // 限制在画布范围内
        newPosition = Math.max(0, Math.min(newPosition, this.canvasHeight))
        this.draggedGuideline.position = newPosition
      }
    },
    
    /**
     * 显示参考线右键菜单
     * @param {Event} event - 右键事件
     * @param {Object} guideline - 参考线对象
     */
    showGuidelineContextMenu(event, guideline) {
      // 直接删除参考线,简化交互
      this.deleteGuideline(guideline)
    },
    
    /**
     * 删除参考线
     * @param {Object} guideline - 要删除的参考线
     */
    deleteGuideline(guideline) {
      const index = this.guidelines.findIndex(g => g.id === guideline.id)
      if (index > -1) {
        this.guidelines.splice(index, 1)
      }
    },
    
    /**
     * 清除所有参考线
     */
    clearAllGuidelines() {
      if (this.guidelines.length > 0) {
        this.guidelines = []
      }
    }
  }
}
</script>

<style>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  height: 100vh;
  display: flex;
  flex-direction: column;
}

.toolbar {
  height: 50px;
  background: #f5f5f5;
  border-bottom: 1px solid #ddd;
  display: flex;
  align-items: center;
  padding: 0 16px;
  gap: 12px;
}

.toolbar button {
  padding: 6px 12px;
  border: 1px solid #ddd;
  background: white;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
}

.toolbar button:hover:not(:disabled) {
  background: #f0f0f0;
}

.toolbar button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.canvas-size-controls {
  margin-left: auto;
  display: flex;
  gap: 12px;
}

.canvas-size-controls label {
  font-size: 12px;
  display: flex;
  align-items: center;
  gap: 4px;
}

.canvas-size-controls input {
  width: 80px;
  padding: 4px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 12px;
}

.zoom-info {
  font-size: 12px;
  color: #666;
  background: #f5f5f5;
  padding: 4px 8px;
  border-radius: 4px;
  border: 1px solid #ddd;
  margin-left: 16px;
  min-width: 60px;
  text-align: center;
}

.mouse-position-info {
  font-size: 12px;
  color: #666;
  background: #f5f5f5;
  padding: 4px 8px;
  border-radius: 4px;
  border: 1px solid #ddd;
  margin-left: 16px;
  min-width: 120px;
  text-align: center;
  font-family: monospace;
}

.main-content {
  flex: 1;
  display: flex;
  overflow: hidden;
}

.component-panel {
  width: 250px;
  background: #fafafa;
  border-right: 1px solid #ddd;
  padding: 16px;
  overflow-y: auto;
  flex-shrink: 0;
}

.component-panel h3 {
  margin-bottom: 16px;
  font-size: 14px;
  color: #333;
}

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

.component-item {
  cursor: grab;
  border: 1px solid #ddd;
  border-radius: 4px;
  padding: 8px;
  background: white;
  transition: all 0.2s;
}

.component-item:hover {
  border-color: #409eff;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

.component-item:active {
  cursor: grabbing;
}

.component-preview {
  font-size: 12px;
  text-align: center;
  min-height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.canvas-container {
  flex: 1;
  min-width: 0;
  padding: 20px;
  background: #f0f0f0;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
}

.canvas {
  background: white;
  background-image: 
    linear-gradient(rgba(0,0,0,0.1) 1px, transparent 1px),
    linear-gradient(90deg, rgba(0,0,0,0.1) 1px, transparent 1px);
  background-size: 20px 20px;
  border: 1px solid #ddd;
  position: relative;
  margin: 0 auto;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  flex-shrink: 0;
}

.canvas-component {
  position: absolute;
  border: 1px solid transparent;
  cursor: move;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  user-select: none;
}

.canvas-component:hover {
  border-color: #409eff;
}

.canvas-component.selected {
  border-color: #409eff;
  box-shadow: 0 0 0 1px #409eff;
}

.resize-handles {
  position: absolute;
  top: -4px;
  left: -4px;
  right: -4px;
  bottom: -4px;
  pointer-events: none;
}

.resize-handle {
  position: absolute;
  width: 8px;
  height: 8px;
  background: #409eff;
  border: 1px solid white;
  border-radius: 50%;
  pointer-events: all;
  cursor: pointer;
}

.resize-handle.nw {
  top: -4px;
  left: -4px;
  cursor: nw-resize;
}

.resize-handle.n {
  top: -4px;
  left: 50%;
  transform: translateX(-50%);
  cursor: n-resize;
}

.resize-handle.ne {
  top: -4px;
  right: -4px;
  cursor: ne-resize;
}

.resize-handle.e {
  top: 50%;
  right: -4px;
  transform: translateY(-50%);
  cursor: e-resize;
}

.resize-handle.se {
  bottom: -4px;
  right: -4px;
  cursor: se-resize;
}

.resize-handle.s {
  bottom: -4px;
  left: 50%;
  transform: translateX(-50%);
  cursor: s-resize;
}

.resize-handle.sw {
  bottom: -4px;
  left: -4px;
  cursor: sw-resize;
}

.resize-handle.w {
  top: 50%;
  left: -4px;
  transform: translateY(-50%);
  cursor: w-resize;
}

.alignment-line {
  position: absolute;
  pointer-events: none;
  z-index: 9999;
}

.alignment-line.vertical {
  width: 1px;
  background: #ff4757;
  box-shadow: 0 0 2px rgba(255, 71, 87, 0.5);
}

.alignment-line.horizontal {
  height: 1px;
  background: #ff4757;
  box-shadow: 0 0 2px rgba(255, 71, 87, 0.5);
}

.context-menu {
  position: fixed;
  background: white;
  border: 1px solid #ddd;
  border-radius: 4px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.15);
  z-index: 10000;
  min-width: 120px;
}

.context-menu div {
  padding: 8px 12px;
  cursor: pointer;
  font-size: 12px;
  border-bottom: 1px solid #f0f0f0;
}

.context-menu div:last-child {
  border-bottom: none;
}

.context-menu div:hover {
  background: #f5f5f5;
}

/* 标尺相关样式 */
.ruler {
  position: absolute;
  background: #f5f5f5;
  border: 1px solid #ddd;
  z-index: 100;
  /* pointer-events: none; */
  pointer-events: auto;
  user-select: none;
  overflow: hidden; /* 防止刻度溢出 */
}

.horizontal-ruler {
  top: 0;
  left: 20px; /* 留出垂直标尺的宽度 */
  right: 0;
  height: 20px;
}

.vertical-ruler {
  top: 20px; /* 留出水平标尺的高度 */
  left: 0;
  bottom: 0;
  width: 20px;
}

.ruler-corner {
  position: absolute;
  top: 0;
  left: 0;
  width: 20px;
  height: 20px;
  background: #f5f5f5;
  border: 1px solid #ddd;
  z-index: 101;
}

.ruler-tick {
  position: absolute;
  background: #aaa;
}

.horizontal-ruler .ruler-tick {
  width: 1px;
  height: 5px;
  bottom: 0;
}

.vertical-ruler .ruler-tick {
  height: 1px;
  width: 5px;
  right: 0;
}

.ruler-tick.major-tick {
  height: 10px;
  width: 1px;
  background: #666; /* 主刻度颜色更深 */
}

.vertical-ruler .ruler-tick.major-tick {
  height: 1px;
  width: 10px;
  background: #666; /* 主刻度颜色更深 */
}

.tick-label {
  position: absolute;
  font-size: 8px;
  color: #666;
  white-space: nowrap;
}

.horizontal-ruler .tick-label {
  top: 2px;
  left: 4px;
  transform: translateX(-20%);
}

.vertical-ruler .tick-label {
  right: 2px;
  top: -7px;
  transform: translateY(-50%);
}

.ruler-indicator {
  position: absolute;
  background: #409eff;
  z-index: 102;
  pointer-events: none; /* 确保指示器不会阻挡鼠标事件 */
}

.horizontal-indicator {
  width: 1px;
  height: 100%;
  top: 0;
  box-shadow: 0 0 2px rgba(64, 158, 255, 0.5); /* 添加阴影使指示器更明显 */
}

.vertical-indicator {
  width: 100%;
  height: 1px;
  left: 0;
  box-shadow: 0 0 2px rgba(64, 158, 255, 0.5); /* 添加阴影使指示器更明显 */
}

/* 参考线样式 */
.guideline {
  position: absolute;
  cursor: move;
  z-index: 10;
  opacity: 0.8;
}

.guideline:hover {
  opacity: 1;
}

.guideline.vertical {
  width: 1px;
  height: 100%;
  cursor: ew-resize;
  background: linear-gradient(to bottom, #00aaff 50%, transparent 50%);
  background-size: 1px 8px;
}

.guideline.vertical:hover {
  background: linear-gradient(to bottom, #0088cc 50%, transparent 50%);
  background-size: 1px 8px;
}

.guideline.horizontal {
  width: 100%;
  height: 1px;
  cursor: ns-resize;
  background: linear-gradient(to right, #00aaff 50%, transparent 50%);
  background-size: 8px 1px;
}

.guideline.horizontal:hover {
  background: linear-gradient(to right, #0088cc 50%, transparent 50%);
  background-size: 8px 1px;
}

/* 参考线位置信息 */
.guideline-label {
  position: absolute;
  background: rgba(0, 170, 255, 0.9);
  color: white;
  padding: 2px 6px;
  border-radius: 3px;
  font-size: 11px;
  font-family: monospace;
  white-space: nowrap;
  z-index: 20;
  pointer-events: auto;
  cursor: pointer;
}

.guideline-label:hover {
  background: rgba(0, 136, 204, 1);
}

/* 垂直参考线的位置信息 */
.guideline.vertical .guideline-label.start {
  top: -20px;
  left: 50%;
  transform: translateX(-50%);
}

.guideline.vertical .guideline-label.end {
  bottom: -20px;
  left: 50%;
  transform: translateX(-50%);
}

/* 水平参考线的位置信息 */
.guideline.horizontal .guideline-label.start {
  left: -40px;
  top: 50%;
  transform: translateY(-50%);
}

.guideline.horizontal .guideline-label.end {
  right: -40px;
  top: 50%;
  transform: translateY(-50%);
}

/* 标尺上的参考线标记 */
/* .ruler-guideline-mark {
  position: absolute;
  background: #00aaff;
  z-index: 12;
}

.horizontal-ruler .ruler-guideline-mark {
  width: 1px;
  height: 100%;
  top: 0;
}

.vertical-ruler .ruler-guideline-mark {
  width: 100%;
  height: 1px;
  left: 0;
} */

/* 标尺上的参考线标记(新样式) */
.guideline-marker {
  position: absolute;
  background: #00aaff;
  z-index: 12;
  pointer-events: none;
}

.vertical-marker {
  width: 1px;
  height: 100%;
  top: 0;
}

.horizontal-marker {
  width: 100%;
  height: 1px;
  left: 0;
}
</style>

功能描述

1. 组件管理

1.1 组件类型定义

  • 按钮组件:蓝色背景,白色文字,圆角边框
  • 输入框组件:带边框的文本输入区域
  • 文本组件:纯文本显示元素
  • 容器组件:可包含其他元素的容器

1.2 组件操作

  • 拖拽添加:从左侧组件面板拖拽到画布
  • 选中操作:点击组件进行选中,显示选中状态
  • 删除功能:通过右键菜单或快捷键删除组件
  • 边界限制:组件添加和移动时自动限制在画布范围内

2. 画布系统

2.1 画布基础功能

  • 可调整尺寸:支持自定义画布宽度(300-2000px)和高度(200-1500px)
  • 缩放功能:支持0.1x到3x的缩放比例,鼠标滚轮缩放
  • 拖拽平移:按住画布空白区域可拖拽移动视图
  • 视图重置:一键重置画布位置和缩放

2.2 坐标系统

  • 实时坐标显示:工具栏显示鼠标在画布中的精确位置
  • 坐标转换:自动处理缩放和偏移的坐标转换
  • 边界检测:鼠标位置信息仅在画布编辑区域内更新

3. 标尺和参考线

3.1 标尺功能

  • 双向标尺:水平和垂直标尺,显示精确刻度
  • 动态指示器:跟随鼠标移动的蓝色指示线
  • 刻度标签:主要刻度点显示数值标签
  • 区域限制:指示器仅在画布编辑区域内跟随

3.2 参考线系统

  • 点击创建:点击标尺在画布范围内创建参考线
  • 拖拽调整:可拖拽参考线调整位置
  • 位置显示:参考线两端显示精确位置数值
  • 右键删除:右键点击位置信息删除参考线
  • 批量清除:一键清除所有参考线
  • 标尺标记:在对应标尺上显示参考线位置标记

4. 组件编辑

4.1 拖拽移动

  • 精确拖拽:支持像素级精确移动
  • 边界约束:移动时自动限制在画布范围内
  • 实时预览:拖拽过程中实时显示组件位置

4.2 尺寸调整

  • 8点控制:提供8个方向的尺寸调整控制点
  • 比例调整:支持等比例和自由调整
  • 边界检测:调整时防止超出画布边界

4.3 层级管理

  • 置顶/置底:快速调整组件到最顶层或最底层
  • 上移/下移:精确调整组件层级关系
  • 自动层级:新添加组件自动设置为最高层级

5. 对齐辅助

5.1 智能对齐

  • 边缘对齐:组件边缘自动对齐到其他组件
  • 中心对齐:支持水平和垂直中心对齐
  • 辅助线显示:拖拽时显示红色对齐辅助线
  • 磁性吸附:接近对齐位置时自动吸附

6. 历史记录

6.1 操作历史

  • 撤销功能:支持多步撤销操作
  • 重做功能:支持多步重做操作
  • 状态保存:自动保存每次操作状态
  • 按钮状态:根据历史状态动态启用/禁用按钮

7. 数据管理

7.1 保存和加载

  • 项目保存:将整个项目保存为JSON文件
  • 项目加载:从JSON文件加载项目
  • 清空画布:一键清除所有组件
  • 数据完整性:保证保存和加载的数据完整性

8. 交互体验

8.1 右键菜单

  • 组件菜单:右键组件显示操作菜单
  • 快捷操作:删除、层级调整等快捷操作
  • 上下文相关:根据选中组件显示相应选项

8.2 视觉反馈

  • 选中状态:选中组件显示蓝色边框
  • 悬停效果:鼠标悬停时的视觉反馈
  • 操作提示:各种操作的视觉指示

9. 响应式设计

9.1 窗口适配

  • 窗口大小监听:自动适配窗口大小变化
  • 布局调整:动态调整界面布局
  • 防抖处理:窗口大小变化的防抖优化

10.技术特点

10.1 性能优化

  • 事件防抖:窗口大小变化等高频事件的防抖处理
  • 按需更新:只在必要时更新相关组件
  • 内存管理:及时清理事件监听器防止内存泄漏