4.1 设计器架构概览

4.1.1 整体架构设计

可视化设计器是低代码平台的核心组件,负责提供拖拽式的页面设计体验。其架构包括以下几个主要部分:

graph TB
    A[设计器容器] --> B[工具栏组件]
    A --> C[组件面板]
    A --> D[画布区域]
    A --> E[属性面板]
    A --> F[图层面板]
    
    B --> B1[撤销/重做]
    B --> B2[预览/发布]
    B --> B3[保存/导入]
    
    C --> C1[基础组件]
    C --> C2[布局组件]
    C --> C3[表单组件]
    C --> C4[图表组件]
    C --> C5[自定义组件]
    
    D --> D1[拖拽处理]
    D --> D2[选择器]
    D --> D3[辅助线]
    D --> D4[网格系统]
    
    E --> E1[样式编辑器]
    E --> E2[属性编辑器]
    E --> E3[事件编辑器]
    
    F --> F1[图层树]
    F --> F2[显示/隐藏]
    F --> F3[锁定/解锁]

4.1.2 核心数据结构

// 组件定义接口
interface ComponentDefinition {
  id: string;
  name: string;
  type: string;
  category: string;
  icon: string;
  description: string;
  props: ComponentProp[];
  events: ComponentEvent[];
  slots: ComponentSlot[];
  defaultProps: Record<string, any>;
  preview: string; // 预览图片
  version: string;
}

// 组件属性定义
interface ComponentProp {
  name: string;
  type: 'string' | 'number' | 'boolean' | 'array' | 'object' | 'color' | 'image' | 'select' | 'textarea';
  label: string;
  description?: string;
  defaultValue?: any;
  required?: boolean;
  options?: Array<{ label: string; value: any }>;
  validation?: {
    min?: number;
    max?: number;
    pattern?: string;
    message?: string;
  };
}

// 组件事件定义
interface ComponentEvent {
  name: string;
  label: string;
  description?: string;
  params: Array<{
    name: string;
    type: string;
    description?: string;
  }>;
}

// 组件插槽定义
interface ComponentSlot {
  name: string;
  label: string;
  description?: string;
  allowedComponents?: string[];
}

// 页面组件实例
interface ComponentInstance {
  id: string;
  type: string;
  name: string;
  props: Record<string, any>;
  style: ComponentStyle;
  events: Record<string, string>; // 事件名 -> 处理函数代码
  children: ComponentInstance[];
  parentId?: string;
  locked: boolean;
  hidden: boolean;
  position: {
    x: number;
    y: number;
    z: number;
  };
  size: {
    width: number | 'auto';
    height: number | 'auto';
  };
}

// 组件样式
interface ComponentStyle {
  // 布局
  display?: 'block' | 'inline' | 'inline-block' | 'flex' | 'grid' | 'none';
  position?: 'static' | 'relative' | 'absolute' | 'fixed' | 'sticky';
  top?: string;
  right?: string;
  bottom?: string;
  left?: string;
  zIndex?: number;
  
  // 尺寸
  width?: string;
  height?: string;
  minWidth?: string;
  minHeight?: string;
  maxWidth?: string;
  maxHeight?: string;
  
  // 外边距和内边距
  margin?: string;
  marginTop?: string;
  marginRight?: string;
  marginBottom?: string;
  marginLeft?: string;
  padding?: string;
  paddingTop?: string;
  paddingRight?: string;
  paddingBottom?: string;
  paddingLeft?: string;
  
  // 边框
  border?: string;
  borderTop?: string;
  borderRight?: string;
  borderBottom?: string;
  borderLeft?: string;
  borderRadius?: string;
  
  // 背景
  backgroundColor?: string;
  backgroundImage?: string;
  backgroundSize?: string;
  backgroundPosition?: string;
  backgroundRepeat?: string;
  
  // 文字
  color?: string;
  fontSize?: string;
  fontWeight?: string;
  fontFamily?: string;
  lineHeight?: string;
  textAlign?: 'left' | 'center' | 'right' | 'justify';
  textDecoration?: string;
  
  // Flexbox
  flexDirection?: 'row' | 'column' | 'row-reverse' | 'column-reverse';
  justifyContent?: 'flex-start' | 'flex-end' | 'center' | 'space-between' | 'space-around' | 'space-evenly';
  alignItems?: 'flex-start' | 'flex-end' | 'center' | 'baseline' | 'stretch';
  flexWrap?: 'nowrap' | 'wrap' | 'wrap-reverse';
  gap?: string;
  
  // 阴影和透明度
  boxShadow?: string;
  opacity?: number;
  
  // 变换
  transform?: string;
  transition?: string;
}

// 页面定义
interface PageDefinition {
  id: string;
  name: string;
  title: string;
  description?: string;
  route: string;
  components: ComponentInstance[];
  globalStyle: string;
  scripts: string[];
  meta: {
    keywords?: string;
    description?: string;
    author?: string;
  };
  config: {
    responsive: boolean;
    theme: string;
    language: string;
  };
  createdAt: Date;
  updatedAt: Date;
}

4.2 拖拽系统实现

4.2.1 拖拽核心逻辑

// 拖拽管理器
class DragDropManager {
  private dragData: DragData | null = null;
  private dropZones: Map<string, DropZone> = new Map();
  private dragPreview: HTMLElement | null = null;
  private ghostElement: HTMLElement | null = null;
  
  constructor(private eventBus: EventBus) {
    this.initializeDragEvents();
  }

  private initializeDragEvents(): void {
    document.addEventListener('dragstart', this.handleDragStart.bind(this));
    document.addEventListener('dragover', this.handleDragOver.bind(this));
    document.addEventListener('dragenter', this.handleDragEnter.bind(this));
    document.addEventListener('dragleave', this.handleDragLeave.bind(this));
    document.addEventListener('drop', this.handleDrop.bind(this));
    document.addEventListener('dragend', this.handleDragEnd.bind(this));
  }

  // 开始拖拽
  startDrag(element: HTMLElement, data: DragData): void {
    this.dragData = data;
    element.draggable = true;
    
    // 创建拖拽预览
    this.createDragPreview(data);
    
    // 触发拖拽开始事件
    this.eventBus.emit('drag:start', { data, element });
  }

  private handleDragStart(event: DragEvent): void {
    if (!this.dragData) return;
    
    const target = event.target as HTMLElement;
    
    // 设置拖拽数据
    event.dataTransfer?.setData('application/json', JSON.stringify(this.dragData));
    event.dataTransfer!.effectAllowed = 'copy';
    
    // 创建幽灵元素
    this.createGhostElement(target);
    
    // 隐藏原始元素
    target.style.opacity = '0.5';
    
    this.eventBus.emit('drag:started', { data: this.dragData, event });
  }

  private handleDragOver(event: DragEvent): void {
    event.preventDefault();
    
    const dropZone = this.findDropZone(event.target as HTMLElement);
    if (dropZone && this.canDrop(dropZone, this.dragData)) {
      event.dataTransfer!.dropEffect = 'copy';
      this.showDropIndicator(dropZone, event);
    } else {
      event.dataTransfer!.dropEffect = 'none';
      this.hideDropIndicator();
    }
  }

  private handleDragEnter(event: DragEvent): void {
    event.preventDefault();
    
    const dropZone = this.findDropZone(event.target as HTMLElement);
    if (dropZone) {
      dropZone.element.classList.add('drag-over');
      this.eventBus.emit('drag:enter', { dropZone, event });
    }
  }

  private handleDragLeave(event: DragEvent): void {
    const dropZone = this.findDropZone(event.target as HTMLElement);
    if (dropZone) {
      dropZone.element.classList.remove('drag-over');
      this.eventBus.emit('drag:leave', { dropZone, event });
    }
  }

  private handleDrop(event: DragEvent): void {
    event.preventDefault();
    
    const dropZone = this.findDropZone(event.target as HTMLElement);
    if (!dropZone || !this.canDrop(dropZone, this.dragData)) {
      return;
    }
    
    const dragData = JSON.parse(event.dataTransfer?.getData('application/json') || '{}');
    const dropPosition = this.calculateDropPosition(dropZone, event);
    
    // 执行放置操作
    this.performDrop(dropZone, dragData, dropPosition);
    
    // 清理状态
    this.cleanup();
    
    this.eventBus.emit('drag:drop', {
      dropZone,
      dragData,
      dropPosition,
      event
    });
  }

  private handleDragEnd(event: DragEvent): void {
    this.cleanup();
    this.eventBus.emit('drag:end', { event });
  }

  // 注册放置区域
  registerDropZone(id: string, element: HTMLElement, config: DropZoneConfig): void {
    const dropZone: DropZone = {
      id,
      element,
      config,
      active: true
    };
    
    this.dropZones.set(id, dropZone);
    element.setAttribute('data-drop-zone', id);
  }

  // 注销放置区域
  unregisterDropZone(id: string): void {
    const dropZone = this.dropZones.get(id);
    if (dropZone) {
      dropZone.element.removeAttribute('data-drop-zone');
      this.dropZones.delete(id);
    }
  }

  private findDropZone(element: HTMLElement): DropZone | null {
    let current = element;
    
    while (current && current !== document.body) {
      const dropZoneId = current.getAttribute('data-drop-zone');
      if (dropZoneId) {
        return this.dropZones.get(dropZoneId) || null;
      }
      current = current.parentElement!;
    }
    
    return null;
  }

  private canDrop(dropZone: DropZone, dragData: DragData | null): boolean {
    if (!dragData || !dropZone.active) {
      return false;
    }
    
    const { config } = dropZone;
    
    // 检查允许的组件类型
    if (config.allowedTypes && !config.allowedTypes.includes(dragData.type)) {
      return false;
    }
    
    // 检查最大子元素数量
    if (config.maxChildren !== undefined) {
      const currentChildren = dropZone.element.children.length;
      if (currentChildren >= config.maxChildren) {
        return false;
      }
    }
    
    // 自定义验证
    if (config.validator) {
      return config.validator(dragData, dropZone);
    }
    
    return true;
  }

  private calculateDropPosition(dropZone: DropZone, event: DragEvent): DropPosition {
    const rect = dropZone.element.getBoundingClientRect();
    const x = event.clientX - rect.left;
    const y = event.clientY - rect.top;
    
    // 计算插入位置
    const children = Array.from(dropZone.element.children);
    let insertIndex = children.length;
    
    for (let i = 0; i < children.length; i++) {
      const childRect = children[i].getBoundingClientRect();
      const childY = childRect.top - rect.top;
      
      if (y < childY + childRect.height / 2) {
        insertIndex = i;
        break;
      }
    }
    
    return {
      x,
      y,
      insertIndex,
      relative: {
        x: x / rect.width,
        y: y / rect.height
      }
    };
  }

  private performDrop(dropZone: DropZone, dragData: DragData, position: DropPosition): void {
    // 根据拖拽数据类型执行不同的放置逻辑
    switch (dragData.source) {
      case 'component-library':
        this.createComponentFromLibrary(dropZone, dragData, position);
        break;
      case 'canvas':
        this.moveComponentOnCanvas(dropZone, dragData, position);
        break;
      case 'external':
        this.handleExternalDrop(dropZone, dragData, position);
        break;
    }
  }

  private createComponentFromLibrary(dropZone: DropZone, dragData: DragData, position: DropPosition): void {
    const componentDef = dragData.componentDefinition;
    if (!componentDef) return;
    
    // 创建组件实例
    const instance: ComponentInstance = {
      id: generateId(),
      type: componentDef.type,
      name: componentDef.name,
      props: { ...componentDef.defaultProps },
      style: {},
      events: {},
      children: [],
      locked: false,
      hidden: false,
      position: {
        x: position.x,
        y: position.y,
        z: 0
      },
      size: {
        width: 'auto',
        height: 'auto'
      }
    };
    
    // 添加到页面
    this.eventBus.emit('component:add', {
      instance,
      parentId: dropZone.id,
      insertIndex: position.insertIndex
    });
  }

  private moveComponentOnCanvas(dropZone: DropZone, dragData: DragData, position: DropPosition): void {
    const componentId = dragData.componentId;
    if (!componentId) return;
    
    this.eventBus.emit('component:move', {
      componentId,
      newParentId: dropZone.id,
      newPosition: position
    });
  }

  private createDragPreview(data: DragData): void {
    this.dragPreview = document.createElement('div');
    this.dragPreview.className = 'drag-preview';
    this.dragPreview.innerHTML = `
      <div class="drag-preview-content">
        <i class="${data.icon}"></i>
        <span>${data.name}</span>
      </div>
    `;
    
    document.body.appendChild(this.dragPreview);
  }

  private createGhostElement(target: HTMLElement): void {
    this.ghostElement = target.cloneNode(true) as HTMLElement;
    this.ghostElement.className += ' drag-ghost';
    this.ghostElement.style.position = 'absolute';
    this.ghostElement.style.top = '-1000px';
    this.ghostElement.style.pointerEvents = 'none';
    
    document.body.appendChild(this.ghostElement);
  }

  private showDropIndicator(dropZone: DropZone, event: DragEvent): void {
    const indicator = document.querySelector('.drop-indicator') as HTMLElement;
    if (!indicator) return;
    
    const position = this.calculateDropPosition(dropZone, event);
    const rect = dropZone.element.getBoundingClientRect();
    
    indicator.style.display = 'block';
    indicator.style.left = `${rect.left}px`;
    indicator.style.top = `${rect.top + position.y}px`;
    indicator.style.width = `${rect.width}px`;
  }

  private hideDropIndicator(): void {
    const indicator = document.querySelector('.drop-indicator') as HTMLElement;
    if (indicator) {
      indicator.style.display = 'none';
    }
  }

  private cleanup(): void {
    // 恢复原始元素透明度
    document.querySelectorAll('[draggable="true"]').forEach(el => {
      (el as HTMLElement).style.opacity = '';
    });
    
    // 移除拖拽预览
    if (this.dragPreview) {
      document.body.removeChild(this.dragPreview);
      this.dragPreview = null;
    }
    
    // 移除幽灵元素
    if (this.ghostElement) {
      document.body.removeChild(this.ghostElement);
      this.ghostElement = null;
    }
    
    // 清理放置区域状态
    this.dropZones.forEach(dropZone => {
      dropZone.element.classList.remove('drag-over');
    });
    
    // 隐藏放置指示器
    this.hideDropIndicator();
    
    // 重置拖拽数据
    this.dragData = null;
  }
}

// 拖拽数据接口
interface DragData {
  source: 'component-library' | 'canvas' | 'external';
  type: string;
  name: string;
  icon: string;
  componentDefinition?: ComponentDefinition;
  componentId?: string;
  data?: any;
}

// 放置区域接口
interface DropZone {
  id: string;
  element: HTMLElement;
  config: DropZoneConfig;
  active: boolean;
}

// 放置区域配置
interface DropZoneConfig {
  allowedTypes?: string[];
  maxChildren?: number;
  validator?: (dragData: DragData, dropZone: DropZone) => boolean;
}

// 放置位置
interface DropPosition {
  x: number;
  y: number;
  insertIndex: number;
  relative: {
    x: number;
    y: number;
  };
}

// 工具函数
function generateId(): string {
  return 'comp_' + Math.random().toString(36).substr(2, 9);
}

4.2.2 组件库面板实现

<template>
  <div class="component-library">
    <div class="library-header">
      <h3>组件库</h3>
      <div class="search-box">
        <input
          v-model="searchKeyword"
          type="text"
          placeholder="搜索组件..."
          class="search-input"
        />
      </div>
    </div>
    
    <div class="library-content">
      <div class="category-tabs">
        <button
          v-for="category in categories"
          :key="category.id"
          :class="['tab-button', { active: activeCategory === category.id }]"
          @click="activeCategory = category.id"
        >
          <i :class="category.icon"></i>
          {{ category.name }}
        </button>
      </div>
      
      <div class="components-grid">
        <div
          v-for="component in filteredComponents"
          :key="component.id"
          :class="['component-item', { dragging: draggingComponent === component.id }]"
          :draggable="true"
          @dragstart="handleDragStart(component, $event)"
          @dragend="handleDragEnd"
        >
          <div class="component-preview">
            <img
              v-if="component.preview"
              :src="component.preview"
              :alt="component.name"
              class="preview-image"
            />
            <i
              v-else
              :class="component.icon"
              class="preview-icon"
            ></i>
          </div>
          
          <div class="component-info">
            <h4 class="component-name">{{ component.name }}</h4>
            <p class="component-description">{{ component.description }}</p>
          </div>
          
          <div class="component-actions">
            <button
              class="action-button"
              @click="previewComponent(component)"
              title="预览"
            >
              <i class="icon-eye"></i>
            </button>
            <button
              class="action-button"
              @click="showComponentInfo(component)"
              title="详情"
            >
              <i class="icon-info"></i>
            </button>
          </div>
        </div>
      </div>
    </div>
    
    <!-- 组件预览弹窗 -->
    <ComponentPreviewModal
      v-if="previewModalVisible"
      :component="previewComponent"
      @close="previewModalVisible = false"
    />
    
    <!-- 组件详情弹窗 -->
    <ComponentInfoModal
      v-if="infoModalVisible"
      :component="selectedComponent"
      @close="infoModalVisible = false"
    />
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, inject } from 'vue';
import type { ComponentDefinition } from '../types';
import ComponentPreviewModal from './ComponentPreviewModal.vue';
import ComponentInfoModal from './ComponentInfoModal.vue';

// 注入依赖
const dragDropManager = inject<DragDropManager>('dragDropManager')!;
const componentRegistry = inject<ComponentRegistry>('componentRegistry')!;

// 响应式数据
const searchKeyword = ref('');
const activeCategory = ref('basic');
const draggingComponent = ref<string | null>(null);
const previewModalVisible = ref(false);
const infoModalVisible = ref(false);
const selectedComponent = ref<ComponentDefinition | null>(null);

// 组件分类
const categories = ref([
  { id: 'basic', name: '基础组件', icon: 'icon-basic' },
  { id: 'layout', name: '布局组件', icon: 'icon-layout' },
  { id: 'form', name: '表单组件', icon: 'icon-form' },
  { id: 'display', name: '展示组件', icon: 'icon-display' },
  { id: 'feedback', name: '反馈组件', icon: 'icon-feedback' },
  { id: 'navigation', name: '导航组件', icon: 'icon-navigation' },
  { id: 'chart', name: '图表组件', icon: 'icon-chart' },
  { id: 'custom', name: '自定义组件', icon: 'icon-custom' }
]);

// 组件列表
const components = ref<ComponentDefinition[]>([]);

// 计算属性
const filteredComponents = computed(() => {
  let filtered = components.value;
  
  // 按分类过滤
  if (activeCategory.value !== 'all') {
    filtered = filtered.filter(comp => comp.category === activeCategory.value);
  }
  
  // 按关键词搜索
  if (searchKeyword.value) {
    const keyword = searchKeyword.value.toLowerCase();
    filtered = filtered.filter(comp => 
      comp.name.toLowerCase().includes(keyword) ||
      comp.description.toLowerCase().includes(keyword)
    );
  }
  
  return filtered;
});

// 方法
const handleDragStart = (component: ComponentDefinition, event: DragEvent) => {
  draggingComponent.value = component.id;
  
  const dragData: DragData = {
    source: 'component-library',
    type: component.type,
    name: component.name,
    icon: component.icon,
    componentDefinition: component
  };
  
  dragDropManager.startDrag(event.target as HTMLElement, dragData);
};

const handleDragEnd = () => {
  draggingComponent.value = null;
};

const previewComponent = (component: ComponentDefinition) => {
  selectedComponent.value = component;
  previewModalVisible.value = true;
};

const showComponentInfo = (component: ComponentDefinition) => {
  selectedComponent.value = component;
  infoModalVisible.value = true;
};

const loadComponents = async () => {
  try {
    components.value = await componentRegistry.getAllComponents();
  } catch (error) {
    console.error('Failed to load components:', error);
  }
};

// 生命周期
onMounted(() => {
  loadComponents();
});
</script>

<style scoped>
.component-library {
  height: 100%;
  display: flex;
  flex-direction: column;
  background: #fff;
  border-right: 1px solid #e8e8e8;
}

.library-header {
  padding: 16px;
  border-bottom: 1px solid #e8e8e8;
}

.library-header h3 {
  margin: 0 0 12px 0;
  font-size: 16px;
  font-weight: 600;
  color: #333;
}

.search-input {
  width: 100%;
  padding: 8px 12px;
  border: 1px solid #d9d9d9;
  border-radius: 4px;
  font-size: 14px;
  outline: none;
  transition: border-color 0.3s;
}

.search-input:focus {
  border-color: #1890ff;
}

.library-content {
  flex: 1;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.category-tabs {
  display: flex;
  flex-wrap: wrap;
  padding: 8px;
  border-bottom: 1px solid #e8e8e8;
  background: #fafafa;
}

.tab-button {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 6px 12px;
  margin: 2px;
  border: none;
  border-radius: 4px;
  background: transparent;
  color: #666;
  font-size: 12px;
  cursor: pointer;
  transition: all 0.3s;
}

.tab-button:hover {
  background: #e6f7ff;
  color: #1890ff;
}

.tab-button.active {
  background: #1890ff;
  color: #fff;
}

.components-grid {
  flex: 1;
  overflow-y: auto;
  padding: 8px;
}

.component-item {
  display: flex;
  align-items: center;
  padding: 12px;
  margin-bottom: 8px;
  border: 1px solid #e8e8e8;
  border-radius: 6px;
  background: #fff;
  cursor: grab;
  transition: all 0.3s;
  user-select: none;
}

.component-item:hover {
  border-color: #1890ff;
  box-shadow: 0 2px 8px rgba(24, 144, 255, 0.1);
}

.component-item.dragging {
  opacity: 0.5;
  cursor: grabbing;
}

.component-preview {
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-right: 12px;
  border-radius: 4px;
  background: #f5f5f5;
}

.preview-image {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 4px;
}

.preview-icon {
  font-size: 20px;
  color: #666;
}

.component-info {
  flex: 1;
  min-width: 0;
}

.component-name {
  margin: 0 0 4px 0;
  font-size: 14px;
  font-weight: 500;
  color: #333;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.component-description {
  margin: 0;
  font-size: 12px;
  color: #999;
  line-height: 1.4;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.component-actions {
  display: flex;
  gap: 4px;
  opacity: 0;
  transition: opacity 0.3s;
}

.component-item:hover .component-actions {
  opacity: 1;
}

.action-button {
  width: 24px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 4px;
  background: #f5f5f5;
  color: #666;
  cursor: pointer;
  transition: all 0.3s;
}

.action-button:hover {
  background: #1890ff;
  color: #fff;
}
</style>

4.3 画布渲染引擎

4.3.1 渲染引擎核心

// 渲染引擎
class CanvasRenderer {
  private container: HTMLElement;
  private componentRegistry: ComponentRegistry;
  private eventBus: EventBus;
  private renderCache: Map<string, VNode> = new Map();
  private componentInstances: Map<string, ComponentInstance> = new Map();
  
  constructor(
    container: HTMLElement,
    componentRegistry: ComponentRegistry,
    eventBus: EventBus
  ) {
    this.container = container;
    this.componentRegistry = componentRegistry;
    this.eventBus = eventBus;
    
    this.initializeRenderer();
  }

  private initializeRenderer(): void {
    // 监听组件变化事件
    this.eventBus.on('component:add', this.handleComponentAdd.bind(this));
    this.eventBus.on('component:update', this.handleComponentUpdate.bind(this));
    this.eventBus.on('component:remove', this.handleComponentRemove.bind(this));
    this.eventBus.on('component:move', this.handleComponentMove.bind(this));
    
    // 初始化画布
    this.setupCanvas();
  }

  private setupCanvas(): void {
    this.container.className = 'design-canvas';
    this.container.innerHTML = `
      <div class="canvas-content">
        <div class="canvas-viewport" id="canvas-viewport">
          <!-- 组件将在这里渲染 -->
        </div>
        <div class="canvas-overlay">
          <div class="selection-box" id="selection-box"></div>
          <div class="resize-handles" id="resize-handles"></div>
          <div class="alignment-guides" id="alignment-guides"></div>
        </div>
      </div>
    `;
    
    // 设置画布事件
    this.setupCanvasEvents();
  }

  private setupCanvasEvents(): void {
    const viewport = this.container.querySelector('#canvas-viewport') as HTMLElement;
    
    // 点击选择
    viewport.addEventListener('click', this.handleCanvasClick.bind(this));
    
    // 右键菜单
    viewport.addEventListener('contextmenu', this.handleContextMenu.bind(this));
    
    // 键盘事件
    document.addEventListener('keydown', this.handleKeyDown.bind(this));
  }

  // 渲染页面
  renderPage(page: PageDefinition): void {
    const viewport = this.container.querySelector('#canvas-viewport') as HTMLElement;
    
    // 清空画布
    viewport.innerHTML = '';
    this.componentInstances.clear();
    this.renderCache.clear();
    
    // 渲染组件树
    page.components.forEach(component => {
      this.renderComponent(component, viewport);
    });
    
    // 应用全局样式
    this.applyGlobalStyle(page.globalStyle);
  }

  // 渲染单个组件
  private renderComponent(instance: ComponentInstance, parent: HTMLElement): HTMLElement {
    // 检查缓存
    const cached = this.renderCache.get(instance.id);
    if (cached && !this.hasInstanceChanged(instance)) {
      return cached as HTMLElement;
    }
    
    // 获取组件定义
    const definition = this.componentRegistry.getComponent(instance.type);
    if (!definition) {
      console.warn(`Component type "${instance.type}" not found`);
      return this.createErrorComponent(instance);
    }
    
    // 创建组件元素
    const element = this.createComponentElement(instance, definition);
    
    // 应用样式
    this.applyComponentStyle(element, instance.style);
    
    // 设置属性
    this.applyComponentProps(element, instance.props, definition);
    
    // 绑定事件
    this.bindComponentEvents(element, instance.events);
    
    // 渲染子组件
    if (instance.children && instance.children.length > 0) {
      instance.children.forEach(child => {
        const childElement = this.renderComponent(child, element);
        element.appendChild(childElement);
      });
    }
    
    // 添加设计时功能
    this.addDesignTimeFeatures(element, instance);
    
    // 缓存渲染结果
    this.renderCache.set(instance.id, element);
    this.componentInstances.set(instance.id, instance);
    
    // 添加到父容器
    parent.appendChild(element);
    
    return element;
  }

  private createComponentElement(instance: ComponentInstance, definition: ComponentDefinition): HTMLElement {
    // 根据组件类型创建不同的元素
    switch (definition.type) {
      case 'text':
        return this.createTextComponent(instance, definition);
      case 'button':
        return this.createButtonComponent(instance, definition);
      case 'image':
        return this.createImageComponent(instance, definition);
      case 'container':
        return this.createContainerComponent(instance, definition);
      case 'form':
        return this.createFormComponent(instance, definition);
      default:
        return this.createCustomComponent(instance, definition);
    }
  }

  private createTextComponent(instance: ComponentInstance, definition: ComponentDefinition): HTMLElement {
    const element = document.createElement('div');
    element.className = 'component-text';
    element.textContent = instance.props.text || '文本内容';
    return element;
  }

  private createButtonComponent(instance: ComponentInstance, definition: ComponentDefinition): HTMLElement {
    const element = document.createElement('button');
    element.className = 'component-button';
    element.textContent = instance.props.text || '按钮';
    element.type = instance.props.type || 'button';
    
    if (instance.props.disabled) {
      element.disabled = true;
    }
    
    return element;
  }

  private createImageComponent(instance: ComponentInstance, definition: ComponentDefinition): HTMLElement {
    const element = document.createElement('img');
    element.className = 'component-image';
    element.src = instance.props.src || '/placeholder-image.png';
    element.alt = instance.props.alt || '图片';
    
    // 防止拖拽
    element.draggable = false;
    
    return element;
  }

  private createContainerComponent(instance: ComponentInstance, definition: ComponentDefinition): HTMLElement {
    const element = document.createElement('div');
    element.className = 'component-container';
    
    // 设置布局类型
    const layout = instance.props.layout || 'block';
    element.setAttribute('data-layout', layout);
    
    return element;
  }

  private createFormComponent(instance: ComponentInstance, definition: ComponentDefinition): HTMLElement {
    const element = document.createElement('form');
    element.className = 'component-form';
    
    // 阻止表单提交
    element.addEventListener('submit', (e) => {
      e.preventDefault();
    });
    
    return element;
  }

  private createCustomComponent(instance: ComponentInstance, definition: ComponentDefinition): HTMLElement {
    // 对于自定义组件,使用 Vue 组件渲染
    const element = document.createElement('div');
    element.className = `component-${definition.type}`;
    
    // 这里可以集成 Vue 的渲染逻辑
    // 或者使用 Web Components
    
    return element;
  }

  private applyComponentStyle(element: HTMLElement, style: ComponentStyle): void {
    Object.entries(style).forEach(([property, value]) => {
      if (value !== undefined && value !== null) {
        // 转换驼峰命名为短横线命名
        const cssProperty = property.replace(/([A-Z])/g, '-$1').toLowerCase();
        element.style.setProperty(cssProperty, String(value));
      }
    });
  }

  private applyComponentProps(element: HTMLElement, props: Record<string, any>, definition: ComponentDefinition): void {
    definition.props.forEach(propDef => {
      const value = props[propDef.name];
      if (value !== undefined) {
        this.setElementProperty(element, propDef, value);
      }
    });
  }

  private setElementProperty(element: HTMLElement, propDef: ComponentProp, value: any): void {
    switch (propDef.type) {
      case 'string':
      case 'textarea':
        if (propDef.name === 'text' || propDef.name === 'content') {
          element.textContent = value;
        } else {
          element.setAttribute(propDef.name, value);
        }
        break;
        
      case 'number':
        element.setAttribute(propDef.name, String(value));
        break;
        
      case 'boolean':
        if (value) {
          element.setAttribute(propDef.name, '');
        } else {
          element.removeAttribute(propDef.name);
        }
        break;
        
      case 'color':
        element.style.setProperty(`--${propDef.name}`, value);
        break;
        
      case 'image':
        if (element instanceof HTMLImageElement) {
          element.src = value;
        } else {
          element.style.backgroundImage = `url(${value})`;
        }
        break;
        
      default:
        element.setAttribute(propDef.name, JSON.stringify(value));
    }
  }

  private bindComponentEvents(element: HTMLElement, events: Record<string, string>): void {
    Object.entries(events).forEach(([eventName, handlerCode]) => {
      if (handlerCode) {
        element.addEventListener(eventName, (event) => {
          try {
            // 在设计模式下,阻止事件的默认行为
            event.preventDefault();
            event.stopPropagation();
            
            // 这里可以执行事件处理代码
            // 在实际应用中,可能需要更安全的代码执行环境
            console.log(`Event ${eventName} triggered:`, handlerCode);
          } catch (error) {
            console.error(`Error executing event handler for ${eventName}:`, error);
          }
        });
      }
    });
  }

  private addDesignTimeFeatures(element: HTMLElement, instance: ComponentInstance): void {
    // 添加组件标识
    element.setAttribute('data-component-id', instance.id);
    element.setAttribute('data-component-type', instance.type);
    
    // 添加选择功能
    element.addEventListener('click', (event) => {
      event.stopPropagation();
      this.selectComponent(instance.id);
    });
    
    // 添加悬停效果
    element.addEventListener('mouseenter', () => {
      element.classList.add('component-hover');
    });
    
    element.addEventListener('mouseleave', () => {
      element.classList.remove('component-hover');
    });
    
    // 如果组件被锁定,添加锁定样式
    if (instance.locked) {
      element.classList.add('component-locked');
    }
    
    // 如果组件被隐藏,添加隐藏样式
    if (instance.hidden) {
      element.classList.add('component-hidden');
    }
  }

  private selectComponent(componentId: string): void {
    // 清除之前的选择
    this.clearSelection();
    
    // 选择新组件
    const element = this.container.querySelector(`[data-component-id="${componentId}"]`) as HTMLElement;
    if (element) {
      element.classList.add('component-selected');
      this.showSelectionBox(element);
      this.showResizeHandles(element);
      
      // 触发选择事件
      this.eventBus.emit('component:select', { componentId });
    }
  }

  private clearSelection(): void {
    // 移除选择样式
    this.container.querySelectorAll('.component-selected').forEach(el => {
      el.classList.remove('component-selected');
    });
    
    // 隐藏选择框和调整手柄
    this.hideSelectionBox();
    this.hideResizeHandles();
  }

  private showSelectionBox(element: HTMLElement): void {
    const selectionBox = this.container.querySelector('#selection-box') as HTMLElement;
    const rect = element.getBoundingClientRect();
    const canvasRect = this.container.getBoundingClientRect();
    
    selectionBox.style.display = 'block';
    selectionBox.style.left = `${rect.left - canvasRect.left}px`;
    selectionBox.style.top = `${rect.top - canvasRect.top}px`;
    selectionBox.style.width = `${rect.width}px`;
    selectionBox.style.height = `${rect.height}px`;
  }

  private hideSelectionBox(): void {
    const selectionBox = this.container.querySelector('#selection-box') as HTMLElement;
    selectionBox.style.display = 'none';
  }

  private showResizeHandles(element: HTMLElement): void {
    const resizeHandles = this.container.querySelector('#resize-handles') as HTMLElement;
    const rect = element.getBoundingClientRect();
    const canvasRect = this.container.getBoundingClientRect();
    
    resizeHandles.innerHTML = `
      <div class="resize-handle nw" data-direction="nw"></div>
      <div class="resize-handle n" data-direction="n"></div>
      <div class="resize-handle ne" data-direction="ne"></div>
      <div class="resize-handle e" data-direction="e"></div>
      <div class="resize-handle se" data-direction="se"></div>
      <div class="resize-handle s" data-direction="s"></div>
      <div class="resize-handle sw" data-direction="sw"></div>
      <div class="resize-handle w" data-direction="w"></div>
    `;
    
    resizeHandles.style.display = 'block';
    resizeHandles.style.left = `${rect.left - canvasRect.left}px`;
    resizeHandles.style.top = `${rect.top - canvasRect.top}px`;
    resizeHandles.style.width = `${rect.width}px`;
    resizeHandles.style.height = `${rect.height}px`;
    
    // 绑定调整大小事件
    this.bindResizeEvents(resizeHandles, element);
  }

  private hideResizeHandles(): void {
    const resizeHandles = this.container.querySelector('#resize-handles') as HTMLElement;
    resizeHandles.style.display = 'none';
    resizeHandles.innerHTML = '';
  }

  private bindResizeEvents(resizeHandles: HTMLElement, targetElement: HTMLElement): void {
    const handles = resizeHandles.querySelectorAll('.resize-handle');
    
    handles.forEach(handle => {
      handle.addEventListener('mousedown', (event) => {
        event.stopPropagation();
        
        const direction = handle.getAttribute('data-direction')!;
        this.startResize(targetElement, direction, event as MouseEvent);
      });
    });
  }

  private startResize(element: HTMLElement, direction: string, startEvent: MouseEvent): void {
    const startRect = element.getBoundingClientRect();
    const startX = startEvent.clientX;
    const startY = startEvent.clientY;
    
    const handleMouseMove = (event: MouseEvent) => {
      const deltaX = event.clientX - startX;
      const deltaY = event.clientY - startY;
      
      this.updateElementSize(element, direction, deltaX, deltaY, startRect);
    };
    
    const handleMouseUp = () => {
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);
      
      // 触发大小变化事件
      const componentId = element.getAttribute('data-component-id')!;
      this.eventBus.emit('component:resize', {
        componentId,
        newSize: {
          width: element.style.width,
          height: element.style.height
        }
      });
    };
    
    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', handleMouseUp);
  }

  private updateElementSize(element: HTMLElement, direction: string, deltaX: number, deltaY: number, startRect: DOMRect): void {
    let newWidth = startRect.width;
    let newHeight = startRect.height;
    let newLeft = startRect.left;
    let newTop = startRect.top;
    
    switch (direction) {
      case 'e':
        newWidth = startRect.width + deltaX;
        break;
      case 'w':
        newWidth = startRect.width - deltaX;
        newLeft = startRect.left + deltaX;
        break;
      case 's':
        newHeight = startRect.height + deltaY;
        break;
      case 'n':
        newHeight = startRect.height - deltaY;
        newTop = startRect.top + deltaY;
        break;
      case 'se':
        newWidth = startRect.width + deltaX;
        newHeight = startRect.height + deltaY;
        break;
      case 'sw':
        newWidth = startRect.width - deltaX;
        newHeight = startRect.height + deltaY;
        newLeft = startRect.left + deltaX;
        break;
      case 'ne':
        newWidth = startRect.width + deltaX;
        newHeight = startRect.height - deltaY;
        newTop = startRect.top + deltaY;
        break;
      case 'nw':
        newWidth = startRect.width - deltaX;
        newHeight = startRect.height - deltaY;
        newLeft = startRect.left + deltaX;
        newTop = startRect.top + deltaY;
        break;
    }
    
    // 限制最小尺寸
    newWidth = Math.max(20, newWidth);
    newHeight = Math.max(20, newHeight);
    
    // 应用新尺寸
    element.style.width = `${newWidth}px`;
    element.style.height = `${newHeight}px`;
    
    // 如果需要移动位置
    if (direction.includes('w') || direction.includes('n')) {
      element.style.left = `${newLeft}px`;
      element.style.top = `${newTop}px`;
    }
    
    // 更新选择框和调整手柄位置
    this.showSelectionBox(element);
    this.showResizeHandles(element);
  }

  // 事件处理方法
  private handleComponentAdd(data: { instance: ComponentInstance; parentId?: string; insertIndex?: number }): void {
    const { instance, parentId, insertIndex } = data;
    
    let parent: HTMLElement;
    if (parentId) {
      parent = this.container.querySelector(`[data-component-id="${parentId}"]`) as HTMLElement;
    } else {
      parent = this.container.querySelector('#canvas-viewport') as HTMLElement;
    }
    
    if (parent) {
      const element = this.renderComponent(instance, parent);
      
      // 如果指定了插入位置
      if (insertIndex !== undefined && insertIndex < parent.children.length - 1) {
        parent.insertBefore(element, parent.children[insertIndex]);
      }
      
      // 自动选择新添加的组件
      this.selectComponent(instance.id);
    }
  }

  private handleComponentUpdate(data: { componentId: string; updates: Partial<ComponentInstance> }): void {
    const { componentId, updates } = data;
    const element = this.container.querySelector(`[data-component-id="${componentId}"]`) as HTMLElement;
    
    if (element) {
      const instance = this.componentInstances.get(componentId);
      if (instance) {
        // 更新实例数据
        Object.assign(instance, updates);
        
        // 重新渲染组件
        const parent = element.parentElement!;
        const nextSibling = element.nextSibling;
        parent.removeChild(element);
        
        const newElement = this.renderComponent(instance, parent);
        
        if (nextSibling) {
          parent.insertBefore(newElement, nextSibling);
        }
      }
    }
  }

  private handleComponentRemove(data: { componentId: string }): void {
    const { componentId } = data;
    const element = this.container.querySelector(`[data-component-id="${componentId}"]`) as HTMLElement;
    
    if (element) {
      element.remove();
      this.componentInstances.delete(componentId);
      this.renderCache.delete(componentId);
      
      // 清除选择状态
      this.clearSelection();
    }
  }

  private handleComponentMove(data: { componentId: string; newParentId: string; newPosition: DropPosition }): void {
    const { componentId, newParentId, newPosition } = data;
    
    const element = this.container.querySelector(`[data-component-id="${componentId}"]`) as HTMLElement;
    const newParent = this.container.querySelector(`[data-component-id="${newParentId}"]`) as HTMLElement;
    
    if (element && newParent) {
      // 移动元素
      if (newPosition.insertIndex < newParent.children.length) {
        newParent.insertBefore(element, newParent.children[newPosition.insertIndex]);
      } else {
        newParent.appendChild(element);
      }
      
      // 更新位置信息
      const instance = this.componentInstances.get(componentId);
      if (instance) {
        instance.position.x = newPosition.x;
        instance.position.y = newPosition.y;
        instance.parentId = newParentId;
      }
    }
  }

  private handleCanvasClick(event: MouseEvent): void {
    // 如果点击的是画布空白区域,清除选择
    if (event.target === event.currentTarget) {
      this.clearSelection();
      this.eventBus.emit('component:select', { componentId: null });
    }
  }

  private handleContextMenu(event: MouseEvent): void {
    event.preventDefault();
    
    const target = event.target as HTMLElement;
    const componentId = target.getAttribute('data-component-id');
    
    // 显示右键菜单
    this.eventBus.emit('context-menu:show', {
      x: event.clientX,
      y: event.clientY,
      componentId
    });
  }

  private handleKeyDown(event: KeyboardEvent): void {
    // 删除选中的组件
    if (event.key === 'Delete' || event.key === 'Backspace') {
      const selectedElement = this.container.querySelector('.component-selected') as HTMLElement;
      if (selectedElement) {
        const componentId = selectedElement.getAttribute('data-component-id')!;
        this.eventBus.emit('component:delete', { componentId });
      }
    }
    
    // 复制组件
    if (event.ctrlKey && event.key === 'c') {
      const selectedElement = this.container.querySelector('.component-selected') as HTMLElement;
      if (selectedElement) {
        const componentId = selectedElement.getAttribute('data-component-id')!;
        this.eventBus.emit('component:copy', { componentId });
      }
    }
    
    // 粘贴组件
    if (event.ctrlKey && event.key === 'v') {
      this.eventBus.emit('component:paste');
    }
    
    // 撤销
    if (event.ctrlKey && event.key === 'z' && !event.shiftKey) {
      this.eventBus.emit('history:undo');
    }
    
    // 重做
    if (event.ctrlKey && (event.key === 'y' || (event.key === 'z' && event.shiftKey))) {
      this.eventBus.emit('history:redo');
    }
  }

  private createErrorComponent(instance: ComponentInstance): HTMLElement {
    const element = document.createElement('div');
    element.className = 'component-error';
    element.innerHTML = `
      <div class="error-content">
        <i class="icon-warning"></i>
        <span>组件类型 "${instance.type}" 未找到</span>
      </div>
    `;
    return element;
  }

  private hasInstanceChanged(instance: ComponentInstance): boolean {
    const cached = this.componentInstances.get(instance.id);
    if (!cached) return true;
    
    // 简单的深度比较(实际项目中可能需要更高效的比较方法)
    return JSON.stringify(cached) !== JSON.stringify(instance);
  }

  private applyGlobalStyle(globalStyle: string): void {
    let styleElement = document.getElementById('canvas-global-style') as HTMLStyleElement;
    
    if (!styleElement) {
      styleElement = document.createElement('style');
      styleElement.id = 'canvas-global-style';
      document.head.appendChild(styleElement);
    }
    
    styleElement.textContent = globalStyle;
  }

  // 公共方法
  public getSelectedComponent(): string | null {
    const selectedElement = this.container.querySelector('.component-selected') as HTMLElement;
    return selectedElement ? selectedElement.getAttribute('data-component-id') : null;
  }

  public selectComponentById(componentId: string): void {
    this.selectComponent(componentId);
  }

  public clearComponentSelection(): void {
    this.clearSelection();
  }

  public getComponentElement(componentId: string): HTMLElement | null {
    return this.container.querySelector(`[data-component-id="${componentId}"]`) as HTMLElement;
  }

  public updateComponentStyle(componentId: string, style: Partial<ComponentStyle>): void {
    const element = this.getComponentElement(componentId);
    if (element) {
      this.applyComponentStyle(element, style as ComponentStyle);
      
      // 更新实例数据
      const instance = this.componentInstances.get(componentId);
      if (instance) {
        Object.assign(instance.style, style);
      }
    }
  }

  public destroy(): void {
    // 清理事件监听器
    this.eventBus.off('component:add', this.handleComponentAdd);
    this.eventBus.off('component:update', this.handleComponentUpdate);
    this.eventBus.off('component:remove', this.handleComponentRemove);
    this.eventBus.off('component:move', this.handleComponentMove);
    
    // 清理数据
    this.componentInstances.clear();
    this.renderCache.clear();
    
    // 清理DOM
    this.container.innerHTML = '';
  }
}

4.3.2 组件注册系统

// 组件注册表
class ComponentRegistry {
  private components: Map<string, ComponentDefinition> = new Map();
  private categories: Map<string, ComponentCategory> = new Map();
  
  constructor() {
    this.initializeBuiltinComponents();
  }

  // 注册组件
  registerComponent(definition: ComponentDefinition): void {
    // 验证组件定义
    this.validateComponentDefinition(definition);
    
    // 注册组件
    this.components.set(definition.type, definition);
    
    // 更新分类
    this.updateCategory(definition.category, definition);
    
    console.log(`Component "${definition.type}" registered successfully`);
  }

  // 批量注册组件
  registerComponents(definitions: ComponentDefinition[]): void {
    definitions.forEach(def => this.registerComponent(def));
  }

  // 获取组件定义
  getComponent(type: string): ComponentDefinition | undefined {
    return this.components.get(type);
  }

  // 获取所有组件
  getAllComponents(): ComponentDefinition[] {
    return Array.from(this.components.values());
  }

  // 按分类获取组件
  getComponentsByCategory(category: string): ComponentDefinition[] {
    return Array.from(this.components.values())
      .filter(comp => comp.category === category);
  }

  // 获取所有分类
  getCategories(): ComponentCategory[] {
    return Array.from(this.categories.values());
  }

  // 搜索组件
  searchComponents(keyword: string): ComponentDefinition[] {
    const lowerKeyword = keyword.toLowerCase();
    return Array.from(this.components.values())
      .filter(comp => 
        comp.name.toLowerCase().includes(lowerKeyword) ||
        comp.description.toLowerCase().includes(lowerKeyword) ||
        comp.type.toLowerCase().includes(lowerKeyword)
      );
  }

  private validateComponentDefinition(definition: ComponentDefinition): void {
    if (!definition.id || !definition.type || !definition.name) {
      throw new Error('Component definition must have id, type, and name');
    }
    
    if (this.components.has(definition.type)) {
      throw new Error(`Component type "${definition.type}" already exists`);
    }
    
    // 验证属性定义
    definition.props.forEach(prop => {
      if (!prop.name || !prop.type) {
        throw new Error('Component property must have name and type');
      }
    });
    
    // 验证事件定义
    definition.events.forEach(event => {
      if (!event.name) {
        throw new Error('Component event must have name');
      }
    });
  }

  private updateCategory(categoryId: string, component: ComponentDefinition): void {
    let category = this.categories.get(categoryId);
    
    if (!category) {
      category = {
        id: categoryId,
        name: this.getCategoryName(categoryId),
        icon: this.getCategoryIcon(categoryId),
        components: []
      };
      this.categories.set(categoryId, category);
    }
    
    if (!category.components.find(c => c.type === component.type)) {
      category.components.push(component);
    }
  }

  private getCategoryName(categoryId: string): string {
    const categoryNames: Record<string, string> = {
      'basic': '基础组件',
      'layout': '布局组件',
      'form': '表单组件',
      'display': '展示组件',
      'feedback': '反馈组件',
      'navigation': '导航组件',
      'chart': '图表组件',
      'custom': '自定义组件'
    };
    
    return categoryNames[categoryId] || categoryId;
  }

  private getCategoryIcon(categoryId: string): string {
    const categoryIcons: Record<string, string> = {
      'basic': 'icon-basic',
      'layout': 'icon-layout',
      'form': 'icon-form',
      'display': 'icon-display',
      'feedback': 'icon-feedback',
      'navigation': 'icon-navigation',
      'chart': 'icon-chart',
      'custom': 'icon-custom'
    };
    
    return categoryIcons[categoryId] || 'icon-component';
  }

  private initializeBuiltinComponents(): void {
    // 注册内置组件
    const builtinComponents: ComponentDefinition[] = [
      {
        id: 'text-component',
        name: '文本',
        type: 'text',
        category: 'basic',
        icon: 'icon-text',
        description: '用于显示文本内容的组件',
        props: [
          {
            name: 'text',
            type: 'textarea',
            label: '文本内容',
            defaultValue: '文本内容',
            required: true
          },
          {
            name: 'fontSize',
            type: 'number',
            label: '字体大小',
            defaultValue: 14
          },
          {
            name: 'color',
            type: 'color',
            label: '文字颜色',
            defaultValue: '#333333'
          },
          {
            name: 'fontWeight',
            type: 'select',
            label: '字体粗细',
            defaultValue: 'normal',
            options: [
              { label: '正常', value: 'normal' },
              { label: '粗体', value: 'bold' },
              { label: '细体', value: 'lighter' }
            ]
          }
        ],
        events: [
          {
            name: 'click',
            label: '点击事件',
            params: [
              { name: 'event', type: 'MouseEvent', description: '鼠标事件对象' }
            ]
          }
        ],
        slots: [],
        defaultProps: {
          text: '文本内容',
          fontSize: 14,
          color: '#333333',
          fontWeight: 'normal'
        },
        preview: '/previews/text-component.png',
        version: '1.0.0'
      },
      {
        id: 'button-component',
        name: '按钮',
        type: 'button',
        category: 'basic',
        icon: 'icon-button',
        description: '可点击的按钮组件',
        props: [
          {
            name: 'text',
            type: 'string',
            label: '按钮文字',
            defaultValue: '按钮',
            required: true
          },
          {
            name: 'type',
            type: 'select',
            label: '按钮类型',
            defaultValue: 'primary',
            options: [
              { label: '主要按钮', value: 'primary' },
              { label: '次要按钮', value: 'secondary' },
              { label: '危险按钮', value: 'danger' },
              { label: '链接按钮', value: 'link' }
            ]
          },
          {
            name: 'size',
            type: 'select',
            label: '按钮大小',
            defaultValue: 'medium',
            options: [
              { label: '小', value: 'small' },
              { label: '中', value: 'medium' },
              { label: '大', value: 'large' }
            ]
          },
          {
            name: 'disabled',
            type: 'boolean',
            label: '禁用状态',
            defaultValue: false
          },
          {
            name: 'loading',
            type: 'boolean',
            label: '加载状态',
            defaultValue: false
          }
        ],
        events: [
          {
            name: 'click',
            label: '点击事件',
            params: [
              { name: 'event', type: 'MouseEvent', description: '鼠标事件对象' }
            ]
          }
        ],
        slots: [],
        defaultProps: {
          text: '按钮',
          type: 'primary',
          size: 'medium',
          disabled: false,
          loading: false
        },
        preview: '/previews/button-component.png',
        version: '1.0.0'
      }
    ];
    
    this.registerComponents(builtinComponents);
  }
}

// 组件分类接口
interface ComponentCategory {
  id: string;
  name: string;
  icon: string;
  components: ComponentDefinition[];
}

4.4 属性编辑器

4.4.1 属性编辑器核心

// 属性编辑器
class PropertyEditor {
  private container: HTMLElement;
  private currentComponent: ComponentInstance | null = null;
  private propertyControls: Map<string, PropertyControl> = new Map();
  private eventBus: EventBus;
  private componentRegistry: ComponentRegistry;
  
  constructor(
    container: HTMLElement,
    eventBus: EventBus,
    componentRegistry: ComponentRegistry
  ) {
    this.container = container;
    this.eventBus = eventBus;
    this.componentRegistry = componentRegistry;
    
    this.initializeEditor();
    this.bindEvents();
  }

  private initializeEditor(): void {
    this.container.innerHTML = `
      <div class="property-editor">
        <div class="property-header">
          <h3 class="property-title">属性编辑器</h3>
          <div class="property-actions">
            <button class="btn-reset" title="重置属性">
              <i class="icon-reset"></i>
            </button>
            <button class="btn-copy" title="复制属性">
              <i class="icon-copy"></i>
            </button>
            <button class="btn-paste" title="粘贴属性">
              <i class="icon-paste"></i>
            </button>
          </div>
        </div>
        <div class="property-content">
          <div class="property-empty">
            <i class="icon-select"></i>
            <p>请选择一个组件</p>
          </div>
        </div>
      </div>
    `;
  }

  private bindEvents(): void {
    // 监听组件选择事件
    this.eventBus.on('component:select', (componentId: string) => {
      this.loadComponentProperties(componentId);
    });
    
    // 监听组件取消选择事件
    this.eventBus.on('component:deselect', () => {
      this.clearProperties();
    });
    
    // 监听组件更新事件
    this.eventBus.on('component:update', (data: any) => {
      if (this.currentComponent && this.currentComponent.id === data.componentId) {
        this.updatePropertyValues(data.props);
      }
    });
    
    // 绑定操作按钮事件
    this.bindActionEvents();
  }

  private bindActionEvents(): void {
    const resetBtn = this.container.querySelector('.btn-reset') as HTMLButtonElement;
    const copyBtn = this.container.querySelector('.btn-copy') as HTMLButtonElement;
    const pasteBtn = this.container.querySelector('.btn-paste') as HTMLButtonElement;
    
    resetBtn?.addEventListener('click', () => this.resetProperties());
    copyBtn?.addEventListener('click', () => this.copyProperties());
    pasteBtn?.addEventListener('click', () => this.pasteProperties());
  }

  private loadComponentProperties(componentId: string): void {
    // 获取组件实例
    const component = this.getComponentInstance(componentId);
    if (!component) return;
    
    this.currentComponent = component;
    
    // 获取组件定义
    const definition = this.componentRegistry.getComponent(component.type);
    if (!definition) return;
    
    // 渲染属性编辑器
    this.renderPropertyEditor(definition, component);
  }

  private renderPropertyEditor(definition: ComponentDefinition, component: ComponentInstance): void {
    const content = this.container.querySelector('.property-content') as HTMLElement;
    
    content.innerHTML = `
      <div class="property-sections">
        <div class="property-section">
          <div class="section-header">
            <h4>基本属性</h4>
            <button class="section-toggle" data-section="basic">
              <i class="icon-chevron-down"></i>
            </button>
          </div>
          <div class="section-content" data-section="basic">
            ${this.renderBasicProperties(definition, component)}
          </div>
        </div>
        
        <div class="property-section">
          <div class="section-header">
            <h4>样式属性</h4>
            <button class="section-toggle" data-section="style">
              <i class="icon-chevron-down"></i>
            </button>
          </div>
          <div class="section-content" data-section="style">
            ${this.renderStyleProperties(component)}
          </div>
        </div>
        
        <div class="property-section">
          <div class="section-header">
            <h4>事件处理</h4>
            <button class="section-toggle" data-section="events">
              <i class="icon-chevron-down"></i>
            </button>
          </div>
          <div class="section-content" data-section="events">
            ${this.renderEventProperties(definition, component)}
          </div>
        </div>
      </div>
    `;
    
    // 绑定属性控件事件
    this.bindPropertyEvents();
    
    // 绑定折叠事件
    this.bindSectionToggleEvents();
  }

  private renderBasicProperties(definition: ComponentDefinition, component: ComponentInstance): string {
    return definition.props.map(prop => {
      const value = component.props[prop.name] ?? prop.defaultValue;
      return this.renderPropertyControl(prop, value);
    }).join('');
  }

  private renderStyleProperties(component: ComponentInstance): string {
    const styleProps = [
      { name: 'width', type: 'string', label: '宽度' },
      { name: 'height', type: 'string', label: '高度' },
      { name: 'margin', type: 'string', label: '外边距' },
      { name: 'padding', type: 'string', label: '内边距' },
      { name: 'backgroundColor', type: 'color', label: '背景色' },
      { name: 'border', type: 'string', label: '边框' },
      { name: 'borderRadius', type: 'string', label: '圆角' },
      { name: 'opacity', type: 'number', label: '透明度', min: 0, max: 1, step: 0.1 }
    ];
    
    return styleProps.map(prop => {
      const value = component.style[prop.name] || '';
      return this.renderPropertyControl(prop as PropertyDefinition, value);
    }).join('');
  }

  private renderEventProperties(definition: ComponentDefinition, component: ComponentInstance): string {
    return definition.events.map(event => {
      const handler = component.events[event.name] || '';
      return `
        <div class="property-item">
          <label class="property-label">${event.label}</label>
          <div class="property-control">
            <textarea 
              class="property-textarea"
              data-property="${event.name}"
              data-type="event"
              placeholder="输入事件处理代码"
              rows="3"
            >${handler}</textarea>
            <div class="property-hint">${event.params?.map(p => `${p.name}: ${p.type}`).join(', ')}</div>
          </div>
        </div>
      `;
    }).join('');
  }

  private renderPropertyControl(prop: PropertyDefinition, value: any): string {
    const controlId = `prop-${prop.name}`;
    
    switch (prop.type) {
      case 'string':
      case 'textarea':
        return `
          <div class="property-item">
            <label class="property-label" for="${controlId}">${prop.label}</label>
            <div class="property-control">
              ${prop.type === 'textarea' ? 
                `<textarea id="${controlId}" class="property-textarea" data-property="${prop.name}" rows="3">${value}</textarea>` :
                `<input id="${controlId}" type="text" class="property-input" data-property="${prop.name}" value="${value}">`
              }
              ${prop.description ? `<div class="property-hint">${prop.description}</div>` : ''}
            </div>
          </div>
        `;
        
      case 'number':
        return `
          <div class="property-item">
            <label class="property-label" for="${controlId}">${prop.label}</label>
            <div class="property-control">
              <input 
                id="${controlId}" 
                type="number" 
                class="property-input" 
                data-property="${prop.name}" 
                value="${value}"
                ${prop.min !== undefined ? `min="${prop.min}"` : ''}
                ${prop.max !== undefined ? `max="${prop.max}"` : ''}
                ${prop.step !== undefined ? `step="${prop.step}"` : ''}
              >
              ${prop.description ? `<div class="property-hint">${prop.description}</div>` : ''}
            </div>
          </div>
        `;
        
      case 'boolean':
        return `
          <div class="property-item">
            <label class="property-label">
              <input 
                id="${controlId}" 
                type="checkbox" 
                class="property-checkbox" 
                data-property="${prop.name}" 
                ${value ? 'checked' : ''}
              >
              ${prop.label}
            </label>
            ${prop.description ? `<div class="property-hint">${prop.description}</div>` : ''}
          </div>
        `;
        
      case 'select':
        return `
          <div class="property-item">
            <label class="property-label" for="${controlId}">${prop.label}</label>
            <div class="property-control">
              <select id="${controlId}" class="property-select" data-property="${prop.name}">
                ${prop.options?.map(option => 
                  `<option value="${option.value}" ${option.value === value ? 'selected' : ''}>${option.label}</option>`
                ).join('')}
              </select>
              ${prop.description ? `<div class="property-hint">${prop.description}</div>` : ''}
            </div>
          </div>
        `;
        
      case 'color':
        return `
          <div class="property-item">
            <label class="property-label" for="${controlId}">${prop.label}</label>
            <div class="property-control property-color-control">
              <input 
                id="${controlId}" 
                type="color" 
                class="property-color" 
                data-property="${prop.name}" 
                value="${value}"
              >
              <input 
                type="text" 
                class="property-color-text" 
                data-property="${prop.name}" 
                value="${value}"
              >
              ${prop.description ? `<div class="property-hint">${prop.description}</div>` : ''}
            </div>
          </div>
        `;
        
      default:
        return `
          <div class="property-item">
            <label class="property-label" for="${controlId}">${prop.label}</label>
            <div class="property-control">
              <input id="${controlId}" type="text" class="property-input" data-property="${prop.name}" value="${value}">
              ${prop.description ? `<div class="property-hint">${prop.description}</div>` : ''}
            </div>
          </div>
        `;
    }
  }

  private bindPropertyEvents(): void {
    const content = this.container.querySelector('.property-content') as HTMLElement;
    
    // 绑定输入事件
    content.addEventListener('input', (e) => {
      const target = e.target as HTMLInputElement;
      const property = target.getAttribute('data-property');
      const type = target.getAttribute('data-type');
      
      if (property && this.currentComponent) {
        this.handlePropertyChange(property, target.value, type);
      }
    });
    
    // 绑定变化事件
    content.addEventListener('change', (e) => {
      const target = e.target as HTMLInputElement;
      const property = target.getAttribute('data-property');
      const type = target.getAttribute('data-type');
      
      if (property && this.currentComponent) {
        let value: any = target.value;
        
        // 类型转换
        if (target.type === 'checkbox') {
          value = target.checked;
        } else if (target.type === 'number') {
          value = parseFloat(target.value) || 0;
        }
        
        this.handlePropertyChange(property, value, type);
      }
    });
  }

  private handlePropertyChange(property: string, value: any, type?: string): void {
    if (!this.currentComponent) return;
    
    if (type === 'event') {
      // 更新事件处理
      this.currentComponent.events[property] = value;
      
      this.eventBus.emit('component:event:update', {
        componentId: this.currentComponent.id,
        eventName: property,
        handler: value
      });
    } else if (this.isStyleProperty(property)) {
      // 更新样式属性
      this.currentComponent.style[property] = value;
      
      this.eventBus.emit('component:style:update', {
        componentId: this.currentComponent.id,
        style: { [property]: value }
      });
    } else {
      // 更新基本属性
      this.currentComponent.props[property] = value;
      
      this.eventBus.emit('component:props:update', {
        componentId: this.currentComponent.id,
        props: { [property]: value }
      });
    }
  }

  private isStyleProperty(property: string): boolean {
    const styleProperties = [
      'width', 'height', 'margin', 'padding', 'backgroundColor',
      'border', 'borderRadius', 'opacity', 'color', 'fontSize',
      'fontWeight', 'textAlign', 'lineHeight', 'letterSpacing'
    ];
    
    return styleProperties.includes(property);
  }

  private bindSectionToggleEvents(): void {
    const toggleButtons = this.container.querySelectorAll('.section-toggle');
    
    toggleButtons.forEach(button => {
      button.addEventListener('click', (e) => {
        const target = e.currentTarget as HTMLButtonElement;
        const section = target.getAttribute('data-section');
        const content = this.container.querySelector(`[data-section="${section}"]`) as HTMLElement;
        const icon = target.querySelector('i') as HTMLElement;
        
        if (content.style.display === 'none') {
          content.style.display = 'block';
          icon.className = 'icon-chevron-down';
        } else {
          content.style.display = 'none';
          icon.className = 'icon-chevron-right';
        }
      });
    });
  }

  private updatePropertyValues(props: Record<string, any>): void {
    Object.entries(props).forEach(([key, value]) => {
      const control = this.container.querySelector(`[data-property="${key}"]`) as HTMLInputElement;
      if (control) {
        if (control.type === 'checkbox') {
          control.checked = Boolean(value);
        } else {
          control.value = String(value);
        }
      }
    });
  }

  private clearProperties(): void {
    this.currentComponent = null;
    const content = this.container.querySelector('.property-content') as HTMLElement;
    
    content.innerHTML = `
      <div class="property-empty">
        <i class="icon-select"></i>
        <p>请选择一个组件</p>
      </div>
    `;
  }

  private resetProperties(): void {
    if (!this.currentComponent) return;
    
    const definition = this.componentRegistry.getComponent(this.currentComponent.type);
    if (!definition) return;
    
    // 重置为默认属性
    const defaultProps = { ...definition.defaultProps };
    this.currentComponent.props = defaultProps;
    
    // 触发更新事件
    this.eventBus.emit('component:props:update', {
      componentId: this.currentComponent.id,
      props: defaultProps
    });
    
    // 重新渲染属性编辑器
    this.renderPropertyEditor(definition, this.currentComponent);
  }

  private copyProperties(): void {
    if (!this.currentComponent) return;
    
    const propertyData = {
      props: { ...this.currentComponent.props },
      style: { ...this.currentComponent.style },
      events: { ...this.currentComponent.events }
    };
    
    // 存储到剪贴板
    localStorage.setItem('lowcode-copied-properties', JSON.stringify(propertyData));
    
    // 显示提示
    this.showMessage('属性已复制到剪贴板');
  }

  private pasteProperties(): void {
    if (!this.currentComponent) return;
    
    try {
      const copiedData = localStorage.getItem('lowcode-copied-properties');
      if (!copiedData) {
        this.showMessage('剪贴板中没有属性数据', 'warning');
        return;
      }
      
      const propertyData = JSON.parse(copiedData);
      
      // 应用属性
      Object.assign(this.currentComponent.props, propertyData.props);
      Object.assign(this.currentComponent.style, propertyData.style);
      Object.assign(this.currentComponent.events, propertyData.events);
      
      // 触发更新事件
      this.eventBus.emit('component:update', {
        componentId: this.currentComponent.id,
        props: this.currentComponent.props,
        style: this.currentComponent.style,
        events: this.currentComponent.events
      });
      
      // 重新渲染属性编辑器
      const definition = this.componentRegistry.getComponent(this.currentComponent.type);
      if (definition) {
        this.renderPropertyEditor(definition, this.currentComponent);
      }
      
      this.showMessage('属性已粘贴');
    } catch (error) {
      this.showMessage('粘贴属性失败', 'error');
    }
  }

  private getComponentInstance(componentId: string): ComponentInstance | null {
    // 这里应该从组件管理器获取组件实例
    // 为了示例,返回 null
    return null;
  }

  private showMessage(message: string, type: 'success' | 'warning' | 'error' = 'success'): void {
    // 显示消息提示
    console.log(`[${type.toUpperCase()}] ${message}`);
  }
}

4.5 小结

在本章中,我们详细实现了低代码平台的可视化设计器,包括:

核心要点

  1. 设计器架构

    • 采用模块化设计,分离关注点
    • 使用事件总线实现组件间通信
    • 支持插件化扩展
  2. 拖拽系统

    • 实现了完整的拖拽交互逻辑
    • 支持组件库到画布的拖拽
    • 支持画布内组件的移动和排序
    • 提供视觉反馈和拖拽预览
  3. 画布渲染引擎

    • 高性能的组件渲染机制
    • 支持组件选择和高亮
    • 实现了样式应用和更新
    • 提供了缓存优化
  4. 组件注册系统

    • 灵活的组件定义和注册机制
    • 支持组件分类和搜索
    • 内置常用组件库
    • 支持自定义组件扩展
  5. 属性编辑器

    • 动态生成属性编辑界面
    • 支持多种属性类型
    • 实时属性更新和预览
    • 提供属性复制粘贴功能

技术特色

  • TypeScript 强类型支持:提供完整的类型定义和检查
  • 事件驱动架构:松耦合的组件通信机制
  • 高性能渲染:优化的DOM操作和缓存策略
  • 用户体验优化:流畅的拖拽交互和实时反馈
  • 扩展性设计:支持自定义组件和插件

下一步

在下一章中,我们将学习如何实现组件库系统,包括: - 组件库架构设计 - 内置组件实现 - 自定义组件开发 - 组件版本管理 - 组件市场机制 “`