Kaynağa Gözat

add:南京林业大学适配

Mercury 1 ay önce
ebeveyn
işleme
f084c780aa
3 değiştirilmiş dosya ile 360 ekleme ve 0 silme
  1. 5 0
      index/root_index.yaml
  2. 9 0
      resources/NJFU/adapters.yaml
  3. 346 0
      resources/NJFU/njfu_01.js

+ 5 - 0
index/root_index.yaml

@@ -317,3 +317,8 @@ schools:
     name: "桂林医科大学"
     initial: "G"
     resource_folder: "GLMU"
+
+  - id: "NJFU"
+    name: "南京林业大学"
+    initial: "N"
+    resource_folder: "NJFU"

+ 9 - 0
resources/NJFU/adapters.yaml

@@ -0,0 +1,9 @@
+# resources/HUEL/adapters.yaml
+adapters:
+  - adapter_id: "NJFU_01"
+    adapter_name: "南京林业大学"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "njfu_01.js"
+    import_url: "https://jwxt.njfu.edu.cn/sso.jsp"
+    maintainer: "Mercury"
+    description: "南京林业大学适配教务"

+ 346 - 0
resources/NJFU/njfu_01.js

@@ -0,0 +1,346 @@
+const STANDARD_TIME_SLOTS = [
+    { number: 1, startTime: "08:00", endTime: "08:45" },
+    { number: 2, startTime: "08:55", endTime: "09:40" },
+    { number: 3, startTime: "10:00", endTime: "10:45" },
+    { number: 4, startTime: "10:55", endTime: "11:40" },
+    { number: 5, startTime: "14:00", endTime: "14:45" },
+    { number: 6, startTime: "14:50", endTime: "15:35" },
+    { number: 7, startTime: "15:55", endTime: "16:40" },
+    { number: 8, startTime: "16:45", endTime: "17:30" },
+    { number: 9, startTime: "18:30", endTime: "19:15" },
+    { number: 10, startTime: "19:20", endTime: "20:05" },
+    { number: 11, startTime: "20:10", endTime: "20:55" }
+];
+
+const CAMPUS_TIME_SLOTS = {
+    "新庄校区": STANDARD_TIME_SLOTS,
+    "淮安校区": STANDARD_TIME_SLOTS,
+    "白马校区": [
+        { number: 1, startTime: "08:30", endTime: "09:15" },
+        { number: 2, startTime: "09:20", endTime: "10:05" },
+        { number: 3, startTime: "10:25", endTime: "11:10" },
+        { number: 4, startTime: "11:15", endTime: "12:00" },
+        { number: 5, startTime: "14:00", endTime: "14:45" },
+        { number: 6, startTime: "14:50", endTime: "15:35" },
+        { number: 7, startTime: "15:55", endTime: "16:40" },
+        { number: 8, startTime: "16:45", endTime: "17:30" },
+        { number: 9, startTime: "18:30", endTime: "19:15" },
+        { number: 10, startTime: "19:20", endTime: "20:05" },
+        { number: 11, startTime: "20:10", endTime: "20:55" }
+    ]
+};
+
+const CAMPUS_KEYWORDS = [
+    { campus: "淮安校区", keywords: ["淮安校区"] },
+    { campus: "白马校区", keywords: ["白马校区"] },
+    { campus: "新庄校区", keywords: ["新庄校区"] }
+];
+
+function cleanPosition(position) {
+    return String(position || "")
+        .replace(/^(新庄校区|淮安校区|白马校区)/, "")
+        .replace(/[((]\d+人[))]\s*$/g, "")
+        .trim() || "待定";
+}
+
+function showToast(message) {
+    if (typeof window.AndroidBridge !== "undefined") {
+        AndroidBridge.showToast(message);
+    } else {
+        console.log(message);
+    }
+}
+
+function parseWeeks(rawText) {
+    if (!rawText) return [];
+
+    const weekPart = String(rawText)
+        .replace(/\s+/g, "")
+        .replace(/\(周\).*/, "")
+        .replace(/周次[::]?/g, "");
+
+    const weeks = new Set();
+    weekPart.split(/[,,]/).forEach((segment) => {
+        if (!segment) return;
+
+        const isOdd = segment.includes("单");
+        const isEven = segment.includes("双");
+        const cleaned = segment.replace(/[单双周]/g, "");
+        const match = cleaned.match(/^(\d+)(?:-(\d+))?$/);
+        if (!match) return;
+
+        const start = Number(match[1]);
+        const end = Number(match[2] || match[1]);
+        for (let week = start; week <= end; week++) {
+            if (isOdd && week % 2 === 0) continue;
+            if (isEven && week % 2 !== 0) continue;
+            weeks.add(week);
+        }
+    });
+
+    return Array.from(weeks).sort((a, b) => a - b);
+}
+
+function detectCampusOrNull(...texts) {
+    const text = texts
+        .filter(Boolean)
+        .map((item) => String(item))
+        .join(" ");
+
+    for (const item of CAMPUS_KEYWORDS) {
+        if (item.keywords.some((keyword) => text.includes(keyword))) {
+            return item.campus;
+        }
+    }
+    return null;
+}
+
+function readLineTexts(div) {
+    const cloned = div.cloneNode(true);
+    cloned.querySelectorAll(".item-box").forEach((node) => node.remove());
+    return cloned.innerHTML
+        .split(/<br\s*\/?>/i)
+        .map((line) => line.replace(/<[^>]+>/g, "").trim())
+        .filter(Boolean);
+}
+
+function extractCourseName(lines) {
+    const metadataPrefixes = ["通知单编号", "班级", "备注"];
+    const metadataKeywords = ["周", "节", "教师", "教室", "校区"];
+    const nameLines = [];
+
+    for (const line of lines) {
+        if (!line) continue;
+        if (metadataPrefixes.some((prefix) => line.startsWith(prefix))) break;
+        if (metadataKeywords.some((keyword) => line.includes(keyword))) break;
+        nameLines.push(line);
+    }
+
+    return nameLines.join("").trim();
+}
+
+function parseCourseBlock(blockHtml, fallbackDay) {
+    const tempDiv = document.createElement("div");
+    tempDiv.innerHTML = blockHtml;
+
+    const lines = readLineTexts(tempDiv);
+    if (!lines.length) return null;
+
+    const name = extractCourseName(lines);
+    const teacher = tempDiv.querySelector('font[title="教师"]')?.innerText.trim() || "未知";
+    const positionRaw = tempDiv.querySelector('font[title="教室"]')?.innerText.trim() || "待定";
+    const building = tempDiv.querySelector('font[title="教学楼"]')?.innerText.trim()
+        || tempDiv.querySelector('font[name="jxlmc"]')?.innerText.trim()
+        || "";
+    const position = cleanPosition(positionRaw);
+    const timeText = tempDiv.querySelector('font[title="周次(节次)"]')?.innerText.trim() || "";
+    if (!timeText) return null;
+
+    const weekMatch = timeText.match(/^(.*?)\(周\)/);
+    const sectionMatch = timeText.match(/\[(\d+)(?:-(\d+))?(?:-(\d+))?(?:-(\d+))?节\]/);
+    const weeks = parseWeeks(weekMatch ? weekMatch[1] : timeText);
+
+    let startSection = 0;
+    let endSection = 0;
+    if (sectionMatch) {
+        const values = sectionMatch.slice(1).filter(Boolean).map(Number);
+        startSection = values[0];
+        endSection = values[values.length - 1];
+    }
+
+    if (!name || !weeks.length || !startSection || !endSection) return null;
+
+    return {
+        name,
+        teacher,
+        position,
+        day: fallbackDay,
+        startSection,
+        endSection,
+        weeks,
+        campus: detectCampusOrNull(positionRaw, building, lines.join(" "))
+    };
+}
+
+function extractCoursesFromDoc(doc) {
+    const table = doc.getElementById("timetable");
+    if (!table) {
+        throw new Error("未获取到课表表格,请确认当前账号已登录教务系统。");
+    }
+
+    const rows = Array.from(table.querySelectorAll("tr")).slice(1, -1);
+    const courses = [];
+
+    rows.forEach((row) => {
+        const cells = Array.from(row.querySelectorAll("td"));
+        cells.forEach((cell, index) => {
+            const day = index + 1;
+            const detailDivs = cell.querySelectorAll("div.kbcontent");
+            detailDivs.forEach((div) => {
+                const html = div.innerHTML.trim();
+                if (!html || html === "&nbsp;") return;
+
+                const blocks = html.split(/-{10,}\s*<br\s*\/?>/i).filter((item) => item.trim());
+                if (!blocks.length) blocks.push(html);
+
+                blocks.forEach((block) => {
+                    const course = parseCourseBlock(block, day);
+                    if (course) {
+                        courses.push(course);
+                    }
+                });
+            });
+        });
+    });
+
+    const uniqueMap = new Map();
+    courses.forEach((course) => {
+        const key = [
+            course.name,
+            course.teacher,
+            course.position,
+            course.day,
+            course.startSection,
+            course.endSection,
+            course.weeks.join(",")
+        ].join("|");
+        if (!uniqueMap.has(key)) {
+            uniqueMap.set(key, course);
+        }
+    });
+
+    return Array.from(uniqueMap.values());
+}
+
+function choosePrimaryCampus(courses) {
+    for (const course of courses) {
+        if (course.campus) {
+            return course.campus;
+        }
+    }
+    return "新庄校区";
+}
+
+function normalizeCourses(courses, primaryCampus) {
+    return courses.map((course) => {
+        return {
+            name: course.name,
+            teacher: course.teacher,
+            position: course.position,
+            day: course.day,
+            startSection: course.startSection,
+            endSection: course.endSection,
+            weeks: course.weeks,
+            campus: course.campus || primaryCampus
+        };
+    });
+}
+
+function parseSemesterOptions(doc) {
+    const select = doc.getElementById("xnxq01id");
+    if (!select) return { labels: [], values: [], defaultIndex: 0 };
+
+    const labels = [];
+    const values = [];
+    let defaultIndex = 0;
+
+    Array.from(select.querySelectorAll("option")).forEach((option) => {
+        labels.push(option.innerText.trim());
+        values.push(option.value);
+        if (option.selected || option.hasAttribute("selected")) {
+            defaultIndex = labels.length - 1;
+        }
+    });
+
+    return { labels, values, defaultIndex };
+}
+
+async function fetchTermDoc(termValue) {
+    const body = new URLSearchParams();
+    if (termValue) body.append("xnxq01id", termValue);
+
+    const response = await fetch("/jsxsd/xskb/xskb_list.do", {
+        method: termValue ? "POST" : "GET",
+        headers: termValue ? { "Content-Type": "application/x-www-form-urlencoded" } : undefined,
+        body: termValue ? body.toString() : undefined,
+        credentials: "include"
+    });
+
+    const html = await response.text();
+    return new DOMParser().parseFromString(html, "text/html");
+}
+
+async function pickTerm(doc) {
+    const { labels, values, defaultIndex } = parseSemesterOptions(doc);
+    if (!labels.length || typeof window.AndroidBridgePromise === "undefined") {
+        return { doc, termLabel: labels[defaultIndex] || "" };
+    }
+
+    const selectedIndex = await window.AndroidBridgePromise.showSingleSelection(
+        "请选择要导入的学期",
+        JSON.stringify(labels),
+        defaultIndex
+    );
+    if (selectedIndex === null || selectedIndex === -1) {
+        throw new Error("已取消导入");
+    }
+
+    if (selectedIndex === defaultIndex) {
+        return { doc, termLabel: labels[selectedIndex] };
+    }
+
+    const selectedDoc = await fetchTermDoc(values[selectedIndex]);
+    return { doc: selectedDoc, termLabel: labels[selectedIndex] };
+}
+
+async function saveToApp(courses, primaryCampus) {
+    const timeSlots = CAMPUS_TIME_SLOTS[primaryCampus];
+    const allWeeks = courses.flatMap((course) => course.weeks || []);
+    const semesterTotalWeeks = allWeeks.length ? Math.max(...allWeeks) : 20;
+
+    if (typeof window.AndroidBridgePromise === "undefined") {
+        console.log("Primary campus:", primaryCampus);
+        console.log("Time slots:", timeSlots);
+        console.log("Courses:", courses);
+        alert(`解析完成:${primaryCampus},共 ${courses.length} 门课程。请查看控制台输出。`);
+        return;
+    }
+
+    await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify({
+        semesterTotalWeeks,
+        firstDayOfWeek: 1
+    }));
+    await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
+    const appCourses = courses.map(({ campus, ...course }) => course);
+    await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(appCourses));
+}
+
+async function runImportFlow() {
+    try {
+        showToast("正在获取 NJFU 课表数据...");
+        const initialDoc = await fetchTermDoc("");
+        const { doc, termLabel } = await pickTerm(initialDoc);
+        const parsedCourses = extractCoursesFromDoc(doc);
+
+        if (!parsedCourses.length) {
+            throw new Error("未解析到课程,请确认当前账号已登录教务系统。");
+        }
+
+        const primaryCampus = choosePrimaryCampus(parsedCourses);
+        const courses = normalizeCourses(parsedCourses, primaryCampus);
+
+        await saveToApp(courses, primaryCampus);
+
+        const message = `导入完成:${primaryCampus}${termLabel ? ` ${termLabel}` : ""}`;
+
+        showToast(message);
+        if (typeof window.AndroidBridge !== "undefined") {
+            AndroidBridge.notifyTaskCompletion();
+        }
+    } catch (error) {
+        console.error(error);
+        showToast(`导入失败: ${error.message}`);
+    }
+}
+
+runImportFlow();