@@ -13,11 +13,7 @@ import {
|
||||
YAxis
|
||||
} from 'recharts';
|
||||
import {
|
||||
CalendarDays,
|
||||
Clock3,
|
||||
Loader2,
|
||||
Sparkles,
|
||||
WandSparkles
|
||||
} from 'lucide-react';
|
||||
import { ScreenTimeResponse } from '../types';
|
||||
import { formatDuration, getLocalDateInputValue } from '../utils';
|
||||
@@ -157,19 +153,25 @@ const formatUpdateTime = (value: string | null) => {
|
||||
|
||||
const OverviewSkeleton = () => {
|
||||
return (
|
||||
<div className="rounded-[2rem] border border-gray-200 bg-white p-6 shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
||||
<div className="screen-time-skeleton h-6 w-36 rounded-full" />
|
||||
<div className="mt-4 screen-time-skeleton h-14 w-56 rounded-2xl" />
|
||||
<div className="mt-8 rounded-[1.75rem] bg-gray-50 p-5 dark:bg-gray-900/60">
|
||||
<div className="flex h-40 items-end gap-3">
|
||||
{[56, 88, 62, 76, 50, 68, 18].map((height, index) => (
|
||||
<div key={index} className="flex flex-1 flex-col items-center gap-3">
|
||||
<div className="screen-time-skeleton w-full rounded-t-2xl" style={{ height }} />
|
||||
<div className="screen-time-skeleton h-4 w-4 rounded-full" />
|
||||
</div>
|
||||
<div className="rounded-[1.75rem] border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<div className="screen-time-skeleton h-5 w-24 rounded-full" />
|
||||
<div className="mt-2 screen-time-skeleton h-10 w-44 rounded-xl" />
|
||||
</div>
|
||||
<div className="grid w-full grid-cols-3 gap-2 sm:w-64">
|
||||
{[0, 1, 2].map((index) => (
|
||||
<div key={index} className="screen-time-skeleton h-12 rounded-2xl" />
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-8 flex h-32 items-end gap-2">
|
||||
</div>
|
||||
<div className="mt-4 grid gap-5 xl:grid-cols-[minmax(0,0.9fr)_minmax(0,1.35fr)]">
|
||||
<div className="flex h-36 items-end gap-2">
|
||||
{[56, 88, 62, 76, 50, 68, 18].map((height, index) => (
|
||||
<div key={index} className="screen-time-skeleton flex-1 rounded-t-2xl" style={{ height }} />
|
||||
))}
|
||||
</div>
|
||||
<div className="flex h-36 items-end gap-1.5">
|
||||
{Array.from({ length: 18 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
@@ -200,9 +202,6 @@ function WeekTooltip({
|
||||
<div className="mt-1 text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{formatDuration(datum.totalDurationSeconds)}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{datum.isSelected ? '当前选中日,点击其他日期可跳转' : '点击查看这一天'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -303,6 +302,8 @@ export default function ScreenTimeOverviewChart({
|
||||
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 <OverviewSkeleton />;
|
||||
@@ -311,225 +312,219 @@ export default function ScreenTimeOverviewChart({
|
||||
if (!data) return null;
|
||||
|
||||
return (
|
||||
<div className="relative overflow-hidden rounded-[2rem] border border-gray-200 bg-[radial-gradient(circle_at_top_left,rgba(59,130,246,0.10),transparent_28%),linear-gradient(180deg,#ffffff_0%,#f8fafc_100%)] p-6 shadow-sm dark:border-gray-700 dark:bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.14),transparent_28%),linear-gradient(180deg,#111827_0%,#0f172a_100%)]">
|
||||
<div className="relative overflow-hidden rounded-[1.75rem] border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
||||
{refreshing && (
|
||||
<div className="absolute inset-0 z-20 flex items-start justify-end bg-white/35 p-4 backdrop-blur-[2px] dark:bg-gray-950/25">
|
||||
<div className="inline-flex items-center gap-2 rounded-full bg-white/90 px-3 py-1.5 text-xs font-medium text-gray-700 shadow-lg dark:bg-gray-900/90 dark:text-gray-200">
|
||||
<div className="absolute inset-0 z-20 flex items-start justify-end bg-white/40 p-3 backdrop-blur-[2px] dark:bg-gray-950/25">
|
||||
<div className="inline-flex items-center gap-2 rounded-full bg-white/95 px-3 py-1 text-xs font-medium text-gray-700 shadow-lg dark:bg-gray-900/95 dark:text-gray-200">
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
正在刷新统计
|
||||
更新中
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-slate-500 dark:text-slate-300">
|
||||
<CalendarDays className="h-4 w-4" />
|
||||
{formatSelectedDateHeadline(data.overview.selectedDay.date)}
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
{primaryLabel}
|
||||
</div>
|
||||
<div className="mt-2 text-4xl font-semibold tracking-tight text-gray-950 dark:text-white sm:text-5xl">
|
||||
<div className="mt-1 text-4xl font-semibold text-gray-950 dark:text-white sm:text-5xl">
|
||||
{formatDuration(data.totalDurationSeconds)}
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
{unit === 'week' ? '本周总屏幕时间' : '当天总屏幕时间'}
|
||||
<span className="mx-2 text-gray-300 dark:text-gray-600">•</span>
|
||||
选中日 {formatDuration(data.overview.selectedDay.totalDurationSeconds)}
|
||||
<div className="mt-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
{formatUpdateTime(data.lastRecordedAt)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-3 lg:min-w-[22rem]">
|
||||
<div className="rounded-2xl bg-white/80 px-4 py-3 shadow-sm ring-1 ring-black/5 backdrop-blur dark:bg-gray-900/70 dark:ring-white/10">
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">周平均</div>
|
||||
<div className="mt-1 text-lg font-semibold text-gray-900 dark:text-white">
|
||||
<div className="grid grid-cols-3 gap-2 sm:min-w-[22rem]">
|
||||
<div className="rounded-2xl bg-gray-50 px-3 py-2 dark:bg-gray-900/70">
|
||||
<div className="text-[11px] text-gray-500 dark:text-gray-400">周平均</div>
|
||||
<div className="mt-0.5 truncate text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{formatDuration(data.overview.week.averageDurationSeconds)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-2xl bg-white/80 px-4 py-3 shadow-sm ring-1 ring-black/5 backdrop-blur dark:bg-gray-900/70 dark:ring-white/10">
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">活跃应用</div>
|
||||
<div className="mt-1 text-lg font-semibold text-gray-900 dark:text-white">
|
||||
<div className="rounded-2xl bg-gray-50 px-3 py-2 dark:bg-gray-900/70">
|
||||
<div className="text-[11px] text-gray-500 dark:text-gray-400">所选日</div>
|
||||
<div className="mt-0.5 truncate text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{formatDuration(selectedDayDuration)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-2xl bg-gray-50 px-3 py-2 dark:bg-gray-900/70">
|
||||
<div className="text-[11px] text-gray-500 dark:text-gray-400">应用</div>
|
||||
<div className="mt-0.5 text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{data.apps.length}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-2xl bg-white/80 px-4 py-3 shadow-sm ring-1 ring-black/5 backdrop-blur dark:bg-gray-900/70 dark:ring-white/10">
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">采样节奏</div>
|
||||
<div className="mt-1 text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{formatDuration(data.estimatedSampleSeconds)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 rounded-[1.75rem] border border-white/70 bg-white/70 p-5 shadow-inner shadow-white/40 backdrop-blur dark:border-gray-700/70 dark:bg-gray-900/60 dark:shadow-black/20">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-700 dark:text-gray-200">周概览</div>
|
||||
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
点击任意一天可以直接跳到该日期
|
||||
<div className="mt-4 grid gap-5 xl:grid-cols-[minmax(0,0.88fr)_minmax(0,1.35fr)]">
|
||||
<section className="min-w-0">
|
||||
<div className="flex items-baseline justify-between gap-3">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white">周概览</h3>
|
||||
<div className="truncate text-xs text-gray-500 dark:text-gray-400">
|
||||
平均 {formatDuration(data.overview.week.averageDurationSeconds)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="inline-flex items-center gap-2 rounded-full bg-blue-50 px-3 py-1 text-xs font-medium text-blue-700 dark:bg-blue-500/10 dark:text-blue-300">
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
选中日已高亮
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 h-48">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={weekChartData} barCategoryGap="20%">
|
||||
<CartesianGrid vertical={false} strokeDasharray="4 4" stroke="#d9dde5" />
|
||||
<XAxis axisLine={false} tickLine={false} dataKey="label" tick={{ fill: '#94a3b8', fontSize: 12 }} />
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
orientation="right"
|
||||
tick={{ fill: '#b0b7c3', fontSize: 12 }}
|
||||
tickFormatter={formatAxisDuration}
|
||||
/>
|
||||
<ReferenceLine y={data.overview.week.averageDurationSeconds} stroke="#94a3b8" strokeDasharray="4 6" />
|
||||
<Tooltip content={<WeekTooltip />} cursor={{ fill: 'rgba(148, 163, 184, 0.08)' }} />
|
||||
<div className="mt-2 h-36 sm:h-40">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={weekChartData} barCategoryGap="26%" margin={{ top: 8, right: 0, left: 0, bottom: 0 }}>
|
||||
<CartesianGrid vertical={false} strokeDasharray="3 5" stroke="#e0e4eb" />
|
||||
<XAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
dataKey="label"
|
||||
height={22}
|
||||
tick={{ fill: '#9ca3af', fontSize: 12 }}
|
||||
/>
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
orientation="right"
|
||||
width={34}
|
||||
tick={{ fill: '#b0b7c3', fontSize: 12 }}
|
||||
tickFormatter={formatAxisDuration}
|
||||
/>
|
||||
<ReferenceLine y={data.overview.week.averageDurationSeconds} stroke="#9ca3af" strokeDasharray="4 6" />
|
||||
<Tooltip content={<WeekTooltip />} cursor={{ fill: 'rgba(148, 163, 184, 0.08)' }} />
|
||||
|
||||
<Bar dataKey="mutedDuration" stackId="week" shape={createRoundedTopShape('mutedDuration', WEEK_STACK_KEYS, 12)} onClick={(_, index) => {
|
||||
const day = data.overview.week.days[index];
|
||||
if (day) {
|
||||
onSelectDate(day.date, unit === 'week' ? 'day' : undefined);
|
||||
}
|
||||
}}>
|
||||
{weekChartData.map((entry) => (
|
||||
<Cell
|
||||
key={`muted-${entry.date}`}
|
||||
cursor="pointer"
|
||||
fill={entry.isSelected ? 'transparent' : MUTED_BAR_COLOR}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
{[0, 1, 2].map((index) => (
|
||||
<Bar
|
||||
key={`spotlight-${index}`}
|
||||
dataKey={`spotlight_${index}`}
|
||||
stackId="week"
|
||||
shape={createRoundedTopShape(`spotlight_${index}`, WEEK_STACK_KEYS, 12)}
|
||||
onClick={(_, clickedIndex) => {
|
||||
const day = data.overview.week.days[clickedIndex];
|
||||
if (day) {
|
||||
onSelectDate(day.date, unit === 'week' ? 'day' : undefined);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Bar dataKey="mutedDuration" stackId="week" shape={createRoundedTopShape('mutedDuration', WEEK_STACK_KEYS, 12)} onClick={(_, index) => {
|
||||
const day = data.overview.week.days[index];
|
||||
if (day) {
|
||||
onSelectDate(day.date, unit === 'week' ? 'day' : undefined);
|
||||
}
|
||||
}}>
|
||||
{weekChartData.map((entry) => (
|
||||
<Cell
|
||||
key={`spotlight-cell-${index}-${entry.date}`}
|
||||
key={`muted-${entry.date}`}
|
||||
cursor="pointer"
|
||||
fill={entry.isSelected ? SPOTLIGHT_COLORS[index] : 'transparent'}
|
||||
fill={entry.isSelected ? 'transparent' : MUTED_BAR_COLOR}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
))}
|
||||
<Bar dataKey="selectedRemainder" stackId="week" shape={createRoundedTopShape('selectedRemainder', WEEK_STACK_KEYS, 12)} onClick={(_, index) => {
|
||||
const day = data.overview.week.days[index];
|
||||
if (day) {
|
||||
onSelectDate(day.date, unit === 'week' ? 'day' : undefined);
|
||||
}
|
||||
}}>
|
||||
{weekChartData.map((entry) => (
|
||||
<Cell
|
||||
key={`remainder-${entry.date}`}
|
||||
cursor="pointer"
|
||||
fill={entry.isSelected ? OTHER_COLOR : 'transparent'}
|
||||
{[0, 1, 2].map((index) => (
|
||||
<Bar
|
||||
key={`spotlight-${index}`}
|
||||
dataKey={`spotlight_${index}`}
|
||||
stackId="week"
|
||||
shape={createRoundedTopShape(`spotlight_${index}`, WEEK_STACK_KEYS, 12)}
|
||||
onClick={(_, clickedIndex) => {
|
||||
const day = data.overview.week.days[clickedIndex];
|
||||
if (day) {
|
||||
onSelectDate(day.date, unit === 'week' ? 'day' : undefined);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{weekChartData.map((entry) => (
|
||||
<Cell
|
||||
key={`spotlight-cell-${index}-${entry.date}`}
|
||||
cursor="pointer"
|
||||
fill={entry.isSelected ? SPOTLIGHT_COLORS[index] : 'transparent'}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
))}
|
||||
<Bar dataKey="selectedRemainder" stackId="week" shape={createRoundedTopShape('selectedRemainder', WEEK_STACK_KEYS, 12)} onClick={(_, index) => {
|
||||
const day = data.overview.week.days[index];
|
||||
if (day) {
|
||||
onSelectDate(day.date, unit === 'week' ? 'day' : undefined);
|
||||
}
|
||||
}}>
|
||||
{weekChartData.map((entry) => (
|
||||
<Cell
|
||||
key={`remainder-${entry.date}`}
|
||||
cursor="pointer"
|
||||
fill={entry.isSelected ? OTHER_COLOR : 'transparent'}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
<div className="mt-1 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
|
||||
<span>总计 {formatDuration(data.overview.week.totalDurationSeconds)}</span>
|
||||
<span>最高 {formatDuration(maxWeekDuration)}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="min-w-0">
|
||||
<div className="flex items-baseline justify-between gap-3">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white">当日分布</h3>
|
||||
<div className="truncate text-xs text-gray-500 dark:text-gray-400">
|
||||
{formatDuration(selectedDayDuration)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 h-36 sm:h-40">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={hourChartData} barCategoryGap={2} margin={{ top: 8, right: 0, left: 0, bottom: 0 }}>
|
||||
<CartesianGrid vertical={false} strokeDasharray="3 5" stroke="#e0e4eb" />
|
||||
<XAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
dataKey="label"
|
||||
height={22}
|
||||
interval={5}
|
||||
tick={{ fill: '#9ca3af', fontSize: 12 }}
|
||||
/>
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
orientation="right"
|
||||
width={38}
|
||||
tick={{ fill: '#b0b7c3', fontSize: 12 }}
|
||||
tickFormatter={formatAxisDuration}
|
||||
/>
|
||||
<Tooltip content={<HourTooltip spotlightNames={spotlightNames} />} cursor={{ fill: 'rgba(148, 163, 184, 0.08)' }} />
|
||||
{[0, 1, 2].map((index) => (
|
||||
<Bar
|
||||
key={`hour-spotlight-${index}`}
|
||||
dataKey={`spotlight_${index}`}
|
||||
stackId="hours"
|
||||
fill={SPOTLIGHT_COLORS[index]}
|
||||
shape={createRoundedTopShape(`spotlight_${index}`, HOUR_STACK_KEYS, 10)}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
|
||||
<span>周总计 {formatDuration(data.overview.week.totalDurationSeconds)}</span>
|
||||
<span>最高 {formatDuration(maxWeekDuration)}</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-7 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-700 dark:text-gray-200">当日分布</div>
|
||||
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
24 小时内按主应用堆叠,帮你快速定位高强度时段
|
||||
</div>
|
||||
</div>
|
||||
<div className="inline-flex items-center gap-2 rounded-full bg-gray-100 px-3 py-1 text-xs text-gray-600 dark:bg-gray-800 dark:text-gray-300">
|
||||
<Clock3 className="h-3.5 w-3.5" />
|
||||
{formatDuration(data.overview.selectedDay.totalDurationSeconds)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 h-52">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={hourChartData} barCategoryGap={2}>
|
||||
<CartesianGrid vertical={false} strokeDasharray="4 4" stroke="#d9dde5" />
|
||||
<XAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
dataKey="label"
|
||||
interval={2}
|
||||
tick={{ fill: '#94a3b8', fontSize: 12 }}
|
||||
/>
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
orientation="right"
|
||||
tick={{ fill: '#b0b7c3', fontSize: 12 }}
|
||||
tickFormatter={formatAxisDuration}
|
||||
/>
|
||||
<Tooltip content={<HourTooltip spotlightNames={spotlightNames} />} cursor={{ fill: 'rgba(148, 163, 184, 0.08)' }} />
|
||||
{[0, 1, 2].map((index) => (
|
||||
<Bar
|
||||
key={`hour-spotlight-${index}`}
|
||||
dataKey={`spotlight_${index}`}
|
||||
dataKey="otherDurationSeconds"
|
||||
stackId="hours"
|
||||
fill={SPOTLIGHT_COLORS[index]}
|
||||
shape={createRoundedTopShape(`spotlight_${index}`, HOUR_STACK_KEYS, 10)}
|
||||
fill={OTHER_COLOR}
|
||||
shape={createRoundedTopShape('otherDurationSeconds', HOUR_STACK_KEYS, 10)}
|
||||
/>
|
||||
))}
|
||||
<Bar
|
||||
dataKey="otherDurationSeconds"
|
||||
stackId="hours"
|
||||
fill={OTHER_COLOR}
|
||||
shape={createRoundedTopShape('otherDurationSeconds', HOUR_STACK_KEYS, 10)}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex flex-wrap gap-3">
|
||||
{data.overview.selectedDay.spotlightApps.length > 0 ? data.overview.selectedDay.spotlightApps.map((app, index) => (
|
||||
<button
|
||||
key={app.processId}
|
||||
onClick={() => onSpotlightAppClick(app.processId)}
|
||||
className="flex min-w-36 flex-1 items-start gap-3 rounded-2xl bg-white/80 px-4 py-3 text-left shadow-sm ring-1 ring-black/5 transition-transform hover:-translate-y-0.5 dark:bg-gray-900/70 dark:ring-white/10"
|
||||
>
|
||||
<span className="mt-1 h-3 w-3 rounded-full" style={{ backgroundColor: SPOTLIGHT_COLORS[index] }} />
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="flex items-center gap-2 text-xs uppercase tracking-[0.18em] text-gray-400 dark:text-gray-500">
|
||||
<WandSparkles className="h-3.5 w-3.5" />
|
||||
Focus {index + 1}
|
||||
<div className="mt-3 grid gap-2 sm:grid-cols-3">
|
||||
{data.overview.selectedDay.spotlightApps.length > 0 ? data.overview.selectedDay.spotlightApps.map((app, index) => (
|
||||
<button
|
||||
key={app.processId}
|
||||
onClick={() => onSpotlightAppClick(app.processId)}
|
||||
className="flex min-w-0 items-center gap-2 rounded-2xl bg-gray-50 px-3 py-2 text-left transition-colors hover:bg-gray-100 dark:bg-gray-900/70 dark:hover:bg-gray-900"
|
||||
>
|
||||
<span
|
||||
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[11px] font-semibold text-white"
|
||||
style={{ backgroundColor: SPOTLIGHT_COLORS[index] }}
|
||||
>
|
||||
{index + 1}
|
||||
</span>
|
||||
<span className="mt-1 block truncate text-sm font-semibold text-gray-900 dark:text-white" title={app.processName}>
|
||||
{app.processName}
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate text-sm font-medium text-gray-900 dark:text-white" title={app.processName}>
|
||||
{app.processName}
|
||||
</span>
|
||||
<span className="block truncate text-xs text-gray-500 dark:text-gray-400">
|
||||
{formatDuration(app.durationSeconds)} · {app.percentage}%
|
||||
</span>
|
||||
</span>
|
||||
<span className="mt-1 block text-xs text-gray-500 dark:text-gray-400">
|
||||
{formatDuration(app.durationSeconds)} · {app.percentage}%
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
)) : (
|
||||
<div className="rounded-2xl bg-gray-50 px-4 py-3 text-sm text-gray-500 dark:bg-gray-900/60 dark:text-gray-400">
|
||||
当前日期没有足够的活跃应用数据来展示聚焦图例。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
|
||||
<span>{formatUpdateTime(data.lastRecordedAt)}</span>
|
||||
<span>周柱状图可点击跳转,Focus 卡片可直接展开对应应用</span>
|
||||
</button>
|
||||
)) : (
|
||||
<div className="rounded-2xl bg-gray-50 px-3 py-2 text-sm text-gray-500 dark:bg-gray-900/70 dark:text-gray-400">
|
||||
暂无应用数据
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -131,23 +131,23 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-[2rem] border border-gray-200 bg-[linear-gradient(180deg,#ffffff_0%,#f8fafc_100%)] p-5 shadow-sm dark:border-gray-700 dark:bg-[linear-gradient(180deg,#111827_0%,#0f172a_100%)]">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-blue-600 dark:text-blue-400">
|
||||
Screen Time
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-[1.75rem] border border-gray-200 bg-white p-3 shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||
屏幕使用时间
|
||||
</div>
|
||||
<h2 className="mt-2 text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
<h2 className="mt-0.5 truncate text-xl font-semibold text-gray-900 dark:text-white">
|
||||
{periodLabel}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||
<div className="inline-flex rounded-full bg-gray-100 p-1 dark:bg-gray-700/70">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
<div className="inline-flex rounded-full bg-gray-100 p-0.5 dark:bg-gray-700/70">
|
||||
<button
|
||||
onClick={() => setUnit('day')}
|
||||
className={`rounded-full px-5 py-2 text-sm font-medium ${unit === 'day'
|
||||
className={`rounded-full px-4 py-1.5 text-sm font-medium ${unit === 'day'
|
||||
? 'bg-white text-blue-600 shadow-sm dark:bg-gray-900 dark:text-blue-400'
|
||||
: 'text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white'
|
||||
}`}
|
||||
@@ -156,7 +156,7 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setUnit('week')}
|
||||
className={`rounded-full px-5 py-2 text-sm font-medium ${unit === 'week'
|
||||
className={`rounded-full px-4 py-1.5 text-sm font-medium ${unit === 'week'
|
||||
? 'bg-white text-blue-600 shadow-sm dark:bg-gray-900 dark:text-blue-400'
|
||||
: 'text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white'
|
||||
}`}
|
||||
@@ -168,13 +168,13 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => shiftPeriod(-1)}
|
||||
className="rounded-lg border border-gray-200 p-2 text-gray-600 hover:border-blue-500 hover:text-blue-600 dark:border-gray-600 dark:text-gray-300 dark:hover:border-blue-400 dark:hover:text-blue-400"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-full border border-gray-200 text-gray-600 hover:border-blue-500 hover:text-blue-600 dark:border-gray-600 dark:text-gray-300 dark:hover:border-blue-400 dark:hover:text-blue-400"
|
||||
title={unit === 'week' ? '上一周' : '上一天'}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<label className="flex items-center gap-2 rounded-xl border border-gray-200 px-3 py-2 text-sm text-gray-700 dark:border-gray-600 dark:text-gray-200">
|
||||
<label className="flex h-9 items-center gap-2 rounded-full border border-gray-200 px-3 text-sm text-gray-700 dark:border-gray-600 dark:text-gray-200">
|
||||
<Calendar className="h-4 w-4 text-gray-400" />
|
||||
<input
|
||||
type="date"
|
||||
@@ -186,7 +186,7 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
|
||||
|
||||
<button
|
||||
onClick={() => shiftPeriod(1)}
|
||||
className="rounded-lg border border-gray-200 p-2 text-gray-600 hover:border-blue-500 hover:text-blue-600 dark:border-gray-600 dark:text-gray-300 dark:hover:border-blue-400 dark:hover:text-blue-400"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-full border border-gray-200 text-gray-600 hover:border-blue-500 hover:text-blue-600 dark:border-gray-600 dark:text-gray-300 dark:hover:border-blue-400 dark:hover:text-blue-400"
|
||||
title={unit === 'week' ? '下一周' : '下一天'}
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
@@ -211,14 +211,16 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative overflow-hidden rounded-[2rem] border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
||||
<div className="border-b border-gray-200 px-5 py-4 dark:border-gray-700">
|
||||
<div className="relative overflow-hidden rounded-[1.75rem] border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
||||
<div className="border-b border-gray-200 px-4 py-3 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">应用使用排行</h3>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
进程默认折叠,展开后可查看时长最长的前 5 个窗口标题。
|
||||
</p>
|
||||
<h3 className="text-base font-semibold text-gray-900 dark:text-white">最常使用</h3>
|
||||
{screenTime && (
|
||||
<div className="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{screenTime.apps.length} 个应用 · {formatDuration(screenTime.totalDurationSeconds)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isRefreshing && (
|
||||
@@ -231,14 +233,14 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
|
||||
</div>
|
||||
|
||||
{loading && !screenTime && (
|
||||
<div className="flex items-center justify-center gap-3 px-6 py-12 text-sm text-gray-500 dark:text-gray-400">
|
||||
<div className="flex items-center justify-center gap-3 px-6 py-8 text-sm text-gray-500 dark:text-gray-400">
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
正在加载屏幕使用时间数据...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && screenTime && screenTime.apps.length === 0 && !error && (
|
||||
<div className="px-6 py-12 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
<div className="px-6 py-8 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
当前时间段内没有可统计的活跃窗口记录。
|
||||
</div>
|
||||
)}
|
||||
@@ -254,7 +256,7 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
|
||||
return (
|
||||
<div
|
||||
key={app.processId}
|
||||
className={`px-5 py-4 transition-colors ${highlightedProcessId === app.processId
|
||||
className={`px-4 py-3 transition-colors ${highlightedProcessId === app.processId
|
||||
? 'bg-blue-50/70 dark:bg-blue-500/8'
|
||||
: ''
|
||||
}`}
|
||||
@@ -263,41 +265,40 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
|
||||
onClick={() => toggleExpanded(app.processId)}
|
||||
className="w-full text-left"
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="mt-1 text-sm font-semibold text-gray-400 dark:text-gray-500">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-2xl bg-gray-100 text-xs font-semibold text-gray-500 dark:bg-gray-700 dark:text-gray-300">
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-4 w-4 text-gray-400" />
|
||||
<ChevronDown className="h-3.5 w-3.5 shrink-0 text-gray-400" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4 text-gray-400" />
|
||||
<ChevronRight className="h-3.5 w-3.5 shrink-0 text-gray-400" />
|
||||
)}
|
||||
<div className="truncate text-base font-semibold text-gray-900 dark:text-white" title={app.processName}>
|
||||
{app.processName}
|
||||
</div>
|
||||
{highlightedProcessId === app.processId && (
|
||||
<span className="rounded-full bg-blue-100 px-2 py-0.5 text-[11px] font-medium text-blue-700 dark:bg-blue-500/15 dark:text-blue-300">
|
||||
Focus
|
||||
<span className="shrink-0 rounded-full bg-blue-100 px-2 py-0.5 text-[11px] font-medium text-blue-700 dark:bg-blue-500/15 dark:text-blue-300">
|
||||
聚焦
|
||||
</span>
|
||||
)}
|
||||
<span className="rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-600 dark:bg-gray-700 dark:text-gray-300">
|
||||
<span className="shrink-0 rounded-full bg-gray-100 px-2 py-0.5 text-[11px] text-gray-600 dark:bg-gray-700 dark:text-gray-300">
|
||||
{app.titleCount} 个标题
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex items-center gap-3 text-sm text-gray-500 dark:text-gray-400">
|
||||
<span>{formatDuration(app.durationSeconds)}</span>
|
||||
<div className="mt-1 flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span>{app.percentage}%</span>
|
||||
<span className="h-1 w-1 rounded-full bg-gray-300 dark:bg-gray-600" />
|
||||
<span className="truncate" title={app.processPath || '路径未知'}>
|
||||
{app.processPath || '路径未知'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 truncate text-xs text-gray-500 dark:text-gray-400" title={app.processPath || '路径未知'}>
|
||||
路径: {app.processPath || '路径未知'}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 h-2 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-700">
|
||||
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-700">
|
||||
<div
|
||||
className="h-full rounded-full bg-blue-600 dark:bg-blue-500"
|
||||
style={{ width: `${processWidth}%` }}
|
||||
@@ -312,17 +313,17 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="ml-10 mt-4 rounded-2xl bg-gray-50 p-4 dark:bg-gray-900/50">
|
||||
<div className="space-y-3">
|
||||
<div className="ml-12 mt-3 rounded-2xl bg-gray-50 px-3 py-2.5 dark:bg-gray-900/50">
|
||||
<div className="space-y-2.5">
|
||||
{app.titles.map((title) => (
|
||||
<div key={`${app.processId}-${title.title}`}>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-medium text-gray-800 dark:text-gray-100" title={title.title}>
|
||||
{title.title}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
占该进程 {title.percentage}%
|
||||
<div className="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{title.percentage}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -331,7 +332,7 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 h-2 overflow-hidden rounded-full bg-white dark:bg-gray-800">
|
||||
<div className="mt-1.5 h-1.5 overflow-hidden rounded-full bg-white dark:bg-gray-800">
|
||||
<div
|
||||
className="h-full rounded-full bg-emerald-500 dark:bg-emerald-400"
|
||||
style={{ width: `${Math.max(title.percentage, 4)}%` }}
|
||||
@@ -342,8 +343,8 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
|
||||
</div>
|
||||
|
||||
{app.titleCount > app.titles.length && (
|
||||
<div className="mt-4 text-xs text-gray-500 dark:text-gray-400">
|
||||
其余 {app.titleCount - app.titles.length} 个标题未展示。
|
||||
<div className="mt-3 text-xs text-gray-500 dark:text-gray-400">
|
||||
还有 {app.titleCount - app.titles.length} 个标题
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -633,7 +633,7 @@ export default function ScreenshotsTab({
|
||||
{
|
||||
!(navigator as any).connection?.saveData &&
|
||||
records.map(record => record.screenshots).flat()
|
||||
.filter((_, index) => Math.abs(index - currentFrameIndex) <= 20)
|
||||
.filter((_, index) => Math.abs(index - currentFrameIndex) <= 5)
|
||||
.map(screenshot => <link rel="preload" key={screenshot.fileId} href={`/screenshots/${screenshot.fileId}`} as="image" />)
|
||||
}
|
||||
{/* 图片预览区域 */}
|
||||
|
||||
Reference in New Issue
Block a user