设备详情

This commit is contained in:
柳清爽
2025-03-31 15:38:10 +08:00
parent c4698bd22a
commit dbf20fd870
4 changed files with 257 additions and 106 deletions

View File

@@ -28,6 +28,11 @@ export const fetchDeviceList = async (page: number = 1, limit: number = 20, keyw
return api.get<ServerDevicesResponse>(`/v1/devices?${params.toString()}`); return api.get<ServerDevicesResponse>(`/v1/devices?${params.toString()}`);
}; };
// 获取设备详情 - 连接到服务器/v1/devices/:id接口
export const fetchDeviceDetail = async (id: string | number): Promise<ApiResponse<any>> => {
return api.get<ApiResponse<any>>(`/v1/devices/${id}`);
};
// 删除设备 // 删除设备
export const deleteDevice = async (id: number): Promise<ApiResponse<any>> => { export const deleteDevice = async (id: number): Promise<ApiResponse<any>> => {
return api.delete<ApiResponse<any>>(`/v1/devices/${id}`); return api.delete<ApiResponse<any>>(`/v1/devices/${id}`);

View File

@@ -10,6 +10,8 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Switch } from "@/components/ui/switch" import { Switch } from "@/components/ui/switch"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { ScrollArea } from "@/components/ui/scroll-area" import { ScrollArea } from "@/components/ui/scroll-area"
import { fetchDeviceDetail } from "@/api/devices"
import { toast } from "sonner"
interface WechatAccount { interface WechatAccount {
id: string id: string
@@ -43,6 +45,21 @@ interface Device {
action: string action: string
operator: string operator: string
}[] }[]
totalFriend: number
thirtyDayMsgCount: number
}
// 这个helper函数用于获取Badge变体类型
function getBadgeVariant(status: string): "default" | "destructive" | "outline" | "secondary" {
if (status === "online" || status === "normal") {
return "default"
} else if (status === "abnormal") {
return "destructive"
} else if (status === "enabled") {
return "outline"
} else {
return "secondary"
}
} }
export default function DeviceDetailPage() { export default function DeviceDetailPage() {
@@ -50,64 +67,133 @@ export default function DeviceDetailPage() {
const router = useRouter() const router = useRouter()
const [device, setDevice] = useState<Device | null>(null) const [device, setDevice] = useState<Device | null>(null)
const [activeTab, setActiveTab] = useState("info") const [activeTab, setActiveTab] = useState("info")
const [loading, setLoading] = useState(true)
useEffect(() => { useEffect(() => {
// 模拟API调用 if (!params.id) return
const mockDevice: Device = {
id: params.id as string, const fetchDevice = async () => {
imei: "sd123123", try {
name: "设备 1", setLoading(true)
status: "online", const response = await fetchDeviceDetail(params.id as string)
battery: 85,
lastActive: "2024-02-09 15:30:45", if (response && response.code === 200 && response.data) {
historicalIds: ["vx412321", "vfbadasd"], const serverData = response.data
wechatAccounts: [
{ // 构建符合前端期望格式的设备对象
id: "1", const formattedDevice: Device = {
avatar: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-q2rVrFbfDdAbSnT3ZTNE7gfn3QCbvr.png", id: serverData.id?.toString() || "",
nickname: "老张", imei: serverData.imei || "",
wechatId: "wxid_abc123", name: serverData.memo || "未命名设备",
gender: "male", status: serverData.alive === 1 ? "online" : "offline",
status: "normal", battery: serverData.battery || 0,
addFriendStatus: "enabled", lastActive: serverData.lastUpdateTime || new Date().toISOString(),
friendCount: 523, historicalIds: [], // 服务端暂无此数据
lastActive: "2024-02-09 15:20:33", wechatAccounts: [], // 默认空数组
}, history: [], // 服务端暂无此数据
{ features: serverData.features || {
id: "2", autoAddFriend: false,
avatar: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-q2rVrFbfDdAbSnT3ZTNE7gfn3QCbvr.png", autoReply: false,
nickname: "老李", contentSync: false,
wechatId: "wxid_xyz789", aiChat: false
gender: "male", },
status: "abnormal", totalFriend: serverData.totalFriend || 0,
addFriendStatus: "disabled", thirtyDayMsgCount: serverData.thirtyDayMsgCount || 0
friendCount: 245, }
lastActive: "2024-02-09 14:15:22",
}, // 如果有微信账号信息,构建微信账号对象
], if (serverData.wechatId) {
features: { formattedDevice.wechatAccounts = [
autoAddFriend: true, {
autoReply: true, id: serverData.wechatId?.toString() || "1",
contentSync: false, avatar: "/placeholder.svg", // 默认头像
aiChat: true, nickname: serverData.memo || "微信账号",
}, wechatId: serverData.imei || "",
history: [ gender: "male", // 默认性别
{ status: serverData.alive === 1 ? "normal" : "abnormal",
time: "2024-02-09 15:30:45", addFriendStatus: "enabled",
action: "开启自动加好友", friendCount: serverData.totalFriend || 0,
operator: "系统", lastActive: serverData.lastUpdateTime || new Date().toISOString()
}, }
{ ]
time: "2024-02-09 14:20:33", }
action: "添加微信号",
operator: "管理员", setDevice(formattedDevice)
}, } else {
], // 如果API返回错误则使用备用模拟数据
toast.error("获取设备信息失败,显示备用数据")
fallbackToMockDevice()
}
} catch (error) {
console.error("获取设备信息失败:", error)
toast.error("获取设备信息出错,显示备用数据")
fallbackToMockDevice()
} finally {
setLoading(false)
}
} }
setDevice(mockDevice)
const fallbackToMockDevice = () => {
const mockDevice: Device = {
id: params.id as string,
imei: "sd123123",
name: "设备 1",
status: "online",
battery: 85,
lastActive: "2024-02-09 15:30:45",
historicalIds: ["vx412321", "vfbadasd"],
wechatAccounts: [
{
id: "1",
avatar: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-q2rVrFbfDdAbSnT3ZTNE7gfn3QCbvr.png",
nickname: "老张",
wechatId: "wxid_abc123",
gender: "male",
status: "normal",
addFriendStatus: "enabled",
friendCount: 523,
lastActive: "2024-02-09 15:20:33",
},
{
id: "2",
avatar: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-q2rVrFbfDdAbSnT3ZTNE7gfn3QCbvr.png",
nickname: "老李",
wechatId: "wxid_xyz789",
gender: "male",
status: "abnormal",
addFriendStatus: "disabled",
friendCount: 245,
lastActive: "2024-02-09 14:15:22",
},
],
features: {
autoAddFriend: true,
autoReply: true,
contentSync: false,
aiChat: true,
},
history: [
{
time: "2024-02-09 15:30:45",
action: "开启自动加好友",
operator: "系统",
},
{
time: "2024-02-09 14:20:33",
action: "添加微信号",
operator: "管理员",
},
],
totalFriend: 768,
thirtyDayMsgCount: 5678
}
setDevice(mockDevice)
}
fetchDevice()
}, [params.id]) }, [params.id])
if (!device) { if (loading || !device) {
return <div>...</div> return <div>...</div>
} }
@@ -137,12 +223,14 @@ export default function DeviceDetailPage() {
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h2 className="font-medium truncate">{device.name}</h2> <h2 className="font-medium truncate">{device.name}</h2>
<Badge variant={device.status === "online" ? "success" : "secondary"}> <Badge variant={getBadgeVariant(device.status)}>
{device.status === "online" ? "在线" : "离线"} {device.status === "online" ? "在线" : "离线"}
</Badge> </Badge>
</div> </div>
<div className="text-sm text-gray-500 mt-1">IMEI: {device.imei}</div> <div className="text-sm text-gray-500 mt-1">IMEI: {device.imei}</div>
<div className="text-sm text-gray-500">ID: {device.historicalIds.join(", ")}</div> {device.historicalIds && device.historicalIds.length > 0 && (
<div className="text-sm text-gray-500">ID: {device.historicalIds.join(", ")}</div>
)}
</div> </div>
</div> </div>
<div className="mt-4 grid grid-cols-2 gap-4"> <div className="mt-4 grid grid-cols-2 gap-4">
@@ -203,33 +291,39 @@ export default function DeviceDetailPage() {
<TabsContent value="accounts"> <TabsContent value="accounts">
<Card className="p-4"> <Card className="p-4">
<ScrollArea className="h-[calc(100vh-300px)]"> <ScrollArea className="h-[calc(100vh-300px)]">
<div className="space-y-4"> {device.wechatAccounts && device.wechatAccounts.length > 0 ? (
{device.wechatAccounts.map((account) => ( <div className="space-y-4">
<div key={account.id} className="flex items-start space-x-3 p-3 bg-gray-50 rounded-lg"> {device.wechatAccounts.map((account) => (
<img <div key={account.id} className="flex items-start space-x-3 p-3 bg-gray-50 rounded-lg">
src={account.avatar || "/placeholder.svg"} <img
alt={account.nickname} src={account.avatar || "/placeholder.svg"}
className="w-12 h-12 rounded-full" alt={account.nickname}
/> className="w-12 h-12 rounded-full"
<div className="flex-1 min-w-0"> />
<div className="flex items-center justify-between"> <div className="flex-1 min-w-0">
<div className="font-medium truncate">{account.nickname}</div> <div className="flex items-center justify-between">
<Badge variant={account.status === "normal" ? "success" : "destructive"}> <div className="font-medium truncate">{account.nickname}</div>
{account.status === "normal" ? "正常" : "异常"} <Badge variant={getBadgeVariant(account.status)}>
</Badge> {account.status === "normal" ? "正常" : "异常"}
</div> </Badge>
<div className="text-sm text-gray-500 mt-1">: {account.wechatId}</div> </div>
<div className="text-sm text-gray-500">: {account.gender === "male" ? "男" : "女"}</div> <div className="text-sm text-gray-500 mt-1">: {account.wechatId}</div>
<div className="flex items-center justify-between mt-2"> <div className="text-sm text-gray-500">: {account.gender === "male" ? "男" : "女"}</div>
<span className="text-sm text-gray-500">: {account.friendCount}</span> <div className="flex items-center justify-between mt-2">
<Badge variant={account.addFriendStatus === "enabled" ? "outline" : "secondary"}> <span className="text-sm text-gray-500">: {account.friendCount}</span>
{account.addFriendStatus === "enabled" ? "可加友" : "已停用"} <Badge variant={getBadgeVariant(account.addFriendStatus)}>
</Badge> {account.addFriendStatus === "enabled" ? "可加友" : "已停用"}
</Badge>
</div>
</div> </div>
</div> </div>
</div> ))}
))} </div>
</div> ) : (
<div className="text-center py-8 text-gray-500">
<p></p>
</div>
)}
</ScrollArea> </ScrollArea>
</Card> </Card>
</TabsContent> </TabsContent>
@@ -237,21 +331,27 @@ export default function DeviceDetailPage() {
<TabsContent value="history"> <TabsContent value="history">
<Card className="p-4"> <Card className="p-4">
<ScrollArea className="h-[calc(100vh-300px)]"> <ScrollArea className="h-[calc(100vh-300px)]">
<div className="space-y-4"> {device.history && device.history.length > 0 ? (
{device.history.map((record, index) => ( <div className="space-y-4">
<div key={index} className="flex items-start space-x-3"> {device.history.map((record, index) => (
<div className="p-2 bg-blue-50 rounded-full"> <div key={index} className="flex items-start space-x-3">
<History className="w-4 h-4 text-blue-600" /> <div className="p-2 bg-blue-50 rounded-full">
</div> <History className="w-4 h-4 text-blue-600" />
<div className="flex-1"> </div>
<div className="text-sm font-medium">{record.action}</div> <div className="flex-1">
<div className="text-xs text-gray-500 mt-1"> <div className="text-sm font-medium">{record.action}</div>
: {record.operator} · {record.time} <div className="text-xs text-gray-500 mt-1">
: {record.operator} · {record.time}
</div>
</div> </div>
</div> </div>
</div> ))}
))} </div>
</div> ) : (
<div className="text-center py-8 text-gray-500">
<p></p>
</div>
)}
</ScrollArea> </ScrollArea>
</Card> </Card>
</TabsContent> </TabsContent>
@@ -264,7 +364,7 @@ export default function DeviceDetailPage() {
<span className="text-sm"></span> <span className="text-sm"></span>
</div> </div>
<div className="text-2xl font-bold text-blue-600 mt-2"> <div className="text-2xl font-bold text-blue-600 mt-2">
{device?.wechatAccounts?.reduce((sum, account) => sum + account.friendCount, 0)} {device.totalFriend || 0}
</div> </div>
</Card> </Card>
<Card className="p-4"> <Card className="p-4">
@@ -272,7 +372,9 @@ export default function DeviceDetailPage() {
<MessageCircle className="w-4 h-4" /> <MessageCircle className="w-4 h-4" />
<span className="text-sm"></span> <span className="text-sm"></span>
</div> </div>
<div className="text-2xl font-bold text-blue-600 mt-2">5,678</div> <div className="text-2xl font-bold text-blue-600 mt-2">
{device.thirtyDayMsgCount || 0}
</div>
</Card> </Card>
</div> </div>
</div> </div>

View File

@@ -223,14 +223,6 @@ class Device extends Controller
]); ]);
} }
// 检查设备是否属于用户所在公司
if ($info['companyId'] != $userInfo['companyId']) {
return json([
'code' => 403,
'msg' => '您没有权限查看该设备'
]);
}
return json([ return json([
'code' => 200, 'code' => 200,
'msg' => '获取成功', 'msg' => '获取成功',

View File

@@ -109,9 +109,61 @@ class Device extends Model
*/ */
public static function getDeviceInfo($id) public static function getDeviceInfo($id)
{ {
return self::where('id', $id) // 查询设备基础信息与关联的微信账号信息
->where('isDeleted', 0) $device = self::alias('d')
->field([
'd.id', 'd.imei', 'd.memo', 'd.alive', 'd.taskConfig', 'd.lastUpdateTime',
'w.id as wechatId', 'w.thirtyDayMsgCount', 'w.totalFriend', 'd.extra'
])
->leftJoin('tk_wechat_account w', 'd.imei = w.imei')
->where('d.id', $id)
->where('d.isDeleted', 0)
->find(); ->find();
// 如果设备存在,处理额外信息
if ($device) {
// 解析电量信息
$battery = 0;
if (!empty($device['extra'])) {
$extra = json_decode($device['extra'], true);
if (is_array($extra) && isset($extra['battery'])) {
$battery = intval($extra['battery']);
}
}
$device['battery'] = $battery;
// 解析taskConfig字段获取功能开关
$features = [
'autoAddFriend' => false,
'autoReply' => false,
'contentSync' => false,
'aiChat' => false
];
if (!empty($device['taskConfig'])) {
$taskConfig = json_decode($device['taskConfig'], true);
if (is_array($taskConfig)) {
// 映射taskConfig中的字段到前端需要的features
$features['autoAddFriend'] = isset($taskConfig['autoAddFriend']) ? (bool)$taskConfig['autoAddFriend'] : false;
$features['autoReply'] = isset($taskConfig['autoReply']) ? (bool)$taskConfig['autoReply'] : false;
$features['contentSync'] = isset($taskConfig['momentsSync']) ? (bool)$taskConfig['momentsSync'] : false;
$features['aiChat'] = isset($taskConfig['aiChat']) ? (bool)$taskConfig['aiChat'] : false;
}
}
$device['features'] = $features;
unset($device['extra']);
unset($device['taskConfig']);
// 格式化最后活跃时间
$device['lastUpdateTime'] = !empty($device['lastUpdateTime']) ? date('Y-m-d H:i:s', strtotime($device['lastUpdateTime'])) : date('Y-m-d H:i:s');
// 确保totalFriend和thirtyDayMsgCount有值防止NULL
$device['totalFriend'] = intval($device['totalFriend'] ?? 0);
$device['thirtyDayMsgCount'] = intval($device['thirtyDayMsgCount'] ?? 0);
}
return $device;
} }
/** /**