Greasy Fork

Greasy Fork is available in English.

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

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

当前为 2025-07-16 提交的版本,查看 最新版本

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