Pārlūkot izejas kodu

add: 长江大学教务系统适配

Jursin 1 nedēļu atpakaļ
vecāks
revīzija
7ffa18c584
3 mainītis faili ar 466 papildinājumiem un 1 dzēšanām
  1. 6 1
      index/root_index.yaml
  2. 8 0
      resources/YU/adapters.yaml
  3. 452 0
      resources/YU/yu.js

+ 6 - 1
index/root_index.yaml

@@ -226,4 +226,9 @@ schools:
   - id: "ZHKU"
     name: "仲恺农业工程学院"
     initial: "Z"
-    resource_folder: "ZHKU"
+    resource_folder: "ZHKU"
+
+  - id: "YU"
+    name: "长江大学"
+    initial: "C"
+    resource_folder: "YU"

+ 8 - 0
resources/YU/adapters.yaml

@@ -0,0 +1,8 @@
+adapters:
+  - adapter_id: "YU_01"
+    adapter_name: "长江大学教务系统(树维)"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "yu.js"
+    import_url: "https://ehall.yangtzeu.edu.cn"
+    maintainer: "Jursin"
+    description: "长江大学教务系统(树维)适配"

+ 452 - 0
resources/YU/yu.js

@@ -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);
+            }
+        }
+    })();
+})();