ui(dev SPA): 左 pane 280→320px + header 折叠 toggle + 任务行精简 meta 防 CJK 断行
- grid 左列 280→320px (从 chat 借 40px), 任务名 / 描述 / wd 更舒展 - header 最左 toggle 按钮: body.left-collapsed → 列归零 + #pane-left display:none, chevron 翻向, localStorage 持久化 - 任务行 meta 删 id8 (挪到 row title 仍可查) + 各 span white-space:nowrap + badge/time-ago flex-shrink:0 - wd/desc 副行恢复 inline overflow:hidden ellipsis (单文本带不是 flex 子元素) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
afebf25d79
commit
5b67d29f59
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 2-4 句:做了啥 + 关键判断 + 没动什么;细节查 `git log` / `git diff`。
|
||||
|
||||
最后更新:2026-05-20(dev SPA 任务列表换滚动加载 — 删 pager bar / 加 IntersectionObserver sentinel / 共 N 个 count badge)
|
||||
最后更新:2026-05-20(dev SPA 左 pane 280→320px + header 折叠 toggle + 任务列表行去 id8 / meta nowrap 防 CJK 断行)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -23,6 +23,7 @@
|
|||
|
||||
### 2026-05-20
|
||||
|
||||
- **dev SPA 左 pane 调宽 280→320px + header 折叠 toggle + 任务行精简 meta**:用户报 280px 下底行(badge/skill/N条/Ntok/time/id8)被 flex shrink 后 CJK 字符断行(像"10 小时前"裂成两行)。三件套修:① `#app.ready grid-template-columns` `280px → 320px`(右 pane / chat 不动,从 chat 借 40px,任务名 / 描述 / wd 都更舒展);② header 最左插 `<button id="hd-toggle-left" class="icon-btn">‹</button>`,点击 toggle `body.left-collapsed` → CSS `grid-template-columns: 0 1fr 320px` + `#pane-left { display: none }`(列归零腾给 chat,折叠态 chevron 翻 `›`);state 存 `localStorage zcbot.left-collapsed`,boot 即应用,刷新保持。IntersectionObserver 留着不重建(display:none 期间 sentinel 0 高度自然不触发,展开后重算 layout 若 sentinel 在视口自然续传);③ 任务行删 `id8` span(8 位 hex 调试时才用),挪到 row `title=` hover 出 `${name}\n${task_id}` 完整 id 仍可查;`.task-row .meta > *` 全加 `white-space: nowrap; overflow: hidden; text-overflow: ellipsis` 防内部 CJK 字符破断;badge + time-ago 加 `flex-shrink: 0` 保两端不缩;wd / desc 副行恢复 inline 三件套 `overflow:hidden;text-overflow:ellipsis;white-space:nowrap`(它们是单文本带不是 flex 子元素行,`> *` CSS 不命中文本节点)。**没动**:右 pane 320px 不变(文件预览常用)、chat 中列 1fr(自适应剩余);折叠按钮没做右 pane 对应版(用户没要)。
|
||||
- **dev SPA 左侧任务列表 pager bar → 滚动加载(ChatGPT/DeepSeek 范式)**:用户嫌底部分页 chrome 别扭。删 `#task-pager`(prev/next/info bar)+ `renderPager` + `resetPageAndReload`,改 `IntersectionObserver` on `#task-sentinel`(`#task-list` 后兄弟,`min-height:1px`),root = `#pane-left`(整 pane 是 scroll 容器,`.pane{overflow:auto}`)+ `rootMargin: 200px 0px` 提前 200px 触发体感更顺。`loadTaskList({append=false})` 双语义:reset 抢占式(filters / refresh / 写操作后,page=1 替换);`append=true` 仅 sentinel 触发,page+1 拼到底,受 `taskLoading || !taskHasMore` 互斥。**并发模型**:用 `_taskLoadSeq` token 让 reset 永远抢占 — 收到响应时若 `mySeq !== _taskLoadSeq` 整段 short-circuit return(也含 finally 的 `taskLoading=false`,避免 reset 在途时被 stale append 错误解锁),解决"append 在途时改筛选被丢"的旧 bug。**新增**:① 首 pane-head 加 `<span id="task-count">共 N 个</span>` muted 小字补偿总数显示;② sentinel 文案三态(加载中… / — 已加载全部 — / 空字符串);③ `renderTaskList(tasks, append)` append 走 `<div>.innerHTML` 临时容器 + `appendChild` 不 clobber 已渲染行,事件 handler 只挂新行。**没动**:`/v1/tasks` 后端(本来就是标准分页 `{page,page_size,count,results}`)、page_size=20 默认、所有 7 处 `loadTaskList()` 旧调用点(默认 reset 语义与原行为等价)。**Tradeoff**:失"跳到第 N 页"但筛选 / 搜索 / 排序 + 滚动覆盖所有导航场景;失"当前页位置"但写操作后跳回顶端在 zcbot 任务规模(几十~几百)体感自然。
|
||||
- **dev SPA 左侧任务列表行加「最近操作时间」**:用户要"显示最新操作时间"。`renderTaskList` 行 meta 区(badge / skill / N 条 / N tok / id-slice)在 id-slice 之前插一个 `<span class="muted" style="margin-left:auto;">`,文案用新加的 `fmtTimeAgo(iso)` 相对时间 helper:`<60s`→刚刚 / `<1h`→N 分钟前 / 同日→N 小时前 / 昨日→昨天 HH:MM / 同年→MM-DD HH:MM / 跨年→YYYY-MM-DD,`title=` hover 出完整 `fmtTime` locale 串。`margin-left:auto` 从 id-slice 挪到时间 span(让两者一起靠右,中间 8px `.meta gap` 自然分隔)。字段用 `updated_at`(任务任何写操作 — 改名 / 新消息 / 状态切 — 都会更新,贴合"最新操作"语义),`/v1/tasks` payload 早已包含,后端零改。**没动**:左 pane 列表默认排序仍 `-created_at`(用户改排序顺序时另说);id-slice 保留(调试参考)。
|
||||
- **dev SPA 新建任务弹框「工作目录」从 input + datalist 改 `<select>` 下拉**:用户要"做成下拉选择"。原 `<input list="folders-datalist">` autocomplete 改 `<select id="nt-wd-sel">`,选项 = `(留空 · 用任务名作目录)` + 既有目录(`name — N 个任务` / `空目录`) + `+ 新建目录…` sentinel(`__new__`)。选 `__new__` → 显示备用 `<input id="nt-wd-new">` 输入新目录名 + autofocus,提交时 `working_dir = sel === "__new__" ? nt-wd-new.value : sel`。hint 区改 `updateWdHint()` 三分支(新建 / 留空 / 复用),change + new-input + name-input 三事件触发。`<datalist id="folders-datalist">` 留在 modal 内但不再被它消费,**只供左 pane 顶部 `#filter-wd` 筛选 autocomplete**(datalist 按 id 引用,DOM 位置无关);`loadFolderSuggestions()` 同次拉取灌两边。**没动**:`/v1/folders` API、提交 body 形态(仍 `working_dir: string`,空串语义不变 → 后端 fallback 用任务名)、左 pane filter-wd 仍用 input + datalist(用户只点名"任务弹框")、DESIGN / RUN。**Tradeoff**:纯 select 实现最直接但会失"新名则新建",改两段式(select 含 `+ 新建…`,触发后展开 text input)保留所有原能力。
|
||||
|
|
|
|||
|
|
@ -140,13 +140,23 @@
|
|||
|
||||
/* ───── 3-pane layout ───── */
|
||||
#app { display: none; height: 100vh; }
|
||||
#app.ready { display: grid; grid-template-columns: 280px 1fr 320px; grid-template-rows: auto 1fr; grid-template-areas: "head head head" "left mid right"; }
|
||||
#app.ready { display: grid; grid-template-columns: 320px 1fr 320px; grid-template-rows: auto 1fr; grid-template-areas: "head head head" "left mid right"; }
|
||||
/* 折叠左 pane:整列归零 + pane 隐藏(配合 header toggle 按钮 + localStorage 持久化) */
|
||||
body.left-collapsed #app.ready { grid-template-columns: 0 1fr 320px; }
|
||||
body.left-collapsed #pane-left { display: none; }
|
||||
|
||||
header {
|
||||
grid-area: head; background: #fff; border-bottom: 1px solid var(--border);
|
||||
padding: 8px 14px; display: flex; align-items: center; gap: 12px;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,.03);
|
||||
}
|
||||
/* header 小图标按钮(折叠 toggle 等):透明常态 + 悬浮微底 */
|
||||
header .icon-btn {
|
||||
background: transparent; border: 1px solid transparent;
|
||||
color: var(--muted); padding: 2px 8px; font-size: 16px; line-height: 1.2;
|
||||
border-radius: 4px; cursor: pointer;
|
||||
}
|
||||
header .icon-btn:hover { background: var(--hover); color: var(--text); border-color: var(--border); }
|
||||
header .brand { display: flex; align-items: center; gap: 8px; }
|
||||
header .brand .logo {
|
||||
width: 24px; height: 24px; border-radius: 6px;
|
||||
|
|
@ -224,7 +234,10 @@
|
|||
.task-row.active { background: var(--accent-soft); border-left: 3px solid var(--accent); padding-left: 9px; }
|
||||
.task-row .desc { font-weight: 500; color: var(--text); margin-bottom: 2px;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.task-row .meta { font-size: 11px; color: var(--muted); display: flex; gap: 8px; }
|
||||
/* meta 行:flex nowrap + 每个子项 nowrap,防 CJK 字符在窄 pane(280px)被 shrink 后断行 */
|
||||
.task-row .meta { font-size: 11px; color: var(--muted); display: flex; gap: 8px; min-width: 0; }
|
||||
.task-row .meta > * { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
||||
.task-row .meta .badge, .task-row .meta .time-ago { flex-shrink: 0; } /* 状态徽章 + 相对时间不缩 */
|
||||
.task-row .badge {
|
||||
display: inline-block; padding: 0 6px; border-radius: 8px; font-size: 11px;
|
||||
background: #eef; color: #336;
|
||||
|
|
@ -536,6 +549,7 @@
|
|||
<!-- ───── main 3-pane ───── -->
|
||||
<div id="app">
|
||||
<header>
|
||||
<button id="hd-toggle-left" class="icon-btn" title="折叠任务列表">‹</button>
|
||||
<div class="brand">
|
||||
<div class="logo">Z</div>
|
||||
<div class="title">zcbot</div>
|
||||
|
|
@ -690,6 +704,7 @@
|
|||
const LS_TOKEN = "zcbot.token";
|
||||
const LS_UID = "zcbot.user_id";
|
||||
const LS_NAME = "zcbot.name";
|
||||
const LS_LEFT_COLLAPSED = "zcbot.left-collapsed";
|
||||
|
||||
const state = {
|
||||
token: localStorage.getItem(LS_TOKEN) || "",
|
||||
|
|
@ -939,6 +954,20 @@ function logout() {
|
|||
}
|
||||
$("hd-logout").onclick = logout;
|
||||
|
||||
// ───── 左 pane 折叠 toggle(localStorage 持久化) ─────
|
||||
function applyLeftCollapsed(collapsed) {
|
||||
document.body.classList.toggle("left-collapsed", collapsed);
|
||||
const btn = $("hd-toggle-left");
|
||||
btn.textContent = collapsed ? "›" : "‹";
|
||||
btn.title = collapsed ? "展开任务列表" : "折叠任务列表";
|
||||
}
|
||||
$("hd-toggle-left").onclick = () => {
|
||||
const next = !document.body.classList.contains("left-collapsed");
|
||||
localStorage.setItem(LS_LEFT_COLLAPSED, next ? "1" : "");
|
||||
applyLeftCollapsed(next);
|
||||
};
|
||||
applyLeftCollapsed(localStorage.getItem(LS_LEFT_COLLAPSED) === "1");
|
||||
|
||||
// ───── enter app ─────
|
||||
function enterApp() {
|
||||
$("login").style.display = "none";
|
||||
|
|
@ -1033,19 +1062,19 @@ function renderTaskList(tasks, append = false) {
|
|||
const wdName = t.working_dir ? t.working_dir.split("/").filter(Boolean).pop() : "";
|
||||
const desc = t.description || "";
|
||||
const statusLabel = statusLabels[t.status] || t.status;
|
||||
const rowTitle = `${taskName}\n${t.task_id}`; // hover 出全名 + 完整 id(替代 meta 里被去掉的 id8)
|
||||
return `
|
||||
<div class="task-row${active}" data-tid="${t.task_id}" style="display:flex;align-items:flex-start;gap:6px;">
|
||||
<div class="task-row${active}" data-tid="${t.task_id}" title="${escapeHtml(rowTitle)}" style="display:flex;align-items:flex-start;gap:6px;">
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div class="desc" title="${escapeHtml(taskName)}">${escapeHtml(taskName)}</div>
|
||||
${wdName ? `<div class="meta muted" title="${escapeHtml(t.working_dir || "")}">📁 ${escapeHtml(wdName)}</div>` : ""}
|
||||
${desc ? `<div class="meta muted" title="${escapeHtml(desc)}" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;">${escapeHtml(desc)}</div>` : ""}
|
||||
<div class="desc">${escapeHtml(taskName)}</div>
|
||||
${wdName ? `<div class="meta muted" title="${escapeHtml(t.working_dir || "")}" style="display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">📁 ${escapeHtml(wdName)}</div>` : ""}
|
||||
${desc ? `<div class="meta muted" title="${escapeHtml(desc)}" style="display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${escapeHtml(desc)}</div>` : ""}
|
||||
<div class="meta">
|
||||
<span class="badge ${t.status}">${statusLabel}</span>
|
||||
${t.skill ? `<span class="muted">${escapeHtml(t.skill)}</span>` : ""}
|
||||
${t.skill ? `<span class="muted" title="${escapeHtml(t.skill)}">${escapeHtml(t.skill)}</span>` : ""}
|
||||
<span>${t.n_messages || 0} 条</span>
|
||||
<span>${t.tokens || 0} tok</span>
|
||||
<span class="muted" style="margin-left:auto;" title="${escapeHtml(fmtTime(t.updated_at))}">${escapeHtml(fmtTimeAgo(t.updated_at))}</span>
|
||||
<span class="muted" style="font-family:monospace;">${t.task_id.slice(0, 8)}</span>
|
||||
<span class="muted time-ago" style="margin-left:auto;" title="${escapeHtml(fmtTime(t.updated_at))}">${escapeHtml(fmtTimeAgo(t.updated_at))}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="dd-toggle task-menu" data-tid="${t.task_id}" title="任务操作">⋯</button>
|
||||
|
|
|
|||
Loading…
Reference in New Issue