whut_01.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. // 武汉理工大学(whut.edu.cn)拾光课程表适配脚本
  2. // 非该大学开发者适配,开发者无法及时发现问题
  3. // 出现问题请提联系开发者或者提交pr更改,这更加快速
  4. // 核心工具函数:数据验证
  5. function validateYearInput(input) {
  6. if (/^[0-9]{4}$/.test(input) && parseInt(input) > 2000) {
  7. return false;
  8. } else {
  9. return "请输入有效的四位数字学年(例如:2025)!";
  10. }
  11. }
  12. /**
  13. * 辅助函数:解析周次字符串 "111000..." 为数字数组 [1, 2, 3]
  14. */
  15. function parseWeeksFromSkzc(skzc) {
  16. const weeks = [];
  17. const rawSkzc = skzc || '';
  18. for (let i = 0; i < rawSkzc.length; i++) {
  19. if (rawSkzc[i] === '1') {
  20. weeks.push(Number(i + 1));
  21. }
  22. }
  23. return weeks;
  24. }
  25. /**
  26. * 全局课程合并清洗逻辑
  27. * 解决单双周割裂、冗余重复、实验课节次部分重叠等问题
  28. */
  29. function mergeContinuousLessons(lessons) {
  30. if (!lessons || lessons.length === 0) return [];
  31. const groups = {};
  32. lessons.forEach(l => {
  33. const key = `${l.name}|${l.teacher}|${l.position}|${l.day}`;
  34. if (!groups[key]) {
  35. groups[key] = {
  36. name: l.name,
  37. teacher: l.teacher,
  38. position: l.position,
  39. day: l.day,
  40. // 大学的周次一般为 1~30 周,这里开辟 55 长度的矩阵足够,Set 用于节次去重
  41. weeksMatrix: Array.from({ length: 55 }, () => new Set())
  42. };
  43. }
  44. if (l.weeks && Array.isArray(l.weeks)) {
  45. l.weeks.forEach(w => {
  46. const weekNum = parseInt(w);
  47. // 严格限制合法周次,防止越界
  48. if (weekNum >= 1 && weekNum < 55) {
  49. for (let s = l.startSection; s <= l.endSection; s++) {
  50. groups[key].weeksMatrix[weekNum].add(s);
  51. }
  52. }
  53. });
  54. }
  55. });
  56. const merged = [];
  57. for (const key in groups) {
  58. const group = groups[key];
  59. const matrix = group.weeksMatrix;
  60. const blockMap = {};
  61. // 遍历矩阵(从1开始,对齐自然周)
  62. for (let w = 1; w < matrix.length; w++) {
  63. const sections = Array.from(matrix[w]).sort((a, b) => a - b);
  64. if (sections.length === 0) continue;
  65. let start = sections[0];
  66. let prev = sections[0];
  67. for (let i = 1; i < sections.length; i++) {
  68. const curr = sections[i];
  69. if (curr === prev + 1) {
  70. prev = curr;
  71. } else {
  72. const blockKey = `${start}-${prev}`;
  73. if (!blockMap[blockKey]) blockMap[blockKey] = [];
  74. blockMap[blockKey].push(w);
  75. start = curr;
  76. prev = curr;
  77. }
  78. }
  79. const blockKey = `${start}-${prev}`;
  80. if (!blockMap[blockKey]) blockMap[blockKey] = [];
  81. blockMap[blockKey].push(w);
  82. }
  83. for (const blockKey in blockMap) {
  84. const [startSec, endSec] = blockKey.split('-').map(Number);
  85. merged.push({
  86. name: group.name,
  87. teacher: group.teacher,
  88. position: group.position,
  89. day: group.day,
  90. startSection: startSec,
  91. endSection: endSec,
  92. weeks: blockMap[blockKey]
  93. });
  94. }
  95. }
  96. // 格式化输出排序
  97. merged.sort((a, b) => {
  98. if (a.day !== b.day) return a.day - b.day;
  99. if (a.startSection !== b.startSection) return a.startSection - b.startSection;
  100. return a.name.localeCompare(b.name);
  101. });
  102. return merged;
  103. }
  104. /**
  105. * 将教务系统的课程数据转换成 CourseJsonModel 结构(应用物理节次转换)
  106. */
  107. function parseSingleCourse(rawCourse, sectionMap) {
  108. const courseName = rawCourse.KCM;
  109. const teacherName = rawCourse.SKJS ? rawCourse.SKJS.split('/')[0] : '';
  110. const position = rawCourse.JASMC;
  111. const day = rawCourse.SKXQ;
  112. const startSection = sectionMap[rawCourse.KSJC];
  113. const endSection = sectionMap[rawCourse.JSJC];
  114. const weeks = parseWeeksFromSkzc(rawCourse.SKZC);
  115. if (!courseName || !day || !startSection || !endSection || weeks.length === 0) {
  116. return null;
  117. }
  118. return {
  119. "name": courseName,
  120. "teacher": teacherName,
  121. "position": position || '待定',
  122. "day": parseInt(day),
  123. "startSection": startSection,
  124. "endSection": endSection,
  125. "weeks": weeks
  126. };
  127. }
  128. async function promptUserToStart() {
  129. const confirmed = await window.AndroidBridgePromise.showAlert(
  130. "武汉理工大学课表导入",
  131. "本流程将通过教务系统接口获取您的个人课表与开学时间。\n重要提示:\n导入前请确保您已在浏览器中成功登录教务系统,且未关闭登录窗口。",
  132. "好的,开始导入"
  133. );
  134. if (!confirmed) {
  135. AndroidBridge.showToast("用户取消了导入。");
  136. return null;
  137. }
  138. return true;
  139. }
  140. async function getAcademicYear() {
  141. const currentYear = new Date().getFullYear();
  142. const yearSelection = await window.AndroidBridgePromise.showPrompt(
  143. "选择学年",
  144. "请输入要导入课程的起始学年(例如 2025-2026 应输入2025):",
  145. String(currentYear),
  146. "validateYearInput"
  147. );
  148. return yearSelection;
  149. }
  150. async function selectSemester() {
  151. const semesters = ["1 (秋季学期/上学期)", "2 (春季学期/下学期)"];
  152. const semesterIndex = await window.AndroidBridgePromise.showSingleSelection(
  153. "选择学期",
  154. JSON.stringify(semesters),
  155. 0
  156. );
  157. if (semesterIndex === null) return null;
  158. return String(semesterIndex + 1);
  159. }
  160. async function fetchSectionMapping(commonHeaders) {
  161. const sectionMapUrl = "https://jwxt.whut.edu.cn/jwapp/sys/kcbcxby/modules/dzkz/jcjcx.do";
  162. try {
  163. const response = await fetch(sectionMapUrl, {
  164. "headers": commonHeaders,
  165. "method": "POST",
  166. "credentials": "include"
  167. });
  168. const resData = JSON.parse(await response.text());
  169. const rows = resData?.datas?.jcjcx?.rows || [];
  170. const dictionary = {};
  171. let physicalIndex = 1;
  172. for (const row of rows) {
  173. if (row.MC && row.MC.includes("节")) {
  174. dictionary[row.DM] = physicalIndex;
  175. physicalIndex++;
  176. }
  177. }
  178. return dictionary;
  179. } catch (e) {
  180. console.error("Fetch Section Mapping Error, use fallback:", e);
  181. 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 };
  182. }
  183. }
  184. // 数据获取和解析部分
  185. async function fetchAndParseCourses(academicYear, semesterCode) {
  186. const commonHeaders = {
  187. "accept": "application/json, text/javascript, */*; q=0.01",
  188. "accept-language": "zh-CN,zh;q=0.9",
  189. "x-requested-with": "XMLHttpRequest"
  190. };
  191. // 获取物理节次转换映射字典
  192. const sectionMap = await fetchSectionMapping(commonHeaders);
  193. // 获取当前登录用户的学号 (XH)
  194. let studentId = "";
  195. try {
  196. const userUrl = "https://jwxt.whut.edu.cn/jwapp/sys/homeapp/api/home/currentUser.do";
  197. const userResponse = await fetch(userUrl, { "method": "GET", "credentials": "include" });
  198. const userData = JSON.parse(await userResponse.text());
  199. studentId = userData?.datas?.userId;
  200. if (!studentId) throw new Error("无法读取 userId");
  201. } catch (e) {
  202. AndroidBridge.showToast("获取用户信息失败,请确认是否登录教务系统!");
  203. console.error("Fetch User Error:", e);
  204. return null;
  205. }
  206. // 获取该学期的课表配置
  207. let courseConfig = { semesterTotalWeeks: 20 };
  208. try {
  209. const configUrl = "https://jwxt.whut.edu.cn/jwapp/sys/kcbcxby/modules/bjkcb/cxjcs.do";
  210. const configBody = `XN=${academicYear}-${parseInt(academicYear) + 1}&XQ=${semesterCode}`;
  211. const configResponse = await fetch(configUrl, {
  212. "headers": { ...commonHeaders, "content-type": "application/x-www-form-urlencoded; charset=UTF-8" },
  213. "body": configBody,
  214. "method": "POST",
  215. "credentials": "include"
  216. });
  217. const configData = JSON.parse(await configResponse.text());
  218. const configRow = configData?.datas?.cxjcs?.rows?.[0];
  219. if (configRow) {
  220. if (configRow.XQKSRQ) {
  221. courseConfig.semesterStartDate = configRow.XQKSRQ.split(" ")[0];
  222. }
  223. if (configRow.ZZC) {
  224. courseConfig.semesterTotalWeeks = parseInt(configRow.ZZC);
  225. }
  226. }
  227. } catch (e) {
  228. console.error("Fetch Config Error, will use default config:", e);
  229. }
  230. // 获取个人课表数据
  231. const XNXQDM = `${academicYear}-${parseInt(academicYear) + 1}-${semesterCode}`;
  232. const courseUrl = "https://jwxt.whut.edu.cn/jwapp/sys/kcbcxby/modules/xskcb/cxxskcb.do";
  233. const courseBody = `XNXQDM=${XNXQDM}&XH=${studentId}`;
  234. let rawCourseData;
  235. try {
  236. const response = await fetch(courseUrl, {
  237. "headers": { ...commonHeaders, "content-type": "application/x-www-form-urlencoded; charset=UTF-8" },
  238. "body": courseBody,
  239. "method": "POST",
  240. "credentials": "include"
  241. });
  242. rawCourseData = JSON.parse(await response.text());
  243. } catch (e) {
  244. AndroidBridge.showToast("请求课表 API 失败,请检查网络和登录状态。");
  245. console.error("Fetch Course Error:", e);
  246. return null;
  247. }
  248. const rawCourses = rawCourseData?.datas?.cxxskcb?.rows || [];
  249. if (rawCourses.length === 0) {
  250. AndroidBridge.showToast("该学期未查询到您的课程数据。");
  251. return null;
  252. }
  253. const initialCourses = rawCourses.map(c => parseSingleCourse(c, sectionMap)).filter(c => c !== null);
  254. const finalParsedCourses = mergeContinuousLessons(initialCourses);
  255. return {
  256. courses: finalParsedCourses,
  257. config: courseConfig
  258. };
  259. }
  260. async function saveCourses(parsedCourses) {
  261. if (parsedCourses.length === 0) {
  262. AndroidBridge.showToast("没有有效的课程数据可供保存。");
  263. return true;
  264. }
  265. try {
  266. await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(parsedCourses));
  267. AndroidBridge.showToast(`成功导入 ${parsedCourses.length} 个规范后的课程块!`);
  268. return true;
  269. } catch (error) {
  270. AndroidBridge.showToast(`保存课程数据失败: ${error.message}`);
  271. return false;
  272. }
  273. }
  274. /**
  275. * 导入预设时间段数据
  276. */
  277. async function importPresetTimeSlots() {
  278. const presetTimeSlots = [
  279. { "number": 1, "startTime": "08:00", "endTime": "08:45" },
  280. { "number": 2, "startTime": "08:50", "endTime": "09:35" },
  281. { "number": 3, "startTime": "09:55", "endTime": "10:40" },
  282. { "number": 4, "startTime": "10:45", "endTime": "11:30" },
  283. { "number": 5, "startTime": "11:35", "endTime": "12:20" },
  284. { "number": 6, "startTime": "14:00", "endTime": "14:45" },
  285. { "number": 7, "startTime": "14:50", "endTime": "15:35" },
  286. { "number": 8, "startTime": "15:40", "endTime": "16:25" },
  287. { "number": 9, "startTime": "16:45", "endTime": "17:30" },
  288. { "number": 10, "startTime": "17:35", "endTime": "18:20" },
  289. { "number": 11, "startTime": "19:00", "endTime": "19:45" },
  290. { "number": 12, "startTime": "19:50", "endTime": "20:35" },
  291. { "number": 13, "startTime": "20:40", "endTime": "21:25" }
  292. ];
  293. try {
  294. await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(presetTimeSlots));
  295. return true;
  296. } catch (error) {
  297. console.error("导入时间段失败: " + error.message);
  298. return false;
  299. }
  300. }
  301. async function saveConfig(configData) {
  302. try {
  303. await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(configData));
  304. return true;
  305. } catch (error) {
  306. console.error("保存配置失败: " + error.message);
  307. return false;
  308. }
  309. }
  310. async function runImportFlow() {
  311. AndroidBridge.showToast("武汉理工大学课程导入流程启动...");
  312. const alertConfirmed = await promptUserToStart();
  313. if (!alertConfirmed) return;
  314. const academicYear = await getAcademicYear();
  315. if (academicYear === null) {
  316. AndroidBridge.showToast("导入已取消。");
  317. return;
  318. }
  319. const semesterCode = await selectSemester();
  320. if (semesterCode === null) {
  321. AndroidBridge.showToast("导入已取消。");
  322. return;
  323. }
  324. const courseData = await fetchAndParseCourses(academicYear, semesterCode);
  325. if (courseData === null) return;
  326. await importPresetTimeSlots();
  327. const configSaveResult = await saveConfig(courseData.config);
  328. if (!configSaveResult) return;
  329. const saveResult = await saveCourses(courseData.courses);
  330. if (!saveResult) return;
  331. AndroidBridge.notifyTaskCompletion();
  332. }
  333. runImportFlow();