From 42d332f77c6555cae978337f28b0a60d29fb9229 Mon Sep 17 00:00:00 2001 From: jianghao <332515344@qq.com> Date: Tue, 24 Feb 2026 10:34:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E5=A7=8B=E5=8C=96=E7=A7=BB?= =?UTF-8?q?=E5=8A=A8=E7=AB=AF=E9=A1=B9=E7=9B=AE=E5=9F=BA=E7=A1=80=E7=BB=93?= =?UTF-8?q?=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加项目基础配置和核心功能模块: - 配置 TypeScript 和 React Native 环境 - 实现认证状态管理 - 封装 API 请求客户端 - 搭建应用导航框架 --- App.tsx | 56 +++++++++ package.json | 67 +++++++++++ src/navigation/AppNavigator.tsx | 200 ++++++++++++++++++++++++++++++++ src/services/apiClient.ts | 138 ++++++++++++++++++++++ src/stores/authStore.ts | 123 ++++++++++++++++++++ tsconfig.json | 32 +++++ 6 files changed, 616 insertions(+) create mode 100644 App.tsx create mode 100644 package.json create mode 100644 src/navigation/AppNavigator.tsx create mode 100644 src/services/apiClient.ts create mode 100644 src/stores/authStore.ts create mode 100644 tsconfig.json diff --git a/App.tsx b/App.tsx new file mode 100644 index 0000000..3d4292f --- /dev/null +++ b/App.tsx @@ -0,0 +1,56 @@ +/** + * Timeline 移动端 App 入口 + * + * 功能描述: + * React Native 应用入口,配置导航和全局状态管理。 + * + * @author Timeline Team + * @date 2024 + */ + +import React from 'react'; +import { StatusBar } from 'react-native'; +import { SafeAreaProvider } from 'react-native-safe-area-context'; +import { GestureHandlerRootView } from 'react-native-gesture-handler'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import AppNavigator from './src/navigation/AppNavigator'; + +// 创建 React Query 客户端 +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 5 * 60 * 1000, // 5分钟 + cacheTime: 10 * 60 * 1000, // 10分钟 + retry: 2, + refetchOnWindowFocus: false, + }, + }, +}); + +/** + * App 根组件 + * + * 组件层级: + * 1. GestureHandlerRootView - 手势处理 + * 2. SafeAreaProvider - 安全区域 + * 3. QueryClientProvider - 数据请求 + * 4. AppNavigator - 导航 + */ +const App: React.FC = () => { + return ( + + + + + + + + + ); +}; + +export default App; diff --git a/package.json b/package.json new file mode 100644 index 0000000..36eca32 --- /dev/null +++ b/package.json @@ -0,0 +1,67 @@ +/** + * Timeline 移动端 App - React Native 项目配置 + * + * 功能描述: + * 定义 React Native 移动端应用的项目配置。 + * + * 技术栈: + * - React Native 0.73+ + * - TypeScript + * - React Navigation 6 + * - Redux Toolkit / Zustand + * - React Query + * - NativeWind (TailwindCSS for RN) + * + * @author Timeline Team + * @date 2024 + */ + +// package.json 配置 +{ + "name": "timeline-mobile", + "version": "1.0.0", + "private": true, + "scripts": { + "android": "react-native run-android", + "ios": "react-native run-ios", + "start": "react-native start", + "test": "jest", + "lint": "eslint .", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "react": "18.2.0", + "react-native": "0.73.0", + "@react-navigation/native": "^6.1.0", + "@react-navigation/native-stack": "^6.9.0", + "@react-navigation/bottom-tabs": "^6.5.0", + "@react-navigation/drawer": "^6.6.0", + "react-native-screens": "^3.29.0", + "react-native-safe-area-context": "^4.8.0", + "react-native-gesture-handler": "^2.14.0", + "react-native-reanimated": "^3.6.0", + "@react-native-async-storage/async-storage": "^1.21.0", + "@tanstack/react-query": "^5.17.0", + "zustand": "^4.4.0", + "axios": "^1.6.0", + "date-fns": "^3.2.0", + "react-native-image-picker": "^7.1.0", + "react-native-camera": "^4.2.1", + "react-native-geolocation-service": "^5.3.1", + "react-native-push-notification": "^8.1.1", + "@react-native-community/netinfo": "^11.3.0", + "nativewind": "^2.0.11", + "react-native-svg": "^14.1.0", + "react-native-fast-image": "^8.6.3" + }, + "devDependencies": { + "@types/react": "^18.2.0", + "@types/react-native": "^0.73.0", + "typescript": "^5.3.0", + "eslint": "^8.56.0", + "@typescript-eslint/eslint-plugin": "^6.18.0", + "prettier": "^3.2.0", + "tailwindcss": "^3.4.0", + "react-native-svg-transformer": "^1.3.0" + } +} diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx new file mode 100644 index 0000000..c9d4902 --- /dev/null +++ b/src/navigation/AppNavigator.tsx @@ -0,0 +1,200 @@ +/** + * AppNavigator - 应用导航配置 + * + * 功能描述: + * 配置应用的路由导航结构,包括: + * - 认证流程(登录/注册) + * - 主应用流程(底部Tab导航) + * - 详情页面(Stack导航) + * + * 导航结构: + * RootStack + * ├── AuthStack (未登录) + * │ ├── Login + * │ └── Register + * └── MainStack (已登录) + * ├── TabNavigator + * │ ├── Home + * │ ├── Timeline + * │ ├── Gallery + * │ └── Profile + * └── StoryDetail + * + * @author Timeline Team + * @date 2024 + */ + +import React from 'react'; +import { NavigationContainer } from '@react-navigation/native'; +import { createNativeStackNavigator } from '@react-navigation/native-stack'; +import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; +import { useAuthStore } from '@stores/authStore'; + +// 屏幕组件 +import LoginScreen from '@screens/auth/LoginScreen'; +import RegisterScreen from '@screens/auth/RegisterScreen'; +import HomeScreen from '@screens/home/HomeScreen'; +import TimelineScreen from '@screens/timeline/TimelineScreen'; +import TimelineDetailScreen from '@screens/timeline/TimelineDetailScreen'; +import GalleryScreen from '@screens/gallery/GalleryScreen'; +import ProfileScreen from '@screens/profile/ProfileScreen'; +import NotificationScreen from '@screens/notification/NotificationScreen'; +import SettingsScreen from '@screens/settings/SettingsScreen'; + +// 图标组件 +import Icon from '@components/common/Icon'; + +// 导航类型定义 +export type RootStackParamList = { + Auth: undefined; + Main: undefined; + Login: undefined; + Register: undefined; + StoryDetail: { storyId: string }; + Notification: undefined; + Settings: undefined; +}; + +export type TabParamList = { + Home: undefined; + Timeline: undefined; + Gallery: undefined; + Profile: undefined; +}; + +const Stack = createNativeStackNavigator(); +const Tab = createBottomTabNavigator(); + +/** + * Tab 导航器 + * 主应用的底部标签导航 + */ +const TabNavigator: React.FC = () => { + return ( + + ( + + ), + }} + /> + ( + + ), + }} + /> + ( + + ), + }} + /> + ( + + ), + }} + /> + + ); +}; + +/** + * 认证导航器 + * 未登录用户的认证流程 + */ +const AuthStack: React.FC = () => { + return ( + + + + + ); +}; + +/** + * 主导航器 + * 已登录用户的主应用流程 + */ +const MainStack: React.FC = () => { + return ( + + + + + + + ); +}; + +/** + * 根导航器 + * 根据登录状态切换认证/主应用流程 + */ +const AppNavigator: React.FC = () => { + const { isAuthenticated } = useAuthStore(); + + return ( + + + {isAuthenticated ? ( + + ) : ( + + )} + + + ); +}; + +export default AppNavigator; diff --git a/src/services/apiClient.ts b/src/services/apiClient.ts new file mode 100644 index 0000000..013d8d4 --- /dev/null +++ b/src/services/apiClient.ts @@ -0,0 +1,138 @@ +/** + * apiClient - API 请求客户端 + * + * 功能描述: + * 封装 Axios 请求客户端,处理: + * - 请求拦截(添加 Token) + * - 响应拦截(错误处理) + * - Token 刷新 + * - 网络状态检测 + * + * @author Timeline Team + * @date 2024 + */ + +import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios'; +import { useAuthStore } from '@stores/authStore'; +import NetInfo from '@react-native-community/netinfo'; + +// API 基础配置 +const BASE_URL = __DEV__ + ? 'http://10.0.2.2:8080' // Android 模拟器 + : 'https://api.timeline.com'; + +const TIMEOUT = 30000; + +/** + * 创建 Axios 实例 + */ +const apiClient: AxiosInstance = axios.create({ + baseURL: BASE_URL, + timeout: TIMEOUT, + headers: { + 'Content-Type': 'application/json', + }, +}); + +/** + * 请求拦截器 + * 添加 Token 到请求头 + */ +apiClient.interceptors.request.use( + async (config: InternalAxiosRequestConfig) => { + // 检查网络状态 + const netInfo = await NetInfo.fetch(); + if (!netInfo.isConnected) { + return Promise.reject(new Error('网络不可用,请检查网络连接')); + } + + // 添加 Token + const token = useAuthStore.getState().token; + if (token && config.headers) { + config.headers.Authorization = `Bearer ${token}`; + } + + return config; + }, + (error: AxiosError) => { + return Promise.reject(error); + } +); + +/** + * 响应拦截器 + * 处理错误和 Token 刷新 + */ +apiClient.interceptors.response.use( + (response) => { + return response.data; + }, + async (error: AxiosError) => { + const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean }; + + // 处理 401 未授权错误 + if (error.response?.status === 401 && !originalRequest._retry) { + originalRequest._retry = true; + + try { + // 尝试刷新 Token + const refreshToken = useAuthStore.getState().refreshToken; + if (refreshToken) { + const response = await axios.post(`${BASE_URL}/auth/refresh`, { + refreshToken, + }); + + const { token } = response.data; + useAuthStore.getState().setToken(token); + + // 重试原请求 + if (originalRequest.headers) { + originalRequest.headers.Authorization = `Bearer ${token}`; + } + return apiClient(originalRequest); + } + } catch (refreshError) { + // Token 刷新失败,登出 + useAuthStore.getState().logout(); + return Promise.reject(refreshError); + } + } + + // 处理其他错误 + const errorMessage = getErrorMessage(error); + return Promise.reject(new Error(errorMessage)); + } +); + +/** + * 获取错误消息 + */ +function getErrorMessage(error: AxiosError): string { + if (error.message === 'Network Error') { + return '网络错误,请检查网络连接'; + } + + if (error.response) { + const status = error.response.status; + const data: any = error.response.data; + + switch (status) { + case 400: + return data?.message || '请求参数错误'; + case 401: + return '登录已过期,请重新登录'; + case 403: + return '没有权限访问'; + case 404: + return '请求的资源不存在'; + case 500: + return '服务器错误,请稍后重试'; + default: + return data?.message || `请求失败 (${status})`; + } + } + + return error.message || '未知错误'; +} + +export default apiClient; diff --git a/src/stores/authStore.ts b/src/stores/authStore.ts new file mode 100644 index 0000000..fd22204 --- /dev/null +++ b/src/stores/authStore.ts @@ -0,0 +1,123 @@ +/** + * authStore - 认证状态管理 Store + * + * 功能描述: + * 使用 Zustand 管理用户认证状态。 + * + * 状态: + * - isAuthenticated: 是否已认证 + * - user: 用户信息 + * - token: 访问令牌 + * + * 操作: + * - login: 登录 + * - logout: 登出 + * - updateUser: 更新用户信息 + * + * 持久化: + * - 使用 AsyncStorage 持久化 token + * + * @author Timeline Team + * @date 2024 + */ + +import { create } from 'zustand'; +import { persist, createJSONStorage } from 'zustand/middleware'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { User } from '@types/user'; + +interface AuthState { + // 状态 + isAuthenticated: boolean; + user: User | null; + token: string | null; + refreshToken: string | null; + isLoading: boolean; + + // 操作 + login: (token: string, refreshToken: string, user: User) => void; + logout: () => void; + updateUser: (user: Partial) => void; + setToken: (token: string) => void; + setLoading: (loading: boolean) => void; +} + +/** + * 认证状态 Store + */ +export const useAuthStore = create()( + persist( + (set, get) => ({ + // 初始状态 + isAuthenticated: false, + user: null, + token: null, + refreshToken: null, + isLoading: true, + + /** + * 登录 + * 保存 token 和用户信息 + */ + login: (token, refreshToken, user) => { + set({ + isAuthenticated: true, + token, + refreshToken, + user, + isLoading: false, + }); + }, + + /** + * 登出 + * 清除所有认证信息 + */ + logout: () => { + set({ + isAuthenticated: false, + user: null, + token: null, + refreshToken: null, + isLoading: false, + }); + }, + + /** + * 更新用户信息 + */ + updateUser: (userData) => { + const currentUser = get().user; + if (currentUser) { + set({ + user: { ...currentUser, ...userData }, + }); + } + }, + + /** + * 设置 Token + */ + setToken: (token) => { + set({ token }); + }, + + /** + * 设置加载状态 + */ + setLoading: (loading) => { + set({ isLoading: loading }); + }, + }), + { + name: 'auth-storage', + storage: createJSONStorage(() => AsyncStorage), + partialize: (state) => ({ + token: state.token, + refreshToken: state.refreshToken, + user: state.user, + isAuthenticated: state.isAuthenticated, + }), + } + ) +); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c6f9b1e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "commonjs", + "lib": ["es2017"], + "allowJs": true, + "jsx": "react-native", + "noEmit": true, + "isolatedModules": true, + "strict": true, + "moduleResolution": "node", + "baseUrl": "./", + "paths": { + "@/*": ["src/*"], + "@components/*": ["src/components/*"], + "@screens/*": ["src/screens/*"], + "@hooks/*": ["src/hooks/*"], + "@services/*": ["src/services/*"], + "@stores/*": ["src/stores/*"], + "@utils/*": ["src/utils/*"], + "@types/*": ["src/types/*"], + "@assets/*": ["src/assets/*"] + }, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*", "index.js", "App.tsx"], + "exclude": ["node_modules", "babel.config.js", "metro.config.js", "jest.config.js"] +}