vue3实现表格动态列及自定义列排序

一、前言

csharp 复制代码
yarn add vuedraggable  
npm i -S vuedraggable
  • UMD浏览器直接引用JS方式
xml 复制代码
<script src="https://www.itxst.com/package/vue/vue.min.js"></script>  
<script src="https://www.itxst.com/package/sortable/Sortable.min.js"></script>  
<script src="https://www.itxst.com/package/vuedraggable/vuedraggable.umd.min.js"></script>
  • 页面引入
javascript 复制代码
import draggable from "vuedraggable";

二、代码注释详解

  1. 动态列显示控制

    • 可以通过复选框控制哪些列显示/隐藏
    • 支持全选/全不选功能
    • 最少保留一项显示
  2. 列排序功能

    • 使用vuedraggable实现拖拽排序
    • 通过长按拖动图标调整列顺序
  3. 配置持久化

    • 使用localStorage保存列配置
    • 下次打开页面会记住上次的配置
  4. 用户友好提示

    • 操作指南提示
    • 未保存修改确认提示
    • 操作成功/失败反馈
xml 复制代码
<template>
  <!-- 设置按钮 -->
  <div style="padding: 10px">
    <el-icon size="24" style="margin-left:auto;display: block;" @click="drawer=true">
      <Setting />
    </el-icon>
  </div>
  <!-- 主表格 -->
  <!-- 使用tableKey强制重新渲染解决ResizeObserver问题 -->
  <el-table :data="tableData" border style="width: 100%" :key="tableKey">
    <!-- 动态渲染列 -->
    <el-table-column
      v-for="item in citiesConfig"
      :key="item.prop"
      :prop="item.prop"
      :label="item.label"
    >
      <template #default="scope">
        {{ scope.row[item.prop] }}
        <!--{{ item.isShow }}-->
      </template>
    </el-table-column>
  </el-table>

  <!-- 列配置抽屉 -->
  <div class="drawer-box">
    <el-drawer
      v-model="drawer"
      title="表格动态列配置"
      direction="rtl"
      :before-close="handleClose"
    >
      <el-alert type="warning" :closable="false">
        <template #title>
          <div class="el-alert-text">
            <div>1、是否勾选字段将决定是否在列表中显示,最少保留一项。</div>
            <div>2、长按某项图标上下拖动至要调整的位置,即可完成字段排序。</div>
            <div>3、以上两项配置均为保存完成后生效。</div>
          </div>
        </template>
      </el-alert>
      <!-- 全选复选框 -->
      <el-checkbox
        v-model="checkAll"
        :indeterminate="isIndeterminate"
        @change="handleCheckAllChange"
        style="margin-bottom: 10px"
      >
        全选
      </el-checkbox>

      <!-- 可拖拽的列配置列表 -->
      <draggable
        v-model="cities"
        chosen-class="chosen"
        forceFallback="true"
        group="people"
        animation="300"
        item-key="prop"
      >
        <template #item="{index, element }">
          <div class="draggable-item">
            <!-- 单个列的复选框 -->
            <el-checkbox-group
              v-model="checkedCities"
              @change="handleCheckedCitiesChange"
            >
              <el-checkbox :label="element.label" :value="element.prop">
                <div class="checkbox-item">
                  {{ element.label }}
                </div>
              </el-checkbox>
            </el-checkbox-group>
            <!--右侧排序图标-->
            <div>
              <el-icon>
                <DCaret />
              </el-icon>
            </div>
          </div>
        </template>
      </draggable>

      <!-- 底部按钮 -->
      <div class="bottom-btns">
        <el-button @click="drawer = false;">取消</el-button>
        <el-button type="primary" @click="save">保存</el-button>
      </div>
    </el-drawer>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import { Setting, DCaret } from '@element-plus/icons-vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import draggable from "vuedraggable";

// 抽屉显示状态
const drawer = ref(false);
// 全选状态
const checkAll = ref(false);
// 半选状态
const isIndeterminate = ref(false);
// 选中的列
const checkedCities = ref([]);
// 表格重新渲染的key
const tableKey = ref(0);

// 表格数据
const tableData = ref([
  { a: '姓名1', b: '性别1', c: '年龄1', d: '住址1', e: '手机号1', f: '身份证号1' },
  { a: '姓名2', b: '性别2', c: '年龄2', d: '住址2', e: '手机号2', f: '身份证号2' },
  { a: '姓名3', b: '性别3', c: '年龄3', d: '住址3', e: '手机号3', f: '身份证号3' },
  { a: '姓名4', b: '性别4', c: '年龄4', d: '住址4', e: '手机号4', f: '身份证号4' },
  { a: '姓名5', b: '性别5', c: '年龄5', d: '住址5', e: '手机号5', f: '身份证号5' }
]);

// 所有列配置(包括显示/隐藏状态)
const cities = ref([
  { label: '姓名', prop: 'a', isShow: true },
  { label: '身份证号', prop: 'f', isShow: true },
  { label: '性别', prop: 'b', isShow: false },
  { label: '年龄', prop: 'c', isShow: false },
  { label: '住址', prop: 'd', isShow: false },
  { label: '手机号', prop: 'e', isShow: true }
]);

// 实际显示的列配置(初始化时从cities中筛选)
const citiesConfig = ref([]);

/**
 * 保存列配置
 * 1. 检查至少选中一列
 * 2. 保存到localStorage
 * 3. 更新显示的列配置
 * 4. 强制表格重新渲染
 */
function save() {
  // 展示的列数据
  let citiesFilterList = cities.value.filter(item => item.isShow);
  if(citiesFilterList?.length > 0) {
    // 保存到本地存储
    localStorage.setItem('cities', JSON.stringify(cities.value));
    // 更新显示的列配置(使用展开运算符创建新数组)
    citiesConfig.value = [...cities.value.filter(item => item.isShow)];
    // 强制表格重新渲染(解决ResizeObserver问题)
    tableKey.value++;
    ElMessage.success('保存成功!');
  }
  else {
    ElMessage.warning('表格列最少需要保存一项!');
  }
}

/**
 * 关闭抽屉前的确认
 * 检查是否有未保存的修改
 */
function handleClose() {
  let citiesData = localStorage.getItem('cities');
  if(citiesData) {
    // 比较当前配置与保存的配置
    if(citiesData !== JSON.stringify(cities.value)) {
      ElMessageBox.confirm(`当前配置存在变动未保存,请确认是否要关闭?`, {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      })
      .then(() => {
        drawer.value = false;
      })
      .catch(() => {
        // 用户取消关闭
      });
    }
    else {
      drawer.value = false;
    }
  }
  else {
    drawer.value = false;
  }
}

/**
 * 全选/全不选
 * @param {Boolean} val - 是否全选
 */
const handleCheckAllChange = (val) => {
  // 全选全不选时修改选中的数据
  checkedCities.value = val ? cities.value.map((item) => item.prop) : [];
  // 选中的数据  修改列显示状态
  cities.value.forEach((item) => item.isShow = val);
  // 清除半选的样式
  isIndeterminate.value = false;
};

/**
 * 单个列选择变化
 * @param {Array} val - 选中的列prop数组
 */
const handleCheckedCitiesChange = (val) => {
  // 单选时选中的数据长度
  const checkedCount = cities.value.filter((item) => val.includes(item.prop)).length;
  // 选中的数据  修改列显示状态
  cities.value.forEach((item) => item.isShow = val.includes(item.prop));
  // 是不是全选
  checkAll.value = checkedCount === cities.value.length;
  // 有且不是全选 即半选样式
  isIndeterminate.value = checkedCount > 0 && checkedCount < cities.value.length;
};

/**
 * 初始化列配置
 * 1. 从localStorage加载保存的配置
 * 2. 初始化复选框状态
 * 3. 设置实际显示的列
 */
function init() {
  // 是否存在历史配置
  let citiesData = localStorage.getItem('cities');
  if(citiesData) {
    cities.value = JSON.parse(citiesData);
  }

  // 设置全选状态
  checkAll.value = cities.value.every((item) => item.isShow);

  // 设置选中项
  if(checkAll.value) {
    checkedCities.value = cities.value.map((item) => item.prop);
  }
  else {
    // 全选框样式
    isIndeterminate.value = cities.value.some((item) => item.isShow);
    checkedCities.value = isIndeterminate.value
      ? cities.value.filter((item) => item.isShow).map((item) => item.prop)
      : [];
  }

  // 初始化显示的列
  citiesConfig.value = [...cities.value.filter(item => item.isShow)];
}

// 初始化
init();
</script>

<style scoped lang="scss">
/* 底部按钮定位 */
.bottom-btns {
  position: absolute;
  bottom: 10px;
  left: 50%;
  transform: translateX(-50%);
}

/* 标题颜色 */
.title {
  color: var(--text-color);
}

/* 复选框盒子样式 */
:deep(.el-checkbox) {
  display: flex !important;
  align-items: center;
  padding: 10px;
}

/* 可拖拽项样式 */
.draggable-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 10px;
  img {
    width: 24px;
  }
  //  &:hover {
  //    background-color: #eeeeee;
  //  }
}
//被拖拽的项的样式
.chosen {
  background-color: rgba(238, 238, 238, 0.4);
  border: 1px solid #eeeeee;
}
.el-alert-text {
  font-size: 12px;
}
.drawer-box:deep(.el-drawer__header) {
  margin-bottom: 0;
}
</style>
相关推荐
LaoZhangAI43 分钟前
Kiro vs Cursor:2025年AI编程IDE深度对比
前端·后端
止观止1 小时前
CSS3 粘性定位解析:position sticky
前端·css·css3
爱编程的喵1 小时前
深入理解JavaScript单例模式:从Storage封装到Modal弹窗的实战应用
前端·javascript
lemon_sjdk1 小时前
Java飞机大战小游戏(升级版)
java·前端·python
G等你下课1 小时前
如何用 useReducer + useContext 构建全局状态管理
前端·react.js
欧阳天羲1 小时前
AI 增强大前端数据加密与隐私保护:技术实现与合规遵
前端·人工智能·状态模式
慧一居士1 小时前
Axios 和Express 区别对比
前端
I'mxx1 小时前
【html常见页面布局】
前端·css·html
快起来别睡了2 小时前
Vuex 与 Pinia:Vue 状态管理详解,小白也能看懂
vue.js