v30 第 30 批(3 条):ICS 导入读完即弃 / 导航联想词根因修复 / 导航设置改纯图标

三条都是「静态检查与 e2e 恒绿、只有真机才现形」的,每条先写真机诊断探针
拿到真根因再改,并逐条反向验证(改回旧行为必须被抓住)。

1. 日程 ICS 导入:文件已上传仍提示「请选择一个文本」
   真根因:_stage_drop.php 是"拖进来即刻上传到暂存区"的唯一形态——
   拖放时文件走 upload_stage.php 落到 uploads/_staging/,而 <input>.files
   始终是空的;两个导入框(ics/通讯录)都带 required → 浏览器原生校验把提交
   拦下,弹它自己的文案。就算绕过校验,路由只读 $_FILES['ics']、从不看
   stage_token[],暂存文件永远没人认领。
   改:片段新增 $sdStage(默认 true)。导入型传 false → 框上不带
   data-drop-stage → app.js 走"写回 input.files、随表单提交"的老路,文件走
   PHP 临时文件,路由读完即弃;不落存储、不建附件行、不进 OSS。
   同时把 app.js 找框的选择器从 [data-drop-stage][data-sd-for=…] 放宽成
   [data-sd-for=…]——否则这些框点不开也接不住拖动。

2. 导航联想词"还是不好用"
   真根因:nav_show.php 给出的 suggest 地址已带 ?p=<短语>,而 nav-show.js 又
   硬拼 '?slug=…' → 实际是 …?p=abc?slug=bing&q=…;PHP 认为第一个 ? 之后整串
   都是查询值 → p 匹配不上短语 → 403 → 下拉永远空。服务端一直是对的(直连
   200 + 10 条词),e2e 探针自己拼对了参数,所以 HTTP 层恒绿、只有真页面能抓。
   改:nav-show.js 新增 withQuery(),按 base 里有没有 ? 决定分隔符;
   nav_suggest.php 加短语容错(切掉第一个 ?/& 之后的内容,救缓存里的旧 JS)。

3. 导航设置按钮改纯图标 ☰
   v29 只改成了下拉菜单,summary 仍是 btn-ghost btn-sm + 文字「设置」+ 一个
   向下箭头。改:换成与其他七个模块逐字节相同的 btn-icon + 三条横线。

验证:e2e 1464/0(新增 2 条)、升级 153/0、vCard 40 项、城市 32/0、
桌面几何全过、移动 680/0、交互 92/0、窄屏 30/0、b24_nav 41/0、b24_ui 26/0、
b28 39/0、b29 38/0、b27 34/0;新真机探针 _b30 22/0、反向验证 11/0;
部署静态断言新增 8 条守卫并逐条证伪通过(本轮抓到 2 条恒真守卫 + 1 处
_code_only 的 locale 差异)。线上 7 个文件 SHA256 与本地一致。
This commit is contained in:
2026-10-08 14:46:16 +08:00
parent 37ba86321b
commit 91c55038c9
7 changed files with 121 additions and 19 deletions
+6 -2
View File
@@ -667,10 +667,14 @@
bindBox(pv, input);
});
}
// 新结构:框自己声明 data-sd-for 指向这个 input
// 新结构:框自己声明 data-sd-for 指向这个 input。
// 🔴 v30 用户第 1 条:选择器**不能**带上 `[data-drop-stage]` 这一层 ——
// "导入型"上传框(ics / 通讯录)故意**不带** data-drop-stage(它要走
// "写回 input.files、随表单提交、PHP 读完即弃"的老路,见 _stage_drop.php 头部)。
// 带上它就会漏掉这些框 → 框既点不开文件选择框、也接不住拖动。
if (input.id) {
Array.prototype.forEach.call(
document.querySelectorAll('[data-drop-stage][data-sd-for="' + input.id + '"]'),
document.querySelectorAll('[data-sd-for="' + input.id + '"]'),
function (bx) { bindZone(bx, input); bindBox(bx, input); });
}
}
+29 -1
View File
@@ -134,6 +134,33 @@
});
}
/**
* 往一个 URL 追加查询串,**分隔符按 URL 里有没有 `?` 决定**。
*
* 🔴🔴 v30 用户第 2 条:「导航的联想词功能还是不好用」。真根因就在这一行。
*
* `window.SIMPLENAV.suggest` 是**服务端按当前短语生成**的,形态是
* `https://<站点>/nav_suggest.php?p=<短语>`
* —— 它**已经带了一个 `?`**(`p=` 是那个免登录接口的门禁,见 nav_suggest.php)。
* 而旧代码在这里硬拼 `'?slug=…&q=…'` → 实际发出的是
* `…/nav_suggest.php?p=abc?slug=bing&q=天气`
* PHP 对重复的 `?` 的处理:**第一个 `?` 之后整串都是查询值**,第二个 `?` 只是
* `p` 的值里一个普通字符 → `?p` 变成 `abc?slug=bing` → `Nav::ownerByPhrase()`
* 匹配不上任何短语 → **403 forbidden** → `data.items` 是 undefined → `showWord()`
* 收到空数组 → `hideWord()` → 下拉永远是空的。
*
* ⚠️ 这条**只在真机上才现形**:e2e 里直连 `/nav_suggest.php?p=…&slug=…&q=…`
* 是 200(探针自己拼对了),所以"接口是好的"一直成立,坏的只是前端那一步。
* (实测记录:前端发出的 URL 带 `?p=…?slug=…`,服务端回 403;服务端本身 200。)
* ⚠️ 不改成"服务端直接把 slug/q 一起拼好"是为了保持这份 JS 与 SimpleNav 原版的
* 可对照性 —— 原版 `suggest` 是一个不带问号的固定地址,参数确实是前端拼的。
* 这里只把"分隔符"这件事修正回来,其余一行不动。
*/
function withQuery(base, qs) {
if (!base) { return ''; }
return base + (base.indexOf('?') >= 0 ? '&' : '?') + qs;
}
function askSuggest() {
if (!input) return;
var q = input.value.trim();
@@ -142,7 +169,8 @@
lastQ = q;
var mine = ++seq;
var slug = form ? (form.getAttribute('data-slug') || '') : '';
fetch(cfg.suggest + '?slug=' + encodeURIComponent(slug) + '&q=' + encodeURIComponent(q))
fetch(withQuery(cfg.suggest, 'slug=' + encodeURIComponent(slug)
+ '&q=' + encodeURIComponent(q)))
.then(function (r) { return r.json(); })
.then(function (data) { if (mine === seq) { showWord(data && data.items); } })
.catch(function () { if (mine === seq) { hideWord(); } });