| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452 |
- // 从 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);
- }
- }
- })();
- })();
|