diff --git a/src/routes/todo.tsx b/src/routes/todo.tsx index cc4531f..69cc967 100644 --- a/src/routes/todo.tsx +++ b/src/routes/todo.tsx @@ -1,23 +1,118 @@ -import { useSuspenseQuery } from '@tanstack/react-query' +import { useMutation, useSuspenseQuery } from '@tanstack/react-query' import { createFileRoute } from '@tanstack/react-router' import { createServerFn } from '@tanstack/react-start' +import { eq } from 'drizzle-orm' +import type { ChangeEvent, FormEvent } from 'react' +import { useState } from 'react' +import { z } from 'zod' import { db } from '@/db' +import { todoTable } from '@/db/schema' +// Zod Schemas +const createTodoSchema = z.object({ + title: z.string().min(1, '标题不能为空'), +}) + +const updateTodoSchema = z.object({ + id: z.string().uuid(), + completed: z.boolean(), +}) + +const deleteTodoSchema = z.object({ + id: z.string().uuid(), +}) + +// Server Functions - CRUD 操作 const getTodos = createServerFn({ method: 'GET' }).handler(async () => { - const todos = await db.query.todoTable.findMany() + const todos = await db.query.todoTable.findMany({ + orderBy: (todos, { desc }) => [desc(todos.createdAt)], + }) return todos }) +const createTodo = createServerFn({ method: 'POST' }) + .inputValidator(createTodoSchema) + .handler(async ({ data }) => { + const [newTodo] = await db + .insert(todoTable) + .values({ title: data.title }) + .returning() + return newTodo + }) + +const updateTodo = createServerFn({ method: 'POST' }) + .inputValidator(updateTodoSchema) + .handler(async ({ data }) => { + const [updatedTodo] = await db + .update(todoTable) + .set({ completed: data.completed }) + .where(eq(todoTable.id, data.id)) + .returning() + return updatedTodo + }) + +const deleteTodo = createServerFn({ method: 'POST' }) + .inputValidator(deleteTodoSchema) + .handler(async ({ data }) => { + await db.delete(todoTable).where(eq(todoTable.id, data.id)) + return { success: true } + }) + export const Route = createFileRoute('/todo')({ component: Todo, }) function Todo() { - const { data: todos } = useSuspenseQuery({ + const [newTodoTitle, setNewTodoTitle] = useState('') + const { data: todos, refetch } = useSuspenseQuery({ queryKey: ['todos'], queryFn: () => getTodos(), }) + // Mutations + const createMutation = useMutation({ + mutationFn: (title: string) => createTodo({ data: { title } }), + onSuccess: () => { + setNewTodoTitle('') + refetch() + }, + }) + + const updateMutation = useMutation({ + mutationFn: ({ id, completed }: { id: string; completed: boolean }) => + updateTodo({ data: { id, completed } }), + onSuccess: () => { + refetch() + }, + }) + + const deleteMutation = useMutation({ + mutationFn: (id: string) => deleteTodo({ data: { id } }), + onSuccess: () => { + refetch() + }, + }) + + // Handlers + const handleCreateTodo = (e: FormEvent) => { + e.preventDefault() + if (newTodoTitle.trim()) { + createMutation.mutate(newTodoTitle.trim()) + } + } + + const handleInputChange = (e: ChangeEvent) => { + setNewTodoTitle(e.target.value) + } + + const handleToggleTodo = (id: string, currentCompleted: boolean) => { + updateMutation.mutate({ id, completed: !currentCompleted }) + } + + const handleDeleteTodo = (id: string) => { + deleteMutation.mutate(id) + } + const completedCount = todos.filter((todo) => todo.completed).length const totalCount = todos.length @@ -34,6 +129,29 @@ function Todo() {

+ {/* Add Todo Form */} +
+
+
+ + +
+
+
+ {/* Progress Bar */}
@@ -92,7 +210,12 @@ function Todo() { >
{/* Checkbox */} -
+
+ {/* Todo Content */}
@@ -151,8 +274,8 @@ function Todo() {
- {/* Status Badge */} -
+ {/* Status Badge and Delete Button */} +
{todo.completed ? '已完成' : '进行中'} +
diff --git a/tsconfig.json b/tsconfig.json index dd311f4..2f120d7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { // Environment setup & latest features - "lib": ["ESNext"], + "lib": ["ESNext", "DOM", "DOM.Iterable"], "target": "ESNext", "module": "Preserve", "moduleDetection": "force",