Greasy Fork 支持简体中文。

SIT上应第二课堂日程助手

功能:1) 在任意活动内下载ical格式的日程表 2)一键显示前200项活动 3)教务系统内下载ics格式课程表,可用于导入 Google Calendar

  1. // ==UserScript==
  2. // @name SIT上应第二课堂日程助手
  3. // @namespace snomiao@gmail.com
  4. // @version 20200306
  5. // @description 功能:1) 在任意活动内下载ical格式的日程表 2)一键显示前200项活动 3)教务系统内下载ics格式课程表,可用于导入 Google Calendar
  6. // @author snomiao
  7. // @match http*://sc.sit.edu.cn/*
  8. // @match http*://ems.sit.edu.cn:85/student/*
  9. // @match http*://ems1.sit.edu.cn:85/student/*
  10. // @grant none
  11. // @require https://greasyfork.org/scripts/32927-md5-hash/code/MD5%20Hash.js?version=225078
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16. var 绑定Click到元素 = (f, 元素) => (
  17. 元素.addEventListener('click', f), 元素
  18. );
  19. var 新元素 = (innerHTML) => {
  20. var e = document.createElement('div');
  21. e.innerHTML = innerHTML;
  22. return e.children[0];
  23. };
  24.  
  25. //在头信息已导入MD5函数
  26. var 下载文件 = (href, title) => {
  27. const a = document.createElement('a');
  28. a.setAttribute('href', href);
  29. a.setAttribute('download', title);
  30. a.click();
  31. };
  32. var 下载文本文件 = (text, title) => {
  33. 下载文件(URL.createObjectURL(new Blob([text])), title);
  34. };
  35. var 并发数 = 0;
  36. var 异步抓取 = (URL) => {
  37. return new Promise((resolve, reject) => {
  38. var xhr = new XMLHttpRequest();
  39. xhr.onreadystatechange = function () {
  40. if (xhr.readyState == 4) {
  41. 并发数--;
  42. if (xhr.status == 200) {
  43. //if you fetch a file you can JSON.parse(xhr.responseText)
  44. var data = xhr.responseText;
  45. resolve(data);
  46. } else {
  47. // 连接失败后的重试机制
  48. (async () => {
  49. var data = await 异步抓取(URL);
  50. resolve(data);
  51. })();
  52. }
  53. }
  54. };
  55. xhr.open('GET', URL, true);
  56.  
  57. // 限制并发连接数在10个以内'
  58. // 改为1的时候就是串行
  59. var limit = 10;
  60. var timer = setInterval(() => {
  61. if (并发数 < limit) {
  62. xhr.send(null);
  63. 并发数++;
  64. clearInterval(timer);
  65. }
  66. }, 1000 * Math.random());
  67. });
  68. };
  69. var 解析第二课堂活动事件 = (html) => {
  70. var re = {};
  71. // 样例
  72. var _抓取html样例 = `
  73. <h1 class="title_8">【学工部】上海应用技术大学2019届毕业生春季校园综合招聘会</h1>
  74. <div style=" color:#7a7a7a; text-align:center">
  75. 活动编号:1053790 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  76. 活动开始时间:2019-3-29 13:00:00 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  77. 活动地点:大学生活动中心一楼大厅、第二食堂二楼&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  78. 活动时长:150 分钟<br />
  79. 负责人:吴晓燕 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  80. 负责人电话:60873212&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  81. 主办方:学工部&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  82. 承办方:就业指导服务中心&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  83. 刷卡时间段:2019-03-29 12:50:00&nbsp;&nbsp;--至--&nbsp;&nbsp;2019-03-15 15:20:00
  84. !?
  85. </div>`;
  86.  
  87. var v_dom = document.createElement('html');
  88. v_dom.innerHTML = html;
  89.  
  90. // 基本信息
  91. var 活动信息 = v_dom
  92. .querySelector('div.box-1 > div:nth-child(2)')
  93. .innerHTML.replace(/&nbsp;/g, ' ')
  94. .replace(/<br>/g, '')
  95. .split('\n')
  96. .map((x) => x)
  97. .join('\n');
  98. var 活动类型 = v_dom.querySelector('.hover-a').innerText;
  99.  
  100. var 尝试提取活动时间 = (活动信息) => {
  101. if (活动信息.match(/刷卡时间段:[-\d]+ [:\d]+.*?[-\d]+ [:\d]+/m)) {
  102. var 开始 = new Date(
  103. 活动信息.match(
  104. /刷卡时间段:([-\d]+ [:\d]+).*?[-\d]+ [:\d]+/m
  105. )[1]
  106. );
  107. var 结束 = new Date(
  108. 活动信息.match(
  109. /刷卡时间段:[-\d]+ [:\d]+.*?([-\d]+ [:\d]+)/m
  110. )[1]
  111. );
  112. return { 开始, 结束 };
  113. } else if (
  114. 活动信息.match(/活动开始时间:([-\d]+ [:\d]+).*?/m) &&
  115. 活动信息.match(
  116. /活动时长:([零一二三四五六七八九十0-9]*)\s*?个?\s*?((?:年|季度|月|周|天|小时|刻钟|分钟|分钟|星期|礼拜|日|时|刻|分|秒钟|秒)?) 分钟.*?/m
  117. )
  118. ) {
  119. var 活动时长匹配 = 活动信息.match(
  120. /活动时长:([零一二三四五六七八九十0-9]*)\s*?个?\s*?((?:年|季度|月|周|天|小时|刻钟|分钟|分钟|星期|礼拜|日|时|刻|分|秒钟|秒)?) 分钟.*?/m
  121. );
  122. var = 活动时长匹配[1].replace(
  123. /[零一二三四五六七八九十]/g,
  124. (s) => '零一二三四五六七八九十'.indexOf(s)
  125. );
  126. var 单位 = 活动时长匹配[2].length
  127. ? 活动时长匹配[2]
  128. .replace(/年/, (1000 * 60 * 60 * 24 * 365) / 4)
  129. .replace(/季度/, (1000 * 60 * 60 * 24 * 365) / 4)
  130. .replace(/月/, 1000 * 60 * 60 * 24 * 30)
  131. .replace(/周|星期|礼拜/, 1000 * 60 * 60 * 24)
  132. .replace(/天|日/, 1000 * 60 * 60 * 24)
  133. .replace(/小时|时/, 1000 * 60 * 60)
  134. .replace(/刻钟|刻/, 1000 * 60 * 15)
  135. .replace(/分钟|分/, 1000 * 60)
  136. .replace(/秒钟|秒/, 1000)
  137. : 1000 * 60; // 不带单位默认分钟
  138. var 活动时长毫秒 = parseInt(单位) * 量;
  139. var 开始 = new Date(
  140. 活动信息.match(/活动开始时间:([-\d]+ [:\d]+).*?/m)[1]
  141. );
  142. var 结束 = new Date(+开始 + 活动时长毫秒);
  143. return { 开始, 结束 };
  144. } else if (活动信息.match(/活动开始时间:([-\d]+ [:\d]+).*?/m)) {
  145. var 开始 = new Date(
  146. 活动信息.match(/活动开始时间:([-\d]+ [:\d]+).*?/m)[1]
  147. );
  148. var 结束 = new Date(+开始 + 1000 * 60 * 90);
  149. console.warn(`未能解析活动时长,默认90分钟`, 活动信息);
  150. return { 开始, 结束 };
  151. } else {
  152. console.warn(`time doesn't match`, 活动信息);
  153. return undefined;
  154. }
  155. };
  156. var 活动时间 = 尝试提取活动时间(活动信息);
  157. // 返回一个事件
  158. return {
  159. // 事件发生时间地点
  160. TSTART: 活动时间.开始,
  161. TEND: 活动时间.结束,
  162. LOCATION: ((e) => e && e[1])(活动信息.match(/活动地点:(.*)/m)),
  163. // 事件UID,用于更新进展
  164. UID:
  165. MD5('第二课堂活动:' + 活动信息.match(/活动编号:(.*)/m)[1]) +
  166. `@snomiao.com`,
  167. // 事件标题
  168. SUMMARY: v_dom.querySelector('h1').innerText,
  169. // 本活动的相关信息
  170. DESCRIPTION:
  171. '活动类型:' +
  172. 活动类型 +
  173. '\n\n' +
  174. 活动信息 +
  175. '\n\n' +
  176. v_dom.querySelector('div.box-1 > div:nth-child(3)').innerText,
  177. };
  178. };
  179. var 取开学时间自课程序号 = (课程序号) => {
  180. // 课程序号
  181. return ((e) => e && e[(课程序号 + '').slice(0, 3)])({
  182. 185: new Date('2018-09-03 00:00:00 GMT+0800'),
  183. 185: new Date('2019-02-25 00:00:00 GMT+0800'),
  184. 190: new Date('2019-09-02 00:00:00 GMT+0800'),
  185. 195: new Date('2020-03-02 00:00:00 GMT+0800'),
  186. 200: new Date('2020-08-31 00:00:00 GMT+0800'),
  187. 205: new Date('2021-02-24 00:00:00 GMT+0800'),
  188. });
  189. };
  190. var 计算课程时间 = (课程时间, 开学时间凌晨) => {
  191. var match = 课程时间
  192. .replace(/第(\d+)周,周(\d+),第(\d+)节/, '第$1周,周$2,第$3-$3节')
  193. .match(/第(\d+)周,周(\d+),第(\d+)-(\d+)节/);
  194. if (!match) {
  195. console.warn('无法识别为时间:', 课程时间, match);
  196. return undefined;
  197. }
  198. var 周数 = match[1];
  199. var 星期 = match[2];
  200. var 上课节 = match[3];
  201. var 下课节 = match[4];
  202. var 第一天时间 = 开学时间凌晨.getTime();
  203. var 一秒 = 1000; // 1秒
  204. var 一分 = 60 * 一秒; // 1分钟
  205. var 一刻 = 15 * 一分; // 1刻钟
  206. var 一时 = 60 * 一分; // 1小时
  207. var 一天 = 24 * 一时; // 1天
  208. var 一周 = 7 * 一天; // 1周
  209. // 上课时间表
  210. // 第 1-2 节 08:20-09:55
  211. // 第 3-4 节 10:15-11:50
  212. // 第 5-6 节 13:00-14:35
  213. // 第 7-8 节 14:55-16:30
  214. // 第 9-11 节 18:00-20:25
  215. var 上课时间表 = {};
  216. 上课时间表['1s'] = 一时 * 8 + 20 * 一分;
  217. 上课时间表['1e'] = 一时 * 9 + 5 * 一分; // 猜的,假设一小节课是45分钟这样子切开
  218. 上课时间表['2s'] = 一时 * 9 + 10 * 一分; // 猜的
  219. 上课时间表['2e'] = 一时 * 9 + 55 * 一分;
  220. 上课时间表['3s'] = 一时 * 10 + 15 * 一分;
  221. 上课时间表['3e'] = 一时 * 11 + 0 * 一分; // 猜的
  222. 上课时间表['4s'] = 一时 * 11 + 5 * 一分;
  223. 上课时间表['4e'] = 一时 * 11 + 50 * 一分;
  224. 上课时间表['5s'] = 一时 * 13 + 0 * 一分;
  225. 上课时间表['5e'] = 一时 * 13 + 45 * 一分; // 猜的
  226. 上课时间表['6s'] = 一时 * 13 + 50 * 一分; // 猜的
  227. 上课时间表['6e'] = 一时 * 14 + 35 * 一分;
  228. 上课时间表['7s'] = 一时 * 14 + 55 * 一分;
  229. 上课时间表['7e'] = 一时 * 15 + 0 * 一分; // 期末实践课的时间好像不按这个来的。。 1-7节
  230. 上课时间表['8s'] = 一时 * 15 + 45 * 一分; // 有的实训课是第 8 节开始上到 17:30 左右,不过先这样写着好了
  231. 上课时间表['8e'] = 一时 * 16 + 30 * 一分;
  232. 上课时间表['9s'] = 一时 * 18 + 0 * 一分;
  233. 上课时间表['9e'] = 一时 * 18 + 45 * 一分;
  234. 上课时间表['10s'] = 一时 * 18 + 50 * 一分;
  235. 上课时间表['10e'] = 一时 * 18 + 55 * 一分; // 9-10节不知几点结束,猜一个18点55吧
  236. 上课时间表['11s'] = 一时 * 19 + 40 * 一分;
  237. 上课时间表['11e'] = 一时 * 20 + 25 * 一分;
  238.  
  239. var 上课时间 =
  240. 第一天时间 +
  241. 一周 * (周数 - 1) +
  242. 一天 * (星期 - 1) +
  243. 上课时间表[上课节 + 's'];
  244.  
  245. var 下课时间 =
  246. 第一天时间 +
  247. 一周 * (周数 - 1) +
  248. 一天 * (星期 - 1) +
  249. 上课时间表[下课节 + 'e'];
  250.  
  251. if (isNaN(上课时间) || isNaN(下课时间)) {
  252. console.warn('无法识别的课时:', 课程时间);
  253. console.warn(上课节, 下课时间);
  254. return undefined;
  255. }
  256. return [上课时间, 下课时间];
  257. };
  258. var 单节考试转日历事件 = (row) => {
  259. try {
  260. var { 序号, 考试课程, 考试时间, 考试地点, 考试性质 } = row;
  261. // hack: 用当前学期貌课程序号前3位代表学期时间
  262. var 本开学时间 = 取开学时间自课程序号('190');
  263. var 本节考试时间 = 考试时间.replace(
  264. /第(\d+)周 星期(\d+) 第(\d+(?:-\d+))节/,
  265. (_, a, b, c) => `第${a}周,周${b},第${c}节`
  266. );
  267. var 考试时间戳 = 计算课程时间(本节考试时间, 本开学时间);
  268.  
  269. return {
  270. // 事件发生时间地点
  271. TSTART: new Date(考试时间戳[0]),
  272. TEND: new Date(考试时间戳[1]),
  273. LOCATION: 考试地点,
  274. // 事件UID,用于更新进展
  275. UID: MD5(`考试: ${考试性质}-${考试课程}`) + `@snomiao.com`,
  276. // 事件标题
  277. SUMMARY: `${考试性质}考: ${考试课程}/${考试地点}/${考试时间}`,
  278. // 考试的相关信息 (直接导出所有键值对)
  279. DESCRIPTION: [...Object.keys(row)]
  280. .map((key) => `${key}: ${row[key]}\n`)
  281. .join(''),
  282. };
  283. } catch (e) {
  284. console.error(`错误:`, e.message, `, 出错数据: `, row, e.stack);
  285. throw new Error(`错误:`, e.message, `, 出错数据: `, row);
  286. }
  287. };
  288. var 单节课转日历事件 = (row) => {
  289. // 范例输入
  290. // 课程序号 课程名称 课程代码 课程类型 课程学分 授课老师 上课时间 上课地点 校区 计划人数 已选人数 挂牌 配课班 备注
  291. // row = {
  292. // 课程序号,
  293. // 课程代码,
  294. // 课程名称,
  295. // 授课老师,
  296. // 学分,
  297. // 本节上课地点,
  298. // 本节上课时间,
  299. // }
  300. var {
  301. 本节上课时间,
  302. 本节上课地点,
  303. 课程序号,
  304. 课程名称,
  305. 课程序号,
  306. 课程代码,
  307. 授课老师,
  308. 学分,
  309. } = row;
  310. try {
  311. var 开学时间 = 取开学时间自课程序号(课程序号);
  312. var 课程时间戳 = 计算课程时间(本节上课时间, 开学时间);
  313. return {
  314. // 事件发生时间地点
  315. TSTART: new Date(课程时间戳[0]),
  316. TEND: new Date(课程时间戳[1]),
  317. LOCATION: 本节上课地点,
  318. // 事件UID,用于更新进展
  319. UID:
  320. MD5(`课程时间: ${课程序号}-${本节上课时间}`) +
  321. `@snomiao.com`,
  322. // 事件标题
  323. SUMMARY: `${课程名称}/${课程序号}/${课程代码}/${授课老师}/${学分}分/${本节上课时间}`,
  324. // 本节课的相关信息 (直接导出所有键值对)
  325. DESCRIPTION: [...Object.keys(row)]
  326. .map((key) => `${key}: ${row[key]}\n`)
  327. .join(''),
  328. };
  329. } catch (e) {
  330. throw new Error(`错误:`, e.message, `, 出错数据: `, row);
  331. }
  332. };
  333. // 矩阵转置
  334. var 转置 = (m) => m[0].map((x, i) => m.map((x) => x[i]));
  335. // 循环直到不动点,这里的调试技巧:进入死循环时可以在此中断,然后修改变量使其报错或跳出
  336. // update: 可配置超时退出
  337. var 循环直到不动点 = function (s, proc_function) {
  338. var o = s;
  339. while (1) {
  340. var tmp = proc_function(o);
  341. if (tmp == o) {
  342. return o;
  343. } else {
  344. o = tmp;
  345. }
  346. }
  347. };
  348. // 把时间表按具体某周、某节课展开成独立元素,便于比较
  349. var 展开课程节数 = function (s) {
  350. // 单双周筛选
  351. var filter_error = function (s) {
  352. return !(
  353. s.length == 0 ||
  354. s.match(/.*?第\d*?[02468]周\*[^\*].*/) ||
  355. s.match(/.*?第\d*?[13579]周\*\*.*/)
  356. );
  357. };
  358. // 单双周统一化
  359. var normalyze = function (s) {
  360. return s.replace(/周\*+/, '周');
  361. };
  362.  
  363. // 化为 \n 分割
  364. s = s.replace(/(?:;|\<br\>)+/g, '\n');
  365. // “展开 a-b 为好多节课”
  366. s = 循环直到不动点(s, (x) =>
  367. x.replace(/(.*?)(\d+)-(\d+)(.*)/, function (s, a, b, c, d) {
  368. var o = '';
  369. var b = parseInt(b);
  370. var c = parseInt(c);
  371. for (var i = b; i <= c; i++) {
  372. o += a + i + d + '\n';
  373. }
  374. return o;
  375. })
  376. );
  377. // “展开 a,b” 为2节课
  378. s = 循环直到不动点(s, (x) =>
  379. x.replace(/(.*?)(\d+),(\d+)(.*)/, function (s, a, b, c, d) {
  380. var o = '';
  381. var b = parseInt(b);
  382. var c = parseInt(c);
  383. o += a + b + d + '\n';
  384. o += a + c + d + '\n';
  385. return o;
  386. })
  387. );
  388. return s.split('\n').filter(filter_error).map(normalyze);
  389. };
  390. // 把连续的2节课合并成一段时间
  391. var 合并连续的节数 = (节列) => {
  392. var lastLength = 节列.length;
  393. 节列.forEach((_, 序) => {
  394. // 防止比较溢出
  395. if (!(序 + 1 < 节列.length)) return;
  396. // 跳过已经被变成undefined 的值
  397. if (!节列[序]) return;
  398. if (!节列[序 + 1]) return;
  399. var match1 = 节列[序].match(/(第\d+周,周\d+,)第(\d+)(?:-(\d+))?节/);
  400. var match2 = 节列[序 + 1].match(
  401. /(第\d+周,周\d+,)第(\d+)(?:-(\d+))?节/
  402. );
  403.  
  404. // 判断是否同周同天
  405. if (!(match1 && match2 && match1[1] == match2[1])) return;
  406.  
  407. // 补全课程结束时间
  408. match1[3] = match1[3] || match1[2];
  409. match2[3] = match2[3] || match2[2];
  410.  
  411. // 判断两节课是否连续
  412. if (parseInt(match1[3]) + 1 == parseInt(match2[2])) {
  413. // 如果连续,把它们头尾相接
  414. var a = match1[2];
  415. var b = match2[3];
  416. var time_concated = `第${a}-${b}节`;
  417. 节列[序] = 节列[序].replace(
  418. /第(\d+)(?:-(\d+))?节/,
  419. time_concated
  420. );
  421. 节列[序 + 1] = undefined;
  422. }
  423. // periods[index] = 0;
  424. });
  425. // 然后过滤掉计算过程中制造的垃圾
  426. 节列 = 节列.filter((x) => x);
  427.  
  428. // 看看有没有合并掉一些课程
  429. if (lastLength == 节列.length) {
  430. return 节列;
  431. } else {
  432. return 合并连续的节数(节列);
  433. }
  434. };
  435. var 拆分课程按时间地点 = (课程) => {
  436. var 分节课程 = [];
  437. // 把时间表分裂掉,这里 课程[6] 是原始时间表
  438. // 把按教室区分的周表分裂掉
  439. var 周期 = 课程.上课时间.split(/;?\n/);
  440. var 地点 = 课程.上课地点.split(',');
  441. if (周期.length != 地点.length) {
  442. if (周期.length > 地点.length) {
  443. // 遇到locations少于periods的情况,就不断重复locations的最后一个元素(可能会有非预期的事情发生)
  444. for (var i = 周期.length - 地点.length - 1; i >= 0; i--) {
  445. 地点 = 地点.concat(地点.slice(-1));
  446. }
  447. } else {
  448. // 遇到locations少于periods的情况,就不断重复locations的最后一个元素(可能会有非预期的事情发生)
  449. for (var i = 地点.length - 周期.length - 1; i >= 0; i--) {
  450. 周期 = 周期.concat(周期.slice(-1));
  451. }
  452. }
  453. }
  454. var 周期地点列 = 转置([周期, 地点]);
  455. var 分节课程 = 周期地点列
  456. .map((pl) => {
  457. var 节数列 = pl[0];
  458. var 地点 = pl[1];
  459. var 节数列 = 展开课程节数(节数列);
  460. var 节数列 = 合并连续的节数(节数列);
  461.  
  462. // 按分裂的时间表展开
  463. return 节数列.map((上课时间) =>
  464. Object.assign({}, 课程, {
  465. 本节上课时间: 上课时间,
  466. 本节上课地点: 地点,
  467. })
  468. );
  469. })
  470. .flat();
  471. return 分节课程;
  472. };
  473. var 课程表离散化 = (课程表_json) =>
  474. 课程表_json.map(拆分课程按时间地点).flat();
  475.  
  476. var 下载单个课程日历 = (课程) => {
  477. var 课程列分节 = 拆分课程按时间地点(课程);
  478. var 输出ics = 日历事件列表转ICS格式(课程列分节.map(单节课转日历事件));
  479. 下载文本文件(
  480. 输出ics,
  481. `${课程.课程序号}-${课程.课程代码}-${课程.课程名称}.ics`
  482. );
  483. };
  484. var 下载多个课程日历 = (课程列表) => {
  485. var 课程列分节 = 课程列表.map(拆分课程按时间地点).flat();
  486. var 输出ics = 日历事件列表转ICS格式(课程列分节.map(单节课转日历事件));
  487. 下载文本文件(输出ics, `课程日历` + new Date().toISOString() + `.ics`);
  488. };
  489. var 下载单个考试日历 = (考试) => {
  490. var 输出ics = 日历事件列表转ICS格式([单节考试转日历事件(考试)]);
  491. 下载文本文件(
  492. 输出ics,
  493. `${考试.考试序号}-${考试.考试代码}-${考试.考试名称}.ics`
  494. );
  495. };
  496. var 下载多个考试日历 = (考试列表) => {
  497. var 输出ics = 日历事件列表转ICS格式(考试列表.map(单节考试转日历事件));
  498. 下载文本文件(输出ics, `考试日历` + new Date().toISOString() + `.ics`);
  499. };
  500. var 日历事件列表转ICS格式 = (事件列表) => {
  501. // .ics方案
  502. var 事件转iCalendar格式的SECTION = (e) => {
  503. // 范例输入
  504. // {
  505. // TSTART,
  506. // TEND,
  507. // SUMMARY,
  508. // DESCRIPTION?,
  509. // LOCATION?,
  510. // UID?,
  511. // }
  512. var icalStrFormat = (s) =>
  513. s.replace(/\n/g, '\\n').replace(/.{40}/g, (c) => c + '\r\n ');
  514. var EVENT_DTSTAMP = icalStrFormat(
  515. new Date().toISOString().replace(/-|:|\.\d+/g, '')
  516. );
  517. var EVENT_DTSTART =
  518. e.TSTART &&
  519. icalStrFormat(e.TSTART.toISOString().replace(/-|:|\.\d+/g, ''));
  520. var EVENT_DTEND =
  521. e.TEND &&
  522. icalStrFormat(e.TEND.toISOString().replace(/-|:|\.\d+/g, ''));
  523. var section_lines = [
  524. `BEGIN:VEVENT`,
  525. //`DTSTART;TZID=Asia/Shanghai:${EVENT_DTSTART}`,
  526. `DTSTAMP:${EVENT_DTSTAMP}`,
  527. `DTSTART:${EVENT_DTSTART}`,
  528. //`DTEND;TZID=Asia/Shanghai:${EVENT_DTEND}`,
  529. `DTEND:${EVENT_DTEND}`,
  530. //`RRULE:FREQ=WEEKLY;COUNT=11;BYDAY=FR`, // 后续升级
  531. `UID:${
  532. (e.UID && icalStrFormat(e.UID)) ||
  533. hash(e.SUMMARY) + '@snomiao.com'
  534. }`,
  535. e.SUMMARY && `SUMMARY:${icalStrFormat(e.SUMMARY)}`,
  536. e.DESCRIPTION && `DESCRIPTION:${icalStrFormat(e.DESCRIPTION)}`,
  537. e.LOCATION && `LOCATION:${icalStrFormat(e.LOCATION)}`,
  538. `END:VEVENT`,
  539. ];
  540. return section_lines.filter((e) => e).join(`\r\n`);
  541. };
  542. // 范例输出
  543. // BEGIN:VEVENT
  544. // DTSTART:20190308T050000Z
  545. // DTEND:20190308T063500Z
  546. // UID:8523315dacd42732383e3f8d07b9cd88@snomiao.com
  547. // SUMMARY:大学生体育测试(一)/1850769/B1230001/张群/0.5分/第2周,周5,第5-6节
  548. // DESCRIPTION:课程序号: 1850769\n课程名称: 大学生体育测试(一)\n课程代码: B
  549. // 1230001\n课程类型: 公共基础课\n课程学分: 0.5\n授课老师: 张
  550. // 群\n上课时间: 第2-5周,周5,第5-6节\n上课地点: 奉贤操场\n校区:
  551. // 奉贤校区\n计划人数: 44\n已选人数: 44\n挂牌: 是\n配课班: 1
  552. // 6101291, 16101261\n备注: \n
  553. // LOCATION:奉贤操场
  554. // END:VEVENT
  555.  
  556. var lines = [
  557. `BEGIN:VCALENDAR`,
  558. `VERSION:2.0`,
  559. `PRODID:-//Snowstar Laboratory//NONSGML Snowstar Calendar//EN`,
  560. `CALSCALE:GREGORIAN`,
  561. // `X-WR-CALNAME:雪星课程表`,
  562. // `X-WR-TIMEZONE:Asia/Shanghai`,
  563. `BEGIN:VTIMEZONE`,
  564. `TZID:Asia/Shanghai`,
  565. // `X-LIC-LOCATION:Asia/Shanghai`,
  566. `BEGIN:STANDARD`,
  567. `TZOFFSETFROM:+0800`,
  568. `TZOFFSETTO:+0800`,
  569. `TZNAME:CST`,
  570. `DTSTART:19700101T000000`,
  571. `END:STANDARD`,
  572. `END:VTIMEZONE`,
  573. `${事件列表.map(事件转iCalendar格式的SECTION).join('\r\n')}`,
  574. `END:VCALENDAR`,
  575. ];
  576. return lines.filter((e) => e).join(`\r\n`) + `\r\n`;
  577. };
  578.  
  579. if (location.hostname.match(/^sc.*\.sit\.edu\.cn$/)) {
  580. // 解析并下载当前页面的日历
  581. var 下载当前活动日历 = async (e) => {
  582. if (e) e.disabled = true;
  583. var href = window.location;
  584. var html = await 异步抓取(href);
  585. var re = 解析第二课堂活动事件(html);
  586. re.DESCRIPTION += '\n' + href;
  587. var 输出ics = 日历事件列表转ICS格式([re]);
  588. 下载文本文件(
  589. 输出ics,
  590. '第二课堂活动:' +
  591. document.querySelector('h1').innerText +
  592. '.ical'
  593. );
  594. if (e) e.disabled = false;
  595. };
  596.  
  597. var 当前页面活动列表日历 = async (e) => {
  598. if (e) e.disabled = true;
  599. // 获取当前页面上所有活动详情的URL;
  600. var hrefs = [...document.querySelectorAll('a')]
  601. .map((a) => a.href)
  602. .filter((href) => !!href.match('activityDetail.action'));
  603.  
  604. // 异步下载所有事件
  605. var 事件列 = [];
  606. await Promise.all(
  607. hrefs.map(async (href) => {
  608. var html = await 异步抓取(href);
  609. var 事件 = 解析第二课堂活动事件(html);
  610. 事件.DESCRIPTION += '\n' + href;
  611. 事件列.push(事件);
  612.  
  613. // 全部抓取完成之后保存本地
  614. if (事件列.length == hrefs.length) {
  615. var 输出ics = 日历事件列表转ICS格式(事件列);
  616. 下载文本文件(
  617. 输出ics,
  618. document.querySelector('title').innerText + '.ical'
  619. );
  620. }
  621. })
  622. );
  623. if (e) e.disabled = false;
  624. };
  625.  
  626. var 下载近期所有第二课堂活动日历 = async (e) => {
  627. if (e) e.disabled = true;
  628. // TODO: 进度条
  629. // var caption = e.innerText
  630. // e.innerText = caption + "" +
  631. // var doneCount =
  632. var all_events = [];
  633.  
  634. var 获取当前页面第二课堂分类列表 = () => {
  635. var reg =
  636. /\/public\/activity\/activityList\.action\?categoryId=(.*)/;
  637. return [...document.querySelectorAll('#dekt-nav a[href]')]
  638. .map((a) => {
  639. var match = a.href.match(reg);
  640.  
  641. return (
  642. match && {
  643. categoryId: match[1],
  644. actType: a.innerText,
  645. }
  646. );
  647. })
  648. .filter((e) => e);
  649. };
  650.  
  651. var 第二课堂分类列表 = 获取当前页面第二课堂分类列表();
  652. // 异步列出每个分类最近的50个活动,并等待全部返回
  653. await Promise.all(
  654. 第二课堂分类列表.map(async ({ categoryId, actType }) => {
  655. var url = `/public/activity/activityList.action?pageNo=1&pageSize=50&categoryId=${categoryId}`;
  656. // 获取当前分类的活动链接(列表)
  657. var v_dom = document.createElement('html');
  658. v_dom.innerHTML = await 异步抓取(url);
  659. var hrefs = [...v_dom.querySelectorAll('a')]
  660. .map((a) => a.href)
  661. .filter(
  662. (href) => !!href.match('activityDetail.action')
  663. );
  664.  
  665. // 异步下载所有活动,并等待全部下载完成
  666. var events = await Promise.all(
  667. hrefs.map(async (href) => {
  668. var html = await 异步抓取(href);
  669. var event = 解析第二课堂活动事件(html);
  670. event.DESCRIPTION += '\n' + href;
  671.  
  672. // 除此之外还要标出活动类型
  673. event.SUMMARY = actType + '/' + event.SUMMARY;
  674. event.DESCRIPTION =
  675. '活动类型: ' +
  676. actType +
  677. '\n\n' +
  678. event.DESCRIPTION;
  679. return event;
  680. })
  681. );
  682. // 收集
  683. all_events = all_events.concat(events);
  684. })
  685. );
  686.  
  687. // 转成ical格式并触发下载
  688. var 输出ics = 日历事件列表转ICS格式(all_events);
  689. 下载文本文件(
  690. 输出ics,
  691. '[' +
  692. new Date()
  693. .toISOString()
  694. .replace(/[^0-9]/g, '')
  695. .substr(0, 8) +
  696. ']近期第二课堂活动.ical'
  697. );
  698.  
  699. if (e) e.disabled = false;
  700. };
  701.  
  702. var 诚信积分一键加满 = async (e) => {
  703. if (e) e.disabled = true;
  704. // 获取当前分类的活动申请编号(列表)
  705. var v_dom = document.createElement('html');
  706. v_dom.innerHTML = await 异步抓取(
  707. 'http://sc.sit.edu.cn/public/pcenter/activityOrderList.action?pageNo=1&pageSize=999999999'
  708. );
  709. var lsOrderId = [
  710. ...v_dom.querySelectorAll('form td:nth-child(1)>a'),
  711. ].map((e) => parseInt(e.innerText));
  712. await Promise.all(
  713. lsOrderId.map(async (oid) => {
  714. // 五分好评666!
  715. var assess = 100;
  716. var content = '666';
  717. var actityOrderId = oid;
  718. await 异步抓取(
  719. `http://sc.sit.edu.cn/public/pcenter/assess.action?assess=${assess}&content=${content}&actityOrderId=${actityOrderId}`
  720. );
  721. })
  722. );
  723. console.log(lsOrderId.length + '个活动已加满');
  724. if (e) e.disabled = false;
  725. };
  726.  
  727. // 显示工具栏
  728. var flag_已有工具栏 = false;
  729. var 生成工具栏 = () => {
  730. var 容器 = document.querySelector('.user-info');
  731. if (!容器 || flag_已有工具栏) return;
  732.  
  733. var 工具栏元素列表 = [
  734. 新元素(
  735. `<a href="http://sc.sit.edu.cn/public/activity/activityList.action?pageNo=1&pageSize=200&categoryId=&activityName=">查看最近的200个活动</a>`
  736. ),
  737. window.location.href.match('activityDetail.action')
  738. ? 绑定Click到元素(
  739. 下载当前活动日历,
  740. 新元素(`<button>下载 当前事件.ical</button>`)
  741. )
  742. : 绑定Click到元素(
  743. 当前页面活动列表日历,
  744. 新元素(`<button>下载 当前页面活动列表.ical</button>`)
  745. ),
  746. 绑定Click到元素(
  747. 下载近期所有第二课堂活动日历,
  748. 新元素(`<button>下载 近期所有第二课堂活动.ical</button>`)
  749. ),
  750. 绑定Click到元素(
  751. 诚信积分一键加满,
  752. 新元素(`<button>诚信积分一键加满!</button>`)
  753. ),
  754. ];
  755. var 工具栏 = 新元素('<span></span>');
  756. 工具栏元素列表.map((e) => 工具栏.appendChild(e));
  757.  
  758. 容器.append(工具栏);
  759. flag_已有工具栏 = true;
  760. };
  761. window.addEventListener('load', 生成工具栏);
  762. 生成工具栏();
  763. }
  764.  
  765. // 选课页面
  766. var 绑定点击 = (元素, 描述, f) => {
  767. 元素.setAttribute('title', 描述);
  768. 元素.classList.add('clickable');
  769. 元素.onclick = f;
  770. };
  771. if (
  772. location.hostname.match(/^ems.*\.sit\.edu\.cn$/) &&
  773. location.pathname == '/student/selCourse/mycourselist.jsp'
  774. ) {
  775. var 可以选班级列表 = [...document.querySelectorAll(`tr`)].filter(
  776. (x) => x.querySelectorAll(`td`).length == 14
  777. );
  778. 可以选班级列表.map((tr) => {
  779. var 单元格表 = [...tr.querySelectorAll('td')];
  780. var [
  781. 课程序号,
  782. 课程名称,
  783. 课程代码,
  784. 课程类型,
  785. 学分,
  786. 授课老师,
  787. 上课时间,
  788. 上课地点,
  789. 校区,
  790. 计划人数,
  791. 已选人数,
  792. 挂牌,
  793. 配课班,
  794. 备注,
  795. ] = 单元格表.map((e) => e.innerText.trim());
  796. var 课程 = {
  797. 课程序号,
  798. 课程名称,
  799. 课程代码,
  800. 课程类型,
  801. 学分,
  802. 授课老师,
  803. 上课时间,
  804. 上课地点,
  805. 校区,
  806. 计划人数,
  807. 已选人数,
  808. 挂牌,
  809. 配课班,
  810. 备注,
  811. };
  812.  
  813. 绑定点击(单元格表[6], '点击下载本课程日历', () => {
  814. 下载单个课程日历(课程);
  815. });
  816. });
  817. }
  818.  
  819. if (location.pathname == '/student/selCourse/list1.jsp') {
  820. var lsClasses = [...document.querySelectorAll(`tr[align="center"]`)];
  821. lsClasses.map((e) => {
  822. var 单元格表 = [...e.querySelectorAll('td')];
  823. var [
  824. 课程序号,
  825. 课程名称,
  826. 课程代码,
  827. 学分,
  828. 授课老师,
  829. 上课时间,
  830. 上课地点,
  831. 选课类型,
  832. 选课结果,
  833. 操作,
  834. ] = 单元格表.map((e) => e.innerText.trim());
  835. var 课程 = {
  836. 课程序号,
  837. 课程名称,
  838. 课程代码,
  839. 学分,
  840. 授课老师,
  841. 上课时间,
  842. 上课地点,
  843. 选课类型,
  844. 选课结果,
  845. 操作,
  846. };
  847.  
  848. 单元格表[9].innerHTML = `
  849. <a style="color:red" href="#" onclick="tj(2,'${课程序号}','${课程代码}')">取消选择</a><br>
  850. <a style="color:red" href="/student/selCourse/action.jsp?op=del&courseID=${课程代码}&cssID=${课程序号}&url=/student/selCourse/studentSel/teachclasslist.jsp?courseId=${课程代码}" target="_BLANK" alt="有可能选不上">退课重选(慎用)</a>
  851. `;
  852. 绑定点击(单元格表[5], '点击下载本课程日历', () => {
  853. 下载单个课程日历(课程);
  854. });
  855. });
  856.  
  857. var 表头行 = document.querySelector(`form tr`);
  858. var 单元格表 = [...表头行.querySelectorAll('th')];
  859. 绑定点击(单元格表[5], '点击下载多个课程日历', () => {
  860. var 课程列表 = lsClasses.map((e) => {
  861. var 单元格表 = [...e.querySelectorAll('td')];
  862. var [
  863. 课程序号,
  864. 课程名称,
  865. 课程代码,
  866. 学分,
  867. 授课老师,
  868. 上课时间,
  869. 上课地点,
  870. 选课类型,
  871. 选课结果,
  872. 操作,
  873. ] = 单元格表.map((e) => e.innerText.trim());
  874. return {
  875. 课程序号,
  876. 课程名称,
  877. 课程代码,
  878. 学分,
  879. 授课老师,
  880. 上课时间,
  881. 上课地点,
  882. 选课类型,
  883. 选课结果,
  884. 操作,
  885. };
  886. });
  887. 下载多个课程日历(课程列表);
  888. });
  889. }
  890.  
  891. // 考试列表
  892. if (location.pathname == '/student/main.jsp') {
  893. var 表格 = [...document.querySelectorAll('table')].filter((tab) =>
  894. tab.innerHTML.match('我的考试')
  895. );
  896. var 表格 = 表格 && 表格[0];
  897. var = [...表格.querySelectorAll('tr')]
  898. .map((tr) => [...tr.querySelectorAll('td')])
  899. .filter((e) => e.length == 5);
  900. 绑定点击(阵[0][2], '点击下载所有考试日历', () => {
  901. var 考试列表 = 阵.slice(1).map((单元格行) => {
  902. var [序号, 考试课程, 考试时间, 考试地点, 考试性质] =
  903. 单元格行.map((e) => e.innerText.trim());
  904. return { 序号, 考试课程, 考试时间, 考试地点, 考试性质 };
  905. });
  906. 下载多个考试日历(考试列表);
  907. });
  908. 阵.slice(1).map((单元格行) => {
  909. var [序号, 考试课程, 考试时间, 考试地点, 考试性质] = 单元格行.map(
  910. (e) => e.innerText.trim()
  911. );
  912. var 考试 = { 序号, 考试课程, 考试时间, 考试地点, 考试性质 };
  913. 绑定点击(单元格行[2], '点击下载本考试日历', () => {
  914. 下载单个考试日历(考试);
  915. });
  916. });
  917. }
  918. document.body.appendChild(
  919. 新元素('<style>.clickable{cursor: pointer}</style>')
  920. );
  921. })();