From 7989ab093afcfd27f472f135a77853e3bf1bd922 Mon Sep 17 00:00:00 2001 From: Codebuddy Date: Thu, 18 Jun 2026 16:57:20 +0800 Subject: [PATCH] =?utf8?q?=E4=BB=BB=E5=8A=A1=E6=B8=85=E5=8D=95=EF=BC=9A?= =?utf8?q?=E6=9A=82=E5=81=9C/=E6=81=A2=E5=A4=8D=20+=20=E5=8F=97=E9=99=90?= =?utf8?q?=E5=88=A0=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit 后端 (backend/app.py): - GET /api/tasks 返回 status (active/paused) 和 paused_at 字段 - POST /api/tasks/{id}/pause: 标记暂停,记录 paused_at - POST /api/tasks/{id}/resume: 恢复启用,清除 paused_at - DELETE /api/tasks/{id}: 仅允许删除已暂停超 24 小时的任务 前端 (frontend/index.html): - 任务卡片:删除按钮改为暂停(⏸)/恢复(▶)按钮 - 默认仅列出启用状态任务,勾选"显示全部任务"查看暂停的 - 暂停超 24 小时的任务才显示删除按钮(✕) - 删除确认 modal:3 次确认,每次需等待 10 秒才能点击 - 暂停状态任务卡片半透明显示,标注已暂停时长 - ESC/点击遮罩/取消按钮 关闭删除 modal --- backend/app.py | 63 +++++++++++++++-- frontend/index.html | 169 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 214 insertions(+), 18 deletions(-) diff --git a/backend/app.py b/backend/app.py index e863092..c9c14ae 100644 --- a/backend/app.py +++ b/backend/app.py @@ -195,6 +195,8 @@ async def api_tasks(request: Request): for t in data.get("tasks", []): tt = dict(t) tt["active"] = t["id"] in ACTIVE_TASKS + # 兼容旧数据:无 status 字段视为 active + tt.setdefault("status", "active") tasks.append(tt) return JSONResponse({"ok": True, "tasks": tasks}) @@ -240,16 +242,69 @@ async def api_delete_task(task_id: str, request: Request): if not _auth_ok(request): return JSONResponse({"ok": False, "error": "未授权"}, status_code=401) if task_id in ACTIVE_TASKS: - return JSONResponse({"ok": False, "error": "任务正在运行,请先关闭其终端窗口"}, status_code=409) + return JSONResponse({"ok": False, "error": "任务正在运行,请先暂停"}, status_code=409) data = load_tasks() - before = len(data["tasks"]) - data["tasks"] = [t for t in data["tasks"] if t["id"] != task_id] - if len(data["tasks"]) == before: + t = find_task(data, task_id) + if not t: return JSONResponse({"ok": False, "error": "任务不存在"}, status_code=404) + # 仅允许删除已暂停超过 24 小时的任务 + if t.get("status") != "paused": + return JSONResponse({"ok": False, "error": "仅可删除已暂停的任务"}, status_code=409) + paused_at = t.get("paused_at") + if not paused_at: + return JSONResponse({"ok": False, "error": "任务暂停时间未知"}, status_code=409) + try: + paused_dt = datetime.datetime.fromisoformat(paused_at) + elapsed = datetime.datetime.now() - paused_dt + if elapsed.total_seconds() < 24 * 3600: + hours_left = 24 - elapsed.total_seconds() / 3600 + return JSONResponse({"ok": False, "error": f"暂停不足 24 小时,还需 {hours_left:.1f} 小时"}, status_code=409) + except (ValueError, TypeError): + return JSONResponse({"ok": False, "error": "暂停时间格式错误"}, status_code=409) + data["tasks"] = [x for x in data["tasks"] if x["id"] != task_id] save_tasks(data) + logger.info(f"删除任务: id={task_id}, name={t.get('name')}") return JSONResponse({"ok": True}) +@app.post("/api/tasks/{task_id}/pause") +async def api_pause_task(task_id: str, request: Request): + """暂停任务:仅标记状态,不删除任何数据。运行中的任务需先关闭终端窗口。""" + if not _auth_ok(request): + return JSONResponse({"ok": False, "error": "未授权"}, status_code=401) + if task_id in ACTIVE_TASKS: + return JSONResponse({"ok": False, "error": "任务正在运行,请先关闭其终端窗口"}, status_code=409) + data = load_tasks() + t = find_task(data, task_id) + if not t: + return JSONResponse({"ok": False, "error": "任务不存在"}, status_code=404) + if t.get("status") == "paused": + return JSONResponse({"ok": False, "error": "任务已是暂停状态"}, status_code=409) + t["status"] = "paused" + t["paused_at"] = now_iso() + save_tasks(data) + logger.info(f"暂停任务: id={task_id}, name={t.get('name')}") + return JSONResponse({"ok": True, "task": t}) + + +@app.post("/api/tasks/{task_id}/resume") +async def api_resume_task(task_id: str, request: Request): + """恢复任务:从暂停状态切回启用。""" + if not _auth_ok(request): + return JSONResponse({"ok": False, "error": "未授权"}, status_code=401) + data = load_tasks() + t = find_task(data, task_id) + if not t: + return JSONResponse({"ok": False, "error": "任务不存在"}, status_code=404) + if t.get("status", "active") == "active": + return JSONResponse({"ok": False, "error": "任务已是启用状态"}, status_code=409) + t["status"] = "active" + t.pop("paused_at", None) + save_tasks(data) + logger.info(f"恢复任务: id={task_id}, name={t.get('name')}") + return JSONResponse({"ok": True, "task": t}) + + @app.post("/api/kill/{pid}") async def api_kill(pid: int, request: Request): if not _auth_ok(request): diff --git a/frontend/index.html b/frontend/index.html index 285e8ba..18a3ebc 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -112,10 +112,16 @@ html { font-size: 15px; } html, body { height: 100%; background: #1a1a2e; color: } .task-card .tc-enter { background: linear-gradient(135deg, #4ecca3, #38b2ac); color: #1a1a2e; } .task-card .tc-enter:hover { opacity: 0.9; } +.task-card .tc-pause { background: rgba(255,230,109,0.12); color: #ffe66d; border: 1px solid rgba(255,230,109,0.25); flex: 0 0 56px; } +.task-card .tc-pause:hover { background: rgba(255,230,109,0.22); } +.task-card .tc-resume { background: rgba(78,204,163,0.12); color: #4ecca3; border: 1px solid rgba(78,204,163,0.25); flex: 0 0 56px; } +.task-card .tc-resume:hover { background: rgba(78,204,163,0.22); } .task-card .tc-del { background: rgba(255,107,107,0.12); color: #ff8787; border: 1px solid rgba(255,107,107,0.25); flex: 0 0 56px; } .task-card .tc-del:hover { background: rgba(255,107,107,0.22); } +.task-card.tc-paused { opacity: 0.6; border-color: rgba(255,230,109,0.3); } +.task-card .tc-status { font-size: 12px; } .task-card.tc-active { border-color: #ffe66d; } -.task-card.tc-active .tc-status { color: #ffe66d; font-size: 12px; } +.task-card.tc-active .tc-status { color: #ffe66d; } .task-new { display: flex; align-items: center; justify-content: center; gap: 8px; background: rgba(78,204,163,0.06); border: 2px dashed rgba(78,204,163,0.3); @@ -124,6 +130,21 @@ html { font-size: 15px; } html, body { height: 100%; background: #1a1a2e; color: } .task-new:hover { border-color: #4ecca3; background: rgba(78,204,163,0.12); } .picker-empty { color: #666; text-align: center; padding: 60px; font-size: 15px; } +.picker-toggle { display: flex; align-items: center; gap: 8px; font-size: 14px; color: #aaa; cursor: pointer; user-select: none; } +.picker-toggle input { cursor: pointer; } + +/* 删除确认 modal */ +#del-modal { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.7); z-index:200; align-items:center; justify-content:center; } +#del-modal .del-dialog { background:#1a1a2e; border:1px solid #ff6b6b; border-radius:12px; padding:24px; width:440px; max-width:90vw; text-align:center; } +#del-modal .del-title { color:#ff6b6b; font-size:18px; margin-bottom:12px; } +#del-modal .del-msg { color:#e0e0e0; font-size:14px; margin-bottom:8px; line-height:1.6; } +#del-modal .del-warn { color:#ffe66d; font-size:13px; margin-bottom:16px; } +#del-modal .del-count { color:#4ecca3; font-size:13px; margin-bottom:16px; } +#del-modal .del-actions { display:flex; gap:10px; justify-content:center; } +#del-modal .del-btn { padding:10px 24px; border-radius:8px; border:none; cursor:pointer; font-size:14px; font-weight:600; } +#del-modal .del-cancel { background:rgba(255,255,255,0.08); color:#ccc; } +#del-modal .del-confirm { background:#ff6b6b; color:#fff; } +#del-modal .del-confirm:disabled { background:rgba(255,107,107,0.3); cursor:not-allowed; } @@ -144,13 +165,30 @@ html { font-size: 15px; } html, body { height: 100%; background: #1a1a2e; color:

任务清单

- +
+ + +
+ +
+
+
⚠ 删除任务
+
+
此操作仅删除任务记录,不删除工作目录与历史会话。
+
+
+ + +
+
+
+