您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
在方正/西北大学教务系统页面添加一个统一的助手按钮。内置成绩分项导出、课程成绩构成查询(支持排序)和重修课程查询等功能,并支持汇总查询。
// ==UserScript== // @name 教务系统助手 (成绩导出 & 构成查询 & 重修查询 & 成绩汇总) // @namespace https://github.com/wzp100/nwu-jwgl-helper // @version 3.1.0-beta // @description 在方正/西北大学教务系统页面添加一个统一的助手按钮。内置成绩分项导出、课程成绩构成查询(支持排序)和重修课程查询等功能,并支持汇总查询。 // @author wzp100 (Gemini 辅助) xumoe-c (维护成绩汇总模块) // @match https://jwgl.nwu.edu.cn/jwglxt/* // @match *://*/jwglxt/* // @icon https://www.zfsoft.com/img/zf.ico // @grant GM_info // @grant GM_xmlhttpRequest // @grant GM_addStyle // @require https://code.jquery.com/jquery-3.6.0.min.js // @connect jwgl.nwu.edu.cn // @license Apache-2.0 // ==/UserScript== (function() { 'use strict'; /* ================================================================================= */ /* ============================ 全局UI与路由逻辑 ============================ */ /* ================================================================================= */ const assistant = { // --- 核心UI元素 --- elements: { assistantBtn: null, modal: null, modalTitle: null, modalContent: null, closeBtn: null, }, // --- 全局样式 --- addGlobalStyles: () => { GM_addStyle(` #assistant-btn { position: fixed; top: 10px; right: 150px; z-index: 9998; padding: 8px 15px; background-color: #28a745; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; box-shadow: 0 2px 5px rgba(0,0,0,0.2); } #assistant-btn:hover { background-color: #218838; } #assistant-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 80%; max-width: 950px; max-height: 80vh; background-color: white; border: 1px solid #ccc; border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.3); z-index: 10000; display: none; flex-direction: column; } .assistant-header { padding: 15px; background-color: #f7f7f7; border-bottom: 1px solid #ddd; display: flex; justify-content: space-between; align-items: center; } .assistant-header h2 { margin: 0; font-size: 18px; } .header-buttons { display: flex; align-items: center; gap: 10px; } .assistant-close-btn, .header-back-btn { padding: 5px 10px; border-radius: 4px; cursor: pointer; } .assistant-close-btn { background-color: #e0e0e0; border: 1px solid #ccc; } .header-back-btn { background-color: #6c757d; color: white; border: 1px solid #5a6268; } .header-back-btn:hover { background-color: #5a6268; } .assistant-content { padding: 20px; overflow-y: auto; flex-grow: 1; } /* 功能选择菜单按钮样式 */ .function-select-container { display: flex; flex-direction: column; gap: 15px; align-items: center; padding: 20px 0; } .function-select-btn { width: 60%; padding: 15px; font-size: 16px; border-radius: 8px; border: none; background-color: #28a745; color: white; cursor: pointer; text-align: center; transition: all 0.2s ease; } .function-select-btn:hover { background-color: #218838; box-shadow: 0 2px 5px rgba(0,0,0,0.1); } /* 查询控制区域统一样式 */ .query-controls { padding: 15px; display: flex; align-items: center; gap: 15px; border-bottom: 1px solid #eee; margin-bottom: 15px; } .query-controls label { font-weight: bold; margin-bottom: 0; } .query-controls select, .query-controls button { padding: 5px 10px; border-radius: 4px; border: 1px solid #ccc; } .query-controls button { background-color: #28a745; color: white; cursor: pointer; } .query-controls button:hover { background-color: #218838; } /* 结果表格样式 */ .result-table { width: 100%; border-collapse: collapse; table-layout: auto; } .result-table th, .result-table td { border: 1px solid #ddd; padding: 8px; text-align: center; white-space: nowrap; } .result-table th { background-color: #f2f2f2; position: sticky; top: -21px; /* 表头吸顶 */ } .sortable-header { cursor: pointer; user-select: none; } .sortable-header:hover { background-color: #e0e0e0; } /* 为课程名称列设置换行和左对齐 */ .result-table td.col-course-name { word-wrap: break-word; word-break: break-all; text-align: left; padding-left: 10px; white-space: normal; min-width: 180px; } .sortable-header::after { content: ''; display: inline-block; margin-left: 5px; opacity: 0.5; border-left: 5px solid transparent; border-right: 5px solid transparent; } .sort-asc::after { border-bottom: 5px solid #333; opacity: 1; border-top: none; } .sort-desc::after { border-top: 5px solid #333; opacity: 1; border-bottom: none; } #loading-spinner { border: 5px solid #f3f3f3; border-top: 5px solid #28a745; border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 20px auto; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `); }, // --- 创建基础UI框架 --- setupUI: () => { assistant.addGlobalStyles(); assistant.elements.assistantBtn = $('<button>', { id: 'assistant-btn', text: '教务助手' }).appendTo('body'); const modalHTML = ` <div id="assistant-modal"> <div class="assistant-header"> <h2 id="assistant-modal-title">教务助手</h2> <div class="header-buttons"> <button class="assistant-close-btn">关闭</button> </div> </div> <div id="assistant-modal-content" class="assistant-content"></div> </div>`; assistant.elements.modal = $(modalHTML).appendTo('body'); assistant.elements.modalTitle = $('#assistant-modal-title'); assistant.elements.modalContent = $('#assistant-modal-content'); assistant.elements.closeBtn = $('.assistant-close-btn'); assistant.elements.assistantBtn.on('click', assistant.openModal); assistant.elements.closeBtn.on('click', assistant.closeModal); }, openModal: () => { assistant.elements.modal.css('display', 'flex'); assistant.route(); }, closeModal: () => assistant.elements.modal.css('display', 'none'), toggleBackButton: (show) => { const container = $('.header-buttons'); container.find('.header-back-btn').remove(); if (show) { const backBtn = $('<button>', { text: '返回', class: 'header-back-btn' }); backBtn.on('click', assistant.showMainMenu); container.prepend(backBtn); } }, showMainMenu: () => { assistant.toggleBackButton(false); const content = ` <div class="function-select-container"> <p>您好,${assistant.getStudentId() ? 'NWU的同学!' : ''}请选择您需要使用的功能:</p> <button id="select-grade-component-query" class="function-select-btn">课程成绩构成查询(没有期末成绩)</button> <button id="select-grade-export" class="function-select-btn">学期成绩导出(可以查询已出成绩分项)</button> <button id="select-retake-query" class="function-select-btn">重修查询(提前查询成绩)</button> <button id="select-about" class="function-select-btn" style="background-color: #6c757d; margin-top: 10px;">关于脚本</button> </div> `; assistant.populateModal('教务系统助手v'+GM_info.script.version, content, () => { $('#select-grade-component-query').on('click', gradeComponentQuerier.configureModal); $('#select-grade-export').on('click', gradeExporter.configureModal); $('#select-retake-query').on('click', retakeQuerier.configureModal); $('#select-about').on('click', assistant.showAboutPage); }); }, showAboutPage: () => { assistant.toggleBackButton(true); const version = GM_info.script.version; const contentHTML = ` <div style="padding: 10px; font-size: 14px; line-height: 1.8;"> <h3 style="text-align: center; margin-bottom: 20px;">教务系统助手 v${version}</h3> <p><strong>简介:</strong></p> <p style="text-indent: 2em;"> 本脚本旨在为<strong>西北大学(NWU)</strong>新版正方教务系统提供一系列便捷的辅助功能,以增强用户体验,简化常见的查询与数据导出操作。 </p> <hr style="margin: 20px 0;"> <p><strong>主要功能:</strong></p> <ul> <li><strong>课程成绩构成查询:</strong> 以清晰的表格形式展示每门课程的详细成绩组成(如平时、期中、实验等),并支持全列点击排序。</li> <li><strong>学期成绩导出:</strong> 一键导出指定学年学期或全部学年的成绩单为 Excel (XLS) 文件。</li> <li><strong>重修查询:</strong> 快速查询指定学年需要重修或补考的课程列表。</li> <li><strong>成绩汇总(实验性功能):</strong> 仍在开发中,结果仅供参考。可汇总成绩分项、总评成绩、重修成绩,并可根据占比计算期末成绩。</li> </ul> <hr style="margin: 20px 0;"> <p><strong>项目地址:</strong></p> <p> 如果您有任何建议或发现BUG,欢迎通过以下 GitHub 仓库地址提交 Issue 或 Pull Request:<br> <a href="https://github.com/wzp100/nwu-jwgl-helper" target="_blank">https://github.com/wzp100/nwu-jwgl-helper</a> </p> <p style="margin-top: 30px; text-align: right; color: #888;"> 作者: wzp100 xumoe-c<br> AI 辅助: Gemini、Claude </p> </div> `; assistant.populateModal('关于脚本', contentHTML, null, false); }, populateModal: (title, contentHTML, onReady, addAllYearsOption = undefined) => { assistant.elements.modalTitle.text(title); assistant.elements.modalContent.html(contentHTML); if (addAllYearsOption !== undefined) { const yearSelect = assistant.elements.modalContent.find('.year-select'); if (yearSelect.length) { if (addAllYearsOption) { yearSelect.append(new Option('全部学年', '')); } const studentId = assistant.getStudentId(); const entryYear = studentId ? parseInt(studentId.substring(0, 4)) : new Date().getFullYear() - 4; const currentYear = new Date().getFullYear(); for (let year = entryYear; year <= currentYear; year++) { yearSelect.append(new Option(`${year}-${year + 1}学年`, year)); } if (addAllYearsOption) { yearSelect.val(''); } else { const defaultYear = (new Date().getMonth() < 8) ? currentYear - 1 : currentYear; yearSelect.val(defaultYear); } } } if (onReady && typeof onReady === 'function') { onReady(); } }, getStudentId: () => { const sessionUserKey_element = document.getElementById('sessionUserKey'); if (sessionUserKey_element && sessionUserKey_element.value) return sessionUserKey_element.value; const xh_id_element = document.getElementById('xh_id'); if (xh_id_element && xh_id_element.value) return xh_id_element.value; try { let studentId = new URLSearchParams(window.top.location.search).get('su'); if (studentId) return studentId; } catch (e) {} try { const userElement = window.top.document.querySelector('#sessionUser .media-body span.ng-binding'); if (userElement && userElement.textContent) { const match = userElement.textContent.match(/\d+/); if (match) return match[0]; } } catch (e) {} return null; }, route: () => { assistant.showMainMenu(); }, init: () => { assistant.setupUI(); } }; /* ================================================================================= */ /* =================== 功能一: 课程成绩构成查询 (支持排序) ================= */ /* ================================================================================= */ const gradeComponentQuerier = { processedData: [], dynamicHeaders: [], sortState: { key: 'xnmc', direction: 'desc' }, configureModal: () => { assistant.toggleBackButton(true); const content = ` <div class="query-controls"> <label for="gc-year-select">学年:</label> <select id="gc-year-select" class="year-select"></select> <label for="gc-semester-select">学期:</label> <select id="gc-semester-select"> <option value="">全部</option> <option value="3">第一学期</option> <option value="12">第二学期</option> </select> <button id="start-gc-query-btn">查询构成</button> </div> <div id="gc-result-content"><p style="text-align:center; color:#888;">请选择学年和学期后点击查询。</p></div>`; assistant.populateModal('课程成绩构成查询 (点击表头排序)', content, () => { if (!assistant.getStudentId()) { $('#gc-result-content').html('<p style="color:red; text-align:center;">错误:无法自动获取到您的学号!</p>'); $('#start-gc-query-btn').prop('disabled', true); return; } $('#start-gc-query-btn').on('click', gradeComponentQuerier.executeQuery); }, true); }, executeQuery: async () => { const year = $('#gc-year-select').val(); const semester = $('#gc-semester-select').val(); const resultContent = $('#gc-result-content'); resultContent.html('<div id="loading-spinner"></div><p style="text-align:center;">正在查询中...</p>'); try { const responseJson = await gradeComponentQuerier.fetchGradeComponentData(year, semester); if (responseJson && responseJson.items && responseJson.items.length > 0) { gradeComponentQuerier.processAndDisplay(responseJson.items); } else { resultContent.html('<p style="text-align:center; color: green;">查询完成,未找到相关成绩数据。</p>'); } } catch (error) { console.error('成绩构成查询出错:', error); resultContent.html(`<p style="color:red; text-align:center;">查询失败,请检查网络或查看浏览器控制台(F12)的错误信息。</p>`); } }, fetchGradeComponentData: (academicYear, semester) => { const url = '/jwglxt/cjcx/cjjdcx_cxXsjdxmcjIndex.html?doType=query&gnmkdm=N305099'; const formData = new URLSearchParams({ 'xnm': academicYear, 'xqm': semester, 'xh': '', '_search': 'false', 'nd': Date.now(), 'queryModel.showCount': '500', 'queryModel.currentPage': '1', 'queryModel.sortName': 'kch', 'queryModel.sortOrder': 'asc', 'time': '0' }); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "POST", url: url, data: formData.toString(), headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", "Accept": "application/json, text/javascript, */*; q=0.01", "X-Requested-With": "XMLHttpRequest" }, onload: function(response) { if (response.status === 200) { try { resolve(JSON.parse(response.responseText)); } catch (e) { resolve(null); } } else { reject(new Error(`HTTP status ${response.status}`)); } }, onerror: function(error) { reject(error); } }); }); }, processAndDisplay: function(items) { const componentHeaders = new Set(); items.forEach(item => { if (item.xmblmc) componentHeaders.add(item.xmblmc); }); this.dynamicHeaders = Array.from(componentHeaders).sort(); const courses = {}; items.forEach(item => { const key = item.jxb_id; if (!courses[key]) { courses[key] = { kcmc: item.kcmc, kch: item.kch, xnmc: item.xnmc, xqmc: item.xqmc === '3' ? '第一学期' : (item.xqmc === '12' ? '第二学期' : `第${item.xqmc}学期`), components: {} }; } courses[key].components[item.xmblmc] = item.xmcj; }); this.processedData = Object.values(courses); this.sortState = { key: 'xnmc', direction: 'desc' }; // 默认按学年降序 this.sortTable(this.sortState.key, true); // 初始排序和渲染 }, sortTable: function(newSortKey, preventFlip = false) { if (!preventFlip && this.sortState.key === newSortKey) { this.sortState.direction = this.sortState.direction === 'asc' ? 'desc' : 'asc'; } else { this.sortState.key = newSortKey; this.sortState.direction = 'asc'; } const isNumericSort = this.dynamicHeaders.includes(newSortKey); const dir = this.sortState.direction === 'asc' ? 1 : -1; this.processedData.sort((a, b) => { let valA, valB; if (isNumericSort) { valA = parseFloat(a.components[newSortKey] || -1); valB = parseFloat(b.components[newSortKey] || -1); } else { valA = a[newSortKey] || ''; valB = b[newSortKey] || ''; } if (typeof valA === 'string' && typeof valB === 'string') { return valA.localeCompare(valB, 'zh-Hans-CN') * dir; } return (valA > valB ? 1 : (valA < valB ? -1 : 0)) * dir; }); this.renderTable(); }, renderTable: function() { const fixedHeaders = { 'kcmc': '课程名称', 'kch': '课程代码', 'xnmc': '学年', 'xqmc': '学期' }; let tableHTML = `<table class="result-table"><thead><tr><th>序号</th>`; // 固定表头 for (const key in fixedHeaders) { tableHTML += `<th class="sortable-header" data-sort-key="${key}">${fixedHeaders[key]}</th>`; } // 动态成绩分项表头 this.dynamicHeaders.forEach(header => { tableHTML += `<th class="sortable-header" data-sort-key="${header}">${header}</th>`; }); tableHTML += `</tr></thead><tbody>`; this.processedData.forEach((course, index) => { tableHTML += `<tr> <td>${index + 1}</td> <td class="col-course-name">${course.kcmc}</td> <td>${course.kch}</td> <td>${course.xnmc}</td> <td>${course.xqmc}</td>`; this.dynamicHeaders.forEach(header => { const score = course.components[header] || '—'; tableHTML += `<td>${score}</td>`; }); tableHTML += `</tr>`; }); tableHTML += '</tbody></table>'; const resultContent = $('#gc-result-content'); resultContent.html(tableHTML); // 更新排序指示器并重新绑定事件 resultContent.find('.sortable-header').each((i, header) => { const JqHeader = $(header); if (JqHeader.data('sort-key') === this.sortState.key) { JqHeader.addClass(this.sortState.direction === 'asc' ? 'sort-asc' : 'sort-desc'); } JqHeader.on('click', () => this.sortTable(JqHeader.data('sort-key'))); }); } }; /* ================================================================================= */ /* =================== 功能二: 学期成绩导出 (支持全部学年) =================== */ /* ================================================================================= */ const gradeExporter = { configureModal: () => { assistant.toggleBackButton(true); const content = ` <div class="query-controls"> <label for="grade-year-select">学年:</label> <select id="grade-year-select" class="year-select"></select> <label for="grade-semester-select">学期:</label> <select id="grade-semester-select"> <option value="">全部学期</option> <option value="3">第一学期</option> <option value="12">第二学期</option> </select> <button id="start-export-btn">开始导出</button> </div> <div id="export-status" style="text-align:center; color:#888;">请选择学年学期后点击导出。</div>`; assistant.populateModal('学期成绩导出', content, () => { $('#start-export-btn').on('click', gradeExporter.handleExport); }, true); }, downloadFile: (blob, filename) => { const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); }, handleExport: async () => { const statusDiv = $('#export-status'); try { const xnm = $('#grade-year-select').val(); const xqm = $('#grade-semester-select').val(); const yearText = xnm === '' ? '全部学年' : `${xnm}-${parseInt(xnm) + 1}学年`; const semesterText = $('#grade-semester-select option:selected').text(); const fileName = `成绩单-${yearText}-${semesterText}-${Date.now()}.xlsx`; statusDiv.html('<div id="loading-spinner"></div><p>正在请求服务器生成文件...</p>'); const params = new URLSearchParams([ ['gnmkdmKey', 'N305005'], ['xnm', xnm], ['xqm', xqm], ['dcclbh', 'JW_N305005_GLY'], ...['kcmc@课程名称', 'xnmmc@学年', 'xqmmc@学期', 'kkbmmc@开课学院', 'kch@课程代码', 'jxbmc@教学班', 'xf@学分', 'xmcj@成绩', 'xmblmc@成绩分项'].map(col => ['exportModel.selectCol', col]), ['exportModel.exportWjgs', 'xls'], ['fileName', '成绩单'] ]); const response = await fetch('/jwglxt/cjcx/cjcx_dcXsKccjList.html', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: params }); if (!response.ok) throw new Error(`服务器返回异常: ${response.status}`); statusDiv.text('正在下载文件...'); const blob = await response.blob(); gradeExporter.downloadFile(blob, fileName); statusDiv.html('<p style="color: green;">导出成功!</p>'); } catch (error) { console.error('导出操作失败:', error); statusDiv.html(`<p style="color: red;">导出失败: ${error.message}</p>`); } }, }; /* ================================================================================= */ /* =================== 功能三: 重修查询 (按学年) ================== */ /* ================================================================================= */ const retakeQuerier = { currentCourseData: [], sortState: { key: 'cj', direction: 'asc' }, configureModal: () => { assistant.toggleBackButton(true); const content = ` <div class="query-controls"> <label for="retake-year-select">学年:</label> <select id="retake-year-select" class="year-select"></select> <label for="retake-semester-select">学期:</label> <select id="retake-semester-select"> <option value="">全部学期</option><option value="3">第一学期</option><option value="12">第二学期</option> </select> <button id="start-query-btn">开始查询</button> </div> <div id="retake-result-content"><p style="text-align:center; color:#888;">查询指定学年学期的课程。</p></div>`; assistant.populateModal('重修查询', content, () => { if (!assistant.getStudentId()) { $('#retake-result-content').html('<p style="color:red; text-align:center;">错误:无法自动获取到您的学号!</p>'); $('#start-query-btn').prop('disabled', true); return; } $('#start-query-btn').on('click', retakeQuerier.executeQuery); }, false); }, executeQuery: async () => { const studentId = assistant.getStudentId(); const year = $('#retake-year-select').val(); const semester = $('#retake-semester-select').val(); const resultContent = $('#retake-result-content'); resultContent.html('<div id="loading-spinner"></div><p style="text-align:center;">正在查询中...</p>'); let promises = []; if (semester === "") { promises.push(retakeQuerier.fetchRetakeData(studentId, year, "3")); promises.push(retakeQuerier.fetchRetakeData(studentId, year, "12")); } else { promises.push(retakeQuerier.fetchRetakeData(studentId, year, semester)); } try { const results = await Promise.all(promises); let allCourses = []; results.forEach(responseJson => { if (responseJson && responseJson.items) { responseJson.items.forEach(item => { if (item.cj && !allCourses.some(c => c.kch_id === item.kch_id && c.kcmc === item.kcmc)) { allCourses.push(item); } }); } }); retakeQuerier.sortState = { key: 'cj', direction: 'asc' }; allCourses.sort((a, b) => parseFloat(a.cj) - parseFloat(b.cj)); retakeQuerier.currentCourseData = allCourses; retakeQuerier.displayResults(); } catch (error) { console.error('查询出错:', error); resultContent.html(`<p style="color:red; text-align:center;">查询失败,请检查网络或查看浏览器控制台(F12)的错误信息。</p>`); } }, fetchRetakeData: (studentId, academicYear, semester) => { const url = `https://jwgl.nwu.edu.cn/jwglxt/cxbm/cxbm_cxXscxbmList.html?gnmkdm=N1056&su=${studentId}`; const formData = new URLSearchParams({ "doType": "query", "cxxnm": academicYear, "cxxqm": semester, "_search": "false", "nd": Date.now(), "queryModel.showCount": "1000", "queryModel.currentPage": "1", "queryModel.sortName": "cj", "queryModel.sortOrder": "asc" }); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "POST", url: url, data: formData.toString(), headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", "Accept": "application/json, text/javascript, */*; q=0.01", "X-Requested-With": "XMLHttpRequest" }, onload: function(response) { if (response.status === 200) { try { resolve(JSON.parse(response.responseText)); } catch (e) { resolve(null); } } else { reject(new Error(`HTTP status ${response.status}`)); } }, onerror: function(error) { reject(error); } }); }); }, sortTable: (newSortKey) => { if (retakeQuerier.sortState.key === newSortKey) { retakeQuerier.sortState.direction = retakeQuerier.sortState.direction === 'asc' ? 'desc' : 'asc'; } else { retakeQuerier.sortState.key = newSortKey; retakeQuerier.sortState.direction = 'asc'; } retakeQuerier.currentCourseData.sort((a, b) => { const sortKey = retakeQuerier.sortState.key; const sortDir = retakeQuerier.sortState.direction; let valA = a[sortKey], valB = b[sortKey]; if (['cj', 'xf', 'cxcj'].includes(sortKey)) { valA = parseFloat(valA) || -1; valB = parseFloat(valB) || -1; return sortDir === 'asc' ? valA - valB : valB - valA; } else { valA = (valA || '').toString(); valB = (valB || '').toString(); return sortDir === 'asc' ? valA.localeCompare(valB) : valB.localeCompare(valA); } }); retakeQuerier.displayResults(); }, displayResults: () => { const resultContent = $('#retake-result-content'); if (!retakeQuerier.currentCourseData || retakeQuerier.currentCourseData.length === 0) { resultContent.html('<p style="text-align:center; color: green; font-weight: bold;">查询完毕,该时间段内没有需要重修的课程。</p>'); return; } let tableHTML = `<table class="result-table"><thead><tr><th>序号</th><th class="sortable-header" data-sort-key="kcmc">课程名称</th><th class="sortable-header" data-sort-key="xf">学分</th><th>课程属性</th><th class="sortable-header" data-sort-key="cj">原始成绩</th><th class="sortable-header" data-sort-key="cxcj">重修成绩</th></tr></thead><tbody>`; retakeQuerier.currentCourseData.forEach((course, index) => { tableHTML += `<tr><td>${index + 1}</td><td>${course.kcmc ? course.kcmc.split('<br>')[0] : 'N/A'}</td><td>${course.xf || 'N/A'}</td><td>${course.kcsxmc || 'N/A'}</td><td style="color: red; font-weight: bold;">${course.cj || 'N/A'}</td><td>${course.cxcj || '无'}</td></tr>`; }); tableHTML += '</tbody></table>'; resultContent.html(tableHTML); const sortedHeader = resultContent.find(`.sortable-header[data-sort-key="${retakeQuerier.sortState.key}"]`); if(sortedHeader.length) { const sortClass = retakeQuerier.sortState.direction === 'asc' ? 'sort-asc' : 'sort-desc'; sortedHeader.addClass(sortClass); } resultContent.find('.sortable-header').each((i, header) => { $(header).on('click', () => retakeQuerier.sortTable(header.dataset.sortKey)); }); } }; /* ================================================================================= */ /* =================== 功能四: 成绩汇总(实验性功能) ================== */ /* ================================================================================= */ // 成绩汇总功能实现 const gradeIntegrator = { // 存储查询到的成绩构成和重修数据 componentData: [], retakeData: [], dynamicHeaders: [], // 配置弹窗 configureModal: function () { assistant.toggleBackButton(true); const content = ` <div class="query-controls"> <label for="gi-year-select">学年:</label> <select id="gi-year-select" class="year-select"></select> <label for="gi-semester-select">学期:</label> <select id="gi-semester-select"> <option value="">全部</option> <option value="3">第一学期</option> <option value="12">第二学期</option> </select> <button id="start-gi-query-btn">查询汇总</button> </div> <div id="gi-result-content"><p style="text-align:center; color:#888;">请选择学年和学期后点击查询。</p></div> `; assistant.populateModal('成绩汇总(实验性功能,结果仅供参考。后续更新将加入期末成绩计算、分项成绩占比展示等功能。)', content, () => { $('#start-gi-query-btn').on('click', gradeIntegrator.executeQuery); }, true); }, // 执行查询 executeQuery: async function () { const year = $('#gi-year-select').val(); const semester = $('#gi-semester-select').val(); const resultContent = $('#gi-result-content'); resultContent.html('<div id="loading-spinner"></div><p style="text-align:center;">正在查询中...</p>'); try { // 1. 查询成绩构成(功能一) const compJson = await gradeComponentQuerier.fetchGradeComponentData(year, semester); // 2. 查询重修成绩(功能三) const studentId = assistant.getStudentId(); let retakeJsonArr = []; if (semester === "") { retakeJsonArr = await Promise.all([ retakeQuerier.fetchRetakeData(studentId, year, "3"), retakeQuerier.fetchRetakeData(studentId, year, "12") ]); } else { retakeJsonArr = [await retakeQuerier.fetchRetakeData(studentId, year, semester)]; } // 整理重修数据 let retakeItems = []; retakeJsonArr.forEach(json => { if (json && json.items) { json.items.forEach(item => { if (item.kcmc && item.cj) { retakeItems.push({ kcmc: item.kcmc ? item.kcmc.split('<br>')[0] : '', cj: item.cj, cxcj: item.cxcj || '' }); } }); } }); // 整理成绩构成数据 if (!compJson || !compJson.items || compJson.items.length === 0) { resultContent.html('<p style="text-align:center; color: green;">查询完成,未找到相关成绩数据。</p>'); return; } // 复用功能一的处理逻辑 const componentHeaders = new Set(); compJson.items.forEach(item => { if (item.xmblmc) componentHeaders.add(item.xmblmc); }); gradeIntegrator.dynamicHeaders = Array.from(componentHeaders).sort(); // 以课程名称为key聚合 const courses = {}; compJson.items.forEach(item => { const key = item.kcmc; if (!courses[key]) { courses[key] = { kcmc: item.kcmc, kch: item.kch, xnmc: item.xnmc, xqmc: item.xqmc === '3' ? '第一学期' : (item.xqmc === '12' ? '第二学期' : `第${item.xqmc}学期`), components: {} }; } courses[key].components[item.xmblmc] = item.xmcj; }); // 合并重修成绩 Object.values(courses).forEach(course => { const retake = retakeItems.find(r => r.kcmc === course.kcmc); course.cj = retake ? retake.cj : ''; course.cxcj = retake ? retake.cxcj : ''; }); gradeIntegrator.componentData = Object.values(courses); gradeIntegrator.renderTable(); } catch (error) { console.error('成绩汇总查询出错:', error); resultContent.html(`<p style="color:red; text-align:center;">查询失败,请检查网络或查看浏览器控制台(F12)的错误信息。</p>`); } }, // 渲染表格 renderTable: function () { const fixedHeaders = { 'kcmc': '课程名称', 'kch': '课程代码', 'xnmc': '学年', 'xqmc': '学期' }; let tableHTML = `<table class="result-table"><thead><tr><th>序号</th>`; // 固定表头 for (const key in fixedHeaders) { tableHTML += `<th>${fixedHeaders[key]}</th>`; } // 动态成绩分项表头 gradeIntegrator.dynamicHeaders.forEach(header => { tableHTML += `<th>${header}</th>`; }); // 重修成绩表头 tableHTML += `<th>原始成绩</th><th>重修成绩</th>`; tableHTML += `</tr></thead><tbody>`; gradeIntegrator.componentData.forEach((course, index) => { tableHTML += `<tr> <td>${index + 1}</td> <td class="col-course-name">${course.kcmc}</td> <td>${course.kch}</td> <td>${course.xnmc}</td> <td>${course.xqmc}</td>`; gradeIntegrator.dynamicHeaders.forEach(header => { const score = course.components[header] || '—'; tableHTML += `<td>${score}</td>`; }); tableHTML += `<td style="color: red; font-weight: bold;">${course.cj || '—'}</td><td>${course.cxcj || '—'}</td>`; tableHTML += `</tr>`; }); tableHTML += '</tbody></table>'; $('#gi-result-content').html(tableHTML); } }; // 修正按钮调用 window.gradeIntigrater = gradeIntegrator; // 在主菜单添加入口 const oldShowMainMenu = assistant.showMainMenu; assistant.showMainMenu = function() { oldShowMainMenu(); // 动态插入按钮 setTimeout(() => { if ($('#select-grade-integrate').length === 0) { $('<button id="select-grade-integrate" class="function-select-btn" style="background-color:#007bff;">成绩汇总(实验性功能,结果仅供参考)</button>') .insertBefore('#select-about') .on('click', gradeIntigrater.configureModal); } }, 0); } // --- 启动脚本 --- $(document).ready(assistant.init); })();