feat: new project

This commit is contained in:
2025-11-04 13:27:19 +08:00
commit 7526a9b827
49 changed files with 12520 additions and 0 deletions

84
src/hooks/useDebounce.ts Normal file
View File

@@ -0,0 +1,84 @@
/**
* 防抖 Hook
* 使用 lodash-es 的 debounce 函数
*/
import React, { useEffect, useMemo, useRef } from 'react';
import { debounce } from 'lodash-es';
/**
* 防抖值 Hook
* @param value 需要防抖的值
* @param delay 延迟时间(毫秒)
* @returns 防抖后的值
*/
export function useDebounceValue<T>(value: T, delay: number = 300): T {
const [debouncedValue, setDebouncedValue] = React.useState<T>(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
/**
* 防抖函数 Hook
* @param callback 需要防抖的函数
* @param delay 延迟时间(毫秒)
* @returns 防抖后的函数
*/
export function useDebounce<T extends (...args: any[]) => any>(
callback: T,
delay: number = 300
): T {
const callbackRef = useRef(callback);
// 更新 callback ref
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
// 创建防抖函数
const debouncedCallback = useMemo(() => {
const func = (...args: Parameters<T>) => {
callbackRef.current(...args);
};
return debounce(func, delay);
}, [delay]);
// 清理
useEffect(() => {
return () => {
debouncedCallback.cancel();
};
}, [debouncedCallback]);
return debouncedCallback as T;
}
/**
* 使用示例:
*
* // 防抖值
* const [searchText, setSearchText] = useState('');
* const debouncedSearchText = useDebounceValue(searchText, 500);
*
* useEffect(() => {
* // 使用防抖后的值进行搜索
* search(debouncedSearchText);
* }, [debouncedSearchText]);
*
* // 防抖函数
* const handleSearch = useDebounce((text: string) => {
* console.log('Searching:', text);
* }, 500);
*/

114
src/hooks/useHaptics.ts Normal file
View File

@@ -0,0 +1,114 @@
/**
* 触觉反馈 Hook
* 封装 Expo Haptics 功能
*/
import { useCallback } from 'react';
import * as Haptics from 'expo-haptics';
import { useHapticsEnabled } from '@/src/stores/settingsStore';
/**
* 触觉反馈 Hook
* 根据用户设置决定是否触发触觉反馈
*/
export function useHaptics() {
const hapticsEnabled = useHapticsEnabled();
/**
* 轻触反馈
*/
const light = useCallback(async () => {
if (hapticsEnabled) {
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}
}, [hapticsEnabled]);
/**
* 中等反馈
*/
const medium = useCallback(async () => {
if (hapticsEnabled) {
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
}
}, [hapticsEnabled]);
/**
* 重触反馈
*/
const heavy = useCallback(async () => {
if (hapticsEnabled) {
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
}
}, [hapticsEnabled]);
/**
* 成功反馈
*/
const success = useCallback(async () => {
if (hapticsEnabled) {
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
}
}, [hapticsEnabled]);
/**
* 警告反馈
*/
const warning = useCallback(async () => {
if (hapticsEnabled) {
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
}
}, [hapticsEnabled]);
/**
* 错误反馈
*/
const error = useCallback(async () => {
if (hapticsEnabled) {
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
}
}, [hapticsEnabled]);
/**
* 选择反馈
*/
const selection = useCallback(async () => {
if (hapticsEnabled) {
await Haptics.selectionAsync();
}
}, [hapticsEnabled]);
return {
light,
medium,
heavy,
success,
warning,
error,
selection,
};
}
/**
* 使用示例:
*
* function MyComponent() {
* const haptics = useHaptics();
*
* const handlePress = () => {
* haptics.light();
* // 执行其他操作
* };
*
* const handleSuccess = () => {
* haptics.success();
* // 显示成功消息
* };
*
* return (
* <TouchableOpacity onPress={handlePress}>
* <Text>Press me</Text>
* </TouchableOpacity>
* );
* }
*/

61
src/hooks/useThrottle.ts Normal file
View File

@@ -0,0 +1,61 @@
/**
* 节流 Hook
* 使用 lodash-es 的 throttle 函数
*/
import { useEffect, useMemo, useRef } from 'react';
import { throttle } from 'lodash-es';
/**
* 节流函数 Hook
* @param callback 需要节流的函数
* @param delay 延迟时间(毫秒)
* @param options 节流选项
* @returns 节流后的函数
*/
export function useThrottle<T extends (...args: any[]) => any>(
callback: T,
delay: number = 300,
options?: {
leading?: boolean;
trailing?: boolean;
}
): T {
const callbackRef = useRef(callback);
// 更新 callback ref
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
// 创建节流函数
const throttledCallback = useMemo(() => {
const func = (...args: Parameters<T>) => {
callbackRef.current(...args);
};
return throttle(func, delay, options);
}, [delay, options]);
// 清理
useEffect(() => {
return () => {
throttledCallback.cancel();
};
}, [throttledCallback]);
return throttledCallback as T;
}
/**
* 使用示例:
*
* const handleScroll = useThrottle((event) => {
* console.log('Scrolling:', event);
* }, 200);
*
* <ScrollView onScroll={handleScroll}>
* ...
* </ScrollView>
*/

29
src/index.ts Normal file
View File

@@ -0,0 +1,29 @@
/**
* 统一导出所有模块
*/
// Utils
export { default as api, request } from './utils/api';
export { default as Storage, STORAGE_KEYS } from './utils/storage';
export * from './utils/date';
// Stores
export * from './stores/userStore';
export * from './stores/settingsStore';
// Schemas
export * from './schemas/auth';
export * from './schemas/user';
// Services
export { default as authService } from './services/authService';
export { default as userService } from './services/userService';
// Hooks
export * from './hooks/useDebounce';
export * from './hooks/useThrottle';
export * from './hooks/useHaptics';
// Types
export * from './types';

130
src/schemas/auth.ts Normal file
View File

@@ -0,0 +1,130 @@
/**
* 认证相关的 Zod 验证 Schema
*/
import { z } from 'zod';
/**
* 登录表单 Schema
*/
export const loginSchema = z.object({
email: z
.string()
.min(1, '请输入邮箱')
.email('请输入有效的邮箱地址'),
password: z
.string()
.min(6, '密码至少6个字符')
.max(20, '密码最多20个字符'),
rememberMe: z.boolean().optional(),
});
/**
* 注册表单 Schema
*/
export const registerSchema = z
.object({
username: z
.string()
.min(3, '用户名至少3个字符')
.max(20, '用户名最多20个字符')
.regex(/^[a-zA-Z0-9_]+$/, '用户名只能包含字母、数字和下划线'),
email: z
.string()
.min(1, '请输入邮箱')
.email('请输入有效的邮箱地址'),
password: z
.string()
.min(6, '密码至少6个字符')
.max(20, '密码最多20个字符')
.regex(/[A-Z]/, '密码必须包含至少一个大写字母')
.regex(/[a-z]/, '密码必须包含至少一个小写字母')
.regex(/[0-9]/, '密码必须包含至少一个数字'),
confirmPassword: z.string().min(1, '请确认密码'),
agreeToTerms: z.boolean().refine((val) => val === true, {
message: '请同意服务条款',
}),
})
.refine((data) => data.password === data.confirmPassword, {
message: '两次输入的密码不一致',
path: ['confirmPassword'],
});
/**
* 忘记密码 Schema
*/
export const forgotPasswordSchema = z.object({
email: z
.string()
.min(1, '请输入邮箱')
.email('请输入有效的邮箱地址'),
});
/**
* 重置密码 Schema
*/
export const resetPasswordSchema = z
.object({
code: z
.string()
.min(6, '验证码为6位')
.max(6, '验证码为6位')
.regex(/^\d{6}$/, '验证码必须是6位数字'),
password: z
.string()
.min(6, '密码至少6个字符')
.max(20, '密码最多20个字符'),
confirmPassword: z.string().min(1, '请确认密码'),
})
.refine((data) => data.password === data.confirmPassword, {
message: '两次输入的密码不一致',
path: ['confirmPassword'],
});
/**
* 修改密码 Schema
*/
export const changePasswordSchema = z
.object({
oldPassword: z.string().min(1, '请输入当前密码'),
newPassword: z
.string()
.min(6, '新密码至少6个字符')
.max(20, '新密码最多20个字符'),
confirmPassword: z.string().min(1, '请确认新密码'),
})
.refine((data) => data.newPassword === data.confirmPassword, {
message: '两次输入的密码不一致',
path: ['confirmPassword'],
})
.refine((data) => data.oldPassword !== data.newPassword, {
message: '新密码不能与当前密码相同',
path: ['newPassword'],
});
/**
* 手机号登录 Schema
*/
export const phoneLoginSchema = z.object({
phone: z
.string()
.min(11, '请输入11位手机号')
.max(11, '请输入11位手机号')
.regex(/^1[3-9]\d{9}$/, '请输入有效的手机号'),
code: z
.string()
.min(6, '验证码为6位')
.max(6, '验证码为6位')
.regex(/^\d{6}$/, '验证码必须是6位数字'),
});
/**
* TypeScript 类型推断
*/
export type LoginFormData = z.infer<typeof loginSchema>;
export type RegisterFormData = z.infer<typeof registerSchema>;
export type ForgotPasswordFormData = z.infer<typeof forgotPasswordSchema>;
export type ResetPasswordFormData = z.infer<typeof resetPasswordSchema>;
export type ChangePasswordFormData = z.infer<typeof changePasswordSchema>;
export type PhoneLoginFormData = z.infer<typeof phoneLoginSchema>;

76
src/schemas/user.ts Normal file
View File

@@ -0,0 +1,76 @@
/**
* 用户相关的 Zod 验证 Schema
*/
import { z } from 'zod';
/**
* 用户信息 Schema
*/
export const userSchema = z.object({
id: z.string(),
username: z.string(),
email: z.string().email(),
avatar: z.string().url().optional(),
nickname: z.string().optional(),
phone: z.string().optional(),
createdAt: z.string().optional(),
});
/**
* 更新用户资料 Schema
*/
export const updateProfileSchema = z.object({
nickname: z
.string()
.min(2, '昵称至少2个字符')
.max(20, '昵称最多20个字符')
.optional(),
avatar: z.string().url('请输入有效的头像URL').optional(),
phone: z
.string()
.regex(/^1[3-9]\d{9}$/, '请输入有效的手机号')
.optional()
.or(z.literal('')),
bio: z.string().max(200, '个人简介最多200个字符').optional(),
});
/**
* 绑定手机号 Schema
*/
export const bindPhoneSchema = z.object({
phone: z
.string()
.min(11, '请输入11位手机号')
.max(11, '请输入11位手机号')
.regex(/^1[3-9]\d{9}$/, '请输入有效的手机号'),
code: z
.string()
.min(6, '验证码为6位')
.max(6, '验证码为6位')
.regex(/^\d{6}$/, '验证码必须是6位数字'),
});
/**
* 绑定邮箱 Schema
*/
export const bindEmailSchema = z.object({
email: z
.string()
.min(1, '请输入邮箱')
.email('请输入有效的邮箱地址'),
code: z
.string()
.min(6, '验证码为6位')
.max(6, '验证码为6位')
.regex(/^\d{6}$/, '验证码必须是6位数字'),
});
/**
* TypeScript 类型推断
*/
export type User = z.infer<typeof userSchema>;
export type UpdateProfileFormData = z.infer<typeof updateProfileSchema>;
export type BindPhoneFormData = z.infer<typeof bindPhoneSchema>;
export type BindEmailFormData = z.infer<typeof bindEmailSchema>;

134
src/services/authService.ts Normal file
View File

@@ -0,0 +1,134 @@
/**
* 认证服务
* 处理登录、注册等认证相关的 API 请求
*/
import { request } from '@/src/utils/api';
import type {
LoginFormData,
RegisterFormData,
ForgotPasswordFormData,
ResetPasswordFormData,
ChangePasswordFormData,
PhoneLoginFormData,
} from '@/src/schemas/auth';
import type { User } from '@/src/schemas/user';
/**
* API 响应接口
*/
interface ApiResponse<T = any> {
code: number;
message: string;
data: T;
}
/**
* 登录响应
*/
interface LoginResponse {
user: User;
token: string;
refreshToken?: string;
}
/**
* 认证服务类
*/
class AuthService {
/**
* 邮箱登录
*/
async login(data: LoginFormData): Promise<LoginResponse> {
const response = await request.post<ApiResponse<LoginResponse>>(
'/auth/login',
data
);
return response.data;
}
/**
* 手机号登录
*/
async phoneLogin(data: PhoneLoginFormData): Promise<LoginResponse> {
const response = await request.post<ApiResponse<LoginResponse>>(
'/auth/phone-login',
data
);
return response.data;
}
/**
* 注册
*/
async register(data: RegisterFormData): Promise<LoginResponse> {
const response = await request.post<ApiResponse<LoginResponse>>(
'/auth/register',
data
);
return response.data;
}
/**
* 登出
*/
async logout(): Promise<void> {
await request.post('/auth/logout');
}
/**
* 发送忘记密码邮件
*/
async forgotPassword(data: ForgotPasswordFormData): Promise<void> {
await request.post('/auth/forgot-password', data);
}
/**
* 重置密码
*/
async resetPassword(data: ResetPasswordFormData): Promise<void> {
await request.post('/auth/reset-password', data);
}
/**
* 修改密码
*/
async changePassword(data: ChangePasswordFormData): Promise<void> {
await request.post('/auth/change-password', data);
}
/**
* 发送验证码
*/
async sendVerificationCode(phone: string): Promise<void> {
await request.post('/auth/send-code', { phone });
}
/**
* 刷新 token
*/
async refreshToken(refreshToken: string): Promise<{ token: string }> {
const response = await request.post<ApiResponse<{ token: string }>>(
'/auth/refresh-token',
{ refreshToken }
);
return response.data;
}
/**
* 验证 token 是否有效
*/
async verifyToken(): Promise<boolean> {
try {
await request.get('/auth/verify-token');
return true;
} catch {
return false;
}
}
}
// 导出单例
export const authService = new AuthService();
export default authService;

View File

@@ -0,0 +1,90 @@
/**
* 用户服务
* 处理用户信息相关的 API 请求
*/
import { request } from '@/src/utils/api';
import type { User, UpdateProfileFormData } from '@/src/schemas/user';
/**
* API 响应接口
*/
interface ApiResponse<T = any> {
code: number;
message: string;
data: T;
}
/**
* 用户服务类
*/
class UserService {
/**
* 获取当前用户信息
*/
async getCurrentUser(): Promise<User> {
const response = await request.get<ApiResponse<User>>('/user/me');
return response.data;
}
/**
* 获取用户信息(通过 ID
*/
async getUserById(userId: string): Promise<User> {
const response = await request.get<ApiResponse<User>>(`/user/${userId}`);
return response.data;
}
/**
* 更新用户资料
*/
async updateProfile(data: UpdateProfileFormData): Promise<User> {
const response = await request.put<ApiResponse<User>>('/user/profile', data);
return response.data;
}
/**
* 上传头像
*/
async uploadAvatar(file: File | Blob): Promise<{ url: string }> {
const formData = new FormData();
formData.append('avatar', file);
const response = await request.post<ApiResponse<{ url: string }>>(
'/user/avatar',
formData,
{
headers: {
'Content-Type': 'multipart/form-data',
},
}
);
return response.data;
}
/**
* 绑定手机号
*/
async bindPhone(phone: string, code: string): Promise<void> {
await request.post('/user/bind-phone', { phone, code });
}
/**
* 绑定邮箱
*/
async bindEmail(email: string, code: string): Promise<void> {
await request.post('/user/bind-email', { email, code });
}
/**
* 注销账号
*/
async deleteAccount(): Promise<void> {
await request.delete('/user/account');
}
}
// 导出单例
export const userService = new UserService();
export default userService;

147
src/stores/settingsStore.ts Normal file
View File

@@ -0,0 +1,147 @@
/**
* 应用设置状态管理
* 使用 Zustand + AsyncStorage 持久化
*/
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
/**
* 主题类型
*/
export type Theme = 'light' | 'dark' | 'auto';
/**
* 语言类型
*/
export type Language = 'zh-CN' | 'en-US';
/**
* 设置状态接口
*/
interface SettingsState {
// 状态
theme: Theme;
language: Language;
notificationsEnabled: boolean;
soundEnabled: boolean;
hapticsEnabled: boolean;
// 操作
setTheme: (theme: Theme) => void;
setLanguage: (language: Language) => void;
setNotificationsEnabled: (enabled: boolean) => void;
setSoundEnabled: (enabled: boolean) => void;
setHapticsEnabled: (enabled: boolean) => void;
resetSettings: () => void;
}
/**
* 默认设置
*/
const DEFAULT_SETTINGS = {
theme: 'auto' as Theme,
language: 'zh-CN' as Language,
notificationsEnabled: true,
soundEnabled: true,
hapticsEnabled: true,
};
/**
* 设置状态 Store
*/
export const useSettingsStore = create<SettingsState>()(
persist(
(set) => ({
// 初始状态
...DEFAULT_SETTINGS,
// 设置主题
setTheme: (theme) => {
set({ theme });
if (__DEV__) {
console.log('🎨 Theme changed:', theme);
}
},
// 设置语言
setLanguage: (language) => {
set({ language });
if (__DEV__) {
console.log('🌐 Language changed:', language);
}
},
// 设置通知开关
setNotificationsEnabled: (enabled) => {
set({ notificationsEnabled: enabled });
if (__DEV__) {
console.log('🔔 Notifications:', enabled ? 'enabled' : 'disabled');
}
},
// 设置声音开关
setSoundEnabled: (enabled) => {
set({ soundEnabled: enabled });
if (__DEV__) {
console.log('🔊 Sound:', enabled ? 'enabled' : 'disabled');
}
},
// 设置触觉反馈开关
setHapticsEnabled: (enabled) => {
set({ hapticsEnabled: enabled });
if (__DEV__) {
console.log('📳 Haptics:', enabled ? 'enabled' : 'disabled');
}
},
// 重置所有设置
resetSettings: () => {
set(DEFAULT_SETTINGS);
if (__DEV__) {
console.log('🔄 Settings reset to default');
}
},
}),
{
name: 'settings-storage',
storage: createJSONStorage(() => AsyncStorage),
}
)
);
/**
* 选择器 Hooks
*/
// 获取主题
export const useTheme = () => useSettingsStore((state) => state.theme);
// 获取语言
export const useLanguage = () => useSettingsStore((state) => state.language);
// 获取通知状态
export const useNotificationsEnabled = () =>
useSettingsStore((state) => state.notificationsEnabled);
// 获取声音状态
export const useSoundEnabled = () =>
useSettingsStore((state) => state.soundEnabled);
// 获取触觉反馈状态
export const useHapticsEnabled = () =>
useSettingsStore((state) => state.hapticsEnabled);
// 获取设置操作方法
export const useSettingsActions = () =>
useSettingsStore((state) => ({
setTheme: state.setTheme,
setLanguage: state.setLanguage,
setNotificationsEnabled: state.setNotificationsEnabled,
setSoundEnabled: state.setSoundEnabled,
setHapticsEnabled: state.setHapticsEnabled,
resetSettings: state.resetSettings,
}));

142
src/stores/userStore.ts Normal file
View File

@@ -0,0 +1,142 @@
/**
* 用户状态管理
* 使用 Zustand + AsyncStorage 持久化
*/
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
/**
* 用户信息接口
*/
export interface User {
id: string;
username: string;
email: string;
avatar?: string;
nickname?: string;
phone?: string;
createdAt?: string;
}
/**
* 用户状态接口
*/
interface UserState {
// 状态
user: User | null;
isLoggedIn: boolean;
token: string | null;
// 操作
setUser: (user: User) => void;
setToken: (token: string) => void;
login: (user: User, token: string) => void;
logout: () => void;
updateUser: (updates: Partial<User>) => void;
}
/**
* 用户状态 Store
*/
export const useUserStore = create<UserState>()(
persist(
(set, get) => ({
// 初始状态
user: null,
isLoggedIn: false,
token: null,
// 设置用户信息
setUser: (user) => {
set({ user, isLoggedIn: true });
},
// 设置 token
setToken: (token) => {
set({ token });
},
// 登录
login: (user, token) => {
set({
user,
token,
isLoggedIn: true,
});
// 同时保存 token 到 AsyncStorage用于 API 请求)
AsyncStorage.setItem('auth_token', token);
if (__DEV__) {
console.log('✅ User logged in:', user.username);
}
},
// 登出
logout: () => {
set({
user: null,
token: null,
isLoggedIn: false,
});
// 清除 AsyncStorage 中的 token
AsyncStorage.removeItem('auth_token');
if (__DEV__) {
console.log('👋 User logged out');
}
},
// 更新用户信息
updateUser: (updates) => {
const currentUser = get().user;
if (currentUser) {
set({
user: { ...currentUser, ...updates },
});
if (__DEV__) {
console.log('📝 User updated:', updates);
}
}
},
}),
{
name: 'user-storage', // AsyncStorage 中的键名
storage: createJSONStorage(() => AsyncStorage),
// 可以选择性地持久化某些字段
partialize: (state) => ({
user: state.user,
token: state.token,
isLoggedIn: state.isLoggedIn,
}),
}
)
);
/**
* 选择器 Hooks优化性能避免不必要的重渲染
*/
// 获取用户信息
export const useUser = () => useUserStore((state) => state.user);
// 获取登录状态
export const useIsLoggedIn = () => useUserStore((state) => state.isLoggedIn);
// 获取 token
export const useToken = () => useUserStore((state) => state.token);
// 获取用户操作方法
export const useUserActions = () =>
useUserStore((state) => ({
setUser: state.setUser,
setToken: state.setToken,
login: state.login,
logout: state.logout,
updateUser: state.updateUser,
}));

81
src/types/index.ts Normal file
View File

@@ -0,0 +1,81 @@
/**
* 全局类型定义
*/
/**
* API 响应基础接口
*/
export interface ApiResponse<T = any> {
code: number;
message: string;
data: T;
timestamp?: number;
}
/**
* 分页参数
*/
export interface PaginationParams {
page: number;
pageSize: number;
sortBy?: string;
sortOrder?: 'asc' | 'desc';
}
/**
* 分页响应
*/
export interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
/**
* 错误响应
*/
export interface ErrorResponse {
code: number;
message: string;
errors?: Record<string, string[]>;
}
/**
* 上传文件响应
*/
export interface UploadResponse {
url: string;
filename: string;
size: number;
mimeType: string;
}
/**
* 导航参数类型
*/
export type RootStackParamList = {
Home: undefined;
Profile: { userId: string };
Settings: undefined;
Login: undefined;
Register: undefined;
// 添加更多路由...
};
/**
* 环境变量类型
*/
declare global {
namespace NodeJS {
interface ProcessEnv {
EXPO_PUBLIC_API_URL: string;
EXPO_PUBLIC_APP_NAME: string;
EXPO_PUBLIC_APP_VERSION: string;
}
}
}
export {};

144
src/utils/api.ts Normal file
View File

@@ -0,0 +1,144 @@
/**
* Axios API 配置
* 统一管理 HTTP 请求
*/
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios';
import AsyncStorage from '@react-native-async-storage/async-storage';
// API 基础配置
const API_CONFIG = {
baseURL: process.env.EXPO_PUBLIC_API_URL || 'https://api.example.com',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
};
// 创建 axios 实例
const api = axios.create(API_CONFIG);
/**
* 请求拦截器
* 在请求发送前添加 token 等信息
*/
api.interceptors.request.use(
async (config) => {
try {
// 从本地存储获取 token
const token = await AsyncStorage.getItem('auth_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// 打印请求信息(开发环境)
if (__DEV__) {
console.log('📤 API Request:', {
method: config.method?.toUpperCase(),
url: config.url,
data: config.data,
});
}
return config;
} catch (error) {
console.error('Request interceptor error:', error);
return config;
}
},
(error) => {
console.error('Request error:', error);
return Promise.reject(error);
}
);
/**
* 响应拦截器
* 统一处理响应和错误
*/
api.interceptors.response.use(
(response: AxiosResponse) => {
// 打印响应信息(开发环境)
if (__DEV__) {
console.log('📥 API Response:', {
url: response.config.url,
status: response.status,
data: response.data,
});
}
// 返回响应数据
return response.data;
},
async (error: AxiosError) => {
// 打印错误信息
console.error('❌ API Error:', {
url: error.config?.url,
status: error.response?.status,
message: error.message,
data: error.response?.data,
});
// 处理不同的错误状态码
if (error.response) {
switch (error.response.status) {
case 401:
// 未授权,清除 token 并跳转到登录页
await AsyncStorage.removeItem('auth_token');
// TODO: 导航到登录页
// router.replace('/login');
break;
case 403:
// 禁止访问
console.error('Access forbidden');
break;
case 404:
// 资源不存在
console.error('Resource not found');
break;
case 500:
// 服务器错误
console.error('Server error');
break;
default:
console.error('Unknown error');
}
} else if (error.request) {
// 请求已发送但没有收到响应
console.error('No response received');
} else {
// 请求配置出错
console.error('Request configuration error');
}
return Promise.reject(error);
}
);
/**
* 通用请求方法
*/
export const request = {
get: <T = any>(url: string, config?: AxiosRequestConfig) =>
api.get<T, T>(url, config),
post: <T = any>(url: string, data?: any, config?: AxiosRequestConfig) =>
api.post<T, T>(url, data, config),
put: <T = any>(url: string, data?: any, config?: AxiosRequestConfig) =>
api.put<T, T>(url, data, config),
delete: <T = any>(url: string, config?: AxiosRequestConfig) =>
api.delete<T, T>(url, config),
patch: <T = any>(url: string, data?: any, config?: AxiosRequestConfig) =>
api.patch<T, T>(url, data, config),
};
export default api;

219
src/utils/date.ts Normal file
View File

@@ -0,0 +1,219 @@
/**
* Day.js 日期工具函数
* 统一管理日期格式化和处理
*/
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import calendar from 'dayjs/plugin/calendar';
import duration from 'dayjs/plugin/duration';
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
import 'dayjs/locale/zh-cn';
// 扩展插件
dayjs.extend(relativeTime);
dayjs.extend(calendar);
dayjs.extend(duration);
dayjs.extend(isSameOrBefore);
dayjs.extend(isSameOrAfter);
// 设置默认语言为中文
dayjs.locale('zh-cn');
/**
* 日期格式常量
*/
export const DATE_FORMATS = {
FULL: 'YYYY-MM-DD HH:mm:ss',
DATE: 'YYYY-MM-DD',
TIME: 'HH:mm:ss',
DATE_TIME: 'YYYY-MM-DD HH:mm',
MONTH_DAY: 'MM-DD',
HOUR_MINUTE: 'HH:mm',
YEAR_MONTH: 'YYYY-MM',
CHINESE_DATE: 'YYYY年MM月DD日',
CHINESE_FULL: 'YYYY年MM月DD日 HH:mm:ss',
} as const;
/**
* 格式化日期
* @param date 日期Date、时间戳或字符串
* @param format 格式默认YYYY-MM-DD HH:mm:ss
*/
export const formatDate = (
date: Date | string | number,
format: string = DATE_FORMATS.FULL
): string => {
return dayjs(date).format(format);
};
/**
* 格式化为相对时间3分钟前、2小时前
*/
export const formatRelativeTime = (date: Date | string | number): string => {
return dayjs(date).fromNow();
};
/**
* 格式化为日历时间(如:今天、昨天、上周)
*/
export const formatCalendarTime = (date: Date | string | number): string => {
return dayjs(date).calendar(null, {
sameDay: '[今天] HH:mm',
lastDay: '[昨天] HH:mm',
lastWeek: 'MM-DD HH:mm',
sameElse: 'YYYY-MM-DD HH:mm',
});
};
/**
* 格式化聊天时间
* 今天:显示时间
* 昨天:显示"昨天 + 时间"
* 本年:显示"月-日 时间"
* 往年:显示"年-月-日"
*/
export const formatChatTime = (timestamp: number | string | Date): string => {
const date = dayjs(timestamp);
const now = dayjs();
if (date.isSame(now, 'day')) {
// 今天
return date.format('HH:mm');
} else if (date.isSame(now.subtract(1, 'day'), 'day')) {
// 昨天
return '昨天 ' + date.format('HH:mm');
} else if (date.isSame(now, 'year')) {
// 本年
return date.format('MM-DD HH:mm');
} else {
// 往年
return date.format('YYYY-MM-DD');
}
};
/**
* 获取时间差(返回对象)
*/
export const getTimeDiff = (
startDate: Date | string | number,
endDate: Date | string | number = new Date()
) => {
const start = dayjs(startDate);
const end = dayjs(endDate);
const diff = end.diff(start);
const duration = dayjs.duration(diff);
return {
years: duration.years(),
months: duration.months(),
days: duration.days(),
hours: duration.hours(),
minutes: duration.minutes(),
seconds: duration.seconds(),
milliseconds: duration.milliseconds(),
totalDays: Math.floor(duration.asDays()),
totalHours: Math.floor(duration.asHours()),
totalMinutes: Math.floor(duration.asMinutes()),
totalSeconds: Math.floor(duration.asSeconds()),
};
};
/**
* 判断是否是今天
*/
export const isToday = (date: Date | string | number): boolean => {
return dayjs(date).isSame(dayjs(), 'day');
};
/**
* 判断是否是昨天
*/
export const isYesterday = (date: Date | string | number): boolean => {
return dayjs(date).isSame(dayjs().subtract(1, 'day'), 'day');
};
/**
* 判断是否是本周
*/
export const isThisWeek = (date: Date | string | number): boolean => {
return dayjs(date).isSame(dayjs(), 'week');
};
/**
* 判断是否是本月
*/
export const isThisMonth = (date: Date | string | number): boolean => {
return dayjs(date).isSame(dayjs(), 'month');
};
/**
* 判断是否是本年
*/
export const isThisYear = (date: Date | string | number): boolean => {
return dayjs(date).isSame(dayjs(), 'year');
};
/**
* 获取日期范围的开始和结束
*/
export const getDateRange = (type: 'day' | 'week' | 'month' | 'year') => {
const now = dayjs();
return {
start: now.startOf(type).toDate(),
end: now.endOf(type).toDate(),
};
};
/**
* 添加时间
*/
export const addTime = (
date: Date | string | number,
amount: number,
unit: dayjs.ManipulateType
): Date => {
return dayjs(date).add(amount, unit).toDate();
};
/**
* 减去时间
*/
export const subtractTime = (
date: Date | string | number,
amount: number,
unit: dayjs.ManipulateType
): Date => {
return dayjs(date).subtract(amount, unit).toDate();
};
/**
* 判断日期是否在范围内
*/
export const isBetween = (
date: Date | string | number,
startDate: Date | string | number,
endDate: Date | string | number
): boolean => {
const target = dayjs(date);
return target.isAfter(startDate) && target.isBefore(endDate);
};
/**
* 获取当前时间戳(毫秒)
*/
export const now = (): number => {
return dayjs().valueOf();
};
/**
* 获取当前时间戳(秒)
*/
export const nowInSeconds = (): number => {
return Math.floor(dayjs().valueOf() / 1000);
};
export default dayjs;

181
src/utils/storage.ts Normal file
View File

@@ -0,0 +1,181 @@
/**
* AsyncStorage 封装工具
* 提供类型安全的本地存储操作
*/
import AsyncStorage from '@react-native-async-storage/async-storage';
/**
* 存储键名常量
*/
export const STORAGE_KEYS = {
AUTH_TOKEN: 'auth_token',
USER_INFO: 'user_info',
SETTINGS: 'settings',
THEME: 'theme',
LANGUAGE: 'language',
} as const;
/**
* Storage 工具类
*/
class Storage {
/**
* 存储字符串
*/
static async setString(key: string, value: string): Promise<void> {
try {
await AsyncStorage.setItem(key, value);
if (__DEV__) {
console.log(`💾 Storage set: ${key}`);
}
} catch (error) {
console.error(`Storage setString error for key "${key}":`, error);
throw error;
}
}
/**
* 获取字符串
*/
static async getString(key: string): Promise<string | null> {
try {
const value = await AsyncStorage.getItem(key);
if (__DEV__) {
console.log(`📖 Storage get: ${key}`, value ? '✓' : '✗');
}
return value;
} catch (error) {
console.error(`Storage getString error for key "${key}":`, error);
return null;
}
}
/**
* 存储对象(自动序列化为 JSON
*/
static async setObject<T>(key: string, value: T): Promise<void> {
try {
const jsonValue = JSON.stringify(value);
await AsyncStorage.setItem(key, jsonValue);
if (__DEV__) {
console.log(`💾 Storage set object: ${key}`);
}
} catch (error) {
console.error(`Storage setObject error for key "${key}":`, error);
throw error;
}
}
/**
* 获取对象(自动反序列化 JSON
*/
static async getObject<T>(key: string): Promise<T | null> {
try {
const jsonValue = await AsyncStorage.getItem(key);
if (jsonValue === null) {
return null;
}
const value = JSON.parse(jsonValue) as T;
if (__DEV__) {
console.log(`📖 Storage get object: ${key}`);
}
return value;
} catch (error) {
console.error(`Storage getObject error for key "${key}":`, error);
return null;
}
}
/**
* 删除指定键
*/
static async remove(key: string): Promise<void> {
try {
await AsyncStorage.removeItem(key);
if (__DEV__) {
console.log(`🗑️ Storage remove: ${key}`);
}
} catch (error) {
console.error(`Storage remove error for key "${key}":`, error);
throw error;
}
}
/**
* 清空所有存储
*/
static async clear(): Promise<void> {
try {
await AsyncStorage.clear();
if (__DEV__) {
console.log('🗑️ Storage cleared all');
}
} catch (error) {
console.error('Storage clear error:', error);
throw error;
}
}
/**
* 获取所有键名
*/
static async getAllKeys(): Promise<string[]> {
try {
const keys = await AsyncStorage.getAllKeys();
if (__DEV__) {
console.log('🔑 Storage all keys:', keys);
}
return keys;
} catch (error) {
console.error('Storage getAllKeys error:', error);
return [];
}
}
/**
* 批量获取
*/
static async multiGet(keys: string[]): Promise<[string, string | null][]> {
try {
const values = await AsyncStorage.multiGet(keys);
return values;
} catch (error) {
console.error('Storage multiGet error:', error);
return [];
}
}
/**
* 批量设置
*/
static async multiSet(keyValuePairs: [string, string][]): Promise<void> {
try {
await AsyncStorage.multiSet(keyValuePairs);
if (__DEV__) {
console.log(`💾 Storage multiSet: ${keyValuePairs.length} items`);
}
} catch (error) {
console.error('Storage multiSet error:', error);
throw error;
}
}
/**
* 批量删除
*/
static async multiRemove(keys: string[]): Promise<void> {
try {
await AsyncStorage.multiRemove(keys);
if (__DEV__) {
console.log(`🗑️ Storage multiRemove: ${keys.length} items`);
}
} catch (error) {
console.error('Storage multiRemove error:', error);
throw error;
}
}
}
export default Storage;