浏览代码

Merge pull request #491 from XingHeYuZhuan/pending

星河欲转 3 周之前
父节点
当前提交
c7ad42eb5d
共有 2 个文件被更改,包括 791 次插入164 次删除
  1. 2 2
      resources/GPNU/adapters.yaml
  2. 789 162
      resources/GPNU/gpnu_01.js

+ 2 - 2
resources/GPNU/adapters.yaml

@@ -1,4 +1,3 @@
-# resources/BUPT/adapters.yaml
 adapters:
   - adapter_id: "GPNU_01"
     adapter_name: "广师大教务管理系统"
@@ -6,4 +5,5 @@ adapters:
     asset_js_path: "gpnu_01.js"
     import_url: "https://jwglxt.gpnu.edu.cn/sso/lyiotlogin"
     maintainer: "moondish"
-    description: "适配教务管理系统,登录后导入个人课表(可不打开相应页面)如有bug请提交issues,泥雞又赢了"
+    description: "适配教务管理系统,登录后导入个人课表。如有bug请提交issues。泥鸡又赢了!"
+    

+ 789 - 162
resources/GPNU/gpnu_01.js

@@ -1,42 +1,146 @@
 // ===================== 工具函数 =====================
 
 /**
- * 解析周次字符串,例如 "1-16周"、"1-8周,10-16周"、"1-16周(单)" 等。
+ * 节次与周次合并去重函数
+ * @param {Array<Object>} courses 原始解析课程数组
+ * @returns {Array<Object>} 合并去重后的课程数组
+ */
+function mergeAndDistinctCourses(courses) {
+    if (!Array.isArray(courses) || courses.length <= 1) return courses;
+
+    const list = courses.map(c => ({
+        ...c,
+        name: c.name || '',
+        teacher: c.teacher || '',
+        position: c.position || '',
+        weeks: Array.isArray(c.weeks) ? [...c.weeks].sort((a, b) => a - b) : []
+    }));
+
+    list.sort((a, b) => {
+        return a.name.localeCompare(b.name) ||
+               a.teacher.localeCompare(b.teacher) ||
+               a.position.localeCompare(b.position) ||
+               (a.day || 0) - (b.day || 0) ||
+               a.weeks.join(',').localeCompare(b.weeks.join(',')) ||
+               (a.startSection || 0) - (b.startSection || 0);
+    });
+
+    const step1Merged = [];
+    let current = list[0];
+
+    for (let i = 1; i < list.length; i++) {
+        const next = list[i];
+
+        const isSameCourseAndWeeks =
+            current.name === next.name &&
+            current.teacher === next.teacher &&
+            current.position === next.position &&
+            current.day === next.day &&
+            current.weeks.join(',') === next.weeks.join(',');
+
+        const isContinuous = current.endSection + 1 === next.startSection;
+        const isDuplicate = current.startSection === next.startSection && current.endSection === next.endSection;
+
+        if (isSameCourseAndWeeks && isContinuous) {
+            current.endSection = next.endSection;
+        } else if (isSameCourseAndWeeks && isDuplicate) {
+            continue;
+        } else {
+            step1Merged.push(current);
+            current = next;
+        }
+    }
+    step1Merged.push(current);
+
+    step1Merged.sort((a, b) => {
+        return a.name.localeCompare(b.name) ||
+               a.teacher.localeCompare(b.teacher) ||
+               a.position.localeCompare(b.position) ||
+               (a.day || 0) - (b.day || 0) ||
+               (a.startSection || 0) - (b.startSection || 0) ||
+               (a.endSection || 0) - (b.endSection || 0);
+    });
+
+    const step2Merged = [];
+    let cur = step1Merged[0];
+
+    for (let i = 1; i < step1Merged.length; i++) {
+        const nxt = step1Merged[i];
+
+        const isSameCourseAndSection =
+            cur.name === nxt.name &&
+            cur.teacher === nxt.teacher &&
+            cur.position === nxt.position &&
+            cur.day === nxt.day &&
+            cur.startSection === nxt.startSection &&
+            cur.endSection === nxt.endSection;
+
+        if (isSameCourseAndSection) {
+            cur.weeks = Array.from(new Set([...cur.weeks, ...nxt.weeks])).sort((a, b) => a - b);
+        } else {
+            step2Merged.push(cur);
+            cur = nxt;
+        }
+    }
+    step2Merged.push(cur);
+
+    return step2Merged;
+}
+
+/**
+ * 解析周次字符串,例如 "1-16周"、"1-8周,10-16周"、"1-16周(单)"、"1-16周(双)" 等。
  * @param {string} weekStr - 周次描述字符串
  * @returns {number[]} 周次数字数组(升序)
  */
 function parseWeeks(weekStr) {
-    if (!weekStr || typeof weekStr !== 'string') return [];
+    if (typeof weekStr !== 'string') {
+        weekStr = String(weekStr || '');
+    }
+    if (!weekStr) return [];
 
     let cleanStr = weekStr.replace(/周/g, '').replace(/\s+/g, '');
     const weeks = new Set();
     const parts = cleanStr.split(',');
 
     for (let rawPart of parts) {
-        if (!rawPart) continue; // 跳过空段,防止产生 0 周
+        if (!rawPart) continue;
 
-        let part = rawPart;
-        let oddOnly = false;
-        let evenOnly = false;
+        // ========== 改进点 1:先判断单双标记,再提取数字 ==========
+        const hasDan = /单/.test(rawPart);
+        const hasShuang = /双/.test(rawPart);
+        // 仅含“单”不含“双” → 单周(奇数)
+        let oddOnly = hasDan && !hasShuang;
+        // 仅含“双”不含“单” → 双周(偶数)
+        let evenOnly = hasShuang && !hasDan;
+        // 若同时包含“单”和“双”,视为不限制奇偶(每周都上)
+        // 无需额外处理,因为 oddOnly 和 evenOnly 已经为 false
 
-        // 逐段检测单双周标记
-        if (/单/.test(part)) oddOnly = true;
-        if (/双/.test(part)) evenOnly = true;
-        part = part.replace(/\(单\)|\(双\)|单|双/g, '');
+        // 提取片段中的数字和连字符(忽略括号、单双等非数字字符)
+        const numericPart = rawPart.replace(/[^\d\-]/g, '');
+        if (!numericPart) {
+            console.warn(`JS: 解析周次时忽略非法片段: ${rawPart}`);
+            continue;
+        }
 
-        if (part.includes('-')) {
-            const [startStr, endStr] = part.split('-');
+        if (!/^[\d\-]+$/.test(numericPart)) {
+            console.warn(`JS: 解析周次时忽略非法片段: ${rawPart}`);
+            continue;
+        }
+
+        // 解析范围或单个数字
+        if (numericPart.includes('-')) {
+            const [startStr, endStr] = numericPart.split('-');
             const start = Number(startStr);
             const end = Number(endStr);
             if (!isNaN(start) && !isNaN(end) && start <= end) {
                 for (let w = start; w <= end; w++) {
-                    if (oddOnly && w % 2 !== 1) continue;
-                    if (evenOnly && w % 2 !== 0) continue;
+                    if (oddOnly && w % 2 !== 1) continue;   // 单周只保留奇数
+                    if (evenOnly && w % 2 !== 0) continue;   // 双周只保留偶数
                     weeks.add(w);
                 }
             }
         } else {
-            const w = Number(part);
+            const w = Number(numericPart);
             if (!isNaN(w)) {
                 if (oddOnly && w % 2 !== 1) continue;
                 if (evenOnly && w % 2 !== 0) continue;
@@ -45,11 +149,47 @@ function parseWeeks(weekStr) {
         }
     }
 
+    // 备用解析:如果正常解析为空,尝试用正则直接提取所有数字和范围
+    if (weeks.size === 0) {
+        // ========== 改进点 2:备用解析使用一致的奇偶判断逻辑 ==========
+        const hasDanOverall = /单/.test(weekStr);
+        const hasShuangOverall = /双/.test(weekStr);
+        let oddOnlyOverall = hasDanOverall && !hasShuangOverall;
+        let evenOnlyOverall = hasShuangOverall && !hasDanOverall;
+
+        const rangePattern = /(\d+)\s*-\s*(\d+)/g;
+        let match;
+        let matched = false;
+        while ((match = rangePattern.exec(weekStr)) !== null) {
+            const start = Number(match[1]);
+            const end = Number(match[2]);
+            if (start <= end) {
+                for (let w = start; w <= end; w++) {
+                    if (oddOnlyOverall && w % 2 === 0) continue;   // 单周跳过偶数
+                    if (evenOnlyOverall && w % 2 !== 0) continue;   // 双周跳过奇数
+                    weeks.add(w);
+                }
+                matched = true;
+            }
+        }
+        if (!matched) {
+            const singlePattern = /(\d+)周/g;
+            while ((match = singlePattern.exec(weekStr)) !== null) {
+                const w = Number(match[1]);
+                if (!isNaN(w)) {
+                    if (oddOnlyOverall && w % 2 === 0) continue;
+                    if (evenOnlyOverall && w % 2 !== 0) continue;
+                    weeks.add(w);
+                }
+            }
+        }
+    }
+
     return Array.from(weeks).sort((a, b) => a - b);
 }
-
 /**
- * 解析 API 返回的 JSON 数据。
+ * 解析 API 返回的 JSON 数据,提取课程信息。
+ * 保持与旧版完全一致的课程解析行为。
  * @param {Object} jsonData - 教务系统返回的 JSON 对象
  * @returns {Array} 解析后的课程数组
  */
@@ -65,6 +205,7 @@ function parseJsonData(jsonData) {
     const finalCourseList = [];
 
     for (const rawCourse of rawCourseList) {
+        // 旧版严格非空校验,包含 cdmc
         if (!rawCourse.kcmc || !rawCourse.xm || !rawCourse.cdmc ||
             !rawCourse.xqj || !rawCourse.jcs || !rawCourse.zcd) {
             continue;
@@ -75,7 +216,7 @@ function parseJsonData(jsonData) {
             continue;
         }
 
-        // 去除“节”字后再拆分,防止 "1-2节" 导致 NaN
+        // 旧版节次解析:整体去除“节”字,按 '-' 分割取首尾
         const jcs = String(rawCourse.jcs).replace(/节/g, '');
         const sectionParts = jcs.split('-');
         const startSection = Number(sectionParts[0]);
@@ -99,6 +240,7 @@ function parseJsonData(jsonData) {
         });
     }
 
+    // 旧版排序规则
     finalCourseList.sort((a, b) =>
         a.day - b.day ||
         a.startSection - b.startSection ||
@@ -106,51 +248,236 @@ function parseJsonData(jsonData) {
     );
 
     console.log(`JS: JSON 数据解析完成,共找到 ${finalCourseList.length} 门课程。`);
-    return finalCourseList;
+    const merged = mergeAndDistinctCourses(finalCourseList);
+    window.shiguangBridge.showToast(`去重前:${finalCourseList.length} 门,去重后:${merged.length} 门`);
+    return merged;
+}
+/**
+ * 根据课程周次数组推断学期总周数。
+ * @param {Array} courses - 课程数组
+ * @returns {number} 推断出的最大周次,至少为1
+ */
+function inferTotalWeeks(courses) {
+    let maxWeek = 0;
+    for (const course of courses) {
+        const weekNums = course.weeks;
+        if (weekNums.length > 0) {
+            maxWeek = Math.max(maxWeek, ...weekNums);
+        }
+    }
+    // 返回至少1周,若无法推断则使用默认20
+    return Math.max(1, maxWeek || 20);
+}
+
+/**
+ * 从周次数据数组中解析第 1 周的开学日期。
+ * @param {Array} weekList - 教务系统返回的周次数据数组
+ * @returns {string|null} 开学日期字符串(YYYY-MM-DD),解析失败返回 null
+ */
+function parseSchoolStartDate(weekList) {
+    if (!Array.isArray(weekList) || weekList.length === 0) {
+        return null;
+    }
+
+    // 找到第 1 周
+    let firstWeek = weekList.find(item => String(item.zs) === '1');
+
+    if (!firstWeek) {
+        firstWeek = weekList.find(item => Number(item.zs) === 1);
+    }
+
+    // 如果还是没有,就按周次排序,取最小周次
+    if (!firstWeek) {
+        const sorted = [...weekList].sort((a, b) => Number(a.zs) - Number(b.zs));
+        firstWeek = sorted[0];
+    }
+
+    if (!firstWeek) {
+        return null;
+    }
+
+    // 优先从 rq 字段取开始日期
+    const rq = String(firstWeek.rq || '');
+    const rqMatch = rq.match(/\d{4}-\d{1,2}-\d{1,2}/);
+    if (rqMatch) {
+        return rqMatch[0];
+    }
+
+    // 兼容从 zcrq 字段取开始日期
+    const zcrq = String(firstWeek.zcrq || '');
+    const zcrqMatch = zcrq.match(/\d{4}-\d{1,2}-\d{1,2}/);
+    if (zcrqMatch) {
+        return zcrqMatch[0];
+    }
+
+    return null;
 }
 
+/**
+ * 从教务系统周次数据中提取最大周次。
+ * @param {Array} weekList - 教务系统周次数据数组
+ * @returns {number|null} 最大周次,解析失败返回 null
+ */
+function getMaxWeekFromSchoolWeekList(weekList) {
+    if (!Array.isArray(weekList) || weekList.length === 0) {
+        return null;
+    }
+    let max = 0;
+    for (const item of weekList) {
+        const week = Number(item.zs);
+        if (!isNaN(week) && week > max) {
+            max = week;
+        }
+    }
+    return max > 0 ? max : null;
+}
+
+function getDefaultSemesterDate(academicYear, semesterIndex) {
+    const year = Number(academicYear);
+    if (isNaN(year)) return "2026-02-01";
+    if (semesterIndex === 0) {
+        return `${year}-09-01`;
+    } else {
+        return `${year + 1}-02-01`;
+    }
+}
+
+// ===================== 全局常量 =====================
+
+const BASE_URL = 'https://jwglxt.gpnu.edu.cn';
+
 // ===================== 全局验证函数 =====================
 
+/**
+ * 判断一个字符串是否为有效的日期(YYYY-MM-DD),并检查日期是否真实存在。
+ * @param {string} dateStr - 日期字符串
+ * @returns {boolean} 是否有效
+ */
+function isValidDateString(dateStr) {
+    if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) return false;
+    const [year, month, day] = dateStr.split('-').map(Number);
+    if (year < 2000 || year > 2100) return false;
+    if (month < 1 || month > 12) return false;
+    const daysInMonth = new Date(year, month, 0).getDate();
+    return day >= 1 && day <= daysInMonth;
+}
+
+/**
+ * 校验学年输入:必须为四位数字,且在合理范围内(2000-2100)。
+ * @param {string|null|undefined} input - 用户输入
+ * @returns {string|false} 校验失败返回提示字符串,通过返回 false
+ */
 function validateYearInput(input) {
-    console.log("JS: validateYearInput 被调用,输入: " + input);
+    console.log("JS:  validateYearInput被调用,输入: " + input);
     if (input === null || input === undefined) {
         return "请输入四位数字的学年喵~";
     }
     if (/^[0-9]{4}$/.test(input)) {
-        return false; // 校验通过
+        const year = Number(input);
+        if (year >= 2000 && year <= 2100) {
+            return false;
+        } else {
+            return "学年需在 2000 到 2100 之间喵~";
+        }
     } else {
         return "请输入四位数字的学年喵~";
     }
 }
 
+/**
+ * 校验日期输入:允许空值,格式必须为 YYYY-MM-DD,且日期有效。
+ * @param {string|null|undefined} input - 用户输入
+ * @returns {string|false} 校验失败返回提示字符串,通过返回 false
+ */
 function validateDateInput(input) {
     if (input === null || input === undefined || input.trim() === '') {
-        return false; // 允许空值(留空跳过)
+        return false;
     }
-    if (/^\d{4}-\d{2}-\d{2}$/.test(input.trim())) {
-        return false; // 校验通过
+    if (isValidDateString(input.trim())) {
+        return false;
     }
-    return "日期格式应为 YYYY-MM-DD,例如 2026-08-30";
+    return "日期格式应为 YYYY-MM-DD,且为有效日期喵~";
+}
+
+/**
+ * 校验学期总周数输入:必须为 1~30 的整数。
+ * @param {string|null|undefined} input - 用户输入
+ * @returns {string|false} 校验失败返回提示字符串,通过返回 false
+ */
+function validateWeeksInput(input) {
+    if (input === null || input === undefined || input.trim() === '') {
+        return "请输入学期总周数(1-30)喵~";
+    }
+    const num = Number(input.trim());
+    if (Number.isInteger(num) && num >= 1 && num <= 30) {
+        return false;
+    }
+    return "周数必须是 1 到 30 之间的整数喵~";
 }
 
 // ===================== 与原生交互的异步封装 =====================
 
+/**
+ * 询问用户是否退出当前导入流程。
+ * @returns {Promise<boolean>} true=退出,false=留在页面
+ */
+async function promptUserToExit() {
+    console.log("JS: 询问用户是否退出。");
+    const options = ["退出", "留在页面"];
+    const rawIndex = await window.shiguangBridgePromise.showSingleSelection(
+        "导入流程已结束",
+        JSON.stringify(options),
+        0  // 默认选中“退出”
+    );
+
+    // 用户取消选择(返回 null/undefined/'')时,默认不退出
+    if (rawIndex === null || rawIndex === undefined || rawIndex === '') {
+        console.log("JS: 用户取消退出选择,默认留在页面。");
+        return false;
+    }
+    const index = Number(rawIndex);
+    if (isNaN(index) || index < 0 || index >= options.length) {
+        console.warn("JS: 退出选择索引无效,默认留在页面。");
+        return false;
+    }
+    return index === 0;
+}
+
 async function promptUserToStart() {
     console.log("JS: 流程开始:显示公告。");
     return await window.shiguangBridgePromise.showAlert(
-        "教务系统课表导入",
-        "Ciallo~ 导入前请确保您已在浏览器中成功登录教务系统哦~",
-        "好的,开始导入"
+        "教务系统课表导入喵~",
+        "Ciallo~ 导入前请确保您已成功登录教务系统哦~",
+        "好的"
     );
 }
 
+/**
+ * 显示课程导入成功的弹窗公告。
+ * @param {number} length - 导入的课程数量
+ */
+async function promptUserToSUCCESS(length) {
+    console.log("JS: SUCCESS:显示公告。");
+    return await window.shiguangBridgePromise.showAlert(
+        "SUCCESS",
+        `成功了喵~ 共导入 ${length} 门课程!`,
+        "好的"
+    );
+}
+
+const SCHOOL_START_MONTH = 8; // 默认
+
 async function getAcademicYear() {
-    const currentYear = new Date().getFullYear().toString();
-    console.log("JS: 提示用户输入学年。");
+    const now = new Date();
+    let academicYear = now.getFullYear();
+    if (now.getMonth() + 1 < SCHOOL_START_MONTH) {
+        academicYear -= 1;
+    }
+    const defaultYear = academicYear.toString();
     return await window.shiguangBridgePromise.showPrompt(
-        "选择学年喵~",
-        "请输入要导入课程的起始学年喵~(例如 2025-2026 应输入2025):",
-        currentYear,
+        "选择学年喵~(Auto value)",
+        "请输入要导入课程的起始学年喵~:",
+        defaultYear,
         "validateYearInput"
     );
 }
@@ -163,8 +490,17 @@ async function selectSemester() {
         JSON.stringify(semesters),
         0
     );
-    // 统一转换为数字,避免字符串索引导致判断错误
-    return Number(rawIndex);
+
+    if (rawIndex === null || rawIndex === undefined || rawIndex === '') {
+        console.log("JS: 用户取消选择学期。");
+        return null;
+    }
+    const index = Number(rawIndex);
+    if (isNaN(index) || index < 0 || index >= semesters.length) {
+        console.warn("JS: 学期索引无效,返回 null。");
+        return null;
+    }
+    return index;
 }
 
 async function selectArea() {
@@ -175,19 +511,94 @@ async function selectArea() {
         JSON.stringify(areas),
         0
     );
-    // 统一转换为数字
-    return Number(rawIndex);
+
+    if (rawIndex === null || rawIndex === undefined || rawIndex === '') {
+        console.log("JS: 用户取消选择校区。");
+        return null;
+    }
+    const index = Number(rawIndex);
+    if (isNaN(index) || index < 0 || index >= areas.length) {
+        console.warn("JS: 校区索引无效,返回 null。");
+        return null;
+    }
+    return index;
+}
+
+async function selectTotalWeeksSource(courseMaxWeeks, schoolMaxWeeks) {
+    const options = [
+        `课程最大周数(${courseMaxWeeks}周)`,
+        schoolMaxWeeks ? `教务系统周数(${schoolMaxWeeks}周)` : "教务系统周数(无数据)",
+        "手动输入"
+    ];
+    console.log("JS: 提示用户选择周数来源。");
+    const rawIndex = await window.shiguangBridgePromise.showSingleSelection(
+        "选择学期总周数来源喵~(已自动解析)",
+        JSON.stringify(options),
+        0
+    );
+
+    if (rawIndex === null || rawIndex === undefined || rawIndex === '') {
+        console.log("JS: 用户取消选择周数来源。");
+        return null;
+    }
+
+    const index = Number(rawIndex);
+    if (isNaN(index) || index < 0 || index > 2) {
+        console.warn("JS: 周数来源选择无效,默认使用课程最大周数。");
+        return 0;
+    }
+    return index;
 }
 
-async function getSemesterStartDate() {
+/**
+ * 获取学期开始日期。
+ * 优先使用传入的周次数据解析开学日期;若未传入或数据无效,则向教务系统请求。
+ * 解析成功后弹出对话框让用户确认或修改,用户可留空跳过(返回 null)。
+ * @param {string} academicYear - 学年(四位数字)
+ * @param {number} semesterIndex - 学期索引(0 第一学期,1 第二学期)
+ * @param {Array|null} [weekList=null] - 可选的周次数据数组,若提供且非空则直接使用
+ * @returns {Promise<string|null>} 用户确认的日期字符串(YYYY-MM-DD),取消或留空返回 null
+ */
+async function getSemesterStartDate(academicYear, semesterIndex, weekList = null) {
+    console.log("JS: 正在获取学期周次数据以解析开学日期...");
+    let defaultDate = getDefaultSemesterDate(academicYear, semesterIndex);
+
+    // 1. 确定要使用的周次数据来源
+    let data = weekList;
+    if (!Array.isArray(data) || data.length === 0) {
+        // 外部没有提供有效数据,尝试从教务系统获取
+        try {
+            data = await fetchSchoolWeekData(academicYear, getSemesterCode(semesterIndex));
+        } catch (error) {
+            console.warn("JS: 获取周次数据失败,将使用动态默认日期。", error);
+            window.shiguangBridge.showToast("未能获取学期周次数据,开始日期可能需要手动填写。");
+            data = null; // 确保后续不会使用无效数据
+        }
+    }
+
+    // 2. 解析开学日期
+    if (Array.isArray(data) && data.length > 0) {
+        const parsedDate = parseSchoolStartDate(data);
+        if (parsedDate) {
+            defaultDate = parsedDate;
+            console.log(`JS: 解析到的开学日期: ${defaultDate}`);
+        } else {
+            console.log("JS: 未能从周次数据解析出开学日期,使用动态默认值。");
+        }
+    } else {
+        console.log("JS: 无可用周次数据,使用动态默认值。");
+    }
+
+    // 3. 弹出对话框让用户确认或修改日期(可留空跳过)
     console.log("JS: 提示用户输入学期开始日期(可留空跳过)。");
     const input = await window.shiguangBridgePromise.showPrompt(
-        "学期开始日期(可选)",
-        "请输入学期第一天的日期喵~(YYYY-MM-DD),留空则自动留空:",
-        "2026-09-07",
+        "学期开始日期喵~",
+        "请输入学期第一天的日期喵~(YYYY-MM-DD)(已尝试自动解析)",
+        defaultDate,
         "validateDateInput"
     );
-    if (input === null) {
+
+    if (input === null || input === undefined) {
         console.log("JS: 用户取消了开始日期输入,继续流程。");
         return null;
     }
@@ -197,21 +608,55 @@ async function getSemesterStartDate() {
     return input.trim();
 }
 
-function getSemesterCode(semesterIndex) {
-    // 3 表示第一学期,12 表示第二学期
-    return semesterIndex === 0 ? "3" : "12";
+async function getSemesterTotalWeeks(defaultWeeks) {
+    console.log(`JS: 提示用户输入学期总周数,默认值:${defaultWeeks}`);
+    const input = await window.shiguangBridgePromise.showPrompt(
+        "设置学期总周数喵~",
+        "请输入本学期的总周数喵~(1-30)",
+        String(defaultWeeks),
+        "validateWeeksInput"
+    );
+    if (input === null || input === undefined) {
+        console.log("JS: 用户取消了总周数输入,流程继续使用默认值。");
+        return defaultWeeks;
+    }
+    const weeks = Number(input.trim());
+    if (!Number.isInteger(weeks) || weeks < 1 || weeks > 30) {
+        console.warn("JS: 周数输入非法,使用默认值。");
+        return defaultWeeks;
+    }
+    return weeks;
 }
 
-// ===================== 网络请求与课程解析 =====================
+async function promptImportTimeSlots() {
+    console.log("JS: 询问用户是否导入预设时间段。");
+    return await window.shiguangBridgePromise.showAlert(
+        "导入预设时间段喵~",
+        "是否导入该校区对应的预设上课时间段喵~?",
+        "导入"
+    );
+}
 
-async function fetchAndParseCourses(academicYear, semesterIndex) {
+// ===================== 网络请求 =====================
+
+async function fetchCourseData(academicYear, semesterIndex) {
     const semesterCode = getSemesterCode(semesterIndex);
-    const requestBody = `gnmkdm=N2151&xnm=${academicYear}&xqm=${semesterCode}&kzlx=ck&xsdm=&kclbdm=&kclxdm=`;
+    const requestBody = new URLSearchParams({
+        gnmkdm: 'N2151',
+        xnm: academicYear,
+        xqm: semesterCode,
+        kzlx: 'ck',
+        xsdm: '',
+        kclbdm: '',
+        kclxdm: ''
+    }).toString();
 
     const targetUrls = [
-        "https://jwglxt.gpnu.edu.cn/jwglxt/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N2151"
+        `${BASE_URL}/jwglxt/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N2151`,
     ];
 
+    const errors = [];
+
     for (const url of targetUrls) {
         try {
             const response = await fetch(url, {
@@ -219,47 +664,147 @@ async function fetchAndParseCourses(academicYear, semesterIndex) {
                 headers: {
                     "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
                     "X-Requested-With": "XMLHttpRequest"
-                    // 注意:浏览器禁止手动设置 Origin 和 Referer,已移除。
                 },
                 body: requestBody,
                 credentials: "include"
             });
 
-            if (response.ok) {
-                const jsonText = await response.text();
-                const jsonData = JSON.parse(jsonText);
-                if (jsonData && jsonData.kbList) {
-                    const parsedCourses = parseJsonData(jsonData);
-                    if (parsedCourses.length > 0) {
-                        const totalWeeks = inferTotalWeeks(parsedCourses);
-                        return {
-                            courses: parsedCourses,
-                            config: {
-                                semesterStartDate: null,
-                                semesterTotalWeeks: totalWeeks
-                            }
-                        };
-                    }
-                }
+            if (!response.ok) {
+                const msg = `HTTP ${response.status} ${response.statusText}`;
+                errors.push(`${url} -> ${msg}`);
+                console.warn(`课表数据请求失败(${msg}):${url}`);
+                continue;
+            }
+
+            const jsonText = await response.text();
+            if (jsonText.includes('<html')) {
+                errors.push(`${url} -> 返回登录页面,可能登录已过期`);
+                console.warn(`课表数据返回登录页面,可能未登录或会话过期:${url}`);
+                continue;
+            }
+
+            let data;
+            try {
+                data = JSON.parse(jsonText);
+            } catch (parseError) {
+                errors.push(`${url} -> JSON 解析失败: ${parseError.message}`);
+                console.warn(`课表数据 JSON 解析失败:${url}`, parseError);
+                continue;
             }
-        } catch (e) {
-            console.error(`Entry failed: ${url}`, e);
+
+            if (!data || !Array.isArray(data.kbList)) {
+                errors.push(`${url} -> 返回数据缺少 kbList 数组`);
+                console.warn(`课表数据缺少 kbList 字段:${url}`, data);
+                continue;
+            }
+
+            return data;
+        } catch (networkError) {
+            errors.push(`${url} -> 网络异常: ${networkError.message}`);
+            console.warn(`课表数据网络请求异常:${url}`, networkError);
         }
     }
 
-    window.shiguangBridge.showToast("未能获取课表数据,请检查网络环境或登录状态。");
-    return null;
+    const errorMessage = errors.length > 0
+        ? `课表数据获取失败,尝试了 ${targetUrls.length} 个地址:\n${errors.join('\n')}`
+        : '课表数据获取失败,未配置任何请求地址';
+    console.error(errorMessage);
+    throw new Error(errorMessage);
 }
 
-function inferTotalWeeks(courses) {
-    let maxWeek = 0;
-    for (const course of courses) {
-        const weekNums = course.weeks;
-        if (weekNums.length > 0) {
-            maxWeek = Math.max(maxWeek, ...weekNums);
+async function fetchSchoolWeekData(xnm, xqm) {
+    const targetUrls = [
+        `${BASE_URL}/jwglxt/kbcx/xskbcxZccx_cxZcByXnxq.html?gnmkdm=N2154`,
+    ];
+
+    const requestBody = new URLSearchParams({
+        xnm: xnm,
+        xqm: xqm
+    }).toString();
+
+    const errors = [];
+
+    for (const url of targetUrls) {
+        try {
+            const response = await fetch(url, {
+                headers: {
+                    'accept': 'application/json, text/javascript, */*; q=0.01',
+                    'accept-language': 'zh-CN,zh;q=0.9',
+                    'content-type': 'application/x-www-form-urlencoded;charset=UTF-8',
+                    'x-requested-with': 'XMLHttpRequest'
+                },
+                body: requestBody,
+                method: 'POST',
+                mode: 'cors',
+                credentials: 'include'
+            });
+
+            if (!response.ok) {
+                const msg = `HTTP ${response.status} ${response.statusText}`;
+                errors.push(`${url} -> ${msg}`);
+                console.warn(`周次数据请求失败(${msg}):${url}`);
+                continue;
+            }
+
+            const text = await response.text();
+            if (text.includes('<html')) {
+                errors.push(`${url} -> 返回登录页面,可能登录已过期`);
+                console.warn(`周次数据返回登录页面,可能未登录或会话过期:${url}`);
+                continue;
+            }
+
+            let data;
+            try {
+                data = JSON.parse(text);
+            } catch (parseError) {
+                errors.push(`${url} -> JSON 解析失败: ${parseError.message}`);
+                console.warn(`周次数据 JSON 解析失败:${url}`, parseError);
+                continue;
+            }
+
+            if (!Array.isArray(data)) {
+                const msg = '返回数据不是数组';
+                errors.push(`${url} -> ${msg}`);
+                console.warn(`周次数据格式错误(期望数组):${url}`, data);
+                continue;
+            }
+
+            return data;
+        } catch (networkError) {
+            errors.push(`${url} -> 网络异常: ${networkError.message}`);
+            console.warn(`周次数据网络请求异常:${url}`, networkError);
         }
     }
-    return maxWeek || 20;
+
+    const errorMessage = errors.length > 0
+        ? `周次数据获取失败,尝试了 ${targetUrls.length} 个地址:\n${errors.join('\n')}`
+        : '周次数据获取失败,未配置任何请求地址';
+    console.error(errorMessage);
+    throw new Error(errorMessage);
+}
+
+// ===================== 数据解析与配置构建 =====================
+
+function buildCourseDataFromRaw(rawData, startDate) {
+    console.log("JS: buildCourseDataFromRaw 开始解析原始数据...");
+
+    if (!rawData || !Array.isArray(rawData.kbList)) {
+        throw new Error('课表数据格式错误或缺少 kbList 字段');
+    }
+
+    const courses = parseJsonData(rawData);
+    if (courses.length === 0) {
+        throw new Error('未解析到有效课程,请检查课表数据');
+    }
+
+    const inferredWeeks = inferTotalWeeks(courses);
+    const config = {
+        semesterStartDate: startDate,
+        semesterTotalWeeks: inferredWeeks
+    };
+
+    console.log(`JS: 解析成功,课程数:${courses.length},推断总周数:${inferredWeeks}`);
+    return { courses, config };
 }
 
 // ===================== 数据保存 =====================
@@ -278,9 +823,25 @@ async function saveCourses(parsedCourses) {
     }
 }
 
+async function importPresetTimeSlots(timeSlots) {
+    console.log(`JS: 准备导入 ${timeSlots.length} 个预设时间段。`);
+    if (timeSlots.length > 0) {
+        window.shiguangBridge.showToast(`正在导入 ${timeSlots.length} 个预设时间段...`);
+        try {
+            await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
+            window.shiguangBridge.showToast("预设时间段导入成功!");
+        } catch (error) {
+            window.shiguangBridge.showToast("导入时间段失败: " + error.message);
+            console.error('JS: Save Time Slots Error:', error);
+        }
+    } else {
+        window.shiguangBridge.showToast("警告:时间段为空,未导入时间段信息。");
+        console.warn("JS: 警告:传入时间段为空,未导入时间段信息。");
+    }
+}
+
 // ===================== 三个校区的时间表 =====================
 
-// 东校区-西校区-北校区(统一)
 const TimeSlots_one = [
     { number: 1, startTime: "08:20", endTime: "09:00" },
     { number: 2, startTime: "09:10", endTime: "09:50" },
@@ -295,7 +856,6 @@ const TimeSlots_one = [
     { number: 11, startTime: "20:20", endTime: "21:00" }
 ];
 
-// 白云校区
 const TimeSlots_two = [
     { number: 1, startTime: "08:30", endTime: "09:10" },
     { number: 2, startTime: "09:15", endTime: "09:55" },
@@ -310,7 +870,6 @@ const TimeSlots_two = [
     { number: 11, startTime: "20:10", endTime: "20:50" }
 ];
 
-// 河源校区
 const TimeSlots_three = [
     { number: 1, startTime: "08:20", endTime: "09:00" },
     { number: 2, startTime: "09:10", endTime: "09:50" },
@@ -325,108 +884,176 @@ const TimeSlots_three = [
     { number: 11, startTime: "20:20", endTime: "21:00" }
 ];
 
-// 根据校区索引获取对应时间表
+// ===================== 索引 =====================
+
+function getSemesterCode(semesterIndex) {
+    return semesterIndex === 0 ? "3" : "12";
+}
+
 function getTimeSlotsByAreaIndex(areaIndex) {
     if (areaIndex === 0) return TimeSlots_one;
     if (areaIndex === 1) return TimeSlots_two;
     if (areaIndex === 2) return TimeSlots_three;
-    // 默认返回第一个
     return TimeSlots_one;
 }
 
-async function importPresetTimeSlots(timeSlots) {
-    console.log(`JS: 准备导入 ${timeSlots.length} 个预设时间段。`);
-    if (timeSlots.length > 0) {
-        window.shiguangBridge.showToast(`正在导入 ${timeSlots.length} 个预设时间段...`);
-        try {
-            await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
-            window.shiguangBridge.showToast("预设时间段导入成功!");
-        } catch (error) {
-            window.shiguangBridge.showToast("导入时间段失败: " + error.message);
-            console.error('JS: Save Time Slots Error:', error);
-        }
-    } else {
-        window.shiguangBridge.showToast("警告:时间段为空,未导入时间段信息。");
-        console.warn("JS: 警告:传入时间段为空,未导入时间段信息。");
-    }
-}
-
 // ===================== 主流程 =====================
 
 async function runImportFlow() {
+    let shouldExit = false;          // 最终是否退出(由用户选择决定)
+    let flowEnded = false;           // 标记流程是否已经走到需要询问退出的节点
 
-    const alertConfirmed = await promptUserToStart();
-    if (!alertConfirmed) {
-        window.shiguangBridge.showToast("用户取消了导入。");
-        console.log("JS: 用户取消了导入流程。");
-        return;
-    }
+    try {
+        // ================= 流程开始 =================
+        const alertConfirmed = await promptUserToStart();
+        if (!alertConfirmed) {
+            window.shiguangBridge.showToast("导入已取消。");
+            console.log("JS: 用户取消了导入流程。");
+            flowEnded = true;
+            return; // 直接返回,但 finally 中会询问是否退出
+        }
 
-    const academicYear = await getAcademicYear();
-    if (academicYear === null) {
-        window.shiguangBridge.showToast("导入已取消。");
-        console.log("JS: 获取学年失败/取消,流程终止。");
-        return;
-    }
-    console.log(`JS: 已选择学年: ${academicYear}`);
+        // 获取学年
+        const academicYear = await getAcademicYear();
+        if (academicYear === null || academicYear === undefined) {
+            window.shiguangBridge.showToast("导入已取消。");
+            console.log("JS: 获取学年失败/取消,流程终止。");
+            flowEnded = true;
+            return;
+        }
+        console.log(`JS: 已选择学年: ${academicYear}`);
 
-    const semesterIndex = await selectSemester();
-    if (semesterIndex === null || isNaN(semesterIndex) || semesterIndex < 0) {
-        window.shiguangBridge.showToast("导入已取消。");
-        console.log("JS: 选择学期失败/取消,流程终止。");
-        return;
-    }
-    console.log(`JS: 已选择学期索引: ${semesterIndex}`);
+        // 选择学期
+        const semesterIndex = await selectSemester();
+        if (semesterIndex === null) {
+            window.shiguangBridge.showToast("导入已取消。");
+            console.log("JS: 选择学期失败/取消,流程终止。");
+            flowEnded = true;
+            return;
+        }
+        console.log(`JS: 已选择学期索引: ${semesterIndex}`);
 
-    const areaIndex = await selectArea();
-    if (areaIndex === null || isNaN(areaIndex) || areaIndex < 0) {
-        window.shiguangBridge.showToast("导入已取消。");
-        console.log("JS: 选择校区失败/取消,流程终止。");
-        return;
-    }
-    console.log(`JS: 已选择校区索引: ${areaIndex}`);
+        // 选择校区
+        const areaIndex = await selectArea();
+        if (areaIndex === null) {
+            window.shiguangBridge.showToast("导入已取消。");
+            console.log("JS: 选择校区失败/取消,流程终止。");
+            flowEnded = true;
+            return;
+        }
+        console.log(`JS: 已选择校区索引: ${areaIndex}`);
 
-    const startDate = await getSemesterStartDate();
-    console.log(`JS: 学期开始日期输入结果: ${startDate}`);
+        // 获取周次数据(可选)
+        let schoolWeekList = null;
+        try {
+            schoolWeekList = await fetchSchoolWeekData(academicYear, getSemesterCode(semesterIndex));
+        } catch (error) {
+            console.warn("JS: 获取周次数据失败,稍后周数来源可能缺少教务系统选项。", error);
+            window.shiguangBridge.showToast("获取周次数据失败,将使用课程周数或手动输入。");
+            schoolWeekList = [];
+        }
 
-    const result = await fetchAndParseCourses(academicYear, semesterIndex);
-    if (result === null) {
-        console.log("JS: 课程获取或解析失败,流程终止。");
-        return;
-    }
+        // 获取学期开始日期
+        const startDate = await getSemesterStartDate(academicYear, semesterIndex, schoolWeekList);
+        console.log(`JS: 学期开始日期输入结果: ${startDate}`);
 
-    result.config.semesterStartDate = startDate;
-    const { courses, config } = result;
+        // 获取课表原始数据
+        const rawData = await fetchCourseData(academicYear, semesterIndex);
 
-    const saveResult = await saveCourses(courses);
-    if (!saveResult) {
-        console.log("JS: 课程保存失败,流程终止。");
-        return;
-    }
+        // 解析课程
+        let courses, config;
+        try {
+            const parsedResult = buildCourseDataFromRaw(rawData, startDate);
+            courses = parsedResult.courses;
+            config = parsedResult.config;
+        } catch (error) {
+            window.shiguangBridge.showToast(error.message);
+            console.error('JS: 课程解析失败:', error);
+            flowEnded = true;
+            return;
+        }
 
-    try {
-        await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
-        window.shiguangBridge.showToast(`课表配置更新成功!总周数:${config.semesterTotalWeeks}周。`);
-    } catch (error) {
-        window.shiguangBridge.showToast(`课表配置保存失败: ${error.message}`);
-        console.error('JS: Save Config Error:', error);
-    }
+        // 周数来源选择
+        const courseMaxWeeks = config.semesterTotalWeeks;
+        const schoolMaxWeeks = getMaxWeekFromSchoolWeekList(schoolWeekList);
 
-    // 根据校区选择对应时间表并导入
-    const timeSlots = getTimeSlotsByAreaIndex(areaIndex);
-    await importPresetTimeSlots(timeSlots);
+        const weekSource = await selectTotalWeeksSource(courseMaxWeeks, schoolMaxWeeks);
+        if (weekSource === null) {
+            window.shiguangBridge.showToast("未选择周数来源,导入已取消。");
+            console.log("JS: 用户取消周数来源选择,流程终止。");
+            flowEnded = true;
+            return;
+        }
 
-    window.shiguangBridge.showToast(`课程导入成功,共导入 ${courses.length} 门课程!`);
-    console.log("JS: 整个导入流程执行完毕并成功。");
-    window.shiguangBridge.notifyTaskCompletion();
-}
+        let finalWeeks;
+        if (weekSource === 0) {
+            finalWeeks = courseMaxWeeks;
+        } else if (weekSource === 1) {
+            if (schoolMaxWeeks) {
+                finalWeeks = schoolMaxWeeks;
+            } else {
+                window.shiguangBridge.showToast("教务系统周数不可用,已使用课程最大周数。");
+                finalWeeks = courseMaxWeeks;
+            }
+        } else {
+            finalWeeks = await getSemesterTotalWeeks(courseMaxWeeks);
+        }
+        config.semesterTotalWeeks = finalWeeks;
 
-// 启动导入流程,并添加全局异常捕获
-runImportFlow().catch(e => {
-    console.error('JS: 导入流程异常:', e);
-    try {
-        window.shiguangBridge.showToast('导入失败:' + (e && e.message ? e.message : e));
-    } catch (_) {
-        // 忽略
+        // 保存课程
+        const saveResult = await saveCourses(courses);
+        if (!saveResult) {
+            console.log("JS: 课程保存失败,流程终止。");
+            flowEnded = true;
+            return;
+        }
+
+        // 保存配置
+        try {
+            await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
+            window.shiguangBridge.showToast(`课表配置更新成功!总周数:${config.semesterTotalWeeks}周。`);
+        } catch (error) {
+            window.shiguangBridge.showToast(`课表配置保存失败: ${error.message}`);
+            console.error('JS: Save Config Error:', error);
+        }
+
+        // 导入预设时间段
+        const timeSlots = getTimeSlotsByAreaIndex(areaIndex);
+        const shouldImportTimeSlots = await promptImportTimeSlots();
+        if (shouldImportTimeSlots) {
+            await importPresetTimeSlots(timeSlots);
+        } else {
+            console.log("JS: 用户取消导入预设时间段,跳过。");
+            window.shiguangBridge.showToast("已跳过导入时间段。");
+        }
+
+        await promptUserToSUCCESS(courses.length);
+        console.log("JS: 整个导入流程执行完毕并成功。");
+        flowEnded = true; // 成功结束
+
+    } catch (e) {
+        console.error('JS: 导入流程异常:', e);
+        try {
+            window.shiguangBridge.showToast('导入失败:' + (e && e.message ? e.message : e));
+        } catch (_) {}
+        flowEnded = true; // 异常结束
+    } finally {
+        // 无论成功、失败、取消,只要流程已经结束,就询问是否退出
+        if (flowEnded) {
+            shouldExit = await promptUserToExit();
+            if (shouldExit) {
+                console.log("JS: 用户选择退出,通知原生任务结束。");
+                try {
+                    window.shiguangBridge.notifyTaskCompletion();
+                } catch (error) {
+                    console.error("JS: 调用 notifyTaskCompletion 失败:", error);
+                }
+            } else {
+                console.log("JS: 用户选择留在页面,不通知原生。");
+                // 可在此处添加刷新页面或保持现状的逻辑
+                window.location.reload();
+            }
+        }
     }
-});
+}
+runImportFlow();