Bläddra i källkod

Merge pull request #329 from XingHeYuZhuan/pending

pending
星河欲转 1 månad sedan
förälder
incheckning
28b3ed8a1d
1 ändrade filer med 319 tillägg och 199 borttagningar
  1. 319 199
      resources/NEU/neu.js

+ 319 - 199
resources/NEU/neu.js

@@ -1,137 +1,199 @@
 // 文件: neu.js
 
-// 1. 显示校区选择弹窗
+/**
+ * 显示自定义学年学期选择对话框
+ * @returns {Promise<{semesterCode: string, xnxqdm: string, xqdm: string} | null>} 
+ * 返回包含学期代码的对象,若取消则返回 null
+ */
+async function showCustomSemesterDialog() {
+    return new Promise((resolve) => {
+
+        const overlay = document.createElement('div');
+        overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:10000;display:flex;align-items:center;justify-content:center';
+        const dialog = document.createElement('div');
+        dialog.style.cssText = 'background:white;padding:20px;border-radius:8px;box-shadow:0 2px 10px rgba(0,0,0,0.3);min-width:280px;text-align:center';
+        dialog.innerHTML = `
+            <div style="font-size:18px;margin-bottom:20px;font-weight:bold">选择学年学期</div>
+            <div style="display:flex;align-items:center;justify-content:center;margin-bottom:20px">
+                <input type="number" id="startYear" placeholder="起始年份" value="2025" style="width:80px;padding:5px">
+                <span style="margin:0 5px">—</span>
+                <input type="number" id="endYear" placeholder="结束年份" value="2026" style="width:80px;padding:5px">
+            </div>
+            <div style="margin-bottom:20px">
+                <select id="termSelect" style="width:100%;padding:5px">
+                    <option value="fall">秋季学期</option>
+                    <option value="spring">春季学期</option>
+                </select>
+            </div>
+            <div style="display:flex;justify-content:space-around">
+                <button id="confirmBtn" style="padding:5px 15px;background:#4CAF50;color:white;border:none;border-radius:4px;cursor:pointer">确定</button>
+                <button id="cancelBtn" style="padding:5px 15px;background:#f44336;color:white;border:none;border-radius:4px;cursor:pointer">取消</button>
+            </div>
+        `;
+        overlay.appendChild(dialog);
+        document.body.appendChild(overlay);
+
+        const startYearInput = dialog.querySelector('#startYear');
+        const endYearInput = dialog.querySelector('#endYear');
+        const termSelect = dialog.querySelector('#termSelect');
+        const confirmBtn = dialog.querySelector('#confirmBtn');
+        const cancelBtn = dialog.querySelector('#cancelBtn');
+
+        const cleanup = () => document.body.removeChild(overlay);
+        confirmBtn.onclick = () => {
+            const start = parseInt(startYearInput.value, 10);
+            const end = parseInt(endYearInput.value, 10);
+            if (isNaN(start) || isNaN(end)) { alert('请输入有效年份'); return; }
+            const semesterNum = termSelect.value === 'fall' ? '1' : '2';
+            const semesterCode = `${start}-${end}-${semesterNum}`;
+            cleanup();
+            resolve({ semesterCode, xnxqdm: semesterCode, xqdm: '01' });
+        };
+        cancelBtn.onclick = () => { cleanup(); resolve(null); };
+    });
+}
+
+/**
+ * 显示学期选择(封装 showCustomSemesterDialog)
+ * @returns {Promise<string|false>} 返回学期代码字符串,取消则返回 false
+ */
+async function showSemesterSelection() {
+    const res = await showCustomSemesterDialog();
+    return res ? res.semesterCode : false;
+}
+
+/**
+ * 显示校区选择对话框(通过Android原生弹窗)
+ * @returns {Promise<string|false>} 返回校区名称("南湖校区"或"浑南校区"),取消返回 false
+ */
 async function showCampusSelection() {
     const campuses = ["南湖校区", "浑南校区"];
     try {
-        const selectedIndex = await window.AndroidBridgePromise.showSingleSelection(
-            "选择你所在的校区",
-            JSON.stringify(campuses),
-            2
-        );
-        if (selectedIndex !== -1) {
-            return campuses[selectedIndex]; // 返回用户选择的校区
-        } else {
-            return false; // 用户取消时返回 false
-        }
-    } catch (error) {
-        console.error("显示单选列表弹窗时发生错误:", error);
-        AndroidBridge.showToast("Single Selection:显示列表出错!" + error.message);
-        return false; // 出现错误时也返回 false
+        const idx = await window.AndroidBridgePromise.showSingleSelection("选择你所在的校区", JSON.stringify(campuses), 2);
+        return idx !== -1 ? campuses[idx] : false;
+    } catch(e) {
+        AndroidBridge.showToast("显示校区列表出错:" + e.message);
+        return false;
     }
 }
 
-// 2. 从课表页面中提取课程数据
-async function extractCoursesFromPage() {
-    const iframe = document.querySelector('iframe');
-    const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
-    const dayCols = iframeDoc.querySelectorAll('.kbappTimetableDayColumnRoot');
-    const lessons = [];
-
-    dayCols.forEach((dayCol, dayIndex) => {
-        const timeSlots = dayCol.children;
-        const day = dayIndex >= 1 ? dayIndex : 7;
-        let startSection = 0;
-        let endSection = 0;
-        
-        for (let slot of timeSlots) {
-            const flexValue = slot.style.flex;
-            const nums = parseInt(flexValue.split(' ')[0]);
-            startSection = endSection + 1;
-            endSection = startSection + nums - 1;
-            
-            const courseColumns = slot.querySelectorAll('.kbappTimetableCourseRenderColumn');
-            courseColumns.forEach(courseColumn => {
-                const divsWithoutClass = courseColumn.querySelector('div[style*="flex"]:not([class])');
-                let currentSection = startSection;
-                if (divsWithoutClass) {
-                    empty_num = parseInt(divsWithoutClass.style.flex.split(' ')[0]);
-                    currentSection += empty_num;
-                }
-                const courseItem = courseColumn.querySelector('.kbappTimetableCourseRenderCourseItemContainer');
-                const column_nums = parseInt(courseItem.style.flex.split(' ')[0]);
-                const tooltip = courseItem.querySelector('.el-popover');
-                
-                // 检查tooltip是否存在
-                if (tooltip) {
-                    // 获取每一行作为一个单独的元素
-                    const infoItems = tooltip.querySelectorAll('[class*="CourseItemInfoPopperInfo"]');
-                    let name, details;
-                    
-                    if (infoItems.length > 0) {
-                        infoItems.forEach((item, idx) => {
-                            if (idx === 0) name = item.textContent.trim();
-                            else if (idx === 1) details = parseCourseDetails(item.textContent.trim());
-                            else if (idx === 2) return;
-                        });
-                    }
-                    
-                    lessons.push({
-                        name: name || "", 
-                        teacher: details?.teacher || "", 
-                        position: details?.position || "", 
-                        day: day, 
-                        startSection: currentSection, 
-                        endSection: currentSection + column_nums - 1,
-                        weeks: details?.weeks || []
-                    });
-                }
-            });
-        } 
+/**
+ * 弹窗询问用户是否导入考试时间(测试功能)
+ * @returns {Promise<boolean>} true-导入,false-不导入
+ */
+async function askImportExams() {
+    return new Promise((resolve) => {
+        const overlay = document.createElement('div');
+        overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:10000;display:flex;align-items:center;justify-content:center';
+        const dialog = document.createElement('div');
+        dialog.style.cssText = 'background:white;padding:20px;border-radius:8px;box-shadow:0 2px 10px rgba(0,0,0,0.3);min-width:280px;text-align:center';
+        dialog.innerHTML = `
+            <div style="font-size:16px;margin-bottom:10px;font-weight:bold">是否导入考试时间</div>
+            <div style="font-size:12px;color:gray;margin-bottom:20px">测试功能,周数默认为第15周,需手动调整到对应日期。出错请反馈</div>
+            <div style="display:flex;justify-content:space-around">
+                <button id="yesBtn" style="padding:5px 15px;background:#4CAF50;color:white;border:none;border-radius:4px;cursor:pointer">是</button>
+                <button id="noBtn" style="padding:5px 15px;background:#f44336;color:white;border:none;border-radius:4px;cursor:pointer">否</button>
+            </div>
+        `;
+        overlay.appendChild(dialog);
+        document.body.appendChild(overlay);
+        const cleanup = () => document.body.removeChild(overlay);
+        dialog.querySelector('#yesBtn').onclick = () => { cleanup(); resolve(true); };
+        dialog.querySelector('#noBtn').onclick = () => { cleanup(); resolve(false); };
     });
-
-    return { lessons: lessons };
 }
 
-// 2.1 解析课程详情字符串,提取周次、教师和地点信息
-function parseCourseDetails(detailStr) {
-    // 匹配所有周次模式
-    const weekPattern = /(\d+-\d+周(?:\([单双]\))?|\d+周(?:\([单双]\))?)/g;
-    const weekMatches = detailStr.match(weekPattern);
-    
-    let weeks = '';
-    let remaining = detailStr;
-    
-    if (weekMatches) {
-        // 提取所有周次部分
-        weeks = weekMatches.join(',');
-        // 从原字符串中移除周次部分
-        weekMatches.forEach(match => {
-            remaining = remaining.replace(match, '');
-        });
+/**
+ * 解析考试时间描述字符串,提取星期几、开始时间、结束时间
+ * @param {string} desc 例如 "2026年05月06日 10:10-12:10(星期三第1场)"
+ * @returns {{day: number|null, startTime: string|null, endTime: string|null}}
+ * day: 1~7 对应星期一~星期日,无法解析则为 null
+ */
+function parseExamTimeDescription(desc) {
+    const weekMap = { '星期一': 1, '星期二': 2, '星期三': 3, '星期四': 4, '星期五': 5, '星期六': 6, '星期日': 7 };
+    let day = null;
+    let startTime = null;
+    let endTime = null;
+    for (const [cn, num] of Object.entries(weekMap)) {
+        if (desc.includes(cn)) {
+            day = num;
+            break;
+        }
     }
-    
-    // 按空格分割剩余部分
-    const parts = remaining.trim().split(/\s+/).filter(p => p);
-    
-    let teacher = '';
-    let position = '';
-    if (parts.length > 0) {
-        teacher = parts[0];
-        if (parts.length > 1) {
-            position = parts.slice(1).join(' '); // 修正这一行
+    const timeMatch = desc.match(/(\d{1,2}:\d{2})-(\d{1,2}:\d{2})/);
+    if (timeMatch) {
+        startTime = timeMatch[1];
+        endTime = timeMatch[2];
+    }
+    return { day, startTime, endTime };
+}
+
+/**
+ * 从考试API获取指定学期的考试数据,并转换为课程对象格式
+ * @param {string} termCode 学期代码,如 "2025-2026-1"
+ * @returns {Promise<Array<object>>} 课程对象数组,每个对象包含 name, teacher, position, day, weeks, isCustomTime, customStartTime, customEndTime
+ * @throws 网络或API错误
+ */
+async function fetchExamsFromAPI(termCode) {
+    const url = `https://jwxt.neu.edu.cn/jwapp/sys/homeapp/api/home/student/exams.do?termCode=${encodeURIComponent(termCode)}`;
+    const response = await fetch(url, {
+        method: 'GET',
+        headers: { 'Fetch-Api': 'true', 'Referer': 'https://jwxt.neu.edu.cn/jwapp/sys/homeapp/home/index.html', 'User-Agent': navigator.userAgent }
+    });
+    if (!response.ok) throw new Error(`考试API HTTP ${response.status}`);
+    const data = await response.json();
+    if (data.code !== '0') throw new Error(`考试API错误码: ${data.code}`);
+    const exams = data.datas || [];
+    const lessons = [];
+    for (const exam of exams) {
+        const rawName = exam.courseName || "";
+        const examType = exam.examType || "考试";
+        const desc = exam.examTimeDescription || "";
+        let dateStr = "";
+        const dateMatch = desc.match(/(\d{2})年(\d{2})月(\d{2})日/);
+        if (dateMatch) {
+            dateStr = `${dateMatch[2]}月${dateMatch[3]}日`;
+        } else {
+            const simpleMatch = desc.match(/(\d{2})月(\d{2})日/);
+            if (simpleMatch) dateStr = `${simpleMatch[1]}月${simpleMatch[2]}日`;
+        }
+        const name = dateStr ? `${rawName}_${examType}_${dateStr}` : `${rawName}_${examType}`;
+        const teacher = exam.teachers || "";
+        const position = exam.examPlace || "";
+        const { day, startTime, endTime } = parseExamTimeDescription(desc);
+        if (!day || !startTime || !endTime) {
+            console.warn("解析考试时间失败,跳过:", desc);
+            continue;
         }
+        const weeks = [15];  // 考试固定在第15周(测试功能)
+        lessons.push({
+            name: name,
+            teacher: teacher,
+            position: position,
+            day: day,
+            startSection: undefined,
+            endSection: undefined,
+            weeks: weeks,
+            isCustomTime: true,
+            customStartTime: startTime,
+            customEndTime: endTime
+        });
     }
-    
-    // 清理教师名中的多余逗号
-    teacher = teacher.replace(/^[,,]/, '').replace(/[,,]$/, '');
-    
-    return {
-        weeks: parseWeeksString(weeks),
-        teacher: teacher.trim(),
-        position: position.trim()
-    };
+    return lessons;
 }
 
-// 2.2将周次文字提取成数组
+/**
+ * 增强版周次解析:支持 "1-8周", "2-6周(双)", "1,3,5周" 等格式
+ * @param {string} weeksStr 周次字符串,如 "1-8周"
+ * @returns {number[]} 周次数字数组(已去重、排序)
+ */
 function parseWeeksString(weeksStr) {
     if (!weeksStr) return [];
-    
     const result = [];
     const weekParts = weeksStr.split(/[,,]/).map(part => part.trim());
     
     weekParts.forEach(part => {
-        // 匹配单个数字周
+        // 匹配单个数字周,如 "6周" 或 "6周(单)"
         const singleMatch = part.match(/^(\d+)周(?:\(([单双])\))?$/);
         if (singleMatch) {
             const num = parseInt(singleMatch[1]);
@@ -142,7 +204,7 @@ function parseWeeksString(weeksStr) {
             return;
         }
         
-        // 匹配范围周
+        // 匹配范围周,如 "1-8周" 或 "2-6周(双)"
         const rangeMatch = part.match(/^(\d+)-(\d+)周(?:\(([单双])\))?$/);
         if (rangeMatch) {
             const start = parseInt(rangeMatch[1]);
@@ -166,112 +228,170 @@ function parseWeeksString(weeksStr) {
     return [...new Set(result)].sort((a, b) => a - b);
 }
 
-// 3. 导入课程数据
-async function SaveCourses(lessons) {
-    const testCourses = lessons;
+/**
+ * 将API返回的课表原始数据(arrangedList)转换为标准课程对象数组
+ * 新逻辑:直接从 titleDetail 解析课程名、周次、教师、地点
+ * @param {Array} arrangedList API返回的课表列表
+ * @returns {Array<object>} 课程对象,包含 name, teacher, position, day, startSection, endSection, weeks, isCustomTime(false)
+ */
+function convertApiResponseToLessons(arrangedList) {
+    const lessons = [];
+    for (const item of arrangedList) {
+        // 必要字段检查
+        const day = item.dayOfWeek;
+        const startSection = item.beginSection;
+        const endSection = item.endSection;
+        if (!day || !startSection || !endSection) continue;
 
-    try {
-        const result = await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(testCourses));
-    } catch (error) {
-        console.error("导入课程时发生错误:", error);
-        AndroidBridge.showToast("导入课程失败: " + error.message);
+        const titleDetail = item.titleDetail;
+        if (!Array.isArray(titleDetail) || titleDetail.length < 2) {
+            console.warn("titleDetail 无效,跳过课程:", item);
+            continue;
+        }
+
+        // 1. 课程名:从 titleDetail[0] 的第一个空格前提取
+        const title0 = titleDetail[0] || "";
+        const firstSpaceIdx = title0.indexOf(' ');
+        const name = firstSpaceIdx !== -1 ? title0.substring(0, firstSpaceIdx) : title0;
+        if (!name) continue;
+
+        // 2. 解析 titleDetail[1]  => 周次字符串、教师、地点
+        const title1 = titleDetail[1] || "";
+        const tokens = title1.trim().split(/\s+/); // 按空白符分割
+        if (tokens.length < 1) continue;
+        const weeksStr = tokens[0];                 // 例如 "1-8周"
+        const teacher = tokens[1] || "";
+        // 地点:从第2个token开始到末尾,用空格重新拼接
+        const position = tokens.slice(2).join(' ');
+
+        // 3. 解析周次字符串为数字数组
+        const weeks = parseWeeksString(weeksStr);
+        if (weeks.length === 0) {
+            console.warn(`周次解析失败: ${weeksStr}, 课程: ${name}`);
+            continue;
+        }
+
+        lessons.push({
+            name: name,
+            teacher: teacher,
+            position: position,
+            day: day,
+            startSection: startSection,
+            endSection: endSection,
+            weeks: weeks,
+            isCustomTime: false
+        });
     }
+    return lessons;
 }
 
-// 4. 根据校区导入对应的时间段
-async function importTimeSlotsByCampus(campus) {
-    const hunNanTimeSlots = [
-        { "number": 1, "startTime": "08:30", "endTime": "09:15" },
-        { "number": 2, "startTime": "09:25", "endTime": "10:10" },
-        { "number": 3, "startTime": "10:30", "endTime": "11:15" },
-        { "number": 4, "startTime": "11:25", "endTime": "12:10" },
-        { "number": 5, "startTime": "14:00", "endTime": "14:45" },
-        { "number": 6, "startTime": "14:55", "endTime": "15:40" },
-        { "number": 7, "startTime": "16:00", "endTime": "16:45" },
-        { "number": 8, "startTime": "16:55", "endTime": "17:40" },
-        { "number": 9, "startTime": "18:30", "endTime": "19:15" },
-        { "number": 10, "startTime": "19:25", "endTime": "20:10" },
-        { "number": 11, "startTime": "20:30", "endTime": "21:15" },
-        { "number": 12, "startTime": "21:15", "endTime": "22:10" },
-    ];
-    
-    const nanHuTimeSlots = [
-        { "number": 1, "startTime": "08:00", "endTime": "08:45" },
-        { "number": 2, "startTime": "08:55", "endTime": "09:40" },
-        { "number": 3, "startTime": "10:00", "endTime": "10:45" },
-        { "number": 4, "startTime": "10:55", "endTime": "11:40" },
-        { "number": 5, "startTime": "14:00", "endTime": "14:45" },
-        { "number": 6, "startTime": "14:55", "endTime": "15:40" },
-        { "number": 7, "startTime": "16:00", "endTime": "16:45" },
-        { "number": 8, "startTime": "16:55", "endTime": "17:40" },
-        { "number": 9, "startTime": "18:30", "endTime": "19:15" },
-        { "number": 10, "startTime": "19:25", "endTime": "20:10" },
-        { "number": 11, "startTime": "20:20", "endTime": "21:05" },
-        { "number": 12, "startTime": "21:15", "endTime": "22:00" },
-    ];
-    
-    // 根据校区选择对应的时间表
-    const timeSlotsToImport = (campus === "南湖校区") ? nanHuTimeSlots : hunNanTimeSlots;
-
-    try {
-        const result = await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlotsToImport));
-    } catch (error) {
-        console.error("导入时间段时发生错误:", error);
-        window.AndroidBridge.showToast("导入时间段失败: " + error.message);
+/**
+ * 从教务API获取指定学期的课表数据(支持重试)
+ * @param {string} semesterCode 学期代码,如 "2025-2026-1"
+ * @param {number} retries 重试次数,默认2次
+ * @returns {Promise<Array<object>>} 课程对象数组
+ * @throws 网络或API错误
+ */
+async function fetchCoursesFromAPI(semesterCode, retries=2) {
+    const url = 'https://jwxt.neu.edu.cn/jwapp/sys/kbapp/api/wdkbcx/getMyScheduleDetail.do';
+    const xnxqdm = semesterCode;
+    const xqdm = '01';
+    for (let i=1; i<=retries; i++) {
+        try {
+            const ctrl = new AbortController();
+            const tid = setTimeout(()=>ctrl.abort(), 10000);
+            const res = await fetch(url, {
+                method: 'POST',
+                headers: { 'Fetch-Api':'true', 'Referer':'https://jwxt.neu.edu.cn/jwapp/sys/kbapp/home/index.html', 'User-Agent': navigator.userAgent, 'Accept':'application/json' },
+                body: new URLSearchParams({ XNXQDM: xnxqdm, XQDM: xqdm }),
+                signal: ctrl.signal
+            });
+            clearTimeout(tid);
+            if (!res.ok) throw new Error(`HTTP ${res.status}`);
+            const data = await res.json();
+            if (data.code !== '0') throw new Error(`API error ${data.code}`);
+            const list = data?.datas?.getMyScheduleDetail?.arrangedList || [];
+            return convertApiResponseToLessons(list);
+        } catch(e) {
+            if (i===retries) throw e;
+            await new Promise(r=>setTimeout(r,2000));
+        }
     }
 }
 
-// 5. 导入课表配置
-async function SaveConfig() {
-    // 注意:只传入要修改的字段,其他字段(如 semesterTotalWeeks)会使用 Kotlin 模型中的默认值
-    const courseConfigData = {
-        "semesterTotalWeeks": 18,
-        "defaultClassDuration": 45,
-        "defaultBreakDuration": 10,
-        "firstDayOfWeek": 7
-    };
+/**
+ * 调用Android Bridge保存课程列表(覆盖写入)
+ * @param {Array<object>} lessons 课程对象数组
+ */
+async function SaveCourses(lessons) {
+    await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(lessons));
+}
 
-    try {
-        const configJsonString = JSON.stringify(courseConfigData);
-        const result = await window.AndroidBridgePromise.saveCourseConfig(configJsonString);
-    } catch (error) {
-        console.error("导入配置时发生错误:", error);
-        AndroidBridge.showToast("导入配置失败: " + error.message);
-    }
+/**
+ * 根据校区导入预设的上下课时间表(节次时间)
+ * @param {string} campus "南湖校区" 或 "浑南校区"
+ */
+async function importTimeSlotsByCampus(campus) {
+    const hunNan = [{"number":1,"startTime":"08:30","endTime":"09:15"},{"number":2,"startTime":"09:25","endTime":"10:10"},{"number":3,"startTime":"10:30","endTime":"11:15"},{"number":4,"startTime":"11:25","endTime":"12:10"},{"number":5,"startTime":"14:00","endTime":"14:45"},{"number":6,"startTime":"14:55","endTime":"15:40"},{"number":7,"startTime":"16:00","endTime":"16:45"},{"number":8,"startTime":"16:55","endTime":"17:40"},{"number":9,"startTime":"18:30","endTime":"19:15"},{"number":10,"startTime":"19:25","endTime":"20:10"},{"number":11,"startTime":"20:30","endTime":"21:15"},{"number":12,"startTime":"21:15","endTime":"22:10"}];
+    const nanHu = [{"number":1,"startTime":"08:00","endTime":"08:45"},{"number":2,"startTime":"08:55","endTime":"09:40"},{"number":3,"startTime":"10:00","endTime":"10:45"},{"number":4,"startTime":"10:55","endTime":"11:40"},{"number":5,"startTime":"14:00","endTime":"14:45"},{"number":6,"startTime":"14:55","endTime":"15:40"},{"number":7,"startTime":"16:00","endTime":"16:45"},{"number":8,"startTime":"16:55","endTime":"17:40"},{"number":9,"startTime":"18:30","endTime":"19:15"},{"number":10,"startTime":"19:25","endTime":"20:10"},{"number":11,"startTime":"20:20","endTime":"21:05"},{"number":12,"startTime":"21:15","endTime":"22:00"}];
+    const slots = campus === "南湖校区" ? nanHu : hunNan;
+    await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(slots));
+}
+
+/**
+ * 保存课表全局配置(学期总周数、默认课时长度、课间休息、每周起始日)
+ */
+async function SaveConfig() {
+    const cfg = { semesterTotalWeeks:18, defaultClassDuration:45, defaultBreakDuration:10, firstDayOfWeek:7 };
+    await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(cfg));
 }
 
 /**
- * 编排这些异步操作,并在用户取消时停止后续执行。
+ * 主流程:依次选择校区、学期,获取课表,保存,可选导入考试并合并保存
+ * 最后通知Android任务完成
  */
 async function runAllDemosSequentially() {
     AndroidBridge.showToast("开始导入课表...");
+    const campus = await showCampusSelection();
+    if (!campus) { AndroidBridge.showToast("已取消导入"); return; }
+    const semester = await showSemesterSelection();
+    if (!semester) { AndroidBridge.showToast("已取消导入"); return; }
     
-
-    // 1. 校区选择
-    const selectedCampus = await showCampusSelection();
-    if (!selectedCampus) {
-        console.log("用户取消了校区选择,停止后续执行。");
-        AndroidBridge.showToast("已取消导入");
-        return; // 用户取消,立即退出函数
+    AndroidBridge.showToast("正在获取课表数据...");
+    let lessons;
+    try {
+        lessons = await fetchCoursesFromAPI(semester);
+        if (!lessons.length) { AndroidBridge.showToast("未获取到任何课程"); return; }
+        console.log(`获取到 ${lessons.length} 门课程`);
+    } catch(e) {
+        AndroidBridge.showToast("获取课表失败: "+e.message);
+        return;
     }
-
-    // 3. 从课表页面中提取课程数据
-    const PageInfo = await extractCoursesFromPage();
-    const lessons = PageInfo.lessons;
-    
-    // 4. 保存课程数据到数据库
     await SaveCourses(lessons);
+    await importTimeSlotsByCampus(campus);
+    await SaveConfig();
+    AndroidBridge.showToast("课表导入完成!");
     
-    // 5. 根据选择的校区导入对应的时间段
-    await importTimeSlotsByCampus(selectedCampus);
+    const importExams = await askImportExams();
+    if (importExams) {
+        AndroidBridge.showToast("正在获取考试数据...");
+        try {
+            const examLessons = await fetchExamsFromAPI(semester);
+            if (examLessons.length === 0) {
+                AndroidBridge.showToast("未获取到考试数据");
+            } else {
+                const allLessons = [...lessons, ...examLessons];
+                await SaveCourses(allLessons);
+                AndroidBridge.showToast(`已导入 ${examLessons.length} 条考试记录(合并至课表)`);
+            }
+        } catch(e) {
+            AndroidBridge.showToast("导入考试失败: "+e.message);
+            console.error(e);
+        }
+    }
     
-    // 6. 保存底层配置
-    await SaveConfig();
-
-    // 发送最终的生命周期完成信号
     AndroidBridge.notifyTaskCompletion();
-    AndroidBridge.showToast("课表导入完成!");
 }
 
-// 启动所有演示
+// 启动主流程
 runAllDemosSequentially();