- 纪念日列表: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 可改尺寸、调顺序、开关
187 lines
9.2 KiB
PHP
187 lines
9.2 KiB
PHP
<?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; ?>
|