用TypeScript和面向对象来盘了单据状态的各种操作

1,470 阅读5分钟

banner.jpg

一、需求描述

系统设计了 销售单、采购单、入库单、出库单、盘点单、移库单、生产计划单等等等等类型的单据,单据都有着 单据编号单据明细单据状态 等属性,而且每个单据都有不同的状态,但又都包含了 待审核已驳回已完成 等状态,且每个单据都有自己的单据号,如果被驳回审核的话,又都包含了驳回审核的原因等等。每种单据又都有自己的明细单,比如销售单对应的销售明细单等。这些明细单又分别的有数量、已完成数量等。

为了代码复用且便于维护,于是我们将所有单据抽出基类来统一实现公共的一些功能。

二、设计思路

我们需要设计一个数据结构来存储单据信息,包括单据编号、单据明细、单据总数量、单据已完成数量、单据状态等。

  • 1. 抽象单据基类

    export abstract class AbstractBaseBillEntity<D extends AbstractBaseBillDetailEntity> extends BaseEntity {
      /**
       * # 单据编号
       * 
       * 所有的单据都会有单据编号的字段
       */
      @Table({
        orderNumber: 99,
        forceShow: true,
      })
      @Form({
        placeholder: '不填写按编码规则自动生成',
      })
      @Field('单据编号') billCode!: string
    
      /**
       * # 单据状态码
       * 
       * 因为单据的状态码都不确定,所以这里定义为抽象属性
       */
      abstract status: number
    
      /**
       * # 单据明细列表
       * 
       * 因为单据明细的具体类型未知,所以这里给定泛型,并且抽象
       * 子类可以按自己的设计去实现,并且标记强制数据转换的类
       */
      abstract details: D[]
    
      /**
       * # 驳回原因
       * 
       * 所有被驳回的单据都需要驳回的原因字段
       */
      @Form({
        textarea: true,
      })
      @Field('驳回原因') rejectReason!: string
    }
    
  • 2. 抽象单据明细基类

    export abstract class AbstractBaseBillDetailEntity extends BaseEntity {
      /**
       * # 单据ID
       */
      billId!: number
    
      /**
       * # 数量
       * 
       * 有的叫采购数量,有的叫出库数量
       */
      abstract quantity: number
    
      /**
       * # 已完成数量
       * 
       * 有的叫已采购数量,有的叫已出库数量
       */
      abstract finishQuantity: number
    }
    
  • 3. 抽象单据操作Service基类

    因为所有的单据都会有审核、驳回、完成等操作,所以这里定义一个抽象类来统一实现这些功能。

      /**
       * # 单据抽象服务基类
       * 完成单据的审核、驳回、完成等操作
       */
      export abstract class AbstractBaseBillService<D extends AbstractBaseBillDetailEntity, B extends AbstractBaseBillEntity<D>> extends AbstractBaseService<B> {
        /**
         * # 审核单据
         * @param bill 单据
         */
        async audit(bill: B): Promise<void> {
          await this.api('audit').post(bill)
        }
      
        /**
         * # 驳回单据
         * @param bill 单据
         */
        async reject(bill: B): Promise<void> {
          await this.api('reject').post(bill)
        }
      
        /**
         * # 完成单据
         * @param bill 单据
         */
        async finish(bill: B): Promise<void> {
          await this.api('finish').post(bill)
        }
      
        /**
         * # 添加完成数量
         * @param bill 单据
         */
        async addFinish(bill: D): Promise<void> {
          await this.api('addFinish').post(bill)
        }
      }
    
    
  • 4. 封装单据的统一表格Hook

    /**
     * # 单据的表格Hook
     * @param entityClass 单据实体类
     * @param serviceClass 单据服务类
     * @param option 配置项
     */
    export function useBillTable<
      D extends AbstractBaseBillDetailEntity,
      B extends AbstractBaseBillEntity<D>,
      S extends AbstractBaseBillService<D, B>
    >(
      entityClass: ClassConstructor<B>,
      serviceClass: ClassConstructor<S>,
      option: IUseTableOption<B> = {},
    ): IUseBillTableResult<D, B, S> {
      const result = useAirTable(entityClass, serviceClass, option)
    
      async function onAudit(bill: B) {
        await AirConfirm.warning(`是否确认审核选择的${result.entity.getModelName()}?`)
        await result.service.audit(bill)
        result.onReloadData()
      }
    
      async function onFinish(bill: B) {
        await AirConfirm.warning(`是否确认手动完成选择的${result.entity.getModelName()}?`)
        await result.service.finish(bill)
        result.onReloadData()
      }
    
      async function onReject(bill: B) {
        const rejectReason: string = await AirDialog.show(BillRejectDialog, `驳回${result.entity.getModelName()}的原因`)
        await AirConfirm.warning(`是否确认驳回选择的${result.entity.getModelName()}?`)
        bill.rejectReason = rejectReason
        await result.service.reject(bill)
        result.onReloadData()
      }
    
      return {
        onFinish,
        onAudit,
        onReject,
        ...result,
      } as IUseBillTableResult<D, B, S>
    }
    
    
    
    /**
     * # 单据表格结构体声明
     * @param D 单据明细类型
     * @param B 单据类型
     * @param S 单据服务类型
     */
    export interface IUseBillTableResult<
      D extends AbstractBaseBillDetailEntity,
      B extends AbstractBaseBillEntity<D>,
      S extends AbstractBaseBillService<D, B>
    > extends IUseTableResult<B, S> {
      /**
       * # 审核
       * @param bill 单据
       */
      onAudit: (bill: B) => void
    
      /**
       * # 驳回
       * @param bill 单据
       */
      onReject: (bill: B) => void
    
      /**
       * # 完成单据
       * @param bill
       */
      onFinish: (bill: B) => void
    }
    
    
  • 5. 部分子类的实现

    接下来,我们就可以按照具体的单据类型去实现子类了,比如下面的采购单:

    • 1). 采购单实体类

      @Model('采购单')
      export class PurchaseEntity extends AbstractBaseBillEntity<PurchaseDetailEntity> {
        @Field('采购单号') declare billCode: string
      
        @Table({
          nowrap: true,
        })
        @Form({
          textarea: true,
          maxLength: 80,
          requiredString: true,
        })
        @Field('采购事由') reason!: string
      
        @Table({
          width: 150,
          suffixText: '元',
          align: 'right',
          forceShow: true,
        })
        @Form({
          suffixText: '元',
        })
        @Field('总金额') totalPrice!: number
      
        @Table({
          width: 150,
          suffixText: '元',
          align: 'right',
          forceShow: true,
        })
        @Form({
          suffixText: '元',
        })
        @Field('实际金额') totalRealPrice!: number
      
        @Table({
          width: 100,
          showColor: true,
          orderNumber: -80,
          forceShow: true,
        })
        @Dictionary(PurchaseStatusDictionary)
        @Search()
        @Field('采购状态') status!: PurchaseStatus
      
        @Field('采购明细')
        @Type(PurchaseDetailEntity, true) details: PurchaseDetailEntity[] = []
      }
      
    • 2). 采购单明细实体类

      @Model('采购明细')
      export class PurchaseDetailEntity extends AbstractBaseBillDetailEntity {
        /**
         * # 物料
        */
        @Form({
          requiredNumber: true,
        })
        @Type(MaterialEntity) material!: MaterialEntity
      
        /**
         * # 供应商
        */
        @Form({
          requiredNumber: true,
        })
        @Type(SupplierEntity) supplier!: SupplierEntity
      
        @Field('采购单价')
        @Form({
          requiredNumber: true,
          number: true,
        })
        @Table({
          width: 150,
          suffixText: '元',
          align: 'right',
          orderNumber: -1,
        })
        @Type(Number) price!: number
      
        @Field('采购数量')
        @Form({
          requiredNumber: true,
          number: true,
        })
        @Table({
          align: 'right',
          width: 150,
          orderNumber: -2,
        })
        @Type(Number) quantity!: number
      
        @Field('已采购数量')
        @Table({
          align: 'right',
          width: 150,
          orderNumber: -3,
        })
        @Type(Number) finishQuantity!: number
      }
      
    • 3). 采购单状态枚举和字典

      这里需要按照后端给定的状态码去定义好枚举:

      /**
       * # 采购单状态枚举
       */
      export enum PurchaseStatus {
        /**
         * # 审核中
        */
        AUDITING = 1,
      
        /**
         * # 已驳回
        */
        REJECTED = 2,
      
        /**
         * # 采购中
         */
        PURCHASING = 3,
      
        /**
         * # 已完成
         */
        DONE = 4,
      
        /**
         * # 已入库
         */
        FINISHED = 5,
      }
      

      还需要将声明的状态枚举搭配一个字典,用来翻译和显示不同的状态。

      /**
       * # 采购单状态枚举字典
       */
      export const PurchaseStatusDictionary = AirDictionaryArray.create([
        { key: PurchaseStatus.AUDITING, color: AirColor.WARNING, label: '审核中' },
        { key: PurchaseStatus.REJECTED, color: AirColor.DANGER, label: '已驳回' },
        { key: PurchaseStatus.PURCHASING, color: AirColor.SUCCESS, label: '采购中' },
        { key: PurchaseStatus.DONE, color: AirColor.NORMAL, label: '已完成' },
        { key: PurchaseStatus.FINISHED, color: AirColor.NORMAL, label: '已入库' },
      ])
      
      

三、使用方式

  • 1. 列表页面 list.vue

    <template>
      <APanel>
        <AToolBar
          :loading="isLoading"
          :entity="PurchaseEntity"
          :service="PurchaseService"
          show-filter
          @on-add="onAdd"
          @on-search="onSearch"
        />
        <ATable
          v-loading="isLoading"
          :data-list="response.list"
          :entity="PurchaseEntity"
          :select-list="selectList"
          :disable-edit="(row: PurchaseEntity) => row.status !== PurchaseStatus.REJECTED"
          hide-delete
          show-detail
          :ctrl-width="160"
          @on-detail="onDetail"
          @on-edit="onEdit"
          @on-sort-change="onSortChanged"
          @on-select="onSelected"
        >
          <template #customRow="row">
            <AButton
              link-button
              tooltip="审核"
              type="CONFIRM"
              :disabled="(row.data as PurchaseEntity).status !== PurchaseStatus.AUDITING"
              @click="onAudit(row.data)"
            >
              审核
            </AButton>
            <AButton
              link-button
              tooltip="驳回"
              type="LOCK"
              :disabled="(row.data as PurchaseEntity).status !== PurchaseStatus.AUDITING"
              @click="onReject(row.data)"
            >
              驳回
            </AButton>
          </template>
        </ATable>
        <template #footerLeft>
          <APage
            :response="response"
            @on-change="onPageChanged"
          />
        </template>
      </APanel>
    </template>
    
    <script lang="ts" setup>
    // import codes here
    const {
      isLoading,
      response,
      selectList,
      onSearch,
      onAdd,
      onEdit,
      onPageChanged,
      onSortChanged,
      onSelected,
      onDetail,
      onReject,
      onAudit,
    } = useBillTable(PurchaseEntity, PurchaseService, {
      editView: PurchaseEditor,
      detailView: PurchaseDetail,
    })
    </script>
    
  • 2. 新增、修改页面 edit.vue

    <template>
      <ADialog
        :title="title + PurchaseEntity.getModelName()"
        :form-ref="formRef"
        :loading="isLoading"
        width="80%"
        height="80%"
        @on-confirm="onSubmit()"
        @on-cancel="onCancel()"
      >
        <el-form
          ref="formRef"
          :model="formData"
          label-width="120px"
          :rules="rules"
          @submit.prevent
        >
          <AGroup
            title="采购单"
            :column="2"
          >
            <el-form-item
              :label="PurchaseEntity.getFieldName('billCode')"
              prop="billCode"
            >
              <AInput
                v-model.billCode="formData.billCode"
                :entity="PurchaseEntity"
              />
            </el-form-item>
            <el-form-item
              style="width: 100%;"
              :label="PurchaseEntity.getFieldName('reason')"
              prop="reason"
            >
              <AInput
                v-model.reason="formData.reason"
                :entity="PurchaseEntity"
              />
            </el-form-item>
          </AGroup>
          <AGroup title="采购明细">
            <ATable
              :entity="PurchaseDetailEntity"
              :data-list="formData.details"
              :field-list="PurchaseDetailEntity.getTableFieldConfigList().filter(item => !['createTime'].includes(item.key))"
              hide-edit
              hide-delete
            >
              <template #addButton>
                <AButton
                  type="ADD"
                  @click="addDetail()"
                >
                  添加{{ PurchaseEntity.getFieldName('details') }}
                </AButton>
              </template>
              <template #customRow="row">
                <AButton
                  type="DELETE"
                  danger
                  icon-button
                  @click="deleteDetail(row.index)"
                />
              </template>
            </ATable>
          </AGroup>
        </el-form>
      </ADialog>
    </template>
    
    <script lang="ts" setup>
    // import codes here...
    const props = defineProps(airPropsParam(new PurchaseEntity()))
    
    const {
      title, formData, rules, formRef, isLoading,
      onSubmit,
    } = useAirEditor(props, PurchaseEntity, PurchaseService, {
      afterGetDetail(detailData) {
        return detailData
      },
      beforeSubmit(submitData) {
        if (submitData.details.length === 0) {
          AirNotification.warning('请添加明细后再提交')
          return null
        }
        return submitData
      },
    })
    
    async function addDetail() {
      const detail: PurchaseDetailEntity = await AirDialog.show(PurchaseDetailEditor)
      formData.value.details.push(detail)
    }
    
    async function deleteDetail(index: number) {
      await AirConfirm.warning('是否删除选中行的采购明细?')
      formData.value.details.splice(index, 1)
    }
    </script>
    

四、总结

按上述的设计,我们实现了一个简单的采购管理模块,包括采购单的列表、新增、修改、审核等功能。

来吧,你给上述的封装思路和代码打几分?在评论区留下你的看法吧~

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

1. 相关的源代码:

2. 使用的基础库: