import React, { useState, useEffect, useCallback } from 'react' import { ChevronLeft, ChevronRight, Clock, FileText, CheckSquare, Settings, Search, Trash2, BookOpen, Monitor, Code, GitBranch } from 'lucide-react' import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Area, AreaChart, BarChart, Bar } from 'recharts' import { getDayTimeData, getWeekTimeData, getMonthTimeData, getYearTimeData } from './api' import type { WeekTimeData } from './api' import { WEEK_DAYS } from '@shared/constants' import type { DayTimeData, MonthTimeData, YearTimeData, TabType, TabSummary, DaySummary } from '@shared/types/time' import { formatDurationShort, getWeekStart } from '@shared/utils/date' import { ViewMode } from './types' const formatDuration = (seconds: number): string => { return formatDurationShort(seconds * 1000) } const formatDateDisplay = (dateStr: string): string => { const [year, month, day] = dateStr.split('-') const date = new Date(parseInt(year), parseInt(month) - 1, parseInt(day)) const weekDay = WEEK_DAYS[date.getDay()] return `${parseInt(month)}月${parseInt(day)}日 ${weekDay}` } const getTabIcon = (tabType: TabType): React.ReactNode => { switch (tabType) { case 'markdown': return case 'todo': return case 'settings': return case 'search': return case 'recycle-bin': return case 'weread': return case 'remote': return case 'remote-desktop': return case 'remote-git': return case 'pydemos': return default: return } } const getTabColor = (tabType: TabType): string => { switch (tabType) { case 'markdown': return 'text-blue-500' case 'todo': return 'text-green-500' case 'settings': return 'text-gray-500' case 'search': return 'text-purple-500' case 'recycle-bin': return 'text-red-500' case 'weread': return 'text-orange-500' case 'time-tracking': return 'text-cyan-500' case 'remote': return 'text-indigo-500' case 'remote-desktop': return 'text-blue-600' case 'remote-git': return 'text-teal-500' case 'pydemos': return 'text-yellow-500' default: return 'text-gray-400' } } const getTabDisplayName = (tabType: TabType, fileName: string): string => { if (tabType === 'remote') { if (fileName === 'remote-tab') { return '远程' } if (fileName.startsWith('remote-git-')) { return '远程Git' } if (fileName.startsWith('remote-')) { return '远程桌面' } return '远程' } if (tabType === 'remote-desktop') { if (fileName.startsWith('remote-git-')) { return '远程Git' } const match = fileName.match(/device=([^&]+)/) const deviceName = match ? decodeURIComponent(match[1]) : '' if (deviceName) { return `远程桌面-${deviceName}` } return '远程桌面' } if (tabType === 'remote-git') { const match = fileName.match(/device=([^&]+)/) const deviceName = match ? decodeURIComponent(match[1]) : '' if (deviceName) { return `远程Git-${deviceName}` } return '远程Git' } return fileName } interface HourlyData { hour: string duration: number } const getHourlyUsageData = (sessions: Array<{ startTime: string; endTime?: string; duration: number; tabRecords?: Array<{ focusedPeriods: Array<{ start: string; end: string }> }> }>): HourlyData[] => { const hourlyMap: Record = {} for (let i = 0; i < 24; i++) { hourlyMap[i] = 0 } sessions.forEach(session => { if (session.tabRecords) { session.tabRecords.forEach(record => { record.focusedPeriods.forEach(period => { const [startH] = period.start.split(':').map(Number) const [endH, endM, endS] = period.end.split(':').map(Number) const startHour = startH const endHour = endH if (startHour === endHour) { const [_, startM, startS] = period.start.split(':').map(Number) const startSeconds = startM * 60 + startS const endSeconds = endM * 60 + endS hourlyMap[startHour] += (endSeconds - startSeconds) } else { for (let h = startHour; h <= endHour; h++) { if (h === startHour) { const [_, startM, startS] = period.start.split(':').map(Number) hourlyMap[h] += (60 - startM) * 60 - startS } else if (h === endHour) { hourlyMap[h] += endM * 60 + endS } else { hourlyMap[h] += 3600 } } } }) }) } else { const startTime = new Date(session.startTime) const endTime = session.endTime ? new Date(session.endTime) : new Date() const startHour = startTime.getHours() const endHour = endTime.getHours() const startMinute = startTime.getMinutes() const startSecond = startTime.getSeconds() const endMinute = endTime.getMinutes() const endSecond = endTime.getSeconds() if (startHour === endHour) { const duration = session.duration hourlyMap[startHour] += duration } else if (startHour < endHour) { for (let h = startHour; h <= endHour && h < 24; h++) { if (h === startHour) { const remainingMinutes = 60 - startMinute const remainingSeconds = remainingMinutes * 60 - startSecond hourlyMap[h] += remainingSeconds } else if (h === endHour) { hourlyMap[h] += endMinute * 60 + endSecond } else { hourlyMap[h] += 3600 } } } else { for (let h = startHour; h < 24; h++) { if (h === startHour) { const remainingMinutes = 60 - startMinute const remainingSeconds = remainingMinutes * 60 - startSecond hourlyMap[h] += remainingSeconds } else { hourlyMap[h] += 3600 } } for (let h = 0; h <= endHour; h++) { if (h === endHour) { hourlyMap[h] += endMinute * 60 + endSecond } else { hourlyMap[h] += 3600 } } } } }) return Object.entries(hourlyMap) .map(([hour, duration]) => ({ hour: `${hour.padStart(2, '0')}:00`, duration })) .sort((a, b) => parseInt(a.hour) - parseInt(b.hour)) } export const TimeTrackingPage: React.FC = () => { const [viewMode, setViewMode] = useState('day') const [currentDate, setCurrentDate] = useState(new Date()) const [dayData, setDayData] = useState(null) const [weekData, setWeekData] = useState(null) const [monthData, setMonthData] = useState(null) const [yearData, setYearData] = useState(null) const [loading, setLoading] = useState(true) const loadData = useCallback(async () => { setLoading(true) try { const year = currentDate.getFullYear() const month = currentDate.getMonth() + 1 const day = currentDate.getDate() if (viewMode === 'day') { const dateStr = `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}` const data = await getDayTimeData(dateStr) setDayData(data) } else if (viewMode === 'week') { const weekStart = getWeekStart(currentDate) const startDate = `${weekStart.getFullYear()}-${(weekStart.getMonth() + 1).toString().padStart(2, '0')}-${weekStart.getDate().toString().padStart(2, '0')}` const data = await getWeekTimeData(startDate) setWeekData(data) } else if (viewMode === 'month') { const data = await getMonthTimeData(`${year}-${month.toString().padStart(2, '0')}`) setMonthData(data) } else if (viewMode === 'year') { const data = await getYearTimeData(year) setYearData(data) } } catch (error) { console.error('Failed to load time data:', error) } finally { setLoading(false) } }, [viewMode, currentDate]) useEffect(() => { loadData() }, [loadData]) const handlePrev = () => { const newDate = new Date(currentDate) if (viewMode === 'day') { newDate.setDate(newDate.getDate() - 1) } else if (viewMode === 'week') { newDate.setDate(newDate.getDate() - 7) } else if (viewMode === 'month') { newDate.setMonth(newDate.getMonth() - 1) } else if (viewMode === 'year') { newDate.setFullYear(newDate.getFullYear() - 1) } setCurrentDate(newDate) } const handleNext = () => { const newDate = new Date(currentDate) if (viewMode === 'day') { newDate.setDate(newDate.getDate() + 1) } else if (viewMode === 'week') { newDate.setDate(newDate.getDate() + 7) } else if (viewMode === 'month') { newDate.setMonth(newDate.getMonth() + 1) } else if (viewMode === 'year') { newDate.setFullYear(newDate.getFullYear() + 1) } setCurrentDate(newDate) } const getTitle = (): string => { const year = currentDate.getFullYear() const month = currentDate.getMonth() + 1 const day = currentDate.getDate() if (viewMode === 'day') { return `${year}年${month}月${day}日` } else if (viewMode === 'week') { const weekStart = getWeekStart(currentDate) const weekEnd = new Date(weekStart) weekEnd.setDate(weekEnd.getDate() + 6) const startMonth = weekStart.getMonth() + 1 const startDay = weekStart.getDate() const endMonth = weekEnd.getMonth() + 1 const endDay = weekEnd.getDate() if (startMonth === endMonth) { return `${startMonth}月${startDay}日 - ${endDay}日` } else { return `${startMonth}月${startDay}日 - ${endMonth}月${endDay}日` } } else if (viewMode === 'month') { return `${year}年${month}月` } else if (viewMode === 'year') { return `${year}年` } return '' } const renderDayView = () => { if (!dayData) return null const tabSummaries: [string, TabSummary][] = Object.entries(dayData.tabSummary) .sort((a, b) => b[1].totalDuration - a[1].totalDuration) const hourlyData = getHourlyUsageData(dayData.sessions) return ( 今日概览 {formatDuration(dayData.totalDuration)} 总使用时长 {dayData.totalDuration > 0 && ( 使用时段分布 `${Math.floor(value / 60)}m`} /> [formatDuration(value), '使用时长']} /> )} {tabSummaries.length > 0 && ( 标签页使用统计 {tabSummaries.map(([key, summary]) => { const percentage = dayData.totalDuration > 0 ? Math.round((summary.totalDuration / dayData.totalDuration) * 100) : 0 return ( {getTabIcon(summary.tabType)} {getTabDisplayName(summary.tabType, summary.fileName)} {formatDuration(summary.totalDuration)} ({percentage}%) ) })} )} {dayData.sessions.length === 0 && ( 暂无使用记录 )} ) } const renderWeekView = () => { if (!weekData) return null const weekStart = getWeekStart(currentDate) const chartData = weekData.days.map((dayData, index) => { const date = new Date(weekStart) date.setDate(date.getDate() + index) return { day: WEEK_DAYS[date.getDay()], date: `${date.getMonth() + 1}月${date.getDate()}日`, duration: dayData.totalDuration, durationStr: formatDuration(dayData.totalDuration) } }) const maxDuration = Math.max(...chartData.map(d => d.duration), 1) return ( 本周概览 {formatDuration(weekData.totalDuration)} 总时长 {weekData.activeDays} 活跃天数 {formatDuration(weekData.averageDaily)} 日均时长 {weekData.totalDuration > 0 && ( 每日使用时长 formatDuration(value)} /> [formatDuration(value), '使用时长']} labelFormatter={(_, payload) => payload?.[0]?.payload?.date || ''} /> )} 每日明细 {chartData.map((data, index) => ( {data.day} {data.date} {data.durationStr} ))} {weekData.totalDuration === 0 && ( 暂无使用记录 )} ) } const renderMonthView = () => { if (!monthData) return null const days: [string, DaySummary][] = Object.entries(monthData.days) .sort((a, b) => parseInt(a[0]) - parseInt(b[0])) return ( 本月概览 {formatDuration(monthData.monthlyTotal)} 总时长 {monthData.activeDays} 活跃天数 {formatDuration(monthData.averageDaily)} 日均时长 {days.length > 0 && ( 每日统计 {days.map(([day, summary]) => { const year = currentDate.getFullYear() const month = currentDate.getMonth() + 1 const dateStr = `${year}-${month.toString().padStart(2, '0')}-${day}` return ( {formatDateDisplay(dateStr)} {formatDuration(summary.totalDuration)} ) })} )} {days.length === 0 && ( 暂无使用记录 )} ) } const renderYearView = () => { if (!yearData) return null const months: [string, { totalDuration: number; activeDays: number }][] = Object.entries(yearData.months) .sort((a, b) => parseInt(a[0]) - parseInt(b[0])) return ( 本年概览 {formatDuration(yearData.yearlyTotal)} 总时长 {yearData.totalActiveDays} 活跃天数 {formatDuration(yearData.averageDaily)} 日均时长 {months.length > 0 && ( 每月统计 {months.map(([month, summary]) => ( {currentDate.getFullYear()}年{parseInt(month)}月 {summary.activeDays} 天 {formatDuration(summary.totalDuration)} ))} )} {months.length === 0 && ( 暂无使用记录 )} ) } return ( 时间统计 {(['day', 'week', 'month', 'year'] as ViewMode[]).map((mode) => ( setViewMode(mode)} className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${viewMode === mode ? 'bg-gray-800 dark:bg-gray-200 text-white dark:text-gray-800' : 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600' }`} > {mode === 'day' ? '日' : mode === 'week' ? '周' : mode === 'month' ? '月' : '年'} ))} {getTitle()} {loading ? ( 加载中... ) : ( {viewMode === 'day' && renderDayView()} {viewMode === 'week' && renderWeekView()} {viewMode === 'month' && renderMonthView()} {viewMode === 'year' && renderYearView()} )} ) }