From 031b08c75741cd7a227f615dfbda5376216b5f87 Mon Sep 17 00:00:00 2001 From: Codebuddy Date: Thu, 27 Aug 2026 14:36:55 +0800 Subject: [PATCH] =?utf8?q?feat:=20=E4=BB=BB=E5=8A=A1=E5=88=97=E8=A1=A8?= =?utf8?q?=E6=94=AF=E6=8C=81=E7=BC=96=E8=BE=91=E5=90=8D=E7=A7=B0=E4=B8=8E?= =?utf8?q?=E4=BB=BB=E5=8A=A1=E6=8F=8F=E8=BF=B0=EF=BC=88=E6=9C=80=E5=A4=9A5?= =?utf8?q?0=E5=AD=97=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit - 后端新增 PUT /api/tasks/{id}:更新名称(仅改显示名,保留原 cwd)与描述(最多50汉字,空串清空) - 前端任务卡片新增描述行(空时显示淡灰占位),悬停显 ✎ 编辑按钮进入内联编辑 - 描述框带 x/50 字数计数,沿用现有深色主题样式,卡片尺寸基本不变 Co-Authored-By: CodeBuddy --- backend/app.py | 38 +++++++++++++++++++++++++++++ frontend/index.html | 58 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/backend/app.py b/backend/app.py index f804338..2b1a8a2 100644 --- a/backend/app.py +++ b/backend/app.py @@ -637,6 +637,44 @@ async def api_create_task(request: Request): return JSONResponse({"ok": True, "task": task}) +@app.put("/api/tasks/{task_id}") +async def api_update_task(task_id: str, request: Request): + """更新任务:名称(仅改显示名,不移动工作目录)与描述(最多 50 个汉字)。""" + if not _auth_ok(request): + return JSONResponse({"ok": False, "error": "未授权"}, status_code=401) + try: + body = await request.json() + except Exception: + return JSONResponse({"ok": False, "error": "无效的请求体"}, status_code=400) + data = load_tasks() + t = find_task(data, task_id) + if not t: + return JSONResponse({"ok": False, "error": "任务不存在"}, status_code=404) + + name = body.get("name") + if name is not None: + name = name.strip() + if not name: + return JSONResponse({"ok": False, "error": "名称不能为空"}, status_code=400) + if "/" in name or "\\" in name or name in (".", ".."): + return JSONResponse({"ok": False, "error": "名称含有非法字符"}, status_code=400) + # 与除自身外的其它任务重名则拒绝 + if any(x["id"] != task_id and x["name"] == name for x in data["tasks"]): + return JSONResponse({"ok": False, "error": "任务名已存在"}, status_code=409) + t["name"] = name # 仅改显示名,保留原 cwd,避免移动目录破坏会话历史 + + desc = body.get("description") + if desc is not None: + desc = desc.strip() + if len(desc) > 50: + return JSONResponse({"ok": False, "error": "描述最多 50 个汉字"}, status_code=400) + t["description"] = desc + + save_tasks(data) + logger.info(f"更新任务: id={task_id}, name={t.get('name')}, desc_len={len(t.get('description', ''))}") + return JSONResponse({"ok": True, "task": t}) + + @app.delete("/api/tasks/{task_id}") async def api_delete_task(task_id: str, request: Request): if not _auth_ok(request): diff --git a/frontend/index.html b/frontend/index.html index 997c642..2886049 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -184,6 +184,29 @@ body { position: fixed; top: 0; left: 0; right: 0; bottom: 0; width: 100%; heigh .task-card.tc-idle .tc-status { color: #777; } .task-card.tc-done { border-color: #ffa94d; } .task-card.tc-done .tc-status { color: #ffa94d; } +.task-card .tc-desc { font-size: 12px; color: #888; line-height: 1.4; } +.task-card .tc-desc-empty { color: #555; font-style: italic; } +.task-card .tc-edit { + position: absolute; top: 10px; right: 10px; width: 28px; height: 28px; + display: none; align-items: center; justify-content: center; + background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.15); + border-radius: 6px; color: #4ecca3; font-size: 14px; cursor: pointer; +} +.task-card:hover .tc-edit { display: flex; } +.task-card .tc-edit:hover { background: rgba(78,204,163,0.18); color: #fff; } +.task-card .tc-input { + width: 100%; box-sizing: border-box; background: #0f0f1e; color: #e0e0e0; + border: 1px solid rgba(255,255,255,0.2); border-radius: 6px; padding: 6px 8px; + font-size: 14px; font-family: inherit; +} +.task-card .tc-name-input { font-weight: 600; color: #4ecca3; } +.task-card .tc-desc-wrap { display: flex; align-items: center; gap: 6px; } +.task-card .tc-desc-wrap .tc-input { flex: 1; min-width: 0; } +.task-card .tc-counter { font-size: 11px; color: #666; white-space: nowrap; } +.task-card .tc-edit-actions { display: flex; gap: 8px; margin-top: 12px; } +.task-card .tc-save { background: linear-gradient(135deg, #4ecca3, #38b2ac); color: #1a1a2e; border: none; flex: 1; padding: 9px; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; } +.task-card .tc-save:hover { opacity: 0.9; } +.task-card .tc-cancel { background: rgba(255,255,255,0.08); color: #ccc; border: 1px solid rgba(255,255,255,0.15); flex: 0 0 56px; padding: 9px; border-radius: 8px; cursor: pointer; font-size: 14px; } .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); @@ -616,8 +639,10 @@ body { position: fixed; top: 0; left: 0; right: 0; bottom: 0; width: 100%; heigh card.innerHTML = '
' + escapeHtml(t.name) + '
' + '
' + escapeHtml(t.cwd) + '
' + + '
' + (t.description ? escapeHtml(t.description) : '添加描述…') + '
' + '
' + last + (pausedInfo ? ' · ' + pausedInfo : '') + '
' + '
' + statusLine + '
' + + '' + '
' + '' + (isPaused @@ -626,6 +651,7 @@ body { position: fixed; top: 0; left: 0; right: 0; bottom: 0; width: 100%; heigh (canDelete ? '' : '') + '
'; card.querySelector('.tc-enter').onclick = () => openTask(t.id); + card.querySelector('.tc-edit').onclick = (e) => { e.stopPropagation(); enterEditMode(card, t); }; if (isPaused) { card.querySelector('.tc-resume').onclick = () => resumeTask(t.id); if (canDelete) card.querySelector('.tc-del').onclick = () => startDeleteTask(t); @@ -661,6 +687,38 @@ body { position: fixed; top: 0; left: 0; right: 0; bottom: 0; width: 100%; heigh await renderTasks(); } + // 进入卡片内联编辑模式:编辑名称与描述 + function enterEditMode(card, t) { + card.innerHTML = + '
' + + '
' + + '0/50
' + + '
' + + '' + + '' + + '
'; + const nameInput = card.querySelector('.tc-name-input'); + const descInput = card.querySelector('.tc-desc-wrap input'); + const counter = card.querySelector('.tc-counter'); + const updateCounter = () => { counter.textContent = descInput.value.length + '/50'; }; + updateCounter(); + descInput.addEventListener('input', updateCounter); + nameInput.focus(); + nameInput.select(); + card.querySelector('.tc-cancel').onclick = () => renderTasks(); + card.querySelector('.tc-save').onclick = async () => { + const newName = nameInput.value.trim(); + const newDesc = descInput.value; + if (!newName) { alert('名称不能为空'); return; } + const res = await fetch(API + '/api/tasks/' + t.id, { + method: 'PUT', headers: authHeaders(), + body: JSON.stringify({ name: newName, description: newDesc }), + }); + if (!res.ok) { const d = await res.json().catch(()=>({})); alert(d.error || '保存失败'); return; } + await renderTasks(); + }; + } + function escapeHtml(s) { return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); } -- 2.43.0