在移动应用开发中,个人中心页面是用户管理个人信息和操作的核心入口。本文将详细解析一个基于UniApp框架开发的个人中心页面,从功能架构到技术实现,带您了解如何构建一个功能完善、体验流畅的用户中心。
该个人中心页面采用了清晰的分层设计,主要包含以下几个部分:
页面使用了Vue 3的组合式API进行开发,结合了UniApp的跨平台能力,能够同时运行在微信小程序、H5等多个平台。整体设计风格简洁明了,色彩搭配和谐,交互逻辑清晰。
页面通过token
变量管理用户登录状态,实现了登录状态的持久化存储与验证:
javascriptconst token = ref('')
const nickname = ref('')
const avatar = ref('')
const authPopshow = ref(false)
const memberInfo = ref({
realName: '',
nickname: '',
avatar: '',
memberType: '',
password: '',
})
当页面显示时,会自动检查用户登录状态:
javascriptonShow(() => {
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>
每个功能入口都做了登录状态检查,未登录时会引导用户进行登录:
javascriptconst toApply = () => {
if (!token.value) {
memberLogin()
return
}
uni.navigateTo({
url: '/pages/mine/component/apply',
})
}
页面使用计算属性实现了昵称输入的自动清理,过滤掉非法字符:
javascriptconst cleanedNickname = computed({
get: () => nickname.value,
set: (val) => {
nickname.value = val.replace(/[\x00-\x1F\x7F\u200B-\u200F\u2028-\u202F]/g, '')
}
})
完整实现了微信授权登录流程,包括获取code、登录接口调用、用户信息获取和更新:
javascriptconst 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()
})
})
})
}
})
}
页面使用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>
这个UniApp个人中心页面实现了完整的用户管理功能,包括登录状态管理、用户信息展示与编辑、多功能导航等。通过合理的代码结构和清晰的逻辑设计,保证了页面的可维护性和可扩展性。
UniApp框架的使用使得该页面能够轻松适配多个平台,而Vue 3的组合式API则提供了更清晰的代码组织方式。在实际开发中,我们可以根据具体需求对该页面进行进一步优化和扩展,以提供更好的用户体验。
通过这个案例,我们可以看到一个功能完善的个人中心页面是如何从架构设计到代码实现一步步构建起来的,这对于移动应用开发具有很好的参考价值。