From 034a306077612d6ee91d4dff70dabaf4b41f6651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=AE=B8=E6=B0=B8=E5=B9=B3?= Date: Fri, 4 Jul 2025 17:15:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E8=AE=BE=E5=A4=87=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/src/api/devices.ts | 34 +- nkebao/src/pages/devices/Devices.tsx | 702 ++++++++++++++++++++++++++- 2 files changed, 731 insertions(+), 5 deletions(-) diff --git a/nkebao/src/api/devices.ts b/nkebao/src/api/devices.ts index ed7e8458..f90dc1a6 100644 --- a/nkebao/src/api/devices.ts +++ b/nkebao/src/api/devices.ts @@ -60,7 +60,30 @@ export const deleteDevice = async (id: number): Promise> => { }; // 设备管理API -export const deviceApi = { +export const devicesApi = { + // 获取设备列表 + async getList(page: number = 1, limit: number = 20, keyword?: string): Promise { + const params = new URLSearchParams(); + params.append('page', page.toString()); + params.append('limit', limit.toString()); + + if (keyword) { + params.append('keyword', keyword); + } + + return get(`/v1/devices?${params.toString()}`); + }, + + // 获取设备二维码 + async getQRCode(accountId: string): Promise> { + return post>('/v1/api/device/add', { accountId }); + }, + + // 通过IMEI添加设备 + async addByImei(imei: string, name: string): Promise> { + return post>('/v1/api/device/add-by-imei', { imei, name }); + }, + // 创建设备 async create(params: CreateDeviceParams): Promise> { return post>(`${API_BASE}`, params); @@ -97,11 +120,16 @@ export const deviceApi = { return get>>(`${API_BASE}?${queryString}`); }, - // 删除设备 - async delete(id: string): Promise> { + // 删除设备(旧版本) + async deleteById(id: string): Promise> { return del>(`${API_BASE}/${id}`); }, + // 删除设备(新版本) + async delete(id: number): Promise> { + return del>(`/v1/devices/${id}`); + }, + // 重启设备 async restart(id: string): Promise> { return post>(`${API_BASE}/${id}/restart`); diff --git a/nkebao/src/pages/devices/Devices.tsx b/nkebao/src/pages/devices/Devices.tsx index 5445ccd2..4c9fde58 100644 --- a/nkebao/src/pages/devices/Devices.tsx +++ b/nkebao/src/pages/devices/Devices.tsx @@ -1,5 +1,703 @@ -import React from 'react'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { ChevronLeft, Plus, Filter, Search, RefreshCw, QrCode, Smartphone, Loader2, AlertTriangle, Trash2 } from 'lucide-react'; +import { devicesApi } from '../../api'; +import { useToast } from '../../components/ui/toast'; + +// 设备接口 +interface Device { + id: number; + imei: string; + memo: string; + wechatId: string; + totalFriend: number; + alive: number; + status: "online" | "offline"; +} export default function Devices() { - return
设备列表页
; + const navigate = useNavigate(); + const { toast } = useToast(); + 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 [selectedDeviceId, setSelectedDeviceId] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [hasMore, setHasMore] = useState(true); + const [totalCount, setTotalCount] = useState(0); + const observerTarget = useRef(null); + 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 [pollingStatus, setPollingStatus] = useState<{ + isPolling: boolean; + message: string; + messageType: 'default' | 'success' | 'error'; + showAnimation: boolean; + }>({ + isPolling: false, + message: '', + messageType: 'default', + showAnimation: false + }); + + const pollingTimerRef = useRef(null); + const devicesPerPage = 20; + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [deviceToDelete, setDeviceToDelete] = useState(null); + + const loadDevices = useCallback(async (page: number, refresh: boolean = false) => { + if (isLoading) return; + + try { + setIsLoading(true); + const response = await devicesApi.getList(page, devicesPerPage, searchQuery); + + if (response.code === 200 && response.data) { + const serverDevices = response.data.list.map((device: any) => ({ + ...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: any) => d.alive === 1).length; + setStats({ + totalDevices: total, + onlineDevices: online + }); + + setTotalCount(response.data.total); + + const hasMoreData = serverDevices.length > 0 && + serverDevices.length === devicesPerPage && + (page * devicesPerPage) < response.data.total; + setHasMore(hasMoreData); + + pageRef.current = page; + } else { + toast({ + title: "获取设备列表失败", + description: response.message || "请稍后重试", + variant: "destructive", + }); + } + } catch (error) { + console.error("获取设备列表失败", error); + toast({ + title: "获取设备列表失败", + description: "请检查网络连接后重试", + variant: "destructive", + }); + } finally { + setIsLoading(false); + } + }, [searchQuery, isLoading, toast]); + + const loadNextPage = useCallback(() => { + if (isLoading || !hasMore) return; + + const nextPage = pageRef.current + 1; + setCurrentPage(nextPage); + loadDevices(nextPage, false); + }, [hasMore, isLoading, loadDevices]); + + const isMounted = useRef(true); + + useEffect(() => { + return () => { + isMounted.current = false; + }; + }, []); + + useEffect(() => { + if (!isMounted.current) return; + + setCurrentPage(1); + pageRef.current = 1; + loadDevices(1, true); + }, [searchQuery, loadDevices]); + + useEffect(() => { + if (!hasMore || isLoading) return; + + const observer = new IntersectionObserver( + entries => { + if (entries[0].isIntersecting && hasMore && !isLoading && isMounted.current) { + loadNextPage(); + } + }, + { threshold: 0.5 } + ); + + if (typeof window !== 'undefined' && observerTarget.current) { + observer.observe(observerTarget.current); + } + + return () => { + observer.disconnect(); + }; + }, [hasMore, isLoading, loadNextPage]); + + const fetchDeviceQRCode = async () => { + try { + setIsLoadingQRCode(true); + setQrCodeImage(""); + + const accountId = localStorage.getItem('s2_accountId'); + if (!accountId) { + toast({ + title: "获取二维码失败", + description: "未获取到用户信息,请重新登录", + variant: "destructive", + }); + return; + } + + const response = await devicesApi.getQRCode(accountId); + + if (response.code === 200 && response.data) { + setQrCodeImage(response.data.qrCode); + // 开始轮询检测设备添加结果 + setTimeout(() => { + startPolling(); + }, 5000); + } else { + toast({ + title: "获取二维码失败", + description: response.message || "请稍后重试", + variant: "destructive", + }); + } + } catch (error) { + console.error("获取二维码失败:", error); + toast({ + title: "获取二维码失败", + description: "请稍后重试", + variant: "destructive", + }); + } finally { + setIsLoadingQRCode(false); + } + }; + + const startPolling = () => { + setPollingStatus({ + isPolling: true, + message: "正在检测添加结果...", + messageType: 'default', + showAnimation: true + }); + + const poll = async () => { + try { + const response = await devicesApi.getList(1, 1); + if (response.code === 200 && response.data) { + const currentCount = response.data.total; + if (currentCount > totalCount) { + setPollingStatus({ + isPolling: false, + message: "设备添加成功!", + messageType: 'success', + showAnimation: false + }); + setIsAddDeviceOpen(false); + loadDevices(1, true); + if (pollingTimerRef.current) { + clearTimeout(pollingTimerRef.current); + } + return; + } + } + } catch (error) { + console.error("轮询检测失败:", error); + } + + // 继续轮询 + pollingTimerRef.current = setTimeout(poll, 2000); + }; + + poll(); + }; + + const handleOpenAddDeviceModal = () => { + setIsAddDeviceOpen(true); + setActiveTab("scan"); + setQrCodeImage(""); + setDeviceImei(""); + setDeviceName(""); + setPollingStatus({ + isPolling: false, + message: '', + messageType: 'default', + showAnimation: false + }); + }; + + const handleCloseAddDeviceModal = () => { + setIsAddDeviceOpen(false); + if (pollingTimerRef.current) { + clearTimeout(pollingTimerRef.current); + } + }; + + const handleAddDeviceByImei = async () => { + if (!deviceImei.trim() || !deviceName.trim()) { + toast({ + title: "请填写完整信息", + description: "设备名称和IMEI不能为空", + variant: "destructive", + }); + return; + } + + try { + setIsSubmittingImei(true); + const response = await devicesApi.addByImei(deviceImei, deviceName); + + if (response.code === 200) { + toast({ + title: "添加成功", + description: "设备已成功添加", + }); + setIsAddDeviceOpen(false); + loadDevices(1, true); + } else { + toast({ + title: "添加失败", + description: response.message || "请稍后重试", + variant: "destructive", + }); + } + } catch (error) { + console.error('添加设备失败:', error); + toast({ + title: '添加设备失败,请稍后重试', + variant: "destructive", + }); + } finally { + setIsSubmittingImei(false); + } + }; + + const handleRefresh = () => { + setCurrentPage(1); + pageRef.current = 1; + loadDevices(1, true); + }; + + const handleDeleteClick = () => { + if (!selectedDeviceId) { + toast({ + title: "请选择要删除的设备", + variant: "destructive", + }); + return; + } + setDeviceToDelete(selectedDeviceId); + setIsDeleteDialogOpen(true); + }; + + const handleConfirmDelete = async () => { + if (!deviceToDelete) return; + + try { + const response = await devicesApi.delete(deviceToDelete); + + if (response.code === 200) { + toast({ + title: "删除成功", + description: "设备已成功删除", + }); + setSelectedDeviceId(null); + loadDevices(1, true); + } else { + toast({ + title: "删除失败", + description: response.message || "请稍后重试", + variant: "destructive", + }); + } + } catch (error) { + console.error('删除设备失败:', error); + toast({ + title: '删除设备失败,请稍后重试', + variant: "destructive", + }); + } finally { + setIsDeleteDialogOpen(false); + setDeviceToDelete(null); + } + }; + + const handleCancelDelete = () => { + setIsDeleteDialogOpen(false); + setDeviceToDelete(null); + }; + + const handleDeviceClick = (deviceId: number, event: React.MouseEvent) => { + event.stopPropagation(); + navigate(`/devices/${deviceId}`); + }; + + const handleAddDevice = async () => { + if (activeTab === "manual") { + await handleAddDeviceByImei(); + } + }; + + // 过滤设备列表 + const filteredDevices = devices.filter(device => { + if (statusFilter === "online") return device.status === "online"; + if (statusFilter === "offline") return device.status === "offline"; + return true; + }); + + return ( +
+ {/* 固定header */} +
+
+
+ +

设备管理

+
+ +
+
+ + {/* 可滚动的内容区域 */} +
+
+ {/* 统计卡片 */} +
+
+
总设备数
+
{stats.totalDevices}
+
+
+
在线设备
+
{stats.onlineDevices}
+
+
+ + {/* 搜索和过滤 */} +
+
+
+ + setSearchQuery(e.target.value)} + className="w-full pl-9 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +
+ + +
+ +
+
+ +
+ +
+
+ + {/* 设备列表 */} +
+ {filteredDevices.map((device) => ( +
handleDeviceClick(device.id, e)} + > +
+ { + if (e.target.checked) { + setSelectedDeviceId(device.id); + } else { + setSelectedDeviceId(null); + } + }} + onClick={(e) => e.stopPropagation()} + className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" + /> +
+
+
{device.memo || "未命名设备"}
+ + {device.status === "online" ? "在线" : "离线"} + +
+
+ IMEI: {device.imei} +
+
微信号: {device.wechatId || "未绑定或微信离线"}
+
+ 好友数: {device.totalFriend} +
+
+
+
+ ))} + +
+ {isLoading &&
加载中...
} + {!hasMore && devices.length > 0 &&
没有更多设备了
} + {!hasMore && devices.length === 0 &&
暂无设备
} +
+
+
+
+ + {/* 添加设备弹窗 */} + {isAddDeviceOpen && ( +
+
+
+
+

添加设备

+ +
+ +
+
+ + +
+ + {activeTab === "scan" && ( +
+
+
+
+ {pollingStatus.isPolling || pollingStatus.showAnimation ? ( + <> + 正在检测添加结果 +
+
+
+
+
+ + ) : ( + 5秒后将开始检测添加结果 + )} +
+
+
+ {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'); + } + } + }} + /> +
+ +

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

+
+
+

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

+
+ ) : ( +
+ +

点击下方按钮获取二维码

+
+ )} +
+ +
+
+ )} + + {activeTab === "manual" && ( +
+
+
+ + setDeviceName(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +

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

+
+
+ + setDeviceImei(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +

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

+
+
+ + +
+
+
+ )} +
+
+
+
+ )} + + {/* 删除确认弹窗 */} + {isDeleteDialogOpen && ( +
+
+

确认删除

+

+ 设备删除后,本设备配置的计划任务操作也将失效。 +

+
+ + +
+
+
+ )} +
+ ); } \ No newline at end of file