用户 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() 统一保证
201 lines
11 KiB
PHP
201 lines
11 KiB
PHP
<?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; ?>
|