Files
gr_bi_web/src/views/bigscreenDesigner/designer/index.vue
2026-03-05 15:27:35 +08:00

1238 lines
44 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
* @Descripttion: 大屏报表设计器-主体页面
* @Author: Devli
* @Date: 2021-3-19 10:23:27
* @LastEditors: qianlishi
* @LastEditTime: 2023-03-17 17:38:44
-->
<template>
<div class="layout">
<div
v-if="toolIsShow"
class="layout-left"
:style="{ width: widthLeftForTools + 'px' }"
>
<el-tabs class="layout-left" type="border-card" :stretch="true">
<!-- 左侧组件栏-->
<el-tab-pane label="工具栏">
<span slot="label"><i class="el-icon-date icon"></i>工具栏</span>
<div class="chart-type">
<el-tabs class="type-left" tab-position="left">
<el-tab-pane
v-for="(item, index) in widgetTools"
:key="index"
:label="item.name"
>
<li
v-for="(it, idx) in item.list"
:key="idx"
draggable="true"
@dragstart="dragStart(it.code, $event)"
@dragend="dragEnd()"
>
<div class="tools-item">
<span class="tools-item-icon">
<i class="iconfont" :class="it.icon"></i>
</span>
<span class="tools-item-text">{{ it.label }}</span>
</div>
</li>
</el-tab-pane>
</el-tabs>
</div>
</el-tab-pane>
<!-- 左侧图层-->
<el-tab-pane label="图层">
<draggable
v-model="layerWidget"
@update="datadragEnd"
:options="{ animation: 300 }"
>
<transition-group>
<div
v-for="(item, index) in layerWidget"
:key="'item' + index"
class="tools-item"
:class="widgetIndex == index ? 'is-active' : ''"
@click="layerClick($event,index)"
>
<span class="tools-item-icon">
<i class="iconfont" :class="item.icon"></i>
</span>
<span class="tools-item-text">{{ item.label }}</span>
</div>
</transition-group>
</draggable>
</el-tab-pane>
</el-tabs>
</div>
<div
class="layout-left-fold"
:style="{ width: widthLeftForToolsHideButton + 'px' }"
@click="toolIsShow = !toolIsShow"
>
<i class="el-icon-arrow-right"/>
</div>
<div
class="layout-middle"
:style="{ width: middleWidth + 'px', height: middleHeight + 'px' }"
>
<div class="top-button">
<span class="btn" @click="saveData">
<el-tooltip
class="item"
effect="dark"
content="保存"
placement="bottom"
>
<i class="iconfont iconsave"></i>
</el-tooltip>
</span>
<span class="btn" @click="viewScreen">
<el-tooltip
class="item"
effect="dark"
content="预览"
placement="bottom"
>
<i class="iconfont iconyulan"></i>
</el-tooltip>
</span>
<span class="btn" @click="handleUndo">
<el-tooltip
class="item"
effect="dark"
content="撤销"
placement="bottom"
>
<i class="iconfont iconundo"></i>
</el-tooltip>
</span>
<span class="btn" @click="handleRedo">
<el-tooltip
class="item"
effect="dark"
content="恢复"
placement="bottom"
>
<i class="iconfont iconhuifubeifen"></i>
</el-tooltip>
</span>
<span
:class="{
btn: true,
'btn-disable': currentSizeRangeIndex === 0,
}"
@click="setSize(0)"
>
<el-tooltip
class="item"
:disabled="currentSizeRangeIndex === 0"
effect="dark"
content="缩小"
placement="bottom"
>
<i class="el-icon-minus" style="font-size: 16px"/>
</el-tooltip>
</span>
<span
:class="{
btn: true,
'scale-num': true,
'btn-disable': currentSizeRangeIndex === defaultSize.index,
}"
@click="setSize(2)"
>
<el-tooltip
class="item"
:disabled="currentSizeRangeIndex === defaultSize.index"
effect="dark"
content="默认比例"
placement="bottom"
>
<span> {{ parseInt(scaleNum) }}% </span>
</el-tooltip>
</span>
<span
:class="{
btn: true,
'btn-disable': currentSizeRangeIndex === 8,
}"
@click="setSize(1)"
>
<el-tooltip
class="item"
:disabled="currentSizeRangeIndex === 8"
effect="dark"
content="放大"
placement="bottom"
>
<i class="el-icon-plus" style="font-size: 16px"/>
</el-tooltip>
</span>
<span class="btn" v-permission="'bigScreenManage:export'">
<el-tooltip
class="item"
effect="dark"
content="导入"
placement="bottom"
>
<el-upload
class="el-upload"
ref="upload"
:action="uploadUrl"
:headers="headers"
accept=".zip"
:on-success="handleUpload"
:show-file-list="false"
:limit="1"
>
<i class="iconfont icondaoru"></i>
</el-upload>
</el-tooltip>
</span>
<span class="btn border-left" v-permission="'bigScreenManage:import'">
<ul class="nav">
<li>
<i class="iconfont icondaochu"></i
><i class="el-icon-arrow-down"></i>
<ul>
<li>
<el-tooltip
class="item"
effect="dark"
content="适合当前系统"
placement="right"
>
<div @click="exportDashboard(1)">导出(包含数据集)</div>
</el-tooltip>
</li>
<li>
<el-tooltip
class="item"
effect="dark"
content="适合跨系统"
placement="right"
>
<div @click="exportDashboard(0)">导出(不包含数据集)</div>
</el-tooltip>
</li>
</ul>
</li>
</ul>
</span>
</div>
<!-- 中间操作内容 主体 -->
<div class="workbench-container" @mousedown="handleMouseDown">
<div
:style="{
width: (+bigscreenWidth + 18) * bigscreenScaleInWorkbench + 'px',
height: (+bigscreenHeight + 18) * bigscreenScaleInWorkbench + 'px',
}"
class="vue-ruler-tool-wrap"
>
<!-- 大屏设计页面的标尺插件 -->
<vue-ruler-tool
ref="vue-ruler-tool"
v-model="dashboard.presetLine"
class="vueRuler"
:step-length="50"
:parent="true"
:position="'relative'"
:is-scale-revise="true"
:visible.sync="dashboard.presetLineVisible"
:style="{
width: +bigscreenWidth + 18 + 'px',
height: +bigscreenHeight + 18 + 'px',
transform:
currentSizeRangeIndex === defaultSize.index
? workbenchTransform
: `scale(${sizeRange[currentSizeRangeIndex] / 100})`,
transformOrigin: '0 0',
}"
>
<div
id="workbench"
class="workbench"
:style="{
width: bigscreenWidth + 'px',
height: bigscreenHeight + 'px',
'background-color': dashboard.backgroundColor,
'background-image': 'url(' + dashboard.backgroundImage + ')',
'background-position': '0% 0%',
'background-size': '100% 100%',
'background-repeat': 'initial',
'background-attachment': 'initial',
'background-origin': 'initial',
'background-clip': 'initial',
}"
@click.self="setOptionsOnClickScreen"
@drop="widgetOnDragged($event)"
@dragover="dragOver($event)"
@mousedown.self="downEvent($event)"
@mouseup="upEvent($event)"
@mousemove="moveEvent($event)"
>
<div v-if="grade" class="bg-grid"></div>
<widget
ref="widgets"
v-for="(widget, index) in widgets"
:key="index"
v-model="widget.value"
:index="index"
:step="step"
: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"
/>
</div>
</vue-ruler-tool>
</div>
</div>
</div>
<div class="layout-right" :style="{ width: widthLeftForOptions + 'px' }">
<el-tabs v-model="activeName" type="border-card" :stretch="true">
<el-tab-pane
v-if="
isNotNull(widgetOptions.setup) || isNotNull(widgetOptions.collapse)
"
name="first"
label="配置"
>
<dynamicForm
ref="formData1"
:options="widgetOptions.setup"
:layer-widget="layerWidget"
:widget-index="widgetIndex"
:widget-params-config="widgetParamsConfig"
@handleCollapse="handleCollapse"
@onChanged="(val) => widgetValueChanged('setup', val)"
/>
<!-- 如果当前选中的是 Tabs 内部子组件,在配置表单底部显示删除按钮 -->
<div
v-if="innerWidgetSelected"
style="text-align: right; margin-top: 12px;"
>
<el-popconfirm
title="确定删除该子组件吗?"
@onConfirm="deleteInnerWidget"
@confirm="deleteInnerWidget"
>
<el-button
slot="reference"
type="danger"
size="mini"
plain
>
删除该子组件
</el-button>
</el-popconfirm>
</div>
</el-tab-pane>
<el-tab-pane
v-if="isNotNull(widgetOptions.data)"
name="second"
label="数据"
>
<dynamicForm
ref="formData"
:options="widgetOptions.data"
@onChanged="(val) => widgetValueChanged('data', val)"
/>
</el-tab-pane>
<el-tab-pane
v-if="isNotNull(widgetOptions.position)"
name="third"
label="坐标"
>
<dynamicForm
ref="formData"
:options="widgetOptions.position"
@onChanged="(val) => widgetValueChanged('position', val)"
/>
</el-tab-pane>
</el-tabs>
</div>
<el-dialog
title="下钻配置"
:visible.sync="dialogVisibleDrillDrown"
width="100%"
class="dialogDrillDrown"
>
<drill-drown-setting @submitDrillDrownData="submitDrillDrownData" :screenData="screenData"></drill-drown-setting>
</el-dialog>
<content-menu
:visible.sync="visibleContentMenu"
:style-obj="styleObj"
@deletelayer="deletelayer"
@lockLayer="lockLayer"
@noLockLayer="noLockLayer"
@copylayer="copylayer"
@istopLayer="istopLayer"
@setlowLayer="setlowLayer"
@moveupLayer="moveupLayer"
@movedownLayer="movedownLayer"
@alignment="alignment($event)"
/>
</div>
</template>
<script>
import {widgetTools, getToolByCode} from "./tools/index";
import mixin from "@/utils/screenMixins";
import widget from "./widget/widget.vue";
import dynamicForm from "./components/dynamicForm.vue";
import draggable from "vuedraggable";
import VueRulerTool from "vue-ruler-tool"; // 大屏设计页面的标尺插件
import contentMenu from "./components/contentMenu";
import DrillDrownSetting from "./components/drillDrownSetting";
export default {
name: "Login",
components: {
draggable,
VueRulerTool,
widget,
dynamicForm,
contentMenu,
DrillDrownSetting
},
mixins: [mixin],
data() {
return {
grade: false,
dialogVisibleDrillDrown:false,
layerWidget: [],
widgetTools: widgetTools, // 左侧工具栏的组件图标将js变量加入到当前作用域
widthLeftForTools: 200, // 左侧工具栏宽度
widthLeftForToolsHideButton: 15, // 左侧工具栏折叠按钮宽度
widthLeftForOptions: 300, // 右侧属性配置区
widthPaddingTools: 18,
toolIsShow: true, // 左侧工具栏是否显示
screenData:{
dashboard: { },
widgets: []
},
bigscreenWidth: 1920, // 大屏设计的大小
bigscreenHeight: 1080,
dashboard: {},
// 大屏的标记
screenCode: "",
dragWidgetCode: "", //从工具栏拖拽的组件code
// 大屏画布中的组件
widgets: [], // 工作区中拖放的组件
// 当前激活组件
widgetIndex: 0,
// 当前激活组件右侧配置属性
widgetOptions: {
setup: [], // 配置
data: [], // 数据
position: [], // 坐标
},
flagWidgetClickStopPropagation: false, // 点击组件时阻止事件冒泡传递到画布click事件上
styleObj: {
left: 0,
top: 0,
},
visibleContentMenu: false,
activeName: "first",
scaleNum: 0, // 当前缩放百分比的值
sizeRange: [20, 40, 60, 80, 100, 150, 200, 300, 400], // 缩放百分比
currentSizeRangeIndex: -1, // 当前是哪个缩放比分比,
currentWidgetTotal: 0,
widgetParamsConfig: [], // 各组件动态数据集的参数配置情况
selectedWidgets: [], //多选组件集合
moveTimes: 0, //鼠标移动次数
selectFlag: false, //选择标识
kuangSelectFlag: false, //框选标识
downX: 0, //移动开始X坐标
downY: 0, //移动开始Y坐标
downX2: 0, //移动结束X坐标
downY2: 0, //移动结束Y坐标
rect : null, //框选矩形对象
openMulDrag: false, //批量拖拽开关
moveWidgets:{}, //记录移动的组件的起始left和top属性
// 记录当前是否选中了 Tabs 内部子组件,如果为 null 则表示选中的是顶层组件或大屏
innerWidgetSelected: null,
};
},
computed: {
// 左侧折叠切换时,动态计算中间区的宽度
middleWidth() {
let widthLeftAndRight = 0;
if (this.toolIsShow) {
widthLeftAndRight += this.widthLeftForTools; // 左侧工具栏宽度
}
widthLeftAndRight += this.widthLeftForToolsHideButton; // 左侧工具栏折叠按钮宽度
widthLeftAndRight += this.widthLeftForOptions; // 右侧配置栏宽度
let middleWidth = this.bodyWidth - widthLeftAndRight;
return middleWidth;
},
middleHeight() {
return this.bodyHeight;
},
// 设计台按大屏的缩放比例
bigscreenScaleInWorkbench() {
let widthScale =
(this.middleWidth - this.widthPaddingTools) / this.bigscreenWidth;
let heightScale =
(this.middleHeight - this.widthPaddingTools) / this.bigscreenHeight;
return Math.min(widthScale, heightScale);
},
workbenchTransform() {
return `scale(${this.bigscreenScaleInWorkbench}, ${this.bigscreenScaleInWorkbench})`;
},
// 大屏在设计模式的大小
bigscreenWidthInWorkbench() {
return this.getPXUnderScale(this.bigscreenWidth) + this.widthPaddingTools;
},
bigscreenHeightInWorkbench() {
return (
this.getPXUnderScale(this.bigscreenHeight) + this.widthPaddingTools
);
},
},
watch: {
widgets: {
handler(val) {
this.getLayerData(val);
this.handlerdynamicDataParamsConfig(val);
//以下部分是记录历史
this.$nextTick(() => {
this.revoke.push(this.widgets);
});
},
deep: true,
},
},
mounted() {
this.widgets = [];
window.addEventListener("mouseup", () => {
this.grade = false;
});
this.$nextTick(() => {
this.initVueRulerTool(); // 初始化 修正插件样式
});
},
methods: {
submitDrillDrownData(data){
this.dialogVisibleDrillDrown=false
this.$refs.formData1.changeDrillData(data)
},
handleCollapse(val){
this.dialogVisibleDrillDrown=true
this.screenData=val.drill_drown_setting?val.drill_drown_setting:{
dashboard: { },
widgets: []
}
},
// 获取图层数据
getLayerData(val) {
const layerWidgetArr = [];
for (let i = 0; i < val.length; i++) {
const obj = {};
const myItem = getToolByCode(val[i].type);
obj.icon = myItem.icon;
obj.code = myItem.code; // 组件类型code
obj.widgetId = val[i].value.widgetId || ""; // 唯一id
if (val[i].value.paramsKeys) {
obj.paramsKeys = val[i].value.paramsKeys;
}
const options = val[i].options["setup"];
options.forEach((el) => {
if (el.name == "layerName") {
obj.label = el.value;
}
});
layerWidgetArr.push(obj);
}
this.layerWidget = layerWidgetArr;
},
// 返回每个组件的动态数据集参数配置情况
handlerdynamicDataParamsConfig(val) {
this.widgetParamsConfig = val.map((item) => {
return item.value.data;
});
},
// 在缩放模式下的大小
getPXUnderScale(px) {
return this.bigscreenScaleInWorkbench * px;
},
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() {
/**
* 40@remarks 新增组件到操作面板后右边的配置有更新但是当前选中的组件没更新导致配置错乱的bug;
* 由于拖动组件拖到非操作面板上是不会添加组件,还需判断是否添加组件到操作面板上;
*/
this.$nextTick(() => {
if (this.widgets.length === this.currentWidgetTotal + 1) {
// 确实新增了一个组件到操作面板上
console.log(
`新添加 '${
this.widgets[this.currentWidgetTotal].value.setup.layerName
}' 组件到操作面板`
);
const uuid = Number(Math.random().toString().substr(2)).toString(36);
this.widgets[this.currentWidgetTotal].value.widgetId = uuid;
this.layerWidget[this.currentWidgetTotal].widgetId = uuid;
this.widgets[this.currentWidgetTotal].value.widgetCode = this.dragWidgetCode;
const index = this.widgets.length - 1;
this.widgetIndex = index;
this.widgetsClickAndCtrl(index); // 选中当前新增的组件
this.grade = false; // 去除网格线
}
});
},
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;
// 获取结束坐标和列名
let eventX = evt.clientX; // 结束在屏幕的x坐标
let eventY = evt.clientY; // 结束在屏幕的y坐标
let workbenchPosition = this.getDomTopLeftById("workbench");
let widgetTopInWorkbench = eventY - workbenchPosition.top;
let widgetLeftInWorkbench = eventX - workbenchPosition.left;
const targetScale =
this.currentSizeRangeIndex === this.defaultSize.index
? this.bigscreenScaleInWorkbench
: this.sizeRange[this.currentSizeRangeIndex] / 100;
const x = widgetLeftInWorkbench / targetScale;
const y = widgetTopInWorkbench / targetScale;
// 复制一个组件
let tool = getToolByCode(widgetType);
let widgetJson = {
type: widgetType,
value: {
setup: {},
data: {},
position: {
width: 0,
height: 0,
left: 0,
top: 0,
zIndex: 0,
},
},
options: tool.options,
};
// 处理默认值
console.log(widgetType)
const widgetJsonValue = this.getWidgetConfigValue(widgetJson);
widgetJsonValue.value.position.left =
x - widgetJsonValue.value.position.width / 2;
widgetJsonValue.value.position.top =
y - widgetJsonValue.value.position.height / 2;
// 将选中的复制组件,放到工作区中去
this.widgets.push(this.deepClone(widgetJsonValue));
// 激活新组件的配置属性
this.setOptionsOnClickWidget(this.widgets.length - 1);
},
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;
});
});
}
});
}
},
layerClick(event,index) {
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;
// 选中 Tabs 组件本身,触发蓝色点框和右侧配置
this.innerWidgetSelected = null;
this.widgetsClickFocus(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");
if(this.selectedWidgets.length > 0 && this.kuangSelectFlag){
//如果Ctrl多选过程中点击了大屏底层就清空 this.selectedWidgets
return;
}
this.selectFlag = false;
this.kuangSelectFlag = false;
this.selectedWidgets = [];
this.screenCode = "screen";
// 选中不同的组件 右侧都显示第一栏
this.activeName = "first";
this.widgetOptions = getToolByCode("screen")["options"];
},
// 如果是点击某个组件,获取该组件的配置项
setOptionsOnClickWidget(obj) {
this.screenCode = "";
// 选中顶层组件时清空内部子组件的选中状态
this.innerWidgetSelected = null;
if (typeof obj == "number") {
this.widgetOptions = this.deepClone(this.widgets[obj]["options"]);
return;
}
if (obj.index < 0 || obj.index >= this.widgets.length) {
return;
}
this.widgetIndex = obj.index;
this.widgets[obj.index].value.position = obj;
this.widgets[obj.index].options.position.forEach((el) => {
for (const key in obj) {
if (el.name == key) {
el.value = obj[key];
}
}
});
this.widgetOptions = this.deepClone(this.widgets[obj.index]["options"]);
},
/**
* 选中 Tabs 组件内部的子组件,展示该子组件的右侧配置
*/
setOptionsOnClickInnerWidget(payload) {
if (!payload) return;
console.log('setOptionsOnClickInnerWidget payload:', payload);
const { rootWidgetIndex, tabIndex, childIndex } = payload;
const rootIndex = typeof rootWidgetIndex === "number" ? rootWidgetIndex : 0;
const rootWidget = this.widgets[rootIndex];
if (!rootWidget || !rootWidget.value) return;
// 1. 兼容两种存储位置value.setup.tabsList / options.setup 里的 tabsList
const setupObj = rootWidget.value.setup || {};
let tabsList = setupObj.tabsList;
if (!tabsList || !tabsList.length) {
const setupArr = rootWidget.options && rootWidget.options.setup ? rootWidget.options.setup : [];
const tabsItem = setupArr.find(it => it.name === "tabsList");
if (tabsItem && Array.isArray(tabsItem.value)) {
tabsList = tabsItem.value;
}
}
tabsList = tabsList || [];
const targetTab = tabsList[tabIndex];
if (!targetTab || !targetTab.children || !targetTab.children[childIndex]) {
console.warn("Tabs 子组件未找到:", { rootIndex, tabIndex, childIndex, tabsList });
return;
}
let childWidget = targetTab.children[childIndex];
// 2. 确保子组件有 options老数据里可能只有 type + value
if (!childWidget.options) {
try {
const tool = getToolByCode(childWidget.type);
if (tool && tool.options) {
childWidget.options = this.deepClone(tool.options);
} else {
console.warn("未找到子组件 options 配置:", childWidget.type);
}
} catch (e) {
console.error("为子组件补全 options 失败:", e);
}
}
// 3. 用实际 position 回填到 options.position 中,保证坐标表单显示正确
if (childWidget.options && Array.isArray(childWidget.options.position)) {
const pos = (childWidget.value && childWidget.value.position) ? childWidget.value.position : {};
childWidget.options.position.forEach((el) => {
if (pos.hasOwnProperty(el.name)) {
el.value = pos[el.name];
}
});
}
// 4. 记录当前选中的内部子组件路径 & 切换右侧到子组件配置
this.innerWidgetSelected = {
rootWidgetIndex: rootIndex,
tabIndex,
childIndex,
};
this.screenCode = "";
this.widgetIndex = rootIndex;
this.activeName = "first";
this.widgetOptions = this.deepClone(childWidget.options || {});
},
/**
* 删除当前选中的 Tabs 内部子组件
*/
deleteInnerWidget() {
if (!this.innerWidgetSelected) return;
const { rootWidgetIndex, tabIndex, childIndex } = this.innerWidgetSelected;
const rootWidget = this.widgets[rootWidgetIndex];
if (!rootWidget || !rootWidget.value) return;
// 永远只操作“真实绑定”的数据rootWidget.value.setup.tabsList
if (!rootWidget.value.setup) {
this.$set(rootWidget.value, "setup", {});
}
if (!Array.isArray(rootWidget.value.setup.tabsList)) {
this.$set(rootWidget.value.setup, "tabsList", []);
}
const tabsList = rootWidget.value.setup.tabsList;
const targetTab = tabsList[tabIndex];
if (!targetTab || !targetTab.children || !targetTab.children.length) return;
targetTab.children.splice(childIndex, 1);
// 同步回 rootWidget 的 setup / options
rootWidget.value.setup.tabsList = tabsList;
const setupArr = rootWidget.options && rootWidget.options.setup ? rootWidget.options.setup : [];
setupArr.forEach((item) => {
if (item.name === "tabsList") {
item.value = tabsList;
}
});
// 清空内部选中状态,并回到 Tabs 本身配置
this.innerWidgetSelected = null;
this.widgetIndex = rootWidgetIndex;
this.widgetOptions = this.deepClone(rootWidget.options || {});
this.activeName = "first";
},
widgetsClick(event,index) {
console.log("widgetsClick");
// 如果是 Tabs 组件,内部点击由 widgetTabs 自己处理,这里只负责选中 Tabs 整体
const rootWidget = this.widgets[index];
if (rootWidget && rootWidget.type === "widget-tabs") {
this.widgetsClickFocus(index);
return;
}
//判断是否按住了Ctrl按钮表示Ctrl多选
let _this = this;
let eventWidget = null;
if(event.currentTarget.__vue__ != null) { // //解决图层栏点击组件定位问题(批量移动改造导致的问题)
eventWidget = event.currentTarget.__vue__.$parent;//vue3已经弃用__vue__
}
if(eventWidget != null){
if(event.ctrlKey){ //Ctrl左键选中或者取消选中
if(this.selectedWidgets.includes(eventWidget)){
this.selectedWidgets = this.selectedWidgets.filter(w=> w!== eventWidget);
this.$refs.widgets.forEach(w=>{
if(eventWidget.value.widgetId === w.value.widgetId){
setTimeout(function (){
_this.$refs.widgets[index].$refs.draggable.setActive(false);
console.log("触发取消选中, eventWidget.value.widgetId = " + eventWidget.value.widgetId +", w.value.widgetId= "+ w.value.widgetId);
},200); //设置超时,防止效果被覆盖
}
})
return;
}
this.widgetsClickAndCtrl(event, index);
return;
}
if(this.selectedWidgets.includes(eventWidget)){ //右键点击菜单的时候 批量拖拽的时候
this.openMulDrag = true;
this.moveWidgets = {};
for (let i = 0; i < this.$refs.widgets.length; i++) {
let widget = {
left: this.$refs.widgets[i].value.position.left,
top: this.$refs.widgets[i].value.position.top
};
this.moveWidgets[this.$refs.widgets[i].value.widgetId] = widget;
}
this.calculateMousePosition(event, true);
return;
}
}
this.widgetsClickFocus(index);
},
widgetsClickFocus(index){
this.selectedWidgets = []; //单选的时候需要清空
this.selectedWidgets.push(this.$refs.widgets[index]); //确保第一个选中的组件添加到集合不需要按住Ctrl键
const draggableArr = this.$refs.widgets;
for (let i = 0; i < draggableArr.length; i++) {
if (i == index) {
this.$refs.widgets[i].$refs.draggable.setActive(true);
} else {
this.$refs.widgets[i].$refs.draggable.setActive(false);
}
}
this.setOptionsOnClickWidget(index);
this.grade = true;
},
//Ctrl鼠标点击事件
widgetsClickAndCtrl(event, index) {
const draggableArr = this.$refs.widgets;
for (let i = 0; i < draggableArr.length; i++) {
if (i === index && ! this.selectedWidgets.includes(this.$refs.widgets[i])) {
this.selectedWidgets.push(this.$refs.widgets[i]); //选中的添加到集合
}
}
},
handleMouseDown() {
const draggableArr = this.$refs.widgets;
for (let i = 0; i < draggableArr.length; i++) {
this.$refs.widgets[i].$refs.draggable.setActive(false);
}
},
// 将当前选中的组件,右侧属性值更新
widgetValueChanged(key, val) {
console.log("key", key);
console.log("val", val);
console.log(this.widgetOptions);
if (this.screenCode == "screen") {
let newSetup = [];
this.dashboard = this.deepClone(val);
console.log("asd", this.dashboard);
console.log(this.widgetOptions);
if (this.bigscreenWidth != this.dashboard.width) {
this.bigscreenWidth = this.dashboard.width;
}
if (this.bigscreenHeight != this.dashboard.height) {
this.bigscreenHeight = this.dashboard.height;
}
this.widgetOptions.setup.forEach((el) => {
if (el.name == "width") {
el.value = this.bigscreenWidth;
} else if (el.name == "height") {
el.value = this.bigscreenHeight;
} else if (this.dashboard.hasOwnProperty(el.name)) {
el["value"] = this.dashboard[el.name];
}
newSetup.push(el);
});
console.log(newSetup);
this.widgetOptions.setup = newSetup;
} 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++) {
if (this.widgetIndex == i) {
this.widgets[i].value[key] = this.deepClone(val);
this.setDefaultValue(this.widgets[i].options[key], val);
}
}
}
}
},
setDefaultValue(options, val) {
for (let i = 0; i < options.length; i++) {
if (this.isObjectFn(options[i])) {
for (const k in val) {
if (options[i].name == k) {
options[i].value = val[k];
}
}
} else if (this.isArrayFn(options[i])) {
for (let j = 0; j < options[i].length; j++) {
const list = options[i][j].list;
for (let z = 0; z < list.length; z++) {
for (const k in val) {
if (list[z].name == k) {
list[z].value = val[k];
}
}
}
}
}
}
},
datadragEnd(evt) {
evt.preventDefault();
this.widgets = this.swapArr(this.widgets, evt.oldIndex, evt.newIndex);
},
//计算鼠标坐标
calculateMousePosition(event, isStart){
let workbenchPosition = this.getDomTopLeftById("workbench");
let widgetTopInWorkbench = event.clientY - workbenchPosition.top;
let widgetLeftInWorkbench = event.clientX - workbenchPosition.left;
const targetScale =
this.currentSizeRangeIndex === this.defaultSize.index
? this.bigscreenScaleInWorkbench
: this.sizeRange[this.currentSizeRangeIndex] / 100;
const x = widgetLeftInWorkbench / targetScale;
const y = widgetTopInWorkbench / targetScale;
if(isStart){
this.downX = x;
this.downY = y;
}else{
this.downX2 = x;
this.downY2 = y;
}
},
//鼠标按下事件
downEvent(event){
console.log("downEvent")
this.moveTimes = 0;
this.selectedWidgets = [];
this.openMulDrag = false;
this.selectFlag = true;
this.kuangSelectFlag = false; //框选标志
//鼠标位置
this.calculateMousePosition(event, true)
if(this.rect != null){
document.getElementById("workbench").removeChild(this.rect);
this.rect = null;
}
},
//鼠标移动事件
moveEvent(event){
console.log("moveEvent");
//测试的时候发现每次点击组件再次点击大屏的时候偶尔会触发一次moveEvent,导致会生成rect所以加了移动次数moveTimes 变量控制一下,只有移动多次的情况下,才能说明是框选多选
if(this.selectFlag && this.selectedWidgets.length <= 1 && this.moveTimes >= 1){
if(this.rect === null){
//这里说明一下为啥不在downEvent方法中创建是因为
this.rect = document.createElement("div");
this.rect.style.cssText = "position:absolute; width:0px; height:0px; font-size:0px; margin:0px; border: 1px dashed #0099FF; background-color: #C3D5ED";
this.rect.id = "selectedDiv";
this.rect.style.left = this.downX +"px";
this.rect.style.top = this.downY+"px";
this.rect.style.left = this.downX;
this.rect.style.top = this.downY;
}
document.getElementById("workbench").appendChild(this.rect);
this.calculateMousePosition(event, false);
if(this.rect.style.display === "none"){
this.rect.style.display = "";
}
this.rect.style.left = Math.min(this.downX, this.downX2) + "px";
this.rect.style.top = Math.min(this.downY, this.downY2) + "px";
this.rect.style.width = Math.abs(this.downX - this.downX2) + "px";
this.rect.style.height = Math.abs(this.downY - this.downY2) + "px";
if(this.downX2 < this.downX && this.downY2 < this.downY){
this.rect.style.left = this.downX2;
this.rect.style.top = this.downY2;
}
if(this.downX2 > this.downX2 && this.downY2 < this.downY){
this.rect.style.left = this.downX;
this.rect.style.top = this.downY2;
}
if(this.downX2 < this.downX && this.downY2 > this.downY){
this.rect.style.left = this.downX2;
this.rect.style.top = this.downY;
}
if(this.downX2 > this.downX2 && this.downY2 > this.downY){
this.rect.style.left = this.downX;
this.rect.style.top = this.downY;
}
}
if (this.openMulDrag) {
this.mulWidgetMove(event);
}
this.moveTimes++;
},
//批量拖拽移动
mulWidgetMove(event){
let _this = this;
if(this.openMulDrag && this.selectedWidgets.length >= 2){
this.calculateMousePosition(event, false);
setTimeout(function (){
_this.selectedWidgets.forEach(sw=>{
for (let i = 0; i < _this.$refs.widgets.length; i++) {
if(sw.value.widgetId === _this.$refs.widgets[i].value.widgetId){
_this.$refs.widgets[i].value.position.left = _this.moveWidgets[_this.$refs.widgets[i].value.widgetId].left + (_this.downX2 - _this.downX);
_this.$refs.widgets[i].value.position.top = _this.moveWidgets[_this.$refs.widgets[i].value.widgetId].top + (_this.downY2 - _this.downY);
}
}
});
},50);
}
},
upEvent(event){
console.log("upEvent")
if(this.selectFlag && this.selectedWidgets.length === 0 && this.rect !== null){
this.calculateMousePosition(event, false);
//计算选择框内的组件
const draggableArr = this.$refs.widgets;
for (let i = 0; i < draggableArr.length; i++) {
//判断组件是否在选择框内
let widget = this.$refs.widgets[i];
if(this.intersection(widget)){
this.selectedWidgets.push(widget);
widget.$refs.draggable.setActive(true);
}
}
this.selectFlag = false;
this.kuangSelectFlag = true; //框选结束的时候
}
if(this.rect){
document.getElementById("workbench").removeChild(this.rect);
this.rect = null;
}
if(this.openMulDrag){
this.mulWidgetMove(event);
this.openMulDrag = false;
}
},
//判断矩形框与组件是否相交
intersection(widget){
return (
(widget.value.position.left - this.downX) * (widget.value.position.left - this.downX2) < 0 ||
(widget.value.position.left + widget.value.position.width - this.downX) * (widget.value.position.left + widget.value.position.width- this.downX2) < 0
)
&&
(
(widget.value.position.top - this.downY) * (widget.value.position.top - this.downY2) < 0 ||
(widget.value.position.top + widget.value.position.height - this.downY) * (widget.value.position.top + widget.value.position.height- this.downY2) < 0
);
}
},
};
</script>
<style lang="scss">
@import "@/assets/styles/screen.scss";
</style>