From 2173b06b439a7327b4b2f6dfb1ea031598df3970 Mon Sep 17 00:00:00 2001 From: Yang Yongquan Date: Wed, 16 Sep 2026 10:07:57 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=BA=86=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E6=A8=A1=E5=BC=8F=EF=BC=8C=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=8F=AF=E4=BB=A5=E5=9C=A8=E8=AE=BE=E7=BD=AE=E4=B8=AD=E8=BF=9B?= =?UTF-8?q?=E8=A1=8C=E9=80=89=E6=8B=A9=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/AppSettings.ets | 31 ++ entry/src/main/ets/common/CardDataService.ets | 34 +- entry/src/main/ets/pages/Index.ets | 395 +++++++++++++----- entry/src/main/ets/pages/SettingsPage.ets | 61 +++ entry/src/main/ets/pages/widget/Widget4x4.ets | 122 ++++++ entry/src/main/ets/pages/widget/Widget6x4.ets | 122 ++++++ 6 files changed, 668 insertions(+), 97 deletions(-) diff --git a/entry/src/main/ets/common/AppSettings.ets b/entry/src/main/ets/common/AppSettings.ets index 8825c83..dba47c0 100644 --- a/entry/src/main/ets/common/AppSettings.ets +++ b/entry/src/main/ets/common/AppSettings.ets @@ -17,6 +17,7 @@ export class AppSettings { private static readonly KEY_REMINDER_TICK: string = 'reminder_tick'; // 应用内提醒上次检查时间戳 private static readonly KEY_FULL_REFETCH: string = 'full_refetch_done'; // 一次性全量重拉已完成 private static readonly KEY_DEFAULT_VIEW: string = 'default_view'; // 打开 App 默认视图 + private static readonly KEY_DISPLAY_STYLE: string = 'display_style'; // 日程显示方式:'timeline' | 'list' private static readonly KEY_POLICY_AGREED: string = 'policy_agreed'; // 是否已同意隐私政策与用户协议 // 首启功能引导"用户已看过并关闭"的标记。键名带版本号:改动引导内容后把 vN 加 1,用户即可再看一次。 // 注意:只在用户主动关闭("开始使用" / ✕ / 点遮罩)时才置 true,**不再"显示前就置 true"**—— @@ -118,6 +119,36 @@ export class AppSettings { } } + /** + * 日程显示方式:'timeline'(竖向时间轴,默认) | 'list'(列表)。 + * + * 两种风格各有偏好:时间轴强于"看一天的时间占用与冲突",列表强于"快速扫读与长标题"。 + * 该设置同时作用于主界面(月/周视图下方、列表视图)与桌面服务卡片(4×4 / 6×4)。 + * 列表模式下不绘制"当前时间红线"(列表没有时间刻度,红线没有落点)。 + */ + static async getDisplayStyle(context: common.Context): Promise { + try { + const store: preferences.Preferences = + await preferences.getPreferences(context, AppSettings.STORE); + const v: string = await store.get(AppSettings.KEY_DISPLAY_STYLE, 'timeline') as string; + return v === 'list' ? 'list' : 'timeline'; + } catch (err) { + return 'timeline'; + } + } + + static async setDisplayStyle(context: common.Context, style: string): Promise { + try { + const store: preferences.Preferences = + await preferences.getPreferences(context, AppSettings.STORE); + await store.put(AppSettings.KEY_DISPLAY_STYLE, style === 'list' ? 'list' : 'timeline'); + await store.flush(); + } catch (err) { + const e = err as BusinessError; + console.error(`保存日程显示方式失败: ${e.message}`); + } + } + /** * 是否还需要一次性全量重拉:修复历史同步(REPORT 剥离 VALARM 时期)落库的残缺数据。 * 全量重拉期间忽略 etag 复用,所有资源 GET 完整 ICS;成功完成后标记,恢复增量模式。 diff --git a/entry/src/main/ets/common/CardDataService.ets b/entry/src/main/ets/common/CardDataService.ets index 2486a11..4ea277c 100644 --- a/entry/src/main/ets/common/CardDataService.ets +++ b/entry/src/main/ets/common/CardDataService.ets @@ -5,9 +5,10 @@ import { formBindingData, formProvider } from '@kit.FormKit'; import { BusinessError } from '@kit.BasicServicesKit'; import { preferences } from '@kit.ArkData'; import { CalendarDataService, DisplayEvent } from './CalendarDataService'; +import { AppSettings } from './AppSettings'; import { LunarUtil } from './LunarUtil'; import { LogUtil } from './LogUtil'; -import { TimelineUtil, DayTimeline } from './TimelineUtil'; +import { TimelineUtil, DayTimeline, TimelineBlock } from './TimelineUtil'; /** 卡片单条日程(按天分组:组内全天事件在前、有时间的按开始时间排序) */ export class CardItem { @@ -15,6 +16,7 @@ export class CardItem { time: string = ''; // 开始时间 '08:30' / '全天' endTime: string = ''; // 结束时间 '10:00'(全天事件为空) date: string = ''; // '9月15日' + calName: string = ''; // 所属日历本名称(列表模式卡片右侧展示,颜色同日历本) showDate: boolean = false; // 是否为当天分组的第一条(卡片上渲染日期头) color: string = '#007DFF'; // 当前时间红线:start/end 为日程实际起止毫秒(用于定位"现在"位置); @@ -51,6 +53,10 @@ export class CardData { nowRatio: number = 0; // 当前时间在当天时间轴上的比例(0~1),卡片画红线用 nowLabel: string = ''; // 当前时间文字(如 '14:05') allDayJson: string = '[]'; // 全天/跨天日程(CardAllDay[] JSON,卡片画在最上方) + // 列表模式:当前显示日的"行"式日程(CardItem[],按全天在前、时间升序;无红线) + listJson: string = '[]'; + // 日程显示方式:'timeline' | 'list'(与 App 内设置联动;列表模式卡片不画红线) + displayStyle: string = 'timeline'; // ===== 卡片翻页(上一天 / 下一天 / 回到今天)===== dayOffset: number = 0; // 相对今天的天数偏移(0=今天) isToday: boolean = true; // 当前显示的是否为今天(非今天不画红线) @@ -343,7 +349,31 @@ export class CardDataService { data.nowRatio = data.isToday ? TimelineUtil.nowRatio(nowMs) : -1; data.nowLabel = TimelineUtil.nowLabel(nowMs); data.dayCount = tl.blocks.length + tl.allDay.length; - LogUtil.write(`卡片数据刷新:${items.length} 条,今日 ${data.todayCount} 条,进行中 ${ongoingItems.length} 条,时间轴 ${tl.blocks.length} 块`); + // 列表模式数据:当前显示日的"行"式日程(全天在前、有时间按开始时间升序)。 + // 进行中(仅今天的有时间日程)用 isNow 标记 → 卡片上以红条 + "进行中"呈现,**不画横线红线**。 + const dayList: CardItem[] = []; + const dayAllDay: DisplayEvent[] = tl.allDay; + const dayTimed: DisplayEvent[] = tl.blocks + .filter((b: TimelineBlock): boolean => b.ev !== null) + .map((b: TimelineBlock): DisplayEvent => b.ev as DisplayEvent) + .sort((a: DisplayEvent, b: DisplayEvent): number => a.startTime - b.startTime); + for (const e of dayAllDay.concat(dayTimed)) { + const li = new CardItem(); + li.title = e.title === '' ? '(无标题)' : e.title; + const isDayLong: boolean = e.isAllDay || spansDays(e); + const ds: Date = new Date(e.startTime); + const de: Date = new Date(e.endTime); + li.time = isDayLong ? '全天' : `${p(ds.getHours())}:${p(ds.getMinutes())}`; + li.endTime = isDayLong ? '' : `${p(de.getHours())}:${p(de.getMinutes())}`; + li.color = e.color; + li.calName = e.calName; + li.isNow = data.isToday && !isDayLong && e.startTime <= nowMs && nowMs < e.endTime; + dayList.push(li); + } + data.listJson = JSON.stringify(dayList); + // 显示方式:与 App 内设置联动(列表模式卡片不画当前时间红线) + data.displayStyle = await AppSettings.getDisplayStyle(context); + LogUtil.write(`卡片数据刷新:${items.length} 条,今日 ${data.todayCount} 条,进行中 ${ongoingItems.length} 条,时间轴 ${tl.blocks.length} 块,显示方式 ${data.displayStyle}`); } catch (err) { LogUtil.write(`卡片数据刷新失败: ${JSON.stringify(err)}`); } diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index 29c17b9..50b0c22 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -75,6 +75,9 @@ struct Index { // 时间轴视图(月/周/列表共用):按天预计算好色块布局,避免 build 中重算 @State dayTimeline: DayTimeline = new DayTimeline(); // 选中日(月/周视图用) @State agendaTimelines: AgendaTimelineGroup[] = []; // 列表视图:每天一条时间轴 + // 日程显示方式:'timeline'(竖向时间轴,默认) | 'list'(列表)。 + // 列表模式无时间刻度 → 不绘制"当前时间红线",改用"进行中"文字标记。 + @State displayStyle: string = 'timeline'; // ---- 隐私政策与用户协议首启同意(未同意前不申请任何权限、不加载数据) ---- @State policyChecked: boolean = false; // 是否已读取过同意状态(避免首帧闪烁) @@ -339,6 +342,8 @@ struct Index { } this.accounts = await AccountStore.loadAll(context); this.sources = await CalendarDataService.loadSources(context); + // 显示方式(时间轴 / 列表)可能在设置页被改过,每次回到前台都重新读一次 + this.displayStyle = await AppSettings.getDisplayStyle(context); await this.reloadEvents(); this.startAutoSync(); } @@ -2009,120 +2014,320 @@ struct Index { ) } - /** 单日竖向时间轴滚动区(周视图 / 月视图下方共用);DayTimelineView 自身带 Scroll 并自动定位到当前时刻 */ + /** 单日日程展示区(周视图 / 月视图下方共用)。 + * - timeline 模式:竖向时间轴(DayTimelineView,带"当前时间红线")。 + * - list 模式:按"行"平铺(eventRow),无时间刻度 → 不画红线,进行中改用文字标记。 */ @Builder eventList() { - Column() { - DayTimelineView({ - timeline: this.dayTimeline, - nowMs: this.nowMs, - showNowLine: this.selectedDate === this.startOfDay(this.nowMs), - scrollable: true, - onPick: (e: DisplayEvent): void => this.openEvent(e) - }) - if (!this.dayTimeline.hasTimed && this.dayTimeline.allDay.length === 0 && !this.loading) { - Text('当天没有日程') - .fontSize(13) - .fontColor($r('app.color.text_hint')) - .width('100%') - .textAlign(TextAlign.Center) - .padding(20) - } - } - .layoutWeight(1) - .width('100%') - .padding({ left: 12, right: 16, top: 4, bottom: 24 }) - .alignItems(HorizontalAlign.Start) - // 下半区(日程展示区)左右滑 → 按天切换(一次 1 天),仅周视图生效 - .gesture( - SwipeGesture({ direction: SwipeDirection.Horizontal }) - .onAction((event: GestureEvent) => { - if (this.mode !== 'week') { - return; + if (this.displayStyle === 'list') { + Scroll() { + Column({ space: 8 }) { + ForEach(this.eventsOfDate(this.selectedDate), (e: DisplayEvent) => { + this.eventRow(e, this.isEventNow(e, this.selectedDate === this.startOfDay(this.nowMs))) + }, (e: DisplayEvent) => `${e.isSystem ? 's' : 'l'}${e.id}_${e.startTime}_${e.title}`) + if (this.eventsOfDate(this.selectedDate).length === 0 && !this.loading) { + Text('当天没有日程') + .fontSize(13) + .fontColor($r('app.color.text_hint')) + .width('100%') + .textAlign(TextAlign.Center) + .padding(20) } - this.switchDay(this.isSwipeLeft(event) ? 1 : -1); + } + .width('100%') + .padding({ left: 20, right: 20, top: 4, bottom: 24 }) + .constraintSize({ minHeight: '100%' }) + } + .layoutWeight(1) + .scrollBar(BarState.Off) + .edgeEffect(EdgeEffect.Spring) + .align(Alignment.Top) + .gesture( + SwipeGesture({ direction: SwipeDirection.Horizontal }) + .onAction((event: GestureEvent) => { + if (this.mode !== 'week') { + return; + } + this.switchDay(this.isSwipeLeft(event) ? 1 : -1); + }) + ) + } else { + Column() { + DayTimelineView({ + timeline: this.dayTimeline, + nowMs: this.nowMs, + showNowLine: this.selectedDate === this.startOfDay(this.nowMs), + scrollable: true, + onPick: (e: DisplayEvent): void => this.openEvent(e) }) - ) + if (!this.dayTimeline.hasTimed && this.dayTimeline.allDay.length === 0 && !this.loading) { + Text('当天没有日程') + .fontSize(13) + .fontColor($r('app.color.text_hint')) + .width('100%') + .textAlign(TextAlign.Center) + .padding(20) + } + } + .layoutWeight(1) + .width('100%') + .padding({ left: 12, right: 16, top: 4, bottom: 24 }) + .alignItems(HorizontalAlign.Start) + // 下半区(日程展示区)左右滑 → 按天切换(一次 1 天),仅周视图生效 + .gesture( + SwipeGesture({ direction: SwipeDirection.Horizontal }) + .onAction((event: GestureEvent) => { + if (this.mode !== 'week') { + return; + } + this.switchDay(this.isSwipeLeft(event) ? 1 : -1); + }) + ) + } } @Builder agendaBody() { - List({ space: 10, scroller: this.agendaScroller }) { - ListItem() { - Row({ space: 8 }) { - if (this.loading) { - LoadingProgress() - .width(24) - .height(24) - .color($r('app.color.brand')) - } - Text(this.loading ? '正在加载全部日程…' : `共 ${this.agendaEvents.length} 条` + - `,${this.agendaTimelines.length} 天`) - .fontSize(12) - .fontColor($r('app.color.text_hint')) - } - .width('100%') - .padding({ top: 2, bottom: 2 }) - } - - // 每一天:日期头 + 该日一条竖向时间轴(0-23 格,色块按时间平铺,冲突平分宽度) - ForEach(this.agendaTimelines, (day: AgendaTimelineGroup) => { + if (this.displayStyle === 'list') { + // 列表模式:按日分组,每天一条日期头 + 该日若干"行"卡片;不画红线,进行中改用文字标记 + List({ space: 6, scroller: this.agendaScroller }) { ListItem() { - Column({ space: 6 }) { - Row({ space: 8 }) { - Text(day.label) - .fontSize(14) - .fontWeight(FontWeight.Bold) - .fontColor(day.isToday ? $r('app.color.brand') : $r('app.color.text_primary')) - if (day.isToday) { - Text('今天') - .fontSize(10) - .fontColor($r('app.color.button_text')) - .backgroundColor($r('app.color.brand')) - .borderRadius(6) - .padding({ left: 6, right: 6, top: 1, bottom: 1 }) - } - Blank() - Text(`${day.timeline.blocks.length + day.timeline.allDay.length} 条`) - .fontSize(11) - .fontColor($r('app.color.text_hint')) + Row({ space: 8 }) { + if (this.loading) { + LoadingProgress() + .width(24) + .height(24) + .color($r('app.color.brand')) } - .width('100%') - .padding({ top: 4 }) - - DayTimelineView({ - timeline: day.timeline, - nowMs: this.nowMs, - showNowLine: day.isToday, - scrollable: false, - onPick: (e: DisplayEvent): void => this.openEvent(e) - }) + Text(this.loading ? '正在加载全部日程…' : `共 ${this.agendaEvents.length} 条`) + .fontSize(12) + .fontColor($r('app.color.text_hint')) } .width('100%') - .padding(10) - .borderRadius(12) - .backgroundColor($r('app.color.card_bg')) - .border({ width: 1, color: $r('app.color.shadow_color') }) + .padding({ top: 2, bottom: 2 }) } - }, (day: AgendaTimelineGroup) => `day_${day.dateMs}`) + + ForEach(this.agendaTimelines, (day: AgendaTimelineGroup) => { + ListItem() { + Text(day.label) + .fontSize(13) + .fontWeight(FontWeight.Bold) + .fontColor(day.isToday ? $r('app.color.brand') : $r('app.color.text_secondary')) + .width('100%') + .padding({ top: 6, bottom: 2 }) + } + ForEach(this.dayEvents(day), (e: DisplayEvent) => { + ListItem() { + this.eventRow(e, this.isEventNow(e, day.isToday)) + } + .width('100%') + }, (e: DisplayEvent) => `${e.isSystem ? 's' : 'l'}${e.id}_${e.startTime}_${e.title}`) + }, (day: AgendaTimelineGroup) => `day_${day.dateMs}`) + } + .width('100%') + .layoutWeight(1) + .scrollBar(BarState.Auto) + .edgeEffect(EdgeEffect.Spring) + .padding({ left: 20, right: 20, top: 6, bottom: 24 }) + .cachedCount(8) + .onAreaChange((oldVal: Area, newVal: Area): void => { + const h: number = Number(newVal.height); + if (h > 0 && (h - this.agendaViewportH > 1 || h - this.agendaViewportH < -1)) { + this.agendaViewportH = h; + if (this.agendaScrollPending) { + this.doAgendaScroll(); + } + } + }) + } else { + // 时间轴模式:每天一条竖向时间轴(0-23 格,色块按时间平铺,冲突平分宽度;今天画红线) + List({ space: 10, scroller: this.agendaScroller }) { + ListItem() { + Row({ space: 8 }) { + if (this.loading) { + LoadingProgress() + .width(24) + .height(24) + .color($r('app.color.brand')) + } + Text(this.loading ? '正在加载全部日程…' : `共 ${this.agendaEvents.length} 条` + + `,${this.agendaTimelines.length} 天`) + .fontSize(12) + .fontColor($r('app.color.text_hint')) + } + .width('100%') + .padding({ top: 2, bottom: 2 }) + } + + // 每一天:日期头 + 该日一条竖向时间轴(0-23 格,色块按时间平铺,冲突平分宽度) + ForEach(this.agendaTimelines, (day: AgendaTimelineGroup) => { + ListItem() { + Column({ space: 6 }) { + Row({ space: 8 }) { + Text(day.label) + .fontSize(14) + .fontWeight(FontWeight.Bold) + .fontColor(day.isToday ? $r('app.color.brand') : $r('app.color.text_primary')) + if (day.isToday) { + Text('今天') + .fontSize(10) + .fontColor($r('app.color.button_text')) + .backgroundColor($r('app.color.brand')) + .borderRadius(6) + .padding({ left: 6, right: 6, top: 1, bottom: 1 }) + } + Blank() + Text(`${day.timeline.blocks.length + day.timeline.allDay.length} 条`) + .fontSize(11) + .fontColor($r('app.color.text_hint')) + } + .width('100%') + .padding({ top: 4 }) + + DayTimelineView({ + timeline: day.timeline, + nowMs: this.nowMs, + showNowLine: day.isToday, + scrollable: false, + onPick: (e: DisplayEvent): void => this.openEvent(e) + }) + } + .width('100%') + .padding(10) + .borderRadius(12) + .backgroundColor($r('app.color.card_bg')) + .border({ width: 1, color: $r('app.color.shadow_color') }) + } + }, (day: AgendaTimelineGroup) => `day_${day.dateMs}`) + } + .width('100%') + .layoutWeight(1) + .scrollBar(BarState.Auto) + .edgeEffect(EdgeEffect.Spring) + .padding({ left: 12, right: 12, top: 6, bottom: 24 }) + .cachedCount(4) + .onAreaChange((oldVal: Area, newVal: Area): void => { + const h: number = Number(newVal.height); + if (h > 0 && (h - this.agendaViewportH > 1 || h - this.agendaViewportH < -1)) { + this.agendaViewportH = h; + if (this.agendaScrollPending) { + this.doAgendaScroll(); + } + } + }) } - .width('100%') - .layoutWeight(1) - .scrollBar(BarState.Auto) - .edgeEffect(EdgeEffect.Spring) - .padding({ left: 12, right: 12, top: 6, bottom: 24 }) - .cachedCount(4) - .onAreaChange((oldVal: Area, newVal: Area): void => { - const h: number = Number(newVal.height); - if (h > 0 && (h - this.agendaViewportH > 1 || h - this.agendaViewportH < -1)) { - this.agendaViewportH = h; - if (this.agendaScrollPending) { - this.doAgendaScroll(); + } + + /** 日程"行"卡片(列表模式 / 卡片共用):左侧 4px 颜色条 + 标题 + 时间/地点 + 所属日历本。 + * isNow=true 时以红色强调并打"● 进行中"标记(替代时间轴里的"当前时间红线")。 */ + @Builder + eventRow(e: DisplayEvent, isNow: boolean = false) { + Row({ space: 10 }) { + Column() + .width(4) + .height(38) + .borderRadius(2) + .backgroundColor(isNow ? '#FF3B30' : e.color) + Column({ space: 3 }) { + Text(e.title === '' ? '(无标题)' : e.title) + .fontSize(15) + .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(isNow ? '#FF3B30' : $r('app.color.text_secondary')) + if (e.recurring) { + Text('↻ 重复') + .fontSize(10) + .fontColor($r('app.color.text_hint')) + .padding({ left: 4, right: 4, top: 1, bottom: 1 }) + .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 }) { + Text('📍') + .fontSize(11) + .fontColor($r('app.color.text_hint')) + Text(e.location) + .fontSize(12) + .fontColor($r('app.color.text_hint')) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + } + .width('100%') } } + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) + if (e.isSystem) { + Text('系统') + .fontSize(10) + .fontColor($r('app.color.text_hint')) + .padding({ left: 6, right: 6, top: 2, bottom: 2 }) + .borderRadius(6) + .backgroundColor($r('app.color.chip_off_bg')) + } + // 所属日历本:最右侧、垂直居中,颜色同日历本;只读日历本加删除线标识 + if (e.calName !== '') { + Text(e.calName) + .fontSize(11) + .fontColor(e.color) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .constraintSize({ maxWidth: '30%' }) + .decoration({ type: e.writable ? TextDecorationType.None : TextDecorationType.LineThrough }) + } + } + .alignItems(VerticalAlign.Center) + .width('100%') + .padding(10) + .borderRadius(12) + .backgroundColor($r('app.color.card_bg')) + .border({ width: 1, color: $r('app.color.shadow_color') }) + .onClick(() => { + this.openEvent(e); }) } + /** 某条日程此刻是否"正在进行"。仅当天、且为按时段(非全天/非跨天)且 now 落在 [start,end) 才算。 + * 列表模式用这个替代时间轴的"当前时间红线":红条 + "● 进行中"文字标记。 */ + private isEventNow(e: DisplayEvent, isToday: boolean): boolean { + if (!isToday) { + return false; + } + if (e.isAllDay || this.spansDays(e)) { + return false; + } + return e.startTime <= this.nowMs && this.nowMs < e.endTime; + } + + /** 取某天时间轴里要按"行"平铺展示的全部日程(全天 + 时段色块对应事件)。 */ + private dayEvents(day: AgendaTimelineGroup): DisplayEvent[] { + const arr: DisplayEvent[] = []; + for (const a of day.timeline.allDay) { + arr.push(a); + } + for (const b of day.timeline.blocks) { + if (b.ev !== null) { + arr.push(b.ev); + } + } + return arr; + } + /** 列表视图:把每天的分组转成"一天一条竖向时间轴" * 多日/跨天日程会**展开到它覆盖的每一天**(起始日记为跨天,中间日整天), * 确保列表里每天的数据完整,而不是只挂在第一天。 */ diff --git a/entry/src/main/ets/pages/SettingsPage.ets b/entry/src/main/ets/pages/SettingsPage.ets index c35e892..0bb93c6 100644 --- a/entry/src/main/ets/pages/SettingsPage.ets +++ b/entry/src/main/ets/pages/SettingsPage.ets @@ -4,6 +4,7 @@ import { router } from '@kit.ArkUI'; import { common } from '@kit.AbilityKit'; import { notificationManager } from '@kit.NotificationKit'; import { AppSettings } from '../common/AppSettings'; +import { CardDataService } from '../common/CardDataService'; import { BackgroundSyncService } from '../common/BackgroundSyncService'; import { AccountStore, DavAccount } from '../common/AccountStore'; import { ReminderService } from '../common/ReminderService'; @@ -39,6 +40,7 @@ struct SettingsPage { @State docUrl: string = ''; @State showTips: boolean = false; // 功能引导卡片(底部弹出) @State defaultView: string = 'month'; // 打开 App 默认视图:month | week | agenda + @State displayStyle: string = 'timeline'; // 日程显示方式:timeline(时间轴)| list(列表) private context?: common.Context; aboutToAppear(): void { @@ -68,6 +70,9 @@ struct SettingsPage { AppSettings.getDefaultView(ctx).then((v: string): void => { this.defaultView = v; }); + AppSettings.getDisplayStyle(ctx).then((v: string): void => { + this.displayStyle = v; + }); this.refreshNotifyState(); this.loadBackupSettings(); this.loadAllBooks(); @@ -274,6 +279,25 @@ struct SettingsPage { : (this.defaultView === 'week' ? '周视图' : '列表视图'); } + private async saveDisplayStyle(style: string): Promise { + if (this.context === undefined) { + return; + } + this.displayStyle = style; + await AppSettings.setDisplayStyle(this.context, style); + // 同步刷桌面卡片:让 4×4 / 6×4 卡片立刻按新方式重绘(列表模式去掉红线) + try { + await CardDataService.pushToAllForms(this.context); + } catch (err) { + // 卡片刷新失败不影响设置保存,下次同步或卡片自刷新时会生效 + } + this.getUIContext().getPromptAction().showToast({ + message: style === 'list' + ? '已切换为列表显示(不显示当前时间红线)' + : '已切换为时间轴显示' + }); + } + private intervalLabel(minutes: number): string { return minutes >= 60 ? `${minutes / 60} 小时` : `${minutes} 分钟`; } @@ -519,6 +543,43 @@ struct SettingsPage { .backgroundColor($r('app.color.card_bg')) .border({ width: 1, color: $r('app.color.shadow_color') }) + // 日程显示方式:时间轴 / 列表(同时作用于主界面与桌面卡片) + Column({ space: 8 }) { + Row({ space: 10 }) { + Column({ space: 2 }) { + Text('日程显示方式') + .fontSize(15) + .fontWeight(FontWeight.Medium) + .fontColor($r('app.color.text_primary')) + Text('时间轴:按时间刻度看一天的占用与冲突;列表:快速扫读、适合长标题。列表模式不显示当前时间红线') + .fontSize(12) + .fontColor($r('app.color.text_secondary')) + } + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) + Select([{ value: '时间轴' }, { value: '列表' }] as SelectOption[]) + .selected(this.displayStyle === 'list' ? 1 : 0) + .value(this.displayStyle === 'list' ? '列表' : '时间轴') + .fontColor($r('app.color.text_primary')) + .font({ size: 14 }) + .optionFont({ size: 14 }) + .selectedOptionFont({ size: 14 }) + .onSelect((index: number) => { + const v: string = index === 1 ? 'list' : 'timeline'; + if (v !== this.displayStyle) { + this.saveDisplayStyle(v); + } + }) + } + .width('100%') + } + .alignItems(HorizontalAlign.Start) + .width('100%') + .padding(14) + .borderRadius(12) + .backgroundColor($r('app.color.card_bg')) + .border({ width: 1, color: $r('app.color.shadow_color') }) + // 数据修复:手动触发一次性全量重拉(重建提醒等本地残缺字段) Column({ space: 8 }) { Row({ space: 10 }) { diff --git a/entry/src/main/ets/pages/widget/Widget4x4.ets b/entry/src/main/ets/pages/widget/Widget4x4.ets index 7e1fb52..3e13671 100644 --- a/entry/src/main/ets/pages/widget/Widget4x4.ets +++ b/entry/src/main/ets/pages/widget/Widget4x4.ets @@ -33,6 +33,16 @@ class TAllDay { isAllDay: boolean = true; } +/** 列表模式单条日程(解析 listJson) */ +class LItem4 { + title: string = ''; + time: string = ''; + endTime: string = ''; + color: string = '#007DFF'; + calName: string = ''; + isNow: boolean = false; +} + /** 冲突组:组内**贪心分列**,lanes[i] = 第 i 列(同列互不重叠) */ class TGroup4 { startRatio: number = 0; @@ -79,6 +89,8 @@ struct Widget4x4Card { @LocalStorageProp('todayCount') todayCount: number = 0; @LocalStorageProp('isToday') isToday: boolean = true; @LocalStorageProp('dayCount') dayCount: number = 0; + @LocalStorageProp('listJson') listJson: string = '[]'; + @LocalStorageProp('displayStyle') displayStyle: string = 'timeline'; private parseBlocks(): TBlock[] { try { @@ -459,6 +471,112 @@ struct Widget4x4Card { } } + /** 列表模式单条日程(解析 listJson) */ + private parseList(): LItem4[] { + try { + return JSON.parse(this.listJson) as LItem4[]; + } catch (err) { + return []; + } + } + + /** 列表模式单条行:左侧日历色条(进行中变红)+ 时间 + 标题 + 所属日历本;不画红线 */ + @Builder + listRow(item: LItem4) { + Row({ space: 8 }) { + Column() + .width(3) + .height(item.time === '全天' ? 16 : 38) + .borderRadius(2) + .backgroundColor(item.isNow ? '#FF3B30' : item.color) + if (item.time === '全天') { + Text(item.title) + .fontSize(12) + .fontColor('#1A1A1A') + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .layoutWeight(1) + Text('全天') + .fontSize(9) + .fontColor('#FFFFFF') + .backgroundColor(item.color) + .borderRadius(6) + .padding({ left: 5, right: 5, top: 1, bottom: 1 }) + } else { + Column({ space: 2 }) { + Text(item.time) + .fontSize(10) + .fontWeight(FontWeight.Medium) + .fontColor(item.isNow ? '#FF3B30' : '#333333') + Text(item.endTime) + .fontSize(10) + .fontColor('#999999') + } + .width(38) + .alignItems(HorizontalAlign.Center) + Text(item.title) + .fontSize(12) + .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) + .fontColor(item.color) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .constraintSize({ maxWidth: '25%' }) + } + } + .alignItems(VerticalAlign.Center) + .width('100%') + .padding({ left: 8, right: 8, top: 5, bottom: 5 }) + .borderRadius(8) + .backgroundColor(item.isNow ? '#FFF1F0' : '#F5F7FA') + } + + /** 列表模式主体:空态 / 可滚动的"行"列表(无时间轴、无红线) */ + @Builder + listBody() { + if (this.parseList().length === 0) { + Column({ space: 6 }) { + Text('📅') + .fontSize(24) + Text('暂无日程') + .fontSize(13) + .fontColor('#8A8A8A') + } + .width('100%') + .layoutWeight(1) + .justifyContent(FlexAlign.Center) + .onClick(() => this.openApp()) + } else { + List({ space: 4 }) { + ForEach(this.parseList(), (item: LItem4) => { + ListItem() { + this.listRow(item) + } + .width('100%') + }, (item: LItem4) => `${item.time}_${item.title}_${item.color}`) + } + .width('100%') + .layoutWeight(1) + .scrollBar(BarState.Auto) + .cachedCount(8) + .onClick(() => this.openApp()) + } + } + build() { Column({ space: 6 }) { Row({ space: 4 }) { @@ -484,6 +602,9 @@ struct Widget4x4Card { Divider().strokeWidth(0.5).color('#E5E5E5') + if (this.displayStyle === 'list') { + this.listBody() + } else { // 卡片不支持 Scroll,但支持 List / ListItem: // 把"整日时间轴"作为**一个很高的 ListItem**,超出卡片窗口的部分靠上下滑动查看 List() { @@ -567,6 +688,7 @@ struct Widget4x4Card { } .layoutWeight(1) .width('100%') + } } .width('100%') .height('100%') diff --git a/entry/src/main/ets/pages/widget/Widget6x4.ets b/entry/src/main/ets/pages/widget/Widget6x4.ets index b06144a..e660b1f 100644 --- a/entry/src/main/ets/pages/widget/Widget6x4.ets +++ b/entry/src/main/ets/pages/widget/Widget6x4.ets @@ -33,6 +33,16 @@ class TAllDay6 { isAllDay: boolean = true; } +/** 列表模式单条日程(解析 listJson) */ +class LItem6 { + title: string = ''; + time: string = ''; + endTime: string = ''; + color: string = '#007DFF'; + calName: string = ''; + isNow: boolean = false; +} + /** 冲突组:组内**贪心分列**,lanes[i] = 第 i 列(同列互不重叠) */ class TGroup6 { startRatio: number = 0; @@ -78,6 +88,8 @@ struct Widget6x4Card { @LocalStorageProp('todayCount') todayCount: number = 0; @LocalStorageProp('isToday') isToday: boolean = true; @LocalStorageProp('dayCount') dayCount: number = 0; + @LocalStorageProp('listJson') listJson: string = '[]'; + @LocalStorageProp('displayStyle') displayStyle: string = 'timeline'; private parseBlocks(): TBlock6[] { try { @@ -454,6 +466,112 @@ struct Widget6x4Card { } } + /** 列表模式单条日程(解析 listJson) */ + private parseList(): LItem6[] { + try { + return JSON.parse(this.listJson) as LItem6[]; + } catch (err) { + return []; + } + } + + /** 列表模式单条行:左侧日历色条(进行中变红)+ 时间 + 标题 + 所属日历本;不画红线 */ + @Builder + listRow(item: LItem6) { + Row({ space: 8 }) { + Column() + .width(4) + .height(item.time === '全天' ? 18 : 42) + .borderRadius(2) + .backgroundColor(item.isNow ? '#FF3B30' : item.color) + if (item.time === '全天') { + Text(item.title) + .fontSize(13) + .fontColor('#1A1A1A') + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .layoutWeight(1) + Text('全天') + .fontSize(9) + .fontColor('#FFFFFF') + .backgroundColor(item.color) + .borderRadius(6) + .padding({ left: 5, right: 5, top: 1, bottom: 1 }) + } else { + Column({ space: 2 }) { + Text(item.time) + .fontSize(11) + .fontWeight(FontWeight.Medium) + .fontColor(item.isNow ? '#FF3B30' : '#333333') + Text(item.endTime) + .fontSize(11) + .fontColor('#999999') + } + .width(42) + .alignItems(HorizontalAlign.Center) + Text(item.title) + .fontSize(13) + .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) + .fontColor(item.color) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .constraintSize({ maxWidth: '25%' }) + } + } + .alignItems(VerticalAlign.Center) + .width('100%') + .padding({ left: 8, right: 8, top: 6, bottom: 6 }) + .borderRadius(8) + .backgroundColor(item.isNow ? '#FFF1F0' : '#F5F7FA') + } + + /** 列表模式主体:空态 / 可滚动的"行"列表(无时间轴、无红线) */ + @Builder + listBody() { + if (this.parseList().length === 0) { + Column({ space: 6 }) { + Text('📅') + .fontSize(24) + Text('暂无日程') + .fontSize(13) + .fontColor('#8A8A8A') + } + .width('100%') + .layoutWeight(1) + .justifyContent(FlexAlign.Center) + .onClick(() => this.openApp()) + } else { + List({ space: 4 }) { + ForEach(this.parseList(), (item: LItem6) => { + ListItem() { + this.listRow(item) + } + .width('100%') + }, (item: LItem6) => `${item.time}_${item.title}_${item.color}`) + } + .width('100%') + .layoutWeight(1) + .scrollBar(BarState.Auto) + .cachedCount(8) + .onClick(() => this.openApp()) + } + } + build() { Column({ space: 6 }) { Row({ space: 6 }) { @@ -483,6 +601,9 @@ struct Widget6x4Card { Divider().strokeWidth(0.5).color('#E5E5E5') + if (this.displayStyle === 'list') { + this.listBody() + } else { // 卡片不支持 Scroll,但支持 List / ListItem: // 把"整日时间轴"作为**一个很高的 ListItem**,超出卡片窗口的部分靠上下滑动查看 List() { @@ -566,6 +687,7 @@ struct Widget6x4Card { } .layoutWeight(1) .width('100%') + } } .width('100%') .height('100%')