网页端跨天日程显示错误:周/日视图被拆成碎片,中间天错进「全天」行 #35

Closed
opened 2026-10-30 15:34:54 +00:00 by laoyang · 2 comments
Owner

现象

用户 2026-10-30:

网页端,跨天日程显示还是有问题…如果一个日程跨了三天,应该在日历本上怎么显示呢?
在周视图和日视图,应该怎么显示?现在显示的有问题。

实测复现(不是靠猜)

用真实数据跑 build_time_grid:一个「11/10 20:00 → 11/12 02:00」的跨三天日程,
服务端 Calendar::month() 展开成三条(第一天带 time、中间天两者皆空、末天带 end_time),
输入 build_time_grid 后得到:

=== 全天行 ===
  第1列 (2026-11-11): 跨天日程
  第2列 (2026-11-12): 跨天日程
=== 时间网格 ===
  第0列 (2026-11-10): top=83.33%  height=2.08%

即:

哪一天 实际被画成
11/10(开始日) 时间网格里一个小色块,高度只有 2.08%(≈30 分钟)
11/11、11/12 被塞进「全天」行,变成全天日程

三个问题叠加:

  1. 开始日长度错误:结束时间不在同一天 ⇒ $endMin 被判无效 ⇒ 退化成 30 分钟的最短块。
  2. 后两天错进全天行:build_time_grid 里"没有 time 又不是全天 ⇒ 按全天处理"这条兜底
    把跨天的延续日当成了全天日程。
  3. 彼此看不出是一条:三条独立、无关联,用户完全无法感知"这三天是同一个日程"。

根因

build_time_grid()(index.php:517)只做单天定位:

$ci = $dayIndex[(string)$e['date']] ?? null;
if ($ci === null) { continue; }     // 只认起始日那一列
...
if ($t === '' || !preg_match(...)) { $allDay[$ci][] = $e; continue; }  // 无时刻 ⇒ 全天

它不知道某条是"跨天日程的延续",也没有跨越天数的信息。
服务端展开时只给了 date/time/end_time,没有 是第几天/共几天。

方案(用户 2026-10-30 已确认三条)

  1. 周视图:跨列连续长条 —— 一个日程跨三天,就在三列里各画一段,
    视觉上连成一条(同 Google/Apple 日历的做法)。
  2. 日视图:铺满当天并标延续 —— 当天从 00:00 画到实际结束时刻,
    顶部标「← 接昨日」之类的提示。
  3. 中间天归时间网格 —— 不放进「全天」行;「全天」行只留给真正的全天日程。

实现要点

服务端(build_time_grid)

把同一日程的逐天条目合并回一条,并算清跨越了哪几列:

  • 合并键:ref_id + occ(重复日程的不同次发生不能混)
  • 产出每条带 cols => [起始列, 结束列],以及 seg(本段在该列的形态:
    起始/延续/结束/整天)
  • 首日起始时刻 = 第一条的 time;末段结束时刻 = 最后一条的 end_time
  • 中间列与末列在时间网格里占满 0-24 点(末列只到 end_time)
  • 弹层数据(data-fb-entry)仍取原条目,保证"点开能编辑"

模板(calendar/index.php)

  • 时间为绝对定位 + 百分比;跨列需要把色块从"单列内部"提升到跨列层,
    用 left = 列起点%、width = 跨列数 / 总列数 定位
  • 每列仍需保留 overflow: hidden?—— 跨列色块不放在列里,放在 .tg-body
    之上的一层,避免被裁切
  • 段内画法:起始段显示标题+时间;中间段只显示标题(延续感);末段显示到结束时刻

验收标准

  1. 跨三天的日程,周视图里三列连成一条,不再是三个互不相关的块
  2. 中间两天不再出现在「全天」行
  3. 开始日色块从 time 画到当日 24:00(不再退化成 30 分钟)
  4. 最后一天从 00:00 画到 end_time
  5. 日视图里当天铺满 0 点起,并有「延续」提示
  6. 真正的全天日程仍在天行,行为不变
  7. 不跨天的普通日程显示完全不变(回归)
  8. 时间冲突的分列(conflictLanes)对单天日程仍生效
  9. 点跨天色块能正常弹出详情浮层(含编辑入口)

为什么单独建单

网页端跨天日程在周/日视图的渲染问题(build_time_grid 合并与跨列定位);#34 是鸿蒙端日程编辑时间选择器参数写反,#32 是地址联想下拉,#33 是端上地址联想,都是不同端、不同文件。

## 现象 用户 2026-10-30: > 网页端,跨天日程显示还是有问题…如果一个日程跨了三天,应该在日历本上怎么显示呢? > 在周视图和日视图,应该怎么显示?现在显示的有问题。 ## 实测复现(不是靠猜) 用真实数据跑 `build_time_grid`:一个「11/10 20:00 → 11/12 02:00」的跨三天日程, 服务端 `Calendar::month()` 展开成三条(第一天带 `time`、中间天两者皆空、末天带 `end_time`), 输入 `build_time_grid` 后得到: ``` === 全天行 === 第1列 (2026-11-11): 跨天日程 第2列 (2026-11-12): 跨天日程 === 时间网格 === 第0列 (2026-11-10): top=83.33% height=2.08% ``` 即: | 哪一天 | 实际被画成 | |---|---| | 11/10(开始日) | 时间网格里一个小色块,**高度只有 2.08%(≈30 分钟)** | | 11/11、11/12 | **被塞进「全天」行**,变成全天日程 | 三个问题叠加: 1. **开始日长度错误**:结束时间不在同一天 ⇒ `$endMin` 被判无效 ⇒ 退化成 30 分钟的最短块。 2. **后两天错进全天行**:`build_time_grid` 里"没有 `time` 又不是全天 ⇒ 按全天处理"这条兜底 把跨天的**延续日**当成了全天日程。 3. **彼此看不出是一条**:三条独立、无关联,用户完全无法感知"这三天是同一个日程"。 ## 根因 `build_time_grid()`(`index.php:517`)只做单天定位: ```php $ci = $dayIndex[(string)$e['date']] ?? null; if ($ci === null) { continue; } // 只认起始日那一列 ... if ($t === '' || !preg_match(...)) { $allDay[$ci][] = $e; continue; } // 无时刻 ⇒ 全天 ``` 它**不知道**某条是"跨天日程的延续",也**没有跨越天数的信息**。 服务端展开时只给了 `date`/`time`/`end_time`,没有 `是第几天/共几天`。 ## 方案(用户 2026-10-30 已确认三条) 1. **周视图:跨列连续长条** —— 一个日程跨三天,就在三列里各画一段, 视觉上连成一条(同 Google/Apple 日历的做法)。 2. **日视图:铺满当天并标延续** —— 当天从 00:00 画到实际结束时刻, 顶部标「← 接昨日」之类的提示。 3. **中间天归时间网格** —— 不放进「全天」行;「全天」行只留给**真正的全天日程**。 ### 实现要点 **服务端(`build_time_grid`)** 把同一日程的逐天条目**合并回一条**,并算清跨越了哪几列: - 合并键:`ref_id` + `occ`(重复日程的不同次发生不能混) - 产出每条带 `cols => [起始列, 结束列]`,以及 `seg`(本段在该列的形态: 起始/延续/结束/整天) - 首日起始时刻 = 第一条的 `time`;末段结束时刻 = 最后一条的 `end_time` - 中间列与末列在时间网格里**占满 0-24 点**(末列只到 `end_time`) - 弹层数据(`data-fb-entry`)仍取**原条目**,保证"点开能编辑" **模板(`calendar/index.php`)** - 时间为绝对定位 + 百分比;跨列需要把色块从"单列内部"提升到**跨列层**, 用 `left = 列起点%`、`width = 跨列数 / 总列数` 定位 - 每列仍需保留 `overflow: hidden`?—— 跨列色块不放在列里,放在 `.tg-body` 之上的一层,避免被裁切 - 段内画法:起始段显示标题+时间;中间段只显示标题(延续感);末段显示到结束时刻 ## 验收标准 1. 跨三天的日程,周视图里三列**连成一条**,不再是三个互不相关的块 2. 中间两天**不再出现在「全天」行** 3. 开始日色块从 `time` 画到当日 24:00(不再退化成 30 分钟) 4. 最后一天从 00:00 画到 `end_time` 5. 日视图里当天铺满 0 点起,并有「延续」提示 6. **真正的全天日程**仍在天行,行为不变 7. **不跨天的普通日程**显示完全不变(回归) 8. 时间冲突的分列(`conflictLanes`)对单天日程仍生效 9. 点跨天色块能正常弹出详情浮层(含编辑入口) --- ### 为什么单独建单 网页端跨天日程在周/日视图的渲染问题(build_time_grid 合并与跨列定位);#34 是鸿蒙端日程编辑时间选择器参数写反,#32 是地址联想下拉,#33 是端上地址联想,都是不同端、不同文件。
Author
Owner

已修复并上线(commit 01d5bc2)

线上已部署,4 个文件 sha256 与仓库一致,站点无 PHP 错误
(/ 302、/api/v1/calendar/books 401,均符合预期)。

修复前实测到的样子

一个「11/10 20:00 → 11/12 02:00」的跨三天日程,喂给 build_time_grid 后:

全天行:11/11、11/12 各一条      ← 中间两天变成"全天日程"
时间网格:11/10 一个小块,高度仅 2.08%(≈30 分钟)

三个问题叠加:

  1. 开始日长度错:结束时刻不在同一天 ⇒ 判成"没有结束" ⇒ 退化成 30 分钟
  2. 后两天错进全天行:既无 time 又不是全天 ⇒ 被"按全天处理"的兜底收走
  3. 互不相干:三条独立,看不出是同一个日程

按你确认的三条方案实现

场景 现在的表现
周视图 三列各画一段,连成一条(缝隙 1.0px,只剩列边框)
日视图・起始天 20:00 → 24:00,底边贴列底
日视图・中间天 铺满整列(0 点→24 点),标题前加 ↳ 表示接续
日视图・结束天 00:00 → 02:00,顶边贴列顶
中间天归类 进时间网格,不再落到全天行
真全天日程 仍在全天行,行为不变

关键改动

app/Calendar.php —— 补 span_days(跨几天)/ seg(start/mid/end/only)。
同时修掉一个真 bug:原来 $first 是"窗口内遍历的第一个",
日视图只查一天时,中间天会变成 $first 被当成开始日处理。
⇒ 改用真实日期跟 $base/$base+span 比较,不看遍历顺序。

index.php::build_time_grid —— 先按 kind:ref_id:occ 归堆合并,
再按列切段。

归堆键必须带 occ:重复日程不同次发生的 ref_id 相同,
不带它会把 11/10 和 12/01 两次发生揉成一条(画成横跨三周的怪东西)。
也必须带 kind:观影/旅程的 ref_id 是各自表里的 id,
custom:5 和 movie:5 是两回事。

templates/calendar/index.php —— is-seg-* 类 + ↳ 延续标记;
跨天段用 left:0; width:100%,单天块保持原样。

assets/css/app.css —— 段与段之间:起始段下圆角归零 + 下边框透明,
中间段上下都平,结束段上圆角归零 + 上边框透明。

缝隙从 10.9px 降到 1.0px 的关键不只是圆角,
还有「跨天段要贴满整列」——单天块原本的 calc(2px + 1%) / calc(98% - 4px)
内缩在跨天段上会累积成明显的断口。

为什么不做跨列绝对定位层

.tg-body 左边有 56px 固定 gutter,窄屏还会横向滚动
(.cal-tg-card{overflow:auto} + 每列 min-width:92px),.tg-col 又是
overflow:hidden。跨列层要自己算 gutter 偏移、跟着横向滚动跑、还不能被裁,
每个都是新坑。在列内画段天然对齐,代价只是段间 1px 列边框缝,用 CSS 糊掉即可。

验证

逻辑层(PHP,跑真实 build_time_grid)

  • 周视图跨三天:3 段(start/mid/end),全天行为空 ✓
  • 日视图三个位置各自正确 ✓
  • 回归:单天 + 真全天正常;冲突分列仍生效;跨天与普通日程共存正常 ✓
  • 反向验证:拿修复前的代码跑同一判据正确转红
    (旧版 timed=1 all_day=2,高度 2.08%)✓

视觉层(Playwright + 真实 CSS + include 真实模板)32 项全过

  • 横向连续:start→mid 1.0px,mid→end 1.0px(判据 ≤2.5px)✓
  • 纵向:start 底边贴列底 0px;mid 顶 0 / 底 0;end 顶边贴列顶 0px 且只占开头 ✓
  • 样式:圆角与边框透明符合预期 ✓
  • 无裁切:三段都在列内 ✓
  • 回归:真全天仍在全天行;跨天不再出现在全天行;单天正常 ✓
  • 窄屏 375px:三段仍可见(w=91)✓

反向验证 4/4 全部抓到
故意做坏四个变体(段留单天内缩 / 保留圆角 / 中间段只画 30% / 下边框不透明),
判据全部转红 —— 说明"32 项全过"不是因为判据恒真。

已知取舍

跨天段占满整列,同一天的普通日程会与它在视觉上重叠。
这是日历的固有取舍(Google 日历同样如此)。

部署记录

  • 备份:/home/btops/deploy-backups/multiday-20261030-234323/(按目录结构保存 4 个文件)
  • 线上 app.css 已含 is-seg-start,ETag 已更新
  • 回滚方式:把备份目录下 4 个文件按原路径 cp -a 回去即可
## 已修复并上线(commit `01d5bc2`) 线上已部署,4 个文件 sha256 与仓库一致,站点无 PHP 错误 (`/` 302、`/api/v1/calendar/books` 401,均符合预期)。 ### 修复前实测到的样子 一个「11/10 20:00 → 11/12 02:00」的跨三天日程,喂给 `build_time_grid` 后: ``` 全天行:11/11、11/12 各一条 ← 中间两天变成"全天日程" 时间网格:11/10 一个小块,高度仅 2.08%(≈30 分钟) ``` 三个问题叠加: 1. **开始日长度错**:结束时刻不在同一天 ⇒ 判成"没有结束" ⇒ 退化成 30 分钟 2. **后两天错进全天行**:既无 `time` 又不是全天 ⇒ 被"按全天处理"的兜底收走 3. **互不相干**:三条独立,看不出是同一个日程 ### 按你确认的三条方案实现 | 场景 | 现在的表现 | |---|---| | 周视图 | 三列各画一段,**连成一条**(缝隙 1.0px,只剩列边框) | | 日视图・起始天 | 20:00 → 24:00,底边贴列底 | | 日视图・中间天 | **铺满整列**(0 点→24 点),标题前加 `↳` 表示接续 | | 日视图・结束天 | 00:00 → 02:00,顶边贴列顶 | | 中间天归类 | **进时间网格**,不再落到全天行 | | 真全天日程 | 仍在全天行,行为不变 | ### 关键改动 **`app/Calendar.php`** —— 补 `span_days`(跨几天)/ `seg`(start/mid/end/only)。 同时修掉一个真 bug:原来 `$first` 是"窗口内遍历的第一个", **日视图只查一天时,中间天会变成 `$first` 被当成开始日处理**。 ⇒ 改用真实日期跟 `$base`/`$base+span` 比较,不看遍历顺序。 **`index.php::build_time_grid`** —— 先按 `kind:ref_id:occ` 归堆合并, 再按列切段。 > 归堆键必须带 `occ`:重复日程不同次发生的 `ref_id` 相同, > 不带它会把 11/10 和 12/01 两次发生揉成一条(画成横跨三周的怪东西)。 > 也必须带 `kind`:观影/旅程的 `ref_id` 是各自表里的 id, > `custom:5` 和 `movie:5` 是两回事。 **`templates/calendar/index.php`** —— `is-seg-*` 类 + `↳` 延续标记; 跨天段用 `left:0; width:100%`,单天块保持原样。 **`assets/css/app.css`** —— 段与段之间:起始段下圆角归零 + 下边框透明, 中间段上下都平,结束段上圆角归零 + 上边框透明。 > 缝隙从 **10.9px 降到 1.0px** 的关键不只是圆角, > 还有「跨天段要贴满整列」——单天块原本的 `calc(2px + 1%)` / `calc(98% - 4px)` > 内缩在跨天段上会累积成明显的断口。 ### 为什么不做跨列绝对定位层 `.tg-body` 左边有 56px 固定 gutter,窄屏还会横向滚动 (`.cal-tg-card{overflow:auto}` + 每列 `min-width:92px`),`.tg-col` 又是 `overflow:hidden`。跨列层要自己算 gutter 偏移、跟着横向滚动跑、还不能被裁, 每个都是新坑。在列内画段天然对齐,代价只是段间 1px 列边框缝,用 CSS 糊掉即可。 ### 验证 **逻辑层**(PHP,跑真实 `build_time_grid`) - 周视图跨三天:3 段(start/mid/end),全天行为空 ✓ - 日视图三个位置各自正确 ✓ - 回归:单天 + 真全天正常;冲突分列仍生效;跨天与普通日程共存正常 ✓ - **反向验证**:拿修复前的代码跑同一判据**正确转红** (旧版 `timed=1 all_day=2`,高度 2.08%)✓ **视觉层**(Playwright + 真实 CSS + include 真实模板)**32 项全过** - 横向连续:start→mid 1.0px,mid→end 1.0px(判据 ≤2.5px)✓ - 纵向:start 底边贴列底 0px;mid 顶 0 / 底 0;end 顶边贴列顶 0px 且只占开头 ✓ - 样式:圆角与边框透明符合预期 ✓ - 无裁切:三段都在列内 ✓ - 回归:真全天仍在全天行;跨天不再出现在全天行;单天正常 ✓ - 窄屏 375px:三段仍可见(w=91)✓ **反向验证 4/4 全部抓到** 故意做坏四个变体(段留单天内缩 / 保留圆角 / 中间段只画 30% / 下边框不透明), 判据全部转红 —— 说明"32 项全过"不是因为判据恒真。 ### 已知取舍 跨天段占满整列,同一天的普通日程会与它在视觉上重叠。 这是日历的固有取舍(Google 日历同样如此)。 ### 部署记录 - 备份:`/home/btops/deploy-backups/multiday-20261030-234323/`(按目录结构保存 4 个文件) - 线上 `app.css` 已含 `is-seg-start`,ETag 已更新 - 回滚方式:把备份目录下 4 个文件按原路径 `cp -a` 回去即可
Author
Owner

用户验收通过,关单(2026-11-01)

用户 2026-11-01 明确回复「#35,可以关单了」。

交付回顾

commit 01d5bc2,已部署 family.yangyq.net 线上。

  • 周视图:跨天日程按列分段(start/mid/end),三列连成一条,段间缝隙 1.0px(原 10.9px)
  • 日视图:起始天 20:00→24:00、中间天铺满整列带 ↳ 标记、结束天 00:00→02:00
  • 中间天归类修正:进时间网格,不再落到「全天」行
  • 回归:真全天日程仍在全天行;单天日程不变;冲突分列仍生效

关键改动 4 个文件:app/Calendar.php(补 span_days/seg,并修掉日视图 $first 判据 bug)、
index.php::build_time_grid(按 kind:ref_id:occ 归堆后按列切段)、
templates/calendar/index.php(is-seg-* 类 + 延续标记)、assets/css/app.css(分段圆角/边框)。

验证:PHP 逻辑层 + Playwright 视觉层 32 项全过 + 反向验证 4/4 抓到。

已知取舍(用户已知悉)

跨天段占满整列,同一天的普通日程会与它在视觉上重叠 —— 与 Google 日历同样的固有取舍。

## 用户验收通过,关单(2026-11-01) 用户 2026-11-01 明确回复「#35,可以关单了」。 ### 交付回顾 commit `01d5bc2`,已部署 family.yangyq.net 线上。 - 周视图:跨天日程按列分段(start/mid/end),三列连成一条,段间缝隙 1.0px(原 10.9px) - 日视图:起始天 20:00→24:00、中间天铺满整列带 `↳` 标记、结束天 00:00→02:00 - 中间天归类修正:进时间网格,不再落到「全天」行 - 回归:真全天日程仍在全天行;单天日程不变;冲突分列仍生效 关键改动 4 个文件:`app/Calendar.php`(补 span_days/seg,并修掉日视图 $first 判据 bug)、 `index.php::build_time_grid`(按 kind:ref_id:occ 归堆后按列切段)、 `templates/calendar/index.php`(is-seg-* 类 + 延续标记)、`assets/css/app.css`(分段圆角/边框)。 验证:PHP 逻辑层 + Playwright 视觉层 32 项全过 + 反向验证 4/4 抓到。 ### 已知取舍(用户已知悉) 跨天段占满整列,同一天的普通日程会与它在视觉上重叠 —— 与 Google 日历同样的固有取舍。
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: laoyang/FamilyBook#35