第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. 表单字段:用户名、密码、确认密码、邮箱、手机号、验证码等
  2. 表单验证:实时验证和提交验证
  3. 交互体验:密码强度提示、验证码发送倒计时等
  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 官方文档,了解平台差异和解决方案

学习总结

通过本章节的学习,您已经掌握了以下内容:

  1. 表单组件使用:学会了使用 uni-app 提供的各种表单组件,如 input、textarea、picker、switch 等
  2. 表单验证实现:掌握了实时验证和提交验证的实现方法,以及自定义验证规则的编写
  3. 数据收集与处理:学会了使用 v-model 进行数据绑定,以及表单数据的格式化和提交
  4. 表单界面优化:掌握了如何设计用户友好的表单界面,提高用户体验
  5. 实用案例:实现了完整的用户注册表单,包括密码强度提示、验证码发送等功能

表单开发是 uni-app 应用开发中的重要部分,良好的表单设计可以提高用户体验和数据收集的准确性。在实际项目中,您应该根据具体的业务需求和用户场景,灵活应用所学知识,构建既美观又实用的表单界面。

通过本章节的学习,您已经具备了开发复杂表单的能力,可以应对各种表单开发场景,为用户提供流畅、直观的表单交互体验。

« 上一篇 uni-app 最佳实践 下一篇 » uni-app 列表开发