likes
comments
collection
share

先别急着喷,看看我们如何用装饰器替代JSON配置项封装表单

作者站长头像
站长
· 阅读数 56

先别急着喷,看看我们如何用装饰器替代JSON配置项封装表单

一、写在前面

本文使用 Vue3 + Vite + TypeScript + Element-Plus 作为主要的环境和前提,不讨论其他状况下的问题。

在常见的后台管理系统中,前端往往需要大量的表格来显示数据,而表单的封装也经常需要重复的代码,因此,我们希望有一个简单的封装,使得我们能够快速地使用。

二、其他人的常规实现

  1. 封装个 Table 组件,要求传入需要显示的数据数组,以及需要显示的列的配置项 JSON 数组,根据不同的配置项来展示不同形式的列;

  2. 调用时声明一堆 JSON 来配置每个列的参数,高级一些的声明了一个 interface 或者 type 来定义表格列支持的配置列参数;

  3. 声明一个 interface 或者 type 来作为显示数据的属性,如 User

  4. 大功告成。

嗯,看起来很棒的封装,先点个赞了再来吐槽问题。

三、我们不太一样的封装

1)封装原则

  • 相同的数据或配置,全局只存在一份。
  • 不允许使用 interface type 作为显示数据的类型定义方式
  • 不允许使用 JSON 作为配置项的声明方式

2)封装思路

  • 1. 定义数据结构类

首先,我们需要定义一些标准化的基类,如 Entity,Service等;

class BaseEntity{
  id!: number

  createTime!: number
}

class AbstractBaseService<E extends BaseEntity> {
  abstract baseUrl: string

  getDetail(id: number): Promise<E>{
    // 查询详情
  }

  getList(): Promise<E[]>{
    // 查询列表
  }

  update(entity: E): Promise<E>{
    // 更新
  }

  delete(id: number): Promise<E>{
    // 删除
  }
}

接下来,我们定义用户的实体和Service

class User extends BaseEntity{
  name!: string

  age!: number
}

class UserService extends BaseService<UserEntity>{
  // ...
  baseUrl = "user"
}
  • 2. 声明装饰器配置

接下来,我们需要定义一些表格可以使用的配置项

class ITableConfig{
  key!: string

  label!: string

  width?: string

  align?: "left"|"center"|"right"

  // 更多配置
}
  • 3. 定义装饰器

接下来我们需要定义一个装饰器,用来在类上配置一些参数:

function Table(config: ITableFieldConfig = {}): Function {
  return (target: any, key: string) => {
    config.key = key
    return AirDecorator.setFieldConfig(target, key, FIELD_CONFIG_KEY, config, FIELD_LIST_KEY)
  }
}
  • 4. 封装Table组件

<template>
  <el-table :data="data">
    <el-table-column v-for="item in fields" :key="item.key">
      <!--解析 item 配置项 渲染不同的表格列 这里大同小异 -->
    </el-table-column>
  </el-table>
<template>
<script lang="ts" setup generic="E extends BaseEntity">

const props = defineProps({

  /**
     * # 表格显示的数据数组
     */
  dataList: {
    type: Array<E>,
    required: true,
  },

  /**
     * # 表格绑定的数据实体
     */
  entity: {
    type: Function as unknown as PropType<ClassConstructor<E>>,
    required: true,
  },
})

const fields = computed(()=>{
  // 查询装饰器上配置的表格列配置列表
  return AirDecorator.getFieldList(props.entity)
})

// 一些常规的事件抛出
const emits = 
defineEmits(["add","delete","detail","edit"])
</script>


上面只演示了一些简单的需求实现,当然我们实际的封装过程中提供了大量的参数和配置可供使用。

  • 5. Hooks封装

使用过程中,我们还是需要去传入大量的配置和参数,为了简化操作,我们继续封装点 Hooks

export function useAirTable<E extends BaseEntity, S extends AbstractBaseService<E>>(entityClass: ClassConstructor<E>, serviceClass: ClassConstructor<S>, option: IUseTableOption<E> = {}): IUseTableResult<E, S> {
  /**
   * # 表格Hook返回对象
   */
  const result = airTableHook(entityClass, serviceClass, option)

  /**
   * # 表格行编辑事件
   * @param row 行数据
   */
  async function onEdit(row: E) {
  }

  /**
   * # 表格行删除事件
   * @param row 行数据
   */
  async function onDelete(row: E) {
  }

  /**
   * # 表格行禁用事件
   * @param row 行数据
   */
  async function onDisable(row: E) {
  }

  /**
   * # 表格行启用事件
   * @param row 行数据
   */
  async function onEnable(row: E) {
  }

  return Object.assign(result, {
    onEdit,
    onDelete,
    onDisable,
    onEnable,
  }) as IUseTableResult<E, S>
}


四、我们真实封装的使用代码示例

1. 实体定义

@Model('供应商')
export class SupplierEntity extends BaseEntity {
  /**
   * # 供应商编码
   */
  @Table({
    copyField: true,
    forceShow: true,
    orderNumber: 99,
  })
  @Form({
    orderNumber: 99,
    requiredString: true,
  })
  @Field('供应商编码') code!: string

  /**
   * # 供应商名称
   */
  @Table({
    forceShow: true,
  })
  @Form({
    orderNumber: 98,
    requiredString: true,
  })
  @Field('供应商名称') name!: string

  /**
   * # 联系电话
   */
  @Table()
  @Form({
    mobilePhone: true,
  })
  @Field('联系电话') phone!: string
}

2. Service定义

export class SupplierService extends AbstractBaseService<SupplierEntity> {
  entityClass = SupplierEntity

  baseUrl = 'supplier'
}

3. List.vue

<template>
  <APanel>
    <AToolBar
      :loading="isLoading"
      :entity="SupplierEntity"
      :service="SupplierService"
      @on-add="onAdd"
      @on-search="onSearch"
    >
      <template #afterButton>
        <AButton
          v-if="selectList.length > 0"
          type="DELETE_LIST"
          danger
          @click="AirNotification.warning('就是玩'); selectList = []"
        >
          批量删除
        </AButton>
      </template>
    </AToolBar>
    <ATable
      v-loading="isLoading"
      :data-list="response.list"
      :entity="SupplierEntity"
      :select-list="selectList"
      show-select
      :ctrl-width="90"
      @on-edit="onEdit"
      @on-delete="onDelete"
      @on-sort-change="onSortChanged"
      @on-select="onSelected"
    />
    <template #footerLeft>
      <APage
        :response="response"
        @on-change="onPageChanged"
      />
    </template>
  </APanel>
</template>

<script lang="ts" setup>
const {
  isLoading,
  response,
  selectList,
  onSearch, onAdd, onDelete, onEdit, onPageChanged, onSortChanged, onSelected,
} = useAirTable(SupplierEntity, SupplierService, {
  editView: SupplierEditor,
})
</script>
<style scoped lang="scss"></style>

五、总结

通过上述的封装,我们可以非常方便的使用表格组件,而且关于相同的数据配置都集中在了对应的实体类的装饰器上,比如与用户相关的表格、表单、验证器、搜索条件等等,非常方便。

基于装饰器的更多文章,可以阅读我的专栏 “用TypeScript写前端”。

相关的源代码可以参考:

Github: github.com/HammCn/AirP…

Gitee: gitee.com/air-power/A…

转载自:https://juejin.cn/post/7370170468781506575
评论
请登录