| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189 |
- /**
- * 重庆理工大学课表导入脚本
- * author: Dawn Drizzle
- */
- const API_BASE = 'https://timetable-cfc.cqut.edu.cn/api/courseSchedule';
- // 仅允许在课表站点内执行,避免跨站点误触发
- const checkLogin = () => window.location.hostname === 'timetable-cfc.cqut.edu.cn';
- // 统一的接口请求封装:POST + JSON + 携带 Cookie,并在失败时提示原因
- const baseFetch = async (path, body, description) => {
- const response = await fetch(`${API_BASE}/${path}`, {
- method: 'POST',
- credentials: 'include',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: body === undefined ? undefined : JSON.stringify(body),
- });
- if (!response.ok) {
- AndroidBridge.showToast(`获取${description}失败,请确认已登录后重试`);
- throw new Error(`获取${description}失败: ${response.status} ${response.statusText}`);
- }
- try {
- return await response.json();
- } catch (error) {
- AndroidBridge.showToast(`解析${description}失败,请确认当前页面登录状态`);
- throw new Error(`解析${description}失败: ${error.message}`);
- }
- };
- // 获取当前登录用户信息(包含 username、校区等)
- const getUserInfo = async () => await baseFetch('getUserInfo', {}, '用户信息');
- // 获取指定校区的节次时间表
- const getCampusTimeInfo = async (campusName) => await baseFetch('getCampusTimeInfo', { campusName }, '时间表');
- // 获取指定周课程事件列表;weekNum/yearTerm 为空时,接口返回当前学期/当前周信息
- const getWeekEvents = async (userID, weekNum, yearTerm, description) => await baseFetch(
- 'listWeekEvents',
- {
- userID: String(userID),
- weekNum,
- yearTerm,
- },
- description,
- );
- // 将接口的节次时间转换为统一结构,并按节次序号升序排序
- const parseTimeSlots = (timeSlots) => timeSlots
- .slice()
- .sort((left, right) => Number(left.sessionNum) - Number(right.sessionNum))
- .map((timeSlot) => ({
- number: Number(timeSlot.sessionNum) || 0,
- startTime: timeSlot.startTime ?? '',
- endTime: timeSlot.endTime ?? '',
- }));
- // 推算学期开始日期(YYYY-MM-DD):使用 weekDayList 第一条的月/日 + yearTerm 中的学年信息
- const parseSemesterStartDate = (yearTerm, weekDayList) => {
- const firstWeekDate = weekDayList?.[0]?.weekDate;
- if (!yearTerm || !firstWeekDate) {
- return null;
- }
- const [startYear, endYear, termPart] = String(yearTerm).split('-');
- const [month, day] = String(firstWeekDate).split('/').map(Number);
- if (!startYear || !endYear || !termPart || Number.isNaN(month) || Number.isNaN(day)) {
- return null;
- }
- const year = termPart === '1' ? Number(startYear) : Number(endYear);
- if (Number.isNaN(year)) {
- return null;
- }
- const date = new Date(Date.UTC(year, month - 1, day));
- if (Number.isNaN(date.getTime())) {
- return null;
- }
- return date.toISOString().split('T')[0];
- };
- // 将接口的 event 解析为课程结构(节次、星期、周次等)
- const parseCourse = (event) => {
- const sessionList = (event.sessionList ?? []).map(Number).filter((session) => !Number.isNaN(session));
- const startSection = Number(event.sessionStart) || sessionList[0] || 0;
- const endSection = sessionList[sessionList.length - 1] || startSection + (Number(event.sessionLast) || 1) - 1;
- return {
- name: event.eventName ?? '',
- teacher: event.memberName ?? '',
- position: event.address ?? '',
- day: Number(event.weekDay) || 0,
- startSection,
- endSection,
- weeks: (event.weekList ?? []).map(Number).filter((week) => !Number.isNaN(week)),
- };
- };
- // 合并完全相同(课程名/老师/地点/星期/节次范围一致)的课程,将周次去重合并
- const mergeCourses = (events) => {
- const mergedCourses = new Map();
- for (const event of events) {
- const course = parseCourse(event);
- const key = [
- course.name,
- course.teacher,
- course.position,
- course.day,
- course.startSection,
- course.endSection,
- ].join('||');
- if (!mergedCourses.has(key)) {
- mergedCourses.set(key, course);
- continue;
- }
- const existingCourse = mergedCourses.get(key);
- existingCourse.weeks = [...new Set([...existingCourse.weeks, ...course.weeks])].sort((left, right) => left - right);
- }
- return [...mergedCourses.values()];
- };
- // 将解析后的结构写入 App(配置、课程列表、节次时间表)
- const saveSchedule = (parsedSchedule) => Promise.all([
- window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(parsedSchedule.courseConfig)),
- window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(parsedSchedule.courses)),
- window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(parsedSchedule.timeSlots)),
- ]);
- // 主流程:校验页面 → 拉取用户/校区 → 拉取节次/当前学期信息 → 拉取全学期每周课程 → 合并保存
- (async () => {
- if (!checkLogin()) {
- AndroidBridge.showToast('请先打开重庆理工大学课表页面并登录');
- throw new Error('当前不在重庆理工大学课表页面');
- }
- const userInfo = await getUserInfo();
- const userID = userInfo?.username;
- const campusName = userInfo?.userCustomSetting?.campusName;
- if (!userID || !campusName) {
- AndroidBridge.showToast('未获取到完整的用户信息,请确认已登录');
- throw new Error('用户信息不完整');
- }
- const [timeSlotData, currentWeekData] = await Promise.all([
- getCampusTimeInfo(campusName),
- getWeekEvents(userID, null, null, '当前学期信息'),
- ]);
- const yearTerm = currentWeekData?.yearTerm;
- const weekList = Array.isArray(currentWeekData?.weekList) ? currentWeekData.weekList : [];
- const currentWeekNum = currentWeekData?.weekNum;
- const semesterStartDate = parseSemesterStartDate(yearTerm, currentWeekData?.weekDayList);
- if (!yearTerm || weekList.length === 0) {
- AndroidBridge.showToast('未获取到当前学期信息');
- throw new Error('当前学期信息不完整');
- }
- const weekResults = await Promise.all(
- weekList.map((weekNum) => String(weekNum) === String(currentWeekNum)
- ? currentWeekData
- : getWeekEvents(userID, String(weekNum), yearTerm, `第${weekNum}周课程`))
- );
- await saveSchedule({
- courseConfig: {
- semesterStartDate,
- semesterTotalWeeks: weekList.length,
- },
- timeSlots: parseTimeSlots(timeSlotData),
- courses: mergeCourses(weekResults.flatMap((result) => result?.eventList ?? [])),
- });
- AndroidBridge.notifyTaskCompletion();
- })();
|