sdjtu.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. // 山东交通学院 (sdjtu.edu.cn) 拾光课程表适配脚本
  2. // 教务系统类型:强智教务(通过深信服WebVPN访问)
  3. // 直接读取页面DOM解析课表,WebVPN代理下fetch可能失败
  4. /**
  5. * 将周次字符串解析为数字数组
  6. * 支持格式: 任意单周/单双周/区间/混合等组合
  7. */
  8. function parseWeeks(weekStr) {
  9. const weeks = [];
  10. if (!weekStr) return weeks;
  11. const pureWeekData = weekStr.split('(')[0];
  12. pureWeekData.split(',').forEach(seg => {
  13. seg = seg.trim();
  14. if (seg.includes('-')) {
  15. const [s, e] = seg.split('-').map(Number);
  16. if (!isNaN(s) && !isNaN(e)) {
  17. for (let i = s; i <= e; i++) weeks.push(i);
  18. }
  19. } else {
  20. const w = parseInt(seg);
  21. if (!isNaN(w)) weeks.push(w);
  22. }
  23. });
  24. return [...new Set(weeks)].sort((a, b) => a - b);
  25. }
  26. /**
  27. * 获取包含课表表格的文档对象
  28. * 优先当前页面,其次遍历iframe查找
  29. */
  30. function getScheduleDocument() {
  31. // 1. 当前页面直接包含课表
  32. if (document.getElementById('kbtable')) {
  33. return document;
  34. }
  35. // 2. 在iframe中查找课表
  36. const iframes = document.querySelectorAll('iframe');
  37. for (let i = 0; i < iframes.length; i++) {
  38. try {
  39. const iframeDoc = iframes[i].contentDocument;
  40. if (iframeDoc && iframeDoc.getElementById('kbtable')) {
  41. return iframeDoc;
  42. }
  43. } catch (e) {
  44. // 跨域iframe无法访问,跳过
  45. }
  46. }
  47. return null;
  48. }
  49. /**
  50. * 从DOM元素中按<br>分割提取行文本
  51. * 解决未挂载元素innerText无换行的问题
  52. */
  53. function extractLines(element) {
  54. const lines = [];
  55. let currentLine = '';
  56. for (let i = 0; i < element.childNodes.length; i++) {
  57. const node = element.childNodes[i];
  58. if (node.nodeName === 'BR') {
  59. lines.push(currentLine.trim());
  60. currentLine = '';
  61. } else {
  62. currentLine += node.textContent;
  63. }
  64. }
  65. if (currentLine.trim()) lines.push(currentLine.trim());
  66. return lines.filter(l => l.length > 0);
  67. }
  68. /**
  69. * 无法post到课表信息,故从课表HTML中解析时间段信息
  70. * 行头格式: "第一大节\n08:30-10:00"
  71. * 每个大节包含2个小节
  72. */
  73. function parseTimeSlots(doc) {
  74. const table = doc.getElementById('kbtable');
  75. if (!table) return [];
  76. const rows = table.querySelectorAll('tr');
  77. const timeSlots = [];
  78. let sectionNum = 1;
  79. for (let i = 1; i < rows.length; i++) {
  80. const ths = rows[i].querySelectorAll('th');
  81. if (ths.length === 0) continue;
  82. const headerText = ths[0].innerText.trim();
  83. // 跳过备注行
  84. if (headerText.startsWith('备注')) continue;
  85. // 提取时间范围,格式如 "08:30-10:00"
  86. const timeMatch = headerText.match(/(\d{2}:\d{2})-(\d{2}:\d{2})/);
  87. if (!timeMatch) continue;
  88. const startTime = timeMatch[1];
  89. const endTime = timeMatch[2];
  90. // 每小节固定45分钟。
  91. // 第一小节从大节开始时间起算;第二小节从大节结束时间往前推45分钟。
  92. // 这样既兼容90分钟大节(45+45无课间),也兼容95分钟大节(45+5课间+45)
  93. const [sh, sm] = startTime.split(':').map(Number);
  94. const [eh, em] = endTime.split(':').map(Number);
  95. const startMinutes = sh * 60 + sm;
  96. const endMinutes = eh * 60 + em;
  97. const CLASS_DURATION = 45;
  98. // 第一小节
  99. timeSlots.push({
  100. number: sectionNum++,
  101. startTime: startTime,
  102. endTime: formatTime(startMinutes + CLASS_DURATION)
  103. });
  104. // 第二小节(从大节结束时间往前推45分钟)
  105. timeSlots.push({
  106. number: sectionNum++,
  107. startTime: formatTime(endMinutes - CLASS_DURATION),
  108. endTime: endTime
  109. });
  110. }
  111. return timeSlots;
  112. }
  113. /**
  114. * 将分钟数格式化为 HH:mm
  115. */
  116. function formatTime(totalMinutes) {
  117. const h = Math.floor(totalMinutes / 60);
  118. const m = totalMinutes % 60;
  119. return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
  120. }
  121. /**
  122. * 解析课表HTML,提取课程数据
  123. */
  124. function parseCourses(doc) {
  125. const table = doc.getElementById('kbtable');
  126. if (!table) return [];
  127. const courses = [];
  128. const rows = table.querySelectorAll('tr');
  129. // 从第2行开始遍历(第1行是星期表头)
  130. for (let i = 1; i < rows.length; i++) {
  131. const tds = rows[i].querySelectorAll('td');
  132. if (tds.length === 0) continue;
  133. // 遍历每一天的列(td索引0=星期一, 1=星期二, ..., 5=星期六)
  134. tds.forEach((cell, dayIndex) => {
  135. const day = dayIndex + 1; // 1=周一, 6=周六
  136. // 获取显示视图的div(class="kbcontent")
  137. const contentDivs = cell.querySelectorAll('.kbcontent');
  138. contentDivs.forEach(div => {
  139. const rawHtml = div.innerHTML.trim();
  140. if (!rawHtml || rawHtml === '&nbsp;') return;
  141. // 按分隔线拆分为多个课程块
  142. const blocks = rawHtml.split(/-{5,}/);
  143. blocks.forEach(block => {
  144. const tempDiv = document.createElement('div');
  145. tempDiv.innerHTML = block;
  146. // 按<br>分割为行
  147. const lines = extractLines(tempDiv);
  148. if (lines.length < 2) return;
  149. // 找到课程代码行(格式: 数字-数字+字母)
  150. let codeLineIdx = -1;
  151. for (let li = 0; li < lines.length; li++) {
  152. if (/^\d{6}-\d{6}/.test(lines[li])) {
  153. codeLineIdx = li;
  154. break;
  155. }
  156. }
  157. if (codeLineIdx === -1 || codeLineIdx + 1 >= lines.length) return;
  158. // 课程名称 = 代码行下一行,去除尾部O/P调课标记
  159. const name = lines[codeLineIdx + 1]
  160. .replace(/[\s ]*[OP]\s*$/, '')
  161. .trim();
  162. // 提取各字段
  163. const teacherFont = tempDiv.querySelector('font[title="老师"]') ||
  164. tempDiv.querySelector('font[title="教师"]');
  165. const weekFont = tempDiv.querySelector('font[title="周次(节次)"]');
  166. const roomFont = tempDiv.querySelector('font[title="教室"]');
  167. if (!weekFont) return;
  168. const weekInfo = weekFont.textContent.trim();
  169. const teacher = teacherFont ? teacherFont.textContent.trim() : '';
  170. const position = roomFont ? roomFont.textContent.trim() : '';
  171. // 解析节次
  172. const secMatch = weekInfo.match(/\[(\d+)(?:-(\d+))?节\]/);
  173. if (!secMatch) return;
  174. const startSection = parseInt(secMatch[1]);
  175. const endSection = secMatch[2] ? parseInt(secMatch[2]) : startSection;
  176. // 解析周次
  177. const weeks = parseWeeks(weekInfo);
  178. if (weeks.length === 0 || !name) return;
  179. courses.push({
  180. name: name,
  181. teacher: teacher,
  182. position: position,
  183. day: day,
  184. startSection: startSection,
  185. endSection: endSection,
  186. weeks: weeks
  187. });
  188. });
  189. });
  190. });
  191. }
  192. return courses;
  193. }
  194. /**
  195. * 读取当前页面选中的学期ID
  196. */
  197. function getSemesterId(doc) {
  198. const select = doc.getElementById('xnxq01id');
  199. if (!select) return '未知学期';
  200. return select.value || '未知学期';
  201. }
  202. /**
  203. * 日期输入验证函数
  204. */
  205. window.validateDateInput = function (input) {
  206. if (/^\d{4}-\d{2}-\d{2}$/.test(input)) {
  207. const d = new Date(input);
  208. if (!isNaN(d.getTime())) return false; // 验证通过
  209. }
  210. return "请输入正确的日期格式,如 2026-02-23";
  211. };
  212. /**
  213. * 根据学期ID推算默认开学日期
  214. * 第一学期(秋季)默认9月1日,第二学期(春季)默认2月23日
  215. */
  216. function getDefaultStartDate(semesterId) {
  217. const match = semesterId.match(/^(\d{4})-(\d{4})-(\d)$/);
  218. if (!match) return '';
  219. const year = parseInt(match[1]);
  220. const sem = parseInt(match[3]);
  221. if (sem === 1) {
  222. return `${year}-08-24`;
  223. } else {
  224. return `${parseInt(match[2])}-02-23`;
  225. }
  226. }
  227. /**
  228. * 获取用户输入的开学日期
  229. * 本校寒暑假的开学时间都不固定,故采用此方案
  230. */
  231. async function getSemesterStartDate(semesterId) {
  232. const defaultDate = getDefaultStartDate(semesterId);
  233. const dateInput = await window.AndroidBridgePromise.showPrompt(
  234. "设置开学日期",
  235. "请输入本学期开学日期,一般为周一(格式 YYYY-MM-DD,如 2026-02-23):",
  236. defaultDate,
  237. "validateDateInput"
  238. );
  239. return dateInput; // 用户取消返回 null
  240. }
  241. /**
  242. * 主流程
  243. */
  244. async function runImportFlow() {
  245. try {
  246. // 1. 确认提示
  247. const confirmed = await window.AndroidBridgePromise.showAlert(
  248. "导入提示",
  249. "请确保您已登录教务系统并打开了【学期理论课表】页面(已选好学期)。\n脚本将直接读取当前页面的课表数据。",
  250. "确认并开始"
  251. );
  252. if (!confirmed) return;
  253. // 2. 获取课表文档
  254. const doc = getScheduleDocument();
  255. if (!doc) {
  256. AndroidBridge.showToast("未找到课表页面,请先打开【学期理论课表】");
  257. return;
  258. }
  259. const semesterId = getSemesterId(doc);
  260. // 3. 获取开学日期
  261. const startDate = await getSemesterStartDate(semesterId);
  262. if (startDate === null) {
  263. AndroidBridge.showToast("已取消开学日期输入");
  264. return;
  265. }
  266. // 4. 解析课程数据
  267. const courses = parseCourses(doc);
  268. if (courses.length === 0) {
  269. AndroidBridge.showToast("未获取到课程数据,该学期可能暂无课表");
  270. return;
  271. }
  272. // 5. 解析时间段
  273. const timeSlots = parseTimeSlots(doc);
  274. // 6. 保存数据
  275. if (timeSlots.length > 0) {
  276. await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  277. }
  278. await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
  279. // 7. 保存课表配置(含开学日期)
  280. const config = {
  281. semesterStartDate: startDate,
  282. semesterTotalWeeks: 20,
  283. defaultClassDuration: 45,
  284. defaultBreakDuration: 5,
  285. firstDayOfWeek: 1
  286. };
  287. await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(config));
  288. AndroidBridge.showToast(`[${semesterId}] 成功导入 ${courses.length} 条课程记录!`);
  289. AndroidBridge.notifyTaskCompletion();
  290. } catch (error) {
  291. console.error("适配脚本异常:", error);
  292. AndroidBridge.showToast("异常: " + error.message);
  293. }
  294. }
  295. // 启动导入流程
  296. runImportFlow();