Browse Source
- 新增 useUser Hook 用于全局用户状态管理 - 更新 UserCard 和 UserCell 组件,集成 useUser 功能 - 添加 UserProfile 组件示例 - 更新 API 引用,统一使用 useUsermaster
6 changed files with 666 additions and 31 deletions
@ -0,0 +1,277 @@ |
|||
# useUser Hook 使用指南 |
|||
|
|||
## 概述 |
|||
|
|||
`useUser` hook 是一个用于管理用户状态的自定义 React Hook,类似于项目中的 `useCart` hook。它提供了用户登录状态管理、用户信息获取和更新、权限检查等功能,方便在整个应用中全局调用。 |
|||
|
|||
## 功能特性 |
|||
|
|||
- ✅ 用户登录状态管理 |
|||
- ✅ 用户信息本地存储和同步 |
|||
- ✅ 从服务器获取最新用户信息 |
|||
- ✅ 用户信息更新 |
|||
- ✅ 权限和角色检查 |
|||
- ✅ 实名认证状态检查 |
|||
- ✅ 用户余额和积分获取 |
|||
- ✅ 自动处理登录过期 |
|||
|
|||
## 基本用法 |
|||
|
|||
### 1. 导入 Hook |
|||
|
|||
```typescript |
|||
import { useUser } from '@/hooks/useUser'; |
|||
``` |
|||
|
|||
### 2. 在组件中使用 |
|||
|
|||
```typescript |
|||
const MyComponent = () => { |
|||
const { |
|||
user, // 用户信息 |
|||
isLoggedIn, // 是否已登录 |
|||
loading, // 加载状态 |
|||
loginUser, // 登录方法 |
|||
logoutUser, // 退出登录方法 |
|||
fetchUserInfo, // 获取用户信息 |
|||
updateUser, // 更新用户信息 |
|||
getDisplayName, // 获取显示名称 |
|||
isCertified, // 是否已实名认证 |
|||
getBalance, // 获取余额 |
|||
getPoints // 获取积分 |
|||
} = useUser(); |
|||
|
|||
// 使用用户信息 |
|||
if (loading) { |
|||
return <div>加载中...</div>; |
|||
} |
|||
|
|||
if (!isLoggedIn) { |
|||
return <div>请先登录</div>; |
|||
} |
|||
|
|||
return ( |
|||
<div> |
|||
<h1>欢迎,{getDisplayName()}</h1> |
|||
<p>余额:¥{getBalance()}</p> |
|||
<p>积分:{getPoints()}</p> |
|||
{isCertified() && <span>已实名认证</span>} |
|||
</div> |
|||
); |
|||
}; |
|||
``` |
|||
|
|||
## API 参考 |
|||
|
|||
### 状态属性 |
|||
|
|||
| 属性 | 类型 | 描述 | |
|||
|------|------|------| |
|||
| `user` | `User \| null` | 当前用户信息 | |
|||
| `isLoggedIn` | `boolean` | 用户是否已登录 | |
|||
| `loading` | `boolean` | 是否正在加载 | |
|||
|
|||
### 方法 |
|||
|
|||
#### `loginUser(token: string, userInfo: User)` |
|||
用户登录,保存用户信息和 token 到本地存储。 |
|||
|
|||
```typescript |
|||
const handleLogin = async () => { |
|||
const { access_token, user } = await loginApi(credentials); |
|||
loginUser(access_token, user); |
|||
}; |
|||
``` |
|||
|
|||
#### `logoutUser()` |
|||
用户退出登录,清除本地存储的用户信息。 |
|||
|
|||
```typescript |
|||
const handleLogout = () => { |
|||
logoutUser(); |
|||
// 跳转到首页或登录页 |
|||
}; |
|||
``` |
|||
|
|||
#### `fetchUserInfo()` |
|||
从服务器获取最新的用户信息。 |
|||
|
|||
```typescript |
|||
const refreshUserInfo = async () => { |
|||
const userInfo = await fetchUserInfo(); |
|||
console.log('最新用户信息:', userInfo); |
|||
}; |
|||
``` |
|||
|
|||
#### `updateUser(userData: Partial<User>)` |
|||
更新用户信息。 |
|||
|
|||
```typescript |
|||
const updateProfile = async () => { |
|||
try { |
|||
await updateUser({ |
|||
nickname: '新昵称', |
|||
avatar: 'new-avatar-url' |
|||
}); |
|||
console.log('更新成功'); |
|||
} catch (error) { |
|||
console.error('更新失败:', error); |
|||
} |
|||
}; |
|||
``` |
|||
|
|||
### 工具方法 |
|||
|
|||
#### `hasPermission(permission: string)` |
|||
检查用户是否有特定权限。 |
|||
|
|||
```typescript |
|||
if (hasPermission('user:edit')) { |
|||
// 显示编辑按钮 |
|||
} |
|||
``` |
|||
|
|||
#### `hasRole(roleCode: string)` |
|||
检查用户是否有特定角色。 |
|||
|
|||
```typescript |
|||
if (hasRole('admin')) { |
|||
// 显示管理员功能 |
|||
} |
|||
``` |
|||
|
|||
#### `getAvatarUrl()` |
|||
获取用户头像 URL。 |
|||
|
|||
```typescript |
|||
<Avatar src={getAvatarUrl()} /> |
|||
``` |
|||
|
|||
#### `getDisplayName()` |
|||
获取用户显示名称(优先级:昵称 > 真实姓名 > 用户名)。 |
|||
|
|||
```typescript |
|||
<span>欢迎,{getDisplayName()}</span> |
|||
``` |
|||
|
|||
#### `isCertified()` |
|||
检查用户是否已实名认证。 |
|||
|
|||
```typescript |
|||
{isCertified() && <Badge>已认证</Badge>} |
|||
``` |
|||
|
|||
#### `getBalance()` |
|||
获取用户余额。 |
|||
|
|||
```typescript |
|||
<span>余额:¥{getBalance()}</span> |
|||
``` |
|||
|
|||
#### `getPoints()` |
|||
获取用户积分。 |
|||
|
|||
```typescript |
|||
<span>积分:{getPoints()}</span> |
|||
``` |
|||
|
|||
## 使用场景 |
|||
|
|||
### 1. 用户资料页面 |
|||
|
|||
```typescript |
|||
const UserProfile = () => { |
|||
const { user, updateUser, getDisplayName, getAvatarUrl } = useUser(); |
|||
|
|||
const handleUpdateProfile = async (formData) => { |
|||
await updateUser(formData); |
|||
}; |
|||
|
|||
return ( |
|||
<div> |
|||
<Avatar src={getAvatarUrl()} /> |
|||
<h2>{getDisplayName()}</h2> |
|||
<ProfileForm onSubmit={handleUpdateProfile} /> |
|||
</div> |
|||
); |
|||
}; |
|||
``` |
|||
|
|||
### 2. 权限控制 |
|||
|
|||
```typescript |
|||
const AdminPanel = () => { |
|||
const { hasRole, hasPermission } = useUser(); |
|||
|
|||
if (!hasRole('admin')) { |
|||
return <div>无权限访问</div>; |
|||
} |
|||
|
|||
return ( |
|||
<div> |
|||
{hasPermission('user:delete') && ( |
|||
<Button danger>删除用户</Button> |
|||
)} |
|||
</div> |
|||
); |
|||
}; |
|||
``` |
|||
|
|||
### 3. 登录状态检查 |
|||
|
|||
```typescript |
|||
const ProtectedComponent = () => { |
|||
const { isLoggedIn, loading } = useUser(); |
|||
|
|||
if (loading) return <Loading />; |
|||
|
|||
if (!isLoggedIn) { |
|||
return <LoginPrompt />; |
|||
} |
|||
|
|||
return <ProtectedContent />; |
|||
}; |
|||
``` |
|||
|
|||
### 4. 用户余额显示 |
|||
|
|||
```typescript |
|||
const WalletCard = () => { |
|||
const { getBalance, getPoints, fetchUserInfo } = useUser(); |
|||
|
|||
const refreshBalance = () => { |
|||
fetchUserInfo(); // 刷新用户信息包括余额 |
|||
}; |
|||
|
|||
return ( |
|||
<div> |
|||
<div>余额:¥{getBalance()}</div> |
|||
<div>积分:{getPoints()}</div> |
|||
<Button onClick={refreshBalance}>刷新</Button> |
|||
</div> |
|||
); |
|||
}; |
|||
``` |
|||
|
|||
## 注意事项 |
|||
|
|||
1. **自动登录过期处理**:当 API 返回 401 错误时,hook 会自动清除登录状态。 |
|||
|
|||
2. **本地存储同步**:用户信息会自动同步到本地存储,页面刷新后状态会保持。 |
|||
|
|||
3. **错误处理**:所有异步操作都包含错误处理,失败时会显示相应的提示信息。 |
|||
|
|||
4. **性能优化**:用户信息只在必要时从服务器获取,避免不必要的网络请求。 |
|||
|
|||
## 与 useCart 的对比 |
|||
|
|||
| 特性 | useCart | useUser | |
|||
|------|---------|---------| |
|||
| 数据存储 | 购物车商品 | 用户信息 | |
|||
| 本地持久化 | ✅ | ✅ | |
|||
| 服务器同步 | ❌ | ✅ | |
|||
| 状态管理 | ✅ | ✅ | |
|||
| 全局访问 | ✅ | ✅ | |
|||
| 权限控制 | ❌ | ✅ | |
|||
|
|||
这样,用户信息管理就像购物车一样方便了,可以在任何组件中轻松访问和操作用户状态! |
@ -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; |
@ -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 |
|||
}; |
|||
}; |
Loading…
Reference in new issue