From 63298d682719e11601a8497622cd2e10a55a81f3 Mon Sep 17 00:00:00 2001 From: Yaosanqi137 Date: Mon, 6 Apr 2026 01:55:18 +0800 Subject: [PATCH] perf(web-page): memoize todo panels to limit rerenders --- apps/web/src/pages/todo-shell-page.tsx | 554 +++++++++++++++---------- 1 file changed, 332 insertions(+), 222 deletions(-) diff --git a/apps/web/src/pages/todo-shell-page.tsx b/apps/web/src/pages/todo-shell-page.tsx index 643b93d..b1cdd5f 100644 --- a/apps/web/src/pages/todo-shell-page.tsx +++ b/apps/web/src/pages/todo-shell-page.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from "react"; +import { memo, useCallback, useEffect, useRef, useState } from "react"; import { useLiveQuery } from "dexie-react-hooks"; import { CheckCircle2, @@ -54,6 +54,8 @@ type FeedbackNotice = { tone: "success" | "error"; }; +type StorageQuotaSnapshot = Awaited>; + const DRAFT_PERSIST_DEBOUNCE_MS = 500; const DEFAULT_FORM_STATE: TaskFormState = { @@ -266,6 +268,277 @@ function getSyncSummary(status: SyncEngineStatus): { }; } +type SyncStatusCardProps = { + syncStatus: SyncEngineStatus; + onTriggerSync: () => void; +}; + +const SyncStatusCard = memo(function SyncStatusCard({ + syncStatus, + onTriggerSync +}: SyncStatusCardProps) { + const syncSummary = getSyncSummary(syncStatus); + const SyncSummaryIcon = syncSummary.icon; + + return ( +
+
+
+
+ +
+
+

{syncSummary.title}

+

{syncSummary.description}

+
+
+ +
+ + 待上传 {syncStatus.pendingCount} + + + 云端待合并 {syncStatus.pendingRemoteCount} + + {syncStatus.blockedCount > 0 ? ( + + 阻塞 {syncStatus.blockedCount} + + ) : null} + + 上次成功 {formatSyncTimestamp(syncStatus.lastSyncedAt)} + + +
+
+
+ ); +}); + +type TaskListPanelProps = { + tasks: LocalTaskRecord[]; + selectedTaskId: string | null; + quotaSnapshot: StorageQuotaSnapshot | null; + creating: boolean; + onCreateTask: () => void; + onSelectTask: (taskId: string) => void; +}; + +const TaskListPanel = memo(function TaskListPanel({ + tasks, + selectedTaskId, + quotaSnapshot, + creating, + onCreateTask, + onSelectTask +}: TaskListPanelProps) { + return ( +
+
+

任务列表

+ +
+ + {quotaSnapshot ? ( +

= 85 ? "text-destructive" : "text-muted-foreground" + )} + > + 空间占用(估算):{formatStorageSize(quotaSnapshot.usedBytes)} /{" "} + {formatStorageSize(quotaSnapshot.quotaBytes)}({quotaSnapshot.usedPercent.toFixed(1)}%) +

+ ) : null} + + {tasks.length === 0 ? ( +

+ 还没有任务,点击右上角“新建任务”。 +

+ ) : ( +
+ {tasks.map((task) => { + const isActive = task.id === selectedTaskId; + return ( + + ); + })} +
+ )} +
+ ); +}); + +type TaskDetailPanelProps = { + selectedTaskId: string | null; + selectedTask: LocalTaskRecord | undefined; + formState: TaskFormState; + editorKey: string; + editorSeedState: TaskEditorState; + saving: boolean; + deleting: boolean; + onSaveTask: () => void; + onDeleteTask: () => void; + onTitleChange: (value: string) => void; + onStatusChange: (value: LocalTaskStatus) => void; + onPriorityChange: (value: LocalTaskPriority) => void; + onDdlChange: (value: string) => void; + onEditorChange: (payload: { json: string | null; text: string }) => void; +}; + +const TaskDetailPanel = memo(function TaskDetailPanel({ + selectedTaskId, + selectedTask, + formState, + editorKey, + editorSeedState, + saving, + deleting, + onSaveTask, + onDeleteTask, + onTitleChange, + onStatusChange, + onPriorityChange, + onDdlChange, + onEditorChange +}: TaskDetailPanelProps) { + return ( +
+
+

任务详情

+
+ + +
+
+ + {!selectedTaskId || !selectedTask ? ( +

+ 请选择一个任务进行编辑。 +

+ ) : ( +
+ + +
+ + + +
+ + + +
+

任务内容

+
+ +
+
+
+ )} +
+ ); +}); + export function TodoShellPage({ session }: TodoShellPageProps) { const [selectedTaskId, setSelectedTaskId] = useState(null); const [formState, setFormState] = useState(DEFAULT_FORM_STATE); @@ -572,6 +845,38 @@ export function TodoShellPage({ session }: TodoShellPageProps) { [scheduleDraftPersist] ); + const handleSelectTask = useCallback((taskId: string): void => { + setSelectedTaskId(taskId); + }, []); + + const handleTitleChange = useCallback((value: string): void => { + setFormState((previous) => ({ + ...previous, + title: value + })); + }, []); + + const handleStatusChange = useCallback((value: LocalTaskStatus): void => { + setFormState((previous) => ({ + ...previous, + status: value + })); + }, []); + + const handlePriorityChange = useCallback((value: LocalTaskPriority): void => { + setFormState((previous) => ({ + ...previous, + priority: value + })); + }, []); + + const handleDdlChange = useCallback((value: string): void => { + setFormState((previous) => ({ + ...previous, + ddlInput: value + })); + }, []); + useEffect(() => { function handleKeydown(event: KeyboardEvent): void { const isSaveShortcut = (event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "s"; @@ -607,235 +912,40 @@ export function TodoShellPage({ session }: TodoShellPageProps) { } const taskList = tasks ?? []; - const syncSummary = getSyncSummary(syncStatus); - const SyncSummaryIcon = syncSummary.icon; + const quotaPanelSnapshot = quotaSnapshot ?? null; return ( <> {renderFeedbackBanner()}
-
-
-
-
- -
-
-

{syncSummary.title}

-

{syncSummary.description}

-
-
- -
- - 待上传 {syncStatus.pendingCount} - - - 云端待合并 {syncStatus.pendingRemoteCount} - - {syncStatus.blockedCount > 0 ? ( - - 阻塞 {syncStatus.blockedCount} - - ) : null} - - 上次成功 {formatSyncTimestamp(syncStatus.lastSyncedAt)} - - -
-
-
+
-
-
-

任务列表

- -
+ - {quotaSnapshot ? ( -

= 85 ? "text-destructive" : "text-muted-foreground" - )} - > - 空间占用(估算):{formatStorageSize(quotaSnapshot.usedBytes)} /{" "} - {formatStorageSize(quotaSnapshot.quotaBytes)}( - {quotaSnapshot.usedPercent.toFixed(1)} - %) -

- ) : null} - - {taskList.length === 0 ? ( -

- 还没有任务,点击右上角“新建任务”。 -

- ) : ( -
- {taskList.map((task) => { - const isActive = task.id === selectedTaskId; - return ( - - ); - })} -
- )} -
- -
-
-

任务详情

-
- - -
-
- - {!selectedTaskId || !selectedTask ? ( -

- 请选择一个任务进行编辑。 -

- ) : ( -
- - -
- - - -
- - - -
-

任务内容

-
- -
-
-
- )} -
+