第21集:uni-app 表单开发
章节概览
在本章节中,我们将学习 uni-app 表单开发的核心知识点和最佳实践。表单是用户与应用交互的重要界面元素,用于收集用户输入的数据。通过本章节的学习,您将掌握表单组件的使用、表单验证的实现、数据收集的方法,以及如何构建用户友好的表单界面。
核心知识点
1. 表单组件使用
uni-app 提供了丰富的表单组件,包括:
- input:文本输入框,支持多种类型如 text、number、password 等
- textarea:多行文本输入框,用于输入较长的文本
- picker:选择器,支持日期、时间、普通选择等
- picker-view:内嵌选择器,可自定义选择内容
- switch:开关组件,用于布尔值选择
- slider:滑块组件,用于范围选择
- radio:单选按钮组
- checkbox:复选框组
- form:表单容器,用于统一管理表单组件
2. 表单验证
表单验证是确保用户输入数据合法性的重要环节,包括:
- 实时验证:在用户输入过程中实时验证数据
- 提交验证:在表单提交时进行完整验证
- 自定义验证规则:根据业务需求定义验证规则
- 错误提示:向用户显示清晰的错误提示信息
3. 数据收集与处理
数据收集与处理是表单开发的核心目标,包括:
- 数据绑定:使用 v-model 实现数据双向绑定
- 数据格式化:对用户输入的数据进行格式化处理
- 数据提交:将收集到的数据提交到后端服务
- 表单重置:清空表单数据,恢复初始状态
4. 表单界面优化
表单界面优化可以提高用户体验,包括:
- 布局设计:合理的表单布局,提高可读性
- 响应式设计:适配不同屏幕尺寸的设备
- 交互反馈:提供清晰的交互反馈,如加载状态、成功提示等
- 无障碍访问:确保表单可以被屏幕阅读器等辅助技术访问
实用案例
实现用户注册表单
我们将实现一个完整的用户注册表单,包括:
- 表单字段:用户名、密码、确认密码、邮箱、手机号、验证码等
- 表单验证:实时验证和提交验证
- 交互体验:密码强度提示、验证码发送倒计时等
- 数据提交:将表单数据提交到后端服务
代码示例
1. 基础表单组件使用
文本输入框
<template>
<view class="form-item">
<text class="label">用户名:</text>
<input
v-model="form.username"
type="text"
placeholder="请输入用户名"
class="input"
@input="handleInput('username')"
/>
<text v-if="errors.username" class="error-message">{{ errors.username }}</text>
</view>
</template>
<script>
export default {
data() {
return {
form: {
username: ''
},
errors: {
username: ''
}
}
},
methods: {
handleInput(field) {
// 实时验证
this.validateField(field)
},
validateField(field) {
if (field === 'username') {
if (!this.form.username) {
this.errors.username = '用户名不能为空'
} else if (this.form.username.length < 3) {
this.errors.username = '用户名长度不能少于3位'
} else {
this.errors.username = ''
}
}
}
}
}
</script>
<style scoped>
.form-item {
margin-bottom: 30rpx;
}
.label {
display: block;
margin-bottom: 10rpx;
font-size: 28rpx;
font-weight: 500;
}
.input {
width: 100%;
height: 70rpx;
border: 1rpx solid #e5e5e5;
border-radius: 5rpx;
padding: 0 20rpx;
font-size: 30rpx;
}
.error-message {
display: block;
margin-top: 10rpx;
font-size: 24rpx;
color: #ff4d4f;
}
</style>密码输入框
<template>
<view class="form-item">
<text class="label">密码:</text>
<view class="password-input">
<input
v-model="form.password"
type="{{ showPassword ? 'text' : 'password' }}"
placeholder="请输入密码"
class="input"
@input="handleInput('password')"
/>
<text @click="showPassword = !showPassword" class="toggle-password">
{{ showPassword ? '隐藏' : '显示' }}
</text>
</view>
<text v-if="errors.password" class="error-message">{{ errors.password }}</text>
<view v-if="form.password" class="password-strength">
<text class="strength-label">密码强度:</text>
<view class="strength-bars">
<view
v-for="i in 3"
:key="i"
class="strength-bar"
:class="{ 'strong': i <= getPasswordStrength() }"
></view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
form: {
password: ''
},
errors: {
password: ''
},
showPassword: false
}
},
methods: {
handleInput(field) {
this.validateField(field)
},
validateField(field) {
if (field === 'password') {
if (!this.form.password) {
this.errors.password = '密码不能为空'
} else if (this.form.password.length < 6) {
this.errors.password = '密码长度不能少于6位'
} else {
this.errors.password = ''
}
}
},
getPasswordStrength() {
const password = this.form.password
if (!password) return 0
let strength = 0
if (password.length >= 8) strength++
if (/[A-Z]/.test(password)) strength++
if (/[0-9]/.test(password)) strength++
if (/[^A-Za-z0-9]/.test(password)) strength++
return Math.min(strength, 3)
}
}
}
</script>
<style scoped>
.form-item {
margin-bottom: 30rpx;
}
.label {
display: block;
margin-bottom: 10rpx;
font-size: 28rpx;
font-weight: 500;
}
.password-input {
position: relative;
display: flex;
align-items: center;
}
.input {
flex: 1;
height: 70rpx;
border: 1rpx solid #e5e5e5;
border-radius: 5rpx;
padding: 0 20rpx;
font-size: 30rpx;
}
.toggle-password {
position: absolute;
right: 20rpx;
font-size: 26rpx;
color: #007aff;
}
.error-message {
display: block;
margin-top: 10rpx;
font-size: 24rpx;
color: #ff4d4f;
}
.password-strength {
margin-top: 10rpx;
display: flex;
align-items: center;
}
.strength-label {
font-size: 24rpx;
margin-right: 10rpx;
}
.strength-bars {
display: flex;
gap: 5rpx;
}
.strength-bar {
width: 60rpx;
height: 8rpx;
background-color: #e5e5e5;
border-radius: 4rpx;
}
.strength-bar.strong {
background-color: #52c41a;
}
</style>选择器组件
<template>
<view class="form-item">
<text class="label">出生日期:</text>
<view
class="picker"
@tap="showDatePicker = true"
>
<text v-if="form.birthdate">{{ form.birthdate }}</text>
<text v-else class="placeholder">请选择出生日期</text>
</view>
<text v-if="errors.birthdate" class="error-message">{{ errors.birthdate }}</text>
<!-- 日期选择器 -->
<picker
v-if="showDatePicker"
mode="date"
:value="dateValue"
:start="'1900-01-01'"
:end="currentDate"
@change="handleDateChange"
@cancel="showDatePicker = false"
>
<view class="picker-modal"></view>
</picker>
</view>
</template>
<script>
export default {
data() {
return {
form: {
birthdate: ''
},
errors: {
birthdate: ''
},
showDatePicker: false,
dateValue: '',
currentDate: ''
}
},
mounted() {
// 设置当前日期
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
this.currentDate = `${year}-${month}-${day}`
this.dateValue = this.currentDate
},
methods: {
handleDateChange(e) {
this.form.birthdate = e.detail.value
this.showDatePicker = false
this.validateField('birthdate')
},
validateField(field) {
if (field === 'birthdate') {
if (!this.form.birthdate) {
this.errors.birthdate = '请选择出生日期'
} else {
this.errors.birthdate = ''
}
}
}
}
}
</script>
<style scoped>
.form-item {
margin-bottom: 30rpx;
}
.label {
display: block;
margin-bottom: 10rpx;
font-size: 28rpx;
font-weight: 500;
}
.picker {
height: 70rpx;
border: 1rpx solid #e5e5e5;
border-radius: 5rpx;
padding: 0 20rpx;
display: flex;
align-items: center;
font-size: 30rpx;
}
.placeholder {
color: #999;
}
.error-message {
display: block;
margin-top: 10rpx;
font-size: 24rpx;
color: #ff4d4f;
}
.picker-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.4);
}
</style>2. 完整的用户注册表单
<template>
<view class="register-form">
<view class="form-header">
<text class="form-title">用户注册</text>
<text class="form-subtitle">请填写以下信息完成注册</text>
</view>
<view class="form-content">
<!-- 用户名 -->
<view class="form-item">
<text class="label">用户名:</text>
<input
v-model="form.username"
type="text"
placeholder="请输入用户名"
class="input"
@input="handleInput('username')"
/>
<text v-if="errors.username" class="error-message">{{ errors.username }}</text>
</view>
<!-- 密码 -->
<view class="form-item">
<text class="label">密码:</text>
<view class="password-input">
<input
v-model="form.password"
type="{{ showPassword ? 'text' : 'password' }}"
placeholder="请输入密码"
class="input"
@input="handleInput('password')"
/>
<text @click="showPassword = !showPassword" class="toggle-password">
{{ showPassword ? '隐藏' : '显示' }}
</text>
</view>
<text v-if="errors.password" class="error-message">{{ errors.password }}</text>
<view v-if="form.password" class="password-strength">
<text class="strength-label">密码强度:</text>
<view class="strength-bars">
<view
v-for="i in 3"
:key="i"
class="strength-bar"
:class="{ 'strong': i <= getPasswordStrength() }"
></view>
</view>
</view>
</view>
<!-- 确认密码 -->
<view class="form-item">
<text class="label">确认密码:</text>
<input
v-model="form.confirmPassword"
type="password"
placeholder="请再次输入密码"
class="input"
@input="handleInput('confirmPassword')"
/>
<text v-if="errors.confirmPassword" class="error-message">{{ errors.confirmPassword }}</text>
</view>
<!-- 邮箱 -->
<view class="form-item">
<text class="label">邮箱:</text>
<input
v-model="form.email"
type="email"
placeholder="请输入邮箱地址"
class="input"
@input="handleInput('email')"
/>
<text v-if="errors.email" class="error-message">{{ errors.email }}</text>
</view>
<!-- 手机号 -->
<view class="form-item">
<text class="label">手机号:</text>
<input
v-model="form.phone"
type="number"
placeholder="请输入手机号"
class="input"
@input="handleInput('phone')"
/>
<text v-if="errors.phone" class="error-message">{{ errors.phone }}</text>
</view>
<!-- 验证码 -->
<view class="form-item">
<text class="label">验证码:</text>
<view class="verification-code">
<input
v-model="form.code"
type="number"
placeholder="请输入验证码"
class="input code-input"
@input="handleInput('code')"
/>
<button
@click="sendVerificationCode"
class="send-code-btn"
:disabled="countdown > 0"
>
{{ countdown > 0 ? `${countdown}s后重发` : '发送验证码' }}
</button>
</view>
<text v-if="errors.code" class="error-message">{{ errors.code }}</text>
</view>
<!-- 协议同意 -->
<view class="form-item agreement">
<checkbox v-model="form.agreed" class="checkbox" />
<text class="agreement-text">
我已阅读并同意 <text class="link">《用户协议》</text> 和 <text class="link">《隐私政策》</text>
</text>
<text v-if="errors.agreed" class="error-message">{{ errors.agreed }}</text>
</view>
<!-- 提交按钮 -->
<button
@click="handleSubmit"
class="submit-btn"
:loading="loading"
:disabled="loading"
>
注册
</button>
</view>
</view>
</template>
<script>
export default {
data() {
return {
form: {
username: '',
password: '',
confirmPassword: '',
email: '',
phone: '',
code: '',
agreed: false
},
errors: {
username: '',
password: '',
confirmPassword: '',
email: '',
phone: '',
code: '',
agreed: ''
},
showPassword: false,
countdown: 0,
loading: false
}
},
methods: {
// 输入事件处理
handleInput(field) {
this.validateField(field)
},
// 验证单个字段
validateField(field) {
switch (field) {
case 'username':
if (!this.form.username) {
this.errors.username = '用户名不能为空'
} else if (this.form.username.length < 3) {
this.errors.username = '用户名长度不能少于3位'
} else {
this.errors.username = ''
}
break
case 'password':
if (!this.form.password) {
this.errors.password = '密码不能为空'
} else if (this.form.password.length < 6) {
this.errors.password = '密码长度不能少于6位'
} else {
this.errors.password = ''
// 验证确认密码
if (this.form.confirmPassword) {
this.validateField('confirmPassword')
}
}
break
case 'confirmPassword':
if (!this.form.confirmPassword) {
this.errors.confirmPassword = '请确认密码'
} else if (this.form.confirmPassword !== this.form.password) {
this.errors.confirmPassword = '两次输入的密码不一致'
} else {
this.errors.confirmPassword = ''
}
break
case 'email':
if (!this.form.email) {
this.errors.email = '邮箱不能为空'
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.form.email)) {
this.errors.email = '请输入有效的邮箱地址'
} else {
this.errors.email = ''
}
break
case 'phone':
if (!this.form.phone) {
this.errors.phone = '手机号不能为空'
} else if (!/^1[3-9]\d{9}$/.test(this.form.phone)) {
this.errors.phone = '请输入有效的手机号'
} else {
this.errors.phone = ''
}
break
case 'code':
if (!this.form.code) {
this.errors.code = '验证码不能为空'
} else if (this.form.code.length !== 6) {
this.errors.code = '请输入6位验证码'
} else {
this.errors.code = ''
}
break
case 'agreed':
if (!this.form.agreed) {
this.errors.agreed = '请阅读并同意用户协议和隐私政策'
} else {
this.errors.agreed = ''
}
break
}
},
// 验证所有字段
validateAll() {
const fields = Object.keys(this.form)
fields.forEach(field => this.validateField(field))
// 检查是否有错误
return Object.values(this.errors).every(error => error === '')
},
// 获取密码强度
getPasswordStrength() {
const password = this.form.password
if (!password) return 0
let strength = 0
if (password.length >= 8) strength++
if (/[A-Z]/.test(password)) strength++
if (/[0-9]/.test(password)) strength++
if (/[^A-Za-z0-9]/.test(password)) strength++
return Math.min(strength, 3)
},
// 发送验证码
sendVerificationCode() {
// 验证手机号
this.validateField('phone')
if (this.errors.phone) {
return
}
// 模拟发送验证码
uni.showToast({
title: '验证码已发送',
icon: 'success'
})
// 开始倒计时
this.countdown = 60
const timer = setInterval(() => {
this.countdown--
if (this.countdown <= 0) {
clearInterval(timer)
}
}, 1000)
},
// 提交表单
async handleSubmit() {
// 验证所有字段
if (!this.validateAll()) {
return
}
// 显示加载状态
this.loading = true
try {
// 模拟表单提交
await new Promise(resolve => setTimeout(resolve, 1000))
// 提交成功
uni.showToast({
title: '注册成功',
icon: 'success'
})
// 跳转到登录页面
setTimeout(() => {
uni.navigateTo({
url: '/pages/login/login'
})
}, 1000)
} catch (error) {
console.error('注册失败:', error)
uni.showToast({
title: '注册失败,请稍后重试',
icon: 'none'
})
} finally {
// 隐藏加载状态
this.loading = false
}
}
}
}
</script>
<style scoped>
.register-form {
padding: 30rpx;
background-color: #fff;
min-height: 100vh;
}
.form-header {
text-align: center;
margin-bottom: 50rpx;
}
.form-title {
display: block;
font-size: 40rpx;
font-weight: bold;
margin-bottom: 10rpx;
}
.form-subtitle {
display: block;
font-size: 28rpx;
color: #666;
}
.form-content {
max-width: 600rpx;
margin: 0 auto;
}
.form-item {
margin-bottom: 30rpx;
}
.label {
display: block;
margin-bottom: 10rpx;
font-size: 28rpx;
font-weight: 500;
}
.input {
width: 100%;
height: 70rpx;
border: 1rpx solid #e5e5e5;
border-radius: 5rpx;
padding: 0 20rpx;
font-size: 30rpx;
}
.password-input {
position: relative;
display: flex;
align-items: center;
}
.password-input .input {
flex: 1;
}
.toggle-password {
position: absolute;
right: 20rpx;
font-size: 26rpx;
color: #007aff;
}
.error-message {
display: block;
margin-top: 10rpx;
font-size: 24rpx;
color: #ff4d4f;
}
.password-strength {
margin-top: 10rpx;
display: flex;
align-items: center;
}
.strength-label {
font-size: 24rpx;
margin-right: 10rpx;
}
.strength-bars {
display: flex;
gap: 5rpx;
}
.strength-bar {
width: 60rpx;
height: 8rpx;
background-color: #e5e5e5;
border-radius: 4rpx;
}
.strength-bar.strong {
background-color: #52c41a;
}
.verification-code {
display: flex;
gap: 10rpx;
}
.code-input {
flex: 1;
}
.send-code-btn {
width: 200rpx;
height: 70rpx;
background-color: #007aff;
color: #fff;
border-radius: 5rpx;
font-size: 26rpx;
display: flex;
align-items: center;
justify-content: center;
}
.send-code-btn:disabled {
background-color: #ccc;
}
.agreement {
display: flex;
align-items: flex-start;
gap: 10rpx;
}
.checkbox {
margin-top: 5rpx;
}
.agreement-text {
flex: 1;
font-size: 26rpx;
color: #666;
}
.link {
color: #007aff;
}
.submit-btn {
width: 100%;
height: 80rpx;
background-color: #007aff;
color: #fff;
font-size: 32rpx;
border-radius: 5rpx;
margin-top: 40rpx;
}
.submit-btn:disabled {
background-color: #ccc;
}
</style>3. 表单数据处理
表单数据收集与提交
// 表单数据收集
export function collectFormData(form) {
// 可以在这里对表单数据进行处理和格式化
return {
...form,
// 移除不需要提交的字段
confirmPassword: undefined,
agreed: undefined
}
}
// 表单提交
export async function submitForm(formData) {
try {
const response = await uni.request({
url: '/api/auth/register',
method: 'POST',
data: formData
})
if (response.statusCode === 200) {
return response.data
} else {
throw new Error(response.data.message || '提交失败')
}
} catch (error) {
console.error('表单提交失败:', error)
throw error
}
}
// 表单重置
export function resetForm(form) {
Object.keys(form).forEach(key => {
if (typeof form[key] === 'string') {
form[key] = ''
} else if (typeof form[key] === 'boolean') {
form[key] = false
} else if (Array.isArray(form[key])) {
form[key] = []
} else if (typeof form[key] === 'object' && form[key] !== null) {
form[key] = {}
}
})
}表单验证工具
// utils/validator.js
// 验证规则
export const rules = {
// 必填项
required: (value, message = '此项为必填项') => {
if (value === undefined || value === null || value === '') {
return message
}
if (Array.isArray(value) && value.length === 0) {
return message
}
return ''
},
// 最小长度
minLength: (value, min, message = `长度不能少于${min}位`) => {
if (value && value.length < min) {
return message
}
return ''
},
// 最大长度
maxLength: (value, max, message = `长度不能超过${max}位`) => {
if (value && value.length > max) {
return message
}
return ''
},
// 手机号验证
phone: (value, message = '请输入有效的手机号') => {
if (value && !/^1[3-9]\d{9}$/.test(value)) {
return message
}
return ''
},
// 邮箱验证
email: (value, message = '请输入有效的邮箱地址') => {
if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
return message
}
return ''
},
// 密码验证
password: (value, message = '密码长度不能少于6位') => {
if (value && value.length < 6) {
return message
}
return ''
},
// 两次密码一致
confirmPassword: (value, password, message = '两次输入的密码不一致') => {
if (value !== password) {
return message
}
return ''
}
}
// 验证函数
export function validate(value, validations) {
for (const validation of validations) {
if (Array.isArray(validation)) {
const [rule, ...params] = validation
const error = rules[rule](value, ...params)
if (error) {
return error
}
} else {
const error = rules[validation](value)
if (error) {
return error
}
}
}
return ''
}
// 表单验证
export function validateForm(form, validationRules) {
const errors = {}
let isValid = true
for (const field in validationRules) {
const error = validate(form[field], validationRules[field])
errors[field] = error
if (error) {
isValid = false
}
}
return { isValid, errors }
}常见问题与解决方案
1. 表单验证问题
问题:表单验证逻辑复杂,维护困难
解决方案:
- 使用验证工具函数封装验证逻辑,提高代码复用性
- 采用实时验证和提交验证相结合的方式,提高用户体验
- 为不同类型的表单字段定义统一的验证规则
- 使用计算属性或监听器实现实时验证
2. 表单数据处理问题
问题:表单数据处理繁琐,容易出错
解决方案:
- 使用 v-model 实现数据双向绑定,简化数据收集
- 对表单数据进行结构化管理,按业务逻辑分组
- 使用工具函数处理数据格式化和转换
- 实现表单数据的本地缓存,防止页面刷新导致数据丢失
3. 表单界面用户体验问题
问题:表单界面不够友好,用户操作不便
解决方案:
- 合理设计表单布局,使用分组和标签提高可读性
- 为表单字段提供清晰的占位符和错误提示
- 实现表单字段的自动聚焦和键盘导航
- 添加加载状态和成功提示,提高交互反馈
- 支持表单重置和数据恢复功能
4. 跨平台兼容性问题
问题:表单组件在不同平台表现不一致
解决方案:
- 使用 uni-app 提供的跨平台表单组件,避免使用平台特定组件
- 针对不同平台进行样式适配,使用条件编译处理平台差异
- 测试表单在不同平台的表现,确保功能正常
- 参考 uni-app 官方文档,了解平台差异和解决方案
学习总结
通过本章节的学习,您已经掌握了以下内容:
- 表单组件使用:学会了使用 uni-app 提供的各种表单组件,如 input、textarea、picker、switch 等
- 表单验证实现:掌握了实时验证和提交验证的实现方法,以及自定义验证规则的编写
- 数据收集与处理:学会了使用 v-model 进行数据绑定,以及表单数据的格式化和提交
- 表单界面优化:掌握了如何设计用户友好的表单界面,提高用户体验
- 实用案例:实现了完整的用户注册表单,包括密码强度提示、验证码发送等功能
表单开发是 uni-app 应用开发中的重要部分,良好的表单设计可以提高用户体验和数据收集的准确性。在实际项目中,您应该根据具体的业务需求和用户场景,灵活应用所学知识,构建既美观又实用的表单界面。
通过本章节的学习,您已经具备了开发复杂表单的能力,可以应对各种表单开发场景,为用户提供流畅、直观的表单交互体验。