Files
FamilyBook/templates/home/_card_agenda.php
T
laoyang b1d4502148 v80 首页日程:修「右侧缺一块」——色块百分比按含刻度的整宽算了
用户:「网页端,首页,日程好像显示不全,右侧是缺一块的,是不是左侧有刻度,
导致你算宽度的时候,没算对?」—— 猜对了,而且比想的严重。

原因:色块内联 `left: calc(46px + L%)` 是**绝对**让位(46px 给刻度),
`width: calc(W% - 4px)` 的百分比却相对**含刻度的整宽**(.ag-grid 481px)。
于是 100% 时色块宽 477、左端 48,右端落到 744,而卡片右边界只有 700
⇒ 多出 44px 被 .hc-agenda 的 overflow:hidden 裁掉,日历本名正好在那截里。
日历页 .tg-block 没这个病:它左边刻度是独立的一列(.tg-col),百分比天然相对列宽。

修法(换参照系,不是调数字):新增 .ag-lanes 定位层,左让刻度、右留 2px,
色块与红线都挪进去,百分比就相对真正可用的宽度算;刻度宽度收成单一变量
--ag-gutter(50px),以后改刻度只改它。内联删掉 46px。

验证四层:
- _fb_agenda_geom.py 27/0:不越界 / 左右留白 2&4px / 宽度自洽 / 与日历页同公式
- _fb_agenda_pixel.py 8/0:截图数着色列,右沿留白 5px(修前 0px)
- _fb_agenda_falsify.py 11/0:退回旧写法与车道超宽都变红,合法微调仍绿
- e2e 1558/0 · 几何 428/0 · b37 5/0

同时修两处失效探针:
- _fb_b37_ui.py 点 #taskAddBtn(v78 已改成 [data-tk-add-open])导致基线超时全红
- _fb_b37_falsify.py 注入锚点写死 calc(46px + ...),改到会重现该 bug 的 width 上
部署脚本加 v80 三条静态断言(已逐条注入确认会红)。
2026-10-22 20:22:54 +08:00

206 lines
11 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 /* 🔴🔴 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; ?>