效果预览
相关代码
页面–我的
src\pages\my\my.vue
<!-- 个人资料 --> <view class="profile" :style="{ paddingTop: safeAreaInsets!.top + 'px' }"> <!-- 情况1:已登录 --> <view class="overview" v-if="memberStore.profile"> <navigator url="/pagesMember/profile/profile" hover-class="none"> <image class="avatar" mode="aspectFill" :src="memberStore.profile.avatar"></image> </navigator> <view class="meta"> <view class="nickname"> {{ memberStore.profile.nickname || memberStore.profile.account }} </view> <navigator class="extra" url="/pagesMember/profile/profile" hover-class="none"> <text class="update">更新头像昵称</text> </navigator> </view> </view> <!-- 情况2:未登录 --> <view class="overview" v-else> <navigator url="/pages/login/login" hover-class="none"> <image class="avatar gray" mode="aspectFill" src="http://yjy-xiaotuxian-dev.oss-cn-beijing.aliyuncs.com/picture/2021-04-06/db628d42-88a7-46e7-abb8-659448c33081.png" ></image> </navigator> <view class="meta"> <navigator url="/pages/login/login" hover-class="none" class="nickname"> 未登录 </navigator> <view class="extra"> <text class="tips">点击登录账号</text> </view> </view> </view> <navigator class="settings" url="/pagesMember/settings/settings" hover-class="none"> 设置 </navigator> </view>
/* 用户信息 */ .profile { margin-top: 20rpx; position: relative; .overview { display: flex; height: 120rpx; padding: 0 36rpx; color: #fff; } .avatar { width: 120rpx; height: 120rpx; border-radius: 50%; background-color: #eee; } .gray { filter: grayscale(100%); } .meta { display: flex; flex-direction: column; justify-content: center; align-items: flex-start; line-height: 30rpx; padding: 16rpx 0; margin-left: 20rpx; } .nickname { max-width: 350rpx; margin-bottom: 16rpx; font-size: 30rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .extra { display: flex; font-size: 20rpx; } .tips { font-size: 22rpx; } .update { padding: 3rpx 10rpx 1rpx; color: rgba(255, 255, 255, 0.8); border: 1rpx solid rgba(255, 255, 255, 0.8); margin-right: 10rpx; border-radius: 30rpx; } .settings { position: absolute; bottom: 0; right: 40rpx; font-size: 30rpx; color: #fff; } }
// 获取屏幕边界到安全区域距离 const { safeAreaInsets } = uni.getSystemInfoSync() import { useMemberStore } from '@/stores' // 获取会员信息 const memberStore = useMemberStore()
页面–个人信息维护
src\pagesMember\profile\profile.vue
<script setup lang="ts"> import { getMemberProfileAPI, putMemberProfileAPI } from '@/apis/profile' import type { Gender, ProfileDetail } from '@/types/member' import { onLoad } from '@dcloudio/uni-app' import { ref } from 'vue' // 获取屏幕边界到安全区域距离 const { safeAreaInsets } = uni.getSystemInfoSync() // 获取个人信息,修改个人信息需提供初始值 (使用 as 进行类型断言,不用再声明类型) const profile = ref({} as ProfileDetail) const getMemberProfileData = async () => { const res = await getMemberProfileAPI() profile.value = res.result } onLoad(() => { getMemberProfileData() }) import { useMemberStore } from '@/stores' // 获取会员信息 const memberStore = useMemberStore() // 修改头像 const onAvatarChange = () => { // 调用拍照/选择图片 uni.chooseMedia({ // 文件个数 count: 1, // 文件类型 mediaType: ['image'], success: (res) => { // 本地路径 const { tempFilePath } = res.tempFiles[0] // 文件上传 uni.uploadFile({ url: '/member/profile/avatar', name: 'file', // 后端数据字段名 filePath: tempFilePath, // 新头像 success: (res) => { // 判断状态码是否上传成功 if (res.statusCode === 200) { // 提取头像 const { avatar } = JSON.parse(res.data).result // 当前页面更新头像 profile.value!.avatar = avatar // 更新 Store 头像 memberStore.profile!.avatar = avatar uni.showToast({ icon: 'success', title: '更新成功' }) } else { uni.showToast({ icon: 'error', title: '出现错误' }) } }, }) }, }) } // 修改性别 const onGenderChange: UniHelper.RadioGroupOnChange = (ev) => { profile.value.gender = ev.detail.value as Gender } // 修改生日 const onBirthdayChange: UniHelper.DatePickerOnChange = (ev) => { profile.value.birthday = ev.detail.value } // 修改城市 let fullLocationCode: [string, string, string] = ['', '', ''] const onFullLocationChange: UniHelper.RegionPickerOnChange = (ev) => { // 修改前端界面 profile.value.fullLocation = ev.detail.value.join(' ') // 提交后端更新 fullLocationCode = ev.detail.code! } // 点击保存提交表单 const onSubmit = async () => { const { nickname, gender, birthday, profession } = profile.value const res = await putMemberProfileAPI({ nickname, gender, birthday, profession, provinceCode: fullLocationCode[0], cityCode: fullLocationCode[1], countyCode: fullLocationCode[2], }) // 更新Store昵称 memberStore.profile!.nickname = res.result.nickname uni.showToast({ icon: 'success', title: '保存成功' }) setTimeout(() => { uni.navigateBack() }, 400) } </script> <template> <view class="viewport"> <!-- 导航栏 --> <view class="navbar" :style="{ paddingTop: safeAreaInsets?.top + 'px' }"> <navigator open-type="navigateBack" class="back icon-left" hover-class="none"></navigator> <view class="title">个人信息</view> </view> <!-- 头像 --> <view class="avatar"> <view class="avatar-content" @tap="onAvatarChange"> <image class="image" :src="profile?.avatar" mode="aspectFill" /> <text class="text">点击修改头像</text> </view> </view> <!-- 表单 --> <view class="form"> <!-- 表单内容 --> <view class="form-content"> <view class="form-item"> <text class="label">账号</text> <text class="account">{{ profile?.account }}</text> </view> <view class="form-item"> <text class="label">昵称</text> <input class="input" type="text" placeholder="请填写昵称" v-model="profile.nickname" /> </view> <view class="form-item"> <text class="label">性别</text> <radio-group @change="onGenderChange"> <label class="radio"> <radio value="男" color="#27ba9b" :checked="profile?.gender === '男'" /> 男 </label> <label class="radio"> <radio value="女" color="#27ba9b" :checked="profile?.gender === '女'" /> 女 </label> </radio-group> </view> <view class="form-item"> <text class="label">生日</text> <picker class="picker" mode="date" :value="profile?.birthday" start="1900-01-01" :end="new Date()" @change="onBirthdayChange" > <view v-if="profile?.birthday">{{ profile?.birthday }}</view> <view class="placeholder" v-else>请选择日期</view> </picker> </view> <view class="form-item"> <text class="label">城市</text> <picker @change="onFullLocationChange" class="picker" :value="profile?.fullLocation?.split(' ')" mode="region" > <view v-if="profile?.fullLocation">{{ profile.fullLocation }}</view> <view class="placeholder" v-else>请选择城市</view> </picker> </view> <view class="form-item"> <text class="label">职业</text> <input class="input" type="text" placeholder="请填写职业" v-model="profile.profession" /> </view> </view> <!-- 提交按钮 --> <button class="form-button" @tap="onSubmit">保 存</button> </view> </view> </template> <style lang="scss"> page { background-color: #f4f4f4; } .viewport { display: flex; flex-direction: column; height: 100%; background-image: url(https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/images/order_bg.png); background-size: auto 420rpx; background-repeat: no-repeat; } // 导航栏 .navbar { position: relative; .title { height: 40px; display: flex; justify-content: center; align-items: center; font-size: 16px; font-weight: 500; color: #fff; } .back { position: absolute; height: 40px; width: 40px; left: 0; font-size: 20px; color: #fff; display: flex; justify-content: center; align-items: center; } } // 头像 .avatar { text-align: center; width: 100%; height: 260rpx; display: flex; flex-direction: column; justify-content: center; align-items: center; .image { width: 160rpx; height: 160rpx; border-radius: 50%; background-color: #eee; } .text { display: block; padding-top: 20rpx; line-height: 1; font-size: 26rpx; color: #fff; } } // 表单 .form { background-color: #f4f4f4; &-content { margin: 20rpx 20rpx 0; padding: 0 20rpx; border-radius: 10rpx; background-color: #fff; } &-item { display: flex; height: 96rpx; line-height: 46rpx; padding: 25rpx 10rpx; background-color: #fff; font-size: 28rpx; border-bottom: 1rpx solid #ddd; &:last-child { border: none; } .label { width: 180rpx; color: #333; } .account { color: #666; } .input { flex: 1; display: block; height: 46rpx; } .radio { margin-right: 20rpx; } .picker { flex: 1; } .placeholder { color: #808080; } } &-button { height: 80rpx; text-align: center; line-height: 80rpx; margin: 30rpx 20rpx; color: #fff; border-radius: 80rpx; font-size: 30rpx; background-color: #27ba9b; } } </style>
接口
src\apis\profile.ts
import type { ProfileDetail, ProfileParams } from '@/types/member' import { http } from '@/utils/http' /** * 获取个人信息 */ export const getMemberProfileAPI = () => { return http<ProfileDetail>({ method: 'GET', url: '/member/profile', }) } /** * 修改个人信息 * @param data 请求体参数 */ export const putMemberProfileAPI = (data: ProfileParams) => { return http<ProfileDetail>({ method: 'PUT', url: '/member/profile', data, }) }
类型声明
src\types\member.d.ts
/** 封装通用信息 */ type BaseProfile = { /** 用户ID */ id: number /** 头像 */ avatar: string /** 账户名 */ account: string /** 昵称 */ nickname?: string } /** 小程序登录 登录用户信息 */ export type LoginResult = BaseProfile & { /** 用户ID */ id: number /** 头像 */ avatar: string /** 账户名 */ account: string /** 昵称 */ nickname?: string /** 手机号 */ mobile: string /** 登录凭证 */ token: string } /** 个人信息 用户详情信息 */ export type ProfileDetail = BaseProfile & { /** 性别 */ gender?: Gender /** 生日 */ birthday?: string /** 省市区 */ fullLocation?: string /** 职业 */ profession?: string } /** 性别 */ export type Gender = '女' | '男' /** 个人信息 修改请求体参数 */ export type ProfileParams = Pick< ProfileDetail, 'nickname' | 'gender' | 'birthday' | 'profession' > & { /** 省份编码 */ provinceCode?: string /** 城市编码 */ cityCode?: string /** 区/县编码 */ countyCode?: string }
持久化本地存储 store
src\stores\index.ts
import { createPinia } from 'pinia' import persist from 'pinia-plugin-persistedstate' // 创建 pinia 实例 const pinia = createPinia() // 使用持久化存储插件 pinia.use(persist) // 默认导出,给 main.ts 使用 export default pinia // 模块统一导出 export * from './modules/member'
src\stores\modules\member.ts
import { defineStore } from 'pinia' import { ref } from 'vue' // 定义 Store export const useMemberStore = defineStore( 'member', () => { // 会员信息 const profile = ref<any>() // 保存会员信息,登录时使用 const setProfile = (val: any) => { profile.value = val } // 清理会员信息,退出时使用 const clearProfile = () => { profile.value = undefined } // 记得 return return { profile, setProfile, clearProfile, } }, // 持久化 { persist: { // 调整为兼容多端的API storage: { setItem(key, value) { uni.setStorageSync(key, value) }, getItem(key) { return uni.getStorageSync(key) }, }, }, }, )