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

View File

@ -28,7 +28,6 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [expandedApps, setExpandedApps] = useState<Set<string>>(new Set());
const [highlightedProcessId, setHighlightedProcessId] = useState<string | null>(null);
const [initialLoading, setInitialLoading] = useState(true);
const abortControllerRef = React.useRef<AbortController | null>(null);
@ -125,11 +124,6 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
setSelectedDate(date);
};
const handleSpotlightAppClick = (processId: string) => {
setHighlightedProcessId(processId);
setExpandedApps((previous) => new Set(previous).add(processId));
};
return (
<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">
@ -202,7 +196,6 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
loading={loading || initialLoading}
refreshing={isRefreshing}
onSelectDate={handleOverviewDateSelect}
onSpotlightAppClick={handleSpotlightAppClick}
/>
{error && (
@ -250,16 +243,13 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
{screenTime.apps.map((app, index) => {
const isExpanded = expandedApps.has(app.processId);
const processWidth = maxDurationSeconds > 0
? Math.max((app.durationSeconds / maxDurationSeconds) * 100, 4)
? Math.max((app.durationSeconds / maxDurationSeconds) * 100, 2)
: 0;
return (
<div
key={app.processId}
className={`px-4 py-3 transition-colors ${highlightedProcessId === app.processId
? 'bg-blue-50/70 dark:bg-blue-500/8'
: ''
}`}
className={`px-4 py-3 transition-colors`}
>
<button
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}>
{app.processName}
</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">
{app.titleCount}
</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="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>

View File

@ -2,7 +2,7 @@
import React, { useState } from 'react';
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 ScreenTimeTab from './components/ScreenTimeTab';
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">
<nav className="-mb-px flex space-x-8">
<nav className="-mb-px flex space-x-6">
<button
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-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
onClick={() => setActiveTab('screen-time')}
@ -87,16 +87,17 @@ export default function HostDetail() {
}`}
>
<Star className="h-4 w-4 mr-1" />
</button>
<button
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-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>
</nav>
</div>

View File

@ -8,6 +8,7 @@ import { Prisma } from '@prisma/client'
const MAX_GAP_MS = 5 * 60 * 1000
const DEFAULT_SAMPLE_MS = 30 * 1000
const MIN_SAMPLE_MS = 5 * 1000
const SPOTLIGHT_APP_LIMIT = 4
const LABEL_TOKEN_THRESHOLD_RATIO = 0.5
const HOUR_MS = 60 * 60 * 1000
const DAY_MS = 24 * HOUR_MS
@ -458,7 +459,7 @@ async function handleScreenTime(req: NextRequest) {
const aggregateSummary = buildAppList(aggregateUsageMap)
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,
processName: app.processName,
durationSeconds: app.durationSeconds,