diff --git a/css/editor.css b/css/editor.css
index 8da16db..5c449be 100644
--- a/css/editor.css
+++ b/css/editor.css
@@ -515,4 +515,4 @@ body {
font-size: 12px;
color: #999;
margin-top: 4px;
-}
\ No newline at end of file
+}
diff --git a/js/lib/webbuilder/webbuilder.js b/js/lib/webbuilder/webbuilder.js
index 87003a6..05a925e 100644
--- a/js/lib/webbuilder/webbuilder.js
+++ b/js/lib/webbuilder/webbuilder.js
@@ -1,14 +1,11 @@
/**
* WebBuilder - IoT 可视化编辑器
- * 仅支持手机端布局
+ * 支持手机端和电脑端布局
* 单元格比例 1:1(正方形)
*/
var webbuilder = {
- // 编辑器容器
container: null,
-
- // 手机画布配置
phoneConfig: {
width: 375,
height: 812,
@@ -16,43 +13,28 @@ var webbuilder = {
gap: 2,
background: '#f5f5f5'
},
-
- // 计算得出的单元格大小(1:1比例)
cellSize: 0,
rows: 0,
-
- // 已注册的组件类型
componentTypes: {},
-
- // 画布上的组件实例
componentInstances: [],
-
- // 当前选中的组件
selectedComponent: null,
-
- // 拖拽状态
draggingComponent: null,
draggingType: null,
-
- // 拖拽时鼠标相对于组件的偏移
dragOffset: { x: 0, y: 0 },
-
- // 唯一ID计数器
idCounter: 0,
-
- // 变化回调函数
onChangedCallback: null,
-
- // 尺寸控制柄相关状态
resizeHandles: null,
isResizing: false,
resizeDirection: null,
resizeStartPos: { x: 0, y: 0 },
resizeStartSize: { colSpan: 0, rowSpan: 0 },
+ isMobile: false,
+ mobilePanelMode: 'toolbox',
+ touchDragType: null,
+ touchDragComponent: null,
+ touchDragOffset: { x: 0, y: 0 },
+ mobileStyleEl: null,
- /**
- * 应用控制柄的基础样式(不依赖CSS)
- */
_applyHandleBaseStyle: function(handle) {
handle.style.position = 'absolute';
handle.style.width = '8px';
@@ -65,34 +47,97 @@ var webbuilder = {
handle.style.boxShadow = '0 0 2px rgba(0, 0, 0, 0.3)';
},
- /**
- * 计算单元格大小和行数
- */
_calculateGrid: function() {
var config = this.phoneConfig;
-
- // 计算列宽(单元格大小)
- // 列宽 = (画布宽度 - (列数 + 1) * gap) / 列数
this.cellSize = (config.width - (config.columns + 1) * config.gap) / config.columns;
-
- // 计算行数
- // 画布高度 = 行数 * 单元格大小 + (行数 + 1) * gap
- // 812 = 行数 * cellSize + (行数 + 1) * gap
- // 812 = 行数 * cellSize + 行数 * gap + gap
- // 812 - gap = 行数 * (cellSize + gap)
- // 行数 = (812 - gap) / (cellSize + gap)
this.rows = Math.floor((config.height - config.gap) / (this.cellSize + config.gap));
-
- // 调整画布高度以精确适配行数
this.phoneConfig.height = this.rows * this.cellSize + (this.rows + 1) * config.gap;
},
- /**
- * 初始化编辑器
- * @param {string|HTMLElement} containerSelector - 容器选择器或元素
- */
+ _detectMobile: function() {
+ return window.innerWidth <= 768 ||
+ 'ontouchstart' in window ||
+ navigator.maxTouchPoints > 0;
+ },
+
+ _injectMobileStyles: function() {
+ if (this.mobileStyleEl) return;
+ var style = document.createElement('style');
+ style.textContent = [
+ '.wb-mobile {',
+ ' display: flex !important;',
+ ' flex-direction: column !important;',
+ ' height: 100vh !important;',
+ '}',
+ '.wb-mobile .webbuilder-canvas-wrapper {',
+ ' flex: 1 !important;',
+ ' overflow: auto !important;',
+ ' min-height: 0 !important;',
+ '}',
+ '.wb-mobile .webbuilder-toolbox {',
+ ' width: 100% !important;',
+ ' height: 80px !important;',
+ ' flex-shrink: 0 !important;',
+ ' border-right: none !important;',
+ ' border-top: 1px solid #e8e8e8 !important;',
+ '}',
+ '.wb-mobile .webbuilder-toolbox-title {',
+ ' display: none !important;',
+ '}',
+ '.wb-mobile .webbuilder-toolbox-list {',
+ ' display: flex !important;',
+ ' flex-direction: row !important;',
+ ' overflow-x: auto !important;',
+ ' overflow-y: hidden !important;',
+ ' padding: 8px !important;',
+ ' gap: 8px !important;',
+ ' height: 100% !important;',
+ ' -webkit-overflow-scrolling: touch !important;',
+ '}',
+ '.wb-mobile .webbuilder-toolbox-item {',
+ ' flex-direction: column !important;',
+ ' min-width: 64px !important;',
+ ' padding: 8px !important;',
+ ' margin-bottom: 0 !important;',
+ ' justify-content: center !important;',
+ ' align-items: center !important;',
+ '}',
+ '.wb-mobile .webbuilder-toolbox-item .icon {',
+ ' font-size: 24px !important;',
+ '}',
+ '.wb-mobile .webbuilder-toolbox-item .label {',
+ ' font-size: 11px !important;',
+ ' text-align: center !important;',
+ '}',
+ '.wb-mobile .webbuilder-property-panel {',
+ ' width: 100% !important;',
+ ' height: 200px !important;',
+ ' flex-shrink: 0 !important;',
+ ' border-left: none !important;',
+ ' border-top: 1px solid #e8e8e8 !important;',
+ ' overflow-y: auto !important;',
+ ' background: #fff !important;',
+ '}',
+ '.wb-mobile-hidden {',
+ ' display: none !important;',
+ '}',
+ '.wb-mobile-close-panel {',
+ ' width: 100% !important;',
+ ' padding: 10px !important;',
+ ' margin: 8px 0 !important;',
+ ' background: #1890ff !important;',
+ ' border: none !important;',
+ ' border-radius: 4px !important;',
+ ' color: #fff !important;',
+ ' font-size: 14px !important;',
+ ' cursor: pointer !important;',
+ '}'
+ ].join('\n');
+ document.head.appendChild(style);
+ this.mobileStyleEl = style;
+ },
+
init: function(containerSelector) {
- // 获取容器元素
if (typeof containerSelector === 'string') {
this.container = document.querySelector(containerSelector);
} else {
@@ -103,124 +148,122 @@ var webbuilder = {
throw new Error('Container element not found');
}
- // 计算网格
+ this.isMobile = this._detectMobile();
+
+ if (this.isMobile) {
+ this._injectMobileStyles();
+ }
+
this._calculateGrid();
-
- // 创建编辑器结构
this._createEditorStructure();
-
- // 初始化画布
this._initCanvas();
-
- // 初始化工具箱
this._initToolbox();
-
- // 初始化键盘事件
this._initKeyboardEvents();
-
- // 初始化尺寸控制柄事件
this._initResizeEvents();
+ this._initTouchEvents();
+
+ if (this.isMobile) {
+ this._updateMobilePanelVisibility();
+ }
return this;
},
- /**
- * 创建编辑器结构
- */
_createEditorStructure: function() {
this.container.innerHTML = '';
this.container.classList.add('webbuilder-editor');
+ if (this.isMobile) {
+ this.container.classList.add('wb-mobile');
+ }
- // 工具箱容器
- this.toolboxEl = document.createElement('div');
- this.toolboxEl.className = 'webbuilder-toolbox';
- this.container.appendChild(this.toolboxEl);
-
- // 画布容器
this.canvasWrapperEl = document.createElement('div');
this.canvasWrapperEl.className = 'webbuilder-canvas-wrapper';
this.container.appendChild(this.canvasWrapperEl);
- // 属性面板容器
this.propertyPanelEl = document.createElement('div');
this.propertyPanelEl.className = 'webbuilder-property-panel';
this.container.appendChild(this.propertyPanelEl);
+
+ this.toolboxEl = document.createElement('div');
+ this.toolboxEl.className = 'webbuilder-toolbox';
+ this.container.appendChild(this.toolboxEl);
},
- /**
- * 初始化画布
- */
_initCanvas: function() {
var config = this.phoneConfig;
+ var self = this;
- // 创建画布
this.canvasEl = document.createElement('div');
this.canvasEl.className = 'webbuilder-canvas';
this.canvasEl.style.width = config.width + 'px';
this.canvasEl.style.height = config.height + 'px';
this.canvasEl.style.display = 'grid';
- this.canvasEl.style.gridTemplateColumns = `repeat(${config.columns}, ${this.cellSize}px)`;
- this.canvasEl.style.gridAutoRows = `${this.cellSize}px`;
- this.canvasEl.style.gap = `${config.gap}px`;
+ this.canvasEl.style.gridTemplateColumns = 'repeat(' + config.columns + ', ' + this.cellSize + 'px)';
+ this.canvasEl.style.gridAutoRows = this.cellSize + 'px';
+ this.canvasEl.style.gap = config.gap + 'px';
this.canvasEl.style.background = config.background;
this.canvasEl.style.position = 'relative';
- this.canvasEl.style.padding = `${config.gap}px`;
+ this.canvasEl.style.padding = config.gap + 'px';
this.canvasEl.style.boxSizing = 'border-box';
this.canvasWrapperEl.appendChild(this.canvasEl);
- // 画布点击取消选中
- var self = this;
- this.canvasEl.addEventListener('click', (e) => {
+ this.canvasEl.addEventListener('click', function(e) {
if (e.target === self.canvasEl) {
self._deselectAll();
}
});
- // 拖放事件
- this.canvasEl.addEventListener('dragover', (e) => {
+ this.canvasEl.addEventListener('dragover', function(e) {
e.preventDefault();
self.canvasEl.classList.add('dropping');
});
- this.canvasEl.addEventListener('dragleave', (e) => {
+ this.canvasEl.addEventListener('dragleave', function() {
self.canvasEl.classList.remove('dropping');
});
- this.canvasEl.addEventListener('drop', (e) => {
+ this.canvasEl.addEventListener('drop', function(e) {
+ var gridPos;
+ var gridPos2;
e.preventDefault();
self.canvasEl.classList.remove('dropping');
if (self.draggingType) {
- // 从工具箱拖入新组件
- var gridPos = self._calculateGridPosition(e);
+ gridPos = self._calculateGridPosition(e);
self._addComponentToCanvas(self.draggingType, gridPos);
self.draggingType = null;
} else if (self.draggingComponent) {
- // 在画布上移动组件
- var gridPos = self._calculateGridPosition(e);
- self._moveComponent(self.draggingComponent, gridPos);
+ gridPos2 = self._calculateGridPosition(e);
+ self._moveComponent(self.draggingComponent, gridPos2);
self.draggingComponent = null;
}
});
},
- /**
- * 初始化工具箱
- */
_initToolbox: function() {
+ if (this.isMobile) {
+ this._initMobileToolbox();
+ } else {
+ this._initDesktopToolbox();
+ }
+ },
+
+ _initDesktopToolbox: function() {
this.toolboxEl.innerHTML = '
组件库
';
this.toolboxListEl = document.createElement('div');
this.toolboxListEl.className = 'webbuilder-toolbox-list';
this.toolboxEl.appendChild(this.toolboxListEl);
},
- /**
- * 初始化键盘事件
- */
+ _initMobileToolbox: function() {
+ this.toolboxListEl = document.createElement('div');
+ this.toolboxListEl.className = 'webbuilder-toolbox-list wb-mobile-toolbox-list';
+ this.toolboxEl.appendChild(this.toolboxListEl);
+ },
+
_initKeyboardEvents: function() {
var self = this;
- document.addEventListener('keydown', (e) => {
- // 如果焦点在输入框中,不处理删除操作
+ document.addEventListener('keydown', function(e) {
var target = e.target;
var isInputElement = target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
@@ -231,7 +274,6 @@ var webbuilder = {
return;
}
- // Delete键删除选中的组件
if (e.key === 'Delete' || e.key === 'Backspace') {
if (self.selectedComponent) {
e.preventDefault();
@@ -241,22 +283,17 @@ var webbuilder = {
});
},
- /**
- * 初始化尺寸控制柄事件
- */
_initResizeEvents: function() {
var self = this;
- // 鼠标移动事件
- document.addEventListener('mousemove', (e) => {
+ document.addEventListener('mousemove', function(e) {
if (self.isResizing && self.selectedComponent) {
e.preventDefault();
self._handleResizeMove(e);
}
});
- // 鼠标释放事件
- document.addEventListener('mouseup', (e) => {
+ document.addEventListener('mouseup', function(e) {
if (self.isResizing) {
e.preventDefault();
self._handleResizeEnd(e);
@@ -264,65 +301,103 @@ var webbuilder = {
});
},
- /**
- * 注册变化回调
- * @param {function} callback - 回调函数,签名:callback(action, data)
- * action: 'add' | 'delete' | 'move' | 'update' | 'load' | 'clear'
- * data: 相关数据(组件实例、组件ID等)
- */
- onChanged: function(callback) {
- this.onChangedCallback = callback;
+ _initTouchEvents: function() {
+ var self = this;
+ if (!this.isMobile) return;
+
+ this.toolboxListEl.addEventListener('touchstart', function(e) {
+ var item = e.target.closest('.webbuilder-toolbox-item');
+ if (!item) return;
+ self.touchDragType = item.getAttribute('data-type');
+ }, { passive: true });
+
+ this.canvasEl.addEventListener('touchmove', function(e) {
+ if (self.touchDragType || self.touchDragComponent) {
+ e.preventDefault();
+ }
+ }, { passive: false });
+
+ this.canvasEl.addEventListener('touchend', function(e) {
+ var touch;
+ var gridPos;
+ var touch2;
+ var gridPos2;
+ if (self.touchDragType) {
+ touch = e.changedTouches[0];
+ gridPos = self._calculateGridPositionFromTouch(touch);
+ if (gridPos) {
+ self._addComponentToCanvas(self.touchDragType, gridPos);
+ }
+ self.touchDragType = null;
+ } else if (self.touchDragComponent) {
+ touch2 = e.changedTouches[0];
+ gridPos2 = self._calculateGridPositionFromTouch(touch2);
+ if (gridPos2) {
+ self._moveComponent(self.touchDragComponent, gridPos2);
+ }
+ self.touchDragComponent = null;
+ }
+ });
},
- /**
- * 触发变化回调
- */
- _notifyChanged: function(action, data) {
- if (typeof this.onChangedCallback === 'function') {
- this.onChangedCallback(action, data);
- }
- },
-
- /**
- * 计算网格位置
- */
- _calculateGridPosition: function(e) {
+ _calculateGridPositionFromTouch: function(touch) {
var rect = this.canvasEl.getBoundingClientRect();
var config = this.phoneConfig;
+ var x = touch.clientX - rect.left - config.gap;
+ var y = touch.clientY - rect.top - config.gap;
+ var col;
+ var row;
- // 计算鼠标相对于画布的位置,减去偏移量
- var x = e.clientX - rect.left - config.gap - this.dragOffset.x;
- var y = e.clientY - rect.top - config.gap - this.dragOffset.y;
+ if (x < 0 || y < 0 || x > config.width || y > config.height) {
+ return null;
+ }
- // 计算列和行
- var col = Math.floor(x / (this.cellSize + config.gap)) + 1;
- var row = Math.floor(y / (this.cellSize + config.gap)) + 1;
+ col = Math.floor(x / (this.cellSize + config.gap)) + 1;
+ row = Math.floor(y / (this.cellSize + config.gap)) + 1;
- // 边界约束
col = Math.max(1, Math.min(config.columns, col));
row = Math.max(1, Math.min(this.rows, row));
return { column: col, row: row };
},
- /**
- * 添加组件到画布
- */
+ onChanged: function(callback) {
+ this.onChangedCallback = callback;
+ },
+
+ _notifyChanged: function(action, data) {
+ if (typeof this.onChangedCallback === 'function') {
+ this.onChangedCallback(action, data);
+ }
+ },
+
+ _calculateGridPosition: function(e) {
+ var rect = this.canvasEl.getBoundingClientRect();
+ var config = this.phoneConfig;
+ var x = e.clientX - rect.left - config.gap - this.dragOffset.x;
+ var y = e.clientY - rect.top - config.gap - this.dragOffset.y;
+ var col = Math.floor(x / (this.cellSize + config.gap)) + 1;
+ var row = Math.floor(y / (this.cellSize + config.gap)) + 1;
+ col = Math.max(1, Math.min(config.columns, col));
+ row = Math.max(1, Math.min(this.rows, row));
+ return { column: col, row: row };
+ },
+
_addComponentToCanvas: function(componentType, gridPos) {
var typeDef = this.componentTypes[componentType];
+ var id;
+ var startCell;
+ var instance;
+
if (!typeDef) {
console.error('Component type not found:', componentType);
return;
}
- // 生成唯一ID
- var id = componentType + '-' + (++this.idCounter);
+ id = componentType + '-' + (++this.idCounter);
+ startCell = (gridPos.row - 1) * this.phoneConfig.columns + (gridPos.column - 1);
- // 计算 startCell
- var startCell = (gridPos.row - 1) * this.phoneConfig.columns + (gridPos.column - 1);
-
- // 创建组件实例
- var instance = {
+ instance = {
id: id,
type: componentType,
grid: {
@@ -333,59 +408,52 @@ var webbuilder = {
props: JSON.parse(JSON.stringify(typeDef.defaultProps || {}))
};
- // 渲染组件
this._renderComponent(instance);
-
- // 添加到实例列表
this.componentInstances.push(instance);
-
- // 选中新添加的组件
this._selectComponent(instance.id);
-
- // 触发回调
this._notifyChanged('add', instance);
},
- /**
- * 渲染组件到画布(编辑器内部使用)
- */
_renderComponent: function(instance) {
var typeDef = this.componentTypes[instance.type];
+ var columns;
+ var startCol;
+ var startRow;
+ var colSpan;
+ var rowSpan;
+ var el;
+ var content;
+ var handlesContainer;
+ var directions;
+ var self = this;
+
if (!typeDef) return;
- // 计算行列位置
- var columns = this.phoneConfig.columns;
- var startCol = instance.grid.startCell % columns;
- var startRow = Math.floor(instance.grid.startCell / columns);
+ columns = this.phoneConfig.columns;
+ startCol = instance.grid.startCell % columns;
+ startRow = Math.floor(instance.grid.startCell / columns);
+ colSpan = Math.min(instance.grid.colSpan, columns - startCol);
+ rowSpan = instance.grid.rowSpan;
- // 边界约束
- var colSpan = Math.min(instance.grid.colSpan, columns - startCol);
- var rowSpan = instance.grid.rowSpan;
-
- // 创建组件容器元素
- var el = document.createElement('div');
+ el = document.createElement('div');
el.className = 'webbuilder-component';
el.setAttribute('data-id', instance.id);
el.setAttribute('data-type', instance.type);
- el.style.gridColumn = `${startCol + 1} / span ${colSpan}`;
- el.style.gridRow = `${startRow + 1} / span ${rowSpan}`;
+ el.style.gridColumn = (startCol + 1) + ' / span ' + colSpan;
+ el.style.gridRow = (startRow + 1) + ' / span ' + rowSpan;
el.style.position = 'relative';
el.style.overflow = 'visible';
- // 渲染组件内容 - render 函数返回 HTML 元素对象
if (typeDef.render) {
- var content = typeDef.render(instance.props);
- // 如果返回的是字符串,保持兼容
+ content = typeDef.render(instance.props);
if (typeof content === 'string') {
el.innerHTML = content;
} else if (content instanceof HTMLElement) {
- // 如果返回的是 DOM 元素,直接添加
el.appendChild(content);
}
}
- // 创建尺寸控制柄容器
- var handlesContainer = document.createElement('div');
+ handlesContainer = document.createElement('div');
handlesContainer.className = 'resize-handles-container';
handlesContainer.style.display = 'none';
handlesContainer.style.position = 'absolute';
@@ -397,19 +465,14 @@ var webbuilder = {
handlesContainer.style.zIndex = '10';
el.appendChild(handlesContainer);
- // 创建8个控制柄
- var directions = ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'];
- var self = this;
+ directions = ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'];
- directions.forEach((direction) => {
+ directions.forEach(function(direction) {
var handle = document.createElement('div');
- handle.className = `resize-handle resize-handle-${direction}`;
+ handle.className = 'resize-handle resize-handle-' + direction;
handle.setAttribute('data-direction', direction);
-
- // 应用基础样式(不依赖CSS)
self._applyHandleBaseStyle(handle);
- // 控制柄位置
switch(direction) {
case 'nw':
handle.style.top = '-4px';
@@ -457,8 +520,7 @@ var webbuilder = {
break;
}
- // 鼠标按下事件
- handle.addEventListener('mousedown', (e) => {
+ handle.addEventListener('mousedown', function(e) {
e.stopPropagation();
e.preventDefault();
self._handleResizeStart(e, instance.id, direction);
@@ -467,45 +529,48 @@ var webbuilder = {
handlesContainer.appendChild(handle);
});
- // 使组件可拖拽
el.setAttribute('draggable', true);
- el.addEventListener('dragstart', (e) => {
- // 如果正在调整大小,不允许拖拽
+ el.addEventListener('dragstart', function(e) {
+ var rect;
if (self.isResizing) {
e.preventDefault();
return;
}
-
e.stopPropagation();
self.draggingComponent = instance.id;
el.style.opacity = '0.5';
- // 计算鼠标相对于组件的偏移
- var rect = el.getBoundingClientRect();
+ rect = el.getBoundingClientRect();
self.dragOffset.x = e.clientX - rect.left;
self.dragOffset.y = e.clientY - rect.top;
});
- el.addEventListener('dragend', (e) => {
+ el.addEventListener('dragend', function() {
el.style.opacity = '1';
self.draggingComponent = null;
});
- // 点击选中
- el.addEventListener('click', (e) => {
+ el.addEventListener('click', function(e) {
e.stopPropagation();
self._selectComponent(instance.id);
});
- // 存储元素引用
+ if (this.isMobile) {
+ el.addEventListener('touchstart', function(e) {
+ var touch;
+ var rect;
+ e.stopPropagation();
+ self.touchDragComponent = instance.id;
+ touch = e.touches[0];
+ rect = el.getBoundingClientRect();
+ self.touchDragOffset.x = touch.clientX - rect.left;
+ self.touchDragOffset.y = touch.clientY - rect.top;
+ }, { passive: true });
+ }
+
instance.element = el;
instance.handlesContainer = handlesContainer;
-
- // 添加到画布
this.canvasEl.appendChild(el);
},
- /**
- * 处理尺寸调整开始
- */
_handleResizeStart: function(e, componentId, direction) {
var instance = this._findInstance(componentId);
if (!instance) return;
@@ -518,104 +583,95 @@ var webbuilder = {
rowSpan: instance.grid.rowSpan,
startCell: instance.grid.startCell
};
-
- // 禁用画布的拖拽
this.canvasEl.style.pointerEvents = 'none';
},
- /**
- * 处理尺寸调整移动
- */
_handleResizeMove: function(e) {
var instance = this._findInstance(this.selectedComponent);
+ var config;
+ var cellSize;
+ var gap;
+ var deltaX;
+ var deltaY;
+ var colDelta;
+ var rowDelta;
+ var startCol;
+ var startRow;
+ var origColSpan;
+ var origRowSpan;
+ var newColSpan;
+ var newRowSpan;
+ var newStartCol;
+ var newStartRow;
+
if (!instance) return;
- var config = this.phoneConfig;
- var cellSize = this.cellSize;
- var gap = config.gap;
-
- // 计算鼠标移动的距离
- var deltaX = e.clientX - this.resizeStartPos.x;
- var deltaY = e.clientY - this.resizeStartPos.y;
-
- // 计算列和行的变化量
- var colDelta = Math.round(deltaX / (cellSize + gap));
- var rowDelta = Math.round(deltaY / (cellSize + gap));
-
- // 获取起始状态
- var startCol = this.resizeStartSize.startCell % config.columns;
- var startRow = Math.floor(this.resizeStartSize.startCell / config.columns);
- var origColSpan = this.resizeStartSize.colSpan;
- var origRowSpan = this.resizeStartSize.rowSpan;
-
- // 计算新的尺寸和位置
- var newColSpan = origColSpan;
- var newRowSpan = origRowSpan;
- var newStartCol = startCol;
- var newStartRow = startRow;
+ config = this.phoneConfig;
+ cellSize = this.cellSize;
+ gap = config.gap;
+ deltaX = e.clientX - this.resizeStartPos.x;
+ deltaY = e.clientY - this.resizeStartPos.y;
+ colDelta = Math.round(deltaX / (cellSize + gap));
+ rowDelta = Math.round(deltaY / (cellSize + gap));
+ startCol = this.resizeStartSize.startCell % config.columns;
+ startRow = Math.floor(this.resizeStartSize.startCell / config.columns);
+ origColSpan = this.resizeStartSize.colSpan;
+ origRowSpan = this.resizeStartSize.rowSpan;
+ newColSpan = origColSpan;
+ newRowSpan = origRowSpan;
+ newStartCol = startCol;
+ newStartRow = startRow;
switch(this.resizeDirection) {
- case 'se': // 右下角 - 只改变大小,不改变位置
+ case 'se':
newColSpan = Math.max(1, origColSpan + colDelta);
newRowSpan = Math.max(1, origRowSpan + rowDelta);
break;
-
- case 'e': // 右边 - 只改变宽度,不改变位置
+ case 'e':
newColSpan = Math.max(1, origColSpan + colDelta);
break;
-
- case 's': // 下边 - 只改变高度,不改变位置
+ case 's':
newRowSpan = Math.max(1, origRowSpan + rowDelta);
break;
-
- case 'w': // 左边 - 改变宽度,同时改变左边位置,右边保持不变
+ case 'w':
newColSpan = Math.max(1, origColSpan - colDelta);
newStartCol = startCol + (origColSpan - newColSpan);
- // 边界检查:确保不超出画布左边界
if (newStartCol < 0) {
newStartCol = 0;
newColSpan = origColSpan + startCol;
}
break;
-
- case 'sw': // 左下角 - 改变宽度和高度,左边位置改变,下边位置不变
+ case 'sw':
newColSpan = Math.max(1, origColSpan - colDelta);
newStartCol = startCol + (origColSpan - newColSpan);
newRowSpan = Math.max(1, origRowSpan + rowDelta);
- // 边界检查:确保不超出画布左边界
if (newStartCol < 0) {
newStartCol = 0;
newColSpan = origColSpan + startCol;
}
break;
-
- case 'n': // 上边 - 改变高度,同时改变上边位置,下边保持不变
+ case 'n':
newRowSpan = Math.max(1, origRowSpan - rowDelta);
newStartRow = startRow + (origRowSpan - newRowSpan);
- // 边界检查:确保不超出画布上边界
if (newStartRow < 0) {
newStartRow = 0;
newRowSpan = origRowSpan + startRow;
}
break;
-
- case 'ne': // 右上角 - 改变宽度和高度,右边位置不变,上边位置改变
+ case 'ne':
newColSpan = Math.max(1, origColSpan + colDelta);
newRowSpan = Math.max(1, origRowSpan - rowDelta);
newStartRow = startRow + (origRowSpan - newRowSpan);
- // 边界检查:确保不超出画布上边界
if (newStartRow < 0) {
newStartRow = 0;
newRowSpan = origRowSpan + startRow;
}
break;
-
- case 'nw': // 左上角 - 改变宽度和高度,左边和上边位置都改变
+ case 'nw':
newColSpan = Math.max(1, origColSpan - colDelta);
newStartCol = startCol + (origColSpan - newColSpan);
newRowSpan = Math.max(1, origRowSpan - rowDelta);
newStartRow = startRow + (origRowSpan - newRowSpan);
- // 边界检查:确保不超出画布左边界和上边界
if (newStartCol < 0) {
newStartCol = 0;
newColSpan = origColSpan + startCol;
@@ -627,108 +683,83 @@ var webbuilder = {
break;
}
- // 边界约束:确保不超出画布右边界和下边界
newColSpan = Math.min(newColSpan, config.columns - newStartCol);
newRowSpan = Math.min(newRowSpan, this.rows - newStartRow);
-
- // 最终安全检查:确保尺寸至少为1
newColSpan = Math.max(1, newColSpan);
newRowSpan = Math.max(1, newRowSpan);
- // 更新组件实例
instance.grid.startCell = newStartRow * config.columns + newStartCol;
instance.grid.colSpan = newColSpan;
instance.grid.rowSpan = newRowSpan;
+ instance.element.style.gridColumn = (newStartCol + 1) + ' / span ' + newColSpan;
+ instance.element.style.gridRow = (newStartRow + 1) + ' / span ' + newRowSpan;
- // 更新样式
- instance.element.style.gridColumn = `${newStartCol + 1} / span ${newColSpan}`;
- instance.element.style.gridRow = `${newStartRow + 1} / span ${newRowSpan}`;
-
- // 更新属性面板
if (this.selectedComponent === instance.id) {
this._showPropertyPanel(instance);
}
},
- /**
- * 处理尺寸调整结束
- */
- _handleResizeEnd: function(e) {
+ _handleResizeEnd: function() {
+ var instance = this._findInstance(this.selectedComponent);
this.isResizing = false;
this.resizeDirection = null;
-
- // 恢复画布的拖拽
this.canvasEl.style.pointerEvents = 'auto';
- // 触发回调
- var instance = this._findInstance(this.selectedComponent);
if (instance) {
this._notifyChanged('update', instance);
}
},
- /**
- * 显示尺寸控制柄
- */
_showResizeHandles: function(instance) {
if (!instance || !instance.handlesContainer) return;
instance.handlesContainer.style.display = 'block';
},
- /**
- * 隐藏尺寸控制柄
- */
_hideResizeHandles: function(instance) {
if (!instance || !instance.handlesContainer) return;
instance.handlesContainer.style.display = 'none';
},
- /**
- * 将组件渲染到指定的 div 容器
- * @param {HTMLElement} container - 目标容器
- * @returns {Array} 组件列表,每个元素包含 { element, props, id, type }
- */
renderToDiv: function(container) {
var self = this;
var result = [];
+ var columns;
- // 清空容器
container.innerHTML = '';
-
- // 创建画布样式
- container.style.width = `${this.phoneConfig.width}px`;
- container.style.height = `${this.phoneConfig.height}px`;
+ container.style.width = this.phoneConfig.width + 'px';
+ container.style.height = this.phoneConfig.height + 'px';
container.style.display = 'grid';
- container.style.gridTemplateColumns = `repeat(${this.phoneConfig.columns}, ${this.cellSize}px)`;
- container.style.gridAutoRows = `${this.cellSize}px`;
- container.style.gap = `${this.phoneConfig.gap}px`;
+ container.style.gridTemplateColumns = 'repeat(' + this.phoneConfig.columns + ', ' + this.cellSize + 'px)';
+ container.style.gridAutoRows = this.cellSize + 'px';
+ container.style.gap = this.phoneConfig.gap + 'px';
container.style.background = this.phoneConfig.background;
- container.style.padding = `${this.phoneConfig.gap}px`;
+ container.style.padding = this.phoneConfig.gap + 'px';
container.style.boxSizing = 'border-box';
- var columns = this.phoneConfig.columns;
+ columns = this.phoneConfig.columns;
- // 遍历所有组件实例
- this.componentInstances.forEach((instance) => {
+ this.componentInstances.forEach(function(instance) {
var typeDef = self.componentTypes[instance.type];
+ var startCol;
+ var startRow;
+ var colSpan;
+ var rowSpan;
+ var wrapper;
+ var componentElement;
+
if (!typeDef || !typeDef.render) return;
- // 计算行列位置
- var startCol = instance.grid.startCell % columns;
- var startRow = Math.floor(instance.grid.startCell / columns);
- var colSpan = Math.min(instance.grid.colSpan, columns - startCol);
- var rowSpan = instance.grid.rowSpan;
+ startCol = instance.grid.startCell % columns;
+ startRow = Math.floor(instance.grid.startCell / columns);
+ colSpan = Math.min(instance.grid.colSpan, columns - startCol);
+ rowSpan = instance.grid.rowSpan;
- // 创建组件容器
- var wrapper = document.createElement('div');
- wrapper.style.gridColumn = `${startCol + 1} / span ${colSpan}`;
- wrapper.style.gridRow = `${startRow + 1} / span ${rowSpan}`;
+ wrapper = document.createElement('div');
+ wrapper.style.gridColumn = (startCol + 1) + ' / span ' + colSpan;
+ wrapper.style.gridRow = (startRow + 1) + ' / span ' + rowSpan;
wrapper.style.overflow = 'hidden';
- // 调用 render 函数获取组件元素
- var componentElement = typeDef.render(instance.props);
-
- // 如果返回的是字符串,保持兼容
+ componentElement = typeDef.render(instance.props);
if (typeof componentElement === 'string') {
wrapper.innerHTML = componentElement;
componentElement = wrapper.firstChild;
@@ -736,14 +767,11 @@ var webbuilder = {
wrapper.appendChild(componentElement);
}
- // 添加到容器
container.appendChild(wrapper);
-
- // 添加到结果列表
result.push({
- element: componentElement, // 控件本身的 HTML 元素对象
- wrapper: wrapper, // 包装容器
- props: JSON.parse(JSON.stringify(instance.props)), // 自定义组件属性(深拷贝)
+ element: componentElement,
+ wrapper: wrapper,
+ props: JSON.parse(JSON.stringify(instance.props)),
id: instance.id,
type: instance.type
});
@@ -752,139 +780,151 @@ var webbuilder = {
return result;
},
- /**
- * 移动组件
- */
_moveComponent: function(componentId, gridPos) {
var instance = this._findInstance(componentId);
+ var columns;
+ var startCol;
+ var startRow;
+ var colSpan;
+
if (!instance) return;
- // 更新 startCell
instance.grid.startCell = (gridPos.row - 1) * this.phoneConfig.columns + (gridPos.column - 1);
- // 重新计算行列位置
- var columns = this.phoneConfig.columns;
- var startCol = instance.grid.startCell % columns;
- var startRow = Math.floor(instance.grid.startCell / columns);
- var colSpan = Math.min(instance.grid.colSpan, columns - startCol);
+ columns = this.phoneConfig.columns;
+ startCol = instance.grid.startCell % columns;
+ startRow = Math.floor(instance.grid.startCell / columns);
+ colSpan = Math.min(instance.grid.colSpan, columns - startCol);
- // 更新样式
- instance.element.style.gridColumn = `${startCol + 1} / span ${colSpan}`;
- instance.element.style.gridRow = `${startRow + 1} / span ${instance.grid.rowSpan}`;
+ instance.element.style.gridColumn = (startCol + 1) + ' / span ' + colSpan;
+ instance.element.style.gridRow = (startRow + 1) + ' / span ' + instance.grid.rowSpan;
- // 更新属性面板
if (this.selectedComponent === componentId) {
this._showPropertyPanel(instance);
}
- // 触发回调
this._notifyChanged('move', instance);
},
- /**
- * 选中组件
- */
_selectComponent: function(componentId) {
- // 取消之前的选中
+ var instance;
this._deselectAll();
- var instance = this._findInstance(componentId);
+ instance = this._findInstance(componentId);
if (!instance) return;
this.selectedComponent = componentId;
instance.element.classList.add('selected');
-
- // 显示尺寸控制柄
this._showResizeHandles(instance);
-
- // 显示属性面板
this._showPropertyPanel(instance);
+
+ if (this.isMobile) {
+ this.mobilePanelMode = 'property';
+ this._updateMobilePanelVisibility();
+ }
},
- /**
- * 取消所有选中
- */
_deselectAll: function() {
- this.selectedComponent = null;
var selected = this.canvasEl.querySelectorAll('.selected');
- selected.forEach((el) => {
+ var self = this;
+
+ this.selectedComponent = null;
+ selected.forEach(function(el) {
el.classList.remove('selected');
});
- // 隐藏所有尺寸控制柄
- this.componentInstances.forEach((instance) => {
+ this.componentInstances.forEach(function(instance) {
if (instance.handlesContainer) {
instance.handlesContainer.style.display = 'none';
}
});
this.propertyPanelEl.innerHTML = '';
+
+ if (this.isMobile) {
+ this.mobilePanelMode = 'toolbox';
+ this._updateMobilePanelVisibility();
+ }
+ },
+
+ _updateMobilePanelVisibility: function() {
+ if (!this.isMobile) return;
+ if (this.mobilePanelMode === 'property') {
+ this.toolboxEl.classList.add('wb-mobile-hidden');
+ this.propertyPanelEl.classList.remove('wb-mobile-hidden');
+ } else {
+ this.toolboxEl.classList.remove('wb-mobile-hidden');
+ this.propertyPanelEl.classList.add('wb-mobile-hidden');
+ }
},
- /**
- * 显示属性面板
- */
_showPropertyPanel: function(instance) {
var typeDef = this.componentTypes[instance.type];
var self = this;
var columns = this.phoneConfig.columns;
-
- // 计算当前行列
var startCol = instance.grid.startCell % columns;
var startRow = Math.floor(instance.grid.startCell / columns);
+ var html;
+ var deleteBtn;
+ var closeBtn;
- var html = `${typeDef.label || instance.type}
`;
+ html = '' + (typeDef.label || instance.type) + '
';
- // 基础属性
html += '';
- // 自定义属性
if (typeDef.traits && typeDef.traits.length > 0) {
html += '';
}
- // 删除按钮
html += '';
html += '删除组件 ';
html += '
';
this.propertyPanelEl.innerHTML = html;
- // 绑定事件
- this.propertyPanelEl.querySelectorAll('input').forEach((input) => {
- input.addEventListener('change', () => {
+ this.propertyPanelEl.querySelectorAll('input').forEach(function(input) {
+ input.addEventListener('change', function() {
var prop = input.getAttribute('data-prop');
var trait = input.getAttribute('data-trait');
+ var value;
+ var currentRow;
+ var currentCol;
+ var typeDef2;
+ var traitDef;
+ var content;
+ var handlesContainer;
+ var sCol;
+ var sRow;
+ var cSpan;
if (prop) {
- // 更新网格位置
- var value = parseInt(input.value, 10) || 1;
+ value = parseInt(input.value, 10) || 1;
if (prop === 'startCol') {
- var currentRow = Math.floor(instance.grid.startCell / columns);
+ currentRow = Math.floor(instance.grid.startCell / columns);
instance.grid.startCell = currentRow * columns + (value - 1);
} else if (prop === 'startRow') {
- var currentCol = instance.grid.startCell % columns;
+ currentCol = instance.grid.startCell % columns;
instance.grid.startCell = (value - 1) * columns + currentCol;
} else if (prop === 'colSpan') {
instance.grid.colSpan = value;
@@ -892,17 +932,15 @@ var webbuilder = {
instance.grid.rowSpan = value;
}
- // 重新计算位置
- var startCol = instance.grid.startCell % columns;
- var startRow = Math.floor(instance.grid.startCell / columns);
- var colSpan = Math.min(instance.grid.colSpan, columns - startCol);
+ sCol = instance.grid.startCell % columns;
+ sRow = Math.floor(instance.grid.startCell / columns);
+ cSpan = Math.min(instance.grid.colSpan, columns - sCol);
- instance.element.style.gridColumn = `${startCol + 1} / span ${colSpan}`;
- instance.element.style.gridRow = `${startRow + 1} / span ${instance.grid.rowSpan}`;
+ instance.element.style.gridColumn = (sCol + 1) + ' / span ' + cSpan;
+ instance.element.style.gridRow = (sRow + 1) + ' / span ' + instance.grid.rowSpan;
} else if (trait) {
- // 更新组件属性
- var typeDef = self.componentTypes[instance.type];
- var traitDef = typeDef.traits.find((t) => t.name === trait);
+ typeDef2 = self.componentTypes[instance.type];
+ traitDef = typeDef2.traits.find(function(t) { return t.name === trait; });
if (input.type === 'checkbox') {
instance.props[trait] = input.checked;
@@ -912,9 +950,8 @@ var webbuilder = {
instance.props[trait] = input.value;
}
- // 重新渲染组件内容
- if (typeDef.render) {
- var content = typeDef.render(instance.props);
+ if (typeDef2.render) {
+ content = typeDef2.render(instance.props);
instance.element.innerHTML = '';
if (typeof content === 'string') {
instance.element.innerHTML = content;
@@ -922,8 +959,7 @@ var webbuilder = {
instance.element.appendChild(content);
}
- // 重新添加尺寸控制柄容器
- var handlesContainer = document.createElement('div');
+ handlesContainer = document.createElement('div');
handlesContainer.className = 'resize-handles-container';
handlesContainer.style.display = 'block';
handlesContainer.style.position = 'absolute';
@@ -935,48 +971,47 @@ var webbuilder = {
handlesContainer.style.zIndex = '10';
instance.element.appendChild(handlesContainer);
instance.handlesContainer = handlesContainer;
-
- // 重新创建控制柄
self._recreateResizeHandles(instance);
}
}
- // 触发属性更新回调
self._notifyChanged('update', instance);
});
});
- // 删除按钮
- var deleteBtn = this.propertyPanelEl.querySelector('[data-action="delete"]');
+ deleteBtn = this.propertyPanelEl.querySelector('[data-action="delete"]');
if (deleteBtn) {
- deleteBtn.addEventListener('click', () => {
+ deleteBtn.addEventListener('click', function() {
self._deleteComponent(instance.id);
});
}
+
+ if (this.isMobile) {
+ closeBtn = document.createElement('button');
+ closeBtn.className = 'btn wb-mobile-close-panel';
+ closeBtn.textContent = '返回组件库';
+ closeBtn.addEventListener('click', function() {
+ self._deselectAll();
+ });
+ this.propertyPanelEl.appendChild(closeBtn);
+ }
},
- /**
- * 重新创建尺寸控制柄
- */
_recreateResizeHandles: function(instance) {
+ var self = this;
+ var directions;
+
if (!instance || !instance.handlesContainer) return;
- var self = this;
- var directions = ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'];
-
- // 清空控制柄容器
+ directions = ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'];
instance.handlesContainer.innerHTML = '';
- // 重新创建8个控制柄
- directions.forEach((direction) => {
+ directions.forEach(function(direction) {
var handle = document.createElement('div');
- handle.className = `resize-handle resize-handle-${direction}`;
+ handle.className = 'resize-handle resize-handle-' + direction;
handle.setAttribute('data-direction', direction);
-
- // 应用基础样式(不依赖CSS)
self._applyHandleBaseStyle(handle);
- // 控制柄位置
switch(direction) {
case 'nw':
handle.style.top = '-4px';
@@ -1024,8 +1059,7 @@ var webbuilder = {
break;
}
- // 鼠标按下事件
- handle.addEventListener('mousedown', (e) => {
+ handle.addEventListener('mousedown', function(e) {
e.stopPropagation();
e.preventDefault();
self._handleResizeStart(e, instance.id, direction);
@@ -1035,12 +1069,12 @@ var webbuilder = {
});
},
- /**
- * 删除组件
- */
_deleteComponent: function(componentId) {
var index = -1;
- for (var i = 0; i < this.componentInstances.length; i++) {
+ var i;
+ var instance;
+
+ for (i = 0; i < this.componentInstances.length; i++) {
if (this.componentInstances[i].id === componentId) {
index = i;
break;
@@ -1049,28 +1083,18 @@ var webbuilder = {
if (index === -1) return;
- var instance = this.componentInstances[index];
-
- // 从DOM移除
+ instance = this.componentInstances[index];
if (instance.element) {
instance.element.remove();
}
-
- // 从数组移除
this.componentInstances.splice(index, 1);
-
- // 取消选中
this._deselectAll();
-
- // 触发回调
this._notifyChanged('delete', { id: componentId });
},
- /**
- * 查找组件实例
- */
_findInstance: function(componentId) {
- for (var i = 0; i < this.componentInstances.length; i++) {
+ var i;
+ for (i = 0; i < this.componentInstances.length; i++) {
if (this.componentInstances[i].id === componentId) {
return this.componentInstances[i];
}
@@ -1078,11 +1102,6 @@ var webbuilder = {
return null;
},
- /**
- * 注册组件类型
- * @param {string} name - 组件名称
- * @param {object} definition - 组件定义
- */
define: function(name, definition) {
if (this.componentTypes[name]) {
console.warn('Component type already defined:', name);
@@ -1090,46 +1109,51 @@ var webbuilder = {
}
this.componentTypes[name] = definition;
-
- // 添加到工具箱
this._addToToolbox(name, definition);
},
- /**
- * 添加组件到工具箱
- */
_addToToolbox: function(name, definition) {
var self = this;
+ var item;
+ var icon;
+ var label;
- var item = document.createElement('div');
+ item = document.createElement('div');
item.className = 'webbuilder-toolbox-item';
+ if (this.isMobile) {
+ item.classList.add('wb-mobile-toolbox-item');
+ }
item.setAttribute('draggable', true);
item.setAttribute('data-type', name);
- var icon = definition.icon || '📦';
- var label = definition.label || name;
+ icon = definition.icon || '\uD83D\uDCE6';
+ label = definition.label || name;
- item.innerHTML = `${icon} ${label} `;
+ item.innerHTML = '' + icon + ' ' + label + ' ';
- // 拖拽事件
- item.addEventListener('dragstart', () => {
+ item.addEventListener('dragstart', function() {
self.draggingType = name;
item.style.opacity = '0.5';
});
- item.addEventListener('dragend', () => {
+ item.addEventListener('dragend', function() {
item.style.opacity = '1';
self.draggingType = null;
});
+ if (this.isMobile) {
+ item.addEventListener('click', function() {
+ self.touchDragType = name;
+ });
+ }
+
this.toolboxListEl.appendChild(item);
},
- /**
- * 导出为JSONB
- * 返回格式: { version, layouts: { phone: {...}, computer: null } }
- */
toJSONB: function() {
- var components = this.componentInstances.map((instance) => {
+ var self = this;
+ var components;
+
+ components = this.componentInstances.map(function(instance) {
return {
id: instance.id,
type: instance.type,
@@ -1162,33 +1186,32 @@ var webbuilder = {
};
},
- /**
- * 从JSONB加载
- */
fromJSONB: function(jsonb) {
- // 清空画布
+ var phoneLayout;
+ var self = this;
+ var maxId;
+
this.canvasEl.innerHTML = '';
this.componentInstances = [];
this.selectedComponent = null;
- // 获取手机布局
- var phoneLayout = jsonb.layouts && jsonb.layouts.phone;
+ phoneLayout = jsonb.layouts && jsonb.layouts.phone;
if (!phoneLayout) {
console.warn('No phone layout found');
return;
}
- // 渲染组件
if (phoneLayout.components) {
- var self = this;
- phoneLayout.components.forEach((comp) => {
+ phoneLayout.components.forEach(function(comp) {
var typeDef = self.componentTypes[comp.type];
+ var instance;
+
if (!typeDef) {
console.warn('Component type not found:', comp.type);
return;
}
- var instance = {
+ instance = {
id: comp.id,
type: comp.type,
grid: {
@@ -1203,9 +1226,8 @@ var webbuilder = {
self.componentInstances.push(instance);
});
- // 更新ID计数器
- var maxId = 0;
- this.componentInstances.forEach((inst) => {
+ maxId = 0;
+ this.componentInstances.forEach(function(inst) {
var match = inst.id.match(/-(\d+)$/);
if (match) {
maxId = Math.max(maxId, parseInt(match[1], 10));
@@ -1214,13 +1236,9 @@ var webbuilder = {
this.idCounter = maxId;
}
- // 触发回调
this._notifyChanged('load', { count: this.componentInstances.length });
},
- /**
- * 获取画布配置
- */
getCanvasConfig: function() {
return {
width: this.phoneConfig.width,
@@ -1233,23 +1251,15 @@ var webbuilder = {
};
},
- /**
- * 获取所有组件实例
- */
getComponents: function() {
return JSON.parse(JSON.stringify(this.componentInstances));
},
- /**
- * 清空画布
- */
clear: function() {
this.canvasEl.innerHTML = '';
this.componentInstances = [];
this.selectedComponent = null;
this.propertyPanelEl.innerHTML = '';
-
- // 触发回调
this._notifyChanged('clear', null);
}
-};
\ No newline at end of file
+};