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 的记录)。
This commit is contained in:
2026-11-04 08:23:42 +08:00
parent c0b9b44ca2
commit 413900bbce
6 changed files with 459 additions and 56 deletions
+15 -4
View File
@@ -161,8 +161,13 @@ export class ReminderService {
LogUtil.write(`取消全部提醒后系统仍剩 ${before} 条`);
}
const now: number = Date.now();
const rows: LocalEvent[] =
// ⭐ #38 日程 + 待办一起排提醒。两类分别查(过滤条件不同:
// 待办要求 completed=0,日程无此概念),合并后统一处理。
const eventRows: LocalEvent[] =
await EventDb.queryRemindable(context, now, now + ReminderService.WINDOW);
const todoRows: LocalEvent[] =
await EventDb.queryRemindableTodos(context, now, now + ReminderService.WINDOW);
const rows: LocalEvent[] = eventRows.concat(todoRows);
// 提醒静音的日历本(如别人分享的本):其中所有日程都不再提醒
const muted: string[] = await AppSettings.getMutedReminderKeys(context);
// ⭐⭐ 已镜像到系统日历的本:由系统日历负责提醒,App 不再重复排(消除"提醒两次")
@@ -193,6 +198,8 @@ export class ReminderService {
}
const d = new Date(remindAt);
const calName: string = await ReminderService.calName(context, e.calKey);
// #38 文案按类型区分:待办是"到期",日程是"开始"
const isTodo: boolean = e.kind === 'todo';
const request: reminderAgentManager.ReminderRequestCalendar = {
reminderType: reminderAgentManager.ReminderType.REMINDER_TYPE_CALENDAR,
dateTime: {
@@ -203,10 +210,14 @@ export class ReminderService {
minute: d.getMinutes(),
second: 0
},
title: '日程提醒',
content: `「${calName}」中的「${e.title}」将在 ${rem} 分钟后开始`,
title: isTodo ? '待办提醒' : '日程提醒',
content: isTodo
? `「${calName}」中的待办「${e.title}」将在 ${rem} 分钟后到期`
: `「${calName}」中的「${e.title}」将在 ${rem} 分钟后开始`,
// ⚠️ 通知 id 必须唯一:日程与待办的 id 来自同一张表(自增主键),
// 天然不冲突,所以这里不用额外加偏移,否则会与 rem 的乘加混淆。
notificationId: (e.id * 10 + rem) % 100000,
expiredContent: '该日程已开始',
expiredContent: isTodo ? '该待办已到期' : '该日程已开始',
ringDuration: 5,
snoozeTimes: 0,
timeInterval: 0