0.0.8:卡片整卡可点 —— 点空白处也能进 App 并定位到当天

用户反馈:卡片的空白区域点了没反应,只有落在日程/文字上才跳转。
期望:点卡片**任何位置**都进入 App 并**定位到卡片当前显示的那一天**。

实现(4 张卡片:2x2 / 2x4 / 4x4 / 6x4 一致处理):
1. 根容器加 `.onClick(() => this.openApp())` → **整卡可点**(含留白);
2. `openApp()` 携带 `openDate` 深链参数,值为卡片数据里的 `selectedDate`
   (该字段早已在 CardData 中维护:CardDataService.ets:194 = 卡片当前显示日 0 点毫秒);
3. **传参必须转字符串**:`params: { openDate: `${this.selectedDate}` }`。
   ⚠️ 依据 4x4 原有注释:param 的数值按 **int32** 传递,13 位毫秒时间戳会溢出成
   2147483647(曾导致进 App 后定位到 1970)。2x2/4x2 原本传的是 number → 已修正为字符串。
4. **移除内部原有的 openApp 点击**(共 13 处:4x2 四、4x4 四、6x4 五),
   避免"子区域 + 根容器"双重触发同一动作。

关于事件冒泡(重要,避免误改):
- ArkUI 的 `onClick` 是**非冒泡**事件,父子组件同绑时**子组件优先**,同一时刻只有一个响应
  (华为官方论坛结论)。因此:
  · 根容器整卡可点**不会**抢走内部 ‹ › 今 + 按钮的点击;
  · **无需** `stopPropagation`(ClickEvent 上也**没有**这个方法——它只在
    MouseEvent / TouchEvent 上,实测编译报错已验证)。
- 内部原有的 4x4/6x4 注释写"兄弟节点布局不冒泡"是对的;本次统一补上说明。

验证:
- 编译通过(BUILD SUCCESSFUL)。
- 四卡一致性核验:每卡 openApp 点击点=1(仅根容器)、根容器=1、selectedDate 声明=1、
  openDate 字符串传参=1。
- 结构完整性:花括号配平(26/26、53/53、228/228、228/228),编译无语法错误。

⚠️ 未验证(待真机):实际点击留白是否跳转、是否正确定位到所显示的那一天;
以及点 ‹ › 今 + 是否**不会**连带打开 App(依赖上述"子组件优先"结论,需实测确认)。
This commit is contained in:
2026-10-14 22:45:51 +08:00
parent bd3afb7bd1
commit c4dc27ef3a
4 changed files with 31 additions and 23 deletions
@@ -11,8 +11,12 @@ struct Widget2x2Card {
@LocalStorageProp('dateMd') dateMd: string = ''; @LocalStorageProp('dateMd') dateMd: string = '';
@LocalStorageProp('weekday') weekday: string = ''; @LocalStorageProp('weekday') weekday: string = '';
@LocalStorageProp('todayCount') todayCount: number = 0; @LocalStorageProp('todayCount') todayCount: number = 0;
/** 卡片当前显示的那一天 0 点毫秒 —— 点卡片进 App 时定位到这一天 */
@LocalStorageProp('selectedDate') selectedDate: number = 0;
/** 右上角添加按钮:拉起 App 直接进入新建日程页(阻止冒泡,避免同时打开 App 首页) */ /** 右上角添加按钮:拉起 App 直接进入新建日程页。
* ⭐ 无需阻止冒泡:ArkUI 的 `onClick` 是**非冒泡**事件,父子同绑时**子组件优先**,
* 故点 + 不会连带触发根容器的"打开首页"。 */
@Builder @Builder
addButton() { addButton() {
Button() { Button() {
@@ -35,12 +39,12 @@ struct Widget2x2Card {
}) })
} }
/** 卡片"打开 App":点击日期区或今日日程数区域触发;添加按钮是 header 行的兄弟节点,其点击不会冒泡到这里 */ /** 打开 App 并**定位到卡片当前显示的那一天**(用户可能已翻到其他日期) */
private openApp(): void { private openApp(): void {
postCardAction(this, { postCardAction(this, {
action: 'router', action: 'router',
abilityName: 'EntryAbility', abilityName: 'EntryAbility',
params: {} params: { openDate: `${this.selectedDate}` }
}); });
} }
@@ -61,7 +65,6 @@ struct Widget2x2Card {
} }
.layoutWeight(1) .layoutWeight(1)
.alignItems(HorizontalAlign.Start) .alignItems(HorizontalAlign.Start)
.onClick(() => this.openApp())
this.addButton() this.addButton()
} }
@@ -92,12 +95,13 @@ struct Widget2x2Card {
.width('100%') .width('100%')
.layoutWeight(1) .layoutWeight(1)
.alignItems(VerticalAlign.Center) .alignItems(VerticalAlign.Center)
.onClick(() => this.openApp())
} }
.width('100%') .width('100%')
.height('100%') .height('100%')
.padding(14) .padding(14)
.backgroundColor('#FFFFFF') .backgroundColor('#FFFFFF')
.borderRadius(16) .borderRadius(16)
// ⭐ 整卡可点:空白处也能进 App 并定位到当前显示日(原来只有日期区/条数区可点)
.onClick(() => this.openApp())
} }
} }
@@ -23,6 +23,8 @@ struct Widget4x2Card {
@LocalStorageProp('lunarText') lunarText: string = ''; @LocalStorageProp('lunarText') lunarText: string = '';
@LocalStorageProp('todayCount') todayCount: number = 0; @LocalStorageProp('todayCount') todayCount: number = 0;
@LocalStorageProp('ongoingJson') ongoingJson: string = '[]'; @LocalStorageProp('ongoingJson') ongoingJson: string = '[]';
/** 卡片当前显示的那一天 0 点毫秒 —— 点卡片进 App 时定位到这一天 */
@LocalStorageProp('selectedDate') selectedDate: number = 0;
/** 右侧最多渲染 2 条(数据侧已裁剪,这里再兜底截断) */ /** 右侧最多渲染 2 条(数据侧已裁剪,这里再兜底截断) */
private parseOngoing(): CardItem2x4[] { private parseOngoing(): CardItem2x4[] {
@@ -57,12 +59,12 @@ struct Widget4x2Card {
}) })
} }
/** 卡片"打开 App":点击左侧日期区、今日条数区、右侧日程区触发;添加按钮是 header 行兄弟节点,不冒泡 */ /** 打开 App 并**定位到卡片当前显示的那一天**(用户可能已翻到其他日期) */
private openApp(): void { private openApp(): void {
postCardAction(this, { postCardAction(this, {
action: 'router', action: 'router',
abilityName: 'EntryAbility', abilityName: 'EntryAbility',
params: {} params: { openDate: `${this.selectedDate}` }
}); });
} }
@@ -118,7 +120,6 @@ struct Widget4x2Card {
.width('100%') .width('100%')
.layoutWeight(1) .layoutWeight(1)
.justifyContent(FlexAlign.Center) .justifyContent(FlexAlign.Center)
.onClick(() => this.openApp())
} }
build() { build() {
@@ -141,7 +142,6 @@ struct Widget4x2Card {
} }
.layoutWeight(1) .layoutWeight(1)
.alignItems(HorizontalAlign.Start) .alignItems(HorizontalAlign.Start)
.onClick(() => this.openApp())
this.addButton() this.addButton()
} }
@@ -173,7 +173,6 @@ struct Widget4x2Card {
.width('100%') .width('100%')
.layoutWeight(1) .layoutWeight(1)
.alignItems(VerticalAlign.Center) .alignItems(VerticalAlign.Center)
.onClick(() => this.openApp())
} }
.layoutWeight(1) .layoutWeight(1)
.height('100%') .height('100%')
@@ -197,7 +196,6 @@ struct Widget4x2Card {
.width('100%') .width('100%')
.layoutWeight(1) .layoutWeight(1)
.justifyContent(FlexAlign.Center) .justifyContent(FlexAlign.Center)
.onClick(() => this.openApp())
} }
} }
.layoutWeight(1) .layoutWeight(1)
@@ -208,5 +206,7 @@ struct Widget4x2Card {
.padding(12) .padding(12)
.backgroundColor('#FFFFFF') .backgroundColor('#FFFFFF')
.borderRadius(16) .borderRadius(16)
// ⭐ 整卡可点:左右空白处也能进 App 并定位到当前显示日
.onClick(() => this.openApp())
} }
} }
@@ -387,7 +387,9 @@ struct Widget4x4Card {
return h < 10 ? `0${h}` : `${h}`; return h < 10 ? `0${h}` : `${h}`;
} }
/** 右上角添加按钮:拉起 App 直接进入新建日程页(兄弟节点布局,不冒泡到其它点击区) */ /** 右上角添加按钮:拉起 App 直接进入新建日程页。
* ⭐ 无需阻止冒泡:ArkUI 的 `onClick` 是**非冒泡**事件,父子同绑时**子组件优先**,
* 故点 + 不会连带触发根容器的"打开首页"。 */
@Builder @Builder
addButton() { addButton() {
Button() { Button() {
@@ -442,6 +444,7 @@ struct Widget4x4Card {
.borderRadius(11) .borderRadius(11)
.padding(0) .padding(0)
.backgroundColor('#F2F3F5') .backgroundColor('#F2F3F5')
// ⭐ 无需阻止冒泡:onClick 非冒泡、子组件优先 → 点翻页不会连带打开 App
.onClick(() => this.page(act)) .onClick(() => this.page(act))
} }
@@ -462,6 +465,7 @@ struct Widget4x4Card {
.borderRadius(11) .borderRadius(11)
.padding(0) .padding(0)
.backgroundColor('#007DFF') .backgroundColor('#007DFF')
// ⭐ 无需阻止冒泡:onClick 非冒泡、子组件优先
.onClick(() => this.page('today')) .onClick(() => this.page('today'))
} }
} }
@@ -554,7 +558,6 @@ struct Widget4x4Card {
.width('100%') .width('100%')
.layoutWeight(1) .layoutWeight(1)
.justifyContent(FlexAlign.Center) .justifyContent(FlexAlign.Center)
.onClick(() => this.openApp())
} else { } else {
List({ space: 4 }) { List({ space: 4 }) {
ForEach(this.parseList(), (item: LItem4) => { ForEach(this.parseList(), (item: LItem4) => {
@@ -568,7 +571,6 @@ struct Widget4x4Card {
.layoutWeight(1) .layoutWeight(1)
.scrollBar(BarState.Auto) .scrollBar(BarState.Auto)
.cachedCount(8) .cachedCount(8)
.onClick(() => this.openApp())
} }
} }
@@ -589,7 +591,6 @@ struct Widget4x4Card {
} }
.layoutWeight(1) .layoutWeight(1)
.alignItems(HorizontalAlign.Start) .alignItems(HorizontalAlign.Start)
.onClick(() => this.openApp())
this.pageBtn('›', 'next') this.pageBtn('›', 'next')
this.sideBtn() this.sideBtn()
} }
@@ -630,7 +631,6 @@ struct Widget4x4Card {
.padding({ left: 5, right: 5 }) .padding({ left: 5, right: 5 })
.borderRadius(5) .borderRadius(5)
.backgroundColor(a.color) .backgroundColor(a.color)
.onClick(() => this.openApp())
}, (a: TAllDay, idx: number) => `ad_${idx}_${a.eventKey}`) }, (a: TAllDay, idx: number) => `ad_${idx}_${a.eventKey}`)
if (this.w4AllDayRest() > 0) { if (this.w4AllDayRest() > 0) {
Text(`还有 ${this.w4AllDayRest()} 个全天日程`) Text(`还有 ${this.w4AllDayRest()} 个全天日程`)
@@ -690,6 +690,9 @@ struct Widget4x4Card {
.padding(12) .padding(12)
.backgroundColor('#FFFFFF') .backgroundColor('#FFFFFF')
.borderRadius(16) .borderRadius(16)
// ⭐ 整卡可点:空白处也能进 App 并定位到当前显示日。
// ⚠️ onClick 是**非冒泡**事件且**子组件优先** → 内部的 ‹ › 今 + 按钮不会被这里抢走
.onClick(() => this.openApp())
} }
/** 第 1 层:整点网格(每小时一格,格底一条灰线) */ /** 第 1 层:整点网格(每小时一格,格底一条灰线) */
@@ -815,7 +818,6 @@ struct Widget4x4Card {
.width('100%') .width('100%')
.height(this.w4BlockVp(b)) .height(this.w4BlockVp(b))
.constraintSize({ minHeight: 14 }) .constraintSize({ minHeight: 14 })
.onClick(() => this.openApp())
}, (b: TBlock) => `b_${r.key}_${b.eventKey}`) }, (b: TBlock) => `b_${r.key}_${b.eventKey}`)
} }
.layoutWeight(1) .layoutWeight(1)
@@ -384,7 +384,9 @@ struct Widget6x4Card {
return h < 10 ? `0${h}:00` : `${h}:00`; return h < 10 ? `0${h}:00` : `${h}:00`;
} }
/** 右上角添加按钮:拉起 App 直接进入新建日程页(兄弟节点布局,不冒泡) */ /** 右上角添加按钮:拉起 App 直接进入新建日程页。
* ⭐ 无需阻止冒泡:ArkUI 的 `onClick` 是**非冒泡**事件,父子同绑时**子组件优先**,
* 故点 + 不会连带触发根容器的"打开首页"。 */
@Builder @Builder
addButton() { addButton() {
Button() { Button() {
@@ -439,6 +441,7 @@ struct Widget6x4Card {
.borderRadius(11) .borderRadius(11)
.padding(0) .padding(0)
.backgroundColor('#F2F3F5') .backgroundColor('#F2F3F5')
// ⭐ 无需阻止冒泡:onClick 非冒泡、子组件优先 → 点翻页不会连带打开 App
.onClick(() => this.page(act)) .onClick(() => this.page(act))
} }
@@ -457,6 +460,7 @@ struct Widget6x4Card {
.borderRadius(11) .borderRadius(11)
.padding(0) .padding(0)
.backgroundColor('#007DFF') .backgroundColor('#007DFF')
// ⭐ 无需阻止冒泡:onClick 非冒泡、子组件优先
.onClick(() => this.page('today')) .onClick(() => this.page('today'))
} }
} }
@@ -549,7 +553,6 @@ struct Widget6x4Card {
.width('100%') .width('100%')
.layoutWeight(1) .layoutWeight(1)
.justifyContent(FlexAlign.Center) .justifyContent(FlexAlign.Center)
.onClick(() => this.openApp())
} else { } else {
List({ space: 4 }) { List({ space: 4 }) {
ForEach(this.parseList(), (item: LItem6) => { ForEach(this.parseList(), (item: LItem6) => {
@@ -563,7 +566,6 @@ struct Widget6x4Card {
.layoutWeight(1) .layoutWeight(1)
.scrollBar(BarState.Auto) .scrollBar(BarState.Auto)
.cachedCount(8) .cachedCount(8)
.onClick(() => this.openApp())
} }
} }
@@ -584,7 +586,6 @@ struct Widget6x4Card {
} }
.layoutWeight(1) .layoutWeight(1)
.alignItems(HorizontalAlign.Start) .alignItems(HorizontalAlign.Start)
.onClick(() => this.openApp())
Text(`${this.dayCount} 条`) Text(`${this.dayCount} 条`)
.fontSize(10) .fontSize(10)
.fontColor('#B0B0B0') .fontColor('#B0B0B0')
@@ -629,7 +630,6 @@ struct Widget6x4Card {
.padding({ left: 6, right: 6 }) .padding({ left: 6, right: 6 })
.borderRadius(6) .borderRadius(6)
.backgroundColor(a.color) .backgroundColor(a.color)
.onClick(() => this.openApp())
}, (a: TAllDay6, idx: number) => `ad_${idx}_${a.eventKey}`) }, (a: TAllDay6, idx: number) => `ad_${idx}_${a.eventKey}`)
if (this.w6AllDayRest() > 0) { if (this.w6AllDayRest() > 0) {
Text(`还有 ${this.w6AllDayRest()} 个全天日程`) Text(`还有 ${this.w6AllDayRest()} 个全天日程`)
@@ -689,6 +689,9 @@ struct Widget6x4Card {
.padding(12) .padding(12)
.backgroundColor('#FFFFFF') .backgroundColor('#FFFFFF')
.borderRadius(16) .borderRadius(16)
// ⭐ 整卡可点:空白处也能进 App 并定位到当前显示日。
// ⚠️ onClick 是**非冒泡**事件且**子组件优先** → 内部的 ‹ › 今 + 按钮不会被这里抢走
.onClick(() => this.openApp())
} }
/** 第 1 层:整点网格(每小时一格,格底一条灰线) */ /** 第 1 层:整点网格(每小时一格,格底一条灰线) */
@@ -814,7 +817,6 @@ struct Widget6x4Card {
.width('100%') .width('100%')
.height(this.w6BlockVp(b)) .height(this.w6BlockVp(b))
.constraintSize({ minHeight: 16 }) .constraintSize({ minHeight: 16 })
.onClick(() => this.openApp())
}, (b: TBlock6) => `b_${r.key}_${b.eventKey}`) }, (b: TBlock6) => `b_${r.key}_${b.eventKey}`)
} }
.layoutWeight(1) .layoutWeight(1)