cl-form 支持动态字段验证,如:contacts[0].phone

This commit is contained in:
icssoa
2025-08-15 09:13:04 +08:00
parent 23a029a84e
commit ee75abf205
6 changed files with 227 additions and 55 deletions

View File

@@ -15,7 +15,7 @@
<script setup lang="ts">
import { computed, getCurrentInstance, nextTick, ref, watch, type PropType } from "vue";
import { isEmpty, isNull, isString, parsePt, parseToObject } from "@/cool";
import { get, isEmpty, isNull, isString, parsePt, parseToObject } from "@/cool";
import type { ClFormLabelPosition, ClFormRule, ClFormValidateError } from "../../types";
import { $t, t } from "@/locale";
import { usePage } from "../../hooks";
@@ -140,6 +140,105 @@ function getError(prop: string): string {
return "";
}
// 获得错误信息,并滚动到第一个错误位置
async function getErrors(): Promise<ClFormValidateError[]> {
return new Promise((resolve) => {
// 错误信息
const errs = [] as ClFormValidateError[];
// 错误信息位置
const tops = new Map<string, number>();
// 完成回调,将错误信息添加到数组中
function done() {
tops.forEach((top, prop) => {
errs.push({
field: prop,
message: getError(prop)
});
});
// 滚动到第一个错误位置
if (props.scrollToError && errs.length > 0) {
page.scrollTo((tops.get(errs[0].field) ?? 0) + page.getScrollTop());
}
resolve(errs);
}
// 如果错误信息为空,直接返回
if (errors.value.size == 0) {
done();
return;
}
nextTick(() => {
let component = proxy;
// #ifdef MP
let num = 0; // 记录已处理的表单项数量
// 并查找其错误节点的位置
const deep = (el: any, index: number) => {
// 遍历当前节点的所有子节点
el?.$children.map((e: any) => {
// 限制递归深度,防止死循环
if (index < 5) {
// 判断是否为 cl-form-item 组件且 prop 存在
if (e.prop != null && e.$options.name == "cl-form-item") {
// 如果该字段已注册到 fields 中,则计数加一
if (fields.value.has(e.prop)) {
num += 1;
}
// 查询该 cl-form-item 下是否有错误节点,并获取其位置信息
uni.createSelectorQuery()
.in(e)
.select(".cl-form-item--error")
.boundingClientRect((res) => {
// 如果未获取到节点信息,直接返回
if (res == null) {
return;
}
// 记录该字段的错误节点 top 值
tops.set(e.prop, (res as NodeInfo).top!);
// 如果已处理的表单项数量达到总数,执行 done 回调
if (num >= fields.value.size) {
done();
}
})
.exec();
}
// 递归查找子节点
deep(e, index + 1);
}
});
};
deep(component, 0);
// #endif
// #ifndef MP
uni.createSelectorQuery()
.in(component)
.selectAll(".cl-form-item--error")
.boundingClientRect((res) => {
(res as NodeInfo[]).map((e) => {
tops.set((e.id ?? "").replace("cl-form-item-", ""), e.top ?? 0);
});
done();
})
.exec();
// #endif
});
});
}
// 清除所有错误信息
function clearErrors() {
errors.value.clear();
@@ -148,16 +247,36 @@ function clearErrors() {
// 获取字段值
function getValue(prop: string): any | null {
if (prop != "") {
return data.value[prop];
return get(data.value, prop, null);
}
return null;
}
// 获取字段规则
function getRule(prop: string): ClFormRule[] {
return props.rules.get(prop) ?? ([] as ClFormRule[]);
}
// 设置字段规则
function setRule(prop: string, rules: ClFormRule[]) {
if (prop != "" && !isEmpty(rules)) {
props.rules.set(prop, rules);
}
}
// 移除字段规则
function removeRule(prop: string) {
if (prop != "") {
props.rules.delete(prop);
}
}
// 注册表单字段
function addField(prop: string) {
function addField(prop: string, rules: ClFormRule[]) {
if (prop != "") {
fields.value.add(prop);
setRule(prop, rules);
}
}
@@ -165,15 +284,11 @@ function addField(prop: string) {
function removeField(prop: string) {
if (prop != "") {
fields.value.delete(prop);
removeRule(prop);
removeError(prop);
}
}
// 获取字段规则
function getRule(prop: string): ClFormRule[] {
return props.rules.get(prop) ?? ([] as ClFormRule[]);
}
// 验证单个规则
function validateRule(value: any | null, rule: ClFormRule): null | string {
// 必填验证
@@ -270,6 +385,7 @@ function validateField(prop: string): string | null {
});
}
// 移除错误信息
removeError(prop);
}
@@ -280,51 +396,17 @@ function validateField(prop: string): string | null {
return error;
}
// 滚动到第一个错误位置
function scrollToError(prop: string) {
if (props.scrollToError == false) {
return;
}
nextTick(() => {
let component = proxy;
// #ifdef MP
component = proxy?.$children.find((e: any) => e.prop == prop);
// #endif
uni.createSelectorQuery()
.in(component)
.select(".cl-form-item--error")
.boundingClientRect((res) => {
if (!isNull(res)) {
page.scrollTo(((res as NodeInfo).top ?? 0) + page.getScrollTop());
}
})
.exec();
});
}
// 验证整个表单
function validate(callback: (valid: boolean, errors: ClFormValidateError[]) => void) {
const errs = [] as ClFormValidateError[];
async function validate(callback: (valid: boolean, errors: ClFormValidateError[]) => void) {
// 验证所有字段
fields.value.forEach((prop) => {
const result = validateField(prop);
if (result != null) {
errs.push({
field: prop,
message: result
});
}
validateField(prop);
});
// 滚动到第一个错误位置
if (errs.length > 0) {
scrollToError(errs[0].field);
}
// 获取所有错误信息,并滚动到第一个错误位置
const errs = await getErrors();
// 回调
callback(errs.length == 0, errs);
}
@@ -353,9 +435,12 @@ defineExpose({
getValue,
setError,
getError,
getErrors,
removeError,
clearErrors,
getRule,
setRule,
removeRule,
validateRule,
clearValidate,
validateField,

View File

@@ -27,8 +27,8 @@ export class Form {
}
// 注册表单字段
addField = (prop: string): void => {
this.formRef.value!.addField(prop);
addField = (prop: string, rules: ClFormRule[]): void => {
this.formRef.value!.addField(prop, rules);
};
// 注销表单字段
@@ -51,6 +51,11 @@ export class Form {
return this.formRef.value!.getError(prop);
};
// 获取所有错误信息
getErrors = async (): Promise<ClFormValidateError[]> => {
return this.formRef.value!.getErrors();
};
// 移除字段错误信息
removeError = (prop: string): void => {
this.formRef.value!.removeError(prop);
@@ -66,6 +71,16 @@ export class Form {
return this.formRef.value!.getRule(prop);
};
// 设置字段规则
setRule = (prop: string, rules: ClFormRule[]): void => {
this.formRef.value!.setRule(prop, rules);
};
// 移除字段规则
removeRule = (prop: string): void => {
this.formRef.value!.removeRule(prop);
};
// 验证单个规则
validateRule = (value: any | null, rule: ClFormRule): string | null => {
return this.formRef.value!.validateRule(value, rule);

View File

@@ -174,18 +174,21 @@ declare type ClFormComponentPublicInstance = {
data: UTSJSONObject;
errors: Map<string, string>;
fields: Set<string>;
addField: (prop: string) => void;
addField: (prop: string, rules: ClFormRule[]) => void;
removeField: (prop: string) => void;
getValue: (prop: string) => any | null;
setError: (prop: string, error: string) => void;
getError: (prop: string) => string;
getErrors: () => Promise<ClFormValidateError[]>;
removeError: (prop: string) => void;
clearErrors: () => void;
getRule: (prop: string) => ClFormRule[];
setRule: (prop: string, rules: ClFormRule[]) => void;
removeRule: (prop: string) => void;
validateRule: (value: any | null, rule: ClFormRule) => string | null;
clearValidate: () => void;
validateField: (prop: string) => string | null;
validate: (callback: (valid: boolean, errors: ClFormValidateError[]) => void) => void;
validate: (callback: (valid: boolean, errors: ClFormValidateError[]) => void) => Promise<void>;
};
declare type ClFormItemComponentPublicInstance = {