Merge branch 'develop' of https://e.coding.net/g-xtcy5189/cunkebao/cunkebao_v3 into develop
This commit is contained in:
@@ -33,6 +33,11 @@ Route::group('', function () {
|
|||||||
Route::get('detail', 'app\superadmin\controller\traffic\GetPoolDetailController@index');
|
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::group('company', function () {
|
||||||
Route::post('add', 'app\superadmin\controller\company\CreateCompanyController@index');
|
Route::post('add', 'app\superadmin\controller\company\CreateCompanyController@index');
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\superadmin\controller\devices;
|
||||||
|
|
||||||
|
use app\api\controller\DeviceController as ApiDeviceController;
|
||||||
|
use app\common\model\Device as DeviceModel;
|
||||||
|
use app\common\model\User as UserModel;
|
||||||
|
use library\ResponseHelper;
|
||||||
|
use think\Controller;
|
||||||
|
use think\Db;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备控制器
|
||||||
|
*/
|
||||||
|
class GetAddResultedDevicesController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 通过账号id 获取项目id。
|
||||||
|
*
|
||||||
|
* @param int $accountId
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
protected function getCompanyIdByAccountId(int $accountId): int
|
||||||
|
{
|
||||||
|
return UserModel::where('s2_accountId', $accountId)->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
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect, useRef } from "react"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
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 Link from "next/link"
|
||||||
import { toast, Toaster } from "sonner"
|
import { toast, Toaster } from "sonner"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
@@ -58,6 +58,13 @@ export default function EditProjectPage({ params }: { params: { id: string } })
|
|||||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
const [qrCodeData, setQrCodeData] = useState("")
|
const [qrCodeData, setQrCodeData] = useState("")
|
||||||
const [isAddingDevice, setIsAddingDevice] = useState(false)
|
const [isAddingDevice, setIsAddingDevice] = useState(false)
|
||||||
|
const [isPolling, setIsPolling] = useState(false)
|
||||||
|
const [pollingStatus, setPollingStatus] = useState<"waiting" | "polling" | "success" | "error">("waiting")
|
||||||
|
const [addedDevice, setAddedDevice] = useState<Device | null>(null)
|
||||||
|
const [isQrCodeBroken, setIsQrCodeBroken] = useState(false)
|
||||||
|
const pollingTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
|
const pollingCountRef = useRef(0)
|
||||||
|
const MAX_POLLING_COUNT = 120; // 2分钟 * 60秒 = 120次
|
||||||
const { id } = React.use(params)
|
const { id } = React.use(params)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -131,6 +138,13 @@ export default function EditProjectPage({ params }: { params: { id: string } })
|
|||||||
}
|
}
|
||||||
|
|
||||||
setIsAddingDevice(true)
|
setIsAddingDevice(true)
|
||||||
|
// 重置轮询状态
|
||||||
|
setPollingStatus("waiting")
|
||||||
|
setIsPolling(false)
|
||||||
|
setAddedDevice(null)
|
||||||
|
setIsQrCodeBroken(false)
|
||||||
|
pollingCountRef.current = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/v1/api/device/add`, {
|
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/v1/api/device/add`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -147,6 +161,11 @@ export default function EditProjectPage({ params }: { params: { id: string } })
|
|||||||
if (data.code === 200 && data.data?.qrCode) {
|
if (data.code === 200 && data.data?.qrCode) {
|
||||||
setQrCodeData(data.data.qrCode)
|
setQrCodeData(data.data.qrCode)
|
||||||
setIsModalOpen(true)
|
setIsModalOpen(true)
|
||||||
|
|
||||||
|
// 五秒后开始轮询
|
||||||
|
setTimeout(() => {
|
||||||
|
startPolling();
|
||||||
|
}, 5000);
|
||||||
} else {
|
} else {
|
||||||
toast.error(data.msg || "获取设备二维码失败")
|
toast.error(data.msg || "获取设备二维码失败")
|
||||||
}
|
}
|
||||||
@@ -156,11 +175,115 @@ export default function EditProjectPage({ params }: { params: { id: string } })
|
|||||||
setIsAddingDevice(false)
|
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 = () => {
|
const closeModal = () => {
|
||||||
|
stopPolling();
|
||||||
setIsModalOpen(false)
|
setIsModalOpen(false)
|
||||||
setQrCodeData("")
|
setQrCodeData("")
|
||||||
|
setPollingStatus("waiting");
|
||||||
|
setAddedDevice(null);
|
||||||
|
setIsQrCodeBroken(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 组件卸载时清除定时器
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (pollingTimerRef.current) {
|
||||||
|
clearInterval(pollingTimerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <div className="flex items-center justify-center min-h-screen">加载中...</div>
|
return <div className="flex items-center justify-center min-h-screen">加载中...</div>
|
||||||
@@ -351,20 +474,63 @@ export default function EditProjectPage({ params }: { params: { id: string } })
|
|||||||
请使用新设备进行扫码添加
|
请使用新设备进行扫码添加
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="flex justify-center p-6">
|
<div className="flex flex-col items-center justify-center p-6">
|
||||||
<div className="border p-4 rounded-lg">
|
<div className="border p-4 rounded-lg mb-4">
|
||||||
{qrCodeData ? (
|
{qrCodeData ? (
|
||||||
<img
|
<div className="relative">
|
||||||
src={qrCodeData}
|
<img
|
||||||
alt="设备二维码"
|
src={qrCodeData}
|
||||||
className="w-64 h-64 object-contain"
|
alt="设备二维码"
|
||||||
/>
|
className={`w-64 h-64 object-contain ${isQrCodeBroken ? 'opacity-30' : ''}`}
|
||||||
|
/>
|
||||||
|
{isQrCodeBroken && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div className="bg-red-100 p-3 rounded-md border border-red-300">
|
||||||
|
<div className="flex flex-col items-center gap-2 text-red-700">
|
||||||
|
<X className="h-8 w-8" />
|
||||||
|
<p className="font-medium text-center">二维码已失效</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-64 h-64 flex items-center justify-center bg-muted">
|
<div className="w-64 h-64 flex items-center justify-center bg-muted">
|
||||||
<p className="text-muted-foreground">二维码加载中...</p>
|
<p className="text-muted-foreground">二维码加载中...</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 轮询状态显示 */}
|
||||||
|
<div className="w-full mt-2">
|
||||||
|
{pollingStatus === "waiting" && (
|
||||||
|
<p className="text-sm text-center text-muted-foreground">请扫描二维码添加设备,5秒后将开始检测添加结果...</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{pollingStatus === "polling" && (
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-primary"></div>
|
||||||
|
<p className="text-sm text-primary">正在检测添加结果...</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{pollingStatus === "success" && addedDevice && (
|
||||||
|
<div className="bg-green-50 p-3 rounded-md border border-green-200 mt-2">
|
||||||
|
<div className="flex items-center gap-2 text-green-700 mb-1">
|
||||||
|
<CheckCircle2 className="h-4 w-4" />
|
||||||
|
<p className="font-medium">设备添加成功。关闭后可继续</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-green-700">
|
||||||
|
<p>设备名称: {addedDevice.memo}</p>
|
||||||
|
<p>IMEI: {addedDevice.imei || '-'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{pollingStatus === "error" && (
|
||||||
|
<p className="text-sm text-center text-red-500">未检测到设备添加,请关闭后重试</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter className="sm:justify-center">
|
<DialogFooter className="sm:justify-center">
|
||||||
<Button type="button" onClick={closeModal}>
|
<Button type="button" onClick={closeModal}>
|
||||||
|
|||||||
@@ -224,6 +224,7 @@ export default function ProjectsPage() {
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
<TableHead>ID</TableHead>
|
||||||
<TableHead>项目名称</TableHead>
|
<TableHead>项目名称</TableHead>
|
||||||
<TableHead>状态</TableHead>
|
<TableHead>状态</TableHead>
|
||||||
<TableHead>用户数量</TableHead>
|
<TableHead>用户数量</TableHead>
|
||||||
@@ -235,13 +236,14 @@ export default function ProjectsPage() {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={6} className="h-24 text-center">
|
<TableCell colSpan={7} className="h-24 text-center">
|
||||||
加载中...
|
加载中...
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : projects.length > 0 ? (
|
) : projects.length > 0 ? (
|
||||||
projects.map((project) => (
|
projects.map((project) => (
|
||||||
<TableRow key={project.id}>
|
<TableRow key={project.id}>
|
||||||
|
<TableCell className="text-left">{project.id}</TableCell>
|
||||||
<TableCell className="font-medium">{project.name}</TableCell>
|
<TableCell className="font-medium">{project.name}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant={project.status === 1 ? "default" : "secondary"}>
|
<Badge variant={project.status === 1 ? "default" : "secondary"}>
|
||||||
@@ -279,7 +281,7 @@ export default function ProjectsPage() {
|
|||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={6} className="h-24 text-center">
|
<TableCell colSpan={7} className="h-24 text-center">
|
||||||
未找到项目
|
未找到项目
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
Reference in New Issue
Block a user