跨天日程:周/日视图按列分段画成一条,不再拆成碎片 laoyang/FamilyBook#35

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

## 修复前是什么样(实测,不是推断)

一个「11/10 20:00 → 11/12 02:00」的跨三天日程,服务端 month() 展开成三条
(首日带 time、中间天两者皆空、末日带 end_time),喂给 build_time_grid 后:

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

三个问题叠加:
  · 开始日的结束时刻不在同一天 ⇒ 判成"没有结束" ⇒ 退化成 30 分钟
  · 中间/末日既无 time 又不是全天 ⇒ 被"按全天处理"的兜底塞进全天行
  · 三条互不相干,用户看不出"这是同一个日程"

## 方案(用户已确认)

  1. 周视图:跨列连续长条 —— 毎列各画一段,视觉连成一条
  2. 日视图:铺满当天并标延续
  3. 中间天归时间网格,不进全天行

## 改了什么

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

**index.php::build_time_grid** —— 核心重构
  · 先按 `kind:ref_id:occ` 归堆,把同一日程的逐天条目合成一条
    (带 occ 是因为重复日程不同次发生的 ref_id 相同;带 kind 是因为
     观影/旅程的 ref_id 是各自表里的 id,`custom:5` 和 `movie:5` 是两回事)
  · 跨度以服务端 `span_days` 为准,不再用"窗口内铺了几列"推断
    (日视图下那个推断必然不成立)
  · 跨天条目拆成每列一段,按 `seg` 决定纵坐标:
    start = 开始时刻→24:00,mid = 整列,end = 0→结束时刻
  · 跨天条目**不参与 conflictLanes** —— 它必然占满整列,
    拿去分列会把同一天所有日程都挤窄

**templates/calendar/index.php** —— 分段渲染
  · `is-seg-start/mid/end` 类;延续段前面加 `↳` 标记,不重复显示时间
  · 跨天段用 `left:0; width:100%`,单天块保持原来的 `calc(2px + N%)`

**assets/css/app.css** —— 让三段看着连成一条
  · start 下圆角归零、下边框透明;mid 上下都平;end 上圆角归零、上边框透明
  · 段与段之间原本有 1px 白边 + 左右内缩,实测缝隙 10.9px,看着像断成三段
    ⇒ 跨天段贴满整列后降到 1.0px(只剩列边框)

## 为什么在列内画段,不做跨列绝对定位层

`.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 且只占开头一小段 ✓
  · 样式:start 下圆角 0px + 下边框透明;mid 上下圆角 0;end 上圆角 0 +
    上边框透明 ✓
  · 无裁切:三段都落在列内 ✓
  · 回归:真全天仍在全天行;跨天不再出现在全天行;单天日程正常 ✓
  · 窄屏 375px:三段仍可见(w=91)✓

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

## 已知取舍

跨天段占满整列,同一天的普通日程与它在视觉上会重叠。
这是日历的固有取舍(Google 日历同样如此),用户确认的方案里也接受。
This commit is contained in:
2026-10-30 23:43:10 +08:00
parent 73c84c1910
commit 01d5bc262f
4 changed files with 362 additions and 33 deletions
+55
View File
@@ -3529,6 +3529,61 @@ body[class] .semi-datepicker-day-inhover { background-color: color-mix(in srgb,
.tg-block:hover { text-decoration: none; filter: brightness(1.1); z-index: 2; }
.tg-block b { font-weight: 600; margin-right: 3px; }
/* ============================================================
跨天日程的分段(v88 / 工单 #35)
============================================================
用户:「如果一个日程跨了三天,应该在日历本上怎么显示呢?在周视图和日视图,
应该怎么显示?现在显示的有问题。」
做法:一个跨天日程在**每一列各画一段**(`build_time_grid` 算好每段的
top/height),视觉上要让它们看起来是**一条**,而不是三个互不相干的块。
为什么不做跨列绝对定位层:`.tg-body` 左边有 56px 固定 gutter,
窄屏还会横向滚动(`.cal-tg-card{overflow:auto}` + 每列 min-width 92px),
`.tg-col` 又是 `overflow:hidden`。跨列层要自己算偏移、跟滚动跑、还不能被裁。
在列内画段则天然对齐,代价只是段间有 1px 列边框缝 —— 用下面的样式糊上。
三段的关系:
· start(起始段):上圆下平 —— 下方要接中间段
· mid (中间段):上下都平 —— 两头都接
· end (结束段):上平下圆 —— 上方接中间段
⚠️ 内侧不画 border:`.tg-block` 原本有 1px 白边,段与段之间会多出两条白线,
看着像断开了。这里按段位取消对应的那条边。
⚠️ 左右内缩量要比单天块**小**(单天是 left:2px / width:-4px):
跨天段要尽量贴满整列,缝隙才窄,连起来才像一条。模板已给 1%/98%。 */
.tg-block.is-seg-start {
border-top-left-radius: 4px; border-top-right-radius: 4px;
border-bottom-left-radius: 0; border-bottom-right-radius: 0;
border-bottom-color: transparent; /* 下边不画,与中间段连上 */
padding-bottom: 0;
}
.tg-block.is-seg-mid {
border-radius: 0;
border-top-color: transparent;
border-bottom-color: transparent;
padding-top: 1px; padding-bottom: 0;
}
.tg-block.is-seg-end {
border-top-left-radius: 0; border-top-right-radius: 0;
border-bottom-left-radius: 4px; border-bottom-right-radius: 4px;
border-top-color: transparent; /* 上边不画,与中间段连上 */
padding-top: 1px;
}
/* 延续标记:这段不是从今天开始的,用一个小箭头代替开始时刻 */
.tg-cont {
display: inline-block; margin-right: 2px;
opacity: .75; font-size: 10px; line-height: 1;
}
/* 跨天段占满整列宽度,内边距跟着收一点,字才不会被挤 */
.tg-block[data-seg] {
padding-left: 4px; padding-right: 4px;
/* 🔴 v88:跨天段用 left:0 / width:100%(模板按段区分),
必须配 box-sizing:border-box,否则 padding 会把宽度撑出列外。
`.tg-block` 已有 box-sizing:border-box,这里显式再写一次是为了防止
以后有人给单天块改 box-sizing 时顺手把这条也改掉。 */
box-sizing: border-box;
}
/* 天视图只有一列,把列拉宽好读;周视图 7 列时列宽由 flex 平分 */
@media (max-width: 720px) {
/* 窄屏留不住 56px 的刻度沟槽 + 7 列,改成整体可横向滚动(.cal-tg-card 已 overflow:auto),