forked from imbytecat/fullstack-starter
feat: 添加待办事项列表功能并更新环境变量源
- 将运行时环境变量源从 import.meta.env 更新为 process.env。 - 添加待办事项列表功能,包含进度条、任务状态显示、完成统计和响应式界面设计。
This commit is contained in:
@@ -9,6 +9,6 @@ export const env = createEnv({
|
|||||||
client: {
|
client: {
|
||||||
VITE_APP_TITLE: z.string().min(1).optional(),
|
VITE_APP_TITLE: z.string().min(1).optional(),
|
||||||
},
|
},
|
||||||
runtimeEnv: import.meta.env,
|
runtimeEnv: process.env,
|
||||||
emptyStringAsUndefined: true,
|
emptyStringAsUndefined: true,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
import { createServerFn } from '@tanstack/react-start'
|
import { createServerFn } from '@tanstack/react-start'
|
||||||
import { db } from '@/db'
|
import { db } from '@/db'
|
||||||
import { todoTable } from '@/db/schema'
|
|
||||||
|
|
||||||
const getTodos = createServerFn().handler(async () => {
|
const getTodos = createServerFn({ method: 'GET' }).handler(async () => {
|
||||||
const todos = await db.query.todoTable.findMany()
|
const todos = await db.query.todoTable.findMany()
|
||||||
return todos
|
return todos
|
||||||
})
|
})
|
||||||
@@ -13,5 +13,180 @@ export const Route = createFileRoute('/todo')({
|
|||||||
})
|
})
|
||||||
|
|
||||||
function Todo() {
|
function Todo() {
|
||||||
return <div>Hello "/todo"!</div>
|
const { data: todos } = useSuspenseQuery({
|
||||||
|
queryKey: ['todos'],
|
||||||
|
queryFn: () => getTodos(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const completedCount = todos.filter((todo) => todo.completed).length
|
||||||
|
const totalCount = todos.length
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-indigo-50 via-white to-purple-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="max-w-3xl mx-auto">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h1 className="text-4xl font-bold text-gray-900 mb-2">
|
||||||
|
我的待办事项
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
已完成 {completedCount} / {totalCount} 项任务
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress Bar */}
|
||||||
|
<div className="mb-8 bg-white rounded-lg shadow-sm p-4">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-sm font-medium text-gray-700">完成进度</span>
|
||||||
|
<span className="text-sm font-medium text-indigo-600">
|
||||||
|
{totalCount > 0
|
||||||
|
? Math.round((completedCount / totalCount) * 100)
|
||||||
|
: 0}
|
||||||
|
%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-gray-200 rounded-full h-2.5">
|
||||||
|
<div
|
||||||
|
className="bg-gradient-to-r from-indigo-500 to-purple-600 h-2.5 rounded-full transition-all duration-300"
|
||||||
|
style={{
|
||||||
|
width: `${totalCount > 0 ? (completedCount / totalCount) * 100 : 0}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Todo List */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
{todos.length === 0 ? (
|
||||||
|
<div className="bg-white rounded-lg shadow-sm p-12 text-center">
|
||||||
|
<div className="text-gray-400 mb-4">
|
||||||
|
<svg
|
||||||
|
className="mx-auto h-12 w-12"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-500 text-lg">暂无待办事项</p>
|
||||||
|
<p className="text-gray-400 text-sm mt-2">
|
||||||
|
添加你的第一个任务开始吧!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
todos.map((todo) => (
|
||||||
|
<div
|
||||||
|
key={todo.id}
|
||||||
|
className={`bg-white rounded-lg shadow-sm hover:shadow-md transition-all duration-200 p-5 border-l-4 ${
|
||||||
|
todo.completed
|
||||||
|
? 'border-green-500 bg-gray-50'
|
||||||
|
: 'border-indigo-500'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
{/* Checkbox */}
|
||||||
|
<div className="flex-shrink-0 pt-1">
|
||||||
|
<div
|
||||||
|
className={`w-6 h-6 rounded-full border-2 flex items-center justify-center transition-all ${
|
||||||
|
todo.completed
|
||||||
|
? 'bg-green-500 border-green-500'
|
||||||
|
: 'border-gray-300 hover:border-indigo-500'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{todo.completed && (
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4 text-white"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={3}
|
||||||
|
d="M5 13l4 4L19 7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Todo Content */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h3
|
||||||
|
className={`text-lg font-medium ${
|
||||||
|
todo.completed
|
||||||
|
? 'text-gray-500 line-through'
|
||||||
|
: 'text-gray-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{todo.title}
|
||||||
|
</h3>
|
||||||
|
<div className="mt-2 flex items-center gap-2 text-xs text-gray-500">
|
||||||
|
<span className="inline-flex items-center gap-1">
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{new Date(todo.createdAt).toLocaleDateString('zh-CN')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status Badge */}
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium ${
|
||||||
|
todo.completed
|
||||||
|
? 'bg-green-100 text-green-800'
|
||||||
|
: 'bg-indigo-100 text-indigo-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{todo.completed ? '已完成' : '进行中'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer Stats */}
|
||||||
|
{todos.length > 0 && (
|
||||||
|
<div className="mt-8 grid grid-cols-2 gap-4">
|
||||||
|
<div className="bg-white rounded-lg shadow-sm p-4 text-center">
|
||||||
|
<p className="text-2xl font-bold text-indigo-600">
|
||||||
|
{totalCount - completedCount}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-600 mt-1">待完成</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white rounded-lg shadow-sm p-4 text-center">
|
||||||
|
<p className="text-2xl font-bold text-green-600">
|
||||||
|
{completedCount}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-600 mt-1">已完成</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user