// 景德镇艺术职业大学 (jdzvua.edu.cn) 拾光课程表适配脚本 // 纯 HTML 抓取:课表在 iframe 内 #manualArrangeCourseTable,切换学期无网络请求, // 请先在页面上切换到目标学期课表再执行导入 // 课程信息在 td.infoTitle 的 title 属性:`课程名(编码) (教师);(周次,地点;...` 成对出现, // td id 形如 TD{星期0-6}{节次-1}_0,rowspan 为跨节数 // 作息按楼栋分两组(08:00 起 / 08:20 起),课程覆盖多的一组写入预设作息, // 少的一组课程按其楼栋时间写入自定义时间 const TIME_SLOT_A = [ { number: 1, startTime: "08:20", endTime: "09:05" }, { number: 2, startTime: "09:15", endTime: "10:00" }, { number: 3, startTime: "10:35", endTime: "11:20" }, { number: 4, startTime: "11:30", endTime: "12:15" }, { number: 5, startTime: "14:20", endTime: "15:05" }, { number: 6, startTime: "15:15", endTime: "16:00" }, { number: 7, startTime: "16:35", endTime: "17:20" }, { number: 8, startTime: "17:30", endTime: "18:15" }, { number: 9, startTime: "19:20", endTime: "20:05" }, { number: 10, startTime: "20:15", endTime: "21:00" } ]; const TIME_SLOT_B = [ { number: 1, startTime: "08:00", endTime: "08:45" }, { number: 2, startTime: "08:55", endTime: "09:40" }, { number: 3, startTime: "10:25", endTime: "11:10" }, { number: 4, startTime: "11:20", endTime: "12:05" }, { number: 5, startTime: "14:00", endTime: "14:45" }, { number: 6, startTime: "14:55", endTime: "15:40" }, { number: 7, startTime: "16:25", endTime: "17:10" }, { number: 8, startTime: "17:20", endTime: "18:05" }, { number: 9, startTime: "19:20", endTime: "20:05" }, { number: 10, startTime: "20:15", endTime: "21:00" } ]; // 08:20 起的楼栋组 const BUILDINGS_A = ["上茶苑教6栋", "上茶苑教8栋", "芭蕉苑教14栋", "芭蕉苑教16栋", "芭蕉苑教18栋", "芭蕉苑教20栋", "长芗苑教10栋", "长芗苑教12栋", "香樟苑教2栋", "香樟苑教4栋"]; // 08:00 起的楼栋组 const BUILDINGS_B = ["上茶苑教7栋", "上茶苑教9栋", "体育馆", "梧桐苑", "芭蕉苑教15栋", "芭蕉苑教17栋", "芭蕉苑教19栋", "芭蕉苑教21栋", "长芗苑教11栋", "长芗苑教13栋", "香樟苑教1栋", "香樟苑教3栋", "香樟苑教5栋"]; // 周次:"16-17" / "2-6 9-12" / "18",空格分隔多段 function parseWeeks(text) { const weeks = []; for (const part of String(text).trim().split(/\s+/)) { const range = part.match(/^(\d+)-(\d+)$/); if (range) { for (let w = Number(range[1]); w <= Number(range[2]); w++) weeks.push(w); } else if (/^\d+$/.test(part)) { weeks.push(Number(part)); } } return [...new Set(weeks)].sort((a, b) => a - b); } // 地点末尾的校区标记 (景艺) 在 title 里常缺右括号或多一个右括号,按实际形态归一 function normalizeRoom(room) { const text = String(room).trim(); if (/\(景艺$/.test(text)) return text + ")"; if (/\(景艺\)\)$/.test(text)) return text.slice(0, -1); return text; } // 解析单个课表格子,title 按分号切成「课程/周次地点」交替的 token function parseCell(id, rowspan, title) { const matched = String(id).match(/^TD(\d+)_/); if (!matched) return []; const n = Number(matched[1]); const day = Math.floor(n / 10) + 1; const startSection = (n % 10) + 1; const endSection = startSection + (Number(rowspan) || 1) - 1; if (day > 7 || endSection > 10) return []; const courses = []; const tokens = String(title).split(";").map(s => s.trim()).filter(Boolean); for (let i = 0; i < tokens.length; i++) { if (tokens[i].charAt(0) === "(") continue; const info = tokens[i + 1]; if (!info || info.charAt(0) !== "(") continue; i++; const body = info.slice(1); const comma = body.indexOf(","); if (comma < 0) continue; const weeks = parseWeeks(body.slice(0, comma)); if (weeks.length === 0) continue; const head = tokens[i - 1].match(/^(.+?)\(([^)]+)\)\s*\(([^)]*)\)/); courses.push({ name: head ? head[1] : tokens[i - 1], teacher: head ? head[3] : "", position: normalizeRoom(body.slice(comma + 1)), day: day, startSection: startSection, endSection: endSection, weeks: weeks }); } return courses; } // 在当前文档与各级 frame 中查找课表 function findCourseTable(doc) { const table = doc.querySelector("#manualArrangeCourseTable"); if (table) return table; for (const frame of doc.querySelectorAll("iframe,frame")) { try { const found = findCourseTable(frame.contentDocument); if (found) return found; } catch (e) { // 跨域 frame 取不到 contentDocument,跳过 } } return null; } // 楼栋匹配作息组,未匹配(如实践场地)返回 null 用预设作息 function groupOfPosition(position) { for (const building of BUILDINGS_A) if (position.includes(building)) return TIME_SLOT_A; for (const building of BUILDINGS_B) if (position.includes(building)) return TIME_SLOT_B; return null; } // 按所在作息组写入自定义时间(两组作息仅少数节次不同) function applyCustomTime(course, group) { const start = group.find(s => s.number === course.startSection); const end = group.find(s => s.number === course.endSection); if (!start || !end) return course; return { ...course, isCustomTime: true, customStartTime: start.startTime, customEndTime: end.endTime }; } async function runImportFlow() { const confirmed = await window.shiguangBridgePromise.showAlert( "教务系统课表导入", "导入前请先在页面上切换到要导入的学期课表,再点击导入", "好的,开始导入"); if (!confirmed) { window.shiguangBridge.showToast("用户取消了导入。"); return; } const table = findCourseTable(document); if (!table) { window.shiguangBridge.showToast("导入失败:未找到课表,请确认已打开课表页面。"); return; } const courses = []; for (const td of table.querySelectorAll('td[id^="TD"]')) { const title = td.getAttribute("title"); if (!title) continue; courses.push(...parseCell(td.id, td.getAttribute("rowspan"), title)); } if (courses.length === 0) { window.shiguangBridge.showToast("未解析到课程,请确认课表已加载。"); return; } // 统计两组楼栋各覆盖多少课程:覆盖多的一组直接写入预设作息, // 少的一组课程按其楼栋时间写入自定义时间,数量相同时默认 08:00 组 let countA = 0; let countB = 0; for (const course of courses) { const group = groupOfPosition(course.position); if (group === TIME_SLOT_A) countA++; else if (group === TIME_SLOT_B) countB++; } const majority = countA > countB ? TIME_SLOT_A : TIME_SLOT_B; const result = courses.map(course => { const group = groupOfPosition(course.position); return group && group !== majority ? applyCustomTime(course, group) : course; }); result.sort((a, b) => a.day - b.day || a.startSection - b.startSection || a.name.localeCompare(b.name)); try { await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(result)); } catch (e) { window.shiguangBridge.showToast("课程保存失败: " + e.message); return; } try { await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(majority)); } catch (e) {} window.shiguangBridge.showToast("课程导入成功,共导入 " + result.length + " 门课程!"); window.shiguangBridge.notifyTaskCompletion(); } runImportFlow();