A34. 表单
⭐ 4.1. form 元素
此前我们一直在聚焦 JavaScript 的动态操作,但是我们一直没有获得用户的输入——尤其是数字与文本。而表单就是用来收集用户输入的工具。
⭐ 4.1.1. 表单的基本结构
<form>
<label for="username">用户名:</label>
<input type="text" id="username" name="username">
<button type="submit">提交</button>
</form>
<form>
是表单容器,包裹所有表单控件。其中的 label
与 input
通过 for
和 id
关联,形成输入控件的逻辑单元。
TIP
表单控件一般需包含在 <form>
内部,才能便于 JavaScript 统一处理。
⭐ 4.1.2. 表单的属性
<!-- 默认提交行为演示 -->
<form action="/submit" method="GET">
<input type="text" name="search" placeholder="搜索关键词">
<button type="submit">提交</button>
</form>
<!-- 阻止默认行为演示 -->
<form onsubmit="event.preventDefault(); console.log('提交被阻止');">
<input type="text" name="test">
<button type="submit">提交</button>
</form>
<form>
元素支持以下属性:
method
:GET
:参数附加在 URL 后(适合搜索),刷新页面可重现。POST
:参数在请求体中发送(适合敏感数据)。
action
: 传统后端处理的目标地址,现代前端开发中常被 JavaScript 覆盖。
历史遗留问题
在传统前端开发中,前端只作为界面的辅助,表单的提交行为通常由后端处理,页面自动跳转,浏览器请求服务器渲染新的网页。
而在现代前端开发中,表单的提交行为通常由 JavaScript 处理,然后由 JavaScript 再渲染网页。这时默认提交行为导致的页面跳转便成了累赘,需通过 event.preventDefault()
阻止。
4.2. 🌟 表单核心控件 input
form
只是一个骨架,内部需要有填充数据的控件:input
元素便是表单的核心控件,它可以用于输入各种类型的数据。
4.2.1. ⭐ label 元素的用法
label
元素用于定义表单控件的说明文本,通常与 input
元素一起使用,用于增强用户体验,防止 input
过小不便操作。
1. 使用 for
属性显示绑定
for
,即“为了”,用于 label
元素,表示它与相应 ID 的表单控件相关联,点击 label
中的文本也可以激活 input
控件。
<p>
<label for="email">邮箱:</label>
<input type="email" id="email" name="email">
</p>
2. 嵌套结构隐式绑定
而若觉得设置 id
和 for
太麻烦,可以直接将 input
元素嵌套在 label
元素内部,此时 label
元素会自动绑定到 input
元素。
<label>
<span>邮箱</span>
<input type="email" id="email" name="email">
</label>
注意事项
label
元素的for
属性与input
元素的id
必须一致。- 若采用嵌套式结构,
label
元素内只应出现唯一的表单控件,其余部分都是普通的提示文本。
4.2.2. 🌟 input 元素的属性
属性名 | 作用 |
---|---|
type | 定义输入类型(如 text 、password 、number 等),决定输入行为和验证规则 |
value | 设置初始值值 |
name | 表单提交时字段的标识符 |
autocomplete | 控制浏览器自动填充行为(支持 on /off /username /email 等具体字段) |
<input type="text" name="username" autocomplete="username">
<input type="email" name="email" autocomplete="email">
其中:
type
是最核心属性,直接影响输入格式和验证。autocomplete
的具体值(如username
)能引导浏览器智能填充数据。
TIP
- 表单元素按最佳实践应填写
name
,保证语义化。 - 表单元素的第一步是确定何种类型。
接下来我们便介绍几种常见的 input
类型,和它在这些类型下特有的属性。
4.2.3. 🌟 普通文本框与密码框
最常见的需求是输入文本:
<label>
<span>用户名</span>
<input type="text" name="username" autocomplete="username">
</label>
<label>
<span>密码</span>
<input type="password" name="password" autocomplete="current-password">
</label>
文本框会显示用户输入内容,适合用户名等公开信息,而 密码框 则隐藏输入内容(显示为圆点),适合密码等敏感信息。
4.2.4. 🌟 数字输入框
需要数字输入的场景,如年龄、价格等,可使用 number
类型。
<label for="age">年龄:</label>
<input type="number" id="age" name="age" min="0" max="150" step="1">
属性说明
属性名 | 作用 | 默认值 |
---|---|---|
min | 最小值 | 无 |
max | 最大值 | 无 |
step | 输入步长 | 1 |
默认情况下,number
类型允许用户输入任意整数,也可以将 step
设置为 0.01
、0.05
、0.1
、0.5
等小数(或 5
、10
等大整数),实现更精细的输入控制;或者直接使用 step="any"
允许用户输入任意精度的数字。
4.2.5. ⭐ 日期输入框
需要选择日期的场景,如出生日期、活动日期等,可使用 date
类型,这样就不需要用户用纯文本手动输入了。
<label for="birthday">出生日期:</label>
<input type="date" id="birthday" name="birthday" min="1900-01-01" max="2025-05-12">
其中 min
和 max
限制了可选日期范围。
4.2.6. ⭐ 单选框
单选框是一组互斥的选项,用户只能选择其中一个。
与前面的控件不同,它必须设置 name
属性用于标识同一组选项,并且需要设置的 value
不再表示默认值,而是选中该元素后表单数据的值。
同时,由于单选框在界面上很小,使用 label
元素绑定几乎是必须的。
<p>性别:</p>
<input type="radio" id="male" name="gender" value="male">
<label for="male">男</label>
<input type="radio" id="female" name="gender" value="female">
<label for="female">女</label>
4.2.7. ⭐ 复选框
复选框是一组可多选的选项,用户可以选择任意个选项。
与单选框类似,它也必须通过 name
属性用于标识同一组选项,并且需要设置的 value
也不是默认值,而是选中该元素后表单数据的值。
<p>兴趣爱好:</p>
<input type="checkbox" id="reading" name="hobby" value="reading">
<label for="reading">阅读</label>
<input type="checkbox" id="sports" name="hobby" value="sports">
<label for="sports">运动</label>
4.2.8. ⭐ 文件上传框
有时用户需要的不仅仅是文本,还需要上传文件,这时可以使用 file
类型。
<label for="attachments">上传附件:</label>
<input type="file" id="attachments" name="attachments" accept=".pdf,.docx" multiple>
其中:
accept
属性用于限制文件类型,若有多个则使用英文逗号分隔,例如.pdf,.docx
表示只允许上传 PDF 和 Word 文档。multiple
属性用于允许用户选择多个文件。这是 HTML 的一种特殊属性,不需要设置值,根据该属性是否存在来决定是否允许多选。也就是说,如果只允许上传一个文件,不写multiple
属性即可。
4.2.9. 隐藏域
<input type="hidden" name="startTime" value="2025-05-12T12:00:00">
隐藏域用于存储不可见的表单数据(如问卷开始时间),提交时随表单一同发送,常用于前后端交互中传递上下文信息,在现代前端开发中不再常用。
4.3. ⭐ 表单其他控件
除了
input
元素,表单还包含其他控件,如select
、textarea
和button
等。
4.3.1. ⭐ 下拉框
单选和多选不一定要把全部选项罗列在界面上,也可以下拉选中,使界面更加简洁。
<label for="city">城市:</label>
<select id="city" name="city">
<option value="beijing">北京</option>
<option value="shanghai">上海</option>
<option value="guangzhou">广州</option>
</select>
其语法为,在 <select>
元素下使用 <option>
元素定义选项,每个选项都必须有一个 value
属性。最后选中哪个选项,则表单数据的值为该选项的 value
。
4.3.2. ⭐ 多行文本框
<label for="bio">个人简介:</label>
<textarea id="bio" name="bio" rows="4" cols="50"></textarea>
注意事项:
<textarea>
元素的默认值通过闭合标签内的文本设置。<textarea>
的换行和缩进会被保留至输入框内,建议在 HTML 中一行写完默认值。rows
和cols
定义行与列的字符数而非像素数,比如rows="4"
表示大约展示4
行的高度。
4.3.3. 🌟 按钮元素
最后请出一个非常重要的控件:按钮元素,用于提交表单或执行其他操作。
<button type="submit">提交</button>
<button type="reset">重置</button>
<button type="button" onclick="alert('点击了按钮')">自定义操作</button>
type 属性值 | 作用 |
---|---|
submit | 提交表单 |
reset | 重置表单(不常用) |
button | 自定义操作 |
4.4. 🌟 表单 DOM 交互
前面我们已经认识了如何在 HTML 中完成表单的编写,接下来便是接入 JavaScript 进行动态应用了。
4.4.1. 🌟 获取普通表单控件的值
从 JavaScript 的视角来看,表单控件与普通元素没有区别,都是 DOM 节点。
而选中它们之后访问 .value
属性,即可获取表单的值。
<input type="text" id="username" value="初始值">
<textarea id="bio">默认文本</textarea>
<select id="city">
<option selected>北京</option>
</select>
<script>
const username = document.getElementById('username').value;
const bio = document.getElementById('bio').value;
const city = document.getElementById('city').value;
</script>
注意事项
- 在 HTML 中所写的
value
只是表示初始值,而 JavaScript 获得的.value
是用户输入的最新的表单值。 .value
返回的类型是string
(即使设置为type="number"
也仍然是),如果需要数字类型,则需要手动转换。
4.4.2. ⭐ 文件处理
网页的文件处理比本地复杂很多,需要调用多个高级 API,涉及异步思想。故读者只需先了解基本流程,会模仿即可,之后会详细介绍。
这样设计主要是为了防止文件过大导致页面卡顿。
<input type="file" id="file" multiple>
<script>
const fileInput = document.getElementById('file'); // 获取上传框元素
fileInput.addEventListener('change', (event) => {
// 监听文件变化
const files = event.target.files; // 访问 .files 属性获得文件列表
for (const file of files) { // for 循环遍历
const reader = new FileReader(); // 创建文件读取器
// 注意:FileReader 是异步架构,通过读取完毕触发事件的方式,涉及闭包
reader.onload = (ev) => {
const content = ev.target.result;
console.log(content);
};
// 设置完 onload 属性后,调用 readAsText 方法读取文件内容
// 注意:readAsText 不会返回文件内容,而是读取完毕后触发 onload 事件
reader.readAsText(file);
}
});
</script>
WARNING
现代浏览器为了安全性考虑,不会将路径暴露给 JavaScript,浏览器只能通过文件名区分。所以即使上传的是两个不同文件夹的同名文件,JavaScript 也可能不会认为文件发生了变化。
4.4.3. 🌟 绑定表单控件的事件
表单控件的事件与普通元素的事件类似,都是通过 on
开头的属性绑定。
最重要的是 oninput
和 onchange
事件,它们分别在用户输入任意字符时和用户输入结束时触发。
on
开头系列的属性值都是 JavaScript 代码,当触发时执行其中的内容。
<input type="text" id="input" oninput="logInput(event)" onchange="logChange(event)">
<script>
function logInput(e) {
console.log('输入事件:', e.target.value);
}
function logChange(e) {
console.log('改变事件:', e.target.value);
}
</script>
当然也可以通过 addEventListener
绑定事件:
<input type="text" id="input">
<script>
const input = document.getElementById('input');
input.addEventListener('input', logInput);
input.addEventListener('change', logChange);
function logInput(e) {
console.log('输入事件:', e.target.value);
}
function logChange(e) {
console.log('改变事件:', e.target.value);
}
event
是哪里来的?
event
是 JavaScript 提供的一个全局变量,它表示当前事件的详细信息。当用户触发事件时,JavaScript 会自动将事件对象传递给事件处理函数,我们可以通过 event
来访问当前事件对象的属性和方法。
4.4.4. ⭐ 数据绑定范式
对于纯前端开发,我们需要将表单数据实时更新至 JavaScript 变量中,然后 JavaScript 变量变化后自动重新计算并更新界面,这时便需要用到数据绑定的范式。
主要的思想是:
- 将变量更新封装为
set
开头的函数,用于统一更新数据与界面。 - 在表单相应事件中调用
set
函数,实现数据与界面的同步。
<input type="number" id="num">
<div id="result"></div>
<script>
let num = 0;
document.getElementById('num').addEventListener('input', updateNum);
function updateNum(ev) {
const newNum = parseFloat(ev.target.value);
setNum(newNum);
}
function setNum(newNum) {
num = newNum;
document.getElementById('result').innerText = `${num} 的立方是 ${num * num * num}`;
}
</script>
效果:输入数字后,下方 div
实时显示其立方值。
4.4.5. 表单的提交
注意:本小节涉及对象的思想,读者可先暂且了解,后续会详细介绍。现代前端开发中,提交表单并不是必须的,JavaScript 可以绕过表单体系直接针对控件进行操作。
<form id="myForm" onsubmit="handleSubmit(event)">
<input type="text" name="username">
<button type="submit">提交</button>
</form>
<script>
function handleSubmit(e) {
e.preventDefault(); // 阻止默认提交行为
const formData = new FormData(e.target);
console.log(Object.fromEntries(formData));
}
</script>
说明:
FormData
对象可提取表单数据。Object.fromEntries()
方法将FormData
对象转换为普通对象。preventDefault()
阻止页面刷新,实现纯前端处理。
4.4.6. 示例:BMI 计算器
以下代码展示了一个完整的 BMI 计算器,包括表单输入、计算和结果展示,是本教程第一个真正意义上具有完整交互功能的微型项目。
<form id="bmiForm">
<label for="height">
<span>身高 (cm):</span>
<input type="number" id="height" name="height" required>
</label>
<label for="weight">
<span>体重 (kg):</span>
<input type="number" id="weight" name="weight">
</label>
<button type="submit">计算 BMI</button>
</form>
<div id="result"></div>
<style>
form {
border: 2px dashed black;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
padding: 1rem;
}
#result {
color: blue;
text-align: center;
}
</style>
<script>
document.getElementById("bmiForm").addEventListener('submit', calculateBMI);
function calculateBMI(e) {
e.preventDefault();
const height = parseFloat(document.getElementById('height').value) / 100;
const weight = parseFloat(document.getElementById('weight').value);
const bmi = (weight / (height * height)).toFixed(2);
document.getElementById('result').innerText = `您的 BMI 值为 ${bmi}`;
}
</script>
4.5. ⭐ 表单验证
用户的输入永远是不可信的。为了防止用户输入错误的数据,我们需要对表单进行验证。
4.5.1. ⭐ 表单验证的基本概念
表单验证是指,在用户提交表单之前,对表单中的数据进行验证,以确保数据的有效性。
表单验证依靠一系列的验证规则,如:
- 必填字段,用来防止用户遗漏必要信息
- 格式验证,用来防止用户输入不合格式的数据,如无效的邮箱、手机号等
- 范围限制,用来防止用户输入不合理的数据,如超出正常范围的年龄、价格等
- 长度限制,用来防止用户输入过长或过短的数据,如姓名、密码等
- 其他自定义验证
值得注意的是,本教程暂时只探讨纯前端的项目,而对于服务器参与的项目,表单验证需要在前端与后端两次进行。
为什么前端和后端的表单验证不能只验证一次?
- 前端验证是为了防止用户无意输入不合规的数据,即时的反馈有助于提升用户体验。
- 后端验证是为了防止用户恶意输入不合规的数据,用于维护服务器的安全。
- 恶意用户可以通过各类手段(如修改请求头、修改 JavaScript 代码等)绕过前端验证,所以服务器验证尤其不可省略。
4.5.2. 表单验证的传统实现
<input type="text" name="username" required minlength="3" maxlength="20">
<input type="number" name="age" min="18" max="99">
属性说明:
属性名 | 作用 | 默认值 |
---|---|---|
required | 必须填写 | 无 |
minlength | 最小长度 | 无 |
maxlength | 最大长度 | 无 |
min | 最小值 | 无 |
max | 最大值 | 无 |
这类手段依靠浏览器 HTML 自带的验证,当用户输入不符合规则的数据时,浏览器会自动在相应位置弹出错误提示。
但这种方法存在以下问题:
- 浏览器自带的验证规则有限,无法满足某些常见但浏览器不支持的需求。
- 无法在 JavaScript 中获取验证结果,无法进行需要计算的自定义验证。
4.5.3. 使用 valibot 进行表单验证
因此,一批前端框架开始提供表单验证的解决方案,其中 valibot
是一个轻量级的表单验证库,支持链式规则定义。
valibot
的验证涉及较高级的思想,如对象思想,读者可以先尝试理解下面的代码。
同时,valibot
作为一个现代的模块,需要通过 import
导入,而不是通过 <script>
标签引入。
含有 import
语句的 JavaScript 文件需要在 <script>
标签中添加 type="module"
属性。
<form id="application-form">
<label>
用户名:
<input type="text" autocomplete="username" name="username">
</label>
<label>
邮箱:
<input type="text" autocomplete="email" name="email">
</label>
<button>提交</button>
<div id="errors"></div>
<div id="success"></div>
</form>
<style>
form {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
#errors {
color: red;
}
#success {
color: green;
}
</style>
<script type="module">
import * as v from 'https://unpkg.com/valibot@1.1.0/dist/index.min.js';
const schema = v.object({
username: v.pipe(v.string('请输入用户名'), v.minLength(3), v.maxLength(15)),
email: v.pipe(v.string('请输入邮箱'), v.email())
});
const elSuccess = document.getElementById("success");
const elErrors = document.getElementById("errors");
document.getElementById('application-form').addEventListener("submit", function (event) {
event.preventDefault();
const formData = new FormData(event.target);
const obj = Object.fromEntries(formData);
const result = v.safeParse(schema, obj);
console.log(result);
if (result.success) {
elSuccess.textContent = "验证成功!";
elErrors.textContent = "";
} else {
elSuccess.textContent = "";
elErrors.textContent = "由于以下原因验证失败:";
for (const issue of result.issues) {
elErrors.append(document.createElement("br"));
const key = issue.path.map(p => p.key).join(">")
elErrors.append(`[${key}] ${issue.message}`);
}
}
})
</script>
说明:
valibot
是轻量级表单验证库,支持链式规则定义。- 虽然是“轻量级”,只是相比于代码更多的
zod
等库而言的。valibot
仍然具有非常齐全的功能和复杂的用法。 - 读者不需要对自己难以理解其中内容而担心。当前水平只需要先让用户“能用”,然后再考虑如何进行验证等优化。
知识回顾
表单基础结构与属性
<form>
元素- 作为表单容器,包裹所有表单控件。
method
和action
属性决定表单提交方式(GET/POST)和目标地址。- 默认提交行为需通过
event.preventDefault()
阻止,避免页面跳转。
<label>
元素- 通过
for
与id
关联控件,或通过嵌套结构隐式绑定。 - 扩大用户可点击区域,提升交互体验。
- 通过
- 表单控件嵌套
- 控件需包含在
<form>
内部,便于 JavaScript 统一处理。
- 控件需包含在
表单核心控件
<input>
元素- `基本使用
type
是最核心属性,决定输入类型(文本、密码、数字、日期、单选、复选、文件等)。autocomplete
支持多种取值(如username
、email
),引导浏览器智能填充。name
用于表单提交时标识字段。
- 单选框与复选框
- 单选框需共享
name
属性,用户只能选择一个选项。 - 复选框允许多选,需独立设置
name
或使用数组形式提交。
- 单选框需共享
- 文件上传框
accept
限制文件类型,multiple
允许多文件选择。- 隐藏域(
<input type="hidden">
)用于传递不可见的上下文信息(如问卷开始时间)。
- `基本使用
表单其他控件
- 下拉框(
<select>
)- 通过
<option>
定义选项,选中值为value
属性。 - 支持
multiple
属性实现多选。
- 通过
- 多行文本框(
<textarea>
)rows
和cols
定义字符数而非像素尺寸。- 换行和缩进会被保留,建议在 HTML 中一行写完默认值。
- 按钮元素(
<button>
)type
决定行为(submit
、reset
、button
)。- 自定义操作需绑定
onclick
事件或addEventListener
。
- 下拉框(
表单 DOM 交互
- 获取控件值
- 通过
element.value
获取文本框、下拉框等控件的值。 textarea
的默认值通过闭合标签内的文本设置。
- 通过
- 文件处理
- 使用
FileReader
API 读取文件内容(readAsText
方法)。 - 注意
files
属性获取文件列表,而非value
。
- 使用
- 事件绑定
oninput
实时触发(适合搜索),onchange
用户离开输入框后触发(适合验证)。- 推荐使用
addEventListener
绑定事件,避免内联事件。
- 获取控件值
表单提交与数据绑定
- 阻止默认提交行为
- 通过
event.preventDefault()
阻止页面刷新,实现纯前端处理。
- 通过
FormData
对象- 提取表单数据并转换为普通对象(
Object.fromEntries(formData)
)。
- 提取表单数据并转换为普通对象(
- 数据绑定范式
- 将变量更新封装为
set
函数,实现数据与视图的同步(如计算立方值示例)。
- 将变量更新封装为
- 阻止默认提交行为
表单验证
- HTML 内置验证
required
、min
/max
、minlength
/maxlength
等属性。- 浏览器自带验证规则有限,无法满足复杂需求。
valibot
库- 轻量级表单验证库,支持链式规则定义。
- 适用于复杂表单的结构化校验(如用户名长度、邮箱格式)。
- HTML 内置验证
课后练习
(单选题)以下哪个属性用于限制输入框的最大值?
- A.
min
- B.
max
- C.
step
- D.
required
- A.
(单选题)要创建一个可多选的下拉框,应使用属性 ______。
- A.
multiple
- B.
required
- C.
checked
- D.
selected
- A.
(填空题)
<input type="number">
的______
属性用于设定步长。(填空题)使用
FileReader
读取文件内容的方法是 ______。(纠错题)以下代码无法正确获取文件内容的原因是什么?
html<input type="file" id="file"> <script> const file = document.getElementById('file').value; console.log(file); </script>
(编程题)编写代码:实现一个表单,用户输入
a, b
两个数,下方实时显示的方程的解。 要求:
- 允许用户输入任意数字,包括负数和小数。
- 使用 HTML 自带验证工具拦截非数字或空输入。当用户输入合法时,计算并显示方程的解。
- 若方程没有实数解,则显示
无解
;若方程有两个相同的实根,则显示x1=x2=...
;若方程有两个不同的实根,则显示x1=...; x2=...
。 - 只需支持五位有效数字的数值解,不需要使用根号等符号输出。
提示:
- 可以使用
Math.sqrt()
计算平方根。 - 可以对
number
使用toPrecision()
方法获取指定位数有效数字的字符串。 - 对
form
元素也可以直接绑定input
事件,同时监听内部的所有控件。
参考答案
- B. 解析:
min
表示最小值,max
表示最大值,step
表示步长,required
表示必须填写。 - A. 解析:
multiple
是<select>
元素的属性,用于允许多选;required
表示必填,checked
用于复选框/单选框的默认选中状态,selected
用于下拉框的默认选中项。 step
. 解析:step
属性定义输入数值的步长(如step="0.5"
表示每次增减 0.5)。readAsText
. 解析:FileReader
的readAsText
方法将文件内容读取为字符串,需通过onload
事件获取结果。- 应通过
.files
属性获取文件对象,而非.value
。 - 参考代码实现:html解析:
<form id="quadratic-form"> <label> a: <input type="number" id="a" name="a" step="any" s required> </label> <label> b: <input type="number" id="b" name="b" step="any" s required> </label> <div id="result"></div> </form> <style> form { display: flex; flex-direction: column; align-items: center; } #result { font-weight: bold; font-size: 1.25rem; font-family: 'Cambria Math', '楷体', 'sans-serif'; } form input { width: 5rem; } </style> <script> document.getElementById("quadratic-form").addEventListener("input", solveEquation); function solveEquation() { const a = parseFloat(document.getElementById("a").value); const b = parseFloat(document.getElementById("b").value); const result = document.getElementById("result"); let output = ""; if (isNaN(a) || isNaN(b)) { result.textContent = "请输入有效数字"; return; } const delta = a * a - 4 * b; if (delta < 0) { output = "无解"; } else if (delta === 0) { const root = (-a) / 2; output = `x1=x2=${root.toPrecision(5)}`; } else { const sqrtDelta = Math.sqrt(delta); const x1 = (-a + sqrtDelta) / 2; const x2 = (-a - sqrtDelta) / 2; output = `x1=${x1.toPrecision(5)}; x2=${x2.toPrecision(5)}`; } result.textContent = output; } </script>
- 表单验证:通过
type="number"
和required
确保输入合法。 - 计算逻辑:
- 判别式
delta = a² - 4b
决定解的类型。 - 若
delta < 0
:无实数解。 - 若
delta === 0
:唯一解x = -a/2
。 - 若
delta > 0
:两个不同解。
- 判别式
- 精度控制:使用
toPrecision(5)
保留五位有效数字。 - 实时更新:通过
input
事件监听输入变化,即时计算并更新结果。
- 表单验证:通过