fix(home): 近期任务勾选完成后该行不消失(连概述卡计数也不减)(#20)
用户原话:「首页,近期任务,如果勾选了完成,应该刷新一下这个列表,把完成的删掉。」 ## 原来的行为 首页那段复选框脚本 AJAX 成功后只改徽章文字(`kb-st` → 「已完成」), **没有任何"移除这一行"的逻辑**。而且 templates/home.php:160 那条注释还写着 这是**当年的刻意设计**: > ⚠️ 改完**不摘掉这一行**:用户点完正想看自己刚完成什么,立刻消失会让人怀疑 > "是不是点到删除了"。刷新时由服务端决定它还在不在。 用户现在明确推翻这条:完成的就该从列表里删掉。 ## 服务端本身是对的 首页路由只取 `doing` + `todo`(index.php:839),`done` 本来就不在列表里 ⇒ 手动刷新这一行确实会消失,问题纯粹在"AJAX 完成后前端没同步"。 所以这次一行服务端代码都不用改。 ## 改法 新增 `dropTaskRow(row, wasStatus)`,AJAX 成功后(**按服务端返回的 `d.status` 判断**, 不是拿点击前的 `next`)做三件事: 1. **删掉这一行**; 2. **同步「概述」卡右下的「N 件在做 / N 件待办」** —— 那两个数字与这份列表 **同源**(同一个 `$tasks`,见 _card_brief.php:53),只删行不减去数字 ⇒ 列表 2 条、概述说 3 件在做,自相矛盾; 3. **一条不剩时把空态提示露出来**(服务端渲染的 `[data-tk-empty]` 原来一直 hidden) —— 否则卡片变成一片空白,用户以为加载坏了。 配套的小改动: - `_card_tasks.php`:空态提示改成**一直渲染、靠 `hidden` 切换**,加 `data-tk-empty` 钩子(不靠匹配文案 —— 文案改一个字就失效)。外层加 `data-tk-home-host`。 - `_card_brief.php`:两个统计 `<a>` 加 `data-hbs="doing|todo"` 钩子, 同样**不靠中文文案定位**(这是 JS 要读的东西,不能把展示文案变成接口)。 - `app.css`:补 `.empty-hint[hidden] { display: none !important; }` —— 浏览器默认的 `[hidden]{display:none}` 优先级最低,哪天有人给它加 `display:flex` 这句提示就会在"有任务时"也冒出来(与 app.css:601 那条注释是同一约定)。 ## 几个判据上的讲究 - **按 `d.status` 判断而不是 `next`**:`tasks/status` 是幂等接口, 拿服务端返回的状态才不会"手快连点两下,第二下把行删了"。 - **`wasStatus` 要在改 `data-tk-status` 之前存**:删行时要靠它决定减「在做」还是「待办」。 第一版直接在删行时读 `data-tk-status`,读到的已经是 `done` —— 自己写的时候就踩了。 - **提示文案补一句"已从近期任务移出,去「全部」里能看到"**: 删行的原罪是"用户以为点到删除了",所以必须**明说它去哪了**, 而不是沿用服务端那句「已标成「已完成」」(那会让人以为行还在,下一眼发现没了更慌)。 ## 验证 **主判据 `_tooling/_b37_home_task_drop.py` —— 20 通过 / 0 失败** 夹具:把库里历史 doing/todo 任务临时改状态腾空卡片(**不能靠 `deleted_at`** —— `Task::byStatus()` 的 SQL 里根本没有 `deleted_at IS NULL`,软删对首页无效,实测过), 造 2 条 doing + 1 条 todo,跑完逐个恢复。 覆盖:勾「进行中」→ 行移除 + 「件在做」2→1 + 「件待办」不误减; 勾「待办」→ 行移除 + 「件待办」1→0 且链接藏起; 删到最后一条 → 0 行 + 空态露出;刷新后与服务端事实一致;库里状态真的是 done。 **注入反证 `_tooling/_b37_falsify.py` —— 三次独立注入,各自红在对应处** | 注入 | 结果 | 命中的判据 | |---|---|---| | ① 不删行(原始 bug) | 通过 12 / **失败 8** | ③④⑤⑪⑭⑮ | | ② 删行但不减概述卡计数 | 通过 17 / **失败 3** | ⑤⑫⑯ | | ③ 不限空态(删完一片空白) | 通过 19 / **失败 1** | ⑮ | 每次注入前断言锚点唯一(#474),恢复后回到 20/0。 **回归 `_tooling/_b37_regress.py` —— 11 通过 / 0 失败** 看板勾选仍能改状态且**不删行**(与首页行为不同是有意的)、 详情页子任务复选框仍能改状态、`[hidden]` 的 `display` 真的是 `none`、 `data-hbs` 钩子在、卡片「全部」入口仍指向 tasks。 ⚠️ 回归里踩了一个判据假设错:我一开始按"详情页也该有父任务复选框"写, 直接 FAIL。真实情况是详情页的复选框**只给子任务**(show.php:181 在子任务循环里), 父任务没有 —— 是判据错了不是产品坏了(#429 同族)。 **内联 JS 校验 `_tooling/_b37_inline_js_check.py`**:把渲染后 HTML 的 `<script>` 抽出来逐个 `node --check`(`php -l` 不看 HTML 里的 JS)。 并做了反证:喂一段坏 JS,它确实报 1 块坏,不是恒绿。 工单 #20
This commit is contained in:
+74
-1
@@ -196,6 +196,10 @@
|
||||
ev.stopPropagation();
|
||||
var now = b.getAttribute('data-tk-status') || 'todo';
|
||||
var next = (now === 'done') ? 'todo' : 'done';
|
||||
/* 🔴 工单 #20:先把"改之前的状态"存下来 —— 后面删行时要用它决定
|
||||
「概述」卡里该减「件在做」还是「件待办」。
|
||||
⚠️ 不能在删行时再读 `data-tk-status`:那时它已经被下面改成 `d.status` 了。 */
|
||||
var was = now;
|
||||
document.getElementById('doneId').value = b.getAttribute('data-tk-done');
|
||||
document.getElementById('doneStatus').value = next;
|
||||
document.getElementById('doneBack').value = b.getAttribute('data-tk-back') || 'home';
|
||||
@@ -217,10 +221,79 @@
|
||||
badge.className = 'kb-st is-' + d.status + ' is-static';
|
||||
badge.textContent = staticWord[d.status] || d.word;
|
||||
}
|
||||
say(d.msg || '已更新。', true);
|
||||
/* 🔴🔴 工单 #20(用户 2026-10-28):「首页,近期任务,如果勾选了完成,
|
||||
应该刷新一下这个列表,把完成的删掉。」
|
||||
⇒ 在这里把这一行**从列表里摘掉**,并同步概述卡的计数。
|
||||
⚠️ 判据是**服务端返回的 d.status**,不是我点之前算出的 next:
|
||||
`tasks/status` 是幂等接口(传 done 再来一次还是 done),
|
||||
拿 d.status 判断才不会"用户手快连点两下,第二下又把行删了"。
|
||||
⚠️ 首页这份列表只收 doing + todo(index.php:839),done 本来就不在其中
|
||||
⇒ 手动刷新时它确实会消失,所以"删行"与刷新后的结果**一致**,
|
||||
不是前端自己造了个假象。
|
||||
⚠️ 当年这里是刻意**不删**的(原注释:怕用户以为"点到删除了")——
|
||||
用户现在明确要它消失,所以改掉;但**补偿**了一个可撤回的提示,
|
||||
见下面 say() 那段(既能满足"看着干净",又不至于找不到刚完成的东西)。 */
|
||||
if (d.status === 'done' && row) {
|
||||
dropTaskRow(row, was);
|
||||
/* 🔴 工单 #20:行被摘掉了,必须**明说**它去哪了 ——
|
||||
当年"不删行"的理由就是"怕用户以为点到删除了"(见上面原注释)。
|
||||
删了行就要把这条路补上:告诉他已从近期任务移除,
|
||||
而卡片右上角本来就有「全部」能找回($hcMore → tasks)。
|
||||
⚠️ 不能只沿用服务端那句 `已标成「已完成」` —— 那会让人以为行还在,
|
||||
下一眼发现没了反而更慌。 */
|
||||
say((d.msg || '已标成「已完成」。') + '(已从近期任务移出,去「全部」里能看到)', true);
|
||||
} else {
|
||||
say(d.msg || '已更新。', true);
|
||||
}
|
||||
}).catch(function () { b.disabled = false; form.submit(); });
|
||||
});
|
||||
});
|
||||
|
||||
/* ============================================================
|
||||
把一条刚完成的任务行从首页列表里摘掉(工单 #20)
|
||||
============================================================
|
||||
三件事一起做,少一件就露馅:
|
||||
① 删掉这一行;
|
||||
② 同步「概述」卡片右下的「N 件在做 / N 件待办」
|
||||
—— 那两个数字**与这份列表同源**(同一个 $tasks,见 _card_brief.php:53),
|
||||
只删行不减去数字 ⇒ 列表 2 条、概述说 3 件在做,自相矛盾;
|
||||
③ 一条不剩时把空态提示露出来(服务端渲染的 `[data-tk-empty]`,
|
||||
原来一直 hidden)—— 否则卡片变成一片空白,用户以为加载坏了。
|
||||
|
||||
⚠️ 撤回按钮:完成的任务没地方看了,得留一条回去的路。
|
||||
`data-tk-status` 已经是 done,再点一次这颗按钮就是"改回 todo",
|
||||
但行已经不在 DOM 里了 —— 所以撤回走的是**直接发一次状态请求**,
|
||||
成功后整页刷新把行放回去(列表的真相在服务端,前端不自己"插回一行")。 */
|
||||
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 时把那个 <a> 藏起来:服务端是 `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; }
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -51,12 +51,16 @@ foreach (($tasks ?? []) as $__t) {
|
||||
<b><?= count($todayEvents ?? []) ?></b> 条日程(今天)
|
||||
</a>
|
||||
<?php /* 两个数字与右侧「近期任务」卡片**同源**(同一个 $tasks 数组),
|
||||
「在做」与「待办」按状态拆开数 —— 见 v53 第 2 条那条注释。 */ ?>
|
||||
<a class="hbs" href="<?= e(url('tasks')) ?>" title="近期任务(进行中)">
|
||||
「在做」与「待办」按状态拆开数 —— 见 v53 第 2 条那条注释。
|
||||
🔴 工单 #20:`data-hbs` 是给首页脚本定位用的 —— 勾完成一条任务后,
|
||||
这两个数字要跟着减 1(否则与列表自相矛盾)。
|
||||
⚠️ 不能让脚本去匹配中文「件在做」:文案改一个字就失效,
|
||||
而且这里是 JS 会读的钩子,写成文案匹配等于把展示文案变成接口。 */ ?>
|
||||
<a class="hbs" data-hbs="doing" href="<?= e(url('tasks')) ?>" title="近期任务(进行中)">
|
||||
<b><?= $nDoing ?></b> 件在做
|
||||
</a>
|
||||
<?php if ($nTodo > 0): ?>
|
||||
<a class="hbs" href="<?= e(url('tasks')) ?>" title="记下来了、还没动手的那些">
|
||||
<a class="hbs" data-hbs="todo" href="<?= e(url('tasks')) ?>" title="记下来了、还没动手的那些">
|
||||
<b><?= $nTodo ?></b> 件待办
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -20,10 +20,15 @@
|
||||
* 面板取不到(404 / 网络)时,浮层会退回 `rows` 那份只读信息 —— 不会白屏。
|
||||
*/
|
||||
?>
|
||||
<div class="hc-body">
|
||||
<?php if (!$tasks): ?>
|
||||
<p class="empty-hint">没有进行中的任务,也没有待办。</p>
|
||||
<?php else: ?>
|
||||
<div class="hc-body" data-tk-home-host>
|
||||
<?php /* 🔴 工单 #20(用户 2026-10-28):「首页,近期任务,如果勾选了完成,
|
||||
应该刷新一下这个列表,把完成的删掉。」
|
||||
⇒ 勾完要把这一行**从列表里摘掉**(原来只改徽章文字,见 home.php 那段脚本)。
|
||||
⚠️ 但"删到一条不剩"时卡里不能是一片空白 —— 下面这句 `empty-hint` 由服务端渲染,
|
||||
前端删完不会自己冒出来,所以脚本要能在空的时候把它补上。
|
||||
`data-tk-empty` 就是给脚本找它的钩子(不靠文案匹配,文案改一个字就失效)。 */ ?>
|
||||
<p class="empty-hint" data-tk-empty<?= $tasks ? ' hidden' : '' ?>>没有进行中的任务,也没有待办。</p>
|
||||
<?php if ($tasks): ?>
|
||||
<ul class="dash-list tk-list">
|
||||
<?php foreach ($tasks as $t): ?>
|
||||
<?php $late = \App\Task::isOverdue($t['due_at'] ?? null); ?>
|
||||
|
||||
Reference in New Issue
Block a user