feat: 设备详情
This commit is contained in:
@@ -171,4 +171,30 @@ export const devicesApi = {
|
|||||||
async checkStatus(ids: string[]): Promise<ApiResponse<Record<string, DeviceStatus>>> {
|
async checkStatus(ids: string[]): Promise<ApiResponse<Record<string, DeviceStatus>>> {
|
||||||
return post<ApiResponse<Record<string, DeviceStatus>>>(`${API_BASE}/status`, { deviceIds: ids });
|
return post<ApiResponse<Record<string, DeviceStatus>>>(`${API_BASE}/status`, { deviceIds: ids });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 获取设备关联的微信账号
|
||||||
|
async getRelatedAccounts(id: string | number): Promise<ApiResponse<any>> {
|
||||||
|
return get<ApiResponse<any>>(`/v1/wechats/related-device/${id}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取设备操作记录
|
||||||
|
async getHandleLogs(id: string | number, page: number = 1, limit: number = 10): Promise<ApiResponse<any>> {
|
||||||
|
return get<ApiResponse<any>>(`/v1/devices/${id}/handle-logs?page=${page}&limit=${limit}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新设备任务配置
|
||||||
|
async updateTaskConfig(config: {
|
||||||
|
deviceId: string | number;
|
||||||
|
autoAddFriend?: boolean;
|
||||||
|
autoReply?: boolean;
|
||||||
|
momentsSync?: boolean;
|
||||||
|
aiChat?: boolean;
|
||||||
|
}): Promise<ApiResponse<any>> {
|
||||||
|
return post<ApiResponse<any>>(`/v1/devices/task-config`, config);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取设备任务配置
|
||||||
|
async getTaskConfig(id: string | number): Promise<ApiResponse<any>> {
|
||||||
|
return get<ApiResponse<any>>(`/v1/devices/${id}/task-config`);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
@@ -1,7 +1,849 @@
|
|||||||
import React from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
import { ChevronLeft, Smartphone, Battery, Wifi, MessageCircle, Users, Settings, History, RefreshCw, Loader2 } from 'lucide-react';
|
||||||
|
import { devicesApi, fetchDeviceDetail, fetchDeviceRelatedAccounts, fetchDeviceHandleLogs, updateDeviceTaskConfig } from '../../api/devices';
|
||||||
|
import { useToast } from '../../components/ui/toast';
|
||||||
|
|
||||||
|
interface WechatAccount {
|
||||||
|
id: string;
|
||||||
|
avatar: string;
|
||||||
|
nickname: string;
|
||||||
|
wechatId: string;
|
||||||
|
gender: number;
|
||||||
|
status: number;
|
||||||
|
statusText: string;
|
||||||
|
wechatAlive: number;
|
||||||
|
wechatAliveText: string;
|
||||||
|
addFriendStatus: number;
|
||||||
|
totalFriend: number;
|
||||||
|
lastActive: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Device {
|
||||||
|
id: string;
|
||||||
|
imei: string;
|
||||||
|
name: string;
|
||||||
|
status: "online" | "offline";
|
||||||
|
battery: number;
|
||||||
|
lastActive: string;
|
||||||
|
historicalIds: string[];
|
||||||
|
wechatAccounts: WechatAccount[];
|
||||||
|
features: {
|
||||||
|
autoAddFriend: boolean;
|
||||||
|
autoReply: boolean;
|
||||||
|
momentsSync: boolean;
|
||||||
|
aiChat: boolean;
|
||||||
|
};
|
||||||
|
history: {
|
||||||
|
time: string;
|
||||||
|
action: string;
|
||||||
|
operator: string;
|
||||||
|
}[];
|
||||||
|
totalFriend: number;
|
||||||
|
thirtyDayMsgCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HandleLog {
|
||||||
|
id: string | number;
|
||||||
|
content: string;
|
||||||
|
username: string;
|
||||||
|
createTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function DeviceDetail() {
|
export default function DeviceDetail() {
|
||||||
const { id } = useParams();
|
const { id } = useParams<{ id: string }>();
|
||||||
return <div>设备详情页,当前ID: {id}</div>;
|
const navigate = useNavigate();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [device, setDevice] = useState<Device | null>(null);
|
||||||
|
const [activeTab, setActiveTab] = useState("info");
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [accountsLoading, setAccountsLoading] = useState(false);
|
||||||
|
const [logsLoading, setLogsLoading] = useState(false);
|
||||||
|
const [handleLogs, setHandleLogs] = useState<HandleLog[]>([]);
|
||||||
|
const [logPage, setLogPage] = useState(1);
|
||||||
|
const [hasMoreLogs, setHasMoreLogs] = useState(true);
|
||||||
|
const logsPerPage = 10;
|
||||||
|
const logsEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [savingFeatures, setSavingFeatures] = useState({
|
||||||
|
autoAddFriend: false,
|
||||||
|
autoReply: false,
|
||||||
|
momentsSync: false,
|
||||||
|
aiChat: false
|
||||||
|
});
|
||||||
|
const [tabChangeLoading, setTabChangeLoading] = useState(false);
|
||||||
|
const [accountPage, setAccountPage] = useState(1);
|
||||||
|
const [hasMoreAccounts, setHasMoreAccounts] = useState(true);
|
||||||
|
const accountsPerPage = 10;
|
||||||
|
const accountsEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// 获取设备详情
|
||||||
|
useEffect(() => {
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
const fetchDevice = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetchDeviceDetail(id);
|
||||||
|
|
||||||
|
if (response && response.code === 200 && response.data) {
|
||||||
|
const serverData = response.data;
|
||||||
|
|
||||||
|
// 构建符合前端期望格式的设备对象
|
||||||
|
const formattedDevice: Device = {
|
||||||
|
id: serverData.id?.toString() || "",
|
||||||
|
imei: serverData.imei || "",
|
||||||
|
name: serverData.memo || "未命名设备",
|
||||||
|
status: serverData.alive === 1 ? "online" : "offline",
|
||||||
|
battery: serverData.battery || 0,
|
||||||
|
lastActive: serverData.lastUpdateTime || new Date().toISOString(),
|
||||||
|
historicalIds: [],
|
||||||
|
wechatAccounts: [],
|
||||||
|
history: [],
|
||||||
|
features: {
|
||||||
|
autoAddFriend: false,
|
||||||
|
autoReply: false,
|
||||||
|
momentsSync: false,
|
||||||
|
aiChat: false
|
||||||
|
},
|
||||||
|
totalFriend: serverData.totalFriend || 0,
|
||||||
|
thirtyDayMsgCount: serverData.thirtyDayMsgCount || 0
|
||||||
|
};
|
||||||
|
|
||||||
|
// 解析features
|
||||||
|
if (serverData.features) {
|
||||||
|
formattedDevice.features = {
|
||||||
|
autoAddFriend: Boolean(serverData.features.autoAddFriend),
|
||||||
|
autoReply: Boolean(serverData.features.autoReply),
|
||||||
|
momentsSync: Boolean(serverData.features.momentsSync || serverData.features.contentSync),
|
||||||
|
aiChat: Boolean(serverData.features.aiChat)
|
||||||
|
};
|
||||||
|
} else if (serverData.taskConfig) {
|
||||||
|
try {
|
||||||
|
const taskConfig = JSON.parse(serverData.taskConfig || '{}');
|
||||||
|
|
||||||
|
if (taskConfig) {
|
||||||
|
formattedDevice.features = {
|
||||||
|
autoAddFriend: Boolean(taskConfig.autoAddFriend),
|
||||||
|
autoReply: Boolean(taskConfig.autoReply),
|
||||||
|
momentsSync: Boolean(taskConfig.momentsSync),
|
||||||
|
aiChat: Boolean(taskConfig.aiChat)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('解析taskConfig失败:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setDevice(formattedDevice);
|
||||||
|
|
||||||
|
// 如果当前激活标签是"accounts",则立即加载关联微信账号
|
||||||
|
if (activeTab === "accounts") {
|
||||||
|
fetchRelatedAccounts();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: "获取设备信息失败",
|
||||||
|
description: response.message || "未知错误",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取设备信息失败:", error);
|
||||||
|
toast({
|
||||||
|
title: "获取设备信息失败",
|
||||||
|
description: "请稍后重试",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchDevice();
|
||||||
|
}, [id, toast]);
|
||||||
|
|
||||||
|
// 获取设备关联微信账号
|
||||||
|
const fetchRelatedAccounts = async (page = 1) => {
|
||||||
|
if (!id || accountsLoading) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setAccountsLoading(true);
|
||||||
|
const response = await fetchDeviceRelatedAccounts(id);
|
||||||
|
|
||||||
|
if (response && response.code === 200 && response.data) {
|
||||||
|
const accounts = response.data.accounts || [];
|
||||||
|
|
||||||
|
if (page === 1) {
|
||||||
|
setDevice(prev => prev ? {
|
||||||
|
...prev,
|
||||||
|
wechatAccounts: accounts
|
||||||
|
} : null);
|
||||||
|
} else {
|
||||||
|
setDevice(prev => prev ? {
|
||||||
|
...prev,
|
||||||
|
wechatAccounts: [...prev.wechatAccounts, ...accounts]
|
||||||
|
} : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasMoreAccounts(accounts.length === accountsPerPage);
|
||||||
|
setAccountPage(page);
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: "获取关联账号失败",
|
||||||
|
description: response.message || "请稍后重试",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取关联账号失败:", error);
|
||||||
|
toast({
|
||||||
|
title: "获取关联账号失败",
|
||||||
|
description: "请稍后重试",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setAccountsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取操作记录
|
||||||
|
const fetchHandleLogs = async () => {
|
||||||
|
if (!id || logsLoading) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLogsLoading(true);
|
||||||
|
const response = await fetchDeviceHandleLogs(id, logPage, logsPerPage);
|
||||||
|
|
||||||
|
if (response && response.code === 200 && response.data) {
|
||||||
|
const logs = response.data.list || [];
|
||||||
|
|
||||||
|
if (logPage === 1) {
|
||||||
|
setHandleLogs(logs);
|
||||||
|
} else {
|
||||||
|
setHandleLogs(prev => [...prev, ...logs]);
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasMoreLogs(logs.length === logsPerPage);
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: "获取操作记录失败",
|
||||||
|
description: response.message || "请稍后重试",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取操作记录失败:", error);
|
||||||
|
toast({
|
||||||
|
title: "获取操作记录失败",
|
||||||
|
description: "请稍后重试",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLogsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载更多操作记录
|
||||||
|
const loadMoreLogs = () => {
|
||||||
|
if (logsLoading || !hasMoreLogs) return;
|
||||||
|
setLogPage(prev => prev + 1);
|
||||||
|
fetchHandleLogs();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 无限滚动加载操作记录
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hasMoreLogs || logsLoading) return;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
entries => {
|
||||||
|
if (entries[0].isIntersecting && hasMoreLogs && !logsLoading) {
|
||||||
|
loadMoreLogs();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.5 }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (logsEndRef.current) {
|
||||||
|
observer.observe(logsEndRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, [hasMoreLogs, logsLoading]);
|
||||||
|
|
||||||
|
// 获取任务配置
|
||||||
|
const fetchTaskConfig = async () => {
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await devicesApi.getTaskConfig(id);
|
||||||
|
if (response && response.code === 200 && response.data) {
|
||||||
|
const config = response.data;
|
||||||
|
setDevice(prev => prev ? {
|
||||||
|
...prev,
|
||||||
|
features: {
|
||||||
|
autoAddFriend: Boolean(config.autoAddFriend),
|
||||||
|
autoReply: Boolean(config.autoReply),
|
||||||
|
momentsSync: Boolean(config.momentsSync),
|
||||||
|
aiChat: Boolean(config.aiChat)
|
||||||
|
}
|
||||||
|
} : null);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取任务配置失败:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 标签页切换处理
|
||||||
|
const handleTabChange = (value: string) => {
|
||||||
|
setActiveTab(value);
|
||||||
|
setTabChangeLoading(true);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (value === "accounts" && device && (!device.wechatAccounts || device.wechatAccounts.length === 0)) {
|
||||||
|
fetchRelatedAccounts(1);
|
||||||
|
} else if (value === "history" && handleLogs.length === 0) {
|
||||||
|
setLogPage(1);
|
||||||
|
setHasMoreLogs(true);
|
||||||
|
fetchHandleLogs();
|
||||||
|
}
|
||||||
|
setTabChangeLoading(false);
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 功能开关处理
|
||||||
|
const handleFeatureChange = async (feature: keyof Device['features'], checked: boolean) => {
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
setSavingFeatures(prev => ({ ...prev, [feature]: true }));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await updateDeviceTaskConfig({
|
||||||
|
deviceId: id,
|
||||||
|
[feature]: checked
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response && response.code === 200) {
|
||||||
|
setDevice(prev => prev ? {
|
||||||
|
...prev,
|
||||||
|
features: {
|
||||||
|
...prev.features,
|
||||||
|
[feature]: checked
|
||||||
|
}
|
||||||
|
} : null);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "设置成功",
|
||||||
|
description: `${getFeatureName(feature)}已${checked ? '启用' : '禁用'}`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: "设置失败",
|
||||||
|
description: response.message || "请稍后重试",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("设置功能失败:", error);
|
||||||
|
toast({
|
||||||
|
title: "设置失败",
|
||||||
|
description: "请稍后重试",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setSavingFeatures(prev => ({ ...prev, [feature]: false }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取功能名称
|
||||||
|
const getFeatureName = (feature: string): string => {
|
||||||
|
const names: Record<string, string> = {
|
||||||
|
autoAddFriend: "自动加好友",
|
||||||
|
autoReply: "自动回复",
|
||||||
|
momentsSync: "朋友圈同步",
|
||||||
|
aiChat: "AI会话"
|
||||||
|
};
|
||||||
|
return names[feature] || feature;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载更多账号
|
||||||
|
const loadMoreAccounts = () => {
|
||||||
|
if (accountsLoading || !hasMoreAccounts) return;
|
||||||
|
fetchRelatedAccounts(accountPage + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 无限滚动加载账号
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hasMoreAccounts || accountsLoading) return;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
entries => {
|
||||||
|
if (entries[0].isIntersecting && hasMoreAccounts && !accountsLoading) {
|
||||||
|
loadMoreAccounts();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.5 }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (accountsEndRef.current) {
|
||||||
|
observer.observe(accountsEndRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, [hasMoreAccounts, accountsLoading]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||||
|
<div className="flex flex-col items-center space-y-4">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
|
||||||
|
<p className="text-gray-500">加载设备信息中...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!device) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||||
|
<div className="flex flex-col items-center space-y-4 p-6 bg-white rounded-xl shadow-sm max-w-md">
|
||||||
|
<div className="w-12 h-12 flex items-center justify-center rounded-full bg-red-100">
|
||||||
|
<Smartphone className="h-6 w-6 text-red-500" />
|
||||||
|
</div>
|
||||||
|
<div className="text-xl font-medium text-center">设备不存在或已被删除</div>
|
||||||
|
<div className="text-sm text-gray-500 text-center">
|
||||||
|
无法加载ID为 "{id}" 的设备信息,请检查设备是否存在。
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => navigate(-1)}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
返回上一页
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
{/* 固定header */}
|
||||||
|
<header className="fixed top-0 left-0 right-0 z-20 bg-white border-b border-gray-200">
|
||||||
|
<div className="flex items-center justify-between px-4 py-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
|
onClick={() => navigate(-1)}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
<h1 className="text-lg font-semibold">设备详情</h1>
|
||||||
|
</div>
|
||||||
|
<button className="p-2 hover:bg-gray-100 rounded-lg transition-colors">
|
||||||
|
<Settings className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* 内容区域 */}
|
||||||
|
<div className="pt-16 pb-20">
|
||||||
|
<div className="p-4 space-y-4">
|
||||||
|
{/* 设备基本信息卡片 */}
|
||||||
|
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-100">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="p-3 bg-blue-50 rounded-lg">
|
||||||
|
<Smartphone className="h-6 w-6 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="font-semibold truncate">{device.name}</h2>
|
||||||
|
<span className={`px-2.5 py-1 text-xs rounded-full font-medium ${
|
||||||
|
device.status === "online"
|
||||||
|
? "bg-green-100 text-green-700"
|
||||||
|
: "bg-gray-100 text-gray-600"
|
||||||
|
}`}>
|
||||||
|
{device.status === "online" ? "在线" : "离线"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500 mt-1">
|
||||||
|
<span className="mr-1">IMEI:</span>
|
||||||
|
{device.imei}
|
||||||
|
</div>
|
||||||
|
{device.historicalIds && device.historicalIds.length > 0 && (
|
||||||
|
<div className="text-sm text-gray-500">历史ID: {device.historicalIds.join(", ")}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 grid grid-cols-2 gap-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Battery className={`w-4 h-4 ${device.battery < 20 ? "text-red-500" : "text-green-500"}`} />
|
||||||
|
<span className="text-sm">{device.battery}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Wifi className="w-4 h-4 text-blue-500" />
|
||||||
|
<span className="text-sm">{device.status === "online" ? "已连接" : "未连接"}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-sm text-gray-500">最后活跃:{device.lastActive}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 标签页 */}
|
||||||
|
<div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
||||||
|
<div className="flex border-b border-gray-200">
|
||||||
|
<button
|
||||||
|
className={`flex-1 py-3 px-4 text-sm font-medium transition-colors ${
|
||||||
|
activeTab === "info"
|
||||||
|
? "text-blue-600 border-b-2 border-blue-600"
|
||||||
|
: "text-gray-500 hover:text-gray-700"
|
||||||
|
}`}
|
||||||
|
onClick={() => handleTabChange("info")}
|
||||||
|
>
|
||||||
|
基本信息
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`flex-1 py-3 px-4 text-sm font-medium transition-colors ${
|
||||||
|
activeTab === "accounts"
|
||||||
|
? "text-blue-600 border-b-2 border-blue-600"
|
||||||
|
: "text-gray-500 hover:text-gray-700"
|
||||||
|
}`}
|
||||||
|
onClick={() => handleTabChange("accounts")}
|
||||||
|
>
|
||||||
|
关联账号
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`flex-1 py-3 px-4 text-sm font-medium transition-colors ${
|
||||||
|
activeTab === "history"
|
||||||
|
? "text-blue-600 border-b-2 border-blue-600"
|
||||||
|
: "text-gray-500 hover:text-gray-700"
|
||||||
|
}`}
|
||||||
|
onClick={() => handleTabChange("history")}
|
||||||
|
>
|
||||||
|
操作记录
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 基本信息标签页 */}
|
||||||
|
{activeTab === "info" && (
|
||||||
|
<div className="p-4 space-y-4">
|
||||||
|
{/* 功能配置 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-sm font-medium">自动加好友</div>
|
||||||
|
<div className="text-xs text-gray-500">自动通过好友验证</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
{savingFeatures.autoAddFriend && (
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin text-blue-500" />
|
||||||
|
)}
|
||||||
|
<label className="relative inline-flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={Boolean(device.features.autoAddFriend)}
|
||||||
|
onChange={(e) => handleFeatureChange('autoAddFriend', e.target.checked)}
|
||||||
|
disabled={savingFeatures.autoAddFriend}
|
||||||
|
className="sr-only peer"
|
||||||
|
/>
|
||||||
|
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-sm font-medium">自动回复</div>
|
||||||
|
<div className="text-xs text-gray-500">自动回复好友消息</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
{savingFeatures.autoReply && (
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin text-blue-500" />
|
||||||
|
)}
|
||||||
|
<label className="relative inline-flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={Boolean(device.features.autoReply)}
|
||||||
|
onChange={(e) => handleFeatureChange('autoReply', e.target.checked)}
|
||||||
|
disabled={savingFeatures.autoReply}
|
||||||
|
className="sr-only peer"
|
||||||
|
/>
|
||||||
|
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-sm font-medium">朋友圈同步</div>
|
||||||
|
<div className="text-xs text-gray-500">自动同步朋友圈内容</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
{savingFeatures.momentsSync && (
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin text-blue-500" />
|
||||||
|
)}
|
||||||
|
<label className="relative inline-flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={Boolean(device.features.momentsSync)}
|
||||||
|
onChange={(e) => handleFeatureChange('momentsSync', e.target.checked)}
|
||||||
|
disabled={savingFeatures.momentsSync}
|
||||||
|
className="sr-only peer"
|
||||||
|
/>
|
||||||
|
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-sm font-medium">AI会话</div>
|
||||||
|
<div className="text-xs text-gray-500">启用AI智能对话</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
{savingFeatures.aiChat && (
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin text-blue-500" />
|
||||||
|
)}
|
||||||
|
<label className="relative inline-flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={Boolean(device.features.aiChat)}
|
||||||
|
onChange={(e) => handleFeatureChange('aiChat', e.target.checked)}
|
||||||
|
disabled={savingFeatures.aiChat}
|
||||||
|
className="sr-only peer"
|
||||||
|
/>
|
||||||
|
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 统计卡片 */}
|
||||||
|
<div className="grid grid-cols-2 gap-4 mt-4">
|
||||||
|
<div className="bg-gray-50 p-4 rounded-xl">
|
||||||
|
<div className="flex items-center gap-2 text-gray-500">
|
||||||
|
<Users className="w-4 h-4" />
|
||||||
|
<span className="text-sm">好友总数</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-blue-600 mt-2">
|
||||||
|
{(device.totalFriend || 0).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-gray-50 p-4 rounded-xl">
|
||||||
|
<div className="flex items-center gap-2 text-gray-500">
|
||||||
|
<MessageCircle className="w-4 h-4" />
|
||||||
|
<span className="text-sm">消息数量</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-blue-600 mt-2">
|
||||||
|
{(device.thirtyDayMsgCount || 0).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 关联账号标签页 */}
|
||||||
|
{activeTab === "accounts" && (
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h3 className="text-md font-medium">微信账号列表</h3>
|
||||||
|
<button
|
||||||
|
className="px-3 py-1.5 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors text-sm flex items-center gap-2"
|
||||||
|
onClick={() => {
|
||||||
|
setAccountPage(1);
|
||||||
|
setHasMoreAccounts(true);
|
||||||
|
fetchRelatedAccounts(1);
|
||||||
|
}}
|
||||||
|
disabled={accountsLoading}
|
||||||
|
>
|
||||||
|
{accountsLoading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
刷新中
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="h-4 w-4" />
|
||||||
|
刷新
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="min-h-[120px] max-h-[calc(100vh-300px)] overflow-y-auto">
|
||||||
|
{accountsLoading && !device?.wechatAccounts?.length ? (
|
||||||
|
<div className="flex justify-center items-center py-8">
|
||||||
|
<Loader2 className="w-6 h-6 animate-spin text-blue-500 mr-2" />
|
||||||
|
<span className="text-gray-500">加载微信账号中...</span>
|
||||||
|
</div>
|
||||||
|
) : device?.wechatAccounts && device.wechatAccounts.length > 0 ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{device.wechatAccounts.map((account) => (
|
||||||
|
<div key={account.id} className="flex items-start gap-3 p-3 bg-gray-50 rounded-lg">
|
||||||
|
<img
|
||||||
|
src={account.avatar || "/placeholder.svg"}
|
||||||
|
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="font-medium truncate">{account.nickname}</div>
|
||||||
|
<span className={`px-2 py-1 text-xs rounded-full ${
|
||||||
|
account.wechatAlive === 1
|
||||||
|
? "bg-green-100 text-green-700"
|
||||||
|
: "bg-red-100 text-red-700"
|
||||||
|
}`}>
|
||||||
|
{account.wechatAliveText}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500 mt-1">微信号: {account.wechatId}</div>
|
||||||
|
<div className="text-sm text-gray-500">性别: {account.gender === 1 ? "男" : "女"}</div>
|
||||||
|
<div className="flex items-center justify-between mt-2">
|
||||||
|
<span className="text-sm text-gray-500">好友数: {account.totalFriend}</span>
|
||||||
|
<span className={`px-2 py-1 text-xs rounded-full ${
|
||||||
|
account.status === 1
|
||||||
|
? "bg-blue-100 text-blue-700"
|
||||||
|
: "bg-gray-100 text-gray-600"
|
||||||
|
}`}>
|
||||||
|
{account.statusText}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-400 mt-1">最后活跃: {account.lastActive}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* 加载更多区域 */}
|
||||||
|
<div
|
||||||
|
ref={accountsEndRef}
|
||||||
|
className="py-2 flex justify-center items-center"
|
||||||
|
>
|
||||||
|
{accountsLoading && hasMoreAccounts ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
|
||||||
|
<span className="text-sm text-gray-500">加载更多...</span>
|
||||||
|
</div>
|
||||||
|
) : hasMoreAccounts ? (
|
||||||
|
<button
|
||||||
|
className="text-sm text-blue-500 hover:text-blue-600"
|
||||||
|
onClick={loadMoreAccounts}
|
||||||
|
>
|
||||||
|
加载更多
|
||||||
|
</button>
|
||||||
|
) : device.wechatAccounts.length > 0 && (
|
||||||
|
<span className="text-xs text-gray-400">- 已加载全部记录 -</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
<p>此设备暂无关联的微信账号</p>
|
||||||
|
<button
|
||||||
|
className="mt-2 px-3 py-1.5 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors text-sm flex items-center gap-2 mx-auto"
|
||||||
|
onClick={() => fetchRelatedAccounts(1)}
|
||||||
|
>
|
||||||
|
<RefreshCw className="h-4 w-4" />
|
||||||
|
刷新
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 操作记录标签页 */}
|
||||||
|
{activeTab === "history" && (
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h3 className="text-md font-medium">操作记录</h3>
|
||||||
|
<button
|
||||||
|
className="px-3 py-1.5 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors text-sm flex items-center gap-2"
|
||||||
|
onClick={() => {
|
||||||
|
setLogPage(1);
|
||||||
|
setHasMoreLogs(true);
|
||||||
|
fetchHandleLogs();
|
||||||
|
}}
|
||||||
|
disabled={logsLoading}
|
||||||
|
>
|
||||||
|
{logsLoading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
加载中
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="h-4 w-4" />
|
||||||
|
刷新
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-[calc(min(80vh, 500px))] overflow-y-auto">
|
||||||
|
{logsLoading && handleLogs.length === 0 ? (
|
||||||
|
<div className="flex justify-center items-center py-8">
|
||||||
|
<Loader2 className="w-6 h-6 animate-spin text-blue-500 mr-2" />
|
||||||
|
<span className="text-gray-500">加载操作记录中...</span>
|
||||||
|
</div>
|
||||||
|
) : handleLogs.length > 0 ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{handleLogs.map((log) => (
|
||||||
|
<div key={log.id} className="flex items-start gap-3">
|
||||||
|
<div className="p-2 bg-blue-50 rounded-full">
|
||||||
|
<History className="w-4 h-4 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-sm font-medium">{log.content}</div>
|
||||||
|
<div className="text-xs text-gray-500 mt-1">
|
||||||
|
操作人: {log.username} · {log.createTime}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* 加载更多区域 */}
|
||||||
|
<div
|
||||||
|
ref={logsEndRef}
|
||||||
|
className="py-2 flex justify-center items-center"
|
||||||
|
>
|
||||||
|
{logsLoading && hasMoreLogs ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
|
||||||
|
<span className="text-sm text-gray-500">加载更多...</span>
|
||||||
|
</div>
|
||||||
|
) : hasMoreLogs ? (
|
||||||
|
<button
|
||||||
|
className="text-sm text-blue-500 hover:text-blue-600"
|
||||||
|
onClick={loadMoreLogs}
|
||||||
|
>
|
||||||
|
加载更多
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-gray-400">- 已加载全部记录 -</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
<p>暂无操作记录</p>
|
||||||
|
<button
|
||||||
|
className="mt-2 px-3 py-1.5 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors text-sm flex items-center gap-2 mx-auto"
|
||||||
|
onClick={() => {
|
||||||
|
setLogPage(1);
|
||||||
|
setHasMoreLogs(true);
|
||||||
|
fetchHandleLogs();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RefreshCw className="h-4 w-4" />
|
||||||
|
刷新
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user