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 }); + }, }, };