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

155 阅读11分钟

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

效果演示

1. 概述

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

本教程将涵盖以下内容:

  • 课程详情页的实现
  • 课程收藏和预约功能
  • 课程筛选和排序功能
  • 课程推荐和相关课程
  • 高级动效和交互优化

2. 课程详情页实现

2.1 详情页布局设计

当用户点击课程卡片时,我们需要展示课程的详细信息。下面是课程详情页的实现:

@State showCourseDetail: boolean = false; // 是否显示课程详情
@State currentCourse: FitnessCourse | null = null; // 当前查看的课程

// 在CourseCard方法中添加点击事件
Column() {
  // 课程卡片内容
  // ...
}
.width('100%')
.backgroundColor(Color.White)
.borderRadius(8)
.shadow({
  radius: 4,
  color: '#1A000000',
  offsetX: 0,
  offsetY: 2
})
.onClick(() => {
  this.currentCourse = course;
  this.showCourseDetail = true;
})

// 在build方法末尾添加课程详情页
if (this.showCourseDetail && this.currentCourse) {
  this.CourseDetailPage()
}

2.2 详情页组件实现

@Builder
private CourseDetailPage() {
  Stack() {
    Column() {
      // 顶部图片区域
      Stack() {
        Image(this.currentCourse.image)
          .width('100%')
          .height(240)
          .objectFit(ImageFit.Cover)
        
        // 返回按钮
        Button({ type: ButtonType.Circle }) {
          Image($r('app.media.ic_back'))
            .width(20)
            .height(20)
            .fillColor('#333333')
        }
        .width(36)
        .height(36)
        .backgroundColor('#FFFFFF')
        .position({ x: 16, y: 16 })
        .onClick(() => {
          this.showCourseDetail = false;
        })
        
        // 收藏按钮
        Button({ type: ButtonType.Circle }) {
          Image(this.isFavorite(this.currentCourse.id) ? $r('app.media.ic_favorite_filled') : $r('app.media.ic_favorite'))
            .width(20)
            .height(20)
            .fillColor(this.isFavorite(this.currentCourse.id) ? '#FF5722' : '#333333')
        }
        .width(36)
        .height(36)
        .backgroundColor('#FFFFFF')
        .position({ x: '90%', y: 16 })
        .onClick(() => {
          this.toggleFavorite(this.currentCourse.id);
        })
        
        // 难度级别
        Text(this.currentCourse.difficulty)
          .fontSize(12)
          .fontColor(Color.White)
          .backgroundColor(this.getDifficultyColor(this.currentCourse.difficulty))
          .borderRadius(10)
          .padding({ left: 6, right: 6 })
          .height(20)
          .position({ x: 16, y: 60 })
        
        // 标签(如果有)
        if (this.currentCourse.tags && this.currentCourse.tags.length > 0) {
          Row() {
            ForEach(this.currentCourse.tags, (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: 16, y: 90 })
        }
        
        // 课程信息卡片
        Row() {
          // 教练头像
          Image(this.currentCourse.coachAvatar)
            .width(60)
            .height(60)
            .borderRadius(30)
            .border({ width: 2, color: Color.White })
          
          // 课程基本信息
          Column() {
            Text(this.currentCourse.name)
              .fontSize(18)
              .fontWeight(FontWeight.Bold)
              .fontColor(Color.White)
              .maxLines(1)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
              .width('100%')
            
            Text(`教练:${this.currentCourse.coach}`)
              .fontSize(14)
              .fontColor(Color.White)
              .opacity(0.9)
              .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(this.currentCourse.rating) ? '#FFB300' : '#E0E0E0')
                    .margin({ right: 2 })
                })
                
                Text(this.currentCourse.rating.toFixed(1))
                  .fontSize(12)
                  .fontColor('#FFB300')
                  .margin({ left: 4 })
              }
              
              Blank()
              
              // 参与人数
              Text(`${this.currentCourse.participants}人参与`)
                .fontSize(12)
                .fontColor(Color.White)
                .opacity(0.9)
            }
            .width('100%')
            .margin({ top: 4 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          .margin({ left: 12 })
        }
        .width('90%')
        .padding(16)
        .backgroundColor('rgba(0, 0, 0, 0.6)')
        .borderRadius(8)
        .position({ x: '5%', y: 180 })
      }
      .width('100%')
      .height(240)
      
      // 课程详细信息
      Scroll() {
        Column() {
          // 课程信息卡片
          Row() {
            // 时长
            Column() {
              Image($r('app.media.ic_time'))
                .width(24)
                .height(24)
                .fillColor('#FF5722')
              
              Text(`${this.currentCourse.duration}分钟`)
                .fontSize(14)
                .fontColor('#333333')
                .margin({ top: 4 })
            }
            .width('33%')
            .alignItems(HorizontalAlign.Center)
            
            // 卡路里
            Column() {
              Image($r('app.media.ic_calories'))
                .width(24)
                .height(24)
                .fillColor('#FF5722')
              
              Text(`${this.currentCourse.calories}千卡`)
                .fontSize(14)
                .fontColor('#333333')
                .margin({ top: 4 })
            }
            .width('33%')
            .alignItems(HorizontalAlign.Center)
            
            // 价格
            Column() {
              Image($r('app.media.ic_price'))
                .width(24)
                .height(24)
                .fillColor('#FF5722')
              
              Text(this.currentCourse.isFree ? '免费' : ${this.currentCourse.price}`)
                .fontSize(14)
                .fontColor('#333333')
                .margin({ top: 4 })
            }
            .width('33%')
            .alignItems(HorizontalAlign.Center)
          }
          .width('100%')
          .padding({ top: 16, bottom: 16 })
          .backgroundColor(Color.White)
          .borderRadius(8)
          .margin({ top: 16, bottom: 16 })
          
          // 课程描述
          Column() {
            Text('课程介绍')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#333333')
              .width('100%')
              .margin({ bottom: 8 })
            
            Text(this.currentCourse.description)
              .fontSize(14)
              .fontColor('#666666')
              .width('100%')
              .margin({ bottom: 16 })
            
            // 课程亮点
            Text('课程亮点')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#333333')
              .width('100%')
              .margin({ bottom: 8 })
            
            Column() {
              Row() {
                Image($r('app.media.ic_check'))
                  .width(16)
                  .height(16)
                  .fillColor('#4CAF50')
                  .margin({ right: 8 })
                
                Text('专业教练一对一指导')
                  .fontSize(14)
                  .fontColor('#666666')
              }
              .width('100%')
              .margin({ bottom: 8 })
              
              Row() {
                Image($r('app.media.ic_check'))
                  .width(16)
                  .height(16)
                  .fillColor('#4CAF50')
                  .margin({ right: 8 })
                
                Text('科学的训练方法和动作要领')
                  .fontSize(14)
                  .fontColor('#666666')
              }
              .width('100%')
              .margin({ bottom: 8 })
              
              Row() {
                Image($r('app.media.ic_check'))
                  .width(16)
                  .height(16)
                  .fillColor('#4CAF50')
                  .margin({ right: 8 })
                
                Text('适合各个级别的练习者')
                  .fontSize(14)
                  .fontColor('#666666')
              }
              .width('100%')
              .margin({ bottom: 8 })
            }
            .width('100%')
            .margin({ bottom: 16 })
            
            // 适合人群
            Text('适合人群')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#333333')
              .width('100%')
              .margin({ bottom: 8 })
            
            Text(this.getSuitableCrowdText(this.currentCourse.difficulty))
              .fontSize(14)
              .fontColor('#666666')
              .width('100%')
              .margin({ bottom: 16 })
            
            // 教练信息
            Text('教练信息')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#333333')
              .width('100%')
              .margin({ bottom: 8 })
            
            Row() {
              Image(this.currentCourse.coachAvatar)
                .width(60)
                .height(60)
                .borderRadius(30)
              
              Column() {
                Text(this.currentCourse.coach)
                  .fontSize(16)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#333333')
                  .width('100%')
                
                Text(this.getCoachDescription(this.currentCourse.coach))
                  .fontSize(14)
                  .fontColor('#666666')
                  .width('100%')
                  .margin({ top: 4 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Start)
              .margin({ left: 12 })
            }
            .width('100%')
            .margin({ bottom: 16 })
            
            // 相关课程
            Text('相关课程')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#333333')
              .width('100%')
              .margin({ bottom: 8 })
            
            this.RelatedCourses()
          }
          .width('100%')
          .padding(16)
          .backgroundColor(Color.White)
          .borderRadius(8)
        }
        .width('100%')
        .padding({ left: 16, right: 16 })
      }
      .scrollBar(BarState.Off)
      .scrollable(ScrollDirection.Vertical)
      .width('100%')
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
    
    // 底部操作栏
    Row() {
      // 分享按钮
      Column() {
        Image($r('app.media.ic_share'))
          .width(24)
          .height(24)
          .fillColor('#666666')
        
        Text('分享')
          .fontSize(12)
          .fontColor('#666666')
          .margin({ top: 4 })
      }
      .width(80)
      .height(56)
      .onClick(() => {
        this.shareCourse();
      })
      
      Blank()
      
      // 预约按钮
      Button(this.isBooked(this.currentCourse.id) ? '已预约' : '立即预约')
        .width(160)
        .height(40)
        .backgroundColor(this.isBooked(this.currentCourse.id) ? '#999999' : '#FF5722')
        .borderRadius(20)
        .fontColor(Color.White)
        .onClick(() => {
          if (!this.isBooked(this.currentCourse.id)) {
            this.bookCourse(this.currentCourse.id);
          } else {
            this.cancelBooking(this.currentCourse.id);
          }
        })
    }
    .width('100%')
    .height(64)
    .padding({ left: 16, right: 16 })
    .backgroundColor(Color.White)
    .borderWidth({ top: 0.5 })
    .borderColor('#E0E0E0')
    .position({ x: 0, y: '92%' })
  }
  .width('100%')
  .height('100%')
  .position({ x: 0, y: 0 })
  .zIndex(100)
}

2.3 相关课程组件

@Builder
private RelatedCourses() {
  Scroll() {
    Row() {
      ForEach(this.getRelatedCourses(), (course: FitnessCourse) => {
        Column() {
          Image(course.image)
            .width(120)
            .height(80)
            .borderRadius(8)
            .objectFit(ImageFit.Cover)
          
          Text(course.name)
            .fontSize(14)
            .fontColor('#333333')
            .maxLines(1)
            .textOverflow({ overflow: TextOverflow.Ellipsis })
            .width('100%')
            .margin({ top: 4 })
          
          Row() {
            Text(course.difficulty)
              .fontSize(10)
              .fontColor(Color.White)
              .backgroundColor(this.getDifficultyColor(course.difficulty))
              .borderRadius(4)
              .padding({ left: 4, right: 4, top: 2, bottom: 2 })
            
            Blank()
            
            Text(course.isFree ? '免费' : ${course.price}`)
              .fontSize(12)
              .fontColor('#FF5722')
              .fontWeight(FontWeight.Bold)
          }
          .width('100%')
          .margin({ top: 4 })
        }
        .width(120)
        .padding(8)
        .backgroundColor('#F9F9F9')
        .borderRadius(8)
        .margin({ right: 12 })
        .onClick(() => {
          this.currentCourse = course;
        })
      })
    }
  }
  .scrollBar(BarState.Off)
  .scrollable(ScrollDirection.Horizontal)
  .width('100%')
  .height(160)
  .margin({ bottom: 16 })
  
  // 获取相关课程
  private getRelatedCourses(): FitnessCourse[] {
    return this.courses.filter(item => 
      item.id !== this.currentCourse.id && 
      (item.categoryId === this.currentCourse.categoryId || 
       item.difficulty === this.currentCourse.difficulty)
    ).slice(0, 5);
  }
  
  // 获取适合人群文本
  private getSuitableCrowdText(difficulty: DifficultyLevel): string {
    switch (difficulty) {
      case DifficultyLevel.BEGINNER:
        return '适合初学者,无需任何健身基础,想要开始健身之旅的人群。';
      case DifficultyLevel.INTERMEDIATE:
        return '适合有一定健身基础,想要提升训练强度和效果的人群。';
      case DifficultyLevel.ADVANCED:
        return '适合健身经验丰富,想要挑战自我极限的健身爱好者。';
      default:
        return '适合所有人群。';
    }
  }
  
  // 获取教练描述
  private getCoachDescription(coach: string): string {
    // 模拟数据,实际应用中应该从数据库获取
    const descriptions = {
      '李明': '瑜伽专业教练,5年教学经验,擅长初级瑜伽教学。',
      '张强': 'HIIT训练专家,健身教练认证,擅长高强度间歇训练。',
      '王刚': '力量训练专家,10年健身经验,专注于肌肉塑造和力量提升。',
      '刘芳': '拉伸和康复训练专家,理疗师认证,擅长身体放松和恢复训练。',
      '周丽': '舞蹈教练,专业舞蹈背景,擅长有氧舞蹈和形体训练。',
      '张华': '高级瑜伽教练,瑜伽冥想专家,擅长高难度瑜伽姿势教学。',
      '李娜': '普拉提教练,核心训练专家,擅长体态矫正和核心力量训练。',
      '王明': '有氧训练专家,马拉松运动员,擅长心肺功能训练。',
      '张伟': '力量训练教练,健美运动员,擅长初级力量训练指导。',
      '刘强': 'HIIT训练专家,CrossFit教练认证,擅长高强度全身训练。',
      '马丽': '拉丁舞教练,专业舞者背景,擅长舞蹈基础教学。',
      '王芳': '瑜伽冥想教练,心理学背景,擅长压力释放和放松训练。'
    };
    
    return descriptions[coach] || '专业健身教练,拥有丰富的教学经验。';
  }
}

3. 课程收藏和预约功能

3.1 数据结构和状态变量

// 状态变量
@State favorites: string[] = []; // 收藏的课程ID列表
@State bookings: string[] = []; // 预约的课程ID列表

// 收藏相关方法
private isFavorite(courseId: string): boolean {
  return this.favorites.includes(courseId);
}

private toggleFavorite(courseId: string): void {
  if (this.isFavorite(courseId)) {
    // 取消收藏
    this.favorites = this.favorites.filter(id => id !== courseId);
    this.showToast('已取消收藏');
  } else {
    // 添加收藏
    this.favorites.push(courseId);
    this.showToast('已添加到收藏');
  }
}

// 预约相关方法
private isBooked(courseId: string): boolean {
  return this.bookings.includes(courseId);
}

private bookCourse(courseId: string): void {
  if (!this.isBooked(courseId)) {
    this.bookings.push(courseId);
    this.showToast('预约成功');
  }
}

private cancelBooking(courseId: string): void {
  if (this.isBooked(courseId)) {
    this.bookings = this.bookings.filter(id => id !== courseId);
    this.showToast('已取消预约');
  }
}

// 分享课程
private shareCourse(): void {
  this.showToast('分享功能开发中...');
}

// 显示提示信息
private showToast(message: string): void {
  // 实现提示信息
  AlertDialog.show({
    message: message,
    autoCancel: true,
    alignment: DialogAlignment.Bottom,
    offset: { dx: 0, dy: -100 },
    gridCount: 3,
    duration: 2000
  });
}

3.2 在课程卡片中显示收藏状态

// 修改CourseCard方法,添加收藏图标
@Builder
private CourseCard(course: FitnessCourse) {
  Column() {
    // 课程封面图
    Stack() {
      // 原有内容
      // ...
      
      // 收藏图标
      Image(this.isFavorite(course.id) ? $r('app.media.ic_favorite_filled') : $r('app.media.ic_favorite'))
        .width(24)
        .height(24)
        .fillColor(this.isFavorite(course.id) ? '#FF5722' : '#FFFFFF')
        .position({ x: '85%', y: 8 })
        .onClick((event: ClickEvent) => {
          this.toggleFavorite(course.id);
          event.stopPropagation(); // 阻止事件冒泡,避免触发卡片点击
        })
    }
    // 其他内容
    // ...
  }
  // ...
}

4. 课程筛选和排序功能

4.1 筛选面板实现

@State showFilter: boolean = false; // 是否显示筛选面板
@State filterOptions: FilterOptions = { // 筛选选项
  difficultyLevels: [],
  priceRange: [0, 100],
  durationRange: [0, 60],
  sortBy: 'default' // 'default', 'rating-desc', 'participants-desc', 'duration-asc', 'price-asc', 'price-desc'
};

// 在SearchBar方法中添加筛选按钮点击事件
Image($r('app.media.ic_filter'))
  .width(24)
  .height(24)
  .fillColor('#333333')
  .margin({ left: 12 })
  .onClick(() => {
    this.showFilter = true;
  })

// 在build方法末尾添加筛选面板
if (this.showFilter) {
  this.FilterPanel()
}

@Builder
private FilterPanel() {
  Stack() {
    // 半透明背景
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('#80000000')
      .onClick(() => {
        this.showFilter = false;
      })
    
    // 筛选面板
    Column() {
      // 顶部标题栏
      Row() {
        Text('筛选')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
        
        Blank()
        
        Button('重置')
          .backgroundColor('transparent')
          .fontColor('#999999')
          .fontSize(14)
          .onClick(() => {
            this.resetFilter();
          })
      }
      .width('100%')
      .height(48)
      .padding({ left: 16, right: 16 })
      .borderWidth({ bottom: 0.5 })
      .borderColor('#E0E0E0')
      
      // 筛选选项
      Scroll() {
        Column() {
          // 难度级别
          Text('难度级别')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')
            .width('100%')
            .margin({ top: 16, bottom: 8 })
          
          Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
            ForEach(Object.values(DifficultyLevel), (level: string) => {
              Text(level)
                .fontSize(14)
                .fontColor(this.filterOptions.difficultyLevels.includes(level) ? '#FFFFFF' : '#666666')
                .backgroundColor(this.filterOptions.difficultyLevels.includes(level) ? '#FF5722' : '#F5F5F5')
                .borderRadius(16)
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .margin({ right: 8, bottom: 8 })
                .onClick(() => {
                  if (this.filterOptions.difficultyLevels.includes(level)) {
                    this.filterOptions.difficultyLevels = this.filterOptions.difficultyLevels.filter(item => item !== level);
                  } else {
                    this.filterOptions.difficultyLevels.push(level);
                  }
                })
            })
          }
          .width('100%')
          .margin({ bottom: 16 })
          
          // 价格范围
          Row() {
            Text('价格范围')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#333333')
            
            Text(${this.filterOptions.priceRange[0]} - ¥${this.filterOptions.priceRange[1]}`)
              .fontSize(14)
              .fontColor('#999999')
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .margin({ top: 16, bottom: 8 })
          
          Slider({
            min: 0,
            max: 100,
            step: 5,
            value: this.filterOptions.priceRange[0],
            secondValue: this.filterOptions.priceRange[1],
            style: SliderStyle.OutSet
          })
            .width('100%')
            .blockColor('#FF5722')
            .trackColor('#E0E0E0')
            .selectedColor('#FF9800')
            .showSteps(true)
            .showTips(true)
            .onChange((value: number, mode: SliderChangeMode) => {
              if (mode === SliderChangeMode.Begin) {
                this.filterOptions.priceRange[0] = value;
              } else if (mode === SliderChangeMode.End) {
                this.filterOptions.priceRange[1] = value;
              } else if (mode === SliderChangeMode.Move) {
                // 双向滑块移动
                if (Array.isArray(value)) {
                  this.filterOptions.priceRange = [value[0], value[1]];
                }
              }
            })
            .margin({ bottom: 16 })
          
          // 课程时长
          Row() {
            Text('课程时长')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#333333')
            
            Text(`${this.filterOptions.durationRange[0]} - ${this.filterOptions.durationRange[1]}分钟`)
              .fontSize(14)
              .fontColor('#999999')
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .margin({ top: 16, bottom: 8 })
          
          Slider({
            min: 0,
            max: 60,
            step: 5,
            value: this.filterOptions.durationRange[0],
            secondValue: this.filterOptions.durationRange[1],
            style: SliderStyle.OutSet
          })
            .width('100%')
            .blockColor('#FF5722')
            .trackColor('#E0E0E0')
            .selectedColor('#FF9800')
            .showSteps(true)
            .showTips(true)
            .onChange((value: number, mode: SliderChangeMode) => {
              if (mode === SliderChangeMode.Begin) {
                this.filterOptions.durationRange[0] = value;
              } else if (mode === SliderChangeMode.End) {
                this.filterOptions.durationRange[1] = value;
              } else if (mode === SliderChangeMode.Move) {
                // 双向滑块移动
                if (Array.isArray(value)) {
                  this.filterOptions.durationRange = [value[0], value[1]];
                }
              }
            })
            .margin({ bottom: 16 })
          
          // 排序方式
          Text('排序方式')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')
            .width('100%')
            .margin({ top: 16, bottom: 8 })
          
          Column() {
            this.SortOption('默认排序', 'default')
            this.SortOption('评分从高到低', 'rating-desc')
            this.SortOption('参与人数从多到少', 'participants-desc')
            this.SortOption('时长从短到长', 'duration-asc')
            this.SortOption('价格从低到高', 'price-asc')
            this.SortOption('价格从高到低', 'price-desc')
          }
          .width('100%')
        }
        .width('100%')
        .padding({ left: 16, right: 16 })
      }
      .scrollBar(BarState.Off)
      .scrollable(ScrollDirection.Vertical)
      .width('100%')
      .layoutWeight(1)
      
      // 底部按钮
      Row() {
        Button('取消')
          .width('48%')
          .height(40)
          .backgroundColor('#F5F5F5')
          .fontColor('#666666')
          .borderRadius(20)
          .onClick(() => {
            this.showFilter = false;
          })
        
        Button('确定')
          .width('48%')
          .height(40)
          .backgroundColor('#FF5722')
          .fontColor(Color.White)
          .borderRadius(20)
          .onClick(() => {
            this.applyFilter();
            this.showFilter = false;
          })
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 12, bottom: 12 })
      .justifyContent(FlexAlign.SpaceBetween)
      .backgroundColor(Color.White)
    }
    .width('100%')
    .height('70%')
    .backgroundColor(Color.White)
    .borderRadius({ topLeft: 16, topRight: 16 })
    .position({ x: 0, y: '30%' })
  }
  .width('100%')
  .height('100%')
  .position({ x: 0, y: 0 })
  .zIndex(300)
}

// 排序选项构建器
@Builder
private SortOption(text: string, value: string) {
  Row() {
    Text(text)
      .fontSize(14)
      .fontColor('#666666')
    
    Blank()
    
    Radio({ value: value, group: 'sortBy' })
      .checked(this.filterOptions.sortBy === value)
      .onChange((isChecked: boolean) => {
        if (isChecked) {
          this.filterOptions.sortBy = value;
        }
      })
  }
  .width('100%')
  .height(40)
  .padding({ left: 8, right: 8 })
  .borderRadius(4)
  .backgroundColor(this.filterOptions.sortBy === value ? '#FFF3E0' : 'transparent')
  .margin({ bottom: 8 })
}

4.2 筛选和排序方法

// 筛选选项接口
interface FilterOptions {
  difficultyLevels: string[];
  priceRange: number[];
  durationRange: number[];
  sortBy: string;
}

// 重置筛选选项
private resetFilter(): void {
  this.filterOptions = {
    difficultyLevels: [],
    priceRange: [0, 100],
    durationRange: [0, 60],
    sortBy: 'default'
  };
}

// 应用筛选
private applyFilter(): void {
  // 筛选已在getFilteredCourses方法中实现
}

// 修改getFilteredCourses方法,添加筛选和排序逻辑
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)
    );
  }
  
  // 按难度级别筛选
  if (this.filterOptions.difficultyLevels.length > 0) {
    filtered = filtered.filter(item => 
      this.filterOptions.difficultyLevels.includes(item.difficulty)
    );
  }
  
  // 按价格范围筛选
  filtered = filtered.filter(item => {
    const price = item.isFree ? 0 : item.price;
    return price >= this.filterOptions.priceRange[0] && 
           price <= this.filterOptions.priceRange[1];
  });
  
  // 按课程时长筛选
  filtered = filtered.filter(item => 
    item.duration >= this.filterOptions.durationRange[0] && 
    item.duration <= this.filterOptions.durationRange[1]
  );
  
  // 排序
  switch (this.filterOptions.sortBy) {
    case 'rating-desc':
      filtered.sort((a, b) => b.rating - a.rating);
      break;
    case 'participants-desc':
      filtered.sort((a, b) => b.participants - a.participants);
      break;
    case 'duration-asc':
      filtered.sort((a, b) => a.duration - b.duration);
      break;
    case 'price-asc':
      filtered.sort((a, b) => {
        const priceA = a.isFree ? 0 : a.price;
        const priceB = b.isFree ? 0 : b.price;
        return priceA - priceB;
      });
      break;
    case 'price-desc':
      filtered.sort((a, b) => {
        const priceA = a.isFree ? 0 : a.price;
        const priceB = b.isFree ? 0 : b.price;
        return priceB - priceA;
      });
      break;
    default:
      // 默认排序,保持原顺序
      break;
  }
  
  return filtered;
}

5. 课程推荐和相关课程

5.1 推荐课程区域实现

// 在CourseGrid方法中添加推荐课程区域
@Builder
private CourseGrid() {
  Scroll() {
    Column() {
      // 如果当前是全部分类,显示推荐区域
      if (this.currentCategory === 'all') {
        this.RecommendedSection()
      }
      
      // 分类标题
      // ...
      
      // 课程网格
      // ...
    }
    .width('100%')
  }
  .scrollBar(BarState.Off)
  .scrollable(ScrollDirection.Vertical)
  .width('100%')
  .layoutWeight(1)
  .backgroundColor('#F5F5F5')
}

// 推荐课程区域构建器
@Builder
private RecommendedSection() {
  Column() {
    // 热门课程
    Row() {
      Text('热门课程')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')
      
      Blank()
      
      Text('查看全部')
        .fontSize(14)
        .fontColor('#FF5722')
        .onClick(() => {
          // 查看全部热门课程
        })
    }
    .width('100%')
    .padding({ left: 16, right: 16, top: 16, bottom: 8 })
    
    // 热门课程横向滚动
    Scroll() {
      Row() {
        ForEach(this.getPopularCourses(), (course: FitnessCourse) => {
          Column() {
            Stack() {
              Image(course.image)
                .width(240)
                .height(135)
                .borderRadius({ topLeft: 8, topRight: 8 })
                .objectFit(ImageFit.Cover)
              
              // 难度级别
              Text(course.difficulty)
                .fontSize(12)
                .fontColor(Color.White)
                .backgroundColor(this.getDifficultyColor(course.difficulty))
                .borderRadius(10)
                .padding({ left: 6, right: 6 })
                .height(20)
                .position({ x: 8, y: 8 })
              
              // 时长
              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: 36 })
              
              // 收藏图标
              Image(this.isFavorite(course.id) ? $r('app.media.ic_favorite_filled') : $r('app.media.ic_favorite'))
                .width(24)
                .height(24)
                .fillColor(this.isFavorite(course.id) ? '#FF5722' : '#FFFFFF')
                .position({ x: 208, y: 8 })
                .onClick((event: ClickEvent) => {
                  this.toggleFavorite(course.id);
                  event.stopPropagation(); // 阻止事件冒泡
                })
            }
            .width(240)
            .height(135)
            
            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.isFree ? '免费' : ${course.price}`)
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#FF5722')
              }
              .width('100%')
              .margin({ top: 4 })
            }
            .width('100%')
            .padding(8)
          }
          .width(240)
          .backgroundColor(Color.White)
          .borderRadius(8)
          .margin({ right: 12 })
          .shadow({
            radius: 4,
            color: '#1A000000',
            offsetX: 0,
            offsetY: 2
          })
          .onClick(() => {
            this.currentCourse = course;
            this.showCourseDetail = true;
          })
        })
      }
      .padding({ left: 16, right: 16 })
    }
    .scrollBar(BarState.Off)
    .scrollable(ScrollDirection.Horizontal)
    .width('100%')
    .height(210)
    .margin({ bottom: 16 })
    
    // 新手推荐
    Row() {
      Text('新手推荐')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')
      
      Blank()
      
      Text('查看全部')
        .fontSize(14)
        .fontColor('#FF5722')
        .onClick(() => {
          // 查看全部新手推荐
        })
    }
    .width('100%')
    .padding({ left: 16, right: 16, bottom: 8 })
    
    // 新手推荐课程
    Scroll() {
      Row() {
        ForEach(this.getBeginnerCourses(), (course: FitnessCourse) => {
          Row() {
            // 课程图片
            Image(course.image)
              .width(80)
              .height(80)
              .borderRadius(8)
              .objectFit(ImageFit.Cover)
            
            // 课程信息
            Column() {
              Text(course.name)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#333333')
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
                .width('100%')
              
              Text(`${course.duration}分钟 | ${course.coach}`)
                .fontSize(12)
                .fontColor('#666666')
                .margin({ top: 4 })
              
              Row() {
                Text(course.isFree ? '免费' : ${course.price}`)
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#FF5722')
                
                Blank()
                
                Button('开始学习')
                  .width(80)
                  .height(28)
                  .fontSize(12)
                  .backgroundColor('#FF5722')
                  .borderRadius(14)
                  .fontColor(Color.White)
                  .onClick((event: ClickEvent) => {
                    this.currentCourse = course;
                    this.showCourseDetail = true;
                    event.stopPropagation(); // 阻止事件冒泡
                  })
              }
              .width('100%')
              .margin({ top: 4 })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
            .margin({ left: 12 })
          }
          .width('100%')
          .padding(12)
          .backgroundColor(Color.White)
          .borderRadius(8)
          .margin({ bottom: 12 })
          .onClick(() => {
            this.currentCourse = course;
            this.showCourseDetail = true;
          })
        })
      }
      .width('100%')
      .padding({ left: 16, right: 16 })
    }
    .scrollBar(BarState.Off)
    .scrollable(ScrollDirection.Horizontal)
    .width('100%')
    .height(104)
    .margin({ bottom: 16 })
  }
  .width('100%')
}

5.2 推荐课程方法

// 获取热门课程
private getPopularCourses(): FitnessCourse[] {
  return this.courses
    .sort((a, b) => b.participants - a.participants)
    .slice(0, 5);
}

// 获取新手推荐课程
private getBeginnerCourses(): FitnessCourse[] {
  return this.courses
    .filter(item => item.difficulty === DifficultyLevel.BEGINNER)
    .sort((a, b) => b.rating - a.rating)
    .slice(0, 5);
}

6. 高级动效和交互优化

6.1 课程卡片动效

// 修改CourseCard方法,添加动效
@Builder
private CourseCard(course: FitnessCourse) {
  Column() {
    // 课程卡片内容
    // ...
  }
  .width('100%')
  .backgroundColor(Color.White)
  .borderRadius(8)
  .shadow({
    radius: 4,
    color: '#1A000000',
    offsetX: 0,
    offsetY: 2
  })
  .stateStyles({
    pressed: {
      scale: { x: 0.95, y: 0.95 },
      opacity: 0.8,
      translate: { x: 0, y: 2 }
    },
    normal: {
      scale: { x: 1, y: 1 },
      opacity: 1,
      translate: { x: 0, y: 0 }
    }
  })
  .animation({
    duration: 200,
    curve: Curve.EaseOut
  })
  .onClick(() => {
    this.currentCourse = course;
    this.showCourseDetail = true;
  })
}

6.2 下拉刷新和加载更多

@State refreshing: boolean = false; // 刷新状态
@State loading: boolean = false; // 加载更多状态
@State hasMore: boolean = true; // 是否有更多数据

// 修改CourseGrid方法,添加下拉刷新和加载更多
@Builder
private CourseGrid() {
  Refresh({ refreshing: this.refreshing }) {
    Scroll() {
      Column() {
        // 推荐区域和课程网格
        // ...
        
        // 加载更多
        if (this.hasMore) {
          Row() {
            if (this.loading) {
              LoadingProgress()
                .width(24)
                .height(24)
                .color('#999999')
              
              Text('加载中...')
                .fontSize(14)
                .fontColor('#999999')
                .margin({ left: 8 })
            } else {
              Text('上拉加载更多')
                .fontSize(14)
                .fontColor('#999999')
            }
          }
          .width('100%')
          .height(50)
          .justifyContent(FlexAlign.Center)
        } else {
          Row() {
            Text('没有更多了')
              .fontSize(14)
              .fontColor('#999999')
          }
          .width('100%')
          .height(50)
          .justifyContent(FlexAlign.Center)
        }
      }
      .width('100%')
    }
    .scrollBar(BarState.Off)
    .scrollable(ScrollDirection.Vertical)
    .width('100%')
    .layoutWeight(1)
    .backgroundColor('#F5F5F5')
    .onScrollEdge((side: Edge) => {
      if (side === Edge.Bottom && this.hasMore && !this.loading) {
        this.loadMore();
      }
    })
  }
  .onRefresh(() => {
    this.refreshData();
  })
}

// 刷新数据
private refreshData(): void {
  this.refreshing = true;
  
  // 模拟网络请求
  setTimeout(() => {
    // 重置数据
    this.hasMore = true;
    this.refreshing = false;
    
    // 显示提示
    this.showToast('刷新成功');
  }, 1500);
}

// 加载更多
private loadMore(): void {
  this.loading = true;
  
  // 模拟网络请求
  setTimeout(() => {
    // 模拟没有更多数据
    if (Math.random() > 0.7) {
      this.hasMore = false;
    }
    
    this.loading = false;
  }, 1500);
}

6.3 分类标签栏动画

// 修改CategoryTabs方法,添加动画
@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 })
              .animation({
                duration: 300,
                curve: Curve.EaseOut
              })
          }
          
          Text(category.name)
            .fontSize(14)
            .fontWeight(this.currentCategory === category.id ? FontWeight.Bold : FontWeight.Normal)
            .fontColor(this.currentCategory === category.id ? '#FF5722' : '#666666')
            .animation({
              duration: 300,
              curve: Curve.EaseOut
            })
        }
        .width(category.id === 'all' ? 56 : 80)
        .height(56)
        .justifyContent(FlexAlign.Center)
        .backgroundColor(this.currentCategory === category.id ? '#FFF3E0' : 'transparent')
        .borderRadius(8)
        .margin({ right: 12 })
        .animation({
          duration: 300,
          curve: Curve.EaseOut
        })
        .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 })
}

7. 完整代码

由于完整代码较长,这里只展示部分关键代码。完整代码包含了本教程中介绍的所有功能,包括课程详情页、课程收藏和预约功能、课程筛选和排序功能、课程推荐和相关课程,以及高级动效和交互优化。

8. 总结

本教程详细讲解了如何优化健身课程网格布局,添加交互功能,以及实现更多高级特性。通过使用HarmonyOS NEXT的GridRow和GridCol组件的高级特性,我们实现了响应式布局,使应用能够适应不同屏幕尺寸的设备。同时,我们还添加了课程详情页、课程收藏和预约功能、课程筛选和排序功能、课程推荐和相关课程,以及高级动效和交互优化,打造了一个功能完善的健身课程应用。

通过本教程,你应该已经掌握了:

  • 如何实现课程详情页和收藏预约功能
  • 如何添加课程筛选和排序功能
  • 如何实现课程推荐和相关课程
  • 如何添加高级动效和交互优化

这些技能可以应用到各种需要网格布局的场景中,如电商商品展示、新闻列表、音乐专辑等。通过合理使用GridRow和GridCol组件,以及添加丰富的交互功能和高级特性,可以打造出用户体验更好的应用界面。