vue3.0使用JSX封装Table表格

1,055 阅读2分钟

使用过antd表格组件的,应该会被他那种风格吸引,一个表格只需要传入一个column就可以了,我在使用element表格组件的时候就有点不习惯,所以自己封装了一个类似与antd表格组件。

封装这个table表格的思路

这个组件是用vue3.0+ts+jsx语法实现的,因为vue3.0官网上实现了支持JSX语法。

  1. 首先得在tsconfig.json文件中配置jsx。
  2. 封装table时怎么模拟antd表格中的render方法。
  3. JSX语法中怎么使用vue中的插槽。
/**
首先我们这个table需要外部传入什么?
表格的配置项:tableOptions
表格每一项的配置项: tableItemOptions
表格需要的数据:tableData
*/
<script lang="tsx">
import { defineComponent, ref, nextTick } from "vue";
import moment from "moment";
import { ElTable } from "element-plus";
export default defineComponent({
  props: {
    tableItemOptions: {
      type: Array,
      required: true,
    },
    tableData: {
      type: Array,
      required: true,
    },
    tableOptions: {
      type: Object,
    },
  },
  setup(props) {
    // 类型处理事件Map  在map中定义好类型及处理类型的方法,直接根据传入的配置项进行事件调用
    const mapHandler = new Map([
      ["render", renderHandler],
      ["text", textHandler],
      ["operation", operationHandler],
      ["date", dateHandler],
      ["switch", switchHandler],
      ["number", numberHandler],
    ]);
    // 判断类型调用方法
    const typeRenderHandler = (item: tableItemBaseType) => {
      let type = item.type as string;
      if (mapHandler.has(type)) {
        return mapHandler.get(type)?.call(null, item);
      } else {
        return mapHandler.get("text")?.call(null, item);
      }
    };
   
    return () => (
      <el-table
        data={props.tableData}
        border={false}
        {...props.tableOptions}
        style={{ width: "100%", height: "100%" }}
      >
        {tableItemOptions.map((item) => typeRenderHandler(item))}
      </el-table>
    );
  },
});
</script>

下一步开始写每一种数组类型的事件处理

// 文本类型的处理事件
    const textHandler = (item: tableItemBaseType) => {
      const cb = (scope: { row: { [x: string]: any } }) => (
        <span>{scope.row[item.key]}</span>
      );
      return elTableColumnTpl(item, cb);
    };
    // 时间类型的处理方法
    const dateHandler = (item: tableItemBaseType) => {
      const cb = (scope: { row: { [x: string]: any } }) => (
        <span>
          {scope.row[item.key]
            ? moment(scope.row[item.key]).format(item.format || "YYYY-MM-DD")
            : ""}
        </span>
      );
      return elTableColumnTpl(item, cb);
    };
    // switch开关的处理方法
    const switchHandler = (item: tableItemBaseType) => {
      const cb = (scope: { row: { [x: string]: any } }) => (
        <el-switch
          v-model={scope.row[item.key]}
          {...(item.switchOptions && item.switchOptions)}
        />
      );
      return elTableColumnTpl(item, cb);
    };
    // 操作按钮的处理方法   尽量不要用any 我这里忘记定义类型了,所以直接使用了any
    const operationHandler = (item: any) => {
      const cb = (scope: { row: any; $index: any }) => {
        return (
          <div class="table-button-box">
            {item.operationList.map((buttonItem: operationListType) => (
              <el-button
                type={buttonItem.type || null}
                onClick={buttonItem.handler.bind(
                  null,
                  scope.row,
                  scope.$index,
                  buttonItem?.key
                )}
              >
                {buttonItem.title}
              </el-button>
            ))}
          </div>
        );
      };
      return elTableColumnTpl(item, cb);
    };
    // 数字类型的处理方法 尽量不要用any 我这里忘记定义类型了,所以直接使用了any
    const numberHandler = (item: any) => {
      const cb = (scope: { row: any; $index: any }) => {
        return (
          <span>
            {scope.row[item.key]
              ? (
                  scope.row[item.key] / (item.numberOptions?.divide || 100)
                ).toFixed(item.numberOptions?.keepPoint || 2)
              : "0.00"}
          </span>
        );
      };
      return elTableColumnTpl(item, cb);
    };
    // 表格渲染模板的处理方法 通过回调函数的方式将vue插槽中的数据传入
    const elTableColumnTpl = (item: any, cb: Function) => {
      return (
        <el-table-column
          {...item}
          prop={item.key}
          label={item.title}
          width={item.width || null}
          key={item.key}
          fixed={item.fixed || (item.type === "operation" ? "right" : false)}
        >
          {{
            default: (scope: { row: any }) => {
              if (scope && scope.row) {
                return cb(scope);
              }
            },
          }}
        </el-table-column>
      );
    };
    // 模拟antd中render方法,这里我使用的是组件进行的替换,可以通过传入组件的形式进行个性化的处理
    const renderHandler = (item: any) => {
      return (
        <el-table-column
          {...item}
          prop={item.key}
          label={item.title}
          width={item.width || null}
          key={item.key}
          fixed={item.fixed || (item.type === "operation" ? "right" : false)}
        >
          {{
            default: (scope: { row: any }) => {
              if (scope && scope.row) {
                return (
                  <item.component
                    scope={scope.row}
                    componentPorps={item.componentPorps}
                  />
                );
              }
            },
          }}
        </el-table-column>
      );
    };

下边是全部的代码,希望大佬们多多提出意见。

<script lang="tsx">
import { defineComponent, ref, nextTick } from "vue";
import moment from "moment";
import { ElTable } from "element-plus";
export default defineComponent({
  props: {
    tableItemOptions: {
      type: Array,
      required: true,
    },
    tableData: {
      type: Array,
      required: true,
    },
    tableOptions: {
      type: Object,
    },
  },
  setup(props) {
    const tableItemOptions: tableItemBaseType[] =
      props.tableItemOptions as unknown as tableItemBaseType[];
    // const tableOptions: tableOptionsType =
    //   props.tableOptions as tableOptionsType;
    const textHandler = (item: tableItemBaseType) => {
      const cb = (scope: { row: { [x: string]: any } }) => (
        <span>{scope.row[item.key]}</span>
      );
      return elTableColumnTpl(item, cb);
    };
    const dateHandler = (item: tableItemBaseType) => {
      const cb = (scope: { row: { [x: string]: any } }) => (
        <span>
          {scope.row[item.key]
            ? moment(scope.row[item.key]).format(item.format || "YYYY-MM-DD")
            : ""}
        </span>
      );
      return elTableColumnTpl(item, cb);
    };
    const switchHandler = (item: tableItemBaseType) => {
      const cb = (scope: { row: { [x: string]: any } }) => (
        <el-switch
          v-model={scope.row[item.key]}
          {...(item.switchOptions && item.switchOptions)}
        />
      );
      return elTableColumnTpl(item, cb);
    };
    const operationHandler = (item: any) => {
      const cb = (scope: { row: any; $index: any }) => {
        return (
          <div class="table-button-box">
            {item.operationList.map((buttonItem: operationListType) => (
              <el-button
                type={buttonItem.type || null}
                onClick={buttonItem.handler.bind(
                  null,
                  scope.row,
                  scope.$index,
                  buttonItem?.key
                )}
              >
                {buttonItem.title}
              </el-button>
            ))}
          </div>
        );
      };
      return elTableColumnTpl(item, cb);
    };
    const numberHandler = (item: any) => {
      const cb = (scope: { row: any; $index: any }) => {
        return (
          <span>
            {scope.row[item.key]
              ? (
                  scope.row[item.key] / (item.numberOptions?.divide || 100)
                ).toFixed(item.numberOptions?.keepPoint || 2)
              : "0.00"}
          </span>
        );
      };
      return elTableColumnTpl(item, cb);
    };
    const elTableColumnTpl = (item: any, cb: Function) => {
      return (
        <el-table-column
          {...item}
          prop={item.key}
          label={item.title}
          width={item.width || null}
          key={item.key}
          fixed={item.fixed || (item.type === "operation" ? "right" : false)}
        >
          {{
            default: (scope: { row: any }) => {
              if (scope && scope.row) {
                return cb(scope);
              }
            },
          }}
        </el-table-column>
      );
    };
    const renderHandler = (item: any) => {
      return (
        <el-table-column
          {...item}
          prop={item.key}
          label={item.title}
          width={item.width || null}
          key={item.key}
          fixed={item.fixed || (item.type === "operation" ? "right" : false)}
        >
          {{
            default: (scope: { row: any }) => {
              if (scope && scope.row) {
                return (
                  <item.component
                    scope={scope.row}
                    componentPorps={item.componentPorps}
                  />
                );
              }
            },
          }}
        </el-table-column>
      );
    };
    // 类型处理事件Map
    const mapHandler = new Map([
      ["render", renderHandler],
      ["text", textHandler],
      ["operation", operationHandler],
      ["date", dateHandler],
      ["switch", switchHandler],
      ["number", numberHandler],
    ]);
    // 判断类型调用方法
    const typeRenderHandler = (item: tableItemBaseType) => {
      let type = item.type as string;
      if (mapHandler.has(type)) {
        return mapHandler.get(type)?.call(null, item);
      } else {
        return mapHandler.get("text")?.call(null, item);
      }
    };
    const multipleTableRef = ref<InstanceType<typeof ElTable>>();

    const toggleSelection = (rows?: any[]) => {
      if (rows) {
        rows.forEach((row) => {
          // TODO: improvement typing when refactor table
          // eslint-disable-next-line @typescript-eslint/ban-ts-comment
          nextTick(() => {
            multipleTableRef.value!.toggleRowSelection(row, true);
          });
        });
      } else {
        multipleTableRef.value!.clearSelection();
      }
    };
    const filterTableData = () => {
      console.log(props.tableData, ">>>>>>>>>>>>>");
      const arr = props.tableData.filter((item: any) => item.checked);
      toggleSelection(arr);
    };
    if (props.tableOptions?.ref === "multipleTableRef") {
      filterTableData();
    }

    console.log(props.tableOptions);
    return () => (
      <el-table
        data={props.tableData}
        border={false}
        style={{ width: "100%", height: "100%" }}
        {...props.tableOptions}
      >
        {props.tableOptions?.ref === "multipleTableRef" ? (
          <el-table-column type="selection" width="55" />
        ) : null}
        {tableItemOptions.map((item) => typeRenderHandler(item))}
      </el-table>
    );
  },
});
</script>

<style lang="scss" scoped>
.table-button-box {
  display: flex;
}
</style>

下边是ts类型定义文件

// 会导致全局声明文件失效
// import { DefineComponent } from "vue";

declare interface operationListType {
  key?: string;
  type: string;
  handler: (a: any, b: number, c?: string) => void;
  title: string;
}
declare interface tableOptionsType {
  height?: number;
  "max-height"?: number;
  border?: boolean;
  stripe?: boolean;
  size?: string;
  fit?: boolean;
}

declare interface switchOptionsType {
  "active-text"?: string;
  "inactive-text"?: string;
  "active-color"?: string;
  "inactive-color"?: string;
  disabled?: boolean;
  loading?: boolean;
  size?: string;
  "inline-prompt"?: boolean;
  change?: () => {};
  class: string;
}

declare interface numberOptionsType {
  keepPoint: number;
  divide: number;
}

declare interface tableItemBaseType {
  key: string;
  title: string;
  type?: string;
  width?: number;
  "min-width"?: number;
  fixed?: string | boolean;
  sortable?: boolean | string;
  resizable?: boolean;
  format?: string;
  componentPorps?: any;
  component?: import("vue").DefineComponent<any, any, any>;
  switchOptions?: switchOptionsType;
  operationList?: operationListType[];
  numberOptions?: numberOptionsType;
}