好的, 让我们详细规划第 6 部分: Todo 功能实现。我们将这部分分为 3 天, 每天专注于特定的 Todo 相关功能和组件。

第 6 部分: Todo 功能实现 (3 天)

Day 1: Todo 列表和创建功能

  1. 更新 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>
  );
};
  1. 更新 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>
  );
};
  1. 更新 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 编辑和完成状态切换功能

  1. 创建 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>
  );
};
  1. 更新 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: 添加过滤和排序功能

  1. 创建 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>
  );
};
  1. 更新 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>
        {/* ... (保留之前的表格结