修改课表模块
This commit is contained in:
74
front/src/lib/schedule-table.test.ts
Normal file
74
front/src/lib/schedule-table.test.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
|
||||
import type { CourseResponse } from './types';
|
||||
import { buildScheduleTable, getScheduleCellHeight, getScheduleDividerOffsets } from './schedule-table';
|
||||
|
||||
test('buildScheduleTable creates 12 sections with empty slots preserved', () => {
|
||||
const schedule: CourseResponse[] = [
|
||||
{
|
||||
courseName: 'Advanced Java',
|
||||
teacher: 'Li',
|
||||
classroom: 'A101',
|
||||
dayOfWeek: 1,
|
||||
startTime: 1,
|
||||
endTime: 2,
|
||||
},
|
||||
{
|
||||
courseName: 'Networks',
|
||||
teacher: 'Wang',
|
||||
classroom: 'B202',
|
||||
dayOfWeek: 3,
|
||||
startTime: 5,
|
||||
endTime: 6,
|
||||
},
|
||||
];
|
||||
|
||||
const table = buildScheduleTable(schedule);
|
||||
|
||||
assert.equal(table.length, 12);
|
||||
assert.equal(table[0].slots.length, 7);
|
||||
assert.equal(table[0].section, 1);
|
||||
assert.equal(table[11].section, 12);
|
||||
assert.equal(table[0].period, 'morning');
|
||||
assert.equal(table[4].period, 'noon');
|
||||
assert.equal(table[5].period, 'afternoon');
|
||||
assert.equal(table[9].period, 'evening');
|
||||
assert.equal(table[0].slots[0]?.course?.courseName, 'Advanced Java');
|
||||
assert.equal(table[1].slots[0]?.type, 'covered');
|
||||
assert.equal(table[2].slots[0]?.type, 'empty');
|
||||
assert.equal(table[4].slots[2]?.course?.courseName, 'Networks');
|
||||
assert.equal(table[5].slots[2]?.type, 'covered');
|
||||
assert.equal(table[8].slots[4]?.type, 'empty');
|
||||
assert.equal(table[8].slots[6]?.type, 'empty');
|
||||
});
|
||||
|
||||
test('buildScheduleTable clamps invalid section ranges safely', () => {
|
||||
const schedule: CourseResponse[] = [
|
||||
{
|
||||
courseName: 'Night Studio',
|
||||
teacher: 'Xu',
|
||||
classroom: 'C303',
|
||||
dayOfWeek: 5,
|
||||
startTime: 11,
|
||||
endTime: 14,
|
||||
},
|
||||
];
|
||||
|
||||
const table = buildScheduleTable(schedule);
|
||||
|
||||
assert.equal(table[10].slots[4]?.rowSpan, 2);
|
||||
assert.equal(table[11].slots[4]?.type, 'covered');
|
||||
});
|
||||
|
||||
test('getScheduleCellHeight returns merged visual height for rowspan cells', () => {
|
||||
assert.equal(getScheduleCellHeight(1), 96);
|
||||
assert.equal(getScheduleCellHeight(2), 200);
|
||||
assert.equal(getScheduleCellHeight(4), 408);
|
||||
});
|
||||
|
||||
test('getScheduleDividerOffsets returns internal section boundaries for merged cells', () => {
|
||||
assert.deepEqual(getScheduleDividerOffsets(1), []);
|
||||
assert.deepEqual(getScheduleDividerOffsets(2), [100]);
|
||||
assert.deepEqual(getScheduleDividerOffsets(4), [100, 204, 308]);
|
||||
});
|
||||
77
front/src/lib/schedule-table.ts
Normal file
77
front/src/lib/schedule-table.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import type { CourseResponse } from './types';
|
||||
|
||||
export interface ScheduleSlot {
|
||||
type: 'empty' | 'course' | 'covered';
|
||||
course?: CourseResponse;
|
||||
rowSpan?: number;
|
||||
}
|
||||
|
||||
export interface ScheduleRow {
|
||||
section: number;
|
||||
period: 'morning' | 'noon' | 'afternoon' | 'evening';
|
||||
slots: ScheduleSlot[];
|
||||
}
|
||||
|
||||
const SECTION_COUNT = 12;
|
||||
const WEEKDAY_COUNT = 7;
|
||||
const SECTION_CELL_HEIGHT = 96;
|
||||
const SECTION_CELL_GAP = 8;
|
||||
|
||||
function getPeriod(section: number): ScheduleRow['period'] {
|
||||
if (section <= 4) {
|
||||
return 'morning';
|
||||
}
|
||||
if (section === 5) {
|
||||
return 'noon';
|
||||
}
|
||||
if (section <= 8) {
|
||||
return 'afternoon';
|
||||
}
|
||||
return 'evening';
|
||||
}
|
||||
|
||||
export function buildScheduleTable(schedule: CourseResponse[]) {
|
||||
const rows: ScheduleRow[] = Array.from({ length: SECTION_COUNT }, (_, index) => ({
|
||||
section: index + 1,
|
||||
period: getPeriod(index + 1),
|
||||
slots: Array.from({ length: WEEKDAY_COUNT }, () => ({ type: 'empty' as const })),
|
||||
}));
|
||||
|
||||
for (const course of schedule) {
|
||||
const dayIndex = (course.dayOfWeek ?? 0) - 1;
|
||||
if (dayIndex < 0 || dayIndex >= WEEKDAY_COUNT) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const startSection = Math.max(1, Math.min(SECTION_COUNT, course.startTime ?? 1));
|
||||
const endSection = Math.max(startSection, Math.min(SECTION_COUNT, course.endTime ?? startSection));
|
||||
const rowSpan = endSection - startSection + 1;
|
||||
const startRowIndex = startSection - 1;
|
||||
|
||||
rows[startRowIndex].slots[dayIndex] = {
|
||||
type: 'course',
|
||||
course,
|
||||
rowSpan,
|
||||
};
|
||||
|
||||
for (let section = startSection + 1; section <= endSection; section += 1) {
|
||||
rows[section - 1].slots[dayIndex] = {
|
||||
type: 'covered',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
export function getScheduleCellHeight(rowSpan: number) {
|
||||
const safeRowSpan = Math.max(1, rowSpan);
|
||||
return safeRowSpan * SECTION_CELL_HEIGHT + (safeRowSpan - 1) * SECTION_CELL_GAP;
|
||||
}
|
||||
|
||||
export function getScheduleDividerOffsets(rowSpan: number) {
|
||||
const safeRowSpan = Math.max(1, rowSpan);
|
||||
return Array.from({ length: safeRowSpan - 1 }, (_, index) =>
|
||||
(index + 1) * SECTION_CELL_HEIGHT + index * SECTION_CELL_GAP + SECTION_CELL_GAP / 2,
|
||||
);
|
||||
}
|
||||
22
front/src/lib/school.ts
Normal file
22
front/src/lib/school.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { apiRequest } from './api';
|
||||
import { writeCachedValue } from './cache';
|
||||
import { getSchoolResultsCacheKey, writeStoredSchoolQuery } from './page-cache';
|
||||
import type { LatestSchoolDataResponse } from './types';
|
||||
|
||||
export async function fetchLatestSchoolData() {
|
||||
return apiRequest<LatestSchoolDataResponse | null>('/cqu/latest');
|
||||
}
|
||||
|
||||
export function cacheLatestSchoolData(latest: LatestSchoolDataResponse) {
|
||||
writeStoredSchoolQuery({
|
||||
studentId: latest.studentId,
|
||||
semester: latest.semester,
|
||||
});
|
||||
writeCachedValue(getSchoolResultsCacheKey(latest.studentId, latest.semester), {
|
||||
queried: true,
|
||||
studentId: latest.studentId,
|
||||
semester: latest.semester,
|
||||
schedule: latest.schedule,
|
||||
grades: latest.grades,
|
||||
});
|
||||
}
|
||||
@@ -46,3 +46,10 @@ export interface GradeResponse {
|
||||
grade: number | null;
|
||||
semester: string | null;
|
||||
}
|
||||
|
||||
export interface LatestSchoolDataResponse {
|
||||
studentId: string;
|
||||
semester: string;
|
||||
schedule: CourseResponse[];
|
||||
grades: GradeResponse[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user