推荐阅读 vue3+ts实现drag拖拽任务看板(taskboard)
效果;
目标:
拖动列进行上下平移。
流程:
成功渲染列后,悬浮选择按钮,进行上下拖拽。
技术实现:
为每行数据和最后一个填充行添加如下拖拽代码
过程对象样式
:class="[
{ 'drag-active': item.key === dragActiveKey }
]"
拖拽事件
@dragover="dragOver($event, item)"
@dragleave="dragLeave($event, item)"
@drop="dropEvent($event, item)"
&.drag-active::before {
display: block;
top: 0px;
content: '';
height: 2px;
width: 100%;
background-color: #409eff;
}
为按钮添加拖拽属性
:draggable="true"
@dragstart="dragStart($event, item.key, index)"
代码具体实现
拖拽页面
<template>
<div>
<columns-setting
:source="columns"
is-default
is-drag
@change="handleSettingChange"
>
<i class="el-icon-setting"></i> 设置查看列
</columns-setting>
<dynamics-table :data="data" :columns="tableColumns"> </dynamics-table>
</div>
</template>
<script>
import { ColumnsSetting, DynamicsTable } from '/vex-components';
export default {
components: {
ColumnsSetting,
DynamicsTable
},
data() {
return {
columns: [
{
key: 'column1',
title: '数据列1',
width: 100,
always: true
},
{
key: 'column2',
title: '数据列2',
width: 100,
default: true
},
{
key: 'column3',
title: '数据列3',
width: 100,
default: true
},
{
key: 'column4',
title: '数据列4',
width: 100,
default: true
},
{
key: 'column5',
title: '数据列5',
width: 100,
default: true
},
{
key: 'column6',
title: '数据列6',
width: 100
},
{
key: 'column7',
title: '数据列7',
width: 100
},
{
key: 'column8',
title: '数据列8',
width: 100
},
{
key: 'column9',
title: '数据列9',
width: 100
},
{
key: 'column10',
title: '数据列10',
width: 100
},
{
key: 'column11',
title: '数据列11',
width: 100
}
],
tableColumns: [],
data: [
{
id: 1,
column1: '测试',
column2: '测试',
column3: '测试',
column4: '测试',
column5: '测试',
column6: '测试',
column7: '测试',
column8: '测试',
column9: '测试',
column10: '测试',
column11: '测试'
}
]
};
},
methods: {
handleSettingChange(columns) {
// 实现拖拽需要触发表格tableColumns异步刷新
this.tableColumns = [];
this.$nextTick(() => {
this.tableColumns = columns;
});
}
}
};
</script>
拖拽组件实现
<template>
<!-- 默认 trigger="click" -->
<el-popover
v-model="visible"
:placement="placement"
:width="width"
:popper-class="`${prefixCls}`"
>
<!-- 触发弹窗按钮 -->
<span
slot="reference"
:class="{
[`${prefixCls}-trigger`]: true,
[`${$attrs.class}`]: !!$attrs.class
}"
>
<slot></slot>
</span>
<!-- 弹窗内容部分 -->
<div :class="`${prefixCls}-popover`">
<!-- 标题 title + 全选 -->
<div :class="`${prefixCls}-title`">
<span>{{ title }}</span>
<el-checkbox
v-model="checkAll"
:class="`${prefixCls}-check-all`"
:indeterminate="isIndeterminate"
@change="handleCheckAllChange"
>
全选
</el-checkbox>
</div>
<!-- 列选项 checkbox-group -->
<div :class="`${prefixCls}-checkbox-group`">
<el-checkbox-group v-model="selectedKeys">
<div
v-for="(item, index) in selectColumns"
:key="item.key"
:class="[
`${prefixCls}-checkbox-group-item`,
{ 'drag-active': item.key === dragActiveKey }
]"
@dragover="dragOver($event, item)"
@dragleave="dragLeave($event, item)"
@drop="dropEvent($event, item)"
>
<el-checkbox
:label="item.key"
:class="`${prefixCls}-checkbox-group-item-option`"
>
{{ item.title }}
</el-checkbox>
<i
v-if="isDrag"
class="el-icon-position"
:draggable="true"
:class="`${prefixCls}-checkbox-group-item-icon`"
@dragstart="dragStart($event, item.key, index)"
></i>
</div>
<div
v-if="isDrag"
:class="[
`${prefixCls}-checkbox-group-item-fill`,
{ 'drag-active': defaultColumns.key === dragActiveKey }
]"
@dragover="dragOver($event, defaultColumns)"
@dragleave="dragLeave($event, defaultColumns)"
@drop="dropEvent($event, defaultColumns)"
></div>
</el-checkbox-group>
</div>
<!-- 按钮 关闭 恢复默认 确定 -->
<div :class="`${prefixCls}-footer`">
<el-button size="mini" @click="handleClose"> 关闭 </el-button>
<el-button
v-if="isDefault"
size="mini"
class="m-l-1"
@click="handleReset"
>
恢复默认
</el-button>
<el-button
size="mini"
type="primary"
class="m-l-1"
@click="handleConfirm"
>
确定
</el-button>
</div>
</div>
</el-popover>
</template>
<script>
export default {
name: 'ColumnsSetting',
props: {
title: {
type: String,
default: '设置查看列'
},
width: {
type: [String, Number],
default: '230'
},
placement: {
type: String,
default: 'bottom'
},
source: {
type: Array,
default: () => []
},
storageKey: {
type: String,
default: undefined
},
getStore: {
type: Function,
default: (key) => {
const rst = localStorage.getItem(key);
if (rst) {
return JSON.parse(rst);
}
return undefined;
}
},
setStore: {
type: Function,
default: (key, value) => {
localStorage.setItem(key, JSON.stringify(value));
}
},
isDefault: {
type: Boolean,
default: false
},
isDrag: {
type: Boolean,
default: false
}
},
data() {
return {
prefixCls: 'vex-columns-setting',
visible: false,
selectColumns: this.source.filter((v) => v.always !== true), // 过滤always列
selectedKeys: [], // 被选中列的key
stateColumns: [], // 表格显示列
dragKeys: {
dragStartKey: undefined,
dragStartIndex: undefined
},
dragElement: undefined,
dragCloneElenmt: undefined,
clickElement: { x: 0, y: 0 },
// parentElement: undefined,
dragActiveKey: '',
defaultColumns: {
key: 'defaultColumns'
}
};
},
computed: {
// 全选
checkAll() {
return this.selectedKeys.length === this.selectColumns.length;
},
// 部分被选
isIndeterminate() {
return (
this.selectedKeys.length > 0 &&
this.selectedKeys.length < this.selectColumns.length
);
}
},
created() {
this.init();
},
methods: {
init() {
let storage;
// 获取缓存数据
if (this.storageKey) {
const result = this.getStore(this.storageKey);
if (result instanceof Promise) {
result.then((data) => {
storage = data;
this.setColumns(storage);
});
} else {
storage = result;
this.setColumns(storage);
}
} else {
this.setColumns(storage);
}
},
// 表格展示列数据
setColumns(storage) {
// 缓存表格列数据
if (storage) {
this.stateColumns = this.source.filter(
(v) => storage.includes(v.key) || v.always
);
// 显示默认列数据 always default
} else if (this.isDefault) {
this.stateColumns = this.source.filter((v) => v.always || v.default);
} else {
// 显示所有列数据
this.stateColumns = [...this.source];
}
// 回显列数据
this.$emit('change', this.stateColumns);
// 回显弹窗勾选数据
this.selectedKeys = this.stateColumns
.filter((v) => !v.always)
.map((v) => v.key);
},
// 全选操作
handleCheckAllChange(val) {
this.selectedKeys = val ? this.selectColumns.map((v) => v.key) : [];
},
// 关闭
handleClose() {
this.visible = false;
},
// 恢复默认
handleReset() {
this.selectedKeys = this.selectColumns
.filter((v) => v.default)
.map((v) => v.key);
},
// 确认
handleConfirm() {
// 表格列展示数据
this.stateColumns = this.selectColumns.filter((v) =>
this.selectedKeys.includes(v.key)
);
// 插入always=true的值
this.source.forEach((v, index) => {
if (v.always) {
this.stateColumns.splice(index, 0, v);
}
});
this.$emit('change', this.stateColumns);
this.visible = false;
},
// 拖拽节点
dragStart(event, key, index) {
// 去掉浏览器默认拖拽图片
if (event.dataTransfer) this.clearDefaultImage(event.dataTransfer);
if (key && event.target && event.target instanceof HTMLElement) {
// 克隆被拖拽节点
this.dragCloneElenmt = document.createElement('div');
const { width, height } = window.getComputedStyle(
event.target.parentElement
);
this.dragCloneElenmt.style.width = width;
this.dragCloneElenmt.style.height = height;
this.dragCloneElenmt.appendChild(
event.target.parentElement.cloneNode(true)
);
document.body.appendChild(this.dragCloneElenmt);
// 记录拖拽节点中鼠标相对位置
const { left, top } =
event.target.parentElement.getBoundingClientRect();
this.clickElement.x = event.clientX - left;
this.clickElement.y = event.clientY - top;
// 记录拖拽节点信息
this.dragKeys.dragStartKey = key;
this.dragKeys.dragStartIndex = index;
event.target.parentElement.style.opacity = '0.3'; // 拖拽节点显示0.5
this.dragElement = event.target.parentElement; // 记录拖拽节点HTML
// this.parentElement = event.target.parentElement.parentElement;
// 监听dragover和dragend事件
window.addEventListener('dragover', this.bindDragOver, {
capture: true
});
window.addEventListener('dragend', this.clearDrag, { capture: true });
}
},
bindDragOver(event) {
// 动态改变 克隆节点位置
if (this.dragCloneElenmt) {
// 修改跟随样式
this.dragCloneElenmt.style.position = 'fixed';
this.dragCloneElenmt.style.backgroundColor = 'rgba(230, 235, 241, 0.6)';
this.dragCloneElenmt.style.top = `${
event.clientY - this.clickElement.y
}px`;
this.dragCloneElenmt.style.left = `${
event.clientX - this.clickElement.x
}px`;
this.dragCloneElenmt.style.zIndex = '10001';
this.dragCloneElenmt.style.pointerEvents = 'none';
}
},
clearDrag() {
this.dragElement.style.opacity = '1'; // 恢复被拖拽节点样式
// 删除克隆节点
if (this.dragCloneElenmt) {
document.body.removeChild(this.dragCloneElenmt);
this.dragCloneElenmt = undefined;
}
// if (this.parentElement) this.parentElement = undefined;
this.dragKeys.dragStartKey = undefined;
this.dragKeys.dragStartIndex = undefined;
this.dragActiveKey = '';
window.removeEventListener('dragover', this.bindDragOver);
window.removeEventListener('dragend', this.clearDrag);
},
dragOver(event, item) {
this.pauseEvent(event);
// 若过程节点 不是被拖拽节点 或是默认节点(最后一个节点)
// 显示顶部标记
if (
item.key !== this.dragKeys.dragStartKey ||
item.key === this.defaultColumns.key
) {
this.dragActiveKey = item.key;
}
},
dragLeave(event, item) {
this.pauseEvent(event);
// 若过程节点 不是被拖拽节点 或是默认节点(最后一个节点)
// 隐藏顶部标记
if (
item.key !== this.dragKeys.dragStartKey ||
item.key === this.defaultColumns.key
) {
this.dragActiveKey = '';
}
},
// 目标节点
dropEvent(event, item) {
this.pauseEvent(event);
// 目标节点是被拖拽节点,不进行任何操作
if (item.key === this.dragKeys.dragStartKey) return;
// 第一步 删除数组被拖拽元素
const dargStartItem = this.selectColumns.splice(
this.dragKeys.dragStartIndex,
1
);
if (item.key === this.defaultColumns.key) {
// 第二步 如果目标节点是最后节点,则将被拖拽节点插入到末尾
this.selectColumns.push(dargStartItem[0]);
} else {
// 第二步 找到目标元素索引
const dropItemIndex = this.selectColumns.findIndex(
(v) => v.key === item.key
);
// 第三步 在目标元素前插入被拖拽元素
this.selectColumns.splice(dropItemIndex, 0, dargStartItem[0]);
}
},
// 阻止事件冒泡
pauseEvent(e) {
e.stopPropagation();
e.preventDefault();
},
// 清除浏览器默认拖拽图片
clearDefaultImage(dataTransfer) {
const img = new Image();
img.src =
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' %3E%3Cpath /%3E%3C/svg%3E";
dataTransfer.setDragImage(img, 0, 0);
}
}
};
</script>
样式
@columns-setting-prefix: ~'vex-columns-setting';
.@{columns-setting-prefix} {
background: var(--body-background);
padding: 0;
min-width: 230px;
&-trigger {
cursor: pointer;
display: inline-block;
}
&-title {
height: 32px;
line-height: 32px;
width: 100%;
padding: 0 16px;
border-bottom: 1px solid var(--border-color);
span {
font-weight: 400;
}
}
&-check-all {
font-weight: 400;
margin-left: 24px;
}
&-checkbox-group {
padding: 4px 16px;
&-item {
display: flex;
position: relative;
&.drag-active::before {
position: absolute;
top: -1px;
content: '';
height: 2px;
width: 100%;
background-color: #409eff;
}
&-option {
flex: 6;
display: inline-block;
padding: 4px 0;
}
&-icon {
flex: 1;
font-size: 16px;
color: var(--primary-color);
padding: 6px 0;
text-align: center;
&:hover {
background-color: #f6f6f6;
cursor: move;
}
}
}
}
&-checkbox-group-item-fill {
height: 8px;
display: block;
border: 0;
&.drag-active::before {
display: block;
top: 0px;
content: '';
height: 2px;
width: 100%;
background-color: #409eff;
}
}
&-footer {
height: 48px;
line-height: 48px;
border-top: 1px solid var(--border-color);
padding: 0 8px;
text-align: right;
}
}
组件注册
import ColumnsSetting from './ColumnsSetting.vue';
ColumnsSetting.install = (Vue) => {
Vue.component(ColumnsSetting.name, ColumnsSetting);
};
export default ColumnsSetting;