ctgu.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. // 三峡大学(ctgu.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. * 将教务系统的课程数据转换成 CourseJsonModel 结构
  27. */
  28. function parseSingleCourse(rawCourse) {
  29. const courseName = rawCourse.KCM;
  30. const teacherName = rawCourse.SKJS ? rawCourse.SKJS.split('/')[0] : '';
  31. const position = rawCourse.JASMC;
  32. const day = rawCourse.SKXQ;
  33. const startSection = rawCourse.KSJC;
  34. const endSection = rawCourse.JSJC;
  35. const weeks = parseWeeksFromSkzc(rawCourse.SKZC);
  36. if (!courseName || !day || !startSection || !endSection || weeks.length === 0) {
  37. return null;
  38. }
  39. const course = {
  40. "name": courseName,
  41. "teacher": teacherName,
  42. "position": position || '待定',
  43. "day": parseInt(day),
  44. "startSection": parseInt(startSection),
  45. "endSection": parseInt(endSection),
  46. "weeks": weeks
  47. };
  48. course._kbId = rawCourse.KBID;
  49. course._day = course.day;
  50. course._startSection = course.startSection;
  51. course._endSection = course.endSection;
  52. return course;
  53. }
  54. /**
  55. * 将调课数据应用到已解析的课程列表上
  56. */
  57. function applyCourseChanges(parsedCourses, rawChanges) {
  58. let successCount = 0;
  59. for (const change of rawChanges) {
  60. const kbID = change.KBID;
  61. const originalTeacher = change.YSKJS ? change.YSKJS.split('/')[0] : '';
  62. const weeksToRemove = parseWeeksFromSkzc(change.SKZC);
  63. let changeApplied = false;
  64. const affectedOriginalCourses = parsedCourses.filter(c =>
  65. c._kbId === kbID &&
  66. c._day === parseInt(change.SKXQ) &&
  67. c._startSection === parseInt(change.KSJC) &&
  68. c._endSection === parseInt(change.JSJC)
  69. );
  70. if (affectedOriginalCourses.length === 0) {
  71. continue;
  72. }
  73. if (weeksToRemove.length > 0) {
  74. affectedOriginalCourses.forEach(originalCourse => {
  75. const beforeLength = originalCourse.weeks.length;
  76. originalCourse.weeks = originalCourse.weeks.filter(w => !weeksToRemove.includes(w));
  77. if (originalCourse.weeks.length < beforeLength) {
  78. changeApplied = true;
  79. }
  80. });
  81. }
  82. const isTimeLocationChange = (change.TKLXDM === '01' || change.TKLXDM === '03');
  83. if (isTimeLocationChange && change.XSKZC && change.XSKXQ && change.XKSJC && change.XJSJC) {
  84. const newWeeks = parseWeeksFromSkzc(change.XSKZC);
  85. if (newWeeks.length > 0) {
  86. const newCourse = {
  87. "name": change.KCM,
  88. "teacher": change.XSKJS ? change.XSKJS.split('/')[0] : originalTeacher,
  89. "position": change.XJASMC || change.JASMC || '待定',
  90. "day": parseInt(change.XSKXQ),
  91. "startSection": parseInt(change.XKSJC),
  92. "endSection": parseInt(change.XJSJC),
  93. "weeks": newWeeks,
  94. "_kbId": kbID,
  95. "_day": parseInt(change.XSKXQ),
  96. "_startSection": parseInt(change.XKSJC),
  97. "_endSection": parseInt(change.XJSJC)
  98. };
  99. parsedCourses.push(newCourse);
  100. changeApplied = true;
  101. }
  102. }
  103. if (changeApplied) {
  104. successCount++;
  105. }
  106. }
  107. if (successCount > 0) {
  108. AndroidBridge.showToast(`已应用 ${successCount} 条调课/停课变更,获得实际课表。`);
  109. }
  110. return parsedCourses.map(c => {
  111. delete c._kbId;
  112. delete c._day;
  113. delete c._startSection;
  114. delete c._endSection;
  115. return c;
  116. }).filter(c => c.weeks.length > 0);
  117. }
  118. async function promptUserToStart() {
  119. const confirmed = await window.AndroidBridgePromise.showAlert(
  120. "重要通知:三峡大学课表导入",
  121. "本流程将通过教务系统接口获取您的个人课表。\n重要提示:\n导入前请确保您已在浏览器中成功登录教务系统,且未关闭登录窗口,确认当前页面有显示课表不然获取不了数据",
  122. "好的,开始导入"
  123. );
  124. if (!confirmed) {
  125. AndroidBridge.showToast("用户取消了导入。");
  126. return null;
  127. }
  128. return true;
  129. }
  130. async function getAcademicYear() {
  131. const currentYear = new Date().getFullYear();
  132. const yearSelection = await window.AndroidBridgePromise.showPrompt(
  133. "选择学年",
  134. "请输入要导入课程的学年(例如 2025):",
  135. String(currentYear),
  136. "validateYearInput"
  137. );
  138. return yearSelection;
  139. }
  140. async function selectSemester() {
  141. const semesters = ["1 (秋季学期/上学期)", "2 (春季学期/下学期)"];
  142. const semesterIndex = await window.AndroidBridgePromise.showSingleSelection(
  143. "选择学期",
  144. JSON.stringify(semesters),
  145. 0
  146. );
  147. if (semesterIndex === null) return null;
  148. return String(semesterIndex + 1);
  149. }
  150. // 数据获取和解析部分
  151. async function fetchAndParseCourses(academicYear, semesterCode) {
  152. const XNXQDM = `${academicYear}-${parseInt(academicYear) + 1}-${semesterCode}`;
  153. const headers = {
  154. "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
  155. "x-requested-with": "XMLHttpRequest",
  156. };
  157. // 获取个人课表数据
  158. const courseUrl = "http://jwxt.ctgu.edu.cn/jwapp/sys/wdkb/modules/xskcb/cxxszhxqkb.do";
  159. const courseBody = `XNXQDM=${XNXQDM}`;
  160. let rawCourseData;
  161. try {
  162. const response = await fetch(courseUrl, { "headers": headers, "body": courseBody, "method": "POST", "credentials": "include" });
  163. rawCourseData = JSON.parse(await response.text());
  164. } catch (e) {
  165. AndroidBridge.showToast("请求课表 API 失败,请检查网络和登录状态,以及是否跳转到课表页面");
  166. console.error("Fetch Course Error:", e);
  167. return null;
  168. }
  169. const rawCourses = rawCourseData?.datas?.cxxszhxqkb?.rows || [];
  170. if (rawCourses.length === 0) {
  171. AndroidBridge.showToast("该学期未查询到您的课程数据。");
  172. return null;
  173. }
  174. let parsedCourses = rawCourses.map(c => parseSingleCourse(c)).filter(c => c !== null);
  175. const changeUrl = "http://jwxt.ctgu.edu.cn/jwapp/sys/wdkb/modules/xskcb/xsdkkc.do";
  176. const changeBody = `XNXQDM=${XNXQDM}&*order=-SQSJ`;
  177. let rawChangeData;
  178. try {
  179. const response = await fetch(changeUrl, { "headers": headers, "body": changeBody, "method": "POST", "credentials": "include" });
  180. rawChangeData = JSON.parse(await response.text());
  181. } catch (e) {
  182. AndroidBridge.showToast("请求调课 API 失败,将使用未调整的课表数据。");
  183. console.error("Fetch Change Error:", e);
  184. }
  185. const rawChanges = rawChangeData?.datas?.xsdkkc?.rows || [];
  186. // 应用调课变更
  187. if (rawChanges.length > 0) {
  188. parsedCourses = applyCourseChanges(parsedCourses, rawChanges);
  189. }
  190. // 课表配置数据
  191. const courseConfig = {
  192. semesterTotalWeeks: 20
  193. };
  194. return {
  195. courses: parsedCourses,
  196. config: courseConfig
  197. };
  198. }
  199. async function saveCourses(parsedCourses) {
  200. if (parsedCourses.length === 0) {
  201. AndroidBridge.showToast("没有有效的课程数据可供保存。");
  202. return true;
  203. }
  204. try {
  205. await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(parsedCourses));
  206. AndroidBridge.showToast(`成功导入 ${parsedCourses.length} 门课程!`);
  207. return true;
  208. } catch (error) {
  209. AndroidBridge.showToast(`保存课程数据失败: ${error.message}`);
  210. return false;
  211. }
  212. }
  213. /**
  214. * 导入预设时间段数据
  215. */
  216. async function importPresetTimeSlots() {
  217. AndroidBridge.showToast("正在导入预设节次时间...");
  218. const presetTimeSlots = [
  219. { "number": 1, "startTime": "08:00", "endTime": "08:45" },
  220. { "number": 2, "startTime": "08:50", "endTime": "09:35" },
  221. { "number": 3, "startTime": "09:55", "endTime": "10:40" },
  222. { "number": 4, "startTime": "10:45", "endTime": "11:30" },
  223. { "number": 5, "startTime": "11:35", "endTime": "12:20" },
  224. { "number": 6, "startTime": "14:20", "endTime": "15:05" },
  225. { "number": 7, "startTime": "15:10", "endTime": "15:55" },
  226. { "number": 8, "startTime": "16:15", "endTime": "17:00" },
  227. { "number": 9, "startTime": "17:05", "endTime": "17:50" },
  228. { "number": 10, "startTime": "19:00", "endTime": "19:45" },
  229. { "number": 11, "startTime": "19:50", "endTime": "20:35" },
  230. { "number": 12, "startTime": "20:40", "endTime": "21:25" }
  231. ];
  232. try {
  233. await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(presetTimeSlots));
  234. AndroidBridge.showToast("预设时间段导入成功!");
  235. return true;
  236. } catch (error) {
  237. AndroidBridge.showToast("导入时间段失败: " + error.message);
  238. return false;
  239. }
  240. }
  241. async function saveConfig(configData) {
  242. try {
  243. await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(configData));
  244. AndroidBridge.showToast("课表配置更新成功!");
  245. return true;
  246. } catch (error) {
  247. AndroidBridge.showToast("保存配置失败: " + error.message);
  248. return false;
  249. }
  250. }
  251. // 主流程入口
  252. async function runImportFlow() {
  253. AndroidBridge.showToast("三峡大学课程导入流程启动...");
  254. // 1. 公告和前置检查。
  255. const alertConfirmed = await promptUserToStart();
  256. if (!alertConfirmed) return;
  257. // 2. 获取用户输入参数 (学年和学期)。
  258. const academicYear = await getAcademicYear();
  259. if (academicYear === null) {
  260. AndroidBridge.showToast("导入已取消。");
  261. return;
  262. }
  263. const semesterCode = await selectSemester();
  264. if (semesterCode === null) {
  265. AndroidBridge.showToast("导入已取消。");
  266. return;
  267. }
  268. // 3. 导入预设时间段
  269. await importPresetTimeSlots();
  270. // 4. 网络请求和数据解析。
  271. const courseData = await fetchAndParseCourses(academicYear, semesterCode);
  272. if (courseData === null) return;
  273. // 5. 保存配置数据
  274. const configSaveResult = await saveConfig(courseData.config);
  275. if (!configSaveResult) return;
  276. // 6. 课程数据保存。
  277. const saveResult = await saveCourses(courseData.courses);
  278. if (!saveResult) return;
  279. // 7. 流程完全成功,发送结束信号。
  280. AndroidBridge.showToast("所有任务已完成!课表导入成功。");
  281. AndroidBridge.notifyTaskCompletion();
  282. }
  283. // 启动导入流程
  284. runImportFlow();