fix(home): 从浮层把任务标成完成时该行不消失(只修了复选框那条路)(#20)

用户第二次提同一条需求。核了线上代码:4 文件 md5 与本地一致、mtime 18:14,
部署没问题;复跑原判据仍 20/0 全绿。判据绿 + 用户能复现 ⇒ 判据有盲区。

盲区:首页把任务标成完成有**两个入口**
  ① 每行前面那颗复选框 —— home.php 末尾的脚本(上一版已修)
  ② 点整行弹浮层 → 面板里「状态」三段按钮 → assets/js/task_panel.js 的 syncRow()
上一版只改了 ①。syncRow() 只改徽章与复选框的 aria 状态,**从不删行**,
所以从面板点「已完成」后行留在原地,只把徽标换成「已完成」。
用户点的就是 ②,而原判据 grep 全文没有 panel / 浮层 / data-fb-entry —— 完全没测这条路。

改法:
- 把删行逻辑抽成 window.fbHomeTaskDrop(row, wasStatus) 放 assets/js/app.js,
  两个入口共用一份。作用域限定 [data-tk-home-host]:任务**看板**页的行也带
  data-task-row,那里勾完成只改状态、**必须保留该行**(看板按状态分栏,删了就没处看)。
- task_panel.js 的 syncRow() 在 d.status === 'done' 时调它;
  wasStatus 必须在改写 data-tk-status **之前**读,否则概述卡不知道该减「在做」还是「待办」。
- home.php 原来那份本地实现改成薄转发,避免两份实现改一处忘一处。
- 顺带修掉一条说谎的注释:原注释写"补偿了一个可撤回的提示",但代码里从来没有
  撤回按钮,只有一句 3.2 秒自动消失的 say()。
- _card_tasks.php 补记"两个入口"这件事。

子任务安全:task_panel_json() 用 panel_of 找**父任务**,回的 status 是父任务的,
所以面板里勾子任务不会把父任务那一行删掉(判据 ⑱ 专门守这条)。

判据:
- 新增 _b38_panel_done_drop.py(21 条,走真实浮层点击):
  修复前 **16 通过 / 5 失败**(⑥⑦⑧⑬⑭)→ 修复后 **21 / 0**
- 注入反证 _b38_falsify.py:把删行那两行用 if(false) 挡掉,锚点断言 count==1,
  立刻红回同样的 5 条、关键条目 ⑥⑧⑬⑭ 命中 4/4,副本自动还原
- 原 _b37_home_task_drop.py(复选框路径)仍 **20 / 0**
- _b37_regress.py **11 / 0**:看板仍改状态且**不删行**、详情页子任务复选框正常、
  空态 display 真的是 none
- home.php 内联 JS 0 块坏;app.js / task_panel.js node --check 全过
This commit is contained in:
2026-10-28 18:29:59 +08:00
parent ea6e42c27f
commit 4941e9db9c
4 changed files with 123 additions and 48 deletions
+65
View File
@@ -2580,3 +2580,68 @@
if (!document.hidden) { place(); }
});
})();
/* ============================================================================
首页「近期任务」:把一条已完成的任务行从卡片里摘掉(工单 #20)
----------------------------------------------------------------------------
用户原话(2026-10-28,两次强调):「首页,近期任务,如果勾选了完成,
应该刷新一下这个列表,把完成的删掉。」
🔴🔴 为什么这段放在 app.js 而不是留在 home.php 里:
"把这条标成完成"在首页有**两个入口**——
① 行首那颗复选框(脚本在 home.php 末尾)
② 点整行弹浮层 → 面板里的「状态」三段按钮(脚本是 task_panel.js)
第一版只改了 ①,于是同一个用户动作(都是"标完成")两条路径两种结果:
从复选框勾 → 行消失;从面板点「已完成」→ 行留着,徽标变成「已完成」。
用户复现的就是第二条(我先前的判据完全没测浮层,所以 20/0 全绿也是假绿,
→ pitfalls「需求横跨几个入口就每个入口都要判据」)。
⚠️ 作用域必须限定 `[data-tk-home-host]`(首页任务卡片的外层):
任务**看板**页的行也带 `data-task-row`,那里勾完成是**只改状态、不删行**
(看板是按状态分栏的,"行"就是那个任务本身,删了就没处看了)。
所以这段绝不能按 `[data-task-row]` 全局找。
⚠️ 只在"变更后确实是 done"时删(判据用服务端回的 `d.status`,不是我算出来的 next):
`tasks/status` 是幂等接口,传 done 再来一次还是 done;拿返回值判断才不会
"手快连点两下,第二下把行删了"。
@param {Element} row 要摘掉的那一行 `li[data-task-row]`
@param {string} wasStatus 改**之前**的状态('doing' / 'todo')——
决定概述卡该减「件在做」还是「件待办」。
⚠️ 不能在这里读 `data-tk-status`:调用方通常已经把它改成 done 了。
@return {boolean} 真的删了才 true(行不在首页卡片里就 false,什么也不做)
============================================================================ */
window.fbHomeTaskDrop = function (row, wasStatus) {
if (!row) { return false; }
var host = row.closest ? row.closest('[data-tk-home-host]') : null;
/* 不在首页卡片里(任务看板 / 详情页子任务列表)→ 一律不删。
⚠️ 这条判断是这段代码能被两个入口共用的**前提**,别为了"通用"去掉它。 */
if (!host || !host.contains(row)) { return false; }
var ul = row.parentNode;
if (row.parentNode) { row.parentNode.removeChild(row); }
/* 概述卡的「N 件在做 / N 件待办」同步减 1。
⚠️ 用 `[data-hbs]` 定位(模板里专门加的钩子),**不靠中文文案** ——
"件在做"改一个字就失效,而这是 JS 要读的东西。 */
if (wasStatus === 'doing' || wasStatus === 'todo') {
var a = document.querySelector('.hb-stats a.hbs[data-hbs="' + wasStatus + '"]');
var b = a ? a.querySelector('b') : null;
if (b) {
var n = Math.max(0, (parseInt(b.textContent, 10) || 0) - 1);
b.textContent = String(n);
/* 减到 0 就把整条藏起来:服务端是 `if ($nTodo > 0)` 才渲染待办那一条,
前端不藏会出现「0 件待办」——服务端永远不会画出来的状态。 */
if (n === 0) { a.hidden = true; }
}
}
/* 删到一条不剩:摘掉空 ul,把服务端渲染的空态提示露出来
—— 否则卡片变成一片空白,用户以为加载坏了。 */
if (ul && ul.querySelectorAll && ul.querySelectorAll('[data-task-row]').length === 0) {
if (ul.parentNode) { ul.parentNode.removeChild(ul); }
var empty = host.querySelector('[data-tk-empty]');
if (empty) { empty.hidden = false; }
}
return true;
};
+33 -5
View File
@@ -88,21 +88,49 @@
* 用户改完关掉浮层,如果卡片上还写着「待办」,他会怀疑"到底改上了没有"。
* 只改状态徽标与复选框 —— 看板徽标/逾期色这些要重算的东西留给下次刷新,
* 不在这里猜(猜错比不猜更糟)。
*
* 🔴🔴 工单 #20(用户 2026-10-28,第二次提同一条需求):
* 「首页,近期任务,如果勾选了完成,应该刷新一下这个列表,把完成的删掉。」
* 第一版只改了行首那颗复选框(home.php 里的脚本),**漏了这里**——
* 从面板点「已完成」时,行留在原地只把徽标改成「已完成」。
* 同一个用户动作(都是"标成完成")两条路径两种结果,用户复现的就是这条。
*
* ⇒ 变更后是 done 时,也把这一行从首页卡片里摘掉。
* 真正干活的在 app.js 的 `window.fbHomeTaskDrop()`:它自己判断
* "行在不在 `[data-tk-home-host]` 里",**不在就完全不碰**
* (任务看板页的行也在 `[data-task-row]` 上,那里必须保留该行)。
*
* ⚠️ `d.status` 是**父任务**的状态(`task_panel_json()` 用 `panel_of` 找的父任务,
* 见 app/helpers.php:506)。面板里勾的是**子任务**时,请求带 `panel_of`,
* 回来的 status 仍是父任务的 ⇒ 这里不会因为"子任务做完了"就删掉父任务那一行。
*
* ⚠️ 删行前先把 `wasStatus` 取出来:下面几行会改写 `data-tk-status`,
* 改完再读就只能读到 done,概述卡不知道该减「在做」还是「待办」。
*/
function syncRow(d) {
if (!d || !d.status) { return; }
var row = document.querySelector('[data-task-row="' + taskId() + '"]');
if (!row) { return; }
/* 🔴 必须在改写 `data-tk-status` **之前**读 —— 见上面最后一条 ⚠️。 */
var chk0 = row.querySelector('.tk-check[data-tk-done]');
var was = chk0 ? (chk0.getAttribute('data-tk-status') || 'todo') : '';
var badge = row.querySelector('.kb-st');
if (badge) {
badge.className = 'kb-st is-' + d.status + ' is-static';
if (d.word) { badge.textContent = d.word; }
}
var chk = row.querySelector('.tk-check[data-tk-done]');
if (chk) {
chk.setAttribute('data-tk-status', d.status);
chk.classList.toggle('is-done', d.status === 'done');
chk.setAttribute('aria-pressed', d.status === 'done' ? 'true' : 'false');
if (chk0) {
chk0.setAttribute('data-tk-status', d.status);
chk0.classList.toggle('is-done', d.status === 'done');
chk0.setAttribute('aria-pressed', d.status === 'done' ? 'true' : 'false');
}
/* 标成完成 ⇒ 从首页「近期任务」里摘掉(工单 #20)。
⚠️ 用服务端回的 `d.status`,不是"我点之前猜的 next"。 */
if (d.status === 'done' && typeof window.fbHomeTaskDrop === 'function') {
window.fbHomeTaskDrop(row, was);
}
}