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 小结
在本章中,我们详细实现了低代码平台的可视化设计器,包括:
核心要点
设计器架构
- 采用模块化设计,分离关注点
- 使用事件总线实现组件间通信
- 支持插件化扩展
拖拽系统
- 实现了完整的拖拽交互逻辑
- 支持组件库到画布的拖拽
- 支持画布内组件的移动和排序
- 提供视觉反馈和拖拽预览
画布渲染引擎
- 高性能的组件渲染机制
- 支持组件选择和高亮
- 实现了样式应用和更新
- 提供了缓存优化
组件注册系统
- 灵活的组件定义和注册机制
- 支持组件分类和搜索
- 内置常用组件库
- 支持自定义组件扩展
属性编辑器
- 动态生成属性编辑界面
- 支持多种属性类型
- 实时属性更新和预览
- 提供属性复制粘贴功能
技术特色
- TypeScript 强类型支持:提供完整的类型定义和检查
- 事件驱动架构:松耦合的组件通信机制
- 高性能渲染:优化的DOM操作和缓存策略
- 用户体验优化:流畅的拖拽交互和实时反馈
- 扩展性设计:支持自定义组件和插件
下一步
在下一章中,我们将学习如何实现组件库系统,包括: - 组件库架构设计 - 内置组件实现 - 自定义组件开发 - 组件版本管理 - 组件市场机制 “`