diff --git a/apps/web/package.json b/apps/web/package.json index d3cc23b..45d4755 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -15,6 +15,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dexie": "^4.4.2", + "dexie-react-hooks": "^4.4.0", "lucide-react": "^1.7.0", "react": "^19.2.4", "react-dom": "^19.2.4", diff --git a/apps/web/src/pages/todo-shell-page.tsx b/apps/web/src/pages/todo-shell-page.tsx index cb6878b..74e19c7 100644 --- a/apps/web/src/pages/todo-shell-page.tsx +++ b/apps/web/src/pages/todo-shell-page.tsx @@ -1,33 +1,381 @@ -import type { WebSession } from "@/services/session-storage"; +import { useEffect, useState } from "react"; +import { useLiveQuery } from "dexie-react-hooks"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { + createLocalTask, + deleteLocalTask, + getLocalTaskById, + listLocalTasksByUser, + updateLocalTask +} from "@/services/local-task-repo"; +import type { LocalTaskPriority, LocalTaskStatus } from "@/services/local-db"; +import type { WebSession } from "@/services/session-storage"; type TodoShellPageProps = { session: WebSession | null; }; +type TaskFormState = { + title: string; + contentText: string; + priority: LocalTaskPriority; + status: LocalTaskStatus; + ddlInput: string; +}; + +const DEFAULT_FORM_STATE: TaskFormState = { + title: "", + contentText: "", + priority: "MEDIUM", + status: "TODO", + ddlInput: "" +}; + +const PRIORITY_OPTIONS: Array<{ value: LocalTaskPriority; label: string }> = [ + { value: "LOW", label: "低" }, + { value: "MEDIUM", label: "中" }, + { value: "HIGH", label: "高" }, + { value: "URGENT", label: "紧急" } +]; + +const STATUS_OPTIONS: Array<{ value: LocalTaskStatus; label: string }> = [ + { value: "TODO", label: "待办" }, + { value: "IN_PROGRESS", label: "进行中" }, + { value: "DONE", label: "已完成" }, + { value: "ARCHIVED", label: "已归档" } +]; + +function toDatetimeLocalValue(timestamp: number | null): string { + if (timestamp === null) { + return ""; + } + + const date = new Date(timestamp); + const timezoneOffset = date.getTimezoneOffset() * 60_000; + return new Date(timestamp - timezoneOffset).toISOString().slice(0, 16); +} + +function parseDatetimeLocalValue(value: string): number | null { + if (!value) { + return null; + } + + const parsed = new Date(value).getTime(); + if (Number.isNaN(parsed)) { + return null; + } + + return parsed; +} + +function formatUpdatedAt(timestamp: number): string { + return new Date(timestamp).toLocaleString("zh-CN", { + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit" + }); +} + export function TodoShellPage({ session }: TodoShellPageProps) { - return ( -
-

TodoList 工作台

-

- {session ? `当前登录邮箱:${session.user.email}` : "当前未建立登录会话,请先完成登录。"} -

-
-
-

今日重点

-

待接入

-
-
-

临近截止

-

待接入

-
-
-

任务分析

-

待接入

-
+ const [selectedTaskId, setSelectedTaskId] = useState(null); + const [formState, setFormState] = useState(DEFAULT_FORM_STATE); + const [saving, setSaving] = useState(false); + const [creating, setCreating] = useState(false); + const [deleting, setDeleting] = useState(false); + const [feedback, setFeedback] = useState(null); + + const userId = session?.user.id ?? ""; + + const tasks = useLiveQuery(async () => { + if (!userId) { + return []; + } + + return listLocalTasksByUser(userId); + }, [userId]); + + const selectedTask = useLiveQuery(async () => { + if (!selectedTaskId) { + return undefined; + } + + return getLocalTaskById(selectedTaskId); + }, [selectedTaskId]); + + useEffect(() => { + if (!tasks || tasks.length === 0) { + setSelectedTaskId(null); + return; + } + + if (!selectedTaskId) { + setSelectedTaskId(tasks[0].id); + return; + } + + const exists = tasks.some((task) => task.id === selectedTaskId); + if (!exists) { + setSelectedTaskId(tasks[0].id); + } + }, [selectedTaskId, tasks]); + + useEffect(() => { + if (!selectedTask) { + setFormState(DEFAULT_FORM_STATE); + return; + } + + setFormState({ + title: selectedTask.title, + contentText: selectedTask.contentText ?? "", + priority: selectedTask.priority, + status: selectedTask.status, + ddlInput: toDatetimeLocalValue(selectedTask.ddlAt) + }); + }, [selectedTask]); + + if (!session) { + return ( +
+ 当前未建立登录会话,请先完成登录。
-

- 当前为界面阶段,统计卡片将在任务数据接入后显示真实结果。 -

+ ); + } + + async function handleCreateTask(): Promise { + if (creating || !userId) { + return; + } + + try { + setCreating(true); + const createdTask = await createLocalTask({ userId }); + setSelectedTaskId(createdTask.id); + setFeedback("已创建新任务。"); + } finally { + setCreating(false); + } + } + + async function handleSaveTask(): Promise { + if (!selectedTaskId || saving) { + return; + } + + try { + setSaving(true); + const updatedTask = await updateLocalTask({ + id: selectedTaskId, + title: formState.title, + contentText: formState.contentText || null, + contentJson: null, + priority: formState.priority, + status: formState.status, + ddlAt: parseDatetimeLocalValue(formState.ddlInput) + }); + + if (!updatedTask) { + setFeedback("任务不存在或已被删除。"); + return; + } + + setFeedback("任务已保存。"); + } finally { + setSaving(false); + } + } + + async function handleDeleteTask(): Promise { + if (!selectedTaskId || deleting) { + return; + } + + try { + setDeleting(true); + const deleted = await deleteLocalTask(selectedTaskId); + if (!deleted) { + setFeedback("任务已不存在。"); + return; + } + + setFeedback("任务已删除。"); + } finally { + setDeleting(false); + } + } + + const taskList = tasks ?? []; + + return ( +
+
+
+

任务列表

+ +
+ + {taskList.length === 0 ? ( +

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

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

任务详情

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

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

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