瀏覽代碼

✨ feat(utils): introduce @tanstack/react-query for data fetching

- add @tanstack/react-query@5.84.1 dependency to package.json

♻️ refactor(auth): rewrite auth system with react context and hooks

- replace AuthManager class with AuthContext and useAuth hook
- implement user authentication with react-query for state management
- add loading states for auth operations
- improve error handling with toast notifications
- remove old auth.ts file and create new auth.tsx with context provider

✅ test(auth): enhance auth flow reliability

- add automatic user data refresh on login/register
- implement proper cache invalidation on logout
- set optimal staleTime and refetch policies for user data
yourname 4 月之前
父節點
當前提交
a159ac4615
共有 4 個文件被更改,包括 167 次插入139 次删除
  1. 2 1
      mini/package.json
  2. 18 0
      mini/pnpm-lock.yaml
  3. 0 138
      mini/src/utils/auth.ts
  4. 147 0
      mini/src/utils/auth.tsx

+ 2 - 1
mini/package.json

@@ -62,7 +62,8 @@
     "@tarojs/shared": "4.1.4",
     "@tarojs/taro": "4.1.4",
     "react": "^18.0.0",
-    "react-dom": "^18.0.0"
+    "react-dom": "^18.0.0",
+    "@tanstack/react-query": "^5.84.1"
   },
   "devDependencies": {
     "@babel/core": "^7.24.4",

+ 18 - 0
mini/pnpm-lock.yaml

@@ -11,6 +11,9 @@ importers:
       '@babel/runtime':
         specifier: ^7.24.4
         version: 7.28.2
+      '@tanstack/react-query':
+        specifier: ^5.84.1
+        version: 5.84.1(react@18.3.1)
       '@tarojs/components':
         specifier: 4.1.4
         version: 4.1.4(@tarojs/helper@4.1.4)(@types/react@18.3.23)(html-webpack-plugin@5.6.3(webpack@5.91.0(@swc/core@1.3.96)))(postcss@8.5.6)(rollup@3.29.5)(vue@3.5.18(typescript@5.8.3))(webpack-chain@6.5.1)(webpack-dev-server@4.15.2(webpack@5.91.0(@swc/core@1.3.96)))(webpack@5.91.0(@swc/core@1.3.96))
@@ -1936,6 +1939,14 @@ packages:
   '@tailwindcss/postcss@4.1.11':
     resolution: {integrity: sha512-q/EAIIpF6WpLhKEuQSEVMZNMIY8KhWoAemZ9eylNAih9jxMGAYPPWBn3I9QL/2jZ+e7OEz/tZkX5HwbBR4HohA==}
 
+  '@tanstack/query-core@5.83.1':
+    resolution: {integrity: sha512-OG69LQgT7jSp+5pPuCfzltq/+7l2xoweggjme9vlbCPa/d7D7zaqv5vN/S82SzSYZ4EDLTxNO1PWrv49RAS64Q==}
+
+  '@tanstack/react-query@5.84.1':
+    resolution: {integrity: sha512-zo7EUygcWJMQfFNWDSG7CBhy8irje/XY0RDVKKV4IQJAysb+ZJkkJPcnQi+KboyGUgT+SQebRFoTqLuTtfoDLw==}
+    peerDependencies:
+      react: ^18 || ^19
+
   '@tarojs/api@4.1.4':
     resolution: {integrity: sha512-pIoP7TQLJSPtoCEQFY8qYllnPTH/K1Llz/nGQVWyVqMkKhK8HDpUlEqu21baR586AOrVxAWfmPD01n3XDm5AmA==}
     engines: {node: '>= 18'}
@@ -9238,6 +9249,13 @@ snapshots:
       postcss: 8.5.6
       tailwindcss: 4.1.11
 
+  '@tanstack/query-core@5.83.1': {}
+
+  '@tanstack/react-query@5.84.1(react@18.3.1)':
+    dependencies:
+      '@tanstack/query-core': 5.83.1
+      react: 18.3.1
+
   '@tarojs/api@4.1.4(@tarojs/runtime@4.1.4)(@tarojs/shared@4.1.4)':
     dependencies:
       '@tarojs/runtime': 4.1.4

+ 0 - 138
mini/src/utils/auth.ts

@@ -1,138 +0,0 @@
-import Taro from '@tarojs/taro'
-import { authClient } from '../api'
-import { InferResponseType } from 'hono'
-
-// 用户类型定义
-export type User = InferResponseType<typeof authClient.me.$get, 200>
-
-// 认证状态管理
-class AuthManager {
-  private static instance: AuthManager
-  
-  static getInstance(): AuthManager {
-    if (!AuthManager.instance) {
-      AuthManager.instance = new AuthManager()
-    }
-    return AuthManager.instance
-  }
-
-  // 获取token
-  getToken(): string | null {
-    return Taro.getStorageSync('token')
-  }
-
-  // 设置token
-  setToken(token: string): void {
-    Taro.setStorageSync('token', token)
-  }
-
-  // 清除token
-  removeToken(): void {
-    Taro.removeStorageSync('token')
-  }
-
-  // 获取用户信息
-  getUserInfo(): User | null {
-    const userInfo = Taro.getStorageSync('userInfo')
-    return userInfo ? JSON.parse(userInfo) : null
-  }
-
-  // 设置用户信息
-  setUserInfo(user: User): void {
-    Taro.setStorageSync('userInfo', JSON.stringify(user))
-  }
-
-  // 清除用户信息
-  removeUserInfo(): void {
-    Taro.removeStorageSync('userInfo')
-  }
-
-  // 检查是否已登录
-  isLoggedIn(): boolean {
-    return !!this.getToken()
-  }
-
-  // 登录
-  async login(username: string, password: string): Promise<User> {
-    try {
-      const response = await authClient.login.$post({
-        json: { username, password }
-      })
-      if (response.status !== 200) {
-        throw new Error('登录失败')
-      }
-      const { token, user } = await response.json()
-      
-      this.setToken(token)
-      this.setUserInfo(user)
-      
-      return user
-    } catch (error) {
-      throw new Error('登录失败,请检查用户名和密码')
-    }
-  }
-
-  // 注册
-  async register(data: {
-    username: string
-    password: string
-    email?: string
-  }): Promise<User> {
-    try {
-      const response = await authClient.register.$post({
-        json: data
-      })
-      if (response.status !== 201) {
-        throw new Error('注册失败')
-      }
-      const { token, user } = await response.json()
-      
-      this.setToken(token)
-      this.setUserInfo(user)
-      
-      return user
-    } catch (error) {
-      throw new Error('注册失败,请重试')
-    }
-  }
-
-  // 登出
-  async logout(): Promise<void> {
-    try {
-      const response = await authClient.logout.$post({})
-      if (response.status !== 200) {
-        throw new Error('登出失败')
-      }
-    } catch (error) {
-      console.error('Logout error:', error)
-    } finally {
-      this.removeToken()
-      this.removeUserInfo()
-      Taro.redirectTo({ url: '/pages/login/index' })
-    }
-  }
-
-  // 获取当前用户信息
-  async getCurrentUser(): Promise<User | null> {
-    if (!this.isLoggedIn()) {
-      return null
-    }
-
-    try {
-      const response = await authClient.me.$get({})
-      if (response.status !== 200) {
-        throw new Error('获取用户信息失败')
-      }
-      const user = await response.json()
-      this.setUserInfo(user)
-      return user
-    } catch (error) {
-      this.removeToken()
-      this.removeUserInfo()
-      return null
-    }
-  }
-}
-
-// 导出单例实例
-export const authManager = AuthManager.getInstance()

+ 147 - 0
mini/src/utils/auth.tsx

@@ -0,0 +1,147 @@
+import { createContext, useContext, PropsWithChildren, useState } from 'react'
+import Taro from '@tarojs/taro'
+import { authClient } from '../api'
+import { InferResponseType, InferRequestType } from 'hono'
+import { QueryClient, QueryClientProvider, useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
+
+// 用户类型定义
+export type User = InferResponseType<typeof authClient.me.$get, 200>
+type LoginRequest = InferRequestType<typeof authClient.login.$post>['json']
+type RegisterRequest = InferRequestType<typeof authClient.register.$post>['json']
+
+interface AuthContextType {
+  user: User | null
+  login: (data: LoginRequest) => Promise<User>
+  logout: () => Promise<void>
+  register: (data: RegisterRequest) => Promise<User>
+  isLoading: boolean
+  isLoggedIn: boolean
+}
+
+const AuthContext = createContext<AuthContextType | undefined>(undefined)
+
+const queryClient = new QueryClient()
+
+export const AuthProvider: React.FC<PropsWithChildren> = ({ children }) => {
+  const queryClient = useQueryClient()
+
+  const { data: user, isLoading } = useQuery<User | null, Error>({
+    queryKey: ['currentUser'],
+    queryFn: async () => {
+      const token = Taro.getStorageSync('token')
+      if (!token) {
+        return null
+      }
+      try {
+        const response = await authClient.me.$get({})
+        if (response.status !== 200) {
+          throw new Error('获取用户信息失败')
+        }
+        const user = await response.json()
+        Taro.setStorageSync('userInfo', JSON.stringify(user))
+        return user
+      } catch (error) {
+        Taro.removeStorageSync('token')
+        Taro.removeStorageSync('userInfo')
+        return null
+      }
+    },
+    staleTime: Infinity, // 用户信息不常变动,设为无限期
+    refetchOnWindowFocus: false, // 失去焦点不重新获取
+    refetchOnReconnect: false, // 网络重连不重新获取
+  })
+
+  const loginMutation = useMutation<User, Error, LoginRequest>({
+    mutationFn: async (data) => {
+      const response = await authClient.login.$post({ json: data })
+      if (response.status !== 200) {
+        throw new Error('登录失败')
+      }
+      const { token, user } = await response.json()
+      Taro.setStorageSync('token', token)
+      Taro.setStorageSync('userInfo', JSON.stringify(user))
+      return user
+    },
+    onSuccess: (newUser) => {
+      queryClient.setQueryData(['currentUser'], newUser)
+    },
+    onError: (error) => {
+      Taro.showToast({
+        title: error.message || '登录失败,请检查用户名和密码',
+        icon: 'none',
+        duration: 2000,
+      })
+    },
+  })
+
+  const registerMutation = useMutation<User, Error, RegisterRequest>({
+    mutationFn: async (data) => {
+      const response = await authClient.register.$post({ json: data })
+      if (response.status !== 201) {
+        throw new Error('注册失败')
+      }
+      const { token, user } = await response.json()
+      Taro.setStorageSync('token', token)
+      Taro.setStorageSync('userInfo', JSON.stringify(user))
+      return user
+    },
+    onSuccess: (newUser) => {
+      queryClient.setQueryData(['currentUser'], newUser)
+    },
+    onError: (error) => {
+      Taro.showToast({
+        title: error.message || '注册失败,请重试',
+        icon: 'none',
+        duration: 2000,
+      })
+    },
+  })
+
+  const logoutMutation = useMutation<void, Error>({
+    mutationFn: async () => {
+      try {
+        const response = await authClient.logout.$post({})
+        if (response.status !== 200) {
+          throw new Error('登出失败')
+        }
+      } catch (error) {
+        console.error('Logout error:', error)
+      } finally {
+        Taro.removeStorageSync('token')
+        Taro.removeStorageSync('userInfo')
+      }
+    },
+    onSuccess: () => {
+      queryClient.setQueryData(['currentUser'], null)
+      Taro.redirectTo({ url: '/pages/login/index' })
+    },
+    onError: (error) => {
+      Taro.showToast({
+        title: error.message || '登出失败',
+        icon: 'none',
+        duration: 2000,
+      })
+    },
+  })
+
+  const value = {
+    user: user || null,
+    login: loginMutation.mutateAsync,
+    logout: logoutMutation.mutateAsync,
+    register: registerMutation.mutateAsync,
+    isLoading: isLoading || loginMutation.isPending || registerMutation.isPending || logoutMutation.isPending,
+    isLoggedIn: !!user,
+  }
+
+  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
+}
+
+export const useAuth = () => {
+  const context = useContext(AuthContext)
+  if (context === undefined) {
+    throw new Error('useAuth must be used within an AuthProvider')
+  }
+  return context
+}
+
+export { queryClient }