tabs组件优化
This commit is contained in:
@@ -290,6 +290,8 @@
|
|||||||
:type="widget.type"
|
:type="widget.type"
|
||||||
:bigscreen="{ bigscreenWidth, bigscreenHeight }"
|
:bigscreen="{ bigscreenWidth, bigscreenHeight }"
|
||||||
@onActivated="setOptionsOnClickWidget"
|
@onActivated="setOptionsOnClickWidget"
|
||||||
|
@onChildActivated="setOptionsOnClickInnerWidget"
|
||||||
|
@onTabsHeaderMouseDown="onTabsHeaderMouseDown($event)"
|
||||||
@contextmenu.prevent.native="rightClick($event, index)"
|
@contextmenu.prevent.native="rightClick($event, index)"
|
||||||
@mousedown.prevent.native="widgetsClick($event, index)"
|
@mousedown.prevent.native="widgetsClick($event, index)"
|
||||||
@mouseup.prevent.native="grade = false"
|
@mouseup.prevent.native="grade = false"
|
||||||
@@ -432,6 +434,8 @@ export default {
|
|||||||
rect : null, //框选矩形对象
|
rect : null, //框选矩形对象
|
||||||
openMulDrag: false, //批量拖拽开关
|
openMulDrag: false, //批量拖拽开关
|
||||||
moveWidgets:{}, //记录移动的组件的起始left和top属性
|
moveWidgets:{}, //记录移动的组件的起始left和top属性
|
||||||
|
// 记录当前是否选中了 Tabs 内部子组件,如果为 null 则表示选中的是顶层组件或大屏
|
||||||
|
innerWidgetSelected: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -683,6 +687,50 @@ export default {
|
|||||||
this.widgetIndex = index;
|
this.widgetIndex = index;
|
||||||
this.widgetsClick(event,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() {
|
setOptionsOnClickScreen() {
|
||||||
console.log("setOptionsOnClickScreen");
|
console.log("setOptionsOnClickScreen");
|
||||||
@@ -701,6 +749,8 @@ export default {
|
|||||||
// 如果是点击某个组件,获取该组件的配置项
|
// 如果是点击某个组件,获取该组件的配置项
|
||||||
setOptionsOnClickWidget(obj) {
|
setOptionsOnClickWidget(obj) {
|
||||||
this.screenCode = "";
|
this.screenCode = "";
|
||||||
|
// 选中顶层组件时清空内部子组件的选中状态
|
||||||
|
this.innerWidgetSelected = null;
|
||||||
if (typeof obj == "number") {
|
if (typeof obj == "number") {
|
||||||
this.widgetOptions = this.deepClone(this.widgets[obj]["options"]);
|
this.widgetOptions = this.deepClone(this.widgets[obj]["options"]);
|
||||||
return;
|
return;
|
||||||
@@ -719,8 +769,72 @@ export default {
|
|||||||
});
|
});
|
||||||
this.widgetOptions = this.deepClone(this.widgets[obj.index]["options"]);
|
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) {
|
widgetsClick(event,index) {
|
||||||
console.log("widgetsClick");
|
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多选
|
//判断是否按住了Ctrl按钮,表示Ctrl多选
|
||||||
let _this = this;
|
let _this = this;
|
||||||
let eventWidget = null;
|
let eventWidget = null;
|
||||||
@@ -818,6 +932,35 @@ export default {
|
|||||||
console.log(newSetup);
|
console.log(newSetup);
|
||||||
this.widgetOptions.setup = newSetup;
|
this.widgetOptions.setup = newSetup;
|
||||||
} else {
|
} else {
|
||||||
|
// 如果当前选中的是 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++) {
|
for (let i = 0; i < this.widgets.length; i++) {
|
||||||
if (this.widgetIndex == i) {
|
if (this.widgetIndex == i) {
|
||||||
this.widgets[i].value[key] = this.deepClone(val);
|
this.widgets[i].value[key] = this.deepClone(val);
|
||||||
@@ -825,6 +968,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
setDefaultValue(options, val) {
|
setDefaultValue(options, val) {
|
||||||
for (let i = 0; i < options.length; i++) {
|
for (let i = 0; i < options.length; i++) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* @Last Modified time: 2024-01-01 00:00:00
|
* @Last Modified time: 2024-01-01 00:00:00
|
||||||
!-->
|
!-->
|
||||||
<template>
|
<template>
|
||||||
<div class="widget-tabs">
|
<div class="widget-tabs" @mousedown.capture="onTabsRootCapture">
|
||||||
<el-tabs
|
<el-tabs
|
||||||
:style="styleObj"
|
:style="styleObj"
|
||||||
:tab-position="tabPosition"
|
:tab-position="tabPosition"
|
||||||
@@ -35,27 +35,43 @@
|
|||||||
@dragenter.capture.prevent="handleTabDragEnter($event)"
|
@dragenter.capture.prevent="handleTabDragEnter($event)"
|
||||||
@dragenter.prevent="handleTabDragEnter($event)"
|
@dragenter.prevent="handleTabDragEnter($event)"
|
||||||
>
|
>
|
||||||
<!-- 渲染子组件 -->
|
<!-- 渲染子组件(支持在标签内部拖动、删除) -->
|
||||||
<template v-if="tab.children && tab.children.length > 0">
|
<template v-if="tab.children && tab.children.length > 0">
|
||||||
<component
|
<div
|
||||||
v-for="(childWidget, childIndex) in tab.children"
|
v-for="(childWidget, childIndex) in tab.children"
|
||||||
:key="childWidget.value.widgetId || `child-${index}-${childIndex}`"
|
: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)"
|
:is="getComponentName(childWidget.type)"
|
||||||
:value="childWidget.value"
|
:value="childWidget.value"
|
||||||
:widget-index="childIndex"
|
:widget-index="childIndex"
|
||||||
:ispreview="ispreview"
|
:ispreview="ispreview"
|
||||||
:style="{
|
class="tab-child-component"
|
||||||
position: 'absolute',
|
|
||||||
left: (childWidget.value.position && childWidget.value.position.left) ? childWidget.value.position.left + 'px' : '0px',
|
|
||||||
top: (childWidget.value.position && childWidget.value.position.top) ? 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',
|
|
||||||
zIndex: 10,
|
|
||||||
}"
|
|
||||||
@contextmenu.prevent.native="handleChildWidgetRightClick($event, childIndex, index)"
|
|
||||||
@mousedown.prevent.native="handleChildWidgetClick($event, childIndex, index)"
|
|
||||||
@mouseup.prevent.native="grade = false"
|
|
||||||
/>
|
/>
|
||||||
|
<span
|
||||||
|
class="tab-child-delete"
|
||||||
|
title="删除组件"
|
||||||
|
@mousedown.stop
|
||||||
|
@click.stop="removeChildWidget(index, childIndex)"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else class="tab-content-empty">
|
<div v-else class="tab-content-empty">
|
||||||
拖拽组件到这里
|
拖拽组件到这里
|
||||||
@@ -190,6 +206,9 @@ export default {
|
|||||||
dynamicTabsList: [],
|
dynamicTabsList: [],
|
||||||
flagInter: null,
|
flagInter: null,
|
||||||
grade: false,
|
grade: false,
|
||||||
|
draggingChild: null,
|
||||||
|
childDragMoveHandler: null,
|
||||||
|
childDragUpHandler: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -287,6 +306,23 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
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() {
|
setActiveTab() {
|
||||||
const tabs = this.tabsList;
|
const tabs = this.tabsList;
|
||||||
if (tabs && tabs.length > 0) {
|
if (tabs && tabs.length > 0) {
|
||||||
@@ -564,17 +600,160 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleChildWidgetActivated(data) {
|
handleChildWidgetActivated(data) {
|
||||||
// 处理子组件激活事件
|
// 处理子组件激活事件(预留)
|
||||||
console.log('子组件激活:', data);
|
console.log('子组件激活:', data);
|
||||||
},
|
},
|
||||||
handleChildWidgetClick(event, childIndex, tabIndex) {
|
/**
|
||||||
// 处理子组件点击
|
* 捕获阶段处理内部子区域 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();
|
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.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) {
|
handleChildWidgetRightClick(event, childIndex, tabIndex) {
|
||||||
// 处理子组件右键
|
// 处理子组件右键 - 支持直接删除
|
||||||
event.stopPropagation();
|
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) {
|
getComponentName(type) {
|
||||||
@@ -602,6 +781,7 @@ export default {
|
|||||||
|
|
||||||
.el-tabs__header {
|
.el-tabs__header {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
cursor: move;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-tabs__content {
|
.el-tabs__content {
|
||||||
@@ -622,6 +802,37 @@ export default {
|
|||||||
min-height: 100px;
|
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 {
|
.tab-content-empty {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<avue-draggable
|
<avue-draggable
|
||||||
|
:data-widget-index="index"
|
||||||
:step="step"
|
:step="step"
|
||||||
:width="widgetsWidth"
|
:width="widgetsWidth"
|
||||||
:height="widgetsHeight"
|
:height="widgetsHeight"
|
||||||
@@ -12,7 +13,15 @@
|
|||||||
@blur="handleBlur"
|
@blur="handleBlur"
|
||||||
>
|
>
|
||||||
<!-- :z-index="-1" -->
|
<!-- :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>
|
</avue-draggable>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -148,6 +157,8 @@ export default {
|
|||||||
/* leftMargin: null,
|
/* leftMargin: null,
|
||||||
topMargin: null*/
|
topMargin: null*/
|
||||||
},
|
},
|
||||||
|
// 当 Tabs 内部拖拽子组件时,暂时禁用外层 avue-draggable,防止整个 Tabs 被拖动
|
||||||
|
innerDragging: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -167,7 +178,11 @@ export default {
|
|||||||
return this.value.position.zIndex || 1;
|
return this.value.position.zIndex || 1;
|
||||||
},
|
},
|
||||||
widgetDisabled() {
|
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() {
|
mounted() {
|
||||||
@@ -220,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>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user