您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
直观展现死线情况,点击即可跳转;导出所有课程至日历;一键标记公告已读;保持登录(不可用)状态
// ==UserScript== // @icon http://tns.thss.tsinghua.edu.cn/~yangzheng/images/Tsinghua_University_Logo_Big.png // @name 网络学堂4202助手 // @namespace [email protected] // @version 2024年02月25日开学快乐版 // @license AGPL-3.0-or-later // @description 直观展现死线情况,点击即可跳转;导出所有课程至日历;一键标记公告已读;保持登录(不可用)状态 // @require https://cdn.bootcdn.net/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js // @grant GM.xmlHttpRequest // @connect zhjw.cic.tsinghua.edu.cn // @author thuyesh // @match http*://learn.tsinghua.edu.cn/f/wlxt/index/course/student/ // @match http*://learn.tsinghua.edu.cn/f/wlxt/* // @connect learn.tsinghua.edu.cn // @run-at document-end // ==/UserScript== document.head.appendChild(document.createElement('style')); const sheet = document.styleSheets[document.styleSheets.length - 1]; [ // 左上角图标 ` #banner .left a img[src="/res/app/wlxt/img/netcourse_logo.svg"] { cursor: pointer; margin-top: 13px; padding-top: 0px; background: linear-gradient(90deg, rgb(67, 159, 226) 0%, rgb(75, 168, 234) 55.29%, rgb(0, 114, 198) 55.29%); border-radius: 10px; } `, // 右上角课程日历显示修复 ` .qtip { width: max-content; } `, // 周数显示部分样式 ` .nav #myTabs { margin-bottom: 0; } `, ` .nav #myTabContent .boxdetail { margin-top: 0.3em; } `, // 鼠标加载动画 ` body.loading * { cursor: progress !important; } `, // 不显示课程的第一个框里显示的默认图片,我们把它替换为 DDL 提醒 ` .p_img { display: none; } `, // 下面是 DDL 提醒的样式 ` ul.stu.clearfix li.clearfix:first-child { text-align: center; display: flex; flex-flow: column; justify-content: center; } `, ` ul.stu.clearfix li.clearfix:first-child a { text-decoration: underline; font-size: 1.5em; color: black; } `, ` ul.stu.clearfix li.clearfix:first-child a span { display: block; margin-top: 1em; font-size: 0.6em; } `, ` ul.stu.clearfix.clean li.clearfix:first-child { background-color: lightgreen; color: white; font-size: 2em; font-weight: bolder; } `, ` ul.stu.clearfix.hw-yellow li.clearfix:first-child { background-color: gold; } `, ` ul.stu.clearfix.hw-orange li.clearfix:first-child { background-color: orange; } `, ` ul.stu.clearfix.hw-red li.clearfix:first-child { background-color: red; } `, ` ul.stu.clearfix.hw-red li.clearfix:first-child a { color: yellow; } `, ` ul.stu.clearfix.hw-overdue li.clearfix:first-child { background-color: lightgrey; } `, ` ul.stu.clearfix.clean li.clearfix:first-child::after { content: "✓"; } `, // 周数显示 ` .week-count { display: inline-block; font-size: 1em; } `, ` .week-count span { color: brown; font-size: 1.2em; margin: 0 0.2em 0 0.2em; } `, ` .state.stu.clearfix + .operations { margin-top: 0.5em; } `, ` button.operation { background-color: white; padding: 0.3em; border: 1px grey solid; cursor: pointer; border-radius: 3px; transition: background-color 0.1s; } `, ` button.operation:not(:first-child) { margin-left: 1em; } `, ` button.operation:hover, button.operation:active { background-color: lightgrey; } `, ` .boxdetail dd.clearfix.stu { padding-bottom: 0; } `, ` .boxdetail dd.clearfix.stu:last-child { padding-bottom: 16px; } `, // 用于自动刷新的 iframe ` iframe#refresher { position: absolute; left: -10em; top: -10em; width: 10em; height: 10em; border: 1px solid magenta; opacity: 0.5; } `, ].forEach((rule) => sheet.insertRule(rule, 0)); // 鼠标加载动画开启关闭 function setLoading() { document.body.classList.add('loading'); } function unsetLoading() { document.body.classList.remove('loading'); } // 仿造请求 var csrf = ''; const dummy = { sEcho: 1, iColumns: 8, sColumns: ',,,,,,,', iDisplayStart: 0, iDisplayLength: '30', mDataProp_0: 'wz', bSortable_0: false, mDataProp_1: 'bt', bSortable_1: true, mDataProp_2: 'mxdxmc', bSortable_2: true, mDataProp_3: 'zywcfs', bSortable_3: true, mDataProp_4: 'kssj', bSortable_4: true, mDataProp_5: 'jzsj', bSortable_5: true, mDataProp_6: 'jzsj', bSortable_6: true, mDataProp_7: 'function', bSortable_7: false, iSortCol_0: 5, sSortDir_0: 'desc', iSortCol_1: 6, sSortDir_1: 'desc', iSortingCols: 2, wlkcid: '', }; function shallowCopyObject(obj) { return Object.fromEntries(Object.entries(obj)); } function fetchResponse(url, method, data) { return fetch(`${url}${url.includes('?') ? '&' : '?'}_csrf=${csrf}`, { method: method, headers: { Accept: 'application/json, text/javascript, */*; q=0.01', 'Cache-Control': 'max-age=0', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest', }, credentials: 'include', referrer: 'https://learn.tsinghua.edu.cn/', referrerPolicy: 'strict-origin-when-cross-origin', body: method.toUpperCase() === 'POST' ? new URLSearchParams({ aoData: JSON.stringify( Object.entries( Object.assign(shallowCopyObject(dummy), data), ).map((e) => ({ name: e[0], value: e[1] })), ), }) : undefined, }); } function fetchJSON(url, method, data) { return fetchResponse(url, method, data).then(function (response) { if (response.ok) { return response.json(); } else { return Promise.resolve(); } }); } function getJSON(url) { return fetchJSON(url, 'GET'); } // 抓取作业剩余时间信息并显示 function displayDDL(e, wlkcid) { var parent = e; if ( parseInt(e.querySelector('li.clearfix:nth-child(4) .rt').innerText) > 0 ) { fetchJSON( 'https://learn.tsinghua.edu.cn/b/wlxt/kczy/zy/student/zyListWj', 'POST', { wlkcid: wlkcid }, ).then(function (json) { if (json) { var now = Date.now(); var deadlines = json.object.aaData.filter( (hw) => hw.jzsj - now > 0, ); var hwLink = document.createElement('a'); if (deadlines.length === 0) { hwLink.innerText = '已过期'; parent.classList.add('hw-overdue'); } else { var deadline = deadlines.reduce((hw1, hw2) => hw1.jzsj > hw2.jzsj ? hw2 : hw1, ); var days = Math.floor( (deadline.jzsj - now) / 1000 / 60 / 60 / 24, ); var dueTime = new Date(deadline.jzsj + 8 * 60 * 60 * 1000) .toISOString() .substring(5, 16) .replace('T', ' '); hwLink.innerHTML = `剩余 ${days} 天<span class="due-time">${dueTime} 截止</span>`; hwLink.href = `https://learn.tsinghua.edu.cn/f/wlxt/kczy/zy/student/viewZy?wlkcid=${deadline.wlkcid}&sfgq=0&zyid=${deadline.zyid}&xszyid=${deadline.xszyid}`; const bgColors = [ 'hw-red', 'hw-red', 'hw-orange', 'hw-orange', 'hw-orange', 'hw-orange', 'hw-orange', ]; parent.classList.add( bgColors[days] ? bgColors[days] : 'hw-yellow', ); } parent .querySelector('li.clearfix:first-child') .appendChild(hwLink); } }); } else { parent.classList.add('clean'); } } // 保存新课件 function saveNewFiles(e, wlkcid) { getJSON( `http://learn.tsinghua.edu.cn/b/wlxt/kj/wlkc_kjxxb/student/kjxxbByWlkcidAndSizeForStudent?size=999&wlkcid=${wlkcid}`, ).then((json) => { if (json) { var files = json.object.filter((f) => f.isNew); if (files.length === 0) { alert('无新课件'); } else { var size = files.reduce((i, file) => i + file.wjdx, 0); const sizeSuffix = ['kB', 'MB', 'GB', 'TB']; var humanReadable = sizeSuffix.reduce( (size, name) => { if (size[0] >= 1024) { return [size[0] / 1024, name]; } else { return size; } }, [size, 'B'], ); if ( confirm( `下载以下 ${humanReadable[0].toFixed(2)} ${ humanReadable[1] } 的新课件?\n→ ${files .map((f) => `${f.bt} - ${f.fileSize}`) .join('\n→ ')}`, ) ) { files.forEach((f) => window.open( `http://learn.tsinghua.edu.cn/b/wlxt/kj/wlkc_kjxxb/student/downloadFile?sfgk=0&wjid=${f.wjid}`, ), ); } } } }); } // 公告已读 function markRead(e, wlkcid) { setLoading(); getJSON( `http://learn.tsinghua.edu.cn/b/wlxt/kcgg/wlkc_ggb/student/kcggListXs?size=999&wlkcid=${wlkcid}`, ).then((json) => { if (json) { var unreadItems = json.object.aaData.filter((e) => e.sfyd === '否'); if (unreadItems.length === 0) { alert('无公告'); unsetLoading(); } else if ( confirm( `按确认键将以下公告设为已读:\n${unreadItems .map(function (e) { return ( '→ ' + e.bt + (e.fjmc ? `(有附件 ${e.fjmc})` : '') ); }) .join('\n')}`, ) ) { let total = unreadItems.length; let count = 0; var handleResponse = (response) => { count++; if (total === count) { alert('已读完成'); location.reload(); } }; unreadItems.forEach((e) => fetchResponse( `http://learn.tsinghua.edu.cn/f/wlxt/kcgg/wlkc_ggb/student/beforeViewXs?wlkcid=${wlkcid}&id=${e.ggid}`, 'GET', ).then(handleResponse), ); } } else { alert('网络错误'); unsetLoading(); } }); } // 把按钮加到界面 function displayOperations(e, wlkcid) { const operations = { 所有公告标为已读: markRead, 批量保存新课件: saveNewFiles, }; var buttonContainer = document.createElement('div'); buttonContainer.classList.add('operations'); for (var i in operations) { var button = document.createElement('button'); button.innerText = i; button.classList.add('operation'); button.onclick = ( (operation) => () => operation(e, wlkcid) )(operations[i]); buttonContainer.appendChild(button); } e.parentElement.parentElement.appendChild(buttonContainer); } // 抓取课程日历信息 function fetchEvents(year, month, events) { var prevMonth = new Date(); prevMonth.setFullYear(year, month - 1, 15); var thisMonth = new Date(); thisMonth.setFullYear(year, month, 15); var nextMonth = new Date(); nextMonth.setFullYear(year, month + 1, 15); return new Promise((resolve, reject) => { const graduate = unsafeWindow.role === 'yjs'; GM.xmlHttpRequest({ method: 'GET', url: `https://zhjw.cic.tsinghua.edu.cn/jxmh_out.do?m=${ graduate ? 'yjs' : 'bks' }_jxrl_all&p_start_date=${prevMonth .toISOString() .slice(0, 7) .replaceAll('-', '')}01` + `&p_end_date=${nextMonth .toISOString() .slice(0, 7) .replaceAll('-', '')}01&jsoncallback=no_such_method` + `&_csrf=${csrf}`, onload: (response) => { if (response.status < 400) { var text = response.responseText; var monthCalendar = JSON.parse(text.slice(15, -1)); if (monthCalendar.length === 0) { resolve(events); } else { resolve( fetchEvents( year, month + 1, events.concat( monthCalendar.filter( (event) => new Date(event.nq).getMonth() === thisMonth.getMonth(), ), ), ), ); } } else { resolve(events); } }, onerror: reject, }); }); } function getTZ(date, time) { return `${date.replaceAll('-', '')}T${time.replaceAll(':', '')}00`; } // 生成 ics 文件,注意用的是 \r\n function makeIEvent(event, prior) { return `BEGIN:VEVENT\r UID:${getTZ(event.nq, event.kssj)}-${Math.floor( Math.random() * 10000, )}@tsinghua.edu.cn\r DTSTAMP;TZID=Asia/Shanghai:${getTZ(event.nq, event.kssj)}\r DTSTART;TZID=Asia/Shanghai:${getTZ(event.nq, event.kssj)}\r DTEND;TZID=Asia/Shanghai:${getTZ(event.nq, event.jssj)}\r SUMMARY:${event.nr}\r LOCATION:${event.dd}\r BEGIN:VALARM\r TRIGGER:-PT${prior}M\r ACTION:DISPLAY\r DESCRIPTION:${event.nr}前 ${prior} 分钟提醒\r END:VALARM\r END:VEVENT`; } function makeICalendar(events, prior) { return `BEGIN:VCALENDAR\r VERSION:2.0\r PRODID:-//Web THU Helper//iCalender//EN\r BEGIN:VTIMEZONE\r TZID:Asia/Shanghai\r BEGIN:STANDARD\r TZOFFSETFROM:+0800\r TZOFFSETTO:+0800\r TZNAME:CST\r DTSTART:19700101T000000\r END:STANDARD\r END:VTIMEZONE\r ${events.map((e) => makeIEvent(e, prior)).join('\r\n')}\r END:VCALENDAR\r `; } // 两个学期临界的几周的时候,result 是上一学期,而 resultList[0] 是下一学期 function getLatestResult(json) { if (json.resultList && json.resultList[0]) { var nextStart = new Date(json.resultList[0].kssj); nextStart.setDate(nextStart.getDate() - 7 * 4); if (nextStart < Date.now()) { return json.resultList[0]; } } return json.result; } // 下载日程为日历 const semesterUrl = 'https://learn.tsinghua.edu.cn/b/kc/zhjw_v_code_xnxq/getCurrentAndNextSemester'; function calendarizeAll() { setLoading(); getJSON(semesterUrl).then((json) => { if (json) { var now = new Date(getLatestResult(json).kssj); var month = now.getMonth(); var year = now.getFullYear(); var alarmPrior = parseInt( prompt('日历文件可设置日程提醒,每节课提前多少分钟提醒?', 30), ); if (alarmPrior > 0) { fetchEvents(year, month, []).then((events) => { unsetLoading(); var blob = new Blob([makeICalendar(events, alarmPrior)], { type: 'text/plain', }); // eslint-disable-next-line no-undef saveAs(blob, `${getLatestResult(json).xnxqmc}.ics`); }); } else { unsetLoading(); } } else { unsetLoading(); } }); } // 在界面里加入周数 function addWeekCount(container) { getJSON(semesterUrl).then((json) => { if (json) { var start = new Date(getLatestResult(json).kssj); var day = start.getDay(); day = day == 0 ? 7 : day; var nextMonday = new Date( start.setDate(start.getDate() + 7 - day + 1), ); var week = Math.ceil( (Date.now() - nextMonday) / 1000 / 60 / 60 / 24 / 7, ); var weekDiv = document.createElement('div'); weekDiv.innerHTML = `第<span>${week}</span>周:`; weekDiv.classList.add('week-count'); container.insertBefore(weekDiv, container.childNodes[0]); } }); } // 从图片上获取 csrf token function initCsrf() { for (var imgNode of document.querySelectorAll('img')) { csrf = new URL(imgNode.src).searchParams.get('_csrf'); if (csrf) { break; } } } // 两学期交界处的特殊处理 function getCourseContainer() { if (document.getElementById('nextSemester').value !== '') { let container = document.getElementById('nextsuojiaocourse'); if (container && container.innerText.trim() !== '') { return container; } } return document.getElementById('suoxuecourse'); } // 初始化 function customize() { initCsrf(); // 课程 DDL 显示、课件下载按钮 document .querySelectorAll('.state.stu .name a[href^="/f/wlxt/kczy/zy/"]') .forEach((e) => { var wlkcid = new URL(e.href).searchParams.get('wlkcid'); const parent = e.closest('ul'); displayDDL(parent, wlkcid); displayOperations(parent, wlkcid); }); // 周数显示、导出日历按钮 if (!document.getElementById('calendarizer')) { var container = getCourseContainer().parentElement.querySelector('.title'); var calendarButton = document.createElement('button'); calendarButton.classList.add('operation'); calendarButton.id = 'calendarizer'; calendarButton.style.marginLeft = '1em'; calendarButton.innerText = '导出所有课程至日历文件'; calendarButton.onclick = calendarizeAll; container.appendChild(calendarButton); addWeekCount(container); } // 切换到网络学堂旧样式 const tabSwitch = document.querySelector('.in .tab2'); if (tabSwitch) { tabSwitch.click(); } } const insideIframe = window.location.hash === '#refresher'; function autoRefresh() { if (insideIframe) { return; } const iframe = document.createElement('iframe'); const page = '/f/wlxt/index/course/student/#refresher'; iframe.id = 'refresher'; iframe.style.visibility = 'hidden'; iframe.onload = () => { setTimeout(() => { iframe.src = ''; }, 5000); setTimeout(() => { iframe.src = page; }, 10 * 60 * 1000); }; iframe.onerror = iframe.onload; document.body.append(iframe); iframe.src = page; } function init() { if (!document.querySelector('.state.stu.clearfix .operations')) { customize(); } if (!document.querySelector('iframe#refresher')) { autoRefresh(); } } // 延迟初始化,等到页面加载完、脚本跑完再说 const homePath = '/f/wlxt/index/course/student/'; if (window.location.pathname === homePath) { if (document.querySelector('dd.stu') === null) { var container = getCourseContainer(); var observer = new MutationObserver(function () { if (document.querySelector('dd.stu') !== null) { setTimeout(function () { init(); }, 50); } }); observer.observe(container, { attributes: false, childList: true, subtree: false, }); } else { init(); } } // 左上角的图标分割 var logo = document.querySelector('#banner .left a>img'); if (logo) { logo.parentElement.href = '#'; var iconMap = document.createElement('map'); iconMap.name = 'iconmap'; var iconMapArea1 = document.createElement('area'); var iconMapArea2 = document.createElement('area'); Object.entries({ shape: 'rect', coords: '0,0,115,47', href: homePath + 'index', title: '登录(不可用)界面', }).forEach((entry) => iconMapArea1.setAttribute(entry[0], entry[1])); Object.entries({ shape: 'rect', coords: '115,0,202,47', href: homePath, title: '我的课程', }).forEach((entry) => iconMapArea2.setAttribute(entry[0], entry[1])); iconMap.appendChild(iconMapArea1); iconMap.appendChild(iconMapArea2); logo.parentElement.appendChild(iconMap); logo.setAttribute('usemap', '#iconmap'); }
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址