
引言
学习记录页面是用户追踪学习进度、了解学习习惯的核心入口。一个设计良好的学习记录页面不仅能展示数据,更能激励用户持续学习。本文将实现一个完整的学习记录页面,通过数据可视化和成就系统,为用户提供直观的学习反馈和动力。
学习目标
完成本文后,你将能够:
- ✅ 理解学习记录页面的设计原则
- ✅ 实现数据可视化组件
- ✅ 创建学习日历展示
- ✅ 实现成就系统
- ✅ 绘制学习趋势图表
需求分析
功能模块设计
学习记录页面通常包含以下核心模块:
| 模块 | 功能描述 | 技术要点 | 用户价值 |
|---|---|---|---|
| 学习统计 | 总学习天数、阅读文章数、测验次数、积分 | 卡片布局、数字展示 | 快速了解学习概况 |
| 学习日历 | 展示最近30天学习情况 | 日历网格、颜色深浅 | 直观展示学习连续性 |
| 学习成就 | 展示获得的成就徽章 | 网格布局、状态管理 | 激励持续学习 |
| 学习趋势 | 展示学习趋势变化 | 柱状图、数据绑定 | 分析学习规律 |
用户场景分析
用户访问学习记录页面的常见场景:
- 每日回顾:查看当天学习情况
- 目标追踪:检查学习目标完成进度
- 成就解锁:查看已获得和未获得的成就
- 数据分析:了解学习习惯和规律
设计思路
数据可视化方案
数据可视化是学习记录页面的核心,需要选择合适的展示方式:
| 数据类型 | 推荐展示方式 | 技术实现 |
|---|---|---|
| 统计数字 | 卡片+大数字 | Grid布局 + Text组件 |
| 日历数据 | 热力图 | Grid布局 + Circle组件 |
| 成就徽章 | 图标网格 | Grid布局 + Image组件 |
| 趋势变化 | 柱状图 | Row布局 + 自定义组件 |
状态管理策略
学习记录涉及多种数据源,需要合理的状态管理:
typescript
// 状态分类设计
interface PageState {
// 统计数据 - 直接展示
statistics: Statistics;
// 日历数据 - 需计算学习等级
calendarData: CalendarDay[];
// 成就数据 - 需判断解锁状态
achievements: Achievement[];
// 趋势数据 - 需从日历数据提取
trendData: TrendPoint[];
}
布局架构
页面采用垂直滚动布局,按信息层级组织:
┌─────────────────────────────────┐
│ 顶部导航栏 │
├─────────────────────────────────┤
│ 统计卡片区域 │
├─────────────────────────────────┤
│ 学习日历区域 │
├─────────────────────────────────┤
│ 学习成就区域 │
├─────────────────────────────────┤
│ 学习趋势区域 │
└─────────────────────────────────┘
核心实现
步骤1: 页面结构设计
学习记录页面采用模块化架构,将UI分解为多个独立组件:
typescript
// pages/LearningRecords.ets
import router from '@ohos.router';
import type { Achievement } from '../models/AchievementModel';
@Entry
@Component
struct LearningRecords {
// 统计数据
@State statistics: Statistics = {
totalDays: 365,
articles: 128,
quizzes: 24,
score: 8520
};
// 学习日历数据(最近30天)
@State calendarData: CalendarDay[] = [];
// 成就列表
@State achievements: Achievement[] = [];
/**
* 页面加载时执行 - 初始化数据
*/
aboutToAppear() {
this.loadCalendarData();
this.loadAchievements();
}
/**
* 加载日历数据 - 生成最近30天的模拟数据
*/
loadCalendarData(): void {
const today = new Date();
this.calendarData = [];
for (let i = 29; i >= 0; i--) {
const date = new Date(today);
date.setDate(date.getDate() - i);
// 根据日期计算学习状态(模拟数据)
const hasStudy = Math.random() > 0.3;
const studyMinutes = hasStudy ? Math.floor(Math.random() * 60) + 10 : 0;
this.calendarData.push({
date: date,
hasStudy,
studyMinutes,
level: hasStudy ? (studyMinutes > 40 ? 3 : studyMinutes > 20 ? 2 : 1) : 0
});
}
}
/**
* 加载成就数据
*/
loadAchievements(): void {
this.achievements = [
{ id: '1', name: '初学者', description: '完成第一次学习', icon: 'star', unlocked: true },
{ id: '2', name: '坚持不懈', description: '连续学习7天', icon: 'flame', unlocked: true },
{ id: '3', name: '知识达人', description: '阅读100篇文章', icon: 'book', unlocked: true },
{ id: '4', name: '测验高手', description: '完成所有测验', icon: 'trophy', unlocked: false },
{ id: '5', name: '节气专家', description: '掌握所有节气知识', icon: 'crown', unlocked: false },
{ id: '6', name: '学习之星', description: '累计学习1000分钟', icon: 'star', unlocked: true },
{ id: '7', name: '分享达人', description: '分享10次应用', icon: 'share', unlocked: false },
{ id: '8', name: '邀请好友', description: '邀请5位好友', icon: 'users', unlocked: false }
];
}
/**
* 构建UI - 垂直滚动布局
*/
build() {
Scroll() {
Column({ space: 0 }) {
// 1. 顶部导航
this.buildHeader()
// 2. 统计卡片
this.buildStatisticsCard()
// 3. 学习日历
this.buildStudyCalendar()
// 4. 学习成就
this.buildAchievements()
// 5. 学习趋势
this.buildTrendChart()
}
}
.width('100%')
.height('100%')
.backgroundColor('#F8F7F2')
}
}
interface Statistics {
totalDays: number;
articles: number;
quizzes: number;
score: number;
}
interface CalendarDay {
date: Date;
hasStudy: boolean;
studyMinutes: number;
level: number; // 0: 无学习, 1: 少量, 2: 中等, 3: 大量
}
架构设计要点:
- 使用@State管理响应式状态
- 模块化构建方法,每个区域独立实现
- 数据加载放在aboutToAppear生命周期中
步骤2: 统计卡片区域
统计卡片展示核心学习数据,采用四列网格布局:
typescript
/**
* 构建统计卡片
*/
@Builder
buildStatisticsCard(): void {
Card() {
Column({ space: 16 }) {
// 区域标题
Row({ space: 8 }) {
Image($r('app.media.ic_trending'))
.width(20)
.height(20)
.fillColor('#4A9B6D')
Text('学习统计')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
}
// 统计数据 - 四列网格
Grid() {
GridItem() {
this.buildStatItem('学习天数', this.statistics.totalDays.toString(), '天', $r('app.media.ic_calendar'))
}
GridItem() {
this.buildStatItem('阅读文章', this.statistics.articles.toString(), '篇', $r('app.media.ic_book'))
}
GridItem() {
this.buildStatItem('参与测验', this.statistics.quizzes.toString(), '次', $r('app.media.ic_test'))
}
GridItem() {
this.buildStatItem('累计积分', this.statistics.score.toString(), '分', $r('app.media.ic_score'))
}
}
.columnsTemplate('1fr 1fr 1fr 1fr')
}
.padding(16)
}
.width('92%')
.margin({ left: '4%', right: '4%', top: 12 })
}
/**
* 构建单个统计项
*/
@Builder
buildStatItem(title: string, value: string, unit: string, icon: Resource): void {
Column({ space: 4 }) {
// 图标
Image(icon)
.width(24)
.height(24)
.fillColor('#4A9B6D')
// 数值和单位
Row({ space: 2 }) {
Text(value)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text(unit)
.fontSize(12)
.fontColor('#999999')
}
// 标题
Text(title)
.fontSize(12)
.fontColor('#999999')
}
.width('100%')
.alignItems(HorizontalAlign.Center)
}
设计要点:
- 使用Grid实现四列等宽布局
- 图标+数值+单位+标题的统一结构
- 绿色主题色突出学习进度
步骤3: 学习日历
学习日历使用颜色深浅表示学习量,直观展示学习连续性:
typescript
/**
* 构建学习日历
*/
@Builder
buildStudyCalendar(): void {
Card() {
Column({ space: 12 }) {
// 标题区域
Row({ space: 8 }) {
Image($r('app.media.ic_calendar'))
.width(20)
.height(20)
.fillColor('#4A9B6D')
Text('学习日历')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Blank()
Text('近30天')
.fontSize(13)
.fontColor('#999999')
}
// 星期标题
Row() {
const weekDays = ['日', '一', '二', '三', '四', '五', '六'];
ForEach(weekDays, (day: string) => {
Text(day)
.fontSize(12)
.fontColor('#999999')
.width('14.28%')
.textAlign(TextAlign.Center)
})
}
// 日期网格
Grid() {
ForEach(this.calendarData, (day: CalendarDay) => {
GridItem() {
this.buildCalendarDay(day)
}
})
}
.columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr')
.rowsGap(8)
// 图例说明
Row({ space: 16 }) {
Row({ space: 4 }) {
Circle().width(10).height(10).fillColor('#E8F5E9')
Text('无学习').fontSize(12).fontColor('#999999')
}
Row({ space: 4 }) {
Circle().width(10).height(10).fillColor('#81C784')
Text('少量').fontSize(12).fontColor('#999999')
}
Row({ space: 4 }) {
Circle().width(10).height(10).fillColor('#4CAF50')
Text('中等').fontSize(12).fontColor('#999999')
}
Row({ space: 4 }) {
Circle().width(10).height(10).fillColor('#2E7D32')
Text('大量').fontSize(12).fontColor('#999999')
}
}
.justifyContent(FlexAlign.Center)
}
.padding(16)
}
.width('92%')
.margin({ left: '4%', right: '4%', top: 12 })
}
/**
* 构建单个日历日期
*/
@Builder
buildCalendarDay(day: CalendarDay): void {
// 根据学习等级选择颜色
const colors = ['#E8F5E9', '#81C784', '#4CAF50', '#2E7D32'];
Stack({ alignContent: Alignment.Center }) {
// 背景圆圈
Circle()
.width(32)
.height(32)
.fillColor(colors[day.level])
// 日期数字
Text(day.date.getDate().toString())
.fontSize(12)
.fontColor(day.hasStudy ? '#FFFFFF' : '#999999')
}
}
设计亮点:
- 颜色深浅直观表示学习量
- 白色日期数字在深色背景上清晰可见
- 图例说明帮助用户理解颜色含义
步骤4: 学习成就
成就系统通过徽章展示激励用户持续学习:
typescript
/**
* 构建学习成就区域
*/
@Builder
buildAchievements(): void {
Card() {
Column({ space: 12 }) {
// 标题区域
Row({ space: 8 }) {
Image($r('app.media.ic_trophy'))
.width(20)
.height(20)
.fillColor('#4A9B6D')
Text('学习成就')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Blank()
// 解锁进度
Text(`${this.achievements.filter(a => a.unlocked).length}/${this.achievements.length}`)
.fontSize(13)
.fontColor('#4A9B6D')
}
// 成就网格
Grid() {
ForEach(this.achievements, (achievement: Achievement) => {
GridItem() {
this.buildAchievementItem(achievement)
}
}, (achievement: Achievement) => achievement.id)
}
.columnsTemplate('1fr 1fr 1fr 1fr')
.rowsGap(12)
}
.padding(16)
}
.width('92%')
.margin({ left: '4%', right: '4%', top: 12 })
}
/**
* 构建单个成就项
*/
@Builder
buildAchievementItem(achievement: Achievement): void {
Column({ space: 8 }) {
// 图标区域
Stack({ alignContent: Alignment.Center }) {
Circle()
.width(48)
.height(48)
.fillColor(achievement.unlocked ? '#FFD700' : '#EEEEEE')
Image(achievement.unlocked ? $r('app.media.ic_' + achievement.icon) : $r('app.media.ic_lock'))
.width(24)
.height(24)
.fillColor(achievement.unlocked ? '#FFFFFF' : '#CCCCCC')
}
// 成就名称
Text(achievement.name)
.fontSize(12)
.fontColor(achievement.unlocked ? '#333333' : '#CCCCCC')
.fontWeight(achievement.unlocked ? FontWeight.Medium : FontWeight.Normal)
// 成就描述
Text(achievement.description)
.fontSize(10)
.fontColor(achievement.unlocked ? '#999999' : '#DDDDDD')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.width('100%')
.alignItems(HorizontalAlign.Center)
}
成就系统设计:
- 金色徽章表示已解锁,灰色表示未解锁
- 锁图标表示未解锁状态
- 显示解锁进度,激励用户继续努力
步骤5: 学习趋势图表
趋势图表展示近7天的学习情况,帮助用户分析学习规律:
typescript
/**
* 构建学习趋势图
*/
@Builder
buildTrendChart(): void {
Card() {
Column({ space: 12 }) {
// 标题区域
Row({ space: 8 }) {
Image($r('app.media.ic_trend'))
.width(20)
.height(20)
.fillColor('#4A9B6D')
Text('学习趋势')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Blank()
Text('近7天')
.fontSize(13)
.fontColor('#999999')
}
// 趋势图容器
Stack() {
// 背景网格线
Column({ space: 0 }) {
ForEach([0, 25, 50, 75, 100], (value: number) => {
Row({ space: 0 }) {
Text(value.toString())
.fontSize(10)
.fontColor('#CCCCCC')
.width(30)
Divider().width('100%').color('#EEEEEE').height(1)
}
})
}
// 柱状图
Row({ space: 8 }) {
ForEach(this.calendarData.slice(-7), (day: CalendarDay) => {
Column({ space: 4 }) {
Stack({ alignContent: Alignment.Bottom }) {
Row()
.width(32)
.height(day.studyMinutes * 2)
.backgroundColor('#4A9B6D')
.borderRadius(4)
}
.width(32)
.height(100)
Text(this.getDayName(day.date))
.fontSize(10)
.fontColor('#999999')
}
})
}
.padding({ left: 30 })
}
.height(120)
}
.padding(16)
}
.width('92%')
.margin({ left: '4%', right: '4%', top: 12, bottom: 100 })
}
/**
* 获取日期名称(星期几)
*/
getDayName(date: Date): string {
const days = ['日', '一', '二', '三', '四', '五', '六'];
return days[date.getDay()];
}
图表设计要点:
- Y轴显示学习分钟数(0-100)
- X轴显示星期几
- 柱状高度反映学习时长
数据持久化方案
学习记录数据需要持久化存储,确保应用重启后数据不丢失:
typescript
// 使用Preferences实现数据持久化
import dataPreferences from '@ohos.data.preferences';
class LearningDataManager {
private static PREFERENCES_KEY = 'learningRecords';
/**
* 保存学习记录
*/
static async saveRecords(records: LearningRecord[]): Promise<void> {
try {
const preferences = await dataPreferences.getPreferences(this.context, this.PREFERENCES_KEY);
await preferences.put('records', JSON.stringify(records));
await preferences.flush();
} catch (error) {
console.error('保存学习记录失败: ', error);
}
}
/**
* 加载学习记录
*/
static async loadRecords(): Promise<LearningRecord[]> {
try {
const preferences = await dataPreferences.getPreferences(this.context, this.PREFERENCES_KEY);
const data = await preferences.get('records', '[]');
return JSON.parse(data);
} catch (error) {
console.error('加载学习记录失败: ', error);
return [];
}
}
}
性能优化建议
1. 虚拟列表优化
当成就数量较多时,使用LazyForEach优化渲染性能:
typescript
// 使用LazyForEach替代ForEach
Grid() {
LazyForEach(this.achievementDataSource, (achievement: Achievement) => {
GridItem() {
this.buildAchievementItem(achievement)
}
}, (achievement: Achievement) => achievement.id)
}
2. 数据预处理
在页面加载时预先计算趋势数据:
typescript
aboutToAppear() {
this.loadCalendarData();
this.loadAchievements();
// 预先计算趋势数据
this.precomputeTrendData();
}
private precomputeTrendData(): void {
this.trendData = this.calendarData.slice(-7).map(day => ({
date: day.date,
minutes: day.studyMinutes
}));
}
3. 图片懒加载
成就图标使用懒加载减少初始加载时间:
typescript
Image(achievement.unlocked ? $r('app.media.ic_' + achievement.icon) : $r('app.media.ic_lock'))
.width(24)
.height(24)
.syncLoad(false) // 异步加载
本章小结
核心知识点
本文完成了学习记录页面的完整实现:
1. 数据可视化
- 统计卡片展示核心指标
- 学习日历展示学习连续性
- 成就徽章激励用户
- 趋势图表分析学习规律
2. 状态管理
- 使用@State管理响应式状态
- 模块化数据加载
- 数据持久化存储
3. 用户体验
- 颜色编码直观展示学习量
- 成就系统提供正向反馈
- 图表帮助用户了解学习习惯
下一步预告
学习记录页面已经完成!在下一篇文章中,我们将学习:
- 知识问答页面
- 问答列表展示
- 提问功能
- 回答功能
节气通应用已发布上线,可在应用市场下载体验
相关链接
- 项目源码 : Atomgit仓库