From fa152415cd312e4ecc387956d6d57b472de81749 Mon Sep 17 00:00:00 2001 From: Yang Yongquan Date: Mon, 14 Sep 2026 10:18:31 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=BA=E6=97=A5=E7=A8=8B=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=E4=B8=80=E6=9D=A1=E5=BD=93=E5=89=8D?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E7=9A=84=E6=A8=AA=E7=BA=BF=EF=BC=8C=E7=94=A8?= =?UTF-8?q?=E6=9D=A5=E8=A1=A8=E7=A4=BA=E5=BD=93=E5=89=8D=E8=BF=9B=E5=BA=A6?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Yang Yongquan --- entry/src/main/ets/common/CardDataService.ets | 46 ++++++ entry/src/main/ets/pages/Index.ets | 144 +++++++++++++++++- entry/src/main/ets/pages/widget/Widget2x2.ets | 16 +- entry/src/main/ets/pages/widget/Widget4x2.ets | 47 +++++- entry/src/main/ets/pages/widget/Widget4x4.ets | 47 +++++- entry/src/main/ets/pages/widget/Widget6x4.ets | 47 +++++- 6 files changed, 326 insertions(+), 21 deletions(-) diff --git a/entry/src/main/ets/common/CardDataService.ets b/entry/src/main/ets/common/CardDataService.ets index 14fd6db..729f67b 100644 --- a/entry/src/main/ets/common/CardDataService.ets +++ b/entry/src/main/ets/common/CardDataService.ets @@ -17,6 +17,13 @@ export class CardItem { showDate: boolean = false; // 是否为当天分组的第一条(卡片上渲染日期头) calName: string = ''; // 所属日历本名(右侧显示,颜色同日历色) color: string = '#007DFF'; + // 当前时间红线:start/end 为日程实际起止毫秒(用于定位"现在"位置); + // showNowLine 表示该日程上方应绘制红线,isNow 表示该日程正在进行中 + startMs: number = 0; + endMs: number = 0; + showNowLine: boolean = false; // 红线画在该日程上方 + isNow: boolean = false; // 该日程正在进行中 + nowLineBelow: boolean = false; // 今天所有有时间日程已结束:红线画在最后一条下方 } /** 卡片整体数据 */ @@ -28,6 +35,7 @@ export class CardData { dateMd: string = ''; weekday: string = ''; todayCount: number = 0; + ongoingCount: number = 0; // 此刻正在进行的日程条数(2x2 卡片红点提示用) } export class CardDataService { @@ -192,9 +200,47 @@ export class CardDataService { item.showDate = i === 0; // 当天分组第一条 → 卡片上显示日期头 item.calName = e.calName; item.color = e.color; + // 红线定位:记录实际起止毫秒(用于卡片渲染时判断"现在"位置) + item.startMs = e.startTime; + item.endMs = e.endTime; items.push(item); } } + // 红线逻辑:仅对"今天"分组、且为"有时间"的日程生效(全天/跨天按全天展示,不参与时间轴定位) + const nowMs: number = Date.now(); + let linePlaced: boolean = false; + // 1) 正在进行的日程(start<=now nowMs) { + it.showNowLine = true; + linePlaced = true; + break; + } + } + } + // 3) 否则:今天所有有时间日程均已结束 → 红线画在最后一条下方(标记"今日已结束") + if (!linePlaced) { + for (let k = items.length - 1; k >= 0; k--) { + const it = items[k]; + if (it.date === '今天' && it.time !== '全天') { + it.nowLineBelow = true; + linePlaced = true; + break; + } + } + } + // 进行中条数:用于 2x2 卡片红点提示 + data.ongoingCount = items.filter((i: CardItem): boolean => i.isNow).length; data.eventsJson = JSON.stringify(items); // 今日日程条数:item.date === '今天' 的均为今天分组(含全天/跨天进行中) data.todayCount = items.filter((i: CardItem): boolean => i.date === '今天').length; diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index 2730b80..7d09e4d 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -54,12 +54,14 @@ struct Index { private swiperController: SwiperController = new SwiperController(); private swiperGuard: boolean = false; private autoSyncTimer: number = -1; + private nowTimer: number = -1; // 红线位置"播放"计时器(每 30s 刷新 nowMs) private lastSyncTime: number = 0; private permissionAsked: boolean = false; @State detailShow: boolean = false; // 只读日程详情半屏弹层 @State detailEvent: DisplayEvent | null = null; // 服务卡片"添加"按钮深链:EntryAbility 写入,本页读取后拉起新建日程页 @Watch('onWidgetActionChange') @StorageLink('widgetAction') widgetAction: string = ''; + @State nowMs: number = Date.now(); // 当前时刻毫秒(驱动红线"播放"位置,每 30s 更新) aboutToAppear(): void { const ctx = this.getUIContext().getHostContext(); @@ -74,6 +76,10 @@ struct Index { this.rebuildPages(); this.initLandscapeListener(); this.initPermissionAndLoad(); + // 红线"播放"效果:每 30s 更新 nowMs,让当前时间红线随真实时间推进 + this.nowTimer = setInterval((): void => { + this.nowMs = Date.now(); + }, 30000); } aboutToDisappear(): void { @@ -81,6 +87,10 @@ struct Index { clearInterval(this.autoSyncTimer); this.autoSyncTimer = -1; } + if (this.nowTimer !== -1) { + clearInterval(this.nowTimer); + this.nowTimer = -1; + } if (this.landscapeListener !== null) { this.landscapeListener.off('change'); this.landscapeListener = null; @@ -279,6 +289,50 @@ struct Index { this.startOfDay(e.startTime) <= dateMs && e.endTime >= dateMs); } + /** 红线日程唯一 key(与 ForEach key 保持一致) */ + private nowLineKey(e: DisplayEvent): string { + return `${e.isSystem ? 's' : 'l'}${e.id}_${e.startTime}`; + } + + /** 计算某日列表中"当前时间红线"位置:返回应画在上方/下方的日程 key,及正在进行的日程 key 集合。 + * 仅 isToday 为 true 时生效(红线表达的是"此刻",非今天无意义)。 */ + private computeNowLine(list: DisplayEvent[], isToday: boolean): NowLineInfo { + const res: NowLineInfo = new NowLineInfo(); + if (!isToday) { + return res; + } + const now: number = this.nowMs; + const timed: DisplayEvent[] = list + .filter((e: DisplayEvent): boolean => !e.isAllDay && !this.spansDays(e)) + .sort((a: DisplayEvent, b: DisplayEvent): number => a.startTime - b.startTime); + // 1) 正在进行的(start<=now now) { + res.aboveKey = this.nowLineKey(e); + return res; + } + } + // 3) 全部已结束 → 红线画在最后一条下方 + if (timed.length > 0) { + res.belowKey = this.nowLineKey(timed[timed.length - 1]); + } + return res; + } + private switchMonth(delta: number): void { const nm = this.addMonths(this.displayYear, this.displayMonth, delta); this.displayYear = nm[0]; @@ -1526,7 +1580,19 @@ struct Index { Scroll() { Column({ space: 8 }) { ForEach(this.eventsOfDate(this.selectedDate), (e: DisplayEvent) => { - this.eventRow(e) + Column() { + if (this.computeNowLine(this.eventsOfDate(this.selectedDate), + this.selectedDate === this.startOfDay(this.nowMs)).aboveKey === this.nowLineKey(e)) { + NowLineMarker() + } + this.eventRow(e, this.computeNowLine(this.eventsOfDate(this.selectedDate), + this.selectedDate === this.startOfDay(this.nowMs)).nowKeys.has(this.nowLineKey(e))) + if (this.computeNowLine(this.eventsOfDate(this.selectedDate), + this.selectedDate === this.startOfDay(this.nowMs)).belowKey === this.nowLineKey(e)) { + NowLineMarker() + } + } + .width('100%') }, (e: DisplayEvent) => `${e.isSystem ? 's' : 'l'}${e.id}_${e.startTime}`) if (this.eventsOfDate(this.selectedDate).length === 0 && !this.loading) { Text('当天没有日程') @@ -1577,7 +1643,17 @@ struct Index { } ForEach(group.items, (e: DisplayEvent) => { ListItem() { - this.eventRow(e) + Column() { + if (this.computeNowLine(group.items, group.label === '今天').aboveKey === this.nowLineKey(e)) { + NowLineMarker() + } + this.eventRow(e, this.computeNowLine(group.items, group.label === '今天') + .nowKeys.has(this.nowLineKey(e))) + if (this.computeNowLine(group.items, group.label === '今天').belowKey === this.nowLineKey(e)) { + NowLineMarker() + } + } + .width('100%') } }, (e: DisplayEvent) => `${e.isSystem ? 's' : 'l'}${e.id}_${e.startTime}_${e.title}`) }, (group: AgendaGroup) => group.label) @@ -1744,24 +1820,24 @@ struct Index { } @Builder - eventRow(e: DisplayEvent) { + eventRow(e: DisplayEvent, isNow: boolean = false) { Row({ space: 10 }) { Column() .width(4) .height(38) .borderRadius(2) - .backgroundColor(e.color) + .backgroundColor(isNow ? '#FF3B30' : e.color) Column({ space: 3 }) { Text(e.title === '' ? '(无标题)' : e.title) .fontSize(15) - .fontColor($r('app.color.text_primary')) + .fontColor(isNow ? '#FF3B30' : $r('app.color.text_primary')) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Row({ space: 6 }) { Text(e.isAllDay || this.spansDays(e) ? '全天' : `${this.fmtTime(e.startTime)} - ${this.fmtTime(e.endTime)}`) .fontSize(12) - .fontColor($r('app.color.text_secondary')) + .fontColor(isNow ? '#FF3B30' : $r('app.color.text_secondary')) if (e.recurring) { Text('↻ 重复') .fontSize(10) @@ -1770,6 +1846,14 @@ struct Index { .borderRadius(4) .backgroundColor($r('app.color.chip_off_bg')) } + if (isNow) { + Text('● 进行中') + .fontSize(10) + .fontColor('#FF3B30') + .padding({ left: 4, right: 4, top: 1, bottom: 1 }) + .borderRadius(4) + .backgroundColor('#FFECEA') + } } if (e.location !== '') { Row({ space: 4 }) { @@ -1872,4 +1956,52 @@ struct Index { class AgendaGroup { label: string = ''; items: DisplayEvent[] = []; +} + +/** 某日列表中"当前时间红线"位置信息(避免内联对象字面量类型,ArkTS 不允许) */ +class NowLineInfo { + aboveKey: string = ''; // 红线画在该 key 对应日程的上方 + belowKey: string = ''; // 今天所有有时间日程已结束:红线画在该 key 对应日程的下方 + nowKeys: Set = new Set(); // 正在进行的日程 key 集合 +} + +/** 当前时间红线标记(带呼吸/播放效果):左侧红点 + 贯穿整行的红色细线。 + * 自带动画(opacity 往复),不依赖外部状态刷新,避免整页重绘。 */ +@Component +struct NowLineMarker { + @State private dim: boolean = false; + private timer: number = -1; + + aboutToAppear(): void { + // 每 ~1.1s 切换一次,配合 .animation 形成呼吸式"播放"效果 + this.timer = setInterval((): void => { + this.dim = !this.dim; + }, 1100); + } + + aboutToDisappear(): void { + if (this.timer !== -1) { + clearInterval(this.timer); + this.timer = -1; + } + } + + build() { + Row() { + Column() + .width(6) + .height(6) + .borderRadius(3) + .backgroundColor('#FF3B30') + Column() + .height(2) + .layoutWeight(1) + .backgroundColor('#FF3B30') + .borderRadius(1) + } + .width('100%') + .padding({ top: 3, bottom: 3 }) + .opacity(this.dim ? 0.45 : 1) + .animation({ duration: 1000 }) + } } \ No newline at end of file diff --git a/entry/src/main/ets/pages/widget/Widget2x2.ets b/entry/src/main/ets/pages/widget/Widget2x2.ets index 6a5138d..626cd02 100644 --- a/entry/src/main/ets/pages/widget/Widget2x2.ets +++ b/entry/src/main/ets/pages/widget/Widget2x2.ets @@ -11,6 +11,7 @@ struct Widget2x2Card { @LocalStorageProp('dateMd') dateMd: string = ''; @LocalStorageProp('weekday') weekday: string = ''; @LocalStorageProp('todayCount') todayCount: number = 0; + @LocalStorageProp('ongoingCount') ongoingCount: number = 0; // 此刻正在进行的日程条数 /** 右上角添加按钮:拉起 App 直接进入新建日程页(阻止冒泡,避免同时打开 App 首页) */ @Builder @@ -78,9 +79,18 @@ struct Widget2x2Card { .fontWeight(FontWeight.Bold) .fontColor('#007DFF') Column({ space: 2 }) { - Text('今日日程') - .fontSize(12) - .fontColor('#1A1A1A') + Row({ space: 4 }) { + if (this.ongoingCount > 0) { + Column() + .width(8) + .height(8) + .borderRadius(4) + .backgroundColor('#FF3B30') + } + Text(this.ongoingCount > 0 ? `进行中 ${this.ongoingCount}` : '今日日程') + .fontSize(12) + .fontColor(this.ongoingCount > 0 ? '#FF3B30' : '#1A1A1A') + } Text(this.todayCount === 0 ? '点击添加' : '点击查看') .fontSize(10) .fontColor('#8A8A8A') diff --git a/entry/src/main/ets/pages/widget/Widget4x2.ets b/entry/src/main/ets/pages/widget/Widget4x2.ets index feae9f7..47db44c 100644 --- a/entry/src/main/ets/pages/widget/Widget4x2.ets +++ b/entry/src/main/ets/pages/widget/Widget4x2.ets @@ -10,6 +10,12 @@ class CardItem2x4 { showDate: boolean = false; calName: string = ''; color: string = '#007DFF'; + // 红线:startMs/endMs 实际起止;showNowLine 上方画红线;isNow 正在进行;nowLineBelow 画在底部 + startMs: number = 0; + endMs: number = 0; + showNowLine: boolean = false; + isNow: boolean = false; + nowLineBelow: boolean = false; } @Entry(storage2x4) @@ -88,6 +94,25 @@ struct Widget4x2Card { .backgroundColor('#F5F7FA') } + /** 当前时间红线标记:左侧红点 + 贯穿整行的红色细线 */ + @Builder + nowLine() { + Row() { + Column() + .width(6) + .height(6) + .borderRadius(3) + .backgroundColor('#FF3B30') + Column() + .height(2) + .layoutWeight(1) + .backgroundColor('#FF3B30') + .borderRadius(1) + } + .width('100%') + .padding({ top: 3, bottom: 3 }) + } + /** 有时间事件行(时间轴):圆角矩形 + 左色竖条 + 开始时间(上)-竖线-结束时间(下) + 标题 */ @Builder buildTimedRow(item: CardItem2x4) { @@ -96,13 +121,13 @@ struct Widget4x2Card { .width(3) .height(36) .borderRadius(2) - .backgroundColor(item.color) + .backgroundColor(item.isNow ? '#FF3B30' : item.color) // 时间列:开始时间在上、结束时间在下、中间竖线连接 Column({ space: 2 }) { Text(item.time) .fontSize(10) .fontWeight(FontWeight.Medium) - .fontColor('#333333') + .fontColor(item.isNow ? '#FF3B30' : '#333333') Column() .width(1.5) .layoutWeight(1) @@ -117,10 +142,18 @@ struct Widget4x2Card { .height(36) Text(item.title) .fontSize(12) - .fontColor('#1A1A1A') + .fontColor(item.isNow ? '#FF3B30' : '#1A1A1A') .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) .layoutWeight(1) + if (item.isNow) { + Text('● 进行中') + .fontSize(9) + .fontColor('#FF3B30') + .padding({ left: 4, right: 4, top: 1, bottom: 1 }) + .borderRadius(4) + .backgroundColor('#FFECEA') + } if (item.calName !== '') { Text(item.calName) .fontSize(9) @@ -134,7 +167,7 @@ struct Widget4x2Card { .width('100%') .padding({ left: 8, right: 8, top: 4, bottom: 4 }) .borderRadius(8) - .backgroundColor('#F5F7FA') + .backgroundColor(item.isNow ? '#FFF1F0' : '#F5F7FA') } /** 卡片"打开 App":点击日期区或日程列表触发;添加按钮是 header 行的兄弟节点,其点击不会冒泡到这里 */ @@ -190,11 +223,17 @@ struct Widget4x2Card { .fontColor('#666666') .width('100%') } + if (item.showNowLine && !item.nowLineBelow) { + this.nowLine() + } if (item.time === '全天') { this.buildAllDayRow(item) } else { this.buildTimedRow(item) } + if (item.nowLineBelow) { + this.nowLine() + } } .width('100%') } diff --git a/entry/src/main/ets/pages/widget/Widget4x4.ets b/entry/src/main/ets/pages/widget/Widget4x4.ets index 55d184a..6128f03 100644 --- a/entry/src/main/ets/pages/widget/Widget4x4.ets +++ b/entry/src/main/ets/pages/widget/Widget4x4.ets @@ -10,6 +10,12 @@ class CardItem4x4 { showDate: boolean = false; calName: string = ''; color: string = '#007DFF'; + // 红线:startMs/endMs 实际起止;showNowLine 上方画红线;isNow 正在进行;nowLineBelow 画在底部 + startMs: number = 0; + endMs: number = 0; + showNowLine: boolean = false; + isNow: boolean = false; + nowLineBelow: boolean = false; } @Entry(storage4x4) @@ -87,6 +93,25 @@ struct Widget4x4Card { .backgroundColor('#F5F7FA') } + /** 当前时间红线标记:左侧红点 + 贯穿整行的红色细线 */ + @Builder + nowLine() { + Row() { + Column() + .width(6) + .height(6) + .borderRadius(3) + .backgroundColor('#FF3B30') + Column() + .height(2) + .layoutWeight(1) + .backgroundColor('#FF3B30') + .borderRadius(1) + } + .width('100%') + .padding({ top: 3, bottom: 3 }) + } + /** 有时间事件行(时间轴):圆角矩形 + 左色竖条 + 开始时间(上)-竖线-结束时间(下) + 标题 */ @Builder buildTimedRow(item: CardItem4x4) { @@ -95,13 +120,13 @@ struct Widget4x4Card { .width(3) .height(38) .borderRadius(2) - .backgroundColor(item.color) + .backgroundColor(item.isNow ? '#FF3B30' : item.color) // 时间列:开始时间在上、结束时间在下、中间竖线连接 Column({ space: 2 }) { Text(item.time) .fontSize(10) .fontWeight(FontWeight.Medium) - .fontColor('#333333') + .fontColor(item.isNow ? '#FF3B30' : '#333333') Column() .width(1.5) .layoutWeight(1) @@ -116,10 +141,18 @@ struct Widget4x4Card { .height(38) Text(item.title) .fontSize(12) - .fontColor('#1A1A1A') + .fontColor(item.isNow ? '#FF3B30' : '#1A1A1A') .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) .layoutWeight(1) + if (item.isNow) { + Text('● 进行中') + .fontSize(9) + .fontColor('#FF3B30') + .padding({ left: 4, right: 4, top: 1, bottom: 1 }) + .borderRadius(4) + .backgroundColor('#FFECEA') + } if (item.calName !== '') { Text(item.calName) .fontSize(9) @@ -133,7 +166,7 @@ struct Widget4x4Card { .width('100%') .padding({ left: 8, right: 8, top: 5, bottom: 5 }) .borderRadius(8) - .backgroundColor('#F5F7FA') + .backgroundColor(item.isNow ? '#FFF1F0' : '#F5F7FA') } /** 卡片"打开 App":点击日期区或日程列表触发;添加按钮是 header 行的兄弟节点,其点击不会冒泡到这里 */ @@ -192,11 +225,17 @@ struct Widget4x4Card { .fontColor('#666666') .width('100%') } + if (item.showNowLine && !item.nowLineBelow) { + this.nowLine() + } if (item.time === '全天') { this.buildAllDayRow(item) } else { this.buildTimedRow(item) } + if (item.nowLineBelow) { + this.nowLine() + } } .width('100%') } diff --git a/entry/src/main/ets/pages/widget/Widget6x4.ets b/entry/src/main/ets/pages/widget/Widget6x4.ets index cbd806a..3067dc4 100644 --- a/entry/src/main/ets/pages/widget/Widget6x4.ets +++ b/entry/src/main/ets/pages/widget/Widget6x4.ets @@ -10,6 +10,12 @@ class CardItem6x4 { showDate: boolean = false; calName: string = ''; color: string = '#007DFF'; + // 红线:startMs/endMs 实际起止;showNowLine 上方画红线;isNow 正在进行;nowLineBelow 画在底部 + startMs: number = 0; + endMs: number = 0; + showNowLine: boolean = false; + isNow: boolean = false; + nowLineBelow: boolean = false; } @Entry(storage6x4) @@ -87,6 +93,25 @@ struct Widget6x4Card { .backgroundColor('#F5F7FA') } + /** 当前时间红线标记:左侧红点 + 贯穿整行的红色细线 */ + @Builder + nowLine() { + Row() { + Column() + .width(6) + .height(6) + .borderRadius(3) + .backgroundColor('#FF3B30') + Column() + .height(2) + .layoutWeight(1) + .backgroundColor('#FF3B30') + .borderRadius(1) + } + .width('100%') + .padding({ top: 3, bottom: 3 }) + } + /** 有时间事件行(时间轴):圆角矩形 + 左色竖条 + 开始时间(上)-竖线-结束时间(下) + 标题 */ @Builder buildTimedRow(item: CardItem6x4) { @@ -95,13 +120,13 @@ struct Widget6x4Card { .width(3) .height(38) .borderRadius(2) - .backgroundColor(item.color) + .backgroundColor(item.isNow ? '#FF3B30' : item.color) // 时间列:开始时间在上、结束时间在下、中间竖线连接 Column({ space: 2 }) { Text(item.time) .fontSize(10) .fontWeight(FontWeight.Medium) - .fontColor('#333333') + .fontColor(item.isNow ? '#FF3B30' : '#333333') Column() .width(1.5) .layoutWeight(1) @@ -116,10 +141,18 @@ struct Widget6x4Card { .height(38) Text(item.title) .fontSize(12) - .fontColor('#1A1A1A') + .fontColor(item.isNow ? '#FF3B30' : '#1A1A1A') .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) .layoutWeight(1) + if (item.isNow) { + Text('● 进行中') + .fontSize(9) + .fontColor('#FF3B30') + .padding({ left: 4, right: 4, top: 1, bottom: 1 }) + .borderRadius(4) + .backgroundColor('#FFECEA') + } if (item.calName !== '') { Text(item.calName) .fontSize(9) @@ -133,7 +166,7 @@ struct Widget6x4Card { .width('100%') .padding({ left: 8, right: 8, top: 5, bottom: 5 }) .borderRadius(8) - .backgroundColor('#F5F7FA') + .backgroundColor(item.isNow ? '#FFF1F0' : '#F5F7FA') } /** 卡片"打开 App":点击日期区或日程列表触发;添加按钮是 header 行的兄弟节点,其点击不会冒泡到这里 */ @@ -192,11 +225,17 @@ struct Widget6x4Card { .fontColor('#666666') .width('100%') } + if (item.showNowLine && !item.nowLineBelow) { + this.nowLine() + } if (item.time === '全天') { this.buildAllDayRow(item) } else { this.buildTimedRow(item) } + if (item.nowLineBelow) { + this.nowLine() + } } .width('100%') }