2026-09-13 15:50:37 +08:00
|
|
|
|
// entry/src/main/ets/common/CalendarDataService.ets
|
|
|
|
|
|
// 合并数据源:本地库(DAV 同步 + 本机事件) + 系统日历(只读展示)
|
2026-09-16 20:54:41 +08:00
|
|
|
|
import { common, abilityAccessCtrl, bundleManager, Permissions } from '@kit.AbilityKit';
|
2026-09-13 15:50:37 +08:00
|
|
|
|
import { BusinessError } from '@kit.BasicServicesKit';
|
|
|
|
|
|
import { calendarManager } from '@kit.CalendarKit';
|
|
|
|
|
|
import { AccountStore, CalSource, BookPalette, LOCAL_CAL_KEY, DavAccount } from './AccountStore';
|
|
|
|
|
|
import { EventDb, LocalEvent } from './EventDb';
|
|
|
|
|
|
import { LogUtil } from './LogUtil';
|
|
|
|
|
|
import { AppSettings } from './AppSettings';
|
|
|
|
|
|
import { RruleUtil } from './RruleUtil';
|
|
|
|
|
|
import { IcsUtil } from './IcsUtil';
|
|
|
|
|
|
|
|
|
|
|
|
/** 界面展示用事件 */
|
|
|
|
|
|
export class DisplayEvent {
|
|
|
|
|
|
id: number = 0; // 本地库 id 或系统日历事件 id
|
|
|
|
|
|
isSystem: boolean = false;
|
|
|
|
|
|
title: string = '';
|
|
|
|
|
|
description: string = '';
|
|
|
|
|
|
location: string = '';
|
|
|
|
|
|
startTime: number = 0;
|
|
|
|
|
|
endTime: number = 0;
|
|
|
|
|
|
isAllDay: boolean = false;
|
|
|
|
|
|
calKey: string = '';
|
|
|
|
|
|
calName: string = '';
|
|
|
|
|
|
color: string = '#007DFF';
|
|
|
|
|
|
completed: boolean = false; // 仅待办使用
|
|
|
|
|
|
recurring: boolean = false; // 是否为重复日程展开出的发生
|
2026-11-02 16:53:15 +08:00
|
|
|
|
/** ⭐ 待办的 RRULE 原文(空 = 非循环)。与 recurring 不同:
|
|
|
|
|
|
* recurring 只说"重不重复",这里带的是"多久重复一次"。 */
|
|
|
|
|
|
recurRule: string = '';
|
|
|
|
|
|
/** 待办的 DTSTART(重复基准,毫秒;0 = 无) */
|
|
|
|
|
|
dtstart: number = 0;
|
2026-09-13 20:25:18 +08:00
|
|
|
|
writable: boolean = true; // 所属日历本是否可写(只读时点击显示详情而非编辑)
|
2026-09-13 15:50:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export class CalendarDataService {
|
2026-09-16 20:54:41 +08:00
|
|
|
|
/** 本功能依赖的日历权限:读取本应用日历 + 读取全部日历(即"读取全部日程") */
|
|
|
|
|
|
private static readonly CALENDAR_PERMISSIONS: Permissions[] = [
|
|
|
|
|
|
'ohos.permission.READ_CALENDAR',
|
|
|
|
|
|
'ohos.permission.READ_WHOLE_CALENDAR'
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 是否已获得"读取全部日程"权限。
|
|
|
|
|
|
* **只检查、绝不弹窗** —— 供界面判断状态、决定是否需要引导用户主动授权。
|
|
|
|
|
|
* 注意:checkAccessToken 只反映本应用授权状态,不会触发任何系统弹窗。
|
|
|
|
|
|
*/
|
|
|
|
|
|
static async hasSystemCalendarPermission(): Promise<boolean> {
|
2026-09-13 15:50:37 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const atManager = abilityAccessCtrl.createAtManager();
|
2026-09-16 20:54:41 +08:00
|
|
|
|
const info = bundleManager.getBundleInfoForSelfSync(
|
|
|
|
|
|
bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
|
|
|
|
|
|
const tokenId: number = info.appInfo.accessTokenId;
|
|
|
|
|
|
for (const p of CalendarDataService.CALENDAR_PERMISSIONS) {
|
|
|
|
|
|
const status = await atManager.checkAccessToken(tokenId, p);
|
|
|
|
|
|
if (status !== abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 申请"读取全部日程"权限(READ_CALENDAR + READ_WHOLE_CALENDAR)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 合规约束(《审核指南》7.17):**只能在用户主动点击使用对应功能时调用**,
|
|
|
|
|
|
* 例如在设置页打开"混合显示系统日历"开关、把系统日历模式切到"备份到 CalDAV 日历本"。
|
|
|
|
|
|
* 严禁在 App 启动 / 首次打开 / 未同意隐私政策等用户未主动操作的时机调用。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 已授权时直接返回 true,不会再弹窗(先 checkAccessToken,避免多余弹窗)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
static async requestSystemCalendarPermission(context: common.UIAbilityContext): Promise<boolean> {
|
|
|
|
|
|
if (await CalendarDataService.hasSystemCalendarPermission()) {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
const atManager = abilityAccessCtrl.createAtManager();
|
|
|
|
|
|
const result = await atManager.requestPermissionsFromUser(
|
|
|
|
|
|
context, CalendarDataService.CALENDAR_PERMISSIONS);
|
2026-09-13 15:50:37 +08:00
|
|
|
|
for (const r of result.authResults) {
|
|
|
|
|
|
if (r !== 0) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
const e = err as BusinessError;
|
|
|
|
|
|
console.error(`日历权限申请失败: ${e.code} - ${e.message}`);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 收集全部日历来源:DAV 日历本 + 本机 + 系统日历账户 */
|
|
|
|
|
|
static async loadSources(context: common.Context): Promise<CalSource[]> {
|
|
|
|
|
|
const sources: CalSource[] = [];
|
2026-09-13 20:25:18 +08:00
|
|
|
|
const manualKeys: string[] = await AppSettings.getManualReadonlyKeys(context);
|
2026-09-13 15:50:37 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const accounts: DavAccount[] = await AccountStore.loadAll(context);
|
|
|
|
|
|
for (const acc of accounts) {
|
|
|
|
|
|
for (let i = 0; i < acc.calendarHrefs.length; i++) {
|
|
|
|
|
|
const s = new CalSource();
|
|
|
|
|
|
s.calKey = `${acc.id}_${i}`;
|
|
|
|
|
|
let name: string = i < acc.calendarNames.length ? acc.calendarNames[i] : '';
|
|
|
|
|
|
if (name === '') {
|
|
|
|
|
|
name = acc.calendarHrefs.length === 1 ? acc.name : `日历本 ${i + 1}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
s.name = `${acc.name}:${name}`;
|
2026-09-15 23:03:58 +08:00
|
|
|
|
// 优先使用服务器端定义的颜色;未定义时按**集合 href** 派生(不随顺序变化,见 colorForHref 说明)
|
2026-09-13 15:50:37 +08:00
|
|
|
|
let color: string = i < acc.calendarColors.length ? AccountStore.normalizeColor(acc.calendarColors[i]) : '';
|
2026-09-15 23:03:58 +08:00
|
|
|
|
s.color = color !== '' ? color : BookPalette.colorForHref(acc.calendarHrefs[i]);
|
2026-09-13 15:50:37 +08:00
|
|
|
|
s.source = 'dav';
|
|
|
|
|
|
s.visible = true;
|
2026-09-13 20:25:18 +08:00
|
|
|
|
// 写权限:服务器探测结果 + 手动标记(部分服务器不在 CalDAV 层拒绝写入)
|
|
|
|
|
|
const detected: boolean = acc.calendarWritable.length > i
|
|
|
|
|
|
? acc.calendarWritable[i] !== '0' : true;
|
|
|
|
|
|
s.writable = detected && !manualKeys.includes(s.calKey);
|
2026-11-03 23:04:00 +08:00
|
|
|
|
// 透传本支持的类型(新建待办/日程时据此过滤,避免把待办写进纯日历本)
|
|
|
|
|
|
s.comps = i < acc.calendarComps.length && acc.calendarComps[i] !== ''
|
|
|
|
|
|
? acc.calendarComps[i] : 'unknown';
|
2026-09-13 15:50:37 +08:00
|
|
|
|
sources.push(s);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 本机事件虚拟日历
|
|
|
|
|
|
const local = new CalSource();
|
|
|
|
|
|
local.calKey = LOCAL_CAL_KEY;
|
|
|
|
|
|
local.name = '本机';
|
|
|
|
|
|
local.color = '#5A6068';
|
|
|
|
|
|
local.source = 'local';
|
|
|
|
|
|
local.visible = true;
|
|
|
|
|
|
sources.push(local);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
const e = err as BusinessError;
|
|
|
|
|
|
console.error(`加载 DAV 来源失败: ${e.message}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 系统日历账户(受"混合显示系统日历"开关控制;按账户名去重)
|
|
|
|
|
|
const showSys: boolean = await AppSettings.getShowSystemCalendar(context);
|
|
|
|
|
|
if (!showSys) {
|
|
|
|
|
|
LogUtil.write('来源加载:系统日历混合显示已关闭,跳过全部系统日历');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const uiContext = context as common.UIAbilityContext;
|
|
|
|
|
|
const mgr: calendarManager.CalendarManager = calendarManager.getCalendarManager(uiContext);
|
|
|
|
|
|
const calendars: calendarManager.Calendar[] = await mgr.getAllCalendars();
|
|
|
|
|
|
let sysRaw: number = 0;
|
|
|
|
|
|
for (const cal of calendars) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const account: calendarManager.CalendarAccount = cal.getAccount();
|
|
|
|
|
|
sysRaw++;
|
|
|
|
|
|
const sysKey: string = `sys_${account.name}`;
|
|
|
|
|
|
if (sources.some((x: CalSource): boolean => x.calKey === sysKey)) {
|
|
|
|
|
|
continue; // 同名账户只保留一个来源
|
|
|
|
|
|
}
|
|
|
|
|
|
const s = new CalSource();
|
|
|
|
|
|
s.calKey = sysKey;
|
|
|
|
|
|
s.name = '系统';
|
|
|
|
|
|
let color = '#9AA0A6';
|
|
|
|
|
|
const config: calendarManager.CalendarConfig = cal.getConfig();
|
|
|
|
|
|
if (typeof config.color === 'string') {
|
|
|
|
|
|
color = config.color;
|
|
|
|
|
|
}
|
|
|
|
|
|
s.color = color;
|
|
|
|
|
|
s.source = 'system';
|
|
|
|
|
|
s.visible = true;
|
|
|
|
|
|
sources.push(s);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
// 单个日历账户读取失败不影响整体
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
LogUtil.write(`来源加载:系统日历 ${sysRaw} 个(按账户去重后保留,共 ${sources.length} 个来源)`);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
const e = err as BusinessError;
|
|
|
|
|
|
console.info(`系统日历不可用(可能未授权): ${e.code}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return sources;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 加载时间区间内的合并事件(按来源显隐过滤) */
|
|
|
|
|
|
static async loadEvents(context: common.Context, start: number, end: number,
|
|
|
|
|
|
sources: CalSource[]): Promise<DisplayEvent[]> {
|
|
|
|
|
|
LogUtil.init(context);
|
|
|
|
|
|
const result: DisplayEvent[] = [];
|
|
|
|
|
|
const visibleKeys: string[] = sources.filter((s: CalSource): boolean => s.visible)
|
|
|
|
|
|
.map((s: CalSource): string => s.calKey);
|
|
|
|
|
|
const colorOf = (key: string): string => {
|
|
|
|
|
|
const found = sources.find((s: CalSource): boolean => s.calKey === key);
|
|
|
|
|
|
return found !== undefined ? found.color : '#9AA0A6';
|
|
|
|
|
|
};
|
|
|
|
|
|
const nameOf = (key: string): string => {
|
|
|
|
|
|
const found = sources.find((s: CalSource): boolean => s.calKey === key);
|
|
|
|
|
|
return found !== undefined ? found.name : '';
|
|
|
|
|
|
};
|
2026-09-13 20:25:18 +08:00
|
|
|
|
const writableOf = (key: string): boolean => {
|
|
|
|
|
|
const found = sources.find((s: CalSource): boolean => s.calKey === key);
|
|
|
|
|
|
return found !== undefined ? found.writable : true;
|
|
|
|
|
|
};
|
2026-09-13 15:50:37 +08:00
|
|
|
|
|
|
|
|
|
|
// 1) 本地库(DAV + 本机);重复日程(RRULE)按规则展开为窗口内的多次发生
|
|
|
|
|
|
try {
|
|
|
|
|
|
const rows: LocalEvent[] = await EventDb.queryRange(context, start, end);
|
|
|
|
|
|
// 重复主事件的首次发生可能在窗口外,也纳入(仅用于展开)
|
|
|
|
|
|
const masters: LocalEvent[] = await EventDb.queryRecurringMasters(context, end);
|
|
|
|
|
|
for (const m of masters) {
|
|
|
|
|
|
if (!rows.some((r: LocalEvent): boolean => r.id === m.id)) {
|
|
|
|
|
|
rows.push(m);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-13 17:48:39 +08:00
|
|
|
|
// 单次覆盖实例(RECURRENCE-ID 独立行):展开时跳过对应发生,避免重复。
|
|
|
|
|
|
// 注意:①排除键必须带 calKey(同一系列可能出现在多个日历本);
|
2026-09-19 23:07:03 +08:00
|
|
|
|
// ②键必须用 **recurrenceId(原始发生时刻)**——覆盖行的 startTime 是"被改到的新时间",
|
|
|
|
|
|
// 用它做键时主事件在原时间的那次发生不会被排除 → 同一日程显示两条(幽灵日程);
|
|
|
|
|
|
// ③只有"可见日历本"的覆盖行才参与排除——历史孤儿行(旧账号迁移残留)
|
2026-09-13 17:48:39 +08:00
|
|
|
|
// 自己不显示,却会把主事件的对应发生排除掉,导致"第一次不显示"
|
2026-09-13 15:50:37 +08:00
|
|
|
|
const overrideKeys: string[] = [];
|
|
|
|
|
|
for (const e of rows) {
|
2026-09-13 17:48:39 +08:00
|
|
|
|
if (e.rrule === '' && e.recurring && visibleKeys.includes(e.calKey)) {
|
2026-09-19 23:07:03 +08:00
|
|
|
|
overrideKeys.push(EventDb.overrideKey(e.calKey, e.uid, e.recurrenceId, e.startTime));
|
2026-09-13 15:50:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
let occTotal: number = 0;
|
|
|
|
|
|
for (const e of rows) {
|
|
|
|
|
|
if (!visibleKeys.includes(e.calKey)) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
const baseColor: string = colorOf(e.calKey);
|
|
|
|
|
|
const baseName: string = nameOf(e.calKey);
|
|
|
|
|
|
const dur: number = Math.max(0, e.endTime - e.startTime);
|
|
|
|
|
|
let times: number[] = [e.startTime];
|
|
|
|
|
|
if (e.rrule !== '') {
|
|
|
|
|
|
const exNums: number[] = [];
|
|
|
|
|
|
if (e.exdate !== '') {
|
|
|
|
|
|
for (const raw of e.exdate.split(';')) {
|
|
|
|
|
|
const t = IcsUtil.parseTime(raw, !raw.includes('T'));
|
|
|
|
|
|
if (t !== null) {
|
|
|
|
|
|
exNums.push(t.time);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
const occs: number[] = RruleUtil.expand(e.rrule, e.startTime, start, end, exNums, 1500);
|
|
|
|
|
|
if (occs.length > 0) {
|
2026-09-13 17:48:39 +08:00
|
|
|
|
times = occs.filter((occ: number): boolean =>
|
2026-09-19 23:07:03 +08:00
|
|
|
|
!overrideKeys.includes(EventDb.overrideKey(e.calKey, e.uid, 0, occ)));
|
2026-09-13 15:50:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
for (const occ of times) {
|
|
|
|
|
|
const d = new DisplayEvent();
|
|
|
|
|
|
d.id = e.id;
|
|
|
|
|
|
d.isSystem = false;
|
|
|
|
|
|
d.title = e.title;
|
|
|
|
|
|
d.description = e.description;
|
|
|
|
|
|
d.location = e.location;
|
|
|
|
|
|
d.startTime = occ;
|
|
|
|
|
|
d.endTime = occ + dur;
|
|
|
|
|
|
d.isAllDay = e.isAllDay;
|
|
|
|
|
|
d.calKey = e.calKey;
|
|
|
|
|
|
d.calName = baseName;
|
|
|
|
|
|
d.color = baseColor;
|
|
|
|
|
|
d.recurring = e.rrule !== '';
|
2026-09-13 20:25:18 +08:00
|
|
|
|
d.writable = writableOf(e.calKey);
|
2026-09-13 15:50:37 +08:00
|
|
|
|
result.push(d);
|
|
|
|
|
|
occTotal++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
LogUtil.write(`显示加载:本地库 ${rows.length} 行,展开后 ${occTotal} 条(区间 ${new Date(start).toLocaleString()} ~ ${new Date(end).toLocaleString()})`);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
const e = err as BusinessError;
|
|
|
|
|
|
console.error(`读取本地事件失败: ${e.message}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2) 系统日历(只读混合展示)——单日历失败只跳过该日历,不影响其余
|
|
|
|
|
|
if (visibleKeys.some((k: string): boolean => k.startsWith('sys_'))) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const uiContext = context as common.UIAbilityContext;
|
|
|
|
|
|
const mgr: calendarManager.CalendarManager = calendarManager.getCalendarManager(uiContext);
|
|
|
|
|
|
const calendars: calendarManager.Calendar[] = await mgr.getAllCalendars();
|
|
|
|
|
|
for (const cal of calendars) {
|
|
|
|
|
|
let accountName: string = '';
|
|
|
|
|
|
let color = '#9AA0A6';
|
|
|
|
|
|
try {
|
|
|
|
|
|
const account: calendarManager.CalendarAccount = cal.getAccount();
|
|
|
|
|
|
accountName = account.name;
|
|
|
|
|
|
const config: calendarManager.CalendarConfig = cal.getConfig();
|
|
|
|
|
|
if (typeof config.color === 'string') {
|
|
|
|
|
|
color = config.color;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
// 忽略单个日历
|
|
|
|
|
|
}
|
|
|
|
|
|
const sysKey: string = `sys_${accountName}`;
|
|
|
|
|
|
if (!visibleKeys.includes(sysKey)) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
const events: calendarManager.Event[] = await cal.queryEventInstances(start, end);
|
|
|
|
|
|
let skipped: number = 0;
|
|
|
|
|
|
for (const ev of events) {
|
|
|
|
|
|
const d = new DisplayEvent();
|
|
|
|
|
|
d.id = ev.id !== undefined ? ev.id : 0;
|
|
|
|
|
|
d.isSystem = true;
|
|
|
|
|
|
d.title = ev.title !== undefined ? ev.title : '(无标题)';
|
|
|
|
|
|
d.description = ev.description !== undefined ? ev.description : '';
|
|
|
|
|
|
d.location = ev.location !== undefined && ev.location.location !== undefined ? ev.location.location : '';
|
|
|
|
|
|
d.startTime = ev.startTime;
|
|
|
|
|
|
d.endTime = ev.endTime;
|
|
|
|
|
|
d.isAllDay = ev.isAllDay === true;
|
|
|
|
|
|
d.calKey = sysKey;
|
|
|
|
|
|
d.calName = nameOf(sysKey);
|
|
|
|
|
|
d.color = color;
|
|
|
|
|
|
// 去重:与本地(DAV/本机)同标题+同时间的视为同一条,避免两个来源重复展示
|
|
|
|
|
|
const dup: boolean = result.some((x: DisplayEvent): boolean => !x.isSystem &&
|
|
|
|
|
|
x.title === d.title && x.startTime === d.startTime &&
|
|
|
|
|
|
x.endTime === d.endTime && x.isAllDay === d.isAllDay);
|
|
|
|
|
|
if (!dup) {
|
|
|
|
|
|
result.push(d);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
skipped++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
LogUtil.write(`系统日历 ${sysKey}:查询到 ${events.length} 条,与本地重复跳过 ${skipped} 条`);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
const e = err as BusinessError;
|
|
|
|
|
|
console.info(`查询系统日历 ${sysKey} 失败(跳过): ${e.code}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
const e = err as BusinessError;
|
|
|
|
|
|
console.info(`读取系统日历失败(可能未授权): ${e.code}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 排序:全天日程最前,然后按开始时间
|
|
|
|
|
|
result.sort((a: DisplayEvent, b: DisplayEvent): number => {
|
|
|
|
|
|
if (a.isAllDay !== b.isAllDay) {
|
|
|
|
|
|
return a.isAllDay ? -1 : 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
return a.startTime - b.startTime;
|
|
|
|
|
|
});
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 加载全部待办(VTODO,来自 DAV 同步,只读展示) */
|
|
|
|
|
|
static async loadTodos(context: common.Context, sources: CalSource[]): Promise<DisplayEvent[]> {
|
|
|
|
|
|
const result: DisplayEvent[] = [];
|
|
|
|
|
|
const visibleKeys: string[] = sources.filter((s: CalSource): boolean => s.visible)
|
|
|
|
|
|
.map((s: CalSource): string => s.calKey);
|
|
|
|
|
|
const colorOf = (key: string): string => {
|
|
|
|
|
|
const found = sources.find((s: CalSource): boolean => s.calKey === key);
|
|
|
|
|
|
return found !== undefined ? found.color : '#9AA0A6';
|
|
|
|
|
|
};
|
2026-11-02 15:47:03 +08:00
|
|
|
|
// ⭐ 待办按日历本分组显示需要"本名";原来只带 calKey,分组标题无法显示
|
|
|
|
|
|
const nameOf = (key: string): string => {
|
|
|
|
|
|
const found = sources.find((s: CalSource): boolean => s.calKey === key);
|
|
|
|
|
|
return found !== undefined ? found.name : '';
|
|
|
|
|
|
};
|
2026-09-13 15:50:37 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const rows: LocalEvent[] = await EventDb.queryTodos(context);
|
|
|
|
|
|
for (const e of rows) {
|
|
|
|
|
|
if (!visibleKeys.includes(e.calKey)) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
const d = new DisplayEvent();
|
|
|
|
|
|
d.id = e.id;
|
|
|
|
|
|
d.isSystem = false;
|
|
|
|
|
|
d.title = e.title;
|
|
|
|
|
|
d.description = e.description;
|
|
|
|
|
|
d.location = e.location;
|
|
|
|
|
|
d.startTime = e.startTime;
|
|
|
|
|
|
d.endTime = e.endTime;
|
|
|
|
|
|
d.isAllDay = e.isAllDay;
|
|
|
|
|
|
d.calKey = e.calKey;
|
2026-11-02 15:47:03 +08:00
|
|
|
|
d.calName = nameOf(e.calKey);
|
2026-09-13 15:50:37 +08:00
|
|
|
|
d.color = colorOf(e.calKey);
|
|
|
|
|
|
d.completed = e.completed;
|
2026-11-02 16:53:15 +08:00
|
|
|
|
d.recurRule = e.rrule;
|
|
|
|
|
|
d.dtstart = e.dtstart;
|
|
|
|
|
|
d.recurring = e.rrule !== '';
|
2026-09-13 15:50:37 +08:00
|
|
|
|
result.push(d);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
const e = err as BusinessError;
|
|
|
|
|
|
console.error(`读取待办失败: ${e.message}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|