diff --git a/src/views/bigscreenDesigner/designer/index.vue b/src/views/bigscreenDesigner/designer/index.vue
index 2a92dd2..406975a 100644
--- a/src/views/bigscreenDesigner/designer/index.vue
+++ b/src/views/bigscreenDesigner/designer/index.vue
@@ -319,6 +319,26 @@
:widget-params-config="widgetParamsConfig"
@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,
@@ -789,51 +855,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 });
+ },
},
};