一、需求描述
系统设计了 销售单、采购单、入库单、出库单、盘点单、移库单、生产计划单等等等等类型的单据,单据都有着 单据编号、单据明细、单据状态 等属性,而且每个单据都有不同的状态,但又都包含了 待审核,已驳回,已完成 等状态,且每个单据都有自己的单据号,如果被驳回审核的话,又都包含了驳回审核的原因等等。每种单据又都有自己的明细单,比如销售单对应的销售明细单等。这些明细单又分别的有数量、已完成数量等。
为了代码复用且便于维护,于是我们将所有单据抽出基类来统一实现公共的一些功能。
二、设计思路
我们需要设计一个数据结构来存储单据信息,包括单据编号、单据明细、单据总数量、单据已完成数量、单据状态等。
-
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. 相关的源代码:
- Github: github.com/s-pms/SPMS-…
- Gitee: gitee.com/s-pms/SPMS-…
2. 使用的基础库:
- Github: github.com/HammCn/AirP…
- Gitee: gitee.com/air-power/A…