鸿蒙原生应用 ArkUI 实战:三列三色汇总卡 + 红点竖线时间轴 —— 续费日历页的全年扣款可视化架构
鸿蒙原生应用 ArkUI 实战:三列三色汇总卡 + 红点竖线时间轴 —— 续费日历页的全年扣款可视化架构
App 55「自动续费提醒」日历页(Func2Tab),主题色
#FF6B6B珊瑚红,4 个 Tab 分别为首页(⏰)、添加(➕)、日历(📅)、我的(👤)。本篇基于55-renewal-reminder/entry/src/main/ets/pages/Func2Tab.ets(共 103 行)逐段拆解,附 4 张实机截图。

一、整体结构:三区"Header + 汇总卡 + 时间轴"布局
日历页是"年度总览 + 全年时间轴"的组合布局——3 个 @Builder 块:
build() {
Column() {
this.Header()
Scroll() {
Column({ space: 14 }) {
this.YearSummary()
this.SectionTitle('全年续费时间轴')
this.Timeline()
}
.width('100%')
.padding({ left: D.pad, right: D.pad, top: 14, bottom: D.pad + this.safeBottom + 20 })
}
.layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top)
}
.width('100%').height('100%').backgroundColor(C.bg)
}
3 块结构:YearSummary(三列三色年度汇总)→ SectionTitle(全年续费时间轴)→ Timeline(5 条红点竖线时间轴)。与 App 54 的"月历网格"不同,本页是纵向时间轴——不按日历格子排列,而按时间顺序把全年扣款事件串成一条线。月历看"哪天有空",时间轴看"接下来发生什么",续费场景后者更贴切。
二、数据模型:跨月全年五项
interface RenewItem {
id: number;
emoji: string;
name: string;
month: number;
day: number;
amount: number;
}
private items: RenewItem[] = [
{ id: 1, emoji: '🎬', name: '视频VIP会员', month: 9, day: 1, amount: 25 },
{ id: 2, emoji: '🎵', name: '音乐会员', month: 9, day: 5, amount: 15 },
{ id: 3, emoji: '☁️', name: '云存储服务', month: 9, day: 10, amount: 6 },
{ id: 4, emoji: '📰', name: '新闻订阅', month: 10, day: 3, amount: 12 },
{ id: 5, emoji: '💻', name: '设计软件年费', month: 1, day: 20, amount: 688 }
];
RenewItem 六字段:比首页多了 month——日历页需要跨月表达(9 月三项、10 月一项、1 月一项)。注意排序是"近期优先"而非"月份升序":9月(近)在前、1月(远)在末,时间轴按"距离当前的远近"组织,而非自然月份顺序——这正是提醒类应用的时间观。
三、Header:单行标题
Header() {
Row() { Text('续费日历').fontSize(20).fontWeight(FontWeight.Bold).fontColor(C.text) }
.width('100%').height(this.safeTop + 56).padding({ top: this.safeTop, left: D.pad, right: D.pad })
.backgroundColor(C.card).alignItems(VerticalAlign.Bottom)
}
20 号加粗、safeTop + 56 固定高、文字贴底——系列功能页 Header 统一范式。与 App 54 日历页同名"续费日历",但 54 用月历网格、55 用时间轴,同名不同形。
四、YearSummary:三列三色汇总卡
YearSummary() {
Row() {
Column({ space: 4 }) {
Text('¥746').fontSize(22).fontWeight(FontWeight.Bold).fontColor(C.primary)
Text('年度总额').fontSize(11).fontColor(C.textDim)
}.layoutWeight(1)
Column({ space: 4 }) {
Text('5').fontSize(22).fontWeight(FontWeight.Bold).fontColor(C.accent)
Text('续费项目').fontSize(11).fontColor(C.textDim)
}.layoutWeight(1)
Column({ space: 4 }) {
Text('¥62').fontSize(22).fontWeight(FontWeight.Bold).fontColor(C.ok)
Text('月均').fontSize(11).fontColor(C.textDim)
}.layoutWeight(1)
}
.width('100%').padding({ top: 16, bottom: 16 }).backgroundColor(C.card).borderRadius(D.rLg).border({ width: 1, color: C.stroke })
}
逐点拆解:
- 白底卡而非渐变卡:
backgroundColor(C.card)+ 细描边——与首页/我的页的渐变卡不同,汇总卡是白底彩字(信息卡而非品牌卡),三个数字各用一色。 - 三列三色:年度总额
C.primary红(钱=警示)/ 续费项目C.accent浅红(数量=主题辅色)/ 月均C.ok绿(均值=健康参考)——颜色编码数据语义:红=要花的钱、浅红=项目规模、绿=月均水平,一卡三色各司其职。 - 三列均分:
layoutWeight(1)×3——与 App 53 首页三列汇总同款布局,但 53 是渐变底白字、55 是白底彩字,同构异形。 - 数据自洽:月均 ¥62 × 12 ≈ ¥746 年度总额——月均与年度互为换算,演示数据自洽。

五、Timeline:红点竖线时间轴
Timeline() {
Column() {
ForEach(this.items, (r: RenewItem, idx: number) => {
Row({ space: 12 }) {
Column() {
Row().width(12).height(12).backgroundColor(C.primary).borderRadius(6)
if (idx < this.items.length - 1) {
Column().width(2).height(50).backgroundColor(C.stroke)
}
}.width(12)
Row({ space: 12 }) {
Text(r.emoji).fontSize(22)
Column({ space: 3 }) {
Text(r.name).fontSize(14).fontWeight(FontWeight.Medium).fontColor(C.text)
Text(r.month + '月' + r.day + '日').fontSize(12).fontColor(C.textDim)
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
Text('¥' + r.amount).fontSize(14).fontWeight(FontWeight.Bold).fontColor(C.danger)
}
.layoutWeight(1).padding(12).margin({ bottom: 12 })
.backgroundColor(C.card).borderRadius(D.rMd).border({ width: 1, color: C.stroke })
}
.width('100%').alignItems(VerticalAlign.Top)
}, (r: RenewItem) => r.id.toString())
}.width('100%')
}
逐点拆解:
- 时间轴三段式:左侧竖轨(圆点 + 连接线)+ 右侧内容卡——
Row内左Column(12 宽轨道)右Row(layoutWeight(1)内容区),轨道与内容分离。 - 12×12 红色圆点:
Row().width(12).height(12).backgroundColor(C.primary).borderRadius(6)——宽高 12、圆角 6 = 正圆,珊瑚红圆点是时间轴的"节点"。 - 条件竖线(84-86 行):
if (idx < this.items.length - 1)才渲染2×50灰色连接线——最后一项无竖线,时间轴在末尾自然收束。这是时间轴组件的经典细节:连接线是"通向下一项"的语义,末项无后续故无线。 alignItems(VerticalAlign.Top):外层 Row 顶部对齐——圆点与卡片顶部对齐(而非垂直居中),时间轴节点对齐卡片标题行,视觉锚点统一。- 红色金额:
C.danger红字¥688——扣款金额统一警示红(与 App 54 日历页同款语义:扣钱 = 红色)。

六、跨页数据自洽:全年扣款的全局闭环
Text('¥746').fontSize(22).fontWeight(FontWeight.Bold).fontColor(C.primary)
{ value: '¥746', label: '年度支出' },
- 日历页"¥746 年度总额 / 5 续费项目" ↔ 我的页"¥746 年度支出 / 5 提醒项目"——数字完全一致,日历从时间轴、我的从统计展示同一批数据。
- 日历页"视频VIP 9月1日 ¥25" ↔ 首页"视频VIP 2 天后扣款 ¥25"——同一扣款的两种表达(日历给日期、首页给倒计时)。
- 日历页"设计软件年费 1月20日 ¥688" ↔ 首页"设计软件年费 15 天后 ¥688"——年费 688 是最大单项,两页均收录。
- 日历页 5 项 ↔ 首页 4 项——日历是全年全量(含 10 月新闻订阅),首页是近期重点(4 项),全量 vs 精选的范围分工。

七、系列横向对比:时间可视化的两种形态
| 日历页 | 代表 App | 可视化形态 | 数据范围 | 主题色 |
|---|---|---|---|---|
| 纵向时间轴 | App 55 续费 | 红点 + 竖线 + 事件卡 | 全年跨月 5 项 | #FF6B6B 珊瑚红 |
| 月历网格 | App 54 订阅 | 7 列 Grid + 续费日圆点 | 单月 31 天 | #9B51E0 梦幻紫 |
差异点:App 54 用月历网格(空间化:哪天有空一眼看分布),App 55 用纵向时间轴(线性化:接下来发生什么按序呈现)。月历适合"找日期",时间轴适合"看流程"——续费提醒的核心是"接下来还有哪些扣款",时间轴的线性叙事比月历的格子分布更贴合。两 App 同日历主题、不同可视化选型,是系列中"同题异构"的典型案例。
八、开发者视角:调试与验证技巧
- 条件竖线的索引判定:
idx < this.items.length - 1——依赖 ForEach 的第二参数idx,若数据动态增删(如删除中间项),连接线自动适配,无需额外处理;但若用filter过滤后索引重排,需确认渲染用的是过滤后数组的索引。 - 竖线高度 50 硬编码:
Column().width(2).height(50)——连接线长度写死,若内容卡高度变化(如名称换行变两行),50 的竖线会与下一圆点脱节;产品化应用layoutWeight(1)让竖线自适应内容高度。 - 跨月排序:数据按"近期优先"(9月→10月→1月)而非月份升序——产品化应
sort按"距今天数"排序,跨年场景(12 月 vs 1 月)需正确处理月份回绕(1 月排在 12 月后而非前)。 - 金额红色语义:
C.danger统一红字金额——与首页的"条件变红"(仅临期红)不同,日历页金额恒红(日历页所有金额都是"要扣的钱");同 App 内两处红色语义有细微差异(首页红=紧急、日历红=支出),需设计文档明确。
九、演进方向
| 方向 | 当前状态 | 产品化演进 |
|---|---|---|
| 动态排序 | 数组声明顺序 | 按距今天数 sort,跨年回绕处理 |
| 竖线自适应 | 高度 50 硬编码 | layoutWeight(1) 随内容卡伸缩 |
| 今天定位 | 无 | 时间轴滚动定位到"今天"节点并高亮 |
| 已完成态 | 无 | 已过扣款日期的项目灰置 + 勾选标记 |
| 金额合计 | 仅年度/月均 | 时间轴按月分组 + 每月小计 |
十、一句话总结
日历页以"三列三色汇总卡 + 红点竖线时间轴"完成了全年扣款的线性可视化。白底彩字汇总卡(C.primary 红 / C.accent 浅红 / C.ok 绿 三色编码金额/数量/均值)、12×12 珊瑚红圆点 + idx < length - 1 条件竖线(末项自然收束)、alignItems(VerticalAlign.Top) 节点与卡片顶对齐——103 行代码,一个"年度看总账 → 时间轴看流程"的续费日历页,也是"纵向时间轴"型日历的标准范式。

更多推荐





所有评论(0)