vue2 使用 Sortable 库进行拖拽操作

一、vue 项目使用

文档地址: https://www.itxst.com/sortablejs/neuinffi.html

1、安装依赖

npm i -S vuedraggable

2、.vue 文件引入组件

import draggable from "vuedraggable";
components: { draggable },

在这里插入图片描述

3、.使用

查看文档中的示例即可:https://debug.itxst.com/js/ivv3eivm

我们使用的 npm 安装,不需要其他东西,只需要下方标注的主要代码部分,其中的css为演示展示用,无实际用处

在这里插入图片描述

二、进阶案例演示代码(UMD版)

1、采用技术

  • vue2
  • element-ui2
  • 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,
                                //         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>

### Vue2使用 Sortable.js 实现 Element UI 的 el-table 组件拖动行功能并保持序号不变 为了在 Vue2 项目中通过 `Sortable.js` 实现 `el-table` 行拖拽排序的功能,同时确保表格中的序号不会因拖拽而改变,可以按照以下方式实现。 #### 主要思路 1. 使用 `Sortable.js` 提供的 API 来监听拖拽事件。 2. 在数据模型层面更新数组顺序以同步视图变化。 3. 添加一个独立字段用于存储原始索引值,在渲染时利用该字段来显示固定的序号[^1]。 以下是具体实现: ```javascript <template> <div> <el-table :data="tableData" border style="width: 100%"> <!-- 定义一列表格序号 --> <el-table-column type="index" label="#" width="50"></el-table-column> <el-table-column prop="name" label="名称" width="180"></el-table-column> <el-table-column prop="age" label="年龄"></el-table-column> </el-table> <!-- 可选:添加一个隐藏 DOM 节点作为 Sortable 初始化目标 --> <tr ref="dragHandle" class="hidden-drag-handle"> <td></td><td></td><td></td> </tr> </div> </template> <script> import Sortable from 'sortablejs'; export default { data() { return { tableData: [ { name: '张三', age: 24 }, { name: '李四', age: 30 }, { name: '王五', age: 27 } ] }; }, mounted() { this.initSortable(); }, methods: { initSortable() { const elTableBody = this.$refs.dragHandle || document.querySelector('.el-table__body-wrapper tbody'); new Sortable(elTableBody, { animation: 150, handle: '.handle', // 如果有自定义手柄类名,则在此指定 onEnd: (evt) => { const oldIndex = evt.oldIndex; const newIndex = evt.newIndex; // 更新数据源顺序 const movedItem = this.tableData.splice(oldIndex, 1)[0]; this.tableData.splice(newIndex, 0, movedItem); } }); } } }; </script> <style scoped> .hidden-drag-handle { display: none; /* 隐藏辅助节点 */ } .handle { cursor: move; /* 自定义手柄样式 */ } </style> ``` #### 关键点解析 - **初始化 Sortable** 将 `Sortable.create()` 方法绑定到 `.el-table__body-wrapper tbody` 上,这是实际承载表格主体的部分。如果需要更精确的选择器,可以通过 `$refs` 或者动态查询获取对应的 DOM 元素。 - **onEnd 回调函数** 当用户完成拖拽操作后触发此回调。通过调整原生 JavaScript 数组的方法(如 `splice`),重新排列内部的数据结构,从而让前端展示的内容与逻辑一致[^1]。 - **固定序号处理** 利用 `<el-table-column>` 的内置属性 `type="index"` 自动生成基于当前页面分页后的连续编号;或者手动维护额外的一个字段记录初始位置,并将其映射至界面层面上呈现给用户的视觉效果。 --- ####
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值