Skip to content

Form 表单

用于统一管理表单字段、声明式校验规则、错误提示和无障碍属性。

命令行安装

bash
npx @loongship-kit/ui add form input textarea file-upload select combobox checkbox radio-group calendar date-picker date-range-picker time-field

组件结构

角色组件职责使用条件
根组件Form管理数据模型、规则和整体验证必需
字段容器FormItem关联字段、标签、控件和错误信息需要字段验证时使用

完整表单校验

Form 接收整个数据模型与规则,FormItem 通过 prop 关联字段。下面的示例集中展示所有表单字段组件的必填、长度和格式校验。提交失败时会定位并聚焦第一个错误字段。

2026年8月
27
28
29
30
31
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
1
2
3
4
5
6
日期选择, 2026年8月
––––
查看代码
vue
<script setup lang="ts">
import { CalendarDate, Time } from '@internationalized/date'
import { reactive, ref } from 'vue'
import { Calendar } from '@/components/loongship/calendar'
import { Checkbox } from '@/components/loongship/checkbox'
import {
  Combobox,
  ComboboxAnchor,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxViewport
} from '@/components/loongship/combobox'
import { DatePicker } from '@/components/loongship/date-picker'
import { DateRangePicker, type DateRangeValue } from '@/components/loongship/date-range-picker'
import { FileUpload, type FileUploadItem } from '@/components/loongship/file-upload'
import { Form, FormItem, type FormExpose, type FormRules } from '@/components/loongship/form'
import { Input } from '@/components/loongship/input'
import { RadioGroup, RadioGroupItem } from '@/components/loongship/radio-group'
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue
} from '@/components/loongship/select'
import { Textarea } from '@/components/loongship/textarea'
import { TimeField } from '@/components/loongship/time-field'

interface CompleteFormModel {
  vesselName: string
  notes: string
  attachments: FileUploadItem[]
  port: string
  searchPort: string
  accepted: boolean
  route: string
  calendarDate?: CalendarDate
  arrivalDate?: CalendarDate
  voyageRange?: DateRangeValue
  departureTime?: Time
}

const formRef = ref<FormExpose>()
const submitted = ref(false)
const ports = ['Shanghai', 'Singapore', 'Yokohama']
const datePlaceholder = new CalendarDate(2026, 8, 1)
const model = reactive<CompleteFormModel>({
  vesselName: '',
  notes: '',
  attachments: [],
  port: '',
  searchPort: '',
  accepted: false,
  route: '',
  calendarDate: undefined,
  arrivalDate: undefined,
  voyageRange: undefined,
  departureTime: undefined
})
const rules: FormRules = {
  vesselName: [
    { required: true, message: '请输入船舶名称', trigger: 'blur' },
    { min: 2, message: '船舶名称至少需要 2 个字符', trigger: ['blur', 'change'] }
  ],
  notes: [
    { required: true, message: '请输入操作说明' },
    { min: 10, message: '至少输入 10 个字符' }
  ],
  attachments: { required: true, message: '请选择附件' },
  port: { required: true, message: '请选择港口' },
  searchPort: { required: true, message: '请搜索并选择港口' },
  accepted: { required: true, message: '请确认申报信息' },
  route: { required: true, message: '请选择航向' },
  calendarDate: { required: true, message: '请选择日历日期' },
  arrivalDate: { required: true, message: '请选择抵达日期' },
  voyageRange: { required: true, message: '请选择完整航次日期' },
  departureTime: { required: true, message: '请选择离港时间' }
}

async function submitForm() {
  submitted.value = (await formRef.value?.validate()) ?? false
}
</script>

<template>
  <Form ref="formRef" :model="model" :rules="rules" @submit.prevent="submitForm">
    <FormItem label="Vessel name" prop="vesselName">
      <Input v-model="model.vesselName" placeholder="EVER GIVEN" />
    </FormItem>
    <FormItem label="Operation notes" prop="notes">
      <Textarea v-model="model.notes" placeholder="At least 10 characters" />
    </FormItem>
    <FormItem label="Attachment" prop="attachments">
      <FileUpload v-model:file-list="model.attachments" />
    </FormItem>
    <FormItem label="Port" prop="port">
      <Select v-model="model.port">
        <SelectTrigger><SelectValue placeholder="Select a port" /></SelectTrigger>
        <SelectContent>
          <SelectItem value="cnsgh">Shanghai</SelectItem>
          <SelectItem value="sgsin">Singapore</SelectItem>
        </SelectContent>
      </Select>
    </FormItem>
    <FormItem label="Search port" prop="searchPort">
      <Combobox v-model="model.searchPort">
        <ComboboxAnchor><ComboboxInput placeholder="Search port" /></ComboboxAnchor>
        <ComboboxContent>
          <ComboboxViewport>
            <ComboboxEmpty>No port found.</ComboboxEmpty>
            <ComboboxItem v-for="port in ports" :key="port" :value="port">{{ port }}</ComboboxItem>
          </ComboboxViewport>
        </ComboboxContent>
      </Combobox>
    </FormItem>
    <FormItem label="Declaration" prop="accepted">
      <label><Checkbox v-model="model.accepted" /> I confirm the declaration</label>
    </FormItem>
    <FormItem label="Route" prop="route">
      <RadioGroup v-model="model.route" orientation="horizontal">
        <label><RadioGroupItem value="east" /> Eastbound</label>
        <label><RadioGroupItem value="west" /> Westbound</label>
      </RadioGroup>
    </FormItem>
    <FormItem label="Calendar date" prop="calendarDate">
      <Calendar v-model="model.calendarDate" :default-placeholder="datePlaceholder" />
    </FormItem>
    <FormItem label="Arrival date" prop="arrivalDate">
      <DatePicker v-model="model.arrivalDate" :default-placeholder="datePlaceholder" />
    </FormItem>
    <FormItem label="Voyage range" prop="voyageRange">
      <DateRangePicker v-model="model.voyageRange" :default-placeholder="datePlaceholder" />
    </FormItem>
    <FormItem label="Departure time" prop="departureTime">
      <TimeField v-model="model.departureTime" />
    </FormItem>
    <button type="submit">提交</button>
    <span v-if="submitted">校验通过</span>
  </Form>
</template>

嵌套路径与自定义校验

字段路径与校验返回值

prop 支持点路径或路径数组,例如 contacts.0.phone['contacts', '0', 'phone']。自定义 validator 可返回错误文本、false 或 Promise;返回空字符串、true 或无返回值表示通过。

ts
const rules: FormRules = {
  'contacts.0.phone': {
    trigger: 'change',
    validator: async (_rule, value) => {
      const available = await checkPhone(String(value))
      return available ? '' : '该号码已被使用'
    }
  }
}

标签布局

统一标签宽度

label-position="top" 为默认的顶部标签。设置 label-position="left" 后,可通过 label-width 统一左侧标签宽度,也可在单个 FormItem 上覆盖。

API

Form(主组件)

属性

属性类型默认值说明
modelRecord<string, unknown>-表单数据模型,必填
rulesFormRules{}以字段路径为 key 的校验规则
labelPosition'top' | 'left''top'标签位置
labelWidthstring | number-左侧标签宽度
disabledbooleanfalse禁用表单内的原生表单控件
validateOnRuleChangebooleantrue规则改变后重新校验已触发字段

事件

事件参数说明
validate(prop, valid: boolean, message: string)字段校验完成后触发

插槽

插槽参数说明
default表单内容

暴露方法

方法返回值说明
validate()Promise<boolean>校验全部字段
validateField(prop)Promise<boolean>校验指定字段
resetFields(prop?)void恢复字段注册时的值并清除错误
clearValidate(prop?)void清除校验状态,不修改数据
scrollToField(prop)void将指定字段滚入视图

子组件

FormItem — 字段、标签与校验状态容器

属性

属性类型默认值说明
labelstring-字段标签
propstring | string[]-模型字段路径
rulesFormRule | FormRule[]-字段局部规则
requiredbooleanfalse添加必填约束与标记
errorstring-外部错误文本
showMessagebooleantrue是否显示字段下方的错误文本
labelWidthstring | number-覆盖本字段的标签宽度

插槽

插槽参数说明
default{ error: string, validateState: ValidateState }字段控件
label{ label: string }自定义字段标签
error{ error: string }自定义校验错误内容

暴露方法

方法返回值说明
validate(trigger?)Promise<boolean>校验当前字段
resetField()void恢复初值并清除错误
clearValidate()void清除校验状态

类型

FormRule

属性类型说明
requiredboolean非空校验
min / maxnumber字符串或数组校验长度,数字校验数值范围
patternRegExp字符串格式校验
messagestring校验失败文案
trigger'blur' | 'change' | Array触发时机;未设置时响应两种触发
validator(rule, value, model) => result | Promise<result>自定义同步或异步校验