宜宾学院评教助手

自动完成宜宾学院教学评价,支持自定义评分(90-95分),自动填写评语,一键完成评教

// ==UserScript==
// @name         宜宾学院评教助手
// @namespace    宜宾学院教务系统评教自动化工具,帮助学生快速完成教学评价===来自计算机科学与技术学院
// @version      2.0
// @description  自动完成宜宾学院教学评价,支持自定义评分(90-95分),自动填写评语,一键完成评教
// @author       计算机科学与技术学院---软工
// @match        https://ehall.yibinu.edu.cn/jwapp/sys/jwwspj/*
// @icon         https://pic.imgdb.cn/item/673c85b1d29ded1a8ce8b97c.png
// @grant        none
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 添加样式
    const style = document.createElement('style');
    style.textContent = `
        .auto-eval-btn {
            padding: 8px 15px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
        }
        .auto-eval-btn:hover {
            background-color: #45a049;
        }
        .score-dialog {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.2);
            z-index: 10000;
            max-height: 80vh;
            overflow-y: auto;
            min-width: 300px;
        }
        .eval-status {
            position: fixed;
            top: 50px;
            right: 10px;
            z-index: 9999;
            padding: 10px;
            background-color: #fff;
            border: 1px solid #ddd;
            border-radius: 4px;
            max-width: 300px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
        }
        .eval-complete-dialog {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 30px;
            border-radius: 12px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.2);
            text-align: center;
            z-index: 10001;
            min-width: 300px;
        }
        .eval-complete-dialog h2 {
            color: #4CAF50;
            margin: 0 0 20px 0;
        }
        .eval-complete-dialog .icon {
            font-size: 48px;
            margin-bottom: 20px;
            color: #4CAF50;
        }
        .eval-complete-dialog .message {
            color: #666;
            margin-bottom: 20px;
        }
        .eval-complete-dialog button {
            padding: 8px 20px;
            background: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
        }
        .eval-complete-dialog button:hover {
            background: #45a049;
        }
        .eval-complete-dialog .author-info {
            color: #888;
            font-size: 12px;
            margin-top: 15px;
            padding-top: 15px;
            border-top: 1px solid #eee;
        }
        .eval-complete-dialog .author-info span {
            color: #4CAF50;
            font-weight: bold;
        }
    `;
    document.head.appendChild(style);

    // 状态显示函数
    let statusDiv = null;
    function showStatus(message, duration = 3000) {
        if (!statusDiv) {
            statusDiv = document.createElement('div');
            statusDiv.className = 'eval-status';
            document.body.appendChild(statusDiv);
        }
        statusDiv.textContent = message;
        statusDiv.style.display = 'block';
        console.log(message);

        // 添加淡出动画样式
        statusDiv.style.transition = 'opacity 0.5s ease-in-out';

        // 设置定时使提示消失
        setTimeout(() => {
            statusDiv.style.opacity = '0';
            setTimeout(() => {
                statusDiv.style.display = 'none';
                statusDiv.style.opacity = '1';
            }, 500);
        }, duration);
    }

    // 等待函数
    function wait(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    // 创建分数设置对话框
    function createScoreDialog() {
        const dialog = document.createElement('div');
        dialog.className = 'score-dialog';

        // 获取所有教师
        const teachers = [];
        const teacherElements = document.querySelectorAll('.sc-panel-js');
        teacherElements.forEach(el => {
            const name = el.textContent.trim();
            const course = el.closest('.kc-js-array').querySelector('.sc-panel-kc').textContent.trim();
            // 使用教师名+课程名作为唯一标识
            const teacherId = `${name}_${course}`;
            if (!teachers.some(t => t.id === teacherId)) {
                teachers.push({ id: teacherId, name, course });
            }
        });

        // 创建对话框内容
        let html = '<h3>教师评分设置(建议90-95分)</h3>';

        // 批量设置
        html += `
            <div style="margin-bottom: 15px; padding: 10px; border: 1px solid #eee; border-radius: 4px;">
                <div>批量设置:
                    <input type="number" id="batchScore" value="92" min="0" max="95" style="width: 60px; padding: 4px;">
                    <button id="applyBatchBtn" style="padding: 4px 8px; background: #4CAF50; color: white; border: none; border-radius: 3px; cursor: pointer;">应用到全部</button>
                </div>
            </div>
        `;

        // 教师列表
        teachers.forEach(teacher => {
            html += `
                <div style="margin-bottom: 15px; padding: 10px; border: 1px solid #eee; border-radius: 4px;">
                    <div style="font-weight: bold; margin-bottom: 5px;">${teacher.name}</div>
                    <div style="font-size: 12px; color: #666; margin-bottom: 8px;">${teacher.course}</div>
                    <div style="display: flex; align-items: center; gap: 10px;">
                        <input type="number"
                            class="teacher-score"
                            data-teacher-id="${teacher.id}"
                            value="${window.teacherScores[teacher.id] || 92}"
                            min="0"
                            max="95"
                            style="width: 80px; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
                        <span style="color: #666; font-size: 12px;">分 (建议90-95分)</span>
                    </div>
                </div>
            `;
        });

        // 按钮
        html += `
            <div style="margin-top: 15px; text-align: right;">
                <button id="cancelBtn" style="padding: 6px 12px; margin-right: 10px; background: #f5f5f5; border: none; border-radius: 4px; cursor: pointer;">取消</button>
                <button id="confirmBtn" style="padding: 6px 12px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">确定</button>
            </div>
        `;

        dialog.innerHTML = html;
        document.body.appendChild(dialog);

        // 绑定事件
        dialog.querySelector('#applyBatchBtn').addEventListener('click', window.applyBatchScore);
        dialog.querySelector('#cancelBtn').addEventListener('click', window.closeScoreDialog);
        dialog.querySelector('#confirmBtn').addEventListener('click', window.saveScores);

        // 添加输入验证
        dialog.querySelectorAll('input[type="number"]').forEach(input => {
            input.addEventListener('input', (e) => {
                let value = parseInt(e.target.value) || 0;
                value = Math.min(95, Math.max(0, value));
                e.target.value = value;
            });
        });
    }

    // 在评教函数前添加评语数组
    const teacherComments = [
        '老师授课的方式非常适合我们,他根据本课程知识结构的特点,重点突出,层次分明。和实际相结合,通过例题使知识更条理化。',
        '老师授课有条理,有重点,对同学既热情又严格,是各位老师学习的榜样。',
        '老师上课有时非常幽默,有时非常严格,不过还是非常有教授风度的。课堂氛围非常好!',
        '教师在课堂上通过丰富的教学方法和生动的案例,有效提高了学生的学习兴趣,增强了知识的吸收与理解能力。',
        '这位老师耐心细致,善于引导学生思考,课堂氛围活跃,学生积极参与,真正做到了寓教于乐。',
        '课程内容丰富深刻,老师的讲解条理清晰,能够将复杂的概念简单化,使学生在轻松愉快的环境中掌握知识。',
        '老师非常关心学生的学习进度,针对不同学生的需求,提供个性化的指导,帮助我们克服学习中的困难。',
        '教师善于与学生沟通,倾听我们的意见与建议,鼓励我们提出问题,让课堂成为一个充满互动和创造力的学习场所。',
        '课堂上教师运用多媒体辅助教学,生动形象地展示知识,激发了我们的学习热情,使我们对课程内容产生了浓厚的兴趣。',
        '这位老师不仅学识渊博,而且为人师表,言传身教,让我们不仅仅学到知识,更领悟了学习的态度。',
        '老师重视学术诚信,始终鼓励我们独立思考,抵制抄袭和依赖,使我们懂得诚信的重要性,塑造了良好学风。',
        '教师对学生的问题总是给予积极反馈,鼓励我们提出更多的疑问,这对促进我们的思考能力非常有帮助。',
        '这位老师热情洋溢,上课时能有效调动学生的积极性,使得课堂不再枯燥,激发了我们的学习热情。',
        '老师的教案准备得非常充分,知识点安排合理,有助于我们进行有效的复习,帮助我们提高学习效率。',
        '课堂氛围轻松愉快,教师善于使用幽默的方式讲解知识,减轻了我们的学习压力,让我们能更好地吸收。',
        '这位老师非常注重学生的反馈,经常根据我们的意见进行调整,使得课程的灵活性和适应性越来越强。',
        '教师的教学理念先进,强调批判性思维的培养,让我们在学习中不断提出问题,探索新的知识和解决方案。',
        '老师非常关心学生的全面发展,除了学业追求,还鼓励我们在课外参加丰富的活动,提升综合素质。',
        '教师非常关注学生的心理健康,善于倾听我们的问题,并给予恰当的建议,让我们在学习中更加自信。',
        '这位老师把实际案例与理论教学结合得很好,让我们在理解知识的同时,也能看到其实际应用的价值。'
    ];

    // 评教函数
    async function startEvaluation() {
        try {
            showStatus('开始评教流程...');

            // 等待页面加载
            await wait(2000);

            // 选择分数
            const scoreContainers = document.querySelectorAll('.sc-panel-content.bh-clearfix.bh-mv-8.wjzb-card-jskc');
            if (!scoreContainers || scoreContainers.length === 0) {
                throw new Error('未找到评分项');
            }

            // 按教师分组处理评分项
            const teacherQuestions = {};
            scoreContainers.forEach(container => {
                const teacherElement = container.querySelector('.sc-panel-js');
                if (!teacherElement) return; // 跳过没有教师名的容器

                const teacherName = teacherElement.textContent.trim();
                const courseElement = teacherElement.closest('.kc-js-array').querySelector('.sc-panel-kc');
                if (!courseElement) return;

                const courseName = courseElement.textContent.trim();
                // 使用教师名+课程名作为唯一标识
                const teacherId = `${teacherName}_${courseName}`;

                if (!teacherQuestions[teacherId]) {
                    teacherQuestions[teacherId] = [];
                }
                teacherQuestions[teacherId].push(container);
            });

            // 处理每个教师的评分
            for (let teacherId in teacherQuestions) {
                const targetScore = window.teacherScores[teacherId] || 92;
                const questions = teacherQuestions[teacherId];

                // 计算需要减去的总分数
                const pointsToDeduct = 100 - targetScore;
                let remainingPoints = pointsToDeduct;

                // 将题目按分值分组
                const questionGroups = {
                    high: [], // 10分以上
                    medium: [], // 5-9分
                    low: [] // 2-4分
                };

                questions.forEach(container => {
                    const buttons = container.querySelectorAll('.fzItem.bh-pull-left');
                    if (!buttons || buttons.length <= 2) return;

                    const maxScore = buttons.length;
                    if (maxScore >= 10) {
                        questionGroups.high.push(container);
                    } else if (maxScore >= 5) {
                        questionGroups.medium.push(container);
                    } else {
                        questionGroups.low.push(container);
                    }
                });

                // 随机决定是否在高分题目中扣分
                const shouldDeductFromHigh = Math.random() < 0.3; // 30%概率从高分题扣

                // 处理每个分值组的题目
                for (let container of questions) {
                    const scoreButtons = container.querySelectorAll('.fzItem.bh-pull-left');
                    if (!scoreButtons || scoreButtons.length === 0) continue;

                    const maxScore = scoreButtons.length;
                    let targetIndex = maxScore - 1; // 默认满分

                    if (remainingPoints > 0) {
                        // 根据题目分值和随机性决定是否扣分
                        let shouldDeduct = false;
                        let maxDeduct = 0;

                        if (maxScore >= 10) {
                            // 高分题目 (10分及以上)
                            shouldDeduct = Math.random() < 0.8; // 80%概率扣分
                            maxDeduct = shouldDeduct ? 2 : 0;
                        } else if (maxScore >= 7) {
                            // 较高分题目 (7-9分)
                            shouldDeduct = Math.random() < 0.6; // 60%概率扣分
                            maxDeduct = 2;
                        } else if (maxScore >= 5) {
                            // 中分题目 (5-6分)
                            shouldDeduct = Math.random() < 0.4; // 40%概率扣分
                            maxDeduct = 1;
                        } else if (maxScore > 2) {
                            // 低分题目 (3-4分)
                            shouldDeduct = Math.random() < 0.2; // 20%概率扣分
                            maxDeduct = 1;
                        }

                        if (shouldDeduct) {
                            // 根据剩余需要扣的分数调整实际扣分
                            let actualDeduct = Math.min(maxDeduct, remainingPoints);

                            // 对于高分值题目,确保至少扣1分
                            if (maxScore >= 7 && actualDeduct > 0) {
                                actualDeduct = Math.max(1, actualDeduct);
                            }

                            if (actualDeduct > 0) {
                                targetIndex = maxScore - 1 - actualDeduct;
                                remainingPoints -= actualDeduct;
                            }
                        }
                    }

                    // 确保索引在有效范围内并点击
                    targetIndex = Math.max(0, Math.min(targetIndex, maxScore - 1));
                    if (scoreButtons[targetIndex] && !scoreButtons[targetIndex].classList.contains('active')) {
                        scoreButtons[targetIndex].click();
                        await wait(100);
                    }
                }
            }

            // 创建评语副本用于随机选择
            let availableComments = [...teacherComments];

            // 填写评语
            const textareas = document.querySelectorAll('.bh-txt-input__txtarea');
            for (let textarea of textareas) {
                // 如果评语用完了,重新填充
                if (availableComments.length === 0) {
                    availableComments = [...teacherComments];
                }

                // 随机选择一条评语并从可用列表中移除
                const randomIndex = Math.floor(Math.random() * availableComments.length);
                const comment = availableComments.splice(randomIndex, 1)[0];

                textarea.value = comment;
                textarea.dispatchEvent(new Event('input', { bubbles: true }));
                textarea.dispatchEvent(new Event('change', { bubbles: true }));
                await wait(100);
            }

            // 修改提交后的提示
            const submitBtn = document.querySelector('a.bh-btn.bh-btn-success.bh-btn-large[data-action="提交"]');
            if (submitBtn) {
                submitBtn.click();
                showStatus('正在提交评教...', 2000);
                await wait(2000);
                showCompleteDialog();
            }
        } catch (error) {
            console.error('评教过程出错:', error);
            showStatus('评教出错:' + error.message);
            alert('评教出错:' + error.message + '\n请刷新页面后重试');
        }
    }

    // 初始化函数
    async function initialize() {
        try {
            if (document.readyState !== 'complete') {
                await new Promise(resolve => window.addEventListener('load', resolve));
            }
            await wait(3000);
            showStatus('页面加载完成,正在初始化评教脚本...');
            addButton();
        } catch (error) {
            console.error('初始化失败:', error);
            showStatus('初始化失败:' + error.message);
        }
    }

    // 启动脚本
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initialize);
    } else {
        initialize();
    }

    // 添加按钮函数
    function addButton() {
        const buttonContainer = document.createElement('div');
        buttonContainer.style.position = 'fixed';
        buttonContainer.style.top = '10px';
        buttonContainer.style.right = '10px';
        buttonContainer.style.zIndex = '9999';

        // 设置分数按钮
        const scoreButton = document.createElement('button');
        scoreButton.className = 'auto-eval-btn';
        scoreButton.style.marginRight = '10px';
        scoreButton.textContent = '设置分数';
        scoreButton.addEventListener('click', createScoreDialog);

        // 一键评教按钮
        const evalButton = document.createElement('button');
        evalButton.className = 'auto-eval-btn';
        evalButton.textContent = '一键评教';
        evalButton.addEventListener('click', startEvaluation);

        buttonContainer.appendChild(scoreButton);
        buttonContainer.appendChild(evalButton);
        document.body.appendChild(buttonContainer);

        showStatus('评教脚本已加载,请点击"设置分数"设置教师分数,然后点击"一键评教"开始评教', 5000);
    }

    // 添加全局变量
    window.teacherScores = {};

    // 添加全局函数
    window.applyBatchScore = function() {
        const batchScore = parseInt(document.getElementById('batchScore').value) || 92;
        const validScore = Math.min(95, Math.max(0, batchScore));

        document.querySelectorAll('.teacher-score').forEach(input => {
            input.value = validScore;
        });
    };

    window.saveScores = function() {
        document.querySelectorAll('.teacher-score').forEach(input => {
            const teacherId = input.dataset.teacherId;
            const score = parseInt(input.value) || 92;
            window.teacherScores[teacherId] = score;
        });

        window.closeScoreDialog();
        showStatus('教师评分已保存', 2000);
    };

    window.closeScoreDialog = function() {
        const dialog = document.querySelector('.score-dialog');
        if (dialog) {
            dialog.remove();
        }
    };

    // 修改评教完成提示函数
    function showCompleteDialog() {
        const dialog = document.createElement('div');
        dialog.className = 'eval-complete-dialog';
        dialog.innerHTML = `
            <div class="icon">✅</div>
            <h2>评教完成</h2>
            <div class="message">所有教师评分已完成,感谢您的参与!</div>
            <button onclick="this.parentElement.remove()">确定</button>
            <div class="author-info">作者:<span>软工</span>,谢谢支持!</div>
        `;
        document.body.appendChild(dialog);
    }

})();