Files
FamilyBook/templates/home.php
T
laoyang 7138314dc7 v24 第 15 批:活动 / 旅程两级语义(术语 · 派生时间城市 · 表单三项 · 题图可点)
用户原话四条,逐条落地:

1. 「旅程模块,按钮是记一次活动,而不是记一趟旅程。旅程是活动里面的概念。」
   → 顶层一律改称**活动**:按钮「记一次活动」、页标题「活动」、`N 次` / `N 段旅程`;
     活动详情三组改叫「计划的旅程 / 走过的旅程 / 没成行的」;
     节点表单「这段旅程叫什么」「阶段」;flash 文案 17 处同步。
     (导航条仍叫「旅程」——用户就是这么称呼这个模块,列表页加副标题说清两级关系。)

2. 「活动不需要开始和结束时间,是根据活动中的旅程,最早的旅程开始时间,和最晚的
     旅程结束时间,计算出来的。目的地也不需要…去的城市,也是根据旅程,得到的。」
   → 活动**不再存**这三样,全部**读时算出来**:
     · Trip::derived()      最早开始 / 最晚结束 / 城市去重
     · Trip::cities()       按最早出现时间排(先后顺序本身是信息,不是字母序)
     · Trip::attachDerived() 列表页批量补(避免 N+1)
     · Trip::activitySpan()  没有旅程时说「还没有旅程」,不用「日期待补」
     create/update 不再写 events 的起止与地点(老值保留不动,页面不读)。
     列表排序/搜索、首页「最近去了」改走子查询。

3. 「题图那里,没有办法上传图片,点击哪里都没反应。」——**真 bug**
   「点图 = 开文件框」那句 JS 只写在 people/form.php 的内页脚本里(头像专用);
   第 14 批加的旅程题图与图书封面照抄了同一套 DOM 却没有对应的 JS,
   于是图块有 cursor:pointer 与 hover 高亮(看着能点),点下去毫无反应。
   → 做成声明式通用绑定:图上写 data-pick-input="#fileInputId",
     app.js 统一接管(一个页面多个也各绑各的),并删掉旧的那份专用实现。
     三处(头像 / 题图 / 图书封面)全挂上。

4. 「添加活动的页面,顺序是这样的:添加标题,上传并裁剪题图,然后一段简介,就可以了。」
   → 主区就这三项,顺序照原话;时间/城市不做输入项(只留一句说明它们从哪来,
     不说清用户会满页找"怎么没地方填时间")。
     备注 / 公开 / 同行的人收进「更多设置」折叠区——功能一个没丢。

顺带修掉两处会炸的实现问题:
· ORDER BY 引用聚合别名(first_at)在 MariaDB/MySQL 报 1247,且只在运行时炸、
  php -l 看不出 → 改成 ORDER BY MIN(starts_at),否则整个旅程模块 Fatal。
· e2e 里"详情页显示了派生时间"原本写成整页查找,下面每段旅程自己也渲染日期,
  于是把派生值塞成 null 也照样绿(借光假绿)→ 判据收进页头那一段。

验证:e2e 845/0(+15 条本批断言)· 升级 134/0 · 桌面几何 428/0 · 移动几何 688/0 ·
交互 92/0 · 题图真机点击 4/0 · 读书窄屏 15/0 · 家谱 34/0 + 10/0 + 反向 3/3 ·
观影按钮 60/0 + 反向 3/3 · 本批反向验证 4/4(题图绑定抽掉→红、派生值不挂→红、
城市退回读 location→红、时间字段加回表单→红)· 部署静态断言新增两组(含"活动不许
再写起止时间/目的地"与"ORDER BY 不许引用聚合别名"的反退化防线)。
部署后服务器自检 19/19,五个关键文件 md5 与线上逐一核对一致。
2026-10-05 10:58:17 +08:00

244 lines
12 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
/**
* 首页 —— 一块 dashboard(用户 2026-10-01 第 13 条)。
*
* 用户 2026-10-03 第 3 条把它重排成:
* · hero:标题 + 个人 slogan + 两个图标按钮(我的资料 / 我的家谱)
* · 统计信息:随机问候语 + 今日天气 + 近七天日程数 / 待办数
* · 未来七天的日程(自定义 + 观影 + 旅行 + 有截止时间的任务,一次汇总)
* · 手头在做(进行中 + 已逾期的任务)
* · 各模块的统计与模块内信息(**可按人在个人设置里开关与排序**)
* · 往年今日
* ⚠️ 「接下来的日子(两个月内的生日)」已去掉:生日/纪念日本来就是日程,
* 已经在「未来七天」里,两块并排是重复。
*
* ⚠️ **家人卡片已经不在首页了**(用户明确要求):看人去「名册」、看关系去「家谱」,
* 首页留给"今天该关心什么"。统计数字仍在,因为那是唯一的全局概览。
*
* ⚠️ 标题下的那句个人口号已按用户要求撤掉(v8):hero 区只留标题与三个统计。
*/
// 生日事件上用简称,卡片才不会一行塞不下;长名字在详情页里出
$today = date('Y-m-d');
// 首页要显示哪些模块、什么顺序 —— 用户在「个人设置 → 首页上的模块」里定(每人一份)
$homeMods = array_values(array_filter(
(array)($homeModules ?? []),
static fn($k) => \App\Module::exists((string)$k)
));
?>
<?php /* 用户 2026-10-03 第 3 条:
① 标题下显示**个人 slogan**(原来撤掉了,现在要回来);
② 右侧那三块统计(与我有关 / 名册总数 / 亲属连线)**去掉**;
③ 「我的资料 / 我的家谱」保留,但从文字按钮换成**图标按钮**;
④ 「名册」按钮去掉 —— 它与顶部导航条里的「名册」重复。
⚠️ 图标用行内 SVG(不引外部字体/图片),与观影设置下拉同一套做法。 */ ?>
<section class="hero">
<img class="hero-logo" src="<?= e(asset('/assets/icons/icon-192.png')) ?>" alt="">
<div class="hero-main">
<h1><?= e(\App\Auth::homeTitle()) ?></h1>
<?php $heroSlogan = trim((string)($mySlogan ?? '')) !== ''
? (string)$mySlogan
: trim((string)($family['motto'] ?? '')); ?>
<?php if ($heroSlogan !== ''): ?>
<p class="hero-slogan"><?= e($heroSlogan) ?></p>
<?php endif; ?>
</div>
<?php /* ⚠️ 这两个链接必须带上真实的 person id:曾经 layout.php 里写
`$me = Auth::check()` 把模板里的 $me 覆盖成布尔值,链接就变成 id=0
("补充亲属关系"点进去是空白页)。所以这里显式用 $meId。 */ ?>
<div class="hero-actions">
<a class="icon-btn" href="<?= e(url('people/show', ['id' => (int)$meId])) ?>"
title="我的资料" aria-label="我的资料">
<svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" fill="none"
stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<circle cx="10" cy="7" r="3.4"/><path d="M3.6 17c0-3.3 2.9-5.4 6.4-5.4s6.4 2.1 6.4 5.4"/>
</svg>
</a>
<a class="icon-btn" href="<?= e(url('kin')) ?>"
title="我的家谱" aria-label="我的家谱">
<svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" fill="none"
stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<rect x="7.4" y="2.2" width="5.2" height="4.2" rx="1"/>
<rect x="2.2" y="13.6" width="5.2" height="4.2" rx="1"/>
<rect x="12.6" y="13.6" width="5.2" height="4.2" rx="1"/>
<path d="M10 6.4v3.4M4.8 13.6v-1.9h10.4v1.9M4.8 11.7h10.4"/>
</svg>
</a>
</div>
</section>
<?php /* 用户 2026-10-03 第 3 条:「接下来的日子(两个月内)」整块**去掉** ——
它列的是生日,而生日/纪念日本来就是日程,已经在下面的「未来七天」里出现过,
两块并排是重复信息。生日的"提前提醒"由日历与纪念日模块负责。 */
?>
<?php /* ==================== 统计信息(用户第 3 条) ====================
一句话问候 + 今天的天气 + 几个数字。放在最上面,因为它回答的是
"今天是个什么日子、我该关心什么"——比下面任何一个列表都更该先看到。 */ ?>
<section class="section">
<div class="card card-pad home-brief">
<div class="home-brief-hello">
<b><?= e((string)($greeting ?? '')) ?></b>
<span class="muted small"><?= e(date('n 月 j 日')) ?> <?= e((string)($weekday ?? '')) ?></span>
</div>
<?php if (!empty($weather)): ?>
<div class="home-brief-weather">
<span class="w-icon" aria-hidden="true"><?= e((string)($weather['icon'] ?? '')) ?></span>
<span class="w-temp"><?= e((string)($weather['temp'] ?? '')) ?></span>
<span class="w-desc"><?= e((string)($weather['text'] ?? '')) ?></span>
<?php if (($weather['place'] ?? '') !== ''): ?>
<span class="muted small"><?= e((string)$weather['place']) ?></span>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="home-brief-stats">
<span class="hbs"><b><?= (int)($stat7['events'] ?? 0) ?></b> 条日程(近七天)</span>
<span class="hbs"><b><?= (int)($stat7['todos'] ?? 0) ?></b> 件待办</span>
<span class="hbs"><b><?= (int)($stat7['overdue'] ?? 0) ?></b> 件逾期</span>
</div>
</div>
</section>
<?php /* ==================== 各模块统计(用户第 3 条) ====================
每个模块给一个"一眼看得懂的小结"。用户在个人设置里能挑显示哪些、以及顺序。 */ ?>
<?php if ($homeMods && !empty($moduleCards)): ?>
<section class="section">
<div class="page-head" style="margin-bottom:12px">
<h1 style="font-size:15px">家里的近况</h1>
<div class="actions">
<a class="btn btn-ghost btn-sm" href="<?= e(url('settings/me')) ?>#home-mods">调整显示</a>
</div>
</div>
<div class="mod-cards">
<?php foreach ($homeMods as $mk): ?>
<?php $mc = $moduleCards[$mk] ?? null; if (!$mc) { continue; } ?>
<a class="mod-card" href="<?= e((string)$mc['href']) ?>"
style="--mc: <?= e((string)($mc['color'] ?? '#888')) ?>">
<div class="mod-card-head">
<span class="mod-card-icon"><?= $mc['icon'] /* 行内 SVG,自己人写的,不转义 */ ?></span>
<span class="mod-card-name"><?= e((string)$mc['name']) ?></span>
</div>
<?php if (($mc['big'] ?? '') !== ''): ?>
<div class="mod-card-big"><?= e((string)$mc['big']) ?></div>
<?php endif; ?>
<?php if (!empty($mc['lines'])): ?>
<ul class="mod-card-lines">
<?php foreach ($mc['lines'] as $ln): ?>
<li><?= e((string)$ln) ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</a>
<?php endforeach; ?>
</div>
</section>
<?php endif; ?>
<?php /* ==================== 日程(未来七天) ==================== */ ?>
<section class="section">
<div class="page-head" style="margin-bottom:12px">
<h1 style="font-size:15px">未来七天 <span class="sub">日程 · 观影 · 旅行 · 到期任务</span></h1>
<div class="actions">
<a class="btn btn-ghost btn-sm" href="<?= e(url('calendar')) ?>">看整月</a>
</div>
</div>
<?php if (!$soon): ?>
<div class="card card-pad empty"><b>这七天还没有安排</b>有事先记一笔,日后再翻才有得看。</div>
<?php else: ?>
<div class="card card-pad">
<ul class="dash-list">
<?php foreach ($soon as $ev): ?>
<li class="dash-row">
<span class="dash-dot" style="background: <?= e((string)$ev['color']) ?>;"></span>
<span class="dash-when">
<?= e((string)$ev['date'] === $today ? '今天' : substr((string)$ev['date'], 5)) ?>
<?php if (($ev['time'] ?? '') !== ''): ?><b><?= e((string)$ev['time']) ?></b><?php endif; ?>
</span>
<a class="dash-what" href="<?= e((string)$ev['href']) ?>"><?= e((string)$ev['title']) ?></a>
<?php /* 用户 2026-10-04 第 11 批 #1:「右侧的日历本,现在看不清,
应该用一个色块,颜色就用日历的颜色,文字是日历本的文字,用白色。」
原来是 .dash-tag(--faint 灰色 11px 小字),在浅色纸上几乎看不见 ——
现在做成实心色块:底色 = 这本日历的颜色,字 = 白,一眼能对上左侧那个小圆点。 */ ?>
<span class="dash-cal"
style="background: <?= e((string)$ev['color']) ?>;"><?= e((string)$ev['calname']) ?></span>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
</section>
<?php /* ==================== 进行中的任务 ==================== */ ?>
<section class="section">
<div class="page-head" style="margin-bottom:12px">
<h1 style="font-size:15px">手头在做 <span class="sub">进行中 · 已逾期</span></h1>
<div class="actions">
<a class="btn btn-ghost btn-sm" href="<?= e(url('tasks')) ?>">任务看板</a>
</div>
</div>
<?php if (!$tasks): ?>
<div class="card card-pad empty"><b>没有进行中的任务</b>也没有逾期的事,挺好。</div>
<?php else: ?>
<div class="card card-pad">
<ul class="dash-list">
<?php foreach ($tasks as $t): ?>
<?php $late = \App\Task::isOverdue($t['due_at'] ?? null); ?>
<li class="dash-row">
<span class="dash-dot" style="background: <?= e($late ? 'var(--red)' : 'var(--brand)') ?>;"></span>
<span class="dash-when">
<?php if (($t['due_at'] ?? '') !== ''): ?>
<?= e(\App\Task::dueWord($t['due_at'] ?? null)) ?>
<?php else: ?>
进行中
<?php endif; ?>
</span>
<a class="dash-what" href="<?= e(url('tasks/show', ['id' => (int)$t['id']])) ?>">
<?= e((string)$t['title']) ?>
</a>
<?php /* v24 第 14 批 用户第 2 条:首页这一块是**按状态**捞出来的
(进行中 + 已逾期的待办,跨看板),所以把状态标出来 ——
否则"待办为什么出现在这里"看不出来。 */ ?>
<span class="kb-st is-<?= e((string)($t['status'] ?? 'todo')) ?> is-static">
<?= e(\App\Task::statusWord((string)($t['status'] ?? 'todo'))) ?>
</span>
<?php if ((int)($t['child_total'] ?? 0) > 0): ?>
<span class="dash-tag"><?= (int)$t['child_done'] ?>/<?= (int)$t['child_total'] ?></span>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
</section>
<?php /* ==================== 往年今日 ==================== */ ?>
<section class="section">
<div class="page-head" style="margin-bottom:12px">
<?php /* v24 第 19 条:只认「往年的今天」,不再退回同月 —— 见 home_memories() 的说明。 */ ?>
<h1 style="font-size:15px">
往年的今天
<span class="sub"><?= e(date('n 月 j 日')) ?></span>
</h1>
</div>
<?php if (empty($memories['rows'])): ?>
<div class="card card-pad empty">
<b>往年的这一天还没有记录</b>
日子还长,等有了第一次,明年这时候就会自己冒出来。
</div>
<?php else: ?>
<div class="card card-pad">
<ul class="dash-list">
<?php foreach ($memories['rows'] as $mm): ?>
<li class="dash-row">
<span class="dash-dot" style="background: <?= $mm['kind'] === 'trip' ? '#3F7D5A' : '#7A4E8C' ?>;"></span>
<span class="dash-when"><?= (int)$mm['years_ago'] ?> 年前</span>
<a class="dash-what" href="<?= e((string)$mm['href']) ?>">
<?= $mm['kind'] === 'trip' ? '🧳 ' : '🎬 ' ?><?= e((string)$mm['title']) ?>
</a>
<span class="dash-tag"><?= e(substr((string)$mm['starts_at'], 0, 10)) ?></span>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
</section>