208 lines
5.5 KiB
Vue
208 lines
5.5 KiB
Vue
<template>
|
|
<view :class="`wd-form ${customClass}`" :style="customStyle">
|
|
<slot></slot>
|
|
<wd-toast v-if="props.errorType === 'toast'" selector="wd-form-toast" />
|
|
</view>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
export default {
|
|
name: 'wd-form',
|
|
options: {
|
|
addGlobalClass: true,
|
|
virtualHost: true,
|
|
styleIsolation: 'shared'
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<script lang="ts" setup>
|
|
import wdToast from '../wd-toast/wd-toast.vue'
|
|
import { reactive, watch } from 'vue'
|
|
import { deepClone, getPropByPath, isArray, isDef, isPromise, isString } from '../common/util'
|
|
import { useChildren } from '../composables/useChildren'
|
|
import { useToast } from '../wd-toast'
|
|
import { type FormRules, FORM_KEY, type ErrorMessage, formProps, type FormExpose } from './types'
|
|
|
|
const { show: showToast } = useToast('wd-form-toast')
|
|
const props = defineProps(formProps)
|
|
|
|
const { children, linkChildren } = useChildren(FORM_KEY)
|
|
let errorMessages = reactive<Record<string, string>>({})
|
|
|
|
linkChildren({ props, errorMessages })
|
|
|
|
watch(
|
|
() => props.model,
|
|
() => {
|
|
if (props.resetOnChange) {
|
|
clearMessage()
|
|
}
|
|
},
|
|
{ immediate: true, deep: true }
|
|
)
|
|
|
|
/**
|
|
* 表单校验
|
|
* @param prop 指定校验字段或字段数组
|
|
*/
|
|
async function validate(prop?: string | string[]): Promise<{ valid: boolean; errors: ErrorMessage[] }> {
|
|
const errors: ErrorMessage[] = []
|
|
let valid: boolean = true
|
|
const promises: Promise<void>[] = []
|
|
const formRules: FormRules = getMergeRules()
|
|
const propsToValidate = isArray(prop) ? prop : isDef(prop) ? [prop] : []
|
|
const rulesToValidate: FormRules =
|
|
propsToValidate.length > 0
|
|
? propsToValidate.reduce((acc, key) => {
|
|
if (formRules[key]) {
|
|
acc[key] = formRules[key]
|
|
}
|
|
return acc
|
|
}, {} as FormRules)
|
|
: formRules
|
|
|
|
for (const propName in rulesToValidate) {
|
|
const rules = rulesToValidate[propName]
|
|
const value = getPropByPath(props.model, propName)
|
|
|
|
if (rules && rules.length > 0) {
|
|
for (const rule of rules) {
|
|
if (rule.required && (!isDef(value) || value === '')) {
|
|
errors.push({
|
|
prop: propName,
|
|
message: rule.message
|
|
})
|
|
valid = false
|
|
break
|
|
}
|
|
if (rule.pattern && !rule.pattern.test(value)) {
|
|
errors.push({
|
|
prop: propName,
|
|
message: rule.message
|
|
})
|
|
valid = false
|
|
break
|
|
}
|
|
const { validator, ...ruleWithoutValidator } = rule
|
|
if (validator) {
|
|
const result = validator(value, ruleWithoutValidator)
|
|
if (isPromise(result)) {
|
|
promises.push(
|
|
result
|
|
.then((res) => {
|
|
if (typeof res === 'string') {
|
|
errors.push({
|
|
prop: propName,
|
|
message: res
|
|
})
|
|
valid = false
|
|
} else if (typeof res === 'boolean' && !res) {
|
|
errors.push({
|
|
prop: propName,
|
|
message: rule.message
|
|
})
|
|
valid = false
|
|
}
|
|
})
|
|
.catch((error?: string | Error) => {
|
|
const message = isDef(error) ? (isString(error) ? error : error.message || rule.message) : rule.message
|
|
errors.push({ prop: propName, message })
|
|
valid = false
|
|
})
|
|
)
|
|
} else {
|
|
if (!result) {
|
|
errors.push({
|
|
prop: propName,
|
|
message: rule.message
|
|
})
|
|
valid = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
await Promise.all(promises)
|
|
|
|
showMessage(errors)
|
|
|
|
if (valid) {
|
|
if (propsToValidate.length) {
|
|
propsToValidate.forEach(clearMessage)
|
|
} else {
|
|
clearMessage()
|
|
}
|
|
}
|
|
|
|
return {
|
|
valid,
|
|
errors
|
|
}
|
|
}
|
|
|
|
// 合并子组件的rules到父组件的rules
|
|
function getMergeRules() {
|
|
const mergedRules: FormRules = deepClone(props.rules)
|
|
const childrenProps = children.map((child) => child.prop)
|
|
|
|
// 过滤掉在 children 中不存在对应子组件的规则
|
|
Object.keys(mergedRules).forEach((key) => {
|
|
if (!childrenProps.includes(key)) {
|
|
delete mergedRules[key]
|
|
}
|
|
})
|
|
|
|
children.forEach((item) => {
|
|
if (isDef(item.prop) && isDef(item.rules) && item.rules.length) {
|
|
if (mergedRules[item.prop]) {
|
|
mergedRules[item.prop] = [...mergedRules[item.prop], ...item.rules]
|
|
} else {
|
|
mergedRules[item.prop] = item.rules
|
|
}
|
|
}
|
|
})
|
|
return mergedRules
|
|
}
|
|
|
|
function showMessage(errors: ErrorMessage[]) {
|
|
const childrenProps = children.map((e) => e.prop).filter(Boolean)
|
|
const messages = errors.filter((error) => error.message && childrenProps.includes(error.prop))
|
|
if (messages.length) {
|
|
messages.sort((a, b) => {
|
|
return childrenProps.indexOf(a.prop) - childrenProps.indexOf(b.prop)
|
|
})
|
|
if (props.errorType === 'toast') {
|
|
showToast(messages[0].message)
|
|
} else if (props.errorType === 'message') {
|
|
messages.forEach((error) => {
|
|
errorMessages[error.prop] = error.message
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
function clearMessage(prop?: string) {
|
|
if (prop) {
|
|
errorMessages[prop] = ''
|
|
} else {
|
|
Object.keys(errorMessages).forEach((key) => {
|
|
errorMessages[key] = ''
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 重置表单项的验证提示
|
|
*/
|
|
function reset() {
|
|
clearMessage()
|
|
}
|
|
|
|
defineExpose<FormExpose>({ validate, reset })
|
|
</script>
|
|
|
|
<style lang="scss" scoped></style>
|