跨端: 鸿蒙日历补「月/周/日」三档 —— 原来只有月视图
用户列的缺失之一(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:
@ -385,3 +385,95 @@ test('★ 判据自检:写侧的错误写法必须判红', () => {
|
||||
assert.ok(!/put<CalendarEvent>/.test("post<CalendarEvent>('/calendar/events/' + id, input)"),
|
||||
'编辑用 POST 能被"必须 PUT"那条命中');
|
||||
});
|
||||
|
||||
/* ───────────────────────── 视图档:月 / 周 / 日 ───────────────────────── */
|
||||
|
||||
/*
|
||||
* 2026-09-18:鸿蒙的日历原来**只有月视图**(`CalendarPage.ets` 的注释列过"没做"清单)。
|
||||
* 对齐 WebUI `CalendarView.tsx` 的 `type Scale = 'month' | 'week' | 'day'`。
|
||||
*
|
||||
* 档位切错**不会报错**,只会静静地不对 —— 所以下面每一条都对着一个具体的错法。
|
||||
*/
|
||||
|
||||
test('★ 翻页步长按档走:月=按天 0(按月翻)、周=7、日=1 —— 认错档位就是静默跳过/原地不动', () => {
|
||||
assert.equal(C.stepDaysOf('month'), 0, '月档按**月**翻,不按天 —— 返回 0 表示走 addMonths');
|
||||
assert.equal(C.stepDaysOf('week'), 7, '周档必须按 7 天翻(按 1 天翻看起来像日档)');
|
||||
assert.equal(C.stepDaysOf('day'), 1, '日档按 1 天翻(按 7 天翻会跳过一周且毫无提示)');
|
||||
// 自检:三个档的步长必须**互不相同**,否则"分档"本身没意义
|
||||
const steps = C.SCALE_ORDER.map(s => C.stepDaysOf(s));
|
||||
assert.equal(new Set(steps).size, 3, `三档步长必须两两不同,实际 ${steps.join('/')}`);
|
||||
});
|
||||
|
||||
test('★ 周档跨月时标题两头都写月份(只写"9月28日-4日"看不出 4 日是十月)', () => {
|
||||
// 2026-09-30 是周三 → 周一是 09-28,周日是 10-04 ⇒ 跨月
|
||||
const t = C.rangeTitleOf('week', '2026-09-30', 1);
|
||||
assert.match(t, /9\.28/, `跨月周标题要写左端月日,实际「${t}」`);
|
||||
assert.match(t, /10\.4/, `跨月周标题要写右端月日(含月份),实际「${t}」`);
|
||||
// 不跨月时反过来:不该出现"两个月份"
|
||||
const same = C.rangeTitleOf('week', '2026-09-18', 1);
|
||||
assert.ok(!same.includes('–') || (same.match(/月/g) ?? []).length === 1,
|
||||
`同一月内的周不该写两个月份,实际「${same}」`);
|
||||
});
|
||||
|
||||
test('★ 日档标题带星期;月档标题不带(三档各一种写法,别互相串)', () => {
|
||||
const d = C.rangeTitleOf('day', '2026-09-18', 1);
|
||||
assert.match(d, /^2026年9月18日 周./, `日档要「年月日 + 周X」,实际「${d}」`);
|
||||
assert.equal(C.rangeTitleOf('month', '2026-09-18', 1), '2026年9月', '月档只有年月');
|
||||
});
|
||||
|
||||
test('★ 周/日档的 7 天必须与月网格用**同一个** weekStart(否则同一日期两档列位置不同)', () => {
|
||||
/*
|
||||
* 这是"看起来只是排版不同"的那类错:月视图第一列是周一、周视图第一列是周日,
|
||||
* 同一个日期在两档下会落在不同的列 —— 用户会觉得"切个视图日期就跳位了"。
|
||||
* 两处都必须从**同一个** `weekStart` 参数出发。
|
||||
*/
|
||||
for (const ws of [0, 1]) {
|
||||
const days = C.weekDaysOf('2026-09-18', ws);
|
||||
assert.equal(days.length, 7, '一周必须恰好 7 天');
|
||||
// 这 7 天的星期序列必须等于 weekdayLabels(ws) 的顺序
|
||||
const names = C.weekdayLabels(ws);
|
||||
const got = days.map(iso => {
|
||||
const p = iso.split('-').map(Number);
|
||||
return C.weekdayNameOf(p[0], p[1], p[2]);
|
||||
});
|
||||
assert.deepEqual(got, names, `weekStart=${ws} 时两处星期序必须一致`);
|
||||
}
|
||||
});
|
||||
|
||||
test('★ 周/日档日期推进:跨月、跨年、闰年都对(与月档同一个 Date.UTC 口径)', () => {
|
||||
assert.equal(C.addDaysIso('2026-09-30', 1), '2026-10-01', '跨月');
|
||||
assert.equal(C.addDaysIso('2026-01-01', -1), '2025-12-31', '跨年(往回)');
|
||||
assert.equal(C.addDaysIso('2024-02-28', 1), '2024-02-29', '闰年二月');
|
||||
assert.equal(C.addDaysIso('2025-02-28', 1), '2025-03-01', '平年二月');
|
||||
assert.equal(C.addDaysIso('2026-09-18', 7), '2026-09-25', '周档一整步(前)');
|
||||
assert.equal(C.addDaysIso('2026-09-18', -7), '2026-09-11', '周档一整步(后)');
|
||||
// 非法输入不许猜一个日子
|
||||
assert.deepEqual(C.weekDaysOf('not-a-date', 1), [], '坏日期键要给空数组,不能猜');
|
||||
});
|
||||
|
||||
test('★ 页面:档位必须驱动「标题/步长/格子」三处,且三处都来自 Calendar.ts', () => {
|
||||
const page = code(join(ETS, 'pages', 'CalendarPage.ets'));
|
||||
/*
|
||||
* 三处里任一处漏改 ⇒ "看起来正常但翻错量/标题不对"。
|
||||
* 所以判据不只要求"有档位这个状态",而是要求**三个消费者**都在。
|
||||
*/
|
||||
assert.match(page, /calScale:\s*CalendarScale/, '要有档位状态(类型来自 Calendar.ts)');
|
||||
assert.match(page, /stepDaysOf\(\s*this\.calScale\s*\)/, '翻页步长要按档走(不许在页面里写 if/else)');
|
||||
assert.match(page, /rangeTitleOf\(\s*this\.calScale/, '标题要按档走');
|
||||
assert.match(page, /this\.calScale === 'month'/, '格子形状要按档走(月=整月网格,其余=一行)');
|
||||
/*
|
||||
* ★ 档位状态名**不能**叫 `scale`:ArkUI 的 `CustomComponent` 已经有一个 `scale`
|
||||
* 属性(通用修饰符 `scale(value: ScaleOptions)`),同名会编译失败
|
||||
* (实测:`Property 'scale' ... is not assignable to ... '{ (value: ScaleOptions)... }'`)。
|
||||
* 这条钉住它,免得后人图省事改回去。
|
||||
*/
|
||||
assert.ok(!/@State\s+scale\s*:/.test(page),
|
||||
'档位状态不许叫 `scale` —— 与 ArkUI 内置的 scale 修饰符同名,编译不过');
|
||||
});
|
||||
|
||||
test('★ 页面:周/日档下空态要占满剩余高度(否则下半屏是一大片空壁纸)', () => {
|
||||
const page = code(join(ETS, 'pages', 'CalendarPage.ets'));
|
||||
const empty = /这一天没有日程[\s\S]{0,200}?layoutWeight\(1\)/.exec(page);
|
||||
assert.ok(empty,
|
||||
'空态必须 layoutWeight(1):实测 09-18 周视图下"这一天没有日程"贴顶、下半屏全空');
|
||||
});
|
||||
|
||||
@ -102,7 +102,7 @@ const SUITE = [
|
||||
// 明文只对公网告警 / 404 说清“少了 /api/v1” / 网络错误码分类 / 归一化只有一份实现。
|
||||
// 值判据跑真逻辑(model/ApiBase.ts);`.ets` 那几条是**静态**接线判据(无设备)。
|
||||
['test/harmony-apibase.test.mjs', ['--experimental-strip-types', '--no-warnings'], 13],
|
||||
['test/harmony-calendar.test.mjs', ['--experimental-strip-types', '--no-warnings'], 23],
|
||||
['test/harmony-calendar.test.mjs', ['--experimental-strip-types', '--no-warnings'], 30],
|
||||
['test/debt-visibility.test.mjs', [], 1],
|
||||
['test/commit-hygiene.test.mjs', ['--experimental-strip-types', '--no-warnings'], 4],
|
||||
// 判据目录自身的卫生:读文本必须走 test/lib/read.mjs 的具名入口
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -30,7 +30,15 @@ import {
|
||||
monthGrid,
|
||||
monthLabel,
|
||||
addMonths,
|
||||
addDaysIso,
|
||||
weekDaysOf,
|
||||
rangeTitleOf,
|
||||
stepDaysOf,
|
||||
scaleLabel,
|
||||
SCALE_ORDER,
|
||||
CalendarScale,
|
||||
weekdayLabels,
|
||||
weekdayNameOf,
|
||||
isoOfLocal,
|
||||
localIsoOf,
|
||||
hhmmAtOffset,
|
||||
@ -89,6 +97,14 @@ export struct CalendarPage {
|
||||
@State month: number = 1;
|
||||
@State todayIso: string = '';
|
||||
@State selectedIso: string = '';
|
||||
/**
|
||||
* 视图档:`month` | `week` | `day`(对齐 WebUI `type Scale`)。
|
||||
*
|
||||
* 它同时决定三件事:标题怎么写、前后翻多大一步、要显示哪些格子 ——
|
||||
* 三处算法都在 `model/Calendar.ts` 里(那里有单测判据),页面只负责调用,
|
||||
* 不在渲染里重写一遍(重写就是"三处里漏改一处"那种静默错位)。
|
||||
*/
|
||||
@State calScale: CalendarScale = 'month';
|
||||
@State events: CalendarEvent[] = [];
|
||||
@State loading: boolean = false;
|
||||
@State error: string = '';
|
||||
@ -199,30 +215,84 @@ export struct CalendarPage {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
/** 翻月走 `addMonths`(纯逻辑判据钉过跨年两个方向),不自己算 month+1 */
|
||||
private shiftMonth(delta: number): void {
|
||||
/*
|
||||
* 翻月要有**方向感的横向滑入**(对齐 WebUI `@keyframes cal-in-next/prev`)。
|
||||
*
|
||||
* WebUI 那边靠 `cal-slide-next` / `cal-slide-prev` 两个类 + 200ms 动画;
|
||||
* ArkUI 这里靠 `animateTo` 开窗口 + 格子挂 `calendarSlide(forward)` 过渡。
|
||||
*
|
||||
* ★ 两个都**不能少**(实测过:只包 animateTo 与硬切看不出区别)——
|
||||
* 与 `switchIndex` 里那条注释是同一回事。
|
||||
*/
|
||||
/**
|
||||
* 前后翻动 —— **步长取决于视图档**(月:±1 月;周:±7 天;日:±1 天)。
|
||||
*
|
||||
* 步长去 `stepDaysOf(scale)` 要,不在这里写 if/else:
|
||||
* 档位与步长分开写的话,"加了日档但忘了改步长"会静静地按一天翻(看着像周档)。
|
||||
*
|
||||
* ★ 翻页要有**方向感的横向滑入**(对齐 WebUI `@keyframes cal-in-next/prev`)。
|
||||
* WebUI 靠 `cal-slide-next/prev` 两个类 + 200ms 动画;这里靠 `animateTo` 开窗口
|
||||
* + 格子挂 `calendarSlide(forward)` 过渡。两个都**不能少**
|
||||
* (实测过:只包 animateTo 与硬切看不出区别)。
|
||||
*/
|
||||
private shiftRange(delta: number): void {
|
||||
const forward: boolean = delta > 0;
|
||||
const step: number = stepDaysOf(this.calScale);
|
||||
this.getUIContext().animateTo({
|
||||
duration: Theme.durCal,
|
||||
curve: Theme.easeRise
|
||||
}, () => {
|
||||
this.slideForward = forward;
|
||||
const ym: number[] = addMonths(this.year, this.month, delta);
|
||||
this.year = ym[0];
|
||||
this.month = ym[1];
|
||||
if (step === 0) {
|
||||
const ym: number[] = addMonths(this.year, this.month, delta);
|
||||
this.year = ym[0];
|
||||
this.month = ym[1];
|
||||
} else {
|
||||
/* 锚点位移后再把年月同步过去(锚点是唯一真相,年月是它的投影) */
|
||||
this.selectedIso = addDaysIso(this.selectedIso, delta * step);
|
||||
this.syncYearMonthFrom(this.selectedIso);
|
||||
}
|
||||
});
|
||||
this.loadEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 `year/month` 对齐到某个日期键 —— 周/日档翻动后调用。
|
||||
*
|
||||
* 不允许两处各自保存"当前是几月":锚点一变而年月没跟上,
|
||||
* 就会出现"标题写着 9 月而周视图显示 10 月那一周"。
|
||||
*/
|
||||
private syncYearMonthFrom(iso: string): void {
|
||||
const parts: number[] = iso.split('-').map((s: string) => parseInt(s, 10));
|
||||
if (parts.length === 3 && !parts.some((n: number) => Number.isNaN(n))) {
|
||||
this.year = parts[0];
|
||||
this.month = parts[1];
|
||||
}
|
||||
}
|
||||
|
||||
/** 切视图档 —— 同时把标题/步长/格子一起换(三处都由 scale 驱动) */
|
||||
private setScale(next: CalendarScale): void {
|
||||
if (next === this.calScale) {
|
||||
return;
|
||||
}
|
||||
this.getUIContext().animateTo({
|
||||
duration: Theme.durCal,
|
||||
curve: Theme.easeRise
|
||||
}, () => {
|
||||
this.calScale = next;
|
||||
/* 切档时锚点不变(用户看的是同一个日子),但年月要跟着锚点对齐 */
|
||||
this.syncYearMonthFrom(this.selectedIso);
|
||||
});
|
||||
}
|
||||
|
||||
/** 标题文案 —— 按月/周/日三种写法(算法在 model/Calendar.ts,有单测) */
|
||||
private rangeTitle(): string {
|
||||
if (this.calScale === 'month') {
|
||||
return monthLabel(this.year, this.month);
|
||||
}
|
||||
return rangeTitleOf(this.calScale, this.selectedIso, WEEK_START);
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻月(保留旧名 —— 别处还有调用)。
|
||||
*
|
||||
* 现在统一走 `shiftRange`:月档的步长就是 ±1 月,行为与旧实现一致。
|
||||
*/
|
||||
private shiftMonth(delta: number): void {
|
||||
this.shiftRange(delta);
|
||||
}
|
||||
|
||||
private goToday(): void {
|
||||
const now: Date = new Date();
|
||||
this.refreshToday();
|
||||
@ -232,8 +302,62 @@ export struct CalendarPage {
|
||||
this.loadEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* 要显示哪些格子 —— **由视图档决定**(月:整月网格;周:一行 7 天;日:单天)。
|
||||
*
|
||||
* ★ 三档都返回**同一个形状**(`DayCell[][]`,每行 7 格):
|
||||
* 周/日档只是行数少(周 1 行、日 1 行但只亮一格),渲染代码不需要按档分支。
|
||||
* 换成"三档各自一种渲染"的话,改一处样式要改三遍,迟早漏一个。
|
||||
*
|
||||
* ★ 月档的日期范围由 `year/month` 定,周/日档由 `selectedIso`(锚点)定 ——
|
||||
* 这两者靠 `syncYearMonthFrom()` 保持同步(见 `shiftRange`)。
|
||||
*/
|
||||
private rows(): DayCell[][] {
|
||||
return monthGrid(this.year, this.month, WEEK_START, this.todayIso);
|
||||
if (this.calScale === 'month') {
|
||||
return monthGrid(this.year, this.month, WEEK_START, this.todayIso);
|
||||
}
|
||||
if (this.calScale === 'week') {
|
||||
const days: string[] = weekDaysOf(this.selectedIso, WEEK_START);
|
||||
if (days.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return [days.map((iso: string) => this.cellOf(iso))];
|
||||
}
|
||||
/* 日档:只要一天。仍然摆在一行里(列位置由本周的星期决定),
|
||||
这样「上下翻日」时格子**不会在屏幕上跳来跳去** —— 它始终在星期列里。 */
|
||||
const week: string[] = weekDaysOf(this.selectedIso, WEEK_START);
|
||||
if (week.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const only: DayCell[] = [];
|
||||
for (let i = 0; i < week.length; i++) {
|
||||
const cell: DayCell = this.cellOf(week[i]);
|
||||
/* 单天视图:不是那天的格子标成"不在本月"(渲染上就是淡出),不画数字 */
|
||||
if (week[i] !== this.selectedIso) {
|
||||
cell.inMonth = false;
|
||||
cell.label = 0;
|
||||
}
|
||||
only.push(cell);
|
||||
}
|
||||
return [only];
|
||||
}
|
||||
|
||||
/**
|
||||
* 把一个日期键包成一个 `DayCell`(周/日档用)。
|
||||
*
|
||||
* ★ `inMonth` 在周/日档里表示"属于**本周**"(而不是"属于本月"):
|
||||
* 渲染层用它决定是不是淡出,语义是"这是当前视野里的日子吗"。
|
||||
* 叫 `inMonth` 是沿用现有字段名(改名会动到渲染与判据)。
|
||||
*/
|
||||
private cellOf(iso: string): DayCell {
|
||||
const cell: DayCell = new DayCell();
|
||||
const parts: number[] = iso.split('-').map((s: string) => parseInt(s, 10));
|
||||
cell.day = parts.length === 3 ? parts[2] : 0;
|
||||
cell.label = cell.day;
|
||||
cell.inMonth = true;
|
||||
cell.isToday = iso === this.todayIso;
|
||||
cell.iso = iso;
|
||||
return cell;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -246,16 +370,16 @@ export struct CalendarPage {
|
||||
@State slideForward: boolean = true;
|
||||
|
||||
/**
|
||||
* 换一个"格子内容"的身份 —— `ForEach` 的键挂上它,翻月时才会重放入场过渡。
|
||||
* 换一个"格子内容"的身份 —— `ForEach` 的键挂上它,翻页时才会重放入场过渡。
|
||||
*
|
||||
* ★ 为什么需要这个:ArkUI 的过渡只在**节点被替换**时播。`rows()` 直接由
|
||||
* `this.year/month` 算出来,键是 `c${cell.iso}#${cell.label}` —— 翻月后
|
||||
* 日期确实变了(键也会变),所以过渡会播。但同一个键在"跨年同月同名"等
|
||||
* 边角下可能不变,那时动画会静默不播。带上 `ym` 前缀把这个可能性去掉:
|
||||
* 月份一变,所有键必然全变。
|
||||
* ★ 为什么需要这个:ArkUI 的过渡只在**节点被替换**时播。带上 `月份 + 档位` 前缀:
|
||||
* ① 翻月 —— 日期变了键本来就变,但"跨年同名月"那类边角可能不变;
|
||||
* ② 切档 —— 从整月网格变成一行 7 天,**格子的键可能完全重叠**
|
||||
* (周档的那 7 个 iso 就是月档里的 7 个),不带档位前缀的话
|
||||
* 切档时一个节点都不会被替换 ⇒ 动画静默不播、且旧格子的 `inMonth` 残留。
|
||||
*/
|
||||
private monthKey(): string {
|
||||
return `${this.year}-${this.month}`;
|
||||
return `${this.calScale}-${this.year}-${this.month}-${this.selectedIso}`;
|
||||
}
|
||||
|
||||
/** 这一天有几条日程(画事件点用)。数量很小,线性扫即可,不引入第二份索引。 */
|
||||
@ -743,37 +867,71 @@ export struct CalendarPage {
|
||||
build() {
|
||||
Stack() {
|
||||
Column() {
|
||||
/* 头部:翻月 + 回今天 */
|
||||
/*
|
||||
* 头部:翻页 + 标题 + 视图档 + 新建。
|
||||
*
|
||||
* 对齐 WebUI `CalendarView.tsx:293-370` 那条工具条的分组与顺序:
|
||||
* ‹ › 今天 | 标题 | 月/周/日 | 新建
|
||||
* WebUI 还有导入/导出 .ics 两个图标,鸿蒙这边暂未做(那两个要文件选择器 + .ics 解析,
|
||||
* 与"月份/星期"这类算术不同,属于一个独立的工作量)。
|
||||
* 这里是**窄屏**,四段挤一行会溢出 —— 拆成两行:
|
||||
* 上行 = 翻页 + 标题;下行 = 今天 + 视图档 + 新建。
|
||||
*/
|
||||
Row() {
|
||||
Button('‹')
|
||||
.fontSize(Theme.fontBody)
|
||||
.backgroundColor(Color.Transparent)
|
||||
.fontColor(Theme.textPrimary)
|
||||
.onClick(() => this.shiftMonth(-1))
|
||||
Text(monthLabel(this.year, this.month))
|
||||
Text(this.rangeTitle())
|
||||
.fontSize(Theme.fontBody)
|
||||
.fontColor(Theme.textPrimary)
|
||||
.layoutWeight(1)
|
||||
.textAlign(TextAlign.Center)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
Button('›')
|
||||
.fontSize(Theme.fontBody)
|
||||
.backgroundColor(Color.Transparent)
|
||||
.fontColor(Theme.textPrimary)
|
||||
.onClick(() => this.shiftMonth(1))
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 6, right: 6, top: 4, bottom: 0 })
|
||||
|
||||
Row() {
|
||||
Button('今天')
|
||||
.fontSize(Theme.fontSmall)
|
||||
.backgroundColor(Theme.accentSoft)
|
||||
.fontColor(Theme.accentStrong)
|
||||
.onClick(() => this.goToday())
|
||||
Blank()
|
||||
/*
|
||||
* 视图档:**分段式**三个按钮(对齐 WebUI 的 `flex rounded border` 那一组)。
|
||||
* 选中态只换**底色与文字色**(与底栏导航同纪律:不引入额外的形状/指示条)。
|
||||
*/
|
||||
ForEach(SCALE_ORDER, (s: CalendarScale) => {
|
||||
Text(scaleLabel(s))
|
||||
.fontSize(Theme.fontSmall)
|
||||
.fontColor(this.calScale === s ? Theme.accentFg : Theme.textPrimary)
|
||||
.backgroundColor(this.calScale === s ? Theme.accent : Theme.accentSoft)
|
||||
.textAlign(TextAlign.Center)
|
||||
.width(34)
|
||||
.height(26)
|
||||
.borderRadius(6)
|
||||
.margin({ left: 4 })
|
||||
.onClick(() => this.setScale(s))
|
||||
}, (s: CalendarScale) => `scale${s}`)
|
||||
Button('+')
|
||||
.fontSize(Theme.fontBody)
|
||||
.backgroundColor(Theme.accentSoft)
|
||||
.fontColor(Theme.accentStrong)
|
||||
.margin({ left: 6 })
|
||||
.backgroundColor(Theme.accent)
|
||||
.fontColor(Theme.accentFg)
|
||||
.height(26)
|
||||
.margin({ left: 8 })
|
||||
.onClick(() => this.openNew())
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 6, right: 6, top: 4, bottom: 4 })
|
||||
.padding({ left: 10, right: 10, top: 6, bottom: 4 })
|
||||
|
||||
/* 周表头:顺序**必须**来自 weekdayLabels(WEEK_START),不许在这里硬编码 */
|
||||
Row() {
|
||||
@ -832,13 +990,20 @@ export struct CalendarPage {
|
||||
.padding({ left: 10, right: 10, top: 8, bottom: 2 })
|
||||
|
||||
if (this.selectedEvents().length === 0) {
|
||||
/*
|
||||
* 空态也要 `layoutWeight(1)` —— 原来只有 `padding(16)`,
|
||||
* 于是周/日档下网格只有一行,"这一天没有日程"贴在上面、
|
||||
* 下面留出一大片空白壁纸(实测 09-18:周视图整个下半屏是空的)。
|
||||
* 占满剩余高度后空态居中,三种档位的观感才是同一个版式。
|
||||
*/
|
||||
Column() {
|
||||
Text('这一天没有日程')
|
||||
.fontSize(Theme.fontSmall)
|
||||
.fontColor(Theme.textSubtle)
|
||||
}
|
||||
.width('100%')
|
||||
.padding(16)
|
||||
.layoutWeight(1)
|
||||
.justifyContent(FlexAlign.Center)
|
||||
} else {
|
||||
List({ space: 8 }) {
|
||||
ForEach(this.selectedEvents(), (e: CalendarEvent) => {
|
||||
|
||||
Reference in New Issue
Block a user