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>