从零开始:HarmonyOS ArkTS课程表APP开发完整教程
课程表展示:以周为单位显示课程安排,支持星期切换添加课程:填写课程名称、教室、备注等信息编辑课程:修改已有课程信息查看详情:点击课程卡片查看详细信息周数计算:自动计算当前学期周数。
本文详细介绍如何使用DevEco Studio和ArkTS语言开发一个功能完整的课程表手机应用,适合HarmonyOS开发新手学习。
一、项目简介
1.1 项目功能
本项目是一个基于HarmonyOS的课程表管理应用,主要功能包括:
-
课程表展示:以周为单位显示课程安排,支持星期切换
-
添加课程:填写课程名称、教室、备注等信息
-
编辑课程:修改已有课程信息
-
查看详情:点击课程卡片查看详细信息
-
周数计算:自动计算当前学期周数
1.2 技术栈
-
开发工具:DevEco Studio
-
开发语言:ArkTS
1.3 项目源码
项目完整代码已上传至Gitee,欢迎大家下载使用。ScheduleAPP项目开发源码
https://gitee.com/zhen-shi_1_0/schedule.git
二、开发环境搭建
DevEco Studio的下载及使用请查看下面这篇博文的前两节。零基础使用 Flutter 编译开发 鸿蒙 HarmonyOS 项目教程——搭建环境篇
https://blog.csdn.net/2401_82544706/article/details/155164990?sharetype=blogdetail&sharerId=155164990&sharerefer=WAP&sharesource=2401_82544706
三、创建项目
3.1 新建项目
-
打开DevEco Studio,点击
File→New→Create Project -
选择模板:选择
Empty Ability(空白应用)
-
配置项目信息:
-
Project name:
ScheduleAPP -
Bundle name:
com.example.scheduleapp -
Compile SDK: API 9或更高
-
Device type:
Phone
-

4. 点击 Finish 完成创建
3.2 项目结构说明
创建完成后,项目结构如下:
ScheduleAPP/
├── AppScope/ # 应用级配置
│ ├── app.json5 # 应用配置
│ └── resources/ # 应用资源
├── entry/ # 应用主模块
│ ├── src/
│ │ ├── main/
│ │ │ ├── ets/ # 代码目录
│ │ │ │ ├── entryability/ # 应用入口
│ │ │ │ └── pages/ # 页面目录(我们要创建的地方)
│ │ │ ├── resources/ # 资源目录
│ │ │ └── module.json5 # 模块配置
四、核心功能实现
4.1 第一步:创建课程数据模型
在 entry/src/main/ets/pages/ 目录下创建 class 文件夹,然后创建 Course.ets 文件:
export class Course {
public courseName: string = ''; // 课程名
public classroom: string = ''; // 教室
public remark: string = ''; // 备注(如老师)
public index: number = 0; // 课程索引(用于定位)
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)
主页面是课程表的展示界面,包含周数显示、星期选择、课程网格等。
4.2.1 页面结构
创建 entry/src/main/ets/pages/Index.ets 文件:
import router from '@ohos.router';
import { Course } from './class/Course'
// 课程节数数组(1-15节)
const content1: string[] = (() => {
const arr: string[] = new Array(15).fill('');
for (let i = 0; i < 15; i += 1) {
arr[i] = i + 1 + '';
}
return arr;
})()
@Entry
@Component
struct TableHome {
// 使用 @StorageLink 连接全局状态
@StorageLink('name') name: string = '课程表1';
@StorageLink('showDialog') showDialog: boolean = false;
@StorageLink('showDetails') showDetails: boolean = false;
@StorageLink('selectedCourse') selectedCourse: Course = new Course('', '', '', -1);
@StorageLink('updatedCourse') updatedCourse: Course = new Course('', '', '', -1);
// 课程数据数组(60个时间槽:12节×5天)
@State content: Course[] = (() => {
const arr: Course[] = new Array(60);
for (let i = 0; i < 60; i++) {
arr[i] = new Course('', '', '', i);
}
return arr;
})()
// 当前选中的星期(1-7,1=周一)
@State currentWeekday: number = new Date().getDay() || 7;
// 获取星期名称
getWeekdayName(weekday: number): string {
const names = ['', '周一', '周二', '周三', '周四', '周五', '周六', '周日'];
return names[weekday] || '';
}
// 获取指定星期几的日期
getWeekDate(dayOfWeek: number): string {
let date = new Date();
let targetDay = dayOfWeek - 1; // 转换为JS星期索引(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);
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;
}
aboutToAppear(): void {
// 初始化全局状态
AppStorage.SetOrCreate('updatedCourse', new Course('', '', '', -1));
AppStorage.SetOrCreate('showDetails', false);
}
// 页面显示时同步更新
onPageShow() {
this.syncUpdatedCourseToLocal();
}
syncUpdatedCourseToLocal(): void {
const uc = this.updatedCourse;
if (uc.index >= 0 && uc.index < this.content.length) {
this.content[uc.index] = new Course(
uc.courseName,
uc.classroom,
uc.remark,
uc.index
);
}
}
// 构建一行课程(5个时间段)
@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() {
// 上午课程(1-4节)
ForEach([0,1,2,3], (item: number)=> {
this.hingeBody(item)
})
// 午休
GridItem() {
Text('午休');
}
.GridItemRestFn()
// 下午课程(5-8节)
ForEach([4,5,6,7], (index: number)=> {
this.hingeBody(index)
})
// 晚休
GridItem() {
Text('晚休');
}
.GridItemRestFn()
// 晚上课程(9-11节)
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()
} else {
Text(this.name)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.alignSelf(ItemAlign.Center)
.onClick(() => {this.showDialog = true;})
}
}
.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)
}
}
.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(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()
}
.width('100%')
.height('100%')
.columnsTemplate('13fr 18fr 18fr 18fr 18fr 18fr')
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.Spring)
}
.height('100%')
.width('100%')
// 课程详情弹窗
if (this.showDetails) {
Details()
}
}
.height('100%')
.width('100%')
}
}
4.2.2 课程单元格组件
在主页面中添加课程单元格组件:
@Component
struct GridItemCase {
@State isSelected: boolean = false;
@Prop course: Course = new Course('','','',0);
@State isCourse: boolean = false;
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)
.fontSize(11)
.fontColor("#d0d0d0")
Text(this.course.remark)
.fontSize(11)
.fontColor("#d0d0d0")
} 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
})
}
}
4.2.3 侧边栏组件(时间轴)
@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")
.fontSize(11)
.fontColor("#d0d0d0")
Text("08:45")
.fontSize(11)
.fontColor("#d0d0d0")
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.height(90)
}
.border({
width: 1,
color: "#F2F2F4",
style: BorderStyle.Solid
})
}
}
4.2.4 课程详情组件
@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(this.selectedCourse.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 })
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%')
}
}
.width('90%')
.margin({ top: 15 })
.padding(20)
.justifyContent(FlexAlign.Start)
.backgroundColor(Color.White)
.borderRadius(20)
}
.width('100%')
.padding(12)
.borderRadius({
topLeft: 50,
topRight: 50,
bottomLeft: 20,
bottomRight: 20
})
.backgroundColor('#f7f7f7')
.justifyContent(FlexAlign.Center)
.position({ bottom: 0 })
}
}
4.2.5 扩展方法
在文件末尾添加扩展方法:
// 扩展GridItem,用于午休/晚休样式
@Extend(GridItem)
function GridItemRestFn() {
.width('100%')
.backgroundColor("#F2F2F2")
.columnStart(0)
.columnEnd(5)
}
// 扩展Text,用于小字样式
@Extend(Text)
function TextFn() {
.fontSize(11)
.fontColor("#d0d0d0")
}
// 课程表名称编辑对话框
@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)
})
}
}
}
let newName: string = '';
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.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 课程数据丢失
问题:应用重启后课程消失
解决:当前使用内存存储,需要添加持久化存储(后续可扩展)
八、学习资源
更多推荐



所有评论(0)