【HarmonyOS 6】练习记录页面 UI 设计

在学习和练习类应用中,记录用户的练习进度是激励持续使用的重要功能。本文将讲解如何设计一个清晰直观的练习记录页面,包含统计概览、日历打卡和成就徽章三个核心模块。

功能概述

练习记录页面主要展示以下内容:

  1. 统计概览卡片:连续练习天数、获得成就数、最长连续记录
  2. 练习日历:30天打卡视图,一眼看出练习频次
  3. 成就徽章:已解锁和未解锁的成就列表
  4. 练习详情:最近有记录的日期详细数据

页面预览

整体采用垂直布局,效果如下:

顶部导航设计

顶部栏使用 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
}

设计要点

  1. 周布局:每行显示一周7天,共4行
  2. 打卡状态
    • 已练习:绿色对勾 + 浅绿背景 #E8F5E9
    • 未练习:灰色圆点 · + 默认背景
  3. 信息层级:日期(月/日)→ 状态图标 → 星期
  4. 格子尺寸: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 设计要点:

  • 统计卡片:三列等宽布局,不同颜色区分数据类型
  • 标签切换:视觉反馈清晰的按钮组
  • 日历视图:周分组布局,颜色标识打卡状态
  • 详情列表:日期 + 三项数据的横向排布
  • 成就徽章:解锁/未解锁状态的差异化设计
  • 空状态:友好的引导提示

页面整体采用卡片式布局,信息层次清晰,配色鲜明但不杂乱,用户可以快速了解自己的练习情况并获得成就感。

相关推荐
CodeForCoffee2 小时前
Adobe illustrator将AI绘制图片转换为矢量图
ui·adobe·illustrator
浮芷.2 小时前
生命科学数据视界防御:基于鸿蒙Flutter陀螺仪云台与三维体积光栅的视轴锁定架构
flutter·华为·架构·开源·harmonyos·鸿蒙
RReality2 小时前
【Unity Shader URP】平面反射(Planar Reflection)实战教程
ui·平面·unity·游戏引擎·图形渲染·材质
浮芷.2 小时前
微观搜打撤:基于鸿蒙flutter的内存快照算法的局内外状态隔离与高阶背包系统设计
算法·flutter·华为·开源·harmonyos·鸿蒙
浮芷.3 小时前
东方修仙模拟器:基于 鸿蒙Flutter 状态机与 CustomPainter 的境界跃升与天劫渲染架构
科技·flutter·华为·架构·开源·harmonyos·鸿蒙
民乐团扒谱机3 小时前
基于ArkTS与端云协同的鸿蒙智慧校园助手——项目报告(AIGC预警⚠️)
华为·aigc·harmonyos
汽车芯猿3 小时前
压扁的图像:嵌入式设备中的长方形像素之谜
嵌入式硬件·ui·photoshop
互联网散修3 小时前
鸿蒙实战:运动健康类应用核心组件——语音播报模块设计与实现
华为·harmonyos·tts·语音播报
想你依然心痛3 小时前
HarmonyOS 6智能家居实战:基于悬浮导航与沉浸光感的“光影智家“全屋智能控制系统
华为·智能家居·harmonyos·智能控制·悬浮导航·沉浸光感