nynu.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. /**
  2. * 南阳师范学院教务系统课表导入适配脚本
  3. * 青果软件系统,通过 showLessonScheduleInfosV14.action 接口获取课表
  4. * 课程信息分布在 id 形如 weekly0X_Y 的 div 中(X 为星期,Y 为节次单元)
  5. */
  6. function parseWeeks(weekStr) {
  7. // 兼容 "1-2,5-18" 逗号分段 + 区间
  8. const weeks = [];
  9. weekStr.split(',').forEach(part => {
  10. part = part.trim();
  11. if (part.includes('-')) {
  12. const dashSegs = part.split('-').map(Number);
  13. if (dashSegs.length === 2 && !isNaN(dashSegs[0]) && !isNaN(dashSegs[1])) {
  14. for (let i = dashSegs[0]; i <= dashSegs[1]; i++) weeks.push(i);
  15. }
  16. } else if (!isNaN(Number(part))) {
  17. weeks.push(Number(part));
  18. }
  19. });
  20. return [...new Set(weeks)].sort((a, b) => a - b);
  21. }
  22. /**
  23. * 解析单个 weekly 块里的 li 文本,返回课程信息
  24. */
  25. function parseWeeklyDiv(divId, liTexts) {
  26. // id 形如 weekly03_1 -> 星期=3,节次单元=1
  27. const m = divId.match(/^weekly0?(\d+)_(\d+)$/);
  28. if (!m) return null;
  29. const day = parseInt(m[1], 10);
  30. if (day < 1 || day > 7) return null;
  31. let name = "", teacher = "", timeText = "", position = "";
  32. for (const t of liTexts) {
  33. const str = t.trim();
  34. if (str.startsWith("课程名称")) name = str.replace(/^课程名称:/, "").replace(/^<b>/, "").replace(/<\/b>$/, "");
  35. else if (str.startsWith("任课教师")) teacher = str.replace(/^任课教师:/, "").replace(/^<b>/, "").replace(/<\/b>$/, "");
  36. else if (str.startsWith("上课时间")) timeText = str.replace(/^上课时间:/, "").replace(/^<b>/, "").replace(/<\/b>$/, "");
  37. else if (str.startsWith("上课地点")) position = str.replace(/^上课地点:/, "").replace(/^<b>/, "").replace(/<\/b>$/, "");
  38. }
  39. if (!name || !timeText) return null;
  40. // 上课时间形如:[1-2,5-18周] 三[1-2节]
  41. const weekMatch = timeText.match(/\[([\d,\-]+)周\]/);
  42. const sectionMatch = timeText.match(/\[(\d+)(?:-(\d+))?节\]/);
  43. if (!weekMatch || !sectionMatch) return null;
  44. const weeks = parseWeeks(weekMatch[1]);
  45. const startSection = parseInt(sectionMatch[1], 10);
  46. const endSection = sectionMatch[2] ? parseInt(sectionMatch[2], 10) : startSection;
  47. if (weeks.length === 0) return null;
  48. return {
  49. name: name,
  50. teacher: teacher || "未知教师",
  51. position: position || "未知地点",
  52. day: day,
  53. startSection: startSection,
  54. endSection: endSection,
  55. weeks: weeks
  56. };
  57. }
  58. /**
  59. * 从接口返回的 HTML 中提取所有 weekly div 并解析课程
  60. */
  61. function transformSchedule(htmlString) {
  62. console.log("JS: transformSchedule 正在解析 HTML...");
  63. const tempDoc = new DOMParser().parseFromString(htmlString, "text/html");
  64. // 只取包裹课程详情的 weekly div(跳过表格等内容)
  65. const weekDivs = Array.from(tempDoc.querySelectorAll('[id^="weekly"]'))
  66. .filter(d => d.classList.contains("weeklesson"));
  67. console.log(`JS: 找到 ${weekDivs.length} 个 weekly 课程块`);
  68. const rawCourses = [];
  69. weekDivs.forEach(div => {
  70. const divId = div.id;
  71. const lis = Array.from(div.querySelectorAll("ul li"));
  72. const liTexts = lis.map(li => li.innerHTML);
  73. const course = parseWeeklyDiv(divId, liTexts);
  74. if (course) rawCourses.push(course);
  75. });
  76. console.log(`JS: 解析出 ${rawCourses.length} 条课程记录`);
  77. // 青果系统同一门课会因节次格/周次分段生成重复的 weekly 块,
  78. // 按组合键去重(同一课程、同时段、同周次只保留一条)。
  79. const seen = new Set();
  80. const courses = [];
  81. for (const c of rawCourses) {
  82. const key = `${c.name}|${c.day}|${c.startSection}|${c.endSection}|${c.weeks.join(',')}|${c.teacher}|${c.position}`;
  83. if (seen.has(key)) continue;
  84. seen.add(key);
  85. courses.push(c);
  86. }
  87. console.log(`JS: 去重后剩 ${courses.length} 门课程`);
  88. return courses;
  89. }
  90. function isLoginPage() {
  91. const url = window.location.href;
  92. return url.includes('login') || url.includes('cas');
  93. }
  94. function validateYearInput(input) {
  95. if (/^[0-9]{4}$/.test(input)) return false;
  96. return "请输入四位数字的学年!";
  97. }
  98. async function promptUserToStart() {
  99. console.log("JS: 流程开始:显示公告。");
  100. return await window.shiguangBridgePromise.showAlert(
  101. "教务系统课表导入",
  102. "导入前请确保您已在浏览器中成功登录教务系统",
  103. "好的,开始导入"
  104. );
  105. }
  106. async function getAcademicYear() {
  107. const currentYear = new Date().getFullYear().toString();
  108. return await window.shiguangBridgePromise.showPrompt(
  109. "选择学年",
  110. "请输入要导入课程的起始学年(例如 2025-2026 应输入2025):",
  111. currentYear,
  112. "validateYearInput"
  113. );
  114. }
  115. async function selectSemester() {
  116. const semesters = ["第一学期 (0)", "第二学期 (1)"];
  117. const semesterIndex = await window.shiguangBridgePromise.showSingleSelection(
  118. "选择学期",
  119. JSON.stringify(semesters),
  120. 0
  121. );
  122. return semesterIndex;
  123. }
  124. async function fetchAndParseCourses(academicYear, semesterIndex) {
  125. window.shiguangBridge.showToast("正在请求课表数据...");
  126. const semesterCode = semesterIndex === 0 ? "0" : "1";
  127. // 从当前页面 URL 提取 vpn-12-o1-{源站} 形式的后缀(如 nysyjw.nynu.edu.cn),
  128. // 不同登录会话源站域名可能不同;没匹配到时回退到教务源站固定值。
  129. const originMatch = window.location.href.match(/vpn-12-o1-([a-zA-Z0-9.\-]+(?:\.edu\.cn)?)/);
  130. const originHost = originMatch ? originMatch[1] : "nysyjw.nynu.edu.cn";
  131. const baseUrl = "https://vpn.nynu.edu.cn/http/77726476706e69737468656265737421feee52852d27265e67069ce29d51367b58d2/nysfjw/frame/desk/showLessonScheduleInfosV14.action";
  132. const url = `${baseUrl}?vpn-12-o1-${originHost}&xn=${academicYear}&xq=${semesterCode}`;
  133. console.log(`JS: 请求课表接口: ${url}`);
  134. try {
  135. const response = await fetch(url, {
  136. method: "GET",
  137. credentials: "include"
  138. });
  139. if (!response.ok) {
  140. throw new Error(`网络请求失败。状态码: ${response.status}`);
  141. }
  142. const htmlText = await response.text();
  143. const courses = transformSchedule(htmlText);
  144. if (courses.length === 0) {
  145. window.shiguangBridge.showToast("未找到任何课程数据,请检查所选学年学期是否正确。");
  146. return null;
  147. }
  148. console.log(`JS: 课程数据解析成功,共找到 ${courses.length} 门课程。`);
  149. return { courses };
  150. } catch (error) {
  151. window.shiguangBridge.showToast(`请求或解析失败: ${error.message}`);
  152. console.error('JS: Fetch/Parse Error:', error);
  153. return null;
  154. }
  155. }
  156. async function saveCourses(parsedCourses) {
  157. window.shiguangBridge.showToast(`正在保存 ${parsedCourses.length} 门课程...`);
  158. try {
  159. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(parsedCourses, null, 2));
  160. return true;
  161. } catch (error) {
  162. window.shiguangBridge.showToast(`课程保存失败: ${error.message}`);
  163. console.error('JS: Save Courses Error:', error);
  164. return false;
  165. }
  166. }
  167. async function runImportFlow() {
  168. if (isLoginPage()) {
  169. window.shiguangBridge.showToast("导入失败:请先登录教务系统!");
  170. return;
  171. }
  172. const alertConfirmed = await promptUserToStart();
  173. if (!alertConfirmed) {
  174. window.shiguangBridge.showToast("用户取消了导入。");
  175. return;
  176. }
  177. const academicYear = await getAcademicYear();
  178. if (academicYear === null) {
  179. window.shiguangBridge.showToast("导入已取消。");
  180. return;
  181. }
  182. const semesterIndex = await selectSemester();
  183. if (semesterIndex === null || semesterIndex === -1) {
  184. window.shiguangBridge.showToast("导入已取消。");
  185. return;
  186. }
  187. const result = await fetchAndParseCourses(academicYear, semesterIndex);
  188. if (result === null) {
  189. return;
  190. }
  191. const { courses } = result;
  192. const saveResult = await saveCourses(courses);
  193. if (!saveResult) {
  194. return;
  195. }
  196. window.shiguangBridge.showToast(`课程导入成功,共导入 ${courses.length} 门课程!`);
  197. window.shiguangBridge.notifyTaskCompletion();
  198. }
  199. runImportFlow();