设备详情
This commit is contained in:
@@ -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}`);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -222,15 +222,7 @@ class Device extends Controller
|
|||||||
'msg' => '设备不存在'
|
'msg' => '设备不存在'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查设备是否属于用户所在公司
|
|
||||||
if ($info['companyId'] != $userInfo['companyId']) {
|
|
||||||
return json([
|
|
||||||
'code' => 403,
|
|
||||||
'msg' => '您没有权限查看该设备'
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return json([
|
return json([
|
||||||
'code' => 200,
|
'code' => 200,
|
||||||
'msg' => '获取成功',
|
'msg' => '获取成功',
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user