From b6f17d3b4ff06ecbf6eb44e0fa59c2db24463173 Mon Sep 17 00:00:00 2001 From: wtz Date: Mon, 6 Apr 2026 17:39:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=89=8B=E6=9C=BA=E7=AB=AF=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E5=99=A8=E9=80=82=E9=85=8D=EF=BC=8C=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E5=BA=93=E5=BA=95=E9=83=A8=E6=A8=AA=E5=90=91=E6=BB=91=E5=8A=A8?= =?UTF-8?q?=EF=BC=8C=E5=B1=9E=E6=80=A7=E9=9D=A2=E6=9D=BF=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- css/editor.css | 2 +- js/lib/webbuilder/webbuilder.js | 920 ++++++++++++++++---------------- 2 files changed, 466 insertions(+), 456 deletions(-) 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 += '
'; html += '
位置大小
'; - html += `
`; - html += `
`; - html += `
`; - html += `
`; + html += '
'; + html += '
'; + html += '
'; + html += '
'; html += '
'; - // 自定义属性 if (typeDef.traits && typeDef.traits.length > 0) { html += '
'; html += '
组件属性
'; - typeDef.traits.forEach((trait) => { + typeDef.traits.forEach(function(trait) { var value = instance.props[trait.name] !== undefined ? instance.props[trait.name] : (trait.default || ''); html += '
'; - html += ``; + html += ''; if (trait.type === 'number') { - html += ``; + html += ''; } else if (trait.type === 'checkbox') { - html += ``; + html += ''; } else { - html += ``; + html += ''; } html += '
'; }); 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 +};