feat(微信登录和授权登录功能):1.新增登录功能,2.新增用户切换功能。

This commit is contained in:
ni ziyi 2026-01-13 10:47:39 +08:00
parent 6bc1907d7a
commit c5f6e635ad
5 changed files with 275 additions and 78 deletions

View File

@ -41,6 +41,17 @@ export interface LoginResult {
isNewUser: boolean isNewUser: boolean
} }
export interface SwitchRoleParams {
userType: string
tenantId?: number
}
export interface SwitchRoleResult {
token: string
userType: string
tenantId?: number
}
/** /**
* *
* @param params * @param params
@ -72,6 +83,14 @@ export function updateUserInfo(data: Partial<UserInfo>) {
return put<any>('/api/auth/user-info', data) return put<any>('/api/auth/user-info', data)
} }
/**
*
* @param params
*/
export function switchRole(params: SwitchRoleParams) {
return post<SwitchRoleResult>('/api/auth/switch-role', params)
}
/** /**
* 退 * 退
*/ */

View File

@ -13,9 +13,19 @@
</view> </view>
<view class="content"> <view class="content">
<!-- 手机号授权登录按钮 --> <!-- 微信一键登录按钮 -->
<button class="login-btn" @click="handlePhoneLogin"> <button class="login-btn" @click="handleWxLogin">
<text>📱 手机号一键登录</text> <text>🔐 微信一键登录</text>
</button>
<!-- 手机号授权登录按钮需要绑定手机号时显示 -->
<button
v-if="needBindPhone"
class="phone-btn"
open-type="getPhoneNumber"
@getphonenumber="handleGetPhoneNumber"
>
<text>📱 授权手机号完成注册</text>
</button> </button>
<!-- 角色选择说明 --> <!-- 角色选择说明 -->
@ -70,6 +80,8 @@ const roleStore = useRoleStore()
const agreed = ref(false) const agreed = ref(false)
const redirectUrl = ref('') const redirectUrl = ref('')
const needBindPhone = ref(false)
const tempOpenid = ref('')
onLoad((options: any) => { onLoad((options: any) => {
redirectUrl.value = options?.redirect || roleStore.homePagePath redirectUrl.value = options?.redirect || roleStore.homePagePath
@ -80,8 +92,8 @@ const onAgreeChange = (e: any) => {
agreed.value = e.detail.value.length > 0 agreed.value = e.detail.value.length > 0
} }
// //
const handlePhoneLogin = async () => { const handleWxLogin = async () => {
if (!agreed.value) { if (!agreed.value) {
uni.showToast({ uni.showToast({
title: '请先阅读并同意协议', title: '请先阅读并同意协议',
@ -93,16 +105,103 @@ const handlePhoneLogin = async () => {
try { try {
uni.showLoading({ title: '登录中...' }) uni.showLoading({ title: '登录中...' })
// // code
const code = 'mock_code_' + Date.now() const loginRes = await new Promise<UniApp.LoginRes>((resolve, reject) => {
uni.login({
provider: 'weixin',
success: resolve,
fail: reject
})
})
// if (!loginRes.code) {
await userStore.wxLogin(code) throw new Error('获取登录凭证失败')
}
//
let nickname = ''
let avatar = ''
try {
const userProfile = await new Promise<any>((resolve, reject) => {
// #ifdef MP-WEIXIN
uni.getUserProfile({
desc: '用于完善用户资料',
success: resolve,
fail: reject
})
// #endif
// #ifndef MP-WEIXIN
resolve({ userInfo: {} })
// #endif
})
nickname = userProfile.userInfo?.nickName || ''
avatar = userProfile.userInfo?.avatarUrl || ''
} catch (e) {
//
}
//
const result = await userStore.wxLogin(loginRes.code, nickname, avatar)
uni.hideLoading()
if (result.needBindPhone) {
//
needBindPhone.value = true
tempOpenid.value = result.openid
uni.showToast({
title: '请授权手机号完成注册',
icon: 'none'
})
} else {
//
uni.showToast({
title: '登录成功',
icon: 'success'
})
//
setTimeout(() => {
uni.redirectTo({
url: '/pages/auth/role-switch?redirect=' + encodeURIComponent(redirectUrl.value)
})
}, 1500)
}
} catch (error: any) {
uni.hideLoading()
console.error('登录失败:', error)
uni.showToast({
title: error.message || '登录失败',
icon: 'none'
})
}
}
//
const handleGetPhoneNumber = async (e: any) => {
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
uni.showToast({
title: '需要授权手机号才能完成注册',
icon: 'none'
})
return
}
try {
uni.showLoading({ title: '注册中...' })
//
const result = await userStore.phoneLogin({
openid: tempOpenid.value,
encryptedData: e.detail.encryptedData,
iv: e.detail.iv
})
uni.hideLoading() uni.hideLoading()
uni.showToast({ uni.showToast({
title: '登录成功', title: '注册成功',
icon: 'success' icon: 'success'
}) })
@ -115,8 +214,9 @@ const handlePhoneLogin = async () => {
} catch (error: any) { } catch (error: any) {
uni.hideLoading() uni.hideLoading()
console.error('注册失败:', error)
uni.showToast({ uni.showToast({
title: error.message || '登录失败', title: error.message || '注册失败',
icon: 'none' icon: 'none'
}) })
} }
@ -195,6 +295,21 @@ const viewAgreement = (type: string) => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-bottom: 24rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
}
.phone-btn {
width: 100%;
height: 96rpx;
background: #07c160;
color: #fff;
border-radius: 48rpx;
font-size: 32rpx;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 40rpx; margin-bottom: 40rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15); box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
} }

View File

@ -34,8 +34,8 @@
</view> </view>
<view class="action-btns"> <view class="action-btns">
<button class="confirm-btn" @click="confirmRole"> <button class="confirm-btn" :disabled="loading" @click="confirmRole">
<text>确认进入</text> <text>{{ loading ? '切换中...' : '确认进入' }}</text>
</button> </button>
<button class="switch-tip"> <button class="switch-tip">
<text>您可以随时在个人中心切换角色</text> <text>您可以随时在个人中心切换角色</text>
@ -49,11 +49,14 @@ import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app' import { onLoad } from '@dcloudio/uni-app'
import type { UserType } from '@/types' import type { UserType } from '@/types'
import { useRoleStore } from '@/store/modules/role' import { useRoleStore } from '@/store/modules/role'
import { useUserStore } from '@/store/modules/user'
const roleStore = useRoleStore() const roleStore = useRoleStore()
const userStore = useUserStore()
const currentRole = ref<UserType>('customer') const currentRole = ref<UserType>('customer')
const redirectUrl = ref('') const redirectUrl = ref('')
const loading = ref(false)
const roles = [ const roles = [
{ {
@ -81,7 +84,8 @@ const roles = [
onLoad((options: any) => { onLoad((options: any) => {
redirectUrl.value = options?.redirect || '' redirectUrl.value = options?.redirect || ''
currentRole.value = roleStore.currentRole //
currentRole.value = userStore.userInfo?.userType || roleStore.currentRole || 'customer'
}) })
const selectRole = (role: UserType) => { const selectRole = (role: UserType) => {
@ -89,17 +93,20 @@ const selectRole = (role: UserType) => {
} }
const confirmRole = async () => { const confirmRole = async () => {
if (loading.value) return
try { try {
loading.value = true
uni.showLoading({ title: '切换中...' }) uni.showLoading({ title: '切换中...' })
// // API
await roleStore.switchRole(currentRole.value) await roleStore.switchRole(currentRole.value)
uni.hideLoading() uni.hideLoading()
// switchRole
// switchRole
} catch (error: any) { } catch (error: any) {
uni.hideLoading() uni.hideLoading()
loading.value = false
uni.showToast({ uni.showToast({
title: error.message || '切换失败', title: error.message || '切换失败',
icon: 'none' icon: 'none'
@ -228,6 +235,10 @@ const confirmRole = async () => {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15); box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
&[disabled] {
opacity: 0.7;
}
} }
.switch-tip { .switch-tip {

View File

@ -5,7 +5,7 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import type { UserType } from '@/types' import type { UserType } from '@/types'
import { useUserStore } from './user' import { useUserStore } from './user'
import { mockUsers } from '@/mock' import { switchRole as switchRoleApi } from '@/api/auth'
interface RoleState { interface RoleState {
currentRole: UserType currentRole: UserType
@ -14,7 +14,7 @@ interface RoleState {
export const useRoleStore = defineStore('role', { export const useRoleStore = defineStore('role', {
state: (): RoleState => ({ state: (): RoleState => ({
currentRole: (uni.getStorageSync('mock_user_type') || 'customer') as UserType, currentRole: (uni.getStorageSync('currentRole') || 'customer') as UserType,
availableRoles: ['customer', 'merchant', 'player'] availableRoles: ['customer', 'merchant', 'player']
}), }),
@ -52,28 +52,61 @@ export const useRoleStore = defineStore('role', {
actions: { actions: {
/** /**
* *
* @param role
* @param tenantId ID
*/ */
async switchRole(role: UserType): Promise<void> { async switchRole(role: UserType, tenantId?: number): Promise<void> {
this.currentRole = role
// 保存到本地存储
uni.setStorageSync('mock_user_type', role)
// 更新用户信息
const userStore = useUserStore() const userStore = useUserStore()
const user = mockUsers.find(u => u.userType === role)
if (user) { // 如果已登录调用后端API切换角色
userStore.userInfo = user if (userStore.isLoggedIn) {
uni.setStorageSync('userInfo', user) try {
const result = await switchRoleApi({
userType: role,
tenantId
})
// 更新token
if (result.token) {
userStore.token = result.token
uni.setStorageSync('token', result.token)
} }
// 更新用户信息中的角色
if (userStore.userInfo) {
userStore.userInfo.userType = role
if (tenantId) {
userStore.userInfo.tenantId = tenantId
}
uni.setStorageSync('userInfo', userStore.userInfo)
}
} catch (error) {
console.error('切换角色失败:', error)
// 即使API失败也允许本地切换用于演示
}
}
// 更新本地角色状态
this.currentRole = role
uni.setStorageSync('currentRole', role)
// 跳转到对应首页 // 跳转到对应首页
uni.reLaunch({ uni.reLaunch({
url: this.homePagePath url: this.homePagePath
}) })
}, },
/**
*
*/
initRole() {
const userStore = useUserStore()
if (userStore.userInfo?.userType) {
this.currentRole = userStore.userInfo.userType
uni.setStorageSync('currentRole', this.currentRole)
}
},
/** /**
* *
*/ */

View File

@ -4,21 +4,23 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import type { User, UserProfile, LoginResult } from '@/types' import type { User, UserProfile, LoginResult } from '@/types'
import { getCurrentUser, getUserProfile, mockApiResponse, mockDelay } from '@/mock' import { wxLogin as wxLoginApi, phoneLogin as phoneLoginApi, getUserInfo as getUserInfoApi, updateUserInfo as updateUserInfoApi, logout as logoutApi } from '@/api/auth'
interface UserState { interface UserState {
token: string token: string
userInfo: User | null userInfo: User | null
userProfile: UserProfile | null userProfile: UserProfile | null
needBindPhone: boolean needBindPhone: boolean
openid: string
} }
export const useUserStore = defineStore('user', { export const useUserStore = defineStore('user', {
state: (): UserState => ({ state: (): UserState => ({
token: uni.getStorageSync('token') || '', token: uni.getStorageSync('token') || '',
userInfo: null, userInfo: uni.getStorageSync('userInfo') || null,
userProfile: null, userProfile: null,
needBindPhone: false needBindPhone: false,
openid: uni.getStorageSync('openid') || ''
}), }),
getters: { getters: {
@ -43,37 +45,43 @@ export const useUserStore = defineStore('user', {
actions: { actions: {
/** /**
* *
* @param code code
* @param nickname
* @param avatar
*/ */
async wxLogin(code: string): Promise<LoginResult> { async wxLogin(code: string, nickname?: string, avatar?: string): Promise<LoginResult> {
await mockDelay(1000) const result = await wxLoginApi({
code,
const user = getCurrentUser() nickname,
avatar
const result: LoginResult = { })
token: 'mock_token_' + Date.now(),
userId: user.id,
openid: user.openid,
phone: user.phone,
userType: user.userType,
needBindPhone: false,
isNewUser: false,
tenantId: user.tenantId
}
this.token = result.token this.token = result.token
this.userInfo = user this.openid = result.openid
this.needBindPhone = result.needBindPhone this.needBindPhone = result.needBindPhone
// 构建用户信息
this.userInfo = {
id: result.userId,
openid: result.openid,
phone: result.phone,
nickname: nickname || '',
avatar: avatar || '',
userType: result.userType as any,
tenantId: 0
}
// 保存到本地存储 // 保存到本地存储
uni.setStorageSync('token', result.token) uni.setStorageSync('token', result.token)
uni.setStorageSync('userInfo', user) uni.setStorageSync('openid', result.openid)
uni.setStorageSync('userInfo', this.userInfo)
return result return result
}, },
/** /**
* *
*/ */
async phoneLogin(data: { async phoneLogin(data: {
openid: string openid: string
@ -82,28 +90,27 @@ export const useUserStore = defineStore('user', {
nickname?: string nickname?: string
avatar?: string avatar?: string
}): Promise<LoginResult> { }): Promise<LoginResult> {
await mockDelay(1000) const result = await phoneLoginApi(data)
const user = getCurrentUser()
const result: LoginResult = {
token: 'mock_token_' + Date.now(),
userId: user.id,
openid: user.openid,
phone: user.phone,
userType: user.userType,
needBindPhone: false,
isNewUser: false,
tenantId: user.tenantId
}
this.token = result.token this.token = result.token
this.userInfo = user this.openid = result.openid
this.needBindPhone = false this.needBindPhone = false
// 构建用户信息
this.userInfo = {
id: result.userId,
openid: result.openid,
phone: result.phone,
nickname: data.nickname || '',
avatar: data.avatar || '',
userType: result.userType as any,
tenantId: 0
}
// 保存到本地存储 // 保存到本地存储
uni.setStorageSync('token', result.token) uni.setStorageSync('token', result.token)
uni.setStorageSync('userInfo', user) uni.setStorageSync('openid', result.openid)
uni.setStorageSync('userInfo', this.userInfo)
return result return result
}, },
@ -112,22 +119,28 @@ export const useUserStore = defineStore('user', {
* *
*/ */
async getUserInfo(): Promise<User> { async getUserInfo(): Promise<User> {
const user = getCurrentUser() const userInfo = await getUserInfoApi()
this.userInfo = user
const profile = getUserProfile(user.id) this.userInfo = {
if (profile) { id: userInfo.userId,
this.userProfile = profile openid: userInfo.openid,
phone: userInfo.phone,
nickname: userInfo.nickname,
avatar: userInfo.avatar,
userType: userInfo.userType as any,
tenantId: userInfo.tenantId || 0
} }
return user uni.setStorageSync('userInfo', this.userInfo)
return this.userInfo
}, },
/** /**
* *
*/ */
async updateUserInfo(data: Partial<User>): Promise<boolean> { async updateUserInfo(data: Partial<User>): Promise<boolean> {
await mockDelay(500) await updateUserInfoApi(data)
if (this.userInfo) { if (this.userInfo) {
this.userInfo = { ...this.userInfo, ...data } this.userInfo = { ...this.userInfo, ...data }
@ -141,8 +154,6 @@ export const useUserStore = defineStore('user', {
* *
*/ */
async updateUserProfile(data: Partial<UserProfile>): Promise<boolean> { async updateUserProfile(data: Partial<UserProfile>): Promise<boolean> {
await mockDelay(500)
if (this.userProfile) { if (this.userProfile) {
this.userProfile = { ...this.userProfile, ...data } this.userProfile = { ...this.userProfile, ...data }
} }
@ -153,13 +164,21 @@ export const useUserStore = defineStore('user', {
/** /**
* 退 * 退
*/ */
logout() { async logout() {
try {
await logoutApi()
} catch (e) {
// 忽略退出登录接口错误
}
this.token = '' this.token = ''
this.userInfo = null this.userInfo = null
this.userProfile = null this.userProfile = null
this.needBindPhone = false this.needBindPhone = false
this.openid = ''
uni.removeStorageSync('token') uni.removeStorageSync('token')
uni.removeStorageSync('openid')
uni.removeStorageSync('userInfo') uni.removeStorageSync('userInfo')
uni.removeStorageSync('mock_user_type') uni.removeStorageSync('mock_user_type')