Browse Source

add: 华中科技大学教务适配 (#388)

Lucas 3 weeks ago
parent
commit
f8ce7e6909
3 changed files with 200 additions and 0 deletions
  1. 5 0
      index/root_index.yaml
  2. 8 0
      resources/HUST/adapters.yaml
  3. 187 0
      resources/HUST/hust.js

+ 5 - 0
index/root_index.yaml

@@ -213,6 +213,11 @@ schools:
     initial: "H"
     resource_folder: "HUSE"
 
+  - id: "HUST"
+    name: "华中科技大学"
+    initial: "H"
+    resource_folder: "HUST"
+
   - id: "JSNU"
     name: "江苏师范大学"
     initial: "J"

+ 8 - 0
resources/HUST/adapters.yaml

@@ -0,0 +1,8 @@
+adapters:
+  - adapter_id: "HUST_01"
+    adapter_name: "华中科技大学 HUB 系统"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "hust.js"
+    import_url: "http://mhub.hust.edu.cn"
+    maintainer: "Lucas04-nhr"
+    description: "登录华中科技大学 HUB 系统后执行导入,可在近 6 年的春季、秋季学期之间切换。课程导入后请根据学校校历设置准确的开学日期。"

+ 187 - 0
resources/HUST/hust.js

@@ -0,0 +1,187 @@
+/**
+ * 华中科技大学 HUB 系统课表适配器。
+ *
+ * 接口与解析规则参考 StayUP-Calendar 的 HUST importer:
+ * https://github.com/Lucas04-nhr/StayUP-Calendar/blob/main/lib/src/pages/crawler/hust.dart
+ */
+
+const HUST_COURSE_API = new URL("/LsController/findNameCourse", location.origin).toString();
+const HUST_DEFAULT_SEMESTER_WEEKS = 20;
+
+function getDefaultTerm(now = new Date()) {
+    return {
+        academicYear: now.getMonth() === 0 ? now.getFullYear() - 1 : now.getFullYear(),
+        semester: now.getMonth() >= 1 && now.getMonth() <= 6 ? 2 : 1
+    };
+}
+
+/**
+ * 生成与原 importer 相同范围的学期:当前年份前 4 年至后 1 年,共 12 个。
+ * semester=1 为秋季,semester=2 为春季;倒序排列便于优先选择近期学期。
+ */
+function buildTermOptions(now = new Date()) {
+    const currentYear = now.getFullYear();
+    const terms = [];
+
+    for (let year = currentYear + 1; year >= currentYear - 4; year--) {
+        terms.push({ academicYear: year, semester: 1, label: `${year} 年秋季学期` });
+        terms.push({ academicYear: year, semester: 2, label: `${year} 年春季学期` });
+    }
+
+    return terms;
+}
+
+async function selectTerm() {
+    const terms = buildTermOptions();
+    const defaultTerm = getDefaultTerm();
+    const defaultIndex = Math.max(0, terms.findIndex((term) =>
+        term.academicYear === defaultTerm.academicYear &&
+        term.semester === defaultTerm.semester
+    ));
+
+    const selectedIndex = await window.AndroidBridgePromise.showSingleSelection(
+        "选择学期",
+        JSON.stringify(terms.map((term) => term.label)),
+        defaultIndex
+    );
+
+    if (selectedIndex === null || selectedIndex === -1) {
+        return null;
+    }
+    if (!Number.isInteger(selectedIndex) || selectedIndex < 0 || selectedIndex >= terms.length) {
+        throw new Error("学期选择结果无效");
+    }
+
+    return terms[selectedIndex];
+}
+
+function parsePositiveInteger(value) {
+    const parsed = Number.parseInt(String(value), 10);
+    return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
+}
+
+function expandWeeks(startWeek, endWeek) {
+    const weeks = [];
+    for (let week = startWeek; week <= endWeek; week++) {
+        weeks.push(week);
+    }
+    return weeks;
+}
+
+/**
+ * HUST 的一门课程可包含多个 tr 时段。本项目没有 extraSlots 字段,
+ * 因此每个有效时段转换成一个标准课程块。
+ */
+function parseCourses(payload) {
+    if (!payload || String(payload.code) !== "200" || !Array.isArray(payload.data)) {
+        throw new Error(`课表接口返回异常(code: ${payload?.code ?? "未知"})`);
+    }
+
+    const courses = [];
+
+    for (const item of payload.data) {
+        const name = String(item?.KCMC ?? "").trim();
+        if (!name || !Array.isArray(item?.tr)) {
+            continue;
+        }
+
+        for (const slot of item.tr) {
+            if (slot?.XQS === "<待定>" || slot?.QSJC === "<待定>") {
+                continue;
+            }
+
+            const day = parsePositiveInteger(slot?.XQS);
+            const startSection = parsePositiveInteger(slot?.QSJC);
+            const endSection = parsePositiveInteger(slot?.JSJC);
+            const startWeek = parsePositiveInteger(slot?.QSZC);
+            const endWeek = parsePositiveInteger(slot?.JSZC);
+
+            if (
+                day === null || day > 7 ||
+                startSection === null || endSection === null || endSection < startSection ||
+                startWeek === null || endWeek === null || endWeek < startWeek
+            ) {
+                console.warn("跳过无法解析的 HUST 课程时段:", name, slot);
+                continue;
+            }
+
+            courses.push({
+                name,
+                teacher: String(slot?.XM ?? "").trim(),
+                position: String(slot?.JSMC ?? "").trim(),
+                day,
+                startSection,
+                endSection,
+                weeks: expandWeeks(startWeek, endWeek)
+            });
+        }
+    }
+
+    return courses;
+}
+
+async function fetchCourses(term) {
+    const termCode = `${term.academicYear}${term.semester}`;
+    const response = await fetch(`${HUST_COURSE_API}?kcbxqh=${encodeURIComponent(termCode)}`, {
+        method: "GET",
+        credentials: "include",
+        headers: {
+            Accept: "application/json, text/plain, */*"
+        }
+    });
+
+    if (!response.ok) {
+        throw new Error(`课表请求失败(HTTP ${response.status})`);
+    }
+
+    const raw = await response.text();
+    if (!raw.trim() || raw.trim().startsWith("<")) {
+        throw new Error("未读取到课表数据,请确认已登录 HUB 系统并选择了正确的学期");
+    }
+
+    try {
+        return parseCourses(JSON.parse(raw));
+    } catch (error) {
+        if (error instanceof SyntaxError) {
+            throw new Error("课表接口未返回有效 JSON,请重新登录后再试");
+        }
+        throw error;
+    }
+}
+
+async function saveCourses(courses) {
+    await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
+
+    const semesterTotalWeeks = courses.reduce((maximum, course) => {  
+        const courseMaximum = course.weeks.length > 0 ? Math.max(...course.weeks) : 0;  
+        return Math.max(maximum, courseMaximum);  
+    }, HUST_DEFAULT_SEMESTER_WEEKS);  
+
+    await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify({ semesterTotalWeeks }));  
+}
+
+async function runImportFlow() {
+    try {
+        const term = await selectTerm();
+        if (term === null) {
+            AndroidBridge.showToast("已取消导入");
+            return;
+        }
+
+        AndroidBridge.showToast(`正在获取 ${term.label} 课表...`);
+        const courses = await fetchCourses(term);
+        if (courses.length === 0) {
+            AndroidBridge.showToast("该学期未查询到有效课程,请检查学期或登录状态");
+            return;
+        }
+
+        await saveCourses(courses);
+        AndroidBridge.showToast(`成功导入 ${courses.length} 个课程时段,请按校历设置开学日期`);
+        AndroidBridge.notifyTaskCompletion();
+    } catch (error) {
+        console.error("课表导入失败:", error);
+        AndroidBridge.showToast(`导入失败:${error?.message ?? error}`);
+    }
+}
+
+runImportFlow();