用户原话:「首页,近期任务,如果勾选了完成,应该刷新一下这个列表,把完成的删掉。」 ## 原来的行为 首页那段复选框脚本 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
80 lines
5.1 KiB
PHP
80 lines
5.1 KiB
PHP
<?php
|
||
/**
|
||
* 首页卡片 · 近期任务(v62 从 templates/home.php 拆出来的)。
|
||
*
|
||
* 用户原话(v24 第 24 批 第 3 条):「右侧显示任务模块里面的数据,显示当前正在
|
||
* 进行当中的所有的任务,除了显示任务的标题之外,还要显示他的状态。比如说正在
|
||
* 进行中待确认等等。除了这之外,还要显示他所处的看板的名称。」
|
||
*
|
||
* 🔴 v53 第 2 条:待办也放进来,放在进行中的**下面**(不是混排)。
|
||
* ⚠️ 每行前面有复选框,点了就地完成(AJAX,不跳页);表单与脚本在 home.php 末尾
|
||
* 各一份 —— 那份**必须留着**,别跟着卡片一起删。
|
||
* ⚠️ 标题的颜色跟着**看板色**(不是任务模块的主题色),见 v32 第 4 条那条注释。
|
||
*
|
||
* 🔴🔴 v78(用户 2026-10-22):「首页点击一个任务后,除了展示信息,应该有相应的功能,
|
||
* 可以修改状态:待办、进行中,可以挪动分组,可以拖动上传附件,可以添加评论,
|
||
* 添加子任务等。其目地,是为了让用户不离开首页,就能对任务进行简单的处理。」
|
||
* ⇒ 每一行除 `data-fb-entry`(浮层要的基础信息)外,再给一个 `panel` 地址:
|
||
* 浮层拿到它就把正文换成**可操作的面板**(`templates/tasks/_panel.php`),
|
||
* 地址是 `tasks/panel?id=`,动作全走既有路由(见 helpers.php 的 task_panel_*)。
|
||
* 面板取不到(404 / 网络)时,浮层会退回 `rows` 那份只读信息 —— 不会白屏。
|
||
*/
|
||
?>
|
||
<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); ?>
|
||
<?php $tkColor = \App\Task::boardColorById((int)\App\Auth::familyId(),
|
||
(int)($t['board_id'] ?? 0) ?: null); ?>
|
||
<li class="dash-row tk-row" data-task-row="<?= (int)$t['id'] ?>"
|
||
style="--tk-c: <?= e($tkColor) ?>;">
|
||
<?php
|
||
$tkId = (int)$t['id']; $tkStatus = (string)($t['status'] ?? 'todo');
|
||
$tkBack = 'home'; $tkStatic = !\App\Auth::canWrite();
|
||
require __DIR__ . '/../_task_check.php';
|
||
?>
|
||
<span class="dash-dot" style="background: <?= e($late ? 'var(--red)' : 'var(--tk-c)') ?>;"></span>
|
||
<a class="dash-what" href="<?= e(url('tasks/show', ['id' => (int)$t['id']])) ?>"
|
||
title="<?= e((string)$t['title']) ?>"
|
||
data-fb-entry='<?= fb_entry_json([
|
||
'title' => (string)$t['title'],
|
||
'status_word' => \App\Task::statusWord((string)($t['status'] ?? 'todo')),
|
||
'note' => (string)($t['note'] ?? ''),
|
||
/* v78:浮层把正文换成这块面板(可就地改状态/看板/子任务/附件/评论) */
|
||
'panel' => url('tasks/panel', ['id' => (int)$t['id']]),
|
||
], url('tasks/show', ['id' => (int)$t['id']]),
|
||
\App\Auth::canWrite() ? url('tasks/edit', ['id' => (int)$t['id']]) : '',
|
||
[['截止', ($t['due_at'] ?? '') !== '' ? substr((string)$t['due_at'], 0, 16) : ''],
|
||
['看板', trim((string)($t['board_name'] ?? ''))]]) ?>'
|
||
style="color: <?= e($late ? 'var(--red)' : $tkColor) ?>;">
|
||
<?= e((string)$t['title']) ?>
|
||
</a>
|
||
<span class="kb-st is-<?= e((string)($t['status'] ?? 'todo')) ?> is-static">
|
||
<?= e(\App\Task::statusWord((string)($t['status'] ?? 'todo'))) ?>
|
||
</span>
|
||
<?php if (trim((string)($t['board_name'] ?? '')) !== ''): ?>
|
||
<?php $bColor = \App\Task::boardColor($t['board_color'] ?? null, (string)$t['board_name']); ?>
|
||
<span class="tk-board" style="--bc: <?= e($bColor) ?>;">
|
||
<i class="tk-board-dot" aria-hidden="true"></i><?= e((string)$t['board_name']) ?>
|
||
</span>
|
||
<?php endif; ?>
|
||
<?php if ((int)($t['child_total'] ?? 0) > 0): ?>
|
||
<span class="dash-tag"><?= (int)$t['child_done'] ?>/<?= (int)$t['child_total'] ?></span>
|
||
<?php endif; ?>
|
||
<?php if (($t['due_at'] ?? '') !== ''): ?>
|
||
<span class="tk-due"><?= e(\App\Task::dueWord($t['due_at'] ?? null)) ?></span>
|
||
<?php endif; ?>
|
||
</li>
|
||
<?php endforeach; ?>
|
||
</ul>
|
||
<?php endif; ?>
|
||
</div>
|