一、项目简介
本项目基于 HarmonyOS 最新开发框架 ArkTS,使用 DevEco Studio 开发一款轻量级但功能完整的课程表应用(ScheduleAPP)。该应用支持用户查看每日课程安排、添加/编辑课程信息、切换周视图等功能,适用于大学或中学师生日常使用。
1.1 核心功能
课程表展示:以周为单位显示课程安排,支持星期切换
添加课程:填写课程名称、教室、备注等信息
编辑课程:修改已有课程信息
查看详情:点击课程卡片查看详细信息
周数计算:自动计算当前学期周数
1.2 技术栈
- 开发平台:HarmonyOS SDK(API 20)
- 编程语言:ArkTS
- 工具链:DevEco Studio 6.0.0
1.3 项目源码
项目完整代码已上传至Gitee,欢迎大家下载使用。
ScheduleAPP项目开发源码https://gitee.com/zhen-shi_1_0/schedule.git
二、开发环境搭建
- 安装DevEco Studio 6.0 或更高版本
- 配置 HarmonyOS SDK(确保包含 API Version 20)
- 创建模拟器(推荐使用 Phone 类型设备)
- 验证环境:新建空白项目并成功运行 Hello World
提示:开发环境搭建详细步骤请看作者的其它博文。如:下面这篇博文的前两节:零基础使用 Flutter 编译开发 鸿蒙 HarmonyOS 项目教程——搭建环境篇
三、项目创建与结构说明
3.1 新建项目
在 DevEco Studio 中:
- 选择
File → New → Create Project - 模板:
Empty Ability - 项目配置:
- Project name:
ScheduleAPP - Bundle name:
com.example.scheduleapp - Compile SDK:API 20
- Device type:Phone
- Project name:
点击Finish完成创建。
3.2 项目目录结构
创建完成后,项目结构如下:
ScheduleAPP/ ├── AppScope/ # 应用全局配置 │ ├── app.json5 │ └── resources/ └── entry/ # 主模块 └── src/main/ ├── ets/ │ ├── entryability/ # 应用入口 │ └── pages/ # 页面代码(重点) ├── resources/ # 图片、字符串等资源 └── module.json5 # 模块配置四、核心功能实现
4.1 定义课程数据模型(Course.ets)
在entry/src/main/ets/pages/class/目录下创建Course.ets:
export class Course { public courseName: string = ''; // 课程名称 public classroom: string = ''; // 教室 public remark: string = ''; // 备注(如教师姓名) public index: number = 0; // 唯一位置索引(0~59) constructor(courseName: string, classroom: string, remark: string, index: number) { this.courseName = courseName; this.classroom = classroom; this.remark = remark; this.index = index; } }代码说明:
使用
class定义课程数据类包含课程的基本信息:名称、教室、备注、索引
index用于标识课程在课程表中的位置(0-59,表示60个时间槽)
4.2 主页面开发(Index.ets)
主页面代码:
import router from '@ohos.router'; import { Course } from './class/Course' const content1: string[] = (() => { const arr: string[] = new Array(15).fill(''); for (let i = 0; i < 15; i += 1) { arr[i] = i + 1 + ''; } return arr; })() //节数 let newName: string = ''; @Entry @Component struct TableHome { @StorageLink('name') name: string = '课程表1'; @StorageLink('showDialog') showDialog: boolean = false; //课程数据数组(课程总数量) @State content: Course[] = (() => { const arr: Course[] = new Array(60); for (let i = 0; i < 60; i++) { arr[i] = new Course('', '', '',i); } return arr; })() @State currentWeekday: number = new Date().getDay() || 7 // 0-6, 0=周日,转换为1-7 // 使用 @StorageLink 监听 AppStorage 变化 @StorageLink('updatedCourse') updatedCourse: Course = new Course('', '', '', -1); @StorageLink('showDetails') showDetails: boolean = false; // 在 TableHome 组件中添加状态 @StorageLink('selectedCourse') selectedCourse: Course = new Course('', '', '', -1); // 获取指定星期几的日期 getWeekDate(dayOfWeek: number): string { let date = new Date(); // 计算目标日期(0=周日,1=周一...) let targetDay = dayOfWeek - 1; // 调整为JavaScript的星期索引(0=周日) let diff = targetDay - date.getDay(); // 当前星期几与目标星期几的差值 date.setDate(date.getDate() + diff); let month = date.getMonth() + 1; let day = date.getDate(); return `${month}/${day}`; } // 计算从指定日期到当前日期经过的周数 getWeeksSinceStart(startYear: number, startMonth: number, startDay: number): number { let startDate = new Date(startYear, startMonth - 1, startDay); // 注意:月份从0开始 let currentDate = new Date(); // 计算两个日期之间的毫秒差 let timeDiff = currentDate.getTime() - startDate.getTime(); // 转换为天数,再转换为周数 let daysDiff = Math.floor(timeDiff / (1000 * 60 * 60 * 24)); let weeksDiff = Math.floor(daysDiff / 7) + 1; return weeksDiff; } // 获取星期名称 getWeekdayName(weekday: number): string { const names = ['', '周一', '周二', '周三', '周四', '周五', '周六', '周日']; return names[weekday] || ''; } aboutToAppear(): void { // 初始化 AppStorage AppStorage.SetOrCreate('updatedCourse', new Course('', '', '', -1)); AppStorage.SetOrCreate('showDetails', false);// 控制Details组件显示/隐藏 } // 关键:页面每次显示时同步 updatedCourse 到 courses onPageShow() { // console.log('onPageShow: 检查 updatedCourse 是否有更新'); this.syncUpdatedCourseToLocal(); } syncUpdatedCourseToLocal(): void { const uc = this.updatedCourse; if (uc.index >= 0 && uc.index < this.content.length) { // 只更新对应 index 的课程 this.content[uc.index] = new Course( uc.courseName, uc.classroom, uc.remark, uc.index ); // console.log(`同步第 ${uc.index} 节课成功:`, JSON.stringify(uc)); } } @Builder hingeBody(contNumber: number) { Sidebar({ inputValue: content1[contNumber] }) ForEach(this.content.slice(contNumber*5+1, contNumber*5+6), (item: Course) => { GridItemCase({ course: item }); }) } @Builder mainBody() { ForEach([0,1,2,3], (item: number)=> { this.hingeBody(item) }) GridItem() { Text('午休'); } .GridItemRestFn() ForEach([4,5,6,7], (index: number)=> { this.hingeBody(index) }) GridItem() { Text('晚休'); } .GridItemRestFn() ForEach([8,9,10], (index: number)=> { this.hingeBody(index) }) } build() { //层叠布局 Stack({ alignContent: Alignment.BottomStart }) { Column() { // 导航栏 Row() { Image($r('app.media.chevron_left')) .height(40) .onClick(() => { router.back() }) .margin({ right: 10 }) if (this.showDialog) { Dialog() /*.position({ top: 100, left: 0, right: 0 })*/ }else { Text(this.name) .fontSize(18) .fontWeight(FontWeight.Bold) .alignSelf(ItemAlign.Center) .onClick(() => {this.showDialog = true;}) } /*Blank() Image($r('app.media.menu_01')) .height(30) .onClick(() => { //跳转到全部课程表 }) .margin({ right: 10 })*/ } .width('100%') .height(60) .padding(5) .backgroundColor("#F2F2F4") .alignItems(VerticalAlign.Center) Row() { //表头——星期 Grid() { //网格布局 GridItem(){ Column() { Text(){ Span(this.getWeeksSinceStart(2025,9,8).toString()) //周数 Span('周') } .fontSize(12) .fontWeight(FontWeight.Bolder) Image($r('app.media.chevron_down')) .height(20) } .onClick(() => { //跳转到周数选择页面 /*router.pushUrl({ url: 'pages/WeekSelectPage' })*/ }) } .height('100%') // 星期选择器 ForEach([1,2,3,4,5,6,7],(weekday: number)=>{ GridItem(){ Column() { Text(this.getWeekdayName(weekday)) .fontSize(14) .fontColor(this.currentWeekday === weekday ? '#FFFFFF' : '#666666') .fontWeight(this.currentWeekday === weekday ? FontWeight.Bold : FontWeight.Normal) .fontWeight(FontWeight.Bold) .margin({ bottom: 2 }) Text(this.getWeekDate(weekday+ 1)) //获取星期几的日期 .fontSize(10) } .justifyContent(FlexAlign.Center) } .height(40) .backgroundColor(this.currentWeekday === weekday ? '#007DFF' : '#F5F5F5') .borderRadius(8) .onClick(() => { this.currentWeekday = weekday; }) }) } .columnsTemplate('13fr 18fr 18fr 18fr 18fr 18fr') .padding({ left: 8, right: 8, top: 8, bottom: 8 }) .columnsGap(1) // 设置列间距 } .height('8%') .backgroundColor("#F2F2F4") .margin({bottom:2}) // 底部间距 //主体内容 Grid() { this.mainBody() GridItem() { Row() { // 你的内容 } .height(90) .width('100%') } } .width('100%') .height('100%') .columnsTemplate('13fr 18fr 18fr 18fr 18fr 18fr') .scrollBar(BarState.Off) .edgeEffect(EdgeEffect.Spring) // 边缘效果 } .height('100%') .width('100%') .alignItems(HorizontalAlign.Start) if (this.showDetails) { Details() } } .height('100%') .width('100%') } } @Component struct GridItemCase { @State isSelected: boolean = false; // 是否被选中 @Prop course: Course = new Course('','','',0); @State isCourse: boolean = false; //课程是否被用户填写 @State params: Course = router.getParams() as Course; //获取用户填写的课程信息 // 构造函数方式接收参数 constructor(courseProp: Course) { super(); this.course = courseProp; } aboutToAppear(): void { // 在组件即将出现时进行一次初始化判断 if (this.course.courseName === '') { this.isCourse = false; } else { this.isCourse = true; } } build() { GridItem(){ Row(){ Column(){ if (this.isCourse) //课程被用户填写 { Text(this.course.courseName) .fontSize(13) .fontWeight(FontWeight.Bold) Text(this.course.classroom) .TextFn() Text(this.course.remark) .TextFn() }else { if (this.isSelected) { //用户点击 被选中 Text('+') .fontSize(24) .fontWeight(FontWeight.Bold) .foregroundColor(Color.Black) } } } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) } .onClick(()=>{ if (this.isCourse) { //课程被用户填写 AppStorage.Set('selectedCourse', this.course) // 保存选中的课程 AppStorage.Set('showDetails', true) //显示课程信息 }else { if (this.isSelected) { router.pushUrl({ url: 'pages/AddCoursePage', params: this.course }) } this.isSelected = !this.isSelected AppStorage.Set('showDetails', false) //隐藏课程信息 } }) .height(90) .backgroundColor(this.isSelected ? "#f2f2f2" : Color.White) } .border({ width: 1, color: "#F2F2F4", style: BorderStyle.Solid }) } } //侧边栏 @Component struct Sidebar { @Prop inputValue: string = ''; // 添加输入参数 // 构造函数方式接收参数 constructor(inputValue: string) { super(); this.inputValue = inputValue; } build() { GridItem(){ Column(){ Text(this.inputValue) .fontSize(14) .fontWeight(FontWeight.Bold) .margin(3) Text("08:00") .TextFn() Text("08:45") .TextFn() } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) .height(90) .onClick(()=>{ // 跳转到课程时间设置页面 /*router.pushUrl({ url: 'pages/AddCoursePage' })*/ }) } .border({ width: 1, color: "#F2F2F4", style: BorderStyle.Solid }) } } //课程详情 @Component struct Details { @StorageLink('selectedCourse') selectedCourse: Course = new Course('', '', '', -1) build() { //课程详情 Column() { //标题 Row() { Text('课程详情') .width('65%') .fontSize(20) .fontWeight(FontWeight.Bolder) .textAlign(TextAlign.End) Blank() Image($r('app.media.x_close')) .width(30) .onClick(() => { //关闭详情页面 AppStorage.Set('showDetails', false) }) } .padding({ top: 15, right: 15 }) .width('100%') Column() { //课程名 Row(){ Circle() .width(10) .height(10) .borderRadius(5) //背景颜色需要传入 .backgroundColor(Color.Black) .margin({ right: 10 }) // Text('计算机网络') Text(this.selectedCourse.courseName) //参数courseName传入 .fontSize(18) .textAlign(TextAlign.Start) Blank() Button('编辑') .size({width: 60, height: 30}) .fontColor('#2f2f2f') .backgroundColor('#f5f5f5') .onClick(() => { //跳转到课程编辑页面 router.pushUrl({ url: 'pages/AddCoursePage', params: this.selectedCourse }) }) } .width('100%') .padding({ right: 15 }) .margin({ bottom: 10 }) /*组件测试数据 Text('教室:' + '基础实验楼701') .margin({ bottom: 5 }) .width('100%') Text('备注:' + '章老师') .margin({ bottom: 5 }) .width('100%')*/ if (this.selectedCourse.classroom !== ''){ Text('教室:' + this.selectedCourse.classroom) .margin({ bottom: 5 }) .width('100%') } if (this.selectedCourse.remark !== ''){ Text('备注(如老师):' + this.selectedCourse.remark) .margin( { bottom: 5 }) .width('100%') } Text('周三 ' + '第1-2节' + '(8:00 - 9:40)') //参数week、section、time传入 .width('100%') .margin({ bottom: 5 }) Row() { Text('第1-18周') .margin({right: 5}) Text('单周') .fontSize(12) .backgroundColor(Color.Gray) } .width('100%') } .width('90%') .margin({ top: 15 }) .padding(20) .justifyContent(FlexAlign.Start) .backgroundColor(Color.White) .borderRadius(20) /*Text('新建课程') .fontSize(20) .fontWeight(FontWeight.Bold) .fontColor(Color.Blue) .margin({ top: 25, bottom: 10}) .onClick(() => { router.pushUrl({ url: 'pages/AddCoursePage' }) })*/ } .width('100%') .padding(12) .borderRadius({ topLeft: 50, topRight: 50, bottomLeft: 20, bottomRight: 20 }) .backgroundColor('#f7f7f7') // .backgroundColor(Color.Pink) .justifyContent(FlexAlign.Center) .position({ bottom: 0 }) } } @Preview @Component struct Dialog { build() { Row() { TextInput({ placeholder: '请输入课程表名称' }) .width('60%') .height(35) .onChange((value: string) => { newName = value; }) .margin({ right: 15 }) Button('确定') .height(35) .onClick(() => { if (newName === '') { newName = '课程表1' } AppStorage.Set('name', newName) AppStorage.Set('showDialog', false) }) } } } @Extend(GridItem) function GridItemRestFn() { .width('100%') .backgroundColor("#F2F2F2") .columnStart(0) .columnEnd(5) } @Extend(Text) function TextFn() { .fontSize(11) .fontColor("#d0d0d0") }4.2.1 页面核心功能
该页面实现了以下核心功能:
- 显示一周(周一至周日)的课程安排网格
- 高亮当前星期,并显示对应日期(如“12/16”)
- 展示课程节数(1~15节),并分段插入“午休”“晚休”提示
- 点击空课格 → 弹出添加课程对话框(Dialog)
- 点击已有课程 → 弹出课程详情/编辑弹窗(Details)
- 支持从其他页面返回时同步更新课程数据(onPageShow + syncUpdatedCourseToLocal)
- 顶部导航栏支持返回上一页和重命名课程表
4.2.2 主要组成部分
1.主页面(TableHome):你打开 App 第一眼看到的课程表格。
2.课程格子(GridItemCase):表格里的每一个小方块,有的是空的(+号),有的写着“高数”“教室301”。
3.课程详情弹窗(Details):点击一个课程后,从底部滑出的详细信息窗口,还能点“编辑”去修改。
此外还有:
- 侧边栏(Sidebar):左边显示“第1节”“第2节”……和上课时间。
- 改名对话框(Dialog):点击顶部“课程表1”时弹出的输入框。
4.2.3 关键技术与方法解析
1.@State 和 @StorageLink
@State:组件自己的共享数据,比如content数组记录60节课。@StorageLink:连接全局共享数据(叫 AppStorage),多个页面都能读写同一个数据。
举个例子:
你在详情页编辑了“高数 → 教室401”,保存后返回主页面。
主页面通过@StorageLink('updatedCourse')立刻知道课程变了,自动刷新显示。
2.Course 类
class Course { courseName: string; // 课程名,如“计算机网络” classroom: string; // 教室,如“实验楼701” remark: string; // 备注,如“张老师” index: number; // 在表格中的位置编号(0~59) }每个课程格子背后都有这样一个“对象”,存着所有信息。
3.Grid 布局 —— 做表格的“神器”
HarmonyOS 用Grid+GridItem来画表格:
- 主页面用
Grid分成6列(1列节数 + 5列周一到周五) - 每行用
hingeBody构建一节课在5天的情况 - 插入“午休”“晚休”时,用
.columnStart(0).columnEnd(5)占满整行
效果:整齐的课程表,像 Excel 一样。
4.ForEach —— 批量生成重复内容
ForEach([0,1,2,3], (item) => { this.hingeBody(item) })自动生成“第1节、第2节、第3节、第4节”的行,不用手写4遍。
5.日期计算 —— 自动显示“今天周几”“第几周”
new Date().getDay():获取今天是星期几(0=周日,1=周一…)getWeekDate(dayOfWeek):算出“周一对应12月16日”这样的日期getWeeksSinceStart(2025,9,8):从开学日(2025-09-08)算起,今天是第几周
效果:顶部显示“第15周”,每个星期按钮下显示“12/16”等日期。
6.页面跳转与传参(router)
router.pushUrl({ url: 'pages/AddCoursePage', params: this.course // 把课程信息传过去 })点击空格子 ➜ 跳转到“添加课程页”,并告诉它:“你要填的是第3节周三的位置”。
7.点击交互逻辑(GridItemCase)
每个课程格子有两种状态:
| 状态 | 显示内容 | 点击效果 |
|---|---|---|
| 空格子 | 显示+号 | 再点一次 ➜ 跳转去添加课程 |
| 有课程 | 显示课程名、教室 | 点击 ➜ 弹出详情页 |
8.弹窗显示(Details + Dialog)
- Details:用
if (this.showDetails) { Details() }控制是否显示 - Dialog:点击顶部名称时,临时替换成输入框,输完点“确定”就改名
9.@Builder
@Builder hingeBody(contNumber: number) { ... }把“一节课的5个格子”做成一个可复用的组件,哪里需要就写在哪里。
4.3 添加/编辑课程页面开发(AddCoursePage.ets)
创建entry/src/main/ets/pages/AddCoursePage.ets文件:
import router from '@ohos.router'; import { Course } from './class/Course' @Entry @Component struct AddCoursePage { @StorageLink('updatedCourse') updatedCourse: Course = new Course('', '', '', -1); @State course: Course = new Course('','','',-1); @State timeSlotCount: number = 1; // 时间段数量 private weekRange: string = '第9-18周'; private backgroundColor1: ResourceColor = '#007DFF'; @State isEditMode: boolean = false; // 是否为编辑模式 aboutToAppear(): void { const params: Course = router.getParams() as Course; if (params && params.index >= 0) { this.course.index = params.index; if (params.courseName !== '') { this.course = params; this.isEditMode = true; } } } build() { Column() { // 导航栏 Row() { Text('取消') .fontColor("#0075E6") .fontSize(16) .onClick(() => { router.back() AppStorage.Set('showDetails', false) }) Text(this.isEditMode ? '编辑课程' : '新建课程') .fontSize(18) .fontWeight(FontWeight.Bold) .alignSelf(ItemAlign.Center) Text('完成') .fontColor("#0075E6") .fontSize(16) .onClick(() => { AppStorage.SetOrCreate('updatedCourse', this.course); AppStorage.Set('showDetails', false) router.back(); }) } .width('100%') .height(60) .padding(10) .backgroundColor(Color.White) .justifyContent(FlexAlign.SpaceBetween) .alignItems(VerticalAlign.Center) // 内容区域 Scroll() { Column() { // 课程名输入 Row() { Text('课程名') .fontSize(20) .fontWeight(FontWeight.Medium) .margin({ right: 10 }) TextInput({ placeholder:'必填', text:this.course.courseName }) .layoutWeight(1) .backgroundColor(Color.White) .onChange((value: string) => { this.course.courseName = value; }) } .height(60) .margin({ top: 20, bottom: 15 }) .borderRadius(10) .backgroundColor(Color.White) .padding(10) .width('90%') // 教室输入 Row() { Text('教室') .fontSize(20) .fontWeight(FontWeight.Medium) .margin({ right: 10 }) TextInput({ placeholder: '非必填', text:this.course.classroom }) .layoutWeight(1) .backgroundColor(Color.White) .onChange((value: string) => { this.course.classroom = value; }) } .width('90%') .height(60) .margin({ bottom: 15 }) .borderRadius(10) .backgroundColor(Color.White) .padding(10) // 备注输入 Row() { Text('备注(如老师)') .fontSize(20) .fontWeight(FontWeight.Medium) .margin({ right: 10 }) TextInput({ placeholder: '非必填', text:this.course.remark }) .layoutWeight(1) .backgroundColor(Color.White) .onChange((value: string) => { this.course.remark = value; }) } .width('90%') .height(60) .margin({ bottom: 15 }) .borderRadius(10) .backgroundColor(Color.White) .padding(10) // 时段选择(简化版,后续可扩展) Column() { Row() { Text('时段') .fontSize(20) .fontWeight(FontWeight.Medium) .margin({ right: 10 }) Blank() Row() { Button(){ Text('-') .fontSize(30) .fontColor(Color.Black) } .width(40) .height(40) .type(ButtonType.Circle) .backgroundColor("#F3F3F3") .onClick(() => { if (this.timeSlotCount > 1) { this.timeSlotCount--; } }) Text(this.timeSlotCount.toString()) .textAlign(TextAlign.Center) .fontSize(16) .width(40) .height(40) Button(){ Text('+') .fontSize(30) .fontColor(Color.Black) } .width(40) .height(40) .type(ButtonType.Circle) .backgroundColor("#F3F3F3") .onClick(() => { if (this.timeSlotCount < 3) { this.timeSlotCount++; } }) } .margin(10) .alignItems(VerticalAlign.Center) .justifyContent(FlexAlign.SpaceBetween) } .width('100%') .height(60) } .backgroundColor(Color.White) .borderRadius(10) .width('90%') .padding({ left: 10, right: 10 }) // 上课周数 Column() { Row() { Text('上课周数') .fontSize(20) .fontWeight(FontWeight.Medium) Blank() Text(this.weekRange) .fontSize(14) .fontColor(Color.Gray) .margin(5) Image($r('app.media.chevron_right')) .width(20) .height(20) } .width('100%') .height(60) .alignItems(VerticalAlign.Center) // 课程背景色 Row() { Text('课程背景色') .fontSize(20) .fontWeight(FontWeight.Medium) Blank() Circle() .width(20) .height(20) .fill(this.backgroundColor1) .margin(5) Image($r('app.media.chevron_right')) .width(20) .height(20) } .width('100%') .height(60) .alignItems(VerticalAlign.Center) } .width('90%') .margin(20) .borderRadius(10) .backgroundColor(Color.White) .padding({ left: 10, right: 10 }) } .width('100%') } } .width('100%') .height('100%') .backgroundColor("#F2F2F4") } }4.3.1页面核心功能
这个文件叫AddCoursePage.ets,它是课程表 App 中的“添加/编辑课程”页面。
就像在手机上点一个空课格子后,弹出来的那个填写课程信息的界面:
- 可以输入:课程名(必填)
- 可以输入:教室、老师备注(选填)
- 可以设置:这门课占几节课(1节、2节或3节)
- 还能选:上课周数、课程颜色(颜色和周数目前只是展示,还没做选择功能)
支持两种模式:
- 新建课程(从空格子进来)
- 编辑课程(从已有课程点进来)
4.3.2 核心功能解析
1. 接收信息
const params: Course = router.getParams() as Course;当你从主页面点击某个课程格子时,App 会把那个格子的“位置编号”(比如第3节周三)和课程内容一起传过来。
这个页面一打开,就会先看看是否有数据传入,然后进行下一步操作。
效果:
- 如果传过来的是空课程→ 显示“新建课程”
- 如果传过来的是已有课程(比如“高数”)→ 自动填好信息,标题变成“编辑课程”
2. 顶部导航栏:取消 / 标题 / 完成
Row() { Text('取消')...onClick(() => router.back()) Text(this.isEditMode ? '编辑课程' : '新建课程') Text('完成')...onClick(() => { 保存并返回 }) }- 点取消:直接返回上一页,不保存
- 点完成:把填好的课程信息存到全局共享数据(AppStorage),然后返回
关键代码:
AppStorage.SetOrCreate('updatedCourse', this.course);这样主页面就能知道有课程信息更新,然后自动刷新显示。
3. 输入框:填课程信息
用了 HarmonyOS 的TextInput组件:
| 输入项 | 是否必填 | 如何工作 |
|---|---|---|
| 课程名 | 必填 | 用户一打字,this.course.courseName = value立刻记住 |
| 教室 | 选填 | 同理,自动更新到this.course.classroom |
| 备注 | 选填 | 比如填“张老师”,存到remark字段 |
所有信息都存在
this.course“课程对象”里。
4. “时段数量”调节器(+ / - 按钮)
this.timeSlotCount // 默认是1节- 显示一个数字(1、2 或 3)
- 左边是减号按钮(不能少于1)
- 右边是加号按钮(最多3节)
虽然现在只是改数字,但未来可以用来控制“这门课横跨几行”(比如高数上2节,就占第1-2行)。
5. “上课周数”和“背景色”(预留功能)
private weekRange: string = '第9-18周'; private backgroundColor1: ResourceColor = '#007DFF';- 目前只是静态显示,比如“第9-18周”、“蓝色小圆点”
颜色数组
colors准备了8种好看的颜色,方便以后做“课程分类着色”。
4.4 配置页面路由
编辑entry/src/main/resources/base/profile/main_pages.json:
{ "src": [ "pages/Index", "pages/AddCoursePage" ] }4.5添加资源文件
4.5.1 添加图标资源
SVG图标下载地址:阿里巴巴矢量图标库
在entry/src/main/resources/base/media/目录下添加以下SVG图标:
chevron_left.svg- 左箭头chevron_right.svg- 右箭头chevron_down.svg- 下箭头x_close.svg- 关闭图标
4.5.2 字符串资源
编辑entry/src/main/resources/base/element/string.json:
{ "string": [ { "name": "app_name", "value": "课程表" }, { "name": "module_desc", "value": "课程表模块" }, { "name": "EntryAbility_desc", "value": "课程表应用入口" }, { "name": "EntryAbility_label", "value": "课程表" } ] }五、核心知识点汇总
5.1 状态管理
@State 装饰器
用于组件内部状态管理,状态变化会触发UI更新:
@State currentWeekday: number = 1;@StorageLink 装饰器
连接全局AppStorage,实现跨组件状态共享:
@StorageLink('name') name: string = '课程表1';AppStorage
全局状态存储,类似React的Context:
AppStorage.Set('name', '新课程表名'); AppStorage.Get('name');5.2 组件通信
父子组件传参(@Prop)
// 父组件 GridItemCase({ course: item }) // 子组件 @Prop course: Course;页面跳转传参
// 跳转 router.pushUrl({ url: 'pages/AddCoursePage', params: this.course }) // 接收参数 const params: Course = router.getParams() as Course;5.3 布局组件
Grid(网格布局)
用于创建课程表网格:
Grid() { // 内容 } .columnsTemplate('13fr 18fr 18fr 18fr 18fr 18fr') // 6列,比例布局Stack(层叠布局)
用于叠加详情弹窗:
Stack() { Column() { /* 主内容 */ } if (this.showDetails) { Details() /* 详情弹窗 */ } }5.4 生命周期
aboutToAppear(): 组件即将出现时调用onPageShow(): 页面显示时调用onPageHide(): 页面隐藏时调用
六、运行与测试
6.1 运行应用
连接设备或启动模拟器
点击
Run按钮(绿色三角形)或按Shift+F10等待编译完成,应用自动安装运行
6.2 测试功能
查看课程表:启动后应看到空白课程表
添加课程:
点击空白单元格,出现"+"号
再次点击进入添加页面
填写课程信息,点击"完成"
查看详情:点击已有课程,查看详情弹窗
编辑课程:在详情页点击"编辑",修改信息
七、常见问题解决
7.1 编译错误
问题:找不到资源文件$r('app.media.xxx')
解决:检查资源文件路径和名称是否正确
7.2 页面跳转失败
问题:router.pushUrl报错
解决:检查main_pages.json中是否注册了页面
7.3 状态更新不生效
问题:修改数据后UI不更新
解决:确保使用了@State或@StorageLink装饰器
7.4 课程数据丢失
问题:应用重启后课程消失
解决:当前使用内存存储,需要添加持久化存储(后续可扩展)
八、学习资源
- ArkTS开发指南
- DevEco Studio使用指南
- 阿里巴巴矢量图标库