|
|
@@ -0,0 +1,379 @@
|
|
|
+// 武汉理工大学(whut.edu.cn)拾光课程表适配脚本
|
|
|
+// 非该大学开发者适配,开发者无法及时发现问题
|
|
|
+// 出现问题请提联系开发者或者提交pr更改,这更加快速
|
|
|
+
|
|
|
+// 核心工具函数:数据验证
|
|
|
+function validateYearInput(input) {
|
|
|
+ if (/^[0-9]{4}$/.test(input) && parseInt(input) > 2000) {
|
|
|
+ return false;
|
|
|
+ } else {
|
|
|
+ return "请输入有效的四位数字学年(例如:2025)!";
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 辅助函数:解析周次字符串 "111000..." 为数字数组 [1, 2, 3]
|
|
|
+ */
|
|
|
+function parseWeeksFromSkzc(skzc) {
|
|
|
+ const weeks = [];
|
|
|
+ const rawSkzc = skzc || '';
|
|
|
+ for (let i = 0; i < rawSkzc.length; i++) {
|
|
|
+ if (rawSkzc[i] === '1') {
|
|
|
+ weeks.push(Number(i + 1));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return weeks;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 全局课程合并清洗逻辑
|
|
|
+ * 解决单双周割裂、冗余重复、实验课节次部分重叠等问题
|
|
|
+ */
|
|
|
+function mergeContinuousLessons(lessons) {
|
|
|
+ if (!lessons || lessons.length === 0) return [];
|
|
|
+
|
|
|
+ const groups = {};
|
|
|
+ lessons.forEach(l => {
|
|
|
+ const key = `${l.name}|${l.teacher}|${l.position}|${l.day}`;
|
|
|
+ if (!groups[key]) {
|
|
|
+ groups[key] = {
|
|
|
+ name: l.name,
|
|
|
+ teacher: l.teacher,
|
|
|
+ position: l.position,
|
|
|
+ day: l.day,
|
|
|
+ // 大学的周次一般为 1~30 周,这里开辟 55 长度的矩阵足够,Set 用于节次去重
|
|
|
+ weeksMatrix: Array.from({ length: 55 }, () => new Set())
|
|
|
+ };
|
|
|
+ }
|
|
|
+ if (l.weeks && Array.isArray(l.weeks)) {
|
|
|
+ l.weeks.forEach(w => {
|
|
|
+ const weekNum = parseInt(w);
|
|
|
+ // 严格限制合法周次,防止越界
|
|
|
+ if (weekNum >= 1 && weekNum < 55) {
|
|
|
+ for (let s = l.startSection; s <= l.endSection; s++) {
|
|
|
+ groups[key].weeksMatrix[weekNum].add(s);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ const merged = [];
|
|
|
+
|
|
|
+ for (const key in groups) {
|
|
|
+ const group = groups[key];
|
|
|
+ const matrix = group.weeksMatrix;
|
|
|
+ const blockMap = {};
|
|
|
+
|
|
|
+ // 遍历矩阵(从1开始,对齐自然周)
|
|
|
+ for (let w = 1; w < matrix.length; w++) {
|
|
|
+ const sections = Array.from(matrix[w]).sort((a, b) => a - b);
|
|
|
+ if (sections.length === 0) continue;
|
|
|
+
|
|
|
+ let start = sections[0];
|
|
|
+ let prev = sections[0];
|
|
|
+
|
|
|
+ for (let i = 1; i < sections.length; i++) {
|
|
|
+ const curr = sections[i];
|
|
|
+ if (curr === prev + 1) {
|
|
|
+ prev = curr;
|
|
|
+ } else {
|
|
|
+ const blockKey = `${start}-${prev}`;
|
|
|
+ if (!blockMap[blockKey]) blockMap[blockKey] = [];
|
|
|
+ blockMap[blockKey].push(w);
|
|
|
+
|
|
|
+ start = curr;
|
|
|
+ prev = curr;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const blockKey = `${start}-${prev}`;
|
|
|
+ if (!blockMap[blockKey]) blockMap[blockKey] = [];
|
|
|
+ blockMap[blockKey].push(w);
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const blockKey in blockMap) {
|
|
|
+ const [startSec, endSec] = blockKey.split('-').map(Number);
|
|
|
+ merged.push({
|
|
|
+ name: group.name,
|
|
|
+ teacher: group.teacher,
|
|
|
+ position: group.position,
|
|
|
+ day: group.day,
|
|
|
+ startSection: startSec,
|
|
|
+ endSection: endSec,
|
|
|
+ weeks: blockMap[blockKey]
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 格式化输出排序
|
|
|
+ merged.sort((a, b) => {
|
|
|
+ if (a.day !== b.day) return a.day - b.day;
|
|
|
+ if (a.startSection !== b.startSection) return a.startSection - b.startSection;
|
|
|
+ return a.name.localeCompare(b.name);
|
|
|
+ });
|
|
|
+
|
|
|
+ return merged;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 将教务系统的课程数据转换成 CourseJsonModel 结构(应用物理节次转换)
|
|
|
+ */
|
|
|
+function parseSingleCourse(rawCourse, sectionMap) {
|
|
|
+ const courseName = rawCourse.KCM;
|
|
|
+ const teacherName = rawCourse.SKJS ? rawCourse.SKJS.split('/')[0] : '';
|
|
|
+ const position = rawCourse.JASMC;
|
|
|
+ const day = rawCourse.SKXQ;
|
|
|
+ const startSection = sectionMap[rawCourse.KSJC];
|
|
|
+ const endSection = sectionMap[rawCourse.JSJC];
|
|
|
+ const weeks = parseWeeksFromSkzc(rawCourse.SKZC);
|
|
|
+
|
|
|
+ if (!courseName || !day || !startSection || !endSection || weeks.length === 0) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ "name": courseName,
|
|
|
+ "teacher": teacherName,
|
|
|
+ "position": position || '待定',
|
|
|
+ "day": parseInt(day),
|
|
|
+ "startSection": startSection,
|
|
|
+ "endSection": endSection,
|
|
|
+ "weeks": weeks
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+async function promptUserToStart() {
|
|
|
+ const confirmed = await window.AndroidBridgePromise.showAlert(
|
|
|
+ "武汉理工大学课表导入",
|
|
|
+ "本流程将通过教务系统接口获取您的个人课表与开学时间。\n重要提示:\n导入前请确保您已在浏览器中成功登录教务系统,且未关闭登录窗口。",
|
|
|
+ "好的,开始导入"
|
|
|
+ );
|
|
|
+ if (!confirmed) {
|
|
|
+ AndroidBridge.showToast("用户取消了导入。");
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+async function getAcademicYear() {
|
|
|
+ const currentYear = new Date().getFullYear();
|
|
|
+ const yearSelection = await window.AndroidBridgePromise.showPrompt(
|
|
|
+ "选择学年",
|
|
|
+ "请输入要导入课程的起始学年(例如 2025-2026 应输入2025):",
|
|
|
+ String(currentYear),
|
|
|
+ "validateYearInput"
|
|
|
+ );
|
|
|
+ return yearSelection;
|
|
|
+}
|
|
|
+
|
|
|
+async function selectSemester() {
|
|
|
+ const semesters = ["1 (秋季学期/上学期)", "2 (春季学期/下学期)"];
|
|
|
+ const semesterIndex = await window.AndroidBridgePromise.showSingleSelection(
|
|
|
+ "选择学期",
|
|
|
+ JSON.stringify(semesters),
|
|
|
+ 0
|
|
|
+ );
|
|
|
+ if (semesterIndex === null) return null;
|
|
|
+ return String(semesterIndex + 1);
|
|
|
+}
|
|
|
+
|
|
|
+async function fetchSectionMapping(commonHeaders) {
|
|
|
+ const sectionMapUrl = "https://jwxt.whut.edu.cn/jwapp/sys/kcbcxby/modules/dzkz/jcjcx.do";
|
|
|
+ try {
|
|
|
+ const response = await fetch(sectionMapUrl, {
|
|
|
+ "headers": commonHeaders,
|
|
|
+ "method": "POST",
|
|
|
+ "credentials": "include"
|
|
|
+ });
|
|
|
+ const resData = JSON.parse(await response.text());
|
|
|
+ const rows = resData?.datas?.jcjcx?.rows || [];
|
|
|
+
|
|
|
+ const dictionary = {};
|
|
|
+ let physicalIndex = 1;
|
|
|
+
|
|
|
+ for (const row of rows) {
|
|
|
+ if (row.MC && row.MC.includes("节")) {
|
|
|
+ dictionary[row.DM] = physicalIndex;
|
|
|
+ physicalIndex++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return dictionary;
|
|
|
+ } catch (e) {
|
|
|
+ console.error("Fetch Section Mapping Error, use fallback:", e);
|
|
|
+ return { "1":1, "2":2, "3":3, "4":4, "5":5, "8":6, "9":7, "10":8, "11":9, "12":10, "14":11, "15":12, "16":13 };
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 数据获取和解析部分
|
|
|
+
|
|
|
+async function fetchAndParseCourses(academicYear, semesterCode) {
|
|
|
+ const commonHeaders = {
|
|
|
+ "accept": "application/json, text/javascript, */*; q=0.01",
|
|
|
+ "accept-language": "zh-CN,zh;q=0.9",
|
|
|
+ "x-requested-with": "XMLHttpRequest"
|
|
|
+ };
|
|
|
+
|
|
|
+ // 获取物理节次转换映射字典
|
|
|
+ const sectionMap = await fetchSectionMapping(commonHeaders);
|
|
|
+
|
|
|
+ // 获取当前登录用户的学号 (XH)
|
|
|
+ let studentId = "";
|
|
|
+ try {
|
|
|
+ const userUrl = "https://jwxt.whut.edu.cn/jwapp/sys/homeapp/api/home/currentUser.do";
|
|
|
+ const userResponse = await fetch(userUrl, { "method": "GET", "credentials": "include" });
|
|
|
+ const userData = JSON.parse(await userResponse.text());
|
|
|
+ studentId = userData?.datas?.userId;
|
|
|
+ if (!studentId) throw new Error("无法读取 userId");
|
|
|
+ } catch (e) {
|
|
|
+ AndroidBridge.showToast("获取用户信息失败,请确认是否登录教务系统!");
|
|
|
+ console.error("Fetch User Error:", e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取该学期的课表配置
|
|
|
+ let courseConfig = { semesterTotalWeeks: 20 };
|
|
|
+ try {
|
|
|
+ const configUrl = "https://jwxt.whut.edu.cn/jwapp/sys/kcbcxby/modules/bjkcb/cxjcs.do";
|
|
|
+ const configBody = `XN=${academicYear}-${parseInt(academicYear) + 1}&XQ=${semesterCode}`;
|
|
|
+ const configResponse = await fetch(configUrl, {
|
|
|
+ "headers": { ...commonHeaders, "content-type": "application/x-www-form-urlencoded; charset=UTF-8" },
|
|
|
+ "body": configBody,
|
|
|
+ "method": "POST",
|
|
|
+ "credentials": "include"
|
|
|
+ });
|
|
|
+ const configData = JSON.parse(await configResponse.text());
|
|
|
+ const configRow = configData?.datas?.cxjcs?.rows?.[0];
|
|
|
+
|
|
|
+ if (configRow) {
|
|
|
+ if (configRow.XQKSRQ) {
|
|
|
+ courseConfig.semesterStartDate = configRow.XQKSRQ.split(" ")[0];
|
|
|
+ }
|
|
|
+ if (configRow.ZZC) {
|
|
|
+ courseConfig.semesterTotalWeeks = parseInt(configRow.ZZC);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error("Fetch Config Error, will use default config:", e);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取个人课表数据
|
|
|
+ const XNXQDM = `${academicYear}-${parseInt(academicYear) + 1}-${semesterCode}`;
|
|
|
+ const courseUrl = "https://jwxt.whut.edu.cn/jwapp/sys/kcbcxby/modules/xskcb/cxxskcb.do";
|
|
|
+ const courseBody = `XNXQDM=${XNXQDM}&XH=${studentId}`;
|
|
|
+ let rawCourseData;
|
|
|
+ try {
|
|
|
+ const response = await fetch(courseUrl, {
|
|
|
+ "headers": { ...commonHeaders, "content-type": "application/x-www-form-urlencoded; charset=UTF-8" },
|
|
|
+ "body": courseBody,
|
|
|
+ "method": "POST",
|
|
|
+ "credentials": "include"
|
|
|
+ });
|
|
|
+ rawCourseData = JSON.parse(await response.text());
|
|
|
+ } catch (e) {
|
|
|
+ AndroidBridge.showToast("请求课表 API 失败,请检查网络和登录状态。");
|
|
|
+ console.error("Fetch Course Error:", e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ const rawCourses = rawCourseData?.datas?.cxxskcb?.rows || [];
|
|
|
+ if (rawCourses.length === 0) {
|
|
|
+ AndroidBridge.showToast("该学期未查询到您的课程数据。");
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ const initialCourses = rawCourses.map(c => parseSingleCourse(c, sectionMap)).filter(c => c !== null);
|
|
|
+ const finalParsedCourses = mergeContinuousLessons(initialCourses);
|
|
|
+
|
|
|
+ return {
|
|
|
+ courses: finalParsedCourses,
|
|
|
+ config: courseConfig
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+async function saveCourses(parsedCourses) {
|
|
|
+ if (parsedCourses.length === 0) {
|
|
|
+ AndroidBridge.showToast("没有有效的课程数据可供保存。");
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(parsedCourses));
|
|
|
+ AndroidBridge.showToast(`成功导入 ${parsedCourses.length} 个规范后的课程块!`);
|
|
|
+ return true;
|
|
|
+ } catch (error) {
|
|
|
+ AndroidBridge.showToast(`保存课程数据失败: ${error.message}`);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 导入预设时间段数据
|
|
|
+ */
|
|
|
+async function importPresetTimeSlots() {
|
|
|
+ const presetTimeSlots = [
|
|
|
+ { "number": 1, "startTime": "08:00", "endTime": "08:45" },
|
|
|
+ { "number": 2, "startTime": "08:50", "endTime": "09:35" },
|
|
|
+ { "number": 3, "startTime": "09:55", "endTime": "10:40" },
|
|
|
+ { "number": 4, "startTime": "10:45", "endTime": "11:30" },
|
|
|
+ { "number": 5, "startTime": "11:35", "endTime": "12:20" },
|
|
|
+ { "number": 6, "startTime": "14:00", "endTime": "14:45" },
|
|
|
+ { "number": 7, "startTime": "14:50", "endTime": "15:35" },
|
|
|
+ { "number": 8, "startTime": "15:40", "endTime": "16:25" },
|
|
|
+ { "number": 9, "startTime": "16:45", "endTime": "17:30" },
|
|
|
+ { "number": 10, "startTime": "17:35", "endTime": "18:20" },
|
|
|
+ { "number": 11, "startTime": "19:00", "endTime": "19:45" },
|
|
|
+ { "number": 12, "startTime": "19:50", "endTime": "20:35" },
|
|
|
+ { "number": 13, "startTime": "20:40", "endTime": "21:25" }
|
|
|
+ ];
|
|
|
+
|
|
|
+ try {
|
|
|
+ await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(presetTimeSlots));
|
|
|
+ return true;
|
|
|
+ } catch (error) {
|
|
|
+ console.error("导入时间段失败: " + error.message);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function saveConfig(configData) {
|
|
|
+ try {
|
|
|
+ await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(configData));
|
|
|
+ return true;
|
|
|
+ } catch (error) {
|
|
|
+ console.error("保存配置失败: " + error.message);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function runImportFlow() {
|
|
|
+ AndroidBridge.showToast("武汉理工大学课程导入流程启动...");
|
|
|
+
|
|
|
+ const alertConfirmed = await promptUserToStart();
|
|
|
+ if (!alertConfirmed) return;
|
|
|
+
|
|
|
+ const academicYear = await getAcademicYear();
|
|
|
+ if (academicYear === null) {
|
|
|
+ AndroidBridge.showToast("导入已取消。");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const semesterCode = await selectSemester();
|
|
|
+ if (semesterCode === null) {
|
|
|
+ AndroidBridge.showToast("导入已取消。");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const courseData = await fetchAndParseCourses(academicYear, semesterCode);
|
|
|
+ if (courseData === null) return;
|
|
|
+
|
|
|
+ await importPresetTimeSlots();
|
|
|
+
|
|
|
+ const configSaveResult = await saveConfig(courseData.config);
|
|
|
+ if (!configSaveResult) return;
|
|
|
+
|
|
|
+ const saveResult = await saveCourses(courseData.courses);
|
|
|
+ if (!saveResult) return;
|
|
|
+
|
|
|
+ AndroidBridge.notifyTaskCompletion();
|
|
|
+}
|
|
|
+
|
|
|
+runImportFlow();
|