"use client" import { useState, useEffect, useRef, useCallback } from "react" import { useRouter } from "next/navigation" import { Card } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { ChevronLeft, Plus, Filter, Search, RefreshCw, QrCode, Smartphone, Loader2, AlertTriangle } from "lucide-react" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Checkbox } from "@/components/ui/checkbox" import { toast } from "@/components/ui/use-toast" import { Badge } from "@/components/ui/badge" import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { fetchDeviceList, deleteDevice } from "@/api/devices" import { ServerDevice } from "@/types/device" import { api } from "@/lib/api" import { ImeiDisplay } from "@/components/ImeiDisplay" // 设备接口更新为与服务端接口对应的类型 interface Device extends ServerDevice { status: "online" | "offline"; } export default function DevicesPage() { const router = useRouter() const [devices, setDevices] = useState([]) const [isAddDeviceOpen, setIsAddDeviceOpen] = useState(false) const [stats, setStats] = useState({ totalDevices: 0, onlineDevices: 0, }) const [searchQuery, setSearchQuery] = useState("") const [statusFilter, setStatusFilter] = useState("all") const [currentPage, setCurrentPage] = useState(1) const [selectedDevices, setSelectedDevices] = useState([]) const [isLoading, setIsLoading] = useState(false) const [hasMore, setHasMore] = useState(true) const [totalCount, setTotalCount] = useState(0) const observerTarget = useRef(null) // 使用ref来追踪当前页码,避免依赖effect循环 const pageRef = useRef(1) // 添加设备相关状态 const [deviceImei, setDeviceImei] = useState("") const [deviceName, setDeviceName] = useState("") const [qrCodeImage, setQrCodeImage] = useState("") const [isLoadingQRCode, setIsLoadingQRCode] = useState(false) const [isSubmittingImei, setIsSubmittingImei] = useState(false) const [activeTab, setActiveTab] = useState("scan") const devicesPerPage = 20 // 每页显示20条记录 // 获取设备列表 const loadDevices = useCallback(async (page: number, refresh: boolean = false) => { // 检查是否已经在加载中,避免重复请求 if (isLoading) return; try { setIsLoading(true) const response = await fetchDeviceList(page, devicesPerPage, searchQuery) if (response.code === 200 && response.data) { // 转换数据格式,确保status类型正确 const serverDevices = response.data.list.map(device => ({ ...device, status: device.alive === 1 ? "online" as const : "offline" as const })) // 更新设备列表 if (refresh) { setDevices(serverDevices) } else { setDevices(prev => [...prev, ...serverDevices]) } // 更新统计信息 const total = response.data.total const online = response.data.list.filter(d => d.alive === 1).length setStats({ totalDevices: total, onlineDevices: online }) // 更新分页信息 setTotalCount(response.data.total) // 更新hasMore状态,确保有更多数据且返回的数据数量等于每页数量 const hasMoreData = serverDevices.length > 0 && serverDevices.length === devicesPerPage && (page * devicesPerPage) < response.data.total; setHasMore(hasMoreData) // 更新当前页码的ref值 pageRef.current = page } else { toast({ title: "获取设备列表失败", description: response.msg || "请稍后重试", variant: "destructive", }) } } catch (error) { console.error("获取设备列表失败", error) toast({ title: "获取设备列表失败", description: "请检查网络连接后重试", variant: "destructive", }) } finally { setIsLoading(false) } // 移除isLoading依赖,只保留真正需要的依赖 }, [searchQuery, devicesPerPage]) // 加载下一页数据的函数,使用ref来追踪页码,避免依赖循环 const loadNextPage = useCallback(() => { // 如果正在加载或者没有更多数据,直接返回 if (isLoading || !hasMore) return; // 使用ref来获取下一页码,避免依赖currentPage const nextPage = pageRef.current + 1; // 设置UI显示的当前页 setCurrentPage(nextPage); // 加载下一页数据 loadDevices(nextPage, false); // 只依赖必要的状态 }, [hasMore, isLoading, loadDevices]); // 初始加载和搜索时刷新列表 useEffect(() => { // 重置页码 setCurrentPage(1) pageRef.current = 1 // 加载第一页数据 loadDevices(1, true) }, [searchQuery, loadDevices]) // 无限滚动加载实现 useEffect(() => { // 如果没有更多数据或者正在加载,不创建observer if (!hasMore || isLoading) return; let isMounted = true; // 追踪组件是否已挂载 // 创建观察器观察加载点 const observer = new IntersectionObserver( entries => { // 如果交叉了,且有更多数据,且当前不在加载状态,且组件仍然挂载 if (entries[0].isIntersecting && hasMore && !isLoading && isMounted) { loadNextPage(); } }, { threshold: 0.5 } ) // 只在客户端时观察节点 if (typeof window !== 'undefined' && observerTarget.current) { observer.observe(observerTarget.current) } // 清理观察器 return () => { isMounted = false; observer.disconnect(); } }, [hasMore, isLoading, loadNextPage]) // 获取设备二维码 const fetchDeviceQRCode = async () => { try { setIsLoadingQRCode(true) setQrCodeImage("") // 清空当前二维码 console.log("正在请求二维码..."); // 发起请求获取二维码 - 直接使用fetch避免api工具添加基础URL const response = await fetch('http://yi.54word.com/v1/api/device/add', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({}) }) console.log("二维码请求响应状态:", response.status); // 保存原始响应文本以便调试 const responseText = await response.text(); console.log("原始响应内容:", responseText); // 尝试将响应解析为JSON let result; try { result = JSON.parse(responseText); } catch (e) { console.error("响应不是有效的JSON:", e); toast({ title: "获取二维码失败", description: "服务器返回的数据格式无效", variant: "destructive", }); return; } console.log("二维码响应数据:", result); if (result && result.code === 200) { // 尝试多种可能的返回数据结构 let qrcodeData = null; if (result.data?.qrCode) { qrcodeData = result.data.qrCode; console.log("找到二维码数据在 result.data.qrCode"); } else if (result.data?.qrcode) { qrcodeData = result.data.qrcode; console.log("找到二维码数据在 result.data.qrcode"); } else if (result.data?.image) { qrcodeData = result.data.image; console.log("找到二维码数据在 result.data.image"); } else if (result.data?.url) { // 如果返回的是URL而不是base64 qrcodeData = result.data.url; console.log("找到二维码URL在 result.data.url"); setQrCodeImage(qrcodeData); toast({ title: "二维码已更新", description: "请使用手机扫描新的二维码添加设备", }); return; // 直接返回,不进行base64处理 } else if (typeof result.data === 'string') { // 如果data直接是字符串 qrcodeData = result.data; console.log("二维码数据直接在 result.data 字符串中"); } else { console.error("无法找到二维码数据:", result); toast({ title: "获取二维码失败", description: "返回数据格式不正确", variant: "destructive", }); return; } // 检查数据是否为空 if (!qrcodeData) { console.error("二维码数据为空"); toast({ title: "获取二维码失败", description: "服务器返回的二维码数据为空", variant: "destructive", }); return; } console.log("处理前的二维码数据:", qrcodeData); // 检查是否已经是完整的data URL if (qrcodeData.startsWith('data:image')) { console.log("数据已包含data:image前缀"); setQrCodeImage(qrcodeData); } // 检查是否是URL else if (qrcodeData.startsWith('http')) { console.log("数据是HTTP URL"); setQrCodeImage(qrcodeData); } // 尝试作为base64处理 else { try { // 确保base64字符串没有空格等干扰字符 const cleanedBase64 = qrcodeData.trim(); console.log("处理后的base64数据:", cleanedBase64.substring(0, 30) + "..."); // 直接以图片src格式设置 setQrCodeImage(`data:image/png;base64,${cleanedBase64}`); // 预加载图片,确认是否有效 const img = new Image(); img.onload = () => { console.log("二维码图片加载成功"); }; img.onerror = (e) => { console.error("二维码图片加载失败:", e); toast({ title: "二维码加载失败", description: "服务器返回的数据无法显示为图片", variant: "destructive", }); }; img.src = `data:image/png;base64,${cleanedBase64}`; } catch (e) { console.error("处理base64数据出错:", e); toast({ title: "获取二维码失败", description: "图片数据处理失败", variant: "destructive", }); return; } } toast({ title: "二维码已更新", description: "请使用手机扫描新的二维码添加设备", }); } else { console.error("获取二维码失败:", result); toast({ title: "获取二维码失败", description: result?.msg || "请稍后重试", variant: "destructive", }); } } catch (error) { console.error("获取二维码失败", error); toast({ title: "获取二维码失败", description: "请检查网络连接后重试", variant: "destructive", }); } finally { setIsLoadingQRCode(false); } } // 打开添加设备模态框时获取二维码 const handleOpenAddDeviceModal = () => { setIsAddDeviceOpen(true) setDeviceImei("") setDeviceName("") fetchDeviceQRCode() } // 通过IMEI添加设备 const handleAddDeviceByImei = async () => { if (!deviceImei) { toast({ title: "IMEI不能为空", description: "请输入有效的设备IMEI", variant: "destructive", }); return; } try { setIsSubmittingImei(true); console.log("正在添加设备,IMEI:", deviceImei, "设备名称:", deviceName); // 使用api.post发送请求到/v1/devices const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/v1/devices`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('token')}` }, body: JSON.stringify({ imei: deviceImei, memo: deviceName }) }); console.log("添加设备响应状态:", response.status); // 保存原始响应文本以便调试 const responseText = await response.text(); console.log("原始响应内容:", responseText); // 尝试将响应解析为JSON let result; try { result = JSON.parse(responseText); } catch (e) { console.error("响应不是有效的JSON:", e); toast({ title: "添加设备失败", description: "服务器返回的数据格式无效", variant: "destructive", }); return; } console.log("添加设备响应:", result); if (result && result.code === 200) { toast({ title: "设备添加成功", description: result.data?.msg || "设备已成功添加", }); // 清空输入并关闭弹窗 setDeviceImei(""); setDeviceName(""); setIsAddDeviceOpen(false); // 刷新设备列表 loadDevices(1, true); } else { console.error("添加设备失败:", result); toast({ title: "添加设备失败", description: result?.msg || "请检查设备信息是否正确", variant: "destructive", }); } } catch (error) { console.error("添加设备请求失败:", error); toast({ title: "请求失败", description: "网络错误,请稍后重试", variant: "destructive", }); } finally { setIsSubmittingImei(false); } } // 刷新设备列表 const handleRefresh = () => { setCurrentPage(1) pageRef.current = 1 loadDevices(1, true) toast({ title: "刷新成功", description: "设备列表已更新", }) } // 筛选设备 const filteredDevices = devices.filter(device => { const matchesStatus = statusFilter === "all" || (statusFilter === "online" && device.alive === 1) || (statusFilter === "offline" && device.alive === 0) return matchesStatus }) // 处理批量删除 const handleBatchDelete = async () => { if (selectedDevices.length === 0) { toast({ title: "请选择设备", description: "您需要选择至少一个设备来执行批量删除操作", variant: "destructive", }) return } // 这里需要实现批量删除逻辑 // 目前只是单个删除的循环 let successCount = 0 for (const deviceId of selectedDevices) { try { const response = await deleteDevice(deviceId) if (response.code === 200) { successCount++ } } catch (error) { console.error(`删除设备 ${deviceId} 失败`, error) } } // 删除后刷新列表 if (successCount > 0) { toast({ title: "批量删除成功", description: `已删除 ${successCount} 个设备`, }) setSelectedDevices([]) handleRefresh() } else { toast({ title: "批量删除失败", description: "请稍后重试", variant: "destructive", }) } } // 设备详情页跳转 const handleDeviceClick = (deviceId: number, event: React.MouseEvent) => { // 判断点击事件是否来自ImeiDisplay组件或其后代元素 // 如果点击事件已经被处理(例如ImeiDisplay中已阻止传播),则不执行跳转 if (event.defaultPrevented) { return; } // 如果点击的元素或其父元素有imei-display类,则不跳转 let target = event.target as HTMLElement; while (target && target !== event.currentTarget) { if (target.classList.contains('imei-display-area')) { return; } target = target.parentElement as HTMLElement; } router.push(`/devices/${deviceId}`); } return (

设备管理

总设备数
{stats.totalDevices}
在线设备
{stats.onlineDevices}
setSearchQuery(e.target.value)} className="pl-9" />
0} onCheckedChange={(checked) => { if (checked) { setSelectedDevices(filteredDevices.map((d) => d.id)) } else { setSelectedDevices([]) } }} /> 全选
{filteredDevices.map((device) => ( handleDeviceClick(device.id, e)} >
{ if (checked) { setSelectedDevices([...selectedDevices, device.id]) } else { setSelectedDevices(selectedDevices.filter((id) => id !== device.id)) } }} onClick={(e) => e.stopPropagation()} />
{device.memo}
{device.status === "online" ? "在线" : "离线"}
IMEI:
微信号: {device.wechatId || "未绑定"}
好友数: {device.totalFriend}
))} {/* 加载更多观察点 */}
{isLoading &&
加载中...
} {!hasMore && devices.length > 0 &&
没有更多设备了
} {!hasMore && devices.length === 0 &&
暂无设备
}
{/* 添加设备对话框 */} 添加设备 扫码添加 手动添加
{isLoadingQRCode ? (

正在获取二维码...

) : qrCodeImage ? (
设备添加二维码 { console.error("二维码图片加载失败"); // 隐藏图片 e.currentTarget.style.display = 'none'; // 显示错误信息 const container = document.getElementById('qrcode-container'); if (container) { const errorEl = container.querySelector('.qrcode-error'); if (errorEl) { errorEl.classList.remove('hidden'); } } }} />

未能加载二维码,请点击刷新按钮重试

请使用手机扫描此二维码添加设备

) : (

点击下方按钮获取二维码

)}
setDeviceName(e.target.value)} />

为设备添加一个便于识别的名称

setDeviceImei(e.target.value)} />

请输入设备IMEI码,可在设备信息中查看

) }