ecjtu_01.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. // 文件: ECJTU_01.js
  2. // 功能:从华东交通大学系统获取课程表,解析后导入到拾光课程表
  3. // 适配:华东交通大学教务系统
  4. // 维护者:glxgo
  5. const BASE = window.location.origin;
  6. const SCHEDULE_PATHS = [
  7. '/Schedule/Schedule_getUserSchedume.action?item=0207',
  8. '/Schedule/Schedule_getUserSchedume.action?item=0205',
  9. '/Schedule/Schedule_getUserSchedume.action'
  10. ];
  11. function cleanText(value) {
  12. return String(value || '')
  13. .replace(/[​-‍]/g, '')
  14. .replace(/ /g, ' ')
  15. .trim();
  16. }
  17. function parseWeeks(weekStr) {
  18. const weeks = [];
  19. String(weekStr || '')
  20. .replace(/\s+/g, '')
  21. .split(/[,,]/)
  22. .forEach((part) => {
  23. if (!part) return;
  24. const isSingle = part.includes('单');
  25. const isDouble = part.includes('双');
  26. const rangeMatch = part.match(/(\d+)-(\d+)/);
  27. if (rangeMatch) {
  28. const start = parseInt(rangeMatch[1], 10);
  29. const end = parseInt(rangeMatch[2], 10);
  30. for (let i = start; i <= end; i++) {
  31. if (isSingle && i % 2 === 0) continue;
  32. if (isDouble && i % 2 !== 0) continue;
  33. weeks.push(i);
  34. }
  35. } else {
  36. const num = parseInt(part.replace(/[^\d]/g, ''), 10);
  37. if (!Number.isNaN(num)) weeks.push(num);
  38. }
  39. });
  40. return [...new Set(weeks)].sort((a, b) => a - b);
  41. }
  42. function parseSections(sectionStr) {
  43. const sections = String(sectionStr || '')
  44. .split(',')
  45. .map(s => parseInt(s.trim(), 10))
  46. .filter(n => !Number.isNaN(n));
  47. if (!sections.length) return null;
  48. return {
  49. startSection: sections[0],
  50. endSection: sections[sections.length - 1]
  51. };
  52. }
  53. function parseTeacherPosition(line) {
  54. const raw = cleanText(line);
  55. const atIndex = raw.indexOf('@');
  56. if (atIndex === -1) {
  57. return { teacher: raw, position: '' };
  58. }
  59. return {
  60. teacher: cleanText(raw.slice(0, atIndex)),
  61. position: cleanText(raw.slice(atIndex + 1))
  62. };
  63. }
  64. function parseCourseLines(lines, day) {
  65. const items = [];
  66. for (let i = 2; i < lines.length; i++) {
  67. const line = cleanText(lines[i]);
  68. const match = line.match(/^([\d,,\-单双()]+)\s+(\d+(?:,\d+)*)$/);
  69. if (!match) continue;
  70. const name = cleanText(lines[i - 2]);
  71. const teacherPosition = parseTeacherPosition(lines[i - 1]);
  72. const weekText = match[1];
  73. const sectionText = match[2];
  74. const weeks = parseWeeks(weekText);
  75. const sections = parseSections(sectionText);
  76. if (!name || !weeks.length || !sections) continue;
  77. items.push({
  78. name,
  79. teacher: teacherPosition.teacher || '未知教师',
  80. position: teacherPosition.position || '未排地点',
  81. day,
  82. startSection: sections.startSection,
  83. endSection: sections.endSection,
  84. weeks
  85. });
  86. }
  87. return items;
  88. }
  89. function mergeCourses(rawItems) {
  90. const groupMap = new Map();
  91. rawItems.forEach((item) => {
  92. const key = `${item.name}|${item.teacher}|${item.position}|${item.day}`;
  93. if (!groupMap.has(key)) groupMap.set(key, {});
  94. const weekMap = groupMap.get(key);
  95. item.weeks.forEach((week) => {
  96. if (!weekMap[week]) weekMap[week] = new Set();
  97. for (let section = item.startSection; section <= item.endSection; section++) {
  98. weekMap[week].add(section);
  99. }
  100. });
  101. });
  102. const finalCourses = [];
  103. groupMap.forEach((weekMap, key) => {
  104. const [name, teacher, position, day] = key.split('|');
  105. const patternMap = new Map();
  106. Object.keys(weekMap).forEach((weekStr) => {
  107. const week = parseInt(weekStr, 10);
  108. const sections = Array.from(weekMap[week]).sort((a, b) => a - b);
  109. if (!sections.length) return;
  110. let start = sections[0];
  111. for (let i = 0; i < sections.length; i++) {
  112. if (i === sections.length - 1 || sections[i + 1] !== sections[i] + 1) {
  113. const pKey = `${start}-${sections[i]}`;
  114. if (!patternMap.has(pKey)) patternMap.set(pKey, []);
  115. patternMap.get(pKey).push(week);
  116. if (i < sections.length - 1) start = sections[i + 1];
  117. }
  118. }
  119. });
  120. patternMap.forEach((weeks, patternKey) => {
  121. const [startSection, endSection] = patternKey.split('-').map(Number);
  122. finalCourses.push({
  123. name,
  124. teacher,
  125. position,
  126. day: parseInt(day, 10),
  127. startSection,
  128. endSection,
  129. weeks: weeks.sort((a, b) => a - b)
  130. });
  131. });
  132. });
  133. return finalCourses.sort((a, b) => (
  134. a.day - b.day
  135. || a.startSection - b.startSection
  136. || a.name.localeCompare(b.name, 'zh-CN')
  137. ));
  138. }
  139. function parseScheduleTable(doc) {
  140. const table = doc.getElementById('courseSche');
  141. if (!table) return [];
  142. const rows = Array.from(table.rows);
  143. if (rows.length < 2) return [];
  144. const rawItems = [];
  145. for (let r = 1; r < rows.length; r++) {
  146. const cells = Array.from(rows[r].cells);
  147. if (cells.length < 2) continue;
  148. const dayCells = cells.slice(1, 8);
  149. dayCells.forEach((cell, index) => {
  150. const rawText = cleanText(cell.innerText);
  151. if (!rawText || !rawText.includes('@')) return;
  152. const lines = cell.innerText
  153. .split(/\n+/)
  154. .map(cleanText)
  155. .filter(Boolean);
  156. rawItems.push(...parseCourseLines(lines, index + 1));
  157. });
  158. }
  159. return mergeCourses(rawItems);
  160. }
  161. function getCurrentTermInfo(doc) {
  162. const select = doc.querySelector('#term');
  163. if (!select) return null;
  164. const selected = select.querySelector('option:checked') || select.options[select.selectedIndex];
  165. return {
  166. value: String(select.value || '').trim(),
  167. text: selected ? cleanText(selected.textContent) : ''
  168. };
  169. }
  170. function isScheduleDoc(doc) {
  171. return !!(doc && (doc.getElementById('courseSche') || doc.querySelector('#term')));
  172. }
  173. function findScheduleDoc(win) {
  174. try {
  175. if (isScheduleDoc(win.document)) return win.document;
  176. } catch (_) {}
  177. for (let i = 0; i < win.frames.length; i++) {
  178. try {
  179. const found = findScheduleDoc(win.frames[i]);
  180. if (found) return found;
  181. } catch (_) {}
  182. }
  183. return null;
  184. }
  185. async function fetchScheduleDoc() {
  186. for (const path of SCHEDULE_PATHS) {
  187. try {
  188. const res = await fetch(`${BASE}${path}`, { credentials: 'include' });
  189. if (!res.ok) continue;
  190. const html = await res.text();
  191. const doc = new DOMParser().parseFromString(html, 'text/html');
  192. if (isScheduleDoc(doc)) return doc;
  193. } catch (_) {}
  194. }
  195. return null;
  196. }
  197. async function loadScheduleDoc() {
  198. const currentDoc = findScheduleDoc(window);
  199. if (currentDoc && currentDoc.getElementById('courseSche')) return currentDoc;
  200. const fetched = await fetchScheduleDoc();
  201. if (fetched) return fetched;
  202. throw new Error('未找到课表页面,请先登录后进入“我的课表/个人课表”页面');
  203. }
  204. async function runImportFlow() {
  205. try {
  206. const confirmed = await window.AndroidBridgePromise.showAlert(
  207. '华东交通大学教务导入',
  208. '请确认你已经登录教务系统;如需导入其他学期,请先在页面上切换到目标学期后再导入。',
  209. '确定,开始导入'
  210. );
  211. if (!confirmed) return;
  212. const doc = await loadScheduleDoc();
  213. const termInfo = getCurrentTermInfo(doc);
  214. AndroidBridge.showToast(termInfo?.text ? `正在导入 ${termInfo.text} 课表...` : '正在解析课表数据...');
  215. const courses = parseScheduleTable(doc);
  216. if (!courses.length) {
  217. throw new Error('未解析到课程,请确认当前课表已正常显示');
  218. }
  219. const allWeeks = courses.flatMap(course => course.weeks);
  220. const semesterTotalWeeks = allWeeks.length ? Math.max(...allWeeks) : 20;
  221. await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify({
  222. semesterTotalWeeks,
  223. semesterStartDate: null,
  224. firstDayOfWeek: 1
  225. }));
  226. await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
  227. AndroidBridge.showToast(`导入成功:共 ${courses.length} 门课程`);
  228. AndroidBridge.notifyTaskCompletion();
  229. } catch (error) {
  230. console.error(error);
  231. AndroidBridge.showToast(`导入失败: ${error.message}`);
  232. }
  233. }
  234. runImportFlow();