diff --git a/nkebao/src/api/interceptors.ts b/nkebao/src/api/interceptors.ts index c07c4570..3b8da1b3 100644 --- a/nkebao/src/api/interceptors.ts +++ b/nkebao/src/api/interceptors.ts @@ -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(); }; }; \ No newline at end of file diff --git a/nkebao/src/api/request.ts b/nkebao/src/api/request.ts index f152a431..31f4ddd0 100644 --- a/nkebao/src/api/request.ts +++ b/nkebao/src/api/request.ts @@ -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 = (url: string, config?: AxiosRequestConfig): Promise => { - return request.get(url, config); + return requestDeduplicator.execute( + 'GET', + url, + () => request.get(url, config), + config + ); }; -// 封装POST请求 +// 带节流和去重的POST请求 export const post = (url: string, data?: any, config?: AxiosRequestConfig): Promise => { - return request.post(url, data, config); + return requestDeduplicator.execute( + 'POST', + url, + () => request.post(url, data, config), + data + ); }; -// 封装PUT请求 +// 带节流和去重的PUT请求 export const put = (url: string, data?: any, config?: AxiosRequestConfig): Promise => { - return request.put(url, data, config); + return requestDeduplicator.execute( + 'PUT', + url, + () => request.put(url, data, config), + data + ); }; -// 封装DELETE请求 +// 带节流和去重的DELETE请求 export const del = (url: string, config?: AxiosRequestConfig): Promise => { - 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; \ No newline at end of file diff --git a/nkebao/src/api/utils.ts b/nkebao/src/api/utils.ts index f53ddfd9..a1c6c7f4 100644 --- a/nkebao/src/api/utils.ts +++ b/nkebao/src/api/utils.ts @@ -1,5 +1,85 @@ import { authApi } from './auth'; +// 节流函数 - 防止重复请求 +export const throttle = 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> = new Map(); + + // 生成请求的唯一键 + private generateKey(method: string, url: string, data?: any): string { + const dataStr = data ? JSON.stringify(data) : ''; + return `${method}:${url}:${dataStr}`; + } + + // 执行请求,如果相同请求正在进行中则返回已存在的Promise + async execute( + method: string, + url: string, + requestFn: () => Promise, + data?: any + ): Promise { + 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') { diff --git a/nkebao/src/components/ThrottledButton.tsx b/nkebao/src/components/ThrottledButton.tsx new file mode 100644 index 00000000..ed7fef58 --- /dev/null +++ b/nkebao/src/components/ThrottledButton.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { useThrottledRequestWithLoading } from '../hooks/useThrottledRequest'; + +interface ThrottledButtonProps { + onClick: () => Promise; + children: React.ReactNode; + delay?: number; + disabled?: boolean; + className?: string; +} + +export const ThrottledButton: React.FC = ({ + onClick, + children, + delay = 1000, + disabled = false, + className = '' +}) => { + const { throttledRequest, loading } = useThrottledRequestWithLoading(onClick, delay); + + return ( + + ); +}; \ No newline at end of file diff --git a/nkebao/src/hooks/useThrottledRequest.ts b/nkebao/src/hooks/useThrottledRequest.ts new file mode 100644 index 00000000..289d3edc --- /dev/null +++ b/nkebao/src/hooks/useThrottledRequest.ts @@ -0,0 +1,97 @@ +import { useCallback, useRef, useState } from 'react'; +import { requestDeduplicator } from '../api/utils'; + +// 节流请求Hook +export const useThrottledRequest = any>( + requestFn: T, + delay: number = 1000 +) => { + const lastCallRef = useRef(0); + const timeoutRef = useRef(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 = any>( + requestFn: T, + delay: number = 300 +) => { + const timeoutRef = useRef(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 = Promise>( + 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 = Promise>( + requestFn: T, + delay: number = 1000 +) => { + const { requestWithLoading, loading } = useRequestWithLoading(requestFn); + const throttledRequest = useThrottledRequest(requestWithLoading, delay); + + return { throttledRequest, loading }; +}; \ No newline at end of file diff --git a/nkebao/src/pages/devices/DeviceDetail.tsx b/nkebao/src/pages/devices/DeviceDetail.tsx index a950cae1..40ac83d4 100644 --- a/nkebao/src/pages/devices/DeviceDetail.tsx +++ b/nkebao/src/pages/devices/DeviceDetail.tsx @@ -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}" 的设备信息,请检查设备是否存在。 @@ -462,7 +463,7 @@ export default function DeviceDetail() {
diff --git a/nkebao/src/pages/devices/Devices.tsx b/nkebao/src/pages/devices/Devices.tsx index 422ae292..11705f0f 100644 --- a/nkebao/src/pages/devices/Devices.tsx +++ b/nkebao/src/pages/devices/Devices.tsx @@ -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() {
diff --git a/nkebao/src/pages/plans/PlanDetail.tsx b/nkebao/src/pages/plans/PlanDetail.tsx index 902ba18e..27e159e9 100644 --- a/nkebao/src/pages/plans/PlanDetail.tsx +++ b/nkebao/src/pages/plans/PlanDetail.tsx @@ -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() {
diff --git a/nkebao/src/utils/navigation.ts b/nkebao/src/utils/navigation.ts new file mode 100644 index 00000000..774deae1 --- /dev/null +++ b/nkebao/src/utils/navigation.ts @@ -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 ? '返回上一页' : '返回主页'; +}; \ No newline at end of file