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

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

效果演示

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[];           // 标签(如"热门"、"新课"等)
}

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'; // 当前选中的分类

4. 布局实现

4.1 整体布局结构

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

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

  build() {
    Column() {
      // 顶部搜索栏
      this.SearchBar()
      
      // 分类标签栏
      this.CategoryTabs()
      
      // 课程网格列表
      this.CourseGrid()
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }
  
  // 其他方法
  // ...
}

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')
}

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 })
}

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;
  }
}

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'; // 灰色
    }
  }
}

5. GridRow和GridCol配置详解

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

5.1 GridRow配置

GridRow({ columns: { sm: 2, md: 3, lg: 4 }, gutter: { x: 12, y: 12 } }) {
  // 内容
}
  • columns:定义不同屏幕尺寸下的列数

    • sm:小屏幕(如手机)显示2列
    • md:中等屏幕(如平板)显示3列
    • lg:大屏幕(如桌面)显示4列
  • gutter:定义网格项之间的间距

    • x:水平间距为12vp
    • y:垂直间距为12vp

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

5.2 GridCol配置

GridCol() {
  // 内容
}

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

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

GridCol({ span: { sm: 2, md: 1, lg: 1 } }) {
  // 内容
}

这表示在小屏幕上,该网格项占据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'; // 灰色
    }
  }
}

8. 总结

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

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


全栈若城
1 声望2 粉丝