Files
taimed-international-app/uni_modules/wot-design-uni/components/wd-form/wd-form.vue
2025-11-04 12:37:04 +08:00

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>