Files
FamilyBook/templates/home.php
T
laoyang c8839fa728 v26 第 26 批(5 条):财务多选按集合取图 / 首页全天置顶与红线定位 / 任务复选框去提示与看板颜色 / 导航搜索引擎切换与站点复用 / 详情页去复选框
- 财务:勾选多个标签时上面那张「总金额」图按**选中集合**统计(新增
  ledger/total_chart 路由 + 单一容器 data-ck-total-host,按集合现取并缓存;
  Ledger::timeline 第二参数放宽为 string|array)
- 首页日程:全天日程移出滚动容器固定置顶;进场把当前时刻红线居中
  (scrollTop 计算,不用 scrollIntoView),触发条件不绑 $showNow
- 任务:复选框去除 title 提示;看板可设颜色(Task::boardColor/updateBoard),
  首页任务行用该颜色显示所属看板
- 导航:展示页补回 id=engine-tabs(切换搜索引擎无效的根因);补内置引擎;
  新增站点输 URL 可复用他人已加过的站点(sharedByUrl/copyIconFile/adoptSite)
- 任务详情页页头去掉复选框(已有「标记完成」按钮)
2026-10-07 15:31:26 +08:00

500 lines
28 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
/**
* 首页 —— 一块 dashboard。
*
* ────────────────────────────────────────────────────────────────
* v24 第 24 批 用户第 2、3 条(本次重排,用户原话):
*
* 第 2 条:「首页上的功能…一个就是上面的简要信息,这样去写。早上好,中午好,
* 或者晚上好,某某某今天是几月几日?星期几农历几月几日?如果有节气的
* 话,或者有重要的节日的话,在这里显示。比如说今天是这个国庆节啊,
* 今天是端午节,或者是今天是中秋节,或者是今天是芒种,今天是大寒,
* 今天是小寒,今天是什么纪念日,今天是谁的生日等等。显示在这里
* 紧接着它的下面显示今天一共有多少条日程,今天有几件代办正在进行。
* 把天气放在这个信息面板的最右侧,以图标的形式,彩色图标的形式去进行
* 展示。比如说晴天多云下雨,然后下面显示呃温度、风力等等,在最下面
* 再显示呃地址。就是比如说他选中的是他在个人设置里面设置的是青岛,
* 那么就显示山东青岛就是让它放在这个信息面板的最右侧,靠右放。」
*
* 第 3 条:「家里的镜框这里呃全部不要了…紧接着直接显示日程和任务日程和任务在
* 整个页面里面左右,并排放各占一半,日程就显示今天的日程,也就是
* 日视图那样形式,为了好看高度需要固定。这样的话,鼠标放上去之后,
* 向下可以滚动,暂时不要出现滚动条太丑了。为了节省上下滚动的这个空间,
* 我们可以把没有日程的那些时间掐头去尾给它去掉。…同样的和日视图一样,
* 需要在当前的时间或者是时刻那里显示一条红线。右侧显示任务模块里面的
* 数据显示当前正在进行当中的所有的任务,除了显示任务的标题之外,
* 还要显示他的状态。比如说正在进行中待确认等等。除了这之外,
* 还要显示他所处的看板的名称,就是他现在被放在哪一个看板里面,
* 就后最下面显示的是往年的今天这个模块。」
*
* ⇒ 所以现在首页自上而下只有三块:
* ① 信息面板(问候 + 日子标记 + 今日数字 | 最右:彩色天气图标 + 温度风力 + 地址)
* ② 日程 / 任务 **左右各半**(各自固定高度、内部滚动、不显滚动条)
* ③ 往年的今天
*
* ⚠️ 被删掉的(用户明确说"全部不要了"):
* · 「家里的近况」模块卡片墙(moduleCards)—— 那张一片一片的统计卡
* · 「未来七天」清单 —— 与左侧「今天日程」重复
* 两块都不再渲染。服务端仍照旧算 moduleCards(个人设置里那个开关还留着),
* 但首页不再用 —— 留着它是为了"以后要恢复"时不用动后端。
*
* ⚠️ 「家人卡片」「两个月内的生日」早先就已按用户要求撤掉,本批不变。
*/
// 生日事件上用简称,卡片才不会一行塞不下;长名字在详情页里出
$today = date('Y-m-d');
// 首页要显示哪些模块、什么顺序 —— 用户在「个人设置 → 首页上的模块」里定(每人一份)
$homeMods = array_values(array_filter(
(array)($homeModules ?? []),
static fn($k) => \App\Module::exists((string)$k)
));
$todayMarks = $todayMarks ?? ['tags' => [], 'line' => ''];
?>
<?php /* 用户 2026-10-03 第 3 条:
① 标题下显示**个人 slogan**(原来撤掉了,现在要回来);
② 右侧那三块统计(与我有关 / 名册总数 / 亲属连线)**去掉**;
③ 「我的资料 / 我的家谱」保留,但从文字按钮换成**图标按钮**;
④ 「名册」按钮去掉 —— 它与顶部导航条里的「名册」重复。
⚠️ 图标用行内 SVG(不引外部字体/图片),与观影设置下拉同一套做法。 */ ?>
<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('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 /* ==================== ① 信息面板(用户第 2 条)====================
左:问候 + 姓名 → 日期/星期/农历 → 日子标记(节气/节日/纪念日/生日)→ 今日数字
右:**彩色**天气图标 + 温度 + 风力 → 地址
⚠️ 天气靠右:用户原话「把天气放在这个信息面板的最右侧…靠右放」。
用 flex + margin-left:auto 把它顶到右端(窄屏会自动折到下一行,见 CSS)。 */ ?>
<section class="section">
<div class="card card-pad home-brief">
<div class="hb-left">
<?php /* ① 问候(早上好/中午好…)+ 我的称呼 —— 问候语里已经带了名字,
但用户明确要求「早上好,某某某」这样的形式,所以名字单独再强调一次。
(Home::greeting 本身会拼 "早上好,老杨,今天有 3 件事",
这里只用它的"时段"部分,尾巴换成面板里更该看的信息。) */ ?>
<?php
$hbPart = '你好';
$hbH = (int)date('G');
if ($hbH < 5) { $hbPart = '夜深了'; }
elseif ($hbH < 11) { $hbPart = '早上好'; }
elseif ($hbH < 13) { $hbPart = '中午好'; }
elseif ($hbH < 18) { $hbPart = '下午好'; }
else { $hbPart = '晚上好'; }
?>
<div class="hb-hello">
<b><?= e($hbPart) ?></b><?php if (trim((string)($meName ?? '')) !== ''): ?>,<?= e((string)$meName) ?><?php endif; ?>
</div>
<div class="hb-date">
<span class="hb-d"><?= e((string)($todayLabel ?? date('n 月 j 日'))) ?></span>
<span class="hb-w"><?= e((string)($weekday ?? '')) ?></span>
<?php if (($lunarShort ?? '') !== ''): ?>
<span class="hb-lunar" title="<?= e((string)($lunarLine ?? '')) ?>"><?= e((string)$lunarShort) ?></span>
<?php endif; ?>
</div>
<?php if (!empty($todayMarks['tags'])): ?>
<div class="hb-marks">
<?php foreach ($todayMarks['tags'] as $mk): ?>
<span class="hb-mark is-<?= e((string)$mk['kind']) ?>"><?= e((string)$mk['word']) ?></span>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div class="hb-stats">
<a class="hbs" href="<?= e(url('calendar')) ?>">
<b><?= count($todayEvents ?? []) ?></b> 条日程(今天)
</a>
<?php /* 🔴🔴 v24 第 25 批 用户第 3 条修:「为什么显示 0 条待办呢?
任务看板明明有一个进行中的任务啊。」
根因:这句话原来读的是 `stat7['todos']` = `count(Task::byStatus(...,'todo'))`
—— 只数**待办**。而右边那块「手头在做」列的是 **doing(+ 已逾期的待办)**,
用户刚在看板上拖成「进行中」的那件任务于是**不计入**,数字就显示 0。
两个口径不同源,数字和列表当场对不上。
⇒ 现在这个数字与右侧列表**同源**:`doing + 已逾期的 todo`
(也就是 `count($tasks)`,与 index.php 传给本页的那个数组完全一致)。
文案也说清是什么:「件在做」,而不是含糊的「件待办」。
⚠️ 「已经动手」与「排队等着」是两件事(用户第 6 条要求说清区别):
首页要回答的是"我手头在做什么",所以主数字用「在做」的口径;
排队的那些(todo)另给一个小字,不必让人点进任务页才知道还有多少。 */ ?>
<a class="hbs" href="<?= e(url('tasks')) ?>"
title="手头在做(进行中 + 已逾期的待办)">
<b><?= count($tasks ?? []) ?></b> 件在做
</a>
<?php if ((int)($stat7['todos'] ?? 0) > 0): ?>
<a class="hbs" href="<?= e(url('tasks')) ?>" title="记下来了、还没动手的那些">
<b><?= (int)$stat7['todos'] ?></b> 件待办
</a>
<?php endif; ?>
<?php if ((int)($stat7['overdue'] ?? 0) > 0): ?>
<a class="hbs is-late" href="<?= e(url('tasks')) ?>">
<b><?= (int)$stat7['overdue'] ?></b> 件逾期
</a>
<?php endif; ?>
</div>
</div>
<?php if (!empty($weather)): ?>
<div class="hb-weather is-<?= e((string)($weather['tone'] ?? 'cloud')) ?>">
<span class="hbw-icon" aria-hidden="true"><?= e((string)($weather['icon'] ?? '')) ?></span>
<div class="hbw-main">
<div class="hbw-temp"><?= e((string)($weather['temp'] ?? '')) ?></div>
<div class="hbw-text"><?= e((string)($weather['text'] ?? '')) ?></div>
<?php if (($weather['wind'] ?? '') !== ''): ?>
<div class="hbw-wind">
<?= $weather['wdir'] !== '' ? e((string)$weather['wdir']) . ' ' : '' ?><?= e((string)$weather['wind']) ?>
</div>
<?php endif; ?>
</div>
<?php if (($weather['place'] ?? '') !== ''): ?>
<div class="hbw-place"><?= e((string)$weather['place']) ?></div>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</section>
<?php /* ==================== ② 日程 / 任务 左右各半(用户第 3 条)====================
⚠️ 高度**固定**(CSS 里给 .hd-pane-body 一个 max-height):
用户原话「为了好看高度需要固定。这样的话,鼠标放上去之后,向下可以滚动,
暂时不要出现滚动条太丑了」——
所以是 `overflow-y:auto` + 隐藏滚动条(`scrollbar-width:none` 等三套写法)。
⚠️ 日程掐头去尾:见下面 $hr 的算法 —— 只画"第一个日程前一小时"到"最后一个日程后一小时",
中间空档不裁(裁了会让人以为日程连在一起)。全天日程永远在顶上。 */ ?>
<?php
// ---- 日程时间轴:掐头去尾(用户第 3 条"没有日程的那些时间掐头去尾给它去掉")----
$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 {
$p = explode(':', (string)$e['time']);
return ((int)($p[0] ?? 0)) * 60 + (int)($p[1] ?? 0);
}, $timed);
// 前后各留一小时:紧了会让人以为"到点就结束了",松了又浪费高度
$startH = max(0, intdiv(min($mins), 60) - 1);
$endH = min(24, intdiv(max($mins), 60) + 2); // +2 = 跨过那一小时再收尾
if ($endH <= $startH) { $endH = min(24, $startH + 2); }
}
$nowH = (int)date('G');
$nowPos = ($nowH - $startH) / max(1, ($endH - $startH)); // 红线位置(0~1)
$showNow = $nowPos >= 0 && $nowPos <= 1;
?>
<section class="section">
<div class="home-duo">
<?php /* ---- 左:今天的日程(日视图形式)---- */ ?>
<div class="card card-pad hd-pane" id="homeAgenda">
<div class="hd-pane-head">
<h2 class="cinema-h2">今天的日程</h2>
<a class="btn btn-ghost btn-sm" href="<?= e(url('calendar')) ?>">看整月</a>
</div>
<?php /* 🔴 v24 第 26 批 用户第 2 条(前半):「今天的日程,除了全天日程**一直显示在上面**外,
其他日程随着滚动」。
⇒ DOM 结构必须分成两层:
① `.ag-allday` **留在 pane 里、不在滚动容器内** —— 它永远钉在标题下方;
② `.hd-pane-body`(滚动容器)**只包** `.ag-grid` —— 只有时段网格随滚动。
为什么不能在同一个滚动容器里给全天区 `position: sticky`:
它在滚动区**内部**,sticky 只能粘在滚动区顶端,而滚动区顶端在标题下面 ——
滚起来时全天区会被"标题 + 滚动区"两层挤着,看起来仍然在动;
而且 sticky 需要不透明底色遮住底下滚过去的日程,与现在的浅色卡片叠色不好看。
直接把它移出滚动容器,是**零副作用**的做法。 */ ?>
<?php if (!$todayEvents): ?>
<div class="hd-pane-body">
<p class="empty-hint">今天没有安排。</p>
</div>
<?php else: ?>
<?php if ($allDay): ?>
<div class="ag-allday">
<?php foreach ($allDay as $ev): ?>
<a class="ag-allday-item" href="<?= e((string)$ev['href']) ?>">
<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 /* 当前时刻红线:用户原话「需要在当前的时间或者是时刻那里显示一条红线」 */ ?>
<?php if ($showNow): ?>
<div class="ag-now" style="top: <?= number_format($nowPos * 100, 2) ?>%">
<span class="ag-now-dot"></span>
</div>
<?php endif; ?>
<?php /* 日程块:按"相对第一个刻度的小时数"定位(与日视图同一套比例)---- */ ?>
<?php foreach ($timed as $ev): ?>
<?php
$p = explode(':', (string)$ev['time']);
$evMin = ((int)($p[0] ?? 0)) * 60 + (int)($p[1] ?? 0);
$topPct = (($evMin / 60) - $startH) / max(1, ($endH - $startH)) * 100;
$topPct = max(0, min(96, $topPct));
?>
<a class="ag-ev" href="<?= e((string)$ev['href']) ?>"
style="top: <?= number_format($topPct, 2) ?>%; border-left-color: <?= e((string)$ev['color']) ?>;">
<span class="ag-ev-t"><?= e((string)$ev['time']) ?></span>
<span class="ag-ev-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 /* /.ag-grid */ ?>
</div><?php /* /.hd-pane-body(滚动区,只含时段网格) */ ?>
<?php endif; ?>
</div><?php /* /.hd-pane */ ?>
<?php /* ---- 右:进行中的任务(标题 + 状态 + 看板名)---- */ ?>
<div class="card card-pad hd-pane" id="homeTasks">
<div class="hd-pane-head">
<h2 class="cinema-h2">手头在做</h2>
<a class="btn btn-ghost btn-sm" href="<?= e(url('tasks')) ?>">任务看板</a>
</div>
<div class="hd-pane-body">
<?php /* 用户第 6 条:「所有的任务,前面都带一个复选框,在任何地方,点这个复选框,
这个任务就完成了。」→ 首页这一块也算"任何地方",
所以每行前面也有复选框,点了就地完成(AJAX,不跳页)。
表单与 JS 在页面末尾各一份(见 home.php 的 taskDoneForm 段)。 */ ?>
<?php if (!$tasks): ?>
<p class="empty-hint">没有进行中的任务。</p>
<?php else: ?>
<ul class="dash-list tk-list">
<?php foreach ($tasks as $t): ?>
<?php $late = \App\Task::isOverdue($t['due_at'] ?? null); ?>
<li class="dash-row tk-row" data-task-row="<?= (int)$t['id'] ?>">
<?php
$tkId = (int)$t['id']; $tkStatus = (string)($t['status'] ?? 'todo');
$tkBack = 'home'; $tkStatic = !\App\Auth::canWrite();
require __DIR__ . '/_task_check.php';
?>
<span class="dash-dot" style="background: <?= e($late ? 'var(--red)' : 'var(--brand)') ?>;"></span>
<a class="dash-what" href="<?= e(url('tasks/show', ['id' => (int)$t['id']])) ?>">
<?= e((string)$t['title']) ?>
</a>
<?php /* 用户第 3 条:「除了显示任务的标题之外,还要显示他的状态。
比如说正在进行中待确认等等。」 */ ?>
<span class="kb-st is-<?= e((string)($t['status'] ?? 'todo')) ?> is-static">
<?= e(\App\Task::statusWord((string)($t['status'] ?? 'todo'))) ?>
</span>
<?php /* 用户第 3 条:「还要显示他所处的看板的名称,就是他现在被放在哪一个看板里面」 */ ?>
<?php if (trim((string)($t['board_name'] ?? '')) !== ''): ?>
<?php /* 🔴 v24 第 26 批 用户第 3 条(后半):「任务在设置看板的时候,可以选择颜色,
这样在首页列表中显示的时候,就可以更加直观的看到他属于哪个看板。」
⇒ 徽标**同时带颜色**:左边一个小圆点(看板色)+ 看板名。
为什么不用"整块背景染色":首页这一行已经有状态徽标、看板徽标、逾期等几个
色块,整块染色会互相打架、反而看不出哪个色对应哪个语义。
一个小圆点信息量足够、又不抢戏。 */
$bColor = \App\Task::boardColor($t['board_color'] ?? null, (string)$t['board_name']); ?>
<span class="tk-board" style="--bc: <?= e($bColor) ?>;">
<i class="tk-board-dot" aria-hidden="true"></i><?= e((string)$t['board_name']) ?>
</span>
<?php endif; ?>
<?php if ((int)($t['child_total'] ?? 0) > 0): ?>
<span class="dash-tag"><?= (int)$t['child_done'] ?>/<?= (int)$t['child_total'] ?></span>
<?php endif; ?>
<?php if (($t['due_at'] ?? '') !== ''): ?>
<span class="tk-due"><?= e(\App\Task::dueWord($t['due_at'] ?? null)) ?></span>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
</div>
</div>
</section>
<?php /* ==================== ③ 往年的今天 ==================== */ ?>
<section class="section">
<div class="page-head" style="margin-bottom:12px">
<?php /* v24 第 19 条:只认「往年的今天」,不再退回同月 —— 见 home_memories() 的说明。 */ ?>
<h1 style="font-size:15px">
往年的今天
<span class="sub"><?= e(date('n 月 j 日')) ?></span>
</h1>
</div>
<?php if (empty($memories['rows'])): ?>
<div class="card card-pad empty">
<b>往年的这一天还没有记录</b>
日子还长,等有了第一次,明年这时候就会自己冒出来。
</div>
<?php else: ?>
<div class="card card-pad">
<ul class="dash-list">
<?php foreach ($memories['rows'] as $mm): ?>
<li class="dash-row">
<span class="dash-dot" style="background: <?= $mm['kind'] === 'trip' ? '#3F7D5A' : '#7A4E8C' ?>;"></span>
<span class="dash-when"><?= (int)$mm['years_ago'] ?> 年前</span>
<a class="dash-what" href="<?= e((string)$mm['href']) ?>">
<?= $mm['kind'] === 'trip' ? '🧳 ' : '🎬 ' ?><?= e((string)$mm['title']) ?>
</a>
<span class="dash-tag"><?= e(substr((string)$mm['starts_at'], 0, 10)) ?></span>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
</section>
<?php /* ==================== 首页任务复选框的表单与脚本(用户第 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 /* ==================== 日程滚动区:进场定位到当前时刻(v24 第 26 批 用户第 2 条)======
用户原话:「每次进来,都要定位到当前时刻,就是红线出现的位置,让红线在中间。」
⇒ 打开首页时把**滚动容器**滚到一个位置,使"当前时刻"落在可视区**正中**。
⚠️ 触发条件只看**网格在不在**(`$timed` 非空才有网格),**不要**再加 `$showNow`。
第一版写了 `if (!empty($timed) && $showNow)`,后果是:
· 红线落在区间外(现在 23:40、日程只到 18 点 → 页面不画红线)时**整段脚本不渲染**
→ "每次进来都要定位"这条需求在最常见的场景(晚上开首页)直接失效;
· 更糟的是**判据依赖了被优化掉的东西**:探针那次库里没有分时段日程,
脚本根本没输出,页面看着"没报错",实测 `scrollTop=0`(本轮实测抓到的)。
⇒ 现在:**有网格就定位**。红线在就对齐红线;红线不在(在区间外)就用
`$nowPos` 同一个比例算出"此刻"在网格里的位置来对齐(数值同源,不会两套口径)。
⚠️ 用 `scrollTop = 目标偏移 - 容器高/2`,**不要**用 `scrollIntoView({block:'center'})`:
它会**连带滚动外层页面**(把整个浏览器窗口也滚下去),
用户一进首页就被拽到"页面中部" —— 那比不定位更糟。
直接给容器赋 `scrollTop` 只动这一个盒子,外层纹丝不动。
⚠️ 容器"没得滚"时(内容比容器矮)赋任何值都是 0,天然安全,不必特判。
⚠️ 目标位置**由服务端算好**传进来(`$nowPos`),不在 JS 里再算一遍时间 ——
两处各算一次迟早不一致(服务端用的是站点时区,JS 用的是浏览器时区)。 */ ?>
<?php if (!$todayEvents || empty($timed)): ?>
<?php /* 没有分时段日程 → 没有网格可滚,什么都不做 */ ?>
<?php else: ?>
<script>
(function () {
var box = document.querySelector('[data-ag-scroll]');
if (!box) { return; }
// 红线(在的话)优先;不在就用服务端算好的 nowPos 现算一个位置。
var line = box.querySelector('.ag-now');
var RATIO = <?= json_encode(round($nowPos, 6)) ?>; // 0~1(可能落在区间外)
function targetOffset() {
if (line) { return line.offsetTop; }
var grid = box.querySelector('.ag-grid');
if (!grid) { return 0; }
// ⚠️ 用**排掉全天区之后**的内容高度算:全天区现在不在滚动容器里,
// 所以 grid 的高度就是"整条时间轴"的高度(与服务端算 nowPos 的口径一致)。
return grid.offsetHeight * RATIO;
}
function centerOnNow() {
// offsetTop 相对**定位祖先**(.ag-grid,position:relative),即"内容坐标"
box.scrollTop = Math.max(0, targetOffset() - (box.clientHeight / 2));
}
centerOnNow();
/* ⚠️ 再补两拍:字体加载完 / 浏览器恢复滚动位置(back-forward cache)之后,
内容高度会变,第一次算出来的 scrollTop 可能偏。
用 rAF 而不是 setTimeout:rAF 在排版完成后跑,时序更准;
两拍是为了覆盖"布局在渲染后才最终确定"的那一帧。 */
if (window.requestAnimationFrame) {
requestAnimationFrame(function () { requestAnimationFrame(centerOnNow); });
}
})();
</script>
<?php endif; ?>