laoyang
|
051257e3be
|
fix(日程): 居中弹层补齐背景色,之前透明看不清内容 (#52)
## 现象
时间选择框弹出来后没有背景色,"啥都看不见"。
## 根因
日程页的三个弹层(时间选择 / 重复范围 / 重复截止)原来靠 **bindSheet 自带的背景色**
(配置里的 `backgroundColor: $r('app.color.card_bg')`)。
#51 改成自绘居中弹层后,这三个 @Builder 的根 Column 只有
`.width('100%')` 和 `.padding(...)`,**没有设置 backgroundColor** →
弹层透明,背后的表单内容直接透出来,与滚轮文字糊在一起。
待办页的 dueDialog 当时写对了(有 backgroundColor),所以只有日程页出问题。
## 修法
三个弹层统一改为居中卡片样式:
.width('86%')
.padding(18)
.borderRadius(16)
.backgroundColor($r('app.color.card_bg'))
## 真机验证(5NC0225B27004718)
像素采样:
| 位置 | 实测颜色 | 期望 |
|---|---|---|
| 弹层内 y=750/900/1200/1500 | (255,255,255) 纯白 | 卡片背景 ✅ |
| 弹层按钮 y=1650 | (0,126,255) 品牌蓝 | 确定按钮 ✅ |
| 弹层外 y=200/400 | (149,150,152) 灰 | 遮罩压暗 ✅ |
| 弹层外 y=2000/2200 | (145,146,148) 灰 | 遮罩压暗 ✅ |
内白外灰,对比清晰,内容可读。
|
2026-11-05 11:22:43 +08:00 |
|
laoyang
|
daa577e88e
|
feat(待办/日程): 两页时间选择器统一居中弹出 + 待办截止行去掉「修改」字样 (#51)
## 1. 日程页时间选择器也改为居中弹出
用户要求:「新建日程那里,开始和结束时间也用这种方式弹出来吧」
与待办页保持一致:根节点改 Stack,弹层居中浮在内容之上。
移除 bindSheet(SDK 的 SheetOptions 没有 position/alignment,只能底部弹)。
三种弹层(时间选择/重复范围/重复截止)继续共用同一个 sheetMode 切换。
### 真机验证(5NC0225B27004718)
弹层 y=713~1691,中心 1202;屏幕中心 1222,偏差 20px ✅
## 2. 待办页截止时间行去掉「修改」二字
用户要求:「修改那两个字没必要,直接点那个时间框,出现选择时间的地方」
整行本来就可点,右侧改为一个 `›` 箭头作视觉暗示(与日程页「开始/结束时间」行一致)。
## 3. 修复:纯待办本的 event 通道误落库(导致 Inbox 待办消失 + 编辑报错)
### 根因
`SyncEngine` 的 event 通道用 `DavClient.reportEtags(href)`,**不区分本的 COMP_SET 类型**。
对纯待办本(如群晖 Inbox,COMP_SET=todo)也照常跑 event REPORT:
- 服务器返回了文件列表(里面其实是 VTODO 文件)
- `parseEvents` 解析出 0 条日程,**但文件仍被落库成 kind='event'**
- 结果:本地库出现"属于待办本的日程记录"(实测 id=748 title=Hvf)
- 用户新建的「测试」待办进不来,反而被这条假日程占用 cal_key
### 后果
- 待办列表看不到(列表只查 kind='todo')
- 点它编辑走日程通道 → 报「需要联网 + 日历本需可写」
- 用户反馈的「编辑待办失败」由此而来
### 修法
event 通道前判断 `acc.calendarComps[i] === 'todo'` 就跳过,
日志明确输出「是纯待办本(COMP_SET=todo),跳过 event 通道」。
### 验证
日志实测:`日历本[1]「Inbox」是纯待办本(COMP_SET=todo),跳过 event 通道` ✅
## 编译
BUILD SUCCESSFUL
## 遗留
Inbox 的 VTODO REPORT 在群晖上仍返回空 multistatus(68 字符),
「测试」待办能否同步需用户重新建一条再验证。
|
2026-11-05 11:14:46 +08:00 |
|
laoyang
|
39337a5530
|
feat(待办): 选择器改为居中弹层 + 对勾改矢量绘制 (#50)
## 1. 时间选择器改为屏幕居中弹出
### 用户诉求
「点完开始之后,在中间弹出一个框来选择时间,操作起来更方便」
### 为什么不用 bindSheet
查 SDK `SheetOptions`(common.d.ts:11753)确认:**没有 position / alignment 参数**,
bindSheet 只能从底部弹,无法居中。
### 做法:自绘居中覆盖层
把 build() 根节点改为 `Stack`:
Stack(alignContent: Center) {
Column { 顶部栏 + Scroll 表单 } ← 原有内容
if (sheetShow) {
遮罩(半透明,点空白关闭)
centerDialog() ← 居中卡片
}
}
选这条路而不是 CustomDialogController,依据是本工程 MEMORY:
系统弹层在本环境曾"静默失败"(Index.ets:1421 已记录),
首启引导页 tipsOverlay 就是改用自绘覆盖层才可靠的。
## 2. 对勾改为矢量 Path
### 问题
原来用字符 `✓`(U+2713),三个毛病:
- 字体依赖:不同设备粗细/形状不同
- 粗细不可控:fontWeight 对符号字形影响有限
- 居中不稳:字形基线导致视觉重心偏移
### 改法
用 `Path` 画折线,配 strokeWidth + 圆头圆角:
commands('M6 12.5 L9.5 16 L16.5 8') // 22vp 列表
commands('M5 9.5 L7.5 12.5 L13 6.5') // 18vp 时间轴
得到粗细均匀、端点圆润、可精确居中的勾。
两处统一:待办列表勾选圈、时间轴待办区勾选圈。
## 编译
BUILD SUCCESSFUL
## 真机验证(5NC0225B27004718)
| 验证项 | 实测 |
|---|---|
| 弹层居中 | 弹层 y 750~1728,中心 **1239**;屏幕中心 1222,偏差仅 17px |
| 对比改造前 | 中心 1800(偏下 578px)→ 现在偏 17px |
| 日期/时间滚轮 | `DatePicker 2026年11月5日` + `TimePicker 09:00` 正常渲染 |
| 对勾 Path 数量 | 1 个 `[96,2013][165,2082]` |
| 字符 ✓ 残留 | 0 个 |
|
2026-11-05 10:55:15 +08:00 |
|
laoyang
|
007c112af2
|
fix(待办): 补上截止时间选择器和父任务选择器弹层 (#49)
## 现象
新建待办时点「截止时间 - 修改」没反应;点「父任务 - 选择」也没反应。
## 根因
迁移到独立页面(#47)时,只搬了字段 UI 和状态变量,
**两个选择器弹层本身漏写了**:
`pickShow` / `parentPickShow` 只被赋值为 true,
但 build() 里没有任何 UI 消费它们 → 赋值等于空操作。
对比原弹层版本:时间选择器是叠在弹层之上的一个 if 块;
迁到页面后需要改成 bindSheet,这一步遗漏了。
## 修复
按 EventEditPage 的做法补两个弹层,**共用唯一一个 bindSheet**:
⚠️ 关键约束(EventEditPage:105 记载的坑):
ArkUI 同一组件挂多个 bindSheet 时,后一个会覆盖前一个,
实测表现就是"白框一闪而过、什么都没出来"。
所以用一个 `sheetShow` + `sheetMode`:
sheetMode='due' → DatePicker + TimePicker + 确定
sheetMode='parent' → 父任务列表 + 无父任务 + 取消
## 改动点
- 加 `sheetShow` / `sheetMode` 状态
- 「修改」→ sheetMode='due'
- 「选择」/整行 → sheetMode='parent'
- 根容器挂唯一 bindSheet,`bottomSheet()` 按 mode 分发
- 新增 `confirmDue()`:把 DatePicker/TimePicker 的值合成毫秒并置 hasDue=true
## 编译
BUILD SUCCESSFUL
## 静态自检
- bindSheet 实际调用 1 处(861 行;79 行是注释)
- DatePicker ×1、TimePicker ×1
- confirmDue 定义 + 调用
- sheetMode 赋值 3 处(默认值 + 两个入口)
## 未验证
真机(安装时设备离线)。需验证:时间滚轮能弹、选完确定后截止时间更新。
|
2026-11-05 09:47:45 +08:00 |
|
laoyang
|
aa3187f7d9
|
fix(待办): 待办编辑页补齐沉浸式,与新建日程完全一致 (#47)
## 问题
用户指出:新建日程是沉浸式的(状态栏背景与页面同色),
而新建待办页顶部标题栏区域是白的,与状态栏割裂。
## 根因
EventEditPage 的根容器有两行关键设置,TodoEditPage 漏了:
.padding({ top: 12 })
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
`expandSafeArea` 让**背景**延伸到状态栏/导航条区域(只扩绘制范围、不移动子组件),
`padding top:12` 给状态栏留视觉空间。缺了前者,根容器高度被限制在安全区内,
状态栏下方露出独立色块 → 视觉断层。
## 验证(像素级对比,设备 5NC0225B27004718)
| 采样点 | 新建日程 | 新建待办(修复后) |
|---|---|---|
| 状态栏 y=0~160(x=540) | 全部 (242,243,245) | 全部 (242,243,245) |
| 标题栏区平均色 | (229,232,236) | (229,232,236) |
| 取消按钮 bounds | [50,158][174,267] | [50,158][174,267] |
| 标题 bounds | [427,180][653,246] | [427,180][653,246] |
| 保存按钮 bounds | [906,158][1030,267] | [906,158][1030,267] |
三项 UI 元素 bounds 与两处背景色采样**完全相同** → 沉浸式效果一致。
## 踩坑记录
`hdc file recv` 拉截图会失败(产物只有 30 字节);
`hdc shell cat` 拉二进制同样损坏。
改用 `hdc shell "base64 -w 0 <path>"` 再本地解码,可靠。
|
2026-11-04 19:29:52 +08:00 |
|
laoyang
|
fe7945d300
|
refactor(待办): 待办编辑改为独立页面,与新建日程一致 (#47)
## 用户诉求
「做成跟新建日程一样,就是打开一个新的页面。左上角取消,右边保存,
所有信息拉下来。」
之前是 Index 里的底部弹层(todoEditorOverlay),即使加了内部 Scroll,
形态仍与日程页不一致。
## 改动
### 新增 pages/TodoEditPage.ets
完全对齐 EventEditPage 的结构:
- 固定顶部栏:`取消`(左)· `待办标题`(中)· `保存`(右)
- 中间整页 Scroll 表单:标题/日历本/截止时间/重复/优先级/提醒/标签/地点/
父任务/附件/备注
- 编辑态底部「删除待办」,与日程页一致
### 传参方式
与日程页一致用 AppStorage:
`pendingTodoId`(0=新建,>0=编辑)
`pendingTodoDate`(新建时预填截止日)
### Index.ets 改动
- `openTodoCreator` / `openTodoEditor` 改为 `router.pushUrl` 跳转
- 删除死代码 104 行(旧弹层的两处入口逻辑体)
- 注册路由 `pages/TodoEditPage`
### 注意点
`CalSource` 没有 `href` 字段,需从账号按 `calKey` 反解;
且要按 `comps` 过滤掉纯日历本(写不进 VTODO)。
## 编译
BUILD SUCCESSFUL
## 真机验证(5NC0225B27004718)
| 验证项 | 实测 |
|---|---|
| 独立页面 | 日志 `pages/TodoEditPage finish inPage transition` ✅ |
| 顶部栏布局 | 取消 x=50、标题 x=427(居中)、保存 x=906 ✅ |
| 表单字段 | 日历本4个/截止/重复/优先级/提醒 全部渲染 |
| 滚动 | 上滑后 附件 y=1668、添加文件 y=1668、备注 y=1760 全部出现 ✅ |
| 顶部栏固定 | 滚动前后 取消/标题/保存 的 y 完全不变(122/143/122)✅ |
|
2026-11-04 19:26:11 +08:00 |
|
laoyang
|
22d67e8a9d
|
fix(待办): 新建/编辑弹层改为「固定头 + 可滚动字段 + 固定底部按钮」(#46)
## 现象
新建待办弹层字段越来越多(日历本/标题/截止/循环/优先级/提醒/标签/地点/
父任务/附件/备注),整块 Column 一路往下铺,总高超出屏幕后**滑不动**,
底部「取消」「创建」按钮点不到,导致无法建单。
## 根因
弹层外层 `Column({ space: 12 })` 只有 `width('100%')`,**没有高度约束,也没有 Scroll**。
内容超过屏幕高度时直接溢出,超出部分既不可见也不可滚。
## 改法(参照新建日程页 EventEditPage 的做法)
1. 外层 Column 加 `.constraintSize({ maxHeight: '88%' })`
—— 内容少时自动收缩,内容多时不再顶出屏幕
2. 标题行保留在 Scroll 之外(固定可见)
3. 所有字段包进 `Scroll` + `Column`,`layoutWeight(1)` 吃掉剩余高度
4. 「取消/创建」按钮移出 Scroll,固定在底部
结构变成:固定头 → Scroll(字段) → 固定底部按钮,与日程编辑页一致。
## 编译
BUILD SUCCESSFUL
## 未验证
真机(设备锁屏),需解锁后验证:能滚动到备注、取消/创建可点。
|
2026-11-04 19:17:21 +08:00 |
|
laoyang
|
8497cfeb3f
|
feat(待办): 投射到日历视图 —— 日时间轴顶部显示"当天截止"待办区 (#45)
待办只有 DUE 没有 DTSTART,无法画时间轴色块;历史上 buildEventDaySet()
明确"待办不计入月视图"。本次按方案 A:在日视图时间轴顶部加「当天截止」待办区。
## 显示
- 位置:日时间轴(月/周视图选中日下方 + 列表视图每天卡片)顶部、
全天/跨天区之上,随内容一起滚动
- 每行:勾选圈(颜色跟随日历本)+ 标题(完成态划线)
- 只有「当天截止 + 未完成 + 有截止时间」的待办才显示,按截止时间升序
## 实现
- `DayTimeline` 加 `todos` 字段
- `Index.todosDueOn(dateMs)` 筛选当天截止待办
- `rebuildDayTimeline()` / `buildAgendaTimelines()` 两处填充
- `DayTimelineView` 加 `onToggleTodo` 回调 + 待办区渲染
- 勾选直接复用 `Index.toggleTodo`(级联完成、提醒重排等既有逻辑全部生效)
## 刷新
toggleTodo 成功后除 reload this.todos,还重建 dayTimeline / agendaTimelines
—— 否则待办区是快照,勾选后不消失/不划线。
## 验证
- 编译 BUILD SUCCESSFUL
- 筛选逻辑等价验证:只显示(当天截止+未完成+有截止),按时间升序,边界正确
- 真机未验(设备离线)
## 未验证
真机:勾选圈交互、待办区随数据刷新、列表视图卡片里的待办区显示。
|
2026-11-04 17:44:56 +08:00 |
|
laoyang
|
d9db8b476a
|
feat(待办): 子任务(RELATED-TO)+ 附件(内联 base64 ATTACH)(#43 #44)
补齐待办模块最后两项协议能力。
## #43 子任务(父子结构)
### 协议
`RELATED-TO;RELTYPE=PARENT:<父uid>`,值用**父任务 UID**(不是本地 id——
父子关系是服务器资源属性,跨设备同步时 UID 才稳定)。
### 数据层
- `LocalEvent` / `RemoteEvent` / `DisplayEvent` 加 `parentUid`
- 表加 `parent_uid` 列,`toBucket`/`fromRow`/`applyRemote` 双向映射
### UI
- 列表:子任务缩进 34vp + 标题前「∟」连线符
- 父任务副行追加子任务进度「N/M」
- 编辑弹层加「父任务」行 → 覆盖层选择(候选 = 未完成顶层任务,
排除自己及后代,防成环)
- **父任务勾选完成 → 级联完成所有未完成子任务**(用户选定的语义)
## #44 附件(内联 base64)
### 查证 + 实测(已更新 #44 工单)
- iCalendar RFC 5545 原生支持 ATTACH,两种形式:URI / **内联 base64**
- RFC 8607 托管附件需服务器特殊支持(群晖没有)
- **实测群晖**:PUT 带 `ATTACH;...;ENCODING=BASE64;VALUE=BINARY:<b64>` → 201,
GET 回原文原样保留(4558 字符整行、含中文文件名「报告.pdf」都不丢)
### 实现(按标准协议,直接上传同步)
- 附件 = VTODO 的 ATTACH 行(内联 base64),随 .ics 一起 PUT 同步
- 不需要自己传文件贴链接
- `buildTodo` / `rewriteTodoFields` 读写 ATTACH;`parseTodos` 解析回填
- 数据层 `attachments` 存 JSON([{name,mime,base64}]),base64 直接存库零转换
- UI:弹层加「附件」区,文件选择器选文件 → 读二进制 → base64 编码 → 随待办保存
### 限制
最多 3 个附件、单文件 ≤5MB(内联 base64 会让 .ics 膨胀,限制防爆)。
### 折行决策
**不折行**。原因:① 工程现有 SUMMARY/DESCRIPTION/LOCATION 一贯整行不折;
② 群晖实测 4558 字符整行也能存;③ RFC 折行是 SHOULD 非 MUST,一致性优先;
④ 折行若按 75 octets 硬切会把 UTF-8 中文名(「报告.pdf」)拦腰切断成乱码。
## 兼容性
`rewriteTodoFields` 新增 `touchParent` / `touchAttach` 哨兵:
编辑只改标题时传 false,服务器上**其他客户端写的 RELATED-TO / ATTACH 原样保留**;
只有用户实际编辑了父任务/附件才 touch=true 重写。
## 验证
- 编译 `BUILD SUCCESSFUL`
- 协议往返(Python 等价复刻):生成→解析 无损,附件 base64 还原正确
- 服务器端到端:PUT(201) + GET 原样返回 RELATED-TO + 中文文件名 ATTACH
## 未验证
真机 UI(设备离线)。需人工验:子任务缩进、级联完成、文件选择器上传、
附件随待办同步到另一客户端。
|
2026-11-04 17:19:43 +08:00 |
|
laoyang
|
b623a876dd
|
fix(待办): 修正行高异常(我误读了测量单位,绕了三轮)
## 现象
待办列表每一条都非常高,几乎占满半屏。
## 根因
做 #42(编辑入口扩大热区)时,我给内容区 Column 加了 `.height('100%')`。
在 ListItem 里,百分比的参照系不是"行内容高度",而是被拉伸的可用高度 ⇒
单条 ListItem 被撑到 **1790px(573vp)**。
## 修正
删掉 `.height('100%')`,热区改由外层 Row 的 `padding(10)` 承担 ——
行高恢复由内容自然决定。
同时把待办列表从 `ListView + ListItemGroup + ForEach(Column)`
重构为**扁平 List**(大标题/分组头/条目摊平成一维数组),
因为原先改成 List 后分组渲染需要 ListItemGroup;扁平化后
分组视觉完全保留(分组头就是一行普通 ListItem),结构更可控。
## ⚠️ 我这轮绕了远路,记录下来
**误把 px 当 vp。** 屏幕 1080px ÷ 3.125 密度 ⇒ 1vp = 3.125px。
| 测量 | 换算 | 当时我的判断 | 实际 |
|---|---|---|---|
| 1790px | 573vp | 异常 ❌ | 对,确实异常 |
| 176px | **56vp** | 以为异常,继续改 | **正常**(两行文字+padding)|
| 117px | 37vp | 以为异常 | 正常(探针单行)|
| 275px | 88vp | 以为"修好了" | 我强行加 height(88) 造成 |
于是白白做了三轮"优化"(加 constraintSize、改删除按钮固定高、
给 ListItem 加 height(88)),每次都让行高更高,最后全部回退。
教训:**dump 出来的 bounds 是 px,改代码用的是 vp,必须先换算再判断**。
## 编译
`BUILD SUCCESSFUL`
## 真机验证
全部待办行统一 **56.3vp**(176px):未完成 1 条、已完成 6 条一致;
分组头 21.8vp、大标题 15.4vp,层级清晰。
|
2026-11-04 08:44:27 +08:00 |
|
laoyang
|
413900bbce
|
feat(待办): 提醒 / 左滑删除 / 优先级标签地点 / 编辑热区 / 卡片刷新 (#38 #39 #40 #41 #42)
一次性完成待办模块的 5 项缺口。
## #38 待办提醒
`ReminderService` 原本只服务 `kind='event'`,待办完全不提醒。
- `EventDb.queryRemindableTodos`:查未完成待办,**额外要求 `completed=0`**
(否则做完的待办到期还会弹)
- `refreshReminders` 合并日程 + 待办两路查询,文案按 kind 分支:
待办是「将在 X 分钟后到期」/`该待办已到期`,日程仍为「开始」
- `IcsUtil.buildTodo` 写 VALARM;`rewriteTodoFields` 编辑时整段重建 VALARM
(逐行改 TRIGGER 容易漏 ACTION/DESCRIPTION)
- 待办弹层加提醒芯片(不提醒 / 5 / 15 / 30 分钟 / 1 小时 / 1 天,多选)
- 三个写操作后调 `refreshReminders`,保证改完立刻重排
协议要点:VTODO 的 VALARM `TRIGGER` 相对 **DUE**(RFC 5545 §3.8.6.3),
所以 `TRIGGER:-PT5M` 就是"到期前 5 分钟",无需额外写 RELATED=END。
## #39 待办列表左滑删除
`swipeAction` 只能挂 ListItem,而原结构是 `Scroll + Column + ForEach`。
- 外层 Scroll → List,分组改用 `ListItemGroup`(保留"色点+本名+条数"分组头)
- 每个 ListItem 挂 `swipeAction({ end: ... })`,滑出红底「删除」
- 点击复用现有 `askDeleteTodo`(二次确认)→ `doDeleteTodo`(软删 + 服务器 DELETE)
- 未完成 / 已完成两个清单都支持
## #40 优先级 / 标签 / 地点
- 表加 `priority`(INTEGER) / `categories`(TEXT);`location` 本来就有
- PRIORITY 存 **iCal 原生值**(1 最高 … 9 最低,0 未设置),写回零转换
- ICS:`parseTodos` 读 PRIORITY/CATEGORIES,`buildTodo` 与 `rewriteTodoFields` 写
- 新增 `todoExtras()` 供新建与改写共用,避免两处生成结构不一致
- `rewriteTodoFields` 用 `priority=-1` 做"不改"哨兵(0 是合法语义:清空)
- 弹层加优先级芯片(高/中/低)、标签输入、地点输入
## #41 重复待办打勾推进
核查结论:**已实现且完整**。`toggleTodo` 已按 `recurRule !== ''` 分流到
`completeRecurringRound`,且该方法会保留"基准→DUE 偏移"、复位 STATUS 后推进。
本次未改逻辑,仅补了完成后刷新卡片与提醒。
## #42 编辑入口热区
内容区 `Column` 原来高度只到文字高度,行内上下留白点不到。
加 `.height('100%')` + `justifyContent(Center)` 撑满行高。
勾选圈仍是独立 onClick(ArkUI 不冒泡),点圈只切完成态、不进编辑。
## 顺带修复(共性遗漏)
`toggleTodo` / `saveTodoEdit` / `saveNewTodo` / `doDeleteTodo` 四处
改完库只刷 `this.todos`,**都没推送桌面卡片** —— 卡片停在旧数据。
四处统一补 `CardDataService.pushToAllForms`。
## 验证
- 编译 `BUILD SUCCESSFUL`
- 真机 dump 确认弹层新 UI 全部渲染:优先级(高/中/低)、
提醒(不提醒/提前5分钟/15/30/1小时/1天)、标签、地点
- ICS 生成逻辑用等价实现校验:PRIORITY、CATEGORIES(逗号转义 \,
LOCATION、VALARM(TRIGGER:-PT5M 位于 VTODO 内、结构闭合)全部通过
## 未验证
端到端「填字段→创建→查服务器 ICS」未跑通:`uitest inputText` 按坐标输入时
多个输入框会累积到同一个(把标签/地点内容追加进了标题框),是自动化限制而非代码问题。
需人工建一条带各字段的待办,确认服务器端 ICS 与上述结构一致。
提醒能否实际弹出依赖代理提醒权限(此前有配额为 0 的记录)。
|
2026-11-04 08:23:42 +08:00 |
|
laoyang
|
c0b9b44ca2
|
fix(粘贴): URL 提取按括号配对裁剪,修复高德链接被截断导致地址丢失
## 现象(用户实测)
粘贴高德分享链接后,识别出的地点"只有一半,只有一部分"。
## 根因
高德分享链接里的 POI 名称含**未编码的半角括号**:
...%E9%9D%92%E5%B2%9B%E8%8F%9C(黄岛珠江路店)%2C武夷山路118号...
↑ 就是这个 ( 和 )
而 URL 提取正则是 `/https?:\/\/[^\s"'<>()【】]+/i`(当时还排除了半角 `)`),
于是一到第一个 `)` 就停:
提取长度 221 / 原文 456 ← 只拿到不到一半
__p 切成 4 段(应为 5 段)
名称 = "船歌·鱼水饺青岛菜(黄岛珠江路店" ← 括号不闭合
地址 = 空 ← 第 5 段整个丢失
## 为什么当初排除括号
本意是防止把 URL 后跟着的中文括注一起吞掉,如
`看这个 https://x.com/a(路线图在这里)`。
但规则太粗暴,误伤了 URL 内部的合法括号 —— RFC 3986 允许 `(` `)` 出现在 query。
## 修法
1. 正则放宽:只排除空白 / 引号 / 尖括号 / **全角**括号(全角括号不会被编码进 URL,
出现在明文里必然是用户自己加的中文括注)
2. 新增 `trimUrlTail()`:从左扫描数括号,`depth < 0` 说明遇到多余的右括号,
那里才是真正的 URL 边界
3. 再裁掉末尾可能粘上的中文标点(。,、;!?)
## 验证(用用户提供的真实 URL 跑等价逻辑)
修复后:
提取长度 780 / 原文 780 ← 完整
__p 段数 5
[0] B02140WITR
[1] 35.95086086125046
[2] 120.19152268767353
[3] 船歌·鱼水饺青岛菜(黄岛珠江路店) ← 括号完整
[4] 武夷山路118号中南·海湾新城(珠江路金石国际广场旁) ← 地址回来了
边界场景:
"https://x?a=1(路线图)" → https://x?a=1 ✅ 中文括注被裁掉
"https://x?a=1) 后面有字" → https://x?a=1 ✅ 多余右括号被裁掉
同时确认另外两处 `split(',')` 都用了 `slice(2).join(',')` 拼回,
地址内部含逗号时不会被切错。
## 编译
`BUILD SUCCESSFUL`
## 未验证
真机端到端(写剪贴板 → 点粘贴 → 看结果)需人工操作:
hdc 没有可用的剪贴板写入工具(`paste` 是 Linux 文本合并命令,非剪贴板服务)。
解析逻辑本身已用真实 URL 在等价实现上验证通过。
|
2026-11-04 07:36:51 +08:00 |
|
laoyang
|
b660a986dc
|
fix(卡片): 打勾后刷新所有卡片,解决"切换卡片仍是旧数据"
## 问题
在 A 卡片上完成一个待办,划到 B 卡片,B 里还是那条待办;
必须在 B 上再完成一个,B 才刷新。
## 根因
`completeTodoFromCard(formId, localId)` 两处推送都只针对 `formId`:
this.pushDataExcludingTodo(formId, localId);
this.pushData(formId);
每张卡片是**独立的 formId 实例、各持一份数据**,`updateForm` 只作用于传入的那一个。
所以其它卡片完全收不到更新,直到它们各自被触发(用户点了别的待办)才重拉。
## 解法
改用 `CardDataService.pushToAllForms()`,它会遍历所有已注册 formId 逐个推送。
两个关键点:
**① 乐观更新必须保留**
点勾要立刻有视觉反馈。`pushToAllForms` 会重新查库,此刻库里还是未完成状态,
直接推会让该条又冒出来 ⇒ 看起来"点了没反应"。
所以给 `pushToAllForms` 增加可选参数 `excludeTodoId`,先推一次"排除该条"的数据
(对所有卡片生效)。
**② 时序**
点击 → pushToAllForms(excludeId) 所有卡片立即移除该条
→ setCompleted(GET+PUT) 真正落库推服务器
→ pushToAllForms() 所有卡片推真实数据
成功:该条已完成,不在未完成列表
失败:该条重新出现,用户能看出没成功
## 顺带修掉一个隐患
`completeTodoFromCard` 从 void 改为 async Promise 后,`onFormEvent`(同步回调)
里的调用必须兜住 reject —— 否则未捕获的 Promise 会让 FormExtension 进程异常
(该进程生命周期只有 10 秒,异常会直接导致本次卡片更新丢失)。已补 `.catch`。
## 未改的地方(有意为之)
`onAddForm` / `onUpdateForm` / 翻页(`onFormEvent` 的 prev/next/today)仍只推单卡:
- 翻页只该影响被翻的那张卡,不该把所有卡片拽到同一天
- 系统定时刷新由各卡片各自的 formId 触发,天然独立
数据最终一致性由"同步完成 → pushToAllForms"兜底。
## 编译
`BUILD SUCCESSFUL`
## 未验证(需桌面上有卡片)
本机当前没有注册的卡片(formIds 为空),无法触发 `onFormEvent` 实测。
逻辑改动明确(单卡推送 → 全卡推送),但"切换卡片是否立即同步"需在
添加 ≥2 张待办卡片后实测确认。
|
2026-11-04 07:10:28 +08:00 |
|
laoyang
|
8b8b69b287
|
style(粘贴/待办卡片): 粘贴按钮与「地图」同款外观;待办卡片对齐 App 内样式
## 1. 粘贴按钮改成与「地图」一样
原来只用系统默认样式,与旁边的「地图」按钮风格割裂。
安全控件支持 fontSize / fontColor / backgroundColor / borderRadius / padding
(见 security_component.d.ts),所以可以完全对齐:
13 号字 · brand_text 色 · card_bg 底 · 圆角 12 · 高 44
与「地图」并排、同高、同样留白。文字内容由系统枚举决定
(`PasteDescription` 只有 `PASTE` 一个值,即「粘贴」)。
真机实测两个按钮并排:
PasteButton [680,1802][837,1940] 文字='粘贴'
Text(地图) [862,1802][1018,1940] 文字='地图'
同高同排,13 号字。
## 2. 待办卡片列表对齐 App 内样式
用户指出:卡片里的待办样式(灰勾选框 + 色条)与 App 内待办列表不一致,
两套设计没必要。App 内 `Index.todoRow` 的样式是:
- 勾选圈 **颜色跟随日历本**(未完成=空心环 2px,已完成=实心+✓)
- 标题 15 号,已完成加删除线
- 副行 12 号 = 截止时间 · 日历本
- 行背景 card_bg,圆角 12,1px 边框
卡片侧改为同一套结构,只按卡片尺寸把号数收小:
| 卡片 | 圈 | 标题 | 副行 |
|---|---|---|---|
| 4×4 / 6×4 | 18 | 13 | 10 |
| 2×4 | 16 | 12 | 9 |
去掉原来的色条和灰色勾选框 —— 用日历本色圆圈,与 App 内视觉一致。
数据侧给 `TodoCardItem` 增加 `subText`(「截止时间 · 日历本」拼好的副行文字),
由数据层统一拼接,避免三张卡片各拼一遍导致不一致。
## 一致性校验
逐行 diff 确认 4×4 与 6×4 的 `todoRow` **完全一致**(归一化类型名后逐字符相等);
2×4 结构相同,仅尺寸小一档。
## 编译
`BUILD SUCCESSFUL`
## 未验证
粘贴按钮已确认渲染与布局(dump 坐标/文字),视觉观感需上机目视;
待办卡片需删除桌面旧卡片后重新添加才能看到新样式。
|
2026-11-03 23:23:25 +08:00 |
|
laoyang
|
b8d5aa630c
|
fix(卡片/粘贴): 5 项反馈修复 —— 粘贴按钮、卡片数据、卡片加号、卡片滚动、列表统一
## 1. 粘贴按钮文字看不清(EventEditPage)
根因不是"忘了设文字"——空参时系统本就给默认样式(LINES 图标 + PASTE 文案)。
真正原因是我**给了一个和系统文字颜色太接近的背景色**(card_bg 近乎白)。
查 SDK 类型定义 `paste_button.d.ts`,错误码 2 明确列出:
「字体或图标颜色与组件背景颜色相近」「文字超出组件背景区域」等
安全组件创建时会做样式校验,对比度不足会让文字灰淡难辨、甚至授权失败。
同时发现 icon/text/buttonType **只能走构造参数**,不是链式属性
(`.text()` 会编译报 "Property 'text' does not exist")。
改为:
```
PasteButton({
icon: PasteIconStyle.LINES, // 该枚举只有 LINES
text: PasteDescription.PASTE, // 该枚举只有 PASTE
buttonType: ButtonType.Capsule
})
```
并**去掉自定义背景色**,交给系统默认样式,只保留尺寸与圆角。
## 2. 待办卡片显示 0 条(CardDataService)
`pushToAllForms()` 是同步完成后刷新所有卡片的主路径,
但它只调 `buildCardData()`(日程),**从未调 `buildTodoCardData()`** ⇒
推给卡片的 binding 里 todoCount / todoListJson 全是默认值。
而 EntryFormAbility.pushData() 里合并了待办 —— 两条路径不一致,
所以表现成"有时对、有时 0"。
修:抽出 `mergeTodoInto()` 供两处共用;待办数据与 dayOffset 无关,
整个 pushToAllForms 只构建一次。
真机日志:`待办卡片数据刷新:未完成 5 条…列表输出 5 条`(原为 0)。
## 3. 待办卡片右上角 + 点了没反应(EntryAbility + Index)
待办卡片发的是 `widgetAction: 'addTodo'` / `'openTodo'`,
但 EntryAbility 只判断 `raw === 'add'`,Index 也只处理 `'add'`
⇒ 参数被静默丢弃。
修:两端都补上 addTodo / openTodo。
addTodo → 切待办 Tab + 打开新建待办
openTodo → 切待办 Tab
## 4. 日程 2×4 卡片右侧可滚动,15 条(CardDataService + Widget4x2)
数据侧 `slice(0, 2)` → `slice(0, 15)`
视图侧 `Column + ForEach` → `List + ListItem`(虚拟化 + 滚动惯性 + 回弹)
真机日志:`进行中 15 条`,确认上限生效。
## 5. 待办 4×4 / 6×4 列表样式统一,全部可滚动
按用户选择,**统一成 4×4 样式**(勾选框 + 色条 + 标题 + 副行「截止时间 · 日历本」)。
6×4 原来是「标题 + 独立截止时间列 + 独立日历本列」,窄卡片里会把标题挤没,
改为与 4×4 完全相同的结构。已用逐行 diff 校验:两卡 todoRow **36 行完全一致**。
`MAX_TODO_ITEMS` 20 → 15,与日程卡片口径一致(所有待办卡片都已可滚动,
不必再按卡片尺寸给不同上限)。
## 编译
`BUILD SUCCESSFUL`
## 未验证
真机仅验证了「待办卡片数据不再为 0」与「日程 15 条上限」两项。
粘贴按钮视觉、卡片内滚动、卡片 + 跳转、两卡样式一致性需上机目视确认。
|
2026-11-03 23:14:43 +08:00 |
|
laoyang
|
2bb620a6ed
|
fix(日历本): 按 COMP_SET 类型分别列出 —— 待办只给待办本,日程只给日历本
上一个提交把"全部可写本"铺开,导致新建待办时连纯日历本(娱乐/生活/工作/家庭)
也列出来,新建日程时又把纯待办本列出来。用户选错类型时服务器会拒绝写入,
或写进去但永远不在对应列表显示。
## 依据
`DavAccount.calendarComps[i]` 早就在库里存着服务器 COMP_SET 声明:
'event' 只有 VEVENT
'todo' 只有 VTODO
'both' 两者都支持
'unknown' 未声明(旧数据)
但 `CalSource` 没透传它,界面层拿不到类型,只能按"可写"一刀切。
## 改动
- `CalSource` 增加 `comps` 字段
- `CalendarDataService.loadSources` 从 `acc.calendarComps[i]` 透传(空串归 'unknown')
- 待办侧(`Index.openTodoCreator`):排除 `comps === 'event'`
- 日程侧(`EventEditPage.CalendarDataBridge.loadWritableDavSources`):排除 `comps === 'todo'`
**'unknown' 两端都保留**:旧数据没有 COMP_SET 信息,武断排除会让老用户
升级后发现日历本"消失"。
## 真机验证(5NC0225B27004718)
服务器实际类型(PROPFIND 实测):
| 本 | COMP_SET | 待办列表 | 日程列表 |
|---|---|---|---|
| 娱乐 / 生活 / 工作 / 家庭 | VEVENT | 不显示 | 显示 |
| Inbox / 家庭计划 / 工作任务 / 个人成长 | VTODO | 显示 | 不显示 |
改后实测:
- 新建待办 → 只列「群晖:Inbox / 家庭计划 / 工作任务 / 个人成长」4 个 ✅
- 新建日程 → 只列「群晖 · 娱乐 / 生活 / 工作 / 家庭」4 个 ✅
两端的本数量都从 8 降到 4,与服务器声明完全一致。
## 编译
`BUILD SUCCESSFUL`
|
2026-11-03 23:04:00 +08:00 |
|
laoyang
|
ca97ec7fcd
|
feat(待办): +号职责感知 + 新建待办(所有可写日历本铺开选择)
在「待办」视图下点 + 新建待办,其余视图仍新建日程。
新建时把全部可写 CalDAV 日历本铺开供选择,交互与新建日程一致。
## 原状
`TodoService` 只有 改/删/设重复/打勾,**没有新建方法**;
`saveTodoEdit` 开头 `if (... || this.todoEditId <= 0) return;` 直接挡掉新建。
+号固定 `addEvent()`,在待办页点它却跳到日程编辑页,与当前语境不符。
## 改动
### IcsUtil.buildTodo(新增)
从零构造 VTODO 资源。不能复用 `rewriteTodoCompletion`——后者是在服务器原文上
打补丁(保留未知字段),前提是原文已存在;新建时没有原文。
字段取舍:
- UID / DTSTAMP / SUMMARY 必写
- DESCRIPTION 为空则不写
- DUE 可选;只有日期时写 `DUE;VALUE=DATE`
- STATUS 固定 NEEDS-ACTION(完成态由 setCompleted 切换)
- 不写 PRIORITY,避免凭空替用户设优先级
⚠️ 时间格式必须与现有待办写入保持一致:本工程 `rewriteTodoSchedule` 用的是
`DUE:` + `fmtIcsTime(dueMs, false)`,即**浮动本地时间、不带 Z**。
若这里写成 UTC,同一字段出现两种格式,服务器上会显示成早 8 小时的待办。
### TodoService.createTodo(新增)
**先 PUT 服务器、成功后才落库**——与 updateTodo 的顺序相反。
更新时本地已有行可回滚;新建若先落库再 PUT 失败,会留下"本地有、服务器没有"
的孤儿待办,用户看着它存在却永远同步不出去。
同时校验:标题非空、href 非空、账号可定位、日历本可写(只读本提前拦下,避免 403)。
### Index.ets
- `todoCreating` 状态区分新建/编辑
- `openTodoCreator()`:收集全部可写 dav 本;默认选中 Inbox(名字含
inbox/收件箱 者优先),找不到用第一个 —— 少点一步但仍可改
- `saveNewTodo()`:走 createTodo
- +号分流:`mode === 'todo'` → 新建待办,否则新建日程
- 弹层标题「新建待办」/按钮「创建」,新建时隐藏"删除"入口
- 日历本区:新建时 Flex 铺开色块胶囊(48% 宽,选中染本色调),
编辑时保持原来只读显示
`CalSource` 没有 href 字段,`createTodo` 需要的 URL 由 calKey(`<accountId>_<index>`)
反解出账号与索引后从 `account.calendarHrefs[index]` 取,映射存在 `todoBookHrefs`。
## 真机验证(5NC0225B27004718)
待办视图点 + → 弹层标题「新建待办」、8 个日历本全部铺开、
按钮显示「创建」。填标题后点创建:
```
PUT .../synccalendartest/home_todo/todo-1793717941310-bl6ffh.ics → 201
待办新建成功 id=749 → todo-1793717941310-bl6ffh.ics
```
- 默认落到 `home_todo`(Inbox)✅ Inbox 智能匹配生效
- 服务器端复查该资源返回 **200** ✅ 真的创建了
- App 列表即时刷新:未完成 5→6 项,出现「群晖:Inbox(1)」✅
测试数据已删除(DELETE → 204),未在服务器留残留。
## 编译
`BUILD SUCCESSFUL`,本次改动无新增警告。
|
2026-11-03 23:00:07 +08:00 |
|
laoyang
|
09e2373965
|
docs(README): 更新到 0.2.0 现状,补「自行编译上架需申请哪些权限」
README 停留在 0.1.0 且有多处与代码不符,本次逐项核对后更新。
所有结论均以 module.json5 / AppScope / SettingsPage.ets / release profile 实测为准。
## 修正的过时内容
- 版本号 0.1.0 (10000) → **0.2.0 (10200)**
- 待办(VTODO)描述错误:「只读展示,不参与推送」
→ 实际 0.2.0 已由 `TodoService` 实现新建/编辑/打勾/删除全量双向推送,
且写回时先 GET 原 .ics 原地改写,保留服务端未知属性
- 设置项一览与界面对不上
→ 按 SettingsPage.ets 实际分组重写为「系统日历 / 通用 / 数据 / 关于」四组,
补上此前漏掉的「系统日历中的显示名称」「清除系统日历数据」「日历本只读·静音」
## 新增
### 📝 最近更新
补 v0.2.0 与 v0.1.0 两个版本条目,让版本变化可追溯。v0.2.0 记录:
待办双向同步、3 项同步核心修复(删除 404 / 防复活 / 404 物理清库)、反馈渠道迁移。
### 🔐 权限清单与用途(独立小节)
用表格列清 5 个权限的类型、用途、申请时机。之前只在技术要点里一行带过,
读者无法判断哪些要额外动作。
### 🔑 自行编译并上架:权限要怎么处理
这是本次重点。回答「fork 后自己上架需要申请哪些权限」:
1. **必须自己重走的**:READ_WHOLE_CALENDAR(ACL)、PUBLISH_AGENT_REMINDER。
并明确一个大坑:**ACL 绑定 bundleName**,本项目已获批的 ACL 只对
`synccalendar.yangyq.net` 有效;fork 后改了包名(几乎必然要改)就立即失效,
必须用新包名重新申请。很多人会误以为代码里声明了就自动继承。
2. **ACL 怎么验证落地**:给出 openssl 解 p7b + 读 allowed-acls 的命令。
ACL 不在 module.json5 里,而在签名 Profile 里——这正是我自己这次判断失误的地方,
写进文档避免后来者重蹈。
3. **不想申请 ACL 的替代方案**:删掉权限,日历经 READ_CALENDAR 仍可读本应用写入的日历,
只是看不到其他来源。社区实践过的合规绕行。
4. **其他审核注意点**:图标 alpha 通道要求、审核员无 CalDAV 账号需 Demo 模式、
隐私政策与 module.json5 必须一致。
## 未改动
- 构建/签名章节(与当前 build-profile.json5 一致)
- 隐私、功能特性正文、相关链接(链接已是新的 Feedlite 地址)
README 353 → 479 行。
|
2026-11-03 09:55:06 +08:00 |
|
laoyang
|
bcaa578ebe
|
docs(验证): 补记 9a67fce 的真机验证结果(3 条修复全部通过)
9a67fce 的提交说明里写着「真机未验证」,现已完成验证并全部通过。
本提交仅补充文档,不改代码。
## 设备与环境
- 设备: 5NC0225B27004718 (HarmonyOS)
- 服务器: DAViCal 1.1.9 @ nas.yangyq.net/caldav.php/synccalendartest
## 关键发现:该服务器天然测不出此 bug
DAViCal 的命名规则是「uid 做 URL 编码后直接当文件名」:
uid : 20261102T153543-10c5d3cb@169.254.186.221
文件名 : 20261102T153543-10c5d3cb%40169.254.186.221.ics
猜的名字 == 真名,同步 746 条后 remotePath 全部等于 uid 猜测值,
无论修复是否生效都看不出差异。必须造「文件名 ≠ UID」的资源才能验出。
## 陷阱资源
直接 PUT 一个刻意不匹配的资源:
文件名 : trap-file.ics
UID : trap-uid-with-special-chars@example.com
服务器实测:
tfxnz/trap-file.ics → 200 存在
tfxnz/trap-uid-with-special-chars%40example.com.ics → 404 不存在
⇒ 旧代码删除时必然打到 404。
## 验证结果
### 1. remotePath 存真名 ✅
实际存储: trap-file.ics
旧代码会: trap-uid-with-special-chars%40example.com.ics
### 2. 删除打到正确地址并成功 ✅
DELETE .../tfxnz/trap-file.ics → 204
日志: 推送删除 uid=trap-uid-with-special-chars@example.com → .../trap-file.ics
### 3. 服务器端确实删掉 ✅
trap-file.ics → 404
REPORT tfxnz 资源数 → 0
### 4. 本地物理清库 ✅
本地库 trap 行数 → 0
### 5. 防复活白名单未误伤 ✅
日志: 新增0 更新0 删除0 不变1
同 uid 始终 1 行,未插重复
## 完整同步结果(746 行)
home 716 lvirlu 9 ycbco 7 cojiujq 7
tfxnz 3 zlqlzm 2 tdotbsnl 2
## 踩坑记录:拉库必须连 WAL
首次拉库读到 735 行、remotePath 全等于 uid 猜测值,差点误判修复失效。
真实原因是 WAL 未合并:
sync_calendar.db 344 KB
sync_calendar.db-wal 428 KB ← 最新数据全在这里
只拉主库会看到过期快照,必须连 -wal / -shm 一起拉。
## 清理
陷阱数据已彻底清除(服务器 404、本地 0 行),测试日历无残留。
详见工单 #8。
|
2026-11-03 09:06:02 +08:00 |
|
laoyang
|
9a67fce296
|
fix(同步): 修复删除 404、远端覆盖本地修改、404 残留无法清理 #6
采纳社区反馈(小褚的 fork)中的 3 处高危修复。均为纯逻辑修复,
不改 UI 架构、不动状态管理范式。
## 1. remotePath 用服务器真实文件名(删除/推送全链路)
### 缺陷
上传/删除 URL 里的资源名来自 `remotePath`,而它一直被硬编码成
`encodeURIComponent(uid) + '.ics'` —— **猜的**。服务器不保证文件名等于 UID:
家书 CalDav.php 用 `preg_replace('/[^A-Za-z0-9._-]/', '_', $uid)` 生成文件名,
UID `movie-188@familybook` 的真实资源叫 `movie-188_familybook.ics`。
猜错 ⇒ DELETE 打到不存在的路径 ⇒ 404 ⇒ 服务器上删不掉(用户看到"删了又回来");
PUT 同理会写到另一个文件名下,造成同一事件两个资源。
### 修法(三条链路都要覆盖)
1. `RemoteEvent` 新增 `remotePath` 字段
2. **GET 补拉**:`r.remotePath = fileName`(从 REPORT 的 href 末段取)
3. **etag 未变复用**(常态路径,之前漏了):`rowToRemote` 增加第三个参数,
传当前 href 的真实文件名。不补这里,前一条等于白做 ——
因为大部分资源 etag 不变、根本不走 GET。
4. `applyRemote` 落库:优先用 `r.remotePath`,为空才退回按 uid 猜
(本地新建等服务器上还没有资源的场景)。
5. 更新分支同步 `found.remotePath`:服务器上资源改名后要跟着改,
否则后续 DELETE/PUT 仍打旧路径。
## 2. applyRemote 加 dirty/deleted 白名单(防复活)
### 缺陷
原来只有 `if (e.dirty) continue`(dirty 行不入 keyed 索引)。
**只排除出索引不够**:远端数据 `keyed.get(key)` 拿不到 → 走"新增"分支
→ 又插一行。结果同一事件在本地有两行(用户改的 + 远端旧版),
用户看到"改完又变回去"或"出现两条"。
### 修法(补两层)
- `dirtyKeyed`:dirty 行单独索引,远端同 key 出现时直接 `continue`,
保留本地版本,不新增不覆盖。
- `dirtyDeletedUids`:本地已标记删除的 uid 集合,远端该 uid 的任何版本
一律忽略。**按 uid 而非 key** —— 删重复系列时本地只留主事件标 deleted,
但远端还有一串 RECURRENCE-ID 实例行,key 各不相同,只有 uid 能整族挡住。
(`markDeleted` 同时置 `deleted=1` 和 `dirty=1`,故 `dirty && deleted` 可同时覆盖。)
## 3. DELETE 404/410 物理清库
### 缺陷(真正的坑在 pushSeries)
`DavClient.deleteRemote` 已把 404 视作成功,但**系列删除路径**仍踩坑:
```ts
if (master.deleted) {
try { await DavClient.deleteRemote(url, auth); }
catch (err) {
...401 抛出...
LogUtil.write(`推送删除重复系列失败:...`);
return; // ⭐ 404/410 也走到这里 → 直接 return
}
await EventDb.purgeByUid(context, calKey, uid); // ← 永远执行不到
```
一旦 URL 拼错(正是上面第 1 条 bug 造成的)、或服务器上资源本就不在,
DELETE 返回 404 → 进 catch → **return,不清本地**。
用户看到日程还在、怎么删都删不掉、每次同步都重复失败。
### 修法
两条路径(单条 + 系列)都区分状态码:
- 401 → 抛出(凭据失效,后续每条都会失败)
- **404 / 410 → 服务器上本就没有,视为删除成功,继续清理本地**
- 其它 → 真失败,保留待推送
## 验证状态
- 编译:`BUILD SUCCESSFUL`
- 静态交叉核查:
- `rowToRemote` 唯一调用处已补第三参数
- `getDirty` 只查 `dirty=1`,deleted 行(双置)仍会被推送,无漏推
- `purge` / `purgeByUid` 均为 `store.delete`,确认是物理删除
- 剩余 3 处 `encodeURIComponent(uid)+'.ics'`(MirrorInbound 247、
SystemCalendarImport 99、EventEditPage 1088)属**本地新建**场景,
此时服务器上尚无资源,用 uid 作初始文件名是正确行为;
PUT 一旦成功,该 URL 即成为资源真名,故无需再改。
- ⚠️ **真机未验证**:设备当前未连接(hdc list targets 返回空),
需连上后按下列步骤验证。
## 待真机验证项
1. 新建日程 → 同步 → 在应用内删除 → 服务器端确认已消失
(重点验服务器实际文件名 ≠ uid 的账号,如家书)
2. 修改日程标题 → 不手动同步 → 等自动同步 → 标题应保持修改后的值
3. 删除重复日程整个系列 → 服务器端确认整族消失,本地无残留
4. 若服务器上资源已被外部删除,应用内删除该日程应能正常清掉本地行
|
2026-11-03 07:36:06 +08:00 |
|
laoyang
|
23b5d4cb9e
|
docs(README): 修正权限描述 —— 移除已删的 LOCATION,标注 READ_WHOLE_CALENDAR 为受限权限需 ACL
|
2026-11-02 22:59:31 +08:00 |
|
laoyang
|
37062cc75b
|
feat(0.2.0): 待办同步、事件标记、反馈系统迁移、版本升 0.2.0 #5 #6
## 版本
versionCode 10200 / versionName 0.2.0(原 10000 / 0.1.0)
## 新增
- **VTODO 待办完整支持**:同步、展示、勾选完成、编辑、循环待办。
关键修复:待办此前**从未被同步过** —— 同步主循环只处理 VEVENT,
`DavClient.reportTodos()` 写了却零调用(死代码)。现已在
`SyncEngine.syncAccountInner` 接入,并对未知类型本也拉一次
(服务器多半不声明 supported-calendar-component-set,宁多拉不漏)。
整段包 try/catch,单本失败不影响日程同步。
- **日历本类型图标**:`supported-calendar-component-set` 解析出
event/todo/both/unknown,账号页在颜色前显示对应 SVG 图标。
unknown 按日历对待(服务器不声明时不能武断判成待办)。
- **问题反馈入口**:设置页新增卡片,指向 FeedLite
(https://feedlite.yangyq.net/?sw=synccalendar)。
## 改进
- 月视图事件标记:农历下方浅红点,每日期最多一个
- 选中/今天改为正圆底,周视图复用月视图 `dayCellCircle`
- 「今天」按钮状态化(在不在今天,视觉可辨)
- 待办页签按需显示(只看 todos.length,不依赖可能缺失的 comps)
- 新建非今天日程默认 09:00(统一 DEFAULT_START_HOUR)
## 修复
- 首屏农历不变色:外层 ForEach key 缺数据版本 → 整树复用不重绘
- 翻月闪跳:async 未等待 + @State 同栈不可读 → 改 rebuildPagesFor(y,m) 显式传参
- 翻月后整月无选中:选中日未随月迁移,且钳制导致日号漂移 → 加 preferredDay
- 周视图颜色过浅/选中不变白/星期标签丢失:复用组件时未设 inMonth
## 官网
- sf.html 新增「三、待办清单」章节(原三~八顺延为四~九)
- index.html 版本号 V0.1.0 → V0.2.0
## 工单
关闭 #3 #4 #5 #6;新建 #7 记录「VTODO 无法镜像到系统待办」的技术限制。
## 未做(技术限制)
镜像到系统日历「待办」页签:鸿蒙未开放待办 API,不可行。见 #7。
v0.2.0
|
2026-11-02 22:58:30 +08:00 |
|
laoyang
|
c39a48e95f
|
fix(待办): 标签改为只看数据事实,不依赖可能缺失的 comps 元数据 #6
## 现象
上一版把 VTODO 同步接进同步循环后,待办数据**确实拉回来了**
(日志:家庭计划3条 / 工作任务2条 / 个人成长2条),
但待办标签仍然不显示。
## 根因:判定挂在元数据上,而元数据拿不到
`shouldShowTodoTab()` 是 `hasTodoBook() && todos.length > 0`。
`hasTodoBook()` 读 `acc.calendarComps`,而这个字段来自服务器的
`supported-calendar-component-set`。
实测:日志里**完全没有**"日历本组件类型 ..."这条记录,
说明该解析路径根本没执行 / 服务器不返回这个属性 ⇒ `calendarComps` 恒为空
⇒ `hasTodoBook()` 恒 false ⇒ 明明有待办也不显示标签。
## 修法:改看事实
直接 `return this.todos.length > 0;`
因为 `CalendarDataService.loadTodos` 内部**已经按 visibleKeys(用户勾选的
日历本)过滤**过了,所以 `todos.length > 0` 本身就等价于
"用户勾选范围内确实有待办"。用户一个待办本都没勾时,这个值必然是 0 ——
`hasTodoBook()` 原本想防的场景天然被覆盖,它是多余的。
## 原需求仍然满足
用户要求"没勾选任何待办本 → 标签不显示":
没勾选 ⇒ loadTodos 按 visibleKeys 过滤后为空 ⇒ 标签不显示。✓
"勾选了但没待办数据 → 也不显示":0 条 ⇒ 不显示。✓
## comps 还留着,但只给图标用
账号页用 comps 区分日历/待办图标 —— 那里判断错了只是图标不准,
不影响功能可用性;功能判定不能挂在这种"可能缺失"的元数据上。
## 验证(真机)
日志:
日历本[5]「家庭计划」REPORT 返回 0 个资源 → 待办同步:3 条
日历本[6]「工作任务」REPORT 返回 0 个资源 → 待办同步:2 条
日历本[7]「个人成长」REPORT 返回 0 个资源 → 待办同步:2 条
界面 dump:顶部标签 同步日历|今天|月|周|列表|**待办**|2026年11月
待办标签存在 = True ✓
(另注意:这几个本 VEVENT 为 0、只有 VTODO,正是纯待办本。)
## 教训
功能可见性判定必须挂在**数据事实**上,不能挂在"可能缺失的元数据"上。
服务器不保证返回 supported-calendar-component-set,用它做开关会制造假阴性。
|
2026-11-02 22:43:10 +08:00 |
|
laoyang
|
ffaffff9d6
|
fix(同步): 待办从未被同步 —— 取消勾选后重新勾选也拉不回来 #6
## 现象(用户反馈)
1. 账号设置里取消勾选待办本 → 保存同步 → 待办标签消失(这一步是正常的)
2. 再进去重新勾选 → 保存并同步 → **待办没回来,标签也不显示**
## 根因:VTODO 从来就没被同步过
同步主循环 `syncAccountInner` 全程只处理 VEVENT:
- `DavClient.reportEtags(href)`:REPORT 的 comp-filter 只写了 `VEVENT`
- `EventDb.queryByCalKey(context, calKey, 'event')`:只查 kind='event'
- `EventDb.applyRemote(..., isTodo)`:这个 isTodo 参数**从未传过 true**
而 `DavClient.reportTodos()`(拉 VTODO)虽然早就写好了,
**全项目零调用** —— 是一段死代码。
因果链:
· 取消勾选待办本 → `pruneStaleHrefs` 清掉不属于已选本的 todo 行
· 重新勾选 → 同步只拉 VEVENT,不拉 VTODO
· 本地库没有待办数据 → `loadTodos` 返回空 → 标签不显示
待办功能"能读能改"(勾选完成、设置循环都能用),但**数据来源一直是空的**,
只有以前残留的行在撑着。一旦被清掉就再也不回来。
## 修法
在同步循环里对每个日历本额外拉一次 VTODO:
`reportTodos(href)` → 逐个 GET 原文 → `IcsUtil.parseTodos` → `applyRemote(..., true)`
⭐ 为什么对 unknown 类型的本也拉:
服务器若没声明 supported-calendar-component-set,我们分不清哪个本是待办本。
待办量远小于日程,多拉一次的代价很小;宁可贵一点,
也不能让用户的待办再次"消失"。
⭐ 失败隔离:
待办同步整段包在 try/catch 里,单本失败只记日志、不影响日程同步。
很多服务器对 VTODO 查询返回 403 或不支持该 REPORT,
不能让这种情况把整个账号同步拖垮。
## 影响面
`applyRemote` 内部按 `kind` 隔离(existing 只查同 kind),
且自带 removed 处理 ⇒ 不会误删日程行,也不会漏删已消失的待办。
|
2026-11-02 22:39:01 +08:00 |
|
laoyang
|
24cd51970c
|
style(账号): 日历本类型图标改用 SVG 线性图标 #6
用户反馈字符图标(▦ / ✓)不好看。
## 为什么字符图标不行
- `▦` 在多数系统字体里缺字形,会 fallback 成方框或模糊图案
- `✓` 与 `▦` 笔画粗细、视觉重量完全不同,并排放不协调
- 13 号字时对齐依赖 font metrics,跨设备会飘
## 换成一对自绘 SVG
`ic_event_book.svg`:日历轮廓 + 两个日期格(填充)
`ic_todo_book.svg` :清单三条 + 右下角勾
两者**共用同一套笔触**:stroke-width 2、round linecap、24×24 viewBox,
渲染成 16vp 时视觉重量一致。颜色分别为灰 #666666 与绿 #00B34A,
沿用项目里已有的"待办用成功色"约定。
## 校验方式
本地无 SVG 渲染工具(无 cairosvg/rsvg/inkscape),改用脚本按坐标
+ 描边半宽计算每个元素的包围盒,确认全部落在 24×24 内不被裁切:
日历:6 个元素 全部在界内
待办:5 个元素 全部在界内
这是静态校验,实际观感仍需真机确认。
## 混合本(both)
显示待办图标(它确实能存待办),不再塞两个图标挤一行。
|
2026-11-02 22:35:25 +08:00 |
|
laoyang
|
09cb0f9664
|
feat(账号/待办): 日历本区分日历与待办类型,待办标签两级判定 #6
用户要求两点:
1. 添加/修改账号时,在颜色前面显示图标区分「日历本 / 待办本」
2. 没勾选任何待办本 → 待办标签不显示;选了待办本但没数据 → 也不显示
## 协议层:supported-calendar-component-set
CalDAV 用这个属性声明集合支持哪些组件:
<C:supported-calendar-component-set>
<C:comp name="VEVENT"/> ← 纯日历本
<C:comp name="VTODO"/> ← 纯待办本
</C:supported-calendar-component-set>
DavClient 改动:
- PROPFIND 请求加该属性(两处:propfindColors / propfindCalendars)
- 新增 `parseComps()` 归一成四类:event / todo / both / **unknown**
- `DavColorEntry` 与 `DavCalendarEntry` 都加 comps 字段
⭐ 为什么保留 unknown:
很多服务器(含自研实现)压根不返回这个属性。若武断判成"不是待办本",
用户原本能用的日历会被错误分类。unknown 一律按**日历**对待,保持兼容。
## 存储
DavAccount 增加 `calendarComps: string[]`,与 calendarHrefs 一一对应。
JSON 存储天然支持;旧数据读出来是空数组 → 全部 unknown → 视为日历。
## 界面
EditCalendarRow 在颜色圆点**前面**加类型图标,用户先看类型再看颜色:
· 待办本 → ✓ 配成功色
· 混合本 → ▦✓
· 日历/unknown → ▦
## 待办标签两级判定
1. `hasTodoBook()`:被选中的日历本里存在 comps 为 todo/both 的
—— 一个都没选 ⇒ 必然没待办,直接不显示
2. `todos.length > 0`
—— 选了待办本但没数据,也不显示
比只看条数更准:没勾待办本时,同步可能拉到残留数据,
只看条数会误显示;第一级在"还没同步"时就能给出确定答案,不会闪一下。
## 待确认(需真机日志)
服务器是否真的返回 supported-calendar-component-set。
诊断日志已加:`日历本组件类型 <名称> → <类型>`。
若全是 unknown,说明服务器没声明 ⇒ 图标会退化成日历图标,
此时需要改用其它判据(如按本内是否含 VTODO 探测)。
|
2026-11-02 22:32:51 +08:00 |
|
laoyang
|
24e33ed5c9
|
fix(月视图): 翻月闪跳 —— 改为显式传参重建,不依赖 @State 即时可读 #6
用户反馈:11 月滑到 12 月时,先闪回 11 月,然后才跳到 12 月。
## 根因(两层)
### 第一层:异步时序
`reloadEvents()` 是 async(内部 await 读库),调用只是**启动**。
原代码:
this.reloadEvents(); // 只启动
this.swiperController.changeIndex(1); // 立刻回弹
回弹那一刻 monthPages 还是旧数据,中间页仍是上个月 ⇒ 闪回。
修:把 `rebuildPages()` 提到 changeIndex **之前**同步执行。
### 第二层:@State 赋值在当前 tick 内不可读
改完第一层后仍会闪。日志证据:
onChange index=2 当前=2026-11
同步重建完成 中间页=2026-11 ← 明明先赋了 displayMonth=12
说明 `this.displayMonth = v` 之后、同一个同步调用栈内读取它,
**不一定拿到新值**(@State 走可观察代理,写入与读取之间有调度)。
修:新增 `rebuildPagesFor(year, month)`,把月份**作为参数显式传入**,
内部不再读 `this.displayXxx`。这样无论 @State 何时生效都算得对。
## 附带修正:诊断日志本身有 bug
之前用 `monthPages[1][0].dateMs` 当作"中间页月份",
但月历第一格常属于**上个月**(12月的第一格是 11/30),
日志就写成"中间页=11月",让我误判成"月份没变",白查了一轮。
改用记录**传入参数**后真相立刻清楚:入参全程都是 2026-12,逻辑一直是对的。
## 真机验证
11月 → 左滑 → 2026年12月,选中圆存在,无闪回。
日志确认:
TRACE_PARAM 重建入参=2026-12(同步重建)
onChange index=1 guard=true → 被 guard 正确拦下
TRACE_PARAM 重建入参=2026-12(数据重建)
诊断日志已在提交前移除(grep 0 处)。
## 教训
1. @State 赋值后**不要在同一同步栈内立刻读它**做计算,要用局部变量/参数传递。
2. 写诊断日志时,验证"我关心的那个值"必须用可靠来源。
用 `数组[0]` 代表整体属性是常见陷阱 —— 这次就因此浪费了一轮排查。
|
2026-11-02 22:25:00 +08:00 |
|
laoyang
|
6305b4a7f0
|
fix(月视图): 翻月后选中态丢失,整月没有一天被选中 #6
用户反馈:月视图滑动切换月份后会"跳一下",整月没有任何一天被选中。
期望:切到下个月后,**同样的日号**保持选中(如 11/2 → 12/2)。
## 根因
handleSwiperChange 只改了 displayYear/displayMonth,**完全没动 selectedDate**。
于是 selectedDate 还停在上个月,而新月份所有格子的 dateMs 都变了,
`cell.dateMs === this.selectedDate` 永远为 false ⇒ 整月无选中。
## 修法
翻月后把 selectedDate 迁到新月份的同一天;该月没有这天时钳到月底
(1/31 → 2/28,与 incForFreq 的钳制策略一致)。
## 顺带修掉日号漂移
单纯的"钳制"有个副作用:1/31 → 2/28 → 3/28 → 4/28 …
日号被永久改成 28,翻到有 31 天的月份也回不去了。
加 `preferredDay` 记录**用户主动选中的日号**(点日期时更新,翻月时不更新):
偏好31号:1/31 → 2/28(钳制显示) → 3/31(回到偏好) → 4/30 → 5/31
"月末"的语义得以保留。
## 验证
脚本 18 项全通过(跨年、闰年 2/29、大月小月、连续翻月、往前往后)。
真机实测:
11月2日 → 左滑 → 12月,选中圆存在(144×144)
→ 再左滑 → 2027年1月,圆内日期仍是"2"
## 教训
"只改显示状态、不改选中状态"是日历类视图的高频 bug。
翻页/翻月时,凡是与"当前选中项"绑定的视觉(高亮、背景),
都要检查选中项本身有没有跟着迁移 —— 否则 diff 时匹配不上就全空白。
|
2026-11-02 22:16:08 +08:00 |
|
laoyang
|
d8ee2aa2be
|
fix(周视图): 星期标签移到圆外,圆径回到 46vp 与月视图一致 #6
用户反馈:上一版圆太大,把"一"(星期标签)也圈进去了,不应包进来。
## 结构调整
原来(错):
Stack{ 圆(54vp), Column{ 星期+日期+农历+点 } }
—— 为了让圆包住"一"把圆径撑到 54vp,结果星期也被圈进去
现在(对):
Column
├ Text(星期) ← 在圆外
└ Stack{ 圆(46vp), Column{ 日期+农历+点 } }
圆只包 日期(72)+农历(27)+点(11)+间隙 ≈ 116px,46vp(138px) 够用,
圆径回到与月视图完全一致。
## 过程说明(走了弯路)
这轮我先用脚本做局部替换,结果括号层级错乱,改了三版都没编译过:
· 版1:找不到锚点(注释文字叠加,锚点不匹配)
· 版2:补闭合时多写了一个 `}`,报 "Declaration or statement expected"
· 版3:`Stack` 不支持 `justifyContent`,报 "Property 'justifyContent' does not
exist on type 'StackAttribute'"
最后**整体重写整个 dayCellCircle 函数**(按行号范围一次性替换)才干净。
## 结论
多层嵌套(Column > Stack > Column)的 ArkUI 结构,靠脚本做局部增删很容易
括号错位。**这类改动应当整块替换而不是局部 patch**,一次写对更省时间。
|
2026-11-02 22:11:21 +08:00 |
|
laoyang
|
ddaf41fee8
|
fix(周视图): 圆径按内容高度适配,修"上露星期、下漏圆点" #6
用户反馈:周视图圆的上沿包进了"一"(星期标签),下沿却把事件点漏在外面。
## 根因
周条加了星期标签后,内容实际高度约 147px:
星期 10px字号 ≈ 28
日期框 24vp ≈ 72
农历 9px字号 ≈ 27
事件点 3.5vp ≈ 11
3 个间隙 ≈ 9
合计 ≈ 147px
而圆固定 46vp = 138px ⇒ **内容比圆高 9px**。
Stack 的 alignContent 默认居中,导致内容上下对称溢出:
上面露出星期标签,下面漏掉事件点。
## 修法
圆径按内容取,不搞一刀切:
· 周条(showWeekLabel=true,内容≈147px)→ 54vp(≈162px)
· 月视图(无星期标签,内容≈118px) → 46vp(≈138px)
同时 Stack 显式 alignContent(Alignment.Center),避免内容高于圆时对齐跑偏。
## 验证(真机 dump 实测)
圆范围 y 495~664(169px)
星期"一" y 497~534 圆内 ✓
日期"2" y 537~612 圆内 ✓
农历"廿四" y 615~648 圆内 ✓
事件点×7 y 651~662 圆内 ✓
## 教训
Stack 里"固定尺寸的装饰元素"(圆)+ "不定高的内容"(文字块)组合时,
圆径必须 >= 内容高度,否则内容一定溢出。
这类问题看截图不容易发现(因为溢出部分常被裁掉或重叠),
**必须 dump 出两者的 bounds 做数值比对**才准。
|
2026-11-02 22:08:29 +08:00 |
|
laoyang
|
2cc3b1ba42
|
fix(周视图): 修复复用后颜色过浅、星期标签丢失 #6
用户反馈:周视图复用月视图后,日期/文字颜色太浅(灰),选中也不变白。
## 根因:weekStripDays 从未设置 inMonth
dayCellCircle 的颜色分支是:
.fontColor(!cell.inMonth ? text_hint(灰) : ...)
但 weekStripDays() 只设了 dateMs/day/isToday/lunar/hasEvent,
**inMonth 一直是默认值 false** ⇒ 周条每天都被当成"非本月",全部走灰色分支。
修法:weekStripDays 里显式 `cell.inMonth = true`
(周条 7 天都是有效日期,没有"月外"概念)。
## 顺带补回丢失的星期标签
复用 dayCellCircle 时,原来周条自己那行 WEEK_LABELS(一/二/三…)被一并删掉了,
现在周条没有星期。
做法:dayCellCircle 加参数 `showWeekLabel`(默认 false),
周条传 true。月视图的星期在表头单独一行、不在格子里,所以默认不显示。
## 农历提亮
实测采样(JPEG,最暗 25%):
周条农历 rgb(175~183) ← text_hint(#4D000000)
月历日期 rgb(27~54) ← text_primary(#182431)
周条里农历紧挨日期数字,对比过强会显"糊"。
周条农历改用 text_secondary(约 rgb(120~150))、字号 8→9;
月视图农历保持 text_hint,不破坏已有观感。
## 教训
复用组件时必须核对**数据契约**:dayCellCircle 依赖 inMonth 决定颜色,
而 weekStripDays 从没提供这个字段。
"能编译、能显示"不等于"语义正确" —— 默认值 false 这种静默错误最难发现。
|
2026-11-02 22:06:15 +08:00 |
|
laoyang
|
22db069bee
|
style(周视图): 选中/今天的圆放大到 42vp #6
用户反馈:周视图的圆不够大。
## 实测数据(设备 dump)
周条每列 Stack = 133 × 192 px
原来圆 = 113 × 113 px(36vp)
周条每列有 192px 高,圆只占 113px,显得空;而且比月视图的 138px 还小,
两个视图的"选中"视觉分量不一致。
## 关键约束:宽度是硬上限
列宽 133px ≈ 44vp。给 52vp(≈156px) 会**横向溢出被裁切**。
所以取 42vp(≈126px),留 7px 余量。
## 字号同步放大
大圆里字太小会失衡,一起调:
日期 14 → 16、日期框 30 → 34
农历 8 → 9、事件点 3.5 → 4
⚠️ 教训:改尺寸前先 dump 量格子实际宽高,别拍脑袋。
这次如果直接按"感觉不够大"给 52,会被裁成方角,又得返工。
|
2026-11-02 21:59:30 +08:00 |
|
laoyang
|
4f19c9482d
|
style(月/周视图): 今天也变大圆、周视图统一正圆、待办标签按需显示、今天按钮状态化 #6
用户提的四个细节,一次改完。
## 1. 今天的淡蓝底也要变大圆
原来 `isToday` 的淡蓝底贴在日期数字上(24×24 小圈),而选中态是 46vp 大圆。
同一天"选不选中"看起来像两种不同的东西。
现在两者尺寸统一:选中=实蓝圆,今天=淡蓝圆,都是 46vp。
日期数字自身的背景一律透明,避免"大圆里套小圈"。
## 2. 周视图选中态也变大圆
周条原来只有 30×30 的小圈,跟月视图 46vp 的大圆不一致。
同一个"选中"在两个视图里长得不一样,会让人以为是不同状态。
改用 Stack + 36vp 正方形 + 50% 圆角(周条比月历窄,36 够用且不压邻居)。
## 3. 没有待办就不显示「待办」标签
判定用 `this.todos.length > 0`。
todos 已按"可见日历本"过滤,所以这个判断等于"用户实际能看到几条待办"。
否则用户点进去一片空白,会以为功能坏了。
## 4. 「今天」按钮要能看出"在不在今天"
· 已在今天 → 透明底 + 蓝框蓝字(无需跳转)
· 不在今天 → 蓝底白字(可点,点了回今天)
新增 isOnToday() 判定:selectedDate === startOfDay(now)。
## 为什么圆一定要用 Stack
Column 是 layoutWeight(1) 撑满的**矩形**,圆角给再大也只是"圆角方块/胶囊"。
只有独立的正方形容器取 50% 圆角才是真圆。
这一点这轮改了两遍才对(第一版铺在 Column 上,得到 149×133 的方块)。
|
2026-11-02 21:56:25 +08:00 |
|
laoyang
|
b41eaabc41
|
style(月视图): 选中态改为正圆蓝底 #6
用户要求:选中时的蓝色背景做成正圆,把日期、农历、事件点都包进去。
## 为什么第一版不圆
上一版把蓝底铺在最外层 Column 上。Column 是 layoutWeight(1) 撑满格子的
**矩形**(149×133),无论圆角给多大,只能是"圆角方块"或"胶囊",
变不成正圆。
## 这一版
新增 dayCellCircle,结构改为 Stack 叠放:
├─ 底层:选中时一个 46vp × 46vp 的正方形容器,borderRadius 50% → 真正的圆
└─ 上层:原有内容(日期数字 / 农历 / 事件点),背景透明
尺寸 46vp ≈ 138px 的依据:
· 内容实际高约 121px(日期框 75 + 农历 29 + 点 11 + 间隙)
· 格子宽 149px
⇒ 138 能包住全部内容,又不超出格子宽度
选中时日期数字、农历、事件点统一转白(与日期同色),
因为浅红压在深蓝底上会发灰。
⚠️ 踩坑记录:改这个之前最好先量一下尺寸。我是从 dump 里读到
"日期 599~674、农历 677~706、点 709~720" 才确定 46vp 这个值的,
拍脑袋给尺寸很容易包不住或溢出格子。
|
2026-11-02 21:51:34 +08:00 |
|
laoyang
|
e92daf5297
|
style(月视图): 农历恢复原色,下方加浅红小圆点 #6
用户反馈:农历整体染红不好看(数据本身是对的,确实每天都有日程),
要求改成"农历下面一个浅红色的点,不要太红、不要太刺眼"。
## 改动
- 农历文字恢复原色(灰 / 选中日仍是提示灰),不再染红
- 农历**下方**单独一行放一个 3.5vp 小圆点
- 颜色新增 `soft_red`,不用之前的 event_mark(#D93025)
· 浅色 #F2938A —— 带粉的浅红,是"柔和提示"而非"警报"
· 深色 #C97A72 —— 黑底上浅粉会发灰,用稍沉的暖红
- 无事件时圆点透明但仍占位,保证有/无事件行高一致,整月不跳
## ⚠️ 两个必须处理的布局约束
1. **圆点必须独立成行**,不能塞进农历那一行的 Row 里。
上一版就错在这:农历带 layoutWeight(1) 撑满整行,圆点被挤到行尾,
视觉上贴着下一个日期。独立成行后天然居中在格子下方。
2. **格子高度已被占满**(实测 175px 全用尽),直接加一行会被顶出格子。
所以把 Column space 从 2 收到 1、日期框 26→24,换出约 4px 给圆点。
周视图同步改成同一套标记,避免"月历一个样、周条另一个样"。
|
2026-11-02 21:31:52 +08:00 |
|
laoyang
|
8ca1eb76e0
|
fix(月视图): 首屏不变色 —— 真正根因是外层 ForEach key 不变 #6
上一版只改内层 key 没修好,因为问题不在内层。
## 真正根因
外层 ForEach 的 key 是:
(cells) => String(cells[0].dateMs) // "这页第一天"
重建前后这个值**完全相同**(还是那个月的 1 号)。
⇒ ArkUI 判定"这一项没变" ⇒ **不重新执行 itemGenerator**
⇒ 里面的 chunkCells(cells) 不重算
⇒ 内层 ForEach 收到的还是**旧的 cells 数组**,旧数组里 hasEvent 仍是 false
内层 key 再怎么改都没用 —— 内层压根没被重新调用。
## 修法
新增 `@State monthDataVersion`,每次 rebuildPages 后自增,拼进外层 key:
(cells) => `${cells[0].dateMs}_v${this.monthDataVersion}`
key 一变 → 外层重建 → 内层重新执行 → 颜色正确。
为什么不用"把整页 hasEvent 拼成 key":一页 42 格,key 会长很多,
且会参与 diff 比较,有性能代价。版本号是 O(1),语义也更清楚。
## 同时补了诊断日志
buildMonthCells 里输出:事件条数 / 有事件日期数 / 本页命中格数 / 当前版本号。
用来判断问题卡在哪一层:
· "有事件的日期 0 天" → 数据层没算对
· "命中 X 格" 但界面没变色 → 渲染/key 问题
## ⚠️ 教训(两层 ForEach 嵌套时)
这类 bug 我第一轮判断错了:只看到最内层 key 缺 hasEvent 就改了,
没意识到**外层 key 不变会让整棵子树被复用**,内层改什么都没用。
排查嵌套 ForEach 的刷新问题时,要从**最外层**往里查,
每一层的 key 都必须随数据变化。
|
2026-11-02 17:12:55 +08:00 |
|
laoyang
|
d49b041a2c
|
fix(月视图): 首屏农历不变红,切视图才生效 #6
用户反馈:刚打开应用时,月视图里有事件的日期农历没变红;
切到周视图再切回月视图,才变红。
## 根因:ForEach 的 key 不完整
月视图的 key 是:
(cell) => `${cell.dateMs}_${cell.dateMs === this.selectedDate}`
**没有包含 hasEvent。**
首屏时序:
1. aboutToAppear → rebuildPages(),此时 events 为空,hasEvent 全 false
2. reloadEvents() 拉回数据 → rebuildPages() 再次重建,hasEvent 变 true
3. 但每个格子的 key 完全没变(dateMs 和选中态都没变)
⇒ ArkUI 判定"这些项没有变化",**复用旧组件、跳过重绘**,颜色停在旧值
切视图能恢复,是因为切回来时整棵子树被重新创建,绕过了 diff。
## 修法
把影响渲染的**全部**状态纳入 key:hasEvent / isToday / 是否选中。
key 变了 → ArkUI 视为新项 → 重建该格子 → 颜色正确。
## 顺带加固
中间层"每周"的 key 原来是 `String(idx)`(0..5),跨月翻页时序号永远重复,
会让 ArkUI 误判为同一周而复用旧节点。改为用该周首日的 dateMs 做 key。
## 为什么不改成 @Observed / @ObjectLink
monthPages 是二维数组(MonthCell[][]),buildMonthCells 返回新数组、
chunkCells 又一次切片,改成可观察对象要动整条构建链路,
牵涉面大且容易引入其它 diff 问题。用 key 是最小、最稳的改法。
⚠️ 通用教训:ArkUI 的 ForEach 用自定义 key 时,**key 必须覆盖所有影响渲染的
字段**。漏一个字段,该字段更新后界面就不刷新 —— 而且这类 bug 只在
"同一项的数据变了、key 没变"时出现,首次渲染永远是正常的,很容易漏测。
|
2026-11-02 17:10:38 +08:00 |
|
laoyang
|
fa3dbc6157
|
style(月视图): 有事的日期标记色改为红色 #6
用户要求先看红色效果,蓝色太不显眼。
## 新增 event_mark 语义色
不直接复用现有的 `error`(#E02020):
error 是语义色,到处被"失败/危险"复用。把日程标记挂上去,
以后想调错误提示的红就会连带改掉日历标记,两件事被耦合。
所以单独加一个 event_mark。
色值(农历只有 8px,小字对对比度要求高):
· 浅色 #D93025 —— 比 #E02020 更沉,压在白底/浅灰底上更清晰,
也不像"出错"那么刺眼
· 深色 #FF6B60 —— 黑底上要亮一点才够,纯红会显脏
## 改动范围(3 处)
· 月视图(非宽屏)农历文字
· 月视图(宽屏)补位小圆点
· 周视图农历文字
选中日仍用 button_text(白):那天日期数字坐在深红底上,
农历必须白色,否则红字压深底看不清。
|
2026-11-02 17:06:21 +08:00 |
|
laoyang
|
ff62cf7f00
|
fix(月视图): 事件标记改为农历文字变色,修圆点位置错乱 #6
用户反馈:圆点位置很怪,离农历很远、落在两个日期中间。
根因:圆点塞在农历行的 Row 里,而农历用了 layoutWeight(1) 撑满整行,
圆点被挤到格子最右侧;格子宽 133px 远大于文字宽,
视觉上就成了"贴着下一个日期"。
采纳用户方案:把标记合并到农历文字本身
· 有事件 → 农历字用主题色并加粗
· 无事件 → 保持原来的 hint 灰
好处:不再额外占空间、位置天然跟着文字走、也不会像满屏圆点那样显乱
("点太多显得乱"正是上一版被砍掉的原因)。
对比度:选中日的日期数字坐在深色底上,农历若仍用主题蓝会看不清,
所以选中日改用 button_text(白),与原圆点方案的策略一致。
周视图同步统一为农历变色:两种视图用两套标记会让人以为是不同含义。
宽屏(wideLayout)农历被省略,保留一个小圆点补位(此时格子窄,圆点居中,无位置问题)。
|
2026-11-02 16:58:56 +08:00 |
|
laoyang
|
788171d1b2
|
feat(待办): 支持循环任务(标准 CalDAV / VTODO RRULE)#5
只按标准 CalDAV 实现,不依赖家书的自定义机制。
群晖等标准服务器:RRULE 写在 VTODO 上,由客户端负责展开。
## 核心语义决策
循环待办「勾完成」= 这一轮做完了,推进到下一周期,而不是结束。
若照搬非循环逻辑(置 COMPLETED 就完结),用户每勾一次循环任务它就永久结束,
循环规则等于白设。所以:
· 非循环 → 置 COMPLETED,永久结束
· 循环 → DUE 推到下一周期 + 状态复位 NEEDS-ACTION
## 关键修复:DTSTART 与 DUE 必须分开
原 parseTodos 把 DUE 和 DTSTART 都写进 startTime,后出现的覆盖先出现的。
但循环下两者语义完全不同:
· DTSTART = RRULE 的重复基准
· DUE = 这一条的截止时间
混在一起会导致重复基准被截止时间覆盖,算出来的发生日全错。
现在拆成独立字段,数据库补 dtstart 列(rrule 列已有)。
## 顺带修掉两个既有 bug(测试连带查出)
rewriteTodoCompletion 与 rewriteTodoFields 里,插入阶段的块序号
vtodoIdx 在遇到 END:VTODO 时被重置成 -1 ⇒ 每块都从 0 重新数,
多 VTODO 文件里永远匹配不到目标块,字段加不上(静默失败,
表现为「点了没反应」)。三处一并修正,vtodoIdx 改为全局块序号。
## 实现
- IcsUtil.incForFreq:按 FREQ 推周期,处理月末溢出
(1/31 + 1月 → 2/28 钳制,不跳到 3 月;2/29 + 1年 → 2/28)
- IcsUtil.freqOfRrule / rewriteTodoRecurrence
- TodoService.setRecurrence / completeRecurringRound
- 写回时同时给 DTSTART + DUE:RFC 5545 里 VTODO 的 DTSTART 是 OPTIONAL,
但缺它时各客户端对 RRULE 基准理解不一致,显式给能消除歧义
- 待办行显示「↻ 每周」标记(否则循环任务和普通待办长得一样,
用户勾完发现「又冒出新的」会困惑)
- 编辑弹框加 每天/每周/每月/每年 选择,再点一次取消
## 验证
scripts/test-ics-todo-recurrence.js:24 项全通过
(月末钳制、闰年、字段保留、取消循环、多 VTODO 定位、结构完整性)。
编译 BUILD SUCCESSFUL。
## 说明
服务器若不支持 RRULE(如群晖 Calendar 规格里未提及循环任务),
则循环只在客户端可见,同步到其它客户端会退化为普通任务。
|
2026-11-02 16:53:30 +08:00 |
|
laoyang
|
9066a12bbc
|
feat(待办): 按日历本分组 + 编辑/删除 + 修整行点击误完成 + 周视图圆点 #5 #6
## 1. 修 bug:整行点击都会勾选完成
上一版把 onClick 挂在最外层 Row 上,点标题/点截止时间/点空白都会改状态。
改为:
· 只有左侧圆圈 → 勾选/取消完成
· 其余内容区 → 打开编辑弹框(不改变完成状态)
## 2. 待办按日历本分组
用户:待办应按日历本分类,未完成里也能按家庭/生活分开,要有区分度。
未完成/已完成 两大段内部,再按日历本聚合:
· 分组标题前带该本颜色圆点(这是区分度的来源)
· 组标题显示 本名(数量)
· 组内按截止时间升序,无截止时间排最后
· 组之间按组内最早截止时间排序(最紧急的本在前)
CalendarDataService.loadTodos 补 d.calName(原来只带 calKey,分组标题没名字)。
## 3. 待办编辑弹框
点内容区弹出,可改:标题 / 备注 / 截止时间(含日期+时间选择器,可清除),
另有删除(二次确认)。只读本禁用编辑并提示。
⚠️ 用**整页覆盖层**而不是 bindSheet:ArkUI 同一组件只能挂一个 bindSheet,
Index 已用 detailShow 挂了日程详情,再挂会被覆盖(EventEditPage 顶部记过此坑)。
写入仍走「GET 原文 → 改字段 → PUT」,只动 SUMMARY/DESCRIPTION/DUE,
**不动 STATUS/COMPLETED**(改内容不该重置完成状态),
并保留 PRIORITY/CATEGORIES/X-* 等本地没存的字段。
## 4. 周视图圆点
weekStripDays 原来没算 hasEvent;补上与月视图**共用同一份**事件日期集合
(buildEventDaySet),口径完全一致。
颜色:用主题色 brand(选中日换白),**不取日历本的颜色** ——
圆点只表示这天有安排,若取本色会误导成只有那个本有日程。
## 验证
scripts/test-ics-todo-fields.js:19 项全通过
(保留 PRIORITY/CATEGORIES/X-扩展、特殊字符转义、多 VTODO 只改目标、
缺字段补齐位置、结构完整性)。编译 BUILD SUCCESSFUL。
## 未做
子任务(父子层级 / RELATED-TO):需先确认服务器是否支持,另开工单。
|
2026-11-02 15:47:03 +08:00 |
|
laoyang
|
98c4b6a96f
|
feat: 待办可勾选完成 + 月视图事件圆点 #5 #6
== #6 月视图事件标记(每日期最多一个圆点)==
应用市场反馈希望有事件的日期带标记。此前该功能存在但被移除,
原因是**按日历本逐个渲染**:3 个本 3 个点,15 个本满屏都是点。
- MonthCell 只加**一个布尔** hasEvent,不存每本一个的集合
- 用 buildEventDaySet() 一次遍历把有事件的日期收进 Set,
渲染时 O(1) 查(避免 42 格 × N 事件的 O(n²),本机近 3000 条数据会卡)
- 判定口径与 eventsOfDate 一致(区间相交)→ 跨天事件覆盖的每一天都有点
- 待办不计入(无时间点语义,混进来点会失去意义)
- 视觉用主题色 5vp 小点而非纯红:42 格里 30 个红点会吵,
正是用户嫌乱的同类问题;选中日深底下换白色保证对比度
- 无事件时用等高透明占位,避免行高跳动
== #5 待办勾选完成 ==
先回答关键问题:**系统未开放待办 API**。
本机 SDK 检索 @ohos.calendarManager 全文,todo/VTODO/task 零命中,
Calendar/Event 只处理 VEVENT ⇒ 第三方应用无法读写系统「待办」页签。
故只能在本应用内实现,并已在待办页注明不会出现在系统待办中。
现状是**只读**(点击提示请在源端修改),本次补上写回:
- TodoService.setCompleted():GET 原文 → 改状态字段 → PUT 回写
- EventDb: queryById / setTodoCompleted
- DavClient.getRawStrict():失败**抛错**(原 getRaw 失败返回 HTTP 4xx
字符串,若当 ICS 内容 PUT 回去会把待办覆盖成一句错误信息)
- IcsUtil.rewriteTodoCompletion()
⚠️ 两个关键设计:
1. **不重建 VTODO,只在原文上改字段**
VTODO 有 SUMMARY/DESCRIPTION/DUE/PRIORITY/CATEGORIES/X-* 等字段,
本地库只存了少数几个。重建后 PUT 会把没存的字段全部抹掉(丢数据)。
2. **两遍处理,与字段顺序无关**
RFC 5545 不规定字段顺序。单遍实现若 STATUS 出现在 UID 之前,
matched 仍为 false → 状态改不到 → 用户点完成没反应。
改为第 1 遍定位目标 VTODO 块、第 2 遍在块内改写。
插入补字段时按**块序号**定位而非行号(改动会删掉旧 COMPLETED 行,
行号不再对齐,会让字段落到 END:VCALENDAR 之后,部分服务端直接 400)。
同步维护三个字段(按 RFC 5545,缺一个各客户端显示就不一致):
STATUS / COMPLETED / PERCENT-COMPLETE。
UI:顶部新增「待办」标签(月/周/列表/待办),未完成在前、已完成在后,
点击勾选即写回;写回失败本地状态不变(避免界面与服务器不一致)。
验证:scripts/test-ics-todo.js 覆盖 26 项(状态在 UID 前/后、取消完成、
多 VTODO 只改目标、缺 STATUS 补字段、折行、结构完整性),全部通过。
编译 BUILD SUCCESSFUL。
|
2026-11-02 14:48:43 +08:00 |
|
laoyang
|
1cc35f147d
|
fix(新建日程): 非今天默认 09:00,今天仍用当前时刻 #4
应用市场反馈:「新建非今天的日程时,默认时间固定在当天 9 点更好」。
根因:原来日期取所选日,但**时分固定取 now.getHours()**,
与所选日期无关。所以下午三点建明天的日程会得到「明天 15:00」。
改为按日期分支:
· 今天 → 当前时刻(今天 09:00 往往已过,「现在就记一笔」才是常态)
· 非今天 → 09:00(现在几点对别的日子没有参考价值)
顺带统一:toggleAllDay 关闭全天时原来硬编码 9 点,改为引用
DEFAULT_START_HOUR 常量,两处口径一致。
|
2026-11-02 14:48:28 +08:00 |
|
laoyang
|
199b6c177f
|
feat(清理): 卸载残留治理 —— 显式清除入口 + 孤儿兜底加强 #3
鸿蒙没有「应用即将被卸载」的回调(网传的 preUninstallHook /
uninstallDataPolicy 在本机 SDK 中均不存在),而系统日历数据属于
沙箱外,卸载时不会被系统清理 ⇒ 用户卸载后会永久残留镜像账户
(还会继续提醒)。本次提供显式入口 + 状态兜底。
1. AccountStore.isEmptyVaultConfirmed()
区分「账号库真的空」与「loadAll 吞异常返回空」。
依据:loadAll 内部把所有异常 catch 后返回 [],调用方无法从返回值
辨别,而孤儿清理一旦误判就会把用户真实在用的镜像全删掉。
2. SystemCalendarMirror.purgeOrphanAccountMirrors()
原来 accounts.length === 0 一律跳过(安全阀),导致卸载重装后
旧镜像永远清不掉。改为三态:
① 有账号 → 只清 accId 不匹配的(原逻辑)
② 空 + 确认空白 → 全清(卸载/重置后的残留)
③ 空 + 拿不准 → 仍跳过(读异常,不能误删)
全清模式下不再因「解析不出 accId」跳过脏账户,否则清不干净。
3. SettingsPage 新增独立卡片「清除系统日历数据」
- 「彻底清除系统日历镜像数据」:二次确认后 removeAll,
并关闭镜像开关(否则下一轮同步立刻重建,用户以为没清掉),
镜像本消失后把提醒交回 App(避免漏提醒)
- 零结果提示区分「没权限」与「本来就没有」,用户要的是确认清干净
- 卡片放在镜像开关之外:关着镜像时也能清残留(正是典型场景)
- 原「清理残留镜像账户(诊断)」一并从镜像卡片内移入本卡
只删系统日历镜像;CalDAV 账号与服务器日程完全不动。
|
2026-10-30 13:34:09 +08:00 |
|
laoyang
|
63b1a75b03
|
docs: README 更新至 0.1.0,补齐 0.0.7~0.1.0 缺失内容
README 的版本信息此前停在 0.0.6(versionCode 600),已滞后 4 个版本。
- 版本行:0.0.6/600 -> 0.1.0/10000
- 补「地点搜索补全」「一键导航」「必填项校验强提醒」(0.1.0)
- 新增「CalDAV 地址自动发现」小节(0.0.7:.well-known 发现 + 人话错误提示)
- 隐私段落:明确「不申请定位权限」,补充华为地图服务说明与
AES-256-GCM / HUKS 凭据加密细节
|
2026-10-28 23:18:48 +08:00 |
|
laoyang
|
d993dd3d4b
|
移除定位权限:地理编码不需要 LOCATION(AGC 隐私标签审核)
AGC 以「未如实填写隐私标签」退回本次提审。核查后发现根因是
申请了两个用不到的敏感权限,与隐私标签无法自洽。
依据(本机 SDK API 26,targetSdk 26.0.0):
- @ohos.geoLocationManager.d.ts 中 getCurrentLocation / getLastLocation /
on('locationChange') 均有 @permission APPROXIMATELY_LOCATION;
- 而 getAddressesFromLocationName(地址→坐标)与 getAddressesFromLocation
(坐标→地址)**没有 @permission**,只有 @syscap ...Location.Geocoder。
- 本应用从不调用 getCurrentLocation,本就不读设备实时位置,只在「一键导航」
时把用户填写的地址换算为坐标。
改动:
- module.json5:删除 LOCATION、APPROXIMATELY_LOCATION(现仅 5 个权限)
- Index.ets:删除 ensureLocationPermission(),openInAmap() 直接调
geocodeAddress();同步清掉随之无用的 abilityAccessCtrl / bundleManager 导入
- string.json:删除 perm_location、perm_location_approx
- AppSettings.ets:更新权限一致性注释,警示后续勿再照旧文档写错政策
验证:assembleHap BUILD SUCCESSFUL;解包 hap 与 .app 内 module.json 均只剩
5 个权限、LOCATION 零命中;真机 hdc install -r 成功,aa start 无崩溃。
注意:华为旧文档 @ohos.geolocation(API 9 已废弃)仍写"需要权限 LOCATION",
那是老接口,勿据此判断——应以本机 .d.ts 为准。
v0.1.0
|
2026-10-28 23:14:08 +08:00 |
|
laoyang
|
b7bc4b5403
|
chore: 版本号 0.0.9 -> 0.1.0(versionCode 10000)
0.0.9 已上架,开启 0.1.0 开发周期。
versionCode 按 major*1e6 + minor*1e4 + patch*100 计 = 10000。
同步更新官网 deploy/site:index.html / sf.html / uh.html 的版本标识,
_404.tpl.html 改后重跑 _gen404.py 重新生成 404.html。
|
2026-10-21 14:12:52 +08:00 |
|
laoyang
|
96335728f5
|
feat(编辑页): 必填项校验失败改为强提醒(Toast + 字段标红 + 滚回定位)
原来校验失败只写 statusMsg,而它渲染在表单最底部("删除日程"按钮上方)。
用户在顶部点"保存"时那行红字远在屏幕之外,观感就是"点了没反应"。
新增 showFormError(msg, field) 作为唯一错误出口,同时做三件事:
- Toast:系统级浮层,用 bottom:'70%' 抬到屏幕偏上方,不受滚动位置影响,必定可见;
- 字段标红:标题输入框 / 时间卡片 / 日历本区域加 error 色边框,并在紧邻位置给出红字说明;
- 滚回出错位置:scrollEdge(Top) + 对标题 requestFocus('titleInput')。
配套:
- 标题 placeholder 改为「标题(必填)」,静态说明必填属性;
- 用户开始输入标题 / 选中日历本后自动撤掉红框与状态行,不会"改了还一直红着";
- 日历本未选也走 showFormError(此前只写状态行);
- 底部状态行仅在 errorField==='' 时显示,避免与就地提示重复;
- 保存异常分支补 Toast:该分支紧接着 router.back(),状态行来不及被看到。
|
2026-10-21 10:17:46 +08:00 |
|
laoyang
|
a32c049199
|
fix(地点): 候选改为 bindPopup 浮层,根治输入框跳动与选完复现
旧方案把候选做成内联列表,虽放在输入框上方,但:
1. 候选高度变化(搜索中 / 出结果 / 条数变化)会改变表单高度,输入框随之反复移动,
再靠 scrollBy 事后纠正,表现为输入框来回跳好几次;
2. 选中候选后 applyPlace 程序回写 text 会触发 TextInput.onChange,
把 placePicked 重置为 false 并重新发起搜索,导致候选框消失后又冒出来一次。
改为:
- 候选用 .bindPopup($$placeOpen, { placement: Top }) 挂在输入框上,
不参与布局,出现与消失完全不移动输入框;
- 移除全部 scrollBy 对齐逻辑(keyboardPadVp / ensurePlaceVisible / 键盘监听),
键盘避让交由 KeyboardAvoidMode.RESIZE 的默认光标可见行为;
- 用 lastAppliedText 精确识别「程序回写 text 造成的 onChange」,不再发搜索;
- runSuggest 期间浮层持续展开(搜索中 → 结果),避免收起再弹造成闪烁;
- 抽出 closePlacePopup() 作为统一收起入口。
|
2026-10-21 09:03:31 +08:00 |
|
laoyang
|
6ab7bcfb76
|
fix(地点): 修掉 px/vp 单位混用的对齐 bug,输入框稳定钉在键盘上方,候选向上生长
- ensurePlaceVisible: getRectangleById 返回 px,scrollBy 收 vp,必须换算(上一版输入框被推飞的真因)
- 去掉 keyboardPadVp 动态底部留白:数学上不需要,只会在手动滚到底时露出大块空白
- 聚焦期间候选条数变化(加载中/有结果/无结果)每次都重新钉一次,避免输入框上下跳
- 提交请求前先收起旧候选,统一由一次对齐收口
- 新增 onBlur:失焦即收起候选,不留残影
|
2026-10-21 08:52:13 +08:00 |
|