您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
现代化设计的GPA计算器,带有动画效果、深色模式和更多功能
当前为
// ==UserScript== // @name Enhanced GPA Calculator // @namespace http://tampermonkey.net/ // @version 2.0.0 // @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; // 获取课程名称 const courseNameElement = row.querySelector('td div.text-color-6.one-line-nowarp span[data-original-title="课程名称"]'); 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: 20px; right: 20px; 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; } .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(); })();