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) => ( ))}
{getTitle()}
{loading ? (
加载中...
) : (
{viewMode === 'day' && renderDayView()} {viewMode === 'week' && renderWeekView()} {viewMode === 'month' && renderMonthView()} {viewMode === 'year' && renderYearView()}
)}
) }