stud-manager / components /TodoList.tsx
dvc890's picture
Upload 51 files
5e8f957 verified
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>
);
};