feat: new project
This commit is contained in:
84
src/hooks/useDebounce.ts
Normal file
84
src/hooks/useDebounce.ts
Normal 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
114
src/hooks/useHaptics.ts
Normal 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
61
src/hooks/useThrottle.ts
Normal 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
29
src/index.ts
Normal 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
130
src/schemas/auth.ts
Normal 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
76
src/schemas/user.ts
Normal 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
134
src/services/authService.ts
Normal 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;
|
||||
|
||||
90
src/services/userService.ts
Normal file
90
src/services/userService.ts
Normal 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
147
src/stores/settingsStore.ts
Normal 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
142
src/stores/userStore.ts
Normal 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
81
src/types/index.ts
Normal 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
144
src/utils/api.ts
Normal 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
219
src/utils/date.ts
Normal 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
181
src/utils/storage.ts
Normal 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;
|
||||
|
||||
Reference in New Issue
Block a user