成果展示
todo app demo
文件目次
backend
src
1. 创建python环境
- mkdir farm-todo
- cd farm-todo
复制代码- mkdir frontend backend
- cd backend
复制代码- python -m venv venv
- source venv/bin/activate
复制代码- 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库,它提供了异步流处理的工具。这个库可以资助管理和处理数据流,实用于处理多个并发任务的数据操作,使数据流管理更加方便。
- pip freeze > requirements.txt
复制代码 2. 创建2个文档:Dockerfile和pyproject.toml
Dockerfile: 界说一个用于运行Python应用步伐的Docker镜像。
- FROM python:3
- WORKDIR /usr/src/app
- COPY requirements.txt ./
- RUN pip install --no-cache-dir --upgrade -r ./requirements.txt
- EXPOSE 3001
- 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路径。
- [tool.pytest.ini_options]
- pythonpath = "src"
复制代码
- pytest是Python的一个常用测试框架,用于运行单位测试、集成测试等。
- 将src目次添加到PYTHONPATH中。这样在运行测试时,pytest可以直接导入src目次中的模块,而不需要额外的路径设置。
3. 在backend的文件夹之下建立src文件夹,并创建 dal.py和 server.py
dal.py
- from bson import ObjectId
- # ObjectId是MongoDB的默认主键类型,通常用于在查询和处理数据库中的唯一标识符。
- from motor.motor_asyncio import AsyncIOMotorCollection
- # motor是MongoDB的异步驱动程序,用于与MongoDB进行异步交互。
- # AsyncIOMotorCollection是一个集合(collection)对象,提供了对MongoDB集合的异步操作。
- from pymongo import ReturnDocument
- # ReturnDocument 是pymongo中的常量,用于在更新操作时指定返回更新前或更新后的文档。
- from pydantic import BaseModel
- # Pydantic的BaseModel类是一个数据验证和数据结构定义工具。
- from uuid import uuid4
- class ListSummary(BaseModel):
- id: str # 文档的唯一标识符,将MongoDB中的ObjectId转换为字符串。
- name: str # 文档的名称字段。
- item_count: int # 文档中项目的数量字段。
- @staticmethod
- def from_doc(doc) -> "ListSummary":
- return ListSummary(
- id=str(doc["_id"]),
- name=doc["name"],
- item_count=doc["item_count"],
- )
- # 这是一个静态方法,接受一个MongoDB文档(字典类型)作为参数,
- # 并将该文档转换为ListSummary对象。doc通常是从MongoDB集合中查询返回的文档数据。
- class ToDoListItem(BaseModel):
- id: str
- label: str
- checked: bool
- @staticmethod
- def from_doc(item) -> "ToDoListItem":
- return ToDoListItem(
- id=item["id"],
- label=item["label"],
- checked=item["checked"],
- )
- # 将MongoDB中表示单个待办事项的文档(item)转换为ToDoListItem对象。
- # 它提取id、label和checked字段,并使用这些字段来创建一个ToDoListItem实例。
- class ToDoList(BaseModel):
- id: str
- name: str
- items: list[ToDoListItem]
- @staticmethod
- def from_doc(doc) -> "ToDoList":
- return ToDoList(
- id=str(doc["_id"]),
- name=doc["name"],
- items=[ToDoListItem.from_doc(item) for item in doc["items"]],
- )
- # 接收一个待办事项列表的MongoDB文档(doc),将其转换为ToDoList对象。
- # 方法从doc中提取_id和name字段,并将items字段中的每个待办事项文档
- # 通过ToDoListItem.from_doc方法转换为ToDoListItem对象,最终返回一个ToDoList实例。
- # 处理对MongoDB中待办事项数据的增删改查操作。
- class ToDoDAL:
- def __init__(self, todo_collection: AsyncIOMotorCollection):
- self._todo_collection = todo_collection
- # 返回所有待办事项列表的简要信息(名称和项目数)
- async def list_todo_lists(self, session=None):
- async for doc in self._todo_collection.find(
- {},
- projection={
- "name": 1,
- "item_count": {"$size": "$items"},
- },
- sort={"name": 1},
- session=session,
- ):
- yield ListSummary.from_doc(doc)
- # 创建一个新的待办事项列表。
- # 插入文档的_id字符串形式。
- async def create_todo_list(self, name: str, session=None) -> str:
- response = await self._todo_collection.insert_one(
- {"name": name, "items": []},
- session=session,
- )
- return str(response.inserted_id)
- # 根据ID获取特定待办事项列表的完整信息。
- async def get_todo_list(self, id: str | ObjectId, session=None) -> ToDoList:
- doc = await self._todo_collection.find_one(
- {"_id": ObjectId(id)},
- session=session,
- )
- return ToDoList.from_doc(doc)
- # 根据ID删除一个待办事项列表。
- async def delete_todo_list(self, id: str | ObjectId, session=None) -> bool:
- response = await self._todo_collection.delete_one(
- {"_id": ObjectId(id)},
- session=session,
- )
- return response.deleted_count == 1
- # 向指定待办事项列表中添加新项目
- async def create_item(
- self,
- id: str | ObjectId,
- label: str,
- session=None,
- ) -> ToDoList | None:
- result = await self._todo_collection.find_one_and_update(
- {"_id": ObjectId(id)},
- {
- "$push": {
- "items": {
- "id": uuid4().hex,
- "label": label,
- "checked": False,
- }
- }
- },
- session=session,
- return_document=ReturnDocument.AFTER,
- )
- if result:
- return ToDoList.from_doc(result)
- # 设置待办事项item的checked状态。
- async def set_checked_state(
- self,
- doc_id: str | ObjectId,
- item_id: str,
- checked_state: bool,
- session=None,
- ) -> ToDoList | None:
- result = await self._todo_collection.find_one_and_update(
- {"_id": ObjectId(doc_id), "items.id": item_id},
- {"$set": {"items.$.checked": checked_state}},
- session=session,
- return_document=ReturnDocument.AFTER,
- )
- if result:
- return ToDoList.from_doc(result)
- # 从指定待办事项列表中删除一个项目。
- async def delete_item(
- self,
- doc_id: str | ObjectId,
- item_id: str,
- session=None,
- ) -> ToDoList | None:
- result = await self._todo_collection.find_one_and_update(
- {"_id": ObjectId(doc_id)},
- {"$pull": {"items": {"id": item_id}}},
- session=session,
- return_document=ReturnDocument.AFTER,
- )
- if result:
- return ToDoList.from_doc(result)
复制代码 server.py
- from contextlib import asynccontextmanager
- from datetime import datetime
- import os
- import sys
- from bson import ObjectId
- from fastapi import FastAPI, status
- from motor.motor_asyncio import AsyncIOMotorClient
- # 是MongoDB异步客户端(motor库)来连接数据库。
- from pydantic import BaseModel
- import uvicorn # 是ASGI服务器,用于运行FastAPI应用。
- from dal import ToDoDAL, ListSummary, ToDoList
- COLLECTION_NAME = "todo_lists"
- MONGODB_URI = os.environ["MONGODB_URI"]
- # 从环境变量中获取MongoDB的URI连接字符串。
- DEBUG = os.environ.get("DEBUG", "").strip().lower() in {"1", "true", "on", "yes"}
- # 从环境变量读取调试模式,接受多种True的写法。
- # 这是一个异步上下文管理器,管理FastAPI应用的启动和关闭。
- @asynccontextmanager
- async def lifespan(app: FastAPI):
- # Startup:
- client = AsyncIOMotorClient(MONGODB_URI)
- # 创建MongoDB异步客户端实例,连接到数据库。
-
- database = client.get_default_database()
- # 获取MongoDB数据库实例,默认使用连接字符串中的数据库名称。
- # ping命令用于测试MongoDB连接是否正常。pong["ok"] == 1表示连接成功。
- pong = await database.command("ping")
- if int(pong["ok"]) != 1:
- raise Exception("Cluster connection is not okay!")
- todo_lists = database.get_collection(COLLECTION_NAME)
- # 获取待办事项集合todo_lists。
-
- app.todo_dal = ToDoDAL(todo_lists)
- # 将ToDoDAL实例附加到FastAPI应用实例上,便于其他模块使用。
- # Yield back to FastAPI Application:
- yield
- # 暂停上下文管理器,将控制权交给应用,以继续运行其他操作。
- # Shutdown:
- client.close()
- # 应用关闭时,自动关闭MongoDB连接以释放资源。
- app = FastAPI(lifespan=lifespan, debug=DEBUG)
- # 使用lifespan管理器(在应用启动和关闭时管理数据库连接)。
- # debug=DEBUG设定调试模式,使得在调试时更容易看到错误和详细信息。
- # 异步获取所有待办事项列表的摘要信息。
- @app.get("/api/lists")
- async def get_all_lists() -> list[ListSummary]:
- return [i async for i in app.todo_dal.list_todo_lists()]
- # 用于创建新的待办事项列表,包含列表名称。
- class NewList(BaseModel):
- name: str
- # 用于创建操作成功后的响应,包含列表的id和name。
- class NewListResponse(BaseModel):
- id: str
- name: str
- # 接受NewList格式的数据,创建新的待办事项列表。
- @app.post("/api/lists", status_code=status.HTTP_201_CREATED)
- async def create_todo_list(new_list: NewList) -> NewListResponse:
- return NewListResponse(
- id=await app.todo_dal.create_todo_list(new_list.name),
- name=new_list.name,
- )
- # 使用列表的唯一标识符list_id获取单个待办事项列表。
- # 调用get_todo_list方法,返回一个包含列表详细信息的ToDoList对象。
- @app.get("/api/lists/{list_id}")
- async def get_list(list_id: str) -> ToDoList:
- """Get a single to-do list"""
- return await app.todo_dal.get_todo_list(list_id)
- # 根据list_id删除指定的待办事项列表。
- # 调用delete_todo_list方法,返回布尔值表示删除是否成功。
- @app.delete("/api/lists/{list_id}")
- async def delete_list(list_id: str) -> bool:
- return await app.todo_dal.delete_todo_list(list_id)
- # 用于添加新待办事项的数据模型,包含label字段,表示待办事项的标签(描述)。
- class NewItem(BaseModel):
- label: str
- # 用于返回新添加项的响应模型,包含id和label字段。
- class NewItemResponse(BaseModel):
- id: str
- label: str
- # 用于向指定的待办事项列表(list_id)中添加新待办事项items。
- @app.post(
- "/api/lists/{list_id}/items/",
- status_code=status.HTTP_201_CREATED,
- )
- # 调用create_item方法,将list_id和new_item.label传入。
- # 返回更新后的完整待办事项列表ToDoList,包含新的待办事项item。
- async def create_item(list_id: str, new_item: NewItem) -> ToDoList:
- return await app.todo_dal.create_item(list_id, new_item.label)
- # 根据list_id和item_id删除指定待办事项列表中的特定项。
- # 返回更新后的ToDoList对象,删除项后更新的列表。
- @app.delete("/api/lists/{list_id}/items/{item_id}")
- async def delete_item(list_id: str, item_id: str) -> ToDoList:
- return await app.todo_dal.delete_item(list_id, item_id)
- # 定义更新待办事项item的完成状态所需的数据模型。
- # 包含item_id(待办事项item的唯一标识符)和checked_state(布尔值,表示是否已完成)字段。
- class ToDoItemUpdate(BaseModel):
- item_id: str
- checked_state: bool
- # 用于更新指定待办事项列表中某个待办事项item的完成状态。
- # 返回更新后的ToDoList对象,反映状态变更后的完整列表。
- @app.patch("/api/lists/{list_id}/checked_state")
- async def set_checked_state(list_id: str, update: ToDoItemUpdate) -> ToDoList:
- return await app.todo_dal.set_checked_state(
- list_id, update.item_id, update.checked_state
- )
- class DummyResponse(BaseModel):
- id: str
- when: datetime
- @app.get("/api/dummy")
- async def get_dummy() -> DummyResponse:
- return DummyResponse(
- id=str(ObjectId()),
- when=datetime.now(),
- )
- # 使用ObjectId()生成一个新的唯一标识符并转换为字符串,赋值给id。
- # 每个文档在MongoDB数据库中都有一个_id字段,默认情况下,它的值是一个ObjectId。
- # 使用datetime.now()获取当前时间并赋值给when。
- def main(argv=sys.argv[1:]):
- try:
- # 使用uvicorn来启动FastAPI应用
- uvicorn.run("server:app", host="0.0.0.0", port=3001, reload=DEBUG)
- # host="0.0.0.0":让应用在所有可用的网络接口上监听。
- # reload=DEBUG:如果DEBUG为True,在代码更改时自动重载应用(适合开发阶段)。
- except KeyboardInterrupt:
- pass
- # 使用try...except来捕获KeyboardInterrupt异常,以便在按下Ctrl+C时优雅地停止服务。
- if __name__ == "__main__":
- main()
复制代码 4. 在MONGODB上建立一个cluster,建立.env文件(与backend文件夹同一级),在.env文件中贴上MONGODB_URI,并在问号前加上todo。
5. 创建compose.yaml(与backend文件夹同一级)
- name: todo-app
- services:
- nginx:
- image: nginx:1.17
- volumes:
- - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
- ports:
- - 8000:80
- depends_on:
- - backend
- - frontend
- frontend:
- image: "node:22"
- user: "node"
- working_dir: /home/node/app
- environment:
- - NODE_ENV=development
- - WDS_SOCKET_PORT=0
- volumes:
- - ./frontend/:/home/node/app
- expose:
- - "3000"
- ports:
- - "3000:3000"
- command: "npm start"
- backend:
- image: todo-app/backend
- build: ./backend
- volumes:
- - ./backend/:/usr/src/app
- expose:
- - "3001"
- ports:
- - "8001:3001"
- command: "python src/server.py"
- environment:
- - DEBUG=true
- env_file:
- - path: ./.env
- 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。
- server {
- listen 80;
- server_name farm_intro;
- location / {
- proxy_pass http://frontend:3000;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
- }
- location /api {
- proxy_pass http://backend:3001/api;
- }
- }
复制代码
- listen 80:指定服务器监听在端口 80,这是 HTTP 的默认端口。
- proxy_pass http://frontend:3000:将请求转发给名为 frontend 的服务,其内部端口为 3000。这里假设 frontend 是一个运行在 Docker Compose 中的 Node.js 服务。
7. 切换到frontend文件夹
- npx create-react-app .
- npm install axios react-icons
复制代码
- create-react-app 是一个官方的 React 脚手架工具,用于快速创建尺度的 React 项目结构。
- axios 是一个用于发送 HTTP 请求的库,常用于从后端 API 获取数据。
- react-icons 提供了大量常用的图标,可以很方便地在 React 组件中使用。
8. 更新App.js文件
- import { useEffect, useState } from "react";
- import axios from "axios";
- import "./App.css";
- import ListToDoLists from "./ListTodoLists";
- import ToDoList from "./ToDoList";
- function App() {
- const [listSummaries, setListSummaries] = useState(null);
- // 存储从 /api/lists 获取的所有待办事项列表的摘要。
-
- const [selectedItem, setSelectedItem] = useState(null);
- // 此变量用于存储当前选中的待办事项列表的 ID。
- useEffect(() => {
- reloadData().catch(console.error);
- }, []);
- // useEffect 在组件挂载时调用 reloadData 函数,从服务器加载待办事项列表的数据。
- async function reloadData() {
- const response = await axios.get("/api/lists");
- const data = await response.data;
- setListSummaries(data);
- }
- // 通过 axios.get 向 /api/lists 发送请求,从服务器获取所有待办事项列表的摘要信息。
- function handleNewToDoList(newName) {
- const updateData = async () => {
- const newListData = {
- name: newName,
- };
- await axios.post(`/api/lists`, newListData);
- reloadData().catch(console.error);
- };
- updateData();
- }
- // 接受 newName 作为新列表的名称,发送 POST 请求到 /api/lists,然后刷新数据以更新界面。
- function handleDeleteToDoList(id) {
- const updateData = async () => {
- await axios.delete(`/api/lists/${id}`);
- reloadData().catch(console.error);
- };
- updateData();
- }
- // 根据 id 向 /api/lists/{id} 发送 DELETE 请求,然后刷新数据。
- function handleSelectList(id) {
- console.log("Selecting item", id);
- setSelectedItem(id);
- // 设置 selectedItem 为选中的列表的 id。
- }
- function backToList() {
- setSelectedItem(null);
- // 清除 selectedItem 的值,将视图切换回所有待办事项列表的界面。
- reloadData().catch(console.error);
- }
- if (selectedItem === null) {
- return (
- <div className="App">
- <ListToDoLists
- listSummaries={listSummaries}
- handleSelectList={handleSelectList}
- handleNewToDoList={handleNewToDoList}
- handleDeleteToDoList={handleDeleteToDoList}
- />
- </div>
- );
- } else {
- return (
- <div className="App">
- <ToDoList listId={selectedItem} handleBackButton={backToList} />
- </div>
- );
- }
- }
- export default App;
复制代码 9. 建立ListTodoLists.js文件
- import "./ListTodoLists.css";
- import { useRef } from "react";
- import { BiSolidTrash } from "react-icons/bi"; // 引入一个垃圾桶图标,用于删除功能的按钮。
- function ListToDoLists({
- listSummaries, // 待办事项列表的概要信息数组。
- handleSelectList, // 处理用户选择特定待办事项列表的函数。
- handleNewToDoList, // 处理创建新待办事项列表的函数。
- handleDeleteToDoList, // 处理删除指定待办事项列表的函数。
- }) {
- const labelRef = useRef(); // 用于访问新待办事项列表名称的输入框值。
- if (listSummaries === null) {
- return <div className="ListToDoLists loading">Loading to-do lists ...</div>;
- // 如果 listSummaries 为 null,组件会显示“Loading to-do lists ...”字样,表示数据正在加载。
- } else if (listSummaries.length === 0) {
- return (
- <div className="ListToDoLists">
- <div className="box">
- <label>
- New To-Do List:
- <input id={labelRef} type="text" />
- </label>
- <button
- onClick={() =>
- handleNewToDoList(document.getElementById(labelRef).value)
- // 提交新列表名称。
- }
- >
- New
- </button>
- </div>
- <p>There are no to-do lists!</p>
- </div>
- );
- }
- return (
- <div className="ListToDoLists">
- <h1>All To-Do Lists</h1>
- <div className="box">
- <label>
- New To-Do List:
- <input id={labelRef} type="text" />
- </label>
- <button
- onClick={() =>
- handleNewToDoList(document.getElementById(labelRef).value)
- }
- >
- New
- </button>
- // 提供添加新待办事项列表的功能,输入新名称并点击“New”按钮。
- </div>
- {listSummaries.map((summary) => {
- return (
- <div
- key={summary.id}
- className="summary"
- onClick={() => handleSelectList(summary.id)}
- // 条目点击时会调用 handleSelectList 并传入 summary.id,
- // 从而选择该列表并展示详细信息。
- >
- <span className="name">{summary.name} </span>
- <span className="count">({summary.item_count} items)</span>
- <span className="flex"></span>
- <span
- className="trash"
- onClick={(evt) => {
- evt.stopPropagation(); // 阻止事件冒泡到父元素
- handleDeleteToDoList(summary.id);
- }}
- >
- <BiSolidTrash />
- // <BiSolidTrash /> 是一个垃圾桶图标,
- // 点击时会触发 handleDeleteToDoList 删除该条目。
- </span>
- </div>
- );
- })}
- </div>
- );
- }
- export default ListToDoLists;
复制代码

10. 建立ListTodoLists.css文件
- .ListToDoLists .summary {
- border: 1px solid lightgray;
- padding: 1em;
- margin: 1em;
- cursor: pointer;
- display: flex;
- }
-
- .ListToDoLists .count {
- padding-left: 1ex;
- color: blueviolet;
- font-size: 92%;
- }
复制代码
- border: 1px solid lightgray;:设置浅灰色的 1 像素边框,给每个条目增长视觉边界。
- cursor: pointer;:将鼠标悬停在条目上时体现为指针(手型光标),提示用户条目是可点击的。
- display: flex;:使用 flex 布局,将条目内的内容(如名称、项目计数、垃圾桶图标等)按水平方向排列。
- .ListToDoLists .count该选择器为每个待办事项列表的项目计数文本提供样式。
- color: blueviolet;:将文本颜色设置为蓝紫色,以突出体现项目计数。
11. 建立ToDoList.js文件
- import "./ToDoList.css";
- import { useEffect, useState, useRef } from "react";
- import axios from "axios";
- import { BiSolidTrash } from "react-icons/bi";
- function ToDoList({ listId, handleBackButton }) {
- let labelRef = useRef();
- const [listData, setListData] = useState(null);
- // 获取列表数据
- useEffect(() => {
- const fetchData = async () => {
- const response = await axios.get(`/api/lists/${listId}`);
- const newData = await response.data;
- setListData(newData);
- };
- fetchData();
- }, [listId]);
- // 创建新待办事项
- function handleCreateItem(label) {
- const updateData = async () => {
- const response = await axios.post(`/api/lists/${listData.id}/items/`, {
- label: label,
- });
- setListData(await response.data);
- };
- updateData();
- }
- // 删除待办事项
- function handleDeleteItem(id) {
- const updateData = async () => {
- const response = await axios.delete(
- `/api/lists/${listData.id}/items/${id}`
- );
- setListData(await response.data);
- };
- updateData();
- }
- // 更新项目状态
- function handleCheckToggle(itemId, newState) {
- const updateData = async () => {
- const response = await axios.patch(
- `/api/lists/${listData.id}/checked_state`,
- {
- item_id: itemId,
- checked_state: newState,
- }
- );
- setListData(await response.data);
- };
- updateData();
- }
- if (listData === null) {
- return (
- <div className="ToDoList loading">
- <button className="back" onClick={handleBackButton}>
- Back
- </button>
- Loading to-do list ...
- </div>
- );
- }
- return (
- <div className="ToDoList">
- <button className="back" onClick={handleBackButton}>
- Back
- </button>
- <h1>List: {listData.name}</h1>
- <div className="box">
- <label>
- New Item:
- <input id={labelRef} type="text" />
- </label>
- <button
- onClick={() =>
- handleCreateItem(document.getElementById(labelRef).value)
- }
- >
- New
- </button>
- // 用户可在输入框输入内容后点击 New 按钮添加项目。
- </div>
- {listData.items.length > 0 ? (
- listData.items.map((item) => {
- return (
- <div
- key={item.id}
- className={item.checked ? "item checked" : "item"}
- onClick={() => handleCheckToggle(item.id, !item.checked)}
- >
- <span>{item.checked ? "✅" : "⬜️"} </span>
- // 复选框:使用 ✅ 和 ⬜️ 表示项目的完成状态,点击切换状态。
- <span className="label">{item.label} </span>
- <span className="flex"></span>
- <span
- className="trash"
- onClick={(evt) => {
- evt.stopPropagation();
- handleDeleteItem(item.id);
- }}
- >
- <BiSolidTrash /> // 垃圾桶图标:点击删除项目。
- </span>
- </div>
- );
- })
- ) : (
- <div className="box">There are currently no items.</div>
- )}
- </div>
- );
- }
- export default ToDoList;
复制代码


12. 建立ToDoList.css文件
- .ToDoList .back {
- margin: 0 1em;
- padding: 1em;
- float: left;
- }
-
- .ToDoList .item {
- border: 1px solid lightgray;
- padding: 1em;
- margin: 1em;
- cursor: pointer;
- display: flex;
- }
-
- .ToDoList .label {
- margin-left: 1ex;
- }
-
- .ToDoList .checked .label {
- text-decoration: line-through;
- color: lightgray;
- }
复制代码
- border: 1px solid lightgray;:为每个待办事项条目添加一个浅灰色边框,分隔不同条目。
- text-decoration: line-through;:为已完成的项目添加删除线,表现项目已完成。
- color: lightgray;:将文本颜色设置为浅灰色,降低视觉优先级,使已完成的项目显着区分于未完成的项目。
13. 更新index.css文件
- html, body {
- margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
- 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
- sans-serif;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- font-size: 12pt;
- }
- input, button {
- font-size: 1em;
- }
- code {
- font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
- monospace;
- }
- .box {
- border: 1px solid lightgray;
- padding: 1em;
- margin: 1em;
- }
- .flex {
- flex: 1;
- }
复制代码 14. 构建服务的镜像
- docker-compose up --build
复制代码 15. 浏览器中使用todo应用步伐
localhost:8000
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |