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

Merge pull request #431 from XingHeYuZhuan/pending

星河欲转 14 часов назад
Родитель
Сommit
b07e3fc821
3 измененных файлов с 360 добавлено и 1 удалено
  1. 5 1
      index/root_index.yaml
  2. 9 0
      resources/SDJTU/adapters.yaml
  3. 346 0
      resources/SDJTU/sdjtu.js

+ 5 - 1
index/root_index.yaml

@@ -658,6 +658,10 @@ schools:
     initial: "D"
     resource_folder: "UESTC"
 
+  - id: "SDJTU"
+    name: "山东交通学院"
+    initial: "S"
+    resource_folder: "SDJTU"
   - id: "GDOU"
     name: "广东海洋大学"
     initial: "G"
@@ -671,4 +675,4 @@ schools:
   - id: "NIIT"
     name: "南京工业职业技术大学"
     initial: "N"
-    resource_folder: "NIIT"
+    resource_folder: "NIIT"

+ 9 - 0
resources/SDJTU/adapters.yaml

@@ -0,0 +1,9 @@
+# resources/SDJTU/adapters.yaml
+adapters:
+  - adapter_id: "SDJTU"
+    adapter_name: "山东交通学院教务"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "sdjtu.js"
+    import_url: "https://wwebvpn.sdjtu.edu.cn"
+    maintainer: "WuL0ong"
+    description: "适配山东交通学院教务系统,登录成功后进入教务系统,找到“学期理论课表”,在“学年学期”下拉框内选择要导入的学期课表,之后点击“执行导入”按钮即可。\n学期理论课表默认为当前学期,若导入本学期课表则无需修改。\n目前仅支持导入“学期理论课表”内任意学期的课表,暂不支持导入其它类型的课表。"

+ 346 - 0
resources/SDJTU/sdjtu.js

@@ -0,0 +1,346 @@
+// 山东交通学院 (sdjtu.edu.cn) 拾光课程表适配脚本
+// 教务系统类型:强智教务(通过深信服WebVPN访问)
+// 直接读取页面DOM解析课表,WebVPN代理下fetch可能失败
+
+/**
+ * 将周次字符串解析为数字数组
+ * 支持格式: 任意单周/单双周/区间/混合等组合
+ */
+function parseWeeks(weekStr) {
+    const weeks = [];
+    if (!weekStr) return weeks;
+
+    const pureWeekData = weekStr.split('(')[0];
+
+    pureWeekData.split(',').forEach(seg => {
+        seg = seg.trim();
+        if (seg.includes('-')) {
+            const [s, e] = seg.split('-').map(Number);
+            if (!isNaN(s) && !isNaN(e)) {
+                for (let i = s; i <= e; i++) weeks.push(i);
+            }
+        } else {
+            const w = parseInt(seg);
+            if (!isNaN(w)) weeks.push(w);
+        }
+    });
+    return [...new Set(weeks)].sort((a, b) => a - b);
+}
+
+/**
+ * 获取包含课表表格的文档对象
+ * 优先当前页面,其次遍历iframe查找
+ */
+function getScheduleDocument() {
+    // 1. 当前页面直接包含课表
+    if (document.getElementById('kbtable')) {
+        return document;
+    }
+    // 2. 在iframe中查找课表
+    const iframes = document.querySelectorAll('iframe');
+    for (let i = 0; i < iframes.length; i++) {
+        try {
+            const iframeDoc = iframes[i].contentDocument;
+            if (iframeDoc && iframeDoc.getElementById('kbtable')) {
+                return iframeDoc;
+            }
+        } catch (e) {
+            // 跨域iframe无法访问,跳过
+        }
+    }
+    return null;
+}
+
+/**
+ * 从DOM元素中按<br>分割提取行文本
+ * 解决未挂载元素innerText无换行的问题
+ */
+function extractLines(element) {
+    const lines = [];
+    let currentLine = '';
+    for (let i = 0; i < element.childNodes.length; i++) {
+        const node = element.childNodes[i];
+        if (node.nodeName === 'BR') {
+            lines.push(currentLine.trim());
+            currentLine = '';
+        } else {
+            currentLine += node.textContent;
+        }
+    }
+    if (currentLine.trim()) lines.push(currentLine.trim());
+    return lines.filter(l => l.length > 0);
+}
+
+/**
+ * 无法post到课表信息,故从课表HTML中解析时间段信息
+ * 行头格式: "第一大节\n08:30-10:00"
+ * 每个大节包含2个小节
+ */
+function parseTimeSlots(doc) {
+    const table = doc.getElementById('kbtable');
+    if (!table) return [];
+
+    const rows = table.querySelectorAll('tr');
+    const timeSlots = [];
+    let sectionNum = 1;
+
+    for (let i = 1; i < rows.length; i++) {
+        const ths = rows[i].querySelectorAll('th');
+        if (ths.length === 0) continue;
+
+        const headerText = ths[0].innerText.trim();
+        // 跳过备注行
+        if (headerText.startsWith('备注')) continue;
+
+        // 提取时间范围,格式如 "08:30-10:00"
+        const timeMatch = headerText.match(/(\d{2}:\d{2})-(\d{2}:\d{2})/);
+        if (!timeMatch) continue;
+
+        const startTime = timeMatch[1];
+        const endTime = timeMatch[2];
+
+        // 每小节固定45分钟。
+        // 第一小节从大节开始时间起算;第二小节从大节结束时间往前推45分钟。
+        // 这样既兼容90分钟大节(45+45无课间),也兼容95分钟大节(45+5课间+45)
+        const [sh, sm] = startTime.split(':').map(Number);
+        const [eh, em] = endTime.split(':').map(Number);
+        const startMinutes = sh * 60 + sm;
+        const endMinutes = eh * 60 + em;
+        const CLASS_DURATION = 45;
+
+        // 第一小节
+        timeSlots.push({
+            number: sectionNum++,
+            startTime: startTime,
+            endTime: formatTime(startMinutes + CLASS_DURATION)
+        });
+
+        // 第二小节(从大节结束时间往前推45分钟)
+        timeSlots.push({
+            number: sectionNum++,
+            startTime: formatTime(endMinutes - CLASS_DURATION),
+            endTime: endTime
+        });
+    }
+
+    return timeSlots;
+}
+
+/**
+ * 将分钟数格式化为 HH:mm
+ */
+function formatTime(totalMinutes) {
+    const h = Math.floor(totalMinutes / 60);
+    const m = totalMinutes % 60;
+    return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
+}
+
+/**
+ * 解析课表HTML,提取课程数据
+ */
+function parseCourses(doc) {
+    const table = doc.getElementById('kbtable');
+    if (!table) return [];
+
+    const courses = [];
+    const rows = table.querySelectorAll('tr');
+
+    // 从第2行开始遍历(第1行是星期表头)
+    for (let i = 1; i < rows.length; i++) {
+        const tds = rows[i].querySelectorAll('td');
+        if (tds.length === 0) continue;
+
+        // 遍历每一天的列(td索引0=星期一, 1=星期二, ..., 5=星期六)
+        tds.forEach((cell, dayIndex) => {
+            const day = dayIndex + 1; // 1=周一, 6=周六
+
+            // 获取显示视图的div(class="kbcontent")
+            const contentDivs = cell.querySelectorAll('.kbcontent');
+            contentDivs.forEach(div => {
+                const rawHtml = div.innerHTML.trim();
+                if (!rawHtml || rawHtml === '&nbsp;') return;
+
+                // 按分隔线拆分为多个课程块
+                const blocks = rawHtml.split(/-{5,}/);
+
+                blocks.forEach(block => {
+                    const tempDiv = document.createElement('div');
+                    tempDiv.innerHTML = block;
+
+                    // 按<br>分割为行
+                    const lines = extractLines(tempDiv);
+                    if (lines.length < 2) return;
+
+                    // 找到课程代码行(格式: 数字-数字+字母)
+                    let codeLineIdx = -1;
+                    for (let li = 0; li < lines.length; li++) {
+                        if (/^\d{6}-\d{6}/.test(lines[li])) {
+                            codeLineIdx = li;
+                            break;
+                        }
+                    }
+                    if (codeLineIdx === -1 || codeLineIdx + 1 >= lines.length) return;
+
+                    // 课程名称 = 代码行下一行,去除尾部O/P调课标记
+                    const name = lines[codeLineIdx + 1]
+                        .replace(/[\s ]*[OP]\s*$/, '')
+                        .trim();
+
+                    // 提取各字段
+                    const teacherFont = tempDiv.querySelector('font[title="老师"]') ||
+                        tempDiv.querySelector('font[title="教师"]');
+                    const weekFont = tempDiv.querySelector('font[title="周次(节次)"]');
+                    const roomFont = tempDiv.querySelector('font[title="教室"]');
+
+                    if (!weekFont) return;
+
+                    const weekInfo = weekFont.textContent.trim();
+                    const teacher = teacherFont ? teacherFont.textContent.trim() : '';
+                    const position = roomFont ? roomFont.textContent.trim() : '';
+
+                    // 解析节次
+                    const secMatch = weekInfo.match(/\[(\d+)(?:-(\d+))?节\]/);
+                    if (!secMatch) return;
+
+                    const startSection = parseInt(secMatch[1]);
+                    const endSection = secMatch[2] ? parseInt(secMatch[2]) : startSection;
+
+                    // 解析周次
+                    const weeks = parseWeeks(weekInfo);
+                    if (weeks.length === 0 || !name) return;
+
+                    courses.push({
+                        name: name,
+                        teacher: teacher,
+                        position: position,
+                        day: day,
+                        startSection: startSection,
+                        endSection: endSection,
+                        weeks: weeks
+                    });
+                });
+            });
+        });
+    }
+
+    return courses;
+}
+
+/**
+ * 读取当前页面选中的学期ID
+ */
+function getSemesterId(doc) {
+    const select = doc.getElementById('xnxq01id');
+    if (!select) return '未知学期';
+    return select.value || '未知学期';
+}
+
+/**
+ * 日期输入验证函数
+ */
+window.validateDateInput = function (input) {
+    if (/^\d{4}-\d{2}-\d{2}$/.test(input)) {
+        const d = new Date(input);
+        if (!isNaN(d.getTime())) return false; // 验证通过
+    }
+    return "请输入正确的日期格式,如 2026-02-23";
+};
+
+/**
+ * 根据学期ID推算默认开学日期
+ * 第一学期(秋季)默认9月1日,第二学期(春季)默认2月23日
+ */
+function getDefaultStartDate(semesterId) {
+    const match = semesterId.match(/^(\d{4})-(\d{4})-(\d)$/);
+    if (!match) return '';
+    const year = parseInt(match[1]);
+    const sem = parseInt(match[3]);
+    if (sem === 1) {
+        return `${year}-08-24`;
+    } else {
+        return `${parseInt(match[2])}-02-23`;
+    }
+}
+
+/**
+ * 获取用户输入的开学日期
+ * 本校寒暑假的开学时间都不固定,故采用此方案
+ */
+async function getSemesterStartDate(semesterId) {
+    const defaultDate = getDefaultStartDate(semesterId);
+    const dateInput = await window.AndroidBridgePromise.showPrompt(
+        "设置开学日期",
+        "请输入本学期开学日期,一般为周一(格式 YYYY-MM-DD,如 2026-02-23):",
+        defaultDate,
+        "validateDateInput"
+    );
+    return dateInput; // 用户取消返回 null
+}
+
+/**
+ * 主流程
+ */
+async function runImportFlow() {
+    try {
+        // 1. 确认提示
+        const confirmed = await window.AndroidBridgePromise.showAlert(
+            "导入提示",
+            "请确保您已登录教务系统并打开了【学期理论课表】页面(已选好学期)。\n脚本将直接读取当前页面的课表数据。",
+            "确认并开始"
+        );
+        if (!confirmed) return;
+
+        // 2. 获取课表文档
+        const doc = getScheduleDocument();
+        if (!doc) {
+            AndroidBridge.showToast("未找到课表页面,请先打开【学期理论课表】");
+            return;
+        }
+
+        const semesterId = getSemesterId(doc);
+
+        // 3. 获取开学日期
+        const startDate = await getSemesterStartDate(semesterId);
+        if (startDate === null) {
+            AndroidBridge.showToast("已取消开学日期输入");
+            return;
+        }
+
+        // 4. 解析课程数据
+        const courses = parseCourses(doc);
+        if (courses.length === 0) {
+            AndroidBridge.showToast("未获取到课程数据,该学期可能暂无课表");
+            return;
+        }
+
+        // 5. 解析时间段
+        const timeSlots = parseTimeSlots(doc);
+
+        // 6. 保存数据
+        if (timeSlots.length > 0) {
+            await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
+        }
+
+        await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
+
+        // 7. 保存课表配置(含开学日期)
+        const config = {
+            semesterStartDate: startDate,
+            semesterTotalWeeks: 20,
+            defaultClassDuration: 45,
+            defaultBreakDuration: 5,
+            firstDayOfWeek: 1
+        };
+        await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(config));
+
+        AndroidBridge.showToast(`[${semesterId}] 成功导入 ${courses.length} 条课程记录!`);
+        AndroidBridge.notifyTaskCompletion();
+
+    } catch (error) {
+        console.error("适配脚本异常:", error);
+        AndroidBridge.showToast("异常: " + error.message);
+    }
+}
+
+// 启动导入流程
+runImportFlow();