252 lines
12 KiB
TypeScript
252 lines
12 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useEffect } from 'react';
|
||
import { useRouter } from 'next/navigation';
|
||
import {
|
||
Monitor,
|
||
Clock,
|
||
LogOut,
|
||
Upload,
|
||
Calendar,
|
||
Search,
|
||
Server,
|
||
AlertCircle,
|
||
FileText
|
||
} from 'lucide-react';
|
||
import { format, differenceInMinutes } from 'date-fns';
|
||
import PushSubscription from './components/PushSubscription';
|
||
|
||
interface Host {
|
||
hostname: string;
|
||
lastUpdate: string;
|
||
}
|
||
|
||
export default function Home() {
|
||
const router = useRouter();
|
||
const [hosts, setHosts] = useState<Host[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [searchTerm, setSearchTerm] = useState('');
|
||
|
||
const fetchHosts = async () => {
|
||
try {
|
||
setLoading(true);
|
||
const response = await fetch('/hosts');
|
||
if (!response.ok) throw new Error('获取主机列表失败');
|
||
const data = await response.json();
|
||
setHosts(data);
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : '未知错误');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleLogout = async () => {
|
||
try {
|
||
await fetch('/api/auth/logout', { method: 'POST' });
|
||
// 刷新页面以触发重新认证(Basic Auth 或重定向)
|
||
window.location.reload();
|
||
} catch (error) {
|
||
console.error('Logout failed', error);
|
||
}
|
||
};
|
||
|
||
const formatDate = (date: string) => {
|
||
return format(new Date(date), 'yyyy-MM-dd HH:mm:ss');
|
||
};
|
||
|
||
const isRecent = (date: string) => {
|
||
const diffMinutes = differenceInMinutes(new Date(), new Date(date));
|
||
return diffMinutes <= 60; // 1小时内
|
||
};
|
||
|
||
const navigateToHost = (hostname: string) => {
|
||
router.push(`/hosts/${hostname}`);
|
||
};
|
||
|
||
useEffect(() => {
|
||
fetchHosts();
|
||
}, []);
|
||
|
||
const filteredHosts = hosts.filter(host =>
|
||
host.hostname.toLowerCase().includes(searchTerm.toLowerCase())
|
||
);
|
||
|
||
const activeHostsCount = hosts.filter(h => isRecent(h.lastUpdate)).length;
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 transition-colors duration-200">
|
||
{/* Navigation Bar */}
|
||
<nav className="bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700 sticky top-0 z-10">
|
||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||
<div className="flex justify-between h-16">
|
||
<div className="flex items-center">
|
||
<Monitor className="h-8 w-8 text-blue-600 dark:text-blue-400" />
|
||
<span className="ml-3 text-xl font-bold text-gray-900 dark:text-white">
|
||
屏幕监控系统
|
||
</span>
|
||
</div>
|
||
<div className="flex items-center space-x-4">
|
||
<PushSubscription />
|
||
<button
|
||
onClick={() => router.push('/tasks')}
|
||
className="inline-flex items-center justify-center px-3 py-2 border border-transparent text-sm font-medium rounded-md text-gray-700 dark:text-gray-200 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||
title="定时任务管理"
|
||
>
|
||
<Calendar className="h-4 w-4 sm:mr-2" />
|
||
<span className="hidden sm:inline">任务管理</span>
|
||
</button>
|
||
<button
|
||
onClick={() => router.push('/decode')}
|
||
className="inline-flex items-center justify-center px-3 py-2 border border-transparent text-sm font-medium rounded-md text-gray-700 dark:text-gray-200 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||
title="Base64 日志解码器"
|
||
>
|
||
<FileText className="h-4 w-4 sm:mr-2" />
|
||
<span className="hidden sm:inline">日志解码</span>
|
||
</button>
|
||
<button
|
||
onClick={() => router.push('/upload')}
|
||
className="inline-flex items-center justify-center px-3 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 shadow-sm"
|
||
title="上传新版本客户端"
|
||
>
|
||
<Upload className="h-4 w-4 sm:mr-2" />
|
||
<span className="hidden sm:inline">上传版本</span>
|
||
</button>
|
||
<div className="h-6 w-px bg-gray-300 dark:bg-gray-600 mx-2"></div>
|
||
<button
|
||
onClick={handleLogout}
|
||
className="inline-flex items-center justify-center px-3 py-2 border border-transparent text-sm font-medium rounded-md text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20 hover:bg-red-100 dark:hover:bg-red-900/40 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
|
||
title="退出登录"
|
||
>
|
||
<LogOut className="h-4 w-4 sm:mr-2" />
|
||
<span className="hidden sm:inline">退出</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<main className="max-w-7xl mx-auto py-8 px-4 sm:px-6 lg:px-8">
|
||
{/* Stats & Search */}
|
||
<div className="mb-8 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||
<div className="flex items-center space-x-4">
|
||
<div className="bg-white dark:bg-gray-800 px-4 py-3 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 flex items-center">
|
||
<div className="p-2 rounded-full bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 mr-3">
|
||
<Server className="h-5 w-5" />
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-gray-500 dark:text-gray-400 font-medium uppercase">总主机数</p>
|
||
<p className="text-xl font-bold text-gray-900 dark:text-white">{hosts.length}</p>
|
||
</div>
|
||
</div>
|
||
<div className="bg-white dark:bg-gray-800 px-4 py-3 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 flex items-center">
|
||
<div className="p-2 rounded-full bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400 mr-3">
|
||
<div className="h-2.5 w-2.5 rounded-full bg-green-500 ring-4 ring-green-100 dark:ring-green-900/30"></div>
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-gray-500 dark:text-gray-400 font-medium uppercase">在线主机</p>
|
||
<p className="text-xl font-bold text-gray-900 dark:text-white">{activeHostsCount}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="relative max-w-md w-full sm:w-64">
|
||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||
<Search className="h-5 w-5 text-gray-400" />
|
||
</div>
|
||
<input
|
||
type="text"
|
||
className="block w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md leading-5 bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 sm:text-sm transition-colors"
|
||
placeholder="搜索主机名..."
|
||
value={searchTerm}
|
||
onChange={(e) => setSearchTerm(e.target.value)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Content */}
|
||
{loading ? (
|
||
<div className="flex flex-col justify-center items-center h-64">
|
||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 dark:border-blue-400"></div>
|
||
<p className="mt-4 text-gray-500 dark:text-gray-400">正在加载主机列表...</p>
|
||
</div>
|
||
) : error ? (
|
||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-6 flex items-center justify-center">
|
||
<div className="text-center">
|
||
<AlertCircle className="mx-auto h-12 w-12 text-red-400 dark:text-red-500 mb-4" />
|
||
<h3 className="text-lg font-medium text-red-800 dark:text-red-200">加载失败</h3>
|
||
<p className="mt-2 text-red-700 dark:text-red-300">{error}</p>
|
||
<button
|
||
onClick={fetchHosts}
|
||
className="mt-4 px-4 py-2 bg-red-100 dark:bg-red-800 text-red-700 dark:text-red-100 rounded-md hover:bg-red-200 dark:hover:bg-red-700 transition-colors"
|
||
>
|
||
重试
|
||
</button>
|
||
</div>
|
||
</div>
|
||
) : filteredHosts.length === 0 ? (
|
||
<div className="text-center py-12 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||
<Server className="mx-auto h-12 w-12 text-gray-400" />
|
||
<h3 className="mt-2 text-sm font-medium text-gray-900 dark:text-white">没有找到主机</h3>
|
||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||
{searchTerm ? '尝试调整搜索关键词' : '暂无主机连接记录'}
|
||
</p>
|
||
</div>
|
||
) : (
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||
{filteredHosts.map((host) => {
|
||
const online = isRecent(host.lastUpdate);
|
||
return (
|
||
<div
|
||
key={host.hostname}
|
||
onClick={() => navigateToHost(host.hostname)}
|
||
className="group bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden hover:shadow-md hover:border-blue-300 dark:hover:border-blue-700 transition-all duration-200 cursor-pointer relative"
|
||
>
|
||
<div className={`absolute top-0 left-0 w-1 h-full ${online ? 'bg-green-500' : 'bg-gray-300 dark:bg-gray-600'}`}></div>
|
||
<div className="p-5 pl-6">
|
||
<div className="flex justify-between items-start mb-4">
|
||
<div className="bg-gray-100 dark:bg-gray-700 p-2 rounded-lg group-hover:bg-blue-50 dark:group-hover:bg-blue-900/20 transition-colors">
|
||
<Monitor className="h-6 w-6 text-gray-600 dark:text-gray-300 group-hover:text-blue-600 dark:group-hover:text-blue-400" />
|
||
</div>
|
||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||
online
|
||
? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400'
|
||
: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'
|
||
}`}>
|
||
{online ? '在线' : '离线'}
|
||
</span>
|
||
</div>
|
||
|
||
<h3 className="text-lg font-bold text-gray-900 dark:text-white truncate mb-1" title={decodeURI(host.hostname)}>
|
||
{decodeURI(host.hostname)}
|
||
</h3>
|
||
|
||
<div className="flex items-center text-sm text-gray-500 dark:text-gray-400 mt-3">
|
||
<Clock className="h-4 w-4 mr-1.5 flex-shrink-0" />
|
||
<span className="truncate">
|
||
{formatDate(host.lastUpdate)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-gray-50 dark:bg-gray-700/30 px-5 py-3 border-t border-gray-100 dark:border-gray-700 flex justify-between items-center">
|
||
<span className="text-xs font-medium text-blue-600 dark:text-blue-400 group-hover:underline">
|
||
查看详情
|
||
</span>
|
||
<div className="h-6 w-6 rounded-full bg-white dark:bg-gray-800 flex items-center justify-center shadow-sm text-gray-400 group-hover:text-blue-500 transition-colors">
|
||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</main>
|
||
</div>
|
||
);
|
||
}
|