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

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

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

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

效果演示

img_34198a71.png

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组件,以及添加丰富的交互功能和高级特性,可以打造出用户体验更好的应用界面。

相关文章
|
17天前
|
定位技术 UED
70. [HarmonyOS NEXT 实战案例九] 旅游景点网格布局(下)
在上一篇教程中,我们学习了如何使用GridRow和GridCol组件实现基本的旅游景点网格布局。本篇教程将在此基础上,深入探讨如何优化布局、添加交互功能,以及实现更多高级特性,打造一个功能完善的旅游景点应用。
32 1
|
17天前
|
容器
69.[HarmonyOS NEXT 实战案例九] 旅游景点网格布局(上)
本教程将详细讲解如何使用HarmonyOS NEXT中的GridRow和GridCol组件实现旅游景点网格布局。通过网格布局,我们可以以美观、规整的方式展示各种旅游景点信息,为用户提供良好的浏览体验。
31 1
|
容器
HarmonyOS实战—布局和组件的概述
HarmonyOS实战—布局和组件的概述
263 0
HarmonyOS实战—布局和组件的概述
|
25天前
|
开发框架 前端开发 JavaScript
【HarmonyOS Next之旅】基于ArkTS开发(二) -> UI开发一
本文介绍了方舟开发框架(ArkUI)及其两种开发范式:基于ArkTS的声明式开发范式和类Web开发范式。ArkUI是用于构建HarmonyOS应用界面的UI框架,提供极简UI语法和基础设施。声明式开发范式使用ArkTS语言,以组件、动画和状态管理为核心,适合复杂团队协作;类Web开发范式采用HML、CSS、JavaScript三段式开发,适用于简单界面应用,贴近Web开发者习惯。文中还概述了两者的架构和基础能力,帮助开发者选择合适的范式进行高效开发。
71 15
|
25天前
|
编解码 前端开发 Java
【HarmonyOS Next之旅】基于ArkTS开发(二) -> UI开发三
本文介绍了基于声明式UI范式的图形绘制与动画效果实现方法,涵盖绘制图形、添加动画效果及常见组件说明三部分内容。在绘制图形部分,详细讲解了如何通过Circle组件为食物成分表添加圆形标签,以及使用Path组件结合SVG命令绘制自定义图形(如应用Logo)。动画效果部分则展示了如何利用animateTo实现闪屏动画,包括渐出、放大效果,并设置页面跳转;同时介绍了页面间共享元素转场动画的实现方式。最后,文章列举了声明式开发范式中的各类组件及其功能,帮助开发者快速上手构建复杂交互页面。
67 11
|
26天前
|
开发工具
鸿蒙开发:DevEcoStudio中的代码生成
其实大家可以发现,一篇文章下来,都是基于右键后的Generate选项,所以,还是非常的简单的,当然了,还是希望大家,以上的功能,能够应用在实际的开发中,而不是停留在纸面上。
鸿蒙开发:DevEcoStudio中的代码生成
|
22天前
|
UED 容器
5.HarmonyOS Next开发宝典:掌握Flex布局的艺术
Flex布局(弹性布局)是HarmonyOS Next中最强大的布局方式之一,它提供了一种更加高效、灵活的方式来对容器中的子元素进行排列、对齐和分配空间。无论是简单的居中显示,还是复杂的自适应界面,Flex布局都能轻松应对。
49 0
|
27天前
|
存储 JSON 搜索推荐
鸿蒙5开发宝藏案例分享---自由流转的拖拽多屏联动
本文分享了鸿蒙开发中的五大实用案例,包括页面跳转、列表渲染、网络请求封装、数据持久化和系统能力调用。通过具体代码示例与避坑指南,手把手教你掌握常用功能,助你高效开发。无论是初学者还是进阶开发者,都能从中受益!
|
26天前
|
JavaScript 小程序 API
UniApp X:鸿蒙原生开发的机会与DCloud的崛起之路·优雅草卓伊凡
UniApp X:鸿蒙原生开发的机会与DCloud的崛起之路·优雅草卓伊凡
86 12
UniApp X:鸿蒙原生开发的机会与DCloud的崛起之路·优雅草卓伊凡
|
22天前
|
JSON IDE Java
鸿蒙开发:json转对象插件回来了
首先,我重新编译了插件,进行了上传,大家可以下载最新的安装包进行体验了,还是和以前一样,提供了在线版和IDE插件版,两个选择,最新的版本,除了升级了版本,兼容了最新的DevEco Studio ,还做了一层优化,就是针对嵌套对象和属性的生成,使用方式呢,一年前的文章中有过详细的概述,这里呢也简单介绍一下。
鸿蒙开发:json转对象插件回来了

热门文章

最新文章

OSZAR »