跨端: 鸿蒙日历补「月/周/日」三档 —— 原来只有月视图

用户列的缺失之一(WebUI `CalendarView.tsx` 的 `type Scale = 'month'|'week'|'day'`)。
鸿蒙这边 `CalendarPage.ets` 自己的注释里就写着"没做"。

## 三处必须一起改,所以档位进 model 而不是页面里一串 if

档位切换同时改变三件事,任一漏改都**不报错、只是静静地不对**:

| | 月 | 周 | 日 |
|---|---|---|---|
| 标题 | `2026年9月` | 跨月时两头写月份 `9.28 – 10.4` | `2026年9月18日 周五` |
| 翻页步长 | ±1 **月** | ±7 天 | ±1 天 |
| 显示格子 | 整月网格 | 一行 7 天 | 一行、只亮一格 |

所以 `model/Calendar.ts`(纯逻辑,node 直接跑)新增:
`CalendarScale` / `addDaysIso` / `weekDaysOf` / `rangeTitleOf` / `stepDaysOf` / `scaleLabel`。
页面只调用,不在渲染里重写 —— 重写就是"三处里漏改一处"。

★ 步长按档走是**必须**的:周档按 1 天翻看起来像日档、日档按 7 天翻会跳过一周,
  两者都不报错。判据钉"三档步长两两不同"。
★ 周/日档的 7 天必须与月网格用**同一个** `weekStart`:不一致的话同一日期在两档下列位置
  不同,用户看到的是"切个视图日期就跳位了"。
★ 日档的格子仍摆在一行的**星期列**里(不是居中大字):上下翻日时格子不会在屏幕上跳。

## 实现要点

- 档位状态叫 `calScale` 而**不是** `scale` —— ArkUI 的 `CustomComponent` 已有 `scale`
  修饰符,同名直接编译失败(实测报 `Property 'scale' ... is not assignable to ...`)。
  判据钉住这个命名。
- 周/日档下 `year/month` 由锚点 `selectedIso` 推出来(`syncYearMonthFrom`),
  不允许两处各自保存"当前是几月" —— 否则会出现"标题写 9 月、周视图显示 10 月那周"。
- `monthKey()` 加上**档位**前缀:切档时那 7 个 iso 就是月网格里的 7 个,
  不带档位的话键完全重叠 ⇒ 一个节点都不被替换 ⇒ 过渡静默不播、旧 `inMonth` 残留。
- 空态补 `layoutWeight(1)`:实测周视图下"这一天没有日程"贴顶、**下半屏是一大片空壁纸**。

## 判据(harmony-calendar 23 → 30 条)

7 条新增,每条对着一个具体错法。**变异自检跑过两个方向**:
- 周档步长改成 1(看起来像日档)⇒ 判红 ✓
- 跨月周标题漏掉结束月份(`9.28 – 4`)⇒ 判红 ✓

## 实测(模拟器,逐档截图)

月/周/日三档切换后:周档标题 `2026年9月14–20日` + 一行 14~20,日档标题
`2026年9月18日 周五` + 只亮 18,月档回到整月网格。三张截图逐一核过。
This commit is contained in:
2026-09-18 11:08:05 +08:00
parent af2d2b5cad
commit fbe7879981
4 changed files with 401 additions and 30 deletions

View File

@ -206,3 +206,117 @@ export function localDateOf(timestamp: string): Date {
}
return new Date(ms);
}
/*
* ── 视图档(月 / 周 / 日)──
*
* 对齐 WebUI `CalendarView.tsx` 的 `type Scale = 'month' | 'week' | 'day'`。
* 鸿蒙这边原来**只有月视图**(`CalendarPage.ets:8` 的注释列过"没做"的清单)。
*
* 档位切换会同时改变三件事,所以它必须是**数据类型**而不是页面上的一串 if:
* ① 标题怎么写(月:`2026年9月`;周:跨月时两头都写月份;日:带星期)
* ② 前后翻动的**步长**(月:±1 月;周:±7 天;日:±1 天)
* ③ 要显示哪些格子(月:整月网格;周:一行 7 天;日:单天)
* 这三处任一漏改都会"看起来正常但翻错量",所以三处都由这里同一组函数算。
*/
/** 视图档。与 WebUI 的 `Scale` 同值(字符串也一致,便于跨端对照)。 */
export type CalendarScale = 'month' | 'week' | 'day';
/** 档位的中文短名(给切换按钮)。顺序与 WebUI 的 `['month','week','day']` 一致。 */
export const SCALE_ORDER: CalendarScale[] = ['month', 'week', 'day'];
export function scaleLabel(scale: CalendarScale): string {
if (scale === 'month') {
return '月';
}
return scale === 'week' ? '周' : '日';
}
/**
* 把 `YYYY-MM-DD` 位移若干天。
*
* 用 `Date.UTC` 构造再位移:与 `firstWeekday` 同一口径(UTC 取数、不受设备时区影响)。
* 日期键是"本地年月日"的**字面量**,不承载瞬间语义 —— 见文件顶部关于
* "日期键 vs 时间戳"的那条说明。
*/
export function addDaysIso(iso: string, delta: number): string {
const parts: number[] = iso.split('-').map((s: string) => parseInt(s, 10));
const d: Date = new Date(Date.UTC(parts[0], parts[1] - 1, parts[2]));
d.setUTCDate(d.getUTCDate() + delta);
const y: number = d.getUTCFullYear();
const m: number = d.getUTCMonth() + 1;
const dd: number = d.getUTCDate();
return isoOf(y, m, dd);
}
/**
* 某一天所在周的 7 天(按 `weekStart` 起始),返回 `YYYY-MM-DD` 数组。
*
* ★ 必须与月网格用**同一个** `weekStart` —— 两处不一致的话,月视图第一列是周日、
* 周视图第一列是周一,同一个日期在两个档位下的列位置不同,看起来像错位。
* 判据钉"两处用同一个常量"。
*
* 返回空数组表示入参不是合法日期键(调用方据此跳过,不猜一个日子)。
*/
export function weekDaysOf(iso: string, weekStart: number): string[] {
const parts: number[] = iso.split('-').map((s: string) => parseInt(s, 10));
if (parts.length !== 3 || parts.some((n: number) => Number.isNaN(n))) {
return [];
}
const d: Date = new Date(Date.UTC(parts[0], parts[1] - 1, parts[2]));
const w: number = d.getUTCDay();
const back: number = (w - weekStart + 7) % 7;
const out: string[] = [];
for (let i = 0; i < 7; i++) {
out.push(addDaysIso(iso, i - back));
}
return out;
}
/**
* 标题文案 —— 三档各一种写法(与 WebUI `CalendarView.tsx:242-258` 同口径)。
*
* ★ 周视图**跨月时两头都写月**(`9.28 – 10.4`):
* 只写 `9月28日 - 4日` 的话,"4日"看不出是十月 —— 这是 WebUI 注释里明说的坑。
* ★ 日视图带星期:单天视图上"今天是周几"比"几号"更常用。
*/
export function rangeTitleOf(scale: CalendarScale, iso: string, weekStart: number): string {
const parts: number[] = iso.split('-').map((s: string) => parseInt(s, 10));
if (parts.length !== 3 || parts.some((n: number) => Number.isNaN(n))) {
return '';
}
const year: number = parts[0];
const month: number = parts[1];
const day: number = parts[2];
if (scale === 'month') {
return `${year}年${month}月`;
}
if (scale === 'week') {
const days: string[] = weekDaysOf(iso, weekStart);
if (days.length === 0) {
return '';
}
const a: number[] = days[0].split('-').map((s: string) => parseInt(s, 10));
const b: number[] = days[6].split('-').map((s: string) => parseInt(s, 10));
if (a[1] === b[1]) {
return `${a[0]}年${a[1]}月${a[2]}–${b[2]}日`;
}
return `${a[1]}.${a[2]} – ${b[1]}.${b[2]}`;
}
/* 日视图:星期名走 weekdayNameOf(与月网格同一处算法,不另写一份) */
return `${year}年${month}月${day}日 周${weekdayNameOf(year, month, day)}`;
}
/**
* 前后翻动的步长(天)—— 月档返回 0,因为月档按**月**翻而不是按天。
*
* 单独抽出来是防"档位改了但步长没改":日档按 7 天翻会跳过一周,
* 周档按 1 天翻看起来像日档 —— 两者都不会报错,只会静静地不对。
*/
export function stepDaysOf(scale: CalendarScale): number {
if (scale === 'week') {
return 7;
}
return scale === 'day' ? 1 : 0;
}