一、vue 项目使用
文档地址: https://www.itxst.com/sortablejs/neuinffi.html
1、安装依赖
npm i -S vuedraggable
2、.vue 文件引入组件
import draggable from "vuedraggable";
components: { draggable },
![在这里插入图片描述](https://img-blog.csdnimg.cn/e58ed63b19a449318f6dd9fa4ccb7e56.png)
3、.使用
查看文档中的示例即可:https://debug.itxst.com/js/ivv3eivm
我们使用的 npm 安装,不需要其他东西,只需要下方标注的主要代码部分,其中的css为演示展示用,无实际用处
![在这里插入图片描述](https://img-blog.csdnimg.cn/22b5205111d84c42a90f1f9ef0b1a404.png)
二、进阶案例演示代码(UMD版)
1、采用技术
- vue2
- element-ui2
- avue : 基于vue2 + element-ui2 二次封装组件库
- Sortable : 拖拽
- vuedraggable : vue二次封装的拖拽,基于Sortable
2、展示图
![在这里插入图片描述](https://img-blog.csdnimg.cn/aa633d5d22a74627a924ce51c4823aa6.png)
3、源码
原为npm 版, 抽取成 UMD 版便于大家学习参考
1、本地新建 .html文件
2、复制下方代码到 .html
3、打开htm 即得到上方 展示效果中 相同效果
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<!-- 引入样式文件 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@smallwei/avue/lib/index.css"/>
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"/>
<!-- 引入相关JS 文件 -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@smallwei/avue/lib/avue.min.js"></script>
<!-- 图片拖拽排序 -->
<script src="https://cdn.staticfile.org/Sortable/1.10.0-rc2/Sortable.min.js"></script>
<!-- 已经加载过了 -->
<!--<script src="https://www.itxst.com/package/vue/vue.min.js"></script>-->
<!--<script src="https://www.itxst.com/package/sortable/Sortable.min.js"></script>-->
<script src="https://www.itxst.com/package/vuedraggable/vuedraggable.umd.min.js"></script>
<div id="app">
{{ message }}
<div class="bt-article-all">
<el-row>
<el-col :span="24">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>文章信息区</span>
<el-button style="float: right; padding: 3px 0" type="text">保存</el-button>
</div>
<avue-form ref="form" v-model="obj" :option="option"
@reset-change="emptytChange"
@submit="submit">
<template slot-scope="{row}" slot="content">
<TinymceEditor v-if="initSuccess" :content.sync="obj.content"/>
</template>
</avue-form>
</el-card>
</el-col>
</el-row>
<el-row>
<!-- 左侧区 -->
<el-col :span="12">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>内容编辑区</span>
<!-- <el-button style="float: right; padding: 3px 0" type="text">保存</el-button>-->
</div>
<div>
<!-- group="itxst" -->
<draggable v-model="contentItems" chosen-class="chosen" force-fallback="true" group="itxst" :disabled="disabledDrag" animation="1000" @start="onStart" @end="onEnd">
<transition-group>
<div id="contentBox" class="bt-card-box" v-for="(item,index) in contentItems" :key="index" style="padding-top: 2%">
<el-card class="box-card bt-card-box">
<div slot="header" class="clearfix singlePerson">
<span>{{index+1}} : </span>
<span>{{item.lableName}} </span>
<el-button style="float: right; padding: 3px 0" type="text" @click="delItemRow(item)">删除</el-button>
</div>
<div v-if="item.lable == 'H1' || item.lable == 'H2' || item.lable == 'H3'">
<el-input type="input" placeholder="请输入内容" v-model="item.value"></el-input>
</div>
<div v-if="item.lable == 'P' ">
富文本组件
<!-- 富文本组件 当前单页无法加载 -->
<!-- <TinymceEditor v-if="drag==false" :content.sync="item.value"/>-->
</div>
<div v-if="item.lable == 'IMAGE'">
<el-image style="width: 80px; height: 80px" :src="item.value" fit="cover"></el-image>
</div>
<div v-if="item.lable == 'VIDEO'">
<el-input type="input" placeholder="请输入内容" v-model="item.value"></el-input>
<!-- <el-image style="width: 80px; height: 80px" :src="item.value" fit="cover"></el-image>-->
</div>
<div v-if="item.lable == 'ARRAY'">
<!-- <avue-form :option="{column: [{label:'数组框',prop:'array', type:'array', value:[0,1]}]}"></avue-form>-->
<avue-array v-model="item.value" :option="{dataType:'string'}" placeholder="请输入内容"></avue-array>
</div>
</el-card>
</div>
</transition-group>
</draggable>
</div>
</el-card>
</el-col>
<!-- 右侧区 -->
<el-col :span="12">
<!-- 媒体区 -->
<div class="bt-card-box">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>媒体资源区</span>
</div>
<div>
<el-row>
<draggable v-model="imageItems" chosen-class="chosen" force-fallback="true" :options="{group:{name: 'itxst',pull:'clone'}, sort: true}" animation="1000" @start="onStartImages" @end="onEndImages">
<transition-group>
<el-col :span="4" v-for="(item,index) in imageItems" :key="index">
<div style="padding: 5%">
<el-card class="box-card">
<el-image
style="width: 80px; height: 80px"
:src="item.value"
fit="cover"></el-image>
<span style="text-align: center;display:block;">{{ item.alt }}</span>
</el-card>
</div>
</el-col>
</transition-group>
</draggable>
</el-row>
</div>
</el-card>
</div>
<!-- 内容预览区 -->
<div class="bt-card-box" style="padding-top: 2%">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>内容预览区</span>
</div>
<div v-for="(item,index) in contentItems" :key="index">
<h1 v-if="item.lable == 'H1'">{{item.value}}</h1>
<h2 v-if="item.lable == 'H2'">{{item.value}}</h2>
<h3 v-if="item.lable == 'H3'">{{item.value}}</h3>
<span v-if="item.lable == 'P'" v-html="item.value"></span>
<span v-if="item.lable == 'IMAGE'">
<img style="width: 50%" :src="item.value" alt="item.alt">
</span>
<div v-if="item.lable == 'VIDEO'">
<video width="50%" controls :autoplay="false">
<source :src="item.value" type="video/mp4">
</video>
</div>
<div v-if="item.lable == 'ARRAY'">
<li v-for="(item,index) in item.value">{{index+1}}、{{item}}</li>
</div>
</div>
</el-card>
</div>
</el-col>
</el-row>
{{contentItems}}
<div>{{drag?'拖拽中':'拖拽停止'}}</div>
</div>
</div>
<body>
<script>
//import draggable from "vuedraggable";
var vm = new Vue({
// 绑定 id="app" 的元素
el: "#app",
// components: {
// draggable
// },
// 定义数据
data: {
message: "这是一个拖拽示例demo",
obj: {},
initSuccess: false,
defaultData: {
name: null,
alias: null,
author: "测试",
categoryIds: null,
coverUrl: "http://xijia-sz.oss-cn-shenzhen.aliyuncs.com/oss/file/file/gc/08769453-1(6).jpeg",
lables: null,
content: null,
state: 1,
describe: "-",
auth: 1,
sort: 0,
seoTitle: null,
seoKeyword: null,
seoDescription: null,
},
categoryTree: [],
disabledDrag: false, //默认开启拖拽
drag: false,
dragImages: false,
contentItems: [
{lable: 'H1', lableName: "一级标题", value: ''},
{lable: 'H2', lableName: "二级标题", value: ''},
{lable: 'H3', lableName: "三级标题", value: ''},
{lable: 'P', lableName: "段落", value: ''},
{lable: 'IMAGE', lableName: "图片", value: 'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg'},
{lable: 'VIDEO', lableName: "视频", value: 'http://127.0.0.1:10006/upload/video/swagger-ui.html/20221109-0315-11、恭喜你发现宝藏!!!.mp4'},
{lable: 'ARRAY', lableName: "有序列表", value: [0, 1]},
],
// "lable": "H1", "name": "一级标题", "sort": 1, "value": "-"
imageItems: [
//fits: ['fill', 'contain', 'cover', 'none', 'fill'],
//fits: ['fill', 'contain', 'cover', 'none', 'scale-down'],
{lable: "IMAGE", lableName: "图片", alt: "a1", value: "https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg"},
{lable: "IMAGE", lableName: "图片", alt: "a2", value: "https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg"},
{lable: "IMAGE", lableName: "图片", alt: "a3", value: "https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg"},
{lable: "IMAGE", lableName: "图片", alt: "a4", value: "http://xijia-sz.oss-cn-shenzhen.aliyuncs.com/oss/file/file/gc/08769453-1(6).jpeg"},
{lable: "IMAGE", lableName: "图片", alt: "a5", value: "http://xijia-sz.oss-cn-shenzhen.aliyuncs.com/oss/file/file/gc/08769453-1(6).jpeg"},
{lable: "IMAGE", lableName: "图片", alt: "a6", value: "http://xijia-sz.oss-cn-shenzhen.aliyuncs.com/oss/file/file/gc/08769453-1(6).jpeg"},
],
},
props: {
closeDialog: [],
uri: {},
},
computed: {
option() {
return {
submitBtn: false,
emptyBtn: false,
submitText: '提交',
emptyText: "关闭",
group: [
{
// icon: 'el-icon-info',
label: '展开/收缩文章信息',
collapse: false,
prop: 'group1',
column: [
{
label: '文章名',
prop: 'name',
maxlength: 64,
showWordLimit: true,
span: 10,
rules: [{
required: true,
message: "请输入 文章名",
trigger: "blur"
}]
},
{
label: '别名',
prop: 'alias',
maxlength: 64,
showWordLimit: true,
span: 10,
rules: [{
required: true,
message: "请输入 别名",
trigger: "blur"
}]
},
{
label: '作者',
prop: 'author',
maxlength: 32,
showWordLimit: true,
span: 10,
rules: [{
required: true,
message: "请输入 作者",
trigger: "blur"
}]
},
{
label: '分类',
prop: 'categoryIds',
span: 10,
type: "cascader",
dataType: 'string',
filterable: true,
dicData: this.categoryTree, // 自行替换字典数据
props: {
value: "id",
label: "name",
children: "categorys"
},
rules: [{
required: true,
message: "请选择 分类ids ",
trigger: "blur"
}]
},
{
label: '封面图',
prop: 'coverUrl',
span: 10,
rules: [{
required: true,
message: "请上传 文章封面图url ",
trigger: "blur"
}],
dataType: 'string',
accept: 'image/png, image/jpeg, image/jpg, image/gif',
type: 'upload',
listType: 'picture-img',
action: '128.0.0.1/update/image/cover/', // 上传地址 + 文件保存上传地址(详见接口描叙)
multiple: true, // 文件多选
drag: true, // 拖拽排序
limit: 1, // 上传数量 1 个
//fileSize: 500, // 上传大小 500 kb内
tip: '只能上传 jpg/png/gif 格式的图片',
loadText: '上传中...',
propsHttp: {
res: 'data'
},
uploadBefore: (file, done) => {
// 文件上传前处理
done(file)
},
uploadAfter: (res, done) => {
this.$message.success('上传成功');
done()
},
uploadError(error, column) {
// 上传失败
this.$message.error(error);
},
uploadExceed(limit, files, fileList, column) {
// 文件数量验证
this.$message.warning(`当前限制文件数量为 $1, 当前共 ${files.length + fileList.length} `);
},
},
// {
// label: '标签',
// prop: 'lables',
// type: 'array',
// dataType: 'string',
// limit: 10,
// span: 10,
// rules: [{
// required: false,
// message: "请添加 标签集",
// trigger: "blur"
// }]
// },
{
label: '文章描述',
prop: 'describe',
type: 'textarea',
maxlength: 256,
showWordLimit: true,
span: 10,
rules: [{
required: true,
message: "请输入 文章描述",
trigger: "blur"
}]
},
// {
// label: '文章内容 ',
// prop: 'content',
// maxlength: 0,
// showWordLimit: true,
// span: 10,
// rules: [{
// required: true,
// message: "请输入 文章内容 ",
// trigger: "blur"
// }]
// },
// {
// label: '状态 ',
// prop: 'state',
// type: 'radio',
// //dicData: this.dict.get('ARTICLE_STATE'),
// span: 10,
// rules: [{
// required: true,
// message: "请选择 状态 ",
// trigger: "blur"
// }]
// },
// {
// label: '访问权限 ',
// prop: 'auth',
// type: 'radio',
// //dicData: this.dict.get('ARTICLE_AUTH'),
// span: 10,
// rules: [{
// required: true,
// message: "请选择 访问权限 ",
// trigger: "blur"
// }]
// },
// {
// label: '排序',
// prop: 'sort',
// maxlength: 11,
// showWordLimit: true,
// span: 10,
// rules: [{
// required: true,
// message: "请输入 排序",
// trigger: "blur"
// }]
// },
{
label: 'seo: Title',
prop: 'seoTitle',
maxlength: 128,
showWordLimit: true,
span: 20,
labelWidth: 130,
rules: [{
required: false,
message: "请输入 seo优化字段 Title",
trigger: "blur"
}]
},
{
label: 'seo: Keyword',
prop: 'seoKeyword',
maxlength: 256,
showWordLimit: true,
span: 20,
labelWidth: 130,
rules: [{
required: false,
message: "请输入 seo优化字段 Keyword",
trigger: "blur"
}]
},
{
label: 'seo: Description',
prop: 'seoDescription',
maxlength: 256,
showWordLimit: true,
span: 20,
labelWidth: 130,
rules: [{
required: false,
message: "请输入 seo优化字段 Description",
trigger: "blur"
}]
}]
}],
}
}
},
created() {
this.obj = this.defaultData;
//this.findCategorTree();
this.initSuccess = true;
},
// 实例被挂载后调用
mounted: function () {
console.log("mounted=实例已被挂载")
// 输入内容时禁止拖拽
var inputs = document.getElementsByClassName('el-input__inner');
for (let item of inputs) {
item.addEventListener('blur', event => {
console.log("inputting!!111");
this.disabledDrag = false;
});
item.addEventListener('focus', event => {
console.log("inputting!!222");
this.disabledDrag = true;
});
}
},
methods: {
emptytChange() {
this.closeDialog(false);
}
,
submit(form, done) {
this.crud.post(this.uri.info, this.obj).then((res) => {
console.debug(res);
if (res.data.code == 200) {
this.closeDialog(true);
}
done(form);
}).catch((err) => {
console.error(err);
done(form);
})
}
,
/**
* 查询分类数据
*/
async findCategorTree() {
let res = await this.crud.get(this.uri.findCategoryTree);
this.categoryTree = res.data.data;
}
,
/**
* 删除行数据
* @param element
*/
delItemRow(item) {
if (this.contentItems.length <= 1) {
this.$message.error('最后一条数据不能删除');
return
}
this.contentItems.splice(this.contentItems.indexOf(item), 1)
}
,
// 内容区拖动
onStart() {
this.drag = true;
}
,
onEnd() {
// this.resetImageSort();
this.drag = false;
}
,
// 图片区拖动
onStartImages() {
this.dragImages = true;
}
,
onEndImages() {
this.dragImages = false;
}
,
}
});
// app.use(AVUE);
</script>
<style scoped>
/* 主宽度 */
.bt-article-all {
width: 96%;
padding-left: 2%;
}
/* 卡片上间距 */
.bt-card-box {
padding-left: 1%;
}
/* 卡片默认样式 */
.text {
font-size: 14px;
}
.clearfix:before,
.clearfix:after {
display: table;
content: "";
}
.clearfix:after {
clear: both
}
.box-card {
width: 100%;
/*padding: 1%;*/
padding-top: 1%;
}
</style>
</body>
</html>