// ==UserScript==
// @name BIT-物理实验中心-实验选修
// @namespace http://tampermonkey.net/
// @version 1.2.0
// @description 给满员、冲突的课程自动上色
// @license GPL-3.0-or-later
// @supportURL https://github.com/YDX-2147483647/BIT-enhanced/issues
// @author Y.D.X.
// @match http://10.133.22.200:7100/XPK/StuCourseElective
// @grant none
// ==/UserScript==
// 修改函数`my_conflict_referee()`中的变量`bans`,或者完全重构`my_conflict_referee()`
(function () {
'use strict'
/**
* 判定是否冲突
* @param {Course} course
* @returns {boolean}
*/
function my_conflict_referee (course) {
// 如果您看不懂这段代码,也许读读`BIT-物理实验中心-实验选修.md`会有帮助。
if (course.class_time.week >= 8 && course.class_time.week <= 11 &&
course.class_time.day === 3 && course.class_time.section === 5) {
return true
}
/**
* 不能上的时间
* @type {number[][]} [[星期几(1-7), 第几大节(1-5)]]
*/
const bans = [
[1, 2], [1, 4],
[2, 2], [2, 3], [2, 4], [2, 5],
[3, 2], [3, 3],
[4, 1], [4, 2], [4, 3],
[5, 2]
]
return bans.find(([day, section]) =>
day === course.class_time.day && section === course.class_time.section)
}
/**
* 课程类别,用于 CSS class
*/
const Categories = {
full: 'full-course',
conflict: 'conflict-course',
available: 'available-course'
}
function add_style_sheet () {
const sheet = document.createElement('style')
sheet.innerHTML = `
tr.${Categories.full} {
background-color: #FF000040;
}
tr.${Categories.conflict} {
background-color: yellow;
}
.datagrid-view1 tr.${Categories.conflict}.${Categories.full} {
background-color: #FF000040;
}
.datagrid-view2 tr.${Categories.conflict}.${Categories.full} {
background: linear-gradient(to left, yellow, 20%, #FF000040);
}
tr.${Categories.available} {
background-color: greenyellow;
}
`
document.head.appendChild(sheet)
}
/**
* 解析汉字数字
* @description 只支持一位数
* @param {string} s
* @returns {number}
*/
function parse_int_zh (s) {
return '零一二三四五六七八九'.indexOf(s)
}
/**
* 解析时间
* @param {string} time
* @returns {number[]}
*/
function parse_time (time) {
return time.split(':').map(x => Number(x))
}
/**
* 选课表格的一行
* @description 因为选课表格设计得太奇怪了:分明只有一个表格,实际却是四个`<table>`……
*/
class Course {
/**
* 解析形如“2/3”的比例
* @param {string} ratio
* @returns {number[]}
*/
static parse_ratio (ratio) {
return ratio.split('/').map(x => Number(x))
}
/**
* 解析上课时间
* @param {string} time 例如“第8周星期一上午第二大节2021-10-11 09:55-12:10”
*/
static parse_class_time (time) {
const match_obj = time.match(
/第(?<week>\d+)周星期(?<day>[一二三四五六日])(上午|下午|晚上)第(?<section>[一二三四五])大节(?<date>[-\d]{10}) (?<start>[:\d]{5})-(?<end>[:\d]{5})/)
if (!match_obj) {
throw Error(`无法识别上课时间:${time}。`)
}
const groups = match_obj.groups
return {
week: Number(groups.week),
day: '一二三四五六日'.indexOf(groups.day) + 1,
section: parse_int_zh(groups.section),
date: new Date(groups.date),
start: parse_time(groups.start),
end: parse_time(groups.end)
}
}
/**
*
* @param {HTMLElement} left view1 中的半行
* @param {HTMLElement} right view2 中的另外半行
*/
constructor (left, right) {
/** 实际元素
* @type {HTMLElement[]} */
this._row = [left, right]
/** 已选人数
* @type number */
this.student_count = 0
/** 课程容量
* @type number */
this.capacity = 0;
[this.student_count, this.capacity] = Course.parse_ratio(this._row[1]
.querySelector("td[field='ElectivedNum']").textContent)
/** 上课时间
* @type {{
week: number, day: number, section: number, date: Date, start: number[], end: number[]
}} */
this.class_time = Course.parse_class_time(this._row[1]
.querySelector("td[field='GradeValue'], td[field='value1']").textContent)
}
/**
* 设置类别
* @param {string[]} categories
*/
_set_category (...categories) {
this._row.forEach(side => {
side.classList.add(...categories)
})
}
/**
* 去除类别(恢复默认)
*/
_remove_category () {
this._row.forEach(side => {
side.classList.remove(...Object.values(Categories))
})
}
/**
* 根据内容更新类别
* @param {(course: Course) => boolean} conflict_referee 判定是否冲突
*/
update_category (conflict_referee = () => false) {
this._remove_category()
const is_full = this.student_count >= this.capacity
const is_conflict = conflict_referee(this)
if (is_full) {
this._set_category(Categories.full)
}
if (is_conflict) {
this._set_category(Categories.conflict)
}
if (!is_full && !is_conflict) {
this._set_category(Categories.available)
}
}
};
/**
* 获取课程列表
* @returns {Course[]}
*/
function get_courses () {
const [left_side, right_side] = [1, 2]
.map(n => `#tt .datagrid-view${n} .datagrid-body tbody tr`)
.map(selector => Array.from(document.querySelectorAll(selector)))
return left_side.map((left, index) => new Course(left, right_side[index]))
}
function paint () {
const courses = get_courses()
courses.forEach(c => c.update_category(my_conflict_referee))
}
async function add_painter () {
const observer_config = {
attributes: false,
childList: true,
subtree: true
}
// 等待“实验选修”按钮出现
const button = await new Promise((resolve, reject) => {
const button_observer = new MutationObserver(() => {
// 是的,网页里的确是“lable”而非“label”
const _button = document.querySelector('.panel.datagrid .datagrid-view2 .datagrid-body table lable:first-child')
if (_button) {
button_observer.disconnect()
resolve(_button)
}
})
button_observer.observe(document.body, observer_config)
})
// 单击“实验选修”后稍等,会出现选课列表,这时再开始自动上色
button.addEventListener('click', () => {
const observer = new MutationObserver(() => {
if (document.querySelector('.panel-title')) {
new MutationObserver(paint).observe(document.querySelector('#dialogChooseCourses .datagrid-view'), observer_config)
observer.disconnect()
}
})
observer.observe(document.body, observer_config)
})
}
add_style_sheet()
add_painter()
})()