feat:对返回上一页做了处理
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { refreshAuthToken, isTokenExpiringSoon, clearToken } from './utils';
|
||||
import { refreshAuthToken, isTokenExpiringSoon, clearToken, requestDeduplicator } from './utils';
|
||||
|
||||
// Token过期处理
|
||||
export const handleTokenExpired = () => {
|
||||
@@ -6,6 +6,9 @@ export const handleTokenExpired = () => {
|
||||
// 清除本地存储
|
||||
clearToken();
|
||||
|
||||
// 清除所有待处理的请求
|
||||
requestDeduplicator.clear();
|
||||
|
||||
// 跳转到登录页面
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login';
|
||||
@@ -125,6 +128,8 @@ export const setupNetworkListener = () => {
|
||||
|
||||
const handleOffline = () => {
|
||||
console.log('网络已断开');
|
||||
// 清除所有待处理的请求
|
||||
requestDeduplicator.clear();
|
||||
showApiError(null, '网络连接已断开,请检查网络设置');
|
||||
};
|
||||
|
||||
@@ -148,5 +153,7 @@ export const initInterceptors = () => {
|
||||
if (cleanupNetwork) {
|
||||
cleanupNetwork();
|
||||
}
|
||||
// 清理所有待处理的请求
|
||||
requestDeduplicator.clear();
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import { requestInterceptor, responseInterceptor, errorInterceptor } from './interceptors';
|
||||
import { requestDeduplicator, throttle } from './utils';
|
||||
|
||||
// 创建axios实例
|
||||
const request: AxiosInstance = axios.create({
|
||||
@@ -48,26 +49,52 @@ request.interceptors.response.use(
|
||||
}
|
||||
);
|
||||
|
||||
// 封装GET请求
|
||||
// 带节流和去重的GET请求
|
||||
export const get = <T = any>(url: string, config?: AxiosRequestConfig): Promise<T> => {
|
||||
return request.get(url, config);
|
||||
return requestDeduplicator.execute(
|
||||
'GET',
|
||||
url,
|
||||
() => request.get(url, config),
|
||||
config
|
||||
);
|
||||
};
|
||||
|
||||
// 封装POST请求
|
||||
// 带节流和去重的POST请求
|
||||
export const post = <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> => {
|
||||
return request.post(url, data, config);
|
||||
return requestDeduplicator.execute(
|
||||
'POST',
|
||||
url,
|
||||
() => request.post(url, data, config),
|
||||
data
|
||||
);
|
||||
};
|
||||
|
||||
// 封装PUT请求
|
||||
// 带节流和去重的PUT请求
|
||||
export const put = <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> => {
|
||||
return request.put(url, data, config);
|
||||
return requestDeduplicator.execute(
|
||||
'PUT',
|
||||
url,
|
||||
() => request.put(url, data, config),
|
||||
data
|
||||
);
|
||||
};
|
||||
|
||||
// 封装DELETE请求
|
||||
// 带节流和去重的DELETE请求
|
||||
export const del = <T = any>(url: string, config?: AxiosRequestConfig): Promise<T> => {
|
||||
return request.delete(url, config);
|
||||
return requestDeduplicator.execute(
|
||||
'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实例
|
||||
export { request };
|
||||
export default request;
|
||||
@@ -1,5 +1,85 @@
|
||||
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
|
||||
export const setToken = (token: string) => {
|
||||
if (typeof window !== 'undefined') {
|
||||
|
||||
30
nkebao/src/components/ThrottledButton.tsx
Normal file
30
nkebao/src/components/ThrottledButton.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { useThrottledRequestWithLoading } from '../hooks/useThrottledRequest';
|
||||
|
||||
interface ThrottledButtonProps {
|
||||
onClick: () => Promise<any>;
|
||||
children: React.ReactNode;
|
||||
delay?: number;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ThrottledButton: React.FC<ThrottledButtonProps> = ({
|
||||
onClick,
|
||||
children,
|
||||
delay = 1000,
|
||||
disabled = false,
|
||||
className = ''
|
||||
}) => {
|
||||
const { throttledRequest, loading } = useThrottledRequestWithLoading(onClick, delay);
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={throttledRequest}
|
||||
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}`}
|
||||
>
|
||||
{loading ? '处理中...' : children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
97
nkebao/src/hooks/useThrottledRequest.ts
Normal file
97
nkebao/src/hooks/useThrottledRequest.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { requestDeduplicator } from '../api/utils';
|
||||
|
||||
// 节流请求Hook
|
||||
export const useThrottledRequest = <T extends (...args: any[]) => any>(
|
||||
requestFn: T,
|
||||
delay: number = 1000
|
||||
) => {
|
||||
const lastCallRef = useRef(0);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const throttledRequest = useCallback(
|
||||
((...args: any[]) => {
|
||||
const now = Date.now();
|
||||
|
||||
if (now - lastCallRef.current < delay) {
|
||||
// 如果在节流时间内,取消之前的定时器并设置新的
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
lastCallRef.current = now;
|
||||
requestFn(...args);
|
||||
}, delay - (now - lastCallRef.current));
|
||||
} else {
|
||||
// 如果超过节流时间,直接执行
|
||||
lastCallRef.current = now;
|
||||
requestFn(...args);
|
||||
}
|
||||
}) as T,
|
||||
[requestFn, delay]
|
||||
);
|
||||
|
||||
return throttledRequest;
|
||||
};
|
||||
|
||||
// 防抖请求Hook
|
||||
export const useDebouncedRequest = <T extends (...args: any[]) => any>(
|
||||
requestFn: T,
|
||||
delay: number = 300
|
||||
) => {
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const debouncedRequest = useCallback(
|
||||
((...args: any[]) => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
requestFn(...args);
|
||||
}, delay);
|
||||
}) as T,
|
||||
[requestFn, delay]
|
||||
);
|
||||
|
||||
return debouncedRequest;
|
||||
};
|
||||
|
||||
// 带加载状态的请求Hook
|
||||
export const useRequestWithLoading = <T extends (...args: any[]) => Promise<any>>(
|
||||
requestFn: T
|
||||
) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const requestWithLoading = useCallback(
|
||||
(async (...args: any[]) => {
|
||||
if (loading) {
|
||||
console.log('请求正在进行中,跳过重复请求');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await requestFn(...args);
|
||||
return result;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}) as T,
|
||||
[requestFn, loading]
|
||||
);
|
||||
|
||||
return { requestWithLoading, loading };
|
||||
};
|
||||
|
||||
// 组合Hook:节流 + 加载状态
|
||||
export const useThrottledRequestWithLoading = <T extends (...args: any[]) => Promise<any>>(
|
||||
requestFn: T,
|
||||
delay: number = 1000
|
||||
) => {
|
||||
const { requestWithLoading, loading } = useRequestWithLoading(requestFn);
|
||||
const throttledRequest = useThrottledRequest(requestWithLoading, delay);
|
||||
|
||||
return { throttledRequest, loading };
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
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 { devicesApi, fetchDeviceDetail, fetchDeviceRelatedAccounts, fetchDeviceHandleLogs, updateDeviceTaskConfig } from '@/api/devices';
|
||||
import { useToast } from '@/components/ui/toast';
|
||||
@@ -443,11 +444,11 @@ export default function DeviceDetail() {
|
||||
无法加载ID为 "{id}" 的设备信息,请检查设备是否存在。
|
||||
</div>
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
onClick={() => smartGoBack(navigate)}
|
||||
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" />
|
||||
返回上一页
|
||||
{getBackButtonText()}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -462,7 +463,7 @@ export default function DeviceDetail() {
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
onClick={() => navigate(-1)}
|
||||
onClick={() => smartGoBack(navigate)}
|
||||
>
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
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 { devicesApi } from '@/api';
|
||||
import { useToast } from '@/components/ui/toast';
|
||||
@@ -381,7 +382,7 @@ export default function Devices() {
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
onClick={() => navigate(-1)}
|
||||
onClick={() => smartGoBack(navigate)}
|
||||
>
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { smartGoBack } from '@/utils/navigation';
|
||||
import { ArrowLeft, Users, TrendingUp, Calendar, Settings, Play, Pause, Edit } from 'lucide-react';
|
||||
|
||||
interface PlanData {
|
||||
@@ -92,7 +93,7 @@ export default function PlanDetail() {
|
||||
<div className="flex items-center justify-between p-4">
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
onClick={() => smartGoBack(navigate)}
|
||||
className="mr-3 p-1 hover:bg-gray-100 rounded"
|
||||
>
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { smartGoBack } from '../../utils/navigation';
|
||||
import { ArrowLeft, Check } from 'lucide-react';
|
||||
|
||||
interface ScenarioOption {
|
||||
@@ -87,7 +88,7 @@ export default function NewPlan() {
|
||||
<header className="sticky top-0 z-10 bg-white border-b">
|
||||
<div className="flex items-center p-4">
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
onClick={() => smartGoBack(navigate)}
|
||||
className="mr-3 p-1 hover:bg-gray-100 rounded"
|
||||
>
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { smartGoBack } from '../../utils/navigation';
|
||||
import { ArrowLeft, Plus, Users, TrendingUp, Calendar } from 'lucide-react';
|
||||
|
||||
interface Plan {
|
||||
@@ -170,7 +171,7 @@ export default function ScenarioDetail() {
|
||||
<div className="flex items-center justify-between p-4">
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
onClick={() => smartGoBack(navigate)}
|
||||
className="mr-3 p-1 hover:bg-gray-100 rounded"
|
||||
>
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
|
||||
@@ -110,12 +110,7 @@ export default function WechatAccountDetail() {
|
||||
// 如果没有账号数据,返回上一页
|
||||
useEffect(() => {
|
||||
if (!currentAccount) {
|
||||
toast({
|
||||
title: "数据错误",
|
||||
description: "未找到账号信息,请重新选择",
|
||||
variant: "destructive"
|
||||
});
|
||||
navigate('/wechat-accounts');
|
||||
navigate('/');
|
||||
return;
|
||||
}
|
||||
}, [currentAccount, navigate, toast]);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { smartGoBack } from '@/utils/navigation';
|
||||
import { ChevronLeft, Search, RefreshCw, ArrowRightLeft, AlertCircle, Loader2 } from 'lucide-react';
|
||||
import { fetchWechatAccountList, transformWechatAccount } from '@/api/wechat-accounts';
|
||||
import { useToast } from '@/components/ui/toast';
|
||||
@@ -155,7 +156,7 @@ export default function WechatAccounts() {
|
||||
<div className="flex items-center px-4 py-3">
|
||||
<button
|
||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
onClick={() => navigate(-1)}
|
||||
onClick={() => smartGoBack(navigate)}
|
||||
>
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
52
nkebao/src/utils/navigation.ts
Normal file
52
nkebao/src/utils/navigation.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
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