新增了列表显示模式,用户可以在设置中进行选择。

Signed-off-by: Yang Yongquan <i@yangyq.net>
This commit is contained in:
2026-09-16 10:07:57 +08:00
parent 5ab9648b5a
commit 2173b06b43
6 changed files with 668 additions and 97 deletions
+300 -95
View File
@@ -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;
}
/** 列表视图:把每天的分组转成"一天一条竖向时间轴"
* 多日/跨天日程会**展开到它覆盖的每一天**(起始日记为跨天,中间日整天),
* 确保列表里每天的数据完整,而不是只挂在第一天。 */