| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346 |
- // 山东交通学院 (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元素中按<br>分割提取行文本
- * 解决未挂载元素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;
- // 按<br>分割为行
- 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();
|