image-20211125092911200

这里对子组件进行了封装,若不需要去掉拖拽函数中的 $children[0] 即可。

// 例如
this.$refs.gridlayout.$children[this.layout.length].$children[0].style.display = "none";
// ==>
this.$refs.gridlayout.$children[this.layout.length].style.display = "none";

以下是完整案例:

<template>
  <grid-item
      style="padding: 15px 20px 20px 20px;background-color: #fafafa;"
      :i="layout.i"
      :x="layout.x"
      :y="layout.y"
      :w="layout.w"
      :h="layout.h"
      :minW="layout.minW"
      :minH="layout.minH"
      @moved="moved"
      @resized="resized"
  >
    <!-- @formatter:off -->
    <span class="vue-remove-handle" @click="remove"><a-icon type="close" /></span>
    <!-- 折线图 -->
    <echarts-line    ref="line"    v-if="layout.t==='line'"    :i="layout.i"></echarts-line>
    <!-- 柱状图 -->
    <echarts-bar     ref="bar"     v-if="layout.t==='bar'"     :i="layout.i"></echarts-bar>
    <!-- 散点图 -->
    <echarts-scatter ref="scatter" v-if="layout.t==='scatter'" :i="layout.i"></echarts-scatter>
    <!-- K-线图 -->
    <echarts-k-line  ref="k-line"  v-if="layout.t==='k-line'"  :i="layout.i"></echarts-k-line>
    <!-- 饼状图 -->
    <echarts-pie     ref="pie"     v-if="layout.t==='pie'"     :i="layout.i"></echarts-pie>
    <!-- 雷达图 -->
    <echarts-radar   ref="radar"   v-if="layout.t==='radar'"   :i="layout.i"></echarts-radar>
    <!-- 仪表盘 -->
    <echarts-gauge   ref="gauge"   v-if="layout.t==='gauge'"   :i="layout.i"></echarts-gauge>
    <!-- 漏斗图 -->
    <echarts-funnel  ref="funnel"  v-if="layout.t==='funnel'"  :i="layout.i"></echarts-funnel>
    <!-- 中国地图 -->
    <echarts-map     ref="map"     v-if="layout.t==='map'"     :i="layout.i"></echarts-map>
    <!-- @formatter:on -->
  </grid-item>
</template>

<script>
import {GridItem} from "vue-grid-layout"
import EchartsLine from "./EchartsLine"
import EchartsBar from "./EchartsBar"
import EchartsScatter from "./EchartsScatter"
import EchartsKLine from "./EchartsKLine"
import EchartsPie from "./EchartsPie"
import EchartsGauge from "./EchartsGauge"
import EchartsRadar from "./EchartsRadar"
import EchartsFunnel from "./EchartsFunnel"
import EchartsMap from "./EchartsMap"

export default {
  name: "CustomGridItem",
  props: {
    layout: {
      type: Object,
      required: false
    }
  },
  components: {
    GridItem,
    EchartsLine,    // 折线图
    EchartsBar,     // 柱状图
    EchartsScatter, // 散点图
    EchartsKLine,   // K-线图
    EchartsPie,     // 饼状图
    EchartsRadar,   // 雷达图
    EchartsGauge,   // 仪表盘
    EchartsFunnel,  // 漏斗图
    EchartsMap,     // 中国地图
  },
  methods: {
    remove() {
      this.$emit('remove', this.layout.i);
    },
    // 放大缩小结束时触发
    resized(i, w, h) {
      this.$refs[this.layout.t].resized();
      this.$emit('resized', i, w, h);
    },
    // 移动结束触发
    moved(i, x, y) {
      this.$emit('moved', i, x, y);
    }
  }
}
</script>

<style scoped>
/* @formatter:off */
.vue-grid-item:not(.vue-grid-placeholder){background:#ffffff;}
.vue-draggable-handle{position:absolute;width:20px;height:20px;top:0;left:0;background:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10'><circle cx='5' cy='5' r='5' fill='#999999'/></svg>") no-repeat;background-position:bottom right;padding:0 8px 8px 0;background-repeat:no-repeat;background-origin:content-box;box-sizing:border-box;cursor:pointer;}
.vue-remove-handle{position:absolute;font-size:10px;width:20px;height:20px;top:0;right:0;background-position:100% 100%;background-repeat:no-repeat;background-origin:content-box;-webkit-box-sizing:border-box;box-sizing:border-box;cursor:pointer;}
.vue-remove-handle i{position:absolute;right:3px;top:3px;cursor:pointer;}
.vue-grid-item .resizing{opacity:0.9;}
.vue-grid-item .static{background:#cce;}
.vue-grid-item .text{font-size:24px;text-align:center;position:absolute;top:0;bottom:0;left:0;right:0;margin:auto;height:100%;width:100%;}
.vue-grid-item .no-drag{height:100%;width:100%;}
.vue-grid-item .minMax{font-size:12px;}
</style>
<template>
  <div class="gutter">
    <a-row :gutter="16">
      <!-- 折叠面板 -->
      <a-col class="gutter-row" :span="5">
        <div class="gutter-box">
          <a-collapse v-model="activeKey">
            <a-collapse-panel key="1" header="统计图表">
              <!-- 组件生成按钮(可点击与拖拽) -->
              <a-button v-for="(item, index) in charts" :key="index"
                        :icon="item.icon"
                        draggable="true"
                        unselectable="on"
                        @drag="drag(item)"
                        @dragend="dragend(item)"
                        @click="addItem(item)"
              >{{ item.n }}
              </a-button>
            </a-collapse-panel>
          </a-collapse>
        </div>
      </a-col>
      <!-- 拖拽布局容器 -->
      <a-col class="gutter-row" :span="18">
        <div class="gutter-box drag-content" id="content">
          <grid-layout
              ref="gridlayout"
              :layout.sync="layout"
              :col-num="grid.colNum"
              :row-height="grid.rowHeight"
              :max-rows="grid.maxRows"
              :is-draggable="grid.draggable"
              :is-resizable="grid.resizable"
              :is-mirrored="grid.isMirrored"
              :vertical-compact="grid.verticalCompact"
              :margin="grid.margin"
              :use-css-transforms="grid.useCssTransforms"
              @layout-updated="layoutUpdatedEvent"
              @layout-ready="layoutReadyEvent"
          >
            <!-- 布局组件 -->
            <custom-grid-item
                v-for="item in layout" :key="item.i"
                :layout="item"
                @moved="itemMoved"
                @resized="itemResized"
                @remove="removeItem"
            ></custom-grid-item>
          </grid-layout>
        </div>
      </a-col>
    </a-row>
  </div>
</template>

<script>
import {GridLayout} from "vue-grid-layout"
import CustomGridItem from "./module/CustomGridItem"

export default {
  components: {
    GridLayout,
    CustomGridItem
  },
  data() {
    return {
      mouseXY: {x: 0, y: 0}, // 拖拽鼠标定位
      DragPos: {x: 0, y: 0}, // 拖拽组件定位
      // 拖拽布局组件信息
      layout: [
        {i: this.getUUID(3), x: 0, y: 0, w: 3, h: 2, minW: 2, minH: 1, t: 'line'},
      ],
      // 拖拽布局参数
      grid: {
        colNum: 6,              // 网格列数
        maxRows: 20,            // 表示网格中最大行数
        rowHeight: 150,         // 网格每行的高度(以像素为单位)数字
        margin: [10, 10],       // 网格之间的边距
        isMirrored: false,      // RTL/LTR 的转换
        useCssTransforms: true, // 是否使用 `css` 的 `transforms` 来排版
        verticalCompact: true,  // 垂直方向上是否应该紧凑布局
        draggable: true,        // 组件是否可以拖拽
        resizable: true,        // 组件是否可以调整大小
      },
      // 可拖拽图表整理
      charts: [
        {t: 'line', icon: 'line-chart', n: '折线图', w: 3, h: 2, minW: 2, minH: 1},
        {t: 'bar', icon: 'bar-chart', n: '柱状图', w: 3, h: 2, minW: 2, minH: 1},
        {t: 'scatter', icon: 'dot-chart', n: '散点图', w: 3, h: 2, minW: 2, minH: 1},
        {t: 'k-line', icon: 'sliders', n: 'K-线图', w: 3, h: 2, minW: 2, minH: 1},
        {t: 'pie', icon: 'pie-chart', n: '饼状图', w: 3, h: 2, minW: 2, minH: 1},
        {t: 'radar', icon: 'radar-chart', n: '雷达图', w: 3, h: 2, minW: 2, minH: 1},
        {t: 'gauge', icon: 'fund', n: '仪表盘', w: 3, h: 2, minW: 2, minH: 1},
        {t: 'funnel', icon: 'heat-map', n: '漏斗图', w: 3, h: 2, minW: 2, minH: 1},
        {t: 'map', icon: 'area', n: '中国地图', w: 6, h: 4, minW: 3, minH: 2},
      ],
      activeKey: ['1'],     // 折叠面板状态,默认展开
    };
  },
  mounted() {
    const _this = this;
    // 监听拖拽事件,实时获取鼠标定位
    document.addEventListener("dragover", function (e) {
      _this.mouseXY.x = e.clientX;
      _this.mouseXY.y = e.clientY;
    }, false);
  },
  methods: {
    // 点击添加新组件(空位添加新元素)
    addItem(item) {
      // 初始化元素
      let newItem = {
        i: this.getUUID(3),
        x: 0,
        y: 0,
        w: item.w,
        h: item.h,
        minW: item.minW,
        minH: item.minH,
        t: item.t
      }
      // 确定边界
      let Ys = [], maxX = 0, maxY = 0, edgeX = 0, edgeY = 0
      this.layout.map(l => {
        Ys.push(l.y + l.h)
      })
      maxY = Ys.length && Math.max.apply(null, Ys) || 1
      edgeX = this.grid.colNum
      edgeY = maxY
      // 使用二维数组生成地图
      let gridMap = []
      for (let x = 0; x < edgeX; x++) {
        gridMap[x] = []
        for (let y = 0; y < edgeY; y++) {
          gridMap[x][y] = 0
        }
      }
      // 标记占位
      this.layout.map(l => {
        // 将layout中卡片所占区域标记为1
        for (let x = l.x; x < (l.x + l.w); x++) {
          for (let y = l.y; y < (l.y + l.h); y++) {
            gridMap[x][y] = 1
          }
        }
      })
      // 遍历地图,申请位置
      for (let y = 0; y < edgeY; y++) {
        for (let x = 0; x < edgeX; x++) {
          // 申请所需空间
          if (edgeX - x >= item.w && edgeY - y >= item.h) {
            let itemSignArr = []
            for (let a = x; a < (x + item.w); a++) {
              for (let b = y; b < (y + item.h); b++) {
                itemSignArr.push(gridMap[x][y])
              }
            }
            if (itemSignArr.indexOf(1) < 0) {
              newItem.x = x
              newItem.y = y
              this.layout.push(newItem)
              return
            }
          }
        }
      }
      // 无满足条件
      newItem.x = 0
      newItem.y = edgeY + 1
      this.layout.push(newItem)
    },
    // 组件更新完成生命周期
    layoutReadyEvent(newLayout) {
      // console.log("Ready", this.layout);
    },
    // 布局更新时间
    layoutUpdatedEvent() {
      // console.log("Updated", this.layout);
    },
    // 移除组件
    removeItem(i) {
      // console.log('Destroy ', i)
      if (i) {
        // 根据id移除组件
        const layout = this.layout.filter(l => l.i === i);
        const index = this.layout.indexOf(layout[0]);
        index > -1 && this.layout.splice(index, 1);
      }
    },
    // 组件放大缩小结束时触发
    itemResized(i, w, h) {
      console.log('Resized', i, 'layout:' + 'w:' + w + ', h:' + h)
    },
    // 组件移动结束触发
    itemMoved(i, x, y) {
      console.log('Moved', i, 'layout:' + 'x:' + x + ', y:' + y)
    },
    // 拖拽生产新组件 - 拖拽回调事件
    drag(item) {
      let mouseInGrid = false;
      // 获取拖拽组件父容器定位数据
      let parentRect = document.getElementById('content').getBoundingClientRect();
      // 检测鼠标是否到容器内部
      if (((this.mouseXY.x > parentRect.left) && (this.mouseXY.x < parentRect.right)) &&
          ((this.mouseXY.y > parentRect.top) && (this.mouseXY.y < parentRect.bottom))) {
        mouseInGrid = true;
      }
      // 检测是否有临时拖拽演示组件,若无则添加到最后
      let index = this.layout.findIndex(item => item.i === 'drop');
      if (mouseInGrid === true && index === -1) {
        this.layout.push({
          i: 'drop',
          x: (this.layout.length * 2) % (this.grid.colNum || 12),
          y: this.layout.length + (this.grid.colNum || 12),
          w: item.w,
          h: item.h,
        });
        // 再次检测是否有临时拖拽演示组件
        index = this.layout.findIndex(item => item.i === 'drop');
      }
      // 当检测临时拖拽演示组件不为空时,根据鼠标移动实时更新组件定位信息以达到拖拽演示效果
      if (index !== -1) {
        try {
          // 拖拽时隐藏主体内容,只显示拖拽留影
          this.$refs.gridlayout.$children[this.layout.length].$children[0].style.display = "none";
        } catch {
        }
        // 获取临时拖拽演示组件对象
        let el = this.$refs.gridlayout.$children[index];
        // 拖拽定位变更
        el.dragging = {"top": this.mouseXY.y - parentRect.top, "left": this.mouseXY.x - parentRect.left};
        // 计算获取容器内定位
        let new_pos = (el.calcXY ? el : el.$children[0]).calcXY(
            this.mouseXY.y - parentRect.top - (parentRect.height / 4),
            this.mouseXY.x - parentRect.left - (parentRect.width / 4),
        );
        // 鼠标移入容器时更新容器内组件定位
        if (mouseInGrid === true) {
          this.$refs.gridlayout.dragEvent('dragstart', 'drop', new_pos.x, new_pos.y, item.h, item.w);
          this.DragPos.i = String(index);
          this.DragPos.x = this.layout[index].x;
          this.DragPos.y = this.layout[index].y;
        }
        // 鼠标移出容器时移除演示组件
        if (mouseInGrid === false) {
          this.$refs.gridlayout.dragEvent('dragend', 'drop', new_pos.x, new_pos.y, item.h, item.w);
          this.layout = this.layout.filter(obj => obj.i !== 'drop');
        }
      }
    },
    // 拖拽生产新组件 - 拖拽结束回调事件
    dragend(item) {
      let mouseInGrid = false;
      // 获取拖拽组件父容器定位数据
      let parentRect = document.getElementById('content').getBoundingClientRect();
      // 检测鼠标是否到容器内部
      if (((this.mouseXY.x > parentRect.left) && (this.mouseXY.x < parentRect.right)) &&
          ((this.mouseXY.y > parentRect.top) && (this.mouseXY.y < parentRect.bottom))) {
        mouseInGrid = true;
      }
      // 拖拽结束时鼠标在容器内部时
      if (mouseInGrid === true) {
        this.$refs.gridlayout.dragEvent('dragend', 'drop', this.DragPos.x, this.DragPos.y, item.h, item.w);
        // 移除演示组件
        this.layout = this.layout.filter(obj => obj.i !== 'drop');
        // 添加一个正式的拖拽组件
        this.layout.push({
          i: this.getUUID(3),
          x: this.DragPos.x,
          y: this.DragPos.y,
          w: item.w,
          h: item.h,
          minW: item.minW,
          minH: item.minH,
          t: item.t,
        });
      }
    },
    // 生成一个不重复的ID
    getUUID(randomLength) {
      return Number(Math.random().toString().substr(2, randomLength) + Date.now()).toString(36)
    }
  }
};
</script>

<style scoped>
/* @formatter:off */
*{touch-action:none;}
.gutter >>> .ant-row > div{background:transparent;border:0;}
.gutter-box{color:rgba(0,0,0,0.65);background-color:#fff;min-height:600px;border:1px solid #ccc;overflow:hidden;padding:5px;}
.ant-collapse-content .ant-btn{width:102px;margin:5px;cursor:move;}
.vue-grid-layout{background:#ffffff;}
</style>