AHZYYGZ.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. const BASE = `${window.location.origin}/ahzyygzjw`;
  2. const CONTROL_PAGE = '/student/xkjg.wdkb.jsp?menucode=S20301';
  3. const TIMETABLE_PAGE = '/student/wsxk.xskcb10319.jsp?params=';
  4. const TIME_SLOTS = [
  5. { number: 1, startTime: '08:00', endTime: '08:40' },
  6. { number: 2, startTime: '08:50', endTime: '09:30' },
  7. { number: 3, startTime: '09:45', endTime: '10:25' },
  8. { number: 4, startTime: '10:35', endTime: '11:15' },
  9. { number: 5, startTime: '11:25', endTime: '12:05' },
  10. { number: 6, startTime: '14:00', endTime: '14:40' },
  11. { number: 7, startTime: '14:50', endTime: '15:30' },
  12. { number: 8, startTime: '15:45', endTime: '16:25' },
  13. { number: 9, startTime: '16:35', endTime: '17:15' },
  14. { number: 10, startTime: '19:00', endTime: '20:00' },
  15. { number: 11, startTime: '20:00', endTime: '21:00' },
  16. { number: 12, startTime: '21:40', endTime: '22:30' }
  17. ];
  18. function cleanText(value) {
  19. return String(value || '')
  20. .replace(/[​-‍]/g, '')
  21. .replace(/ /g, ' ')
  22. .trim();
  23. }
  24. function encodeParams(xn, xq, xh) {
  25. return btoa(`xn=${xn}&xq=${xq}&xh=${xh}`);
  26. }
  27. function parseWeeks(weekStr) {
  28. const weeks = [];
  29. String(weekStr || '')
  30. .replace(/\s+/g, '')
  31. .split(/[,,]/)
  32. .forEach((part) => {
  33. if (!part) return;
  34. const isSingle = part.includes('单');
  35. const isDouble = part.includes('双');
  36. const rangeMatch = part.match(/(\d+)-(\d+)/);
  37. if (rangeMatch) {
  38. const start = parseInt(rangeMatch[1], 10);
  39. const end = parseInt(rangeMatch[2], 10);
  40. for (let i = start; i <= end; i++) {
  41. if (isSingle && i % 2 === 0) continue;
  42. if (isDouble && i % 2 !== 0) continue;
  43. weeks.push(i);
  44. }
  45. } else {
  46. const num = parseInt(part.replace(/[^\d]/g, ''), 10);
  47. if (!Number.isNaN(num)) weeks.push(num);
  48. }
  49. });
  50. return [...new Set(weeks)].sort((a, b) => a - b);
  51. }
  52. function decodeParams(encoded) {
  53. try {
  54. return atob(encoded);
  55. } catch (_) {
  56. return '';
  57. }
  58. }
  59. function parseXhFromEncodedParams(encoded) {
  60. const decoded = decodeParams(encoded);
  61. if (!decoded) return '';
  62. const search = new URLSearchParams(decoded);
  63. return String(search.get('xh') || '').trim();
  64. }
  65. function extractParamsFromHtml(html) {
  66. const match = String(html || '').match(/wsxk\.xskcb10319\.jsp\?params=([^"'&\s>]+)/);
  67. return match ? decodeURIComponent(match[1]) : '';
  68. }
  69. function findControlFrame(win) {
  70. try {
  71. if (win.document.querySelector('#xnxq')) return win;
  72. } catch (_) {}
  73. for (let i = 0; i < win.frames.length; i++) {
  74. try {
  75. const found = findControlFrame(win.frames[i]);
  76. if (found) return found;
  77. } catch (_) {}
  78. }
  79. return null;
  80. }
  81. function findTimetableFrame(win) {
  82. try {
  83. if (win.document.getElementById('mytable')) return win;
  84. } catch (_) {}
  85. for (let i = 0; i < win.frames.length; i++) {
  86. try {
  87. const found = findTimetableFrame(win.frames[i]);
  88. if (found) return found;
  89. } catch (_) {}
  90. }
  91. return null;
  92. }
  93. async function fetchControlDoc() {
  94. const res = await fetch(`${BASE}${CONTROL_PAGE}`, { credentials: 'include' });
  95. if (!res.ok) throw new Error(`课表控制页请求失败: ${res.status}`);
  96. const html = await res.text();
  97. return new DOMParser().parseFromString(html, 'text/html');
  98. }
  99. async function fetchTimetableDoc(xn, xq, xh) {
  100. const params = encodeParams(xn, xq, xh);
  101. const res = await fetch(`${BASE}${TIMETABLE_PAGE}${encodeURIComponent(params)}`, {
  102. method: 'GET',
  103. credentials: 'include'
  104. });
  105. if (!res.ok) throw new Error(`课表页面请求失败: ${res.status}`);
  106. const buffer = await res.arrayBuffer();
  107. let html = '';
  108. try {
  109. html = new TextDecoder('gbk').decode(buffer);
  110. } catch (_) {
  111. html = new TextDecoder('utf-8').decode(buffer);
  112. }
  113. return new DOMParser().parseFromString(html, 'text/html');
  114. }
  115. function parseSelectOptions(selectEl) {
  116. if (!selectEl) return { options: [], defaultIndex: 0 };
  117. const options = [];
  118. let defaultIndex = 0;
  119. Array.from(selectEl.querySelectorAll('option')).forEach((opt) => {
  120. const value = String(opt.value || '').trim();
  121. if (!value) return;
  122. const text = cleanText(opt.textContent) || value;
  123. if (opt.selected) defaultIndex = options.length;
  124. options.push({ value, text });
  125. });
  126. return { options, defaultIndex };
  127. }
  128. async function resolveTermSelection() {
  129. let controlDoc = null;
  130. let controlFrame = findControlFrame(window);
  131. if (controlFrame) {
  132. controlDoc = controlFrame.document;
  133. } else {
  134. controlDoc = await fetchControlDoc();
  135. }
  136. const select = controlDoc.querySelector('#xnxq');
  137. if (!select) {
  138. throw new Error('未找到学期选择器,请先登录并打开“学生个人课表”页面');
  139. }
  140. const { options, defaultIndex } = parseSelectOptions(select);
  141. if (!options.length) {
  142. throw new Error('未读取到学期列表,请先进入“学生个人课表”页面');
  143. }
  144. const selectedIndex = await window.AndroidBridgePromise.showSingleSelection(
  145. '选择学期',
  146. JSON.stringify(options.map(item => item.text)),
  147. defaultIndex
  148. );
  149. if (selectedIndex === null || selectedIndex === -1) {
  150. throw new Error('已取消学期选择');
  151. }
  152. const selected = options[selectedIndex];
  153. const [xn, xq] = String(selected.value).split('-');
  154. if (!xn || typeof xq === 'undefined') {
  155. throw new Error(`学期值解析失败: ${selected.value}`);
  156. }
  157. let encodedParams = extractParamsFromHtml(controlDoc.documentElement.outerHTML);
  158. if (!encodedParams) {
  159. const timetableFrame = findTimetableFrame(window);
  160. if (timetableFrame) {
  161. const url = new URL(timetableFrame.location.href);
  162. encodedParams = url.searchParams.get('params') || '';
  163. }
  164. }
  165. const xh = parseXhFromEncodedParams(encodedParams);
  166. if (!xh) {
  167. throw new Error('未读取到学号参数,请先点击进入“学生个人课表”后再导入');
  168. }
  169. return { xn, xq, xh, selectedValue: selected.value };
  170. }
  171. function findTable(doc) {
  172. return doc.getElementById('mytable')
  173. || Array.from(doc.querySelectorAll('table')).find(table => {
  174. const text = cleanText(table.innerText);
  175. return text.includes('星期一') && text.includes('[');
  176. })
  177. || null;
  178. }
  179. function parseCourseFromLines(lines, day) {
  180. const joined = lines.join('\n');
  181. const match = joined.match(/([\d,-单双]+)\[(\d+)-(\d+)\]/);
  182. if (!match) return null;
  183. const before = joined
  184. .slice(0, match.index)
  185. .split(/\n+/)
  186. .map(cleanText)
  187. .filter(Boolean);
  188. const after = joined
  189. .slice(match.index + match[0].length)
  190. .split(/\n+/)
  191. .map(cleanText)
  192. .filter(Boolean);
  193. let name = '';
  194. let teacher = '';
  195. if (before.length >= 2) {
  196. name = before[0];
  197. teacher = before[1];
  198. } else if (before.length === 1) {
  199. name = before[0];
  200. }
  201. if (!name) return null;
  202. return {
  203. name,
  204. teacher,
  205. position: after.join(' '),
  206. day,
  207. startSection: parseInt(match[2], 10),
  208. endSection: parseInt(match[3], 10),
  209. weeks: parseWeeks(match[1])
  210. };
  211. }
  212. function parseCellByDivBlocks(cell, day) {
  213. const blocks = Array.from(cell.querySelectorAll('div[style*="padding-bottom:5px"], div[style*="padding-bottom: 5px"]'));
  214. if (!blocks.length) return [];
  215. const items = [];
  216. blocks.forEach((block) => {
  217. const lines = block.innerText
  218. .split(/\n+/)
  219. .map(cleanText)
  220. .filter(Boolean);
  221. const parsed = parseCourseFromLines(lines, day);
  222. if (parsed && parsed.weeks.length) items.push(parsed);
  223. });
  224. return items;
  225. }
  226. function parseCellByTextFallback(cell, day) {
  227. const lines = cell.innerText
  228. .split(/\n+/)
  229. .map(cleanText)
  230. .filter(Boolean);
  231. if (!lines.length) return [];
  232. const timeIndices = [];
  233. lines.forEach((line, index) => {
  234. if (/([\d,-单双]+)\[(\d+)-(\d+)\]/.test(line)) {
  235. timeIndices.push(index);
  236. }
  237. });
  238. const items = [];
  239. timeIndices.forEach((currentIndex, i) => {
  240. const nextIndex = i + 1 < timeIndices.length ? timeIndices[i + 1] : lines.length;
  241. let beforeLines = i === 0 ? lines.slice(0, currentIndex) : lines.slice(timeIndices[i - 1] + 1, currentIndex);
  242. if (beforeLines.length > 2) beforeLines = beforeLines.slice(beforeLines.length - 2);
  243. const match = lines[currentIndex].match(/([\d,-单双]+)\[(\d+)-(\d+)\]/);
  244. if (!match) return;
  245. const name = beforeLines[0] || '未知课程';
  246. const teacher = beforeLines[1] || '';
  247. const positionLines = lines.slice(currentIndex + 1, nextIndex);
  248. items.push({
  249. name,
  250. teacher,
  251. position: positionLines.join(' '),
  252. day,
  253. startSection: parseInt(match[2], 10),
  254. endSection: parseInt(match[3], 10),
  255. weeks: parseWeeks(match[1])
  256. });
  257. });
  258. return items;
  259. }
  260. function parseAndMergeQingguoTable(doc) {
  261. const table = findTable(doc);
  262. if (!table) {
  263. throw new Error('未找到课表表格,请先进入“学生个人课表”页面');
  264. }
  265. const rawItems = [];
  266. Array.from(table.rows).forEach((row) => {
  267. const cells = Array.from(row.cells);
  268. if (cells.length < 7) return;
  269. cells.forEach((cell, colIndex) => {
  270. const distanceToLast = cells.length - 1 - colIndex;
  271. if (distanceToLast > 6) return;
  272. const day = 7 - distanceToLast;
  273. const rawText = cleanText(cell.innerText);
  274. if (!rawText.includes('[')) return;
  275. const divParsed = parseCellByDivBlocks(cell, day);
  276. if (divParsed.length) {
  277. rawItems.push(...divParsed);
  278. return;
  279. }
  280. rawItems.push(...parseCellByTextFallback(cell, day));
  281. });
  282. });
  283. const groupMap = new Map();
  284. rawItems.forEach((item) => {
  285. if (!item || !item.name || !item.weeks.length) return;
  286. const key = `${item.name}|${item.teacher}|${item.position}|${item.day}`;
  287. if (!groupMap.has(key)) groupMap.set(key, {});
  288. const weekMap = groupMap.get(key);
  289. item.weeks.forEach((week) => {
  290. if (!weekMap[week]) weekMap[week] = new Set();
  291. for (let section = item.startSection; section <= item.endSection; section++) {
  292. weekMap[week].add(section);
  293. }
  294. });
  295. });
  296. const finalCourses = [];
  297. groupMap.forEach((weekMap, key) => {
  298. const [name, teacher, position, day] = key.split('|');
  299. const patternMap = new Map();
  300. Object.keys(weekMap).forEach((weekStr) => {
  301. const week = parseInt(weekStr, 10);
  302. const sections = Array.from(weekMap[week]).sort((a, b) => a - b);
  303. if (!sections.length) return;
  304. let start = sections[0];
  305. for (let i = 0; i < sections.length; i++) {
  306. if (i === sections.length - 1 || sections[i + 1] !== sections[i] + 1) {
  307. const pKey = `${start}-${sections[i]}`;
  308. if (!patternMap.has(pKey)) patternMap.set(pKey, []);
  309. patternMap.get(pKey).push(week);
  310. if (i < sections.length - 1) start = sections[i + 1];
  311. }
  312. }
  313. });
  314. patternMap.forEach((weeks, patternKey) => {
  315. const [startSection, endSection] = patternKey.split('-').map(Number);
  316. finalCourses.push({
  317. name,
  318. teacher,
  319. position,
  320. day: parseInt(day, 10),
  321. startSection,
  322. endSection,
  323. weeks: weeks.sort((a, b) => a - b)
  324. });
  325. });
  326. });
  327. return finalCourses;
  328. }
  329. async function loadTimetableDoc(term) {
  330. const currentTableFrame = findTimetableFrame(window);
  331. if (currentTableFrame && currentTableFrame.document.getElementById('mytable')) {
  332. const currentUrl = new URL(currentTableFrame.location.href);
  333. const currentParams = currentUrl.searchParams.get('params') || '';
  334. const decoded = decodeParams(currentParams);
  335. if (decoded.includes(`xn=${term.xn}`) && decoded.includes(`xq=${term.xq}`)) {
  336. return currentTableFrame.document;
  337. }
  338. }
  339. return await fetchTimetableDoc(term.xn, term.xq, term.xh);
  340. }
  341. async function runImportFlow() {
  342. try {
  343. const confirmed = await window.AndroidBridgePromise.showAlert(
  344. '安徽中医药高等专科学校教务导入',
  345. '请确认你已登录教务系统,并且最好已经打开“学生个人课表”页面。',
  346. '确定,开始导入'
  347. );
  348. if (!confirmed) return;
  349. const term = await resolveTermSelection();
  350. AndroidBridge.showToast('正在提取青果课表数据...');
  351. const doc = await loadTimetableDoc(term);
  352. const courses = parseAndMergeQingguoTable(doc);
  353. if (!courses.length) {
  354. throw new Error('未找到有效课程,请确认当前学期课表已正常显示');
  355. }
  356. const allWeeks = courses.flatMap(course => course.weeks);
  357. const semesterTotalWeeks = allWeeks.length ? Math.max(...allWeeks) : 20;
  358. await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify({
  359. semesterTotalWeeks,
  360. semesterStartDate: null,
  361. firstDayOfWeek: 1
  362. }));
  363. await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(TIME_SLOTS));
  364. await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
  365. AndroidBridge.showToast(`导入成功:共 ${courses.length} 门课程`);
  366. AndroidBridge.notifyTaskCompletion();
  367. } catch (error) {
  368. console.error(error);
  369. AndroidBridge.showToast(`导入失败: ${error.message}`);
  370. }
  371. }
  372. runImportFlow();