后台新增「联想词按键」设置:可用 , 和 . 代替 ↑/↓ 选联想词

输入法手感 —— 习惯中文输入法的话,手不用离开主键区就能在联想下拉里选词。

- 设置项 suggest_keys:arrows(默认,等同改造前)/ punct(, 和 .)/ both。
  后台为三选一下拉,保存走白名单回退(非白名单一律 arrows),默认值 = 旧行为。
- app.js 认**物理键**(e.code 的 Comma / Period)而不是字符 —— 中文输入法下按逗号
  打出来的是全角「,」,e.key 会变成「,」,但物理键不变;「跟输入法一样」就该这么认。
- ⚠️ 逗号和句号同时是搜索框的合法输入字符(如「python, pandas」)—— 所以只在
  **联想下拉打开时**才把它们当选词键;没有下拉时放行,照常输入。这条边界写进了
  后台说明(想输入逗号本身先按 Esc 关掉下拉)。
- 「,」= 上一条(同 ↑)、「.」= 下一条(同 ↓),首尾循环、与方向键行为完全一致。
- 前台输出两处同源标记:#search-form 的 data-keys(供断言/排查)与
  window.SIMPLENAV.suggestKeys(JS 实际读它决定用哪组键)。

验证:
- jsdom 交互测试 54/54 —— arrows / punct / both 三种模式各跑一遍;覆盖全角「,」「。」
  (中文输入法场景)、「没有下拉时 ,/. 不被拦截」(关键回归)、
  以及上一轮的核心回归「直接回车用原文」在三种模式下都成立。
- 反向验证两个变体:把回车改回旧行为 → 「直接回车不改动输入框」精确失败(27 项);
  把「没下拉放行」改坏 → 恰好只失败「没有下拉时按 ./, 不被拦截」那两条(2 项),
  其余全过 —— 两条核心断言各自有效、定位精准。
- e2e 128/128(新增联想按键场景:保存 → data-keys / suggestKeys 同步、
  非法值回退 arrows、保存不动其它设置)。
- 线上部署 0 FAIL(77 条静态断言、38 文件哈希逐字节一致);线上几何 25/25;
  直连线上核验 data-keys / suggestKeys / app.js 内容均已就位。
This commit is contained in:
2026-09-28 20:25:33 +08:00
parent 9225113dae
commit 13b12a3d67
5 changed files with 55 additions and 7 deletions
+4
View File
@@ -216,12 +216,16 @@ try {
// 打开方式:'0' = 当前页面;其余(含字段缺失、被篡改成怪值)一律回到 '1'(新标签页) // 打开方式:'0' = 当前页面;其余(含字段缺失、被篡改成怪值)一律回到 '1'(新标签页)
// ——默认必须等于改造前的旧行为,这样老库里已有的值上线后不会突然变样。 // ——默认必须等于改造前的旧行为,这样老库里已有的值上线后不会突然变样。
$open = (string)($_POST['open_blank'] ?? '1') === '0' ? '0' : '1'; $open = (string)($_POST['open_blank'] ?? '1') === '0' ? '0' : '1';
// 联想词按键:arrows(↓/↑)/ punct(, 和 .)/ both;非白名单一律回到 arrows
$keys = (string)($_POST['suggest_keys'] ?? 'arrows');
if (!in_array($keys, ['arrows', 'punct', 'both'], true)) { $keys = 'arrows'; }
$map = [ $map = [
'site_name' => trim((string)($_POST['site_name'] ?? '')), 'site_name' => trim((string)($_POST['site_name'] ?? '')),
'subtitle' => trim((string)($_POST['subtitle'] ?? '')), 'subtitle' => trim((string)($_POST['subtitle'] ?? '')),
'default_engine' => preg_replace('/[^a-z0-9\-_]/i', '', (string)($_POST['default_engine'] ?? '')), 'default_engine' => preg_replace('/[^a-z0-9\-_]/i', '', (string)($_POST['default_engine'] ?? '')),
'primary_color' => preg_match('/^#[0-9a-fA-F]{3,8}$/', (string)($_POST['primary_color'] ?? '')) ? $_POST['primary_color'] : '#2F6BF7', 'primary_color' => preg_match('/^#[0-9a-fA-F]{3,8}$/', (string)($_POST['primary_color'] ?? '')) ? $_POST['primary_color'] : '#2F6BF7',
'open_blank' => $open, 'open_blank' => $open,
'suggest_keys' => $keys,
'icon_services' => isset($_POST['icon_services']) ? '1' : '0', 'icon_services' => isset($_POST['icon_services']) ? '1' : '0',
'suggest_source' => $src, 'suggest_source' => $src,
'collections_layout' => $coll, 'collections_layout' => $coll,
+14
View File
@@ -321,6 +321,20 @@ else: // settings ?>
所以默认「统一用百度」——这样切到任何引擎下拉都有词。选「各搜索引擎自己的接口」则恢复成 所以默认「统一用百度」——这样切到任何引擎下拉都有词。选「各搜索引擎自己的接口」则恢复成
各用各的(没恢复好的引擎会自动回退到百度的建议)。 各用各的(没恢复好的引擎会自动回退到百度的建议)。
</p> </p>
<label>联想词按键
<?php $sk = setting('suggest_keys', 'arrows'); ?>
<select name="suggest_keys">
<option value="arrows" <?= $sk === 'arrows' ? 'selected' : '' ?>>↑ / ↓ 方向键(默认)</option>
<option value="punct" <?= $sk === 'punct' ? 'selected' : '' ?>>, 和 .(输入法手感)</option>
<option value="both" <?= $sk === 'both' ? 'selected' : '' ?>>两者都可以</option>
</select>
</label>
<p class="hint span2">
决定用哪组键在联想下拉里<b>选词</b>:选中某条后回车才按该词搜索;不选就直接回车 = 用你输入的原文。
「, 和 .」跟输入法一样,手不用离开主键区,习惯中文输入法的话更顺手。
⚠️ 选它之后,<b>联想下拉打开时</b>逗号与句号会当作选词键,不再直接输入 ——
想搜「python, pandas」这类带逗号的词,先按 <b>Esc</b> 关掉下拉再输。
</p>
<label>合集显示方式 <label>合集显示方式
<?php $cl = setting('collections_layout', 'tabs'); ?> <?php $cl = setting('collections_layout', 'tabs'); ?>
<select name="collections_layout"> <select name="collections_layout">
+26 -5
View File
@@ -31,7 +31,17 @@
var lastQ = null; var lastQ = null;
var seq = 0; // 请求序号:防止先发的慢响应覆盖后发的快响应 var seq = 0; // 请求序号:防止先发的慢响应覆盖后发的快响应
var items = []; // 当前展示的建议词 var items = []; // 当前展示的建议词
var active = -1; // 选中项下标(键盘 ↓/↑ 与鼠标划过共用);-1 = 没选中 var active = -1; // 选中项下标(键盘 ↓/↑、,/. 与鼠标划过共用);-1 = 没选中
/* 联想词的选中按键(后台「设置 → 联想词按键」):
arrows(默认,等同改造前)/ punct / both */
var keyMode = cfg.suggestKeys || 'arrows';
function acceptsArrows() { return keyMode === 'arrows' || keyMode === 'both'; }
function acceptsPunct() { return keyMode === 'punct' || keyMode === 'both'; }
/* ⚠️ 认**物理键**而不是字符:中文输入法下按逗号打出的是全角「,」,e.key 会变成「,」,
但 e.code 仍然是 'Comma' —— 「跟输入法一样」就该按物理键认,否则中文用户按不出来。 */
function isComma(e) { return e.code === 'Comma' || e.key === ','; }
function isPeriod(e) { return e.code === 'Period' || e.key === '.'; }
function hideWord() { function hideWord() {
if (word) { word.innerHTML = ''; word.style.display = 'none'; } if (word) { word.innerHTML = ''; word.style.display = 'none'; }
@@ -85,14 +95,25 @@
input.addEventListener('keydown', function (e) { input.addEventListener('keydown', function (e) {
var open = !!word && word.style.display === 'block' && items.length > 0; var open = !!word && word.style.display === 'block' && items.length > 0;
// ↓ / ↑:在下拉里移动选中项(只动高亮,不动输入框;回车时才落到输入框) // 移动选中项:只动高亮、不动输入框(回车时才落到输入框)。
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { // 用哪组键由后台「联想词按键」决定:arrows = ↓/↑ / punct = , 和 . / both = 都行。
var dir = 0;
if (acceptsArrows() && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
dir = e.key === 'ArrowDown' ? 1 : -1;
} else if (acceptsPunct() && isComma(e)) {
dir = -1; // , = 上一条(同 ↑)
} else if (acceptsPunct() && isPeriod(e)) {
dir = 1; // . = 下一条(同 ↓)
}
if (dir !== 0) {
// ⚠️ 没有下拉时**必须放行** —— 这时 , 和 . 是正常输入字符(如搜「python, pandas」)。
if (!open) { return; } if (!open) { return; }
e.preventDefault(); // 别让光标跳到文本首尾、也别让页面跟着滚 e.preventDefault(); // 别让光标跳到文本首尾、也别让页面跟着滚
if (active < 0) { if (active < 0) {
active = e.key === 'ArrowDown' ? 0 : items.length - 1; active = dir > 0 ? 0 : items.length - 1;
} else { } else {
active = (active + (e.key === 'ArrowDown' ? 1 : -1) + items.length) % items.length; active = (active + dir + items.length) % items.length;
} }
paintWord(); paintWord();
return; return;
+3
View File
@@ -107,6 +107,9 @@ function seed_settings(PDO $pdo): void
// '1' = 顶部搜索框与所有站点/合集链接都在**新标签页**打开;'0' = 都在**当前页面**打开。 // '1' = 顶部搜索框与所有站点/合集链接都在**新标签页**打开;'0' = 都在**当前页面**打开。
// 默认 '1' 与引入该设置前的旧行为一致;老库若已有这一行不会被覆盖(INSERT OR IGNORE)。 // 默认 '1' 与引入该设置前的旧行为一致;老库若已有这一行不会被覆盖(INSERT OR IGNORE)。
'open_blank' => '1', 'open_blank' => '1',
// 'arrows' = ↓/↑ 选词(默认,与引入该设置前的旧行为一致);
// 'punct' = 用 , 和 . 选词(输入法手感);'both' = 两组都认。
'suggest_keys' => 'arrows',
]; ];
$stmt = $pdo->prepare('INSERT OR IGNORE INTO settings (k, v) VALUES (?, ?)'); $stmt = $pdo->prepare('INSERT OR IGNORE INTO settings (k, v) VALUES (?, ?)');
foreach ($defaults as $k => $v) { foreach ($defaults as $k => $v) {
+8 -2
View File
@@ -32,6 +32,12 @@ function open_target(): string
$openTarget = open_target(); $openTarget = open_target();
$openMode = $openTarget === '_blank' ? 'blank' : 'self'; $openMode = $openTarget === '_blank' ? 'blank' : 'self';
// 联想词的选中按键(后台「设置 → 联想词按键」):
// arrows = 只认 ↓/↑(默认,等同改造前) / punct = 只认 , 和 . (输入法手感)/ both = 都认
// 白名单回退:任何非法值都回到 arrows,别让手改库/被篡改的表单把交互搞坏。
$suggestKeys = setting('suggest_keys', 'arrows');
if (!in_array($suggestKeys, ['arrows', 'punct', 'both'], true)) { $suggestKeys = 'arrows'; }
// 默认引擎 // 默认引擎
$defSlug = setting('default_engine', 'bing'); $defSlug = setting('default_engine', 'bing');
$default = null; $default = null;
@@ -122,7 +128,7 @@ function site_tile(array $s): void
</div> </div>
<form id="search-form" action="<?= e($default['search_url'] ?? 'https://cn.bing.com/search') ?>" method="get" target="<?= e($openTarget) ?>"<?= $openTarget === '_blank' ? ' rel="noopener"' : '' ?> <form id="search-form" action="<?= e($default['search_url'] ?? 'https://cn.bing.com/search') ?>" method="get" target="<?= e($openTarget) ?>"<?= $openTarget === '_blank' ? ' rel="noopener"' : '' ?>
data-slug="<?= e($default['slug'] ?? '') ?>"> data-slug="<?= e($default['slug'] ?? '') ?>" data-keys="<?= e($suggestKeys) ?>">
<input type="text" id="search-text" name="<?= e($default['param'] ?? 'q') ?>" <input type="text" id="search-text" name="<?= e($default['param'] ?? 'q') ?>"
placeholder="<?= e(($default['name'] ?? '搜索') . '搜索') ?>" autocomplete="off"> placeholder="<?= e(($default['name'] ?? '搜索') . '搜索') ?>" autocomplete="off">
<button class="submit" type="submit" aria-label="搜索"> <button class="submit" type="submit" aria-label="搜索">
@@ -188,7 +194,7 @@ function site_tile(array $s): void
</footer> </footer>
</div> </div>
<script>window.SIMPLENAV = { suggest: "<?= e(url('suggest.php')) ?>" };</script> <script>window.SIMPLENAV = { suggest: "<?= e(url('suggest.php')) ?>", suggestKeys: "<?= e($suggestKeys) ?>" };</script>
<script src="<?= e(asset('assets/js/app.js')) ?>"></script> <script src="<?= e(asset('assets/js/app.js')) ?>"></script>
</body> </body>
</html> </html>