[完善]表单使用async-validator4.0.7实现校验功能

🐛[修复]下拉框点击自身不能隐藏和设置modelValue为空时不能置空下拉框
This commit is contained in:
xumi
2021-12-19 07:12:33 +08:00
parent 63d1e7538d
commit cd727c0ef5
9 changed files with 912 additions and 33 deletions

View File

@@ -1,25 +1,140 @@
<template>
<form class="layui-form" @submit="submit">
<form class="layui-form" :onsubmit="submit">
<slot />
</form>
</template>
<script setup name="LayForm" lang="ts">
<script lang="ts">
export default{
name: 'LayForm'
}
</script>
<script setup lang="ts">
import { toRefs, provide,reactive, onMounted } from "vue"
import { Rule, ValidateError, ValidateMessages } from "async-validator"
import {layFormKey, LayFormItemContext, FormCallback, modelType} from "../type/form"
const props = withDefaults(
defineProps<{
model?: object
model?: modelType
required?: boolean,
rules?: Rule,
initValidate?: boolean,
requiredIcons?: string,
requiredErrorMessage?: string,
validateMessage?: ValidateMessages,
useCN?: boolean
}>(),
{
model: function(){
return {}
}
},
useCN : true,
requiredIcons : '',
initValidate : false
}
)
const formItems : LayFormItemContext[] = [];
const formItemMap : {[key:string]:LayFormItemContext} = {};
const emit = defineEmits(['submit'])
// 初始化表单就进行校验
onMounted(()=>{
props.initValidate && validate()?.catch(err => {});
})
// 原生提交表单事件
const submit = function () {
emit('submit',props.model)
let _isValidate = false;
validate((isValidate, model, errors) => {
_isValidate = isValidate as boolean;
emit('submit', isValidate, model, errors);
});
// 如果表单失败则阻止提交表单,成功则进行提交表单
return _isValidate;
}
/**
* 校验表单数据
* @param fields 需要校验的表单字段(string|string[]); 该字段如果为function, 则默认为回调函数,校验全部字段;
* @param callback 校验表单之后的回调函数
**/
const validate = function(fields?: string|string[]|FormCallback|null, callback?: FormCallback | null){
// 根据参数识别需要校验的表单项
let validateItems : LayFormItemContext[] = formItems;
if (typeof fields === 'function') {
callback = fields;
} else if (typeof fields === 'string' || (Array.isArray(fields) && fields.length > 0)) {
validateItems = [];
const validateFields = !fields ? [] : ([] as string[]).concat(fields);
validateFields.forEach(field => formItemMap[field] && validateItems.push(formItemMap[field]));
}
// 通过调用每个子项进行校验
let errorsArrs: ValidateError[] = [];
validateItems.forEach(filed => {
filed.validate((errors, _fields)=>{
errorsArrs = errorsArrs.concat(errors as ValidateError[]);
});
});
const isValidate = errorsArrs.length === 0;
// 有回调则进行回调
if (typeof callback === 'function') {
isValidate ? callback(true, props.model, null) : callback(false, props.model, errorsArrs);
return null;
}
// 没有回调则创建一个Promise的链式调用
return new Promise((resolve, reject) => {
const callbackParams = {
isValidate,
model : props.model,
errors: isValidate ? null : errorsArrs
};
callbackParams.isValidate ? resolve(callbackParams) : reject(callbackParams);
});
}
/**
* 清除校验
* @param fields 需要进行清除校验的表单字段(string|string[]); 该字段如果为null, 则默认为全部字段清除校验;
**/
const clearValidate = function(fields?: string | string[]){
const clearFields = !fields ? [] : ([] as string[]).concat(fields);
if (clearFields.length === 0) {
formItems.forEach(filed => filed.clearValidate());
} else {
clearFields.forEach(field => formItemMap[field] && formItemMap[field].clearValidate());
}
}
/**
* 重置表单所有值
**/
const reset = function(){
for (const key in props.model) {
props.model[key] = null;
}
// 重新校验
setTimeout(()=>validate()?.catch(err => {}), 0);
}
// 添加子项
const addField = function(item : LayFormItemContext) {
formItems.push(item);
formItemMap[item.prop as string] = item;
}
defineExpose({validate, clearValidate, reset});
provide(layFormKey, reactive({
formItems,
addField,
clearValidate,
validate,
...toRefs(props)
}));
</script>

View File

@@ -0,0 +1,49 @@
import { ValidateMessages } from "async-validator";
// 中文翻译 --> 根据 async-validator 中 ValidateMessages 进行翻译
export default {
default: "%s验证失败",
required: "%s不能为空",
enum: "%s不在枚举%s里面",
whitespace: "%s不能为空",
date: {
format: "%s日期%s不是一个有效格式的日期%s",
parse: "%s无法解析为日期,%s是无效的",
invalid: "%s日期%s是无效的"
},
types: {
number: '%s不是一个有效的数字',
boolean: '%s不是一个有效的布尔类型',
method: '%s不是一个有效的方法',
regexp: '%s不是一个有效的正则表达式',
integer: '%s不是一个有效的整型数字',
float: '%s不是一个有效的浮点小数',
array: '%s不是一个有效的数组',
object: '%s不是一个有效的对象',
enum: '%s不是一个有效的枚举',
date: '%s不是一个有效的日期',
url: '%s不是一个有效的url',
hex: '%s不是一个有效的十六进制',
email: '%s不是一个有效的邮箱'
},
string: {
len: "%s必须是长度为%s个字符",
min: "%s最小长度为%s个字符",
max: "%s最长%s个字符",
range: "%s字符长度需要在%s和%s直接"
},
number: {
len: "%s长度必须为%s",
min: "%s必须小于%s",
max: "%s必须大于%s",
range: "%s需要在%s和%s之间"
},
array: {
len: "%s长度必须为%s",
min: "%s长度必须小于%s",
max: "%s长度必须大于%s",
range: "%s长度需要在%s和%s之间"
},
pattern: {
"mismatch": "%s值%s不能匹配%s"
}
} as ValidateMessages;

View File

@@ -0,0 +1,47 @@
@error_color : red;
.layui-required{
color: @error_color;
font-size: 12px;
line-height: 1;
}
.layui-form .layui-form-item{
.layui-input-block
,.layui-input-inline{
.layui-form-danger {
border-color: #ff5722 !important;
}
}
}
.layui-error-message {
color: @error_color;
font-size: 12px;
line-height: 1;
padding-top: 2px;
position: absolute;
top: 100%;
left: 0;
}
.layui-error-message-anim {
-ms-transform-origin: 0 0;
-webkit-transform-origin: 0 0;
transform-origin: 0 0;
-webkit-animation: layui-top-show-anim 0.3s ease 1;
animation: layui-top-show-anim 0.3s ease 1;
}
@keyframes layui-top-show-anim {
0% {
opacity: 0.3;
transform: rotateX(45deg);
}
100% {
opacity: 1;
transform: rotateX(0);
}
}

View File

@@ -1,21 +1,140 @@
<template>
<div class="layui-form-item">
<label class="layui-form-label">{{ label }}</label>
<div class="layui-form-item" ref="formItemRef">
<label class="layui-form-label">
<span v-if="props.prop &&isRequired" :class="['layui-required', 'layui-icon'].concat(layForm.requiredIcons??'')">
<slot name="required" :props="{...props, model: layForm.model}">{{layForm.requiredIcons? '' : '*'}}</slot>
</span>
<slot name="label" :props="{...props, model: layForm.model}">
{{ label }}
</slot>
</label>
<div :class="[mode ? 'layui-input-' + mode : '']">
<slot />
<div ref="slotParent">
<slot :props="{...props, model: layForm.model}"/>
</div>
<span v-if="errorStatus" :class="['layui-error-message', {'layui-error-message-anim': errorStatus}]">{{errorMsg}}</span>
</div>
</div>
</template>
<script setup name="LayFormItem" lang="ts">
import { defineProps, withDefaults } from 'vue'
import "./index.less";
import { defineProps, inject, withDefaults, ref, reactive, toRefs, onMounted, computed, watch} from 'vue'
import {layFormKey, LayFormContext, LayFormItemContext, FieldValidateError} from "../type/form"
import Schema, { Rule, RuleItem, Rules, ValidateCallback, ValidateError, ValidateMessages} from 'async-validator';
import cnValidateMessage from './cnValidateMessage';
const props = withDefaults(
defineProps<{
prop?: string
mode?: string
label?: string
errorMessage?: string
rules?: Rule
required?: boolean
}>(),
{
mode: 'block'
}
)
const layForm = inject(layFormKey, {} as LayFormContext)
const formItemRef = ref<HTMLDivElement>()
const slotParent = ref<HTMLDivElement>()
// 是否必填
const isRequired = computed(()=>{
return props.required || layForm.required;
})
// 拼接校验规则
const ruleItems = computed(()=>{
const prop = props.prop;
if (!prop) {
return {};
}
let rulesArrs : RuleItem[] = [];
if (isRequired.value) {
rulesArrs.push({required: true});
}
if (props.rules) {
rulesArrs = rulesArrs.concat((props.rules as RuleItem | RuleItem[]));
}
if (layForm.rules && layForm.rules[prop]) {
rulesArrs = rulesArrs.concat((layForm.rules[prop] as RuleItem | RuleItem[]));
}
return rulesArrs;
});
// 值 计算 和 监听
const filedValue = computed(()=> props.prop ? layForm.model[props.prop] : undefined);
watch(()=>filedValue.value, (val)=> validate());
// 错误状态和信息
const errorStatus = ref(false);
const errorMsg = ref();
// 校验数据有效性
const validate = (callback ?: ValidateCallback)=> {
if (props.prop && (ruleItems.value as RuleItem[]).length > 0) {
// 校验规则
const descriptor : Rules = {};
descriptor[layForm.useCN? (props.label||props.prop ): props.prop] = ruleItems.value;
const validator = new Schema(descriptor);
let model : {[key : string]:any} = {};
let validateMessage = null;
// 使用中文错误提示
if (layForm.useCN) {
validateMessage = Object.assign({}, cnValidateMessage, layForm.validateMessage);
model[props.label||props.prop] = filedValue.value;
} else {
layForm.validateMessage && (validateMessage = layForm.validateMessage);
model[props.prop] = filedValue.value;
}
// 自定义校验消息
layForm.requiredErrorMessage && (validateMessage = Object.assign(validateMessage, {required : layForm.requiredErrorMessage}));
validateMessage && validator.messages(validateMessage);
// 开始校验
validator.validate(model, (errors, fields) => {
errorStatus.value = errors !== null && errors.length > 0;
const slotParentDiv = slotParent.value as HTMLDivElement;
if (errorStatus.value) {
const _errors = (errors as FieldValidateError[]);
// 如果是中文,将错误信息转换成FieldValidateError类型
layForm.useCN && _errors.forEach(error => {
error.label = props.label;
error.field = props.prop;
})
errorMsg.value = props.errorMessage??_errors[0].message;
slotParentDiv.childElementCount > 0 && slotParentDiv.firstElementChild?.classList.add('layui-form-danger');
callback && callback(_errors, fields);
} else {
clearValidate();
}
});
}
}
// 清除校验
const clearValidate = ()=> {
errorStatus.value = false;
errorMsg.value = '';
const slotParentDiv = slotParent.value as HTMLDivElement;
slotParentDiv.childElementCount > 0 && slotParentDiv.firstElementChild?.classList.remove('layui-form-danger');
}
defineExpose({validate, clearValidate});
onMounted(()=>{
if (props.prop) {
layForm.addField(reactive({
...toRefs(props),
$el: formItemRef,
validate,
clearValidate
}) as LayFormItemContext);
}
})
</script>

View File

@@ -41,10 +41,10 @@ const props = defineProps<{
const openState = ref(false)
const open = function () {
openState.value = true
openState.value = !openState.value
}
const selectItem = reactive({ label: '', value: props.modelValue })
const selectItem = reactive({ label: null, value: props.modelValue })
provide('selectItem', selectItem)
provide('openState', openState)
@@ -56,4 +56,12 @@ watch(selectItem, function (item) {
emit('change', item.value)
emit('update:modelValue', item.value)
})
watch(()=>props.modelValue, function (value) {
if (!value) {
selectItem.label = null;
selectItem.value = '';
emit('update:modelValue', null);
}
})
</script>

37
src/module/type/form.ts Normal file
View File

@@ -0,0 +1,37 @@
import type { ValidateCallback, ValidateError, ValidateMessages } from 'async-validator'
export const layFormKey = 'LayForm'
export interface LayFormContext {
model: modelType
required?: boolean
requiredErrorMessage?: string
validateMessage: ValidateMessages
rules?: Record<string, unknown>
useCN : boolean
requiredIcons?: string
addField: (field: LayFormItemContext) => void
}
export interface LayFormItemContext {
prop?: string
$el: HTMLDivElement
required?: boolean
rules?: Record<string, unknown>
validate(callback?: ValidateCallback): void
clearValidate(): void
}
export declare type modelType = { [key: string]: any }
export declare interface FormCallback {
(
isValid?: boolean,
model?: modelType,
errors?: ValidateError[] | null
): void
}
export declare interface FieldValidateError extends ValidateError {
label ?: string
}

View File

@@ -1,2 +1,3 @@
export * from './public'
export * from './select'
export * from './form'