feat: dark mode

This commit is contained in:
2025-06-29 09:21:29 +08:00
parent 2a0115ea17
commit 81d73cf17c
10 changed files with 139 additions and 190 deletions
@@ -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>
))}
@@ -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>
)}
+57 -57
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>