2025-06-20
Vue
0

目录

UniApp个人中心页面开发实践:功能详解与实现分析
页面整体架构概述
核心功能模块详解
用户状态管理模块
用户信息展示与编辑
功能菜单导航
技术实现亮点
数据清理与验证
微信授权流程处理
响应式UI设计
完整代码
效果展示
优化建议
性能优化
交互优化
功能扩展
总结

UniApp个人中心页面开发实践:功能详解与实现分析

在移动应用开发中,个人中心页面是用户管理个人信息和操作的核心入口。本文将详细解析一个基于UniApp框架开发的个人中心页面,从功能架构到技术实现,带您了解如何构建一个功能完善、体验流畅的用户中心。

页面整体架构概述

该个人中心页面采用了清晰的分层设计,主要包含以下几个部分:

  1. 顶部用户信息展示区
  2. 功能菜单导航区
  3. 授权弹窗交互区

页面使用了Vue 3的组合式API进行开发,结合了UniApp的跨平台能力,能够同时运行在微信小程序、H5等多个平台。整体设计风格简洁明了,色彩搭配和谐,交互逻辑清晰。

核心功能模块详解

用户状态管理模块

页面通过token变量管理用户登录状态,实现了登录状态的持久化存储与验证:

javascript
const token = ref('') const nickname = ref('') const avatar = ref('') const authPopshow = ref(false) const memberInfo = ref({ realName: '', nickname: '', avatar: '', memberType: '', password: '', })

当页面显示时,会自动检查用户登录状态:

javascript
onShow(() => { init() uni.$on('memberLogin', () => { console.log('监听需要登录指令') memberLogin() }) }) const init = () => { token.value = uni.getStorageSync('token') if (token.value) { uni.checkSession({ success: () => { getInfo().then(res => { memberInfo.value = res.data avatar.value = res.data.avatar nickname.value = res.data.nickname uni.setStorageSync('memberInfo', res.data) }).catch(() => { clearInitData() }) }, fail: () => { toLogout() uni.$u.toast('您的登录信息已过期,请重新登录') memberLogin() } }) } else { clearInitData() } }

用户信息展示与编辑

页面顶部展示了用户头像、昵称和会员类型信息,未登录时显示"注册/登录"按钮:

html
<view class="top-content"> <image v-if="token" src="@/static/images/logout.png" class="logout-bt" @click="toLogout" /> <button open-type="chooseAvatar" :disabled="true" @chooseavatar="getChooseAvatar" class="button-reset avatar-btn"> <image :src="(token && avatar) || '../../static/images/default-avatar.png'" class="avatar-img" mode="aspectFit" /> </button> <view class="member-nickname"> <view v-if="token"> {{ nickname }} <up-tag v-if="memberInfo.memberType && memberInfo.memberType !== 0" plain borderColor="transparent" :text="memberInfo.memberType === 1 ? '代理' : 'CRA'" :type="memberInfo.memberType === 1 ? 'warning' : 'success'" /> </view> <view v-else @click="memberLogin">注册 / 登录</view> </view> </view>

用户信息编辑通过授权弹窗实现,包含头像选择、昵称输入和隐私协议同意功能:

html
<up-popup round="20" :show="authPopshow"> <view class="auth-container"> <view class="form-item"> <text class="form-label">选择头像:</text> <view class="form-label-content"> <button open-type="chooseAvatar" @chooseavatar="getChooseAvatar" class="button-reset avatar-btn"> <image :src="avatar || '../../static/images/default-avatar.png'" class="avatar-img" mode="aspectFit" /> </button> </view> </view> <view class="form-item"> <text class="form-label">填写昵称:</text> <up-input v-model="cleanedNickname" type="nickname" border="none" inputAlign="right" class="nickname-input" placeholder="请输入昵称" @change="changeNickname" /> </view> <view class="privacy-area"> <up-radio-group v-model="aggressPrivacyAgreement" @change="agreeOrdisagree"> <up-radio shape="square" :name="true" label="点击同意" @change="changeRadio" /> </up-radio-group> <button class="button-reset link-button" @click="handleOpenPrivacyContract">《用户隐私保护协议》</button> </view> <!-- 更多弹窗内容... --> </view> </up-popup>

功能菜单导航

页面提供了丰富的功能入口,包括密码设置、我的报名、BMI计算器等:

html
<up-cell-group> <up-cell v-if="token" :isLink="true" size="large" icon="lock" @click="toPassword"> <template #title> <view> <text>设置密码</text> <up-text v-if="token && !memberInfo.password" type="error" text="您还没有设置密码,请设置密码" /> </view> </template> </up-cell> <up-cell title="我的报名" :isLink="true" size="large" icon="order" @click="toApply" /> <up-cell v-if="token && memberInfo.memberType === 2" title="我的项目" :isLink="true" size="large" icon="order" @click="toProject" /> <up-cell title="BMI计算器" :isLink="true" size="large" icon="edit-pen" @click="toBmi" /> <up-cell title="申请代理" :isLink="true" size="large" icon="account" @click="toAgent" /> <up-cell title="我的佣金" :isLink="true" size="large" icon="rmb" @click="toWithdraw" /> <up-cell title="团队成员" :isLink="true" size="large" icon="file-text" @click="toMember" /> </up-cell-group>

每个功能入口都做了登录状态检查,未登录时会引导用户进行登录:

javascript
const toApply = () => { if (!token.value) { memberLogin() return } uni.navigateTo({ url: '/pages/mine/component/apply', }) }

技术实现亮点

数据清理与验证

页面使用计算属性实现了昵称输入的自动清理,过滤掉非法字符:

javascript
const cleanedNickname = computed({ get: () => nickname.value, set: (val) => { nickname.value = val.replace(/[\x00-\x1F\x7F\u200B-\u200F\u2028-\u202F]/g, '') } })

微信授权流程处理

完整实现了微信授权登录流程,包括获取code、登录接口调用、用户信息获取和更新:

javascript
const confirmLogin = () => { if (!avatar.value) { uni.$u.toast('请选择头像') return } if (!nickname.value) { uni.$u.toast('请选择或输入昵称') return } if (!aggressPrivacyAgreement.value) { uni.$u.toast('必须阅读并同意《用户隐私保护协议》') return } uni.showLoading({ title: '登录中,请稍后' }) nickname.value.replace(/[\x00-\x1F\x7F\u200B-\u200F\u2028-\u202F]/g, '') uni.login({ success: response => { login({ code: response.code }).then(res => { // 获取并缓存token token.value = res.data.tokenValue uni.setStorageSync('token', res.data.tokenValue) uni.setStorageSync('openId', res.data.tag) // 获取成员信息 getInfo().then(memberRes => { // 处理分享关系 const shareMemberId = uni.getStorageSync('shareMemberId') if (shareMemberId && !memberInfo.value.parentId && memberInfo.value.parentId !== shareMemberId) { memberInfo.value.parentId = shareMemberId } // 更新成员信息 memberInfo.value.avatar = avatar.value memberInfo.value.nickname = nickname.value update(memberInfo.value).then(() => { authPopshow.value = false uni.$u.toast('欢迎登录!') uni.hideLoading() }) }) }) } }) }

响应式UI设计

页面使用rpx作为单位,实现了在不同尺寸屏幕上的自适应显示:

css
.avatar-btn { display: flex; align-items: center; justify-content: center; width: 150rpx; height: 150rpx; padding: 0; border-radius: 75rpx; } .avatar-img { width: 150rpx; height: 150rpx; border-radius: 75rpx; }

完整代码

html
<template> <view class="background-image"> <view class="content"> <view class="top-content"> <image v-if="token" src="@/static/images/logout.png" class="logout-bt" @click="toLogout" /> <button open-type="chooseAvatar" :disabled="true" @chooseavatar="getChooseAvatar" class="button-reset avatar-btn"> <image :src="(token && avatar) || '../../static/images/default-avatar.png'" class="avatar-img" mode="aspectFit" /> </button> <view class="member-nickname"> <view v-if="token"> {{ nickname }} <up-tag v-if="memberInfo.memberType && memberInfo.memberType !== 0" plain borderColor="transparent" :text="memberInfo.memberType === 1 ? '代理' : 'CRA'" :type="memberInfo.memberType === 1 ? 'warning' : 'success'" /> </view> <view v-else @click="memberLogin">注册 / 登录</view> </view> </view> <view style="padding-top: 60rpx"> <up-cell-group> <up-cell v-if="token" :isLink="true" size="large" icon="lock" @click="toPassword"> <template #title> <view> <text>设置密码</text> <up-text v-if="token && !memberInfo.password" type="error" text="您还没有设置密码,请设置密码" /> </view> </template> </up-cell> <up-cell title="我的报名" :isLink="true" size="large" icon="order" @click="toApply" /> <up-cell v-if="token && memberInfo.memberType === 2" title="我的项目" :isLink="true" size="large" icon="order" @click="toProject" /> <up-cell title="BMI计算器" :isLink="true" size="large" icon="edit-pen" @click="toBmi" /> <up-cell title="申请代理" :isLink="true" size="large" icon="account" @click="toAgent" /> <up-cell title="我的佣金" :isLink="true" size="large" icon="rmb" @click="toWithdraw" /> <up-cell title="团队成员" :isLink="true" size="large" icon="file-text" @click="toMember" /> </up-cell-group> </view> <up-popup round="20" :show="authPopshow"> <view class="auth-container"> <view class="form-item"> <text class="form-label">选择头像:</text> <view class="form-label-content"> <button open-type="chooseAvatar" @chooseavatar="getChooseAvatar" class="button-reset avatar-btn"> <image :src="avatar || '../../static/images/default-avatar.png'" class="avatar-img" mode="aspectFit" /> </button> </view> </view> <view class="form-item"> <text class="form-label">填写昵称:</text> <up-input v-model="cleanedNickname" type="nickname" border="none" inputAlign="right" class="nickname-input" placeholder="请输入昵称" @change="changeNickname" /> </view> <view class="privacy-area"> <up-radio-group v-model="aggressPrivacyAgreement" @change="agreeOrdisagree"> <up-radio shape="square" :name="true" label="点击同意" @change="changeRadio" /> </up-radio-group> <button class="button-reset link-button" @click="handleOpenPrivacyContract">《用户隐私保护协议》</button> </view> <up-line class="form-divider" /> <view class="form-instructions"> <text>说明:</text> <view class="instruction-list"> <text class="instruction-item">1、点击头像选择框可以选择使用"使用微信头像";</text> <text class="instruction-item">2、点击昵称输入框后可以选择使用"使用微信昵称";</text> <text class="instruction-item">3、平台不会将您的信息泄露给第三方平台。</text> </view> </view> <view class="button-group"> <up-button shape="circle" @click="toCancelAuth" :customStyle="{width: '200rpx;','background-color': '#DDDDDD;', color: '#fff'}">取消</up-button> <up-button shape="circle" @click="confirmLogin" :customStyle="{width: '350rpx;','background-color': '#FB7052;', color: '#fff'}">立即登录</up-button> </view> </view> </up-popup> </view> </view> </template> <script setup> import { computed, ref } from 'vue' import { onShow } from '@dcloudio/uni-app' import { login, logout } from '@/api/auth' import { getInfo, update } from '@/api/member' const token = ref('') const nickname = ref('') const avatar = ref('') const authPopshow = ref(false) // 同意隐私协议授权 const radioValue = ref('') const num = ref(0) const aggressPrivacyAgreement = ref(false) const memberInfo = ref({ realName: '', nickname: '', avatar: '', memberType: '', password: '', }) // 计算属性自动清理值 const cleanedNickname = computed({ get: () => nickname.value, set: (val) => { nickname.value = val.replace(/[\x00-\x1F\x7F\u200B-\u200F\u2028-\u202F]/g, '') } }) onShow(() => { init() uni.$on('memberLogin', () => { console.log('监听需要登录指令') memberLogin() }) }) const init = () => { token.value = uni.getStorageSync('token') if (token.value) { uni.checkSession({ success: () => { getInfo().then(res => { memberInfo.value = res.data avatar.value = res.data.avatar nickname.value = res.data.nickname uni.setStorageSync('memberInfo', res.data) }).catch(() => { clearInitData() }) }, fail: () => { toLogout() uni.$u.toast('您的登录信息已过期,请重新登录') memberLogin() } }) } else { clearInitData() } } const memberLogin = () => { clearInitData(true) } // 选择头像 const getChooseAvatar = (e) => { avatar.value = e.detail.avatarUrl } // 选择/修改昵称 const changeNickname = (value) => { nickname.value = value.replace(/[\x00-\x1F\x7F\u200B-\u200F\u2028-\u202F]/g, '') } const changeRadio = (option) => { radioValue.value = option } // 同意/不同意用户隐私保护协议 const agreeOrdisagree = (option) => { if (option === radioValue.value && num.value === 0) { aggressPrivacyAgreement.value = true // 第一次相等即执行以下代码 num.value = 1 } else { // 第一次后相等即执行以下代码 // 置空 radioGroupValue 即取消选中的值 aggressPrivacyAgreement.value = false // 初始化 num num.value = 0 } } const handleOpenPrivacyContract = () => { // 打开隐私协议页面 wx.openPrivacyContract() } const confirmLogin = () => { if (!avatar.value) { uni.$u.toast('请选择头像') return } if (!nickname.value) { uni.$u.toast('请选择或输入昵称') return } if (!aggressPrivacyAgreement.value) { uni.$u.toast('必须阅读并同意《用户隐私保护协议》') return } uni.showLoading({ title: '登录中,请稍后' }) nickname.value.replace(/[\x00-\x1F\x7F\u200B-\u200F\u2028-\u202F]/g, '') uni.login({ success: response => { login({ code: response.code }).then(res => { // 获取并缓存token token.value = res.data.tokenValue uni.setStorageSync('token', res.data.tokenValue) uni.setStorageSync('openId', res.data.tag) // 获取成员信息 getInfo().then(memberRes => { // 获取并缓存成员信息 memberInfo.value = memberRes.data uni.setStorageSync('memberInfo', memberRes.data) // 关联分享人的id const shareMemberId = uni.getStorageSync('shareMemberId') // 有分享人、自己没有父级成员、不是自己分享给自己 if (shareMemberId && !memberInfo.value.parentId && memberInfo.value.parentId !== shareMemberId) { memberInfo.value.parentId = shareMemberId } // 更新成员 memberInfo.value.avatar = avatar.value memberInfo.value.nickname = nickname.value update(memberInfo.value).then(() => { authPopshow.value = false uni.$u.toast('欢迎登录!') uni.hideLoading() }) }) }) } }) } // 取消授权 const toCancelAuth = () => { clearInitData() } const toLogout = () => { clearInitData() const openId = uni.getStorageSync('openId') || '0' logout(openId).then(() => { console.log('成功退出') }).catch(err => { console.log('退出异常', err) }) const shareMemberId = uni.getStorageSync('shareMemberId') uni.clearStorageSync() if (shareMemberId) { uni.setStorageSync('shareMemberId', shareMemberId) } } const clearInitData = (authPopshowValue = false) => { token.value = '' avatar.value = '' nickname.value = '' memberInfo.value.password = '' memberInfo.value.memberType = '' authPopshow.value = authPopshowValue radioValue.value = '' num.value = 0 aggressPrivacyAgreement.value = false } // 设置密码 const toPassword = () => { uni.navigateTo({ url: '/pages/mine/component/password', }) } // 我的报名 const toApply = () => { if (!token.value) { memberLogin() return } uni.navigateTo({ url: '/pages/mine/component/apply', }) } // 我的项目 const toProject = () => { uni.navigateTo({ url: '/pages/mine/component/project', }) } // BMI计算器 const toBmi = () => { uni.navigateTo({ url: '/pages/mine/component/bmi', }) } // 申请代理 const toAgent = () => { if (!token.value) { memberLogin() return } uni.navigateTo({ url: '/pages/mine/component/agent', }) } // 我的佣金 const toWithdraw = () => { if (!token.value) { memberLogin() return } uni.navigateTo({ url: '/pages/mine/component/withdraw', }) } // 团队成员 const toMember = () => { if (!token.value) { memberLogin() return } uni.navigateTo({ url: '/pages/mine/component/member', }) } </script> <style lang="scss" scoped> .background-image { background-color: #f0f8ff; background-size: cover; background-position: center; background-repeat: no-repeat; width: 100%; height: 100vh; } .content { padding: 20rpx; } .top-content { display: flex; margin: 20rpx; justify-content: flex-start; } .member-nickname { font-size: 40rpx; font-weight: bolder; padding-top: 50rpx; text-align: center; cursor: pointer; margin-left: 80rpx; } .logout-bt { position: absolute; top: 50rpx; right: 50rpx; height: 50rpx; width: 50rpx; } .auth-container { padding: 35rpx; } .form-item { display: flex; align-items: center; padding: 40rpx 0; } .form-label { width: 250px; color: #333; font-size: 28rpx; } .form-label-content { margin-left: 60%; } .avatar-btn { display: flex; align-items: center; justify-content: center; width: 150rpx; height: 150rpx; padding: 0; border-radius: 75rpx; } .avatar-img { width: 150rpx; height: 150rpx; border-radius: 75rpx; } .nickname-input { flex: 1; margin-left: 20rpx; } .form-divider { margin: 20rpx 0; } .form-instructions { padding: 20rpx 0; } .instruction-list { margin-left: 70rpx; margin-top: 10rpx; } .instruction-item { display: block; font-size: 26rpx; color: #666; line-height: 1.6; } .privacy-area { display: flex; align-items: center; padding: 20rpx 0; } .link-button { background-color: #FFFFFF; color: #004499; font-size: 28rpx; } .button-group { display: flex; justify-content: center; margin-top: 40rpx; } .button-reset { margin: 0; padding: 0; border: 0; &::before, &::after { display: none !important; border: none !important; } } </style>

效果展示

image.png

image.png

优化建议

性能优化

  1. 图片懒加载:对于头像等图片资源,添加懒加载机制,提高页面加载速度
  2. 组件缓存:对于频繁访问的功能页面,启用组件缓存
  3. 数据批量更新:合并多次数据更新操作,减少API调用次数

交互优化

  1. 添加加载状态反馈:在数据请求过程中显示加载动画
  2. 优化表单验证:提供实时输入验证和反馈
  3. 增加操作反馈:对于重要操作如退出登录,添加二次确认

功能扩展

  1. 增加消息通知中心
  2. 添加设置页面,允许用户自定义更多选项
  3. 实现社交分享功能,扩大应用传播

总结

这个UniApp个人中心页面实现了完整的用户管理功能,包括登录状态管理、用户信息展示与编辑、多功能导航等。通过合理的代码结构和清晰的逻辑设计,保证了页面的可维护性和可扩展性。

UniApp框架的使用使得该页面能够轻松适配多个平台,而Vue 3的组合式API则提供了更清晰的代码组织方式。在实际开发中,我们可以根据具体需求对该页面进行进一步优化和扩展,以提供更好的用户体验。

通过这个案例,我们可以看到一个功能完善的个人中心页面是如何从架构设计到代码实现一步步构建起来的,这对于移动应用开发具有很好的参考价值。