您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
期末成绩不理想?担心被穿小鞋?不用怕!这款脚本让你期末成绩和平时成绩一目了然!支持VPN环境!
当前为
// ==UserScript== // @name 方正教务系统成绩分项下载 // @namespace [email protected] // @version 1.9 // @description 期末成绩不理想?担心被穿小鞋?不用怕!这款脚本让你期末成绩和平时成绩一目了然!支持VPN环境! // @author iKaiKail // @match *://*/jwglxt/cjcx/* // @include *:/*/cjcx/* // @icon https://www.zfsoft.com/img/zf.ico // @grant GM_addStyle // @require https://code.jquery.com/jquery-3.6.0.min.js // ==/UserScript== (function() { 'use strict'; GM_addStyle(` #score-detail-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 90%; max-width: 1000px; 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; overflow: hidden; } .score-modal-header { padding: 12px 15px; background-color: #2587de; display: flex; justify-content: space-between; align-items: center; border-radius: 8px 8px 0 0; } .score-modal-title { margin: 0; font-size: 16px; font-weight: bold; color: white; } .score-modal-close { background: none; border: none; font-size: 24px; font-weight: 200; opacity: 0.8; cursor: pointer; color: white; padding: 0; line-height: 1; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; border-radius: 4px; transition: all 0.3s ease; } .score-modal-close:hover { opacity: 1; background-color: rgba(255,255,255,0.3); } .score-modal-close span { position: relative; top: -2px; } .score-modal-content { padding: 20px; overflow-y: auto; flex-grow: 1; } .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: -1px; } .col-course-name { word-wrap: break-word; word-break: break-all; text-align: left; padding-left: 10px; white-space: normal; min-width: 180px; } #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); } } .sortable-header { cursor: pointer; user-select: none; } .sortable-header:hover { background-color: #e0e0e0; } .sort-asc::after { content: ''; display: inline-block; margin-left: 5px; border-left: 5px solid transparent; border-right: 5px solid transparent; border-bottom: 5px solid #333; } .sort-desc::after { content: ''; display: inline-block; margin-left: 5px; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 5px solid #333; } .modal-backdrop { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); z-index: 9999; display: none; } .score-action-btn { background-color: #337ab7 !important; color: white !important; border: none !important; } .score-action-btn:hover { background-color: #286090 !important; } #queryScoresBtn { margin-left: 12px; margin-right: 12px; } #exportAllScoresBtn { margin-left: 0; margin-right: 0; } @media (max-width: 767px) { .col-md-4.col-sm-5 button { margin-top: 8px; margin-left: 0 !important; margin-right: 8px !important; } .col-md-4.col-sm-5 button:last-child { margin-right: 0 !important; } } `); const createScoreModal = () => { const modalHTML = ` <div id="score-detail-modal"> <div class="score-modal-header"> <h4 class="score-modal-title">成绩分项详情</h4> <button class="score-modal-close" type="button"> <span>×</span> </button> </div> <div class="score-modal-content" id="score-modal-content"> <div id="loading-spinner"></div> <p style="text-align:center;">正在加载成绩数据...</p> </div> </div> <div class="modal-backdrop" id="modal-backdrop"></div> `; $('body').append(modalHTML); $('.score-modal-close').click(closeScoreModal); $('#modal-backdrop').click(closeScoreModal); }; const openScoreModal = () => { $('#score-detail-modal, #modal-backdrop').show(); }; const closeScoreModal = () => { $('#score-detail-modal, #modal-backdrop').hide(); }; const createButtons = () => { return { $queryButton: $('<button>', { type: 'button', class: 'btn btn-sm score-action-btn', text: '查询分项成绩', id: 'queryScoresBtn' }), $downloadButton: $('<button>', { type: 'button', class: 'btn btn-sm score-action-btn', text: '导出分项成绩', id: 'exportAllScoresBtn' }) }; }; const downloadFile = (blob, filename = `成绩单_${Date.now()}.xlsx`) => { 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); }; const getBasePath = () => { const currentPath = window.location.pathname; const cjcxIndex = currentPath.indexOf('/cjcx/'); if (cjcxIndex !== -1) return currentPath.substring(0, cjcxIndex); const lastSlashIndex = currentPath.lastIndexOf('/'); if (lastSlashIndex !== -1) return currentPath.substring(0, lastSlashIndex); return '/jwglxt'; }; const 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; }; const fetchGradeComponentData = async (academicYear, semester) => { const studentId = getStudentId(); if (!studentId) throw new Error('无法获取学号信息'); const url = `${getBasePath()}/cjcx/cjjdcx_cxXsjdxmcjIndex.html?doType=query&gnmkdm=N305099`; const formData = new URLSearchParams({ 'xnm': academicYear, 'xqm': semester, 'xh': studentId, '_search': 'false', 'nd': Date.now(), 'queryModel.showCount': '500', 'queryModel.currentPage': '1', 'queryModel.sortName': 'kch', 'queryModel.sortOrder': 'asc', 'time': '0' }); const response = await fetch(url, { method: "POST", headers: {"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"}, body: formData }); if (!response.ok) throw new Error(`请求失败: ${response.status}`); return await response.json(); }; const processAndDisplayScores = (items) => { if (!items || items.length === 0) { $('#score-modal-content').html('<p style="text-align:center; color: green;">没有找到成绩数据</p>'); return; } const componentHeaders = new Set(); items.forEach(item => { if (item.xmblmc) componentHeaders.add(item.xmblmc); }); const dynamicHeaders = Array.from(componentHeaders).sort(); const courses = {}; items.forEach(item => { const key = item.jxb_id || `${item.kch}_${item.jxbmc}`; if (!courses[key]) { courses[key] = { kcmc: item.kcmc, kch: item.kch, xnmmc: item.xnmc, xqmmc: item.xqmc === '3' ? '第一学期' : (item.xqmc === '12' ? '第二学期' : `第${item.xqmc}学期`), jxbmc: item.jxbmc, xmcj: item.xmcj, components: {} }; } courses[key].components[item.xmblmc] = item.xmcj; }); 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="xnmmc">学年</th><th class="sortable-header" data-sort-key="xqmmc">学期</th><th class="sortable-header" data-sort-key="kch">课程代码</th><th class="sortable-header" data-sort-key="jxbmc">教学班</th><th class="sortable-header" data-sort-key="xmcj">总成绩</th>`; dynamicHeaders.forEach(header => { tableHTML += `<th class="sortable-header" data-sort-key="${header}">${header}</th>`; }); tableHTML += `</tr></thead><tbody>`; let index = 1; Object.values(courses).forEach(course => { tableHTML += `<tr><td>${index++}</td><td class="col-course-name">${course.kcmc}</td><td>${course.xnmmc}</td><td>${course.xqmmc}</td><td>${course.kch}</td><td>${course.jxbmc}</td><td>${course.xmcj}</td>`; dynamicHeaders.forEach(header => { tableHTML += `<td>${course.components[header] || '—'}</td>`; }); tableHTML += `</tr>`; }); tableHTML += `</tbody></table>`; $('#score-modal-content').html(tableHTML); $('.sortable-header').click(function() { const sortKey = $(this).data('sort-key'); const $table = $(this).closest('table'); const $rows = $table.find('tbody > tr').get(); const isAsc = $(this).hasClass('sort-asc'); $(this).closest('tr').find('.sort-asc, .sort-desc').removeClass('sort-asc sort-desc'); $(this).toggleClass('sort-asc', !isAsc).toggleClass('sort-desc', isAsc); $rows.sort((a, b) => { const columnIndex = $(this).index(); const aVal = $(a).find(`td:eq(${columnIndex})`).text().trim(); const bVal = $(b).find(`td:eq(${columnIndex})`).text().trim(); if (!isNaN(aVal) && !isNaN(bVal) && aVal !== '—' && bVal !== '—') { return isAsc ? parseFloat(aVal) - parseFloat(bVal) : parseFloat(bVal) - parseFloat(aVal); } return isAsc ? aVal.localeCompare(bVal, 'zh') : bVal.localeCompare(aVal, 'zh'); }); $.each($rows, (index, row) => { $table.find('tbody').append(row); }); }); }; const handleQuery = async () => { try { const xnm = document.getElementById('xnm').value; const xqm = document.getElementById('xqm').value; if (!xnm || !xqm) throw new Error('请先选择学年和学期'); openScoreModal(); $('#score-modal-content').html(`<div id="loading-spinner"></div><p style="text-align:center;">正在查询成绩数据,请稍候...</p>`); const response = await fetchGradeComponentData(xnm, xqm); if (response && response.items) processAndDisplayScores(response.items); else $('#score-modal-content').html('<p style="text-align:center; color: green;">没有找到成绩数据</p>'); } catch (error) { console.error('查询操作失败:', error); $('#score-modal-content').html(`<p style="color:red; text-align:center;">查询失败: ${error.message}</p><button class="btn btn-primary" style="display:block; margin: 10px auto;" onclick="location.reload()">刷新重试</button>`); } }; const handleExport = async () => { try { const xnm = document.getElementById('xnm').value; const xqm = document.getElementById('xqm').value; if (!xnm || !xqm) throw new Error('请先选择学年和学期'); 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 targetUrl = `${getBasePath()}/cjcx/cjcx_dcXsKccjList.html`; const response = await fetch(targetUrl, { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: params }); if (!response.ok) throw new Error(`服务器返回异常状态码: ${response.status}`); const blob = await response.blob(); downloadFile(blob); const btn = document.getElementById('exportAllScoresBtn'); if (btn) { const originalText = btn.innerHTML; btn.innerHTML = ' 导出成功'; btn.style.backgroundColor = '#218838'; setTimeout(() => { btn.innerHTML = originalText; btn.style.backgroundColor = ''; }, 2000); } } catch (error) { console.error('导出操作失败:', error); const btn = document.getElementById('exportAllScoresBtn'); if (btn) { const originalText = btn.innerHTML; btn.innerHTML = ' 导出失败'; btn.style.backgroundColor = '#dc3545'; setTimeout(() => { btn.innerHTML = originalText; btn.style.backgroundColor = ''; }, 2000); } alert(`导出失败: ${error.message}`); } }; const init = () => { $('#queryScoresBtn, #exportAllScoresBtn').remove(); createScoreModal(); const buttons = createButtons(); buttons.$queryButton.click(handleQuery); buttons.$downloadButton.click(handleExport); const $searchButton = $('#search_go'); if ($searchButton.length) $searchButton.after(buttons.$downloadButton).after(buttons.$queryButton); else if ($('.panel-info:has(.panel-body)').length) $('.panel-info:has(.panel-body)').find('.panel-body').append(buttons.$queryButton).append(buttons.$downloadButton); else if ($('form:has(#xnm)').length) $('form:has(#xnm)').append(buttons.$queryButton).append(buttons.$downloadButton); else $('body').prepend(buttons.$queryButton).prepend(buttons.$downloadButton); }; const checkDOM = () => { if ($('#xnm, #xqm').length >= 2) init(); else setTimeout(checkDOM, 500); }; setTimeout(checkDOM, 1000); $(document).ajaxStop(function() { if (!$('#queryScoresBtn').length) setTimeout(init, 500); }); })();