refactor: optimize Vue canvas interaction logic
English | 简体中文
PR 重构: 优化 Vue 画布交互逻辑,提取元素定位和交互方法
PR Checklist
Please check if your PR fulfills the following requirements:
- [x] The commit message follows our Commit Message Guidelines
- [ ] Tests for the changes have been added (for bug fixes / features)
- [ ] Docs have been added / updated (for bug fixes / features)
- [ ] Built its own designer, fully self-validated
PR Type
What kind of change does this PR introduce?
- [ ] Bugfix
- [x] Feature
- [ ] Code style update (formatting, local variables)
- [ ] Refactoring (no functional changes, no api changes)
- [ ] Build related changes
- [ ] CI related changes
- [ ] Documentation content changes
- [ ] Other... Please describe:
Background and solution
What is the current behavior?
Issue Number: N/A
What is the new behavior?
【功能修改点说明】:
- 修复: Vue画布下 Element Plus 的 DatePicker、组件被 disabled 的场景无法选中组件的 bug。
- 修复:初始状态下,没有选中任何组件,hover 组件不生效的 bug。
- 增加:插入节点之后的选中节点的延时,确保画布已经更新,避免拖拽更换节点后,节点不选中的 bug。
- 增加: Element Plus DatePicker 组件。
- 增加:右键选中节点的功能,在组件disabled 状态时,右键能够触发事件,从而进行选中。
- 增加:点击 hover 节点左上角组件名进行选中的逻辑,在组件被 disabled 状态,data-uid 属性无法挂载的场景下特别有用。
【重构修改说明】
- 抽取节点选中、hover 相关逻辑,使得选中、hover 逻辑集中;并根据配置区分 Vue 画布和通用画布的选中hover逻辑。
- 增加 Vue 画布下的元素定位逻辑,使得 disabled 状态的组件以及无法挂在 data-uid 属性的组件也能被选中。
- 将 inactiveHover 和 hoverState 合并,通过
isInactiveNode进行区分当前 hover 的是否是当前画布的节点。 - 将 hover、insertLine 的相关节点从
CanvasAction组件中分别抽离到CanvasHover、CanvasInsertLine组件,避免在多选的情况下,重复渲染多个 hover 节点。
【通用画布元素定位新逻辑说明】
- 查询最近的带有 data-uid 属性的 DOM 节点
- 通过 data-uid 属性值查询得到对应的 schema 节点。
- 通过
getBoundingClientRect计算DOM 节点的矩形信息。
适配:与画布技术栈无关,通用性强 缺陷:如果画布无法挂载 data-uid 属性到 DOM 节点上,那么该节点无法反查到对应的 node 节点,导致 hover 、选中等逻辑无法生效。
【Vue 画布元素定位新逻辑说明】
前置依赖:需要 vue 画布注入 __vueComponent 到当前 DOM 节点中。
- 通过鼠标事件获取到当前点击节点树最近的带有
__vueComponent的节点,通过 __vueComponent 获取到 vue 实例。 - 通过 vue 实例获取到最近的带有 schema.id 的真正 vue 实例,通过 id 获取到对应的 node 节点。
- 计算真正的 vue 实例的 rect 信息,更新到 hoverState 中。
Does this PR introduce a breaking change?
- [x] Yes
- [ ] No
Other information
测试页面 schema:
{
"componentName": "Page",
"css": ".page-base-style {\n padding: 24px;\n background: #ffffff;\n}\n.block-base-style {\n margin: 16px;\n}\n.component-base-style {\n margin: 8px;\n}\n.text-ofpin {\n margin: 8px;\n font-size: 18px;\n font-weight: 700;\n}\n.text-tkonm {\n margin: 8px;\n font-size: 20px;\n font-weight: 700;\n display: block;\n}\n.div-kqykn {\n margin: 8px;\n margin-top: 20px;\n}\n.div-cukqn {\n margin: 8px;\n margin-top: 20px;\n}\n.text-mrjfb {\n margin: 8px;\n font-size: 20px;\n font-weight: 700;\n}\n.text-qovif {\n margin: 8px;\n font-size: 20px;\n font-weight: 700;\n}\n.text-gojtr {\n margin: 8px;\n font-size: 20px;\n font-weight: 700;\n}\n.text-nqswn {\n margin: 8px;\n font-size: 20px;\n font-weight: 700;\n display: block;\n}\n.text-flgjp {\n margin: 8px;\n font-size: 20px;\n font-weight: 700;\n}\n.text-jvlui {\n margin: 8px;\n font-size: 20px;\n font-weight: 700;\n display: block;\n}\n.text-ivmur {\n margin: 8px;\n font-size: 20px;\n font-weight: 700;\n display: block;\n}\n.text-guubn {\n margin: 8px;\n font-size: 20px;\n font-weight: 700;\n}\n.div-usgfl {\n margin: 8px;\n margin-top: 20px;\n}\n.div-rufmu {\n margin: 8px;\n margin-top: 20px;\n padding-bottom: 20px;\n}\n.div-orqwq {\n margin: 8px;\n margin-top: 20px;\n}\n.text-ljlhh {\n margin: 8px;\n font-size: 20px;\n font-weight: 700;\n}\n.text-qmwuv {\n margin: 8px;\n font-size: 20px;\n font-weight: 700;\n}\n",
"props": {
"className": "page-base-style"
},
"lifeCycles": {},
"children": [
{
"componentName": "div",
"props": {
"className": "component-base-style div-rufmu"
},
"children": [
{
"componentName": "div",
"props": {
"className": "component-base-style"
},
"children": [],
"id": "666b9254"
},
{
"componentName": "div",
"props": {
"className": "component-base-style"
},
"children": [
{
"componentName": "Text",
"props": {
"style": "display: inline-block;",
"className": "component-base-style text-ljlhh",
"text": "特殊组件:ElDatePicker"
},
"children": [],
"id": "e3633545"
},
{
"componentName": "Text",
"props": {
"style": "display: inline-block;",
"className": "component-base-style",
"text": "element-plus 的 DatePicker 是一个 Fragment 组件,实际的内容被挂在到 body 上面了,所以没法挂在 data-uid 属性,以前的方法无法选中"
},
"children": [],
"id": "52564e2d"
}
],
"id": "53262513"
},
{
"componentName": "ElDatePicker",
"props": {
"className": "component-base-style",
"disabled": false
},
"children": [],
"id": "36366565"
}
],
"id": "15325456"
},
{
"componentName": "div",
"props": {
"className": "component-base-style div-usgfl"
},
"children": [
{
"componentName": "div",
"props": {
"className": "component-base-style"
},
"children": [
{
"componentName": "Text",
"props": {
"style": "display: inline-block;",
"className": "component-base-style text-guubn",
"text": "Disabled 组件被禁用场景"
},
"children": [],
"id": "2c646862"
},
{
"componentName": "Text",
"props": {
"style": "display: inline-block;",
"className": "component-base-style",
"text": "组件被禁用,可以通过右键,或者是点击 hover 时左上角显示的组件名称进行选中"
},
"children": [],
"id": "635436c2"
}
],
"id": "24f64625"
},
{
"componentName": "TinyButton",
"props": {
"text": "按钮文案",
"className": "component-base-style",
"disabled": true
},
"children": [],
"id": "6155c24d"
},
{
"componentName": "TinySearch",
"props": {
"modelValue": "",
"placeholder": "输入关键词",
"className": "component-base-style",
"disabled": true
},
"children": [],
"id": "35153244"
},
{
"componentName": "TinyInput",
"props": {
"placeholder": "请输入",
"modelValue": "",
"className": "component-base-style",
"disabled": true
},
"children": [],
"id": "248e4d21"
}
],
"id": "36655334"
},
{
"componentName": "div",
"props": {
"className": "component-base-style div-orqwq"
},
"children": [
{
"componentName": "Text",
"props": {
"className": "component-base-style text-ivmur",
"text": "循环渲染"
},
"children": [],
"id": "6a65454a"
},
{
"componentName": "TinyButton",
"props": {
"text": {
"type": "JSExpression",
"value": "item.text"
},
"className": "component-base-style",
"type": {
"type": "JSExpression",
"value": "item.type"
}
},
"children": [],
"id": "2345445d",
"loop": {
"type": "JSExpression",
"value": "[\n {\n text: '主要按钮',\n type: 'primary'\n },\n {\n text: '次要按钮'\n },\n {\n text: '成功按钮',\n type: 'success'\n },\n {\n text: '信息按钮',\n type: 'info'\n },\n {\n text: '警告按钮',\n type: 'warning'\n }\n]"
},
"loopArgs": [
"item",
"index"
]
}
],
"id": "54c56422"
},
{
"componentName": "div",
"props": {
"className": "component-base-style"
},
"children": [
{
"componentName": "Text",
"props": {
"style": "display: inline-block;",
"className": "component-base-style text-ofpin",
"text": "布局与容器"
},
"children": [],
"id": "4b633543"
},
{
"componentName": "CanvasRowColContainer",
"props": {
"rowGap": "16px",
"className": "component-base-style"
},
"children": [
{
"componentName": "CanvasRow",
"props": {
"rowGap": "16px",
"colGap": "16px"
},
"children": [
{
"componentName": "CanvasCol",
"props": {
"rowGap": "16px",
"colGap": "16px",
"grow": true,
"shrink": true,
"widthType": "auto"
},
"id": "32423124"
}
],
"id": "43315562"
}
],
"id": "15594525"
},
{
"componentName": "div",
"props": {
"className": "component-base-style"
},
"children": [],
"id": "2242c153"
},
{
"componentName": "CanvasFlexBox",
"props": {
"flexDirection": "row",
"gap": "8px",
"padding": "8px",
"className": "component-base-style"
},
"children": [],
"id": "a6225365"
},
{
"componentName": "CanvasSection",
"props": {
"className": "component-base-style"
},
"children": [],
"id": "23336513"
},
{
"componentName": "TinyLayout",
"props": {
"className": "component-base-style"
},
"children": [
{
"componentName": "TinyRow",
"props": {
"style": "padding: 10px;"
},
"children": [
{
"componentName": "TinyCol",
"props": {
"span": 3
},
"id": "525d2664"
},
{
"componentName": "TinyCol",
"props": {
"span": 3
},
"id": "4534364b"
},
{
"componentName": "TinyCol",
"props": {
"span": 3
},
"id": "6423ca16"
},
{
"componentName": "TinyCol",
"props": {
"span": 3
},
"id": "4bd32366"
}
],
"id": "4c332165"
},
{
"componentName": "TinyRow",
"props": {
"style": "padding: 10px;"
},
"children": [
{
"componentName": "TinyCol",
"props": {
"span": 3
},
"id": "15233246"
},
{
"componentName": "TinyCol",
"props": {
"span": 3
},
"id": "a3f14353"
},
{
"componentName": "TinyCol",
"props": {
"span": 3
},
"id": "63536264"
},
{
"componentName": "TinyCol",
"props": {
"span": 3
},
"id": "9b51465e"
}
],
"id": "9442364b"
}
],
"id": "23466436"
}
],
"id": "22124528"
},
{
"componentName": "div",
"props": {
"className": "component-base-style div-kqykn"
},
"children": [
{
"componentName": "Text",
"props": {
"className": "component-base-style text-tkonm",
"text": "基础元素"
},
"children": [],
"id": "132a464d"
},
{
"componentName": "Text",
"props": {
"style": "display: inline-block;",
"text": "TinyEngine 前端可视化设计器,为设计器开发者提供定制服务,在线构建出自己专属的设计器。",
"className": "component-base-style"
},
"children": [],
"id": "11314228"
},
{
"componentName": "Icon",
"props": {
"name": "IconDel",
"className": "component-base-style"
},
"children": [],
"id": "e21a5724"
},
{
"componentName": "Img",
"props": {
"src": "https://tinyengine-assets.obs.cn-north-4.myhuaweicloud.com/files/designer-default-icon.jpg",
"className": "component-base-style"
},
"children": [],
"id": "64b65e28"
},
{
"componentName": "p",
"children": "TinyEngine 前端可视化设计器致力于通过友好的用户交互提升业务应用的开发效率。",
"props": {
"className": "component-base-style"
},
"id": "25125564"
},
{
"componentName": "hr",
"props": {
"className": "component-base-style"
},
"children": [],
"id": "64225325"
},
{
"componentName": "a",
"children": "链接",
"props": {
"className": "component-base-style"
},
"id": "34553331"
},
{
"componentName": "h1",
"props": {
"className": "component-base-style"
},
"children": "Heading",
"id": "6355c334"
},
{
"componentName": "video",
"props": {
"src": "img/webNova.jpg",
"width": "200",
"height": "100",
"style": "border:1px solid #ccc",
"className": "component-base-style"
},
"children": [],
"id": "63266244"
},
{
"componentName": "TinyButton",
"props": {
"text": "按钮文案",
"className": "component-base-style"
},
"children": [],
"id": "4c122462"
},
{
"componentName": "div",
"props": {
"className": "component-base-style"
},
"children": [
{
"componentName": "TinyButton",
"props": {
"text": "提交",
"type": "primary",
"style": "margin: 0 5px 0 5px;"
},
"id": "43563525"
},
{
"componentName": "TinyButton",
"props": {
"text": "重置",
"style": "margin: 0 5px 0 5px;"
},
"id": "56d34256"
},
{
"componentName": "TinyButton",
"props": {
"text": "取消"
},
"id": "85226153"
},
{
"componentName": "TinyButtonGroup",
"props": {
"data": [
{
"text": "Button1",
"value": "1"
},
{
"text": "Button2",
"value": "2"
},
{
"text": "Button3",
"value": "3"
}
],
"modelValue": "1",
"className": "component-base-style"
},
"children": [],
"id": "62231442"
},
{
"componentName": "TinySearch",
"props": {
"modelValue": "",
"placeholder": "输入关键词",
"className": "component-base-style"
},
"children": [],
"id": "13552646"
}
],
"id": "55e51933"
}
],
"id": "52868343"
},
{
"componentName": "div",
"props": {
"className": "component-base-style div-cukqn"
},
"children": [
{
"componentName": "Text",
"props": {
"style": "display: inline-block;",
"className": "component-base-style text-mrjfb",
"text": "高级元素"
},
"children": [],
"id": "53527565"
},
{
"componentName": "Slot",
"props": {
"className": "component-base-style"
},
"children": [],
"id": "2432f143",
"show": true,
"showEye": false
},
{
"componentName": "RouterView",
"props": {
"className": "component-base-style"
},
"children": [],
"id": "63e53416"
},
{
"componentName": "RouterLink",
"props": {
"className": "component-base-style"
},
"children": [
{
"componentName": "Text",
"props": {
"text": "路由文本"
},
"id": "67c62462",
"children": []
},
{
"componentName": "div",
"props": {
"style": "text-align: center; padding: 8px 12px; box-shadow: 0 0 4px #0003;",
"className": "component-base-style"
},
"children": [
{
"componentName": "RouterLink",
"props": {
"to": "",
"style": "display: inline-flex; gap: 8px; padding: 10px 20px; color: inherit; text-decoration: none;"
},
"children": [
{
"componentName": "Icon",
"props": {
"name": "IconPublicHome",
"style": "margin-top: 3px;"
},
"id": "63d45626"
},
{
"componentName": "Text",
"props": {
"text": "首页"
},
"id": "62664355"
}
],
"id": "57951e33"
},
{
"componentName": "RouterLink",
"props": {
"to": "",
"style": "display: inline-flex; gap: 8px; padding: 10px 20px; color: inherit; text-decoration: none;"
},
"children": [
{
"componentName": "Icon",
"props": {
"name": "IconTaskCooperation",
"style": "margin-top: 3px;"
},
"id": "4d442e54"
},
{
"componentName": "Text",
"props": {
"text": "介绍"
},
"id": "63544924"
}
],
"id": "5212526a"
},
{
"componentName": "RouterLink",
"props": {
"to": "",
"style": "display: inline-flex; gap: 8px; padding: 10px 20px; color: inherit; text-decoration: none;"
},
"children": [
{
"componentName": "Icon",
"props": {
"name": "IconText",
"style": "margin-top: 3px;"
},
"id": "d21c6a33"
},
{
"componentName": "Text",
"props": {
"text": "文档"
},
"id": "32474d6e"
}
],
"id": "2b3b6655"
}
],
"id": "43553314"
}
],
"id": "f4343566"
},
{
"componentName": "div",
"props": {
"style": "padding: 8px 12px; border-right: 1px solid #0003;",
"className": "component-base-style"
},
"children": [
{
"componentName": "RouterLink",
"props": {
"to": "",
"style": "display: flex; gap: 8px; padding: 10px 20px; color: inherit; text-decoration: none;"
},
"children": [
{
"componentName": "Icon",
"props": {
"name": "IconPublicHome",
"style": "margin-top: 3px;"
},
"id": "2644a122"
},
{
"componentName": "Text",
"props": {
"text": "首页"
},
"id": "b1c5ea66"
}
],
"id": "3552b433"
},
{
"componentName": "RouterLink",
"props": {
"to": "",
"style": "display: flex; gap: 8px; padding: 10px 20px; color: inherit; text-decoration: none;"
},
"children": [
{
"componentName": "Icon",
"props": {
"name": "IconTaskCooperation",
"style": "margin-top: 3px;"
},
"id": "32696f11"
},
{
"componentName": "Text",
"props": {
"text": "介绍"
},
"id": "16621635"
}
],
"id": "3ab451b5"
},
{
"componentName": "RouterLink",
"props": {
"to": "",
"style": "display: flex; gap: 8px; padding: 10px 20px; color: inherit; text-decoration: none;"
},
"children": [
{
"componentName": "Icon",
"props": {
"name": "IconText",
"style": "margin-top: 3px;"
},
"id": "53c63452"
},
{
"componentName": "Text",
"props": {
"text": "文档"
},
"id": "5213222f"
}
],
"id": "16223245"
},
{
"componentName": "RouterLink",
"props": {
"to": "",
"style": "display: flex; gap: 8px; padding: 10px 20px; color: inherit; text-decoration: none;"
},
"children": [
{
"componentName": "Icon",
"props": {
"name": "IconText",
"style": "margin-top: 3px;"
},
"id": "43432e66"
},
{
"componentName": "Text",
"props": {
"text": "文档"
},
"id": "63275465"
}
],
"id": "e4434442"
},
{
"componentName": "RouterLink",
"props": {
"to": "",
"style": "display: flex; gap: 8px; padding: 10px 20px; color: inherit; text-decoration: none;"
},
"children": [
{
"componentName": "Icon",
"props": {
"name": "IconText",
"style": "margin-top: 3px;"
},
"id": "63225541"
},
{
"componentName": "Text",
"props": {
"text": "文档"
},
"id": "2b3934c6"
}
],
"id": "65a1634c"
}
],
"id": "6536546f"
},
{
"componentName": "Collection",
"props": {
"className": "component-base-style"
},
"children": [],
"id": "43166746"
}
],
"id": "23624f49"
},
{
"componentName": "div",
"props": {
"className": "component-base-style"
},
"children": [
{
"componentName": "Text",
"props": {
"style": "display: inline-block;",
"className": "component-base-style text-qovif",
"text": "表单类型"
},
"children": [],
"id": "26542636"
},
{
"componentName": "TinySelect",
"props": {
"modelValue": "",
"placeholder": "请选择",
"options": [
{
"value": "1",
"label": "黄金糕"
},
{
"value": "2",
"label": "双皮奶"
}
],
"className": "component-base-style"
},
"children": [],
"id": "23244363"
},
{
"componentName": "TinySwitch",
"props": {
"modelValue": "",
"className": "component-base-style"
},
"children": [],
"id": "45f27546"
},
{
"componentName": "TinyCheckboxGroup",
"props": {
"modelValue": [
"name1",
"name2"
],
"type": "checkbox",
"options": [
{
"text": "复选框1",
"label": "name1"
},
{
"text": "复选框2",
"label": "name2"
},
{
"text": "复选框3",
"label": "name3"
}
],
"className": "component-base-style"
},
"children": [],
"id": "45425331"
},
{
"componentName": "TinyCheckboxGroup",
"props": {
"modelValue": [],
"className": "component-base-style"
},
"children": [
{
"componentName": "TinyCheckboxButton",
"children": [
{
"componentName": "div",
"id": "515223e4"
}
],
"id": "21465323"
}
],
"id": "53433672"
},
{
"componentName": "TinyInput",
"props": {
"placeholder": "请输入",
"modelValue": "",
"className": "component-base-style"
},
"children": [],
"id": "545f4433"
},
{
"componentName": "TinyRadio",
"props": {
"label": "1",
"text": "单选文本",
"className": "component-base-style"
},
"children": [],
"id": "26535652"
},
{
"componentName": "TinyCheckbox",
"props": {
"text": "复选框文案",
"className": "component-base-style"
},
"children": [],
"id": "3553f455"
},
{
"componentName": "TinyDatePicker",
"props": {
"placeholder": "请输入",
"modelValue": "",
"className": "component-base-style"
},
"children": [],
"id": "33be5551"
},
{
"componentName": "TinyNumeric",
"props": {
"allow-empty": true,
"placeholder": "请输入",
"controlsPosition": "right",
"step": 1,
"className": "component-base-style"
},
"children": [],
"id": "1632551b"
},
{
"componentName": "TinyForm",
"props": {
"labelWidth": "80px",
"labelPosition": "top",
"className": "component-base-style"
},
"children": [
{
"componentName": "TinyFormItem",
"props": {
"label": "人员"
},
"children": [
{
"componentName": "TinyInput",
"props": {
"placeholder": "请输入",
"modelValue": ""
},
"id": "28b54342"
}
],
"id": "42a3a256"
},
{
"componentName": "TinyFormItem",
"props": {
"label": "密码"
},
"children": [
{
"componentName": "TinyInput",
"props": {
"placeholder": "请输入",
"modelValue": "",
"type": "password"
},
"id": "34e56631"
}
],
"id": "6f5332f6"
},
{
"componentName": "TinyFormItem",
"props": {
"label": ""
},
"children": [
{
"componentName": "TinyButton",
"props": {
"text": "提交",
"type": "primary",
"style": "margin-right: 10px"
},
"id": "655435a5"
},
{
"componentName": "TinyButton",
"props": {
"text": "重置",
"type": "primary"
},
"id": "26553353"
}
],
"id": "1524561e"
}
],
"id": "22246525"
}
],
"id": "62246843"
},
{
"componentName": "div",
"props": {
"className": "component-base-style"
},
"children": [
{
"componentName": "Text",
"props": {
"style": "display: inline-block;",
"className": "component-base-style text-gojtr",
"text": "表格类型"
},
"children": [],
"id": "65111b51"
},
{
"componentName": "TinyGrid",
"props": {
"editConfig": {
"trigger": "click",
"mode": "cell",
"showStatus": true
},
"columns": [
{
"type": "index",
"width": 60
},
{
"type": "selection",
"width": 60
},
{
"field": "employees",
"title": "员工数"
},
{
"field": "created_date",
"title": "创建日期"
},
{
"field": "city",
"title": "城市"
}
],
"data": [
{
"id": "1",
"name": "GFD科技有限公司",
"city": "福州",
"employees": 800,
"created_date": "2014-04-30 00:56:00",
"boole": false
},
{
"id": "2",
"name": "WWW科技有限公司",
"city": "深圳",
"employees": 300,
"created_date": "2016-07-08 12:36:22",
"boole": true
}
],
"className": "component-base-style"
},
"children": [],
"id": "25526464"
},
{
"componentName": "TinyPager",
"props": {
"layout": "total, sizes, prev, pager, next",
"total": 100,
"pageSize": 10,
"currentPage": 1,
"className": "component-base-style"
},
"children": [],
"id": "955945af"
},
{
"componentName": "Text",
"props": {
"style": "display: inline-block;",
"className": "component-base-style text-qmwuv",
"text": "表格带插槽的选中 hover 场景"
},
"children": [],
"id": "2433a464"
},
{
"componentName": "TinyGrid",
"props": {
"editConfig": {
"trigger": "click",
"mode": "cell",
"showStatus": true
},
"columns": [
{
"type": "index",
"width": 60
},
{
"type": "selection",
"width": 60
},
{
"field": "employees",
"title": "员工数"
},
{
"field": "created_date",
"title": "创建日期"
},
{
"field": "city",
"title": "城市",
"slots": {
"default": {
"type": "JSSlot",
"value": [
{
"componentName": "div",
"id": "68331136",
"children": [
{
"componentName": "Text",
"props": {
"style": "display: inline-block;",
"text": {
"type": "JSExpression",
"value": "row.name"
},
"className": "component-base-style"
},
"children": [],
"id": "36232312"
}
]
}
],
"params": [
"row"
]
}
}
}
],
"data": [
{
"id": "1",
"name": "GFD科技有限公司",
"city": "福州",
"employees": 800,
"created_date": "2014-04-30 00:56:00",
"boole": false
},
{
"id": "2",
"name": "WWW科技有限公司",
"city": "深圳",
"employees": 300,
"created_date": "2016-07-08 12:36:22",
"boole": true
}
],
"className": "component-base-style"
},
"children": [],
"id": "54b61743"
}
],
"id": "26632543"
},
{
"componentName": "div",
"props": {
"className": "component-base-style"
},
"children": [
{
"componentName": "Text",
"props": {
"className": "component-base-style text-nqswn",
"text": "数据展示类"
},
"children": [],
"id": "436c32c3"
},
{
"componentName": "TinyPopover",
"props": {
"width": 200,
"title": "弹框标题",
"trigger": "manual",
"modelValue": true,
"className": "component-base-style"
},
"children": [
{
"componentName": "Template",
"props": {
"slot": "reference"
},
"children": [
{
"componentName": "div",
"props": {
"placeholder": "触发源"
},
"id": "54c45142"
}
],
"id": "6334e3e3"
},
{
"componentName": "Template",
"props": {
"slot": "default"
},
"children": [
{
"componentName": "div",
"props": {
"placeholder": "提示内容"
},
"id": "21536213"
}
],
"id": "e6834224"
}
],
"id": "44233c63"
},
{
"componentName": "TinyTooltip",
"props": {
"content": "Top Left 提示文字",
"placement": "top-start",
"manual": true,
"modelValue": true,
"className": "component-base-style"
},
"children": [
{
"componentName": "span",
"children": [
{
"componentName": "div",
"props": {},
"id": "3552c642"
}
],
"id": "437f2356"
},
{
"componentName": "Template",
"props": {
"slot": "content"
},
"children": [
{
"componentName": "span",
"children": [
{
"componentName": "div",
"props": {
"placeholder": "提示内容"
},
"id": "22232622"
}
],
"id": "14435544"
}
],
"id": "26346535"
}
],
"id": "232511b7"
},
{
"componentName": "TinyTree",
"props": {
"data": [
{
"label": "一级 1",
"children": [
{
"label": "二级 1-1",
"children": [
{
"label": "三级 1-1-1"
}
]
}
]
},
{
"label": "一级 2",
"children": [
{
"label": "二级 2-1",
"children": [
{
"label": "三级 2-1-1"
}
]
},
{
"label": "二级 2-2",
"children": [
{
"label": "三级 2-2-1"
}
]
}
]
}
],
"className": "component-base-style"
},
"children": [],
"id": "62128352"
},
{
"componentName": "TinyPopeditor",
"props": {
"modelValue": "",
"placeholder": "请选择",
"gridOp": {
"columns": [
{
"field": "id",
"title": "ID",
"width": 40
},
{
"field": "name",
"title": "名称",
"showOverflow": "tooltip"
},
{
"field": "province",
"title": "省份",
"width": 80
},
{
"field": "city",
"title": "城市",
"width": 80
}
],
"data": [
{
"id": "1",
"name": "GFD科技有限公司GFD科技有限公司GFD科技有限公司GFD科技有限公司GFD科技有限公司GFD科技有限公司GFD科技有限公司",
"city": "福州",
"province": "福建"
},
{
"id": "2",
"name": "WWW科技有限公司",
"city": "深圳",
"province": "广东"
},
{
"id": "3",
"name": "RFV有限责任公司",
"city": "中山",
"province": "广东"
},
{
"id": "4",
"name": "TGB科技有限公司",
"city": "龙岩",
"province": "福建"
},
{
"id": "5",
"name": "YHN科技有限公司",
"city": "韶关",
"province": "广东"
},
{
"id": "6",
"name": "WSX科技有限公司",
"city": "黄冈",
"province": "武汉"
}
]
},
"className": "component-base-style"
},
"children": [],
"id": "a423544a"
},
{
"componentName": "TinyDialogBox",
"props": {
"visible": false,
"show-close": true,
"title": "dialogBox title",
"className": "component-base-style"
},
"children": [
{
"componentName": "div",
"id": "632a6586"
}
],
"id": "1b222642"
},
{
"componentName": "TinyCollapse",
"props": {
"modelValue": "collapse1",
"className": "component-base-style"
},
"children": [
{
"componentName": "TinyCollapseItem",
"props": {
"name": "collapse1",
"title": "折叠项1"
},
"children": [
{
"componentName": "div",
"id": "35252c26"
}
],
"id": "3334c336"
},
{
"componentName": "TinyCollapseItem",
"props": {
"name": "collapse2",
"title": "折叠项2"
},
"children": [
{
"componentName": "div",
"id": "3c323c55"
}
],
"id": "32d5424c"
},
{
"componentName": "TinyCollapseItem",
"props": {
"name": "collapse3",
"title": "折叠项3"
},
"children": [
{
"componentName": "div",
"id": "55353855"
}
],
"id": "45226235"
}
],
"id": "343143e6"
},
{
"componentName": "TinyCarousel",
"props": {
"height": "180px",
"className": "component-base-style"
},
"children": [
{
"componentName": "TinyCarouselItem",
"props": {
"title": "carousel-item-a"
},
"children": [
{
"componentName": "div",
"props": {
"style": "margin:10px 0 0 30px"
},
"id": "231243fc"
}
],
"id": "364d24b3"
},
{
"componentName": "TinyCarouselItem",
"props": {
"title": "carousel-item-b"
},
"children": [
{
"componentName": "div",
"props": {
"style": "margin:10px 0 0 30px"
},
"id": "65314333"
}
],
"id": "44332a3f"
}
],
"id": "6525553b"
}
],
"id": "42464556"
},
{
"componentName": "div",
"props": {
"className": "component-base-style"
},
"children": [
{
"componentName": "Text",
"props": {
"style": "display: inline-block;",
"className": "component-base-style text-flgjp",
"text": "导航类型"
},
"children": [],
"id": "541d5253"
},
{
"componentName": "TinyBreadcrumb",
"props": {
"options": [
{
"to": "{ path: '/' }",
"label": "首页"
},
{
"to": "{ path: '/breadcrumb' }",
"label": "产品"
},
{
"replace": "true",
"label": "软件"
}
],
"className": "component-base-style"
},
"children": [],
"id": "723e21a2"
},
{
"componentName": "TinyTabs",
"props": {
"modelValue": "first",
"className": "component-base-style"
},
"children": [
{
"componentName": "TinyTabItem",
"props": {
"title": "标签页1",
"name": "first"
},
"children": [
{
"componentName": "div",
"props": {
"style": "margin:10px 0 0 30px"
},
"id": "35564654"
}
],
"id": "33726322"
},
{
"componentName": "TinyTabItem",
"props": {
"title": "标签页2",
"name": "second"
},
"children": [
{
"componentName": "div",
"props": {
"style": "margin:10px 0 0 30px"
},
"id": "914d9243"
}
],
"id": "16e43254"
}
],
"id": "22539326"
},
{
"componentName": "TinyTimeLine",
"props": {
"active": "2",
"data": [
{
"name": "已下单"
},
{
"name": "运输中"
},
{
"name": "已签收"
}
],
"className": "component-base-style"
},
"children": [],
"id": "42376355"
}
],
"id": "35275312"
},
{
"componentName": "div",
"props": {
"className": "component-base-style"
},
"children": [
{
"componentName": "Text",
"props": {
"className": "component-base-style text-jvlui",
"text": "Element Plus组件"
},
"children": [],
"id": "15b6521c"
},
{
"componentName": "ElForm",
"children": [
{
"componentName": "ElFormItem",
"props": {
"label": "账号",
"prop": "account"
},
"children": [
{
"componentName": "ElInput",
"props": {
"modelValue": "",
"placeholder": "请输入账号"
},
"id": "56463115"
}
],
"id": "35331456"
},
{
"componentName": "ElFormItem",
"props": {
"label": "密码",
"prop": "password"
},
"children": [
{
"componentName": "ElInput",
"props": {
"modelValue": "",
"placeholder": "请输入密码",
"type": "password"
},
"id": "645561f2"
}
],
"id": "64543742"
},
{
"componentName": "ElFormItem",
"props": {},
"children": [
{
"componentName": "ElButton",
"props": {
"type": "primary",
"style": "margin-right: 10px"
},
"children": [
{
"componentName": "Text",
"props": {
"text": "提交"
},
"id": "a4547362"
}
],
"id": "24663532"
},
{
"componentName": "ElButton",
"props": {
"type": "primary"
},
"children": [
{
"componentName": "Text",
"props": {
"text": "重置"
},
"id": "3c235664"
}
],
"id": "1223c536"
}
],
"id": "54352515"
}
],
"props": {
"className": "component-base-style"
},
"id": "66227565"
},
{
"componentName": "ElButton",
"children": [
{
"componentName": "Text",
"props": {
"text": "按钮文本"
},
"id": "d3542248"
}
],
"props": {
"className": "component-base-style"
},
"id": "3522322c"
},
{
"componentName": "ElTable",
"props": {
"data": [
{
"date": "2016-05-03",
"name": "Tom",
"address": "No. 189, Grove St, Los Angeles"
},
{
"date": "2016-05-02",
"name": "Tom",
"address": "No. 189, Grove St, Los Angeles"
},
{
"date": "2016-05-04",
"name": "Tom",
"address": "No. 189, Grove St, Los Angeles"
},
{
"date": "2016-05-01",
"name": "Tom",
"address": "No. 189, Grove St, Los Angeles"
}
],
"columns": [
{
"type": "index"
},
{
"label": "Date",
"prop": "date"
},
{
"label": "Name",
"prop": "name"
},
{
"label": "Address",
"prop": "address"
}
],
"className": "component-base-style"
},
"children": [
{
"componentName": "ElTableColumn",
"props": {
"type": "index"
},
"id": "d26d2655"
},
{
"componentName": "ElTableColumn",
"props": {
"label": "Date",
"prop": "date"
},
"id": "524431a5"
},
{
"componentName": "ElTableColumn",
"props": {
"label": "Name",
"prop": "name"
},
"id": "37355322"
},
{
"componentName": "ElTableColumn",
"props": {
"label": "Address",
"prop": "address"
},
"id": "36543353"
}
],
"id": "f7256395"
},
{
"componentName": "ElInput",
"props": {
"className": "component-base-style"
},
"children": [],
"id": "3a314542"
}
],
"id": "6445193f"
}
],
"dataSource": {
"list": []
},
"state": {},
"methods": {},
"utils": [],
"bridge": [],
"inputs": [],
"outputs": [],
"fileName": "TestSelect",
"id": "body"
}
Summary by CodeRabbit
-
New Features
- Added a new "Date Picker" component to the component library, supporting a wide range of configuration options and events.
- Introduced enhanced selection and hover interaction capabilities for canvas nodes, including new visual components for hover and insertion lines.
- Enabled both single and multiple node selection with improved visual feedback and keyboard/clipboard support.
- Added configuration options for interaction modes.
-
Refactor
- Replaced previous multi-selection logic with a unified selection state management system.
- Centralized hover and selection state management using new composable hooks.
- Simplified and improved event handling and state updates across the canvas and related components.
- Streamlined hover state handling by removing inactive hover states and focusing on active hover states.
- Updated internal rectangle calculations to use nested properties for better clarity.
- Introduced a Vue plugin to link DOM elements with Vue component instances for improved interaction.
-
Bug Fixes
- Improved reliability and consistency of node selection, hover, and deletion behaviors.
-
Chores
- Updated internal configuration and code structure to support new interaction modes and maintainability.
Walkthrough
This update introduces a comprehensive refactor of the canvas container's node selection and hover logic, centralizing state management through new composable hooks (useHoverNode and useSelectNode) with support for both Vue and HTML interaction modes. Multi-selection state management is removed and replaced with a unified selection state. Several new components are added for improved hover and insertion line visualization. The event handling and keyboard logic are updated to use the new composables. The bundle configuration is extended with a new date picker component, and the engine configuration is updated for Vue mode. Supporting utilities for Vue instance rectangle calculation and DOM-to-component mapping are introduced, and the overall API surface is streamlined for clarity and modularity.
Changes
| File(s) | Change Summary |
|---|---|
| designer-demo/engine.config.js | Added dslMode and selectMode properties set to 'vue' in the exported config object. Minor formatting adjustment. |
| designer-demo/public/mock/bundle.json | Added a new date picker component (ElDatePicker) with full configuration and schema. Included in the "Element Plus组件" snippet group. |
| packages/canvas/container/index.ts | Replaced useMultiSelect with useSelectNode in the exported API. |
| packages/canvas/container/src/CanvasContainer.vue | Refactored to use new composables for hover and selection state. Replaced multi-selection logic. Updated event handling. Added CanvasHover and CanvasInsertLine components. |
| packages/canvas/container/src/components/CanvasAction.vue | Updated to use selectState.rect for rectangle data. Removed hover and line state props and related logic. Switched to useSelectNode for selection. |
| packages/canvas/container/src/components/CanvasDivider.vue packages/canvas/container/src/components/CanvasResizeBorder.vue |
Changed access to rectangle properties to use selectState.rect instead of root selectState. |
| packages/canvas/container/src/components/CanvasHover.vue | New: Added CanvasHover component for hover rectangle visualization and selection. |
| packages/canvas/container/src/components/CanvasInsertLine.vue | New: Added CanvasInsertLine component for visualizing insertion lines and slot selection. |
| packages/canvas/container/src/components/CanvasRouterJumper.vue packages/canvas/container/src/components/CanvasViewerSwitcher.vue |
Removed inactiveHoverState prop and logic. Simplified hover state handling. |
| packages/canvas/container/src/composables/useMultiSelect.ts | Deleted: Removed multi-selection composable and related interface. |
| packages/canvas/container/src/container.ts | Refactored to use new composables for hover and selection. Removed direct state management. Updated API to wrap new composable methods. |
| packages/canvas/container/src/interactions/common.ts | New: Added shared interfaces/utilities for hover/selection state and DOM traversal. |
| packages/canvas/container/src/interactions/index.ts | New: Added dynamic interaction hook selection based on engine config (vue or html). |
| packages/canvas/container/src/interactions/vue-interactions.ts | New: Added Vue-based hover and selection logic using Vue instance traversal. |
| packages/canvas/container/src/interactions/html-interactions.ts | New: Added HTML-based hover and selection logic using DOM attributes. |
| packages/canvas/container/src/interactions/vue-rect.ts | New: Added utilities for calculating Vue instance bounding rectangles. |
| packages/canvas/container/src/keyboard.ts | Refactored keyboard and clipboard logic to use new composables for selection and hover. Removed multi-selection logic. |
| packages/canvas/render/src/render.ts | Removed onMouseover event handler assignment in design mode from getBindProps. |
| packages/canvas/render/src/runner.ts | Added a Vue plugin to link DOM nodes to Vue component instances. Applied the plugin in app creation. |
| packages/plugins/tree/src/Main.vue | Switched to new selection composable. Updated selected IDs logic. Removed call to selectNode after drop. |
| packages/toolbars/save/src/js/index.ts | Replaced selectNode(null) with clearSelect?.() after saving. |
Sequence Diagram(s)
sequenceDiagram
participant User
participant CanvasContainer
participant useHoverNode
participant useSelectNode
participant CanvasHover
participant CanvasInsertLine
User->>CanvasContainer: Mouse event (mousedown/mouseover/contextmenu)
CanvasContainer->>useSelectNode: updateSelectedNode (for selection)
CanvasContainer->>useHoverNode: updateHoverNode (for hover)
CanvasContainer->>CanvasHover: Render hover rectangle (with curHoverState)
CanvasContainer->>CanvasInsertLine: Render insert line (with lineState)
useSelectNode-->>CanvasContainer: Update selectState
useHoverNode-->>CanvasContainer: Update curHoverState
CanvasContainer-->>User: Visual feedback (selection, hover, insert line)
sequenceDiagram
participant Keyboard
participant useSelectNode
participant useHoverNode
participant CanvasContainer
Keyboard->>useSelectNode: selectNodeById / clearSelect (arrow/delete)
Keyboard->>useHoverNode: clearHover (on delete)
useSelectNode-->>CanvasContainer: Update selectState
useHoverNode-->>CanvasContainer: Update curHoverState
CanvasContainer-->>Keyboard: Update UI
Poem
🐇✨
In the garden of canvas, where nodes gently gleam,
Selection and hover now flow as a team.
No more tangled states, just composables bright,
With Vue or with HTML, all works just right.
New rectangles and lines, a hover that glows—
This bunny hops forward, where clarity grows!
🌱🎨
✨ Finishing Touches
- [ ] 📝 Generate Docstrings
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
🪧 Tips
Chat
There are 3 ways to chat with CodeRabbit:
- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
I pushed a fix in commit <commit_id>, please review it.Generate unit testing code for this file.Open a follow-up GitHub issue for this discussion.
- Files and specific lines of code (under the "Files changed" tab): Tag
@coderabbitaiin a new review comment at the desired location with your query. Examples:@coderabbitai generate unit testing code for this file.@coderabbitai modularize this function.
- PR comments: Tag
@coderabbitaiin a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.@coderabbitai read src/utils.ts and generate unit testing code.@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.@coderabbitai help me debug CodeRabbit configuration file.
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.
CodeRabbit Commands (Invoked using PR comments)
@coderabbitai pauseto pause the reviews on a PR.@coderabbitai resumeto resume the paused reviews.@coderabbitai reviewto trigger an incremental review. This is useful when automatic reviews are disabled for the repository.@coderabbitai full reviewto do a full review from scratch and review all the files again.@coderabbitai summaryto regenerate the summary of the PR.@coderabbitai generate docstringsto generate docstrings for this PR.@coderabbitai generate sequence diagramto generate a sequence diagram of the changes in this PR.@coderabbitai resolveresolve all the CodeRabbit review comments.@coderabbitai configurationto show the current CodeRabbit configuration for the repository.@coderabbitai helpto get help.
Other keywords and placeholders
- Add
@coderabbitai ignoreanywhere in the PR description to prevent this PR from being reviewed. - Add
@coderabbitai summaryto generate the high-level summary at a specific location in the PR description. - Add
@coderabbitaianywhere in the PR title to generate the title automatically.
CodeRabbit Configuration File (.coderabbit.yaml)
- You can programmatically configure CodeRabbit by adding a
.coderabbit.yamlfile to the root of your repository. - Please see the configuration documentation for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation:
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
Documentation and Community
- Visit our Documentation for detailed information on how to use CodeRabbit.
- Join our Discord Community to get help, request features, and share feedback.
- Follow us on X/Twitter for updates and announcements.