跨端: 鸿蒙端功能同步第一步——日历(只读月视图)上架,入口进底部导航
用户:「要给鸿蒙端做功能同步」。按 API 面盘点(WebUI 62 个 API 函数 vs 鸿蒙 38 个), 最大的用户面缺口是**日历**:纯逻辑(model/Calendar.ts)与判据早就在,一直没页面。 新增: - api/CalendarApi.ets:GET /calendar/events?from=&to=(与 WebUI 同参;区间按**网格**取, 不是月首月末 —— 首尾格子会显示邻月,只查当月会让那些格子永远空着) - pages/CalendarPage.ets:月网格(翻月/回今天)、点某天看当天日程、事件点、今天/选中两态、 加载失败说出来。**没做**:写侧(增删改)、农历重复、.ics、滑动翻页 —— 逐条写在文件头 - model/Calendar.ts:补 localIsoOf / hhmmAtOffset / deviceOffsetMinutes(偏移是入参 ⇒ 三时区可真跑) - model/Models.ets:CalendarEvent / CalendarListResponse(字段对齐服务端 JSON) - NavItems:加「日历」,底部成为 通信/日历/联系人 三项(与 WebUI 同序) - MainPage:日历是**常驻 pane**(visibility 控制),首次可见才拉数据;today 走 @Prop @Watch(visible) 在 pane 变可见时重算 ⇒ 结算欠账 calendar-today-recompute (DEBTS 15 笔 → 14 笔,余额里不再计这一笔) 判据:harmony-calendar 新增 6 条(网格/表头同源、事件归日走 localIsoOf、三时区钟点、 today 重算路径、翻月走 addMonths、变异自检);harmony-nav ② 分派与 ④ 让位跟着改成结构性判据 (④ 原来那个 400 字符窗口一加 pane 就红 —— 窗口式判据的又一次现身);harmony-logic 两处 「只剩两个平级页签」跟着改成三项。 真机实测(harmony-emu + hvigorw assembleHap + hdc install + uitest click + dumpLayout): 9 月网格星期对齐(周一起始,2026-09-01 落在「二」列)、事件点恰好在有日程的那 6 天 (11/17/18/24/25/30)、点 09-17 列出当天两条日程且钟点是本地时间(DB 里 02:20Z/08:30Z → 界面 10:20/16:30)。
This commit is contained in:
@ -85,8 +85,12 @@ test('① 点击:每个导航项点下去落到**它自己**那个 index(配
|
||||
test('② 挂载:index 决定挂哪个页面(点第 2 项必须挂联系人)', () => {
|
||||
/*
|
||||
* 换了条之后最容易出的错是"点了没反应"——点击改了状态,但内容仍写死挂第一个页面。
|
||||
* 所以把 index → 页面的映射钉死:0 → CommPage,否则 → ContactsTab,
|
||||
* 且两边都要收到 `bgActive`(背景开着时让出页面底,否则壁纸被内容盖住)。
|
||||
* 所以把 index → 页面的映射钉死:0 → CommPage、2 → ContactsTab、1 → CalendarPage,
|
||||
* 且三边都要收到 `bgActive`(背景开着时让出页面底,否则壁纸被内容盖住)。
|
||||
*
|
||||
* 日历与另两个不同:它**常驻挂载**(不是 if/else 的一支),用 `visibility` 控制显示 ——
|
||||
* 理由写在 `CalendarPage.ets` 与 build() 的注释里(today 跨午夜、保住"在看哪个月")。
|
||||
* 所以这里断言的是**两件事**:索引→页面的配对,以及日历那一支确实是"常驻 + 可见性开关"。
|
||||
*/
|
||||
const stack = main.slice(main.indexOf('Stack({ alignContent: Alignment.Bottom })'));
|
||||
// 只在**根 build()** 里数分派(页面内部也有 if/else,扫全文会数错)
|
||||
@ -96,13 +100,32 @@ test('② 挂载:index 决定挂哪个页面(点第 2 项必须挂联系人
|
||||
const first = braceBody(root, 'if (this.currentIndex === 0)');
|
||||
const second = braceBody(root.slice(root.indexOf('if (this.currentIndex === 0)')), 'else');
|
||||
assert.match(first, /CommPage\(\{ bgActive: this\.bgActive \}\)/, 'index 0 要挂通信页');
|
||||
assert.match(second, /ContactsTab\(\{ bgActive: this\.bgActive \}\)/, 'index 1 要挂联系人页');
|
||||
assert.match(second, /ContactsTab\(\{ bgActive: this\.bgActive \}\)/, 'index 2 要挂联系人页');
|
||||
/*
|
||||
* 分派必须只有这两支,且必须**穷尽** NAV_ITEMS —— 现在只有两项,用 if/else 就够了;
|
||||
* 一旦 NAV_ITEMS 变长,这条会红并提醒改成按清单挂载(否则第 3 项点了没内容)。
|
||||
* 联系人那一支必须**显式绑在 index 2**。注意条件在 `else if (...)` 里,
|
||||
* 不在 `else` 的**花括号正文**里 —— braceBody 只取正文,所以这条要看 root 原文。
|
||||
* 写成"否则就挂联系人"的后果:以后再加一项,新索引会被它静默接住(点了显示联系人)。
|
||||
*/
|
||||
assert.equal(N.NAV_ITEMS.length, 2,
|
||||
`NAV_ITEMS 现在是 ${N.NAV_ITEMS.length} 项,而内容分派只有 if/else 两支 —— 加了项必须同时加分派`);
|
||||
assert.match(root, /else if \(this\.currentIndex === 2\)/, '联系人必须显式绑在 index 2,不许用 else 兜底');
|
||||
/*
|
||||
* 日历(index 1):**常驻挂载 + visible 由索引驱动 + Visibility 开关**,三样缺一不可。
|
||||
* · `visible: this.currentIndex === 1` 是 `@Watch` 的触发源 —— 不带它,today 就不会在
|
||||
* pane 变可见时重算(DEBTS 的 calendar-today-recompute);
|
||||
* · `visibility(...=== 1 ? Visible : None)` 是显示开关;常驻 + 不隐藏 = 三个 pane 叠在一起。
|
||||
*/
|
||||
assert.match(root, /CalendarPage\(\{ bgActive: this\.bgActive, visible: this\.currentIndex === 1 \}\)/,
|
||||
'index 1 要挂日历页,并把"是否可见"传下去(它是 today 重算的触发源)');
|
||||
assert.match(root, /\.visibility\(this\.currentIndex === 1 \? Visibility\.Visible : Visibility\.None\)/,
|
||||
'常驻挂载就要用 visibility 控制显示');
|
||||
assert.ok(!/if \(this\.currentIndex === 1\)/.test(root),
|
||||
'日历不该写成 if/else 的一支:它是常驻 pane(卸载重挂会丢掉"在看哪个月",且 today 只能靠挂载重算)');
|
||||
/*
|
||||
* 分派必须**穷尽** NAV_ITEMS。2026-09-14 日历入口上架时这条**如约变红**
|
||||
* (当时是 `length === 2` + 两支 if/else)—— 加项必须同时加分派,否则第 3 项点了没内容。
|
||||
* 现在的形状与"三项"绑定:通信(0) / 日历(1) / 联系人(2),日历那一支**常驻挂载**。
|
||||
*/
|
||||
assert.equal(N.NAV_ITEMS.length, 3,
|
||||
`NAV_ITEMS 现在是 ${N.NAV_ITEMS.length} 项,与分派(通信/日历/联系人)不匹配 —— 加了项必须同时加分派`);
|
||||
// 内容挂在浮动条**下面**(先内容后条),否则条会被内容盖住、点不到
|
||||
const navAt = stack.indexOf('this.NavBar()');
|
||||
const contentAt = stack.indexOf('CommPage({ bgActive: this.bgActive })');
|
||||
@ -180,13 +203,31 @@ test('④ 悬浮 + 让位:自绘浮动层(留白/圆角/系统材质),
|
||||
assert.ok(N.NAV_CONTENT_RESERVE >= N.NAV_BAR_HEIGHT + N.NAV_BAR_BOTTOM,
|
||||
`内容让位(${N.NAV_CONTENT_RESERVE})必须 ≥ 条高 + 离底留白(${N.NAV_BAR_HEIGHT + N.NAV_BAR_BOTTOM})`);
|
||||
assert.match(main, /\.padding\(\{\s*bottom:\s*NAV_CONTENT_RESERVE\s*\}\)/, '内容底部要让出这段高度');
|
||||
// 让位要生效在**内容**上,不是条上(条自己 padding 不解决遮挡)
|
||||
const contentPad = main.slice(main.indexOf('Column() {\n if (this.currentIndex === 0)'));
|
||||
assert.match(contentPad.slice(0, 400), /\.padding\(\{\s*bottom:\s*NAV_CONTENT_RESERVE\s*\}\)/,
|
||||
'让位要加在挂载内容的那层上');
|
||||
/*
|
||||
* 让位要生效在**内容**上,不是条上(条自己 padding 不解决遮挡)。
|
||||
*
|
||||
* 这里原来是 `main.slice(定位).slice(0, 400)` 的**窗口式**断言 —— 2026-09-14 加日历
|
||||
* 那一支时它红了:内容层里多了一个 pane,`.padding(...)` 落到了 400 字符之外。
|
||||
* 判的是"同一件事",红的原因却只是"变长了",正是本仓库反复记的窗口式判据。
|
||||
* 改成**按花括号配对取正文**:内容层那个 Column 的 `{...}` 里必须出现让位。
|
||||
*/
|
||||
const contentOpen = main.indexOf('Column() {\n if (this.currentIndex === 0)');
|
||||
assert.ok(contentOpen > 0, '要能找到挂载内容的那层 Column');
|
||||
// `.padding(...)` 是**链在 `}` 之后**的,所以不能只看 {} 里面 —— 从配对结束处往后取到 NavBar 之前
|
||||
const braceAt = main.indexOf('{', contentOpen);
|
||||
let depth = 0;
|
||||
let end = -1;
|
||||
for (let i = braceAt; i < main.length; i++) {
|
||||
if (main[i] === '{') depth++;
|
||||
else if (main[i] === '}') { depth--; if (depth === 0) { end = i; break; } }
|
||||
}
|
||||
assert.ok(end > 0, '内容层的花括号要闭合');
|
||||
const chain = main.slice(end, main.indexOf('this.NavBar()', end));
|
||||
assert.match(chain, /\.padding\(\{\s*bottom:\s*NAV_CONTENT_RESERVE\s*\}\)/,
|
||||
'让位要加在挂载内容的那层上(链在那个 Column 的 } 之后)');
|
||||
});
|
||||
|
||||
test('★ 系统 `Tabs` 的 bar 已经不在(这一期换的就是它),且平级项仍是两项', () => {
|
||||
test('★ 系统 `Tabs` 的 bar 已经不在(这一期换的就是它),且平级项是 通信/日历/联系人', () => {
|
||||
/*
|
||||
* 反面:如果只是"加了自绘条但仍留着系统 bar",界面上会出现两条导航(且系统那条
|
||||
* 依然贴边、依然占位),这正是这一期要消除的东西。所以断言根导航里**没有**
|
||||
@ -194,7 +235,7 @@ test('★ 系统 `Tabs` 的 bar 已经不在(这一期换的就是它),且
|
||||
* 注意:**通信页内部**的三栏仍然用系统 `Tabs`(那是页面内部页签,不是根导航)——
|
||||
* 所以这条只在"根组件"的范围内断言,别扫全文。
|
||||
*/
|
||||
const root = main.slice(main.indexOf('build() {\n /*\n * 底部两项'));
|
||||
const root = main.slice(main.indexOf('build() {\n /*\n * 底部三项'));
|
||||
assert.ok(root.length > 300, '要能取到根 build() 的正文');
|
||||
const navCode = stripComments(root);
|
||||
assert.ok(!/\bTabs\(/.test(navCode), '根导航里不该再有系统 Tabs');
|
||||
@ -203,8 +244,8 @@ test('★ 系统 `Tabs` 的 bar 已经不在(这一期换的就是它),且
|
||||
// 反面之二:也不许把旧 builder 留着不用(留着就是死代码,下一个人会以为它还在生效)
|
||||
assert.ok(!/TabBarBuilder/.test(stripComments(main)), 'TabBarBuilder 已被 NavBar 取代,不该留在文件里');
|
||||
|
||||
// 平级项仍是两项且顺序不变(信息架构这一期不动)
|
||||
assert.deepEqual(N.NAV_ITEMS.map(i => i.label), ['通信', '联系人'], '平级项与顺序');
|
||||
// 平级项:通信/日历/联系人(日历是 P6 第 1 步做完后上架的,顺序与 WebUI 一致)
|
||||
assert.deepEqual(N.NAV_ITEMS.map(i => i.label), ['通信', '日历', '联系人'], '平级项与顺序');
|
||||
assert.equal(new Set(N.NAV_ITEMS.map(i => i.key)).size, N.NAV_ITEMS.length, '键要唯一(ForEach 的 key)');
|
||||
assert.deepEqual(N.NAV_ITEMS.map(i => N.navKeyAt(N.NAV_ITEMS.indexOf(i))), N.NAV_ITEMS.map(i => i.key),
|
||||
'navKeyAt 与清单一致');
|
||||
@ -212,6 +253,7 @@ test('★ 系统 `Tabs` 的 bar 已经不在(这一期换的就是它),且
|
||||
assert.equal(N.normalizeNavIndex(-1), 0);
|
||||
assert.equal(N.normalizeNavIndex(99), 0);
|
||||
assert.equal(N.normalizeNavIndex(1), 1);
|
||||
assert.equal(N.normalizeNavIndex(2), 2, '第三项(联系人)也必须归一到自己');
|
||||
assert.throws(() => N.navKeyAt(1.5) === undefined, undefined, '非整数下标不该悄悄生效');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user