This commit is contained in:
mll
2026-02-10 13:39:22 +08:00
5 changed files with 1302 additions and 8 deletions

View File

@@ -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()"
>
<div class="tools-item">
@@ -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);
}
}
}
}

View File

@@ -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,
},
],
}
}

View File

@@ -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,

View File

@@ -0,0 +1,849 @@
<!--
* @Author: Devli
* @Date: 2024-01-01 00:00:00
* @Last Modified by: Devli
* @Last Modified time: 2024-01-01 00:00:00
!-->
<template>
<div class="widget-tabs" @mousedown.capture="onTabsRootCapture">
<el-tabs
:style="styleObj"
:tab-position="tabPosition"
:type="tabsType"
:closable="closable"
:addable="addable"
:stretch="stretch"
v-model="activeTab"
@tab-click="handleTabClick"
@tab-remove="handleTabRemove"
@tab-add="handleTabAdd"
>
<el-tab-pane
v-for="(tab, index) in tabsList"
:key="tab.name || index"
:label="tab.label"
:name="tab.name || `tab${index}`"
:closable="closable"
>
<div
class="tab-content"
data-tab-content
:style="contentStyle"
@drop="handleTabDrop($event, tab, index)"
@dragover.capture.prevent="handleTabDragOver($event)"
@dragover.prevent="handleTabDragOver($event)"
@dragenter.capture.prevent="handleTabDragEnter($event)"
@dragenter.prevent="handleTabDragEnter($event)"
>
<!-- 渲染子组件支持在标签内部拖动删除 -->
<template v-if="tab.children && tab.children.length > 0">
<div
v-for="(childWidget, childIndex) in tab.children"
: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',
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',
}"
>
<div
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)"
>
<component
:is="getComponentName(childWidget.type)"
:value="childWidget.value"
:widget-index="childIndex"
:ispreview="ispreview"
class="tab-child-component"
/>
<span
class="tab-child-delete"
title="删除组件"
@mousedown.stop
@click.stop="removeChildWidget(index, childIndex)"
>
×
</span>
</div>
</div>
</template>
<div v-else class="tab-content-empty">
拖拽组件到这里
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import {
originWidgetLinkageLogic,
targetWidgetLinkageLogic,
} from "@/views/bigscreenDesigner/designer/linkageLogic";
import { getToolByCode } from "@/views/bigscreenDesigner/designer/tools/index";
// 与主画布 widget.vue 保持一致:注册所有可拖入的组件,否则图表、下拉等会报 Unknown custom element
import widgetHref from "../texts/widgetHref.vue";
import widgetText from "../texts/widgetText.vue";
import widgetButton from "./widgetButton.vue";
import widgetImage from "../texts/widgetImage.vue";
import widgetTable from "../texts/widgetTable.vue";
import WidgetMarquee from "../texts/widgetMarquee.vue";
import widgetTime from "../texts/widgetTime.vue";
import widgetSlider from "../texts/widgetSlider.vue";
import widgetVideo from "../texts/widgetVideo.vue";
import widgetVideoMonitor from "../texts/widgetVideoMonitor.vue";
import WidgetIframe from "../texts/widgetIframe.vue";
import widgetCalendar from "../texts/widgetCalendar.vue";
import widgetBarchart from "../bar/widgetBarchart.vue";
import widgetScatter from "../scatter/widgetScatter.vue";
import widgetGradientColorBarchart from "../bar/widgetGradientColorBarchart.vue";
import widgetLinechart from "../line/widgetLinechart.vue";
import widgetBarlinechart from "../barline/widgetBarlinechart";
import WidgetPiechart from "../pie/widgetPiechart.vue";
import WidgetFunnel from "../funnel/widgetFunnel.vue";
import WidgetGauge from "../percent/widgetGauge.vue";
import WidgetPieNightingaleRoseArea from "../pie/widgetPieNightingaleRose";
import widgetLineMap from "../map/widgetLineMap.vue";
import widgetPiePercentageChart from "../percent/widgetPiePercentageChart";
import widgetAirBubbleMap from "../map/widgetAirBubbleMap.vue";
import widgetBarStackChart from "../bar/widgetBarStackChart.vue";
import widgetLineStackChart from "../line/widgetLineStackChart.vue";
import widgetBarCompareChart from "../bar/widgetBarCompareChart.vue";
import widgetLineCompareChart from "../line/widgetLineCompareChart.vue";
import widgetDecoratePieChart from "../styleWidget/widgetDecoratePieChart.vue";
import widgetMoreBarLineChart from "../barline/widgetMoreBarLineChart";
import widgetWordCloud from "../wordcloud/widgetWordCloud.vue";
import widgetHeatmap from "../heatmap/widgetHeatmap.vue";
import widgetRadar from "../radar/widgetRadar.vue";
import widgetBarLineStackChart from "../barline/widgetBarLineStackChart";
import widgetSelect from "./widgetSelect.vue";
import widgetFormTime from "./widgetFormTime.vue";
import widgetScaleVertical from "../scale/widgetScaleVertical.vue";
import widgetScaleHorizontal from "../scale/widgetScaleHorizontal.vue";
import widgetBarDoubleYaxisChart from "../bar/widgetBarDoubleYaxisChart.vue";
import widgetBorder from "../styleWidget/widgetBorder.vue";
import widgetDecorateFlowLine from "../styleWidget/widgetDecorateFlowLine.vue";
import widgetDecoration from "../styleWidget/widgetDecoration.vue";
import widgetBarMap from "../map/widgetBarMap.vue";
import widgetChinaMap from "../map/widgetChinaMap.vue";
import widgetGlobalMap from "../map/widgetGlobalMap.vue";
import widgetBarStackMoreShowChart from "../bar/widgetBarStackMoreShowChart.vue";
import widgetBarLineSingleChart from "../barline/widgetBarLineSingleChart.vue";
export default {
name: "WidgetTabs",
components: {
widgetHref,
widgetText,
widgetButton,
widgetImage,
widgetTable,
WidgetMarquee,
widgetTime,
widgetSlider,
widgetVideo,
widgetVideoMonitor,
WidgetIframe,
widgetCalendar,
widgetBarchart,
widgetGradientColorBarchart,
widgetLinechart,
widgetBarlinechart,
WidgetPiechart,
WidgetFunnel,
WidgetGauge,
WidgetPieNightingaleRoseArea,
widgetLineMap,
widgetPiePercentageChart,
widgetAirBubbleMap,
widgetBarStackChart,
widgetLineStackChart,
widgetBarCompareChart,
widgetLineCompareChart,
widgetDecoratePieChart,
widgetMoreBarLineChart,
widgetWordCloud,
widgetHeatmap,
widgetRadar,
widgetBarLineStackChart,
widgetScaleVertical,
widgetScaleHorizontal,
widgetSelect,
widgetFormTime,
widgetBarDoubleYaxisChart,
widgetBorder,
widgetDecorateFlowLine,
widgetDecoration,
widgetBarMap,
widgetChinaMap,
widgetGlobalMap,
widgetScatter,
widgetBarStackMoreShowChart,
widgetBarLineSingleChart,
},
props: {
value: Object,
ispreview: Boolean,
widgetIndex: {
type: Number,
default: 0,
},
},
data() {
return {
options: {},
optionsSetup: {},
optionsData: {},
optionsStyle: {},
activeTab: '',
dynamicTabsList: [],
flagInter: null,
grade: false,
draggingChild: null,
childDragMoveHandler: null,
childDragUpHandler: null,
};
},
computed: {
transStyle() {
return this.objToOne(this.options);
},
styleObj() {
return {
position: this.ispreview ? "relative" : "static",
width: this.optionsStyle.width + "px",
height: this.optionsStyle.height + "px",
left: this.optionsStyle.left + "px",
top: this.optionsStyle.top + "px",
display:
this.transStyle.hideLayer === undefined
? "block"
: this.transStyle.hideLayer ? "none" : "block",
};
},
contentStyle() {
return {
color: this.transStyle.labelColor || '#303133',
'font-size': (this.transStyle.fontSize || 14) + 'px',
padding: '20px',
height: '100%',
'box-sizing': 'border-box',
};
},
tabPosition() {
return this.transStyle.tabPosition || 'top';
},
tabsType() {
return this.transStyle.type || '';
},
closable() {
return this.transStyle.closable || false;
},
addable() {
return this.transStyle.addable || false;
},
stretch() {
return this.transStyle.stretch || false;
},
tabsList() {
if (this.optionsData && this.optionsData.dataType === 'dynamicData' && this.optionsData.dynamicData && this.dynamicTabsList.length > 0) {
// 动态数据
return this.dynamicTabsList;
} else {
// 静态数据 - 优先使用setup中的tabsList确保children数据不丢失
const tabsList = this.optionsSetup.tabsList || this.transStyle.tabsList || [];
// 确保每个tab都有children数组
tabsList.forEach(tab => {
if (!tab.children) {
this.$set(tab, 'children', []);
}
});
return tabsList;
}
},
allComponentLinkage() {
return this.$store.state.designer.allComponentLinkage;
},
tabBigscreen() {
// 子组件的bigscreen应该是相对于tab容器的
return {
bigscreenWidth: this.optionsStyle.width,
bigscreenHeight: this.optionsStyle.height,
};
},
},
watch: {
value: {
handler(val) {
this.options = val;
this.optionsSetup = val.setup || {};
this.optionsData = val.data || {};
this.optionsStyle = val.position || {};
this.setActiveTab();
},
deep: true,
immediate: true,
},
},
mounted() {
this.options = this.value;
this.optionsSetup = this.value.setup;
this.optionsData = this.value.data;
this.optionsStyle = this.value.position;
this.setActiveTab();
targetWidgetLinkageLogic(this); // 联动-目标组件逻辑
// 设置动态数据
if (this.optionsData && this.optionsData.dataType === 'dynamicData') {
this.dynamicDataFn(this.optionsData.dynamicData, this.optionsData.refreshTime);
}
},
methods: {
/**
* 触发 Tabs 内部子组件选中事件,让设计器右侧展示该子组件的配置
*/
emitChildActivated(tabIndex, childIndex) {
// 预览模式下不需要触发设计事件
if (this.ispreview) return;
const currentTabsList = this.tabsList;
const targetTab = currentTabsList[tabIndex];
if (!targetTab || !targetTab.children || !targetTab.children[childIndex]) return;
const childWidget = targetTab.children[childIndex];
this.$emit("childActivated", {
rootWidgetIndex: this.widgetIndex,
tabIndex,
childIndex,
widget: childWidget,
});
},
setActiveTab() {
const tabs = this.tabsList;
if (tabs && tabs.length > 0) {
this.activeTab = tabs[0].name || 'tab0';
}
},
handleTabClick(tab) {
const currentTab = this.tabsList.find(t => (t.name || `tab${this.tabsList.indexOf(t)}`) === tab.name);
if (currentTab) {
originWidgetLinkageLogic(this, true, {
currentData: currentTab,
}); // 联动-源组件逻辑
}
},
handleTabRemove(name) {
// 处理标签页移除
console.log('移除标签页:', name);
},
handleTabAdd() {
// 处理标签页添加
console.log('添加标签页');
},
//动态数据字典解析
dynamicDataFn(val, refreshTime) {
if (!val) return;
clearInterval(this.flagInter); // 清除之前的定时器
if (this.ispreview) {
this.getEchartData(val);
this.flagInter = setInterval(() => {
this.getEchartData(val);
}, refreshTime);
} else {
this.getEchartData(val);
}
},
getEchartData(val) {
const data = this.queryEchartsData(val);
data.then((res) => {
this.renderingFn(res);
}).catch((err) => {
console.error('获取tabs数据失败:', err);
});
},
renderingFn(val) {
// 将动态数据转换为tabs格式
this.dynamicTabsList = Array.isArray(val) ? val.map((item, index) => ({
label: item.label || item.name || `标签${index + 1}`,
name: item.name || item.value || `tab${index}`,
content: item.content || item.label || item.name || '',
children: item.children || [],
})) : [];
this.$nextTick(() => {
this.setActiveTab();
});
},
// 处理tab内容区域的拖拽
handleTabDragOver(evt) {
evt.preventDefault();
evt.stopPropagation();
evt.stopImmediatePropagation();
evt.dataTransfer.dropEffect = "copy";
return false;
},
handleTabDragEnter(evt) {
evt.preventDefault();
evt.stopPropagation();
evt.stopImmediatePropagation();
return false;
},
handleTabDrop(evt, tab, tabIndex) {
evt.preventDefault();
evt.stopPropagation();
evt.stopImmediatePropagation();
console.log('Tabs drop事件触发', tab, tabIndex);
// 优先从 dataTransfer 读取index 在 dragStart 时已写入),否则再沿父组件查找
let dragWidgetCode = null;
if (evt.dataTransfer && evt.dataTransfer.types && evt.dataTransfer.types.indexOf('application/x-widget-code') !== -1) {
dragWidgetCode = evt.dataTransfer.getData('application/x-widget-code');
}
if (!dragWidgetCode) {
dragWidgetCode = this.getDragWidgetCode();
}
console.log('拖拽的组件类型:', dragWidgetCode);
if (!dragWidgetCode) {
console.warn('未获取到拖拽组件类型');
return;
}
// 获取相对于tab内容区域的坐标
const tabContentEl = evt.currentTarget;
const tabContentRect = tabContentEl.getBoundingClientRect();
const x = evt.clientX - tabContentRect.left;
const y = evt.clientY - tabContentRect.top;
// 创建新组件
const tool = getToolByCode(dragWidgetCode);
if (!tool) {
console.warn('未找到组件配置:', dragWidgetCode);
return;
}
const widgetJson = {
type: dragWidgetCode,
value: {
setup: {},
data: {},
position: {
width: 0,
height: 0,
left: 0,
top: 0,
zIndex: 0,
},
},
options: this.deepClone(tool.options),
};
// 处理默认值
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;
}
// 生成唯一ID
const uuid = Number(Math.random().toString().substr(2)).toString(36);
widgetJsonValue.value.widgetId = uuid;
widgetJsonValue.value.widgetCode = dragWidgetCode;
// 获取当前的tabsList确保使用正确的数据源
let currentTabsList = this.tabsList;
// 找到对应的tab并添加子组件
const targetTab = currentTabsList[tabIndex];
if (!targetTab) {
console.warn('未找到目标tab');
return;
}
if (!targetTab.children) {
this.$set(targetTab, 'children', []);
}
targetTab.children.push(this.deepClone(widgetJsonValue));
// 更新到setup中
this.optionsSetup.tabsList = currentTabsList;
// 更新value.setup
if (!this.value.setup) {
this.$set(this.value, 'setup', {});
}
this.value.setup.tabsList = currentTabsList;
// 触发更新
this.$emit('input', this.value);
console.log('子组件已添加:', targetTab.children);
this.grade = false;
},
/**
* 当 drop 落在 Tabs 外层容器(如 avue-draggable时由设计器调用将组件加入当前激活的 tab
*/
addWidgetFromDrop(evt) {
if (!evt || !evt.dataTransfer) return;
let dragWidgetCode = null;
if (evt.dataTransfer.types && evt.dataTransfer.types.indexOf("application/x-widget-code") !== -1) {
dragWidgetCode = evt.dataTransfer.getData("application/x-widget-code");
}
if (!dragWidgetCode) dragWidgetCode = this.getDragWidgetCode();
if (!dragWidgetCode) return;
const tabIndex = this.tabsList.findIndex(
(t, i) => (t.name || `tab${i}`) === this.activeTab
);
const tab = tabIndex >= 0 ? this.tabsList[tabIndex] : this.tabsList[0];
const actualIndex = tabIndex >= 0 ? tabIndex : 0;
const contentEls = this.$el.querySelectorAll("[data-tab-content]");
const contentEl = contentEls[actualIndex] || contentEls[0];
const rect = contentEl ? contentEl.getBoundingClientRect() : this.$el.getBoundingClientRect();
const x = evt.clientX - rect.left;
const y = evt.clientY - rect.top;
const tool = getToolByCode(dragWidgetCode);
if (!tool) return;
const widgetJson = {
type: dragWidgetCode,
value: {
setup: {},
data: {},
position: { width: 0, height: 0, left: 0, top: 0, zIndex: 0 },
},
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;
}
const uuid = Number(Math.random().toString().substr(2)).toString(36);
widgetJsonValue.value.widgetId = uuid;
widgetJsonValue.value.widgetCode = dragWidgetCode;
const currentTabsList = this.tabsList;
const targetTab = currentTabsList[actualIndex];
if (!targetTab) return;
if (!targetTab.children) this.$set(targetTab, "children", []);
targetTab.children.push(this.deepClone(widgetJsonValue));
this.optionsSetup.tabsList = currentTabsList;
if (!this.value.setup) this.$set(this.value, "setup", {});
this.value.setup.tabsList = currentTabsList;
this.$emit("input", this.value);
},
getDragWidgetCode() {
// 尝试从父组件获取dragWidgetCode
let parent = this.$parent;
while (parent) {
if (parent.dragWidgetCode !== undefined) {
return parent.dragWidgetCode;
}
parent = parent.$parent;
}
return null;
},
getWidgetConfigValue(widgetJson) {
this.setWidgetConfigValue(
widgetJson.options.setup,
widgetJson.value.setup
);
this.setWidgetConfigValue(
widgetJson.options.position,
widgetJson.value.position
);
this.setWidgetConfigValue(widgetJson.options.data, widgetJson.value.data);
return widgetJson;
},
setWidgetConfigValue(config, configValue) {
if (config) {
config.forEach((item) => {
if (this.isObjectFn(item)) {
configValue[item.name] = item.value;
}
if (this.isArrayFn(item)) {
item.forEach((itemChild) => {
itemChild.list.forEach((ev) => {
configValue[ev.name] = ev.value;
});
});
}
});
}
},
handleChildWidgetActivated(data) {
// 处理子组件激活事件(预留)
console.log('子组件激活:', data);
},
/**
* 捕获阶段处理内部子区域 mousedown先于 avue-draggable 拦截,避免整块 Tabs 被拖动
* 点击删除按钮时不拦截,让删除能正常触发
*/
/**
* 根节点捕获:仅当点击在标题栏时发出事件,让设计器拖动整块 Tabs外层 avue-draggable 已禁用)
*/
onTabsRootCapture(evt) {
if (this.ispreview) return;
if (evt.target && evt.target.closest && evt.target.closest('.el-tabs__header')) {
evt.stopPropagation();
this.$emit('tabsHeaderMouseDown', evt);
}
},
onTabChildMouseDown(event, tabIndex, childIndex) {
if (event.target && event.target.closest && event.target.closest('.tab-child-delete')) {
return; // 点在删除按钮上,不拦截,删除按钮的 @mousedown.stop 会阻止冒泡到外层
}
event.preventDefault();
event.stopPropagation();
// 选中内部组件并通知外层暂时禁用拖拽
this.emitChildActivated(tabIndex, childIndex);
this.$emit('innerDragStart');
this.startChildDrag(event, childIndex, tabIndex);
},
/**
* 开始拖拽标签内子组件(可由 onTabChildMouseDown 或 workbench 捕获层调用)
*/
startChildDrag(event, childIndex, tabIndex) {
this.$emit('innerDragStart');
const currentTabsList = this.tabsList;
const targetTab = currentTabsList[tabIndex];
if (!targetTab || !targetTab.children || !targetTab.children[childIndex]) return;
const child = targetTab.children[childIndex];
if (!child.value.position) {
this.$set(child.value, 'position', {});
}
const pos = child.value.position;
const startLeft = typeof pos.left === 'number' ? pos.left : 0;
const startTop = typeof pos.top === 'number' ? pos.top : 0;
this.draggingChild = {
tabIndex,
childIndex,
startX: event.clientX,
startY: event.clientY,
startLeft,
startTop,
};
// 选中效果
this.grade = true;
// 绑定全局鼠标事件
this.childDragMoveHandler = (e) => this.onChildDragMove(e);
this.childDragUpHandler = (e) => this.onChildDragEnd(e);
document.addEventListener('mousemove', this.childDragMoveHandler);
document.addEventListener('mouseup', this.childDragUpHandler);
},
/**
* 拖拽过程中移动子组件
*/
onChildDragMove(event) {
if (!this.draggingChild) return;
const { tabIndex, childIndex, startX, startY, startLeft, startTop } = this.draggingChild;
const dx = event.clientX - startX;
const dy = event.clientY - startY;
const currentTabsList = this.tabsList;
const targetTab = currentTabsList[tabIndex];
if (!targetTab || !targetTab.children || !targetTab.children[childIndex]) return;
const child = targetTab.children[childIndex];
if (!child.value.position) {
this.$set(child.value, 'position', {});
}
const pos = child.value.position;
const width = pos.width || 100;
const height = pos.height || 50;
let newLeft = startLeft + dx;
let newTop = startTop + dy;
// 约束在 tab 内容区域内部
if (newLeft < 0) newLeft = 0;
if (newTop < 0) newTop = 0;
if (newLeft + width > this.optionsStyle.width) {
newLeft = this.optionsStyle.width - width;
}
if (newTop + height > this.optionsStyle.height) {
newTop = this.optionsStyle.height - height;
}
this.$set(pos, 'left', newLeft);
this.$set(pos, 'top', newTop);
// 实时同步到配置,保证保存后位置不丢
this.optionsSetup.tabsList = currentTabsList;
if (!this.value.setup) {
this.$set(this.value, 'setup', {});
}
this.value.setup.tabsList = currentTabsList;
this.$emit('input', this.value);
},
/**
* 结束拖拽
*/
onChildDragEnd() {
if (this.childDragMoveHandler) {
document.removeEventListener('mousemove', this.childDragMoveHandler);
this.childDragMoveHandler = null;
}
if (this.childDragUpHandler) {
document.removeEventListener('mouseup', this.childDragUpHandler);
this.childDragUpHandler = null;
}
this.draggingChild = null;
// 通知外层 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
if (!type) return '';
return type.split('-').map((part, index) => {
if (index === 0) return part;
return part.charAt(0).toUpperCase() + part.slice(1);
}).join('');
},
},
};
</script>
<style scoped lang="scss">
.widget-tabs {
width: 100%;
height: 100%;
::v-deep .el-tabs {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
.el-tabs__header {
margin: 0;
cursor: move;
}
.el-tabs__content {
flex: 1;
overflow: auto;
}
.el-tab-pane {
height: 100%;
}
}
.tab-content {
width: 100%;
height: 100%;
overflow: auto;
position: relative;
min-height: 100px;
}
.tab-child-wrapper {
width: 100%;
height: 100%;
position: relative;
}
.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%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #999;
font-size: 14px;
border: 2px dashed #ddd;
border-radius: 4px;
}
}
</style>

View File

@@ -1,5 +1,6 @@
<template>
<avue-draggable
:data-widget-index="index"
:step="step"
:width="widgetsWidth"
:height="widgetsHeight"
@@ -12,7 +13,15 @@
@blur="handleBlur"
>
<!-- :z-index="-1" -->
<component :is="type" :widget-index="index" :value="value"/>
<component
:is="type"
:widget-index="index"
:value="value"
@childActivated="handleChildActivated"
@innerDragStart="handleInnerDragStart"
@innerDragEnd="handleInnerDragEnd"
@tabsHeaderMouseDown="handleTabsHeaderMouseDown"
/>
</avue-draggable>
</template>
@@ -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 });
},
},
};
</script>