vue2实现表格拖拽功能。整列的数据可以随意拖拽排序,但是行的拖拽只影响当前列

概述

本文介绍基于 Vue2 实现的表格组件,支持以下核心功能:

  • 列拖拽排序(整列位置交换)
  • 行拖拽排序(每列内部独立排序)
  • 自适应列宽与内容溢出提示
  • 可视化拖拽反馈效果
  • 数据与视图的自动同步

功能演示

源码分享

<template>
  <div class="table-container">
    <!-- 列拖拽区域 -->
    <draggable v-model="columns" class="columns-wrapper" :options="colOptions">
      <!-- 单列容器 -->
      <div v-for="column in columns" :key="column.id" class="column">
        <!-- 列标题 -->
        <div class="column-header">
          <span>{{ column.label }}</span>
          <!-- <div class="resize-handle"></div> -->
          <!-- 列宽调整手柄 -->
        </div>

        <!-- 行拖拽区域 -->
        <draggable v-model="column.order" class="rows-wrapper nicescroll" :options="rowOptions">
          <!-- 单行元素 -->
          <div v-for="rowIndex in column.order" :key="rowIndex" class="row">
            <div class="drag-handle">⠿</div>
            <!-- 拖拽图标 -->
            <span class="content" v-tooltip="data[rowIndex][column.prop]">{{ data[rowIndex][column.prop] }}</span>
          </div>
        </draggable>
      </div>
    </draggable>
  </div>
</template>

<script>
import draggable from 'vuedraggable';

export default {
  components: { draggable },
  data() {
    return {
      // 原始数据(行式存储)
      data: Array.from({ length: 20 }, (_, i) => ({
        // 生成20行数据
        id: i,
        name: `用户${String(i + 1).padStart(2, '0')}`,
        age: 20 + Math.floor(Math.random() * 15),
        gender: ['男', '女'][i % 2],
        city: ['北京时代峰峻电饭锅黄齑淡饭嘎哈就打发很大干哈扩大国际会发生的款到发货', '上海', '广州', '深圳'][i % 4],
        job: ['工程师', '设计师', '产品经理', '运营'][i % 4],
      })),
      columns: [
        // 扩展为5列
        { id: 'name', label: '姓名', prop: 'name', order: [...Array(20).keys()] },
        { id: 'age', label: '年龄', prop: 'age', order: [...Array(20).keys()] },
        { id: 'gender', label: '性别', prop: 'gender', order: [...Array(20).keys()] },
        { id: 'city', label: '城市', prop: 'city', order: [...Array(20).keys()] },
        { id: 'job', label: '职位', prop: 'job', order: [...Array(20).keys()] },
      ],
    };
  },
  // 新增指令配置
  directives: {
    tooltip: {
      inserted(el, binding) {
        let tooltip = null;

        // 鼠标移入事件
        el._showTooltip = () => {
          console.log(el.scrollWidth);
          console.log(el.clientWidth);
          if (el.scrollWidth <= el.clientWidth) return; // 溢出判断[2]

          tooltip = document.createElement('div');
          tooltip.className = 'custom-tooltip';
          tooltip.textContent = binding.value;
          document.body.appendChild(tooltip);

          // 动态定位(参考网页1)
          const rect = el.getBoundingClientRect();
          tooltip.style.left = `${rect.left + window.scrollX}px`;
          tooltip.style.top = `${rect.top + rect.height + 5 + window.scrollY}px`;
        };

        // 鼠标移出事件
        el._hideTooltip = () => {
          tooltip?.remove();
        };

        el.addEventListener('mouseenter', el._showTooltip);
        el.addEventListener('mouseleave', el._hideTooltip);
      },
      unbind(el) {
        el.removeEventListener('mouseenter', el._showTooltip);
        el.removeEventListener('mouseleave', el._hideTooltip);
      },
    },
  },
  computed: {
    colOptions() {
      return {
        direction: 'horizontal',
        ghostClass: 'column-ghost',
        chosenClass: 'column-chosen',
        animation: 200,
      };
    },
    rowOptions() {
      return {
        group: 'rows',
        handle: '.drag-handle',
        ghostClass: 'row-ghost',
        animation: 150,
        forceFallback: true, // 优化移动端体验[3]
      };
    },
  },
  watch: {
    // 数据变化时同步order数组(示例处理新增行)
    data(newVal, oldVal) {
      if (newVal.length > oldVal.length) {
        const newIndex = newVal.length - 1;
        this.columns.forEach((col) => {
          if (!col.order.includes(newIndex)) {
            col.order.push(newIndex);
          }
        });
      }
    },
  },
  methods: {},
};
</script>

<style lang="scss" scoped>
.table-container {
  width: 100%; // 铺满页面
  height: calc(100% - 40px);
  padding: 20px;
  overflow: auto;

  .columns-wrapper {
    position: relative;
    z-index: 1;
    display: grid; // 使用网格布局实现等宽[2]
    grid-auto-columns: minmax(200px, 1fr); // 最小200px等宽
    grid-auto-flow: column;
    min-width: 100%; // 强制撑满容器
    // width: max-content;
    width: fit-content;
  }

  .column {
    display: grid;
    grid-template-rows: 40px auto; // 固定表头高度
    border-right: 1px solid #cdcdcd; // Element UI风格边框[2]
    background: #ffffff;

    &:first-child {
      border-left: 1px solid #cdcdcd;
    }
  }

  .rows-wrapper {
    // height: calc(100% - 40px); // 根据视窗高度自适应
    // min-height: calc(100vh - 100px); // 根据视窗高度自适应
    overflow-y: auto; // 添加纵向滚动
  }

  .row {
    display: flex; // 启用flex布局
    align-items: center;
    padding: 12px 8px; // 调整内边距
    border-bottom: 1px solid #cdcdcd; // Element UI风格分割线[2]
    transition: background 0.2s;
    margin: 0;
    &:nth-child(even) {
      background: #f8f8f8; // 基础斑马纹

      // 拖拽状态下的颜色覆盖逻辑
      &.sortable-chosen {
        background: #f0f7ff !important;
      }
      &:hover {
        background: #f5f7fa !important;
      }
    }

    &.sortable-chosen {
      background: #f0f7ff; // 拖拽时蓝色背景[2]
      box-shadow: 0 2px 12px rgba(64, 158, 255, 0.1); // Element UI阴影
    }
    .content {
      flex: 1; // 填充剩余空间
      min-width: 0; // 关键:允许内容收缩
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
  }
}
.drag-handle {
  display: inline-block;
  margin: 0 8px;
  color: #909399; // Element UI辅助文本色[2]
  cursor: move;
  opacity: 0.6;
  transition: opacity 0.2s;
  flex-shrink: 0; // 禁止拖拽图标收缩
  &:hover {
    opacity: 1;
  }
}
.column-header {
  position: relative;
  display: flex;
  align-items: center;
  padding: 0 12px;
  height: 40px;
  background-color: #eef2f8;
  border-bottom: 1px solid #cdcdcd;
  border-top: 1px solid #cdcdcd;
  //   position: sticky;
  //   top: 0;
  //   z-index: 2;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
  //   .resize-handle {
  //     position: absolute;
  //     right: -2px;
  //     top: 0;
  //     width: 4px;
  //     height: 100%;
  //     cursor: col-resize;
  //     background: #409eff;
  //     opacity: 0;
  //     transition: opacity 0.2s;

  //     &:hover {
  //       opacity: 0.6;
  //     }
  //   }
}

.column-ghost {
  opacity: 0.8;
  background: #f0f9eb !important; // Element UI成功色浅背景[2]
}

.row-ghost {
  opacity: 0.6;
  background: #f0f9ff;
}
</style>

补充样式

.custom-tooltip {
  position: absolute;
  padding: 6px 12px;
  background: rgba(0, 0, 0, 0.8);
  color: white;
  border-radius: 4px;
  font-size: 12px;
  z-index: 9999;
  max-width: 300px;
  white-space: pre-wrap;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);

  &::after {
    // 添加小三角指示器
    content: '';
    position: absolute;
    top: -5px;
    left: 50%;
    transform: translateX(-50%);
    border-right: 5px solid transparent;
    border-left: 5px solid transparent;
    border-bottom: 5px solid rgba(0, 0, 0, 0.8);
  }
}
posted @ 2025-03-18 10:49  火炬冬天  阅读(844)  评论(0)    收藏  举报