diff --git a/Cunkebao/app/components/device-grid.tsx b/Cunkebao/app/components/device-grid.tsx index 028119a7..097b2dce 100644 --- a/Cunkebao/app/components/device-grid.tsx +++ b/Cunkebao/app/components/device-grid.tsx @@ -1,12 +1,15 @@ "use client" -import { useState } from "react" +import { useState, useEffect } from "react" import { Card } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" import { Checkbox } from "@/components/ui/checkbox" import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" -import { Battery, Smartphone, MessageCircle, Users, Clock } from "lucide-react" +import { Battery, Smartphone, MessageCircle, Users, Clock, Search, Power, RefreshCcw, Settings, AlertTriangle } from "lucide-react" import { ImeiDisplay } from "@/components/ImeiDisplay" +import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" export interface Device { id: string @@ -38,32 +41,55 @@ export function DeviceGrid({ itemsPerRow = 2, }: DeviceGridProps) { const [selectedDevice, setSelectedDevice] = useState(null) + const [searchTerm, setSearchTerm] = useState("") + const [filteredDevices, setFilteredDevices] = useState(devices) + + useEffect(() => { + const filtered = devices.filter( + (device) => + device.name.toLowerCase().includes(searchTerm.toLowerCase()) || + device.imei.includes(searchTerm) || + device.wechatId.toLowerCase().includes(searchTerm.toLowerCase()) + ) + setFilteredDevices(filtered) + }, [searchTerm, devices]) const handleSelectAll = () => { - if (selectedDevices.length === devices.length) { + if (selectedDevices.length === filteredDevices.length) { onSelect?.([]) } else { - onSelect?.(devices.map((d) => d.id)) + onSelect?.(filteredDevices.map((d) => d.id)) } } return (
- {selectable && ( -
-
- 0} - onCheckedChange={handleSelectAll} - /> - 全选 -
- 已选择 {selectedDevices.length} 个设备 +
+
+ + setSearchTerm(e.target.value)} + />
- )} + {selectable && ( +
+
+ 0} + onCheckedChange={handleSelectAll} + /> + 全选 +
+ 已选择 {selectedDevices.length} 个设备 +
+ )} +
-
- {devices.map((device) => ( +
+ {filteredDevices.map((device) => (
-
{device.name}
-
- - {device.status === "online" ? "在线" : "离线"} +
+ {device.name} + {device.addFriendStatus === "abnormal" && ( + + 加友异常 + + )} +
+
+ +
+
+ {device.status === "online" ? "在线" : "离线"} +
-
-
- - {device.battery}% +
+
+ + {device.battery}%
-
- - {device.friendCount} +
+ + {device.friendCount}
-
- - {device.messageCount} +
+ + {device.messageCount}
-
- - +{device.todayAdded} +
+ + +{device.todayAdded}
-
-
IMEI: {device.imei}
-
微信号: {device.wechatId}
+
+
+ IMEI: + +
+
+ 微信号: + {device.wechatId} +
- - - {device.addFriendStatus === "normal" ? "加友正常" : "加友异常"} -
@@ -134,77 +193,144 @@ export function DeviceGrid({
setSelectedDevice(null)}> - + 设备详情 {selectedDevice && ( -
+
-
- +
+
-

{selectedDevice.name}

-

- IMEI: - -

+

+ {selectedDevice.name} + +
+
+ {selectedDevice.status === "online" ? "在线" : "离线"} +
+ +

+
+ IMEI: + 微信号: {selectedDevice.wechatId} +
- - {selectedDevice.status === "online" ? "在线" : "离线"} - -
- -
-
-
电池电量
-
- - {selectedDevice.battery}% -
-
-
-
好友数量
-
- - {selectedDevice.friendCount} -
-
-
-
今日新增
-
- - +{selectedDevice.todayAdded} -
-
-
-
消息数量
-
- - {selectedDevice.messageCount} -
+
+ + +
-
-
微信账号
-
{selectedDevice.wechatId}
-
+ + + 状态信息 + 统计数据 + 任务管理 + 运行日志 + + +
+
+
电池电量
+
+ + {selectedDevice.battery}% +
+
+
+
好友数量
+
+ + {selectedDevice.friendCount} +
+
+
+
今日新增
+
+ + +{selectedDevice.todayAdded} +
+
+
+
消息数量
+
+ + {selectedDevice.messageCount} +
+
+
-
-
最后活跃
-
{selectedDevice.lastActive}
-
- -
-
加友状态
- - {selectedDevice.addFriendStatus === "normal" ? "加友正常" : "加友异常"} - -
+ {selectedDevice.addFriendStatus === "abnormal" && ( +
+
+ + 加友异常警告 +
+

+ 该设备当前存在加友异常情况,请检查设备状态和相关配置。 +

+
+ )} +
+ +
+ 统计数据开发中... +
+
+ +
+ 任务管理开发中... +
+
+ +
+ 运行日志开发中... +
+
+
)} diff --git a/Cunkebao/app/devices/[id]/page.tsx b/Cunkebao/app/devices/[id]/page.tsx index 51d89b4d..fe0c18ed 100644 --- a/Cunkebao/app/devices/[id]/page.tsx +++ b/Cunkebao/app/devices/[id]/page.tsx @@ -13,6 +13,7 @@ import { ScrollArea } from "@/components/ui/scroll-area" import { fetchDeviceDetail, fetchDeviceRelatedAccounts, updateDeviceTaskConfig, fetchDeviceHandleLogs } from "@/api/devices" import { toast } from "sonner" import { ImeiDisplay } from "@/components/ImeiDisplay" +import { api } from "@/lib/api" interface WechatAccount { id: string @@ -315,6 +316,37 @@ export default function DeviceDetailPage() { } }, [logPage, activeTab]) + // 获取任务配置 + const fetchTaskConfig = async () => { + try { + const response = await api.get(`/v1/devices/${deviceId}/task-config`) + + if (response && response.code === 200 && response.data) { + setDevice(prev => { + if (!prev) return null + return { + ...prev, + features: { + autoAddFriend: Boolean(response.data.autoAddFriend), + autoReply: Boolean(response.data.autoReply), + momentsSync: Boolean(response.data.momentsSync), + aiChat: Boolean(response.data.aiChat) + } + } + }) + } + } catch (error) { + console.error("获取任务配置失败:", error) + } + } + + // 在组件加载时获取任务配置 + useEffect(() => { + if (deviceId) { + fetchTaskConfig() + } + }, [deviceId]) + // 处理标签页切换 const handleTabChange = (value: string) => { setActiveTab(value) @@ -331,6 +363,11 @@ export default function DeviceDetailPage() { if (value === "history") { fetchHandleLogs() } + + // 当切换到"基本信息"标签时,获取最新的任务配置 + if (value === "info") { + fetchTaskConfig() + } // 设置短暂的延迟来关闭加载状态,模拟加载过程 setTimeout(() => { diff --git a/Cunkebao/styles/globals.css b/Cunkebao/styles/globals.css index ac684423..1623bf9c 100644 --- a/Cunkebao/styles/globals.css +++ b/Cunkebao/styles/globals.css @@ -15,24 +15,24 @@ body { @layer base { :root { --background: 0 0% 100%; - --foreground: 0 0% 3.9%; + --foreground: 222.2 84% 4.9%; --card: 0 0% 100%; - --card-foreground: 0 0% 3.9%; + --card-foreground: 222.2 84% 4.9%; --popover: 0 0% 100%; - --popover-foreground: 0 0% 3.9%; - --primary: 0 0% 9%; - --primary-foreground: 0 0% 98%; - --secondary: 0 0% 96.1%; - --secondary-foreground: 0 0% 9%; - --muted: 0 0% 96.1%; - --muted-foreground: 0 0% 45.1%; - --accent: 0 0% 96.1%; - --accent-foreground: 0 0% 9%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 89.8%; - --input: 0 0% 89.8%; - --ring: 0 0% 3.9%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 215 20.2% 65.1%; --chart-1: 12 76% 61%; --chart-2: 173 58% 39%; --chart-3: 197 37% 24%; @@ -49,25 +49,25 @@ body { --sidebar-ring: 217.2 91.2% 59.8%; } .dark { - --background: 0 0% 3.9%; - --foreground: 0 0% 98%; - --card: 0 0% 3.9%; - --card-foreground: 0 0% 98%; - --popover: 0 0% 3.9%; - --popover-foreground: 0 0% 98%; - --primary: 0 0% 98%; - --primary-foreground: 0 0% 9%; - --secondary: 0 0% 14.9%; - --secondary-foreground: 0 0% 98%; - --muted: 0 0% 14.9%; - --muted-foreground: 0 0% 63.9%; - --accent: 0 0% 14.9%; - --accent-foreground: 0 0% 98%; + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 14.9%; - --input: 0 0% 14.9%; - --ring: 0 0% 83.1%; + --destructive-foreground: 0 85.7% 97.3%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 217.2 32.6% 17.5%; --chart-1: 220 70% 50%; --chart-2: 160 60% 45%; --chart-3: 30 80% 55%; @@ -92,3 +92,8 @@ body { @apply bg-background text-foreground; } } + +/* 隐藏 Next.js 静态指示器 */ +.nextjs-static-indicator-toast-wrapper { + display: none !important; +} diff --git a/Server/application/cunkebao/config/route.php b/Server/application/cunkebao/config/route.php index b2efda2f..af78ca1b 100644 --- a/Server/application/cunkebao/config/route.php +++ b/Server/application/cunkebao/config/route.php @@ -12,7 +12,7 @@ Route::group('v1/', function () { Route::put('refresh', 'app\cunkebao\controller\device\RefreshDeviceDetailV1Controller@index'); Route::get('add-results', 'app\cunkebao\controller\device\GetAddResultedV1Controller@index'); Route::post('task-config', 'app\cunkebao\controller\device\UpdateDeviceTaskConfigV1Controller@index'); - Route::get(':id/task-config', 'app\cunkebao\controller\device\UpdateDeviceTaskConfigV1Controller@index'); + Route::get(':id/task-config', 'app\cunkebao\controller\device\GetDeviceTaskConfigV1Controller@index'); Route::get(':id/handle-logs', 'app\cunkebao\controller\device\GetDeviceHandleLogsV1Controller@index'); Route::get(':id', 'app\cunkebao\controller\device\GetDeviceDetailV1Controller@index'); Route::delete(':id', 'app\cunkebao\controller\device\DeleteDeviceV1Controller@index'); diff --git a/Server/application/cunkebao/controller/device/GetDeviceDetailV1Controller.php b/Server/application/cunkebao/controller/device/GetDeviceDetailV1Controller.php index b9c92600..e42f5a9e 100644 --- a/Server/application/cunkebao/controller/device/GetDeviceDetailV1Controller.php +++ b/Server/application/cunkebao/controller/device/GetDeviceDetailV1Controller.php @@ -59,29 +59,6 @@ class GetDeviceDetailV1Controller extends BaseController return 0; } - /** - * 解析taskConfig字段获取功能开关 - * - * @param int $deviceId - * @return int[] - * @throws \Exception - */ - protected function getTaskConfig(int $deviceId): array - { - $conf = DeviceTaskconfModel::alias('c')->field([ - 'c.autoAddFriend', 'c.autoReply', 'c.momentsSync', 'c.aiChat' - ]) - ->where( - [ - 'companyId' => $this->getUserInfo('companyId'), - 'deviceId' => $deviceId - ] - ) - ->find(); - - // 未配置时赋予默认关闭的状态 - return !is_null($conf) ? $conf->toArray() : ArrHelper::getValue('autoAddFriend,autoReply,momentsSync,aiChat', [], 0); - } /** * 获取设备最新登录微信的 wechatId @@ -102,55 +79,6 @@ class GetDeviceDetailV1Controller extends BaseController ->value('wechatId'); } - /** - * 统计设备登录微信的好友 - * - * @param int $deviceId - * @return int - * @throws \Exception - */ - protected function getTotalFriend(int $deviceId): int - { - $ownerWechatId = $this->getDeviceLatestWechatLogin($deviceId); - - if ($ownerWechatId) { - return WechatFriendShipModel::where( - [ - 'companyId' => $this->getUserInfo('companyId'), - 'ownerWechatId' => $ownerWechatId - ] - ) - ->count(); - } - - return 0; - } - - /** - * 获取设备绑定微信的消息总数 - * - * @param int $deviceId - * @return int - */ - protected function getThirtyDayMsgCount(int $deviceId): int - { - $ownerWechatId = $this->getDeviceLatestWechatLogin($deviceId); - - if ($ownerWechatId) { - $activity = (string)WechatCustomerModel::where( - [ - 'wechatId' => $ownerWechatId, - 'companyId' => $this->getUserInfo('companyId') - ] - ) - ->value('activity'); - - return json_decode($activity)->totalMsgCount ?? 0; - } - - return 0; - } - /** * 获取设备详情 * @param int $id 设备ID @@ -170,11 +98,6 @@ class GetDeviceDetailV1Controller extends BaseController } $device['battery'] = $this->parseExtraForBattery($device['extra']); - $device['features'] = $this->getTaskConfig($id); - $device['totalFriend'] = $this->getTotalFriend($id); - $device['thirtyDayMsgCount'] = $this->getThirtyDayMsgCount($id); - - // 设备最后活跃时间为设备状态更新时间 $device['lastUpdateTime'] = date('Y-m-d H:i:s', $device['lastUpdateTime']); // 删除冗余字段 diff --git a/Server/application/cunkebao/controller/device/GetDeviceTaskConfigV1Controller.php b/Server/application/cunkebao/controller/device/GetDeviceTaskConfigV1Controller.php new file mode 100644 index 00000000..5cacb6a7 --- /dev/null +++ b/Server/application/cunkebao/controller/device/GetDeviceTaskConfigV1Controller.php @@ -0,0 +1,85 @@ + $deviceId, + 'userId' => $this->getUserInfo('id'), + 'companyId' => $this->getUserInfo('companyId') + ] + ) + ->count() > 0; + + if (!$hasPermission) { + throw new \Exception('您没有权限查看该设备', 403); + } + } + + /** + * 解析taskConfig字段获取功能开关 + * + * @param int $deviceId + * @return int[] + * @throws \Exception + */ + protected function getTaskConfig(int $deviceId): array + { + $conf = DeviceTaskconfModel::alias('c') + ->field([ + 'c.autoAddFriend', 'c.autoReply', 'c.momentsSync', 'c.aiChat' + ]) + ->where( + [ + 'companyId' => $this->getUserInfo('companyId'), + 'deviceId' => $deviceId + ] + ) + ->find(); + + // 未配置时赋予默认关闭的状态 + return !is_null($conf) ? $conf->toArray() : ArrHelper::getValue('autoAddFriend,autoReply,momentsSync,aiChat', [], 0); + } + + /** + * 获取设备详情 + * + * @return \think\response\Json + */ + public function index() + { + try { + $id = $this->request->param('id/d'); + + if ($this->getUserInfo('isAdmin') != UserModel::ADMIN_STP) { + $this->checkUserDevicePermission($id); + } + + return ResponseHelper::success( + $this->getTaskConfig($id) + ); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode()); + } + } +} \ No newline at end of file