在这里插入图片描述
在这里插入图片描述

实例:课程表管理(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);
      }
    });
  }
}

三、注册与运行

  1. main_pages.json 追加 "pages/samples/CoursePage"
  2. Index.ets 追加:
    Button('📅 15 课程表管理').fontSize(15).width('70%')
      .onClick(() => this.getUIContext().getRouter().pushUrl({ url: 'pages/samples/CoursePage' }))
    
  3. 构建验证 → 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 排除自身)的应用层软提示冲突处理、连堂的「相邻多记录」建模。这是「网格/排班类应用」的完整范式。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐