ToB企服应用市场:ToB评测及商务社交产业平台

标题: 建立一个简单的todo应用步伐(前端React;后端FastAPI;数据库MongoDB) [打印本页]

作者: 没腿的鸟    时间: 2024-11-26 08:03
标题: 建立一个简单的todo应用步伐(前端React;后端FastAPI;数据库MongoDB)
成果展示


     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
复制代码

  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" ]
复制代码

这个Dockerfile的作用是创建一个Docker镜像,用于运行一个Python应用步伐,依赖于requirements.txt中列出的库,并在端口3001上监听请求。

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

  1. [tool.pytest.ini_options]
  2. pythonpath = "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 服务

frontend 服务

backend 服务

6. 创建nginx文件夹(与backend文件夹同一级),建立nginx.conf文件。


  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. }
复制代码

7. 切换到frontend文件夹

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

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.   }
复制代码

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.   }
复制代码

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企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4