urp_01.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. // 通用 URP 教务拾光课程表适配脚本
  2. /**
  3. * 解析复杂的周次文本
  4. * 示例: "1-8,10-17周" -> [1,2,3,4,5,6,7,8,10,11,12,13,14,15,16,17]
  5. * 示例: "1-5,7-8,10-12周双周,13-17周" -> 自动过滤单双周
  6. */
  7. function parseWeekText(weekStr) {
  8. let weeks = new Set();
  9. if (!weekStr) return [];
  10. // 清理无用后缀,按逗号或分号切割
  11. const rawSegments = weekStr.replace(/周/g, '').split(/[,,;;]/);
  12. rawSegments.forEach(segment => {
  13. let isEven = segment.includes('双');
  14. let isOdd = segment.includes('单');
  15. let cleanSegment = segment.replace(/(单|双)/g, '').replace(/第/g, '').trim();
  16. if (cleanSegment.includes('-')) {
  17. const [start, end] = cleanSegment.split('-').map(Number);
  18. if (!isNaN(start) && !isNaN(end)) {
  19. for (let i = start; i <= end; i++) {
  20. if (isEven && i % 2 !== 0) continue;
  21. if (isOdd && i % 2 === 0) continue;
  22. weeks.add(i);
  23. }
  24. }
  25. } else {
  26. const num = Number(cleanSegment);
  27. if (!isNaN(num) && num > 0) {
  28. weeks.add(num);
  29. }
  30. }
  31. });
  32. return Array.from(weeks).sort((a, b) => a - b);
  33. }
  34. /**
  35. * 解析节次文本
  36. */
  37. function parseSectionText(sectionStr) {
  38. let startSection = 1;
  39. let endSection = 1;
  40. if (!sectionStr) return { startSection, endSection };
  41. const match = sectionStr.match(/(\d+)-(\d+)节?/);
  42. if (match) {
  43. startSection = parseInt(match[1]);
  44. endSection = parseInt(match[2]);
  45. } else {
  46. const singleMatch = sectionStr.match(/(\d+)节?/);
  47. if (singleMatch) {
  48. startSection = parseInt(singleMatch[1]);
  49. endSection = parseInt(singleMatch[1]);
  50. }
  51. }
  52. return { startSection, endSection };
  53. }
  54. /**
  55. * 从表格中动态解析时间段信息 (寻找 id="0_x" 的 th)
  56. */
  57. function parseTimeSlots() {
  58. let timeSlots = [];
  59. const timeThs = document.querySelectorAll('th[id^="0_"]');
  60. timeThs.forEach(th => {
  61. const idParts = th.id.split('_');
  62. const sectionNumber = parseInt(idParts[1]); // 获取节次序号
  63. const text = th.textContent || "";
  64. // 匹配格式如 "(08:00-08:45)"
  65. const timeMatch = text.match(/\((\d{2}:\d{2})-(\d{2}:\d{2})\)/);
  66. if (timeMatch && !isNaN(sectionNumber)) {
  67. timeSlots.push({
  68. number: sectionNumber,
  69. startTime: timeMatch[1],
  70. endTime: timeMatch[2]
  71. });
  72. }
  73. });
  74. return timeSlots.sort((a, b) => a.number - b.number);
  75. }
  76. /**
  77. * 核心解析:基于 HTML DOM 结构解析课程数据
  78. */
  79. async function fetchAndParseJwData() {
  80. try {
  81. window.shiguangBridge.showToast("正在解析网页课表...");
  82. let courses = [];
  83. // 获取所有带有有效 id (格式如 2_5) 且内部包含课程块的单元格
  84. const allTds = document.querySelectorAll('td[id*="_"]');
  85. allTds.forEach(td => {
  86. const idParts = td.id.split('_');
  87. if (idParts.length !== 2) return;
  88. // _前面从1到7代表星期
  89. const day = parseInt(idParts[0]);
  90. if (isNaN(day) || day < 1 || day > 7) return;
  91. // 找到单元格内所有的课程卡片
  92. const classDivs = td.querySelectorAll('.class_div');
  93. classDivs.forEach(div => {
  94. const pTags = div.querySelectorAll('p');
  95. if (pTags.length < 5) return; // 格式不健全的格子直接跳过
  96. // 兼容两种结构:
  97. // p[1]=教师,p[2]=周次,p[3]=节次,p[4]=地点
  98. // p[2]=教师,p[3]=周次,p[4]=节次,p[5]=地点
  99. let teacherIdx = 2, weekIdx = 3, sectionIdx = 4, posIdx = 5;
  100. const p1Text = pTags[1] ? pTags[1].textContent.trim() : '';
  101. if (p1Text && !/\d/.test(p1Text) && !p1Text.includes('周') && p1Text.length < 10) {
  102. teacherIdx = 1;
  103. weekIdx = 2;
  104. sectionIdx = 3;
  105. posIdx = 4;
  106. }
  107. const name = pTags[0].textContent.trim();
  108. const teacher = pTags[teacherIdx].textContent.replace(/^[\s*]+|[\s*]+$/g, '').replace(/\*/g, ' ').replace(/\s+/g, ' ');
  109. const weekStr = pTags[weekIdx].textContent.trim();
  110. const sectionStr = pTags[sectionIdx].textContent.trim();
  111. const position = pTags[posIdx] ? pTags[posIdx].textContent.trim() : "未知地点";
  112. // 解析周次与真实的开始/结束节次
  113. const weeks = parseWeekText(weekStr);
  114. const { startSection, endSection } = parseSectionText(sectionStr);
  115. if (name && weeks.length > 0) {
  116. courses.push({
  117. name: name,
  118. teacher: teacher,
  119. position: position,
  120. day: day,
  121. startSection: startSection,
  122. endSection: endSection,
  123. weeks: weeks
  124. });
  125. }
  126. });
  127. });
  128. // 动态提取时间段
  129. const timeSlots = parseTimeSlots();
  130. if (courses.length === 0) {
  131. throw new Error("未能在当前页面检测到有效的课表数据,请确认是否处于课表视图页面");
  132. }
  133. return { courses, timeSlots };
  134. } catch (e) {
  135. console.error("HTML解析失败详情:", e);
  136. window.shiguangBridge.showToast("同步失败: " + e.message);
  137. return null;
  138. }
  139. }
  140. /**
  141. * 辅助:保存数据到外部 APP
  142. */
  143. async function saveToApp(result) {
  144. const courseSuccess = await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(result.courses));
  145. if (!courseSuccess) return false;
  146. if (result.timeSlots && result.timeSlots.length > 0) {
  147. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(result.timeSlots));
  148. }
  149. return true;
  150. }
  151. /**
  152. * 流程控制流程
  153. */
  154. async function runImportFlow() {
  155. const alertResult = await window.shiguangBridgePromise.showAlert(
  156. "教务网页课表导入",
  157. "请确保您当前的网页已加载出课表视图后再开始导入",
  158. "开始同步"
  159. );
  160. if (!alertResult) return;
  161. const result = await fetchAndParseJwData();
  162. if (!result || result.courses.length === 0) return;
  163. if (await saveToApp(result)) {
  164. window.shiguangBridge.showToast(`成功从网页导入 ${result.courses.length} 个课程时段`);
  165. window.shiguangBridge.notifyTaskCompletion();
  166. }
  167. }
  168. // 启动导入流程
  169. runImportFlow();