feat: 添加任务管理功能并优化交互体验

- 添加任务创建、完成状态切换和删除功能,优化界面交互并集成表单验证与加载状态反馈。
- 添加DOM和DOM.Iterable库支持以增强类型定义和浏览器API兼容性。
This commit is contained in:
2026-01-17 03:09:08 +08:00
parent e5bcf44bc6
commit 928a78a335
2 changed files with 153 additions and 8 deletions

View File

@@ -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<HTMLFormElement>) => {
e.preventDefault()
if (newTodoTitle.trim()) {
createMutation.mutate(newTodoTitle.trim())
}
}
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
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() {
</p>
</div>
{/* Add Todo Form */}
<form onSubmit={handleCreateTodo} className="mb-8">
<div className="bg-white rounded-lg shadow-sm p-4">
<div className="flex gap-3">
<input
type="text"
value={newTodoTitle}
onChange={handleInputChange}
placeholder="添加新任务..."
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
disabled={createMutation.isPending}
/>
<button
type="submit"
disabled={createMutation.isPending || !newTodoTitle.trim()}
className="px-6 py-2 bg-gradient-to-r from-indigo-500 to-purple-600 text-white rounded-lg font-medium hover:from-indigo-600 hover:to-purple-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
>
{createMutation.isPending ? '添加中...' : '添加'}
</button>
</div>
</div>
</form>
{/* Progress Bar */}
<div className="mb-8 bg-white rounded-lg shadow-sm p-4">
<div className="flex items-center justify-between mb-2">
@@ -92,7 +210,12 @@ function Todo() {
>
<div className="flex items-start gap-4">
{/* Checkbox */}
<div className="flex-shrink-0 pt-1">
<button
type="button"
onClick={() => handleToggleTodo(todo.id, todo.completed)}
disabled={updateMutation.isPending}
className="flex-shrink-0 pt-1 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 rounded-full disabled:opacity-50"
>
<div
className={`w-6 h-6 rounded-full border-2 flex items-center justify-center transition-all ${
todo.completed
@@ -117,7 +240,7 @@ function Todo() {
</svg>
)}
</div>
</div>
</button>
{/* Todo Content */}
<div className="flex-1 min-w-0">
@@ -151,8 +274,8 @@ function Todo() {
</div>
</div>
{/* Status Badge */}
<div className="flex-shrink-0">
{/* Status Badge and Delete Button */}
<div className="flex-shrink-0 flex items-center gap-2">
<span
className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium ${
todo.completed
@@ -162,6 +285,28 @@ function Todo() {
>
{todo.completed ? '已完成' : '进行中'}
</span>
<button
type="button"
onClick={() => handleDeleteTodo(todo.id)}
disabled={deleteMutation.isPending}
className="p-2 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 disabled:opacity-50"
title="删除任务"
>
<svg
className="w-5 h-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button>
</div>
</div>
</div>