Files
FamilyBook/templates/home.php
T
laoyang b45cf671d1 v71c:首页「通知消息」改成正规卡片,样式直接复用通知页
用户 2026-10-21:
「先把web端的首页修一下,通知消息这个,也是首页的卡片,可以调整顺序和大小。
 另外,这个样式也不好看,我看个人头像点击后,通知消息那个页面的样式就不错,
 直接复用就可以了。」

① 卡片化(HomeCards 注册表 + 默认布局 + 取数)
   · TYPES 新增 'notif'(w4/h4,route=notifications,more=全部通知)
   · DEFAULT_LAYOUT 钉在 brief 之后,与 days 并一行(宽 4)——
     若插在 brief 与 agenda 之间,agenda/tasks 会被挤成一上一下(实测 C13 顶边 407 vs 751)
   · data() 加 case 'notif' → 新增 notifData():最新 10 条 + 未读数,
     与原来的取数口径一字不差;关掉卡片就不查这两条 SQL
   · templates/home.php 删掉网格前那块写死区块(v30 第 5 条「必须在往年的今天前面」
     这条约束作废 —— 那时还没有卡片体系);index.php 不再单独传 notifs/notifUnread

② 样式复用 templates/notifications/index.php
   · 结构与通知页完全一致:头像在前 → 正文 → kind 图标在右(带 ntf-kind-{kind})
     (旧首页那块是"图标在前、头像第二",正是用户说不好看的那一版)
   · 新增 .hc-ntf 作用域只补"卡片比整页小"的三条差异:正文允许折两行(宽 2 时
     单行省略会把正文全吃掉)、时间挪到正文下一行、未读数做小注脚
   · 不带每条右侧「删除」按钮(4×4 的卡里塞两个按钮会把正文挤成两三个字),
     删除在通知页做,卡片右上角有「全部通知」

③ 顺带修掉 v63 A1 抓住的一处:'notif' 的 mod 给 'home' 而不是空串
   (通知不在 Module::LIST,但它是首页自己的卡片;空串会让「全部通知」成为
   一排同色模块按钮里唯一白底 ghost 的那个)
   ⇒ TYPES docstring 立规矩:route 非空就必须有 mod,认不出归哪个模块就归 home

验证(familybook_push.sh 第 6 轮全绿,PUSH_RC=0,2h11m)
   v62 首页卡片 56/0 · v62 反向验证 14/0(新增 M8–M12 五条注入)
   b30 通知 16/0 · b30 反向验证 18/0
   v63 40/0 · v63 反向验证 51/0 · e2e 1523/0 · 部署静态断言空跑 RC=0

同批带上上一轮 v70/v71 的日历修复(已单独验过,本轮流水线一并回归):
   · v70 日历导入后条目不显示:+N 条从纯文本 span 改成真按钮,点开是当天全部
     条目浮层;AJAX 新建后就地插入与服务端同形(回 entry,不让前端自己拼)
   · v71 ICS 导入顺序:主事件 → 覆盖行 → EXDATE 由 Calendar::importIcs() 统一保证
2026-10-21 13:22:37 +08:00

201 lines
11 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 用户 2026-10-18 第 3 条)。
*
* 用户原话:「今天,我们正式引入卡片的概念,在首页上,概述是卡片,日程是卡片,
* 近期任务是卡片,往年的今天也是卡片,纪念日,要有一个卡片,电影、日程、
* 读书,都要有自己的卡片。……卡片的规格,我们可以定义一下,例如,把宽度
* 定义为 8,高度可以是 2、4、6、8,这样。如果是 2x2,那么就可以一排放 4 个,
* 如果是 2x8,那就是一行放一个,类似于这样的定义。」
*
* ────────────────────────────────────────────────────────────────
* 结构变了,但**内容一字未改**:原来的三块(信息面板 / 日程+任务 / 往年的今天)
* 现在各自是一张卡片,内容块原样搬进 `templates/home/_card_*.php`。
*
* 尺寸口径(与 app.css 的 `.hc-*`、设置页文字完全一致):
* · 一排 **8 格**;宽 2 / 4 / 6 / 8 → 一排 4 张 / 2 张 / 一大一小 / 独占一排;
* · 高 2 / 4 / 6 / 8 行(一行 76px + 10px 间距)。
* · 排版完全由 CSS 网格决定:**卡片顺序 = 数组顺序**,不需要任何浮动/清浮。
*
* ⚠️ 别在模板里自己算"该多宽":宽高由 `$homeCards`(用户设置)给,
* 模板只负责把类名拼对(`hc-w4 hc-h8`)。
*
* 🔴 v71c(用户 2026-10-21)推翻了一条旧约束:「通知消息这个,也是首页的卡片,
* 可以调整顺序和大小。」—— 它原来在本文件里是一块**网格之前**的写死区块
* (v30 第 5 条当时还没有卡片体系,只能用"排在网格前"保证它在「往年的今天」上面)。
* 现在它是一张**正规卡片**(`_card_notif.php`):位置/宽高/开不开都由用户定。
* ⇒ 这条"必须在网格前"的约束**到此为止**,别再按它往网格上加写死区块。
*
* @var array $homeCards HomeCards::layout() 的结果(每项 k/w/h/n)
* @var array $cardData HomeCards::data() 取的那几张卡片的额外数据
*/
?>
<?php /* 用户 2026-10-03 第 3 条:
① 标题下显示**个人 slogan**;
② 右侧那三块统计(与我有关 / 名册总数 / 亲属连线)**去掉**;
③ 「我的资料 / 我的家谱」保留,但从文字按钮换成**图标按钮**;
④ 「名册」按钮去掉 —— 它与顶部导航条里的「名册」重复。
v62 追加第五个图标按钮:「首页卡片」布局设置(与这排图标同一套做法)。 */ ?>
<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('home/cards')) ?>"
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="2.4" y="2.4" width="6" height="6" rx="1.2"/>
<rect x="11.6" y="2.4" width="6" height="6" rx="1.2"/>
<rect x="2.4" y="11.6" width="6" height="6" rx="1.2"/>
<rect x="11.6" y="11.6" width="6" height="6" rx="1.2"/>
</svg>
</a>
<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 /* ==================== 卡片网格(v62)====================
⚠️ `require` 进来的每个 `_card_*.php` 与这里**同一作用域**(共享 $tasks / $todayEvents
/ $cardData …):这是刻意的 —— 卡片的取数与首页的取数必须同源,
各自再查一遍就会出现"数字和列表对不上"那类老问题。 */ ?>
<section class="section">
<?php if (empty($homeCards)): ?>
<div class="card card-pad empty">
<b>首页卡片都被关掉了</b>
到 <a href="<?= e(url('home/cards')) ?>">首页卡片</a> 里重新勾几张出来。
</div>
<?php else: ?>
<div class="hc-grid" id="homeCards">
<?php foreach ($homeCards as $card): ?>
<?php
$hcKey = (string)($card['k'] ?? '');
if (!\App\HomeCards::exists($hcKey)) {
continue;
}
$hcDef = \App\HomeCards::TYPES[$hcKey];
$hcTpl = __DIR__ . '/home/_card_' . $hcKey . '.php';
if (!is_file($hcTpl)) {
continue; // 注册了但还没写模板:跳过,不要白屏
}
$hcMore = (string)($hcDef['route'] ?? '') !== '' ? url((string)$hcDef['route']) : '';
/* 🔴 v63(用户 2026-10-19 第 1 条):「卡片右上角的按钮,应该跟这个卡片
来自于什么模块的主题色一样才对。」
⇒ 颜色从**卡片登记表**取(`HomeCards::module()`),不再在这里硬编码白名单。
日程卡来自 calendar、任务卡来自 tasks、观影来自 movies…… 一张不漏。 */
$hcMod = \App\HomeCards::module($hcKey);
$hcColor = $hcMod !== '' ? \App\Module::color($hcMod, (int)\App\Auth::familyId()) : '';
?>
<section class="card hc-card hc-w<?= (int)$card['w'] ?> hc-h<?= (int)$card['h'] ?>"
data-hc="<?= e($hcKey) ?>" id="homeCard-<?= e($hcKey) ?>">
<div class="hc-head">
<h2 class="cinema-h2"><?= e((string)$hcDef['name']) ?></h2>
<?php if ($hcMore !== ''): ?>
<a class="btn btn-sm hd-more<?= $hcColor === '' ? ' btn-ghost' : '' ?>"
href="<?= e($hcMore) ?>"<?= $hcColor !== '' ? ' style="--hd-c: ' . e($hcColor) . ';"' : '' ?>><?= e((string)$hcDef['more']) ?></a>
<?php endif; ?>
</div>
<?php require $hcTpl; ?>
</section>
<?php endforeach; ?>
</div>
<?php endif; ?>
</section>
<?php /* ==================== 首页任务复选框的表单与脚本(v24 第 25 批 用户第 6 条)====================
「在任何地方点复选框,这个任务就完成了」—— 首页也算"任何地方"。
与任务看板同一套做法:**表单全页一份**,行上只放 data-tk-done。
⚠️ 改完**不摘掉这一行**:用户点完正想看自己刚完成什么,立刻消失会让人怀疑
"是不是点到删除了"。刷新时由服务端决定它还在不在。
⚠️ 这一段是**页面级**的(不属于某张卡片):任务卡片被关掉后它也跟着没用,
但留着无害 —— 别为了"省几行"把它挪进卡片模板里,那样卡片一换模板就丢功能。 */ ?>
<?php if (!empty($tasks) && \App\Auth::canWrite()): ?>
<form method="post" action="<?= e(url('tasks/status')) ?>" id="taskDoneForm" class="hidden-form">
<?= csrf_field() ?>
<input type="hidden" name="id" id="doneId">
<input type="hidden" name="status" id="doneStatus">
<input type="hidden" name="back" id="doneBack" value="home">
</form>
<script>
(function () {
var form = document.getElementById('taskDoneForm');
if (!form) { return; }
var staticWord = {todo: '待办', doing: '进行中', done: '已完成'};
function say(msg, ok) {
var host = document.querySelector('.flash');
if (!host) {
host = document.createElement('div');
host.className = 'flash';
var main = document.querySelector('main') || document.body;
main.insertBefore(host, main.firstChild);
}
var el = document.createElement('div');
el.className = 'flash-item' + (ok ? '' : ' err');
el.setAttribute('role', 'status');
el.textContent = msg;
host.insertBefore(el, host.firstChild);
setTimeout(function () { if (el.parentNode) { el.parentNode.removeChild(el); } }, 3200);
}
Array.prototype.forEach.call(document.querySelectorAll('.tk-check[data-tk-done]'), function (b) {
b.addEventListener('click', function (ev) {
ev.preventDefault();
ev.stopPropagation();
var now = b.getAttribute('data-tk-status') || 'todo';
var next = (now === 'done') ? 'todo' : 'done';
document.getElementById('doneId').value = b.getAttribute('data-tk-done');
document.getElementById('doneStatus').value = next;
document.getElementById('doneBack').value = b.getAttribute('data-tk-back') || 'home';
if (!window.fetch || !window.FormData) { form.submit(); return; }
b.disabled = true;
fetch(form.getAttribute('action'), {
method: 'POST', body: new FormData(form), credentials: 'same-origin',
headers: { 'X-Requested-With': 'XMLHttpRequest' }
}).then(function (r) { return r.json().catch(function () { return null; }); })
.then(function (d) {
b.disabled = false;
if (!d || !d.ok) { form.submit(); return; }
b.setAttribute('data-tk-status', d.status);
b.classList.toggle('is-done', d.status === 'done');
b.setAttribute('aria-pressed', d.status === 'done' ? 'true' : 'false');
var row = b.closest('[data-task-row]');
var badge = row ? row.querySelector('.kb-st') : null;
if (badge) {
badge.className = 'kb-st is-' + d.status + ' is-static';
badge.textContent = staticWord[d.status] || d.word;
}
say(d.msg || '已更新。', true);
}).catch(function () { b.disabled = false; form.submit(); });
});
});
})();
</script>
<?php endif; ?>