ソースを参照

Merge pull request #99 from XingHeYuZhuan/pending

Merge pull request #98 from Foraes/feat/wbu-adapter

Feat/武汉商学院超星教务系统适配
星河欲转 2 週間 前
コミット
9bdf9157d8
3 ファイル変更368 行追加1 行削除
  1. 6 1
      index/root_index.yaml
  2. 8 0
      resources/WBU/adapters.yaml
  3. 354 0
      resources/WBU/wbu_01.js

+ 6 - 1
index/root_index.yaml

@@ -186,4 +186,9 @@ schools:
   - id: "CQRK"
     name: "重庆人文科技学院"
     initial: "C"
-    resource_folder: "CQRK"    
+    resource_folder: "CQRK"    
+
+  - id: "WBU"
+    name: "武汉商学院"
+    initial: "W"
+    resource_folder: "WBU"

+ 8 - 0
resources/WBU/adapters.yaml

@@ -0,0 +1,8 @@
+adapters:
+  - adapter_id: "WBU_01"
+    adapter_name: "武汉商学院-超星教务系统"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "wbu_01.js"
+    import_url: "https://webvpn.wbu.edu.cn/"   # 这里放真实登录页
+    maintainer: "Foraes"
+    description: "武汉商学院教务系统课表导入适配"

+ 354 - 0
resources/WBU/wbu_01.js

@@ -0,0 +1,354 @@
+// 适配目标:武汉商学院-超星综合教学管理系统
+
+(function () {
+    function toast(message) {
+        if (window.AndroidBridge && typeof window.AndroidBridge.showToast === "function") {
+            window.AndroidBridge.showToast(message);
+        }
+    }
+
+    function sleep(ms) {
+        return new Promise((resolve) => setTimeout(resolve, ms));
+    }
+
+    async function getTargetDocument() {
+        if (location.href.includes("queryKbForXsd")) {
+            return document;
+        }
+
+        const iframe = document.querySelector("iframe[src*='queryKbForXsd']");
+        if (!iframe) return null;
+
+        for (let i = 0; i < 20; i += 1) {
+            try {
+                const doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document);
+                if (doc && doc.readyState && doc.readyState !== "loading") {
+                    return doc;
+                }
+            } catch (e) {
+                // ignore
+            }
+            await sleep(500);
+        }
+        return null;
+    }
+
+    async function waitForScheduleData(doc, timeoutMs = 15000) {
+        const start = Date.now();
+        while (Date.now() - start < timeoutMs) {
+            const cells = Array.from(doc.querySelectorAll("td.cell, td[id^='Cell']"));
+            const filled = cells.filter((cell) => {
+                const text = (cell.innerText || cell.textContent || "").trim();
+                return text.length > 0 && /周/.test(text);
+            });
+            if (filled.length > 0) {
+                return true;
+            }
+            await sleep(500);
+        }
+        return false;
+    }
+
+    function uniqueSortedNumbers(nums) {
+        const set = new Set(nums.filter((n) => Number.isFinite(n)));
+        return Array.from(set).sort((a, b) => a - b);
+    }
+
+    function parseWeekText(weekText) {
+        if (!weekText) return [];
+        let text = String(weekText).trim();
+        if (!text) return [];
+
+        let oddOnly = false;
+        let evenOnly = false;
+        if (text.includes("单")) oddOnly = true;
+        if (text.includes("双")) evenOnly = true;
+
+        text = text.replace(/周/g, "");
+        text = text.replace(/\s+/g, "");
+        text = text.replace(/\(.*?\)/g, "");
+        text = text.replace(/(.*?)/g, "");
+
+        const weeks = [];
+        const segments = text.split(",").map((s) => s.trim()).filter(Boolean);
+        segments.forEach((seg) => {
+            if (!seg) return;
+            const rangeMatch = seg.match(/^(\d+)-(\d+)$/);
+            if (rangeMatch) {
+                const start = parseInt(rangeMatch[1], 10);
+                const end = parseInt(rangeMatch[2], 10);
+                if (!Number.isFinite(start) || !Number.isFinite(end)) return;
+                for (let w = start; w <= end; w += 1) {
+                    weeks.push(w);
+                }
+                return;
+            }
+            const single = parseInt(seg, 10);
+            if (Number.isFinite(single)) weeks.push(single);
+        });
+
+        let filtered = weeks;
+        if (oddOnly && !evenOnly) {
+            filtered = weeks.filter((w) => w % 2 === 1);
+        } else if (evenOnly && !oddOnly) {
+            filtered = weeks.filter((w) => w % 2 === 0);
+        }
+
+        return uniqueSortedNumbers(filtered);
+    }
+
+    function splitCourseBlocks(cellText) {
+        const text = cellText.replace(/\r/g, "").trim();
+        if (!text) return [];
+        return text
+            .split(/\n{2,}/)
+            .map((block) => block.trim())
+            .filter(Boolean);
+    }
+
+    function extractWeeksTextFromLine(line) {
+        if (!line) return { weeksText: "", rest: line || "" };
+        const match = line.match(/(\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*)\s*(?:\((单|双)\))?\s*周/);
+        if (!match) return { weeksText: "", rest: line };
+        const weeksCore = match[1];
+        const oddEven = match[2] ? `(${match[2]})` : "";
+        const weeksText = `${weeksCore}${oddEven}周`;
+        const rest = line.replace(match[0], "").trim();
+        return { weeksText, rest };
+    }
+
+    function parseCourseBlock(block) {
+        const lines = block
+            .split(/\n+/)
+            .map((l) => l.trim())
+            .filter(Boolean);
+        if (!lines.length) return null;
+
+        const name = lines[0] || "";
+        let teacher = "";
+        let weeksText = "";
+        let position = "";
+
+        const weekLineIndex = lines.findIndex((l) => /周/.test(l));
+        if (weekLineIndex >= 0) {
+            const { weeksText: extractedWeeks, rest } = extractWeeksTextFromLine(
+                lines[weekLineIndex]
+            );
+            weeksText = extractedWeeks;
+            if (weekLineIndex === 1) {
+                teacher = rest || lines[1];
+            }
+        }
+
+        if (!teacher && lines.length > 1) {
+            teacher = lines[1];
+            const { weeksText: extractedWeeks, rest } = extractWeeksTextFromLine(teacher);
+            if (extractedWeeks) {
+                weeksText = weeksText || extractedWeeks;
+                teacher = rest;
+            }
+        }
+
+        if (!weeksText) {
+            for (const line of lines) {
+                const { weeksText: extractedWeeks } = extractWeeksTextFromLine(line);
+                if (extractedWeeks) {
+                    weeksText = extractedWeeks;
+                    break;
+                }
+            }
+        }
+
+        if (weekLineIndex >= 0 && weekLineIndex + 1 < lines.length) {
+            position = lines[weekLineIndex + 1];
+        }
+        if (!position) {
+            position =
+                lines.find((l) => l !== name && l !== teacher && !/周/.test(l)) || "";
+        }
+
+        return {
+            name: name || "未知课程",
+            teacher: teacher || "",
+            weeksText,
+            position: position || "",
+        };
+    }
+
+    function padTime(value) {
+        const text = String(value || "").trim();
+        const match = text.match(/^(\d{1,2}):(\d{2})$/);
+        if (!match) return text;
+        const h = match[1].padStart(2, "0");
+        return `${h}:${match[2]}`;
+    }
+
+    function randomColor() {
+        return Math.floor(Math.random() * 12) + 1;
+    }
+
+    function createId() {
+        if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
+            return crypto.randomUUID();
+        }
+        return `id-${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
+    }
+
+    function mergeCourses(courses) {
+        const byKey = new Map();
+        courses.forEach((course) => {
+            const weeksKey = (course.weeks || []).join(",");
+            const key = [course.name, course.teacher, course.position, course.day, weeksKey].join("|");
+            if (!byKey.has(key)) byKey.set(key, []);
+            byKey.get(key).push({ ...course });
+        });
+
+        const merged = [];
+        byKey.forEach((items) => {
+            items.sort((a, b) => a.startSection - b.startSection);
+            let current = null;
+            items.forEach((item) => {
+                if (!current) {
+                    current = { ...item };
+                    return;
+                }
+                if (item.startSection === current.endSection + 1) {
+                    current.endSection = Math.max(current.endSection, item.endSection);
+                } else {
+                    merged.push(current);
+                    current = { ...item };
+                }
+            });
+            if (current) merged.push(current);
+        });
+
+        return merged;
+    }
+
+    function parseScheduleFromDocument(doc) {
+        const cells = Array.from(doc.querySelectorAll("td.cell"));
+        const fallbackCells = cells.length ? [] : Array.from(doc.querySelectorAll("td[id^='Cell']"));
+        const targetCells = cells.length ? cells : fallbackCells;
+        const courses = [];
+        const seen = new Set();
+
+        targetCells.forEach((cell) => {
+            const id = cell.getAttribute("id") || "";
+            const match = id.match(/^Cell(\d)(\d{1,2})$/);
+            if (!match) return;
+
+            const day = parseInt(match[1], 10);
+            const startSection = parseInt(match[2], 10);
+            const rowspan = parseInt(cell.getAttribute("rowspan") || "1", 10);
+            const endSection = startSection + Math.max(rowspan, 1) - 1;
+
+            const blocks = splitCourseBlocks(cell.innerText || "");
+            blocks.forEach((blockText) => {
+                const parsed = parseCourseBlock(blockText);
+                if (!parsed) return;
+                const weeks = parseWeekText(parsed.weeksText);
+                if (!weeks.length) return;
+
+                const key = [
+                    parsed.name,
+                    parsed.teacher,
+                    parsed.position,
+                    day,
+                    startSection,
+                    endSection,
+                    weeks.join(","),
+                ].join("|");
+                if (seen.has(key)) return;
+                seen.add(key);
+
+                courses.push({
+                    id: createId(),
+                    name: parsed.name,
+                    teacher: parsed.teacher,
+                    position: parsed.position,
+                    day,
+                    startSection,
+                    endSection,
+                    color: randomColor(),
+                    weeks,
+                });
+            });
+        });
+
+        return mergeCourses(courses);
+    }
+
+    function parseTimeSlots(doc) {
+        const slots = [];
+        const seenNumbers = new Set();
+        const timeRegex = /(\d{1,2}:\d{2})/g;
+
+        const timeCells = Array.from(
+            doc.querySelectorAll("td[data-jcindex], td[data-jcIndex]")
+        );
+
+        timeCells.forEach((cell) => {
+            const text = (cell.innerText || cell.textContent || "").trim();
+            if (!text) return;
+
+            const indexAttr = cell.getAttribute("data-jcindex") || cell.getAttribute("data-jcIndex");
+            const numberMatch = text.match(/^(\d{1,2})/);
+            const number = parseInt(indexAttr || (numberMatch && numberMatch[1]) || "", 10);
+            if (!Number.isFinite(number)) return;
+
+            const times = text.match(timeRegex) || [];
+            if (times.length < 2) return;
+
+            if (seenNumbers.has(number)) return;
+            seenNumbers.add(number);
+
+            slots.push({
+                number,
+                startTime: padTime(times[0]),
+                endTime: padTime(times[1]),
+            });
+        });
+
+        return slots.sort((a, b) => a.number - b.number);
+    }
+
+    async function run() {
+        toast("开始解析课表...");
+        const doc = await getTargetDocument();
+        if (!doc) {
+            toast("未找到课表页面 iframe");
+            return;
+        }
+
+        await waitForScheduleData(doc);
+
+        const courses = parseScheduleFromDocument(doc);
+        const timeSlots = parseTimeSlots(doc);
+
+        if (!courses.length) {
+            toast("未解析到课程,请确认课表已加载完成");
+            return;
+        }
+
+        try {
+            const result = await window.AndroidBridgePromise.saveImportedCourses(
+                JSON.stringify(courses)
+            );
+            if (result === true) {
+                if (timeSlots.length) {
+                    await window.AndroidBridgePromise.savePresetTimeSlots(
+                        JSON.stringify(timeSlots)
+                    );
+                }
+                toast("课表导出成功");
+                window.AndroidBridge.notifyTaskCompletion();
+            } else {
+                toast("课表导出失败,请查看控制台日志");
+            }
+        } catch (error) {
+            toast("导出失败: " + error.message);
+        }
+    }
+
+    run();
+})();