drag拖拽vue组件实现列表列配置

804 阅读1分钟

推荐阅读 vue3+ts实现drag拖拽任务看板(taskboard)

image.png

效果;

image.png

image.png

目标:

拖动列进行上下平移。

流程:

成功渲染列后,悬浮选择按钮,进行上下拖拽。

技术实现:

为每行数据和最后一个填充行添加如下拖拽代码

过程对象样式
: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;

补充说明

image.png

image.png