// 从 HTML 获取版
(function () {
function safeToast(message) {
try {
window.AndroidBridge && AndroidBridge.showToast(message);
} catch (_) {
console.log("[Toast Fallback]", message);
}
}
function firstNonEmpty(...values) {
for (const value of values) {
if (value !== undefined && value !== null && String(value).trim() !== "") {
return String(value).trim();
}
}
return "";
}
function parseValidWeeksBitmap(bitmap) {
if (!bitmap || typeof bitmap !== "string") return [];
const weeks = [];
for (let i = 0; i < bitmap.length; i++) {
if (bitmap[i] === "1") {
if (i >= 1) weeks.push(i);
}
}
return weeks;
}
function parseWeeksExpression(expr) {
const text = (expr || "").trim();
if (!text) return [];
const oddOnly = text.startsWith("单");
const evenOnly = text.startsWith("双");
const raw = text.replace(/^[单双]/, "");
const matchRange = raw.match(/^(\d+)\s*-\s*(\d+)$/);
if (matchRange) {
const start = parseInt(matchRange[1], 10);
const end = parseInt(matchRange[2], 10);
if (Number.isNaN(start) || Number.isNaN(end) || end < start) return [];
const weeks = [];
for (let w = start; w <= end; w++) {
if (oddOnly && w % 2 === 0) continue;
if (evenOnly && w % 2 !== 0) continue;
weeks.push(w);
}
return weeks;
}
const nums = raw
.split(/[,,、\s]+/)
.map((t) => parseInt(t, 10))
.filter((n) => !Number.isNaN(n) && n > 0);
if (!oddOnly && !evenOnly) return nums;
return nums.filter((w) => (oddOnly ? w % 2 === 1 : w % 2 === 0));
}
function normalizeWeeks(weeks) {
const uniq = Array.from(new Set((weeks || []).filter((n) => Number.isInteger(n) && n > 0)));
uniq.sort((a, b) => a - b);
return uniq;
}
function cleanCourseName(name) {
return String(name).replace(/\(\d+\)\s*$/, "").trim();
}
function extractTeacherFromCourse(obj) {
return firstNonEmpty(
obj.teacherName,
obj.teachers,
obj.teacher,
obj.teacherNames,
obj.teachername,
obj.courseteacher
);
}
function extractPositionFromCourse(obj) {
return firstNonEmpty(
obj.room,
obj.roomName,
obj.position,
obj.place,
obj.classroom,
obj.location,
obj.addr
);
}
function extractWeeksFromCourse(obj) {
return normalizeWeeks(parseValidWeeksBitmap(firstNonEmpty(
obj.vaildWeeks,
obj.validWeeks,
obj.weeks,
obj.weekBitmap,
obj.weekString
)));
}
function createCourseObject(name, teacher, position, day, startSection, endSection, weeks) {
return {
name: cleanCourseName(name),
teacher: teacher || "",
position: position || "",
day,
startSection,
endSection,
weeks: normalizeWeeks(weeks)
};
}
function parseCourseNameAndTeacher(courseWithTeacher) {
const text = (courseWithTeacher || "").trim();
if (!text) return { name: "", teacher: "" };
// 去掉课程名称末尾的序号
let cleaned = cleanCourseName(text);
// 匹配末尾教师名
const match = cleaned.match(/^(.*)\s+\(([^()]*)\)\s*$/);
if (match) {
return {
name: match[1].trim(),
teacher: match[2].trim()
};
}
return { name: cleaned, teacher: "" };
}
function parseTitleToCourses(titleText, day, section) {
if (!titleText || !titleText.trim()) return [];
const parts = titleText
.split(";")
.map((p) => p.trim())
.filter((p) => p.length > 0);
const results = [];
for (let i = 0; i < parts.length; i++) {
const current = parts[i];
const next = parts[i + 1] || "";
if (current.startsWith("(")) continue;
const { name, teacher } = parseCourseNameAndTeacher(current);
if (!name) continue;
let weeks = [];
let position = "";
if (next.startsWith("(") && next.endsWith(")")) {
const inner = next.slice(1, -1);
const commaIndex = inner.indexOf(",");
if (commaIndex >= 0) {
const weekExpr = inner.slice(0, commaIndex).trim();
position = inner.slice(commaIndex + 1).trim();
weeks = parseWeeksExpression(weekExpr);
} else {
const isPureWeeks = /^\d+[-,,]|^[单双]\d/.test(inner);
if (isPureWeeks) {
weeks = parseWeeksExpression(inner);
} else {
position = inner;
}
}
}
results.push(createCourseObject(name, teacher, position, day, section, section, weeks));
}
return results;
}
function parseFromCourseTableObjects() {
const candidates = [];
for (const key of Object.keys(window)) {
if (!/^table\d+$/.test(key)) continue;
const obj = window[key];
if (obj && Array.isArray(obj.activities) && Number.isInteger(obj.unitCounts)) {
candidates.push({ name: key, obj });
}
}
const courses = [];
for (const candidate of candidates) {
const table = candidate.obj;
const totalCells = table.activities.length;
let unitCount = table.unitCounts;
// 如果 unitCount > 7,尝试推断为总数,计算单行列数
if (unitCount > 7 && totalCells > 0) {
const deducedUnitCount = Math.floor(totalCells / 7);
if (deducedUnitCount > 0 && deducedUnitCount < totalCells && deducedUnitCount <= 12) {
unitCount = deducedUnitCount;
}
}
console.log(`[Debug] Table ${candidate.name}: unitCounts=${table.unitCounts}, totalCells=${totalCells}, deduced unitCount=${unitCount}`);
if (unitCount < 1 || unitCount >= totalCells) {
console.warn(`[Warn] Invalid unitCount ${unitCount} for table ${candidate.name}, skip`);
continue;
}
for (let index = 0; index < totalCells; index++) {
const activitiesInCell = table.activities[index];
if (!Array.isArray(activitiesInCell) || activitiesInCell.length === 0) continue;
const day = Math.floor(index / unitCount) + 1;
const section = (index % unitCount) + 1;
if (day < 1 || day > 7 || section < 1 || section > 12) continue;
for (const act of activitiesInCell) {
if (!act) continue;
let name = firstNonEmpty(act.courseName, act.name);
if (!name) continue;
const teacher = extractTeacherFromCourse(act);
const position = extractPositionFromCourse(act);
const weeks = extractWeeksFromCourse(act);
courses.push(createCourseObject(name, teacher, position, day, section, section, weeks));
}
}
}
return courses;
}
function parseFromHtmlTableFallback() {
const table = document.querySelector("#manualArrangeCourseTable");
if (!table) return [];
const bodyRows = table.querySelectorAll("tbody tr");
const courses = [];
bodyRows.forEach((row, rowIndex) => {
const cells = row.querySelectorAll("td");
if (cells.length < 8) return;
const section = rowIndex + 1;
for (let col = 1; col <= 7; col++) {
const td = cells[col];
if (!td) continue;
const title = td.getAttribute("title") || "";
if (!title.trim()) continue;
const day = col;
const parsed = parseTitleToCourses(title, day, section);
courses.push(...parsed);
}
});
return courses;
}
function extractPositionFromTitle(title) {
const positionMatch = title.match(/\(([^(),]*)\)\s*$/);
if (!positionMatch) return "";
const potential = positionMatch[1].trim();
// 排除掉是周次表达式的情况
if (!/^\d+[-~]|^[单双]|^\d+$/.test(potential)) {
return potential;
}
return "";
}
function supplementPositionFromHtml(courses) {
const table = document.querySelector("#manualArrangeCourseTable");
if (!table) return courses;
const courseMap = {};
for (const course of courses) {
// 用课程名、教师、日期、时间作为 key
const key = `${course.name}|${course.teacher}|${course.day}|${course.startSection}`;
if (!courseMap[key]) {
courseMap[key] = [];
}
courseMap[key].push(course);
}
const bodyRows = table.querySelectorAll("tbody tr");
bodyRows.forEach((row, rowIndex) => {
const cells = row.querySelectorAll("td");
if (cells.length < 8) return;
const section = rowIndex + 1;
for (let col = 1; col <= 7; col++) {
const td = cells[col];
if (!td) continue;
const title = td.getAttribute("title") || "";
if (!title.trim()) continue;
const day = col;
const position = extractPositionFromTitle(title);
// 从 title 提取课程信息并匹配
const titleParts = title.split(";").map(p => p.trim()).filter(p => p && !p.startsWith("("));
for (const part of titleParts) {
const { name, teacher } = parseCourseNameAndTeacher(part);
if (!name) continue;
const key = `${name}|${teacher}|${day}|${section}`;
if (courseMap[key]) {
for (const course of courseMap[key]) {
if (!course.position) {
course.position = position;
}
}
}
}
}
});
return courses;
}
function mergeContiguousSections(courses) {
const normalized = (courses || [])
.filter((c) => c && c.name && Number.isInteger(c.day) && Number.isInteger(c.startSection) && Number.isInteger(c.endSection))
.map((c) => ({
...c,
weeks: normalizeWeeks(c.weeks)
}));
normalized.sort((a, b) => {
const ak = `${a.name}|${a.teacher}|${a.position}|${a.day}|${a.weeks.join(",")}`;
const bk = `${b.name}|${b.teacher}|${b.position}|${b.day}|${b.weeks.join(",")}`;
if (ak < bk) return -1;
if (ak > bk) return 1;
return a.startSection - b.startSection;
});
const merged = [];
for (const item of normalized) {
const prev = merged[merged.length - 1];
const isContinuous = prev
&& prev.name === item.name
&& prev.teacher === item.teacher
&& prev.position === item.position
&& prev.day === item.day
&& prev.weeks.join(",") === item.weeks.join(",")
&& prev.endSection + 1 >= item.startSection;
if (isContinuous) {
prev.endSection = Math.max(prev.endSection, item.endSection);
} else {
merged.push({ ...item });
}
}
return merged;
}
async function exportAllCourseData() {
safeToast("开始解析教务课表...");
console.log("[Exporter] 开始解析课表");
let parsedCourses = parseFromCourseTableObjects();
if (parsedCourses.length === 0) {
console.warn("[Exporter] 未从 tableX.activities 取到数据,尝试 HTML 兜底解析");
parsedCourses = parseFromHtmlTableFallback();
} else {
console.log(`[Exporter] 从 table.activities 获取 ${parsedCourses.length} 条课程,尝试补充位置信息...`);
// 尝试从 HTML 补充位置信息
parsedCourses = supplementPositionFromHtml(parsedCourses);
console.log(`[Exporter] 补充位置后 ${parsedCourses.length} 条课程`);
}
parsedCourses = mergeContiguousSections(parsedCourses);
if (parsedCourses.length === 0) {
throw new Error("未在当前页面识别到可导出的课程数据,请确认已打开我的课表页面。");
}
console.log(`[Exporter] 解析完成,课程条目数: ${parsedCourses.length}`);
console.log(`[Exporter] 样本课程:`, JSON.stringify(parsedCourses.slice(0, 2), null, 2));
const presetTimeSlots = [
{
"number": 1,
"startTime": "08:00",
"endTime": "08:45"
},
{
"number": 2,
"startTime": "10:05",
"endTime": "11:40"
},
{
"number": 3,
"startTime": "14:00",
"endTime": "15:35"
},
{
"number": 4,
"startTime": "16:05",
"endTime": "17:40"
},
{
"number": 5,
"startTime": "19:00",
"endTime": "20:35"
},
{
"number": 6,
"startTime": "20:45",
"endTime": "22:20"
}
];
await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(parsedCourses));
await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(presetTimeSlots));
safeToast(`导出成功,共 ${parsedCourses.length} 条课程`);
}
(async function run() {
try {
await exportAllCourseData();
} catch (error) {
console.error("[Exporter] 导出失败:", error);
safeToast(`导出失败:${error.message}`);
} finally {
try {
window.AndroidBridge && AndroidBridge.notifyTaskCompletion();
} catch (e) {
console.error("[Exporter] notifyTaskCompletion 调用失败:", e);
}
}
})();
})();