gdou.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. /**
  2. * 广东海洋大学教务适配
  3. * @date 2026-7-30
  4. * @author Mccurtain
  5. * @version 1.1
  6. */
  7. /**
  8. * 解析周次字符串,处理单双周和周次范围。
  9. * 兼容格式:"1-16周"、"6周"、"1-8周(单)"、"1-10周(双)"、"1-5周,9周"
  10. */
  11. function parseWeeks(weekStr) {
  12. if (!weekStr) return [];
  13. const weekSets = weekStr.split(',');
  14. let weeks = [];
  15. for (const set of weekSets) {
  16. const trimmedSet = set.trim();
  17. const rangeMatch = trimmedSet.match(/(\d+)-(\d+)周/);
  18. const singleMatch = trimmedSet.match(/^(\d+)周/); // 匹配以数字周结束的
  19. let start = 0;
  20. let end = 0;
  21. let processed = false;
  22. if (rangeMatch) { // 范围, 如 "1-5周"
  23. start = Number(rangeMatch[1]);
  24. end = Number(rangeMatch[2]);
  25. processed = true;
  26. } else if (singleMatch) { // 单个周, 如 "6周"
  27. start = end = Number(singleMatch[1]);
  28. processed = true;
  29. }
  30. if (processed) {
  31. // 确定单双周
  32. const isSingle = trimmedSet.includes('(单)');
  33. const isDouble = trimmedSet.includes('(双)');
  34. for (let w = start; w <= end; w++) {
  35. if (isSingle && w % 2 === 0) continue; // 单周跳过偶数
  36. if (isDouble && w % 2 !== 0) continue; // 双周跳过奇数
  37. weeks.push(w);
  38. }
  39. }
  40. }
  41. // 去重并排序
  42. return [...new Set(weeks)].sort((a, b) => a - b);
  43. }
  44. /**
  45. * 解析正方 v9 课表查询接口返回的 JSON 数据。
  46. */
  47. function parseJsonData(jsonData) {
  48. console.log("JS: parseJsonData 正在解析 JSON 数据...");
  49. // 正方 v9 个人课表数据放在 kbList 字段中
  50. if (!jsonData || !Array.isArray(jsonData.kbList)) {
  51. console.warn("JS: JSON 数据结构错误或缺少 kbList 字段。");
  52. return [];
  53. }
  54. const rawCourseList = jsonData.kbList;
  55. const finalCourseList = [];
  56. for (const rawCourse of rawCourseList) {
  57. // 关键字段检查:kcmc(课名), xm(教师), cdmc(教室), xqj(星期), jcs(节次范围), zcd(周次描述)
  58. if (!rawCourse.kcmc || !rawCourse.xm || !rawCourse.cdmc ||
  59. !rawCourse.xqj || !rawCourse.jcs || !rawCourse.zcd) {
  60. continue;
  61. }
  62. const weeksArray = parseWeeks(rawCourse.zcd);
  63. // 周次有效性检查
  64. if (weeksArray.length === 0) {
  65. continue;
  66. }
  67. // 解析节次范围,例如 "1-2" 或 "1-2节"(兼容带"节"字的情况)
  68. const sectionParts = rawCourse.jcs.split('-');
  69. const startSection = parseInt(sectionParts[0].match(/\d+/)[0], 10);
  70. const endSection = parseInt(sectionParts[sectionParts.length - 1].match(/\d+/)[0], 10);
  71. const day = Number(rawCourse.xqj); // xqj: 星期几 (周一为1, 周日为7)
  72. // 数字有效性检查
  73. if (isNaN(day) || isNaN(startSection) || isNaN(endSection) ||
  74. day < 1 || day > 7 || startSection > endSection) {
  75. continue;
  76. }
  77. finalCourseList.push({
  78. name: rawCourse.kcmc.trim(),
  79. teacher: rawCourse.xm.trim(),
  80. position: rawCourse.cdmc.trim(),
  81. day: day,
  82. startSection: startSection,
  83. endSection: endSection,
  84. weeks: weeksArray
  85. });
  86. }
  87. finalCourseList.sort((a, b) =>
  88. a.day - b.day ||
  89. a.startSection - b.startSection ||
  90. a.name.localeCompare(b.name)
  91. );
  92. console.log(`JS: JSON 数据解析完成,共找到 ${finalCourseList.length} 门课程。`);
  93. return finalCourseList;
  94. }
  95. /**
  96. * showPrompt 的校验函数:限定四位数字学年。
  97. */
  98. function validateYearInput(input) {
  99. console.log("JS: validateYearInput 被调用,输入: " + input);
  100. if (/^[0-9]{4}$/.test(input)) {
  101. console.log("JS: validateYearInput 验证通过。");
  102. return false;
  103. } else {
  104. console.log("JS: validateYearInput 验证失败。");
  105. return "请输入四位数字的学年!";
  106. }
  107. }
  108. async function promptUserToStart() {
  109. console.log("JS: 流程开始:显示公告。");
  110. return await window.AndroidBridgePromise.showAlert(
  111. "广东海洋大学教务系统课表导入",
  112. "导入前请确保您已在浏览器中成功登录广东海洋大学教务系统(jw.gdou.edu.cn)。\n本脚本将通过接口直接获取课表,无需停留在特定页面。",
  113. "好的,开始导入"
  114. );
  115. }
  116. async function getAcademicYear() {
  117. const currentYear = new Date().getFullYear().toString();
  118. console.log("JS: 提示用户输入学年。");
  119. return await window.AndroidBridgePromise.showPrompt(
  120. "选择学年",
  121. "请输入要导入课程的起始学年(例如 2025-2026 应输入 2025):",
  122. currentYear,
  123. "validateYearInput"
  124. );
  125. }
  126. async function selectSemester() {
  127. const semesters = ["第一学期", "第二学期"];
  128. console.log("JS: 提示用户选择学期。");
  129. const semesterIndex = await window.AndroidBridgePromise.showSingleSelection(
  130. "选择学期",
  131. JSON.stringify(semesters),
  132. 0
  133. );
  134. return semesterIndex;
  135. }
  136. /**
  137. * 将选择索引转换为正方教务接口所需的学期码。
  138. * 正方 v9:第一学期 = "3",第二学期 = "12"
  139. */
  140. function getSemesterCode(semesterIndex) {
  141. return semesterIndex === 0 ? "3" : "12";
  142. }
  143. /**
  144. * 请求正方 v9 课表接口并解析课程数据。
  145. */
  146. async function fetchAndParseCourses(academicYear, semesterIndex) {
  147. const semesterCode = getSemesterCode(semesterIndex);
  148. const requestBody = `xnm=${academicYear}&xqm=${semesterCode}&kzlx=ck&xsdm=&kclbdm=`;
  149. // 广东海洋大学正方教务 v9 个人课表查询接口
  150. const targetUrl = "https://jw.gdou.edu.cn/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N2151";
  151. try {
  152. const response = await fetch(targetUrl, {
  153. method: "POST",
  154. headers: {
  155. "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
  156. },
  157. body: requestBody,
  158. credentials: "include"
  159. });
  160. if (!response.ok) {
  161. AndroidBridge.showToast(`课表请求失败:HTTP ${response.status}`);
  162. console.error(`JS: 接口返回非 200 状态码:${response.status}`);
  163. return null;
  164. }
  165. const jsonText = await response.text();
  166. const jsonData = JSON.parse(jsonText);
  167. if (!jsonData || !Array.isArray(jsonData.kbList) || jsonData.kbList.length === 0) {
  168. AndroidBridge.showToast("未查询到课表数据,请检查学年/学期是否选择正确,或确认已登录教务系统。");
  169. return null;
  170. }
  171. const parsedCourses = parseJsonData(jsonData);
  172. if (parsedCourses.length === 0) {
  173. AndroidBridge.showToast("课表数据为空或解析失败,请确认所选学年学期。");
  174. return null;
  175. }
  176. return {
  177. courses: parsedCourses,
  178. // CourseConfigJsonModel(wiki 1.3):所有字段可选,未提供则用默认值。
  179. // GDOU 各节课间隔不统一,因此用 TimeSlot 节次表达时间,此处仅设置总周数。
  180. config: {
  181. semesterStartDate: null, // 不提供则由应用按校历校准
  182. semesterTotalWeeks: 20 // 本学期总周数
  183. }
  184. };
  185. } catch (e) {
  186. console.error("JS: 获取课表失败:", e);
  187. AndroidBridge.showToast("获取课表失败,请确认已登录教务系统且网络可访问 jw.gdou.edu.cn。");
  188. return null;
  189. }
  190. }
  191. async function saveCourses(parsedCourses) {
  192. AndroidBridge.showToast(`正在保存 ${parsedCourses.length} 门课程...`);
  193. console.log(`JS: 尝试保存 ${parsedCourses.length} 门课程...`);
  194. try {
  195. await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(parsedCourses, null, 2));
  196. console.log("JS: 课程保存成功!");
  197. return true;
  198. } catch (error) {
  199. AndroidBridge.showToast(`课程保存失败: ${error.message}`);
  200. console.error('JS: Save Courses Error:', error);
  201. return false;
  202. }
  203. }
  204. // 广东海洋大学上课时间(真实作息,仅供参考,可自行核对)
  205. const TimeSlots = [
  206. { number: 1, startTime: "08:10", endTime: "08:55" },
  207. { number: 2, startTime: "09:00", endTime: "09:45" },
  208. { number: 3, startTime: "10:15", endTime: "11:00" },
  209. { number: 4, startTime: "11:05", endTime: "11:50" },
  210. { number: 5, startTime: "14:30", endTime: "15:15" },
  211. { number: 6, startTime: "15:20", endTime: "16:05" },
  212. { number: 7, startTime: "16:30", endTime: "17:15" },
  213. { number: 8, startTime: "17:20", endTime: "18:05" },
  214. { number: 9, startTime: "19:30", endTime: "20:15" },
  215. { number: 10, startTime: "20:25", endTime: "21:10" }
  216. ];
  217. async function importPresetTimeSlots(timeSlots) {
  218. console.log(`JS: 准备导入 ${timeSlots.length} 个预设时间段。`);
  219. if (timeSlots.length > 0) {
  220. AndroidBridge.showToast(`正在导入 ${timeSlots.length} 个预设时间段...`);
  221. try {
  222. await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  223. AndroidBridge.showToast("预设时间段导入成功!");
  224. console.log("JS: 预设时间段导入成功。");
  225. } catch (error) {
  226. AndroidBridge.showToast("导入时间段失败: " + error.message);
  227. console.error('JS: Save Time Slots Error:', error);
  228. }
  229. } else {
  230. AndroidBridge.showToast("警告:时间段为空,未导入时间段信息。");
  231. console.warn("JS: 警告:传入时间段为空,未导入时间段信息。");
  232. }
  233. }
  234. async function runImportFlow() {
  235. const alertConfirmed = await promptUserToStart();
  236. if (!alertConfirmed) {
  237. AndroidBridge.showToast("用户取消了导入。");
  238. console.log("JS: 用户取消了导入流程。");
  239. return;
  240. }
  241. const academicYear = await getAcademicYear();
  242. if (academicYear === null) {
  243. AndroidBridge.showToast("导入已取消。");
  244. console.log("JS: 获取学年失败/取消,流程终止。");
  245. return;
  246. }
  247. console.log(`JS: 已选择学年: ${academicYear}`);
  248. const semesterIndex = await selectSemester();
  249. if (semesterIndex === null || semesterIndex === -1) {
  250. AndroidBridge.showToast("导入已取消。");
  251. console.log("JS: 选择学期失败/取消,流程终止。");
  252. return;
  253. }
  254. console.log(`JS: 已选择学期索引: ${semesterIndex}`);
  255. const result = await fetchAndParseCourses(academicYear, semesterIndex);
  256. if (result === null) {
  257. console.log("JS: 课程获取或解析失败,流程终止。");
  258. return;
  259. }
  260. const { courses, config } = result;
  261. const saveResult = await saveCourses(courses);
  262. if (!saveResult) {
  263. console.log("JS: 课程保存失败,流程终止。");
  264. return;
  265. }
  266. try {
  267. await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(config));
  268. AndroidBridge.showToast(`课表配置更新成功!总周数:${config.semesterTotalWeeks}周。`);
  269. } catch (error) {
  270. AndroidBridge.showToast(`课表配置保存失败: ${error.message}`);
  271. console.error('JS: Save Config Error:', error);
  272. }
  273. await importPresetTimeSlots(TimeSlots);
  274. AndroidBridge.showToast(`课程导入成功,共导入 ${courses.length} 门课程!`);
  275. console.log("JS: 整个导入流程执行完毕并成功。");
  276. AndroidBridge.notifyTaskCompletion();
  277. }
  278. // 脚本执行入口
  279. runImportFlow();