88 lines
3.4 KiB
TypeScript
88 lines
3.4 KiB
TypeScript
import { useMutation, useSuspenseQuery } from '@tanstack/react-query'
|
||
import { createFileRoute } from '@tanstack/react-router'
|
||
import { orpc } from '@/client/orpc'
|
||
import { TodoForm } from '@/components/TodoForm'
|
||
import { TodoItem } from '@/components/TodoItem'
|
||
|
||
export const Route = createFileRoute('/')({
|
||
component: Todos,
|
||
loader: async ({ context }) => {
|
||
await context.queryClient.ensureQueryData(orpc.todo.list.queryOptions())
|
||
},
|
||
})
|
||
|
||
function Todos() {
|
||
const listQuery = useSuspenseQuery(orpc.todo.list.queryOptions())
|
||
const createMutation = useMutation(orpc.todo.create.mutationOptions())
|
||
const updateMutation = useMutation(orpc.todo.update.mutationOptions())
|
||
const deleteMutation = useMutation(orpc.todo.remove.mutationOptions())
|
||
|
||
const todos = listQuery.data
|
||
const completedCount = todos.filter((todo) => todo.completed).length
|
||
const totalCount = todos.length
|
||
const progress = totalCount > 0 ? (completedCount / totalCount) * 100 : 0
|
||
|
||
return (
|
||
<div className="min-h-screen bg-slate-50 py-12 px-4 sm:px-6 font-sans">
|
||
<div className="max-w-2xl mx-auto space-y-8">
|
||
{/* Header */}
|
||
<div className="flex items-end justify-between">
|
||
<div>
|
||
<h1 className="text-3xl font-bold text-slate-900 tracking-tight">我的待办</h1>
|
||
<p className="text-slate-500 mt-1">保持专注,逐个击破</p>
|
||
</div>
|
||
<div className="text-right">
|
||
<div className="text-2xl font-semibold text-slate-900">
|
||
{completedCount}
|
||
<span className="text-slate-400 text-lg">/{totalCount}</span>
|
||
</div>
|
||
<div className="text-xs font-medium text-slate-400 uppercase tracking-wider">已完成</div>
|
||
</div>
|
||
</div>
|
||
|
||
<TodoForm onSubmit={(title) => createMutation.mutate({ title })} isPending={createMutation.isPending} />
|
||
|
||
{/* Progress Bar */}
|
||
{totalCount > 0 && (
|
||
<div className="h-1.5 w-full bg-slate-200 rounded-full overflow-hidden">
|
||
<div
|
||
className="h-full bg-indigo-500 transition-all duration-500 ease-out rounded-full"
|
||
style={{ width: `${progress}%` }}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* Todo List */}
|
||
<div className="space-y-3">
|
||
{todos.length === 0 ? (
|
||
<div className="py-20 text-center">
|
||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-slate-100 mb-4">
|
||
<svg
|
||
className="w-8 h-8 text-slate-400"
|
||
fill="none"
|
||
viewBox="0 0 24 24"
|
||
stroke="currentColor"
|
||
aria-hidden="true"
|
||
>
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||
</svg>
|
||
</div>
|
||
<p className="text-slate-500 text-lg font-medium">没有待办事项</p>
|
||
<p className="text-slate-400 text-sm mt-1">输入上方内容添加您的第一个任务</p>
|
||
</div>
|
||
) : (
|
||
todos.map((todo) => (
|
||
<TodoItem
|
||
key={todo.id}
|
||
todo={todo}
|
||
onToggle={(id, completed) => updateMutation.mutate({ id, data: { completed: !completed } })}
|
||
onDelete={(id) => deleteMutation.mutate({ id })}
|
||
/>
|
||
))
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|