日程编辑:确认「开始」后结束时间自动 = 开始 + 1 小时 (laoyang/FamilyBook#34)

用户 2026-11-01:
  「当我选择了开始时间并确定之后,结束时间要跟着开始时间变成开始时间加1个小时。
    也就是说,所有时间我们都默认它是一个小时。如果不是一个小时,再让他自己去改。」

## 改前

`applyPicker()` 只在**会变成负时长**时才动结束时间:

    if (this.fEndDate < iso || (this.fEndDate === iso && this.fEndTime < hm)) { ... }

⇒ 用户改了开始、结束还挂在老位置,实际时长变成随机的。

## 改后

点「开始」确定 ⇒ 结束**无条件**重置为「开始 + 1 小时」。
改「结束」那条路一个字不动(改结束不影响开始)—— 这是「默认一小时、
不像一小时再自己改」的另一半。

全天日程跳过这段:时刻部分本来就不生效,改了不会落到服务端,
但会让"切回非全天"时看到一个莫名的时间。

## 新增 fbPlusHours()

`FbDateTimeField.ets` 里加 `fbPlusHours(date, time, hours)`,返回 `{date, time}`。
**不复用现成的 `fbAddHour()`**,因为那个有两个问题:

    return fbPad2(Math.min(hh + 1, 23)) + ':' + mm;

① `Math.min(hh+1, 23)` **封顶 23 点** ⇒ 23:30 加一小时还是 23:30;
② 只返回时间、不带日期 ⇒ 表达不了「次日 00:30」。

跨天/跨月/跨年全部交给 `Date` 运算,不自己算天数
(自己 `if (hh>=24) dd+=1` 在"11 月 30 日 + 1 天"上就会错成 11 月 31 日)。

## 验证

**纯函数真值表 13 项**(从真实 `.ets` 抽函数、只擦类型标注后 node 直接跑,
不手抄替身;擦除后断言行数不变、无类型残留、没引入新字符):

普通 / 跨天整点 / 跨天半点 / 跨月 / 跨年 / 非闰年 2 月末 / **闰年 2 月末 ⇒ 2/29** / 午夜 / 带分钟
—— 全过。其中 23:30+1h ⇒ **次日 00:30**、12/31 ⇒ 次年 01/01、2028-02-28 ⇒ 2028-02-29。

**反向验证 3 项**(证明判据不是恒真):
坏A(封顶 23 点 / 不带日期)被抓到、坏B(丢分钟)被抓到、坏C(固定 28 天)被抓到。

**接线断言 12 项 + 反向验证 5 项**(静态,因为"函数对 ≠ 接进去了",
上一轮 #34 主体 bug 就是参数名对不上、编译运行都不报错):
- 开始分支调用 `fbPlusHours(iso, hm, 1)`、结果**同时**写 `fEndDate` 与 `fEndTime`
- 结束分支**没有任何** `fStart*` 赋值
- 全天日程有 `!this.fAllDay` 判断
- `pickIsEnd = !isStart`(#34 主体防回归)
- 不再残留 `this.fEndDate < iso` 那种条件顺延
- 反向:改回老写法 / 只写日期不写时间 / 改结束顺手改开始 / `pickIsEnd` 再反过来
  → 逐条转红;**对照组**(只改一句注释)仍绿 —— 证明断言不会"见到改动就红"

> ⚠️ 接线判据自己第一版有 bug,是反向验证抓出来的:坏1 用正则
> `if \(!this\.fAllDay\) \{[\s\S]*?\n      \}` 找替换目标,但渲染层还有一个
> `if (!this.fAllDay)`(控制「结束」那一行显示),正则先吃到那个 ⇒ 注入落在无关位置
> ⇒ 断言仍绿(假阴性)。改成"先把 applyPicker 的函数体切出来再替换"。

**编译**:BUILD SUCCESSFUL,33 tasks(17 executed)。验签通过。

**⚠️ 已知存量失败(与本次无关)**:`_fb_arkts_guard.js` 跑出 1321 通过 / 3 失败,
三条都在工作区里**别的会话未提交的改动**上(`AGENDA_BACK_DAYS` 被换成 `CAL_FULL_FROM/TO`
导致"列表视图显式传日期范围"判据失配、`canEdit` 相关两条属 #31)。
本次只改 `FbDateTimeField.ets` 与 `FbEventEditSheet.ets`,不碰那些文件;
用 `git show HEAD:` 核实过 HEAD 版本里 `AGENDA_BACK_DAYS` 还在(判据本来通过)。
This commit is contained in:
2026-11-01 09:38:14 +08:00
parent d2d0a435a3
commit 5661312679
2 changed files with 60 additions and 6 deletions
@@ -105,6 +105,38 @@ export function fbAddHour(time: string): string {
return fbPad2(Math.min(hh + 1, 23)) + ':' + mm; return fbPad2(Math.min(hh + 1, 23)) + ':' + mm;
} }
/** `fbPlusHours()` 的返回形状(ArkTS 不允许匿名对象字面量作为返回值类型书写,单独给个接口) */
export interface FbDateTime {
date: string;
time: string;
}
/**
* `YYYY-MM-DD` + `HH:mm` 往后推 N 小时 ⇒ `{ date, time }`(**跨天自动进位**)。
*
* 起源:工单 #34 追加需求(2026-11-01)——
* 「当我选择了开始时间并确定之后,结束时间要跟着开始时间变成开始时间加1个小时。
* 也就是说,所有时间我们都默认它是一个小时。如果不是一个小时,再让他自己去改。」
*
* ⚠️ 为什么不复用上面的 `fbAddHour()`:那只返回时间、不带日期,
* 而且用 `Math.min(hh+1, 23)` **封顶 23 点** ⇒ 23:30 加一小时还是 23:30。
* 日程跨天是常态(20:00 → 次日 02:00 是真实用例),必须有日期。
*
* ⚠️ 用 `Date` 做进位而不是自己算:跨月、跨年、闰年、月末天数全交给运行时。
* 自己 `if (hh >= 24) { dd += 1 }` 看着简单,但"11 月 30 日 + 1 天"就会错成 11 月 31 日。
*/
export function fbPlusHours(date: string, time: string, hours: number): FbDateTime {
const d: Date = fbParseDate(date);
const h: number = time.length >= 5 ? parseInt(time.substring(0, 2), 10) : 9;
const m: number = time.length >= 5 ? parseInt(time.substring(3, 5), 10) : 0;
const out: Date = new Date(d.getFullYear(), d.getMonth(), d.getDate());
out.setHours(h + hours, m, 0, 0);
return {
date: fbIsoOf(out),
time: fbPad2(out.getHours()) + ':' + fbPad2(out.getMinutes())
};
}
/** 日期 + 时分拼成服务端认的 `YYYY-MM-DD HH:MM`(秒由服务端补) */ /** 日期 + 时分拼成服务端认的 `YYYY-MM-DD HH:MM`(秒由服务端补) */
export function fbJoinDateTime(date: string, time: string): string { export function fbJoinDateTime(date: string, time: string): string {
return date + ' ' + time; return date + ' ' + time;
@@ -63,7 +63,10 @@ import {
fbDateOf, fbDateOf,
fbTimeOf, fbTimeOf,
fbIsoOf, fbIsoOf,
fbAddHour fbAddHour,
// 工单 #34 追加需求:点「开始」确定后,结束 = 开始 + 1 小时(跨天自动进位)
fbPlusHours,
FbDateTime
} from './FbDateTimeField'; } from './FbDateTimeField';
const DOMAIN: number = 0x0000; const DOMAIN: number = 0x0000;
@@ -671,7 +674,25 @@ export struct FbEventEditSheet {
this.pickOpen = true; this.pickOpen = true;
} }
/** 「确定」:把草稿写回;开始改了还会把结束顺延(避免"负时长") */ /**
* 「确定」:把草稿写回。
*
* 🔵 工单 #34 追加需求(用户 2026-11-01):
* 「当我选择了开始时间并确定之后,结束时间要跟着开始时间变成开始时间加1个小时。
* 也就是说,所有时间我们都默认它是一个小时。如果不是一个小时,再让他自己去改。」
*
* ⇒ **点「开始」确定后,结束无条件重置为「开始 + 1 小时」**。
*
* 改动前只在"会变成负时长"时才动结束(`if (结束 < 开始)`),
* 于是用户改了开始、结束还挂在老位置,实际时长变成随机的 —— 就是要灭掉这个。
*
* ⚠️ 改「结束」那条路**一个字不动**:改结束不能影响开始。
* 这正是"默认一小时、不像一小时再自己改"的另一半。
* ⚠️ 跨天用 `fbPlusHours()` 的 `Date` 运算:23:30 + 1h ⇒ **次日 00:30**,
* 日期一起进一天。不要图省事写成 `fbAddHour()` —— 那个封顶 23 点且不带日期。
* ⚠️ 全天日程不参与:时刻部分本来就不生效(`save()` 里全天走 `00:00:00`),
* 这里改了也不会落到服务端,但会让"切回非全天"时看到一个莫名的时间,所以跳过。
*/
private applyPicker(iso: string, hm: string): void { private applyPicker(iso: string, hm: string): void {
if (this.pickIsEnd) { if (this.pickIsEnd) {
this.fEndDate = iso; this.fEndDate = iso;
@@ -679,10 +700,11 @@ export struct FbEventEditSheet {
} else { } else {
this.fStartDate = iso; this.fStartDate = iso;
this.fStartTime = hm; this.fStartTime = hm;
// ⚠️ 开始改到"结束之后" ⇒ 把结束顺延到同一时刻(服务端不校验,但负时长是坏的) if (!this.fAllDay) {
if (this.fEndDate < iso || (this.fEndDate === iso && this.fEndTime < hm)) { // 结束 = 开始 + 1 小时(默认时长一律一小时;要别的时长用户自己去点「结束」改)
this.fEndDate = iso; const plus: FbDateTime = fbPlusHours(iso, hm, 1);
this.fEndTime = hm; this.fEndDate = plus.date;
this.fEndTime = plus.time;
} }
} }
this.pickOpen = false; this.pickOpen = false;