Skip to content

表单组件详解 - 输入框、选择器、开关按钮

📝 表单是用户与应用交互的重要方式,今天我们来学习各种表单组件

1. input - 输入框

input 是最常用的表单组件:

基本用法

vue
<template>
  <view class="form-demo">
    <!-- 基础输入框 -->
    <input 
      v-model="username" 
      placeholder="请输入用户名"
      class="input-item"
    />
    
    <!-- 密码输入框 -->
    <input 
      v-model="password" 
      type="password"
      placeholder="请输入密码"
      class="input-item"
    />
    
    <!-- 数字输入框 -->
    <input 
      v-model="phone" 
      type="number"
      placeholder="请输入手机号"
      maxlength="11"
      class="input-item"
    />
    
    <!-- 带事件的输入框 -->
    <input 
      v-model="email" 
      type="text"
      placeholder="请输入邮箱"
      @input="onEmailInput"
      @blur="onEmailBlur"
      @focus="onEmailFocus"
      class="input-item"
    />
  </view>
</template>

<script>
export default {
  data() {
    return {
      username: '',
      password: '',
      phone: '',
      email: ''
    }
  },
  methods: {
    onEmailInput(e) {
      console.log('邮箱输入中:', e.detail.value)
    },
    onEmailBlur(e) {
      console.log('邮箱失去焦点:', e.detail.value)
      this.validateEmail(e.detail.value)
    },
    onEmailFocus(e) {
      console.log('邮箱获得焦点')
    },
    validateEmail(email) {
      const emailReg = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
      if (email && !emailReg.test(email)) {
        uni.showToast({
          title: '邮箱格式不正确',
          icon: 'error'
        })
      }
    }
  }
}
</script>

<style>
.form-demo {
  padding: 20px;
}
.input-item {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 12px 15px;
  margin-bottom: 15px;
  font-size: 16px;
}
</style>

2. picker - 选择器

picker 用于从预设选项中选择:

普通选择器

vue
<template>
  <view class="picker-demo">
    <!-- 单列选择器 -->
    <picker 
      :range="cities" 
      :value="cityIndex"
      @change="onCityChange"
    >
      <view class="picker-item">
        <text>选择城市:\{\{ cities[cityIndex] \}\}</text>
        <text class="arrow">></text>
      </view>
    </picker>
    
    <!-- 多列选择器 -->
    <picker 
      mode="multiSelector"
      :range="regions" 
      :value="regionIndex"
      @change="onRegionChange"
    >
      <view class="picker-item">
        <text>选择地区:\{\{ selectedRegion \}\}</text>
        <text class="arrow">></text>
      </view>
    </picker>
  </view>
</template>

<script>
export default {
  data() {
    return {
      cities: ['北京', '上海', '广州', '深圳', '杭州'],
      cityIndex: 0,
      
      regions: [
        ['广东省', '浙江省', '江苏省'],
        ['广州市', '深圳市', '珠海市'],
        ['天河区', '越秀区', '海珠区']
      ],
      regionIndex: [0, 0, 0]
    }
  },
  computed: {
    selectedRegion() {
      const [provinceIndex, cityIndex, districtIndex] = this.regionIndex
      return `\${this.regions[0][provinceIndex]} \${this.regions[1][cityIndex]} \${this.regions[2][districtIndex]}`
    }
  },
  methods: {
    onCityChange(e) {
      this.cityIndex = e.detail.value
      console.log('选择的城市:', this.cities[this.cityIndex])
    },
    onRegionChange(e) {
      this.regionIndex = e.detail.value
      console.log('选择的地区:', this.selectedRegion)
    }
  }
}
</script>

时间和日期选择器

vue
<template>
  <view class="time-picker-demo">
    <!-- 日期选择器 -->
    <picker 
      mode="date"
      :value="selectedDate"
      start="2020-01-01"
      end="2030-12-31"
      @change="onDateChange"
    >
      <view class="picker-item">
        <text>选择日期:\{\{ selectedDate \}\}</text>
        <text class="arrow">></text>
      </view>
    </picker>
    
    <!-- 时间选择器 -->
    <picker 
      mode="time"
      :value="selectedTime"
      @change="onTimeChange"
    >
      <view class="picker-item">
        <text>选择时间:\{\{ selectedTime \}\}</text>
        <text class="arrow">></text>
      </view>
    </picker>
  </view>
</template>

<script>
export default {
  data() {
    return {
      selectedDate: '2024-01-01',
      selectedTime: '12:00'
    }
  },
  methods: {
    onDateChange(e) {
      this.selectedDate = e.detail.value
      console.log('选择的日期:', this.selectedDate)
    },
    onTimeChange(e) {
      this.selectedTime = e.detail.value
      console.log('选择的时间:', this.selectedTime)
    }
  }
}
</script>

3. switch - 开关

switch 用于开关状态的切换:

vue
<template>
  <view class="switch-demo">
    <view class="setting-item">
      <text>消息推送</text>
      <switch 
        :checked="settings.notification"
        @change="onNotificationChange"
        color="#007aff"
      />
    </view>
    
    <view class="setting-item">
      <text>夜间模式</text>
      <switch 
        :checked="settings.darkMode"
        @change="onDarkModeChange"
        color="#34c759"
      />
    </view>
    
    <view class="setting-item">
      <text>自动播放</text>
      <switch 
        :checked="settings.autoPlay"
        @change="onAutoPlayChange"
        color="#ff9500"
      />
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      settings: {
        notification: true,
        darkMode: false,
        autoPlay: true
      }
    }
  },
  methods: {
    onNotificationChange(e) {
      this.settings.notification = e.detail.value
      console.log('消息推送:', this.settings.notification)
      
      // 保存设置到本地
      this.saveSettings()
    },
    onDarkModeChange(e) {
      this.settings.darkMode = e.detail.value
      console.log('夜间模式:', this.settings.darkMode)
      this.saveSettings()
    },
    onAutoPlayChange(e) {
      this.settings.autoPlay = e.detail.value
      console.log('自动播放:', this.settings.autoPlay)
      this.saveSettings()
    },
    saveSettings() {
      uni.setStorageSync('userSettings', this.settings)
    }
  }
}
</script>

<style>
.switch-demo {
  padding: 20px;
}
.setting-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 15px 0;
  border-bottom: 1px solid #f0f0f0;
}
</style>

4. slider - 滑块

slider 用于选择数值范围:

vue
<template>
  <view class="slider-demo">
    <view class="slider-item">
      <text>音量:\{\{ volume \}\}%</text>
      <slider 
        :value="volume"
        min="0"
        max="100"
        @change="onVolumeChange"
        activeColor="#007aff"
        backgroundColor="#e9e9e9"
        block-color="#007aff"
        block-size="20"
      />
    </view>
    
    <view class="slider-item">
      <text>亮度:\{\{ brightness \}\}%</text>
      <slider 
        :value="brightness"
        min="10"
        max="100"
        step="10"
        @change="onBrightnessChange"
        activeColor="#34c759"
      />
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      volume: 50,
      brightness: 80
    }
  },
  methods: {
    onVolumeChange(e) {
      this.volume = e.detail.value
      console.log('音量调整为:', this.volume)
    },
    onBrightnessChange(e) {
      this.brightness = e.detail.value
      console.log('亮度调整为:', this.brightness)
    }
  }
}
</script>

<style>
.slider-demo {
  padding: 20px;
}
.slider-item {
  margin-bottom: 30px;
}
.slider-item text {
  display: block;
  margin-bottom: 10px;
  font-size: 16px;
}
</style>

5. 完整表单案例:用户注册

让我们做一个完整的注册表单:

vue
<template>
  <view class="register-form">
    <view class="form-title">用户注册</view>
    
    <!-- 用户名 -->
    <view class="form-group">
      <text class="label">用户名</text>
      <input 
        v-model="formData.username"
        placeholder="请输入用户名"
        class="form-input"
        @blur="validateUsername"
      />
      <text v-if="errors.username" class="error-text">\{\{ errors.username \}\}</text>
    </view>
    
    <!-- 密码 -->
    <view class="form-group">
      <text class="label">密码</text>
      <input 
        v-model="formData.password"
        type="password"
        placeholder="请输入密码"
        class="form-input"
        @blur="validatePassword"
      />
      <text v-if="errors.password" class="error-text">\{\{ errors.password \}\}</text>
    </view>
    
    <!-- 手机号 -->
    <view class="form-group">
      <text class="label">手机号</text>
      <input 
        v-model="formData.phone"
        type="number"
        placeholder="请输入手机号"
        maxlength="11"
        class="form-input"
        @blur="validatePhone"
      />
      <text v-if="errors.phone" class="error-text">\{\{ errors.phone \}\}</text>
    </view>
    
    <!-- 性别选择 -->
    <view class="form-group">
      <text class="label">性别</text>
      <picker 
        :range="genderOptions" 
        :value="genderIndex"
        @change="onGenderChange"
      >
        <view class="picker-input">
          <text>\{\{ genderOptions[genderIndex] \}\}</text>
          <text class="arrow">></text>
        </view>
      </picker>
    </view>
    
    <!-- 生日选择 -->
    <view class="form-group">
      <text class="label">生日</text>
      <picker 
        mode="date"
        :value="formData.birthday"
        end="2010-12-31"
        @change="onBirthdayChange"
      >
        <view class="picker-input">
          <text>\{\{ formData.birthday || '请选择生日' \}\}</text>
          <text class="arrow">></text>
        </view>
      </picker>
    </view>
    
    <!-- 同意协议 -->
    <view class="form-group">
      <view class="agreement">
        <switch 
          :checked="formData.agreeTerms"
          @change="onAgreeChange"
          color="#007aff"
        />
        <text class="agreement-text">我已阅读并同意</text>
        <text class="link-text" @click="showTerms">《用户协议》</text>
      </view>
    </view>
    
    <!-- 提交按钮 -->
    <button 
      class="submit-btn"
      :disabled="!canSubmit"
      @click="handleSubmit"
      :loading="isSubmitting"
    >
      \{\{ isSubmitting ? '注册中...' : '立即注册' \}\}
    </button>
  </view>
</template>

<script>
export default {
  data() {
    return {
      formData: {
        username: '',
        password: '',
        phone: '',
        gender: '',
        birthday: '',
        agreeTerms: false
      },
      errors: {},
      genderOptions: ['男', '女', '保密'],
      genderIndex: 0,
      isSubmitting: false
    }
  },
  
  computed: {
    canSubmit() {
      return this.formData.username && 
             this.formData.password && 
             this.formData.phone && 
             this.formData.agreeTerms &&
             Object.keys(this.errors).length === 0
    }
  },
  
  methods: {
    validateUsername() {
      if (!this.formData.username) {
        this.$set(this.errors, 'username', '请输入用户名')
      } else if (this.formData.username.length < 3) {
        this.$set(this.errors, 'username', '用户名至少3个字符')
      } else {
        this.$delete(this.errors, 'username')
      }
    },
    
    validatePassword() {
      if (!this.formData.password) {
        this.$set(this.errors, 'password', '请输入密码')
      } else if (this.formData.password.length < 6) {
        this.$set(this.errors, 'password', '密码至少6个字符')
      } else {
        this.$delete(this.errors, 'password')
      }
    },
    
    validatePhone() {
      const phoneReg = /^1[3-9]\d{9}$/
      if (!this.formData.phone) {
        this.$set(this.errors, 'phone', '请输入手机号')
      } else if (!phoneReg.test(this.formData.phone)) {
        this.$set(this.errors, 'phone', '手机号格式不正确')
      } else {
        this.$delete(this.errors, 'phone')
      }
    },
    
    onGenderChange(e) {
      this.genderIndex = e.detail.value
      this.formData.gender = this.genderOptions[this.genderIndex]
    },
    
    onBirthdayChange(e) {
      this.formData.birthday = e.detail.value
    },
    
    onAgreeChange(e) {
      this.formData.agreeTerms = e.detail.value
    },
    
    showTerms() {
      uni.navigateTo({
        url: '/pages/terms/terms'
      })
    },
    
    async handleSubmit() {
      // 验证所有字段
      this.validateUsername()
      this.validatePassword()
      this.validatePhone()
      
      if (!this.canSubmit) {
        uni.showToast({
          title: '请完善表单信息',
          icon: 'error'
        })
        return
      }
      
      this.isSubmitting = true
      
      try {
        const res = await uni.request({
          url: '/api/register',
          method: 'POST',
          data: this.formData
        })
        
        uni.showToast({
          title: '注册成功',
          icon: 'success'
        })
        
        // 跳转到登录页面
        setTimeout(() => {
          uni.redirectTo({
            url: '/pages/login/login'
          })
        }, 1500)
        
      } catch (err) {
        console.error('注册失败:', err)
        uni.showToast({
          title: '注册失败,请重试',
          icon: 'error'
        })
      } finally {
        this.isSubmitting = false
      }
    }
  }
}
</script>

<style>
.register-form {
  padding: 20px;
  background-color: #f8f8f8;
  min-height: 100vh;
}

.form-title {
  font-size: 24px;
  font-weight: bold;
  text-align: center;
  margin-bottom: 30px;
  color: #333;
}

.form-group {
  margin-bottom: 20px;
}

.label {
  display: block;
  font-size: 16px;
  color: #333;
  margin-bottom: 8px;
}

.form-input {
  width: 100%;
  padding: 12px 15px;
  border: 1px solid #ddd;
  border-radius: 8px;
  font-size: 16px;
  background-color: white;
}

.picker-input {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 15px;
  border: 1px solid #ddd;
  border-radius: 8px;
  background-color: white;
}

.arrow {
  color: #999;
}

.error-text {
  color: #ff4757;
  font-size: 14px;
  margin-top: 5px;
  display: block;
}

.agreement {
  display: flex;
  align-items: center;
  gap: 10px;
}

.agreement-text {
  font-size: 14px;
  color: #666;
}

.link-text {
  font-size: 14px;
  color: #007aff;
}

.submit-btn {
  width: 100%;
  background-color: #007aff;
  color: white;
  border: none;
  border-radius: 8px;
  padding: 15px;
  font-size: 18px;
  margin-top: 20px;
}

.submit-btn[disabled] {
  background-color: #ccc;
}
</style>

小结

今天我们学习了:

  • input - 各种类型的输入框
  • picker - 选择器(普通、多列、时间、日期)
  • switch - 开关组件
  • slider - 滑块组件
  • ✅ 完整的注册表单案例

表单开发要点

  • 数据双向绑定用 v-model
  • 表单验证要及时反馈
  • 用户体验要友好
  • 数据提交要有加载状态

下一篇预告

下一篇我们将学习《布局组件使用 - 让页面排版更美观》,学习如何用各种布局组件构建美观的页面结构。


表单是用户输入的桥梁,做好表单体验,用户才会愿意与你的应用互动!