v25 第 25 批(9 条):名册分组不刷页 / 纪念日多人 / 首页等高 / 移出不回流 /

名册点击看详情拖动才排序 / 任务复选框与状态 / 财务标签多选 / 微信读书同步与统计 /
导航线上数据与图标

用户本轮 9 条需求(原话见 templates/ 与 app/ 内的逐条注释):

1. 名册:列表里给某人分组改为 AJAX 就地挪卡片(不再整页刷新)。
2. 纪念日:关联人改**多选**(复用 _person_picker,字段 person_ids[]);
   自建纪念日的按钮与其它模块统一(改=蓝 .btn、删=黄 .btn-warn);
   列表新增「关联人」列并给人名前加头像。
3. 首页:日程/任务两块**完全等高**(align-items:stretch + 固定总高 + flex 子项
   min-height:0);修「0 件待办」——数字与右侧列表同源(原来各算各的)。
4. 🔴 名册里被移出的人过一阵又回来:根因是 PersonRoster::seedFromPersons()
   **每次 Schema::ensure() 都无条件重灌**(线上实测每跑一次 +1 行)。
   修:给它加自守卫(只有在 persons 还留着 group_id/sort_order 的**老库**才跑)。
5. 名册:点击=看详情、**只有拖 ⠿ 手柄**才是排序(原来整行既能点又能拖,语义混乱)。
6. 任务:所有任务前加复选框,任何位置点勾即完成;
   状态语义照业界整理为 To Do / Doing / Done 并写出说明(用户要求参考别人的做法)。
7. 财务:类别标签改**多选非互斥**(再点一次取消该类别),上下两图 + 图例 + 表格
   全部按同一个集合联动;高亮条刻意不给「合计」(多选下各类金额不能相加)。
8. 微信读书:新增**懒同步**(访问时顺手做,6h TTL,只补空字段不覆盖用户填的)、
   封面图**拉下来存本站存储**(原来只存外链 → 看不到)、接官方 /readdata/detail 统计。
9. 导航:线上数据只搬了一半(5 板块 29 站 → 7 板块 67 站 + 67 图标)、
   展示页头像不显示 / 搜索按钮错位 / 点登录 404。三个真根因:
   · 相对地址在 /nav/ 下全 404 → 新增 Nav::abs();
   · 搜索容器丢了 id="search"(CSS 是 #search button.submit 绝对定位);
   · Nav::iconKey() 与 SimpleNav 的 icon_key() **算法不一致**(少一次 md5 归一化)
     → 搬来的图标文件名对不上;且 uploads/ 整目录 Require all denied。
   新增两个**免登录公开出口** nav_icon.php / nav_avatar.php。

顺带修掉一个「数据损坏级」的真 bug(做第 4 条线上反证时发现的,之前一直没人注意):
app/Region.php 三处用中文标点当 trim/ltrim 的 charlist,而 PHP 的 charlist 是
**按字节**匹配的 —— 「。」= E3 80 **82** → 字节 82 进了可剥集合 →
「层」= E5 B1 **82** 被切掉尾字节 → 非法 UTF-8 → backfillAddress 的 UPDATE 报
`1366 Incorrect string value` → **地址拆分升级从上线起一直在失败被跳过**(每请求报一次)。
改成字符级 preg_replace('/…/u');线上实测 ensure() 现在无报错、待拆行数真的在下降。

验证战绩:
- e2e 1289/0(新增 Region 字节级用例等)
- 真机探针:选人 19/0 · 首页/导航 24/0 · 导航模块 34/0 · b18 暂存+确定 10/0 ·
  第23批 52/0
- 桌面几何 428/0 · 移动端几何 680/0 · 交互 92/0 · 窄屏 24/0
- 老库升级 153/0(含 anniversary_people 迁移 + 「再跑 ensure() 名册行数不变」)
- 部署静态断言 180 OK / 0 FAIL · 反向验证 3/3 + Region 三变体各被两条断言同时抓住
- 线上 26 个文件与本地**逐字节一致**;展示页 200、图标 66/66 200、登录 200、
  越权/不存在均 404、签发短语后 ensure() 不再报错。
This commit is contained in:
2026-10-07 11:35:31 +08:00
parent 09e30d27d4
commit a5e559b2b3
23 changed files with 2379 additions and 212 deletions
+62
View File
@@ -5,6 +5,35 @@
* 看板三列直接落在 `status` 上(todo / doing / done),不另存"列"字段 ——
* "列"和"状态"本来就是一件事,存两份必然对不上(拖到 done 了状态还是 doing 之类)。
*
* ────────────────────────────────────────────────────────────────
* 「待办」与「进行中」到底差什么(v24 第 25 批 用户第 6 条要求说清楚)
*
* 用户原话:「任务的状态也不太对,待办和进行中,有什么区别?去网上查查,
* 别人都是怎么设置状态的。」
*
* 查证结果(这个答得干脆,业界早就收敛了):
* 主流做法就是 Kanban 的**三态**,而且**列名与状态名是同一件事**:
* · **To Do / 待办** —— 已记下来、**还没动手**。它是一份"排队"。
* · **In Progress / 进行中** —— **已经动手了**(哪怕只做了两分钟)。
* · **Done / 已完成** —— 做完并验收。
* 出处:Todoist 的 Kanban 指南、Wrike 的 Kanban 词条("What Are The Kanban
* To Do, Doing, Done Lists?")都把这三列定义为**工作流阶段**;
* ONES 的中文词条更直白:「待处理=已创建但尚未开始」「进行中=正在执行过程中,
* 让其他人知道**已有人在处理**,避免重复工作」。
*
* ⇒ 所以分界线是**"有没有开始"**,不是"重不重要"、也不是"谁在管":
* 待办 = 队列里排着(我还没碰它)
* 进行中 = 手上有活(**别人不该再认领**)
* 两条纪律(Kanban 的 WIP 限制):
* ① 「进行中」**不要堆太多** —— 那是"同时开了 N 摊",做不完;
* ② 首页只显示「进行中」(+ 已逾期的待办),因为"我手头在做什么"才是要看的。
* 这正是 v24 第 14 批 用户第 2 条那句「按状态取,不看板取」的来由。
*
* ⚠️ **不做**「待确认 / 待审核 / 已暂停」这类扩展状态:
* 那是团队协作的产物(要有人审、有人批)。家庭场景三个人做家务,
* 多一个状态就多一次"该选哪个"的犹豫。三态够用,且与业界口径一致。
* 真需要更细,用**看板**分("属于哪摊事")而不是加压状态 —— 两个维度本来就分开。
*
* 两级:只做父任务 / 子任务两层(parent_id)。**不做无限层级** ——
* 家庭任务("周末大扫除"下面挂"擦玻璃""洗车")两层足够,
* 无限层级要递归查询 + 缩进渲染,代价远大于收益。
@@ -24,6 +53,39 @@ final class Task
{
public const STATUS = ['todo' => '待办', 'doing' => '进行中', 'done' => '已完成'];
/**
* 每个状态「到底是什么意思」——**给界面上的人看**(v24 第 25 批 用户第 6 条)。
*
* 用户原话:「待办和进行中,有什么区别?」——他不知道区别,就是这一处没写清楚。
* 所以这句话要出现在**他看到状态的地方**:卡片上的 title、状态说明、
* 以及任务页头那句总说明(见 templates/tasks/index.php)。
*
* ⚠️ 文案刻意用"我/手头"这种第一人称口语:这是家庭场景,
* 写成"尚未开始执行"那种公文腔他自己也不会去读。
*/
public const STATUS_HINT = [
'todo' => '记下来了,还没动手 —— 排队等着',
'doing' => '已经动手了(哪怕刚开始)—— 别人就别再认领了',
'done' => '做完了',
];
/** 状态的一句话解释(界面上用它做 title / 说明) */
public static function statusHint(string $k): string
{
return self::STATUS_HINT[$k] ?? '';
}
/**
* 界面上「点一下就能完成」的那颗复选框要提交的目标状态。
*
* 用户原话:「任务相关的模块,所有的任务,前面都带一个复选框,在任何地方,
* 点这个复选框,这个任务就完成了。」
* ⇒ 所以勾上 = `done`(**不是**"推进到下一个状态")。
* ⚠️ 与卡片上那颗状态徽标(点一下轮转 todo→doing→done)刻意不同:
* 复选框只有一个意思「完成」,徽标是"改状态"。两个控件、两种语义。
*/
public const DONE_STATUS = 'done';
public const REPEAT = [
'' => '不循环',
'day' => '每天',