您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
szu获取详细成绩,包含平时成绩,期末成绩,平时成绩和期末成绩占比
// ==UserScript== // @name szu获取详细成绩 // @namespace http://greasyfork.icu/ // @version 1.2 // @description szu获取详细成绩,包含平时成绩,期末成绩,平时成绩和期末成绩占比 // @author You // @run-at document-start // @match *://ehall.szu.edu.cn/jwapp/sys/cjcx/* // @require https://scriptcat.org/lib/513/2.1.0/ElementGetter.js#sha256=aQF7JFfhQ7Hi+weLrBlOsY24Z2ORjaxgZNoni7pAz5U= // @license MIT // ==/UserScript== (function () { 'use strict'; const url = "https://ehall.szu.edu.cn/jwapp/sys/cjcx/modules/cjcx/xscjcx.do"; const cookie = document.cookie; async function fetchScores() { try { const headers = { "Cookie": cookie }; const response = await fetch(url, { method: "GET", headers }); const data = await response.json(); return data.datas.xscjcx.rows; } catch (error) { console.error("获取成绩失败:", error); return []; } } async function fetchPSCJ(value) { const requestBody = new URLSearchParams(); requestBody.append("querySetting", `[{"name":"PSCJ","value":${value},"linkOpt":"and","builder":"equal"}]`); requestBody.append("pageSize", "100"); requestBody.append("pageNumber", "1"); const headers = { "Content-Type": "application/x-www-form-urlencoded", "Cookie": cookie }; const response = await fetch(url, { method: "POST", headers, body: requestBody }); const data = await response.json(); return data.datas.xscjcx.rows; } async function fetchQMCJ(value) { const requestBody = new URLSearchParams(); requestBody.append("querySetting", `[{"name":"QMCJ","value":${value},"linkOpt":"and","builder":"equal"}]`); requestBody.append("pageSize", "100"); requestBody.append("pageNumber", "1"); const headers = { "Content-Type": "application/x-www-form-urlencoded", "Cookie": cookie }; const response = await fetch(url, { method: "POST", headers, body: requestBody }); const data = await response.json(); return data.datas.xscjcx.rows; } async function fetchAllScores() { const scores = await fetchScores(); const nameToRow = {}; scores.forEach(row => { nameToRow[row.KCM + row.XNXQDM_DISPLAY] = row; }); let totalSize = scores.length let count = totalSize for (let i = 100; i >= 0; i--) { const pscjRows = await fetchPSCJ(i); pscjRows.forEach(row => { const key = row.KCM + row.XNXQDM_DISPLAY; if (nameToRow[key]){ nameToRow[key].PSCJ = i.toString(); count--; } }); if (count <= 0) { break; } } count = totalSize for (let i = 100; i >= 0; i--) { const qmcjRows = await fetchQMCJ(i); qmcjRows.forEach(row => { const key = row.KCM + row.XNXQDM_DISPLAY; if (nameToRow[key]){ nameToRow[key].QMCJ = i.toString(); count--; } }); if (count <= 0) { break; } } const groupedScores = {}; Object.values(nameToRow).forEach(row => { const term = row.XNXQDM_DISPLAY; if (!groupedScores[term]) groupedScores[term] = []; groupedScores[term].push(row); }); // Sort the keys of groupedScores const sortedTerms = Object.keys(groupedScores).sort((a, b) => { // Extract the academic year and semester from the term strings const yearRegex = /^(\d{4})-(\d{4})学年/; const semesterRegex = /(第一|第二)学期$/; const extractYearSemester = (term) => { const yearMatch = term.match(yearRegex); const semesterMatch = term.match(semesterRegex); if (yearMatch && semesterMatch) { const startYear = parseInt(yearMatch[1], 10); const isFirstSemester = semesterMatch[1] === "第一"; return { startYear, isFirstSemester }; } return { startYear: 0, isFirstSemester: true }; // Default if no match }; const aTermDetails = extractYearSemester(a); const bTermDetails = extractYearSemester(b); // Compare years first if (aTermDetails.startYear !== bTermDetails.startYear) { return bTermDetails.startYear - aTermDetails.startYear; } // If years are equal, compare the semester (First semester comes before Second semester) return aTermDetails.isFirstSemester ? 1 : -1; }); // Now re-sort the groupedScores object by the sorted keys const sortedGroupedScores = {}; sortedTerms.forEach(term => { sortedGroupedScores[term] = groupedScores[term]; }); return sortedGroupedScores; } async function addScoreTab() { // 获取 <ul class="jqx-tabs-title-container"> const ul = await elmGetter.get(".jqx-tabs-title-container"); if (!ul) { console.error("未找到 jqx-tabs-title-container"); return; } // 创建新的 <li> 元素 const newLi = document.createElement("li"); newLi.setAttribute("role", "tab"); newLi.className = "jqx-reset jqx-disableselect jqx-tabs-title jqx-item jqx-rc-t jqx-fill-state-pressed"; newLi.style.float = "left"; // 创建 <div class="jqx-tabs-titleWrapper"> const titleWrapper = document.createElement("div"); titleWrapper.className = "jqx-tabs-titleWrapper"; titleWrapper.style.cssText = "outline: none; position: relative; z-index: 15; height: 100%;"; // 创建 <div class="jqx-tabs-titleContentWrapper"> const titleContentWrapper = document.createElement("div"); titleContentWrapper.className = "jqx-tabs-titleContentWrapper jqx-disableselect"; titleContentWrapper.style.cssText = "float: left; margin-top: -0.5px;"; titleContentWrapper.textContent = "详细成绩"; // 文字内容 // 组装 DOM 结构 titleWrapper.appendChild(titleContentWrapper); newLi.appendChild(titleWrapper); // 插入到 <ul> 中 ul.appendChild(newLi); // 创建新的内容元素 const newContentDiv = document.createElement("div"); newContentDiv.className = "cjcx-tab-content-2 bh-mt-8 jqx-tabs-content-element jqx-rc-b"; newContentDiv.setAttribute("role", "tabpanel"); newContentDiv.style.display = "none"; // 初始隐藏 // 插入到适当的容器中(假设有一个容器用于显示内容) const container = document.querySelector(".jqx-widget-content"); // 替换成你实际的容器 container.appendChild(newContentDiv); // 添加点击事件 newLi.addEventListener("click", async function () { // 移除所有 <li> 的 `jqx-tabs-title-selected-top` document.querySelectorAll(".jqx-tabs-title-container > li").forEach(li => { li.classList.remove("jqx-tabs-title-selected-top"); }); // 给当前 <li> 添加 `jqx-tabs-title-selected-top` newLi.classList.add("jqx-tabs-title-selected-top"); // 隐藏所有内容 document.querySelectorAll(".jqx-tabs-content-element").forEach(content => { content.style.display = "none"; }); // 显示加载中动画 const loadingSpinner = createLoadingSpinner(); newContentDiv.innerHTML = ''; // 清空内容 newContentDiv.appendChild(loadingSpinner); newContentDiv.style.display = 'block'; // 先检查本地存储是否有 SZU_ALLSCORES let allScores = localStorage.getItem("SZU_ALLSCORES"); if (allScores) { // 如果本地存储有数据,解析为 JSON allScores = JSON.parse(allScores); } else { // 如果本地没有数据,请求获取成绩 allScores = await fetchAllScores(); // 请求成功后,存储到本地存储 if (allScores) { localStorage.setItem("SZU_ALLSCORES", JSON.stringify(allScores)); } } // 渲染到 `newContentDiv` renderScores(allScores, newContentDiv); // 隐藏加载中动画 loadingSpinner.style.display = 'none'; // 显示 "详细成绩" 的内容 newContentDiv.style.display = "block"; }); const contents = [document.querySelector(".cjcx-tab-content-0"), document.querySelector(".cjcx-tab-content-1")]; // 监听所有 <li>,确保点击其他标签时,移除 "详细成绩" 选项卡的选中状态 document.querySelectorAll("ul.jqx-tabs-title-container > li").forEach((li, index) => { if (li !== newLi) { li.addEventListener("click", function () { // 输出索引 contents[index].style.display = "block" li.classList.add("jqx-tabs-title-selected-top"); newLi.classList.remove("jqx-tabs-title-selected-top"); newContentDiv.style.display = "none"; // 隐藏"详细成绩"内容 }); } }); } function renderScores(scores, container) { container.innerHTML = ""; // 清空内容 // 创建更新成绩按钮 const updateButton = document.createElement("button"); updateButton.innerHTML = "重新获取成绩"; // 修改按钮样式,添加圆角、阴影、渐变背景等 updateButton.style.cssText = ` padding: 8px 20px; font-size: 16px; background: #E6A23C; color: white; border: none; border-radius: 25px; cursor: pointer; box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.1), -2px -2px 10px rgba(255, 255, 255, 0.3); margin-right: 30px; `; container.appendChild(updateButton); // 添加按钮到 newContentDiv // 添加按钮点击事件 updateButton.addEventListener("click", async function () { // 显示加载中动画 const loadingSpinner = createLoadingSpinner(); container.innerHTML = ''; // 清空内容 container.appendChild(loadingSpinner); container.style.display = 'block'; const newAllScores = await fetchAllScores(); // 请求成功后,存储到本地存储 if (newAllScores) { localStorage.setItem("SZU_ALLSCORES", JSON.stringify(newAllScores)); } // 渲染到 newContentDiv renderScores(newAllScores, container); // 隐藏加载中动画 loadingSpinner.style.display = 'none'; }); if (!scores || Object.keys(scores).length === 0) { container.innerHTML = "<p style='text-align: center; font-size: 16px;'>暂无成绩数据</p>"; return; } Object.entries(scores).forEach(([semester, rows]) => { // 创建学期标题 const semesterTitle = document.createElement("div"); semesterTitle.style.cssText = "text-align: center; margin: 20px 0; font-size: 20px; font-weight: bold; color: #333;"; semesterTitle.textContent = semester; container.appendChild(semesterTitle); // 创建表格 const table = document.createElement("table"); table.style.cssText = "width: 100%; border-collapse: collapse; margin-bottom: 20px; background-color: #fff; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);"; // 表头 const thead = document.createElement("thead"); thead.innerHTML = ` <tr style="background-color: #3498db; color: white;"> <th style="padding: 10px; text-align: left;">课程名称</th> <th style="padding: 10px; text-align: left;">课程性质</th> <th style="padding: 10px; text-align: left;">学分</th> <th style="padding: 10px; text-align: left;">总成绩</th> <th style="padding: 10px; text-align: left;">等级成绩</th> <th style="padding: 10px; text-align: left;">平时成绩</th> <th style="padding: 10px; text-align: left;">期末成绩</th> <th style="padding: 10px; text-align: left;">平时成绩系数</th> <th style="padding: 10px; text-align: left;">期末成绩系数</th> </tr> `; table.appendChild(thead); // 表体 const tbody = document.createElement("tbody"); rows.forEach(row => { const tr = document.createElement("tr"); tr.style.cssText = "transition: background-color 0.3s ease;"; tr.innerHTML = ` <td style="padding: 10px; border-top: 1px solid #ddd;">${row.KCM}</td> <td style="padding: 10px; border-top: 1px solid #ddd;">${row.KCXZDM_DISPLAY}</td> <td style="padding: 10px; border-top: 1px solid #ddd;">${row.XF}</td> <td style="padding: 10px; border-top: 1px solid #ddd;">${row.ZCJ}</td> <td style="padding: 10px; border-top: 1px solid #ddd;">${row.DJCJMC}</td> <td style="padding: 10px; border-top: 1px solid #ddd;">${row.PSCJ}</td> <td style="padding: 10px; border-top: 1px solid #ddd;">${row.QMCJ}</td> <td style="padding: 10px; border-top: 1px solid #ddd;">${row.PSCJXS}%</td> <td style="padding: 10px; border-top: 1px solid #ddd;">${row.QMCJXS}%</td> `; // 鼠标悬停效果 tr.addEventListener('mouseenter', () => { tr.style.backgroundColor = "#f1f1f1"; }); tr.addEventListener('mouseleave', () => { tr.style.backgroundColor = ""; }); tbody.appendChild(tr); }); table.appendChild(tbody); container.appendChild(table); }); } // 创建加载动画元素 function createLoadingSpinner() { const spinner = document.createElement('div'); spinner.classList.add('loading-spinner'); spinner.style.border = '4px solid rgba(0, 0, 0, 0.1)'; spinner.style.borderLeftColor = '#3498db'; spinner.style.borderRadius = '50%'; spinner.style.width = '40px'; spinner.style.height = '40px'; spinner.style.animation = 'spin 1s linear infinite'; spinner.style.margin = '20px auto'; return spinner; } // 旋转动画关键帧 const style = document.createElement('style'); style.innerHTML = ` @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `; document.head.appendChild(style); addScoreTab(); })();