From 4941e9db9c28e3169368e30c451b1b0b1a64f764 Mon Sep 17 00:00:00 2001 From: laoyang Date: Wed, 28 Oct 2026 18:29:59 +0800 Subject: [PATCH] =?UTF-8?q?fix(home):=20=E4=BB=8E=E6=B5=AE=E5=B1=82?= =?UTF-8?q?=E6=8A=8A=E4=BB=BB=E5=8A=A1=E6=A0=87=E6=88=90=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E6=97=B6=E8=AF=A5=E8=A1=8C=E4=B8=8D=E6=B6=88=E5=A4=B1=EF=BC=88?= =?UTF-8?q?=E5=8F=AA=E4=BF=AE=E4=BA=86=E5=A4=8D=E9=80=89=E6=A1=86=E9=82=A3?= =?UTF-8?q?=E6=9D=A1=E8=B7=AF=EF=BC=89=EF=BC=88#20=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户第二次提同一条需求。核了线上代码: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 全过 --- assets/js/app.js | 65 ++++++++++++++++++++++++++++++++++ assets/js/task_panel.js | 38 +++++++++++++++++--- templates/home.php | 58 +++++++++--------------------- templates/home/_card_tasks.php | 10 +++++- 4 files changed, 123 insertions(+), 48 deletions(-) diff --git a/assets/js/app.js b/assets/js/app.js index de419d4..4a7e3c8 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -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; +}; diff --git a/assets/js/task_panel.js b/assets/js/task_panel.js index 63cb1a3..02a6f12 100644 --- a/assets/js/task_panel.js +++ b/assets/js/task_panel.js @@ -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); } } diff --git a/templates/home.php b/templates/home.php index 3a1d508..8ae4902 100644 --- a/templates/home.php +++ b/templates/home.php @@ -231,8 +231,12 @@ ⇒ 手动刷新时它确实会消失,所以"删行"与刷新后的结果**一致**, 不是前端自己造了个假象。 ⚠️ 当年这里是刻意**不删**的(原注释:怕用户以为"点到删除了")—— - 用户现在明确要它消失,所以改掉;但**补偿**了一个可撤回的提示, - 见下面 say() 那段(既能满足"看着干净",又不至于找不到刚完成的东西)。 */ + 用户现在明确要它消失,所以改掉。 + 🔴 补偿方式是**一句说明去哪了**(下面 say() 那段),**不是"撤回按钮"**: + 先前这里的注释写着"补偿了一个可撤回的提示",但代码里从来没有撤回按钮, + 只有一句 3.2 秒自动消失的 `say()`。 + 注释描述不存在的功能,比没有注释更糟 —— 下一个人会去找它、找不到、然后再写一个。 + (撤回的正路:卡片右上角「全部」→ 任务看板,那里 done 的还在,能改回来。) */ if (d.status === 'done' && row) { dropTaskRow(row, was); /* 🔴 工单 #20:行被摘掉了,必须**明说**它去哪了 —— @@ -252,47 +256,17 @@ /* ============================================================ 把一条刚完成的任务行从首页列表里摘掉(工单 #20) ============================================================ - 三件事一起做,少一件就露馅: - ① 删掉这一行; - ② 同步「概述」卡片右下的「N 件在做 / N 件待办」 - —— 那两个数字**与这份列表同源**(同一个 $tasks,见 _card_brief.php:53), - 只删行不减去数字 ⇒ 列表 2 条、概述说 3 件在做,自相矛盾; - ③ 一条不剩时把空态提示露出来(服务端渲染的 `[data-tk-empty]`, - 原来一直 hidden)—— 否则卡片变成一片空白,用户以为加载坏了。 - - ⚠️ 撤回按钮:完成的任务没地方看了,得留一条回去的路。 - `data-tk-status` 已经是 done,再点一次这颗按钮就是"改回 todo", - 但行已经不在 DOM 里了 —— 所以撤回走的是**直接发一次状态请求**, - 成功后整页刷新把行放回去(列表的真相在服务端,前端不自己"插回一行")。 */ + 🔴🔴 实现在 `assets/js/app.js` 的 `window.fbHomeTaskDrop()` —— 这里只是转发。 + **不要**把那段逻辑抄回来:同一个动作在首页有两个入口 + (① 这颗复选框 ② 点整行弹浮层、在面板里点「已完成」, + 脚本是 task_panel.js),抄两份必然只改一份。 + 第一版就是抄在本地,于是"从复选框勾会删行、从面板点不删", + 用户复现了后者,而我的判据只测前者 ⇒ 20/0 全绿也照样是假绿。 + ⚠️ 保留这个薄封装是因为下面两处调用都读 `was`,就地传更清楚; + 同时也让 home.php 里"删行"这件事还有一处可 grep 的落点。 */ function dropTaskRow(row, wasStatus) { - var host = row.closest('[data-tk-home-host]'); - var ul = row.parentNode; - // ① 删行 - if (row.parentNode) { row.parentNode.removeChild(row); } - // ② 概述卡计数:wasStatus 是"改之前"的状态 — doing 的完成就减「在做」,todo 的减「待办」 - syncBriefCount(wasStatus); - // ③ 空态 - if (ul && ul.querySelectorAll('[data-task-row]').length === 0) { - if (ul.parentNode) { ul.parentNode.removeChild(ul); } - var empty = host ? host.querySelector('[data-tk-empty]') : null; - if (empty) { empty.hidden = false; } - } - } - - /* 概述卡的「N 件在做 / N 件待办」减 1(工单 #20) - ⚠️ 用 `[data-hbs]` 定位(模板里刚加的钩子),**不靠中文文案** —— - "件在做"改一个字就失效,而这是 JS 要读的东西。 - ⚠️ 减到 0 时把那个 藏起来:服务端是 `if ($nTodo > 0)` 才渲染待办那一条, - 前端不藏的话会出现「0 件待办」——服务端永远不会画出来的状态。 */ - function syncBriefCount(wasStatus) { - if (wasStatus !== 'doing' && wasStatus !== 'todo') { return; } - var a = document.querySelector('.hb-stats a.hbs[data-hbs="' + wasStatus + '"]'); - if (!a) { return; } - var b = a.querySelector('b'); - if (!b) { return; } - var n = Math.max(0, (parseInt(b.textContent, 10) || 0) - 1); - b.textContent = String(n); - if (n === 0) { a.hidden = true; } + if (typeof window.fbHomeTaskDrop !== 'function') { return; } + window.fbHomeTaskDrop(row, wasStatus); } })(); diff --git a/templates/home/_card_tasks.php b/templates/home/_card_tasks.php index 6deff6f..b6fbf52 100644 --- a/templates/home/_card_tasks.php +++ b/templates/home/_card_tasks.php @@ -26,7 +26,15 @@ ⇒ 勾完要把这一行**从列表里摘掉**(原来只改徽章文字,见 home.php 那段脚本)。 ⚠️ 但"删到一条不剩"时卡里不能是一片空白 —— 下面这句 `empty-hint` 由服务端渲染, 前端删完不会自己冒出来,所以脚本要能在空的时候把它补上。 - `data-tk-empty` 就是给脚本找它的钩子(不靠文案匹配,文案改一个字就失效)。 */ ?> + `data-tk-empty` 就是给脚本找它的钩子(不靠文案匹配,文案改一个字就失效)。 + + 🔴🔴 把任务标成完成在首页有**两个入口**,两个都要删行: + ① 每行前面那颗复选框 → 脚本在 `home.php` 末尾 + ② **点整行弹浮层** → 面板里的「状态」三段按钮 → `assets/js/task_panel.js` + 真正干活的是 `assets/js/app.js` 的 `window.fbHomeTaskDrop()`,两个入口共用。 + ⚠️ 第一版只改了 ①,用户点的是 ②,于是"改完还是不消失"—— + 而我的判据只测了 ①,所以 20/0 全绿也是假绿。 + **需求有几个入口,就要有几份判据。** */ ?>

>没有进行中的任务,也没有待办。