Skip to content

useForm 表单管理

表单数据管理组合函数,提供表单验证、数据处理、提交等完整的表单操作功能。

📋 基础用法

简单表单

vue
<template>
  <el-form
    ref="formRef"
    :model="formData"
    :rules="formRules"
    label-width="100px"
  >
    <el-form-item label="用户名" prop="username">
      <el-input
        v-model="formData.username"
        placeholder="请输入用户名"
        clearable
      />
    </el-form-item>

    <el-form-item label="邮箱" prop="email">
      <el-input
        v-model="formData.email"
        placeholder="请输入邮箱"
        clearable
      />
    </el-form-item>

    <el-form-item label="手机号" prop="phone">
      <el-input
        v-model="formData.phone"
        placeholder="请输入手机号"
        clearable
      />
    </el-form-item>

    <el-form-item label="状态" prop="status">
      <el-radio-group v-model="formData.status">
        <el-radio value="0">正常</el-radio>
        <el-radio value="1">停用</el-radio>
      </el-radio-group>
    </el-form-item>

    <el-form-item label="角色" prop="roleIds">
      <el-select
        v-model="formData.roleIds"
        placeholder="请选择角色"
        multiple
        clearable
      >
        <el-option
          v-for="role in roleOptions"
          :key="role.value"
          :label="role.label"
          :value="role.value"
        />
      </el-select>
    </el-form-item>

    <el-form-item label="备注" prop="remark">
      <el-input
        v-model="formData.remark"
        type="textarea"
        :rows="4"
        placeholder="请输入备注"
      />
    </el-form-item>

    <el-form-item>
      <el-button
        type="primary"
        :loading="submitLoading"
        @click="handleSubmit"
      >
        提交
      </el-button>
      <el-button @click="handleReset">
        重置
      </el-button>
      <el-button @click="handleCancel">
        取消
      </el-button>
    </el-form-item>
  </el-form>
</template>

<script setup lang="ts">
import { useForm } from '@/composables/use-form'
import { addUser, updateUser } from '@/api/system/user'

// 表单数据类型
interface UserForm {
  id?: number
  username: string
  email: string
  phone: string
  status: string
  roleIds: number[]
  remark?: string
}

// 表单验证规则
const formRules = {
  username: [
    { required: true, message: '请输入用户名', trigger: 'blur' },
    { min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' }
  ],
  email: [
    { required: true, message: '请输入邮箱', trigger: 'blur' },
    { type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
  ],
  phone: [
    { required: true, message: '请输入手机号', trigger: 'blur' },
    { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
  ],
  status: [
    { required: true, message: '请选择状态', trigger: 'change' }
  ]
}

const {
  // 表单状态
  formRef,
  formData,
  formRules: rules,
  submitLoading,

  // 表单方法
  handleSubmit,
  handleReset,
  validateForm,
  setFormData,
  resetFormData
} = useForm<UserForm>({
  // 默认数据
  defaultData: {
    username: '',
    email: '',
    phone: '',
    status: '0',
    roleIds: [],
    remark: ''
  },

  // 验证规则
  rules: formRules,

  // 提交处理
  onSubmit: async (data) => {
    if (data.id) {
      await updateUser(data)
    } else {
      await addUser(data)
    }
  },

  // 提交成功回调
  onSuccess: () => {
    ElMessage.success('操作成功')
    handleCancel()
  },

  // 提交失败回调
  onError: (error) => {
    ElMessage.error(error.message || '操作失败')
  }
})

// 角色选项
const roleOptions = ref([
  { label: '管理员', value: 1 },
  { label: '普通用户', value: 2 }
])

// 取消操作
const handleCancel = () => {
  console.log('取消操作')
}

// 如果是编辑模式,设置初始数据
onMounted(() => {
  const editData = {
    id: 1,
    username: 'admin',
    email: 'admin@example.com',
    phone: '13888888888',
    status: '0',
    roleIds: [1],
    remark: '管理员用户'
  }
  setFormData(editData)
})
</script>

🎯 核心功能

useForm 实现

typescript
// composables/use-form.ts
import { ref, reactive, computed } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage } from 'element-plus'

export interface FormConfig<T = any> {
  // 默认数据
  defaultData: T

  // 验证规则
  rules?: FormRules

  // 提交处理函数
  onSubmit?: (data: T) => Promise<any>

  // 成功回调
  onSuccess?: (result: any) => void

  // 失败回调
  onError?: (error: any) => void

  // 验证失败回调
  onValidateError?: (errors: any) => void

  // 提交前处理
  beforeSubmit?: (data: T) => T | Promise<T>

  // 自动重置
  autoReset?: boolean

  // 显示消息
  showMessage?: boolean
}

export function useForm<T extends Record<string, any>>(config: FormConfig<T>) {
  const {
    defaultData,
    rules,
    onSubmit,
    onSuccess,
    onError,
    onValidateError,
    beforeSubmit,
    autoReset = false,
    showMessage = true
  } = config

  // 表单引用
  const formRef = ref<FormInstance>()

  // 表单数据
  const formData = reactive<T>({ ...defaultData })

  // 提交状态
  const submitLoading = ref(false)

  // 验证状态
  const validating = ref(false)

  // 表单验证规则
  const formRules = computed(() => rules)

  // 表单是否有变更
  const isDirty = computed(() => {
    return JSON.stringify(formData) !== JSON.stringify(defaultData)
  })

  /**
   * 验证表单
   */
  const validateForm = async (): Promise<boolean> => {
    if (!formRef.value) return false

    validating.value = true

    try {
      await formRef.value.validate()
      return true
    } catch (errors) {
      if (showMessage) {
        ElMessage.error('表单验证失败,请检查输入')
      }
      onValidateError?.(errors)
      return false
    } finally {
      validating.value = false
    }
  }

  /**
   * 验证指定字段
   */
  const validateField = async (field: keyof T): Promise<boolean> => {
    if (!formRef.value) return false

    try {
      await formRef.value.validateField(field as string)
      return true
    } catch (error) {
      return false
    }
  }

  /**
   * 清除验证结果
   */
  const clearValidate = (fields?: (keyof T)[]) => {
    if (!formRef.value) return

    if (fields) {
      formRef.value.clearValidate(fields as string[])
    } else {
      formRef.value.clearValidate()
    }
  }

  /**
   * 设置表单数据
   */
  const setFormData = (data: Partial<T>) => {
    Object.assign(formData, data)
  }

  /**
   * 重置表单数据
   */
  const resetFormData = () => {
    Object.assign(formData, defaultData)
    clearValidate()
  }

  /**
   * 重置表单
   */
  const handleReset = () => {
    if (formRef.value) {
      formRef.value.resetFields()
    } else {
      resetFormData()
    }
  }

  /**
   * 提交表单
   */
  const handleSubmit = async () => {
    if (!onSubmit) {
      console.warn('未配置提交处理函数')
      return
    }

    // 验证表单
    const isValid = await validateForm()
    if (!isValid) return

    submitLoading.value = true

    try {
      // 处理提交前数据
      let submitData = { ...formData }
      if (beforeSubmit) {
        submitData = await beforeSubmit(submitData)
      }

      // 执行提交
      const result = await onSubmit(submitData)

      // 成功回调
      onSuccess?.(result)

      // 自动重置
      if (autoReset) {
        handleReset()
      }

      return result
    } catch (error) {
      console.error('表单提交失败:', error)
      onError?.(error)
      throw error
    } finally {
      submitLoading.value = false
    }
  }

  /**
   * 获取表单数据
   */
  const getFormData = (): T => {
    return { ...formData }
  }

  /**
   * 获取变更的字段
   */
  const getChangedFields = (): Partial<T> => {
    const changed: Partial<T> = {}

    Object.keys(formData).forEach(key => {
      if (formData[key] !== defaultData[key]) {
        changed[key] = formData[key]
      }
    })

    return changed
  }

  return {
    // 表单状态
    formRef,
    formData,
    formRules,
    submitLoading,
    validating,
    isDirty,

    // 验证方法
    validateForm,
    validateField,
    clearValidate,

    // 数据操作
    setFormData,
    resetFormData,
    getFormData,
    getChangedFields,

    // 表单操作
    handleSubmit,
    handleReset
  }
}

🔄 高级功能

动态表单

typescript
// composables/use-dynamic-form.ts
export interface FormField {
  key: string
  label: string
  type: 'input' | 'select' | 'radio' | 'checkbox' | 'date' | 'textarea'
  required?: boolean
  placeholder?: string
  options?: Array<{ label: string; value: any }>
  rules?: any[]
  props?: Record<string, any>
  visible?: boolean | ((formData: any) => boolean)
  disabled?: boolean | ((formData: any) => boolean)
}

export function useDynamicForm(fields: Ref<FormField[]>) {
  const formData = reactive<Record<string, any>>({})
  const formRules = computed(() => {
    const rules: Record<string, any> = {}

    fields.value.forEach(field => {
      if (field.required || field.rules) {
        rules[field.key] = [
          ...(field.required ? [{ required: true, message: `请输入${field.label}`, trigger: 'blur' }] : []),
          ...(field.rules || [])
        ]
      }
    })

    return rules
  })

  // 可见字段
  const visibleFields = computed(() => {
    return fields.value.filter(field => {
      if (typeof field.visible === 'function') {
        return field.visible(formData)
      }
      return field.visible !== false
    })
  })

  // 字段是否禁用
  const isFieldDisabled = (field: FormField) => {
    if (typeof field.disabled === 'function') {
      return field.disabled(formData)
    }
    return field.disabled === true
  }

  // 初始化表单数据
  const initFormData = () => {
    fields.value.forEach(field => {
      if (!(field.key in formData)) {
        formData[field.key] = getDefaultValue(field.type)
      }
    })
  }

  // 获取默认值
  const getDefaultValue = (type: string) => {
    switch (type) {
      case 'checkbox':
        return []
      case 'radio':
      case 'select':
        return ''
      default:
        return ''
    }
  }

  // 监听字段变化,初始化新字段
  watch(fields, () => {
    initFormData()
  }, { immediate: true, deep: true })

  return {
    formData,
    formRules,
    visibleFields,
    isFieldDisabled,
    initFormData
  }
}

表单联动

typescript
// composables/use-form-linkage.ts
export interface LinkageRule {
  trigger: string // 触发字段
  target: string // 目标字段
  condition: (value: any, formData: any) => boolean
  action: 'show' | 'hide' | 'enable' | 'disable' | 'setValue' | 'setOptions'
  value?: any
  options?: any[]
}

export function useFormLinkage(
  formData: any,
  rules: LinkageRule[]
) {
  const fieldStates = reactive<Record<string, {
    visible: boolean
    disabled: boolean
    options: any[]
  }>>({})

  // 初始化字段状态
  const initFieldStates = () => {
    rules.forEach(rule => {
      if (!fieldStates[rule.target]) {
        fieldStates[rule.target] = {
          visible: true,
          disabled: false,
          options: []
        }
      }
    })
  }

  // 执行联动规则
  const executeRules = (changedField?: string) => {
    rules.forEach(rule => {
      // 如果指定了变更字段,只处理相关规则
      if (changedField && rule.trigger !== changedField) {
        return
      }

      const triggerValue = formData[rule.trigger]
      const shouldExecute = rule.condition(triggerValue, formData)

      if (shouldExecute) {
        executeAction(rule)
      }
    })
  }

  // 执行动作
  const executeAction = (rule: LinkageRule) => {
    const targetState = fieldStates[rule.target]

    switch (rule.action) {
      case 'show':
        targetState.visible = true
        break
      case 'hide':
        targetState.visible = false
        break
      case 'enable':
        targetState.disabled = false
        break
      case 'disable':
        targetState.disabled = true
        break
      case 'setValue':
        formData[rule.target] = rule.value
        break
      case 'setOptions':
        targetState.options = rule.options || []
        break
    }
  }

  // 监听表单数据变化
  watch(formData, (newData, oldData) => {
    // 找出变更的字段
    const changedFields = Object.keys(newData).filter(
      key => newData[key] !== oldData?.[key]
    )

    changedFields.forEach(field => {
      executeRules(field)
    })
  }, { deep: true })

  // 初始化
  onMounted(() => {
    initFieldStates()
    executeRules()
  })

  return {
    fieldStates,
    executeRules
  }
}

表单步骤器

typescript
// composables/use-form-steps.ts
export interface FormStep {
  key: string
  title: string
  description?: string
  fields: string[]
  rules?: Record<string, any>
  beforeNext?: (formData: any) => Promise<boolean> | boolean
  beforePrev?: (formData: any) => Promise<boolean> | boolean
}

export function useFormSteps(
  steps: FormStep[],
  formData: any,
  formRef: Ref<FormInstance | undefined>
) {
  const currentStep = ref(0)
  const stepStatuses = ref<Array<'wait' | 'process' | 'finish' | 'error'>>(
    steps.map((_, index) => index === 0 ? 'process' : 'wait')
  )

  // 当前步骤配置
  const currentStepConfig = computed(() => steps[currentStep.value])

  // 是否第一步
  const isFirstStep = computed(() => currentStep.value === 0)

  // 是否最后一步
  const isLastStep = computed(() => currentStep.value === steps.length - 1)

  // 可以下一步
  const canNext = computed(() => !isLastStep.value)

  // 可以上一步
  const canPrev = computed(() => !isFirstStep.value)

  /**
   * 验证当前步骤
   */
  const validateCurrentStep = async (): Promise<boolean> => {
    if (!formRef.value) return false

    const currentFields = currentStepConfig.value.fields

    try {
      await formRef.value.validateField(currentFields)
      return true
    } catch (error) {
      return false
    }
  }

  /**
   * 下一步
   */
  const nextStep = async (): Promise<boolean> => {
    if (isLastStep.value) return false

    // 验证当前步骤
    const isValid = await validateCurrentStep()
    if (!isValid) {
      stepStatuses.value[currentStep.value] = 'error'
      return false
    }

    // 执行前置检查
    const beforeNext = currentStepConfig.value.beforeNext
    if (beforeNext) {
      const canProceed = await beforeNext(formData)
      if (!canProceed) return false
    }

    // 更新状态
    stepStatuses.value[currentStep.value] = 'finish'
    currentStep.value++
    stepStatuses.value[currentStep.value] = 'process'

    return true
  }

  /**
   * 上一步
   */
  const prevStep = async (): Promise<boolean> => {
    if (isFirstStep.value) return false

    // 执行前置检查
    const beforePrev = currentStepConfig.value.beforePrev
    if (beforePrev) {
      const canProceed = await beforePrev(formData)
      if (!canProceed) return false
    }

    // 更新状态
    stepStatuses.value[currentStep.value] = 'wait'
    currentStep.value--
    stepStatuses.value[currentStep.value] = 'process'

    return true
  }

  /**
   * 跳转到指定步骤
   */
  const goToStep = (stepIndex: number) => {
    if (stepIndex < 0 || stepIndex >= steps.length) return false

    currentStep.value = stepIndex

    // 更新状态
    stepStatuses.value = steps.map((_, index) => {
      if (index < stepIndex) return 'finish'
      if (index === stepIndex) return 'process'
      return 'wait'
    })

    return true
  }

  /**
   * 重置步骤
   */
  const resetSteps = () => {
    currentStep.value = 0
    stepStatuses.value = steps.map((_, index) => index === 0 ? 'process' : 'wait')
  }

  return {
    currentStep,
    stepStatuses,
    currentStepConfig,
    isFirstStep,
    isLastStep,
    canNext,
    canPrev,
    validateCurrentStep,
    nextStep,
    prevStep,
    goToStep,
    resetSteps
  }
}

📱 表单组件

动态表单渲染器

vue
<!-- DynamicForm.vue -->
<template>
  <el-form
    ref="formRef"
    :model="formData"
    :rules="formRules"
    :label-width="labelWidth"
    :label-position="labelPosition"
  >
    <template v-for="field in visibleFields" :key="field.key">
      <el-form-item
        :label="field.label"
        :prop="field.key"
        :required="field.required"
      >
        <!-- 输入框 -->
        <el-input
          v-if="field.type === 'input'"
          v-model="formData[field.key]"
          :placeholder="field.placeholder"
          :disabled="isFieldDisabled(field)"
          v-bind="field.props"
        />

        <!-- 文本域 -->
        <el-input
          v-else-if="field.type === 'textarea'"
          v-model="formData[field.key]"
          type="textarea"
          :placeholder="field.placeholder"
          :disabled="isFieldDisabled(field)"
          v-bind="field.props"
        />

        <!-- 选择器 -->
        <el-select
          v-else-if="field.type === 'select'"
          v-model="formData[field.key]"
          :placeholder="field.placeholder"
          :disabled="isFieldDisabled(field)"
          v-bind="field.props"
        >
          <el-option
            v-for="option in getFieldOptions(field)"
            :key="option.value"
            :label="option.label"
            :value="option.value"
          />
        </el-select>

        <!-- 单选框 -->
        <el-radio-group
          v-else-if="field.type === 'radio'"
          v-model="formData[field.key]"
          :disabled="isFieldDisabled(field)"
          v-bind="field.props"
        >
          <el-radio
            v-for="option in getFieldOptions(field)"
            :key="option.value"
            :value="option.value"
          >
            {{ option.label }}
          </el-radio>
        </el-radio-group>

        <!-- 复选框 -->
        <el-checkbox-group
          v-else-if="field.type === 'checkbox'"
          v-model="formData[field.key]"
          :disabled="isFieldDisabled(field)"
          v-bind="field.props"
        >
          <el-checkbox
            v-for="option in getFieldOptions(field)"
            :key="option.value"
            :value="option.value"
          >
            {{ option.label }}
          </el-checkbox>
        </el-checkbox-group>

        <!-- 日期选择器 -->
        <el-date-picker
          v-else-if="field.type === 'date'"
          v-model="formData[field.key]"
          :placeholder="field.placeholder"
          :disabled="isFieldDisabled(field)"
          v-bind="field.props"
        />

        <!-- 自定义字段 -->
        <slot
          v-else
          :name="`field-${field.key}`"
          :field="field"
          :value="formData[field.key]"
          :disabled="isFieldDisabled(field)"
        />
      </el-form-item>
    </template>

    <slot name="footer" :form-data="formData" />
  </el-form>
</template>

<script setup lang="ts">
interface Props {
  fields: FormField[]
  modelValue?: Record<string, any>
  labelWidth?: string
  labelPosition?: 'left' | 'right' | 'top'
  linkageRules?: LinkageRule[]
}

interface Emits {
  (e: 'update:modelValue', value: Record<string, any>): void
}

const props = withDefaults(defineProps<Props>(), {
  labelWidth: '100px',
  labelPosition: 'right'
})

const emit = defineEmits<Emits>()

const formRef = ref<FormInstance>()

// 动态表单hook
const { formData, formRules, visibleFields, isFieldDisabled } = useDynamicForm(
  toRef(props, 'fields')
)

// 表单联动hook
const { fieldStates } = useFormLinkage(
  formData,
  props.linkageRules || []
)

// 获取字段选项
const getFieldOptions = (field: FormField) => {
  const linkageState = fieldStates[field.key]
  if (linkageState?.options?.length) {
    return linkageState.options
  }
  return field.options || []
}

// 同步数据
watch(formData, (newData) => {
  emit('update:modelValue', newData)
}, { deep: true })

watch(() => props.modelValue, (newValue) => {
  if (newValue) {
    Object.assign(formData, newValue)
  }
}, { immediate: true, deep: true })

// 暴露表单方法
defineExpose({
  formRef,
  validate: () => formRef.value?.validate(),
  validateField: (field: string) => formRef.value?.validateField(field),
  resetFields: () => formRef.value?.resetFields(),
  clearValidate: () => formRef.value?.clearValidate()
})
</script>

表单步骤组件

vue
<!-- FormSteps.vue -->
<template>
  <div class="form-steps">
    <!-- 步骤指示器 -->
    <el-steps
      :active="currentStep"
      finish-status="success"
      align-center
    >
      <el-step
        v-for="(step, index) in steps"
        :key="step.key"
        :title="step.title"
        :description="step.description"
        :status="stepStatuses[index]"
      />
    </el-steps>

    <!-- 表单内容 -->
    <div class="step-content">
      <el-form
        ref="formRef"
        :model="formData"
        :rules="currentStepRules"
        label-width="120px"
      >
        <slot
          :name="`step-${currentStepConfig.key}`"
          :step="currentStepConfig"
          :form-data="formData"
          :step-index="currentStep"
        />
      </el-form>
    </div>

    <!-- 操作按钮 -->
    <div class="step-actions">
      <el-button
        v-if="!isFirstStep"
        @click="handlePrevStep"
      >
        上一步
      </el-button>

      <el-button
        v-if="!isLastStep"
        type="primary"
        @click="handleNextStep"
      >
        下一步
      </el-button>

      <el-button
        v-if="isLastStep"
        type="primary"
        @click="handleSubmit"
      >
        提交
      </el-button>
    </div>
  </div>
</template>

<script setup lang="ts">
interface Props {
  steps: FormStep[]
  modelValue: Record<string, any>
}

interface Emits {
  (e: 'update:modelValue', value: Record<string, any>): void
  (e: 'submit', value: Record<string, any>): void
  (e: 'step-change', step: number): void
}

const props = defineProps<Props>()
const emit = defineEmits<Emits>()

const formRef = ref<FormInstance>()
const formData = computed({
  get: () => props.modelValue,
  set: (value) => emit('update:modelValue', value)
})

// 表单步骤hook
const {
  currentStep,
  stepStatuses,
  currentStepConfig,
  isFirstStep,
  isLastStep,
  nextStep,
  prevStep
} = useFormSteps(props.steps, formData, formRef)

// 当前步骤验证规则
const currentStepRules = computed(() => {
  return currentStepConfig.value.rules || {}
})

// 处理下一步
const handleNextStep = async () => {
  const success = await nextStep()
  if (success) {
    emit('step-change', currentStep.value)
  }
}

// 处理上一步
const handlePrevStep = async () => {
  const success = await prevStep()
  if (success) {
    emit('step-change', currentStep.value)
  }
}

// 处理提交
const handleSubmit = async () => {
  const isValid = await validateCurrentStep()
  if (isValid) {
    emit('submit', formData.value)
  }
}
</script>

<style scoped>
.form-steps {
  .step-content {
    margin: 40px 0;
    min-height: 400px;
  }

  .step-actions {
    text-align: center;
    padding: 20px 0;
  }
}
</style>

useForm组合函数为Vue3应用提供了完整的表单管理解决方案,支持数据验证、动态表单、表单联动和步骤表单等高级功能。