"use client"; import React, { useMemo } from 'react'; import { Bar, BarChart, CartesianGrid, Cell, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; import { Loader2, } from 'lucide-react'; import { ScreenTimeResponse } from '../types'; import { formatDuration, getLocalDateInputValue } from '../utils'; interface ScreenTimeOverviewChartProps { data: ScreenTimeResponse | null; unit: 'day' | 'week'; loading: boolean; refreshing: boolean; onSelectDate: (date: string, nextUnit?: 'day' | 'week') => void; } const SPOTLIGHT_COLORS = ['#2f7cff', '#17c3d1', '#ff9f43', '#ff6b6b'] as const; const SPOTLIGHT_KEYS = ['spotlight_0', 'spotlight_1', 'spotlight_2', 'spotlight_3'] as const; type SpotlightKey = typeof SPOTLIGHT_KEYS[number]; type WeekChartDatum = Record & { date: string; label: string; totalDurationSeconds: number; mutedDuration: number; selectedRemainder: number; isSelected: boolean; }; type HourChartDatum = Record & { hour: number; label: string; totalDurationSeconds: number; otherDurationSeconds: number; }; interface TooltipPayloadRow { payload: T; } interface RoundedBarShapeProps { fill?: string; x?: number; y?: number; width?: number; height?: number; payload?: Record; } const OTHER_COLOR = '#d4d7df'; const MUTED_BAR_COLOR = '#cfd3dc'; const WEEK_STACK_KEYS = ['mutedDuration', ...SPOTLIGHT_KEYS, 'selectedRemainder'] as const; const HOUR_STACK_KEYS = [...SPOTLIGHT_KEYS, 'otherDurationSeconds'] as const; const getNumericValue = (value: unknown) => { return typeof value === 'number' && Number.isFinite(value) ? value : 0; }; const getRoundedTopBarPath = (x: number, y: number, width: number, height: number, radius: number) => { const safeRadius = Math.max(0, Math.min(radius, width / 2, height)); return [ `M${x},${y + height}`, `L${x},${y + safeRadius}`, `Q${x},${y} ${x + safeRadius},${y}`, `L${x + width - safeRadius},${y}`, `Q${x + width},${y} ${x + width},${y + safeRadius}`, `L${x + width},${y + height}`, 'Z' ].join(' '); }; const createRoundedTopShape = ( dataKey: string, stackKeys: readonly string[], radius: number ) => { return ({ fill, x, y, width, height, payload }: RoundedBarShapeProps) => { if ( typeof x !== 'number' || typeof y !== 'number' || typeof width !== 'number' || typeof height !== 'number' || width <= 0 || height <= 0 ) { return null; } const currentValue = getNumericValue(payload?.[dataKey]); if (currentValue <= 0) { return null; } const currentIndex = stackKeys.indexOf(dataKey); const isTopMostSegment = currentIndex === -1 || stackKeys .slice(currentIndex + 1) .every((key) => getNumericValue(payload?.[key]) <= 0); if (!isTopMostSegment) { return ; } return ; }; }; const formatAxisDuration = (durationSeconds: number) => { if (durationSeconds <= 0) return '0'; if (durationSeconds >= 3600) { const hours = durationSeconds / 3600; return `${hours >= 10 ? Math.round(hours) : hours.toFixed(hours >= 2 ? 0 : 1)}h`; } return `${Math.round(durationSeconds / 60)}m`; }; const formatSelectedDateHeadline = (dateString: string) => { const [year, month, day] = dateString.split('-').map(Number); const target = new Date(year, month - 1, day); const today = getLocalDateInputValue(); const label = `${target.getMonth() + 1}月${target.getDate()}日`; if (dateString === today) { return `${label} 今天`; } return label; }; const formatUpdateTime = (value: string | null) => { if (!value) return '暂无记录'; const date = new Date(value); return `更新于 ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`; }; const OverviewSkeleton = () => { return (
{[0, 1, 2].map((index) => (
))}
{[56, 88, 62, 76, 50, 68, 18].map((height, index) => (
))}
{Array.from({ length: 18 }).map((_, index) => (
))}
); }; function WeekTooltip({ active, payload }: { active?: boolean; payload?: Array>; }) { if (!active || !payload || payload.length === 0) return null; const datum = payload[0].payload; return (
{datum.date}
{formatDuration(datum.totalDurationSeconds)}
); } function HourTooltip({ active, payload, spotlightNames }: { active?: boolean; payload?: Array>; spotlightNames: string[]; }) { if (!active || !payload || payload.length === 0) return null; const datum = payload[0].payload; const rows: Array<{ label: string; value: number; color: string }> = SPOTLIGHT_KEYS .map((key, index) => ({ label: spotlightNames[index] ?? '', value: datum[key], color: SPOTLIGHT_COLORS[index] })) .filter((row) => row.label && row.value > 0); if (datum.otherDurationSeconds > 0) { rows.push({ label: '其他', value: datum.otherDurationSeconds, color: OTHER_COLOR }); } return (
{datum.label}
总计 {formatDuration(datum.totalDurationSeconds)}
{rows.map((row) => (
{row.label}
{formatDuration(row.value)}
))}
); } export default function ScreenTimeOverviewChart({ data, unit, loading, refreshing, onSelectDate, }: ScreenTimeOverviewChartProps) { const weekChartData = useMemo(() => { if (!data) return [] as WeekChartDatum[]; const spotlightTotal = data.overview.selectedDay.spotlightApps .reduce((sum, app) => sum + app.durationSeconds, 0); return data.overview.week.days.map((day) => { const spotlightDurations = Object.fromEntries( SPOTLIGHT_KEYS.map((key, index) => [ key, day.isSelected ? (data.overview.selectedDay.spotlightApps[index]?.durationSeconds ?? 0) : 0 ]) ) as Record; return { date: day.date, label: day.weekdayLabel, totalDurationSeconds: day.totalDurationSeconds, mutedDuration: day.isSelected ? 0 : day.totalDurationSeconds, selectedRemainder: day.isSelected ? Math.max(day.totalDurationSeconds - spotlightTotal, 0) : 0, ...spotlightDurations, isSelected: day.isSelected }; }); }, [data]); const hourChartData = useMemo(() => { if (!data) return [] as HourChartDatum[]; const spotlightApps = data.overview.selectedDay.spotlightApps; return data.overview.selectedDay.hours.map((hour) => { const spotlightDurations = Object.fromEntries( SPOTLIGHT_KEYS.map((key, index) => [ key, spotlightApps[index] ? hour.spotlightDurations[spotlightApps[index].processId] ?? 0 : 0 ]) ) as Record; return { hour: hour.hour, label: hour.label, totalDurationSeconds: hour.totalDurationSeconds, ...spotlightDurations, otherDurationSeconds: hour.otherDurationSeconds }; }); }, [data]); const spotlightNames = data?.overview.selectedDay.spotlightApps.map((app) => app.processName) ?? []; const maxWeekDuration = useMemo(() => { if (!data) return 0; return Math.max(...data.overview.week.days.map((day) => day.totalDurationSeconds), 0); }, [data]); const primaryLabel = data ? (unit === 'week' ? '本周' : formatSelectedDateHeadline(data.overview.selectedDay.date)) : ''; const selectedDayDuration = data?.overview.selectedDay.totalDurationSeconds ?? 0; if (loading && !data) { return ; } if (!data) return null; return (
{refreshing && (
更新中
)}
{primaryLabel}
{formatDuration(data.totalDurationSeconds)}
{formatUpdateTime(data.lastRecordedAt)}
周平均
{formatDuration(data.overview.week.averageDurationSeconds)}
所选日
{formatDuration(selectedDayDuration)}
应用
{data.apps.length}

周概览

平均 {formatDuration(data.overview.week.averageDurationSeconds)}
} cursor={{ fill: 'rgba(148, 163, 184, 0.08)' }} /> { const day = data.overview.week.days[index]; if (day) { onSelectDate(day.date, unit === 'week' ? 'day' : undefined); } }}> {weekChartData.map((entry) => ( ))} {SPOTLIGHT_KEYS.map((key, index) => ( { const day = data.overview.week.days[clickedIndex]; if (day) { onSelectDate(day.date, unit === 'week' ? 'day' : undefined); } }} > {weekChartData.map((entry) => ( ))} ))} { const day = data.overview.week.days[index]; if (day) { onSelectDate(day.date, unit === 'week' ? 'day' : undefined); } }}> {weekChartData.map((entry) => ( ))}
总计 {formatDuration(data.overview.week.totalDurationSeconds)} 最高 {formatDuration(maxWeekDuration)}

当日分布

{formatDuration(selectedDayDuration)}
} cursor={{ fill: 'rgba(148, 163, 184, 0.08)' }} /> {SPOTLIGHT_KEYS.map((key, index) => ( ))}
{data.overview.selectedDay.spotlightApps.length > 0 ? data.overview.selectedDay.spotlightApps.map((app, index) => ( )) : (
暂无应用数据
)}
); }