HarmonyOS APP<玩转React>开源教程十二:ModuleCard 模块卡片组件

第11次:ModuleCard 模块卡片组件

ModuleCard 是应用中最常用的组件之一,用于展示学习模块的信息。本次课程将深入学习卡片组件的设计,包括难度标签、进度条、锁定状态等功能的实现。


学习目标

  • 掌握复杂卡片组件的设计方法
  • 学会实现难度等级标签
  • 实现进度条展示功能
  • 处理组件的锁定状态
  • 完成 ModuleCard 组件的完整开发

11.1 卡片组件设计

需求分析

ModuleCard 需要展示以下信息:

  • 模块图标
  • 难度等级标签
  • 模块标题
  • 模块描述
  • 课时数和预计时长
  • 学习进度(可选)
  • 锁定状态(可选)

Props 设计

typescript 复制代码
@Component
export struct ModuleCard {
  // 模块数据
  @Prop module: LearningModule = {} as LearningModule;
  
  // 学习进度(0-100)
  @Prop progress: number = 0;
  
  // 是否锁定
  @Prop isLocked: boolean = false;
  
  // 主题状态
  @StorageLink('isDarkMode') isDarkMode: boolean = false;
  
  // 点击回调
  onTap?: () => void;
}

数据模型回顾

typescript 复制代码
// LearningModule 模型(来自 Models.ets)
export interface LearningModule {
  id: string;
  title: string;
  description: string;
  icon: string;
  difficulty: string;  // 'beginner' | 'basic' | 'intermediate' | 'advanced' | 'ecosystem'
  lessonCount: number;
  estimatedTime: string;
  lessons: Lesson[];
}

11.2 难度等级标签

难度颜色配置

Constants.ets 中定义了难度等级的颜色:

typescript 复制代码
// 浅色模式
export class DifficultyColorsLight {
  static readonly BEGINNER: string = '#28a745';     // 入门 - 绿色
  static readonly BASIC: string = '#007bff';        // 基础 - 蓝色
  static readonly INTERMEDIATE: string = '#fd7e14'; // 进阶 - 橙色
  static readonly ADVANCED: string = '#dc3545';     // 高级 - 红色
  static readonly ECOSYSTEM: string = '#6f42c1';    // 生态 - 紫色
}

// 深色模式
export class DifficultyColorsDark {
  static readonly BEGINNER: string = '#51cf66';
  static readonly BASIC: string = '#339af0';
  static readonly INTERMEDIATE: string = '#ff922b';
  static readonly ADVANCED: string = '#ff6b6b';
  static readonly ECOSYSTEM: string = '#9775fa';
}

获取难度颜色函数

typescript 复制代码
export type DifficultyLevel = 'beginner' | 'basic' | 'intermediate' | 'advanced' | 'ecosystem';

export const DifficultyNames: Record<DifficultyLevel, string> = {
  'beginner': '入门',
  'basic': '基础',
  'intermediate': '进阶',
  'advanced': '高级',
  'ecosystem': '生态'
};

export function getDifficultyColor(difficulty: DifficultyLevel, isDarkMode: boolean): string {
  if (isDarkMode) {
    switch (difficulty) {
      case 'beginner': return DifficultyColorsDark.BEGINNER;
      case 'basic': return DifficultyColorsDark.BASIC;
      case 'intermediate': return DifficultyColorsDark.INTERMEDIATE;
      case 'advanced': return DifficultyColorsDark.ADVANCED;
      case 'ecosystem': return DifficultyColorsDark.ECOSYSTEM;
      default: return DifficultyColorsDark.BEGINNER;
    }
  } else {
    // 浅色模式颜色...
  }
}

难度标签实现

typescript 复制代码
Text(DifficultyNames[this.module.difficulty as DifficultyLevel] ?? '入门')
  .fontSize(10)
  .fontColor('#ffffff')
  .backgroundColor(getDifficultyColor(this.module.difficulty as DifficultyLevel, this.isDarkMode))
  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
  .borderRadius(8)

11.3 进度条展示

Progress 组件

ArkUI 提供了 Progress 组件用于展示进度:

typescript 复制代码
Progress({ 
  value: 50,           // 当前值
  total: 100,          // 总值
  type: ProgressType.Linear  // 线性进度条
})
  .width('100%')
  .height(4)
  .color('#61DAFB')           // 进度颜色
  .backgroundColor('#e9ecef') // 背景颜色

ProgressType 类型

类型 说明
Linear 线性进度条
Ring 环形进度条
Eclipse 圆形进度条
ScaleRing 刻度环形进度条
Capsule 胶囊形进度条

进度条与百分比组合

typescript 复制代码
if (this.progress > 0) {
  Row() {
    Progress({ value: this.progress, total: 100, type: ProgressType.Linear })
      .width('70%')
      .height(3)
      .color(this.isDarkMode ? '#61DAFB' : '#0077b6')
      .backgroundColor(this.isDarkMode ? '#3d3d5c' : '#e9ecef')

    Text(`${this.progress}%`)
      .fontSize(10)
      .fontColor(this.isDarkMode ? '#61DAFB' : '#0077b6')
      .margin({ left: 4 })
  }
  .width('100%')
  .margin({ top: 8 })
}

11.4 锁定状态处理

锁定遮罩设计

当模块被锁定时,显示半透明遮罩和锁定提示:

typescript 复制代码
if (this.isLocked) {
  Column() {
    Text('🔒')
      .fontSize(20)
    Text('完成前置课程解锁')
      .fontSize(10)
      .fontColor(this.isDarkMode ? '#d1d5db' : '#495057')
      .margin({ top: 4 })
  }
  .position({ x: 0, y: 0 })  // 绝对定位
  .width('100%')
  .height('100%')
  .backgroundColor(this.isDarkMode ? 'rgba(26,26,46,0.8)' : 'rgba(255,255,255,0.8)')
  .justifyContent(FlexAlign.Center)
  .borderRadius(16)
}

position 绝对定位

使用 position 属性可以将元素定位到父容器的指定位置:

typescript 复制代码
Column() {
  // 正常内容
  Text('内容')
  
  // 遮罩层(绝对定位覆盖在内容上方)
  Column()
    .position({ x: 0, y: 0 })
    .width('100%')
    .height('100%')
}

点击事件处理

锁定状态下禁止点击:

typescript 复制代码
.onClick(() => {
  if (!this.isLocked && this.onTap) {
    this.onTap();
  }
})

11.5 阴影与圆角效果

shadow 阴影属性

typescript 复制代码
.shadow({
  radius: 8,                    // 模糊半径
  color: 'rgba(0,0,0,0.08)',   // 阴影颜色
  offsetX: 0,                   // X 轴偏移
  offsetY: 4                    // Y 轴偏移
})

主题适配阴影

typescript 复制代码
.shadow({
  radius: 8,
  color: this.isDarkMode ? 'rgba(0,0,0,0.3)' : 'rgba(0,0,0,0.08)',
  offsetX: 0,
  offsetY: 4
})

borderRadius 圆角

typescript 复制代码
// 统一圆角
.borderRadius(16)

// 分别设置
.borderRadius({
  topLeft: 16,
  topRight: 16,
  bottomLeft: 0,
  bottomRight: 0
})

11.6 实操:完成 ModuleCard.ets 组件

步骤一:创建组件文件

entry/src/main/ets/components/ 目录下创建 ModuleCard.ets 文件。

步骤二:编写完整组件代码

typescript 复制代码
/**
 * 模块卡片组件
 * 用于首页轮播和课程列表展示
 */
import { LearningModule } from '../models/Models';
import { getDifficultyColor, DifficultyNames, DifficultyLevel } from '../common/Constants';

@Component
export struct ModuleCard {
  // Props
  @Prop module: LearningModule = {} as LearningModule;
  @Prop progress: number = 0;
  @Prop isLocked: boolean = false;
  
  // 全局状态
  @StorageLink('isDarkMode') isDarkMode: boolean = false;
  
  // 回调
  onTap?: () => void;

  build() {
    Column() {
      // 顶部:图标和难度标签
      Row() {
        Text(this.module.icon)
          .fontSize(24)

        Blank()

        Text(DifficultyNames[this.module.difficulty as DifficultyLevel] ?? '入门')
          .fontSize(10)
          .fontColor('#ffffff')
          .backgroundColor(getDifficultyColor(this.module.difficulty as DifficultyLevel, this.isDarkMode))
          .padding({ left: 6, right: 6, top: 2, bottom: 2 })
          .borderRadius(8)
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)

      // 标题
      Text(this.module.title)
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor(this.isDarkMode ? '#ffffff' : '#1a1a2e')
        .margin({ top: 8 })
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })

      // 描述
      Text(this.module.description)
        .fontSize(11)
        .fontColor(this.isDarkMode ? '#d1d5db' : '#495057')
        .margin({ top: 4 })
        .maxLines(2)
        .textOverflow({ overflow: TextOverflow.Ellipsis })

      // 弹性空间,将底部内容推到底部
      Blank()

      // 底部信息:课时数和时长
      Row() {
        Text(`${this.module.lessonCount} 课时`)
          .fontSize(10)
          .fontColor(this.isDarkMode ? '#d1d5db' : '#495057')

        Text('·')
          .fontSize(10)
          .fontColor(this.isDarkMode ? '#d1d5db' : '#495057')
          .margin({ left: 4, right: 4 })

        Text(this.module.estimatedTime)
          .fontSize(10)
          .fontColor(this.isDarkMode ? '#d1d5db' : '#495057')
      }
      .margin({ top: 8 })

      // 进度条(仅当有进度时显示)
      if (this.progress > 0) {
        Row() {
          Progress({ value: this.progress, total: 100, type: ProgressType.Linear })
            .width('70%')
            .height(3)
            .color(this.isDarkMode ? '#61DAFB' : '#0077b6')
            .backgroundColor(this.isDarkMode ? '#3d3d5c' : '#e9ecef')

          Text(`${this.progress}%`)
            .fontSize(10)
            .fontColor(this.isDarkMode ? '#61DAFB' : '#0077b6')
            .margin({ left: 4 })
        }
        .width('100%')
        .margin({ top: 8 })
      }

      // 锁定状态遮罩
      if (this.isLocked) {
        Column() {
          Text('🔒')
            .fontSize(20)
          Text('完成前置课程解锁')
            .fontSize(10)
            .fontColor(this.isDarkMode ? '#d1d5db' : '#495057')
            .margin({ top: 4 })
        }
        .position({ x: 0, y: 0 })
        .width('100%')
        .height('100%')
        .backgroundColor(this.isDarkMode ? 'rgba(26,26,46,0.8)' : 'rgba(255,255,255,0.8)')
        .justifyContent(FlexAlign.Center)
        .borderRadius(16)
      }
    }
    .width(160)
    .height(180)
    .padding(12)
    .backgroundColor(this.isDarkMode ? '#282c34' : '#ffffff')
    .borderRadius(16)
    .shadow({
      radius: 8,
      color: this.isDarkMode ? 'rgba(0,0,0,0.3)' : 'rgba(0,0,0,0.08)',
      offsetX: 0,
      offsetY: 4
    })
    .onClick(() => {
      if (!this.isLocked && this.onTap) {
        this.onTap();
      }
    })
  }
}

步骤三:在首页中使用组件

更新 Index.ets 的推荐模块区域:

typescript 复制代码
import { ModuleCard } from '../components/ModuleCard';
import { LearningModule } from '../models/Models';
import { router } from '@kit.ArkUI';

@Builder
RecommendedModulesSection() {
  Column() {
    Row() {
      Text('推荐模块')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(this.isDarkMode ? '#ffffff' : '#1a1a2e')

      Blank()

      Text('查看全部 →')
        .fontSize(14)
        .fontColor(this.isDarkMode ? '#61DAFB' : '#0077b6')
        .onClick(() => {
          this.currentTab = 1;  // 切换到课程 Tab
        })
    }
    .width('100%')
    .margin({ bottom: 12 })

    // 横向滚动模块卡片
    Scroll() {
      Row({ space: 12 }) {
        ForEach(this.modules.slice(0, 5), (module: LearningModule) => {
          ModuleCard({
            module: module,
            progress: this.getModuleProgress(module),
            isLocked: false,
            onTap: () => {
              router.pushUrl({
                url: 'pages/ModuleDetail',
                params: { moduleId: module.id }
              });
            }
          })
        })
      }
      .padding({ right: 16 })
    }
    .scrollable(ScrollDirection.Horizontal)
    .scrollBar(BarState.Off)
  }
  .width('100%')
  .padding({ left: 16, top: 20 })
  .alignItems(HorizontalAlign.Start)
}

// 获取模块进度
private getModuleProgress(module: LearningModule): number {
  // 计算已完成课程数
  const completedCount = module.lessons.filter(
    lesson => this.progress.completedLessons.includes(lesson.id)
  ).length;
  
  if (module.lessonCount === 0) return 0;
  return Math.round((completedCount / module.lessonCount) * 100);
}

步骤四:测试组件

  1. 运行应用,查看首页推荐模块区域
  2. 检查卡片样式是否正确
  3. 验证难度标签颜色
  4. 测试点击跳转功能
  5. 切换深色模式,验证主题适配

组件设计要点

1. 文本溢出处理

typescript 复制代码
Text(this.module.title)
  .maxLines(1)                                    // 最大行数
  .textOverflow({ overflow: TextOverflow.Ellipsis })  // 溢出显示省略号

2. Blank 弹性空间

Blank() 组件会占据所有剩余空间,常用于将内容推到容器两端:

typescript 复制代码
Column() {
  Text('顶部内容')
  Blank()           // 占据中间空间
  Text('底部内容')  // 被推到底部
}

3. 条件渲染

使用 if 语句进行条件渲染:

typescript 复制代码
if (this.progress > 0) {
  // 只有进度大于 0 时才显示进度条
  Progress({ value: this.progress, total: 100 })
}

4. 类型断言

当 TypeScript 无法推断类型时,使用类型断言:

typescript 复制代码
this.module.difficulty as DifficultyLevel

本次课程小结

通过本次课程,你已经:

✅ 掌握了复杂卡片组件的设计方法

✅ 学会了实现难度等级标签

✅ 实现了进度条展示功能

✅ 处理了组件的锁定状态

✅ 完成了 ModuleCard 组件的完整开发


课后练习

  1. 添加收藏按钮:在卡片右上角添加收藏图标

  2. 点击动画:添加点击时的缩放动画效果

  3. 自定义尺寸:添加 width 和 height Props,支持自定义卡片尺寸


下次预告

第12次:教程数据服务层

我们将开发 TutorialService 服务层:

  • 服务层设计模式
  • 模块数据组织
  • 课程内容结构
  • 数据缓存策略

进入业务逻辑层开发!

相关推荐
早點睡3902 小时前
ReactNative项目OpenHarmony三方库集成实战:@react-native-oh-tpl/masked-view
javascript·react native·react.js
架构师李肯3 小时前
TypeScript与React全栈实战:从架构搭建到项目部署,避开常见陷阱
react.js·架构·typescript
英俊潇洒美少年11 小时前
react如何实现 vue的$nextTick的效果
javascript·vue.js·react.js
冬奇Lab13 小时前
一天一个开源项目(第53篇):PDF 补丁丁 - 功能全面的 PDF 工具箱,编辑书签、解除限制、合并拆分、OCR 识别
开源·资讯
Arya_aa14 小时前
Mysql数据库-管理和存储数据库(开源管理系统)与JDBC操作数据库步骤,JUnit以及如何将压缩包中exe程序添加上桌面图标
数据库·mysql·junit·开源
国医中兴14 小时前
Flutter 三方库 stack_blur 鸿蒙适配指南 - 实现工业级高性能模糊滤镜、在 OpenHarmony 上打造极致视觉质感实战
flutter·华为·harmonyos
敲代码的约德尔人15 小时前
React 性能优化完全指南:从渲染机制到实战技巧
react.js
许留山15 小时前
前端 PDF 导出:从文件流下载到自动分页
前端·react.js