refactor(web): 中栏操作收进 ⋯ 菜单 + 消息阅读限宽 + 色彩收敛 + bump 0.12.2

- 中栏顶栏 5 个平铺按钮(导出/清空/完成/废弃/删除)→「完成」+「⋯」菜单,
  菜单复用 taskMenuItems(过滤 complete),与任务行同一范式;破坏性操作不再平铺易误点。
  顺带让菜单「清空」按 run_status 也禁用(修运行中 409-after-confirm 小坑)
- 消息限宽:.msg max-width 92% → min(92%,48rem),user 气泡 min(92%,36rem),宽屏可读性↑
- 色彩收敛:颜色=后果(完成/下载绿、废弃橙、删除红),导出/清空中性不着色;移除紫/蓝冗余

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-06-13 12:52:56 +08:00
parent 1f57bbd201
commit ae9790601a
4 changed files with 31 additions and 40 deletions

View File

@ -21,6 +21,14 @@
## 已完成关键能力 ## 已完成关键能力
### 2026-06-13 / 前端 UI 优化:中栏操作收菜单 + 阅读限宽 + 色彩收敛
- **中栏顶栏 5 按钮 → 「完成」+「⋯」菜单**:原导出/清空/完成/废弃/删除 平铺,与任务行的 `⋯` 浮层菜单两套范式打架,且破坏性操作(废弃/删除)平铺易误点、移动端挤。改为只留高频「完成」+ 一个 `⋯`,菜单复用 `taskMenuItems`(过滤掉 complete);单一事实源,两处共用。顺带把「清空」在菜单里按 `run_status` 也禁用(taskMeta 带该字段,修了之前菜单清空运行中会 409-after-confirm 的小坑)。
- **消息阅读限宽**:`.msg` 由 `max-width:92%` 收到 `min(92%,48rem)`(assistant ~60-80 字/行),user 气泡 `min(92%,36rem)`;宽屏长文不再满屏铺开难回扫,窄屏 92% 仍生效。
- **色彩负载收敛**:语义色由"每个操作一色"改为"颜色=后果"——正向(完成/下载)绿、破坏性(废弃橙/删除红),中性(导出/清空)不着色;移除紫色"清空"与蓝色"导出"。删掉已不存在的顶栏按钮 hover 规则(保留 file-picker 的 sp-copy/sp-move)。
- 改动文件:`dev.html`(中栏 markup + 三处 CSS)、`chat.js`(菜单接线 + renderChatMeta/deleteTask 收口)。**未动**左栏 4 行筛选头折叠(点 2,行为变化较大,留作下一步)。
- bump 0.12.1 → 0.12.2(patch:UI 重构 + 样式)。
### 2026-06-13 / 前端小修:导出按钮简写 + 任务菜单加清空 + 移动端 task 可滚 + admin 自适应 ### 2026-06-13 / 前端小修:导出按钮简写 + 任务菜单加清空 + 移动端 task 可滚 + admin 自适应
- **顶栏「导出对话记录」→「导出对话」**:与「清空对话」对齐(`dev.html` 按钮 + `chat.js` 任务菜单 export 项同步)。 - **顶栏「导出对话记录」→「导出对话」**:与「清空对话」对齐(`dev.html` 按钮 + `chat.js` 任务菜单 export 项同步)。

View File

@ -1,3 +1,3 @@
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 # zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
# 改版本只动这一行。 # 改版本只动这一行。
__version__ = "0.12.1" __version__ = "0.12.2"

View File

@ -413,15 +413,11 @@
background: #fff; background: #fff;
border-bottom: 1px solid var(--border-soft); border-bottom: 1px solid var(--border-soft);
} }
/* 对话顶栏按钮:常态中性 + hover 上语义色 — 完成 绿/导出 蓝/清空 紫/废弃 橙/删除 红 /* 对话顶栏只剩「完成」(绿)+「⋯」菜单;其余操作收进浮层菜单按语义色(见 .dd-item.act-*)。
同色组合并 selector(export ≈ sp-copy 蓝, abandon ≈ sp-move 橙) */ file-picker 的 sp-copy/sp-move 仍复用蓝/橙。 */
#btn-done:hover:not(:disabled) { color: var(--c-green); border-color: var(--c-green-bd); background: var(--c-green-bg); } #btn-done:hover:not(:disabled) { color: var(--c-green); border-color: var(--c-green-bd); background: var(--c-green-bg); }
#btn-export:hover:not(:disabled),
#sp-copy:hover:not(:disabled) { color: var(--c-blue); border-color: var(--c-blue-bd); background: var(--c-blue-bg); } #sp-copy:hover:not(:disabled) { color: var(--c-blue); border-color: var(--c-blue-bd); background: var(--c-blue-bg); }
#btn-clear-msgs:hover:not(:disabled) { color: var(--c-purple); border-color: var(--c-purple-bd); background: var(--c-purple-bg); }
#btn-abandon:hover:not(:disabled),
#sp-move:hover:not(:disabled) { color: var(--c-orange); border-color: var(--c-orange-bd); background: var(--c-orange-bg); } #sp-move:hover:not(:disabled) { color: var(--c-orange); border-color: var(--c-orange-bd); background: var(--c-orange-bg); }
#btn-delete-task:hover:not(:disabled) { color: var(--c-red); border-color: var(--c-red-bd); background: var(--c-red-bg); }
/* ───── floating dropdown menu ───── */ /* ───── floating dropdown menu ───── */
/* 单例:position: fixed 逃出 pane overflow 裁剪;右上角触发,向下展开 */ /* 单例:position: fixed 逃出 pane overflow 裁剪;右上角触发,向下展开 */
@ -448,10 +444,11 @@
.dd-item:hover { background: var(--hover); } .dd-item:hover { background: var(--hover); }
.dd-item:disabled { color: var(--muted); cursor: not-allowed; opacity: 0.55; } .dd-item:disabled { color: var(--muted); cursor: not-allowed; opacity: 0.55; }
.dd-item:disabled:hover { background: transparent; } .dd-item:disabled:hover { background: transparent; }
/* 菜单项颜色 = 操作后果:正向(完成/下载)绿,破坏性(废弃)橙、(删除)红;
中性操作(导出/清空)不着色,降低色彩负载。rename(文件菜单)保留蓝。 */
.dd-item.act-complete, .dd-item.act-download { color: var(--c-green); } .dd-item.act-complete, .dd-item.act-download { color: var(--c-green); }
.dd-item.act-abandon { color: var(--c-orange); } .dd-item.act-abandon { color: var(--c-orange); }
.dd-item.act-export, .dd-item.act-rename { color: var(--c-blue); } .dd-item.act-rename { color: var(--c-blue); }
.dd-item.act-clear { color: var(--c-purple); }
.dd-item.act-delete { color: var(--accent); } .dd-item.act-delete { color: var(--accent); }
/* ───── task list ───── */ /* ───── task list ───── */
@ -498,8 +495,10 @@
display: flex; flex-direction: column; gap: 8px; display: flex; flex-direction: column; gap: 8px;
min-height: 0; /* 允许在 flex 容器里收缩 + 触发自身滚动 */ min-height: 0; /* 允许在 flex 容器里收缩 + 触发自身滚动 */
} }
.msg { border: 1px solid var(--border); border-radius: var(--r-md); padding: 8px 12px; max-width: 92%; animation: msg-in .22s cubic-bezier(.2,.7,.2,1); } /* 阅读宽度:assistant/system/tool 限到 ~48rem(约 60-80 字/行,长文不至于满屏铺开难回扫);
.msg.user { background: var(--user-bg); align-self: flex-end; } user 气泡更窄(36rem)。宽屏下提升可读性,窄屏 92% 仍生效(min 取小者) */
.msg { border: 1px solid var(--border); border-radius: var(--r-md); padding: 8px 12px; max-width: min(92%, 48rem); animation: msg-in .22s cubic-bezier(.2,.7,.2,1); }
.msg.user { background: var(--user-bg); align-self: flex-end; max-width: min(92%, 36rem); }
.msg.assistant, .msg.system, .msg.tool, .msg.error { background: var(--asst-bg); align-self: flex-start; } .msg.assistant, .msg.system, .msg.tool, .msg.error { background: var(--asst-bg); align-self: flex-start; }
.msg.error { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); } .msg.error { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }
.cancelled-badge { margin-top: 8px; padding: 4px 10px; font-size: 12px; color: var(--accent); background: var(--accent-soft); border: 1px dashed var(--accent); border-radius: var(--r-md); display: inline-block; } .cancelled-badge { margin-top: 8px; padding: 4px 10px; font-size: 12px; color: var(--accent); background: var(--accent-soft); border: 1px dashed var(--accent); border-radius: var(--r-md); display: inline-block; }
@ -1241,11 +1240,8 @@
<div class="pane-head"> <div class="pane-head">
<span class="label">对话</span> <span class="label">对话</span>
<span class="spacer"></span> <span class="spacer"></span>
<button id="btn-export" class="small" disabled>导出对话</button>
<button id="btn-clear-msgs" class="small" disabled title="清空当前任务的对话历史(messages + token 累计归零),工作目录文件保留">清空对话</button>
<button id="btn-done" class="small" disabled>完成</button> <button id="btn-done" class="small" disabled>完成</button>
<button id="btn-abandon" class="small danger" disabled>废弃</button> <button id="btn-task-menu" class="small dd-toggle" disabled title="更多任务操作(导出 / 清空 / 废弃 / 删除)"></button>
<button id="btn-delete-task" class="small danger" disabled title="硬删除:清 DB 行 + messages,FS 文件不动">删除</button>
</div> </div>
<div id="chat-meta"><span class="muted">(未选中任务)</span></div> <div id="chat-meta"><span class="muted">(未选中任务)</span></div>
<div id="wd-concurrent-warn" style="display:none;"></div> <div id="wd-concurrent-warn" style="display:none;"></div>

View File

@ -174,6 +174,8 @@ function renderTaskList(tasks, append = false) {
function taskMenuItems(t) { function taskMenuItems(t) {
const isActive = t.status === "active"; const isActive = t.status === "active";
const hasMsg = (t.n_messages || 0) > 0; const hasMsg = (t.n_messages || 0) > 0;
// run_status 仅 taskMeta(中栏 ⋯)带;列表行摘要无此字段 → undefined → running=false(与改前一致)
const running = t.run_status === "running" || t.run_status === "cancelling";
return [ return [
{ act: "complete", label: "完成", cls: "act-complete", disabled: !isActive, { act: "complete", label: "完成", cls: "act-complete", disabled: !isActive,
onclick: () => setTaskStatus(t.task_id, "completed", t.name || "(未命名)") }, onclick: () => setTaskStatus(t.task_id, "completed", t.name || "(未命名)") },
@ -181,7 +183,7 @@ function taskMenuItems(t) {
onclick: () => setTaskStatus(t.task_id, "abandoned", t.name || "(未命名)") }, onclick: () => setTaskStatus(t.task_id, "abandoned", t.name || "(未命名)") },
{ act: "export", label: "导出对话", cls: "act-export", disabled: !hasMsg, { act: "export", label: "导出对话", cls: "act-export", disabled: !hasMsg,
onclick: () => exportTask(t.task_id) }, onclick: () => exportTask(t.task_id) },
{ act: "clear", label: "清空对话", cls: "act-clear", disabled: !hasMsg, { act: "clear", label: "清空对话", cls: "act-clear", disabled: !hasMsg || running,
onclick: () => clearMessages(t.task_id, t.name || "(未命名)", t.n_messages || 0) }, onclick: () => clearMessages(t.task_id, t.name || "(未命名)", t.n_messages || 0) },
{ act: "delete", label: "删除", cls: "act-delete", { act: "delete", label: "删除", cls: "act-delete",
onclick: () => deleteTask(t.task_id, t.name || "(未命名)", t.n_messages || 0) }, onclick: () => deleteTask(t.task_id, t.name || "(未命名)", t.n_messages || 0) },
@ -314,15 +316,9 @@ function renderChatMeta() {
$("chat-form").style.display = active ? "flex" : "none"; $("chat-form").style.display = active ? "flex" : "none";
syncOptimizeBtn(); syncOptimizeBtn();
$("btn-done").disabled = !active; $("btn-done").disabled = !active;
$("btn-abandon").disabled = !active; // ⋯ 菜单:选中即可用;各项 enable/disable(完成/废弃按 status、清空按 run_status+n_messages)
$("btn-delete-task").disabled = false; // delete 不限 status(用户显式 confirm) // 全在 taskMenuItems 内部判定,这里只管整体可用性。
// 导出 / 清空:只要选中 task 就允许点(不按 n_messages 门禁 —— 历史 bug: $("btn-task-menu").disabled = false;
// 清空后 n_messages=0 disable,但新对话进来后 taskMeta 不重渲一直 disable;
// 0 条时点击不会出错(导出空 docx / 清空 confirm 显 0 条),让 UX 一致更省心)。
$("btn-export").disabled = false;
// 清空对话:仅活跃 run 期间禁用(后端 409,confirm 通过后才报错 UX 差)
const running = t.run_status === "running" || t.run_status === "cancelling";
$("btn-clear-msgs").disabled = running;
} }
function renderModelDropdown(t) { function renderModelDropdown(t) {
@ -1080,17 +1076,11 @@ function appendErrorCard(msg) {
// ───── done / abandon / delete / export ───── // ───── done / abandon / delete / export ─────
$("btn-done").onclick = () => state.taskId && setTaskStatus(state.taskId, "completed", (state.taskMeta && state.taskMeta.name) || ""); $("btn-done").onclick = () => state.taskId && setTaskStatus(state.taskId, "completed", (state.taskMeta && state.taskMeta.name) || "");
$("btn-abandon").onclick = () => state.taskId && setTaskStatus(state.taskId, "abandoned", (state.taskMeta && state.taskMeta.name) || ""); // 其余操作(废弃/导出/清空/删除)走与任务行同款 ⋯ 浮层菜单;「完成」已是独立按钮 → 菜单里去掉
$("btn-delete-task").onclick = () => { $("btn-task-menu").onclick = (e) => {
if (!state.taskId) return; if (!state.taskId || !state.taskMeta) return;
const t = state.taskMeta || {}; e.stopPropagation();
deleteTask(state.taskId, t.name || "(未命名)", t.n_messages || 0); showMenu($("btn-task-menu"), taskMenuItems(state.taskMeta).filter((it) => it.act !== "complete"));
};
$("btn-export").onclick = () => state.taskId && exportTask(state.taskId);
$("btn-clear-msgs").onclick = () => {
if (!state.taskId) return;
const t = state.taskMeta || {};
clearMessages(state.taskId, t.name || "(未命名)", t.n_messages || 0);
}; };
async function clearMessages(tid, name, nMsg) { async function clearMessages(tid, name, nMsg) {
@ -1138,10 +1128,7 @@ async function deleteTask(tid, name, nMsg) {
renderTaskProgressDock([]); renderTaskProgressDock([]);
$("chat-form").style.display = "none"; $("chat-form").style.display = "none";
$("btn-done").disabled = true; $("btn-done").disabled = true;
$("btn-abandon").disabled = true; $("btn-task-menu").disabled = true;
$("btn-delete-task").disabled = true;
$("btn-export").disabled = true;
$("btn-clear-msgs").disabled = true;
} }
loadTaskList(); loadTaskList();
loadFiles(); loadFiles();