【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飞车,可一起来玩玩鸭~

相关推荐
L耀早睡13 分钟前
mapreduce打包运行
大数据·前端·spark·mapreduce
MaCa .BaKa25 分钟前
38-日语学习小程序
java·vue.js·spring boot·学习·mysql·小程序·maven
HouGISer27 分钟前
副业小程序YUERGS,从开发到变现
前端·小程序
outstanding木槿33 分钟前
react中安装依赖时的问题 【集合】
前端·javascript·react.js·node.js
霸王蟹1 小时前
React中useState中更新是同步的还是异步的?
前端·javascript·笔记·学习·react.js·前端框架
霸王蟹1 小时前
React Hooks 必须在组件最顶层调用的原因解析
前端·javascript·笔记·学习·react.js
专注VB编程开发20年1 小时前
asp.net IHttpHandler 对分块传输编码的支持,IIs web服务器后端技术
服务器·前端·asp.net
爱分享的程序员2 小时前
全栈项目搭建指南:Nuxt.js + Node.js + MongoDB
前端
隐含3 小时前
webpack打包,把png,jpg等文件按照在src目录结构下的存储方式打包出来。解决同一命名的图片资源在打包之后,重复命名的图片就剩下一个图片了。
前端·webpack·node.js
lightYouUp3 小时前
windows系统中下载好node无法使用npm
前端·npm·node.js