Bladeren bron

!183 大屏设计多组件对齐和拖拽移动实现
Merge pull request !183 from JiangHH/dev

Foming 7 maanden geleden
bovenliggende
commit
e33e50cac1

+ 81 - 0
doc/docs/guide/community/JiangHH/AJ_Report大屏设计时多组件对齐和拖拽移动实现.md

@@ -0,0 +1,81 @@
+## 多组件对齐和拖拽移动功能
+
+注意前端版本 vue版本
+
+1.多组件选中实现
+2.对齐实现
+3.拖拽实现
+
+### 1、多组件选中
+1. Ctrl键
+2. 鼠标框选
+3. 组合
+
+#### 方式1  Ctrl键实现多选
+说明:
+
+1. 第一次单击组件,会默认把选中的组件加入到已选中的组件集合中.
+2. 按住Ctrl键选中的组件,会加入到已选中的组件集合中.
+3. 按住Ctrl键选中的组件,如果已选中的组件中包含该组件,则该组件取消选中.
+4. 点击大屏其他位置(非组件),会把选中的组件清空.
+
+测试截图:
+
+![](.\picture\img_01.png)
+
+#### 方式2  鼠标框选实现多选
+说明:
+
+1. 鼠标 (按下,移动,释放)生成矩形框,跟矩形框相交的组件会被选中.
+2. 组合方式多选的情况下,框选之前已选中的组件不会加入(除非框选也有).
+3. 框选的组件,也支持按住Ctrl键取消选中.
+4. 点击大屏其他位置(非组件),会把选中的组件清空.
+
+1. ​    鼠标按下,移动,释放,会生成一个矩形框,跟矩形框相交的组件爱你,会被选中.
+2. ​    2.组合方式多选的情况下,框选之前已选中的组件不会加入(除非框选也有).
+3. ​    3.框选的组件,也支持按住Ctrl键取消选中.
+4. ​    4.点击大屏其他位置(非组件),会把选中的组件清空.
+
+- ​    1.鼠标按下,移动,释放,会生成一个矩形框,跟矩形框相交的组件爱你,会被选中.
+
+测试截图:
+
+![](.\picture\img_02.png)
+
+![](.\picture\img_03.png)
+
+![](.\picture\img_04.png)
+
+### 2、多组件对齐
+
+单选右键菜单
+
+![](.\picture\img_05.png)
+
+多选右键菜单
+
+![](.\picture\img_06.png)
+
+#### 左对齐/右对齐/居中对齐
+
+选择左对齐 (以最上边的组件为标准对齐)---不合适自己可以修改代码
+
+![](.\picture\img_07.png)
+
+#### 上对齐/下对齐/居中对齐
+
+选择上对齐(以最左边的组件为标准对齐)----不合适自己可以修改代码
+
+![](.\picture\img_08.png)
+
+![](.\picture\img_09.png)
+
+### 3、多组件移动拖拽
+
+#### 多选状态
+
+![](.\picture\img_10.png)
+
+#### 拖拽后
+
+![](.\picture\img_11.png)

BIN
doc/docs/guide/community/JiangHH/picture/img_01.png


BIN
doc/docs/guide/community/JiangHH/picture/img_02.png


BIN
doc/docs/guide/community/JiangHH/picture/img_03.png


BIN
doc/docs/guide/community/JiangHH/picture/img_04.png


BIN
doc/docs/guide/community/JiangHH/picture/img_05.png


BIN
doc/docs/guide/community/JiangHH/picture/img_06.png


BIN
doc/docs/guide/community/JiangHH/picture/img_07.png


BIN
doc/docs/guide/community/JiangHH/picture/img_08.png


BIN
doc/docs/guide/community/JiangHH/picture/img_09.png


BIN
doc/docs/guide/community/JiangHH/picture/img_10.png


BIN
doc/docs/guide/community/JiangHH/picture/img_11.png


+ 74 - 0
report-ui/src/utils/screenMixins.js

@@ -258,6 +258,13 @@ const mixin = {
           display: "block",
         };
       }
+      //设置多选和单选的菜单项展示
+      document.getElementsByName("singleSelect").forEach(e=>{
+        e.style.display= this.selectedWidgets.length >= 2 ?"none":"block";
+      });
+      document.getElementsByName("mulSelect").forEach(e=>{
+        e.style.display= this.selectedWidgets.length >= 2 ?"block":"none";
+      })
       this.visibleContentMenu = true;
       return false;
     },
@@ -268,7 +275,11 @@ const mixin = {
     },
     // 删除
     deletelayer() {
+      let _this = this;
       this.widgets.splice(this.rightClickIndex, 1);
+      this.selectedWidgets.forEach(sw=>{
+        _this.widgets = _this.widgets.filter(w=>w.value.widgetId !== sw.value.widgetId);
+      })
     },
     // 锁定
     lockLayer() {
@@ -329,6 +340,69 @@ const mixin = {
       } else {
         this.widgets.unshift(this.widgets.splice(this.rightClickIndex, 1)[0]);
       }
+    },
+    //对齐
+    alignment(align) {
+      if(this.selectedWidgets.length <= 1) {
+        this.$message.error("请至少选择两个组件对齐");
+        return;
+      }
+      console.log("对齐方式:" + align);
+      let topWidget = this.selectedWidgets[0]; //最上组件(左右对齐使用)
+      let leftWidget = this.selectedWidgets[0]; //最左组件(上下对齐使用)
+      let minTop = this.selectedWidgets[0].value.position.top;
+      let minLeft = this.selectedWidgets[0].value.position.left;
+      //如果是框选的话,以【最上组件】【最左组件】为标准对齐;如果是Ctrl多选方式,则以第一个选中的组件为标准对齐,组合多选以框选逻辑为准
+      if(this.kuangSelectFlag){
+        for(let i = 0; i< this.selectedWidgets.length; i++){
+          let widget = this.selectedWidgets[i];
+          if( minTop > widget.value.position.top){
+            minTop = widget.value.position.top;
+            topWidget = widget;
+          }
+          if( minLeft > widget.value.position.left){
+            minLeft = widget.value.position.left;
+            leftWidget = widget;
+          }
+        }
+      }
+      for(let i = 0; i< this.selectedWidgets.length; i++){
+        let widget = this.selectedWidgets[i];
+        this.$refs.widgets.forEach(w=>{
+          if(w.value.widgetId === widget.value.widgetId){
+            w.$refs.draggable.setActive(false);
+          }
+        });
+        this.widgets.forEach(w=>{
+          if(w.value.widgetId === widget.value.widgetId){
+            switch (align){
+              case "left":  //左对齐
+                w.value.position.left = topWidget.value.position.left;
+                break;
+              case "right":  //右对齐
+                w.value.position.left = topWidget.value.position.left + topWidget.value.position.width - w.value.position.width;
+                break;
+              case "horizontal_center":  //左右居中对齐
+                w.value.position.left = topWidget.value.position.left + topWidget.value.position.width/2 - w.value.position.width /2;
+                break;
+              case "top":  //上对齐
+                w.value.position.top = leftWidget.value.position.top;
+                break;
+              case "bottom":  //下对齐
+                w.value.position.top = leftWidget.value.position.top + leftWidget.value.position.height - w.value.position.height;
+                break;
+              case "vertical_center":  //上下居中对齐
+                w.value.position.top = leftWidget.value.position.top + leftWidget.value.position.height/2 - w.value.position.height /2;
+                break;
+            }
+          }
+        });
+      }
+      this.selectedWidgets = [];
+      if(this.rect){
+        document.getElementById("workbench").removeChild(this.rect);
+      }
+      this.kuangSelectFlag = false;
     }
   }
 }

+ 28 - 7
report-ui/src/views/bigscreenDesigner/designer/components/contentMenu.vue

@@ -10,27 +10,45 @@
     <div class="contentmenu__item" @click="deleteLayer">
       <i class="iconfont iconguanbi"></i> 删除图层
     </div>
-    <div class="contentmenu__item" @click="lockLayer">
+    <div class="contentmenu__item" @click="lockLayer" name="singleSelect">
       <i class="iconfont iconfuzhi1"></i> 锁定图层
     </div>
-    <div class="contentmenu__item" @click="noLockLayer">
+    <div class="contentmenu__item" @click="noLockLayer" name="singleSelect">
       <i class="iconfont iconfuzhi1"></i> 解除锁定
     </div>
-    <div class="contentmenu__item" @click="copyLayer">
+    <div class="contentmenu__item" @click="copyLayer" name="singleSelect">
       <i class="iconfont iconfuzhi1"></i> 复制图层
     </div>
-    <div class="contentmenu__item" @click="istopLayer">
+    <div class="contentmenu__item" @click="istopLayer" name="singleSelect">
       <i class="iconfont iconjinlingyingcaiwangtubiao01"></i> 置顶图层
     </div>
-    <div class="contentmenu__item" @click="setlowLayer">
+    <div class="contentmenu__item" @click="setlowLayer" name="singleSelect">
       <i class="iconfont iconleft-copy"></i> 置底图层
     </div>
-    <div class="contentmenu__item" @click="moveupLayer">
+    <div class="contentmenu__item" @click="moveupLayer" name="singleSelect">
       <i class="iconfont iconjinlingyingcaiwangtubiao01"></i> 上移一层
     </div>
-    <div class="contentmenu__item" @click="movedownLayer">
+    <div class="contentmenu__item" @click="movedownLayer" name="singleSelect">
       <i class="iconfont iconleft-copy"></i> 下移一层
     </div>
+    <div class="contentmenu__item" @click="alignment('left')" name="mulSelect">
+      <i class="iconfont iconzhankai"></i> 左对齐
+    </div>
+    <div class="contentmenu__item" @click="alignment('right')" name="mulSelect">
+      <i class="iconfont iconzhankai"></i> 右对齐
+    </div>
+    <div class="contentmenu__item" @click="alignment('horizontal_center')" name="mulSelect">
+      <i class="iconfont iconzhankai"></i> 左右居中对齐
+    </div>
+    <div class="contentmenu__item" @click="alignment('top')" name="mulSelect">
+      <i class="iconfont iconzhankai"></i> 上对齐
+    </div>
+    <div class="contentmenu__item" @click="alignment('bottom')" name="mulSelect">
+      <i class="iconfont iconzhankai"></i> 下对齐
+    </div>
+    <div class="contentmenu__item" @click="alignment('vertical_center')" name="mulSelect">
+      <i class="iconfont iconzhankai"></i> 上下居中对齐
+    </div>
   </div>
 </template>
 <script>
@@ -96,6 +114,9 @@ export default {
     movedownLayer() {
       this.$emit("movedownLayer");
     },
+    alignment(align){
+      this.$emit("alignment",align);
+    }
   },
 };
 </script>

+ 211 - 5
report-ui/src/views/bigscreenDesigner/designer/index.vue

@@ -54,7 +54,7 @@
                 :key="'item' + index"
                 class="tools-item"
                 :class="widgetIndex == index ? 'is-active' : ''"
-                @click="layerClick(index)"
+                @click="layerClick($event,index)"
               >
                 <span class="tools-item-icon">
                   <i class="iconfont" :class="item.icon"></i>
@@ -275,6 +275,9 @@
               @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
@@ -288,7 +291,7 @@
                 :bigscreen="{ bigscreenWidth, bigscreenHeight }"
                 @onActivated="setOptionsOnClickWidget"
                 @contextmenu.prevent.native="rightClick($event, index)"
-                @mousedown.prevent.native="widgetsClick(index)"
+                @mousedown.prevent.native="widgetsClick($event, index)"
                 @mouseup.prevent.native="grade = false"
               />
             </div>
@@ -351,6 +354,7 @@
       @setlowLayer="setlowLayer"
       @moveupLayer="moveupLayer"
       @movedownLayer="movedownLayer"
+      @alignment="alignment($event)"
     />
   </div>
 </template>
@@ -416,6 +420,18 @@ export default {
       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属性
     };
   },
   computed: {
@@ -621,12 +637,20 @@ export default {
         });
       }
     },
-    layerClick(index) {
+    layerClick(event,index) {
       this.widgetIndex = index;
-      this.widgetsClick(index);
+      this.widgetsClick(event,index);
     },
     // 如果是点击大屏设计器中的底层,加载大屏底层属性
     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";
@@ -653,7 +677,42 @@ export default {
       });
       this.widgetOptions = this.deepClone(this.widgets[obj.index]["options"]);
     },
-    widgetsClick(index) {
+    widgetsClick(event,index) {
+      console.log("widgetsClick");
+      //判断是否按住了Ctrl按钮,表示Ctrl多选
+      let _this = this;
+      let eventWidget = event.currentTarget.__vue__.$parent;//vue3已经弃用__vue__
+      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.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) {
@@ -665,6 +724,15 @@ export default {
       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++) {
@@ -734,6 +802,144 @@ export default {
       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>