✨[完善]表单使用async-validator4.0.7实现校验功能
🐛[修复]下拉框点击自身不能隐藏和设置modelValue为空时不能置空下拉框
This commit is contained in:
@@ -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>
|
||||
|
||||
49
src/module/formItem/cnValidateMessage.ts
Normal file
49
src/module/formItem/cnValidateMessage.ts
Normal 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;
|
||||
47
src/module/formItem/index.less
Normal file
47
src/module/formItem/index.less
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
37
src/module/type/form.ts
Normal 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
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './public'
|
||||
export * from './select'
|
||||
export * from './form'
|
||||
|
||||
Reference in New Issue
Block a user