Skip to content

表单

版本说明

时间修改人备注
2025-04-25YG补充文档

表单设计规范

1. 布局与结构

  • 采用垂直布局,每个表单项独占一行
  • 相关表单项进行分组,使用卡片或分隔线区分不同分组
  • 重要字段放在前面,非必填项放在后面
  • 过长表单应考虑分步填写,减轻用户认知负担

2. 标签与输入框

  • 标签文字简洁明了,使用名词而非动词
  • 对齐方式采用顶部对齐
  • 输入框宽度占满剩余空间
  • 必填项使用星号(*)标记
  • 输入框获取焦点时有明显的视觉反馈

3. 输入引导

  • 使用placeholder提示输入格式
  • 复杂输入提供示例或说明文字
  • 特殊格式输入(如日期、手机号)提供格式化输入控件
  • 密码输入提供显示/隐藏切换按钮

表单验证规范

1. 验证时机

  • 输入过程中实时验证(失焦验证)
  • 提交表单时进行全面验证

2. 验证规则

  • 手机号验证:11位数字,以1开头 /^1[3-9]\d{9}$/
  • 邮箱验证:符合邮箱格式规范 /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/
  • 密码验证:长度、复杂度要求明确提示(8-20位,包含数字和字母) /^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{8,20}$/
  • 身份证验证:18位,最后一位可能是X /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/
  • 必填项不能为空
  • 数字输入范围限制
  • 文本长度限制

3. 错误提示

  • 错误提示文字清晰具体,指出错误原因
  • 错误提示位置靠近对应输入框,通常在下方
  • 使用醒目但不刺眼的颜色(如红色)
  • 同时只显示一个错误,优先显示第一个错误项

表单交互规范

1. 提交操作

  • 提交按钮醒目,通常使用主题色
  • 提交按钮文字明确表达动作,如"提交"、"注册"
  • 提交前进行表单验证,提交过程中显示loading状态,防止重复提交
  • 提交成功后给予明确反馈,如成功页面或提示
  • 退出页面后使用 resetFields() 方法重置表单

2. 特殊交互

  • 验证码获取后60秒倒计时,倒计时期间禁用获取按钮
  • 倒计时过程中不允许修改手机号
  • 敏感操作(如删除账号)需二次确认

uniapp 组件使用

1. 表单组件结构

  • 使用 uni-forms 作为表单容器组件
  • 每个表单项使用 uni-forms-item 包裹
  • 表单项内可嵌套 uni-easyinput 、 uni-data-checkbox 等组件或原生表单组件

2. 表单布局与样式

  • 通过 label-position 设置标签位置:top(顶部)、left(左侧,默认)
  • 使用 label-width 设置标签宽度,默认75px
  • 设置 label-align 控制标签对齐方式:left(左对齐)、center(居中)、right(右对齐)
vue
<uni-forms :modelValue="formData" label-position="top">
  <uni-forms-item label="姓名" name="name">
    <uni-easyinput v-model="formData.name" placeholder="请输入姓名" />
  </uni-forms-item>
</uni-forms>

3. 表单验证规范

  • 定义 rules 对象设置验证规则
  • 支持多种验证类型:required(必填)、format(格式)、range(范围)、length(长度)等
  • 可通过 validateFunction 自定义验证函数
vue
<template>
  <uni-forms ref="form" :rules="rules" :modelValue="formData">
    <!-- 表单项 -->
  </uni-forms>
</template>

<script>
export default {
  data() {
    return {
      formData: {
        name: '',
        age: '',
        email: ''
      },
      rules: {
        name: {
          rules: [
            { required: true, errorMessage: '请输入姓名' },
            { minLength: 2, maxLength: 10, errorMessage: '姓名长度在2-10个字符' }
          ]
        },
        email: {
          rules: [
            { required: true, errorMessage: '请输入邮箱' },
            { format: 'email', errorMessage: '邮箱格式不正确' }
          ]
        }
      }
    }
  }
}
</script>

4. 表单验证方法

  • 使用 validate() 方法进行表单整体验证
  • 使用 validateField(name) 验证单个表单项
  • 使用 clearValidate([names]) 清除验证结果
js
// 表单验证示例
submitForm() {
  this.$refs.form.validate().then(res => {
    // 验证通过
    console.log('表单数据信息:', res);
  }).catch(err => {
    // 验证失败
    console.log('表单错误信息:', err);
  })
}

5. 表单项特性

  • 使用 required 属性显示必填星号
  • 设置 err-show-type 控制是否显示错误信息,undertext/toast/modal
  • 通过插槽自定义表单项内容:label(标签)、default(默认)

vant 组件使用

  • 表单的保存和删除等按钮,一般需要虚浮在底部;
  • 表单的必填项需要添加验证和红点标记;
  • 表单验证使用公共方法:/src/libs/common/WxValidate.js
  • 表单项必须使用van-field保持样式一致(实际使用是根据UI统一调整样式),如果有其他输入方式使用van-field的slot:input;
  • 表单的布局:
    • form > van-field
    • form > van-field > template #input > ...

1. 表单示例

vue
<template>
  <view class="content">
    <form @submit="onSubmit">
      <view class="form_box">
        <!-- 输入框 -->
        <van-field
          name="name"
          type="text"
          label="名称"
          placeholder="请输入名称"
          :value="formModel.name"
          :required="formRules.name.required"
          :error-message="objValidateErr.name"
          @input="onInput($event, 'name')"
        ></van-field>
        <van-field
          name="desc"
          type="text"
          label="描述"
          placeholder="请输入描述"
          :value="formModel.desc"
          :required="formRules.desc.required"
          :error-message="objValidateErr.desc"
          @input="onInput($event, 'desc')"
        ></van-field>
        <van-field
          name="phone"
          type="phone"
          label="手机号"
          placeholder="请输入手机号"
          :value="formModel.phone"
          :required="formRules.phone.required"
          :error-message="objValidateErr.phone"
          @input="onInput($event, 'phone')"
        ></van-field>
        <van-field
          name="password"
          type="phone"
          label="密码"
          placeholder="请输入密码"
          :password="!controlItem.showPassword"
          :value="formModel.password"
          :required="formRules.password.required"
          :error-message="objValidateErr.password"
          @input="onInput($event, 'password')"
        >
          <template #right-icon>
            <view
              @click.stop="controlItem.showPassword = !controlItem.showPassword"
            >
              <image
                v-if="controlItem.showPassword"
                class="showPawIcon"
                src="https://pro-core.babycdn.com/2022/09/zsh/xcx/images/home/show.png"
              ></image>
              <image
                v-else
                class="showPawIcon"
                src="https://pro-core.babycdn.com/2022/09/zsh/xcx/images/home/not_show.png"
              ></image>
            </view>
          </template>
        </van-field>
        <!-- 开关 -->
        <!-- 小程序样式问题,高度比size多2px -->
        <van-field name="open" label="是否开启">
          <template #input>
            <van-switch
              :checked="formModel.open"
              style="height:20px"
              size="18"
              @change="onInput($event, 'open')"
            />
          </template>
        </van-field>
        <!-- 单选框 -->
        <van-field
          name="sex"
          label="性别"
          :required="formRules.sex.required"
          :error-message="objValidateErr.sex"
        >
          <template #input>
            <van-radio-group
              :value="formModel.sex.toString()"
              direction="horizontal"
              @change="onInput($event, 'sex')"
            >
              <template v-for="(option, option_i) in formOption.sex">
                <van-radio :key="option_i" :name="option.key" shape="round">
                  {{ option.text }}
                </van-radio>
              </template>
            </van-radio-group>
          </template>
        </van-field>
        <!-- 复选框 -->
        <van-field
          name="likes"
          label="爱好"
          :required="formRules.likes.required"
          :error-message="objValidateErr.likes"
        >
          <template #input>
            <van-checkbox-group
              :value="formModel.likes"
              direction="horizontal"
              @change="onInput($event, 'likes')"
            >
              <template v-for="(option, option_i) in formOption.likes">
                <van-checkbox :key="option_i" :name="option.key" shape="square">
                  {{ option.text }}
                </van-checkbox>
              </template>
            </van-checkbox-group>
          </template>
        </van-field>
        <!-- 日期选择器 -->
        <view @click="controlItem.showDatePopup = true">
          <van-field
            name="date"
            label="日期"
            :value="formModel.date"
            readonly
            placeholder="请选择日期"
            :error-message="objValidateErr.date"
          >
          </van-field>
        </view>
        <van-popup
          :show="controlItem.showDatePopup"
          position="bottom"
          round
          @close="controlItem.showDatePopup = false"
        >
          <van-datetime-picker
            type="date"
            title="请选择日期"
            :value="new Date(formModel.date).getTime()"
            :min-date="formRules.date.minDate"
            :max-date="formRules.date.maxDate"
            @cancel="controlItem.showDatePopup = false"
            @confirm="onInputDate($event, 'date', 'showDatePopup')"
          />
        </van-popup>
        <!-- 评分 -->
        <van-field
          name="score"
          label="评分"
          :required="formRules.score.required"
          :error-message="objValidateErr.score"
        >
          <template #input>
            <van-rate
              :value="formModel.score"
              size="18"
              gutter="2"
              count="10"
              allow-half
              @change="onInput($event, 'score')"
            />
          </template>
        </van-field>
        <!-- 步进器-->
        <van-field
          name="num"
          label="数量"
          :required="formRules.num.required"
          :error-message="objValidateErr.num"
        >
          <template #input>
            <van-stepper
              :value="formModel.num"
              step="1"
              @change="onInput($event, 'num')"
            />
          </template>
        </van-field>
        <!-- 进度条 -->
        <van-field
          name="speed"
          label="进度"
          :required="formRules.speed.required"
          :error-message="objValidateErr.speed"
        >
          <template #input>
            <view class="slider_box">
              <van-slider
                :value="formModel.speed"
                step="1"
                :use-button-slot="true"
                @drag="onInputValue($event, 'speed')"
              >
                <view class="slider_btn" slot="button">
                  {{ formModel.speed }}
                </view>
              </van-slider>
            </view>
          </template>
        </van-field>
        <!-- 图片上传 -->
        <van-field
          name="image"
          label="图片base64上传"
          :required="formRules.image.required"
          :error-message="objValidateErr.image"
        >
          <template #input>
            <van-uploader
              :file-list="formModel.image"
              :max-count="4"
              accept="image"
              :imageFitL="'center'"
              @after-read="onInputImg($event, 'image')"
              @delete="onDelImg($event, 'image')"
            />
          </template>
        </van-field>
        <!-- oss图片上传 -->
        <van-field
          name="oss_img"
          label="图片oss上传"
          :border="false"
          :required="formRules.oss_img.required"
          :error-message="objValidateErr.oss_img"
        >
          <template #input>
            <van-uploader
              :file-list="formModel.oss_img"
              :max-count="4"
              accept="image"
              :imageFitL="'center'"
              @after-read="onInputImg($event, 'oss_img')"
              @delete="onDelImg($event, 'oss_img')"
            />
          </template>
        </van-field>
      </view>
      <view class="operation_box">
        <template v-for="(btn, btn_i) in operation">
          <template v-if="btn.type == 'submit'">
            <van-button
              :key="btn_i"
              size="normal"
              block
              round
              :type="btn.btnType"
              form-type="submit"
              :class="[btn.customClass]"
            >
              {{ btn.text }}
            </van-button>
          </template>
          <template v-else>
            <van-button
              :key="btn_i"
              size="normal"
              block
              round
              :type="btn.btnType"
              :class="[btn.customClass]"
            >
              {{ btn.text }}
            </van-button>
          </template>
        </template>
      </view>
    </form>
  </view>
</template>
<script>
import addData from "./add.js";

export default {
  components: {},
  data() {
    return {
      formModel: JSON.parse(JSON.stringify(addData.formModel)),
      formRules: addData.formRules,
      formOption: addData.formOption,
      validate: addData.validate,
      objValidateErr: addData.objValidateErr,
      controlItem: addData.controlItem,
      operation: addData.operation,
    };
  },
  onLoad() {},
  methods: {
    //input、switch、rate、field、radio、checkbox
    onInput(e, key) {
      console.log(e);
      let value = e.detail;
      this.formModel[key] = value;
      this.checkField(key);
    },
    //slider
    onInputValue(e, key) {
      console.log(e);
      let value = e.detail.value;
      this.formModel[key] = value;
      this.checkField(key);
    },
    //datetime-picker
    onInputDate(e, key, controlLey) {
      let date = new Date(e.detail);
      this.formModel[key] = `${date.getFullYear()}-${
        date.getMonth() + 1 >= 10
          ? date.getMonth() + 1
          : "0" + (date.getMonth() + 1)
      }-${date.getDate() >= 10 ? date.getDate() : "0" + date.getDate()}`;
      this.checkField(key);
      //关闭对应弹出框
      if (controlLey) {
        this.controlItem[controlLey] = false;
      }
    },
    //图片
    onInputImg(e, key) {
      this.formModel[key].push(e.detail.file);
    },
    onDelImg(e, key) {
      this.formModel[key].splice(e.detail.index, 1);
    },
    //验证单项
    checkField(key) {
      if (this.validate.checkField(key, this.formModel)) {
        delete this.objValidateErr[key];
      } else {
        this.objValidateErr[key] = this.validate.errorList[0].msg;
      }
    },
    onSubmit(e) {
      let submit = this.operation.filter((it) => it.type == "submit")[0];
      submit.fun(this);
    },
    touchFun(fun, item) {
      fun(item);
    },
  },
};
</script>

2. 校验文件的使用

官方文档:https://github.com/wux-weapp/wx-extend/blob/master/docs/components/validate.md

  1. 引入文件
js
import WxValidate from "/src/libs/common/WxValidate.js";
  1. 使用
js
//表单对应的字段名
const formModel = {
  name: "",
  desc: "",
  phone: "",
  password: "",
  open: true,
  sex: "",
  likes: [],
  date: "",
  score: null,
  num: 1,
  speed: 0,
  image: [],
  oss_img: [],
  oss_file: [],
};

//表单对应字段的校验规则
const formRules = {
  name: { required: true, maxlength: 5 },
  desc: { required: true, maxlength: 50 },
  phone: { required: true, tel: true },
  password: { required: true, password: /^[0-9|a-z|A-Z]{6,16}$/ },
  open: { required: true },
  sex: { required: true },
  likes: { required: true, maxlength: 3 },
  date: {
    required: true,
    minDate: new Date().getTime(),
    maxDate: new Date("2023-12-12").getTime(),
  },
  score: { required: true },
  num: { required: true, max: 10 },
  speed: { required: true, min: 1 },
  image: { required: true },
  oss_img: { required: true },
};

//校验规则的错误提示
const validateMessage = {
  name: {
    required: "请输入名称",
    maxLength: "不能多于5个字符",
  },
  desc: {
    required: "请输入描述",
    maxLength: "不能多于50个字符",
  },
  phone: {
    required: "请输入手机号",
    tel: "请输入正确的手机号码",
  },
  password: {
    required: "请输入密码",
  },
  open: {},
  sex: {
    required: "请选择性别",
  },
  likes: {
    required: "请选择爱好",
    maxlength: "最多选择3个",
  },
  date: {
    required: "请选择日期",
  },
  score: {
    required: "请选择评分",
  },
  num: {
    required: "请选择数量",
    max: "数量不能大于10",
  },
  speed: {
    required: "请选择进度",
    max: "进度不能小于1",
  },
  image: { required: "请上传图片" },
  oss_img: { required: "请上传图片" },
};

const validate = new WxValidate(formRules, validateMessage);

// 自定义验证规则
validate.addMethod(
  "password",
  (value, param) => {
    return param.test(value);
  },
  "请输入6~16位由数字、大小字母组成的密码"
);

校验全部

js
validate.checkForm(formModel);

校验单项

js
validate.checkField(key, data);
  1. 内置的校验规则
序号规则描述
1required: true这是必填字段。
2email: true请输入有效的电子邮件地址。
3tel: true请输入 11 位的手机号码。
4url: true请输入有效的网址。
5date: true请输入有效的日期。
6dateISO: true请输入有效的日期(ISO),例如:2009-06-23,1998/01/22。
7number: true请输入有效的数字。
8digits: true只能输入数字。
9idcard: true请输入 18 位的有效身份证。
10equalTo: 'field'输入值必须和 field 相同。
11contains: 'ABC'输入值必须包含 ABC。
12minlength: 5最少要输入 5 个字符。
13maxlength: 10最多可以输入 10 个字符。
14rangelength: [5, 10]请输入长度在 5 到 10 之间的字符。
15min: 5请输入不小于 5 的数值。
16max: 10请输入不大于 10 的数值。
17range: [5, 10]请输入范围在 5 到 10 之间的数值。
  1. 方法
名称返回类型描述
checkForm(formModel)boolean验证所有字段的规则,返回验证是否通过。
checkField(key,data)boolean验证所有字段的规则,返回验证是否通过。
valid()boolean返回验证是否通过。
size()number返回错误信息的个数。
validationErrors()array返回所有错误信息。
addMethod(name, method, message)boolean添加自定义验证方法。

3. 示例项目 SVN 地址

uni-app 项目:svn://39.107.13.238:12580/base_digi

示例


/_uniapp/src/pages/demo/add/wxapp/add.vue