|
|
@@ -0,0 +1,189 @@
|
|
|
+/**
|
|
|
+ * 重庆理工大学课表导入脚本
|
|
|
+ * 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();
|
|
|
+})();
|