Greasy Fork

Greasy Fork is available in English.

方正教务系统成绩分项下载

期末成绩不理想?担心被穿小鞋?不用怕!这款脚本让你期末成绩和平时成绩一目了然!支持VPN环境!

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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