您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
拦截 currenttt.js 与 maindbi.js 请求,每次拦截后弹窗询问用户是否下载,并提供生成课表的功能
// ==UserScript== // @name Edupage 文件拦截并下载 + 课表生成器 // @namespace http://tampermonkey.net/ // @version 1.5 // @description 拦截 currenttt.js 与 maindbi.js 请求,每次拦截后弹窗询问用户是否下载,并提供生成课表的功能 // @author schweigen // @license GPL-3.0 // @match https://freshman.edupage.org/* // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // ==/UserScript== (function () { "use strict"; // 用于避免重复下载(如果你希望每次拦截都询问,可以移除此重复检查) const downloadedFiles = {}; // 初始化拦截模式状态,默认为关闭 let interceptEnabled = GM_getValue('interceptEnabled', false); // 生成课表的 HTML(已经更新为你给出的最新版本) const timetableHTML = `<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <title>课表生成器</title> <style> /* 引入苹果风格的字体 */ @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap'); body { font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; background-color: #f9f9f9; margin: 0; padding: 0; color: #333; } h1 { text-align: center; color: #00C8FF; /* 蓝色标题 */ margin-top: 30px; font-weight: 600; font-size: 2.5em; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1); } .container { max-width: 800px; margin: 0 auto; padding: 20px; background-color: #fff; border-radius: 20px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); margin-top: 30px; } /* 调整后的textarea样式 */ .code-block { position: relative; background-color: #f0f8ff; border-radius: 15px; padding: 20px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); margin-bottom: 30px; overflow-x: auto; } .code-block textarea { width: 100%; height: 200px; /* 将高度调整为200px */ border: none; background: transparent; resize: vertical; font-size: 14px; line-height: 1.5; font-family: 'SF Mono', 'Roboto Mono', 'Courier New', monospace; color: #555; padding: 0; margin: 0; outline: none; } button { display: block; width: 100%; padding: 15px; background-color: #00C8FF; /* 蓝色按钮 */ color: #fff; border: none; border-radius: 15px; font-size: 18px; font-weight: 600; cursor: pointer; transition: background-color 0.3s ease, transform 0.3s ease; margin-bottom: 30px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); position: relative; overflow: hidden; } button:hover { background-color: #00AEEF; /* 悬停时颜色变浅 */ transform: translateY(-2px); } .button-text { position: relative; z-index: 1; } .bubble { position: absolute; border-radius: 50%; background-color: rgba(255, 105, 180, 0.6); animation: bubbleAnimation 1s ease-out; pointer-events: none; } @keyframes bubbleAnimation { 0% { transform: scale(0); opacity: 1; } 100% { transform: scale(1); opacity: 0; } } table { width: 100%; border-collapse: collapse; background-color: #fff; border-radius: 15px; overflow: hidden; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); margin-bottom: 30px; } th, td { padding: 15px; text-align: center; font-size: 16px; color: #555; } th { background-color: #00C8FF; color: #fff; font-weight: 600; } tr:nth-child(even) { background-color: #fafafa; } .copy-button { position: absolute; top: 10px; right: 10px; background-color: #00C8FF; color: #fff; border: none; cursor: pointer; padding: 6px 10px; font-size: 14px; border-radius: 12px; transition: background-color 0.3s ease, transform 0.3s ease; display: inline-block; width: auto; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .copy-button:hover { background-color: #00AEEF; transform: translateY(-2px); } .toast { position: fixed; bottom: -100px; left: 50%; transform: translateX(-50%); background-color: rgba(0, 0, 0, 0.8); color: #fff; padding: 12px 20px; border-radius: 25px; font-size: 16px; transition: bottom 0.5s ease; z-index: 1000; } .toast.show { bottom: 50px; } @keyframes bounceIn { 0% { transform: scale(0.3); opacity: 0; } 50% { transform: scale(1.05); opacity: 0.7; } 70% { transform: scale(0.9); opacity: 0.9; } 100% { transform: scale(1); opacity: 1; } } .animated { animation: bounceIn 0.8s both; } .icon { width: 24px; height: 24px; vertical-align: middle; margin-right: 10px; } .message { font-style: italic; background: linear-gradient(45deg, #ff7e5f, #feb47b); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-size: 1.2em; margin-top: 10px; } </style> <!-- 引入 anime.js 库 --> <script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js"></script> </head> <body> <!-- 添加烟花特效的 canvas 元素 --> <canvas class="fireworks" style="position: fixed; left: 0; top: 0; z-index: 99999999; pointer-events: none;"></canvas> <div class="container"> <h1>🌟 课表生成器 🌟</h1> <!-- 添加输入数据的标题 --> <h2>🔢 输入数据</h2> <!-- currenttt.js 拖拽或手动输入 --> <div class="code-block"> <textarea id="jsonInput" placeholder="请输入或拖拽 currenttt.js 数据"></textarea> </div> <!-- maindbi.js 拖拽或手动输入 --> <div class="code-block"> <textarea id="maindbiInput" placeholder="请输入或拖拽 maindbi.js 数据"></textarea> </div> <div class="code-block"> <textarea id="subjectMapInput" placeholder="请输入科目对应关系"> # 请按照以下格式添加科目对应关系: # 科目ID=科目名称 # 例如: FeP-VWL=经济 FeP-W-Mathe=数学 FeP-Deutsch=德语 FeP-Englisch=英语 noteninfo=Noteninfo Fep-Tutorium Mathe=数学辅导 </textarea> </div> <button onclick="generateTimetable()"> <span class="button-text">生成课表</span> </button> <h2>📅 课表</h2> <table id="timetable"> <tr> <th>日期</th> <th>时间</th> <th>科目</th> <th>教师</th> <th>教室</th> </tr> </table> <h2>📊 课程统计</h2> <div id="statistics"></div> <h2>📝 Markdown格式</h2> <div class="code-block" id="markdownOutputContainer"> <button class="copy-button" onclick="copyToClipboard('markdownOutput', 'markdown')">复制</button> <pre id="markdownOutput"></pre> </div> <h2>📆 ICS格式</h2> <div class="code-block" id="icsOutputContainer"> <button class="copy-button" onclick="copyToClipboard('icsOutput', 'ics')">复制</button> <pre id="icsOutput"></pre> </div> </div> <div id="toast" class="toast">已复制到剪贴板</div> <script> function generateTimetable() { const jsonInput = document.getElementById('jsonInput').value; const maindbiInput = document.getElementById('maindbiInput').value; const subjectMapInput = document.getElementById('subjectMapInput').value; try { const timetableData = JSON.parse(jsonInput); const maindbiData = JSON.parse(maindbiInput); const timetable = document.getElementById('timetable'); // 清空表格内容 timetable.innerHTML = ''; timetable.innerHTML = '<tr><th>日期</th><th>时间</th><th>科目</th><th>教师</th><th>教室</th></tr>'; const courses = timetableData.r.ttitems.filter(item => item.type === 'card' && !item.removed); const teachers = maindbiData.r.tables.find(table => table.id === 'teachers').data_rows; const subjects = maindbiData.r.tables.find(table => table.id === 'subjects').data_rows; const classrooms = maindbiData.r.tables.find(table => table.id === 'classrooms').data_rows; // 创建教师、科目和教室的映射表 const teacherMap = new Map(teachers.map(teacher => [teacher.id, teacher.short])); const subjectMap = new Map(subjects.map(subject => [subject.id, subject.name])); const classroomMap = new Map(classrooms.map(classroom => [classroom.id, classroom.name])); // 创建自定义科目映射表 const customSubjectMap = new Map(); const lines = subjectMapInput.split('\\n'); lines.forEach(line => { const [key, value] = line.split('='); if (key && value) { customSubjectMap.set(key.trim().toLowerCase(), value.trim()); } }); // 定义左、右教室列表 const leftClassrooms = ['105','106','107','108','109','203','204','205','206','207','223','311','307','310','308','309']; const rightClassrooms = ['202', '222a', '212', '201', '305', '304', '303', '302', '301']; const lunchBreakStart = '11:45'; const lunchBreakEnd = '12:45'; let finalCourses = []; function timeToMinutes(timeStr) { const [hours, minutes] = timeStr.split(':').map(Number); return hours * 60 + minutes; } courses.forEach(course => { const startTimeMinutes = timeToMinutes(course.starttime); const endTimeMinutes = timeToMinutes(course.endtime); const lunchStartMinutes = timeToMinutes(lunchBreakStart); const lunchEndMinutes = timeToMinutes(lunchBreakEnd); // 如果课程跨越午休时间段,我们将其拆分 if (startTimeMinutes < lunchEndMinutes && endTimeMinutes > lunchStartMinutes) { // 午休前 if (startTimeMinutes < lunchStartMinutes) { finalCourses.push({ date: course.date, starttime: course.starttime, endtime: lunchBreakStart, subjectid: course.subjectid, teacherids: course.teacherids, classroomid: course.classroomids[0] }); } // 午休后 if (endTimeMinutes > lunchEndMinutes) { finalCourses.push({ date: course.date, starttime: lunchBreakEnd, endtime: course.endtime, subjectid: course.subjectid, teacherids: course.teacherids, classroomid: course.classroomids[0] }); } } else { finalCourses.push({ date: course.date, starttime: course.starttime, endtime: course.endtime, subjectid: course.subjectid, teacherids: course.teacherids, classroomid: course.classroomids[0] }); } }); // 排序 finalCourses finalCourses.sort((a, b) => { if (a.date === b.date) { return a.starttime.localeCompare(b.starttime); } return a.date.localeCompare(b.date); }); // 合并相邻、相同课程 const mergedCourses = []; for (let i = 0; i < finalCourses.length; i++) { const current = finalCourses[i]; if (mergedCourses.length === 0) { mergedCourses.push({ ...current }); continue; } const last = mergedCourses[mergedCourses.length - 1]; // 检查是否可以合并 if ( last.date === current.date && last.endtime === current.starttime && last.subjectid === current.subjectid && JSON.stringify(last.teacherids) === JSON.stringify(current.teacherids) && last.classroomid === current.classroomid ) { // 合并时间 last.endtime = current.endtime; } else { mergedCourses.push({ ...current }); } } let markdownOutput = '| 日期 | 时间 | 科目 | 教师 | 教室 |\\n| ---- | ---- | ---- | ---- | ---- |\\n'; let icsOutput = 'BEGIN:VCALENDAR\\nVERSION:2.0\\nPRODID:-//Example Corp.//CalDAV Client//EN\\nCALSCALE:GREGORIAN\\n'; // 定义节次时间段 const periods = [ { start: '08:30', end: '09:15' }, { start: '09:15', end: '10:00' }, { start: '10:15', end: '11:00' }, { start: '11:00', end: '11:45' }, { start: '12:45', end: '13:30' }, { start: '13:30', end: '14:15' }, { start: '14:30', end: '15:15' }, { start: '15:15', end: '16:00' }, { start: '16:15', end: '17:00' }, { start: '17:00', end: '17:45' }, { start: '18:00', end: '18:45' }, { start: '18:45', end: '19:30' }, ]; // 统计科目节次数 const subjectPeriodCounts = {}; let totalPeriods = 0; mergedCourses.forEach(course => { let subjectName = subjectMap.get(course.subjectid) || ''; subjectName = subjectName.trim().toLowerCase(); if (customSubjectMap.has(subjectName)) { subjectName = customSubjectMap.get(subjectName); } const teacherNames = course.teacherids.map(id => teacherMap.get(id) || '').join(', '); let classroomName = classroomMap.get(course.classroomid) || '无教室'; // 根据左、右教室来确定后缀 let classroomSide = ''; if (classroomName === '306') { classroomSide = '前'; } else { const leftClassrooms = ['105','106','107','108','109','203','204','205','206','207','223','311','307','310','308','309']; const rightClassrooms = ['202', '222a', '212', '201', '305', '304', '303', '302', '301']; if (leftClassrooms.includes(classroomName)) { classroomSide = '左'; } else if (rightClassrooms.includes(classroomName)) { classroomSide = '右'; } else if (classroomName.startsWith('0') || classroomName.startsWith('1')) { // 你可以根据需求再细化规则 classroomSide = '右'; } } if (classroomSide) { classroomName += classroomSide; } // 计算课程覆盖了多少节次 const courseStart = timeToMinutes(course.starttime); const courseEnd = timeToMinutes(course.endtime); let periodsCovered = 0; for (let i = 0; i < periods.length; i++) { const periodStart = timeToMinutes(periods[i].start); const periodEnd = timeToMinutes(periods[i].end); if (courseEnd > periodStart && courseStart < periodEnd) { periodsCovered++; } } if (!subjectPeriodCounts[subjectName]) { subjectPeriodCounts[subjectName] = 0; } subjectPeriodCounts[subjectName] += periodsCovered; totalPeriods += periodsCovered; const row = timetable.insertRow(); row.insertCell().textContent = course.date; row.insertCell().textContent = \`\${course.starttime}-\${course.endtime}\`; row.insertCell().textContent = subjectName; row.insertCell().textContent = teacherNames; row.insertCell().textContent = classroomName; markdownOutput += \`| \${course.date} | \${course.starttime}-\${course.endtime} | \${subjectName} | \${teacherNames} | \${classroomName} |\\n\`; const startDateTime = \`\${course.date.replace(/-/g, '')}T\${course.starttime.replace(':', '')}00\`; const endDateTime = \`\${course.date.replace(/-/g, '')}T\${course.endtime.replace(':', '')}00\`; icsOutput += 'BEGIN:VEVENT\\n'; icsOutput += \`DTSTART;TZID=Europe/Berlin:\${startDateTime}\\n\`; icsOutput += \`DTEND;TZID=Europe/Berlin:\${endDateTime}\\n\`; icsOutput += \`SUMMARY:\${subjectName} \${teacherNames}\\n\`; icsOutput += \`LOCATION:\${classroomName || '网课'}\\n\`; icsOutput += 'DESCRIPTION:\\n'; icsOutput += 'END:VEVENT\\n'; }); icsOutput += 'END:VCALENDAR\\n'; document.getElementById('markdownOutput').textContent = markdownOutput; document.getElementById('icsOutput').textContent = icsOutput; timetable.classList.add('animated'); let statisticsHtml = '<ul>'; const sortedSubjects = Object.keys(subjectPeriodCounts).sort((a, b) => subjectPeriodCounts[b] - subjectPeriodCounts[a]); sortedSubjects.forEach(subject => { statisticsHtml += \`<li>\${subject}: \${subjectPeriodCounts[subject]} 节课</li>\`; }); statisticsHtml += \`<li><strong>总共: \${totalPeriods} 节课</strong></li>\`; statisticsHtml += '</ul>'; let message = ''; if (totalPeriods <= 25) { message = "尊嘟假嘟,这么少!你是想退休吗?"; } else if (totalPeriods >= 40) { message = "恭喜您,您的课程数已经突破天际!是不是在训练成为课程超人?"; } else if (totalPeriods >= 25 && totalPeriods < 40) { message = "接招吧!半径为2.5格的课程表水花!"; } statisticsHtml += \`<div class="message">\${message}</div>\`; document.getElementById('statistics').innerHTML = statisticsHtml; } catch (error) { alert(\`错误: \${error.message}\`); console.error(error); } } function copyToClipboard(elementId, format) { const element = document.getElementById(elementId); const textArea = document.createElement("textarea"); textArea.value = element.textContent; document.body.appendChild(textArea); textArea.select(); document.execCommand("copy"); document.body.removeChild(textArea); const toast = document.getElementById('toast'); toast.textContent = \`\${format.toUpperCase()} 格式已复制到剪贴板\`; toast.classList.add('show'); setTimeout(() => { toast.classList.remove('show'); }, 3000); } // 全局拖拽 document.addEventListener('dragover', function(e) { e.preventDefault(); }); document.addEventListener('drop', function(e) { e.preventDefault(); const files = e.dataTransfer.files; if (!files || !files.length) return; // 处理多个文件 for (let i = 0; i < files.length; i++) { const file = files[i]; const reader = new FileReader(); reader.onload = function(event) { // 根据文件名或内容,判断该放哪一个输入框 const content = event.target.result; // ① 根据文件名判断 const lowerName = file.name.toLowerCase(); if (lowerName.includes('maindbi')) { document.getElementById('maindbiInput').value = content; } else if (lowerName.includes('currenttt')) { document.getElementById('jsonInput').value = content; } else { // ② 若文件名不含 maindbi 或 currenttt,尝试判断内容 try { const jsonObj = JSON.parse(content); // 如果包含 tables 则认为是maindbi.js if (jsonObj?.r?.tables) { document.getElementById('maindbiInput').value = content; } // 如果包含 ttitems 则认为是currenttt.js else if (jsonObj?.r?.ttitems) { document.getElementById('jsonInput').value = content; } else { // 可能是未知文件,或者结构不一样 alert('无法识别文件类型: ' + file.name); } } catch (ex) { alert('文件不是合法的JSON,无法解析: ' + file.name); } } }; reader.readAsText(file); } }); document.querySelector('button').addEventListener('click', function(event) { const button = event.currentTarget; const rect = button.getBoundingClientRect(); const x = event.clientX - rect.left; const y = event.clientY - rect.top; const bubbleSize = Math.random() * 50 + 20; const bubble = document.createElement('span'); bubble.classList.add('bubble'); bubble.style.left = \`\${x - bubbleSize / 2}px\`; bubble.style.top = \`\${y - bubbleSize / 2}px\`; bubble.style.width = \`\${bubbleSize}px\`; bubble.style.height = \`\${bubbleSize}px\`; button.appendChild(bubble); bubble.addEventListener('animationend', () => { bubble.remove(); }); }); </script> <!-- 烟花特效代码(删除了圆圈效果,仅保留颗粒效果) --> <script> function updateCoords(e) { pointerX = (e.clientX || e.touches[0].clientX) - canvasEl.getBoundingClientRect().left; pointerY = (e.clientY || e.touches[0].clientY) - canvasEl.getBoundingClientRect().top; } function setParticuleDirection(e) { var t = anime.random(0, 360) * Math.PI / 180, a = anime.random(50, 180), n = [-1, 1][anime.random(0, 1)] * a; return { x: e.x + n * Math.cos(t), y: e.y + n * Math.sin(t) }; } function createParticule(e, t) { var a = {}; a.x = e; a.y = t; a.color = colors[anime.random(0, colors.length - 1)]; a.radius = anime.random(16, 32); a.endPos = setParticuleDirection(a); a.draw = function() { ctx.beginPath(); ctx.arc(a.x, a.y, a.radius, 0, 2 * Math.PI, true); ctx.fillStyle = a.color; ctx.fill(); }; return a; } function renderParticule(e) { for (var t = 0; t < e.animatables.length; t++) e.animatables[t].target.draw(); } function animateParticules(e, t) { var particules = []; for (var i = 0; i < numberOfParticules; i++) particules.push(createParticule(e, t)); anime.timeline().add({ targets: particules, x: function(e) { return e.endPos.x; }, y: function(e) { return e.endPos.y; }, radius: 0.1, duration: anime.random(1200, 1800), easing: "easeOutExpo", update: renderParticule }); } function debounce(fn, delay) { var timer; return function () { var context = this; var args = arguments; clearTimeout(timer); timer = setTimeout(function () { fn.apply(context, args); }, delay); } } var canvasEl = document.querySelector(".fireworks"); if (canvasEl) { var ctx = canvasEl.getContext("2d"), numberOfParticules = 30, pointerX = 0, pointerY = 0, tap = "mousedown", colors = ["#FF1461", "#18FF92", "#5A87FF", "#FBF38C"], setCanvasSize = debounce(function() { canvasEl.width = 2 * window.innerWidth; canvasEl.height = 2 * window.innerHeight; canvasEl.style.width = window.innerWidth + "px"; canvasEl.style.height = window.innerHeight + "px"; canvasEl.getContext("2d").scale(2, 2); }, 500), render = anime({ duration: Infinity, update: function() { ctx.clearRect(0, 0, canvasEl.width, canvasEl.height); } }); document.addEventListener(tap, function(e) { if (e.target.id !== "sidebar" && e.target.id !== "toggle-sidebar" && e.target.nodeName !== "A" && e.target.nodeName !== "IMG") { render.play(); updateCoords(e); animateParticules(pointerX, pointerY); } }, false); setCanvasSize(); window.addEventListener("resize", setCanvasSize, false); } </script> </body> </html>`; // 注册菜单命令 function updateMenu() { GM_registerMenuCommand( interceptEnabled ? "关闭拦截模式" : "开启拦截模式", toggleInterceptMode ); // 新增“生成课表”菜单项 GM_registerMenuCommand("生成课表", openTimetablePage); } // 切换拦截模式 function toggleInterceptMode() { interceptEnabled = !interceptEnabled; GM_setValue('interceptEnabled', interceptEnabled); alert(interceptEnabled ? "拦截模式已开启" : "拦截模式已关闭"); updateMenu(); } // 打开课表生成器页面 function openTimetablePage() { const blob = new Blob([timetableHTML], { type: 'text/html' }); const url = URL.createObjectURL(blob); window.open(url, '_blank'); } // 初始化菜单 updateMenu(); // 根据 URL 固定重命名文件 function getFixedFilename(url) { if (url.includes("currenttt.js")) { return "currenttt.js"; } if (url.includes("maindbi.js")) { return "maindbi.js"; } return url.split("/").pop(); } // 弹窗询问后下载文件 function promptAndDownload(url, text) { // 如果拦截模式关闭,则不执行任何操作 if (!interceptEnabled) { return; } const filename = getFixedFilename(url); // 如果已经下载过,就不再重复提示(如果希望每次都提示,可以将这段判断删除) if (downloadedFiles[filename]) { return; } if (confirm("是否下载文件 " + filename + " ?")) { const blob = new Blob([text], { type: "application/javascript" }); const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); downloadedFiles[filename] = true; } } // 拦截 XMLHttpRequest 请求 const originalXHRopen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (method, url, async, user, password) { this._interceptUrl = url; return originalXHRopen.apply(this, arguments); }; const originalXHRsend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send = function (body) { this.addEventListener("load", function () { if ( this._interceptUrl && (this._interceptUrl.includes("currenttt.js") || this._interceptUrl.includes("maindbi.js")) ) { promptAndDownload(this._interceptUrl, this.responseText); } }); return originalXHRsend.apply(this, arguments); }; // 拦截 fetch 请求 const originalFetch = window.fetch; window.fetch = function (...args) { return originalFetch.apply(this, args).then(response => { if ( response.url && (response.url.includes("currenttt.js") || response.url.includes("maindbi.js")) ) { response.clone().text().then(text => { promptAndDownload(response.url, text); }); } return response; }); }; })();