Enhanced GPA Calculator

现代化设计的GPA计算器,带有动画效果、深色模式和更多功能,支持全屏自由拖动,正确显示课程名称

目前為 2025-03-25 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Enhanced GPA Calculator
// @namespace    http://tampermonkey.net/
// @version      2.1.1
// @description  现代化设计的GPA计算器,带有动画效果、深色模式和更多功能,支持全屏自由拖动,正确显示课程名称
// @author       Toony (Enhanced by Claude)
// @match        https://jw.ahu.edu.cn/student/home
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function() {
    'use strict';

    // 配置选项
    const CONFIG = {
        defaultExcludedCourses: ['GG18002', 'GG82001'],
        animationDuration: 300,
        storageKeys: {
            position: 'gpaCalculatorPosition',
            darkMode: 'gpaCalculatorDarkMode',
            excludedCourses: 'gpaCalculatorExcludedCourses',
            history: 'gpaCalculatorHistory'
        }
    };

    // 缓存DOM元素的引用
    const DOM = {
        container: null,
        gpaValue: null,
        creditsValue: null,
        pointsValue: null,
        excludedCoursesList: null,
        historyList: null
    };

    // 状态管理
    const state = {
        isDragging: false,
        currentX: 0,
        currentY: 0,
        initialX: 0,
        initialY: 0,
        xOffset: 0,
        yOffset: 0,
        isDarkMode: false,
        excludedCourses: [...CONFIG.defaultExcludedCourses],
        calculationHistory: [],
        currentTab: 'stats' // 'stats', 'courses', 'history'
    };

    /**
     * 计算GPA的核心功能
     * @param {Document} doc - 包含成绩表的文档对象
     * @returns {Object|null} - 计算结果或null
     */
    function calculateGPA(doc) {
        try {
            const tables = doc.querySelectorAll('.student-grade-table');
            if (tables.length === 0) return null;

            let totalGPA = 0;
            let totalCredits = 0;
            let totalGradePoints = 0;
            let courseDetails = [];

            tables.forEach(table => {
                const rows = table.querySelectorAll('tbody tr');
                rows.forEach(row => {
                    try {
                        const courseCodeElement = row.querySelector('td div.text-color-6.one-line-nowarp span[data-original-title="课程代码"]');
                        let courseCode = courseCodeElement ? courseCodeElement.textContent.trim() : '';

                        // 检查课程是否被排除
                        if (state.excludedCourses.includes(courseCode)) return;

                        const cells = row.cells;
                        if (cells.length < 3) return;

                        // 获取课程名称 - 修复选择器,课程名称在div.course-name中
                        const courseNameElement = row.querySelector('td div.course-name');
                        let courseName = courseNameElement ? courseNameElement.textContent.trim() : '未知课程';

                        const creditsCell = cells[1];
                        const gpaCell = 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;

                                // 保存课程详情
                                courseDetails.push({
                                    code: courseCode,
                                    name: courseName,
                                    credits: credits,
                                    gpa: gpa,
                                    points: credits * gpa
                                });
                            }
                        }
                    } catch (rowError) {
                        console.error('处理课程行时出错:', rowError);
                    }
                });
            });

            if (totalCredits === 0) return null;

            // 排序课程详情
            courseDetails.sort((a, b) => b.gpa - a.gpa);

            const result = {
                gpa: totalGPA / totalCredits,
                totalCredits: totalCredits,
                totalGradePoints: totalGradePoints,
                courses: courseDetails,
                timestamp: new Date().toISOString()
            };

            // 保存到历史记录
            saveToHistory(result);

            return result;
        } catch (error) {
            console.error('计算GPA时出错:', error);
            return null;
        }
    }

    /**
     * 保存计算结果到历史记录
     * @param {Object} result - GPA计算结果
     */
    function saveToHistory(result) {
        // 只保存主要数据到历史记录
        const historyEntry = {
            gpa: result.gpa,
            totalCredits: result.totalCredits,
            totalGradePoints: result.totalGradePoints,
            timestamp: result.timestamp,
            excludedCourses: [...state.excludedCourses]
        };

        // 限制历史记录最多保存10条
        state.calculationHistory.unshift(historyEntry);
        if (state.calculationHistory.length > 10) {
            state.calculationHistory.pop();
        }

        // 保存到存储
        GM_setValue(CONFIG.storageKeys.history, JSON.stringify(state.calculationHistory));
    }

    /**
     * 从存储加载数据
     */
    function loadSavedData() {
        try {
            // 加载位置
            const savedPosition = GM_getValue(CONFIG.storageKeys.position);
            if (savedPosition) {
                const position = JSON.parse(savedPosition);
                state.xOffset = position.x || 0;
                state.yOffset = position.y || 0;
            }

            // 加载主题
            const savedDarkMode = GM_getValue(CONFIG.storageKeys.darkMode);
            if (savedDarkMode !== undefined) {
                state.isDarkMode = savedDarkMode === 'true';
            }

            // 加载排除课程
            const savedExcludedCourses = GM_getValue(CONFIG.storageKeys.excludedCourses);
            if (savedExcludedCourses) {
                state.excludedCourses = JSON.parse(savedExcludedCourses);
            }

            // 加载历史记录
            const savedHistory = GM_getValue(CONFIG.storageKeys.history);
            if (savedHistory) {
                state.calculationHistory = JSON.parse(savedHistory);
            }
        } catch (error) {
            console.error('加载保存的数据时出错:', error);
            // 出错时使用默认值
        }
    }

    /**
     * 保存位置信息
     */
    function savePosition() {
        const position = { x: state.xOffset, y: state.yOffset };
        GM_setValue(CONFIG.storageKeys.position, JSON.stringify(position));
    }

    /**
     * 保存深色模式设置
     */
    function saveDarkMode() {
        GM_setValue(CONFIG.storageKeys.darkMode, state.isDarkMode.toString());
    }

    /**
     * 保存排除课程列表
     */
    function saveExcludedCourses() {
        GM_setValue(CONFIG.storageKeys.excludedCourses, JSON.stringify(state.excludedCourses));
    }

    /**
     * 创建样式
     */
    function injectStyles() {
        const style = document.createElement('style');
        style.textContent = `
            .gpa-calculator {
                position: fixed;
                top: 0;
                left: 0;
                width: 320px;
                background: linear-gradient(145deg, #ffffff, #f5f5f5);
                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, transform 0.1s ease;
                z-index: 9999;
                max-height: 80vh;
                display: flex;
                flex-direction: column;
                transform: translate(20px, 20px);
            }

            .gpa-calculator.dark-mode {
                background: linear-gradient(145deg, #2d2d2d, #1a1a1a);
                color: #ffffff;
                box-shadow: 0 10px 20px rgba(0,0,0,0.3);
            }

            .gpa-calculator:hover {
                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;
                user-select: none;
            }

            .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);
                box-shadow: 0 2px 5px rgba(0,0,0,0.1);
            }

            .gpa-button:active {
                transform: translateY(0);
                box-shadow: none;
            }

            .gpa-button.secondary {
                background: #2196F3;
            }

            .gpa-button.secondary:hover {
                background: #1E88E5;
            }

            .gpa-button.danger {
                background: #F44336;
            }

            .gpa-button.danger:hover {
                background: #E53935;
            }

            .gpa-button.small {
                padding: 4px 8px;
                font-size: 12px;
            }

            .gpa-theme-toggle {
                background: none;
                border: none;
                cursor: pointer;
                padding: 5px;
                border-radius: 50%;
                display: flex;
                align-items: center;
                justify-content: center;
                transition: transform 0.3s ease;
            }

            .gpa-theme-toggle:hover {
                transform: rotate(30deg);
            }

            .gpa-tabs {
                display: flex;
                gap: 5px;
                margin-bottom: 15px;
                border-bottom: 1px solid #eee;
                padding-bottom: 10px;
            }

            .dark-mode .gpa-tabs {
                border-bottom-color: #444;
            }

            .gpa-tab {
                padding: 5px 10px;
                cursor: pointer;
                border-radius: 5px;
                transition: all 0.2s ease;
                font-size: 14px;
                user-select: none;
            }

            .gpa-tab:hover {
                background: rgba(0, 0, 0, 0.05);
            }

            .dark-mode .gpa-tab:hover {
                background: rgba(255, 255, 255, 0.1);
            }

            .gpa-tab.active {
                background: #4CAF50;
                color: white;
            }

            .tab-content {
                display: none;
                animation: fadeIn 0.3s ease-out;
                overflow-y: auto;
                max-height: 300px;
                scrollbar-width: thin;
            }

            .tab-content.active {
                display: block;
            }

            .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;
                box-shadow: 0 2px 5px rgba(0,0,0,0.05);
            }

            .dark-mode .gpa-stat {
                background: rgba(255, 255, 255, 0.1);
                box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            }

            .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;
                user-select: none;
            }

            .course-list {
                display: flex;
                flex-direction: column;
                gap: 10px;
            }

            .course-item {
                display: flex;
                justify-content: space-between;
                align-items: center;
                background: rgba(255, 255, 255, 0.9);
                padding: 10px;
                border-radius: 8px;
                transition: all 0.2s ease;
            }

            .dark-mode .course-item {
                background: rgba(255, 255, 255, 0.1);
            }

            .course-info {
                flex: 1;
            }

            .course-code {
                font-size: 12px;
                color: #666;
            }

            .dark-mode .course-code {
                color: #aaa;
            }

            .course-name {
                font-weight: 500;
            }

            .course-gpa {
                font-weight: 600;
                color: #2196F3;
            }

            .dark-mode .course-gpa {
                color: #64B5F6;
            }

            .excluded-courses {
                display: flex;
                flex-direction: column;
                gap: 10px;
            }

            .excluded-item {
                display: flex;
                justify-content: space-between;
                align-items: center;
                background: rgba(255, 255, 255, 0.9);
                padding: 10px;
                border-radius: 8px;
            }

            .dark-mode .excluded-item {
                background: rgba(255, 255, 255, 0.1);
            }

            .add-excluded {
                display: flex;
                gap: 5px;
                margin-top: 10px;
            }

            .add-excluded input {
                flex: 1;
                padding: 8px;
                border-radius: 5px;
                border: 1px solid #ddd;
                background: #fff;
            }

            .dark-mode .add-excluded input {
                background: #333;
                border-color: #555;
                color: #fff;
            }

            .history-list {
                display: flex;
                flex-direction: column;
                gap: 10px;
            }

            .history-item {
                background: rgba(255, 255, 255, 0.9);
                padding: 15px;
                border-radius: 12px;
                transition: all 0.2s ease;
            }

            .dark-mode .history-item {
                background: rgba(255, 255, 255, 0.1);
            }

            .history-date {
                font-size: 12px;
                color: #666;
                margin-bottom: 5px;
            }

            .dark-mode .history-date {
                color: #aaa;
            }

            .history-value {
                font-size: 18px;
                font-weight: 600;
                color: #2196F3;
            }

            .dark-mode .history-value {
                color: #64B5F6;
            }

            .history-details {
                font-size: 14px;
                color: #666;
                margin-top: 5px;
            }

            .dark-mode .history-details {
                color: #aaa;
            }

            @keyframes fadeIn {
                from { opacity: 0; transform: translateY(10px); }
                to { opacity: 1; transform: translateY(0); }
            }

            /* 滚动条样式 */
            .tab-content::-webkit-scrollbar {
                width: 6px;
            }

            .tab-content::-webkit-scrollbar-track {
                background: rgba(0, 0, 0, 0.05);
                border-radius: 10px;
            }

            .tab-content::-webkit-scrollbar-thumb {
                background: rgba(0, 0, 0, 0.2);
                border-radius: 10px;
            }

            .dark-mode .tab-content::-webkit-scrollbar-track {
                background: rgba(255, 255, 255, 0.05);
            }

            .dark-mode .tab-content::-webkit-scrollbar-thumb {
                background: rgba(255, 255, 255, 0.2);
            }
        `;
        document.head.appendChild(style);
    }

    /**
     * 创建UI界面
     * @returns {HTMLElement} 计算器容器元素
     */
    function createGPADisplay() {
        const container = document.createElement('div');
        container.className = 'gpa-calculator';
        if (state.isDarkMode) {
            container.classList.add('dark-mode');
        }

        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="切换主题">${state.isDarkMode ? '🌞' : '🌙'}</button>
                    <button class="gpa-button" id="calculate-gpa">
                        <span>计算</span>
                        <span class="calculation-icon">📊</span>
                    </button>
                </div>
            </div>

            <div class="gpa-tabs">
                <div class="gpa-tab active" data-tab="stats">统计</div>
                <div class="gpa-tab" data-tab="courses">课程</div>
                <div class="gpa-tab" data-tab="history">历史</div>
            </div>

            <div id="stats-tab" class="tab-content active">
                <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>
            </div>

            <div id="courses-tab" class="tab-content">
                <h3>排除的课程</h3>
                <div class="excluded-courses" id="excluded-courses-list">
                    ${state.excludedCourses.map(code => `
                        <div class="excluded-item" data-code="${code}">
                            <span>${code}</span>
                            <button class="gpa-button small danger remove-excluded">移除</button>
                        </div>
                    `).join('')}
                </div>
                <div class="add-excluded">
                    <input type="text" id="new-excluded-course" placeholder="输入课程代码">
                    <button class="gpa-button small" id="add-excluded-btn">添加</button>
                </div>
                <h3>课程列表</h3>
                <div class="course-list" id="course-list">
                    <div class="gpa-error">请先计算GPA以查看课程列表</div>
                </div>
            </div>

            <div id="history-tab" class="tab-content">
                <div class="history-list" id="history-list">
                    ${state.calculationHistory.length === 0 ?
                        '<div class="gpa-error">暂无历史记录</div>' :
                        state.calculationHistory.map(entry => {
                            const date = new Date(entry.timestamp);
                            return `
                                <div class="history-item">
                                    <div class="history-date">${date.toLocaleString()}</div>
                                    <div class="history-value">GPA: ${entry.gpa.toFixed(4)}</div>
                                    <div class="history-details">
                                        学分: ${entry.totalCredits.toFixed(1)} |
                                        总绩点: ${entry.totalGradePoints.toFixed(4)}
                                    </div>
                                </div>
                            `;
                        }).join('')
                    }
                </div>
            </div>
        `;

        return container;
    }

    /**
     * 使元素可拖动
     * @param {HTMLElement} element - 需要拖动的元素
     */
    function makeDraggable(element) {
        const handle = element.querySelector('.move-handle');

        handle.addEventListener('mousedown', dragStart);
        document.addEventListener('mousemove', drag);
        document.addEventListener('mouseup', dragEnd);

        function dragStart(e) {
            if (e.button !== 0) return; // 只响应左键

            state.initialX = e.clientX - state.xOffset;
            state.initialY = e.clientY - state.yOffset;

            if (e.target === handle || handle.contains(e.target)) {
                state.isDragging = true;
                element.style.transition = 'none';
            }
        }

        function drag(e) {
            if (state.isDragging) {
                e.preventDefault();

                state.currentX = e.clientX - state.initialX;
                state.currentY = e.clientY - state.initialY;

                // 边界检查
                const rect = element.getBoundingClientRect();
                const maxX = window.innerWidth - rect.width;
                const maxY = window.innerHeight - rect.height;

                state.xOffset = Math.min(Math.max(0, state.currentX), maxX);
                state.yOffset = Math.min(Math.max(0, state.currentY), maxY);

                updateElementPosition(element);
            }
        }

        function dragEnd() {
            if (state.isDragging) {
                state.isDragging = false;
                element.style.transition = 'all 0.3s ease, transform 0.1s ease';
                savePosition();
            }
        }
    }

    /**
     * 更新元素位置
     * @param {HTMLElement} element - 需要更新位置的元素
     */
    function updateElementPosition(element) {
        element.style.transform = `translate(${state.xOffset}px, ${state.yOffset}px)`;
    }

    /**
     * 更新GPA显示
     * @param {Object|null} gpaData - GPA计算结果
     */
    function updateGPADisplay(gpaData) {
        if (gpaData === null) {
            const statsContent = DOM.container.querySelector('.gpa-content');
            statsContent.innerHTML = `
                <div class="gpa-error">
                    未找到成绩表格或有效数据
                </div>
            `;

            // 清空课程列表
            const courseList = DOM.container.querySelector('#course-list');
            courseList.innerHTML = '<div class="gpa-error">无可用数据</div>';
            return;
        }

        // 更新统计数据
        DOM.gpaValue.textContent = gpaData.gpa.toFixed(4);
        DOM.creditsValue.textContent = gpaData.totalCredits.toFixed(1);
        DOM.pointsValue.textContent = gpaData.totalGradePoints.toFixed(4);

        // 添加动画效果
        [DOM.gpaValue, DOM.creditsValue, DOM.pointsValue].forEach(el => {
            el.style.animation = 'none';
            el.offsetHeight; // 触发重绘
            el.style.animation = 'fadeIn 0.5s ease-out';
        });

        // 更新课程列表
        const courseList = DOM.container.querySelector('#course-list');
        if (gpaData.courses && gpaData.courses.length > 0) {
            courseList.innerHTML = gpaData.courses.map(course => `
                <div class="course-item">
                    <div class="course-info">
                        <div class="course-code">${course.code}</div>
                        <div class="course-name">${course.name}</div>
                    </div>
                    <div class="course-gpa">${course.gpa.toFixed(2)}</div>
                </div>
            `).join('');
        } else {
            courseList.innerHTML = '<div class="gpa-error">无课程数据</div>';
        }

        // 更新历史记录列表
        updateHistoryList();
    }

    /**
     * 更新历史记录列表
     */
    function updateHistoryList() {
        if (!DOM.historyList) return;

        if (state.calculationHistory.length === 0) {
            DOM.historyList.innerHTML = '<div class="gpa-error">暂无历史记录</div>';
            return;
        }

        DOM.historyList.innerHTML = state.calculationHistory.map(entry => {
            const date = new Date(entry.timestamp);
            return `
                <div class="history-item">
                    <div class="history-date">${date.toLocaleString()}</div>
                    <div class="history-value">GPA: ${entry.gpa.toFixed(4)}</div>
                    <div class="history-details">
                        学分: ${entry.totalCredits.toFixed(1)} |
                        总绩点: ${entry.totalGradePoints.toFixed(4)}
                    </div>
                </div>
            `;
        }).join('');
    }

    /**
     * 更新排除课程列表
     */
    function updateExcludedCoursesList() {
        if (!DOM.excludedCoursesList) return;

        DOM.excludedCoursesList.innerHTML = state.excludedCourses.map(code => `
            <div class="excluded-item" data-code="${code}">
                <span>${code}</span>
                <button class="gpa-button small danger remove-excluded">移除</button>
            </div>
        `).join('');

        // 重新绑定移除按钮事件
        DOM.excludedCoursesList.querySelectorAll('.remove-excluded').forEach(btn => {
            btn.addEventListener('click', function() {
                const code = this.parentElement.dataset.code;
                const index = state.excludedCourses.indexOf(code);
                if (index > -1) {
                    state.excludedCourses.splice(index, 1);
                    updateExcludedCoursesList();
                    saveExcludedCourses();
                }
            });
        });
    }

    /**
     * 初始化标签页切换功能
     */
    function initTabs() {
        const tabs = DOM.container.querySelectorAll('.gpa-tab');
        const tabContents = DOM.container.querySelectorAll('.tab-content');

        tabs.forEach(tab => {
            tab.addEventListener('click', () => {
                // 移除所有活动标签
                tabs.forEach(t => t.classList.remove('active'));
                tabContents.forEach(c => c.classList.remove('active'));

                // 激活当前标签
                tab.classList.add('active');
                const tabId = tab.dataset.tab;
                DOM.container.querySelector(`#${tabId}-tab`).classList.add('active');
                state.currentTab = tabId;
            });
        });
    }

    /**
     * 计算GPA并更新显示
     */
    function calculateAndUpdate() {
        const iframe = document.querySelector('iframe');
        if (iframe) {
            try {
                const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
                if (iframeDoc) {
                    const gpaData = calculateGPA(iframeDoc);
                    updateGPADisplay(gpaData);
                } else {
                    updateGPADisplay(null);
                }
            } catch (error) {
                console.error('访问iframe内容时出错:', error);
                updateGPADisplay(null);
            }
        } else {
            updateGPADisplay(null);
        }
    }

    /**
     * 绑定事件
     */
    function bindEvents() {
        // 计算按钮事件
        const calculateButton = DOM.container.querySelector('#calculate-gpa');
        calculateButton.addEventListener('click', calculateAndUpdate);

        // 主题切换
        const themeToggle = DOM.container.querySelector('.gpa-theme-toggle');
        themeToggle.addEventListener('click', () => {
            state.isDarkMode = !state.isDarkMode;
            DOM.container.classList.toggle('dark-mode');
            themeToggle.innerHTML = state.isDarkMode ? '🌞' : '🌙';
            saveDarkMode();
        });

        // 添加排除课程
        const addExcludedBtn = DOM.container.querySelector('#add-excluded-btn');
        const newExcludedInput = DOM.container.querySelector('#new-excluded-course');

        if (addExcludedBtn && newExcludedInput) {
            addExcludedBtn.addEventListener('click', () => {
                const code = newExcludedInput.value.trim();
                if (code && !state.excludedCourses.includes(code)) {
                    state.excludedCourses.push(code);
                    updateExcludedCoursesList();
                    saveExcludedCourses();
                    newExcludedInput.value = '';
                }
            });

            newExcludedInput.addEventListener('keypress', (e) => {
                if (e.key === 'Enter') {
                    addExcludedBtn.click();
                }
            });
        }

        // 监听窗口大小变化,确保计算器不会超出界面
        window.addEventListener('resize', () => {
            const rect = DOM.container.getBoundingClientRect();
            const maxX = window.innerWidth - rect.width;
            const maxY = window.innerHeight - rect.height;

            if (state.xOffset > maxX || state.yOffset > maxY) {
                state.xOffset = Math.min(state.xOffset, maxX);
                state.yOffset = Math.min(state.yOffset, maxY);
                updateElementPosition(DOM.container);
                savePosition();
            }
        });
    }

    /**
     * 缓存DOM引用
     */
    function cacheDOMReferences() {
        DOM.container = document.querySelector('.gpa-calculator');
        DOM.gpaValue = DOM.container.querySelector('#gpa-value');
        DOM.creditsValue = DOM.container.querySelector('#credits-value');
        DOM.pointsValue = DOM.container.querySelector('#points-value');
        DOM.excludedCoursesList = DOM.container.querySelector('#excluded-courses-list');
        DOM.historyList = DOM.container.querySelector('#history-list');
    }

    /**
     * 初始化应用
     */
    function init() {
        // 加载保存的数据
        loadSavedData();

        // 创建UI
        injectStyles();
        DOM.container = createGPADisplay();
        document.body.appendChild(DOM.container);

        // 缓存DOM引用
        cacheDOMReferences();

        // 初始化功能
        updateElementPosition(DOM.container);
        makeDraggable(DOM.container);
        initTabs();
        bindEvents();

        // 应用深色模式
        if (state.isDarkMode) {
            DOM.container.classList.add('dark-mode');
        }
    }

    // 启动应用
    init();
})();