Files
cunkebao_v3/nkebao/src/pages/devices/DeviceDetail.tsx
2025-07-07 17:28:11 +08:00

869 lines
35 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect, useRef } from 'react';
import { useParams } from 'react-router-dom';
import PageHeader from '@/components/PageHeader';
import BackButton from '@/components/BackButton';
import { useSimpleBack } from '@/hooks/useBackNavigation';
import { 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';
import Layout from '@/components/Layout';
import BottomNav from '@/components/BottomNav';
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() {
const { id } = useParams<{ id: string }>();
const { goBack } = useSimpleBack('/devices');
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 [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);
// 获取设备任务配置
await fetchTaskConfig();
// 如果当前激活标签是"accounts",则立即加载关联微信账号
if (activeTab === "accounts") {
fetchRelatedAccounts();
}
} else {
toast({
title: "获取设备信息失败",
description: response.msg || "未知错误",
variant: "destructive",
});
}
} catch (error) {
console.error("获取设备信息失败:", error);
toast({
title: "获取设备信息失败",
description: "请稍后重试",
variant: "destructive",
});
} finally {
setLoading(false);
}
};
fetchDevice();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);
// 获取设备关联微信账号
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.msg || "请稍后重试",
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.msg || "请稍后重试",
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, loadMoreLogs]);
// 获取任务配置
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);
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();
}
}, 100);
};
// 功能开关处理 - 只更新开关状态,不重新加载页面
const handleFeatureChange = async (feature: keyof Device['features'], checked: boolean) => {
if (!id) return;
// 立即更新UI状态提供即时反馈
setDevice(prev => prev ? {
...prev,
features: {
...prev.features,
[feature]: checked
}
} : null);
setSavingFeatures(prev => ({ ...prev, [feature]: true }));
try {
const response = await updateDeviceTaskConfig({
deviceId: id,
[feature]: checked
});
if (response && response.code === 200) {
// 请求成功,显示成功提示
toast({
title: "设置成功",
description: `${getFeatureName(feature)}${checked ? '启用' : '禁用'}`,
});
} else {
// 请求失败回滚UI状态
setDevice(prev => prev ? {
...prev,
features: {
...prev.features,
[feature]: !checked
}
} : null);
toast({
title: "设置失败",
description: response.msg || "请稍后重试",
variant: "destructive",
});
}
} catch (error) {
console.error("设置功能失败:", error);
// 网络错误回滚UI状态
setDevice(prev => prev ? {
...prev,
features: {
...prev.features,
[feature]: !checked
}
} : null);
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, loadMoreAccounts]);
if (loading) {
return (
<Layout
header={<PageHeader title="设备详情" defaultBackPath="/devices" rightContent={<button className="p-2 hover:bg-gray-100 rounded-lg transition-colors"><Settings className="h-5 w-5" /></button>} />}
footer={<BottomNav />}
>
<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>
</Layout>
);
}
if (!device) {
return (
<Layout
header={<PageHeader title="设备详情" defaultBackPath="/devices" rightContent={<button className="p-2 hover:bg-gray-100 rounded-lg transition-colors"><Settings className="h-5 w-5" /></button>} />}
footer={<BottomNav />}
>
<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>
<BackButton
variant="button"
text="返回上一页"
onBack={goBack}
/>
</div>
</div>
</Layout>
);
}
return (
<Layout
header={<PageHeader title="设备详情" defaultBackPath="/devices" rightContent={<button className="p-2 hover:bg-gray-100 rounded-lg transition-colors"><Settings className="h-5 w-5" /></button>} />}
footer={<BottomNav />}
>
<div className="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-xs 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 ? (
<React.Fragment key="loading">
<Loader2 className="h-4 w-4 animate-spin" />
</React.Fragment>
) : (
<React.Fragment key="refresh">
<RefreshCw className="h-4 w-4" />
</React.Fragment>
)}
</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 ? (
<React.Fragment key="logs-loading">
<Loader2 className="h-4 w-4 animate-spin" />
</React.Fragment>
) : (
<React.Fragment key="logs-refresh">
<RefreshCw className="h-4 w-4" />
</React.Fragment>
)}
</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>
</Layout>
);
}