// ==UserScript==
// @name Enhanced GPA Calculator
// @namespace http://tampermonkey.net/
// @version 1.0.0
// @description 安徽大学教务系统中的GPA计算器
// @author Toony
// @match https://jw.ahu.edu.cn/student/home
// @grant none
// ==/UserScript==
(function() {
'use strict';
function calculateGPA(doc) {
const tables = doc.querySelectorAll('.student-grade-table');
if (tables.length === 0) return null;
let totalGPA = 0;
let totalCredits = 0;
let totalGradePoints = 0;
const excludedCourseCodes = ['GG18002', 'GG82001'];
tables.forEach(table => {
const rows = table.querySelectorAll('tbody tr');
rows.forEach(row => {
const courseCodeElement = row.querySelector('td div.text-color-6.one-line-nowarp span[data-original-title="课程代码"]');
let courseCode = courseCodeElement ? courseCodeElement.textContent.trim() : '';
if (excludedCourseCodes.includes(courseCode)) return;
const creditsCell = row.cells[1];
const gpaCell = row.cells[2];
if (creditsCell && gpaCell) {
const credits = parseFloat(creditsCell.textContent.trim());
const gpa = parseFloat(gpaCell.textContent.trim());
if (!isNaN(credits) && !isNaN(gpa)) {
totalGPA += credits * gpa;
totalCredits += credits;
totalGradePoints += credits * gpa;
}
}
});
});
if (totalCredits === 0) return null;
return {
gpa: totalGPA / totalCredits,
totalCredits: totalCredits,
totalGradePoints: totalGradePoints
};
}
// 创建样式
function injectStyles() {
const style = document.createElement('style');
style.textContent = `
.gpa-calculator {
position: fixed;
top: 20px;
right: 20px;
width: 300px;
background: linear-gradient(145deg, #ffffff, #f0f0f0);
border-radius: 20px;
padding: 20px;
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
transition: all 0.3s ease;
z-index: 9999;
}
.gpa-calculator.dark-mode {
background: linear-gradient(145deg, #2d2d2d, #1a1a1a);
color: #ffffff;
}
.gpa-calculator:hover {
transform: translateY(-5px);
box-shadow: 0 15px 30px rgba(0,0,0,0.15);
}
.gpa-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid #eee;
}
.dark-mode .gpa-header {
border-bottom-color: #444;
}
.gpa-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0;
}
.dark-mode .gpa-title {
color: #fff;
}
.gpa-controls {
display: flex;
gap: 10px;
}
.gpa-button {
background: #4CAF50;
color: white;
border: none;
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 5px;
}
.gpa-button:hover {
background: #45a049;
transform: translateY(-2px);
}
.gpa-theme-toggle {
background: none;
border: none;
cursor: pointer;
padding: 5px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.gpa-content {
display: grid;
gap: 15px;
}
.gpa-stat {
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 12px;
display: flex;
flex-direction: column;
gap: 5px;
transition: all 0.2s ease;
}
.dark-mode .gpa-stat {
background: rgba(255, 255, 255, 0.1);
}
.gpa-stat:hover {
transform: translateX(5px);
}
.gpa-stat-label {
font-size: 14px;
color: #666;
}
.dark-mode .gpa-stat-label {
color: #aaa;
}
.gpa-stat-value {
font-size: 24px;
font-weight: 600;
color: #2196F3;
}
.dark-mode .gpa-stat-value {
color: #64B5F6;
}
.gpa-error {
color: #f44336;
font-size: 14px;
text-align: center;
padding: 15px;
}
.dark-mode .gpa-error {
color: #ef9a9a;
}
.move-handle {
cursor: move;
padding: 5px;
margin: -5px;
}
`;
document.head.appendChild(style);
}
// 创建UI
function createGPADisplay() {
const container = document.createElement('div');
container.className = 'gpa-calculator';
container.innerHTML = `
<div class="gpa-header">
<div class="move-handle">
<div class="gpa-title">GPA 计算器</div>
</div>
<div class="gpa-controls">
<button class="gpa-theme-toggle" title="切换主题">🌓</button>
<button class="gpa-button">
<span>计算</span>
<span class="calculation-icon">📊</span>
</button>
</div>
</div>
<div class="gpa-content">
<div class="gpa-stat">
<span class="gpa-stat-label">总平均 GPA</span>
<span class="gpa-stat-value" id="gpa-value">-</span>
</div>
<div class="gpa-stat">
<span class="gpa-stat-label">总学分</span>
<span class="gpa-stat-value" id="credits-value">-</span>
</div>
<div class="gpa-stat">
<span class="gpa-stat-label">总绩点</span>
<span class="gpa-stat-value" id="points-value">-</span>
</div>
</div>
`;
return container;
}
// 使元素可拖动
function makeDraggable(element) {
const handle = element.querySelector('.move-handle');
let isDragging = false;
let currentX;
let currentY;
let initialX;
let initialY;
let xOffset = 0;
let yOffset = 0;
handle.addEventListener('mousedown', dragStart);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
function dragStart(e) {
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
if (e.target === handle || handle.contains(e.target)) {
isDragging = true;
}
}
function drag(e) {
if (isDragging) {
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
xOffset = currentX;
yOffset = currentY;
// 边界检查
const rect = element.getBoundingClientRect();
const maxX = window.innerWidth - rect.width;
const maxY = window.innerHeight - rect.height;
xOffset = Math.min(Math.max(0, xOffset), maxX);
yOffset = Math.min(Math.max(0, yOffset), maxY);
setTranslate(xOffset, yOffset, element);
}
}
function setTranslate(xPos, yPos, el) {
el.style.transform = `translate(${xPos}px, ${yPos}px)`;
}
function dragEnd() {
initialX = currentX;
initialY = currentY;
isDragging = false;
}
}
// 更新显示
function updateGPADisplay(gpaData, container) {
const gpaValue = container.querySelector('#gpa-value');
const creditsValue = container.querySelector('#credits-value');
const pointsValue = container.querySelector('#points-value');
if (gpaData === null) {
container.querySelector('.gpa-content').innerHTML = `
<div class="gpa-error">
未找到成绩表格或有效数据
</div>
`;
return;
}
gpaValue.textContent = gpaData.gpa.toFixed(4);
creditsValue.textContent = gpaData.totalCredits.toFixed(1);
pointsValue.textContent = gpaData.totalGradePoints.toFixed(4);
// 添加动画效果
[gpaValue, creditsValue, pointsValue].forEach(el => {
el.style.animation = 'none';
el.offsetHeight; // 触发重绘
el.style.animation = 'fadeIn 0.5s ease-out';
});
}
// 初始化
function init() {
injectStyles();
const container = createGPADisplay();
document.body.appendChild(container);
makeDraggable(container);
// 计算按钮事件
const calculateButton = container.querySelector('.gpa-button');
calculateButton.addEventListener('click', () => {
const iframe = document.querySelector('iframe');
if (iframe) {
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
if (iframeDoc) {
const gpaData = calculateGPA(iframeDoc);
updateGPADisplay(gpaData, container);
} else {
updateGPADisplay(null, container);
}
} else {
updateGPADisplay(null, container);
}
});
// 主题切换
const themeToggle = container.querySelector('.gpa-theme-toggle');
themeToggle.addEventListener('click', () => {
container.classList.toggle('dark-mode');
});
// 保存位置信息
window.addEventListener('beforeunload', () => {
const position = {
x: container.style.transform.match(/translateX\((.+)px\)/) ? parseFloat(RegExp.$1) : 0,
y: container.style.transform.match(/translateY\((.+)px\)/) ? parseFloat(RegExp.$1) : 0
};
localStorage.setItem('gpaCalculatorPosition', JSON.stringify(position));
});
// 恢复位置信息
const savedPosition = localStorage.getItem('gpaCalculatorPosition');
if (savedPosition) {
const position = JSON.parse(savedPosition);
container.style.transform = `translate(${position.x}px, ${position.y}px)`;
}
}
// 启动应用
init();
})();