Greasy Fork

Greasy Fork is available in English.

电子科技大学教务助手

电子科技大学教务助手帮你更便捷地使用教务系统

当前为 2021-07-13 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         电子科技大学教务助手
// @namespace    http://shawroger.gitee.io/
// @version      0.0.6
// @description  电子科技大学教务助手帮你更便捷地使用教务系统
// @author       shawroger
// @match        *://eams.uestc.edu.cn/eams/*
// @require   	 https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js
// @require   	 https://cdn.bootcdn.net/ajax/libs/highcharts/9.1.2/highcharts.min.js
// @icon         https://www.uestc.edu.cn/favicon.ico
// ==/UserScript==

/**
 * 获取用户数据
 * 只用于数据处理
 * 不会上传数据,可以审查代码证明
 */
const user = {
	id: "", // 学号
	name: "", // 姓名
	home: "http://eams.uestc.edu.cn/eams",
};

/**
 * 监听页面异常时排除指定页面
 */
const crashExcluder = [
	"stdElectCourse",
	"security/my.action",
	"teach/grade/usual/usual-grade-std!usualInfo.action",
];

/**
 * 配置全局定时器时间间隔
 */
const interval = {
	toHome: 0,
	renderScore: 600,
	crashListen: 1000,
	globalListener: 200,
};

/**
 * 配置 window.localStorage 参数
 */
const storage = [
	/**
	 *  配置 是否显示顶部栏目
	 */
	{
		key: "UESTC_STUDYSYS_HELPER_SHOWBAR_ALL",
		defaultVal: 1,
		val: 1,
	},
	/**
	 * 配置 是否显示成绩图表
	 */
	{
		key: "UESTC_STUDYSYS_HELPER_SHOW_SCORE_CHART",
		defaultVal: 1,
		val: 1,
	},
];

/**
 * 配置 是否继续循环
 */
const loop = {
	/**
	 * 循环监听渲染 计划页面
	 */
	renderPlan: true,
	/**
	 * 循环监听渲染 成绩页面
	 */
	renderScore: true,
};

/**
 * 配置首页导航
 */
const navs = [
	{
		text: "查询平时成绩",
		img: "https://pic.imgdb.cn/item/60ead7fd5132923bf8f5b136.png",
		href: "http://eams.uestc.edu.cn/eams/teach/grade/usual/usual-grade-std.action",
	},
	{
		text: "查询我的成绩",
		img: "https://pic.imgdb.cn/item/60ead77f5132923bf8f3f56b.png",
		href: "http://eams.uestc.edu.cn/eams/teach/grade/course/person.action",
	},
	{
		text: "查询我的计划",
		img: "https://pic.imgdb.cn/item/60eadacf5132923bf8ffcca2.png",
		href: "http://eams.uestc.edu.cn/eams/myPlanCompl.action",
	},
];

/**
 * 配置成绩表格背景颜色
 * 以分数段染色
 */
const scoreColors = [
	{
		range: [85, 101],
		background: "LightSeaGreen",
		color: "",
		level: 1,
	},
	{
		range: [70, 85],
		background: "LawnGreen",
		color: "",
		level: 2,
	},
	{
		range: [60, 70],
		background: "NavajoWhite",
		color: "",
		level: 3,
	},
	{
		range: [-1, 60],
		background: "IndianRed",
		color: "white",
		level: 4,
	},
];

/**
 * 配置成绩表格背景颜色
 * 以关键词染色
 */
const keywColors = [
	{
		words: ["P", "A", "通过"],
		background: "LightSeaGreen",
		color: "",
		index: -3,
		level: 1,
	},
	{
		words: ["否"],
		background: "IndianRed",
		color: "white",
		index: -2,
		level: 4,
	},
];

/**
 *  成绩记录类
 *  由于生成图表
 */
class Recorder {
	constructor() {
		this.v1 = 0; // 第一等级
		this.v2 = 0; // 第二等级
		this.v3 = 0; // 第三等级
		this.bad = 0; // 未合格
	}

	/**
	 * 对成绩等级进行计数
	 * @param {number} 配置的 level
	 */
	add(level) {
		if (level === 1) {
			this.v1++;
		} else if (level === 2) {
			this.v2++;
		} else if (level === 3) {
			this.v3++;
		} else if (level === 4) {
			this.bad++;
		}

		if (this.onChange) {
			this.onChange();
		}
	}

	/**
	 * 生成 highchart 图表配置数据
	 * @returns
	 */
	chart() {
		const config = {
			title: {
				text: `${user.name.length > 0 ? user.name + "的" : ""}成绩分布图`,
			},
			chart: {
				plotBackgroundColor: null,
				plotBorderWidth: null,
				plotShadow: false,
				backgroundColor: "#f2f2f2",
			},
			tooltip: {
				pointFormat: "共 {point.y} 门课程 {point.percentage:.1f}%",
			},
		};

		const plotOptions = {
			pie: {
				allowPointSelect: true,
				cursor: "pointer",
				dataLabels: {
					enabled: true,
					format: "{point.name} {point.percentage:.1f} %",
					style: {
						color:
							(Highcharts.theme && Highcharts.theme.contrastTextColor) ||
							"black",
					},
				},
			},
		};

		const series = [
			{
				type: "pie",
				data: [
					["优秀", this.v1],
					["较好", this.v2],
					["一般", this.v3],
					["未完成", this.bad],
				],
			},
		];

		config.series = series;
		config.plotOptions = plotOptions;

		return config;
	}

	/**
	 * 读取成绩所在行的信息
	 * @param {any} tr 成绩所在行
	 * @param {string} text 成绩数据文字
	 */
	info(_tr, _text) {
		// todo!()
	}
}

/**
 * 创建一个新的 Recorder 类
 * @returns
 */
function createRecorder() {
	return new Recorder();
}

/**
 * 初始化全局数据
 */
function readUser() {
	const userdom = $("a[title=查看登录记录]").text();
	const index = userdom.indexOf("(");
	user.name = userdom.slice(0, index);
	user.id = userdom.slice(index + 1, userdom.length - 1);
}

/**
 *
 * @returns  返回localStorage配置列表
 */
function initStorage() {
	const safeParseInt = (key) => {
		const val = window.localStorage.getItem(key);
		if (typeof val === "number") {
			// Maybe NaN itself
			return val;
		} else if (typeof val === "string") {
			return Number(val);
		} else {
			return NaN;
		}
	};

	for (let i = 0; i < storage.length; i++) {
		const { key, defaultVal } = storage[i];
		const v = safeParseInt(key);
		if (isNaN(v)) {
			storage[i].val = defaultVal;
			window.localStorage.setItem(key, String(defaultVal));
		} else {
			storage[i].val = v;
		}
	}
}

/**
 *
 * @param {string | string[]} url 地址栏关键词
 * @returns 当前地址是否包含指定字符串
 */
function includeUrl(url) {
	if (Array.isArray(url)) {
		for (const u of url) {
			if (window.location.href.includes(u)) {
				return true;
			}
		}
	} else {
		return window.location.href.includes(url);
	}
	return false;
}

/**
 * 直接跳转首页
 */
function toHome(interv) {
	if (!interv || interv < 100) {
		window.location.href = user.home;
		return;
	}

	setTimeout(() => (window.location.href = user.home), interv);
}

/**
 * 注入全局 CSS 样式
 */
function injectCSS() {
	const head = $("head");
	const css = `
	<style type="text/css">
		.uestc-helper-box {
			padding: 5px 10px;
			background: whiteSmoke;
			display: flex;
			justify-content: space-between;
			border-bottom: solid grey thin;
		}

		.btn-group button {
			margin-right: 15px;
		}

		#helper-text {
			font-size: 16px;
			padding-left: 10px;
		}

		.list_box_1 ul .li_1 {
			margin-bottom: 20px;
		}

		.btn-group {
			display: flex;
			justify-content: center;
			align-items: center;
			padding: 5px;
		}

		#ush_chart_box {
			width: 100%;
			height: auto;
			background: #f2f2f2; 
		}
	</style>`;
	head.append(css);
}

/**
 * 绑定全局按钮事件
 */
function injectEvent() {
	$("a[href='/eams/home!index.action']").click(toHome);

	$("#reset_btn").click(() => {
		storage.forEach(({ key, defaultVal }) => {
			window.localStorage.setItem(key, JSON.stringify(defaultVal));
		});
		toHome();
	});

	$("#show_btn").click(() => {
		$(".uestc-helper-box").show();
		window.localStorage.setItem(storage[0].key, "1");
		toHome(interval.toHome);
	});

	$("#close_btn").click(() => {
		$(".uestc-helper-box").hide();
		window.localStorage.setItem(storage[0].key, "0");
		toHome(interval.toHome);
	});

	$("#chart_btn").click(() => {
		window.localStorage.setItem(storage[1].key, storage[1].val > 0 ? "0" : "1");
		toHome(interval.toHome);
	});

	$("#position_bar span.secondMenu a").click(() => {
		$("#ush_chart_box").hide();
		loop.renderScore = true;
	});

	$(".toolbar-item-ge0 toolbar-item").click(() => {
		renderScore();
		loop.renderScore = true;
	});

	$("input[value='切换学期']").click(() => {
		renderScore();
		loop.renderScore = true;
	});
}

/**
 * 注入 HTML 代码
 */
function injectHTML() {
	if (storage[0].val > 0) {
		const html = `
		<div class="uestc-helper-box">
			<p id="helper-text">欢迎使用<a 
			target="_blank"
			href="http://greasyfork.icu/zh-CN/scripts/429207-%E7%94%B5%E5%AD%90%E7%A7%91%E6%8A%80%E5%A4%A7%E5%AD%A6%E6%95%99%E5%8A%A1%E5%8A%A9%E6%89%8B">
			电子科技大学教务助手
		</a></p>
			<div class="btn-group">
				<button id="chart_btn">图表功能:${storage[1].val > 0 ? "开" : "关"}</button>
				<button id="reset_btn">重置助手</button>
				<button id="close_btn">隐藏</button>
			</div>
		</div>`;

		$("body").prepend(html);
	} else {
		const html = `<input type="button" id="show_btn" value="显示助手">`;
		$("form[action='/eams/home.action']").append(html);
	}
}

/**
 * 监听教务系统
 * 出现异常直接跳转首页
 */
function watchCrash() {
	if (includeUrl(crashExcluder)) {
		return;
	}

	// 检查关键 dom 是否存在

	if ($("#position_bar").length === 0 && $("#Nav_bar").length === 0) {
		$("#helper-text").css("color", "red");
		$("#helper-text").text("教务系统页面显示异常,即将自动重置");

		// 等待跳回
		setTimeout(toHome, interval.crashListen);
	}
}

/**
 * 调整首页布局
 */
function initLayout() {
	$("img[src = '/eams/avatar/my.action']").hide();
	if (includeUrl("home!submenus.action?")) {
		const node = $("div.list_box_1 li.li_1").last().clone(true);

		navs.forEach(({ text, img, href }) => {
			node.find("h3").text(text);
			node.find("a").attr("href", href);
			node.find("div").css("background", `url("${img}")  no-repeat 50% 50%`);

			$("div.list_box_1 li.li_1")
				.last()
				.parent()
				.append(`<li class="li_1">` + node.html() + `</li>`);
		});
	}
}

/**
 * 加入 footer 内容
 */
function injectFooter() {
	const footer = $("#BottomBg");
	footer.html(
		footer.text() +
			`<br/><br/> 
			页面启用了
			<span style="color: orange">
				<a 
					target="_blank"
					style="color: orange"
					href="http://greasyfork.icu/zh-CN/scripts/429207-%E7%94%B5%E5%AD%90%E7%A7%91%E6%8A%80%E5%A4%A7%E5%AD%A6%E6%95%99%E5%8A%A1%E5%8A%A9%E6%89%8B">
					电子科技大学教务助手
				</a></span> 
				made by shawroger`
	);
}

/**
 * 通过分数区间来染色
 * @param {Recorder} recorder
 * @param {any} tr
 * @param {string} text
 * @returns void
 */
function paintScore(recorder, tr, text) {
	const score = parseInt(text);

	for (const { color, range, background, level } of scoreColors) {
		if (range[0] <= score && score < range[1]) {
			recorder.add(level);
			recorder.info(tr, text);
			tr.css("background", background);

			if (color.length > 1) {
				tr.find("td").each((_, e) => {
					$(e).css("color", color);
					if ($(e).children("a").length > 0) {
						$(e).children("a").css("color", color);
					}
				});
			}

			return true;
		}
	}

	return false;
}

/**
 * 通过关键词来染色
 * @param {Recorder} recorder
 * @param {any} tr
 * @param {string} text
 * @returns void
 */
function paintKeywd(recorder, tr, text) {
	function t_(words, text) {
		for (const word of words) {
			if (text === word) {
				return true;
			}
		}
		return false;
	}
	for (const { color, words, background, index, level } of keywColors) {
		if (t_(words, text || tr.find("td").eq(index).text().trim())) {
			recorder.add(level);
			recorder.info(tr, text);
			tr.css("background", background);

			if (color.length > 1) {
				tr.find("td").each((_, e) => {
					$(e).css("color", color);
					if ($(e).children("font").length > 0) {
						$(e).children("font").attr("color", color);
					}

					if ($(e).children("a").length > 0) {
						$(e).children("a").css("color", color);
					}
				});
			}

			return true;
		}
	}

	return false;
}

/**
 * 渲染 计划完成情况 页面
 * @returns
 */
function renderPlan() {
	if (!includeUrl("myPlanCompl.action")) {
		return;
	}

	$("#ush_chart_box").show();
	if ($("#ush_chart_box").length === 0) {
		$("#main").before(`<div id="ush_chart_box"></div>`);
	}

	loop.renderPlan = false;
	const table = $("table.formTable");
	const recordBook = createRecorder();

	table.find("tr").each((_, e) => {
		const tr = $(e);
		const text = tr.find("td").eq(-3).text().trim();

		if (!paintScore(recordBook, tr, text)) {
			paintKeywd(recordBook, tr);
		}
	});

	if (storage[1].val > 0) {
		$("#ush_chart_box").highcharts(recordBook.chart());
		removeHightchartCredit();
	}
}

/**
 * 渲染 我的成绩 页面
 * @returns
 */
function renderScore() {
	if (!includeUrl(["teach/grade/course/person", "teach/grade/usual"])) {
		return;
	}
	$("#ush_chart_box").show();
	if ($("#ush_chart_box").length === 0) {
		$("#main").before(`<div id="ush_chart_box"></div>`);
	}

	$("div[title='所有学期成绩']").click(() => {
		loop.renderScore = true;
		console.log("loopVars.renderScore = true");
	});

	const recordBook = createRecorder();

	const table = $("table.gridtable").last();

	table.find("tr").each((_, e) => {
		const tr = $(e);
		const text = includeUrl("historyCourseGrade")
			? tr.find("td").last().text().trim()
			: tr.find("td").eq(-2).text().trim();
		if (!paintScore(recordBook, tr, text)) {
			paintKeywd(recordBook, tr, text);
		}
	});

	// 停止监听
	if (storage[1].val > 0) {
		setTimeout(() => {
			loop.renderScore = false;
			if ($("#ush_chart_box").length > 0) {
				$("#ush_chart_box").highcharts(recordBook.chart());
				removeHightchartCredit();
			}
		}, interval.renderScore);
	}
}

/**
 * 主任务
 */
function mainAction() {
	// 初始化数据
	readUser();
	initStorage();

	// 初始 DOM 部分
	injectCSS();
	injectHTML();
	initLayout();
	injectFooter();

	// 初始 事件部分
	watchCrash();
	renderPlan();
	renderScore();

	/**
	 * 配置 Highcharts 的主颜色
	 */
	Highcharts.setOptions({
		colors: scoreColors.map(({ background }) => background),
	});
}

/**
 * 删除 highchart 的 credit
 */
function removeHightchartCredit() {
	$(".highcharts-credits").hide();
}

/**
 * 循环监听任务
 */
function loopAction() {
	watchCrash();
	injectEvent();

	if (loop.renderPlan) {
		renderPlan();
	}

	if (loop.renderScore) {
		renderScore();
	}
}

(function () {
	"use strict";
	mainAction();

	setInterval(() => {
		loopAction();
	}, interval.globalListener);
})();