HarmonyOS7 ArkUI 课程表应用 - 周视图、课程管理、课程详情实战
前言
很多 ArkUI 示例看着不大,真正难的是把状态、列表和交互放在合适的位置。这个案例就很典型。

这个案例的主线是 课程表应用,后面的 周视图、课程管理、课程详情 则把页面做得更像真实业务页。读代码时,建议把交互、状态和列表这三条线一起看。
先看页面目标
CourseSchedulePage 对应的是 课程表应用 - 周视图、课程管理、课程详情 这个业务场景。别把它只当成一个 UI 练习页,它真正有价值的地方在于:同一个页面里同时出现了状态切换、列表渲染、条件展示和用户反馈。
| 观察点 | 在这个案例里的表现 |
|---|---|
| 页面主题 | 课程表应用 |
| 主要文案 | 高等数学, 李教授, 1-16周, ArkTS开发, 张老师, 1-8周 |
| 常用组件 | Column, ForEach, Row, Scroll, Text |
| 适合练习 | 状态驱动 UI、局部刷新、事件回调、页面结构拆分 |
使用方法
把完整代码放进 ArkTS 页面文件后,就可以直接在预览器里运行。实际使用时,建议先手动点一遍页面里的主要交互,看状态有没有跟着变化;然后再去对照代码,理解每个 @State 字段到底控制了哪一块区域。
如果你想把这个案例改成自己的版本,我建议先做三件事:换掉模拟数据、调整筛选规则、再把重复结构抽成 Builder 或子组件。顺序别反,先改结构往往最容易把自己绕进去。
数据怎么驱动 UI
ArkUI 页面写顺手之后,你会发现很多问题其实都不是样式问题,而是状态没放对位置。这个案例里能直接看到哪些数据是输入、哪些是选择、哪些是列表源,读起来会比较顺。
| 状态字段 | 类型 | 说明 |
|---|---|---|
currentWeek |
number |
当前选中项,决定内容切换、高亮或过滤结果 |
selectedDay |
number |
当前选中项,决定内容切换、高亮或过滤结果 |
courses |
CourseItem_bgsb[] |
列表数据源,通常会交给 ForEach 或卡片区域渲染 |
selectedCourse |
`CourseItem_bgsb | undefined` |
我自己看这类示例时,会先把
@State和事件函数圈出来,再去看布局。这样不会被大段Column、Row搞乱节奏。
值得抄的片段
片段 1: getCoursesForDay
这段代码我建议单独看。它通常负责派生数据、交互动作或者局部渲染逻辑,把这部分抽出去之后,页面主结构会干净很多,后续要改规则也更省事。
getCoursesForDay(day: number): CourseItem_bgsb[] {
return this.courses.filter((c: CourseItem_bgsb) => c.dayOfWeek === day)
}
片段 2: getCourseAtSection
这段代码我建议单独看。它通常负责派生数据、交互动作或者局部渲染逻辑,把这部分抽出去之后,页面主结构会干净很多,后续要改规则也更省事。
getCourseAtSection(day: number, section: number): CourseItem_bgsb | undefined {
return this.courses.find((c: CourseItem_bgsb) =>
c.dayOfWeek === day && c.startSection <= section && section <= c.endSection
)
}

必读小结
适合拿来练手的能力:页面拆分、状态联动、条件渲染、交互反馈。
推荐阅读顺序:先看前言 -> 再跑页面 -> 再看状态表 -> 最后啃完整代码
如果只想快速上手,优先改模拟数据和交互函数,收益最高
我会重点检查的几个地方
- 点击或输入之后,界面有没有立即更新
- 列表渲染是否依赖了稳定的数据结构
- 筛选、统计、派生数据是不是单独放进函数里
- 是否还有重复布局可以继续抽出去
- 文案、颜色、间距是不是同一套风格
完整实现
// CourseSchedulePage - 课程表应用:周视图、课程管理、课程详情
interface CourseItem_bgsb {
id: number
name: string
teacher: string
classroom: string
color: string
dayOfWeek: number
startSection: number
endSection: number
weeks: string
}
interface DaySchedule {
dayIndex: number
dayName: string
date: string
isToday: boolean
}
interface InfoData { label: string, value: string, emoji: string }
@Entry
@Component
struct CourseSchedulePage {
@State currentWeek: number = 12
@State selectedDay: number = 5
@State courses: CourseItem_bgsb[] = [
{ id: 1, name: '高等数学', teacher: '李教授', classroom: 'A101', color: '#FF7875', dayOfWeek: 1, startSection: 1, endSection: 2, weeks: '1-16周' },
{ id: 2, name: 'ArkTS开发', teacher: '张老师', classroom: 'B205', color: '#69C0FF', dayOfWeek: 1, startSection: 3, endSection: 4, weeks: '1-8周' },
{ id: 3, name: '英语写作', teacher: 'Smith', classroom: 'C301', color: '#95DE64', dayOfWeek: 2, startSection: 1, endSection: 2, weeks: '1-16周' },
{ id: 4, name: '数据结构', teacher: '王教授', classroom: 'A201', color: '#FFB347', dayOfWeek: 2, startSection: 5, endSection: 6, weeks: '1-16周' },
{ id: 5, name: '线性代数', teacher: '赵老师', classroom: 'B101', color: '#D3ADF7', dayOfWeek: 3, startSection: 3, endSection: 4, weeks: '1-16周' },
{ id: 6, name: '计算机网络', teacher: '刘工程师', classroom: 'A302', color: '#87E8DE', dayOfWeek: 4, startSection: 1, endSection: 2, weeks: '5-16周' },
{ id: 7, name: '操作系统', teacher: '陈老师', classroom: 'B208', color: '#FFA39E', dayOfWeek: 4, startSection: 5, endSection: 6, weeks: '1-16周' },
{ id: 8, name: 'UI设计基础', teacher: '设计老师', classroom: '设计室', color: '#FFEC3D', dayOfWeek: 5, startSection: 1, endSection: 4, weeks: '1-8周' },
{ id: 9, name: '体育', teacher: '体育老师', classroom: '操场', color: '#B7EB8F', dayOfWeek: 3, startSection: 7, endSection: 8, weeks: '1-16周' },
]
@State selectedCourse: CourseItem_bgsb | undefined = undefined
private sections: string[] = ['08:00', '08:55', '10:00', '10:55', '14:00', '14:55', '16:00', '16:55', '19:00', '19:55']
private days: DaySchedule[] = [
{ dayIndex: 1, dayName: '一', date: '6/2', isToday: false },
{ dayIndex: 2, dayName: '二', date: '6/3', isToday: false },
{ dayIndex: 3, dayName: '三', date: '6/4', isToday: false },
{ dayIndex: 4, dayName: '四', date: '6/5', isToday: true },
{ dayIndex: 5, dayName: '五', date: '6/6', isToday: false },
{ dayIndex: 6, dayName: '六', date: '6/7', isToday: false },
{ dayIndex: 7, dayName: '日', date: '6/8', isToday: false },
]
getCoursesForDay(day: number): CourseItem_bgsb[] {
return this.courses.filter((c: CourseItem_bgsb) => c.dayOfWeek === day)
}
getCourseAtSection(day: number, section: number): CourseItem_bgsb | undefined {
return this.courses.find((c: CourseItem_bgsb) =>
c.dayOfWeek === day && c.startSection <= section && section <= c.endSection
)
}
shouldShowCourse(day: number, section: number): boolean {
const course = this.getCourseAtSection(day, section)
return course !== undefined && course.startSection === section
}
getCourseHeight(day: number, section: number): number {
const course = this.getCourseAtSection(day, section)
if (course === undefined) return 0
return (course.endSection - course.startSection + 1) * 56 - 2
}
shouldShowEmptyCell(day: number, section: number): boolean {
const course = this.getCourseAtSection(day, section)
return course === undefined || course.startSection === section
}
build() {
Column() {
// 顶部周信息
Row() {
Text('◀').fontSize(18).fontColor('#595959').padding(8)
.onClick(() => { if (this.currentWeek > 1) this.currentWeek-- })
Text(`第 ${this.currentWeek} 周`).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#1A1A1A').layoutWeight(1).textAlign(TextAlign.Center)
Text('▶').fontSize(18).fontColor('#595959').padding(8)
.onClick(() => { if (this.currentWeek < 20) this.currentWeek++ })
}.width('100%').padding({ left: 8, right: 8, top: 12, bottom: 8 })
// 星期表头
Row() {
Text('节次').fontSize(11).fontColor('#BFBFBF').width(36).textAlign(TextAlign.Center)
ForEach(this.days, (d: DaySchedule) => {
Column() {
Text(`周${d.dayName}`).fontSize(12)
.fontColor(d.isToday ? '#1890FF' : '#595959')
.fontWeight(d.isToday ? FontWeight.Bold : FontWeight.Normal)
Text(d.date).fontSize(10)
.fontColor(d.isToday ? Color.White : '#BFBFBF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor(d.isToday ? '#1890FF' : 'transparent')
.borderRadius(10).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 6, bottom: 6 })
})
}.width('100%').backgroundColor(Color.White).border({ width: { bottom: 1 }, color: '#F0F0F0' })
// 课程表格子
Scroll() {
Column() {
ForEach([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], (section: number) => {
Row() {
// 节次时间
Column() {
Text(`${section}`).fontSize(12).fontWeight(FontWeight.Bold).fontColor('#8C8C8C')
Text(this.sections[section - 1]).fontSize(9).fontColor('#BFBFBF')
}.width(36).height(56).alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
// 各天课程格子
ForEach(this.days, (d: DaySchedule) => {
if (this.shouldShowCourse(d.dayIndex, section)) {
Column() {
Text(this.getCourseAtSection(d.dayIndex, section)!.name).fontSize(11).fontWeight(FontWeight.Bold).fontColor(Color.White)
.textAlign(TextAlign.Center).maxLines(2)
Text(this.getCourseAtSection(d.dayIndex, section)!.classroom).fontSize(9).fontColor('rgba(255,255,255,0.9)').margin({ top: 2 })
}
.width('100%').height(this.getCourseHeight(d.dayIndex, section))
.backgroundColor(this.getCourseAtSection(d.dayIndex, section)!.color)
.borderRadius(4).padding(4)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
.layoutWeight(1).margin(1)
.onClick(() => { this.selectedCourse = this.getCourseAtSection(d.dayIndex, section) })
} else if (this.shouldShowEmptyCell(d.dayIndex, section)) {
Column().layoutWeight(1).height(54).backgroundColor('transparent').margin(1)
} else {
// 课程已占位
Column().layoutWeight(1).height(0).margin(1)
}
})
}.width('100%').border({ width: { bottom: 1 }, color: '#F7F7F7' })
})
}.padding({ bottom: 20 })
}.layoutWeight(1)
// 课程详情弹层
if (this.selectedCourse !== undefined) {
Column() {
Column() {
Row() {
Column() {
Text('').width(20).height(20).backgroundColor(this.selectedCourse!.color).borderRadius(10)
}.width(24)
Text(this.selectedCourse!.name).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#1A1A1A').layoutWeight(1).margin({ left: 8 })
Text('✕').fontSize(20).fontColor('#8C8C8C').onClick(() => { this.selectedCourse = undefined })
}.margin({ bottom: 16 })
ForEach([
{ label: '授课教师', value: this.selectedCourse!.teacher, emoji: '👨🏫' },
{ label: '上课地点', value: this.selectedCourse!.classroom, emoji: '📍' },
{ label: '上课节次', value: `第${this.selectedCourse!.startSection}-${this.selectedCourse!.endSection}节`, emoji: '⏰' },
{ label: '上课周数', value: this.selectedCourse!.weeks, emoji: '📅' },
], (info: InfoData) => {
Row() {
Text(info.emoji).fontSize(16).width(24)
Text(info.label).fontSize(13).fontColor('#8C8C8C').width(70)
Text(info.value).fontSize(13).fontColor('#1A1A1A').layoutWeight(1)
}.padding({ top: 10, bottom: 10 }).border({ width: { bottom: 1 }, color: '#F0F0F0' })
})
}
.width('100%').padding(20).backgroundColor(Color.White)
.borderRadius({ topLeft: 20, topRight: 20 })
}
.width('100%').position({ x: 0, y: 0 })
.height('100%')
.backgroundColor('rgba(0,0,0,0.4)')
.justifyContent(FlexAlign.End)
.onClick(() => { this.selectedCourse = undefined })
}
}
.width('100%').height('100%').backgroundColor('#F5F5F5')
}
}

再补一句
这类案例最值得学的,其实不是某一个控件怎么写,而是页面组织方式。把状态放对、把交互函数收好、把布局压简单,后面你接正式项目时会轻松很多。
更多推荐



所有评论(0)