useAuth.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'
  2. import Taro from '@tarojs/taro'
  3. import { QueryClient, useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
  4. import { talentAuthClient } from '../api'
  5. import type { InferResponseType } from 'hono'
  6. // 使用RPC类型推断,确保类型与后端API完全一致
  7. export type TalentUserInfo = InferResponseType<typeof talentAuthClient.me.$get, 200>
  8. export interface AuthContextType {
  9. isLoggedIn: boolean
  10. user: TalentUserInfo | null
  11. token: string | null
  12. login: (identifier: string, password: string) => Promise<void>
  13. logout: () => Promise<void>
  14. refreshUser: () => Promise<void>
  15. loading: boolean
  16. }
  17. const AuthContext = createContext<AuthContextType | undefined>(undefined)
  18. const TOKEN_KEY = 'talent_token'
  19. const USER_KEY = 'talent_user'
  20. // 导出queryClient以供外部使用(与mini-enterprise-auth-ui保持一致)
  21. export const queryClient = new QueryClient()
  22. export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
  23. const queryClient = useQueryClient()
  24. const [isLoggedIn, setIsLoggedIn] = useState(false)
  25. const [token, setToken] = useState<string | null>(null)
  26. const [loading, setLoading] = useState(true)
  27. // 使用React Query管理用户状态
  28. const { data: user, isLoading } = useQuery<TalentUserInfo | null, Error>({
  29. queryKey: ['talentCurrentUser'],
  30. queryFn: async () => {
  31. const storedToken = Taro.getStorageSync(TOKEN_KEY)
  32. if (!storedToken) {
  33. return null
  34. }
  35. try {
  36. const response = await talentAuthClient.me.$get()
  37. if (response.status !== 200) {
  38. throw new Error('获取用户信息失败')
  39. }
  40. // 使用 response.json() 解析数据
  41. const userInfo = await response.json()
  42. // 缓存到本地存储(序列化为JSON字符串)
  43. Taro.setStorageSync(USER_KEY, JSON.stringify(userInfo))
  44. return userInfo
  45. } catch (error) {
  46. console.error('获取用户信息失败:', error)
  47. Taro.removeStorageSync(TOKEN_KEY)
  48. Taro.removeStorageSync(USER_KEY)
  49. return null
  50. }
  51. },
  52. staleTime: Infinity,
  53. refetchOnWindowFocus: false,
  54. refetchOnReconnect: false,
  55. })
  56. // 同步isLoggedIn状态
  57. useEffect(() => {
  58. setIsLoggedIn(!!user)
  59. setToken(Taro.getStorageSync(TOKEN_KEY) || null)
  60. setLoading(false)
  61. }, [user])
  62. const loginMutation = useMutation<void, Error, { identifier: string; password: string }>({
  63. mutationFn: async ({ identifier, password }) => {
  64. const response = await talentAuthClient.login.$post({
  65. json: {
  66. identifier: identifier.trim(),
  67. password: password.trim()
  68. }
  69. })
  70. if (response.status !== 200) {
  71. throw new Error('登录失败')
  72. }
  73. // 使用 response.json() 解析数据
  74. const data = await response.json()
  75. const { token, user } = data
  76. // 保存到本地存储(user序列化为JSON字符串)
  77. Taro.setStorageSync(TOKEN_KEY, token)
  78. Taro.setStorageSync(USER_KEY, JSON.stringify(user))
  79. setToken(token)
  80. },
  81. onSuccess: () => {
  82. // 刷新用户信息
  83. queryClient.invalidateQueries({ queryKey: ['talentCurrentUser'] })
  84. },
  85. onError: (error) => {
  86. Taro.showToast({
  87. title: error.message || '登录失败,请检查账号和密码',
  88. icon: 'none',
  89. duration: 2000,
  90. })
  91. },
  92. })
  93. const logoutMutation = useMutation<void, Error>({
  94. mutationFn: async () => {
  95. try {
  96. // TODO: 调用退出API
  97. // await talentAuthClient.logout.$post()
  98. } catch (error) {
  99. console.error('Logout error:', error)
  100. } finally {
  101. Taro.removeStorageSync(TOKEN_KEY)
  102. Taro.removeStorageSync(USER_KEY)
  103. }
  104. },
  105. onSuccess: () => {
  106. queryClient.setQueryData(['talentCurrentUser'], null)
  107. Taro.reLaunch({ url: '/pages/login/index' })
  108. },
  109. onError: (error) => {
  110. Taro.showToast({
  111. title: error.message || '登出失败',
  112. icon: 'none',
  113. duration: 2000,
  114. })
  115. },
  116. })
  117. const login = async (identifier: string, password: string): Promise<void> => {
  118. await loginMutation.mutateAsync({ identifier, password })
  119. }
  120. const logout = async (): Promise<void> => {
  121. await logoutMutation.mutateAsync()
  122. }
  123. const refreshUser = async (): Promise<void> => {
  124. await queryClient.invalidateQueries({ queryKey: ['talentCurrentUser'] })
  125. }
  126. const value: AuthContextType = {
  127. isLoggedIn: !!user,
  128. user: user || null,
  129. token,
  130. login,
  131. logout,
  132. refreshUser,
  133. loading: loading || isLoading,
  134. }
  135. return (
  136. <AuthContext.Provider value={value}>
  137. {children}
  138. </AuthContext.Provider>
  139. )
  140. }
  141. export const useAuth = (): AuthContextType => {
  142. const context = useContext(AuthContext)
  143. if (context === undefined) {
  144. throw new Error('useAuth must be used within an AuthProvider')
  145. }
  146. return context
  147. }
  148. /**
  149. * useRequireAuth Hook
  150. * 检查登录状态,未登录则重定向到登录页
  151. */
  152. export const useRequireAuth = (): void => {
  153. const { isLoggedIn } = useAuth()
  154. useEffect(() => {
  155. if (!isLoggedIn) {
  156. Taro.reLaunch({ url: '/pages/login/index' })
  157. }
  158. }, [isLoggedIn])
  159. }
  160. export default AuthContext