// 山东交通学院 (sdjtu.edu.cn) 拾光课程表适配脚本 // 教务系统类型:强智教务(通过深信服WebVPN访问) // 直接读取页面DOM解析课表,WebVPN代理下fetch可能失败 /** * 将周次字符串解析为数字数组 * 支持格式: 任意单周/单双周/区间/混合等组合 */ function parseWeeks(weekStr) { const weeks = []; if (!weekStr) return weeks; const pureWeekData = weekStr.split('(')[0]; pureWeekData.split(',').forEach(seg => { seg = seg.trim(); if (seg.includes('-')) { const [s, e] = seg.split('-').map(Number); if (!isNaN(s) && !isNaN(e)) { for (let i = s; i <= e; i++) weeks.push(i); } } else { const w = parseInt(seg); if (!isNaN(w)) weeks.push(w); } }); return [...new Set(weeks)].sort((a, b) => a - b); } /** * 获取包含课表表格的文档对象 * 优先当前页面,其次遍历iframe查找 */ function getScheduleDocument() { // 1. 当前页面直接包含课表 if (document.getElementById('kbtable')) { return document; } // 2. 在iframe中查找课表 const iframes = document.querySelectorAll('iframe'); for (let i = 0; i < iframes.length; i++) { try { const iframeDoc = iframes[i].contentDocument; if (iframeDoc && iframeDoc.getElementById('kbtable')) { return iframeDoc; } } catch (e) { // 跨域iframe无法访问,跳过 } } return null; } /** * 从DOM元素中按
分割提取行文本 * 解决未挂载元素innerText无换行的问题 */ function extractLines(element) { const lines = []; let currentLine = ''; for (let i = 0; i < element.childNodes.length; i++) { const node = element.childNodes[i]; if (node.nodeName === 'BR') { lines.push(currentLine.trim()); currentLine = ''; } else { currentLine += node.textContent; } } if (currentLine.trim()) lines.push(currentLine.trim()); return lines.filter(l => l.length > 0); } /** * 无法post到课表信息,故从课表HTML中解析时间段信息 * 行头格式: "第一大节\n08:30-10:00" * 每个大节包含2个小节 */ function parseTimeSlots(doc) { const table = doc.getElementById('kbtable'); if (!table) return []; const rows = table.querySelectorAll('tr'); const timeSlots = []; let sectionNum = 1; for (let i = 1; i < rows.length; i++) { const ths = rows[i].querySelectorAll('th'); if (ths.length === 0) continue; const headerText = ths[0].innerText.trim(); // 跳过备注行 if (headerText.startsWith('备注')) continue; // 提取时间范围,格式如 "08:30-10:00" const timeMatch = headerText.match(/(\d{2}:\d{2})-(\d{2}:\d{2})/); if (!timeMatch) continue; const startTime = timeMatch[1]; const endTime = timeMatch[2]; // 每小节固定45分钟。 // 第一小节从大节开始时间起算;第二小节从大节结束时间往前推45分钟。 // 这样既兼容90分钟大节(45+45无课间),也兼容95分钟大节(45+5课间+45) const [sh, sm] = startTime.split(':').map(Number); const [eh, em] = endTime.split(':').map(Number); const startMinutes = sh * 60 + sm; const endMinutes = eh * 60 + em; const CLASS_DURATION = 45; // 第一小节 timeSlots.push({ number: sectionNum++, startTime: startTime, endTime: formatTime(startMinutes + CLASS_DURATION) }); // 第二小节(从大节结束时间往前推45分钟) timeSlots.push({ number: sectionNum++, startTime: formatTime(endMinutes - CLASS_DURATION), endTime: endTime }); } return timeSlots; } /** * 将分钟数格式化为 HH:mm */ function formatTime(totalMinutes) { const h = Math.floor(totalMinutes / 60); const m = totalMinutes % 60; return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`; } /** * 解析课表HTML,提取课程数据 */ function parseCourses(doc) { const table = doc.getElementById('kbtable'); if (!table) return []; const courses = []; const rows = table.querySelectorAll('tr'); // 从第2行开始遍历(第1行是星期表头) for (let i = 1; i < rows.length; i++) { const tds = rows[i].querySelectorAll('td'); if (tds.length === 0) continue; // 遍历每一天的列(td索引0=星期一, 1=星期二, ..., 5=星期六) tds.forEach((cell, dayIndex) => { const day = dayIndex + 1; // 1=周一, 6=周六 // 获取显示视图的div(class="kbcontent") const contentDivs = cell.querySelectorAll('.kbcontent'); contentDivs.forEach(div => { const rawHtml = div.innerHTML.trim(); if (!rawHtml || rawHtml === ' ') return; // 按分隔线拆分为多个课程块 const blocks = rawHtml.split(/-{5,}/); blocks.forEach(block => { const tempDiv = document.createElement('div'); tempDiv.innerHTML = block; // 按
分割为行 const lines = extractLines(tempDiv); if (lines.length < 2) return; // 找到课程代码行(格式: 数字-数字+字母) let codeLineIdx = -1; for (let li = 0; li < lines.length; li++) { if (/^\d{6}-\d{6}/.test(lines[li])) { codeLineIdx = li; break; } } if (codeLineIdx === -1 || codeLineIdx + 1 >= lines.length) return; // 课程名称 = 代码行下一行,去除尾部O/P调课标记 const name = lines[codeLineIdx + 1] .replace(/[\s ]*[OP]\s*$/, '') .trim(); // 提取各字段 const teacherFont = tempDiv.querySelector('font[title="老师"]') || tempDiv.querySelector('font[title="教师"]'); const weekFont = tempDiv.querySelector('font[title="周次(节次)"]'); const roomFont = tempDiv.querySelector('font[title="教室"]'); if (!weekFont) return; const weekInfo = weekFont.textContent.trim(); const teacher = teacherFont ? teacherFont.textContent.trim() : ''; const position = roomFont ? roomFont.textContent.trim() : ''; // 解析节次 const secMatch = weekInfo.match(/\[(\d+)(?:-(\d+))?节\]/); if (!secMatch) return; const startSection = parseInt(secMatch[1]); const endSection = secMatch[2] ? parseInt(secMatch[2]) : startSection; // 解析周次 const weeks = parseWeeks(weekInfo); if (weeks.length === 0 || !name) return; courses.push({ name: name, teacher: teacher, position: position, day: day, startSection: startSection, endSection: endSection, weeks: weeks }); }); }); }); } return courses; } /** * 读取当前页面选中的学期ID */ function getSemesterId(doc) { const select = doc.getElementById('xnxq01id'); if (!select) return '未知学期'; return select.value || '未知学期'; } /** * 日期输入验证函数 */ window.validateDateInput = function (input) { if (/^\d{4}-\d{2}-\d{2}$/.test(input)) { const d = new Date(input); if (!isNaN(d.getTime())) return false; // 验证通过 } return "请输入正确的日期格式,如 2026-02-23"; }; /** * 根据学期ID推算默认开学日期 * 第一学期(秋季)默认9月1日,第二学期(春季)默认2月23日 */ function getDefaultStartDate(semesterId) { const match = semesterId.match(/^(\d{4})-(\d{4})-(\d)$/); if (!match) return ''; const year = parseInt(match[1]); const sem = parseInt(match[3]); if (sem === 1) { return `${year}-08-24`; } else { return `${parseInt(match[2])}-02-23`; } } /** * 获取用户输入的开学日期 * 本校寒暑假的开学时间都不固定,故采用此方案 */ async function getSemesterStartDate(semesterId) { const defaultDate = getDefaultStartDate(semesterId); const dateInput = await window.AndroidBridgePromise.showPrompt( "设置开学日期", "请输入本学期开学日期,一般为周一(格式 YYYY-MM-DD,如 2026-02-23):", defaultDate, "validateDateInput" ); return dateInput; // 用户取消返回 null } /** * 主流程 */ async function runImportFlow() { try { // 1. 确认提示 const confirmed = await window.AndroidBridgePromise.showAlert( "导入提示", "请确保您已登录教务系统并打开了【学期理论课表】页面(已选好学期)。\n脚本将直接读取当前页面的课表数据。", "确认并开始" ); if (!confirmed) return; // 2. 获取课表文档 const doc = getScheduleDocument(); if (!doc) { AndroidBridge.showToast("未找到课表页面,请先打开【学期理论课表】"); return; } const semesterId = getSemesterId(doc); // 3. 获取开学日期 const startDate = await getSemesterStartDate(semesterId); if (startDate === null) { AndroidBridge.showToast("已取消开学日期输入"); return; } // 4. 解析课程数据 const courses = parseCourses(doc); if (courses.length === 0) { AndroidBridge.showToast("未获取到课程数据,该学期可能暂无课表"); return; } // 5. 解析时间段 const timeSlots = parseTimeSlots(doc); // 6. 保存数据 if (timeSlots.length > 0) { await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots)); } await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses)); // 7. 保存课表配置(含开学日期) const config = { semesterStartDate: startDate, semesterTotalWeeks: 20, defaultClassDuration: 45, defaultBreakDuration: 5, firstDayOfWeek: 1 }; await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(config)); AndroidBridge.showToast(`[${semesterId}] 成功导入 ${courses.length} 条课程记录!`); AndroidBridge.notifyTaskCompletion(); } catch (error) { console.error("适配脚本异常:", error); AndroidBridge.showToast("异常: " + error.message); } } // 启动导入流程 runImportFlow();