feat: update

This commit is contained in:
2025-11-06 16:37:01 +08:00
parent c0d54b8513
commit 855f289579
59 changed files with 3398 additions and 572 deletions

26
hooks/index.ts Normal file
View File

@@ -0,0 +1,26 @@
/**
* Hooks 模块统一导出
*/
// Debounce
export { useDebounce, useDebouncedCallback } from './useDebounce';
// Throttle
export { useThrottle } from './useThrottle';
// Haptics
export { useHaptics } from './useHaptics';
// Request
export { useRequest } from './useRequest';
// Theme
export {
useColorScheme,
useThemeColor,
useThemeColors,
useThemeInfo,
} from './useTheme';
// Client-only value (for SSR/Web compatibility)
export { useClientOnlyValue } from './useClientOnlyValue';

View File

@@ -0,0 +1,23 @@
/**
* Client-only value Hook
*
* This hook is used to provide different values for server-side rendering (SSR)
* and client-side rendering. It's particularly useful for React Native Web
* to prevent hydration errors.
*
* @param server - Value to use during server-side rendering
* @param client - Value to use during client-side rendering
* @returns The appropriate value based on the rendering context
*
* @example
* ```tsx
* // Disable header on server, enable on client
* headerShown: useClientOnlyValue(false, true)
* ```
*/
// This function is web-only as native doesn't currently support server (or build-time) rendering.
export function useClientOnlyValue<S, C>(server: S, client: C): S | C {
return client;
}

View File

@@ -0,0 +1,34 @@
/**
* Client-only value Hook (Web version)
*
* This hook is used to provide different values for server-side rendering (SSR)
* and client-side rendering on web platforms.
*
* On web, we use `useEffect` to detect if we're on the client or server,
* since `useEffect` is not invoked during server rendering.
*
* @param server - Value to use during server-side rendering
* @param client - Value to use during client-side rendering
* @returns The appropriate value based on the rendering context
*
* @example
* ```tsx
* // Disable header on server, enable on client
* headerShown: useClientOnlyValue(false, true)
* ```
*/
import React from 'react';
// `useEffect` is not invoked during server rendering, meaning
// we can use this to determine if we're on the server or not.
export function useClientOnlyValue<S, C>(server: S, client: C): S | C {
const [value, setValue] = React.useState<S | C>(server);
React.useEffect(() => {
setValue(client);
}, [client]);
return value;
}

82
hooks/useDebounce.ts Normal file
View File

@@ -0,0 +1,82 @@
/**
* 防抖 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);
*/

113
hooks/useHaptics.ts Normal file
View File

@@ -0,0 +1,113 @@
/**
* 触觉反馈 Hook
* 封装 Expo Haptics 功能
*/
import { useCallback } from 'react';
import * as Haptics from 'expo-haptics';
import { useHapticsEnabled } from '@/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>
* );
* }
*/

291
hooks/useRequest.ts Normal file
View File

@@ -0,0 +1,291 @@
/**
* 请求 Hook
* 提供统一的请求状态管理
*/
import { useState, useCallback, useRef, useEffect } from 'react';
import { AxiosError } from 'axios';
import type { RequestConfig } from '@/utils/network/api';
/**
* 请求状态
*/
export interface RequestState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
/**
* 请求选项
*/
export interface UseRequestOptions<T> extends RequestConfig {
/** 是否立即执行 */
immediate?: boolean;
/** 成功回调 */
onSuccess?: (data: T) => void;
/** 失败回调 */
onError?: (error: Error) => void;
/** 完成回调(无论成功失败) */
onFinally?: () => void;
/** 默认数据 */
defaultData?: T;
}
/**
* 请求 Hook
*
* @example
* ```tsx
* const { data, loading, error, run, refresh } = useRequest(
* () => request.get('/api/users'),
* { immediate: true }
* );
* ```
*/
export function useRequest<T = any>(
requestFn: () => Promise<T>,
options: UseRequestOptions<T> = {}
) {
const { immediate = false, onSuccess, onError, onFinally, defaultData = null } = options;
const [state, setState] = useState<RequestState<T>>({
data: defaultData,
loading: false,
error: null,
});
const requestRef = useRef(requestFn);
requestRef.current = requestFn;
const abortControllerRef = useRef<AbortController | null>(null);
/**
* 执行请求
*/
const run = useCallback(
async (...args: any[]) => {
// 取消之前的请求
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
// 创建新的 AbortController
abortControllerRef.current = new AbortController();
setState((prev) => ({
...prev,
loading: true,
error: null,
}));
try {
const data = await requestRef.current();
setState({
data,
loading: false,
error: null,
});
onSuccess?.(data);
return data;
} catch (error) {
const err = error as Error;
setState((prev) => ({
...prev,
loading: false,
error: err,
}));
onError?.(err);
throw error;
} finally {
onFinally?.();
}
},
[onSuccess, onError, onFinally]
);
/**
* 刷新(重新执行请求)
*/
const refresh = useCallback(() => {
return run();
}, [run]);
/**
* 重置状态
*/
const reset = useCallback(() => {
setState({
data: defaultData,
loading: false,
error: null,
});
}, [defaultData]);
/**
* 取消请求
*/
const cancel = useCallback(() => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
}, []);
// 立即执行
useEffect(() => {
if (immediate) {
run();
}
// 组件卸载时取消请求
return () => {
cancel();
};
}, [immediate]); // eslint-disable-line react-hooks/exhaustive-deps
return {
...state,
run,
refresh,
reset,
cancel,
};
}
/**
* 分页请求 Hook
*
* @example
* ```tsx
* const { data, loading, loadMore, refresh, hasMore } = usePagination(
* (page, pageSize) => request.get('/api/users', { params: { page, pageSize } })
* );
* ```
*/
export function usePagination<T = any>(
requestFn: (
page: number,
pageSize: number
) => Promise<{
list: T[];
total: number;
hasMore: boolean;
}>,
options: {
pageSize?: number;
immediate?: boolean;
onSuccess?: (data: T[]) => void;
onError?: (error: Error) => void;
} = {}
) {
const { pageSize = 20, immediate = false, onSuccess, onError } = options;
const [state, setState] = useState({
data: [] as T[],
loading: false,
loadingMore: false,
error: null as Error | null,
page: 1,
total: 0,
hasMore: true,
});
/**
* 加载数据
*/
const load = useCallback(
async (page: number, append = false) => {
setState((prev) => ({
...prev,
loading: !append,
loadingMore: append,
error: null,
}));
try {
const result = await requestFn(page, pageSize);
setState((prev) => ({
...prev,
data: append ? [...prev.data, ...result.list] : result.list,
loading: false,
loadingMore: false,
page,
total: result.total,
hasMore: result.hasMore,
}));
onSuccess?.(result.list);
return result;
} catch (error) {
const err = error as Error;
setState((prev) => ({
...prev,
loading: false,
loadingMore: false,
error: err,
}));
onError?.(err);
throw error;
}
},
[requestFn, pageSize, onSuccess, onError]
);
/**
* 加载更多
*/
const loadMore = useCallback(async () => {
if (state.loadingMore || !state.hasMore) {
return;
}
return load(state.page + 1, true);
}, [state.loadingMore, state.hasMore, state.page, load]);
/**
* 刷新(重新加载第一页)
*/
const refresh = useCallback(async () => {
return load(1, false);
}, [load]);
/**
* 重置
*/
const reset = useCallback(() => {
setState({
data: [],
loading: false,
loadingMore: false,
error: null,
page: 1,
total: 0,
hasMore: true,
});
}, []);
// 立即执行
useEffect(() => {
if (immediate) {
load(1, false);
}
}, [immediate]); // eslint-disable-line react-hooks/exhaustive-deps
return {
...state,
loadMore,
refresh,
reset,
};
}

100
hooks/useTheme.ts Normal file
View File

@@ -0,0 +1,100 @@
/**
* 主题 Hooks
*
* 提供统一的主题访问接口
*/
import { useMemo } from 'react';
import { useColorScheme as useSystemColorScheme } from 'react-native';
import { useTheme as useThemeStore } from '@/stores';
import Colors from '@/constants/Colors';
/**
* 获取当前颜色方案light | dark
*
* 从 settingsStore 读取用户设置的主题
* 支持 'light' | 'dark' | 'auto' 三种模式
*/
export function useColorScheme(): 'light' | 'dark' {
const userTheme = useThemeStore();
const systemTheme = useSystemColorScheme();
// 如果用户选择了 'auto',则使用系统主题
if (userTheme === 'auto') {
return systemTheme === 'dark' ? 'dark' : 'light';
}
// 否则使用用户选择的主题
return userTheme;
}
/**
* 获取主题颜色
*
* @param props - 可选的自定义颜色 { light?: string; dark?: string }
* @param colorName - Colors 中定义的颜色名称
* @returns 当前主题对应的颜色值
*
* @example
* ```tsx
* const textColor = useThemeColor({}, 'text');
* const customColor = useThemeColor({ light: '#000', dark: '#fff' }, 'text');
* ```
*/
export function useThemeColor(
props: { light?: string; dark?: string },
colorName: keyof typeof Colors.light & keyof typeof Colors.dark
): string {
const theme = useColorScheme();
const colorFromProps = props[theme];
if (colorFromProps) {
return colorFromProps;
} else {
return Colors[theme][colorName];
}
}
/**
* 获取完整的主题颜色对象
*
* @returns 当前主题的所有颜色配置
*
* @example
* ```tsx
* const colors = useThemeColors();
* <View style={{ backgroundColor: colors.background }}>
* <Text style={{ color: colors.text }}>Hello</Text>
* </View>
* ```
*/
export function useThemeColors() {
const theme = useColorScheme();
return useMemo(() => {
return Colors[theme];
}, [theme]);
}
/**
* 获取主题相关的所有信息
*
* @returns 主题信息对象
*
* @example
* ```tsx
* const { theme, colors, isDark } = useThemeInfo();
* ```
*/
export function useThemeInfo() {
const theme = useColorScheme();
const colors = useThemeColors();
return useMemo(() => ({
theme,
colors,
isDark: theme === 'dark',
isLight: theme === 'light',
}), [theme, colors]);
}

60
hooks/useThrottle.ts Normal file
View File

@@ -0,0 +1,60 @@
/**
* 节流 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>
*/