联想词键盘选词彻底修复:上下键 / 逗号句号 / 输入法组合上屏四条路径全通
背景:用户反馈「上下键和逗号句号都无法选择候选词,只能用鼠标」。逐条查下去
发现是**三个各自独立的原因**叠在一起,任何一个没修都会让人觉得「还是坏的」。
① 单组按键模式让另一组键静默失效
线上 settings.suggest_keys 停在 'punct'(只认 , 和 .)—— 是该模式的正确
行为,但用户不知道那是设置项在起作用,只当坏了。
改法:默认值改为 'both'(↓/↑ 与 , . 都认)。'both' 是 'arrows' 与 'punct'
各自行为的**严格超集**,升级不会让任何人的习惯失效,只是多了一组。
老库用**一次性迁移**(哨兵键 mig_suggest_keys_both 把关)把已知单组值并成
'both'。⚠️ 迁移必须只跑一次:db_init() 每次请求都执行,无条件 UPDATE 会把
用户在后台特意选回的 'arrows' 于下一个请求改回去 —— 设置项直接失效。
同时同步三处默认值:index.php / admin/index.php / admin/actions.php。
② 【元凶】beforeinput 只认 insertText,漏了 insertCompositionText
输入法把标点当**组合内容**提交时,beforeinput 的 inputType 是
insertCompositionText,旧代码直接 return → 这条路径完全失效。
修法:两种 inputType 都认。
③ 【真机实测的硬限制】insertCompositionText 的 beforeinput cancelable = false
Chromium 里这条事件**不允许** preventDefault(CDP 实测记录:
beforeinput it:insertCompositionText comp:true data:"," pd:false cc:false),
所以「命中就拦」对这条路根本无效 —— 标点会被真的插进搜索框。
修法:加「事后收口」——beforeinput 记下快照与该移到的目标下标,
等这一帧事件洪峰过去(setTimeout 0)再把标点从输入框里撤掉、并补上选词移动。
目标下标必须**当场算好**:收口跑在宏任务里,那时 input 的 `active = -1`
已经把选中清掉了,再从 active 起算会每次都回到第一条(不累加)。
顺带把标点判定抽成共用函数 punctDir/keyPunctDir(e.key 与 e.code 互为兜底),
并修掉「方向键也无差别置去重闸门」——方向键不产生 beforeinput,置了纯属副作用,
反而会误挡紧随其后的真实标点。
测试与验证(全部实跑,非推断):
- jsdom 交互测试 89 项全过,新增「真实浏览器形态」场景(cancelable=false +
组合上屏),并**纠正了旧版一条在断言 bug 本身的断言**(原写「组合上屏不该被拦」)。
- 反向验证 6 变体全部精确挂掉对应断言(含新增的「只认 insertText」与
「方向键置闸门」两个变体);旧变体引用的 isComma/isPeriod 已随重构更新。
- **真实浏览器**(Playwright + msedge + CDP IME)21/21:上下键 / 半角 / 全角
insertText / IME 组合上屏四条路径全部能选词。
- 真实浏览器反向验证:基准全绿,删掉「事后收口」或改回只认 insertText 都精确报红。
- 迁移语义测试 16 项全过(真实 db_init,含「用户改回 arrows 后不再被覆盖」)。
- 静态断言空跑 155 条全过;线上部署 0 FAIL,诊断页 57 条断言全过。
- 线上真机直连 n.yangyq.net 21/21。
This commit is contained in:
+3
-3
@@ -268,9 +268,9 @@ try {
|
||||
// 打开方式:'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'; }
|
||||
// 联想词按键:both(都认,默认)/ arrows(↓/↑)/ punct(, 和 .);非白名单一律回到 both
|
||||
$keys = (string)($_POST['suggest_keys'] ?? 'both');
|
||||
if (!in_array($keys, ['arrows', 'punct', 'both'], true)) { $keys = 'both'; }
|
||||
$map = [
|
||||
'site_name' => trim((string)($_POST['site_name'] ?? '')),
|
||||
'subtitle' => trim((string)($_POST['subtitle'] ?? '')),
|
||||
|
||||
+9
-8
@@ -325,18 +325,19 @@ else: // settings ?>
|
||||
各用各的(没恢复好的引擎会自动回退到百度的建议)。
|
||||
</p>
|
||||
<label>联想词按键
|
||||
<?php $sk = setting('suggest_keys', 'arrows'); ?>
|
||||
<?php $sk = setting('suggest_keys', 'both'); ?>
|
||||
<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>
|
||||
<option value="both" <?= $sk === 'both' ? 'selected' : '' ?>>两者都可以(默认,推荐)</option>
|
||||
<option value="arrows" <?= $sk === 'arrows' ? 'selected' : '' ?>>只用 ↑ / ↓ 方向键</option>
|
||||
<option value="punct" <?= $sk === 'punct' ? 'selected' : '' ?>>只用 , 和 .(输入法手感)</option>
|
||||
</select>
|
||||
</label>
|
||||
<p class="hint span2">
|
||||
决定用哪组键在联想下拉里<b>选词</b>:选中某条后回车才按该词搜索;不选就直接回车 = 用你输入的原文。
|
||||
「, 和 .」跟输入法一样,手不用离开主键区,习惯中文输入法的话更顺手。
|
||||
⚠️ 选它之后,<b>联想下拉打开时</b>逗号与句号会当作选词键,不再直接输入 ——
|
||||
想搜「python, pandas」这类带逗号的词,先按 <b>Esc</b> 关掉下拉再输。
|
||||
决定用哪组键在联想下拉里<b>选词</b>:选中某条后回车才按该词搜索;不选就直接回车 = 用你输入的原文。<br>
|
||||
<b>推荐「两者都可以」</b>——手不用离开主键区,习惯中文输入法的话按 , . 更顺手,同时也保留方向键。
|
||||
⚠️ 只要联想下拉<b>打开着</b>,逗号与句号就会当作选词键、不再直接输入 ——
|
||||
想搜「python, pandas」这类带逗号的词,先按 <b>Esc</b> 关掉下拉再输。<br>
|
||||
若只留一组键,<b>另一组会完全没反应</b>(不是坏了,是这里的选择),所以除非你明确只想用一组,建议保持默认。
|
||||
</p>
|
||||
<label>合集显示方式
|
||||
<?php $cl = setting('collections_layout', 'tabs'); ?>
|
||||
|
||||
+134
-36
@@ -33,24 +33,37 @@
|
||||
var items = []; // 当前展示的建议词
|
||||
var active = -1; // 选中项下标(键盘 ↓/↑、,/. 与鼠标划过共用);-1 = 没选中
|
||||
var punctByKeyDown = false; // 本次标点是否已由 keydown 处理(防 keydown+beforeinput 双触发跳两格)
|
||||
var pendingPunct = null; // 「拦不住的标点」信息 {ch, dir, before}(见下),null = 无
|
||||
var pendingTimer = null; // 收口定时器(去抖:组合上屏会连发两次 input)
|
||||
|
||||
/* 联想词的选中按键(后台「设置 → 联想词按键」):
|
||||
arrows(默认,等同改造前)/ punct / both */
|
||||
var keyMode = cfg.suggestKeys || 'arrows';
|
||||
both(默认:两组都认)/ arrows(只认 ↓/↑)/ punct(只认 , 和 .)
|
||||
⚠️ 默认已从 arrows 改为 both(2026-09-29):用户反馈「上下键和逗号句号都选不了词」,
|
||||
而线上设置恰好是单组 punct —— 单组模式天然让**另一组键静默失效**,极易被当成 bug。
|
||||
两组都认是它们的**严格超集**:想用哪组都行,没有任何损失。 */
|
||||
var keyMode = cfg.suggestKeys || 'both';
|
||||
function acceptsArrows() { return keyMode === 'arrows' || keyMode === 'both'; }
|
||||
function acceptsPunct() { return keyMode === 'punct' || keyMode === 'both'; }
|
||||
/* ⚠️ 中文输入法的标点坑(真实用户反馈,2026-09-28):
|
||||
中文输入法下按逗号/句号,上屏的是**全角** `,`(U+FF0C) / `。`(U+3002),
|
||||
/* ⚠️ 中文输入法的标点坑(真实用户反馈,2026-09-28 / 2026-09-29):
|
||||
中文输入法下按逗号/句号,上屏的常是**全角** `,`(U+FF0C) / `。`(U+3002),
|
||||
而且此时 e.code **不一定**是 'Comma'/'Period'(取决于浏览器与输入法)——
|
||||
早先只认 `e.code === 'Comma' || e.key === ','`(半角)就是因此漏判,
|
||||
结果标点被当普通字符输进了搜索框、没当成选词键。
|
||||
修法:以 e.key 的**字符**为主判据,半角/全角都认,e.code 仅作辅助。
|
||||
全角逗号 = `,`(U+FF0C);全角句号常见是 `。`(U+3002),另兼容全角句点 `.`(U+FF0E)。 */
|
||||
function isComma(e) {
|
||||
return e.key === ',' || e.key === '\uFF0C' || e.code === 'Comma';
|
||||
早先只认半角的 e.code / e.key(半角逗号点号)就是因此漏判,
|
||||
结果标点被当普通字符输进搜索框、没当成选词键。
|
||||
修法:**e.key 与 e.code 两条都认**(互为兜底)—— 有的环境 e.key 是全角而 code 缺,
|
||||
有的环境 e.key 仍半角而 code 稳;只取其一都会漏。
|
||||
全角逗号 `,`(U+FF0C);全角句号常见 `。`(U+3002),另兼容全角句点 `.`(U+FF0E)。
|
||||
返回:-1 = 逗号(上一条,同 ↑);+1 = 句号(下一条,同 ↓);0 = 不是标点。 */
|
||||
function punctDir(ch) {
|
||||
if (ch === ',' || ch === '\uFF0C') { return -1; }
|
||||
if (ch === '.' || ch === '\u3002' || ch === '\uFF0E') { return 1; }
|
||||
return 0;
|
||||
}
|
||||
function isPeriod(e) {
|
||||
return e.key === '.' || e.key === '\u3002' || e.key === '\uFF0E' || e.code === 'Period';
|
||||
function keyPunctDir(e) {
|
||||
var d = punctDir(e.key);
|
||||
if (d !== 0) { return d; }
|
||||
if (e.code === 'Comma') { return -1; }
|
||||
if (e.code === 'Period') { return 1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
function hideWord() {
|
||||
@@ -106,6 +119,9 @@
|
||||
// ⚠️ IME 组合态保护:用中文输入法**打拼音**时也会触发 keydown(isComposing=true,
|
||||
// 或 keyCode=229),那些按键属于输入法自己的候选选择,不能当成选联想词的键拦下来。
|
||||
// 组合结束(拼音上屏)后 isComposing=false,下面的 , / . 选词才生效。
|
||||
// ⚠️ 这里的「返回」**不是**把这次标点丢掉 —— 输入法上屏标点会再走 beforeinput,
|
||||
// 那条路径会接住(见下面 beforeinput 的 insertCompositionText 分支)。
|
||||
// 所以「组合态里按标点」在用户看来照样能选词。
|
||||
if (e.isComposing || e.keyCode === 229) { return; }
|
||||
|
||||
var open = !!word && word.style.display === 'block' && items.length > 0;
|
||||
@@ -113,12 +129,12 @@
|
||||
// 移动选中项:只动高亮、不动输入框(回车时才落到输入框)。
|
||||
// 用哪组键由后台「联想词按键」决定:arrows = ↓/↑ / punct = , 和 . / both = 都行。
|
||||
var dir = 0;
|
||||
var isPunct = false; // 这次移动是否由**标点**触发(决定要不要标去重闸门)
|
||||
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; // . = 下一条(同 ↓)
|
||||
} else if (acceptsPunct()) {
|
||||
dir = keyPunctDir(e); // , = 上一条(同 ↑)/ . = 下一条(同 ↓),半角全角都认
|
||||
isPunct = dir !== 0;
|
||||
}
|
||||
|
||||
if (dir !== 0) {
|
||||
@@ -126,10 +142,13 @@
|
||||
if (!open) { return; }
|
||||
e.preventDefault(); // 别让光标跳到文本首尾、也别让页面跟着滚
|
||||
moveSelection(dir);
|
||||
// 同一次按键 keydown 已经处理过标点 → 标记一下,让紧随其后的 beforeinput 别再移一次,
|
||||
// 否则会「跳两格」(keydown 与 beforeinput 的真实触发组合因浏览器/输入法而异,jsdom 测不全)。
|
||||
// ⚠️ 只有**标点**才需要标闸门:同一次按键会先走 keydown、再走 beforeinput,
|
||||
// 不标就会「跳两格」。方向键压根不产生 beforeinput,标了纯属副作用
|
||||
// —— 反而会把紧随其后的真实标点误挡掉(两者相隔可能不足 1ms)。
|
||||
if (isPunct) {
|
||||
punctByKeyDown = true;
|
||||
setTimeout(function () { punctByKeyDown = false; }, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -150,37 +169,116 @@
|
||||
}
|
||||
});
|
||||
|
||||
/* ⚠️ beforeinput 兜底 —— 专治中文输入法的全角标点(用户实测反馈,2026-09-28)。
|
||||
某些输入法下,按逗号/句号上屏的是全角「,」「。」,这时:
|
||||
· keydown 的 e.key 可能不是半角,e.code 也可能不是 'Comma'/'Period' → 漏判;
|
||||
· 或者根本在 isComposing 组合态里被上面那道保护挡掉 → 也漏判。
|
||||
beforeinput 在**字符即将插入文本框之前**触发,e.data 就是要插入的字符,
|
||||
是拦全角标点最可靠的一道。命中就 preventDefault(不插入)并移动选中项。
|
||||
只在「下拉打开 + punct/both 模式」下生效;没有下拉时不拦,逗号句号照常输入。 */
|
||||
/* ⚠️ beforeinput 兜底 —— 专治输入法的标点(用户实测反馈,2026-09-28)。
|
||||
为什么需要它:按逗号/句号时,keydown 那一路可能整条接不住 ——
|
||||
· 输入法上屏的是**全角**「,」「。」,e.key 不是半角、e.code 也不可靠 → 漏判;
|
||||
· 或者按键发生在 isComposing 组合态里,被上面那道 IME 保护挡掉 → 漏判;
|
||||
· 甚至**根本没有 keydown**(输入法把标点作为组合内容直接提交)→ 更接不住。
|
||||
beforeinput 在**字符即将插入文本框之前**触发,e.data 就是要插入的字符,是最可靠的一道。
|
||||
⚠️ 两种 inputType 都要认(2026-09-29 实测补齐,这正是「选了标点模式也没反应」的元凶):
|
||||
· `insertText` —— 普通输入 / 部分输入法上屏标点;
|
||||
· `insertCompositionText` —— 输入法把标点当**组合内容**提交(CDP `imeSetComposition` 复现)。
|
||||
旧代码只认 insertText,于是这条路径完全失效。
|
||||
命中就 preventDefault(不让它插入)并移动选中项。
|
||||
只在「下拉打开 + punct/both 模式」下生效;没有下拉时不拦,逗号句号照常输入。
|
||||
|
||||
🔴🔴 【2026-09-29 真实浏览器实测】`insertCompositionText` 的 beforeinput 在 Chromium 里
|
||||
**cancelable = false** —— preventDefault() 根本无效!所以「组合上屏标点」这条路
|
||||
**只能事后撤销**:这里记住标点,等紧随的 input 事件把字符插进输入框后,再把它删掉
|
||||
并移动选中项(见 undoPendingPunct)。只靠 beforeinput 是拦不住的。
|
||||
(CDP 实测记录:beforeinput it:insertCompositionText comp:true data:"," pd:false cc:false) */
|
||||
input.addEventListener('beforeinput', function (e) {
|
||||
if (e.inputType !== 'insertText') { return; }
|
||||
var it = e.inputType;
|
||||
if (it !== 'insertText' && it !== 'insertCompositionText') { return; }
|
||||
if (punctByKeyDown) { return; } // keydown 已处理过本次标点,别再移一次(防跳两格)
|
||||
var d = e.data;
|
||||
if (d === null || d === undefined) { return; }
|
||||
if (typeof d !== 'string' || d.length === 0) { return; }
|
||||
// 只在**恰好是一个标点字符**时当选词键。带别的内容(如整串组合文本)一律放行,
|
||||
// 免得把输入法正常上屏的字吃掉。
|
||||
var dir = punctDir(d);
|
||||
if (dir === 0) { return; }
|
||||
var open = !!word && word.style.display === 'block' && items.length > 0;
|
||||
if (!open || !acceptsPunct()) { return; }
|
||||
var dir = 0;
|
||||
if (d === ',' || d === '\uFF0C') { dir = -1; } // 半角/全角逗号
|
||||
else if (d === '.' || d === '\u3002' || d === '\uFF0E') { dir = 1; } // 半角/全角句号
|
||||
if (dir !== 0) { e.preventDefault(); moveSelection(dir); }
|
||||
if (e.cancelable) {
|
||||
// —— 能拦就拦:insertText 路径,preventDefault 立刻生效,字符根本不会进输入框。
|
||||
e.preventDefault();
|
||||
moveSelection(dir);
|
||||
return;
|
||||
}
|
||||
// —— 拦不住:insertCompositionText 路径(Chromium 实测 cancelable=false)。
|
||||
// 只能**事后收口**:先记下「这次标点进来之前」的输入框值,等事件洪峰过去
|
||||
// 再统一撤销(见 flushPending)。用快照而不是记字符,是因为这条路径的
|
||||
// 事件序列是「先插入标点 → 再回滚成原样」,只记字符会撤错。
|
||||
// ⚠️ 目标下标必须**当场算好**:收口跑在宏任务里,那时 input 的 `active = -1`
|
||||
// 已经把选中清掉了,再从 active 起算就会每次都从第一条重新开始(不累加)。
|
||||
pendingPunct = {
|
||||
ch: d,
|
||||
before: input.value,
|
||||
target: nextActive(active, dir),
|
||||
};
|
||||
schedulePendingFlush();
|
||||
});
|
||||
}
|
||||
|
||||
/** 安排一次「拦不住的标点」收口(去抖:组合上屏会在一帧内连发两次 input) */
|
||||
function schedulePendingFlush() {
|
||||
clearTimeout(pendingTimer);
|
||||
pendingTimer = setTimeout(flushPending, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 收口「拦不住的标点」(beforeinput 里 cancelable=false 的那条路径)。
|
||||
*
|
||||
* 为什么必须**延迟到宏任务**而不是当场处理:这条路径的真实事件序列是
|
||||
* ① beforeinput(insertCompositionText, cancelable=false) ← 拦不住
|
||||
* ② input 值 = 原文 + 「,」 ← 标点真的进去了
|
||||
* ③ input 值 = 原文(回滚) ← 又被自己撤掉了
|
||||
* 若在 ② 当场处理,会被 ③ 冲掉。等这一帧的事件洪峰结束(setTimeout 0)再收口才稳。
|
||||
*
|
||||
* 处理方式:把输入框值**恢复成标点进来之前的快照**(若此刻值确实被标点污染了),
|
||||
* 再把选中项设到 beforeinput 时算好的目标下标。
|
||||
*/
|
||||
function flushPending() {
|
||||
if (!pendingPunct) { return; }
|
||||
var info = pendingPunct;
|
||||
pendingPunct = null;
|
||||
if (!input) { return; }
|
||||
// 恢复快照:抹掉「拦不住的标点」留下的痕迹。
|
||||
// ⚠️ 只在值确实与快照不同、且**差值就是那个标点**时才动,避免抹掉用户真实输入。
|
||||
var tv = input.value;
|
||||
if (tv !== info.before && tv.indexOf(info.ch) >= 0) {
|
||||
var tb = info.before;
|
||||
if (tv.length > tb.length && tv.slice(0, tb.length) === tb) {
|
||||
input.value = tb; // 标点被追加在末尾
|
||||
} else if (tv.length === tb.length) {
|
||||
input.value = tb; // 等长(插入后再回滚的中间态)
|
||||
} else {
|
||||
var idx = tv.lastIndexOf(info.ch);
|
||||
if (idx >= 0) { input.value = tv.slice(0, idx) + tv.slice(idx + info.ch.length); }
|
||||
}
|
||||
// ⚠️ 这里**不要**重置 lastQ:值被恢复成「标点进来之前」的原文,而 lastQ 记的正是那份原文,
|
||||
// 两者本来就相等 → 180ms 后的 askSuggest 会走 `q === lastQ` 直接 return,
|
||||
// 不会重新请求、也就不会 showWord 把刚设好的选中清掉。重置反而会自找麻烦。
|
||||
}
|
||||
active = info.target;
|
||||
paintWord();
|
||||
}
|
||||
|
||||
/** 移动联想下拉的选中项(keydown 与 beforeinput 共用;dir=-1 上一条 / +1 下一条) */
|
||||
function moveSelection(dir) {
|
||||
if (active < 0) {
|
||||
active = dir > 0 ? 0 : items.length - 1;
|
||||
} else {
|
||||
active = (active + dir + items.length) % items.length;
|
||||
}
|
||||
active = nextActive(active, dir);
|
||||
paintWord();
|
||||
}
|
||||
|
||||
/** 算出「从 base 再按 dir 一步」应落在哪个下标(items 为空时返回 -1)。
|
||||
* 独立出来是为了「拦不住的标点」那条路:它要在 beforeinput 时就算好目标下标,
|
||||
* 因为收口跑在宏任务里,那时 active 已被 input 的 `active = -1` 清掉了。 */
|
||||
function nextActive(base, dir) {
|
||||
if (!items.length) { return -1; }
|
||||
if (base < 0) { return dir > 0 ? 0 : items.length - 1; }
|
||||
return (base + dir + items.length) % items.length;
|
||||
}
|
||||
|
||||
/* 鼠标划过某条建议 → 同步高亮(与 ↓/↑ 共用 active,回车即可确认) */
|
||||
document.addEventListener('mouseover', function (e) {
|
||||
if (!word || !e.target.closest) { return; }
|
||||
|
||||
@@ -87,6 +87,22 @@ SQL);
|
||||
$pdo->prepare('UPDATE engines SET suggest_url=?, suggest_type=? WHERE slug=? AND suggest_url=?')
|
||||
->execute(['', '', 'sogou', 'https://www.sogou.com/suggnew/ajajjson?type=web&key={q}']);
|
||||
|
||||
// 老库升级:把「联想词按键」的单组设置(arrows / punct)并成 'both'(两组都认)。
|
||||
// 为什么:用户反馈「上下键和逗号句号都选不了词」,根因之一就是线上停在单组 'punct'
|
||||
// —— 单组模式会让另一组键**静默失效**,用户不知道那是设置项在起作用,只当成坏了。
|
||||
// 'both' 是 'arrows' 与 'punct' 各自行为的**严格超集**:升级不会让任何人的习惯失效
|
||||
// (原来能用的键,升级后照样能用,只是多了一组)。
|
||||
// ⚠️ 只改这两个已知单组值;值不在白名单里的行不动,避免误伤将来的新取值。
|
||||
// ⚠️⚠️ 必须**只跑一次**:db_init() 是**每次请求**都会执行的,若无条件 UPDATE,
|
||||
// 用户在后台特意选回「只认方向键」,下一个请求就被改回去 —— 设置项等于失效。
|
||||
// 用 settings 里一个哨兵键把关,跑过就永不再跑。
|
||||
$migDone = (int)$pdo->query("SELECT COUNT(*) FROM settings WHERE k='mig_suggest_keys_both'")->fetchColumn() > 0;
|
||||
if (!$migDone) {
|
||||
$pdo->exec("UPDATE settings SET v='both' WHERE k='suggest_keys' AND v IN ('arrows','punct')");
|
||||
$pdo->exec("INSERT INTO settings (k,v) VALUES ('mig_suggest_keys_both','1')
|
||||
ON CONFLICT(k) DO UPDATE SET v='1'");
|
||||
}
|
||||
|
||||
// 是否为空库(用于判断是否写入默认数据)
|
||||
$isEmpty = (int)$pdo->query('SELECT COUNT(*) FROM groups')->fetchColumn() === 0;
|
||||
|
||||
@@ -107,9 +123,12 @@ function seed_settings(PDO $pdo): void
|
||||
// '1' = 顶部搜索框与所有站点/合集链接都在**新标签页**打开;'0' = 都在**当前页面**打开。
|
||||
// 默认 '1' 与引入该设置前的旧行为一致;老库若已有这一行不会被覆盖(INSERT OR IGNORE)。
|
||||
'open_blank' => '1',
|
||||
// 'arrows' = ↓/↑ 选词(默认,与引入该设置前的旧行为一致);
|
||||
// 'punct' = 用 , 和 . 选词(输入法手感);'both' = 两组都认。
|
||||
'suggest_keys' => 'arrows',
|
||||
// 'both' = ↓/↑ 与 , . **两组都认**(默认);
|
||||
// 'arrows' = 只认 ↓/↑;'punct' = 只认 , 和 .。
|
||||
// ⚠️ 默认从 'arrows' 改为 'both'(2026-09-29):用户反馈「上下键和逗号句号都选不了词」,
|
||||
// 线上设置恰好停在单组 'punct' —— 单组模式会让**另一组键静默失效**,极易被当成故障。
|
||||
// 'both' 是两组各自行为的**严格超集**(想用哪组都行),没有任何损失,故取为默认。
|
||||
'suggest_keys' => 'both',
|
||||
];
|
||||
$stmt = $pdo->prepare('INSERT OR IGNORE INTO settings (k, v) VALUES (?, ?)');
|
||||
foreach ($defaults as $k => $v) {
|
||||
|
||||
@@ -33,10 +33,10 @@ $openTarget = open_target();
|
||||
$openMode = $openTarget === '_blank' ? 'blank' : 'self';
|
||||
|
||||
// 联想词的选中按键(后台「设置 → 联想词按键」):
|
||||
// arrows = 只认 ↓/↑(默认,等同改造前) / punct = 只认 , 和 . (输入法手感)/ both = 都认
|
||||
// 白名单回退:任何非法值都回到 arrows,别让手改库/被篡改的表单把交互搞坏。
|
||||
$suggestKeys = setting('suggest_keys', 'arrows');
|
||||
if (!in_array($suggestKeys, ['arrows', 'punct', 'both'], true)) { $suggestKeys = 'arrows'; }
|
||||
// both = ↓/↑ 与 , . 都认(默认) / arrows = 只认 ↓/↑ / punct = 只认 , 和 .
|
||||
// 白名单回退:任何非法值都回到 both(默认值,与 db.php 一致),别让手改库/被篡改的表单把交互搞坏。
|
||||
$suggestKeys = setting('suggest_keys', 'both');
|
||||
if (!in_array($suggestKeys, ['arrows', 'punct', 'both'], true)) { $suggestKeys = 'both'; }
|
||||
|
||||
// 默认引擎
|
||||
$defSlug = setting('default_engine', 'bing');
|
||||
|
||||
Reference in New Issue
Block a user