私域操盘手端 - 修复设备相关页面中api重复调用的问题

This commit is contained in:
柳清爽
2025-05-08 14:23:24 +08:00
parent 0426485177
commit 1ec9863a8a
4 changed files with 143 additions and 21 deletions

View File

@@ -124,7 +124,7 @@ export default function DevicesPage() {
setIsLoading(false) setIsLoading(false)
} }
// 移除isLoading依赖只保留真正需要的依赖 // 移除isLoading依赖只保留真正需要的依赖
}, [searchQuery, devicesPerPage]) }, [searchQuery]) // devicesPerPage是常量,不需要加入依赖
// 加载下一页数据的函数使用ref来追踪页码避免依赖循环 // 加载下一页数据的函数使用ref来追踪页码避免依赖循环
const loadNextPage = useCallback(() => { const loadNextPage = useCallback(() => {
@@ -140,8 +140,21 @@ export default function DevicesPage() {
// 只依赖必要的状态 // 只依赖必要的状态
}, [hasMore, isLoading, loadDevices]); }, [hasMore, isLoading, loadDevices]);
// 追踪组件是否已挂载
const isMounted = useRef(true);
// 组件卸载时更新挂载状态
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
// 初始加载和搜索时刷新列表 // 初始加载和搜索时刷新列表
useEffect(() => { useEffect(() => {
// 组件未挂载,不执行操作
if (!isMounted.current) return;
// 重置页码 // 重置页码
setCurrentPage(1) setCurrentPage(1)
pageRef.current = 1 pageRef.current = 1
@@ -154,13 +167,11 @@ export default function DevicesPage() {
// 如果没有更多数据或者正在加载不创建observer // 如果没有更多数据或者正在加载不创建observer
if (!hasMore || isLoading) return; if (!hasMore || isLoading) return;
let isMounted = true; // 追踪组件是否已挂载
// 创建观察器观察加载点 // 创建观察器观察加载点
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
entries => { entries => {
// 如果交叉了,且有更多数据,且当前不在加载状态,且组件仍然挂载 // 如果交叉了,且有更多数据,且当前不在加载状态,且组件仍然挂载
if (entries[0].isIntersecting && hasMore && !isLoading && isMounted) { if (entries[0].isIntersecting && hasMore && !isLoading && isMounted.current) {
loadNextPage(); loadNextPage();
} }
}, },
@@ -174,7 +185,6 @@ export default function DevicesPage() {
// 清理观察器 // 清理观察器
return () => { return () => {
isMounted = false;
observer.disconnect(); observer.disconnect();
} }
}, [hasMore, isLoading, loadNextPage]) }, [hasMore, isLoading, loadNextPage])

View File

@@ -1,6 +1,6 @@
"use client" "use client"
import { useState, useEffect } from "react" import { useState, useEffect, useRef } from "react"
import { ChevronLeft, Copy, Link, HelpCircle } from "lucide-react" import { ChevronLeft, Copy, Link, HelpCircle } from "lucide-react"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
@@ -95,18 +95,35 @@ export default function ChannelPage({ params }: { params: { channel: string } })
// 从URL query参数获取场景ID // 从URL query参数获取场景ID
const [sceneId, setSceneId] = useState<number | null>(null); const [sceneId, setSceneId] = useState<number | null>(null);
// 使用ref追踪sceneId值避免重复请求
const sceneIdRef = useRef<number | null>(null);
// 追踪组件是否已挂载
const isMounted = useRef(true);
// 组件卸载时更新挂载状态
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
// 获取URL中的查询参数 // 获取URL中的查询参数
useEffect(() => { useEffect(() => {
// 组件未挂载,不执行操作
if (!isMounted.current) return;
// 从URL获取id参数 // 从URL获取id参数
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const idParam = urlParams.get('id'); const idParam = urlParams.get('id');
if (idParam && !isNaN(Number(idParam))) { if (idParam && !isNaN(Number(idParam))) {
setSceneId(Number(idParam)); setSceneId(Number(idParam));
sceneIdRef.current = Number(idParam);
} else { } else {
// 如果没有传递有效的ID使用函数获取默认ID // 如果没有传递有效的ID使用函数获取默认ID
setSceneId(getSceneIdFromChannel(channel)); const defaultId = getSceneIdFromChannel(channel);
setSceneId(defaultId);
sceneIdRef.current = defaultId;
} }
}, [channel]); }, [channel]);
@@ -114,7 +131,7 @@ export default function ChannelPage({ params }: { params: { channel: string } })
{ {
id: "1", id: "1",
name: `${channelName}直播获客计划`, name: `${channelName}直播获客计划`,
status: "running", status: "running" as const,
stats: { stats: {
devices: 5, devices: 5,
acquired: 31, acquired: 31,
@@ -131,7 +148,7 @@ export default function ChannelPage({ params }: { params: { channel: string } })
{ {
id: "2", id: "2",
name: `${channelName}评论区获客计划`, name: `${channelName}评论区获客计划`,
status: "paused", status: "paused" as const,
stats: { stats: {
devices: 3, devices: 3,
acquired: 15, acquired: 15,
@@ -145,7 +162,7 @@ export default function ChannelPage({ params }: { params: { channel: string } })
customers: Math.floor(Math.random() * 20) + 20, customers: Math.floor(Math.random() * 20) + 20,
})), })),
}, },
] ] as Task[];
const [tasks, setTasks] = useState<Task[]>([]) const [tasks, setTasks] = useState<Task[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
@@ -233,6 +250,14 @@ export default function ChannelPage({ params }: { params: { channel: string } })
// 修改API数据处理部分 // 修改API数据处理部分
useEffect(() => { useEffect(() => {
// 组件未挂载,不执行操作
if (!isMounted.current) return;
// 防止重复请求如果sceneId没有变化且已经加载过数据则不重新请求
if (sceneId === sceneIdRef.current && tasks.length > 0 && !loading) {
return;
}
const fetchPlanList = async () => { const fetchPlanList = async () => {
try { try {
setLoading(true); setLoading(true);
@@ -298,7 +323,7 @@ export default function ChannelPage({ params }: { params: { channel: string } })
}; };
fetchPlanList(); fetchPlanList();
}, [channel, initialTasks, sceneId]); }, [sceneId]); // 只依赖sceneId变化触发请求
// 辅助函数根据渠道获取场景ID // 辅助函数根据渠道获取场景ID
const getSceneIdFromChannel = (channel: string): number => { const getSceneIdFromChannel = (channel: string): number => {

View File

@@ -1,6 +1,6 @@
"use client" "use client"
import { useState, useEffect } from "react" import { useState, useEffect, useRef } from "react"
import type React from "react" import type React from "react"
import { TrendingUp, Users, ChevronLeft, Bot, Sparkles, Plus, Phone } from "lucide-react" import { TrendingUp, Users, ChevronLeft, Bot, Sparkles, Plus, Phone } from "lucide-react"
import { Card } from "@/components/ui/card" import { Card } from "@/components/ui/card"
@@ -97,8 +97,25 @@ export default function ScenariosPage() {
const [channels, setChannels] = useState<Channel[]>([]) const [channels, setChannels] = useState<Channel[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
// 使用ref跟踪组件挂载状态
const isMounted = useRef(true);
// 使用ref跟踪是否已经加载过数据
const hasLoadedRef = useRef(false);
// 组件卸载时更新挂载状态
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
useEffect(() => { useEffect(() => {
// 组件未挂载,不执行操作
if (!isMounted.current) return;
// 如果已经加载过数据,不再重复请求
if (hasLoadedRef.current && channels.length > 0) return;
const loadScenes = async () => { const loadScenes = async () => {
try { try {
setLoading(true) setLoading(true)
@@ -116,15 +133,26 @@ export default function ScenariosPage() {
} }
}) })
setChannels(transformedScenes) // 只有在组件仍然挂载的情况下才更新状态
if (isMounted.current) {
setChannels(transformedScenes)
// 标记已加载过数据
hasLoadedRef.current = true;
}
} else { } else {
setError(response.msg || "获取场景列表失败") if (isMounted.current) {
setError(response.msg || "获取场景列表失败")
}
} }
} catch (err) { } catch (err) {
console.error("Failed to fetch scenes:", err) console.error("Failed to fetch scenes:", err)
setError("获取场景列表失败") if (isMounted.current) {
setError("获取场景列表失败")
}
} finally { } finally {
setLoading(false) if (isMounted.current) {
setLoading(false)
}
} }
} }

View File

@@ -1,6 +1,6 @@
"use client" "use client"
import { useState, useEffect } from "react" import { useState, useEffect, useRef } from "react"
import type { Device } from "@/components/device-grid" import type { Device } from "@/components/device-grid"
interface DeviceStatus { interface DeviceStatus {
@@ -25,21 +25,80 @@ async function fetchDeviceStatuses(deviceIds: string[]): Promise<Record<string,
export function useDeviceStatusPolling(devices: Device[]) { export function useDeviceStatusPolling(devices: Device[]) {
const [statuses, setStatuses] = useState<Record<string, DeviceStatus>>({}) const [statuses, setStatuses] = useState<Record<string, DeviceStatus>>({})
// 使用ref跟踪上一次的设备ID列表
const prevDeviceIdsRef = useRef<string[]>([]);
// 使用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(() => { 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 () => { const pollStatus = async () => {
try { try {
const newStatuses = await fetchDeviceStatuses(devices.map((d) => d.id)) 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) { } catch (error) {
console.error("Failed to fetch device statuses:", 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]) }, [devices])
return statuses return statuses