auth.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  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. tenantId: Number(process.env.TARO_APP_TENANT_ID) || 1
  64. }
  65. })
  66. if (response.status === 200) {
  67. const { token, user } = await response.json()
  68. Taro.setStorageSync('mini_token', token)
  69. Taro.setStorageSync('userInfo', JSON.stringify(user))
  70. return user
  71. }
  72. return null // 静默登录失败
  73. } catch (error) {
  74. // 静默登录失败不抛出错误,不影响用户体验
  75. console.debug('静默登录失败:', error)
  76. return null
  77. }
  78. },
  79. onSuccess: (user) => {
  80. if (user) {
  81. queryClient.setQueryData(['currentUser'], user)
  82. }
  83. }
  84. })
  85. // 在小程序启动时执行静默登录,刷新token和sessionKey
  86. useLaunch(() => {
  87. silentLoginMutation.mutate()
  88. })
  89. const loginMutation = useMutation<User, Error, LoginRequest>({
  90. mutationFn: async (data) => {
  91. const response = await authClient.login.$post({ json: data })
  92. if (response.status !== 200) {
  93. throw new Error('登录失败')
  94. }
  95. const { token, user } = await response.json()
  96. Taro.setStorageSync('mini_token', token)
  97. Taro.setStorageSync('userInfo', JSON.stringify(user))
  98. return user
  99. },
  100. onSuccess: (newUser) => {
  101. queryClient.setQueryData(['currentUser'], newUser)
  102. },
  103. onError: (error) => {
  104. Taro.showToast({
  105. title: error.message || '登录失败,请检查用户名和密码',
  106. icon: 'none',
  107. duration: 2000,
  108. })
  109. },
  110. })
  111. const registerMutation = useMutation<User, Error, RegisterRequest>({
  112. mutationFn: async (data) => {
  113. const response = await authClient.register.$post({ json: data })
  114. if (response.status !== 201) {
  115. throw new Error('注册失败')
  116. }
  117. const { token, user } = await response.json()
  118. Taro.setStorageSync('mini_token', token)
  119. Taro.setStorageSync('userInfo', JSON.stringify(user))
  120. return user
  121. },
  122. onSuccess: (newUser) => {
  123. queryClient.setQueryData(['currentUser'], newUser)
  124. },
  125. onError: (error) => {
  126. Taro.showToast({
  127. title: error.message || '注册失败,请重试',
  128. icon: 'none',
  129. duration: 2000,
  130. })
  131. },
  132. })
  133. const logoutMutation = useMutation<void, Error>({
  134. mutationFn: async () => {
  135. try {
  136. const response = await authClient.logout.$post({})
  137. if (response.status !== 200) {
  138. throw new Error('登出失败')
  139. }
  140. } catch (error) {
  141. console.error('Logout error:', error)
  142. } finally {
  143. Taro.removeStorageSync('mini_token')
  144. Taro.removeStorageSync('userInfo')
  145. }
  146. },
  147. onSuccess: () => {
  148. queryClient.setQueryData(['currentUser'], null)
  149. Taro.redirectTo({ url: '/pages/login/index' })
  150. },
  151. onError: (error) => {
  152. Taro.showToast({
  153. title: error.message || '登出失败',
  154. icon: 'none',
  155. duration: 2000,
  156. })
  157. },
  158. })
  159. const updateUserMutation = useMutation<User, Error, Partial<User>>({
  160. mutationFn: async (userData) => {
  161. const response = await authClient.me.$put({ json: userData })
  162. if (response.status !== 200) {
  163. throw new Error('更新用户信息失败')
  164. }
  165. const updatedUser = await response.json()
  166. Taro.setStorageSync('userInfo', JSON.stringify(updatedUser))
  167. return updatedUser
  168. },
  169. onSuccess: (updatedUser) => {
  170. queryClient.setQueryData(['currentUser'], updatedUser)
  171. Taro.showToast({
  172. title: '更新成功',
  173. icon: 'success',
  174. duration: 2000,
  175. })
  176. },
  177. onError: (error) => {
  178. Taro.showToast({
  179. title: error.message || '更新失败,请重试',
  180. icon: 'none',
  181. duration: 2000,
  182. })
  183. },
  184. })
  185. const updateUser = updateUserMutation.mutateAsync
  186. // // 使用React Query获取用户信息
  187. // const { data: user } = useQuery<User | null>({
  188. // queryKey: ['currentUser'],
  189. // queryFn: async () => {
  190. // // 直接从本地存储获取用户信息
  191. // const userInfoStr = Taro.getStorageSync('userInfo')
  192. // if (userInfoStr) {
  193. // try {
  194. // return JSON.parse(userInfoStr)
  195. // } catch {
  196. // return null
  197. // }
  198. // }
  199. // return null
  200. // },
  201. // staleTime: Infinity, // 用户信息不常变动,设为无限期
  202. // refetchOnWindowFocus: false, // 失去焦点不重新获取
  203. // refetchOnReconnect: false, // 网络重连不重新获取
  204. // enabled: false, // 不自动执行,由静默登录和登录mutation更新
  205. // })
  206. const value = {
  207. user: user || null,
  208. login: loginMutation.mutateAsync,
  209. logout: logoutMutation.mutateAsync,
  210. register: registerMutation.mutateAsync,
  211. updateUser,
  212. isLoading: loginMutation.isPending || registerMutation.isPending || logoutMutation.isPending || silentLoginMutation.isPending,
  213. isLoggedIn: !!user,
  214. }
  215. return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
  216. }
  217. export const useAuth = () => {
  218. const context = useContext(AuthContext)
  219. if (context === undefined) {
  220. throw new Error('useAuth must be used within an AuthProvider')
  221. }
  222. return context
  223. }
  224. export { queryClient }