4.1 路由配置基础
4.1.1 pages.json配置详解
pages.json
是UniApp的核心配置文件,用于配置页面路由、窗口表现、底部导航等。
1. 基础配置结构
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"navigationBarBackgroundColor": "#007aff",
"navigationBarTextStyle": "white",
"backgroundColor": "#f8f8f8",
"enablePullDownRefresh": true,
"onReachBottomDistance": 50
}
},
{
"path": "pages/user/user",
"style": {
"navigationBarTitleText": "个人中心",
"navigationStyle": "custom"
}
},
{
"path": "pages/detail/detail",
"style": {
"navigationBarTitleText": "详情页",
"app-plus": {
"bounce": "none",
"titleNView": {
"buttons": [
{
"text": "分享",
"fontSize": "16px",
"color": "#ffffff"
}
]
}
}
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "UniApp教程",
"navigationBarBackgroundColor": "#ffffff",
"backgroundColor": "#f8f8f8",
"app-plus": {
"background": "#efeff4"
}
},
"tabBar": {
"color": "#7A7E83",
"selectedColor": "#007aff",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/index/index",
"iconPath": "static/tab-home.png",
"selectedIconPath": "static/tab-home-active.png",
"text": "首页"
},
{
"pagePath": "pages/category/category",
"iconPath": "static/tab-category.png",
"selectedIconPath": "static/tab-category-active.png",
"text": "分类"
},
{
"pagePath": "pages/cart/cart",
"iconPath": "static/tab-cart.png",
"selectedIconPath": "static/tab-cart-active.png",
"text": "购物车"
},
{
"pagePath": "pages/user/user",
"iconPath": "static/tab-user.png",
"selectedIconPath": "static/tab-user-active.png",
"text": "我的"
}
]
},
"condition": {
"current": 0,
"list": [
{
"name": "详情页",
"path": "pages/detail/detail",
"query": "id=123&title=测试商品"
}
]
},
"subPackages": [
{
"root": "subpkg",
"pages": [
{
"path": "goods/list",
"style": {
"navigationBarTitleText": "商品列表"
}
}
]
}
],
"preloadRule": {
"pages/index/index": {
"network": "all",
"packages": ["subpkg"]
}
}
}
2. 配置项详解
- pages: 页面路径配置数组,第一个页面为应用入口页面
- globalStyle: 全局默认窗口表现
- tabBar: 底部导航配置
- condition: 启动模式配置(开发工具中使用)
- subPackages: 分包配置
- preloadRule: 分包预下载规则
4.1.2 页面样式配置
1. 导航栏配置
{
"style": {
"navigationBarTitleText": "页面标题",
"navigationBarBackgroundColor": "#007aff",
"navigationBarTextStyle": "white",
"navigationStyle": "default",
"app-plus": {
"titleNView": {
"buttons": [
{
"text": "\ue601",
"fontSrc": "/static/uni.ttf",
"fontSize": "22px",
"color": "#ffffff",
"width": "40px"
}
],
"searchInput": {
"backgroundColor": "rgba(255, 255, 255, 0.8)",
"borderRadius": "20px",
"placeholder": "请输入搜索内容",
"disabled": false
}
}
}
}
}
2. 下拉刷新配置
{
"style": {
"enablePullDownRefresh": true,
"backgroundTextStyle": "dark",
"app-plus": {
"pullToRefresh": {
"support": true,
"color": "#007aff",
"style": "circle",
"height": "50px",
"range": "100px",
"offset": "0px"
}
}
}
}
3. 上拉加载配置
{
"style": {
"onReachBottomDistance": 50,
"backgroundTextStyle": "dark"
}
}
4.2 页面跳转与导航
4.2.1 编程式导航
1. uni.navigateTo() - 保留当前页面
<template>
<view class="navigation-demo">
<text class="title">编程式导航演示</text>
<!-- 基础跳转 -->
<view class="section">
<text class="section-title">基础页面跳转</text>
<button @click="navigateToDetail">跳转到详情页</button>
<button @click="navigateToUser">跳转到用户页</button>
<button @click="navigateToList">跳转到列表页</button>
</view>
<!-- 带参数跳转 -->
<view class="section">
<text class="section-title">带参数跳转</text>
<button @click="navigateWithParams">跳转并传递参数</button>
<button @click="navigateWithObject">跳转并传递对象</button>
<button @click="navigateWithQuery">跳转并传递查询参数</button>
</view>
<!-- 条件跳转 -->
<view class="section">
<text class="section-title">条件跳转</text>
<button @click="conditionalNavigate">登录后跳转</button>
<button @click="checkAndNavigate">检查权限后跳转</button>
</view>
<!-- 动画跳转 -->
<view class="section">
<text class="section-title">动画跳转</text>
<button @click="navigateWithAnimation">带动画跳转</button>
<button @click="navigateSlideUp">上滑动画</button>
</view>
</view>
</template>
<script>
export default {
name: 'NavigationDemo',
data() {
return {
userInfo: {
id: 123,
name: '张三',
avatar: '/static/avatar.jpg'
}
}
},
methods: {
// 基础跳转
navigateToDetail() {
uni.navigateTo({
url: '/pages/detail/detail',
success: (res) => {
console.log('跳转成功', res)
},
fail: (err) => {
console.error('跳转失败', err)
uni.showToast({
title: '跳转失败',
icon: 'error'
})
}
})
},
navigateToUser() {
uni.navigateTo({
url: '/pages/user/user'
})
},
navigateToList() {
uni.navigateTo({
url: '/pages/list/list'
})
},
// 带参数跳转
navigateWithParams() {
const productId = 12345
const productName = 'iPhone 15 Pro'
uni.navigateTo({
url: `/pages/detail/detail?id=${productId}&name=${encodeURIComponent(productName)}`
})
},
navigateWithObject() {
// 复杂对象需要序列化
const productInfo = {
id: 12345,
name: 'iPhone 15 Pro',
price: 8999,
specs: {
color: '深空黑色',
storage: '256GB'
}
}
uni.navigateTo({
url: `/pages/detail/detail?data=${encodeURIComponent(JSON.stringify(productInfo))}`
})
},
navigateWithQuery() {
const params = {
category: 'electronics',
brand: 'apple',
minPrice: 1000,
maxPrice: 10000
}
const queryString = Object.keys(params)
.map(key => `${key}=${encodeURIComponent(params[key])}`)
.join('&')
uni.navigateTo({
url: `/pages/list/list?${queryString}`
})
},
// 条件跳转
conditionalNavigate() {
// 检查登录状态
const token = uni.getStorageSync('token')
if (!token) {
uni.showModal({
title: '提示',
content: '请先登录',
confirmText: '去登录',
success: (res) => {
if (res.confirm) {
uni.navigateTo({
url: '/pages/login/login'
})
}
}
})
return
}
// 已登录,跳转到目标页面
uni.navigateTo({
url: '/pages/profile/profile'
})
},
checkAndNavigate() {
// 检查用户权限
const userRole = uni.getStorageSync('userRole')
if (userRole !== 'admin') {
uni.showToast({
title: '权限不足',
icon: 'error'
})
return
}
uni.navigateTo({
url: '/pages/admin/admin'
})
},
// 动画跳转
navigateWithAnimation() {
uni.navigateTo({
url: '/pages/detail/detail',
animationType: 'slide-in-right',
animationDuration: 300
})
},
navigateSlideUp() {
uni.navigateTo({
url: '/pages/modal/modal',
animationType: 'slide-in-bottom',
animationDuration: 250
})
}
}
}
</script>
<style lang="scss" scoped>
.navigation-demo {
padding: 20rpx;
.title {
display: block;
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
color: #333;
}
.section {
margin-bottom: 40rpx;
padding: 20rpx;
background-color: #f8f8f8;
border-radius: 12rpx;
.section-title {
display: block;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: #333;
}
button {
width: 100%;
margin-bottom: 15rpx;
padding: 20rpx;
background-color: #007aff;
color: white;
border: none;
border-radius: 8rpx;
font-size: 26rpx;
&:last-child {
margin-bottom: 0;
}
&:active {
background-color: #0056cc;
}
}
}
}
</style>
4.6 本章总结
4.6.1 学习要点回顾
1. 路由配置
- pages.json
是 UniApp 路由配置的核心文件
- pages
数组定义应用的页面路径和样式
- globalStyle
设置全局页面样式
- tabBar
配置底部导航栏
- condition
配置启动模式
- subPackages
实现分包加载
- preloadRule
配置分包预下载规则
2. 页面导航
- 编程式导航:使用 uni.navigateTo()
、uni.redirectTo()
、uni.reLaunch()
、uni.switchTab()
、uni.navigateBack()
等 API
- 声明式导航:使用 <navigator>
组件实现页面跳转
- 页面栈管理:理解不同导航方式对页面栈的影响
- 参数传递:URL 参数、事件传参、存储传参等多种方式
3. TabBar 导航 - 原生 TabBar 配置和自定义 TabBar 组件 - TabBar 切换控制和状态管理 - 角标显示和动态更新 - 权限控制和条件跳转
4. 路由守卫 - 路由拦截器的实现和使用 - 前置守卫和后置守卫 - 权限验证系统 - 登录状态管理
4.6.2 实践练习
练习1:创建多页面应用
创建一个包含以下页面的应用: - 首页(TabBar) - 分类页(TabBar) - 购物车(TabBar,需要登录) - 个人中心(TabBar,需要登录) - 商品详情页 - 登录页 - 注册页
要求: 1. 配置 TabBar 导航 2. 实现页面间的跳转和参数传递 3. 添加登录验证 4. 实现购物车角标功能
练习2:实现路由权限控制
基于练习1,添加以下功能: 1. 创建路由拦截器 2. 实现登录状态检查 3. 添加权限验证系统 4. 创建管理员页面(需要管理员权限) 5. 实现动态权限更新
练习3:自定义TabBar组件
创建一个功能丰富的自定义TabBar: 1. 支持角标显示(数字和红点) 2. 支持中间凸起按钮 3. 支持动画效果 4. 适配不同设备的安全区域 5. 支持主题切换
4.6.3 常见问题解答
Q1:页面跳转时参数丢失怎么办?
A1:检查以下几点:
- 确保参数正确编码,特殊字符需要使用 encodeURIComponent()
- 复杂对象建议使用 JSON.stringify() 序列化后传递
- 大量数据建议使用本地存储或全局状态管理
Q2:TabBar 页面无法使用 navigateTo 跳转?
A2:TabBar 页面必须使用 uni.switchTab()
进行跳转,不能使用 uni.navigateTo()
。
Q3:如何实现页面返回时刷新数据?
A3:可以使用以下方法:
- 在 onShow
生命周期中刷新数据
- 使用事件总线在页面间通信
- 使用全局状态管理(如 Vuex/Pinia)
Q4:自定义TabBar如何与原生TabBar共存?
A4:设置 "custom": true
后,原生TabBar会被隐藏,需要完全使用自定义组件。如果需要部分页面使用原生TabBar,建议使用条件渲染。
Q5:路由拦截器会影响性能吗?
A5:合理使用路由拦截器不会显著影响性能,但要注意: - 避免在拦截器中执行耗时操作 - 异步操作要正确处理 - 避免无限循环重定向
Q6:如何处理深层嵌套的页面返回?
A6:可以使用以下方法:
- uni.navigateBack({ delta: n })
返回多层
- uni.reLaunch()
重新启动到指定页面
- 记录页面栈状态,实现智能返回
4.6.4 最佳实践建议
1. 路由设计 - 保持路由结构清晰,使用语义化的路径名 - 合理规划页面层级,避免过深的嵌套 - 统一路由命名规范,便于维护
2. 参数传递 - 简单参数使用 URL 传递 - 复杂数据使用本地存储或状态管理 - 敏感信息不要通过 URL 传递
3. 权限控制 - 前端权限控制主要用于用户体验优化 - 重要权限验证必须在后端进行 - 实现细粒度的权限控制系统
4. 性能优化 - 使用分包加载减少首屏加载时间 - 合理配置预加载规则 - 避免频繁的页面跳转
5. 用户体验 - 提供清晰的导航指示 - 实现平滑的页面切换动画 - 处理网络异常和错误状态
4.7 下一章预告
在下一章《网络请求与数据处理》中,我们将学习:
5.1 HTTP 请求
uni.request()
API 详解- 请求拦截器和响应拦截器
- 错误处理和重试机制
- 文件上传和下载
5.2 数据缓存
- 本地存储 API(同步和异步)
- 缓存策略和过期管理
- 数据加密和安全存储
5.3 状态管理
- Vuex/Pinia 在 UniApp 中的使用
- 全局状态设计模式
- 数据持久化方案
5.4 接口封装
- 统一的 API 管理
- 请求和响应的标准化处理
- Mock 数据和开发调试
5.5 实时通信
- WebSocket 连接管理
- 消息推送和处理
- 断线重连机制
通过下一章的学习,你将掌握 UniApp 中数据处理的核心技能,能够构建功能完整的数据驱动应用。
2. uni.redirectTo() - 关闭当前页面
<template>
<view class="redirect-demo">
<text class="title">页面重定向演示</text>
<view class="section">
<text class="section-title">页面替换</text>
<button @click="redirectToHome">重定向到首页</button>
<button @click="redirectToLogin">重定向到登录页</button>
<button @click="redirectWithParams">带参数重定向</button>
</view>
<view class="section">
<text class="section-title">登录流程</text>
<button @click="loginSuccess">登录成功跳转</button>
<button @click="logoutRedirect">退出登录</button>
</view>
</view>
</template>
<script>
export default {
name: 'RedirectDemo',
methods: {
redirectToHome() {
uni.redirectTo({
url: '/pages/index/index'
})
},
redirectToLogin() {
uni.redirectTo({
url: '/pages/login/login'
})
},
redirectWithParams() {
const returnUrl = '/pages/profile/profile'
uni.redirectTo({
url: `/pages/login/login?returnUrl=${encodeURIComponent(returnUrl)}`
})
},
loginSuccess() {
// 模拟登录成功
uni.setStorageSync('token', 'mock-token-123')
uni.setStorageSync('userInfo', {
id: 123,
name: '张三',
avatar: '/static/avatar.jpg'
})
uni.showToast({
title: '登录成功',
icon: 'success'
})
// 登录成功后重定向到首页
setTimeout(() => {
uni.redirectTo({
url: '/pages/index/index'
})
}, 1500)
},
logoutRedirect() {
uni.showModal({
title: '确认退出',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
// 清除登录信息
uni.removeStorageSync('token')
uni.removeStorageSync('userInfo')
uni.showToast({
title: '已退出登录',
icon: 'success'
})
// 重定向到登录页
setTimeout(() => {
uni.redirectTo({
url: '/pages/login/login'
})
}, 1500)
}
}
})
}
}
}
</script>
<style lang="scss" scoped>
.redirect-demo {
padding: 20rpx;
.title {
display: block;
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
color: #333;
}
.section {
margin-bottom: 40rpx;
padding: 20rpx;
background-color: #f8f8f8;
border-radius: 12rpx;
.section-title {
display: block;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: #333;
}
button {
width: 100%;
margin-bottom: 15rpx;
padding: 20rpx;
background-color: #ff6b35;
color: white;
border: none;
border-radius: 8rpx;
font-size: 26rpx;
&:last-child {
margin-bottom: 0;
}
&:active {
background-color: #e55a2b;
}
}
}
}
</style>
3. uni.reLaunch() - 关闭所有页面
<template>
<view class="relaunch-demo">
<text class="title">应用重启演示</text>
<view class="section">
<text class="section-title">应用重启</text>
<button @click="relaunchToHome">重启到首页</button>
<button @click="relaunchToWelcome">重启到欢迎页</button>
</view>
<view class="section">
<text class="section-title">特殊场景</text>
<button @click="resetApp">重置应用</button>
<button @click="switchAccount">切换账号</button>
</view>
</view>
</template>
<script>
export default {
name: 'RelaunchDemo',
methods: {
relaunchToHome() {
uni.reLaunch({
url: '/pages/index/index'
})
},
relaunchToWelcome() {
uni.reLaunch({
url: '/pages/welcome/welcome'
})
},
resetApp() {
uni.showModal({
title: '重置应用',
content: '这将清除所有数据并重启应用,确定继续吗?',
success: (res) => {
if (res.confirm) {
// 清除所有存储数据
uni.clearStorageSync()
uni.showToast({
title: '重置成功',
icon: 'success'
})
// 重启应用
setTimeout(() => {
uni.reLaunch({
url: '/pages/welcome/welcome'
})
}, 1500)
}
}
})
},
switchAccount() {
uni.showModal({
title: '切换账号',
content: '切换账号将清除当前登录信息',
success: (res) => {
if (res.confirm) {
// 清除用户相关数据
uni.removeStorageSync('token')
uni.removeStorageSync('userInfo')
uni.removeStorageSync('userPreferences')
// 重启到登录页
uni.reLaunch({
url: '/pages/login/login'
})
}
}
})
}
}
}
</script>
<style lang="scss" scoped>
.relaunch-demo {
padding: 20rpx;
.title {
display: block;
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
color: #333;
}
.section {
margin-bottom: 40rpx;
padding: 20rpx;
background-color: #f8f8f8;
border-radius: 12rpx;
.section-title {
display: block;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: #333;
}
button {
width: 100%;
margin-bottom: 15rpx;
padding: 20rpx;
background-color: #ff3b30;
color: white;
border: none;
border-radius: 8rpx;
font-size: 26rpx;
&:last-child {
margin-bottom: 0;
}
&:active {
background-color: #d32f2f;
}
}
}
}
</style>
4.2.2 声明式导航
1. navigator组件基础用法
<template>
<view class="navigator-demo">
<text class="title">声明式导航演示</text>
<!-- 基础导航 -->
<view class="section">
<text class="section-title">基础导航</text>
<navigator
url="/pages/detail/detail"
class="nav-item"
>
<text class="nav-text">跳转到详情页</text>
<text class="nav-arrow">></text>
</navigator>
<navigator
url="/pages/user/user"
open-type="navigate"
class="nav-item"
>
<text class="nav-text">跳转到用户页</text>
<text class="nav-arrow">></text>
</navigator>
</view>
<!-- 不同跳转类型 -->
<view class="section">
<text class="section-title">不同跳转类型</text>
<navigator
url="/pages/index/index"
open-type="redirect"
class="nav-item redirect"
>
<text class="nav-text">重定向到首页</text>
<text class="nav-arrow">></text>
</navigator>
<navigator
url="/pages/welcome/welcome"
open-type="reLaunch"
class="nav-item relaunch"
>
<text class="nav-text">重启到欢迎页</text>
<text class="nav-arrow">></text>
</navigator>
<navigator
open-type="navigateBack"
delta="1"
class="nav-item back"
>
<text class="nav-text">返回上一页</text>
<text class="nav-arrow"><</text>
</navigator>
</view>
<!-- 带参数导航 -->
<view class="section">
<text class="section-title">带参数导航</text>
<navigator
:url="detailUrl"
class="nav-item"
>
<text class="nav-text">查看商品详情</text>
<text class="nav-desc">ID: {{ productId }}</text>
<text class="nav-arrow">></text>
</navigator>
<navigator
:url="listUrl"
class="nav-item"
>
<text class="nav-text">查看商品列表</text>
<text class="nav-desc">分类: {{ category }}</text>
<text class="nav-arrow">></text>
</navigator>
</view>
<!-- 条件导航 -->
<view class="section">
<text class="section-title">条件导航</text>
<navigator
v-if="isLoggedIn"
url="/pages/profile/profile"
class="nav-item"
>
<text class="nav-text">个人资料</text>
<text class="nav-arrow">></text>
</navigator>
<navigator
v-else
url="/pages/login/login"
class="nav-item login"
>
<text class="nav-text">请先登录</text>
<text class="nav-arrow">></text>
</navigator>
<navigator
v-if="userRole === 'admin'"
url="/pages/admin/admin"
class="nav-item admin"
>
<text class="nav-text">管理后台</text>
<text class="nav-arrow">></text>
</navigator>
</view>
<!-- 动画导航 -->
<view class="section">
<text class="section-title">动画导航</text>
<navigator
url="/pages/detail/detail"
animation-type="slide-in-right"
animation-duration="300"
class="nav-item animated"
>
<text class="nav-text">右滑进入</text>
<text class="nav-arrow">></text>
</navigator>
<navigator
url="/pages/modal/modal"
animation-type="slide-in-bottom"
animation-duration="250"
class="nav-item animated"
>
<text class="nav-text">底部弹出</text>
<text class="nav-arrow">^</text>
</navigator>
</view>
<!-- 自定义样式导航 -->
<view class="section">
<text class="section-title">自定义样式</text>
<navigator
url="/pages/detail/detail"
class="custom-nav-item card-style"
hover-class="card-hover"
>
<view class="card-content">
<view class="card-icon">📱</view>
<view class="card-info">
<text class="card-title">商品详情</text>
<text class="card-desc">查看商品的详细信息</text>
</view>
<text class="card-arrow">></text>
</view>
</navigator>
<navigator
url="/pages/list/list"
class="custom-nav-item button-style"
hover-class="button-hover"
>
<text class="button-text">浏览商品列表</text>
</navigator>
</view>
</view>
</template>
<script>
export default {
name: 'NavigatorDemo',
data() {
return {
productId: 12345,
category: 'electronics',
isLoggedIn: false,
userRole: 'user'
}
},
computed: {
detailUrl() {
return `/pages/detail/detail?id=${this.productId}&name=${encodeURIComponent('iPhone 15 Pro')}`
},
listUrl() {
return `/pages/list/list?category=${this.category}&sort=price`
}
},
onLoad() {
// 检查登录状态
const token = uni.getStorageSync('token')
this.isLoggedIn = !!token
// 获取用户角色
const userInfo = uni.getStorageSync('userInfo')
if (userInfo && userInfo.role) {
this.userRole = userInfo.role
}
}
}
</script>
<style lang="scss" scoped>
.navigator-demo {
padding: 20rpx;
.title {
display: block;
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
color: #333;
}
.section {
margin-bottom: 40rpx;
.section-title {
display: block;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: #333;
}
}
// 基础导航样式
.nav-item {
display: flex;
align-items: center;
padding: 25rpx 20rpx;
margin-bottom: 15rpx;
background-color: white;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
&:last-child {
margin-bottom: 0;
}
&:active {
background-color: #f5f5f5;
}
.nav-text {
flex: 1;
font-size: 26rpx;
color: #333;
}
.nav-desc {
font-size: 22rpx;
color: #999;
margin-right: 10rpx;
}
.nav-arrow {
font-size: 24rpx;
color: #999;
}
}
// 不同类型的样式
.redirect {
border-left: 4rpx solid #ff6b35;
}
.relaunch {
border-left: 4rpx solid #ff3b30;
}
.back {
border-left: 4rpx solid #666;
}
.login {
border-left: 4rpx solid #007aff;
}
.admin {
border-left: 4rpx solid #ff9500;
}
.animated {
border-left: 4rpx solid #09bb07;
}
// 自定义样式
.custom-nav-item {
margin-bottom: 20rpx;
&:last-child {
margin-bottom: 0;
}
}
.card-style {
background-color: white;
border-radius: 16rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
overflow: hidden;
.card-content {
display: flex;
align-items: center;
padding: 30rpx 25rpx;
}
.card-icon {
font-size: 48rpx;
margin-right: 20rpx;
}
.card-info {
flex: 1;
}
.card-title {
display: block;
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 5rpx;
}
.card-desc {
display: block;
font-size: 22rpx;
color: #999;
}
.card-arrow {
font-size: 28rpx;
color: #007aff;
}
}
.card-hover {
background-color: #f8f8f8;
}
.button-style {
background: linear-gradient(45deg, #007aff, #09bb07);
border-radius: 25rpx;
padding: 25rpx;
text-align: center;
.button-text {
color: white;
font-size: 28rpx;
font-weight: bold;
}
}
.button-hover {
opacity: 0.8;
transform: scale(0.98);
}
}
</style>
4.2.3 页面栈管理
1. 页面栈概念
页面栈是UniApp管理页面的机制,类似于浏览器的历史记录:
uni.navigateTo()
: 新页面入栈uni.redirectTo()
: 当前页面出栈,新页面入栈uni.navigateBack()
: 页面出栈uni.reLaunch()
: 清空页面栈,新页面入栈uni.switchTab()
: 清空页面栈,切换到tabBar页面
2. 页面栈操作示例
<template>
<view class="page-stack-demo">
<text class="title">页面栈管理演示</text>
<!-- 页面栈信息 -->
<view class="section">
<text class="section-title">当前页面栈信息</text>
<view class="stack-info">
<text class="info-item">页面栈长度: {{ stackLength }}</text>
<text class="info-item">当前页面: {{ currentPage }}</text>
<text class="info-item">可返回: {{ canGoBack ? '是' : '否' }}</text>
</view>
<button @click="getPageStack">刷新页面栈信息</button>
</view>
<!-- 页面栈操作 -->
<view class="section">
<text class="section-title">页面栈操作</text>
<button @click="pushPage">入栈新页面</button>
<button @click="replacePage">替换当前页面</button>
<button @click="goBack">返回上一页</button>
<button @click="goBackMultiple">返回多级页面</button>
<button @click="clearStack">清空页面栈</button>
</view>
<!-- 页面栈历史 -->
<view class="section">
<text class="section-title">页面栈历史</text>
<view class="stack-history">
<view
v-for="(page, index) in pageHistory"
:key="index"
class="history-item"
>
<text class="page-index">{{ index + 1 }}</text>
<text class="page-path">{{ page.route }}</text>
<text class="page-time">{{ formatTime(page.timestamp) }}</text>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'PageStackDemo',
data() {
return {
stackLength: 0,
currentPage: '',
canGoBack: false,
pageHistory: []
}
},
onLoad() {
this.getPageStack()
this.recordPageVisit()
},
onShow() {
this.getPageStack()
},
methods: {
// 获取页面栈信息
getPageStack() {
const pages = getCurrentPages()
this.stackLength = pages.length
this.canGoBack = pages.length > 1
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
this.currentPage = currentPage.route || 'unknown'
}
},
// 记录页面访问
recordPageVisit() {
const pages = getCurrentPages()
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
const pageInfo = {
route: currentPage.route || 'unknown',
timestamp: Date.now()
}
// 获取历史记录
let history = uni.getStorageSync('pageHistory') || []
history.push(pageInfo)
// 限制历史记录数量
if (history.length > 20) {
history = history.slice(-20)
}
// 保存历史记录
uni.setStorageSync('pageHistory', history)
this.pageHistory = history
}
},
// 入栈新页面
pushPage() {
uni.navigateTo({
url: '/pages/detail/detail?from=stack-demo',
success: () => {
console.log('页面入栈成功')
}
})
},
// 替换当前页面
replacePage() {
uni.redirectTo({
url: '/pages/list/list?from=stack-demo',
success: () => {
console.log('页面替换成功')
}
})
},
// 返回上一页
goBack() {
if (this.canGoBack) {
uni.navigateBack({
delta: 1,
success: () => {
console.log('返回成功')
},
fail: (err) => {
console.error('返回失败', err)
uni.showToast({
title: '无法返回',
icon: 'error'
})
}
})
} else {
uni.showToast({
title: '已是首页',
icon: 'none'
})
}
},
// 返回多级页面
goBackMultiple() {
if (this.stackLength > 2) {
uni.navigateBack({
delta: 2,
success: () => {
console.log('返回两级页面成功')
}
})
} else {
uni.showToast({
title: '页面栈不足',
icon: 'none'
})
}
},
// 清空页面栈
clearStack() {
uni.showModal({
title: '确认操作',
content: '这将清空所有页面并返回首页',
success: (res) => {
if (res.confirm) {
uni.reLaunch({
url: '/pages/index/index',
success: () => {
console.log('页面栈已清空')
}
})
}
}
})
},
// 格式化时间
formatTime(timestamp) {
const date = new Date(timestamp)
const hours = date.getHours().toString().padStart(2, '0')
const minutes = date.getMinutes().toString().padStart(2, '0')
const seconds = date.getSeconds().toString().padStart(2, '0')
return `${hours}:${minutes}:${seconds}`
}
}
}
</script>
<style lang="scss" scoped>
.page-stack-demo {
padding: 20rpx;
.title {
display: block;
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
color: #333;
}
.section {
margin-bottom: 40rpx;
padding: 20rpx;
background-color: #f8f8f8;
border-radius: 12rpx;
.section-title {
display: block;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: #333;
}
button {
width: 100%;
margin-bottom: 15rpx;
padding: 20rpx;
background-color: #007aff;
color: white;
border: none;
border-radius: 8rpx;
font-size: 26rpx;
&:last-child {
margin-bottom: 0;
}
&:active {
background-color: #0056cc;
}
}
}
.stack-info {
margin-bottom: 20rpx;
.info-item {
display: block;
font-size: 24rpx;
color: #666;
margin-bottom: 10rpx;
padding: 10rpx 15rpx;
background-color: white;
border-radius: 8rpx;
}
}
.stack-history {
max-height: 400rpx;
overflow-y: auto;
.history-item {
display: flex;
align-items: center;
padding: 15rpx;
margin-bottom: 10rpx;
background-color: white;
border-radius: 8rpx;
.page-index {
width: 60rpx;
text-align: center;
font-size: 22rpx;
color: #999;
background-color: #f0f0f0;
border-radius: 50%;
padding: 5rpx;
margin-right: 15rpx;
}
.page-path {
flex: 1;
font-size: 24rpx;
color: #333;
}
.page-time {
font-size: 20rpx;
color: #999;
}
}
}
}
</style>
4.3 路由参数传递
4.3.1 URL参数传递
1. 基础参数传递
<!-- 发送页面 -->
<template>
<view class="param-sender">
<text class="title">参数传递演示</text>
<view class="section">
<text class="section-title">基础参数</text>
<button @click="sendBasicParams">传递基础参数</button>
<button @click="sendMultipleParams">传递多个参数</button>
</view>
<view class="section">
<text class="section-title">复杂参数</text>
<button @click="sendObjectParam">传递对象参数</button>
<button @click="sendArrayParam">传递数组参数</button>
</view>
<view class="section">
<text class="section-title">特殊字符</text>
<button @click="sendSpecialChars">传递特殊字符</button>
<button @click="sendChineseText">传递中文文本</button>
</view>
</view>
</template>
<script>
export default {
name: 'ParamSender',
methods: {
// 传递基础参数
sendBasicParams() {
const id = 12345
const name = 'iPhone 15 Pro'
const price = 8999
uni.navigateTo({
url: `/pages/detail/detail?id=${id}&name=${encodeURIComponent(name)}&price=${price}`
})
},
// 传递多个参数
sendMultipleParams() {
const params = {
category: 'electronics',
brand: 'apple',
model: 'iPhone 15 Pro',
color: '深空黑色',
storage: '256GB',
price: 8999,
discount: 0.95,
inStock: true
}
const queryString = Object.keys(params)
.map(key => `${key}=${encodeURIComponent(params[key])}`)
.join('&')
uni.navigateTo({
url: `/pages/detail/detail?${queryString}`
})
},
// 传递对象参数
sendObjectParam() {
const productInfo = {
id: 12345,
name: 'iPhone 15 Pro',
price: 8999,
specs: {
color: '深空黑色',
storage: '256GB',
screen: '6.1英寸',
camera: '4800万像素'
},
features: ['Face ID', '无线充电', '防水'],
seller: {
id: 'apple_store',
name: 'Apple官方旗舰店',
rating: 4.9
}
}
uni.navigateTo({
url: `/pages/detail/detail?data=${encodeURIComponent(JSON.stringify(productInfo))}`
})
},
// 传递数组参数
sendArrayParam() {
const categories = ['手机', '电脑', '平板', '耳机']
const tags = ['热销', '新品', '推荐']
const params = {
categories: JSON.stringify(categories),
tags: JSON.stringify(tags),
type: 'list'
}
const queryString = Object.keys(params)
.map(key => `${key}=${encodeURIComponent(params[key])}`)
.join('&')
uni.navigateTo({
url: `/pages/list/list?${queryString}`
})
},
// 传递特殊字符
sendSpecialChars() {
const specialText = 'Hello & World! @#$%^&*()_+-=[]{}|;:",./<>?'
const url = 'https://www.example.com/api?param=value'
uni.navigateTo({
url: `/pages/detail/detail?text=${encodeURIComponent(specialText)}&url=${encodeURIComponent(url)}`
})
},
// 传递中文文本
sendChineseText() {
const title = '苹果iPhone 15 Pro 深空黑色 256GB'
const description = '全新设计,钛金属材质,A17 Pro芯片,专业级摄像系统'
uni.navigateTo({
url: `/pages/detail/detail?title=${encodeURIComponent(title)}&desc=${encodeURIComponent(description)}`
})
}
}
}
</script>
<style lang="scss" scoped>
.param-sender {
padding: 20rpx;
.title {
display: block;
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
color: #333;
}
.section {
margin-bottom: 40rpx;
padding: 20rpx;
background-color: #f8f8f8;
border-radius: 12rpx;
.section-title {
display: block;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: #333;
}
button {
width: 100%;
margin-bottom: 15rpx;
padding: 20rpx;
background-color: #007aff;
color: white;
border: none;
border-radius: 8rpx;
font-size: 26rpx;
&:last-child {
margin-bottom: 0;
}
&:active {
background-color: #0056cc;
}
}
}
}
</style>
2. 接收页面参数
<!-- 接收页面 -->
<template>
<view class="param-receiver">
<text class="title">接收到的参数</text>
<!-- 基础参数显示 -->
<view class="section" v-if="basicParams">
<text class="section-title">基础参数</text>
<view class="param-list">
<view
v-for="(value, key) in basicParams"
:key="key"
class="param-item"
>
<text class="param-key">{{ key }}:</text>
<text class="param-value">{{ value }}</text>
</view>
</view>
</view>
<!-- 对象参数显示 -->
<view class="section" v-if="objectData">
<text class="section-title">对象参数</text>
<view class="object-display">
<text class="object-json">{{ JSON.stringify(objectData, null, 2) }}</text>
</view>
</view>
<!-- 数组参数显示 -->
<view class="section" v-if="arrayData.length > 0">
<text class="section-title">数组参数</text>
<view class="array-display">
<view
v-for="(item, index) in arrayData"
:key="index"
class="array-item"
>
<text class="array-index">{{ index }}:</text>
<text class="array-value">{{ item }}</text>
</view>
</view>
</view>
<!-- 原始查询字符串 -->
<view class="section">
<text class="section-title">原始查询字符串</text>
<text class="query-string">{{ rawQuery }}</text>
</view>
<!-- 操作按钮 -->
<view class="section">
<text class="section-title">操作</text>
<button @click="refreshParams">刷新参数</button>
<button @click="goBack">返回上一页</button>
</view>
</view>
</template>
<script>
export default {
name: 'ParamReceiver',
data() {
return {
basicParams: null,
objectData: null,
arrayData: [],
rawQuery: ''
}
},
onLoad(options) {
console.log('接收到的参数:', options)
this.parseParams(options)
},
methods: {
// 解析参数
parseParams(options) {
this.rawQuery = JSON.stringify(options, null, 2)
// 处理基础参数
const basicParams = {}
const excludeKeys = ['data', 'categories', 'tags']
Object.keys(options).forEach(key => {
if (!excludeKeys.includes(key)) {
basicParams[key] = this.decodeParam(options[key])
}
})
if (Object.keys(basicParams).length > 0) {
this.basicParams = basicParams
}
// 处理对象参数
if (options.data) {
try {
this.objectData = JSON.parse(decodeURIComponent(options.data))
} catch (error) {
console.error('解析对象参数失败:', error)
uni.showToast({
title: '参数解析失败',
icon: 'error'
})
}
}
// 处理数组参数
const arrayData = []
if (options.categories) {
try {
const categories = JSON.parse(decodeURIComponent(options.categories))
arrayData.push(...categories.map(item => `分类: ${item}`))
} catch (error) {
console.error('解析分类参数失败:', error)
}
}
if (options.tags) {
try {
const tags = JSON.parse(decodeURIComponent(options.tags))
arrayData.push(...tags.map(item => `标签: ${item}`))
} catch (error) {
console.error('解析标签参数失败:', error)
}
}
this.arrayData = arrayData
},
// 解码参数
decodeParam(param) {
try {
return decodeURIComponent(param)
} catch (error) {
console.error('参数解码失败:', error)
return param
}
},
// 刷新参数
refreshParams() {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
if (currentPage && currentPage.options) {
this.parseParams(currentPage.options)
uni.showToast({
title: '参数已刷新',
icon: 'success'
})
}
},
// 返回上一页
goBack() {
uni.navigateBack()
}
}
}
</script>
<style lang="scss" scoped>
.param-receiver {
padding: 20rpx;
.title {
display: block;
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
color: #333;
}
.section {
margin-bottom: 40rpx;
padding: 20rpx;
background-color: #f8f8f8;
border-radius: 12rpx;
.section-title {
display: block;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: #333;
}
}
.param-list {
.param-item {
display: flex;
padding: 15rpx;
margin-bottom: 10rpx;
background-color: white;
border-radius: 8rpx;
.param-key {
width: 150rpx;
font-size: 24rpx;
font-weight: bold;
color: #666;
}
.param-value {
flex: 1;
font-size: 24rpx;
color: #333;
word-break: break-all;
}
}
}
.object-display {
background-color: white;
border-radius: 8rpx;
padding: 20rpx;
.object-json {
font-family: 'Courier New', monospace;
font-size: 22rpx;
color: #333;
white-space: pre-wrap;
word-break: break-all;
}
}
.array-display {
.array-item {
display: flex;
padding: 15rpx;
margin-bottom: 10rpx;
background-color: white;
border-radius: 8rpx;
.array-index {
width: 80rpx;
font-size: 24rpx;
font-weight: bold;
color: #666;
}
.array-value {
flex: 1;
font-size: 24rpx;
color: #333;
}
}
}
.query-string {
display: block;
background-color: white;
border-radius: 8rpx;
padding: 20rpx;
font-family: 'Courier New', monospace;
font-size: 22rpx;
color: #333;
white-space: pre-wrap;
word-break: break-all;
}
button {
width: 100%;
margin-bottom: 15rpx;
padding: 20rpx;
background-color: #007aff;
color: white;
border: none;
border-radius: 8rpx;
font-size: 26rpx;
&:last-child {
margin-bottom: 0;
}
&:active {
background-color: #0056cc;
}
}
}
</style>
4.3.2 事件传参
1. 全局事件总线
// 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]) {
const index = this.events[event].indexOf(callback)
if (index > -1) {
this.events[event].splice(index, 1)
}
}
}
// 只监听一次
once(event, callback) {
const onceCallback = (data) => {
callback(data)
this.off(event, onceCallback)
}
this.on(event, onceCallback)
}
}
export default new EventBus()
2. 使用事件总线传递数据
<!-- 发送页面 -->
<template>
<view class="event-sender">
<text class="title">事件传参演示</text>
<view class="section">
<text class="section-title">发送数据</text>
<button @click="sendUserData">发送用户数据</button>
<button @click="sendProductData">发送商品数据</button>
<button @click="sendFormData">发送表单数据</button>
</view>
<view class="section">
<text class="section-title">跳转并发送</text>
<button @click="navigateAndSend">跳转并发送数据</button>
</view>
</view>
</template>
<script>
import EventBus from '@/utils/eventBus.js'
export default {
name: 'EventSender',
data() {
return {
userData: {
id: 123,
name: '张三',
email: 'zhangsan@example.com',
avatar: '/static/avatar.jpg'
},
productData: {
id: 12345,
name: 'iPhone 15 Pro',
price: 8999,
images: ['/static/phone1.jpg', '/static/phone2.jpg']
}
}
},
methods: {
sendUserData() {
EventBus.emit('userDataUpdate', this.userData)
uni.showToast({
title: '用户数据已发送',
icon: 'success'
})
},
sendProductData() {
EventBus.emit('productDataUpdate', this.productData)
uni.showToast({
title: '商品数据已发送',
icon: 'success'
})
},
sendFormData() {
const formData = {
title: '新建订单',
items: [
{ id: 1, name: '商品A', quantity: 2 },
{ id: 2, name: '商品B', quantity: 1 }
],
total: 299.99,
timestamp: Date.now()
}
EventBus.emit('formDataSubmit', formData)
uni.showToast({
title: '表单数据已发送',
icon: 'success'
})
},
navigateAndSend() {
// 先发送数据
EventBus.emit('pageDataTransfer', {
type: 'navigation',
source: 'event-sender',
data: this.productData,
timestamp: Date.now()
})
// 然后跳转页面
uni.navigateTo({
url: '/pages/receiver/receiver'
})
}
}
}
</script>
3. 接收页面监听事件
<!-- 接收页面 -->
<template>
<view class="event-receiver">
<text class="title">事件接收演示</text>
<view class="section" v-if="receivedData.length > 0">
<text class="section-title">接收到的数据</text>
<view
v-for="(item, index) in receivedData"
:key="index"
class="data-item"
>
<text class="data-type">{{ item.type }}</text>
<text class="data-time">{{ formatTime(item.timestamp) }}</text>
<text class="data-content">{{ JSON.stringify(item.data, null, 2) }}</text>
</view>
</view>
<view class="section">
<text class="section-title">操作</text>
<button @click="clearData">清空数据</button>
<button @click="goBack">返回上一页</button>
</view>
</view>
</template>
<script>
import EventBus from '@/utils/eventBus.js'
export default {
name: 'EventReceiver',
data() {
return {
receivedData: []
}
},
onLoad() {
this.setupEventListeners()
},
onUnload() {
this.removeEventListeners()
},
methods: {
setupEventListeners() {
// 监听用户数据更新
EventBus.on('userDataUpdate', this.handleUserData)
// 监听商品数据更新
EventBus.on('productDataUpdate', this.handleProductData)
// 监听表单数据提交
EventBus.on('formDataSubmit', this.handleFormData)
// 监听页面数据传输
EventBus.on('pageDataTransfer', this.handlePageData)
},
removeEventListeners() {
EventBus.off('userDataUpdate', this.handleUserData)
EventBus.off('productDataUpdate', this.handleProductData)
EventBus.off('formDataSubmit', this.handleFormData)
EventBus.off('pageDataTransfer', this.handlePageData)
},
handleUserData(data) {
this.addReceivedData('用户数据', data)
},
handleProductData(data) {
this.addReceivedData('商品数据', data)
},
handleFormData(data) {
this.addReceivedData('表单数据', data)
},
handlePageData(data) {
this.addReceivedData('页面数据', data)
},
addReceivedData(type, data) {
this.receivedData.unshift({
type,
data,
timestamp: Date.now()
})
// 限制数据数量
if (this.receivedData.length > 10) {
this.receivedData = this.receivedData.slice(0, 10)
}
},
formatTime(timestamp) {
const date = new Date(timestamp)
return date.toLocaleTimeString()
},
clearData() {
this.receivedData = []
uni.showToast({
title: '数据已清空',
icon: 'success'
})
},
goBack() {
uni.navigateBack()
}
}
}
</script>
4.3.3 存储传参
1. 本地存储传参
<template>
<view class="storage-demo">
<text class="title">存储传参演示</text>
<view class="section">
<text class="section-title">存储数据</text>
<button @click="storeUserInfo">存储用户信息</button>
<button @click="storeShoppingCart">存储购物车</button>
<button @click="storeFormData">存储表单数据</button>
</view>
<view class="section">
<text class="section-title">读取数据</text>
<button @click="readStoredData">读取存储数据</button>
<button @click="clearStorage">清空存储</button>
</view>
<view class="section" v-if="storedData">
<text class="section-title">存储的数据</text>
<text class="data-display">{{ JSON.stringify(storedData, null, 2) }}</text>
</view>
</view>
</template>
<script>
export default {
name: 'StorageDemo',
data() {
return {
storedData: null
}
},
onLoad() {
this.readStoredData()
},
methods: {
// 存储用户信息
storeUserInfo() {
const userInfo = {
id: 123,
name: '张三',
email: 'zhangsan@example.com',
avatar: '/static/avatar.jpg',
preferences: {
theme: 'light',
language: 'zh-CN',
notifications: true
},
lastLogin: Date.now()
}
try {
uni.setStorageSync('userInfo', userInfo)
uni.showToast({
title: '用户信息已存储',
icon: 'success'
})
} catch (error) {
console.error('存储失败:', error)
uni.showToast({
title: '存储失败',
icon: 'error'
})
}
},
// 存储购物车
storeShoppingCart() {
const cartData = {
items: [
{
id: 1,
name: 'iPhone 15 Pro',
price: 8999,
quantity: 1,
image: '/static/phone.jpg'
},
{
id: 2,
name: 'AirPods Pro',
price: 1999,
quantity: 2,
image: '/static/airpods.jpg'
}
],
total: 12997,
discount: 500,
finalTotal: 12497,
updateTime: Date.now()
}
uni.setStorage({
key: 'shoppingCart',
data: cartData,
success: () => {
uni.showToast({
title: '购物车已存储',
icon: 'success'
})
},
fail: (error) => {
console.error('存储失败:', error)
uni.showToast({
title: '存储失败',
icon: 'error'
})
}
})
},
// 存储表单数据
storeFormData() {
const formData = {
personalInfo: {
name: '李四',
phone: '13800138000',
email: 'lisi@example.com',
address: '北京市朝阳区'
},
orderInfo: {
orderNo: 'ORD20240101001',
products: ['iPhone 15', 'MacBook Pro'],
amount: 25998,
paymentMethod: 'alipay'
},
timestamp: Date.now(),
status: 'draft'
}
// 使用加密存储敏感数据
const encryptedData = this.encryptData(formData)
uni.setStorageSync('formData', encryptedData)
uni.showToast({
title: '表单数据已存储',
icon: 'success'
})
},
// 读取存储数据
readStoredData() {
try {
const userInfo = uni.getStorageSync('userInfo')
const cartData = uni.getStorageSync('shoppingCart')
const formData = uni.getStorageSync('formData')
this.storedData = {
userInfo: userInfo || null,
shoppingCart: cartData || null,
formData: formData ? this.decryptData(formData) : null
}
if (this.storedData.userInfo || this.storedData.shoppingCart || this.storedData.formData) {
uni.showToast({
title: '数据读取成功',
icon: 'success'
})
} else {
uni.showToast({
title: '暂无存储数据',
icon: 'none'
})
}
} catch (error) {
console.error('读取数据失败:', error)
uni.showToast({
title: '读取失败',
icon: 'error'
})
}
},
// 清空存储
clearStorage() {
uni.showModal({
title: '确认清空',
content: '确定要清空所有存储数据吗?',
success: (res) => {
if (res.confirm) {
try {
uni.removeStorageSync('userInfo')
uni.removeStorageSync('shoppingCart')
uni.removeStorageSync('formData')
this.storedData = null
uni.showToast({
title: '存储已清空',
icon: 'success'
})
} catch (error) {
console.error('清空失败:', error)
uni.showToast({
title: '清空失败',
icon: 'error'
})
}
}
}
})
},
// 简单加密(实际项目中应使用更安全的加密方法)
encryptData(data) {
const jsonString = JSON.stringify(data)
return btoa(encodeURIComponent(jsonString))
},
// 简单解密
decryptData(encryptedData) {
try {
const jsonString = decodeURIComponent(atob(encryptedData))
return JSON.parse(jsonString)
} catch (error) {
console.error('解密失败:', error)
return null
}
}
}
}
</script>
4.4 TabBar导航
4.4.1 TabBar配置
1. 基础TabBar配置
{
"tabBar": {
"color": "#7A7E83",
"selectedColor": "#007aff",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"position": "bottom",
"fontSize": "10px",
"iconWidth": "24px",
"spacing": "3px",
"height": "50px",
"midButton": {
"width": "80px",
"height": "50px",
"text": "发布",
"iconPath": "static/tab-publish.png",
"iconWidth": "24px",
"backgroundImage": "static/tab-mid-bg.png"
},
"list": [
{
"pagePath": "pages/index/index",
"iconPath": "static/tab-home.png",
"selectedIconPath": "static/tab-home-active.png",
"text": "首页"
},
{
"pagePath": "pages/category/category",
"iconPath": "static/tab-category.png",
"selectedIconPath": "static/tab-category-active.png",
"text": "分类"
},
{
"pagePath": "pages/publish/publish",
"iconPath": "static/tab-publish.png",
"selectedIconPath": "static/tab-publish-active.png",
"text": "发布"
},
{
"pagePath": "pages/message/message",
"iconPath": "static/tab-message.png",
"selectedIconPath": "static/tab-message-active.png",
"text": "消息"
},
{
"pagePath": "pages/user/user",
"iconPath": "static/tab-user.png",
"selectedIconPath": "static/tab-user-active.png",
"text": "我的"
}
]
}
}
2. 自定义TabBar样式
{
"tabBar": {
"custom": true,
"color": "#7A7E83",
"selectedColor": "#007aff",
"borderStyle": "white",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页"
},
{
"pagePath": "pages/category/category",
"text": "分类"
},
{
"pagePath": "pages/cart/cart",
"text": "购物车"
},
{
"pagePath": "pages/user/user",
"text": "我的"
}
]
}
}
4.4.2 自定义TabBar组件
1. 创建自定义TabBar组件
<!-- custom-tab-bar/index.vue -->
<template>
<view class="custom-tab-bar" :style="{ paddingBottom: safeAreaBottom + 'px' }">
<view class="tab-bar-container">
<view
v-for="(item, index) in tabList"
:key="index"
class="tab-item"
:class="{ active: currentIndex === index }"
@click="switchTab(item, index)"
>
<!-- 图标 -->
<view class="tab-icon">
<image
v-if="item.iconPath"
:src="currentIndex === index ? item.selectedIconPath : item.iconPath"
class="icon-image"
/>
<text
v-else
class="icon-text"
:class="{ active: currentIndex === index }"
>
{{ item.icon }}
</text>
<!-- 角标 -->
<view
v-if="item.badge"
class="badge"
:class="{ dot: item.badge === 'dot' }"
>
<text v-if="item.badge !== 'dot'" class="badge-text">{{ item.badge }}</text>
</view>
</view>
<!-- 文字 -->
<text class="tab-text" :class="{ active: currentIndex === index }">
{{ item.text }}
</text>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'CustomTabBar',
data() {
return {
currentIndex: 0,
safeAreaBottom: 0,
tabList: [
{
pagePath: '/pages/index/index',
text: '首页',
iconPath: '/static/tab-home.png',
selectedIconPath: '/static/tab-home-active.png',
badge: null
},
{
pagePath: '/pages/category/category',
text: '分类',
iconPath: '/static/tab-category.png',
selectedIconPath: '/static/tab-category-active.png',
badge: null
},
{
pagePath: '/pages/cart/cart',
text: '购物车',
iconPath: '/static/tab-cart.png',
selectedIconPath: '/static/tab-cart-active.png',
badge: 3
},
{
pagePath: '/pages/message/message',
text: '消息',
iconPath: '/static/tab-message.png',
selectedIconPath: '/static/tab-message-active.png',
badge: 'dot'
},
{
pagePath: '/pages/user/user',
text: '我的',
iconPath: '/static/tab-user.png',
selectedIconPath: '/static/tab-user-active.png',
badge: null
}
]
}
},
mounted() {
this.getSafeAreaBottom()
this.updateCurrentIndex()
},
methods: {
// 获取安全区域底部高度
getSafeAreaBottom() {
const systemInfo = uni.getSystemInfoSync()
this.safeAreaBottom = systemInfo.safeAreaInsets ? systemInfo.safeAreaInsets.bottom : 0
},
// 更新当前选中索引
updateCurrentIndex() {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const currentRoute = '/' + currentPage.route
const index = this.tabList.findIndex(item => item.pagePath === currentRoute)
if (index !== -1) {
this.currentIndex = index
}
},
// 切换Tab
switchTab(item, index) {
if (this.currentIndex === index) {
return
}
this.currentIndex = index
uni.switchTab({
url: item.pagePath,
fail: (err) => {
console.error('切换Tab失败:', err)
}
})
},
// 更新角标
updateBadge(index, badge) {
if (index >= 0 && index < this.tabList.length) {
this.tabList[index].badge = badge
}
},
// 清除角标
clearBadge(index) {
this.updateBadge(index, null)
}
}
}
</script>
<style lang="scss" scoped>
.custom-tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #ffffff;
border-top: 1rpx solid #e5e5e5;
z-index: 1000;
.tab-bar-container {
display: flex;
height: 100rpx;
.tab-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
padding: 10rpx 0;
&.active {
.tab-icon {
transform: scale(1.1);
}
}
.tab-icon {
position: relative;
margin-bottom: 8rpx;
transition: transform 0.2s ease;
.icon-image {
width: 48rpx;
height: 48rpx;
}
.icon-text {
font-size: 48rpx;
color: #7A7E83;
&.active {
color: #007aff;
}
}
.badge {
position: absolute;
top: -8rpx;
right: -8rpx;
min-width: 32rpx;
height: 32rpx;
background-color: #ff3b30;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
&.dot {
width: 16rpx;
height: 16rpx;
min-width: 16rpx;
border-radius: 8rpx;
top: -4rpx;
right: -4rpx;
}
.badge-text {
color: white;
font-size: 20rpx;
font-weight: bold;
padding: 0 8rpx;
}
}
}
.tab-text {
font-size: 20rpx;
color: #7A7E83;
transition: color 0.2s ease;
&.active {
color: #007aff;
font-weight: bold;
}
}
}
}
}
</style>
2. 在页面中使用自定义TabBar
<!-- pages/index/index.vue -->
<template>
<view class="page-container">
<!-- 页面内容 -->
<view class="content">
<text class="title">首页内容</text>
<view class="section">
<text class="section-title">TabBar操作</text>
<button @click="updateCartBadge">更新购物车角标</button>
<button @click="showMessageDot">显示消息红点</button>
<button @click="clearAllBadges">清除所有角标</button>
</view>
</view>
<!-- 自定义TabBar -->
<custom-tab-bar ref="tabBar" />
</view>
</template>
<script>
import CustomTabBar from '@/custom-tab-bar/index.vue'
export default {
name: 'IndexPage',
components: {
CustomTabBar
},
data() {
return {
cartCount: 0
}
},
methods: {
updateCartBadge() {
this.cartCount += 1
this.$refs.tabBar.updateBadge(2, this.cartCount)
uni.showToast({
title: `购物车角标: ${this.cartCount}`,
icon: 'success'
})
},
showMessageDot() {
this.$refs.tabBar.updateBadge(3, 'dot')
uni.showToast({
title: '消息红点已显示',
icon: 'success'
})
},
clearAllBadges() {
this.$refs.tabBar.clearBadge(2)
this.$refs.tabBar.clearBadge(3)
this.cartCount = 0
uni.showToast({
title: '角标已清除',
icon: 'success'
})
}
}
}
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
padding-bottom: 100rpx; // 为TabBar留出空间
.content {
padding: 20rpx;
.title {
display: block;
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
color: #333;
}
.section {
margin-bottom: 40rpx;
padding: 20rpx;
background-color: #f8f8f8;
border-radius: 12rpx;
.section-title {
display: block;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: #333;
}
button {
width: 100%;
margin-bottom: 15rpx;
padding: 20rpx;
background-color: #007aff;
color: white;
border: none;
border-radius: 8rpx;
font-size: 26rpx;
&:last-child {
margin-bottom: 0;
}
&:active {
background-color: #0056cc;
}
}
}
}
}
</style>
4.4.3 TabBar切换控制
1. 编程式TabBar切换
<template>
<view class="tab-control-demo">
<text class="title">TabBar切换控制</text>
<view class="section">
<text class="section-title">基础切换</text>
<button @click="switchToHome">切换到首页</button>
<button @click="switchToCategory">切换到分类</button>
<button @click="switchToCart">切换到购物车</button>
<button @click="switchToUser">切换到我的</button>
</view>
<view class="section">
<text class="section-title">条件切换</text>
<button @click="switchToCartWithCheck">检查登录后切换</button>
<button @click="switchToUserWithAuth">验证权限后切换</button>
</view>
<view class="section">
<text class="section-title">TabBar信息</text>
<view class="info-item">
<text class="info-label">当前Tab:</text>
<text class="info-value">{{ currentTabText }}</text>
</view>
<view class="info-item">
<text class="info-label">Tab索引:</text>
<text class="info-value">{{ currentTabIndex }}</text>
</view>
<button @click="getCurrentTabInfo">获取当前Tab信息</button>
</view>
</view>
</template>
<script>
export default {
name: 'TabControlDemo',
data() {
return {
currentTabText: '',
currentTabIndex: -1,
tabList: [
{ path: '/pages/index/index', text: '首页' },
{ path: '/pages/category/category', text: '分类' },
{ path: '/pages/cart/cart', text: '购物车' },
{ path: '/pages/user/user', text: '我的' }
]
}
},
onLoad() {
this.getCurrentTabInfo()
},
onShow() {
this.getCurrentTabInfo()
},
methods: {
// 切换到首页
switchToHome() {
uni.switchTab({
url: '/pages/index/index',
success: () => {
console.log('切换到首页成功')
},
fail: (err) => {
console.error('切换失败:', err)
uni.showToast({
title: '切换失败',
icon: 'error'
})
}
})
},
// 切换到分类
switchToCategory() {
uni.switchTab({
url: '/pages/category/category'
})
},
// 切换到购物车
switchToCart() {
uni.switchTab({
url: '/pages/cart/cart'
})
},
// 切换到我的
switchToUser() {
uni.switchTab({
url: '/pages/user/user'
})
},
// 检查登录后切换到购物车
switchToCartWithCheck() {
const token = uni.getStorageSync('token')
if (!token) {
uni.showModal({
title: '提示',
content: '请先登录后再查看购物车',
confirmText: '去登录',
success: (res) => {
if (res.confirm) {
uni.navigateTo({
url: '/pages/login/login'
})
}
}
})
return
}
uni.switchTab({
url: '/pages/cart/cart'
})
},
// 验证权限后切换
switchToUserWithAuth() {
const userInfo = uni.getStorageSync('userInfo')
if (!userInfo) {
uni.showToast({
title: '请先登录',
icon: 'none'
})
return
}
if (userInfo.status !== 'active') {
uni.showModal({
title: '账号异常',
content: '您的账号状态异常,请联系客服',
showCancel: false
})
return
}
uni.switchTab({
url: '/pages/user/user'
})
},
// 获取当前Tab信息
getCurrentTabInfo() {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const currentRoute = '/' + currentPage.route
const tabIndex = this.tabList.findIndex(item => item.path === currentRoute)
if (tabIndex !== -1) {
this.currentTabIndex = tabIndex
this.currentTabText = this.tabList[tabIndex].text
} else {
this.currentTabIndex = -1
this.currentTabText = '非Tab页面'
}
}
}
}
</script>
<style lang="scss" scoped>
.tab-control-demo {
padding: 20rpx;
.title {
display: block;
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
color: #333;
}
.section {
margin-bottom: 40rpx;
padding: 20rpx;
background-color: #f8f8f8;
border-radius: 12rpx;
.section-title {
display: block;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: #333;
}
button {
width: 100%;
margin-bottom: 15rpx;
padding: 20rpx;
background-color: #007aff;
color: white;
border: none;
border-radius: 8rpx;
font-size: 26rpx;
&:last-child {
margin-bottom: 0;
}
&:active {
background-color: #0056cc;
}
}
.info-item {
display: flex;
align-items: center;
padding: 15rpx;
margin-bottom: 10rpx;
background-color: white;
border-radius: 8rpx;
.info-label {
width: 150rpx;
font-size: 24rpx;
color: #666;
}
.info-value {
flex: 1;
font-size: 24rpx;
color: #333;
font-weight: bold;
}
}
}
}
</style>
4.5 路由守卫与权限控制
4.5.1 路由拦截器
1. 创建路由拦截器
// utils/routeGuard.js
class RouteGuard {
constructor() {
this.beforeHooks = []
this.afterHooks = []
this.init()
}
// 初始化路由拦截
init() {
this.interceptNavigateTo()
this.interceptRedirectTo()
this.interceptReLaunch()
this.interceptSwitchTab()
}
// 添加前置守卫
beforeEach(hook) {
this.beforeHooks.push(hook)
}
// 添加后置守卫
afterEach(hook) {
this.afterHooks.push(hook)
}
// 执行前置守卫
async runBeforeHooks(to, from) {
for (const hook of this.beforeHooks) {
const result = await hook(to, from)
if (result === false) {
return false
}
if (typeof result === 'string') {
return result // 重定向路径
}
}
return true
}
// 执行后置守卫
runAfterHooks(to, from) {
this.afterHooks.forEach(hook => {
hook(to, from)
})
}
// 拦截 navigateTo
interceptNavigateTo() {
const originalNavigateTo = uni.navigateTo
uni.navigateTo = async (options) => {
const to = this.parseRoute(options.url)
const from = this.getCurrentRoute()
const result = await this.runBeforeHooks(to, from)
if (result === false) {
return
}
if (typeof result === 'string') {
options.url = result
}
const originalSuccess = options.success
options.success = (res) => {
this.runAfterHooks(to, from)
originalSuccess && originalSuccess(res)
}
return originalNavigateTo(options)
}
}
// 拦截 redirectTo
interceptRedirectTo() {
const originalRedirectTo = uni.redirectTo
uni.redirectTo = async (options) => {
const to = this.parseRoute(options.url)
const from = this.getCurrentRoute()
const result = await this.runBeforeHooks(to, from)
if (result === false) {
return
}
if (typeof result === 'string') {
options.url = result
}
const originalSuccess = options.success
options.success = (res) => {
this.runAfterHooks(to, from)
originalSuccess && originalSuccess(res)
}
return originalRedirectTo(options)
}
}
// 拦截 reLaunch
interceptReLaunch() {
const originalReLaunch = uni.reLaunch
uni.reLaunch = async (options) => {
const to = this.parseRoute(options.url)
const from = this.getCurrentRoute()
const result = await this.runBeforeHooks(to, from)
if (result === false) {
return
}
if (typeof result === 'string') {
options.url = result
}
const originalSuccess = options.success
options.success = (res) => {
this.runAfterHooks(to, from)
originalSuccess && originalSuccess(res)
}
return originalReLaunch(options)
}
}
// 拦截 switchTab
interceptSwitchTab() {
const originalSwitchTab = uni.switchTab
uni.switchTab = async (options) => {
const to = this.parseRoute(options.url)
const from = this.getCurrentRoute()
const result = await this.runBeforeHooks(to, from)
if (result === false) {
return
}
if (typeof result === 'string') {
options.url = result
}
const originalSuccess = options.success
options.success = (res) => {
this.runAfterHooks(to, from)
originalSuccess && originalSuccess(res)
}
return originalSwitchTab(options)
}
}
// 解析路由
parseRoute(url) {
const [path, query] = url.split('?')
const params = {}
if (query) {
query.split('&').forEach(param => {
const [key, value] = param.split('=')
params[key] = decodeURIComponent(value || '')
})
}
return {
path,
query: params,
fullPath: url
}
}
// 获取当前路由
getCurrentRoute() {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
if (!currentPage) {
return { path: '', query: {}, fullPath: '' }
}
const path = '/' + currentPage.route
const query = currentPage.options || {}
const queryString = Object.keys(query)
.map(key => `${key}=${encodeURIComponent(query[key])}`)
.join('&')
return {
path,
query,
fullPath: queryString ? `${path}?${queryString}` : path
}
}
}
export default new RouteGuard()
2. 使用路由拦截器
// main.js
import { createSSRApp } from 'vue'
import App from './App.vue'
import routeGuard from './utils/routeGuard'
// 添加路由守卫
routeGuard.beforeEach(async (to, from) => {
console.log('路由跳转:', from.path, '->', to.path)
// 检查登录状态
const token = uni.getStorageSync('token')
const needAuthPages = [
'/pages/user/profile',
'/pages/order/list',
'/pages/cart/cart',
'/pages/user/settings'
]
if (needAuthPages.includes(to.path) && !token) {
uni.showToast({
title: '请先登录',
icon: 'none'
})
return '/pages/login/login'
}
// 检查用户权限
const userInfo = uni.getStorageSync('userInfo')
const adminPages = ['/pages/admin/dashboard', '/pages/admin/users']
if (adminPages.includes(to.path) && (!userInfo || userInfo.role !== 'admin')) {
uni.showToast({
title: '权限不足',
icon: 'none'
})
return false
}
return true
})
routeGuard.afterEach((to, from) => {
console.log('路由跳转完成:', to.path)
// 统计页面访问
uni.request({
url: 'https://api.example.com/analytics/page-view',
method: 'POST',
data: {
page: to.path,
timestamp: Date.now()
}
})
})
export function createApp() {
const app = createSSRApp(App)
return {
app
}
}
4.5.2 权限验证系统
1. 权限管理工具
// utils/permission.js
class PermissionManager {
constructor() {
this.permissions = []
this.roles = []
this.init()
}
// 初始化权限数据
async init() {
try {
const userInfo = uni.getStorageSync('userInfo')
if (userInfo) {
this.permissions = userInfo.permissions || []
this.roles = userInfo.roles || []
}
} catch (error) {
console.error('权限初始化失败:', error)
}
}
// 检查是否有指定权限
hasPermission(permission) {
if (!permission) return true
return this.permissions.includes(permission)
}
// 检查是否有指定角色
hasRole(role) {
if (!role) return true
return this.roles.includes(role)
}
// 检查是否有任一权限
hasAnyPermission(permissions) {
if (!permissions || permissions.length === 0) return true
return permissions.some(permission => this.hasPermission(permission))
}
// 检查是否有所有权限
hasAllPermissions(permissions) {
if (!permissions || permissions.length === 0) return true
return permissions.every(permission => this.hasPermission(permission))
}
// 检查是否有任一角色
hasAnyRole(roles) {
if (!roles || roles.length === 0) return true
return roles.some(role => this.hasRole(role))
}
// 更新权限数据
updatePermissions(userInfo) {
this.permissions = userInfo.permissions || []
this.roles = userInfo.roles || []
// 缓存到本地
uni.setStorageSync('userInfo', userInfo)
}
// 清除权限数据
clearPermissions() {
this.permissions = []
this.roles = []
uni.removeStorageSync('userInfo')
uni.removeStorageSync('token')
}
// 检查页面访问权限
checkPagePermission(pagePath) {
const pagePermissions = {
'/pages/admin/dashboard': ['admin.dashboard'],
'/pages/admin/users': ['admin.users'],
'/pages/order/manage': ['order.manage'],
'/pages/product/edit': ['product.edit'],
'/pages/finance/report': ['finance.view']
}
const requiredPermissions = pagePermissions[pagePath]
if (!requiredPermissions) return true
return this.hasAnyPermission(requiredPermissions)
}
// 检查功能权限
checkFeaturePermission(feature) {
const featurePermissions = {
'create_order': ['order.create'],
'edit_product': ['product.edit'],
'delete_user': ['user.delete'],
'view_report': ['finance.view', 'admin.dashboard']
}
const requiredPermissions = featurePermissions[feature]
if (!requiredPermissions) return true
return this.hasAnyPermission(requiredPermissions)
}
}
export default new PermissionManager()
2. 权限指令
// utils/directives.js
import permission from './permission'
// v-permission 指令
export const vPermission = {
mounted(el, binding) {
const { value } = binding
if (value && !permission.hasAnyPermission(Array.isArray(value) ? value : [value])) {
el.style.display = 'none'
}
},
updated(el, binding) {
const { value } = binding
if (value && !permission.hasAnyPermission(Array.isArray(value) ? value : [value])) {
el.style.display = 'none'
} else {
el.style.display = ''
}
}
}
// v-role 指令
export const vRole = {
mounted(el, binding) {
const { value } = binding
if (value && !permission.hasAnyRole(Array.isArray(value) ? value : [value])) {
el.style.display = 'none'
}
},
updated(el, binding) {
const { value } = binding
if (value && !permission.hasAnyRole(Array.isArray(value) ? value : [value])) {
el.style.display = 'none'
} else {
el.style.display = ''
}
}
}
3. 在页面中使用权限控制
<template>
<view class="permission-demo">
<text class="title">权限控制示例</text>
<!-- 基础权限控制 -->
<view class="section">
<text class="section-title">基础权限</text>
<button
v-permission="['order.create']"
@click="createOrder"
>
创建订单
</button>
<button
v-permission="['product.edit']"
@click="editProduct"
>
编辑商品
</button>
<button
v-permission="['user.delete']"
@click="deleteUser"
>
删除用户
</button>
</view>
<!-- 角色权限控制 -->
<view class="section">
<text class="section-title">角色权限</text>
<view v-role="['admin']" class="admin-panel">
<text class="panel-title">管理员面板</text>
<button @click="goToAdminDashboard">管理后台</button>
<button @click="manageUsers">用户管理</button>
</view>
<view v-role="['manager', 'admin']" class="manager-panel">
<text class="panel-title">管理者面板</text>
<button @click="viewReports">查看报表</button>
<button @click="manageOrders">订单管理</button>
</view>
<view v-role="['user']" class="user-panel">
<text class="panel-title">用户面板</text>
<button @click="viewProfile">个人资料</button>
<button @click="viewOrders">我的订单</button>
</view>
</view>
<!-- 动态权限检查 -->
<view class="section">
<text class="section-title">动态权限检查</text>
<view class="permission-info">
<text class="info-label">当前权限:</text>
<text class="info-value">{{ currentPermissions.join(', ') }}</text>
</view>
<view class="permission-info">
<text class="info-label">当前角色:</text>
<text class="info-value">{{ currentRoles.join(', ') }}</text>
</view>
<button @click="checkPermission('order.create')">检查创建订单权限</button>
<button @click="checkRole('admin')">检查管理员角色</button>
<button @click="refreshPermissions">刷新权限</button>
</view>
</view>
</template>
<script>
import permission from '@/utils/permission'
export default {
name: 'PermissionDemo',
data() {
return {
currentPermissions: [],
currentRoles: []
}
},
onLoad() {
this.loadPermissions()
},
methods: {
// 加载权限信息
loadPermissions() {
this.currentPermissions = permission.permissions
this.currentRoles = permission.roles
},
// 创建订单
createOrder() {
if (!permission.checkFeaturePermission('create_order')) {
uni.showToast({
title: '权限不足',
icon: 'none'
})
return
}
uni.showToast({
title: '创建订单成功',
icon: 'success'
})
},
// 编辑商品
editProduct() {
if (!permission.checkFeaturePermission('edit_product')) {
uni.showToast({
title: '权限不足',
icon: 'none'
})
return
}
uni.navigateTo({
url: '/pages/product/edit'
})
},
// 删除用户
deleteUser() {
if (!permission.checkFeaturePermission('delete_user')) {
uni.showToast({
title: '权限不足',
icon: 'none'
})
return
}
uni.showModal({
title: '确认删除',
content: '确定要删除该用户吗?',
success: (res) => {
if (res.confirm) {
uni.showToast({
title: '删除成功',
icon: 'success'
})
}
}
})
},
// 跳转到管理后台
goToAdminDashboard() {
if (!permission.checkPagePermission('/pages/admin/dashboard')) {
uni.showToast({
title: '权限不足',
icon: 'none'
})
return
}
uni.navigateTo({
url: '/pages/admin/dashboard'
})
},
// 用户管理
manageUsers() {
uni.navigateTo({
url: '/pages/admin/users'
})
},
// 查看报表
viewReports() {
if (!permission.checkFeaturePermission('view_report')) {
uni.showToast({
title: '权限不足',
icon: 'none'
})
return
}
uni.navigateTo({
url: '/pages/finance/report'
})
},
// 订单管理
manageOrders() {
uni.navigateTo({
url: '/pages/order/manage'
})
},
// 查看个人资料
viewProfile() {
uni.navigateTo({
url: '/pages/user/profile'
})
},
// 查看我的订单
viewOrders() {
uni.navigateTo({
url: '/pages/order/list'
})
},
// 检查权限
checkPermission(perm) {
const hasPermission = permission.hasPermission(perm)
uni.showToast({
title: hasPermission ? '有权限' : '无权限',
icon: hasPermission ? 'success' : 'none'
})
},
// 检查角色
checkRole(role) {
const hasRole = permission.hasRole(role)
uni.showToast({
title: hasRole ? '有角色' : '无角色',
icon: hasRole ? 'success' : 'none'
})
},
// 刷新权限
async refreshPermissions() {
try {
// 模拟从服务器获取最新权限
const response = await uni.request({
url: 'https://api.example.com/user/permissions',
header: {
'Authorization': 'Bearer ' + uni.getStorageSync('token')
}
})
if (response.data.code === 200) {
permission.updatePermissions(response.data.data)
this.loadPermissions()
uni.showToast({
title: '权限刷新成功',
icon: 'success'
})
}
} catch (error) {
console.error('刷新权限失败:', error)
uni.showToast({
title: '刷新失败',
icon: 'error'
})
}
}
}
}
</script>
<style lang="scss" scoped>
.permission-demo {
padding: 20rpx;
.title {
display: block;
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
color: #333;
}
.section {
margin-bottom: 40rpx;
padding: 20rpx;
background-color: #f8f8f8;
border-radius: 12rpx;
.section-title {
display: block;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: #333;
}
button {
width: 100%;
margin-bottom: 15rpx;
padding: 20rpx;
background-color: #007aff;
color: white;
border: none;
border-radius: 8rpx;
font-size: 26rpx;
&:last-child {
margin-bottom: 0;
}
&:active {
background-color: #0056cc;
}
}
.admin-panel,
.manager-panel,
.user-panel {
padding: 20rpx;
margin-bottom: 20rpx;
border-radius: 8rpx;
.panel-title {
display: block;
font-size: 24rpx;
font-weight: bold;
margin-bottom: 15rpx;
color: #333;
}
}
.admin-panel {
background-color: #ffe6e6;
border: 2rpx solid #ff4d4f;
}
.manager-panel {
background-color: #fff7e6;
border: 2rpx solid #fa8c16;
}
.user-panel {
background-color: #e6f7ff;
border: 2rpx solid #1890ff;
}
.permission-info {
display: flex;
align-items: center;
padding: 15rpx;
margin-bottom: 10rpx;
background-color: white;
border-radius: 8rpx;
.info-label {
width: 150rpx;
font-size: 24rpx;
color: #666;
}
.info-value {
flex: 1;
font-size: 24rpx;
color: #333;
font-weight: bold;
}
}
}
}
</style>
4.5.3 登录状态管理
1. 登录状态工具
// utils/auth.js
class AuthManager {
constructor() {
this.token = ''
this.userInfo = null
this.loginTime = 0
this.tokenExpireTime = 0
this.init()
}
// 初始化认证信息
init() {
try {
this.token = uni.getStorageSync('token') || ''
this.userInfo = uni.getStorageSync('userInfo') || null
this.loginTime = uni.getStorageSync('loginTime') || 0
this.tokenExpireTime = uni.getStorageSync('tokenExpireTime') || 0
} catch (error) {
console.error('认证信息初始化失败:', error)
}
}
// 检查是否已登录
isLoggedIn() {
return !!this.token && !!this.userInfo && !this.isTokenExpired()
}
// 检查Token是否过期
isTokenExpired() {
if (!this.tokenExpireTime) return false
return Date.now() > this.tokenExpireTime
}
// 登录
async login(credentials) {
try {
const response = await uni.request({
url: 'https://api.example.com/auth/login',
method: 'POST',
data: credentials
})
if (response.data.code === 200) {
const { token, userInfo, expiresIn } = response.data.data
this.token = token
this.userInfo = userInfo
this.loginTime = Date.now()
this.tokenExpireTime = Date.now() + (expiresIn * 1000)
// 保存到本地存储
uni.setStorageSync('token', this.token)
uni.setStorageSync('userInfo', this.userInfo)
uni.setStorageSync('loginTime', this.loginTime)
uni.setStorageSync('tokenExpireTime', this.tokenExpireTime)
return { success: true, data: userInfo }
} else {
return { success: false, message: response.data.message }
}
} catch (error) {
console.error('登录失败:', error)
return { success: false, message: '网络错误' }
}
}
// 登出
async logout() {
try {
// 调用服务器登出接口
if (this.token) {
await uni.request({
url: 'https://api.example.com/auth/logout',
method: 'POST',
header: {
'Authorization': 'Bearer ' + this.token
}
})
}
} catch (error) {
console.error('服务器登出失败:', error)
} finally {
// 清除本地数据
this.clearAuthData()
}
}
// 清除认证数据
clearAuthData() {
this.token = ''
this.userInfo = null
this.loginTime = 0
this.tokenExpireTime = 0
uni.removeStorageSync('token')
uni.removeStorageSync('userInfo')
uni.removeStorageSync('loginTime')
uni.removeStorageSync('tokenExpireTime')
}
// 刷新Token
async refreshToken() {
try {
const response = await uni.request({
url: 'https://api.example.com/auth/refresh',
method: 'POST',
header: {
'Authorization': 'Bearer ' + this.token
}
})
if (response.data.code === 200) {
const { token, expiresIn } = response.data.data
this.token = token
this.tokenExpireTime = Date.now() + (expiresIn * 1000)
uni.setStorageSync('token', this.token)
uni.setStorageSync('tokenExpireTime', this.tokenExpireTime)
return true
}
} catch (error) {
console.error('刷新Token失败:', error)
}
return false
}
// 获取用户信息
getUserInfo() {
return this.userInfo
}
// 获取Token
getToken() {
return this.token
}
// 更新用户信息
updateUserInfo(userInfo) {
this.userInfo = { ...this.userInfo, ...userInfo }
uni.setStorageSync('userInfo', this.userInfo)
}
// 检查登录状态并自动刷新
async checkAndRefreshAuth() {
if (!this.token) {
return false
}
// Token即将过期,尝试刷新
if (this.tokenExpireTime - Date.now() < 5 * 60 * 1000) { // 5分钟内过期
const refreshed = await this.refreshToken()
if (!refreshed) {
this.clearAuthData()
return false
}
}
return true
}
}
export default new AuthManager()
2. 登录页面示例
<template>
<view class="login-page">
<view class="login-container">
<text class="title">用户登录</text>
<view class="form">
<view class="input-group">
<text class="label">用户名</text>
<input
v-model="form.username"
class="input"
placeholder="请输入用户名"
:disabled="loading"
/>
</view>
<view class="input-group">
<text class="label">密码</text>
<input
v-model="form.password"
class="input"
type="password"
placeholder="请输入密码"
:disabled="loading"
/>
</view>
<view class="checkbox-group">
<checkbox
v-model="form.rememberMe"
class="checkbox"
/>
<text class="checkbox-label">记住我</text>
</view>
<button
class="login-btn"
:class="{ loading: loading }"
:disabled="loading || !canSubmit"
@click="handleLogin"
>
{{ loading ? '登录中...' : '登录' }}
</button>
<view class="links">
<text class="link" @click="goToRegister">注册账号</text>
<text class="link" @click="goToForgotPassword">忘记密码</text>
</view>
</view>
</view>
</view>
</template>
<script>
import auth from '@/utils/auth'
import permission from '@/utils/permission'
export default {
name: 'LoginPage',
data() {
return {
form: {
username: '',
password: '',
rememberMe: false
},
loading: false
}
},
computed: {
canSubmit() {
return this.form.username.trim() && this.form.password.trim()
}
},
onLoad(options) {
// 检查是否已登录
if (auth.isLoggedIn()) {
this.redirectAfterLogin(options.redirect)
return
}
// 自动填充记住的用户名
const rememberedUsername = uni.getStorageSync('rememberedUsername')
if (rememberedUsername) {
this.form.username = rememberedUsername
this.form.rememberMe = true
}
},
methods: {
// 处理登录
async handleLogin() {
if (!this.canSubmit) {
uni.showToast({
title: '请填写完整信息',
icon: 'none'
})
return
}
this.loading = true
try {
const result = await auth.login({
username: this.form.username,
password: this.form.password
})
if (result.success) {
// 记住用户名
if (this.form.rememberMe) {
uni.setStorageSync('rememberedUsername', this.form.username)
} else {
uni.removeStorageSync('rememberedUsername')
}
// 更新权限信息
permission.updatePermissions(result.data)
uni.showToast({
title: '登录成功',
icon: 'success'
})
// 延迟跳转,让用户看到成功提示
setTimeout(() => {
this.redirectAfterLogin()
}, 1500)
} else {
uni.showToast({
title: result.message || '登录失败',
icon: 'none'
})
}
} catch (error) {
console.error('登录错误:', error)
uni.showToast({
title: '登录失败',
icon: 'error'
})
} finally {
this.loading = false
}
},
// 登录后重定向
redirectAfterLogin(redirect) {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const redirectUrl = redirect || currentPage.options?.redirect || '/pages/index/index'
if (redirectUrl.startsWith('/pages/') && redirectUrl.includes('tabBar')) {
uni.switchTab({ url: redirectUrl })
} else {
uni.reLaunch({ url: redirectUrl })
}
},
// 跳转到注册页面
goToRegister() {
uni.navigateTo({
url: '/pages/register/register'
})
},
// 跳转到忘记密码页面
goToForgotPassword() {
uni.navigateTo({
url: '/pages/forgot-password/forgot-password'
})
}
}
}
</script>
<style lang="scss" scoped>
.login-page {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
.login-container {
width: 100%;
max-width: 600rpx;
background-color: white;
border-radius: 20rpx;
padding: 60rpx 40rpx;
box-shadow: 0 20rpx 40rpx rgba(0, 0, 0, 0.1);
.title {
display: block;
font-size: 48rpx;
font-weight: bold;
text-align: center;
margin-bottom: 60rpx;
color: #333;
}
.form {
.input-group {
margin-bottom: 40rpx;
.label {
display: block;
font-size: 28rpx;
color: #666;
margin-bottom: 15rpx;
}
.input {
width: 100%;
height: 80rpx;
padding: 0 20rpx;
border: 2rpx solid #e5e5e5;
border-radius: 12rpx;
font-size: 28rpx;
background-color: #f8f8f8;
&:focus {
border-color: #007aff;
background-color: white;
}
&:disabled {
opacity: 0.6;
}
}
}
.checkbox-group {
display: flex;
align-items: center;
margin-bottom: 40rpx;
.checkbox {
margin-right: 15rpx;
}
.checkbox-label {
font-size: 26rpx;
color: #666;
}
}
.login-btn {
width: 100%;
height: 80rpx;
background-color: #007aff;
color: white;
border: none;
border-radius: 12rpx;
font-size: 32rpx;
font-weight: bold;
margin-bottom: 40rpx;
&:active {
background-color: #0056cc;
}
&:disabled {
background-color: #ccc;
opacity: 0.6;
}
&.loading {
background-color: #ccc;
}
}
.links {
display: flex;
justify-content: space-between;
.link {
font-size: 26rpx;
color: #007aff;
&:active {
opacity: 0.7;
}
}
}
}
}
}
</style>