diff --git a/src/views/bigscreenDesigner/designer/index.vue b/src/views/bigscreenDesigner/designer/index.vue index 15b6350..f819709 100644 --- a/src/views/bigscreenDesigner/designer/index.vue +++ b/src/views/bigscreenDesigner/designer/index.vue @@ -320,6 +320,26 @@ @handleCollapse="handleCollapse" @onChanged="(val) => widgetValueChanged('setup', val)" /> + +
+ + + 删除该子组件 + + +
it.name === "tabsList"); + if (tabsItem && Array.isArray(tabsItem.value)) { + tabsList = tabsItem.value; + } + } + tabsList = tabsList || []; + + const targetTab = tabsList[tabIndex]; + if (!targetTab || !targetTab.children || !targetTab.children[childIndex]) { + console.warn("Tabs 子组件未找到:", { rootIndex, tabIndex, childIndex, tabsList }); + return; + } + + let childWidget = targetTab.children[childIndex]; + + // 2. 确保子组件有 options(老数据里可能只有 type + value) + if (!childWidget.options) { + try { + const tool = getToolByCode(childWidget.type); + if (tool && tool.options) { + childWidget.options = this.deepClone(tool.options); + } else { + console.warn("未找到子组件 options 配置:", childWidget.type); + } + } catch (e) { + console.error("为子组件补全 options 失败:", e); + } + } + + // 3. 用实际 position 回填到 options.position 中,保证坐标表单显示正确 + if (childWidget.options && Array.isArray(childWidget.options.position)) { + const pos = (childWidget.value && childWidget.value.position) ? childWidget.value.position : {}; + childWidget.options.position.forEach((el) => { + if (pos.hasOwnProperty(el.name)) { + el.value = pos[el.name]; + } + }); + } + + // 4. 记录当前选中的内部子组件路径 & 切换右侧到子组件配置 this.innerWidgetSelected = { rootWidgetIndex: rootIndex, tabIndex, @@ -812,51 +878,53 @@ export default { this.widgetIndex = rootIndex; this.activeName = "first"; - // 找到真正的子组件对象 - const tabsList = (rootWidget.value.setup && rootWidget.value.setup.tabsList) || []; + this.widgetOptions = this.deepClone(childWidget.options || {}); + }, + /** + * 删除当前选中的 Tabs 内部子组件 + */ + deleteInnerWidget() { + if (!this.innerWidgetSelected) return; + const { rootWidgetIndex, tabIndex, childIndex } = this.innerWidgetSelected; + const rootWidget = this.widgets[rootWidgetIndex]; + if (!rootWidget || !rootWidget.value) return; + + // 永远只操作“真实绑定”的数据:rootWidget.value.setup.tabsList + if (!rootWidget.value.setup) { + this.$set(rootWidget.value, "setup", {}); + } + if (!Array.isArray(rootWidget.value.setup.tabsList)) { + this.$set(rootWidget.value.setup, "tabsList", []); + } + const tabsList = rootWidget.value.setup.tabsList; + const targetTab = tabsList[tabIndex]; - if (!targetTab || !targetTab.children || !targetTab.children[childIndex]) { - return; - } - const childWidget = widget || targetTab.children[childIndex]; + if (!targetTab || !targetTab.children || !targetTab.children.length) return; - // 用实际 position 值回填到 options.position 中,保证坐标表单显示正确 - if (childWidget.options && Array.isArray(childWidget.options.position)) { - const pos = childWidget.value && childWidget.value.position ? childWidget.value.position : {}; - childWidget.options.position.forEach((el) => { - if (pos.hasOwnProperty(el.name)) { - el.value = pos[el.name]; - } - }); - } + targetTab.children.splice(childIndex, 1); - this.widgetOptions = this.deepClone(childWidget.options); + // 同步回 rootWidget 的 setup / options + rootWidget.value.setup.tabsList = tabsList; + const setupArr = rootWidget.options && rootWidget.options.setup ? rootWidget.options.setup : []; + setupArr.forEach((item) => { + if (item.name === "tabsList") { + item.value = tabsList; + } + }); + + // 清空内部选中状态,并回到 Tabs 本身配置 + this.innerWidgetSelected = null; + this.widgetIndex = rootWidgetIndex; + this.widgetOptions = this.deepClone(rootWidget.options || {}); + this.activeName = "first"; }, widgetsClick(event,index) { console.log("widgetsClick"); - // 如果点击发生在 Tabs 组件内部的子组件上,优先选中内部组件 - if (event && event.target && event.target.closest) { - const tabChildWrapper = event.target.closest(".tab-child-wrapper"); - const tabsRootEl = event.target.closest(".widget-tabs"); - if (tabChildWrapper && tabsRootEl) { - const tabIndexAttr = tabChildWrapper.getAttribute("data-tab-index"); - const childIndexAttr = tabChildWrapper.getAttribute("data-child-index"); - const tabIndex = Number(tabIndexAttr); - const childIndex = Number(childIndexAttr); - if (!isNaN(tabIndex) && !isNaN(childIndex)) { - this.setOptionsOnClickInnerWidget({ - rootWidgetIndex: index, - tabIndex, - childIndex, - }); - return; - } - } - // 如果只是点击在 Tabs 内容区的空白处,则交给 Tabs 自己内部处理,避免整个 Tabs 组件被选中 - const tabContentEl = event.target.closest(".tab-content"); - if (tabContentEl && tabsRootEl) { - return; - } + // 如果是 Tabs 组件,内部点击由 widgetTabs 自己处理,这里只负责选中 Tabs 整体 + const rootWidget = this.widgets[index]; + if (rootWidget && rootWidget.type === "widget-tabs") { + this.widgetsClickFocus(index); + return; } //判断是否按住了Ctrl按钮,表示Ctrl多选 let _this = this; diff --git a/src/views/bigscreenDesigner/designer/widget/form/widgetTabs.vue b/src/views/bigscreenDesigner/designer/widget/form/widgetTabs.vue index b23f4e8..d254068 100644 --- a/src/views/bigscreenDesigner/designer/widget/form/widgetTabs.vue +++ b/src/views/bigscreenDesigner/designer/widget/form/widgetTabs.vue @@ -29,6 +29,7 @@ class="tab-content" data-tab-content :style="contentStyle" + @click.capture="onTabContentClick($event, index)" @drop="handleTabDrop($event, tab, index)" @dragover.capture.prevent="handleTabDragOver($event)" @dragover.prevent="handleTabDragOver($event)" @@ -42,8 +43,8 @@ :key="childWidget.value.widgetId || `child-${index}-${childIndex}`" class="tab-child-draggable" :style="{ - left: (childWidget.value.position && typeof childWidget.value.position.left === 'number') ? childWidget.value.position.left + 'px' : '0px', - top: (childWidget.value.position && typeof childWidget.value.position.top === 'number') ? childWidget.value.position.top + 'px' : '0px', + marginLeft: (childWidget.value.position && typeof childWidget.value.position.left === 'number') ? childWidget.value.position.left + 'px' : '0px', + marginTop: (childWidget.value.position && typeof childWidget.value.position.top === 'number') ? childWidget.value.position.top + 'px' : '0px', width: (childWidget.value.position && childWidget.value.position.width) ? childWidget.value.position.width + 'px' : '100px', height: (childWidget.value.position && childWidget.value.position.height) ? childWidget.value.position.height + 'px' : '50px', }" @@ -52,9 +53,15 @@ class="tab-child-wrapper" :data-tab-index="index" :data-child-index="childIndex" - @mousedown.capture="onTabChildMouseDown($event, index, childIndex)" - @contextmenu.prevent.stop="handleChildWidgetRightClick($event, childIndex, index)" > + + + + - - × - @@ -310,7 +309,6 @@ export default { * 触发 Tabs 内部子组件选中事件,让设计器右侧展示该子组件的配置 */ emitChildActivated(tabIndex, childIndex) { - // 预览模式下不需要触发设计事件 if (this.ispreview) return; const currentTabsList = this.tabsList; const targetTab = currentTabsList[tabIndex]; @@ -323,6 +321,38 @@ export default { widget: childWidget, }); }, + /** + * tab 内容区域点击:单击子组件 => 选中子组件;双击空白 => 选中整个 Tabs + */ + onTabContentClick(evt, tabIndex) { + if (this.ispreview || !evt.target || !evt.target.closest) return; + // 1)如果点在某个子组件包装层上,优先选中该子组件 + const wrapper = evt.target.closest(".tab-child-wrapper"); + if (wrapper) { + const childIndexAttr = wrapper.getAttribute("data-child-index"); + const childIndex = Number(childIndexAttr); + if (!isNaN(childIndex)) { + this.emitChildActivated(tabIndex, childIndex); + } + evt.stopPropagation(); + return; + } + // 2)双击内容区空白,通知外层选中整个 Tabs + const contentEl = evt.currentTarget; + const hasChildWrapper = contentEl.querySelector(".tab-child-wrapper"); + if (!hasChildWrapper && evt.detail === 2) { + this.$emit("tabsContentDblClick", { tabIndex, event: evt }); + evt.stopPropagation(); + return; + } + }, + /** + * 点击左上角选择手柄:只做“选中内部子组件”的动作 + */ + onTabChildHandleDown(tabIndex, childIndex) { + if (this.ispreview) return; + this.emitChildActivated(tabIndex, childIndex); + }, setActiveTab() { const tabs = this.tabsList; if (tabs && tabs.length > 0) { @@ -445,24 +475,9 @@ export default { // 处理默认值 const widgetJsonValue = this.getWidgetConfigValue(widgetJson); - - // 设置位置(相对于tab内容区域) - widgetJsonValue.value.position.left = x - widgetJsonValue.value.position.width / 2; - widgetJsonValue.value.position.top = y - widgetJsonValue.value.position.height / 2; - - // 确保位置在容器内 - if (widgetJsonValue.value.position.left < 0) { - widgetJsonValue.value.position.left = 0; - } - if (widgetJsonValue.value.position.top < 0) { - widgetJsonValue.value.position.top = 0; - } - if (widgetJsonValue.value.position.left + widgetJsonValue.value.position.width > this.optionsStyle.width) { - widgetJsonValue.value.position.left = this.optionsStyle.width - widgetJsonValue.value.position.width; - } - if (widgetJsonValue.value.position.top + widgetJsonValue.value.position.height > this.optionsStyle.height) { - widgetJsonValue.value.position.top = this.optionsStyle.height - widgetJsonValue.value.position.height; - } + // Tabs 内部子组件:默认边距统一从 0 开始,由右侧“坐标”面板控制间距 + widgetJsonValue.value.position.left = 0; + widgetJsonValue.value.position.top = 0; // 生成唯一ID const uuid = Number(Math.random().toString().substr(2)).toString(36); @@ -537,14 +552,9 @@ export default { options: this.deepClone(tool.options), }; const widgetJsonValue = this.getWidgetConfigValue(widgetJson); - widgetJsonValue.value.position.left = Math.max(0, x - widgetJsonValue.value.position.width / 2); - widgetJsonValue.value.position.top = Math.max(0, y - widgetJsonValue.value.position.height / 2); - if (widgetJsonValue.value.position.left + widgetJsonValue.value.position.width > this.optionsStyle.width) { - widgetJsonValue.value.position.left = this.optionsStyle.width - widgetJsonValue.value.position.width; - } - if (widgetJsonValue.value.position.top + widgetJsonValue.value.position.height > this.optionsStyle.height) { - widgetJsonValue.value.position.top = this.optionsStyle.height - widgetJsonValue.value.position.height; - } + // 同样,外层 drop 到 Tabs 上时,内部子组件也从 0 边距起步 + widgetJsonValue.value.position.left = 0; + widgetJsonValue.value.position.top = 0; const uuid = Number(Math.random().toString().substr(2)).toString(36); widgetJsonValue.value.widgetId = uuid; widgetJsonValue.value.widgetCode = dragWidgetCode; @@ -611,16 +621,19 @@ export default { * 根节点捕获:仅当点击在标题栏时发出事件,让设计器拖动整块 Tabs(外层 avue-draggable 已禁用) */ onTabsRootCapture(evt) { - if (this.ispreview) return; - if (evt.target && evt.target.closest && evt.target.closest('.el-tabs__header')) { + if (this.ispreview || !evt.target || !evt.target.closest) return; + const headerEl = evt.target.closest(".el-tabs__header"); + if (headerEl) { + // 点击在 Tabs 头部:阻止事件冒泡到外层 draggable,并通知外层开始拖动整块 Tabs evt.stopPropagation(); - this.$emit('tabsHeaderMouseDown', evt); + this.$emit("tabsHeaderMouseDown", evt); } }, onTabChildMouseDown(event, tabIndex, childIndex) { if (event.target && event.target.closest && event.target.closest('.tab-child-delete')) { return; // 点在删除按钮上,不拦截,删除按钮的 @mousedown.stop 会阻止冒泡到外层 } + console.log('onTabChildMouseDown:', tabIndex, childIndex); event.preventDefault(); event.stopPropagation(); // 选中内部组件并通知外层暂时禁用拖拽 @@ -726,35 +739,6 @@ export default { // 通知外层 Widget:内部拖拽结束,恢复外层 avue-draggable this.$emit("innerDragEnd"); }, - handleChildWidgetRightClick(event, childIndex, tabIndex) { - // 处理子组件右键 - 支持直接删除 - event.stopPropagation(); - this.$confirm("确定删除该组件吗?", "提示", { - type: "warning", - confirmButtonText: "确定", - cancelButtonText: "取消", - }) - .then(() => { - this.removeChildWidget(tabIndex, childIndex); - }) - .catch(() => {}); - }, - /** - * 从指定 tab 中移除一个子组件 - */ - removeChildWidget(tabIndex, childIndex) { - const currentTabsList = this.tabsList; - const targetTab = currentTabsList[tabIndex]; - if (!targetTab || !targetTab.children) return; - targetTab.children.splice(childIndex, 1); - - this.optionsSetup.tabsList = currentTabsList; - if (!this.value.setup) { - this.$set(this.value, "setup", {}); - } - this.value.setup.tabsList = currentTabsList; - this.$emit("input", this.value); - }, // 将组件类型字符串转换为组件名称 getComponentName(type) { // 将 widget-text 转换为 widgetText @@ -808,30 +792,34 @@ export default { position: relative; } + .tab-child-select-handle { + position: absolute; + top: -6px; + right: -2px; + width: 18px; + height: 18px; + border-radius: 50%; + background: rgba(0, 0, 0, 0.45); + z-index: 30; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + } + + .tab-child-select-handle:hover { + background: rgba(64, 158, 255, 0.9); + } + + .tab-child-select-icon { + font-size: 14px; + color: #fff; + } + .tab-child-component { width: 100%; height: 100%; } - - .tab-child-delete { - position: absolute; - right: 2px; - top: 2px; - width: 14px; - height: 14px; - line-height: 14px; - text-align: center; - font-size: 12px; - border-radius: 50%; - background: rgba(0, 0, 0, 0.4); - color: #fff; - cursor: pointer; - z-index: 20; - } - - .tab-child-delete:hover { - background: rgba(255, 0, 0, 0.8); - } .tab-content-empty { width: 100%; diff --git a/src/views/bigscreenDesigner/designer/widget/widget.vue b/src/views/bigscreenDesigner/designer/widget/widget.vue index ea00ac4..e74d509 100644 --- a/src/views/bigscreenDesigner/designer/widget/widget.vue +++ b/src/views/bigscreenDesigner/designer/widget/widget.vue @@ -21,6 +21,7 @@ @innerDragStart="handleInnerDragStart" @innerDragEnd="handleInnerDragEnd" @tabsHeaderMouseDown="handleTabsHeaderMouseDown" + @tabsContentDblClick="handleTabsContentDblClick" /> @@ -178,10 +179,9 @@ export default { return this.value.position.zIndex || 1; }, widgetDisabled() { - // Tabs 必须禁用外层拖拽,内部子组件才能选中;Tabs 整体拖动改由标题栏单独处理 - if (this.type === 'widget-tabs') { - return true; - } + // 统一走一套逻辑: + // - 当内部子组件正在拖拽时禁用外层拖拽,避免误拖整块组件 + // - 其余情况按组件自身的 disabled 控制 return this.value.position.disabled || this.innerDragging || false; }, }, @@ -239,6 +239,7 @@ export default { * 接收 Tabs 内部子组件发出的激活事件,并转发给设计器主页面 */ handleChildActivated(payload) { + console.log('handleChildActivated in widget.vue:', payload); const info = Object.assign({}, payload || {}); if (info.rootWidgetIndex === undefined || info.rootWidgetIndex === null) { info.rootWidgetIndex = this.index; @@ -255,6 +256,11 @@ export default { handleTabsHeaderMouseDown(evt) { this.$emit('onTabsHeaderMouseDown', { event: evt, rootWidgetIndex: this.index }); }, + // Tabs 内容区双击空白时,也视为选中整个 Tabs + handleTabsContentDblClick(payload) { + if (!payload || !payload.event) return; + this.$emit('onTabsHeaderMouseDown', { event: payload.event, rootWidgetIndex: this.index }); + }, }, };