Enhanced GPA Calculator

现代化设计的GPA计算器,支持全屏自由拖动、手动计算、课程筛选和导出功能

// ==UserScript==
// @name         Enhanced GPA Calculator
// @namespace    http://tampermonkey.net/
// @version      2.2.0
// @description  现代化设计的GPA计算器,支持全屏自由拖动、手动计算、课程筛选和导出功能
// @author       Toony
// @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,
        manualCredits: null,
        manualPoints: null,
        manualGpaResult: 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', 'manual', 'export'
        lastCalculatedGPA: null,
        courseCategories: {}, // 用于存储课程分类统计
        courseGrades: [] // 用于存储所有课程成绩
    };

    /**
     * 计算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 = [];
            state.courseCategories = {}; // 重置分类统计
            state.courseGrades = []; // 重置成绩数组

            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 courseCategoryElement = row.querySelector('td div.text-color-6.one-line-nowarp span[data-original-title="课程分类"]');
                        let courseCategory = courseCategoryElement ? courseCategoryElement.textContent.trim() : '未知分类';

                        const creditsCell = cells[1];
                        const gpaCell = cells[2];
                        const gradeCell = cells[3];

                        if (creditsCell && gpaCell) {
                            const credits = parseFloat(creditsCell.textContent.trim());
                            const gpa = parseFloat(gpaCell.textContent.trim());
                            const grade = gradeCell ? gradeCell.textContent.trim() : '';

                            if (!isNaN(credits) && !isNaN(gpa)) {
                                totalGPA += credits * gpa;
                                totalCredits += credits;
                                totalGradePoints += credits * gpa;

                                // 更新分类统计
                                if (!state.courseCategories[courseCategory]) {
                                    state.courseCategories[courseCategory] = {
                                        totalCredits: 0,
                                        totalPoints: 0,
                                        count: 0
                                    };
                                }
                                state.courseCategories[courseCategory].totalCredits += credits;
                                state.courseCategories[courseCategory].totalPoints += credits * gpa;
                                state.courseCategories[courseCategory].count += 1;

                                // 保存成绩分布
                                state.courseGrades.push({
                                    gpa: gpa,
                                    credits: credits
                                });

                                // 保存课程详情
                                courseDetails.push({
                                    code: courseCode,
                                    name: courseName,
                                    category: courseCategory,
                                    credits: credits,
                                    gpa: gpa,
                                    grade: grade,
                                    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,
                categories: state.courseCategories,
                timestamp: new Date().toISOString()
            };

            // 保存到状态
            state.lastCalculatedGPA = result;

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

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

    /**
     * 手动计算GPA
     * @param {number} credits - 总学分
     * @param {number} points - 总绩点
     * @returns {number} - 计算得到的GPA
     */
    function calculateManualGPA(credits, points) {
        if (credits <= 0) return 0;
        return points / credits;
    }

    /**
     * 保存计算结果到历史记录
     * @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;
                flex-wrap: wrap;
            }

            .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;
                margin-bottom: 5px;
            }

            .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-detail {
                font-size: 12px;
                color: #666;
                margin-top: 2px;
            }

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

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

            .manual-calculator {
                display: flex;
                flex-direction: column;
                gap: 15px;
            }

            .manual-form {
                display: flex;
                flex-direction: column;
                gap: 10px;
                background: rgba(255, 255, 255, 0.9);
                padding: 15px;
                border-radius: 12px;
            }

            .dark-mode .manual-form {
                background: rgba(255, 255, 255, 0.1);
            }

            .form-group {
                display: flex;
                flex-direction: column;
                gap: 5px;
            }

            .form-group label {
                font-size: 14px;
                color: #666;
            }

            .dark-mode .form-group label {
                color: #aaa;
            }

            .form-group input {
                padding: 8px;
                border-radius: 5px;
                border: 1px solid #ddd;
                background: #fff;
            }

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

            .manual-result {
                background: rgba(255, 255, 255, 0.9);
                padding: 15px;
                border-radius: 12px;
                display: flex;
                flex-direction: column;
                gap: 5px;
                text-align: center;
            }

            .dark-mode .manual-result {
                background: rgba(255, 255, 255, 0.1);
            }

            .manual-gpa {
                font-size: 32px;
                font-weight: 600;
                color: #2196F3;
            }

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

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

            .category-item {
                background: rgba(255, 255, 255, 0.9);
                padding: 15px;
                border-radius: 12px;
            }

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

            .category-name {
                font-weight: 600;
                margin-bottom: 5px;
            }

            .category-stats {
                display: flex;
                justify-content: space-between;
                font-size: 14px;
                color: #666;
            }

            .dark-mode .category-stats {
                color: #aaa;
            }

            .export-section {
                display: flex;
                flex-direction: column;
                gap: 15px;
            }

            .export-option {
                background: rgba(255, 255, 255, 0.9);
                padding: 15px;
                border-radius: 12px;
                display: flex;
                gap: 10px;
                align-items: center;
            }

            .dark-mode .export-option {
                background: rgba(255, 255, 255, 0.1);
            }

            .export-icon {
                font-size: 24px;
            }

            .export-info {
                flex: 1;
            }

            .export-title {
                font-weight: 600;
                margin-bottom: 3px;
            }

            .export-desc {
                font-size: 12px;
                color: #666;
            }

            .dark-mode .export-desc {
                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);
            }

            /* 分布图样式 */
            .distribution-chart {
                width: 100%;
                height: 8px;
                background: #eee;
                border-radius: 4px;
                margin-top: 10px;
                position: relative;
                overflow: hidden;
            }

            .dark-mode .distribution-chart {
                background: #444;
            }

            .chart-segment {
                height: 100%;
                position: absolute;
                transition: width 0.5s ease;
            }

            .segment-excellent {
                background: #4CAF50;
                left: 0;
            }

            .segment-good {
                background: #2196F3;
            }

            .segment-average {
                background: #FFC107;
            }

            .segment-poor {
                background: #F44336;
                right: 0;
            }

            .chart-legend {
                display: flex;
                justify-content: space-between;
                margin-top: 5px;
                font-size: 10px;
                color: #666;
            }

            .dark-mode .chart-legend {
                color: #aaa;
            }

            .legend-item {
                display: flex;
                align-items: center;
                gap: 3px;
            }

            .legend-dot {
                width: 8px;
                height: 8px;
                border-radius: 50%;
            }

            .dot-excellent {
                background: #4CAF50;
            }

            .dot-good {
                background: #2196F3;
            }

            .dot-average {
                background: #FFC107;
            }

            .dot-poor {
                background: #F44336;
            }

            .filter-options {
                display: flex;
                flex-wrap: wrap;
                gap: 5px;
                margin-bottom: 15px;
            }

            .filter-tag {
                background: #eee;
                padding: 3px 8px;
                border-radius: 15px;
                font-size: 12px;
                cursor: pointer;
                transition: all 0.2s ease;
            }

            .dark-mode .filter-tag {
                background: #444;
            }

            .filter-tag:hover {
                background: #ddd;
            }

            .dark-mode .filter-tag:hover {
                background: #555;
            }

            .filter-tag.active {
                background: #4CAF50;
                color: white;
            }
        `;
        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="manual">手动计算</div>
                <div class="gpa-tab" data-tab="history">历史</div>
                <div class="gpa-tab" data-tab="export">导出</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 class="gpa-stat">
                        <span class="gpa-stat-label">成绩分布</span>
                        <div class="distribution-chart">
                            <div class="chart-segment segment-excellent" style="width: 0%"></div>
                            <div class="chart-segment segment-good" style="width: 0%"></div>
                            <div class="chart-segment segment-average" style="width: 0%"></div>
                            <div class="chart-segment segment-poor" style="width: 0%"></div>
                        </div>
                        <div class="chart-legend">
                            <div class="legend-item">
                                <span class="legend-dot dot-excellent"></span>
                                <span>90+</span>
                            </div>
                            <div class="legend-item">
                                <span class="legend-dot dot-good"></span>
                                <span>80-89</span>
                            </div>
                            <div class="legend-item">
                                <span class="legend-dot dot-average"></span>
                                <span>70-79</span>
                            </div>
                            <div class="legend-item">
                                <span class="legend-dot dot-poor"></span>
                                <span>≤69</span>
                            </div>
                        </div>
                    </div>

                    <h3>课程分类统计</h3>
                    <div class="category-list" id="category-stats">
                        <div class="gpa-error">请先计算GPA以查看分类统计</div>
                    </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="filter-options" id="category-filters">
                    <div class="filter-tag active" data-category="all">全部</div>
                </div>
                <div class="course-list" id="course-list">
                    <div class="gpa-error">请先计算GPA以查看课程列表</div>
                </div>
            </div>

            <div id="manual-tab" class="tab-content">
                <div class="manual-calculator">
                    <div class="manual-form">
                        <div class="form-group">
                            <label for="manual-credits">总学分</label>
                            <input type="number" id="manual-credits" placeholder="输入总学分" step="0.1" min="0">
                        </div>
                        <div class="form-group">
                            <label for="manual-points">总绩点</label>
                            <input type="number" id="manual-points" placeholder="输入总绩点" step="0.1" min="0">
                        </div>
                        <button class="gpa-button" id="calculate-manual">计算</button>
                    </div>
                    <div class="manual-result">
                        <div class="gpa-stat-label">计算结果</div>
                        <div class="manual-gpa" id="manual-gpa-result">-</div>
                    </div>
                    <div class="gpa-error" id="manual-error" style="display: none;"></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>

            <div id="export-tab" class="tab-content">
                <div class="export-section">
                    <div class="export-option">
                        <div class="export-icon">📋</div>
                        <div class="export-info">
                            <div class="export-title">复制为文本</div>
                            <div class="export-desc">将GPA计算结果复制为纯文本格式</div>
                        </div>
                        <button class="gpa-button small" id="export-text">复制</button>
                    </div>
                    <div class="export-option">
                        <div class="export-icon">📊</div>
                        <div class="export-info">
                            <div class="export-title">导出为CSV</div>
                            <div class="export-desc">导出课程详细成绩为CSV文件</div>
                        </div>
                        <button class="gpa-button small" id="export-csv">导出</button>
                    </div>
                    <div class="export-option">
                        <div class="export-icon">🖨️</div>
                        <div class="export-info">
                            <div class="export-title">打印成绩单</div>
                            <div class="export-desc">生成打印友好的成绩单</div>
                        </div>
                        <button class="gpa-button small" id="export-print">打印</button>
                    </div>
                </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>';

            // 清空分类列表
            const categoryStats = DOM.container.querySelector('#category-stats');
            categoryStats.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';
        });

        // 更新成绩分布
        updateGradeDistribution(state.courseGrades);

        // 更新分类统计
        updateCategoryStats(gpaData.categories);

        // 更新课程列表和过滤器
        updateCourseFilters(gpaData.courses);
        updateCourseList(gpaData.courses);

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

        // 将最新计算结果填充到手动计算器
        if (DOM.manualCredits && DOM.manualPoints) {
            DOM.manualCredits.value = gpaData.totalCredits.toFixed(1);
            DOM.manualPoints.value = gpaData.totalGradePoints.toFixed(2);
        }
    }

    /**
     * 更新成绩分布
     * @param {Array} grades - 成绩数组
     */
    function updateGradeDistribution(grades) {
        if (!grades || grades.length === 0) return;

        // 计算成绩分布
        let excellent = 0, good = 0, average = 0, poor = 0;
        let totalCredits = 0;

        grades.forEach(grade => {
            if (grade.gpa >= 4.5) { // 90-100分
                excellent += grade.credits;
            } else if (grade.gpa >= 3.5) { // 80-89分
                good += grade.credits;
            } else if (grade.gpa >= 2.5) { // 70-79分
                average += grade.credits;
            } else { // 60-69分
                poor += grade.credits;
            }
            totalCredits += grade.credits;
        });

        // 计算百分比
        const excellentPercent = (excellent / totalCredits) * 100;
        const goodPercent = (good / totalCredits) * 100;
        const averagePercent = (average / totalCredits) * 100;
        const poorPercent = (poor / totalCredits) * 100;

        // 更新图表
        const chartSegments = DOM.container.querySelectorAll('.chart-segment');
        chartSegments[0].style.width = `${excellentPercent}%`;
        chartSegments[1].style.width = `${goodPercent}%`;
        chartSegments[1].style.left = `${excellentPercent}%`;
        chartSegments[2].style.width = `${averagePercent}%`;
        chartSegments[2].style.left = `${excellentPercent + goodPercent}%`;
        chartSegments[3].style.width = `${poorPercent}%`;
    }

    /**
     * 更新分类统计
     * @param {Object} categories - 课程分类统计
     */
    function updateCategoryStats(categories) {
        const categoryStats = DOM.container.querySelector('#category-stats');
        if (!categories || Object.keys(categories).length === 0) {
            categoryStats.innerHTML = '<div class="gpa-error">无分类数据</div>';
            return;
        }

        let categoryHtml = '';
        for (const [category, stats] of Object.entries(categories)) {
            const categoryGPA = stats.totalPoints / stats.totalCredits;
            categoryHtml += `
                <div class="category-item">
                    <div class="category-name">${category}</div>
                    <div class="category-stats">
                        <span>GPA: ${categoryGPA.toFixed(2)}</span>
                        <span>学分: ${stats.totalCredits.toFixed(1)}</span>
                        <span>课程数: ${stats.count}</span>
                    </div>
                </div>
            `;
        }

        categoryStats.innerHTML = categoryHtml;
    }

    /**
     * 更新课程过滤器
     * @param {Array} courses - 课程数组
     */
    function updateCourseFilters(courses) {
        if (!courses || courses.length === 0) return;

        // 获取所有分类
        const categories = new Set();
        courses.forEach(course => categories.add(course.category));

        // 更新过滤器
        const filtersContainer = DOM.container.querySelector('#category-filters');
        let filtersHtml = '<div class="filter-tag active" data-category="all">全部</div>';

        categories.forEach(category => {
            filtersHtml += `<div class="filter-tag" data-category="${category}">${category}</div>`;
        });

        filtersContainer.innerHTML = filtersHtml;

        // 绑定过滤器点击事件
        const filterTags = filtersContainer.querySelectorAll('.filter-tag');
        filterTags.forEach(tag => {
            tag.addEventListener('click', function() {
                filterTags.forEach(t => t.classList.remove('active'));
                this.classList.add('active');
                const category = this.dataset.category;
                filterCourses(courses, category);
            });
        });
    }

    /**
     * 过滤课程列表
     * @param {Array} courses - 所有课程
     * @param {string} category - 要过滤的分类
     */
    function filterCourses(courses, category) {
        let filteredCourses = courses;
        if (category !== 'all') {
            filteredCourses = courses.filter(course => course.category === category);
        }

        updateCourseList(filteredCourses);
    }

    /**
     * 更新课程列表
     * @param {Array} courses - 课程数组
     */
    function updateCourseList(courses) {
        const courseList = DOM.container.querySelector('#course-list');
        if (!courses || courses.length === 0) {
            courseList.innerHTML = '<div class="gpa-error">无课程数据</div>';
            return;
        }

        courseList.innerHTML = courses.map(course => `
            <div class="course-item">
                <div class="course-info">
                    <div class="course-name">${course.name}</div>
                    <div class="course-code">${course.code}</div>
                    <div class="course-detail">
                        ${course.category} | ${course.credits}学分 | ${course.grade}
                    </div>
                </div>
                <div class="course-gpa">${course.gpa.toFixed(1)}</div>
            </div>
        `).join('');
    }

    /**
     * 更新历史记录列表
     */
    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();
                }
            });
        });
    }

    /**
     * 生成导出文本
     * @returns {string} 格式化的文本
     */
    function generateExportText() {
        if (!state.lastCalculatedGPA) return '暂无数据可导出';

        const data = state.lastCalculatedGPA;
        let text = `GPA计算结果\n`;
        text += `------------------------\n`;
        text += `总平均GPA: ${data.gpa.toFixed(4)}\n`;
        text += `总学分: ${data.totalCredits.toFixed(1)}\n`;
        text += `总绩点: ${data.totalGradePoints.toFixed(4)}\n`;
        text += `计算时间: ${new Date(data.timestamp).toLocaleString()}\n`;
        text += `------------------------\n\n`;

        text += `课程分类统计:\n`;
        for (const [category, stats] of Object.entries(data.categories)) {
            const categoryGPA = stats.totalPoints / stats.totalCredits;
            text += `${category}: GPA=${categoryGPA.toFixed(2)}, 学分=${stats.totalCredits.toFixed(1)}, 课程数=${stats.count}\n`;
        }
        text += `------------------------\n\n`;

        text += `课程列表 (共${data.courses.length}门):\n`;
        data.courses.forEach((course, index) => {
            text += `${index + 1}. ${course.name} (${course.code})\n`;
            text += `   分类: ${course.category}, 学分: ${course.credits}, GPA: ${course.gpa.toFixed(1)}, 成绩: ${course.grade}\n`;
        });

        return text;
    }

    /**
     * 生成CSV数据
     * @returns {string} CSV格式的字符串
     */
    function generateCSV() {
        if (!state.lastCalculatedGPA) return null;

        const data = state.lastCalculatedGPA;
        let csv = '课程代码,课程名称,课程分类,学分,GPA,成绩,绩点\n';

        data.courses.forEach(course => {
            csv += `${course.code},${course.name},${course.category},${course.credits},${course.gpa.toFixed(1)},${course.grade},${course.points.toFixed(2)}\n`;
        });

        return csv;
    }

    /**
     * 导出为CSV文件
     */
    function exportCSV() {
        const csv = generateCSV();
        if (!csv) {
            alert('暂无数据可导出');
            return;
        }

        const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
        const link = document.createElement('a');
        const url = URL.createObjectURL(blob);

        link.setAttribute('href', url);
        link.setAttribute('download', `GPA统计_${new Date().toISOString().split('T')[0]}.csv`);
        link.style.visibility = 'hidden';

        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
    }

    /**
     * 复制文本到剪贴板
     * @param {string} text - 要复制的文本
     */
    function copyToClipboard(text) {
        const textarea = document.createElement('textarea');
        textarea.value = text;
        textarea.style.position = 'fixed';
        document.body.appendChild(textarea);
        textarea.select();

        try {
            document.execCommand('copy');
            alert('复制成功');
        } catch (err) {
            console.error('复制失败:', err);
            alert('复制失败');
        }

        document.body.removeChild(textarea);
    }

    /**
     * 打印成绩单
     */
    function printGradeReport() {
        if (!state.lastCalculatedGPA) {
            alert('暂无数据可打印');
            return;
        }

        const data = state.lastCalculatedGPA;
        const printWindow = window.open('', '_blank');

        printWindow.document.write(`
            <html>
            <head>
                <title>成绩单 - ${new Date().toLocaleDateString()}</title>
                <style>
                    body {
                        font-family: Arial, sans-serif;
                        margin: 30px;
                        line-height: 1.5;
                    }
                    h1, h2, h3 {
                        color: #333;
                    }
                    .summary {
                        margin: 20px 0;
                        padding: 15px;
                        border: 1px solid #ddd;
                        border-radius: 5px;
                    }
                    .summary-item {
                        margin: 10px 0;
                    }
                    .value {
                        font-weight: bold;
                        color: #2196F3;
                    }
                    .category {
                        margin: 15px 0;
                        padding: 10px;
                        border-left: 4px solid #4CAF50;
                        background: #f9f9f9;
                    }
                    table {
                        width: 100%;
                        border-collapse: collapse;
                        margin: 20px 0;
                    }
                    th, td {
                        padding: 8px 12px;
                        text-align: left;
                        border-bottom: 1px solid #ddd;
                    }
                    th {
                        background-color: #f5f5f5;
                    }
                    tr:hover {
                        background-color: #f8f8f8;
                    }
                    .footer {
                        margin-top: 30px;
                        font-size: 12px;
                        color: #888;
                        text-align: center;
                    }
                    @media print {
                        body {
                            margin: 0.5cm;
                        }
                        .no-print {
                            display: none;
                        }
                    }
                </style>
            </head>
            <body>
                <h1>成绩单</h1>
                <div class="summary">
                    <div class="summary-item">总平均GPA: <span class="value">${data.gpa.toFixed(4)}</span></div>
                    <div class="summary-item">总学分: <span class="value">${data.totalCredits.toFixed(1)}</span></div>
                    <div class="summary-item">总绩点: <span class="value">${data.totalGradePoints.toFixed(4)}</span></div>
                    <div class="summary-item">计算时间: ${new Date(data.timestamp).toLocaleString()}</div>
                </div>

                <h2>课程分类统计</h2>
                <div class="categories">
        `);

        for (const [category, stats] of Object.entries(data.categories)) {
            const categoryGPA = stats.totalPoints / stats.totalCredits;
            printWindow.document.write(`
                <div class="category">
                    <h3>${category}</h3>
                    <div>GPA: <span class="value">${categoryGPA.toFixed(2)}</span></div>
                    <div>学分: ${stats.totalCredits.toFixed(1)}</div>
                    <div>课程数: ${stats.count}</div>
                </div>
            `);
        }

        printWindow.document.write(`
                </div>

                <h2>课程列表 (共${data.courses.length}门)</h2>
                <table>
                    <thead>
                        <tr>
                            <th>课程名称</th>
                            <th>课程代码</th>
                            <th>分类</th>
                            <th>学分</th>
                            <th>GPA</th>
                            <th>成绩</th>
                        </tr>
                    </thead>
                    <tbody>
        `);

        data.courses.forEach(course => {
            printWindow.document.write(`
                <tr>
                    <td>${course.name}</td>
                    <td>${course.code}</td>
                    <td>${course.category}</td>
                    <td>${course.credits}</td>
                    <td>${course.gpa.toFixed(1)}</td>
                    <td>${course.grade}</td>
                </tr>
            `);
        });

        printWindow.document.write(`
                    </tbody>
                </table>

                <div class="footer">
                    此成绩单由GPA计算器生成于 ${new Date().toLocaleString()}
                </div>

                <div class="no-print" style="text-align: center; margin-top: 20px;">
                    <button onclick="window.print()">打印</button>
                    <button onclick="window.close()">关闭</button>
                </div>
            </body>
            </html>
        `);

        printWindow.document.close();
    }

    /**
     * 初始化标签页切换功能
     */
    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);
        }
    }

    /**
     * 手动计算GPA
     */
    function handleManualCalculation() {
        // 清除错误信息
        const errorElement = DOM.container.querySelector('#manual-error');
        errorElement.style.display = 'none';

        // 获取输入值
        const credits = parseFloat(DOM.manualCredits.value);
        const points = parseFloat(DOM.manualPoints.value);

        // 验证输入
        if (isNaN(credits) || isNaN(points)) {
            errorElement.textContent = '请输入有效的数字';
            errorElement.style.display = 'block';
            return;
        }

        if (credits <= 0) {
            errorElement.textContent = '学分必须大于0';
            errorElement.style.display = 'block';
            return;
        }

        // 计算GPA
        const gpa = calculateManualGPA(credits, points);

        // 显示结果
        DOM.manualGpaResult.textContent = gpa.toFixed(4);
        DOM.manualGpaResult.style.animation = 'none';
        DOM.manualGpaResult.offsetHeight; // 触发重绘
        DOM.manualGpaResult.style.animation = 'fadeIn 0.5s ease-out';
    }

    /**
     * 绑定事件
     */
    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();
                }
            });
        }

        // 手动计算
        const calculateManualBtn = DOM.container.querySelector('#calculate-manual');
        if (calculateManualBtn) {
            calculateManualBtn.addEventListener('click', handleManualCalculation);
        }

        // 导出功能
        const exportTextBtn = DOM.container.querySelector('#export-text');
        if (exportTextBtn) {
            exportTextBtn.addEventListener('click', () => {
                const text = generateExportText();
                copyToClipboard(text);
            });
        }

        const exportCsvBtn = DOM.container.querySelector('#export-csv');
        if (exportCsvBtn) {
            exportCsvBtn.addEventListener('click', exportCSV);
        }

        const exportPrintBtn = DOM.container.querySelector('#export-print');
        if (exportPrintBtn) {
            exportPrintBtn.addEventListener('click', printGradeReport);
        }

        // 监听窗口大小变化,确保计算器不会超出界面
        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');
        DOM.manualCredits = DOM.container.querySelector('#manual-credits');
        DOM.manualPoints = DOM.container.querySelector('#manual-points');
        DOM.manualGpaResult = DOM.container.querySelector('#manual-gpa-result');
    }

    /**
     * 初始化应用
     */
    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();
})();