65. [HarmonyOS NEXT 实战案例七] 健身课程网格布局(上)

简介: 本教程将介绍如何使用HarmonyOS NEXT的GridRow和GridCol组件实现健身课程的网格布局展示。健身课程网格布局是一种常见的UI设计模式,适用于展示各种健身课程信息,包括课程名称、教练信息、课程时长、难度级别等。通过网格布局,用户可以快速浏览多个课程,并根据自己的需求选择合适的课程。

[HarmonyOS NEXT 实战案例七] 健身课程网格布局(上)

项目已开源,开源地址: https://gitcode.com/nutpi/HarmonyosNextCaseStudyTutorial , 欢迎fork & star

效果演示

img_34198a71.png

1. 概述

本教程将介绍如何使用HarmonyOS NEXT的GridRow和GridCol组件实现健身课程的网格布局展示。健身课程网格布局是一种常见的UI设计模式,适用于展示各种健身课程信息,包括课程名称、教练信息、课程时长、难度级别等。通过网格布局,用户可以快速浏览多个课程,并根据自己的需求选择合适的课程。

本教程将涵盖以下内容:

  • 健身课程数据结构设计
  • 数据准备
  • 布局实现
    • 整体布局结构
    • 顶部搜索栏
    • 分类标签栏
    • 课程网格列表
    • 课程卡片实现
  • GridRow和GridCol配置详解
  • 布局效果分析

2. 数据结构设计

首先,我们需要定义健身课程的数据结构,包括课程的基本信息和分类信息。

// 课程难度级别
enum DifficultyLevel {
   
  BEGINNER = '初级',
  INTERMEDIATE = '中级',
  ADVANCED = '高级'
}

// 课程类型
interface CourseCategory {
   
  id: string;        // 分类ID
  name: string;      // 分类名称
  icon?: ResourceStr; // 分类图标
}

// 健身课程
interface FitnessCourse {
   
  id: string;                // 课程ID
  name: string;              // 课程名称
  image: ResourceStr;        // 课程封面图
  duration: number;          // 课程时长(分钟)
  difficulty: DifficultyLevel; // 难度级别
  categoryId: string;        // 所属分类ID
  coach: string;             // 教练姓名
  coachAvatar?: ResourceStr; // 教练头像
  calories: number;          // 消耗卡路里
  rating: number;            // 评分(1-5)
  participants: number;      // 参与人数
  description: string;       // 课程描述
  isFree: boolean;           // 是否免费
  price?: number;            // 价格(如果不免费)
  tags?: string[];           // 标签(如"热门"、"新课"等)
}
AI 代码解读

3. 数据准备

接下来,我们准备一些模拟数据用于展示。

// 课程分类数据
private categories: CourseCategory[] = [
  {
    id: 'all', name: '全部' },
  {
    id: 'yoga', name: '瑜伽', icon: $r('app.media.ic_yoga') },
  {
    id: 'hiit', name: '高强度间歇训练', icon: $r('app.media.ic_hiit') },
  {
    id: 'cardio', name: '有氧训练', icon: $r('app.media.ic_cardio') },
  {
    id: 'strength', name: '力量训练', icon: $r('app.media.ic_strength') },
  {
    id: 'pilates', name: '普拉提', icon: $r('app.media.ic_pilates') },
  {
    id: 'dance', name: '舞蹈', icon: $r('app.media.ic_dance') },
  {
    id: 'stretching', name: '拉伸', icon: $r('app.media.ic_stretching') }
];

// 课程数据
private courses: FitnessCourse[] = [
  {
   
    id: '1',
    name: '初级瑜伽入门',
    image: $r('app.media.yoga_beginner'),
    duration: 30,
    difficulty: DifficultyLevel.BEGINNER,
    categoryId: 'yoga',
    coach: '李明',
    coachAvatar: $r('app.media.coach1'),
    calories: 150,
    rating: 4.8,
    participants: 1250,
    description: '适合瑜伽初学者的入门课程,帮助你掌握基本姿势和呼吸技巧。',
    isFree: true,
    tags: ['热门', '新手推荐']
  },
  {
   
    id: '2',
    name: '20分钟HIIT燃脂',
    image: $r('app.media.hiit_fat_burn'),
    duration: 20,
    difficulty: DifficultyLevel.INTERMEDIATE,
    categoryId: 'hiit',
    coach: '张强',
    coachAvatar: $r('app.media.coach2'),
    calories: 300,
    rating: 4.7,
    participants: 980,
    description: '高效燃脂的HIIT训练,20分钟内最大化燃烧卡路里。',
    isFree: false,
    price: 15,
    tags: ['热门', '燃脂']
  },
  {
   
    id: '3',
    name: '全身力量训练',
    image: $r('app.media.strength_full_body'),
    duration: 45,
    difficulty: DifficultyLevel.INTERMEDIATE,
    categoryId: 'strength',
    coach: '王刚',
    coachAvatar: $r('app.media.coach3'),
    calories: 350,
    rating: 4.6,
    participants: 850,
    description: '全面锻炼全身肌肉群的力量训练课程,提升肌肉力量和耐力。',
    isFree: false,
    price: 20,
    tags: ['肌肉塑造']
  },
  {
   
    id: '4',
    name: '舒缓拉伸',
    image: $r('app.media.stretching_relax'),
    duration: 25,
    difficulty: DifficultyLevel.BEGINNER,
    categoryId: 'stretching',
    coach: '刘芳',
    coachAvatar: $r('app.media.coach4'),
    calories: 100,
    rating: 4.9,
    participants: 1500,
    description: '舒缓的拉伸课程,帮助放松肌肉,增加柔韧性,减轻疲劳。',
    isFree: true,
    tags: ['放松', '恢复']
  },
  {
   
    id: '5',
    name: '有氧舞蹈',
    image: $r('app.media.dance_cardio'),
    duration: 40,
    difficulty: DifficultyLevel.INTERMEDIATE,
    categoryId: 'dance',
    coach: '周丽',
    coachAvatar: $r('app.media.coach5'),
    calories: 280,
    rating: 4.7,
    participants: 1100,
    description: '充满活力的舞蹈课程,在欢快的音乐中燃烧卡路里。',
    isFree: false,
    price: 18,
    tags: ['热门', '有趣']
  },
  {
   
    id: '6',
    name: '高级瑜伽挑战',
    image: $r('app.media.yoga_advanced'),
    duration: 60,
    difficulty: DifficultyLevel.ADVANCED,
    categoryId: 'yoga',
    coach: '张华',
    coachAvatar: $r('app.media.coach6'),
    calories: 220,
    rating: 4.5,
    participants: 650,
    description: '适合有经验的瑜伽练习者,挑战高难度姿势和流程。',
    isFree: false,
    price: 25,
    tags: ['挑战']
  },
  {
   
    id: '7',
    name: '普拉提核心训练',
    image: $r('app.media.pilates_core'),
    duration: 35,
    difficulty: DifficultyLevel.INTERMEDIATE,
    categoryId: 'pilates',
    coach: '李娜',
    coachAvatar: $r('app.media.coach7'),
    calories: 180,
    rating: 4.8,
    participants: 920,
    description: '专注于核心肌群的普拉提训练,增强核心力量,改善姿势。',
    isFree: false,
    price: 20,
    tags: ['核心', '塑形']
  },
  {
   
    id: '8',
    name: '30分钟快速有氧',
    image: $r('app.media.cardio_quick'),
    duration: 30,
    difficulty: DifficultyLevel.INTERMEDIATE,
    categoryId: 'cardio',
    coach: '王明',
    coachAvatar: $r('app.media.coach8'),
    calories: 250,
    rating: 4.6,
    participants: 780,
    description: '高效的有氧训练,在短时间内提高心率,增强心肺功能。',
    isFree: true,
    tags: ['快速', '高效']
  },
  {
   
    id: '9',
    name: '初级力量入门',
    image: $r('app.media.strength_beginner'),
    duration: 40,
    difficulty: DifficultyLevel.BEGINNER,
    categoryId: 'strength',
    coach: '张伟',
    coachAvatar: $r('app.media.coach9'),
    calories: 200,
    rating: 4.7,
    participants: 950,
    description: '适合力量训练初学者的入门课程,学习基本动作和技巧。',
    isFree: true,
    tags: ['新手推荐']
  },
  {
   
    id: '10',
    name: '高强度HIIT挑战',
    image: $r('app.media.hiit_challenge'),
    duration: 45,
    difficulty: DifficultyLevel.ADVANCED,
    categoryId: 'hiit',
    coach: '刘强',
    coachAvatar: $r('app.media.coach10'),
    calories: 400,
    rating: 4.5,
    participants: 680,
    description: '极具挑战性的高强度间歇训练,适合有经验的健身爱好者。',
    isFree: false,
    price: 22,
    tags: ['挑战', '燃脂']
  },
  {
   
    id: '11',
    name: '拉丁舞基础',
    image: $r('app.media.dance_latin'),
    duration: 50,
    difficulty: DifficultyLevel.BEGINNER,
    categoryId: 'dance',
    coach: '马丽',
    coachAvatar: $r('app.media.coach11'),
    calories: 260,
    rating: 4.8,
    participants: 1050,
    description: '学习拉丁舞的基本步伐和动作,在舞蹈中享受乐趣。',
    isFree: false,
    price: 18,
    tags: ['有趣', '新手推荐']
  },
  {
   
    id: '12',
    name: '冥想与放松',
    image: $r('app.media.yoga_meditation'),
    duration: 20,
    difficulty: DifficultyLevel.BEGINNER,
    categoryId: 'yoga',
    coach: '王芳',
    coachAvatar: $r('app.media.coach12'),
    calories: 80,
    rating: 4.9,
    participants: 1300,
    description: '通过冥想和呼吸练习,缓解压力,放松身心。',
    isFree: true,
    tags: ['放松', '减压']
  }
];

// 状态变量
@State searchText: string = ''; // 搜索文本
@State currentCategory: string = 'all'; // 当前选中的分类
AI 代码解读

4. 布局实现

4.1 整体布局结构

首先,我们实现整体布局结构,包括顶部搜索栏、分类标签栏和课程网格列表。

@Entry
@Component
struct FitnessCourseGrid {
   
  // 数据和状态变量定义
  // ...

  build() {
   
    Column() {
   
      // 顶部搜索栏
      this.SearchBar()

      // 分类标签栏
      this.CategoryTabs()

      // 课程网格列表
      this.CourseGrid()
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  // 其他方法
  // ...
}
AI 代码解读

4.2 顶部搜索栏

@Builder
private SearchBar() {
   
  Row() {
   
    // 搜索框
    Row() {
   
      Image($r('app.media.ic_search'))
        .width(20)
        .height(20)
        .fillColor('#999999')
        .margin({
    right: 8 })

      TextInput({
    placeholder: '搜索健身课程', text: this.searchText })
        .fontSize(14)
        .fontColor('#333333')
        .placeholderColor('#999999')
        .backgroundColor('transparent')
        .width('100%')
        .height(36)
        .onChange((value: string) => {
   
          this.searchText = value;
        })
    }
    .width('85%')
    .height(36)
    .backgroundColor('#FFFFFF')
    .borderRadius(18)
    .padding({
    left: 12, right: 12 })

    // 筛选按钮
    Image($r('app.media.ic_filter'))
      .width(24)
      .height(24)
      .fillColor('#333333')
      .margin({
    left: 12 })
  }
  .width('100%')
  .height(56)
  .padding({
    left: 16, right: 16 })
  .backgroundColor('#FFFFFF')
}
AI 代码解读

4.3 分类标签栏

@Builder
private CategoryTabs() {
   
  Scroll() {
   
    Row() {
   
      ForEach(this.categories, (category: CourseCategory) => {
   
        Column() {
   
          if (category.icon && category.id !== 'all') {
   
            Image(category.icon)
              .width(24)
              .height(24)
              .fillColor(this.currentCategory === category.id ? '#FF5722' : '#666666')
              .margin({
    bottom: 4 })
          }

          Text(category.name)
            .fontSize(14)
            .fontWeight(this.currentCategory === category.id ? FontWeight.Bold : FontWeight.Normal)
            .fontColor(this.currentCategory === category.id ? '#FF5722' : '#666666')
        }
        .width(category.id === 'all' ? 56 : 80)
        .height(56)
        .justifyContent(FlexAlign.Center)
        .backgroundColor(this.currentCategory === category.id ? '#FFF3E0' : 'transparent')
        .borderRadius(8)
        .margin({
    right: 12 })
        .onClick(() => {
   
          this.currentCategory = category.id;
        })
      })
    }
    .padding({
    left: 16, right: 16 })
  }
  .scrollBar(BarState.Off)
  .scrollable(ScrollDirection.Horizontal)
  .width('100%')
  .height(72)
  .backgroundColor('#FFFFFF')
  .margin({
    bottom: 8 })
}
AI 代码解读

4.4 课程网格列表

@Builder
private CourseGrid() {
   
  Scroll() {
   
    Column() {
   
      // 分类标题
      if (this.currentCategory !== 'all') {
   
        Row() {
   
          Text(this.getCategoryName(this.currentCategory))
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')

          Text(`${
     this.getFilteredCourses().length}个课程`)
            .fontSize(14)
            .fontColor('#999999')
            .margin({
    left: 8 })
        }
        .width('100%')
        .padding({
    left: 16, right: 16, top: 16, bottom: 8 })
      }

      // 课程网格
      GridRow({
    columns: {
    sm: 2, md: 3, lg: 4 }, gutter: {
    x: 12, y: 12 } }) {
   
        ForEach(this.getFilteredCourses(), (course: FitnessCourse) => {
   
          GridCol() {
   
            this.CourseCard(course)
          }
        })
      }
      .width('100%')
      .padding(16)
    }
    .width('100%')
  }
  .scrollBar(BarState.Off)
  .scrollable(ScrollDirection.Vertical)
  .width('100%')
  .layoutWeight(1)
  .backgroundColor('#F5F5F5')

  // 获取分类名称
  private getCategoryName(categoryId: string): string {
   
    const category = this.categories.find(item => item.id === categoryId);
    return category ? category.name : '';
  }

  // 获取筛选后的课程
  private getFilteredCourses(): FitnessCourse[] {
   
    let filtered = this.courses;

    // 按分类筛选
    if (this.currentCategory !== 'all') {
   
      filtered = filtered.filter(item => item.categoryId === this.currentCategory);
    }

    // 按搜索文本筛选
    if (this.searchText.trim() !== '') {
   
      const keyword = this.searchText.toLowerCase();
      filtered = filtered.filter(item => 
        item.name.toLowerCase().includes(keyword) || 
        item.description.toLowerCase().includes(keyword) ||
        item.coach.toLowerCase().includes(keyword)
      );
    }

    return filtered;
  }
}
AI 代码解读

4.5 课程卡片实现

@Builder
private CourseCard(course: FitnessCourse) {
   
  Column() {
   
    // 课程封面图
    Stack() {
   
      Image(course.image)
        .width('100%')
        .height(120)
        .borderRadius({
    topLeft: 8, topRight: 8 })
        .objectFit(ImageFit.Cover)

      // 课程时长
      Row() {
   
        Image($r('app.media.ic_time'))
          .width(12)
          .height(12)
          .fillColor(Color.White)
          .margin({
    right: 4 })

        Text(`${
     course.duration}分钟`)
          .fontSize(12)
          .fontColor(Color.White)
      }
      .height(20)
      .padding({
    left: 6, right: 6 })
      .backgroundColor('rgba(0, 0, 0, 0.6)')
      .borderRadius(10)
      .position({
    x: 8, y: 8 })

      // 难度级别
      Text(course.difficulty)
        .fontSize(12)
        .fontColor(Color.White)
        .backgroundColor(this.getDifficultyColor(course.difficulty))
        .borderRadius(10)
        .padding({
    left: 6, right: 6 })
        .height(20)
        .position({
    x: '70%', y: 8 })

      // 标签(如果有)
      if (course.tags && course.tags.length > 0) {
   
        Row() {
   
          ForEach(course.tags.slice(0, 2), (tag: string) => {
   
            Text(tag)
              .fontSize(10)
              .fontColor(Color.White)
              .backgroundColor('#FF9800')
              .borderRadius(4)
              .padding({
    left: 4, right: 4, top: 2, bottom: 2 })
              .margin({
    right: 4 })
          })
        }
        .position({
    x: 8, y: 90 })
      }

      // 免费或价格标签
      Text(course.isFree ? '免费' : ${
     course.price}`)
        .fontSize(12)
        .fontColor(Color.White)
        .backgroundColor(course.isFree ? '#4CAF50' : '#FF5722')
        .borderRadius(10)
        .padding({
    left: 6, right: 6 })
        .height(20)
        .position({
    x: '70%', y: 90 })
    }
    .width('100%')
    .height(120)

    // 课程信息
    Column() {
   
      // 课程名称
      Text(course.name)
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')
        .maxLines(1)
        .textOverflow({
    overflow: TextOverflow.Ellipsis })
        .width('100%')

      // 教练信息
      Row() {
   
        if (course.coachAvatar) {
   
          Image(course.coachAvatar)
            .width(16)
            .height(16)
            .borderRadius(8)
            .margin({
    right: 4 })
        }

        Text(course.coach)
          .fontSize(12)
          .fontColor('#666666')
      }
      .width('100%')
      .margin({
    top: 4 })

      // 评分和参与人数
      Row() {
   
        // 评分
        Row() {
   
          ForEach([1, 2, 3, 4, 5], (item: number) => {
   
            Image($r('app.media.ic_star'))
              .width(12)
              .height(12)
              .fillColor(item <= Math.floor(course.rating) ? '#FFB300' : '#E0E0E0')
              .margin({
    right: 2 })
          })

          Text(course.rating.toFixed(1))
            .fontSize(12)
            .fontColor('#FFB300')
            .margin({
    left: 4 })
        }

        Blank()

        // 参与人数
        Text(`${
     course.participants}人参与`)
          .fontSize(12)
          .fontColor('#999999')
      }
      .width('100%')
      .margin({
    top: 4 })

      // 卡路里消耗
      Row() {
   
        Image($r('app.media.ic_calories'))
          .width(14)
          .height(14)
          .fillColor('#FF5722')
          .margin({
    right: 4 })

        Text(`${
     course.calories}千卡`)
          .fontSize(12)
          .fontColor('#FF5722')
      }
      .width('100%')
      .margin({
    top: 4 })
    }
    .width('100%')
    .padding(8)
  }
  .width('100%')
  .backgroundColor(Color.White)
  .borderRadius(8)
  .shadow({
   
    radius: 4,
    color: '#1A000000',
    offsetX: 0,
    offsetY: 2
  })

  // 获取难度对应的颜色
  private getDifficultyColor(difficulty: DifficultyLevel): string {
   
    switch (difficulty) {
   
      case DifficultyLevel.BEGINNER:
        return '#4CAF50'; // 绿色
      case DifficultyLevel.INTERMEDIATE:
        return '#FF9800'; // 橙色
      case DifficultyLevel.ADVANCED:
        return '#F44336'; // 红色
      default:
        return '#999999'; // 灰色
    }
  }
}
AI 代码解读

5. GridRow和GridCol配置详解

在本案例中,我们使用GridRow和GridCol组件实现了响应式的网格布局。下面详细解析这些组件的配置:

5.1 GridRow配置

GridRow({
    columns: {
    sm: 2, md: 3, lg: 4 }, gutter: {
    x: 12, y: 12 } }) {
   
  // 内容
}
AI 代码解读
  • columns:定义不同屏幕尺寸下的列数
    • sm:小屏幕(如手机)显示2列
    • md:中等屏幕(如平板)显示3列
    • lg:大屏幕(如桌面)显示4列
  • gutter:定义网格项之间的间距
    • x:水平间距为12vp
    • y:垂直间距为12vp

这种配置使得我们的布局能够自动适应不同屏幕尺寸,在小屏幕上显示较少的列数,在大屏幕上显示更多的列数,提供更好的用户体验。

5.2 GridCol配置

GridCol() {
   
  // 内容
}
AI 代码解读

在本案例中,我们没有为GridCol指定特殊的配置,这意味着每个网格项将根据GridRow的columns配置自动计算宽度。例如,在小屏幕上,每个网格项的宽度为50%(减去间距);在中等屏幕上,每个网格项的宽度为33.33%(减去间距);在大屏幕上,每个网格项的宽度为25%(减去间距)。

如果需要某个网格项占据多列,可以使用span属性,例如:

GridCol({
    span: {
    sm: 2, md: 1, lg: 1 } }) {
   
  // 内容
}
AI 代码解读

这表示在小屏幕上,该网格项占据2列(即整行);在中等和大屏幕上,该网格项占据1列。

6. 布局效果分析

通过使用GridRow和GridCol组件,我们实现了一个响应式的健身课程网格布局,具有以下特点:

  1. 响应式布局:根据屏幕尺寸自动调整列数,提供最佳的视觉体验。
  2. 灵活的卡片设计:每个课程卡片包含丰富的信息,包括课程封面图、课程名称、教练信息、评分、参与人数、卡路里消耗等。
  3. 分类筛选:通过分类标签栏,用户可以快速筛选不同类型的课程。
  4. 搜索功能:通过顶部搜索栏,用户可以搜索特定的课程。
  5. 视觉层次:通过阴影、圆角、颜色等视觉元素,创建清晰的视觉层次,突出重要信息。

这种布局特别适合展示大量结构化的课程信息,让用户能够快速浏览和筛选感兴趣的课程。

7. 完整代码

@Entry
@Component
struct FitnessCourseGrid {
   
  // 课程难度级别
  enum DifficultyLevel {
   
    BEGINNER = '初级',
    INTERMEDIATE = '中级',
    ADVANCED = '高级'
  }

  // 课程类型
  interface CourseCategory {
   
    id: string;        // 分类ID
    name: string;      // 分类名称
    icon?: ResourceStr; // 分类图标
  }

  // 健身课程
  interface FitnessCourse {
   
    id: string;                // 课程ID
    name: string;              // 课程名称
    image: ResourceStr;        // 课程封面图
    duration: number;          // 课程时长(分钟)
    difficulty: DifficultyLevel; // 难度级别
    categoryId: string;        // 所属分类ID
    coach: string;             // 教练姓名
    coachAvatar?: ResourceStr; // 教练头像
    calories: number;          // 消耗卡路里
    rating: number;            // 评分(1-5)
    participants: number;      // 参与人数
    description: string;       // 课程描述
    isFree: boolean;           // 是否免费
    price?: number;            // 价格(如果不免费)
    tags?: string[];           // 标签(如"热门"、"新课"等)
  }

  // 课程分类数据
  private categories: CourseCategory[] = [
    {
    id: 'all', name: '全部' },
    {
    id: 'yoga', name: '瑜伽', icon: $r('app.media.ic_yoga') },
    {
    id: 'hiit', name: '高强度间歇训练', icon: $r('app.media.ic_hiit') },
    {
    id: 'cardio', name: '有氧训练', icon: $r('app.media.ic_cardio') },
    {
    id: 'strength', name: '力量训练', icon: $r('app.media.ic_strength') },
    {
    id: 'pilates', name: '普拉提', icon: $r('app.media.ic_pilates') },
    {
    id: 'dance', name: '舞蹈', icon: $r('app.media.ic_dance') },
    {
    id: 'stretching', name: '拉伸', icon: $r('app.media.ic_stretching') }
  ];

  // 课程数据
  private courses: FitnessCourse[] = [
    {
   
      id: '1',
      name: '初级瑜伽入门',
      image: $r('app.media.yoga_beginner'),
      duration: 30,
      difficulty: DifficultyLevel.BEGINNER,
      categoryId: 'yoga',
      coach: '李明',
      coachAvatar: $r('app.media.coach1'),
      calories: 150,
      rating: 4.8,
      participants: 1250,
      description: '适合瑜伽初学者的入门课程,帮助你掌握基本姿势和呼吸技巧。',
      isFree: true,
      tags: ['热门', '新手推荐']
    },
    {
   
      id: '2',
      name: '20分钟HIIT燃脂',
      image: $r('app.media.hiit_fat_burn'),
      duration: 20,
      difficulty: DifficultyLevel.INTERMEDIATE,
      categoryId: 'hiit',
      coach: '张强',
      coachAvatar: $r('app.media.coach2'),
      calories: 300,
      rating: 4.7,
      participants: 980,
      description: '高效燃脂的HIIT训练,20分钟内最大化燃烧卡路里。',
      isFree: false,
      price: 15,
      tags: ['热门', '燃脂']
    },
    {
   
      id: '3',
      name: '全身力量训练',
      image: $r('app.media.strength_full_body'),
      duration: 45,
      difficulty: DifficultyLevel.INTERMEDIATE,
      categoryId: 'strength',
      coach: '王刚',
      coachAvatar: $r('app.media.coach3'),
      calories: 350,
      rating: 4.6,
      participants: 850,
      description: '全面锻炼全身肌肉群的力量训练课程,提升肌肉力量和耐力。',
      isFree: false,
      price: 20,
      tags: ['肌肉塑造']
    },
    {
   
      id: '4',
      name: '舒缓拉伸',
      image: $r('app.media.stretching_relax'),
      duration: 25,
      difficulty: DifficultyLevel.BEGINNER,
      categoryId: 'stretching',
      coach: '刘芳',
      coachAvatar: $r('app.media.coach4'),
      calories: 100,
      rating: 4.9,
      participants: 1500,
      description: '舒缓的拉伸课程,帮助放松肌肉,增加柔韧性,减轻疲劳。',
      isFree: true,
      tags: ['放松', '恢复']
    },
    {
   
      id: '5',
      name: '有氧舞蹈',
      image: $r('app.media.dance_cardio'),
      duration: 40,
      difficulty: DifficultyLevel.INTERMEDIATE,
      categoryId: 'dance',
      coach: '周丽',
      coachAvatar: $r('app.media.coach5'),
      calories: 280,
      rating: 4.7,
      participants: 1100,
      description: '充满活力的舞蹈课程,在欢快的音乐中燃烧卡路里。',
      isFree: false,
      price: 18,
      tags: ['热门', '有趣']
    },
    {
   
      id: '6',
      name: '高级瑜伽挑战',
      image: $r('app.media.yoga_advanced'),
      duration: 60,
      difficulty: DifficultyLevel.ADVANCED,
      categoryId: 'yoga',
      coach: '张华',
      coachAvatar: $r('app.media.coach6'),
      calories: 220,
      rating: 4.5,
      participants: 650,
      description: '适合有经验的瑜伽练习者,挑战高难度姿势和流程。',
      isFree: false,
      price: 25,
      tags: ['挑战']
    },
    {
   
      id: '7',
      name: '普拉提核心训练',
      image: $r('app.media.pilates_core'),
      duration: 35,
      difficulty: DifficultyLevel.INTERMEDIATE,
      categoryId: 'pilates',
      coach: '李娜',
      coachAvatar: $r('app.media.coach7'),
      calories: 180,
      rating: 4.8,
      participants: 920,
      description: '专注于核心肌群的普拉提训练,增强核心力量,改善姿势。',
      isFree: false,
      price: 20,
      tags: ['核心', '塑形']
    },
    {
   
      id: '8',
      name: '30分钟快速有氧',
      image: $r('app.media.cardio_quick'),
      duration: 30,
      difficulty: DifficultyLevel.INTERMEDIATE,
      categoryId: 'cardio',
      coach: '王明',
      coachAvatar: $r('app.media.coach8'),
      calories: 250,
      rating: 4.6,
      participants: 780,
      description: '高效的有氧训练,在短时间内提高心率,增强心肺功能。',
      isFree: true,
      tags: ['快速', '高效']
    },
    {
   
      id: '9',
      name: '初级力量入门',
      image: $r('app.media.strength_beginner'),
      duration: 40,
      difficulty: DifficultyLevel.BEGINNER,
      categoryId: 'strength',
      coach: '张伟',
      coachAvatar: $r('app.media.coach9'),
      calories: 200,
      rating: 4.7,
      participants: 950,
      description: '适合力量训练初学者的入门课程,学习基本动作和技巧。',
      isFree: true,
      tags: ['新手推荐']
    },
    {
   
      id: '10',
      name: '高强度HIIT挑战',
      image: $r('app.media.hiit_challenge'),
      duration: 45,
      difficulty: DifficultyLevel.ADVANCED,
      categoryId: 'hiit',
      coach: '刘强',
      coachAvatar: $r('app.media.coach10'),
      calories: 400,
      rating: 4.5,
      participants: 680,
      description: '极具挑战性的高强度间歇训练,适合有经验的健身爱好者。',
      isFree: false,
      price: 22,
      tags: ['挑战', '燃脂']
    },
    {
   
      id: '11',
      name: '拉丁舞基础',
      image: $r('app.media.dance_latin'),
      duration: 50,
      difficulty: DifficultyLevel.BEGINNER,
      categoryId: 'dance',
      coach: '马丽',
      coachAvatar: $r('app.media.coach11'),
      calories: 260,
      rating: 4.8,
      participants: 1050,
      description: '学习拉丁舞的基本步伐和动作,在舞蹈中享受乐趣。',
      isFree: false,
      price: 18,
      tags: ['有趣', '新手推荐']
    },
    {
   
      id: '12',
      name: '冥想与放松',
      image: $r('app.media.yoga_meditation'),
      duration: 20,
      difficulty: DifficultyLevel.BEGINNER,
      categoryId: 'yoga',
      coach: '王芳',
      coachAvatar: $r('app.media.coach12'),
      calories: 80,
      rating: 4.9,
      participants: 1300,
      description: '通过冥想和呼吸练习,缓解压力,放松身心。',
      isFree: true,
      tags: ['放松', '减压']
    }
  ];

  // 状态变量
  @State searchText: string = ''; // 搜索文本
  @State currentCategory: string = 'all'; // 当前选中的分类

  build() {
   
    Column() {
   
      // 顶部搜索栏
      this.SearchBar()

      // 分类标签栏
      this.CategoryTabs()

      // 课程网格列表
      this.CourseGrid()
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  @Builder
  private SearchBar() {
   
    Row() {
   
      // 搜索框
      Row() {
   
        Image($r('app.media.ic_search'))
          .width(20)
          .height(20)
          .fillColor('#999999')
          .margin({
    right: 8 })

        TextInput({
    placeholder: '搜索健身课程', text: this.searchText })
          .fontSize(14)
          .fontColor('#333333')
          .placeholderColor('#999999')
          .backgroundColor('transparent')
          .width('100%')
          .height(36)
          .onChange((value: string) => {
   
            this.searchText = value;
          })
      }
      .width('85%')
      .height(36)
      .backgroundColor('#FFFFFF')
      .borderRadius(18)
      .padding({
    left: 12, right: 12 })

      // 筛选按钮
      Image($r('app.media.ic_filter'))
        .width(24)
        .height(24)
        .fillColor('#333333')
        .margin({
    left: 12 })
    }
    .width('100%')
    .height(56)
    .padding({
    left: 16, right: 16 })
    .backgroundColor('#FFFFFF')
  }

  @Builder
  private CategoryTabs() {
   
    Scroll() {
   
      Row() {
   
        ForEach(this.categories, (category: CourseCategory) => {
   
          Column() {
   
            if (category.icon && category.id !== 'all') {
   
              Image(category.icon)
                .width(24)
                .height(24)
                .fillColor(this.currentCategory === category.id ? '#FF5722' : '#666666')
                .margin({
    bottom: 4 })
            }

            Text(category.name)
              .fontSize(14)
              .fontWeight(this.currentCategory === category.id ? FontWeight.Bold : FontWeight.Normal)
              .fontColor(this.currentCategory === category.id ? '#FF5722' : '#666666')
          }
          .width(category.id === 'all' ? 56 : 80)
          .height(56)
          .justifyContent(FlexAlign.Center)
          .backgroundColor(this.currentCategory === category.id ? '#FFF3E0' : 'transparent')
          .borderRadius(8)
          .margin({
    right: 12 })
          .onClick(() => {
   
            this.currentCategory = category.id;
          })
        })
      }
      .padding({
    left: 16, right: 16 })
    }
    .scrollBar(BarState.Off)
    .scrollable(ScrollDirection.Horizontal)
    .width('100%')
    .height(72)
    .backgroundColor('#FFFFFF')
    .margin({
    bottom: 8 })
  }

  @Builder
  private CourseGrid() {
   
    Scroll() {
   
      Column() {
   
        // 分类标题
        if (this.currentCategory !== 'all') {
   
          Row() {
   
            Text(this.getCategoryName(this.currentCategory))
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#333333')

            Text(`${
     this.getFilteredCourses().length}个课程`)
              .fontSize(14)
              .fontColor('#999999')
              .margin({
    left: 8 })
          }
          .width('100%')
          .padding({
    left: 16, right: 16, top: 16, bottom: 8 })
        }

        // 课程网格
        GridRow({
    columns: {
    sm: 2, md: 3, lg: 4 }, gutter: {
    x: 12, y: 12 } }) {
   
          ForEach(this.getFilteredCourses(), (course: FitnessCourse) => {
   
            GridCol() {
   
              this.CourseCard(course)
            }
          })
        }
        .width('100%')
        .padding(16)
      }
      .width('100%')
    }
    .scrollBar(BarState.Off)
    .scrollable(ScrollDirection.Vertical)
    .width('100%')
    .layoutWeight(1)
    .backgroundColor('#F5F5F5')
  }

  @Builder
  private CourseCard(course: FitnessCourse) {
   
    Column() {
   
      // 课程封面图
      Stack() {
   
        Image(course.image)
          .width('100%')
          .height(120)
          .borderRadius({
    topLeft: 8, topRight: 8 })
          .objectFit(ImageFit.Cover)

        // 课程时长
        Row() {
   
          Image($r('app.media.ic_time'))
            .width(12)
            .height(12)
            .fillColor(Color.White)
            .margin({
    right: 4 })

          Text(`${
     course.duration}分钟`)
            .fontSize(12)
            .fontColor(Color.White)
        }
        .height(20)
        .padding({
    left: 6, right: 6 })
        .backgroundColor('rgba(0, 0, 0, 0.6)')
        .borderRadius(10)
        .position({
    x: 8, y: 8 })

        // 难度级别
        Text(course.difficulty)
          .fontSize(12)
          .fontColor(Color.White)
          .backgroundColor(this.getDifficultyColor(course.difficulty))
          .borderRadius(10)
          .padding({
    left: 6, right: 6 })
          .height(20)
          .position({
    x: '70%', y: 8 })

        // 标签(如果有)
        if (course.tags && course.tags.length > 0) {
   
          Row() {
   
            ForEach(course.tags.slice(0, 2), (tag: string) => {
   
              Text(tag)
                .fontSize(10)
                .fontColor(Color.White)
                .backgroundColor('#FF9800')
                .borderRadius(4)
                .padding({
    left: 4, right: 4, top: 2, bottom: 2 })
                .margin({
    right: 4 })
            })
          }
          .position({
    x: 8, y: 90 })
        }

        // 免费或价格标签
        Text(course.isFree ? '免费' : ${
     course.price}`)
          .fontSize(12)
          .fontColor(Color.White)
          .backgroundColor(course.isFree ? '#4CAF50' : '#FF5722')
          .borderRadius(10)
          .padding({
    left: 6, right: 6 })
          .height(20)
          .position({
    x: '70%', y: 90 })
      }
      .width('100%')
      .height(120)

      // 课程信息
      Column() {
   
        // 课程名称
        Text(course.name)
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
          .maxLines(1)
          .textOverflow({
    overflow: TextOverflow.Ellipsis })
          .width('100%')

        // 教练信息
        Row() {
   
          if (course.coachAvatar) {
   
            Image(course.coachAvatar)
              .width(16)
              .height(16)
              .borderRadius(8)
              .margin({
    right: 4 })
          }

          Text(course.coach)
            .fontSize(12)
            .fontColor('#666666')
        }
        .width('100%')
        .margin({
    top: 4 })

        // 评分和参与人数
        Row() {
   
          // 评分
          Row() {
   
            ForEach([1, 2, 3, 4, 5], (item: number) => {
   
              Image($r('app.media.ic_star'))
                .width(12)
                .height(12)
                .fillColor(item <= Math.floor(course.rating) ? '#FFB300' : '#E0E0E0')
                .margin({
    right: 2 })
            })

            Text(course.rating.toFixed(1))
              .fontSize(12)
              .fontColor('#FFB300')
              .margin({
    left: 4 })
          }

          Blank()

          // 参与人数
          Text(`${
     course.participants}人参与`)
            .fontSize(12)
            .fontColor('#999999')
        }
        .width('100%')
        .margin({
    top: 4 })

        // 卡路里消耗
        Row() {
   
          Image($r('app.media.ic_calories'))
            .width(14)
            .height(14)
            .fillColor('#FF5722')
            .margin({
    right: 4 })

          Text(`${
     course.calories}千卡`)
            .fontSize(12)
            .fontColor('#FF5722')
        }
        .width('100%')
        .margin({
    top: 4 })
      }
      .width('100%')
      .padding(8)
    }
    .width('100%')
    .backgroundColor(Color.White)
    .borderRadius(8)
    .shadow({
   
      radius: 4,
      color: '#1A000000',
      offsetX: 0,
      offsetY: 2
    })
  }

  // 获取分类名称
  private getCategoryName(categoryId: string): string {
   
    const category = this.categories.find(item => item.id === categoryId);
    return category ? category.name : '';
  }

  // 获取筛选后的课程
  private getFilteredCourses(): FitnessCourse[] {
   
    let filtered = this.courses;

    // 按分类筛选
    if (this.currentCategory !== 'all') {
   
      filtered = filtered.filter(item => item.categoryId === this.currentCategory);
    }

    // 按搜索文本筛选
    if (this.searchText.trim() !== '') {
   
      const keyword = this.searchText.toLowerCase();
      filtered = filtered.filter(item => 
        item.name.toLowerCase().includes(keyword) || 
        item.description.toLowerCase().includes(keyword) ||
        item.coach.toLowerCase().includes(keyword)
      );
    }

    return filtered;
  }

  // 获取难度对应的颜色
  private getDifficultyColor(difficulty: DifficultyLevel): string {
   
    switch (difficulty) {
   
      case DifficultyLevel.BEGINNER:
        return '#4CAF50'; // 绿色
      case DifficultyLevel.INTERMEDIATE:
        return '#FF9800'; // 橙色
      case DifficultyLevel.ADVANCED:
        return '#F44336'; // 红色
      default:
        return '#999999'; // 灰色
    }
  }
}
AI 代码解读

8. 总结

本教程详细讲解了如何使用HarmonyOS NEXT的GridRow和GridCol组件实现健身课程的网格布局展示。我们设计了健身课程的数据结构,准备了模拟数据,并实现了整体布局结构,包括顶部搜索栏、分类标签栏和课程网格列表。通过GridRow和GridCol组件的配置,我们实现了响应式布局,使应用能够适应不同屏幕尺寸的设备。

在下一篇教程中,我们将在此基础上,深入探讨如何优化布局、添加交互功能,以及实现更多高级特性,打造一个功能完善的健身课程应用。

目录
打赏
0
1
1
0
46
分享
相关文章
鸿蒙NEXT时代你所不知道的全平台跨端框架:CMP、Kuikly、Lynx、uni-app x等
本篇基于当前各大活跃的跨端框架的现状,对比当前它们的情况和未来的可能,帮助你在选择框架时更好理解它们的特点和差异。
58 0
纯血鸿蒙NEXT即时通讯/IM系统:RinbowTalk正式发布,全源码、纯ArkTS编写
RainbowTalk是一套基于MobileIMSDK的产品级鸿蒙NEXT端IM系统,目前已正式发布。纯ArkTS、从零编写,无套壳、没走捷径,每一行代码都够“纯”(详见:《RainbowTalk详细介绍》)。 MobileIMSDK是一整套开源IM即时通讯框架,历经10年,超轻量级、高度提炼,一套API优雅支持 UDP 、TCP 、WebSocket 三种协议,支持 iOS、Android、H5、标准Java、小程序、Uniapp、鸿蒙NEXT,服务端基于Netty编写。
67 1
鸿蒙NEXT-鸿蒙三层架构搭建,嵌入HMRouter,实现便捷跳转,新手攻略。(2/3)
本文介绍在三层架构中实现模块依赖的步骤。首先在产品定制层(features)的oh-package.json5文件中导入共享包依赖,如"basic":"file:../../commons/basic"。然后在产品层(products)的配置文件中同时导入公共能力层和产品定制层的依赖,示例展示了如何添加"basic"和"my"两个依赖项。通过这些配置,三层架构的各模块之间建立了完整的依赖关系。
93 0
鸿蒙NEXT-鸿蒙三层架构搭建,嵌入HMRouter,实现便捷跳转,新手攻略。(2/3)
【HarmonyOS 5】鸿蒙组件&模板服务详解 - 助力高效开发的利器
在移动应用开发领域,效率与质量始终是开发者追求的核心目标。鸿蒙系统作为新兴的操作系统,为开发者提供了丰富且强大的开发资源,其中鸿蒙组件&模板服务更是成为开发者快速构建高质量应用的得力助手。
87 0
HarmonyOS NEXT仓颉开发语言实战案例:外卖App
仓颉语言实战分享,教你如何用仓颉开发外卖App界面。内容包括页面布局、导航栏自定义、搜索框实现、列表模块构建等,附完整代码示例。轻松掌握Scroll、List等组件使用技巧,提升HarmonyOS应用开发能力。
HarmonyOS NEXT仓颉开发语言实战案例:健身App
本期分享一个健身App首页的布局实现,顶部采用Stack容器实现重叠背景与偏移效果,列表部分使用List结合Scroll实现可滚动内容。代码结构清晰,适合学习HarmonyOS布局技巧。
HarmonyOS NEXT仓颉开发语言实战案例:银行App
仓颉语言银行App项目分享,页面布局采用List容器,实现沉浸式体验与模块化设计。顶部资产模块结合Stack与Row布局,背景图与内容分离,代码清晰易懂;功能按钮部分通过负边距实现上移效果,圆角仅保留顶部;热门推荐使用header组件,结构更规范。整体代码风格与ArkTS相似,但细节更灵活,适合金融类应用开发。
鸿蒙开发:基于最新API,如何实现组件化运行
手动只是让大家了解切换的原理,在实际开发中,可不推荐手动,下篇文章,我们将通过脚本或者插件,快速实现组件化模块之间的切换,实现独立运行,敬请期待!
100 0
鸿蒙开发:基于最新API,如何实现组件化运行
鸿蒙开发:资讯项目实战之项目初始化搭建
目前来说,我们的资讯项目只是往前迈了很小的一步,仅仅实现了项目创建,步虽小,但概念性的知识很多,这也是这个项目的初衷,让大家不仅仅可以掌握日常的技术开发,也能让大家理解实际的项目开发知识。
鸿蒙开发:资讯项目实战之项目初始化搭建
鸿蒙5开发宝藏案例分享---优化应用时延问题
鸿蒙性能优化指南来了!从UI渲染到数据库操作,6大实战案例助你提升应用流畅度。布局层级优化、数据加载并发、数据库查询提速、相机资源延迟释放、手势识别灵敏调整及转场动画精调,全面覆盖性能痛点。附赠性能自检清单,帮助开发者高效定位问题,让应用运行如飞!来自华为官方文档的精华内容,建议收藏并反复研读,共同探讨更多优化技巧。
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等

登录插画

登录以查看您的控制台资源

管理云资源
状态一览
快捷访问