feat: 标签调整宽度和图标,top 3 -> top 4

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
feie9454 2026-04-25 19:00:52 +08:00
parent 520cfd96c7
commit fea99d37fe
4 changed files with 73 additions and 78 deletions

View File

@ -24,30 +24,27 @@ interface ScreenTimeOverviewChartProps {
loading: boolean; loading: boolean;
refreshing: boolean; refreshing: boolean;
onSelectDate: (date: string, nextUnit?: 'day' | 'week') => void; onSelectDate: (date: string, nextUnit?: 'day' | 'week') => void;
onSpotlightAppClick: (processId: string) => void;
} }
interface WeekChartDatum { 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<SpotlightKey, number> & {
date: string; date: string;
label: string; label: string;
totalDurationSeconds: number; totalDurationSeconds: number;
mutedDuration: number; mutedDuration: number;
selectedRemainder: number; selectedRemainder: number;
spotlight_0: number;
spotlight_1: number;
spotlight_2: number;
isSelected: boolean; isSelected: boolean;
} };
interface HourChartDatum { type HourChartDatum = Record<SpotlightKey, number> & {
hour: number; hour: number;
label: string; label: string;
totalDurationSeconds: number; totalDurationSeconds: number;
spotlight_0: number;
spotlight_1: number;
spotlight_2: number;
otherDurationSeconds: number; otherDurationSeconds: number;
} };
interface TooltipPayloadRow<T> { interface TooltipPayloadRow<T> {
payload: T; payload: T;
@ -62,11 +59,10 @@ interface RoundedBarShapeProps {
payload?: Record<string, unknown>; payload?: Record<string, unknown>;
} }
const SPOTLIGHT_COLORS = ['#2f7cff', '#17c3d1', '#ff9f43'];
const OTHER_COLOR = '#d4d7df'; const OTHER_COLOR = '#d4d7df';
const MUTED_BAR_COLOR = '#cfd3dc'; const MUTED_BAR_COLOR = '#cfd3dc';
const WEEK_STACK_KEYS = ['mutedDuration', 'spotlight_0', 'spotlight_1', 'spotlight_2', 'selectedRemainder'] as const; const WEEK_STACK_KEYS = ['mutedDuration', ...SPOTLIGHT_KEYS, 'selectedRemainder'] as const;
const HOUR_STACK_KEYS = ['spotlight_0', 'spotlight_1', 'spotlight_2', 'otherDurationSeconds'] as const; const HOUR_STACK_KEYS = [...SPOTLIGHT_KEYS, 'otherDurationSeconds'] as const;
const getNumericValue = (value: unknown) => { const getNumericValue = (value: unknown) => {
return typeof value === 'number' && Number.isFinite(value) ? value : 0; return typeof value === 'number' && Number.isFinite(value) ? value : 0;
@ -218,10 +214,10 @@ function HourTooltip({
if (!active || !payload || payload.length === 0) return null; if (!active || !payload || payload.length === 0) return null;
const datum = payload[0].payload; const datum = payload[0].payload;
const rows = [0, 1, 2] const rows: Array<{ label: string; value: number; color: string }> = SPOTLIGHT_KEYS
.map((index) => ({ .map((key, index) => ({
label: spotlightNames[index], label: spotlightNames[index] ?? '',
value: datum[`spotlight_${index}` as keyof HourChartDatum] as number, value: datum[key],
color: SPOTLIGHT_COLORS[index] color: SPOTLIGHT_COLORS[index]
})) }))
.filter((row) => row.label && row.value > 0); .filter((row) => row.label && row.value > 0);
@ -261,7 +257,6 @@ export default function ScreenTimeOverviewChart({
loading, loading,
refreshing, refreshing,
onSelectDate, onSelectDate,
onSpotlightAppClick
}: ScreenTimeOverviewChartProps) { }: ScreenTimeOverviewChartProps) {
const weekChartData = useMemo(() => { const weekChartData = useMemo(() => {
if (!data) return [] as WeekChartDatum[]; if (!data) return [] as WeekChartDatum[];
@ -269,32 +264,46 @@ export default function ScreenTimeOverviewChart({
const spotlightTotal = data.overview.selectedDay.spotlightApps const spotlightTotal = data.overview.selectedDay.spotlightApps
.reduce((sum, app) => sum + app.durationSeconds, 0); .reduce((sum, app) => sum + app.durationSeconds, 0);
return data.overview.week.days.map((day) => ({ 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<SpotlightKey, number>;
return {
date: day.date, date: day.date,
label: day.weekdayLabel, label: day.weekdayLabel,
totalDurationSeconds: day.totalDurationSeconds, totalDurationSeconds: day.totalDurationSeconds,
mutedDuration: day.isSelected ? 0 : day.totalDurationSeconds, mutedDuration: day.isSelected ? 0 : day.totalDurationSeconds,
selectedRemainder: day.isSelected ? Math.max(day.totalDurationSeconds - spotlightTotal, 0) : 0, selectedRemainder: day.isSelected ? Math.max(day.totalDurationSeconds - spotlightTotal, 0) : 0,
spotlight_0: day.isSelected ? (data.overview.selectedDay.spotlightApps[0]?.durationSeconds ?? 0) : 0, ...spotlightDurations,
spotlight_1: day.isSelected ? (data.overview.selectedDay.spotlightApps[1]?.durationSeconds ?? 0) : 0,
spotlight_2: day.isSelected ? (data.overview.selectedDay.spotlightApps[2]?.durationSeconds ?? 0) : 0,
isSelected: day.isSelected isSelected: day.isSelected
})); };
});
}, [data]); }, [data]);
const hourChartData = useMemo(() => { const hourChartData = useMemo(() => {
if (!data) return [] as HourChartDatum[]; if (!data) return [] as HourChartDatum[];
const spotlightApps = data.overview.selectedDay.spotlightApps; const spotlightApps = data.overview.selectedDay.spotlightApps;
return data.overview.selectedDay.hours.map((hour) => ({ 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<SpotlightKey, number>;
return {
hour: hour.hour, hour: hour.hour,
label: hour.label, label: hour.label,
totalDurationSeconds: hour.totalDurationSeconds, totalDurationSeconds: hour.totalDurationSeconds,
spotlight_0: spotlightApps[0] ? hour.spotlightDurations[spotlightApps[0].processId] ?? 0 : 0, ...spotlightDurations,
spotlight_1: spotlightApps[1] ? hour.spotlightDurations[spotlightApps[1].processId] ?? 0 : 0,
spotlight_2: spotlightApps[2] ? hour.spotlightDurations[spotlightApps[2].processId] ?? 0 : 0,
otherDurationSeconds: hour.otherDurationSeconds otherDurationSeconds: hour.otherDurationSeconds
})); };
});
}, [data]); }, [data]);
const spotlightNames = data?.overview.selectedDay.spotlightApps.map((app) => app.processName) ?? []; const spotlightNames = data?.overview.selectedDay.spotlightApps.map((app) => app.processName) ?? [];
@ -402,12 +411,12 @@ export default function ScreenTimeOverviewChart({
/> />
))} ))}
</Bar> </Bar>
{[0, 1, 2].map((index) => ( {SPOTLIGHT_KEYS.map((key, index) => (
<Bar <Bar
key={`spotlight-${index}`} key={key}
dataKey={`spotlight_${index}`} dataKey={key}
stackId="week" stackId="week"
shape={createRoundedTopShape(`spotlight_${index}`, WEEK_STACK_KEYS, 12)} shape={createRoundedTopShape(key, WEEK_STACK_KEYS, 12)}
onClick={(_, clickedIndex) => { onClick={(_, clickedIndex) => {
const day = data.overview.week.days[clickedIndex]; const day = data.overview.week.days[clickedIndex];
if (day) { if (day) {
@ -417,7 +426,7 @@ export default function ScreenTimeOverviewChart({
> >
{weekChartData.map((entry) => ( {weekChartData.map((entry) => (
<Cell <Cell
key={`spotlight-cell-${index}-${entry.date}`} key={`spotlight-cell-${key}-${entry.date}`}
cursor="pointer" cursor="pointer"
fill={entry.isSelected ? SPOTLIGHT_COLORS[index] : 'transparent'} fill={entry.isSelected ? SPOTLIGHT_COLORS[index] : 'transparent'}
/> />
@ -477,13 +486,13 @@ export default function ScreenTimeOverviewChart({
tickFormatter={formatAxisDuration} tickFormatter={formatAxisDuration}
/> />
<Tooltip content={<HourTooltip spotlightNames={spotlightNames} />} cursor={{ fill: 'rgba(148, 163, 184, 0.08)' }} /> <Tooltip content={<HourTooltip spotlightNames={spotlightNames} />} cursor={{ fill: 'rgba(148, 163, 184, 0.08)' }} />
{[0, 1, 2].map((index) => ( {SPOTLIGHT_KEYS.map((key, index) => (
<Bar <Bar
key={`hour-spotlight-${index}`} key={`hour-${key}`}
dataKey={`spotlight_${index}`} dataKey={key}
stackId="hours" stackId="hours"
fill={SPOTLIGHT_COLORS[index]} fill={SPOTLIGHT_COLORS[index]}
shape={createRoundedTopShape(`spotlight_${index}`, HOUR_STACK_KEYS, 10)} shape={createRoundedTopShape(key, HOUR_STACK_KEYS, 10)}
/> />
))} ))}
<Bar <Bar
@ -496,11 +505,10 @@ export default function ScreenTimeOverviewChart({
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
<div className="mt-3 grid gap-2 sm:grid-cols-3"> <div className="mt-3 grid grid-cols-2 gap-2">
{data.overview.selectedDay.spotlightApps.length > 0 ? data.overview.selectedDay.spotlightApps.map((app, index) => ( {data.overview.selectedDay.spotlightApps.length > 0 ? data.overview.selectedDay.spotlightApps.map((app, index) => (
<button <button
key={app.processId} 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" 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 <span
@ -519,7 +527,7 @@ export default function ScreenTimeOverviewChart({
</span> </span>
</button> </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 className="col-span-2 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>
)} )}

View File

@ -28,7 +28,6 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [expandedApps, setExpandedApps] = useState<Set<string>>(new Set()); const [expandedApps, setExpandedApps] = useState<Set<string>>(new Set());
const [highlightedProcessId, setHighlightedProcessId] = useState<string | null>(null);
const [initialLoading, setInitialLoading] = useState(true); const [initialLoading, setInitialLoading] = useState(true);
const abortControllerRef = React.useRef<AbortController | null>(null); const abortControllerRef = React.useRef<AbortController | null>(null);
@ -125,11 +124,6 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
setSelectedDate(date); setSelectedDate(date);
}; };
const handleSpotlightAppClick = (processId: string) => {
setHighlightedProcessId(processId);
setExpandedApps((previous) => new Set(previous).add(processId));
};
return ( return (
<div className="space-y-4"> <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="rounded-[1.75rem] border border-gray-200 bg-white p-3 shadow-sm dark:border-gray-700 dark:bg-gray-800">
@ -202,7 +196,6 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
loading={loading || initialLoading} loading={loading || initialLoading}
refreshing={isRefreshing} refreshing={isRefreshing}
onSelectDate={handleOverviewDateSelect} onSelectDate={handleOverviewDateSelect}
onSpotlightAppClick={handleSpotlightAppClick}
/> />
{error && ( {error && (
@ -250,16 +243,13 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
{screenTime.apps.map((app, index) => { {screenTime.apps.map((app, index) => {
const isExpanded = expandedApps.has(app.processId); const isExpanded = expandedApps.has(app.processId);
const processWidth = maxDurationSeconds > 0 const processWidth = maxDurationSeconds > 0
? Math.max((app.durationSeconds / maxDurationSeconds) * 100, 4) ? Math.max((app.durationSeconds / maxDurationSeconds) * 100, 2)
: 0; : 0;
return ( return (
<div <div
key={app.processId} key={app.processId}
className={`px-4 py-3 transition-colors ${highlightedProcessId === app.processId className={`px-4 py-3 transition-colors`}
? 'bg-blue-50/70 dark:bg-blue-500/8'
: ''
}`}
> >
<button <button
onClick={() => toggleExpanded(app.processId)} onClick={() => toggleExpanded(app.processId)}
@ -280,11 +270,6 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
<div className="truncate text-base font-semibold text-gray-900 dark:text-white" title={app.processName}> <div className="truncate text-base font-semibold text-gray-900 dark:text-white" title={app.processName}>
{app.processName} {app.processName}
</div> </div>
{highlightedProcessId === app.processId && (
<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="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"> <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} {app.titleCount}
</span> </span>
@ -335,7 +320,7 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
<div className="mt-1.5 h-1.5 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 <div
className="h-full rounded-full bg-emerald-500 dark:bg-emerald-400" className="h-full rounded-full bg-emerald-500 dark:bg-emerald-400"
style={{ width: `${Math.max(title.percentage, 4)}%` }} style={{ width: `${Math.max(title.percentage, 2)}%` }}
/> />
</div> </div>
</div> </div>

View File

@ -2,7 +2,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { ArrowLeft, BarChart3, Star } from 'lucide-react'; import { ArrowLeft, BarChart3, Star, KeySquare, ChartNoAxesGantt } from 'lucide-react';
import ScreenshotsTab from './components/ScreenshotsTab'; import ScreenshotsTab from './components/ScreenshotsTab';
import ScreenTimeTab from './components/ScreenTimeTab'; import ScreenTimeTab from './components/ScreenTimeTab';
import StarredTab from './components/StarredTab'; import StarredTab from './components/StarredTab';
@ -59,15 +59,15 @@ export default function HostDetail() {
{/* 选项卡导航 */} {/* 选项卡导航 */}
<div className="mb-6 border-b border-gray-200 dark:border-gray-700"> <div className="mb-6 border-b border-gray-200 dark:border-gray-700">
<nav className="-mb-px flex space-x-8"> <nav className="-mb-px flex space-x-6">
<button <button
onClick={() => setActiveTab('screenshots')} onClick={() => setActiveTab('screenshots')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${activeTab === 'screenshots' className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center ${activeTab === 'screenshots'
? 'border-blue-600 text-blue-600' ? 'border-blue-600 text-blue-600'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600' : 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'
}`} }`}
> ><ChartNoAxesGantt className="h-4 w-4 mr-1"/>
线 线
</button> </button>
<button <button
onClick={() => setActiveTab('screen-time')} onClick={() => setActiveTab('screen-time')}
@ -87,16 +87,17 @@ export default function HostDetail() {
}`} }`}
> >
<Star className="h-4 w-4 mr-1" /> <Star className="h-4 w-4 mr-1" />
</button> </button>
<button <button
onClick={() => setActiveTab('credentials')} onClick={() => setActiveTab('credentials')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${activeTab === 'credentials' className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center ${activeTab === 'credentials'
? 'border-blue-600 text-blue-600' ? 'border-blue-600 text-blue-600'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600' : 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'
}`} }`}
> >
<KeySquare className="h-4 w-4 mr-1" />
</button> </button>
</nav> </nav>
</div> </div>

View File

@ -8,6 +8,7 @@ import { Prisma } from '@prisma/client'
const MAX_GAP_MS = 5 * 60 * 1000 const MAX_GAP_MS = 5 * 60 * 1000
const DEFAULT_SAMPLE_MS = 30 * 1000 const DEFAULT_SAMPLE_MS = 30 * 1000
const MIN_SAMPLE_MS = 5 * 1000 const MIN_SAMPLE_MS = 5 * 1000
const SPOTLIGHT_APP_LIMIT = 4
const LABEL_TOKEN_THRESHOLD_RATIO = 0.5 const LABEL_TOKEN_THRESHOLD_RATIO = 0.5
const HOUR_MS = 60 * 60 * 1000 const HOUR_MS = 60 * 60 * 1000
const DAY_MS = 24 * HOUR_MS const DAY_MS = 24 * HOUR_MS
@ -458,7 +459,7 @@ async function handleScreenTime(req: NextRequest) {
const aggregateSummary = buildAppList(aggregateUsageMap) const aggregateSummary = buildAppList(aggregateUsageMap)
const selectedDaySummary = buildAppList(selectedDayUsageMap) const selectedDaySummary = buildAppList(selectedDayUsageMap)
const spotlightApps = selectedDaySummary.apps.slice(0, 3).map(app => ({ const spotlightApps = selectedDaySummary.apps.slice(0, SPOTLIGHT_APP_LIMIT).map(app => ({
processId: app.processId, processId: app.processId,
processName: app.processName, processName: app.processName,
durationSeconds: app.durationSeconds, durationSeconds: app.durationSeconds,