{{wikiTitle}}
移动端添加页面说明
复制链接
编辑文档
CRMEB PRO 移动端添加页面说明
一、概述
CRMEB PRO移动端采用uni-app框架开发,支持一次编写多端运行(H5、微信小程序、APP等)。本文档详细说明如何在移动端添加新页面。
1.1 项目结构
view/uniapp/
├── pages/ # 主包页面
│ ├── index/ # 首页
│ ├── user/ # 用户中心
│ ├── goods_cate/ # 商品分类
│ └── ...
├── pages/users/ # 用户相关分包
├── pages/activity/ # 活动相关分包
├── pages/order/ # 订单相关分包
├── pages/goods_details/ # 商品详情分包
├── components/ # 公共组件
├── api/ # API接口
├── store/ # Vuex状态管理
├── utils/ # 工具函数
├── config/ # 配置文件
├── static/ # 静态资源
└── pages.json # 页面配置
二、添加页面的完整流程
2.1 创建页面文件
步骤1:确定页面位置
根据功能模块选择合适的位置:
| 功能类型 | 推荐位置 | 说明 |
|---|---|---|
| 核心功能 | pages/ |
主包,首次加载 |
| 用户相关 | pages/users/ |
分包,按需加载 |
| 活动相关 | pages/activity/ |
分包 |
| 订单相关 | pages/order/ |
分包 |
| 商品相关 | pages/goods_details/ |
分包 |
步骤2:创建页面目录和文件
以添加”会员福利”页面为例:
# 在分包中创建页面目录
mkdir -p view/uniapp/pages/users/member_benefit
# 创建页面文件
touch view/uniapp/pages/users/member_benefit/index.vue
步骤3:编写页面代码
pages/users/member_benefit/index.vue:
<template>
<view class="member-benefit">
<!-- 页面头部 -->
<view class="header">
<text class="title">会员福利</text>
</view>
<!-- 福利列表 -->
<view class="benefit-list">
<view
class="benefit-item"
v-for="(item, index) in benefitList"
:key="index"
>
<image class="icon" :src="item.icon" mode="aspectFill"></image>
<view class="info">
<text class="name">{{ item.name }}</text>
<text class="desc">{{ item.description }}</text>
</view>
<view class="action">
<button
class="btn"
:class="{ disabled: item.used }"
@click="handleUse(item)"
>
{{ item.used ? '已使用' : '立即使用' }}
</button>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty" v-if="benefitList.length === 0 && !loading">
<emptyPage title="暂无福利"></emptyPage>
</view>
<!-- 加载状态 -->
<view class="loading" v-if="loading">
<Loading></Loading>
</view>
</view>
</template>
<script>
import { getMemberBenefitList, useMemberBenefit } from '@/api/user';
import emptyPage from '@/components/emptyPage.vue';
import Loading from '@/components/Loading/index.vue';
export default {
name: 'MemberBenefit',
components: {
emptyPage,
Loading
},
data() {
return {
benefitList: [],
loading: false,
page: 1,
limit: 10,
loadend: false
};
},
onLoad(options) {
// 页面加载时获取参数
console.log('页面参数:', options);
this.loadData();
},
onShow() {
// 页面显示时刷新数据
},
onPullDownRefresh() {
// 下拉刷新
this.page = 1;
this.loadend = false;
this.loadData().then(() => {
uni.stopPullDownRefresh();
});
},
onReachBottom() {
// 上拉加载更多
if (!this.loadend && !this.loading) {
this.loadMore();
}
},
methods: {
// 加载数据
async loadData() {
this.loading = true;
try {
const res = await getMemberBenefitList({
page: this.page,
limit: this.limit
});
if (res.status === 200) {
this.benefitList = res.data.list || [];
if (res.data.list.length < this.limit) {
this.loadend = true;
}
}
} catch (error) {
console.error('加载福利列表失败:', error);
uni.showToast({
title: '加载失败',
icon: 'none'
});
} finally {
this.loading = false;
}
},
// 加载更多
async loadMore() {
if (this.loadend) return;
this.page++;
this.loading = true;
try {
const res = await getMemberBenefitList({
page: this.page,
limit: this.limit
});
if (res.status === 200) {
this.benefitList = [...this.benefitList, ...res.data.list];
if (res.data.list.length < this.limit) {
this.loadend = true;
}
}
} catch (error) {
this.page--;
console.error('加载更多失败:', error);
} finally {
this.loading = false;
}
},
// 使用福利
async handleUse(item) {
if (item.used) return;
uni.showLoading({ title: '处理中...' });
try {
const res = await useMemberBenefit(item.id);
if (res.status === 200) {
uni.showToast({
title: '使用成功',
icon: 'success'
});
// 刷新列表
this.page = 1;
this.loadData();
}
} catch (error) {
uni.showToast({
title: error.msg || '操作失败',
icon: 'none'
});
} finally {
uni.hideLoading();
}
},
// 跳转页面
goToPage(url) {
uni.navigateTo({ url });
}
}
};
</script>
<style lang="scss" scoped>
.member-benefit {
min-height: 100vh;
background-color: #f5f5f5;
padding: 20rpx;
.header {
padding: 30rpx;
background: linear-gradient(135deg, #ff6b6b, #ee5a24);
border-radius: 20rpx;
margin-bottom: 20rpx;
.title {
font-size: 36rpx;
color: #fff;
font-weight: bold;
}
}
.benefit-list {
.benefit-item {
display: flex;
align-items: center;
padding: 30rpx;
background: #fff;
border-radius: 16rpx;
margin-bottom: 20rpx;
.icon {
width: 100rpx;
height: 100rpx;
border-radius: 12rpx;
margin-right: 24rpx;
}
.info {
flex: 1;
.name {
font-size: 32rpx;
color: #333;
font-weight: 500;
display: block;
margin-bottom: 8rpx;
}
.desc {
font-size: 26rpx;
color: #999;
}
}
.action {
.btn {
font-size: 26rpx;
padding: 12rpx 30rpx;
background: #ff6b6b;
color: #fff;
border-radius: 30rpx;
border: none;
&.disabled {
background: #ccc;
}
}
}
}
}
.empty, .loading {
padding: 100rpx 0;
text-align: center;
}
}
</style>
2.2 配置页面路由
在 pages.json 中注册页面:
{
"pages": [
// ... 其他主包页面
],
"subPackages": [
{
"root": "pages/users",
"name": "users",
"pages": [
// ... 其他页面
// 新增页面配置
{
"path": "member_benefit/index",
"style": {
"navigationBarTitleText": "会员福利",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"enablePullDownRefresh": true,
"app-plus": {
"titleNView": {
"type": "default"
}
}
}
}
]
}
]
}
2.3 创建API接口
在 api/user.js 中添加接口:
import request from '@/utils/request';
/**
* 获取会员福利列表
* @param {Object} params - 查询参数
* @param {number} params.page - 页码
* @param {number} params.limit - 每页数量
*/
export function getMemberBenefitList(params) {
return request.get('/api/user/member/benefit/list', params);
}
/**
* 使用会员福利
* @param {number} id - 福利ID
*/
export function useMemberBenefit(id) {
return request.post(`/api/user/member/benefit/use/${id}`);
}
三、页面配置详解
3.1 pages.json 配置项
{
"path": "member_benefit/index",
"style": {
// 导航栏标题
"navigationBarTitleText": "会员福利",
// 导航栏背景色
"navigationBarBackgroundColor": "#ffffff",
// 导航栏标题颜色 black/white
"navigationBarTextStyle": "black",
// 自定义导航栏(隐藏原生导航)
"navigationStyle": "custom",
// 开启下拉刷新
"enablePullDownRefresh": true,
// 页面背景色
"backgroundColor": "#f5f5f5",
// 禁止页面滚动
"disableScroll": false,
// App特有配置
"app-plus": {
"titleNView": {
"type": "default",
// 标题对齐方式 auto/left/center
"titleAlign": "center",
// 自定义按钮
"buttons": [
{
"type": "share"
}
]
},
// 禁止页面回弹
"bounce": "none",
// 软键盘模式
"softinputMode": "adjustResize"
},
// 微信小程序特有配置
"mp-weixin": {
"navigationBarTitleText": "会员福利"
}
}
}
3.2 分包配置
{
"subPackages": [
{
// 分包根目录
"root": "pages/users",
// 分包别名(用于分包预加载)
"name": "users",
// 分包页面
"pages": [
{
"path": "member_benefit/index",
"style": {}
}
]
}
],
// 分包预加载配置
"preloadRule": {
"pages/user/index": {
// 进入用户中心时预加载users分包
"network": "all",
"packages": ["users"]
}
}
}
四、页面生命周期
4.1 常用生命周期钩子
export default {
// 页面加载,参数为页面传递的数据
onLoad(options) {
console.log('页面加载', options);
},
// 页面显示,每次显示都会触发
onShow() {
console.log('页面显示');
},
// 页面初次渲染完成
onReady() {
console.log('页面渲染完成');
},
// 页面隐藏
onHide() {
console.log('页面隐藏');
},
// 页面卸载
onUnload() {
console.log('页面卸载');
},
// 下拉刷新
onPullDownRefresh() {
// 处理下拉刷新
uni.stopPullDownRefresh();
},
// 上拉加载更多
onReachBottom() {
// 加载更多数据
},
// 页面滚动
onPageScroll(e) {
console.log('滚动位置', e.scrollTop);
},
// 点击导航栏分享按钮
onShareAppMessage() {
return {
title: '分享标题',
path: '/pages/users/member_benefit/index',
imageUrl: '/static/share.png'
};
},
// 分享到朋友圈
onShareTimeline() {
return {
title: '分享标题',
query: 'id=1'
};
},
// 点击TabBar
onTabItemTap(item) {
console.log('点击TabBar', item);
}
};
4.2 组件生命周期
export default {
// Vue标准生命周期
beforeCreate() {},
created() {},
beforeMount() {},
mounted() {},
beforeUpdate() {},
updated() {},
beforeDestroy() {},
destroyed() {}
};
五、页面导航
5.1 导航方法
// 保留当前页面,跳转到新页面(可返回)
uni.navigateTo({
url: '/pages/users/member_benefit/index?id=123&type=vip'
});
// 关闭当前页面,跳转到新页面(不可返回)
uni.redirectTo({
url: '/pages/users/member_benefit/index'
});
// 关闭所有页面,打开到新页面
uni.reLaunch({
url: '/pages/index/index'
});
// 跳转到TabBar页面
uni.switchTab({
url: '/pages/user/index'
});
// 返回上一页
uni.navigateBack({
delta: 1 // 返回的页面数
});
5.2 页面间传参
发送参数:
// URL参数
uni.navigateTo({
url: `/pages/users/member_benefit/index?id=${id}&name=${encodeURIComponent(name)}`
});
// 事件通道(复杂数据)
uni.navigateTo({
url: '/pages/users/member_benefit/index',
success: function(res) {
res.eventChannel.emit('acceptDataFromOpener', {
data: complexData
});
}
});
接收参数:
export default {
onLoad(options) {
// URL参数
const id = options.id;
const name = decodeURIComponent(options.name);
// 事件通道数据
const eventChannel = this.getOpenerEventChannel();
eventChannel.on('acceptDataFromOpener', (data) => {
console.log('接收到的数据:', data);
});
}
};
5.3 返回传参
// 子页面
const pages = getCurrentPages();
const prevPage = pages[pages.length - 2];
prevPage.$vm.receiveData = '返回的数据';
uni.navigateBack();
// 父页面
export default {
data() {
return {
receiveData: ''
};
},
onShow() {
if (this.receiveData) {
console.log('收到返回数据:', this.receiveData);
}
}
};
六、自定义导航栏
6.1 配置自定义导航
{
"path": "member_benefit/index",
"style": {
"navigationStyle": "custom"
}
}
6.2 自定义导航栏组件
<template>
<view class="custom-nav" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-content" :style="{ height: navBarHeight + 'px' }">
<view class="nav-left" @click="handleBack">
<image class="back-icon" src="/static/images/back.png"></image>
</view>
<view class="nav-title">
<text>{{ title }}</text>
</view>
<view class="nav-right">
<slot name="right"></slot>
</view>
</view>
</view>
<!-- 占位元素 -->
<view :style="{ height: (statusBarHeight + navBarHeight) + 'px' }"></view>
</template>
<script>
export default {
name: 'CustomNav',
props: {
title: {
type: String,
default: ''
},
showBack: {
type: Boolean,
default: true
}
},
data() {
return {
statusBarHeight: 0,
navBarHeight: 44
};
},
created() {
const systemInfo = uni.getSystemInfoSync();
this.statusBarHeight = systemInfo.statusBarHeight;
// #ifdef MP-WEIXIN
// 小程序需要额外计算胶囊位置
const menuButton = uni.getMenuButtonBoundingClientRect();
this.navBarHeight = (menuButton.top - this.statusBarHeight) * 2 + menuButton.height;
// #endif
},
methods: {
handleBack() {
const pages = getCurrentPages();
if (pages.length > 1) {
uni.navigateBack();
} else {
uni.switchTab({ url: '/pages/index/index' });
}
}
}
};
</script>
<style lang="scss" scoped>
.custom-nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 999;
background: #fff;
.nav-content {
display: flex;
align-items: center;
padding: 0 20rpx;
.nav-left {
width: 80rpx;
.back-icon {
width: 40rpx;
height: 40rpx;
}
}
.nav-title {
flex: 1;
text-align: center;
font-size: 32rpx;
font-weight: 500;
}
.nav-right {
width: 80rpx;
text-align: right;
}
}
}
</style>
七、登录验证
7.1 路由守卫实现
// utils/auth.js
import { LOGIN_STATUS } from '@/config/cache';
import Cache from '@/utils/cache';
/**
* 需要登录的页面
*/
const needLoginPages = [
'/pages/users/member_benefit/index',
'/pages/users/user_money/index',
'/pages/order/order_list/index'
];
/**
* 检查是否需要登录
*/
export function checkLogin(url) {
const isLogin = Cache.get(LOGIN_STATUS);
const needLogin = needLoginPages.some(page => url.includes(page));
if (needLogin && !isLogin) {
uni.navigateTo({
url: `/pages/users/wechat_login/index?redirect=${encodeURIComponent(url)}`
});
return false;
}
return true;
}
/**
* 跳转页面(带登录检查)
*/
export function navigateTo(url) {
if (checkLogin(url)) {
uni.navigateTo({ url });
}
}
7.2 页面中使用
import { navigateTo } from '@/utils/auth';
export default {
methods: {
goToMemberBenefit() {
navigateTo('/pages/users/member_benefit/index');
}
}
};
八、条件编译
8.1 模板条件编译
<template>
<view>
<!-- #ifdef MP-WEIXIN -->
<button open-type="share">分享</button>
<!-- #endif -->
<!-- #ifdef H5 -->
<button @click="h5Share">分享</button>
<!-- #endif -->
<!-- #ifdef APP-PLUS -->
<button @click="appShare">分享</button>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view>非微信小程序显示</view>
<!-- #endif -->
</view>
</template>
8.2 JS条件编译
export default {
methods: {
share() {
// #ifdef MP-WEIXIN
// 微信小程序分享
// #endif
// #ifdef H5
// H5分享
this.h5Share();
// #endif
// #ifdef APP-PLUS
// APP分享
this.appShare();
// #endif
}
}
};
8.3 CSS条件编译
.container {
/* #ifdef MP-WEIXIN */
padding-bottom: env(safe-area-inset-bottom);
/* #endif */
/* #ifdef H5 */
padding-bottom: 100rpx;
/* #endif */
}
九、最佳实践
9.1 页面模板
<template>
<view class="page-container">
<!-- 自定义导航栏 -->
<CustomNav title="页面标题" />
<!-- 页面内容 -->
<scroll-view
class="content"
scroll-y
@scrolltolower="loadMore"
:refresher-enabled="true"
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
>
<!-- 列表内容 -->
<view class="list">
<view
class="list-item"
v-for="item in list"
:key="item.id"
>
{{ item.name }}
</view>
</view>
<!-- 加载状态 -->
<view class="load-status">
<text v-if="loading">加载中...</text>
<text v-else-if="loadend">没有更多了</text>
</view>
</scroll-view>
<!-- 底部操作栏 -->
<view class="footer safe-area-bottom">
<button class="btn-primary" @click="handleSubmit">提交</button>
</view>
</view>
</template>
<script>
export default {
data() {
return {
list: [],
page: 1,
limit: 10,
loading: false,
loadend: false,
refreshing: false
};
},
onLoad() {
this.loadData();
},
methods: {
async loadData(isRefresh = false) {
if (this.loading) return;
this.loading = true;
try {
const res = await api.getList({
page: this.page,
limit: this.limit
});
if (isRefresh) {
this.list = res.data.list;
} else {
this.list = [...this.list, ...res.data.list];
}
this.loadend = res.data.list.length < this.limit;
} finally {
this.loading = false;
this.refreshing = false;
}
},
onRefresh() {
this.refreshing = true;
this.page = 1;
this.loadend = false;
this.loadData(true);
},
loadMore() {
if (this.loadend || this.loading) return;
this.page++;
this.loadData();
}
}
};
</script>
<style lang="scss" scoped>
.page-container {
display: flex;
flex-direction: column;
min-height: 100vh;
background: #f5f5f5;
}
.content {
flex: 1;
overflow: hidden;
}
.footer {
padding: 20rpx 30rpx;
background: #fff;
&.safe-area-bottom {
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
}
}
</style>
9.2 目录命名规范
pages/users/
├── member_benefit/ # 使用下划线分隔
│ ├── index.vue # 主页面
│ ├── detail.vue # 详情页
│ └── components/ # 页面私有组件
│ └── BenefitCard.vue
9.3 页面开发检查清单
- [ ] 创建页面文件 (.vue)
- [ ] 配置 pages.json
- [ ] 创建 API 接口
- [ ] 实现页面逻辑
- [ ] 添加下拉刷新/上拉加载
- [ ] 处理空状态
- [ ] 处理加载状态
- [ ] 处理错误状态
- [ ] 适配多端(条件编译)
- [ ] 测试各端表现
注意事项
- 分包策略:非首屏必需的页面应放在分包中,减小主包体积,提升首屏加载速度
- pages.json 配置:新增页面必须在
pages.json中配置,否则页面无法访问 - 路径大小写:页面路径在不同平台对大小写敏感度不同,建议统一使用小写和下划线
- 页面生命周期:
onLoad只在页面创建时执行一次,onShow每次显示都会执行,注意选择合适的生命周期 - 组件引用:页面中使用的组件需要在
components中注册,或在pages.json中配置全局组件 - 样式单位:使用
rpx作为尺寸单位,确保多端显示一致性 - 条件编译:不同平台的差异化代码使用
#ifdef和#ifndef条件编译处理 - 导航栏配置:自定义导航栏需要设置
navigationStyle: 'custom',并处理状态栏高度适配
常见问题
Q: 新增页面后跳转提示”页面不存在”?
A: 检查pages.json是否已配置该页面路径,路径格式是否正确(不带开头斜杠和 .vue 后缀)Q: 页面样式在小程序和 H5 表现不一致?
A: uni-app 对部分 CSS 属性支持有差异,使用条件编译针对不同平台编写样式,或使用兼容性更好的写法Q: 分包页面如何跳转到主包页面?
A: 使用uni.switchTab跳转 tabBar 页面,使用uni.navigateTo或uni.redirectTo跳转普通页面Q: 页面传参过长或包含特殊字符怎么办?
A: 复杂数据建议使用 Vuex 存储或本地缓存传递,URL 参数需要使用encodeURIComponent编码Q: 下拉刷新不生效?
A: 检查pages.json中该页面是否配置了enablePullDownRefresh: trueQ: 如何获取页面来源(从哪个页面跳转来的)?
A: 使用getCurrentPages()获取页面栈,通过pages[pages.length - 2]获取上一个页面信息
评论({{cateWiki.comment_num}})
最新
最早
{{cateWiki.page_view_num}}人看过该文档
评论(0)
最新
最早
164人看过
登录/注册
即可发表评论
{{item.user ? item.user.nickname : ''}}
(自评)
{{item.content}}
搜索结果
为您找到{{wikiCount}}条结果
{{item.page_view_num}}
{{item.like ? item.like.like_num : 0}}
{{item.comment ? item.comment.comment_num : 0}}
位置:
{{path.name}}
{{(i+1) == getCataloguePathData(item.catalogue).length ? '':'/'}}
