vue 不依赖框架实现table合并单元格,固定列/表头,按照列排序

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

最近一个技术考核要实现 使用vue不依赖任何UI实现 封装一个公共的
table组件,基本的table功能,表头,行,列功能,api用法参看elementUI/antUI,实现固定表头和列实现按照列排序,实现合并单元格,花了我三天时间,也在网上看了很多资料,废话不多少,看手法


一、实现基本table的封装

先看文件结构
看着四个文件就好

index就是我们的主入口 接下来上index.vue的代码

<template>
  <!-- 把tableData,表格数据传入table组件, -->
  <!-- -->

  <my-table
    :model="tableData"
    :prop="label"
    :fixed="'right'"
    :merge="mergeData"
  >
    <!--:height="200"
        sortable:是否排序,点击标题内的button触发
        table-cluomn:MyTableColumn组件传来的生成的列的顺序
        prop:每个格子的属性名
        label:每个格子的中文标题
        width:给每一个表头设置宽度
        height:是否传递高度 传递高度代表固定表头
        fixed:是否固定列
       -->
    <!-- <my-table-column
      :sortable="false"
      @table-cluomn="cluomn($event)"
      :prop="'big'"
      :fixed="'top'"
      :label="'大小'"
    ></my-table-column>
    <my-table-column
      :sortable="true"
      @table-cluomn="cluomn($event)"
      :prop="'name'"
      :label="'姓名'"
    ></my-table-column>
    <my-table-column
      :sortable="true"
      @table-cluomn="cluomn($event)"
      :prop="'eat'"
      :label="'食物'"
    ></my-table-column>
    <my-table-column
      :sortable="true"
      @table-cluomn="cluomn($event)"
      :prop="'date'"
      :label="'日期'"
    ></my-table-column>
    <my-table-column
      :sortable="true"
      @table-cluomn="cluomn($event)"
      :prop="'phoneNum'"
      :label="'电话'"
    ></my-table-column>
    <my-table-column
      :sortable="true"
      @table-cluomn="cluomn($event)"
      :prop="'hobbit'"
      :label="'兴趣'"
    ></my-table-column>
    <my-table-column
      :sortable="true"
      @table-cluomn="cluomn($event)"
      :prop="'address'"
      :label="'地址'"
    ></my-table-column> -->

    <!-- 进行精简化 -->
    <my-table-column
      v-for="(item, index) in tableHeader"
      :key="index"
      :sortable="true"
      @table-cluomn="cluomn($event)"
      :prop="item.property"
      :label="item.name"
      :fixed="'rightTop'"
    ></my-table-column>

    <!--如果传递fixed 则为固定顶部行 -->
  </my-table>
</template>

<script>
// 导入组件
import MyTable from "./MyTable.vue";
import MyTableColumn from "@/components/MyTable/MyTableColumn.vue";
export default {
  components: { MyTable, MyTableColumn },
  data() {
    return {
      // 动态获取表格 标题的顺序 big,address,name,date
      label: [],

      mergeData: ["hobbit", "eat"], //合并单元格
      tableHeader: [
        { name: "大小", property: "big" },
        { name: "姓名", property: "name" },
        { name: "食物", property: "eat" },
        { name: "日期", property: "date" },
        { name: "电话", property: "phoneNum" },
        { name: "兴趣", property: "hobbit" },
        { name: "地址", property: "address" }
      ], //表头对应字段
      // 表格数据
      tableData: [
        {
          big: 1,
          date: "2016-05-02",
          name: "王小虎",
          address: "上海市普陀区金沙江路 1517 弄",
          eat: "面条",
          hobbit: "唱跳",
          phoneNum: "13133996644"
        },
        {
          big: 2,
          date: "2016-05-04",
          name: "王小虎",
          address: "上海市普陀区金沙江路 1518 弄",
          eat: "面条",
          hobbit: "唱跳",
          phoneNum: "13133996644"
        },
        {
          big: 3,
          date: "2016-04-05",
          name: "王小虎",
          address: "上海市普陀区金沙江路 1519 弄",
          eat: "面条",
          hobbit: "唱跳",
          phoneNum: "13133996644"
        },
        {
          big: 4,
          date: "2016-05-03",
          name: "王小虎",
          address: "上海市普陀区金沙江路 1520 弄"
        },
        {
          big: 5,
          date: "2016-04-05",
          name: "王小虎",
          address: "上海市普陀区金沙江路 1521 弄",
          eat: "面条",
          hobbit: "唱跳",
          phoneNum: "13133996644"
        },
        {
          big: 6,
          date: "2016-04-05",
          name: "王小虎",
          address: "上海市普陀区金沙江路 1522 弄",
          eat: "面条",
          hobbit: "唱跳",
          phoneNum: "13133996644"
        },
        {
          big: 7,
          date: "2016-04-05",
          name: "王小虎",
          address: "上海市普陀区金沙江路 1523 弄",
          eat: "面条",
          hobbit: "唱跳",
          phoneNum: "13133996644"
        },
        {
          big: 8,
          date: "2016-04-05",
          name: "王小虎",
          address: "上海市普陀区金沙江路 1524 弄",
          eat: "面条",
          hobbit: "唱跳",
          phoneNum: "13133996644"
        },
        {
          big: 9,
          date: "2016-04-05",
          name: "王小虎",
          address: "上海市普陀区金沙江路 1525 弄",
          eat: "面条",
          hobbit: "唱跳",
          phoneNum: "13133996644"
        },
        {
          big: 10,
          date: "2016-04-05",
          name: "王小虎",
          address: "上海市普陀区金沙江路 1526 弄",
          eat: "面条",
          hobbit: "唱跳",
          phoneNum: "13133996644"
        },
        {
          big: 11,
          date: "2016-04-05",
          name: "王小虎",
          address: "上海市普陀区金沙江路 1527 弄",
          eat: "面条",
          hobbit: "唱跳",
          phoneNum: "13133996644"
        },
        {
          big: 12,
          date: "2016-04-05",
          name: "王小虎",
          address: "上海市普陀区金沙江路 1528 弄",
          eat: "面条",
          hobbit: "唱跳",
          phoneNum: "13133996644"
        },
        {
          big: 13,
          date: "2016-04-05",
          name: "王小虎",
          address: "上海市普陀区金沙江路 1529 弄",
          eat: "面条",
          hobbit: "唱跳",
          phoneNum: "13133996644"
        },
        {
          big: 14,
          date: "2016-04-05",
          name: "王小虎",
          address: "上海市普陀区金沙江路 1530 弄",
          eat: "面条",
          hobbit: "唱跳",
          phoneNum: "13133996644"
        },
        {
          big: 15,
          date: "2016-04-05",
          name: "王小虎",
          address: "上海市普陀区金沙江路 1531 弄",
          eat: "面条",
          hobbit: "唱跳",
          phoneNum: "13133996644"
        },
        {
          big: 16,
          date: "2016-04-05",
          name: "王小虎",
          address: "上海市普陀区金沙江路 1532 弄",
          eat: "面条",
          hobbit: "唱跳",
          phoneNum: "13133996644"
        },
        {
          big: 17,
          date: "2016-04-05",
          name: "王小虎",
          address: "上海市普陀区金沙江路 1533 弄",
          eat: "面条",
          hobbit: "唱跳",
          phoneNum: "13133996644"
        },
        {
          big: 18,
          date: "2016-04-05",
          name: "王小虎",
          address: "上海市普陀区金沙江路 1534 弄",
          eat: "面条",
          hobbit: "唱跳",
          phoneNum: "13133996644"
        },
        {
          big: 19,
          date: "2016-04-05",
          name: "王小虎",
          address: "上海市普陀区金沙江路 1535 弄",
          eat: "面条",
          hobbit: "唱跳",
          phoneNum: "13133996644"
        },
        {
          big: 20,
          date: "2016-04-05",
          name: "王小虎",
          address: "上海市普陀区金沙江路 1536 弄",
          eat: "面条",
          hobbit: "唱跳",
          phoneNum: "13133996644"
        },
        {
          big: 21,
          date: "2016-04-05",
          name: "王小虎",
          address: "上海市普陀区金沙江路 1537 弄",
          eat: "面条",
          hobbit: "唱跳",
          phoneNum: "13133996644"
        },
        {
          big: 22,
          date: "2016-04-05",
          name: "王小虎",
          address: "上海市普陀区金沙江路 1538 弄",
          eat: "面条",
          hobbit: "唱跳",
          phoneNum: "13133996644"
        }
      ]
    };
  },
  mounted() {
    this.merge();
  },
  methods: {
    // 通过MyTableColumn组件的this.$emit() 把每一个列的标题传过来,不然生成列的顺序会是data 里面tableData的顺序
    cluomn(e) {
      // 每次生成一个列的标题就push一个标题到label
      this.label.push(e);
    },

    // 合并单元格
    merge() {
      let tdArr = [];
      this.tableData.forEach((item, index) => {
        for (let i in item) {
        }
        // let key = Object.key(item);
      });
    }
  }
};
</script>

下面的methods方法可以先不看 这里面是后面介绍的方法

接下来是我们引用的 myTable 文件

<template>
  <div class="table">
    <!-- 表格整体组件 -->
    <table border="1" width="130%" class="table-body">
      <thead>
        <tr :class="fixed === 'top' ? 'table-head' : ''">
          <!-- 标题行插槽 用来接收 MyTableColumn组件 -->
          <!-- 进行固定表头 -->
          <slot></slot>
        </tr>
      </thead>
      <tbody>
        <!-- 每一行的组件,根据index过来的数组生成行内数据 -->
        <!--
      model:传入每一个数组元素
      prop:标题的顺序,用来传给下一级组件,来实现标题和列的对应
     -->
        <my-line
          v-for="(item, index) in model"
          :model="item"
          :prop="prop"
          :fixed="fixed"
          :key="index"
          :merge="merge"
        ></my-line>
      </tbody>
    </table>
  </div>
</template>
<script>
import MyLine from "./MyLine.vue";
export default {
  components: { MyLine },
  props: {
    // tableData
    model: {
      type: Array
    },
    // 标题顺序
    prop: {
      type: Array
    },
    height: {
      type: Number
    },
    fixed: {
      type: String
    },
    merge: {
      type: Array
    }
  },

  data() {
    return {
      listData: this.model
    };
  },

  watch: {
    //! 在watch中使用箭头函数会导致this访问不到,需改成function
    listData: function(newVal, oldVal) {
      this.combineCell(this.listData);
      immediate: true;
      deep: true;
    }
  },

  methods: {
    // 合并单元格
    // 1.找到相同的数据
    // 2.针对相同的数据并且合并
    /** 数据处理-合并单元格 */
    /** list: 后台传回的数据 */
    combineCell(list) {
      // 获取数据中的字段,也就是table中的column,只需要取其中一条记录的就可以了
      // 定义数据list的index
      for (let field in list[0]) {
        let k = 0;
        let i = 0;
        while (k < list.length) {
          // 增加字段-用于统计有多少重复值
          list[k][field + "span"] = 1;
          // 增加字段-用于控制显示与隐藏
          list[k][field + "dis"] = "";
          for (i = k + 1; i <= list.length - 1; i++) {
            // 判断第k条数据的field字段,与下一条是否重复
            if (list[k][field] === list[i][field] && list[k][field] !== "") {
              // 如果重复,第k条数据的字段统计+1
              list[k][field + "span"]++;
              // 设置为显示
              list[k][field + "dis"] = "";
              // 重复的记录,则设置为1,表示不跨行
              list[i][field + "span"] = 1;
              // 并且该字段设置为隐藏
              list[i][field + "dis"] = "none";
            } else {
              break;
            }
          }
          // 跳转到第i条数据的索引
          k = i;
        }
      }
      return list;
    }
  }
};
</script>

可以看到我们用了slot进行 插槽 这个插槽里的内容就是 MyLine.vue
下面贴上 MyLine 文件的代码 MyLine文件主要的作用是做成行 也就是表格

<template>
  <tr class="body-row fixed-left">
    <!--
      生成一行数据,
      遍历生成的标题顺序,
      从每一个对象中取出用户选定的标题的数据
     -->
    <td
      v-for="(td, index) in prop"
      ref="td"
      colspan="1"
      :rowspan="merge.indexOf(td) !== -1 ? model.eatspan : 1"
      :style="getDisPlay(td)"
      :key="index"
      :class="
        fixed === 'left'
          ? 'table-cell fixed-left'
          : fixed === 'right'
          ? 'table-cell fixed-right'
          : 'table-cell'
      "
    >
      <div class="content">{{ model[td] }}</div>
    </td>
  </tr>
</template>

<script>
export default {
  props: {
    // 绑定的tableData
    model: {
      type: Object
    },
    // 标题顺序
    prop: {
      type: Array
    },
    fixed: {
      type: String
    },
    merge: {
      type: Array
    }
  },
  data() {
    return {
      data: this.model
    };
  },

  created() {},

  mounted() {},

  methods: {
    // 根据td来进行动态表格行合并
    getDisPlay(td) {
      if (this.merge.indexOf(td) !== -1) {
        return { display: this.model.eatdis };
      } else {
        return {};
      }
    }
  }
};
</script>

可以看到这个文件非常简单,主要也就是进行一些行合并的操作
接下来是 MyTableColumn 文件 这个文件 主要是表头的操作

<template>
  <th colspan="1" rowspan="1" :class="className">
    <div class="top-content">
      <!-- 展示列名 -->
      <span>{{ label }}</span>
      <!-- button 实现升降排序 -->
      <button @click="sort('asc')" v-if="sortable">升序</button>
      <button @click="sort('desc')" v-if="sortable">降序</button>
    </div>
  </th>
</template>
<script>
export default {
  props: {
    // 当前行的标签属性名
    prop: {
      type: String,
      default: ""
    },
    // 当前行的标签属性文本
    label: {
      type: String,
      default: ""
    },
    // 是否排序
    sortable: {
      type: Boolean,
      default: true
    },
    fixed: {
      type: String,
      default: ""
    }
  },
  data() {
    return {
      // 实现排序功能
      "asc-n": (a, b) => a - b,
      "desc-n": (a, b) => b - a,
      //js字符串对比
      // 升序
      "asc-s": (a, b) => a.localeCompare(b),
      // 降序
      "desc-s": (a, b) => b.localeCompare(a),

      // th 类型 =》是否固定
      className: ""
    };
  },

  watch: {
    fixed: {
      handler(newVal, oldVal) {
        switch (this.fixed) {
          case "top":
            this.className = "cell fixed-top";
            break;
          case "leftTop":
            this.className = "cell fixed-left-top";
            break;
          case "rightTop":
            this.className = "cell fixed-right-top";
            break;
          case "left":
            this.className = "cell fixed-left";
            break;
          case "right":
            this.className = "cell fixed-right";
            break;
          default:
            this.className = "cell";
            break;
        }
      },
      immediate: true
    }
  },

  mounted() {
    // 传回父组件,通知父组件把字符串加入顺序数组
    this.$emit("table-cluomn", this.prop);
  },
  methods: {
    sort(type) {
      // 是否排序
      this.$nextTick(() => {
        if (this.sortable) {
          // 对父父亲元素进行排序
          this.$parent.$parent.tableData.sort((a, b) => {
            // 调用排序规则,传入排序方式asc、desc,然后判断数据类型进行拼接后调用方法实现排序
            return this[type + this.getSortType(a[this.prop])](
              a[this.prop],
              b[this.prop]
            );
          });
        }
      });
    },

    // 处理排序类型
    getSortType(val) {
      // 返回标识串进行拼接
      return typeof val === "string" ? "-s" : "-n";
    }
  }
};
</script>

二、如何食用

可能看起来不太明白?先讲一下 我的的index.vue数据结构也是模仿的elementUI 进行传输 但是我的column进行了循环而不是死板的写一个是一个,虽然只是技术考核但是咱也得考虑实际情况不是。 需要注意的是我这里没有贴出来css代码 这里固定列和行使用的是 css的 position:sticky 方法,有需要的可以去百度了解下,放一下效果图。
在这里插入图片描述

总结

其实写的时候还是遇到很多困难的,甚至有一段时间甚至想凑活凑活得了。结果还是写了下来 记得点赞!

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
好的,下面是用 Vue 实现动态合并单元格的表格代码示例: ```html <template> <table> <thead> <tr> <th>姓名</th> <th>年龄</th> <th>性别</th> <th>所在城市</th> </tr> </thead> <tbody> <template v-for="(item, index) in tableData"> <tr :key="index"> <template v-if="isFirstCell(index)"> <td :rowspan="getRowSpan(item)">{{ item.name }}</td> </template> <td>{{ item.age }}</td> <td>{{ item.gender }}</td> <td>{{ item.city }}</td> </tr> </template> </tbody> </table> </template> <script> export default { data() { return { tableData: [ { name: '张三', age: 20, gender: '男', city: '北京' }, { name: '李四', age: 25, gender: '女', city: '上海' }, { name: '王五', age: 30, gender: '男', city: '广州' }, { name: '赵六', age: 35, gender: '女', city: '深圳' }, ], mergeRows: [], }; }, methods: { isFirstCell(index) { return index === 0 || this.mergeRows.indexOf(index) === -1; }, getRowSpan(item) { let count = 1; for (let i = this.tableData.indexOf(item) + 1; i < this.tableData.length; i++) { if (item.name === this.tableData[i].name) { count++; this.mergeRows.push(i); } else { break; } } return count; }, }, }; </script> ``` 在这个示例中,我们先定义了一个表格,表头包括姓名、年龄、性别和所在城市四。接着使用 `v-for` 指令遍历 `tableData` 数组,通过 `isFirstCell` 方法判断是否为每组数据中的第一个单元格,如果是,则使用 `rowspan` 属性来合并单元格。 `getRowSpan` 方法用于获取每个单元格需要横跨的行数,如果当前行的姓名与下一行的姓名相同,则累加计数器 `count`,同时将需要被合并的行的索引添加到 `mergeRows` 数组中,最后返回计数器的值。最后在模板中使用条件渲染来控制单元格的合并。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值