Bladeren bron

Merge pull request #858 from ShiGuangSchedule/pending

星河欲转 2 weken geleden
bovenliggende
commit
c19d1f1850

+ 10 - 0
index/root_index.yaml

@@ -203,6 +203,11 @@ schools:
     initial: "G"
     resource_folder: "GZST"
 
+  - id: "GZIST"
+    name: "广州理工学院"
+    initial: "G"
+    resource_folder: "GZIST"
+
   - id: "FJCPC"
     name: "福建船政交通职业学院"
     initial: "F"
@@ -1262,3 +1267,8 @@ schools:
     name: "景德镇艺术职业大学"
     initial: "J"
     resource_folder: "JDZVUA"
+
+  - id: "ZWU"
+    name: "浙江万里学院"
+    initial: "Z"
+    resource_folder: "ZWU"

+ 18 - 0
resources/GZIST/adapters.yaml

@@ -0,0 +1,18 @@
+# resources/GZIST/adapters.yaml
+# 广州理工学院(GZIST)· maintainer: YT-O5
+adapters:
+  - adapter_id: "GZIST_01"
+    adapter_name: "广州理工学院(统一身份认证)"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "gzist_01.js"
+    import_url: "https://ids.gzist.edu.cn/lyuapServer/login?service=http%3A%2F%2Fjw.gzist.edu.cn%2Fsso%2Flyiotlogin"
+    maintainer: "YT-O5"
+    description: "输入统一认证门户账号密码后在信息查询页面进入学生课表查询,随后点击底部“执行导入”。"
+
+  - adapter_id: "GZIST_02"
+    adapter_name: "广州理工学院(WebVPN 校外)"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "gzist_01.js"
+    import_url: "https://webvpn.gzist.edu.cn/login"
+    maintainer: "YT-O5"
+    description: "提示:本系统是为非校园网用户提供访问校内资源的途径,如用户使用校园网上网时无需登录本系统。正常可使用统一身份认证通过相关账号密码登录。此项功能未进行测试。"

+ 474 - 0
resources/GZIST/gzist_01.js

@@ -0,0 +1,474 @@
+// 广州理工学院(GZIST)拾光课程表适配脚本
+// 正方教务 v9 接口,参考案例 JSEI_01:
+// https://github.com/XingHeYuZhuan/shiguang_warehouse/tree/main/resources/JSEI
+// 非该校开发者适配,发现问题请提 issue 或 PR
+
+/** 基础域名:校园网直连 / WebVPN(按当前页面自动选择) */
+const BASE_URLS = {
+    campus: "http://jw.gzist.edu.cn/jwglxt",
+    webvpn: "https://webvpn.gzist.edu.cn/https/jw.gzist.edu.cn/jwglxt"
+};
+
+// 本校正方页面覆盖了原型方法且行为不符标准:filter 回调拿到的第一个参数是 index、trim 会删掉字符串内部空白,
+// 故下面两处改用不依赖原型的写法(for 循环 / 正则)。
+const safeTrim = (s) => String(s).replace(/^\s+|\s+$/g, "");
+
+function getBaseUrl() {
+    const { origin, hostname, pathname } = window.location;
+
+    // WebVPN:路径形如 /https/jw.gzist.edu.cn/jwglxt/...
+    if (hostname.includes("webvpn")) {
+        const matched = pathname.match(/\/https\/[^/]+\/jwglxt/);
+        return origin + (matched ? matched[0] : "/https/jw.gzist.edu.cn/jwglxt");
+    }
+
+    // 校园网:从 /jwglxt 起截断
+    const matched = pathname.match(/\/jwglxt(?=\/|$)/);
+    if (matched) return origin + pathname.substring(0, matched.index + matched[0].length);
+
+    return BASE_URLS.campus;
+}
+
+/** 节次与周次合并去重(取自上游 wiki:课程合并与去重函数) */
+function mergeAndDistinctCourses(courses) {
+    if (!Array.isArray(courses) || courses.length <= 1) return courses;
+
+    // 1. 深拷贝并规范周次数据,过滤无效项
+    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) : []
+    }));
+
+    // 阶段 1:合并连续节次与完全重复记录(前提:名称、教师、地点、星期、周次一致)
+    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) {
+            // 节次连续:延长结束节次 (如 1-2 节 + 3-4 节 -> 1-4 节)
+            current.endSection = next.endSection;
+        } else if (isSameCourseAndWeeks && isDuplicate) {
+            // 完全重复:跳过
+            continue;
+        } else {
+            step1Merged.push(current);
+            current = next;
+        }
+    }
+    step1Merged.push(current);
+
+    // 阶段 2:合并同节次的周次(前提:名称、教师、地点、星期、开始/结束节次一致)
+    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) {
+            // 周次合并去重 (如 1-8 周 + 9-16 周 -> 1-16 周)
+            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;
+}
+
+/** 解析周次字符串,处理单双周和周次范围 */
+function parseWeeks(weekStr) {
+    if (!weekStr) return [];
+
+    const cleaned = weekStr
+        .replace(/周数[::]/g, '')
+        .replace(/第/g, '')
+        .replace(/周/g, '')
+        .replace(/共\s*\d+\s*.*$/g, '');
+
+    const segments = cleaned.split(/[,,、;;]/);
+
+    const weeks = [];
+    const segRegex = /(\d+)(?:\s*[-~]\s*(\d+))?\s*(?:[((]?\s*([单双])\s*周?\s*[))]?)?/g;
+
+    for (const seg of segments) {
+        const s = safeTrim(seg);
+        if (!s) continue;
+
+        segRegex.lastIndex = 0;
+        let m;
+        while ((m = segRegex.exec(s)) !== null) {
+            if (m[0] === '') {
+                segRegex.lastIndex++;
+                continue;
+            }
+
+            const start = parseInt(m[1], 10);
+            const end = m[2] ? parseInt(m[2], 10) : start;
+            const flagStr = m[3] || '';
+
+            let flag = 0;
+            if (flagStr.includes('单')) flag = 1;
+            else if (flagStr.includes('双')) flag = 2;
+
+            for (let w = start; w <= end; w++) {
+                if (flag === 1 && w % 2 === 0) continue;
+                if (flag === 2 && w % 2 !== 0) continue;
+                weeks.push(w);
+            }
+        }
+    }
+
+    return [...new Set(weeks)].sort((a, b) => a - b);
+}
+
+/** 解析课表接口返回的 JSON */
+function parseJsonData(jsonData) {
+    if (!jsonData || !Array.isArray(jsonData.kbList)) return [];
+
+    const initialCourseList = [];
+
+    for (const raw of jsonData.kbList) {
+        if (!raw.kcmc || !raw.xqj || !raw.jcs || !raw.zcd) continue;
+
+        const weeksArray = parseWeeks(raw.zcd);
+        if (weeksArray.length === 0) continue;
+
+        const sectionParts = String(raw.jcs).split("-");
+        const startSection = Number(sectionParts[0]);
+        const endSection = Number(sectionParts[sectionParts.length - 1]);
+        const day = Number(raw.xqj);
+
+        if (isNaN(day) || isNaN(startSection) || isNaN(endSection) ||
+            day < 1 || day > 7 || startSection > endSection) {
+            continue;
+        }
+
+        initialCourseList.push({
+            name: safeTrim(raw.kcmc),
+            teacher: safeTrim(raw.xm || ""),      // 无教师(如线上课)时传空串
+            position: safeTrim(raw.cdmc || ""),   // 无固定教室时传空串
+            day: day,
+            startSection: startSection,
+            endSection: endSection,
+            weeks: weeksArray
+        });
+    }
+
+    return mergeAndDistinctCourses(initialCourseList);
+}
+
+async function promptUserToStart() {
+    return await window.shiguangBridgePromise.showAlert(
+        "广州理工学院课表导入",
+        "导入前请确保已进入教务系统页面(本校学生请用统一身份认证登录,教务自身的账号密码不可用;校外请走 WebVPN)",
+        "好的,开始导入"
+    );
+}
+
+/** 获取学年学期选项;学年以选中项为中心取前后共 5 个 */
+async function fetchAcademicOptions() {
+    const url = `${getBaseUrl()}/kbcx/xskbcx_cxXskbcxIndex.html?gnmkdm=N2151&layout=default`;
+
+    try {
+        const response = await fetch(url, {
+            method: "GET",
+            credentials: "include"
+        });
+
+        if (!response.ok) return null;
+
+        const doc = new DOMParser().parseFromString(await response.text(), "text/html");
+        const readOptions = (selector) => {
+            const result = [];
+            doc.querySelectorAll(selector).forEach(opt => {
+                if (opt.value === "") return;
+                result.push({
+                    value: opt.value,
+                    text: safeTrim(opt.textContent),
+                    selected: opt.selected
+                });
+            });
+            return result;
+        };
+
+        const allYearOptions = readOptions("#xnm option");
+        const semesterOptions = readOptions("#xqm option");
+
+        if (allYearOptions.length === 0 || semesterOptions.length === 0) return null;
+
+        const semesterIndex = semesterOptions.findIndex(opt => opt.selected);
+        const defaultSemesterIndex = semesterIndex === -1 ? 0 : semesterIndex;
+        const selectedIndex = allYearOptions.findIndex(opt => opt.selected);
+
+        if (selectedIndex === -1) {
+            return {
+                yearOptions: allYearOptions.slice(0, 5),
+                semesterOptions,
+                defaultYearIndex: 0,
+                defaultSemesterIndex
+            };
+        }
+
+        const start = Math.max(0, selectedIndex - 2);
+        const end = Math.min(allYearOptions.length, selectedIndex + 3);
+
+        return {
+            yearOptions: allYearOptions.slice(start, end),
+            semesterOptions,
+            defaultYearIndex: selectedIndex - start,
+            defaultSemesterIndex
+        };
+    } catch (e) {
+        return null;
+    }
+}
+
+/** 提示用户选择学年和学期 */
+async function selectAcademicYearAndSemester() {
+    const optionsData = await fetchAcademicOptions();
+
+    if (!optionsData) {
+        window.shiguangBridge.showToast("读取学年学期失败,请确认已登录教务系统。");
+        return null;
+    }
+
+    const { yearOptions, semesterOptions, defaultYearIndex, defaultSemesterIndex } = optionsData;
+
+    const yearIndex = await window.shiguangBridgePromise.showSingleSelection(
+        "选择学年",
+        JSON.stringify(yearOptions.map(item => item.text)),
+        defaultYearIndex
+    );
+    if (yearIndex === null || yearIndex === undefined || yearIndex < 0) return null;
+
+    const semesterIndex = await window.shiguangBridgePromise.showSingleSelection(
+        "选择学期",
+        JSON.stringify(semesterOptions.map(item => item.text)),
+        defaultSemesterIndex
+    );
+    if (semesterIndex === null || semesterIndex === undefined || semesterIndex < 0) return null;
+
+    return {
+        academicYear: yearOptions[yearIndex].value,
+        semesterCode: semesterOptions[semesterIndex].value
+    };
+}
+
+/** 获取学期开学日期(失败返回 null,不影响主流程) */
+async function fetchSemesterStartDate(academicYear, semesterCode) {
+    const url = `${getBaseUrl()}/kbcx/xskbcxZccx_cxZcByXnxq.html?gnmkdm=N2154`;
+    const requestBody = `xnm=${academicYear}&xqm=${semesterCode}`;
+
+    try {
+        const response = await fetch(url, {
+            method: "POST",
+            headers: {
+                "accept": "application/json, text/javascript, */*; q=0.01",
+                "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
+                "x-requested-with": "XMLHttpRequest"
+            },
+            body: requestBody,
+            credentials: "include"
+        });
+
+        if (!response.ok) return null;
+
+        const json = await response.json();
+        if (!Array.isArray(json) || json.length === 0) return null;
+
+        // 优先找第 1 周,否则取第一项
+        const firstWeekObj = json.find(item => String(item.zs) === "1" || String(item.zsmc) === "1") || json[0];
+
+        if (firstWeekObj.rq) {
+            const startDateStr = firstWeekObj.rq.split('/')[0];
+            if (/^\d{4}-\d{2}-\d{2}$/.test(startDateStr)) return startDateStr;
+        }
+        for (const field of ["zcrq", "ksrq"]) {
+            const matched = firstWeekObj[field] && String(firstWeekObj[field]).match(/(\d{4}-\d{2}-\d{2})/);
+            if (matched) return matched[1];
+        }
+    } catch (e) {
+        // 开学日期获取失败不影响导入
+    }
+
+    return null;
+}
+
+/** 请求并解析课程数据 */
+async function fetchAndParseCourses(academicYear, semesterCode) {
+    const requestBody = `xnm=${academicYear}&xqm=${semesterCode}&kzlx=ck&xsdm=&kclbdm=`;
+    const targetUrl = `${getBaseUrl()}/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N2151`;
+
+    try {
+        // 并行获取课程数据与开学日期
+        const [courseResponse, semesterStartDate] = await Promise.all([
+            fetch(targetUrl, {
+                method: "POST",
+                headers: {
+                    "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
+                },
+                body: requestBody,
+                credentials: "include"
+            }),
+            fetchSemesterStartDate(academicYear, semesterCode)
+        ]);
+
+        if (!courseResponse.ok) {
+            window.shiguangBridge.showToast(`获取课表失败(HTTP ${courseResponse.status}),请稍后重试。`);
+            return null;
+        }
+
+        const jsonText = await courseResponse.text();
+
+        // 会话失效时正方返回 200 + 登录页,单独提示,避免被误认为网络问题
+        if (/login_slogin|name=["']yhm["']/.test(jsonText)) {
+            window.shiguangBridge.showToast("登录状态已失效,请重新登录教务系统后再导入。");
+            return null;
+        }
+
+        const parsedCourses = parseJsonData(JSON.parse(jsonText));
+        if (parsedCourses.length === 0) {
+            window.shiguangBridge.showToast("该学期没有查询到课程,请确认所选学年学期是否正确。");
+            return null;
+        }
+
+        return {
+            courses: parsedCourses,
+            config: {
+                semesterStartDate: semesterStartDate,
+                semesterTotalWeeks: 20
+            }
+        };
+    } catch (e) {
+        window.shiguangBridge.showToast("获取课表失败,请检查网络环境或登录状态。");
+        return null;
+    }
+}
+
+async function saveCourses(parsedCourses) {
+    try {
+        await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(parsedCourses));
+        return true;
+    } catch (error) {
+        window.shiguangBridge.showToast(`课程保存失败:${error}`);
+        return false;
+    }
+}
+
+// 本校作息(学校作息表口径:每节 45 分钟 + 10 分钟课间)。
+// 本校按此口径导入作息(接口另有“每节 50 分钟、两节相连”的行政口径,经决定不采用)。
+const FALLBACK_TIME_SLOTS = [
+    { number: 1, startTime: "08:20", endTime: "09:05" },
+    { number: 2, startTime: "09:15", endTime: "10:00" },
+    { number: 3, startTime: "10:20", endTime: "11:05" },
+    { number: 4, startTime: "11:15", endTime: "12:00" },
+    { 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:40", endTime: "19:20" },
+    { number: 10, startTime: "19:20", endTime: "20:00" },
+    { number: 11, startTime: "20:20", endTime: "21:00" },
+    { number: 12, startTime: "21:00", endTime: "21:40" }
+];
+
+async function importPresetTimeSlots() {
+    try {
+        await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(FALLBACK_TIME_SLOTS));
+        window.shiguangBridge.showToast("时间段导入成功!");
+        return true;
+    } catch (error) {
+        window.shiguangBridge.showToast(`时间段导入失败:${error}`);
+        return false;
+    }
+}
+
+async function runImportFlow() {
+    const alertConfirmed = await promptUserToStart();
+    if (!alertConfirmed) {
+        window.shiguangBridge.showToast("用户取消了导入。");
+        return;
+    }
+
+    const selection = await selectAcademicYearAndSemester();
+    if (!selection) {
+        window.shiguangBridge.showToast("未选择学年学期,导入流程终止。");
+        return;
+    }
+
+    const { academicYear, semesterCode } = selection;
+
+    const result = await fetchAndParseCourses(academicYear, semesterCode);
+    if (result === null) return;
+
+    const { courses, config } = result;
+
+    // 保存顺序对齐 JSEI_01:课程 → 作息 → 课表配置
+    const saveResult = await saveCourses(courses);
+    if (!saveResult) return;
+
+    const timeSlotsSaved = await importPresetTimeSlots();
+
+    try {
+        await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
+        window.shiguangBridge.showToast(
+            config.semesterStartDate
+                ? `课表配置更新成功!开学日期:${config.semesterStartDate}`
+                : "课表配置更新成功!"
+        );
+    } catch (error) {
+        window.shiguangBridge.showToast(`课表配置保存失败:${error}`);
+    }
+
+    window.shiguangBridge.showToast(
+        timeSlotsSaved
+            ? `课程导入成功,共导入 ${courses.length} 门课程!`
+            : `课程已导入 ${courses.length} 门,但作息时间未写入,请在「作息时间」中手动设置。`
+    );
+    window.shiguangBridge.notifyTaskCompletion();
+}
+
+runImportFlow();

+ 31 - 11
resources/JSEI/jsei_01.js

@@ -403,20 +403,40 @@ async function fetchTimeSlots(academicYear, semesterCode) {
         if (!response.ok) return null;
 
         const json = await response.json();
-        if (!Array.isArray(json)) return null;
-
-        // 过滤有效节次并转换格式
-        const timeSlots = json
-            .filter(item => item.jcmc && item.qssj && item.jssj)
-            .map(item => ({
-                number: Number(item.jcmc),
-                startTime: item.qssj.substring(0, 5), // "08:30:00" -> "08:30"
-                endTime: item.jssj.substring(0, 5)
-            }))
-            .sort((a, b) => a.number - b.number);
 
+        if (!Array.isArray(json) || json.length === 0) {
+            return null;
+        }
+
+        const seen = {};
+        const timeSlots = [];
+
+        for (let i = 0; i < json.length; i++) {
+            const item = json[i];
+            if (!item || !item.jcmc || !item.qssj || !item.jssj) {
+                continue;
+            }
+
+            const num = Number(item.jcmc);
+            if (isNaN(num) || seen[num]) {
+                continue;
+            }
+            seen[num] = true;
+
+            timeSlots.push({
+                number: num,
+                startTime: String(item.qssj).substring(0, 5),
+                endTime: String(item.jssj).substring(0, 5)
+            });
+        }
+
+        Array.prototype.sort.call(timeSlots, (a, b) => a.number - b.number);
+
+        console.log("[作息] 解析到", timeSlots.length, "条:", timeSlots);
         return timeSlots.length > 0 ? timeSlots : null;
+
     } catch (e) {
+        console.error("[作息] 异常:", e);
         return null;
     }
 }

+ 1 - 1
resources/ZHKU/adapters.yaml

@@ -6,4 +6,4 @@ adapters:
     asset_js_path: "zhku_lc6464.js"
     import_url: "https://edu-admin.zhku.edu.cn/"
     maintainer: "lc6464"
-    description: "登录教务系统后在左侧找到“培养管理”->“我的课表”->“学期理论课表”,选择学期后点击右下角“执行导入”即可。\n学期理论课表默认为当前学期,若导入本学期课表则无需修改。\n目前仅支持导入“学期理论课表”内任意学期的课表,暂不支持导入其它类型的课表。"
+    description: "便捷导入流程:登录教务系统后点击右下角“执行导入”,在弹窗中选择学年学期即可。\n\n传统导入流程:登录教务系统后在左侧找到“培养管理”->“我的课表”->“学期理论课表”,选择学期后点击右下角“执行导入”即可。\n\n目前仅支持导入“学期理论课表”内任意学期的课表,暂不支持导入其它类型的课表。"

+ 100 - 24
resources/ZHKU/zhku_lc6464.js

@@ -266,13 +266,7 @@ function findNextFont(fonts, startIndex, predicate) {
 	return null;
 }
 
-// 教务页面会用 display:none 隐藏辅助节点,这里只保留可见信息
-function isVisibleFont(font) {
-	const styleText = (font.getAttribute("style") ?? "").replace(/\s+/g, "").toLowerCase();
-	return !styleText.includes("display:none");
-}
-
-// 从课表 iframe 中解析课程
+// 从课表文档中解析课程
 // 输出为扁平数组,不做同名课程合并
 function parseCoursesFromIframeDocument(iframeDoc) {
 	const courses = [];
@@ -294,9 +288,6 @@ function parseCoursesFromIframeDocument(iframeDoc) {
 			if (!title.includes("周次")) {
 				return;
 			}
-			if (!isVisibleFont(font)) {
-				return;
-			}
 
 			const weekText = cleanText(font.textContent);
 			if (weekText === "") {
@@ -306,7 +297,7 @@ function parseCoursesFromIframeDocument(iframeDoc) {
 			// 以“周次(节次)”行为锚点,向前找教师和课程名,向后找教室
 			const teacherFont = findPreviousFont(fonts, idx, (candidate) => {
 				const candidateTitle = cleanText(candidate.getAttribute("title") ?? "");
-				return candidateTitle.includes("教师") && isVisibleFont(candidate);
+				return candidateTitle.includes("教师");
 			});
 
 			const teacherIndex = teacherFont == null ? idx : fonts.indexOf(teacherFont);
@@ -317,14 +308,13 @@ function parseCoursesFromIframeDocument(iframeDoc) {
 				return (
 					candidateTitle === "" &&
 					candidateNameAttr === "" &&
-					isVisibleFont(candidate) &&
 					text !== ""
 				);
 			});
 
 			const locationFont = findNextFont(fonts, idx, (candidate) => {
 				const candidateTitle = cleanText(candidate.getAttribute("title") ?? "");
-				return candidateTitle.includes("教室") && isVisibleFont(candidate);
+				return candidateTitle.includes("教室");
 			});
 
 			const courseName = cleanText(nameFont?.textContent ?? "");
@@ -384,22 +374,79 @@ function parseCoursesFromIframeDocument(iframeDoc) {
 // 获取课表 iframe 的文档对象
 function getScheduleIframeDocument() {
 	const iframe = document.querySelector("iframe[src*='/jsxsd/xskb/xskb_list.do']");
-	if (iframe == null || iframe.contentDocument == null) {
-		throw new Error("未找到课表 iframe,或 iframe 内容尚未加载完成");
+	if (iframe == null) {
+		return null;
+	}
+	if (iframe.contentDocument == null) {
+		throw new Error("课表 iframe 内容尚未加载完成");
 	}
 	return iframe.contentDocument;
 }
 
-// 获取当前学年学期 ID,例如 2025-2026-2
-function getSemesterId(iframeDoc) {
+// 获取列表中的全部以及当前选择的学年学期 ID
+function getSemesterIds(iframeDoc) {
 	const select = iframeDoc.querySelector("#xnxq01id");
 	if (select == null) {
-		throw new Error("未找到学年学期选择框 #xnxq01id");
+		throw new Error("未找到学年学期选择框 #xnxq01id,请确认已登录教务系统");
 	}
 
-	// 优先读取 option[selected],读取失败再回退到 select.value
-	const selectedOption = select.querySelector("option[selected]");
-	return cleanText(selectedOption?.value ?? select.value);
+	const options = Array.from(select.querySelectorAll("option"))
+		.map((opt) => cleanText(opt.value))
+		.filter((value) => value !== "");
+	const selectedOption = cleanText(select.value);
+
+	if (selectedOption === "") {
+		throw new Error("未找到当前激活的学年学期,请确认已登录教务系统");
+	}
+
+	return { options, selectedOption };
+}
+
+// 拉取学期理论课表并解析为文档对象
+// 未指定学年学期时,教务系统返回当前激活学期的课表
+async function fetchScheduleDocument(semesterId = "") {
+	const query = semesterId === "" ? "" : `?xnxq01id=${encodeURIComponent(semesterId)}`;
+	const url = `https://edu-admin.zhku.edu.cn/jsxsd/xskb/xskb_list.do${query}`;
+	const response = await fetch(url, {
+		method: "GET",
+		credentials: "include"
+	});
+	if (!response.ok) {
+		throw new Error(`获取学期理论课表失败:HTTP ${response.status}`);
+	}
+
+	const html = await response.text();
+	const parser = new DOMParser();
+	const doc = parser.parseFromString(html, "text/html");
+	return doc;
+}
+
+// 获取学年学期列表并让用户选择,默认选中当前激活学期
+async function chooseScheduleDocument() {
+	const cachedDoc = await fetchScheduleDocument();
+	const { options, selectedOption } = getSemesterIds(cachedDoc);
+	const selection = await window.shiguangBridgePromise.showSingleSelection(
+		"选择导入的学年学期",
+		JSON.stringify(options),
+		options.indexOf(selectedOption)
+	);
+	if (selection == null) {
+		throw new Error("已取消选择学年学期");
+	}
+
+	const selectedIndex = Number(selection);
+	if (!Number.isInteger(selectedIndex) || selectedIndex < 0 || selectedIndex >= options.length) {
+		throw new Error("未选择有效的学年学期");
+	}
+
+	const semesterId = options[selectedIndex];
+
+	// 激活学期直接复用首次获取的课表,其它学期再按 ID 拉取
+	let scheduleDoc = semesterId === selectedOption
+		? cachedDoc
+		: await fetchScheduleDocument(semesterId);
+
+	return { scheduleDoc, semesterId };
 }
 
 // 拉取教学周历并提取开学日期与总周数
@@ -441,16 +488,40 @@ async function fetchSemesterCalendarInfo(semesterId) {
 	};
 }
 
+// 单独区分未登录错误,入口处使用弹窗提示
+class LoginRequiredError extends Error {}
+
+// 通过登录表单识别登录页面
+function checkLoginDocument(doc) {
+	const loginForm = doc.querySelector('#loginForm[action="/Logon.do?method=logon"]');
+	if (loginForm != null) {
+		throw new LoginRequiredError("请先登录教务系统,再点击“执行导入”。");
+	}
+}
+
 // 主流程:读取课表 -> 选择校区 -> 拉周历 -> 生成节次 -> 调桥接导入
+// 传统流程:从页面中读取用户打开的课表 iframe
+// 便捷流程:若未打开课表,则让用户选择学年学期,直接拉取课表
 async function importSchedule() {
+	// 未登录时直接提示,不继续读取课表
+	checkLoginDocument(document);
+
 	window.shiguangBridge.showToast("开始读取教务课表……");
 
-	// 读取 iframe 并获取当前学年学期 ID
+	// 读取 iframe,未打开课表时则走便捷导入流程
 	const iframeDoc = getScheduleIframeDocument();
-	const semesterId = getSemesterId(iframeDoc);
+	let scheduleDoc = iframeDoc;
+	let semesterId;
+	if (iframeDoc == null) {
+		const selectedSchedule = await chooseScheduleDocument();
+		scheduleDoc = selectedSchedule.scheduleDoc;
+		semesterId = selectedSchedule.semesterId;
+	} else {
+		semesterId = getSemesterIds(iframeDoc).selectedOption;
+	}
 
 	// 解析课程信息
-	const courses = parseCoursesFromIframeDocument(iframeDoc);
+	const courses = parseCoursesFromIframeDocument(scheduleDoc);
 	if (courses.length === 0) {
 		throw new Error("未解析到任何课程,请确认当前课表页面已加载完成");
 	}
@@ -487,6 +558,11 @@ async function importSchedule() {
 	try {
 		await importSchedule();
 	} catch (error) {
+		if (error instanceof LoginRequiredError) {
+			// 弹窗强提醒,同时也在控制台输出并展示 Toast
+			await window.shiguangBridgePromise.showAlert("未登录教务系统", error.message, "好的");
+		}
+
 		console.error("课表导入失败:", error);
 		// 失败原因直接提示给用户,便于在移动端快速定位问题
 		window.shiguangBridge.showToast(`导入失败:${error.message}`);

+ 9 - 0
resources/ZWU/adapters.yaml

@@ -0,0 +1,9 @@
+# resources/ZWU/adapters.yaml
+adapters:
+  - adapter_id: "ZWU_01"
+    adapter_name: "浙江万里学院正方教务"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "zwu_01.js"
+    import_url: "https://ehall.zwu.edu.cn/_layouts/15/ZWUWSBS/BSDT/HomePage.aspx#"
+    maintainer: "星河欲转"
+    description: "浙江万里学院正方教务,校内访问和vpn访问都可以,进入教务系统即可开始导入,支持获取开学日期,已添加令时作息"

+ 618 - 0
resources/ZWU/zwu_01.js

@@ -0,0 +1,618 @@
+// 浙江万里学院(zwu.edu.cn) 拾光课程表适配脚本
+// 基于正方教务系统接口适配
+// 非该大学开发者适配,开发者无法及时发现问题
+// 出现问题请提issues或者提交pr更改,这更加快速
+
+/**
+ * 基础域名配置
+ * 浙江万里学院仅支持校园网内网直连
+ */
+const BASE_URL = "https://jw.zwu.edu.cn/jwglxt";
+
+/**
+ * 获取基础地址
+ */
+function getBaseUrl() {
+    return BASE_URL;
+}
+
+/**
+ * 节次与周次合并去重函数
+ * @param {Array<Object>} courses 原始解析课程数组
+ * @returns {Array<Object>} 合并去重后的课程数组
+ */
+function mergeAndDistinctCourses(courses) {
+    if (!Array.isArray(courses) || courses.length <= 1) return courses;
+
+    // 1. 深拷贝并规范周次数据,过滤无效项
+    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) : []
+    }));
+
+    // 阶段 1:合并连续节次与完全重复记录(前提:名称、教师、地点、星期、周次一致)
+    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) {
+            // 节次连续:延长结束节次 (如 1-2 节 + 3-4 节 -> 1-4 节)
+            current.endSection = next.endSection;
+        } else if (isSameCourseAndWeeks && isDuplicate) {
+            // 完全重复:跳过
+            continue;
+        } else {
+            step1Merged.push(current);
+            current = next;
+        }
+    }
+    step1Merged.push(current);
+
+    // 阶段 2:合并同节次的周次(前提:名称、教师、地点、星期、开始/结束节次一致)
+    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) {
+            // 周次合并去重 (如 1-8 周 + 9-16 周 -> 1-16 周)
+            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;
+}
+
+/**
+ * 解析周次字符串,处理单双周和周次范围
+ */
+function parseWeeks(weekStr) {
+    if (!weekStr) return [];
+
+    const cleaned = weekStr
+        .replace(/周数[::]/g, '')
+        .replace(/第/g, '')
+        .replace(/周/g, '')
+        .replace(/共\s*\d+\s*.*$/g, '');
+
+    const segments = cleaned.split(/[,,、;;]/);
+
+    const weeks = [];
+    const segRegex = /(\d+)(?:\s*[-~]\s*(\d+))?\s*(?:[((]?\s*([单双])\s*周?\s*[))]?)?/g;
+
+    for (const seg of segments) {
+        const s = seg.trim();
+        if (!s) continue;
+
+        segRegex.lastIndex = 0;
+        let m;
+        while ((m = segRegex.exec(s)) !== null) {
+            if (m[0] === '') {
+                segRegex.lastIndex++;
+                continue;
+            }
+
+            const start = parseInt(m[1], 10);
+            const end = m[2] ? parseInt(m[2], 10) : start;
+            const flagStr = m[3] || '';
+
+            let flag = 0;
+            if (flagStr.includes('单')) flag = 1;
+            else if (flagStr.includes('双')) flag = 2;
+
+            for (let w = start; w <= end; w++) {
+                if (flag === 1 && w % 2 === 0) continue;
+                if (flag === 2 && w % 2 !== 0) continue;
+                weeks.push(w);
+            }
+        }
+    }
+
+    return [...new Set(weeks)].sort((a, b) => a - b);
+}
+
+/**
+ * 解析 API 返回的 JSON 数据
+ */
+function parseJsonData(jsonData) {
+    if (!jsonData || !Array.isArray(jsonData.kbList)) {
+        return [];
+    }
+
+    const rawCourseList = jsonData.kbList;
+    const initialCourseList = [];
+
+    for (const rawCourse of rawCourseList) {
+        if (!rawCourse.kcmc || !rawCourse.xm || !rawCourse.cdmc || 
+            !rawCourse.xqj || !rawCourse.jcs || !rawCourse.zcd) {
+            continue;
+        }
+
+        const weeksArray = parseWeeks(rawCourse.zcd);
+        if (weeksArray.length === 0) {
+            continue;
+        }
+        
+        const sectionParts = rawCourse.jcs.split('-');
+        const startSection = Number(sectionParts[0]);
+        const endSection = Number(sectionParts[sectionParts.length - 1]);
+        const day = Number(rawCourse.xqj);
+        
+        if (isNaN(day) || isNaN(startSection) || isNaN(endSection) || 
+            day < 1 || day > 7 || startSection > endSection) {
+            continue;
+        }
+
+        initialCourseList.push({
+            name: rawCourse.kcmc.trim(),
+            teacher: rawCourse.xm.trim(),
+            position: rawCourse.cdmc.trim(),
+            day: day,
+            startSection: startSection,
+            endSection: endSection,
+            weeks: weeksArray
+        });
+    }
+
+    return mergeAndDistinctCourses(initialCourseList);
+}
+
+async function promptUserToStart() {
+    return await window.shiguangBridgePromise.showAlert(
+        "教务系统课表导入",
+        "导入前请确保您已在浏览器中成功登录教务系统",
+        "好的,开始导入"
+    );
+}
+
+/**
+ * 从教务系统获取学年学期选项
+ * 学年:以选中项为中心,取前2年+后2年,共5个选项
+ */
+async function fetchAcademicOptions() {
+    const url = `${getBaseUrl()}/kbcx/xskbcx_cxXskbcxIndex.html?gnmkdm=N2151&layout=default`;
+    
+    try {
+        const response = await fetch(url, {
+            method: "GET",
+            credentials: "include"
+        });
+
+        if (!response.ok) return null;
+
+        const htmlText = await response.text();
+        const parser = new DOMParser();
+        const doc = parser.parseFromString(htmlText, "text/html");
+
+        const allYearOptions = Array.from(doc.querySelectorAll("#xnm option"))
+            .filter(opt => opt.value !== "")
+            .map(opt => ({
+                value: opt.value,
+                text: opt.textContent.trim(),
+                selected: opt.selected
+            }));
+
+        const semesterOptions = Array.from(doc.querySelectorAll("#xqm option"))
+            .filter(opt => opt.value !== "")
+            .map(opt => ({
+                value: opt.value,
+                text: opt.textContent.trim(),
+                selected: opt.selected
+            }));
+
+        if (allYearOptions.length === 0 || semesterOptions.length === 0) {
+            return null;
+        }
+
+        const selectedIndex = allYearOptions.findIndex(opt => opt.selected);
+        
+        if (selectedIndex === -1) {
+            return {
+                yearOptions: allYearOptions.slice(0, 5),
+                semesterOptions,
+                defaultYearIndex: 0,
+                defaultSemesterIndex: semesterOptions.findIndex(opt => opt.selected) !== -1 
+                    ? semesterOptions.findIndex(opt => opt.selected) 
+                    : 0
+            };
+        }
+
+        const start = Math.max(0, selectedIndex - 2);
+        const end = Math.min(allYearOptions.length, selectedIndex + 3);
+        const yearOptions = allYearOptions.slice(start, end);
+        const newDefaultIndex = selectedIndex - start;
+        const defaultSemesterIndex = semesterOptions.findIndex(opt => opt.selected);
+
+        return {
+            yearOptions,
+            semesterOptions,
+            defaultYearIndex: newDefaultIndex,
+            defaultSemesterIndex: defaultSemesterIndex !== -1 ? defaultSemesterIndex : 0
+        };
+
+    } catch (e) {
+        return null;
+    }
+}
+
+/**
+ * 提示用户选择学年和学期
+ */
+async function selectAcademicYearAndSemester() {
+    const optionsData = await fetchAcademicOptions();
+
+    if (!optionsData) {
+        window.shiguangBridge.showToast("从教务系统读取学年学期失败,请确保登录状态。");
+        return null;
+    }
+
+    const { yearOptions, semesterOptions, defaultYearIndex, defaultSemesterIndex } = optionsData;
+
+    const yearTexts = yearOptions.map(item => item.text);
+    const yearIndex = await window.shiguangBridgePromise.showSingleSelection(
+        "选择学年",
+        JSON.stringify(yearTexts),
+        defaultYearIndex
+    );
+
+    if (yearIndex === null || yearIndex === -1) return null;
+    const selectedYearCode = yearOptions[yearIndex].value;
+
+    const semesterTexts = semesterOptions.map(item => item.text);
+    const semesterIndex = await window.shiguangBridgePromise.showSingleSelection(
+        "选择学期",
+        JSON.stringify(semesterTexts),
+        defaultSemesterIndex
+    );
+
+    if (semesterIndex === null || semesterIndex === -1) return null;
+    const selectedSemesterCode = semesterOptions[semesterIndex].value;
+
+    return {
+        academicYear: selectedYearCode,
+        semesterCode: selectedSemesterCode
+    };
+}
+
+/**
+ * 获取学期开学日期
+ */
+async function fetchSemesterStartDate(academicYear, semesterCode) {
+    const url = `${getBaseUrl()}/kbcx/xskbcxZccx_cxZcByXnxq.html?gnmkdm=N2154`;
+    const requestBody = `xnm=${academicYear}&xqm=${semesterCode}`;
+
+    try {
+        const response = await fetch(url, {
+            method: "POST",
+            headers: {
+                "accept": "application/json, text/javascript, */*; q=0.01",
+                "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
+                "x-requested-with": "XMLHttpRequest"
+            },
+            body: requestBody,
+            credentials: "include"
+        });
+
+        if (response.ok) {
+            const json = await response.json();
+            if (Array.isArray(json) && json.length > 0) {
+                // 优先找第1周,否则取第一项
+                const firstWeekObj = json.find(item => String(item.zs) === "1" || String(item.zsmc) === "1") || json[0];
+                
+                if (firstWeekObj.rq) {
+                    const startDateStr = firstWeekObj.rq.split('/')[0];
+                    if (/^\d{4}-\d{2}-\d{2}$/.test(startDateStr)) {
+                        return startDateStr;
+                    }
+                }
+                if (firstWeekObj.zcrq) {
+                    const match = firstWeekObj.zcrq.match(/(\d{4}-\d{2}-\d{2})/);
+                    if (match) return match[1];
+                }
+                // 某些系统用 ksrq 字段
+                if (firstWeekObj.ksrq) {
+                    const match = firstWeekObj.ksrq.match(/(\d{4}-\d{2}-\d{2})/);
+                    if (match) return match[1];
+                }
+            }
+        }
+    } catch (e) {
+        // 获取失败不影响主流程
+    }
+    return null;
+}
+
+/**
+ * 请求和解析课程数据
+ * 不再请求教务作息接口,作息时间由内置的冬令时/夏令时方案提供
+ */
+async function fetchAndParseCourses(academicYear, semesterCode) {
+    const requestBody = `xnm=${academicYear}&xqm=${semesterCode}&kzlx=ck&xsdm=&kclbdm=`;
+    const targetUrl = `${getBaseUrl()}/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N2151`;
+
+    const [courseResponse, semesterStartDate] = await Promise.all([
+        fetch(targetUrl, {
+            method: "POST",
+            headers: {
+                "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
+            },
+            body: requestBody,
+            credentials: "include"
+        }),
+        fetchSemesterStartDate(academicYear, semesterCode)
+    ]);
+
+    try {
+        if (courseResponse.ok) {
+            const jsonText = await courseResponse.text();
+            const jsonData = JSON.parse(jsonText);
+            if (jsonData && jsonData.kbList) {
+                const parsedCourses = parseJsonData(jsonData);
+                if (parsedCourses.length > 0) {
+                    return {
+                        courses: parsedCourses,
+                        config: {
+                            semesterStartDate: semesterStartDate,
+                            semesterTotalWeeks: 20
+                        }
+                    };
+                }
+            }
+        }
+    } catch (e) {
+        // 请求失败
+    }
+
+    window.shiguangBridge.showToast("未能获取课表数据,请检查网络环境或登录状态。");
+    return null;
+}
+
+async function saveCourses(parsedCourses) {
+    try {
+        await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(parsedCourses));
+        return true;
+    } catch (error) {
+        window.shiguangBridge.showToast(`课程保存失败: ${error.message}`);
+        return false;
+    }
+}
+
+/**
+ * 浙江万里学院作息时间表
+ * 冬令时(10月-次年4月)
+ * 夏令时(5月-9月)
+ * 1-5 节两个令时一致,6-14 节不同
+ */
+
+// 冬令时(完整 14 节)
+const WINTER_TIME_SLOTS = [
+    { number: 1,  startTime: "08:00", endTime: "08:45" },
+    { number: 2,  startTime: "08:50", endTime: "09:35" },
+    { number: 3,  startTime: "09:50", endTime: "10:35" },
+    { number: 4,  startTime: "10:40", endTime: "11:25" },
+    { number: 5,  startTime: "11:30", endTime: "12:15" },
+    { number: 6,  startTime: "13:30", endTime: "14:15" },
+    { number: 7,  startTime: "14:20", endTime: "15:05" },
+    { number: 8,  startTime: "15:20", endTime: "16:05" },
+    { number: 9,  startTime: "16:10", endTime: "16:55" },
+    { number: 10, startTime: "18:00", endTime: "18:45" },
+    { number: 11, startTime: "18:50", endTime: "19:35" },
+    { number: 12, startTime: "19:40", endTime: "20:25" },
+    { number: 13, startTime: "20:30", endTime: "21:15" },
+    { number: 14, startTime: "21:20", endTime: "22:05" }
+];
+
+// 夏令时(完整 14 节)
+const SUMMER_TIME_SLOTS = [
+    { number: 1,  startTime: "08:00", endTime: "08:45" },
+    { number: 2,  startTime: "08:50", endTime: "09:35" },
+    { number: 3,  startTime: "09:50", endTime: "10:35" },
+    { number: 4,  startTime: "10:40", endTime: "11:25" },
+    { number: 5,  startTime: "11:30", endTime: "12:15" },
+    { number: 6,  startTime: "14:00", endTime: "14:45" },
+    { number: 7,  startTime: "14:50", endTime: "15:35" },
+    { number: 8,  startTime: "15:50", endTime: "16:35" },
+    { number: 9,  startTime: "16:40", endTime: "17:25" },
+    { number: 10, startTime: "18:30", endTime: "19:15" },
+    { number: 11, startTime: "19:20", endTime: "20:05" },
+    { number: 12, startTime: "20:10", endTime: "20:55" },
+    { number: 13, startTime: "21:00", endTime: "21:45" },
+    { number: 14, startTime: "21:50", endTime: "22:35" }
+];
+
+/**
+ * 生成组合作息方案
+ * - 名称用学年参数拼成 "2026-2027 学年作息方案"
+ * - 基础骨架用当前月份对应的令时(全量 14 节)
+ * - combo 里两个令时都提交完整 14 节
+ * - 日期区间根据脚本运行时的当前月份动态生成
+ *   - 夏令时:每年 5-9 月
+ *   - 冬令时:每年 10 月-次年 4 月
+ */
+function buildComboSchedule(academicYear) {
+    const now = new Date();
+    const year = now.getFullYear();
+    const month = now.getMonth() + 1; // 1-12
+
+    let summerStart, summerEnd, winterStart, winterEnd;
+    let isSummerNow;
+
+    if (month >= 5 && month <= 9) {
+        // 当前是夏令时
+        isSummerNow = true;
+        summerStart = `${year}-05-01`;
+        summerEnd = `${year}-09-30`;
+        winterStart = `${year}-10-01`;
+        winterEnd = `${year + 1}-04-30`;
+    } else if (month >= 10) {
+        // 当前是冬令时(10-12月)
+        isSummerNow = false;
+        winterStart = `${year}-10-01`;
+        winterEnd = `${year + 1}-04-30`;
+        summerStart = `${year + 1}-05-01`;
+        summerEnd = `${year + 1}-09-30`;
+    } else {
+        // 当前是冬令时(1-4月)
+        isSummerNow = false;
+        winterStart = `${year - 1}-10-01`;
+        winterEnd = `${year}-04-30`;
+        summerStart = `${year}-05-01`;
+        summerEnd = `${year}-09-30`;
+    }
+
+    // 名称用学年,academicYear 是 "2026" 这种
+    const yearLabel = academicYear
+        ? `${academicYear}-${Number(academicYear) + 1}`
+        : `${year}`;
+
+    // 当前令时作为骨架(全量),另一个令时作为覆盖(也全量)
+    const skeletonSlots = isSummerNow ? SUMMER_TIME_SLOTS : WINTER_TIME_SLOTS;
+
+    return {
+        skeletonSlots,
+        combo: {
+            name: `${yearLabel} 学年作息方案`,
+            publicSchedules: [
+                {
+                    name: "夏令时",
+                    startDate: summerStart,
+                    endDate: summerEnd,
+                    timeSlots: SUMMER_TIME_SLOTS
+                },
+                {
+                    name: "冬令时",
+                    startDate: winterStart,
+                    endDate: winterEnd,
+                    timeSlots: WINTER_TIME_SLOTS
+                }
+            ]
+        }
+    };
+}
+
+/**
+ * 导入基础作息(当前月份对应的令时,全量 14 节)作为骨架
+ */
+async function importBaseTimeSlots(skeletonSlots) {
+    try {
+        await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(skeletonSlots));
+        return true;
+    } catch (error) {
+        window.shiguangBridge.showToast("导入基础作息失败: " + error.message);
+        return false;
+    }
+}
+
+/**
+ * 导入组合作息方案
+ * 必须在 importBaseTimeSlots 成功之后调用
+ */
+async function importComboSchedule(combo) {
+    try {
+        await window.shiguangBridgePromise.saveComboSchedule(JSON.stringify(combo));
+        window.shiguangBridge.showToast("夏令时/冬令时作息方案导入成功!");
+        return true;
+    } catch (error) {
+        window.shiguangBridge.showToast("导入组合作息失败: " + error.message);
+        return false;
+    }
+}
+
+async function runImportFlow() {
+    const alertConfirmed = await promptUserToStart();
+    if (!alertConfirmed) {
+        window.shiguangBridge.showToast("用户取消了导入。");
+        return;
+    }
+
+    const selection = await selectAcademicYearAndSemester();
+    if (!selection) {
+        window.shiguangBridge.showToast("未选择学年学期,导入流程终止。");
+        return;
+    }
+
+    const { academicYear, semesterCode } = selection;
+
+    const result = await fetchAndParseCourses(academicYear, semesterCode);
+    if (result === null) {
+        return;
+    }
+
+    const { courses, config } = result;
+
+    const saveResult = await saveCourses(courses);
+    if (!saveResult) {
+        return;
+    }
+    
+    try {
+        await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
+        let configMsg = "课表配置更新成功!";
+        if (config.semesterStartDate) {
+            configMsg += ` 开学日期:${config.semesterStartDate}`;
+        }
+        window.shiguangBridge.showToast(configMsg);
+    } catch (error) {
+        window.shiguangBridge.showToast(`课表配置保存失败: ${error.message}`);
+    }
+
+    // 先导入基础作息(当前月份对应的令时),再导入组合作息方案
+    const { skeletonSlots, combo } = buildComboSchedule(academicYear);
+    const baseSaved = await importBaseTimeSlots(skeletonSlots);
+    if (!baseSaved) {
+        window.shiguangBridge.showToast("基础作息导入失败,跳过组合作息方案。");
+    } else {
+        await importComboSchedule(combo);
+    }
+
+    window.shiguangBridge.showToast(`课程导入成功,共导入 ${courses.length} 门课程!`);
+    window.shiguangBridge.notifyTaskCompletion();
+}
+
+runImportFlow();