zhengfang_01.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. // 基于 HTML 页面抓取的拾光课表正方适配脚本
  2. /**
  3. * 解析表格
  4. */
  5. function parserTbale() {
  6. const regexName = /[●★○]/g;
  7. const courseInfoList = [];
  8. document.querySelectorAll('#kbgrid_table_0 td').forEach((td) => {
  9. if (td.classList.contains('td_wrap') && td.textContent.trim() !== '') {
  10. const day = parseInt(td.getAttribute('id').split('-')[0]);
  11. td.querySelectorAll('.timetable_con.text-left').forEach((course) => {
  12. const name = [...course.querySelectorAll('.title font')].map(e => e.textContent).join('').replace(regexName, '').trim();
  13. const ps = course.querySelectorAll('p');
  14. const infoStr = ps[0]?.querySelectorAll('font[color]')[1]?.textContent.trim() || '';
  15. const position = ps[1] ? [...ps[1].querySelectorAll('font[color]')].map(e => e.textContent).join('').trim() : '';
  16. const teacherRaw = ps[2] ? [...ps[2].querySelectorAll('font[color]')].map(e => e.textContent).join('').trim() : '';
  17. const teacher = teacherRaw.replace(/[\((][^\))]*[\))]/g, '').trim();
  18. if (infoStr && infoStr.match(/\((\d+-\d+节)\)/) && infoStr.split('节)')[1]) {
  19. const [sections, weeks] = parserInfo(infoStr);
  20. if (name && position && teacher && sections.length && weeks.length) {
  21. const startSection = sections[0];
  22. const endSection = sections[sections.length - 1];
  23. const finalPosition = position.split(/\s+/).pop();
  24. const data = { name, day, weeks, teacher, position: finalPosition, startSection, endSection };
  25. courseInfoList.push(data);
  26. }
  27. }
  28. });
  29. }
  30. });
  31. return courseInfoList;
  32. }
  33. /**
  34. * 解析列表
  35. */
  36. function parserList() {
  37. const regexName = /[●★○]/g;
  38. const regexWeekNum = /周数:|周/g;
  39. const regexPosition = /上课地点:/g;
  40. const regexTeacher = /教师 :/g;
  41. let courseInfoList = [];
  42. document.querySelectorAll('#kblist_table tbody').forEach((tbody, day) => {
  43. if (day > 0 && day < 8) {
  44. let sections;
  45. tbody.querySelectorAll('tr:not(:first-child)').forEach((tr) => {
  46. let name, font;
  47. if (tr.querySelectorAll('td').length > 1) {
  48. const firstTd = tr.querySelector('td:first-child');
  49. sections = parserSections(firstTd.textContent);
  50. const secondTd = tr.querySelector('td:nth-child(2)');
  51. name = [...secondTd.querySelectorAll('.title')].map(e => e.textContent).join('').replace(regexName, '').trim();
  52. font = secondTd.querySelectorAll('p font');
  53. } else {
  54. const td = tr.querySelector('td');
  55. name = [...td.querySelectorAll('.title')].map(e => e.textContent).join('').replace(regexName, '').trim();
  56. font = td.querySelectorAll('p font');
  57. }
  58. const weekStr = font[0] ? font[0].textContent.replace(regexWeekNum, '').trim() : '';
  59. const weeks = parserWeeks(weekStr);
  60. const positionRaw = font[1] ? font[1].textContent.replace(regexPosition, '').trim() : '';
  61. const finalPosition = positionRaw.split(/\s+/).pop();
  62. const teacherRaw = font[2] ? font[2].textContent.replace(regexTeacher, '').trim() : '';
  63. const teacher = teacherRaw.replace(/[\((][^\))]*[\))]/g, '').trim();
  64. if (name && sections && weeks.length && teacher && finalPosition) {
  65. const startSection = sections[0];
  66. const endSection = sections[sections.length - 1];
  67. const data = {
  68. name,
  69. day,
  70. weeks,
  71. teacher,
  72. position: finalPosition,
  73. startSection,
  74. endSection
  75. };
  76. courseInfoList.push(data);
  77. }
  78. });
  79. }
  80. });
  81. return courseInfoList;
  82. }
  83. /**
  84. * 解析课程信息
  85. */
  86. function parserInfo(str) {
  87. const sections = parserSections(str.match(/\((\d+-\d+节)\)/)[1].replace(/节/g, ''));
  88. const weekStrWithMarker = str.split('节)')[1];
  89. const weeks = parserWeeks(weekStrWithMarker.replace(/周/g, '').trim());
  90. return [sections, weeks];
  91. }
  92. /**
  93. * 解析节次
  94. */
  95. function parserSections(str) {
  96. const [start, end] = str.split('-').map(Number);
  97. if (isNaN(start) || isNaN(end) || start > end) return [];
  98. return Array.from({ length: end - start + 1 }, (_, i) => start + i);
  99. }
  100. /**
  101. * 解析周次
  102. */
  103. function parserWeeks(str) {
  104. const segments = str.split(',');
  105. let weeks = [];
  106. const segmentRegex = /(\d+)(?:-(\d+))?\s*(\([单双]\))?/g;
  107. for (const segment of segments) {
  108. const cleanSegment = segment.replace(/周/g, '').trim();
  109. segmentRegex.lastIndex = 0;
  110. let match;
  111. while ((match = segmentRegex.exec(cleanSegment)) !== null) {
  112. const start = parseInt(match[1]);
  113. const end = match[2] ? parseInt(match[2]) : start;
  114. const flagStr = match[3] || '';
  115. let flag = 0;
  116. if (flagStr.includes('单')) {
  117. flag = 1;
  118. } else if (flagStr.includes('双')) {
  119. flag = 2;
  120. }
  121. for (let i = start; i <= end; i++) {
  122. if (flag === 1 && i % 2 !== 1) continue;
  123. if (flag === 2 && i % 2 !== 0) continue;
  124. if (!weeks.includes(i)) {
  125. weeks.push(i);
  126. }
  127. }
  128. }
  129. }
  130. return weeks.sort((a, b) => a - b);
  131. }
  132. async function scrapeAndParseCourses() {
  133. window.shiguangBridge.showToast("正在检查页面并抓取课程数据...");
  134. const ts = `1.登陆教务系统\n2.导航到学生课表查询页面\n3.等待课表信息加载,选择对应学年、学期,确认无误后点击【查询】\n4.确保页面上显示了课程表\n5.点击下方【一键导入】`
  135. try {
  136. const response = await fetch(window.location.href);
  137. const text = await response.text();
  138. if (!text.includes("课表查询")) {
  139. console.log("页面内容检查失败!");
  140. await window.shiguangBridgePromise.showAlert("导入失败", "当前页面似乎不是学生课表查询页面。请检查:\n" + ts, "确定");
  141. return null;
  142. }
  143. const typeElement = document.querySelector('#shcPDF');
  144. if (!typeElement) {
  145. console.log("未能找到视图类型元素 (#shcPDF)");
  146. await window.shiguangBridgePromise.showAlert("导入失败", "未能识别课表视图类型,请确认您已点击查询且课表已加载完毕。", "确定");
  147. return null;
  148. }
  149. const type = typeElement.dataset['type'];
  150. const tableElement = document.querySelector(type === 'list' ? '#kblist_table' : '#kbgrid_table_0');
  151. if (!tableElement) {
  152. console.log("未能找到课表主体 HTML");
  153. await window.shiguangBridgePromise.showAlert("导入失败", `未能找到课表主体 (${type} 视图),请确认您已点击查询且课表已加载完毕。`, "确定");
  154. return null;
  155. }
  156. let result = [];
  157. if (type === 'list') {
  158. result = parserList();
  159. } else {
  160. result = parserTbale();
  161. }
  162. if (result.length === 0) {
  163. window.shiguangBridge.showToast("未找到任何课程数据,请检查所选学年学期是否正确或本学期无课。");
  164. return null;
  165. }
  166. console.log(`JS: 课程数据解析成功,共找到 ${result.length} 门课程。`);
  167. return { courses: result };
  168. } catch (error) {
  169. window.shiguangBridge.showToast(`抓取或解析失败: ${error.message}`);
  170. console.error('JS: Scrape/Parse Error:', error);
  171. await window.shiguangBridgePromise.showAlert("抓取或解析失败", `发生错误:${error.message}。请重试或联系开发者。`, "确定");
  172. return null;
  173. }
  174. }
  175. async function saveCourses(parsedCourses) {
  176. window.shiguangBridge.showToast(`正在保存 ${parsedCourses.length} 门课程...`);
  177. console.log(`JS: 尝试保存 ${parsedCourses.length} 门课程...`);
  178. try {
  179. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(parsedCourses, null, 2));
  180. console.log("JS: 课程保存成功!");
  181. return true;
  182. } catch (error) {
  183. window.shiguangBridge.showToast(`课程保存失败: ${error.message}`);
  184. console.error('JS: Save Courses Error:', error);
  185. return false;
  186. }
  187. }
  188. async function runImportFlow() {
  189. const alertConfirmed = await window.shiguangBridgePromise.showAlert(
  190. "教务系统课表导入",
  191. "导入前请确保您已在浏览器中成功登录教务系统,并处于课表查询页面且已点击查询。",
  192. "好的,开始导入"
  193. );
  194. if (!alertConfirmed) {
  195. window.shiguangBridge.showToast("用户取消了导入。");
  196. return;
  197. }
  198. const result = await scrapeAndParseCourses();
  199. if (result === null) {
  200. console.log("JS: 课程获取或解析失败,流程终止。");
  201. return;
  202. }
  203. const { courses } = result;
  204. const saveResult = await saveCourses(courses);
  205. if (!saveResult) {
  206. console.log("JS: 课程保存失败,流程终止。");
  207. return;
  208. }
  209. window.shiguangBridge.showToast(`课程导入成功,共导入 ${courses.length} 门课程!`);
  210. console.log("JS: 整个导入流程执行完毕并成功。");
  211. window.shiguangBridge.notifyTaskCompletion();
  212. }
  213. runImportFlow();