|
|
@@ -0,0 +1,452 @@
|
|
|
+// 从 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);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })();
|
|
|
+})();
|