diff --git a/docs/useUser-hook-guide.md b/docs/useUser-hook-guide.md new file mode 100644 index 0000000..b8cdbef --- /dev/null +++ b/docs/useUser-hook-guide.md @@ -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
加载中...
; + } + + if (!isLoggedIn) { + return
请先登录
; + } + + return ( +
+

欢迎,{getDisplayName()}

+

余额:¥{getBalance()}

+

积分:{getPoints()}

+ {isCertified() && 已实名认证} +
+ ); +}; +``` + +## 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)` +更新用户信息。 + +```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 + +``` + +#### `getDisplayName()` +获取用户显示名称(优先级:昵称 > 真实姓名 > 用户名)。 + +```typescript +欢迎,{getDisplayName()} +``` + +#### `isCertified()` +检查用户是否已实名认证。 + +```typescript +{isCertified() && 已认证} +``` + +#### `getBalance()` +获取用户余额。 + +```typescript +余额:¥{getBalance()} +``` + +#### `getPoints()` +获取用户积分。 + +```typescript +积分:{getPoints()} +``` + +## 使用场景 + +### 1. 用户资料页面 + +```typescript +const UserProfile = () => { + const { user, updateUser, getDisplayName, getAvatarUrl } = useUser(); + + const handleUpdateProfile = async (formData) => { + await updateUser(formData); + }; + + return ( +
+ +

{getDisplayName()}

+ +
+ ); +}; +``` + +### 2. 权限控制 + +```typescript +const AdminPanel = () => { + const { hasRole, hasPermission } = useUser(); + + if (!hasRole('admin')) { + return
无权限访问
; + } + + return ( +
+ {hasPermission('user:delete') && ( + + )} +
+ ); +}; +``` + +### 3. 登录状态检查 + +```typescript +const ProtectedComponent = () => { + const { isLoggedIn, loading } = useUser(); + + if (loading) return ; + + if (!isLoggedIn) { + return ; + } + + return ; +}; +``` + +### 4. 用户余额显示 + +```typescript +const WalletCard = () => { + const { getBalance, getPoints, fetchUserInfo } = useUser(); + + const refreshBalance = () => { + fetchUserInfo(); // 刷新用户信息包括余额 + }; + + return ( +
+
余额:¥{getBalance()}
+
积分:{getPoints()}
+ +
+ ); +}; +``` + +## 注意事项 + +1. **自动登录过期处理**:当 API 返回 401 错误时,hook 会自动清除登录状态。 + +2. **本地存储同步**:用户信息会自动同步到本地存储,页面刷新后状态会保持。 + +3. **错误处理**:所有异步操作都包含错误处理,失败时会显示相应的提示信息。 + +4. **性能优化**:用户信息只在必要时从服务器获取,避免不必要的网络请求。 + +## 与 useCart 的对比 + +| 特性 | useCart | useUser | +|------|---------|---------| +| 数据存储 | 购物车商品 | 用户信息 | +| 本地持久化 | ✅ | ✅ | +| 服务器同步 | ❌ | ✅ | +| 状态管理 | ✅ | ✅ | +| 全局访问 | ✅ | ✅ | +| 权限控制 | ❌ | ✅ | + +这样,用户信息管理就像购物车一样方便了,可以在任何组件中轻松访问和操作用户状态! diff --git a/src/api/cms/link/model/index.ts b/src/api/cms/link/model/index.ts index 40f5988..b22d9dc 100644 --- a/src/api/cms/link/model/index.ts +++ b/src/api/cms/link/model/index.ts @@ -1,4 +1,4 @@ -import type { PageParam } from '@/api'; +import type { PageParam } from '@/api/index'; /** * 链接 diff --git a/src/components/UserProfile.tsx b/src/components/UserProfile.tsx new file mode 100644 index 0000000..96c664f --- /dev/null +++ b/src/components/UserProfile.tsx @@ -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 ( + + 加载中... + + ); + } + + if (!isLoggedIn) { + return ( + + + 请先登录 + + + + ); + } + + return ( + + + + + {getDisplayName()} + ID: {user?.userId} + {isCertified() && ( + 已实名认证 + )} + + + + + + ¥{getBalance()} + 余额 + + + {getPoints()} + 积分 + + + + + + + + + ); +}; + +export default UserProfile; diff --git a/src/hooks/useUser.ts b/src/hooks/useUser.ts new file mode 100644 index 0000000..67f28bc --- /dev/null +++ b/src/hooks/useUser.ts @@ -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(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) => { + 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 + }; +}; diff --git a/src/pages/user/components/UserCard.tsx b/src/pages/user/components/UserCard.tsx index 0148c30..3e55627 100644 --- a/src/pages/user/components/UserCard.tsx +++ b/src/pages/user/components/UserCard.tsx @@ -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(false) const [userInfo, setUserInfo] = useState() - const [roleName, setRoleName] = useState('注册用户') 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() { ) }
-
{IsLogin ? userInfo?.nickname : '请点击头像登录'}
+
{getDisplayName()}
{IsLogin ? (
-
{roleName || '注册用户'}
+
+ {getRoleName()} +
) : ''} diff --git a/src/pages/user/components/UserCell.tsx b/src/pages/user/components/UserCell.tsx index 7890234..1991619 100644 --- a/src/pages/user/components/UserCell.tsx +++ b/src/pages/user/components/UserCell.tsx @@ -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 ( <> - navTo('/dealer/index', true)}> - - 开通会员 - 享优惠 - - } - extra={} - /> + + {/*是否分销商*/} + {!hasRole('dealer') && !isAdmin() && ( + navTo('/dealer/index', true)}> + + 开通会员 + 享优惠 + + } + extra={} + /> + )} + + {/*是否管理员*/} + {isAdmin() && ( + navTo('/admin/article/index', true)}> + + 管理中心 + + } + extra={} + /> + )} + 我的服务 @@ -81,8 +103,11 @@ const UserCell = () => { className="nutui-cell-clickable" title={ - + 实名认证 + {isCertified() && ( + 已认证 + )} } align="center"