auth.tsx 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. import { createContext, useContext, PropsWithChildren, useState } from 'react'
  2. import Taro, { useLaunch } from '@tarojs/taro'
  3. import { authClient } from '../api'
  4. import { InferResponseType, InferRequestType } from 'hono'
  5. import { useMutation } from '@tanstack/react-query'
  6. import { isWeapp, isH5 } from './platform'
  7. // 用户类型定义
  8. export type User = InferResponseType<typeof authClient.me.$get, 200>
  9. export type MiniLoginUser = InferResponseType<typeof authClient['mini-login']['$post'], 200>['user']
  10. type LoginRequest = InferRequestType<typeof authClient.login.$post>['json']
  11. type RegisterRequest = InferRequestType<typeof authClient.register.$post>['json']
  12. interface AuthContextType {
  13. user: User | null
  14. login: (data: LoginRequest) => Promise<User>
  15. logout: () => Promise<void>
  16. register: (data: RegisterRequest) => Promise<User>
  17. updateUser: (userData: Partial<User>) => void
  18. refreshUser: () => Promise<User | null>
  19. setUser: (user: User | null) => void // 添加 setUser 方法类型
  20. isLoading: boolean
  21. isLoggedIn: boolean
  22. }
  23. const AuthContext = createContext<AuthContextType | undefined>(undefined)
  24. export const AuthProvider: React.FC<PropsWithChildren> = ({ children }) => {
  25. // 用户状态
  26. const [user, setUser] = useState<User | null>(() => {
  27. // 从本地存储初始化用户信息
  28. const userInfoStr = Taro.getStorageSync('userInfo')
  29. if (userInfoStr) {
  30. try {
  31. return JSON.parse(userInfoStr)
  32. } catch {
  33. return null
  34. }
  35. }
  36. return null
  37. })
  38. // 获取用户信息的通用函数
  39. const fetchUserInfo = async (): Promise<User | null> => {
  40. // const token = Taro.getStorageSync('mini_token')
  41. // if (!token) {
  42. // return null
  43. // }
  44. try {
  45. const response = await authClient.me.$get({})
  46. if (response.status !== 200) {
  47. throw new Error('获取用户信息失败')
  48. }
  49. const user = await response.json()
  50. Taro.setStorageSync('userInfo', JSON.stringify(user))
  51. return user
  52. } catch (error) {
  53. // Token失效时的处理逻辑
  54. console.debug('Token失效,开始处理重新登录逻辑')
  55. if (isWeapp()) {
  56. // 小程序环境:使用静默登录重新获取token
  57. console.debug('小程序环境,尝试静默登录')
  58. try {
  59. const loginRes = await Taro.login()
  60. if (loginRes.code) {
  61. const response = await authClient['mini-login'].$post({
  62. json: {
  63. code: loginRes.code,
  64. tenantId: Number(process.env.TARO_APP_TENANT_ID) || 1
  65. }
  66. })
  67. if (response.status === 200) {
  68. const { token: newToken } = await response.json()
  69. Taro.setStorageSync('mini_token', newToken)
  70. // 重新获取完整的用户信息
  71. const meResponse = await authClient.me.$get({})
  72. if (meResponse.status === 200) {
  73. const user = await meResponse.json()
  74. Taro.setStorageSync('userInfo', JSON.stringify(user))
  75. console.debug('静默登录成功,返回新用户信息')
  76. return user
  77. }
  78. }
  79. }
  80. } catch (silentLoginError) {
  81. console.debug('静默登录失败:', silentLoginError)
  82. }
  83. } else if (isH5()) {
  84. // H5环境:跳转到登录页
  85. console.debug('H5环境,跳转到登录页')
  86. Taro.redirectTo({ url: '/pages/login/index' })
  87. }
  88. // 如果重新登录失败或不是小程序环境,清除token并返回null
  89. Taro.removeStorageSync('mini_token')
  90. Taro.removeStorageSync('userInfo')
  91. return null
  92. }
  93. }
  94. // 静默登录mutation - 移除自动登录,改为需要用户主动同意
  95. // 保留此函数供需要时手动调用(如用户主动点击登录按钮)
  96. const silentLoginMutation = useMutation<User | null, Error, void>({
  97. mutationFn: async () => {
  98. // 使用统一的用户信息获取函数
  99. return await fetchUserInfo()
  100. },
  101. onSuccess: (user) => {
  102. if (user) {
  103. setUser(user)
  104. }
  105. }
  106. })
  107. // 移除自动静默登录逻辑 - 小程序启动时不再自动登录
  108. // 用户需要主动点击登录按钮并同意授权后才能登录
  109. // 如需恢复自动登录,取消以下注释:
  110. // useLaunch(() => {
  111. // silentLoginMutation.mutate()
  112. // })
  113. const loginMutation = useMutation<User, Error, LoginRequest>({
  114. mutationFn: async (data) => {
  115. const response = await authClient.login.$post({ json: data })
  116. if (response.status !== 200) {
  117. throw new Error('登录失败')
  118. }
  119. const { token, user } = await response.json()
  120. Taro.setStorageSync('mini_token', token)
  121. Taro.setStorageSync('userInfo', JSON.stringify(user))
  122. return user
  123. },
  124. onSuccess: (newUser) => {
  125. setUser(newUser)
  126. },
  127. onError: (error) => {
  128. Taro.showToast({
  129. title: error.message || '登录失败,请检查用户名和密码',
  130. icon: 'none',
  131. duration: 2000,
  132. })
  133. },
  134. })
  135. const registerMutation = useMutation<User, Error, RegisterRequest>({
  136. mutationFn: async (data) => {
  137. const response = await authClient.register.$post({ json: data })
  138. if (response.status !== 201) {
  139. throw new Error('注册失败')
  140. }
  141. const { token, user } = await response.json()
  142. Taro.setStorageSync('mini_token', token)
  143. Taro.setStorageSync('userInfo', JSON.stringify(user))
  144. return user
  145. },
  146. onSuccess: (newUser) => {
  147. setUser(newUser)
  148. },
  149. onError: (error) => {
  150. Taro.showToast({
  151. title: error.message || '注册失败,请重试',
  152. icon: 'none',
  153. duration: 2000,
  154. })
  155. },
  156. })
  157. const logoutMutation = useMutation<void, Error>({
  158. mutationFn: async () => {
  159. try {
  160. const response = await authClient.logout.$post({})
  161. if (response.status !== 200) {
  162. throw new Error('登出失败')
  163. }
  164. } catch (error) {
  165. console.error('Logout error:', error)
  166. } finally {
  167. Taro.removeStorageSync('mini_token')
  168. Taro.removeStorageSync('userInfo')
  169. }
  170. },
  171. onSuccess: () => {
  172. setUser(null)
  173. Taro.redirectTo({ url: '/pages/login/index' })
  174. },
  175. onError: (error) => {
  176. Taro.showToast({
  177. title: error.message || '登出失败',
  178. icon: 'none',
  179. duration: 2000,
  180. })
  181. },
  182. })
  183. const updateUserMutation = useMutation<User, Error, Partial<User>>({
  184. mutationFn: async (userData) => {
  185. const response = await authClient.me.$put({ json: userData })
  186. if (response.status !== 200) {
  187. throw new Error('更新用户信息失败')
  188. }
  189. const updatedUser = await response.json()
  190. Taro.setStorageSync('userInfo', JSON.stringify(updatedUser))
  191. return updatedUser
  192. },
  193. onSuccess: (updatedUser) => {
  194. setUser(updatedUser)
  195. Taro.showToast({
  196. title: '更新成功',
  197. icon: 'success',
  198. duration: 2000,
  199. })
  200. },
  201. onError: (error) => {
  202. Taro.showToast({
  203. title: error.message || '更新失败,请重试',
  204. icon: 'none',
  205. duration: 2000,
  206. })
  207. },
  208. })
  209. const updateUser = updateUserMutation.mutateAsync
  210. const refreshUser = async (): Promise<User | null> => {
  211. try {
  212. const freshUser = await fetchUserInfo()
  213. if (freshUser) {
  214. setUser(freshUser)
  215. } else {
  216. setUser(null)
  217. }
  218. return freshUser
  219. } catch (error) {
  220. console.error('刷新用户信息失败:', error)
  221. return null
  222. }
  223. }
  224. const value = {
  225. user: user || null,
  226. login: loginMutation.mutateAsync,
  227. logout: logoutMutation.mutateAsync,
  228. register: registerMutation.mutateAsync,
  229. updateUser,
  230. refreshUser,
  231. setUser, // 导出 setUser 方法供外部更新用户状态
  232. isLoading: loginMutation.isPending || registerMutation.isPending || logoutMutation.isPending || silentLoginMutation.isPending,
  233. isLoggedIn: !!user,
  234. }
  235. return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
  236. }
  237. export const useAuth = () => {
  238. const context = useContext(AuthContext)
  239. if (context === undefined) {
  240. throw new Error('useAuth must be used within an AuthProvider')
  241. }
  242. return context
  243. }