school.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. /**
  2. * 济南大学教务适配
  3. * @since 2026-3-13
  4. * @description 支持班级课表和个人课表的导入
  5. * @author Moyu
  6. * @version 1.0
  7. */
  8. class CourseModel {
  9. name = ""; // 课程名称 (String)
  10. teacher = ""; // 教师姓名 (String)
  11. position = ""; // 上课地点 (String)
  12. day = 0; //星期几 (Int, 1=周一, 7=周日)
  13. startSection = 0; // 开始节次 (Int, 如果 isCustomTime 为 false 或未提供,则必填)
  14. endSection = 0; // 结束节次 (Int, 如果 isCustomTime 为 false 或未提供,则必填)
  15. weeks = [0]; // 上课周数 (Int Array, 必须是数字数组,例如 [1, 3, 5, 7])
  16. isCustomTime = false; // 是否使用自定义时间 (Boolean, 可选,默认为 false。如果为 true,则 customStartTime 和 customEndTime 必填;如果为 false 或未提供,则 startSection 和 endSection 必填)
  17. customStartTime = ""; // 自定义开始时间 (String, 格式 HH:mm, 如果 isCustomTime 为 true 则必填)
  18. customEndTime = ""; // 自定义结束时间 (String, 格式 HH:mm, 如果 isCustomTime 为 true 则必填)
  19. constructor(
  20. name, // 课程名称 (String)
  21. teacher, // 教师姓名 (String)
  22. position, // 上课地点 (String)
  23. day, // 星期几 (Int, 1=周一,7=周日)
  24. startSection, // 开始节次 (Int)
  25. endSection, // 结束节次 (Int)
  26. weeks = [], // 上课周数 (Int Array)
  27. isCustomTime = false, // 是否自定义时间 (Boolean,默认false)
  28. customStartTime = "", // 自定义开始时间 (可选)
  29. customEndTime = "", // 自定义结束时间 (可选)
  30. ) {
  31. // 1. 基础字段赋值(必选参数)
  32. this.name = name;
  33. this.teacher = teacher;
  34. this.position = position;
  35. this.day = day;
  36. this.startSection = startSection;
  37. this.endSection = endSection;
  38. this.weeks = weeks;
  39. this.isCustomTime = isCustomTime;
  40. this.customStartTime = customStartTime;
  41. this.customEndTime = customEndTime;
  42. }
  43. }
  44. class CustomTimeModel {
  45. number = 0;
  46. startTime = ""; // 开始时间 (String, 格式 HH:mm)
  47. endTime = ""; // 结束时间 (String, 格式 HH:mm)
  48. constructor(num, start, end) {
  49. this.number = num;
  50. this.startTime = start;
  51. this.endTime = end;
  52. }
  53. }
  54. const urlPersonnalClassTable =
  55. "jwgl.ujn.edu.cn/jwglxt/kbcx/xskbcx_cxXskbcxIndex.html";
  56. const urlClassTable = "jwgl.ujn.edu.cn/jwglxt/kbdy/bjkbdy_cxBjkbdyIndex.html";
  57. //解析周数据
  58. function parseWeekText(text) {
  59. if (!text) return [];
  60. text = text.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, "").trim();
  61. const allWeeks = new Set();
  62. const noJie = text.replace(/\(\d+-\d+节\)/g, " ");
  63. const rangePattern = /(\d+)-(\d+)周(?:\((单|双)\))?/g;
  64. const singlePattern = /(\d+)周(?:\((单|双)\))?/g;
  65. let match;
  66. while ((match = rangePattern.exec(noJie)) !== null) {
  67. const [, start, end, type] = match;
  68. for (let w = parseInt(start, 10); w <= parseInt(end, 10); w++) {
  69. if (
  70. !type ||
  71. (type === "单" && w % 2 === 1) ||
  72. (type === "双" && w % 2 === 0)
  73. ) {
  74. allWeeks.add(w);
  75. }
  76. }
  77. }
  78. const processedRanges = [];
  79. rangePattern.lastIndex = 0;
  80. while ((match = rangePattern.exec(noJie)) !== null) {
  81. processedRanges.push({
  82. start: match.index,
  83. end: match.index + match[0].length,
  84. });
  85. }
  86. singlePattern.lastIndex = 0;
  87. while ((match = singlePattern.exec(noJie)) !== null) {
  88. const weekNum = parseInt(match[1], 10);
  89. const type = match[2];
  90. // 检查这个匹配是否已经被范围正则匹配过了(简单判断:如果包含"-"就跳过)
  91. const matchStr = match[0];
  92. if (matchStr.includes("-")) continue;
  93. if (
  94. !type ||
  95. (type === "单" && weekNum % 2 === 1) ||
  96. (type === "双" && weekNum % 2 === 0)
  97. ) {
  98. allWeeks.add(weekNum);
  99. }
  100. }
  101. return [...allWeeks].sort((a, b) => a - b);
  102. }
  103. function offsetColByRow(row) {
  104. row = row - 2;
  105. if (row % 4 == 0) {
  106. return 0;
  107. }
  108. if (row % 4 == 2) {
  109. return 1;
  110. }
  111. }
  112. function analyzeCourseModel(item, flag) {
  113. let td = item.closest("td");
  114. let elements = item.querySelectorAll("p");
  115. if (!td) {
  116. console.error("找不到单元格");
  117. return null;
  118. }
  119. let tr = td.parentElement;
  120. let site = {
  121. row: tr.rowIndex, //第几行
  122. rowSpan: td.rowSpan || 1, //跨几行
  123. col: td.cellIndex, //第几列
  124. colSpan: td.colSpan || 1, //跨几列
  125. cell: td, //本身
  126. };
  127. let currentItem = item.querySelector(".title");
  128. let name = currentItem.textContent;
  129. let teacher;
  130. let position;
  131. let weeks;
  132. if (flag == 1) {
  133. teacher = elements[2].lastElementChild.innerText;
  134. position = elements[1].lastElementChild.innerText;
  135. weeks = parseWeekText(elements[0].lastElementChild.innerText);
  136. } else {
  137. if (elements.length != 1) {
  138. teacher =
  139. elements[4].firstElementChild.nextSibling.textContent.split("(")[0];
  140. position = elements[3].firstElementChild.nextSibling.textContent;
  141. weeks = parseWeekText(
  142. elements[2].firstElementChild.nextSibling.textContent,
  143. );
  144. } else {
  145. teacher = "";
  146. position = "";
  147. weeks = parseWeekText("1-20周");
  148. }
  149. }
  150. return new CourseModel(
  151. name.replace(/[■☆★◆]/g, ""),
  152. teacher.trim(),
  153. position.trim(),
  154. site.col - 1 + offsetColByRow(site.row),
  155. site.row - 1,
  156. site.row + site.rowSpan - 2,
  157. [...weeks],
  158. );
  159. }
  160. async function saveCourses() {
  161. let flag = null;
  162. let elements = [];
  163. if (window.location.href.includes(urlPersonnalClassTable)) {
  164. elements = document.querySelectorAll(
  165. "#innerContainer #table1 div.timetable_con",
  166. );
  167. flag = 1;
  168. } else {
  169. if (window.location.href.includes(urlClassTable)) {
  170. elements = document.querySelectorAll(
  171. "#table1.tab-pane>.timetable1 div.timetable_con",
  172. );
  173. flag = 0;
  174. }
  175. }
  176. let courseModels = [];
  177. elements.forEach((item) => {
  178. let course = analyzeCourseModel(item, flag);
  179. if (course) {
  180. courseModels.push({ ...course });
  181. }
  182. });
  183. try {
  184. await window.AndroidBridgePromise.saveImportedCourses(
  185. JSON.stringify(courseModels),
  186. );
  187. return courseModels.length;
  188. } catch (error) {
  189. console.error("保存课程失败:", error);
  190. window.AndroidBridge.showToast("保存课程失败,请重试");
  191. return 0;
  192. }
  193. }
  194. async function checkEnvirenment() {
  195. const nowSite = window.location.href;
  196. const tableType = ["班级课表", "个人课表"];
  197. if (
  198. !nowSite.includes(urlPersonnalClassTable) &&
  199. !nowSite.includes(urlClassTable)
  200. ) {
  201. window.AndroidBridge.showToast("当前页面不在支持的导入范围内");
  202. const selectedOption =
  203. await window.AndroidBridgePromise.showSingleSelection(
  204. "现在不在可导入的页面中,请选择导入班级课表还是个人课表,之后并确保打开具体课程页面",
  205. JSON.stringify(tableType), // 必须是 JSON 字符串
  206. -1, // 默认不选中
  207. );
  208. if (selectedOption === 0) {
  209. clickMenu(
  210. "N214505",
  211. "/kbdy/bjkbdy_cxBjkbdyIndex.html",
  212. "班级课表查询",
  213. "null",
  214. );
  215. return false;
  216. } else if (selectedOption === 1) {
  217. clickMenu(
  218. "N253508",
  219. "/kbcx/xskbcx_cxXskbcxIndex.html",
  220. "个人课表",
  221. "null",
  222. );
  223. return false;
  224. } else {
  225. return false;
  226. }
  227. }
  228. return true;
  229. }
  230. async function runImportFlow() {
  231. window.AndroidBridge.showToast("课程导入流程即将开始...");
  232. if (!(await checkEnvirenment())) return;
  233. const savedCourseCount = await saveCourses();
  234. if (!savedCourseCount) {
  235. return;
  236. }
  237. const slots = [
  238. new CustomTimeModel(1, "08:00", "08:50"),
  239. new CustomTimeModel(2, "08:55", "09:45"),
  240. new CustomTimeModel(3, "10:15", "11:05"),
  241. new CustomTimeModel(4, "11:10", "12:00"),
  242. new CustomTimeModel(5, "14:00", "14:50"),
  243. new CustomTimeModel(6, "14:55", "15:45"),
  244. new CustomTimeModel(7, "16:15", "17:05"),
  245. new CustomTimeModel(8, "17:10", "18:00"),
  246. new CustomTimeModel(9, "19:00", "19:50"),
  247. new CustomTimeModel(10, "19:55", "20:45"),
  248. new CustomTimeModel(11, "20:50", "21:45"),
  249. ];
  250. try {
  251. await window.AndroidBridgePromise.savePresetTimeSlots(
  252. JSON.stringify(slots),
  253. );
  254. } catch (error) {
  255. console.error("保存时间段失败:", error);
  256. window.AndroidBridge.showToast("保存时间段失败,请重试");
  257. return;
  258. }
  259. // 8. 流程**完全成功**,发送结束信号。
  260. AndroidBridge.showToast(`导入成功:共 ${savedCourseCount} 门课程`);
  261. AndroidBridge.notifyTaskCompletion();
  262. }
  263. // 启动导入流程
  264. runImportFlow();