feat: 标签调整宽度和图标,top 3 -> top 4
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
parent
520cfd96c7
commit
fea99d37fe
@ -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) => ({
|
||||
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,
|
||||
isSelected: day.isSelected
|
||||
}));
|
||||
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,
|
||||
...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) => ({
|
||||
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,
|
||||
otherDurationSeconds: hour.otherDurationSeconds
|
||||
}));
|
||||
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,
|
||||
...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>
|
||||
)}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user