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