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