好的, 让我们详细规划第 6 部分: Todo 功能实现。我们将这部分分为 3 天, 每天专注于特定的 Todo 相关功能和组件。
第 6 部分: Todo 功能实现 (3 天)
Day 1: Todo 列表和创建功能
- 更新 TodoList 组件
在
src/components/TodoList.tsx中:
import React, { useEffect, useState } from 'react';
import { Table, Checkbox, ActionIcon, Group, Text } from '@mantine/core';
import { IconEdit, IconTrash } from '@tabler/icons-react';
import { getTodos, updateTodo, deleteTodo } from '../services/api';
interface Todo {
id: string;
title: string;
description: string;
completed: boolean;
}
export const TodoList: React.FC = () => {
const [todos, setTodos] = useState<Todo[]>([]);
useEffect(() => {
fetchTodos();
}, []);
const fetchTodos = async () => {
try {
const response = await getTodos();
setTodos(response.data);
} catch (error) {
console.error('Failed to fetch todos', error);
}
};
const handleToggleComplete = async (id: string, completed: boolean) => {
try {
await updateTodo(id, { completed: !completed });
fetchTodos();
} catch (error) {
console.error('Failed to update todo', error);
}
};
const handleDelete = async (id: string) => {
try {
await deleteTodo(id);
fetchTodos();
} catch (error) {
console.error('Failed to delete todo', error);
}
};
return (
<Table>
<thead>
<tr>
<th>Complete</th>
<th>Title</th>
<th>Description</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{todos.map((todo) => (
<tr key={todo.id}>
<td>
<Checkbox
checked={todo.completed}
onChange={() => handleToggleComplete(todo.id, todo.completed)}
/>
</td>
<td>
<Text strikethrough={todo.completed}>{todo.title}</Text>
</td>
<td>{todo.description}</td>
<td>
<Group spacing={0} position="left">
<ActionIcon>
<IconEdit size={16} />
</ActionIcon>
<ActionIcon color="red" onClick={() => handleDelete(todo.id)}>
<IconTrash size={16} />
</ActionIcon>
</Group>
</td>
</tr>
))}
</tbody>
</Table>
);
};- 更新 TodoForm 组件
在
src/components/TodoForm.tsx中:
import React from 'react';
import { useForm } from '@mantine/form';
import { TextInput, Textarea, Button, Box } from '@mantine/core';
import { createTodo } from '../services/api';
interface TodoFormProps {
onTodoCreated: () => void;
}
export const TodoForm: React.FC<TodoFormProps> = ({ onTodoCreated }) => {
const form = useForm({
initialValues: {
title: '',
description: '',
},
validate: {
title: (value) => (value.trim().length > 0 ? null : 'Title is required'),
},
});
const handleSubmit = async (values: { title: string; description: string }) => {
try {
await createTodo(values);
form.reset();
onTodoCreated();
} catch (error) {
console.error('Failed to create todo', error);
}
};
return (
<Box mx="auto" mb="xl">
<form onSubmit={form.onSubmit(handleSubmit)}>
<TextInput
required
label="Title"
placeholder="Enter todo title"
{...form.getInputProps('title')}
/>
<Textarea
mt="md"
label="Description"
placeholder="Enter todo description"
{...form.getInputProps('description')}
/>
<Button type="submit" mt="md">
Add Todo
</Button>
</form>
</Box>
);
};- 更新 Home 页面
在
src/pages/Home.tsx中:
import React, { useState } from 'react';
import { Container, Title } from '@mantine/core';
import { TodoList } from '../components/TodoList';
import { TodoForm } from '../components/TodoForm';
export const Home: React.FC = () => {
const [refreshTrigger, setRefreshTrigger] = useState(0);
const handleTodoCreated = () => {
setRefreshTrigger((prev) => prev + 1);
};
return (
<Container>
<Title order={2} mb="xl">My Todos</Title>
<TodoForm onTodoCreated={handleTodoCreated} />
<TodoList key={refreshTrigger} />
</Container>
);
};Day 2: Todo 编辑和完成状态切换功能
- 创建 TodoEditForm 组件
在
src/components/TodoEditForm.tsx中:
import React from 'react';
import { useForm } from '@mantine/form';
import { TextInput, Textarea, Button, Group } from '@mantine/core';
import { updateTodo } from '../services/api';
interface TodoEditFormProps {
todo: {
id: string;
title: string;
description: string;
};
onClose: () => void;
onUpdate: () => void;
}
export const TodoEditForm: React.FC<TodoEditFormProps> = ({ todo, onClose, onUpdate }) => {
const form = useForm({
initialValues: {
title: todo.title,
description: todo.description,
},
validate: {
title: (value) => (value.trim().length > 0 ? null : 'Title is required'),
},
});
const handleSubmit = async (values: { title: string; description: string }) => {
try {
await updateTodo(todo.id, values);
onUpdate();
onClose();
} catch (error) {
console.error('Failed to update todo', error);
}
};
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<TextInput
required
label="Title"
placeholder="Enter todo title"
{...form.getInputProps('title')}
/>
<Textarea
mt="md"
label="Description"
placeholder="Enter todo description"
{...form.getInputProps('description')}
/>
<Group position="right" mt="md">
<Button type="button" variant="outline" onClick={onClose}>
Cancel
</Button>
<Button type="submit">Save</Button>
</Group>
</form>
);
};- 更新 TodoList 组件以包含编辑功能
在
src/components/TodoList.tsx中:
import React, { useEffect, useState } from 'react';
import { Table, Checkbox, ActionIcon, Group, Text, Modal } from '@mantine/core';
import { IconEdit, IconTrash } from '@tabler/icons-react';
import { getTodos, updateTodo, deleteTodo } from '../services/api';
import { TodoEditForm } from './TodoEditForm';
interface Todo {
id: string;
title: string;
description: string;
completed: boolean;
}
export const TodoList: React.FC = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const [editingTodo, setEditingTodo] = useState<Todo | null>(null);
useEffect(() => {
fetchTodos();
}, []);
const fetchTodos = async () => {
try {
const response = await getTodos();
setTodos(response.data);
} catch (error) {
console.error('Failed to fetch todos', error);
}
};
const handleToggleComplete = async (id: string, completed: boolean) => {
try {
await updateTodo(id, { completed: !completed });
fetchTodos();
} catch (error) {
console.error('Failed to update todo', error);
}
};
const handleDelete = async (id: string) => {
try {
await deleteTodo(id);
fetchTodos();
} catch (error) {
console.error('Failed to delete todo', error);
}
};
const handleEditClick = (todo: Todo) => {
setEditingTodo(todo);
};
const handleEditClose = () => {
setEditingTodo(null);
};
const handleEditUpdate = () => {
fetchTodos();
setEditingTodo(null);
};
return (
<>
<Table>
<thead>
<tr>
<th>Complete</th>
<th>Title</th>
<th>Description</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{todos.map((todo) => (
<tr key={todo.id}>
<td>
<Checkbox
checked={todo.completed}
onChange={() => handleToggleComplete(todo.id, todo.completed)}
/>
</td>
<td>
<Text strikethrough={todo.completed}>{todo.title}</Text>
</td>
<td>{todo.description}</td>
<td>
<Group spacing={0} position="left">
<ActionIcon onClick={() => handleEditClick(todo)}>
<IconEdit size={16} />
</ActionIcon>
<ActionIcon color="red" onClick={() => handleDelete(todo.id)}>
<IconTrash size={16} />
</ActionIcon>
</Group>
</td>
</tr>
))}
</tbody>
</Table>
<Modal
Opened={editingTodo !== null}
OnClose={handleEditClose}
title="Edit Todo"
>
{editingTodo && (
<TodoEditForm
todo={editingTodo}
onClose={handleEditClose}
onUpdate={handleEditUpdate}
/>
)}
</Modal>
</>
);
};Day 3: 添加过滤和排序功能
- 创建 TodoFilters 组件
在
src/components/TodoFilters.tsx中:
import React from 'react';
import { Group, Select, TextInput } from '@mantine/core';
interface TodoFiltersProps {
filter: string;
setFilter: (value: string) => void;
sort: string;
setSort: (value: string) => void;
search: string;
setSearch: (value: string) => void;
}
export const TodoFilters: React.FC<TodoFiltersProps> = ({
filter,
setFilter,
sort,
setSort,
search,
setSearch,
}) => {
return (
<Group mb="md">
<Select
label="Filter"
value={filter}
onChange={(value) => setFilter(value || '')}
data={[
{ value: 'all', label: 'All' },
{ value: 'active', label: 'Active' },
{ value: 'completed', label: 'Completed' },
]}
/>
<Select
label="Sort"
value={sort}
onChange={(value) => setSort(value || '')}
data={[
{ value: 'createdAt_desc', label: 'Newest' },
{ value: 'createdAt_asc', label: 'Oldest' },
{ value: 'title_asc', label: 'Title (A-Z)' },
{ value: 'title_desc', label: 'Title (Z-A)' },
]}
/>
<TextInput
label="Search"
value={search}
onChange={(event) => setSearch(event.currentTarget.value)}
placeholder="Search todos..."
/>
</Group>
);
};- 更新 TodoList 组件以包含过滤和排序功能
在
src/components/TodoList.tsx中:
import React, { useEffect, useState } from 'react';
import { Table, Checkbox, ActionIcon, Group, Text, Modal } from '@mantine/core';
import { IconEdit, IconTrash } from '@tabler/icons-react';
import { getTodos, updateTodo, deleteTodo } from '../services/api';
import { TodoEditForm } from './TodoEditForm';
import { TodoFilters } from './TodoFilters';
interface Todo {
id: string;
title: string;
description: string;
completed: boolean;
createdAt: string;
}
export const TodoList: React.FC = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const [editingTodo, setEditingTodo] = useState<Todo | null>(null);
const [filter, setFilter] = useState('all');
const [sort, setSort] = useState('createdAt_desc');
const [search, setSearch] = useState('');
useEffect(() => {
fetchTodos();
}, [filter, sort, search]);
const fetchTodos = async () => {
try {
const response = await getTodos({ filter, sort, search });
setTodos(response.data);
} catch (error) {
console.error('Failed to fetch todos', error);
}
};
// ... (保留之前的handleToggleComplete, handleDelete, handleEditClick, handleEditClose, handleEditUpdate函数)
const filteredAndSortedTodos = todos
.filter((todo) => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
})
.filter((todo) =>
todo.title.toLowerCase().includes(search.toLowerCase()) ||
todo.description.toLowerCase().includes(search.toLowerCase())
)
.sort((a, b) => {
const [field, direction] = sort.split('_');
if (field === 'createdAt') {
return direction === 'asc'
? new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
: new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
}
if (field === 'title') {
return direction === 'asc'
? a.title.localeCompare(b.title)
: b.title.localeCompare(a.title);
}
return 0;
});
return (
<>
<TodoFilters
filter={filter}
setFilter={setFilter}
sort={sort}
setSort={setSort}
search={search}
setSearch={setSearch}
/>
<Table>
{/* ... (保留之前的表格结