// pages/Index.ets --- 课程表 App
import relationalStore from '@ohos.data.relationalStore';
import notification from '@ohos.notification';
const TIME_SLOTS = [
'08:00-08:45', '08:55-09:40', '09:50-10:35', '10:45-11:30',
'11:40-12:25', '14:00-14:45', '14:55-15:40', '15:50-16:35',
'16:45-17:30', '18:30-19:15', '19:25-20:10', '20:20-21:05'
];
const COLORS = ['#FF3B30','#FF9500','#FFCC00','#34C759','#007AFF','#5856D6','#AF52DE'];
@Entry
@Component
struct TimetableApp {
@State courses: Course[] = [];
@State currentWeek: number = 1;
@State totalWeeks: number = 20;
@State showAddDialog: boolean = false;
@State editName: string = '';
@State editTeacher: string = '';
@State editRoom: string = '';
@State editDay: number = 1;
@State editStart: number = 1;
@State editEnd: number = 2;
@State editColor: string = '#007AFF';
@State editWeekType: 'all' | 'odd' | 'even' = 'all';
@State selectedCourse: Course | null = null;
private store!: relationalStore.RdbStore;
// 星期标题
private weekDays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
aboutToAppear() {
this.initDB();
}
async initDB() {
const config = { name: 'timetable.db', securityLevel: relationalStore.SecurityLevel.S1 };
this.store = await relationalStore.getRdbStore(getContext(this), config);
await this.store.executeSql(
`CREATE TABLE IF NOT EXISTS courses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT, teacher TEXT, classroom TEXT,
dayOfWeek INTEGER, startSlot INTEGER, endSlot INTEGER,
weekStart INTEGER, weekEnd INTEGER,
weekType TEXT DEFAULT 'all', color TEXT DEFAULT '#007AFF'
)`
);
await this.loadCourses();
}
async loadCourses() {
const p = new relationalStore.RdbPredicates('courses');
p.orderByAsc('dayOfWeek').orderByAsc('startSlot');
const r = await this.store.query(p, ['id','name','teacher','classroom','dayOfWeek','startSlot','endSlot','weekStart','weekEnd','weekType','color']);
const list: Course[] = [];
while (r.goToNextRow()) {
list.push({
id: r.getLong(r.getColumnIndex('id')),
name: r.getString(r.getColumnIndex('name')),
teacher: r.getString(r.getColumnIndex('teacher') || ''),
classroom: r.getString(r.getColumnIndex('classroom') || ''),
dayOfWeek: r.getLong(r.getColumnIndex('dayOfWeek')),
startSlot: r.getLong(r.getColumnIndex('startSlot')),
endSlot: r.getLong(r.getColumnIndex('endSlot')),
weekStart: r.getLong(r.getColumnIndex('weekStart')),
weekEnd: r.getLong(r.getColumnIndex('weekEnd')),
weekType: r.getString(r.getColumnIndex('weekType')) as 'all' | 'odd' | 'even',
color: r.getString(r.getColumnIndex('color'))
});
}
this.courses = list;
r.close();
}
async addCourse() {
if (!this.editName.trim()) return;
await this.store.insert('courses', {
name: this.editName, teacher: this.editTeacher,
classroom: this.editRoom, dayOfWeek: this.editDay,
startSlot: this.editStart, endSlot: this.editEnd,
weekStart: 1, weekEnd: this.totalWeeks,
weekType: this.editWeekType, color: this.editColor
});
await this.loadCourses();
this.showAddDialog = false;
this.editName = '';
}
async deleteCourse(id: number) {
const p = new relationalStore.RdbPredicates('courses');
p.equalTo('id', id);
await this.store.delete(p);
await this.loadCourses();
}
// ======== 判断当前周是否上课 ========
isCourseActive(course: Course): boolean {
if (this.currentWeek < course.weekStart || this.currentWeek > course.weekEnd) return false;
if (course.weekType === 'odd' && this.currentWeek % 2 === 0) return false;
if (course.weekType === 'even' && this.currentWeek % 2 === 1) return false;
return true;
}
// ======== 获取某天某节次的课程 ========
getCourse(day: number, slot: number): Course | undefined {
return this.courses.find(c =>
c.dayOfWeek === day &&
c.startSlot <= slot && c.endSlot >= slot &&
this.isCourseActive(c)
);
}
// ======== 计算课程占据的行数 ========
getCourseRowSpan(course: Course): number {
return course.endSlot - course.startSlot + 1;
}
// ======== 获取某天的所有课程(按节次排序) ========
getCoursesForDay(day: number): Course[] {
return this.courses.filter(c => c.dayOfWeek === day && this.isCourseActive(c))
.sort((a, b) => a.startSlot - b.startSlot);
}
// ======== 时间格式化 ========
formatCourseTime(course: Course): string {
return `${TIME_SLOTS[course.startSlot - 1]}~${TIME_SLOTS[course.endSlot - 1].split('-')[1]}`;
}
// ======== 获取周的奇偶 ========
getWeekLabel(): string {
return `第 ${this.currentWeek} 周${this.currentWeek % 2 === 1 ? ' (单周)' : ' (双周)'}`;
}
build() {
Column() {
// ---- 周次导航 ----
Row() {
Button('◀').fontSize(20).backgroundColor('transparent').fontColor('#007AFF')
.onClick(() => { if (this.currentWeek > 1) this.currentWeek--; })
Text(this.getWeekLabel()).fontSize(18).fontWeight(FontWeight.Bold).layoutWeight(1)
.textAlign(TextAlign.Center)
Button('▶').fontSize(20).backgroundColor('transparent').fontColor('#007AFF')
.onClick(() => { if (this.currentWeek < this.totalWeeks) this.currentWeek++; })
Button('➕').fontSize(18).backgroundColor('transparent').fontColor('#FF3B30')
.onClick(() => { this.showAddDialog = true; })
}.width('96%').padding({ top: 8 })
// ---- 星期标题行 ----
Grid() {
ForEach(this.weekDays, (day: string) => {
GridItem() {
Text(day).fontSize(12).fontColor('#666').fontWeight(FontWeight.Bold)
.textAlign(TextAlign.Center).width('100%')
}
})
}
.columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr')
.width('96%').height(24)
// ---- 课程网格 (节次×星期) ----
Scroll() {
Grid() {
ForEach([1,2,3,4,5,6,7,8,9,10,11,12], (slot: number) => {
ForEach([1,2,3,4,5,6,7], (day: number) => {
GridItem() {
const course = this.getCourse(day, slot);
if (course && course.startSlot === slot) {
// 课程卡片(只渲染起始节次)
Column() {
Text(course.name).fontSize(12).fontWeight(FontWeight.Bold)
.fontColor('#fff').maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis })
Text(`@${course.classroom || '未定'}`).fontSize(9).fontColor('rgba(255,255,255,0.8)')
.margin({ top: 2 })
}
.padding(4).width('100%').height('100%')
.backgroundColor(course.color)
.borderRadius(6)
.onClick(() => {
this.selectedCourse = course;
AlertDialog.show({
title: course.name,
message: `👨🏫 ${course.teacher}\n🏫 ${course.classroom}\n⏰ ${this.formatCourseTime(course)}`,
confirm: { value: '删除', fontColor: '#FF3B30', action: () => { this.deleteCourse(course.id); } }
});
})
} else if (!course) {
// 空格
Column().width('100%').height(55)
} else {
// 已被占据(跨节的课程)
Column().width('100%').height(55)
}
}
})
})
}
.columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr')
.rowsTemplate('repeat(12, 55px)')
.width('96%')
}
.layoutWeight(1).width('100%')
// ---- 底部统计 ----
Row() {
Text(`📚 本周 ${this.courses.filter(c => this.isCourseActive(c)).length} 节课`)
.fontSize(13).fontColor('#888')
}.padding(8)
}
.width('100%').height('100%').backgroundColor('#F8F9FA')
// 添加课程弹窗
.bindSheet(this.showAddDialog, this.AddSheet())
}
@Builder
AddSheet() {
Column() {
Text('添加课程').fontSize(20).fontWeight(FontWeight.Bold).margin({ bottom: 16 })
TextInput({ placeholder: '课程名称 *', text: this.editName })
.width('100%').height(40).backgroundColor('#F8F8F8').borderRadius(8).padding({ left: 12 })
TextInput({ placeholder: '授课老师', text: this.editTeacher })
.width('100%').height(40).backgroundColor('#F8F8F8').borderRadius(8).padding({ left: 12 }).margin({ top: 8 })
TextInput({ placeholder: '教室', text: this.editRoom })
.width('100%').height(40).backgroundColor('#F8F8F8').borderRadius(8).padding({ left: 12 }).margin({ top: 8 })
Row() {
Text('星期:').fontSize(14)
ForEach(['周一','周二','周三','周四','周五','周六','周日'], (day: string, i: number) => {
Button(day).fontSize(12).height(30)
.backgroundColor(this.editDay === i+1 ? '#007AFF' : '#F0F0F0')
.fontColor(this.editDay === i+1 ? '#fff' : '#333')
.onClick(() => { this.editDay = i+1; })
})
}.width('100%').gap(4).margin({ top: 8 })
Row() {
Text('节次:').fontSize(14)
Select([{ value: '1-2' }, { value: '3-4' }, { value: '5-6' }, { value: '7-8' }, { value: '9-10' }])
.selected(0)
.onSelect((_, val) => {
const parts = val.split('-');
this.editStart = parseInt(parts[0]);
this.editEnd = parseInt(parts[1]);
})
}.margin({ top: 8 })
Row() {
Button('取消').backgroundColor('#E5E5EA').fontColor('#333').borderRadius(8).width('45%')
.onClick(() => { this.showAddDialog = false; })
Button('保存').backgroundColor('#007AFF').fontColor('#fff').borderRadius(8).width('45%')
.onClick(() => { this.addCourse(); })
}.width('100%').margin({ top: 16 })
}.padding(24).width('100%')
}
}