Files
FamilyBook/templates/home/_card_brief.php
T
laoyang ea6e42c27f 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
2026-10-28 18:13:49 +08:00

93 lines
4.1 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 首页卡片 · 概述(v62 从 templates/home.php 拆出来的)。
*
* 内容与 v24 第 24 批 用户第 2 条完全一致(问候 → 日期/星期/农历 → 日子标记 →
* 今日数字 | 右侧彩色天气图标 + 温度/风力 + 地址),只换了外壳:
* 以前是 `section.section > div.card.home-brief`,现在是卡片的 `.hc-body`。
*
* ⚠️ 这块**永远是横贯的**(默认 8 宽):它是"首页第一眼",被挤成一半宽就会折行折得很碎。
* ⚠️ 用到的变量都由 home 路由传进来($hbPart 除外,这里自己按时段算)。
*/
$hbPart = '你好';
$hbH = (int)date('G');
if ($hbH < 5) { $hbPart = '夜深了'; }
elseif ($hbH < 11) { $hbPart = '早上好'; }
elseif ($hbH < 13) { $hbPart = '中午好'; }
elseif ($hbH < 18) { $hbPart = '下午好'; }
else { $hbPart = '晚上好'; }
$nDoing = 0;
$nTodo = 0;
foreach (($tasks ?? []) as $__t) {
if ((string)($__t['status'] ?? 'todo') === 'doing') {
$nDoing++;
} else {
$nTodo++;
}
}
?>
<div class="hc-body home-brief">
<div class="hb-left">
<div class="hb-hello">
<b><?= e($hbPart) ?></b><?php if (trim((string)($meName ?? '')) !== ''): ?>,<?= e((string)$meName) ?><?php endif; ?>
</div>
<div class="hb-date">
<span class="hb-d"><?= e((string)($todayLabel ?? date('n 月 j 日'))) ?></span>
<span class="hb-w"><?= e((string)($weekday ?? '')) ?></span>
<?php if (($lunarShort ?? '') !== ''): ?>
<span class="hb-lunar" title="<?= e((string)($lunarLine ?? '')) ?>"><?= e((string)$lunarShort) ?></span>
<?php endif; ?>
</div>
<?php if (!empty($todayMarks['tags'])): ?>
<div class="hb-marks">
<?php foreach ($todayMarks['tags'] as $mk): ?>
<span class="hb-mark is-<?= e((string)$mk['kind']) ?>"><?= e((string)$mk['word']) ?></span>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div class="hb-stats">
<a class="hbs" href="<?= e(url('calendar')) ?>">
<b><?= count($todayEvents ?? []) ?></b> 条日程(今天)
</a>
<?php /* 两个数字与右侧「近期任务」卡片**同源**(同一个 $tasks 数组),
「在做」与「待办」按状态拆开数 —— 见 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" data-hbs="todo" href="<?= e(url('tasks')) ?>" title="记下来了、还没动手的那些">
<b><?= $nTodo ?></b> 件待办
</a>
<?php endif; ?>
<?php if ((int)($stat7['overdue'] ?? 0) > 0): ?>
<a class="hbs is-late" href="<?= e(url('tasks')) ?>">
<b><?= (int)$stat7['overdue'] ?></b> 件逾期
</a>
<?php endif; ?>
</div>
</div>
<?php if (!empty($weather)): ?>
<div class="hb-weather is-<?= e((string)($weather['tone'] ?? 'cloud')) ?>">
<span class="hbw-icon" aria-hidden="true"><?= e((string)($weather['icon'] ?? '')) ?></span>
<div class="hbw-main">
<div class="hbw-temp"><?= e((string)($weather['temp'] ?? '')) ?></div>
<div class="hbw-text"><?= e((string)($weather['text'] ?? '')) ?></div>
<?php if (($weather['wind'] ?? '') !== ''): ?>
<div class="hbw-wind">
<?= $weather['wdir'] !== '' ? e((string)$weather['wdir']) . ' ' : '' ?><?= e((string)$weather['wind']) ?>
</div>
<?php endif; ?>
</div>
<?php if (($weather['place'] ?? '') !== ''): ?>
<div class="hbw-place"><?= e((string)$weather['place']) ?></div>
<?php endif; ?>
</div>
<?php endif; ?>
</div>