From 8497cfeb3f8a2c3dc55dca88ea8842ba63a4dfae Mon Sep 17 00:00:00 2001 From: laoyang Date: Wed, 4 Nov 2026 17:44:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=BE=85=E5=8A=9E):=20=E6=8A=95=E5=B0=84?= =?UTF-8?q?=E5=88=B0=E6=97=A5=E5=8E=86=E8=A7=86=E5=9B=BE=20=E2=80=94?= =?UTF-8?q?=E2=80=94=20=E6=97=A5=E6=97=B6=E9=97=B4=E8=BD=B4=E9=A1=B6?= =?UTF-8?q?=E9=83=A8=E6=98=BE=E7=A4=BA"=E5=BD=93=E5=A4=A9=E6=88=AA?= =?UTF-8?q?=E6=AD=A2"=E5=BE=85=E5=8A=9E=E5=8C=BA=20(#45)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 待办只有 DUE 没有 DTSTART,无法画时间轴色块;历史上 buildEventDaySet() 明确"待办不计入月视图"。本次按方案 A:在日视图时间轴顶部加「当天截止」待办区。 ## 显示 - 位置:日时间轴(月/周视图选中日下方 + 列表视图每天卡片)顶部、 全天/跨天区之上,随内容一起滚动 - 每行:勾选圈(颜色跟随日历本)+ 标题(完成态划线) - 只有「当天截止 + 未完成 + 有截止时间」的待办才显示,按截止时间升序 ## 实现 - `DayTimeline` 加 `todos` 字段 - `Index.todosDueOn(dateMs)` 筛选当天截止待办 - `rebuildDayTimeline()` / `buildAgendaTimelines()` 两处填充 - `DayTimelineView` 加 `onToggleTodo` 回调 + 待办区渲染 - 勾选直接复用 `Index.toggleTodo`(级联完成、提醒重排等既有逻辑全部生效) ## 刷新 toggleTodo 成功后除 reload this.todos,还重建 dayTimeline / agendaTimelines —— 否则待办区是快照,勾选后不消失/不划线。 ## 验证 - 编译 BUILD SUCCESSFUL - 筛选逻辑等价验证:只显示(当天截止+未完成+有截止),按时间升序,边界正确 - 真机未验(设备离线) ## 未验证 真机:勾选圈交互、待办区随数据刷新、列表视图卡片里的待办区显示。 --- entry/src/main/ets/common/TimelineUtil.ets | 2 + entry/src/main/ets/pages/Index.ets | 73 +++++++++++++++++++++- 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/entry/src/main/ets/common/TimelineUtil.ets b/entry/src/main/ets/common/TimelineUtil.ets index 884646d..b89e2ec 100644 --- a/entry/src/main/ets/common/TimelineUtil.ets +++ b/entry/src/main/ets/common/TimelineUtil.ets @@ -36,6 +36,8 @@ export class DayTimeline { allDay: DisplayEvent[] = []; // 全天 / 跨天(画在 0 点之前) blocks: TimelineBlock[] = []; // 时间轴色块 hasTimed: boolean = false; // 当天是否有有时间日程 + /** 当天截止、尚未完成的待办(画在时间轴顶部、全天区之上,带勾选框) */ + todos: DisplayEvent[] = []; } /** 冲突分组:一组时间上互相重叠的日程(用于横向平分宽度) */ diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index f6bcdf1..4d7f44f 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -654,13 +654,27 @@ struct Index { this.rebuildDayTimeline(); } + /** + * 当天截止、尚未完成的待办(#45 待办投射到日历)。 + * 待办的 DUE 存在 startTime;只有未完成且有截止时间的才显示。 + */ + private todosDueOn(dateMs: number): DisplayEvent[] { + const day: number = this.startOfDay(dateMs); + const out: DisplayEvent[] = this.todos.filter( + (t: DisplayEvent): boolean => !t.completed && t.startTime > 0 + && this.startOfDay(t.startTime) === day); + out.sort((a: DisplayEvent, b: DisplayEvent): number => a.startTime - b.startTime); + return out; + } + /** 重建"选中日"的时间轴布局(月/周视图下方那条) */ private rebuildDayTimeline(): void { const dayEvents: DisplayEvent[] = this.eventsOfDate(this.selectedDate); const tl: DayTimeline = TimelineUtil.build(dayEvents, this.selectedDate); + tl.todos = this.todosDueOn(this.selectedDate); TimelineUtil.markNow(tl.blocks, this.nowMs); this.dayTimeline = tl; - LogUtil.write(`时间轴(${this.fmtDateCn(this.selectedDate)}):输入 ${dayEvents.length} 条 → 色块 ${tl.blocks.length} 块,全天/跨天 ${tl.allDay.length} 条`); + LogUtil.write(`时间轴(${this.fmtDateCn(this.selectedDate)}):输入 ${dayEvents.length} 条 → 色块 ${tl.blocks.length} 块,全天/跨天 ${tl.allDay.length} 条,待办 ${tl.todos.length} 条`); } /** 选中日期变化(点月格/周条)→ 重算该日时间轴 */ @@ -680,6 +694,7 @@ struct Index { d.allDay = this.dayTimeline.allDay; d.blocks = this.dayTimeline.blocks; d.hasTimed = this.dayTimeline.hasTimed; + d.todos = this.dayTimeline.todos; this.dayTimeline = d; this.agendaTimelines = this.agendaTimelines.slice(); } @@ -2382,7 +2397,10 @@ struct Index { nowMs: this.nowMs, showNowLine: this.selectedDate === this.startOfDay(this.nowMs), scrollable: true, - onPick: (e: DisplayEvent): void => this.openEvent(e) + onPick: (e: DisplayEvent): void => this.openEvent(e), + onToggleTodo: (t: DisplayEvent): void => { + this.toggleTodo(t); + } }) if (!this.dayTimeline.hasTimed && this.dayTimeline.allDay.length === 0 && !this.loading) { Text('当天没有日程') @@ -2513,7 +2531,10 @@ struct Index { nowMs: this.nowMs, showNowLine: day.isToday, scrollable: false, - onPick: (e: DisplayEvent): void => this.openEvent(e) + onPick: (e: DisplayEvent): void => this.openEvent(e), + onToggleTodo: (t: DisplayEvent): void => { + this.toggleTodo(t); + } }) } .width('100%') @@ -2698,6 +2719,7 @@ struct Index { g.label = key === today ? '今天' : this.fmtDateCn(key); g.isToday = key === today; const tl: DayTimeline = TimelineUtil.build(map.get(key) ?? [], key); + tl.todos = this.todosDueOn(key); TimelineUtil.markNow(tl.blocks, this.nowMs); g.timeline = tl; out.push(g); @@ -4132,6 +4154,11 @@ struct Index { } // 重新读库刷新列表(服务层已更新 completed) this.todos = await CalendarDataService.loadTodos(ctx, this.sources); + // ⭐ 时间轴上的待办区是快照,勾选后必须重建,否则不消失/不划线(#45) + this.rebuildDayTimeline(); + if (this.mode === 'agenda') { + this.agendaTimelines = this.buildAgendaTimelines(this.agendaEvents); + } // ⭐ 桌面卡片是独立实例,必须显式推送,否则停在旧数据 await CardDataService.pushToAllForms(ctx); // 完成/取消完成会改变"该不该提醒",需要重排 @@ -4353,6 +4380,7 @@ struct DayTimelineView { private twoLineVp: number = 30; // 能放下"标题 + 时间/地点"两行的高度门槛 private threeLineVp: number = 46; // 能放下"标题 / 时间 / 地址"三行的高度门槛 onPick: (e: DisplayEvent) => void = (e: DisplayEvent): void => {}; + onToggleTodo: (t: DisplayEvent) => void = (t: DisplayEvent): void => {}; // 勾选待办回调 private scroller: Scroller = new Scroller(); @State private viewportH: number = 0; // 滚动视口高度(由 Scroll 实测,用于把"当前时刻"居中) @State private allDayH: number = 0; // 顶部"全天/跨天"区高度(它现在随内容滚动,居中定位要加上) @@ -4629,6 +4657,45 @@ struct DayTimelineView { @Builder private timelineBody() { Column() { + // ⭐ 当天截止待办区(#45):画在最顶,全天/跨天之上,随内容一起滚动。 + if (this.timeline.todos.length > 0) { + Column({ space: 4 }) { + ForEach(this.timeline.todos, (td: DisplayEvent) => { + Row({ space: 8 }) { + // 勾选圈:点击切换完成(与待办列表同一回调) + Text(td.completed ? '✓' : '') + .fontSize(12) + .fontColor(td.completed ? '#FFFFFF' : 'rgba(0,0,0,0)') + .width(18) + .height(18) + .textAlign(TextAlign.Center) + .borderRadius(9) + .backgroundColor(td.completed ? td.color : 'rgba(0,0,0,0)') + .border({ width: 1.5, color: td.color }) + .onClick(() => { + this.onToggleTodo(td); + }) + Text(td.title === '' ? '(无标题待办)' : td.title) + .fontSize(12) + .fontColor($r('app.color.text_primary')) + .decoration({ type: td.completed ? TextDecorationType.LineThrough : TextDecorationType.None }) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .layoutWeight(1) + } + .alignItems(VerticalAlign.Center) + .width('100%') + .height(24) + .padding({ left: 8, right: 8 }) + .borderRadius(6) + .backgroundColor($r('app.color.card_bg')) + .border({ width: 0.5, color: $r('app.color.shadow_color') }) + }, (td: DisplayEvent) => `todo_${this.timeline.dateKey}_${td.id}_${td.completed}`) + } + .width('100%') + .padding({ bottom: 6 }) + } + // 全天 / 跨天日程:同样用**色块**展示(与普通日程保持一致的视觉), // 画在 0 点之前并**随内容一起滚动**(向下滑时跟 0 点一起上移,不再常驻顶部) if (this.timeline.allDay.length > 0) {