背景:
1、首页模板由多个子组件组成,如图表、新闻、公告、轮播图等,一般都由前端引入子组件,写在固定的位置上,最终形成一个固定的首页模板;
2、像这样直接在代码中写死的首页没有灵活性,不同用户想展示出来的首页模板千篇一律;
3、若是首页模板可以根据用户自己的需求拖拽生成,首页模板的灵活性与交互性将会两者兼备。
描述:
1、通过使用draggable拖拽插件可实现自由拖拽子组件;
2、draggable可实现左右拖拽,若想完全根据用户的意愿上下左右都可拖拽,在这里我使用的是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)
前端小白积累经验篇~~