厦门大学课程表导出助手-ICS(功能修复版)

基于 yangqian 的原版脚本进行修复和功能增强。解决了因教务系统更新导致的失效问题,支持任意节数连上课程, 支持日程提醒配置。

// ==UserScript==
// @name         厦门大学课程表导出助手-ICS(功能修复版)
// @version      2025-09-15.3
// @description  基于 yangqian 的原版脚本进行修复和功能增强。解决了因教务系统更新导致的失效问题,支持任意节数连上课程, 支持日程提醒配置。
// @author       jader (原作者 pydroid)
// @match        https://jw.xmu.edu.cn/gsapp/sys/wdkbapp/*
// @grant        none
// @license      MIT License
// @namespace    https://gf.qytechs.cn/zh-CN/users/1515284-%E4%BA%A6%E7%91%BE
// ==/UserScript==

(function() {
    'use strict';

    // ================== 配置区域 ==================
    const DEBUG = true;
    const START_DATE = '2025-09-08'; // !!!【重要】请再次确认此日期为开学第一周的周一!!!

    // 【新功能】 在此设置课前提醒,单位为分钟。可以设置多个,例如 [15, 5]
    // 如果不需要提醒,请设置为空数组 []
    const REMINDER_MINUTES = [10];
    // ============================================

    const periodTimes = [
        { jc: 1,  start: [8, 0],  end: [8, 45]  }, { jc: 2,  start: [8, 55], end: [9, 40]  },
        { jc: 3,  start: [10, 10],end: [10, 55] }, { jc: 4,  start: [11, 5], end: [11, 50] },
        { jc: 5,  start: [14, 30],end: [15, 15] }, { jc: 6,  start: [15, 25],end: [16, 10] },
        { jc: 7,  start: [16, 40],end: [17, 25] }, { jc: 8,  start: [17, 35],end: [18, 20] },
        { jc: 9,  start: [19, 10],end: [19, 55] }, { jc: 10, start: [20, 5], end: [20, 50] },
        { jc: 11, start: [21, 0], end: [21, 45] }
    ];

    const log = (message, ...args) => {
        if (DEBUG) console.log(`[课表助手] ${message}`, ...args);
    };

    const waitUntilElementPresent = (cssLocator, callback) => {
        log(`脚本启动,正在等待课表元素 '${cssLocator}' 加载...`);
        const checkExist = setInterval(() => {
            if (document.querySelector(cssLocator)) {
                clearInterval(checkExist);
                log(`课表元素已找到,准备添加 '导出ICS' 按钮。`);
                callback();
                return;
            }
        }, 100);
    };

    waitUntilElementPresent(".arrage", () => {
        const tab = document.getElementById("xsXx")?.firstElementChild;
        if (!tab) { console.error("[课表助手] 无法找到按钮的挂载点 '#xsXx'。"); return; }
        if (tab.querySelector('.export-ics-btn')) return;
        const getButton = document.createElement('a');
        getButton.innerHTML = "导出ICS";
        getButton.classList.add("bh-btn-default", "bh-btn", "export-ics-btn");
        tab.appendChild(getButton);
        getButton.addEventListener('click', main);
        log("'导出ICS' 按钮已成功添加到页面。");
    });

    function main() {
        log("===== 开始执行课表导出主程序 =====");
        const HEADERS = [
            "BEGIN:VCALENDAR", "METHOD:PUBLISH", "VERSION:2.0",
            "X-WR-CALNAME:XMU课程表", "X-WR-TIMEZONE:Asia/Shanghai", "CALSCALE:GREGORIAN",
            "BEGIN:VTIMEZONE", "TZID:Asia/Shanghai", "END:VTIMEZONE"
        ];
        const FOOTERS = ["END:VCALENDAR"];

        const formatTime = (timeArray) => `${timeArray[0].toString().padStart(2, '0')}${timeArray[1].toString().padStart(2, '0')}00`;
        const getStartTime = (startJc) => periodTimes[startJc - 1]?.start ? formatTime(periodTimes[startJc - 1].start) : null;
        const getEndTime = (startJc, duration) => {
            const endJc = startJc + duration - 1;
            return periodTimes[endJc - 1]?.end ? formatTime(periodTimes[endJc - 1].end) : null;
        };
        const getFirstDate = (day, week) => {
            const startDate = new Date(START_DATE);
            startDate.setDate(startDate.getDate() + (week - 1) * 7 + (day - 1));
            return `${startDate.getFullYear()}${(startDate.getMonth() + 1).toString().padStart(2, '0')}${startDate.getDate().toString().padStart(2, '0')}`;
        };

        const courseBlocks = document.querySelectorAll(".arrage");
        log(`在页面上找到了 ${courseBlocks.length} 个 <div class="arrage"> 课程块,现在开始筛选并处理...`);
        const classes = [];

        for (const block of courseBlocks) {
            const td = block.closest('td');
            if (!td || td.style.display === 'none') {
                continue;
            }

            const jcVal = td.getAttribute("jc");
            const xqVal = td.getAttribute("xq");
            const rowspanVal = td.getAttribute("rowspan") || "1";

            if (DEBUG) console.groupCollapsed(`[处理课程] 星期: ${xqVal}, 起始节次: ${jcVal}, 跨度: ${rowspanVal}节`);

            try {
                const courseInfoDivs = block.querySelectorAll('div');
                if (courseInfoDivs.length < 4) { log("信息不全,跳过。"); continue; }

                const weeksText = courseInfoDivs[0]?.textContent.trim();
                const summary = courseInfoDivs[1]?.textContent.replace(/\(.*?\)/g, '').trim();
                const description = courseInfoDivs[2]?.textContent.trim();
                const location = courseInfoDivs[3]?.textContent.replace(/(.*?)/g, '').trim();

                if (!weeksText || !summary) { log("缺少周次或课程名,跳过。"); continue; }

                const weeksMatch = weeksText.match(/(\d+)-(\d+)(单|双)?周/);
                if (!weeksMatch) { log("周次格式不匹配,跳过。"); continue; }

                const [, startWeekStr, endWeekStr, weekType] = weeksMatch;
                const startWeek = parseInt(startWeekStr, 10), endWeek = parseInt(endWeekStr, 10);

                let weekCount, interval;
                if (weekType === "单" || weekType === "双") {
                    let count = 0; const isOdd = weekType === "单";
                    for (let i = startWeek; i <= endWeek; i++) if ((isOdd && i % 2 !== 0) || (!isOdd && i % 2 === 0)) count++;
                    weekCount = count; interval = ";INTERVAL=2";
                } else {
                    weekCount = endWeek - startWeek + 1; interval = "";
                }
                if(weekCount <= 0) { log("计算出的上课周数为0,跳过。"); continue; }

                const jc = parseInt(jcVal, 10), day = parseInt(xqVal, 10), duration = parseInt(rowspanVal, 10);
                let firstWeek = startWeek;
                if ((weekType === '单' && startWeek % 2 === 0) || (weekType === '双' && startWeek % 2 !== 0)) {
                    firstWeek = startWeek + 1;
                }

                const date = getFirstDate(day, firstWeek);
                const start = getStartTime(jc);
                const end = getEndTime(jc, duration);
                if (!start || !end) { log(`无法计算时间(jc:${jc}, dur:${duration}),跳过。`); continue; }

                // --- 生成提醒组件 ---
                const alarms = REMINDER_MINUTES.map(minute => {
                    return [
                        "BEGIN:VALARM",
                        "ACTION:DISPLAY",
                        `DESCRIPTION:${summary} 即将开始`,
                        `TRIGGER:-PT${minute}M`, // PT = Period of Time, M = Minutes
                        "END:VALARM"
                    ].join('\n');
                }).join('\n');
                if (alarms) log(`已为本课程生成 ${REMINDER_MINUTES.length} 个提醒。`);
                // --- 提醒组件结束 ---

                const eventParts = [
                    "BEGIN:VEVENT",
                    `SUMMARY:${summary}`,
                    `DESCRIPTION:${description}`,
                    `DTSTART;TZID=Asia/Shanghai:${date}T${start}`,
                    `DTEND;TZID=Asia/Shanghai:${date}T${end}`,
                    `LOCATION:${location}`,
                    `RRULE:FREQ=WEEKLY;COUNT=${weekCount}${interval}`
                ];

                if(alarms) {
                    eventParts.push(alarms);
                }

                eventParts.push("END:VEVENT");
                
                classes.push(eventParts.join('\n'));
                log("成功生成 VEVENT 事件。");

            } catch (error) {
                console.error(`[课表助手] 处理课程块时发生错误:`, error, block);
            } finally {
                if (DEBUG) console.groupEnd();
            }
        }

        log(`===== 处理完成,共生成了 ${classes.length} 个课程事件 =====`);

        if (classes.length === 0) {
            alert("未找到任何有效的课程信息!请按 F12 查看控制台中的日志以排查问题。");
            return;
        }

        const textContent = [...HEADERS, ...classes, ...FOOTERS].join('\n');
        const blob = new Blob([textContent], { type: 'text/calendar;charset=utf-8' });
        const url = URL.createObjectURL(blob);

        const a = document.createElement('a');
        a.href = url; a.download = `schedule_${new Date().toISOString().slice(0, 10)}.ics`;
        document.body.appendChild(a); a.click();
        URL.revokeObjectURL(url); document.body.removeChild(a);
        log("ICS文件已生成并触发下载。");
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址