鸿蒙原生应用 ArkUI 实战:手写 7 列月历网格 + 续费日紫色圆点 —— 订阅日历页的扣款可视化架构
鸿蒙原生应用 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 })
}
逐点拆解:
- 月标题:"2026年 8月" 15 号 Bold 左对齐——年月标识,指向当前展示月份。
- 周表头:
['日','一','二','三','四','五','六']7 列layoutWeight(1)等分 +textAlign(TextAlign.Center)居中——周日开头(国内日历惯例),11 号淡灰。 - 7 列日历网格:
columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr')+rowsGap(6)——31 天排为 7 列网格(约 5 行),每个日期一个 34×34 格子,不引日历库纯 ArkUI 手绘。 - 续费日紫色圆点:
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%')
}
逐点拆解:
- 紫色日期块:左侧 44 宽竖列——18 号 Bold 紫色日期 + 10 号淡灰"8月",日期是列表第一信息(哪天扣款),紫色与日历标记同色呼应。
- emoji + 名称:22 号 emoji(🎬🎵☁️📰)+ 14 号订阅名——第 4 项是"新闻订阅"(日历页展示当月真实扣款流水,与首页/订阅页的"软件订阅"不同)。
- 红色金额:14 号 Bold
¥25用C.danger(#FF5A6E红)——扣款金额用警示红(扣钱 = 支出 = 红色语义),与首页的"品牌色金额"不同,这里统一用红色表示"要花钱"。 - 扣款日递增排序:1 日 → 5 日 → 10 日 → 18 日——按日期升序,用户按时间线浏览本月扣款节奏。
- 无点击交互:本列表无 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 下方计划列表用"日期块 + 红色金额"组合,把"哪天扣多少"一屏给全。
八、开发者视角:调试与验证技巧
- 月份天数固定:
monthDays硬编码 1-31 天——8 月确实 31 天,但其他月份(2 月 28 天、4 月 30 天)天数不同,产品化应根据年月动态生成天数(new Date(year, month, 0).getDate())。 - 周起始对齐:当前网格直接排 1-31 日(第 1 天固定在第一行第一列)——没有按"8 月 1 日是星期几"对齐,产品化需计算每月 1 日的星期偏移量(
new Date(y, m, 1).getDay())补空位。 - 续费日数据源重复:
renews与renewDays是两个独立数组——数据源重复,产品化应合并为单一数据源(renews派生renewDays),避免两处不同步。 - 红色金额语义:扣款金额用红色(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 行代码,一个"日历看哪天扣款 → 列表看扣什么扣多少"的扣款日历页,也是"支出日历"型页面的标准范式。

更多推荐





所有评论(0)