feat: dark mode
This commit is contained in:
@@ -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">
|
||||
<
|
||||
</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">
|
||||
>
|
||||
</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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user