From 1ec9863a8a58ab0a468d2c2c6992bfb67c1fe37a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9F=B3=E6=B8=85=E7=88=BD?= Date: Thu, 8 May 2025 14:23:24 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=81=E5=9F=9F=E6=93=8D=E7=9B=98=E6=89=8B?= =?UTF-8?q?=E7=AB=AF=20-=20=E4=BF=AE=E5=A4=8D=E8=AE=BE=E5=A4=87=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E9=A1=B5=E9=9D=A2=E4=B8=ADapi=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cunkebao/app/devices/page.tsx | 20 +++++-- Cunkebao/app/scenarios/[channel]/page.tsx | 37 ++++++++++-- Cunkebao/app/scenarios/page.tsx | 38 +++++++++++-- Cunkebao/hooks/useDeviceStatusPolling.ts | 69 +++++++++++++++++++++-- 4 files changed, 143 insertions(+), 21 deletions(-) diff --git a/Cunkebao/app/devices/page.tsx b/Cunkebao/app/devices/page.tsx index 6a8eaa96..3d6fe02e 100644 --- a/Cunkebao/app/devices/page.tsx +++ b/Cunkebao/app/devices/page.tsx @@ -124,7 +124,7 @@ export default function DevicesPage() { setIsLoading(false) } // 移除isLoading依赖,只保留真正需要的依赖 - }, [searchQuery, devicesPerPage]) + }, [searchQuery]) // devicesPerPage是常量,不需要加入依赖 // 加载下一页数据的函数,使用ref来追踪页码,避免依赖循环 const loadNextPage = useCallback(() => { @@ -140,8 +140,21 @@ export default function DevicesPage() { // 只依赖必要的状态 }, [hasMore, isLoading, loadDevices]); + // 追踪组件是否已挂载 + const isMounted = useRef(true); + + // 组件卸载时更新挂载状态 + useEffect(() => { + return () => { + isMounted.current = false; + }; + }, []); + // 初始加载和搜索时刷新列表 useEffect(() => { + // 组件未挂载,不执行操作 + if (!isMounted.current) return; + // 重置页码 setCurrentPage(1) pageRef.current = 1 @@ -154,13 +167,11 @@ export default function DevicesPage() { // 如果没有更多数据或者正在加载,不创建observer if (!hasMore || isLoading) return; - let isMounted = true; // 追踪组件是否已挂载 - // 创建观察器观察加载点 const observer = new IntersectionObserver( entries => { // 如果交叉了,且有更多数据,且当前不在加载状态,且组件仍然挂载 - if (entries[0].isIntersecting && hasMore && !isLoading && isMounted) { + if (entries[0].isIntersecting && hasMore && !isLoading && isMounted.current) { loadNextPage(); } }, @@ -174,7 +185,6 @@ export default function DevicesPage() { // 清理观察器 return () => { - isMounted = false; observer.disconnect(); } }, [hasMore, isLoading, loadNextPage]) diff --git a/Cunkebao/app/scenarios/[channel]/page.tsx b/Cunkebao/app/scenarios/[channel]/page.tsx index 18004ce8..5126be5b 100644 --- a/Cunkebao/app/scenarios/[channel]/page.tsx +++ b/Cunkebao/app/scenarios/[channel]/page.tsx @@ -1,6 +1,6 @@ "use client" -import { useState, useEffect } from "react" +import { useState, useEffect, useRef } from "react" import { ChevronLeft, Copy, Link, HelpCircle } from "lucide-react" import { Button } from "@/components/ui/button" import { useRouter } from "next/navigation" @@ -95,18 +95,35 @@ export default function ChannelPage({ params }: { params: { channel: string } }) // 从URL query参数获取场景ID const [sceneId, setSceneId] = useState(null); + // 使用ref追踪sceneId值,避免重复请求 + const sceneIdRef = useRef(null); + // 追踪组件是否已挂载 + const isMounted = useRef(true); + + // 组件卸载时更新挂载状态 + useEffect(() => { + return () => { + isMounted.current = false; + }; + }, []); // 获取URL中的查询参数 useEffect(() => { + // 组件未挂载,不执行操作 + if (!isMounted.current) return; + // 从URL获取id参数 const urlParams = new URLSearchParams(window.location.search); const idParam = urlParams.get('id'); if (idParam && !isNaN(Number(idParam))) { setSceneId(Number(idParam)); + sceneIdRef.current = Number(idParam); } else { // 如果没有传递有效的ID,使用函数获取默认ID - setSceneId(getSceneIdFromChannel(channel)); + const defaultId = getSceneIdFromChannel(channel); + setSceneId(defaultId); + sceneIdRef.current = defaultId; } }, [channel]); @@ -114,7 +131,7 @@ export default function ChannelPage({ params }: { params: { channel: string } }) { id: "1", name: `${channelName}直播获客计划`, - status: "running", + status: "running" as const, stats: { devices: 5, acquired: 31, @@ -131,7 +148,7 @@ export default function ChannelPage({ params }: { params: { channel: string } }) { id: "2", name: `${channelName}评论区获客计划`, - status: "paused", + status: "paused" as const, stats: { devices: 3, acquired: 15, @@ -145,7 +162,7 @@ export default function ChannelPage({ params }: { params: { channel: string } }) customers: Math.floor(Math.random() * 20) + 20, })), }, - ] + ] as Task[]; const [tasks, setTasks] = useState([]) const [loading, setLoading] = useState(true) @@ -233,6 +250,14 @@ export default function ChannelPage({ params }: { params: { channel: string } }) // 修改API数据处理部分 useEffect(() => { + // 组件未挂载,不执行操作 + if (!isMounted.current) return; + + // 防止重复请求:如果sceneId没有变化且已经加载过数据,则不重新请求 + if (sceneId === sceneIdRef.current && tasks.length > 0 && !loading) { + return; + } + const fetchPlanList = async () => { try { setLoading(true); @@ -298,7 +323,7 @@ export default function ChannelPage({ params }: { params: { channel: string } }) }; fetchPlanList(); - }, [channel, initialTasks, sceneId]); + }, [sceneId]); // 只依赖sceneId变化触发请求 // 辅助函数:根据渠道获取场景ID const getSceneIdFromChannel = (channel: string): number => { diff --git a/Cunkebao/app/scenarios/page.tsx b/Cunkebao/app/scenarios/page.tsx index a598b978..11ecaea0 100644 --- a/Cunkebao/app/scenarios/page.tsx +++ b/Cunkebao/app/scenarios/page.tsx @@ -1,6 +1,6 @@ "use client" -import { useState, useEffect } from "react" +import { useState, useEffect, useRef } from "react" import type React from "react" import { TrendingUp, Users, ChevronLeft, Bot, Sparkles, Plus, Phone } from "lucide-react" import { Card } from "@/components/ui/card" @@ -97,8 +97,25 @@ export default function ScenariosPage() { const [channels, setChannels] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) + // 使用ref跟踪组件挂载状态 + const isMounted = useRef(true); + // 使用ref跟踪是否已经加载过数据 + const hasLoadedRef = useRef(false); + + // 组件卸载时更新挂载状态 + useEffect(() => { + return () => { + isMounted.current = false; + }; + }, []); useEffect(() => { + // 组件未挂载,不执行操作 + if (!isMounted.current) return; + + // 如果已经加载过数据,不再重复请求 + if (hasLoadedRef.current && channels.length > 0) return; + const loadScenes = async () => { try { setLoading(true) @@ -116,15 +133,26 @@ export default function ScenariosPage() { } }) - setChannels(transformedScenes) + // 只有在组件仍然挂载的情况下才更新状态 + if (isMounted.current) { + setChannels(transformedScenes) + // 标记已加载过数据 + hasLoadedRef.current = true; + } } else { - setError(response.msg || "获取场景列表失败") + if (isMounted.current) { + setError(response.msg || "获取场景列表失败") + } } } catch (err) { console.error("Failed to fetch scenes:", err) - setError("获取场景列表失败") + if (isMounted.current) { + setError("获取场景列表失败") + } } finally { - setLoading(false) + if (isMounted.current) { + setLoading(false) + } } } diff --git a/Cunkebao/hooks/useDeviceStatusPolling.ts b/Cunkebao/hooks/useDeviceStatusPolling.ts index 8863e592..dfb82ab9 100644 --- a/Cunkebao/hooks/useDeviceStatusPolling.ts +++ b/Cunkebao/hooks/useDeviceStatusPolling.ts @@ -1,6 +1,6 @@ "use client" -import { useState, useEffect } from "react" +import { useState, useEffect, useRef } from "react" import type { Device } from "@/components/device-grid" interface DeviceStatus { @@ -25,21 +25,80 @@ async function fetchDeviceStatuses(deviceIds: string[]): Promise>({}) + // 使用ref跟踪上一次的设备ID列表 + const prevDeviceIdsRef = useRef([]); + // 使用ref跟踪组件挂载状态 + const isMounted = useRef(true); + // 记录轮询错误次数,用于实现退避策略 + const errorCountRef = useRef(0); + + // 检查设备列表是否有实质性变化 + const hasDevicesChanged = (prevIds: string[], currentIds: string[]): boolean => { + if (prevIds.length !== currentIds.length) return true; + + // 使用Set检查两个数组是否包含相同的元素 + const prevSet = new Set(prevIds); + return currentIds.some(id => !prevSet.has(id)); + }; useEffect(() => { + // 重置组件挂载状态 + isMounted.current = true; + + // 获取当前设备ID列表 + const deviceIds = devices.map(d => d.id); + + // 检查设备列表是否有变化 + const deviceListChanged = hasDevicesChanged(prevDeviceIdsRef.current, deviceIds); + + // 更新设备ID引用 + prevDeviceIdsRef.current = deviceIds; + const pollStatus = async () => { try { const newStatuses = await fetchDeviceStatuses(devices.map((d) => d.id)) - setStatuses((prevStatuses) => ({ ...prevStatuses, ...newStatuses })) + // 确保组件仍然挂载 + if (isMounted.current) { + setStatuses((prevStatuses) => ({ ...prevStatuses, ...newStatuses })) + // 重置错误计数 + errorCountRef.current = 0; + } } catch (error) { console.error("Failed to fetch device statuses:", error) + // 增加错误计数 + errorCountRef.current += 1; } } - pollStatus() // 立即执行一次 - const intervalId = setInterval(pollStatus, 30000) // 每30秒更新一次 + // 仅当设备列表有变化或初始加载时才立即执行一次 + if (deviceListChanged || Object.keys(statuses).length === 0) { + pollStatus(); + } + + // 使用基于错误次数的指数退避策略 + const getPollingInterval = () => { + const baseInterval = 30000; // 基础间隔 30 秒 + const maxInterval = 2 * 60 * 1000; // 最大间隔 2 分钟 + + if (errorCountRef.current === 0) return baseInterval; + + // 计算指数退避间隔,但不超过最大间隔 + const backoffInterval = Math.min( + baseInterval * Math.pow(1.5, Math.min(errorCountRef.current, 5)), + maxInterval + ); + + return backoffInterval; + }; + + // 设置轮询间隔 + const intervalId = setInterval(pollStatus, getPollingInterval()); - return () => clearInterval(intervalId) + // 清理函数 + return () => { + isMounted.current = false; + clearInterval(intervalId) + } }, [devices]) return statuses