基于vue2.7的element-ui后台封装,包含form组件、table组件、搜索和列表的综合应用

809 阅读4分钟

我正在参加「掘金·启航计划」

本文内容

项目封装了Form组件、Table组件、Search组件,useTable hooks,能够抽象出最简单的后台搜索+列表模式,基本上只需要传入搜索json、列表api、column。组件只封装了最基本的功能但是,可以基于此扩展。参考了element-plus-admin的代码,但是element-plus是基于vue3+ts+element-plus的,而我的项目是基于vue2.7+element-ui.

阅读建议

建议直接克隆代码运行,因为代码比较直接,如果有问题再回头看下面说明。本文代码在github.com/chengfengfe…

  1. src/views/Page.vue 是综合实例,建议从这看起
  2. src/components是各个组件定义

关于代码请求

用mockjs拦截了ajax请求,所以network是看不到的,但是可以在控制台console查看数据请求和返回情况。

封装Form 组件

因为jsx-vue2在vue2.7上有vModel的bug,所以组件没有使用setup来写jsx, bug链接,而是传统vue2的render选项

整体思路

通过一个json生成表单,json里描述了表单所具有的label,field等,并且能够拿到表单的值。我们用schema prop来接收这个json,然后遍历这个json生存表单

<el-form>
  {this.schema.map((item) => (
    {
      if (type === 'input') {
        return (
          <el-input
            vModell={this.formModel[field]}
            placeholder="请输入"
          />
        );
      } else if (type === 'select') {
        return (
          <el-select
            vModell={this.formModel[field]}
          >
      ...
    }
  ))}
</el-form>

数据流

一开始是想让使用Form的组件可以传一个value作为初始值,然后让Form组件更新了自己的值后,再同步回这个value,就是vue的.sync模式

<Form :value.sync="formValue"></Form>

但是考虑到后续还有分组form,而且.sync的维护成本太高,而且也没必要,使用者如果想要获取Form的值,可以通过ref获取组件实例来获取Form值。这样避免了很多问题,开发起来比较简单。

const getFormValue = () => {
  console.log(formRef.value?.formModel);
};

初始值

因为组件内部自身有值,而且要响应用户操作,而且element-ui还是用v-model方便,因此会有这样的写法, 如果formModel初始值为{}时,会有this.formModel[field]是undefined情况,因此有必要根据schema进行formModel初始化

if (type === 'input') {
  return (
    <el-input
      vModell={this.formModel[field]}
      placeholder="请输入"
      {...spreadProps}
    />
  );
} else if (type === 'select') {
  return (
    <el-select
      vModell={this.formModel[field]}
      placeholder="请选择"
      {...spreadProps}
    >

初始化

initValue() {
  for (const { type, field } of this.schema) {
    if (typeof this.formModel[field] === 'undefined') {
      this.$set(this.formModel, field, '');
    }
  }
}

属性透传

不管是组件还是包裹组件等FormItem,在element-ui都可以有很多属性,比如FormItem组件有label-width, select组件有multiple, clearable, filterable等。最灵活的解决办法是组件本身可以根据自己的常用习惯定义一些默认属性,然后通过在外部json上定义一个对象,在组件里展开这个对象覆盖组件默认属性。

const defaultFormProps = {
  size: 'small',
  inline: true
};
...
<el-form {...{props: {...defaultFormProps, ...this.formProps}}}>
  {this.schema.map((item) => (
    renderItem(item)
  ))}
</el-form>

select组件下拉列表异步获取

select的下拉列表一般我们都是从后端接口获取的,vue2.7里面有了ref,我们可以给selectOption传递一个ref<any[]>, 这样从后端获取列表后,这个下拉框也能正常渲染

const hobbys = ref<any[]>([]);
const schema: FormSchema = [
  {
    label: "爱好",
    type: "select",
    field: "hobby",
    selecTions: hobbys,
    componentProps: {
      placeholder: "qwer",
    },
  },
  ...
const getHobbys = () => {
  setTimeout(() => {
    hobbys.value = [      {        code: "1",        value: "唱",      },      {        code: "2",        value: "跳",      },      {        code: "3",        value: "Rap",      },    ];    
  }, 1000);
};

结合Search组件和Table组件

一般来说我们会写出这样的代码:

  1. 定义请求数据方法,处理参数传入,分页、loading
  2. 监听分页变化,再请求数据
// 定义请求数据方法,处理参数传入,分页、loading
const getData = async (isSearch = 0) => {
  if (isSearch) {
    tableOptions.page = 1;
  }
  tableOptions.loading = true;
  const params = {
    params: {
      ...searParam.value,
      page: tableOptions.page,
      count: tableOptions.count,
    },
  };
  console.log(chalk.bgYellowBright("request params>>>>>: "), params);
  const res = await axios
    .get("hello.json", params)
    .finally(() => (tableOptions.loading = false));
  console.log(chalk.bgYellowBright("response>>>>>: "), res);
  tableOptions.total = res.data.total;
  tableOptions.data = res.data.list;
};
// 监听分页变化,再请求数据
watch(
  () => tableOptions.count,
  () => {
    getData();
  }
);
watch(
  () => tableOptions.page,
  () => {
    getData();
  }
);

再来一个同样的search和table,也是类似。但是抽象来看,虽然很多业务都是search+table,但是对于前端来说核心的只是请求数据api和search参数、返回字段。模式都是一样的。因此,我们可以把公共的模式抽象成hooks。 这个hooks只接收

// 接收api地址,table columns, 搜索组件实例作为参数,返回响应式的tableOption(包括loading、data)搜索方法
// 相当于把各个业务公共的部分抽离出来,如loading、page处理,只留各业务最核心的各自不同的api方法给外面,作为参数传进来
import { watch, reactive, onMounted } from 'vue';
import get from 'lodash.get';
import axios from "axios";
import chalk from "chalk";

export default function (config) {
  const tableOption = reactive({
    loading: false,
    data: [],
    columns: config.columns,
    page: 1,
    count: 10,
    total: 0
  });
  const methods = {
    getList: async (isSearch = 0) => {
      if (isSearch) {
        tableOption.page = 1;
      }
      const searchParam = config.searchCom?.value.getSearchFormValue();
      const params = {
        ...(config.modifyParam ? config.modifyParam(searchParam) : searchParam),
        ...{ page: tableOption.page, count: tableOption.count }
      };
      tableOption.loading = true;
      console.log(chalk.bgYellowBright("request params>>>>>: "), params);
      const res = await axios.get(config.api, {params}).finally(() => {
        tableOption.loading = false;
      });
      console.log(chalk.bgYellowBright("response>>>>>: "), res);
      const resData = res.data;
      tableOption.data = get(resData, config.listPath || 'list');
      tableOption.total = get(resData, config.totalPath || 'total');
    },
    handleSearch() {
      if (tableOption.page === 1) {
        methods.getList();
      } else {
        // 这里不需要methods.getList(),因为watch page那里处理过了,否则会调用两次接口
        tableOption.page = 1;
      }
    }
  };
  onMounted(() => {
    methods.getList(1);
  });
  watch(
    () => tableOption.page,
    () => {
      methods.getList();
    }
  );
  return {
    methods,
    tableOption
  };
}

具体看github代码