// 苏州大学(suda.edu.cn)拾光课程表适配脚本
// Author:BernardYan2357
// ========================== 作息时间表 ==========================
const TimeSlots = [
{ number: 1, startTime: "08:00", endTime: "08:45" },
{ number: 2, startTime: "08:50", endTime: "09:35" },
{ number: 3, startTime: "09:55", endTime: "10:40" },
{ number: 4, startTime: "10:45", endTime: "11:30" },
{ number: 5, startTime: "11:35", endTime: "12:20" },
{ number: 6, startTime: "14:00", endTime: "14:45" },
{ number: 7, startTime: "14:50", endTime: "15:35" },
{ number: 8, startTime: "15:55", endTime: "16:40" },
{ number: 9, startTime: "16:45", endTime: "17:30" },
{ number: 10, startTime: "18:30", endTime: "19:15" },
{ number: 11, startTime: "19:25", endTime: "20:10" },
{ number: 12, startTime: "20:20", endTime: "21:05" }
];
// ========================== 解析函数 ==========================
/**
* 解析时间行,提取星期、节次、周次
* 示例: "周一第3,4,5节{第1-17周}" / "周二第8,9节{第2-16周|双周}"
*/
function parseTimeLine(timeLine) {
const dayMap = { "一": 1, "二": 2, "三": 3, "四": 4, "五": 5, "六": 6, "日": 7, "天": 7 };
const dayM = timeLine.match(/周([一二三四五六日天])/);
if (!dayM) return null;
const day = dayMap[dayM[1]];
const secM = timeLine.match(/第([0-9]+(?:,[0-9]+)*)节/);
if (!secM) return null;
const sections = secM[1].split(",").map(s => parseInt(s, 10)).filter(Number.isFinite);
if (!sections.length) return null;
const braceM = timeLine.match(/\{([^}]*)\}/);
if (!braceM) return null;
const brace = braceM[1];
const rangeM = brace.match(/第(\d+)-(\d+)周/);
if (!rangeM) return null;
const startWeek = parseInt(rangeM[1], 10);
const endWeek = parseInt(rangeM[2], 10);
if (!Number.isFinite(startWeek) || !Number.isFinite(endWeek) || endWeek < startWeek) return null;
let oddEven = "all";
if (/\|单周/.test(brace)) oddEven = "odd";
if (/\|双周/.test(brace)) oddEven = "even";
const weeks = [];
for (let w = startWeek; w <= endWeek; w++) {
if (oddEven === "odd" && w % 2 === 0) continue;
if (oddEven === "even" && w % 2 === 1) continue;
weeks.push(w);
}
return { day, sections, weeks };
}
/**
* 从课表 table 元素中解析所有课程
*/
function parseCourseTable(table) {
const anchors = Array.from(table.querySelectorAll("a"));
const courses = [];
for (const a of anchors) {
// innerHTML 确保
被转为换行符
const rawText = (a.innerHTML || "")
.replace(/
/gi, "\n")
.replace(/<[^>]*>/g, "")
.replace(/ /gi, " ")
.replace(/\u00a0/g, " ")
.replace(/\r/g, "")
.trim();
// 每个 的文本结构: 课程名 / 时间行 / 老师 / 地点
const lines = rawText.split("\n").map(s => s.trim()).filter(Boolean);
if (lines.length < 2) continue;
const name = lines[0] || "";
const timeLine = lines[1] || "";
const teacher = lines[2] || "";
const position = lines[3] || "";
const parsed = parseTimeLine(timeLine);
if (!parsed) continue;
courses.push({
name,
teacher,
position,
day: parsed.day,
startSection: Math.min(...parsed.sections),
endSection: Math.max(...parsed.sections),
weeks: parsed.weeks
});
}
// 去重 → 合并相邻节次
return mergeAdjacentSections(deduplicateCourses(courses));
}
/**
* 去除重复课程(同名同老师同地点同时间同周次视为重复)
*/
function deduplicateCourses(list) {
const seen = new Set();
return list.filter(c => {
const key = `${c.name}|${c.teacher}|${c.position}|${c.day}|${c.startSection}|${c.endSection}|${c.weeks.join(",")}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
/**
* 合并同一课程相邻/连续的节次
* 例如: 电工学(二)周一 第2节 + 第3,4,5节 → startSection=2, endSection=5
* 不同 weeks 不合并(如单双周保持独立)
*/
function mergeAdjacentSections(list) {
const groupMap = new Map();
for (const c of list) {
const key = `${c.name}|${c.teacher}|${c.position}|${c.day}|${c.weeks.join(",")}`;
if (!groupMap.has(key)) groupMap.set(key, []);
groupMap.get(key).push(c);
}
const merged = [];
for (const entries of groupMap.values()) {
entries.sort((a, b) => a.startSection - b.startSection);
let cur = { ...entries[0] };
for (let i = 1; i < entries.length; i++) {
const next = entries[i];
if (next.startSection <= cur.endSection + 1) {
cur.endSection = Math.max(cur.endSection, next.endSection);
} else {
merged.push(cur);
cur = { ...next };
}
}
merged.push(cur);
}
return merged;
}
/**
* 检测学期开始日期
* 读取 select#xqd(学期1/2)和 select#xnd(学年如"2025-2026")
* @param {Document} [doc=document] - 课表所在的 document(可能来自 iframe)
*/
function detectSemesterStartDate(doc) {
try {
doc = doc || document;
const xqdSelect = doc.querySelector('select#xqd');
const xndSelect = doc.querySelector('select#xnd');
if (!xqdSelect) return null;
const semester = parseInt(xqdSelect.value, 10);
let startYear = new Date().getFullYear();
if (xndSelect && xndSelect.value) {
const m = xndSelect.value.match(/(\d{4})/);
if (m) startYear = parseInt(m[1], 10);
}
if (semester === 1) return `${startYear}-09-01`;
if (semester === 2) return `${startYear + 1}-03-02`;
return null;
} catch (e) {
console.warn("检测学期开始日期失败:", e);
return null;
}
}
/**
* 查找课表 table,兼容直接打开课表页和通过 iframe 嵌套的情况
* @returns {HTMLTableElement|null}
*/
function findScheduleTable() {
const selector = 'table#Table1.schedule';
// 1. 先在当前文档查找
let table = document.querySelector(selector);
if (table) return table;
// 2. 遍历 iframe 查找(同源才能访问)
const iframes = document.querySelectorAll('iframe');
for (const iframe of iframes) {
try {
const doc = iframe.contentDocument || iframe.contentWindow?.document;
if (!doc) continue;
table = doc.querySelector(selector);
if (table) return table;
// 再查一层嵌套 iframe
for (const inner of doc.querySelectorAll('iframe')) {
try {
const innerDoc = inner.contentDocument || inner.contentWindow?.document;
if (!innerDoc) continue;
table = innerDoc.querySelector(selector);
if (table) return table;
} catch (_) { /* 跨域忽略 */ }
}
} catch (_) { /* 跨域忽略 */ }
}
return null;
}
// ========================== 主流程 ==========================
async function runImportFlow() {
// 1. 开始提示
const confirmed = await AndroidBridgePromise.showAlert(
"苏大课表导入",
"请确保当前页面已显示「学生个人课表」\n导入前请先在页面上选好学年和学期",
"开始导入"
);
if (!confirmed) {
AndroidBridge.showToast("用户取消了导入。");
return;
}
// 2. 查找课表
AndroidBridge.showToast("正在查找课表...");
const table = findScheduleTable();
if (!table) {
AndroidBridge.showToast("未找到课表,请先打开「学生个人课表」页面。");
return;
}
// 3. 解析课程
AndroidBridge.showToast("正在解析课程数据...");
const courses = parseCourseTable(table);
if (courses.length === 0) {
AndroidBridge.showToast("未解析到任何课程,请确认课表已正确加载。");
return;
}
// 4. 保存课程
AndroidBridge.showToast(`正在保存 ${courses.length} 条课程...`);
await AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
// 5. 保存作息时间
AndroidBridge.showToast(`正在导入 ${TimeSlots.length} 个时间段...`);
await AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(TimeSlots));
// 6. 保存配置(select 在课表同一文档中,用 ownerDocument 确保 iframe 场景正确)
const semesterStartDate = detectSemesterStartDate(table.ownerDocument);
const config = {
semesterStartDate: semesterStartDate,
semesterTotalWeeks: 20,
firstDayOfWeek: 1
};
await AndroidBridgePromise.saveCourseConfig(JSON.stringify(config));
// 7. 完成
AndroidBridge.showToast(`课程导入成功,共导入 ${courses.length} 条课程!`);
AndroidBridge.notifyTaskCompletion();
}
runImportFlow();