cl-form 支持动态字段验证,如:contacts[0].phone
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cool-unix",
|
"name": "cool-unix",
|
||||||
"version": "8.0.8",
|
"version": "8.0.9",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build-ui": "node ./uni_modules/cool-ui/scripts/generate-types.js",
|
"build-ui": "node ./uni_modules/cool-ui/scripts/generate-types.js",
|
||||||
|
|||||||
@@ -24,13 +24,55 @@
|
|||||||
></cl-input>
|
></cl-input>
|
||||||
</cl-form-item>
|
</cl-form-item>
|
||||||
|
|
||||||
<cl-form-item :label="t('邮箱')" prop="email" required>
|
<cl-form-item :label="t('邮箱')" prop="email">
|
||||||
<cl-input
|
<cl-input
|
||||||
v-model="formData.email"
|
v-model="formData.email"
|
||||||
:placeholder="t('请输入邮箱地址')"
|
:placeholder="t('请输入邮箱地址')"
|
||||||
></cl-input>
|
></cl-input>
|
||||||
</cl-form-item>
|
</cl-form-item>
|
||||||
|
|
||||||
|
<cl-form-item :label="t('动态验证')" required prop="contacts">
|
||||||
|
<view
|
||||||
|
class="contacts border border-solid border-surface-200 rounded-xl p-3 dark:!border-surface-700"
|
||||||
|
>
|
||||||
|
<cl-form-item
|
||||||
|
v-for="(item, index) in formData.contacts"
|
||||||
|
:key="index"
|
||||||
|
:label="t('联系人') + ` - ${index + 1}`"
|
||||||
|
:prop="`contacts[${index}].phone`"
|
||||||
|
:rules="
|
||||||
|
[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t('手机号不能为空')
|
||||||
|
}
|
||||||
|
] as ClFormRule[]
|
||||||
|
"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<view class="flex flex-row items-center">
|
||||||
|
<cl-input
|
||||||
|
:pt="{
|
||||||
|
className: 'flex-1 mr-2'
|
||||||
|
}"
|
||||||
|
v-model="item.phone"
|
||||||
|
:placeholder="t('请输入手机号')"
|
||||||
|
></cl-input>
|
||||||
|
|
||||||
|
<cl-button
|
||||||
|
type="light"
|
||||||
|
icon="subtract-line"
|
||||||
|
@tap="removeContact(index)"
|
||||||
|
></cl-button>
|
||||||
|
</view>
|
||||||
|
</cl-form-item>
|
||||||
|
|
||||||
|
<cl-button icon="add-line" @tap="addContact">{{
|
||||||
|
t("添加联系人")
|
||||||
|
}}</cl-button>
|
||||||
|
</view>
|
||||||
|
</cl-form-item>
|
||||||
|
|
||||||
<cl-form-item :label="t('身高')" prop="height" required>
|
<cl-form-item :label="t('身高')" prop="height" required>
|
||||||
<cl-slider v-model="formData.height" :max="220" show-value>
|
<cl-slider v-model="formData.height" :max="220" show-value>
|
||||||
<template #value="{ value }">
|
<template #value="{ value }">
|
||||||
@@ -182,6 +224,10 @@ const tagsOptions = [
|
|||||||
// 地区选项
|
// 地区选项
|
||||||
const pcaOptions = useCascader(pca);
|
const pcaOptions = useCascader(pca);
|
||||||
|
|
||||||
|
type Contact = {
|
||||||
|
phone: string;
|
||||||
|
};
|
||||||
|
|
||||||
// 自定义表单数据类型
|
// 自定义表单数据类型
|
||||||
type FormData = {
|
type FormData = {
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
@@ -195,6 +241,7 @@ type FormData = {
|
|||||||
tags: number[];
|
tags: number[];
|
||||||
birthday: string;
|
birthday: string;
|
||||||
isPublic: boolean;
|
isPublic: boolean;
|
||||||
|
contacts: Contact[];
|
||||||
};
|
};
|
||||||
|
|
||||||
// 表单数据
|
// 表单数据
|
||||||
@@ -209,7 +256,8 @@ const formData = ref<FormData>({
|
|||||||
pca: [],
|
pca: [],
|
||||||
tags: [1, 2],
|
tags: [1, 2],
|
||||||
birthday: "",
|
birthday: "",
|
||||||
isPublic: false
|
isPublic: false,
|
||||||
|
contacts: []
|
||||||
}) as Ref<FormData>;
|
}) as Ref<FormData>;
|
||||||
|
|
||||||
// 表单验证规则
|
// 表单验证规则
|
||||||
@@ -265,6 +313,15 @@ const rules = new Map<string, ClFormRule[]>([
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"contacts",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("联系人不能为空")
|
||||||
|
}
|
||||||
|
]
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -284,6 +341,7 @@ function reset() {
|
|||||||
formData.value.tags = [];
|
formData.value.tags = [];
|
||||||
formData.value.birthday = "";
|
formData.value.birthday = "";
|
||||||
formData.value.isPublic = false;
|
formData.value.isPublic = false;
|
||||||
|
formData.value.contacts = [];
|
||||||
|
|
||||||
clearValidate();
|
clearValidate();
|
||||||
}
|
}
|
||||||
@@ -310,4 +368,14 @@ function submit() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addContact() {
|
||||||
|
formData.value.contacts.push({
|
||||||
|
phone: ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeContact(index: number) {
|
||||||
|
formData.value.contacts.splice(index, 1);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
1
types/uni-app.d.ts
vendored
1
types/uni-app.d.ts
vendored
@@ -40,6 +40,7 @@ declare interface Uni {
|
|||||||
}
|
}
|
||||||
|
|
||||||
declare interface NodeInfo {
|
declare interface NodeInfo {
|
||||||
|
id?: string;
|
||||||
bottom?: number;
|
bottom?: number;
|
||||||
context?: number;
|
context?: number;
|
||||||
dataset?: number;
|
dataset?: number;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, getCurrentInstance, nextTick, ref, watch, type PropType } from "vue";
|
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 type { ClFormLabelPosition, ClFormRule, ClFormValidateError } from "../../types";
|
||||||
import { $t, t } from "@/locale";
|
import { $t, t } from "@/locale";
|
||||||
import { usePage } from "../../hooks";
|
import { usePage } from "../../hooks";
|
||||||
@@ -140,6 +140,105 @@ function getError(prop: string): string {
|
|||||||
return "";
|
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() {
|
function clearErrors() {
|
||||||
errors.value.clear();
|
errors.value.clear();
|
||||||
@@ -148,16 +247,36 @@ function clearErrors() {
|
|||||||
// 获取字段值
|
// 获取字段值
|
||||||
function getValue(prop: string): any | null {
|
function getValue(prop: string): any | null {
|
||||||
if (prop != "") {
|
if (prop != "") {
|
||||||
return data.value[prop];
|
return get(data.value, prop, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return 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 != "") {
|
if (prop != "") {
|
||||||
fields.value.add(prop);
|
fields.value.add(prop);
|
||||||
|
setRule(prop, rules);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,15 +284,11 @@ function addField(prop: string) {
|
|||||||
function removeField(prop: string) {
|
function removeField(prop: string) {
|
||||||
if (prop != "") {
|
if (prop != "") {
|
||||||
fields.value.delete(prop);
|
fields.value.delete(prop);
|
||||||
|
removeRule(prop);
|
||||||
removeError(prop);
|
removeError(prop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取字段规则
|
|
||||||
function getRule(prop: string): ClFormRule[] {
|
|
||||||
return props.rules.get(prop) ?? ([] as ClFormRule[]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证单个规则
|
// 验证单个规则
|
||||||
function validateRule(value: any | null, rule: ClFormRule): null | string {
|
function validateRule(value: any | null, rule: ClFormRule): null | string {
|
||||||
// 必填验证
|
// 必填验证
|
||||||
@@ -270,6 +385,7 @@ function validateField(prop: string): string | null {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 移除错误信息
|
||||||
removeError(prop);
|
removeError(prop);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,51 +396,17 @@ function validateField(prop: string): string | null {
|
|||||||
return error;
|
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) {
|
async function validate(callback: (valid: boolean, errors: ClFormValidateError[]) => void) {
|
||||||
const errs = [] as ClFormValidateError[];
|
// 验证所有字段
|
||||||
|
|
||||||
fields.value.forEach((prop) => {
|
fields.value.forEach((prop) => {
|
||||||
const result = validateField(prop);
|
validateField(prop);
|
||||||
|
|
||||||
if (result != null) {
|
|
||||||
errs.push({
|
|
||||||
field: prop,
|
|
||||||
message: result
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 滚动到第一个错误位置
|
// 获取所有错误信息,并滚动到第一个错误位置
|
||||||
if (errs.length > 0) {
|
const errs = await getErrors();
|
||||||
scrollToError(errs[0].field);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 回调
|
||||||
callback(errs.length == 0, errs);
|
callback(errs.length == 0, errs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,9 +435,12 @@ defineExpose({
|
|||||||
getValue,
|
getValue,
|
||||||
setError,
|
setError,
|
||||||
getError,
|
getError,
|
||||||
|
getErrors,
|
||||||
removeError,
|
removeError,
|
||||||
clearErrors,
|
clearErrors,
|
||||||
getRule,
|
getRule,
|
||||||
|
setRule,
|
||||||
|
removeRule,
|
||||||
validateRule,
|
validateRule,
|
||||||
clearValidate,
|
clearValidate,
|
||||||
validateField,
|
validateField,
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ export class Form {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 注册表单字段
|
// 注册表单字段
|
||||||
addField = (prop: string): void => {
|
addField = (prop: string, rules: ClFormRule[]): void => {
|
||||||
this.formRef.value!.addField(prop);
|
this.formRef.value!.addField(prop, rules);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 注销表单字段
|
// 注销表单字段
|
||||||
@@ -51,6 +51,11 @@ export class Form {
|
|||||||
return this.formRef.value!.getError(prop);
|
return this.formRef.value!.getError(prop);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取所有错误信息
|
||||||
|
getErrors = async (): Promise<ClFormValidateError[]> => {
|
||||||
|
return this.formRef.value!.getErrors();
|
||||||
|
};
|
||||||
|
|
||||||
// 移除字段错误信息
|
// 移除字段错误信息
|
||||||
removeError = (prop: string): void => {
|
removeError = (prop: string): void => {
|
||||||
this.formRef.value!.removeError(prop);
|
this.formRef.value!.removeError(prop);
|
||||||
@@ -66,6 +71,16 @@ export class Form {
|
|||||||
return this.formRef.value!.getRule(prop);
|
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 => {
|
validateRule = (value: any | null, rule: ClFormRule): string | null => {
|
||||||
return this.formRef.value!.validateRule(value, rule);
|
return this.formRef.value!.validateRule(value, rule);
|
||||||
|
|||||||
7
uni_modules/cool-ui/types/component.d.ts
vendored
7
uni_modules/cool-ui/types/component.d.ts
vendored
@@ -174,18 +174,21 @@ declare type ClFormComponentPublicInstance = {
|
|||||||
data: UTSJSONObject;
|
data: UTSJSONObject;
|
||||||
errors: Map<string, string>;
|
errors: Map<string, string>;
|
||||||
fields: Set<string>;
|
fields: Set<string>;
|
||||||
addField: (prop: string) => void;
|
addField: (prop: string, rules: ClFormRule[]) => void;
|
||||||
removeField: (prop: string) => void;
|
removeField: (prop: string) => void;
|
||||||
getValue: (prop: string) => any | null;
|
getValue: (prop: string) => any | null;
|
||||||
setError: (prop: string, error: string) => void;
|
setError: (prop: string, error: string) => void;
|
||||||
getError: (prop: string) => string;
|
getError: (prop: string) => string;
|
||||||
|
getErrors: () => Promise<ClFormValidateError[]>;
|
||||||
removeError: (prop: string) => void;
|
removeError: (prop: string) => void;
|
||||||
clearErrors: () => void;
|
clearErrors: () => void;
|
||||||
getRule: (prop: string) => ClFormRule[];
|
getRule: (prop: string) => ClFormRule[];
|
||||||
|
setRule: (prop: string, rules: ClFormRule[]) => void;
|
||||||
|
removeRule: (prop: string) => void;
|
||||||
validateRule: (value: any | null, rule: ClFormRule) => string | null;
|
validateRule: (value: any | null, rule: ClFormRule) => string | null;
|
||||||
clearValidate: () => void;
|
clearValidate: () => void;
|
||||||
validateField: (prop: string) => string | null;
|
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 = {
|
declare type ClFormItemComponentPublicInstance = {
|
||||||
|
|||||||
Reference in New Issue
Block a user