[uni-app]小兔鲜-05登录+个人中心
登录
微信授权登录: 通过wx.login()获取code登录凭证, 通过特定类型按钮获取用户手机号, 实现授权登录


import type { LoginResult } from '@/types/member'
import { http } from '@/utils/http'type LoginParams = {code: stringencryptedData: stringiv: string
}// 微信登录
export const postLoginWxMin = (data: LoginParams) => {return http<LoginResult>({method: 'POST',url: '/login/wxMin',data,})
}// 模拟微信登录
export const postLoginWxMinSimpleAPI = (phoneNumber: string) => {return http<LoginResult>({method: 'POST',url: '/login/wxMin/simple',data: {phoneNumber,},})
}
// 用户信息类型文件/** 小程序登录 登录用户信息 */
export type LoginResult = {/** 用户ID */id: number/** 头像 */avatar: string/** 账户名 */account: string/** 昵称 */nickname?: string/** 手机号 */mobile: string/** 登录凭证 */token: string
}
<script setup lang="ts">
import { postLoginWxMin } from '@/services/login'
import { onLoad } from '@dcloudio/uni-app'// 获取code
let code = ''
onLoad(async () => {const res = await wx.login()code = res.code
})// 微信登录
const onGetPhoneNumber: UniHelper.ButtonOnGetphonenumber = async (ev) => {const encryptedData = ev.detail!.encryptedData!const iv = ev.detail!.iv!const res = await postLoginWxMin({encryptedData,iv,code,})console.log(res)
}</script><template><view class="viewport"><view class="logo"><imagesrc="https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/images/logo_icon.png"></image></view><view class="login"><!-- 小程序端授权登录 --><button class="button phone" open-type="getPhoneNumber" @getphonenumber="onGetPhoneNumber"><text class="icon icon-phone"></text>手机号快捷登录</button><view class="tips">登录/注册即视为你同意《服务条款》和《小兔鲜儿隐私协议》</view></view></view>
</template>
- 使用企业小程序appid, 并且把微信号添加到开发者列表中, 才能获取到用户手机号
- ! 非空断言排除的是前面的空值, 有需要的话可以使用多次
模拟登录: 获取手机号功能针对非个人开发者, 且认证完成的小程序开发
<sript setup lang="ts">
import { useMemberStore } from '@/stores'
import { postLoginWxMinSimpleAPI } from '@/services/login'// 模拟微信登录
const onGetPhoneNumberSimple = async () => {const res = await postLoginWxMinSimpleAPI('15553266208')console.log(res)
}
</script><template><view class="viewport"><view class="logo"><imagesrc="https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/images/logo_icon.png"></image></view><view class="login"><!-- 小程序端授权登录 --><button class="button phone"><text class="icon icon-phone"></text>手机号快捷登录</button><view class="extra"><view class="caption"><text>其他登录方式</text></view><view class="options"><!-- 通用模拟登录 --><button @tap="onGetPhoneNumberSimple"><text class="icon icon-phone">模拟快捷登录</text></button></view></view><view class="tips">登录/注册即视为你同意《服务条款》和《小兔鲜儿隐私协议》</view></view></view>
</template>
保存登录信息

<sript setup lang="ts">
import { useMemberStore } from '@/stores'
import { postLoginWxMinSimpleAPI } from '@/services/login'// 模拟微信登录
const onGetPhoneNumberSimple = async () => {const res = await postLoginWxMinSimpleAPI('15553266208')// 保存用户信息const memberStore = useMemberStore()memberStore.setProfile(res.result)// 提示uni.showToast({icon: 'none',title: '登录成功',})// 跳转setTimeout(() => {uni.navigateBack()}, 500)
}
</script>
import { defineStore } from 'pinia'
import { ref } from 'vue'// 定义 Store
export const useMemberStore = defineStore('member',() => {// 会员信息const profile = ref<LoginResult>()// 保存会员信息,登录时使用const setProfile = (val: LoginResult) => {profile.value = val}// 清理会员信息,退出时使用const clearProfile = () => {profile.value = undefined}// 记得 returnreturn {profile,setProfile,clearProfile,}},// TODO: 持久化persist: {storage: {getItem(key) {return uni.getStorageSync(key)},setItem(key, value) {uni.setStorageSync(key, value)},},},},
)
我的页面
创建我的页面, 渲染用户信息

"pages": [{"path": "pages/my/my","style": {"navigationStyle": "custom", // 隐藏默认导航"navigationBarTextStyle": "white","navigationBarTitleText": "我的"}}
]
<script setup lang="ts">
import { useMemberStore } from '@/stores'
import { ref } from 'vue'
import { useGuessList } from '@/composables/index'// 获取屏幕边界到安全区域距离
const { safeAreaInsets } = uni.getSystemInfoSync()
// 订单选项
const orderTypes = [{ type: 1, text: '待付款', icon: 'icon-currency' },{ type: 2, text: '待发货', icon: 'icon-gift' },{ type: 3, text: '待收货', icon: 'icon-check' },{ type: 4, text: '待评价', icon: 'icon-comment' },
]
// 获取会员信息
const memberStore = useMemberStore()
</script><template><scroll-view class="viewport" scroll-y enable-back-to-top @scrolltolower="onScorlltolower"><!-- 个人资料 --><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"><imageclass="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><!-- 我的订单 --><view class="orders"><view class="title">我的订单<navigator class="navigator" url="/pagesOrder/list/list?type=0" hover-class="none">查看全部订单<text class="icon-right"></text></navigator></view><view class="section"><!-- 订单 --><navigatorv-for="item in orderTypes":key="item.type":class="item.icon":url="`/pagesOrder/list/list?type=${item.type}`"class="navigator"hover-class="none">{{ item.text }}</navigator><!-- 客服 --><button class="contact icon-handset" open-type="contact">售后</button></view></view></scroll-view>
</template>
猜你喜欢分页加载

<script setup lang="ts">
import { useGuessList } from '@/composables/index'// 调用组合式函数,实现猜你喜欢分页加载
const { guessRef, onScorlltolower } = useGuessList()
</script><template><scroll-view class="viewport" scroll-y enable-back-to-top @scrolltolower="onScorlltolower"><!-- 个人资料 --><view class="profile" :style="{ paddingTop: safeAreaInsets!.top + 'px' }">... ...</view><!-- 猜你喜欢 --><view class="guess"><XtxGuess ref="guessRef" /></view></scroll-view>
</template>
import type { XtxGuessInstance } from '@/types/component'
import { ref } from 'vue'/*** 猜你喜欢组合式函数*/
export const useGuessList = () => {// 猜你喜欢的组件实例const guessRef = ref<XtxGuessInstance>()// 滚动触底事件const onScorlltolower = () => {guessRef.value.getMore()}// 返回ref 和 事件处理函数return {guessRef,onScorlltolower,}
}
- 组合式函数是vue3组合式API支持的语法, 进一步封装vue组件中的函数
- 组合式函数一般以use开头进行命名
效果展示

设置页面
新建分包页面, 配置分包预下载
分包: 将小程序的核心页面和次要页面进行分割,打包成多个小程序包,减少小程序的加载时间 ,提高用户体验
预下载: 在进入小程序的指定页面时,由框架自动提前下载分包资源. 提升进入分包页面的启动速度
<template><view class="viewport"><!-- 列表1 --><view class="list" v-if="true"><navigator url="/pagesMember/address/address" hover-class="none" class="item arrow">我的收货地址</navigator></view><!-- 列表2 --><view class="list"><button hover-class="none" class="item arrow" open-type="openSetting">授权管理</button><button hover-class="none" class="item arrow" open-type="feedback">问题反馈</button><button hover-class="none" class="item arrow" open-type="contact">联系我们</button></view><!-- 列表3 --><view class="list"><navigator hover-class="none" class="item arrow" url=" ">关于小兔鲜儿</navigator></view><!-- 操作按钮 --><view class="action"><view class="button">退出登录</view></view></view>
</template>
- 存放: 主包页面放在pages中管理, 分包单独创建文件夹管理
- 新建: 编辑器都有快捷新建分包页面的功能


配置分包预下载规则

{// 主包页面配置"pages": [...],// 分包页面配置"subPackages": [{// 子包的根目录"root": "pagesMember",// 页面路径和窗口表现"pages": [{"path": "settings/settings","style": {"navigationBarTitleText": "设置"}},]},],// 分包预下载规则"preloadRule": {// 进入页面时预下载的分包"pages/my/my": {// 网络"network": "all",// 预下载的分包"packages": ["pagesMember"]}}
}
退出登录

<script setup lang="ts">
import { useMemberStore } from '@/stores'
// 退出登录
const memberStore = useMemberStore()
const onLogout = () => {// 确认框uni.showModal({content: '是否退出登录?',success: (res) => {if (res.confirm) {// 清理用户信息memberStore.clearProfile()// 页面跳转uni.navigateBack()}},})
}
</script><template><view class="viewport"><!-- 列表1 --><view class="list" v-if="memberStore.profile"><navigator url="/pagesMember/address/address" hover-class="none" class="item arrow">我的收货地址</navigator></view><!-- 列表2 --><view class="list"><button hover-class="none" class="item arrow" open-type="openSetting">授权管理</button><button hover-class="none" class="item arrow" open-type="feedback">问题反馈</button><button hover-class="none" class="item arrow" open-type="contact">联系我们</button></view><!-- 列表3 --><view class="list"><navigator hover-class="none" class="item arrow" url=" ">关于小兔鲜儿</navigator></view><!-- 操作按钮 --><view class="action"><view class="button" @tap="onLogout" v-if="memberStore.profile">退出登录</view></view></view>
</template>
实现效果

个人信息
准备分包页面, 初始化渲染


<script setup lang="ts">
import { getMemberProfileAPI } from '@/services/profile'
import type { ProfileDetail } from '@/types/member'
import { onLoad } from '@dcloudio/uni-app'
import { ref } from 'vue'// 获取屏幕边界到安全区域距离
const { safeAreaInsets } = uni.getSystemInfoSync()// 获取个人信息
const profile = ref({} as ProfileDetail)
const getMemberProfileData = async () => {const res = await getMemberProfileAPI()profile.value = res.result
}onLoad(() => {getMemberProfileData()
})
</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><pickerclass="picker"mode="date"start="1900-01-01":end="new Date()"@change="onBirthdayChange":value="profile?.birthday"><view v-if="profile?.birthday">{{ profile?.birthday }}</view><view class="placeholder" v-else>请选择日期</view></picker></view><view class="form-item"><text class="label">城市</text><pickerclass="picker"mode="region":value="profile?.fullLocation?.split(' ')"@change="onFullLocationChange"><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>
import type { ProfileDetail, ProfileParams } from '@/types/member'
import { http } from '@/utils/http'/*** 获取个人信息*/
export const getMemberProfileAPI = () => {return http<ProfileDetail>({method: 'GET',url: '/member/profile',})
}
// 用户信息类型文件/** 封装通用信息 */
type BaseProfile = {/** 用户ID */id: number/** 头像 */avatar: string/** 账户名 */account: string/** 昵称 */nickname?: string
}/** 小程序登录 登录用户信息 */
export type LoginResult = BaseProfile & {/** 手机号 */mobile: string/** 登录凭证 */token: string
}/** 个人信息 用户详情信息 */
export type ProfileDetail = BaseProfile & {/** 性别 */gender?: Gender/** 生日 */birthday?: string/** 省市区 */fullLocation?: string/** 职业 */profession?: string
}/** 性别 */
export type Gender = '女' | '男'
- 使用联合类型 & 进一步封装类型声明
头像上传

<script setup lang="ts">
import { getMemberProfileAPI, putMemberProfileAPI } from '@/services/profile'
import type { ProfileDetail, Gender } from '@/types/member'
import { ref } from 'vue'
import { useMemberStore } from '@/stores'// 点击头像
const memberStore = useMemberStore()
const onAvatarChange = () => {// 调用拍照/选择图片// #ifdef H5 || APP-PLUS// 微信小程序从基础库 2.21.0 开始, wx.chooseImage 停止维护,请使用 uni.chooseMedia 代替uni.chooseImage({count: 1,success: (res) => {// 文件路径const tempFilePaths = res.tempFilePaths// 上传uploadFile(tempFilePaths[0])},})// #endif// #ifdef MP-WEIXIN// uni.chooseMedia 仅支持微信小程序端uni.chooseMedia({// 文件个数count: 1,// 文件类型mediaType: ['image'],success: (res) => {// 本地路径const { tempFilePath } = res.tempFiles[0]// 上传uploadFile(tempFilePath)},})// #endif
}// 文件上传-兼容小程序端、H5端、App端
const uploadFile = (file: string) => {// 文件上传uni.uploadFile({url: '/member/profile/avatar',name: 'file',filePath: file,success: (res) => {if (res.statusCode === 200) {const avatar = JSON.parse(res.data).result.avatar// 个人信息页数据更新profile.value!.avatar = avatar// Store头像更新memberStore.profile!.avatar = avataruni.showToast({ icon: 'success', title: '更新成功' })} else {uni.showToast({ icon: 'error', title: '出现错误' })}},})
}
</script><template><view class="viewport">...<!-- 头像 --><view class="avatar"><view class="avatar-content" @tap="onAvatarChange"><image class="image" :src="profile?.avatar" mode="aspectFill" /><text class="text">点击修改头像</text></view></view>...</view></view>
</template>
修改用户昵称

import type { ProfileDetail, ProfileParams } from '@/types/member'
import { http } from '@/utils/http'/*** 修改个人信息*/
export const putMemberProfileAPI = (data: ProfileParams) => {return http<ProfileDetail>({method: 'PUT',url: '/member/profile',data,})
}
// 用户信息类型文件/** 封装通用信息 */
type BaseProfile = {/** 用户ID */id: number/** 头像 */avatar: string/** 账户名 */account: string/** 昵称 */nickname?: 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
}
- Pick<目标类型, 选取的属性>泛型工具方法是TS提供的, 用于提取已有类型中的参数, 组合成新的类型
<script setup lang="ts">
import { putMemberProfileAPI } from '@/services/profile'
import type { ProfileDetail } from '@/types/member'
import { ref } from 'vue'// 获取个人信息
const profile = ref({} as ProfileDetail)// 表单提交
const onSubmit = async () => {const res = await putMemberProfileAPI({nickname: profile.value.nickname,})// 更新stroe的昵称memberStore.profile!.nickname = res.result.nicknameuni.showToast({icon: 'success',titlelist: '修改成功',})// 返回上一页setTimeout(() => {uni.navigateBack()}, 400)
}
</script><template><view class="viewport"><!-- 表单 --><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><!-- 提交按钮 --><button class="form-button" @tap="onSubmit">保 存</button></view></view>
</template>
- 在TS中对数据的类型限制非常严格, 一个对象中的属性是不能随便添加的, 要和类型声明保持一致
- 仅做展示的对象数据, 一般只限制类型, 不给对象的定义初始值, 这样比较方便
- 对于需要双向绑定的数据, 就要使用 as类型断言, 告诉TS这个对象以后是什么样子, 这个对象才能顺利的进行数据双向绑定, 也就相当于给对象进行了初始值的定义
修改性别信息

<script setup lang="ts">
import { putMemberProfileAPI } from '@/services/profile'
import type { ProfileDetail, Gender } from '@/types/member'
import { ref } from 'vue'// 获取个人信息
const profile = ref({} as ProfileDetail)// 男女选项变化
const onGenderChange: UniHelper.RadioGroupOnChange = (ev) => {profile.value.gender = ev.detail.value as Gender
}// 表单提交
const onSubmit = async () => {const res = await putMemberProfileAPI({gender: profile.value.gender,})// 更新stroe的昵称memberStore.profile!.nickname = res.result.nicknameuni.showToast({icon: 'success',titlelist: '修改成功',})setTimeout(() => {uni.navigateBack()}, 400)
}
</script><template><view class="viewport">... ...<!-- 表单 --><view class="form"><!-- 表单内容 --><view class="form-content"><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><!-- 提交按钮 --><button class="form-button" @tap="onSubmit">保 存</button></view></view>
</template>
- ev.detail.value 拿到的是string类型数据
- profile.value.gender 限制的数据类型是 gender
- 如果直接赋值TS会爆红, 所以要通过类型断言告诉TS, ev拿到的就是gender类型
修改生日信息

<script setup lang="ts">
import { putMemberProfileAPI } from '@/services/profile'
import type { ProfileDetail, Gender } from '@/types/member'
import { ref } from 'vue'// 获取个人信息
const profile = ref({} as ProfileDetail)// 修改生日
const onBirthdayChange: UniHelper.DatePickerOnChange = (ev) => {profile.value.birthday = ev.detail.value
}// 表单提交
const onSubmit = async () => {const res = await putMemberProfileAPI({birthday: profile.value.birthday,})// 更新stroe的昵称memberStore.profile!.nickname = res.result.nicknameuni.showToast({icon: 'success',titlelist: '修改成功',})setTimeout(() => {uni.navigateBack()}, 400)
}
</script><template><view class="viewport">... ...<!-- 表单 --><view class="form"><!-- 表单内容 --><view class="form-content"><view class="form-item"><text class="label">生日</text><pickerclass="picker"mode="date"start="1900-01-01":end="new Date()"@change="onBirthdayChange":value="profile?.birthday"><view v-if="profile?.birthday">{{ profile?.birthday }}</view><view class="placeholder" v-else>请选择日期</view></picker></view></view><!-- 提交按钮 --><button class="form-button" @tap="onSubmit">保 存</button></view></view>
</template>
修改用户所在城市

<script setup lang="ts">
import { putMemberProfileAPI } from '@/services/profile'
import type { ProfileDetail } from '@/types/member'
import { ref } from 'vue'// 获取个人信息
const profile = ref({} as ProfileDetail)// 修改城市
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 res = await putMemberProfileAPI({provinceCode: fullLocationCode[0],cityCode: fullLocationCode[1],countyCode: fullLocationCode[2],})// 更新stroe的昵称memberStore.profile!.nickname = res.result.nicknameuni.showToast({icon: 'success',titlelist: '修改成功',})setTimeout(() => {uni.navigateBack()}, 400)
}
</script><template><view class="viewport">... <!-- 表单 --><view class="form"><!-- 表单内容 --><view class="form-content">...<view class="form-item"><text class="label">城市</text><pickerclass="picker"mode="region":value="profile?.fullLocation?.split(' ')"@change="onFullLocationChange"><view v-if="profile?.fullLocation">{{ profile?.fullLocation }}</view><view class="placeholder" v-else>请选择城市</view></picker></view></view><!-- 提交按钮 --><button class="form-button" @tap="onSubmit">保 存</button></view></view>
</template>
- 在TS中, 长度和类型都已经确定的数组称为元组
实现效果

