diff --git a/client/electron/test/harmony-calendar.test.mjs b/client/electron/test/harmony-calendar.test.mjs index c3d4221..edbf12e 100644 --- a/client/electron/test/harmony-calendar.test.mjs +++ b/client/electron/test/harmony-calendar.test.mjs @@ -385,3 +385,95 @@ test('★ 判据自检:写侧的错误写法必须判红', () => { assert.ok(!/put/.test("post('/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 周视图下"这一天没有日程"贴顶、下半屏全空'); +}); diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index 15ac45f..cbd636c 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -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 的具名入口 diff --git a/client/harmony/entry/src/main/ets/model/Calendar.ts b/client/harmony/entry/src/main/ets/model/Calendar.ts index f76e6e2..8eac3be 100644 --- a/client/harmony/entry/src/main/ets/model/Calendar.ts +++ b/client/harmony/entry/src/main/ets/model/Calendar.ts @@ -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; +} diff --git a/client/harmony/entry/src/main/ets/pages/CalendarPage.ets b/client/harmony/entry/src/main/ets/pages/CalendarPage.ets index 862e2a5..51365d5 100644 --- a/client/harmony/entry/src/main/ets/pages/CalendarPage.ets +++ b/client/harmony/entry/src/main/ets/pages/CalendarPage.ets @@ -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) => {