diff --git a/demoHtml/flex/GEMINI.md b/demoHtml/flex/GEMINI.md index 9f16136..743b306 100644 --- a/demoHtml/flex/GEMINI.md +++ b/demoHtml/flex/GEMINI.md @@ -19,6 +19,96 @@ ``` +## localStorage 数据结构 +**localStorage存储的数据结构放置在data.json中** + +## index.js核心逻辑 +整个逻辑可以分为以下几个主要部分: + + 1. 全局结构和初始化 + + 代码被包裹在一个立即执行函数表达式 (function () { ... })(); 中,这样做可以创建一个独立的作用域,避免污染全局命名空间。 + + - 核心对象: + - main 和 welcome: 这两个对象分别存储了“主画布”和“欢迎画布”的状态,包括它们的 grid 实例和 initData(布局数据)。 + - currentComponent: 一个全局变量,用于存放当前被选中的组件或画布对象。 + + - `init(type, self)` 函数 (初始化函数): 这是整个应用启动的入口点。 + 1. 加载数据: 尝试从 localStorage 中读取名为 ${type}Data (例如 mainData 或 welcomeData) 的数据。如果存在,就解析它;如果不存在,就创建一套默认的画布配置。 + 2. 坐标转换: 在加载现有数据后,会调用 conputedInitData('init', self),这个关键函数会将存储的像素坐标(x, y, w, h)转换回 GridStack 能理解的栅格单位(行、列)。 + 3. 初始化GridStack: 使用 GridStack.init() 来初始化一个网格布局的容器。GridStack.js 是一个强大的库,它负责处理组件的拖拽、缩放、碰撞检测等核心交互功能。这里配置了行高、边距、是否接受拖入组件等。 + 4. 渲染初始组件: GridStack.init() 时传入的 children 数组(来自 initData)会被自动渲染到画布上。 + 5. 绑定事件: + - 遍历所有已存在的组件,为它们调用 handleAddComponent 函数,以附加点击事件和设置初始视图。 + - 监听 grid 的 added 事件:当一个新组件被拖入时触发,同样调用 handleAddComponent。 + - 监听 grid 的 removed 事件:当一个组件被删除时触发 handleRemoveComponent。 + + 2. 组件处理逻辑 + + - `GridStack.setupDragIn(...)`: 这段代码定义了左侧“组件列表”中的可拖拽项。它为“图片”和“文本”这两种组件预设了默认的属性(如尺寸、类型、名称、背景色等)。当用户把它们拖到画布上时,GridStack 就会使用这些预设值创建一个新的组件实例。 + + - `handleAddComponent(component, self)` 函数: + 1. 分配ID: 如果组件没有ID,则使用 generateUniqueId() 生成一个唯一ID。 + 2. 更新视图: 调用 setComponentView(component),根据组件的属性(如背景色、图片路径、文字内容等)来渲染其实际外观。 + 3. 绑定点击事件: 为每个组件的DOM元素添加一个点击事件监听器。 + - 当组件被点击时,首先会清除上一个选中组件的“聚焦样式”(通过 clearOldFocusStyle)。 + - 将 currentComponent 变量更新为当前点击的组件。 + - 调用 setCurrentComponentProps(currentComponent),将该组件的属性填充到右侧的“属性面板”中。 + - 调用 setCurrentFocusStyle(),为当前组件添加“聚焦样式”(例如,改变边框、背景或应用 transform: scale)。 + + 3. 属性面板交互 + + 右侧的属性面板是用户配置组件和画布的地方。 + + - `setCurrentComponentProps(component)` 函数: 这是一个核心的UI更新函数。 + - 它接收一个组件(或画布)对象。 + - 根据对象的类型和拥有的属性(hasOwnProperty),动态地显示或隐藏表单中的各个输入框(如名称、背景色、图片URL、字体大小等)。 + - 将组件的当前值填充到对应的输入框中。例如,文本组件会显示字体大小、颜色等输入框,而图片组件则不会。 + + - `bindComponentEvents()` 函数: 这个函数为属性面板中的所有输入框绑定了事件。 + - 它使用一个 elementMappings 对象来定义每个输入框ID、它应该监听的事件(blur 或 change)以及它对应的组件属性名。 + - 当用户修改了某个输入框的值并触发事件(如输入框失去焦点)时,会执行一个回调: + 1. 获取输入框的新值。 + 2. 更新 currentComponent 对象上对应的属性。 + 3. 调用 setComponentView(currentComponent, [property]),并传入被修改的属性名,从而即时更新画布上组件的视觉样式,实现“所见即所得”。 + + 4. 数据保存与坐标计算 + + - `handleSaveClick()` 函数: + - 当点击“保存”按钮时,它会分别调用 main.grid.save() 和 welcome.grid.save()。grid.save() 是 GridStack 的一个方法,它会返回一个描述所有子组件布局信息的数组(包含 x, y, w, h 等栅格单位的坐标)。 + - 将获取到的组件数组存回 main.initData.children 和 welcome.initData.children。 + - 调用 conputedInitData('save', ...)。 + + - `conputedInitData(type, self)` 函数: 这个函数是数据持久化的关键。 + - 保存时 (`type === 'save'`): 它遍历所有组件,将 GridStack 提供的栅格坐标(如 x: 1, w: 2)乘以一个固定的缩放因子(如画布宽度 1920 / 12),将其转换成像素单位的坐标。这样做是为了让保存的数据与具体的 GridStack 配置解耦。 + - 初始化时 (`type === 'init'`): 它执行相反的操作,将之前保存的像素坐标除以缩放因子,还原成 GridStack 能识别的栅格坐标。 + + 5. 其他功能 + + - `handleTabSwitch()`: 处理“主画布”和“欢迎画布”之间的切换,通过控制 display 样式来显示或隐藏对应的画布。 + - `setComponentView()`: 使用了“策略模式”(componentStrategies 对象),为不同的组件属性(如 background, image, fontSize)定义了专门的DOM操作函数,使代码更清晰、易于扩展。 + - 焦点样式: setCurrentFocusStyle 和 clearOldFocusStyle 两个函数配合,实现了选中组件时的高亮视觉效果。 + + 总结 + + 整个应用的运行流程如下: + + 1. 启动: 页面加载后,init() 函数被调用,分别初始化 main 和 welcome 两个画布。 + 2. 加载/创建: init() 从 localStorage 加载布局数据,或创建新布局,并使用 GridStack.js 渲染出来。 + 3. 交互: + - 用户可以从左侧拖动新组件到画布上。 + - 用户可以点击画布上的任一组件,此时该组件被“激活”。 + 4. 配置: + - 组件被激活后,右侧属性面板会显示其所有可配置属性。 + - 用户在属性面板中修改值,所做的更改会实时反映在画布的组件上。 + 5. 保存: + - 用户点击“保存”按钮。 + - 当前两个画布的布局(使用栅格坐标)被 GridStack 导出。 + - 坐标被转换为像素单位。 + - 最终的布局数据(包含像素坐标)被序列化成JSON字符串,存储到 localStorage 中。 + + 下次用户打开页面时,流程会从第1步重新开始,但这次会从 localStorage 加载上次保存的数据,从而恢复之前的布局状态。 + ## 请阅读以下要求,必须严格遵守 * 执行任何任务的时候,请先输出你的思路,等待我确认之后再修改代码 * 有任何不确认的地方,请先向我确认再执行任务 diff --git a/demoHtml/flex/data.json b/demoHtml/flex/data.json new file mode 100644 index 0000000..be63f32 --- /dev/null +++ b/demoHtml/flex/data.json @@ -0,0 +1,136 @@ +{ + "mainData": { + "type": "screen", + "id": "grid_mjwdt2rookzdwukwhkd", + "children": [ + { + "w": 320, + "type": "text", + "childrenType": "", + "name": "文本", + "background": "#e15b5b91", + "fontSize": 20, + "color": "#22ff0091", + "text": "我是测试文本", + "fontWeight": "normal", + "eventsType": "click", + "eventsAction": "", + "defaultFocus": false, + "leftId": "", + "rightId": "", + "upId": "", + "downId": "", + "focusedStyle_background": "", + "focusedStyle_border_width": 0, + "focusedStyle_border_color": "", + "focusedStyle_scale": 1, + "x": 1600, + "y": 0, + "id": "text_mjwed122tesl6qelizj", + "content": "我是测试文本", + "xCopy": 10, + "yCopy": 0, + "wCopy": 2, + "hCopy": 1, + "h": 120 + }, + { + "w": 320, + "h": 240, + "type": "image", + "name": "图片", + "background": "#114db58f", + "image": "https://mgt.xlzzslzy.top/demoHtml/flex/image/movie.png", + "eventsType": "click", + "eventsAction": "", + "defaultFocus": false, + "leftId": "", + "rightId": "", + "upId": "", + "downId": "", + "focusedStyle_background": "", + "focusedStyle_border_width": 0, + "focusedStyle_border_color": "", + "focusedStyle_scale": 1, + "x": 0, + "y": 840, + "id": "image_mjweczmglkeuds146x", + "content": "\n \n ", + "xCopy": 0, + "yCopy": 7, + "wCopy": 2, + "hCopy": 2 + } + ], + "version": "1.0.0", + "width": 1920, + "height": 1080, + "background": "", + "image": "https://mgt.xlzzslzy.top/demoHtml/flex/image/1.png" + }, + "welcomeData": { + "type": "screen", + "id": "grid_mjwdt2rmpf3kas429w", + "children": [ + { + "w": 320, + "h": 240, + "type": "image", + "name": "图片111", + "background": "#4addb8c7", + "image": "https://mgt.xlzzslzy.top/demoHtml/flex/image/movie.png", + "eventsType": "click", + "eventsAction": "", + "defaultFocus": false, + "leftId": "", + "rightId": "", + "upId": "", + "downId": "", + "focusedStyle_background": "#000000", + "focusedStyle_border_width": "2", + "focusedStyle_border_color": "#ffffff", + "focusedStyle_scale": "1.2", + "x": 160, + "y": 720, + "id": "image_mjwdt6mv4cx4dmc9e7y", + "content": "\n \n ", + "xCopy": 1, + "yCopy": 6, + "wCopy": 2, + "hCopy": 2 + }, + { + "w": 320, + "h": 240, + "type": "image", + "name": "图片", + "background": "#14009629", + "image": "https://mgt.xlzzslzy.top/demoHtml/flex/image/movie.png", + "eventsType": "click", + "eventsAction": "", + "defaultFocus": false, + "leftId": "", + "rightId": "", + "upId": "", + "downId": "", + "focusedStyle_background": "", + "focusedStyle_border_width": 0, + "focusedStyle_border_color": "", + "focusedStyle_scale": 1, + "x": 1600, + "y": 840, + "id": "image_mjwdtdn85h9ngxddaiv", + "content": "\n \n ", + "xCopy": 10, + "yCopy": 7, + "wCopy": 2, + "hCopy": 2 + } + ], + "version": "1.0.0", + "width": 1920, + "height": 1080, + "background": "", + "image": "https://mgt.xlzzslzy.top/demoHtml/flex/image/1.png" + } +} diff --git a/demoHtml/flex/js/index.js b/demoHtml/flex/js/index.js index 090c817..e8bc596 100644 --- a/demoHtml/flex/js/index.js +++ b/demoHtml/flex/js/index.js @@ -149,7 +149,9 @@ if (!component.id) { component.id = `${component.type}_${generateUniqueId()}`; } + // 组件首次加载或被添加到画布时,调用 setComponentView,且没有传递 `fields` 参数。这意味着对组件进行一次完整渲染。它会读取组件对象上所有的初始属性,并一股脑地应用到DOM元素上,确保组件从一开始就显示正确的样式。 setComponentView(component); + component.el.addEventListener('click', (e) => { e.stopPropagation(); console.log(isScreen(component.type) ? `点击画布` : `点击组件`, component); @@ -511,6 +513,11 @@ $element.off(mapping.event).on(mapping.event, function () { const value = $(this).val(); currentComponent[mapping.property] = value; + /** + * 组件的属性在右侧面板中被修改时 + * 用户选中一个组件后,在右侧的属性面板中修改了某个值(例如,修改了字号,或者更改了图片URL),然后输入框失去焦点(blur)或值改变(change) + * 在这个时机调用 setComponentView,并且传递了 `fields` 参数,该参数是一个只包含被修改属性名称的数组(例如 ['fontSize'])。这意味着进行一次高效的增量更新。程序精确地告诉setComponentView:“只需要更新字体大小这一个样式,其他的别动。” + */ setComponentView(currentComponent, [mapping.property]); }); }