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表内容,简单封装了拖拽表格功能。

如果有问题或有更好的见解,请在下面评论