diff --git a/src/views/bigscreenDesigner/designer/index.vue b/src/views/bigscreenDesigner/designer/index.vue
index c897ae7..15b6350 100644
--- a/src/views/bigscreenDesigner/designer/index.vue
+++ b/src/views/bigscreenDesigner/designer/index.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"
@@ -446,6 +448,8 @@ export default {
rect : null, //框选矩形对象
openMulDrag: false, //批量拖拽开关
moveWidgets:{}, //记录移动的组件的起始left和top属性
+ // 记录当前是否选中了 Tabs 内部子组件,如果为 null 则表示选中的是顶层组件或大屏
+ innerWidgetSelected: null,
};
},
computed: {
@@ -549,9 +553,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() {
/**
@@ -578,12 +587,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;
// 获取结束坐标和列名
@@ -665,6 +710,50 @@ export default {
this.widgetIndex = index;
this.widgetsClick(event,index);
},
+ /**
+ * 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;
+ this.innerWidgetSelected = null;
+ this.widgetIndex = rootIndex;
+ this.setOptionsOnClickWidget(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);
+ },
// 如果是点击大屏设计器中的底层,加载大屏底层属性
setOptionsOnClickScreen() {
console.log("setOptionsOnClickScreen");
@@ -683,6 +772,8 @@ export default {
// 如果是点击某个组件,获取该组件的配置项
setOptionsOnClickWidget(obj) {
this.screenCode = "";
+ // 选中顶层组件时清空内部子组件的选中状态
+ this.innerWidgetSelected = null;
if (typeof obj == "number") {
this.widgetOptions = this.deepClone(this.widgets[obj]["options"]);
return;
@@ -701,8 +792,72 @@ export default {
});
this.widgetOptions = this.deepClone(this.widgets[obj.index]["options"]);
},
+ /**
+ * 选中 Tabs 组件内部的子组件,展示该子组件的右侧配置
+ */
+ setOptionsOnClickInnerWidget(payload) {
+ if (!payload) return;
+ const { rootWidgetIndex, tabIndex, childIndex, widget } = payload;
+ const rootIndex = typeof rootWidgetIndex === "number" ? rootWidgetIndex : 0;
+ const rootWidget = this.widgets[rootIndex];
+ if (!rootWidget || !rootWidget.value || !rootWidget.value.setup) return;
+
+ // 记录当前选中的内部子组件路径
+ this.innerWidgetSelected = {
+ rootWidgetIndex: rootIndex,
+ tabIndex,
+ childIndex,
+ };
+ this.screenCode = "";
+ this.widgetIndex = rootIndex;
+ this.activeName = "first";
+
+ // 找到真正的子组件对象
+ const tabsList = (rootWidget.value.setup && rootWidget.value.setup.tabsList) || [];
+ const targetTab = tabsList[tabIndex];
+ if (!targetTab || !targetTab.children || !targetTab.children[childIndex]) {
+ return;
+ }
+ const childWidget = widget || targetTab.children[childIndex];
+
+ // 用实际 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];
+ }
+ });
+ }
+
+ this.widgetOptions = this.deepClone(childWidget.options);
+ },
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;
+ }
+ }
//判断是否按住了Ctrl按钮,表示Ctrl多选
let _this = this;
let eventWidget = null;
@@ -800,10 +955,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/tools/configure/form/widget-tabs.js b/src/views/bigscreenDesigner/designer/tools/configure/form/widget-tabs.js
new file mode 100644
index 0000000..efbc991
--- /dev/null
+++ b/src/views/bigscreenDesigner/designer/tools/configure/form/widget-tabs.js
@@ -0,0 +1,221 @@
+/*
+ * @Descripttion: Tabs标签组件
+ * @version:
+ * @Author: Devli
+ * @Date: 2024-01-01 00:00:00
+ * @LastEditors: Devli
+ * @LastEditTime: 2024-01-01 00:00:00
+ */
+export const widgetTabs = {
+ code: 'widget-tabs',
+ type: 'form',
+ tabName: '表单',
+ label: 'Tabs标签',
+ icon: 'iconkuangjia',
+ options: {
+ // 配置
+ setup: [
+ {
+ type: 'el-input-text',
+ label: '图层名称',
+ name: 'layerName',
+ required: false,
+ placeholder: '',
+ value: 'Tabs标签',
+ },
+ {
+ type: 'el-select',
+ label: '标签位置',
+ name: 'tabPosition',
+ required: false,
+ placeholder: '',
+ selectOptions: [
+ { code: 'top', name: '顶部' },
+ { code: 'right', name: '右侧' },
+ { code: 'bottom', name: '底部' },
+ { code: 'left', name: '左侧' },
+ ],
+ value: 'top',
+ },
+ {
+ type: 'el-select',
+ label: '标签类型',
+ name: 'type',
+ required: false,
+ placeholder: '',
+ selectOptions: [
+ { code: '', name: '默认' },
+ { code: 'border-card', name: '边框卡片' },
+ { code: 'card', name: '卡片' },
+ ],
+ value: '',
+ },
+ {
+ type: 'el-switch',
+ label: '可关闭',
+ name: 'closable',
+ required: false,
+ placeholder: '',
+ value: false,
+ },
+ {
+ type: 'el-switch',
+ label: '可添加',
+ name: 'addable',
+ required: false,
+ placeholder: '',
+ value: false,
+ },
+ {
+ type: 'el-switch',
+ label: '可拉伸',
+ name: 'stretch',
+ required: false,
+ placeholder: '',
+ value: false,
+ },
+ {
+ type: 'vue-color',
+ label: '标签字体颜色',
+ name: 'labelColor',
+ required: false,
+ placeholder: '',
+ value: '#303133',
+ },
+ {
+ type: 'vue-color',
+ label: '激活标签颜色',
+ name: 'activeColor',
+ required: false,
+ placeholder: '',
+ value: '#409EFF',
+ },
+ {
+ type: 'el-input-number',
+ label: '字体字号',
+ name: 'fontSize',
+ required: false,
+ placeholder: '',
+ value: 14,
+ },
+ {
+ type: 'el-button',
+ label: '标签列表',
+ name: 'tabsList',
+ required: false,
+ placeholder: '',
+ value: [
+ { label: '标签一', name: 'tab1', content: '标签一的内容', children: [] },
+ { label: '标签二', name: 'tab2', content: '标签二的内容', children: [] },
+ { label: '标签三', name: 'tab3', content: '标签三的内容', children: [] },
+ ],
+ },
+ [
+ {
+ name: '组件联动',
+ list: [
+ {
+ type: 'componentLinkage',
+ label: '',
+ name: 'componentLinkage',
+ required: false,
+ value: []
+ }
+ ]
+ }
+ ]
+ ],
+ // 数据
+ data: [
+ {
+ type: 'el-radio-group',
+ label: '数据类型',
+ name: 'dataType',
+ require: false,
+ placeholder: '',
+ selectValue: true,
+ selectOptions: [
+ {
+ code: 'staticData',
+ name: '静态数据',
+ },
+ {
+ code: 'dynamicData',
+ name: '动态数据',
+ },
+ ],
+ value: 'staticData',
+ },
+ {
+ type: 'el-input-number',
+ label: '刷新时间(毫秒)',
+ name: 'refreshTime',
+ relactiveDom: 'dataType',
+ relactiveDomValue: 'dynamicData',
+ value: 30000
+ },
+ {
+ type: 'el-button',
+ label: '静态数据',
+ name: 'staticData',
+ required: false,
+ placeholder: '',
+ relactiveDom: 'dataType',
+ relactiveDomValue: 'staticData',
+ value: [
+ { label: '标签一', name: 'tab1', content: '标签一的内容', children: [] },
+ { label: '标签二', name: 'tab2', content: '标签二的内容', children: [] },
+ { label: '标签三', name: 'tab3', content: '标签三的内容', children: [] },
+ ],
+ },
+ {
+ type: 'dycustComponents',
+ label: '',
+ name: 'dynamicData',
+ required: false,
+ placeholder: '',
+ relactiveDom: 'dataType',
+ relactiveDomValue: 'dynamicData',
+ chartType: 'widget-tabs',
+ dictKey: 'TABS_PROPERTIES',
+ value: '',
+ },
+ ],
+ // 坐标
+ position: [
+ {
+ type: 'el-input-number',
+ label: '左边距',
+ name: 'left',
+ required: false,
+ placeholder: '',
+ value: 0,
+ },
+ {
+ type: 'el-input-number',
+ label: '上边距',
+ name: 'top',
+ required: false,
+ placeholder: '',
+ value: 0,
+ },
+ {
+ type: 'el-input-number',
+ label: '宽度',
+ name: 'width',
+ required: false,
+ placeholder: '该容器在1920px大屏中的宽度',
+ value: 400,
+ },
+ {
+ type: 'el-input-number',
+ label: '高度',
+ name: 'height',
+ required: false,
+ placeholder: '该容器在1080px大屏中的高度',
+ value: 300,
+ },
+ ],
+ }
+}
+
diff --git a/src/views/bigscreenDesigner/designer/tools/main.js b/src/views/bigscreenDesigner/designer/tools/main.js
index 12c98fd..a322fb2 100644
--- a/src/views/bigscreenDesigner/designer/tools/main.js
+++ b/src/views/bigscreenDesigner/designer/tools/main.js
@@ -13,6 +13,7 @@ import {widgetHref} from "./configure/texts/widget-href"
import {widgetTime} from "./configure/texts/widget-time"
import {widgetImage} from "./configure/texts/widget-image"
import {widgetButton} from "./configure/form/widget-button"
+import {widgetTabs} from "./configure/form/widget-tabs"
import {widgetSliders} from "./configure/texts/widget-slider"
import {widgetVideo} from "./configure/texts/widget-video"
import {widgetVideoMonitor} from "./configure/texts/widget-videoMonitor"
@@ -68,6 +69,7 @@ export const widgetTool = [
widgetTime,
widgetImage,
widgetButton,
+ widgetTabs,
// widgetSliders,
widgetVideo,
widgetVideoMonitor,
diff --git a/src/views/bigscreenDesigner/designer/widget/form/widgetTabs.vue b/src/views/bigscreenDesigner/designer/widget/form/widgetTabs.vue
new file mode 100644
index 0000000..b23f4e8
--- /dev/null
+++ b/src/views/bigscreenDesigner/designer/widget/form/widgetTabs.vue
@@ -0,0 +1,849 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/views/bigscreenDesigner/designer/widget/widget.vue b/src/views/bigscreenDesigner/designer/widget/widget.vue
index 084e9b1..ea00ac4 100644
--- a/src/views/bigscreenDesigner/designer/widget/widget.vue
+++ b/src/views/bigscreenDesigner/designer/widget/widget.vue
@@ -1,5 +1,6 @@
-
+
@@ -20,6 +29,7 @@
import widgetHref from "./texts/widgetHref.vue";
import widgetText from "./texts/widgetText.vue";
import widgetButton from './form/widgetButton.vue';
+import widgetTabs from './form/widgetTabs.vue';
import WidgetMarquee from "./texts/widgetMarquee.vue";
import widgetTime from "./texts/widgetTime.vue";
import widgetImage from "./texts/widgetImage.vue";
@@ -72,6 +82,7 @@ export default {
widgetHref,
widgetText,
widgetButton,
+ widgetTabs,
widgetBorder,
widgetDecorateFlowLine,
widgetDecoration,
@@ -146,6 +157,8 @@ export default {
/* leftMargin: null,
topMargin: null*/
},
+ // 当 Tabs 内部拖拽子组件时,暂时禁用外层 avue-draggable,防止整个 Tabs 被拖动
+ innerDragging: false,
};
},
computed: {
@@ -165,7 +178,11 @@ export default {
return this.value.position.zIndex || 1;
},
widgetDisabled() {
- return this.value.position.disabled || false;
+ // Tabs 必须禁用外层拖拽,内部子组件才能选中;Tabs 整体拖动改由标题栏单独处理
+ if (this.type === 'widget-tabs') {
+ return true;
+ }
+ return this.value.position.disabled || this.innerDragging || false;
},
},
mounted() {
@@ -218,6 +235,26 @@ export default {
});
}
},
+ /**
+ * 接收 Tabs 内部子组件发出的激活事件,并转发给设计器主页面
+ */
+ handleChildActivated(payload) {
+ const info = Object.assign({}, payload || {});
+ if (info.rootWidgetIndex === undefined || info.rootWidgetIndex === null) {
+ info.rootWidgetIndex = this.index;
+ }
+ this.$emit("onChildActivated", info);
+ },
+ // Tabs 内部开始拖拽/点击子组件时,暂时禁用外层拖拽
+ handleInnerDragStart() {
+ this.innerDragging = true;
+ },
+ handleInnerDragEnd() {
+ this.innerDragging = false;
+ },
+ handleTabsHeaderMouseDown(evt) {
+ this.$emit('onTabsHeaderMouseDown', { event: evt, rootWidgetIndex: this.index });
+ },
},
};