wit_01.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. // 武汉工程大学强智教务。接口与结构依据本校课表响应验证。
  2. // 开发参考:拾光官方强智适配案例(YSSDUFE,星河欲转)。
  3. (async function () {
  4. const lock = '__witImportRunning';
  5. if (window[lock]) return;
  6. window[lock] = true;
  7. const bridge = window.shiguangBridgePromise;
  8. const toast = message => window.shiguangBridge.showToast(message);
  9. const endpoint = '/jsxsd/xskb/xskb_list.do';
  10. async function semesterInfo(semester) {
  11. const controller = new AbortController();
  12. const timer = setTimeout(() => controller.abort(), 20000);
  13. try {
  14. const response = await fetch('/jsxsd/jxzl/jxzl_query', {
  15. method: 'POST', credentials: 'same-origin', signal: controller.signal,
  16. body: new URLSearchParams({ xnxq01id: semester })
  17. });
  18. if (!response.ok) throw new Error('教学周历请求失败');
  19. const doc = new DOMParser().parseFromString(await response.text(), 'text/html');
  20. const selected = doc.querySelector('#xnxq01id');
  21. if (selected && selected.value !== semester) throw new Error('教学周历学期不匹配');
  22. const rows = [...doc.querySelectorAll('#kbtable tr')].filter(row => row.querySelector('td'));
  23. const match = rows[0]?.querySelector('td[title]')?.getAttribute('title')?.match(/^(\d{4})年(\d{2})月(\d{2})日?$/);
  24. const weeks = rows.map(row => parseInt(row.querySelector('td').textContent.trim(), 10)).filter(n => n > 0 && n <= 100);
  25. if (!match || !weeks.length) throw new Error('无法解析教学周历');
  26. const date = `${match[1]}-${match[2]}-${match[3]}`;
  27. const parsed = new Date(`${date}T00:00:00Z`);
  28. if (isNaN(parsed) || parsed.toISOString().slice(0, 10) !== date) throw new Error('教学周历日期无效');
  29. return { semesterStartDate: date, semesterTotalWeeks: Math.max(...weeks), firstDayOfWeek: 1 };
  30. } finally { clearTimeout(timer); }
  31. }
  32. const timeSlots = [
  33. ['08:00','08:45'], ['08:50','09:35'], ['09:55','10:40'], ['10:45','11:30'],
  34. ['11:35','12:20'], ['14:00','14:45'], ['14:50','15:35'], ['15:55','16:40'],
  35. ['16:45','17:30'], ['19:00','19:45'], ['19:50','20:35'], ['20:40','21:25']
  36. ].map(([startTime, endTime], i) => ({ number: i + 1, startTime, endTime }));
  37. async function request(options) {
  38. const controller = new AbortController();
  39. const timer = setTimeout(() => controller.abort(), 20000);
  40. try {
  41. const response = await fetch(endpoint, { ...options, credentials: 'same-origin', signal: controller.signal });
  42. if (!response.ok) throw new Error(`教务请求失败:${response.status}`);
  43. const doc = new DOMParser().parseFromString(await response.text(), 'text/html');
  44. if (!doc.querySelector('#kbtable') || !doc.querySelector('#xnxq01id')) {
  45. throw new Error('登录已失效或课表结构不匹配,请重新进入教务课表页面');
  46. }
  47. return doc;
  48. } finally { clearTimeout(timer); }
  49. }
  50. function parseWeeks(text) {
  51. const odd = /单/.test(text), even = /双/.test(text);
  52. const source = text.replace(/(/g, '(').split('[')[0]
  53. .replace(/\([^)]*\)/g, '').replace(/[周单双\s]/g, '').replace(/,/g, ',');
  54. const weeks = new Set();
  55. for (const part of source.split(',')) {
  56. const match = part.match(/^(\d+)(?:-(\d+))?$/);
  57. if (!match) throw new Error('无法识别周次');
  58. const start = Number(match[1]), end = Number(match[2] || match[1]);
  59. if (start < 1 || end < start || end > 100) throw new Error('周次超出有效范围');
  60. for (let w = start; w <= end; w++) if ((!odd || w % 2 === 1) && (!even || w % 2 === 0)) weeks.add(w);
  61. }
  62. if (!weeks.size) throw new Error('周次为空');
  63. return [...weeks].sort((a, b) => a - b);
  64. }
  65. try {
  66. if (location.hostname !== 'jwxt.wit.edu.cn') throw new Error('请先进入武汉工程大学教务系统');
  67. const initial = await request();
  68. const select = initial.querySelector('#xnxq01id');
  69. const semesters = [...select.options].filter(option => option.value);
  70. const index = await bridge.showSingleSelection('选择学期', JSON.stringify(semesters.map(o => o.textContent.trim())), semesters.findIndex(o => o.selected));
  71. if (index === null) return;
  72. if (!semesters[index]) throw new Error('学期选择无效');
  73. const semester = semesters[index].value;
  74. const body = new URLSearchParams();
  75. for (const name of ['jx0404id', 'cj0701id', 'demo', 'kbjcmsid']) body.set(name, initial.querySelector(`[name="${name}"]`)?.value || '');
  76. body.set('xnxq01id', semester);
  77. body.set('zc', '');
  78. const detail = initial.querySelector('[name="sfFD"]');
  79. if (detail?.checked) body.set('sfFD', detail.value);
  80. const doc = await request({ method: 'POST', body });
  81. if (doc.querySelector('#xnxq01id').value !== semester || doc.querySelector('#zc')?.value) throw new Error('返回学期或周次与查询条件不一致');
  82. const table = doc.querySelector('#kbtable');
  83. const headers = [...table.rows[0].cells].map(c => c.textContent.trim());
  84. const days = ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'];
  85. const courses = new Map();
  86. let invalid = 0;
  87. for (const row of [...table.rows].slice(1)) {
  88. for (let column = 0; column < row.cells.length; column++) {
  89. const cell = row.cells[column];
  90. for (const div of cell.querySelectorAll('div.kbcontent')) {
  91. if (!div.textContent.trim()) continue;
  92. for (const html of div.innerHTML.split(/(?:<br\s*\/?\s*>\s*)?-{5,}(?:\s*<br\s*\/?\s*>)?/i)) {
  93. const block = doc.createElement('div'); block.innerHTML = html;
  94. if (!block.textContent.trim()) continue;
  95. try {
  96. const day = days.indexOf(headers[column]) + 1;
  97. const timing = block.querySelector('font[title="周次(节次)"]')?.textContent || '';
  98. const section = timing.match(/\[([\d,,\-\s]+)节\]/);
  99. const name = [...block.childNodes].filter(n => n.nodeType === 3).map(n => n.textContent.trim()).find(Boolean);
  100. if (!name || !day || !section) throw new Error('课程信息不完整');
  101. const weeks = parseWeeks(timing);
  102. const teacher = block.querySelector('font[title="老师"],font[title="教师"]')?.textContent.trim() || '';
  103. const position = block.querySelector('font[title="教室"]')?.textContent.trim() || '';
  104. for (const part of section[1].replace(/,/g, ',').split(',')) {
  105. const match = part.trim().match(/^(\d+)(?:-(\d+))?$/);
  106. if (!match) throw new Error('节次无法识别');
  107. const startSection = Number(match[1]), endSection = Number(match[2] || match[1]);
  108. if (startSection < 1 || endSection < startSection || endSection > 50) throw new Error('节次无效');
  109. const key = JSON.stringify([name, teacher, position, day, startSection, endSection]);
  110. const old = courses.get(key);
  111. courses.set(key, { name, teacher, position, day, startSection, endSection, weeks: [...new Set([...(old?.weeks || []), ...weeks])].sort((a,b) => a-b) });
  112. }
  113. } catch (_) { invalid++; }
  114. }
  115. }
  116. }
  117. }
  118. if (invalid) throw new Error(`${invalid} 条课程无法完整解析,已停止导入,请反馈页面样本`);
  119. if (!courses.size) {
  120. await bridge.showAlert('该学期暂无课程', `${semester} 没有已排定时间的课程,本次未导入数据。可重新运行脚本选择其他学期。`, '知道了');
  121. return;
  122. }
  123. const remarks = [...table.rows].find(r => r.cells[0]?.textContent.includes('备注'))?.cells[1]?.textContent.trim();
  124. let config = null;
  125. try { config = await semesterInfo(semester); } catch (_) { /* 保留现有学期设置,提示用户核对。 */ }
  126. if (!await bridge.showAlert('导入前确认', `将导入 ${courses.size} 条课程安排,并写入学校作息。${config ? `教学周历:${config.semesterStartDate} 开始,共 ${config.semesterTotalWeeks} 周。` : '未能获取教学周历,请在 App 内手动核对开学日期和总周数。'}请核对节次作息。${remarks ? '\n课表含备注,请回教务核对其中未排定时间的课程并手动添加。' : ''}`, '确认导入')) return;
  127. if (config) await bridge.saveCourseConfig(JSON.stringify(config));
  128. await bridge.savePresetTimeSlots(JSON.stringify(timeSlots));
  129. await bridge.saveImportedCourses(JSON.stringify([...courses.values()]));
  130. toast(`成功导入 ${courses.size} 条课程安排`);
  131. window.shiguangBridge.notifyTaskCompletion();
  132. } catch (error) { toast(`导入停止:${error.message}`); }
  133. finally { window[lock] = false; }
  134. })();