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