feat:回退操作
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { refreshAuthToken, isTokenExpiringSoon, clearToken, requestDeduplicator } from './utils';
|
import { refreshAuthToken, isTokenExpiringSoon, clearToken } from './utils';
|
||||||
|
|
||||||
// Token过期处理
|
// Token过期处理
|
||||||
export const handleTokenExpired = () => {
|
export const handleTokenExpired = () => {
|
||||||
@@ -6,9 +6,6 @@ export const handleTokenExpired = () => {
|
|||||||
// 清除本地存储
|
// 清除本地存储
|
||||||
clearToken();
|
clearToken();
|
||||||
|
|
||||||
// 清除所有待处理的请求
|
|
||||||
requestDeduplicator.clear();
|
|
||||||
|
|
||||||
// 跳转到登录页面
|
// 跳转到登录页面
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
@@ -128,8 +125,6 @@ export const setupNetworkListener = () => {
|
|||||||
|
|
||||||
const handleOffline = () => {
|
const handleOffline = () => {
|
||||||
console.log('网络已断开');
|
console.log('网络已断开');
|
||||||
// 清除所有待处理的请求
|
|
||||||
requestDeduplicator.clear();
|
|
||||||
showApiError(null, '网络连接已断开,请检查网络设置');
|
showApiError(null, '网络连接已断开,请检查网络设置');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -153,7 +148,5 @@ export const initInterceptors = () => {
|
|||||||
if (cleanupNetwork) {
|
if (cleanupNetwork) {
|
||||||
cleanupNetwork();
|
cleanupNetwork();
|
||||||
}
|
}
|
||||||
// 清理所有待处理的请求
|
|
||||||
requestDeduplicator.clear();
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||||
import { requestInterceptor, responseInterceptor, errorInterceptor } from './interceptors';
|
import { requestInterceptor, responseInterceptor, errorInterceptor } from './interceptors';
|
||||||
import { requestDeduplicator, throttle } from './utils';
|
|
||||||
|
|
||||||
// 创建axios实例
|
// 创建axios实例
|
||||||
const request: AxiosInstance = axios.create({
|
const request: AxiosInstance = axios.create({
|
||||||
@@ -49,52 +48,26 @@ request.interceptors.response.use(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// 带节流和去重的GET请求
|
// 封装GET请求
|
||||||
export const get = <T = any>(url: string, config?: AxiosRequestConfig): Promise<T> => {
|
export const get = <T = any>(url: string, config?: AxiosRequestConfig): Promise<T> => {
|
||||||
return requestDeduplicator.execute(
|
return request.get(url, config);
|
||||||
'GET',
|
|
||||||
url,
|
|
||||||
() => request.get(url, config),
|
|
||||||
config
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 带节流和去重的POST请求
|
// 封装POST请求
|
||||||
export const post = <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> => {
|
export const post = <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> => {
|
||||||
return requestDeduplicator.execute(
|
return request.post(url, data, config);
|
||||||
'POST',
|
|
||||||
url,
|
|
||||||
() => request.post(url, data, config),
|
|
||||||
data
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 带节流和去重的PUT请求
|
// 封装PUT请求
|
||||||
export const put = <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> => {
|
export const put = <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> => {
|
||||||
return requestDeduplicator.execute(
|
return request.put(url, data, config);
|
||||||
'PUT',
|
|
||||||
url,
|
|
||||||
() => request.put(url, data, config),
|
|
||||||
data
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 带节流和去重的DELETE请求
|
// 封装DELETE请求
|
||||||
export const del = <T = any>(url: string, config?: AxiosRequestConfig): Promise<T> => {
|
export const del = <T = any>(url: string, config?: AxiosRequestConfig): Promise<T> => {
|
||||||
return requestDeduplicator.execute(
|
return request.delete(url, config);
|
||||||
'DELETE',
|
|
||||||
url,
|
|
||||||
() => request.delete(url, config),
|
|
||||||
config
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 带节流的请求函数(用于需要节流但不需要去重的场景)
|
|
||||||
export const throttledGet = throttle(get, 1000);
|
|
||||||
export const throttledPost = throttle(post, 1000);
|
|
||||||
export const throttledPut = throttle(put, 1000);
|
|
||||||
export const throttledDelete = throttle(del, 1000);
|
|
||||||
|
|
||||||
// 导出request实例
|
// 导出request实例
|
||||||
export { request };
|
export { request };
|
||||||
export default request;
|
export default request;
|
||||||
@@ -1,85 +1,5 @@
|
|||||||
import { authApi } from './auth';
|
import { authApi } from './auth';
|
||||||
|
|
||||||
// 节流函数 - 防止重复请求
|
|
||||||
export const throttle = <T extends (...args: any[]) => any>(
|
|
||||||
func: T,
|
|
||||||
delay: number = 1000
|
|
||||||
): T => {
|
|
||||||
let lastCall = 0;
|
|
||||||
let lastCallTimer: NodeJS.Timeout | null = null;
|
|
||||||
|
|
||||||
return ((...args: any[]) => {
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
if (now - lastCall < delay) {
|
|
||||||
// 如果在节流时间内,取消之前的定时器并设置新的
|
|
||||||
if (lastCallTimer) {
|
|
||||||
clearTimeout(lastCallTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
lastCallTimer = setTimeout(() => {
|
|
||||||
lastCall = now;
|
|
||||||
func(...args);
|
|
||||||
}, delay - (now - lastCall));
|
|
||||||
} else {
|
|
||||||
// 如果超过节流时间,直接执行
|
|
||||||
lastCall = now;
|
|
||||||
func(...args);
|
|
||||||
}
|
|
||||||
}) as T;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 请求去重管理器
|
|
||||||
class RequestDeduplicator {
|
|
||||||
private pendingRequests: Map<string, Promise<any>> = new Map();
|
|
||||||
|
|
||||||
// 生成请求的唯一键
|
|
||||||
private generateKey(method: string, url: string, data?: any): string {
|
|
||||||
const dataStr = data ? JSON.stringify(data) : '';
|
|
||||||
return `${method}:${url}:${dataStr}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 执行请求,如果相同请求正在进行中则返回已存在的Promise
|
|
||||||
async execute<T>(
|
|
||||||
method: string,
|
|
||||||
url: string,
|
|
||||||
requestFn: () => Promise<T>,
|
|
||||||
data?: any
|
|
||||||
): Promise<T> {
|
|
||||||
const key = this.generateKey(method, url, data);
|
|
||||||
|
|
||||||
// 如果相同的请求正在进行中,返回已存在的Promise
|
|
||||||
if (this.pendingRequests.has(key)) {
|
|
||||||
console.log(`请求去重: ${key}`);
|
|
||||||
return this.pendingRequests.get(key)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建新的请求Promise
|
|
||||||
const requestPromise = requestFn().finally(() => {
|
|
||||||
// 请求完成后从pendingRequests中移除
|
|
||||||
this.pendingRequests.delete(key);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 将请求Promise存储到Map中
|
|
||||||
this.pendingRequests.set(key, requestPromise);
|
|
||||||
|
|
||||||
return requestPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清除所有待处理的请求
|
|
||||||
clear() {
|
|
||||||
this.pendingRequests.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取当前待处理请求数量
|
|
||||||
getPendingCount(): number {
|
|
||||||
return this.pendingRequests.size;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建全局请求去重实例
|
|
||||||
export const requestDeduplicator = new RequestDeduplicator();
|
|
||||||
|
|
||||||
// 设置token到localStorage
|
// 设置token到localStorage
|
||||||
export const setToken = (token: string) => {
|
export const setToken = (token: string) => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useThrottledRequestWithLoading } from '../hooks/useThrottledRequest';
|
import {
|
||||||
|
useThrottledRequestWithLoading,
|
||||||
|
useThrottledRequestWithError,
|
||||||
|
useRequestWithRetry,
|
||||||
|
useCancellableRequest
|
||||||
|
} from '../hooks/useThrottledRequest';
|
||||||
|
|
||||||
interface ThrottledButtonProps {
|
interface ThrottledButtonProps {
|
||||||
onClick: () => Promise<any>;
|
onClick: () => Promise<any>;
|
||||||
@@ -7,6 +12,14 @@ interface ThrottledButtonProps {
|
|||||||
delay?: number;
|
delay?: number;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
variant?: 'throttle' | 'debounce' | 'retry' | 'cancellable';
|
||||||
|
maxRetries?: number;
|
||||||
|
retryDelay?: number;
|
||||||
|
showLoadingText?: boolean;
|
||||||
|
loadingText?: string;
|
||||||
|
errorText?: string;
|
||||||
|
onSuccess?: (result: any) => void;
|
||||||
|
onError?: (error: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ThrottledButton: React.FC<ThrottledButtonProps> = ({
|
export const ThrottledButton: React.FC<ThrottledButtonProps> = ({
|
||||||
@@ -14,17 +27,265 @@ export const ThrottledButton: React.FC<ThrottledButtonProps> = ({
|
|||||||
children,
|
children,
|
||||||
delay = 1000,
|
delay = 1000,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
className = ''
|
className = '',
|
||||||
|
variant = 'throttle',
|
||||||
|
maxRetries = 3,
|
||||||
|
retryDelay = 1000,
|
||||||
|
showLoadingText = true,
|
||||||
|
loadingText = '处理中...',
|
||||||
|
errorText,
|
||||||
|
onSuccess,
|
||||||
|
onError
|
||||||
}) => {
|
}) => {
|
||||||
|
// 处理请求结果
|
||||||
|
const handleRequest = async () => {
|
||||||
|
try {
|
||||||
|
const result = await onClick();
|
||||||
|
onSuccess?.(result);
|
||||||
|
} catch (error) {
|
||||||
|
onError?.(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据variant渲染不同的按钮
|
||||||
|
const renderButton = () => {
|
||||||
|
switch (variant) {
|
||||||
|
case 'retry':
|
||||||
|
return <RetryButtonContent
|
||||||
|
onClick={handleRequest}
|
||||||
|
maxRetries={maxRetries}
|
||||||
|
retryDelay={retryDelay}
|
||||||
|
loadingText={loadingText}
|
||||||
|
showLoadingText={showLoadingText}
|
||||||
|
disabled={disabled}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</RetryButtonContent>;
|
||||||
|
|
||||||
|
case 'cancellable':
|
||||||
|
return <CancellableButtonContent
|
||||||
|
onClick={handleRequest}
|
||||||
|
loadingText={loadingText}
|
||||||
|
showLoadingText={showLoadingText}
|
||||||
|
disabled={disabled}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</CancellableButtonContent>;
|
||||||
|
|
||||||
|
case 'debounce':
|
||||||
|
return <DebounceButtonContent
|
||||||
|
onClick={handleRequest}
|
||||||
|
delay={delay}
|
||||||
|
loadingText={loadingText}
|
||||||
|
showLoadingText={showLoadingText}
|
||||||
|
disabled={disabled}
|
||||||
|
className={className}
|
||||||
|
errorText={errorText}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</DebounceButtonContent>;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return <ThrottleButtonContent
|
||||||
|
onClick={handleRequest}
|
||||||
|
delay={delay}
|
||||||
|
loadingText={loadingText}
|
||||||
|
showLoadingText={showLoadingText}
|
||||||
|
disabled={disabled}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ThrottleButtonContent>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return renderButton();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 节流按钮内容组件
|
||||||
|
const ThrottleButtonContent: React.FC<{
|
||||||
|
onClick: () => Promise<any>;
|
||||||
|
delay: number;
|
||||||
|
loadingText: string;
|
||||||
|
showLoadingText: boolean;
|
||||||
|
disabled: boolean;
|
||||||
|
className: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}> = ({ onClick, delay, loadingText, showLoadingText, disabled, className, children }) => {
|
||||||
const { throttledRequest, loading } = useThrottledRequestWithLoading(onClick, delay);
|
const { throttledRequest, loading } = useThrottledRequestWithLoading(onClick, delay);
|
||||||
|
|
||||||
|
const getButtonText = () => {
|
||||||
|
return loading && showLoadingText ? loadingText : children;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getButtonClassName = () => {
|
||||||
|
const baseClasses = 'px-4 py-2 rounded font-medium transition-colors duration-200 disabled:cursor-not-allowed';
|
||||||
|
const variantClasses = loading
|
||||||
|
? 'bg-gray-400 text-white cursor-not-allowed'
|
||||||
|
: 'bg-blue-500 text-white hover:bg-blue-600 disabled:bg-gray-400';
|
||||||
|
|
||||||
|
return `${baseClasses} ${variantClasses} ${className}`;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={throttledRequest}
|
onClick={throttledRequest}
|
||||||
disabled={disabled || loading}
|
disabled={disabled || loading}
|
||||||
className={`px-4 py-2 rounded bg-blue-500 text-white hover:bg-blue-600 disabled:bg-gray-400 disabled:cursor-not-allowed ${className}`}
|
className={getButtonClassName()}
|
||||||
>
|
>
|
||||||
{loading ? '处理中...' : children}
|
{getButtonText()}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 防抖按钮内容组件
|
||||||
|
const DebounceButtonContent: React.FC<{
|
||||||
|
onClick: () => Promise<any>;
|
||||||
|
delay: number;
|
||||||
|
loadingText: string;
|
||||||
|
showLoadingText: boolean;
|
||||||
|
disabled: boolean;
|
||||||
|
className: string;
|
||||||
|
errorText?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}> = ({ onClick, delay, loadingText, showLoadingText, disabled, className, errorText, children }) => {
|
||||||
|
const { debouncedRequest, loading, error } = useThrottledRequestWithError(onClick, delay);
|
||||||
|
|
||||||
|
const getButtonText = () => {
|
||||||
|
return loading && showLoadingText ? loadingText : children;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getButtonClassName = () => {
|
||||||
|
const baseClasses = 'px-4 py-2 rounded font-medium transition-colors duration-200 disabled:cursor-not-allowed';
|
||||||
|
let variantClasses = '';
|
||||||
|
if (loading) {
|
||||||
|
variantClasses = 'bg-gray-400 text-white cursor-not-allowed';
|
||||||
|
} else if (error) {
|
||||||
|
variantClasses = 'bg-red-500 text-white hover:bg-red-600';
|
||||||
|
} else {
|
||||||
|
variantClasses = 'bg-blue-500 text-white hover:bg-blue-600 disabled:bg-gray-400';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${baseClasses} ${variantClasses} ${className}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={debouncedRequest}
|
||||||
|
disabled={disabled || loading}
|
||||||
|
className={getButtonClassName()}
|
||||||
|
>
|
||||||
|
{getButtonText()}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{error && errorText && (
|
||||||
|
<span className="text-red-500 text-sm">{errorText}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重试按钮内容组件
|
||||||
|
const RetryButtonContent: React.FC<{
|
||||||
|
onClick: () => Promise<any>;
|
||||||
|
maxRetries: number;
|
||||||
|
retryDelay: number;
|
||||||
|
loadingText: string;
|
||||||
|
showLoadingText: boolean;
|
||||||
|
disabled: boolean;
|
||||||
|
className: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}> = ({ onClick, maxRetries, retryDelay, loadingText, showLoadingText, disabled, className, children }) => {
|
||||||
|
const { requestWithRetry, loading, retryCount } = useRequestWithRetry(onClick, maxRetries, retryDelay);
|
||||||
|
|
||||||
|
const getButtonText = () => {
|
||||||
|
if (loading) {
|
||||||
|
if (retryCount > 0) {
|
||||||
|
return `${loadingText} (重试 ${retryCount}/${maxRetries})`;
|
||||||
|
}
|
||||||
|
return showLoadingText ? loadingText : children;
|
||||||
|
}
|
||||||
|
return children;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getButtonClassName = () => {
|
||||||
|
const baseClasses = 'px-4 py-2 rounded font-medium transition-colors duration-200 disabled:cursor-not-allowed';
|
||||||
|
const variantClasses = loading
|
||||||
|
? 'bg-gray-400 text-white cursor-not-allowed'
|
||||||
|
: 'bg-blue-500 text-white hover:bg-blue-600 disabled:bg-gray-400';
|
||||||
|
|
||||||
|
return `${baseClasses} ${variantClasses} ${className}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={requestWithRetry}
|
||||||
|
disabled={disabled || loading}
|
||||||
|
className={getButtonClassName()}
|
||||||
|
>
|
||||||
|
{getButtonText()}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 可取消按钮内容组件
|
||||||
|
const CancellableButtonContent: React.FC<{
|
||||||
|
onClick: () => Promise<any>;
|
||||||
|
loadingText: string;
|
||||||
|
showLoadingText: boolean;
|
||||||
|
disabled: boolean;
|
||||||
|
className: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}> = ({ onClick, loadingText, showLoadingText, disabled, className, children }) => {
|
||||||
|
const { cancellableRequest, loading, cancelRequest } = useCancellableRequest(onClick);
|
||||||
|
|
||||||
|
const getButtonText = () => {
|
||||||
|
return loading && showLoadingText ? loadingText : children;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getButtonClassName = () => {
|
||||||
|
const baseClasses = 'px-4 py-2 rounded font-medium transition-colors duration-200 disabled:cursor-not-allowed';
|
||||||
|
const variantClasses = loading
|
||||||
|
? 'bg-gray-400 text-white cursor-not-allowed'
|
||||||
|
: 'bg-blue-500 text-white hover:bg-blue-600 disabled:bg-gray-400';
|
||||||
|
|
||||||
|
return `${baseClasses} ${variantClasses} ${className}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={cancellableRequest}
|
||||||
|
disabled={disabled || loading}
|
||||||
|
className={getButtonClassName()}
|
||||||
|
>
|
||||||
|
{getButtonText()}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{loading && cancelRequest && (
|
||||||
|
<button
|
||||||
|
onClick={cancelRequest}
|
||||||
|
className="px-3 py-2 rounded bg-red-500 text-white hover:bg-red-600 text-sm"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导出其他类型的按钮组件
|
||||||
|
export const DebouncedButton: React.FC<Omit<ThrottledButtonProps, 'variant'> & { delay?: number }> = (props) => (
|
||||||
|
<ThrottledButton {...props} variant="debounce" delay={props.delay || 300} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export const RetryButton: React.FC<Omit<ThrottledButtonProps, 'variant'> & { maxRetries?: number; retryDelay?: number }> = (props) => (
|
||||||
|
<ThrottledButton {...props} variant="retry" maxRetries={props.maxRetries || 3} retryDelay={props.retryDelay || 1000} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export const CancellableButton: React.FC<Omit<ThrottledButtonProps, 'variant'>> = (props) => (
|
||||||
|
<ThrottledButton {...props} variant="cancellable" />
|
||||||
|
);
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useRef, useState } from 'react';
|
import { useCallback, useRef, useState, useEffect } from 'react';
|
||||||
import { requestDeduplicator } from '../api/utils';
|
import { requestDeduplicator, requestCancelManager } from '../api/utils';
|
||||||
|
|
||||||
// 节流请求Hook
|
// 节流请求Hook
|
||||||
export const useThrottledRequest = <T extends (...args: any[]) => any>(
|
export const useThrottledRequest = <T extends (...args: any[]) => any>(
|
||||||
@@ -94,4 +94,172 @@ export const useThrottledRequestWithLoading = <T extends (...args: any[]) => Pro
|
|||||||
const throttledRequest = useThrottledRequest(requestWithLoading, delay);
|
const throttledRequest = useThrottledRequest(requestWithLoading, delay);
|
||||||
|
|
||||||
return { throttledRequest, loading };
|
return { throttledRequest, loading };
|
||||||
|
};
|
||||||
|
|
||||||
|
// 带错误处理的请求Hook
|
||||||
|
export const useRequestWithError = <T extends (...args: any[]) => Promise<any>>(
|
||||||
|
requestFn: T
|
||||||
|
) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const requestWithError = useCallback(
|
||||||
|
(async (...args: any[]) => {
|
||||||
|
setError(null);
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await requestFn(...args);
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : '请求失败';
|
||||||
|
setError(errorMessage);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}) as T,
|
||||||
|
[requestFn]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { requestWithError, loading, error, clearError: () => setError(null) };
|
||||||
|
};
|
||||||
|
|
||||||
|
// 带重试的请求Hook
|
||||||
|
export const useRequestWithRetry = <T extends (...args: any[]) => Promise<any>>(
|
||||||
|
requestFn: T,
|
||||||
|
maxRetries: number = 3,
|
||||||
|
retryDelay: number = 1000
|
||||||
|
) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [retryCount, setRetryCount] = useState(0);
|
||||||
|
|
||||||
|
const requestWithRetry = useCallback(
|
||||||
|
(async (...args: any[]) => {
|
||||||
|
setLoading(true);
|
||||||
|
setRetryCount(0);
|
||||||
|
|
||||||
|
let lastError: any;
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
setRetryCount(attempt);
|
||||||
|
const result = await requestFn(...args);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
|
||||||
|
if (attempt === maxRetries) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待后重试
|
||||||
|
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError;
|
||||||
|
}) as T,
|
||||||
|
[requestFn, maxRetries, retryDelay]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { requestWithRetry, loading, retryCount };
|
||||||
|
};
|
||||||
|
|
||||||
|
// 可取消的请求Hook
|
||||||
|
export const useCancellableRequest = <T extends (...args: any[]) => Promise<any>>(
|
||||||
|
requestFn: T
|
||||||
|
) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
|
const cancellableRequest = useCallback(
|
||||||
|
(async (...args: any[]) => {
|
||||||
|
// 取消之前的请求
|
||||||
|
if (abortControllerRef.current) {
|
||||||
|
abortControllerRef.current.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新的AbortController
|
||||||
|
abortControllerRef.current = new AbortController();
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await requestFn(...args);
|
||||||
|
return result;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
abortControllerRef.current = null;
|
||||||
|
}
|
||||||
|
}) as T,
|
||||||
|
[requestFn]
|
||||||
|
);
|
||||||
|
|
||||||
|
const cancelRequest = useCallback(() => {
|
||||||
|
if (abortControllerRef.current) {
|
||||||
|
abortControllerRef.current.abort();
|
||||||
|
setLoading(false);
|
||||||
|
abortControllerRef.current = null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 组件卸载时取消请求
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (abortControllerRef.current) {
|
||||||
|
abortControllerRef.current.abort();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { cancellableRequest, loading, cancelRequest };
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组合Hook:节流 + 加载状态 + 错误处理
|
||||||
|
export const useThrottledRequestWithError = <T extends (...args: any[]) => Promise<any>>(
|
||||||
|
requestFn: T,
|
||||||
|
delay: number = 1000
|
||||||
|
) => {
|
||||||
|
const { requestWithError, loading, error, clearError } = useRequestWithError(requestFn);
|
||||||
|
const throttledRequest = useThrottledRequest(requestWithError, delay);
|
||||||
|
|
||||||
|
return { throttledRequest, loading, error, clearError };
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组合Hook:防抖 + 加载状态 + 错误处理
|
||||||
|
export const useDebouncedRequestWithError = <T extends (...args: any[]) => Promise<any>>(
|
||||||
|
requestFn: T,
|
||||||
|
delay: number = 300
|
||||||
|
) => {
|
||||||
|
const { requestWithError, loading, error, clearError } = useRequestWithError(requestFn);
|
||||||
|
const debouncedRequest = useDebouncedRequest(requestWithError, delay);
|
||||||
|
|
||||||
|
return { debouncedRequest, loading, error, clearError };
|
||||||
|
};
|
||||||
|
|
||||||
|
// 请求状态监控Hook
|
||||||
|
export const useRequestMonitor = () => {
|
||||||
|
const [pendingCount, setPendingCount] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updatePendingCount = () => {
|
||||||
|
setPendingCount(requestDeduplicator.getPendingCount());
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始更新
|
||||||
|
updatePendingCount();
|
||||||
|
|
||||||
|
// 定期检查待处理请求数量
|
||||||
|
const interval = setInterval(updatePendingCount, 100);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const cancelAllRequests = useCallback(() => {
|
||||||
|
requestCancelManager.cancelAllRequests();
|
||||||
|
requestDeduplicator.clear();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { pendingCount, cancelAllRequests };
|
||||||
};
|
};
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { smartGoBack, getBackButtonText } from '@/utils/navigation';
|
|
||||||
import { ChevronLeft, Smartphone, Battery, Wifi, MessageCircle, Users, Settings, History, RefreshCw, Loader2 } from 'lucide-react';
|
import { ChevronLeft, Smartphone, Battery, Wifi, MessageCircle, Users, Settings, History, RefreshCw, Loader2 } from 'lucide-react';
|
||||||
import { devicesApi, fetchDeviceDetail, fetchDeviceRelatedAccounts, fetchDeviceHandleLogs, updateDeviceTaskConfig } from '@/api/devices';
|
import { devicesApi, fetchDeviceDetail, fetchDeviceRelatedAccounts, fetchDeviceHandleLogs, updateDeviceTaskConfig } from '@/api/devices';
|
||||||
import { useToast } from '@/components/ui/toast';
|
import { useToast } from '@/components/ui/toast';
|
||||||
@@ -444,11 +443,11 @@ export default function DeviceDetail() {
|
|||||||
无法加载ID为 "{id}" 的设备信息,请检查设备是否存在。
|
无法加载ID为 "{id}" 的设备信息,请检查设备是否存在。
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => smartGoBack(navigate)}
|
onClick={() => navigate(-1)}
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-colors"
|
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-colors"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="h-4 w-4" />
|
<ChevronLeft className="h-4 w-4" />
|
||||||
{getBackButtonText()}
|
返回上一页
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -463,7 +462,7 @@ export default function DeviceDetail() {
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
onClick={() => smartGoBack(navigate)}
|
onClick={() => navigate(-1)}
|
||||||
>
|
>
|
||||||
<ChevronLeft className="h-5 w-5" />
|
<ChevronLeft className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
@@ -714,7 +713,7 @@ export default function DeviceDetail() {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="font-medium truncate">{account.nickname}</div>
|
<div className="font-medium truncate">{account.nickname}</div>
|
||||||
<span className={`px-2 py-1 text-xs rounded-full ${
|
<span className={`px-2 py-1 text-xs rounded-full ${
|
||||||
account.wechatAliveText === '正常'
|
account.wechatAlive === 1
|
||||||
? "bg-green-100 text-green-700"
|
? "bg-green-100 text-green-700"
|
||||||
: "bg-red-100 text-red-700"
|
: "bg-red-100 text-red-700"
|
||||||
}`}>
|
}`}>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { smartGoBack } from '@/utils/navigation';
|
|
||||||
import { ChevronLeft, Plus, Search, RefreshCw, QrCode, Smartphone, Loader2, AlertTriangle, Trash2, X } from 'lucide-react';
|
import { ChevronLeft, Plus, Search, RefreshCw, QrCode, Smartphone, Loader2, AlertTriangle, Trash2, X } from 'lucide-react';
|
||||||
import { devicesApi } from '@/api';
|
import { devicesApi } from '@/api';
|
||||||
import { useToast } from '@/components/ui/toast';
|
import { useToast } from '@/components/ui/toast';
|
||||||
@@ -382,7 +381,7 @@ export default function Devices() {
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
onClick={() => smartGoBack(navigate)}
|
onClick={() => navigate(-1)}
|
||||||
>
|
>
|
||||||
<ChevronLeft className="h-5 w-5" />
|
<ChevronLeft className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { smartGoBack } from '@/utils/navigation';
|
|
||||||
import { ArrowLeft, Users, TrendingUp, Calendar, Settings, Play, Pause, Edit } from 'lucide-react';
|
import { ArrowLeft, Users, TrendingUp, Calendar, Settings, Play, Pause, Edit } from 'lucide-react';
|
||||||
|
|
||||||
interface PlanData {
|
interface PlanData {
|
||||||
@@ -93,7 +92,7 @@ export default function PlanDetail() {
|
|||||||
<div className="flex items-center justify-between p-4">
|
<div className="flex items-center justify-between p-4">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<button
|
<button
|
||||||
onClick={() => smartGoBack(navigate)}
|
onClick={() => navigate(-1)}
|
||||||
className="mr-3 p-1 hover:bg-gray-100 rounded"
|
className="mr-3 p-1 hover:bg-gray-100 rounded"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-5 w-5" />
|
<ArrowLeft className="h-5 w-5" />
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { smartGoBack } from '../../utils/navigation';
|
|
||||||
import { ArrowLeft, Check } from 'lucide-react';
|
import { ArrowLeft, Check } from 'lucide-react';
|
||||||
|
|
||||||
interface ScenarioOption {
|
interface ScenarioOption {
|
||||||
@@ -88,7 +87,7 @@ export default function NewPlan() {
|
|||||||
<header className="sticky top-0 z-10 bg-white border-b">
|
<header className="sticky top-0 z-10 bg-white border-b">
|
||||||
<div className="flex items-center p-4">
|
<div className="flex items-center p-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => smartGoBack(navigate)}
|
onClick={() => navigate(-1)}
|
||||||
className="mr-3 p-1 hover:bg-gray-100 rounded"
|
className="mr-3 p-1 hover:bg-gray-100 rounded"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-5 w-5" />
|
<ArrowLeft className="h-5 w-5" />
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { smartGoBack } from '../../utils/navigation';
|
|
||||||
import { ArrowLeft, Plus, Users, TrendingUp, Calendar } from 'lucide-react';
|
import { ArrowLeft, Plus, Users, TrendingUp, Calendar } from 'lucide-react';
|
||||||
|
|
||||||
interface Plan {
|
interface Plan {
|
||||||
@@ -171,7 +170,7 @@ export default function ScenarioDetail() {
|
|||||||
<div className="flex items-center justify-between p-4">
|
<div className="flex items-center justify-between p-4">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<button
|
<button
|
||||||
onClick={() => smartGoBack(navigate)}
|
onClick={() => navigate(-1)}
|
||||||
className="mr-3 p-1 hover:bg-gray-100 rounded"
|
className="mr-3 p-1 hover:bg-gray-100 rounded"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-5 w-5" />
|
<ArrowLeft className="h-5 w-5" />
|
||||||
|
|||||||
@@ -110,7 +110,12 @@ export default function WechatAccountDetail() {
|
|||||||
// 如果没有账号数据,返回上一页
|
// 如果没有账号数据,返回上一页
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentAccount) {
|
if (!currentAccount) {
|
||||||
navigate('/');
|
toast({
|
||||||
|
title: "数据错误",
|
||||||
|
description: "未找到账号信息,请重新选择",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
navigate('/wechat-accounts');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}, [currentAccount, navigate, toast]);
|
}, [currentAccount, navigate, toast]);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { smartGoBack } from '@/utils/navigation';
|
|
||||||
import { ChevronLeft, Search, RefreshCw, ArrowRightLeft, AlertCircle, Loader2 } from 'lucide-react';
|
import { ChevronLeft, Search, RefreshCw, ArrowRightLeft, AlertCircle, Loader2 } from 'lucide-react';
|
||||||
import { fetchWechatAccountList, transformWechatAccount } from '@/api/wechat-accounts';
|
import { fetchWechatAccountList, transformWechatAccount } from '@/api/wechat-accounts';
|
||||||
import { useToast } from '@/components/ui/toast';
|
import { useToast } from '@/components/ui/toast';
|
||||||
@@ -156,7 +155,7 @@ export default function WechatAccounts() {
|
|||||||
<div className="flex items-center px-4 py-3">
|
<div className="flex items-center px-4 py-3">
|
||||||
<button
|
<button
|
||||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
onClick={() => smartGoBack(navigate)}
|
onClick={() => navigate(-1)}
|
||||||
>
|
>
|
||||||
<ChevronLeft className="h-5 w-5" />
|
<ChevronLeft className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
import { NavigateFunction } from 'react-router-dom';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 智能返回上一页
|
|
||||||
* 如果没有上一页历史记录,则返回到主页
|
|
||||||
* @param navigate React Router的navigate函数
|
|
||||||
* @param fallbackPath 默认返回路径,默认为'/'
|
|
||||||
*/
|
|
||||||
export const smartGoBack = (navigate: NavigateFunction, fallbackPath: string = '/') => {
|
|
||||||
// 检查是否有历史记录
|
|
||||||
if (window.history.length > 1) {
|
|
||||||
// 尝试返回上一页
|
|
||||||
navigate(-1);
|
|
||||||
} else {
|
|
||||||
// 如果没有历史记录,返回到主页
|
|
||||||
navigate(fallbackPath);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 带确认的智能返回
|
|
||||||
* @param navigate React Router的navigate函数
|
|
||||||
* @param message 确认消息
|
|
||||||
* @param fallbackPath 默认返回路径
|
|
||||||
*/
|
|
||||||
export const smartGoBackWithConfirm = (
|
|
||||||
navigate: NavigateFunction,
|
|
||||||
message: string = '确定要返回吗?未保存的内容将丢失。',
|
|
||||||
fallbackPath: string = '/'
|
|
||||||
) => {
|
|
||||||
if (window.confirm(message)) {
|
|
||||||
smartGoBack(navigate, fallbackPath);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查是否可以返回上一页
|
|
||||||
* @returns boolean
|
|
||||||
*/
|
|
||||||
export const canGoBack = (): boolean => {
|
|
||||||
return window.history.length > 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取返回按钮文本
|
|
||||||
* @param hasHistory 是否有历史记录
|
|
||||||
* @returns 按钮文本
|
|
||||||
*/
|
|
||||||
export const getBackButtonText = (hasHistory?: boolean): string => {
|
|
||||||
const canBack = hasHistory !== undefined ? hasHistory : canGoBack();
|
|
||||||
return canBack ? '返回上一页' : '返回主页';
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user