建立一个简单的todo应用步伐(前端React;后端FastAPI;数据库MongoDB) ...

打印 上一主题 下一主题

主题 863|帖子 863|积分 2589

成果展示


     todo app demo
  
文件目次


backend

src

1. 创建python环境

  1. mkdir farm-todo
  2. cd farm-todo
复制代码
  1. mkdir frontend backend
  2. cd backend
复制代码
  1. python -m venv venv
  2. source venv/bin/activate
复制代码
  1. pip install "fastapi[all]" "motor[srv]" beanie aiostream
复制代码


  • 安装了FastAPI的所有可选依赖(包括一些web框架常用的依赖库,如uvicorn、pydantic等),使它能够运行完整的API功能。FastAPI是一个用于构建高性能API的Python框架,以异步编程和Python类型注解为底子,易于构建快速而灵活的API。
  • 安装Motor库并附带srv选项,确保能够使用MongoDB的SRV协议毗连字符串。Motor是MongoDB官方的异步驱动步伐,与FastAPI等异步框架兼容,可以异步地操作MongoDB数据库,提升处理大量并发请求的效率。
  • 安装了Beanie库,它是一个MongoDB的ORM(对象关系映射库),支持Pydantic模型并与Motor集成。Pydantic模型是一种在Python中使用的数据验证和数据结构界说工具。Beanie可以让开发者使用Python对象的形式来界说和操作MongoDB中的数据模型,减少了操作数据库的复杂性。
  • 安装了Aiostream库,它提供了异步流处理的工具。这个库可以资助管理和处理数据流,实用于处理多个并发任务的数据操作,使数据流管理更加方便。
  1. pip freeze > requirements.txt
复制代码
2. 创建2个文档:Dockerfile和pyproject.toml

Dockerfile: 界说一个用于运行Python应用步伐的Docker镜像。

  1. FROM python:3
  2. WORKDIR /usr/src/app
  3. COPY requirements.txt ./
  4. RUN pip install --no-cache-dir --upgrade -r ./requirements.txt
  5. EXPOSE 3001
  6. CMD [ "python", "./src/server.py" ]
复制代码


  • 使用Python 3的官方镜像作为底子镜像,提供了Python 3环境以及相干依赖,这样就不需要手动安装Python。
  • 指定容器内的工作目次为/usr/src/app。之后的所有下令都会在这个目次中实行。假如目次不存在,Docker会主动创建它。
  • 将当地目次中的requirements.txt文件复制到工作目次/usr/src/app中。requirements.txt包罗了应用步伐所需的Python依赖库列表。
  • 运行pip install下令,安装requirements.txt中列出的所有依赖项。--no-cache-dir选项可以减少镜像体积,因为不会缓存安装包。--upgrade确保安装的是最新版本的依赖。
  • 声明容器会监听3001端口,通常表现应用会在这个端口上提供服务。虽然EXPOSE不会直接启用端口访问,但它是一个声明性指示,用于告知运行时应该暴露的端口。
  • 指定容器启动时的默认下令,这里运行Python脚本./src/server.py。当容器启动时,这个下令会被实行,用于启动应用服务器。
这个Dockerfile的作用是创建一个Docker镜像,用于运行一个Python应用步伐,依赖于requirements.txt中列出的库,并在端口3001上监听请求。

pyproject.toml: 指定了pytest工具的一个选项,界说了测试时的Python路径。

  1. [tool.pytest.ini_options]
  2. pythonpath = "src"
复制代码


  • pytest是Python的一个常用测试框架,用于运行单位测试、集成测试等。
  • 将src目次添加到PYTHONPATH中。这样在运行测试时,pytest可以直接导入src目次中的模块,而不需要额外的路径设置。
3. 在backend的文件夹之下建立src文件夹,并创建 dal.py和 server.py

dal.py

  1. from bson import ObjectId
  2. # ObjectId是MongoDB的默认主键类型,通常用于在查询和处理数据库中的唯一标识符。
  3. from motor.motor_asyncio import AsyncIOMotorCollection
  4. # motor是MongoDB的异步驱动程序,用于与MongoDB进行异步交互。
  5. # AsyncIOMotorCollection是一个集合(collection)对象,提供了对MongoDB集合的异步操作。
  6. from pymongo import ReturnDocument
  7. # ReturnDocument 是pymongo中的常量,用于在更新操作时指定返回更新前或更新后的文档。
  8. from pydantic import BaseModel
  9. # Pydantic的BaseModel类是一个数据验证和数据结构定义工具。
  10. from uuid import uuid4
  11. class ListSummary(BaseModel):
  12.   id: str # 文档的唯一标识符,将MongoDB中的ObjectId转换为字符串。
  13.   name: str # 文档的名称字段。
  14.   item_count: int # 文档中项目的数量字段。
  15.   @staticmethod
  16.   def from_doc(doc) -> "ListSummary":
  17.       return ListSummary(
  18.           id=str(doc["_id"]),
  19.           name=doc["name"],
  20.           item_count=doc["item_count"],
  21.       )
  22. # 这是一个静态方法,接受一个MongoDB文档(字典类型)作为参数,
  23. # 并将该文档转换为ListSummary对象。doc通常是从MongoDB集合中查询返回的文档数据。
  24. class ToDoListItem(BaseModel):
  25.   id: str
  26.   label: str
  27.   checked: bool
  28.   @staticmethod
  29.   def from_doc(item) -> "ToDoListItem":
  30.       return ToDoListItem(
  31.           id=item["id"],
  32.           label=item["label"],
  33.           checked=item["checked"],
  34.       )
  35. # 将MongoDB中表示单个待办事项的文档(item)转换为ToDoListItem对象。
  36. # 它提取id、label和checked字段,并使用这些字段来创建一个ToDoListItem实例。
  37. class ToDoList(BaseModel):
  38.   id: str
  39.   name: str
  40.   items: list[ToDoListItem]
  41.   @staticmethod
  42.   def from_doc(doc) -> "ToDoList":
  43.       return ToDoList(
  44.           id=str(doc["_id"]),
  45.           name=doc["name"],
  46.           items=[ToDoListItem.from_doc(item) for item in doc["items"]],
  47.       )
  48. # 接收一个待办事项列表的MongoDB文档(doc),将其转换为ToDoList对象。
  49. # 方法从doc中提取_id和name字段,并将items字段中的每个待办事项文档
  50. # 通过ToDoListItem.from_doc方法转换为ToDoListItem对象,最终返回一个ToDoList实例。
  51. # 处理对MongoDB中待办事项数据的增删改查操作。
  52. class ToDoDAL:
  53.   def __init__(self, todo_collection: AsyncIOMotorCollection):
  54.       self._todo_collection = todo_collection
  55.   # 返回所有待办事项列表的简要信息(名称和项目数)
  56.   async def list_todo_lists(self, session=None):
  57.       async for doc in self._todo_collection.find(
  58.           {},
  59.           projection={
  60.               "name": 1,
  61.               "item_count": {"$size": "$items"},
  62.           },
  63.           sort={"name": 1},
  64.           session=session,
  65.       ):
  66.           yield ListSummary.from_doc(doc)
  67.   # 创建一个新的待办事项列表。
  68.   # 插入文档的_id字符串形式。
  69.   async def create_todo_list(self, name: str, session=None) -> str:
  70.       response = await self._todo_collection.insert_one(
  71.           {"name": name, "items": []},
  72.           session=session,
  73.       )
  74.       return str(response.inserted_id)
  75.   # 根据ID获取特定待办事项列表的完整信息。
  76.   async def get_todo_list(self, id: str | ObjectId, session=None) -> ToDoList:
  77.       doc = await self._todo_collection.find_one(
  78.           {"_id": ObjectId(id)},
  79.           session=session,
  80.       )
  81.       return ToDoList.from_doc(doc)
  82.   # 根据ID删除一个待办事项列表。
  83.   async def delete_todo_list(self, id: str | ObjectId, session=None) -> bool:
  84.       response = await self._todo_collection.delete_one(
  85.           {"_id": ObjectId(id)},
  86.           session=session,
  87.       )
  88.       return response.deleted_count == 1
  89.   # 向指定待办事项列表中添加新项目
  90.   async def create_item(
  91.       self,
  92.       id: str | ObjectId,
  93.       label: str,
  94.       session=None,
  95.   ) -> ToDoList | None:
  96.       result = await self._todo_collection.find_one_and_update(
  97.           {"_id": ObjectId(id)},
  98.           {
  99.               "$push": {
  100.                   "items": {
  101.                       "id": uuid4().hex,
  102.                       "label": label,
  103.                       "checked": False,
  104.                   }
  105.               }
  106.           },
  107.           session=session,
  108.           return_document=ReturnDocument.AFTER,
  109.       )
  110.       if result:
  111.           return ToDoList.from_doc(result)
  112.   # 设置待办事项item的checked状态。
  113.   async def set_checked_state(
  114.       self,
  115.       doc_id: str | ObjectId,
  116.       item_id: str,
  117.       checked_state: bool,
  118.       session=None,
  119.   ) -> ToDoList | None:
  120.       result = await self._todo_collection.find_one_and_update(
  121.           {"_id": ObjectId(doc_id), "items.id": item_id},
  122.           {"$set": {"items.$.checked": checked_state}},
  123.           session=session,
  124.           return_document=ReturnDocument.AFTER,
  125.       )
  126.       if result:
  127.           return ToDoList.from_doc(result)
  128.   # 从指定待办事项列表中删除一个项目。
  129.   async def delete_item(
  130.       self,
  131.       doc_id: str | ObjectId,
  132.       item_id: str,
  133.       session=None,
  134.   ) -> ToDoList | None:
  135.       result = await self._todo_collection.find_one_and_update(
  136.           {"_id": ObjectId(doc_id)},
  137.           {"$pull": {"items": {"id": item_id}}},
  138.           session=session,
  139.           return_document=ReturnDocument.AFTER,
  140.       )
  141.       if result:
  142.           return ToDoList.from_doc(result)
复制代码
server.py

  1. from contextlib import asynccontextmanager
  2. from datetime import datetime
  3. import os
  4. import sys
  5. from bson import ObjectId
  6. from fastapi import FastAPI, status
  7. from motor.motor_asyncio import AsyncIOMotorClient
  8. # 是MongoDB异步客户端(motor库)来连接数据库。
  9. from pydantic import BaseModel
  10. import uvicorn # 是ASGI服务器,用于运行FastAPI应用。
  11. from dal import ToDoDAL, ListSummary, ToDoList
  12. COLLECTION_NAME = "todo_lists"
  13. MONGODB_URI = os.environ["MONGODB_URI"]
  14. # 从环境变量中获取MongoDB的URI连接字符串。
  15. DEBUG = os.environ.get("DEBUG", "").strip().lower() in {"1", "true", "on", "yes"}
  16. # 从环境变量读取调试模式,接受多种True的写法。
  17. # 这是一个异步上下文管理器,管理FastAPI应用的启动和关闭。
  18. @asynccontextmanager
  19. async def lifespan(app: FastAPI):
  20.     # Startup:
  21.     client = AsyncIOMotorClient(MONGODB_URI)
  22.     # 创建MongoDB异步客户端实例,连接到数据库。
  23.    
  24.     database = client.get_default_database()
  25.     # 获取MongoDB数据库实例,默认使用连接字符串中的数据库名称。
  26.         # ping命令用于测试MongoDB连接是否正常。pong["ok"] == 1表示连接成功。
  27.     pong = await database.command("ping")
  28.     if int(pong["ok"]) != 1:
  29.         raise Exception("Cluster connection is not okay!")
  30.     todo_lists = database.get_collection(COLLECTION_NAME)
  31.     # 获取待办事项集合todo_lists。
  32.    
  33.     app.todo_dal = ToDoDAL(todo_lists)
  34.     # 将ToDoDAL实例附加到FastAPI应用实例上,便于其他模块使用。
  35.     # Yield back to FastAPI Application:
  36.     yield
  37.     # 暂停上下文管理器,将控制权交给应用,以继续运行其他操作。
  38.     # Shutdown:
  39.     client.close()
  40.     # 应用关闭时,自动关闭MongoDB连接以释放资源。
  41. app = FastAPI(lifespan=lifespan, debug=DEBUG)
  42. # 使用lifespan管理器(在应用启动和关闭时管理数据库连接)。
  43. # debug=DEBUG设定调试模式,使得在调试时更容易看到错误和详细信息。
  44. # 异步获取所有待办事项列表的摘要信息。
  45. @app.get("/api/lists")
  46. async def get_all_lists() -> list[ListSummary]:
  47.     return [i async for i in app.todo_dal.list_todo_lists()]
  48. # 用于创建新的待办事项列表,包含列表名称。
  49. class NewList(BaseModel):
  50.     name: str
  51. # 用于创建操作成功后的响应,包含列表的id和name。
  52. class NewListResponse(BaseModel):
  53.     id: str
  54.     name: str
  55. # 接受NewList格式的数据,创建新的待办事项列表。
  56. @app.post("/api/lists", status_code=status.HTTP_201_CREATED)
  57. async def create_todo_list(new_list: NewList) -> NewListResponse:
  58.     return NewListResponse(
  59.         id=await app.todo_dal.create_todo_list(new_list.name),
  60.         name=new_list.name,
  61.     )
  62. # 使用列表的唯一标识符list_id获取单个待办事项列表。
  63. # 调用get_todo_list方法,返回一个包含列表详细信息的ToDoList对象。
  64. @app.get("/api/lists/{list_id}")
  65. async def get_list(list_id: str) -> ToDoList:
  66.     """Get a single to-do list"""
  67.     return await app.todo_dal.get_todo_list(list_id)
  68. # 根据list_id删除指定的待办事项列表。
  69. # 调用delete_todo_list方法,返回布尔值表示删除是否成功。
  70. @app.delete("/api/lists/{list_id}")
  71. async def delete_list(list_id: str) -> bool:
  72.     return await app.todo_dal.delete_todo_list(list_id)
  73. # 用于添加新待办事项的数据模型,包含label字段,表示待办事项的标签(描述)。
  74. class NewItem(BaseModel):
  75.     label: str
  76. # 用于返回新添加项的响应模型,包含id和label字段。
  77. class NewItemResponse(BaseModel):
  78.     id: str
  79.     label: str
  80. # 用于向指定的待办事项列表(list_id)中添加新待办事项items。
  81. @app.post(
  82.     "/api/lists/{list_id}/items/",
  83.     status_code=status.HTTP_201_CREATED,
  84. )
  85. # 调用create_item方法,将list_id和new_item.label传入。
  86. # 返回更新后的完整待办事项列表ToDoList,包含新的待办事项item。
  87. async def create_item(list_id: str, new_item: NewItem) -> ToDoList:
  88.     return await app.todo_dal.create_item(list_id, new_item.label)
  89. # 根据list_id和item_id删除指定待办事项列表中的特定项。
  90. # 返回更新后的ToDoList对象,删除项后更新的列表。
  91. @app.delete("/api/lists/{list_id}/items/{item_id}")
  92. async def delete_item(list_id: str, item_id: str) -> ToDoList:
  93.     return await app.todo_dal.delete_item(list_id, item_id)
  94. # 定义更新待办事项item的完成状态所需的数据模型。
  95. # 包含item_id(待办事项item的唯一标识符)和checked_state(布尔值,表示是否已完成)字段。
  96. class ToDoItemUpdate(BaseModel):
  97.     item_id: str
  98.     checked_state: bool
  99. # 用于更新指定待办事项列表中某个待办事项item的完成状态。
  100. # 返回更新后的ToDoList对象,反映状态变更后的完整列表。
  101. @app.patch("/api/lists/{list_id}/checked_state")
  102. async def set_checked_state(list_id: str, update: ToDoItemUpdate) -> ToDoList:
  103.     return await app.todo_dal.set_checked_state(
  104.         list_id, update.item_id, update.checked_state
  105.     )
  106. class DummyResponse(BaseModel):
  107.     id: str
  108.     when: datetime
  109. @app.get("/api/dummy")
  110. async def get_dummy() -> DummyResponse:
  111.     return DummyResponse(
  112.         id=str(ObjectId()),
  113.         when=datetime.now(),
  114.     )
  115. # 使用ObjectId()生成一个新的唯一标识符并转换为字符串,赋值给id。
  116. # 每个文档在MongoDB数据库中都有一个_id字段,默认情况下,它的值是一个ObjectId。
  117. # 使用datetime.now()获取当前时间并赋值给when。
  118. def main(argv=sys.argv[1:]):
  119.     try:
  120.             # 使用uvicorn来启动FastAPI应用
  121.         uvicorn.run("server:app", host="0.0.0.0", port=3001, reload=DEBUG)
  122.         # host="0.0.0.0":让应用在所有可用的网络接口上监听。
  123.         # reload=DEBUG:如果DEBUG为True,在代码更改时自动重载应用(适合开发阶段)。
  124.     except KeyboardInterrupt:
  125.         pass
  126.         # 使用try...except来捕获KeyboardInterrupt异常,以便在按下Ctrl+C时优雅地停止服务。
  127. if __name__ == "__main__":
  128.     main()
复制代码
4. 在MONGODB上建立一个cluster,建立.env文件(与backend文件夹同一级),在.env文件中贴上MONGODB_URI,并在问号前加上todo。

5. 创建compose.yaml(与backend文件夹同一级)

  1. name: todo-app
  2. services:
  3.   nginx:
  4.     image: nginx:1.17
  5.     volumes:
  6.       - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
  7.     ports:
  8.       - 8000:80
  9.     depends_on:
  10.       - backend
  11.       - frontend
  12.   frontend:
  13.     image: "node:22"
  14.     user: "node"
  15.     working_dir: /home/node/app
  16.     environment:
  17.       - NODE_ENV=development
  18.       - WDS_SOCKET_PORT=0
  19.     volumes:
  20.       - ./frontend/:/home/node/app
  21.     expose:
  22.       - "3000"
  23.     ports:
  24.       - "3000:3000"
  25.     command: "npm start"
  26.   backend:
  27.     image: todo-app/backend
  28.     build: ./backend
  29.     volumes:
  30.       - ./backend/:/usr/src/app
  31.     expose:
  32.       - "3001"
  33.     ports:
  34.       - "8001:3001"
  35.     command: "python src/server.py"
  36.     environment:
  37.       - DEBUG=true
  38.     env_file:
  39.       - path: ./.env
  40.         required: true
复制代码
nginx 服务


  • 使用 Nginx 1.17 版本的官方镜像。
  • 挂载当地 nginx.conf 设置文件到容器中,以覆盖默认设置。使 Nginx 可以按照自界说设置处理请求。
  • 将容器的 80 端口映射到主机的 8000 端口。这样外部可以通过 localhost:8000 访问 Nginx。
  • 指定 backend 和 frontend 是 Nginx 服务的依赖项。Nginx 启动时,这两个服务应该先启动。
frontend 服务


  • 使用 node 版本为 22 的镜像。
  • 以 node 用户身份运行,克制使用 root 权限以增强安全性。
  • volumes:将当地的 ./frontend/ 目次挂载到容器的工作目次中,以便同步代码改动。
  • 暴露内部 3000 端口给其他 Docker 容器访问。
  • 将当地端口 3000 映射到容器的 3000 端口,使开发环境可以访问前端应用。
  • command: "npm start":启动前端应用的下令,通常运行开发服务器。
backend 服务


  • build: ./backend:使用 ./backend 目次中的 Dockerfile 来构建该服务的镜像。
  • volumes:将当地的 ./backend/ 目次挂载到容器中的 /usr/src/app,以便实时更新代码。
  • ports:将主机的 8001 端口映射到容器的 3001 端口,使外部可以访问后端 API。
  • environment:设置环境变量,如 DEBUG=true 开启调试模式。
6. 创建nginx文件夹(与backend文件夹同一级),建立nginx.conf文件。



  • 静态资源或前端请求通过 / 路由到 frontend 服务。
  • 后端 API 请求通过 /api 路由到 backend 服务的 API。
  1. server {
  2.     listen 80;
  3.     server_name farm_intro;
  4.     location / {
  5.         proxy_pass http://frontend:3000;
  6.         proxy_set_header Upgrade $http_upgrade;
  7.         proxy_set_header Connection "upgrade";
  8.     }
  9.     location /api {
  10.         proxy_pass http://backend:3001/api;
  11.     }
  12. }
复制代码


  • listen 80:指定服务器监听在端口 80,这是 HTTP 的默认端口。
  • proxy_pass http://frontend:3000:将请求转发给名为 frontend 的服务,其内部端口为 3000。这里假设 frontend 是一个运行在 Docker Compose 中的 Node.js 服务。
7. 切换到frontend文件夹

  1. npx create-react-app .
  2. npm install axios react-icons
复制代码


  • create-react-app 是一个官方的 React 脚手架工具,用于快速创建尺度的 React 项目结构。
  • axios 是一个用于发送 HTTP 请求的库,常用于从后端 API 获取数据。
  • react-icons 提供了大量常用的图标,可以很方便地在 React 组件中使用。
8. 更新App.js文件

  1. import { useEffect, useState } from "react";
  2. import axios from "axios";
  3. import "./App.css";
  4. import ListToDoLists from "./ListTodoLists";
  5. import ToDoList from "./ToDoList";
  6. function App() {
  7.   const [listSummaries, setListSummaries] = useState(null);
  8.   // 存储从 /api/lists 获取的所有待办事项列表的摘要。
  9.   
  10.   const [selectedItem, setSelectedItem] = useState(null);
  11.   // 此变量用于存储当前选中的待办事项列表的 ID。
  12.   useEffect(() => {
  13.     reloadData().catch(console.error);
  14.   }, []);
  15.   // useEffect 在组件挂载时调用 reloadData 函数,从服务器加载待办事项列表的数据。
  16.   async function reloadData() {
  17.     const response = await axios.get("/api/lists");
  18.     const data = await response.data;
  19.     setListSummaries(data);
  20.   }
  21.   // 通过 axios.get 向 /api/lists 发送请求,从服务器获取所有待办事项列表的摘要信息。
  22.   function handleNewToDoList(newName) {
  23.     const updateData = async () => {
  24.       const newListData = {
  25.         name: newName,
  26.       };
  27.       await axios.post(`/api/lists`, newListData);
  28.       reloadData().catch(console.error);
  29.     };
  30.     updateData();
  31.   }
  32.   // 接受 newName 作为新列表的名称,发送 POST 请求到 /api/lists,然后刷新数据以更新界面。
  33.   function handleDeleteToDoList(id) {
  34.     const updateData = async () => {
  35.       await axios.delete(`/api/lists/${id}`);
  36.       reloadData().catch(console.error);
  37.     };
  38.     updateData();
  39.   }
  40.   // 根据 id 向 /api/lists/{id} 发送 DELETE 请求,然后刷新数据。
  41.   function handleSelectList(id) {
  42.     console.log("Selecting item", id);
  43.     setSelectedItem(id);
  44.     // 设置 selectedItem 为选中的列表的 id。
  45.   }
  46.   function backToList() {
  47.     setSelectedItem(null);
  48.     // 清除 selectedItem 的值,将视图切换回所有待办事项列表的界面。
  49.     reloadData().catch(console.error);
  50.   }
  51.   if (selectedItem === null) {
  52.     return (
  53.       <div className="App">
  54.         <ListToDoLists
  55.           listSummaries={listSummaries}
  56.           handleSelectList={handleSelectList}
  57.           handleNewToDoList={handleNewToDoList}
  58.           handleDeleteToDoList={handleDeleteToDoList}
  59.         />
  60.       </div>
  61.     );
  62.   } else {
  63.     return (
  64.       <div className="App">
  65.         <ToDoList listId={selectedItem} handleBackButton={backToList} />
  66.       </div>
  67.     );
  68.   }
  69. }
  70. export default App;
复制代码
9. 建立ListTodoLists.js文件

  1. import "./ListTodoLists.css";
  2. import { useRef } from "react";
  3. import { BiSolidTrash } from "react-icons/bi"; // 引入一个垃圾桶图标,用于删除功能的按钮。
  4. function ListToDoLists({
  5.   listSummaries, // 待办事项列表的概要信息数组。
  6.   handleSelectList, // 处理用户选择特定待办事项列表的函数。
  7.   handleNewToDoList, // 处理创建新待办事项列表的函数。
  8.   handleDeleteToDoList, // 处理删除指定待办事项列表的函数。
  9. }) {
  10.   const labelRef = useRef(); // 用于访问新待办事项列表名称的输入框值。
  11.   if (listSummaries === null) {
  12.     return <div className="ListToDoLists loading">Loading to-do lists ...</div>;
  13.     // 如果 listSummaries 为 null,组件会显示“Loading to-do lists ...”字样,表示数据正在加载。
  14.   } else if (listSummaries.length === 0) {
  15.     return (
  16.       <div className="ListToDoLists">
  17.         <div className="box">
  18.         <label>
  19.           New To-Do List:&nbsp;
  20.           <input id={labelRef} type="text" />
  21.         </label>
  22.         <button
  23.           onClick={() =>
  24.             handleNewToDoList(document.getElementById(labelRef).value)
  25.             // 提交新列表名称。
  26.           }
  27.         >
  28.           New
  29.         </button>
  30.         </div>
  31.         <p>There are no to-do lists!</p>
  32.       </div>
  33.     );
  34.   }
  35.   return (
  36.     <div className="ListToDoLists">
  37.       <h1>All To-Do Lists</h1>
  38.       <div className="box">
  39.         <label>
  40.           New To-Do List:&nbsp;
  41.           <input id={labelRef} type="text" />
  42.         </label>
  43.         <button
  44.           onClick={() =>
  45.             handleNewToDoList(document.getElementById(labelRef).value)
  46.           }
  47.         >
  48.           New
  49.         </button>
  50.         // 提供添加新待办事项列表的功能,输入新名称并点击“New”按钮。
  51.       </div>
  52.       {listSummaries.map((summary) => {
  53.         return (
  54.           <div
  55.             key={summary.id}
  56.             className="summary"
  57.             onClick={() => handleSelectList(summary.id)}
  58.             // 条目点击时会调用 handleSelectList 并传入 summary.id,
  59.             // 从而选择该列表并展示详细信息。
  60.           >
  61.             <span className="name">{summary.name} </span>
  62.             <span className="count">({summary.item_count} items)</span>
  63.             <span className="flex"></span>
  64.             <span
  65.               className="trash"
  66.               onClick={(evt) => {
  67.                 evt.stopPropagation(); // 阻止事件冒泡到父元素
  68.                 handleDeleteToDoList(summary.id);
  69.               }}
  70.             >
  71.               <BiSolidTrash />
  72.               // <BiSolidTrash /> 是一个垃圾桶图标,
  73.               // 点击时会触发 handleDeleteToDoList 删除该条目。
  74.             </span>
  75.           </div>
  76.         );
  77.       })}
  78.     </div>
  79.   );
  80. }
  81. export default ListToDoLists;
复制代码


10. 建立ListTodoLists.css文件

  1. .ListToDoLists .summary {
  2.     border: 1px solid lightgray;
  3.     padding: 1em;
  4.     margin: 1em;
  5.     cursor: pointer;
  6.     display: flex;
  7.   }
  8.   
  9.   .ListToDoLists .count {
  10.     padding-left: 1ex;
  11.     color: blueviolet;
  12.     font-size: 92%;
  13.   }
复制代码


  • border: 1px solid lightgray;:设置浅灰色的 1 像素边框,给每个条目增长视觉边界。
  • cursor: pointer;:将鼠标悬停在条目上时体现为指针(手型光标),提示用户条目是可点击的。
  • display: flex;:使用 flex 布局,将条目内的内容(如名称、项目计数、垃圾桶图标等)按水平方向排列。
  • .ListToDoLists .count该选择器为每个待办事项列表的项目计数文本提供样式。
  • color: blueviolet;:将文本颜色设置为蓝紫色,以突出体现项目计数。
11. 建立ToDoList.js文件

  1. import "./ToDoList.css";
  2. import { useEffect, useState, useRef } from "react";
  3. import axios from "axios";
  4. import { BiSolidTrash } from "react-icons/bi";
  5. function ToDoList({ listId, handleBackButton }) {
  6.   let labelRef = useRef();
  7.   const [listData, setListData] = useState(null);
  8. // 获取列表数据
  9.   useEffect(() => {
  10.     const fetchData = async () => {
  11.       const response = await axios.get(`/api/lists/${listId}`);
  12.       const newData = await response.data;
  13.       setListData(newData);
  14.     };
  15.     fetchData();
  16.   }, [listId]);
  17. // 创建新待办事项
  18.   function handleCreateItem(label) {
  19.     const updateData = async () => {
  20.       const response = await axios.post(`/api/lists/${listData.id}/items/`, {
  21.         label: label,
  22.       });
  23.       setListData(await response.data);
  24.     };
  25.     updateData();
  26.   }
  27. // 删除待办事项
  28.   function handleDeleteItem(id) {
  29.     const updateData = async () => {
  30.       const response = await axios.delete(
  31.         `/api/lists/${listData.id}/items/${id}`
  32.       );
  33.       setListData(await response.data);
  34.     };
  35.     updateData();
  36.   }
  37. // 更新项目状态
  38.   function handleCheckToggle(itemId, newState) {
  39.     const updateData = async () => {
  40.       const response = await axios.patch(
  41.         `/api/lists/${listData.id}/checked_state`,
  42.         {
  43.           item_id: itemId,
  44.           checked_state: newState,
  45.         }
  46.       );
  47.       setListData(await response.data);
  48.     };
  49.     updateData();
  50.   }
  51.   if (listData === null) {
  52.     return (
  53.       <div className="ToDoList loading">
  54.         <button className="back" onClick={handleBackButton}>
  55.           Back
  56.         </button>
  57.         Loading to-do list ...
  58.       </div>
  59.     );
  60.   }
  61.   return (
  62.     <div className="ToDoList">
  63.       <button className="back" onClick={handleBackButton}>
  64.         Back
  65.       </button>
  66.       <h1>List: {listData.name}</h1>
  67.       <div className="box">
  68.         <label>
  69.           New Item:&nbsp;
  70.           <input id={labelRef} type="text" />
  71.         </label>
  72.         <button
  73.           onClick={() =>
  74.             handleCreateItem(document.getElementById(labelRef).value)
  75.           }
  76.         >
  77.           New
  78.         </button>
  79.         // 用户可在输入框输入内容后点击 New 按钮添加项目。
  80.       </div>
  81.       {listData.items.length > 0 ? (
  82.         listData.items.map((item) => {
  83.           return (
  84.             <div
  85.               key={item.id}
  86.               className={item.checked ? "item checked" : "item"}
  87.               onClick={() => handleCheckToggle(item.id, !item.checked)}
  88.             >
  89.               <span>{item.checked ? "✅" : "⬜️"} </span>
  90.               // 复选框:使用 ✅ 和 ⬜️ 表示项目的完成状态,点击切换状态。
  91.               <span className="label">{item.label} </span>
  92.               <span className="flex"></span>
  93.               <span
  94.                 className="trash"
  95.                 onClick={(evt) => {
  96.                   evt.stopPropagation();
  97.                   handleDeleteItem(item.id);
  98.                 }}
  99.               >
  100.                 <BiSolidTrash /> // 垃圾桶图标:点击删除项目。
  101.               </span>
  102.             </div>
  103.           );
  104.         })
  105.       ) : (
  106.         <div className="box">There are currently no items.</div>
  107.       )}
  108.     </div>
  109.   );
  110. }
  111. export default ToDoList;
复制代码



12. 建立ToDoList.css文件

  1. .ToDoList .back {
  2.     margin: 0 1em;
  3.     padding: 1em;
  4.     float: left;
  5.   }
  6.   
  7.   .ToDoList .item {
  8.     border: 1px solid lightgray;
  9.     padding: 1em;
  10.     margin: 1em;
  11.     cursor: pointer;
  12.     display: flex;
  13.   }
  14.   
  15.   .ToDoList .label {
  16.     margin-left: 1ex;
  17.   }
  18.   
  19.   .ToDoList .checked .label {
  20.     text-decoration: line-through;
  21.     color: lightgray;
  22.   }
复制代码


  • border: 1px solid lightgray;:为每个待办事项条目添加一个浅灰色边框,分隔不同条目。
  • text-decoration: line-through;:为已完成的项目添加删除线,表现项目已完成。
  • color: lightgray;:将文本颜色设置为浅灰色,降低视觉优先级,使已完成的项目显着区分于未完成的项目。
13. 更新index.css文件

  1. html, body {
  2.   margin: 0;
  3.   font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
  4.     'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
  5.     sans-serif;
  6.   -webkit-font-smoothing: antialiased;
  7.   -moz-osx-font-smoothing: grayscale;
  8.   font-size: 12pt;
  9. }
  10. input, button {
  11.   font-size: 1em;
  12. }
  13. code {
  14.   font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
  15.     monospace;
  16. }
  17. .box {
  18.     border: 1px solid lightgray;
  19.     padding: 1em;
  20.     margin: 1em;
  21. }
  22. .flex {
  23.   flex: 1;
  24. }
复制代码
14. 构建服务的镜像

  1. docker-compose up --build
复制代码
15. 浏览器中使用todo应用步伐

localhost:8000

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

没腿的鸟

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表