参考引用作者:作者:李白不吃茶v
原作者源代码git地址:大神的源代码
安装:vuedraggable
语法:npm install vuedraggable
draggable.vue的代码:
<template>
<!-- section标签定义文档中的节(section、区段),比如章节、页眉、页脚或文档中的其他部分 -->
<section class="decoration-edit">
<!-- 页面的左侧内容 -->
<section class="l">
<ul @dragstart="dragStart" @dragend="dragEnd">
<li v-for="(val, key, index) in typeList" draggable :key="index + 1" :data-type="key">
<span :class="val.icon"></span>
<p>{{ val.name }}</p>
</li>
</ul>
</section>
<!-- 页面的中间内容 -->
<section class="c">
<!-- header(导航栏) 不可拖拽 -->
<div class="top-nav" @click="selectType(0)">
<img src="../../assets/images/topNavBlack.png" alt="" />
<span class="tit">{{ info.title }}</span>
</div>
<div class="view-content" @drop="drog" @dragover="dragOver" :style="{ backgroundColor: info.backgroundColor }">
<Draggable v-model="view" draggable=".item">
<template v-for="(item, index) in view">
<div v-if="index > 0" :data-index="index" :key="index" class="item" @click="selectType(index)">
<!-- waiting 可拖拽配置区域 -->
<template v-if="item.status && item.status === 2">
<div class="wait">
{{ item.type }}
</div>
</template>
<template v-else>
<component :is="typeList[item.type]['com']" :data="item" :className="className[item.tabType]"></component>
</template>
<i @click="deleteItem($event, index)" class="el-icon-error"></i>
</div>
</template>
</Draggable>
</div>
</section>
<!-- 页面的右侧内容 -->
<section class="r"></section>
</section>
</template>
<script>
// 导入draggable组件
import Draggable from 'vuedraggable'
// 导入商品视图组件
import Product from '@/components/draggView/Product.vue'
// 导入图片视图组件
import Images from '@/components/draggView/Images.vue'
// 导入轮播图组件
import Banner from '@/components/draggView/Banner.vue'
export default {
// 注册组件
components: {
Draggable,
Product,
Images,
Banner
},
data() {
return {
// 定义左侧循环展示的内容以及拖拽后中间区域所显示的组件
// 为什么是对象不是数组,应为循环的时候,对象的键值有用处
typeList: {
banner: {
name: '轮播图',
icon: 'el-icon-picture',
com: Banner
},
product: {
name: '商品',
icon: 'el-icon-s-goods',
com: Product
},
images: {
name: '图片',
icon: 'el-icon-picture',
com: Images
}
},
view: [
{
type: 'info',
title: '页面标题'
}
], // 中间视图所存下的数组
title: '页面标题', // 中间导航栏上的标题
type: '', // 进行拖拽的类型
index: null, // 当前组件的索引
isPush: false, // 是否已添加组件
props: {}, // 传值
isRight: false,
className: {
1: 'one',
2: 'two',
3: 'three'
}
}
},
computed: {
info() {
return this.view[0]
}
},
methods: {
// 切换视图组件
selectType(index) {
this.isRight = false
this.props = this.view[index]
this.$nextTick(() => (this.isRight = true))
},
// 删除已经拖拽进入的组件
deleteItem(e, index) {
e.preventDefault()
e.stopPropagation()
this.view.splice(index, 1)
this.isRight = false
this.props = {}
},
// 拖拽类型
dragStart(e) {
console.log(e)
console.log(e.target.dataset.type)
this.type = e.target.dataset.type
console.log(this.type)
console.log(this.index)
},
// 结束拖拽
dragEnd(e) {
console.log(e)
console.log(this.index)
this.$delete(this.view[this.index], 'status')
this.isPush = false
this.type = null
},
// 当元素放下到drop元素触发
drog(e) {
console.log(e)
console.log(this.type)
if (!this.type) {
// 内容拖拽
return
}
e.preventDefault()
e.stopPropagation()
this.dragEnd()
},
// 当元素拖动到drop元素上时触发
dragOver(e) {
console.log(e)
if (!this.type) {
// 内容拖拽
return
}
e.preventDefault()
e.stopPropagation()
const className = e.target.className
const name = className !== 'view-content' ? 'item' : 'view-content'
console.log(className)
const defaultData = {
type: this.type, // 组件类型
status: 2, // 默认状态
data: [], // 数据
options: {} // 选项操作
}
if (name === 'view-content') {
if (!this.isPush) {
this.index = this.view.length
console.log(this.view.length)
this.isPush = true
this.view.push(defaultData)
console.log(this.view)
}
} else if (name === 'item') {
const target = e.target
var [y, h, curIndex] = [e.offsetY, target.offsetHeight, target.dataset.index]
const direction = y < h / 2
if (!this.isPush) {
// push to top or bottom
if (direction) {
if (curIndex === 0) {
this.view.unshift(defaultData)
} else {
this.view.splice(curIndex, 0, defaultData)
}
} else {
curIndex = +curIndex + 1
this.view.splice(curIndex, 0, defaultData)
}
} else {
// Moving
if (direction) {
var i = curIndex === 0 ? 0 : curIndex - 1
var result = this.view[i].status === 2
} else {
i = +curIndex + 1
result = this.view.length > i && this.view[i].status === 2
}
if (result) return
const temp = this.view.splice(this.index, 1)
this.view.splice(curIndex, 0, temp[0])
}
this.index = curIndex
this.isPush = true
}
}
}
}
</script>
<style lang="scss" scoped>
// 页面
.decoration-edit {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 0;
background: #f7f8f9;
height: calc(100vh - 200px);
// 此处的height注意不能写成 height: calc(100vh-200px);注意中间的空格
position: relative;
}
// 左侧拖拽区域
.l,
.r {
width: 340px;
height: 100%;
padding: 15px 0;
background: #fff;
}
.l {
ul {
margin: 0;
padding: 0;
li {
width: 80px;
height: 80px;
display: flex;
flex-direction: column;
cursor: default;
justify-content: center;
align-items: center;
list-style: none;
font-size: 14px;
color: #666;
float: left;
margin: 0 10px;
border-radius: 6px;
transition: all 0.3s;
cursor: pointer;
// & 表示在嵌套层次中回溯一层 此处:&:hover => li:hover
&:hover {
background: #efefef;
}
span {
display: block;
font-size: 40px;
margin-bottom: 8px;
color: #999;
}
p {
display: block;
margin: 0;
font-size: 12px;
}
}
}
}
.c {
width: auto;
// 子元素(包括content+padding+border+margin) 撑满这个父级元素的content区域
// 子元素有 margin、border、padding时,会减去子元素content区域相对应的width值
// 父元素的content = 子元素(content +padding + border + margin)
max-width: 400px;
position: relative;
.top-nav {
position: absolute;
top: 0;
background: #fff;
z-index: 999;
transition: all 0.3s;
&* {
// 表示top-nav下的所有元素
pointer-events: none;
// 表示能阻止点击、状态变化和鼠标指针变化
}
&:hover {
transform: scale(0.95);
// 默认为1 缩小为 0.95
border-radius: 10px;
overflow: hidden;
box-shadow: 0 0 10px #afafaf;
}
.tit {
position: absolute;
left: 50%;
bottom: 10px;
transform: translateX(-50%);
}
img {
max-width: 100%;
image-rendering: -moz-crisp-edges;
image-rendering: -o-crisp-edges;
image-rendering: -webkit-optimize-contrast;
image-rendering: crisp-edges;
-ms-interpolation-mode: nearest-neighbor;
}
}
.view-content {
width: 400px;
height: 700px;
background: #f5f5f5;
overflow-y: auto;
overflow-x: hidden;
padding-top: 72px;
box-shadow: 0 2px 6px #ccc;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #dbdbdb;
}
&::-webkit-scrollbar-track {
background: #f6f6f6;
}
.item {
transition: all 0.3s;
background: #fff;
&:hover {
transform: scale(0.95);
border-radius: 10px;
box-shadow: 0 0 10px #afafaf;
.el-icon-error {
display: block;
}
}
div {
pointer-events: none;
}
.wait {
background: #deedff;
height: 35px;
text-align: center;
line-height: 35px;
font-size: 12px;
color: #666;
}
.el-icon-error {
position: absolute;
right: -10px;
top: -6px;
color: red;
font-size: 25px;
cursor: pointer;
display: none;
z-index: 9999;
}
}
}
}
</style>
总结:先定义三个组件,Product、Banner、Images,导入进来,根据拖拽的类型进行判断,动态渲染组件。
在components
目录下定义draggView文件,该目录底下存放中间区域组件。Banner.vue、Images.vue、Product.vue三个视图组件。
Product.vue
<template>
<div class="product" :class="className">
<!-- 商品有数据情况下 -->
<template v-if="data.data && data.data.length > 0">
<div class="product-item" v-for="(item, index) in data.data" :key="index">
<div class="image">
<img :src="item.productImg" alt="" />
</div>
<div class="info">
<p class="name">{{ item.productName }}</p>
<p class="num">{{ options.volumeStr ? item.volumeStr + '已购买' : '' }} {{ line }}{{ options.goodRatio ? item.goodRatio + '99%' : '' }}</p>
<p class="price">
<span>¥{{ item.productPrice }}</span>
<span v-if="options.origonalPrice">¥{{ item.originalPrice }}</span>
</p>
</div>
</div>
</template>
<!-- 商品静态未选择商品情况下 -->
<template v-else>
<div class="product-item product-default" v-for="index in 3" :key="index">
<div class="image">
<img src="https://img.quanminyanxuan.com/other/21188f7a1e9340759c113aa569f96699.jpg?x-oss-process=image/resize,h_600,m_lfit" alt="" />
</div>
<div class="info">
<p class="name">这是商品名称</p>
<p class="num">12124 已经购买 | 99%</p>
<p class="price">
<span>¥9.99</span>
<span>¥9.99</span>
</p>
</div>
</div>
</template>
</div>
</template>
<script>
export default {
name: 'Product',
props: ['data', 'className'],
computed: {
options() {
return this.data.options
},
line() {
return this.options.volumeStr && this.options.goodRatio ? ' | ' : ''
}
}
}
</script>
<style lang="scss" scoped>
.product {
display: flex;
flex-wrap: wrap;
padding: 4px 8px;
box-sizing: border-box;
* {
box-sizing: border-box;
}
&.one .product-item {
width: 100%;
padding: 10px;
display: flex;
border-bottom: 1px dashed #eee;
.image {
width: 100px;
border-radius: 5px;
overflow: hidden;
margin-right: 10px;
}
.info {
padding: 0 5px;
display: flex;
flex-direction: space-between;
flex: 1;
.price {
font-size: 20px;
margin: 0;
}
.num {
margin: 12px 0 0;
}
}
}
&.three .product-item {
width: 33.33%;
.info .price {
font-size: 16px;
}
&.product-default:nth-of-type(3) {
display: block;
}
}
.product-item {
width: 50%;
padding: 5px;
&.product-default:nth-of-type(3) {
display: none;
}
.image {
font-size: 0;
img {
max-width: 100%;
}
}
.info {
padding: 10px 5px 0;
.name {
font-size: 14px;
margin: 0;
color: #333;
text-overflow: ellipsis;
word-break: break-all;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
height: 38px;
line-height: 18px;
}
.num {
font-size: 12px;
color: #d23000;
font-weight: 600;
}
.price {
font-weight: 600;
margin: 12px 0 0;
font-size: 18px;
span:nth-of-type(1) {
color: red;
}
span:nth-of-type(2) {
color: #b5b5b5;
font-weight: 400;
font-size: 12px;
margin-left: 4px;
text-decoration: line-through;
}
}
}
}
}
</style>
在components目录下新建draggEdit目录,该目录下新建index.vue、Product.vue、Images.vue和Info.vue目录。
index.vue 表示 右侧表单编辑父级组件。
Product.vue 表示商品编辑子组件。
Images.vue 表示图片和轮播的子组件。
Info.vue 便是页面头部编辑的子组件。
index.vue
<template>
<section>
<div class="tab-content">
<h2>{{ type && list[type]['tit'] }}</h2>
<div class="tab" v-if="type != 'info'">
<span v-for="(val, key, index) in tabType" :key="index" @click="tab(key)" :class="{ active: val }">
<i class="el-icon-s-data">{{ key }}</i>
</span>
</div>
</div>
<component :is="type && list[type]['com']" :data="data" @changeTab="tab"> </component>
</section>
</template>
<script>
import Product from '@/components/draggEdit/Product.vue'
export default {
name: 'EditForm',
components: {
Product
},
props: {
data: {
type: Object,
default: () => {}
}
},
data() {
return {
type: '',
list: {
info: {
tit: '页面信息',
com: 'Info'
},
images: {
tit: '图片',
com: 'Images'
},
banner: {
tit: '轮播图',
com: 'Images'
},
product: {
tit: '商品',
com: 'Product'
}
},
tabType: {
1: true,
2: false,
3: false
}
}
},
mounted() {
console.log(this.data)
console.log(this.data.type)
this.type = this.data.type
if (this.data.tabType) {
// this.tab(this.data.tabType)
}
},
methods: {
tab(key) {
for (const i in this.tabType) {
if (key === i) {
this.tabType[key] = true
this.$set(this.data, 'tabType', key)
} else {
this.tabType[i] = false
}
}
}
}
}
</script>
<style lang="scss" scoped>
section {
height: 100%;
overflow: hidden;
display: flex;
flex-wrap: nowrap;
flex-direction: column;
}
.tab-content {
margin: 0 15px;
h2 {
font-size: 16px;
color: #333;
}
.tab {
display: flex;
justify-content: space-around;
border: 1px solid #ddd;
border-radius: 6px;
span {
width: 33.33%;
text-align: center;
font-size: 14px;
color: #666;
display: block;
height: 36px;
line-height: 36px;
cursor: pointer;
&.active {
color: #fff;
background: #409eff;
border-radius: 2px;
}
&:nth-of-type(2) {
border-left: 1px solid #ddd;
border-right: 1px solid #ddd;
}
}
}
}
</style>
Product.vue
<template>
<div class="product-content">
<p class="tit">商品列表<span>(可拖动排序)</span></p>
<el-button class="add-btn" type="primary" @click="toggleSearchPopup"><i class="el-icon-plus"></i>添加商品</el-button>
<template v-if="list.data && list.data.length > 0">
<vuedraggable v-model="list.data" tag="ul" draggable="li" v-if="list.data && list.data.length > 0" class="list">
<li class="item" v-for="(item, index) in list.data" :key="index">
<img :src="item.productImg" alt="" />
<i class="el-icon-error" @click="deleteItem(index)"></i>
</li>
</vuedraggable>
</template>
<div class="options">
<el-form label-width="80px">
<template v-for="(key, val, index) in options">
<el-form-item :label="key" :key="index" v-if="loadingOption">
<el-switch v-model="list['options'][val]" :name="val" @change="optionsChange(val, $event)"></el-switch>
</el-form-item>
</template>
</el-form>
</div>
<el-dialog title="添加商品" :visible.sync="show" @close="close">
<el-form label-width="100px">
<el-form-item label="选择商品">
<el-select v-model="selectProduct" filterable remote reserve-keyword placeholder="请输入商品名称" :remote-method="searchProductList" @change="addProduct" :loading="loading">
<el-option v-for="item in productList" :key="item.productId" :label="item.productName" :value-key="item.productName" :value="item"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="confirm">确定</el-button>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>
<script>
import vuedraggable from 'vuedraggable'
export default {
name: 'Product',
components: {
vuedraggable
},
props: {
data: {
type: Object,
default: () => {}
}
},
data() {
return {
list: {},
productList: [],
loading: false,
show: false,
selectItem: null,
selectProduct: '',
options: {
originalPrice: '划线价',
goodRatio: '好评率',
volumeStr: '销售量'
},
loadingOption: false
}
},
mounted() {
this.list = this.data
if (!this.data.tabType) {
this.$emit('changeTab', 2)
}
// 默认开启所有选项
for (const key in this.options) {
if (this.data.options[key] === undefined) {
this.$set(this.list.options, key, true)
}
this.loadingOption = true
}
},
methods: {
optionsChange(key, result) {
this.$set(this.list.options, key, result)
},
deleteItem(index) {
this.list.data.splice(index, 1)
},
// 搜索商品
searchProductList(productName) {
this.productList = productList
},
confirm() {
this.list.data.push(this.selectItem)
this.close()
},
toggleSearchPopup() {
this.show = true
},
close() {
this.show = false
this.selectItem = null
this.selectProduct = ''
},
addProduct(data) {
this.selectItem = data
}
}
}
// 模拟产品列表
var productList = [
{
productId: 3601,
productName: '驼大大新疆正宗骆驼奶粉初乳骆驼乳粉蓝罐礼盒装120g*4罐',
productImg: 'https://img.quanminyanxuan.com/excel/f6860885547648d9996474bbf21fdca9.jpg',
productPrice: 299,
originalPrice: 598,
volumeStr: '741',
goodRatio: 98
},
{
productId: 3268,
productName: '百合28件套新骨质瓷餐具',
productImg: 'https://img.quanminyanxuan.com/excel/185e7365f65543f2b4ebc67036d6a78f.jpg',
productPrice: 370,
originalPrice: 1388,
volumeStr: '400',
goodRatio: 99
},
{
productId: 3343,
productName: '和商臻品槐花蜜250克/瓶',
productImg: 'https://img.quanminyanxuan.com/excel/4626c8c628d04935b0262d04991416b2.jpg',
productPrice: 34.5,
originalPrice: 72,
volumeStr: '258',
goodRatio: 98
},
{
productId: 3330,
productName: '鲍参翅肚浓羹350g袋装',
productImg: 'https://img.quanminyanxuan.com/excel/58a0c968dc7d42c3ac21e09d1862aa6f.jpg',
productPrice: 75,
originalPrice: 128,
volumeStr: '258',
goodRatio: 98
}
]
</script>
<style lang="scss" scoped>
.product-content {
.tit {
text-align: center;
font-size: 12px;
color: #666;
margin: 18px 0;
padding-bottom: 10px;
border-bottom: 1px dashed #ddd;
}
.add-btn {
width: calc(100% - 30px);
height: 34px;
line-height: 34px;
padding: 0;
font-size: 12px;
margin-left: 15px;
margin-top: 5px;
}
.list {
display: flex;
flex-wrap: wrap;
padding: 12px;
margin: 0;
.item {
width: 70px;
height: 70px;
border-radius: 6px;
margin: 4px;
position: relative;
transition: all 0.3s;
list-style: none;
img {
width: 100%;
height: 100%;
border-radius: 4px;
}
i {
position: absolute;
top: -6px;
right: -6px;
cursor: pointer;
opacity: 0;
transition: all 0.3s;
color: red;
}
&::before {
content: '';
height: 100%;
width: 100%;
position: absolute;
top: 0;
right: 0;
background: rgba(0, 0, 0, 0.4);
border-radius: 4px;
opacity: 0;
transition: all 0.3s;
}
&:hover {
cursor: grab;
&::before,
i {
opacity: 1;
}
}
}
}
.options {
padding: 15px;
border-radius: 6px;
.el-form {
background: #f7f8f9;
overflow: hidden;
padding: 10px 0;
.el-form-item {
margin: 0;
label {
font-size: 12px;
}
}
}
}
}
</style>
Info.vue
<template>
<div class="info-content">
<el-form label-width="80px">
<el-form-item label="页面标题">
<el-input v-model="list.title"></el-input>
</el-form-item>
<el-form-item label="页面备注">
<el-input type="textarea" :rows="4" v-model="list.remarks"></el-input>
</el-form-item>
<el-form-item label="页面背景">
<el-color-picker v-model="list.backgroundColor" show-alpha></el-color-picker>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
name: 'Info',
props: ['data', 'className'],
data() {
return {
list: {}
}
},
mounted() {
this.list = this.data
}
}
</script>
<style lang="scss" scoped></style>