- avue : 基于vue2 + element-ui2 二次封装组件库
- Sortable : 拖拽
- vuedraggable : vue二次封装的拖拽,基于Sortable
2、展示图
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,
最后
--
**[开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】](https://bbs.youkuaiyun.com/topics/618166371)**
> 最后写上我自己一直喜欢的一句名言:`世界上只有一种真正的英雄主义就是在认清生活真相之后仍然热爱它`
> 