Enhanced GPA Calculator

安徽大学教务系统中的GPA计算器

// ==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();
})();