HarmonyOS应用<节气通>开发第15篇:学习记录页面

引言

学习记录页面是用户追踪学习进度、了解学习习惯的核心入口。一个设计良好的学习记录页面不仅能展示数据,更能激励用户持续学习。本文将实现一个完整的学习记录页面,通过数据可视化和成就系统,为用户提供直观的学习反馈和动力。


学习目标

完成本文后,你将能够:

  • ✅ 理解学习记录页面的设计原则
  • ✅ 实现数据可视化组件
  • ✅ 创建学习日历展示
  • ✅ 实现成就系统
  • ✅ 绘制学习趋势图表

需求分析

功能模块设计

学习记录页面通常包含以下核心模块:

模块 功能描述 技术要点 用户价值
学习统计 总学习天数、阅读文章数、测验次数、积分 卡片布局、数字展示 快速了解学习概况
学习日历 展示最近30天学习情况 日历网格、颜色深浅 直观展示学习连续性
学习成就 展示获得的成就徽章 网格布局、状态管理 激励持续学习
学习趋势 展示学习趋势变化 柱状图、数据绑定 分析学习规律

用户场景分析

用户访问学习记录页面的常见场景:

  1. 每日回顾:查看当天学习情况
  2. 目标追踪:检查学习目标完成进度
  3. 成就解锁:查看已获得和未获得的成就
  4. 数据分析:了解学习习惯和规律

设计思路

数据可视化方案

数据可视化是学习记录页面的核心,需要选择合适的展示方式:

数据类型 推荐展示方式 技术实现
统计数字 卡片+大数字 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. 用户体验

  • 颜色编码直观展示学习量
  • 成就系统提供正向反馈
  • 图表帮助用户了解学习习惯

下一步预告

学习记录页面已经完成!在下一篇文章中,我们将学习:

  • 知识问答页面
  • 问答列表展示
  • 提问功能
  • 回答功能

节气通应用已发布上线,可在应用市场下载体验


相关链接

相关推荐
YangYang9YangYan2 小时前
民办本科大数据专业学习数据分析的价值分析
大数据·学习·数据分析
阿钱真强道2 小时前
28 鸿蒙LiteOS RK2206 LwIP Raw API 实现无阻塞UDP双向通信
udp·harmonyos·鸿蒙·lwip·开源鸿蒙
brave_zhao2 小时前
head方法可以用于http url嗅探吗
学习
库奇噜啦呼2 小时前
【iOS】源码学习-YYModel源码学习
学习·ios·cocoa
Davina_yu2 小时前
网络请求基础:使用http模块发起GET/POST请求(12)
harmonyos·鸿蒙·鸿蒙系统
伶俜662 小时前
# [特殊字符] 零基础学 ArkUI 手势(专题五):从点击到多指触控,一网打尽 6 种手势
学习·华为·harmonyos
枫叶丹42 小时前
【HarmonyOS 6.0】MDM Kit 新增限制策略深度解析:短信、蜂窝数据、飞行模式、通知消息与 NFC 管控
开发语言·华为·harmonyos
辰海Coding2 小时前
MiniSpring框架学习笔记-JDBC 访问框架:如何抽取 JDBC 模板并隔离数据库?
java·数据库·笔记·学习·spring
十月的皮皮2 小时前
C语言学习笔记20260609-字符串反转两种实现方法
c语言·笔记·学习