课程表全量代码与效果:ArkTS 在 HarmonyOS 的数字课表


实例:课程表管理(Course)|收官文章
一、文件清单
| 文件 | 职责 | 行数 |
|---|---|---|
database/CourseDao.ets |
数据层:坐标查询、冲突检测、CRUD、14 门种子 | 约 220 行 |
pages/samples/CoursePage.ets |
UI 层:周切换 + 课表网格 + 当天列表 + 添加弹窗 | 约 240 行 |
resources/base/profile/main_pages.json |
路由注册:pages/samples/CoursePage |
追加一行 |
pages/Index.ets |
首页入口按钮 | 追加一个按钮 |
本篇文章完整展示可编译运行的 CoursePage 代码,最后描述运行效果。
二、CoursePage 完整代码
import { common } from '@kit.AbilityKit';
import { promptAction } from '@kit.ArkUI';
import { CourseDao, Course, SECTION_TIMES, COURSE_COLORS } from '../../database/CourseDao';
@Entry
@Component
struct CoursePage {
@State courses: Course[] = [];
@State currentDay: number = 1; // 1~7
@State formVisible: boolean = false;
@State fName: string = '';
@State fTeacher: string = '';
@State fLocation: string = '';
@State fWeekday: number = 1;
@State fSection: number = 1;
@State fColor: string = COURSE_COLORS[0];
private context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
private readonly days: string[] = ['一', '二', '三', '四', '五', '六', '日'];
aboutToAppear(): void {
this.refresh();
}
async refresh(): Promise<void> {
try {
await CourseDao.initSeedData(this.context);
this.courses = await CourseDao.queryAll(this.context);
} catch (e) {
promptAction.showToast({ message: `加载失败: ${e}` });
}
}
private dayCourses(day: number): Course[] {
return this.courses.filter((c: Course) => c.weekday === day);
}
async onSave(): Promise<void> {
if (!this.fName.trim()) {
promptAction.showToast({ message: '课程名必填' });
return;
}
const conflict = await CourseDao.checkConflict(this.context, this.fWeekday, this.fSection, 0);
if (conflict) {
promptAction.showToast({ message: '⚠️ 该时段已有课程,冲突!' });
return;
}
const course: Course = {
id: 0,
name: this.fName.trim(),
teacher: this.fTeacher.trim(),
location: this.fLocation.trim(),
weekday: this.fWeekday,
section: this.fSection,
color: this.fColor,
weeks: '1-16',
remark: '',
};
await CourseDao.insert(this.context, course);
this.formVisible = false;
this.fName = ''; this.fTeacher = ''; this.fLocation = '';
await this.refresh();
promptAction.showToast({ message: '✅ 课程已添加' });
}
deleteCourse(c: Course): void {
promptAction.showDialog({
title: '删除课程',
message: `删除「${c.name}」?`,
buttons: [
{ text: '取消', color: '#808080' },
{ text: '删除', color: '#EF4444' },
],
}).then((res: promptAction.ShowDialogSuccessResponse) => {
if (res.index === 1) {
CourseDao.delete(this.context, c.id).then(async () => {
await this.refresh();
promptAction.showToast({ message: '🗑 已删除' });
});
}
});
}
build() {
Column() {
// ===== 标题栏 =====
Row() {
Column() {
Text('📅 课程表管理').fontSize(22).fontWeight(FontWeight.Bold)
Text(`共 ${this.courses.length} 门课程`).fontSize(12).fontColor('#999999').margin({ top: 2 })
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
Text('+').fontSize(26).fontColor('#3B82F6').onClick(() => this.formVisible = true)
}.width('100%').padding({ left: 16, right: 16, top: 12 })
// ===== 周切换 =====
Row({ space: 4 }) {
ForEach(this.days, (d: string, idx: number) => {
Text(`周${d}`)
.fontSize(13).layoutWeight(1).textAlign(TextAlign.Center)
.padding({ top: 8, bottom: 8 })
.borderRadius(8)
.backgroundColor(this.currentDay === idx + 1 ? '#3B82F6' : '#FFFFFF')
.fontColor(this.currentDay === idx + 1 ? Color.White : '#4B5563')
.onClick(() => this.currentDay = idx + 1)
}, (d: string, idx: number) => `${idx}-${d}`)
}.width('94%').margin({ top: 10 })
// ===== 课表格子 =====
Column() {
Row() {
Text('节次').fontSize(12).fontColor('#9CA3AF').width(46).textAlign(TextAlign.Center)
Text(`周${this.days[this.currentDay - 1]}`).fontSize(12).fontColor('#9CA3AF').layoutWeight(1).textAlign(TextAlign.Center)
}.width('100%').padding({ bottom: 6 })
ForEach([1, 2, 3, 4, 5, 6, 7, 8], (section: number) => {
Row() {
Column() {
Text(`${section}`).fontSize(12).fontColor('#9CA3AF')
Text(SECTION_TIMES[section - 1].substring(0, 5)).fontSize(9).fontColor('#D1D5DB')
}.width(46)
Row({ space: 4 }) {
ForEach(this.courses.filter((c: Course) => c.weekday === this.currentDay && c.section === section), (c: Course) => {
Column() {
Text(c.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(Color.White).maxLines(1)
Text(`${c.teacher} · ${c.location}`).fontSize(10).fontColor('rgba(255,255,255,0.85)').maxLines(1)
}
.layoutWeight(1).padding(6).borderRadius(8).backgroundColor(c.color)
.alignItems(HorizontalAlign.Start)
.onClick(() => this.showCourseMenu(c))
}, (c: Course) => `${c.id}-${c.section}`)
if (this.courses.filter((c: Course) => c.weekday === this.currentDay && c.section === section).length === 0) {
Text('').layoutWeight(1).height(46).borderRadius(8).backgroundColor('#F8FAFC')
}
}
.layoutWeight(1).height(54)
}
.width('100%').padding({ top: 3, bottom: 3 })
}, (section: number) => `sec-${section}`)
}
.width('94%').padding(12).backgroundColor(Color.White).borderRadius(12).margin({ top: 10 })
// ===== 当天课程列表 =====
Column() {
Text(`周${this.days[this.currentDay - 1]} · 共 ${this.dayCourses(this.currentDay).length} 节课`)
.fontSize(15).fontWeight(FontWeight.Bold)
ForEach(this.dayCourses(this.currentDay), (c: Course) => {
Row({ space: 10 }) {
Text(`${c.section}`).fontSize(14).fontWeight(FontWeight.Bold)
.width(30).height(30).borderRadius(15).backgroundColor(c.color)
.fontColor(Color.White).textAlign(TextAlign.Center)
Column({ space: 2 }) {
Text(c.name).fontSize(15).fontWeight(FontWeight.Medium)
Text(`${SECTION_TIMES[c.section - 1]} · ${c.teacher} · ${c.location}`).fontSize(11).fontColor('#999999')
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
}
.width('100%').padding(12).backgroundColor(Color.White).borderRadius(10).margin({ top: 8 })
}, (c: Course) => `d-${c.id}`)
}
.width('94%').padding(16).margin({ top: 12, bottom: 24 })
.alignItems(HorizontalAlign.Start)
// ===== 添加课程弹窗 =====
if (this.formVisible) {
Column() {
Text('📚 添加课程').fontSize(18).fontWeight(FontWeight.Bold)
TextInput({ placeholder: '课程名 *', text: this.fName }).margin({ top: 10 }).onChange((v: string) => this.fName = v)
TextInput({ placeholder: '老师', text: this.fTeacher }).margin({ top: 8 }).onChange((v: string) => this.fTeacher = v)
TextInput({ placeholder: '教室', text: this.fLocation }).margin({ top: 8 }).onChange((v: string) => this.fLocation = v)
Row() {
Text('星期').fontSize(13).fontColor('#6B7280')
Scroll() {
Row({ space: 6 }) {
ForEach(this.days, (d: string, idx: number) => {
Text(`周${d}`)
.fontSize(12).padding({ left: 10, right: 10, top: 5, bottom: 5 })
.borderRadius(12)
.backgroundColor(this.fWeekday === idx + 1 ? '#3B82F6' : '#EEF2F7')
.fontColor(this.fWeekday === idx + 1 ? Color.White : '#4B5563')
.onClick(() => this.fWeekday = idx + 1)
}, (d: string, idx: number) => `w-${idx}`)
}
}.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).layoutWeight(1)
}.width('100%').margin({ top: 10 })
Row() {
Text('节次').fontSize(13).fontColor('#6B7280')
Scroll() {
Row({ space: 6 }) {
ForEach([1, 2, 3, 4, 5, 6, 7, 8], (s: number) => {
Text(`${s}`)
.fontSize(12).width(32).height(28).textAlign(TextAlign.Center)
.borderRadius(12)
.backgroundColor(this.fSection === s ? '#3B82F6' : '#EEF2F7')
.fontColor(this.fSection === s ? Color.White : '#4B5563')
.onClick(() => this.fSection = s)
}, (s: number) => `sec-${s}`)
}
}.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).layoutWeight(1)
}.width('100%').margin({ top: 8 })
Row({ space: 6 }) {
ForEach(COURSE_COLORS, (c: string) => {
Row().width(24).height(24).borderRadius(12).backgroundColor(c)
.border({ width: this.fColor === c ? 3 : 0, color: '#3B82F6' })
.onClick(() => this.fColor = c)
}, (c: string) => c)
}.margin({ top: 12 })
Row({ space: 8 }) {
Button('取消').layoutWeight(1).backgroundColor('#EEF2F7').fontColor('#555555')
.onClick(() => this.formVisible = false)
Button('保存(自动查冲突)').layoutWeight(1).backgroundColor('#3B82F6').fontSize(13)
.onClick(() => this.onSave())
}.margin({ top: 16 })
}
.padding(20).borderRadius(16).backgroundColor(Color.White).width('90%')
.position({ x: '5%', y: '8%' })
}
}
.width('100%').height('100%').backgroundColor('#F8FAFC')
}
showCourseMenu(c: Course): void {
promptAction.showActionMenu({
title: c.name,
buttons: [
{ text: '查看详情', color: '#3B82F6' },
{ text: '删除', color: '#EF4444' },
],
}).then((res: promptAction.ActionMenuSuccessResponse) => {
if (res.index === 0) {
promptAction.showDialog({
title: c.name,
message: `${c.teacher} · ${c.location}\n周${this.days[c.weekday - 1]} 第${c.section}节\n${SECTION_TIMES[c.section - 1]}\n教学周:${c.weeks}`,
buttons: [{ text: '知道了', color: '#3B82F6' }],
});
}
if (res.index === 1) {
this.deleteCourse(c);
}
});
}
}
三、注册与运行
main_pages.json追加"pages/samples/CoursePage";Index.ets追加:Button('📅 15 课程表管理').fontSize(15).width('70%') .onClick(() => this.getUIContext().getRouter().pushUrl({ url: 'pages/samples/CoursePage' }))- 构建验证 →
BUILD SUCCESSFUL。
四、运行效果描述
进入「📅 15 课程表管理」:
第一屏:标题栏「📅 课程表管理 · 共 14 门课程」+ 右上角蓝色「+」;周切换条「周一」高亮;课表格子——左侧节次轴(第 1~8 节 + 时间),右侧当天格子:周一第 1-2 节蓝色「高等数学 王教授 · A101」连堂块、第 3-4 节绿色「大学英语」连堂块,其余浅灰空位;下方「周一 · 共 4 节课」列表 4 行(节次圆标 + 课程名 + 时间老师教室)。
交互一(周切换):点「周二」→ 格子变紫色数据结构(1-2 节)+ 橙色体育(5 节)→ 点「周日」→ 8 个空位格 → 点「周一」切回。
交互二(课卡详情):点蓝色高数块 → 动作菜单「查看详情 / 删除」→ 点详情 → 弹窗「王教授 · A101 / 周一 第1节 / 08:00-08:45 / 教学周:1-16」→ 知道了关闭。
交互三(添加课程 + 冲突):点「+」→ 填「人工智能 / 郑教授 / C501」→ 星期选周一、节次选第 1 节 → 保存 → Toast「⚠️ 该时段已有课程,冲突!」(高数占着)→ 改节次第 6 节 → 保存 → Toast「✅ 课程已添加」→ 课表出现新色块。
交互四(删除课程):点新增的「人工智能」色块 → 菜单删除 → 确认框 → 删除 → 格子变空、总数 15→14。
五、代码质量要点回顾
| 关注点 | 本实例做法 |
|---|---|
| 二维坐标 | weekday + section 字段 |
| 冲突检测 | checkConflict + excludeId |
| 联合索引 | idx_course_grid |
| 连堂建模 | 相邻节次多条记录 |
| 空位占位 | 浅灰空格 |
| 动作菜单 | showActionMenu |
| 色板选择 | 10 色圆点 + 描边选中 |
六、文章小结
实例 15「课程表管理」收官。五篇文章覆盖:二维坐标建模(15-1)→ 课表网格 UI(15-2)→ 冲突检测与坐标查询(15-3)→ 14 门课程种子(15-4)→ 全量代码(15-5)。核心技术是 weekday × section 二维坐标建模、联合索引加速坐标查询、checkConflict(同坐标查重 + excludeId 排除自身)的应用层软提示冲突处理、连堂的「相邻多记录」建模。这是「网格/排班类应用」的完整范式。
更多推荐



所有评论(0)