用户 2026-10-16 五条。
① 纪念日要记录年份,才能知道「第几个」
用户原话:「纪念日需要记录年份,这样才能知道**是第几个纪念日**。」
**真根因有两层**(两处一起才构成"没有年份"):
· 表里没有:anniversaries 只有 month + day(当年当成"一年一度,年份无意义");
· 表单有、但被丢掉:日期控件本来就带年份(<input type="date"> 是 YYYY-MM-DD),
而拆分 JS **只取 mm/dd**(`mo.value = m[2]; dy.value = m[3];`)⇒ 想存也没存上。
修法:anniversaries.year(SMALLINT UNSIGNED NULL)+ 升级路径 addColumn;
表单补 hidden year、拆分 JS 一并写;Anniversary::save 收 year(**区间守卫 1900..2100**);
occurrences() 输出 year 与 nth;列表页显示「2018 年起 · 第 9 个」。
⚠️ 年份**可空、且不给默认值**:老数据就是没有年份,猜一个(按创建年之类)
会让页面算出一个**错的**"第 N 个"。NULL = 只按月日过、不显示序号 —— 诚实。
⚠️ 顺带修一个自相矛盾:repeat_type='once' 原来落在公历分支 ⇒ **年年都冒出来**;
现在有年份了 ⇒ 只在那一年出现(没年份的保持老行为,不凭空丢)。
🔴 **序号算法抽成单点来源 `Anniversary::nthFor()`** —— `occurrences()`(临近区)与
days/index.php(我添加的那张表)**都调它**;两处各写一遍是"改一处忘一处"的温床。
用**今天所在的年**("今年是第几个"),不是"下一次发生那年"。
🔴🔴 **踩坑(最值钱)**:`addColumn`(ALTER TABLE)**必须排在那张表 CREATE TABLE 之后**。
第一版放进 movie_details 那段(anniversaries 的建表更靠后)⇒ 全新安装
`1146 Table 'fb_test.anniversaries' doesn't exist` ⇒ 整段 ensure() 抛异常 ⇒
**install 直接"安装失败"**(POST 回 200、不自删、schema_version=None、库一片空白)。
新建库走 CREATE TABLE、老库走 addColumn —— **两条路都要在**。
② 首页近期任务:把待办也放进来(排在进行中下面)
用户原话:「首页的近期任务,**把待办也放进来吧,放在进行中的下面**。」
原来只收「进行中 + **已逾期**的待办」(当年怕待办一多首页变长清单)。
⇒ 现在**全部**收进来、按**状态分组**(doing 在前、todo 在后);
两组内部仍按截止时间升序(byStatus 的 ORDER BY 已排好)。
⚠️ 配套改统计:`count($tasks) 件在做` 会把待办也算成"在做" ⇒ 改成**按状态拆开数**
(进行中→「件在做」、待办→「件待办」,两数相加=列表行数,仍同源)。
「件待办」原读 stat7['todos'](**与列表不同源**,v24 踩过"数字和列表对不上")。
⚠️ 空态:「没有进行中的任务,也没有待办。」
⚠️ .hd-pane-body 本来就是 overflow-y:auto + flex:1 1 auto/min-height:0,行数变多**内部滚动**、
卡片高度不变(v32"两块外框恒等高"那套兜住了)。
③ 日程编辑浮层「只有左侧有内容,右侧空白,好像分栏了」
**真根因**(assets/css/app.css):日程编辑页 event.php 里,`.mv-grid`(观影式**纵向**列表)
被嵌在**两列**的 `.form-grid` 里 ⇒ 它只占**第一列**。整页形态下看不出来(页面宽),
但**浮层里面板只有 620px** ⇒ 两列各 286px ⇒ 所有字段挤在左边 286px、右边整列空着。
实测:panel=620 / form-grid=588 / cols="286px 286px" / mv-grid w=286
→修后 mv-grid w=588(95% of panel)、标题输入框 w=516、距右缘 ~2px。
修法:`.form-grid > .mv-grid { grid-column: 1 / -1; }` —— **一行 CSS**。
⚠️ 不去改 event.php 里"整页保留两列、浮层用 mv-grid"那个结构判断(那是有意保留的);
`.mv-grid` 本身就是"一整块、占满宽度"的语义,天生不该被塞进半列。
⚠️ 排查手法:这种"看起来像分栏"的问题**量几何比读 CSS 快** ——
getBoundingClientRect 打出 panel/grid/cols/内层宽,一眼看出"嵌了几层 grid"。
④ 添加成员时是**打标签**,不是选分组
用户原话:「名册中添加成员时,**不是选择分组,而是打标签**。现在已经没有分组的概念了,
**只有标签的概念**。」
⇒ 新增分支的「所属分组」下拉(group_id)换成**标签输入**(复用 v51 的 #ptBlock 那套:
候选勾选 + 自由输入,**文本框是唯一提交源**)。
⚠️ 只在**新增**时出现在那个位置(编辑时上面那个 #ptBlock 已在管标签,出现两块会让人以为填两遍)。
⚠️ **分组表结构 person_roster.group_id 保留不动**(名册"拖动归档"还在用、历史数据也在),
只是**新增入口不再要求人选**。
⚠️ e2e 里 v14「新增表单**有**分组下拉」与新需求正相反 ⇒ 改成「没有 group_id + 有 name=tags」。
⑤ 添加成员页一次填完「我对他的称呼 + 我的备注」
用户原话:「我在添加一个成员的时候,是不是可以直接在添加成员的页面里填写**我对他的称呼**
以及**我对他的备注**,**一次就解决**?要不然还得先添加、再返回他的名册去填,有点麻烦。」
⇒ 新增页也出现这两栏:
· **我对他的备注**(person_notes)—— 条件从 `$isEdit && !$isSelf` 改成 `!$isSelf`;
· **我对他的称呼**(nicknames)—— **新加的输入框** `name="my_nick"`。
服务端 people/create 落两表(挂**我**名下:viewer=我, target=他)。
⚠️ 判"表单里有没有这一栏"用 **array_key_exists**(不是判值是否为空)——
与编辑路径同一口径:**空串是明确的"清空"语义**(Nickname::set('') 会删行)。
⚠️ 四处**重渲染路径**(撞名确认/校验失败/手机号重复/people/new 初始)都要带上
myTags/tagChoices/myNick,否则一撞名或一报错,刚打的标签与刚写的称呼备注就全没了。
⚠️ 编辑页也补 myNick 回显(否则"新增时填了、编辑页看不见"会以为没存上)。
验证(新增 2 套件 + 更新 e2e/deploy 断言):
· _fb_v53_ui.py(**26 条**真机断言)
· _b53_falsify.py(**10 注入 / 10 全抓**,基线 26/0)
· e2e **1517/0**;familybook_deploy.sh 加 v53 静态断言段(14 正向 + 1 反向)
· 全流水线 **PUSH_RC=0**、部署 0 FAIL(含"第53批形态")
· 线上 8 文件 md5 逐字节一致;`anniversaries.year` 已由升级路径补上(nullable smallint);
线上实测 nthFor 4 用例全对(2018→第9个 / null→NULL / 2019→2 / 未来年→NULL);
诊断页已删并复验 404
⚠️ 本轮踩坑(已写进 pitfalls.md / 工作日志):
· **`addColumn` 必须在同表的 CREATE TABLE 之后**(否则全新安装 `1146` ⇒ ensure() 抛异常);
· **探针里 `dispatchEvent(new Event('submit'))` 不会真的提交表单**
(不导航、不发请求)⇒ 判"存没存上"必须真点提交按钮或 `requestSubmit()`;
· **反向验证的 `restore()` 不能只是"再跑一遍 build_work()"**(指纹闸门会把它变空操作
⇒ 第二轮基线是脏的,26/0 掉到 14/12);恢复必须**无条件** copy2;
· **注入锚点在源码里出现两次 ⇒ 打到别处 ⇒ 那条断言恒不红**
(v53 给新增页也加了 `id="ptBlock"`,打偏了 v51 那条"编辑页标签块"的注入);
· **别把两个共用 fb_test 库的套件并行跑**(互相删播种行 ⇒ 看着像"整套功能坏了");
· **部署静态断言的锚点引号/反斜杠必须与源码一字不差**:`\[` 在 bash 双引号里是
**字面反斜杠**、单双引号不能混、漏个 `(int)` 也搜不到 —— 一轮里连红 4 条全是这个。
716 lines
44 KiB
PHP
716 lines
44 KiB
PHP
<?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。
|
||
两个口径不同源,数字和列表当场对不上。
|
||
|
||
⇒ 现在这个数字与右侧列表**同源**(与 index.php 传给本页的**同一个数组**)。
|
||
|
||
🔴 v53(用户 2026-10-16 第 2 条):右侧列表现在**也含待办**了(放在进行中下面)
|
||
⇒ 「件在做」不能再数整个数组(那会把待办也算成"在做")。
|
||
改成**按状态拆开数**:进行中 →「件在做」,待办 →「件待办」。
|
||
⚠️ 两个数字相加 = 右侧列表的行数(列表也是这两段拼起来的),仍然同源。
|
||
|
||
⚠️ 「已经动手」与「排队等着」是两件事(用户第 6 条要求说清区别):
|
||
首页要回答的是"我手头在做什么",所以主数字用「在做」的口径;
|
||
排队的那些(todo)另给一个小字,不必让人点进任务页才知道还有多少。 */ ?>
|
||
<?php $nDoing = 0; $nTodo = 0;
|
||
foreach (($tasks ?? []) as $__t) {
|
||
if ((string)($__t['status'] ?? 'todo') === 'doing') { $nDoing++; } else { $nTodo++; }
|
||
} ?>
|
||
<a class="hbs" href="<?= e(url('tasks')) ?>"
|
||
title="近期任务(进行中)">
|
||
<b><?= $nDoing ?></b> 件在做
|
||
</a>
|
||
<?php if ($nTodo > 0): ?>
|
||
<a class="hbs" href="<?= e(url('tasks')) ?>" title="记下来了、还没动手的那些">
|
||
<b><?= $nTodo ?></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 = 跨过那一小时再收尾
|
||
/* 🔴 v28 用户第 1 条:「今天的日程,并没有定位到红线所在的位置」。
|
||
根因:**区间里没有"现在"**,于是红线不画、定位算出来是个越界的值。
|
||
(实测:现在 22:44、日程 09:00 与 14:00 → 区间掐成 08–17,
|
||
`$nowPos = (22-8)/9 = 1.56` → `$showNow=false` → `class="ag-now"` 整个不渲染;
|
||
而定位脚本用同一个越界的 RATIO 去乘网格高度,被 clamp 到最底 → 停在底部。)
|
||
⇒ 修法不是"没红线就别定位",而是**让区间永远包含当前时刻**:
|
||
用户要的正是"一眼看到现在处在哪儿",那就必须把"现在"纳入刻度。
|
||
下面的 `$nowH` 是当前小时(服务端时区,与页面口径一致),
|
||
把它并进区间端点 —— 于是红线必然落在区间内,定位也就有了正确的目标。
|
||
⚠️ 必须在**算 $nowH 之前**完成这一步(原代码把 $nowH 放在区间之后算,
|
||
这里把它提上来,避免"改了区间却没重新算位置"这种漏改)。
|
||
🔴🔴 v44(用户 2026-10-12 第 3 条):「首页上的红线刻度显示不准确。
|
||
现在明明已经 7:44 了,但它那个红线还停留在最早的 7 点钟,
|
||
并没有走到 7:44 那个位置。这个只有在首页上是这样的。我点到日程里面,
|
||
那个红线的刻度好像是对的。」
|
||
⇒ 真根因:首页用的是 `date('G')`(**只取整小时**),而日程页用的是
|
||
`(int)date('G')*60 + (int)date('i')`(**精确到分**)。
|
||
7:44 时首页算出 7 → 红线停在 7:00,正是用户看到的。
|
||
⇒ 这里改成与日程页**同一套**(小时*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); }
|
||
}
|
||
/* 红线位置:**分钟级**(与日程页同一算法)。见上面 v44 那条 —— 原来是 `date('G')`
|
||
(整小时),所以"7:44 的红线停在 7:00"。 */
|
||
$nowMinutes = ((int)date('G')) * 60 + (int)date('i');
|
||
$nowPos = (($nowMinutes / 60) - $startH) / max(1, ($endH - $startH)); // 红线位置(0~1)
|
||
$showNow = $nowPos >= 0 && $nowPos <= 1;
|
||
|
||
/* 🔴 v30 第 2/4 条:首页日程也用**和日程模块同一套冲突分列**。
|
||
⇒ 复用 App\Calendar::conflictLanes(算法借鉴 SyncCalendar 的 TimelineUtil):
|
||
两条撞车就左右各半、三条就三分 —— 首页与日历页看到的是同一个布局。
|
||
⚠️ 必须**先按时间排序**再分列(lanes 是按传入顺序返回的):
|
||
`$todayEvents` 从路由出来已是"全天在前、其余按时刻"升序,这里再稳一次。
|
||
⚠️ 入参只挑**有时间**的那些(全天日程不进时间网格)。 */
|
||
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);
|
||
?>
|
||
<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>
|
||
<?php /* 🔴 v28 用户第 2 条:「看正月这个按钮,改成:更多日程,
|
||
背景颜色和日程的主题颜色保持一致。」
|
||
(「看正月」是用户对「看整月」的口误。) */ ?>
|
||
<a class="btn btn-sm hd-more" href="<?= e(url('calendar')) ?>"
|
||
style="--hd-c: <?= e(\App\Module::color('calendar', (int)\App\Auth::familyId())) ?>;">更多日程</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): ?>
|
||
<?php /* 🔴 v43(用户第 6 条):点条目弹详情浮层(不直接跳页)。 */ ?>
|
||
<?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 /* 当前时刻红线:用户原话「需要在当前的时间或者是时刻那里显示一条红线」 */ ?>
|
||
<?php /* 🔴 v44(用户 2026-10-12 第 3 条):「红线左侧是有一个红点吗?
|
||
这个红点显示也不全,也不太好看。直接把红点去掉就行了,
|
||
直接一根红线就可以了。」
|
||
⇒ 那颗 `.ag-now-dot` 整个删掉(原来它是个 8×8 的圆,左端 `left:-4px`,
|
||
靠外溢一点点来"指示起点";用户觉得显示不全又不好看)。
|
||
⚠️ CSS 里那条 `.ag-now-dot` 也一并删(留在那儿是死规则,
|
||
还会让"红线只有一根"这件事在样式表里看不出结论)。 */ ?>
|
||
<?php if ($showNow): ?>
|
||
<?php /* 🔴 v45:把**服务端算红线用的那个分钟数**也写到元素上(`data-now-min`)。
|
||
为什么:探针原来只能"拿服务端渲染出的 top% 去和浏览器此刻的时间比" ——
|
||
两者的容差被 0.3% 一道卡着,而"整小时 vs 分钟"的差只等于
|
||
`分钟/60/跨度`:当分钟数很小时(如第 2 分钟)占比不到 0.3%,
|
||
**注入旧行为也不会变红** ⇒ 反向验证当场判它"恒真摆设"。
|
||
写上 data-now-min 之后,探针可以做一个**零容差**的判据:
|
||
"服务端用的分钟数 == 浏览器此刻的分钟数",整小时注入必然对不上。
|
||
⚠️ 它只是给测试用的观测点,不影响任何渲染/行为。 */ ?>
|
||
<div class="ag-now" style="top: <?= number_format($nowPos * 100, 2) ?>%"
|
||
data-now-min="<?= (int)$nowMinutes ?>"></div>
|
||
<?php endif; ?>
|
||
<?php /* 日程块:按"相对第一个刻度的小时数"定位(与日视图同一套比例)---- */ ?>
|
||
<?php
|
||
/* 🔴 v30 第 2 条:「首页上的今天的日程,用色块表示,和日程保持一致。」
|
||
用户要的是**和日程模块(周/日视图)一样的实心色块**,而不是原来的
|
||
"白底 + 左边一条色线" —— 后者色只占 3px,一眼看不出是哪个日历本。
|
||
|
||
⇒ 三处对齐日程模块的 `.tg-block`:
|
||
① 底色 = 该日程所属日历本的颜色(不是主题色);
|
||
② 白字(`.tg-block` 就是 `color:#fff`);
|
||
③ 冲突错开(left/width 由 Calendar::conflictLanes 算,见 index.php)。
|
||
⚠️ 深浅:`--tg-*` 那几个本子的颜色都是中低明度,白字压得住;
|
||
但用户可能自定义成很浅的色 —— 所以标题那一行加 `text-shadow`
|
||
兜底(比"再加一层半透明黑罩"更不破坏本色)。
|
||
⚠️ 高度用 `min-height` 而不是固定值:15 分钟的日程也要看得见
|
||
(与 .tg-block 的 15 分钟最短块口径一致)。 */
|
||
foreach ($timed as $ev):
|
||
$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));
|
||
// 结束时刻:缺省时按"下一件事的起点"或 30 分钟(与日历口径一致)
|
||
$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); // 至少能塞下一行
|
||
?>
|
||
<?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-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) ?>%;
|
||
<?php /* 🔴 v37 用户第 3 条:「首页上今天的日程…如果它分了三列
|
||
或者是几列,这三列之间的距离或者是空隙非常非常的大,
|
||
是分开的,中间有非常大的空隙。我看了一下日程里面的那个
|
||
显示,那两个色块就是紧挨着的。所以…应该让它们紧挨着。」
|
||
|
||
根因(与日历页 .tg-block 逐字对比出来的):
|
||
· 日历页写的是 `left: calc(2px + L%)` / `width: calc(W% - 4px)`
|
||
—— 只留 2px+2px 的缝,所以色块**紧挨**;
|
||
· 这里却写死了 `calc(46px + L%)` / `calc(W% - 50px)`
|
||
—— 每块左右各扣 23px/25px。两列时那 46px 的扣减
|
||
**没法均分**,全挤进了两块之间的缝里 →
|
||
实测缝 ≈ 50px(等于把 46px 全加在了中间),
|
||
日历页只有 ≈ 4px。
|
||
|
||
⇒ 改法与日历页对齐:
|
||
left = 46px(给小时刻度)+ 列内的 2px
|
||
width = 列宽 - 4px(左右各 2px)
|
||
这样列间缝恒为 4px(紧挨),而且与"放得下多少列、
|
||
每列多宽"的解耦是干净的 —— 列宽由 width% 决定,
|
||
缝由那 4px 决定,两者不再互相污染。 */ ?>
|
||
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><?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>
|
||
<?php /* 🔴 v28 用户第 3 条:「『手头在做』改成『近期任务』,『任务看板』这个按钮
|
||
改成『全部任务』,按钮颜色和任务的主题颜色保持一致。」 */ ?>
|
||
<a class="btn btn-sm hd-more" href="<?= e(url('tasks')) ?>"
|
||
style="--hd-c: <?= e(\App\Module::color('tasks', (int)\App\Auth::familyId())) ?>;">全部任务</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); ?>
|
||
<?php
|
||
/* 🔴 v32 第 4 条(用户 2026-10-08):「首页,近期任务,任务的文字和前面的圆点,
|
||
颜色并没有跟**看板颜色**一致,这个需要修正。」
|
||
|
||
⚠️ v30 那轮我把它改成了 `--tk-c`(= **任务模块**的主题色),
|
||
当时理解成"用户说的主题色"。**理解错了** —— 用户要的是
|
||
「这条任务属于哪个**看板**」的颜色(任务模块自己早就支持
|
||
"给看板选颜色",用户原话在第 26 批:「任务在设置看板的时候,
|
||
可以选择颜色,这样在首页列表中显示的时候,就可以更加直观的
|
||
看到他属于哪个看板。」)。
|
||
⇒ 用 `Task::boardColorById(家庭, 看板id)`:
|
||
看板没设色时它按**看板名派生**一个稳定色(同名永远同色),
|
||
所以这里不需要任何回退分支。
|
||
⚠️ 逾期仍用红色(状态语义,不能被看板色盖掉)——
|
||
但那是"整行变红",与"没逾期时跟看板一致"并不冲突。
|
||
⚠️ 没归看板的(board_id 为空)走"默认看板"的派生色,
|
||
与任务模块列表页看到的完全一致(同一函数)。 */
|
||
$tkColor = \App\Task::boardColorById((int)\App\Auth::familyId(),
|
||
(int)($t['board_id'] ?? 0) ?: null);
|
||
?>
|
||
<li class="dash-row tk-row" data-task-row="<?= (int)$t['id'] ?>"
|
||
style="--tk-c: <?= e($tkColor) ?>;">
|
||
<?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(--tk-c)') ?>;"></span>
|
||
<?php /* 🔴 v43(用户第 6 条):点任务标题也弹详情浮层(不直接跳页)。
|
||
要点在**标题那个 <a>** 上而不是整行 ——
|
||
行里还有复选框(勾一下完成)与其它链接,
|
||
整行可点会让"勾选"和"打开"互相抢。 */ ?>
|
||
<a class="dash-what" href="<?= e(url('tasks/show', ['id' => (int)$t['id']])) ?>"
|
||
data-fb-entry='<?= fb_entry_json([
|
||
'title' => (string)$t['title'],
|
||
'status_word' => \App\Task::statusWord((string)($t['status'] ?? 'todo')),
|
||
'note' => (string)($t['note'] ?? ''),
|
||
], url('tasks/show', ['id' => (int)$t['id']]),
|
||
\App\Auth::canWrite() ? url('tasks/edit', ['id' => (int)$t['id']]) : '',
|
||
[['截止', ($t['due_at'] ?? '') !== '' ? substr((string)$t['due_at'], 0, 16) : ''],
|
||
['看板', trim((string)($t['board_name'] ?? ''))]]) ?>'
|
||
style="color: <?= e($late ? 'var(--red)' : $tkColor) ?>;">
|
||
<?= 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 /* ==================== 通知消息(v30 第 5 条)====================
|
||
用户原话:「…发一个通知,通知出现在首页中,**往年今天的前面**。」
|
||
⇒ 位置是硬性要求:必须在「往年的今天」**之前**。
|
||
⚠️ 没有通知时**整块不渲染** —— 首页是"每天第一眼看的地方",
|
||
不要用一个空盒子占掉一屏(与"今天没有安排"那种信息位不同:
|
||
通知本来就是事件驱动的,没有就真的不该出现)。 */ ?>
|
||
<?php if (!empty($notifs)): ?>
|
||
<section class="section">
|
||
<div class="page-head" style="margin-bottom:12px">
|
||
<h1 style="font-size:15px">
|
||
通知消息
|
||
<?php if ($notifUnread > 0): ?><span class="sub"><?= (int)$notifUnread ?> 条未读</span><?php endif; ?>
|
||
</h1>
|
||
<div class="actions">
|
||
<a class="btn btn-ghost btn-sm" href="<?= e(url('notifications')) ?>">全部通知</a>
|
||
</div>
|
||
</div>
|
||
<div class="card card-pad">
|
||
<ul class="ntf-list ntf-list-home">
|
||
<?php foreach ($notifs as $n): ?>
|
||
<?php
|
||
$actorName = \App\Nickname::display(
|
||
(int)$meId,
|
||
['name' => $n['actor_name'] ?? '', 'my_nickname' => $n['actor_nick'] ?? '']
|
||
);
|
||
if (trim((string)$actorName) === '') { $actorName = '有人'; }
|
||
?>
|
||
<li class="ntf-row ntf-row-<?= e((string)($n['kind'] ?? '')) ?><?= empty($n['is_read']) ? ' is-unread' : '' ?>">
|
||
<span class="ntf-kind" aria-hidden="true"><?= \App\Notification::icon((string)($n['kind'] ?? '')) ?></span>
|
||
<img class="ntf-av" src="<?= e(avatar_url($n['actor_avatar'] ?? null)) ?>" alt="">
|
||
<a class="ntf-main" href="<?= e(\App\Notification::href($n)) ?>">
|
||
<span class="ntf-title">
|
||
<b><?= e((string)$actorName) ?></b><?= e(\App\Notification::bodyWithoutActor((string)$n['title'])) ?>
|
||
</span>
|
||
<span class="ntf-time"><?= e(fb_time_ago((string)$n['created_at'])) ?></span>
|
||
</a>
|
||
</li>
|
||
<?php endforeach; ?>
|
||
</ul>
|
||
</div>
|
||
</section>
|
||
<?php endif; ?>
|
||
|
||
<?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 条)======
|
||
用户原话:「每次进来,都要定位到当前时刻,就是红线出现的位置,让红线在中间。」
|
||
⇒ 打开首页时把**滚动容器**滚到一个位置,使"当前时刻"落在可视区**正中**。
|
||
|
||
⚠️ 触发条件:**今天有日程就定位**(只要 `$todayEvents` 非空,页面上就有
|
||
`.ag-grid` 与红线)。曾经写成 `if (!$todayEvents || empty($timed))`,
|
||
后果是**今天只有全天日程 / 全天日程都没有分时段的那种日子,脚本整段不输出**
|
||
—— 而网格(0–24h)和红线照样渲染,于是"红线在,页面却停在最上面"
|
||
(v29 用户第 1 条仍然报「没有定位到红线」,实测 `scrollTop=0`、脚本不在页面上,
|
||
就是这个条件卡掉的;有分时段日程时定位其实完全正确、偏差 0px)。
|
||
⇒ 只判"有没有网格可滚",别判"是不是分时段日程"。
|
||
|
||
⚠️ 用 `scrollTop = 目标偏移 - 容器高/2`,**不要**用 `scrollIntoView({block:'center'})`:
|
||
它会**连带滚动外层页面**(把整个浏览器窗口也滚下去),
|
||
用户一进首页就被拽到"页面中部" —— 那比不定位更糟。
|
||
直接给容器赋 `scrollTop` 只动这一个盒子,外层纹丝不动。
|
||
⚠️ 容器"没得滚"时(内容比容器矮)赋任何值都是 0,天然安全,不必特判。
|
||
⚠️ 目标位置**由服务端算好**传进来(`$nowPos`),不在 JS 里再算一遍时间 ——
|
||
两处各算一次迟早不一致(服务端用的是站点时区,JS 用的是浏览器时区)。 */ ?>
|
||
<?php if (!$todayEvents): ?>
|
||
<?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; ?>
|