管理系统通用表格组件封装

文章目的

在后台管理系统中,经常会有列表页,他们的长相基本一致,不同的就是搜索条件、表格列、搜索列等内容。为了提高工作效率我们经常会进行公共列表组件的封装。在网上找了很多资料,都不是很完整,也不是很契合。所以在这里记录一下自己的整合,以及封装好的通用表格组件。

核心技术点

Vue3+ElementPlus+TS+axios

核心思路

尽可能的封装的完善,只给使用着特定的使用方法,将接口的调用、默认值的设置等都通过配置的方式进行设置。以期做到开箱即用,不需要关注组件的具体实现效果。

上代码

组件用到的type

export type CustomConfig = {
  defaultValue?: any; // 当前表单项的默认值
  selectOptions?: { label: string; value: number | string }[];
  url?: string; // 远程搜索select的请求地址
  requestType?: string; // 远程搜索的请求类型
};

export const FormType = {
  INPUT: "INPUT", // 输入框
  SELECT: "SELECT", // 下拉选框
  CASCADER: "CASCADER", // 级联选择器
  DATEPICKER: "DATEPICKER", // 日期选择器,可以通过
};

export interface FormOption {
  // type:当前搜索项的表单类型
  type: string;
  // 当前搜索表单项的属性名
  field: string;
  // props:elementplus组件支持的绑定属性
  props: any;
  // $queryFormatter:提供给父组件用于自定义请求参数,比如可远程搜索的select
  $queryFormatter?: (T: any) => any;
  // $responseFormatter:有时对于请求返回的数据需要自己处理,可以使用这个方法
  $responseFormatter?: (T: any) => any;
  // 自定义的一些其他搜索表单相关的信息:比如select的options;当前表单项的默认值等
  customConfig: CustomConfig;
}
type RequestConfig = {
  searchUrl?: string;
  method?: "GET" | "POST";
  pageNum: number;
  pageSize: number;
  otherSearchField?: any; // 不在搜索项展示的其他请求参数
};
export interface FormProps {
  formOptions: FormOption[];
  // 请求配置
  requestConfig: RequestConfig;
}

export interface header {
  // props是element plus表格列支持的属性
  props: {
    label: string;
    prop: string;
    [key: string]: string;
  };
  // 这是我们自定义的一些属性
  config?: {
    isCustom: boolean; // 是否支持自定义表格项;如果为true,那么我们以props中的prop作为具名插槽的name来进行渲染
  };
}

export interface TableConfigType {
  headers: header[]; // 表格列有哪些
  tableData: any[]; // 表格数据
  paginationConfig: any; // 分页配置
}

SearchForm组件

<template>
  <el-form ref="searchFormRef" :model="searchForm" size="default">
    <el-row :gutter="20">
      <el-col
        :xs="24"
        :sm="24"
        :md="24"
        :lg="12"
        :xl="6"
        v-for="option in formOptions"
        :key="option.field"
      >
        <el-form-item :label="option.props.label">
          <!-- 处理输入框类型 -->
          <template v-if="option.type === FormType.INPUT">
            <el-input
              v-model="searchForm[option.field]"
              v-bind="option.props"
            ></el-input>
          </template>
          <!-- 处理级联选择器 -->
          <template v-if="option.type === FormType.CASCADER">
            <el-cascader
              v-model="searchForm[option.field]"
              v-bind="option.props"
            />
          </template>
          <!-- 处理下拉选框 -->
          <template v-if="option.type === FormType.SELECT">
            <el-select
              v-model="searchForm[option.field]"
              v-bind="option.props"
              :remote-method="(query: string) => remoteMethod(query, option)"
              fit-input-width
            >
              <el-option
                v-for="item in option.customConfig.selectOptions"
                :key="item.value"
                :label="item.label"
                :value="item.value"
              />
            </el-select>
          </template>
          <!-- 处理日期选择器 -->
          <template v-if="option.type === FormType.DATEPICKER">
            <el-date-picker
              v-model="searchForm[option.field]"
              v-bind="option.props"
            />
          </template>
        </el-form-item>
      </el-col>
      <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="6">
        <el-form-item>
          <el-button type="default" size="default" @click="reset"
            >重置</el-button
          >
          <el-button type="primary" size="default" @click="search"
            >搜索</el-button
          >
        </el-form-item>
      </el-col>
    </el-row>
  </el-form>
</template>

<script setup lang="ts">
import { ref, onMounted } from "vue";
import { FormProps, FormOption, FormType } from "./type";
import axios from "axios";
import { FormInstance } from "element-plus";

const props = defineProps<FormProps>();
// emits接口
interface Emits {
  (e: "search", data: any): void;
  (e: "reset"): void;
  (e: "change"): void;
}

// emits
const emits = defineEmits<Emits>();
const searchForm = ref<any>({});
const searchFormRef = ref<FormInstance>();
// 搜索请求,将搜索出来的结果emit出去,供外部使用
const search = async () => {
  if (!props.requestConfig.searchUrl) {
    return false;
  }
  const params: {
    pageNum: number;
    pageSize: number;
    [key: string]: any;
  } = {
    pageNum: props.requestConfig.pageNum,
    pageSize: props.requestConfig.pageSize,
    ...searchForm.value,
    ...props.requestConfig.otherSearchField,
  };
  const { data } = await axios({
    method: props.requestConfig.method,
    url: props.requestConfig.searchUrl,
    data: params,
  });
  // 将搜索到的结果返回给父组件,用于展示
  emits("search", { total: parseInt(data.total), data: data.list });
};

// 重置表单,同时emit出去,方便父组件修改page
const reset = () => {
  // 重置将searchForm初始化,防止中间过程有一些污染数据
  searchForm.value = {};
  searchFormRef.value?.resetFields();
  props.formOptions.map((item) => {
    searchForm.value[item.field] = item.customConfig.defaultValue || "";
  });
  emits("reset");
};

// 自定远程搜索逻辑
async function remoteMethod(query: string, item: FormOption) {
  const {
    customConfig: { url = "", requestType = "GET" },
    $queryFormatter,
    $responseFormatter,
  } = item;

  let params =
    $queryFormatter && typeof $queryFormatter === "function"
      ? $queryFormatter(query)
      : { name: query };

  const data = await axios({
    method: requestType,
    url,
    data: {},
    params,
  });

  item.customConfig.selectOptions =
    $responseFormatter && typeof $responseFormatter === "function"
      ? $responseFormatter(data)
      : (data as unknown as Array<{ label: string; value: string }>);
}

onMounted(() => {
  // 组件挂载时将搜索选项初始化
  props.formOptions.forEach((option: FormOption) => {
    // 如果有传入的默认值就赋值,否则默认给定空字符串
    searchForm[option.field] = option?.customConfig?.defaultValue
      ? option?.customConfig?.defaultValue
      : "";
  });
});

// 暴露出去一些方法供父组件调用 searchForm暴露出去,父组件可直接修改一些属性,方便操作;search方法当父组件的页码改变时方便重新搜索
defineExpose({ reset, search, searchForm });
</script>

SearchTable组件

<template>
  <div class="table">
    <el-table :data="tableData">
      <el-table-column
        v-for="{ props, config } in headers"
        :key="props.prop"
        v-bind="props"
      >
        <template v-slot="{ row }">
          <!-- 处理自定义数据展示:使用具名插槽和作用域插槽,将该行的数据传递出去 -->
          <slot v-if="config?.isCustom" :name="props.prop" :row="row"></slot>
          <!-- 非自定义,默认展示 -->
          <span v-else>{{ row[props.prop] }}</span>
        </template>
      </el-table-column>
    </el-table>
    <el-pagination
      style="justify-content: center; margin-top: 20px"
      small
      background
      layout="prev, pager, next, jumper, total"
      :total="paginationConfig.total"
      @size-change="handlerSizeChange"
      @current-change="handlerCurrentChange"
      @prev-click="handlerCurrentChange"
      @next-click="handlerCurrentChange"
    />
  </div>
</template>

<script setup lang="ts">
import { toRefs } from "vue";
import { header } from "./type";

interface TableConfigType {
  headers: header[]; // 表格列有哪些
  tableData: any[]; // 表格数据
  paginationConfig: any; // 分页配置
}

const props = defineProps<TableConfigType>();
const emits = defineEmits(["size-change", "current-change"]);
const { headers, tableData, paginationConfig } = toRefs(props);

const handlerSizeChange = (size: number) => {
  emits("size-change", size);
};
const handlerCurrentChange = (page: number) => {
  emits("current-change", page);
};
</script>

<style lang="scss" scoped>
.table {
  width: 100%;
}
</style>

搜索组件及表格组件的基本使用

<template>
  <div class="page">
    <div class="page-main">
      <SearchForm
        ref="searchFormRef"
        v-bind="formOptions"
        @reset="reset"
        @search="search"
      ></SearchForm>
      <SearchTable
        v-bind="tableConfigs"
        @size-change="sizeChange"
        @current-change="currentChange"
      >
        <template #sex="{ row }">
          <el-tag v-if="row.sex === 1" type="success">男</el-tag>
          <el-tag v-if="row.sex === 2" type="danger">女</el-tag>
        </template>
        <template #operator="{ row }">
          <el-button type="primary" size="small" @click="edit(row)">
            编辑
          </el-button>
          <el-button type="danger" size="small" @click="del(row)">
            删除
          </el-button>
        </template>
      </SearchTable>
    </div>
  </div>
</template>

<script setup lang="ts" name="home">
import { defineAsyncComponent, ref } from "vue";
import { FormProps, FormType, TableConfigType } from "./type";
const SearchForm = defineAsyncComponent(() => import("./SearchForm.vue"));
const SearchTable = defineAsyncComponent(() => import("./SearchTable.vue"));
const searchFormRef = ref<InstanceType<typeof SearchForm>>();
const formOptions = ref<FormProps>({
  formOptions: [
    {
      type: FormType.INPUT,
      field: "name",
      props: {
        label: "姓名",
        placeholder: "请输入姓名",
      },
      customConfig: {},
    },
    {
      type: FormType.SELECT,
      field: "status",
      props: {
        label: "状态",
        placeholder: "请选择",
      },
      customConfig: {
        selectOptions: [
          {
            label: "全部",
            value: "",
          },
          {
            label: "已通过",
            value: 1,
          },
          {
            label: "未通过",
            value: 0,
          },
        ],
      },
    },
    {
      type: FormType.CASCADER,
      field: "department",
      props: {
        label: "所属部门",
        placeholder: "请选择",
        options: [
          {
            label: "一级部门",
            value: 1,
            children: [
              {
                label: "二级部门",
                value: 2,
              },
            ],
          },
        ],
      },
      customConfig: {},
    },
    {
      type: FormType.DATEPICKER,
      field: "createTime",
      props: {
        label: "创建时间",
        placeholder: "请选择",
      },
      customConfig: {},
    },
  ],
  requestConfig: {
    searchUrl: "",
    method: "POST",
    pageNum: 1,
    pageSize: 10,
  },
});
const tableConfigs = ref<TableConfigType>({
  headers: [
    {
      props: {
        label: "姓名",
        prop: "name",
      },
      config: {
        isCustom: false,
      },
    },
    {
      props: {
        label: "性别",
        prop: "sex",
      },
      config: {
        isCustom: true,
      },
    },
    {
      props: {
        label: "操作",
        prop: "operator",
      },
      config: {
        isCustom: true,
      },
    },
  ],
  tableData: [
    {
      name: "张三",
      sex: 1,
    },
    {
      name: "王五",
      sex: 2,
    },
  ],
  paginationConfig: {
    total: 0,
  },
});

const reset = () => {
  formOptions.value.requestConfig.pageNum = 1;
  searchFormRef.value?.search();
};

const search: (data: any) => void = ({ total, data }) => {
  tableConfigs.value.tableData = data;
  tableConfigs.value.paginationConfig.total = total;
};

const sizeChange = (size: number) => {
  formOptions.value.requestConfig.pageSize = size;
  searchFormRef.value?.search();
};

const currentChange = (page: number) => {
  formOptions.value.requestConfig.pageNum = page;
  searchFormRef.value?.search();
};

const edit = (row: any) => {
  console.log(row);
};

const del = (row: any) => {
  console.log(row);
};
</script>

<style lang="scss" scoped>
.page {
  width: 100%;
  height: 100%;

  &-main {
    width: 100%;
    height: 100%;
  }
}
</style>

思路总结:

1. 将表格或搜索表单需要的操作尽可能封装,做到开箱即用。

2. 如果有定制化的需求,我们提供特定的api给用户自由发挥。

目前使用情况来看,这个组件能够满足我项目中的大部分场景,不过需要跟后端沟通好请求接口的数据格式,保持统一。毕竟我们封装本来的目的就是为了可复用嘛,如果接口变来变去封装也就没有意义了。这算是一个基础版本,根据不同的业务需求,可能组件也需要不断地修改丰富,大家可以根据自己的情况进行优化。

这是自己的一些小经验,大家感觉有不合适的地方欢迎评论区讨论,咱们共同进步。

编码不易点个关注再走咯!!!

  • 8
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值