1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
| <!-- components/Form/Form.vue --> <template> <form :class="formClasses" @submit="handleSubmit"> <slot :validate="validate" :reset="reset" :errors="errors" /> </form> </template>
<script setup lang="ts"> import { reactive, provide, computed } from 'vue' import { FormValidator } from './validation' import type { ValidationRule } from './validation'
export interface FormProps { /** * 表单布局 */ layout?: 'vertical' | 'horizontal' | 'inline' /** * 标签宽度(水平布局时) */ labelWidth?: string /** * 表单尺寸 */ size?: 'sm' | 'md' | 'lg' /** * 是否禁用整个表单 */ disabled?: boolean }
export interface FormEmits { submit: [data: Record<string, any>, isValid: boolean] 'field-change': [name: string, value: any] 'validation-change': [errors: Record<string, string>] }
defineOptions({ name: 'UiForm' })
const props = withDefaults(defineProps<FormProps>(), { layout: 'vertical', size: 'md', disabled: false })
const emit = defineEmits<FormEmits>()
// 表单验证器 const validator = new FormValidator()
// 表单数据 const formData = reactive<Record<string, any>>({})
// 错误信息 const errors = reactive<Record<string, string>>({})
// 计算样式类 const formClasses = computed(() => [ 'form', `form--${props.layout}`, `form--${props.size}`, { 'form--disabled': props.disabled } ])
// 注册字段 const registerField = (name: string, rules: ValidationRule[], initialValue: any = '') => { validator.addField(name, rules, initialValue) formData[name] = initialValue }
// 注销字段 const unregisterField = (name: string) => { validator.removeField(name) delete formData[name] delete errors[name] }
// 设置字段值 const setFieldValue = (name: string, value: any) => { formData[name] = value validator.setValue(name, value) // 验证字段 const isValid = validator.validateField(name, formData) const error = validator.getFieldError(name) if (error) { errors[name] = error } else { delete errors[name] } emit('field-change', name, value) emit('validation-change', { ...errors }) }
// 验证表单 const validate = (): boolean => { const isValid = validator.validateAll(formData) const newErrors = validator.getErrors() // 清空现有错误 Object.keys(errors).forEach(key => { delete errors[key] }) // 设置新错误 Object.assign(errors, newErrors) emit('validation-change', { ...errors }) return isValid }
// 重置表单 const reset = () => { validator.reset() // 清空数据和错误 Object.keys(formData).forEach(key => { formData[key] = '' }) Object.keys(errors).forEach(key => { delete errors[key] }) emit('validation-change', {}) }
// 提交表单 const handleSubmit = (event: Event) => { event.preventDefault() const isValid = validate() emit('submit', { ...formData }, isValid) }
// 提供给子组件的上下文 provide('form', { layout: props.layout, size: props.size, disabled: props.disabled, labelWidth: props.labelWidth, registerField, unregisterField, setFieldValue, getFieldValue: (name: string) => formData[name], getFieldError: (name: string) => errors[name] })
// 暴露方法 defineExpose({ validate, reset, setFieldValue, getFormData: () => ({ ...formData }), getErrors: () => ({ ...errors }) }) </script>
<style lang="scss" scoped> .form { &--vertical { .form-item { margin-bottom: var(--spacing-4); } } &--horizontal { .form-item { display: flex; align-items: flex-start; margin-bottom: var(--spacing-4); .form-item-label { flex-shrink: 0; margin-bottom: 0; margin-right: var(--spacing-3); padding-top: var(--spacing-2); } .form-item-content { flex: 1; } } } &--inline { display: flex; flex-wrap: wrap; gap: var(--spacing-4); .form-item { margin-bottom: 0; } } &--disabled { pointer-events: none; opacity: 0.6; } } </style>
|