gpnu_01.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. // ===================== 工具函数 =====================
  2. /**
  3. * 解析周次字符串,例如 "1-16周"、"1-8周,10-16周"、"1-16周(单)" 等。
  4. * @param {string} weekStr - 周次描述字符串
  5. * @returns {number[]} 周次数字数组(升序)
  6. */
  7. function parseWeeks(weekStr) {
  8. if (!weekStr || typeof weekStr !== 'string') return [];
  9. let cleanStr = weekStr.replace(/周/g, '').replace(/\s+/g, '');
  10. const weeks = new Set();
  11. const parts = cleanStr.split(',');
  12. for (let rawPart of parts) {
  13. if (!rawPart) continue; // 跳过空段,防止产生 0 周
  14. let part = rawPart;
  15. let oddOnly = false;
  16. let evenOnly = false;
  17. // 逐段检测单双周标记
  18. if (/单/.test(part)) oddOnly = true;
  19. if (/双/.test(part)) evenOnly = true;
  20. part = part.replace(/\(单\)|\(双\)|单|双/g, '');
  21. if (part.includes('-')) {
  22. const [startStr, endStr] = part.split('-');
  23. const start = Number(startStr);
  24. const end = Number(endStr);
  25. if (!isNaN(start) && !isNaN(end) && start <= end) {
  26. for (let w = start; w <= end; w++) {
  27. if (oddOnly && w % 2 !== 1) continue;
  28. if (evenOnly && w % 2 !== 0) continue;
  29. weeks.add(w);
  30. }
  31. }
  32. } else {
  33. const w = Number(part);
  34. if (!isNaN(w)) {
  35. if (oddOnly && w % 2 !== 1) continue;
  36. if (evenOnly && w % 2 !== 0) continue;
  37. weeks.add(w);
  38. }
  39. }
  40. }
  41. return Array.from(weeks).sort((a, b) => a - b);
  42. }
  43. /**
  44. * 解析 API 返回的 JSON 数据。
  45. * @param {Object} jsonData - 教务系统返回的 JSON 对象
  46. * @returns {Array} 解析后的课程数组
  47. */
  48. function parseJsonData(jsonData) {
  49. console.log("JS: parseJsonData 正在解析 JSON 数据...");
  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. if (!rawCourse.kcmc || !rawCourse.xm || !rawCourse.cdmc ||
  58. !rawCourse.xqj || !rawCourse.jcs || !rawCourse.zcd) {
  59. continue;
  60. }
  61. const weeksArray = parseWeeks(rawCourse.zcd);
  62. if (weeksArray.length === 0) {
  63. continue;
  64. }
  65. // 去除“节”字后再拆分,防止 "1-2节" 导致 NaN
  66. const jcs = String(rawCourse.jcs).replace(/节/g, '');
  67. const sectionParts = jcs.split('-');
  68. const startSection = Number(sectionParts[0]);
  69. const endSection = Number(sectionParts[sectionParts.length - 1]);
  70. const day = Number(rawCourse.xqj);
  71. if (isNaN(day) || isNaN(startSection) || isNaN(endSection) ||
  72. day < 1 || day > 7 || startSection > endSection) {
  73. continue;
  74. }
  75. finalCourseList.push({
  76. name: rawCourse.kcmc.trim(),
  77. teacher: rawCourse.xm.trim(),
  78. position: rawCourse.cdmc.trim(),
  79. day: day,
  80. startSection: startSection,
  81. endSection: endSection,
  82. weeks: weeksArray
  83. });
  84. }
  85. finalCourseList.sort((a, b) =>
  86. a.day - b.day ||
  87. a.startSection - b.startSection ||
  88. a.name.localeCompare(b.name)
  89. );
  90. console.log(`JS: JSON 数据解析完成,共找到 ${finalCourseList.length} 门课程。`);
  91. return finalCourseList;
  92. }
  93. // ===================== 全局验证函数 =====================
  94. function validateYearInput(input) {
  95. console.log("JS: validateYearInput 被调用,输入: " + input);
  96. if (input === null || input === undefined) {
  97. return "请输入四位数字的学年喵~";
  98. }
  99. if (/^[0-9]{4}$/.test(input)) {
  100. return false; // 校验通过
  101. } else {
  102. return "请输入四位数字的学年喵~";
  103. }
  104. }
  105. function validateDateInput(input) {
  106. if (input === null || input === undefined || input.trim() === '') {
  107. return false; // 允许空值(留空跳过)
  108. }
  109. if (/^\d{4}-\d{2}-\d{2}$/.test(input.trim())) {
  110. return false; // 校验通过
  111. }
  112. return "日期格式应为 YYYY-MM-DD,例如 2026-08-30";
  113. }
  114. // ===================== 与原生交互的异步封装 =====================
  115. async function promptUserToStart() {
  116. console.log("JS: 流程开始:显示公告。");
  117. return await window.shiguangBridgePromise.showAlert(
  118. "教务系统课表导入",
  119. "Ciallo~ 导入前请确保您已在浏览器中成功登录教务系统哦~",
  120. "好的,开始导入"
  121. );
  122. }
  123. async function getAcademicYear() {
  124. const currentYear = new Date().getFullYear().toString();
  125. console.log("JS: 提示用户输入学年。");
  126. return await window.shiguangBridgePromise.showPrompt(
  127. "选择学年喵~",
  128. "请输入要导入课程的起始学年喵~(例如 2025-2026 应输入2025):",
  129. currentYear,
  130. "validateYearInput"
  131. );
  132. }
  133. async function selectSemester() {
  134. const semesters = ["第一学期", "第二学期"];
  135. console.log("JS: 提示用户选择学期。");
  136. const rawIndex = await window.shiguangBridgePromise.showSingleSelection(
  137. "选择学期喵~",
  138. JSON.stringify(semesters),
  139. 0
  140. );
  141. // 统一转换为数字,避免字符串索引导致判断错误
  142. return Number(rawIndex);
  143. }
  144. async function selectArea() {
  145. const areas = ["东/西/北校区", "白云校区", "河源校区"];
  146. console.log("JS: 提示用户选择校区。");
  147. const rawIndex = await window.shiguangBridgePromise.showSingleSelection(
  148. "选择校区喵~",
  149. JSON.stringify(areas),
  150. 0
  151. );
  152. // 统一转换为数字
  153. return Number(rawIndex);
  154. }
  155. async function getSemesterStartDate() {
  156. console.log("JS: 提示用户输入学期开始日期(可留空跳过)。");
  157. const input = await window.shiguangBridgePromise.showPrompt(
  158. "学期开始日期(可选)",
  159. "请输入学期第一天的日期喵~(YYYY-MM-DD),留空则自动留空:",
  160. "2026-09-07",
  161. "validateDateInput"
  162. );
  163. if (input === null) {
  164. console.log("JS: 用户取消了开始日期输入,继续流程。");
  165. return null;
  166. }
  167. if (input.trim() === '') {
  168. return null;
  169. }
  170. return input.trim();
  171. }
  172. function getSemesterCode(semesterIndex) {
  173. // 3 表示第一学期,12 表示第二学期
  174. return semesterIndex === 0 ? "3" : "12";
  175. }
  176. // ===================== 网络请求与课程解析 =====================
  177. async function fetchAndParseCourses(academicYear, semesterIndex) {
  178. const semesterCode = getSemesterCode(semesterIndex);
  179. const requestBody = `gnmkdm=N2151&xnm=${academicYear}&xqm=${semesterCode}&kzlx=ck&xsdm=&kclbdm=&kclxdm=`;
  180. const targetUrls = [
  181. "https://jwglxt.gpnu.edu.cn/jwglxt/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N2151"
  182. ];
  183. for (const url of targetUrls) {
  184. try {
  185. const response = await fetch(url, {
  186. method: "POST",
  187. headers: {
  188. "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
  189. "X-Requested-With": "XMLHttpRequest"
  190. // 注意:浏览器禁止手动设置 Origin 和 Referer,已移除。
  191. },
  192. body: requestBody,
  193. credentials: "include"
  194. });
  195. if (response.ok) {
  196. const jsonText = await response.text();
  197. const jsonData = JSON.parse(jsonText);
  198. if (jsonData && jsonData.kbList) {
  199. const parsedCourses = parseJsonData(jsonData);
  200. if (parsedCourses.length > 0) {
  201. const totalWeeks = inferTotalWeeks(parsedCourses);
  202. return {
  203. courses: parsedCourses,
  204. config: {
  205. semesterStartDate: null,
  206. semesterTotalWeeks: totalWeeks
  207. }
  208. };
  209. }
  210. }
  211. }
  212. } catch (e) {
  213. console.error(`Entry failed: ${url}`, e);
  214. }
  215. }
  216. window.shiguangBridge.showToast("未能获取课表数据,请检查网络环境或登录状态。");
  217. return null;
  218. }
  219. function inferTotalWeeks(courses) {
  220. let maxWeek = 0;
  221. for (const course of courses) {
  222. const weekNums = course.weeks;
  223. if (weekNums.length > 0) {
  224. maxWeek = Math.max(maxWeek, ...weekNums);
  225. }
  226. }
  227. return maxWeek || 20;
  228. }
  229. // ===================== 数据保存 =====================
  230. async function saveCourses(parsedCourses) {
  231. window.shiguangBridge.showToast(`正在保存 ${parsedCourses.length} 门课程...`);
  232. console.log(`JS: 尝试保存 ${parsedCourses.length} 门课程...`);
  233. try {
  234. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(parsedCourses));
  235. console.log("JS: 课程保存成功!");
  236. return true;
  237. } catch (error) {
  238. window.shiguangBridge.showToast(`课程保存失败: ${error.message}`);
  239. console.error('JS: Save Courses Error:', error);
  240. return false;
  241. }
  242. }
  243. // ===================== 三个校区的时间表 =====================
  244. // 东校区-西校区-北校区(统一)
  245. const TimeSlots_one = [
  246. { number: 1, startTime: "08:20", endTime: "09:00" },
  247. { number: 2, startTime: "09:10", endTime: "09:50" },
  248. { number: 3, startTime: "10:00", endTime: "10:40" },
  249. { number: 4, startTime: "10:50", endTime: "11:30" },
  250. { number: 5, startTime: "13:30", endTime: "14:10" },
  251. { number: 6, startTime: "14:20", endTime: "15:00" },
  252. { number: 7, startTime: "15:10", endTime: "15:50" },
  253. { number: 8, startTime: "16:00", endTime: "16:40" },
  254. { number: 9, startTime: "18:40", endTime: "19:20" },
  255. { number: 10, startTime: "19:30", endTime: "20:10" },
  256. { number: 11, startTime: "20:20", endTime: "21:00" }
  257. ];
  258. // 白云校区
  259. const TimeSlots_two = [
  260. { number: 1, startTime: "08:30", endTime: "09:10" },
  261. { number: 2, startTime: "09:15", endTime: "09:55" },
  262. { number: 3, startTime: "10:05", endTime: "10:45" },
  263. { number: 4, startTime: "10:50", endTime: "11:30" },
  264. { number: 5, startTime: "13:30", endTime: "14:10" },
  265. { number: 6, startTime: "14:15", endTime: "14:55" },
  266. { number: 7, startTime: "15:05", endTime: "15:45" },
  267. { number: 8, startTime: "15:50", endTime: "16:30" },
  268. { number: 9, startTime: "18:40", endTime: "19:20" },
  269. { number: 10, startTime: "19:25", endTime: "20:05" },
  270. { number: 11, startTime: "20:10", endTime: "20:50" }
  271. ];
  272. // 河源校区
  273. const TimeSlots_three = [
  274. { number: 1, startTime: "08:20", endTime: "09:00" },
  275. { number: 2, startTime: "09:10", endTime: "09:50" },
  276. { number: 3, startTime: "10:10", endTime: "10:50" },
  277. { number: 4, startTime: "11:00", endTime: "11:40" },
  278. { number: 5, startTime: "13:50", endTime: "14:30" },
  279. { number: 6, startTime: "14:40", endTime: "15:20" },
  280. { number: 7, startTime: "15:40", endTime: "16:20" },
  281. { number: 8, startTime: "16:30", endTime: "17:10" },
  282. { number: 9, startTime: "18:40", endTime: "19:20" },
  283. { number: 10, startTime: "19:30", endTime: "20:10" },
  284. { number: 11, startTime: "20:20", endTime: "21:00" }
  285. ];
  286. // 根据校区索引获取对应时间表
  287. function getTimeSlotsByAreaIndex(areaIndex) {
  288. if (areaIndex === 0) return TimeSlots_one;
  289. if (areaIndex === 1) return TimeSlots_two;
  290. if (areaIndex === 2) return TimeSlots_three;
  291. // 默认返回第一个
  292. return TimeSlots_one;
  293. }
  294. async function importPresetTimeSlots(timeSlots) {
  295. console.log(`JS: 准备导入 ${timeSlots.length} 个预设时间段。`);
  296. if (timeSlots.length > 0) {
  297. window.shiguangBridge.showToast(`正在导入 ${timeSlots.length} 个预设时间段...`);
  298. try {
  299. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  300. window.shiguangBridge.showToast("预设时间段导入成功!");
  301. } catch (error) {
  302. window.shiguangBridge.showToast("导入时间段失败: " + error.message);
  303. console.error('JS: Save Time Slots Error:', error);
  304. }
  305. } else {
  306. window.shiguangBridge.showToast("警告:时间段为空,未导入时间段信息。");
  307. console.warn("JS: 警告:传入时间段为空,未导入时间段信息。");
  308. }
  309. }
  310. // ===================== 主流程 =====================
  311. async function runImportFlow() {
  312. const alertConfirmed = await promptUserToStart();
  313. if (!alertConfirmed) {
  314. window.shiguangBridge.showToast("用户取消了导入。");
  315. console.log("JS: 用户取消了导入流程。");
  316. return;
  317. }
  318. const academicYear = await getAcademicYear();
  319. if (academicYear === null) {
  320. window.shiguangBridge.showToast("导入已取消。");
  321. console.log("JS: 获取学年失败/取消,流程终止。");
  322. return;
  323. }
  324. console.log(`JS: 已选择学年: ${academicYear}`);
  325. const semesterIndex = await selectSemester();
  326. if (semesterIndex === null || isNaN(semesterIndex) || semesterIndex < 0) {
  327. window.shiguangBridge.showToast("导入已取消。");
  328. console.log("JS: 选择学期失败/取消,流程终止。");
  329. return;
  330. }
  331. console.log(`JS: 已选择学期索引: ${semesterIndex}`);
  332. const areaIndex = await selectArea();
  333. if (areaIndex === null || isNaN(areaIndex) || areaIndex < 0) {
  334. window.shiguangBridge.showToast("导入已取消。");
  335. console.log("JS: 选择校区失败/取消,流程终止。");
  336. return;
  337. }
  338. console.log(`JS: 已选择校区索引: ${areaIndex}`);
  339. const startDate = await getSemesterStartDate();
  340. console.log(`JS: 学期开始日期输入结果: ${startDate}`);
  341. const result = await fetchAndParseCourses(academicYear, semesterIndex);
  342. if (result === null) {
  343. console.log("JS: 课程获取或解析失败,流程终止。");
  344. return;
  345. }
  346. result.config.semesterStartDate = startDate;
  347. const { courses, config } = result;
  348. const saveResult = await saveCourses(courses);
  349. if (!saveResult) {
  350. console.log("JS: 课程保存失败,流程终止。");
  351. return;
  352. }
  353. try {
  354. await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
  355. window.shiguangBridge.showToast(`课表配置更新成功!总周数:${config.semesterTotalWeeks}周。`);
  356. } catch (error) {
  357. window.shiguangBridge.showToast(`课表配置保存失败: ${error.message}`);
  358. console.error('JS: Save Config Error:', error);
  359. }
  360. // 根据校区选择对应时间表并导入
  361. const timeSlots = getTimeSlotsByAreaIndex(areaIndex);
  362. await importPresetTimeSlots(timeSlots);
  363. window.shiguangBridge.showToast(`课程导入成功,共导入 ${courses.length} 门课程!`);
  364. console.log("JS: 整个导入流程执行完毕并成功。");
  365. window.shiguangBridge.notifyTaskCompletion();
  366. }
  367. // 启动导入流程,并添加全局异常捕获
  368. runImportFlow().catch(e => {
  369. console.error('JS: 导入流程异常:', e);
  370. try {
  371. window.shiguangBridge.showToast('导入失败:' + (e && e.message ? e.message : e));
  372. } catch (_) {
  373. // 忽略
  374. }
  375. });