Explorar el Código

Merge pull request #490 from XingHeYuZhuan/pending

星河欲转 hace 3 semanas
padre
commit
b17399d662
Se han modificado 3 ficheros con 268 adiciones y 0 borrados
  1. 5 0
      index/root_index.yaml
  2. 8 0
      resources/NTU/adapters.yaml
  3. 255 0
      resources/NTU/ntu.js

+ 5 - 0
index/root_index.yaml

@@ -737,3 +737,8 @@ schools:
     name: "青岛黄海学院"
     initial: "Q"
     resource_folder: "QDHHC"
+
+  - id: "NTU"
+    name: "南通大学"
+    initial: "N"
+    resource_folder: "NTU"

+ 8 - 0
resources/NTU/adapters.yaml

@@ -0,0 +1,8 @@
+adapters:
+  - adapter_id: "NTU"
+    adapter_name: "南通大学正方教务"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "ntu.js"
+    import_url: "https://tdjw.ntu.edu.cn/jwglxt/xtgl/login_slogin.html"
+    maintainer: "MX703"
+    description: "南通大学正方教务V9适配。"

+ 255 - 0
resources/NTU/ntu.js

@@ -0,0 +1,255 @@
+// 南通大学拾光课程表适配脚本
+// 通过正方教务V9个人/班级课表接口获取课程,并转换为导入格式
+
+(function () {
+const WINTER_TIME_SLOTS = [
+    { number: 1, startTime: "07:50", endTime: "08:30" },
+    { number: 2, startTime: "08:40", endTime: "09:20" },
+    { number: 3, startTime: "09:35", endTime: "10:15" },
+    { number: 4, startTime: "10:30", endTime: "11:10" },
+    { number: 5, startTime: "11:20", endTime: "12:00" },
+    { number: 6, startTime: "13:30", endTime: "14:10" },
+    { number: 7, startTime: "14:20", endTime: "15:00" },
+    { number: 8, startTime: "15:20", endTime: "16:00" },
+    { number: 9, startTime: "16:10", endTime: "16:50" },
+    { number: 10, startTime: "18:30", endTime: "19:10" },
+    { number: 11, startTime: "19:20", endTime: "20:00" },
+    { number: 12, startTime: "20:10", endTime: "20:50" }
+];
+
+const SUMMER_TIME_SLOTS = [
+    { number: 1, startTime: "07:50", endTime: "08:30" },
+    { number: 2, startTime: "08:40", endTime: "09:20" },
+    { number: 3, startTime: "09:35", endTime: "10:15" },
+    { number: 4, startTime: "10:30", endTime: "11:10" },
+    { number: 5, startTime: "11:20", endTime: "12:00" },
+    { number: 6, startTime: "14:00", endTime: "14:40" },
+    { number: 7, startTime: "14:50", endTime: "15:30" },
+    { number: 8, startTime: "15:50", endTime: "16:30" },
+    { number: 9, startTime: "16:40", endTime: "17:20" },
+    { number: 10, startTime: "19:00", endTime: "19:40" },
+    { number: 11, startTime: "19:50", endTime: "20:30" },
+    { number: 12, startTime: "20:40", endTime: "21:20" }
+];
+
+function parseSections(value) {
+    const numbers = String(value || "").match(/\d+/g);
+    if (!numbers || numbers.length === 0) return null;
+    const startSection = Number(numbers[0]);
+    const endSection = Number(numbers[numbers.length - 1]);
+    if (!Number.isInteger(startSection) || !Number.isInteger(endSection) || startSection < 1 || endSection < startSection) return null;
+    return { startSection, endSection };
+}
+
+// 将周次文本转换为周数数组
+function parseWeeks(value) {
+    const weeks = new Set();
+    String(value || "").replace(/(/g, "(").replace(/)/g, ")").split(/[,,、;]/).forEach((part) => {
+        const numbers = part.match(/\d+/g);
+        if (!numbers) return;
+        const start = Number(numbers[0]);
+        const end = Number(numbers[numbers.length - 1]);
+        const odd = part.includes("单");
+        const even = part.includes("双");
+        for (let week = start; week <= end; week += 1) {
+            if (odd && week % 2 === 0) continue;
+            if (even && week % 2 !== 0) continue;
+            weeks.add(week);
+        }
+    });
+    return [...weeks].sort((left, right) => left - right);
+}
+
+function normalizeStartDate(value) {
+    const match = String(value || "").match(/(\d{4})[-\/.年](\d{1,2})[-\/.月](\d{1,2})/);
+    if (!match) return null;
+    return `${match[1]}-${match[2].padStart(2, "0")}-${match[3].padStart(2, "0")}`;
+}
+
+function findStartDate(value) {
+    if (!value || typeof value !== "object") return normalizeStartDate(value);
+    if (Array.isArray(value)) {
+        const firstWeek = value.find((item) => String(item?.zs) === "1" || String(item?.zsmc) === "1") || value[0];
+        return normalizeStartDate(firstWeek?.zrq || firstWeek?.zcrq || firstWeek?.rq);
+    }
+    const firstWeekDate = normalizeStartDate(value.zrq || value.zcrq || value.rq);
+    if (firstWeekDate) return firstWeekDate;
+    for (const [key, item] of Object.entries(value)) {
+        const date = item && typeof item === "object" ? findStartDate(item) : null;
+        if (date) return date;
+    }
+    return null;
+}
+
+async function fetchSemesterStartDate() {
+    const xnm = document.querySelector("#xnm")?.value;
+    const xqm = document.querySelector("#xqm")?.value;
+    if (!xnm || !xqm) {
+        console.warn("NTU semester start date: missing xnm or xqm", { xnm, xqm });
+        return null;
+    }
+    const url = new URL("/jwglxt/kbcx/xskbcxZccx_cxZcByXnxq.html?gnmkdm=N2154", window.location.origin);
+    try {
+        const response = await fetch(url.href, {
+            headers: {
+                accept: "application/json, text/javascript, */*; q=0.01",
+                "content-type": "application/x-www-form-urlencoded;charset=UTF-8",
+                "x-requested-with": "XMLHttpRequest"
+            },
+            body: `xnm=${encodeURIComponent(xnm)}&xqm=${encodeURIComponent(xqm)}`,
+            method: "POST",
+            credentials: "include"
+        });
+        const rawText = await response.text();
+        let data;
+        try {
+            data = JSON.parse(rawText);
+        } catch {
+            console.warn("NTU semester start date: response is not JSON", response.status, rawText);
+            return null;
+        }
+        console.log("NTU semester start date response:", data);
+        const startDate = findStartDate(data);
+        console.log("NTU semester start date:", startDate || "not found");
+        return startDate;
+    } catch (error) {
+        console.warn("NTU semester start date request failed:", error);
+        return null;
+    }
+}
+
+// 将课程字段转换为导入协议字段
+function normalizeCourses(rawCourses) {
+    const uniqueCourses = new Map();
+    rawCourses.forEach((rawCourse) => {
+        const name = String(rawCourse.kcmc || "").replace(/[■◆▲]/g, "").trim();
+        const teacher = String(rawCourse.xm || "未知").trim() || "未知";
+        const position = String(rawCourse.cdmc || "待定").trim() || "待定";
+        const day = Number(rawCourse.xqj);
+        const sections = parseSections(rawCourse.jcs || rawCourse.jc);
+        const weeks = parseWeeks(rawCourse.zcd);
+        if (!name || !sections || !Number.isInteger(day) || day < 1 || day > 7 || weeks.length === 0) return;
+        const course = { name, teacher, position, day, startSection: sections.startSection, endSection: sections.endSection, weeks };
+        const key = [name, teacher, position, day, sections.startSection, sections.endSection, weeks.join(",")].join("|");
+        if (!uniqueCourses.has(key)) uniqueCourses.set(key, course);
+    });
+    return [...uniqueCourses.values()].sort((left, right) => left.day - right.day || left.startSection - right.startSection || left.name.localeCompare(right.name));
+}
+
+// 合并同一课程的重复节次
+function mergeAndDistinctCourses(courses) {
+    if (courses.length <= 1) return courses;
+    const list = courses.map((course) => ({ ...course, weeks: [...new Set(course.weeks)].sort((left, right) => left - right) }));
+    const sameBase = (left, right) => left.name === right.name && left.teacher === right.teacher && left.position === right.position && left.day === right.day;
+    list.sort((left, right) => left.name.localeCompare(right.name) || left.teacher.localeCompare(right.teacher) || left.position.localeCompare(right.position) || left.day - right.day || left.startSection - right.startSection);
+    const merged = [];
+    for (const course of list) {
+        const previous = merged[merged.length - 1];
+        if (previous && sameBase(previous, course) && previous.startSection === course.startSection && previous.endSection === course.endSection) {
+            previous.weeks = [...new Set([...previous.weeks, ...course.weeks])].sort((left, right) => left - right);
+        } else if (previous && sameBase(previous, course) && previous.weeks.join(",") === course.weeks.join(",") && previous.endSection + 1 === course.startSection) {
+            previous.endSection = course.endSection;
+        } else {
+            merged.push(course);
+        }
+    }
+    return merged;
+}
+
+// 个人课表
+async function fetchApiCourses() {
+    const xnm = document.querySelector("#xnm")?.value;
+    const xqm = document.querySelector("#xqm")?.value;
+    if (!xnm || !xqm) return null;
+    const url = new URL("/jwglxt/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N2151", window.location.origin);
+    try {
+        const response = await fetch(url.href, {
+            method: "POST",
+            headers: { "content-type": "application/x-www-form-urlencoded;charset=UTF-8", "x-requested-with": "XMLHttpRequest" },
+            body: `xnm=${encodeURIComponent(xnm)}&xqm=${encodeURIComponent(xqm)}&kzlx=ck&xsdm=&kclbdm=`,
+            credentials: "include"
+        });
+        const data = await response.json();
+        console.log("NTU course API response:", data);
+        const rawCourses = data?.kbList || data?.datas?.kbList || data?.rows;
+        if (!Array.isArray(rawCourses) || !rawCourses.length) return null;
+        return mergeAndDistinctCourses(normalizeCourses(rawCourses));
+    } catch (error) {
+        console.warn("NTU course API request failed:", error);
+        return null;
+    }
+}
+
+// 班级课表
+async function fetchClassApiCourses() {
+    const pageMap = window.api?.data?.map || {};
+    const readValue = (key) => pageMap[key] ?? document.querySelector(`#${key}`)?.value ?? "";
+    const requestFields = [
+        "xnm",
+        "xqm",
+        "njdm_id",
+        "zyh_id",
+        "bh_id",
+        "tjkbzdm",
+        "tjkbzxsdm",
+        "zxszjjs"
+    ];
+    const body = new URLSearchParams();
+    requestFields.forEach((key) => {
+        const value = readValue(key);
+        if (value !== "" && value !== null && typeof value !== "undefined") body.set(key, String(value));
+    });
+    if (!body.get("xnm") || !body.get("xqm") || !body.get("bh_id")) {
+        console.warn("NTU class course API parameters are incomplete", Object.fromEntries(body));
+        return null;
+    }
+    const url = new URL("/jwglxt/kbdy/bjkbdy_cxBjKb.html", window.location.origin);
+    try {
+        const response = await fetch(url.href, {
+            method: "POST",
+            headers: { "content-type": "application/x-www-form-urlencoded;charset=UTF-8", "x-requested-with": "XMLHttpRequest" },
+            body,
+            credentials: "include"
+        });
+        const data = await response.json();
+        console.log("NTU class course API response:", data);
+        const rawCourses = data?.kbList || data?.datas?.kbList || data?.rows;
+        if (!Array.isArray(rawCourses) || !rawCourses.length) return null;
+        return mergeAndDistinctCourses(normalizeCourses(rawCourses));
+    } catch (error) {
+        console.warn("NTU class course API request failed:", error);
+        return null;
+    }
+}
+
+async function runImport() {
+    try {
+        if (!await window.shiguangBridgePromise.showAlert("南通大学课表导入", "导入前请确保已登录南通大学教务系统。", "开始导入")) return;
+        const formAction = document.querySelector("#ajaxForm")?.getAttribute("action") || "";
+        const isPersonalPage = /xskbcx_cxXskbcxIndex/i.test(formAction) || /\/kbcx\/xskbcx/i.test(window.location.pathname);
+        const pageCourses = isPersonalPage ? await fetchApiCourses() : await fetchClassApiCourses();
+        if (pageCourses?.length) {
+            const pageWeeks = pageCourses.flatMap((course) => course.weeks);
+            const semesterStartDate = await fetchSemesterStartDate();
+            if (!semesterStartDate) throw new Error("未获取到开学日期,无法判断令时");
+            const startMonth = Number(semesterStartDate.slice(5, 7));
+            if (!Number.isInteger(startMonth) || startMonth < 1 || startMonth > 12) throw new Error(`开学日期格式无效:${semesterStartDate}`);
+            const timeSlots = startMonth <= 6 ? WINTER_TIME_SLOTS : SUMMER_TIME_SLOTS;
+            const courseConfig = { semesterTotalWeeks: Math.max(19, ...pageWeeks), defaultClassDuration: 40, firstDayOfWeek: 1 };
+            courseConfig.semesterStartDate = semesterStartDate;
+            await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(courseConfig));
+            await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
+            await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(pageCourses));
+            window.shiguangBridge.showToast(`导入成功:${pageCourses.length} 条课程安排`);
+            window.shiguangBridge.notifyTaskCompletion();
+            return;
+        }
+        window.shiguangBridge.showToast("当前页面未找到课表,请先打开个人或班级课表页面。");
+    } catch (error) {
+        console.error("NTU adapter error", error);
+        window.shiguangBridge.showToast(`导入失败:${error.message}`);
+    }
+}
+
+runImport();
+})();