|
@@ -0,0 +1,186 @@
|
|
|
|
|
+// 景德镇艺术职业大学 (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();
|