Greasy Fork

Greasy Fork is available in English.

电子科技大学教务助手

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         电子科技大学教务助手
// @namespace    http://shawroger.gitee.io/
// @version      0.1.0
// @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 itv = {
	app: 500,
	toHome: 0,
	crasher: 500,
	renderPlan: 400,
	renderScore: 400,
	crasherForTable: 800,
};

/**
 * 配置 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 text = function () {
			if (includeUrl("myPlanCompl.action")) {
				return "计划完成分布图";
			} else if (includeUrl("teach/grade/usual")) {
				return "平时成绩分布图";
			} else if (includeUrl("teach/grade/course/person!historyCourseGrade")) {
				return "所有成绩分布图";
			} else {
				return "成绩分布图";
			}
		};
		const config = {
			title: {
				text: `${user.name.length > 0 ? user.name + "的" : ""}${text()}`,
			},
			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() {
		// todo
		// waiting for v0.1.x
	}
}

/**
 * 判断指定元素是否 `不存在`
 * @param {string | object} selector 选择器
 * @returns
 */
function notfound(selector) {
	if (typeof selector === "string") {
		return $(selector).length === 0;
	}

	return selector.length === 0;
}

/**
 * 初始化全局数据
 */
function initUser() {
	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 parse = (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 = parse(key);
		if (isNaN(v)) {
			storage[i].val = defaultVal;
			window.localStorage.setItem(key, String(defaultVal));
		} else {
			storage[i].val = v;
		}
	}
}

/**
 *
 * @param {string | string[]} address 地址栏关键词
 * @returns 当前地址是否包含指定字符串
 */
function includeUrl(address) {
	if (typeof address === "string") {
		return window.location.href.includes(address);
	}
	for (const url of address) {
		if (window.location.href.includes(url)) {
			return true;
		}
	}
	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;
		}

		#BottomBg a {
			color: orange
		}
	</style>`;
	head.append(css);
}

/**
 * 绑定全局按钮事件
 */
function injectEvent() {
	$("span.secondMenu a").click(() => {
		$("#ush_chart_box").hide();
	});

	$("a[href='/eams/home!index.action']").click(() => {
		$("#ush_chart_box").hide();
	});

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

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

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

	$("#chart_btn").click(() => {
		window.localStorage.setItem(storage[1].key, storage[1].val > 0 ? "0" : "1");
		toHome(itv.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 (notfound("#ush_chart_box")) {
		$("#main").before(`<div id="ush_chart_box"></div>`);
	}

	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>`;

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

/**
 * 报错自动回到首页
 * @param {string | undefined } text 报错信息
 */
function errMsg(text) {
	if (!text) {
		text = "教务系统页面显示异常,即将自动重置";
	}
	$("#helper-text").text(text);
	$("#helper-text").css("color", "red");
	// 等待跳回
	setTimeout(toHome, itv.crasher);
}

/**
 * 监听教务系统
 * 出现异常直接跳转首页
 */
function crasher() {
	if (
		includeUrl([
			"stdElectCourse",
			"security/my.action",
			"teach/grade/usual/usual-grade-std!usualInfo.action",
		])
	) {
		return;
	}

	// 检查关键 dom 是否存在

	if (notfound("#position_bar") && notfound("#Nav_bar")) {
		errMsg();
	}
}

/**
 * 调整首页布局
 */
function injectLayout() {
	$("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() {
	if (notfound("#helper_link")) {
		$("#BottomBg").append(
			`<br/><br/>
				页面启用了
					<a
						id="helper_link"
						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> 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;
	}

	if (includeUrl("myPlanCompl.action") && notfound("span.secondMenu a")) {
		$("#position_bar").html(
			'您的当前位置:<a href="/eams/home!index.action">首页</a><span class="secondMenu">&nbsp;&gt;&nbsp;<a href="/eams/home!childmenus.action?menu.id=844">课程管理</a></span><span class="fontMenu">&nbsp;&gt;&nbsp;计划完成情况</span>'
		);
	}

	$("#ush_chart_box").show();
	const table = $("table.formTable");

	const recordBook = new Recorder();

	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);
		}
	});

	// 停止监听

	setTimeout(() => {
		loop.renderPlan = false;
		if (!notfound(table)) {
			$("#ush_chart_box").highcharts(recordBook.chart());
			$(".highcharts-credits").hide();
		}
	}, itv.renderPlan);
}

/**
 * 渲染 我的成绩 页面
 * @returns
 */
function renderScore() {
	if (!includeUrl(["teach/grade/course/person", "teach/grade/usual"])) {
		return;
	}

	if (includeUrl("teach/grade/usual") && notfound("span.secondMenu a")) {
		$("#position_bar").html(
			'您的当前位置:<a href="/eams/home!index.action">首页</a><span class="secondMenu">&nbsp;&gt;&nbsp;<a href="/eams/home!childmenus.action?menu.id=844">课程管理</a></span><span class="fontMenu">&nbsp;&gt;&nbsp;平时成绩查询</span>'
		);
	} else if (
		includeUrl("teach/grade/course/person") &&
		notfound("span.secondMenu a")
	) {
		$("#position_bar").html(
			'您的当前位置:<a href="/eams/home!index.action">首页</a><span class="secondMenu">&nbsp;&gt;&nbsp;<a href="/eams/home!childmenus.action?menu.id=844">课程管理</a></span><span class="fontMenu">&nbsp;&gt;&nbsp;我的成绩</span>'
		);
	}

	$("#ush_chart_box").show();

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

	const recordBook = new Recorder();

	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 && !notfound(table)) {
		setTimeout(() => {
			loop.renderScore = false;
			$("#ush_chart_box").highcharts(recordBook.chart());
			$(".highcharts-credits").hide();
		}, itv.renderScore);
	}
}

/**
 *  定义任务
 */
const task = {
	/**
	 * 页面加载完成后执行任务
	 */
	main() {
		// 初始化数据
		initUser();
		initStorage();

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

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

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

	/**
	 * 循环监听任务
	 */
	loop() {
		crasher();
		injectEvent();

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

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

(function () {
	$(document).ready(task.main);
	setInterval(task.loop, itv.app);
})();