## 问题 用户:「首页点悬浮按钮进日程,加载得好几秒」。 实测数据:`calendar_events` 2026 条 + 328 条 rrule,按范围展开后 **28628 条**。 ## 根因:渲染路径上有 O(n²) | 函数 | 每次成本 | 调用次数 | |---|---|---| | `lunarOf(iso)` | 遍历 28628 条 **×2 遍**(先找节日、再找农历) | 月视图每格 + 周视图每天 + 列表每天 | | `visibleEvents()` | **新建数组** + 遍历 28628 条 | `eventsOf()` 每次调用,列表按天调 | 月视图 42 格 × 2 遍 × 28628 ≈ **240 万次比较**,全部同步跑在 build 路径上。 ## A:索引化(客户端) - 新增 `ensureIndex()`:**一次遍历**同时建 `lunarIdx`(iso→农历文字) 和 `dayIdx`(iso→当天条目,已排序) - `lunarOf()` 改为 O(1) 查 Map - `eventsOf()` 改为 O(1) 查桶,返回副本(防止调用方 sort/push 改到索引) - 缓存失效:`evtVer` 计数,`allEvents` / `books` 变化时 `++`; `idxVer` 记录索引对应版本,不一致才重建 ⚠️ **语义必须一致**:旧 `lunarOf` 是「先找 traditional/solarterm/holiday, 找不到才用 kind=lunar」。建索引时用两个 Map 分级收集、最后按"节日优先"合并, 否则会出现「初一」盖掉「中秋」。 保留了这些特例: - `calId === 0` 的衍生条目不受"本可见性"控制 - `books` 为空时全部通过 - 桶内按 `byTime` 排序 ## B:范围收窄 + 服务端上限 ### 客户端 `CAL_FULL_FROM/TO` 写死的 `1970-01-01 ~ 2100-12-31`(47846 天) 改为 `calFullRange()` 动态算「今年 ±5 年」(`CAL_SPAN_YEARS = 5`)。 理由: - 服务端要为**每一天**生成农历条目 ⇒ 130 年范围要传上万条(约 6MB) - 实际使用只看眼前几年 - 动态计算而非写死年份,避免随时间推移失效 - 缓存键仍带 from/to ⇒ 跨年后范围平移、旧缓存自然失效 ### 服务端 `app/Holiday.php`(三处都修) | 函数 | 原问题 | 修法 | |---|---|---| | `lunarRange()` | `$i < 400` ⇒ 只算前 400 天,**今天的农历空** | 去上限,改年数上限 30 年 + 夹 Lunar 支持范围 | | `traditionalRange()` | 同样 400 天 ⇒ **今年春节/中秋全丢** | 同上 | | `solarTermRange()` | 只取首末两年 ⇒ 130 年仅 2 年节气 | 改逐年遍历 + 年数上限 | `MAX_LUNAR_YEARS = 30` 是防御性的:客户端只要 ±5 年, 留余量足够;同时防止有人手拼 1900~2100 把服务器拖死(算 7 万天农历需数十秒)。 ## 验证 ### 服务端实测(部署后) ``` 今天 2026-11-02 → 「廿四」 ← 修复前为空 本月农历覆盖 30/30 天 今年 11 个传统节日、24 个节气 ← 齐全 2021~2031 农历 4017 条(应 4017) ← 修复前只有 400 条 2021~2031 节气 264 条(应 264) ← 修复前只有 48 条 1970~2100 农历 10958 条,0.47 秒(按 30 年上限截断,符合设计) ``` > ⚠️ 中途发现改动**没上传到服务器**就跑了验证,一度得出"已修好"的错误结论。 > 比对 md5 才定位到。现在 `_tooling/check_cal_perf.js` 里有这项检查(7.1)。 ### 静态校验 `_tooling/check_cal_perf.js`:**36 项全过**,含 - 渲染路径无全表遍历 - 索引语义与原实现一致(8 项) - 缓存失效正确 - 范围收窄且动态 - 服务端三处上限已修 - 服务器文件 md5 与本地一致 ### 编译 `build_hap debug` → `BUILD SUCCESSFUL` ### 未验证 **真机加载耗时未测**(设备当时未连接)。理论提速约 84 倍 (240 万次比较 → 28628 次遍历 + 42 次查表),但实际感知需真机确认。 ## 顺带记录 服务端 `Holiday.php` 需要**单独上传**(它不在客户端仓库里)。 以后改服务端文件,验证前必须先比对服务器 md5,否则会拿旧代码验出假结果。
FamilyBook_harmonyos · 家书鸿蒙客户端
家书(FamilyBook)的 HarmonyOS 客户端。 服务端是那套「自托管的家庭生活系统」,这个仓库是它的手机 / 平板端。
把一个家的「人」「日子」「去过的地方、看过的电影」收进同一本书里。
当前状态
界面框架阶段(v0.1.0) —— 骨架与全部模块页面已就位,数据全部是演示数据。 服务端还没有客户端用的接口,所以这一轮只做「长什么样」,不做任何读写。
| 已完成 | 说明 |
|---|---|
| 工程骨架 | Stage 模型 + ArkTS / ArkUI,targetSdk 26.0.0 / compatibleSdk 6.1.1(24) |
| 沉浸式 | 照本机 SyncCalendar 的方案:系统栏全透明 + 根容器 expandSafeArea,通知栏与页面完全同色 |
| 左侧抽屉 | 宽屏常驻侧栏 / 窄屏滑出层,两套形态同一组件 |
| 12 个模块页面 | 首页、名册、观影、旅程、日程、资源、任务、纪念日、财务、读书、笔记(+ 家谱) |
| 附加页面 | 通知消息、个人设置 |
| 主题体系 | 宣纸米 / 砖红配色,深浅色两套资源,深浅自动跟随系统 |
| 模块主题色 | 与 Web 端 Module::PRESET_COLORS 同一套 12 色预设 |
| 编译验证 | hvigorw assembleHap 0 error / 0 warning |
| 真机验证 | 装机成功;15 个页面逐个点开、断言渲染内容,15/15 通过 |
| 待办(下一轮) | 说明 |
|---|---|
| 接口对接 | 服务端需要提供客户端用的 JSON 接口(登录、各模块列表与详情) |
| 登录态 | 会话持久化、令牌刷新、启动时的登录门禁 |
| 交互实现 | 新建 / 编辑 / 删除、搜索、筛选、拖拽排序 |
| 日程周/天视图 | 按半小时定位的时间轴 + 手势(本轮只做了月视图) |
| 附件能力 | 上传、图片/视频预览、裁剪 |
| 财务折线图 | 净资产与各账目的盘点变化曲线 |
与 Web 端的取舍
数据看 Web 端,排版按手机重做 —— 不照搬电脑端版面。
模块
不做「导航」模块(Web 端有)。用户原话:「导航是专门为电脑端开发的,都是网站,
主要用在浏览器的主页,所以不需要」。手机上每个网站都有 App,收藏夹没有使用场景。
⇒ 本工程的模块清单与 Web 端 Module::LIST 有意不等,别"顺手补回去"。
「家谱」不在抽屉里(与 Web 端一致):它默认不上导航条(用户说过 「不需要每天都查看家谱」),走「名册 → 家谱」的分段切换进入。
版面
| 模块 | Web 端 | 鸿蒙端 | 为什么 |
|---|---|---|---|
| 首页 | hero 招牌 + 信息面板 + 日程/任务并排 + 我的家人 + 未来七天 + 模块卡 | 去掉 hero;信息面板作无卡片的页面头部;其余保持顺序上下堆叠 | hero 只重复信息面板已有的信息(家庭名 × 问候),手机首屏高度宝贵 |
| 观影 | 瀑布流海报墙 | 两列定尺寸网格 | 瀑布流列宽随图高浮动,手机上很难扫 |
| 任务 | 多列看板(拖动改状态) | 按状态分组的三段列表 | 手机宽度放不下多列;用户关心的是"做到哪一步" |
| 笔记 | 定尺寸卡片网格 | 两列定高卡片网格 | 与 Web 端一致(用户要求过"块大小要确定") |
| 家谱 | HTML 折线树 | 「以我为中心的圈层」(直系 / 其他亲属) | 折线树在窄屏排不下 |
| 财务 | 概览 + 折线图 + 账目表 | 概览(图占位)+ 按类别分组的账目 | 折线图需要图表能力,留到第二轮 |
目录结构
entry/src/main/ets/
entryability/EntryAbility.ets 应用入口:主题跟随、系统栏配色
entrybackupability/ 备份扩展
common/
Theme.ets 设计令牌(颜色 / 间距 / 字号)+ 模块调色板
Store.ets 全局状态(V2:@ObservedV2 + @Trace)
model/
Modules.ets 模块注册中心(对应 Web 端 App\Module)
Types.ets 全部数据类型(对应各模板渲染的字段)
DemoData.ets 演示数据(接上接口后可整个删除)
components/
Widgets.ets 共享展示件(头像 / 标签 / 空态 / 圆点…)
Drawer.ets 左侧抽屉 + 骨架(并排 / 浮层两形态)
TopBar.ets 顶栏
pages/
Index.ets 主界面:模块分发
home/ movies/ trips/ calendar/ tasks/ people/
resources/ days/ ledger/ books/ misc/ settings/
设计说明
沉浸式(三件套,缺一不可)
参考本机另一个应用 SyncCalendar(用户指定)的做法:
- 系统栏涂成全透明 ——
statusBarColor: '#00000000'、navigationBarColor: '#00000000'; - 窗口底色 = 页面底色 ——
win.setWindowBackgroundColor(pageBg),供透明系统栏透出; - 紧邻系统栏的元素不设自己的背景色 —— 顶栏不能有
backgroundColor, 让它透出根容器底色;根容器再expandSafeArea([SYSTEM], [TOP, BOTTOM])把背景铺到状态栏区域。
原理:透明系统栏透出的是窗口底色,而顶栏也透出同一个底色 ⇒ 天然同色、 「通知栏与上方完全融为一体」。内容仍在安全区内,不需要手算状态栏高度。
⚠️ 常见的两个坑(都踩过):
- 调
setWindowLayoutFullScreen(true)后子组件会钻到状态栏底下,页面得自己让位, 极易算错(曾出现顶部重复避让 156px); - 给状态栏或顶栏写死颜色必然与相邻层割裂(曾出现「米色状态栏 + 白色顶栏」的拼接感)。
验证方式:逐行像素采样(_tooling/familybook_immersion_check.py)。
判据是「状态栏区颜色 == 顶栏区颜色」,用数字裁定而不是目测 ——
最终实测 y=0..271 全为 #F7F3EA(纸色),达标。
Web 端顶栏是一排横向模块入口,最多放 12 个模块 —— 手机宽度下必然溢出。 改成左侧抽屉后,竖向排列不受数量限制,还能顺带放下「模块说明 + 主题色」 这层信息,一眼看清每个模块是干什么的。
抽屉有两套形态,由窗口短边决定(≥ 600vp 为宽屏):
- 宽屏(平板 / 折叠屏展开 / 横屏手机):抽屉常驻,与内容并排;
- 窄屏:内容铺满,抽屉从左侧滑出,点遮罩或选中模块即收起。
判据用「短边」而不是「宽度」—— 短边在旋转时不变,所以比例接近 1:1 的设备 横竖屏布局保持一致。
为什么是抽屉
Web 端顶栏是一排横向模块入口,最多放 12 个模块 —— 手机宽度下必然溢出。 改成左侧抽屉后,竖向排列不受数量限制,还能顺带放下「模块说明 + 主题色」 这层信息,一眼看清每个模块是干什么的。
抽屉有两套形态,由窗口短边决定(≥ 600vp 为宽屏):
- 宽屏(平板 / 折叠屏展开 / 横屏手机):抽屉常驻,与内容并排;
- 窄屏:内容铺满,抽屉从左侧滑出,点遮罩或选中模块即收起。
判据用「短边」而不是「宽度」—— 短边在旋转时不变,所以比例接近 1:1 的设备 横竖屏布局保持一致。
模块注册中心
模块清单只有一个来源:model/Modules.ets 的 FB_MODULES,
与 Web 端 app/Module.php 的 Module::LIST 一一对应(key 必须等于路由名)。
加一个模块只改那一处 —— 抽屉、设置页的显隐开关、主题色选择全部自动出现。 这是 Web 端踩过的坑(漏改一处就「有页面但导航里找不到入口」),端上照搬这套结构。
主题色
每个模块一个主题色,三档(按钮底 / hover / 文字色)由主色按 HSL 压明度算出来,
保证三档永远同色系。算法与 Web 端 Module::cssVars() 共用同一套公式。
预设色板 FB_PRESET_COLORS 也与 Web 端 Module::PRESET_COLORS 逐值一致 ——
否则同一个人在手机和电脑上选出的「观影紫」会是两种紫。
编译期约束(踩过的坑)
| 坑 | 处理 |
|---|---|
Circle().fill() 是 API 26 才有的重载 |
compatibleSdk 是 24,改用「方块 + 圆角」画圆(components/Widgets.ets::FbDot),零版本风险 |
setWindowBackgroundColor 对 phone/tablet 不可用 |
去掉,页面自身已用同色铺满整屏 |
window.getLastWindow() 只有异步版 |
由 EntryAbility 用 getMainWindowSync() 取一次存进 AppStorage,页面侧同步读 |
跨文件顶层名冲突(arkts-no-decl-merging) |
所有导出名统一加 Fb 前缀 |
Button('label') { ... } 报 10905202 |
一律用 Button() { Row() { ... } } |
@Param 类型写成 string 导致传 Resource 报错 |
颜色类入参统一声明 ResourceColor |
| 嵌套目录下 import 相对路径层级 | 页面在 pages/<模块>/,公共件在 components/、model/,相对路径要多退一级 |
构建
需要 HarmonyOS SDK(DevEco Studio 自带)。本机开发时:
export NODE_HOME="<DevEco>/tools/node"
export DEVECO_SDK_HOME="<DevEco>/sdk"
<DevEco>/tools/ohpm/bin/ohpm install --all
<DevEco>/tools/hvigor/bin/hvigorw assembleHap --mode module -p product=default
产物在 entry/build/default/outputs/default/entry-default-unsigned.hap。
local.properties 里写本机 SDK 路径,不入库(见 .gitignore)。