重复日程:新增结束条件(到某天/共几次);修复仅这一次失效与标题被清空
新增能力: - 新建重复日程时可设"结束条件":永不结束 / 重复到某天 / 共重复几次 (之前只能选重复频率,无法指定次数或截止日期) - 编辑既有重复日程会**回显**服务器上已有的 COUNT / UNTIL 并可修改 修复三个真 bug: 1. ⭐ "仅这一次"失效(实际改了全部):重复日程在列表里展开后,所有次共用 主事件的 id,编辑页只拿到 id、拿不到"用户点的是哪一次" → occStartMs 被当成 主事件的第一次发生 → EXDATE 排除的是第一次、覆盖行写在第一次上, 用户当次既没排掉、系列属性又被动过,看起来就是"全改了"。 修法:Index 打开编辑页时用 pendingOccurrenceTs 传入被点那次的 startTime; seriesBoundaries 再把该时刻"吸附"到真实发生(容差 12h),确保 EXDATE 能匹配。 2. ⭐ 已有标题被删空:original 快照漏拷贝 title/description/location/reminders, restoreRowById 于是把主事件行还原成空标题。 修法:快照补全全部字段。 3. ⭐ setExdate/setRrule 被 restoreRowById 覆盖(顺序错误): 先写 EXDATE 再还原 → 快照里的旧 exdate 又把排除覆盖掉。 修法:一律"先还原主事件行、再写 EXDATE/截断 RRULE"。 其它: - 编辑页现在显示"被点那一次"的实际时间(此前显示的是主事件第一次的时间) - 'all' 分支改为把改动写到主事件行,整系列一起改 - 复用唯一底部弹层(新增 sheetMode='repeatEnd' 选截止日期),仍保持只挂 1 个 bindSheet
This commit is contained in:
@@ -44,6 +44,19 @@ struct EventEditPage {
|
||||
@State statusMsg: string = '';
|
||||
@State isExisting: boolean = false;
|
||||
@State repeatMode: string = 'none'; // none|daily|workday|weekly|monthly|yearly|custom
|
||||
/**
|
||||
* ⭐ 重复的"结束条件"(新建与编辑都可用):
|
||||
* never 永不结束
|
||||
* until 重复到某个日期为止(界面上选具体日期)
|
||||
* count 重复固定次数
|
||||
*/
|
||||
@State repeatEnd: string = 'never';
|
||||
@State repeatUntil: number = 0; // 截止日期(当天 0 点毫秒)
|
||||
@State repeatCount: number = 10; // 重复次数
|
||||
/** ⭐ 编辑时载入的原始 RRULE:用于判断"新建的结束条件是否会覆盖已有规则" */
|
||||
private originalRrule: string = '';
|
||||
/** ⭐ 发生时刻是否由调用方传入(true=用户点的是某一次,false=用户点的是主事件本身) */
|
||||
private occProvided: boolean = false;
|
||||
@State reminderList: number[] = []; // 提前提醒分钟数(多选),空 = 不提醒
|
||||
/**
|
||||
* ⭐ 底部弹层:**整个页面只能挂一个 `bindSheet`**。
|
||||
@@ -106,13 +119,18 @@ struct EventEditPage {
|
||||
const loaded = await EventDb.getById(context, pendingId);
|
||||
if (loaded !== null) {
|
||||
this.event = loaded;
|
||||
// ⭐ 原始行快照:series 范围计算必须用"载入时"的值(doSave 会改写 this.event)
|
||||
// ⭐ 原始行快照:series 范围计算必须用"载入时"的值(doSave 会改写 this.event)。
|
||||
// ⚠️ 必须是**完整**拷贝 —— 曾经漏了 title/description/location/reminders,
|
||||
// 导致"只改这一次"时把这一行还原成空标题(BUG:已有标题被删没)。
|
||||
const snap = new LocalEvent();
|
||||
snap.id = loaded.id;
|
||||
snap.uid = loaded.uid;
|
||||
snap.calKey = loaded.calKey;
|
||||
snap.href = loaded.href;
|
||||
snap.remotePath = loaded.remotePath;
|
||||
snap.title = loaded.title;
|
||||
snap.description = loaded.description;
|
||||
snap.location = loaded.location;
|
||||
snap.startTime = loaded.startTime;
|
||||
snap.endTime = loaded.endTime;
|
||||
snap.recurrenceId = loaded.recurrenceId;
|
||||
@@ -121,7 +139,10 @@ struct EventEditPage {
|
||||
snap.isAllDay = loaded.isAllDay;
|
||||
snap.recurring = loaded.recurring;
|
||||
snap.deleted = loaded.deleted;
|
||||
snap.reminder = loaded.reminder;
|
||||
snap.reminders = loaded.reminders.slice();
|
||||
this.original = snap;
|
||||
this.originalRrule = loaded.rrule;
|
||||
this.isExisting = true;
|
||||
this.title = loaded.title;
|
||||
this.location = loaded.location;
|
||||
@@ -152,10 +173,32 @@ struct EventEditPage {
|
||||
this.statusMsg = '这条日程原先只存在「本机」(不同步)。请选一个 CalDAV 日历本,保存后即会同步到服务器。';
|
||||
}
|
||||
this.repeatMode = loaded.rrule !== '' ? this.modeFromRrule(loaded.rrule) : 'none';
|
||||
this.parseRepeatEnd(loaded.rrule);
|
||||
this.reminderList = loaded.reminders.length > 0 ? loaded.reminders
|
||||
: (loaded.reminder > 0 ? [loaded.reminder] : []);
|
||||
// ⭐ 目标发生时刻:优先用 RECURRENCE-ID(覆盖行=被改写的原始时刻),否则用行本身开始时间
|
||||
this.occStartMs = loaded.recurrenceId > 0 ? loaded.recurrenceId : loaded.startTime;
|
||||
// ⭐ 目标发生时刻("仅这一次"要排除/覆盖的那一次):
|
||||
// ① 覆盖行 → 用 RECURRENCE-ID(原始发生时刻)
|
||||
// ② 调用方显式传入了"用户点的是哪一次" → 用它(列表里点开的某次发生,行的 id 是主事件 id,
|
||||
// 若不传,就会把"主事件的第一次发生"当成目标 → 表现出来就是"选仅这一次却改了全部")
|
||||
// ③ 兜底才用行本身的开始时间
|
||||
const occFromCaller: number | undefined = AppStorage.get<number>('pendingOccurrenceTs');
|
||||
if (loaded.recurrenceId > 0) {
|
||||
this.occStartMs = loaded.recurrenceId;
|
||||
} else if (occFromCaller !== undefined && occFromCaller > 0) {
|
||||
this.occStartMs = occFromCaller;
|
||||
this.occProvided = true;
|
||||
} else {
|
||||
this.occStartMs = loaded.startTime;
|
||||
}
|
||||
// ⭐ 用户点开的是"某一次发生"时,编辑页必须显示**那一次**的实际时间,
|
||||
// 而不是重复主事件第一次发生的时间(否则时间框显示错误,用户改时间也改错基准)。
|
||||
if (this.occProvided && this.occStartMs > 0) {
|
||||
const dur: number = Math.max(0, loaded.endTime - loaded.startTime);
|
||||
this.startMs = this.occStartMs;
|
||||
this.endMs = this.allDay ? this.occStartMs + 86399999 : this.occStartMs + dur;
|
||||
}
|
||||
// 消费后立即清掉,避免影响下一次新建/编辑
|
||||
AppStorage.setOrCreate<number>('pendingOccurrenceTs', 0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -242,6 +285,106 @@ struct EventEditPage {
|
||||
return 'custom';
|
||||
}
|
||||
|
||||
/** 从已有 RRULE 解析出"结束条件"(用于编辑时回显"重复到某天/共几次") */
|
||||
private parseRepeatEnd(rrule: string): void {
|
||||
if (rrule === '') {
|
||||
this.repeatEnd = 'never';
|
||||
return;
|
||||
}
|
||||
const u: string = rrule.toUpperCase();
|
||||
// COUNT=n
|
||||
const cm = /COUNT=(\d+)/.exec(u);
|
||||
if (cm !== null) {
|
||||
this.repeatEnd = 'count';
|
||||
this.repeatCount = Number(cm[1]) > 0 ? Number(cm[1]) : 10;
|
||||
return;
|
||||
}
|
||||
// UNTIL=yyyymmddThhmmssZ 或 yyyymmdd
|
||||
const um = /UNTIL=(\d{8})(T\d{6}Z?)?/.exec(u);
|
||||
if (um !== null) {
|
||||
this.repeatEnd = 'until';
|
||||
const y: number = Number(um[1].substring(0, 4));
|
||||
const mo: number = Number(um[1].substring(4, 6));
|
||||
const d: number = Number(um[1].substring(6, 8));
|
||||
this.repeatUntil = new Date(y, mo - 1, d).getTime();
|
||||
return;
|
||||
}
|
||||
this.repeatEnd = 'never';
|
||||
}
|
||||
|
||||
/** 把当前"重复模式 + 结束条件"合成 RRULE 字符串 */
|
||||
private buildRrule(): string {
|
||||
const base: string = this.rruleForMode(this.repeatMode);
|
||||
if (base === '' || this.repeatMode === 'custom') {
|
||||
return ''; // custom 由调用方保留原规则
|
||||
}
|
||||
if (this.repeatEnd === 'count') {
|
||||
return `${base};COUNT=${Math.max(1, Math.round(this.repeatCount))}`;
|
||||
}
|
||||
if (this.repeatEnd === 'until' && this.repeatUntil > 0) {
|
||||
// UNTIL 用"截止日当天 23:59:59"(本地时区),避免把当天那次排除掉
|
||||
const d = new Date(this.repeatUntil);
|
||||
const end: number = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59).getTime();
|
||||
return `${base};UNTIL=${IcsUtil.fmtIcsTime(end, false)}`;
|
||||
}
|
||||
return base; // 永不结束
|
||||
}
|
||||
|
||||
/**
|
||||
* ⭐ 编辑既有重复日程时:用户在界面上调整了"结束条件"才覆盖原 RRULE;
|
||||
* 没动过就原样保留(防止把服务器上的复杂规则简单化掉)。
|
||||
*/
|
||||
private effectiveRrule(): string {
|
||||
if (this.repeatMode === 'custom') {
|
||||
return this.originalRrule;
|
||||
}
|
||||
const built: string = this.buildRrule();
|
||||
if (this.originalRrule === '') {
|
||||
return built;
|
||||
}
|
||||
// 原有规则的 FREQ 与当前模式一致、且用户没有显式改结束条件 → 保留原规则
|
||||
const freqOf = (m: string): string => this.modeFromRrule(m);
|
||||
if (freqOf(this.originalRrule) === this.repeatMode
|
||||
&& this.repeatEndMatches(this.originalRrule)) {
|
||||
return this.originalRrule;
|
||||
}
|
||||
return built;
|
||||
}
|
||||
|
||||
/** 原 RRULE 的结束条件是否与界面当前选择一致 */
|
||||
private repeatEndMatches(rrule: string): boolean {
|
||||
const u: string = rrule.toUpperCase();
|
||||
if (this.repeatEnd === 'never') {
|
||||
return !u.includes('COUNT=') && !u.includes('UNTIL=');
|
||||
}
|
||||
if (this.repeatEnd === 'count') {
|
||||
const m = /COUNT=(\d+)/.exec(u);
|
||||
return m !== null && Number(m[1]) === Math.round(this.repeatCount);
|
||||
}
|
||||
if (this.repeatEnd === 'until') {
|
||||
const m = /UNTIL=(\d{8})/.exec(u);
|
||||
if (m === null) {
|
||||
return false;
|
||||
}
|
||||
const y = Number(m[1].substring(0, 4));
|
||||
const mo = Number(m[1].substring(4, 6));
|
||||
const d = Number(m[1].substring(6, 8));
|
||||
const a = new Date(y, mo - 1, d);
|
||||
const b = new Date(this.repeatUntil);
|
||||
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth()
|
||||
&& a.getDate() === b.getDate();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 打开"重复结束条件"日期选择弹层(复用唯一的底部弹层) */
|
||||
private openRepeatUntilPicker(): void {
|
||||
const base = this.repeatUntil > 0 ? new Date(this.repeatUntil) : new Date(this.startMs);
|
||||
this.pickDate = new Date(base.getFullYear(), base.getMonth(), base.getDate());
|
||||
this.sheetMode = 'repeatEnd';
|
||||
this.sheetShow = true;
|
||||
}
|
||||
|
||||
private reminderLabel(m: number): string {
|
||||
if (m === 0) {
|
||||
return '不提醒';
|
||||
@@ -336,13 +479,17 @@ struct EventEditPage {
|
||||
return src;
|
||||
}
|
||||
|
||||
/** 该次发生之前一次发生与之后一次发生的边界(用于 EXDATE / UNTIL / 拆分) */
|
||||
/** 该次发生之前一次发生与之后一次发生的边界(用于 EXDATE / UNTIL / 拆分)。
|
||||
* ⭐ 会用展开结果**校正 occStartMs**:调用方传来的时刻可能带秒/毫秒偏差
|
||||
* (列表里点开的那次发生时间是精确的,但主事件运行到卡片等路径可能有偏差),
|
||||
* 必须吸附到"真实存在的那一次",否则 EXDATE 写出的时刻匹配不上任何发生 →
|
||||
* 表现为"选了仅这一次,却把所有次都改了"。 */
|
||||
private async seriesBoundaries(context: common.Context): Promise<SeriesBounds> {
|
||||
const b = new SeriesBounds();
|
||||
const occ: number = this.occStartMs;
|
||||
let occ: number = this.occStartMs;
|
||||
b.thisOcc = occ;
|
||||
const master: LocalEvent | null = await this.loadMaster(context);
|
||||
if (master === null) {
|
||||
if (master === null || master.rrule === '') {
|
||||
b.beforeThis = occ - 1000;
|
||||
b.nextOcc = 0;
|
||||
return b;
|
||||
@@ -356,10 +503,30 @@ struct EventEditPage {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 展开窗口:主事件开始 ~ "该次"之后一年的发生,用于找"前后相邻"的两次
|
||||
const winEnd: number = occ + 366 * 86400000;
|
||||
// 展开窗口:主事件开始 ~ "该次"之后一年(覆盖主事件的首次发生)
|
||||
const winEnd: number = Math.max(occ, master.startTime) + 366 * 86400000;
|
||||
const occs: number[] = RruleUtil.expand(master.rrule, master.startTime,
|
||||
master.startTime, winEnd, exNums, 3000);
|
||||
// ⭐ 吸附:找与 occ 最接近的那一次真实发生(容差 12 小时,足以跨时区/跨日误差)
|
||||
let snapped: number = 0;
|
||||
let bestDiff: number = -1;
|
||||
for (const o of occs) {
|
||||
const d: number = Math.abs(o - occ);
|
||||
if (bestDiff < 0 || d < bestDiff) {
|
||||
bestDiff = d;
|
||||
snapped = o;
|
||||
}
|
||||
}
|
||||
if (snapped > 0 && bestDiff <= 12 * 3600000) {
|
||||
occ = snapped;
|
||||
b.thisOcc = snapped;
|
||||
this.occStartMs = snapped; // 回写,保证后续 EXDATE / 覆盖行用同一时刻
|
||||
} else if (!this.occProvided && master.startTime > 0) {
|
||||
// 没有可信的"点选时刻"时,退回主事件的首次发生(至少是真实存在的一次)
|
||||
occ = master.startTime;
|
||||
b.thisOcc = occ;
|
||||
this.occStartMs = occ;
|
||||
}
|
||||
let prev: number = 0;
|
||||
let next: number = 0;
|
||||
for (const o of occs) {
|
||||
@@ -390,6 +557,10 @@ struct EventEditPage {
|
||||
// ⚠️ 用**原始快照**判据:doSave 已把 e(= this.event)的 calKey/href/startTime 改成新值,
|
||||
// 直接用 this.event 会把"改后的日期/日历本"当成原系列,导致 EXDATE / UNTIL 算错。
|
||||
const cur: LocalEvent | null = this.original;
|
||||
// 用户点开的是"某一次发生"(occProvided=true)时,list/周视图里的行 id 是**主事件 id**,
|
||||
// 所以"改全部"也必须给用户明确的确认——但语义上就是改整条系列:
|
||||
// · 点开的是主事件行本身(occProvided=false)→ 直接改这一行就是改整个系列,无需特殊处理
|
||||
// · 点开的是某一次发生 且选了"全部" → 把改动写到主事件行上(整系列平移/改名)
|
||||
if (isNew || cur === null || !this.isSeriesOccurrence()) {
|
||||
if (isNew) {
|
||||
await EventDb.insertLocal(context, target);
|
||||
@@ -400,8 +571,24 @@ struct EventEditPage {
|
||||
}
|
||||
const bounds: SeriesBounds = await this.seriesBoundaries(context);
|
||||
if (scope === 'all') {
|
||||
// 全部:直接更新当前行。若当前行是某次覆盖实例 → 保留其 recurrenceId 改内容;
|
||||
// 若当前行是主事件 → 更新主事件(DTSTART 若被改 → 整系列平移,符合"修改全部"直觉)
|
||||
// 全部:改到**主事件行**上(整系列一起改)。
|
||||
// ⚠️ 不能直接 updateLocal(target):target 是"用户点开的那一行",
|
||||
// 当用户点的是某次发生、而该行其实是主事件行时没问题;
|
||||
// 但若这一行是覆盖实例(rrule=''),把整系列的属性写进覆盖行只会改到那一次。
|
||||
const masterForAll: LocalEvent | null = await this.loadMaster(context);
|
||||
if (masterForAll !== null && masterForAll.rrule !== '') {
|
||||
// 把编辑内容写到主事件行(保留主事件的 uid / rrule / exdate)
|
||||
masterForAll.title = target.title;
|
||||
masterForAll.description = target.description;
|
||||
masterForAll.location = target.location;
|
||||
masterForAll.startTime = target.startTime;
|
||||
masterForAll.endTime = target.endTime;
|
||||
masterForAll.isAllDay = target.isAllDay;
|
||||
masterForAll.reminders = target.reminders;
|
||||
masterForAll.reminder = target.reminder;
|
||||
await EventDb.updateLocal(context, masterForAll);
|
||||
return;
|
||||
}
|
||||
await EventDb.updateLocal(context, target);
|
||||
return;
|
||||
}
|
||||
@@ -417,17 +604,18 @@ struct EventEditPage {
|
||||
if (master === null) {
|
||||
return;
|
||||
}
|
||||
const exList: string[] = master.exdate === '' ? [] : master.exdate.split(';');
|
||||
// ⚠️ **顺序很重要**:先把主事件行还原为原值(用户在编辑页改的标题/时间只属于"这一次",
|
||||
// 不能污染主事件),**再**写 EXDATE。反过来会被还原操作覆盖掉 EXDATE
|
||||
// (快照里存的是旧的 exdate),导致排除失效 → 原时间残留一条、看起来"没删掉/改错了"。
|
||||
if (cur.rrule !== '' && master.id === cur.id) {
|
||||
await EventDb.restoreRowById(context, master.id, cur);
|
||||
}
|
||||
const exList: string[] = cur.exdate === '' ? [] : cur.exdate.split(';');
|
||||
const occIcs: string = IcsUtil.fmtIcsTime(bounds.thisOcc, master.isAllDay);
|
||||
if (!exList.includes(occIcs)) {
|
||||
exList.push(occIcs);
|
||||
}
|
||||
await EventDb.setExdate(context, master.id, exList.join(';'));
|
||||
// ⚠️ 主事件行本身的内容保持不变("仅这一次"只影响这一次);
|
||||
// 如果用户是在主事件行上改的,需要把主事件改回原值(target 已被写成新时间)
|
||||
if (cur.rrule !== '') {
|
||||
await EventDb.restoreRowById(context, master.id, cur);
|
||||
}
|
||||
// 覆盖行:uid 同主事件、DTSTART=改到的新时间、RECURRENCE-ID=原始发生时刻
|
||||
const ov = new LocalEvent();
|
||||
ov.uid = master.uid;
|
||||
@@ -455,6 +643,8 @@ struct EventEditPage {
|
||||
}
|
||||
// 主事件本身被改期 → "此次及以后"里包含主事件(第一次发生)本身,
|
||||
// 需要让新系列从"改到的时间"开始(否则原 DTSTART 会被原系列继续展开)。
|
||||
// ⚠️ 判据要用**原始主事件的开始时间**(cur.startTime 是"载入时的行时间",
|
||||
// 对主事件行而言就是它的第一次发生时刻)。
|
||||
const startedAtThis: boolean =
|
||||
(cur.rrule !== '' && Math.abs(cur.startTime - bounds.thisOcc) < 1000);
|
||||
// 1) 先把"此次及以后"的覆盖实例行搬到新 uid(主事件行 recurrenceId=0,不受影响)
|
||||
@@ -475,7 +665,8 @@ struct EventEditPage {
|
||||
newMaster.endTime = target.endTime;
|
||||
newMaster.isAllDay = target.isAllDay;
|
||||
newMaster.recurring = true;
|
||||
// 新的重复规则:沿用原系列(用户没显式改规则时),保持"此次及以后"的重复节奏
|
||||
// 新的重复规则:优先用界面上设置的(含"重复到某天 / 共几次"),
|
||||
// 否则沿用原系列的重复节奏,保持"此次及以后"不变
|
||||
newMaster.rrule = target.rrule !== '' ? target.rrule : master.rrule;
|
||||
newMaster.reminders = target.reminders;
|
||||
newMaster.reminder = target.reminder;
|
||||
@@ -486,13 +677,13 @@ struct EventEditPage {
|
||||
if (startedAtThis) {
|
||||
await EventDb.markDeleted(context, master.id);
|
||||
} else {
|
||||
const truncated: string = IcsUtil.rruleWithUntil(master.rrule, bounds.beforeThis);
|
||||
await EventDb.setRruleForUid(context, master.calKey, master.uid, truncated);
|
||||
// ⚠️ 若用户编辑的正是"主事件这一行",target(= this.event)已被 doSave 写成新值,
|
||||
// 而它属于被截断的旧系列 → 必须还原为原值,否则旧系列末尾会多出一条被改过的幽灵行。
|
||||
if (cur.rrule !== '') {
|
||||
// ⚠️ 顺序:先还原主事件行(本次编辑只属于"此次及以后"的新系列,
|
||||
// 不能留在原主事件上),**再**截断 RRULE;反过来会被还原覆盖掉截断结果。
|
||||
if (cur.rrule !== '' && master.id === cur.id) {
|
||||
await EventDb.restoreRowById(context, master.id, cur);
|
||||
}
|
||||
const truncated: string = IcsUtil.rruleWithUntil(master.rrule, bounds.beforeThis);
|
||||
await EventDb.setRruleForUid(context, master.calKey, master.uid, truncated);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -564,13 +755,8 @@ struct EventEditPage {
|
||||
// 一律写入所选 CalDAV 日历本(新建/编辑/从「本机」迁过来都一样)
|
||||
e.calKey = book.calKey;
|
||||
e.href = book.href;
|
||||
// 重复规则与提醒
|
||||
if (this.repeatMode === 'custom') {
|
||||
// 无法识别的既有规则原样保留
|
||||
e.rrule = this.event !== null ? this.event.rrule : '';
|
||||
} else {
|
||||
e.rrule = this.rruleForMode(this.repeatMode);
|
||||
}
|
||||
// 重复规则与提醒(含"重复到某天 / 共几次"的结束条件)
|
||||
e.rrule = this.effectiveRrule();
|
||||
e.recurring = e.rrule !== '';
|
||||
e.reminders = EventDb.normalizeReminders(this.reminderList);
|
||||
e.reminder = e.reminders.length > 0 ? e.reminders[0] : 0;
|
||||
@@ -845,6 +1031,71 @@ struct EventEditPage {
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
// ⭐ 结束条件:永不结束 / 重复到某天 / 共重复几次
|
||||
// (新建与编辑都可用;编辑时会回显服务器上已有的 COUNT / UNTIL)
|
||||
if (this.repeatMode !== 'none' && this.repeatMode !== 'custom') {
|
||||
Divider().color($r('app.color.shadow_color'))
|
||||
Text('结束条件')
|
||||
.fontSize(14)
|
||||
.fontColor($r('app.color.text_secondary'))
|
||||
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
|
||||
this.repeatEndChip('never', '永不结束')
|
||||
this.repeatEndChip('until', '重复到某天')
|
||||
this.repeatEndChip('count', '共几次')
|
||||
}
|
||||
.width('100%')
|
||||
if (this.repeatEnd === 'until') {
|
||||
Row({ space: 8 }) {
|
||||
Text('截止日期')
|
||||
.fontSize(14)
|
||||
.fontColor($r('app.color.text_secondary'))
|
||||
Text(this.repeatUntil > 0 ? this.fmtDate(this.repeatUntil) : '请选择')
|
||||
.fontSize(15)
|
||||
.fontColor($r('app.color.brand'))
|
||||
.layoutWeight(1)
|
||||
Text('›')
|
||||
.fontSize(16)
|
||||
.fontColor($r('app.color.text_hint'))
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 8, right: 8, top: 10, bottom: 10 })
|
||||
.onClick(() => {
|
||||
this.openRepeatUntilPicker();
|
||||
})
|
||||
}
|
||||
if (this.repeatEnd === 'count') {
|
||||
Row({ space: 8 }) {
|
||||
Text('重复次数')
|
||||
.fontSize(14)
|
||||
.fontColor($r('app.color.text_secondary'))
|
||||
Blank()
|
||||
Text('−')
|
||||
.fontSize(20)
|
||||
.fontColor($r('app.color.brand'))
|
||||
.padding({ left: 14, right: 14 })
|
||||
.onClick(() => {
|
||||
this.repeatCount = Math.max(1, Math.round(this.repeatCount) - 1);
|
||||
})
|
||||
Text(String(Math.round(this.repeatCount)))
|
||||
.fontSize(16)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
.width(44)
|
||||
.textAlign(TextAlign.Center)
|
||||
Text('+')
|
||||
.fontSize(18)
|
||||
.fontColor($r('app.color.brand'))
|
||||
.padding({ left: 14, right: 14 })
|
||||
.onClick(() => {
|
||||
this.repeatCount = Math.min(999, Math.round(this.repeatCount) + 1);
|
||||
})
|
||||
Text('次')
|
||||
.fontSize(14)
|
||||
.fontColor($r('app.color.text_secondary'))
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 8, right: 8, top: 6, bottom: 6 })
|
||||
}
|
||||
}
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.width('100%')
|
||||
@@ -1002,23 +1253,69 @@ struct EventEditPage {
|
||||
// 表现为"时间选择框白框一闪而过"(范围弹层顶掉了它)。
|
||||
// 这里用 sheetMode 在同一个弹层里切换内容。
|
||||
.bindSheet($$this.sheetShow, this.bottomSheet(), {
|
||||
height: this.sheetMode === 'scope' ? 320 : 380,
|
||||
height: this.sheetMode === 'scope' ? 320 : (this.sheetMode === 'repeatEnd' ? 340 : 380),
|
||||
showClose: false,
|
||||
dragBar: true,
|
||||
backgroundColor: $r('app.color.card_bg')
|
||||
})
|
||||
}
|
||||
|
||||
/** ⭐ 唯一的底部弹层:按 sheetMode 切换显示"时间选择"或"重复范围选择" */
|
||||
/** ⭐ 唯一的底部弹层:按 sheetMode 切换显示"时间选择"/"重复范围选择"/"重复截止日期" */
|
||||
@Builder
|
||||
bottomSheet() {
|
||||
if (this.sheetMode === 'scope') {
|
||||
this.scopeSheet()
|
||||
} else if (this.sheetMode === 'repeatEnd') {
|
||||
this.repeatEndSheet()
|
||||
} else {
|
||||
this.dateTimePickerSheet()
|
||||
}
|
||||
}
|
||||
|
||||
/** 重复"截止日期"选择弹层(只选日期) */
|
||||
@Builder
|
||||
repeatEndSheet() {
|
||||
Column({ space: 14 }) {
|
||||
Text('重复到哪一天')
|
||||
.fontSize(16)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
.width('100%')
|
||||
.textAlign(TextAlign.Center)
|
||||
.padding({ top: 12 })
|
||||
DatePicker({
|
||||
start: new Date(2000, 0, 1),
|
||||
end: new Date(2060, 11, 31),
|
||||
selected: this.pickDate
|
||||
})
|
||||
.onDateChange((value: Date) => {
|
||||
this.pickDate = value;
|
||||
})
|
||||
.textStyle({ color: $r('app.color.text_secondary'), font: { size: 14 } })
|
||||
.selectedTextStyle({
|
||||
color: $r('app.color.text_primary'),
|
||||
font: { size: 16, weight: FontWeight.Medium }
|
||||
})
|
||||
.disappearTextStyle({ color: $r('app.color.text_hint'), font: { size: 14 } })
|
||||
.layoutWeight(1)
|
||||
.height(210)
|
||||
.width('100%')
|
||||
Button('确定')
|
||||
.width('100%')
|
||||
.height(44)
|
||||
.borderRadius(12)
|
||||
.fontSize(15)
|
||||
.backgroundColor($r('app.color.brand'))
|
||||
.onClick(() => {
|
||||
const d = this.pickDate;
|
||||
this.repeatUntil = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
||||
this.sheetShow = false;
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 16, right: 16, bottom: 24 })
|
||||
}
|
||||
|
||||
/** ⭐ 重复日程"作用范围"选择弹层:保存/删除前让用户明确改哪几次 */
|
||||
@Builder
|
||||
scopeSheet() {
|
||||
@@ -1135,6 +1432,31 @@ struct EventEditPage {
|
||||
.backgroundColor(this.repeatMode === mode ? $r('app.color.brand') : $r('app.color.chip_off_bg'))
|
||||
.onClick(() => {
|
||||
this.repeatMode = mode;
|
||||
// 切换到"重复"时给个合理默认:永不结束(用户可按需改为到某天/共几次)
|
||||
if (mode !== 'none' && this.repeatEnd === '') {
|
||||
this.repeatEnd = 'never';
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 结束条件 chip:永不结束 / 重复到某天 / 共几次 */
|
||||
@Builder
|
||||
repeatEndChip(mode: string, label: string) {
|
||||
Text(label)
|
||||
.fontSize(12)
|
||||
.fontColor(this.repeatEnd === 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.repeatEnd === mode ? $r('app.color.brand') : $r('app.color.chip_off_bg'))
|
||||
.onClick(() => {
|
||||
this.repeatEnd = mode;
|
||||
if (mode === 'until' && this.repeatUntil <= 0) {
|
||||
// 默认给"从开始时间起 3 个月后"
|
||||
const s = new Date(this.startMs);
|
||||
this.repeatUntil = new Date(s.getFullYear(), s.getMonth() + 3, s.getDate()).getTime();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1030,9 +1030,13 @@ struct Index {
|
||||
.borderRadius(12)
|
||||
.onClick(() => {
|
||||
const id: number = this.detailEvent !== null ? this.detailEvent.id : 0;
|
||||
// ⭐ 把"用户点的是哪一次发生"一起带过去:重复日程展开后所有次共用主事件的 id,
|
||||
// 不带发生时刻的话,编辑页无法知道用户点的是哪一次 → "仅这一次"会算错。
|
||||
const occ: number = this.detailEvent !== null ? this.detailEvent.startTime : 0;
|
||||
this.detailShow = false;
|
||||
if (id > 0) {
|
||||
AppStorage.setOrCreate<number>('pendingEventId', id);
|
||||
AppStorage.setOrCreate<number>('pendingOccurrenceTs', occ);
|
||||
router.pushUrl({ url: 'pages/EventEditPage' });
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user