From ddb542d89b32cdf609cd2e5af622fc91edf72a99 Mon Sep 17 00:00:00 2001 From: Erin66 Date: Thu, 12 Feb 2026 22:30:38 +0800 Subject: [PATCH 1/2] =?UTF-8?q?tabs=E5=86=85=E9=83=A8=E8=99=9A=E7=BA=BF?= =?UTF-8?q?=E5=8C=BA=E5=9F=9F=E6=94=BE=E5=A4=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/bigscreenDesigner/designer/widget/form/widgetTabs.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/src/views/bigscreenDesigner/designer/widget/form/widgetTabs.vue b/src/views/bigscreenDesigner/designer/widget/form/widgetTabs.vue index d254068..9082c96 100644 --- a/src/views/bigscreenDesigner/designer/widget/form/widgetTabs.vue +++ b/src/views/bigscreenDesigner/designer/widget/form/widgetTabs.vue @@ -824,6 +824,7 @@ export default { .tab-content-empty { width: 100%; height: 100%; + min-height: 200px; display: flex; align-items: center; justify-content: center; -- 2.52.0.windows.1 From 5cce73902daec3340cb0bdbbdea9168403599c3c Mon Sep 17 00:00:00 2001 From: Erin66 Date: Fri, 13 Feb 2026 00:35:20 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E4=B8=8B=E9=92=BB=E9=85=8D=E7=BD=AE=20tabs?= =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../designer/components/drillDrownSetting.vue | 200 +++++++++++++++++- .../designer/widget/form/widgetTabs.vue | 11 +- 2 files changed, 203 insertions(+), 8 deletions(-) diff --git a/src/views/bigscreenDesigner/designer/components/drillDrownSetting.vue b/src/views/bigscreenDesigner/designer/components/drillDrownSetting.vue index ad3e135..aa2a816 100644 --- a/src/views/bigscreenDesigner/designer/components/drillDrownSetting.vue +++ b/src/views/bigscreenDesigner/designer/components/drillDrownSetting.vue @@ -27,7 +27,7 @@ v-for="(it, idx) in item.list" :key="idx" draggable="true" - @dragstart="dragStart(it.code)" + @dragstart="dragStart(it.code, $event)" @dragend="dragEnd()" >
@@ -290,6 +290,8 @@ :type="widget.type" :bigscreen="{ bigscreenWidth, bigscreenHeight }" @onActivated="setOptionsOnClickWidget" + @onChildActivated="setOptionsOnClickInnerWidget" + @onTabsHeaderMouseDown="onTabsHeaderMouseDown($event)" @contextmenu.prevent.native="rightClick($event, index)" @mousedown.prevent.native="widgetsClick($event, index)" @mouseup.prevent.native="grade = false" @@ -449,6 +451,8 @@ export default { rect : null, //框选矩形对象 openMulDrag: false, //批量拖拽开关 moveWidgets:{}, //记录移动的组件的起始left和top属性 + // 记录当前是否选中了 Tabs 内部子组件,如果为 null 则表示选中的是顶层组件或大屏 + innerWidgetSelected: null, }; }, computed: { @@ -988,9 +992,14 @@ export default { getPXUnderScale(px) { return this.bigscreenScaleInWorkbench * px; }, - dragStart(widgetCode) { + dragStart(widgetCode, evt) { this.dragWidgetCode = widgetCode; this.currentWidgetTotal = this.widgets.length; // 当前操作面板上有多少各组件 + // 通过 dataTransfer 传递组件 code,便于 Tabs 等嵌套容器在 drop 时读取(不依赖父组件链) + if (evt && evt.dataTransfer) { + evt.dataTransfer.setData("application/x-widget-code", widgetCode); + evt.dataTransfer.effectAllowed = "copy"; + } }, dragEnd() { /** @@ -1017,12 +1026,48 @@ export default { }); }, dragOver(evt) { + // 鼠标在 Tabs 标签内容区内时不处理,让 tab 内容区成为 drop 目标,避免工作台抢走 + if (evt.target && evt.target.closest && (evt.target.closest(".tab-content") || evt.target.closest("[data-tab-content]"))) { + return; + } evt.preventDefault(); evt.stopPropagation(); evt.dataTransfer.dropEffect = "copy"; }, // 拖动一个组件放到工作区中去,在拖动结束时,放到工作区对应的坐标点上去 widgetOnDragged(evt) { + // 若落在 Tabs 标签内容区内,由 widgetTabs 自己处理,不在此处添加到主画布 + if (evt.target && evt.target.closest && (evt.target.closest(".tab-content") || evt.target.closest("[data-tab-content]"))) { + return; + } + // 若落在 Tabs 组件的外层容器上(如 avue-draggable),委托给对应 Tabs 加入当前激活的 tab + const widgetsRef = this.$refs.widgets; + const widgetList = Array.isArray(widgetsRef) ? widgetsRef : (widgetsRef ? [widgetsRef] : []); + if (evt.target && widgetList.length) { + for (let i = 0; i < widgetList.length; i++) { + const w = widgetList[i]; + if (!w || !w.$el || !w.$el.contains(evt.target)) continue; + if (this.widgets[i] && this.widgets[i].type === "widget-tabs") { + const findTabs = (comp) => { + if (!comp) return null; + if (typeof comp.addWidgetFromDrop === "function") return comp; + if (comp.$children && comp.$children.length) { + for (let j = 0; j < comp.$children.length; j++) { + const found = findTabs(comp.$children[j]); + if (found) return found; + } + } + return null; + }; + const tabsComp = findTabs(w); + if (tabsComp) { + tabsComp.addWidgetFromDrop(evt); + return; + } + } + break; + } + } let widgetType = this.dragWidgetCode; // 获取结束坐标和列名 @@ -1058,6 +1103,7 @@ export default { options: tool.options, }; // 处理默认值 + console.log(widgetType) const widgetJsonValue = this.getWidgetConfigValue(widgetJson); widgetJsonValue.value.position.left = @@ -1138,6 +1184,118 @@ export default { }); this.widgetOptions = this.deepClone(this.widgets[obj.index]["options"]); }, + /** + * 选中 Tabs 组件内部的子组件,展示该子组件的右侧配置 + */ + setOptionsOnClickInnerWidget(payload) { + if (!payload) return; + console.log('setOptionsOnClickInnerWidget payload:', payload); + + const { rootWidgetIndex, tabIndex, childIndex } = payload; + const rootIndex = typeof rootWidgetIndex === "number" ? rootWidgetIndex : 0; + const rootWidget = this.widgets[rootIndex]; + if (!rootWidget || !rootWidget.value) return; + + // 1. 兼容两种存储位置:value.setup.tabsList / options.setup 里的 tabsList + const setupObj = rootWidget.value.setup || {}; + let tabsList = setupObj.tabsList; + if (!tabsList || !tabsList.length) { + const setupArr = rootWidget.options && rootWidget.options.setup ? rootWidget.options.setup : []; + const tabsItem = setupArr.find(it => 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, + childIndex, + }; + this.screenCode = ""; + this.widgetIndex = rootIndex; + this.activeName = "first"; + + this.widgetOptions = this.deepClone(childWidget.options || {}); + }, + /** + * Tabs 标题栏按下:启动整块 Tabs 的拖动(因 Tabs 外层 avue-draggable 已禁用) + */ + onTabsHeaderMouseDown(payload) { + if (!payload || !payload.event || typeof payload.rootWidgetIndex !== 'number') return; + const rootIndex = payload.rootWidgetIndex; + const widget = this.widgets[rootIndex]; + if (!widget || !widget.value || !widget.value.position) return; + // 选中 Tabs 组件本身,触发蓝色点框和右侧配置 + this.innerWidgetSelected = null; + this.widgetsClickFocus(rootIndex); + const evt = payload.event; + const workbenchRect = document.getElementById('workbench') && document.getElementById('workbench').getBoundingClientRect(); + if (!workbenchRect) return; + const scale = this.currentSizeRangeIndex === this.defaultSize.index + ? this.bigscreenScaleInWorkbench + : this.sizeRange[this.currentSizeRangeIndex] / 100; + const startX = evt.clientX; + const startY = evt.clientY; + const startLeft = widget.value.position.left; + const startTop = widget.value.position.top; + const onMove = (e) => { + const dx = (e.clientX - startX) / scale; + const dy = (e.clientY - startY) / scale; + let newLeft = startLeft + dx; + let newTop = startTop + dy; + if (newLeft < 0) newLeft = 0; + if (newTop < 0) newTop = 0; + if (newLeft + widget.value.position.width > this.bigscreenWidth) { + newLeft = this.bigscreenWidth - widget.value.position.width; + } + if (newTop + widget.value.position.height > this.bigscreenHeight) { + newTop = this.bigscreenHeight - widget.value.position.height; + } + this.$set(widget.value.position, 'left', newLeft); + this.$set(widget.value.position, 'top', newTop); + }; + const onUp = () => { + document.removeEventListener('mousemove', onMove); + document.removeEventListener('mouseup', onUp); + }; + document.addEventListener('mousemove', onMove); + document.addEventListener('mouseup', onUp); + }, widgetsClick(event,index) { console.log("widgetsClick"); //判断是否按住了Ctrl按钮,表示Ctrl多选 @@ -1237,10 +1395,40 @@ export default { console.log(newSetup); this.widgetOptions.setup = newSetup; } else { - for (let i = 0; i < this.widgets.length; i++) { - if (this.widgetIndex == i) { - this.widgets[i].value[key] = this.deepClone(val); - this.setDefaultValue(this.widgets[i].options[key], val); + // 如果当前选中的是 Tabs 内部子组件,优先更新内部子组件的配置 + if (this.innerWidgetSelected) { + const { rootWidgetIndex, tabIndex, childIndex } = this.innerWidgetSelected; + const rootWidget = this.widgets[rootWidgetIndex]; + if (rootWidget && rootWidget.value && rootWidget.value.setup) { + const tabsList = rootWidget.value.setup.tabsList || []; + const targetTab = tabsList[tabIndex]; + if (targetTab && targetTab.children && targetTab.children[childIndex]) { + const childWidget = targetTab.children[childIndex]; + // 更新子组件的 value 和 options + if (!childWidget.value) { + this.$set(childWidget, "value", {}); + } + childWidget.value[key] = this.deepClone(val); + if (childWidget.options && childWidget.options[key]) { + this.setDefaultValue(childWidget.options[key], val); + } + // 回写到 rootWidget 的 setup 中,保证 Tabs 组件保存时能带上最新配置 + 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; + } + }); + } + } + } else { + // 普通顶层组件的配置更新逻辑保持不变 + for (let i = 0; i < this.widgets.length; i++) { + if (this.widgetIndex == i) { + this.widgets[i].value[key] = this.deepClone(val); + this.setDefaultValue(this.widgets[i].options[key], val); + } } } } diff --git a/src/views/bigscreenDesigner/designer/widget/form/widgetTabs.vue b/src/views/bigscreenDesigner/designer/widget/form/widgetTabs.vue index 9082c96..0cb7935 100644 --- a/src/views/bigscreenDesigner/designer/widget/form/widgetTabs.vue +++ b/src/views/bigscreenDesigner/designer/widget/form/widgetTabs.vue @@ -355,9 +355,16 @@ export default { }, setActiveTab() { const tabs = this.tabsList; - if (tabs && tabs.length > 0) { - this.activeTab = tabs[0].name || 'tab0'; + if (!tabs || tabs.length === 0) return; + + // 如果当前 activeTab 还在 tabs 中,则保持不变,避免每次数据变化都跳回第一个标签 + if (this.activeTab) { + const exist = tabs.some((t, i) => (t.name || `tab${i}`) === this.activeTab); + if (exist) return; } + + // 当前没有选中标签,或原选中标签已被删除时,才默认选中第一个 + this.activeTab = tabs[0].name || 'tab0'; }, handleTabClick(tab) { const currentTab = this.tabsList.find(t => (t.name || `tab${this.tabsList.indexOf(t)}`) === tab.name); -- 2.52.0.windows.1