Commit Graph

4 Commits

Author SHA1 Message Date
bcd7e7f217 跨端: 鸿蒙日历补 .ics 导入导出(WebUI 有、鸿蒙一直没有)
WebUI 工具条那两个入口(`CalendarView` 的导入/导出图标)鸿蒙侧一直缺,
`CalendarPage` 的文件头也一直诚实写着"没做:…….ics 导入导出"。

## 为什么不能照抄 WebUI 的做法

WebUI 在浏览器里:导出是 `Blob` + `<a download>`、导入是 `<input type=file>`,
两条都由浏览器提供。鸿蒙**没有"下载目录"这个概念**,必须显式走系统
`DocumentViewPicker` —— 这不是多此一举,是这个平台上唯一能让用户拿到/指定文件的路。
新增 `common/IcsFile.ets` 封装选/读/写(`pickIcsText` / `saveIcsText`)。

## ApiClient 加两个方法(不能复用 `request<T>`)

`request<T>` 无条件 `JSON.parse(response.result)`,而 iCalendar 不是 JSON
(与 `getBytes` 取壁纸二进制同一个理由)。所以:
- `getText()`:`expectDataType: STRING` 取原文;
- `postText()`:raw body + **显式** `Content-Type: text/calendar`
  —— 服务端是**按 Content-Type 分流**的(`strings.HasPrefix(ct, "multipart/")`,
  否则当 raw text)。写错成 `application/json` 会走对分支但语义不对。

## 三处对齐 WebUI 的细节

- **区间用屏幕上正在看的那段**(`rangeFrom/rangeTo`),不写死 ±1 年 ——
  服务端注释里写着同一条理由:「写死 ±1 年会让人点导出后得到一堆与屏幕上不符的事件」。
- **文件名带日期**:`agentmail-<selectedIso>.ics`(WebUI 是
  `agentmail-${dayKey(anchor)}.ics`)。固定名的问题是连导两次就分不清哪份是哪份,
  而导出天然会被重复做(看一个月导一份)。实测截图里默认文件名
  `agentmail-2026-09-18.ics`。
- **`imported` 与 `skipped` 两个数都报**:只说"导入成功"会把
  "20 条里跳过了 18 条"读成一切正常,而"跳过"正是用户需要知道的那部分。

## 结果要分三种说(取消不是失败)

导出成功(给出落盘路径)/ 用户取消(**什么都不说**)/ 真失败(说原因)。
把"取消"弹成"导出失败"是"用户什么也没做却被骂一句"。
导入成功后**重拉当前区间** —— 不重拉用户看不到刚导进来的东西,会以为失败。

## 验证

- 服务端两端点实测(curl):导出 8 个 VEVENT、Content-Type/Disposition 正确;
  把导出的原样导回去 `{"imported":8,"skipped":0,"total":8}`。
- ★ 这次验证**污染了生产库**(那 8 条真写进去了):已按 event_id 逐条删除
  (47 → 39),删除前备份 `/tmp/db-before-cleanup.db`。
  教训:拿生产实例做写侧验证要先想清楚怎么回滚 —— 我这次是先写后想。
- 设备侧:导出选择器实测打开(系统 filemanager 的 `PathPicker`),
  默认文件名正确、目录可选;选中保存后回到日历。
2026-09-18 12:55:46 +08:00
2da38bba83 农历走服务端端点:换算只在服务端做一次(两边各写一遍天文算法迟早差一天)
用户定的方案:「加 api 端点」。

## 为什么不移植到 ArkTS

`lunar-javascript` 的 `lunar.js` 有 **43 万字节**,内部是**日月位置的级数展开**
(实测:全文件最大的数字字面量是 16KB 的系数数组,**不是**"某年到某年的月长表")。
即"照搬一张小数据表"这条路**不存在** —— 移植等于在 ArkTS 里再实现一遍天文算法。
两份实现迟早会在某个闰月或某个朔日上差一天,而那种错**表现为日期错位、不是报错**,
界面上完全看不出(用户得自己去查日历才知道)。

所以:`GET /api/v1/calendar/lunar?from=&to=`(服务端 `internal/lunar`,同一作者的 lunar-go)。

## 形状是「按日期键索引的映射」,不是数组

客户端拿到 `map[iso] -> 标签` 直接按格子键查,不用自己遍历比对。
`text` 字段是**格子里直接显示的那个串**(初一=月名、其余=日名)——
由服务端定,两端同源。客户端各拼一份的话,同一天在两边日历上可能长得不一样
(例如闰月到底写不写「闰」)。

几个刻意的取舍:
- **不设默认 from/to**:默认范围会让「我要 3 月」与「服务端以为我要这个月」悄悄不一致;
- 入参只收 `YYYY-MM-DD`(**日期键**,不是 RFC3339):农历是"这一天是农历几号"的
  纯日期语义,混用时间戳会被时区挪一天;
- 换不出来的日子**不进 map**(客户端查不到 ⇒ 那格不显示农历),而不是塞空对象 ——
  空对象会让客户端以为"有农历、只是没内容";
- 区间上限 400 天(不是安全边界,是防客户端传十年前到十年后)。

## 判据(`server/internal/handler/lunar_test.go`)

参照物是服务端的 `internal/lunar`(权威实现),**不抄一份答案表** —— 库升级时判据跟着走。
钉的点各自对着一个会静默出错的错法:
- `Full` 与权威实现逐字一致(5 个日期,含春节、跨世纪、29 天月的边界年份);
- 日名表覆盖 1..30 且**五种前缀形态都在**(WebUI 那版漏过「二十」);
- ★ 初一显示**月名**、其余显示**日名**(与 WebUI `cellLunarLabel()` 同一口径);
- 闰月必须带「闰」字(不带的话闰六月与六月在格子里一样);
- 极端值(公元 1 年 / 1900 / 2100 / 9999)**不许 panic**,且换出来时文字里不许含
  「无效/NaN」这类失败标记。

★ 最后一条我第一版**写错了**:断言「`time.Time{}` 应当换不出来」,实测库**换得出来**
  (0001-01-01 → 「〇年冬月十八」)—— 我断言的是自己的想象而不是实际行为。
  改成断言真正的契约(不 panic / 失败就不给 / 给了就得是真结果)后才对。

## 客户端

`LunarLabels` / `LunarRangeResponse` 两个模型 + `CalendarApi.listLunar(fromIso, toIso)`;
`CalendarPage` 在 `loadEvents()` 之后**不 await** 地拉农历(附加信息不该拖慢事件列表),
失败**不算 `this.error`**(否则"农历服务抖一下"会变成"整个日历打不开"),
只写 hilog 留痕。区间按**网格**取(不只本月 —— 月视图首尾显示上/下月格子)。
2026-09-18 11:23:12 +08:00
pi
d3f6f3baf6 跨端: 鸿蒙日历写侧(新建/编辑/删除/暂停恢复)+ 表单不再是一面滚轮墙
用户:「是的去吧」(批准上一封列出的下一步)。

- CalendarApi:createEvent / updateEvent / deleteEvent(POST /calendar/events、
  PUT/DELETE /calendar/events/{id});新建与编辑发**同一份** CalendarEventInput
  —— 服务端两个端点同形(CreateCalendarEvent 里那个 Status 字段的注释就写着这句),
  拆两份会在严格解码下 400
- CalendarPage:写侧表单(新建与编辑共用一份)、二次确认删除、暂停/恢复走 update
  (调度器只触发 active;没有暂停就只能删掉重加,而那会丢 event_id 与历史)
- 服务端 400 文案原样显示(收件地址无法解析:…)—— 建事件时就校验地址,
  吞成保存失败等于让人猜,而猜的代价是以为设好了、实际永远发不出去
- 空标题/空收件人在本地就挡住,不发注定 400 的请求
- 提交的是**时间戳**(isoTimestampOfLocal),不是日期键;编辑回填走 localDateOf
- ★ 表单形状:日期/时间选择器合起来 ~1400px,展开着放会把标题与收件人挤出屏幕 ——
  改成一行摘要 + 点开展开(默认收起)。这不是美观问题:展开态下保存在屏幕外,
  要跨过一整面滚轮才够得着
- 判据 +7(端点/载荷/前置校验/文案/二次确认/时间戳往返/变异自检),23 条全绿
- 真机实测(hvigorw + hdc + uitest 逐字段输入 + 逐键点击,每一步都回库核对):
  建 → 库里 event_time 是 2026-09-28T01:00Z(= 本地 09:00 整,默认值正确)、
  暂停 → status=paused、删除第一下只变确认删除(库里还在)、第二下才真删(库里 0 条)
2026-09-14 18:57:53 +08:00
pi
94ba4b9c58 跨端: 鸿蒙端功能同步第一步——日历(只读月视图)上架,入口进底部导航
用户:「要给鸿蒙端做功能同步」。按 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)。
2026-09-14 18:38:51 +08:00