Просмотр исходного кода

Merge pull request #93 from BernardYan2357/main

add: 苏州大学教务适配
星河欲转 2 недель назад
Родитель
Сommit
dd3546b909
3 измененных файлов с 254 добавлено и 1 удалено
  1. 6 1
      index/root_index.yaml
  2. 8 0
      resources/SUDA/adapters.yaml
  3. 240 0
      resources/SUDA/suda.js

+ 6 - 1
index/root_index.yaml

@@ -176,4 +176,9 @@ schools:
   - id: "NEAU"
     name: "东北农业大学"
     initial: "D"
-    resource_folder: "NEAU"
+    resource_folder: "NEAU"
+
+  - id: "SUDA"
+    name: "苏州大学"
+    initial: "S"
+    resource_folder: "SUDA"

+ 8 - 0
resources/SUDA/adapters.yaml

@@ -0,0 +1,8 @@
+adapters: #固定字段
+      - adapter_id: "SUDA_01"
+            adapter_name: "苏州大学教务适配"
+            category: "BACHELOR_AND_ASSOCIATE"
+            asset_js_path: "suda.js"
+            import_url: "http://xk.suda.edu.cn/"
+            maintainer: "BernardYan2357"
+            description: "网页仅在校园网/连接校内VPN环境下可访问"

+ 240 - 0
resources/SUDA/suda.js

@@ -0,0 +1,240 @@
+// 苏州大学(suda.edu.cn)拾光课程表适配脚本
+// Author:BernardYan2357
+
+// ========================== 作息时间表 ==========================
+
+const TimeSlots = [
+    { number: 1,  startTime: "08:00", endTime: "08:45" },
+    { number: 2,  startTime: "08:50", endTime: "09:35" },
+    { number: 3,  startTime: "09:55", endTime: "10:40" },
+    { number: 4,  startTime: "10:45", endTime: "11:30" },
+    { number: 5,  startTime: "11:35", endTime: "12:20" },
+    { number: 6,  startTime: "14:00", endTime: "14:45" },
+    { number: 7,  startTime: "14:50", endTime: "15:35" },
+    { number: 8,  startTime: "15:55", endTime: "16:40" },
+    { number: 9,  startTime: "16:45", endTime: "17:30" },
+    { number: 10, startTime: "18:30", endTime: "19:15" },
+    { number: 11, startTime: "19:25", endTime: "20:10" },
+    { number: 12, startTime: "20:20", endTime: "21:05" }
+];
+
+// ========================== 解析函数 ==========================
+
+/**
+ * 解析时间行,提取星期、节次、周次
+ * 示例: "周一第3,4,5节{第1-17周}" / "周二第8,9节{第2-16周|双周}"
+ */
+function parseTimeLine(timeLine) {
+    const dayMap = { "一": 1, "二": 2, "三": 3, "四": 4, "五": 5, "六": 6, "日": 7, "天": 7 };
+    const dayM = timeLine.match(/周([一二三四五六日天])/);
+    if (!dayM) return null;
+    const day = dayMap[dayM[1]];
+    const secM = timeLine.match(/第([0-9]+(?:,[0-9]+)*)节/);
+    if (!secM) return null;
+    const sections = secM[1].split(",").map(s => parseInt(s, 10)).filter(Number.isFinite);
+    if (!sections.length) return null;
+    const braceM = timeLine.match(/\{([^}]*)\}/);
+    if (!braceM) return null;
+    const brace = braceM[1];
+    const rangeM = brace.match(/第(\d+)-(\d+)周/);
+    if (!rangeM) return null;
+    const startWeek = parseInt(rangeM[1], 10);
+    const endWeek = parseInt(rangeM[2], 10);
+    if (!Number.isFinite(startWeek) || !Number.isFinite(endWeek) || endWeek < startWeek) return null;
+    let oddEven = "all";
+    if (/\|单周/.test(brace)) oddEven = "odd";
+    if (/\|双周/.test(brace)) oddEven = "even";
+    const weeks = [];
+    for (let w = startWeek; w <= endWeek; w++) {
+        if (oddEven === "odd" && w % 2 === 0) continue;
+        if (oddEven === "even" && w % 2 === 1) continue;
+        weeks.push(w);
+    }
+    return { day, sections, weeks };
+}
+
+/**
+ * 从课表 table 元素中解析所有课程
+ */
+function parseCourseTable(table) {
+    const anchors = Array.from(table.querySelectorAll("a"));
+    const courses = [];
+    for (const a of anchors) {
+        // innerHTML 确保 <br> 被转为换行符
+        const rawText = (a.innerHTML || "")
+            .replace(/<br\s*\/?>/gi, "\n")
+            .replace(/<[^>]*>/g, "")
+            .replace(/&nbsp;/gi, " ")
+            .replace(/\u00a0/g, " ")
+            .replace(/\r/g, "")
+            .trim();
+        // 每个 <a> 的文本结构:  课程名 / 时间行 / 老师 / 地点
+        const lines = rawText.split("\n").map(s => s.trim()).filter(Boolean);
+        if (lines.length < 2) continue;
+        const name = lines[0] || "";
+        const timeLine = lines[1] || "";
+        const teacher = lines[2] || "";
+        const position = lines[3] || "";
+        const parsed = parseTimeLine(timeLine);
+        if (!parsed) continue;
+        courses.push({
+            name,
+            teacher,
+            position,
+            day: parsed.day,
+            startSection: Math.min(...parsed.sections),
+            endSection: Math.max(...parsed.sections),
+            weeks: parsed.weeks
+        });
+    }
+    // 去重 → 合并相邻节次
+    return mergeAdjacentSections(deduplicateCourses(courses));
+}
+
+/**
+ * 去除重复课程(同名同老师同地点同时间同周次视为重复)
+ */
+function deduplicateCourses(list) {
+    const seen = new Set();
+    return list.filter(c => {
+        const key = `${c.name}|${c.teacher}|${c.position}|${c.day}|${c.startSection}|${c.endSection}|${c.weeks.join(",")}`;
+        if (seen.has(key)) return false;
+        seen.add(key);
+        return true;
+    });
+}
+
+/**
+ * 合并同一课程相邻/连续的节次
+ * 例如: 电工学(二)周一 第2节 + 第3,4,5节 → startSection=2, endSection=5
+ * 不同 weeks 不合并(如单双周保持独立)
+ */
+function mergeAdjacentSections(list) {
+    const groupMap = new Map();
+    for (const c of list) {
+        const key = `${c.name}|${c.teacher}|${c.position}|${c.day}|${c.weeks.join(",")}`;
+        if (!groupMap.has(key)) groupMap.set(key, []);
+        groupMap.get(key).push(c);
+    }
+    const merged = [];
+    for (const entries of groupMap.values()) {
+        entries.sort((a, b) => a.startSection - b.startSection);
+        let cur = { ...entries[0] };
+        for (let i = 1; i < entries.length; i++) {
+            const next = entries[i];
+            if (next.startSection <= cur.endSection + 1) {
+                cur.endSection = Math.max(cur.endSection, next.endSection);
+            } else {
+                merged.push(cur);
+                cur = { ...next };
+            }
+        }
+        merged.push(cur);
+    }
+    return merged;
+}
+
+/**
+ * 检测学期开始日期
+ * 读取 select#xqd(学期1/2)和 select#xnd(学年如"2025-2026")
+ * @param {Document} [doc=document] - 课表所在的 document(可能来自 iframe)
+ */
+function detectSemesterStartDate(doc) {
+    try {
+        doc = doc || document;
+        const xqdSelect = doc.querySelector('select#xqd');
+        const xndSelect = doc.querySelector('select#xnd');
+        if (!xqdSelect) return null;
+        const semester = parseInt(xqdSelect.value, 10);
+        let startYear = new Date().getFullYear();
+        if (xndSelect && xndSelect.value) {
+            const m = xndSelect.value.match(/(\d{4})/);
+            if (m) startYear = parseInt(m[1], 10);
+        }
+        if (semester === 1) return `${startYear}-09-01`;
+        if (semester === 2) return `${startYear + 1}-03-02`;
+        return null;
+    } catch (e) {
+        console.warn("检测学期开始日期失败:", e);
+        return null;
+    }
+}
+
+/**
+ * 查找课表 table,兼容直接打开课表页和通过 iframe 嵌套的情况
+ * @returns {HTMLTableElement|null}
+ */
+function findScheduleTable() {
+    const selector = 'table#Table1.schedule';
+    // 1. 先在当前文档查找
+    let table = document.querySelector(selector);
+    if (table) return table;
+    // 2. 遍历 iframe 查找(同源才能访问)
+    const iframes = document.querySelectorAll('iframe');
+    for (const iframe of iframes) {
+        try {
+            const doc = iframe.contentDocument || iframe.contentWindow?.document;
+            if (!doc) continue;
+            table = doc.querySelector(selector);
+            if (table) return table;
+            // 再查一层嵌套 iframe
+            for (const inner of doc.querySelectorAll('iframe')) {
+                try {
+                    const innerDoc = inner.contentDocument || inner.contentWindow?.document;
+                    if (!innerDoc) continue;
+                    table = innerDoc.querySelector(selector);
+                    if (table) return table;
+                } catch (_) { /* 跨域忽略 */ }
+            }
+        } catch (_) { /* 跨域忽略 */ }
+    }
+    return null;
+}
+
+// ========================== 主流程 ==========================
+
+async function runImportFlow() {
+    // 1. 开始提示
+    const confirmed = await AndroidBridgePromise.showAlert(
+        "苏大课表导入",
+        "请确保当前页面已显示「学生个人课表」\n导入前请先在页面上选好学年和学期",
+        "开始导入"
+    );
+    if (!confirmed) {
+        AndroidBridge.showToast("用户取消了导入。");
+        return;
+    }
+    // 2. 查找课表
+    AndroidBridge.showToast("正在查找课表...");
+    const table = findScheduleTable();
+    if (!table) {
+        AndroidBridge.showToast("未找到课表,请先打开「学生个人课表」页面。");
+        return;
+    }
+    // 3. 解析课程
+    AndroidBridge.showToast("正在解析课程数据...");
+    const courses = parseCourseTable(table);
+    if (courses.length === 0) {
+        AndroidBridge.showToast("未解析到任何课程,请确认课表已正确加载。");
+        return;
+    }
+    // 4. 保存课程
+    AndroidBridge.showToast(`正在保存 ${courses.length} 条课程...`);
+    await AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
+    // 5. 保存作息时间
+    AndroidBridge.showToast(`正在导入 ${TimeSlots.length} 个时间段...`);
+    await AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(TimeSlots));
+    // 6. 保存配置(select 在课表同一文档中,用 ownerDocument 确保 iframe 场景正确)
+    const semesterStartDate = detectSemesterStartDate(table.ownerDocument);
+    const config = {
+        semesterStartDate: semesterStartDate,
+        semesterTotalWeeks: 20,
+        firstDayOfWeek: 1
+    };
+    await AndroidBridgePromise.saveCourseConfig(JSON.stringify(config));
+    // 7. 完成
+    AndroidBridge.showToast(`课程导入成功,共导入 ${courses.length} 条课程!`);
+    AndroidBridge.notifyTaskCompletion();
+}
+
+runImportFlow();