鸿蒙原生应用 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 })
}

逐点拆解:

  1. 白底卡而非渐变卡:backgroundColor(C.card) + 细描边——与首页/我的页的渐变卡不同,汇总卡是白底彩字(信息卡而非品牌卡),三个数字各用一色。
  2. 三列三色:年度总额 C.primary 红(钱=警示)/ 续费项目 C.accent 浅红(数量=主题辅色)/ 月均 C.ok 绿(均值=健康参考)——颜色编码数据语义:红=要花的钱、浅红=项目规模、绿=月均水平,一卡三色各司其职。
  3. 三列均分:layoutWeight(1) ×3——与 App 53 首页三列汇总同款布局,但 53 是渐变底白字、55 是白底彩字,同构异形。
  4. 数据自洽:月均 ¥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%')
}

逐点拆解:

  1. 时间轴三段式:左侧竖轨(圆点 + 连接线)+ 右侧内容卡——Row 内左 Column(12 宽轨道)右 Row(layoutWeight(1) 内容区),轨道与内容分离。
  2. 12×12 红色圆点:Row().width(12).height(12).backgroundColor(C.primary).borderRadius(6)——宽高 12、圆角 6 = 正圆,珊瑚红圆点是时间轴的"节点"。
  3. 条件竖线(84-86 行):if (idx < this.items.length - 1) 才渲染 2×50 灰色连接线——最后一项无竖线,时间轴在末尾自然收束。这是时间轴组件的经典细节:连接线是"通向下一项"的语义,末项无后续故无线。
  4. alignItems(VerticalAlign.Top):外层 Row 顶部对齐——圆点与卡片顶部对齐(而非垂直居中),时间轴节点对齐卡片标题行,视觉锚点统一。
  5. 红色金额: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 同日历主题、不同可视化选型,是系列中"同题异构"的典型案例。

八、开发者视角:调试与验证技巧

  1. 条件竖线的索引判定:idx < this.items.length - 1——依赖 ForEach 的第二参数 idx,若数据动态增删(如删除中间项),连接线自动适配,无需额外处理;但若用 filter 过滤后索引重排,需确认渲染用的是过滤后数组的索引。
  2. 竖线高度 50 硬编码:Column().width(2).height(50)——连接线长度写死,若内容卡高度变化(如名称换行变两行),50 的竖线会与下一圆点脱节;产品化应用 layoutWeight(1) 让竖线自适应内容高度。
  3. 跨月排序:数据按"近期优先"(9月→10月→1月)而非月份升序——产品化应 sort 按"距今天数"排序,跨年场景(12 月 vs 1 月)需正确处理月份回绕(1 月排在 12 月后而非前)。
  4. 金额红色语义:C.danger 统一红字金额——与首页的"条件变红"(仅临期红)不同,日历页金额恒红(日历页所有金额都是"要扣的钱");同 App 内两处红色语义有细微差异(首页红=紧急、日历红=支出),需设计文档明确。

九、演进方向

方向当前状态产品化演进
动态排序数组声明顺序按距今天数 sort,跨年回绕处理
竖线自适应高度 50 硬编码layoutWeight(1) 随内容卡伸缩
今天定位无时间轴滚动定位到"今天"节点并高亮
已完成态无已过扣款日期的项目灰置 + 勾选标记
金额合计仅年度/月均时间轴按月分组 + 每月小计

十、一句话总结

日历页以"三列三色汇总卡 + 红点竖线时间轴"完成了全年扣款的线性可视化。白底彩字汇总卡(C.primary 红 / C.accent 浅红 / C.ok 绿 三色编码金额/数量/均值)、12×12 珊瑚红圆点 + idx < length - 1 条件竖线(末项自然收束)、alignItems(VerticalAlign.Top) 节点与卡片顶对齐——103 行代码,一个"年度看总账 → 时间轴看流程"的续费日历页,也是"纵向时间轴"型日历的标准范式。

项目源码开源:https://gitee.com/codenestFlow/HarmonyOSHub

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐