Files
FamilyBook/templates/days/index.php
T
laoyang a5e559b2b3 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() 不再报错。
2026-10-07 11:35:31 +08:00

187 lines
8.3 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
/**
* 纪念日(v24 用户第 13 条;2026-10-03 第 9 条重排;**2026-10-07 第 2 条加「关联人」列**)
*
* 第 9 条原话:「纪念日中,手动添加的纪念日和来自其他模块的,例如生日的纪念日,
* 要分开,手动纪念日在上面。在新建纪念日和右侧,也增加设置按钮,
* 可以设置纪念日的分类。」
*
* 第 2 条原话:「纪念日模块,每个纪念日现在只能关联一个人,应该可以关联多个人。
* 和选择观影人一样,多选模式。自己添加的纪念日,右侧按钮,和其他模块的按钮
* 样式保持一致,修改用蓝色,删除用黄色。再加一列,关联人。列表中,
* 人的前面,都加头像。」
*
* ⇒ 三块(顺序按用户第 9 条要求):
* ① 接下来的日子(未来 60 天)
* ② **手动添加的**纪念日(在上)—— 可改可删;右侧按钮:改=蓝(.btn)、删=黄(.btn-warn)
* ③ 来自名册生日的(在下,实时派生、不能改删)
*
* @param array $custom 用户自建的(行上带 person_ids)
* @param array $derived 从名册生日派生的(行上也有 person_ids)
* @param array $upcoming 接下来 60 天的
* @param array $kinds 分类清单(设置里维护)
* @param array $people 名册(把 person id 换成头像 + 称呼)
*/
$canWrite = \App\Auth::canWrite();
$kindBadge = static function (array $r): string {
$k = (string)($r['kind'] ?? '');
return $k === '' ? '' : '<span class="node-badge">' . e($k) . '</span>';
};
$srcWord = static function (array $r): string {
$src = (string)($r['source'] ?? 'custom');
if ($src === 'birthday_solar') {
return '名册生日(阳历)';
}
if ($src === 'birthday_lunar') {
return '名册生日(农历)';
}
return \App\Anniversary::repeatWord((string)($r['repeat_type'] ?? 'solar'));
};
/* 关联人(用户第 2 条:「再加一列,关联人。列表中,人的前面,都加头像。」)
—— 用全站统一的 person_chip()(它自带头像 + 我对他/她的称呼 + 本人底色)。
⚠️ chips 一律**不传 href**:这一格在一张表格里,`<a>` 套 `<a>` 是非法 HTML,
浏览器会把外层结构切开(person_chip 的注释里记过这个坑)。
⚠️ $peopleMap 由路由层传进来(一次查全,避免逐行查库);缺失时退化成"有几个显示几个"。 */
$peopleMap = $peopleMap ?? [];
$peopleCell = static function (array $r) use ($peopleMap): string {
$ids = (array)($r['person_ids'] ?? []);
if (!$ids) {
return '<span class="small muted">—</span>';
}
$out = '';
$n = 0;
foreach ($ids as $pid) {
$pid = (int)$pid;
$p = $peopleMap[$pid] ?? null;
if ($p === null) {
continue;
}
// 一个人时正常画;多人时最多画 4 个 + 「+N」,免得一行被撑爆
if ($n >= 4) {
$rest = count($ids) - 4;
$out .= '<span class="node-badge" title="还有 ' . $rest . ' 人">+' . $rest . '</span>';
break;
}
$out .= person_chip($p);
$n++;
}
return $out === '' ? '<span class="small muted">—</span>' : $out;
};
/* 画一张纪念日表。两块(手动 / 派生)共用同一个渲染 —— 复制一遍迟早改漏一处。 */
$renderTable = function (array $rows, bool $editable) use ($canWrite, $kindBadge, $srcWord, $peopleCell): void {
if (!$rows) {
return;
}
?>
<div class="table-wrap">
<table class="table">
<thead>
<tr><th>日期</th><th>纪念日</th><th>关联人</th><th>分类</th><th>重复</th><th></th></tr>
</thead>
<tbody>
<?php foreach ($rows as $r): ?>
<tr>
<td class="nowrap"><?= (int)$r['month'] ?> 月 <?= (int)$r['day'] ?> 日</td>
<td>
<?= e((string)$r['title']) ?>
<?php if (!empty($r['is_leap'])): ?><span class="small muted">(闰月)</span><?php endif; ?>
</td>
<td class="ann-people"><?= $peopleCell($r) ?></td>
<td><?= $kindBadge($r) ?></td>
<td class="small muted"><?= e($srcWord($r)) ?></td>
<td class="nowrap ann-acts">
<?php if ($editable && $canWrite): ?>
<?php /* 用户第 2 条:「右侧按钮,和其他模块的按钮样式保持一致,
修改用蓝色,删除用黄色。」
→ 改 = `.btn`(主按钮的**蓝**),删 = `.btn-warn`(**黄**实心)。
⚠️ 原来用的是 `.link-quiet`(纯文字链接)+ `.btn-ghost`(白底描边),
两者都"看不出来是什么动作",与观影/旅程行上那套不一致。 */ ?>
<a class="btn btn-sm" href="<?= e(url('days/edit', ['id' => (int)$r['id']])) ?>">修改</a>
<form method="post" action="<?= e(url('days/destroy')) ?>" class="res-inline">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$r['id'] ?>">
<button class="btn btn-warn btn-sm" type="submit"
onclick="return confirm('删掉这条纪念日?')">删除</button>
</form>
<?php else: ?>
<span class="small muted">来自名册</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php
};
?>
<div class="page-head">
<h1>纪念日</h1>
<span class="sub">生日本身就是一种纪念日 · 阳历与农历都能记</span>
<div class="actions">
<?php if ($canWrite): ?>
<a class="btn" href="<?= e(url('days/new')) ?>">新建纪念日</a>
<?php endif; ?>
<a class="btn btn-ghost" href="<?= e(url('calendar')) ?>">看日历</a>
<?php /* 用户第 9 条:设置按钮就在「新建纪念日」右边(☰,排最右) */ ?>
<?php $setCurrent = 'kinds'; require __DIR__ . '/_settings_menu.php'; ?>
</div>
</div>
<div class="card card-pad">
<h2 class="cinema-h2">接下来的日子</h2>
<?php if (!$upcoming): ?>
<p class="empty-hint">未来 60 天里没有纪念日。</p>
<?php else: ?>
<ul class="ann-up">
<?php foreach ($upcoming as $u): ?>
<li class="ann-up-item">
<span class="ann-days"><?= (int)$u['days'] === 0 ? '今天' : ((int)$u['days'] . ' 天后') ?></span>
<span class="ann-date"><?= e(substr((string)$u['date'], 5)) ?></span>
<span class="ann-title"><?= e((string)$u['title']) ?></span>
<?php if ($u['age'] !== null): ?><span class="small muted"><?= (int)$u['age'] ?> 岁</span><?php endif; ?>
<?= $kindBadge($u) ?>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
<?php /* ==================== ② 手动添加的(在上,用户第 9 条)==================== */ ?>
<div class="card card-pad">
<h2 class="cinema-h2">
我自己添加的(<?= count($custom) ?>)
<span class="hint">只有这些能改能删</span>
</h2>
<?php if (!$custom): ?>
<p class="empty-hint">
还没有自己添加的纪念日。
<?php if ($canWrite): ?>
结婚纪念日、忌日、相识的日子…点右上角「新建纪念日」加一条。
<?php endif; ?>
</p>
<?php else: ?>
<?php $renderTable($custom, true); ?>
<?php endif; ?>
</div>
<?php /* ==================== ③ 来自名册的生日(在下)==================== */ ?>
<div class="card card-pad">
<h2 class="cinema-h2">
来自名册的(<?= count($derived) ?>)
<span class="hint">实时读出来的,改生日这里跟着变</span>
</h2>
<?php if (!$derived): ?>
<p class="empty-hint">
名册里还没有人填过生日。到「名册 → 编辑成员」把生日填上,这里就会自动出现。
</p>
<?php else: ?>
<?php $renderTable($derived, false); ?>
<p class="tip" style="margin-top:10px">
这一块是**实时派生**的 —— 改生日,这里跟着变,不用重复维护。
不需要在这里新建,也没法单独删(删掉生日就等于删了它)。
</p>
<?php endif; ?>
</div>