feat: 初始化移动端项目基础结构
添加项目基础配置和核心功能模块: - 配置 TypeScript 和 React Native 环境 - 实现认证状态管理 - 封装 API 请求客户端 - 搭建应用导航框架
This commit is contained in:
200
src/navigation/AppNavigator.tsx
Normal file
200
src/navigation/AppNavigator.tsx
Normal file
@@ -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<RootStackParamList>();
|
||||
const Tab = createBottomTabNavigator<TabParamList>();
|
||||
|
||||
/**
|
||||
* Tab 导航器
|
||||
* 主应用的底部标签导航
|
||||
*/
|
||||
const TabNavigator: React.FC = () => {
|
||||
return (
|
||||
<Tab.Navigator
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
tabBarActiveTintColor: '#1890ff',
|
||||
tabBarInactiveTintColor: '#999',
|
||||
tabBarStyle: {
|
||||
backgroundColor: '#fff',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#f0f0f0',
|
||||
paddingBottom: 8,
|
||||
paddingTop: 8,
|
||||
height: 60,
|
||||
},
|
||||
tabBarLabelStyle: {
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tab.Screen
|
||||
name="Home"
|
||||
component={HomeScreen}
|
||||
options={{
|
||||
tabBarLabel: '首页',
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Icon name="home" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Timeline"
|
||||
component={TimelineScreen}
|
||||
options={{
|
||||
tabBarLabel: '时间线',
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Icon name="timeline" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Gallery"
|
||||
component={GalleryScreen}
|
||||
options={{
|
||||
tabBarLabel: '相册',
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Icon name="image" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Profile"
|
||||
component={ProfileScreen}
|
||||
options={{
|
||||
tabBarLabel: '我的',
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Icon name="user" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Tab.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 认证导航器
|
||||
* 未登录用户的认证流程
|
||||
*/
|
||||
const AuthStack: React.FC = () => {
|
||||
return (
|
||||
<Stack.Navigator
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="Login" component={LoginScreen} />
|
||||
<Stack.Screen name="Register" component={RegisterScreen} />
|
||||
</Stack.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 主导航器
|
||||
* 已登录用户的主应用流程
|
||||
*/
|
||||
const MainStack: React.FC = () => {
|
||||
return (
|
||||
<Stack.Navigator
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="TabNavigator" component={TabNavigator} />
|
||||
<Stack.Screen
|
||||
name="StoryDetail"
|
||||
component={TimelineDetailScreen}
|
||||
options={{
|
||||
headerShown: true,
|
||||
title: '时间线详情',
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen name="Notification" component={NotificationScreen} />
|
||||
<Stack.Screen name="Settings" component={SettingsScreen} />
|
||||
</Stack.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 根导航器
|
||||
* 根据登录状态切换认证/主应用流程
|
||||
*/
|
||||
const AppNavigator: React.FC = () => {
|
||||
const { isAuthenticated } = useAuthStore();
|
||||
|
||||
return (
|
||||
<NavigationContainer>
|
||||
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
||||
{isAuthenticated ? (
|
||||
<Stack.Screen name="Main" component={MainStack} />
|
||||
) : (
|
||||
<Stack.Screen name="Auth" component={AuthStack} />
|
||||
)}
|
||||
</Stack.Navigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppNavigator;
|
||||
138
src/services/apiClient.ts
Normal file
138
src/services/apiClient.ts
Normal file
@@ -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;
|
||||
123
src/stores/authStore.ts
Normal file
123
src/stores/authStore.ts
Normal file
@@ -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<User>) => void;
|
||||
setToken: (token: string) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证状态 Store
|
||||
*/
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
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,
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
Reference in New Issue
Block a user