Files
FamilyBook/templates/home.php
T
laoyang 4b58fec796 v82: 首页卡片补「新建日程 / 新增成员」+ 两处红线
用户原话:「日程,没有新建日程按钮,名册,也没有新增成员这个按钮。
另外,首页上的日程,红色线不对,变短了左侧缺了一块。在日程模块中,
那个红线左侧是不是还有一个红点?如果有,去掉。」

首页卡片就地操作(不用进模块):
- HomeCards.php 的 agenda/contacts 补 add + add_modal 元数据
- templates/calendar/_add_modal.php:从 calendar/index.php 抽出的
  「新建日程」弹框片段(首页与日程页共用;片段自读变量一律加 cal 前缀,
  避免 require 共享作用域覆盖宿主变量)
- templates/people/form.php:加弹框形态(pfModalOnly / pfModal),
  弹框形态下「取消」走 data-fb-add-close
- index.php:首页 agenda/contacts 两个 $homeAddModals 分支;
  calendar/event_add 支持 back=home 白名单跳转(不采信任意 POST 字符串)

红线(左端缺一块 / 多余红点):
- .ag-now 原来写 left: var(--ag-gutter,50px),但 v80 起它已经在
  .ag-lines 内部(那层自己已有同样缩进)⇒ 左沿跑到 100px、比车道多
  出一整份刻度宽。改成 left: 0
- .tg-now::before 整条删除(日程模块页那个 8px 红点)

涉及共用件(HomeCards/index.php),已跑全量:e2e 1572/0、
v82 正向 20/0、v82 反向 15/0、流水线全绿并已部署。
2026-10-24 04:25:02 +08:00

354 lines
21 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; ?>
<?php /* 🔴 v78(用户 2026-10-22):「在全部任务旁边,放一个新建任务的功能,
其实就是把任务中的新建和编辑功能,搬过来,用弹出框来处理。」
⇒ 卡片登记表里登记了 `add` 的,就在「更多」**右边**再多一颗 ——
同一套 `.hd-more` 配色,所以它是那个模块的主题色,不会抢眼。
🔴 v81(用户同日):「首页上的其他卡片也应该有这些基本功能。
比如电影观影,它有一个"新增电影"的功能;如果是纪念日,就有一个
"新增纪念日"。实际上,就是把原来模块里弹的那个框,现在拿到这边来。」
⇒ 属性改成**通用**的 `data-fb-add-open="#<弹框容器 id>"`(v81 的
`assets/js/add_modal.js` 认这个),弹框 id 由登记表 `add_modal` 给出。
⚠️ 任务的弹框仍由老脚本 `task_add.js` + `data-tk-add-open` 管
(v78 已上线,不动它以免回归);所以这里**按登记表决定挂哪个属性**,
不是一刀切换掉 —— 两种写法并存但各自有唯一来源。 */ ?>
<?php
$hcAdd = (string)($hcDef['add'] ?? '');
$hcAddModal = (string)($hcDef['add_modal'] ?? '');
?>
<?php if ($hcAdd !== '' && \App\Auth::canWrite()): ?>
<button type="button" class="btn btn-sm hd-more<?= $hcColor === '' ? ' btn-ghost' : '' ?>"
<?php if ($hcAddModal !== ''): ?>
data-fb-add-open="#<?= e($hcAddModal) ?>" aria-haspopup="dialog" aria-expanded="false"
<?php else: ?>
data-tk-add-open
<?php endif; ?>
<?= $hcColor !== '' ? ' style="--hd-c: ' . e($hcColor) . ';"' : '' ?>><?= e($hcAdd) ?></button>
<?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; ?>
<?php /* ==================== 「新建任务」弹框(v78 用户 2026-10-22)====================
用户原话:「在全部任务旁边,放一个新建任务的功能,其实就是把任务中的新建和编辑功能,
搬过来,用弹出框来处理。」
⚠️ 与任务页(`templates/tasks/index.php`)**共用同一份** `tasks/_add_modal.php` ——
两处各写一张表单,字段必然有一天不同步(`_edit_form.php` 头部记着同一条教训)。
⚠️ `$tkAdd` 只在「近期任务」卡片真的摆在首页、且当前账号可写时才由路由准备
(见 index.php 的 home 分支):卡片被关掉就没有入口,也就不该白跑三条查询。
⚠️ 新建完回**首页**(`back=home`),卡片上立刻能看到刚建的那条 ——
用户要的是"不离开首页就把事办了"。 */ ?>
<?php if (!empty($tkAdd)): ?>
<?php
$tkAddBack = 'home';
$tkAddBoards = (array)($tkAdd['boards'] ?? []);
$tkAddCats = (array)($tkAdd['categories'] ?? []);
$tkAddPeople = (array)($tkAdd['people'] ?? []);
require __DIR__ . '/tasks/_add_modal.php';
unset($tkAddBack, $tkAddBoards, $tkAddCats, $tkAddPeople, $tkAdd);
?>
<?php endif; ?>
<?php /* ==================== 「新增电影」「新建纪念日」弹框(v81 用户 2026-10-22)============
用户原话:「首页上的其他卡片也应该有这些基本功能。比如电影观影,它有一个"新增电影"
的功能;如果是纪念日,就有一个"新增纪念日"。实际上,就是把原来模块里弹的那个框,
现在拿到这边来。目的就是为了在首页上就能快速完成一定工作,而不用进到模块里面。」
⚠️ 与模块页**共用同一份模板**(`movies/form.php` / `days/form.php`),不另写表单:
字段、校验、服务端契约、附件上传全部同一套,永远不会不同步。
⚠️ 三件事必须一起给,否则这张弹框"长出来了但用不了":
① `$fbAddModalAttr = true` —— 让模板挂 `data-fb-add-modal`(通用开关认它);
② 模板需要的**数据**(名册 / 影院 / 分类)由路由按"卡片在不在"准备,见 index.php;
③ 表单里的 `back=home`(模板按 `$fbAddModalAttr` 自动输出)—— 提交完回首页。
⚠️ 只在对应卡片**真的摆在首页**且账号可写时才渲染:卡片被关掉就没有入口,
不该白跑 People::roster / Cinema::all 这些查询。 */ ?>
<?php if (!empty($homeAddModals) && \App\Auth::canWrite()): ?>
<?php foreach ((array)$homeAddModals as $hcAddKey => $hcAddData): ?>
<?php
$fbAddModalAttr = true; // 通用开关属性(见上面第 ① 条)
/* 每个片段自己兜底变量:模板片段不许偷宿主的变量(本项目重复踩过的坑)。 */
$m = null; $errors = [];
if ($hcAddKey === 'movies') {
$people = (array)($hcAddData['people'] ?? []);
$cinemas = (array)($hcAddData['cinemas'] ?? []);
$mvModalOnly = true; // 不输出 page-head / 设置菜单
$mvModalForce = true; // 强制弹框壳(新增态本来就是,写明更稳)
$mvOpen = false; // 初始关闭,点卡片按钮才开
require __DIR__ . '/movies/form.php';
unset($mvModalOnly, $mvModalForce, $mvOpen, $people, $cinemas, $m, $errors);
} elseif ($hcAddKey === 'days') {
$anModalOnly = true; $anOpen = false; $anModal = true;
$anEmbed = false; $anShell = true;
/* ⚠️ 这三个必须逐个显式给:`days/form.php` 自己会 `$kindsFull ?? 查库` 兜底,
但**兜底会多跑一次查询**,而且与模块页那份不一定同源。
这里直接给它容器已经查好的那份(路由里按"卡片在不在"准备)。 */
$editing = null;
$people = (array)($hcAddData['people'] ?? []);
$kindsFull = (array)($hcAddData['kindsFull'] ?? []);
$kinds = array_column($kindsFull, 'name');
require __DIR__ . '/days/form.php';
unset($anModalOnly, $anOpen, $anModal, $anEmbed, $anShell,
$editing, $people, $kindsFull, $kinds);
} elseif ($hcAddKey === 'agenda') {
/* 🔴 v82(用户 2026-10-23):「日程,没有新建日程按钮……」
⇒ 首页「今天的日程」卡片右上角一颗「新建日程」,点开的是**与日程页同一张**
弹框(`templates/calendar/_add_modal.php`,v82 从 calendar/index.php 抽出来的)。
⚠️ `$calCtx['back'] = 'home'` 是这个分支**唯一**与日程页不同的地方:
提交完回首页(`calendar/event_add` 里认这个白名单值),
用户不用离开首页就能看见刚建的那条。
⚠️ `ym`/`anchor` 用"今天"——首页卡片本来就只显示今天,
新建的日程默认落在今天才符合直觉(日程页那边才用页面锚点)。 */
$calCanNew = true; // 进到这里已经过了 Auth::canWrite()
$calWritable = (array)($hcAddData['writable'] ?? []);
$calCtx = [
'ym' => date('Y-m'),
'view' => 'day', // 首页日程卡就是"日视图那样"
'anchor' => date('Y-m-d'),
'new_at' => '',
'back' => 'home',
];
require __DIR__ . '/calendar/_add_modal.php';
/* ⚠️ 🔴 v82 再修:这段 unset 清单**必须与片段真实赋过的名字一致**。
第一版抄的是模块页的老变量名(`$writable/$ym/$view/$anchor/$newAt`)——
那是片段改用 `$cal*` 前缀**之前**的清单,抄过来有两个问题:
① 那五个名字**片段已经不再赋值**,unset 它们等于**误杀宿主** ——
首页后面还有别的卡片在用(尤其 `$writable`);
② 片段真正留下的 `$cal*` 反而没清干净。
⇒ 现在按片段的真实输出逐个清:`$calCanNew/$calWritable/$calCtx`
三个入参 + 片段内部那 7 个 `$cal*` + `_event_fields.php` 那 8 个。
⚠️ 加新变量时**先回片段末尾那段清单对一遍**再往这里抄。 */
unset($calCanNew, $calWritable, $calCtx,
$calYm, $calView, $calAnchor, $calNewAt, $calBack, $calThisYm, $calBooks,
$firstCal, $hasPersonal, $defDay, $defHM, $defEndH, $evFields,
$evAllDay, $evMvStyle,
/* ⚠️ 片段自己会设 `$fbAddModalAttr`(按 `$calBack` 判定);
外层循环每轮也要设它,不清掉会让第二轮拿到上一轮的值。 */
$fbAddModalAttr);
} elseif ($hcAddKey === 'contacts') {
/* 🔴 v82(用户 2026-10-23):「名册,也没有新增成员这个按钮。」
⇒ 首页「联系人」卡片右上角一颗「新增成员」,点开的是**与名册页同一张**
表单(`templates/people/form.php` 新加的弹框形态)。
⚠️ 这套开关(`$pfModalOnly` / `$pfModal` / `$pfOpen`)是 v82 才加到
people/form.php 的,之前那页只能整页打开 —— 所以这里**必须**逐个给全,
漏一个就会退回"整页表单塞进卡片里"那种坏形态。
⚠️ `$person = null` + `$isEdit = false` 是新增态的判据(模板第一行按
`$person['id']` 算 $isEdit),不要靠"不传"来默认。 */
$pfModalOnly = true;
$pfModal = true;
$pfOpen = false;
$person = null;
$errors = [];
$canEditAll = true;
$isSelf = false;
$jobRows = [[]]; // 工作经历先给一行空白(与 people/new 一致)
$myNote = '';
$myTags = [];
$tagChoices = (array)($hcAddData['tagChoices'] ?? []);
$myNick = '';
$contacts = [];
require __DIR__ . '/people/form.php';
unset($pfModalOnly, $pfModal, $pfOpen, $person, $errors, $canEditAll, $isSelf,
$jobRows, $myNote, $myTags, $tagChoices, $myNick, $contacts);
}
unset($fbAddModalAttr);
?>
<?php endforeach; ?>
<?php endif; ?>