Bladeren bron

fix:修复HUAT适配代码 (#483)

John Smith 3 weken geleden
bovenliggende
commit
e7d24a3855
2 gewijzigde bestanden met toevoegingen van 419 en 577 verwijderingen
  1. 418 576
      resources/HUAT/HUAT.js
  2. 1 1
      resources/HUAT/adapters.yaml

+ 418 - 576
resources/HUAT/HUAT.js

@@ -1,4 +1,8 @@
-// 文件: HUAT1_fixed_shift.js - 修复节次偏移 + 季节时间选择 (夏季/秋季)
+// 文件: HUAT.js - 湖北汽车工业学院课表导入(基于教务系统 API) 
+// 数据源:
+//   /api/semester/list  → 可选学期列表(用于学期选择 + 学期基本信息)
+//   /api/teachTask/list  → 教学任务列表(按 yearTermId 过滤)
+
 // ==================== 验证函数 ====================
 function validateDate(dateStr) {
     if (!dateStr || dateStr.trim().length === 0) {
@@ -18,430 +22,225 @@ function validateDate(dateStr) {
     }
     return false;
 }
-function validateName(name) {
-    if (name === null || name.trim().length === 0) {
-        return "输入不能为空!";
-    }
-    if (name.length < 2) {
-        return "至少需要2个字符!";
+
+// ==================== API 数据获取 ====================
+// 用当前页面协议构造 API 基址,避免 HTTP/HTTPS 跨域
+// (http:// 和 https:// 在 CORS 看来是不同 origin,会触发预检并失败)
+const API_BASE = window.location.protocol + '//neweas.huat.edu.cn';
+
+/**
+ * 获取认证 token(X-Token 头的值)
+ * 优先级:localStorage / sessionStorage 常见 key → Admin-Token cookie → window 全局
+ */
+function getAuthToken() {
+    const storageKeys = ['Admin-Token', 'adminToken', 'token', 'X-Token', 'x-token', 'admin_token'];
+    for (const key of storageKeys) {
+        const v = localStorage.getItem(key) || sessionStorage.getItem(key);
+        if (v) return v;
     }
-    return false;
+    // 从 cookie 读 Admin-Token
+    const cookieMatch = document.cookie.match(/Admin-Token=([^;]+)/);
+    if (cookieMatch) return cookieMatch[1];
+    // window 全局
+    if (window.token) return window.token;
+    if (window.userToken) return window.userToken;
+    return null;
 }
-// ==================== 课表数据提取函数 ====================
+
 /**
- * 从页面提取课表数据 - 修复3-4和7-8节偏移
+ * 解析 JWT payload(仅解码,不验签)
  */
-function extractCourseData() {
-    console.log("开始提取湖北汽车工业学院课表数据...");
-    
-    const courses = [];
-    
-    // 获取所有课表行 - 使用更精确的选择器
-    const rows = document.querySelectorAll('.el-table__body-wrapper tbody tr');
-    console.log(`找到 ${rows.length} 行课表数据`);
-    
-    // 获取时间标签,用于验证行对应的节次
-    const timeLabels = document.querySelectorAll('.el-table__header-wrapper th .cell, .el-table__header-wrapper th span');
-    console.log("时间标签:", Array.from(timeLabels).map(el => el.textContent));
-    
-    rows.forEach((row, rowIndex) => {
-        // 获取该行的所有单元格
-        const cells = row.querySelectorAll('td');
-        
-        // 从第1个单元格开始是周一至周日(跳过第0个时间单元格)
-        for (let dayIndex = 1; dayIndex < cells.length; dayIndex++) {
-            const cell = cells[dayIndex];
-            
-            // 星期映射修正:第1列=周一(0), 第2列=周二(1), ... 第7列=周日(6)
-            let day = dayIndex - 1;
-            
-            // 修复3-4节和7-8节的偏移
-            // 3-4节在行索引2-3,7-8节在行索引6-7
-            if ((rowIndex >= 2 && rowIndex <= 3) || (rowIndex >= 6 && rowIndex <= 7)) {
-                // 将周一向后推一天
-                if (day === 0) day = 1;      // 周一 -> 周二
-                else if (day === 1) day = 2; // 周二 -> 周三
-                else if (day === 2) day = 3; // 周三 -> 周四
-                else if (day === 3) day = 4; // 周四 -> 周五
-                else if (day === 4) day = 5; // 周五 -> 周六
-                else if (day === 5) day = 6; // 周六 -> 周日
-                // 周日(6)保持不变
-            }
-            
-            // 查找单元格内的所有课程块 - 使用更通用的选择器
-            const courseBlocks = cell.querySelectorAll('[class*="theory"], .theory, [class*="course"], div[style*="background"]');
-            
-            if (courseBlocks.length > 0) {
-                courseBlocks.forEach(block => {
-                    try {
-                        const course = parseCourseBlock(block, day, rowIndex);
-                        if (course) {
-                            courses.push(course);
-                        }
-                    } catch (e) {
-                        console.error("解析课程块失败:", e);
-                    }
-                });
-            }
-        }
-    });
-    
-    // 如果没找到课程,尝试另一种选择器
-    if (courses.length === 0) {
-        console.log("尝试使用备用选择器...");
-        const allCourseElements = document.querySelectorAll('[class*="theory"], .theory, [class*="course"]');
-        console.log(`找到 ${allCourseElements.length} 个可能的课程元素`);
-        
-        allCourseElements.forEach(element => {
-            // 尝试找到元素所在的单元格和行
-            const td = element.closest('td');
-            if (td) {
-                const tr = td.closest('tr');
-                if (tr) {
-                    const rowIndex = Array.from(tr.parentNode.children).indexOf(tr);
-                    const dayIndex = Array.from(td.parentNode.children).indexOf(td);
-                    
-                    if (dayIndex >= 1 && dayIndex <= 7) {
-                        // 星期映射修正:第1列=周一(0), 第2列=周二(1), ... 第7列=周日(6)
-                        let day = dayIndex - 1;
-                        
-                        // 修复3-4节和7-8节的偏移
-                        if ((rowIndex >= 2 && rowIndex <= 3) || (rowIndex >= 6 && rowIndex <= 7)) {
-                            if (day === 0) day = 1;
-                            else if (day === 1) day = 2;
-                            else if (day === 2) day = 3;
-                            else if (day === 3) day = 4;
-                            else if (day === 4) day = 5;
-                            else if (day === 5) day = 6;
-                        }
-                        
-                        const course = parseCourseBlock(element, day, rowIndex);
-                        if (course) {
-                            courses.push(course);
-                        }
-                    }
-                }
-            }
-        });
+function decodeJwtPayload(jwt) {
+    if (!jwt) return null;
+    const parts = jwt.split('.');
+    if (parts.length !== 3) return null;
+    try {
+        const b64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
+        const json = decodeURIComponent(atob(b64).split('').map(c =>
+            '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
+        ).join(''));
+        return JSON.parse(json);
+    } catch (e) {
+        return null;
     }
-    
-    // 去重
-    const uniqueCourses = removeDuplicates(courses);
-    
-    // 按星期和节次排序
-    uniqueCourses.sort((a, b) => {
-        if (a.day !== b.day) return a.day - b.day;
-        return a.startSection - b.startSection;
-    });
-    
-    console.log(`共提取到 ${uniqueCourses.length} 门课程`);
-    uniqueCourses.forEach(c => {
-        console.log(`星期${c.day+1}: ${c.name} - ${c.teacher} - ${c.position} - 第${c.startSection}-${c.endSection}节`);
-    });
-    
-    return uniqueCourses;
 }
+
 /**
- * 解析课程块 - 修复节次映射
+ * 从 JWT 的 sub 字段提取学生 ID
  */
-function parseCourseBlock(block, day, rowIndex) {
-    // 获取所有子元素
-    const children = block.children;
-    let name = '', teacher = '', position = '', weeks = [];
-    
-    // 方法1:通过子元素解析
-    if (children.length >= 3) {
-        // 课程名称通常在h3标签中
-        const nameElement = block.querySelector('h3');
-        if (nameElement) {
-            name = nameElement.innerText.trim();
-            // 只移除括号内是纯数字且不是课程名称标识的情况
-            // 但保留(24)这种课程代码
-            // 如果括号内是纯数字且长度大于2,可能是周次信息,否则保留
-            name = name.replace(/[((]\d+[))]/g, function(match) {
-                // 提取括号内的数字
-                const num = match.replace(/[(())]/g, '');
-                // 如果数字大于30,可能是周次信息,移除;否则保留(如24是课程代码)
-                if (parseInt(num) > 30) {
-                    return '';
-                }
-                return match;
-            }).trim();
-        }
-        
-        // 教师信息通常在第一个p标签中
-        const teacherElement = block.querySelector('p:first-child span:first-child, p:nth-child(1) span');
-        if (teacherElement) {
-            teacher = teacherElement.innerText.trim();
-            // 移除课时信息(如"2H")
-            teacher = teacher.replace(/\d+H?$/, '').trim();
-        }
-        
-        // 周次和教室信息通常在第二个p标签中
-        const weekPositionElement = block.querySelector('p:nth-child(2) span, p:last-child span');
-        if (weekPositionElement) {
-            const text = weekPositionElement.innerText.trim();
-            const result = parseWeekAndPosition(text);
-            weeks = result.weeks;
-            position = result.position;
-        }
+function getStudentId() {
+    const token = getAuthToken();
+    if (!token) return null;
+    const payload = decodeJwtPayload(token);
+    return (payload && payload.sub) || null;
+}
+
+/**
+ * 诊断信息:token 来源 + 预览 + 当前页面 URL
+ * 用于手机端排查认证失败问题(手机端通常没 DevTools 可看)
+ */
+function getTokenDiagnostic() {
+    const storageKeys = ['Admin-Token', 'adminToken', 'token', 'X-Token', 'x-token', 'admin_token'];
+    let source = '未找到';
+    let token = null;
+
+    for (const key of storageKeys) {
+        const v = localStorage.getItem(key);
+        if (v) { token = v; source = 'localStorage[' + key + ']'; break; }
     }
-    
-    // 方法2:如果子元素解析失败,通过文本行解析
-    if (!name || !teacher || !position) {
-        const text = block.innerText;
-        const lines = text.split('\n').filter(line => line.trim());
-        
-        if (lines.length >= 3) {
-            // 第1行:课程名称
-            if (!name) {
-                name = lines[0].trim();
-                // 只移除括号内是纯数字且不是课程名称标识的情况
-                name = name.replace(/[((]\d+[))]/g, function(match) {
-                    const num = match.replace(/[(())]/g, '');
-                    if (parseInt(num) > 30) {
-                        return '';
-                    }
-                    return match;
-                }).trim();
-            }
-            
-            // 第2行:教师信息
-            if (!teacher && lines[1]) {
-                teacher = lines[1].trim();
-                teacher = teacher.replace(/\d+H?$/, '').trim();
-            }
-            
-            // 第3行:周次和教室
-            if (!position && lines[2]) {
-                const result = parseWeekAndPosition(lines[2].trim());
-                weeks = result.weeks;
-                position = result.position;
-            }
+    if (!token) {
+        for (const key of storageKeys) {
+            const v = sessionStorage.getItem(key);
+            if (v) { token = v; source = 'sessionStorage[' + key + ']'; break; }
         }
     }
-    
-    // 如果还是没有找到教室,尝试从整个块中提取数字教室
-    if (!position || position === '未知教室') {
-        position = extractClassroom(block.innerText);
+    if (!token) {
+        const m = document.cookie.match(/Admin-Token=([^;]+)/);
+        if (m) { token = m[1]; source = 'cookie[Admin-Token]'; }
     }
-    
-    // 节次映射修正:按行索引重新定义startSection和endSection
-    let startSection, endSection;
-    switch(rowIndex) {
-        case 0: // 第1行:第1节
-            startSection = 1;
-            endSection = 1;
-            break;
-        case 1: // 第2行:第2节
-            startSection = 2;
-            endSection = 2;
-            break;
-        case 2: // 第3行:第3节
-            startSection = 3;
-            endSection = 3;
-            break;
-        case 3: // 第4行:第4节
-            startSection = 4;
-            endSection = 4;
-            break;
-        case 4: // 第5行:第5节
-            startSection = 5;
-            endSection = 5;
-            break;
-        case 5: // 第6行:第6节
-            startSection = 6;
-            endSection = 6;
-            break;
-        case 6: // 第7行:第7节
-            startSection = 7;
-            endSection = 7;
-            break;
-        case 7: // 第8行:第8节
-            startSection = 8;
-            endSection = 8;
-            break;
-        case 8: // 第9行:第9节
-            startSection = 9;
-            endSection = 9;
-            break;
-        case 9: // 第10行:第10节
-            startSection = 10;
-            endSection = 10;
-            break;
-        case 10: // 第11行:第11节
-            startSection = 11;
-            endSection = 11;
-            break;
-        default: // 默认第1节
-            startSection = 1;
-            endSection = 1;
+    if (!token && window.token) { token = window.token; source = 'window.token'; }
+    if (!token && window.userToken) { token = window.userToken; source = 'window.userToken'; }
+
+    const preview = token
+        ? (token.length > 24 ? token.slice(0, 12) + '...' + token.slice(-8) : token)
+        : 'null';
+    return 'token=' + preview + ' (长度=' + (token ? token.length : 0) + ', source=' + source + ')\nURL=' + window.location.href;
+}
+
+async function fetchSemesterList() {
+    console.log("拉取学期列表...");
+    const token = getAuthToken();
+    if (!token) {
+        throw new Error('无法获取认证 token\n\n诊断:\n' + getTokenDiagnostic());
     }
-    
-    // 检查是否有rowspan(连堂课程)- 修正连堂节次计算逻辑
-    const td = block.closest('td');
-    if (td) {
-        const rowspan = td.getAttribute('rowspan');
-        if (rowspan) {
-            const span = parseInt(rowspan);
-            if (span > 1) {
-                // 连堂时,结束节次 = 开始节次 + 跨行数 - 1
-                endSection = startSection + span - 1;
-                // 限制节次最大值不超过11
-                if (endSection > 11) endSection = 11;
+
+    // cURL 抓包:POST 无 body(Content-Length: 0),无 Content-Type
+    let res;
+    try {
+        res = await fetch(API_BASE + '/api/semester/list', {
+            method: 'POST',
+            credentials: 'include',
+            headers: {
+                'Accept': 'application/json, text/plain, */*',
+                'X-Token': token
             }
-        }
+        });
+    } catch (fetchErr) {
+        // 网络层失败:CORS、URL 不可达、SSL 错误等
+        throw new Error('fetch 失败: ' + fetchErr.message + '\n\n诊断:\n' + getTokenDiagnostic());
     }
-    
-    // 只有提取到有效的课程名称才返回
-    if (!name || name.includes('节') || name.length < 2 || name.includes('理论课')) {
-        return null;
+    if (!res.ok) {
+        throw new Error('semester/list HTTP ' + res.status + '\n\n诊断:\n' + getTokenDiagnostic());
     }
-    
-    // 特殊处理体育课(可能没有教室)
-    if (name.includes('体育') && position === '未知教室') {
-        position = '操场';
+    const json = await res.json();
+    if (json.code !== 200) {
+        throw new Error('semester/list 返回错误 [code=' + json.code + ']: ' + json.msg + '\n\n诊断:\n' + getTokenDiagnostic());
     }
-    
-    const course = {
-        name: name,
-        teacher: teacher || '未知教师',
-        position: position || '未知教室',
-        day: day,
-        startSection: startSection,
-        endSection: endSection,
-        weeks: weeks.length > 0 ? weeks : [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
-        isCustomTime: false
-    };
-    
-    return course;
+    return json.data || [];
 }
-/**
- * 解析周次和教室
- */
-function parseWeekAndPosition(text) {
-    let weeks = [];
-    let position = '未知教室';
-    
-    if (!text) return { weeks, position };
-    
-    console.log("解析周次和教室:", text);
-    
-    // 匹配周次模式:如 "3-16周"、"1-12周"、"6-8周"
-    const weekPattern = /(\d+-\d+周|\d+,\d+周|\d+周)/;
-    const weekMatch = text.match(weekPattern);
-    
-    if (weekMatch) {
-        const weekStr = weekMatch[1];
-        weeks = parseWeeks(weekStr);
-        
-        // 剩余部分可能是教室
-        let remaining = text.replace(weekStr, '').trim();
-        
-        // 如果剩余部分包含数字,很可能是教室
-        if (remaining && /\d+/.test(remaining)) {
-            position = remaining;
-        } else {
-            // 尝试从原文本中提取教室(通常是4位数字)
-            const roomMatch = text.match(/\b\d{3,4}\b/);
-            if (roomMatch) {
-                position = roomMatch[0];
-            }
-        }
-    } else {
-        // 如果没有周次信息,直接尝试提取教室
-        const roomMatch = text.match(/\b\d{3,4}\b/);
-        if (roomMatch) {
-            position = roomMatch[0];
-            weeks = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20];
-        }
+
+async function fetchTeachTaskList(yearTermId) {
+    console.log("拉取教学任务... yearTermId=" + (yearTermId || '(未指定)'));
+    const token = getAuthToken();
+    if (!token) {
+        throw new Error('无法获取认证 token,请确认已登录教务系统');
+    }
+    const studId = getStudentId() || '';
+
+    // body 格式来自 cURL 抓包:{"weekNum":"","yearTermId":"20252","studId":"202402417"}
+    let res;
+    try {
+        res = await fetch(API_BASE + '/api/teachTask/list', {
+            method: 'POST',
+            credentials: 'include',
+            headers: {
+                'Content-Type': 'application/json',
+                'X-Token': token,
+                'X-Requested-With': 'XMLHttpRequest'
+            },
+            body: JSON.stringify({
+                weekNum: "",
+                yearTermId: yearTermId || "",
+                studId: studId
+            })
+        });
+    } catch (fetchErr) {
+        throw new Error('fetch 失败: ' + fetchErr.message + '\n\n诊断:\n' + getTokenDiagnostic());
     }
-    
-    // 清理教室字符串
-    if (position && position !== '未知教室') {
-        // 只保留数字
-        position = position.replace(/[^\d]/g, '');
+    if (!res.ok) {
+        throw new Error('teachTask/list HTTP ' + res.status);
     }
-    
-    return { weeks, position };
+    const json = await res.json();
+    if (json.code !== 200) {
+        throw new Error('teachTask/list 返回错误 [code=' + json.code + ']: ' + json.msg);
+    }
+    return json.data || [];
 }
+
+// ==================== 数据转换 ====================
 /**
- * 从文本中提取教室
+ * 把选中的学期对象转成内部用的 semesterInfo
+ * semesterStartDate 来自学期 beginDate
+ * semesterTotalWeeks 来自 beginWeek..endWeek
  */
-function extractClassroom(text) {
-    // 匹配常见的教室格式:1205、6604、1231、2303、6403、6212、2103、1233、6104、1203
-    const roomPatterns = [
-        /\b\d{4}\b/,           // 4位数字
-        /\b\d{3}\b/,           // 3位数字
-        /[0-9]{3,4}/           // 任意3-4位数字
-    ];
-    
-    for (let pattern of roomPatterns) {
-        const match = text.match(pattern);
-        if (match) {
-            return match[0];
-        }
+function buildSemesterInfoFromSemester(semester) {
+    if (!semester) {
+        return { semesterStartDate: '2026-09-07', semesterTotalWeeks: 25, yearTermId: null };
     }
-    
-    return '未知教室';
+    const beginWeek = semester.beginWeek || 1;
+    const endWeek = semester.endWeek || 25;
+    return {
+        semesterStartDate: semester.beginDate,
+        semesterTotalWeeks: endWeek - beginWeek + 1,
+        yearTermId: semester.yearTermID
+    };
 }
+
 /**
- * 解析周次字符串
+ * 根据 lessType 把 weekBegin..weekEnd 展开为周次数组
+ *   "全周" → 所有周
+ *   "单周" → 仅奇数周
+ *   "双周" → 仅偶数周
  */
-function parseWeeks(weekStr) {
+function buildWeeksArray(weekBegin, weekEnd, lessType) {
     const weeks = [];
-    if (!weekStr) return [];
-    
-    // 移除"周"字
-    weekStr = weekStr.replace(/周/g, '').trim();
-    
-    try {
-        if (weekStr.includes(',')) {
-            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);
-                }
-            });
-        } else if (weekStr.includes('-')) {
-            const [start, end] = weekStr.split('-').map(Number);
-            for (let i = start; i <= end; i++) weeks.push(i);
-        } else {
-            const w = parseInt(weekStr);
-            if (!isNaN(w)) weeks.push(w);
-        }
-    } catch (e) {
-        console.error("解析周次失败:", weekStr, e);
+    const start = weekBegin || 1;
+    const end = weekEnd || start;
+    for (let w = start; w <= end; w++) {
+        if (lessType === '单周' && w % 2 === 0) continue;
+        if (lessType === '双周' && w % 2 === 1) continue;
+        weeks.push(w);
     }
-    
-    return weeks.length > 0 ? weeks.sort((a,b) => a-b) : [];
+    return weeks;
 }
+
 /**
- * 去重
+ * 把 teachTask/list 返回的数组转换成 saveImportedCourses 所需格式
+ * 注意:day 直接用 API 的 weekDay(1=周一 ... 7=周日)
+ *       若发现课程排错星期,改成 task.weekDay - 1(0=周一)即可
  */
-function removeDuplicates(courses) {
-    const seen = new Map();
-    return courses.filter(course => {
-        // 创建唯一键:课程名+教师+星期+开始节次
-        const key = `${course.name}-${course.teacher}-${course.day}-${course.startSection}`;
-        if (seen.has(key)) {
-            return false;
-        }
-        seen.set(key, true);
-        return true;
+function transformTeachTaskToCourses(taskList) {
+    const courses = [];
+    (taskList || []).forEach(task => {
+        // 教师名拼接(多个教师用「、」连接)
+        const teachers = (task.teacherList || [])
+            .map(t => t.teacherName)
+            .filter(name => name && String(name).trim().length > 0);
+        const teacher = teachers.length > 0 ? teachers.join('、') : '未知教师';
+
+        const course = {
+            name: task.courName || '未知课程',
+            teacher: teacher,
+            position: task.roomId ? String(task.roomId) : '未知教室',
+            day: task.weekDay,                              // API: 1=周一 ... 7=周日
+            startSection: task.startSection,
+            endSection: task.endSection,
+            weeks: buildWeeksArray(task.weekBegin, task.weekEnd, task.lessType),
+            isCustomTime: false
+        };
+        courses.push(course);
     });
-}
-/**
- * 提取学期信息
- */
-function extractSemesterInfo() {
-    return {
-        semesterStartDate: "2026-03-01",
-        semesterTotalWeeks: 20
-    };
+    return courses;
 }
 
 // ========== 根据季节获取时间段(夏季/秋季) ==========
@@ -458,7 +257,7 @@ function getSeasonTimeSlots(season) {
         {"number": 3, "startTime": "10:05", "endTime": "10:50"},
         {"number": 4, "startTime": "10:55", "endTime": "11:40"}
     ];
-    
+
     // 夏季下午/晚上时间
     const summer_afternoon_evening = [
         {"number": 5, "startTime": "14:30", "endTime": "15:15"},
@@ -469,7 +268,7 @@ function getSeasonTimeSlots(season) {
         {"number": 10, "startTime": "19:35", "endTime": "20:20"},
         {"number": 11, "startTime": "20:25", "endTime": "21:10"}
     ];
-    
+
     // 秋季下午/晚上时间
     const autumn_afternoon_evening = [
         {"number": 5, "startTime": "14:00", "endTime": "14:45"},
@@ -481,15 +280,11 @@ function getSeasonTimeSlots(season) {
         {"number": 11, "startTime": "19:55", "endTime": "20:40"}
     ];
 
-    if (season === 'summer') {
-        return morning_classes.concat(summer_afternoon_evening);
-    } else if (season === 'autumn') {
+    if (season === 'autumn') {
         return morning_classes.concat(autumn_afternoon_evening);
-    } else {
-        // 默认返回夏季(兼容旧调用)
-        console.warn("未知季节参数,使用夏季时间");
-        return morning_classes.concat(summer_afternoon_evening);
     }
+    // 默认夏季(兼容旧调用)
+    return morning_classes.concat(summer_afternoon_evening);
 }
 
 // ==================== 弹窗和导入函数 ====================
@@ -497,9 +292,9 @@ async function demoAlert() {
     try {
         const confirmed = await window.shiguangBridgePromise.showAlert(
             "📚 湖北汽车工业学院课表导入",
-            "将提取当前页面的课表数据并导入到App\n\n" +
-            "📌 请确认已在课表页面\n" +
-            "📌 将提取所有可见课程",
+            "将通过教务系统 API 拉取课表数据并导入到 App\n\n" +
+            "📌 请确认已登录教务系统\n" +
+            "📌 流程:选择学期 → 选择季节 → 拉取课程",
             "开始导入",
             "取消"
         );
@@ -509,242 +304,289 @@ async function demoAlert() {
         return false;
     }
 }
-async function demoPrompt() {
+
+async function demoPrompt(semesterInfo) {
     try {
-        const semesterInfo = extractSemesterInfo();
+        const defaultDate = (semesterInfo && semesterInfo.semesterStartDate) || '2026-09-07';
         const semesterStart = await window.shiguangBridgePromise.showPrompt(
             "📅 设置开学日期",
-            "请输入本学期开学日期",
-            semesterInfo.semesterStartDate,
+            "已从教务系统获取开学日期,如需修改请直接编辑",
+            defaultDate,
             "validateDate"
         );
-        return semesterStart || semesterInfo.semesterStartDate;
+        return semesterStart || defaultDate;
     } catch (error) {
         console.error("日期输入错误:", error);
-        return "2026-03-01";
+        return "2026-09-07";
+    }
+}
+
+/**
+ * 学期选择 - 用单选列表让用户选学期
+ * 默认选中 currYearTerm === 1 的当前学期
+ */
+async function pickSemester(semesterList) {
+    if (!semesterList || semesterList.length === 0) return null;
+
+    const names = semesterList.map(s => s.yearTermName || s.yearTermID);
+    // 默认选中当前学期(currYearTerm === 1)
+    let defaultIdx = semesterList.findIndex(s => s.currYearTerm === 1);
+    if (defaultIdx < 0) defaultIdx = 0;
+
+    try {
+        console.log("即将显示学期选择弹窗...");
+        const selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
+            "选择要导入的学期",
+            JSON.stringify(names),
+            defaultIdx
+        );
+
+        if (selectedIndex !== null && selectedIndex >= 0 && selectedIndex < semesterList.length) {
+            const picked = semesterList[selectedIndex];
+            console.log("用户选择了: " + picked.yearTermName + " (yearTermID=" + picked.yearTermID + ")");
+            window.shiguangBridge.showToast("已选 " + picked.yearTermName);
+            return picked;
+        }
+        console.log("用户取消了学期选择。");
+        window.shiguangBridge.showToast("学期选择:用户取消");
+        return null;
+    } catch (error) {
+        console.error("显示学期选择弹窗出错:", error);
+        window.shiguangBridge.showToast("学期选择出错:" + error.message);
+        return null;
     }
 }
 
 /**
- * 导入时间段 - 让用户选择夏季或秋季
- * 修复:使用更可靠的方式来处理两个选项
+ * 导入时间段 - 使用单选列表弹窗让用户选择夏季或秋季
+ * 参考 demoSingleSelection 的调用模式
  */
 async function importPresetTimeSlots() {
-    console.log("正在准备预设时间段数据...");
-    
-    // 修复:使用showConfirmDialog而不是showAlert来确保两个按钮都显示
+    const seasons = ["夏季", "秋季"];
     try {
-        // 尝试使用showConfirmDialog(如果有的话)
-        if (window.shiguangBridgePromise && typeof window.shiguangBridgePromise.showConfirmDialog === 'function') {
-            const seasonChoice = await window.shiguangBridgePromise.showConfirmDialog(
-                "⏰ 选择作息时间",
-                "请根据当前学期选择作息时间:\n\n🌞 夏季(5月1日-9月30日)\n🍂 秋季(10月1日-次年4月30日)",
-                "夏季",
-                "秋季"
-            );
-            
-            // showConfirmDialog 点击左侧按钮返回 true,点击右侧按钮返回 false
-            let season = seasonChoice === true ? 'summer' : 'autumn';
-            
-            // 获取对应季节的时间段
-            const presetTimeSlots = getSeasonTimeSlots(season);
-            console.log(`选择的季节: ${season},时间段:`, presetTimeSlots);
-            
-            // 导入时间段
-            const result = await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(presetTimeSlots));
-            if (result === true) {
-                console.log("预设时间段导入成功!");
-                window.shiguangBridge.showToast(`✅ ${season === 'summer' ? '夏季' : '秋季'}时间段导入成功!`);
-                return true;
-            } else {
-                console.log("预设时间段导入未成功,结果:" + result);
-                window.shiguangBridge.showToast("❌ 时间段导入失败,请查看日志。");
-                return false;
-            }
-        } 
-        // 使用showAlert但确保两个按钮都显示
-        else {
-            // 方法1:尝试使用showAlert两个按钮 - 修复按钮显示问题
-            // 注意:有些AndroidBridge实现中,showAlert的第三个参数是左侧按钮,第四个参数是右侧按钮
-            // 确保两个按钮文本都不为空
-            const seasonChoice = await window.shiguangBridgePromise.showAlert(
-                "⏰ 选择作息时间",
-                "请根据当前学期选择作息时间:\n\n🌞 夏季(5月1日-9月30日)\n🍂 秋季(10月1日-次年4月30日)",
-                "夏季",  // 左侧按钮
-                "秋季"   // 右侧按钮
-            );
-            
-            // 根据返回值判断用户点击了哪个按钮
-            // showAlert 点击左侧按钮返回 true,点击右侧按钮返回 false
-            let season = 'summer';
-            
-            // 更精确地判断返回值
-            if (seasonChoice === true || seasonChoice === "true" || seasonChoice === 1) {
-                season = 'summer';
-                console.log("用户选择了夏季");
-            } else if (seasonChoice === false || seasonChoice === "false" || seasonChoice === 0) {
-                season = 'autumn';
-                console.log("用户选择了秋季");
-            } else if (typeof seasonChoice === 'string') {
-                // 如果返回的是按钮文本
-                if (seasonChoice === '夏季') {
-                    season = 'summer';
-                } else {
-                    season = 'autumn';
-                }
-            }
-            
+        console.log("即将显示季节选择单选列表弹窗...");
+        const selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
+            "选择作息时间",
+            JSON.stringify(seasons),
+            0
+        );
+        if (selectedIndex !== null && selectedIndex >= 0 && selectedIndex < seasons.length) {
+            console.log("用户选择了: " + seasons[selectedIndex] + " (索引: " + selectedIndex + ")");
+            window.shiguangBridge.showToast("你选择了 " + seasons[selectedIndex]);
+
+            // 索引 0 -> 夏季,索引 1 -> 秋季
+            const season = selectedIndex === 0 ? 'summer' : 'autumn';
+
             // 获取对应季节的时间段
             const presetTimeSlots = getSeasonTimeSlots(season);
             console.log(`选择的季节: ${season},时间段:`, presetTimeSlots);
-            
+
             // 导入时间段
             const result = await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(presetTimeSlots));
             if (result === true) {
                 console.log("预设时间段导入成功!");
-                window.shiguangBridge.showToast(`✅ ${season === 'summer' ? '夏季' : '秋季'}时间段导入成功!`);
-                return true;
+                window.shiguangBridge.showToast(`✅ ${seasons[selectedIndex]}时间段导入成功!`);
+                return true; // 成功时返回 true
             } else {
                 console.log("预设时间段导入未成功,结果:" + result);
                 window.shiguangBridge.showToast("❌ 时间段导入失败,请查看日志。");
                 return false;
             }
+        } else {
+            console.log("用户取消了选择。");
+            window.shiguangBridge.showToast("季节选择:用户取消了选择!");
+            return false; // 用户取消时返回 false
         }
     } catch (error) {
-        console.error("导入时间段时发生错误:", error);
-        
-        // 方法2:如果两个按钮的方法失败,尝试使用showPrompt让用户输入
-        try {
-            console.log("尝试使用备用方法选择季节...");
-            const seasonInput = await window.shiguangBridgePromise.showPrompt(
-                "⏰ 选择作息时间",
-                "请输入季节(输入 1 选择夏季,输入 2 选择秋季):\n\n1 - 夏季 (5月1日-9月30日)\n2 - 秋季 (10月1日-次年4月30日)",
-                "1"
-            );
-            
-            let season = 'summer';
-            if (seasonInput === '2') {
-                season = 'autumn';
-            }
-            
-            const presetTimeSlots = getSeasonTimeSlots(season);
-            const result = await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(presetTimeSlots));
-            
-            if (result === true) {
-                window.shiguangBridge.showToast(`✅ ${season === 'summer' ? '夏季' : '秋季'}时间段导入成功!`);
-                return true;
-            }
-        } catch (secondError) {
-            console.error("备用方法也失败:", secondError);
-            window.shiguangBridge.showToast("导入时间段失败,使用默认夏季时间");
-            
-            // 方法3:使用默认夏季
-            const defaultTimeSlots = getSeasonTimeSlots('summer');
-            await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(defaultTimeSlots));
-            window.shiguangBridge.showToast("✅ 默认夏季时间段导入成功");
-            return true;
-        }
+        console.error("显示季节选择单选列表弹窗时发生错误:", error);
+        window.shiguangBridge.showToast("季节选择:显示列表出错!" + error.message);
+        return false; // 出现错误时也返回 false
     }
 }
 
-async function importSchedule() {
+/**
+ * 从 API 拉取课程并导入(按选中的 yearTermId 过滤)
+ */
+async function importSchedule(semesterInfo) {
+    const result = { success: false, courseCount: 0, errorMsg: null };
     try {
-        window.shiguangBridge.showToast("正在提取课表数据...");
-        
-        const courses = extractCourseData();
-        
+        window.shiguangBridge.showToast("正在从教务系统拉取课表...");
+
+        const yearTermId = semesterInfo && semesterInfo.yearTermId;
+        const tasks = await fetchTeachTaskList(yearTermId);
+        console.log(`拉取到 ${tasks.length} 条教学任务`);
+
+        const courses = transformTeachTaskToCourses(tasks);
+        result.courseCount = courses.length;
+
         if (courses.length === 0) {
+            result.errorMsg = "教务系统未返回任何课程数据";
             await window.shiguangBridgePromise.showAlert(
-                "⚠️ 提取失败",
-                "未找到课表数据,请确认已在课表页面",
+                "⚠️ 拉取失败",
+                result.errorMsg + ",请确认已登录且所选学期有课程",
                 "知道了"
             );
-            return false;
+            return result;
         }
-        
+
         // 预览
         const preview = await window.shiguangBridgePromise.showAlert(
             "📊 数据预览",
-            `共找到 ${courses.length} 门课程\n\n` +
-            `示例:\n${courses.slice(0, 5).map(c => 
-                `• 周${c.day+1} ${c.name} - 第${c.startSection}-${c.endSection}节`
+            `共拉取到 ${courses.length} 条课程记录\n\n` +
+            `示例:\n${courses.slice(0, 5).map(c =>
+                `• 周${c.day} ${c.name} - 第${c.startSection}-${c.endSection}节`
             ).join('\n')}`,
             "确认导入",
             "取消"
         );
-        
+
         if (!preview) {
-            return false;
+            result.errorMsg = "用户在预览时取消";
+            return result;
         }
-        
+
         // 导入课程
         window.shiguangBridge.showToast("正在导入课程...");
-        const result = await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
-        
-        if (result === true) {
-            const semesterDate = await demoPrompt();
-            const configResult = await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify({
-                semesterStartDate: semesterDate,
-                semesterTotalWeeks: 20,
-                defaultClassDuration: 45,
-                defaultBreakDuration: 10,
-                firstDayOfWeek: 1
-            }));
-            
-            if (configResult === true) {
-                window.shiguangBridge.showToast(`✅ 导入成功!共${courses.length}门课程`);
-                return true;
-            }
+        const saveResult = await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
+
+        if (saveResult !== true) {
+            result.errorMsg = "saveImportedCourses 返回非 true";
+            return result;
         }
-        
-        window.shiguangBridge.showToast("❌ 导入失败");
-        return false;
-        
+
+        // 设置开学日期(默认值来自学期 API 的 beginDate)
+        const semesterDate = await demoPrompt(semesterInfo);
+        const totalWeeks = (semesterInfo && semesterInfo.semesterTotalWeeks) || 25;
+
+        // 保存课表配置
+        const configResult = await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify({
+            semesterStartDate: semesterDate,
+            semesterTotalWeeks: totalWeeks,
+            defaultClassDuration: 45,
+            defaultBreakDuration: 10,
+            firstDayOfWeek: 1
+        }));
+
+        if (configResult === true) {
+            result.success = true;
+            return result;
+        }
+
+        result.errorMsg = "saveCourseConfig 返回非 true";
+        return result;
     } catch (error) {
         console.error("导入错误:", error);
-        window.shiguangBridge.showToast("导入出错: " + error.message);
-        return false;
+        result.errorMsg = error.message;
+        return result;
+    }
+}
+
+/**
+ * 显示最终结果汇总弹窗
+ */
+async function showFinalResult(results) {
+    const success = results.schedule && results.schedule.success;
+    const title = success ? '🎉 导入成功' : '❌ 导入失败';
+
+    let msg = '';
+    if (results.semesterName) msg += '📚 学期: ' + results.semesterName + '\n';
+    if (results.schedule && results.schedule.courseCount > 0) {
+        msg += '📖 课程: ' + results.schedule.courseCount + ' 条\n';
+    }
+    msg += '\n📋 步骤状态:\n';
+    msg += (results.semesterList ? '✅' : '❌') + ' 获取学期列表\n';
+    msg += (results.season ? '✅' : '❌') + ' 导入时间段';
+    if (results.seasonName) msg += ' (' + results.seasonName + ')';
+    msg += '\n';
+    msg += (results.schedule ? (results.schedule.success ? '✅' : '❌') : '⏭️') + ' 导入课表';
+    if (results.schedule && results.schedule.courseCount > 0) msg += ' (' + results.schedule.courseCount + '条)';
+    msg += '\n';
+
+    if (results.errorMsg) {
+        msg += '\n❗ 错误: ' + results.errorMsg;
+    }
+
+    try {
+        await window.shiguangBridgePromise.showAlert(title, msg, '知道了');
+    } catch (e) {
+        console.error('显示最终结果弹窗失败:', e);
+        window.shiguangBridge.showToast(title);
     }
 }
 
 async function runAllDemosSequentially() {
     window.shiguangBridge.showToast("🚀 课表导入助手启动...");
-    
-    // 检查页面
-    if (!window.location.href.includes('studentHome/expectCourseTable')) {
-        const goToPage = await window.shiguangBridgePromise.showAlert(
-            "页面提示",
-            "当前不在课表页面,是否跳转?",
-            "跳转",
-            "取消"
-        );
-        if (goToPage) {
-            window.location.href = 'http://neweas.huat.edu.cn/#/studentHome/expectCourseTable';
+
+    const results = {
+        semesterList: false,
+        semesterName: '',
+        season: false,
+        seasonName: '',
+        schedule: null,
+        errorMsg: null
+    };
+
+    try {
+        // 1. 确认开始
+        const start = await demoAlert();
+        if (!start) {
+            results.errorMsg = '用户取消(开始按钮)';
+            await showFinalResult(results);
+            return;
         }
-        return;
-    }
-    
-    const start = await demoAlert();
-    if (!start) {
-        window.shiguangBridge.showToast("已取消");
-        return;
+
+        // 2. 拉学期列表
+        let semesterList = [];
+        try {
+            window.shiguangBridge.showToast("正在获取学期列表...");
+            semesterList = await fetchSemesterList();
+            console.log(`拉取到 ${semesterList.length} 个学期`);
+            results.semesterList = true;
+        } catch (e) {
+            console.error("获取学期列表失败:", e);
+            results.errorMsg = e.message;
+            await showFinalResult(results);
+            return;
+        }
+
+        // 3. 让用户选择学期
+        const pickedSemester = await pickSemester(semesterList);
+        if (!pickedSemester) {
+            results.errorMsg = '用户取消了学期选择';
+            await showFinalResult(results);
+            return;
+        }
+        results.semesterName = pickedSemester.yearTermName;
+        const semesterInfo = buildSemesterInfoFromSemester(pickedSemester);
+
+        // 4. 导入时间段(季节选择:夏季/秋季)
+        results.season = await importPresetTimeSlots();
+        // 季节名映射(与 importPresetTimeSlots 内部一致)
+        // 这里不直接拿名字,留空也行;如果想显示需要改 importPresetTimeSlots 返回值
+
+        // 5. 导入课表(用选中的 yearTermId 拉 teachTask)
+        results.schedule = await importSchedule(semesterInfo);
+    } catch (e) {
+        console.error("运行错误:", e);
+        results.errorMsg = e.message;
     }
-    
-    // 导入时间段(带季节选择:夏季/秋季)
-    await importPresetTimeSlots();
-    
-    // 导入课表
-    await importSchedule();
-    
+
+    await showFinalResult(results);
     window.shiguangBridge.notifyTaskCompletion();
 }
 
 // 导出函数
 window.validateDate = validateDate;
-window.validateName = validateName;
-window.extractCourseData = extractCourseData;
-window.importPresetTimeSlots = importPresetTimeSlots;
+window.getAuthToken = getAuthToken;
+window.getStudentId = getStudentId;
+window.decodeJwtPayload = decodeJwtPayload;
+window.fetchSemesterList = fetchSemesterList;
+window.fetchTeachTaskList = fetchTeachTaskList;
+window.pickSemester = pickSemester;
+window.transformTeachTaskToCourses = transformTeachTaskToCourses;
 window.getSeasonTimeSlots = getSeasonTimeSlots;
+window.importPresetTimeSlots = importPresetTimeSlots;
+window.importSchedule = importSchedule;
 
 // 启动
 runAllDemosSequentially();

+ 1 - 1
resources/HUAT/adapters.yaml

@@ -6,4 +6,4 @@ adapters:
     asset_js_path: "HUAT.js"
     import_url: "https://cas.huat.edu.cn/authserver/login?service=http%3A%2F%2Fneweas.huat.edu.cn%2Flogin"
     maintainer: "老考滤一下"
-    description: "湖北汽车工业学院适配教务,非本校开发者适配如果有误建议提交issues"
+    description: "湖北汽车工业学院适配教务"