diff --git a/Server/application/superadmin/config/route.php b/Server/application/superadmin/config/route.php index fa544508..337cdfcb 100644 --- a/Server/application/superadmin/config/route.php +++ b/Server/application/superadmin/config/route.php @@ -33,6 +33,11 @@ Route::group('', function () { Route::get('detail', 'app\superadmin\controller\traffic\GetPoolDetailController@index'); }); + // 设备管理吗 + Route::group('devices', function () { + Route::get('add-results', 'app\superadmin\controller\devices\GetAddResultedDevicesController@index'); + }); + // 公司路由 Route::group('company', function () { Route::post('add', 'app\superadmin\controller\company\CreateCompanyController@index'); diff --git a/Server/application/superadmin/controller/devices/GetAddResultedDevicesController.php b/Server/application/superadmin/controller/devices/GetAddResultedDevicesController.php new file mode 100644 index 00000000..71c85f51 --- /dev/null +++ b/Server/application/superadmin/controller/devices/GetAddResultedDevicesController.php @@ -0,0 +1,119 @@ +value('companyId'); + } + + /** + * 获取项目下的所有设备。 + * + * @param int $companyId + * @return array + */ + protected function getAllDevicesIdWithInCompany(int $companyId): array + { + return DeviceModel::where('companyId', $companyId)->column('id') ?: [0]; + } + + /** + * 执行数据迁移。 + * + * @param int $accountId + * @return void + */ + protected function migrateData(int $accountId): void + { + $companyId = $this->getCompanyIdByAccountId($accountId); + $ids = $this->getAllDevicesIdWithInCompany($companyId); + + // 从 s2_device 导入数据。 + $this->getNewDeviceFromS2_device($ids); + } + + /** + * 从 s2_device 导入数据。 + * + * @param array $ids + * @return array + */ + protected function getNewDeviceFromS2_device(array $ids): array + { + $sql = "insert into ck_device(`id`, `imei`, `model`, phone, operatingSystem,memo,alive,brand,rooted,xPosed,softwareVersion,extra,createTime,updateTime,deleteTime,companyId) + select + d.id,d.imei,d.model,d.phone,d.operatingSystem,d.memo,d.alive,d.brand,d.rooted,d.xPosed,d.softwareVersion,d.extra,d.createTime,d.lastUpdateTime,d.deleteTime,a.departmentId companyId + from s2_device d + join s2_company_account a on d.currentAccountId = a.id + where isDeleted = 0 and deletedAndStop = 0 and d.id not in (:ids) + "; + + dd($sql); + + Db::query($sql, ['ids' => implode(',', $ids)]); + } + + /** + * 获取添加的关联设备结果。 + * + * @param int $accountId + * @return bool + */ + protected function getAddResulted(int $accountId): bool + { + $result = (new ApiDeviceController())->getlist( + [ + 'accountId' => $accountId, + 'pageIndex' => 0, + 'pageSize' => 1 + ], + true + ); + + $result = json_decode($result, true); + $result = $result['data']['results'][0]; + + return ( + // 125是前端延迟5秒 + 轮询120次 1次/s + time() - strtotime($result['lastUpdateTime']) <= 125 + ); + } + + /** + * 获取基础统计信息 + * + * @return \think\response\Json + */ + public function index() + { + $accountId = $this->request->param('accountId/d'); + + $isAdded = $this->getAddResulted($accountId); + $isAdded && $this->migrateData($accountId); + + return ResponseHelper::success( + [ + 'added' => $isAdded + ] + ); + } +} \ No newline at end of file diff --git a/SuperAdmin/app/dashboard/projects/[id]/edit/page.tsx b/SuperAdmin/app/dashboard/projects/[id]/edit/page.tsx index b1413ab7..86b4066e 100644 --- a/SuperAdmin/app/dashboard/projects/[id]/edit/page.tsx +++ b/SuperAdmin/app/dashboard/projects/[id]/edit/page.tsx @@ -1,14 +1,14 @@ "use client" import * as React from "react" -import { useState, useEffect } from "react" +import { useState, useEffect, useRef } from "react" import { useRouter } from "next/navigation" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" import { Label } from "@/components/ui/label" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" -import { ArrowLeft, Plus, Trash, X } from "lucide-react" +import { ArrowLeft, Plus, Trash, X, CheckCircle2 } from "lucide-react" import Link from "next/link" import { toast, Toaster } from "sonner" import Image from "next/image" @@ -58,6 +58,13 @@ export default function EditProjectPage({ params }: { params: { id: string } }) const [isModalOpen, setIsModalOpen] = useState(false) const [qrCodeData, setQrCodeData] = useState("") const [isAddingDevice, setIsAddingDevice] = useState(false) + const [isPolling, setIsPolling] = useState(false) + const [pollingStatus, setPollingStatus] = useState<"waiting" | "polling" | "success" | "error">("waiting") + const [addedDevice, setAddedDevice] = useState(null) + const [isQrCodeBroken, setIsQrCodeBroken] = useState(false) + const pollingTimerRef = useRef(null) + const pollingCountRef = useRef(0) + const MAX_POLLING_COUNT = 120; // 2分钟 * 60秒 = 120次 const { id } = React.use(params) useEffect(() => { @@ -131,6 +138,13 @@ export default function EditProjectPage({ params }: { params: { id: string } }) } setIsAddingDevice(true) + // 重置轮询状态 + setPollingStatus("waiting") + setIsPolling(false) + setAddedDevice(null) + setIsQrCodeBroken(false) + pollingCountRef.current = 0; + try { const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/v1/api/device/add`, { method: "POST", @@ -147,6 +161,11 @@ export default function EditProjectPage({ params }: { params: { id: string } }) if (data.code === 200 && data.data?.qrCode) { setQrCodeData(data.data.qrCode) setIsModalOpen(true) + + // 五秒后开始轮询 + setTimeout(() => { + startPolling(); + }, 5000); } else { toast.error(data.msg || "获取设备二维码失败") } @@ -156,11 +175,115 @@ export default function EditProjectPage({ params }: { params: { id: string } }) setIsAddingDevice(false) } } + + const startPolling = () => { + setIsPolling(true); + setPollingStatus("polling"); + + // 清除可能存在的旧定时器 + if (pollingTimerRef.current) { + clearInterval(pollingTimerRef.current); + } + + // 设置轮询定时器 + pollingTimerRef.current = setInterval(() => { + pollAddResult(); + pollingCountRef.current += 1; + + // 如果达到最大轮询次数,停止轮询 + if (pollingCountRef.current >= MAX_POLLING_COUNT) { + stopPolling(); + } + }, 1000); + } + + const pollAddResult = async () => { + if (!project?.s2_accountId) { + console.error("未找到账号ID,无法轮询"); + return; + } + + try { + const accountId = project.s2_accountId; + // 通过URL参数传递accountId + const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/devices/add-results?accountId=${accountId}`, { + method: "GET", + headers: { + "Content-Type": "application/json" + } + }); + + const data = await response.json(); + + if (data.code === 200) { + // 检查是否最后一次轮询且设备未添加 + if (pollingCountRef.current >= MAX_POLLING_COUNT && !data.added) { + setPollingStatus("error"); + setIsQrCodeBroken(true); + stopPolling(); + return; + } + + // 检查设备是否已添加成功 + if (data.added) { + setPollingStatus("success"); + setAddedDevice(data.device); + stopPolling(); + + // 刷新设备列表 + refreshProjectData(); + toast.success("设备添加成功"); + } + } else { + // 请求失败但继续轮询 + console.error("轮询请求失败:", data.msg); + } + } catch (error) { + console.error("轮询请求出错:", error); + } + } + + // 刷新项目数据的方法 + const refreshProjectData = async () => { + try { + const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/company/detail/${id}`) + const data = await response.json() + + if (data.code === 200) { + setProject(data.data) + } else { + toast.error(data.msg || "刷新项目信息失败") + } + } catch (error) { + toast.error("网络错误,请稍后重试") + } + } + + const stopPolling = () => { + if (pollingTimerRef.current) { + clearInterval(pollingTimerRef.current); + pollingTimerRef.current = null; + } + setIsPolling(false); + } const closeModal = () => { + stopPolling(); setIsModalOpen(false) setQrCodeData("") + setPollingStatus("waiting"); + setAddedDevice(null); + setIsQrCodeBroken(false); } + + // 组件卸载时清除定时器 + useEffect(() => { + return () => { + if (pollingTimerRef.current) { + clearInterval(pollingTimerRef.current); + } + }; + }, []); if (isLoading) { return
加载中...
@@ -351,20 +474,63 @@ export default function EditProjectPage({ params }: { params: { id: string } }) 请使用新设备进行扫码添加 -
-
+
+
{qrCodeData ? ( - 设备二维码 +
+ 设备二维码 + {isQrCodeBroken && ( +
+
+
+ +

二维码已失效

+
+
+
+ )} +
) : (

二维码加载中...

)}
+ + {/* 轮询状态显示 */} +
+ {pollingStatus === "waiting" && ( +

请扫描二维码添加设备,5秒后将开始检测添加结果...

+ )} + + {pollingStatus === "polling" && ( +
+
+

正在检测添加结果...

+
+ )} + + {pollingStatus === "success" && addedDevice && ( +
+
+ +

设备添加成功。关闭后可继续

+
+
+

设备名称: {addedDevice.memo}

+

IMEI: {addedDevice.imei || '-'}

+
+
+ )} + + {pollingStatus === "error" && ( +

未检测到设备添加,请关闭后重试

+ )} +