Files

241 lines
13 KiB
PHP
Raw Permalink Normal View History

<?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) {
/* 🔴🔴 v85(用户 2026-10-24):「今天的日程,有一个日程到下午五点半,
首页日程刻度只有九点。」
真根因:这里**只按 `time`(开始时间)算范围**,`end_time` 压根没参与
⇒ 一条 08:30–17:30 的日程被当成"8 点半就结束了",
于是 `endH = 8 + 2 = 10`,刻度只画到 09:00,
色块高 540 分钟却只塞进 3 小时的格子里(被 `min(96, …)` 夹住,糊成一片)。
⚠️ 判据必须是「**结束时间也参与**」,不能只是"把 +2 改成 +12":
写死一个更大的偏移量,遇到 20:00 结束的日程照样截断,
而且会把"只有一条 9 点日程"的卡片硬拉成 12 小时高(用户嫌空)。
⚠️ `end_time` 可能是空(服务端没给)⇒ 那时按"开始 + 30 分钟"兜底,
与下面算 `hPct` 时的兜底口径**必须一致**,否则刻度与色块又会错位。 */
$agStarts = [];
$agEnds = [];
foreach ($timed as $agEv) {
$agHM0 = explode(':', (string)$agEv['time']);
$agStart = ((int)($agHM0[0] ?? 0)) * 60 + (int)($agHM0[1] ?? 0);
$agStarts[] = $agStart;
$agEnd = 0;
$agET = (string)($agEv['end_time'] ?? '');
if ($agET !== '' && preg_match('/^(\d{1,2}):(\d{2})/', $agET, $agMM)) {
$agEnd = ((int)$agMM[1]) * 60 + ((int)$agMM[2]);
}
/* 跨天(结束 <= 开始)时按"到当天 24 点"算 —— 不能取 24:00 以上的值,
否则 $endH 被夹到 24 之后刻度会凭空多出几行。 */
if ($agEnd <= $agStart) {
$agEnd = $agStart + 30;
}
if ($agEnd > 1440) {
$agEnd = 1440;
}
$agEnds[] = $agEnd;
}
/* ⚠️ 变量名带 `ag` 前缀:这是**被 require 进来的片段**,裸赋值会撞上
宿主模板里的同名变量(流水线的"片段不许覆盖宿主变量"检查会拦下来)。 */
$startH = max(0, intdiv(min($agStarts), 60) - 1);
/* 🔴 用**结束时间**的上界(向上取整到小时)—— 这就是本条的真修法。
`+1` 之后又 `ceil`:17:30 → ceil(1050/60)=18,留半小时缓冲;再 +0(不额外留
一小时),因为卡片本来就矮,"最后一个日程结束再画一整小时空行"太浪费。 */
$endH = min(24, (int)ceil(max($agEnds) / 60));
if ($endH <= intdiv(max($agStarts), 60)) {
$endH = min(24, intdiv(max($agStarts), 60) + 1);
}
/* 🔴 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 /* 🔴🔴 v80(用户 2026-10-22):「首页日程右侧是缺一块的,是不是左侧有刻度,
导致你算宽度的时候,没算对?」—— 用户猜对了。
⇒ 色块全部挪进这个 `.ag-lanes` 层:它左边界让出刻度(`--ag-gutter`)、
右边留 2px,于是下面那些 `left: X%` / `width: calc(Y% - 4px)`
的百分比相对的是**真正可用的宽度**。
⚠️ 之前没有这一层,百分比按"含刻度的整宽"算,色块右端比卡片**多出 44px**
被 `overflow:hidden` 裁掉 —— 那就是"缺一块"。 */
?>
<div class="ag-lanes">
<?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
/* 日程块:按"相对第一个刻度的小时数"定位。
⚠️ v80 起 `left` / `width` 的百分比**只相对车道宽**(`.ag-lanes`),
不再自己加 46px —— 加过一次就是"两套账混算"(见上面那段注释)。
`- 4px` 是左右各 2px 的视觉留白,与日历页 `.tg-block` 的 `- 4px` 同款。 */
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(<?= (float)($ev['left'] ?? 0) ?>% + 2px);
width: calc(<?= (float)($ev['width'] ?? 100) ?>% - 4px);"
title="<?= e((string)$ev['title']) ?>"
data-cal="<?= (int)$ev['cal'] ?>">
<?php /* 🔴 v63(用户 2026-10-19 第 2 条):「首页日程显示,把所属的日历本,
放在色块的右侧。」⇒ 时间与标题包进 `.ag-ev-main`(占据剩余宽度、可收缩),
日历本作为它右边的**兄弟**。
⚠️ 原来是「时间 float:left + 标题 block + 日历本 block」三板斧:日历本
`display:block` 必然被顶到第二行,色块一矮就完全看不见。改成弹性行
后,日历本与标题同一行、永远在右。 */ ?>
<span class="ag-ev-main">
<span class="ag-ev-t"><?= e((string)$ev['time']) ?></span>
<span class="ag-ev-w"><?= e((string)$ev['title']) ?></span>
</span>
<span class="ag-ev-cal"><?= e((string)$ev['calname']) ?></span>
</a>
<?php endforeach; ?>
</div>
</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; ?>