// ==UserScript==
// @name 乘方教务系统学生学分计算工具
// @namespace http://tampermonkey.net/
// @version 2.0.2
// @description 乘方教务系统的绩点计算工具😆
// @author GamerNoTitle
// @match https://jxfw.gdut.edu.cn/*
// @match https://zhjw.smu.edu.cn/*
// @grant GM_addStyle
// @run-at document-idle
// @homepageURL https://github.com/GDUTMeow/GPACalculator
// @supportURL https://github.com/GDUTMeow/GPACalculator/issues
// @license GPLv3
// ==/UserScript==
/*
2.0.2 更新:让按钮注入更加精准,现在大概应该不会注入到别的表格里面去了
2.0.1 更新:将复制链接按钮的描述改为“复制 Github 链接”,更加直观
2.0.0 更新:把 Alert 换成了自定义的 Material You Design 模态框,更加好看了
*/
const CONFIG = {
VERSION: '2.0.2',
REPO_URL: 'https://github.com/GDUTMeow/GPACalculator'
};
(function() {
'use strict';
// 样式声明
GM_addStyle(`
#calcGPA {
margin-left: 12px;
padding: 2px 8px;
background: #5bc0de;
color: white;
border: 1px solid #46b8da;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
vertical-align: middle;
transition: all 0.3s;
}
#calcGPA:hover {
background: #31b0d5;
transform: translateY(-1px);
}
#calcGPA:active {
transform: translateY(0);
}
.gpa-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
backdrop-filter: blur(4px);
}
.gpa-modal {
background: #F7F2FA;
border-radius: 28px;
padding: 24px;
width: min(90%, 600px);
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 8px 24px rgba(0,0,0,0.2);
animation: modalEnter 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes modalEnter {
from { transform: scale(0.9); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.modal-title {
font-size: 22px;
font-weight: 600;
color: #6750A4;
}
.modal-close {
cursor: pointer;
padding: 8px;
border-radius: 50%;
transition: background 0.2s;
font-size: 24px;
line-height: 1;
}
.modal-close:hover {
background: rgba(0,0,0,0.1);
}
.modal-content {
line-height: 1.6;
font-family: monospace;
white-space: pre-wrap;
padding: 12px 0;
border-top: 1px solid #79747E;
border-bottom: 1px solid #79747E;
margin: 16px 0;
color: #333;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.md-button {
padding: 8px 16px;
border-radius: 20px;
border: 1px solid #79747E;
background: transparent;
cursor: pointer;
transition: all 0.2s;
font-family: system-ui;
}
.md-button.primary {
background: #6750A4;
color: white;
border: none;
}
.md-button:hover {
opacity: 0.9;
transform: translateY(-1px);
}
`);
// 模态框创建函数
function createModal(content) {
const overlay = document.createElement('div');
overlay.className = 'gpa-modal-overlay';
const modal = document.createElement('div');
modal.className = 'gpa-modal';
modal.innerHTML = `
<div class="modal-header">
<div class="modal-title">📊 绩点计算结果 | GPACalculator v${CONFIG.VERSION}</div>
<div class="modal-close">×</div>
</div>
<div class="modal-content">${content}</div>
<div class="modal-actions">
<button class="md-button" onclick="this.closest('.gpa-modal-overlay').remove()">关闭</button>
<button class="md-button primary" id="confirmCopy">复制 Github 仓库链接</button>
</div>
`;
modal.querySelector('.modal-close').addEventListener('click', () => overlay.remove());
overlay.addEventListener('click', (e) => {
if (e.target === overlay) overlay.remove();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') overlay.remove();
});
modal.querySelector('#confirmCopy').addEventListener('click', () => {
copyToClipboard(CONFIG.REPO_URL);
overlay.remove();
});
overlay.appendChild(modal);
document.body.appendChild(overlay);
}
// 按钮注入函数
function injectButton() {
document.querySelectorAll('iframe').forEach(iframe => {
try {
// 筛选目标 iframe
if (!iframe.src.includes('xskccjxx!xskccjList.action?firstquery=1')) return;
// 获取内部文档
const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
if (!iframeDoc) return;
// 防止重复注入
if (iframeDoc.getElementById('calcGPA')) return;
// 查找目标元素
const toolbar = iframeDoc.getElementById('tb');
const scoreTable = iframeDoc.querySelector('table.datagrid-btable');
const targetRow = toolbar?.querySelector('tr');
if (!toolbar || !scoreTable || !targetRow) return;
// 创建按钮元素
const buttonCell = iframeDoc.createElement('td');
buttonCell.style.cssText = 'padding-left:15px; position:relative; top:-1px;';
const button = iframeDoc.createElement('a');
button.id = 'calcGPA';
button.innerHTML = '📊 计算绩点';
button.onclick = () => calculateGPA(iframe);
buttonCell.appendChild(button);
targetRow.appendChild(buttonCell);
} catch (error) {
console.error('iframe 操作错误:', error);
}
});
}
// 绩点计算函数
function calculateGPA(targetIframe) {
try {
const iframeDoc = targetIframe.contentDocument || targetIframe.contentWindow?.document;
if (!iframeDoc) return;
const table = iframeDoc.querySelector('table.datagrid-btable');
if (!table) return;
let totalCredits = 0, weightedSum = 0;
let totalCreditsWithExemption = 0, weightedSumWithExemption = 0;
table.querySelectorAll('tr').forEach(row => {
if (row.querySelector('th')) return;
const creditCell = row.querySelector('td[field="xf"] div');
const gradeCell = row.querySelector('td[field="cjjd"] div');
if (!creditCell || !gradeCell) return;
const credits = parseFloat(creditCell.textContent.trim());
const gradeText = gradeCell.textContent.trim();
const isExempt = gradeText === '免修' || gradeText === '--';
if (isNaN(credits)) return;
if (!isExempt) {
const grade = parseFloat(gradeText);
if (!isNaN(grade)) {
totalCredits += credits;
weightedSum += grade * credits;
}
}
const effectiveGrade = isExempt ? 3.0 : parseFloat(gradeText);
if (!isNaN(effectiveGrade)) {
totalCreditsWithExemption += credits;
weightedSumWithExemption += effectiveGrade * credits;
}
});
const resultMessage = [
`⚠️ 不含免修的是教务系统里面的计算方式`,
`⚠️ 含免修的是GDUTDays的计算方式`,
`⚠️ 绩点 = 加权总分 / 总学分`,
`✨ 点击确定复制GitHub链接 ✨`,
`📦 ${CONFIG.REPO_URL}`,
`----------------------------------------------------------`,
`✅ 总学分(不含免修):${totalCredits}`,
`🚩 加权总分(不含免修):${weightedSum.toFixed(4)}`,
`🎉 最终绩点(不含免修):${totalCredits > 0 ? (weightedSum / totalCredits).toFixed(4) : 0}`,
`----------------------------------------------------------`,
`✅ 总学分(含免修):${totalCreditsWithExemption}`,
`🚩 加权总分(含免修):${weightedSumWithExemption.toFixed(4)}`,
`🎉 最终绩点(含免修):${totalCreditsWithExemption > 0 ? (weightedSumWithExemption / totalCreditsWithExemption).toFixed(4) : 0}`,
].join('\n');
createModal(resultMessage);
} catch (error) {
console.error('绩点计算错误:', error);
}
}
// 剪贴板工具函数
function copyToClipboard(text) {
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
}
// DOM 观察器
let observer;
function initObserver() {
if (observer) observer.disconnect();
observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.addedNodes) {
mutation.addedNodes.forEach(node => {
if (node.tagName === 'IFRAME') {
node.addEventListener('load', () => injectButton());
}
});
}
});
injectButton();
});
observer.observe(document.body, { childList: true, subtree: true });
}
// 路由变化检测
let lastUrl = location.href;
setInterval(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
initObserver();
setTimeout(injectButton, 1000);
}
}, 1000);
// 初始化入口
function initialize() {
initObserver();
setTimeout(injectButton, 1500);
}
if (document.readyState === 'complete') {
initialize();
} else {
window.addEventListener('load', initialize);
}
window.addEventListener('popstate', () => {
setTimeout(injectButton, 500);
});
})();