auth.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. import { createContext, useContext, PropsWithChildren } from 'react'
  2. import Taro, { useLaunch } from '@tarojs/taro'
  3. import { authClient } from '../api'
  4. import { InferResponseType, InferRequestType } from 'hono'
  5. import { QueryClient, useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
  6. // 用户类型定义
  7. export type User = InferResponseType<typeof authClient.me.$get, 200>
  8. export type MiniLoginUser = InferResponseType<typeof authClient['mini-login']['$post'], 200>['user']
  9. type LoginRequest = InferRequestType<typeof authClient.login.$post>['json']
  10. type RegisterRequest = InferRequestType<typeof authClient.register.$post>['json']
  11. interface AuthContextType {
  12. user: User | null
  13. login: (data: LoginRequest) => Promise<User>
  14. logout: () => Promise<void>
  15. register: (data: RegisterRequest) => Promise<User>
  16. updateUser: (userData: Partial<User>) => void
  17. isLoading: boolean
  18. isLoggedIn: boolean
  19. }
  20. const AuthContext = createContext<AuthContextType | undefined>(undefined)
  21. const queryClient = new QueryClient()
  22. export const AuthProvider: React.FC<PropsWithChildren> = ({ children }) => {
  23. const queryClient = useQueryClient()
  24. const { data: user, isLoading } = useQuery<User | null, Error>({
  25. queryKey: ['currentUser'],
  26. queryFn: async () => {
  27. const token = Taro.getStorageSync('mini_token')
  28. if (!token) {
  29. return null
  30. }
  31. try {
  32. const response = await authClient.me.$get({})
  33. if (response.status !== 200) {
  34. throw new Error('获取用户信息失败')
  35. }
  36. const user = await response.json()
  37. Taro.setStorageSync('userInfo', JSON.stringify(user))
  38. return user
  39. } catch (error) {
  40. Taro.removeStorageSync('mini_token')
  41. Taro.removeStorageSync('userInfo')
  42. return null
  43. }
  44. },
  45. staleTime: Infinity, // 用户信息不常变动,设为无限期
  46. refetchOnWindowFocus: false, // 失去焦点不重新获取
  47. refetchOnReconnect: false, // 网络重连不重新获取
  48. })
  49. // 静默登录mutation - 应用启动时自动尝试登录
  50. const silentLoginMutation = useMutation<MiniLoginUser | null, Error, void>({
  51. mutationFn: async () => {
  52. try {
  53. // 尝试静默登录
  54. const loginRes = await Taro.login()
  55. if (!loginRes.code) {
  56. return null // 静默登录失败,但不抛出错误
  57. }
  58. // 使用小程序code进行静默登录
  59. const response = await authClient['mini-login'].$post({
  60. json: {
  61. code: loginRes.code
  62. // 静默登录不请求用户信息
  63. }
  64. })
  65. if (response.status === 200) {
  66. const { token, user } = await response.json()
  67. Taro.setStorageSync('mini_token', token)
  68. Taro.setStorageSync('userInfo', JSON.stringify(user))
  69. return user
  70. }
  71. return null // 静默登录失败
  72. } catch (error) {
  73. // 静默登录失败不抛出错误,不影响用户体验
  74. console.debug('静默登录失败:', error)
  75. return null
  76. }
  77. },
  78. onSuccess: (user) => {
  79. if (user) {
  80. queryClient.setQueryData(['currentUser'], user)
  81. }
  82. }
  83. })
  84. // 在小程序启动时执行静默登录,刷新token和sessionKey
  85. useLaunch(() => {
  86. silentLoginMutation.mutate()
  87. })
  88. const loginMutation = useMutation<User, Error, LoginRequest>({
  89. mutationFn: async (data) => {
  90. const response = await authClient.login.$post({ json: data })
  91. if (response.status !== 200) {
  92. throw new Error('登录失败')
  93. }
  94. const { token, user } = await response.json()
  95. Taro.setStorageSync('mini_token', token)
  96. Taro.setStorageSync('userInfo', JSON.stringify(user))
  97. return user
  98. },
  99. onSuccess: (newUser) => {
  100. queryClient.setQueryData(['currentUser'], newUser)
  101. },
  102. onError: (error) => {
  103. Taro.showToast({
  104. title: error.message || '登录失败,请检查用户名和密码',
  105. icon: 'none',
  106. duration: 2000,
  107. })
  108. },
  109. })
  110. const registerMutation = useMutation<User, Error, RegisterRequest>({
  111. mutationFn: async (data) => {
  112. const response = await authClient.register.$post({ json: data })
  113. if (response.status !== 201) {
  114. throw new Error('注册失败')
  115. }
  116. const { token, user } = await response.json()
  117. Taro.setStorageSync('mini_token', token)
  118. Taro.setStorageSync('userInfo', JSON.stringify(user))
  119. return user
  120. },
  121. onSuccess: (newUser) => {
  122. queryClient.setQueryData(['currentUser'], newUser)
  123. },
  124. onError: (error) => {
  125. Taro.showToast({
  126. title: error.message || '注册失败,请重试',
  127. icon: 'none',
  128. duration: 2000,
  129. })
  130. },
  131. })
  132. const logoutMutation = useMutation<void, Error>({
  133. mutationFn: async () => {
  134. try {
  135. const response = await authClient.logout.$post({})
  136. if (response.status !== 200) {
  137. throw new Error('登出失败')
  138. }
  139. } catch (error) {
  140. console.error('Logout error:', error)
  141. } finally {
  142. Taro.removeStorageSync('mini_token')
  143. Taro.removeStorageSync('userInfo')
  144. }
  145. },
  146. onSuccess: () => {
  147. queryClient.setQueryData(['currentUser'], null)
  148. Taro.redirectTo({ url: '/pages/login/index' })
  149. },
  150. onError: (error) => {
  151. Taro.showToast({
  152. title: error.message || '登出失败',
  153. icon: 'none',
  154. duration: 2000,
  155. })
  156. },
  157. })
  158. const updateUserMutation = useMutation<User, Error, Partial<User>>({
  159. mutationFn: async (userData) => {
  160. const response = await authClient.me.$put({ json: userData })
  161. if (response.status !== 200) {
  162. throw new Error('更新用户信息失败')
  163. }
  164. const updatedUser = await response.json()
  165. Taro.setStorageSync('userInfo', JSON.stringify(updatedUser))
  166. return updatedUser
  167. },
  168. onSuccess: (updatedUser) => {
  169. queryClient.setQueryData(['currentUser'], updatedUser)
  170. Taro.showToast({
  171. title: '更新成功',
  172. icon: 'success',
  173. duration: 2000,
  174. })
  175. },
  176. onError: (error) => {
  177. Taro.showToast({
  178. title: error.message || '更新失败,请重试',
  179. icon: 'none',
  180. duration: 2000,
  181. })
  182. },
  183. })
  184. const updateUser = updateUserMutation.mutateAsync
  185. // // 使用React Query获取用户信息
  186. // const { data: user } = useQuery<User | null>({
  187. // queryKey: ['currentUser'],
  188. // queryFn: async () => {
  189. // // 直接从本地存储获取用户信息
  190. // const userInfoStr = Taro.getStorageSync('userInfo')
  191. // if (userInfoStr) {
  192. // try {
  193. // return JSON.parse(userInfoStr)
  194. // } catch {
  195. // return null
  196. // }
  197. // }
  198. // return null
  199. // },
  200. // staleTime: Infinity, // 用户信息不常变动,设为无限期
  201. // refetchOnWindowFocus: false, // 失去焦点不重新获取
  202. // refetchOnReconnect: false, // 网络重连不重新获取
  203. // enabled: false, // 不自动执行,由静默登录和登录mutation更新
  204. // })
  205. const value = {
  206. user: user || null,
  207. login: loginMutation.mutateAsync,
  208. logout: logoutMutation.mutateAsync,
  209. register: registerMutation.mutateAsync,
  210. updateUser,
  211. isLoading: loginMutation.isPending || registerMutation.isPending || logoutMutation.isPending || silentLoginMutation.isPending,
  212. isLoggedIn: !!user,
  213. }
  214. return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
  215. }
  216. export const useAuth = () => {
  217. const context = useContext(AuthContext)
  218. if (context === undefined) {
  219. throw new Error('useAuth must be used within an AuthProvider')
  220. }
  221. return context
  222. }
  223. export { queryClient }