【Vue+ElementUI】Table表格实现自定义表头展示+表头拖拽排序(附源码)

效果图


因项目采用的是Vue2,所以这个功能目前采用的是Vue2的写法。

Vue3请自行修改扩展代码;或收藏关注帖子,后续Vue3项目如有用到会在本帖子更新修改。

安装vuedraggable(拖拽插件)

javascript 复制代码
cnpm i vuedraggable

先说用法,下方附全源码

引入自定义表头组件

javascript 复制代码
import indicatorTable from "@/components/indicatorTable/index.vue";

使用:(传参说明已在下方标识)

javascript 复制代码
<indicatorTable
  ref="rois"
  :defaultArr="columns"
  :cardDataProp="cardDataProp"
  cacheKeyProp="keyROI"
  @propData="propsTableHander"
  currenKey="ROT"
/>

props参数说明:(均为必传字段)

javascript 复制代码
// ref:用于调用子组件方法。
// columns:表头数据,例如:
[{
 prop: "cost_platform",
 label: "广告金",
}]

// cardDataProp:可选表头复选框,列如:
cardDataProp: [
  {
    title: "指标", // 每一项的分类title标题,详见第一张效果图
    checkboxes: [...columns], // columns这个就是上面的一样
  },
],

// cacheKeyProp:储存的key名,名字自定义来,避免缓存的key一样就行,列如:
cacheKeyProp="keyROI"

// propData:回调方法,用于更新表头,接受函数,直接表头columns数据 = 参数即可

// currenKey:保存的指标key,避免缓存的key一样就行。

页面table使用方法,需用循环:

javascript 复制代码
<el-table
  v-loading="loading"
  :data="tableList"
  border
  @sort-change="tableSort"
  :height="tableHeight"
  ref="tableRef"
>
  <el-table-column
    v-for="item in columns"
    :prop="item.prop"
    :label="item.label"
    :width="item.width"
    align="center"
    sortable="custom"
    :show-overflow-tooltip="true"
  >
  </el-table-column>
</el-table>

上面表格的参数不用多说了吧,除非你不会前端!

附源码(拿来直接用!只要参数没问题!)

如遇到报错、不显示等问题,一定是参数不对!已自测 无任何报错或警告信息!

如需要Vue3版本,自行开发或私信,有空定会帮助!
新建组件直接复制:

javascript 复制代码
<template>
  <div class="indicator-all-box">
    <el-popover placement="bottom" width="300" trigger="click">
      <div class="add-custom-indicator-container">
        <el-button type="primary" @click="addUserDefinedIndicators">
          新增自定义指标
        </el-button>
        <div class="indicator-list">
          <ul>
            <li
              v-for="(item, index) in pointerArr"
              :key="index"
              :class="currenPointIndex == index ? 'active-li' : ''"
              @click="pointClick(item, index)"
            >
              <div class="flex-indicator-item">
                <span>{{ item.title }}</span>
                <div class="right-indicator">
                  <i
                    class="el-icon-edit"
                    @click.stop.prevent="pointIndexHander(item, 'edit')"
                  ></i>
                  <i
                    class="el-icon-delete"
                    @click.stop.prevent="pointIndexHander(item, 'delete')"
                  ></i>
                </div>
              </div>
            </li>
          </ul>
        </div>
      </div>
      <el-button slot="reference" type="success">自定义指标</el-button>
    </el-popover>

    <!-- 弹窗自定义 -->
    <el-dialog :title="openTitle" :visible.sync="dialogVisible" width="70%">
      <div class="customize-indicator-data-container">
        <div class="card-checkbox-content-left">
          <el-card
            v-for="(item, index) in cardData"
            :key="index"
            class="box-card"
          >
            <div slot="header" class="clearfix">
              <span>{{ item.title }}</span>
              <el-checkbox
                v-model="item.selectedAll"
                @change="handleSelectAll(item)"
                :indeterminate="isIndeterminate(item)"
                style="float: right"
                >全选</el-checkbox
              >
            </div>
            <div class="check-card-item">
              <el-checkbox-group
                ref="checkboxGroup"
                v-model="selectedCheckboxes"
              >
                <el-checkbox
                  v-for="(checkbox, idx) in item.checkboxes"
                  :key="idx"
                  :label="checkbox.label"
                  >{{ checkbox.label }}</el-checkbox
                >
              </el-checkbox-group>
            </div>
          </el-card>
        </div>
        <div class="sort-view-dx">
          <el-divider>排序</el-divider>
          <div class="sort-row">
            <draggable
              v-if="selectedCheckboxes.length > 0"
              v-model="selectedCheckboxes"
              animation="300"
            >
              <p v-for="(item, index) in selectedCheckboxes" :key="index">
                {{ item }}
              </p>
            </draggable>
            <el-empty v-else></el-empty>
          </div>
        </div>
      </div>
      <span slot="footer" class="dialog-footer">
        <el-button @click="dialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="addPointerSubmit">确 定</el-button>
      </span>
    </el-dialog>
  </div>
</template>

<script>
import draggable from "vuedraggable";

export default {
  name: "indicatorTable",
  components: {
    draggable,
  },
  props: {
    // 默认指标
    defaultArr: {
      type: Array,
      required: true,
    },
    // 可选指标
    cardDataProp: {
      type: Array,
      required: true,
    },
    // 存储指标key
    cacheKeyProp: {
      type: String,
      required: true,
    },
    // 存储的索引key名
    currenKey: {
      type: String,
      required: true,
    },
  },
  data() {
    return {
      // 弹窗show
      dialogVisible: false,
      // 全部指标数组
      cardData: this.cardDataProp,
      // 勾选指标
      selectedCheckboxes: [],
      // 弹框title
      openTitle: "添加",
      // 下拉指标列表
      pointerArr: [],
      // 获取当前编辑item
      editItem: null,
      // 传出去的prop数组
      emitArr: null,
      // 当前选择的指标
      currenPointIndex: null,
    };
  },
  computed: {
    Local() {
      return {
        get(key) {
          const value = localStorage.getItem(key);
          if (value == "[]") {
            return null;
          } else {
            return value !== null ? JSON.parse(value) : null;
          }
        },
        set(key, value) {
          localStorage.setItem(key, JSON.stringify(value));
        },
        remove(key) {
          localStorage.removeItem(key);
        },
      };
    },
    // 指标指定label排序
    sortColumns() {
      return function (data, sort) {
        if (data) {
          return data.sort(
            (a, b) => sort.indexOf(a.label) - sort.indexOf(b.label)
          );
        }
      };
    },
    // 获取选指标label
    filterCheckbox() {
      return function (data, isSort = false, sortData) {
        if (data) {
          let filteredCheckboxes = [];
          this.cardData.forEach((item) => {
            item.checkboxes.forEach((checkbox) => {
              if (data.arrayCheck.includes(checkbox.label)) {
                filteredCheckboxes.push(checkbox);
              }
            });
          });
          // 获取后是否排序
          if (isSort) {
            return this.sortColumns(filteredCheckboxes, sortData);
          } else {
            return filteredCheckboxes;
          }
        }
      };
    },
  },
  created() {
    // this.Local.remove("displayType");
    this.getPointData("init");
  },
  methods: {
    // 存储key索引
    storeSetCurrentIndex(type = "set") {
      if (type === "set") {
        const getIndexObj = this.Local.get("pointIndex") || {};
        getIndexObj[this.currenKey] = this.currenPointIndex;
        this.Local.set("pointIndex", getIndexObj);
      } else {
        return this.Local.get("pointIndex") || {};
      }
    },
    // 选择当前指标
    pointClick(row, index) {
      if (this.currenPointIndex != index) {
        this.currenPointIndex = index;
        // 存储当前点击指标index
        this.storeSetCurrentIndex("set");
        const checkData = this.filterCheckbox(
          row,
          true,
          this.pointerArr[this.currenPointIndex].arrayCheck
        );
        this.$emit("propData", checkData);
      }
    },
    // 扩展方法-倍数ROI处理
    // roiDisposeFn() {
    //   const getPonit = this.Local.get(this.cacheKeyProp);
    //   const displayType = this.Local.get("displayType");
    //   const prointArrItem = this.pointerArr[this.currenPointIndex];
    //   const updatedArray = prointArrItem.arrayCheck.map((item) => {
    //     if (
    //       displayType == 2 &&
    //       item.startsWith("ROI") &&
    //       !item.includes("倍数")
    //     ) {
    //       return item + "倍数";
    //     } else if (displayType != 2 && item.includes("倍数")) {
    //       return item.replace("倍数", "");
    //     }
    //     return item;
    //   });
    //   const labelCheckBoxAll = this.filterCheckbox({
    //     arrayCheck: updatedArray,
    //   }).map((item) => item.label);
    //   if (prointArrItem.arrayCheck !== labelCheckBoxAll) {
    //     getPonit[this.currenPointIndex].arrayCheck = labelCheckBoxAll;
    //     this.Local.set(this.cacheKeyProp, getPonit);
    //     this.pointerArr[this.currenPointIndex].arrayCheck = labelCheckBoxAll;
    //   }
    // },
    // 获取-更新指标
    getPointData(type) {
      const getPonit = this.Local.get(this.cacheKeyProp);
      if (getPonit) {
        this.pointerArr = getPonit;
        this.currenIndexNob();
        const prointArrItem = this.pointerArr[this.currenPointIndex];
        this.roiDisposeFn();
        const checkData = this.filterCheckbox(
          this.pointerArr[this.currenPointIndex],
          true,
          prointArrItem.arrayCheck
        );
        if (checkData) {
          this.$emit("propData", checkData);
        }
      } else if (!getPonit && type !== "init") {
        // 如果是空
        this.Local.remove(this.cacheKeyProp);
        this.$emit("propData", []);
      } else {
        // 如果默认的
        if (this.defaultArr && type === "init" && this.pointerArr.length <= 0) {
          const arrs = JSON.parse(JSON.stringify(this.defaultArr));
          const labelsArray = arrs.map((item) => item.label);
          this.currenIndexNob();
          this.pointerArr.push({
            title: "默认指标",
            arrayCheck: labelsArray,
          });
          const prointArrItem = this.pointerArr[this.currenPointIndex];
          const checkData = this.filterCheckbox(
            prointArrItem,
            true,
            labelsArray
          );
          this.$emit("propData", checkData);
        }
      }
    },
    // 编辑-删除指标
    pointIndexHander(item, type) {
      if (type === "edit") {
        this.openTitle = "编辑";
        this.selectedCheckboxes = item.arrayCheck;
        this.editItem = item;
        this.dialogVisible = true;
      } else {
        const itemToDelete = this.pointerArr.find(
          (ls) => ls.title === item.title
        );
        if (itemToDelete) {
          const indexToDelete = this.pointerArr.indexOf(itemToDelete);
          if (indexToDelete > -1) {
            this.pointerArr.splice(indexToDelete, 1);
            this.Local.set(this.cacheKeyProp, this.pointerArr);
            // 删除当前行更新,否则不更新
            if (indexToDelete === this.currenPointIndex) {
              this.getPointData();
            } else {
              this.currenIndexNob();
            }
          }
        }
      }
    },
    // 全选当前指标
    handleSelectAll(item) {
      item.checkboxes.forEach((checkbox) => {
        const checkboxIndex = this.selectedCheckboxes.indexOf(checkbox.label);
        if (item.selectedAll && checkboxIndex === -1) {
          this.selectedCheckboxes.push(checkbox.label);
        } else if (!item.selectedAll && checkboxIndex !== -1) {
          this.selectedCheckboxes.splice(checkboxIndex, 1);
        }
      });
    },
    // 全选状态判断
    isIndeterminate(item) {
      const selectedLabels = this.selectedCheckboxes;
      const allLabels = item.checkboxes.map((checkbox) => checkbox.label);
      const selectedCount = selectedLabels.filter((label) =>
        allLabels.includes(label)
      ).length;
      item.selectedAll = selectedCount === allLabels.length;
      return selectedCount > 0 && selectedCount < allLabels.length;
    },
    // 指定索引
    currenIndexNob() {
      const getIndexObj = this.storeSetCurrentIndex("get");
      this.currenPointIndex = getIndexObj[this.currenKey];
      if (!this.currenPointIndex) {
        this.currenPointIndex = 0;
      } else {
        if (this.pointerArr.length <= 1) {
          this.currenPointIndex = 0;
        } else {
          this.currenPointIndex = getIndexObj[this.currenKey] || 0;
        }
      }
      this.storeSetCurrentIndex("set");
    },
    // 添加指标
    addPointerSubmit() {
      this.dialogVisible = false;
      this.emitArr = this.filterCheckbox(
        {
          arrayCheck: this.selectedCheckboxes,
        },
        true,
        this.selectedCheckboxes
      );
      const dataItem = {
        title: "",
        arrayCheck: this.selectedCheckboxes,
      };
      if (this.openTitle === "添加") {
        this.$prompt("请输入指标名", "提示", {
          confirmButtonText: "确定",
          cancelButtonText: "取消",
          closeOnClickModal: false,
          inputValidator: (value) => {
            if (!value) {
              return "不能为空!";
            }
          },
          beforeClose: (action, instance, done) => {
            if (action === "confirm") {
              const isDuplicate = this.pointerArr.some(
                (item) => item.title === instance.inputValue
              );
              if (isDuplicate) {
                this.$message.error("已存在相同指标名");
                return false;
              } else {
                done();
              }
            } else {
              done();
            }
          },
        }).then(({ value }) => {
          dataItem.title = value;
          if (this.pointerArr && Array.isArray(this.pointerArr)) {
            const updatedData = [...this.pointerArr, dataItem];
            this.Local.set(this.cacheKeyProp, updatedData);
          } else {
            const newData = [dataItem];
            this.Local.set(this.cacheKeyProp, newData);
          }
          this.$emit("propData", this.emitArr);
          this.getPointData();
        });
      } else {
        const editIndex = this.pointerArr.findIndex(
          (item) => item.title === this.editItem.title
        );
        if (editIndex !== -1) {
          (dataItem.title = this.editItem.title),
            (this.pointerArr[editIndex] = dataItem);
          this.Local.set(this.cacheKeyProp, this.pointerArr);
        }
        this.$emit("propData", this.emitArr);
      }
    },
    // 新增自定义指标
    addUserDefinedIndicators() {
      this.openTitle = "添加";
      this.selectedCheckboxes = [];
      this.dialogVisible = true;
    },
  },
};
</script>

<style lang="scss" scoped>
.indicator-all-box {
  float: right;
  margin-right: 5px;
}
.indicator-list {
  ul {
    padding: 0;

    li {
      padding: 10px 0;
      border-bottom: 1px solid #e1e1e1;
    }
  }

  .flex-indicator-item {
    display: flex;
    justify-content: space-between;
    padding: 0 15px;

    .right-indicator {
      i {
        padding-left: 10px;
        display: inline-block;
        font-size: 16px;
        cursor: pointer;
      }
    }
  }
}

.box-card {
  margin-bottom: 10px;
}

.el-divider {
  margin: 10px 0;
}

.rihgt-all-check {
  float: right;
  padding: 3px 0;
}

.el-checkbox {
  margin-bottom: 10px;
}

.customize-indicator-data-container {
  display: flex;
  min-height: 60vh;

  .card-checkbox-content-left {
    max-height: 600px;
    overflow-y: scroll;
    flex: 1;
  }

  .sort-view-dx {
    width: 300px;
    margin-left: 15px;
    .sort-row {
      height: 60vh;
      overflow-y: scroll;
      p {
        background-color: #fdfdfd;
        height: 32px;
        line-height: 32px;
        border: 1px solid #ebebeb;
        padding: 0 10px;
        margin: 5px 0;

        &:hover {
          cursor: move;
        }
      }
    }
  }
}

.active-li {
  background-color: #efefef;
}
</style>

上方注释扩展方法说明:

比如你上方有筛选条件需要关联切换的,拿我自己的例子,见顶部ROI区域

他筛选条件有一个ROI、ROI倍数的筛选。然后字段展示是ROI123456...等,是循环的数量。切换ROI倍数的时候 表头原有的ROI需要变成ROI倍数 以及prop也一样要变化。

列如顶部ROI附加复选框的方法:

javascript 复制代码
this.cardDataProp[1] = {
  title: "ROI指标",
  checkboxes: Array.from(
    { length: this.queryParams.displayNum },
    (_, i) => ({
      prop: `roi${i + 1}${
        this.queryParams.displayType == 1 ? "_rate" : ""
      }`,
      label: `ROI${i + 1}${
        this.queryParams.displayType == 2 ? "倍数" : ""
      }`,
    })
  ),
};

筛选条件选择切换displayType类型后调用 this.$refs.rois.getPointData("init"); 刷新表头

以上根据了选项displayType变化label和prop 但又是属于同一个label表头 只是字段不一样的 或者要用循环的,可采用这种方式,扩展方法自己研究...估计没有其他人需要用这个扩展的,就注释了,不用的可以删掉!

感谢你的阅读,如对你有帮助请收藏+关注!

只分享干货实战精品从不啰嗦!!!

如某处不对请留言评论,欢迎指正~

博主可收徒、常玩QQ飞车,可一起来玩玩鸭~

相关推荐
迷雾漫步者2 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-2 小时前
验证码机制
前端·后端
燃先生._.3 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖4 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235244 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240255 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar5 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人6 小时前
前端知识补充—CSS
前端·css
GISer_Jing6 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试