grid布局实现一个自定义可拖拽的页面

99 阅读2分钟

需求:因为是报表类的页面,客户要求一个可以自定义配置宽高、位置、可拖拽的页面

原理

使用到了jquery、jquery的插件gridster

1、通过document的id使用gridster去创建一个对象。

this.gridster = $(`#${this.bench_biz_id} ul`).gridster({
  namespace: `#${this.bench_biz_id}`,
  // 是否可以放大缩小单元格
  resize: {
    enabled: true,
    axes: ["both"],
    min_size: [1, 2],
    max_size: [1, 8]
  },
  min_cols: 4,
  // max_size_y: 4,
  // 模块的边距,第一个参数是上下边距,第二个参数是左右边距。
  widget_margins: [4, 4],
  // 模块的宽高 [宽,高]
  widget_base_dimensions: [172, 24],
  // 序列化
  // size_x: wgd.size_x, size_y: wgd.size_y,col: wgd.col, row: wgd.row
  serialize_params: function ($w, wgd) {
    // 返回序列化后widget的数据
    return {
      id: $(wgd.el).attr("data-key"),
      col: wgd.col,
      row: wgd.row,
      size_x: wgd.size_x,
      size_y: wgd.size_y
    };
  }
})

2、gridster.add_widget去绘制出对应的网格布局。

豆腐块的删除、新增通过gridster.remove_widgetgridster.add_widget去设置

拖拽、拉伸、移动完成后,再取gridster对象的值出来,需要数据持久化就可以调接口去存取数据 完整代码如下:

<template>
  <div>
    <tooltip content="自定义配置" placement="left">
      <icon name="android-apps" class="config-icon" size="35" @on-click="showMsgBox"></icon>
    </tooltip>
    <!-- 配置弹出框 -->
    <msgBox
      v-model="isShow"
      title="自定义配置"
      width="900"
      class-name="pad16"
      :closable="false"
      :mask-closable="false"
      top="50"
    >
      <div class="config">
        <div id="show-config" class="show-config">
          <div class="show-config-body">
            <checkbox v-for="portal in portals" :key="portal.id" v-model="portal.stats"
                        @on-change="togglePortal(portal)">
              {{ portal.name }}
            </checkbox>
          </div>
        </div>
        <div class="config-dashbroad">
          <div :id="bench_biz_id" class="gridster">
            <ul></ul>
          </div>
        </div>
      </div>
      <div slot="footer">
        <button type="ghost" @click="close">关闭</button>
        <button type="primary" @click="ok">确定</button>
      </div>
    </msgBox>
  </div>
</template>

<script>
// 引入组件
import "@trust-base-common/utils/gridster/jquery.gridster.css";
import "@trust-base-common/utils/gridster/jquery.gridster.js";
import $ from "jquery";
import { isFunction, cloneDeep } from "lodash-es";

export default {
  name: "PortalConfig",
  props: {
    // 配置的唯一id,必填
    bench_biz_id: {
      type: String,
      default: ""
    }
  },
  data() {
    return {
      // 临时测试数据,应通过接口获取配置项
      /*    col 数字:起始列
            row 数字: 起始行
            size_x 数字 : 占用的行数
            size_y 数字: 占用的列数
            min_rows 最小行数
            max_rows 最大行数
            "min_cols": 最小列数
            "max_cols": 最大列数
      */
      portals: [
        {
          "id": "expenditures",
          "name": "支出",
          "stats": true,
          "col": 1,
          "row": 1,
          "size_x": 4,
          "size_y": 4,
          "min_rows": 5,
          "min_cols": 1,
          "max_rows": 12,
          "max_cols": 4
        },
        {
          "id": "meetingSituation",
          "name": "上会情况",
          "stats": true,
          "col": 1,
          "row": 5,
          "size_x": 2,
          "size_y": 7,
          "min_rows": 5,
          "min_cols": 1,
          "max_rows": 11,
          "max_cols": 4
        },
        {
          "id": "riskProjectSituation",
          "name": "风险项目情况",
          "stats": true,
          "col": 3,
          "row": 5,
          "size_x": 2,
          "size_y": 8,
          "min_rows": 5,
          "min_cols": 1,
          "max_rows": 11,
          "max_cols": 4
        },
        {
          "id": "projectSituation",
          "name": "项目开展情况",
          "stats": true,
          "col": 1,
          "row": 14,
          "size_x": 2,
          "size_y": 3,
          "min_rows": 5,
          "min_cols": 1,
          "max_rows": 11,
          "max_cols": 4
        }
      ],
      isShow: false,
      gridster: {} // gridster对象,网格拖拽
    };
  },
  created() {
    this.configPortal();
    // 初始化时,抛出值的变更
    this.$emit("updateGrid", true, this.portals);
  },
  methods: {
    showMsgBox() {
      this.isShow = true;
      this.configPortal();
    },
    // 初始化表格栏栅
    initGridster() {
      this.gridster = $(`#${this.bench_biz_id} ul`).gridster({
        namespace: `#${this.bench_biz_id}`,
        // 是否可以放大缩小单元格
        resize: {
          enabled: true,
          axes: ["both"],
          min_size: [1, 2],
          max_size: [1, 8]
        },
        min_cols: 4,
        // max_size_y: 4,
        // 模块的边距,第一个参数是上下边距,第二个参数是左右边距。
        widget_margins: [4, 4],
        // 模块的宽高 [宽,高]
        widget_base_dimensions: [172, 24],
        // 序列化
        // size_x: wgd.size_x, size_y: wgd.size_y,col: wgd.col, row: wgd.row
        serialize_params: function ($w, wgd) {
          // 返回序列化后widget的数据
          return {
            id: $(wgd.el).attr("data-key"),
            col: wgd.col,
            row: wgd.row,
            size_x: wgd.size_x,
            size_y: wgd.size_y
          };
        }
      })
        .data("gridster");
      // 添加数据
      this.portals.forEach((item) => {
        if (item.stats) {
          const nodeModule = `<li data-key="${item.id}" class="${item.id}"><span>${item.name}</span></li>`;
          // col 数字:起始列  row 数字: 起始行  size_x 数字 :占用的行数  size_y 数字:占用的列数
          this.gridster.add_widget(nodeModule, item.size_x, item.size_y, item.col, item.row, [item.max_cols || 4, item.max_rows], [item.min_cols, item.min_rows]);
        }
      });
    },
    // 配置portal
    configPortal() {
      // 清空现有配置
      this.clear();
      this.portals = this.portals.filter((e) => e.stats);
      // todo 对接接口获取配置数据
      // 按行 从前到后排序存放
      this.portals.sort((a, b) => a.row - b.row);
      // 对列进行排序
      this.portals.sort((a, b) => a.row < b.row ? "" : a.col - b.col);
      this.$nextTick(this.initGridster);
    },
    // 设置需要展现portal窗口
    togglePortal(portal) {
      if (portal.stats) {
        const nodeModule = `<li data-key="${portal.id}" class="${portal.id}"><span>${portal.name}</span></li>`;
        this.gridster.add_widget(nodeModule, portal.size_x, portal.size_y, null, null, [4, portal.max_rows], [portal.min_cols, portal.min_rows]);
      } else {
        for (const i in $(`#${this.bench_biz_id} li`)) {
          if ($(`#${this.bench_biz_id} li`).eq(i)
            .attr("data-key") === portal.id) {
            this.gridster.remove_widget($(`#${this.bench_biz_id} li`).eq(i));
          }
        }
      }
    },
    // 确定按钮,保存数据,更新模块
    ok() {
      // 预处理数据格式,将grid表格数据记录下来
      const newConfig = this.gridster.serialize();
      let portals = cloneDeep(this.portals);
      // eslint-disable-next-line guard-for-in
      for (const i in portals) {
        for (const j in newConfig) {
          if (newConfig[j].id === portals[i].id) {
            portals[i].col = newConfig[j].col;
            portals[i].row = newConfig[j].row;
            portals[i].size_x = newConfig[j].size_x;
            portals[i].size_y = newConfig[j].size_y;
            break;
          }
        }
      }
      this.portals = portals;
      // todo 对接接口保存配置数据
      // 通知刷新
      this.$emit("updateGrid", true, this.portals);
      this.isShow = false;
      this.clear();
    },
    // 取消按钮
    close(change) {
      this.isShow = false;
      this.$emit("updateGrid", change, this.portals);
      this.clear();
    },
    // 清空
    clear() {
      this.gridster && isFunction(this.gridster.remove_all_widgets) && this.gridster.remove_all_widgets();
    }
  }
};
</script>
<style lang="scss" scoped>
/deep/ .pad16 .h-modal-body {
  padding: 16px 16px 0;
}

.header {
  height: 40px;
  margin-bottom: 8px;
  font-size: 14px;
  font-weight: bold;

  &-title {
    width: 250px;
    display: inline-block;
  }

  /deep/ .h-radio-group-button .h-radio-wrapper {
    height: 28px;
    line-height: 26px;
  }
}

/* 是否显示配置 */
.show-config {
  display: inline-block;
  width: 105px;
  vertical-align: top;
  margin-right: 4px;
  overflow-x: hidden;

  &-title {
    font-weight: bold;
    padding-bottom: 8px;
    border-bottom: 1px #d9d9d9 solid;
    margin-bottom: 8px;
    font-size: 14px;

    &::before {
      content: "";
      height: 16px;
      background: #4686f2;
      width: 4px;
      display: inline-block;
      vertical-align: text-bottom;
      margin-right: 4px;
    }
  }

  &-body {
    padding-bottom: 12px;
  }
}

.show-config .h-checkbox-wrapper {
  display: inline-block;
  padding: 5px;
}

.config {
  height: 402px;
  width: 100%;

  &-dashbroad {
    display: inline-block;
    width: calc(100% - 116px);
    min-height: 100%;
    padding: 4px 8px;
    background: #f5f5f5;
    border: 1px #d9d9d9 solid;
    height: 100%;
    overflow: auto;
  }
}

.gridster {
  position: relative;
  display: inline-block;
  vertical-align: top;
  overflow-x: hidden;
  overflow-y: hidden;
  width: 100%;
  min-height: 90px;
  padding: 4px 2px;

}

.gridster > ul {
  list-style: none;
  padding: 0;
  margin: 0;
}

.config-icon {
  background-color: white;
  color: #4686f2;
  border-radius: 50%;
}

</style>
<style lang="scss">

/* 正常状态 */
.gridster {
  .gs-w {
    background: white;
    cursor: move;
    box-shadow: 0 1px 6px rgba(0, 0, 0, 0.15);
    border-radius: 3px;
    padding: 8px;
    font-size: 12px;
    font-weight: bold;
    overflow: hidden;
    color: #666;
  }

  .gs-w:hover {
    box-shadow: 0 3px 18px rgba(0, 0, 0, 0.3);
    color: #4686f2;
  }
}

.gsw-list-body {
  height: calc(80% + 5px);
  overflow: hidden;

  .gsw-list {
    margin: 20px auto;
  }
}

</style>

使用时,可以用动态组件去加载

<template>
  <div class="grid-main-container">
    <div class="grid-container">
      <!--动态布局-->
      <div v-for="item in portals" :key="item.id" minH="50" class="grid-item"
           :style=" `grid-column-start: ${item.col};  grid-column-end: ${item.col+item.size_x};  grid-row-start: ${item.row};  grid-row-end: ${item.row+item.size_y};`">
        <component
          :is="item.id"
          :portal="item"
          class="panel-box"></component>
      </div>
    </div>
    <!--配置组件-->
    <portalConfig
      ref="portalConfig"
      class="h-main-tooltip"
      :bench_biz_id="bench_biz_id"
      @updateGrid="updateGrid">
    </portalConfig>
  </div>
</template>
<script>
// 配置组件
import portalConfig from "./index.vue";
// 业务组件
import expenditures from "../components/expenditures";
import meetingSituation from "../components/meetingSituation";
import riskProjectSituation from "../components/riskProjectSituation";
import projectSituation from "../components/projectSituation";
export default {
  name: "HsfundAmopPrdDataVisualization",
  data() {
    return {
      portals: [],
      bench_biz_id: "vi" // 配置组件唯一id
    };
  },
  components: {
    portalConfig,
    expenditures,
    meetingSituation,
    riskProjectSituation,
    projectSituation
  },
  methods: {
    // 通知更新布局
    updateGrid(change, portals) {
      change && (this.portals = portals);
    }
  },
  created() {
    // this.getPortals();
  }
};
</script>

<style lang="scss" scoped>
.h-main-tooltip {
  position: fixed;
  right: 25px;
  bottom: 15px;
  z-index: 20;
}

/* 公共外框和标题 */
.panel-box {
  background: #fff;
  box-shadow: 0 1px 4px 0 rgba(137, 150, 173, 0.42);
  border-radius: 4px;
  margin-bottom: 8px;
  height: 100%;
  width: 100%;
  overflow: auto;
  padding: 16px;
}

.h-tooltip-inner {
  font-size: 12px;
  min-height: 25px;
  padding: 3px 7px;
}

.h-tooltip-inner {
  font-size: 12px;
  min-height: 25px;
  padding: 3px 7px;
}

.main-tooltip {
  position: absolute;
  top: 60px;
  left: 0;
  width: 38px;
  display: none;
}

.clear {
  clear: both;
}

.h-modal-header {
  border-bottom: 1px solid #ddd;
}

.h-modal-footer {
  border-top: 0;
  padding: 12px 20px;
}

.panel-box .h-table-tip {
  height: 100% !important;
}

$gap: 8px; //grid网格间距
.grid-container {
  display: grid;
  justify-content: start;
  width: calc(100% - 3 * #{$gap});
  min-height: inherit;
  grid-template-columns: 25% 25% 25% 25%;
  grid-auto-rows: 50px;
  grid-row-gap: $gap;
  grid-column-gap: $gap;
}

.grid-main-container {
  background: #f7f7f7;
  height: 100%;
}
</style>

效果

image.png

image.png