Spaces:
Running
Running
| import React, { useState, useEffect } from 'react'; | |
| import { api } from '../services/api'; | |
| import { Todo } from '../types'; | |
| import { Plus, CheckCircle, Circle, Trash2, CalendarCheck } from 'lucide-react'; | |
| export const TodoList: React.FC = () => { | |
| const [todos, setTodos] = useState<Todo[]>([]); | |
| const [inputValue, setInputValue] = useState(''); | |
| const [loading, setLoading] = useState(false); | |
| useEffect(() => { | |
| loadTodos(); | |
| }, []); | |
| const loadTodos = async () => { | |
| try { | |
| const list = await api.todos.getAll(); | |
| setTodos(list); | |
| } catch (e) { console.error(e); } | |
| }; | |
| const handleAdd = async (e?: React.FormEvent) => { | |
| e?.preventDefault(); | |
| if (!inputValue.trim()) return; | |
| setLoading(true); | |
| await api.todos.add(inputValue); | |
| setInputValue(''); | |
| setLoading(false); | |
| loadTodos(); | |
| }; | |
| const handleToggle = async (t: Todo) => { | |
| // Optimistic update | |
| const newStatus = !t.isCompleted; | |
| setTodos(todos.map(item => item._id === t._id ? { ...item, isCompleted: newStatus } : item)); | |
| await api.todos.update(t._id!, { isCompleted: newStatus }); | |
| }; | |
| const handleDelete = async (id: string) => { | |
| setTodos(todos.filter(t => t._id !== id)); | |
| await api.todos.delete(id); | |
| }; | |
| return ( | |
| <div className="bg-white rounded-xl shadow-sm border border-gray-100 flex flex-col h-full"> | |
| <div className="p-4 border-b border-gray-100 bg-gradient-to-r from-blue-50 to-indigo-50 rounded-t-xl"> | |
| <h3 className="font-bold text-gray-800 flex items-center text-sm"> | |
| <CalendarCheck size={16} className="mr-2 text-indigo-600"/> 个人备忘录 | |
| </h3> | |
| </div> | |
| <div className="flex-1 overflow-y-auto p-2 custom-scrollbar min-h-[200px]"> | |
| {todos.length === 0 ? ( | |
| <div className="text-center text-gray-400 py-10 text-xs"> | |
| <p>暂无待办事项</p> | |
| <p>记录今天的教学/学习任务吧!</p> | |
| </div> | |
| ) : ( | |
| <ul className="space-y-1"> | |
| {todos.map(t => ( | |
| <li key={t._id} className="group flex items-start gap-2 p-2 hover:bg-gray-50 rounded transition-colors text-sm"> | |
| <button onClick={() => handleToggle(t)} className={`mt-0.5 shrink-0 ${t.isCompleted ? 'text-green-500' : 'text-gray-400 hover:text-blue-500'}`}> | |
| {t.isCompleted ? <CheckCircle size={16}/> : <Circle size={16}/>} | |
| </button> | |
| <span className={`flex-1 break-all ${t.isCompleted ? 'text-gray-400 line-through' : 'text-gray-700'}`}> | |
| {t.content} | |
| </span> | |
| <button onClick={() => handleDelete(t._id!)} className="text-gray-300 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity"> | |
| <Trash2 size={14}/> | |
| </button> | |
| </li> | |
| ))} | |
| </ul> | |
| )} | |
| </div> | |
| <div className="p-3 border-t border-gray-100 bg-gray-50 rounded-b-xl"> | |
| <form onSubmit={handleAdd} className="flex gap-2"> | |
| <input | |
| className="flex-1 border border-gray-200 rounded-lg px-3 py-1.5 text-sm outline-none focus:ring-2 focus:ring-indigo-200 focus:border-indigo-400 bg-white" | |
| placeholder="添加新任务..." | |
| value={inputValue} | |
| onChange={e => setInputValue(e.target.value)} | |
| disabled={loading} | |
| /> | |
| <button type="submit" disabled={!inputValue.trim() || loading} className="bg-indigo-600 text-white p-1.5 rounded-lg hover:bg-indigo-700 disabled:opacity-50"> | |
| <Plus size={18}/> | |
| </button> | |
| </form> | |
| </div> | |
| </div> | |
| ); | |
| }; | |