feat: add onChanged callback, Delete key support, drag offset fix
- Add onChanged(callback) API for tracking page changes - Support Delete/Backspace key to delete selected component - Fix drag offset issue when moving components - Add unsaved changes tracking in editor demo - Update README documentation
This commit is contained in:
34
README.md
34
README.md
@@ -9,9 +9,11 @@ IoT 可视化编辑器库 - 基于网格布局的拖拽式页面构建器
|
|||||||
- 📱 固定手机比例画布(375×812)
|
- 📱 固定手机比例画布(375×812)
|
||||||
- 📐 网格布局,单元格 1:1 正方形
|
- 📐 网格布局,单元格 1:1 正方形
|
||||||
- 🖱️ 拖拽添加、移动组件
|
- 🖱️ 拖拽添加、移动组件
|
||||||
|
- ⌨️ Delete/Backspace键删除选中组件
|
||||||
- ⚙️ 属性面板,实时预览
|
- ⚙️ 属性面板,实时预览
|
||||||
- 💾 JSONB 格式保存/加载
|
- 💾 JSONB 格式保存/加载
|
||||||
- 🎨 自定义组件支持
|
- 🎨 自定义组件支持
|
||||||
|
- 🔔 变化回调通知
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
@@ -21,10 +23,10 @@ IoT 可视化编辑器库 - 基于网格布局的拖拽式页面构建器
|
|||||||
│ ├── webbuilder.js # 主文件
|
│ ├── webbuilder.js # 主文件
|
||||||
│ └── types/ # 内部模块
|
│ └── types/ # 内部模块
|
||||||
├── editor.html # 示例编辑器
|
├── editor.html # 示例编辑器
|
||||||
├── js/editor/editor.js # 示例编辑器逻辑
|
├── js/editor/editor.js # 示例编辑器逻辑
|
||||||
├── css/editor.css # 示例编辑器样式
|
├── css/editor.css # 示例编辑器样式
|
||||||
├── README.md # 中文文档
|
├── README.md # 中文文档
|
||||||
└── README_EN.md # English
|
└── README_EN.md # English
|
||||||
```
|
```
|
||||||
|
|
||||||
> **注意**: `editor.html` 和 `js/editor/` 是示例代码,展示如何使用本库。
|
> **注意**: `editor.html` 和 `js/editor/` 是示例代码,展示如何使用本库。
|
||||||
@@ -112,11 +114,29 @@ webbuilder.define(name, definition)
|
|||||||
defaultColumnSpan: 6, // 默认跨列数
|
defaultColumnSpan: 6, // 默认跨列数
|
||||||
defaultRowSpan: 4, // 默认跨行数
|
defaultRowSpan: 4, // 默认跨行数
|
||||||
defaultProps: { ... }, // 默认属性
|
defaultProps: { ... }, // 默认属性
|
||||||
traits: [ ... ], // 属性定义(用于属性面板)
|
traits: [ ... ], // 属性定义(用于属性面板)
|
||||||
render: function(props) { ... } // 渲染函数,返回 DOM 元素
|
render: function(props) { ... } // 渲染函数,返回 DOM 元素
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 注册变化回调
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
webbuilder.onChanged(function(action, data) {
|
||||||
|
// action: 'add' | 'delete' | 'move' | 'update' | 'load' | 'clear'
|
||||||
|
// data: 相关数据(组件实例、组件ID等)
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
| action | 说明 | data |
|
||||||
|
|--------|------|------|
|
||||||
|
| add | 添加组件 | 组件实例对象 |
|
||||||
|
| delete | 删除组件 | { id: 组件ID } |
|
||||||
|
| move | 移动组件 | 组件实例对象 |
|
||||||
|
| update | 属性面板修改属性 | 组件实例对象 |
|
||||||
|
| load | 加载JSONB | { count: 组件数量 } |
|
||||||
|
| clear | 清空画布 | null |
|
||||||
|
|
||||||
### 导出 JSONB
|
### 导出 JSONB
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
@@ -176,7 +196,7 @@ var components = webbuilder.renderToDiv(container);
|
|||||||
{
|
{
|
||||||
element: HTMLElement, // 组件 DOM 元素
|
element: HTMLElement, // 组件 DOM 元素
|
||||||
wrapper: HTMLElement, // 包装容器
|
wrapper: HTMLElement, // 包装容器
|
||||||
props: { ... }, // 组件属性(深拷贝)
|
props: { ... }, // 组件属性(深拷贝)
|
||||||
id: 'gauge-1',
|
id: 'gauge-1',
|
||||||
type: 'gauge'
|
type: 'gauge'
|
||||||
}
|
}
|
||||||
@@ -257,4 +277,4 @@ MIT
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*参考项目:[webbuilder.js](https://github.com/qrailibs/webbuilder.js)*
|
*参考项目:[webbuilder.js](https://github.com/qrailibs/webbuilder.js)*
|
||||||
|
|||||||
@@ -3,16 +3,24 @@
|
|||||||
* 仅支持手机端布局
|
* 仅支持手机端布局
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// 未保存标记
|
||||||
|
var isDirty = false;
|
||||||
|
|
||||||
// 初始化编辑器
|
// 初始化编辑器
|
||||||
function initEditor() {
|
function initEditor() {
|
||||||
// 初始化 webbuilder
|
// 初始化 webbuilder
|
||||||
webbuilder.init('#editor');
|
webbuilder.init('#editor');
|
||||||
|
|
||||||
// 注册示例组件
|
// 注册示例组件
|
||||||
registerComponents();
|
registerComponents();
|
||||||
|
|
||||||
// 绑定事件
|
// 绑定事件
|
||||||
bindEvents();
|
bindEvents();
|
||||||
|
|
||||||
|
// 注册变化回调
|
||||||
|
webbuilder.onChanged(function(action, data) {
|
||||||
|
isDirty = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,22 +47,22 @@ function registerComponents() {
|
|||||||
render: function(props) {
|
render: function(props) {
|
||||||
var el = document.createElement('div');
|
var el = document.createElement('div');
|
||||||
el.className = 'iot-gauge';
|
el.className = 'iot-gauge';
|
||||||
|
|
||||||
var valueEl = document.createElement('div');
|
var valueEl = document.createElement('div');
|
||||||
valueEl.className = 'value';
|
valueEl.className = 'value';
|
||||||
valueEl.textContent = props.value + props.unit;
|
valueEl.textContent = props.value + props.unit;
|
||||||
|
|
||||||
var labelEl = document.createElement('div');
|
var labelEl = document.createElement('div');
|
||||||
labelEl.className = 'label';
|
labelEl.className = 'label';
|
||||||
labelEl.textContent = props.label;
|
labelEl.textContent = props.label;
|
||||||
|
|
||||||
el.appendChild(valueEl);
|
el.appendChild(valueEl);
|
||||||
el.appendChild(labelEl);
|
el.appendChild(labelEl);
|
||||||
|
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 开关组件
|
// 开关组件
|
||||||
webbuilder.define('switch', {
|
webbuilder.define('switch', {
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
@@ -73,21 +81,21 @@ function registerComponents() {
|
|||||||
render: function(props) {
|
render: function(props) {
|
||||||
var el = document.createElement('div');
|
var el = document.createElement('div');
|
||||||
el.className = 'iot-switch';
|
el.className = 'iot-switch';
|
||||||
|
|
||||||
var switchEl = document.createElement('div');
|
var switchEl = document.createElement('div');
|
||||||
switchEl.className = 'switch' + (props.state ? ' active' : '');
|
switchEl.className = 'switch' + (props.state ? ' active' : '');
|
||||||
|
|
||||||
var labelEl = document.createElement('div');
|
var labelEl = document.createElement('div');
|
||||||
labelEl.className = 'label';
|
labelEl.className = 'label';
|
||||||
labelEl.textContent = props.label;
|
labelEl.textContent = props.label;
|
||||||
|
|
||||||
el.appendChild(switchEl);
|
el.appendChild(switchEl);
|
||||||
el.appendChild(labelEl);
|
el.appendChild(labelEl);
|
||||||
|
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 滑块组件
|
// 滑块组件
|
||||||
webbuilder.define('slider', {
|
webbuilder.define('slider', {
|
||||||
type: 'slider',
|
type: 'slider',
|
||||||
@@ -110,24 +118,24 @@ function registerComponents() {
|
|||||||
render: function(props) {
|
render: function(props) {
|
||||||
var el = document.createElement('div');
|
var el = document.createElement('div');
|
||||||
el.className = 'iot-slider';
|
el.className = 'iot-slider';
|
||||||
|
|
||||||
var labelEl = document.createElement('div');
|
var labelEl = document.createElement('div');
|
||||||
labelEl.className = 'label';
|
labelEl.className = 'label';
|
||||||
labelEl.textContent = props.label + ': ' + props.value;
|
labelEl.textContent = props.label + ': ' + props.value;
|
||||||
|
|
||||||
var inputEl = document.createElement('input');
|
var inputEl = document.createElement('input');
|
||||||
inputEl.type = 'range';
|
inputEl.type = 'range';
|
||||||
inputEl.min = props.min;
|
inputEl.min = props.min;
|
||||||
inputEl.max = props.max;
|
inputEl.max = props.max;
|
||||||
inputEl.value = props.value;
|
inputEl.value = props.value;
|
||||||
|
|
||||||
el.appendChild(labelEl);
|
el.appendChild(labelEl);
|
||||||
el.appendChild(inputEl);
|
el.appendChild(inputEl);
|
||||||
|
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 按钮组件
|
// 按钮组件
|
||||||
webbuilder.define('button', {
|
webbuilder.define('button', {
|
||||||
type: 'button',
|
type: 'button',
|
||||||
@@ -148,7 +156,7 @@ function registerComponents() {
|
|||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 数值显示组件
|
// 数值显示组件
|
||||||
webbuilder.define('value-display', {
|
webbuilder.define('value-display', {
|
||||||
type: 'value-display',
|
type: 'value-display',
|
||||||
@@ -169,18 +177,18 @@ function registerComponents() {
|
|||||||
render: function(props) {
|
render: function(props) {
|
||||||
var el = document.createElement('div');
|
var el = document.createElement('div');
|
||||||
el.className = 'iot-value-display';
|
el.className = 'iot-value-display';
|
||||||
|
|
||||||
var valueEl = document.createElement('div');
|
var valueEl = document.createElement('div');
|
||||||
valueEl.className = 'value';
|
valueEl.className = 'value';
|
||||||
valueEl.textContent = props.value + props.unit;
|
valueEl.textContent = props.value + props.unit;
|
||||||
|
|
||||||
var labelEl = document.createElement('div');
|
var labelEl = document.createElement('div');
|
||||||
labelEl.className = 'label';
|
labelEl.className = 'label';
|
||||||
labelEl.textContent = props.label;
|
labelEl.textContent = props.label;
|
||||||
|
|
||||||
el.appendChild(valueEl);
|
el.appendChild(valueEl);
|
||||||
el.appendChild(labelEl);
|
el.appendChild(labelEl);
|
||||||
|
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -195,41 +203,46 @@ function bindEvents() {
|
|||||||
var jsonb = webbuilder.toJSONB();
|
var jsonb = webbuilder.toJSONB();
|
||||||
var jsonStr = JSON.stringify(jsonb, null, 2);
|
var jsonStr = JSON.stringify(jsonb, null, 2);
|
||||||
downloadFile('canvas-config.json', jsonStr, 'application/json');
|
downloadFile('canvas-config.json', jsonStr, 'application/json');
|
||||||
|
isDirty = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 加载 JSONB
|
// 加载 JSONB
|
||||||
document.getElementById('btn-load').addEventListener('click', function() {
|
document.getElementById('btn-load').addEventListener('click', function() {
|
||||||
|
if (isDirty && !confirm('当前有未保存的更改,确定要加载新文件吗?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
document.getElementById('file-input').click();
|
document.getElementById('file-input').click();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('file-input').addEventListener('change', function(e) {
|
document.getElementById('file-input').addEventListener('change', function(e) {
|
||||||
var file = e.target.files[0];
|
var file = e.target.files[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
var reader = new FileReader();
|
var reader = new FileReader();
|
||||||
reader.onload = function(e) {
|
reader.onload = function(e) {
|
||||||
try {
|
try {
|
||||||
var jsonb = JSON.parse(e.target.result);
|
var jsonb = JSON.parse(e.target.result);
|
||||||
webbuilder.fromJSONB(jsonb);
|
webbuilder.fromJSONB(jsonb);
|
||||||
|
isDirty = false;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert('加载失败:' + err.message);
|
alert('加载失败:' + err.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
|
|
||||||
// 清空文件输入
|
// 清空文件输入
|
||||||
this.value = '';
|
this.value = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
// 导出 HTML
|
// 导出 HTML
|
||||||
document.getElementById('btn-export').addEventListener('click', function() {
|
document.getElementById('btn-export').addEventListener('click', function() {
|
||||||
// 创建一个临时 div 来渲染
|
// 创建一个临时 div 来渲染
|
||||||
var tempDiv = document.createElement('div');
|
var tempDiv = document.createElement('div');
|
||||||
document.body.appendChild(tempDiv);
|
document.body.appendChild(tempDiv);
|
||||||
|
|
||||||
// 调用 renderToDiv 获取组件列表
|
// 调用 renderToDiv 获取组件列表
|
||||||
var components = webbuilder.renderToDiv(tempDiv);
|
var components = webbuilder.renderToDiv(tempDiv);
|
||||||
|
|
||||||
// 生成 HTML
|
// 生成 HTML
|
||||||
var config = webbuilder.getCanvasConfig();
|
var config = webbuilder.getCanvasConfig();
|
||||||
var html = '<!DOCTYPE html>\n<html>\n<head>\n<meta charset="UTF-8">\n';
|
var html = '<!DOCTYPE html>\n<html>\n<head>\n<meta charset="UTF-8">\n';
|
||||||
@@ -251,25 +264,29 @@ function bindEvents() {
|
|||||||
}), null, 2) + ';\n';
|
}), null, 2) + ';\n';
|
||||||
html += '</script>\n';
|
html += '</script>\n';
|
||||||
html += '</body>\n</html>';
|
html += '</body>\n</html>';
|
||||||
|
|
||||||
// 移除临时 div
|
// 移除临时 div
|
||||||
document.body.removeChild(tempDiv);
|
document.body.removeChild(tempDiv);
|
||||||
|
|
||||||
showPreview(html);
|
showPreview(html);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 清空画布
|
// 清空画布
|
||||||
document.getElementById('btn-clear').addEventListener('click', function() {
|
document.getElementById('btn-clear').addEventListener('click', function() {
|
||||||
|
if (isDirty && !confirm('当前有未保存的更改,确定要清空画布吗?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (confirm('确定要清空画布吗?')) {
|
if (confirm('确定要清空画布吗?')) {
|
||||||
webbuilder.clear();
|
webbuilder.clear();
|
||||||
|
isDirty = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 预览弹窗关闭
|
// 预览弹窗关闭
|
||||||
document.querySelector('.modal-close').addEventListener('click', function() {
|
document.querySelector('.modal-close').addEventListener('click', function() {
|
||||||
document.getElementById('preview-modal').style.display = 'none';
|
document.getElementById('preview-modal').style.display = 'none';
|
||||||
});
|
});
|
||||||
|
|
||||||
// 复制代码
|
// 复制代码
|
||||||
document.getElementById('btn-copy').addEventListener('click', function() {
|
document.getElementById('btn-copy').addEventListener('click', function() {
|
||||||
var code = document.getElementById('preview-code').textContent;
|
var code = document.getElementById('preview-code').textContent;
|
||||||
@@ -277,12 +294,20 @@ function bindEvents() {
|
|||||||
alert('已复制到剪贴板');
|
alert('已复制到剪贴板');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 下载文件
|
// 下载文件
|
||||||
document.getElementById('btn-download').addEventListener('click', function() {
|
document.getElementById('btn-download').addEventListener('click', function() {
|
||||||
var code = document.getElementById('preview-code').textContent;
|
var code = document.getElementById('preview-code').textContent;
|
||||||
downloadFile('page.html', code, 'text/html');
|
downloadFile('page.html', code, 'text/html');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 页面离开提示
|
||||||
|
window.addEventListener('beforeunload', function(e) {
|
||||||
|
if (isDirty) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.returnValue = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
var webbuilder = {
|
var webbuilder = {
|
||||||
// 编辑器容器
|
// 编辑器容器
|
||||||
container: null,
|
container: null,
|
||||||
|
|
||||||
// 手机画布配置
|
// 手机画布配置
|
||||||
phoneConfig: {
|
phoneConfig: {
|
||||||
width: 375,
|
width: 375,
|
||||||
@@ -16,37 +16,43 @@ var webbuilder = {
|
|||||||
gap: 2,
|
gap: 2,
|
||||||
background: '#f5f5f5'
|
background: '#f5f5f5'
|
||||||
},
|
},
|
||||||
|
|
||||||
// 计算得出的单元格大小(1:1比例)
|
// 计算得出的单元格大小(1:1比例)
|
||||||
cellSize: 0,
|
cellSize: 0,
|
||||||
rows: 0,
|
rows: 0,
|
||||||
|
|
||||||
// 已注册的组件类型
|
// 已注册的组件类型
|
||||||
componentTypes: {},
|
componentTypes: {},
|
||||||
|
|
||||||
// 画布上的组件实例
|
// 画布上的组件实例
|
||||||
componentInstances: [],
|
componentInstances: [],
|
||||||
|
|
||||||
// 当前选中的组件
|
// 当前选中的组件
|
||||||
selectedComponent: null,
|
selectedComponent: null,
|
||||||
|
|
||||||
// 拖拽状态
|
// 拖拽状态
|
||||||
draggingComponent: null,
|
draggingComponent: null,
|
||||||
draggingType: null,
|
draggingType: null,
|
||||||
|
|
||||||
|
// 拖拽时鼠标相对于组件的偏移
|
||||||
|
dragOffset: { x: 0, y: 0 },
|
||||||
|
|
||||||
// 唯一ID计数器
|
// 唯一ID计数器
|
||||||
idCounter: 0,
|
idCounter: 0,
|
||||||
|
|
||||||
|
// 变化回调函数
|
||||||
|
onChangedCallback: null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算单元格大小和行数
|
* 计算单元格大小和行数
|
||||||
*/
|
*/
|
||||||
_calculateGrid: function() {
|
_calculateGrid: function() {
|
||||||
var config = this.phoneConfig;
|
var config = this.phoneConfig;
|
||||||
|
|
||||||
// 计算列宽(单元格大小)
|
// 计算列宽(单元格大小)
|
||||||
// 列宽 = (画布宽度 - (列数 + 1) * gap) / 列数
|
// 列宽 = (画布宽度 - (列数 + 1) * gap) / 列数
|
||||||
this.cellSize = (config.width - (config.columns + 1) * config.gap) / config.columns;
|
this.cellSize = (config.width - (config.columns + 1) * config.gap) / config.columns;
|
||||||
|
|
||||||
// 计算行数
|
// 计算行数
|
||||||
// 画布高度 = 行数 * 单元格大小 + (行数 + 1) * gap
|
// 画布高度 = 行数 * 单元格大小 + (行数 + 1) * gap
|
||||||
// 812 = 行数 * cellSize + (行数 + 1) * gap
|
// 812 = 行数 * cellSize + (行数 + 1) * gap
|
||||||
@@ -54,11 +60,11 @@ var webbuilder = {
|
|||||||
// 812 - gap = 行数 * (cellSize + gap)
|
// 812 - gap = 行数 * (cellSize + gap)
|
||||||
// 行数 = (812 - gap) / (cellSize + gap)
|
// 行数 = (812 - gap) / (cellSize + gap)
|
||||||
this.rows = Math.floor((config.height - config.gap) / (this.cellSize + config.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;
|
this.phoneConfig.height = this.rows * this.cellSize + (this.rows + 1) * config.gap;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化编辑器
|
* 初始化编辑器
|
||||||
* @param {string|HTMLElement} containerSelector - 容器选择器或元素
|
* @param {string|HTMLElement} containerSelector - 容器选择器或元素
|
||||||
@@ -70,55 +76,58 @@ var webbuilder = {
|
|||||||
} else {
|
} else {
|
||||||
this.container = containerSelector;
|
this.container = containerSelector;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.container) {
|
if (!this.container) {
|
||||||
throw new Error('Container element not found');
|
throw new Error('Container element not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算网格
|
// 计算网格
|
||||||
this._calculateGrid();
|
this._calculateGrid();
|
||||||
|
|
||||||
// 创建编辑器结构
|
// 创建编辑器结构
|
||||||
this._createEditorStructure();
|
this._createEditorStructure();
|
||||||
|
|
||||||
// 初始化画布
|
// 初始化画布
|
||||||
this._initCanvas();
|
this._initCanvas();
|
||||||
|
|
||||||
// 初始化工具箱
|
// 初始化工具箱
|
||||||
this._initToolbox();
|
this._initToolbox();
|
||||||
|
|
||||||
|
// 初始化键盘事件
|
||||||
|
this._initKeyboardEvents();
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建编辑器结构
|
* 创建编辑器结构
|
||||||
*/
|
*/
|
||||||
_createEditorStructure: function() {
|
_createEditorStructure: function() {
|
||||||
this.container.innerHTML = '';
|
this.container.innerHTML = '';
|
||||||
this.container.classList.add('webbuilder-editor');
|
this.container.classList.add('webbuilder-editor');
|
||||||
|
|
||||||
// 工具箱容器
|
// 工具箱容器
|
||||||
this.toolboxEl = document.createElement('div');
|
this.toolboxEl = document.createElement('div');
|
||||||
this.toolboxEl.className = 'webbuilder-toolbox';
|
this.toolboxEl.className = 'webbuilder-toolbox';
|
||||||
this.container.appendChild(this.toolboxEl);
|
this.container.appendChild(this.toolboxEl);
|
||||||
|
|
||||||
// 画布容器
|
// 画布容器
|
||||||
this.canvasWrapperEl = document.createElement('div');
|
this.canvasWrapperEl = document.createElement('div');
|
||||||
this.canvasWrapperEl.className = 'webbuilder-canvas-wrapper';
|
this.canvasWrapperEl.className = 'webbuilder-canvas-wrapper';
|
||||||
this.container.appendChild(this.canvasWrapperEl);
|
this.container.appendChild(this.canvasWrapperEl);
|
||||||
|
|
||||||
// 属性面板容器
|
// 属性面板容器
|
||||||
this.propertyPanelEl = document.createElement('div');
|
this.propertyPanelEl = document.createElement('div');
|
||||||
this.propertyPanelEl.className = 'webbuilder-property-panel';
|
this.propertyPanelEl.className = 'webbuilder-property-panel';
|
||||||
this.container.appendChild(this.propertyPanelEl);
|
this.container.appendChild(this.propertyPanelEl);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化画布
|
* 初始化画布
|
||||||
*/
|
*/
|
||||||
_initCanvas: function() {
|
_initCanvas: function() {
|
||||||
var config = this.phoneConfig;
|
var config = this.phoneConfig;
|
||||||
|
|
||||||
// 创建画布
|
// 创建画布
|
||||||
this.canvasEl = document.createElement('div');
|
this.canvasEl = document.createElement('div');
|
||||||
this.canvasEl.className = 'webbuilder-canvas';
|
this.canvasEl.className = 'webbuilder-canvas';
|
||||||
@@ -133,7 +142,7 @@ var webbuilder = {
|
|||||||
this.canvasEl.style.padding = config.gap + 'px';
|
this.canvasEl.style.padding = config.gap + 'px';
|
||||||
this.canvasEl.style.boxSizing = 'border-box';
|
this.canvasEl.style.boxSizing = 'border-box';
|
||||||
this.canvasWrapperEl.appendChild(this.canvasEl);
|
this.canvasWrapperEl.appendChild(this.canvasEl);
|
||||||
|
|
||||||
// 画布点击取消选中
|
// 画布点击取消选中
|
||||||
var self = this;
|
var self = this;
|
||||||
this.canvasEl.addEventListener('click', function(e) {
|
this.canvasEl.addEventListener('click', function(e) {
|
||||||
@@ -141,21 +150,21 @@ var webbuilder = {
|
|||||||
self._deselectAll();
|
self._deselectAll();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 拖放事件
|
// 拖放事件
|
||||||
this.canvasEl.addEventListener('dragover', function(e) {
|
this.canvasEl.addEventListener('dragover', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
self.canvasEl.classList.add('dropping');
|
self.canvasEl.classList.add('dropping');
|
||||||
});
|
});
|
||||||
|
|
||||||
this.canvasEl.addEventListener('dragleave', function(e) {
|
this.canvasEl.addEventListener('dragleave', function(e) {
|
||||||
self.canvasEl.classList.remove('dropping');
|
self.canvasEl.classList.remove('dropping');
|
||||||
});
|
});
|
||||||
|
|
||||||
this.canvasEl.addEventListener('drop', function(e) {
|
this.canvasEl.addEventListener('drop', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
self.canvasEl.classList.remove('dropping');
|
self.canvasEl.classList.remove('dropping');
|
||||||
|
|
||||||
if (self.draggingType) {
|
if (self.draggingType) {
|
||||||
// 从工具箱拖入新组件
|
// 从工具箱拖入新组件
|
||||||
var gridPos = self._calculateGridPosition(e);
|
var gridPos = self._calculateGridPosition(e);
|
||||||
@@ -169,7 +178,7 @@ var webbuilder = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化工具箱
|
* 初始化工具箱
|
||||||
*/
|
*/
|
||||||
@@ -179,29 +188,64 @@ var webbuilder = {
|
|||||||
this.toolboxListEl.className = 'webbuilder-toolbox-list';
|
this.toolboxListEl.className = 'webbuilder-toolbox-list';
|
||||||
this.toolboxEl.appendChild(this.toolboxListEl);
|
this.toolboxEl.appendChild(this.toolboxListEl);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化键盘事件
|
||||||
|
*/
|
||||||
|
_initKeyboardEvents: function() {
|
||||||
|
var self = this;
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
// Delete键删除选中的组件
|
||||||
|
if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||||
|
if (self.selectedComponent) {
|
||||||
|
e.preventDefault();
|
||||||
|
self._deleteComponent(self.selectedComponent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册变化回调
|
||||||
|
* @param {function} callback - 回调函数,签名:callback(action, data)
|
||||||
|
* action: 'add' | 'delete' | 'move' | 'update' | 'load' | 'clear'
|
||||||
|
* data: 相关数据(组件实例、组件ID等)
|
||||||
|
*/
|
||||||
|
onChanged: function(callback) {
|
||||||
|
this.onChangedCallback = callback;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发变化回调
|
||||||
|
*/
|
||||||
|
_notifyChanged: function(action, data) {
|
||||||
|
if (typeof this.onChangedCallback === 'function') {
|
||||||
|
this.onChangedCallback(action, data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算网格位置
|
* 计算网格位置
|
||||||
*/
|
*/
|
||||||
_calculateGridPosition: function(e) {
|
_calculateGridPosition: function(e) {
|
||||||
var rect = this.canvasEl.getBoundingClientRect();
|
var rect = this.canvasEl.getBoundingClientRect();
|
||||||
var config = this.phoneConfig;
|
var config = this.phoneConfig;
|
||||||
|
|
||||||
// 计算鼠标相对于画布的位置
|
// 计算鼠标相对于画布的位置,减去偏移量
|
||||||
var x = e.clientX - rect.left - config.gap;
|
var x = e.clientX - rect.left - config.gap - this.dragOffset.x;
|
||||||
var y = e.clientY - rect.top - config.gap;
|
var y = e.clientY - rect.top - config.gap - this.dragOffset.y;
|
||||||
|
|
||||||
// 计算列和行
|
// 计算列和行
|
||||||
var col = Math.floor(x / (this.cellSize + config.gap)) + 1;
|
var col = Math.floor(x / (this.cellSize + config.gap)) + 1;
|
||||||
var row = Math.floor(y / (this.cellSize + config.gap)) + 1;
|
var row = Math.floor(y / (this.cellSize + config.gap)) + 1;
|
||||||
|
|
||||||
// 边界约束
|
// 边界约束
|
||||||
col = Math.max(1, Math.min(config.columns, col));
|
col = Math.max(1, Math.min(config.columns, col));
|
||||||
row = Math.max(1, Math.min(this.rows, row));
|
row = Math.max(1, Math.min(this.rows, row));
|
||||||
|
|
||||||
return { column: col, row: row };
|
return { column: col, row: row };
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加组件到画布
|
* 添加组件到画布
|
||||||
*/
|
*/
|
||||||
@@ -211,13 +255,13 @@ var webbuilder = {
|
|||||||
console.error('Component type not found:', componentType);
|
console.error('Component type not found:', componentType);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成唯一ID
|
// 生成唯一ID
|
||||||
var id = componentType + '-' + (++this.idCounter);
|
var id = componentType + '-' + (++this.idCounter);
|
||||||
|
|
||||||
// 计算 startCell
|
// 计算 startCell
|
||||||
var startCell = (gridPos.row - 1) * this.phoneConfig.columns + (gridPos.column - 1);
|
var startCell = (gridPos.row - 1) * this.phoneConfig.columns + (gridPos.column - 1);
|
||||||
|
|
||||||
// 创建组件实例
|
// 创建组件实例
|
||||||
var instance = {
|
var instance = {
|
||||||
id: id,
|
id: id,
|
||||||
@@ -229,33 +273,36 @@ var webbuilder = {
|
|||||||
},
|
},
|
||||||
props: JSON.parse(JSON.stringify(typeDef.defaultProps || {}))
|
props: JSON.parse(JSON.stringify(typeDef.defaultProps || {}))
|
||||||
};
|
};
|
||||||
|
|
||||||
// 渲染组件
|
// 渲染组件
|
||||||
this._renderComponent(instance);
|
this._renderComponent(instance);
|
||||||
|
|
||||||
// 添加到实例列表
|
// 添加到实例列表
|
||||||
this.componentInstances.push(instance);
|
this.componentInstances.push(instance);
|
||||||
|
|
||||||
// 选中新添加的组件
|
// 选中新添加的组件
|
||||||
this._selectComponent(instance.id);
|
this._selectComponent(instance.id);
|
||||||
|
|
||||||
|
// 触发回调
|
||||||
|
this._notifyChanged('add', instance);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 渲染组件到画布(编辑器内部使用)
|
* 渲染组件到画布(编辑器内部使用)
|
||||||
*/
|
*/
|
||||||
_renderComponent: function(instance) {
|
_renderComponent: function(instance) {
|
||||||
var typeDef = this.componentTypes[instance.type];
|
var typeDef = this.componentTypes[instance.type];
|
||||||
if (!typeDef) return;
|
if (!typeDef) return;
|
||||||
|
|
||||||
// 计算行列位置
|
// 计算行列位置
|
||||||
var columns = this.phoneConfig.columns;
|
var columns = this.phoneConfig.columns;
|
||||||
var startCol = instance.grid.startCell % columns;
|
var startCol = instance.grid.startCell % columns;
|
||||||
var startRow = Math.floor(instance.grid.startCell / columns);
|
var startRow = Math.floor(instance.grid.startCell / columns);
|
||||||
|
|
||||||
// 边界约束
|
// 边界约束
|
||||||
var colSpan = Math.min(instance.grid.colSpan, columns - startCol);
|
var colSpan = Math.min(instance.grid.colSpan, columns - startCol);
|
||||||
var rowSpan = instance.grid.rowSpan;
|
var rowSpan = instance.grid.rowSpan;
|
||||||
|
|
||||||
// 创建组件容器元素
|
// 创建组件容器元素
|
||||||
var el = document.createElement('div');
|
var el = document.createElement('div');
|
||||||
el.className = 'webbuilder-component';
|
el.className = 'webbuilder-component';
|
||||||
@@ -265,7 +312,7 @@ var webbuilder = {
|
|||||||
el.style.gridRow = (startRow + 1) + ' / span ' + rowSpan;
|
el.style.gridRow = (startRow + 1) + ' / span ' + rowSpan;
|
||||||
el.style.position = 'relative';
|
el.style.position = 'relative';
|
||||||
el.style.overflow = 'hidden';
|
el.style.overflow = 'hidden';
|
||||||
|
|
||||||
// 渲染组件内容 - render 函数返回 HTML 元素对象
|
// 渲染组件内容 - render 函数返回 HTML 元素对象
|
||||||
if (typeDef.render) {
|
if (typeDef.render) {
|
||||||
var content = typeDef.render(instance.props);
|
var content = typeDef.render(instance.props);
|
||||||
@@ -277,7 +324,7 @@ var webbuilder = {
|
|||||||
el.appendChild(content);
|
el.appendChild(content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使组件可拖拽
|
// 使组件可拖拽
|
||||||
var self = this;
|
var self = this;
|
||||||
el.setAttribute('draggable', true);
|
el.setAttribute('draggable', true);
|
||||||
@@ -285,25 +332,29 @@ var webbuilder = {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
self.draggingComponent = instance.id;
|
self.draggingComponent = instance.id;
|
||||||
el.style.opacity = '0.5';
|
el.style.opacity = '0.5';
|
||||||
|
// 计算鼠标相对于组件的偏移
|
||||||
|
var rect = el.getBoundingClientRect();
|
||||||
|
self.dragOffset.x = e.clientX - rect.left;
|
||||||
|
self.dragOffset.y = e.clientY - rect.top;
|
||||||
});
|
});
|
||||||
el.addEventListener('dragend', function(e) {
|
el.addEventListener('dragend', function(e) {
|
||||||
el.style.opacity = '1';
|
el.style.opacity = '1';
|
||||||
self.draggingComponent = null;
|
self.draggingComponent = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 点击选中
|
// 点击选中
|
||||||
el.addEventListener('click', function(e) {
|
el.addEventListener('click', function(e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
self._selectComponent(instance.id);
|
self._selectComponent(instance.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 存储元素引用
|
// 存储元素引用
|
||||||
instance.element = el;
|
instance.element = el;
|
||||||
|
|
||||||
// 添加到画布
|
// 添加到画布
|
||||||
this.canvasEl.appendChild(el);
|
this.canvasEl.appendChild(el);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将组件渲染到指定的 div 容器
|
* 将组件渲染到指定的 div 容器
|
||||||
* @param {HTMLElement} container - 目标容器
|
* @param {HTMLElement} container - 目标容器
|
||||||
@@ -312,10 +363,10 @@ var webbuilder = {
|
|||||||
renderToDiv: function(container) {
|
renderToDiv: function(container) {
|
||||||
var self = this;
|
var self = this;
|
||||||
var result = [];
|
var result = [];
|
||||||
|
|
||||||
// 清空容器
|
// 清空容器
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
// 创建画布样式
|
// 创建画布样式
|
||||||
container.style.width = this.phoneConfig.width + 'px';
|
container.style.width = this.phoneConfig.width + 'px';
|
||||||
container.style.height = this.phoneConfig.height + 'px';
|
container.style.height = this.phoneConfig.height + 'px';
|
||||||
@@ -326,29 +377,29 @@ var webbuilder = {
|
|||||||
container.style.background = this.phoneConfig.background;
|
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';
|
container.style.boxSizing = 'border-box';
|
||||||
|
|
||||||
var columns = this.phoneConfig.columns;
|
var columns = this.phoneConfig.columns;
|
||||||
|
|
||||||
// 遍历所有组件实例
|
// 遍历所有组件实例
|
||||||
this.componentInstances.forEach(function(instance) {
|
this.componentInstances.forEach(function(instance) {
|
||||||
var typeDef = self.componentTypes[instance.type];
|
var typeDef = self.componentTypes[instance.type];
|
||||||
if (!typeDef || !typeDef.render) return;
|
if (!typeDef || !typeDef.render) return;
|
||||||
|
|
||||||
// 计算行列位置
|
// 计算行列位置
|
||||||
var startCol = instance.grid.startCell % columns;
|
var startCol = instance.grid.startCell % columns;
|
||||||
var startRow = Math.floor(instance.grid.startCell / columns);
|
var startRow = Math.floor(instance.grid.startCell / columns);
|
||||||
var colSpan = Math.min(instance.grid.colSpan, columns - startCol);
|
var colSpan = Math.min(instance.grid.colSpan, columns - startCol);
|
||||||
var rowSpan = instance.grid.rowSpan;
|
var rowSpan = instance.grid.rowSpan;
|
||||||
|
|
||||||
// 创建组件容器
|
// 创建组件容器
|
||||||
var wrapper = document.createElement('div');
|
var wrapper = document.createElement('div');
|
||||||
wrapper.style.gridColumn = (startCol + 1) + ' / span ' + colSpan;
|
wrapper.style.gridColumn = (startCol + 1) + ' / span ' + colSpan;
|
||||||
wrapper.style.gridRow = (startRow + 1) + ' / span ' + rowSpan;
|
wrapper.style.gridRow = (startRow + 1) + ' / span ' + rowSpan;
|
||||||
wrapper.style.overflow = 'hidden';
|
wrapper.style.overflow = 'hidden';
|
||||||
|
|
||||||
// 调用 render 函数获取组件元素
|
// 调用 render 函数获取组件元素
|
||||||
var componentElement = typeDef.render(instance.props);
|
var componentElement = typeDef.render(instance.props);
|
||||||
|
|
||||||
// 如果返回的是字符串,保持兼容
|
// 如果返回的是字符串,保持兼容
|
||||||
if (typeof componentElement === 'string') {
|
if (typeof componentElement === 'string') {
|
||||||
wrapper.innerHTML = componentElement;
|
wrapper.innerHTML = componentElement;
|
||||||
@@ -356,10 +407,10 @@ var webbuilder = {
|
|||||||
} else if (componentElement instanceof HTMLElement) {
|
} else if (componentElement instanceof HTMLElement) {
|
||||||
wrapper.appendChild(componentElement);
|
wrapper.appendChild(componentElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加到容器
|
// 添加到容器
|
||||||
container.appendChild(wrapper);
|
container.appendChild(wrapper);
|
||||||
|
|
||||||
// 添加到结果列表
|
// 添加到结果列表
|
||||||
result.push({
|
result.push({
|
||||||
element: componentElement, // 控件本身的 HTML 元素对象
|
element: componentElement, // 控件本身的 HTML 元素对象
|
||||||
@@ -369,53 +420,56 @@ var webbuilder = {
|
|||||||
type: instance.type
|
type: instance.type
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 移动组件
|
* 移动组件
|
||||||
*/
|
*/
|
||||||
_moveComponent: function(componentId, gridPos) {
|
_moveComponent: function(componentId, gridPos) {
|
||||||
var instance = this._findInstance(componentId);
|
var instance = this._findInstance(componentId);
|
||||||
if (!instance) return;
|
if (!instance) return;
|
||||||
|
|
||||||
// 更新 startCell
|
// 更新 startCell
|
||||||
instance.grid.startCell = (gridPos.row - 1) * this.phoneConfig.columns + (gridPos.column - 1);
|
instance.grid.startCell = (gridPos.row - 1) * this.phoneConfig.columns + (gridPos.column - 1);
|
||||||
|
|
||||||
// 重新计算行列位置
|
// 重新计算行列位置
|
||||||
var columns = this.phoneConfig.columns;
|
var columns = this.phoneConfig.columns;
|
||||||
var startCol = instance.grid.startCell % columns;
|
var startCol = instance.grid.startCell % columns;
|
||||||
var startRow = Math.floor(instance.grid.startCell / columns);
|
var startRow = Math.floor(instance.grid.startCell / columns);
|
||||||
var colSpan = Math.min(instance.grid.colSpan, columns - startCol);
|
var colSpan = Math.min(instance.grid.colSpan, columns - startCol);
|
||||||
|
|
||||||
// 更新样式
|
// 更新样式
|
||||||
instance.element.style.gridColumn = (startCol + 1) + ' / span ' + colSpan;
|
instance.element.style.gridColumn = (startCol + 1) + ' / span ' + colSpan;
|
||||||
instance.element.style.gridRow = (startRow + 1) + ' / span ' + instance.grid.rowSpan;
|
instance.element.style.gridRow = (startRow + 1) + ' / span ' + instance.grid.rowSpan;
|
||||||
|
|
||||||
// 更新属性面板
|
// 更新属性面板
|
||||||
if (this.selectedComponent === componentId) {
|
if (this.selectedComponent === componentId) {
|
||||||
this._showPropertyPanel(instance);
|
this._showPropertyPanel(instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 触发回调
|
||||||
|
this._notifyChanged('move', instance);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 选中组件
|
* 选中组件
|
||||||
*/
|
*/
|
||||||
_selectComponent: function(componentId) {
|
_selectComponent: function(componentId) {
|
||||||
// 取消之前的选中
|
// 取消之前的选中
|
||||||
this._deselectAll();
|
this._deselectAll();
|
||||||
|
|
||||||
var instance = this._findInstance(componentId);
|
var instance = this._findInstance(componentId);
|
||||||
if (!instance) return;
|
if (!instance) return;
|
||||||
|
|
||||||
this.selectedComponent = componentId;
|
this.selectedComponent = componentId;
|
||||||
instance.element.classList.add('selected');
|
instance.element.classList.add('selected');
|
||||||
|
|
||||||
// 显示属性面板
|
// 显示属性面板
|
||||||
this._showPropertyPanel(instance);
|
this._showPropertyPanel(instance);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 取消所有选中
|
* 取消所有选中
|
||||||
*/
|
*/
|
||||||
@@ -427,7 +481,7 @@ var webbuilder = {
|
|||||||
});
|
});
|
||||||
this.propertyPanelEl.innerHTML = '';
|
this.propertyPanelEl.innerHTML = '';
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 显示属性面板
|
* 显示属性面板
|
||||||
*/
|
*/
|
||||||
@@ -435,13 +489,13 @@ var webbuilder = {
|
|||||||
var typeDef = this.componentTypes[instance.type];
|
var typeDef = this.componentTypes[instance.type];
|
||||||
var self = this;
|
var self = this;
|
||||||
var columns = this.phoneConfig.columns;
|
var columns = this.phoneConfig.columns;
|
||||||
|
|
||||||
// 计算当前行列
|
// 计算当前行列
|
||||||
var startCol = instance.grid.startCell % columns;
|
var startCol = instance.grid.startCell % columns;
|
||||||
var startRow = Math.floor(instance.grid.startCell / columns);
|
var startRow = Math.floor(instance.grid.startCell / columns);
|
||||||
|
|
||||||
var html = '<div class="property-title">' + (typeDef.label || instance.type) + '</div>';
|
var html = '<div class="property-title">' + (typeDef.label || instance.type) + '</div>';
|
||||||
|
|
||||||
// 基础属性
|
// 基础属性
|
||||||
html += '<div class="property-section">';
|
html += '<div class="property-section">';
|
||||||
html += '<div class="property-section-title">位置大小</div>';
|
html += '<div class="property-section-title">位置大小</div>';
|
||||||
@@ -450,7 +504,7 @@ var webbuilder = {
|
|||||||
html += '<div class="property-row"><label>跨列数:</label><input type="number" data-prop="colSpan" value="' + instance.grid.colSpan + '" min="1" max="' + columns + '"></div>';
|
html += '<div class="property-row"><label>跨列数:</label><input type="number" data-prop="colSpan" value="' + instance.grid.colSpan + '" min="1" max="' + columns + '"></div>';
|
||||||
html += '<div class="property-row"><label>跨行数:</label><input type="number" data-prop="rowSpan" value="' + instance.grid.rowSpan + '" min="1"></div>';
|
html += '<div class="property-row"><label>跨行数:</label><input type="number" data-prop="rowSpan" value="' + instance.grid.rowSpan + '" min="1"></div>';
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
// 自定义属性
|
// 自定义属性
|
||||||
if (typeDef.traits && typeDef.traits.length > 0) {
|
if (typeDef.traits && typeDef.traits.length > 0) {
|
||||||
html += '<div class="property-section">';
|
html += '<div class="property-section">';
|
||||||
@@ -470,23 +524,23 @@ var webbuilder = {
|
|||||||
});
|
});
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除按钮
|
// 删除按钮
|
||||||
html += '<div class="property-section">';
|
html += '<div class="property-section">';
|
||||||
html += '<button class="btn-delete" data-action="delete">删除组件</button>';
|
html += '<button class="btn-delete" data-action="delete">删除组件</button>';
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
this.propertyPanelEl.innerHTML = html;
|
this.propertyPanelEl.innerHTML = html;
|
||||||
|
|
||||||
// 绑定事件
|
// 绑定事件
|
||||||
this.propertyPanelEl.querySelectorAll('input').forEach(function(input) {
|
this.propertyPanelEl.querySelectorAll('input').forEach(function(input) {
|
||||||
input.addEventListener('change', function() {
|
input.addEventListener('change', function() {
|
||||||
var prop = input.getAttribute('data-prop');
|
var prop = input.getAttribute('data-prop');
|
||||||
var trait = input.getAttribute('data-trait');
|
var trait = input.getAttribute('data-trait');
|
||||||
|
|
||||||
if (prop) {
|
if (prop) {
|
||||||
// 更新网格位置
|
// 更新网格位置
|
||||||
var value = parseInt(input.value) || 1;
|
var value = parseInt(input.value, 10) || 1;
|
||||||
if (prop === 'startCol') {
|
if (prop === 'startCol') {
|
||||||
var currentRow = Math.floor(instance.grid.startCell / columns);
|
var currentRow = Math.floor(instance.grid.startCell / columns);
|
||||||
instance.grid.startCell = currentRow * columns + (value - 1);
|
instance.grid.startCell = currentRow * columns + (value - 1);
|
||||||
@@ -498,19 +552,19 @@ var webbuilder = {
|
|||||||
} else if (prop === 'rowSpan') {
|
} else if (prop === 'rowSpan') {
|
||||||
instance.grid.rowSpan = value;
|
instance.grid.rowSpan = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重新计算位置
|
// 重新计算位置
|
||||||
var startCol = instance.grid.startCell % columns;
|
var startCol = instance.grid.startCell % columns;
|
||||||
var startRow = Math.floor(instance.grid.startCell / columns);
|
var startRow = Math.floor(instance.grid.startCell / columns);
|
||||||
var colSpan = Math.min(instance.grid.colSpan, columns - startCol);
|
var colSpan = Math.min(instance.grid.colSpan, columns - startCol);
|
||||||
|
|
||||||
instance.element.style.gridColumn = (startCol + 1) + ' / span ' + colSpan;
|
instance.element.style.gridColumn = (startCol + 1) + ' / span ' + colSpan;
|
||||||
instance.element.style.gridRow = (startRow + 1) + ' / span ' + instance.grid.rowSpan;
|
instance.element.style.gridRow = (startRow + 1) + ' / span ' + instance.grid.rowSpan;
|
||||||
} else if (trait) {
|
} else if (trait) {
|
||||||
// 更新组件属性
|
// 更新组件属性
|
||||||
var typeDef = self.componentTypes[instance.type];
|
var typeDef = self.componentTypes[instance.type];
|
||||||
var traitDef = typeDef.traits.find(function(t) { return t.name === trait; });
|
var traitDef = typeDef.traits.find(function(t) { return t.name === trait; });
|
||||||
|
|
||||||
if (input.type === 'checkbox') {
|
if (input.type === 'checkbox') {
|
||||||
instance.props[trait] = input.checked;
|
instance.props[trait] = input.checked;
|
||||||
} else if (traitDef && traitDef.type === 'number') {
|
} else if (traitDef && traitDef.type === 'number') {
|
||||||
@@ -518,7 +572,7 @@ var webbuilder = {
|
|||||||
} else {
|
} else {
|
||||||
instance.props[trait] = input.value;
|
instance.props[trait] = input.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重新渲染组件内容
|
// 重新渲染组件内容
|
||||||
if (typeDef.render) {
|
if (typeDef.render) {
|
||||||
var content = typeDef.render(instance.props);
|
var content = typeDef.render(instance.props);
|
||||||
@@ -530,9 +584,12 @@ var webbuilder = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 触发属性更新回调
|
||||||
|
self._notifyChanged('update', instance);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 删除按钮
|
// 删除按钮
|
||||||
var deleteBtn = this.propertyPanelEl.querySelector('[data-action="delete"]');
|
var deleteBtn = this.propertyPanelEl.querySelector('[data-action="delete"]');
|
||||||
if (deleteBtn) {
|
if (deleteBtn) {
|
||||||
@@ -541,7 +598,7 @@ var webbuilder = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除组件
|
* 删除组件
|
||||||
*/
|
*/
|
||||||
@@ -553,23 +610,26 @@ var webbuilder = {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index === -1) return;
|
if (index === -1) return;
|
||||||
|
|
||||||
var instance = this.componentInstances[index];
|
var instance = this.componentInstances[index];
|
||||||
|
|
||||||
// 从DOM移除
|
// 从DOM移除
|
||||||
if (instance.element) {
|
if (instance.element) {
|
||||||
instance.element.remove();
|
instance.element.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从数组移除
|
// 从数组移除
|
||||||
this.componentInstances.splice(index, 1);
|
this.componentInstances.splice(index, 1);
|
||||||
|
|
||||||
// 取消选中
|
// 取消选中
|
||||||
this._deselectAll();
|
this._deselectAll();
|
||||||
|
|
||||||
|
// 触发回调
|
||||||
|
this._notifyChanged('delete', { id: componentId });
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查找组件实例
|
* 查找组件实例
|
||||||
*/
|
*/
|
||||||
@@ -581,7 +641,7 @@ var webbuilder = {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注册组件类型
|
* 注册组件类型
|
||||||
* @param {string} name - 组件名称
|
* @param {string} name - 组件名称
|
||||||
@@ -592,29 +652,29 @@ var webbuilder = {
|
|||||||
console.warn('Component type already defined:', name);
|
console.warn('Component type already defined:', name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.componentTypes[name] = definition;
|
this.componentTypes[name] = definition;
|
||||||
|
|
||||||
// 添加到工具箱
|
// 添加到工具箱
|
||||||
this._addToToolbox(name, definition);
|
this._addToToolbox(name, definition);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加组件到工具箱
|
* 添加组件到工具箱
|
||||||
*/
|
*/
|
||||||
_addToToolbox: function(name, definition) {
|
_addToToolbox: function(name, definition) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
var item = document.createElement('div');
|
var item = document.createElement('div');
|
||||||
item.className = 'webbuilder-toolbox-item';
|
item.className = 'webbuilder-toolbox-item';
|
||||||
item.setAttribute('draggable', true);
|
item.setAttribute('draggable', true);
|
||||||
item.setAttribute('data-type', name);
|
item.setAttribute('data-type', name);
|
||||||
|
|
||||||
var icon = definition.icon || '📦';
|
var icon = definition.icon || '📦';
|
||||||
var label = definition.label || name;
|
var label = definition.label || name;
|
||||||
|
|
||||||
item.innerHTML = '<span class="icon">' + icon + '</span><span class="label">' + label + '</span>';
|
item.innerHTML = '<span class="icon">' + icon + '</span><span class="label">' + label + '</span>';
|
||||||
|
|
||||||
// 拖拽事件
|
// 拖拽事件
|
||||||
item.addEventListener('dragstart', function(e) {
|
item.addEventListener('dragstart', function(e) {
|
||||||
self.draggingType = name;
|
self.draggingType = name;
|
||||||
@@ -624,10 +684,10 @@ var webbuilder = {
|
|||||||
item.style.opacity = '1';
|
item.style.opacity = '1';
|
||||||
self.draggingType = null;
|
self.draggingType = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.toolboxListEl.appendChild(item);
|
this.toolboxListEl.appendChild(item);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导出为JSONB
|
* 导出为JSONB
|
||||||
* 返回格式: { version, layouts: { phone: {...}, computer: null } }
|
* 返回格式: { version, layouts: { phone: {...}, computer: null } }
|
||||||
@@ -645,7 +705,7 @@ var webbuilder = {
|
|||||||
props: JSON.parse(JSON.stringify(instance.props))
|
props: JSON.parse(JSON.stringify(instance.props))
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
version: '1.0',
|
version: '1.0',
|
||||||
layouts: {
|
layouts: {
|
||||||
@@ -665,7 +725,7 @@ var webbuilder = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从JSONB加载
|
* 从JSONB加载
|
||||||
*/
|
*/
|
||||||
@@ -674,14 +734,14 @@ var webbuilder = {
|
|||||||
this.canvasEl.innerHTML = '';
|
this.canvasEl.innerHTML = '';
|
||||||
this.componentInstances = [];
|
this.componentInstances = [];
|
||||||
this.selectedComponent = null;
|
this.selectedComponent = null;
|
||||||
|
|
||||||
// 获取手机布局
|
// 获取手机布局
|
||||||
var phoneLayout = jsonb.layouts && jsonb.layouts.phone;
|
var phoneLayout = jsonb.layouts && jsonb.layouts.phone;
|
||||||
if (!phoneLayout) {
|
if (!phoneLayout) {
|
||||||
console.warn('No phone layout found');
|
console.warn('No phone layout found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染组件
|
// 渲染组件
|
||||||
if (phoneLayout.components) {
|
if (phoneLayout.components) {
|
||||||
var self = this;
|
var self = this;
|
||||||
@@ -691,7 +751,7 @@ var webbuilder = {
|
|||||||
console.warn('Component type not found:', comp.type);
|
console.warn('Component type not found:', comp.type);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var instance = {
|
var instance = {
|
||||||
id: comp.id,
|
id: comp.id,
|
||||||
type: comp.type,
|
type: comp.type,
|
||||||
@@ -702,23 +762,26 @@ var webbuilder = {
|
|||||||
},
|
},
|
||||||
props: comp.props || {}
|
props: comp.props || {}
|
||||||
};
|
};
|
||||||
|
|
||||||
self._renderComponent(instance);
|
self._renderComponent(instance);
|
||||||
self.componentInstances.push(instance);
|
self.componentInstances.push(instance);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 更新ID计数器
|
// 更新ID计数器
|
||||||
var maxId = 0;
|
var maxId = 0;
|
||||||
this.componentInstances.forEach(function(inst) {
|
this.componentInstances.forEach(function(inst) {
|
||||||
var match = inst.id.match(/-(\d+)$/);
|
var match = inst.id.match(/-(\d+)$/);
|
||||||
if (match) {
|
if (match) {
|
||||||
maxId = Math.max(maxId, parseInt(match[1]));
|
maxId = Math.max(maxId, parseInt(match[1], 10));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.idCounter = maxId;
|
this.idCounter = maxId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 触发回调
|
||||||
|
this._notifyChanged('load', { count: this.componentInstances.length });
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取画布配置
|
* 获取画布配置
|
||||||
*/
|
*/
|
||||||
@@ -733,14 +796,14 @@ var webbuilder = {
|
|||||||
background: this.phoneConfig.background
|
background: this.phoneConfig.background
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有组件实例
|
* 获取所有组件实例
|
||||||
*/
|
*/
|
||||||
getComponents: function() {
|
getComponents: function() {
|
||||||
return JSON.parse(JSON.stringify(this.componentInstances));
|
return JSON.parse(JSON.stringify(this.componentInstances));
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清空画布
|
* 清空画布
|
||||||
*/
|
*/
|
||||||
@@ -749,5 +812,8 @@ var webbuilder = {
|
|||||||
this.componentInstances = [];
|
this.componentInstances = [];
|
||||||
this.selectedComponent = null;
|
this.selectedComponent = null;
|
||||||
this.propertyPanelEl.innerHTML = '';
|
this.propertyPanelEl.innerHTML = '';
|
||||||
|
|
||||||
|
// 触发回调
|
||||||
|
this._notifyChanged('clear', null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user