]> acesimba.cloud Git - codebuddy-web.git/commitdiff
feat: 任务列表支持编辑名称与任务描述(最多50字)
authorCodebuddy <codebuddy@localhost>
Thu, 27 Aug 2026 06:36:55 +0000 (14:36 +0800)
committerCodebuddy <codebuddy@localhost>
Thu, 27 Aug 2026 06:36:55 +0000 (14:36 +0800)
- 后端新增 PUT /api/tasks/{id}:更新名称(仅改显示名,保留原 cwd)与描述(最多50汉字,空串清空)
- 前端任务卡片新增描述行(空时显示淡灰占位),悬停显 ✎ 编辑按钮进入内联编辑
- 描述框带 x/50 字数计数,沿用现有深色主题样式,卡片尺寸基本不变

Co-Authored-By: CodeBuddy <noreply@codebuddy.ai>
backend/app.py
frontend/index.html

index f804338f6ce241955ad52c83e2f471d664ce85ff..2b1a8a23ea30131ae3a278ac0ff69e072b9dd6b2 100644 (file)
@@ -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):
index 997c6421a7b5958ddc4973b02207c54f4091170c..288604963b4cd1aadb7046bb6a86b96d02bd5bcf 100644 (file)
@@ -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 =
         '<div class="tc-name">' + escapeHtml(t.name) + '</div>' +
         '<div class="tc-cwd">' + escapeHtml(t.cwd) + '</div>' +
+        '<div class="tc-desc">' + (t.description ? escapeHtml(t.description) : '<span class="tc-desc-empty">添加描述…</span>') + '</div>' +
         '<div class="tc-time">' + last + (pausedInfo ? ' · ' + pausedInfo : '') + '</div>' +
         '<div class="tc-status">' + statusLine + '</div>' +
+        '<button class="tc-edit" title="编辑名称与描述">✎</button>' +
         '<div class="tc-actions">' +
           '<button class="tc-btn tc-enter">进入</button>' +
           (isPaused
@@ -626,6 +651,7 @@ body { position: fixed; top: 0; left: 0; right: 0; bottom: 0; width: 100%; heigh
           (canDelete ? '<button class="tc-btn tc-del" title="删除任务">✕</button>' : '') +
         '</div>';
       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 =
+      '<div><input class="tc-input tc-name-input" value="' + escapeHtml(t.name) + '" maxlength="40"></div>' +
+      '<div class="tc-desc-wrap"><input class="tc-input" maxlength="50" placeholder="任务描述(最多50字)" value="' + escapeHtml(t.description || '') + '">' +
+        '<span class="tc-counter">0/50</span></div>' +
+      '<div class="tc-edit-actions">' +
+        '<button class="tc-btn tc-save">保存</button>' +
+        '<button class="tc-btn tc-cancel">取消</button>' +
+      '</div>';
+    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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
   }