From b8d5aa630c181caa29831d802f882712ef9e9c73 Mon Sep 17 00:00:00 2001 From: laoyang Date: Tue, 3 Nov 2026 23:14:43 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E5=8D=A1=E7=89=87/=E7=B2=98=E8=B4=B4):=205?= =?UTF-8?q?=20=E9=A1=B9=E5=8F=8D=E9=A6=88=E4=BF=AE=E5=A4=8D=20=E2=80=94?= =?UTF-8?q?=E2=80=94=20=E7=B2=98=E8=B4=B4=E6=8C=89=E9=92=AE=E3=80=81?= =?UTF-8?q?=E5=8D=A1=E7=89=87=E6=95=B0=E6=8D=AE=E3=80=81=E5=8D=A1=E7=89=87?= =?UTF-8?q?=E5=8A=A0=E5=8F=B7=E3=80=81=E5=8D=A1=E7=89=87=E6=BB=9A=E5=8A=A8?= =?UTF-8?q?=E3=80=81=E5=88=97=E8=A1=A8=E7=BB=9F=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 1. 粘贴按钮文字看不清(EventEditPage) 根因不是"忘了设文字"——空参时系统本就给默认样式(LINES 图标 + PASTE 文案)。 真正原因是我**给了一个和系统文字颜色太接近的背景色**(card_bg 近乎白)。 查 SDK 类型定义 `paste_button.d.ts`,错误码 2 明确列出: 「字体或图标颜色与组件背景颜色相近」「文字超出组件背景区域」等 安全组件创建时会做样式校验,对比度不足会让文字灰淡难辨、甚至授权失败。 同时发现 icon/text/buttonType **只能走构造参数**,不是链式属性 (`.text()` 会编译报 "Property 'text' does not exist")。 改为: ``` PasteButton({ icon: PasteIconStyle.LINES, // 该枚举只有 LINES text: PasteDescription.PASTE, // 该枚举只有 PASTE buttonType: ButtonType.Capsule }) ``` 并**去掉自定义背景色**,交给系统默认样式,只保留尺寸与圆角。 ## 2. 待办卡片显示 0 条(CardDataService) `pushToAllForms()` 是同步完成后刷新所有卡片的主路径, 但它只调 `buildCardData()`(日程),**从未调 `buildTodoCardData()`** ⇒ 推给卡片的 binding 里 todoCount / todoListJson 全是默认值。 而 EntryFormAbility.pushData() 里合并了待办 —— 两条路径不一致, 所以表现成"有时对、有时 0"。 修:抽出 `mergeTodoInto()` 供两处共用;待办数据与 dayOffset 无关, 整个 pushToAllForms 只构建一次。 真机日志:`待办卡片数据刷新:未完成 5 条…列表输出 5 条`(原为 0)。 ## 3. 待办卡片右上角 + 点了没反应(EntryAbility + Index) 待办卡片发的是 `widgetAction: 'addTodo'` / `'openTodo'`, 但 EntryAbility 只判断 `raw === 'add'`,Index 也只处理 `'add'` ⇒ 参数被静默丢弃。 修:两端都补上 addTodo / openTodo。 addTodo → 切待办 Tab + 打开新建待办 openTodo → 切待办 Tab ## 4. 日程 2×4 卡片右侧可滚动,15 条(CardDataService + Widget4x2) 数据侧 `slice(0, 2)` → `slice(0, 15)` 视图侧 `Column + ForEach` → `List + ListItem`(虚拟化 + 滚动惯性 + 回弹) 真机日志:`进行中 15 条`,确认上限生效。 ## 5. 待办 4×4 / 6×4 列表样式统一,全部可滚动 按用户选择,**统一成 4×4 样式**(勾选框 + 色条 + 标题 + 副行「截止时间 · 日历本」)。 6×4 原来是「标题 + 独立截止时间列 + 独立日历本列」,窄卡片里会把标题挤没, 改为与 4×4 完全相同的结构。已用逐行 diff 校验:两卡 todoRow **36 行完全一致**。 `MAX_TODO_ITEMS` 20 → 15,与日程卡片口径一致(所有待办卡片都已可滚动, 不必再按卡片尺寸给不同上限)。 ## 编译 `BUILD SUCCESSFUL` ## 未验证 真机仅验证了「待办卡片数据不再为 0」与「日程 15 条上限」两项。 粘贴按钮视觉、卡片内滚动、卡片 + 跳转、两卡样式一致性需上机目视确认。 --- entry/src/main/ets/common/CardDataService.ets | 48 +++++++++++++--- .../main/ets/entryability/EntryAbility.ets | 8 ++- entry/src/main/ets/pages/EventEditPage.ets | 16 +++++- entry/src/main/ets/pages/Index.ets | 20 ++++++- .../main/ets/pages/widget/TodoWidget6x4.ets | 55 +++++++++++-------- entry/src/main/ets/pages/widget/Widget4x2.ets | 22 +++++--- 6 files changed, 123 insertions(+), 46 deletions(-) diff --git a/entry/src/main/ets/common/CardDataService.ets b/entry/src/main/ets/common/CardDataService.ets index 49d959c..4c4c6dc 100644 --- a/entry/src/main/ets/common/CardDataService.ets +++ b/entry/src/main/ets/common/CardDataService.ets @@ -149,25 +149,53 @@ export class CardDataService { } /** App 内同步完成后调用:刷新所有已添加的卡片 */ + /** + * 把待办数据合并进日程数据对象。 + * + * ⭐ 抽出来的原因: EntryFormAbility.pushData 与 CardDataService.pushToAllForms + * 是两条独立的推送路径,之前只有前者做了合并,后者漏了 ⇒ + * 待办卡片永远显示 0 条(todoCount 停在默认值)。 + * 两处共用同一段逻辑,避免再出现"改一处忘一处"。 + */ + private static mergeTodoInto(ev: CardData, td: CardData): void { + ev.todoListJson = td.todoListJson; + ev.todoCount = td.todoCount; + ev.todoTodayCount = td.todoTodayCount; + } + static async pushToAllForms(context: common.Context): Promise { // 先恢复持久化的 formId(App 重启后内存注册表为空) await CardDataService.loadPersistedFormIds(context); if (CardDataService.formIds.length === 0) { return; } + // ⭐ 待办数据与 dayOffset 无关(待办不翻页),只构建一次 + let todoData: CardData | null = null; + try { + todoData = await CardDataService.buildTodoCardData(context); + } catch (err) { + console.error(`待办卡片数据构建失败: ${(err as BusinessError).message}`); + } // 各卡片可能翻到了不同日期 → 按偏移分组构建,避免把用户翻走的日期拽回今天 const cache: Map = new Map(); + const fallbackData: CardData = await CardDataService.buildCardData(context, 0); + if (todoData !== null) { + CardDataService.mergeTodoInto(fallbackData, todoData); + } const fallback: formBindingData.FormBindingData = - formBindingData.createFormBindingData(await CardDataService.buildCardData(context, 0)); + formBindingData.createFormBindingData(fallbackData); cache.set(0, fallback); const stale: string[] = []; for (const formId of CardDataService.formIds) { const off: number = CardDataService.getDayOffset(formId); let binding: formBindingData.FormBindingData | undefined = cache.get(off); if (binding === undefined) { - binding = formBindingData.createFormBindingData( - await CardDataService.buildCardData(context, off)); + const d: CardData = await CardDataService.buildCardData(context, off); + if (todoData !== null) { + CardDataService.mergeTodoInto(d, todoData); + } + binding = formBindingData.createFormBindingData(d); cache.set(off, binding); } try { @@ -327,14 +355,16 @@ export class CardDataService { // 今日日程条数:item.date === '今天' 的均为今天分组(含全天/跨天进行中) data.todayCount = items.filter((i: CardItem): boolean => i.date === '今天').length; - // ===== 2x4 卡片右侧:**当前时间之后最近的 2 条**日程 ===== + // ===== 2x4 卡片右侧:**当前时间之后最近的若干条**日程(卡片可滚动,最多 15 条)===== // 规则:只取"还没开始"的有时间日程(startTime >= now), // **排除全天 / 跨天**,按开始时间升序取前 2 条。 // 不再区分"进行中 / 即将开始" —— 卡片上不再显示这些字样。 const upcomingNext: DisplayEvent[] = upcoming .filter((e: DisplayEvent): boolean => !isDayLong(e) && e.startTime >= nowMs) .sort((a: DisplayEvent, b: DisplayEvent): number => a.startTime - b.startTime); - const ongoingPick: DisplayEvent[] = upcomingNext.slice(0, 2); + // ⭐ 上限 15 条:卡片可滚动,不必再为了"放得下"而截断到 2 条。 + // 15 是经验值 —— 再多则用户更该进 App 看,而不是在卡片里翻。 + const ongoingPick: DisplayEvent[] = upcomingNext.slice(0, 15); const ongoingItems: CardItem[] = []; for (const e of ongoingPick) { const dayLong: boolean = isDayLong(e); @@ -404,8 +434,12 @@ export class CardDataService { return data; } - /** 卡片列表最多渲染多少条待办(4x4 用前 6 条、6x4 用前 10 条,卡片侧自行截断) */ - private static readonly MAX_TODO_ITEMS: number = 20; + /** + * 卡片列表最多输出多少条待办。 + * ⭐ 统一为 15:所有待办卡片都已改为可滚动(List + 虚拟化), + * 不必再为了"一屏放得下"而按卡片尺寸给不同上限。 + */ + private static readonly MAX_TODO_ITEMS: number = 15; /** * 构建**待办卡片**数据。 diff --git a/entry/src/main/ets/entryability/EntryAbility.ets b/entry/src/main/ets/entryability/EntryAbility.ets index 4031210..4a3e57c 100644 --- a/entry/src/main/ets/entryability/EntryAbility.ets +++ b/entry/src/main/ets/entryability/EntryAbility.ets @@ -42,10 +42,12 @@ export default class EntryAbility extends UIAbility { private handleWidgetParam(want: Want): void { const params = want.parameters; if (params !== undefined) { + // ⭐ 白名单:日程卡片发 'add',待办卡片发 'addTodo'(新建待办)/ 'openTodo'(进待办页)。 + // 之前只认 'add',待办卡片点 + 毫无反应 —— 参数被静默丢弃。 const raw = params['widgetAction']; - if (typeof raw === 'string' && raw === 'add') { - AppStorage.setOrCreate('widgetAction', 'add'); - LogUtil.write('卡片添加入口:已设置 widgetAction=add'); + if (typeof raw === 'string' && (raw === 'add' || raw === 'addTodo' || raw === 'openTodo')) { + AppStorage.setOrCreate('widgetAction', raw); + LogUtil.write(`卡片入口:已设置 widgetAction=${raw}`); } // 卡片点击进入日历时,定位到卡片当前显示的那一天(用户可能已翻到其他日期) const od = params['openDate']; diff --git a/entry/src/main/ets/pages/EventEditPage.ets b/entry/src/main/ets/pages/EventEditPage.ets index ce61bfd..cc95e76 100644 --- a/entry/src/main/ets/pages/EventEditPage.ets +++ b/entry/src/main/ets/pages/EventEditPage.ets @@ -1746,11 +1746,21 @@ struct EventEditPage { // 用它而不是自绘按钮 + pasteboard.getData(),原因见 pastePlace() 注释: // READ_PASTEBOARD 是受限权限,普通应用申请会被拒绝。 // PasteButton 无需声明权限、无需弹窗,权限仅在 onClick 内有效。 - PasteButton() + // ⭐ PasteButton:安全控件,点击时临时获得剪贴板读权限 + // (无需申请 READ_PASTEBOARD —— 那是受限权限,普通应用申请会被拒) + // + // ⚠️ icon/text/buttonType **只能走构造参数**,写成链式属性会编译报错。 + // ⚠️ 不要自定义背景色:SDK 明确把"字体颜色与背景相近"列为样式违规 + // (错误码 2),会让人字形灰淡难辨、甚至授权失败。 + // 交给系统默认样式即可,只调尺寸与圆角。 + PasteButton({ + icon: PasteIconStyle.LINES, + text: PasteDescription.PASTE, + buttonType: ButtonType.Capsule + }) .height(44) - .padding({ left: 8, right: 8 }) + .padding({ left: 10, right: 14 }) .borderRadius(12) - .backgroundColor($r('app.color.card_bg')) .onClick(() => { this.pastePlace(); }) diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index 1a0fb99..0b4a1ac 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -291,12 +291,28 @@ struct Index { } } - /** 消费卡片"添加"深链:拉起新建日程页并立即清除标记,避免重复触发 */ + /** + * 消费卡片"添加"深链。 + * + * 三种来源: + * add 日程卡片 + → 新建日程 + * addTodo 待办卡片 + → 新建待办 + * openTodo 待办卡片整卡点击 → 切到待办 Tab + * 处理完立刻清标记,否则页面重建会重复触发。 + */ private consumeWidgetAction(): void { const action: string = AppStorage.get('widgetAction') ?? ''; + if (action === '') { + return; + } + AppStorage.setOrCreate('widgetAction', ''); if (action === 'add') { - AppStorage.setOrCreate('widgetAction', ''); this.addEvent(); + } else if (action === 'addTodo') { + this.mode = 'todo'; + this.openTodoCreator(); + } else if (action === 'openTodo') { + this.mode = 'todo'; } } diff --git a/entry/src/main/ets/pages/widget/TodoWidget6x4.ets b/entry/src/main/ets/pages/widget/TodoWidget6x4.ets index 688d4b4..a88ece8 100644 --- a/entry/src/main/ets/pages/widget/TodoWidget6x4.ets +++ b/entry/src/main/ets/pages/widget/TodoWidget6x4.ets @@ -83,39 +83,50 @@ struct TodoWidget6x4Card { .onClick(() => this.checkTodo(id)) } - /** 单条待办(宽版):勾选框 + 色条 + 标题 + 截止时间列 + 日历本名列 */ + /** + * 单条待办:勾选框 + 色条 + 标题 + 副行(截止时间 · 日历本)。 + * + * ⚠️ 与 TodoWidget4x4 的 todoRow **保持完全一致**(用户要求两个卡片显示效果一模一样)。 + * 改这里必须同步改那边,否则又会不一致。 + * 选这个结构而不是"独立列 + 固定宽度":标题 layoutWeight(1) 能吃满剩余宽度, + * 在窄卡片里也不会被挤出屏幕。 + */ @Builder todoRow(item: TodoItem6x4) { Row({ space: 8 }) { this.checkbox(item.id) Column() .width(3) - .height(34) + .height(32) .borderRadius(2) .backgroundColor(item.color) - Text(item.title) - .fontSize(13) - .fontColor('#1A1A1A') - .maxLines(2) - .textOverflow({ overflow: TextOverflow.Ellipsis }) - .layoutWeight(1) - Text(item.dueText) - .fontSize(11) - .fontColor(item.dueText.indexOf('逾期') >= 0 ? '#FF3B30' : '#8A8A8A') - .maxLines(1) - .width(88) - .textAlign(TextAlign.End) - Text(item.calName) - .fontSize(10) - .fontColor(item.color) - .maxLines(1) - .textOverflow({ overflow: TextOverflow.Ellipsis }) - .width(80) - .textAlign(TextAlign.End) + Column({ space: 3 }) { + Text(item.title) + .fontSize(13) + .fontColor('#1A1A1A') + .maxLines(2) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + Row({ space: 6 }) { + Text(item.dueText) + .fontSize(10) + .fontColor(item.dueText.indexOf('逾期') >= 0 ? '#FF3B30' : '#8A8A8A') + .maxLines(1) + if (item.calName !== '') { + Text(item.calName) + .fontSize(10) + .fontColor(item.color) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .constraintSize({ maxWidth: '55%' }) + } + } + } + .layoutWeight(1) + .alignItems(HorizontalAlign.Start) } .alignItems(VerticalAlign.Center) .width('100%') - .padding({ left: 4, right: 12, top: 6, bottom: 6 }) + .padding({ left: 4, right: 10, top: 6, bottom: 6 }) .borderRadius(10) .backgroundColor('#F5F7FA') } diff --git a/entry/src/main/ets/pages/widget/Widget4x2.ets b/entry/src/main/ets/pages/widget/Widget4x2.ets index 89ba118..57256d7 100644 --- a/entry/src/main/ets/pages/widget/Widget4x2.ets +++ b/entry/src/main/ets/pages/widget/Widget4x2.ets @@ -26,11 +26,10 @@ struct Widget4x2Card { /** 卡片当前显示的那一天 0 点毫秒 —— 点卡片进 App 时定位到这一天 */ @LocalStorageProp('selectedDate') selectedDate: number = 0; - /** 右侧最多渲染 2 条(数据侧已裁剪,这里再兜底截断) */ + /** 右侧列表数据(可滚动;数据侧已按时间升序并限制在 15 条内) */ private parseOngoing(): CardItem2x4[] { try { - const all: CardItem2x4[] = JSON.parse(this.ongoingJson) as CardItem2x4[]; - return all.slice(0, 2); + return JSON.parse(this.ongoingJson) as CardItem2x4[]; } catch (err) { return []; } @@ -102,7 +101,7 @@ struct Widget4x2Card { } .alignItems(VerticalAlign.Center) .width('100%') - .padding({ left: 8, right: 8, top: 5, bottom: 5 }) + .padding({ left: 6, right: 8, top: 4, bottom: 4 }) .borderRadius(8) .backgroundColor('#F5F7FA') } @@ -183,19 +182,24 @@ struct Widget4x2Card { .strokeWidth(0.5) .color('#E5E5E5') - // ===== 右半:当前时间之后最近的两条日程(不滚动)===== + // ===== 右半:未来日程列表(可滚动,最多 15 条)===== + // ⭐ 原来固定 Column + 只取 2 条,用户想看后天的安排就看不到。 + // 改为 List:自带虚拟化与滚动惯性,条目多也不占内存。 Column() { if (this.parseOngoing().length === 0) { this.emptyHint() } else { - Column({ space: 6 }) { + List({ space: 4 }) { ForEach(this.parseOngoing(), (item: CardItem2x4, idx: number) => { - this.ongoingRow(item) - }, (item: CardItem2x4, idx: number) => `${idx}_${item.title}_${item.time}`) + ListItem() { + this.ongoingRow(item) + } + }, (item: CardItem2x4, idx: number) => `o${idx}_${item.time}_${item.title}`) } .width('100%') .layoutWeight(1) - .justifyContent(FlexAlign.Center) + .scrollBar(BarState.Off) + .edgeEffect(EdgeEffect.Spring) } } .layoutWeight(1)