您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
功能:1) 在任意活动内下载ical格式的日程表 2)一键显示前200项活动 3)教务系统内下载ics格式课程表,可用于导入 Google Calendar
// ==UserScript== // @name SIT上应第二课堂日程助手 // @namespace [email protected] // @version 20200306 // @description 功能:1) 在任意活动内下载ical格式的日程表 2)一键显示前200项活动 3)教务系统内下载ics格式课程表,可用于导入 Google Calendar // @author snomiao // @match http*://sc.sit.edu.cn/* // @match http*://ems.sit.edu.cn:85/student/* // @match http*://ems1.sit.edu.cn:85/student/* // @grant none // @require https://greasyfork.org/scripts/32927-md5-hash/code/MD5%20Hash.js?version=225078 // ==/UserScript== (function () { 'use strict'; var 绑定Click到元素 = (f, 元素) => ( 元素.addEventListener('click', f), 元素 ); var 新元素 = (innerHTML) => { var e = document.createElement('div'); e.innerHTML = innerHTML; return e.children[0]; }; //在头信息已导入MD5函数 var 下载文件 = (href, title) => { const a = document.createElement('a'); a.setAttribute('href', href); a.setAttribute('download', title); a.click(); }; var 下载文本文件 = (text, title) => { 下载文件(URL.createObjectURL(new Blob([text])), title); }; var 并发数 = 0; var 异步抓取 = (URL) => { return new Promise((resolve, reject) => { var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function () { if (xhr.readyState == 4) { 并发数--; if (xhr.status == 200) { //if you fetch a file you can JSON.parse(xhr.responseText) var data = xhr.responseText; resolve(data); } else { // 连接失败后的重试机制 (async () => { var data = await 异步抓取(URL); resolve(data); })(); } } }; xhr.open('GET', URL, true); // 限制并发连接数在10个以内' // 改为1的时候就是串行 var limit = 10; var timer = setInterval(() => { if (并发数 < limit) { xhr.send(null); 并发数++; clearInterval(timer); } }, 1000 * Math.random()); }); }; var 解析第二课堂活动事件 = (html) => { var re = {}; // 样例 var _抓取html样例 = ` <h1 class="title_8">【学工部】上海应用技术大学2019届毕业生春季校园综合招聘会</h1> <div style=" color:#7a7a7a; text-align:center"> 活动编号:1053790 活动开始时间:2019-3-29 13:00:00 活动地点:大学生活动中心一楼大厅、第二食堂二楼 活动时长:150 分钟<br /> 负责人:吴晓燕 负责人电话:60873212 主办方:学工部 承办方:就业指导服务中心 刷卡时间段:2019-03-29 12:50:00 --至-- 2019-03-15 15:20:00 !? </div>`; var v_dom = document.createElement('html'); v_dom.innerHTML = html; // 基本信息 var 活动信息 = v_dom .querySelector('div.box-1 > div:nth-child(2)') .innerHTML.replace(/ /g, ' ') .replace(/<br>/g, '') .split('\n') .map((x) => x) .join('\n'); var 活动类型 = v_dom.querySelector('.hover-a').innerText; var 尝试提取活动时间 = (活动信息) => { if (活动信息.match(/刷卡时间段:[-\d]+ [:\d]+.*?[-\d]+ [:\d]+/m)) { var 开始 = new Date( 活动信息.match( /刷卡时间段:([-\d]+ [:\d]+).*?[-\d]+ [:\d]+/m )[1] ); var 结束 = new Date( 活动信息.match( /刷卡时间段:[-\d]+ [:\d]+.*?([-\d]+ [:\d]+)/m )[1] ); return { 开始, 结束 }; } else if ( 活动信息.match(/活动开始时间:([-\d]+ [:\d]+).*?/m) && 活动信息.match( /活动时长:([零一二三四五六七八九十0-9]*)\s*?个?\s*?((?:年|季度|月|周|天|小时|刻钟|分钟|分钟|星期|礼拜|日|时|刻|分|秒钟|秒)?) 分钟.*?/m ) ) { var 活动时长匹配 = 活动信息.match( /活动时长:([零一二三四五六七八九十0-9]*)\s*?个?\s*?((?:年|季度|月|周|天|小时|刻钟|分钟|分钟|星期|礼拜|日|时|刻|分|秒钟|秒)?) 分钟.*?/m ); var 量 = 活动时长匹配[1].replace( /[零一二三四五六七八九十]/g, (s) => '零一二三四五六七八九十'.indexOf(s) ); var 单位 = 活动时长匹配[2].length ? 活动时长匹配[2] .replace(/年/, (1000 * 60 * 60 * 24 * 365) / 4) .replace(/季度/, (1000 * 60 * 60 * 24 * 365) / 4) .replace(/月/, 1000 * 60 * 60 * 24 * 30) .replace(/周|星期|礼拜/, 1000 * 60 * 60 * 24) .replace(/天|日/, 1000 * 60 * 60 * 24) .replace(/小时|时/, 1000 * 60 * 60) .replace(/刻钟|刻/, 1000 * 60 * 15) .replace(/分钟|分/, 1000 * 60) .replace(/秒钟|秒/, 1000) : 1000 * 60; // 不带单位默认分钟 var 活动时长毫秒 = parseInt(单位) * 量; var 开始 = new Date( 活动信息.match(/活动开始时间:([-\d]+ [:\d]+).*?/m)[1] ); var 结束 = new Date(+开始 + 活动时长毫秒); return { 开始, 结束 }; } else if (活动信息.match(/活动开始时间:([-\d]+ [:\d]+).*?/m)) { var 开始 = new Date( 活动信息.match(/活动开始时间:([-\d]+ [:\d]+).*?/m)[1] ); var 结束 = new Date(+开始 + 1000 * 60 * 90); console.warn(`未能解析活动时长,默认90分钟`, 活动信息); return { 开始, 结束 }; } else { console.warn(`time doesn't match`, 活动信息); return undefined; } }; var 活动时间 = 尝试提取活动时间(活动信息); // 返回一个事件 return { // 事件发生时间地点 TSTART: 活动时间.开始, TEND: 活动时间.结束, LOCATION: ((e) => e && e[1])(活动信息.match(/活动地点:(.*)/m)), // 事件UID,用于更新进展 UID: MD5('第二课堂活动:' + 活动信息.match(/活动编号:(.*)/m)[1]) + `@snomiao.com`, // 事件标题 SUMMARY: v_dom.querySelector('h1').innerText, // 本活动的相关信息 DESCRIPTION: '活动类型:' + 活动类型 + '\n\n' + 活动信息 + '\n\n' + v_dom.querySelector('div.box-1 > div:nth-child(3)').innerText, }; }; var 取开学时间自课程序号 = (课程序号) => { // 课程序号 return ((e) => e && e[(课程序号 + '').slice(0, 3)])({ 185: new Date('2018-09-03 00:00:00 GMT+0800'), 185: new Date('2019-02-25 00:00:00 GMT+0800'), 190: new Date('2019-09-02 00:00:00 GMT+0800'), 195: new Date('2020-03-02 00:00:00 GMT+0800'), 200: new Date('2020-08-31 00:00:00 GMT+0800'), 205: new Date('2021-02-24 00:00:00 GMT+0800'), }); }; var 计算课程时间 = (课程时间, 开学时间凌晨) => { var match = 课程时间 .replace(/第(\d+)周,周(\d+),第(\d+)节/, '第$1周,周$2,第$3-$3节') .match(/第(\d+)周,周(\d+),第(\d+)-(\d+)节/); if (!match) { console.warn('无法识别为时间:', 课程时间, match); return undefined; } var 周数 = match[1]; var 星期 = match[2]; var 上课节 = match[3]; var 下课节 = match[4]; var 第一天时间 = 开学时间凌晨.getTime(); var 一秒 = 1000; // 1秒 var 一分 = 60 * 一秒; // 1分钟 var 一刻 = 15 * 一分; // 1刻钟 var 一时 = 60 * 一分; // 1小时 var 一天 = 24 * 一时; // 1天 var 一周 = 7 * 一天; // 1周 // 上课时间表 // 第 1-2 节 08:20-09:55 // 第 3-4 节 10:15-11:50 // 第 5-6 节 13:00-14:35 // 第 7-8 节 14:55-16:30 // 第 9-11 节 18:00-20:25 var 上课时间表 = {}; 上课时间表['1s'] = 一时 * 8 + 20 * 一分; 上课时间表['1e'] = 一时 * 9 + 5 * 一分; // 猜的,假设一小节课是45分钟这样子切开 上课时间表['2s'] = 一时 * 9 + 10 * 一分; // 猜的 上课时间表['2e'] = 一时 * 9 + 55 * 一分; 上课时间表['3s'] = 一时 * 10 + 15 * 一分; 上课时间表['3e'] = 一时 * 11 + 0 * 一分; // 猜的 上课时间表['4s'] = 一时 * 11 + 5 * 一分; 上课时间表['4e'] = 一时 * 11 + 50 * 一分; 上课时间表['5s'] = 一时 * 13 + 0 * 一分; 上课时间表['5e'] = 一时 * 13 + 45 * 一分; // 猜的 上课时间表['6s'] = 一时 * 13 + 50 * 一分; // 猜的 上课时间表['6e'] = 一时 * 14 + 35 * 一分; 上课时间表['7s'] = 一时 * 14 + 55 * 一分; 上课时间表['7e'] = 一时 * 15 + 0 * 一分; // 期末实践课的时间好像不按这个来的。。 1-7节 上课时间表['8s'] = 一时 * 15 + 45 * 一分; // 有的实训课是第 8 节开始上到 17:30 左右,不过先这样写着好了 上课时间表['8e'] = 一时 * 16 + 30 * 一分; 上课时间表['9s'] = 一时 * 18 + 0 * 一分; 上课时间表['9e'] = 一时 * 18 + 45 * 一分; 上课时间表['10s'] = 一时 * 18 + 50 * 一分; 上课时间表['10e'] = 一时 * 18 + 55 * 一分; // 9-10节不知几点结束,猜一个18点55吧 上课时间表['11s'] = 一时 * 19 + 40 * 一分; 上课时间表['11e'] = 一时 * 20 + 25 * 一分; var 上课时间 = 第一天时间 + 一周 * (周数 - 1) + 一天 * (星期 - 1) + 上课时间表[上课节 + 's']; var 下课时间 = 第一天时间 + 一周 * (周数 - 1) + 一天 * (星期 - 1) + 上课时间表[下课节 + 'e']; if (isNaN(上课时间) || isNaN(下课时间)) { console.warn('无法识别的课时:', 课程时间); console.warn(上课节, 下课时间); return undefined; } return [上课时间, 下课时间]; }; var 单节考试转日历事件 = (row) => { try { var { 序号, 考试课程, 考试时间, 考试地点, 考试性质 } = row; // hack: 用当前学期貌课程序号前3位代表学期时间 var 本开学时间 = 取开学时间自课程序号('190'); var 本节考试时间 = 考试时间.replace( /第(\d+)周 星期(\d+) 第(\d+(?:-\d+))节/, (_, a, b, c) => `第${a}周,周${b},第${c}节` ); var 考试时间戳 = 计算课程时间(本节考试时间, 本开学时间); return { // 事件发生时间地点 TSTART: new Date(考试时间戳[0]), TEND: new Date(考试时间戳[1]), LOCATION: 考试地点, // 事件UID,用于更新进展 UID: MD5(`考试: ${考试性质}-${考试课程}`) + `@snomiao.com`, // 事件标题 SUMMARY: `${考试性质}考: ${考试课程}/${考试地点}/${考试时间}`, // 考试的相关信息 (直接导出所有键值对) DESCRIPTION: [...Object.keys(row)] .map((key) => `${key}: ${row[key]}\n`) .join(''), }; } catch (e) { console.error(`错误:`, e.message, `, 出错数据: `, row, e.stack); throw new Error(`错误:`, e.message, `, 出错数据: `, row); } }; var 单节课转日历事件 = (row) => { // 范例输入 // 课程序号 课程名称 课程代码 课程类型 课程学分 授课老师 上课时间 上课地点 校区 计划人数 已选人数 挂牌 配课班 备注 // row = { // 课程序号, // 课程代码, // 课程名称, // 授课老师, // 学分, // 本节上课地点, // 本节上课时间, // } var { 本节上课时间, 本节上课地点, 课程序号, 课程名称, 课程序号, 课程代码, 授课老师, 学分, } = row; try { var 开学时间 = 取开学时间自课程序号(课程序号); var 课程时间戳 = 计算课程时间(本节上课时间, 开学时间); return { // 事件发生时间地点 TSTART: new Date(课程时间戳[0]), TEND: new Date(课程时间戳[1]), LOCATION: 本节上课地点, // 事件UID,用于更新进展 UID: MD5(`课程时间: ${课程序号}-${本节上课时间}`) + `@snomiao.com`, // 事件标题 SUMMARY: `${课程名称}/${课程序号}/${课程代码}/${授课老师}/${学分}分/${本节上课时间}`, // 本节课的相关信息 (直接导出所有键值对) DESCRIPTION: [...Object.keys(row)] .map((key) => `${key}: ${row[key]}\n`) .join(''), }; } catch (e) { throw new Error(`错误:`, e.message, `, 出错数据: `, row); } }; // 矩阵转置 var 转置 = (m) => m[0].map((x, i) => m.map((x) => x[i])); // 循环直到不动点,这里的调试技巧:进入死循环时可以在此中断,然后修改变量使其报错或跳出 // update: 可配置超时退出 var 循环直到不动点 = function (s, proc_function) { var o = s; while (1) { var tmp = proc_function(o); if (tmp == o) { return o; } else { o = tmp; } } }; // 把时间表按具体某周、某节课展开成独立元素,便于比较 var 展开课程节数 = function (s) { // 单双周筛选 var filter_error = function (s) { return !( s.length == 0 || s.match(/.*?第\d*?[02468]周\*[^\*].*/) || s.match(/.*?第\d*?[13579]周\*\*.*/) ); }; // 单双周统一化 var normalyze = function (s) { return s.replace(/周\*+/, '周'); }; // 化为 \n 分割 s = s.replace(/(?:;|\<br\>)+/g, '\n'); // “展开 a-b 为好多节课” s = 循环直到不动点(s, (x) => x.replace(/(.*?)(\d+)-(\d+)(.*)/, function (s, a, b, c, d) { var o = ''; var b = parseInt(b); var c = parseInt(c); for (var i = b; i <= c; i++) { o += a + i + d + '\n'; } return o; }) ); // “展开 a,b” 为2节课 s = 循环直到不动点(s, (x) => x.replace(/(.*?)(\d+),(\d+)(.*)/, function (s, a, b, c, d) { var o = ''; var b = parseInt(b); var c = parseInt(c); o += a + b + d + '\n'; o += a + c + d + '\n'; return o; }) ); return s.split('\n').filter(filter_error).map(normalyze); }; // 把连续的2节课合并成一段时间 var 合并连续的节数 = (节列) => { var lastLength = 节列.length; 节列.forEach((_, 序) => { // 防止比较溢出 if (!(序 + 1 < 节列.length)) return; // 跳过已经被变成undefined 的值 if (!节列[序]) return; if (!节列[序 + 1]) return; var match1 = 节列[序].match(/(第\d+周,周\d+,)第(\d+)(?:-(\d+))?节/); var match2 = 节列[序 + 1].match( /(第\d+周,周\d+,)第(\d+)(?:-(\d+))?节/ ); // 判断是否同周同天 if (!(match1 && match2 && match1[1] == match2[1])) return; // 补全课程结束时间 match1[3] = match1[3] || match1[2]; match2[3] = match2[3] || match2[2]; // 判断两节课是否连续 if (parseInt(match1[3]) + 1 == parseInt(match2[2])) { // 如果连续,把它们头尾相接 var a = match1[2]; var b = match2[3]; var time_concated = `第${a}-${b}节`; 节列[序] = 节列[序].replace( /第(\d+)(?:-(\d+))?节/, time_concated ); 节列[序 + 1] = undefined; } // periods[index] = 0; }); // 然后过滤掉计算过程中制造的垃圾 节列 = 节列.filter((x) => x); // 看看有没有合并掉一些课程 if (lastLength == 节列.length) { return 节列; } else { return 合并连续的节数(节列); } }; var 拆分课程按时间地点 = (课程) => { var 分节课程 = []; // 把时间表分裂掉,这里 课程[6] 是原始时间表 // 把按教室区分的周表分裂掉 var 周期 = 课程.上课时间.split(/;?\n/); var 地点 = 课程.上课地点.split(','); if (周期.length != 地点.length) { if (周期.length > 地点.length) { // 遇到locations少于periods的情况,就不断重复locations的最后一个元素(可能会有非预期的事情发生) for (var i = 周期.length - 地点.length - 1; i >= 0; i--) { 地点 = 地点.concat(地点.slice(-1)); } } else { // 遇到locations少于periods的情况,就不断重复locations的最后一个元素(可能会有非预期的事情发生) for (var i = 地点.length - 周期.length - 1; i >= 0; i--) { 周期 = 周期.concat(周期.slice(-1)); } } } var 周期地点列 = 转置([周期, 地点]); var 分节课程 = 周期地点列 .map((pl) => { var 节数列 = pl[0]; var 地点 = pl[1]; var 节数列 = 展开课程节数(节数列); var 节数列 = 合并连续的节数(节数列); // 按分裂的时间表展开 return 节数列.map((上课时间) => Object.assign({}, 课程, { 本节上课时间: 上课时间, 本节上课地点: 地点, }) ); }) .flat(); return 分节课程; }; var 课程表离散化 = (课程表_json) => 课程表_json.map(拆分课程按时间地点).flat(); var 下载单个课程日历 = (课程) => { var 课程列分节 = 拆分课程按时间地点(课程); var 输出ics = 日历事件列表转ICS格式(课程列分节.map(单节课转日历事件)); 下载文本文件( 输出ics, `${课程.课程序号}-${课程.课程代码}-${课程.课程名称}.ics` ); }; var 下载多个课程日历 = (课程列表) => { var 课程列分节 = 课程列表.map(拆分课程按时间地点).flat(); var 输出ics = 日历事件列表转ICS格式(课程列分节.map(单节课转日历事件)); 下载文本文件(输出ics, `课程日历` + new Date().toISOString() + `.ics`); }; var 下载单个考试日历 = (考试) => { var 输出ics = 日历事件列表转ICS格式([单节考试转日历事件(考试)]); 下载文本文件( 输出ics, `${考试.考试序号}-${考试.考试代码}-${考试.考试名称}.ics` ); }; var 下载多个考试日历 = (考试列表) => { var 输出ics = 日历事件列表转ICS格式(考试列表.map(单节考试转日历事件)); 下载文本文件(输出ics, `考试日历` + new Date().toISOString() + `.ics`); }; var 日历事件列表转ICS格式 = (事件列表) => { // .ics方案 var 事件转iCalendar格式的SECTION = (e) => { // 范例输入 // { // TSTART, // TEND, // SUMMARY, // DESCRIPTION?, // LOCATION?, // UID?, // } var icalStrFormat = (s) => s.replace(/\n/g, '\\n').replace(/.{40}/g, (c) => c + '\r\n '); var EVENT_DTSTAMP = icalStrFormat( new Date().toISOString().replace(/-|:|\.\d+/g, '') ); var EVENT_DTSTART = e.TSTART && icalStrFormat(e.TSTART.toISOString().replace(/-|:|\.\d+/g, '')); var EVENT_DTEND = e.TEND && icalStrFormat(e.TEND.toISOString().replace(/-|:|\.\d+/g, '')); var section_lines = [ `BEGIN:VEVENT`, //`DTSTART;TZID=Asia/Shanghai:${EVENT_DTSTART}`, `DTSTAMP:${EVENT_DTSTAMP}`, `DTSTART:${EVENT_DTSTART}`, //`DTEND;TZID=Asia/Shanghai:${EVENT_DTEND}`, `DTEND:${EVENT_DTEND}`, //`RRULE:FREQ=WEEKLY;COUNT=11;BYDAY=FR`, // 后续升级 `UID:${ (e.UID && icalStrFormat(e.UID)) || hash(e.SUMMARY) + '@snomiao.com' }`, e.SUMMARY && `SUMMARY:${icalStrFormat(e.SUMMARY)}`, e.DESCRIPTION && `DESCRIPTION:${icalStrFormat(e.DESCRIPTION)}`, e.LOCATION && `LOCATION:${icalStrFormat(e.LOCATION)}`, `END:VEVENT`, ]; return section_lines.filter((e) => e).join(`\r\n`); }; // 范例输出 // BEGIN:VEVENT // DTSTART:20190308T050000Z // DTEND:20190308T063500Z // UID:[email protected] // SUMMARY:大学生体育测试(一)/1850769/B1230001/张群/0.5分/第2周,周5,第5-6节 // DESCRIPTION:课程序号: 1850769\n课程名称: 大学生体育测试(一)\n课程代码: B // 1230001\n课程类型: 公共基础课\n课程学分: 0.5\n授课老师: 张 // 群\n上课时间: 第2-5周,周5,第5-6节\n上课地点: 奉贤操场\n校区: // 奉贤校区\n计划人数: 44\n已选人数: 44\n挂牌: 是\n配课班: 1 // 6101291, 16101261\n备注: \n // LOCATION:奉贤操场 // END:VEVENT var lines = [ `BEGIN:VCALENDAR`, `VERSION:2.0`, `PRODID:-//Snowstar Laboratory//NONSGML Snowstar Calendar//EN`, `CALSCALE:GREGORIAN`, // `X-WR-CALNAME:雪星课程表`, // `X-WR-TIMEZONE:Asia/Shanghai`, `BEGIN:VTIMEZONE`, `TZID:Asia/Shanghai`, // `X-LIC-LOCATION:Asia/Shanghai`, `BEGIN:STANDARD`, `TZOFFSETFROM:+0800`, `TZOFFSETTO:+0800`, `TZNAME:CST`, `DTSTART:19700101T000000`, `END:STANDARD`, `END:VTIMEZONE`, `${事件列表.map(事件转iCalendar格式的SECTION).join('\r\n')}`, `END:VCALENDAR`, ]; return lines.filter((e) => e).join(`\r\n`) + `\r\n`; }; if (location.hostname.match(/^sc.*\.sit\.edu\.cn$/)) { // 解析并下载当前页面的日历 var 下载当前活动日历 = async (e) => { if (e) e.disabled = true; var href = window.location; var html = await 异步抓取(href); var re = 解析第二课堂活动事件(html); re.DESCRIPTION += '\n' + href; var 输出ics = 日历事件列表转ICS格式([re]); 下载文本文件( 输出ics, '第二课堂活动:' + document.querySelector('h1').innerText + '.ical' ); if (e) e.disabled = false; }; var 当前页面活动列表日历 = async (e) => { if (e) e.disabled = true; // 获取当前页面上所有活动详情的URL; var hrefs = [...document.querySelectorAll('a')] .map((a) => a.href) .filter((href) => !!href.match('activityDetail.action')); // 异步下载所有事件 var 事件列 = []; await Promise.all( hrefs.map(async (href) => { var html = await 异步抓取(href); var 事件 = 解析第二课堂活动事件(html); 事件.DESCRIPTION += '\n' + href; 事件列.push(事件); // 全部抓取完成之后保存本地 if (事件列.length == hrefs.length) { var 输出ics = 日历事件列表转ICS格式(事件列); 下载文本文件( 输出ics, document.querySelector('title').innerText + '.ical' ); } }) ); if (e) e.disabled = false; }; var 下载近期所有第二课堂活动日历 = async (e) => { if (e) e.disabled = true; // TODO: 进度条 // var caption = e.innerText // e.innerText = caption + "" + // var doneCount = var all_events = []; var 获取当前页面第二课堂分类列表 = () => { var reg = /\/public\/activity\/activityList\.action\?categoryId=(.*)/; return [...document.querySelectorAll('#dekt-nav a[href]')] .map((a) => { var match = a.href.match(reg); return ( match && { categoryId: match[1], actType: a.innerText, } ); }) .filter((e) => e); }; var 第二课堂分类列表 = 获取当前页面第二课堂分类列表(); // 异步列出每个分类最近的50个活动,并等待全部返回 await Promise.all( 第二课堂分类列表.map(async ({ categoryId, actType }) => { var url = `/public/activity/activityList.action?pageNo=1&pageSize=50&categoryId=${categoryId}`; // 获取当前分类的活动链接(列表) var v_dom = document.createElement('html'); v_dom.innerHTML = await 异步抓取(url); var hrefs = [...v_dom.querySelectorAll('a')] .map((a) => a.href) .filter( (href) => !!href.match('activityDetail.action') ); // 异步下载所有活动,并等待全部下载完成 var events = await Promise.all( hrefs.map(async (href) => { var html = await 异步抓取(href); var event = 解析第二课堂活动事件(html); event.DESCRIPTION += '\n' + href; // 除此之外还要标出活动类型 event.SUMMARY = actType + '/' + event.SUMMARY; event.DESCRIPTION = '活动类型: ' + actType + '\n\n' + event.DESCRIPTION; return event; }) ); // 收集 all_events = all_events.concat(events); }) ); // 转成ical格式并触发下载 var 输出ics = 日历事件列表转ICS格式(all_events); 下载文本文件( 输出ics, '[' + new Date() .toISOString() .replace(/[^0-9]/g, '') .substr(0, 8) + ']近期第二课堂活动.ical' ); if (e) e.disabled = false; }; var 诚信积分一键加满 = async (e) => { if (e) e.disabled = true; // 获取当前分类的活动申请编号(列表) var v_dom = document.createElement('html'); v_dom.innerHTML = await 异步抓取( 'http://sc.sit.edu.cn/public/pcenter/activityOrderList.action?pageNo=1&pageSize=999999999' ); var lsOrderId = [ ...v_dom.querySelectorAll('form td:nth-child(1)>a'), ].map((e) => parseInt(e.innerText)); await Promise.all( lsOrderId.map(async (oid) => { // 五分好评666! var assess = 100; var content = '666'; var actityOrderId = oid; await 异步抓取( `http://sc.sit.edu.cn/public/pcenter/assess.action?assess=${assess}&content=${content}&actityOrderId=${actityOrderId}` ); }) ); console.log(lsOrderId.length + '个活动已加满'); if (e) e.disabled = false; }; // 显示工具栏 var flag_已有工具栏 = false; var 生成工具栏 = () => { var 容器 = document.querySelector('.user-info'); if (!容器 || flag_已有工具栏) return; var 工具栏元素列表 = [ 新元素( `<a href="http://sc.sit.edu.cn/public/activity/activityList.action?pageNo=1&pageSize=200&categoryId=&activityName=">查看最近的200个活动</a>` ), window.location.href.match('activityDetail.action') ? 绑定Click到元素( 下载当前活动日历, 新元素(`<button>下载 当前事件.ical</button>`) ) : 绑定Click到元素( 当前页面活动列表日历, 新元素(`<button>下载 当前页面活动列表.ical</button>`) ), 绑定Click到元素( 下载近期所有第二课堂活动日历, 新元素(`<button>下载 近期所有第二课堂活动.ical</button>`) ), 绑定Click到元素( 诚信积分一键加满, 新元素(`<button>诚信积分一键加满!</button>`) ), ]; var 工具栏 = 新元素('<span></span>'); 工具栏元素列表.map((e) => 工具栏.appendChild(e)); 容器.append(工具栏); flag_已有工具栏 = true; }; window.addEventListener('load', 生成工具栏); 生成工具栏(); } // 选课页面 var 绑定点击 = (元素, 描述, f) => { 元素.setAttribute('title', 描述); 元素.classList.add('clickable'); 元素.onclick = f; }; if ( location.hostname.match(/^ems.*\.sit\.edu\.cn$/) && location.pathname == '/student/selCourse/mycourselist.jsp' ) { var 可以选班级列表 = [...document.querySelectorAll(`tr`)].filter( (x) => x.querySelectorAll(`td`).length == 14 ); 可以选班级列表.map((tr) => { var 单元格表 = [...tr.querySelectorAll('td')]; var [ 课程序号, 课程名称, 课程代码, 课程类型, 学分, 授课老师, 上课时间, 上课地点, 校区, 计划人数, 已选人数, 挂牌, 配课班, 备注, ] = 单元格表.map((e) => e.innerText.trim()); var 课程 = { 课程序号, 课程名称, 课程代码, 课程类型, 学分, 授课老师, 上课时间, 上课地点, 校区, 计划人数, 已选人数, 挂牌, 配课班, 备注, }; 绑定点击(单元格表[6], '点击下载本课程日历', () => { 下载单个课程日历(课程); }); }); } if (location.pathname == '/student/selCourse/list1.jsp') { var lsClasses = [...document.querySelectorAll(`tr[align="center"]`)]; lsClasses.map((e) => { var 单元格表 = [...e.querySelectorAll('td')]; var [ 课程序号, 课程名称, 课程代码, 学分, 授课老师, 上课时间, 上课地点, 选课类型, 选课结果, 操作, ] = 单元格表.map((e) => e.innerText.trim()); var 课程 = { 课程序号, 课程名称, 课程代码, 学分, 授课老师, 上课时间, 上课地点, 选课类型, 选课结果, 操作, }; 单元格表[9].innerHTML = ` <a style="color:red" href="#" onclick="tj(2,'${课程序号}','${课程代码}')">取消选择</a><br> <a style="color:red" href="/student/selCourse/action.jsp?op=del&courseID=${课程代码}&cssID=${课程序号}&url=/student/selCourse/studentSel/teachclasslist.jsp?courseId=${课程代码}" target="_BLANK" alt="有可能选不上">退课重选(慎用)</a> `; 绑定点击(单元格表[5], '点击下载本课程日历', () => { 下载单个课程日历(课程); }); }); var 表头行 = document.querySelector(`form tr`); var 单元格表 = [...表头行.querySelectorAll('th')]; 绑定点击(单元格表[5], '点击下载多个课程日历', () => { var 课程列表 = lsClasses.map((e) => { var 单元格表 = [...e.querySelectorAll('td')]; var [ 课程序号, 课程名称, 课程代码, 学分, 授课老师, 上课时间, 上课地点, 选课类型, 选课结果, 操作, ] = 单元格表.map((e) => e.innerText.trim()); return { 课程序号, 课程名称, 课程代码, 学分, 授课老师, 上课时间, 上课地点, 选课类型, 选课结果, 操作, }; }); 下载多个课程日历(课程列表); }); } // 考试列表 if (location.pathname == '/student/main.jsp') { var 表格 = [...document.querySelectorAll('table')].filter((tab) => tab.innerHTML.match('我的考试') ); var 表格 = 表格 && 表格[0]; var 阵 = [...表格.querySelectorAll('tr')] .map((tr) => [...tr.querySelectorAll('td')]) .filter((e) => e.length == 5); 绑定点击(阵[0][2], '点击下载所有考试日历', () => { var 考试列表 = 阵.slice(1).map((单元格行) => { var [序号, 考试课程, 考试时间, 考试地点, 考试性质] = 单元格行.map((e) => e.innerText.trim()); return { 序号, 考试课程, 考试时间, 考试地点, 考试性质 }; }); 下载多个考试日历(考试列表); }); 阵.slice(1).map((单元格行) => { var [序号, 考试课程, 考试时间, 考试地点, 考试性质] = 单元格行.map( (e) => e.innerText.trim() ); var 考试 = { 序号, 考试课程, 考试时间, 考试地点, 考试性质 }; 绑定点击(单元格行[2], '点击下载本考试日历', () => { 下载单个考试日历(考试); }); }); } document.body.appendChild( 新元素('<style>.clickable{cursor: pointer}</style>') ); })();