第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. 文本溢出处理
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 组件的完整开发
课后练习
-
添加收藏按钮:在卡片右上角添加收藏图标
-
点击动画:添加点击时的缩放动画效果
-
自定义尺寸:添加 width 和 height Props,支持自定义卡片尺寸
下次预告
第12次:教程数据服务层
我们将开发 TutorialService 服务层:
- 服务层设计模式
- 模块数据组织
- 课程内容结构
- 数据缓存策略
进入业务逻辑层开发!