在金融风控系统(下文简称Nova系统)的开发中,经常会遇到包含数百个字段的复杂表单页面。这些表单不仅字段类型多样,还存在复杂的联动关系和校验规则,传统的硬编码方式不仅开发效率低下,还会导致代码冗余难以维护。本文将基于Nova系统的真实需求分享公共表单组件封装方案,通过配置化思路解决复杂表单的开发难题。
项目背景与痛点分析
Nova系统基于 Angular
框架开发,采用 ng-zorro
组件库构建 UI 界面。系统中存在大量包含数百个字段的表单,这些表单具有以下特点:
-
字段类型多样:包含 input、日期选择框、select、单选框、多选框等基础组件,还有 input group 组合组件及自定义封装的业务组件;
-
事件需求复杂 :需要支持
ngModelChange
、blur
等基础事件,部分组件还需特殊事件如nzOpenChange
; -
校验规则严格:每个字段都有特定的校验规则,需实时提示错误信息;
-
联动关系紧密:字段间存在复杂的依赖关系,如 A 字段的状态由 B 字段的值决定,多个字段值相互计算等;
-
动态性要求高 :存在可动态增减的列表项
FormArray
,部分字段需根据外部变量动态变化; -
附加功能多:部分字段需要详情查看按钮,支持跳转或弹窗展示;
如果按ng-zorro
逐个编写表单控件的方式会导致代码量激增,维护成本高,且难以保证表单行为的一致性。因此,需要设计一套可配置、可复用的表单组件方案。
核心实现思路
解决方案是封装一个公共表单组件,通过配置数组动态生成表单项。核心思路包括:
- 定义统一的表单项配置接口,涵盖各类组件的属性和行为
- 通过遍历配置数组,动态生成对应的表单控件
- 集中处理表单验证、事件响应和字段联动逻辑
- 支持自定义模板和扩展功能,满足特殊业务需求
这种配置化组件可以将表单的结构与逻辑分离,大幅减少重复代码,提高开发效率。
表单项配置设计
表单项配置是整个方案的核心,配置接口包含以下关键属性:
js
interface FormItemConfig {
// 绑定的表单控件名称
formName: string;
// 表单项类型
type: 'input' | 'select' | 'datepicker' | 'radio' | 'checkbox' | 'number' | 'select-tree' | 'custom';
// 标签文本
label?: string;
// 是否隐藏标签
hideLabel?: boolean;
// 是否必填
required?: boolean | ((formValue: any) => boolean);
// 禁用状态
disabled?: boolean | ((formValue: any, externalData: any) => boolean);
// 隐藏状态
hidden?: boolean | ((formValue: any, externalData: any) => boolean);
// 文本颜色
textColor?: string | ((formValue: any) => string);
// 校验规则
validators?: ValidatorFn | ValidatorFn[];
[key: string]: any;
}
这个配置接口的设计遵循了 "最小知识原则",每个配置项只包含该类型组件必要的信息,同时通过扩展属性支持特殊需求。特别值得注意的是:
- 使用函数类型支持动态计算 required、disabled 等状态,实现字段间的联动
- 通过 events 属性统一管理各类事件,保持接口一致性
- 针对 FormArray 类型单独设计了嵌套配置,支持动态增减项
- 提供 customTemplate 选项,允许在配置化框架中嵌入自定义内容
表单组件实现要点
1. 动态生成表单控件
在组件内部,根据配置数组构建 FormGroup:
js
ngOnInit() {
this.formGroup = new FormGroup({});
// 遍历配置数组,为每个表单项创建FormControl
this.config.forEach(item => {
const validators = this.buildValidators(item);
if (item.type === 'formarray') {
this.formGroup.addControl(
item.formName,
new FormArray([this.createFormGroup(item.formArrayConfig.itemConfig)], validators)
);
} else {
this.formGroup.addControl(
item.formName,
new FormControl('', validators)
);
}
});
// 监听表单值变化,处理联动逻辑
this.formGroup.valueChanges.subscribe(values => {
this.handleFormChanges(values);
});
}
在模板中,使用 ngSwitch 根据不同的 type 渲染对应的组件:
html
<div *ngFor="let item of config" class="form-item">
<nz-form-label [nzSpan]="item.labelSpan || 6" *ngIf="!item.hideLabel">
{{ item.label }}<span *ngIf="isRequired(item)">*</span>
</nz-form-label>
<nz-form-control [nzSpan]="item.controlSpan || 14">
<ng-container [ngSwitch]="item.type">
<!-- Input 组件 -->
<input nz-input
*ngSwitchCase="'input'"
[(ngModel)]="formGroup.get(item.formName).value"
(ngModelChange)="handleEvent(item, 'ngModelChange', $event)"
(blur)="handleEvent(item, 'blur', $event)"
[disabled]="isDisabled(item)">
<!-- Select 组件 -->
<nz-select *ngSwitchCase="'select'"
[(ngModel)]="formGroup.get(item.formName).value"
(ngModelChange)="handleEvent(item, 'ngModelChange', $event)"
(nzOpenChange)="handleEvent(item, 'nzOpenChange', $event)"
[disabled]="isDisabled(item)">
<nz-option *ngFor="let opt of item.options" [nzLabel]="opt.label" [nzValue]="opt.value"></nz-option>
</nz-select>
<!-- 其他类型组件 -->
<!-- ... -->
<!-- 自定义模板 -->
<ng-container *ngSwitchCase="'custom'">
<ng-container *ngTemplateOutlet="item.customTemplate; context: { $implicit: item, form: formGroup }"></ng-container>
</ng-container>
</ng-container>
<!-- 错误提示 -->
<nz-form-explain *ngIf="formGroup.get(item.formName).dirty && formGroup.get(item.formName).invalid">
{{ getErrorMessage(item, formGroup.get(item.formName)) }}
</nz-form-explain>
<!-- 详情按钮 -->
<button *ngIf="item.detail?.visible" nz-button nzType="text" (click)="openDetail(item)">
详情
</button>
</nz-form-control>
</div>
2. 表单验证实现
通过配置项中的 validators 和 errorMessages 实现灵活的表单验证:
js
buildValidators(item: FormItemConfig): ValidatorFn[] {
const validators: ValidatorFn[] = [];
// 处理必填验证
if (item.required) {
validators.push(Validators.required);
}
// 处理自定义验证规则
if (item.validator) {
if (Array.isArray(item.validator)) {
validators.push(...item.validator);
} else {
validators.push(item.validator);
}
}
return validators;
}
// 获取错误提示信息
getErrorMessage(item: FormItemConfig, control: AbstractControl): string {
if (control.hasError('required')) {
return `${item.label || ''}为必填项`;
}
// 处理自定义错误信息
for (const key in control.errors) {
if (item.errorMessages && item.errorMessages[key]) {
return item.errorMessages[key];
}
}
return '';
}
3. 事件处理机制
为了统一处理各类事件,实现一个事件分发器:
js
handleEvent(item: FormItemConfig, eventName: string, event: any) {
// 执行配置中定义的事件处理函数
if (item.events && item.events[eventName]) {
item.events[eventName](event, this.formGroup);
}
// 触发组件输出的统一事件
this.formEvent.emit({
formName: item.formName,
eventName,
event,
formGroup: this.formGroup
});
}
这种机制既支持在配置中定义事件处理逻辑,也允许父组件通过输出事件统一处理,兼顾了灵活性和统一性。
4. 字段联动处理
字段联动是金融表单的核心需求,通过监听表单值变化实现这一功能:
js
handleFormChanges(values: any) {
this.config.forEach(item => {
// 处理必填状态联动
if (typeof item.required === 'function') {
const isRequired = item.required(values);
this.updateValidator(item.formName, 'required', isRequired);
}
// 处理禁用状态联动
if (typeof item.disabled === 'function') {
const isDisabled = item.disabled(values, this.externalData);
const control = this.formGroup.get(item.formName);
if (isDisabled && !control.disabled) {
control.disable();
} else if (!isDisabled && control.disabled) {
control.enable();
}
}
// 处理其他联动逻辑
if (item) {
// 执行配置的联动函数
it(values, this.formGroup);
}
});
// 执行全局联动计算
this.calculateFields(values);
}
// 处理值计算类联动(如A = B + C + D)
calculateFields(values: any) {
// 遍历所有需要计算的字段配置
this.config.filter(item => item.calculator).forEach(item => {
const calculator = item.calculator;
const result = calculator(values);
this.formGroup.get(item.formName).setValue(result, { emitEvent: false });
});
}
对于复杂的联动场景(如 fx swap 交易详情中的多字段计算),支持配置专门的 calculator 函数,实现自定义计算逻辑。
5. FormArray 支持
对于需要动态增减的列表项,通过 FormArray 实现:
js
// 添加新项
addFormArrayItem(formName: string) {
const formArray = this.formGroup.get(formName) as FormArray;
const config = this.config.find(item => item.formName === formName);
if (config && config.formArrayConfig) {
formArray.push(this.createFormGroup(config.formArrayConfig.itemConfig));
}
}
// 删除指定项
removeFormArrayItem(formName: string, index: number) {
const formArray = this.formGroup.get(formName) as FormArray;
formArray.removeAt(index);
}
// 创建FormArray中的表单组
createFormGroup(itemConfigs: FormItemConfig[]): FormGroup {
const group = new FormGroup({});
itemConfigs.forEach(item => {
const validators = this.buildValidators(item);
group.addControl(item.formName, new FormControl('', validators));
});
return group;
}
在模板中,为 FormArray 类型提供了新增和删除按钮:
html
<!-- FormArray 项渲染 -->
<div *ngSwitchCase="'formarray'">
<div *ngFor="let group of formGroup.get(item.formName).controls; let i = index" class="formarray-item">
<div *ngFor="let subItem of item.formArrayConfig.itemConfig">
<!-- 渲染子表单项 -->
<form-item [config]="[subItem]" [formGroup]="group"></form-item>
</div>
<button nz-button nzType="text" (click)="removeFormArrayItem(item.formName, i)">删除</button>
</div>
<button nz-button nzType="dashed" (click)="addFormArrayItem(item.formName)">
<i nz-icon nzType="plus"></i> 新增
</button>
</div>
使用示例
使用封装好的表单组件只需定义配置数组并传入:
html
<app-common-form
[config]="formConfig"
[externalData]="externalData"
(formEvent)="handleFormEvent($event)"
(formSubmit)="handleSubmit($event)">
<ng-template #customTemplate let-item let-form="formGroup">
<!-- 自定义内容 -->
<div class="custom-component">
{{ form.get(item.formName).value | somePipe }}
</div>
</ng-template>
</app-common-form>
对应的配置数组示例:
js
formConfig: FormItemConfig[] = [
{
formName: 'amount',
type: 'number',
label: '交易金额',
required: true,
validator: [Validators.min(0)],
errorMessages: { min: '交易金额不能为负数' },
events: {
ngModelChange: (value, form) => {
// 金额变化时的处理逻辑
}
}
},
{
formName: 'currency',
type: 'select',
label: '币种',
required: (values) => values.amount > 0, // 金额大于0时才必填
options: [
{ label: '人民币', value: 'CNY' },
{ label: '美元', value: 'USD' }
]
},
{
formName: 'term',
type: 'select',
label: '期限',
disabled: (values, external) => external.isDisabled, // 由外部变量控制禁用状态
options: (values) => {
// 根据币种动态生成期限选项
if (values.currency === 'CNY') {
return [{ label: '1个月', value: 1 }, { label: '3个月', value: 3 }];
} else {
return [{ label: '1个月', value: 1 }, { label: '6个月', value: 6 }];
}
},
calculator: (values) => {
// 根据其他字段计算默认值
return values.currency === 'CNY' ? 3 : 6;
}
},
// 更多表单项配置...
];
总结
通过这套表单组件封装方案,成功解决了Nova系统中复杂表单的开发难题,主要收益包括:
- 开发效率提升:新表单开发时间减少 60% 以上;
- 代码质量改善:统一的表单行为和验证逻辑,减少了重复代码;
- 维护成本降低:集中管理表单逻辑,修改一处即可影响所有相关表单;
- 扩展性增强:支持自定义组件和逻辑,满足特殊业务需求。