找回密码
立即注册
搜索
发回帖 发新帖

6873

积分

1

好友

858

主题
发表于 9 小时前 | 查看: 1| 回复: 0

做任务管理器,真正烦人的通常不是增删改查,而是任务状态已经变了,页面却还停留在旧数据上。

比如团队里有几个人同时处理任务,A 刚把任务改成“已完成”,B 不刷新就看不到变化。要是再叠加上任务筛选、状态统计和多人协作,前端代码能不膨胀吗?

这类小工具,我更愿意用 Python、FastHTML 和 MongoDB 组合来做。FastHTML 负责页面与交互,MongoDB 保存任务数据,HTMX 负责把服务器返回的 HTML 片段更新到页面上。

有一点需要提前说清楚:HTMX 能让页面局部更新,但不等于真正的实时推送。如果要求多个用户修改任务后,其他人的页面也立即变化,还需要增加 WebSocket 或 SSE。下面先把任务管理的主流程搭起来,再看实时通知怎么接。

一、先把任务数据设计好

任务管理器的数据不复杂,标题、负责人、状态、创建时间基本够用。MongoDB 使用文档存储,后续需要增加优先级、截止时间,也不用大动表结构。

先安装依赖:

pip install python-fasthtml pymongo

MongoDB 可以使用本地实例,也可以使用 MongoDB Atlas。连接信息通过环境变量传入,别把生产环境的数据库地址和密码直接写在代码里。

import os
from datetime import datetime, timezone

from fasthtml.common import *
from pymongo import MongoClient
from bson import ObjectId

client = MongoClient(os.environ["MONGO_URI"])
db = client["workbench"]
tasks = db["tasks"]
tasks.create_index([("status", 1), ("created_at", -1)])

这里顺手给状态和创建时间建了组合索引。任务数量少的时候,查列表确实感觉不出来差别;等任务积累起来,每次都扫描整张集合,延迟就可能慢慢上来。

还有一个细节:任务状态最好固定取值,例如 todo、doing、done,不要让前端随便传中文状态进去。否则后面统计任务数量时,很容易出现同一个意思却存了好几种值的情况。

二、用 FastHTML 完成任务列表和状态修改

FastHTML 的一个特点是直接用 Python 组织页面元素,不需要先搭一套 React 或 Vue 工程。

先定义页面。下面的代码只负责任务列表和新增任务,状态修改也由后端处理,避免把业务逻辑散落在浏览器里。

from fasthtml.common import *

app, rt = fast_app()

def task_row(task):
    task_id = str(task["_id"])
    return Li(
        Div(
            Strong(task["title"]),
            Small(
                f'负责人:{task["owner"]} · '
                f'状态:{task["status"]}'
            ),
        ),
        Select(
            *[
                Option(
                    state,
                    value=state,
                    selected=state == task["status"],
                )
                for state in ("todo", "doing", "done")
            ],
            name="status",
            hx_post=f"/tasks/{task_id}/status",
            hx_trigger="change",
            hx_target=f"#task-{task_id}",
            hx_swap="outerHTML",
        ),
        id=f"task-{task_id}",
    )

@rt("/")
def home():
    rows = list(tasks.find().sort("created_at", -1).limit(100))
    return Html(
        Head(
            Title("团队任务管理器"),
            Script(src="https://unpkg.com/htmx.org"),
        ),
        Body(
            H1("团队任务"),
            Form(
                Input(name="title", placeholder="这次要处理什么?"),
                Input(name="owner", placeholder="负责人"),
                Button("创建任务"),
                hx_post="/tasks",
                hx_target="#task-list",
                hx_swap="afterbegin",
            ),
            Ul(*(task_row(item) for item in rows), id="task-list"),
        ),
    )

这段代码里有个值得注意的地方:状态下拉框修改后,通过 hx_post 请求服务器,服务器返回更新后的任务行,再由 HTMX 替换页面中的对应元素。

前端不必重新请求整张页面,也不用自己拼接 JSON 再更新 DOM。

不过,任务新增接口和状态接口还没有写完。实际项目中,新增任务时需要校验标题和负责人,状态更新时也必须检查任务是否存在、状态是否合法。否则页面做得再漂亮,接口一样可能收到脏数据。

三、MongoDB 更新不能只图省事

状态更新最好使用 MongoDB 的原子更新操作,而不是先把整个文档读出来、修改,再覆盖写回去。

from bson.errors import InvalidId

@rt("/tasks/{task_id}/status")
def change_status(task_id: str, status: str):
    if status not in {"todo", "doing", "done"}:
        return "不支持的任务状态", 400

    try:
        oid = ObjectId(task_id)
    except InvalidId:
        return "任务 ID 不合法", 400

    task = tasks.find_one_and_update(
        {"_id": oid},
        {"$set": {"status": status}},
        return_document=True,
    )

    if task is None:
        return "任务不存在", 404

    return task_row(task)

这里使用 find_one_and_update,直接完成查询与更新,并返回更新后的文档。

多人同时操作时,这比“先查询、再覆盖整个文档”更稳妥。不过,如果业务要求严格控制状态流转,例如已完成任务不能直接退回待办,就还要把原状态等条件放进更新过滤器,不能只校验新状态是否合法。

新增接口也补上:

@rt("/tasks")
def create_task(title: str, owner: str):
    title = title.strip()
    owner = owner.strip()

    if not title or not owner:
        return "任务标题和负责人不能为空", 400

    task = {
        "title": title[:120],
        "owner": owner[:60],
        "status": "todo",
        "created_at": datetime.now(timezone.utc),
    }
    result = tasks.insert_one(task)
    task["_id"] = result.inserted_id
    return task_row(task)

serve()

到这里,一个基础版本就能创建任务、展示列表并修改状态了。生产环境还要补上登录鉴权、CSRF 防护、分页以及错误日志。特别是任务接口,不能因为知道某个任务的 ID,就允许任何人修改它。

四、让多个用户真正看到实时变化

前面的 HTMX 方案只是请求触发更新。用户 A 修改任务后,用户 B 的页面不会自动刷新。

如果需要实时协作,可以在任务更新成功后发布一条事件,再通过 SSE 或 WebSocket 推送给在线客户端。

例如事件可以设计成这样:

event = {
    "type": "task.updated",
    "task_id": str(task["_id"]),
    "status": task["status"],
}

这里不要急着把整个任务文档广播出去。先明确客户端究竟需要哪些字段,通常任务 ID、状态和更新时间就够了。这样可以减少传输量,也避免把内部字段不小心暴露给其他用户。

如果采用 SSE,可以由服务器维护事件流,在任务更新后通知订阅者刷新对应的任务行;如果使用 WebSocket,则适合后续增加在线协作、即时评论等双向交互。

需要注意,单进程内保存的订阅者列表只能覆盖当前进程。服务部署成多个实例后,任务更新发生在实例 A,连接却落在实例 B,就需要 Redis Pub/Sub 之类的消息通道进行跨实例广播。

MongoDB 变更流(Change Streams)也是一个选择,可以监听集合变化,但它依赖副本集或分片集群等部署条件,不能默认认为本地独立实例就能直接使用。

这个任务管理器真正需要花心思的地方,不是把页面写出来,而是把数据更新、状态约束和事件通知分开。FastHTML 能减少前端样板代码,MongoDB 适合灵活存储任务文档;至于实时推送,要根据用户规模和部署方式选择实现方案。

先让任务更新可靠,再让页面实时变化,后续排查问题也会容易不少。

类似轻量 Web 工具的实现思路,也可以在云栈社区翻一翻,不少开发者会分享这类组合方案。




上一篇:手机有必要买 2TB 吗?普通用户 256GB 与 512GB 更划算
下一篇:零基础学智能体(AI Agent)指南:六阶段学习路线与实战案例
您需要登录后才可以回帖 登录 | 立即注册

关闭

站长推荐上一条 /1 下一条

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-11-13 16:38 , Processed in 0.074126 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

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