// entry/src/main/ets/pages/widget/Widget6x4.ets // 6x4 服务卡片:今日时间轴 —— 与"列表视图"完全同一套逻辑,并且**可以上下滑动**: // · 用 List 包一个"很高的 ListItem"(整日时间轴按真实 vp 高度撑开) // —— 官方卡片能力清单里 List / ListItem 是支持的,Scroll 不支持,所以只能走 List。 // · 视窗截断:只渲染"最早日程 ~ 最晚日程(+ 当前时刻)"这一段,避免上下大片空白 // · 三层堆叠:网格层 / 日程层 / 红线层,色块永不被切割 // · 贪心分列:列数 = 最大同时重叠数 // · 全天日程同样用色块展示 // ListItem 内必须用**确定 vp 高度**(不能再用百分比,否则撑不开 → 无法滚动)。 let storage6x4 = new LocalStorage(); /** 时间轴色块(与 common/TimelineUtil.TimelineBlock 结构一致) */ class TBlock6 { eventKey: string = ''; title: string = ''; timeText: string = ''; location: string = ''; // 地点(第 2/3 行显示) writable: boolean = true; // false → 标题后挂白框「只读」小标 color: string = '#007DFF'; topRatio: number = 0; heightRatio: number = 0; leftRatio: number = 0; widthRatio: number = 1; groupIndex: number = 0; isNow: boolean = false; } /** 全天日程(eventKey 由 CardDataService 写入,用于 ForEach 唯一标识) */ class TAllDay6 { eventKey: string = ''; title: string = ''; color: string = '#007DFF'; isAllDay: boolean = true; } /** 列表模式单条日程(解析 listJson) */ class LItem6 { title: string = ''; time: string = ''; endTime: string = ''; color: string = '#007DFF'; calName: string = ''; isNow: boolean = false; } /** 冲突组:组内**贪心分列**,lanes[i] = 第 i 列(同列互不重叠) */ class TGroup6 { startRatio: number = 0; endRatio: number = 0; laneCount: number = 1; blocks: TBlock6[] = []; lanes: TBlock6[][] = []; } /** 日程层纵向序列中的一行:空隙 / 冲突组(线都在其它两层) */ class T6Row { kind: number = 0; // 0=空隙 1=冲突组 from: number = 0; to: number = 0; group: TGroup6 = new TGroup6(); key: string = ''; } /** 视窗范围(小时) */ class W6Range { s: number = 0; e: number = 0; } /** 每 1 小时的高度(vp)。整日 24h × 40 = 960vp,远超卡片高度 → 可以滑动 */ const W6_HOUR: number = 40; /** 全天日程最多显示条数(超出折叠为"还有 N 个") */ const W6_ALLDAY_MAX: number = 2; /** 左侧刻度占宽 */ const W6_GUTTER: number = 36; /** 全天色块单条高度 / 间距 */ const W6_ALLDAY_H: number = 20; @Entry(storage6x4) @Component struct Widget6x4Card { @LocalStorageProp('dateText') dateText: string = ''; @LocalStorageProp('lunarText') lunarText: string = ''; @LocalStorageProp('timelineJson') timelineJson: string = '[]'; @LocalStorageProp('allDayJson') allDayJson: string = '[]'; @LocalStorageProp('nowRatio') nowRatio: number = 0; @LocalStorageProp('nowLabel') nowLabel: string = ''; @LocalStorageProp('todayCount') todayCount: number = 0; @LocalStorageProp('isToday') isToday: boolean = true; @LocalStorageProp('dayCount') dayCount: number = 0; @LocalStorageProp('listJson') listJson: string = '[]'; @LocalStorageProp('displayStyle') displayStyle: string = 'timeline'; @LocalStorageProp('selectedDate') selectedDate: number = 0; // 卡片当前显示日 0 点毫秒 private parseBlocks(): TBlock6[] { try { return JSON.parse(this.timelineJson) as TBlock6[]; } catch (err) { return []; } } /** 当前时间比例(无效返回 -1) */ private w6NowR(): number { return (this.nowRatio > 0.001 && this.nowRatio < 0.999) ? this.nowRatio : -1; } private w6IsNowHour(h: number): boolean { const r: number = this.w6NowR(); return r >= 0 && Math.floor(r * 24) === h; } /** 视窗范围:**8:00~18:00 恒显示**;仅当 8 点前 / 18 点后有日程时才向该侧扩展。 * (与 App 内时间轴 TimelineUtil.viewRange 同一口径,避免两处漂移。) */ private w6Range(): W6Range { const rg = new W6Range(); const bs: TBlock6[] = this.parseBlocks(); let s: number = 8; let e: number = 18; for (const b of bs) { const bh: number = Math.floor(b.topRatio * 24); const eh: number = Math.ceil((b.topRatio + b.heightRatio) * 24 - 0.0001); if (bh < s) { s = bh; } if (eh > e) { e = eh; } } const r: number = this.w6NowR(); // 只有"今天 + 还有未结束的定时日程"时才把当前小时并入视窗(红线会显示); // 且仅在该侧本来就有日程时才允许继续向外扩(不因当前时刻把 8 点前/18 点后空档重新显示) const nh: number = (r >= 0 && !this.w6AllTimedEnded()) ? Math.floor(r * 24) : -1; if (nh >= 0 && nh < s && s < 8) { s = nh; } if (nh >= 0 && nh + 1 > e && e > 18) { e = nh + 1; } if (s < 0) { s = 0; } if (e > 24) { e = 24; } if (e <= s) { e = s + 1 > 24 ? 24 : s + 1; } rg.s = s; rg.e = e; return rg; } /** 视窗起始小时(取 w6Range 的起点,向下取整) */ private w6StartHour(): number { return this.w6Range().s; } /** 视窗结束小时(取 w6Range 的终点,向上取整) */ private w6EndHour(): number { return this.w6Range().e; } /** 视窗跨度(小时) */ private w6Span(): number { return this.w6EndHour() - this.w6StartHour(); } /** 时间轴内容总高(vp)= 跨度 × 每小时高度 */ private w6ContentH(): number { return this.w6Span() * W6_HOUR; } /** 一段(起止为当日比例)换算成 vp 高度 */ private w6SpanVp(from: number, to: number): number { const v: number = (to - from) * 24 * W6_HOUR; return v > 0 ? v : 0; } /** 全天区高度(vp),没有全天日程时为 0 */ private w6AllDayH(): number { const n: number = this.w6AllDay().length; if (n === 0) { return 0; } let h: number = n * W6_ALLDAY_H; if (this.w6AllDayRest() > 0) { h += 14; } return h + 4; } /** ListItem 总高(vp)= 全天区 + 时间轴 */ private w6ItemH(): number { return this.w6AllDayH() + this.w6ContentH(); } /** 由色块重建冲突分组:组内按开始时间升序做**贪心分列**(列数 = 最大同时重叠数) */ private w6Groups(): TGroup6[] { const map: Map = new Map(); for (const b of this.parseBlocks()) { const arr: TBlock6[] | undefined = map.get(b.groupIndex); if (arr === undefined) { map.set(b.groupIndex, [b]); } else { arr.push(b); } } const idxs: number[] = Array.from(map.keys()).sort((a: number, b: number): number => a - b); const out: TGroup6[] = []; for (const gi of idxs) { const bs: TBlock6[] = map.get(gi) ?? []; bs.sort((a: TBlock6, b: TBlock6): number => { if (a.topRatio !== b.topRatio) { return a.topRatio - b.topRatio; } return b.heightRatio - a.heightRatio; }); const lanes: TBlock6[][] = []; const laneEnd: number[] = []; for (const b of bs) { let li: number = -1; for (let i = 0; i < laneEnd.length; i++) { if (laneEnd[i] <= b.topRatio + 0.0000001) { li = i; break; } } const be: number = b.topRatio + b.heightRatio; if (li < 0) { li = lanes.length; lanes.push([b]); laneEnd.push(be); } else { lanes[li].push(b); laneEnd[li] = be; } } const g = new TGroup6(); g.blocks = bs; g.lanes = lanes; g.laneCount = lanes.length > 0 ? lanes.length : 1; let s: number = 1; let e: number = 0; for (const b of bs) { if (b.topRatio < s) { s = b.topRatio; } const be: number = b.topRatio + b.heightRatio; if (be > e) { e = be; } } g.startRatio = s; g.endRatio = e; out.push(g); } return out; } /** 日程层纵向序列:空隙 / 冲突组(不含任何"线") */ private w6Rows(): T6Row[] { const rows: T6Row[] = []; const gs: TGroup6[] = this.w6Groups(); const top: number = this.w6StartHour() / 24; const bot: number = this.w6EndHour() / 24; let cursor: number = top; for (const g of gs) { const gsx: number = g.startRatio > top ? g.startRatio : top; const gex: number = g.endRatio < bot ? g.endRatio : bot; if (gex - gsx <= 0.0002) { continue; } this.w6Push(rows, cursor, gsx, null); this.w6Push(rows, gsx, gex, g); cursor = gex; } this.w6Push(rows, cursor, bot, null); return rows; } private w6Push(rows: T6Row[], from: number, to: number, g: TGroup6 | null): void { if (to - from <= 0.0002) { return; } const r = new T6Row(); r.from = from; r.to = to; if (g === null) { r.kind = 0; r.key = `g_${rows.length}_${Math.round((to - from) * 100000)}`; } else { r.kind = 1; r.group = g; // key 必须绑定"内容":翻页后行序号不变、只有内容变了; // 若只用序号,ArkUI 会认为是同一批元素而不重绘 → 残留上一天的数据 r.key = `b_${rows.length}_${this.w6GroupKey(g, from, to)}`; } rows.push(r); } /** 冲突组的内容签名(时间窗 + 各块 eventKey),供 ForEach key 使用 */ private w6GroupKey(g: TGroup6, from: number, to: number): string { let sig: string = `${Math.round(from * 100000)}_${Math.round(to * 100000)}`; for (const b of g.blocks) { sig = `${sig}_${b.eventKey}`; } return sig; } /** 列内第 index 个色块之前的空隙(vp)= 本块顶 − 上一块底 */ private w6LanePadVp(g: TGroup6, lane: TBlock6[], index: number): number { let prevEnd: number = g.startRatio; if (index > 0) { const p: TBlock6 = lane[index - 1]; const pe: number = p.topRatio + p.heightRatio; prevEnd = pe > g.startRatio ? pe : g.startRatio; } return this.w6SpanVp(prevEnd, lane[index].topRatio); } /** 色块高度(vp) */ private w6BlockVp(b: TBlock6): number { return b.heightRatio * 24 * W6_HOUR; } /** 色块信息行数(标题永远打头):1=标题+时间+地址同行;2=标题 + 时间·地点;3=标题 / 时间 / 地址 */ private w6Lines(b: TBlock6): number { const h: number = this.w6BlockVp(b); if (h >= 44 && b.location !== '') { return 3; } if (h >= 30) { return 2; } return 1; } /** 两行色块的第二行:'09:00 - 10:30 · 地点'(无地点时只剩时间) */ private w6Meta(b: TBlock6): string { return b.location !== '' ? `${b.timeText} · ${b.location}` : b.timeText; } /** 今天定时日程是否已全部结束(最晚结束比例 <= 当前时刻比例);只剩全天 / 无定时日程也算已结束。 * 非今天(r<0)返回 false,交由原条件判断(非今天本来就不显示红线)。 */ private w6AllTimedEnded(): boolean { const r: number = this.w6NowR(); if (r < 0) { return false; } let maxEnd: number = 0; for (const b of this.parseBlocks()) { const e: number = b.topRatio + b.heightRatio; if (e > maxEnd) { maxEnd = e; } } return r >= maxEnd - 0.0001; } /** 红线是否显示(当前时刻落在视窗内 + 今天还有未结束的定时日程) */ private w6NowVisible(): boolean { const r: number = this.w6NowR(); return r >= 0 && !this.w6AllTimedEnded() && r >= this.w6StartHour() / 24 - 0.0001 && r <= this.w6EndHour() / 24 + 0.0001; } /** 红线距内容顶部的偏移(vp) */ private w6NowPadVp(): number { const v: number = (this.w6NowR() - this.w6StartHour() / 24) * 24 * W6_HOUR; return v > 0 ? v : 0; } private parseAllDay(): TAllDay6[] { try { return JSON.parse(this.allDayJson) as TAllDay6[]; } catch (err) { return []; } } private w6AllDay(): TAllDay6[] { return this.parseAllDay().slice(0, W6_ALLDAY_MAX); } private w6AllDayRest(): number { const n: number = this.parseAllDay().length - W6_ALLDAY_MAX; return n > 0 ? n : 0; } private hourLabels(): number[] { const arr: number[] = []; for (let h = this.w6StartHour(); h < this.w6EndHour(); h++) { arr.push(h); } return arr; } private hourText(h: number): string { return h < 10 ? `0${h}:00` : `${h}:00`; } /** 右上角添加按钮:拉起 App 直接进入新建日程页(兄弟节点布局,不冒泡) */ @Builder addButton() { Button() { Text('+') .fontSize(16) .fontWeight(FontWeight.Medium) .fontColor('#FFFFFF') } .width(26) .height(26) .borderRadius(13) .padding(0) .backgroundColor('#007DFF') .onClick(() => { postCardAction(this, { action: 'router', abilityName: 'EntryAbility', params: { widgetAction: 'add' } }); }) } private openApp(): void { postCardAction(this, { action: 'router', abilityName: 'EntryAbility', // 携带卡片当前显示日(用户可能已翻到其他日期),进入日历时定位到这一天。 // 必须转成字符串:param 的数值按 int32 传递,13 位毫秒时间戳会溢出成 2147483647(曾导致定位到 1970) params: { openDate: `${this.selectedDate}` } }); } /** 翻页:交给 FormExtensionAbility.onFormEvent 重新取数并推送(跨天查看) */ private page(act: string): void { postCardAction(this, { action: 'message', params: { pageAction: act } }); } /** 上一天 / 下一天 圆按钮 */ @Builder private pageBtn(label: string, act: string) { Button() { Text(label) .fontSize(14) .fontWeight(FontWeight.Medium) .fontColor('#333333') } .width(22) .height(22) .borderRadius(11) .padding(0) .backgroundColor('#F2F3F5') .onClick(() => this.page(act)) } /** 非今天时额外给一个"回到今天"按钮 */ @Builder private sideBtn() { if (!this.isToday) { Button() { Text('今') .fontSize(12) .fontWeight(FontWeight.Medium) .fontColor('#FFFFFF') } .width(22) .height(22) .borderRadius(11) .padding(0) .backgroundColor('#007DFF') .onClick(() => this.page('today')) } } /** 列表模式单条日程(解析 listJson) */ private parseList(): LItem6[] { try { return JSON.parse(this.listJson) as LItem6[]; } catch (err) { return []; } } /** 列表模式单条行:左侧日历色条(进行中变红)+ 时间 + 标题 + 所属日历本;不画红线 */ @Builder listRow(item: LItem6) { Row({ space: 8 }) { Column() .width(4) .height(item.time === '全天' ? 18 : 42) .borderRadius(2) .backgroundColor(item.isNow ? '#FF3B30' : item.color) if (item.time === '全天') { Text(item.title) .fontSize(13) .fontColor('#1A1A1A') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .layoutWeight(1) Text('全天') .fontSize(9) .fontColor('#FFFFFF') .backgroundColor(item.color) .borderRadius(6) .padding({ left: 5, right: 5, top: 1, bottom: 1 }) } else { Column({ space: 2 }) { Text(item.time) .fontSize(11) .fontWeight(FontWeight.Medium) .fontColor(item.isNow ? '#FF3B30' : '#333333') Text(item.endTime) .fontSize(11) .fontColor('#999999') } .width(42) .alignItems(HorizontalAlign.Center) Text(item.title) .fontSize(13) .fontColor(item.isNow ? '#FF3B30' : '#1A1A1A') .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) .layoutWeight(1) if (item.isNow) { Text('● 进行中') .fontSize(9) .fontColor('#FF3B30') .padding({ left: 4, right: 4, top: 1, bottom: 1 }) .borderRadius(4) .backgroundColor('#FFECEA') } } if (item.calName !== '') { Text(item.calName) .fontSize(9) .fontColor(item.color) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .constraintSize({ maxWidth: '25%' }) } } .alignItems(VerticalAlign.Center) .width('100%') .padding({ left: 8, right: 8, top: 6, bottom: 6 }) .borderRadius(8) .backgroundColor(item.isNow ? '#FFF1F0' : '#F5F7FA') } /** 列表模式主体:空态 / 可滚动的"行"列表(无时间轴、无红线) */ @Builder listBody() { if (this.parseList().length === 0) { Column({ space: 6 }) { Text('📅') .fontSize(24) Text('暂无日程') .fontSize(13) .fontColor('#8A8A8A') } .width('100%') .layoutWeight(1) .justifyContent(FlexAlign.Center) .onClick(() => this.openApp()) } else { List({ space: 4 }) { ForEach(this.parseList(), (item: LItem6) => { ListItem() { this.listRow(item) } .width('100%') }, (item: LItem6) => `${item.time}_${item.title}_${item.color}`) } .width('100%') .layoutWeight(1) .scrollBar(BarState.Auto) .cachedCount(8) .onClick(() => this.openApp()) } } build() { Column({ space: 6 }) { Row({ space: 6 }) { this.pageBtn('‹', 'prev') Column() { Text(this.dateText) .fontSize(13) .fontWeight(FontWeight.Bold) .fontColor(this.isToday ? '#007DFF' : '#1A1A1A') .maxLines(1) Text(this.lunarText) .fontSize(10) .fontColor('#8A8A8A') .maxLines(1) } .layoutWeight(1) .alignItems(HorizontalAlign.Start) .onClick(() => this.openApp()) Text(`${this.dayCount} 条`) .fontSize(10) .fontColor('#B0B0B0') this.pageBtn('›', 'next') this.sideBtn() this.addButton() } .width('100%') Divider().strokeWidth(0.5).color('#E5E5E5') if (this.displayStyle === 'list') { this.listBody() } else { // 卡片不支持 Scroll,但支持 List / ListItem: // 把"整日时间轴"作为**一个很高的 ListItem**,超出卡片窗口的部分靠上下滑动查看 List() { ListItem() { Column() { // 全天 / 跨天:同样用**色块**展示(与列表视图一致),随内容一起滚动 if (this.w6AllDay().length > 0) { Column({ space: 2 }) { ForEach(this.w6AllDay(), (a: TAllDay6) => { Row({ space: 5 }) { Text(a.isAllDay ? '全天' : '跨天') .fontSize(8) .fontColor('#FFFFFF') .backgroundColor('#26000000') .borderRadius(5) .padding({ left: 4, right: 4, top: 1, bottom: 1 }) Text(a.title) .fontSize(11) .fontWeight(FontWeight.Medium) .fontColor('#FFFFFF') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .layoutWeight(1) } .alignItems(VerticalAlign.Center) .width('100%') .height(W6_ALLDAY_H - 2) .padding({ left: 6, right: 6 }) .borderRadius(6) .backgroundColor(a.color) .onClick(() => this.openApp()) }, (a: TAllDay6, idx: number) => `ad_${idx}_${a.eventKey}`) if (this.w6AllDayRest() > 0) { Text(`还有 ${this.w6AllDayRest()} 个全天日程`) .fontSize(9) .fontColor('#8A8A8A') .width('100%') .padding({ left: 2 }) } } .width('100%') .padding({ bottom: 2 }) } // 时间轴主体:左侧小时刻度(放在 Stack 外,避免被色块盖住)+ 右侧三层堆叠 Row() { Column() { ForEach(this.hourLabels(), (h: number) => { Text(this.hourText(h)) .fontSize(9) .fontColor(this.w6IsNowHour(h) ? '#FF3B30' : '#9AA0A6') .width(W6_GUTTER) .height(W6_HOUR) .textAlign(TextAlign.End) .padding({ right: 3 }) .border({ width: { bottom: 0.5 }, color: { bottom: '#14000000' } }) }, (h: number) => `h${h}`) } .width(W6_GUTTER) .height(this.w6ContentH()) Stack() { this.w6GridLayer() // 第 1 层:整点网格线 this.w6EventLayer() // 第 2 层:日程色块 this.w6NowLayer() // 第 3 层:当前时间红线 } .layoutWeight(1) .height(this.w6ContentH()) .alignContent(Alignment.TopStart) .border({ width: { left: 0.5 }, color: { left: '#14000000' } }) } .width('100%') .height(this.w6ContentH()) .alignItems(VerticalAlign.Top) } .width('100%') .height(this.w6ItemH()) } .width('100%') .height(this.w6ItemH()) } .layoutWeight(1) .width('100%') } } .width('100%') .height('100%') .padding(12) .backgroundColor('#FFFFFF') .borderRadius(16) } /** 第 1 层:整点网格(每小时一格,格底一条灰线) */ @Builder private w6GridLayer() { Column() { ForEach(this.hourLabels(), (h: number) => { Column() .width('100%') .height(W6_HOUR) .border({ width: { bottom: 0.5 }, color: { bottom: '#14000000' } }) .hitTestBehavior(HitTestMode.None) // 装饰层子节点同样不参与命中测试 }, (h: number) => `gd_${h}`) } .width('100%') .height('100%') .hitTestBehavior(HitTestMode.None) // 装饰层:不参与命中测试,触碰事件穿透到下层色块 } /** 第 2 层:日程色块(冲突组分行 + 组内 lane 分列) */ @Builder private w6EventLayer() { Column() { ForEach(this.w6Rows(), (r: T6Row) => { if (r.kind === 1) { Row() { ForEach(r.group.lanes, (lane: TBlock6[], li: number) => { Column() { ForEach(lane, (b: TBlock6, index: number) => { Blank().height(this.w6LanePadVp(r.group, lane, index)) Column({ space: 1 }) { if (this.w6Lines(b) === 1) { // 一行:标题 · [只读] · 时间 · 地址(时间用完整起止,宽度不够自然省略; // 让位顺序:地址先没 → 时间省略 → 标题只保 50%) Row({ space: 4 }) { Text(b.title) .fontSize(10) .fontWeight(FontWeight.Medium) .fontColor('#FFFFFF') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .constraintSize({ maxWidth: '50%' }) .lineHeight(12) if (!b.writable) { Text('只读') .fontSize(8) .fontColor('#FFFFFF') .border({ width: 0.5, color: '#FFFFFF' }) .borderRadius(3) .padding({ left: 4, right: 4, top: 0, bottom: 0 }) .maxLines(1) } Text(b.timeText) .fontSize(9) .fontColor('#E6FFFFFF') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .constraintSize({ maxWidth: '56%' }) .lineHeight(12) if (b.location !== '') { Text(b.location) .fontSize(9) .fontColor('#B3FFFFFF') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .lineHeight(12) .layoutWeight(1) } } .width('100%') .alignItems(VerticalAlign.Center) } else { // 第一行:标题 · [只读] Row({ space: 4 }) { Text(b.title) .fontSize(11) .fontWeight(FontWeight.Medium) .fontColor('#FFFFFF') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .constraintSize({ maxWidth: '70%' }) if (!b.writable) { Text('只读') .fontSize(9) .fontColor('#FFFFFF') .border({ width: 0.5, color: '#FFFFFF' }) .borderRadius(4) .padding({ left: 4, right: 4, top: 0, bottom: 0 }) .maxLines(1) } } .width('100%') .alignItems(VerticalAlign.Center) if (this.w6Lines(b) >= 3) { Text(b.timeText) .fontSize(9) .fontColor('#E6FFFFFF') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .width('100%') Text(b.location) .fontSize(9) .fontColor('#B3FFFFFF') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .width('100%') } else { Text(this.w6Meta(b)) .fontSize(9) .fontColor('#E6FFFFFF') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .width('100%') } } } .alignItems(HorizontalAlign.Start) .padding({ left: 5, right: 3, top: 1, bottom: 1 }) .borderRadius(4) .backgroundColor(b.color) .opacity(b.isNow ? 1 : 0.92) .clip(true) .width('100%') .height(this.w6BlockVp(b)) .constraintSize({ minHeight: 16 }) .onClick(() => this.openApp()) }, (b: TBlock6) => `b_${r.key}_${b.eventKey}`) } .layoutWeight(1) .height('100%') .padding({ right: 2 }) .clip(true) }, (lane: TBlock6[], li: number) => `ln_${r.key}_${li}`) } .width('100%') .height(this.w6SpanVp(r.from, r.to)) .alignItems(VerticalAlign.Top) } else { Blank().height(this.w6SpanVp(r.from, r.to)) } }, (r: T6Row) => r.key) } .width('100%') .height('100%') } /** 第 3 层:当前时间红线(浮在最上层) */ @Builder private w6NowLayer() { Column() { if (this.w6NowVisible()) { Blank().height(this.w6NowPadVp()).hitTestBehavior(HitTestMode.None) // 占位块:不拦截点击 Row() { Column() .width(5) .height(5) .borderRadius(3) .backgroundColor('#FF3B30') Column() .height(2) .layoutWeight(1) .backgroundColor('#FF3B30') .borderRadius(1) } .width('100%') .hitTestBehavior(HitTestMode.None) // 红线本身也不拦截点击,穿透到色块层 } } .width('100%') .height('100%') .hitTestBehavior(HitTestMode.None) // 装饰层:整层不参与命中测试,触碰事件穿透到下层色块 } }