Skip to content

Code Samples

This page provides practical code examples and snippets to help you implement common features in uni-app development.

Basic Components

Custom Button Component

vue
<template>
  <button 
    class="custom-button" 
    :class="[`button-${type}`, { 'button-disabled': disabled }]"
    :disabled="disabled"
    @click="handleClick"
  >
    <text class="button-text">{{ text }}</text>
  </button>
</template>

<script>
export default {
  name: 'CustomButton',
  props: {
    text: {
      type: String,
      default: 'Button'
    },
    type: {
      type: String,
      default: 'default',
      validator: value => ['default', 'primary', 'success', 'warning', 'danger'].includes(value)
    },
    disabled: {
      type: Boolean,
      default: false
    }
  },
  methods: {
    handleClick() {
      if (!this.disabled) {
        this.$emit('click')
      }
    }
  }
}
</script>

<style scoped>
.custom-button {
  padding: 12rpx 24rpx;
  border-radius: 8rpx;
  border: none;
  font-size: 28rpx;
  cursor: pointer;
  transition: all 0.3s ease;
}

.button-default {
  background-color: #f8f9fa;
  color: #333;
}

.button-primary {
  background-color: #007bff;
  color: white;
}

.button-success {
  background-color: #28a745;
  color: white;
}

.button-warning {
  background-color: #ffc107;
  color: #333;
}

.button-danger {
  background-color: #dc3545;
  color: white;
}

.button-disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.button-text {
  font-weight: 500;
}
</style>

Loading Component

vue
<template>
  <view v-if="visible" class="loading-overlay">
    <view class="loading-container">
      <view class="loading-spinner"></view>
      <text class="loading-text">{{ text }}</text>
    </view>
  </view>
</template>

<script>
export default {
  name: 'Loading',
  props: {
    visible: {
      type: Boolean,
      default: false
    },
    text: {
      type: String,
      default: 'Loading...'
    }
  }
}
</script>

<style scoped>
.loading-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 9999;
}

.loading-container {
  background-color: white;
  padding: 40rpx;
  border-radius: 16rpx;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.loading-spinner {
  width: 60rpx;
  height: 60rpx;
  border: 4rpx solid #f3f3f3;
  border-top: 4rpx solid #007bff;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

.loading-text {
  margin-top: 20rpx;
  font-size: 28rpx;
  color: #666;
}
</style>

Network Requests

API Service Class

javascript
// utils/api.js
class ApiService {
  constructor() {
    this.baseURL = 'https://api.example.com'
    this.timeout = 10000
  }

  // Request interceptor
  request(options) {
    return new Promise((resolve, reject) => {
      // Add loading
      uni.showLoading({
        title: 'Loading...'
      })

      // Add token to headers
      const token = uni.getStorageSync('token')
      if (token) {
        options.header = {
          ...options.header,
          'Authorization': `Bearer ${token}`
        }
      }

      uni.request({
        url: this.baseURL + options.url,
        method: options.method || 'GET',
        data: options.data,
        header: {
          'Content-Type': 'application/json',
          ...options.header
        },
        timeout: this.timeout,
        success: (res) => {
          uni.hideLoading()
          
          if (res.statusCode === 200) {
            if (res.data.code === 0) {
              resolve(res.data)
            } else {
              this.handleError(res.data)
              reject(res.data)
            }
          } else {
            this.handleError({ message: 'Network error' })
            reject(res)
          }
        },
        fail: (err) => {
          uni.hideLoading()
          this.handleError({ message: 'Request failed' })
          reject(err)
        }
      })
    })
  }

  // Error handler
  handleError(error) {
    uni.showToast({
      title: error.message || 'Unknown error',
      icon: 'none',
      duration: 2000
    })
  }

  // GET request
  get(url, params) {
    return this.request({
      url: params ? `${url}?${this.buildQuery(params)}` : url,
      method: 'GET'
    })
  }

  // POST request
  post(url, data) {
    return this.request({
      url,
      method: 'POST',
      data
    })
  }

  // PUT request
  put(url, data) {
    return this.request({
      url,
      method: 'PUT',
      data
    })
  }

  // DELETE request
  delete(url) {
    return this.request({
      url,
      method: 'DELETE'
    })
  }

  // Build query string
  buildQuery(params) {
    return Object.keys(params)
      .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
      .join('&')
  }
}

export default new ApiService()

Usage Example

javascript
// pages/index/index.vue
import api from '@/utils/api'

export default {
  data() {
    return {
      userList: [],
      loading: false
    }
  },
  onLoad() {
    this.fetchUsers()
  },
  methods: {
    async fetchUsers() {
      try {
        this.loading = true
        const response = await api.get('/users', { page: 1, limit: 10 })
        this.userList = response.data
      } catch (error) {
        console.error('Failed to fetch users:', error)
      } finally {
        this.loading = false
      }
    },

    async createUser(userData) {
      try {
        const response = await api.post('/users', userData)
        uni.showToast({
          title: 'User created successfully',
          icon: 'success'
        })
        this.fetchUsers() // Refresh list
      } catch (error) {
        console.error('Failed to create user:', error)
      }
    }
  }
}

Data Storage

Storage Utility

javascript
// utils/storage.js
class StorageUtil {
  // Set item with expiration
  setItem(key, value, expireTime) {
    const data = {
      value,
      expireTime: expireTime ? Date.now() + expireTime : null
    }
    
    try {
      uni.setStorageSync(key, JSON.stringify(data))
      return true
    } catch (error) {
      console.error('Storage set error:', error)
      return false
    }
  }

  // Get item with expiration check
  getItem(key) {
    try {
      const dataStr = uni.getStorageSync(key)
      if (!dataStr) return null

      const data = JSON.parse(dataStr)
      
      // Check expiration
      if (data.expireTime && Date.now() > data.expireTime) {
        this.removeItem(key)
        return null
      }

      return data.value
    } catch (error) {
      console.error('Storage get error:', error)
      return null
    }
  }

  // Remove item
  removeItem(key) {
    try {
      uni.removeStorageSync(key)
      return true
    } catch (error) {
      console.error('Storage remove error:', error)
      return false
    }
  }

  // Clear all storage
  clear() {
    try {
      uni.clearStorageSync()
      return true
    } catch (error) {
      console.error('Storage clear error:', error)
      return false
    }
  }

  // Get storage info
  getInfo() {
    try {
      return uni.getStorageInfoSync()
    } catch (error) {
      console.error('Storage info error:', error)
      return null
    }
  }
}

export default new StorageUtil()

Form Validation

Validation Utility

javascript
// utils/validator.js
class Validator {
  constructor() {
    this.rules = {
      required: (value) => {
        return value !== null && value !== undefined && value !== ''
      },
      email: (value) => {
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
        return emailRegex.test(value)
      },
      phone: (value) => {
        const phoneRegex = /^1[3-9]\d{9}$/
        return phoneRegex.test(value)
      },
      minLength: (value, length) => {
        return value && value.length >= length
      },
      maxLength: (value, length) => {
        return value && value.length <= length
      },
      number: (value) => {
        return !isNaN(value) && !isNaN(parseFloat(value))
      },
      url: (value) => {
        const urlRegex = /^https?:\/\/.+/
        return urlRegex.test(value)
      }
    }
  }

  validate(data, rules) {
    const errors = {}

    for (const field in rules) {
      const fieldRules = rules[field]
      const value = data[field]

      for (const rule of fieldRules) {
        const { type, message, ...params } = rule

        if (this.rules[type]) {
          const isValid = this.rules[type](value, ...Object.values(params))
          
          if (!isValid) {
            if (!errors[field]) {
              errors[field] = []
            }
            errors[field].push(message)
            break // Stop at first error for this field
          }
        }
      }
    }

    return {
      isValid: Object.keys(errors).length === 0,
      errors
    }
  }
}

export default new Validator()

Form Component with Validation

vue
<template>
  <view class="form-container">
    <view class="form-item">
      <text class="label">Email *</text>
      <input 
        v-model="formData.email"
        type="text"
        placeholder="Enter your email"
        class="input"
        :class="{ 'input-error': errors.email }"
      />
      <text v-if="errors.email" class="error-text">{{ errors.email[0] }}</text>
    </view>

    <view class="form-item">
      <text class="label">Password *</text>
      <input 
        v-model="formData.password"
        type="password"
        placeholder="Enter your password"
        class="input"
        :class="{ 'input-error': errors.password }"
      />
      <text v-if="errors.password" class="error-text">{{ errors.password[0] }}</text>
    </view>

    <view class="form-item">
      <text class="label">Phone</text>
      <input 
        v-model="formData.phone"
        type="number"
        placeholder="Enter your phone number"
        class="input"
        :class="{ 'input-error': errors.phone }"
      />
      <text v-if="errors.phone" class="error-text">{{ errors.phone[0] }}</text>
    </view>

    <button class="submit-btn" @click="handleSubmit">Submit</button>
  </view>
</template>

<script>
import validator from '@/utils/validator'

export default {
  data() {
    return {
      formData: {
        email: '',
        password: '',
        phone: ''
      },
      errors: {},
      validationRules: {
        email: [
          { type: 'required', message: 'Email is required' },
          { type: 'email', message: 'Please enter a valid email' }
        ],
        password: [
          { type: 'required', message: 'Password is required' },
          { type: 'minLength', length: 6, message: 'Password must be at least 6 characters' }
        ],
        phone: [
          { type: 'phone', message: 'Please enter a valid phone number' }
        ]
      }
    }
  },
  methods: {
    handleSubmit() {
      const validation = validator.validate(this.formData, this.validationRules)
      
      if (validation.isValid) {
        this.errors = {}
        this.submitForm()
      } else {
        this.errors = validation.errors
        uni.showToast({
          title: 'Please fix the errors',
          icon: 'none'
        })
      }
    },

    async submitForm() {
      try {
        // Submit form data
        console.log('Submitting form:', this.formData)
        uni.showToast({
          title: 'Form submitted successfully',
          icon: 'success'
        })
      } catch (error) {
        console.error('Form submission error:', error)
      }
    }
  }
}
</script>

<style scoped>
.form-container {
  padding: 40rpx;
}

.form-item {
  margin-bottom: 40rpx;
}

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

.input {
  width: 100%;
  height: 80rpx;
  padding: 0 20rpx;
  border: 2rpx solid #ddd;
  border-radius: 8rpx;
  font-size: 28rpx;
  box-sizing: border-box;
}

.input-error {
  border-color: #dc3545;
}

.error-text {
  color: #dc3545;
  font-size: 24rpx;
  margin-top: 8rpx;
}

.submit-btn {
  width: 100%;
  height: 80rpx;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 8rpx;
  font-size: 32rpx;
  margin-top: 40rpx;
}
</style>

Image Handling

Image Upload Component

vue
<template>
  <view class="image-upload">
    <view class="image-list">
      <view 
        v-for="(image, index) in imageList" 
        :key="index"
        class="image-item"
      >
        <image :src="image.url" class="image" mode="aspectFill" />
        <view class="image-actions">
          <text class="action-btn" @click="previewImage(index)">Preview</text>
          <text class="action-btn delete" @click="removeImage(index)">Delete</text>
        </view>
      </view>
      
      <view 
        v-if="imageList.length < maxCount"
        class="upload-btn"
        @click="chooseImage"
      >
        <text class="upload-icon">+</text>
        <text class="upload-text">Add Image</text>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  name: 'ImageUpload',
  props: {
    maxCount: {
      type: Number,
      default: 9
    },
    maxSize: {
      type: Number,
      default: 5 * 1024 * 1024 // 5MB
    }
  },
  data() {
    return {
      imageList: []
    }
  },
  methods: {
    chooseImage() {
      uni.chooseImage({
        count: this.maxCount - this.imageList.length,
        sizeType: ['original', 'compressed'],
        sourceType: ['album', 'camera'],
        success: (res) => {
          this.handleImageSelect(res.tempFilePaths)
        }
      })
    },

    handleImageSelect(tempFilePaths) {
      tempFilePaths.forEach(path => {
        // Check file size
        uni.getFileInfo({
          filePath: path,
          success: (res) => {
            if (res.size > this.maxSize) {
              uni.showToast({
                title: 'Image too large',
                icon: 'none'
              })
              return
            }

            this.imageList.push({
              url: path,
              path: path
            })

            // Upload image
            this.uploadImage(path)
          }
        })
      })
    },

    uploadImage(filePath) {
      uni.uploadFile({
        url: 'https://api.example.com/upload',
        filePath: filePath,
        name: 'file',
        header: {
          'Authorization': `Bearer ${uni.getStorageSync('token')}`
        },
        success: (res) => {
          const data = JSON.parse(res.data)
          if (data.code === 0) {
            // Update image URL with server URL
            const index = this.imageList.findIndex(img => img.path === filePath)
            if (index !== -1) {
              this.imageList[index].url = data.data.url
            }
          }
        },
        fail: (err) => {
          console.error('Upload failed:', err)
          uni.showToast({
            title: 'Upload failed',
            icon: 'none'
          })
        }
      })
    },

    previewImage(index) {
      const urls = this.imageList.map(img => img.url)
      uni.previewImage({
        urls: urls,
        current: index
      })
    },

    removeImage(index) {
      uni.showModal({
        title: 'Confirm',
        content: 'Are you sure you want to delete this image?',
        success: (res) => {
          if (res.confirm) {
            this.imageList.splice(index, 1)
          }
        }
      })
    }
  }
}
</script>

<style scoped>
.image-upload {
  padding: 20rpx;
}

.image-list {
  display: flex;
  flex-wrap: wrap;
  gap: 20rpx;
}

.image-item {
  position: relative;
  width: 200rpx;
  height: 200rpx;
}

.image {
  width: 100%;
  height: 100%;
  border-radius: 8rpx;
}

.image-actions {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  background: rgba(0, 0, 0, 0.7);
  display: flex;
  justify-content: space-around;
  padding: 10rpx;
  border-radius: 0 0 8rpx 8rpx;
}

.action-btn {
  color: white;
  font-size: 24rpx;
}

.delete {
  color: #ff4757;
}

.upload-btn {
  width: 200rpx;
  height: 200rpx;
  border: 2rpx dashed #ddd;
  border-radius: 8rpx;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.upload-icon {
  font-size: 60rpx;
  color: #999;
}

.upload-text {
  font-size: 24rpx;
  color: #999;
  margin-top: 10rpx;
}
</style>

List Components

Infinite Scroll List

vue
<template>
  <view class="list-container">
    <view 
      v-for="(item, index) in list" 
      :key="item.id"
      class="list-item"
      @click="handleItemClick(item)"
    >
      <image :src="item.avatar" class="avatar" />
      <view class="content">
        <text class="title">{{ item.title }}</text>
        <text class="description">{{ item.description }}</text>
        <text class="time">{{ formatTime(item.createTime) }}</text>
      </view>
    </view>

    <view v-if="loading" class="loading">
      <text>Loading...</text>
    </view>

    <view v-if="!hasMore && list.length > 0" class="no-more">
      <text>No more data</text>
    </view>

    <view v-if="list.length === 0 && !loading" class="empty">
      <text>No data available</text>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      list: [],
      loading: false,
      hasMore: true,
      page: 1,
      pageSize: 20
    }
  },
  onLoad() {
    this.loadData()
  },
  onReachBottom() {
    if (this.hasMore && !this.loading) {
      this.loadMore()
    }
  },
  onPullDownRefresh() {
    this.refresh()
  },
  methods: {
    async loadData() {
      try {
        this.loading = true
        const response = await this.$api.get('/list', {
          page: this.page,
          pageSize: this.pageSize
        })

        if (this.page === 1) {
          this.list = response.data
        } else {
          this.list.push(...response.data)
        }

        this.hasMore = response.data.length === this.pageSize
      } catch (error) {
        console.error('Load data error:', error)
      } finally {
        this.loading = false
        uni.stopPullDownRefresh()
      }
    },

    loadMore() {
      this.page++
      this.loadData()
    },

    refresh() {
      this.page = 1
      this.hasMore = true
      this.loadData()
    },

    handleItemClick(item) {
      uni.navigateTo({
        url: `/pages/detail/detail?id=${item.id}`
      })
    },

    formatTime(timestamp) {
      const date = new Date(timestamp)
      const now = new Date()
      const diff = now - date

      if (diff < 60000) {
        return 'Just now'
      } else if (diff < 3600000) {
        return `${Math.floor(diff / 60000)} minutes ago`
      } else if (diff < 86400000) {
        return `${Math.floor(diff / 3600000)} hours ago`
      } else {
        return date.toLocaleDateString()
      }
    }
  }
}
</script>

<style scoped>
.list-container {
  padding: 20rpx;
}

.list-item {
  display: flex;
  padding: 30rpx;
  background: white;
  border-radius: 16rpx;
  margin-bottom: 20rpx;
  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}

.avatar {
  width: 80rpx;
  height: 80rpx;
  border-radius: 40rpx;
  margin-right: 20rpx;
}

.content {
  flex: 1;
  display: flex;
  flex-direction: column;
}

.title {
  font-size: 32rpx;
  font-weight: bold;
  color: #333;
  margin-bottom: 10rpx;
}

.description {
  font-size: 28rpx;
  color: #666;
  margin-bottom: 10rpx;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.time {
  font-size: 24rpx;
  color: #999;
}

.loading, .no-more, .empty {
  text-align: center;
  padding: 40rpx;
  color: #999;
  font-size: 28rpx;
}
</style>

Utility Functions

Common Utilities

javascript
// utils/common.js
export const utils = {
  // Debounce function
  debounce(func, wait) {
    let timeout
    return function executedFunction(...args) {
      const later = () => {
        clearTimeout(timeout)
        func(...args)
      }
      clearTimeout(timeout)
      timeout = setTimeout(later, wait)
    }
  },

  // Throttle function
  throttle(func, limit) {
    let inThrottle
    return function() {
      const args = arguments
      const context = this
      if (!inThrottle) {
        func.apply(context, args)
        inThrottle = true
        setTimeout(() => inThrottle = false, limit)
      }
    }
  },

  // Deep clone object
  deepClone(obj) {
    if (obj === null || typeof obj !== 'object') return obj
    if (obj instanceof Date) return new Date(obj.getTime())
    if (obj instanceof Array) return obj.map(item => this.deepClone(item))
    if (typeof obj === 'object') {
      const clonedObj = {}
      for (const key in obj) {
        if (obj.hasOwnProperty(key)) {
          clonedObj[key] = this.deepClone(obj[key])
        }
      }
      return clonedObj
    }
  },

  // Format file size
  formatFileSize(bytes) {
    if (bytes === 0) return '0 Bytes'
    const k = 1024
    const sizes = ['Bytes', 'KB', 'MB', 'GB']
    const i = Math.floor(Math.log(bytes) / Math.log(k))
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
  },

  // Generate UUID
  generateUUID() {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
      const r = Math.random() * 16 | 0
      const v = c === 'x' ? r : (r & 0x3 | 0x8)
      return v.toString(16)
    })
  },

  // Check if object is empty
  isEmpty(obj) {
    if (obj == null) return true
    if (Array.isArray(obj) || typeof obj === 'string') return obj.length === 0
    return Object.keys(obj).length === 0
  },

  // Format currency
  formatCurrency(amount, currency = '$') {
    return currency + parseFloat(amount).toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,')
  }
}

Conclusion

These code samples provide practical examples for common uni-app development scenarios. You can use them as starting points and customize them according to your specific requirements. Remember to:

  1. Test code samples thoroughly in your environment
  2. Adapt the styling to match your app's design
  3. Add proper error handling for production use
  4. Consider performance implications for large datasets
  5. Follow uni-app best practices and guidelines

For more advanced examples and patterns, refer to the official uni-app documentation and community resources.

一次开发,多端部署 - 让跨平台开发更简单