feat: update
This commit is contained in:
26
hooks/index.ts
Normal file
26
hooks/index.ts
Normal 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';
|
||||
23
hooks/useClientOnlyValue.ts
Normal file
23
hooks/useClientOnlyValue.ts
Normal 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;
|
||||
}
|
||||
|
||||
34
hooks/useClientOnlyValue.web.ts
Normal file
34
hooks/useClientOnlyValue.web.ts
Normal 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
82
hooks/useDebounce.ts
Normal 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
113
hooks/useHaptics.ts
Normal 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
291
hooks/useRequest.ts
Normal 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
100
hooks/useTheme.ts
Normal 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
60
hooks/useThrottle.ts
Normal 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>
|
||||
*/
|
||||
Reference in New Issue
Block a user