Skip to content

uni-app 代码示例

本页面提供了uni-app开发中常用的代码示例,帮助开发者快速实现各种功能。

目录

🚀 基础功能

页面导航

页面跳转方式

javascript
// 1. 保留当前页面,跳转到新页面
uni.navigateTo({
  url: '/pages/detail/detail?id=1&name=uniapp'
});

// 2. 关闭当前页面,跳转到新页面
uni.redirectTo({
  url: '/pages/detail/detail'
});

// 3. 关闭所有页面,打开到指定页面
uni.reLaunch({
  url: '/pages/index/index'
});

// 4. 跳转到 tabBar 页面
uni.switchTab({
  url: '/pages/index/index'
});

页面参数处理

javascript
// 目标页面接收参数
export default {
  onLoad(options) {
    const { id, name } = options;
    console.log('页面参数:', { id, name });
  }
}

// 页面返回
uni.navigateBack({
  delta: 1 // 返回层数,默认为1
});

网络请求

基础请求方法

javascript
// GET 请求
uni.request({
  url: 'https://api.example.com/data',
  method: 'GET',
  data: { page: 1, size: 10 },
  header: { 'content-type': 'application/json' },
  success: (res) => console.log('成功:', res.data),
  fail: (err) => console.error('失败:', err)
});

// POST 请求
uni.request({
  url: 'https://api.example.com/login',
  method: 'POST',
  data: { username: 'test', password: '123456' },
  header: { 'content-type': 'application/json' },
  success: (res) => console.log('登录成功:', res.data),
  fail: (err) => console.error('登录失败:', err)
});

请求封装工具

javascript
// 统一请求封装
class HttpRequest {
  constructor(baseURL = '') {
    this.baseURL = baseURL;
    this.timeout = 10000;
  }

  request(options) {
    return new Promise((resolve, reject) => {
      const config = {
        url: this.baseURL + options.url,
        method: options.method || 'GET',
        data: options.data || {},
        header: {
          'content-type': 'application/json',
          ...options.header
        },
        timeout: options.timeout || this.timeout,
        success: (res) => {
          if (res.statusCode === 200) {
            resolve(res.data);
          } else {
            reject(new Error(`请求失败: ${res.statusCode}`));
          }
        },
        fail: reject
      };
      
      uni.request(config);
    });
  }

  get(url, data, options = {}) {
    return this.request({ url, method: 'GET', data, ...options });
  }

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

// 使用示例
const http = new HttpRequest('https://api.example.com');

// 异步请求
async function fetchData() {
  try {
    const result = await http.get('/data', { page: 1 });
    console.log('数据:', result);
  } catch (error) {
    console.error('请求错误:', error);
  }
}

本地存储

存储操作

javascript
// 异步存储
uni.setStorage({
  key: 'userInfo',
  data: { name: '张三', age: 18 },
  success: () => console.log('存储成功')
});

// 同步存储(推荐)
try {
  uni.setStorageSync('token', 'abc123');
  uni.setStorageSync('settings', { theme: 'dark', lang: 'zh' });
} catch (error) {
  console.error('存储失败:', error);
}

读取操作

javascript
// 异步读取
uni.getStorage({
  key: 'userInfo',
  success: (res) => console.log('用户信息:', res.data),
  fail: () => console.log('数据不存在')
});

// 同步读取(推荐)
try {
  const token = uni.getStorageSync('token');
  const settings = uni.getStorageSync('settings');
  console.log('Token:', token, 'Settings:', settings);
} catch (error) {
  console.error('读取失败:', error);
}

删除和清理

javascript
// 删除指定数据
uni.removeStorageSync('token');

// 清除所有数据
uni.clearStorageSync();

// 获取存储信息
const info = uni.getStorageInfoSync();
console.log('存储信息:', info);

存储工具类

javascript
class StorageUtil {
  // 设置存储
  static set(key, value) {
    try {
      uni.setStorageSync(key, JSON.stringify(value));
      return true;
    } catch (error) {
      console.error('存储失败:', error);
      return false;
    }
  }

  // 获取存储
  static get(key, defaultValue = null) {
    try {
      const value = uni.getStorageSync(key);
      return value ? JSON.parse(value) : defaultValue;
    } catch (error) {
      console.error('读取失败:', error);
      return defaultValue;
    }
  }

  // 删除存储
  static remove(key) {
    try {
      uni.removeStorageSync(key);
      return true;
    } catch (error) {
      console.error('删除失败:', error);
      return false;
    }
  }

  // 清空存储
  static clear() {
    try {
      uni.clearStorageSync();
      return true;
    } catch (error) {
      console.error('清空失败:', error);
      return false;
    }
  }
}

// 使用示例
StorageUtil.set('user', { name: '张三', id: 1 });
const user = StorageUtil.get('user');

📱 UI组件示例

列表组件

基础列表

html
<template>
  <view class="list-container">
    <view 
      class="list-item" 
      v-for="item in listData" 
      :key="item.id" 
      @click="handleItemClick(item)"
    >
      <view class="item-content">
        <text class="item-title">{{ item.title }}</text>
        <text class="item-desc">{{ item.description }}</text>
      </view>
      <text class="item-arrow">></text>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      listData: [
        { id: 1, title: '标题一', description: '这是描述内容一' },
        { id: 2, title: '标题二', description: '这是描述内容二' },
        { id: 3, title: '标题三', description: '这是描述内容三' }
      ]
    }
  },
  methods: {
    handleItemClick(item) {
      uni.navigateTo({
        url: `/pages/detail/detail?id=${item.id}`
      });
    }
  }
}
</script>

<style scoped>
.list-container {
  background-color: #f8f8f8;
  padding: 10px;
}

.list-item {
  display: flex;
  align-items: center;
  padding: 15px;
  margin-bottom: 10px;
  background-color: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.item-content {
  flex: 1;
}

.item-title {
  font-size: 16px;
  font-weight: 600;
  color: #333;
  margin-bottom: 5px;
}

.item-desc {
  font-size: 14px;
  color: #666;
}

.item-arrow {
  font-size: 18px;
  color: #ccc;
}
</style>

兄弟组件通信

事件总线方式:

javascript
// utils/eventBus.js - 事件总线工具
class EventBus {
  constructor() {
    this.events = {};
  }

  // 监听事件
  on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
  }

  // 触发事件
  emit(event, data) {
    if (this.events[event]) {
      this.events[event].forEach(callback => callback(data));
    }
  }

  // 移除事件监听
  off(event, callback) {
    if (this.events[event]) {
      this.events[event] = this.events[event].filter(cb => cb !== callback);
    }
  }

  // 只监听一次
  once(event, callback) {
    const onceCallback = (data) => {
      callback(data);
      this.off(event, onceCallback);
    };
    this.on(event, onceCallback);
  }
}

export default new EventBus();

组件A (ComponentA.vue):

html
<template>
  <view class="component-a">
    <view class="component-header">
      <text class="component-title">组件 A</text>
    </view>
    
    <view class="component-content">
      <text class="status-text">当前状态:{{ status }}</text>
      <input 
        class="message-input" 
        v-model="message" 
        placeholder="输入要发送的消息"
      />
      <button class="send-btn" @click="sendMessage">发送消息给组件B</button>
    </view>
  </view>
</template>

<script>
import EventBus from '@/utils/eventBus.js';

export default {
  name: 'ComponentA',
  
  data() {
    return {
      message: '',
      status: '等待消息'
    }
  },
  
  mounted() {
    // 监听来自组件B的消息
    EventBus.on('message-from-b', this.handleMessageFromB);
  },
  
  beforeDestroy() {
    // 移除事件监听
    EventBus.off('message-from-b', this.handleMessageFromB);
  },
  
  methods: {
    sendMessage() {
      if (!this.message.trim()) {
        uni.showToast({
          title: '请输入消息内容',
          icon: 'none'
        });
        return;
      }
      
      // 发送消息给组件B
      EventBus.emit('message-from-a', {
        content: this.message,
        timestamp: new Date().toLocaleTimeString(),
        from: 'ComponentA'
      });
      
      this.status = `已发送:${this.message}`;
      this.message = '';
    },
    
    handleMessageFromB(data) {
      this.status = `收到组件B消息:${data.content}`;
      
      uni.showToast({
        title: '收到新消息',
        icon: 'success'
      });
    }
  }
}
</script>

<style scoped>
.component-a {
  margin: 10px;
  padding: 15px;
  background-color: #e8f5e8;
  border-radius: 8px;
  border: 2px solid #4caf50;
}

.component-header {
  margin-bottom: 15px;
}

.component-title {
  font-size: 18px;
  font-weight: 600;
  color: #2e7d32;
}

.component-content {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.status-text {
  font-size: 14px;
  color: #424242;
  padding: 8px;
  background-color: #f5f5f5;
  border-radius: 4px;
}

.message-input {
  height: 40px;
  border: 1px solid #ddd;
  border-radius: 6px;
  padding: 0 12px;
  font-size: 16px;
}

.send-btn {
  height: 40px;
  background-color: #4caf50;
  color: white;
  border-radius: 6px;
  font-size: 16px;
}
</style>

组件B (ComponentB.vue):

html
<template>
  <view class="component-b">
    <view class="component-header">
      <text class="component-title">组件 B</text>
    </view>
    
    <view class="component-content">
      <text class="status-text">{{ status }}</text>
      
      <view class="message-history" v-if="receivedMessages.length > 0">
        <text class="history-title">消息历史:</text>
        <view 
          class="message-item" 
          v-for="(msg, index) in receivedMessages" 
          :key="index"
        >
          <text class="message-content">{{ msg.content }}</text>
          <text class="message-time">{{ msg.timestamp }}</text>
        </view>
      </view>
      
      <button class="reply-btn" @click="replyMessage">回复消息</button>
    </view>
  </view>
</template>

<script>
import EventBus from '@/utils/eventBus.js';

export default {
  name: 'ComponentB',
  
  data() {
    return {
      status: '等待接收消息',
      receivedMessages: []
    }
  },
  
  mounted() {
    // 监听来自组件A的消息
    EventBus.on('message-from-a', this.handleMessageFromA);
  },
  
  beforeDestroy() {
    // 移除事件监听
    EventBus.off('message-from-a', this.handleMessageFromA);
  },
  
  methods: {
    handleMessageFromA(data) {
      this.receivedMessages.push(data);
      this.status = `收到新消息:${data.content}`;
      
      // 自动回复
      setTimeout(() => {
        this.autoReply(data);
      }, 1000);
    },
    
    replyMessage() {
      const replies = [
        '收到你的消息了!',
        '谢谢你的信息',
        '我正在处理中...',
        '好的,明白了'
      ];
      
      const randomReply = replies[Math.floor(Math.random() * replies.length)];
      
      EventBus.emit('message-from-b', {
        content: randomReply,
        timestamp: new Date().toLocaleTimeString(),
        from: 'ComponentB'
      });
      
      this.status = `已回复:${randomReply}`;
    },
    
    autoReply(originalMessage) {
      const autoReplies = [
        `已收到你的消息:"${originalMessage.content}"`,
        '消息已处理完成',
        '感谢你的消息'
      ];
      
      const reply = autoReplies[Math.floor(Math.random() * autoReplies.length)];
      
      EventBus.emit('message-from-b', {
        content: reply,
        timestamp: new Date().toLocaleTimeString(),
        from: 'ComponentB',
        isAutoReply: true
      });
    }
  }
}
</script>

<style scoped>
.component-b {
  margin: 10px;
  padding: 15px;
  background-color: #e3f2fd;
  border-radius: 8px;
  border: 2px solid #2196f3;
}

.component-header {
  margin-bottom: 15px;
}

.component-title {
  font-size: 18px;
  font-weight: 600;
  color: #1976d2;
}

.component-content {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.status-text {
  font-size: 14px;
  color: #424242;
  padding: 8px;
  background-color: #f5f5f5;
  border-radius: 4px;
}

.message-history {
  padding: 10px;
  background-color: #f8f9fa;
  border-radius: 6px;
  border: 1px solid #dee2e6;
}

.history-title {
  font-size: 14px;
  font-weight: 600;
  color: #495057;
  margin-bottom: 8px;
}

.message-item {
  padding: 8px;
  margin-bottom: 5px;
  background-color: #fff;
  border-radius: 4px;
  border-left: 3px solid #2196f3;
}

.message-content {
  display: block;
  font-size: 14px;
  color: #333;
  margin-bottom: 3px;
}

.message-time {
  font-size: 12px;
  color: #6c757d;
}

.reply-btn {
  height: 40px;
  background-color: #2196f3;
  color: white;
  border-radius: 6px;
  font-size: 16px;
}
</style>

全局状态管理

简单状态管理 (store/index.js):

javascript
// store/index.js - 简单的状态管理
class SimpleStore {
  constructor() {
    this.state = {
      user: {
        id: null,
        name: '',
        avatar: '',
        isLogin: false
      },
      app: {
        theme: 'light',
        language: 'zh-cn',
        version: '1.0.0'
      },
      cart: {
        items: [],
        total: 0
      }
    };
    
    this.listeners = {};
  }

  // 获取状态
  getState(path) {
    if (!path) return this.state;
    
    return path.split('.').reduce((obj, key) => obj && obj[key], this.state);
  }

  // 设置状态
  setState(path, value) {
    const keys = path.split('.');
    const lastKey = keys.pop();
    const target = keys.reduce((obj, key) => obj[key], this.state);
    
    target[lastKey] = value;
    
    // 通知监听者
    this.notify(path, value);
  }

  // 监听状态变化
  subscribe(path, callback) {
    if (!this.listeners[path]) {
      this.listeners[path] = [];
    }
    this.listeners[path].push(callback);
    
    // 返回取消订阅的函数
    return () => {
      this.listeners[path] = this.listeners[path].filter(cb => cb !== callback);
    };
  }

  // 通知监听者
  notify(path, value) {
    if (this.listeners[path]) {
      this.listeners[path].forEach(callback => callback(value));
    }
  }

  // 用户相关操作
  login(userInfo) {
    this.setState('user.id', userInfo.id);
    this.setState('user.name', userInfo.name);
    this.setState('user.avatar', userInfo.avatar);
    this.setState('user.isLogin', true);
  }

  logout() {
    this.setState('user.id', null);
    this.setState('user.name', '');
    this.setState('user.avatar', '');
    this.setState('user.isLogin', false);
  }

  // 购物车操作
  addToCart(item) {
    const items = this.getState('cart.items');
    const existingItem = items.find(i => i.id === item.id);
    
    if (existingItem) {
      existingItem.quantity += item.quantity || 1;
    } else {
      items.push({ ...item, quantity: item.quantity || 1 });
    }
    
    this.setState('cart.items', items);
    this.updateCartTotal();
  }

  removeFromCart(itemId) {
    const items = this.getState('cart.items').filter(item => item.id !== itemId);
    this.setState('cart.items', items);
    this.updateCartTotal();
  }

  updateCartTotal() {
    const items = this.getState('cart.items');
    const total = items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
    this.setState('cart.total', total);
  }
}

export default new SimpleStore();

使用状态管理的组件示例:

html
<template>
  <view class="store-demo">
    <view class="user-section">
      <text class="section-title">用户信息</text>
      <view v-if="user.isLogin" class="user-info">
        <text class="user-name">欢迎,{{ user.name }}!</text>
        <button class="logout-btn" @click="handleLogout">退出登录</button>
      </view>
      <view v-else class="login-form">
        <input 
          class="login-input" 
          v-model="loginForm.name" 
          placeholder="请输入用户名"
        />
        <button class="login-btn" @click="handleLogin">登录</button>
      </view>
    </view>
    
    <view class="cart-section">
      <text class="section-title">购物车 ({{ cart.items.length }})</text>
      <view class="cart-total">总计:¥{{ cart.total.toFixed(2) }}</view>
      
      <view class="cart-items">
        <view 
          class="cart-item" 
          v-for="item in cart.items" 
          :key="item.id"
        >
          <text class="item-name">{{ item.name }}</text>
          <text class="item-price">¥{{ item.price }} × {{ item.quantity }}</text>
          <button class="remove-btn" @click="removeItem(item.id)">移除</button>
        </view>
      </view>
      
      <button class="add-btn" @click="addRandomItem">添加随机商品</button>
    </view>
  </view>
</template>

<script>
import store from '@/store/index.js';

export default {
  name: 'StoreDemo',
  
  data() {
    return {
      user: store.getState('user'),
      cart: store.getState('cart'),
      loginForm: {
        name: ''
      },
      unsubscribes: []
    }
  },
  
  mounted() {
    // 订阅状态变化
    const unsubscribeUser = store.subscribe('user', (user) => {
      this.user = { ...user };
    });
    
    const unsubscribeCart = store.subscribe('cart', (cart) => {
      this.cart = { ...cart };
    });
    
    this.unsubscribes.push(unsubscribeUser, unsubscribeCart);
  },
  
  beforeDestroy() {
    // 取消订阅
    this.unsubscribes.forEach(unsubscribe => unsubscribe());
  },
  
  methods: {
    handleLogin() {
      if (!this.loginForm.name.trim()) {
        uni.showToast({
          title: '请输入用户名',
          icon: 'none'
        });
        return;
      }
      
      store.login({
        id: Date.now(),
        name: this.loginForm.name,
        avatar: 'https://via.placeholder.com/50'
      });
      
      this.loginForm.name = '';
      
      uni.showToast({
        title: '登录成功',
        icon: 'success'
      });
    },
    
    handleLogout() {
      store.logout();
      uni.showToast({
        title: '已退出登录',
        icon: 'success'
      });
    },
    
    addRandomItem() {
      const products = [
        { id: 1, name: '苹果', price: 5.99 },
        { id: 2, name: '香蕉', price: 3.50 },
        { id: 3, name: '橙子', price: 4.20 },
        { id: 4, name: '葡萄', price: 8.80 },
        { id: 5, name: '草莓', price: 12.00 }
      ];
      
      const randomProduct = products[Math.floor(Math.random() * products.length)];
      store.addToCart(randomProduct);
      
      uni.showToast({
        title: `已添加 ${randomProduct.name}`,
        icon: 'success'
      });
    },
    
    removeItem(itemId) {
      store.removeFromCart(itemId);
      uni.showToast({
        title: '商品已移除',
        icon: 'success'
      });
    }
  }
}
</script>

<style scoped>
.store-demo {
  padding: 20px;
}

.user-section, .cart-section {
  margin-bottom: 30px;
  padding: 15px;
  background-color: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.section-title {
  display: block;
  font-size: 18px;
  font-weight: 600;
  color: #333;
  margin-bottom: 15px;
}

.user-info {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.user-name {
  font-size: 16px;
  color: #333;
}

.login-form {
  display: flex;
  gap: 10px;
}

.login-input {
  flex: 1;
  height: 40px;
  border: 1px solid #ddd;
  border-radius: 6px;
  padding: 0 12px;
  font-size: 16px;
}

.login-btn, .logout-btn, .add-btn {
  height: 40px;
  padding: 0 20px;
  border-radius: 6px;
  font-size: 16px;
}

.cart-total {
  font-size: 16px;
  font-weight: 600;
  color: #e74c3c;
  margin-bottom: 15px;
}

.cart-items {
  margin-bottom: 15px;
}

.cart-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px;
  margin-bottom: 8px;
  background-color: #f8f9fa;
  border-radius: 6px;
}

.item-name {
  font-size: 16px;
  color: #333;
}

.item-price {
  font-size: 14px;
  color: #666;
}

.remove-btn {
  height: 30px;
  padding: 0 15px;
  background-color: #dc3545;
  color: white;
  border-radius: 4px;
  font-size: 14px;
}
</style>

下拉刷新和上拉加载

html
<template>
  <view class="page">
    <scroll-view 
      class="scroll-view" 
      scroll-y 
      @scrolltolower="loadMore"
      :refresher-enabled="true"
      :refresher-triggered="isRefreshing"
      @refresherrefresh="onRefresh"
    >
      <!-- 列表内容 -->
      <view class="list-item" v-for="item in list" :key="item.id">
        <text class="item-title">{{ item.title }}</text>
        <text class="item-time">{{ item.time }}</text>
      </view>
      
      <!-- 加载状态 -->
      <view class="load-status">
        <text v-if="isLoading" class="loading">正在加载...</text>
        <text v-else-if="noMore && list.length > 0" class="no-more">没有更多数据</text>
        <text v-else-if="list.length === 0" class="empty">暂无数据</text>
      </view>
    </scroll-view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      list: [],
      page: 1,
      pageSize: 10,
      isLoading: false,
      noMore: false,
      isRefreshing: false
    }
  },
  
  onLoad() {
    this.loadData();
  },
  
  methods: {
    async loadData() {
      if (this.isLoading) return;
      
      this.isLoading = true;
      
      try {
        const response = await this.fetchData({
          page: this.page,
          size: this.pageSize
        });
        
        const newData = response.data || [];
        
        if (this.page === 1) {
          this.list = newData;
        } else {
          this.list.push(...newData);
        }
        
        this.noMore = newData.length < this.pageSize;
        if (!this.noMore) this.page++;
        
      } catch (error) {
        console.error('加载失败:', error);
        uni.showToast({
          title: '加载失败',
          icon: 'none'
        });
      } finally {
        this.isLoading = false;
        this.isRefreshing = false;
      }
    },
    
    onRefresh() {
      this.page = 1;
      this.noMore = false;
      this.isRefreshing = true;
      this.loadData();
    },
    
    loadMore() {
      if (!this.isLoading && !this.noMore) {
        this.loadData();
      }
    },
    
    // 模拟数据请求
    fetchData(params) {
      return new Promise((resolve) => {
        setTimeout(() => {
          const mockData = Array.from({ length: params.size }, (_, i) => ({
            id: (params.page - 1) * params.size + i + 1,
            title: `数据项 ${(params.page - 1) * params.size + i + 1}`,
            time: new Date().toLocaleString()
          }));
          
          // 模拟最多50条数据
          const maxItems = 50;
          const startIndex = (params.page - 1) * params.size;
          const validData = startIndex < maxItems ? 
            mockData.slice(0, Math.max(0, maxItems - startIndex)) : [];
          
          resolve({ data: validData });
        }, 800);
      });
    }
  }
}
</script>

<style scoped>
.page {
  height: 100vh;
  background-color: #f5f5f5;
}

.scroll-view {
  height: 100%;
}

.list-item {
  padding: 15px 20px;
  margin: 10px;
  background-color: #fff;
  border-radius: 8px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

.item-title {
  font-size: 16px;
  color: #333;
  margin-bottom: 5px;
}

.item-time {
  font-size: 12px;
  color: #999;
}

.load-status {
  text-align: center;
  padding: 20px;
}

.loading, .no-more, .empty {
  font-size: 14px;
  color: #999;
}
</style>

表单组件

基础表单

html
<template>
  <view class="form-container">
    <!-- 输入框 -->
    <view class="form-group">
      <text class="form-label">用户名</text>
      <input 
        class="form-input" 
        v-model="formData.username" 
        placeholder="请输入用户名"
        maxlength="20"
      />
    </view>
    
    <!-- 密码框 -->
    <view class="form-group">
      <text class="form-label">密码</text>
      <input 
        class="form-input" 
        v-model="formData.password" 
        type="password" 
        placeholder="请输入密码"
      />
    </view>
    
    <!-- 单选框 -->
    <view class="form-group">
      <text class="form-label">性别</text>
      <radio-group @change="onGenderChange">
        <label class="radio-item">
          <radio value="male" :checked="formData.gender === 'male'" />
          <text>男</text>
        </label>
        <label class="radio-item">
          <radio value="female" :checked="formData.gender === 'female'" />
          <text>女</text>
        </label>
      </radio-group>
    </view>
    
    <!-- 多选框 -->
    <view class="form-group">
      <text class="form-label">兴趣爱好</text>
      <checkbox-group @change="onHobbyChange">
        <label class="checkbox-item" v-for="hobby in hobbies" :key="hobby.value">
          <checkbox :value="hobby.value" :checked="formData.selectedHobbies.includes(hobby.value)" />
          <text>{{ hobby.label }}</text>
        </label>
      </checkbox-group>
    </view>
    
    <!-- 日期选择 -->
    <view class="form-group">
      <text class="form-label">出生日期</text>
      <picker mode="date" :value="formData.birthday" @change="onDateChange">
        <view class="picker-input">
          {{ formData.birthday || '请选择日期' }}
        </view>
      </picker>
    </view>
    
    <!-- 地区选择 -->
    <view class="form-group">
      <text class="form-label">所在地区</text>
      <picker mode="region" :value="formData.region" @change="onRegionChange">
        <view class="picker-input">
          {{ formData.region.length ? formData.region.join(' ') : '请选择地区' }}
        </view>
      </picker>
    </view>
    
    <!-- 文本域 -->
    <view class="form-group">
      <text class="form-label">个人简介</text>
      <textarea 
        class="form-textarea" 
        v-model="formData.bio" 
        placeholder="请输入个人简介"
        maxlength="200"
      />
    </view>
    
    <!-- 提交按钮 -->
    <button class="submit-btn" type="primary" @click="handleSubmit">
      提交表单
    </button>
  </view>
</template>

<script>
export default {
  data() {
    return {
      formData: {
        username: '',
        password: '',
        gender: 'male',
        selectedHobbies: [],
        birthday: '',
        region: [],
        bio: ''
      },
      hobbies: [
        { label: '阅读', value: 'reading' },
        { label: '音乐', value: 'music' },
        { label: '运动', value: 'sports' },
        { label: '旅行', value: 'travel' }
      ]
    }
  },
  
  methods: {
    onGenderChange(e) {
      this.formData.gender = e.detail.value;
    },
    
    onHobbyChange(e) {
      this.formData.selectedHobbies = e.detail.value;
    },
    
    onDateChange(e) {
      this.formData.birthday = e.detail.value;
    },
    
    onRegionChange(e) {
      this.formData.region = e.detail.value;
    },
    
    validateForm() {
      if (!this.formData.username.trim()) {
        uni.showToast({ title: '请输入用户名', icon: 'none' });
        return false;
      }
      
      if (!this.formData.password) {
        uni.showToast({ title: '请输入密码', icon: 'none' });
        return false;
      }
      
      return true;
    },
    
    async handleSubmit() {
      if (!this.validateForm()) return;
      
      uni.showLoading({ title: '提交中...' });
      
      try {
        // 模拟提交
        await new Promise(resolve => setTimeout(resolve, 1500));
        
        console.log('表单数据:', this.formData);
        
        uni.showToast({
          title: '提交成功',
          icon: 'success'
        });
      } catch (error) {
        uni.showToast({
          title: '提交失败',
          icon: 'error'
        });
      } finally {
        uni.hideLoading();
      }
    }
  }
}
</script>

<style scoped>
.form-container {
  padding: 20px;
  background-color: #f8f8f8;
}

.form-group {
  margin-bottom: 20px;
  background-color: #fff;
  padding: 15px;
  border-radius: 8px;
}

.form-label {
  display: block;
  font-size: 16px;
  font-weight: 600;
  color: #333;
  margin-bottom: 10px;
}

.form-input, .form-textarea {
  width: 100%;
  border: 1px solid #e0e0e0;
  border-radius: 6px;
  padding: 12px;
  font-size: 16px;
  box-sizing: border-box;
}

.form-input {
  height: 44px;
}

.form-textarea {
  height: 100px;
  resize: none;
}

.radio-item, .checkbox-item {
  display: inline-flex;
  align-items: center;
  margin-right: 20px;
  margin-bottom: 10px;
}

.radio-item text, .checkbox-item text {
  margin-left: 8px;
  font-size: 16px;
}

.picker-input {
  height: 44px;
  line-height: 44px;
  border: 1px solid #e0e0e0;
  border-radius: 6px;
  padding: 0 12px;
  font-size: 16px;
  color: #333;
}

.submit-btn {
  margin-top: 30px;
  height: 50px;
  font-size: 18px;
  border-radius: 8px;
}
</style>

表单验证

html
<template>
  <view class="form-container">
    <view class="form-group" :class="{ 'has-error': errors.username }">
      <text class="form-label">用户名</text>
      <input 
        class="form-input" 
        v-model="formData.username" 
        placeholder="请输入用户名"
        @blur="validateField('username')"
      />
      <text class="error-message" v-if="errors.username">{{ errors.username }}</text>
    </view>
    
    <view class="form-group" :class="{ 'has-error': errors.email }">
      <text class="form-label">邮箱</text>
      <input 
        class="form-input" 
        v-model="formData.email" 
        placeholder="请输入邮箱"
        @blur="validateField('email')"
      />
      <text class="error-message" v-if="errors.email">{{ errors.email }}</text>
    </view>
    
    <view class="form-group" :class="{ 'has-error': errors.phone }">
      <text class="form-label">手机号</text>
      <input 
        class="form-input" 
        v-model="formData.phone" 
        placeholder="请输入手机号"
        type="number"
        @blur="validateField('phone')"
      />
      <text class="error-message" v-if="errors.phone">{{ errors.phone }}</text>
    </view>
    
    <view class="form-group" :class="{ 'has-error': errors.password }">
      <text class="form-label">密码</text>
      <input 
        class="form-input" 
        v-model="formData.password" 
        type="password" 
        placeholder="请输入密码"
        @blur="validateField('password')"
      />
      <text class="error-message" v-if="errors.password">{{ errors.password }}</text>
    </view>
    
    <view class="form-group" :class="{ 'has-error': errors.confirmPassword }">
      <text class="form-label">确认密码</text>
      <input 
        class="form-input" 
        v-model="formData.confirmPassword" 
        type="password" 
        placeholder="请再次输入密码"
        @blur="validateField('confirmPassword')"
      />
      <text class="error-message" v-if="errors.confirmPassword">{{ errors.confirmPassword }}</text>
    </view>
    
    <button class="submit-btn" type="primary" @click="handleSubmit">
      注册账号
    </button>
  </view>
</template>

<script>
export default {
  data() {
    return {
      formData: {
        username: '',
        email: '',
        phone: '',
        password: '',
        confirmPassword: ''
      },
      errors: {},
      rules: {
        username: [
          { required: true, message: '请输入用户名' },
          { min: 3, max: 20, message: '用户名长度为3-20个字符' }
        ],
        email: [
          { required: true, message: '请输入邮箱' },
          { pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: '请输入有效的邮箱地址' }
        ],
        phone: [
          { required: true, message: '请输入手机号' },
          { pattern: /^1[3-9]\d{9}$/, message: '请输入有效的手机号' }
        ],
        password: [
          { required: true, message: '请输入密码' },
          { min: 6, message: '密码长度不能少于6位' },
          { pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, message: '密码需包含大小写字母和数字' }
        ],
        confirmPassword: [
          { required: true, message: '请确认密码' },
          { validator: this.validateConfirmPassword }
        ]
      }
    }
  },
  
  methods: {
    validateField(field) {
      const rules = this.rules[field];
      const value = this.formData[field];
      
      this.$set(this.errors, field, '');
      
      for (const rule of rules) {
        if (rule.required && !value) {
          this.$set(this.errors, field, rule.message);
          return false;
        }
        
        if (rule.min && value.length < rule.min) {
          this.$set(this.errors, field, rule.message);
          return false;
        }
        
        if (rule.max && value.length > rule.max) {
          this.$set(this.errors, field, rule.message);
          return false;
        }
        
        if (rule.pattern && !rule.pattern.test(value)) {
          this.$set(this.errors, field, rule.message);
          return false;
        }
        
        if (rule.validator && !rule.validator(value)) {
          this.$set(this.errors, field, rule.message);
          return false;
        }
      }
      
      return true;
    },
    
    validateConfirmPassword(value) {
      if (value !== this.formData.password) {
        this.$set(this.errors, 'confirmPassword', '两次输入的密码不一致');
        return false;
      }
      return true;
    },
    
    validateForm() {
      let isValid = true;
      
      Object.keys(this.rules).forEach(field => {
        if (!this.validateField(field)) {
          isValid = false;
        }
      });
      
      return isValid;
    },
    
    async handleSubmit() {
      if (!this.validateForm()) {
        uni.showToast({
          title: '请检查表单信息',
          icon: 'none'
        });
        return;
      }
      
      uni.showLoading({ title: '注册中...' });
      
      try {
        // 模拟注册请求
        await new Promise(resolve => setTimeout(resolve, 2000));
        
        uni.showToast({
          title: '注册成功',
          icon: 'success'
        });
        
        // 清空表单
        this.resetForm();
        
      } catch (error) {
        uni.showToast({
          title: '注册失败',
          icon: 'error'
        });
      } finally {
        uni.hideLoading();
      }
    },
    
    resetForm() {
      this.formData = {
        username: '',
        email: '',
        phone: '',
        password: '',
        confirmPassword: ''
      };
      this.errors = {};
    }
  }
}
</script>

<style scoped>
.form-container {
  padding: 20px;
  background-color: #f8f8f8;
}

.form-group {
  margin-bottom: 20px;
  background-color: #fff;
  padding: 15px;
  border-radius: 8px;
  border: 1px solid #e0e0e0;
}

.form-group.has-error {
  border-color: #ff4757;
}

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

.form-input {
  width: 100%;
  height: 44px;
  border: 1px solid #e0e0e0;
  border-radius: 6px;
  padding: 0 12px;
  font-size: 16px;
  box-sizing: border-box;
}

.has-error .form-input {
  border-color: #ff4757;
}

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

.submit-btn {
  margin-top: 30px;
  height: 50px;
  font-size: 18px;
  border-radius: 8px;
}
</style>

弹窗交互

消息提示

javascript
// 成功提示
uni.showToast({
  title: '操作成功',
  icon: 'success',
  duration: 2000
});

// 错误提示
uni.showToast({
  title: '操作失败',
  icon: 'error',
  duration: 2000
});

// 纯文本提示
uni.showToast({
  title: '这是一条提示信息',
  icon: 'none',
  duration: 3000
});

// 加载提示
uni.showLoading({
  title: '加载中...',
  mask: true // 防止触摸穿透
});

// 延时隐藏加载
setTimeout(() => {
  uni.hideLoading();
}, 2000);

确认对话框

javascript
// 基础确认框
uni.showModal({
  title: '删除确认',
  content: '确定要删除这条记录吗?此操作不可撤销。',
  confirmText: '删除',
  cancelText: '取消',
  confirmColor: '#ff4757',
  success: (res) => {
    if (res.confirm) {
      console.log('用户确认删除');
      performDelete();
    } else {
      console.log('用户取消操作');
    }
  }
});

// 只有确认按钮的提示框
uni.showModal({
  title: '提示',
  content: '您的操作已完成',
  showCancel: false,
  confirmText: '知道了'
});

async function performDelete() {
  uni.showLoading({ title: '删除中...' });
  try {
    // 模拟删除操作
    await new Promise(resolve => setTimeout(resolve, 1000));
    uni.showToast({ title: '删除成功', icon: 'success' });
  } catch (error) {
    uni.showToast({ title: '删除失败', icon: 'error' });
  } finally {
    uni.hideLoading();
  }
}

操作菜单

javascript
// 基础操作菜单
uni.showActionSheet({
  itemList: ['拍照', '从相册选择', '取消'],
  success: (res) => {
    const actions = ['camera', 'album', 'cancel'];
    const action = actions[res.tapIndex];
    
    switch (action) {
      case 'camera':
        chooseImage('camera');
        break;
      case 'album':
        chooseImage('album');
        break;
      case 'cancel':
        console.log('用户取消选择');
        break;
    }
  }
});

function chooseImage(sourceType) {
  uni.chooseImage({
    count: 1,
    sourceType: [sourceType],
    success: (res) => {
      console.log('选择的图片:', res.tempFilePaths[0]);
    }
  });
}

自定义弹窗

html
<template>
  <view class="page">
    <view class="demo-section">
      <button class="demo-btn" @click="showInfoDialog">信息弹窗</button>
      <button class="demo-btn" @click="showFormDialog">表单弹窗</button>
      <button class="demo-btn" @click="showImageDialog">图片弹窗</button>
    </view>
    
    <!-- 信息弹窗 -->
    <custom-dialog 
      :visible="infoDialog.visible"
      :title="infoDialog.title"
      :content="infoDialog.content"
      @confirm="onInfoConfirm"
      @cancel="onInfoCancel"
    />
    
    <!-- 表单弹窗 -->
    <view class="custom-dialog" v-if="formDialog.visible">
      <view class="dialog-mask" @click="hideFormDialog"></view>
      <view class="dialog-content form-dialog">
        <view class="dialog-header">
          <text class="dialog-title">添加备注</text>
          <text class="dialog-close" @click="hideFormDialog">×</text>
        </view>
        <view class="dialog-body">
          <textarea 
            class="form-textarea" 
            v-model="formDialog.remark" 
            placeholder="请输入备注内容"
            maxlength="200"
          />
        </view>
        <view class="dialog-footer">
          <button class="dialog-btn cancel" @click="hideFormDialog">取消</button>
          <button class="dialog-btn confirm" @click="confirmFormDialog">确定</button>
        </view>
      </view>
    </view>
    
    <!-- 图片预览弹窗 -->
    <view class="custom-dialog image-dialog" v-if="imageDialog.visible">
      <view class="dialog-mask" @click="hideImageDialog"></view>
      <view class="image-container">
        <image 
          class="preview-image" 
          :src="imageDialog.src" 
          mode="aspectFit"
          @click.stop
        />
        <text class="close-btn" @click="hideImageDialog">×</text>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      infoDialog: {
        visible: false,
        title: '操作确认',
        content: '确定要执行此操作吗?此操作不可撤销。'
      },
      formDialog: {
        visible: false,
        remark: ''
      },
      imageDialog: {
        visible: false,
        src: 'https://via.placeholder.com/400x300'
      }
    }
  },
  
  methods: {
    showInfoDialog() {
      this.infoDialog.visible = true;
    },
    
    showFormDialog() {
      this.formDialog.visible = true;
      this.formDialog.remark = '';
    },
    
    showImageDialog() {
      this.imageDialog.visible = true;
    },
    
    onInfoConfirm() {
      this.infoDialog.visible = false;
      uni.showToast({
        title: '操作已确认',
        icon: 'success'
      });
    },
    
    onInfoCancel() {
      this.infoDialog.visible = false;
      console.log('用户取消操作');
    },
    
    hideFormDialog() {
      this.formDialog.visible = false;
    },
    
    confirmFormDialog() {
      if (!this.formDialog.remark.trim()) {
        uni.showToast({
          title: '请输入备注内容',
          icon: 'none'
        });
        return;
      }
      
      console.log('备注内容:', this.formDialog.remark);
      this.hideFormDialog();
      
      uni.showToast({
        title: '备注已保存',
        icon: 'success'
      });
    },
    
    hideImageDialog() {
      this.imageDialog.visible = false;
    }
  },
  
  components: {
    'custom-dialog': {
      props: {
        visible: Boolean,
        title: String,
        content: String,
        confirmText: {
          type: String,
          default: '确定'
        },
        cancelText: {
          type: String,
          default: '取消'
        }
      },
      template: `
        <view class="custom-dialog" v-if="visible">
          <view class="dialog-mask" @click="$emit('cancel')"></view>
          <view class="dialog-content">
            <view class="dialog-header">
              <text class="dialog-title">{{ title }}</text>
            </view>
            <view class="dialog-body">
              <text class="dialog-text">{{ content }}</text>
            </view>
            <view class="dialog-footer">
              <button class="dialog-btn cancel" @click="$emit('cancel')">{{ cancelText }}</button>
              <button class="dialog-btn confirm" @click="$emit('confirm')">{{ confirmText }}</button>
            </view>
          </view>
        </view>
      `
    }
  }
}
</script>

<style scoped>
.page {
  padding: 20px;
}

.demo-section {
  display: flex;
  flex-direction: column;
  gap: 15px;
}

.demo-btn {
  height: 50px;
  font-size: 16px;
  border-radius: 8px;
}

/* 弹窗基础样式 */
.custom-dialog {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
}

.dialog-mask {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.6);
}

.dialog-content {
  position: relative;
  width: 85%;
  max-width: 400px;
  background-color: #fff;
  border-radius: 12px;
  overflow: hidden;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}

.dialog-header {
  position: relative;
  padding: 20px;
  border-bottom: 1px solid #f0f0f0;
  text-align: center;
}

.dialog-title {
  font-size: 18px;
  font-weight: 600;
  color: #333;
}

.dialog-close {
  position: absolute;
  top: 15px;
  right: 20px;
  font-size: 24px;
  color: #999;
  cursor: pointer;
}

.dialog-body {
  padding: 20px;
  min-height: 60px;
}

.dialog-text {
  font-size: 16px;
  color: #666;
  line-height: 1.5;
}

.dialog-footer {
  display: flex;
  border-top: 1px solid #f0f0f0;
}

.dialog-btn {
  flex: 1;
  height: 50px;
  font-size: 16px;
  border: none;
  border-radius: 0;
}

.dialog-btn.cancel {
  background-color: #f8f8f8;
  color: #666;
}

.dialog-btn.confirm {
  background-color: #007aff;
  color: #fff;
}

/* 表单弹窗样式 */
.form-dialog .dialog-body {
  padding: 15px 20px;
}

.form-textarea {
  width: 100%;
  height: 100px;
  border: 1px solid #e0e0e0;
  border-radius: 6px;
  padding: 10px;
  font-size: 16px;
  box-sizing: border-box;
  resize: none;
}

/* 图片预览弹窗样式 */
.image-dialog {
  background-color: rgba(0, 0, 0, 0.9);
}

.image-container {
  position: relative;
  width: 90%;
  height: 70%;
}

.preview-image {
  width: 100%;
  height: 100%;
}

.close-btn {
  position: absolute;
  top: -50px;
  right: 0;
  font-size: 30px;
  color: #fff;
  cursor: pointer;
}
</style>

🔧 高级功能

组件通信

父子组件通信

父组件 (Parent.vue):

html
<template>
  <view class="parent-container">
    <view class="parent-info">
      <text class="info-title">父组件状态</text>
      <text class="info-content">{{ parentMessage }}</text>
      <button class="action-btn" @click="updateParentMessage">更新父组件消息</button>
    </view>
    
    <child-component 
      :message="parentMessage"
      :count="parentCount"
      @update-count="handleCountUpdate"
      @send-message="handleChildMessage"
    />
  </view>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  name: 'ParentComponent',
  components: {
    ChildComponent
  },
  
  data() {
    return {
      parentMessage: '来自父组件的初始消息',
      parentCount: 0
    }
  },
  
  methods: {
    updateParentMessage() {
      this.parentMessage = `父组件消息更新于 ${new Date().toLocaleTimeString()}`;
    },
    
    handleCountUpdate(newCount) {
      this.parentCount = newCount;
      console.log('父组件接收到计数更新:', newCount);
    },
    
    handleChildMessage(message) {
      uni.showToast({
        title: `子组件说:${message}`,
        icon: 'none',
        duration: 2000
      });
    }
  }
}
</script>

<style scoped>
.parent-container {
  padding: 20px;
  background-color: #f8f9fa;
}

.parent-info {
  padding: 15px;
  margin-bottom: 20px;
  background-color: #e3f2fd;
  border-radius: 8px;
  border-left: 4px solid #2196f3;
}

.info-title {
  display: block;
  font-size: 16px;
  font-weight: 600;
  color: #1976d2;
  margin-bottom: 8px;
}

.info-content {
  display: block;
  font-size: 14px;
  color: #424242;
  margin-bottom: 15px;
}

.action-btn {
  height: 40px;
  font-size: 14px;
  border-radius: 6px;
}
</style>

子组件 (ChildComponent.vue):

html
<template>
  <view class="child-container">
    <view class="child-info">
      <text class="info-title">子组件状态</text>
      <text class="info-content">接收到的消息:{{ message }}</text>
      <text class="info-content">当前计数:{{ localCount }}</text>
    </view>
    
    <view class="child-actions">
      <button class="action-btn" @click="incrementCount">增加计数</button>
      <button class="action-btn" @click="sendMessageToParent">发送消息给父组件</button>
      <button class="action-btn" @click="resetCount">重置计数</button>
    </view>
  </view>
</template>

<script>
export default {
  name: 'ChildComponent',
  
  props: {
    message: {
      type: String,
      default: ''
    },
    count: {
      type: Number,
      default: 0
    }
  },
  
  data() {
    return {
      localCount: 0
    }
  },
  
  watch: {
    count: {
      handler(newVal) {
        this.localCount = newVal;
      },
      immediate: true
    }
  },
  
  methods: {
    incrementCount() {
      this.localCount++;
      this.$emit('update-count', this.localCount);
    },
    
    resetCount() {
      this.localCount = 0;
      this.$emit('update-count', this.localCount);
    },
    
    sendMessageToParent() {
      const messages = [
        '你好,我是子组件!',
        '计数已更新',
        '子组件运行正常',
        '感谢使用!'
      ];
      const randomMessage = messages[Math.floor(Math.random() * messages.length)];
      this.$emit('send-message', randomMessage);
    }
  }
}
</script>

<style scoped>
.child-container {
  padding: 15px;
  background-color: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.child-info {
  padding: 15px;
  margin-bottom: 15px;
  background-color: #f3e5f5;
  border-radius: 8px;
  border-left: 4px solid #9c27b0;
}

.info-title {
  display: block;
  font-size: 16px;
  font-weight: 600;
  color: #7b1fa2;
  margin-bottom: 8px;
}

.info-content {
  display: block;
  font-size: 14px;
  color: #424242;
  margin-bottom: 5px;
}

.child-actions {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.action-btn {
  height: 40px;
  font-size: 14px;
  border-radius: 6px;
}
</style>

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