From a2d1840e47ac29a5c22c324b95cf6eb5fe892ddb Mon Sep 17 00:00:00 2001 From: Yaosanqi137 Date: Mon, 6 Apr 2026 00:11:22 +0800 Subject: [PATCH] feat(web-task): animate top feedback banner --- apps/web/src/pages/todo-shell-page.tsx | 426 +++++++++++++++---------- 1 file changed, 249 insertions(+), 177 deletions(-) diff --git a/apps/web/src/pages/todo-shell-page.tsx b/apps/web/src/pages/todo-shell-page.tsx index 475aa0b..9787504 100644 --- a/apps/web/src/pages/todo-shell-page.tsx +++ b/apps/web/src/pages/todo-shell-page.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useLiveQuery } from "dexie-react-hooks"; +import { CheckCircle2, CircleAlert } from "lucide-react"; import { TaskRichEditor } from "@/components/task-rich-editor"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; @@ -37,6 +38,11 @@ type TaskFormState = { ddlInput: string; }; +type FeedbackNotice = { + message: string; + tone: "success" | "error"; +}; + const DEFAULT_FORM_STATE: TaskFormState = { title: "", contentJson: null, @@ -124,7 +130,8 @@ export function TodoShellPage({ session }: TodoShellPageProps) { const [saving, setSaving] = useState(false); const [creating, setCreating] = useState(false); const [deleting, setDeleting] = useState(false); - const [feedback, setFeedback] = useState(null); + const [feedback, setFeedback] = useState(null); + const [feedbackVisible, setFeedbackVisible] = useState(false); const [draftReadyTaskId, setDraftReadyTaskId] = useState(null); const savedTaskSnapshotRef = useRef(serializeFormState(DEFAULT_FORM_STATE)); @@ -236,6 +243,66 @@ export function TodoShellPage({ session }: TodoShellPageProps) { void persistDraft(); }, [draftReadyTaskId, formState, selectedTask, selectedTaskId, userId]); + const showFeedback = useCallback((message: string, tone: FeedbackNotice["tone"]): void => { + setFeedback({ message, tone }); + }, []); + + useEffect(() => { + if (!feedback) { + setFeedbackVisible(false); + return; + } + + setFeedbackVisible(false); + const enterAnimationId = window.requestAnimationFrame(() => { + setFeedbackVisible(true); + }); + + const visibleDuration = feedback.tone === "success" ? 2200 : 3200; + const hideTimeoutId = window.setTimeout(() => { + setFeedbackVisible(false); + }, visibleDuration); + + const cleanupTimeoutId = window.setTimeout(() => { + setFeedback((currentFeedback) => + currentFeedback?.message === feedback.message ? null : currentFeedback + ); + }, visibleDuration + 260); + + return () => { + window.cancelAnimationFrame(enterAnimationId); + window.clearTimeout(hideTimeoutId); + window.clearTimeout(cleanupTimeoutId); + }; + }, [feedback]); + + function renderFeedbackBanner() { + if (!feedback) { + return null; + } + + return ( +
+
+ {feedback.tone === "success" ? ( + + ) : ( + + )} +

{feedback.message}

+
+
+ ); + } + const handleCreateTask = useCallback(async (): Promise => { if (creating || !userId) { return; @@ -245,11 +312,11 @@ export function TodoShellPage({ session }: TodoShellPageProps) { setCreating(true); const createdTask = await createLocalTask({ userId }); setSelectedTaskId(createdTask.id); - setFeedback("已创建新任务。"); + showFeedback("已创建新任务。", "success"); } finally { setCreating(false); } - }, [creating, userId]); + }, [creating, showFeedback, userId]); const handleSaveTask = useCallback(async (): Promise => { if (!selectedTaskId || saving) { @@ -269,17 +336,17 @@ export function TodoShellPage({ session }: TodoShellPageProps) { }); if (!updatedTask) { - setFeedback("任务不存在或已被删除。"); + showFeedback("任务不存在或已被删除。", "error"); return; } savedTaskSnapshotRef.current = serializeFormState(createFormStateFromTask(updatedTask)); await deleteLocalTaskDraft(selectedTaskId); - setFeedback("任务已保存。"); + showFeedback("任务已保存。", "success"); } finally { setSaving(false); } - }, [formState, saving, selectedTaskId]); + }, [formState, saving, selectedTaskId, showFeedback]); const handleDeleteTask = useCallback(async (): Promise => { if (!selectedTaskId || deleting) { @@ -290,16 +357,16 @@ export function TodoShellPage({ session }: TodoShellPageProps) { setDeleting(true); const deleted = await deleteLocalTask(selectedTaskId); if (!deleted) { - setFeedback("任务已不存在。"); + showFeedback("任务已不存在。", "error"); return; } await deleteLocalTaskDraft(selectedTaskId); - setFeedback("任务已删除。"); + showFeedback("任务已删除。", "success"); } finally { setDeleting(false); } - }, [deleting, selectedTaskId]); + }, [deleting, selectedTaskId, showFeedback]); useEffect(() => { function handleKeydown(event: KeyboardEvent): void { @@ -326,196 +393,201 @@ export function TodoShellPage({ session }: TodoShellPageProps) { if (!session) { return ( -
- 当前未建立登录会话,请先完成登录。 -
+ <> + {renderFeedbackBanner()} +
+ 当前未建立登录会话,请先完成登录。 +
+ ); } const taskList = tasks ?? []; return ( -
-
-
-

任务列表

- -
- - {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 ( - - ); - })} -
- )} -
- -
-
-

任务详情

-
+ <> + {renderFeedbackBanner()} +
+
+
+

任务列表

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

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

- ) : ( -
- + {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 ? ( +

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

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

任务内容

+
+ + setFormState((previous) => ({ + ...previous, + contentJson: payload.json, + contentText: payload.text + })) + } + /> +
-
- )} - - {feedback ?

{feedback}

: null} -
-
+ )} + + + ); }