feat(user): 实现登录状态实时更新
- 使用 useUser Hook集中管理用户状态 - 登录成功后实时更新 Header 和 UserCard 组件 - 移除页面刷新操作,提升用户体验- 添加登录成功提示
This commit is contained in:
90
LOGIN_STATUS_UPDATE_TEST.md
Normal file
90
LOGIN_STATUS_UPDATE_TEST.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# 登录状态实时更新测试指南
|
||||||
|
|
||||||
|
## 问题描述
|
||||||
|
之前的问题是:用户登录成功后,Header组件的登录状态没有实时更新,需要刷新页面才能看到登录状态的变化。
|
||||||
|
|
||||||
|
## 解决方案
|
||||||
|
修改了以下组件,使其使用 `useUser` Hook 来管理用户状态:
|
||||||
|
|
||||||
|
1. **Header组件** (`src/pages/index/Header.tsx`)
|
||||||
|
2. **UserCard组件** (`src/pages/user/components/UserCard.tsx`)
|
||||||
|
|
||||||
|
### 主要修改内容
|
||||||
|
|
||||||
|
#### 1. Header组件修改
|
||||||
|
- 移除了本地状态 `IsLogin` 和 `userInfo`
|
||||||
|
- 使用 `useUser` Hook 的 `user`, `isLoggedIn`, `loginUser`, `fetchUserInfo`
|
||||||
|
- 登录成功后调用 `loginUser(token, userData)` 而不是直接设置本地状态
|
||||||
|
- 移除了 `Taro.reLaunch` 重新启动小程序的逻辑
|
||||||
|
|
||||||
|
#### 2. UserCard组件修改
|
||||||
|
- 类似的修改,使用 `useUser` Hook 管理状态
|
||||||
|
- 登录成功后调用 `loginUser(token, userData)`
|
||||||
|
|
||||||
|
## 测试步骤
|
||||||
|
|
||||||
|
### 1. 基本登录状态更新测试
|
||||||
|
1. 打开小程序,确保处于未登录状态
|
||||||
|
2. 在首页Header区域,应该显示"未登录"状态
|
||||||
|
3. 点击登录按钮,完成手机号授权登录
|
||||||
|
4. **关键测试点**:登录成功后,Header应该立即显示用户信息,无需刷新页面
|
||||||
|
|
||||||
|
### 2. 跨页面状态同步测试
|
||||||
|
1. 在首页完成登录
|
||||||
|
2. 切换到用户中心页面
|
||||||
|
3. 检查UserCard组件是否正确显示登录状态
|
||||||
|
4. 返回首页,检查Header组件状态是否保持一致
|
||||||
|
|
||||||
|
### 3. 退出登录测试
|
||||||
|
1. 在用户中心点击"退出登录"
|
||||||
|
2. 返回首页,检查Header是否立即显示未登录状态
|
||||||
|
|
||||||
|
### 4. 页面刷新测试
|
||||||
|
1. 登录后刷新页面(或重新进入小程序)
|
||||||
|
2. 检查登录状态是否正确从本地存储恢复
|
||||||
|
|
||||||
|
## 预期结果
|
||||||
|
|
||||||
|
### 修改前的问题
|
||||||
|
- 登录成功后需要 `Taro.reLaunch` 重新启动小程序
|
||||||
|
- 状态更新不实时,用户体验差
|
||||||
|
|
||||||
|
### 修改后的预期效果
|
||||||
|
- 登录成功后立即更新UI状态
|
||||||
|
- 无需重新启动小程序
|
||||||
|
- 所有使用 `useUser` Hook 的组件都能实时同步状态
|
||||||
|
- 更好的用户体验
|
||||||
|
|
||||||
|
## 技术实现原理
|
||||||
|
|
||||||
|
### useUser Hook 的优势
|
||||||
|
1. **集中状态管理**:所有用户相关状态都在一个地方管理
|
||||||
|
2. **自动同步**:所有使用该Hook的组件都会自动同步状态变化
|
||||||
|
3. **持久化存储**:自动处理本地存储的读写
|
||||||
|
4. **错误处理**:统一的错误处理逻辑
|
||||||
|
|
||||||
|
### 状态更新流程
|
||||||
|
```
|
||||||
|
用户登录 → loginUser(token, userData) → 更新Hook状态 → 所有组件自动重新渲染
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **确保所有相关组件都使用useUser Hook**:避免混用本地状态和全局状态
|
||||||
|
2. **测试边界情况**:网络错误、token过期等情况
|
||||||
|
3. **性能考虑**:useUser Hook 已经优化了不必要的重新渲染
|
||||||
|
|
||||||
|
## 如果测试失败
|
||||||
|
|
||||||
|
如果登录后状态仍然没有实时更新,请检查:
|
||||||
|
|
||||||
|
1. 组件是否正确导入和使用了 `useUser` Hook
|
||||||
|
2. 登录成功后是否调用了 `loginUser` 方法
|
||||||
|
3. 是否还有其他地方使用了本地状态而不是Hook状态
|
||||||
|
4. 控制台是否有错误信息
|
||||||
|
|
||||||
|
## 进一步优化建议
|
||||||
|
|
||||||
|
1. 可以考虑添加加载状态指示器
|
||||||
|
2. 可以添加登录状态变化的动画效果
|
||||||
|
3. 可以考虑使用 React Context 进一步优化性能
|
||||||
@@ -3,31 +3,33 @@ import Taro from '@tarojs/taro';
|
|||||||
import {Button, Space} from '@nutui/nutui-react-taro'
|
import {Button, Space} from '@nutui/nutui-react-taro'
|
||||||
import {TriangleDown} from '@nutui/icons-react-taro'
|
import {TriangleDown} from '@nutui/icons-react-taro'
|
||||||
import {Avatar, NavBar} from '@nutui/nutui-react-taro'
|
import {Avatar, NavBar} from '@nutui/nutui-react-taro'
|
||||||
import {getUserInfo, getWxOpenId} from "@/api/layout";
|
import {getWxOpenId} from "@/api/layout";
|
||||||
import {TenantId} from "@/config/app";
|
import {TenantId} from "@/config/app";
|
||||||
import {getOrganization} from "@/api/system/organization";
|
import {getOrganization} from "@/api/system/organization";
|
||||||
import {myUserVerify} from "@/api/system/userVerify";
|
import {myUserVerify} from "@/api/system/userVerify";
|
||||||
import {useShopInfo} from '@/hooks/useShopInfo';
|
import {useShopInfo} from '@/hooks/useShopInfo';
|
||||||
|
import {useUser} from '@/hooks/useUser';
|
||||||
import {handleInviteRelation} from "@/utils/invite";
|
import {handleInviteRelation} from "@/utils/invite";
|
||||||
import {View, Text} from '@tarojs/components'
|
import {View, Text} from '@tarojs/components'
|
||||||
import MySearch from "./MySearch";
|
import MySearch from "./MySearch";
|
||||||
import './Header.scss';
|
import './Header.scss';
|
||||||
import navTo from "@/utils/common";
|
import navTo from "@/utils/common";
|
||||||
import {User} from "@/api/system/user/model";
|
|
||||||
|
|
||||||
const Header = (props: any) => {
|
const Header = (props: any) => {
|
||||||
// 使用新的useShopInfo Hook
|
// 使用新的useShopInfo Hook
|
||||||
const {
|
const {
|
||||||
getWebsiteName,
|
|
||||||
getWebsiteLogo
|
getWebsiteLogo
|
||||||
} = useShopInfo();
|
} = useShopInfo();
|
||||||
|
|
||||||
const [IsLogin, setIsLogin] = useState<boolean>(true)
|
// 使用useUser Hook管理用户状态
|
||||||
|
const {
|
||||||
|
user,
|
||||||
|
isLoggedIn,
|
||||||
|
loginUser,
|
||||||
|
fetchUserInfo
|
||||||
|
} = useUser();
|
||||||
|
|
||||||
const [statusBarHeight, setStatusBarHeight] = useState<number>()
|
const [statusBarHeight, setStatusBarHeight] = useState<number>()
|
||||||
const [userInfo, setUserInfo] = useState<User>({
|
|
||||||
avatar: '',
|
|
||||||
mobile: '未登录'
|
|
||||||
})
|
|
||||||
|
|
||||||
const reload = async () => {
|
const reload = async () => {
|
||||||
Taro.getSystemInfo({
|
Taro.getSystemInfo({
|
||||||
@@ -35,56 +37,58 @@ const Header = (props: any) => {
|
|||||||
setStatusBarHeight(res.statusBarHeight)
|
setStatusBarHeight(res.statusBarHeight)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
// 注意:商店信息现在通过useShopInfo自动管理,不需要手动获取
|
|
||||||
// 获取用户信息
|
// 如果已登录,获取最新用户信息
|
||||||
const data = await getUserInfo();
|
if (isLoggedIn) {
|
||||||
if (data) {
|
try {
|
||||||
setIsLogin(true);
|
const data = await fetchUserInfo();
|
||||||
console.log('用户信息>>>', data.phone)
|
if (data) {
|
||||||
// 保存userId
|
console.log('用户信息>>>', data.phone)
|
||||||
Taro.setStorageSync('UserId', data.userId)
|
// 获取openId
|
||||||
// 获取openId
|
if (!data.openid) {
|
||||||
if (!data.openid) {
|
Taro.login({
|
||||||
Taro.login({
|
success: (res) => {
|
||||||
success: (res) => {
|
getWxOpenId({code: res.code}).then(() => {
|
||||||
getWxOpenId({code: res.code}).then(() => {
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
// 是否已认证
|
||||||
}
|
if (data.certification) {
|
||||||
// 是否已认证
|
Taro.setStorageSync('Certification', '1')
|
||||||
if (data.certification) {
|
}
|
||||||
Taro.setStorageSync('Certification', '1')
|
// 机构ID
|
||||||
}
|
Taro.setStorageSync('OrganizationId', data.organizationId)
|
||||||
// 机构ID
|
// 父级机构ID
|
||||||
Taro.setStorageSync('OrganizationId', data.organizationId)
|
if (Number(data.organizationId) > 0) {
|
||||||
// 父级机构ID
|
getOrganization(Number(data.organizationId)).then(res => {
|
||||||
if (Number(data.organizationId) > 0) {
|
Taro.setStorageSync('OrganizationParentId', res.parentId)
|
||||||
getOrganization(Number(data.organizationId)).then(res => {
|
})
|
||||||
Taro.setStorageSync('OrganizationParentId', res.parentId)
|
}
|
||||||
})
|
// 管理员
|
||||||
}
|
const isKdy = data.roles?.findIndex(item => item.roleCode == 'admin')
|
||||||
// 管理员
|
if (isKdy != -1) {
|
||||||
const isKdy = data.roles?.findIndex(item => item.roleCode == 'admin')
|
Taro.setStorageSync('RoleName', '管理')
|
||||||
if (isKdy != -1) {
|
Taro.setStorageSync('RoleCode', 'admin')
|
||||||
Taro.setStorageSync('RoleName', '管理')
|
return false;
|
||||||
Taro.setStorageSync('RoleCode', 'admin')
|
}
|
||||||
return false;
|
// 注册用户
|
||||||
}
|
const isUser = data.roles?.findIndex(item => item.roleCode == 'user')
|
||||||
// 注册用户
|
if (isUser != -1) {
|
||||||
const isUser = data.roles?.findIndex(item => item.roleCode == 'user')
|
Taro.setStorageSync('RoleName', '注册用户')
|
||||||
if (isUser != -1) {
|
Taro.setStorageSync('RoleCode', 'user')
|
||||||
Taro.setStorageSync('RoleName', '注册用户')
|
return false;
|
||||||
Taro.setStorageSync('RoleCode', 'user')
|
}
|
||||||
return false;
|
// 认证信息
|
||||||
}
|
myUserVerify({status: 1}).then(data => {
|
||||||
// 认证信息
|
if (data?.realName) {
|
||||||
myUserVerify({status: 1}).then(data => {
|
Taro.setStorageSync('RealName', data.realName)
|
||||||
if (data?.realName) {
|
}
|
||||||
Taro.setStorageSync('RealName', data.realName)
|
})
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
setUserInfo(data)
|
console.error('获取用户信息失败:', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,14 +124,16 @@ const Header = (props: any) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// 登录成功
|
// 登录成功
|
||||||
Taro.setStorageSync('access_token', res.data.data.access_token)
|
const token = res.data.data.access_token;
|
||||||
Taro.setStorageSync('UserId', res.data.data.user.userId)
|
const userData = res.data.data.user;
|
||||||
setIsLogin(true)
|
|
||||||
|
// 使用useUser Hook的loginUser方法更新状态
|
||||||
|
loginUser(token, userData);
|
||||||
|
|
||||||
// 处理邀请关系
|
// 处理邀请关系
|
||||||
if (res.data.data.user?.userId) {
|
if (userData?.userId) {
|
||||||
try {
|
try {
|
||||||
const inviteSuccess = await handleInviteRelation(res.data.data.user.userId)
|
const inviteSuccess = await handleInviteRelation(userData.userId)
|
||||||
if (inviteSuccess) {
|
if (inviteSuccess) {
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: '邀请关系建立成功',
|
title: '邀请关系建立成功',
|
||||||
@@ -140,10 +146,16 @@ const Header = (props: any) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重新加载小程序
|
// 显示登录成功提示
|
||||||
Taro.reLaunch({
|
Taro.showToast({
|
||||||
url: '/pages/index/index'
|
title: '登录成功',
|
||||||
|
icon: 'success',
|
||||||
|
duration: 1500
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 不需要重新启动小程序,状态已经通过useUser更新
|
||||||
|
// 可以选择性地刷新当前页面数据
|
||||||
|
reload();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@@ -170,13 +182,13 @@ const Header = (props: any) => {
|
|||||||
onBackClick={() => {
|
onBackClick={() => {
|
||||||
}}
|
}}
|
||||||
left={
|
left={
|
||||||
IsLogin ? (
|
isLoggedIn ? (
|
||||||
<View style={{display: 'flex', alignItems: 'center', gap: '8px'}} onClick={() => navTo(`/user/profile/profile`,true)}>
|
<View style={{display: 'flex', alignItems: 'center', gap: '8px'}} onClick={() => navTo(`/user/profile/profile`,true)}>
|
||||||
<Avatar
|
<Avatar
|
||||||
size="22"
|
size="22"
|
||||||
src={userInfo?.avatar}
|
src={user?.avatar}
|
||||||
/>
|
/>
|
||||||
<Text className={'text-white'}>{userInfo?.mobile}</Text>
|
<Text className={'text-white'}>{user?.nickname || '已登录'}</Text>
|
||||||
<TriangleDown className={'text-white'} size={9}/>
|
<TriangleDown className={'text-white'} size={9}/>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
@@ -187,7 +199,7 @@ const Header = (props: any) => {
|
|||||||
size="22"
|
size="22"
|
||||||
src={getWebsiteLogo()}
|
src={getWebsiteLogo()}
|
||||||
/>
|
/>
|
||||||
<Text style={{color: '#ffffff'}}>{getWebsiteName()}</Text>
|
<Text style={{color: '#ffffff'}}>未登录</Text>
|
||||||
<TriangleDown size={9} className={'text-white'}/>
|
<TriangleDown size={9} className={'text-white'}/>
|
||||||
</Space>
|
</Space>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -14,12 +14,15 @@ import {useUserData} from "@/hooks/useUserData";
|
|||||||
|
|
||||||
function UserCard() {
|
function UserCard() {
|
||||||
const {
|
const {
|
||||||
isAdmin
|
user,
|
||||||
|
isLoggedIn,
|
||||||
|
loginUser,
|
||||||
|
fetchUserInfo,
|
||||||
|
isAdmin,
|
||||||
|
getDisplayName,
|
||||||
|
getRoleName
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const { data, refresh } = useUserData()
|
const { data, refresh } = useUserData()
|
||||||
const {getDisplayName, getRoleName} = useUser();
|
|
||||||
const [IsLogin, setIsLogin] = useState<boolean>(false)
|
|
||||||
const [userInfo, setUserInfo] = useState<User>()
|
|
||||||
const [couponCount, setCouponCount] = useState(0)
|
const [couponCount, setCouponCount] = useState(0)
|
||||||
const [pointsCount, setPointsCount] = useState(0)
|
const [pointsCount, setPointsCount] = useState(0)
|
||||||
const [giftCount, setGiftCount] = useState(0)
|
const [giftCount, setGiftCount] = useState(0)
|
||||||
@@ -77,41 +80,31 @@ function UserCard() {
|
|||||||
// })
|
// })
|
||||||
}
|
}
|
||||||
|
|
||||||
const reload = () => {
|
const reload = async () => {
|
||||||
Taro.getUserInfo({
|
// 如果已登录,获取最新用户信息
|
||||||
success: (res) => {
|
if (isLoggedIn) {
|
||||||
const avatar = res.userInfo.avatarUrl;
|
try {
|
||||||
setUserInfo({
|
const data = await fetchUserInfo();
|
||||||
avatar,
|
if (data) {
|
||||||
nickname: res.userInfo.nickName,
|
// 加载用户统计数据
|
||||||
sexName: res.userInfo.gender == 1 ? '男' : '女'
|
if (data.userId) {
|
||||||
})
|
loadUserStats(data.userId)
|
||||||
getUserInfo().then((data) => {
|
|
||||||
if (data) {
|
|
||||||
setUserInfo(data)
|
|
||||||
setIsLogin(true);
|
|
||||||
Taro.setStorageSync('UserId', data.userId)
|
|
||||||
|
|
||||||
// 加载用户统计数据
|
|
||||||
if (data.userId) {
|
|
||||||
loadUserStats(data.userId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取openId
|
|
||||||
if (!data.openid) {
|
|
||||||
Taro.login({
|
|
||||||
success: (res) => {
|
|
||||||
getWxOpenId({code: res.code}).then(() => {
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}).catch(() => {
|
|
||||||
console.log('未登录')
|
// 获取openId
|
||||||
});
|
if (!data.openid) {
|
||||||
|
Taro.login({
|
||||||
|
success: (res) => {
|
||||||
|
getWxOpenId({code: res.code}).then(() => {
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户信息失败:', error)
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const showAuthModal = () => {
|
const showAuthModal = () => {
|
||||||
@@ -180,10 +173,21 @@ function UserCard() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// 登录成功
|
// 登录成功
|
||||||
Taro.setStorageSync('access_token', res.data.data.access_token)
|
const token = res.data.data.access_token;
|
||||||
Taro.setStorageSync('UserId', res.data.data.user.userId)
|
const userData = res.data.data.user;
|
||||||
setUserInfo(res.data.data.user)
|
|
||||||
setIsLogin(true)
|
// 使用useUser Hook的loginUser方法更新状态
|
||||||
|
loginUser(token, userData);
|
||||||
|
|
||||||
|
// 显示登录成功提示
|
||||||
|
Taro.showToast({
|
||||||
|
title: '登录成功',
|
||||||
|
icon: 'success',
|
||||||
|
duration: 1500
|
||||||
|
})
|
||||||
|
|
||||||
|
// 刷新页面数据
|
||||||
|
reload();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@@ -209,17 +213,17 @@ function UserCard() {
|
|||||||
<View className={'user-card-header flex w-full justify-between items-center pt-4'}>
|
<View className={'user-card-header flex w-full justify-between items-center pt-4'}>
|
||||||
<View className={'flex items-center mx-4'}>
|
<View className={'flex items-center mx-4'}>
|
||||||
{
|
{
|
||||||
IsLogin ? (
|
isLoggedIn ? (
|
||||||
<Avatar size="large" src={userInfo?.avatar} shape="round"/>
|
<Avatar size="large" src={user?.avatar} shape="round"/>
|
||||||
) : (
|
) : (
|
||||||
<Button className={'text-black'} open-type="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber}>
|
<Button className={'text-black'} open-type="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber}>
|
||||||
<Avatar size="large" src={userInfo?.avatar} shape="round"/>
|
<Avatar size="large" src={user?.avatar} shape="round"/>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
<View className={'user-info flex flex-col px-2'}>
|
<View className={'user-info flex flex-col px-2'}>
|
||||||
<View className={'py-1 text-black font-bold'}>{getDisplayName()}</View>
|
<View className={'py-1 text-black font-bold'}>{getDisplayName()}</View>
|
||||||
{IsLogin ? (
|
{isLoggedIn ? (
|
||||||
<View className={'grade text-xs py-1'}>
|
<View className={'grade text-xs py-1'}>
|
||||||
<Tag type="success" round>
|
<Tag type="success" round>
|
||||||
<Text className={'p-1'}>
|
<Text className={'p-1'}>
|
||||||
|
|||||||
Reference in New Issue
Block a user