Bootstrap

draggable + grid 拖拽插件 + 网格布局 动态生成首页模版

背景:

        1、首页模板由多个子组件组成,如图表、新闻、公告、轮播图等,一般都由前端引入子组件,写在固定的位置上,最终形成一个固定的首页模板;

        2、像这样直接在代码中写死的首页没有灵活性,不同用户想展示出来的首页模板千篇一律;

        3、若是首页模板可以根据用户自己的需求拖拽生成,首页模板的灵活性与交互性将会两者兼备。

描述:

        1、通过使用draggable拖拽插件可实现自由拖拽子组件;

        【拖拽插件 draggable 详解及意见 - 掘金

        2、draggable可实现左右拖拽,若想完全根据用户的意愿上下左右都可拖拽,在这里我使用的是grid网格布局实现相应功能。

        【CSS Grid 网格布局教程 - 阮一峰的网络日志

测试案例展示:

        1)左边为子组件列表,暂时使用色块代表可组成首页的所有组件;

        2)右边为拖拽区域,拖拽区域由grid网格布局作为背景板,子组件可根据手动设置的网格布局的大小进行放置;

        3)拖拽后的子组件提供了编辑、删除、拖拽三项功能,编辑可控制该子组件的宽高比例,删除后则回到左部组件列表中,拖拽可实现各个子组件之间的位置交换。

代码展示:

一、子组件列表

        循环dataList拿到子组件对象数组,鼠标放置时增加遮罩层,显示该子组件的宽高比例、名称;draggable是可拖拽的子组件列表区域,通过dragOptions1进行对拖拽区域内容的控制。

<draggable v-bind="dragOptions1" class="left" v-model="dataList">
   <div
      :data-width="item.width"
      :data-height="item.height"
      :class="[
         'item',
         'item_width_' + item.width,
         'item_height_' + item.height,
         'item_' + item.component
      ]"
      v-for="(item, index) in dataList"
      :key="index"
     >
      <div class="cover">
         <span>{{ item.title }}{{ item.width }}×{{ item.height }}</span>
      </div>
   </div>
</draggable>


dragOptions1() {
  return {
      animation: 300,
      group: {
          name:'group',//名称相同的分组可以相互拖拽
          pull:true, //是否允许拖出当前组
          put:true //是否允许拖入当前组
      },
      chosenClass: "sortable-btn",
      forceFallback: true,
      handle: ".item" // 只有按住拖动手柄才能使列表单元进行拖动
  }
},

二、拖拽区域展示

        dataList2存放从dataList中拖拽出来的子组件数据信息,同样增加遮罩层moveDialog,展示子组件的功能按钮:编辑、删除。

<draggable v-bind="dragOptions2" class="rightBox" :list="dataList2">
  <div
    :class="[
      'item',
      'item_width_' + item.width,
      'item_height_' + item.height,
    ]"
    v-for="(item, index) in dataList2"
    :key="index"
  >
    <div class="moveDialog">
      <Button type="primary" size="small" class="edit" @click="edit(item)">编 辑</Button>
      <Button type="primary" size="small" class="del" @click="del(item)">删 除</Button>
    </div>
    <components :ref='item.component' :is="item.component" style="pointer-events: none;cursor: default;"></components>
  </div>
</draggable>


dragOptions2() {
  return {
    animation: 300,
    group: {
      name:'group',
      pull:false,
      put:true
    },
    ghostClass: 'ghost',
    forceFallback: true,
    handle: ".moveDialog"
  }
}

 三、编辑、删除功能

        编辑在这里只写了控制子组件的宽高比例、删除则会将已经拖拽的组件撤回到子组件列表中,具体代码实现如下:

<Modal
  v-model="editModal"
  class="edit-modal"
  :transfer='false'
  :title="`编辑组件信息`"
  @on-ok="submitFormEdit"
  @on-cancel="handleCancelEdit"
  :mask-closable="false">
    <Form ref="formValidate" :model="formValidate" :label-width="100" :rules="ruleValidate">
        <p style="color:gray;font-size:12px;margin:0 0 5px 100px">*(推荐宽度比例:4、8、12、16。)</p>
        <FormItem label="宽度比例:" prop="width">
            <InputNumber v-model="formValidate.width" placeholder="请输入宽度比例"></InputNumber>
        </FormItem>
        <p style="color:gray;font-size:12px;margin:0 0 5px 100px">*(建议高度比例:大于等于当前默认高度。)</p>
        <FormItem label="高度比例:" prop="height">
            <InputNumber v-model="formValidate.height" placeholder="请输入高度比例"></InputNumber>
        </FormItem>
    </Form>
    <p style="color:red;font-size:12px;">*(提示:以上给出的是较为合适的宽高比例,可酌情调整,不建议暴力调整比例。)</p>
    <div slot="footer" align="right">
        <Button @click="handleCancelEdit">取 消</Button>
        <Button type="primary" @click="submitFormEdit">确 定</Button>
    </div>
</Modal>


// 编辑按钮
edit(row) {
    this.editModal = true
    this.formValidate = JSON.parse(JSON.stringify(row))
},
// 删除按钮
del(data){
  let dataList2 = JSON.parse(JSON.stringify(this.dataList2))
  this.dataList2 = dataList2.filter(item => item.chartId !== data.chartId)
  this.dataList.push(data)
},
// 编辑弹窗确定按钮
submitFormEdit: function () {
  this.$refs['formValidate'].validate((valid) => {
    if (valid) {
      this.dataList2.map(item => {
        if(item.chartId === this.formValidate.chartId){
          item.width = this.formValidate.width
          item.height = this.formValidate.height
        }
        return item
      })
      this.handleCancelEdit()
      this.$Message.success('编辑成功!')
    } else {
      console.log('检验不通过!');
    }
  })
},
// 编辑弹窗取消按钮
handleCancelEdit() {
  this.editModal = false
  this.$refs.formValidate.resetFields()
  this.formValidate = {
    width: 4,
    height: 1
  }
}

四、拖拽插件的引入、使用及网格布局的使用

(在这里就不过多解释啦~,参考下面提示的文章了解draggable的使用和grid的相关概念)

 【拖拽插件 draggable 详解及意见 - 掘金】 【CSS Grid 网格布局教程 - 阮一峰的网络日志

全部代码实现:

<template>
  <div class='test'>
    <main>
      <draggable v-bind="dragOptions1" class="left" v-model="dataList">
        <div
          :data-width="item.width"
          :data-height="item.height"
          :class="[
            'item',
            'item_width_' + item.width,
            'item_height_' + item.height,
            'item_' + item.component
          ]"
          v-for="(item, index) in dataList"
          :key="index"
        >
          <div class="cover">
            <span>{{ item.title }}{{ item.width }}×{{ item.height }}</span>
          </div>
        </div>
      </draggable>
      <div class="right">
        <draggable v-bind="dragOptions2" class="rightBox" :list="dataList2">
          <div
            :class="[
              'item',
              'item_width_' + item.width,
              'item_height_' + item.height,
            ]"
            v-for="(item, index) in dataList2"
            :key="index"
          >
            <div class="moveDialog">
              <Button type="primary" size="small" class="edit" @click="edit(item)">编 辑</Button>
              <Button type="primary" size="small" class="del" @click="del(item)">删 除</Button>
            </div>
            <components :ref='item.component' :is="item.component" style="pointer-events: none;cursor: default;"></components>
          </div>
        </draggable>
      </div>
    </main>
    <Modal
      v-model="editModal"
      class="edit-modal"
      :transfer='false'
      :title="`编辑组件信息`"
      @on-ok="submitFormEdit"
      @on-cancel="handleCancelEdit"
      :mask-closable="false">
        <Form ref="formValidate" :model="formValidate" :label-width="100" :rules="ruleValidate">
            <p style="color:gray;font-size:12px;margin:0 0 5px 100px">*(推荐宽度比例:4、8、12、16。)</p>
            <FormItem label="宽度比例:" prop="width">
                <InputNumber v-model="formValidate.width" placeholder="请输入宽度比例"></InputNumber>
            </FormItem>
            <p style="color:gray;font-size:12px;margin:0 0 5px 100px">*(建议高度比例:大于等于当前默认高度。)</p>
            <FormItem label="高度比例:" prop="height">
                <InputNumber v-model="formValidate.height" placeholder="请输入高度比例"></InputNumber>
            </FormItem>
        </Form>
        <p style="color:red;font-size:12px;">*(提示:以上给出的是较为合适的宽高比例,可酌情调整,不建议暴力调整比例。)</p>
        <div slot="footer" align="right">
            <Button @click="handleCancelEdit">取 消</Button>
            <Button type="primary" @click="submitFormEdit">确 定</Button>
        </div>
    </Modal>
  </div>
</template>
<script>
import draggable from "vuedraggable";
import red from "./test/red.vue";
import orange from "./test/orange.vue";
import yellow from "./test/yellow.vue";
import green from "./test/green.vue";
import bfa from "./test/bfa.vue";
import blue from "./test/blue.vue";
import purple from "./test/purper.vue";
import pink from "./test/pink.vue";
import fff from "./test/fff.vue";
import black from "./test/black.vue";
export default {
 name:'test',
  components: {
  red,
  orange,
  yellow,
  green,
  bfa,
  blue,
  purple,
  pink,
  fff,
  black,
  draggable
  },
  data () {
    const widthRule = (rule, value, callback) => {
      if(!value){
        callback(new Error('请输入宽度比例!'))
      }else if(!/^[1-9]([0-9])*$/g.test(value)){
        callback(new Error('只能输入正整数!'))
      }else if(value < 4){
        callback(new Error('宽度比例不能小于4!'))
      }else if(value > 16){
        callback(new Error('宽度比例不能大于16!'))
      }else{
        callback()
      }
    }
    const heightRule = (rule, value, callback) => {
      if(!value){
        callback(new Error('请输入高度比例!'))
      }else if(!/^[1-9]([0-9])*$/g.test(value)){
        callback(new Error('只能输入正整数!'))
      }else{
        callback()
      }
    }
    return {
      // 拖拽总数据
      dataList: [
        {
          chartId: '1',
          title: "红色",
          name: "red",
          width: 4,
          height: 2,
          component: "red",
        },
        {
          chartId: '2',
          title: "橙色",
          name: "orange",
          width: 12,
          height: 2,
          component: "orange",
        },
        {
          chartId: '3',
          title: "黄色",
          name: "yellow",
          width: 16,
          height: 1,
          component: "yellow",
        },
        {
          chartId: '4',
          title: "绿色",
          name: "green",
          width: 4,
          height: 6,
          component: "green",
        },
        {
          chartId: '5',
          title: "青色",
          name: "bfa",
          width: 4,
          height: 6,
          component: "bfa",
        },
        {
          chartId: '6',
          title: "蓝色",
          name: "blue",
          width: 4,
          height: 3,
          component: "blue",
        },
        {
          chartId: '7',
          title: "紫色",
          name: "purple",
          width: 8,
          height: 3,
          component: "purple",
        },
        {
          chartId: '8',
          title: "粉色",
          name: "pink",
          width: 4,
          height: 3,
          component: "pink",
        },
        {
          chartId: '9',
          title: "白色",
          name: "fff",
          width: 12,
          height: 3,
          component: "fff",
        },
        {
          chartId: '10',
          title: "黑色",
          name: "black",
          width: 4,
          height: 3,
          component: "black",
        }
      ],
      // 生成页面数据 
      dataList2: [],
      droplist: [],
      // 编辑弹窗
      editModal: false,
      // 编辑表单
      formValidate: {
        width: 4,
        height: 1
      },
      // 编辑表单限制
      ruleValidate: {
        width: [
          { required: true, validator: widthRule }
        ],
        height: [
          { required: true, validator: heightRule }
        ]
      }
     }
  },
  computed: {
    dragOptions1() {
      return {
          animation: 300,
          group: {
              name:'group',//名称相同的分组可以相互拖拽
              pull:true, //是否允许拖出当前组
              put:true //是否允许拖入当前组
          },
          chosenClass: "sortable-btn",
          forceFallback: true,
          handle: ".item" // 只有按住拖动手柄才能使列表单元进行拖动
      }
    },
    dragOptions2() {
      return {
        animation: 300,
        group: {
          name:'group',
          pull:false,
          put:true
        },
        ghostClass: 'ghost',
        forceFallback: true,
        handle: ".moveDialog"
      }
    }
  },
  mounted() {},
  methods: {
    watchDrag() {},
    // 编辑按钮
    edit(row) {
        this.editModal = true
        this.formValidate = JSON.parse(JSON.stringify(row))
    },
    // 删除按钮
    del(data){
      let dataList2 = JSON.parse(JSON.stringify(this.dataList2))
      this.dataList2 = dataList2.filter(item => item.chartId !== data.chartId)
      this.dataList.push(data)
    },
    // 编辑弹窗确定按钮
    submitFormEdit: function () {
      this.$refs['formValidate'].validate((valid) => {
        if (valid) {
          this.dataList2.map(item => {
            if(item.chartId === this.formValidate.chartId){
              item.width = this.formValidate.width
              item.height = this.formValidate.height
            }
            return item
          })
          this.handleCancelEdit()
          this.$Message.success('编辑成功!')
        } else {
          console.log('检验不通过!');
        }
      })
    },
    // 编辑弹窗取消按钮
    handleCancelEdit() {
      this.editModal = false
      this.$refs.formValidate.resetFields()
      this.formValidate = {
        width: 4,
        height: 1
      }
    }
  }
}
</script>
<style lang='less' scoped>
   .test{
    height: calc( 100vh - 60px);
    position: relative;
    main{
      height: 100%;
      position: relative;
      padding-left: 300px;
        .left{
          position: absolute;
          left: 0;
          float: left;
          width: 300px;
          height: 100%;
          background-color: rgb(238, 238, 240);
          overflow-y: auto;
          overflow-x: hidden;
          padding: 10px;
          .item{
            position: relative;
            height: 180px;
            margin-bottom: 10px;
            white-space: nowrap;
            cursor: pointer;
            background: rgb(239, 243, 247);
            border: 1px solid #DCDFE6;
            border-color: #DCDFE6;
            color: #606266;
            outline: none;
            padding: 12px 20px;
            font-size: 14px;
            border-radius: 4px;
            .cover{
              position: absolute;
              top:0;
              left: 0;
              height: 100%;
              transition: all .2s;
              width: 100%;
              text-align: center;
              background-color: #0006;
              opacity: 0;
              span{
                line-height: 180px;
                color:#fff;
                font-size: 16px;
              }
            }
            &:hover{
              .cover{
                opacity: 1;
              }
            }
          }
          .sortable-btn{
            box-shadow: 0 0 4px 4px #9992;
          }
          .item_red{
            background: url(../../assets/images/red.png);
            background-size: 100% 100%;
            background-repeat: no-repeat;
          }
          .item_orange{
            background: url(../../assets/images/orange.png);
            background-size: 100% 100%;
            background-repeat: no-repeat;
          }
          .item_yellow{
            background: url(../../assets/images/yellow.png);
            background-size: 100% 100%;
            background-repeat: no-repeat;
          }
          .item_green{
            background: url(../../assets/images/green.png);
            background-size: 100% 100%;
            background-repeat: no-repeat;
          }
          .item_bfa{
            background: url(../../assets/images/bfa.png);
            background-size: 100% 100%;
            background-repeat: no-repeat;
          }
          .item_blue{
            background: url(../../assets/images/blue.png);
            background-size: 100% 100%;
            background-repeat: no-repeat;
          }
          .item_purple{
            background: url(../../assets/images/purper.png);
            background-size: 100% 100%;
            background-repeat: no-repeat;
          }
          .item_pink{
            background: url(../../assets/images/pink.png);
            background-size: 100% 100%;
            background-repeat: no-repeat;
          }
          .item_fff{
            background: url(../../assets/images/fff.png);
            background-size: 100% 100%;
            background-repeat: no-repeat;
          }
          .item_black{
            background: url(../../assets/images/black.png);
            background-size: 100% 100%;
            background-repeat: no-repeat;
          }
        }
        .right{
          position: relative;
          height: 100%;
          overflow: auto;
          background-color: rgb(239, 243, 247);
          .rightBox{
            position: relative;
            display: grid;
            grid-template-columns: repeat(16, 1fr);
            grid-auto-rows: 100px; 
            grid-auto-flow: row dense;
            gap: 15px;
            padding: 10px;
            // min-height: 650px;
            padding-bottom: 20px;
            .ghost{
              opacity: .7;
              span{
                display: none;
              }
              &::before{
                content: "";
                position: absolute;
                top:0;
                left: 0;
                padding: 8px;
                cursor: pointer;
                // background-color: red;
              }
              &::after{
                content: "(^_^)~";
                position: absolute;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
              }
            }
            .item{
              position: relative;
              background-color: #fff;
              border-radius: 4px;
              box-shadow: 0 0 2px 2px #9991;
              .edit{
                display: none;
                position: absolute;
                top: 10px;
                right: 75px;
                line-height: 20px;
                cursor: pointer;
              }
              .del{
                display: none;
                position: absolute;
                top: 10px;
                right: 10px;
                line-height: 20px;
                cursor: pointer;
              }
              .moveDialog{
                position: absolute;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                opacity: 0;
                background-color: gray;
                z-index: 999;
              }
              .contentBox{
                pointer-events: none;
                cursor: default;
              }
            }
            .item:hover{
              .edit{
                display: block;
                opacity: 1;
              }
              .del{
                display: block;
                opacity: 1;
              }
              .moveDialog{
                opacity: 0.9;
              }
            }
            .item_width_1{
                grid-column-start: span 1;
                width: 100%;
            }
            .item_width_2{
                grid-column-start: span 2;
                width: 100%;
            }
            .item_width_3{
                grid-column-start: span 3;
                width: 100%;
            }
            .item_width_4{
                grid-column-start: span 4;
                width: 100%;
            }
            .item_width_5{
                grid-column-start: span 5;
                width: 100%;
            }
            .item_width_6{
                grid-column-start: span 6;
                width: 100%;
            }
            .item_width_7{
                grid-column-start: span 7;
                width: 100%;
            }
            .item_width_8{
                grid-column-start: span 8;
                width: 100%;
            }
            .item_width_9{
                grid-column-start: span 9;
                width: 100%;
            }
            .item_width_10{
                grid-column-start: span 10;
                width: 100%;
            }
            .item_width_11{
                grid-column-start: span 11;
                width: 100%;
            }
            .item_width_12{
                grid-column-start: span 12;
                width: 100%;
            }
            .item_width_13{
                grid-column-start: span 13;
                width: 100%;
            }
            .item_width_14{
                grid-column-start: span 14;
                width: 100%;
            }
            .item_width_15{
                grid-column-start: span 15;
                width: 100%;
            }
            .item_width_16{
                grid-column-start: span 16;
                width: 100%;
            }
            .item_height_1{
                grid-row-start: span 1;
                // line-height: 100px;
            }
            .item_height_2{
                grid-row-start: span 2;
                // line-height: 200px;
            }
            .item_height_3{
                grid-row-start: span 3;
                // line-height: 300px;
            }
            .item_height_4{
                grid-row-start: span 4;
                // line-height: 400px;
            }
            .item_height_5{
                grid-row-start: span 5;
                // line-height: 400px;
            }
            .item_height_6{
                grid-row-start: span 6;
                // line-height: 400px;
            }
            .item_height_7{
                grid-row-start: span 7;
                // line-height: 400px;
            }
            .item_height_8{
                grid-row-start: span 8;
                // line-height: 400px;
            }
            .item_height_9{
                grid-row-start: span 9;
                // line-height: 400px;
            }
            .item_height_10{
                grid-row-start: span 10;
                // line-height: 400px;
            }
            .item_height_11{
                grid-row-start: span 11;
                // line-height: 400px;
            }
            .item_height_12{
                grid-row-start: span 12;
                // line-height: 400px;
            }
            .item_height_13{
                grid-row-start: span 13;
                // line-height: 400px;
            }
            .item_height_14{
                grid-row-start: span 14;
                // line-height: 400px;
            }
            .item_height_15{
                grid-row-start: span 15;
                // line-height: 400px;
            }
            .item_height_16{
                grid-row-start: span 16;
                // line-height: 400px;
            }
          }
        }
      }
    }
  </style>

(该案例使用vue2框架书写,实现效果请观看视频https://live.csdn.net/v/284266

前端小白积累经验篇~~

;