forked from imbytecat/fullstack-starter
feat: 优化待办事项页面视觉与交互体验
- 优化待办事项页面的视觉样式与交互体验,重构头部信息展示、输入框与按钮布局、进度条样式,并改进任务项的视觉反馈与操作交互,提升整体设计一致性与用户操作流畅性。
This commit is contained in:
@@ -114,71 +114,68 @@ function Todo() {
|
|||||||
|
|
||||||
const completedCount = todos.filter((todo) => todo.completed).length
|
const completedCount = todos.filter((todo) => todo.completed).length
|
||||||
const totalCount = todos.length
|
const totalCount = todos.length
|
||||||
|
const progress = totalCount > 0 ? (completedCount / totalCount) * 100 : 0
|
||||||
|
|
||||||
return (
|
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="min-h-screen bg-slate-50 py-12 px-4 sm:px-6 font-sans">
|
||||||
<div className="max-w-3xl mx-auto">
|
<div className="max-w-2xl mx-auto space-y-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="text-center mb-8">
|
<div className="flex items-end justify-between">
|
||||||
<h1 className="text-4xl font-bold text-gray-900 mb-2">
|
<div>
|
||||||
我的待办事项
|
<h1 className="text-3xl font-bold text-slate-900 tracking-tight">
|
||||||
</h1>
|
我的待办
|
||||||
<p className="text-gray-600">
|
</h1>
|
||||||
已完成 {completedCount} / {totalCount} 项任务
|
<p className="text-slate-500 mt-1">保持专注,逐个击破</p>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{/* Add Todo Form */}
|
{/* Add Todo Form */}
|
||||||
<form onSubmit={handleCreateTodo} className="mb-8">
|
<form onSubmit={handleCreateTodo} className="relative group z-10">
|
||||||
<div className="bg-white rounded-lg shadow-sm p-4">
|
<div className="relative transform transition-all duration-200 focus-within:-translate-y-1">
|
||||||
<div className="flex gap-3">
|
<input
|
||||||
<input
|
type="text"
|
||||||
type="text"
|
value={newTodoTitle}
|
||||||
value={newTodoTitle}
|
onChange={handleInputChange}
|
||||||
onChange={handleInputChange}
|
placeholder="添加新任务..."
|
||||||
placeholder="添加新任务..."
|
className="w-full pl-6 pr-32 py-5 bg-white rounded-2xl shadow-[0_8px_30px_rgb(0,0,0,0.04)] border-0 ring-1 ring-slate-100 focus:ring-2 focus:ring-indigo-500/50 outline-none transition-all placeholder:text-slate-400 text-lg text-slate-700"
|
||||||
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}
|
||||||
disabled={createMutation.isPending}
|
/>
|
||||||
/>
|
<button
|
||||||
<button
|
type="submit"
|
||||||
type="submit"
|
disabled={createMutation.isPending || !newTodoTitle.trim()}
|
||||||
disabled={createMutation.isPending || !newTodoTitle.trim()}
|
className="absolute right-3 top-3 bottom-3 px-6 bg-indigo-600 hover:bg-indigo-700 text-white rounded-xl font-medium transition-all shadow-md shadow-indigo-200 disabled:opacity-50 disabled:shadow-none hover:shadow-lg hover:shadow-indigo-300 active:scale-95"
|
||||||
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 ? '添加中' : '添加'}
|
||||||
{createMutation.isPending ? '添加中...' : '添加'}
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{/* Progress Bar */}
|
{/* Progress Bar (Only visible when there are tasks) */}
|
||||||
<div className="mb-8 bg-white rounded-lg shadow-sm p-4">
|
{totalCount > 0 && (
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="h-1.5 w-full bg-slate-200 rounded-full overflow-hidden">
|
||||||
<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
|
<div
|
||||||
className="bg-gradient-to-r from-indigo-500 to-purple-600 h-2.5 rounded-full transition-all duration-300"
|
className="h-full bg-indigo-500 transition-all duration-500 ease-out rounded-full"
|
||||||
style={{
|
style={{ width: `${progress}%` }}
|
||||||
width: `${totalCount > 0 ? (completedCount / totalCount) * 100 : 0}%`,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{/* Todo List */}
|
{/* Todo List */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{todos.length === 0 ? (
|
{todos.length === 0 ? (
|
||||||
<div className="bg-white rounded-lg shadow-sm p-12 text-center">
|
<div className="py-20 text-center">
|
||||||
<div className="text-gray-400 mb-4">
|
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-slate-100 mb-4">
|
||||||
<svg
|
<svg
|
||||||
className="mx-auto h-12 w-12"
|
className="w-8 h-8 text-slate-400"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -187,149 +184,93 @@ function Todo() {
|
|||||||
<path
|
<path
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth={2}
|
strokeWidth={1.5}
|
||||||
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"
|
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-500 text-lg">暂无待办事项</p>
|
<p className="text-slate-500 text-lg font-medium">没有待办事项</p>
|
||||||
<p className="text-gray-400 text-sm mt-2">
|
<p className="text-slate-400 text-sm mt-1">
|
||||||
添加你的第一个任务开始吧!
|
输入上方内容添加您的第一个任务
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
todos.map((todo) => (
|
todos.map((todo) => (
|
||||||
<div
|
<div
|
||||||
key={todo.id}
|
key={todo.id}
|
||||||
className={`bg-white rounded-lg shadow-sm hover:shadow-md transition-all duration-200 p-5 border-l-4 ${
|
className={`group relative flex items-center p-4 bg-white rounded-xl border border-slate-100 shadow-sm transition-all duration-200 hover:shadow-md hover:border-slate-200 ${
|
||||||
todo.completed
|
todo.completed ? 'bg-slate-50/50' : ''
|
||||||
? 'border-green-500 bg-gray-50'
|
|
||||||
: 'border-indigo-500'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-4">
|
<button
|
||||||
{/* Checkbox */}
|
type="button"
|
||||||
|
onClick={() => handleToggleTodo(todo.id, todo.completed)}
|
||||||
|
className={`flex-shrink-0 w-6 h-6 rounded-full border-2 transition-all duration-200 flex items-center justify-center mr-4 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 ${
|
||||||
|
todo.completed
|
||||||
|
? 'bg-indigo-500 border-indigo-500'
|
||||||
|
: 'border-slate-300 hover:border-indigo-500 bg-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{todo.completed && (
|
||||||
|
<svg
|
||||||
|
className="w-3.5 h-3.5 text-white"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={3}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M5 13l4 4L19 7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p
|
||||||
|
className={`text-lg transition-all duration-200 truncate ${
|
||||||
|
todo.completed
|
||||||
|
? 'text-slate-400 line-through decoration-slate-300 decoration-2'
|
||||||
|
: 'text-slate-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{todo.title}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center opacity-0 group-hover:opacity-100 transition-opacity duration-200 absolute right-4 pl-4 bg-gradient-to-l from-white via-white to-transparent sm:static sm:bg-none">
|
||||||
|
<span className="text-xs text-slate-400 mr-3 hidden sm:inline-block">
|
||||||
|
{new Date(todo.createdAt).toLocaleDateString('zh-CN')}
|
||||||
|
</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleToggleTodo(todo.id, todo.completed)}
|
onClick={() => handleDeleteTodo(todo.id)}
|
||||||
disabled={updateMutation.isPending}
|
className="p-2 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors focus:outline-none"
|
||||||
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"
|
title="删除"
|
||||||
>
|
>
|
||||||
<div
|
<svg
|
||||||
className={`w-6 h-6 rounded-full border-2 flex items-center justify-center transition-all ${
|
className="w-5 h-5"
|
||||||
todo.completed
|
fill="none"
|
||||||
? 'bg-green-500 border-green-500'
|
viewBox="0 0 24 24"
|
||||||
: 'border-gray-300 hover:border-indigo-500'
|
stroke="currentColor"
|
||||||
}`}
|
strokeWidth={1.5}
|
||||||
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
{todo.completed && (
|
<path
|
||||||
<svg
|
strokeLinecap="round"
|
||||||
className="w-4 h-4 text-white"
|
strokeLinejoin="round"
|
||||||
fill="none"
|
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"
|
||||||
viewBox="0 0 24 24"
|
/>
|
||||||
stroke="currentColor"
|
</svg>
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={3}
|
|
||||||
d="M5 13l4 4L19 7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* 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 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
|
|
||||||
? 'bg-green-100 text-green-800'
|
|
||||||
: 'bg-indigo-100 text-indigo-800'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{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>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user