Files
FamilyBook/templates/home/_card_agenda.php
T
laoyang 9a66068c33 v62:纪念日列表修正 / 分类图标与设置菜单 / 首页卡片体系
- 纪念日列表:display:flex 从 td 移到内层 div(写在 td 上会破坏表格列算法,
  表现为关联人列与其它列高度不一致);行内列序按表头对齐;
  改完再点「新建」不再带出上次修改的数据;文案改为「与我相关」「来自名册」;
  别人记的、把我挂成关联人的条目一并显示(split() 返回 hiddenIds,
  路由层据此过滤「接下来的日子」,与两张表同一套可见性)
- 分类图标:新增 app/AnnivIcon.php(17 个行内 SVG 图标 + 同 key 的 emoji
  供日历纯文本处使用,内网无 CDN 不用 FontAwesome);去掉「看日历」按钮;
  设置按钮改为菜单,第一项进 days/settings 独立页;分类可自选图标,
  列表徽标与日历条目都按分类显示
- 首页卡片:新增 app/HomeCards.php 与 templates/home_cards.php;
  8 格宽 × 高 2/4/6/8;首页拆成 templates/home/_card_*.php 共 8 个片段;
  home/cards 可改尺寸、调顺序、开关
2026-10-19 12:51:17 +08:00

187 lines
9.2 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 从 templates/home.php 拆出来的,逻辑一字未改)。
*
* 用户原话(v24 第 24 批 第 3 条):「日程就显示今天的日程,也就是日视图那样形式,
* 为了好看高度需要固定。这样的话,鼠标放上去之后,向下可以滚动,暂时不要出现
* 滚动条太丑了。为了节省上下滚动的这个空间,我们可以把没有日程的那些时间
* 掐头去尾给它去掉。…需要在当前的时间或者是时刻那里显示一条红线。」
*
* ⚠️ 高度现在由**卡片高度**决定(`.hc-h*`),不再写死 440px —— 这正是"卡片化"
* 的核心:同一份内容,用户让它多高它就多高。
*
* @var array $todayEvents 今天的事件(全天在前、其余按时刻升序)
*/
$allDay = [];
$timed = [];
foreach (($todayEvents ?? []) as $ev) {
if ((string)($ev['time'] ?? '') === '') {
$allDay[] = $ev;
} else {
$timed[] = $ev;
}
}
// ---- 掐头去尾:只画"第一个日程前一小时"到"最后一个日程后一小时" ----
$startH = 0;
$endH = 24;
if ($timed) {
$mins = array_map(static function (array $e): int {
// ⚠️ 变量名带 `ag` 前缀:这是**被 require 进来的片段**,裸赋值 $p 会撞上
// 宿主模板里同名的 $p(流水线的"片段不许覆盖宿主变量"检查会拦下来)。
$agHM = explode(':', (string)$e['time']);
return ((int)($agHM[0] ?? 0)) * 60 + (int)($agHM[1] ?? 0);
}, $timed);
$startH = max(0, intdiv(min($mins), 60) - 1);
$endH = min(24, intdiv(max($mins), 60) + 2);
/* 🔴 v28 用户第 1 条:「今天的日程,并没有定位到红线所在的位置」。
根因:**区间里没有"现在"**,于是红线不画、定位算出来是越界值。
⇒ 让区间永远包含当前时刻(用户要的正是"一眼看到现在处在哪儿")。
🔴🔴 v44 第 3 条:首页红线"停在整点"的真根因是这里以前用 `date('G')`
(只取小时),而日程页用的是 `小时*60+分钟`。现在两处同一口径。 */
$nowHNow = (int)date('G');
if ($nowHNow < $startH) {
$startH = max(0, $nowHNow - 1);
}
if ($nowHNow >= $endH) {
$endH = min(24, $nowHNow + 2);
}
if ($endH <= $startH) {
$endH = min(24, $startH + 2);
}
}
$nowMinutes = ((int)date('G')) * 60 + (int)date('i');
$nowPos = (($nowMinutes / 60) - $startH) / max(1, ($endH - $startH));
$showNow = $nowPos >= 0 && $nowPos <= 1;
/* 冲突分列(v30 第 2/4 条):与日程页同一套算法 —— 两条撞车就左右各半。 */
usort($timed, static function (array $a, array $b): int {
return strcmp((string)($a['time'] ?? ''), (string)($b['time'] ?? ''));
});
$laneInfo = \App\Calendar::conflictLanes($timed);
foreach ($timed as $i => &$evRef) {
[$l, $w] = $laneInfo[$i] ?? [0.0, 1.0];
$evRef['left'] = round($l * 100, 4);
$evRef['width'] = round($w * 100, 4);
}
unset($evRef);
?>
<div class="hc-body hc-agenda">
<?php if (!$todayEvents): ?>
<div class="hd-pane-body">
<p class="empty-hint">今天没有安排。</p>
</div>
<?php else: ?>
<?php /* 全天日程**不在滚动容器里**(v24 第 26 批 第 2 条:要"一直显示在上面")。
为什么不用 sticky:sticky 只能粘在滚动区顶端,滚起来仍会被"标题+滚动区"
两层挤着,看起来还是在动;直接移出滚动容器是零副作用的做法。 */ ?>
<?php if ($allDay): ?>
<div class="ag-allday">
<?php foreach ($allDay as $ev): ?>
<?php
$evKind = (string)($ev['kind'] ?? 'custom');
$evRef = (int)($ev['ref_id'] ?? 0);
$evEdit = '';
if ($evKind === 'custom' && $evRef > 0) { $evEdit = url('calendar/event', ['id' => $evRef, 'embed' => 1]); }
elseif ($evKind === 'movie' && $evRef > 0) { $evEdit = url('movies/edit', ['id' => $evRef, 'embed' => 1]); }
elseif ($evKind === 'task' && $evRef > 0) { $evEdit = url('tasks/edit', ['id' => $evRef]); }
elseif ($evKind === 'anniv' && $evRef > 0) { $evEdit = url('days/edit', ['id' => $evRef, 'embed' => 1]); }
?>
<a class="ag-allday-item" href="<?= e((string)$ev['href']) ?>"
data-fb-entry='<?= fb_entry_json($ev, (string)($ev['href'] ?? ''), $evEdit) ?>'>
<span class="dash-dot" style="background:<?= e((string)$ev['color']) ?>;"></span>
<span class="ag-ad-t">全天</span>
<span class="ag-ad-w"><?= e((string)$ev['title']) ?></span>
<span class="dash-cal" style="background:<?= e((string)$ev['color']) ?>;"><?= e((string)$ev['calname']) ?></span>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div class="hd-pane-body" data-ag-scroll>
<div class="ag-grid">
<?php for ($h = $startH; $h < $endH; $h++): ?>
<div class="ag-row">
<span class="ag-hour"><?= sprintf('%02d:00', $h) ?></span>
<span class="ag-line"></span>
</div>
<?php endfor; ?>
<?php if ($showNow): ?>
<?php /* `data-now-min` 是给探针的零容差观测点(服务端用的分钟数),
不影响渲染 —— 见 v45 那条注释。 */ ?>
<div class="ag-now" style="top: <?= number_format($nowPos * 100, 2) ?>%"
data-now-min="<?= (int)$nowMinutes ?>"></div>
<?php endif; ?>
<?php
/* 日程块:按"相对第一个刻度的小时数"定位。
⚠️ left/width 与日历页 `.tg-block` 同一套(left = 46px 刻度 + 2px,
width = 列宽 - 4px)—— 曾经这里扣了 46px 的缝,导致两块之间空出 50px。 */
foreach ($timed as $ev):
$agHM = explode(':', (string)$ev['time']);
$evMin = ((int)($agHM[0] ?? 0)) * 60 + ((int)($agHM[1] ?? 0));
$topPct = (($evMin / 60) - $startH) / max(1, ($endH - $startH)) * 100;
$topPct = max(0, min(96, $topPct));
$et2 = (string)($ev['end_time'] ?? '');
if ($et2 !== '' && preg_match('/^(\d{1,2}):(\d{2})/', $et2, $mm2)) {
$endMin = ((int)$mm2[1]) * 60 + ((int)$mm2[2]);
} else {
$endMin = $evMin + 30;
}
if ($endMin <= $evMin) { $endMin = $evMin + 30; }
$endMin = min($endMin, $endH * 60);
$hPct = ($endMin / 60 - $startH) / max(1, ($endH - $startH)) * 100 - $topPct;
$hPct = max(2.2, $hPct);
$evKind = (string)($ev['kind'] ?? 'custom');
$evRef = (int)($ev['ref_id'] ?? 0);
$evEdit = '';
if ($evKind === 'custom' && $evRef > 0) { $evEdit = url('calendar/event', ['id' => $evRef, 'embed' => 1]); }
elseif ($evKind === 'movie' && $evRef > 0) { $evEdit = url('movies/edit', ['id' => $evRef, 'embed' => 1]); }
elseif ($evKind === 'task' && $evRef > 0) { $evEdit = url('tasks/edit', ['id' => $evRef]); }
elseif ($evKind === 'anniv' && $evRef > 0) { $evEdit = url('days/edit', ['id' => $evRef, 'embed' => 1]); }
?>
<a class="ag-ev" href="<?= e((string)$ev['href']) ?>"
data-fb-entry='<?= fb_entry_json($ev, (string)($ev['href'] ?? ''), $evEdit) ?>'
style="background: <?= e((string)$ev['color']) ?>;
top: <?= number_format($topPct, 2) ?>%;
height: <?= number_format($hPct, 2) ?>%;
left: calc(46px + <?= (float)($ev['left'] ?? 0) ?>% + 2px);
width: calc(<?= (float)($ev['width'] ?? 100) ?>% - 4px);"
title="<?= e((string)$ev['title']) ?>"
data-cal="<?= (int)$ev['cal'] ?>">
<span class="ag-ev-t"><?= e((string)$ev['time']) ?></span>
<span class="ag-ev-w"><?= e((string)$ev['title']) ?></span>
<span class="ag-ev-cal"><?= e((string)$ev['calname']) ?></span>
</a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
<?php if ($todayEvents): ?>
<script>
/* 进场定位到"现在"(v24 第 26 批 第 2 条):让红线落在可视区**正中**。
⚠️ 用 scrollTop 直接赋值,**不要** scrollIntoView({block:'center'}) ——
后者会把整个浏览器窗口也滚下去,一进首页就被拽到页面中部。
⚠️ 只判"有没有网格可滚",别判"有没有分时段日程"(v29 踩过:只有全天日程时
脚本整段不输出,而网格和红线照样渲染 ⇒ 红线在、页面停在最上面)。 */
(function () {
var box = document.querySelector('#homeCard-agenda [data-ag-scroll]')
|| document.querySelector('[data-ag-scroll]');
if (!box) { return; }
var line = box.querySelector('.ag-now');
var RATIO = <?= json_encode(round($nowPos, 6)) ?>;
function targetOffset() {
if (line) { return line.offsetTop; }
var grid = box.querySelector('.ag-grid');
return grid ? grid.offsetHeight * RATIO : 0;
}
function centerOnNow() {
box.scrollTop = Math.max(0, targetOffset() - (box.clientHeight / 2));
}
centerOnNow();
if (window.requestAnimationFrame) {
requestAnimationFrame(function () { requestAnimationFrame(centerOnNow); });
}
})();
</script>
<?php endif; ?>