szu获取详细成绩

szu获取详细成绩,包含平时成绩,期末成绩,平时成绩和期末成绩占比

// ==UserScript==
// @name         szu获取详细成绩
// @namespace    https://greasyfork.org/
// @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();
})();