Files
FamilyBook/templates/home_cards.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

148 lines
6.9 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.php / app.css 的 `.hc-*` 完全一致):
* · 一排 **8 格**;宽 = 占几格 → 2:一排 4 张 / 4:一排 2 张 / 8:独占一排;
* · 高 = 占几行 → 2 / 4 / 6 / 8(一行 76px + 10px 间距)。
* ⚠️ 卡片宽度**允许 6**(不是只有 2/4/8):6 宽 = 一排"一大一小",
* 这是 8 格制里很自然的一种排法,实现上零成本,就没必要禁掉。
*
* 交互:▲▼ 与拖动都能排序(拖动的状态只存在于 DOM 里,提交时按 DOM 顺序读 order[])。
*
* @param array $rows HomeCards::orderedForPicker() 的结果(含 on/w/h/n)
*/
$hcRows = (array)($rows ?? []);
$hcN = count($hcRows);
$hcWs = \App\HomeCards::WIDTHS;
$hcHs = \App\HomeCards::HEIGHTS;
?>
<div class="page-head">
<h1>首页卡片</h1>
<span class="sub">一排 8 格 · 宽 2 就是一排 4 张,宽 8 就是独占一排</span>
<div class="actions">
<a class="btn btn-ghost" href="<?= e(url('home')) ?>">返回首页</a>
</div>
</div>
<form method="post" action="<?= e(url('home/cards_save')) ?>" class="card card-pad">
<?= csrf_field() ?>
<h2 class="cinema-h2">
摆哪几张、多大
<span class="hint">只影响你自己看到的首页</span>
</h2>
<p class="small muted" style="margin:0 0 16px">
<b>宽</b>:2 = 一排 4 张,4 = 一排 2 张,6 = 一大一小,8 = 独占一排。
<b>高</b>:2 / 4 / 6 / 8 行(一行 76px),卡片内容多的时候内部会自己滚。
<br>关掉的卡片<b>不会丢数据</b>,只是不摆在首页上。
<br>「通知消息」与其它卡片一样可以调顺序和大小 —— 宽 2 时正文会折两行,
想看完整的一句话就给它 4 格或更宽。
</p>
<ol class="hc-set-list" id="hcSetList">
<?php foreach ($hcRows as $i => $r): $k = (string)$r['k']; $def = \App\HomeCards::TYPES[$k]; ?>
<li class="hc-set-row" draggable="true" data-hc-key="<?= e($k) ?>">
<?php /* 顺序藏在 DOM 里:每行自带 order[],拖动搬整行 ⇒ 提交上来的顺序
就是屏幕上的顺序(与「模块设置」那张表同一手法)。 */ ?>
<input type="hidden" name="order[]" value="<?= e($k) ?>">
<span class="hc-set-order">
<span class="drag-handle" title="拖我排序" aria-hidden="true">⋮⋮</span>
<button class="btn btn-ghost btn-sm" type="submit" name="move" value="up:<?= e($k) ?>"
title="上移" <?= $i === 0 ? 'disabled' : '' ?>>▲</button>
<button class="btn btn-ghost btn-sm" type="submit" name="move" value="down:<?= e($k) ?>"
title="下移" <?= $i === $hcN - 1 ? 'disabled' : '' ?>>▼</button>
</span>
<label class="hc-set-on">
<input type="checkbox" name="on[<?= e($k) ?>]" value="1" <?= !empty($r['on']) ? 'checked' : '' ?>>
<span class="hc-set-name">
<b><?= e((string)$def['name']) ?></b>
<span class="small muted"><?= e((string)$def['desc']) ?></span>
</span>
</label>
<label class="hc-set-dim">
<span class="small muted">宽</span>
<select class="select" name="w[<?= e($k) ?>]">
<?php foreach ($hcWs as $w): ?>
<option value="<?= (int)$w ?>" <?= (int)$r['w'] === (int)$w ? 'selected' : '' ?>><?= (int)$w ?></option>
<?php endforeach; ?>
</select>
</label>
<label class="hc-set-dim">
<span class="small muted">高</span>
<select class="select" name="h[<?= e($k) ?>]">
<?php foreach ($hcHs as $h): ?>
<option value="<?= (int)$h ?>" <?= (int)$r['h'] === (int)$h ? 'selected' : '' ?>><?= (int)$h ?></option>
<?php endforeach; ?>
</select>
</label>
<?php if ((int)$def['n'] > 0): ?>
<?php /* 用户原话:「联系人,可以有卡片,让用户选择显示几个联系人在首页上。」 */ ?>
<label class="hc-set-dim">
<span class="small muted">显示</span>
<input class="input hc-set-n" type="number" name="n[<?= e($k) ?>]"
min="1" max="<?= (int)\App\HomeCards::N_MAX ?>"
value="<?= (int)$r['n'] ?>" aria-label="显示几个联系人">
<span class="small muted">个</span>
</label>
<?php else: ?>
<span class="hc-set-dim hc-set-null"></span>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ol>
<div class="btn-row" style="margin-top:16px">
<button class="btn" type="submit" name="save" value="1">保存布局</button>
<span class="small muted">改完记得保存 —— 这一页不会边改边生效。</span>
</div>
</form>
<script>
(function () {
/* 拖动排序(▲▼ 是没有 JS 时的兜底,两条路都能用)。
⚠️ 搬的是整行 <li>:它自带的 order[] 隐藏域跟着走,
提交时按 DOM 顺序读,不用再同步一份顺序数组。 */
var list = document.getElementById('hcSetList');
if (!list) { return; }
var dragEl = null;
Array.prototype.forEach.call(list.querySelectorAll('.hc-set-row'), function (li) {
li.addEventListener('dragstart', function (ev) {
dragEl = li;
li.classList.add('is-dragged');
if (ev.dataTransfer) { ev.dataTransfer.effectAllowed = 'move'; }
});
li.addEventListener('dragend', function () {
li.classList.remove('is-dragged');
dragEl = null;
Array.prototype.forEach.call(list.querySelectorAll('.hc-set-row'), function (x) {
x.classList.remove('is-drop-before');
});
});
li.addEventListener('dragover', function (ev) {
if (!dragEl || dragEl === li) { return; }
ev.preventDefault();
var box = li.getBoundingClientRect();
li.classList.toggle('is-drop-before', ev.clientY < box.top + box.height / 2);
});
li.addEventListener('dragleave', function () { li.classList.remove('is-drop-before'); });
li.addEventListener('drop', function (ev) {
if (!dragEl || dragEl === li) { return; }
ev.preventDefault();
var box = li.getBoundingClientRect();
if (ev.clientY < box.top + box.height / 2) {
list.insertBefore(dragEl, li);
} else {
list.insertBefore(dragEl, li.nextSibling);
}
li.classList.remove('is-drop-before');
});
});
})();
</script>