最近写组件碰到一种可拖拽排序表格,搞死我,今天才搞好,无论如何我要写一篇文章来记录一下,老大前几天说,你来搞个那种可以拖拽排序的组件,我想了想,可拖拽排序是什么样子的,一开始以为是那种单独可以拖拽,再问问老大,结果说是这种效果的。
一开始是想用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 如下
vuedraggablesortablejs.github.io我们来探索一下sortable.js
github地址如下:有很多配置选项,支持vue的
https://github.com/SortableJS/Sortable#optionsgithub.com你可以这样安装 sortable.js
简单用法
<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
其实组件没什么,问题是碰到的问题才有意思,这个行拖拽和列拖拽一开始是有问题,总是出现行拖拽,视图没有更新,但是又好像是更新了,
但是实际上splice()方法是可以改变数组长度,以及更新视图的,官网也说了。所以断定不是这个问题,接下来就是百度,看到有一个说就是 v-for 里面的key没有绑定,那就来绑定 key
但是只是绑定了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 是指 离你想要拖拽的内容的标签元素。
不懂可以留言一起探讨。