vuedraggable固定某一item的记录


基础用法

第一种

<draggable v-model="list" :options="dragOptions">
  <div class="item" v-for="item in list" :key="item.id">
    {{ item.name }}
  </div>
</draggable>
list: [
      { id: 1, name: 'Item 1' },
      { id: 2, name: 'Item 2' },
      { id: 3, name: 'Item 3' },
      { id: 4, name: 'Item 4' },
      { id: 5, name: 'Item 5' },
      { id: 6, name: 'Item 6' },
    ],
    dragOptions: {
      group: 'items',
      // 在拖拽过程中检查每个元素的 draggable 属性
      draggable: '.draggable',
    },
  

第二种

这个版本是需要npm i vuedraggable@4.1.0 。就很奇怪,npm上搜不到这个版本,即使用vuedraggable@latest也都是2.x的版本,但用npm view vuedraggable,能看到next是4.1.0。但你偏偏能安装上去,还能正常显示使用,就绝啦。如果是2.x的版本会报错 Cannot read properties of undefined (reading ‘header’),而且页面也不会显示。超好奇是从哪知道的4.1.0的用法呀,有人知道吗,可以教教我吗

<draggable
class="image-list" 
    v-model="list"
    item-key="id"
    chosen-class="chosenClass"
    animation="300"
    forceFallback="true"
  >
  <template #item="{ element }">
      <div>{{ element.name }}</div>
    </template>
  </draggable>
  list: [
      { id: 1, name: 'Item 1' },
      { id: 2, name: 'Item 2' },
      { id: 3, name: 'Item 3' },
      { id: 4, name: 'Item 4' },
      { id: 5, name: 'Item 5' },
      { id: 6, name: 'Item 6' },
    ],

限制item

vuedraggable拖拽的时候,如果想要某个item不能拖动,有两种方式

diaggable

<draggable v-model="list" :options="dragOptions">
  <div class="item" v-for="item in list" :key="item.id" :draggable="!item.disableDrag" :class="{ 'draggable': !item.disableDrag }">
    {{ item.name }}
  </div>
</draggable>
list: [
      { id: 1, name: 'Item 1', disableDrag: false },
      { id: 2, name: 'Item 2', disableDrag: true },
      { id: 3, name: 'Item 3', disableDrag: false },
      { id: 4, name: 'Item 4', disableDrag: false },
      { id: 5, name: 'Item 5', disableDrag: false },
      { id: 6, name: 'Item 6', disableDrag: false },
    ],
    dragOptions: {
      group: 'items',
      // 在拖拽过程中检查每个元素的 draggable 属性
      draggable: '.draggable',
    },
  

重新排序

使用自带的参数,这个参数能使该item不能被拖动,但是由于vuedraggable移动的时候是把移动的item拖到对应位置,其他item依次往前移,所以会导致导致不能移动的item会被动移动。例如123456,将1移动到4的位置,就会变为234156,而不是我个人以为的423156

交换移动的两个元素的次序

这个方式有bug就是元素不能斜着拖动,因为move会被触发多次,因为斜着拖到会碰到其他item。目前没想到怎么优化

<draggable v-model="list" :move='handleMove' @end='handleEnd' :options="dragOptions">
  <div class="item" v-for="item in list" :key="item.id">
    {{ item.name }}
  </div>
</draggable>
list: [
      { id: 1, name: 'Item 1' },
      { id: 2, name: 'Item 2' },
      { id: 3, name: 'Item 3' },
      { id: 4, name: 'Item 4' },
      { id: 5, name: 'Item 5' },
      { id: 6, name: 'Item 6' },
    ],
    dragOptions: {
      group: 'items',
      // 在拖拽过程中检查每个元素的 draggable 属性
      draggable: '.draggable',
    },
  handleEnd(event) {
      console.log(event, this.list);
      const { oldIndex, newIndex } = event;
      if (oldIndex !== newIndex) {
        // 将1换到4的位置
        this.list.splice(newIndex, 1, this.startItem);
        // 将4换到1的位置
        this.list.splice(oldIndex, 1, this.endItem);
      }
    },
    handleMove(a, b) {
      console.log(a, b);
      if (a.draggedContext.element.id === 2 || a.relatedContext.element.id === 2) {
      // false则不能拖到
        return false;
      }
      const oldIndex = a.draggedContext.index;
      const newIndex = a.draggedContext.futureIndex;
      if (oldIndex !== newIndex) {
      // 如果不在这里存储一下的话,end的方法拿不到。存储移动的元素和目的地的元素
        this.startItem = JSON.parse(JSON.stringify(a.draggedContext.element));
        this.endItem = JSON.parse(JSON.stringify(a.relatedContext.element));
      }
      return true;
    },

每次都重置item的index

这是看的别的方法,贼拉好用

<draggable v-model="list" :options="dragOptions">
  <div class="item" v-for="item in list" :key="item.id">
    {{ item.name }}
  </div>
</draggable>
list: [
      { id: 1, name: 'Item 1' },
      { id: 2, name: 'Item 2' },
      { id: 3, name: 'Item 3' },
      { id: 4, name: 'Item 4' },
      { id: 5, name: 'Item 5' },
      { id: 6, name: 'Item 6' },
    ],
    dragOptions: {
      group: 'items',
      // 在拖拽过程中检查每个元素的 draggable 属性
      draggable: '.draggable',
    },

watch: {
    list: {
      immediate: true,
      deep: true,
      handler(newV, oldV) {
        console.log(newV, oldV);
        const targetIndex = newV.findIndex(i => i.id === 2);
        console.log(targetIndex);
        // 找到item且item的位置变了
        if (targetIndex !== -1 && targetIndex !== 1) {
          // 找到id为2的元素,删除
          const targetItem = newV.splice(targetIndex, 1)[0];
          // 再index为1的地方插入。即可保证该数据用于处于1的位置
          newV.splice(1, 0, targetItem);
        }
      }
    }
  }
  
前端vue2+elementui帮我生成个前端页面,下需求1.2.3.4.5.6部分已经完成完成的代码在下边,仔细分析现有代码,优化下第三部分的部件和接口选项的宽度问题,完善7.8部分的需求,尤其是接口选项多选和主体中间部分的跨容器拖拽功能实现 1.竖向分为头部和主体部分,头部宽度100%,高度60px,头部左侧两个input框,lable分别为:用例名称,脚本编号,右侧3个按钮是生成用例,生成脚本,调试 2.主体部分占据剩下高度,分为左,中,右三部分,宽度占比分别为40%,40%,20%, 3.主体左侧为左右两部分,分别为部件和接口选项,两个均为可以手动打开关闭的抽屉(使用elementui中的el-drawer),重要点:我需要的是将部件用el-drawer包裹和接口选项整体用el-drawer包裹,有个箭头的按钮在整体的右侧部分控制el-drawer的开关,部件宽度为35%,接口选项宽度65%,两个抽屉没有层级关系,独立开启关闭,不会相互影响,无论开启或关闭组件在左,接口选项在右 4.部件部分:竖向个搜索框,输入关键字可对下面具体组件进行筛选,搜索框下是组件名称和图标,组件名称分别为:车门,车窗,座椅,前舱盖,雨刮器,光机幕布,后视镜这部分可使用 列表渲染,每个组件展示为行,左侧组件图标,右侧组件名称,,列,依次向下 5.接口选项部分,竖向个搜索框,输入关键字可对下面具体接口进行筛选,接口选项和左侧部件是有联动的,两者存在跨容器拖拽关系,拖动左侧部件中的个(每次只能拖动个),到右侧接口选项区域,会触发事件,调取后端接口获取对应组件的接口列表(这里可以使用模拟数据),展示在接口选项部分, 接口内容为:打开所有车窗。open_all_wins,控制车辆上电。power_on_off_control...,(可以放点模拟数据) 6.拖动组件到接口选项部分,每个组件对应的接口展示都是个独立的二级树形结构,树形结构级lable为组件名称,右侧有个可以X,可以删除当前组件,树形结构二级内容就为每个具体的接口,每个接口头部都有个多选按钮,可以展示多个,依次竖向向下展示 7.主体中间部分竖向依次为3部分,预置条件,测试步骤,环境恢复,宽度均占据主体中间部分宽度100%,高度分别为30%,40%,20%,左侧接口选项部分通过多选框勾选接口,勾选的接口组成个list,通过拖拽将接口选项中的接口拖拽到右侧主体中间部分展示,支多选批量拖拽 每个区域可视为独立区域,相互不影响,若个区域拖入多个接口,根据先后依次排序 ,拖拽至中间主体部分的接口,在各自独立区域内,接口排序可以通过上下拖拽调换, 8.主体右侧为参数配置:由上至下第行两个单选按钮,Machine,Relay,剩下为3个输入框,各占行,lable分别为:机械控制踏板踩下去和抬起来之间的时间间隔(秒),踏板踩下去和抬起来的时间间隔(秒),踏板踩下去或抬起来需要继电器打开的时间(秒) <template> <div id="app"> <!-- 头部区域 --> <div class="header"> <div class="header-left"> <el-input placeholder="请输入用例名称" class="header-input" v-model="caseName" > <template slot="prepend">用例名称</template> </el-input> <el-input placeholder="请输入脚本编号" class="header-input" v-model="scriptId" > <template slot="prepend">脚本编号</template> </el-input> </div> <div class="header-right"> <el-button type="primary" class="header-button">生成用例</el-button> <el-button type="success" class="header-button">生成脚本</el-button> <el-button type="warning" class="header-button">调试</el-button> </div> </div> <!-- 主体区域 --> <div class="main-container"> <!-- 左侧区域 --> <div class="left-section"> <!-- 部件抽屉 --> <div class="drawer-container"> <div class="drawer-wrapper" :style="{width: componentsDrawerOpen ? '280px' : '0'}"> <div class="components-drawer" v-show="componentsDrawerOpen"> <div class="components-container"> <div class="components-header"> <i class="el-icon-s-grid"></i> 车辆部件列表 </div> <el-input placeholder="搜索部件..." v-model="componentSearch" prefix-icon="el-icon-search" clearable ></el-input> <div style="margin-top: 15px; overflow-y: auto; height: calc(100% - 100px);"> <div class="component-item" v-for="component in filteredComponents" :key="component.id" draggable="true" @dragstart="dragStart(component)" > <div class="icon"> <i :class="component.icon"></i> </div> <div class="name">{{ component.name }}</div> </div> </div> </div> </div> </div> <div class="drawer-toggle" :class="{collapsed: !componentsDrawerOpen}" @click="componentsDrawerOpen = !componentsDrawerOpen" > <i class="el-icon-arrow-left"></i> </div> </div> <!-- 接口抽屉 --> <div class="drawer-container"> <div class="drawer-wrapper" :style="{width: interfacesDrawerOpen ? '500px' : '0'}"> <div class="interfaces-drawer" v-show="interfacesDrawerOpen"> <div class="interfaces-container"> <div class="interfaces-header"> <i class="el-icon-connection"></i> 接口选项 </div> <el-input placeholder="搜索接口..." v-model="interfaceSearch" prefix-icon="el-icon-search" clearable ></el-input> <div class="tree-container" @dragover.prevent @drop="dropComponent" > <div v-if="interfaceGroups.length === 0" class="drop-area"> <i class="el-icon-upload"></i> <div class="drop-hint">请从左侧拖动部件到此处以加载接口</div> </div> <div v-else> <transition-group name="fade"> <div class="tree-node" v-for="group in filteredInterfaceGroups" :key="group.id" > <div class="tree-header"> <div>{{ group.componentName }} 接口</div> <div class="delete-btn" @click="removeGroup(group.id)"> <i class="el-icon-close"></i> </div> </div> <div class="tree-content"> <div class="interface-item" v-for="item in group.interfaces" :key="item.id" > <el-checkbox v-model="item.selected"></el-checkbox> <div class="interface-name">{{ item.name }}</div> <div class="interface-id">{{ item.id }}</div> </div> </div> </div> </transition-group> </div> </div> </div> </div> </div> <div class="drawer-toggle" :class="{collapsed: !interfacesDrawerOpen}" @click="interfacesDrawerOpen = !interfacesDrawerOpen" > <i class="el-icon-arrow-left"></i> </div> </div> </div> <!-- 中间区域 --> <div class="center-section"> <div class="center-placeholder"> <i class="el-icon-s-marketing"></i> <div>用例执行区域</div> <div style="font-size: 14px; margin-top: 10px;">此处将展示用例执行流程和结果</div> </div> </div> <!-- 右侧区域 --> <div class="right-section"> <div class="right-placeholder"> <i class="el-icon-setting"></i> <div>参数配置区域</div> <div style="font-size: 14px; margin-top: 10px;">此处将展示脚本参数配置选项</div> </div> </div> </div> </div> </template> <script> import axios from "axios"; import { } from "@/api/api.js"; import draggable from "vuedraggable"; import Papa from "papaparse"; import * as echarts from "echarts"; import { Message } from "element-ui"; export default { components: { draggable }, data() { return { caseName: '', scriptId: '', componentsDrawerOpen: true, interfacesDrawerOpen: true, componentSearch: '', interfaceSearch: '', draggedComponent: null, // 部件数据 components: [ { id: 1, name: '车门', icon: 'el-icon-s-help' }, { id: 2, name: '车窗', icon: 'el-icon-s-promotion' }, { id: 3, name: '座椅', icon: 'el-icon-s-opportunity' }, { id: 4, name: '前舱盖', icon: 'el-icon-s-flag' }, { id: 5, name: '雨刮器', icon: 'el-icon-s-release' }, { id: 6, name: '光机幕布', icon: 'el-icon-s-platform' }, { id: 7, name: '后视镜', icon: 'el-icon-s-claim' } ], // 接口分组数据 interfaceGroups: [], nextGroupId: 1, // 模拟接口数据 mockInterfaces: { 1: [ // 车门接口 { id: 'door_lock', name: '车门锁定', selected: false }, { id: 'door_unlock', name: '车门解锁', selected: false }, { id: 'door_open', name: '打开车门', selected: false }, { id: 'door_close', name: '关闭车门', selected: false } ], 2: [ // 车窗接口 { id: 'window_open_all', name: '打开所有车窗', selected: false }, { id: 'window_close_all', name: '关闭所有车窗', selected: false }, { id: 'window_open_driver', name: '打开驾驶员车窗', selected: false }, { id: 'window_open_passenger', name: '打开乘客车窗', selected: false } ], 3: [ // 座椅接口 { id: 'seat_heat', name: '座椅加热', selected: false }, { id: 'seat_cool', name: '座椅通风', selected: false }, { id: 'seat_adjust', name: '座椅调节', selected: false }, { id: 'seat_massage', name: '座椅按摩', selected: false } ], 4: [ // 前舱盖接口 { id: 'hood_open', name: '打开前舱盖', selected: false }, { id: 'hood_close', name: '关闭前舱盖', selected: false } ], 5: [ // 雨刮器接口 { id: 'wiper_on', name: '开启雨刮器', selected: false }, { id: 'wiper_off', name: '关闭雨刮器', selected: false }, { id: 'wiper_speed', name: '调节雨刮速度', selected: false } ], 6: [ // 光机幕布接口 { id: 'screen_up', name: '升起幕布', selected: false }, { id: 'screen_down', name: '降下幕布', selected: false }, { id: 'screen_brightness', name: '调节亮度', selected: false } ], 7: [ // 后视镜接口 { id: 'mirror_adjust', name: '调节后视镜', selected: false }, { id: 'mirror_fold', name: '折叠后视镜', selected: false }, { id: 'mirror_heat', name: '后视镜加热', selected: false } ] } }; }, mounted() { }, computed: { // 过滤后的部件列表 filteredComponents() { if (!this.componentSearch) return this.components; return this.components.filter(comp => comp.name.toLowerCase().includes(this.componentSearch.toLowerCase()) ); }, // 过滤后的接口分组 filteredInterfaceGroups() { if (!this.interfaceSearch) return this.interfaceGroups; return this.interfaceGroups.map(group => { const filteredInterfaces = group.interfaces.filter(item => item.name.toLowerCase().includes(this.interfaceSearch.toLowerCase()) || item.id.toLowerCase().includes(this.interfaceSearch.toLowerCase()) ); // 只返回包含匹配接口的分组 if (filteredInterfaces.length > 0) { return { ...group, interfaces: filteredInterfaces }; } return null; }).filter(group => group !== null); } }, methods: { // 开始拖拽 dragStart(component) { this.draggedComponent = component; }, // 放置组件 dropComponent() { if (!this.draggedComponent) return; console.log( '11',JSON.stringify(this.draggedComponent.id)); // 检查是否已存在该组件的接口组 const exists = this.interfaceGroups.some(group => group.componentId === this.draggedComponent.id ); if (exists) { this.$message.warning(`已加载过${this.draggedComponent.name}的接口`); return; } // 模拟API请求获取接口数据 this.$message.success(`正在加载${this.draggedComponent.name}接口...`); // 模拟API延迟 setTimeout(() => { const interfaces = [...this.mockInterfaces[this.draggedComponent.id]]; this.interfaceGroups.push({ id: this.nextGroupId++, componentId: this.draggedComponent.id, componentName: this.draggedComponent.name, interfaces: interfaces }); this.$message.success(`成功加载${this.draggedComponent.name}接口`); }, 500); //this.draggedComponent = null; }, // 删除接口组 removeGroup(groupId) { this.interfaceGroups = this.interfaceGroups.filter(group => group.id !== groupId); this.$message.success('接口组已移除'); } }, watch: { }, beforeDestroy() { }, async created() { }, }; </script> <style scoped lang="less"> * { margin: 0; padding: 0; box-sizing: border-box; } html, body { height: 100%; font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Microsoft YaHei', sans-serif; background-color: #f5f7fa; overflow: hidden; } #app { height: 100%; display: flex; flex-direction: column; } /* 头部样式 */ .header { height: 60px; background: linear-gradient(135deg, #1e3c72, #2a5298); box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); display: flex; align-items: center; padding: 0 20px; color: white; z-index: 1000; } .header-left { display: flex; align-items: center; flex: 1; } .header-input { width: 280px; margin-right: 20px; background-color: rgba(255, 255, 255, 0.15); border-radius: 4px; border: none; } .header-input .el-input__inner { background-color: transparent; color: white; border: none; } .header-input .el-input__inner::placeholder { color: rgba(255, 255, 255, 0.6); } .header-right { display: flex; } .header-button { margin-left: 15px; background: linear-gradient(to right, #4facfe, #00f2fe); border: none; color: white; font-weight: bold; transition: all 0.3s; } .header-button:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); } /* 主体样式 */ .main-container { flex: 1; display: flex; overflow: hidden; background-color: #f0f2f5; } /* 左侧区域 */ .left-section { flex: 0 0 40%; display: flex; position: relative; background-color: #fff; box-shadow: 2px 0 8px rgba(0, 0, 0, 0.05); z-index: 1; } /* 中间区域 */ .center-section { flex: 0 0 40%; background-color: #fff; margin: 10px; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); display: flex; flex-direction: column; align-items: center; justify-content: center; color: #909399; font-size: 18px; position: relative; overflow: hidden; } .center-placeholder { text-align: center; } .center-placeholder i { font-size: 48px; margin-bottom: 20px; color: #c0c4cc; } /* 右侧区域 */ .right-section { flex: 0 0 20%; background-color: #fff; margin: 10px 10px 10px 0; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); display: flex; flex-direction: column; align-items: center; justify-content: center; color: #909399; font-size: 18px; } .right-placeholder { text-align: center; } .right-placeholder i { font-size: 48px; margin-bottom: 20px; color: #c0c4cc; } /* 抽屉样式 */ .drawer-container { height: 100%; display: flex; position: relative; } .drawer-wrapper { height: 100%; overflow: hidden; transition: all 0.3s; } .components-drawer { width: 280px; height: 100%; border-right: 1px solid #ebeef5; background-color: #fff; box-shadow: 2px 0 8px rgba(0, 0, 0, 0.05); } .interfaces-drawer { width: 500px; height: 100%; background-color: #fff; border-right: 1px solid #ebeef5; } .drawer-toggle { position: absolute; top: 50%; transform: translateY(-50%); right: -15px; width: 30px; height: 60px; background-color: #1e3c72; color: white; display: flex; align-items: center; justify-content: center; border-radius: 0 15px 15px 0; cursor: pointer; z-index: 10; box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1); transition: all 0.3s; } .drawer-toggle:hover { background-color: #2a5298; } .drawer-toggle i { transition: transform 0.3s; } .drawer-toggle.collapsed i { transform: rotate(180deg); } /* 部件区域样式 */ .components-container { height: 100%; display: flex; flex-direction: column; padding: 15px; } .components-header { margin-bottom: 15px; font-weight: bold; color: #1e3c72; border-bottom: 1px solid #ebeef5; padding-bottom: 10px; font-size: 16px; } .component-item { display: flex; align-items: center; padding: 12px 15px; margin-bottom: 8px; border-radius: 4px; cursor: move; transition: all 0.2s; border: 1px solid #ebeef5; background-color: #f9fafc; } .component-item:hover { background-color: #ecf5ff; border-color: #c6e2ff; transform: translateX(5px); } .component-item .icon { width: 24px; height: 24px; margin-right: 10px; background-color: #1e3c72; border-radius: 4px; display: flex; align-items: center; justify-content: center; color: white; font-size: 14px; } .component-item .name { font-size: 14px; color: #606266; } /* 接口选项区域 */ .interfaces-container { height: 100%; display: flex; flex-direction: column; padding: 15px; } .interfaces-header { margin-bottom: 15px; font-weight: bold; color: #1e3c72; border-bottom: 1px solid #ebeef5; padding-bottom: 10px; font-size: 16px; } .tree-container { flex: 1; overflow-y: auto; padding: 10px; border: 1px solid #ebeef5; border-radius: 4px; background-color: #f9fafc; } .tree-node { margin-bottom: 10px; border-radius: 4px; overflow: hidden; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); } .tree-header { display: flex; justify-content: space-between; align-items: center; padding: 10px 15px; background: linear-gradient(to right, #4facfe, #00f2fe); color: white; font-weight: bold; } .delete-btn { cursor: pointer; transition: all 0.2s; } .delete-btn:hover { transform: scale(1.2); } .tree-content { padding: 10px; background-color: white; } .interface-item { display: flex; align-items: center; padding: 8px 10px; border-bottom: 1px dashed #ebeef5; } .interface-item:last-child { border-bottom: none; } .interface-name { margin-left: 8px; font-size: 14px; color: #606266; } .interface-id { margin-left: 8px; font-size: 12px; color: #909399; font-family: monospace; } /* 拖拽区域 */ .drop-area { height: 100%; display: flex; align-items: center; justify-content: center; color: #909399; font-size: 16px; flex-direction: column; } .drop-area i { font-size: 48px; margin-bottom: 15px; color: #c0c4cc; } .drop-hint { text-align: center; max-width: 300px; } /* 动画效果 */ .fade-enter-active, .fade-leave-active { transition: opacity 0.5s; } .fade-enter, .fade-leave-to { opacity: 0; } </style>
07-05
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值