form-vue

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Form Vue

Vue 3生产级表单实现方案

Production Vue 3 form patterns. Default stack: VeeValidate + Zod.
适用于生产环境的Vue 3表单实现方案。默认技术栈:VeeValidate + Zod

Quick Start

快速开始

bash
npm install vee-validate @vee-validate/zod zod
vue
<script setup lang="ts">
import { useForm, useField } from 'vee-validate';
import { toTypedSchema } from '@vee-validate/zod';
import { z } from 'zod';

// 1. Define schema
const schema = toTypedSchema(z.object({
  email: z.string().email('Invalid email'),
  password: z.string().min(8, 'Min 8 characters')
}));

// 2. Use form
const { handleSubmit, errors } = useForm({ validationSchema: schema });
const { value: email } = useField('email');
const { value: password } = useField('password');

// 3. Handle submit
const onSubmit = handleSubmit((values) => {
  console.log(values);
});
</script>

<template>
  <form @submit="onSubmit">
    <input v-model="email" type="email" autocomplete="email" />
    <span v-if="errors.email">{{ errors.email }}</span>
    
    <input v-model="password" type="password" autocomplete="current-password" />
    <span v-if="errors.password">{{ errors.password }}</span>
    
    <button type="submit">Sign in</button>
  </form>
</template>
bash
npm install vee-validate @vee-validate/zod zod
vue
<script setup lang="ts">
import { useForm, useField } from 'vee-validate';
import { toTypedSchema } from '@vee-validate/zod';
import { z } from 'zod';

// 1. 定义验证Schema
const schema = toTypedSchema(z.object({
  email: z.string().email('无效的邮箱格式'),
  password: z.string().min(8, '密码长度至少8位')
}));

// 2. 初始化表单
const { handleSubmit, errors } = useForm({ validationSchema: schema });
const { value: email } = useField('email');
const { value: password } = useField('password');

// 3. 处理提交逻辑
const onSubmit = handleSubmit((values) => {
  console.log(values);
});
</script>

<template>
  <form @submit="onSubmit">
    <input v-model="email" type="email" autocomplete="email" />
    <span v-if="errors.email">{{ errors.email }}</span>
    
    <input v-model="password" type="password" autocomplete="current-password" />
    <span v-if="errors.password">{{ errors.password }}</span>
    
    <button type="submit">登录</button>
  </form>
</template>

When to Use Which

如何选择方案

CriteriaVeeValidateVuelidate
API StyleDeclarative (schema)Imperative (rules)
Zod Integration✅ Native adapterManual
Bundle Size~15KB~10KB
Component Support✅ Built-in Field/FormManual binding
Async Validation✅ Built-in✅ Built-in
Cross-field Validation✅ EasyMore manual
Learning CurveLowMedium
Default: VeeValidate — Better DX, native Zod support.
Use Vuelidate when:
  • Need extremely fine-grained control
  • Existing Vuelidate codebase
  • Prefer imperative validation style
评估维度VeeValidateVuelidate
API风格声明式(基于Schema)命令式(基于规则)
Zod集成✅ 原生适配手动实现
包体积~15KB~10KB
组件支持✅ 内置Field/Form组件手动绑定
异步验证✅ 原生支持✅ 原生支持
跨字段验证✅ 简单易实现需手动处理
学习曲线中等
默认推荐:VeeValidate — 开发体验更优,原生支持Zod。
以下场景推荐使用Vuelidate:
  • 需要极致的细粒度控制
  • 已有基于Vuelidate的代码库
  • 偏好命令式验证风格

VeeValidate Patterns

VeeValidate 实现模式

Basic Form with Composition API

基于Composition API的基础表单

vue
<script setup lang="ts">
import { useForm, useField } from 'vee-validate';
import { toTypedSchema } from '@vee-validate/zod';
import { loginSchema, type LoginFormData } from './schemas';

const emit = defineEmits<{
  submit: [data: LoginFormData]
}>();

// Form setup
const { handleSubmit, errors, meta } = useForm<LoginFormData>({
  validationSchema: toTypedSchema(loginSchema),
  validateOnMount: false
});

// Field setup
const { value: email, errorMessage: emailError, meta: emailMeta } = useField('email');
const { value: password, errorMessage: passwordError, meta: passwordMeta } = useField('password');
const { value: rememberMe } = useField('rememberMe');

// Submit handler
const onSubmit = handleSubmit((values) => {
  emit('submit', values);
});
</script>

<template>
  <form @submit="onSubmit" novalidate>
    <div class="form-field" :class="{ 'has-error': emailMeta.touched && emailError }">
      <label for="email">Email</label>
      <input
        id="email"
        v-model="email"
        type="email"
        autocomplete="email"
        :aria-invalid="emailMeta.touched && !!emailError"
        :aria-describedby="emailError ? 'email-error' : undefined"
      />
      <span v-if="emailMeta.touched && emailError" id="email-error" role="alert">
        {{ emailError }}
      </span>
    </div>

    <div class="form-field" :class="{ 'has-error': passwordMeta.touched && passwordError }">
      <label for="password">Password</label>
      <input
        id="password"
        v-model="password"
        type="password"
        autocomplete="current-password"
        :aria-invalid="passwordMeta.touched && !!passwordError"
        :aria-describedby="passwordError ? 'password-error' : undefined"
      />
      <span v-if="passwordMeta.touched && passwordError" id="password-error" role="alert">
        {{ passwordError }}
      </span>
    </div>

    <label class="checkbox">
      <input v-model="rememberMe" type="checkbox" />
      Remember me
    </label>

    <button type="submit" :disabled="meta.pending">
      {{ meta.pending ? 'Signing in...' : 'Sign in' }}
    </button>
  </form>
</template>
vue
<script setup lang="ts">
import { useForm, useField } from 'vee-validate';
import { toTypedSchema } from '@vee-validate/zod';
import { loginSchema, type LoginFormData } from './schemas';

const emit = defineEmits<{
  submit: [data: LoginFormData]
}>();

// 表单初始化设置
const { handleSubmit, errors, meta } = useForm<LoginFormData>({
  validationSchema: toTypedSchema(loginSchema),
  validateOnMount: false
});

// 字段初始化
const { value: email, errorMessage: emailError, meta: emailMeta } = useField('email');
const { value: password, errorMessage: passwordError, meta: passwordMeta } = useField('password');
const { value: rememberMe } = useField('rememberMe');

// 提交处理函数
const onSubmit = handleSubmit((values) => {
  emit('submit', values);
});
</script>

<template>
  <form @submit="onSubmit" novalidate>
    <div class="form-field" :class="{ 'has-error': emailMeta.touched && emailError }">
      <label for="email">邮箱</label>
      <input
        id="email"
        v-model="email"
        type="email"
        autocomplete="email"
        :aria-invalid="emailMeta.touched && !!emailError"
        :aria-describedby="emailError ? 'email-error' : undefined"
      />
      <span v-if="emailMeta.touched && emailError" id="email-error" role="alert">
        {{ emailError }}
      </span>
    </div>

    <div class="form-field" :class="{ 'has-error': passwordMeta.touched && passwordError }">
      <label for="password">密码</label>
      <input
        id="password"
        v-model="password"
        type="password"
        autocomplete="current-password"
        :aria-invalid="passwordMeta.touched && !!passwordError"
        :aria-describedby="passwordError ? 'password-error' : undefined"
      />
      <span v-if="passwordMeta.touched && passwordError" id="password-error" role="alert">
        {{ passwordError }}
      </span>
    </div>

    <label class="checkbox">
      <input v-model="rememberMe" type="checkbox" />
      记住我
    </label>

    <button type="submit" :disabled="meta.pending">
      {{ meta.pending ? '登录中...' : '登录' }}
    </button>
  </form>
</template>

Reusable FormField Component

可复用FormField组件

vue
<!-- components/FormField.vue -->
<script setup lang="ts">
import { useField } from 'vee-validate';
import { computed, useId } from 'vue';

interface Props {
  name: string;
  label: string;
  type?: string;
  autocomplete?: string;
  hint?: string;
  required?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
  type: 'text'
});

const fieldId = useId();
const errorId = `${fieldId}-error`;
const hintId = `${fieldId}-hint`;

const { value, errorMessage, meta } = useField(() => props.name);

const showError = computed(() => meta.touched && !!errorMessage.value);
const showValid = computed(() => meta.touched && !errorMessage.value && meta.valid);

const describedBy = computed(() => {
  const ids = [];
  if (props.hint) ids.push(hintId);
  if (showError.value) ids.push(errorId);
  return ids.length > 0 ? ids.join(' ') : undefined;
});
</script>

<template>
  <div 
    class="form-field" 
    :class="{ 
      'form-field--error': showError,
      'form-field--valid': showValid
    }"
  >
    <label :for="fieldId">
      {{ label }}
      <span v-if="required" class="required" aria-hidden="true">*</span>
    </label>
    
    <span v-if="hint" :id="hintId" class="hint">{{ hint }}</span>
    
    <div class="input-wrapper">
      <input
        :id="fieldId"
        v-model="value"
        :type="type"
        :autocomplete="autocomplete"
        :aria-invalid="showError"
        :aria-describedby="describedBy"
        :aria-required="required"
      />
      
      <span v-if="showValid" class="icon icon--valid" aria-hidden="true">✓</span>
      <span v-if="showError" class="icon icon--error" aria-hidden="true">✗</span>
    </div>
    
    <span v-if="showError" :id="errorId" class="error" role="alert">
      {{ errorMessage }}
    </span>
  </div>
</template>
vue
<!-- components/FormField.vue -->
<script setup lang="ts">
import { useField } from 'vee-validate';
import { computed, useId } from 'vue';

interface Props {
  name: string;
  label: string;
  type?: string;
  autocomplete?: string;
  hint?: string;
  required?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
  type: 'text'
});

const fieldId = useId();
const errorId = `${fieldId}-error`;
const hintId = `${fieldId}-hint`;

const { value, errorMessage, meta } = useField(() => props.name);

const showError = computed(() => meta.touched && !!errorMessage.value);
const showValid = computed(() => meta.touched && !errorMessage.value && meta.valid);

const describedBy = computed(() => {
  const ids = [];
  if (props.hint) ids.push(hintId);
  if (showError.value) ids.push(errorId);
  return ids.length > 0 ? ids.join(' ') : undefined;
});
</script>

<template>
  <div 
    class="form-field" 
    :class="{ 
      'form-field--error': showError,
      'form-field--valid': showValid
    }"
  >
    <label :for="fieldId">
      {{ label }}
      <span v-if="required" class="required" aria-hidden="true">*</span>
    </label>
    
    <span v-if="hint" :id="hintId" class="hint">{{ hint }}</span>
    
    <div class="input-wrapper">
      <input
        :id="fieldId"
        v-model="value"
        :type="type"
        :autocomplete="autocomplete"
        :aria-invalid="showError"
        :aria-describedby="describedBy"
        :aria-required="required"
      />
      
      <span v-if="showValid" class="icon icon--valid" aria-hidden="true">✓</span>
      <span v-if="showError" class="icon icon--error" aria-hidden="true">✗</span>
    </div>
    
    <span v-if="showError" :id="errorId" class="error" role="alert">
      {{ errorMessage }}
    </span>
  </div>
</template>

Using FormField Component

FormField组件的使用示例

vue
<script setup lang="ts">
import { useForm } from 'vee-validate';
import { toTypedSchema } from '@vee-validate/zod';
import { loginSchema } from './schemas';
import FormField from './FormField.vue';

const { handleSubmit, meta } = useForm({
  validationSchema: toTypedSchema(loginSchema)
});

const onSubmit = handleSubmit((values) => {
  console.log(values);
});
</script>

<template>
  <form @submit="onSubmit" novalidate>
    <FormField
      name="email"
      label="Email"
      type="email"
      autocomplete="email"
      required
    />
    
    <FormField
      name="password"
      label="Password"
      type="password"
      autocomplete="current-password"
      required
    />
    
    <button type="submit" :disabled="meta.pending">
      Sign in
    </button>
  </form>
</template>
vue
<script setup lang="ts">
import { useForm } from 'vee-validate';
import { toTypedSchema } from '@vee-validate/zod';
import { loginSchema } from './schemas';
import FormField from './FormField.vue';

const { handleSubmit, meta } = useForm({
  validationSchema: toTypedSchema(loginSchema)
});

const onSubmit = handleSubmit((values) => {
  console.log(values);
});
</script>

<template>
  <form @submit="onSubmit" novalidate>
    <FormField
      name="email"
      label="邮箱"
      type="email"
      autocomplete="email"
      required
    />
    
    <FormField
      name="password"
      label="密码"
      type="password"
      autocomplete="current-password"
      required
    />
    
    <button type="submit" :disabled="meta.pending">
      登录
    </button>
  </form>
</template>

Form with Initial Values

带初始值的表单

vue
<script setup lang="ts">
import { useForm } from 'vee-validate';
import { toTypedSchema } from '@vee-validate/zod';
import { profileSchema } from './schemas';

interface Props {
  initialData?: {
    firstName: string;
    lastName: string;
    email: string;
  }
}

const props = defineProps<Props>();

const { handleSubmit, resetForm } = useForm({
  validationSchema: toTypedSchema(profileSchema),
  initialValues: props.initialData
});

// Reset to initial values
const handleCancel = () => {
  resetForm();
};

// Reset to new values
const handleReset = (newValues: typeof props.initialData) => {
  resetForm({ values: newValues });
};
</script>
vue
<script setup lang="ts">
import { useForm } from 'vee-validate';
import { toTypedSchema } from '@vee-validate/zod';
import { profileSchema } from './schemas';

interface Props {
  initialData?: {
    firstName: string;
    lastName: string;
    email: string;
  }
}

const props = defineProps<Props>();

const { handleSubmit, resetForm } = useForm({
  validationSchema: toTypedSchema(profileSchema),
  initialValues: props.initialData
});

// 重置为初始值
const handleCancel = () => {
  resetForm();
};

// 重置为新值
const handleReset = (newValues: typeof props.initialData) => {
  resetForm({ values: newValues });
};
</script>

Async Validation (Username Check)

异步验证(用户名查重)

vue
<script setup lang="ts">
import { useField } from 'vee-validate';
import { z } from 'zod';
import { toTypedSchema } from '@vee-validate/zod';

// Schema with async validation
const usernameSchema = z.string()
  .min(3, 'Username must be at least 3 characters')
  .refine(async (username) => {
    const response = await fetch(`/api/check-username?u=${username}`);
    const { available } = await response.json();
    return available;
  }, 'Username is already taken');

const { value, errorMessage, meta } = useField('username', toTypedSchema(usernameSchema));
</script>

<template>
  <div class="form-field">
    <label for="username">Username</label>
    <input
      id="username"
      v-model="value"
      type="text"
      autocomplete="username"
    />
    <span v-if="meta.pending" class="loading">Checking...</span>
    <span v-else-if="errorMessage" class="error">{{ errorMessage }}</span>
  </div>
</template>
vue
<script setup lang="ts">
import { useField } from 'vee-validate';
import { z } from 'zod';
import { toTypedSchema } from '@vee-validate/zod';

// 包含异步验证的Schema
const usernameSchema = z.string()
  .min(3, '用户名长度至少3位')
  .refine(async (username) => {
    const response = await fetch(`/api/check-username?u=${username}`);
    const { available } = await response.json();
    return available;
  }, '该用户名已被占用');

const { value, errorMessage, meta } = useField('username', toTypedSchema(usernameSchema));
</script>

<template>
  <div class="form-field">
    <label for="username">用户名</label>
    <input
      id="username"
      v-model="value"
      type="text"
      autocomplete="username"
    />
    <span v-if="meta.pending" class="loading">检查中...</span>
    <span v-else-if="errorMessage" class="error">{{ errorMessage }}</span>
  </div>
</template>

Cross-Field Validation (Password Confirmation)

跨字段验证(密码确认)

vue
<script setup lang="ts">
import { useForm, useField } from 'vee-validate';
import { toTypedSchema } from '@vee-validate/zod';
import { z } from 'zod';

const schema = toTypedSchema(
  z.object({
    password: z.string().min(8, 'Min 8 characters'),
    confirmPassword: z.string()
  }).refine(data => data.password === data.confirmPassword, {
    message: 'Passwords do not match',
    path: ['confirmPassword']
  })
);

const { handleSubmit } = useForm({ validationSchema: schema });
const { value: password } = useField('password');
const { value: confirmPassword, errorMessage: confirmError } = useField('confirmPassword');
</script>

<template>
  <form @submit="handleSubmit(onSubmit)">
    <input v-model="password" type="password" placeholder="Password" />
    <input v-model="confirmPassword" type="password" placeholder="Confirm password" />
    <span v-if="confirmError">{{ confirmError }}</span>
  </form>
</template>
vue
<script setup lang="ts">
import { useForm, useField } from 'vee-validate';
import { toTypedSchema } from '@vee-validate/zod';
import { z } from 'zod';

const schema = toTypedSchema(
  z.object({
    password: z.string().min(8, '密码长度至少8位'),
    confirmPassword: z.string()
  }).refine(data => data.password === data.confirmPassword, {
    message: '两次输入的密码不一致',
    path: ['confirmPassword']
  })
);

const { handleSubmit } = useForm({ validationSchema: schema });
const { value: password } = useField('password');
const { value: confirmPassword, errorMessage: confirmError } = useField('confirmPassword');
</script>

<template>
  <form @submit="handleSubmit(onSubmit)">
    <input v-model="password" type="password" placeholder="密码" />
    <input v-model="confirmPassword" type="password" placeholder="确认密码" />
    <span v-if="confirmError">{{ confirmError }}</span>
  </form>
</template>

Field Arrays (Dynamic Fields)

动态字段列表(Field Arrays)

vue
<script setup lang="ts">
import { useForm, useFieldArray } from 'vee-validate';
import { toTypedSchema } from '@vee-validate/zod';
import { z } from 'zod';

const schema = toTypedSchema(z.object({
  teammates: z.array(z.object({
    name: z.string().min(1, 'Name required'),
    email: z.string().email('Invalid email')
  })).min(1, 'Add at least one teammate')
}));

const { handleSubmit } = useForm({
  validationSchema: schema,
  initialValues: {
    teammates: [{ name: '', email: '' }]
  }
});

const { fields, push, remove } = useFieldArray('teammates');
</script>

<template>
  <form @submit="handleSubmit(onSubmit)">
    <div v-for="(field, index) in fields" :key="field.key">
      <FormField :name="`teammates[${index}].name`" label="Name" />
      <FormField :name="`teammates[${index}].email`" label="Email" type="email" />
      <button type="button" @click="remove(index)" v-if="fields.length > 1">
        Remove
      </button>
    </div>
    
    <button type="button" @click="push({ name: '', email: '' })">
      Add teammate
    </button>
    
    <button type="submit">Submit</button>
  </form>
</template>
vue
<script setup lang="ts">
import { useForm, useFieldArray } from 'vee-validate';
import { toTypedSchema } from '@vee-validate/zod';
import { z } from 'zod';

const schema = toTypedSchema(z.object({
  teammates: z.array(z.object({
    name: z.string().min(1, '请输入姓名'),
    email: z.string().email('无效的邮箱格式')
  })).min(1, '至少添加一位团队成员')
}));

const { handleSubmit } = useForm({
  validationSchema: schema,
  initialValues: {
    teammates: [{ name: '', email: '' }]
  }
});

const { fields, push, remove } = useFieldArray('teammates');
</script>

<template>
  <form @submit="handleSubmit(onSubmit)">
    <div v-for="(field, index) in fields" :key="field.key">
      <FormField :name="`teammates[${index}].name`" label="姓名" />
      <FormField :name="`teammates[${index}].email`" label="邮箱" type="email" />
      <button type="button" @click="remove(index)" v-if="fields.length > 1">
        删除
      </button>
    </div>
    
    <button type="button" @click="push({ name: '', email: '' })">
      添加团队成员
    </button>
    
    <button type="submit">提交</button>
  </form>
</template>

Vuelidate Patterns

Vuelidate 实现模式

Basic Form

基础表单

vue
<script setup lang="ts">
import { reactive, computed } from 'vue';
import { useVuelidate } from '@vuelidate/core';
import { required, email, minLength } from '@vuelidate/validators';

const state = reactive({
  email: '',
  password: ''
});

const rules = computed(() => ({
  email: { required, email },
  password: { required, minLength: minLength(8) }
}));

const v$ = useVuelidate(rules, state);

const onSubmit = async () => {
  const isValid = await v$.value.$validate();
  if (!isValid) return;
  
  console.log('Submitting:', state);
};
</script>

<template>
  <form @submit.prevent="onSubmit">
    <div class="form-field" :class="{ 'has-error': v$.email.$error }">
      <label for="email">Email</label>
      <input
        id="email"
        v-model="state.email"
        type="email"
        autocomplete="email"
        @blur="v$.email.$touch()"
      />
      <span v-if="v$.email.$error" class="error">
        {{ v$.email.$errors[0]?.$message }}
      </span>
    </div>

    <div class="form-field" :class="{ 'has-error': v$.password.$error }">
      <label for="password">Password</label>
      <input
        id="password"
        v-model="state.password"
        type="password"
        autocomplete="current-password"
        @blur="v$.password.$touch()"
      />
      <span v-if="v$.password.$error" class="error">
        {{ v$.password.$errors[0]?.$message }}
      </span>
    </div>

    <button type="submit" :disabled="v$.$pending">
      Sign in
    </button>
  </form>
</template>
vue
<script setup lang="ts">
import { reactive, computed } from 'vue';
import { useVuelidate } from '@vuelidate/core';
import { required, email, minLength } from '@vuelidate/validators';

const state = reactive({
  email: '',
  password: ''
});

const rules = computed(() => ({
  email: { required, email },
  password: { required, minLength: minLength(8) }
}));

const v$ = useVuelidate(rules, state);

const onSubmit = async () => {
  const isValid = await v$.value.$validate();
  if (!isValid) return;
  
  console.log('提交数据:', state);
};
</script>

<template>
  <form @submit.prevent="onSubmit">
    <div class="form-field" :class="{ 'has-error': v$.email.$error }">
      <label for="email">邮箱</label>
      <input
        id="email"
        v-model="state.email"
        type="email"
        autocomplete="email"
        @blur="v$.email.$touch()"
      />
      <span v-if="v$.email.$error" class="error">
        {{ v$.email.$errors[0]?.$message }}
      </span>
    </div>

    <div class="form-field" :class="{ 'has-error': v$.password.$error }">
      <label for="password">密码</label>
      <input
        id="password"
        v-model="state.password"
        type="password"
        autocomplete="current-password"
        @blur="v$.password.$touch()"
      />
      <span v-if="v$.password.$error" class="error">
        {{ v$.password.$errors[0]?.$message }}
      </span>
    </div>

    <button type="submit" :disabled="v$.$pending">
      登录
    </button>
  </form>
</template>

Vuelidate with Zod

Vuelidate结合Zod使用

vue
<script setup lang="ts">
import { reactive } from 'vue';
import { useVuelidate } from '@vuelidate/core';
import { helpers } from '@vuelidate/validators';
import { z } from 'zod';

// Create Vuelidate validator from Zod schema
function zodValidator<T extends z.ZodType>(schema: T) {
  return helpers.withMessage(
    (value: unknown) => {
      const result = schema.safeParse(value);
      if (!result.success) {
        return result.error.errors[0]?.message || 'Invalid';
      }
      return true;
    },
    (value: unknown) => {
      const result = schema.safeParse(value);
      return result.success;
    }
  );
}

const emailSchema = z.string().email('Please enter a valid email');
const passwordSchema = z.string().min(8, 'Password must be at least 8 characters');

const state = reactive({
  email: '',
  password: ''
});

const rules = {
  email: { zodValidator: zodValidator(emailSchema) },
  password: { zodValidator: zodValidator(passwordSchema) }
};

const v$ = useVuelidate(rules, state);
</script>
vue
<script setup lang="ts">
import { reactive } from 'vue';
import { useVuelidate } from '@vuelidate/core';
import { helpers } from '@vuelidate/validators';
import { z } from 'zod';

// 从Zod Schema创建Vuelidate验证器
function zodValidator<T extends z.ZodType>(schema: T) {
  return helpers.withMessage(
    (value: unknown) => {
      const result = schema.safeParse(value);
      if (!result.success) {
        return result.error.errors[0]?.message || '输入无效';
      }
      return true;
    },
    (value: unknown) => {
      const result = schema.safeParse(value);
      return result.success;
    }
  );
}

const emailSchema = z.string().email('请输入有效的邮箱地址');
const passwordSchema = z.string().min(8, '密码长度至少8位');

const state = reactive({
  email: '',
  password: ''
});

const rules = {
  email: { zodValidator: zodValidator(emailSchema) },
  password: { zodValidator: zodValidator(passwordSchema) }
};

const v$ = useVuelidate(rules, state);
</script>

Shared Zod Schemas

共享Zod Schema

typescript
// schemas/index.ts (shared between React and Vue)
import { z } from 'zod';

export const loginSchema = z.object({
  email: z.string().min(1, 'Email is required').email('Invalid email'),
  password: z.string().min(1, 'Password is required'),
  rememberMe: z.boolean().optional().default(false)
});

export type LoginFormData = z.infer<typeof loginSchema>;

// VeeValidate usage
import { toTypedSchema } from '@vee-validate/zod';
const veeSchema = toTypedSchema(loginSchema);

// React Hook Form usage
import { zodResolver } from '@hookform/resolvers/zod';
const rhfResolver = zodResolver(loginSchema);
typescript
// schemas/index.ts(可在React和Vue项目间共享)
import { z } from 'zod';

export const loginSchema = z.object({
  email: z.string().min(1, '请输入邮箱地址').email('无效的邮箱格式'),
  password: z.string().min(1, '请输入密码'),
  rememberMe: z.boolean().optional().default(false)
});

export type LoginFormData = z.infer<typeof loginSchema>;

// VeeValidate使用方式
import { toTypedSchema } from '@vee-validate/zod';
const veeSchema = toTypedSchema(loginSchema);

// React Hook Form使用方式
import { zodResolver } from '@hookform/resolvers/zod';
const rhfResolver = zodResolver(loginSchema);

File Structure

文件结构

form-vue/
├── SKILL.md
├── references/
│   ├── veevalidate-patterns.md   # VeeValidate deep-dive
│   └── vuelidate-patterns.md     # Vuelidate deep-dive
└── scripts/
    ├── veevalidate-form.vue      # VeeValidate patterns
    ├── vuelidate-form.vue        # Vuelidate patterns
    ├── form-field.vue            # Reusable field component
    └── schemas/                  # Shared with form-validation
        ├── auth.ts
        ├── profile.ts
        └── payment.ts
form-vue/
├── SKILL.md
├── references/
│   ├── veevalidate-patterns.md   # VeeValidate深度解析
│   └── vuelidate-patterns.md     # Vuelidate深度解析
└── scripts/
    ├── veevalidate-form.vue      # VeeValidate实现示例
    ├── vuelidate-form.vue        # Vuelidate实现示例
    ├── form-field.vue            # 可复用字段组件
    └── schemas/                  # 与表单验证模块共享
        ├── auth.ts
        ├── profile.ts
        └── payment.ts

Reference

参考文档

  • references/veevalidate-patterns.md
    — Complete VeeValidate patterns
  • references/vuelidate-patterns.md
    — Vuelidate patterns
  • references/veevalidate-patterns.md
    — 完整VeeValidate实现模式
  • references/vuelidate-patterns.md
    — Vuelidate实现模式