修改了通知相关的功能,能够正常通知了。
This commit is contained in:
@@ -14,6 +14,7 @@ export class AppSettings {
|
|||||||
private static readonly KEY_BACKUP_KEY: string = 'sys_backup_cal_key'; // 备份目标 DAV 日历本
|
private static readonly KEY_BACKUP_KEY: string = 'sys_backup_cal_key'; // 备份目标 DAV 日历本
|
||||||
private static readonly KEY_MANUAL_READONLY: string = 'manual_readonly_keys'; // 手动标记只读的 calKey
|
private static readonly KEY_MANUAL_READONLY: string = 'manual_readonly_keys'; // 手动标记只读的 calKey
|
||||||
private static readonly KEY_MUTED_REMINDER: string = 'muted_reminder_keys'; // 提醒静音的 calKey
|
private static readonly KEY_MUTED_REMINDER: string = 'muted_reminder_keys'; // 提醒静音的 calKey
|
||||||
|
private static readonly KEY_REMINDER_TICK: string = 'reminder_tick'; // 应用内提醒上次检查时间戳
|
||||||
private static readonly KEY_FULL_REFETCH: string = 'full_refetch_done'; // 一次性全量重拉已完成
|
private static readonly KEY_FULL_REFETCH: string = 'full_refetch_done'; // 一次性全量重拉已完成
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -114,6 +115,29 @@ export class AppSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 应用内提醒模式的上次检查时间戳(毫秒,0 = 未初始化) */
|
||||||
|
static async getReminderTick(context: common.Context): Promise<number> {
|
||||||
|
try {
|
||||||
|
const store: preferences.Preferences =
|
||||||
|
await preferences.getPreferences(context, AppSettings.STORE);
|
||||||
|
return await store.get(AppSettings.KEY_REMINDER_TICK, 0) as number;
|
||||||
|
} catch (err) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async setReminderTick(context: common.Context, tick: number): Promise<void> {
|
||||||
|
try {
|
||||||
|
const store: preferences.Preferences =
|
||||||
|
await preferences.getPreferences(context, AppSettings.STORE);
|
||||||
|
await store.put(AppSettings.KEY_REMINDER_TICK, tick);
|
||||||
|
await store.flush();
|
||||||
|
} catch (err) {
|
||||||
|
const e = err as BusinessError;
|
||||||
|
console.error(`保存提醒检查时间戳失败: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 系统日历模式:display = 仅混合显示;backup = 本地系统日程备份到选定 CalDAV 日历本 */
|
/** 系统日历模式:display = 仅混合显示;backup = 本地系统日程备份到选定 CalDAV 日历本 */
|
||||||
static async getSysCalMode(context: common.Context): Promise<string> {
|
static async getSysCalMode(context: common.Context): Promise<string> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ export class LocalEvent {
|
|||||||
completed: boolean = false; // 待办是否已完成(STATUS:COMPLETED)
|
completed: boolean = false; // 待办是否已完成(STATUS:COMPLETED)
|
||||||
rrule: string = ''; // 原始 RRULE(空 = 非重复);显示时按规则展开多次发生
|
rrule: string = ''; // 原始 RRULE(空 = 非重复);显示时按规则展开多次发生
|
||||||
exdate: string = ''; // 原始 EXDATE 排除日期,分号分隔
|
exdate: string = ''; // 原始 EXDATE 排除日期,分号分隔
|
||||||
reminder: number = 0; // 提醒提前分钟数(来自 VALARM,0 = 不提醒)
|
reminder: number = 0; // 主提醒提前分钟数(第一个提醒,兼容旧字段),0 = 不提醒
|
||||||
|
reminders: number[] = []; // 全部提醒提前分钟数(多选),按添加顺序
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 远端事件条目(REPORT 解析结果) */
|
/** 远端事件条目(REPORT 解析结果) */
|
||||||
@@ -43,7 +44,8 @@ export class RemoteEvent {
|
|||||||
completed: boolean = false; // VTODO STATUS:COMPLETED
|
completed: boolean = false; // VTODO STATUS:COMPLETED
|
||||||
rrule: string = ''; // 原始 RRULE
|
rrule: string = ''; // 原始 RRULE
|
||||||
exdate: string = ''; // 原始 EXDATE(分号分隔)
|
exdate: string = ''; // 原始 EXDATE(分号分隔)
|
||||||
reminder: number = 0; // 提醒提前分钟数(VALARM)
|
reminder: number = 0; // 主提醒提前分钟数(第一个提醒,兼容旧字段)
|
||||||
|
reminders: number[] = []; // 全部提醒提前分钟数(多选)
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EventDb {
|
export class EventDb {
|
||||||
@@ -97,10 +99,31 @@ export class EventDb {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
// 列已存在
|
// 列已存在
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
await store.executeSql("ALTER TABLE events ADD COLUMN reminders TEXT DEFAULT ''");
|
||||||
|
} catch (err) {
|
||||||
|
// 列已存在
|
||||||
|
}
|
||||||
EventDb.db = store;
|
EventDb.db = store;
|
||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 提醒数组规范化:去掉 0/非法值并去重升序 */
|
||||||
|
static normalizeReminders(list: number[]): number[] {
|
||||||
|
const set = new Set<number>();
|
||||||
|
for (const n of list) {
|
||||||
|
if (n > 0) {
|
||||||
|
set.add(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(set).sort((a: number, b: number): number => a - b);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提醒数组比较键(规范化后逗号连接,用于增量比对) */
|
||||||
|
static remKey(list: number[]): string {
|
||||||
|
return EventDb.normalizeReminders(list).join(',');
|
||||||
|
}
|
||||||
|
|
||||||
private static fromRow(rs: relationalStore.ResultSet): LocalEvent {
|
private static fromRow(rs: relationalStore.ResultSet): LocalEvent {
|
||||||
const e = new LocalEvent();
|
const e = new LocalEvent();
|
||||||
e.id = rs.getLong(rs.getColumnIndex('id'));
|
e.id = rs.getLong(rs.getColumnIndex('id'));
|
||||||
@@ -124,6 +147,14 @@ export class EventDb {
|
|||||||
e.rrule = rs.getString(rs.getColumnIndex('rrule'));
|
e.rrule = rs.getString(rs.getColumnIndex('rrule'));
|
||||||
e.exdate = rs.getString(rs.getColumnIndex('exdate'));
|
e.exdate = rs.getString(rs.getColumnIndex('exdate'));
|
||||||
e.reminder = rs.getLong(rs.getColumnIndex('reminder'));
|
e.reminder = rs.getLong(rs.getColumnIndex('reminder'));
|
||||||
|
// 多提醒(逗号分隔分钟数);旧数据无该列时回退单提醒
|
||||||
|
const remStr: string = rs.getString(rs.getColumnIndex('reminders'));
|
||||||
|
if (remStr !== '') {
|
||||||
|
e.reminders = remStr.split(',').map((s: string): number => Number(s))
|
||||||
|
.filter((n: number): boolean => !Number.isNaN(n) && n > 0);
|
||||||
|
} else if (e.reminder > 0) {
|
||||||
|
e.reminders = [e.reminder];
|
||||||
|
}
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,7 +178,8 @@ export class EventDb {
|
|||||||
'completed': e.completed ? 1 : 0,
|
'completed': e.completed ? 1 : 0,
|
||||||
'rrule': e.rrule,
|
'rrule': e.rrule,
|
||||||
'exdate': e.exdate,
|
'exdate': e.exdate,
|
||||||
'reminder': e.reminder
|
'reminder': e.reminders.length > 0 ? e.reminders[0] : 0,
|
||||||
|
'reminders': e.reminders.map((n: number): string => String(n)).join(',')
|
||||||
};
|
};
|
||||||
return bucket;
|
return bucket;
|
||||||
}
|
}
|
||||||
@@ -365,10 +397,12 @@ export class EventDb {
|
|||||||
e.rrule = r.rrule;
|
e.rrule = r.rrule;
|
||||||
e.exdate = r.exdate;
|
e.exdate = r.exdate;
|
||||||
e.reminder = r.reminder;
|
e.reminder = r.reminder;
|
||||||
|
e.reminders = r.reminders;
|
||||||
await store.insert('events', EventDb.toBucket(e));
|
await store.insert('events', EventDb.toBucket(e));
|
||||||
added++;
|
added++;
|
||||||
} else if (found.etag !== r.etag || found.rrule !== r.rrule || found.exdate !== r.exdate
|
} else if (found.etag !== r.etag || found.rrule !== r.rrule || found.exdate !== r.exdate
|
||||||
|| found.reminder !== r.reminder || found.recurring !== r.recurring) {
|
|| found.reminder !== r.reminder || found.recurring !== r.recurring
|
||||||
|
|| EventDb.remKey(found.reminders) !== EventDb.remKey(r.reminders)) {
|
||||||
// 更新(rrule/exdate/recurring 变化也更新,兼容老数据回填)
|
// 更新(rrule/exdate/recurring 变化也更新,兼容老数据回填)
|
||||||
found.title = r.title;
|
found.title = r.title;
|
||||||
found.description = r.description;
|
found.description = r.description;
|
||||||
@@ -385,6 +419,7 @@ export class EventDb {
|
|||||||
found.rrule = r.rrule;
|
found.rrule = r.rrule;
|
||||||
found.exdate = r.exdate;
|
found.exdate = r.exdate;
|
||||||
found.reminder = r.reminder;
|
found.reminder = r.reminder;
|
||||||
|
found.reminders = r.reminders;
|
||||||
const up = new relationalStore.RdbPredicates('events');
|
const up = new relationalStore.RdbPredicates('events');
|
||||||
up.equalTo('id', found.id);
|
up.equalTo('id', found.id);
|
||||||
await store.update(EventDb.toBucket(found), up);
|
await store.update(EventDb.toBucket(found), up);
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ class ParsedEvent {
|
|||||||
inAlarm: boolean = false; // 是否处于 VALARM 子组件内(内部属性不参与解析)
|
inAlarm: boolean = false; // 是否处于 VALARM 子组件内(内部属性不参与解析)
|
||||||
rrule: string = ''; // 原始 RRULE 值
|
rrule: string = ''; // 原始 RRULE 值
|
||||||
exdates: string[] = []; // 原始 EXDATE 值列表
|
exdates: string[] = []; // 原始 EXDATE 值列表
|
||||||
reminder: number = 0; // 提醒提前分钟数(来自 VALARM TRIGGER,0 = 不提醒)
|
reminder: number = 0; // 主提醒提前分钟数(第一个 VALARM,0 = 不提醒)
|
||||||
|
reminders: number[] = []; // 全部提醒提前分钟数(多 VALARM)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** iCal 时间解析结果 */
|
/** iCal 时间解析结果 */
|
||||||
@@ -71,7 +72,13 @@ export class IcsUtil {
|
|||||||
const av: string = line.substring(ac + 1).trim();
|
const av: string = line.substring(ac + 1).trim();
|
||||||
const an: string = ap.split(';')[0].toUpperCase();
|
const an: string = ap.split(';')[0].toUpperCase();
|
||||||
if (an === 'TRIGGER') {
|
if (an === 'TRIGGER') {
|
||||||
current.reminder = IcsUtil.triggerMinutes(av, ap);
|
const m: number = IcsUtil.triggerMinutes(av, ap);
|
||||||
|
if (m > 0) {
|
||||||
|
current.reminders.push(m);
|
||||||
|
if (current.reminder === 0) {
|
||||||
|
current.reminder = m; // 第一个提醒作为主提醒(兼容旧字段)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@@ -134,6 +141,7 @@ export class IcsUtil {
|
|||||||
r.rrule = p.rrule;
|
r.rrule = p.rrule;
|
||||||
r.exdate = p.exdates.join(';');
|
r.exdate = p.exdates.join(';');
|
||||||
r.reminder = p.reminder;
|
r.reminder = p.reminder;
|
||||||
|
r.reminders = p.reminders;
|
||||||
return r;
|
return r;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -244,11 +252,23 @@ export class IcsUtil {
|
|||||||
`SUMMARY:${IcsUtil.escape(e.title)}\r\n` +
|
`SUMMARY:${IcsUtil.escape(e.title)}\r\n` +
|
||||||
(e.location !== '' ? `LOCATION:${IcsUtil.escape(e.location)}\r\n` : '') +
|
(e.location !== '' ? `LOCATION:${IcsUtil.escape(e.location)}\r\n` : '') +
|
||||||
(e.description !== '' ? `DESCRIPTION:${IcsUtil.escape(e.description)}\r\n` : '') +
|
(e.description !== '' ? `DESCRIPTION:${IcsUtil.escape(e.description)}\r\n` : '') +
|
||||||
(e.reminder > 0 ? IcsUtil.valarm(e.reminder) : '') +
|
IcsUtil.valarms(e.reminders.length > 0 ? e.reminders
|
||||||
|
: (e.reminder > 0 ? [e.reminder] : [])) +
|
||||||
'END:VEVENT\r\n' +
|
'END:VEVENT\r\n' +
|
||||||
'END:VCALENDAR\r\n';
|
'END:VCALENDAR\r\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 由提醒分钟数组生成多个 VALARM(已规范化去重) */
|
||||||
|
private static valarms(minutes: number[]): string {
|
||||||
|
let out: string = '';
|
||||||
|
for (const m of minutes) {
|
||||||
|
if (m > 0) {
|
||||||
|
out += IcsUtil.valarm(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
/** 由提前分钟数生成 VALARM(TRIGGER 负时长 = 提前触发) */
|
/** 由提前分钟数生成 VALARM(TRIGGER 负时长 = 提前触发) */
|
||||||
private static valarm(minutes: number): string {
|
private static valarm(minutes: number): string {
|
||||||
let trigger: string;
|
let trigger: string;
|
||||||
|
|||||||
@@ -1,25 +1,107 @@
|
|||||||
// entry/src/main/ets/common/LogUtil.ets
|
// entry/src/main/ets/common/LogUtil.ets
|
||||||
// 日志功能已按需求移除:保留空实现以兼容既有调用点(不写文件、不再缓冲)
|
// 同步日志:写入应用 filesDir/sync.log(含时间戳),超 512KB 自动截断保留后半。
|
||||||
|
// 设置页「同步日志」卡片可查看/复制/清空,方便真机测试时诊断同步与提醒问题。
|
||||||
import { common } from '@kit.AbilityKit';
|
import { common } from '@kit.AbilityKit';
|
||||||
|
import { fileIo as fs } from '@kit.CoreFileKit';
|
||||||
|
import { util } from '@kit.ArkTS';
|
||||||
|
import { BusinessError } from '@kit.BasicServicesKit';
|
||||||
|
|
||||||
export class LogUtil {
|
export class LogUtil {
|
||||||
static logFilePath(): string {
|
private static path: string = '';
|
||||||
return '';
|
private static readonly MAX_BYTES: number = 512 * 1024;
|
||||||
}
|
|
||||||
|
|
||||||
/** 空实现(日志功能已移除) */
|
|
||||||
static init(context: common.Context): void {
|
static init(context: common.Context): void {
|
||||||
|
try {
|
||||||
|
LogUtil.path = `${context.filesDir}/sync.log`;
|
||||||
|
} catch (err) {
|
||||||
|
LogUtil.path = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 仅输出到 console(hilog),不再写文件 */
|
static logFilePath(): string {
|
||||||
|
return LogUtil.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 写一行日志:console + 文件(文件失败静默,不影响业务) */
|
||||||
static write(msg: string): void {
|
static write(msg: string): void {
|
||||||
console.info(`SyncLog ${msg}`);
|
console.info(`SyncLog ${msg}`);
|
||||||
|
if (LogUtil.path === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const d = new Date();
|
||||||
|
const p = (n: number): string => n < 10 ? '0' + n : String(n);
|
||||||
|
const ts: string = `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}` +
|
||||||
|
` ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
||||||
|
// 超限截断:保留最后 256KB
|
||||||
|
try {
|
||||||
|
const st: fs.Stat = fs.statSync(LogUtil.path);
|
||||||
|
if (st.size > LogUtil.MAX_BYTES) {
|
||||||
|
LogUtil.truncate();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// 文件不存在属正常,直接创建写入
|
||||||
|
}
|
||||||
|
const file: fs.File = fs.openSync(LogUtil.path,
|
||||||
|
fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE | fs.OpenMode.APPEND);
|
||||||
|
fs.writeSync(file.fd, `[${ts}] ${msg}\n`);
|
||||||
|
fs.closeSync(file.fd);
|
||||||
|
} catch (err) {
|
||||||
|
// 写日志失败不影响业务
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 截断:只保留文件末尾 256KB */
|
||||||
|
private static truncate(): void {
|
||||||
|
try {
|
||||||
|
const keep: number = 256 * 1024;
|
||||||
|
const st: fs.Stat = fs.statSync(LogUtil.path);
|
||||||
|
const size: number = Math.min(st.size, keep);
|
||||||
|
const file = fs.openSync(LogUtil.path, fs.OpenMode.READ_ONLY);
|
||||||
|
const buf: ArrayBuffer = new ArrayBuffer(size);
|
||||||
|
fs.readSync(file.fd, buf, { offset: st.size - size });
|
||||||
|
fs.closeSync(file.fd);
|
||||||
|
const out = fs.openSync(LogUtil.path,
|
||||||
|
fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE | fs.OpenMode.TRUNC);
|
||||||
|
fs.writeSync(out.fd, `…(日志过长,已截断保留末尾)\n`);
|
||||||
|
fs.writeSync(out.fd, new Uint8Array(buf));
|
||||||
|
fs.closeSync(out.fd);
|
||||||
|
} catch (err) {
|
||||||
|
// 截断失败忽略,下次再试
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static readAll(context: common.Context): string {
|
static readAll(context: common.Context): string {
|
||||||
return '(日志功能已移除)';
|
try {
|
||||||
|
if (LogUtil.path === '') {
|
||||||
|
LogUtil.init(context);
|
||||||
|
}
|
||||||
|
if (LogUtil.path === '') {
|
||||||
|
return '(无法确定日志路径)';
|
||||||
|
}
|
||||||
|
const st: fs.Stat = fs.statSync(LogUtil.path);
|
||||||
|
if (st.size === 0) {
|
||||||
|
return '(日志为空)';
|
||||||
|
}
|
||||||
|
const file = fs.openSync(LogUtil.path, fs.OpenMode.READ_ONLY);
|
||||||
|
const buf: ArrayBuffer = new ArrayBuffer(st.size);
|
||||||
|
fs.readSync(file.fd, buf);
|
||||||
|
fs.closeSync(file.fd);
|
||||||
|
const decoder: util.TextDecoder = util.TextDecoder.create('utf-8');
|
||||||
|
return decoder.decodeToString(new Uint8Array(buf));
|
||||||
|
} catch (err) {
|
||||||
|
return `(读取日志失败: ${(err as BusinessError).message})`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static clear(context: common.Context): void {
|
static clear(context: common.Context): void {
|
||||||
|
try {
|
||||||
|
if (LogUtil.path === '') {
|
||||||
|
LogUtil.init(context);
|
||||||
|
}
|
||||||
|
fs.unlinkSync(LogUtil.path);
|
||||||
|
} catch (err) {
|
||||||
|
// 文件不存在等,忽略
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,136 @@
|
|||||||
// entry/src/main/ets/common/ReminderService.ets
|
// entry/src/main/ets/common/ReminderService.ets
|
||||||
// 日程提醒:基于后台代理提醒(reminderAgentManager),
|
// 日程提醒,双模式:
|
||||||
// 在日程开始前 N 分钟(来自服务器 VALARM TRIGGER)发系统通知,可右滑删除
|
// 1) 系统代理提醒(reminderAgentManager):需要华为审批的"代理提醒使用权限",
|
||||||
|
// 未申请的普通应用配额为 0(发布报 1700002 且池内为 0)
|
||||||
|
// 2) 应用内通知(notificationManager):配额为 0 时的降级方案。
|
||||||
|
// 依赖应用进程存活(前台或"后台持续同步"长时任务),由自动同步定时器驱动,
|
||||||
|
// 每次检查把"提醒时刻刚好已到"的日程用本地通知发出。
|
||||||
import { reminderAgentManager } from '@kit.BackgroundTasksKit';
|
import { reminderAgentManager } from '@kit.BackgroundTasksKit';
|
||||||
|
import { notificationManager } from '@kit.NotificationKit';
|
||||||
import { common } from '@kit.AbilityKit';
|
import { common } from '@kit.AbilityKit';
|
||||||
import { BusinessError } from '@kit.BasicServicesKit';
|
import { BusinessError } from '@kit.BasicServicesKit';
|
||||||
import { EventDb, LocalEvent } from './EventDb';
|
import { EventDb, LocalEvent } from './EventDb';
|
||||||
import { AppSettings } from './AppSettings';
|
import { AppSettings } from './AppSettings';
|
||||||
|
import { AccountStore } from './AccountStore';
|
||||||
import { LogUtil } from './LogUtil';
|
import { LogUtil } from './LogUtil';
|
||||||
|
|
||||||
export class ReminderService {
|
export class ReminderService {
|
||||||
private static readonly MAX_REMINDERS: number = 30; // 系统对单应用代理提醒数量有限制
|
// 系统对单应用代理提醒上限 30 条,留 5 个余量给测试提醒等临时请求
|
||||||
|
private static readonly MAX_REMINDERS: number = 25;
|
||||||
private static readonly WINDOW: number = 7 * 86400000; // 只排未来 7 天
|
private static readonly WINDOW: number = 7 * 86400000; // 只排未来 7 天
|
||||||
|
|
||||||
/** 全量重建提醒:先取消本应用全部代理提醒,再按当前数据重新发布 */
|
/** 代理提醒可用性:null=未探测(每次进程启动重新探测),false=配额为0(走应用内通知),true=可用 */
|
||||||
static async refreshReminders(context: common.Context): Promise<void> {
|
static agentAvailable: boolean | null = null;
|
||||||
|
|
||||||
|
/** calKey → 日历本名称缓存(通知文案用),60 秒刷新一次 */
|
||||||
|
private static calNameCache: Map<string, string> = new Map();
|
||||||
|
private static calNameLoadedAt: number = 0;
|
||||||
|
|
||||||
|
/** 日历本名称(通知文案用):缓存 60 秒,'local' = 本机日历 */
|
||||||
|
private static async calName(context: common.Context, calKey: string): Promise<string> {
|
||||||
|
if (Date.now() - ReminderService.calNameLoadedAt > 60000
|
||||||
|
|| ReminderService.calNameCache.size === 0) {
|
||||||
|
const map: Map<string, string> = new Map();
|
||||||
|
map.set('local', '本机日历');
|
||||||
|
try {
|
||||||
|
const accounts = await AccountStore.loadAll(context);
|
||||||
|
for (const acc of accounts) {
|
||||||
|
for (let i: number = 0; i < acc.calendarHrefs.length; i++) {
|
||||||
|
const bookName: string = i < acc.calendarNames.length
|
||||||
|
? acc.calendarNames[i] : `日历本${i + 1}`;
|
||||||
|
map.set(`${acc.id}_${i}`, `${acc.name}·${bookName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// 读取失败用旧缓存
|
||||||
|
}
|
||||||
|
ReminderService.calNameCache = map;
|
||||||
|
ReminderService.calNameLoadedAt = Date.now();
|
||||||
|
}
|
||||||
|
const name: string | undefined = ReminderService.calNameCache.get(calKey);
|
||||||
|
return name !== undefined ? name : '日历';
|
||||||
|
}
|
||||||
|
|
||||||
|
private static delay(ms: number): Promise<void> {
|
||||||
|
return new Promise<void>((resolve: () => void) => {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前系统里本应用的有效提醒条数 */
|
||||||
|
private static async poolCount(): Promise<number> {
|
||||||
|
try {
|
||||||
|
const all = await reminderAgentManager.getValidReminders();
|
||||||
|
return all.length;
|
||||||
|
} catch (err) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布单条代理提醒,1700002(池满/配额0)时清空后等待释放再重试,最多 3 轮。
|
||||||
|
* 3 轮后仍满且池内为 0 → 判定配额为 0(需华为邮件申请权限),抛出标记错误。
|
||||||
|
*/
|
||||||
|
private static async publishWithRetry(request: reminderAgentManager.ReminderRequestCalendar): Promise<void> {
|
||||||
|
try {
|
||||||
|
await reminderAgentManager.publishReminder(request);
|
||||||
|
ReminderService.agentAvailable = true;
|
||||||
|
return;
|
||||||
|
} catch (err) {
|
||||||
|
const e = err as BusinessError;
|
||||||
|
if (e.code !== 1700002) {
|
||||||
|
// ArkTS 只允许抛 Error:错误码编入消息,调用方从 message 解析
|
||||||
|
throw new Error(`${e.code}:${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let attempt: number = 1; attempt <= 3; attempt++) {
|
||||||
|
// 已触发的代理提醒会残留为通知,一并清理
|
||||||
|
try {
|
||||||
|
await notificationManager.cancelAll();
|
||||||
|
} catch (err) {
|
||||||
|
// 忽略
|
||||||
|
}
|
||||||
|
await reminderAgentManager.cancelAllReminders();
|
||||||
|
await ReminderService.delay(1500);
|
||||||
|
const remain: number = await ReminderService.poolCount();
|
||||||
|
LogUtil.write(`提醒池满(第 ${attempt} 次腾位):清空后剩余 ${remain} 条,重试发布`);
|
||||||
|
try {
|
||||||
|
await reminderAgentManager.publishReminder(request);
|
||||||
|
ReminderService.agentAvailable = true;
|
||||||
|
return;
|
||||||
|
} catch (err) {
|
||||||
|
const e = err as BusinessError;
|
||||||
|
if (e.code !== 1700002) {
|
||||||
|
throw new Error(`${e.code}:${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('1700002:提醒池腾位 3 轮后仍满');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判定"配额为 0":1700002 且系统内有效提醒为 0(没有超额却报超额 = 没有配额) */
|
||||||
|
private static async isQuotaZero(): Promise<boolean> {
|
||||||
|
const remain: number = await ReminderService.poolCount();
|
||||||
|
return remain <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 全量重建代理提醒(仅在代理提醒可用时调用)。返回发布条数 */
|
||||||
|
static async refreshReminders(context: common.Context): Promise<number> {
|
||||||
|
let published: number = 0;
|
||||||
|
if (ReminderService.agentAvailable === false) {
|
||||||
|
return 0; // 应用内通知模式接管,不再请求代理提醒
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await reminderAgentManager.cancelAllReminders();
|
await reminderAgentManager.cancelAllReminders();
|
||||||
|
const before: number = await ReminderService.poolCount();
|
||||||
|
if (before > 0) {
|
||||||
|
LogUtil.write(`取消全部提醒后系统仍剩 ${before} 条`);
|
||||||
|
}
|
||||||
const now: number = Date.now();
|
const now: number = Date.now();
|
||||||
const rows: LocalEvent[] =
|
const rows: LocalEvent[] =
|
||||||
await EventDb.queryRemindable(context, now, now + ReminderService.WINDOW);
|
await EventDb.queryRemindable(context, now, now + ReminderService.WINDOW);
|
||||||
// 提醒静音的日历本(如别人分享的本):其中所有日程都不再提醒
|
// 提醒静音的日历本(如别人分享的本):其中所有日程都不再提醒
|
||||||
const muted: string[] = await AppSettings.getMutedReminderKeys(context);
|
const muted: string[] = await AppSettings.getMutedReminderKeys(context);
|
||||||
let published: number = 0;
|
|
||||||
for (const e of rows) {
|
for (const e of rows) {
|
||||||
if (muted.includes(e.calKey)) {
|
if (muted.includes(e.calKey)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -29,12 +138,19 @@ export class ReminderService {
|
|||||||
if (published >= ReminderService.MAX_REMINDERS) {
|
if (published >= ReminderService.MAX_REMINDERS) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const remindAt: number = e.startTime - e.reminder * 60000;
|
// 多提醒:每个提前量各发一条(含旧单提醒数据回退)
|
||||||
|
const rems: number[] = e.reminders.length > 0 ? e.reminders
|
||||||
|
: (e.reminder > 0 ? [e.reminder] : []);
|
||||||
|
for (const rem of rems) {
|
||||||
|
if (published >= ReminderService.MAX_REMINDERS) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const remindAt: number = e.startTime - rem * 60000;
|
||||||
if (remindAt <= now) {
|
if (remindAt <= now) {
|
||||||
continue; // 提醒时间已过
|
continue; // 提醒时间已过
|
||||||
}
|
}
|
||||||
const d = new Date(remindAt);
|
const d = new Date(remindAt);
|
||||||
const startTimeText: string = ReminderService.fmt(e.startTime);
|
const calName: string = await ReminderService.calName(context, e.calKey);
|
||||||
const request: reminderAgentManager.ReminderRequestCalendar = {
|
const request: reminderAgentManager.ReminderRequestCalendar = {
|
||||||
reminderType: reminderAgentManager.ReminderType.REMINDER_TYPE_CALENDAR,
|
reminderType: reminderAgentManager.ReminderType.REMINDER_TYPE_CALENDAR,
|
||||||
dateTime: {
|
dateTime: {
|
||||||
@@ -46,20 +162,183 @@ export class ReminderService {
|
|||||||
second: 0
|
second: 0
|
||||||
},
|
},
|
||||||
title: '日程提醒',
|
title: '日程提醒',
|
||||||
content: `「${e.title}」将于 ${startTimeText} 开始(提前 ${e.reminder} 分钟)`,
|
content: `「${calName}」中的「${e.title}」将在 ${rem} 分钟后开始`,
|
||||||
notificationId: e.id % 100000,
|
notificationId: (e.id * 10 + rem) % 100000,
|
||||||
expiredContent: '该日程已开始',
|
expiredContent: '该日程已开始',
|
||||||
ringDuration: 5,
|
ringDuration: 5,
|
||||||
snoozeTimes: 0,
|
snoozeTimes: 0,
|
||||||
timeInterval: 0
|
timeInterval: 0
|
||||||
};
|
};
|
||||||
await reminderAgentManager.publishReminder(request);
|
await ReminderService.publishWithRetry(request);
|
||||||
published++;
|
published++;
|
||||||
}
|
}
|
||||||
LogUtil.write(`提醒刷新完成:发布 ${published} 个提醒(窗口 7 天,静音日历本 ${muted.length} 个)`);
|
}
|
||||||
|
const after: number = await ReminderService.poolCount();
|
||||||
|
LogUtil.write(`提醒刷新完成:发布 ${published} 个提醒(窗口 7 天,静音日历本 ${muted.length} 个,池内现有 ${after} 条)`);
|
||||||
|
return published;
|
||||||
|
} catch (err) {
|
||||||
|
const e = err as Error;
|
||||||
|
if (e.message.startsWith('1700002') && await ReminderService.isQuotaZero()) {
|
||||||
|
// 配额为 0:普通应用未获代理提醒权限,降级为应用内通知模式
|
||||||
|
ReminderService.agentAvailable = false;
|
||||||
|
await AppSettings.setReminderTick(context, Date.now());
|
||||||
|
LogUtil.write('代理提醒配额为 0(需向华为邮件申请权限),已自动切换为应用内通知模式');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
LogUtil.write(`提醒刷新失败: ${e.message}`);
|
||||||
|
return published;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用内提醒检查(降级模式):由自动同步定时器高频调用。
|
||||||
|
* 把"提醒时刻落在上次检查~现在之间"的日程用本地通知发出。
|
||||||
|
* App 进程被杀期间的提醒会错过(超过 2 分钟不补发),长时任务可避免。
|
||||||
|
*/
|
||||||
|
static async tickReminders(context: common.Context): Promise<number> {
|
||||||
|
if (ReminderService.agentAvailable !== false) {
|
||||||
|
return 0; // 代理提醒可用或未判定,不需要降级
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const now: number = Date.now();
|
||||||
|
const last: number = await AppSettings.getReminderTick(context);
|
||||||
|
if (last <= 0) {
|
||||||
|
await AppSettings.setReminderTick(context, now); // 首次运行:只记基线不补发
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const from: number = Math.max(last, now - 2 * 60000); // 最多补发 2 分钟内的错过提醒
|
||||||
|
await AppSettings.setReminderTick(context, now);
|
||||||
|
const rows: LocalEvent[] =
|
||||||
|
await EventDb.queryRemindable(context, now, now + ReminderService.WINDOW);
|
||||||
|
const muted: string[] = await AppSettings.getMutedReminderKeys(context);
|
||||||
|
let fired: number = 0;
|
||||||
|
for (const e of rows) {
|
||||||
|
if (muted.includes(e.calKey)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const rems: number[] = e.reminders.length > 0 ? e.reminders
|
||||||
|
: (e.reminder > 0 ? [e.reminder] : []);
|
||||||
|
for (const rem of rems) {
|
||||||
|
const remindAt: number = e.startTime - rem * 60000;
|
||||||
|
if (remindAt <= from || remindAt > now) {
|
||||||
|
continue; // 不在本次检查窗口内
|
||||||
|
}
|
||||||
|
const calName: string = await ReminderService.calName(context, e.calKey);
|
||||||
|
const minLeft: number = Math.max(1,
|
||||||
|
Math.ceil((e.startTime - Date.now()) / 60000));
|
||||||
|
const fired2: number = await ReminderService.notifyNow(
|
||||||
|
'日程提醒', `「${calName}」中的「${e.title}」将在 ${minLeft} 分钟后开始`,
|
||||||
|
(e.id * 10 + rem) % 100000);
|
||||||
|
if (fired2 === 0) {
|
||||||
|
fired++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fired > 0) {
|
||||||
|
LogUtil.write(`应用内提醒:本次发出 ${fired} 条到点提醒`);
|
||||||
|
}
|
||||||
|
return fired;
|
||||||
|
} catch (err) {
|
||||||
|
const e = err as Error;
|
||||||
|
LogUtil.write(`应用内提醒检查失败: ${e.message}`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 日历本通知渠道(SOCIAL_COMMUNICATION 默认横幅+锁屏+响铃振动),只注册一次 */
|
||||||
|
private static slotReady: boolean = false;
|
||||||
|
|
||||||
|
private static async ensureSlot(): Promise<void> {
|
||||||
|
if (ReminderService.slotReady) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 本 SDK 无锁屏可见性 API(已废弃),锁屏显示由系统"锁屏通知"设置 + 渠道默认行为决定
|
||||||
|
await notificationManager.addSlot(notificationManager.SlotType.SOCIAL_COMMUNICATION);
|
||||||
|
} catch (err) {
|
||||||
|
// 渠道已存在等情况忽略
|
||||||
|
}
|
||||||
|
ReminderService.slotReady = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 立即发一条本地通知。返回 0 表示成功 */
|
||||||
|
private static async notifyNow(title: string, text: string, id: number): Promise<number> {
|
||||||
|
await ReminderService.ensureSlot();
|
||||||
|
try {
|
||||||
|
const request: notificationManager.NotificationRequest = {
|
||||||
|
id: id,
|
||||||
|
notificationSlotType: notificationManager.SlotType.SOCIAL_COMMUNICATION,
|
||||||
|
content: {
|
||||||
|
notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
|
||||||
|
normal: {
|
||||||
|
title: title,
|
||||||
|
text: text,
|
||||||
|
additionalText: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await notificationManager.publish(request);
|
||||||
|
return 0;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const e = err as BusinessError;
|
const e = err as BusinessError;
|
||||||
LogUtil.write(`提醒刷新失败: ${e.code} - ${e.message}`);
|
LogUtil.write(`本地通知发布失败: ${e.code} - ${e.message}`);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 测试入口:代理可用时发 1 分钟后的代理提醒;降级模式发立即本地通知 */
|
||||||
|
static async publishTestReminder(context: common.Context): Promise<string> {
|
||||||
|
if (ReminderService.agentAvailable === false) {
|
||||||
|
const r: number = await ReminderService.notifyNow(
|
||||||
|
'测试通知(应用内模式)', '如果你看到这条通知,说明应用内提醒链路正常', 99998);
|
||||||
|
return r === 0
|
||||||
|
? '已发出测试通知(应用内模式:代理提醒配额为 0,由 App 进程在提醒时刻发通知)'
|
||||||
|
: '本地通知发布失败,详见日志';
|
||||||
|
}
|
||||||
|
// request 构造放在 try 外:腾位重试也要用到
|
||||||
|
const t: number = Date.now() + 60 * 1000;
|
||||||
|
const d = new Date(t);
|
||||||
|
const request: reminderAgentManager.ReminderRequestCalendar = {
|
||||||
|
reminderType: reminderAgentManager.ReminderType.REMINDER_TYPE_CALENDAR,
|
||||||
|
dateTime: {
|
||||||
|
year: d.getFullYear(),
|
||||||
|
month: d.getMonth() + 1,
|
||||||
|
day: d.getDate(),
|
||||||
|
hour: d.getHours(),
|
||||||
|
minute: d.getMinutes(),
|
||||||
|
second: 0
|
||||||
|
},
|
||||||
|
title: '测试提醒',
|
||||||
|
content: '这是一条测试提醒:如果你看到它,说明提醒链路正常',
|
||||||
|
notificationId: 99999,
|
||||||
|
expiredContent: '测试提醒已过期',
|
||||||
|
ringDuration: 5,
|
||||||
|
snoozeTimes: 0,
|
||||||
|
timeInterval: 0
|
||||||
|
};
|
||||||
|
const before: number = await ReminderService.poolCount();
|
||||||
|
LogUtil.write(`测试提醒发布前池内提醒 ${before} 条`);
|
||||||
|
try {
|
||||||
|
await ReminderService.publishWithRetry(request);
|
||||||
|
LogUtil.write('测试提醒已发布:1 分钟后应弹出通知');
|
||||||
|
return `已发布(发布前池内 ${before} 条),1 分钟后应弹出通知`;
|
||||||
|
} catch (err) {
|
||||||
|
const e = err as Error;
|
||||||
|
if (e.message.startsWith('1700002') && await ReminderService.isQuotaZero()) {
|
||||||
|
ReminderService.agentAvailable = false;
|
||||||
|
await AppSettings.setReminderTick(context, Date.now());
|
||||||
|
LogUtil.write('代理提醒配额为 0(需向华为邮件申请权限),已切换为应用内通知模式');
|
||||||
|
const r: number = await ReminderService.notifyNow(
|
||||||
|
'测试通知(应用内模式)', '如果你看到这条通知,说明应用内提醒链路正常', 99998);
|
||||||
|
return r === 0
|
||||||
|
? '代理提醒配额为 0(需邮件向华为申请),已切换为应用内通知模式并发出测试通知'
|
||||||
|
: '代理提醒配额为 0,已切换应用内模式,但测试通知发布失败(详见日志)';
|
||||||
|
}
|
||||||
|
LogUtil.write(`测试提醒发布失败: ${e.message}`);
|
||||||
|
const sep: number = e.message.indexOf(':');
|
||||||
|
return sep > 0
|
||||||
|
? `发布失败:错误码 ${e.message.substring(0, sep)}(${e.message.substring(sep + 1)})`
|
||||||
|
: `发布失败:${e.message}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -277,6 +277,7 @@ export class SyncEngine {
|
|||||||
r.rrule = e.rrule;
|
r.rrule = e.rrule;
|
||||||
r.exdate = e.exdate;
|
r.exdate = e.exdate;
|
||||||
r.reminder = e.reminder;
|
r.reminder = e.reminder;
|
||||||
|
r.reminders = e.reminders.slice();
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,7 +320,7 @@ export class SyncEngine {
|
|||||||
const ics: string = IcsUtil.build(e);
|
const ics: string = IcsUtil.build(e);
|
||||||
const etag: string = await DavClient.putEvent(url, auth, ics);
|
const etag: string = await DavClient.putEvent(url, auth, ics);
|
||||||
await EventDb.clearDirty(context, e.id, etag);
|
await EventDb.clearDirty(context, e.id, etag);
|
||||||
LogUtil.write(`推送保存「${e.title}」→ ${url}(${ics.length} 字节)`);
|
LogUtil.write(`推送保存「${e.title}」→ ${url}(${ics.length} 字节,提醒=${e.reminders.map((n: number): string => String(n)).join('/')}分钟,VALARM=${ics.toUpperCase().includes('BEGIN:VALARM') ? '有' : '无'})`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,16 @@ import { window } from '@kit.ArkUI';
|
|||||||
import { notificationManager } from '@kit.NotificationKit';
|
import { notificationManager } from '@kit.NotificationKit';
|
||||||
import { BusinessError } from '@kit.BasicServicesKit';
|
import { BusinessError } from '@kit.BasicServicesKit';
|
||||||
import { BackgroundSyncService } from '../common/BackgroundSyncService';
|
import { BackgroundSyncService } from '../common/BackgroundSyncService';
|
||||||
|
import { LogUtil } from '../common/LogUtil';
|
||||||
|
|
||||||
const DOMAIN = 0x0000;
|
const DOMAIN = 0x0000;
|
||||||
|
|
||||||
export default class EntryAbility extends UIAbility {
|
export default class EntryAbility extends UIAbility {
|
||||||
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
|
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
|
||||||
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
|
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
|
||||||
|
// 文件日志初始化:所有页面的同步日志统一写入 filesDir/sync.log
|
||||||
|
LogUtil.init(this.context);
|
||||||
|
LogUtil.write('---- App 启动 ----');
|
||||||
// 后台同步:注册延迟任务兜底 + 按设置恢复长时任务
|
// 后台同步:注册延迟任务兜底 + 按设置恢复长时任务
|
||||||
BackgroundSyncService.initOnLaunch(this.context);
|
BackgroundSyncService.initOnLaunch(this.context);
|
||||||
// 申请通知权限:日程提醒、后台同步常驻通知都依赖它(系统只弹一次授权框)
|
// 申请通知权限:日程提醒、后台同步常驻通知都依赖它(系统只弹一次授权框)
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { EventDb, LocalEvent } from '../common/EventDb';
|
|||||||
import { AppSettings } from '../common/AppSettings';
|
import { AppSettings } from '../common/AppSettings';
|
||||||
import { DavClient } from '../common/DavClient';
|
import { DavClient } from '../common/DavClient';
|
||||||
import { SyncEngine } from '../common/SyncEngine';
|
import { SyncEngine } from '../common/SyncEngine';
|
||||||
|
import { ReminderService } from '../common/ReminderService';
|
||||||
|
import { LogUtil } from '../common/LogUtil';
|
||||||
|
|
||||||
/** 可选的日历本 */
|
/** 可选的日历本 */
|
||||||
class BookChoice {
|
class BookChoice {
|
||||||
@@ -32,7 +34,7 @@ struct EventEditPage {
|
|||||||
@State statusMsg: string = '';
|
@State statusMsg: string = '';
|
||||||
@State isExisting: boolean = false;
|
@State isExisting: boolean = false;
|
||||||
@State repeatMode: string = 'none'; // none|daily|workday|weekly|monthly|yearly|custom
|
@State repeatMode: string = 'none'; // none|daily|workday|weekly|monthly|yearly|custom
|
||||||
@State reminderMin: number = 0; // 提前提醒分钟数,0 = 不提醒
|
@State reminderList: number[] = []; // 提前提醒分钟数(多选),空 = 不提醒
|
||||||
@State pickerShow: boolean = false; // 开始/结束时间选择底部弹层(日期+时间一次选完)
|
@State pickerShow: boolean = false; // 开始/结束时间选择底部弹层(日期+时间一次选完)
|
||||||
@State pickerIsEnd: boolean = false;
|
@State pickerIsEnd: boolean = false;
|
||||||
@State pickDate: Date = new Date();
|
@State pickDate: Date = new Date();
|
||||||
@@ -77,16 +79,18 @@ struct EventEditPage {
|
|||||||
this.endMs = loaded.endTime;
|
this.endMs = loaded.endTime;
|
||||||
this.chosenKey = loaded.calKey;
|
this.chosenKey = loaded.calKey;
|
||||||
this.repeatMode = loaded.rrule !== '' ? this.modeFromRrule(loaded.rrule) : 'none';
|
this.repeatMode = loaded.rrule !== '' ? this.modeFromRrule(loaded.rrule) : 'none';
|
||||||
this.reminderMin = loaded.reminder;
|
this.reminderList = loaded.reminders.length > 0 ? loaded.reminders
|
||||||
|
: (loaded.reminder > 0 ? [loaded.reminder] : []);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 新建:默认时间 = 所选日期 9:00-10:00
|
// 新建:日期取所选日(未选则今天),时间取当前时刻;结束 = 开始 + 1 小时
|
||||||
const base: number = AppStorage.get<number>('pendingEventDate') ?? Date.now();
|
const base: number = AppStorage.get<number>('pendingEventDate') ?? Date.now();
|
||||||
const dayStart = new Date(new Date(base).getFullYear(), new Date(base).getMonth(),
|
const dayStart = new Date(new Date(base).getFullYear(), new Date(base).getMonth(),
|
||||||
new Date(base).getDate()).getTime();
|
new Date(base).getDate()).getTime();
|
||||||
this.startMs = dayStart + 9 * 3600000;
|
const now = new Date();
|
||||||
this.endMs = dayStart + 10 * 3600000;
|
this.startMs = dayStart + now.getHours() * 3600000 + now.getMinutes() * 60000;
|
||||||
|
this.endMs = this.startMs + 3600000;
|
||||||
if (choices.length > 0) {
|
if (choices.length > 0) {
|
||||||
this.chosenKey = choices[0].calKey;
|
this.chosenKey = choices[0].calKey;
|
||||||
}
|
}
|
||||||
@@ -179,9 +183,9 @@ struct EventEditPage {
|
|||||||
// 全天日程的结束存为"当天 23:59:59.999"(排他日期前 1 毫秒)
|
// 全天日程的结束存为"当天 23:59:59.999"(排他日期前 1 毫秒)
|
||||||
this.endMs = this.allDay ? picked + 86399999 : picked;
|
this.endMs = this.allDay ? picked + 86399999 : picked;
|
||||||
} else {
|
} else {
|
||||||
const dur: number = Math.max(0, this.endMs - this.startMs);
|
// 设定开始时间后,结束时间自动跟随 = 开始 + 1 小时
|
||||||
this.startMs = picked;
|
this.startMs = picked;
|
||||||
this.endMs = this.allDay ? picked + 86399999 : picked + dur;
|
this.endMs = this.allDay ? picked + 86399999 : picked + 3600000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,7 +250,8 @@ struct EventEditPage {
|
|||||||
e.rrule = this.rruleForMode(this.repeatMode);
|
e.rrule = this.rruleForMode(this.repeatMode);
|
||||||
}
|
}
|
||||||
e.recurring = e.rrule !== '';
|
e.recurring = e.rrule !== '';
|
||||||
e.reminder = this.reminderMin;
|
e.reminders = EventDb.normalizeReminders(this.reminderList);
|
||||||
|
e.reminder = e.reminders.length > 0 ? e.reminders[0] : 0;
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
e.uid = `syncal-${Date.now()}-${Math.floor(Math.random() * 1000000)}`;
|
e.uid = `syncal-${Date.now()}-${Math.floor(Math.random() * 1000000)}`;
|
||||||
e.remotePath = encodeURIComponent(e.uid) + '.ics';
|
e.remotePath = encodeURIComponent(e.uid) + '.ics';
|
||||||
@@ -265,11 +270,18 @@ struct EventEditPage {
|
|||||||
} else {
|
} else {
|
||||||
await SyncEngine.settleLocalEvents(context);
|
await SyncEngine.settleLocalEvents(context);
|
||||||
}
|
}
|
||||||
|
LogUtil.write(`本地保存日程「${e.title}」提醒=${e.reminders.join('/')}分钟 重复=${e.rrule === '' ? '否' : e.rrule}`);
|
||||||
|
// 立即刷新提醒(不等下一轮同步),保证刚保存的提醒马上生效
|
||||||
|
try {
|
||||||
|
await ReminderService.refreshReminders(context as common.UIAbilityContext);
|
||||||
|
} catch (err) {
|
||||||
|
// 刷新失败不影响保存
|
||||||
|
}
|
||||||
this.getUIContext().getPromptAction().showToast({ message: '日程已保存' });
|
this.getUIContext().getPromptAction().showToast({ message: '日程已保存' });
|
||||||
router.back();
|
router.back();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const ex = err as BusinessError;
|
const ex = err as BusinessError;
|
||||||
console.error(`保存日程失败: ${ex.message}`);
|
LogUtil.write(`保存日程流程失败(数据已存本地待重试):${ex.message}`);
|
||||||
this.statusMsg = `保存失败:${ex.message}(已保存到本地,稍后同步会重试)`;
|
this.statusMsg = `保存失败:${ex.message}(已保存到本地,稍后同步会重试)`;
|
||||||
// 数据仍在本地且带 dirty 标记,不会丢
|
// 数据仍在本地且带 dirty 标记,不会丢
|
||||||
router.back();
|
router.back();
|
||||||
@@ -302,6 +314,13 @@ struct EventEditPage {
|
|||||||
} else {
|
} else {
|
||||||
await SyncEngine.settleLocalEvents(context);
|
await SyncEngine.settleLocalEvents(context);
|
||||||
}
|
}
|
||||||
|
LogUtil.write(`本地删除日程「${this.event?.title ?? ''}」`);
|
||||||
|
// 立即刷新提醒(取消已发布但日程已删的提醒)
|
||||||
|
try {
|
||||||
|
await ReminderService.refreshReminders(context as common.UIAbilityContext);
|
||||||
|
} catch (err) {
|
||||||
|
// 刷新失败不影响删除
|
||||||
|
}
|
||||||
this.getUIContext().getPromptAction().showToast({ message: '日程已删除' });
|
this.getUIContext().getPromptAction().showToast({ message: '日程已删除' });
|
||||||
router.back();
|
router.back();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -407,7 +426,7 @@ struct EventEditPage {
|
|||||||
|
|
||||||
// 提醒
|
// 提醒
|
||||||
Column({ space: 8 }) {
|
Column({ space: 8 }) {
|
||||||
Text('提醒')
|
Text('提醒(可多选)')
|
||||||
.fontSize(14)
|
.fontSize(14)
|
||||||
.fontColor($r('app.color.text_secondary'))
|
.fontColor($r('app.color.text_secondary'))
|
||||||
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
|
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
|
||||||
@@ -541,7 +560,8 @@ struct EventEditPage {
|
|||||||
.width('100%')
|
.width('100%')
|
||||||
.padding({ left: 8, right: 8, top: 10, bottom: 10 })
|
.padding({ left: 8, right: 8, top: 10, bottom: 10 })
|
||||||
.onClick(() => {
|
.onClick(() => {
|
||||||
this.openPicker(isStart);
|
// 注意:timeRow 的参数是 isStart,openPicker 的参数是 isEnd,语义相反必须取反
|
||||||
|
this.openPicker(!isStart);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -560,18 +580,42 @@ struct EventEditPage {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 提醒 chip 是否选中:0 = 不提醒(列表为空时选中);其余按是否已选 */
|
||||||
|
private chipOn(minutes: number): boolean {
|
||||||
|
if (minutes === 0) {
|
||||||
|
return this.reminderList.length === 0;
|
||||||
|
}
|
||||||
|
return this.reminderList.includes(minutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 切换提醒:0 = 清空全部;其余多选切换 */
|
||||||
|
private toggleReminder(minutes: number): void {
|
||||||
|
if (minutes === 0) {
|
||||||
|
this.reminderList = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list: number[] = [...this.reminderList];
|
||||||
|
const idx: number = list.indexOf(minutes);
|
||||||
|
if (idx >= 0) {
|
||||||
|
list.splice(idx, 1);
|
||||||
|
} else {
|
||||||
|
list.push(minutes);
|
||||||
|
}
|
||||||
|
this.reminderList = list;
|
||||||
|
}
|
||||||
|
|
||||||
@Builder
|
@Builder
|
||||||
reminderChip(minutes: number) {
|
reminderChip(minutes: number) {
|
||||||
Text(this.reminderLabel(minutes))
|
Text(this.reminderLabel(minutes))
|
||||||
.fontSize(12)
|
.fontSize(12)
|
||||||
.fontColor(this.reminderMin === minutes
|
.fontColor(this.chipOn(minutes)
|
||||||
? $r('app.color.button_text') : $r('app.color.text_primary'))
|
? $r('app.color.button_text') : $r('app.color.text_primary'))
|
||||||
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
|
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
|
||||||
.borderRadius(14)
|
.borderRadius(14)
|
||||||
.margin({ right: 8, bottom: 8 })
|
.margin({ right: 8, bottom: 8 })
|
||||||
.backgroundColor(this.reminderMin === minutes ? $r('app.color.brand') : $r('app.color.chip_off_bg'))
|
.backgroundColor(this.chipOn(minutes) ? $r('app.color.brand') : $r('app.color.chip_off_bg'))
|
||||||
.onClick(() => {
|
.onClick(() => {
|
||||||
this.reminderMin = minutes;
|
this.toggleReminder(minutes);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -181,12 +181,18 @@ struct Index {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.autoSyncTimer = setInterval((): void => {
|
this.autoSyncTimer = setInterval((): void => {
|
||||||
|
// 应用内提醒模式(代理提醒配额为 0 的降级):每 15 秒检查到点的提醒
|
||||||
|
const tickCtx = this.getUIContext().getHostContext();
|
||||||
|
if (tickCtx !== undefined && !this.syncing) {
|
||||||
|
ReminderService.tickReminders(tickCtx as common.UIAbilityContext);
|
||||||
|
}
|
||||||
const minutes: number = AppStorage.get<number>('syncIntervalMinutes') ?? 1;
|
const minutes: number = AppStorage.get<number>('syncIntervalMinutes') ?? 1;
|
||||||
if (Date.now() - this.lastSyncTime < minutes * 60000) {
|
if (Date.now() - this.lastSyncTime < minutes * 60000) {
|
||||||
return; // 未到设置的同步间隔
|
return; // 未到设置的同步间隔
|
||||||
}
|
}
|
||||||
if (!this.syncing && this.accounts.length > 0) {
|
if (!this.syncing && this.accounts.length > 0) {
|
||||||
this.lastSyncTime = Date.now();
|
this.lastSyncTime = Date.now();
|
||||||
|
LogUtil.write(`自动同步触发(间隔 ${minutes} 分钟,账号 ${this.accounts.length} 个)`);
|
||||||
this.syncAll(false);
|
this.syncAll(false);
|
||||||
}
|
}
|
||||||
}, 15000);
|
}, 15000);
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
// 设置页:系统日历模式(仅显示/备份到 CalDAV)+ 混合显示开关 + 自动同步间隔 + 后台同步
|
// 设置页:系统日历模式(仅显示/备份到 CalDAV)+ 混合显示开关 + 自动同步间隔 + 后台同步
|
||||||
import { router } from '@kit.ArkUI';
|
import { router } from '@kit.ArkUI';
|
||||||
import { common } from '@kit.AbilityKit';
|
import { common } from '@kit.AbilityKit';
|
||||||
|
import { pasteboard } from '@kit.BasicServicesKit';
|
||||||
|
import { notificationManager } from '@kit.NotificationKit';
|
||||||
import { AppSettings } from '../common/AppSettings';
|
import { AppSettings } from '../common/AppSettings';
|
||||||
import { BackgroundSyncService } from '../common/BackgroundSyncService';
|
import { BackgroundSyncService } from '../common/BackgroundSyncService';
|
||||||
import { AccountStore, DavAccount } from '../common/AccountStore';
|
import { AccountStore, DavAccount } from '../common/AccountStore';
|
||||||
@@ -29,6 +31,10 @@ struct SettingsPage {
|
|||||||
@State allBooks: BackupTarget[] = []; // 全部 DAV 日历本(只读标记管理用)
|
@State allBooks: BackupTarget[] = []; // 全部 DAV 日历本(只读标记管理用)
|
||||||
@State manualKeys: string[] = []; // 手动标记只读的 calKey
|
@State manualKeys: string[] = []; // 手动标记只读的 calKey
|
||||||
@State mutedKeys: string[] = []; // 提醒静音的 calKey
|
@State mutedKeys: string[] = []; // 提醒静音的 calKey
|
||||||
|
@State showLogSheet: boolean = false; // 同步日志查看弹层
|
||||||
|
@State logText: string = '';
|
||||||
|
@State notifyEnabled: boolean = true; // 通知权限状态(提醒依赖)
|
||||||
|
@State lastTestResult: string = '';
|
||||||
private context?: common.Context;
|
private context?: common.Context;
|
||||||
|
|
||||||
aboutToAppear(): void {
|
aboutToAppear(): void {
|
||||||
@@ -56,6 +62,13 @@ struct SettingsPage {
|
|||||||
AppSettings.getMutedReminderKeys(ctx).then((v: string[]): void => {
|
AppSettings.getMutedReminderKeys(ctx).then((v: string[]): void => {
|
||||||
this.mutedKeys = v;
|
this.mutedKeys = v;
|
||||||
});
|
});
|
||||||
|
notificationManager.isNotificationEnabled()
|
||||||
|
.then((v: boolean): void => {
|
||||||
|
this.notifyEnabled = v;
|
||||||
|
})
|
||||||
|
.catch((): void => {
|
||||||
|
this.notifyEnabled = false;
|
||||||
|
});
|
||||||
this.loadBackupSettings();
|
this.loadBackupSettings();
|
||||||
this.loadAllBooks();
|
this.loadAllBooks();
|
||||||
}
|
}
|
||||||
@@ -274,6 +287,65 @@ struct SettingsPage {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 打开同步日志查看弹层 */
|
||||||
|
private openLogSheet(): void {
|
||||||
|
if (this.context === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.logText = LogUtil.readAll(this.context);
|
||||||
|
this.showLogSheet = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 复制全部日志到剪贴板(方便粘贴给我诊断) */
|
||||||
|
private copyLog(): void {
|
||||||
|
if (this.context === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const data: pasteboard.PasteData = pasteboard.createData(
|
||||||
|
pasteboard.MIMETYPE_TEXT_PLAIN, LogUtil.readAll(this.context));
|
||||||
|
pasteboard.getSystemPasteboard().setData(data)
|
||||||
|
.then((): void => {
|
||||||
|
this.getUIContext().getPromptAction().showToast({ message: '日志已复制到剪贴板' });
|
||||||
|
})
|
||||||
|
.catch((): void => {
|
||||||
|
this.getUIContext().getPromptAction().showToast({ message: '复制失败' });
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.getUIContext().getPromptAction().showToast({ message: '复制失败' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearLog(): void {
|
||||||
|
if (this.context === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LogUtil.clear(this.context);
|
||||||
|
this.logText = '(日志已清空)';
|
||||||
|
this.getUIContext().getPromptAction().showToast({ message: '日志已清空' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 发布 1 分钟后的测试提醒,验证提醒链路 */
|
||||||
|
private async testReminder(): Promise<void> {
|
||||||
|
if (this.context === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.lastTestResult = await ReminderService.publishTestReminder(
|
||||||
|
this.context as common.UIAbilityContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按当前数据立即重建提醒,显示发布条数 */
|
||||||
|
private async refreshNow(): Promise<void> {
|
||||||
|
if (this.context === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const n: number = await ReminderService.refreshReminders(
|
||||||
|
this.context as common.UIAbilityContext);
|
||||||
|
this.getUIContext().getPromptAction().showToast({
|
||||||
|
message: n > 0 ? `已发布 ${n} 个提醒` : '没有发布任何提醒(可能都过了提醒时间、被静音或通知未开启,详见日志)'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
Column() {
|
Column() {
|
||||||
// 顶部
|
// 顶部
|
||||||
@@ -581,6 +653,85 @@ struct SettingsPage {
|
|||||||
.borderRadius(12)
|
.borderRadius(12)
|
||||||
.backgroundColor($r('app.color.card_bg'))
|
.backgroundColor($r('app.color.card_bg'))
|
||||||
.border({ width: 1, color: $r('app.color.shadow_color') })
|
.border({ width: 1, color: $r('app.color.shadow_color') })
|
||||||
|
// 提醒诊断:通知权限状态 + 测试提醒 + 立即刷新
|
||||||
|
Column({ space: 8 }) {
|
||||||
|
Row({ space: 10 }) {
|
||||||
|
Column({ space: 2 }) {
|
||||||
|
Row({ space: 6 }) {
|
||||||
|
Text('提醒诊断')
|
||||||
|
.fontSize(15)
|
||||||
|
.fontWeight(FontWeight.Medium)
|
||||||
|
.fontColor($r('app.color.text_primary'))
|
||||||
|
Text(this.notifyEnabled ? '通知已开启' : '通知未开启!')
|
||||||
|
.fontSize(11)
|
||||||
|
.fontColor(this.notifyEnabled ? $r('app.color.success') : $r('app.color.error'))
|
||||||
|
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
|
||||||
|
.borderRadius(10)
|
||||||
|
.backgroundColor(this.notifyEnabled ? $r('app.color.success_bg') : $r('app.color.error_bg'))
|
||||||
|
}
|
||||||
|
Text(this.notifyEnabled
|
||||||
|
? '点"测试提醒"验证链路:1 分钟后应收到通知'
|
||||||
|
: '系统设置 → 通知管理 → 找到本应用 → 允许通知,日程提醒才能弹出')
|
||||||
|
.fontSize(12)
|
||||||
|
.fontColor($r('app.color.text_secondary'))
|
||||||
|
}
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
.layoutWeight(1)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
Row({ space: 10 }) {
|
||||||
|
Button('测试提醒')
|
||||||
|
.fontSize(13)
|
||||||
|
.backgroundColor($r('app.color.brand'))
|
||||||
|
.onClick(() => {
|
||||||
|
this.testReminder();
|
||||||
|
})
|
||||||
|
Button('立即刷新提醒')
|
||||||
|
.fontSize(13)
|
||||||
|
.backgroundColor($r('app.color.text_secondary'))
|
||||||
|
.onClick(() => {
|
||||||
|
this.refreshNow();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.lastTestResult !== '') {
|
||||||
|
Text(this.lastTestResult)
|
||||||
|
.fontSize(12)
|
||||||
|
.fontColor($r('app.color.text_secondary'))
|
||||||
|
.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') })
|
||||||
|
|
||||||
|
// 同步日志:查看/复制/清空(真机测试诊断用)
|
||||||
|
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)
|
||||||
|
Button('查看')
|
||||||
|
.fontSize(13)
|
||||||
|
.backgroundColor($r('app.color.brand'))
|
||||||
|
.onClick(() => {
|
||||||
|
this.openLogSheet();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.padding(14)
|
||||||
|
.borderRadius(12)
|
||||||
|
.backgroundColor($r('app.color.card_bg'))
|
||||||
|
.border({ width: 1, color: $r('app.color.shadow_color') })
|
||||||
}
|
}
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.padding({ left: 20, right: 20, top: 8, bottom: 20 })
|
.padding({ left: 20, right: 20, top: 8, bottom: 20 })
|
||||||
@@ -588,9 +739,53 @@ struct SettingsPage {
|
|||||||
.layoutWeight(1)
|
.layoutWeight(1)
|
||||||
.align(Alignment.Top)
|
.align(Alignment.Top)
|
||||||
.scrollBar(BarState.Off)
|
.scrollBar(BarState.Off)
|
||||||
|
.bindSheet($$this.showLogSheet, this.logSheetBuilder(), {
|
||||||
|
height: 560,
|
||||||
|
dragBar: true,
|
||||||
|
title: { title: '同步日志' }
|
||||||
|
})
|
||||||
}
|
}
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.height('100%')
|
.height('100%')
|
||||||
.backgroundColor($r('app.color.page_bg'))
|
.backgroundColor($r('app.color.page_bg'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Builder
|
||||||
|
logSheetBuilder() {
|
||||||
|
Column({ space: 10 }) {
|
||||||
|
Row({ space: 10 }) {
|
||||||
|
Button('复制全部')
|
||||||
|
.fontSize(13)
|
||||||
|
.backgroundColor($r('app.color.brand'))
|
||||||
|
.onClick(() => {
|
||||||
|
this.copyLog();
|
||||||
|
})
|
||||||
|
Button('清空')
|
||||||
|
.fontSize(13)
|
||||||
|
.backgroundColor($r('app.color.error'))
|
||||||
|
.onClick(() => {
|
||||||
|
this.clearLog();
|
||||||
|
})
|
||||||
|
Button('刷新')
|
||||||
|
.fontSize(13)
|
||||||
|
.backgroundColor($r('app.color.text_secondary'))
|
||||||
|
.onClick(() => {
|
||||||
|
this.openLogSheet();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Scroll() {
|
||||||
|
Text(this.logText === '' ? '(日志为空)' : this.logText)
|
||||||
|
.fontSize(11)
|
||||||
|
.fontColor($r('app.color.text_primary'))
|
||||||
|
.width('100%')
|
||||||
|
}
|
||||||
|
.layoutWeight(1)
|
||||||
|
.align(Alignment.Top)
|
||||||
|
.scrollBar(BarState.On)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.padding(16)
|
||||||
|
.backgroundColor($r('app.color.page_bg'))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user