Parcourir la source

Merge pull request #74 from XingHeYuZhuan/pending

Pending
星河欲转 il y a 3 semaines
Parent
commit
c9e06ee32f

+ 13 - 3
index/root_index.yaml

@@ -13,10 +13,15 @@ schools:
     initial: "Z"
     resource_folder: "zhengfang_jiaowu"
 
-  - id: "chaoxing"
+  - id: "chaoxing_jiaowu"
     name: "超星教务系统-通用教务"
     initial: "C"
-    resource_folder: "chaoxing"  
+    resource_folder: "chaoxing_jiaowu"  
+
+  - id: "qingguo_jiaowu"
+    name: "青果教务-通用教务"
+    initial: "Q"
+    resource_folder: "qingguo_jiaowu"    
   
   - id: "CQU"
     name: "重庆大学"
@@ -131,4 +136,9 @@ schools:
   - id: "NJNU"
     name: "南京师范大学"
     initial: "N"
-    resource_folder: "NJNU"
+    resource_folder: "NJNU"
+
+  - id: "XMCU"
+    name: "厦门城市职业学院"
+    initial: "X"
+    resource_folder: "XMCU"  

+ 9 - 0
resources/XMCU/adapters.yaml

@@ -0,0 +1,9 @@
+# resources/XMCU/adapters.yaml
+adapters:
+  - adapter_id: "XMCU_01"
+    adapter_name: "厦门城市职业学院青果教务"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "xmcu_01.js"
+    import_url: "https://eone-443.webvpn.xmcu.edu.cn/"
+    maintainer: "星河欲转"
+    description: "输入账号密码登录,进入融合信息门户,在应用中心找到教务系统并进入,在左侧找到教学安排,点击后即可看到学生个人课表。\n厦门城市职业学院适配教务,非本校开发者适配如果有误建议提交issues"

+ 213 - 0
resources/XMCU/xmcu_01.js

@@ -0,0 +1,213 @@
+/**
+ * 基础工具函数:Base64 编码 (用于生成 URL 中的 params 参数)
+ */
+function encodeParams(xn, xq) {
+    const rawStr = `xn=${xn}&xq=${xq}`;
+    return btoa(rawStr);
+}
+
+/**
+ * 判断是否处于教务系统登录环境
+ */
+async function checkLoginEnvironment() {
+    const currentUrl = window.location.href;
+    const targetBase = "https://jws-443.webvpn.xmcu.edu.cn/xmcsjw/";
+    
+    if (!currentUrl.startsWith(targetBase)) {
+        AndroidBridge.showToast("请先登录教务系统再进行导入");
+        return false;
+    }
+    return true;
+}
+
+/**
+ * 数据解析函数
+ */
+function parseAndMergeXmcuData(htmlText) {
+    const parser = new DOMParser();
+    const doc = parser.parseFromString(htmlText, 'text/html');
+    const rawItems = [];
+    const table = doc.getElementById('mytable');
+
+    if (!table) return [];
+
+    const rows = table.querySelectorAll('tr');
+    rows.forEach((row) => {
+        const cells = row.querySelectorAll('td.td');
+        cells.forEach((cell, dayIndex) => {
+            const day = dayIndex + 1;
+            const courseDivs = cell.querySelectorAll('div[style*="padding-bottom:5px"]');
+            courseDivs.forEach(div => {
+                const lines = Array.from(div.childNodes)
+                    .map(n => n.textContent.trim())
+                    .filter(t => t.length > 0);
+                
+                if (lines.length >= 3) {
+                    const match = lines[2].match(/(.*)\[(.*)\]/);
+                    if (match) {
+                        const weeks = [];
+                        match[1].split(',').forEach(g => {
+                            if (g.includes('-')) {
+                                const [s, e] = g.split('-').map(Number);
+                                for (let i = s; i <= e; i++) weeks.push(i);
+                            } else { weeks.push(Number(g)); }
+                        });
+                        const sections = match[2].split('-').map(Number);
+                        rawItems.push({
+                            name: lines[0],
+                            teacher: lines[1],
+                            position: lines[3] || "未知地点",
+                            day: day,
+                            startSection: sections[0],
+                            endSection: sections[sections.length - 1],
+                            weeks: Array.from(new Set(weeks))
+                        });
+                    }
+                }
+            });
+        });
+    });
+
+    const groupMap = new Map();
+    rawItems.forEach(item => {
+        const key = `${item.name}|${item.teacher}|${item.position}|${item.day}`;
+        if (!groupMap.has(key)) groupMap.set(key, []);
+        groupMap.get(key).push(item);
+    });
+
+    const finalCourses = [];
+    groupMap.forEach((items, key) => {
+        const matrix = {}; 
+        items.forEach(item => {
+            item.weeks.forEach(w => {
+                if (!matrix[w]) matrix[w] = new Set();
+                for (let s = item.startSection; s <= item.endSection; s++) matrix[w].add(s);
+            });
+        });
+
+        const patternMap = new Map();
+        Object.keys(matrix).forEach(w => {
+            const week = parseInt(w);
+            const sections = Array.from(matrix[week]).sort((a, b) => a - b);
+            let start = sections[0];
+            for (let i = 0; i < sections.length; i++) {
+                if (i === sections.length - 1 || sections[i+1] !== sections[i] + 1) {
+                    const pKey = `${start}-${sections[i]}`;
+                    if (!patternMap.has(pKey)) patternMap.set(pKey, []);
+                    patternMap.get(pKey).push(week);
+                    if (i < sections.length - 1) start = sections[i+1];
+                }
+            }
+        });
+
+        const [name, teacher, position, day] = key.split('|');
+        patternMap.forEach((weeks, pKey) => {
+            const [sStart, sEnd] = pKey.split('-').map(Number);
+            finalCourses.push({
+                name, teacher, position,
+                day: parseInt(day),
+                startSection: sStart,
+                endSection: sEnd,
+                weeks: weeks.sort((a, b) => a - b)
+            });
+        });
+    });
+    return finalCourses;
+}
+
+/**
+ * 学期获取函数
+ */
+async function getYearAndSemester() {
+    try {
+        AndroidBridge.showToast("正在获取学期列表...");
+        const response = await fetch("https://jws-443.webvpn.xmcu.edu.cn/xmcsjw/frame/droplist/getDropLists.action", {
+            method: "POST",
+            headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" },
+            body: "comboBoxName=StMsXnxqDxDesc&paramValue=&isYXB=0&isCDDW=0&isXQ=0&isDJKSLB=0&isZY=0",
+            credentials: "include"
+        });
+        const list = await response.json();
+        const names = list.map(item => item.name);
+        const selectedIndex = await window.AndroidBridgePromise.showSingleSelection("选择导入学期", JSON.stringify(names), 0);
+        if (selectedIndex === null) return null;
+        const [xn, xq] = list[selectedIndex].code.split('-');
+        return { xn, xq };
+    } catch (error) {
+        AndroidBridge.showToast("获取列表失败");
+        return null;
+    }
+}
+
+/**
+ * 课表抓取函数
+ */
+async function fetchCourses(xn, xq) {
+    try {
+        const paramsBase64 = encodeParams(xn, xq);
+        const url = `https://jws-443.webvpn.xmcu.edu.cn/xmcsjw/student/wsxk.xskcb10319.jsp?params=${paramsBase64}`;
+        AndroidBridge.showToast("正在提取数据...");
+        const response = await fetch(url, { method: "GET", credentials: "include" });
+        const arrayBuffer = await response.arrayBuffer();
+        const htmlText = new TextDecoder('gbk').decode(arrayBuffer);
+        return parseAndMergeXmcuData(htmlText);
+    } catch (error) {
+        AndroidBridge.showToast("抓取课表失败");
+        return null;
+    }
+}
+
+/**
+ * 时间段导入函数
+ */
+async function importPresetTimeSlots() {
+    const slots = [
+        { "number": 1, "startTime": "08:15", "endTime": "09:00" },
+        { "number": 2, "startTime": "09:05", "endTime": "09:50" },
+        { "number": 3, "startTime": "10:15", "endTime": "11:00" },
+        { "number": 4, "startTime": "11:05", "endTime": "11:50" },
+        { "number": 5, "startTime": "14:00", "endTime": "14:45" },
+        { "number": 6, "startTime": "14:50", "endTime": "15:35" },
+        { "number": 7, "startTime": "16:00", "endTime": "16:45" },
+        { "number": 8, "startTime": "16:50", "endTime": "17:35" },
+        { "number": 9, "startTime": "18:30", "endTime": "19:15" },
+        { "number": 10, "startTime": "19:15", "endTime": "20:00" },
+        { "number": 11, "startTime": "20:00", "endTime": "20:45" }
+    ];
+    await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(slots)).catch(() => {});
+}
+
+/**
+ * 最终流程控制
+ */
+async function runImportFlow() {
+    // 环境检查 (非教务页面直接 Toast 退出)
+    const isReady = await checkLoginEnvironment();
+    if (!isReady) return;
+
+    // 弹窗确认
+    const confirmed = await window.AndroidBridgePromise.showAlert("教务导入", "建议在‘课表查询’页面进行导入以确保数据最全。", "确定");
+    if (!confirmed) return;
+
+    // 选择学期
+    const params = await getYearAndSemester();
+    if (!params) return;
+
+    // 获取并解析数据
+    const courses = await fetchCourses(params.xn, params.xq);
+    if (!courses || courses.length === 0) {
+        AndroidBridge.showToast("未找到有效课程");
+        return;
+    }
+
+    // 存储
+    await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
+    await importPresetTimeSlots();
+
+    // 完成
+    AndroidBridge.showToast(`导入成功:共 ${courses.length} 门课程`);
+    AndroidBridge.notifyTaskCompletion();
+}
+
+// 启动
+runImportFlow();

+ 1 - 1
resources/chaoxing/adapters.yaml → resources/chaoxing_jiaowu/adapters.yaml

@@ -1,4 +1,4 @@
-# resources/chaoxing/adapters.yaml
+# resources/chaoxing_jiaowu/adapters.yaml
 adapters:
   - adapter_id: "chaoxing"
     adapter_name: "超星教务系统"

+ 0 - 0
resources/chaoxing/chaoxing.js → resources/chaoxing_jiaowu/chaoxing.js


+ 9 - 0
resources/qingguo_jiaowu/adapters.yaml

@@ -0,0 +1,9 @@
+# resources/qingguo_jiaowu/adapters.yaml
+adapters:
+  - adapter_id: "qingguo_01"
+    adapter_name: "青果教务html通用获取"
+    category: "GENERAL_TOOL"
+    asset_js_path: "qingguo_01.js"
+    import_url: ""
+    maintainer: "星河欲转"
+    description: "输入教务网址,登录,切换到个人课表页面点击执行导入"

+ 145 - 0
resources/qingguo_jiaowu/qingguo_01.js

@@ -0,0 +1,145 @@
+/**
+ * 青果教务系统通用适配脚本
+ * 采用“倒数 7 列”逻辑,解决星期偏移
+ */
+
+function parseWeeks(weekStr) {
+    const weeks = [];
+    weekStr.split(',').forEach(part => {
+        if (part.includes('-')) {
+            const [start, end] = part.split('-').map(Number);
+            for (let i = start; i <= end; i++) weeks.push(i);
+        } else {
+            const w = parseInt(part);
+            if (!isNaN(w)) weeks.push(w);
+        }
+    });
+    return weeks;
+}
+
+async function fetchAndParseCourses() {
+    const rawItems = [];
+    
+    function findTable(win) {
+        const t = Array.from(win.document.querySelectorAll('table')).find(x => x.innerText.includes("星期一") && x.innerText.includes("["));
+        if (t) return t;
+        for (let i = 0; i < win.frames.length; i++) {
+            try { const st = findTable(win.frames[i]); if (st) return st; } catch (e) {}
+        }
+        return null;
+    }
+
+    const table = findTable(window);
+    if (!table) return null;
+
+    // 原始数据清洗抓取
+    Array.from(table.rows).forEach(row => {
+        const cells = Array.from(row.cells);
+        if (cells.length < 7) return;
+
+        cells.forEach((cell, colIndex) => {
+            const distanceToLast = cells.length - 1 - colIndex;
+            if (distanceToLast > 6) return; 
+            const day = 7 - distanceToLast;
+
+            const rawText = cell.innerText.trim();
+            if (!rawText.includes('[')) return;
+
+            // 过滤掉空白行,并清洗每一行的首尾空格
+            const lines = rawText.split('\n').map(l => l.trim()).filter(l => l);
+            
+            lines.forEach((line, i) => {
+                const match = line.match(/([\d\-,]+)\[(\d+)-(\d+)\]/);
+                if (match) {
+                    let name = "未知课程";
+                    if (i >= 2) name = lines[i-2];
+                    else if (i >= 1) name = lines[i-1];
+
+                    let teacher = (i >= 1 && !lines[i-1].includes('[')) ? lines[i-1] : "未知教师";
+                    let position = (i < lines.length - 1) ? lines[i+1] : "未知地点";
+
+                    rawItems.push({
+                        name: name.replace(/\s/g, ""), // 去除所有空格
+                        teacher: teacher.replace(/\s/g, ""),
+                        position: position.replace(/\s/g, ""),
+                        day: day,
+                        startSection: parseInt(match[2]),
+                        endSection: parseInt(match[3]),
+                        weeks: parseWeeks(match[1])
+                    });
+                }
+            });
+        });
+    });
+
+    // 矩阵合并
+    const groupMap = new Map();
+    rawItems.forEach(item => {
+        const key = `${item.name}|${item.teacher}|${item.position}|${item.day}`;
+        if (!groupMap.has(key)) groupMap.set(key, {});
+        
+        const weekMap = groupMap.get(key);
+        item.weeks.forEach(w => {
+            if (!weekMap[w]) weekMap[w] = new Set();
+            for (let s = item.startSection; s <= item.endSection; s++) {
+                weekMap[w].add(s);
+            }
+        });
+    });
+
+    const finalCourses = [];
+    groupMap.forEach((weekMap, key) => {
+        const [name, teacher, position, day] = key.split('|');
+        
+        // 模式聚合:寻找具有相同“节次跨度”的周次
+        const patternMap = new Map(); 
+
+        Object.keys(weekMap).forEach(w => {
+            const week = parseInt(w);
+            const sections = Array.from(weekMap[week]).sort((a, b) => a - b);
+            if (sections.length === 0) return;
+
+            // 重新切分连续节次
+            let start = sections[0];
+            for (let i = 0; i < sections.length; i++) {
+                if (i === sections.length - 1 || sections[i+1] !== sections[i] + 1) {
+                    const pKey = `${start}-${sections[i]}`;
+                    if (!patternMap.has(pKey)) patternMap.set(pKey, []);
+                    patternMap.get(pKey).push(week);
+                    if (i < sections.length - 1) start = sections[i+1];
+                }
+            }
+        });
+
+        patternMap.forEach((weeks, pKey) => {
+            const [sStart, sEnd] = pKey.split('-').map(Number);
+            finalCourses.push({
+                name, teacher, position,
+                day: parseInt(day),
+                startSection: sStart,
+                endSection: sEnd,
+                weeks: weeks.sort((a, b) => a - b)
+            });
+        });
+    });
+
+    return finalCourses;
+}
+
+async function runImportFlow() {
+    try {
+        AndroidBridge.showToast("正在合并课表数据...");
+        const courses = await fetchAndParseCourses();
+        if (!courses || courses.length === 0) {
+            AndroidBridge.showToast("未找到可导入课程");
+            return;
+        }
+        await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
+        AndroidBridge.showToast(`成功:已优化合并为 ${courses.length} 个课块`);
+        AndroidBridge.notifyTaskCompletion();
+    } catch (error) {
+        AndroidBridge.showToast("解析失败: " + error.message);
+    }
+}
+
+runImportFlow();