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

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
+31
View File
@@ -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<string> {
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<void> {
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;成功完成后标记,恢复增量模式。
+32 -2
View File
@@ -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)}`);
}
+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;
}
/** 列表视图:把每天的分组转成"一天一条竖向时间轴"
* 多日/跨天日程会**展开到它覆盖的每一天**(起始日记为跨天,中间日整天),
* 确保列表里每天的数据完整,而不是只挂在第一天。 */
+61
View File
@@ -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<void> {
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 }) {
@@ -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%')
@@ -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%')