From 9225113dae003c241ed281f3784176e8acfa585d Mon Sep 17 00:00:00 2001 From: laoyang Date: Mon, 28 Sep 2026 20:01:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=90=9C=E7=B4=A2=E6=A1=86=EF=BC=9A=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E3=80=8C=E7=9B=B4=E6=8E=A5=E5=9B=9E=E8=BD=A6=E8=A2=AB?= =?UTF-8?q?=E5=A5=97=E7=94=A8=E7=AC=AC=E4=B8=80=E6=9D=A1=E8=81=94=E6=83=B3?= =?UTF-8?q?=E8=AF=8D=E3=80=8D=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原实现只要联想下拉开着,回车就把输入框内容替换成第一条建议 —— 用户自己输入、 而建议里没有的词会被悄悄换掉。现在只有**用 ↓/↑ 选中**(或鼠标划过)某条建议时, 回车才按它搜索;没选中就一律用输入框原文。 - app.js:引入选中项状态(items / active)。↓/↑ 在下拉里循环移动高亮、Escape 关闭、 一开始打字就取消选中;鼠标划过同步高亮,回车即可确认。选中态只动高亮、不动输入框, 回车时才落到输入框。 - style.css:键盘高亮复用 hover 的样式(#word li:hover, #word li.active)。 验证: - jsdom 交互测试 25/25 —— 喂的是**真实渲染出来的首页 HTML**,并显式执行仓库里 的 app.js 源码(jsdom 不会自己加载外部 script)。核心断言是「直接回车不改动输入框」。 - 反向验证:把回车逻辑改回旧行为后,该核心断言确实失败(连同 9 项方向键断言), 说明这套测试真能抓住这个 bug。 - e2e 113/113 无回归;线上部署 0 FAIL(68 条静态断言,含负向断言「旧行为已移除」; 38 个文件哈希逐字节一致);线上几何 25/25; 再直连线上核验 app.js / style.css 内容与版本号均已更新。 --- assets/css/style.css | 2 +- assets/js/app.js | 69 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/assets/css/style.css b/assets/css/style.css index 72cd779..06faa3d 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -94,7 +94,7 @@ a { text-decoration: none; color: inherit; } font-size: 15px; border-radius: 10px; color: #3d4753; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -#word li:hover { background: var(--primary-soft); color: var(--primary); } +#word li:hover, #word li.active { background: var(--primary-soft); color: var(--primary); } /* ---------- 板块卡片 ---------- */ .mylist { diff --git a/assets/js/app.js b/assets/js/app.js index 4d01363..e18e440 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -30,11 +30,20 @@ var timer = null; var lastQ = null; var seq = 0; // 请求序号:防止先发的慢响应覆盖后发的快响应 + var items = []; // 当前展示的建议词 + var active = -1; // 选中项下标(键盘 ↓/↑ 与鼠标划过共用);-1 = 没选中 - function hideWord() { if (word) { word.innerHTML = ''; word.style.display = 'none'; } } - function showWord(items) { + function hideWord() { + if (word) { word.innerHTML = ''; word.style.display = 'none'; } + items = []; + active = -1; + } + + function showWord(list) { if (!word) return; - if (!items || !items.length) { hideWord(); return; } + items = (list || []).slice(); + active = -1; // 换了新建议就取消选中:此时回车应当用输入框原文 + if (!items.length) { hideWord(); return; } word.innerHTML = items.map(function (t) { var d = document.createElement('li'); d.textContent = t; @@ -43,6 +52,14 @@ word.style.display = 'block'; } + /** 把 .active 标到当前选中项上(键盘与鼠标共用这一个状态) */ + function paintWord() { + if (!word) return; + Array.prototype.forEach.call(word.children, function (li, i) { + if (i === active) { li.classList.add('active'); } else { li.classList.remove('active'); } + }); + } + function askSuggest() { if (!input) return; var q = input.value.trim(); @@ -60,18 +77,56 @@ if (input) { input.addEventListener('input', function () { clearTimeout(timer); + active = -1; // 一开始打字就取消选中:之后回车应当用输入框原文 + paintWord(); timer = setTimeout(askSuggest, 180); }); input.addEventListener('keydown', function (e) { - if (e.key === 'Enter' && word && word.style.display === 'block' && word.firstChild) { - // 默认走第一条建议 - input.value = word.firstChild.textContent; - hideWord(); + var open = !!word && word.style.display === 'block' && items.length > 0; + + // ↓ / ↑:在下拉里移动选中项(只动高亮,不动输入框;回车时才落到输入框) + if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { + if (!open) { return; } + e.preventDefault(); // 别让光标跳到文本首尾、也别让页面跟着滚 + if (active < 0) { + active = e.key === 'ArrowDown' ? 0 : items.length - 1; + } else { + active = (active + (e.key === 'ArrowDown' ? 1 : -1) + items.length) % items.length; + } + paintWord(); + return; + } + + if (e.key === 'Escape') { hideWord(); return; } + + if (e.key === 'Enter') { + // 只有**用 ↓/↑ 选中**了某条建议,回车才按建议词搜索。 + // 没选中就一律用输入框里的原文 —— 老实现是「回车就套用第一条建议」, + // 会把用户自己输入、而建议里没有的词悄悄换掉。 + if (open && active >= 0) { + input.value = items[active]; + hideWord(); + e.preventDefault(); + if (form) { form.submit(); } + } + // active < 0:不拦截,交给浏览器默认的表单提交(即用输入框原文搜) + return; } }); } + /* 鼠标划过某条建议 → 同步高亮(与 ↓/↑ 共用 active,回车即可确认) */ + document.addEventListener('mouseover', function (e) { + if (!word || !e.target.closest) { return; } + var hovered = e.target.closest('#word li'); + if (!hovered) { return; } + Array.prototype.forEach.call(word.children, function (li, i) { + if (li === hovered) { active = i; } + }); + paintWord(); + }); + document.addEventListener('click', function (e) { if (word && !word.contains(e.target) && e.target !== input) { hideWord(); } var li = e.target.closest ? e.target.closest('#word li') : null;