ecjtu_01.js 8.4 KB

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