实用的element-ui table二次封装

需求

又到了一天一个新需求的环节了,今天万恶了资本主义又不知道从哪发现了一个新大陆。

要在原有的el-table上 可拖动列、调整列宽、本地按照用户保存、控制动态列项显示

要不是看在我 爱学习 才不给你开发呢!!!!

话不多说上图吧:

image.png

这是一个6列的页面,用户不想要最后一列了,只需要点击列头右侧按钮,在弹出层里,就可以 进行如下操作:

  • 拖拽改变列项顺序,
  • 控制哪列显示,
  • 重置恢复初始代码配置顺序,

image.png

思路

实现关闭浏览器还能缓存住 那最后一定存 localstorage 里了。table列项数据一定要配置项了。然后 控制配置项 就好了。那就撸起袖子一把梭哈。0.o

table组件

<script lang="ts">
import { Component, Prop, Mixins, Emit } from 'vue-property-decorator';
import { TableMix } from 'feok-lib';
import TableSettingMix from '@/views/mixins/TableSettingMix';

import ColumnsDrawer from '@/components/ColumnsDrawer';

@Component({
  components: {
    ColumnsDrawer,
  },
})
export default class Table extends Mixins(
  TableMix,
  TableSettingMix,
) {
  
  @Prop({ default: 'XXXX' })
  public tableUserKey!: string; // local缓存的key名,一定要保证不同哦
  
  public COLUMNS: any[] = [
    {
      prop: 'order_id',
      label: '入库单号/入库仓',
      sort: 0,
      width: '',
      minWidth: '120',
      drag: false, // 是否可拖拽
      display: true, // 是否显示
      disabled: true, // 禁用弹窗选中操作
      slot: 'order_id', // 如果跟prop相同,就认为是名为order_id的插槽
    },
    ...
    // 多个就是复制上面的就好了,简化代码就不写了
  ];
}
</script>

<template>
  <div class="test-table">
    <el-table
      size="small"
      border
      :data="data"
      :ref="refStr"
      v-bind="$attrs"
      v-on="$listeners"
      v-loading="loading"
      :empty-text="emptyText"
      row-key="id"
      @header-dragend="headerDragend"
    >	
      <!-- 我习惯了开头留一个插槽,末尾留一个插槽,可以无视 -->
      <slot name="firstColumn"></slot>
      
      <template v-for="(item, index) of removelastColumnsWidth(columns)">
        <!-- 插槽的配置项 -->
        <slot :name="item.slot" v-if="item.prop === item.slot" :item="item" :index="index"></slot>
        
        <el-table-column
          v-if="item.prop === 'order_id'"
          prop="order_id"
          :width="item.width"
          :key="`${index}-${item.prop}`"
          :minWidth="item.minWidth"
          :resizable="item.resizable"
          :label="label"
        >
          <template slot-scope="{row}">
            <div>{{$ph(row.order_id)}}</div>
          </template>
        </el-table-column>
        <!-- 同理 后面的不写了 -->
      </template>

			<!-- 我习惯了开头留一个插槽,末尾留一个插槽,可以无视 -->
      <slot name="handle" :setting="settingColumns"></slot>
    </el-table>

		// 弹框组件
    <ColumnsDrawer
      v-if="destoryDrawer"
      :visiable="visiable"
      :columns="columns"
      @close="close"
      @reset="reset"
      @on-done="saveSetting"
      @closed="destoryDrawer=false"
    />
  </div>
</template>
复制代码

看完上面的代码,你一定好奇ColumnsDrawer组件是干嘛的,有必要么,那我们来看一下吧,其实他就是弹框组件。

你会发现最终用的el-drawer + el-tree实现的。

弹框组件

<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator';

@Component
export default class ColumnsDrawer extends Vue {
  public get defaultCheckedKeys() {
    // 默认显示想,记得我们刚才配置的display属性么,不记得划上去看一眼吧
    return this.data
      .filter((item: any) => item.display)
      .map((item: any) => item.prop);
  }

  @Prop() public visiable!: boolean; // el-drawer显示项
  @Prop({ default: () => [] }) public columns!: any[]; // table 列项配置项

  public data: any[] = [];	// el-tree数据(其实就是table深拷贝)
  public defaultProps = {		// el-tree 配置项,可以参考el官网
    children: 'children',
    label: 'label',
  };
	
  // el-tree的一系列拖动事件(想了解去官网,没用到就不解释了,看英文直译就好)
  public handleDragStart(snode: any, event: Event) {
    // console.log(snode, event);
  }
  public handleDragEnter(snode: any, enode: any, event: Event) {
    // console.log(snode, enode, event);
  }
  public handleDragLeave(snode: any, enode: any, event: Event) {
    // console.log(snode, enode, event);
  }
  public handleDragOver(snode: any, enode: any, event: Event) {
    // console.log(snode, enode, event);
  }
  // 拖拽结束
  public handleDragEnd(snode: any, enode: any, event: Event) {
    // 互换sort 实现改变列项顺序
    const tempSort = snode.data.sort;
    snode.data.sort = enode.data.sort;
    enode.data.sort = tempSort;
  }
  public handleDrop(snode: any, enode: any, event: Event) {
    // console.log(snode, enode, event);
  }
  // 拖拽时判定目标节点能否被放置
  public allowDrop(draggingNode: any, dropNode: any, type: string) {
    // console.log(dropNode.data.drag);
    if (dropNode.data.drag === false || type === 'inner') {
      return false;
    } else {
      return true;
    }
  }
  // 判断节点能否被拖拽
  public allowDrag(node: any) {
    return node.data.drag;
  }
  // 保存
  public save() {
    this.$emit('on-done', this.data);
  }
  // 选中变化
  public checkChange(item: any, checked: boolean) {
    item.display = checked;
  }
  public created() {
    this.data = JSON.parse(JSON.stringify(this.columns));
  }
}
</script>

<template>
  <div class="test-drawer">
    <el-drawer
      v-on="$listeners"
      title="配置列项"
      :visible="visiable"
      :show-close="false"
      :wrapper-closable="true"
      direction="rtl"
      size="auto"
    >
      <template slot="title">
        <span>配置列项</span>
        <sub class="feok-link re-md-sm-margin-left" @click="$emit('reset')" title="重置列项配置信息">重置</sub>
        <!-- <i class="el-icon-refresh re-md-sm-margin-left" @click="$emit('reset')"></i> -->
      </template>
      <div class="columns-setting">
        <el-tree
          class="setting-columns"
          :data="data"
          :props="defaultProps"
          draggable
          show-checkbox
          node-key="prop"
          :default-checked-keys="defaultCheckedKeys"
          @node-drag-start="handleDragStart"
          @node-drag-enter="handleDragEnter"
          @node-drag-leave="handleDragLeave"
          @node-drag-over="handleDragOver"
          @node-drag-end="handleDragEnd"
          @node-drop="handleDrop"
          @check-change="checkChange"
          :allow-drop="allowDrop"
          :allow-drag="allowDrag"
        >
          <div class="custom-tree-node" slot-scope="{ node, data }">
            <span>{{ node.label }}</span>
            <i class="el-icon-rank" v-if="data.drag"></i>
          </div>
        </el-tree>
        <div class="setting-btn">
          <el-button size="small" type="primary" @click="save">确定</el-button>
          <el-button size="small" @click="$emit('close')">取消</el-button>
        </div>
      </div>
    </el-drawer>
  </div>
</template>
<style lang="less">
.test-drawer {
  .columns-setting {
    display: flex;
    flex-direction: column;
    min-width: 210px;
    height: 100%;
    padding: 10px;
    .setting-columns {
      flex: 1;
      li {
        list-style: none;
      }
    }
    .setting-btn {
      flex: 0 0 auto;
    }
    .custom-tree-node {
      display: flex;
      justify-content: space-between;
      width: 100%;
      [class*='el-icon'] {
        opacity: 0.6;
      }
    }
  }
  .el-drawer__header {
    margin-bottom: 0;
    padding: 20px;
    border-bottom: 1px solid #f0f0f0;
    font-weight: bold;
    background: rgb(243, 243, 244);
  }
  .el-tree-node {
    margin-bottom: 10px;
    [class*='el-tree-node__expand'] {
      display: none;
    }
  }
}
</style>
复制代码

好了,组件写完了,是不是可以直接用了?等等,好像少了点东西没给我啊,我是不是藏私了?

被发现了,那我交代吧。

TableSettingMix混合

/**
 * @author Yuanr
 * @description table列项调整混合
 */
import { Component, Vue, Prop } from 'vue-property-decorator';
import { get } from 'lodash';
import ColumnsDrawer from '@/components/ColumnsDrawer';

interface COLUMNS_SETTING {
	prop: string; // 列key
  label: string; // 列名称
  sort: number; // 列排序,必须每一个项都有排序值
  width: string; // 列宽
  drag: boolean; // 列是否可以拖拽排序
  display: boolean; // 列是否显示
  disabled: boolean; // 列项设置时禁用选中操作
  slot: string; // 列项是否为slot
}

@Component({
  components: {
    ColumnsDrawer,
  },
})
export default class TableSettingMix extends Vue {
  @Prop({ default: 'TableColumns' }) public tableUserKey!: string; // TABLE_USER_KEY tableUserKey
  @Prop({ default: '' }) public tableVersion!: string; // 这里本来想加版本号,后来没用上

  public readonly USER_KEY = `${get(JSON.parse(sessionStorage.getItem('userinfo')!), 'account', '').trim()}`;
  public REFNAME = 'TABLE_REF';
  public COLUMNS: COLUMNS_SETTING[] = [
    // {
    //   prop: 'name', // 列key
    //   label: '姓名', // 列名称
    //   sort: 0, // 列排序,必须每一个项都有排序值
    //   width: '', // 列宽
    //   drag: false, // 列是否可以拖拽排序
    //   display: true, // 列是否显示
    //   disabled: true, // 列项设置时禁用选中操作
    //   slot: 'default', // 列项是否为slot
    // },
  ];
  public visiable = false; // el-drawer visible
  public destoryDrawer = false; // el-drawer v-if
  public columns: any[] = [];

  // 本地存储格式 Table__USER_KEY__TABLE_USER_KEY__TABLE_VERSION
  get localstorageKey() {
    return `Table__${this.USER_KEY}__${this.tableUserKey}__${this.tableVersion}`;
  }
	// 获取table
  get refStr() {
    return (this as any).refName || this.REFNAME;
  }
  // 列model规则: TABLECOLUMNS__项目__模块__Table名称
  // 列项排序
  public sort(data: any[]) {
    return data.sort((s, e) => {
      return s.sort - e.sort;
    });
  }
  // 弹出列项设置
  public settingColumns() {
    this.destoryDrawer = true;
    this.visiable = true;
  }
  public close() {
    this.visiable = false;
  }
  // 修改列项宽度
  public headerDragend(newWidth: number, oldWidth: number, column: any, event: Event) {
    if (!column.resizable) {
      return false;
    }
    const data = JSON.parse(JSON.stringify(this.columns));
    const item: any = data.find((item: any) => item.prop === column.property);
    if (item) {
      item.width = newWidth;
      this.setColumns(data);
      this.syncColumns(data);
    }
  }
  // 保存设置
  public saveSetting(data: any[]) {
    this.setColumns(data);
    this.syncColumns(data);
    this.sortColumns();
    this.close();
  }
  public sortColumns() {
    this.columns = this.sort(this.columns);
  }
  // 根据实际情况,移除最后一项的宽度调整
  public get removelastColumnsWidth() {
    return (data: any[]) => {
      return data.filter((item: any) => item.display).map((item: any, index: number, arr: any[]) => {
        if (index === arr.length - 1) {
          return {
            ...item,
            width: '',
            resizable: false,
          };
        } else {
          return item;
        }
      });
    };
  }
  // sessionStorage.getItem('token');
  // 获取columns
  public getColumns() {
    if (this.tableUserKey && (localStorage as any)[this.localstorageKey]) {
      return JSON.parse((localStorage as any)[this.localstorageKey]);
    } else {
      return [];
    }
  }
  // 设置 columns
  public setColumns(data: any[]) {
    if (this.tableUserKey) {
      (localStorage as any)[this.localstorageKey] = JSON.stringify(data);
    }
  }
  public syncColumns(data: any[]) {
    const map: any = {};
    for (const column of data) {
      map[column.prop] = column;
    }
    this.columns = this.columns.map((column: any) => ({
      ...column,
      ...(map[column.prop] || {}),
      prop: column.prop,
      label: column.label,
    }));
  }
  public created() {
    // 初始化数据
    this.columns = this.COLUMNS;
    // 同步设置
    if (this.tableUserKey && (localStorage as any)[this.localstorageKey]) {
      this.syncColumns(this.getColumns());
    } else {
      this.syncColumns(this.COLUMNS);
    }
    // 排序
    this.sortColumns();
  }
  public mounted() {
    // elementUI Bug 修复
    try {
      const tableHeaderRef: any = ((this.$refs as any)[this.refStr] as any).$refs.tableHeader;
      const handleMouseMove = tableHeaderRef.handleMouseMove;
      const handleMouseOut = tableHeaderRef.handleMouseOut;
      tableHeaderRef.handleMouseMove = (event: Event, column: any) => {
        if (!column.resizable) {
          return false;
        }
        handleMouseMove(event, column);
        const target: any = get(event, 'target.tagName') === 'TH' ? get(event, 'target') : (get(event, 'target.classList').value === 'cell' ? get(event, 'target.parentNode') : undefined);
        if (!target) {
          return false;
        }
        if (tableHeaderRef.draggingColumn) {
          target.classList.add('th-draging');
        } else {
          target.classList.remove('th-draging');
        }
        if (tableHeaderRef.dragging) {
          target.classList.remove('th-draging');
        }
      };
      tableHeaderRef.handleMouseOut = () => {
        handleMouseOut();
        const draggingArr: NodeList = document.querySelectorAll('.th-draging');
        for (const drag of draggingArr) {
          (drag as any).classList.remove('th-draging');
        }
      };
    } catch (e) {
      throw Error('elementUI Bug 待解决');
    }
  }
  // 删除用户自定义
  public reset() {
    if (this.tableUserKey) {
      (localStorage as any)[this.localstorageKey] = '';
      this.columns = this.COLUMNS;
    }
    this.close();
    this.$nextTick(() => {
      this.setColumns(this.COLUMNS);
    });
  }
}

复制代码

好了,剩下就是组件调用了

<PurchaseOrderTable
		ref="PurchaseOrderTable"
    refName="PurchaseOrderTable"
/>
复制代码

总结

其实,重点是弹窗及里面的怎么实现。当我们找到一个方法的时候,剩下的都迎刃而解了。这时候给大兄弟们提个醒,没事就看看官网文档,每次都会有新收获~

兄台,请留步。这里有几点要注意一下哦~

问题1:注意下混合里的本地存储格式定义

因为每个项目定义的规则不一定,所以在这里修改

  // 本地存储格式 Table__USER_KEY__TABLE_USER_KEY__TABLE_VERSION
  get localstorageKey() {
    return `Table__${this.USER_KEY}__${this.tableUserKey}__${this.tableVersion}`;
  }
复制代码

问题2:是不是每一个table都的手写一个配置项啊

这里很遗憾的告诉大家,是的,这只是一个初步解决方案。如果大家有更简洁的方案。欢迎留言~

问题3:留言为什么叫我狗子

我可能是狗,留言哪些小伙伴他们也不-- 当-- 人-- 啊。下回见。👋

彩蛋

啊哈,大家好啊,又进入的彩蛋环节,今天的彩蛋是。 element-ui宣布适配Vue3.0了!!!!

被迫用Ant的小伙伴们可以等一下了!

秀逗马得~~~~~~~~~

公布地址: juejin.cn/post/690073…

Git地址: github.com/element-plu…

官网地址: element-plus.gitee.io/#/zh-CN


如果这篇文章有用,欢迎评论, 点赞, 加关注

我是leo:祝大家早日升职加薪。

分类:
前端
标签: