2026-09-13 15:50:37 +08:00
|
|
|
|
// entry/src/main/ets/pages/EventEditPage.ets
|
|
|
|
|
|
// 日程编辑页:新建 / 修改 / 删除本地(DAV 或本机)日程
|
|
|
|
|
|
import { router } from '@kit.ArkUI';
|
|
|
|
|
|
import { common } from '@kit.AbilityKit';
|
|
|
|
|
|
import { BusinessError } from '@kit.BasicServicesKit';
|
|
|
|
|
|
import { DavAccount, AccountStore, CalSource, BookPalette } from '../common/AccountStore';
|
|
|
|
|
|
import { EventDb, LocalEvent } from '../common/EventDb';
|
2026-09-13 20:25:18 +08:00
|
|
|
|
import { AppSettings } from '../common/AppSettings';
|
2026-09-13 15:50:37 +08:00
|
|
|
|
import { DavClient } from '../common/DavClient';
|
|
|
|
|
|
import { SyncEngine } from '../common/SyncEngine';
|
|
|
|
|
|
|
|
|
|
|
|
/** 可选的日历本 */
|
|
|
|
|
|
class BookChoice {
|
|
|
|
|
|
calKey: string = '';
|
|
|
|
|
|
href: string = '';
|
|
|
|
|
|
name: string = '';
|
|
|
|
|
|
color: string = '#007DFF';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Entry
|
|
|
|
|
|
@Component
|
|
|
|
|
|
struct EventEditPage {
|
|
|
|
|
|
@State title: string = '';
|
|
|
|
|
|
@State location: string = '';
|
|
|
|
|
|
@State description: string = '';
|
|
|
|
|
|
@State allDay: boolean = false;
|
|
|
|
|
|
@State startMs: number = 0;
|
|
|
|
|
|
@State endMs: number = 0;
|
|
|
|
|
|
@State books: BookChoice[] = [];
|
|
|
|
|
|
@State chosenKey: string = '';
|
|
|
|
|
|
@State isSaving: boolean = false;
|
|
|
|
|
|
@State statusMsg: string = '';
|
|
|
|
|
|
@State isExisting: boolean = false;
|
2026-09-13 20:25:18 +08:00
|
|
|
|
@State repeatMode: string = 'none'; // none|daily|workday|weekly|monthly|yearly|custom
|
|
|
|
|
|
@State reminderMin: number = 0; // 提前提醒分钟数,0 = 不提醒
|
|
|
|
|
|
@State pickerShow: boolean = false; // 开始/结束时间选择底部弹层(日期+时间一次选完)
|
|
|
|
|
|
@State pickerIsEnd: boolean = false;
|
|
|
|
|
|
@State pickDate: Date = new Date();
|
|
|
|
|
|
@State pickHour: number = 9;
|
|
|
|
|
|
@State pickMin: number = 0;
|
2026-09-13 15:50:37 +08:00
|
|
|
|
private event: LocalEvent | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
aboutToAppear(): Promise<void> {
|
|
|
|
|
|
return this.initPage();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async initPage(): Promise<void> {
|
|
|
|
|
|
const context = this.getUIContext().getHostContext();
|
|
|
|
|
|
if (context === undefined) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-09-13 20:25:18 +08:00
|
|
|
|
// 收集可写入的日历本(DAV 可写日历本 + 本机);只读日历本不出现在新建/编辑选择中
|
2026-09-13 15:50:37 +08:00
|
|
|
|
const sources: CalSourceWithHref[] = await CalendarDataBridge.loadWritableSources(context);
|
|
|
|
|
|
const choices: BookChoice[] = [];
|
|
|
|
|
|
for (const s of sources) {
|
|
|
|
|
|
const b = new BookChoice();
|
|
|
|
|
|
b.calKey = s.calKey;
|
|
|
|
|
|
b.href = s.href;
|
|
|
|
|
|
b.name = s.name;
|
|
|
|
|
|
b.color = s.color;
|
|
|
|
|
|
choices.push(b);
|
|
|
|
|
|
}
|
|
|
|
|
|
this.books = choices;
|
|
|
|
|
|
|
|
|
|
|
|
// 编辑既有事件
|
|
|
|
|
|
const pendingId: number | undefined = AppStorage.get<number>('pendingEventId');
|
|
|
|
|
|
if (pendingId !== undefined && pendingId > 0) {
|
|
|
|
|
|
const loaded = await EventDb.getById(context, pendingId);
|
|
|
|
|
|
if (loaded !== null) {
|
|
|
|
|
|
this.event = loaded;
|
|
|
|
|
|
this.isExisting = true;
|
|
|
|
|
|
this.title = loaded.title;
|
|
|
|
|
|
this.location = loaded.location;
|
|
|
|
|
|
this.description = loaded.description;
|
|
|
|
|
|
this.allDay = loaded.isAllDay;
|
|
|
|
|
|
this.startMs = loaded.startTime;
|
|
|
|
|
|
this.endMs = loaded.endTime;
|
|
|
|
|
|
this.chosenKey = loaded.calKey;
|
2026-09-13 20:25:18 +08:00
|
|
|
|
this.repeatMode = loaded.rrule !== '' ? this.modeFromRrule(loaded.rrule) : 'none';
|
|
|
|
|
|
this.reminderMin = loaded.reminder;
|
2026-09-13 15:50:37 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 新建:默认时间 = 所选日期 9:00-10:00
|
|
|
|
|
|
const base: number = AppStorage.get<number>('pendingEventDate') ?? Date.now();
|
|
|
|
|
|
const dayStart = new Date(new Date(base).getFullYear(), new Date(base).getMonth(),
|
|
|
|
|
|
new Date(base).getDate()).getTime();
|
|
|
|
|
|
this.startMs = dayStart + 9 * 3600000;
|
|
|
|
|
|
this.endMs = dayStart + 10 * 3600000;
|
|
|
|
|
|
if (choices.length > 0) {
|
|
|
|
|
|
this.chosenKey = choices[0].calKey;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private chosenBook(): BookChoice | null {
|
|
|
|
|
|
return this.books.find((b: BookChoice): boolean => b.calKey === this.chosenKey) ?? null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private fmtDate(ms: number): string {
|
|
|
|
|
|
const d = new Date(ms);
|
|
|
|
|
|
const p = (n: number): string => n < 10 ? '0' + n : String(n);
|
|
|
|
|
|
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private fmtTime(ms: number): string {
|
|
|
|
|
|
const d = new Date(ms);
|
|
|
|
|
|
const p = (n: number): string => n < 10 ? '0' + n : String(n);
|
|
|
|
|
|
return `${p(d.getHours())}:${p(d.getMinutes())}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-13 20:25:18 +08:00
|
|
|
|
/** 重复模式 → RRULE 字符串(RruleUtil 已支持这些规则) */
|
|
|
|
|
|
private rruleForMode(mode: string): string {
|
|
|
|
|
|
if (mode === 'daily') {
|
|
|
|
|
|
return 'FREQ=DAILY';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (mode === 'workday') {
|
|
|
|
|
|
return 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (mode === 'weekly') {
|
|
|
|
|
|
return 'FREQ=WEEKLY';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (mode === 'monthly') {
|
|
|
|
|
|
return 'FREQ=MONTHLY';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (mode === 'yearly') {
|
|
|
|
|
|
return 'FREQ=YEARLY';
|
|
|
|
|
|
}
|
|
|
|
|
|
return '';
|
2026-09-13 15:50:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-13 20:25:18 +08:00
|
|
|
|
/** RRULE → 重复模式(无法识别的规则归为 custom,保存时保留原规则) */
|
|
|
|
|
|
private modeFromRrule(rrule: string): string {
|
|
|
|
|
|
const u: string = rrule.toUpperCase();
|
|
|
|
|
|
if (u.includes('FREQ=DAILY')) {
|
|
|
|
|
|
return 'daily';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (u.includes('FREQ=WEEKLY')) {
|
|
|
|
|
|
const hasWeekday: boolean = u.includes('MO') && u.includes('TU') && u.includes('WE')
|
|
|
|
|
|
&& u.includes('TH') && u.includes('FR');
|
|
|
|
|
|
return hasWeekday ? 'workday' : 'weekly';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (u.includes('FREQ=MONTHLY')) {
|
|
|
|
|
|
return 'monthly';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (u.includes('FREQ=YEARLY')) {
|
|
|
|
|
|
return 'yearly';
|
|
|
|
|
|
}
|
|
|
|
|
|
return 'custom';
|
2026-09-13 15:50:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-13 20:25:18 +08:00
|
|
|
|
private reminderLabel(m: number): string {
|
|
|
|
|
|
if (m === 0) {
|
|
|
|
|
|
return '不提醒';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (m < 60) {
|
|
|
|
|
|
return `提前${m}分钟`;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (m < 1440) {
|
|
|
|
|
|
return `提前${m / 60}小时`;
|
|
|
|
|
|
}
|
|
|
|
|
|
return `提前${m / 1440}天`;
|
2026-09-13 15:50:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-13 20:25:18 +08:00
|
|
|
|
/** 打开时间选择弹层:日期 + 时间一次选完 */
|
|
|
|
|
|
private openPicker(isEnd: boolean): void {
|
|
|
|
|
|
this.pickerIsEnd = isEnd;
|
|
|
|
|
|
const base = new Date(isEnd ? this.endMs : this.startMs);
|
|
|
|
|
|
this.pickDate = new Date(base.getFullYear(), base.getMonth(), base.getDate());
|
|
|
|
|
|
this.pickHour = base.getHours();
|
|
|
|
|
|
this.pickMin = base.getMinutes();
|
|
|
|
|
|
this.pickerShow = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private applyPicker(): void {
|
|
|
|
|
|
const d = this.pickDate;
|
|
|
|
|
|
const picked: number = new Date(d.getFullYear(), d.getMonth(), d.getDate(),
|
|
|
|
|
|
this.pickHour, this.pickMin).getTime();
|
|
|
|
|
|
if (this.pickerIsEnd) {
|
|
|
|
|
|
// 全天日程的结束存为"当天 23:59:59.999"(排他日期前 1 毫秒)
|
|
|
|
|
|
this.endMs = this.allDay ? picked + 86399999 : picked;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const dur: number = Math.max(0, this.endMs - this.startMs);
|
|
|
|
|
|
this.startMs = picked;
|
|
|
|
|
|
this.endMs = this.allDay ? picked + 86399999 : picked + dur;
|
|
|
|
|
|
}
|
2026-09-13 15:50:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private toggleAllDay(): void {
|
|
|
|
|
|
this.allDay = !this.allDay;
|
|
|
|
|
|
if (this.allDay) {
|
|
|
|
|
|
const s = new Date(this.startMs);
|
|
|
|
|
|
const dayStart: number = new Date(s.getFullYear(), s.getMonth(), s.getDate()).getTime();
|
|
|
|
|
|
this.startMs = dayStart;
|
|
|
|
|
|
this.endMs = dayStart + 86399999;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const s = new Date(this.startMs);
|
|
|
|
|
|
this.startMs = s.getTime() + 9 * 3600000;
|
|
|
|
|
|
this.endMs = this.startMs + 3600000;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private validate(): boolean {
|
|
|
|
|
|
if (this.title.trim() === '') {
|
|
|
|
|
|
this.statusMsg = '请输入日程标题';
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.endMs < this.startMs) {
|
|
|
|
|
|
this.statusMsg = '结束时间不能早于开始时间';
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async save(): Promise<void> {
|
|
|
|
|
|
if (this.isSaving || !this.validate()) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-09-13 20:25:18 +08:00
|
|
|
|
// 仅阻止"单次覆盖实例"(RECURRENCE-ID)的修改;重复主事件(含 RRULE)允许编辑
|
|
|
|
|
|
if (this.event !== null && this.event.recurring && this.event.rrule === '') {
|
|
|
|
|
|
this.statusMsg = '重复日程的单次修改暂不支持,请到服务器端调整重复规则';
|
2026-09-13 15:50:37 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.isSaving = true;
|
|
|
|
|
|
this.statusMsg = '';
|
|
|
|
|
|
try {
|
|
|
|
|
|
const context = this.getUIContext().getHostContext();
|
|
|
|
|
|
if (context === undefined) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const book = this.chosenBook();
|
|
|
|
|
|
const e = this.event ?? new LocalEvent();
|
|
|
|
|
|
const isNew: boolean = this.event === null;
|
|
|
|
|
|
e.title = this.title.trim();
|
|
|
|
|
|
e.location = this.location.trim();
|
|
|
|
|
|
e.description = this.description.trim();
|
|
|
|
|
|
e.startTime = this.startMs;
|
|
|
|
|
|
e.endTime = this.allDay ? this.startMs + 86399999 : this.endMs;
|
|
|
|
|
|
e.isAllDay = this.allDay;
|
|
|
|
|
|
e.calKey = book !== null ? book.calKey : 'local';
|
|
|
|
|
|
e.href = book !== null ? book.href : '';
|
2026-09-13 20:25:18 +08:00
|
|
|
|
// 重复规则与提醒
|
|
|
|
|
|
if (this.repeatMode === 'custom') {
|
|
|
|
|
|
// 无法识别的既有规则原样保留
|
|
|
|
|
|
e.rrule = this.event !== null ? this.event.rrule : '';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
e.rrule = this.rruleForMode(this.repeatMode);
|
|
|
|
|
|
}
|
|
|
|
|
|
e.recurring = e.rrule !== '';
|
|
|
|
|
|
e.reminder = this.reminderMin;
|
2026-09-13 15:50:37 +08:00
|
|
|
|
if (isNew) {
|
|
|
|
|
|
e.uid = `syncal-${Date.now()}-${Math.floor(Math.random() * 1000000)}`;
|
|
|
|
|
|
e.remotePath = encodeURIComponent(e.uid) + '.ics';
|
|
|
|
|
|
await EventDb.insertLocal(context, e);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await EventDb.updateLocal(context, e);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 立即推送(尽力而为,失败不打断,下次同步会再推)
|
|
|
|
|
|
if (e.href !== '') {
|
|
|
|
|
|
const accounts: DavAccount[] = await AccountStore.loadAll(context);
|
|
|
|
|
|
const acc = accounts.find((a: DavAccount): boolean => a.calendarHrefs.includes(e.href));
|
|
|
|
|
|
if (acc !== undefined) {
|
|
|
|
|
|
const auth: string = DavClient.authHeader(acc.username, acc.password);
|
|
|
|
|
|
await SyncEngine.pushDirtyForAccount(context, acc, auth);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await SyncEngine.settleLocalEvents(context);
|
|
|
|
|
|
}
|
|
|
|
|
|
this.getUIContext().getPromptAction().showToast({ message: '日程已保存' });
|
|
|
|
|
|
router.back();
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
const ex = err as BusinessError;
|
|
|
|
|
|
console.error(`保存日程失败: ${ex.message}`);
|
|
|
|
|
|
this.statusMsg = `保存失败:${ex.message}(已保存到本地,稍后同步会重试)`;
|
|
|
|
|
|
// 数据仍在本地且带 dirty 标记,不会丢
|
|
|
|
|
|
router.back();
|
|
|
|
|
|
}
|
|
|
|
|
|
this.isSaving = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async removeEvent(): Promise<void> {
|
|
|
|
|
|
if (this.event === null || this.isSaving) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-09-13 20:25:18 +08:00
|
|
|
|
if (this.event !== null && this.event.recurring && this.event.rrule === '') {
|
|
|
|
|
|
this.statusMsg = '重复日程的单次删除暂不支持,请到服务器端调整重复规则';
|
2026-09-13 15:50:37 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.isSaving = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const context = this.getUIContext().getHostContext();
|
|
|
|
|
|
if (context === undefined) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
await EventDb.markDeleted(context, this.event.id);
|
|
|
|
|
|
if (this.event.href !== '') {
|
|
|
|
|
|
const accounts: DavAccount[] = await AccountStore.loadAll(context);
|
|
|
|
|
|
const acc = accounts.find((a: DavAccount): boolean => a.calendarHrefs.includes(this.event?.href ?? ''));
|
|
|
|
|
|
if (acc !== undefined) {
|
|
|
|
|
|
const auth: string = DavClient.authHeader(acc.username, acc.password);
|
|
|
|
|
|
await SyncEngine.pushDirtyForAccount(context, acc, auth);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await SyncEngine.settleLocalEvents(context);
|
|
|
|
|
|
}
|
|
|
|
|
|
this.getUIContext().getPromptAction().showToast({ message: '日程已删除' });
|
|
|
|
|
|
router.back();
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
const ex = err as BusinessError;
|
|
|
|
|
|
this.statusMsg = `删除失败:${ex.message}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.isSaving = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
build() {
|
|
|
|
|
|
Column({ space: 14 }) {
|
2026-09-13 20:25:18 +08:00
|
|
|
|
// 顶部(与内容区左右对齐,避免贴边误触)
|
2026-09-13 15:50:37 +08:00
|
|
|
|
Row({ space: 6 }) {
|
|
|
|
|
|
Text('取消')
|
|
|
|
|
|
.fontSize(16)
|
|
|
|
|
|
.fontColor($r('app.color.brand'))
|
2026-09-13 20:25:18 +08:00
|
|
|
|
.padding({ left: 4, right: 4, top: 8, bottom: 8 })
|
2026-09-13 15:50:37 +08:00
|
|
|
|
.onClick(() => {
|
|
|
|
|
|
router.back();
|
|
|
|
|
|
})
|
|
|
|
|
|
Blank()
|
|
|
|
|
|
Text(this.isExisting ? '编辑日程' : '新建日程')
|
|
|
|
|
|
.fontSize(18)
|
|
|
|
|
|
.fontWeight(FontWeight.Bold)
|
|
|
|
|
|
.fontColor($r('app.color.text_primary'))
|
|
|
|
|
|
Blank()
|
|
|
|
|
|
Text('保存')
|
|
|
|
|
|
.fontSize(16)
|
|
|
|
|
|
.fontColor(this.isSaving ? $r('app.color.text_hint') : $r('app.color.brand'))
|
|
|
|
|
|
.fontWeight(FontWeight.Medium)
|
2026-09-13 20:25:18 +08:00
|
|
|
|
.padding({ left: 4, right: 4, top: 8, bottom: 8 })
|
2026-09-13 15:50:37 +08:00
|
|
|
|
.onClick(() => {
|
|
|
|
|
|
this.save();
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
2026-09-13 20:25:18 +08:00
|
|
|
|
.padding({ left: 16, right: 16 })
|
2026-09-13 15:50:37 +08:00
|
|
|
|
|
|
|
|
|
|
Scroll() {
|
|
|
|
|
|
Column({ space: 14 }) {
|
|
|
|
|
|
// 标题
|
|
|
|
|
|
TextInput({ text: this.title, placeholder: '标题' })
|
|
|
|
|
|
.height(46)
|
|
|
|
|
|
.fontSize(16)
|
|
|
|
|
|
.backgroundColor($r('app.color.card_bg'))
|
|
|
|
|
|
.borderRadius(12)
|
|
|
|
|
|
.onChange((v: string) => {
|
|
|
|
|
|
this.title = v;
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 全天
|
|
|
|
|
|
Row() {
|
|
|
|
|
|
Text('全天')
|
|
|
|
|
|
.fontSize(15)
|
|
|
|
|
|
.fontColor($r('app.color.text_primary'))
|
|
|
|
|
|
Blank()
|
|
|
|
|
|
Toggle({ type: ToggleType.Switch, isOn: this.allDay })
|
|
|
|
|
|
.selectedColor($r('app.color.brand'))
|
|
|
|
|
|
.onChange(() => {
|
|
|
|
|
|
this.toggleAllDay();
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding(14)
|
|
|
|
|
|
.borderRadius(12)
|
|
|
|
|
|
.backgroundColor($r('app.color.card_bg'))
|
|
|
|
|
|
|
2026-09-13 20:25:18 +08:00
|
|
|
|
// 时间卡片(单击任一行:底部弹层中日期+时间一次选完)
|
2026-09-13 15:50:37 +08:00
|
|
|
|
Column({ space: 10 }) {
|
|
|
|
|
|
this.timeRow('开始', true)
|
|
|
|
|
|
Divider().color($r('app.color.shadow_color'))
|
|
|
|
|
|
this.timeRow('结束', false)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding(6)
|
|
|
|
|
|
.borderRadius(12)
|
|
|
|
|
|
.backgroundColor($r('app.color.card_bg'))
|
|
|
|
|
|
|
2026-09-13 20:25:18 +08:00
|
|
|
|
// 重复
|
|
|
|
|
|
Column({ space: 8 }) {
|
|
|
|
|
|
Text('重复')
|
|
|
|
|
|
.fontSize(14)
|
|
|
|
|
|
.fontColor($r('app.color.text_secondary'))
|
|
|
|
|
|
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
|
|
|
|
|
|
this.repeatChip('none', '不重复')
|
|
|
|
|
|
this.repeatChip('daily', '每天')
|
|
|
|
|
|
this.repeatChip('workday', '工作日')
|
|
|
|
|
|
this.repeatChip('weekly', '每周')
|
|
|
|
|
|
this.repeatChip('monthly', '每月')
|
|
|
|
|
|
this.repeatChip('yearly', '每年')
|
|
|
|
|
|
if (this.repeatMode === 'custom') {
|
|
|
|
|
|
// 服务器来的复杂规则(INTERVAL/COUNT 等)标记为自定义,保存时原样保留
|
|
|
|
|
|
this.repeatChip('custom', '自定义')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
}
|
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding(14)
|
|
|
|
|
|
.borderRadius(12)
|
|
|
|
|
|
.backgroundColor($r('app.color.card_bg'))
|
|
|
|
|
|
|
|
|
|
|
|
// 提醒
|
|
|
|
|
|
Column({ space: 8 }) {
|
|
|
|
|
|
Text('提醒')
|
|
|
|
|
|
.fontSize(14)
|
|
|
|
|
|
.fontColor($r('app.color.text_secondary'))
|
|
|
|
|
|
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
|
|
|
|
|
|
this.reminderChip(0)
|
|
|
|
|
|
this.reminderChip(5)
|
|
|
|
|
|
this.reminderChip(10)
|
|
|
|
|
|
this.reminderChip(15)
|
|
|
|
|
|
this.reminderChip(30)
|
|
|
|
|
|
this.reminderChip(60)
|
|
|
|
|
|
this.reminderChip(1440)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
}
|
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding(14)
|
|
|
|
|
|
.borderRadius(12)
|
|
|
|
|
|
.backgroundColor($r('app.color.card_bg'))
|
|
|
|
|
|
|
2026-09-13 15:50:37 +08:00
|
|
|
|
// 日历本选择
|
|
|
|
|
|
Column({ space: 8 }) {
|
|
|
|
|
|
Text('日历本')
|
|
|
|
|
|
.fontSize(14)
|
|
|
|
|
|
.fontColor($r('app.color.text_secondary'))
|
|
|
|
|
|
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
|
|
|
|
|
|
ForEach(this.books, (b: BookChoice) => {
|
|
|
|
|
|
Row({ space: 5 }) {
|
|
|
|
|
|
Circle().width(8).height(8).fill(b.color)
|
|
|
|
|
|
Text(b.name)
|
|
|
|
|
|
.fontSize(12)
|
|
|
|
|
|
.fontColor(this.chosenKey === b.calKey
|
|
|
|
|
|
? $r('app.color.button_text') : $r('app.color.text_primary'))
|
|
|
|
|
|
}
|
|
|
|
|
|
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
|
|
|
|
|
|
.borderRadius(14)
|
|
|
|
|
|
.margin({ right: 8, bottom: 8 })
|
|
|
|
|
|
.backgroundColor(this.chosenKey === b.calKey ? b.color : $r('app.color.chip_off_bg'))
|
|
|
|
|
|
.onClick(() => {
|
|
|
|
|
|
this.chosenKey = b.calKey;
|
|
|
|
|
|
})
|
|
|
|
|
|
}, (b: BookChoice) => b.calKey)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
}
|
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding(14)
|
|
|
|
|
|
.borderRadius(12)
|
|
|
|
|
|
.backgroundColor($r('app.color.card_bg'))
|
|
|
|
|
|
|
|
|
|
|
|
// 地点
|
|
|
|
|
|
TextInput({ text: this.location, placeholder: '地点(可选)' })
|
|
|
|
|
|
.height(44)
|
|
|
|
|
|
.fontSize(14)
|
|
|
|
|
|
.backgroundColor($r('app.color.card_bg'))
|
|
|
|
|
|
.borderRadius(12)
|
|
|
|
|
|
.onChange((v: string) => {
|
|
|
|
|
|
this.location = v;
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 描述
|
|
|
|
|
|
TextArea({ text: this.description, placeholder: '备注(可选)' })
|
|
|
|
|
|
.height(90)
|
|
|
|
|
|
.fontSize(14)
|
|
|
|
|
|
.backgroundColor($r('app.color.card_bg'))
|
|
|
|
|
|
.borderRadius(12)
|
|
|
|
|
|
.onChange((v: string) => {
|
|
|
|
|
|
this.description = v;
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (this.statusMsg !== '') {
|
|
|
|
|
|
Text(this.statusMsg)
|
|
|
|
|
|
.fontSize(13)
|
|
|
|
|
|
.fontColor($r('app.color.error'))
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 删除
|
|
|
|
|
|
if (this.isExisting) {
|
|
|
|
|
|
Button('删除日程')
|
|
|
|
|
|
.fontSize(15)
|
|
|
|
|
|
.fontColor($r('app.color.error'))
|
|
|
|
|
|
.backgroundColor($r('app.color.error_bg'))
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height(44)
|
|
|
|
|
|
.borderRadius(12)
|
|
|
|
|
|
.enabled(!this.isSaving)
|
|
|
|
|
|
.onClick(() => {
|
|
|
|
|
|
this.removeEvent();
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ left: 20, right: 20, bottom: 30 })
|
|
|
|
|
|
.constraintSize({ minHeight: '100%' })
|
|
|
|
|
|
}
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
.scrollBar(BarState.Off)
|
|
|
|
|
|
.edgeEffect(EdgeEffect.Spring)
|
|
|
|
|
|
.align(Alignment.Top)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height('100%')
|
|
|
|
|
|
.padding({ top: 12 })
|
|
|
|
|
|
.backgroundColor($r('app.color.page_bg'))
|
2026-09-13 20:25:18 +08:00
|
|
|
|
.bindSheet($$this.pickerShow, this.dateTimePickerSheet(), {
|
|
|
|
|
|
height: 380,
|
|
|
|
|
|
showClose: false,
|
|
|
|
|
|
dragBar: true,
|
|
|
|
|
|
backgroundColor: $r('app.color.card_bg')
|
|
|
|
|
|
})
|
2026-09-13 15:50:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
timeRow(label: string, isStart: boolean) {
|
|
|
|
|
|
Row({ space: 8 }) {
|
|
|
|
|
|
Text(label)
|
|
|
|
|
|
.fontSize(14)
|
|
|
|
|
|
.fontColor($r('app.color.text_secondary'))
|
|
|
|
|
|
.width(36)
|
2026-09-13 20:25:18 +08:00
|
|
|
|
Text(this.allDay
|
|
|
|
|
|
? this.fmtDate(isStart ? this.startMs : this.endMs)
|
|
|
|
|
|
: `${this.fmtDate(isStart ? this.startMs : this.endMs)} ${this.fmtTime(isStart ? this.startMs : this.endMs)}`)
|
2026-09-13 15:50:37 +08:00
|
|
|
|
.fontSize(15)
|
|
|
|
|
|
.fontColor($r('app.color.brand'))
|
2026-09-13 20:25:18 +08:00
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
Text('›')
|
|
|
|
|
|
.fontSize(16)
|
|
|
|
|
|
.fontColor($r('app.color.text_hint'))
|
2026-09-13 15:50:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
2026-09-13 20:25:18 +08:00
|
|
|
|
.padding({ left: 8, right: 8, top: 10, bottom: 10 })
|
|
|
|
|
|
.onClick(() => {
|
|
|
|
|
|
this.openPicker(isStart);
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
repeatChip(mode: string, label: string) {
|
|
|
|
|
|
Text(label)
|
|
|
|
|
|
.fontSize(12)
|
|
|
|
|
|
.fontColor(this.repeatMode === mode
|
|
|
|
|
|
? $r('app.color.button_text') : $r('app.color.text_primary'))
|
|
|
|
|
|
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
|
|
|
|
|
|
.borderRadius(14)
|
|
|
|
|
|
.margin({ right: 8, bottom: 8 })
|
|
|
|
|
|
.backgroundColor(this.repeatMode === mode ? $r('app.color.brand') : $r('app.color.chip_off_bg'))
|
|
|
|
|
|
.onClick(() => {
|
|
|
|
|
|
this.repeatMode = mode;
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
reminderChip(minutes: number) {
|
|
|
|
|
|
Text(this.reminderLabel(minutes))
|
|
|
|
|
|
.fontSize(12)
|
|
|
|
|
|
.fontColor(this.reminderMin === minutes
|
|
|
|
|
|
? $r('app.color.button_text') : $r('app.color.text_primary'))
|
|
|
|
|
|
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
|
|
|
|
|
|
.borderRadius(14)
|
|
|
|
|
|
.margin({ right: 8, bottom: 8 })
|
|
|
|
|
|
.backgroundColor(this.reminderMin === minutes ? $r('app.color.brand') : $r('app.color.chip_off_bg'))
|
|
|
|
|
|
.onClick(() => {
|
|
|
|
|
|
this.reminderMin = minutes;
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 开始/结束时间选择弹层:左侧日期、右侧时间(全天时只有日期),一次确定 */
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
dateTimePickerSheet() {
|
|
|
|
|
|
Column({ space: 14 }) {
|
|
|
|
|
|
Text(this.pickerIsEnd ? '选择结束时间' : '选择开始时间')
|
|
|
|
|
|
.fontSize(16)
|
|
|
|
|
|
.fontWeight(FontWeight.Bold)
|
|
|
|
|
|
.fontColor($r('app.color.text_primary'))
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.textAlign(TextAlign.Center)
|
|
|
|
|
|
.padding({ top: 12 })
|
|
|
|
|
|
Row({ space: 6 }) {
|
|
|
|
|
|
DatePicker({
|
|
|
|
|
|
start: new Date(2000, 0, 1),
|
|
|
|
|
|
end: new Date(2050, 11, 31),
|
|
|
|
|
|
selected: this.pickDate
|
|
|
|
|
|
})
|
|
|
|
|
|
.onDateChange((value: Date) => {
|
|
|
|
|
|
this.pickDate = value;
|
|
|
|
|
|
})
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
.height(210)
|
|
|
|
|
|
if (!this.allDay) {
|
|
|
|
|
|
TimePicker({
|
|
|
|
|
|
selected: new Date(2000, 0, 1, this.pickHour, this.pickMin)
|
|
|
|
|
|
})
|
|
|
|
|
|
.onChange((value: TimePickerResult) => {
|
|
|
|
|
|
this.pickHour = value.hour;
|
|
|
|
|
|
this.pickMin = value.minute;
|
|
|
|
|
|
})
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
.height(210)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
|
|
|
|
|
|
Button(this.allDay ? '确定日期' : '确定')
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height(44)
|
|
|
|
|
|
.borderRadius(12)
|
|
|
|
|
|
.fontSize(15)
|
|
|
|
|
|
.backgroundColor($r('app.color.brand'))
|
|
|
|
|
|
.onClick(() => {
|
|
|
|
|
|
this.applyPicker();
|
|
|
|
|
|
this.pickerShow = false;
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ left: 16, right: 16, bottom: 24 })
|
2026-09-13 15:50:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 桥接:从账号存储拿可写来源(DAV 日历本 + 本机),附上 href */
|
|
|
|
|
|
class CalendarDataBridge {
|
|
|
|
|
|
static async loadWritableSources(context: common.Context): Promise<CalSourceWithHref[]> {
|
|
|
|
|
|
const result: CalSourceWithHref[] = [];
|
|
|
|
|
|
const accounts: DavAccount[] = await AccountStore.loadAll(context);
|
2026-09-13 20:25:18 +08:00
|
|
|
|
const manualKeys: string[] = await AppSettings.getManualReadonlyKeys(context);
|
2026-09-13 15:50:37 +08:00
|
|
|
|
for (const acc of accounts) {
|
|
|
|
|
|
for (let i = 0; i < acc.calendarHrefs.length; i++) {
|
2026-09-13 20:25:18 +08:00
|
|
|
|
// 只读日历本(服务器无写权限,或用户手动标记只读)不可选
|
|
|
|
|
|
const detected: boolean = acc.calendarWritable.length > i
|
|
|
|
|
|
? acc.calendarWritable[i] !== '0' : true;
|
|
|
|
|
|
if (!detected || manualKeys.includes(`${acc.id}_${i}`)) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-09-13 15:50:37 +08:00
|
|
|
|
const s = new CalSourceWithHref();
|
|
|
|
|
|
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}`;
|
|
|
|
|
|
let color: string = i < acc.calendarColors.length ? AccountStore.normalizeColor(acc.calendarColors[i]) : '';
|
|
|
|
|
|
s.color = color !== '' ? color : BookPalette.colorFor(i);
|
|
|
|
|
|
s.href = acc.calendarHrefs[i];
|
2026-09-13 20:25:18 +08:00
|
|
|
|
s.writable = true;
|
2026-09-13 15:50:37 +08:00
|
|
|
|
result.push(s);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
const local = new CalSourceWithHref();
|
|
|
|
|
|
local.calKey = 'local';
|
|
|
|
|
|
local.name = '本机(不同步)';
|
|
|
|
|
|
local.color = '#5A6068';
|
|
|
|
|
|
result.push(local);
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class CalSourceWithHref extends CalSource {
|
|
|
|
|
|
href: string = '';
|
|
|
|
|
|
}
|