课程表应用 — 涉及的全部知识点
本文档列出这道题涉及的所有 ArkTS / ArkUI 知识点,包括装饰器、组件、布局、样式、事件、状态管理、数据模型等。
一、装饰器(Decorators)
1.1 @Entry
@Entry
@Component
struct Index { ... }
- 标记当前 struct 为页面的入口组件
- 一个页面只能有一个
@Entry
- 必须和
@Component 一起使用
- 相当于 Android 的
Activity 或 Web 的 index.html
1.2 @Component
@Component
struct Index { ... }
- 标记当前 struct 为一个自定义组件
- 所有自定义组件必须加
@Component
- 组件内必须有
build() 方法描述 UI
1.3 @State
@State currentDayIndex: number = 0
@State courseData: CourseDay[] = []
@State isShowDeleteDialog: boolean = false
@State deletingCourse: CourseItem | null = null
- 状态装饰器,标记组件内部的状态变量
- 当
@State 变量的值发生变化时,自动触发 UI 重新渲染
- 这是 ArkUI 响应式编程的核心机制
- 支持的类型:
number、string、boolean、数组、对象、null
1.4 @Builder
@Builder buildWeekNavigation() { ... }
@Builder buildCourseList() { ... }
@Builder buildCourseItem(course: CourseItem, index: number) { ... }
- 构建器装饰器,用于定义可复用的 UI 构建函数
- 相当于把一段 UI 封装成一个方法,在
build() 中通过 this.xxx() 调用
- 可以带参数(如
buildCourseItem(course, index))
- 作用:拆分复杂 UI,提高代码可读性和复用性
二、生命周期
2.1 aboutToAppear()
aboutToAppear() {
this.initCourseData()
}
- 组件即将显示时调用的生命周期钩子
- 在
build() 之前执行
- 适合做数据初始化操作
- 常见生命周期顺序:
aboutToAppear() → build() → aboutToDisappear()
三、布局组件
3.1 Column — 垂直布局
Column() {
// 子组件从上到下排列
Text('标题')
Text('内容')
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
.padding(16)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
- 子组件垂直方向从上到下排列
- 常用属性:
.justifyContent(FlexAlign.Center) — 垂直居中
.alignItems(HorizontalAlign.Center) — 水平居中
.layoutWeight(1) — 占据父容器剩余空间
3.2 Row — 水平布局
Row() {
// 子组件从左到右排列
Text('左')
Text('右')
}
- 子组件水平方向从左到右排列
- 本题用于:时间标签和课程信息的水平排列
Scroll() {
Row() { ... }
}
.scrollable(ScrollDirection.Horizontal)
- 让子组件可以滚动显示
.scrollable(ScrollDirection.Horizontal) — 水平滚动
.scrollable(ScrollDirection.Vertical) — 垂直滚动(默认)
- 本题用于:星期导航的水平滚动,一周 7 天超出屏幕宽度时可以左右滑动
3.4 List + ListItem — 列表
List({ space: 10 }) {
ForEach(this.data, (item: CourseItem, index: number) => {
ListItem() {
this.buildCourseItem(item, index)
}
}, (item: CourseItem) => item.id)
}
.layoutWeight(1)
List 是列表容器,space: 10 设置列表项间距
ListItem 是列表中的每一项
- 支持滚动、分隔线等列表特性
- 比
Column + ForEach 更适合长列表(有懒加载优化)
四、基础组件
4.1 Text — 文本
Text('我的课程表')
.fontSize(24)
.fontWeight(FontWeight.Bold) // 或 900
.fontColor('#000000')
.margin({ top: 20, bottom: 10 })
| 属性 | 作用 | 示例 |
|---|
.fontSize() | 字体大小 | .fontSize(24) |
.fontWeight() | 字体粗细 | .fontWeight(FontWeight.Bold) 或 .fontWeight(900) |
.fontColor() | 字体颜色 | .fontColor('#000000') |
.margin() | 外边距 | .margin({ top: 20, bottom: 10 }) |
- 字体粗细值:
FontWeight.Lighter(100-300)、FontWeight.Normal(400-500)、FontWeight.Medium(500-600)、FontWeight.Bold(600-900),也可以直接写数字
五、循环渲染 — ForEach
5.1 基本用法
ForEach(
arr, // 数据源数组
(item, index) => { ... }, // 渲染函数
(item) => item.id // 键值生成器(可选但推荐)
)
- 遍历数组,为每个元素生成对应的 UI 组件
- 三个参数:
- 数据源:要遍历的数组
- 渲染函数:
(item, index) => { ... },item 是当前元素,index 是索引
- 键值生成器:返回唯一标识字符串,用于 diff 算法优化
5.2 本题的两处 ForEach
星期导航(遍历一周数据):
ForEach(this.courseData, (dayItem: CourseDay, index: number) => {
Column() { ... }
})
课程列表(遍历当天课程):
ForEach(this.courseData[this.currentDayIndex].courses, (course: CourseItem, index: number) => {
ListItem() { ... }
}, (course: CourseItem) => course.id)
六、条件渲染
6.1 if / else
Column() {
if (this.courseData[this.currentDayIndex].courses.length > 0) {
List({ space: 10 }) { ... }
} else {
Column() {
Text('📚')
Text('今天没有课程,好好休息吧!')
}
}
}
- 在
build() 中使用 if/else 根据条件渲染不同的 UI
- 条件表达式的结果为
true 渲染 if 分支,false 渲染 else 分支
- 本题用于:有课程时显示列表,无课程时显示空状态
七、样式属性大全
7.1 尺寸
| 属性 | 作用 | 示例 |
|---|
.width() | 宽度 | .width('100%') 或 .width(200) |
.height() | 高度 | .height('100%') 或 .height(50) |
.layoutWeight() | 弹性布局权重 | .layoutWeight(1) 占据剩余空间 |
7.2 间距
| 属性 | 作用 | 示例 |
|---|
.padding() | 内边距 | .padding(16) 或 .padding({ left: 16, right: 16, top: 12, bottom: 12 }) |
.margin() | 外边距 | .margin({ top: 20, bottom: 10 }) 或 .margin({ right: 8 }) |
padding 是组件内部的空白(边框到内容之间)
margin 是组件外部的空白(边框到其他组件之间)
7.3 背景和边框
| 属性 | 作用 | 示例 |
|---|
.backgroundColor() | 背景色 | .backgroundColor('#F5F5F5') |
.borderRadius() | 圆角 | .borderRadius(12) 或 .borderRadius(20) |
7.4 阴影
.shadow({
radius: 2, // 模糊半径
color: '#1A000000', // 阴影颜色(带透明度)
offsetX: 0, // X 轴偏移
offsetY: 1 // Y 轴偏移
})
color 的前两位 1A 是透明度(十六进制,1A = 约 10% 不透明度)
- 阴影让卡片有"浮起来"的视觉效果
7.5 对齐
| 属性 | 作用 | 取值 |
|---|
.justifyContent() | 主轴对齐 | FlexAlign.Start、FlexAlign.Center、FlexAlign.End、FlexAlign.SpaceBetween |
.alignItems() | 交叉轴对齐 | HorizontalAlign.Start、HorizontalAlign.Center、HorizontalAlign.End |
- Column 中主轴是垂直方向,
justifyContent(FlexAlign.Center) 是垂直居中
- Column 中交叉轴是水平方向,
alignItems(HorizontalAlign.Center) 是水平居中
八、事件处理
8.1 onClick — 点击事件
.onClick(() => {
this.currentDayIndex = index
})
- 组件被点击时触发
- 回调函数中可以修改
@State 变量,触发 UI 更新
8.2 PanGesture — 手势
.gesture(
PanGesture({
direction: PanDirection.Left, // 方向:左滑
distance: 50 // 滑动距离阈值(vp)
})
.onActionEnd(() => {
this.deletingCourse = course
this.showDeleteConfirm()
})
)
PanGesture 是拖拽/滑动手势
direction:滑动方向
PanDirection.Left — 左滑
PanDirection.Right — 右滑
PanDirection.Up — 上滑
PanDirection.Down — 下滑
distance:触发手势的最小滑动距离(单位 vp)
.onActionEnd():手势结束时触发的回调
九、对话框 — AlertDialog
9.1 课程详情对话框(单按钮)
AlertDialog.show({
title: course.name,
message: `⏰ 时间: ${course.time}\n🏫 教室: ${course.classroom}\n👨🏫 教师: ${course.teacher}`,
confirm: {
value: '确定',
action: () => {}
},
alignment: DialogAlignment.Center
})
9.2 删除确认对话框(双按钮)
AlertDialog.show({
title: '删除课程',
message: `确定要删除"${this.deletingCourse.name}"课程吗?`,
primaryButton: {
value: '取消',
action: () => { this.deletingCourse = null }
},
secondaryButton: {
value: '删除',
action: () => { this.deleteCourse() }
},
alignment: DialogAlignment.Center
})
AlertDialog.show() 显示一个模态对话框
- 参数说明:
| 参数 | 作用 |
|---|
title | 对话框标题 |
message | 对话框内容(支持模板字符串) |
confirm | 单按钮模式(详情对话框用) |
primaryButton | 主按钮(通常是"取消") |
secondaryButton | 次按钮(通常是"确认") |
alignment | 对话框位置,DialogAlignment.Center 居中 |
十、数据模型 — class
10.1 CourseDay 类
class CourseDay {
day: string = ''
date: string = ''
courses: CourseItem[] = []
}
10.2 CourseItem 类
class CourseItem {
id: string = ''
name: string = ''
time: string = ''
classroom: string = ''
teacher: string = ''
}
- 使用
class 定义数据模型
- 每个字段有类型和默认值
CourseItem[] 表示 CourseItem 类型的数组
十一、TypeScript / ArkTS 语法
11.1 类型声明
@State currentDayIndex: number = 0
@State courseData: CourseDay[] = []
@State deletingCourse: CourseItem | null = null
number — 数字类型
string — 字符串类型
boolean — 布尔类型
CourseDay[] — 数组类型
CourseItem | null — 联合类型(可以是 CourseItem 或 null)
11.2 模板字符串
`⏰ 时间: ${course.time}\n🏫 教室: ${course.classroom}\n👨🏫 教师: ${course.teacher}`
- 用反引号
` 包裹
${expression} 插入变量或表达式
\n 换行符
11.3 字符串方法
course.time.split('-')[0] // "08:00-09:40" → "08:00"
course.time.split('-')[1] // "08:00-09:40" → "09:40"
split(separator) 按分隔符拆分字符串,返回数组
[0] 取第一个元素,[1] 取第二个元素
11.4 数组方法
// findIndex — 查找元素索引
const index = currentDayCourses.findIndex(course => course.id === this.deletingCourse!.id)
// splice — 删除元素
this.courseData[this.currentDayIndex].courses.splice(index, 1)
// JSON 深拷贝 — 触发 UI 更新
this.courseData = JSON.parse(JSON.stringify(this.courseData))
| 方法 | 作用 | 返回值 |
|---|
findIndex() | 查找满足条件的元素索引 | 找到返回索引,找不到返回 -1 |
splice(index, 1) | 从 index 位置删除 1 个元素 | 被删除的元素(本题不用) |
JSON.parse(JSON.stringify(obj)) | 深拷贝对象 | 新对象,引用地址不同 |
为什么需要 JSON 深拷贝?
@State 检测变化是通过引用比较的。splice 修改的是原数组的内容,但数组引用没变,@State 不会触发 UI 更新。深拷贝生成一个新数组赋值回去,引用变了,UI 就更新了。
11.5 非空断言
this.deletingCourse!.id
! 是非空断言操作符
- 告诉编译器"我确定这个值不是 null/undefined"
- 本题中
deletingCourse 类型是 CourseItem | null,用 ! 断言它不为 null
11.6 箭头函数
() => { this.currentDayIndex = index }
(course: CourseItem) => course.id
(dayItem: CourseDay, index: number) => { ... }
() => expression — 无参箭头函数
(param) => expression — 带参箭头函数
() => { statements } — 多行箭头函数体
11.7 三元表达式
index === this.currentDayIndex ? '#FFFFFF' : '#666666'
条件 ? 值1 : 值2
- 条件为 true 返回值1,否则返回值2
- 本题大量用于:选中/未选中状态的样式切换
十二、颜色规范
本题使用的颜色体系(蓝色主题):
| 颜色值 | 用途 | 含义 |
|---|
#007DFF | 选中状态背景、时间文字 | 主题蓝 |
#E6F2FF | 时间标签背景 | 浅蓝 |
#F5F5F5 | 页面背景 | 浅灰 |
#FFFFFF | 卡片背景、选中文字 | 白色 |
#000000 | 标题文字 | 黑色 |
#333333 | 课程名称 | 深灰 |
#666666 | 辅助信息 | 中灰 |
#999999 | 次要信息 | 浅灰 |
#1A000000 | 阴影 | 10% 黑色 |
颜色格式:#RRGGBB(红绿蓝)或 #AARRGGBB(透明度+红绿蓝)
十三、完整代码结构图
@Entry @Component struct Index
├── @State 状态变量 × 4
├── aboutToAppear() ← 生命周期,调用 initCourseData
├── initCourseData() ← 初始化 7 天课程数据
├── build() ← 主界面
│ ├── Text("我的课程表") ← 标题
│ ├── buildWeekNavigation() ← @Builder 星期导航
│ │ ├── Scroll ← 水平滚动容器
│ │ ├── Row ← 水平布局
│ │ └── ForEach ← 遍历 courseData
│ │ └── Column ← 每个星期项
│ │ ├── Text(day) ← 星期几
│ │ ├── Text(date) ← 日期
│ │ └── onClick ← 切换选中
│ └── buildCourseList() ← @Builder 课程列表
│ ├── if (有课程)
│ │ ├── List ← 列表容器
│ │ └── ForEach ← 遍历当天课程
│ │ └── ListItem
│ │ └── buildCourseItem()
│ └── else (无课程)
│ └── 空状态提示
├── buildCourseItem() ← @Builder 单个课程项
│ ├── Row
│ │ ├── Column (时间标签)
│ │ │ ├── Text(开始时间)
│ │ │ ├── Text(|)
│ │ │ └── Text(结束时间)
│ │ └── Column (课程信息)
│ │ ├── Text(课程名称)
│ │ └── Text(教室 | 教师)
│ ├── PanGesture ← 左滑删除手势
│ └── onClick ← 点击查看详情
├── showCourseDetail() ← 课程详情 AlertDialog
├── showDeleteConfirm() ← 删除确认 AlertDialog
└── deleteCourse() ← 删除课程并更新 UI
class CourseDay ← 数据模型:一天
├── day: string
├── date: string
└── courses: CourseItem[]
class CourseItem ← 数据模型:一门课
├── id: string
├── name: string
├── time: string
├── classroom: string
└── teacher: string
十四、知识点速查索引
| 类别 | 知识点 | 对应填空 |
|---|
| 装饰器 | @Entry, @Component, @State, @Builder | 空 1 |
| 生命周期 | aboutToAppear | 空 2 |
| 数据模型 | class 定义 | 空 14(底部) |
| 布局 | Column, Row | 空 4, 6, 12 |
| 滚动 | Scroll, ScrollDirection.Horizontal | 空 9 |
| 列表 | List, ListItem | 空 10 |
| 循环 | ForEach + 键值生成器 | 空 5, 10 |
| 条件 | if / else | 空 10 |
| 文本 | Text + fontSize/fontWeight/fontColor | 空 1, 6, 12 |
| 样式 | margin, padding, backgroundColor, borderRadius, shadow | 空 4, 7, 13 |
| 对齐 | justifyContent, alignItems, layoutWeight | 空 4, 11 |
| 点击 | onClick | 空 8 |
| 手势 | PanGesture, direction, distance, onActionEnd | 空 13 |
| 对话框 | AlertDialog.show (单按钮/双按钮) | 空 14 |
| 类型 | number, string, boolean, 数组, 联合类型 | 空 1 |
| 字符串 | split(), 模板字符串 | 空 3, 12, 14 |
| 数组 | findIndex(), splice(), JSON 深拷贝 | 空 14 |
| 运算 | 三元表达式, 非空断言(!) | 空 6, 7, 14 |
| 函数 | 箭头函数 | 空 5, 8, 13 |