私域操盘手端 - 修复设备相关页面中api重复调用的问题
This commit is contained in:
@@ -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])
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user