Greasy Fork

一键评教脚本 (HQU) - 全自动版

在华侨大学教务评教列表页添加“一键完成”和“设置”按钮,自动依次填写、确认并提交所有评教问卷,支持自定义答题间隔。

// ==UserScript==
// @name         一键评教脚本 (HQU) - 全自动版
// @namespace    http://tampermonkey.net/
// @version      2.3
// @description  在华侨大学教务评教列表页添加“一键完成”和“设置”按钮,自动依次填写、确认并提交所有评教问卷,支持自定义答题间隔。
// @author       Gemini
// @match        https://jwapp.hqu.edu.cn/jwapp/sys/pjapp/*default/index.do*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- 配置 ---
    const OPINION_TEXT = '课程很好,无其他意见'; // 自动填写的评语
    let ANSWER_DELAY = GM_getValue('answer_delay', 500); // 从存储读取答题间隔,默认为500ms

    /**
     * 延时函数
     * @param {number} ms - 毫秒数
     */
    function delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    /**
     * 等待指定元素出现
     * @param {string} selector - CSS选择器
     * @param {number} timeout - 超时时间
     * @returns {Promise<Element>}
     */
    function waitForElement(selector, timeout = 5000) {
        return new Promise((resolve, reject) => {
            const intervalTime = 100;
            let elapsedTime = 0;
            const interval = setInterval(() => {
                const element = document.querySelector(selector);
                if (element) {
                    clearInterval(interval);
                    resolve(element);
                }
                elapsedTime += intervalTime;
                if (elapsedTime >= timeout) {
                    clearInterval(interval);
                    reject(new Error(`等待元素超时: ${selector}`));
                }
            }, intervalTime);
        });
    }

    /**
     * 等待指定元素消失
     * @param {string} selector - CSS选择器
     * @param {number} timeout - 超时时间
     */
     function waitForElementToDisappear(selector, timeout = 10000) {
        return new Promise((resolve, reject) => {
            const intervalTime = 100;
            let elapsedTime = 0;
            const interval = setInterval(() => {
                if (!document.querySelector(selector)) {
                    clearInterval(interval);
                    resolve();
                }
                 elapsedTime += intervalTime;
                if (elapsedTime >= timeout) {
                    clearInterval(interval);
                    reject(new Error(`等待元素消失超时: ${selector}`));
                }
            }, intervalTime);
        });
    }


    /**
     * 填写并提交单个问卷 (已修复二次确认)
     */
    async function fillAndSubmitForm() {
        console.log("检测到问卷,开始填写...");

        await waitForElement('.wjtxQuestionItem');
        await delay(500);

        const questionItems = document.querySelectorAll('.wjtxQuestionItem');

        for (const item of questionItems) {
            // 1. 单选题
            const firstRadio = item.querySelector('.wjtxQuestionOptionItem .wjtxRadioButton');
            if (firstRadio) firstRadio.click();

            // 2. 多选题
            const checkBoxes = item.querySelectorAll('.jqx-checkbox');
            if (checkBoxes.length > 0) {
                checkBoxes.forEach(box => {
                    const labelText = box.textContent || box.innerText;
                    if (labelText && !labelText.includes('基本无变化')) {
                        if (box.getAttribute('aria-checked') === 'false') {
                            const mouseDownEvent = new MouseEvent('mousedown', { bubbles: true, cancelable: true });
                            const mouseUpEvent = new MouseEvent('mouseup', { bubbles: true, cancelable: true });
                            box.dispatchEvent(mouseDownEvent);
                            box.dispatchEvent(mouseUpEvent);
                        }
                    }
                });
            }

            // 3. 主观题
            const textArea = item.querySelector('textarea.jqx-text-area-element');
            if (textArea) {
                textArea.focus();
                textArea.value = OPINION_TEXT;
                textArea.dispatchEvent(new Event('input', { bubbles: true }));
                textArea.dispatchEvent(new Event('change', { bubbles: true }));
                textArea.blur();
            }

            // 模拟人工答题间隔
            console.log(`- 答题间隔 ${ANSWER_DELAY}ms`);
            await delay(ANSWER_DELAY);
        }

        console.log("问卷填写完毕,准备提交...");
        await delay(500);

        const initialSubmitButton = Array.from(document.querySelectorAll('.bh-paper-pile-dialog .wjtxOperationButtonContainer button'))
                                  .find(btn => btn.textContent.trim() === '提交');

        if (!initialSubmitButton) throw new Error("未找到“提交”按钮!");

        initialSubmitButton.click();
        console.log("已点击“提交”按钮,等待确认弹窗...");

        // FIX: 处理新的二次确认弹窗
        try {
            const finalConfirmButtonSelector = '.bh-dialog-btn.bh-bg-primary';
            const finalConfirmButton = await waitForElement(finalConfirmButtonSelector, 3000);
            console.log("检测到确认对话框,正在点击最终确认...");
            finalConfirmButton.click();
        } catch (e) {
            console.warn("未检测到二次确认弹窗,脚本将继续执行。");
        }
    }

    /**
     * 主流程:处理所有评教
     */
    async function processAllEvaluations(button) {
        const originalButtonText = button.innerHTML;
        button.disabled = true;
        document.getElementById('eval-settings-btn').disabled = true;

        const evalButtons = Array.from(document.querySelectorAll('.wj-wjxx .bh-btn-primary'))
                                 .filter(btn => btn.textContent.trim() === '立刻评教');

        if (evalButtons.length === 0) {
            alert("在当前页面未找到任何“立刻评教”的按钮。\n请确认您在待评课程列表页。");
            button.disabled = false;
            document.getElementById('eval-settings-btn').disabled = false;
            return;
        }

        const total = evalButtons.length;
        console.log(`发现 ${total} 个待评教课程。`);

        for (let i = 0; i < total; i++) {
            const currentEvalButton = Array.from(document.querySelectorAll('.wj-wjxx .bh-btn-primary')).find(btn => btn.textContent.trim() === '立刻评教');

            if(!currentEvalButton) {
                 console.log("找不到下一个“立刻评教”按钮,可能已经全部完成。");
                 break;
            }

            const courseTitle = currentEvalButton.closest('.wjx-container').querySelector('.wjxSubTitle').textContent.trim();
            console.log(`--- 开始处理第 ${i + 1} / ${total} 个: ${courseTitle} ---`);
            button.innerHTML = `⚡ 处理中 (${i + 1}/${total})...`;

            try {
                currentEvalButton.click();
                await fillAndSubmitForm();
                await waitForElementToDisappear('.bh-paper-pile-dialog');
                console.log(`--- ${courseTitle} 处理完成 ---`);
                await delay(1500);
            } catch (error) {
                console.error(`处理课程 "${courseTitle}" 时出错:`, error);
                alert(`处理课程 "${courseTitle}" 时发生错误,已停止。\n请检查控制台获取详细信息,然后刷新页面重试。`);
                button.innerHTML = originalButtonText;
                button.disabled = false;
                document.getElementById('eval-settings-btn').disabled = false;
                return;
            }
        }

        button.innerHTML = '🎉 全部完成';
        button.style.background = 'linear-gradient(45deg, #28a745, #218838)';
        document.getElementById('eval-settings-btn').disabled = true;
        alert(`所有 ${total} 个评教已自动完成并提交!`);
        console.log("所有评教已处理完毕!");
    }

    /**
     * 创建UI界面 (已更新,增加设置按钮)
     */
    function setupUI() {
        if (document.getElementById('eval-button-container')) return;

        GM_addStyle(`
            #eval-button-container {
                position: absolute;
                top: 8px;
                right: 0px;
                z-index: 999;
                display: flex;
                gap: 10px;
            }
            .eval-btn {
                padding: 6px 16px;
                font-size: 14px;
                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
                font-weight: bold;
                color: white;
                border: none;
                border-radius: 18px;
                cursor: pointer;
                transition: all 0.3s ease;
            }
            .eval-btn:hover:not(:disabled) {
                transform: translateY(-2px);
            }
            #process-all-eval-btn {
                background: linear-gradient(45deg, #1E90FF, #0073e6);
                box-shadow: 0 2px 8px rgba(0, 123, 255, 0.4);
            }
            #process-all-eval-btn:hover:not(:disabled) {
                 box-shadow: 0 4px 12px rgba(0, 123, 255, 0.5);
            }
            #eval-settings-btn {
                padding: 6px 12px;
                background: linear-gradient(45deg, #6c757d, #5a6268);
                box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
            }
            #eval-settings-btn:hover:not(:disabled) {
                 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
            }
            .eval-btn:disabled {
                background: linear-gradient(45deg, #9e9e9e, #888888);
                cursor: not-allowed;
                box-shadow: none;
                transform: none;
            }
        `);

        const targetContainer = document.querySelector('#wdpj-wj-section .wdpj-bh-buttons');
        if (targetContainer) {
            targetContainer.style.position = 'relative';

            const buttonContainer = document.createElement('div');
            buttonContainer.id = 'eval-button-container';

            const processAllButton = document.createElement('button');
            processAllButton.id = 'process-all-eval-btn';
            processAllButton.className = 'eval-btn';
            processAllButton.innerHTML = '⚡ 一键完成所有评教';
            processAllButton.addEventListener('click', () => processAllEvaluations(processAllButton));

            const settingsButton = document.createElement('button');
            settingsButton.id = 'eval-settings-btn';
            settingsButton.className = 'eval-btn';
            settingsButton.innerHTML = '⚙️ 设置';
            settingsButton.addEventListener('click', () => {
                const newDelay = prompt(`请输入每个问题之间的答题间隔(单位:毫秒):\n推荐值为 200 到 1000 之间。`, ANSWER_DELAY);
                if (newDelay !== null && !isNaN(newDelay) && newDelay >= 0) {
                    const parsedDelay = parseInt(newDelay, 10);
                    GM_setValue('answer_delay', parsedDelay);
                    ANSWER_DELAY = parsedDelay;
                    alert(`设置成功!新的答题间隔为 ${parsedDelay} 毫秒。`);
                    console.log(`答题间隔已更新为: ${parsedDelay}ms`);
                } else if (newDelay !== null) {
                    alert('输入无效,请输入一个非负数字。');
                }
            });

            buttonContainer.appendChild(settingsButton);
            buttonContainer.appendChild(processAllButton);
            targetContainer.appendChild(buttonContainer);
        }
    }

    window.addEventListener('hashchange', () => setTimeout(setupUI, 1000));
    window.addEventListener('load', () => setTimeout(setupUI, 1000));

})();