From 4864bf337b00265303939500672b050ca25c0222 Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Thu, 6 Aug 2026 07:39:07 +0800 Subject: [PATCH] =?UTF-8?q?emrg:=20GUI=20=E5=8F=B3=E9=94=AE=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E8=A1=A5=E5=85=A8=E9=87=8D=E5=91=BD=E5=90=8D=20?= =?UTF-8?q?=E2=80=94=20=E5=AF=B9=E8=AF=9D=E7=AE=A1=E7=90=86=E9=97=AD?= =?UTF-8?q?=E7=8E=AF=EF=BC=88=E8=AE=BE=E8=AE=A1=20=C2=A73.2=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复 #417 遗留:sidebar.js 注释声明"右键重命名/删除",但 showConvMenu 只实现了删除(注释自认"P3 完善重命名")。main.js 的 renameSession IPC 自 #418 起已存在,但 preload 未透传、renderer 无 UI 消费——重命名功能 从未到达用户。 改动: - index.html:新增 rename-dialog(预填当前标题,Enter 提交)+ ctx-menu 容器 - app.js:showConvMenu 改为真右键菜单(✏️ 重命名 / 🗑 删除对话), 点击别处/ESC 隐藏,视口内定位 - dialogs.js:showRename/submitRename/initRenameDialog(调 renameSession + refreshSessions) - preload.js:透传 renameSession(main.js handler 已存在,补桥接) - components.css:ctx-menu/ctx-item 样式(白底卡片+阴影+大圆角,danger 红色) - renderer.smoke.test.js:新增右键菜单+重命名测试(35 用例全绿) 验证:npm test 35 绿 + pytest 472 绿 + node --check 全通过 --- emrg/gui/preload.js | 1 + emrg/gui/renderer/css/components.css | 29 +++++++++++++++++++ emrg/gui/renderer/index.html | 17 +++++++++++ emrg/gui/renderer/js/app.js | 35 ++++++++++++++++++++-- emrg/gui/renderer/js/dialogs.js | 43 ++++++++++++++++++++++++++++ emrg/gui/test/renderer.smoke.test.js | 37 ++++++++++++++++++++++++ 6 files changed, 159 insertions(+), 3 deletions(-) diff --git a/emrg/gui/preload.js b/emrg/gui/preload.js index b3485800..5c676cd9 100644 --- a/emrg/gui/preload.js +++ b/emrg/gui/preload.js @@ -13,6 +13,7 @@ const api = { switchSession: (payload) => ipcRenderer.invoke("emrg:switchSession", payload), newSession: () => ipcRenderer.invoke("emrg:newSession"), deleteSession: (payload) => ipcRenderer.invoke("emrg:deleteSession", payload), + renameSession: (payload) => ipcRenderer.invoke("emrg:renameSession", payload), setModel: (payload) => ipcRenderer.invoke("emrg:setModel", payload), listModels: () => ipcRenderer.invoke("emrg:listModels"), saveSettings: (payload) => ipcRenderer.invoke("emrg:saveSettings", payload), diff --git a/emrg/gui/renderer/css/components.css b/emrg/gui/renderer/css/components.css index 9bc21362..f6496fcd 100644 --- a/emrg/gui/renderer/css/components.css +++ b/emrg/gui/renderer/css/components.css @@ -741,3 +741,32 @@ dialog::backdrop { color: var(--text-2); margin-bottom: var(--sp-2); } + +/* ── 右键菜单(对话列表:重命名/删除) ─────────── */ +.ctx-menu { + position: fixed; + z-index: 9999; + min-width: 140px; + background: var(--bg-panel); + border: 1px solid var(--border); + border-radius: var(--radius-card); + box-shadow: var(--shadow-lg); + padding: 4px; + animation: fade-in var(--dur-fast) var(--ease); +} +.ctx-item { + display: block; + width: 100%; + text-align: left; + padding: 8px 12px; + border: none; + background: none; + border-radius: 8px; + font-size: var(--fs-secondary); + color: var(--text-1); + cursor: pointer; + transition: background-color var(--dur-fast) var(--ease); +} +.ctx-item:hover { background-color: var(--bg-soft); } +.ctx-item.danger { color: var(--danger, #e5484d); } +.ctx-item.danger:hover { background-color: color-mix(in srgb, var(--danger, #e5484d) 10%, transparent); } diff --git a/emrg/gui/renderer/index.html b/emrg/gui/renderer/index.html index fb1bfbc9..b052d8bd 100644 --- a/emrg/gui/renderer/index.html +++ b/emrg/gui/renderer/index.html @@ -162,6 +162,23 @@

确认

+ + +
+

重命名对话

+ +
+ + +
+
+
+ + + + diff --git a/emrg/gui/renderer/js/app.js b/emrg/gui/renderer/js/app.js index 2998140f..fc09b305 100644 --- a/emrg/gui/renderer/js/app.js +++ b/emrg/gui/renderer/js/app.js @@ -173,12 +173,34 @@ const App = (() => { // ── 右键菜单(重命名 / 删除) ─────────── function showConvMenu(item, sid, title) { - // 简单版:确认删除(P3 完善重命名)。右键菜单容器复用 conv-item 定位。 - Dialogs.showConfirm(EMRG_Copy.COPY.deleteConfirmTitle, EMRG_Copy.COPY.deleteConfirmBody, { + // 设计 §3.2:右键菜单 = 重命名 / 删除(删除有友好确认) + const menu = $("ctx-menu"); + menu.innerHTML = ""; + const mk = (label, danger, action) => { + const b = el("button", { class: "ctx-item" + (danger ? " danger" : "") }, label); + b.addEventListener("click", () => { + hideCtxMenu(); + action(); + }); + menu.appendChild(b); + }; + mk("✏️ 重命名", false, () => Dialogs.showRename(sid, title)); + mk("🗑 删除对话", true, () => Dialogs.showConfirm(EMRG_Copy.COPY.deleteConfirmTitle, EMRG_Copy.COPY.deleteConfirmBody, { okText: "删除", danger: true, onOk: () => deleteSession(sid), - }); + })); + menu.hidden = false; + // 定位在右键处,超出视口则上移/左移 + const rect = item.getBoundingClientRect(); + menu.style.left = Math.min(rect.right, window.innerWidth - 160) + "px"; + menu.style.top = Math.min(rect.bottom, window.innerHeight - 80) + "px"; + } + + function hideCtxMenu() { + const menu = $("ctx-menu"); + menu.hidden = true; + menu.innerHTML = ""; } // ── 模型切换器 ───────────────────────── @@ -439,6 +461,11 @@ const App = (() => { input.style.height = Math.min(input.scrollHeight, 150) + "px"; }); + // 右键菜单:点击别处隐藏(showConvMenu 内已绑定选项点击) + document.addEventListener("click", (e) => { + if (!$("ctx-menu").hidden && !e.target.closest("#ctx-menu")) hideCtxMenu(); + }); + // 快捷键:⌘N 新对话 / ⌘B 折叠侧边栏 / ⌘, 设置 / ESC 停止或关弹窗 document.addEventListener("keydown", (e) => { if (e.metaKey || e.ctrlKey) { @@ -455,6 +482,7 @@ const App = (() => { return; } if (e.key === "Escape") { + hideCtxMenu(); // 右键菜单优先关闭 const dlgs = document.querySelectorAll("dialog[open]"); if (dlgs.length) return; // dialog 原生 ESC 处理 if (state.busy) { @@ -466,6 +494,7 @@ const App = (() => { Dialogs.initThemeButtons(); Dialogs.initModelForm(); + Dialogs.initRenameDialog(); initModelSwitcher(); } diff --git a/emrg/gui/renderer/js/dialogs.js b/emrg/gui/renderer/js/dialogs.js index 7d21f832..74c24a9c 100644 --- a/emrg/gui/renderer/js/dialogs.js +++ b/emrg/gui/renderer/js/dialogs.js @@ -274,6 +274,46 @@ const Dialogs = (() => { } } + // ── 重命名对话(右键菜单 → 重命名) ──── + let renameSid = null; + function showRename(sid, currentTitle) { + renameSid = sid; + const input = $("rename-input"); + input.value = (currentTitle && currentTitle !== sid) ? currentTitle : ""; + $("rename-dialog").showModal(); + input.focus(); + input.select(); + } + + async function submitRename() { + if (!renameSid) return; + const input = $("rename-input"); + const title = input.value.trim(); + if (!title) return; // 空名不提交 + try { + await window.emrg.renameSession({ sessionId: renameSid, title }); + renameSid = null; + $("rename-dialog").close(); + await App.refreshSessions(); + } catch (e) { + Chat.addSystemMessage(`重命名失败了:${e.message}`); + } + } + + function initRenameDialog() { + $("rename-cancel").addEventListener("click", () => { + renameSid = null; + $("rename-dialog").close(); + }); + $("rename-ok").addEventListener("click", submitRename); + $("rename-input").addEventListener("keydown", (e) => { + if (e.key === "Enter") { + e.preventDefault(); + submitRename(); + } + }); + } + // ── 确认对话框(替代 confirm/alert) ──── let confirmCb = null; function showConfirm(title, message, opts = {}) { @@ -302,6 +342,9 @@ const Dialogs = (() => { return { initThemeButtons, initModelForm, + initRenameDialog, + showRename, + submitRename, showSettings, saveSettings, showWelcome, diff --git a/emrg/gui/test/renderer.smoke.test.js b/emrg/gui/test/renderer.smoke.test.js index 79dc97ca..b244f83a 100644 --- a/emrg/gui/test/renderer.smoke.test.js +++ b/emrg/gui/test/renderer.smoke.test.js @@ -55,6 +55,7 @@ function makeEl(id) { insertBefore(c) { this.children.unshift(c); return c; }, remove() {}, focus() {}, + select() {}, }; return node; } @@ -66,6 +67,7 @@ const ELEMENT_IDS = [ "set-model", "pick-dir-btn", "theme-options", "welcome-dialog", "welcome-api-key", "welcome-base-url", "welcome-model", "welcome-project-dir", "welcome-pick-btn", "welcome-save", "confirm-dialog", "confirm-title", "confirm-message", "confirm-cancel", "confirm-ok", "main", + "rename-dialog", "rename-input", "rename-cancel", "rename-ok", "ctx-menu", "model-list", "add-model-btn", "model-form", "model-form-name", "model-form-id", "model-form-vision", "model-form-save", "model-form-cancel", "back-to-bottom", ]; @@ -234,3 +236,38 @@ test("多模型管理:modelDetails 加载渲染 + saveSettings 传 models 数 const gpt = saved.models.find((m) => m.name === "gpt-4o"); assert.strictEqual(gpt.vision, true); }); + +test("右键菜单:重命名对话框 → renameSession 调用(设计 §3.2)", async () => { + let renamed = null; + const { ctx, els } = makeSandbox({ + renameSession: async (payload) => { + renamed = payload; + return { ok: true, title: payload.title }; + }, + listSessions: async () => [ + { session_id: "s1", title: "旧标题", created_at: "2026-08-06T00:00:00" }, + ], + }); + // 模拟 App.showConvMenu(右键菜单构建) + await tick(); + vm.runInContext('App.showConvMenu({ getBoundingClientRect: () => ({ right: 100, bottom: 100 }) }, "s1", "旧标题")', ctx); + const menu = els["ctx-menu"]; + assert.strictEqual(menu.hidden, false, "右键菜单应显示"); + assert.ok(menu.children.length >= 2, "菜单应有 重命名 + 删除 两项"); + const labels = menu.children.map((c) => c.textContent); + assert.ok(labels.some((l) => l.includes("重命名")), `菜单应含重命名,实际 ${labels}`); + assert.ok(labels.some((l) => l.includes("删除")), `菜单应含删除,实际 ${labels}`); + + // 点击重命名 → 对话框打开 + await vm.runInContext('Dialogs.showRename("s1", "旧标题")', ctx); + assert.strictEqual(els["rename-dialog"].open, true, "重命名对话框应打开"); + assert.strictEqual(els["rename-input"].value, "旧标题", "输入框预填当前标题"); + + // 提交新标题 → renameSession 调用 + els["rename-input"].value = "新标题"; + await vm.runInContext("Dialogs.submitRename()", ctx); + await tick(); + assert.ok(renamed, "renameSession 应被调用"); + assert.strictEqual(renamed.sessionId, "s1"); + assert.strictEqual(renamed.title, "新标题"); +});