vue拖拽排序_你碰到了可拖拽排序的列表组件,应该这样搞他

最近写组件碰到一种可拖拽排序表格,搞死我,今天才搞好,无论如何我要写一篇文章来记录一下,老大前几天说,你来搞个那种可以拖拽排序的组件,我想了想,可拖拽排序是什么样子的,一开始以为是那种单独可以拖拽,再问问老大,结果说是这种效果的。

d038f2fa7df0699e676cc28a7f2b1dae.gif

一开始是想用vuedraggable 这个组件库,结果他么的没有详细的demo,我看不太懂,估计是我太年轻了吧,所以我在尝试之后,写出了一个这样的小组件,

<template>
  <div class="fy-list-draggable-wrap">
    <draggable :handle="handleClass"
               v-bind="dragOptions"
               @change="change">
      <!--TODO 这个地方可以用户自定义 -->
      <!--TODO 我们暂时就自己写好 -->
      <transition-group type="transition"
                        name="flip-list"
                        :tag="tag">
        <slot name="drag"></slot>
      </transition-group>
    </draggable>
  </div>
</template>
<script>
export default {
  name: 'fyListDraggable',
  props: {
    listDraggableData: {
      type: Array,
      default: function() {
        return [
          {
            name: 'Jean',
            id: 2,
          },
          {
            name: 'Juan 1',
            id: 1,
          },
          {
            name: 'Juan 2',
            id: 2,
          },
          {
            name: 'Juan 3',
            id: 3,
          },
          {
            name: 'Juan 4',
            id: 4,
          },
          {
            name: 'Juan 5',
            id: 5,
          },
          {
            name: 'Juan 6',
            id: 6,
          },
        ]
      },
    },
    tag: {
      type: String,
      default: '',
    },
    handleClass: {
      type: String,
      default: ""
    },
  },
  data() {
    return {}
  },
  methods: {
    change(evt) {
      console.log('evt===>', evt)
    },
    move(e) {
      console.log('move===>', e)
    },
  },
  computed: {
    dragOptions() {
      return {
        animation: 500,
        group: 'a',
        disabled: false,
        ghostClass: 'ghost',
      }
    },
    // getListDraggableData() {
    //   return this.listDraggableData
    // },
  },
  updated() {
    console.log("updated====>", this.listDraggableData)
  },
  watch: {
    listDraggableData: {
      handler: function(newVal, oldVal) {
        console.log('newVal===>', newVal, 'oldVal===>', oldVal)
      },
      deep: true
    }
  },
}
</script>
<style lang="scss">
.fy-list-draggable-wrap {
  user-select: none;
  display: -webkit-box;
  display: flex;
  -ms-flex-direction: column;
  -webkit-box-orient: vertical;
  -webkit-box-direction: normal;
  flex-direction: column;
  padding-left: 0;
  margin-bottom: 0;
  .list-group-item {
    position: relative;
    display: block;
    padding: 0.75rem 1.25rem;
    margin-bottom: -1px;
    background-color: #fff;
    border: 1px solid rgba(0, 0, 0, 0.125);
    &:first-child {
      border-top-left-radius: 0.25rem;
      border-top-right-radius: 0.25rem;
    }
    span {
      vertical-align: middle;
    }
    i.icon {
      // TODO 其实icon应该写进全局变量
      background: url('~@img/tag/default-message-search-select.png');
      display: inline-block;
      width: 20px;
      height: 20px;
      vertical-align: middle;
      cursor: move;
    }
  }
}
.ghost {
  opacity: 0.5;
  background: #c8ebfb;
}
.no-move {
  transition: transform 0s;
}

.flip-list-move {
  transition: transform 0.5s;
}
</style>

本来想直接使用自己二次封装的组件来看看是否满足公司的需求,结果在尝试使用的过程中出现了许多bug,死活不能拖拽,找了很多方式都解决不了,果断放弃,此路不通,我就另寻他路,为了不耽误时间,就寻找其他路子,好像这个 vuedraggable 组件 使用 sortable.js,最后还是使用 sortable.js 解决了

vuedraggable demo 如下

vuedraggable​sortablejs.github.io

我们来探索一下sortable.js

github地址如下:有很多配置选项,支持vue的

https://github.com/SortableJS/Sortable#options​github.com

你可以这样安装 sortable.js

750aee3e0057c6879f4e26c7c792fad8.png

简单用法

<ul id="items">
	<li>item 1</li>
	<li>item 2</li>
	<li>item 3</li>
</ul>
var el = document.getElementById('items');
var sortable = Sortable.create(el);

配置选项

var sortable = new Sortable(el, {
	group: "name",  // or { name: "...", pull: [true, false, 'clone', array], put: [true, false, array] }
	sort: true,  // sorting inside list
	delay: 0, // time in milliseconds to define when the sorting should start
	delayOnTouchOnly: false, // only delay if user is using touch
	touchStartThreshold: 0, // px, how many pixels the point should move before cancelling a delayed drag event
	disabled: false, // Disables the sortable if set to true.
	store: null,  // @see Store
	animation: 150,  // ms, animation speed moving items when sorting, `0` — without animation
	easing: "cubic-bezier(1, 0, 0, 1)", // Easing for animation. Defaults to null. See https://easings.net/ for examples.
	handle: ".my-handle",  // Drag handle selector within list items
	filter: ".ignore-elements",  // Selectors that do not lead to dragging (String or Function)
	preventOnFilter: true, // Call `event.preventDefault()` when triggered `filter`
	draggable: ".item",  // Specifies which items inside the element should be draggable

	dataIdAttr: 'data-id',

	ghostClass: "sortable-ghost",  // Class name for the drop placeholder
	chosenClass: "sortable-chosen",  // Class name for the chosen item
	dragClass: "sortable-drag",  // Class name for the dragging item

	swapThreshold: 1, // Threshold of the swap zone
	invertSwap: false, // Will always use inverted swap zone if set to true
	invertedSwapThreshold: 1, // Threshold of the inverted swap zone (will be set to swapThreshold value by default)
	direction: 'horizontal', // Direction of Sortable (will be detected automatically if not given)

	forceFallback: false,  // ignore the HTML5 DnD behaviour and force the fallback to kick in

	fallbackClass: "sortable-fallback",  // Class name for the cloned DOM Element when using forceFallback
	fallbackOnBody: false,  // Appends the cloned DOM Element into the Document's Body
	fallbackTolerance: 0, // Specify in pixels how far the mouse should move before it's considered as a drag.

	dragoverBubble: false,
	removeCloneOnHide: true, // Remove the clone element when it is not showing, rather than just hiding it
	emptyInsertThreshold: 5, // px, distance mouse must be from empty sortable to insert drag element into it


	setData: function (/** DataTransfer */dataTransfer, /** HTMLElement*/dragEl) {
		dataTransfer.setData('Text', dragEl.textContent); // `dataTransfer` object of HTML5 DragEvent
	},

	// Element is chosen
	onChoose: function (/**Event*/evt) {
		evt.oldIndex;  // element index within parent
	},

	// Element is unchosen
	onUnchoose: function(/**Event*/evt) {
		// same properties as onEnd
	},

	// Element dragging started
	onStart: function (/**Event*/evt) {
		evt.oldIndex;  // element index within parent
	},

	// Element dragging ended
	onEnd: function (/**Event*/evt) {
		var itemEl = evt.item;  // dragged HTMLElement
		evt.to;    // target list
		evt.from;  // previous list
		evt.oldIndex;  // element's old index within old parent
		evt.newIndex;  // element's new index within new parent
		evt.oldDraggableIndex; // element's old index within old parent, only counting draggable elements
		evt.newDraggableIndex; // element's new index within new parent, only counting draggable elements
		evt.clone // the clone element
		evt.pullMode;  // when item is in another sortable: `"clone"` if cloning, `true` if moving
	},

	// Element is dropped into the list from another list
	onAdd: function (/**Event*/evt) {
		// same properties as onEnd
	},

	// Changed sorting within list
	onUpdate: function (/**Event*/evt) {
		// same properties as onEnd
	},

	// Called by any change to the list (add / update / remove)
	onSort: function (/**Event*/evt) {
		// same properties as onEnd
	},

	// Element is removed from the list into another list
	onRemove: function (/**Event*/evt) {
		// same properties as onEnd
	},

	// Attempt to drag a filtered element
	onFilter: function (/**Event*/evt) {
		var itemEl = evt.item;  // HTMLElement receiving the `mousedown|tapstart` event.
	},

	// Event when you move an item in the list or between lists
	onMove: function (/**Event*/evt, /**Event*/originalEvent) {
		// Example: https://jsbin.com/nawahef/edit?js,output
		evt.dragged; // dragged HTMLElement
		evt.draggedRect; // DOMRect {left, top, right, bottom}
		evt.related; // HTMLElement on which have guided
		evt.relatedRect; // DOMRect
		evt.willInsertAfter; // Boolean that is true if Sortable will insert drag element after target by default
		originalEvent.clientY; // mouse position
		// return false; — for cancel
		// return -1; — insert before target
		// return 1; — insert after target
	},

	// Called when creating a clone of element
	onClone: function (/**Event*/evt) {
		var origEl = evt.item;
		var cloneEl = evt.clone;
	},

	// Called when dragging element changes position
	onChange: function(/**Event*/evt) {
		evt.newIndex // most likely why this event is used is to get the dragging element's current index
		// same properties as onEnd
	}
});

官网还有很多例子,大家可以上 github 看看,或许你以后就会碰到这种需求啦。

因为我们公司的组件都是改写 element-ui 中的组件 符合我们公司的业务需求。

我们我自己的一个 fy-table 组件如下,等会就会用到这个table组件 这个是我们改造element-ui el-table

<template>
  <div class="table-wrap">
    <!-- 表格 -->
    <el-table
      :border="border"
      :data="tableData"
      :default-sort="defaultSort"
      :filterjson="filterjson"
      :header-cell-style="headerCellStyle"
      :height="height"
      :is-operation="isOperation"
      :placement-array="placementArray"
      :row-class-name="tableRowClassName"
      @header-contextmenu="headerContextmenu"
      @re-height="reHeights"
      @row-contextmenu="rowContextmenu"
      @selection-change="selectionChange"
      header-align="headerAlign"
      id="table"
      style="width: 100%;"
      :row-key="rowKey"
    >
      <slot></slot>
      <el-table-column fixed="right" label="操作" prop="operation" v-if="isOperation">
        <template slot-scope="scope">
          <fy-ripple-button @click="setTop(scope.row)" size="mini">置顶</fy-ripple-button>
        </template>
      </el-table-column>
    </el-table>
    <!-- 加载更多 -->

    <div class="table-footer" v-if="isLoadMore">
      <div :class="[isDisable ? 'disable-loading-more' : '']" @click="loadMore()" class="loading-more" v-if="loaded">加载更多...</div>
      <div class="loading-more" v-else>加载完成</div>
      <!-- <div class="pagnition-wrapper">
        <fy-pagination
          :current-page="afterCurrentPage"
          :layout="layout"
          :page-size="size"
          :page-sizes="pageSizes"
          :pager-count="pagerCount"
          :total="total"
          @handle-current-change="handleCurrentChange"
          @handle-size-change="handleSizeChange"
        ></fy-pagination>
      </div>-->
    </div>

    <!--鼠标右键菜单栏,其实就是添加一个基于鼠标位置的模态框 -->
    <div v-show="menuVisible">
      <ul class="menu" id="menu">
        <li @click="clickSetTop" class="menu-item" v-if="isOperation">置顶</li>
        <li @click="clickCanceltop" class="menu-item" v-if="isOperation">取消</li>
        <li
          @click="clickeEvent(clickMenu.fnName)"
          class="menu-item icon-right-arrow"
          v-for="(clickMenu,index) in clickMenus"
        >
          {{clickMenu.text}}
          <span class="right-arrow" v-show="clickMenu.otherMenu"></span>

          <ul class="secondary-menu">
            <li
              @click="clickeEvent(item.fnName)"
              class="menu-item"
              v-for="(item,index) in clickMenu.otherMenu"
            >{{item.text}}</li>
          </ul>
        </li>
      </ul>
    </div>
  </div>
</template>
<script>
import { constants } from 'crypto'
export default {
  name: 'fyTable',
  data() {
    return {
      data: [],
      placementArray: [], // 表头固定行数据
      menuVisible: false,
      clickRow: {}, // 点击行的数据
      clickChangeData: {}, // 复制一份行的数据
      rowRepeat: '',
      highlightKey: null,
      highlightVal: null,
      flag: true,
      height: 0,
      current: 1,
      isDisable: false, // 避免按钮重复点击
      loaded: false,
    }
  },
  props: {
    rowKey: {
      type: String,
      default: ""
    },
    // 主体数据
    tableData: {
      type: Array,
      default: function _default() {
        return []
      },
    },
    // 鼠标右键菜单
    contextmenu: {
      type: Object,
      default: function _default() {
        return { height: 0, width: -10 }
      },
    },
    // 特殊列处理
    cellType: {
      type: Array,
      default: function _default() {
        return []
      },
    },
    // 设置表格高度
    tableHeight: {
      type: Number,
    },
    // 设置表格最大高度
    maxHeight: {
      type: Number,
    },
    // 设置高亮行
    rowHighlight: {
      type: Object,
      default: function _default() {
        return { default: 'defaults' }
      },
    },
    // 过滤不需要显示在列表中的数据
    filterjson: {
      type: Object,
    },
    // 是否启用操作列
    isOperation: {
      type: Boolean,
      default: false,
    },
    //是否启用加载更多
    isLoadMore: {
      type: Boolean,
      default: false,
    },
    //避免重复
    isRepeat: {
      type: String,
      default: '',
    },
    //总数据长度
    total: {
      type: Number,
    },
    defaultSort: {
      type: Object,
      default: function _default() {
        return {}
      },
    },
    border: {
      type: Boolean,
      default: false,
    },
    headerStyle: {
      type: Object,
      default: function _default() {
        return { rowIndex: 0, style: '' }
      },
    },
    // 右键点击事件
    clickMenus: {
      type: Array,
      default: function _default() {
        return []
      },
    },
    // 右键点击事件特殊处理的key
    clickMenuKey: {
      type: String,
      default: '',
    },
    // 右键点击事件特殊处理的value
    clickMenuObject: {
      type: Object,
      default: function _default() {
        return {}
      },
    },
    // 表头对齐方式
    headerAlign: {
      type: String,
      default: 'left',
    },
  },
  created() {
    this.height = this.tableHeight
    this.rowRepeat = this.isRepeat
    this.highlightKey = Object.keys(this.rowHighlight)[0]
    this.highlightVal = this.rowHighlight[this.highlightKey]
    if (this.total <= this.tableData.length || this.tableData.length == 0) {
      this.loaded = false
    } else {
      this.loaded = true
    }
  },
  watch: {
    tableData: {
      handler() {
        if (this.total <= this.tableData.length) {
          this.loaded = false
        } else {
          this.loaded = true
        }
        this.isDisable = false
      },
    },
  },
  methods: {
    reHeights(value) {
      this.height = value - 0.1
    },
    // 多选
    selectionChange(val) {
      this.$emit('selection-change', val)
    },
    // 高亮样式控制
    tableRowClassName({ row, rowIndex }) {
      if (
        row[this.highlightKey] == this.highlightVal &&
        !this.$_utils.isEmpty(this.highlightVal)
      ) {
        return 'success-row'
      }
      return ''
    },
    rowContextmenu(row, column, event) {
      // 先把模态框关死,目的是 第二次或者第n次右键鼠标的时候 它默认的是true
      this.menuVisible = false
      this.menuVisible = true // 显示模态窗口,跳出自定义菜单栏
      this.clickRow = row // 当前点击行数据
      this.clickChangeData = row // 行数据复制给一个新的对象
      var menu = document.querySelector('#menu')
      var table = document.querySelector('#table')
      // 右键模态框显示数据处理
      if (this.clickMenuKey != '' && this.clickMenuObject != {}) {
        let index = this.clickMenus.indexOf(this.clickMenuObject)
        if (row[this.clickMenuKey] == this.clickMenuObject.flag) {
          if (index == -1) {
            this.clickMenus.push(this.clickMenuObject)
          }
        } else {
          if (index > -1) {
            this.clickMenus.splice(index, 1)
          }
        }
      }
      table.oncontextmenu = function(e) {
        // 左键--button属性=1,右键button属性=2
        if (e.button == 2) {
          e.preventDefault()
        }
      }
      menu.style.left = event.clientX - this.contextmenu.width  + 'px'
      // 给整个document添加监听鼠标事件,点击任何位置执行foo方法
      document.addEventListener('click', this.foo)
      menu.style.top = event.clientY - this.contextmenu.height + 'px'
    },
    foo() {
      // 取消鼠标监听事件
      this.menuVisible = false
      document.removeEventListener('click', this.foo) // 要及时关掉监听
    },
    headerContextmenu(column, event) {
      this.rowContextmenu(column, '', event)
    },
    setTop(row) {
      // row是当前点击的行数据
      this.height = this.height + 0.1
      this.flag = true
      for (var number in this.placementArray) {
        // 根据传来的字段名判断需添加的行是否已存在,避免重复添加
        if (this.rowRepeat) {
          if (
            this.placementArray[number][this.rowRepeat] == row[this.rowRepeat]
          ) {
            this.flag = false
          }
        }
      }
      if (this.flag && this.placementArray.length < 3) {
        if (this.isOperation) {
          row.operation = '取消' //button按钮value值
        }
        this.placementArray.unshift(row)
        for (var index in this.cellType) {
          var key = Object.keys(this.cellType[index])[0]
          this.placementArray[key] = this.cellType[index][key]
        }
      }
    },
    // 置顶
    clickSetTop() {
      // 将对象序列化再解析回来,就是重新复制一份对象,修改而不影响原有对象
      let clickRowData = JSON.parse(JSON.stringify(this.clickChangeData)) // 需要渲染的置顶数据
      let filtersData = this.filterjson // 需要移除的数据
      for (let index in clickRowData) {
        if (clickRowData[index] == filtersData[index]) {
          delete clickRowData[index]
        }
      }
      this.setTop(clickRowData)
    },
    // 去掉置顶
    clickCanceltop() {
      var row = this.clickRow
      this.height = this.height - 0.1
      for (var index in this.placementArray) {
        if (this.placementArray[index] == row) {
          this.placementArray.splice(index, 1)
          break
        }
      }
    },
    headerCellStyle({ row, column, rowIndex, columnIndex }) {
      if (rowIndex === this.headerStyle.rowIndex) {
        return this.headerStyle.style
      }
    },
    loadMore() {
      this.isDisable = true
      this.$emit('load-more', this.current + 1)
    },
    clickeEvent(val) {
      let param = new Object()
      param.type = val
      param.rowData = this.clickRow
      this.$emit('click-menu-event', param)
    },
  },
}
</script>
<style lang="scss">
@import '~@css/components/table/fyTable.scss';
</style>

我放弃上面那个 vuedraggable 组件之后,就直接在 index.vue 中测试起来了

<template>
  <div class="index-wrap common-wrap">
    <!-- 表格 -->
    <!-- TODO 注意fy-table这里row-key很重要,没有他,,会导致排序出问题 -->
    <!-- 固定标题 -->
    <!-- TODO 为啥这里需要两个变量col, dropCol,才能联动起来 -->
    <div class="fixed-list-title-wrap">
      <ul id="ul">
        <li v-for="(item, index) in col"
            :key="`col_${index}`"
            :prop="dropCol[index].prop"
            :label="item.label">
          {{dropCol[index].label}}
        </li>
      </ul>
    </div>
    <div class="index-order-list-wrap">
      <fy-table :cell-type="allTest"
                :filterjson="filterjson"
                :height="maxHeight"
                :is-load-more="true"
                :is-repeat="isRepeat"
                :table-data="tableData"
                :is-operation="true"
                row-key="id">
        <!-- <el-table-column :key="1" prop="isShowCompany" class="el-table-column">
                  <template slot-scope="scope">
                    <img
                      alt="公司logo"
                      class="table-company-logo"
                      src="@img/table/company-logo.png"
                      v-show="scope.row.isShowCompany == 1">
                  </template>
                </el-table-column> -->
        <!-- TODO 还是搞不懂为何这里一定要使用两个变量 col,dropCol -->
        <el-table-column v-for="(item, index) in col"
                         :key="`col_${index}`"
                         :label="item.label"
                         :prop="dropCol[index].prop"></el-table-column>
      </fy-table>
    </div>
  </div>
</template>

<script>
import Sortable from 'sortablejs'
export default {
  name: 'index',
  data() {
    return {
      XM: XM,
      // 右键菜单文本
      clickMenus: [
        {
          text: '核算项明细',
          fnName: 'suppleAccountDetail',
        },
        {
          text: '编辑',
          fnName: 'edit',
        },
        {
          text: '删除',
          fnName: 'del',
        },
      ],
      // 表单的数据
      activeName: 'orderList', // 默认订单列表
      maxHeight: 650, // 表格的最大高度
      isRepeat: 'planNumber',
      rowHighlight: { isSale: 0 },
      filterjson: {
        // 过滤的json数据
        isShowCompany: 1, // 是否显示公司的logo
        isSale: 1,
        isStopSale: 0, // 是否停止销售
        expired: 0, // 出团日期是否已过期
      },
      allTest: [
        {
          isSale: {
            key: 'isSale', // 对应table里的字段
            value: [0, 1], // 逻辑判断的条件
            // text: ['',''],// td里额外展示的文本值,可根据具体情况选择: 1.是否添加 2.字符串类型 3.数组类型
            type: ['img', 'div'], // td特殊处理的dom类型
            typeValue: [require('@img/table/flag.png'), ''], // td特殊处理的dom类型所对应的值
          },
          isShowCompany: {
            key: 'isShowCompany', // 对应table里的字段
            value: [0, 1], // 逻辑判断的条件
            // text: ['',''],// td里额外展示的文本值,可根据具体情况选择: 1.是否添加 2.字符串类型 3.数组类型
            type: ['img', 'div'], // td特殊处理的dom类型
            typeValue: [require('@img/table/company-logo.png'), ''], // td特殊处理的dom类型所对应的值
          },
        },
      ],
      tableData: [
        {
          isShowCompany: 1, // 是否显示公司的logo
          isSale: 1,
          name:
            '【Club Med一价全包•欢乐无限】日本•全新北海道•星野度假村•随心所欲自由行(单订房,天天出发 )EBB ',
          planNumber: 'SZGL-1ACHEC60-190122-012',
          label: '-',
          productId: 'C10863',
          planId: 'J676727',
          groupType: '自组团',
          level: '标准团',
          startTime: '2019-01-22',
          adultPrice: '9,416',
          childPrice: '5,785',
          orderNumber: '0+0+0+0',
          planNum: '7',
          remainNumber: '7',
          document: '名',
          id: 1,
        },
        {
          isShowCompany: 1, // 是否显示公司的logo
          isSale: 1,
          name:
            '【Club Med一价全包•欢乐无限】日本•全新北海道•星野度假村•随心所欲自由行(单订房,天天出发 )EBB ',
          planNumber: 'SZGL-1ACHEC60-190122-013',
          label: '-',
          productId: 'C10863',
          planId: 'J676727',
          groupType: '自组团',
          level: '标准团',
          startTime: '2019-01-22',
          adultPrice: '9,416',
          childPrice: '5,785',
          orderNumber: '0+0+0+0',
          planNum: '7',
          remainNumber: '7',
          document: '名',
          id: 2,
        },
        {
          isShowCompany: 1, // 是否显示公司的logo
          isSale: 1,
          name:
            '【Club Med一价全包•欢乐无限】日本•全新北海道•星野度假村•随心所欲自由行(单订房,天天出发 )EBB ',
          planNumber: 'SZGL-1ACHEC60-190122-014',
          label: '-',
          productId: 'C10863',
          planId: 'J676727',
          groupType: '自组团',
          level: '标准团',
          startTime: '2019-01-22',
          adultPrice: '9,416',
          childPrice: '5,785',
          orderNumber: '0+0+0+0',
          planNum: '7',
          remainNumber: '7',
          document: '名',
          id: 3,
        },
        {
          isShowCompany: 1, // 是否显示公司的logo
          isSale: 1,
          name:
            '【Club Med一价全包•欢乐无限】日本•全新北海道•星野度假村•随心所欲自由行(单订房,天天出发 )EBB ',
          planNumber: 'SZGL-1ACHEC60-190122-015',
          label: '-',
          productId: 'C10863',
          planId: 'J676727',
          groupType: '自组团',
          level: '标准团',
          startTime: '2019-01-22',
          adultPrice: '9,416',
          childPrice: '5,785',
          orderNumber: '0+0+0+0',
          planNum: '7',
          remainNumber: '7',
          document: '名',
          id: 4,
        },
        {
          isShowCompany: 1, // 是否显示公司的logo
          isSale: 1,
          name:
            '【Club Med一价全包•欢乐无限】日本•全新北海道•星野度假村•随心所欲自由行(单订房,天天出发 )EBB ',
          planNumber: 'SZGL-1ACHEC60-190122-016',
          label: '-',
          productId: 'C10863',
          planId: 'J676727',
          groupType: '自组团',
          level: '标准团',
          startTime: '2019-01-22',
          adultPrice: '9,416',
          childPrice: '5,785',
          orderNumber: '0+0+0+0',
          planNum: '7',
          remainNumber: '7',
          document: '名',
          id: 5,
        },
        {
          isShowCompany: 1, // 是否显示公司的logo
          expired: 1, // 出团日期是否已过期,1表示过期,0表示未过期
          isSale: 1,
          name:
            '【Club Med一价全包•欢乐无限】日本•全新北海道•星野度假村•随心所欲自由行(单订房,天天出发 )EBB ',
          planNumber: 'SZGL-1ACHEC60-190122-017',
          label: '-',
          productId: 'C10863',
          planId: 'J676727',
          groupType: '自组团',
          level: '标准团',
          startTime: '2019-01-22',
          adultPrice: '9,416',
          childPrice: '5,785',
          orderNumber: '0+0+0+0',
          planNum: '7',
          remainNumber: '7',
          document: '名',
          id: 6,
        },
        {
          isShowCompany: 1, // 是否显示公司的logo
          expired: 1, // 出团日期是否已过期,1表示过期,0表示未过期
          isSale: 1,
          name:
            '【Club Med一价全包•欢乐无限】日本•全新北海道•星野度假村•随心所欲自由行(单订房,天天出发 )EBB ',
          planNumber: 'SZGL-1ACHEC60-190122-018',
          label: '-',
          productId: 'C10863',
          planId: 'J676727',
          groupType: '自组团',
          level: '标准团',
          startTime: '2019-01-22',
          adultPrice: '9,416',
          childPrice: '5,785',
          orderNumber: '0+0+0+0',
          planNum: '7',
          remainNumber: '7',
          document: '名',
          id: 7,
        },
        {
          isShowCompany: 1, // 是否显示公司的logo
          expired: 1, // 出团日期是否已过期,1表示过期,0表示未过期
          isSale: 1,
          name:
            '【Club Med一价全包•欢乐无限】日本•全新北海道•星野度假村•随心所欲自由行(单订房,天天出发 )EBB ',
          planNumber: 'SZGL-1ACHEC60-190122-019',
          label: '-',
          productId: 'C10863',
          planId: 'J676727',
          groupType: '自组团',
          level: '标准团',
          startTime: '2019-01-22',
          adultPrice: '9,416',
          childPrice: '5,785',
          orderNumber: '0+0+0+0',
          planNum: '7',
          remainNumber: '7',
          document: '名',
          id: 8,
        },
        {
          isShowCompany: 1, // 是否显示公司的logo
          expired: 1, // 出团日期是否已过期,1表示过期,0表示未过期
          isSale: 1,
          name:
            '【Club Med一价全包•欢乐无限】日本•全新北海道•星野度假村•随心所欲自由行(单订房,天天出发 )EBB ',
          planNumber: 'SZGL-1ACHEC60-190122-020',
          label: '-',
          productId: 'C10863',
          planId: 'J676727',
          groupType: '自组团',
          level: '标准团',
          startTime: '2019-01-22',
          adultPrice: '9,416',
          childPrice: '5,785',
          orderNumber: '0+0+0+0',
          planNum: '7',
          remainNumber: '7',
          document: '名',
          id: 9,
        },
        {
          isShowCompany: 1, // 是否显示公司的logo
          isStopSale: 1, // 是否停止销售
          expired: 1, // 出团日期是否已过期,1表示过期,0表示未过期
          isSale: 1,
          name:
            '【Club Med一价全包•欢乐无限】日本•全新北海道•星野度假村•随心所欲自由行(单订房,天天出发 )EBB ',
          planNumber: 'SZGL-1ACHEC60-190122-021',
          label: '-',
          productId: 'C10863',
          planId: 'J676727',
          groupType: '自组团',
          level: '标准团',
          startTime: '2019-01-22',
          adultPrice: '9,416',
          childPrice: '5,785',
          orderNumber: '0+0+0+0',
          planNum: '7',
          remainNumber: '7',
          document: '名',
          id: 10,
        },
        {
          isShowCompany: 1, // 是否显示公司的logo
          isStopSale: 1, // 是否停止销售
          expired: 1, // 出团日期是否已过期,1表示过期,0表示未过期
          isSale: 1,
          name: '【Club Med一价全包•欢乐无限】',
          planNumber: 'SZGL-1ACHEC60-190122-022',
          label: '-',
          productId: 'C10863',
          planId: 'J676727',
          groupType: '自组团',
          level: '标准团',
          startTime: '2019-01-22',
          adultPrice: '9,416',
          childPrice: '5,785',
          orderNumber: '0+0+0+0',
          planNum: '7',
          remainNumber: '7',
          document: '名',
          id: 11,
        },
      ],
      dropCol: [
        {
          label: '公司图标',
          prop: 'isShowCompany',
        },
        {
          label: '团队名称',
          prop: 'name',
        },
        {
          label: '计划编号',
          prop: 'planNumber',
        },
        {
          label: '标签',
          prop: 'label',
        },
        {
          label: '产品ID',
          prop: 'productId',
        },
        {
          label: '计划ID',
          prop: 'planId',
        },
        {
          label: '团队类型',
          prop: 'groupType',
        },
        {
          label: '等级',
          prop: 'level',
        },
        {
          label: '出发时间',
          prop: 'startTime',
        },
        {
          label: '成人价',
          prop: 'adultPrice',
        },
        {
          label: '儿童价',
          prop: 'childPrice',
        },
        {
          label: '订单号',
          prop: 'orderNumber',
        },
        {
          label: '计划数',
          prop: 'planNum',
        },
        {
          label: '剩余数',
          prop: 'remainNumber',
        },
        {
          label: '文档',
          prop: 'document',
        },
      ],
      col: [
        {
          label: '公司图标',
          prop: 'isShowCompany',
        },
        {
          label: '团队名称',
          prop: 'name',
        },
        {
          label: '计划编号',
          prop: 'planNumber',
        },
        {
          label: '标签',
          prop: 'label',
        },
        {
          label: '产品ID',
          prop: 'productId',
        },
        {
          label: '计划ID',
          prop: 'planId',
        },
        {
          label: '团队类型',
          prop: 'groupType',
        },
        {
          label: '等级',
          prop: 'level',
        },
        {
          label: '出发时间',
          prop: 'startTime',
        },
        {
          label: '成人价',
          prop: 'adultPrice',
        },
        {
          label: '儿童价',
          prop: 'childPrice',
        },
        {
          label: '订单号',
          prop: 'orderNumber',
        },
        {
          label: '计划数',
          prop: 'planNum',
        },
        {
          label: '剩余数',
          prop: 'remainNumber',
        },
        {
          label: '文档',
          prop: 'document',
        },
      ],
    }
  },
  created() {
    this.filterjson
    // 页面第一次进来的时候,我们要把数据存进 storage 里面, 也要存进 state
    // 如何保存,以及获取
    if (this.$_storage.get('dropCol')) {
      this.$_store.dispatch(
        'dropCol/pushDropCol',
        this.$_storage.get('dropCol'),
      )
      
    }
    if(this.$_storage.get('col')) {
      this.$_store.dispatch(
        'dropCol/pushCol',
        this.$_storage.get('col')
      )
    }

    if(this.$_storage.get('tableData')) {
      this.$store.dispatch(
        'dropCol/pushTableData',
        this.$_storage.get('tableData')
      )
    }
    // 在页面刷新时将vuex里的信息保存到localStorage里
    let self = this
    window.addEventListener('beforeunload', () => {
      self.$_storage.set('dropCol', self.dropCol)
      self.$_storage.set('col', self.col)
      self.$_storage.set('tableData', self.tableData)
      self.$store.dispatch('dropCol/pushDropCol', self.dropCol)
      self.$store.dispatch('dropCol/pushCol', self.col)
      self.$store.dispatch('dropCol/pushTableData', self.tableData)
    })
  },
  mounted() {
    this.rowDrop()
    this.columnDrop()
    this.dropUl()
  },
  methods: {
    searchHandler() {
      this.$store.commit('search/updateIsShow', 0)
    },
    //行拖拽
    rowDrop() {
      const tbody = document.querySelector('.el-table__body-wrapper tbody')
      const _this = this
      Sortable.create(tbody, {
        animation: 500,
        delay: 0,
        onEnd({ newIndex, oldIndex }) {
          console.log('row===>', newIndex, oldIndex)
          const currRow = _this.tableData.splice(oldIndex, 1)[0]
          _this.tableData.splice(newIndex, 0, currRow)
          _this.$store.dispatch("dropCol/pushTableData", _this.tableData)
        },
        onUpdate(evt) {
          console.log('evt===>', evt)
          // console.log("tableData==>", _this.tableData)
        },
      })
    },
    //列拖拽
    columnDrop() {
      const _this = this
      const wrapperTr = document.querySelector('.el-table__header-wrapper tr')
      this.sortable = Sortable.create(wrapperTr, {
        animation: 500,
        delay: 0,
        onEnd(evt) {
          const oldItem = _this.dropCol[evt.oldIndex]
          _this.dropCol.splice(evt.oldIndex, 1)
          _this.dropCol.splice(evt.newIndex, 0, oldItem)
          _this.$store.dispatch("dropCol/pushDropCol", _this.dropCol)
          _this.$store.dispatch("dropCol/pushCol", _this.col)
        },
        onUpdate: evt => {
          // console.log("dropCol===>", _this.dropCol)
        },
      })
    },

    dropUl() {
      const _this = this
      const wrapperTr = document.querySelector('.fixed-list-title-wrap ul')
      this.sortable = Sortable.create(wrapperTr, {
        animation: 500,
        delay: 0,
        onEnd(evt) {
          // 一个变量管 label 属性
          const oldItem = _this.col[evt.oldIndex]
          _this.col.splice(evt.oldIndex, 1)
          _this.col.splice(evt.newIndex, 0, oldItem)

          // 一个变量管 prop 属性
          const oldItem1 = _this.dropCol[evt.oldIndex]
          _this.dropCol.splice(evt.oldIndex, 1)
          _this.dropCol.splice(evt.newIndex, 0, oldItem1)
          _this.$store.dispatch("dropCol/pushDropCol", _this.dropCol)
          _this.$store.dispatch("dropCol/pushCol", _this.col)
        },
        onUpdate: evt => {
          // console.log("dropCol===>", _this.dropCol)
        },
      })
    },
  },
  computed: {},
  updated() {
    // console.log("tableData===>", this.tableData)
  },
}
</script>

<style lang="scss">
@import '~@css/pages/index/index.scss';
.fixed-list-title-wrap {
  position: fixed;
  z-index: 1000;
  right: 0;
  user-select: none;
  box-shadow: 0px 0px 5px 0px rgba(161, 119, 119, 0.08);
  ul {
    li {
      width: 100px;
      height: 30px;
      line-height: 30px;
      font-weight: 500;
      text-align: center;
      background: gray;
      &:hover {
        background: #fff;
      }
    }
  }
}
</style

其实组件没什么,问题是碰到的问题才有意思,这个行拖拽和列拖拽一开始是有问题,总是出现行拖拽,视图没有更新,但是又好像是更新了,

1bf48e1bc252c62459a772ed96b6a2d4.png

但是实际上splice()方法是可以改变数组长度,以及更新视图的,官网也说了。所以断定不是这个问题,接下来就是百度,看到有一个说就是 v-for 里面的key没有绑定,那就来绑定 key

23df2499fe9f9cf7d137f8cdc94e3fef.png

但是只是绑定了key之后,还是不行啊,还是原来那个样子,只能再百度,看看有没有人碰到这种例子。。

结果还真有,就是在 fy-table 中加上 row-key="id", 这是一个唯一 id,必须要添加的,否则就会出现排序混乱的结果。

这一点是及其重要的,行拖拽排序,重要代码如下:

rowDrop() {
      const tbody = document.querySelector('.el-table__body-wrapper tbody')
      const _this = this
      Sortable.create(tbody, {
        animation: 500,
        delay: 0,
        onEnd({ newIndex, oldIndex }) {
          console.log('row===>', newIndex, oldIndex)
          const currRow = _this.tableData.splice(oldIndex, 1)[0]
          _this.tableData.splice(newIndex, 0, currRow)
          _this.$store.dispatch("dropCol/pushTableData", _this.tableData)
        },
        onUpdate(evt) {
          console.log('evt===>', evt)
          // console.log("tableData==>", _this.tableData)
        },
      })
    },

列排序拖拽重要代码如下:

columnDrop() {
      const _this = this
      const wrapperTr = document.querySelector('.el-table__header-wrapper tr')
      this.sortable = Sortable.create(wrapperTr, {
        animation: 500,
        delay: 0,
        onEnd(evt) {
          const oldItem = _this.dropCol[evt.oldIndex]
          _this.dropCol.splice(evt.oldIndex, 1)
          _this.dropCol.splice(evt.newIndex, 0, oldItem)
          _this.$store.dispatch("dropCol/pushDropCol", _this.dropCol)
          _this.$store.dispatch("dropCol/pushCol", _this.col)
        },
        onUpdate: evt => {
          // console.log("dropCol===>", _this.dropCol)
        },
      })
    },

注意 Sortable.create(el, {})中的 el 是指 离你想要拖拽的内容的标签元素。

f09843b18caf94f3b9f5141206391f09.png

fa0a294095832cd6f29c26ab723d3911.png

242579ea0b518ff14e6ac6df69add5de.png

816bb0689ba72b86bb1740e4d5c76f6d.png

不懂可以留言一起探讨。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值