在学习和练习类应用中,记录用户的练习进度是激励持续使用的重要功能。本文将讲解如何设计一个清晰直观的练习记录页面,包含统计概览、日历打卡和成就徽章三个核心模块。
功能概述
练习记录页面主要展示以下内容:
- 统计概览卡片:连续练习天数、获得成就数、最长连续记录
- 练习日历:30天打卡视图,一眼看出练习频次
- 成就徽章:已解锁和未解锁的成就列表
- 练习详情:最近有记录的日期详细数据
页面预览
整体采用垂直布局,效果如下:

顶部导航设计
顶部栏使用 Row 水平布局,左侧返回按钮,中间标题:
typescript
Row() {
Button('← 返回')
.fontSize(18)
.backgroundColor(Color.Transparent)
.fontColor(this.themeColors.textPrimary)
.onClick(() => this.goBack())
Blank()
Text('练习记录')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor(this.themeColors.textPrimary)
}
.width('100%')
.padding(16)
使用 Blank() 占据中间空间,将标题推到视觉中心位置。
提示说明卡片
在统计卡片之前添加说明,告知用户统计规则:
typescript
Column({ space: 8 }) {
Row({ space: 8 }) {
Image($r('app.media.ic_notice_info'))
.width(18)
.height(18)
Text('记录说明')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#2196F3')
}
Text('只有练习模式和挑战模式会记录进度,教学模式不计入统计。')
.fontSize(13)
.fontColor(this.themeColors.textSecondary)
.lineHeight(18)
}
.width('90%')
.padding(12)
.backgroundColor(getThemeManager().isDark() ? '#1A2332' : '#E3F2FD')
.borderRadius(8)
设计要点:
- 宽度设为
90%,与下方卡片形成视觉层次 - 浅色背景使用
#E3F2FD(浅蓝),深色模式使用#1A2332 - 图标 + 标题的组合让说明更醒目
统计概览卡片
三列等宽的统计数据展示:
typescript
Column({ space: 12 }) {
Row({ space: 20 }) {
// 连续练习天数
Column({ space: 4 }) {
Row({ space: 6 }) {
Image($r('app.media.ic_progress_streak'))
.width(22)
.height(22)
Text(`${this.progressData?.currentStreak || 0}`)
.fontSize(32)
.fontWeight(FontWeight.Bold)
.fontColor('#FF5722')
}
Text('连续练习天数')
.fontSize(14)
.fontColor(this.themeColors.textSecondary)
}
// 获得成就数
Column({ space: 4 }) {
Row({ space: 6 }) {
Image($r('app.media.ic_progress_star'))
.width(22)
.height(22)
Text(`${this.progressData?.totalStars || 0}`)
.fontSize(32)
.fontWeight(FontWeight.Bold)
.fontColor('#FF9800')
}
Text('获得成就')
.fontSize(14)
.fontColor(this.themeColors.textSecondary)
}
// 最长连续记录
Column({ space: 4 }) {
Row({ space: 6 }) {
Image($r('app.media.ic_progress_crown'))
.width(22)
.height(22)
Text(`${this.progressData?.longestStreak || 0}`)
.fontSize(32)
.fontWeight(FontWeight.Bold)
.fontColor('#9C27B0')
}
Text('最长连续')
.fontSize(14)
.fontColor(this.themeColors.textSecondary)
}
}
.width('100%')
.justifyContent(FlexAlign.SpaceAround)
}
.width('100%')
.padding(20)
.backgroundColor(this.themeColors.cardBackground)
.borderRadius(12)
设计要点:
| 元素 | 字号 | 颜色 | 说明 |
|---|---|---|---|
| 数字 | 32 | 主题色 | 醒目的大号字体 |
| 标签 | 14 | textSecondary | 灰色说明文字 |
| 图标 | 22 | - | 增加视觉识别度 |
- 三个数据项使用不同的主题色区分:橙红色、橙色、紫色
SpaceAround均匀分布,适配不同屏幕宽度- 卡片背景使用
cardBackground,与页面背景形成层次
标签切换栏
使用按钮组实现标签切换:
typescript
Row({ space: 12 }) {
Button() {
Row({ space: 8 }) {
Image($r('app.media.ic_progress_calendar'))
.width(18)
.height(18)
Text('练习日历')
.fontSize(16)
.fontWeight(this.selectedTab === 0 ? FontWeight.Medium : FontWeight.Regular)
.fontColor(this.selectedTab === 0 ? '#2196F3' : this.themeColors.textSecondary)
}
.justifyContent(FlexAlign.Center)
}
.layoutWeight(1)
.height(45)
.backgroundColor(this.selectedTab === 0 ? '#E3F2FD' : this.themeColors.cardBackground)
.border({ width: 1.5, color: this.selectedTab === 0 ? '#2196F3' : this.themeColors.borderColor })
.borderRadius(12)
.onClick(() => this.selectedTab = 0)
// 成就徽章按钮(类似结构)
Button() { ... }
.onClick(() => this.selectedTab = 1)
}
.width('100%')
设计要点:
- 选中状态:蓝色背景
#E3F2FD+ 蓝色边框 - 未选中状态:卡片背景 + 灰色边框
- 使用
layoutWeight(1)让两个按钮等宽平分 - 图标 + 文字的组合提升识别度
日历视图设计
30天打卡日历,按周分组展示:
typescript
@Builder
buildCalendarView() {
Column({ space: 12 }) {
Text('最近30天练习记录')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
.width('100%')
// 日历网格 - 按周分组
Column({ space: 8 }) {
ForEach(this.getCalendarWeeks(), (week: DailyRecord[], weekIndex: number) => {
Row({ space: 8 }) {
ForEach(week, (record: DailyRecord, dayIndex: number) => {
Column({ space: 4 }) {
Text(this.getMonthDay(record.date))
.fontSize(12)
.fontColor(this.themeColors.textHint)
// 打卡状态
Column() {
if (record.totalCount > 0) {
Text('✓')
.fontSize(20)
.fontColor('#4CAF50')
} else {
Text('·')
.fontSize(20)
.fontColor('#E0E0E0')
}
}
.width(40)
.height(40)
.backgroundColor(record.totalCount > 0 ? '#E8F5E9' : this.themeColors.secondaryBackground)
.borderRadius(8)
.justifyContent(FlexAlign.Center)
Text(`周${this.getWeekDayName(record.date)}`)
.fontSize(10)
.fontColor(this.themeColors.textHint)
}
.layoutWeight(1)
})
}
.width('100%')
})
}
.width('100%')
.padding(16)
.backgroundColor(this.themeColors.cardBackground)
.borderRadius(12)
}
}
// 将记录按周分组
private getCalendarWeeks(): DailyRecord[][] {
const weeks: DailyRecord[][] = []
const records = this.recentRecords.slice(-28) // 最近28天,4周
for (let i = 0; i < records.length; i += 7) {
weeks.push(records.slice(i, i + 7))
}
return weeks
}
设计要点:
- 周布局:每行显示一周7天,共4行
- 打卡状态 :
- 已练习:绿色对勾
✓+ 浅绿背景#E8F5E9 - 未练习:灰色圆点
·+ 默认背景
- 已练习:绿色对勾
- 信息层级:日期(月/日)→ 状态图标 → 星期
- 格子尺寸:40×40dp,圆角 8dp
练习详情列表
展示最近有记录的日期详情:
typescript
Column({ space: 0 }) {
ForEach(this.recentRecords.slice(-7).reverse().filter(r => r.totalCount > 0), (record: DailyRecord) => {
Column() {
Row() {
// 日期信息
Column({ space: 4 }) {
Text(this.getMonthDay(record.date))
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
Text(`周${this.getWeekDayName(record.date)}`)
.fontSize(12)
.fontColor('#999999')
}
.alignItems(HorizontalAlign.Start)
Blank()
// 统计数据
Row({ space: 16 }) {
Column({ space: 2 }) {
Text(`${record.practiceCount}`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#4CAF50')
Text('练习')
.fontSize(12)
.fontColor(this.themeColors.textHint)
}
Column({ space: 2 }) {
Text(`${record.challengeCount}`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FF9800')
Text('挑战')
.fontSize(12)
.fontColor(this.themeColors.textHint)
}
Column({ space: 2 }) {
Text(`${record.totalCount}`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#2196F3')
Text('总次数')
.fontSize(12)
.fontColor(this.themeColors.textHint)
}
}
}
.width('100%')
.padding(16)
Divider()
.color('#F0F0F0')
}
})
}
.width('100%')
.backgroundColor(this.themeColors.cardBackground)
.borderRadius(12)
设计要点:
- 只显示最近7天有记录的日期,避免列表过长
- 每项包含:日期(左)+ 练习/挑战/总计(右)
- 三项数据使用不同颜色区分
Divider分隔每条记录
成就徽章视图
成就列表展示,区分已解锁和未解锁状态:
typescript
@Builder
buildAchievementsView() {
Column({ space: 12 }) {
Text('成就徽章')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
.width('100%')
Column({ space: 12 }) {
ForEach(this.progressData?.achievements || [], (achievement: Achievement) => {
Row({ space: 16 }) {
// 成就图标
Image(this.getAchievementIconResource(achievement))
.width(40)
.height(40)
.opacity(achievement.unlocked ? 1 : 0.3)
// 成就信息
Column({ space: 4 }) {
Text(achievement.name)
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor(achievement.unlocked ? this.themeColors.textPrimary : this.themeColors.textHint)
Text(achievement.description)
.fontSize(14)
.fontColor(achievement.unlocked ? this.themeColors.textSecondary : this.themeColors.textHint)
if (achievement.unlocked && achievement.unlockedDate) {
Text(`解锁于 ${achievement.unlockedDate}`)
.fontSize(12)
.fontColor(this.themeColors.textHint)
.margin({ top: 4 })
}
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
// 解锁标记
if (achievement.unlocked) {
Text('✓')
.fontSize(24)
.fontColor('#4CAF50')
}
}
.width('100%')
.padding(16)
.backgroundColor(achievement.unlocked ? this.themeColors.cardBackground : this.themeColors.secondaryBackground)
.borderRadius(12)
.border({
width: achievement.unlocked ? 2 : 1,
color: achievement.unlocked ? '#4CAF50' : '#E0E0E0'
})
})
}
.width('100%')
}
}
设计要点:
| 状态 | 图标透明度 | 文字颜色 | 背景 | 边框 |
|---|---|---|---|---|
| 已解锁 | 100% | textPrimary | cardBackground | 2dp 绿色 |
| 未解锁 | 30% | textHint | secondaryBackground | 1dp 灰色 |
- 视觉差异明显,已解锁成就更醒目
- 解锁时间作为次要信息小字显示
- 右侧绿色对勾强化完成感
空状态处理
当没有练习记录时显示友好提示:
typescript
if (this.recentRecords.slice(-7).filter(r => r.totalCount > 0).length === 0) {
Column({ space: 8 }) {
Image($r('app.media.ic_progress_empty'))
.width(40)
.height(40)
.opacity(0.45)
Text('还没有练习记录')
.fontSize(16)
.fontColor('#999999')
Text('完成练习模式或挑战模式后会自动记录')
.fontSize(13)
.fontColor('#BDBDBD')
}
.width('100%')
.padding(40)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
设计要点:
- 降低图标透明度(45%),避免过于醒目
- 主标题 + 引导文字的两层信息
- 充足的内边距(40dp)让空状态更舒展
小结
本文介绍了练习记录页面的 UI 设计要点:
- 统计卡片:三列等宽布局,不同颜色区分数据类型
- 标签切换:视觉反馈清晰的按钮组
- 日历视图:周分组布局,颜色标识打卡状态
- 详情列表:日期 + 三项数据的横向排布
- 成就徽章:解锁/未解锁状态的差异化设计
- 空状态:友好的引导提示
页面整体采用卡片式布局,信息层次清晰,配色鲜明但不杂乱,用户可以快速了解自己的练习情况并获得成就感。