您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
通过上传Excel文件,自动读取并录入平时成绩和期末成绩,并一键保存。
// ==UserScript== // @name 上财教务系统成绩一键录入 // @namespace http://tampermonkey.net/ // @version 2025-06-24 // @description 通过上传Excel文件,自动读取并录入平时成绩和期末成绩,并一键保存。 // @author wyih with Gemini // @match https://eams.sufe.edu.cn/eams/teach/grade/course/teacher-ga!input.action* // @icon https://www.google.com/s2/favicons?sz=64&domain=sufe.edu.cn // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/xlsx.full.min.js // @license MIT // @grant GM_addStyle // ==/UserScript== (function() { 'use strict'; /** * 创建并注入UI界面到页面 */ function createUI() { const toolbar = document.querySelector('.toolbar-items'); if (!toolbar) { console.error('上财教务助手:未找到工具栏,无法注入按钮。'); return; } const container = document.createElement('div'); container.id = 'grade-importer-container'; container.style.display = 'inline-block'; container.style.marginLeft = '20px'; container.style.verticalAlign = 'middle'; // 创建按钮和文件输入框的HTML container.innerHTML = ` <input type="file" id="excel-file-input" accept=".xlsx, .xls, .csv" style="display:none;"> <button id="upload-excel-btn" class="custom-btn">导入Excel成绩</button> <button id="save-grades-btn" class="custom-btn">保存成绩</button> <span id="importer-status" style="margin-left: 10px; font-weight: bold;"></span> `; // 将UI元素添加到工具栏中“后退”按钮的后面 const backButton = Array.from(toolbar.children).find(el => el.textContent.includes('后退')); if (backButton) { backButton.after(container); } else { toolbar.appendChild(container); } // 添加按钮样式 GM_addStyle(` .custom-btn { padding: 3px 12px; cursor: pointer; border: 1px solid #ccc; background-color: #f0f0f0; border-radius: 4px; font-size: 13px; margin: 0 5px; } .custom-btn:hover { background-color: #e0e0e0; border-color: #bbb; } `); // 绑定事件 document.getElementById('upload-excel-btn').addEventListener('click', () => { document.getElementById('excel-file-input').value = null; // 允许重复上传同一个文件 document.getElementById('excel-file-input').click(); }); document.getElementById('excel-file-input').addEventListener('change', handleFileSelect, false); document.getElementById('save-grades-btn').addEventListener('click', saveGrades); } /** * 处理文件选择和读取 * @param {Event} event - 文件选择事件 */ function handleFileSelect(event) { const file = event.target.files[0]; if (!file) return; updateStatus('正在读取文件...', 'blue'); const reader = new FileReader(); reader.onload = function(e) { try { const data = new Uint8Array(e.target.result); processExcelData(data); } catch (error) { console.error('文件读取或处理失败:', error); updateStatus('文件处理失败,请检查文件格式。', 'red'); alert('文件处理失败,请检查文件格式或查看控制台(F12)获取更多信息。'); } }; reader.readAsArrayBuffer(file); } /** * 处理读取到的Excel数据 * @param {Uint8Array} data - Excel文件数据 */ function processExcelData(data) { const workbook = XLSX.read(data, { type: 'array' }); const firstSheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[firstSheetName]; const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "" }); const studentGrades = parseStudentDataFromSheet(jsonData); if (studentGrades.length === 0) { updateStatus('未在Excel中找到学生数据。请检查文件格式。', 'red'); alert('未在Excel中找到学生数据。请确保表头包含"学号"、"平时"、"期末"等关键字。'); return; } const pageStudents = mapStudentsOnPage(); fillGrades(studentGrades, pageStudents); } /** * 从工作表数据中解析出学生信息 * @param {Array<Array<any>>} rows - 工作表数据 * @returns {Array<{studentId: string, usualScore: string, endScore: string}>} */ function parseStudentDataFromSheet(rows) { let headerRowIndex = -1; let idCol = -1, usualCol = -1, endCol = -1; // 查找包含“学号”、“平时”、“期末”的表头行 for (let i = 0; i < rows.length; i++) { const row = rows[i]; const idIndex = row.findIndex(cell => typeof cell === 'string' && cell.trim().includes('学号')); const usualIndex = row.findIndex(cell => typeof cell === 'string' && cell.trim().includes('平时')); const endIndex = row.findIndex(cell => typeof cell === 'string' && cell.trim().includes('期末')); if (idIndex !== -1 && usualIndex !== -1 && endIndex !== -1) { headerRowIndex = i; idCol = idIndex; usualCol = usualIndex; endCol = endIndex; break; } } if (headerRowIndex === -1) return []; const grades = []; for (let i = headerRowIndex + 1; i < rows.length; i++) { const row = rows[i]; if (!row || row.length < Math.max(idCol, usualCol, endCol)) continue; const studentId = row[idCol] ? String(row[idCol]).trim() : null; if (studentId && /^\d+$/.test(studentId)) { // 确保学号是数字字符串 grades.push({ studentId: studentId, usualScore: row[usualCol] ?? '', endScore: row[endCol] ?? '' }); } } return grades; } /** * 扫描页面,建立学号到学生信息输入框的映射 * @returns {Object.<string, {usualInput: HTMLElement, endInput: HTMLElement, index: string}>} */ function mapStudentsOnPage() { const studentMap = {}; const rows = document.querySelectorAll('.gridtable > tbody > tr'); rows.forEach(row => { // 跳过表头行 if (row.querySelector('td').textContent.trim() === '序号') return; const cells = row.querySelectorAll('td'); // 每行有两列学生数据 if (cells.length === 12) { // 处理第一列学生 processStudentCell(cells, 0, studentMap); // 处理第二列学生 processStudentCell(cells, 6, studentMap); } }); return studentMap; } /** * 处理单个学生的数据单元格 * @param {NodeListOf<HTMLTableCellElement>} cells - 当前行的所有单元格 * @param {number} startIndex - 学生数据开始的单元格索引 (0 或 6) * @param {object} studentMap - 存储映射结果的对象 */ function processStudentCell(cells, startIndex, studentMap) { const index = cells[startIndex].textContent.trim(); const studentId = cells[startIndex + 1].textContent.trim(); const usualInput = document.getElementById(`USUAL_${index}`); const endInput = document.getElementById(`END_${index}`); if (studentId && usualInput && endInput) { studentMap[studentId] = { usualInput, endInput, index }; } } /** * 将解析出的成绩填入页面表单 * @param {Array} studentGrades - 从Excel解析的成绩数据 * @param {Object} pageStudents - 从页面扫描的学生映射 */ function fillGrades(studentGrades, pageStudents) { let foundCount = 0; let notFoundIds = []; studentGrades.forEach(grade => { if (pageStudents[grade.studentId]) { const { usualInput, endInput } = pageStudents[grade.studentId]; usualInput.value = grade.usualScore; endInput.value = grade.endScore; // 触发onblur事件以自动计算总评成绩 usualInput.dispatchEvent(new Event('blur', { bubbles: true })); endInput.dispatchEvent(new Event('blur', { bubbles: true })); foundCount++; } else { notFoundIds.push(grade.studentId); } }); let statusMessage = `成功填入 ${foundCount} 名学生。`; if (notFoundIds.length > 0) { statusMessage += ` ${notFoundIds.length} 名学生未在页面中找到。`; console.warn('未在页面上找到以下学号:', notFoundIds); } updateStatus(statusMessage, 'green'); if (notFoundIds.length > 0) { alert(statusMessage + '\n\n详细学号列表请查看浏览器控制台 (按F12)。'); } } /** * 触发页面的保存功能 */ function saveGrades() { // 页面自带的保存函数是 `subGrade`, 位于全局作用域 if (typeof window.subGrade === 'function') { updateStatus('正在保存...', 'blue'); // 调用页面自带的保存函数 window.subGrade(true); // 注意:页面会提交并刷新,所以后续的JS可能不会执行 } else { alert('错误:未找到页面的保存功能函数(subGrade),无法自动保存。'); updateStatus('保存失败。', 'red'); } } /** * 更新状态显示文本 * @param {string} message - 要显示的消息 * @param {string} color - 文本颜色 */ function updateStatus(message, color) { const statusEl = document.getElementById('importer-status'); if (statusEl) { statusEl.textContent = message; statusEl.style.color = color; } } // --- 主程序入口 --- // 等待页面完全加载完毕,确保所有JS对象(如gradeTable)都已初始化 window.addEventListener('load', createUI); })();