/** * WebBuilder - IoT 可视化编辑器 * 支持手机端和电脑端布局 * 单元格比例 1:1(正方形) */ var webbuilder = { container: null, phoneConfig: { width: 375, height: 812, columns: 24, gap: 2, background: '#f5f5f5' }, cellSize: 0, rows: 0, componentTypes: {}, componentInstances: [], selectedComponent: null, draggingComponent: null, draggingType: null, dragOffset: { x: 0, y: 0 }, 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, _applyHandleBaseStyle: function(handle) { handle.style.position = 'absolute'; handle.style.width = '8px'; handle.style.height = '8px'; handle.style.background = '#1890ff'; handle.style.border = '1px solid #fff'; handle.style.borderRadius = '2px'; handle.style.pointerEvents = 'auto'; handle.style.zIndex = '11'; handle.style.boxShadow = '0 0 2px rgba(0, 0, 0, 0.3)'; }, _calculateGrid: function() { var config = this.phoneConfig; this.cellSize = (config.width - (config.columns + 1) * config.gap) / config.columns; this.rows = Math.floor((config.height - config.gap) / (this.cellSize + config.gap)); this.phoneConfig.height = this.rows * this.cellSize + (this.rows + 1) * config.gap; }, _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 { this.container = containerSelector; } if (!this.container) { 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.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.background = config.background; this.canvasEl.style.position = 'relative'; this.canvasEl.style.padding = config.gap + 'px'; this.canvasEl.style.boxSizing = 'border-box'; this.canvasWrapperEl.appendChild(this.canvasEl); this.canvasEl.addEventListener('click', function(e) { if (e.target === self.canvasEl) { self._deselectAll(); } }); this.canvasEl.addEventListener('dragover', function(e) { e.preventDefault(); self.canvasEl.classList.add('dropping'); }); this.canvasEl.addEventListener('dragleave', function() { self.canvasEl.classList.remove('dropping'); }); this.canvasEl.addEventListener('drop', function(e) { var gridPos; var gridPos2; e.preventDefault(); self.canvasEl.classList.remove('dropping'); if (self.draggingType) { gridPos = self._calculateGridPosition(e); self._addComponentToCanvas(self.draggingType, gridPos); self.draggingType = null; } else if (self.draggingComponent) { 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', function(e) { var target = e.target; var isInputElement = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.tagName === 'SELECT' || target.isContentEditable; if (isInputElement) { return; } if (e.key === 'Delete' || e.key === 'Backspace') { if (self.selectedComponent) { e.preventDefault(); self._deleteComponent(self.selectedComponent); } } }); }, _initResizeEvents: function() { var self = this; document.addEventListener('mousemove', function(e) { if (self.isResizing && self.selectedComponent) { e.preventDefault(); self._handleResizeMove(e); } }); document.addEventListener('mouseup', function(e) { if (self.isResizing) { e.preventDefault(); self._handleResizeEnd(e); } }); }, _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; } }); }, _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; if (x < 0 || y < 0 || x > config.width || y > config.height) { return null; } 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 = componentType + '-' + (++this.idCounter); startCell = (gridPos.row - 1) * this.phoneConfig.columns + (gridPos.column - 1); instance = { id: id, type: componentType, grid: { startCell: startCell, colSpan: typeDef.defaultColumnSpan || 6, rowSpan: typeDef.defaultRowSpan || 4 }, 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; 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; 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.position = 'relative'; el.style.overflow = 'visible'; if (typeDef.render) { content = typeDef.render(instance.props); if (typeof content === 'string') { el.innerHTML = content; } else if (content instanceof HTMLElement) { el.appendChild(content); } } handlesContainer = document.createElement('div'); handlesContainer.className = 'resize-handles-container'; handlesContainer.style.display = 'none'; handlesContainer.style.position = 'absolute'; handlesContainer.style.top = '0'; handlesContainer.style.left = '0'; handlesContainer.style.width = '100%'; handlesContainer.style.height = '100%'; handlesContainer.style.pointerEvents = 'none'; handlesContainer.style.zIndex = '10'; el.appendChild(handlesContainer); directions = ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se']; directions.forEach(function(direction) { var handle = document.createElement('div'); handle.className = 'resize-handle resize-handle-' + direction; handle.setAttribute('data-direction', direction); self._applyHandleBaseStyle(handle); switch(direction) { case 'nw': handle.style.top = '-4px'; handle.style.left = '-4px'; handle.style.cursor = 'nwse-resize'; break; case 'n': handle.style.top = '-4px'; handle.style.left = '50%'; handle.style.transform = 'translateX(-50%)'; handle.style.cursor = 'ns-resize'; break; case 'ne': handle.style.top = '-4px'; handle.style.right = '-4px'; handle.style.cursor = 'nesw-resize'; break; case 'w': handle.style.top = '50%'; handle.style.left = '-4px'; handle.style.transform = 'translateY(-50%)'; handle.style.cursor = 'ew-resize'; break; case 'e': handle.style.top = '50%'; handle.style.right = '-4px'; handle.style.transform = 'translateY(-50%)'; handle.style.cursor = 'ew-resize'; break; case 'sw': handle.style.bottom = '-4px'; handle.style.left = '-4px'; handle.style.cursor = 'nesw-resize'; break; case 's': handle.style.bottom = '-4px'; handle.style.left = '50%'; handle.style.transform = 'translateX(-50%)'; handle.style.cursor = 'ns-resize'; break; case 'se': handle.style.bottom = '-4px'; handle.style.right = '-4px'; handle.style.cursor = 'nwse-resize'; break; } handle.addEventListener('mousedown', function(e) { e.stopPropagation(); e.preventDefault(); self._handleResizeStart(e, instance.id, direction); }); handlesContainer.appendChild(handle); }); el.setAttribute('draggable', true); el.addEventListener('dragstart', function(e) { var rect; if (self.isResizing) { e.preventDefault(); return; } e.stopPropagation(); self.draggingComponent = instance.id; el.style.opacity = '0.5'; rect = el.getBoundingClientRect(); self.dragOffset.x = e.clientX - rect.left; self.dragOffset.y = e.clientY - rect.top; }); el.addEventListener('dragend', function() { el.style.opacity = '1'; self.draggingComponent = null; }); 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; this.isResizing = true; this.resizeDirection = direction; this.resizeStartPos = { x: e.clientX, y: e.clientY }; this.resizeStartSize = { colSpan: instance.grid.colSpan, 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; 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': newColSpan = Math.max(1, origColSpan + colDelta); newRowSpan = Math.max(1, origRowSpan + rowDelta); break; case 'e': newColSpan = Math.max(1, origColSpan + colDelta); break; case 's': newRowSpan = Math.max(1, origRowSpan + rowDelta); break; case 'w': newColSpan = Math.max(1, origColSpan - colDelta); newStartCol = startCol + (origColSpan - newColSpan); if (newStartCol < 0) { newStartCol = 0; newColSpan = origColSpan + startCol; } break; 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': newRowSpan = Math.max(1, origRowSpan - rowDelta); newStartRow = startRow + (origRowSpan - newRowSpan); if (newStartRow < 0) { newStartRow = 0; newRowSpan = origRowSpan + startRow; } break; 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': 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; } if (newStartRow < 0) { newStartRow = 0; newRowSpan = origRowSpan + startRow; } break; } newColSpan = Math.min(newColSpan, config.columns - newStartCol); newRowSpan = Math.min(newRowSpan, this.rows - newStartRow); 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; if (this.selectedComponent === instance.id) { this._showPropertyPanel(instance); } }, _handleResizeEnd: function() { var instance = this._findInstance(this.selectedComponent); this.isResizing = false; this.resizeDirection = null; this.canvasEl.style.pointerEvents = 'auto'; 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'; }, 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.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.background = this.phoneConfig.background; container.style.padding = this.phoneConfig.gap + 'px'; container.style.boxSizing = 'border-box'; columns = this.phoneConfig.columns; 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; startCol = instance.grid.startCell % columns; startRow = Math.floor(instance.grid.startCell / columns); colSpan = Math.min(instance.grid.colSpan, columns - startCol); rowSpan = instance.grid.rowSpan; wrapper = document.createElement('div'); wrapper.style.gridColumn = (startCol + 1) + ' / span ' + colSpan; wrapper.style.gridRow = (startRow + 1) + ' / span ' + rowSpan; wrapper.style.overflow = 'hidden'; componentElement = typeDef.render(instance.props); if (typeof componentElement === 'string') { wrapper.innerHTML = componentElement; componentElement = wrapper.firstChild; } else if (componentElement instanceof HTMLElement) { wrapper.appendChild(componentElement); } container.appendChild(wrapper); result.push({ element: componentElement, wrapper: wrapper, props: JSON.parse(JSON.stringify(instance.props)), id: instance.id, type: instance.type }); }); return result; }, _moveComponent: function(componentId, gridPos) { var instance = this._findInstance(componentId); var columns; var startCol; var startRow; var colSpan; if (!instance) return; instance.grid.startCell = (gridPos.row - 1) * this.phoneConfig.columns + (gridPos.column - 1); 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; if (this.selectedComponent === componentId) { this._showPropertyPanel(instance); } this._notifyChanged('move', instance); }, _selectComponent: function(componentId) { var instance; this._deselectAll(); 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() { var selected = this.canvasEl.querySelectorAll('.selected'); var self = this; this.selectedComponent = null; selected.forEach(function(el) { el.classList.remove('selected'); }); 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; html = '
' + (typeDef.label || instance.type) + '
'; html += '
'; html += '
位置大小
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; if (typeDef.traits && typeDef.traits.length > 0) { html += '
'; html += '
组件属性
'; typeDef.traits.forEach(function(trait) { var value = instance.props[trait.name] !== undefined ? instance.props[trait.name] : (trait.default || ''); html += '
'; html += ''; if (trait.type === 'number') { html += ''; } else if (trait.type === 'checkbox') { html += ''; } else { html += ''; } html += '
'; }); html += '
'; } html += '
'; html += ''; html += '
'; this.propertyPanelEl.innerHTML = html; 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) { value = parseInt(input.value, 10) || 1; if (prop === 'startCol') { currentRow = Math.floor(instance.grid.startCell / columns); instance.grid.startCell = currentRow * columns + (value - 1); } else if (prop === 'startRow') { currentCol = instance.grid.startCell % columns; instance.grid.startCell = (value - 1) * columns + currentCol; } else if (prop === 'colSpan') { instance.grid.colSpan = value; } else if (prop === 'rowSpan') { instance.grid.rowSpan = value; } sCol = instance.grid.startCell % columns; sRow = Math.floor(instance.grid.startCell / columns); cSpan = Math.min(instance.grid.colSpan, columns - sCol); instance.element.style.gridColumn = (sCol + 1) + ' / span ' + cSpan; instance.element.style.gridRow = (sRow + 1) + ' / span ' + instance.grid.rowSpan; } else if (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; } else if (traitDef && traitDef.type === 'number') { instance.props[trait] = parseFloat(input.value) || 0; } else { instance.props[trait] = input.value; } if (typeDef2.render) { content = typeDef2.render(instance.props); instance.element.innerHTML = ''; if (typeof content === 'string') { instance.element.innerHTML = content; } else if (content instanceof HTMLElement) { instance.element.appendChild(content); } handlesContainer = document.createElement('div'); handlesContainer.className = 'resize-handles-container'; handlesContainer.style.display = 'block'; handlesContainer.style.position = 'absolute'; handlesContainer.style.top = '0'; handlesContainer.style.left = '0'; handlesContainer.style.width = '100%'; handlesContainer.style.height = '100%'; handlesContainer.style.pointerEvents = 'none'; handlesContainer.style.zIndex = '10'; instance.element.appendChild(handlesContainer); instance.handlesContainer = handlesContainer; self._recreateResizeHandles(instance); } } self._notifyChanged('update', instance); }); }); deleteBtn = this.propertyPanelEl.querySelector('[data-action="delete"]'); if (deleteBtn) { 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; directions = ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se']; instance.handlesContainer.innerHTML = ''; directions.forEach(function(direction) { var handle = document.createElement('div'); handle.className = 'resize-handle resize-handle-' + direction; handle.setAttribute('data-direction', direction); self._applyHandleBaseStyle(handle); switch(direction) { case 'nw': handle.style.top = '-4px'; handle.style.left = '-4px'; handle.style.cursor = 'nwse-resize'; break; case 'n': handle.style.top = '-4px'; handle.style.left = '50%'; handle.style.transform = 'translateX(-50%)'; handle.style.cursor = 'ns-resize'; break; case 'ne': handle.style.top = '-4px'; handle.style.right = '-4px'; handle.style.cursor = 'nesw-resize'; break; case 'w': handle.style.top = '50%'; handle.style.left = '-4px'; handle.style.transform = 'translateY(-50%)'; handle.style.cursor = 'ew-resize'; break; case 'e': handle.style.top = '50%'; handle.style.right = '-4px'; handle.style.transform = 'translateY(-50%)'; handle.style.cursor = 'ew-resize'; break; case 'sw': handle.style.bottom = '-4px'; handle.style.left = '-4px'; handle.style.cursor = 'nesw-resize'; break; case 's': handle.style.bottom = '-4px'; handle.style.left = '50%'; handle.style.transform = 'translateX(-50%)'; handle.style.cursor = 'ns-resize'; break; case 'se': handle.style.bottom = '-4px'; handle.style.right = '-4px'; handle.style.cursor = 'nwse-resize'; break; } handle.addEventListener('mousedown', function(e) { e.stopPropagation(); e.preventDefault(); self._handleResizeStart(e, instance.id, direction); }); instance.handlesContainer.appendChild(handle); }); }, _deleteComponent: function(componentId) { var index = -1; var i; var instance; for (i = 0; i < this.componentInstances.length; i++) { if (this.componentInstances[i].id === componentId) { index = i; break; } } if (index === -1) return; 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) { var i; for (i = 0; i < this.componentInstances.length; i++) { if (this.componentInstances[i].id === componentId) { return this.componentInstances[i]; } } return null; }, define: function(name, definition) { if (this.componentTypes[name]) { console.warn('Component type already defined:', name); return; } this.componentTypes[name] = definition; this._addToToolbox(name, definition); }, _addToToolbox: function(name, definition) { var self = this; var item; var icon; var label; 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); icon = definition.icon || '\uD83D\uDCE6'; label = definition.label || name; item.innerHTML = '' + icon + '' + label + ''; item.addEventListener('dragstart', function() { self.draggingType = name; item.style.opacity = '0.5'; }); 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); }, toJSONB: function() { var self = this; var components; components = this.componentInstances.map(function(instance) { return { id: instance.id, type: instance.type, grid: { startCell: instance.grid.startCell, colSpan: instance.grid.colSpan, rowSpan: instance.grid.rowSpan }, props: JSON.parse(JSON.stringify(instance.props)) }; }); return { version: '1.0', layouts: { phone: { canvas: { width: this.phoneConfig.width, height: this.phoneConfig.height, columns: this.phoneConfig.columns, cellSize: this.cellSize, rows: this.rows, gap: this.phoneConfig.gap, background: this.phoneConfig.background }, components: components }, computer: null } }; }, fromJSONB: function(jsonb) { var phoneLayout; var self = this; var maxId; this.canvasEl.innerHTML = ''; this.componentInstances = []; this.selectedComponent = null; phoneLayout = jsonb.layouts && jsonb.layouts.phone; if (!phoneLayout) { console.warn('No phone layout found'); return; } if (phoneLayout.components) { phoneLayout.components.forEach(function(comp) { var typeDef = self.componentTypes[comp.type]; var instance; if (!typeDef) { console.warn('Component type not found:', comp.type); return; } instance = { id: comp.id, type: comp.type, grid: { startCell: comp.grid.startCell, colSpan: comp.grid.colSpan, rowSpan: comp.grid.rowSpan }, props: comp.props || {} }; self._renderComponent(instance); self.componentInstances.push(instance); }); maxId = 0; this.componentInstances.forEach(function(inst) { var match = inst.id.match(/-(\d+)$/); if (match) { maxId = Math.max(maxId, parseInt(match[1], 10)); } }); this.idCounter = maxId; } this._notifyChanged('load', { count: this.componentInstances.length }); }, getCanvasConfig: function() { return { width: this.phoneConfig.width, height: this.phoneConfig.height, columns: this.phoneConfig.columns, cellSize: this.cellSize, rows: this.rows, gap: this.phoneConfig.gap, background: this.phoneConfig.background }; }, 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); } };