From 5ab9648b5a40ed4522f96fcfc0e9d1ab37bccf91 Mon Sep 17 00:00:00 2001 From: Yang Yongquan Date: Tue, 15 Sep 2026 23:03:58 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=97=A5=E5=8E=86=E6=9C=AC=E9=A2=9C=E8=89=B2=E4=B8=8D=E5=AF=B9?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Yang Yongquan --- entry/src/main/ets/common/AccountStore.ets | 16 ++++++++ .../main/ets/common/CalendarDataService.ets | 4 +- entry/src/main/ets/common/DavClient.ets | 28 +++++++++++++ entry/src/main/ets/common/EventDb.ets | 41 +++++++++++++++++++ entry/src/main/ets/common/SyncEngine.ets | 17 ++++---- entry/src/main/ets/pages/EditAccountPage.ets | 26 +++++++++++- entry/src/main/ets/pages/EventEditPage.ets | 2 +- .../src/main/ets/syncwork/SyncWorkAbility.ets | 4 ++ 8 files changed, 127 insertions(+), 11 deletions(-) diff --git a/entry/src/main/ets/common/AccountStore.ets b/entry/src/main/ets/common/AccountStore.ets index 77c7c87..308239b 100644 --- a/entry/src/main/ets/common/AccountStore.ets +++ b/entry/src/main/ets/common/AccountStore.ets @@ -79,6 +79,22 @@ export class BookPalette { static colorFor(index: number): string { return BookPalette.colors[index % BookPalette.colors.length]; } + + /** + * 按日历本的**稳定标识**(集合 href)派生回退色。 + * + * 为什么不用 colorFor(索引):服务器未返回颜色时若按索引取色,一旦在服务器上 + * 新增/删除日历本导致列表顺序变化,索引就会位移,**同一个日历本的颜色会跟着变** + * (表现为"加了个新日历本,其他日历本色块颜色全不对了")。 + * 用 href 做哈希派生则与顺序无关,增删日历本不会影响其余日历本的颜色。 + */ + static colorForHref(href: string): string { + let h: number = 0; + for (let i = 0; i < href.length; i++) { + h = (h * 31 + href.charCodeAt(i)) >>> 0; + } + return BookPalette.colors[h % BookPalette.colors.length]; + } } function bytesToB64(u: Uint8Array): string { diff --git a/entry/src/main/ets/common/CalendarDataService.ets b/entry/src/main/ets/common/CalendarDataService.ets index 96eea8c..3204203 100644 --- a/entry/src/main/ets/common/CalendarDataService.ets +++ b/entry/src/main/ets/common/CalendarDataService.ets @@ -66,9 +66,9 @@ export class CalendarDataService { name = acc.calendarHrefs.length === 1 ? acc.name : `日历本 ${i + 1}`; } s.name = `${acc.name}:${name}`; - // 优先使用服务器端定义的颜色,未定义时按序号取调色板 + // 优先使用服务器端定义的颜色;未定义时按**集合 href** 派生(不随顺序变化,见 colorForHref 说明) let color: string = i < acc.calendarColors.length ? AccountStore.normalizeColor(acc.calendarColors[i]) : ''; - s.color = color !== '' ? color : BookPalette.colorFor(i); + s.color = color !== '' ? color : BookPalette.colorForHref(acc.calendarHrefs[i]); s.source = 'dav'; s.visible = true; // 写权限:服务器探测结果 + 手动标记(部分服务器不在 CalDAV 层拒绝写入) diff --git a/entry/src/main/ets/common/DavClient.ets b/entry/src/main/ets/common/DavClient.ets index 162b6e3..786cfdb 100644 --- a/entry/src/main/ets/common/DavClient.ets +++ b/entry/src/main/ets/common/DavClient.ets @@ -191,6 +191,34 @@ export class DavClient { } } + /** + * 规范化集合 href,用于「本地保存的 calendarHrefs」与「PROPFIND 返回的 href」比对。 + * + * 为什么需要:同一集合在不同 PROPFIND 请求/不同服务器上的写法可能不同—— + * ① 绝对 URL(https://nas/calendars/A/) vs 纯路径(/calendars/A/) + * ② URL 编码差异(/calendars/%E6%96%B0/ vs /calendars/新/) + * ③ 末尾斜杠有无、重复斜杠 + * 直接用字符串相等比对会匹配失败 → 服务器上的颜色取不回来,界面只能退回调色板色。 + * 这里统一成「解码后的路径、无尾斜杠」再比较(路径大小写保持敏感,不转小写)。 + */ + static normalizeHref(href: string): string { + let s: string = href.trim(); + const m = /^[a-zA-Z]+:\/\/[^/]+/i.exec(s); + if (m !== null) { + s = s.substring(m[0].length); + } + try { + s = decodeURIComponent(s); + } catch (err) { + // 解码失败(如含非法 % 序列)保持原样 + } + s = s.replace(/\/{2,}/g, '/'); + if (s.length > 1 && s.endsWith('/')) { + s = s.substring(0, s.length - 1); + } + return s; + } + /** 颜色规范化:#RRGGBBAA → #RRGGBB */ static normalizeHex(raw: string): string { const v: string = raw.trim(); diff --git a/entry/src/main/ets/common/EventDb.ets b/entry/src/main/ets/common/EventDb.ets index d450f98..d2f268c 100644 --- a/entry/src/main/ets/common/EventDb.ets +++ b/entry/src/main/ets/common/EventDb.ets @@ -516,4 +516,45 @@ export class EventDb { } await store.delete(predicates); } + + /** + * 迁移因「日历本序号变动」而错位的日程行(calKey = `${accId}_${序号}`)。 + * + * 背景(本方法要修的现象):序号取自「勾选顺序」。在服务器上新增一个日历本后重新勾选, + * 若新本不是排在末尾而是插在中间,其后所有日历本的序号都会 +1;但已落库的日程仍挂着 + * 旧 calKey,于是**日程与日历本错位**——表现为色块颜色不对、日程显示到别人的日历本下。 + * 同步虽然最终会用远端数据重建,但在同步完成前(以及同步部分失败时)错位一直可见。 + * + * 做法:按 href 对应关系把旧 calKey 的行整体搬到新 calKey。 + * 分两阶段更新(旧键 → 临时键 → 新键),避免 A↔B 互换时后一次更新把前一次的行一起改掉。 + * + * @param pairs [旧calKey, 新calKey] 列表;只传真正发生位移的项 + * @returns 被迁移的行数 + */ + static async remapCalKeys(context: common.Context, pairs: Array<[string, string]>): Promise { + if (pairs.length === 0) { + return 0; + } + const store = await EventDb.getDb(context); + const stamp: number = Date.now(); + const tmpKeys: string[] = []; + let moved: number = 0; + // 阶段一:旧键 → 临时键 + for (let i = 0; i < pairs.length; i++) { + const tmpKey: string = `__remap_${stamp}_${i}`; + const bucket: relationalStore.ValuesBucket = { 'cal_key': tmpKey }; + const p = new relationalStore.RdbPredicates('events'); + p.equalTo('cal_key', pairs[i][0]); + moved += await store.update(bucket, p); + tmpKeys.push(tmpKey); + } + // 阶段二:临时键 → 新键 + for (let i = 0; i < pairs.length; i++) { + const bucket: relationalStore.ValuesBucket = { 'cal_key': pairs[i][1] }; + const p = new relationalStore.RdbPredicates('events'); + p.equalTo('cal_key', tmpKeys[i]); + await store.update(bucket, p); + } + return moved; + } } \ No newline at end of file diff --git a/entry/src/main/ets/common/SyncEngine.ets b/entry/src/main/ets/common/SyncEngine.ets index 94a9dc4..2bb859a 100644 --- a/entry/src/main/ets/common/SyncEngine.ets +++ b/entry/src/main/ets/common/SyncEngine.ets @@ -202,19 +202,22 @@ export class SyncEngine { } for (let i = 0; i < acc.calendarHrefs.length; i++) { const target: string = acc.calendarHrefs[i]; - const originMatch = /https?:\/\/[^/]+/i.exec(target); - let path: string = originMatch !== null ? target.substring(originMatch[0].length) : target; - if (path === '') { - path = '/'; - } - const norm = (s: string): string => s.endsWith('/') ? s : s + '/'; + // 用规范化后的路径比对:兼容「绝对 URL vs 纯路径」「URL 编码差异」「尾斜杠有无」, + // 否则新加入的日历本匹配不上 → 服务器颜色取不回来,界面只能退回调色板色(表现为"颜色不对") + const targetNorm: string = DavClient.normalizeHref(target); const found = entries.find((e: DavColorEntry): boolean => - norm(e.href) === norm(path)); + DavClient.normalizeHref(e.href) === targetNorm); if (found !== undefined && found.color !== '') { while (acc.calendarColors.length <= i) { acc.calendarColors.push(''); } + if (acc.calendarColors[i] !== found.color) { + LogUtil.write(`日历本[${i}] 颜色更新:${acc.calendarColors[i] === '' ? '(空)' : acc.calendarColors[i]} → ${found.color}`); + } acc.calendarColors[i] = found.color; + } else if (found === undefined) { + // 匹配失败不改动已有颜色(保留上次取到的值),但记录便于排查 + LogUtil.write(`日历本[${i}] 颜色未匹配:${target}(PROPFIND 返回 ${entries.length} 个集合)`); } // 回写各日历本写权限('1'=可写 '0'=只读) while (acc.calendarWritable.length <= i) { diff --git a/entry/src/main/ets/pages/EditAccountPage.ets b/entry/src/main/ets/pages/EditAccountPage.ets index 21dff01..31cd972 100644 --- a/entry/src/main/ets/pages/EditAccountPage.ets +++ b/entry/src/main/ets/pages/EditAccountPage.ets @@ -208,11 +208,35 @@ struct EditAccountPage { this.isSaving = false; return; } + // 保存前的旧序号(calKey = accId_序号,序号=勾选顺序): + // 服务器上新增日历本后,新本可能插在列表中间导致后续序号整体位移, + // 若不迁移,已落库的日程会挂错日历本(表现为色块颜色不对)。 + const oldHrefs: string[] = target.calendarHrefs.slice(); target.name = this.accountName.trim(); - target.calendarHrefs = selectedItems.map((c: EditCalendarItem): string => c.href); + const newHrefs: string[] = selectedItems.map((c: EditCalendarItem): string => c.href); + target.calendarHrefs = newHrefs; target.calendarNames = selectedItems.map((c: EditCalendarItem): string => c.name); target.calendarColors = selectedItems.map((c: EditCalendarItem): string => c.color); LogUtil.write(`编辑账号保存:id=${target.id},新勾选 ${selectedItems.length}/${this.calendarList.length} 个日历本`); + // 按 href 求出「旧序号 → 新序号」,迁移已落库日程到正确的 calKey + const remapPairs: Array<[string, string]> = []; + for (let oi: number = 0; oi < oldHrefs.length; oi++) { + const oldNorm: string = DavClient.normalizeHref(oldHrefs[oi]); + let ni: number = -1; + for (let k: number = 0; k < newHrefs.length; k++) { + if (DavClient.normalizeHref(newHrefs[k]) === oldNorm) { + ni = k; + break; + } + } + if (ni >= 0 && ni !== oi) { + remapPairs.push([`${target.id}_${oi}`, `${target.id}_${ni}`]); + } + } + if (remapPairs.length > 0) { + const moved: number = await EventDb.remapCalKeys(context, remapPairs); + LogUtil.write(`编辑账号保存:日历本序号变动,迁移日程 ${moved} 行(${remapPairs.length} 个日历本)`); + } await AccountStore.saveAll(context, accounts); // 重选后 calKey(accId_序号)会变化,清理已取消勾选的日历本数据 const validKeys: string[] = diff --git a/entry/src/main/ets/pages/EventEditPage.ets b/entry/src/main/ets/pages/EventEditPage.ets index 85047aa..7481843 100644 --- a/entry/src/main/ets/pages/EventEditPage.ets +++ b/entry/src/main/ets/pages/EventEditPage.ets @@ -725,7 +725,7 @@ class CalendarDataBridge { } 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.color = color !== '' ? color : BookPalette.colorForHref(acc.calendarHrefs[i]); s.href = acc.calendarHrefs[i]; s.writable = true; result.push(s); diff --git a/entry/src/main/ets/syncwork/SyncWorkAbility.ets b/entry/src/main/ets/syncwork/SyncWorkAbility.ets index 8ad61fa..525c6d2 100644 --- a/entry/src/main/ets/syncwork/SyncWorkAbility.ets +++ b/entry/src/main/ets/syncwork/SyncWorkAbility.ets @@ -54,6 +54,10 @@ export default class SyncWorkAbility extends WorkSchedulerExtensionAbility { } await SyncEngine.settleLocalEvents(context); await SyncEngine.pruneOrphanRows(context, accounts); + // ⚠️ 必须回写账号:syncAccount 会就地更新 acc(服务器日历本颜色、写权限等)。 + // 缺了这一步,后台同步刷到的颜色只留在内存里、进程退出即丢失, + // 界面下次冷启动仍用旧颜色(表现为"同步之后颜色还是不对")。 + await AccountStore.saveAll(context, accounts); // 同步后刷新卡片 + 重建提醒,保证后台同步的成果直接可见 await CardDataService.pushToAllForms(context); await ReminderService.refreshReminders(context);