vue实现拖拽(vuedraggable)

实现效果:

左侧往右侧拖动,右侧列表可以进行拖拽排序。

安装引用:

npm install vuedraggable
import draggable from 'vuedraggable'

使用:

data数据:

      componentList: [
        {
          groupName: '考试题型',
          children: [
            {
              componentType: 'danxuan',
              componentName: '单选题',
              componentIcon: 'icon-danxuan'
            },
            {
              componentType: 'duoxuan',
              componentName: '多选题',
              componentIcon: 'icon-duoxuan'
            },
            {
              componentType: 'panduan',
              componentName: '判断题',
              componentIcon: 'icon-panduan'
            }
          ]
        },
        {
          groupName: '信息题',
          children: [
            {
              componentType: 'message',
              componentName: '姓名',
              componentIcon: 'icon-xingming'
            },
            {
              componentType: 'message',
              componentName: '手机',
              componentIcon: 'icon-shouji'
            },
            {
              componentType: 'message',
              componentName: '邮箱',
              componentIcon: 'icon-youxiang'
            }
          ]
        }
      ],
questionList:[],

html代码:

左侧代码:   
<el-tabs type="border-card" class="tabs">
        <el-tab-pane label="题型">
          <div v-for="(item, index) in componentList" :key="index">
            <b class="fs-14">{{item.groupName}}</b>
            <draggable
              @end="end"
              :clone="cloneElement"
              class="group"
              v-model="item.children"
              :sort="false" //禁止排序
              :group="{
                name: 'component',
                pull: 'clone', 
                put: false  //不允许其他元素拖拽进此空间
              }">
              <div @click="pushComponent(_item)" class="component" v-for="(_item, _index) in item.children" :key="_index">
                <i class="iconfont mr-8" :class="_item.componentIcon"></i>
                <span>{{_item.componentName}}</span>
              </div>
            </draggable>
          </div>
        </el-tab-pane>
        <el-tab-pane label="题库">
          <el-tree
            ref="tree"
            highlight-current
            :data="treeList"
            node-key="id"
            :current-node-key="currentNodekey"
            @node-click="handleNodeClick"
            :load="loadNode"
            :props="props"
            lazy>
            <span slot-scope="{node}">
                <el-tooltip v-if="node.label.length>=8"  class="item" effect="dark" :content="node.label" placement="top">
                  <div class="text-ellipsis width-150">{{ node.label }}</div>
                </el-tooltip>
               <div v-else>{{ node.label }}</div>
            </span>
          </el-tree>
        </el-tab-pane>
      </el-tabs>
右侧代码:

    <div class="content">
        <el-scrollbar ref="scrollbar" style="height: calc(100vh - 220px)">
        {{questionList}}
          <draggable
            class="list"
            forceFallback
            :animation="200"
            ghostClass="ghost"
            handle=".el-icon-rank"
            v-model="questionList"
            :group="{
              name: 'component'
            }">
            <transition-group class="height-percent-100 display-block">
              <div
                class="item"
                :class="{active: item.active, error: item.error}"
                v-for="(item, index) in questionList"
                :key="item.uid">
                <div
                  class="display-flex ai-flex-start padding-20 pt-14"
                  @click="clickQuestion(item)"
                  :id="item.uid">
                  <div class="pt-6 width-40">
                    <b>{{index + 1}}</b>
                  </div>
                  <div class="flex-1">
                    <div class="display-flex ai-flex-start jc-space-between">
                      <b @click="editTitle(item)" class="width-percent-80 pt-6" style="min-height: 26px" v-if="!item.editTitle">{{item.title}}</b>
                      <el-input
                        type="textarea"
                        autosize
                        :ref="item.uid"
                        v-else
                        size="small"
                        class="width-percent-80"
                        @blur="item.editTitle = false"
                        v-model="item.title"></el-input>
                      <span v-if="item.componentType !== 'message'" class="color-info pt-6">( {{item.score}}分 )</span>
                    </div>
                    <div class="mt-12">
                      <el-input
                        v-if="item.componentType === 'message'"
                        readonly
                        placeholder="请输入"
                        type="textarea"
                        autosize
                        v-model="item.answer"
                        size="small"
                        class="width-percent-80"></el-input>
                      <draggable v-model="item.options" handle=".el-icon-d-caret">
                        <transition-group>
                          <div v-for="i in item.options" :key="i.value" class="display-flex ai-center jc-space-between pt-4 pb-4">
                            <div class="flex-1 display-flex ai-center">
                              <el-checkbox
                                v-if="item.componentType === 'duoxuan'"
                                v-model="item.answer" :label="i.value">
                                {{  }}
                              </el-checkbox>
                              <el-radio
                                v-else
                                v-model="item.answer"
                                :label="i.value" class="mr-0">{{  }}</el-radio>
                              <p @click="editOption(i)" v-if="!i.edit" class="margin-0 fs-14 width-percent-80 display-flex ai-center" style="min-height: 32px">{{i.label}}</p>
                              <el-input
                                type="textarea"
                                autosize
                                @blur="i.edit = false"
                                :ref="i.value"
                                v-else
                                v-model="i.label"
                                size="small"
                                class="width-percent-80"></el-input>
                            </div>
                            <div class="display-flex ai-center fd-row-reverse color-info width-130">
                              <i class="el-icon-d-caret ml-8 cursor-move"></i>
                              <i @click="delOption(item, i.value)" class="ml-10 el-icon-remove-outline cursor-pointer"></i>
                              <span class="color-success fs-14" v-if="item.answer.includes(i.value)">( 正确答案 )</span>
                            </div>
                          </div>
                        </transition-group>
                      </draggable>
                      <div v-if="['danxuan', 'duoxuan'].includes(item.componentType)">
                        <el-button class="pb-0" @click="addOption(item)" type="text" icon="el-icon-plus">添加选项</el-button>
                      </div>
                    </div>
                  </div>
                  <div class="display-flex ai-center color-info mt-8">
                    <i class="ml-14 el-icon-rank cursor-move"></i>
                    <i @click.stop="copyQuestion(item, index)" class="ml-14 el-icon-document-copy cursor-pointer"></i>
                    <i @click.stop="delQuestion(item)" class="ml-14 el-icon-delete cursor-pointer"></i>
                  </div>
                </div>
                <div class="errorMessage" v-if="item.error">
                  {{item.errorMessage}}
                </div>
              </div>
              <div key="empty" v-if="!questionList.length" class="height-percent-100 fd-column display-flex ai-center jc-center">
                <el-empty description="请点击右侧或拖入题型进行添加题目"></el-empty>
              </div>
            </transition-group>
          </draggable>
        </el-scrollbar>
      </div>

方法:

     /**
     * 点击组件进行push
     * @param data
     * @param type
     */
    pushComponent (data, type = 0) {
      console.log(data)
      //type=1:后端给的题库项导入  0:题型项导入
      this.questionList.push(type ? data : this.cloneElement(data))
      const newDraggableIndex = this.questionList.length - 1
      const e = {
        to: {
          className: 'pushComponent'
        },
        newDraggableIndex
      }
      this.end(e)
    },



    /**
     * 拖拽结束
     * @param e
     */
    end (e) {
      console.log(e)
      if (e.to.className !== 'group') {
        for (const item of this.questionList) {
          item.active = false
        }
        this.questionList[e.newDraggableIndex].active = true
        this.$nextTick(() => {
          document.getElementById(this.questionList[e.newDraggableIndex].uid).scrollIntoView();
        })
      }
    },

    /**
     * 拖拽clone
     * @param item
     * @returns {any}
     */
    cloneElement (item) {
      const data = JSON.parse(JSON.stringify(item));
      console.log(data)
      data.uid = `${data.componentType}-${Math.floor(Math.random() * 1000000)}`
      data.title = data.componentName
      data.answer = ''
      data.active = false
      data.editTitle = false
      data.error = false
      data.errorMessage = ''
      switch (data.componentType) {
        case 'danxuan':
          data.scoreMethod = '1' // 得分方式
          data.options = [
            {
              edit: false,
              label: '选项1',
              value: `${data.componentType}-${Math.floor(Math.random() * 1000000)}`
            },
            {
              edit: false,
              label: '选项2',
              value: `${data.componentType}-${Math.floor(Math.random() * 1000000)}`
            }
          ] // 选项
          data.answer = data.options[0].value // 答案
          data.score = 10 // 分数
          data.description = '' // 解析
          break
        case 'duoxuan':
          data.scoreMethod = '1'
          data.options = [
            {
              edit: false,
              label: '选项1',
              value: `${data.componentType}-${Math.floor(Math.random() * 1000000)}`
            },
            {
              edit: false,
              label: '选项2',
              value: `${data.componentType}-${Math.floor(Math.random() * 1000000)}`
            }
          ]
          data.answer = [data.options[0].value]
          data.score = 10
          data.description = ''
          break
        case 'panduan':
          data.scoreMethod = '1'
          data.options = [
            {
              edit: false,
              label: '是',
              value: `${data.componentType}-true`
            },
            {
              edit: false,
              label: '否',
              value: `${data.componentType}-false`
            }
          ]
          data.answer = data.options[0].value
          data.score = 10
          data.description = ''
          break
      }
      return data
    },

css:

.tabs {
  width: 240px;
  box-shadow: none;
  border: none;
  height: 100%;
  .group {
    display: grid;
    grid-gap: 12px;
    grid-template-columns: repeat(2, 1fr);
    font-size: 14px;
    padding: 12px 0;
  }
  .component {
    color: #666666;
    border-radius: 4px;
    border: 1px solid #D8D8D8;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 8px 0;
    cursor: pointer;
    &:hover {
      color: #1774FF;
      border-color: #1774FF;
    }
  }
}
.content {
  flex: 1;
  background-color: #EFF2F4;
  border-left: 1px solid #DCDFE6;
  border-right: 1px solid #DCDFE6;
  padding: 20px 4px 0 20px;

  .list {
    height: 100%;
    margin-right: 16px;
    .item {
      padding: 0;
      border: 1px solid transparent;
      border-radius: 4px;
      box-shadow: 0 2px 4px 0 rgba(0,0,0,0.1);
      background-color: #fff;
      margin-bottom: 20px;
      .el-icon-delete,.el-icon-remove-outline {
        &:hover {
          color: #F56C6C;
        }
      }
      .el-icon-document-copy {
        &:hover {
          color: #3377FF;
        }
      }
    }
    .errorMessage {
      color: #FFFFFF;
      background-color: #F56C6C;
      padding: 10px 20px;
      font-size: 14px;
      border-radius: 0 0 4px 4px;
    }
    .active {
      border-color: #2A5EFF;
    }
    .error {
      border-color: #F56C6C;
    }
  }
  .ghost {
    background-color: #499BFF;
    border-radius: 4px;
    padding: 20px;
    margin-bottom: 20px;
    .iconfont {
      display: none;
    }
    span {
      color: #FFFFFF;
    }
  }
}

扩展:

点击题库中的题进行导入:

代码:

<el-tab-pane label="题库">
  <el-tree
    ref="tree"
    highlight-current
    :data="treeList"
    node-key="id"
    :current-node-key="currentNodekey"
    @node-click="handleNodeClick"
    :load="loadNode"
    :props=" {
       label: 'name',
       value: 'id',
       isLeaf: 'isLeaf'
      },"
    lazy>
    <span slot-scope="{node}">
        <el-tooltip v-if="node.label.length>=8"  class="item" effect="dark" :content="node.label" placement="top">
          <div class="text-ellipsis width-150">{{ node.label }}</div>
        </el-tooltip>
       <div v-else>{{ node.label }}</div>
    </span>
  </el-tree>
</el-tab-pane>

方法:

handleNodeClick (node) {
  if (node.level === 2) {
    //点击子节点(叶子节点)
    this.$nextTick(() => {
      this.$refs.tree.setCurrentKey(node.id)
    })
    const data = JSON.parse(JSON.stringify(node))
    data.answer = JSON.parse(node.answer)
    data.uid = `${data.componentType}-${Math.floor(Math.random() * 1000000)}`
    this.pushComponent(data, 1)
  } else {
    this.$nextTick(() => {
      this.$refs.tree.setCurrentKey()
    })
  }
},
loadNode (node, resolve) {
  if (node.level === 0) {
    this.$api.pxExam.getExamSetList({ name: this.fuzzy }).then(res => {
      this.treeList = res.map(res => {
        return {
          name: res.name,
          id: res.id,
          isLeaf: false,
          level: 1
        }
      })
      return resolve(this.treeList)
    })
  }
pushComponent方法通用的(传参不同),上面写的有。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/403246.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【基础】提高前端的增益

低噪声&#xff0c;低偏移电压&#xff0c;低漂移-当你把信号链前端的增益提高后&#xff0c;所有的这些精密小信号处理的目标变得很简单。 这是一个很简单的概念。如图1所示&#xff0c;第二级的误差将除以第一级的增益。比如&#xff0c;第一级增益适度&#xff0c;值为10&a…

制造业客户数据安全解决方案(终端安全/文件加密/介质管理等)

针对前文制造业客户数据安全解决方案&#xff08;数据防泄密需求分析&#xff09;提到的泄密风险&#xff0c;本文详细介绍一套完整、合理的解决方案&#xff0c;通过该方案构建公司数据安全防护边界&#xff0c;自动加密、全方位保护数据安全。 PC端&#xff1a;https://isite…

Qt开发:MAC安装qt、qtcreate(配置桌面应用开发环境)

安装qt-creator brew install qt-creator安装qt brew install qt查看qt安装路径 brew info qtzhbbindembp ~ % brew info qt > qt: stable 6.6.1 (bottled), HEAD Cross-platform application and UI framework https://www.qt.io/ /opt/homebrew/Cellar/qt/6…

创建一个基于Node.js的实时聊天应用

在当今数字化社会&#xff0c;实时通讯已成为人们生活中不可或缺的一部分。无论是在社交媒体平台上与朋友交流&#xff0c;还是在工作场合中与同事协作&#xff0c;实时聊天应用都扮演着重要角色。与此同时&#xff0c;Node.js作为一种流行的后端技术&#xff0c;为开发者提供了…

10大数据恢复软件可以帮助您恢复电脑数据

您可能会非常紧张&#xff0c;因为知道有人意外地从您的硬盘驱动器中删除了您的宝贵数据&#xff08;甚至使用 ShiftDelete 从回收站中删除&#xff09;&#xff0c;并且您确实需要这些数据&#xff0c;并且没有其他备份源可以在其中找到这些数据。不要担心&#xff0c;保持冷静…

B² NETWORK空投

空投要点 众多大机构支持&#xff0c;是为数不多的有 Bitcoin 主网验证 Rollup 解决方案的 BTC Layer2&#xff0c;提前埋伏其实是普通人抢早期筹码最好的方式&#xff0c;参加 B Buzz 就是手握金铲子&#xff0c;对标eth二层网络的繁荣程度你就能想象这个前景明牌空投5%给早期…

Kotlin 进阶 学习 委托

1.接口委托 package com.jmj.jetpackcomposecompositionlocal.byStudy/*** 接口委托*/ interface HomeDao{fun getAllData():List<String> }interface ADao{fun getById(id:Int):String }class HomeDaoImpl:HomeDao{override fun getAllData(): List<String> {ret…

useRef有什么用?

看一下官网定义 useRef是一个React Hook&#xff0c;它能帮助引用一个不需要渲染的值 这句话透露出一个信息&#xff0c;不需要渲染的值可以用useRef引用&#xff0c;那需要渲染的值用什么引用呢&#xff1f;当然是useState了&#xff0c;需要渲染的值指的就是状态嘛&#xff0…

基于YOLOv8/YOLOv7/YOLOv6/YOLOv5的玉米病虫害检测系统(Python+PySide6界面+训练代码)

摘要&#xff1a;本文介绍了一种基于深度学习的玉米病虫害检测系统系统的代码&#xff0c;采用最先进的YOLOv8算法并对比YOLOv7、YOLOv6、YOLOv5等算法的结果&#xff0c;能够准确识别图像、视频、实时视频流以及批量文件中的玉米病虫害。文章详细解释了YOLOv8算法的原理&#…

防御保护--防病毒网关AV

目录 病毒 防病毒处理流量 防病毒的配置 防病毒&#xff08;AV&#xff09; --- 传统的AV防病毒的方式是对文件进行查杀。 传统的防病毒的方式是通过将文件缓存之后&#xff0c;再进行特征库的比对&#xff0c;完成检测。但是&#xff0c;因为需 要缓存文件&#xff0c;则将…

Swift Combine 使用调试器调试管道 从入门到精通二十六

Combine 系列 Swift Combine 从入门到精通一Swift Combine 发布者订阅者操作者 从入门到精通二Swift Combine 管道 从入门到精通三Swift Combine 发布者publisher的生命周期 从入门到精通四Swift Combine 操作符operations和Subjects发布者的生命周期 从入门到精通五Swift Com…

【数据结构初阶 6】二叉树:堆的基本操作 + 堆排序的实现

文章目录 &#x1f308; Ⅰ 二叉树的顺序结构&#x1f308; Ⅱ 堆的概念与性质&#x1f308; Ⅲ 堆的基本操作01. 堆的定义02. 初始化堆03. 堆的销毁04. 堆的插入05. 向上调整堆06. 堆的创建07. 获取堆顶数据08. 堆的删除09. 向下调整堆10. 判断堆空 &#x1f308; Ⅳ 堆的基本…

Spring之AOP源码解析(上)

Aop相关注解 EnableTransactionManagementEnableAspectJAutoProxyEnableAsync... 从注解切入来看看这些注解都干了什么 Import注解作用简述 注入的类一般继承ImportSelector或者ImportBeanDefinitionRegistrar接口 继承ImportSelector接口&#xff1a;selectImports方法返回…

2.22学习总结

1.营救 2.租用游艇 3.砍树 4.买礼物 5.刷题统计 砍树https://www.dotcpp.com/oj/problem3157.html 题目描述 给定一棵由 n 个结点组成的树以及 m 个不重复的无序数对 (a1, b1), (a2, b2), . . . , (am, bm)&#xff0c;其中 ai 互不相同&#xff0c;bi 互不相同&#xff0c;ai…

MYSQL-入门

一.安装和连接 1.1 安装 mysql安装教程&#xff1a; 2021MySql-8.0.26安装详细教程&#xff08;保姆级&#xff09;_2021mysql-8.0.26安装详细教程(保姆级)_mysql8.0.26_ylb呀的博客-cs-CSDN博客 workbench安装&#xff1a; MySQL Workbench 安装及使用-CSDN博客 1.2 配…

戏曲文化苑|戏曲文化苑小程序|基于微信小程序的戏曲文化苑系统设计与实现(源码+数据库+文档)

戏曲文化苑小程序目录 目录 基于微信小程序的戏曲文化苑系统设计与实现 一、前言 二、系统功能设计 三、系统实现 1、微信小程序前台 2、管理员后台 &#xff08;1&#xff09;戏曲管理 &#xff08;2&#xff09;公告信息管理 &#xff08;3&#xff09;公告类型管理…

AI智能分析网关V4智慧工厂视频智能监管与风险预警平台建设方案

一、背景需求分析 1&#xff09;随着信息技术的迅猛发展和制造业竞争的加剧&#xff0c;智慧工厂成为了推动制造业转型升级的重要引擎。智慧工厂解决方案通过整合物联网、人工智能、大数据分析等先进技术&#xff0c;实现生产过程的智能化、自动化和高效化&#xff0c;为企业提…

java-cef jcefmaven java集成CEF

java-cef 项目地址 Bitbucket jcefmaven 项目地址 GitHub - jcefmaven/jcefmaven: Maven artifacts for JCef 两个项目之间的关系 编译java-cef https://www.cnblogs.com/JpgCode/p/9397166.html jcefmaven 使用例子 Java使用JCEF开发 windows桌面应用-腾讯云开发者社区…

python+vue_django编程语言在线学习平台

本论文的主要内容包括&#xff1a; 第一&#xff0c;研究分析当下主流的web技术&#xff0c;结合学校日常管理方式&#xff0c;进行编程语言在线学习平台的数据库设计&#xff0c;设计编程语言在线学习平台功能&#xff0c;并对每个模块进行说明。 第二&#xff0c;陈列说明该系…

Delphi 11 安卓的蓝牙权限申请

上一篇博客里面的代码&#xff0c;演示如何申请安卓的权限。 如何申请安卓的蓝牙权限&#xff1f; 本博客之前有一篇文章写过。 现在 Google 要求 Android API Level 必须是 33。对于 BLE 的权限申请&#xff0c;有了一些新的要求。 以下描述&#xff0c;基于 Delphi 11。 …