winupdate-neo/app/page.tsx

252 lines
12 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.

"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>
);
}