feat(待办/日程): 两页时间选择器统一居中弹出 + 待办截止行去掉「修改」字样 (#51)

## 1. 日程页时间选择器也改为居中弹出
用户要求:「新建日程那里,开始和结束时间也用这种方式弹出来吧」

与待办页保持一致:根节点改 Stack,弹层居中浮在内容之上。
移除 bindSheet(SDK 的 SheetOptions 没有 position/alignment,只能底部弹)。
三种弹层(时间选择/重复范围/重复截止)继续共用同一个 sheetMode 切换。

### 真机验证(5NC0225B27004718)
弹层 y=713~1691,中心 1202;屏幕中心 1222,偏差 20px ✅

## 2. 待办页截止时间行去掉「修改」二字
用户要求:「修改那两个字没必要,直接点那个时间框,出现选择时间的地方」

整行本来就可点,右侧改为一个 `›` 箭头作视觉暗示(与日程页「开始/结束时间」行一致)。

## 3. 修复:纯待办本的 event 通道误落库(导致 Inbox 待办消失 + 编辑报错)

### 根因
`SyncEngine` 的 event 通道用 `DavClient.reportEtags(href)`,**不区分本的 COMP_SET 类型**。
对纯待办本(如群晖 Inbox,COMP_SET=todo)也照常跑 event REPORT:
- 服务器返回了文件列表(里面其实是 VTODO 文件)
- `parseEvents` 解析出 0 条日程,**但文件仍被落库成 kind='event'**
- 结果:本地库出现"属于待办本的日程记录"(实测 id=748 title=Hvf)
- 用户新建的「测试」待办进不来,反而被这条假日程占用 cal_key

### 后果
- 待办列表看不到(列表只查 kind='todo')
- 点它编辑走日程通道 → 报「需要联网 + 日历本需可写」
- 用户反馈的「编辑待办失败」由此而来

### 修法
event 通道前判断 `acc.calendarComps[i] === 'todo'` 就跳过,
日志明确输出「是纯待办本(COMP_SET=todo),跳过 event 通道」。

### 验证
日志实测:`日历本[1]「Inbox」是纯待办本(COMP_SET=todo),跳过 event 通道` ✅

## 编译
BUILD SUCCESSFUL

## 遗留
Inbox 的 VTODO REPORT 在群晖上仍返回空 multistatus(68 字符),
「测试」待办能否同步需用户重新建一条再验证。
This commit is contained in:
2026-11-05 11:14:46 +08:00
parent 39337a5530
commit daa577e88e
5 changed files with 57 additions and 18 deletions
+9
View File
@@ -501,6 +501,8 @@ export class DavClient {
} }
const xml: string = resp.result as string; const xml: string = resp.result as string;
LogUtil.write(`HTTP REPORT(${compName}) 响应体 ${xml.length} 字符`); LogUtil.write(`HTTP REPORT(${compName}) 响应体 ${xml.length} 字符`);
LogUtil.write(`REPORT-诊断(${compName}) 前200字符: ${xml.substring(0, 200)}`);
LogUtil.write(`REPORT-诊断(${compName}) 含VTODO=${xml.indexOf('BEGIN:VTODO') >= 0} 含VEVENT=${xml.indexOf('BEGIN:VEVENT') >= 0} 响应数=${(xml.match(/<[\w-]*:?response[\s>]/gi) || []).length}`);
const originMatch = /https?:\/\/[^/]+/i.exec(href); const originMatch = /https?:\/\/[^/]+/i.exec(href);
const origin: string = originMatch !== null ? originMatch[0] : ''; const origin: string = originMatch !== null ? originMatch[0] : '';
const items: RemoteItem[] = []; const items: RemoteItem[] = [];
@@ -525,6 +527,13 @@ export class DavClient {
item.ics = block.substring(start, end + 'END:VCALENDAR'.length); item.ics = block.substring(start, end + 'END:VCALENDAR'.length);
items.push(item); items.push(item);
} }
LogUtil.write(`诊断 blocks=${blocks.length} items=${items.length} xml长=${xml.length}`);
for (let bi = 0; bi < blocks.length; bi++) {
const bb: string = blocks[bi];
if (!/<[\w-]*:?response[\s>]/i.test(bb)) { continue; }
const h: string = DavClient.extractTag(bb, "href");
LogUtil.write(`诊断 block${bi} len=${bb.length} href=[${h.substring(0,50)}] VCAL=${bb.indexOf("BEGIN:VCALENDAR")>=0} VTODO=${bb.indexOf("BEGIN:VTODO")>=0}`);
}
return items; return items;
} finally { } finally {
httpRequest.destroy(); httpRequest.destroy();
+12
View File
@@ -161,6 +161,16 @@ export class SyncEngine {
const calName: string = i < acc.calendarNames.length ? acc.calendarNames[i] : `日历本${i}`; const calName: string = i < acc.calendarNames.length ? acc.calendarNames[i] : `日历本${i}`;
LogUtil.write(`日历本[${i}]「${calName}」开始同步:${href}`); LogUtil.write(`日历本[${i}]「${calName}」开始同步:${href}`);
const t1: number = Date.now(); const t1: number = Date.now();
// ⭐ 纯待办本(COMP_SET 只有 VTODO)不跑 event 通道。
// 实测群晖:对纯待办本做 event 的 REPORT 会返回文件列表,
// 里面其实是 VTODO 文件,被 parseEvents 解析成 0 条日程却仍落库
// → 本地库出现 kind='event' 记录,而所在本是待办本。
// 后果:这条"假日程"在待办列表看不到(列表只查 kind='todo'),
// 但点它编辑会走日程通道,报"需要联网 + 日历本需可写"。
const thisComps: string = i < acc.calendarComps.length ? acc.calendarComps[i] : '';
if (thisComps === 'todo') {
LogUtil.write(`日历本[${i}]「${calName}」是纯待办本(COMP_SET=todo),跳过 event 通道`);
} else {
let items: RemoteItem[] = []; let items: RemoteItem[] = [];
try { try {
items = await DavClient.reportEtags(href, auth); items = await DavClient.reportEtags(href, auth);
@@ -307,6 +317,8 @@ export class SyncEngine {
const stat: string = await EventDb.applyRemote(context, calKey, href, remote, false); const stat: string = await EventDb.applyRemote(context, calKey, href, remote, false);
LogUtil.write(`日历本[${i}]「${calName}」日程落库完成:${stat},耗时 ${Math.round((Date.now() - t1) / 1000)} 秒`); LogUtil.write(`日历本[${i}]「${calName}」日程落库完成:${stat},耗时 ${Math.round((Date.now() - t1) / 1000)} 秒`);
changed += remote.length; changed += remote.length;
} // end else(非纯待办本才跑 event 通道)
// 3) 拉取该日历本下的待办(VTODO,只读展示,拉取失败不影响日程同步) // 3) 拉取该日历本下的待办(VTODO,只读展示,拉取失败不影响日程同步)
try { try {
const todoItems = await DavClient.reportTodos(href, auth); const todoItems = await DavClient.reportTodos(href, auth);
+30 -15
View File
@@ -1391,7 +1391,8 @@ struct EventEditPage {
} }
build() { build() {
Column({ space: 14 }) { Stack({ alignContent: Alignment.Center }) {
Column({ space: 14 }) {
// 顶部(与内容区左右对齐,避免贴边误触) // 顶部(与内容区左右对齐,避免贴边误触)
Row({ space: 6 }) { Row({ space: 6 }) {
Text('取消') Text('取消')
@@ -1832,27 +1833,41 @@ struct EventEditPage {
.scrollBar(BarState.Off) .scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.Spring) .edgeEffect(EdgeEffect.Spring)
.align(Alignment.Top) .align(Alignment.Top)
}
.width('100%')
.height('100%')
.padding({ top: 12 })
.backgroundColor($r('app.color.page_bg'))
// 沉浸式:背景延伸到状态栏/导航条区域(只扩绘制、不移动子组件)
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
// ===== 居中弹层 =====
if (this.sheetShow) {
Column()
.width('100%')
.height('100%')
.backgroundColor('#66000000')
.onClick(() => {
this.sheetShow = false;
})
this.centerSheet()
}
} }
.width('100%') .width('100%')
.height('100%') .height('100%')
.padding({ top: 12 })
.backgroundColor($r('app.color.page_bg'))
// 沉浸式:背景延伸到状态栏/导航条区域(只扩绘制、不移动子组件)
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
// ⚠️ 整个页面**只允许一个 bindSheet**:挂两个时后者会覆盖前者,
// 表现为"时间选择框白框一闪而过"(范围弹层顶掉了它)。
// 这里用 sheetMode 在同一个弹层里切换内容。
.bindSheet($$this.sheetShow, this.bottomSheet(), {
height: this.sheetMode === 'scope' ? 320 : (this.sheetMode === 'repeatEnd' ? 340 : 380),
showClose: false,
dragBar: true,
backgroundColor: $r('app.color.card_bg')
})
} }
/** ⭐ 唯一的底部弹层:按 sheetMode 切换显示"时间选择"/"重复范围选择"/"重复截止日期" */ /**
* 居中弹层(自绘覆盖层,与待办页一致)。
*
* 原来用 bindSheet 从底部弹出,用户反馈"点完之后选择框在下面出来,操作不顺畅"。
* 改成屏幕居中,视线不用上下跳。
*
* 为什么不用 bindSheet:SDK 的 SheetOptions 没有 position/alignment,只能底部弹。
*/
@Builder @Builder
bottomSheet() { centerSheet() {
if (this.sheetMode === 'scope') { if (this.sheetMode === 'scope') {
this.scopeSheet() this.scopeSheet()
} else if (this.sheetMode === 'repeatEnd') { } else if (this.sheetMode === 'repeatEnd') {
+1
View File
@@ -3637,6 +3637,7 @@ struct Index {
// ⭐ 改为独立页面(与编辑日程一致):把 id 预置后跳转 // ⭐ 改为独立页面(与编辑日程一致):把 id 预置后跳转
AppStorage.setOrCreate<number>('pendingTodoId', t.id); AppStorage.setOrCreate<number>('pendingTodoId', t.id);
AppStorage.setOrCreate<number>('pendingTodoDate', 0); AppStorage.setOrCreate<number>('pendingTodoDate', 0);
LogUtil.write(`打开待办编辑:id=${t.id} uid=${t.uid} 标题长度=${t.title.length} 本=${t.calKey}`);
router.pushUrl({ url: 'pages/TodoEditPage' }); router.pushUrl({ url: 'pages/TodoEditPage' });
return; return;
} }
+5 -3
View File
@@ -615,9 +615,11 @@ struct TodoEditPage {
.fontColor(this.hasDue .fontColor(this.hasDue
? $r('app.color.text_primary') : $r('app.color.text_hint')) ? $r('app.color.text_primary') : $r('app.color.text_hint'))
Blank() Blank()
Text('修改') // ⭐ 不再显示"修改"文字:整行可点,右侧放个箭头作为可点的视觉暗示
.fontSize(13) // (与日程页的「开始/结束时间」行一致)
.fontColor($r('app.color.brand')) Text('›')
.fontSize(18)
.fontColor($r('app.color.text_hint'))
} }
.layoutWeight(1) .layoutWeight(1)
.padding({ left: 12, right: 12, top: 12, bottom: 12 }) .padding({ left: 12, right: 12, top: 12, bottom: 12 })