鸿蒙原生应用 ArkUI 实战:手写 7 列月历网格 + 续费日紫色圆点 —— 订阅日历页的扣款可视化架构

App 54「订阅服务管理」日历页(Func2Tab),主题色 #9B51E0 梦幻紫,4 个 Tab 分别为首页(📺)、订阅(📋)、日历(📅)、统计(📊)。本篇基于 54-sub-mgr/entry/src/main/ets/pages/Func2Tab.ets(共 109 行)逐段拆解,附 4 张实机截图。

日历页整体布局

一、整体结构:三区"Header + 日历卡 + 计划列表"布局

日历页是"日历总览 + 扣款计划"的组合布局——3 个 @Builder 块:

build() {
  Column() {
    this.Header()
    Scroll() {
      Column({ space: 14 }) {
        this.CalendarCard()
        this.SectionTitle('本月续费计划')
        this.RenewList()
      }
      .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 块结构:CalendarCard(7 列日历网格)→ SectionTitle(本月续费计划)→ RenewList(4 条扣款计划)。日历页是"扣款日历"——先用日历网格展示"本月哪天要扣款"(1/5/10/18 紫色标记),再用计划列表展开"哪天扣哪项、扣多少"。日历给"何时"、列表给"何事",两个维度一屏呈现。

二、数据模型 + 续费日判定

interface RenewItem {
  id: number;
  emoji: string;
  name: string;
  day: number;
  price: number;
}
private renews: RenewItem[] = [
  { id: 1, emoji: '🎬', name: '视频VIP会员', day: 1, price: 25 },
  { id: 2, emoji: '🎵', name: '音乐会员', day: 5, price: 15 },
  { id: 3, emoji: '☁️', name: '云存储200G', day: 10, price: 6 },
  { id: 4, emoji: '📰', name: '新闻订阅', day: 18, price: 12 }
];

private weekHead: string[] = ['日', '一', '二', '三', '四', '五', '六'];
private renewDays: number[] = [1, 5, 10, 18];
private monthDays: number[] = [
  1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
  17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31
];

RenewItem 五字段:day(扣款日)+ price(金额)。4 条续费计划按日期升序(1→5→10→18)。weekHead 周表头(日开头)、renewDays 续费日数组、monthDays 8 月 31 天。

private isRenewDay(day: number): boolean {
  return this.renewDays.indexOf(day) >= 0;
}

isRenewDay 用 indexOf 判断是否续费日——日历网格的每个格子调用此方法决定是否紫色标记。

三、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 统一范式。"续费日历"是复合词标题(续费 + 日历),点明"本页是看扣款日期的日历"。

四、CalendarCard:手写 7 列月历网格

CalendarCard() {
  Column({ space: 12 }) {
    Text('2026年 8月').fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).width('100%')
    Row() {
      ForEach(this.weekHead, (w: string) => {
        Text(w).fontSize(11).fontColor(C.textDim).layoutWeight(1).textAlign(TextAlign.Center)
      }, (w: string) => w)
    }.width('100%')
    Grid() {
      ForEach(this.monthDays, (day: number) => {
        GridItem() {
          Column() {
            Text(day.toString()).fontSize(13)
              .fontColor(this.isRenewDay(day) ? '#FFFFFF' : C.text)
          }
          .width(34).height(34).justifyContent(FlexAlign.Center)
          .backgroundColor(this.isRenewDay(day) ? C.primary : Color.Transparent).borderRadius(17)
        }
      }, (day: number) => day.toString())
    }.columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr').rowsGap(6).width('100%')
  }
  .width('100%').padding(16).backgroundColor(C.card).borderRadius(D.rLg).border({ width: 1, color: C.stroke })
}

逐点拆解:

  1. 月标题:"2026年 8月" 15 号 Bold 左对齐——年月标识,指向当前展示月份。
  2. 周表头:['日','一','二','三','四','五','六'] 7 列 layoutWeight(1) 等分 + textAlign(TextAlign.Center) 居中——周日开头(国内日历惯例),11 号淡灰。
  3. 7 列日历网格:columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr') + rowsGap(6)——31 天排为 7 列网格(约 5 行),每个日期一个 34×34 格子,不引日历库纯 ArkUI 手绘。
  4. 续费日紫色圆点:isRenewDay(day) 判断——1/5/10/18 日为紫色圆底白字(backgroundColor(C.primary) + borderRadius(17) 正圆),普通日透明底黑字;续费日在日历上"跳出来",一眼看到本月 4 个扣款日。

日历续费日标记

五、RenewList:本月续费计划列表

RenewList() {
  Column({ space: 10 }) {
    ForEach(this.renews, (r: RenewItem) => {
      Row({ space: 12 }) {
        Column({ space: 2 }) {
          Text(r.day.toString()).fontSize(18).fontWeight(FontWeight.Bold).fontColor(C.primary)
          Text('8月').fontSize(10).fontColor(C.textDim)
        }.width(44).alignItems(HorizontalAlign.Center)
        Text(r.emoji).fontSize(22)
        Text(r.name).fontSize(14).fontColor(C.text).layoutWeight(1)
        Text('¥' + r.price).fontSize(14).fontWeight(FontWeight.Bold).fontColor(C.danger)
      }
      .width('100%').padding(12).backgroundColor(C.card).borderRadius(D.rMd).border({ width: 1, color: C.stroke })
    }, (r: RenewItem) => r.id.toString())
  }.width('100%')
}

逐点拆解:

  1. 紫色日期块:左侧 44 宽竖列——18 号 Bold 紫色日期 + 10 号淡灰"8月",日期是列表第一信息(哪天扣款),紫色与日历标记同色呼应。
  2. emoji + 名称:22 号 emoji(🎬🎵☁️📰)+ 14 号订阅名——第 4 项是"新闻订阅"(日历页展示当月真实扣款流水,与首页/订阅页的"软件订阅"不同)。
  3. 红色金额:14 号 Bold ¥25 用 C.danger(#FF5A6E 红)——扣款金额用警示红(扣钱 = 支出 = 红色语义),与首页的"品牌色金额"不同,这里统一用红色表示"要花钱"。
  4. 扣款日递增排序:1 日 → 5 日 → 10 日 → 18 日——按日期升序,用户按时间线浏览本月扣款节奏。
  5. 无点击交互:本列表无 onClick——纯信息展示(扣款计划不需要操作),与订阅页的开关卡形成"浏览 vs 管理"的分工。

本月续费计划列表

六、跨页数据自洽:扣款日历的全局闭环

private renewDays: number[] = [1, 5, 10, 18];
  { id: 2, emoji: '🎵', name: '音乐会员', price: 15, cycle: '月', nextDate: '09-05', autoRenew: true },
  { id: 3, emoji: '☁️', name: '云存储200G', price: 6, cycle: '月', nextDate: '09-10', autoRenew: false },

日历"1/5/10 日"与订阅页"下次扣款 09-01/09-05/09-10"对应——扣款日期一致,日历页的紫色标记 = 订阅页的下次扣款日。日历 4 笔合计 25+15+6+12 = 58 元 ≈ 统计页 8 月 ¥53(口径差异为演示设计),月度趋势的 8 月柱 ≈ 日历当月扣款合计。

日历页滚动与交互

七、系列横向对比:日历型页面的两种形态

日历页代表 App日历用途标记内容主题色
扣款日历App 54 订阅支出日历续费日紫色圆点#9B51E0 梦幻紫
事件日历App 27 提醒日程管理提醒事件标记#FDCB6E 黄
进度日历App 45 签到连续计数签到天数#FDCB6E 黄

差异点:App 54 的日历是"支出日历"——标记的不是事件而是扣款,红色金额强调"钱在流出",这是订阅管理场景独有的日历形态。与 App 27 的"事件日历"不同,54 下方计划列表用"日期块 + 红色金额"组合,把"哪天扣多少"一屏给全。

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

  1. 月份天数固定:monthDays 硬编码 1-31 天——8 月确实 31 天,但其他月份(2 月 28 天、4 月 30 天)天数不同,产品化应根据年月动态生成天数(new Date(year, month, 0).getDate())。
  2. 周起始对齐:当前网格直接排 1-31 日(第 1 天固定在第一行第一列)——没有按"8 月 1 日是星期几"对齐,产品化需计算每月 1 日的星期偏移量(new Date(y, m, 1).getDay())补空位。
  3. 续费日数据源重复:renews 与 renewDays 是两个独立数组——数据源重复,产品化应合并为单一数据源(renews 派生 renewDays),避免两处不同步。
  4. 红色金额语义:扣款金额用红色(danger)——红色在中文语境是"支出/警示",与首页的品牌色金额可能造成混淆,产品化建议统一"金额颜色语义"。

九、演进方向

方向当前状态产品化演进
月份切换固定 2026 年 8 月左右箭头切换月份,高亮"今天"
天数动态硬编码 31 天new Date(y, m, 0).getDate() 动态生成
周对齐1 日固定第一列按星期几偏移补空位
数据源renews + renewDays 重复renews 派生 renewDays 单一数据源
月度合计无底部展示"本月待扣合计 58 元"

十、一句话总结

日历页以"手写 7 列月历网格 + 续费计划列表"完成了扣款日期的可视化。columnsTemplate('1fr ×7') 7 列网格 + isRenewDay() 续费日判定(紫色圆底白字 borderRadius(17) 正圆)、34×34 日期格子 + rowsGap(6) 行间距、紫色日期块 + 红色金额的计划列表——109 行代码,一个"日历看哪天扣款 → 列表看扣什么扣多少"的扣款日历页,也是"支出日历"型页面的标准范式。

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

Logo

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

更多推荐