vue版本, AntDisign的table表拖拽,需要付费,或者有,我并没找到所以封装了个简单的table拖拽。
使用场景:
使用拖拽一般是为了排序,所以场景就是对列表数据进行排序。
文章目录
- 注意事项
- 一、拖拽table
- 二、拖拽table使用步骤
- 1.基本使用
- 2.使用注意项
- 总结
注意事项
提示:表格拖拽使用了
vuedraggable 拖拽插件,使用前先安装插件
npm install vuedraggable
其次AntDesgin 版本确保2以上,支持vue3的写法
拖拽table代码为封装的拖拽table组件, 拖拽table使用步骤为拖拽表的使用
一、拖拽table
表格拖拽我并没有使用AntDisign的table组件而是自行封装,基本使用还是可以的。
<template>
<div class="table-warp">
<div class="column-warp-box">
<div class="column-warp-scroll" @scroll="warpScroll" ref="columnWarpBox" :style="{ overflowX: autoState ? 'scroll' : 'hidden' }">
<div class="column-warp">
<div class="all-checkbox column-item-fixed default-column-item-fixed-left-last" :class="{ 'default-column-item-fixed': leftFixed }">
<a-checkbox v-if="!!selection" v-model:checked="checked" :indeterminate="indeterminate" @change="onCheckAllChange"></a-checkbox>
</div>
<div v-for="(item, index) in columnsData.data" :key="item.key+index" class="column-item" :style="trendsStyle(item, index)"
:class="{ 'column-item-fixed': item.fixed, 'column-item-fixed-left-last': frontFixed('left', index, leftFixed), 'column-item-fixed-right-last': frontFixed('right', index, lastFixed)}">
<slot name="headerCell" :column="item">
<slot>{{ item.title }}</slot>
</slot>
</div>
</div>
</div>
</div>
<a-spin :spinning="spinning">
<div class="table-body-warp" @scroll="warpScroll" ref="tableBodyWarp"
:style="{ overflowY: scroll.y ? 'scroll' : 'hidden', overflowX: autoState ? 'scroll' : 'hidden', height: scroll.y ? heightY : 'auto' }">
<a-checkbox-group v-model:value="columnChecked" class="table-scroll-warp-box" ref="tableBodyWarpBox">
<draggable
v-model="dataList.data"
item-key="id"
animation="300"
handle=".ele-tool-column-handle"
@change="onColumnSortChange">
<template #item="{ element, index }">
<div class="element-item-one" ref="dataItem">
<div class="ele-tool-column-item data-item-fixed default-data-item-fixed-left-last" :class="{ 'default-data-item-fixed': leftFixed }">
<div class="ele-tool-column-handle">
<more-outlined/>
<more-outlined/>
</div>
<a-checkbox v-if="!!selection" :value="element[rowKey]">
</a-checkbox>
</div>
<div class="data-item-warp">
<div v-for="(item, i) in columnsData.data" :key="item.key+i" class="data-item" :style="trendsStyle(item, i)"
:class="{ 'data-item-fixed': item.fixed, 'data-item-fixed-left-last': frontFixed('left', i, leftFixed), 'data-item-fixed-right-last': frontFixed('right', i, lastFixed) }"
>
<slot name="bodyCell" :column="item" :record="element" :index="index">
<slot>{{ item.customRender ? item.customRender({ text: element[item.key], i }) : element[item.key] }}</slot>
</slot>
</div>
</div>
</div>
</template>
</draggable>
</a-checkbox-group>
</div>
</a-spin>
</div>
</template>
<script setup>
import Draggable from 'vuedraggable';
import { ref, reactive, watch, defineProps, defineEmits, getCurrentInstance, onMounted, computed, nextTick, onUnmounted } from "vue";
const currentInstance = getCurrentInstance();
const emitProps = currentInstance.props;
const props = defineProps({
//排序字段
sort: {
default: ""
},
//唯一key
rowKey: {
default: ""
},
//字段栏
columns: {
type: Array,
default: () => []
},
//数据
dataSource: {
type: Array,
default: () => []
},
//选择id列
selection: {
default: null
},
//scroll 设置 { x, y }
scroll: {
type: Object,
default: () => {}
},
//加载状态
loading: {
type: Boolean
}
})
const emits = defineEmits([
'update:dataSource',
'update:selection',
'change' //拖拽改变
])
/* 必填的props 属性 */
if (!emitProps.rowKey) {
console.error("[draggableTable] Each record in dataSource of table should have a unique `key` prop, or set `rowKey` of Table to an unique primary key");
}else if(!emitProps.sort) {
console.error("[draggableTable] Each record in dataSource of table should have a `sort` prop, or set `sort` value");
}
/* 获取组件渲染宽度 */
const nodeWidth = ref(0);
onMounted(() => {
const realNode = currentInstance.vnode.el;
nodeWidth.value = realNode.offsetWidth;
})
//监听窗口变化
function ListenerWidth() {
// 赋值宽度
const realNode = currentInstance.vnode.el;
nodeWidth.value = realNode.offsetWidth;
// 判断fixed
warpScroll({ target: tableBodyWarp.value })
}
window.addEventListener('resize', ListenerWidth);
onUnmounted(() => {
window.removeEventListener('resize', ListenerWidth)
})
//适应状态 是否出现滚动条
const autoState = computed(() => {
const state = columnsData.data.map(item => item.width).reduce((from, final) => from+final, 0);
if (!nodeWidth.value) {
return false;
}else {
return state > (nodeWidth.value-60);
}
})
//动态样式 判断是否具备滚动条
function trendsStyle({ width = 0, align = 'left', fixed }, index) {
let style;
if (autoState.value) {
style = {
width: width+'px',
textAlign: align,
}
if (fixed === 'right') {
const rightColumns = columnsData.data.slice(index+1);
style.position = 'sticky';
style.zIndex = 2;
style[fixed] = (rightColumns.map(item => item.width).reduce((from, final) => from+final, 0))+'px';
}else if (fixed === 'left') {
const leftColumns = columnsData.data.slice(0, index);
style.position = 'sticky';
style.zIndex = 2;
style[fixed] = 60+(leftColumns.map(item => item.width).reduce((from, final) => from+final, 0))+'px';
}
} else {
const totalWidth = columnsData.data.map(item => item.width).reduce((from, final) => from+final, 0);
const flex = Math.trunc((width / totalWidth) * 100);
style = {
flex,
textAlign: align
}
}
return style;
}
//columnWarpBox tableBodyWarp 双向绑定, 是否fixed 浮起 box-shadow
const tableBodyWarp = ref(null);
const columnWarpBox = ref(null);
const leftFixed = ref(false);
const lastFixed = ref(false);
// 表格滚动
function warpScroll(event) {
if (!autoState.value) { //默认归零
leftFixed.value = false;
lastFixed.value = false;
return null;
}
if (tableBodyWarp?.value && columnWarpBox?.value) {
const scrollLeft = event.target.scrollLeft;
//双向绑定 scrollLeft
const operatedNode = event.target === tableBodyWarp.value ? columnWarpBox.value : tableBodyWarp.value;
operatedNode.scrollLeft = scrollLeft;
//监听scroll 赋值 leftFixed, lastFixed
if (leftFixed.value !== !!scrollLeft) { //值相同不必赋值
leftFixed.value = !!scrollLeft;
}
const distanceEnd = event.target.offsetWidth+scrollLeft === event.target.scrollWidth;
if (lastFixed.value !== !distanceEnd) {
lastFixed.value = !distanceEnd;
}
}
}
//是否为最前的fixed
function frontFixed(fixed, index, status) {
if (!status) return false; //status必要条件
if (fixed === "left") {
let lastFindFixed;
for (let item of columnsData.data) {
if (item.fixed === fixed) { //查找
lastFindFixed = item;
}
}
return lastFindFixed === columnsData.data[index];
}else if (fixed === 'right') {
for (let item of columnsData.data) {
if (item.fixed === fixed) { //查找
return item === columnsData.data[index];
}
}
}
}
/* 数据列表 */
const dataList = reactive({ data: [] });
const allColumnChecked = ref([]); //全部选择
const dataItem = ref(null); //末尾data-item项 node节点
//数据值 dataSource 双向绑定
watch(() => props.dataSource, list => {
dataList.data = list;
allColumnChecked.value = dataList.data.map(item => item[props.rowKey]);
if (dataItem.value) { //监视适应高度
autoHeight();
}
}, { deep: true, immediate: true })
watch(() => dataList.data, list => {
emits('update:dataSource', list);
}, { deep: true })
//数据拖拽排序变化
function onColumnSortChange() {
dataList.data.map((item, index) => {
item[props.sort] = index+1;
})
emits("change", dataList);
}
/* 字段数据 */
const columnsData = reactive({ data: [] });
//字段栏监听赋值
watch(() => props.columns, columnList => {
columnsData.data = columnList;
nextTick(() => {
lastFixed.value = autoState.value;
})
}, { immediate: true })
/* 多选框 */
const columnChecked = ref([]);
const checked = ref(false);
const indeterminate = ref(false);
//选择数据 selection 双向绑定
watch(() => props.selection, list => {
columnChecked.value = list || [];
}, { deep: true })
watch(() => columnChecked.value, list => {
emits('update:selection', list);
}, { deep: true })
//checkbox 选择
function onCheckAllChange(e) {
const state = e.target.checked;
indeterminate.value = false;
columnChecked.value = state ? allColumnChecked.value : [];
}
//监听columnChecked 列表选择
watch(() => columnChecked.value, list => {
indeterminate.value = !!list.length && allColumnChecked.value.length > list.length;
checked.value = !!list.length && allColumnChecked.value.length === list.length;
}, { deep: true })
//加载状态
const spinning = ref(false);
watch(() => props.loading, loading => {
spinning.value = loading;
}, { immediate: true })
//适应高度
const heightY = ref('auto');
const tableBodyWarpBox = ref(null);
watch(() => dataItem.value, () => {
autoHeight();
})
function autoHeight() {
const scrollY = props.scroll?.y;
if (scrollY && scrollY < tableBodyWarpBox.value.$el.offsetHeight) {
heightY.value = scrollY +'px'
}else {
heightY.value = 'auto';
}
}
</script>
<style scoped>
.table-warp {
position: relative;
width: 100%;
}
.column-warp-box {
width: 100%;
background-color: #fafafa;
border-bottom: 1px solid #f0f0f0;
padding-right: 12px;
}
.column-warp-scroll {
position: relative;
display: flex;
width: 100%;
}
.column-warp-scroll::-webkit-scrollbar {
display: none;
}
.column-warp {
flex: 1;
position: relative;
display: flex;
}
.column-item, .data-item {
padding: 10px;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}
.table-body-warp {
width: 100%;
position: relative;
background-color: #fff;
}
.table-scroll-warp-box {
position: relative;
min-width: 100%;
}
.all-checkbox, .ele-tool-column-item {
width: 60px;
display: flex;
text-align: center;
justify-content: center;
}
.all-checkbox {
padding-left: 5px;
display: flex;
align-items: center;
justify-content: center;
}
.element-item-one {
display: flex;
width: 100%;
position: relative;
border-bottom: 1px solid #f0f0f0;
}
.element-item-one:hover {
background-color: #e6f7ff;
}
.element-item-one:hover .data-item, .element-item-one:hover .ele-tool-column-item {
background-color: #e6f7ff;
}
.data-item-warp {
flex: 1;
position: relative;
display: flex;
}
.column-item-fixed, .column-item-fixed {
background-color: #fafafa;
}
.data-item-fixed, .data-item-fixed {
background-color: #fff;
}
.column-item-fixed-right-last::after, .data-item-fixed-right-last::after {
content: '';
position: absolute;
top: 0;
bottom: -1px;
left: 0;
width: 10px;
transform: translateX(-5px);
transition: box-shadow 0.3s;
transition-duration: 0.3s;
transition-timing-function: ease;
transition-delay: 0s;
transition-property: box-shadow;
pointer-events: none;
box-shadow: inset -10px 0 8px -8px rgb(0 0 0 / 15%);
}
.column-item-fixed-left-last::after, .data-item-fixed-left-last::after, .default-data-item-fixed::after, .default-column-item-fixed::after {
content: '';
position: absolute;
top: 0;
bottom: -1px;
right: 0;
width: 30px;
transform: translateX(100%);
transition: box-shadow 0.3s;
transition-duration: 0.3s;
transition-timing-function: ease;
transition-delay: 0s;
transition-property: box-shadow;
pointer-events: none;
box-shadow: inset 10px 0 8px -8px rgb(0 0 0 / 15%);
}
.default-data-item-fixed-left-last, .default-column-item-fixed-left-last {
position: sticky;
z-index: 1;
left: 0;
}
</style>
props传递参数使用有注释, 拖拽table使用步骤中也有案例。
sort: 对列表单个数据拖拽后改变的排序值的key
selection: 默认为null是为了判断没有绑定selection,没绑定则不显示checkbox复选组件
scroll: 的x属性并没用我做了自行判断适应, y属性出现滑动条的高度
二、拖拽table使用步骤
1.基本使用
headerCell, bodyCell 插槽使用和AntDesgin的使用无异。@change事件是拖拽后执行的事件,当值拖拽排序成功后触发。
<template>
<div class="ele-body">
<a-card :bordered="false" class="table-height">
<draggable-table
rowKey="id"
sort="sort"
:columns="columns"
v-model:selection="selection"
:loading="loading"
v-model:dataSource="dataSource.data"
:scroll="{ y: 560, x: 'max-content' }"
@change="draggableChange"
>
<template #bodyCell="{ column }">
<template v-if="column.key === 'action'">
<a-space>
<a>修改</a>
<a-divider type="vertical" />
<a class="ele-text-danger">删除</a>
</a-space>
</template>
</template>
</draggable-table>
</a-card>
</div>
</template>
<script setup>
import DraggableTable from '@/components/common/table/draggableTable';
import { ref, reactive } from 'vue';
const columns = ref([
{ title: '标题', key: 'name', width: 200, align: 'center' },
{ title: '标题2', key: 'name2', width: 175, align: 'center' },
{ title: '标题3', key: 'name3', width: 200, align: 'center' },
{ title: '标题4', key: 'name4', width: 200, align: 'center' },
{ title: '标题5', key: 'name5', width: 200, align: 'center' },
{ title: '标题6', key: 'name6', width: 200, align: 'center' },
{ title: '标题7', key: 'name7', width: 200, align: 'center' },
{ title: '标题8', key: 'name8', width: 200, align: 'center' },
{ title: '标题9', key: 'name9', width: 200, align: 'center' },
{ title: '标题10', key: 'name10', width: 200, align: 'center' },
{ title: '标题2', key: 'title', width: 100, align: 'center', fixed: 'right' },
{ title: '操作', key: 'action', width: 120, align: 'center', fixed: 'right' },
])
const dataSource = reactive({
data: [
{ id: 1, name: 'test1', title: "title1", sort: 1 },
{ id: 2, name: 'test2', title: "title2", sort: 2 },
{ id: 3, name: 'test3', title: "title3", sort: 3 },
{ id: 4, name: 'test4', title: "title4", sort: 4 },
{ id: 5, name: 'test5', title: "title5", sort: 5 },
{ id: 6, name: 'test6', title: "title6", sort: 6 },
{ id: 7, name: 'test7', title: "title7", sort: 7 },
{ id: 8, name: 'test8', title: "title8", sort: 8 },
{ id: 9, name: 'test9', title: "title9", sort: 9 },
{ id: 10, name: 'test10', title: "title10", sort: 10 },
{ id: 11, name: 'test11', title: "title11", sort: 11 },
{ id: 12, name: 'test12', title: "title12", sort: 12 },
{ id: 13, name: 'test13', title: "title13", sort: 13 },
{ id: 14, name: 'test14', title: "title14", sort: 14 },
{ id: 15, name: 'test15', title: "title15", sort: 15 },
{ id: 16, name: 'test16', title: "title16", sort: 16 },
{ id: 17, name: 'test17', title: "title17", sort: 17 },
{ id: 18, name: 'test18', title: "title18", sort: 18 },
],
})
const selection = ref([]);
//拖拽改变
function draggableChange() {
console.log(dataSource.data);
}
const loading = ref(true);
setTimeout(() => {
loading.value = false;
}, 500)
</script>
<style scoped>
</style>
columns的flex有两个参数‘left’, 'right',拖拽的checkbox默认了‘left’,不可更改的,如果需要更改的话自行阅读自行代码优化, 或者我下次有空在改。
2.使用注意项
必须传递的连个参数sort(给数据排序改变的key值),rowKey(唯一值), 不传的话我对此抛出一下错误。
if (!emitProps.rowKey) {
console.error("[draggableTable] Each record in dataSource of table should have a unique `key` prop, or set `rowKey` of Table to an unique primary key");
}else if(!emitProps.sort) {
console.error("[draggableTable] Each record in dataSource of table should have a `sort` prop, or set `sort` value");
}
总结
以上就是拖拽table表内容,简单封装了拖拽表格功能。
如果有问题或有更好的见解,请在下面评论