feat(user): 实现 useUser Hook 并更新相关组件

- 新增 useUser Hook 用于全局用户状态管理
- 更新 UserCard 和 UserCell 组件,集成 useUser 功能
- 添加 UserProfile 组件示例
- 更新 API 引用,统一使用 useUser
This commit is contained in:
2025-08-14 18:08:00 +08:00
parent 2c864ce770
commit 745040d254
6 changed files with 666 additions and 31 deletions

View File

@@ -1,4 +1,4 @@
import type { PageParam } from '@/api';
import type { PageParam } from '@/api/index';
/**
* 链接

View File

@@ -0,0 +1,99 @@
import React from 'react';
import { View, Text, Image } from '@tarojs/components';
import { Button, Avatar } from '@nutui/nutui-react-taro';
import { useUser } from '@/hooks/useUser';
import navTo from '@/utils/common';
// 用户资料组件示例
const UserProfile: React.FC = () => {
const {
user,
isLoggedIn,
loading,
logoutUser,
fetchUserInfo,
getAvatarUrl,
getDisplayName,
isCertified,
getBalance,
getPoints
} = useUser();
// 处理登录跳转
const handleLogin = () => {
navTo('/pages/login/index');
};
// 处理退出登录
const handleLogout = () => {
logoutUser();
navTo('/pages/index/index');
};
// 刷新用户信息
const handleRefresh = async () => {
await fetchUserInfo();
};
if (loading) {
return (
<View className="user-profile loading">
<Text>...</Text>
</View>
);
}
if (!isLoggedIn) {
return (
<View className="user-profile not-logged-in">
<View className="login-prompt">
<Text></Text>
<Button type="primary" onClick={handleLogin}>
</Button>
</View>
</View>
);
}
return (
<View className="user-profile">
<View className="user-header">
<Avatar
size="large"
src={getAvatarUrl()}
alt={getDisplayName()}
/>
<View className="user-info">
<Text className="username">{getDisplayName()}</Text>
<Text className="user-id">ID: {user?.userId}</Text>
{isCertified() && (
<Text className="certified"></Text>
)}
</View>
</View>
<View className="user-stats">
<View className="stat-item">
<Text className="stat-value">¥{getBalance()}</Text>
<Text className="stat-label"></Text>
</View>
<View className="stat-item">
<Text className="stat-value">{getPoints()}</Text>
<Text className="stat-label"></Text>
</View>
</View>
<View className="user-actions">
<Button onClick={handleRefresh}>
</Button>
<Button type="danger" onClick={handleLogout}>
退
</Button>
</View>
</View>
);
};
export default UserProfile;

237
src/hooks/useUser.ts Normal file
View File

@@ -0,0 +1,237 @@
import { useState, useEffect } from 'react';
import Taro from '@tarojs/taro';
import { User } from '@/api/system/user/model';
import { getUserInfo, updateUserInfo } from '@/api/layout';
// 用户Hook
export const useUser = () => {
const [user, setUser] = useState<User | null>(null);
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [loading, setLoading] = useState(true);
// 从本地存储加载用户数据
const loadUserFromStorage = () => {
try {
const token = Taro.getStorageSync('access_token');
const userData = Taro.getStorageSync('User');
const userId = Taro.getStorageSync('UserId');
const tenantId = Taro.getStorageSync('TenantId');
if (token && userData) {
const userInfo = typeof userData === 'string' ? JSON.parse(userData) : userData;
setUser(userInfo);
setIsLoggedIn(true);
} else if (token && userId) {
// 如果有token和userId但没有完整用户信息标记为已登录但需要获取用户信息
setIsLoggedIn(true);
setUser({ userId, tenantId } as User);
} else {
setUser(null);
setIsLoggedIn(false);
}
} catch (error) {
console.error('加载用户数据失败:', error);
setUser(null);
setIsLoggedIn(false);
} finally {
setLoading(false);
}
};
// 保存用户数据到本地存储
const saveUserToStorage = (token: string, userInfo: User) => {
try {
Taro.setStorageSync('access_token', token);
Taro.setStorageSync('User', userInfo);
Taro.setStorageSync('UserId', userInfo.userId);
Taro.setStorageSync('TenantId', userInfo.tenantId);
Taro.setStorageSync('Phone', userInfo.phone);
} catch (error) {
console.error('保存用户数据失败:', error);
}
};
// 登录用户
const loginUser = (token: string, userInfo: User) => {
setUser(userInfo);
setIsLoggedIn(true);
saveUserToStorage(token, userInfo);
};
// 退出登录
const logoutUser = () => {
setUser(null);
setIsLoggedIn(false);
// 清除本地存储
try {
Taro.removeStorageSync('access_token');
Taro.removeStorageSync('User');
Taro.removeStorageSync('UserId');
Taro.removeStorageSync('TenantId');
Taro.removeStorageSync('Phone');
Taro.removeStorageSync('userInfo');
} catch (error) {
console.error('清除用户数据失败:', error);
}
};
// 从服务器获取最新用户信息
const fetchUserInfo = async () => {
if (!isLoggedIn) {
return null;
}
try {
setLoading(true);
const userInfo = await getUserInfo();
setUser(userInfo);
// 更新本地存储
const token = Taro.getStorageSync('access_token');
if (token) {
saveUserToStorage(token, userInfo);
}
return userInfo;
} catch (error) {
console.error('获取用户信息失败:', error);
// 如果获取失败可能是token过期清除登录状态
if (error.message?.includes('401') || error.message?.includes('未授权')) {
logoutUser();
}
return null;
} finally {
setLoading(false);
}
};
// 更新用户信息
const updateUser = async (userData: Partial<User>) => {
if (!user) {
throw new Error('用户未登录');
}
try {
const updatedUser = { ...user, ...userData };
await updateUserInfo(updatedUser);
setUser(updatedUser);
// 更新本地存储
const token = Taro.getStorageSync('access_token');
if (token) {
saveUserToStorage(token, updatedUser);
}
Taro.showToast({
title: '更新成功',
icon: 'success',
duration: 1500
});
return updatedUser;
} catch (error) {
console.error('更新用户信息失败:', error);
Taro.showToast({
title: '更新失败',
icon: 'error',
duration: 1500
});
throw error;
}
};
// 检查是否有特定权限
const hasPermission = (permission: string) => {
if (!user || !user.authorities) {
return false;
}
return user.authorities.some(auth => auth.authority === permission);
};
// 检查是否有特定角色
const hasRole = (roleCode: string) => {
if (!user || !user.roles) {
return false;
}
return user.roles.some(role => role.roleCode === roleCode);
};
// 获取用户头像URL
const getAvatarUrl = () => {
return user?.avatar || user?.avatarUrl || '';
};
// 获取用户显示名称
const getDisplayName = () => {
return user?.nickname || user?.realName || user?.username || '未登录';
};
// 获取用户显示的角色(同步版本)
const getRoleName = () => {
if(hasRole('superAdmin')){
return '超级管理员';
}
if(hasRole('admin')){
return '管理员';
}
if(hasRole('staff')){
return '员工';
}
if(hasRole('vip')){
return 'VIP会员';
}
return '注册用户';
}
// 检查用户是否已实名认证
const isCertified = () => {
return user?.certification === true;
};
// 检查用户是否是管理员
const isAdmin = () => {
return user?.isAdmin === true;
};
// 获取用户余额
const getBalance = () => {
return user?.balance || 0;
};
// 获取用户积分
const getPoints = () => {
return user?.points || 0;
};
// 初始化时加载用户数据
useEffect(() => {
loadUserFromStorage();
}, []);
return {
// 状态
user,
isLoggedIn,
loading,
// 方法
loginUser,
logoutUser,
fetchUserInfo,
updateUser,
loadUserFromStorage,
// 工具方法
hasPermission,
hasRole,
getAvatarUrl,
getDisplayName,
getRoleName,
isCertified,
isAdmin,
getBalance,
getPoints
};
};

View File

@@ -8,16 +8,16 @@ import navTo from "@/utils/common";
import {TenantId} from "@/config/app";
import {getUserCouponCount} from "@/api/user/coupon";
import {getUserPointsStats} from "@/api/user/points";
import {useUser} from "@/hooks/useUser";
function UserCard() {
const {getDisplayName, getRoleName} = useUser();
const [IsLogin, setIsLogin] = useState<boolean>(false)
const [userInfo, setUserInfo] = useState<User>()
const [roleName, setRoleName] = useState<string>('注册用户')
const [couponCount, setCouponCount] = useState(0)
const [pointsCount, setPointsCount] = useState(0)
const [giftCount, setGiftCount] = useState(0)
useEffect(() => {
// Taro.getSetting获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
Taro.getSetting({
@@ -89,11 +89,6 @@ function UserCard() {
}
})
}
// 判断身份
const roleName = Taro.getStorageSync('RoleName');
if (roleName) {
setRoleName(roleName)
}
}
}).catch(() => {
console.log('未登录')
@@ -206,11 +201,13 @@ function UserCard() {
)
}
<div className={'user-info flex flex-col px-2'}>
<div className={'py-1 text-black font-bold'}>{IsLogin ? userInfo?.nickname : '请点击头像登录'}</div>
<div className={'py-1 text-black font-bold'}>{getDisplayName()}</div>
{IsLogin ? (
<div className={'grade text-xs py-1'}>
<Tag type="success" round>
<div className={'p-1'}>{roleName || '注册用户'}</div>
<div className={'p-1'}>
{getRoleName()}
</div>
</Tag>
</div>
) : ''}

View File

@@ -1,10 +1,12 @@
import {Cell} from '@nutui/nutui-react-taro'
import navTo from "@/utils/common";
import Taro from '@tarojs/taro'
import {View,Text} from '@tarojs/components'
import {ArrowRight, ShieldCheck, LogisticsError, Location, Reward, Tips, Ask} from '@nutui/icons-react-taro'
import {View, Text} from '@tarojs/components'
import {ArrowRight, ShieldCheck, LogisticsError, Location, Reward, Tips, Ask, Setting} from '@nutui/icons-react-taro'
import {useUser} from '@/hooks/useUser'
const UserCell = () => {
const {logoutUser, isCertified, hasRole, isAdmin} = useUser();
const onLogout = () => {
Taro.showModal({
@@ -12,10 +14,8 @@ const UserCell = () => {
content: '确定要退出登录吗?',
success: function (res) {
if (res.confirm) {
Taro.removeStorageSync('access_token')
Taro.removeStorageSync('TenantId')
Taro.removeStorageSync('UserId')
Taro.removeStorageSync('userInfo')
// 使用 useUser hook 的 logoutUser 方法
logoutUser();
Taro.reLaunch({
url: '/pages/index/index'
})
@@ -27,20 +27,42 @@ const UserCell = () => {
return (
<>
<View className={'px-4'}>
<Cell
className="nutui-cell-clickable"
style={{
backgroundImage: 'linear-gradient(to right bottom, #54a799, #177b73)',
}}
title={
<View style={{display: 'inline-flex', alignItems: 'center'}} onClick={() => navTo('/dealer/index', true)}>
<Reward className={'text-orange-100 '} size={16}/>
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}></Text>
<Text className={'text-white opacity-80 pl-3'}></Text>
</View>
}
extra={<ArrowRight color="#cccccc" size={18}/>}
/>
{/*是否分销商*/}
{!hasRole('dealer') && !isAdmin() && (
<Cell
className="nutui-cell-clickable"
style={{
backgroundImage: 'linear-gradient(to right bottom, #54a799, #177b73)',
}}
title={
<View style={{display: 'inline-flex', alignItems: 'center'}} onClick={() => navTo('/dealer/index', true)}>
<Reward className={'text-orange-100 '} size={16}/>
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}></Text>
<Text className={'text-white opacity-80 pl-3'}></Text>
</View>
}
extra={<ArrowRight color="#cccccc" size={18}/>}
/>
)}
{/*是否管理员*/}
{isAdmin() && (
<Cell
className="nutui-cell-clickable"
style={{
backgroundImage: 'linear-gradient(to right bottom, #ff8e0c, #ed680d)',
}}
title={
<View style={{display: 'inline-flex', alignItems: 'center'}} onClick={() => navTo('/admin/article/index', true)}>
<Setting className={'text-orange-100 '} size={16}/>
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}></Text>
</View>
}
extra={<ArrowRight color="#cccccc" size={18}/>}
/>
)}
<Cell.Group divider={true} description={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Text style={{marginTop: '12px'}}></Text>
@@ -81,8 +103,11 @@ const UserCell = () => {
className="nutui-cell-clickable"
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<ShieldCheck size={16}/>
<ShieldCheck size={16} color={isCertified() ? '#52c41a' : '#666'}/>
<Text className={'pl-3 text-sm'}></Text>
{isCertified() && (
<Text className={'pl-2 text-xs text-green-500'}></Text>
)}
</View>
}
align="center"