4-正确率查询-按钮

在周测页面添加一个美化按钮,点击后在悬浮窗中显示所有题目、整体正确率(整数)并优化复制功能。现已支持在线更新

目前為 2025-09-30 提交的版本,檢視 最新版本

// ==UserScript==
// @name         4-正确率查询-按钮
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  在周测页面添加一个美化按钮,点击后在悬浮窗中显示所有题目、整体正确率(整数)并优化复制功能。现已支持在线更新
// @author       大生
// @match        https://tyca.codemao.cn/weekly-test/group-center?paperId=*
// @grant        GM_xmlhttpRequest
// @grant        GM_setClipboard
// @icon         https://tyca.codemao.cn/favicon.ico
// @license      MIT; https://github.com/bpjanson/Vibe_Coding/LICENSE
// ==/UserScript==

(function() {
    'use strict';
    console.log('Tampermonkey script loaded: 猫码周测题目及选项正确率和正确答案查询 (Copy all info enhanced)');

    let preloadedData = null;
    let dataLoadingPromise = null;

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initializeScript);
    } else {
        initializeScript();
    }

    async function initializeScript() {
        console.log('DOM Content Loaded, initializing script elements.');

        const button = document.createElement('button');
        button.textContent = '查询正确率';
        button.style.cssText = `
            position: fixed;
            right: 20px;
            bottom: 20px;
            padding: 15px 30px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 30px;
            box-shadow: 0 6px 12px rgba(0, 0, 0, 0.25);
            cursor: pointer;
            font-size: 18px;
            font-weight: bold;
            letter-spacing: 1px;
            transition: all 0.3s ease;
            z-index: 9999;
            outline: none;
        `;
        button.onmouseover = function() {
            this.style.backgroundColor = '#45a049';
            this.style.boxShadow = '0 8px 16px rgba(0, 0, 0, 0.35)';
            this.style.transform = 'translateY(-3px)';
        };
        button.onmouseout = function() {
            this.style.backgroundColor = '#4CAF50';
            this.style.boxShadow = '0 6px 12px rgba(0, 0, 0, 0.25)';
            this.style.transform = 'translateY(0)';
        };
        button.onmousedown = function() {
            this.style.transform = 'translateY(-1px) scale(0.98)';
            this.style.boxShadow = '0 3px 6px rgba(0, 0, 0, 0.15)';
        };
        button.onmouseup = function() {
            this.style.transform = 'translateY(0) scale(1)';
        };
        document.body.appendChild(button);
        console.log('Query button added to document body, positioned at bottom-right.');

        // 在脚本加载时就静默预加载数据
        preloadAllData();

        function createFloatingWindow() {
            let existingWindow = document.getElementById('questionAccuracyWindow');
            if (existingWindow) {
                console.log('Removing existing window.');
                existingWindow.remove();
            }

            const windowDiv = document.createElement('div');
            windowDiv.id = 'questionAccuracyWindow';
            windowDiv.style.cssText = `
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                width: 70%;
                max-width: 800px;
                max-height: 85%;
                background-color: #f8f9fa;
                border: 1px solid #e0e0e0;
                border-radius: 12px;
                box-shadow: 0 8px 25px rgba(0, 0, 0, 0.25);
                z-index: 10000;
                overflow: hidden;
                font-family: 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
                color: #333;
                animation: fadeIn 0.3s ease-out forwards;
                display: flex;
                flex-direction: column;
            `;

            let styleSheet = document.getElementById('tm-accuracy-animations');
            if (!styleSheet) {
                styleSheet = document.createElement("style");
                styleSheet.id = 'tm-accuracy-animations';
                styleSheet.type = "text/css";
                styleSheet.innerText = `
                    @keyframes fadeIn {
                        from { opacity: 0; transform: translate(-50%, -50%) scale(0.95); }
                        to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
                    }
                    @keyframes fadeOut {
                        from { opacity: 1; transform: translate(-50%, -50%) scale(1); }
                        to { opacity: 0; transform: translate(-50%, -50%) scale(0.95); }
                    }
                `;
                document.head.appendChild(styleSheet);
            }

            const headerDiv = document.createElement('div');
            headerDiv.style.cssText = `
                display: flex;
                justify-content: space-between;
                align-items: center;
                padding: 20px 30px 15px;
                border-bottom: 1px solid #e0e0e0;
                background-color: #f8f9fa;
                position: sticky;
                top: 0;
                z-index: 10;
                box-shadow: 0 2px 5px rgba(0,0,0,0.05);
            `;

            const titleSpan = document.createElement('span');
            titleSpan.textContent = '正确率分析';
            titleSpan.id = 'accuracyWindowTitle';
            titleSpan.style.cssText = `
                font-size: 24px;
                font-weight: bold;
                color: #212529;
            `;

            const closeButton = document.createElement('button');
            closeButton.textContent = '✕';
            closeButton.style.cssText = `
                background: none;
                border: none;
                font-size: 28px;
                cursor: pointer;
                color: #6c757d;
                line-height: 1;
                padding: 0 8px;
                transition: color 0.2s ease;
                outline: none;
            `;
            closeButton.onmouseover = function() { this.style.color = '#dc3545'; };
            closeButton.onmouseout = function() { this.style.color = '#6c757d'; };
            closeButton.addEventListener('click', () => {
                windowDiv.style.animation = 'fadeOut 0.3s ease-in forwards';
                windowDiv.addEventListener('animationend', () => {
                    windowDiv.remove();
                }, { once: true });
            });

            headerDiv.appendChild(titleSpan);
            headerDiv.appendChild(closeButton);
            windowDiv.appendChild(headerDiv);

            const contentScrollContainer = document.createElement('div');
            contentScrollContainer.style.cssText = `
                flex-grow: 1;
                overflow-y: auto;
                padding: 20px 30px 30px;
            `;
            contentScrollContainer.id = 'accuracyContent';
            windowDiv.appendChild(contentScrollContainer);

            // --- 复制按钮 ---
            const copyButton = document.createElement('button');
            copyButton.textContent = '复制信息';
            copyButton.style.cssText = `
                position: absolute;
                right: 20px;
                bottom: 20px;
                padding: 10px 20px;
                background-color: #17a2b8;
                color: white;
                border: none;
                border-radius: 20px;
                cursor: pointer;
                font-size: 15px;
                font-weight: bold;
                box-shadow: 0 4px 8px rgba(0,0,0,0.2);
                transition: background-color 0.2s ease, transform 0.2s ease;
                z-index: 100;
                outline: none;
            `;
            copyButton.onmouseover = function() {
                this.style.backgroundColor = '#138496';
                this.style.transform = 'scale(1.05)';
            };
            copyButton.onmouseout = function() {
                this.style.backgroundColor = '#17a2b8';
                this.style.transform = 'scale(1)';
            };

            // --- 核心修改:复制按钮逻辑,使用预加载的纯文本数据进行复制 ---
            copyButton.onclick = function() {
                if (preloadedData && !preloadedData.error && preloadedData.plainText) {
                    GM_setClipboard(preloadedData.plainText);
                    this.textContent = '已复制!';
                    setTimeout(() => {
                        this.textContent = '复制信息';
                    }, 1500);
                    console.log('Copied pre-formatted plain text data.');
                } else {
                    // 兜底方案,如果预加载数据有问题,直接从DOM获取
                    const contentToCopy = document.getElementById('accuracyContent').innerText;
                    GM_setClipboard(contentToCopy);
                    this.textContent = '已复制!';
                    setTimeout(() => {
                        this.textContent = '复制信息';
                    }, 1500);
                    console.warn('Preloaded plain text not available, copied from DOM innerText.');
                }
            };
            // --- 复制按钮逻辑结束 ---

            windowDiv.appendChild(copyButton);
            document.body.appendChild(windowDiv);
            console.log('Floating window created with sticky header and floating copy button.');

            return contentScrollContainer;
        }

        async function preloadAllData() {
            if (dataLoadingPromise) {
                console.log('Data loading already in progress or completed, returning existing promise.');
                return dataLoadingPromise;
            }

            const urlParams = new URLSearchParams(window.location.search);
            const paperId = urlParams.get('paperId');

            if (!paperId) {
                console.error('Paper ID not found in URL. Cannot preload data.');
                preloadedData = { error: true, message: '未在URL中找到 paperId。请确认当前页面是周测页面。' };
                return Promise.resolve(preloadedData);
            }

            dataLoadingPromise = (async () => {
                console.log(`Starting data preload for paperId: ${paperId}`);
                const paperApiUrl = `https://codecamp-teaching-system.codemao.cn/paper/${paperId}`;

                try {
                    const paperResponse = await new Promise((resolve, reject) => {
                        GM_xmlhttpRequest({
                            method: "GET",
                            url: paperApiUrl,
                            onload: resolve,
                            onerror: reject
                        });
                    });

                    const paperData = JSON.parse(paperResponse.responseText);
                    if (!paperData.success || !paperData.data || !paperData.data.questions || paperData.data.questions.length === 0) {
                        console.error('Paper data not successful or no questions found.', paperData);
                        return { error: true, message: '未找到题目信息或数据格式不正确。', data: paperData };
                    }

                    const paperName = paperData.data.paperName || "未知试卷名称";
                    console.log(`Fetched paperName: ${paperName}`);

                    const questionIds = paperData.data.questions.map(q => q.questionId);
                    let questionsHtmlParts = [];
                    let questionsPlainTextParts = []; // 用于存储纯文本内容

                    questionsPlainTextParts.push(`试卷名称: ${paperName}`);
                    questionsPlainTextParts.push(`试卷ID: ${paperId}`);
                    questionsPlainTextParts.push(`---`); // 添加分隔符

                    console.log(`Found ${questionIds.length} questions. Fetching details...`);

                    for (let i = 0; i < questionIds.length; i++) {
                        const qId = questionIds[i];
                        const questionApiUrl = `https://codecamp-teaching-system.codemao.cn/general-question/list?questionId=${qId}`;

                        const questionResponse = await new Promise((resolve, reject) => {
                            GM_xmlhttpRequest({
                                method: "GET",
                                url: questionApiUrl,
                                onload: resolve,
                                onerror: reject
                            });
                        });

                        const questionData = JSON.parse(questionResponse.responseText);
                        let questionHtml = '';
                        let questionPlainText = ''; // 单个题目的纯文本

                        if (questionData.success && questionData.data && questionData.data.items && questionData.data.items.length > 0) {
                            const question = questionData.data.items[0];
                            const overallAccuracy = question.stat && question.stat.accuracy !== undefined ?
                                                    Math.round(question.stat.accuracy * 100) + '%' : 'N/A';

                            if (i > 0) {
                                questionHtml += '<div style="margin-top: 30px; padding-top: 20px; border-top: 1px dashed #e9ecef;"></div>';
                                questionsPlainTextParts.push(`\n---`); // 纯文本分隔符
                            }

                            // 构建HTML
                            questionHtml += `<h3 style="color: #007bff; margin-bottom: 8px;">题目ID: ${qId} (${question.name})</h3>`;
                            questionHtml += `<p style="font-size: 1.1em; margin-bottom: 15px;"><strong>整体正确率: <span style="color: #28a745;">${overallAccuracy}</span></strong></p>`;
                            questionHtml += `<ul style="list-style: none; padding: 0;">`;

                            // 构建纯文本
                            questionPlainText += `\n题目ID: ${qId} (${question.name})\n`;
                            questionPlainText += `整体正确率: ${overallAccuracy}\n`;

                            if (question.stat && question.stat.optionStat && question.stat.optionStat.length > 0 && question.options) {
                                question.options.forEach(option => {
                                    const optionChar = String.fromCharCode(65 + option.seq);
                                    const correctMarkHtml = option.isCorrect ? ' <span style="color: #28a745; font-weight: bold;">- ✅ (正确答案)</span>' : '';
                                    const correctMarkPlainText = option.isCorrect ? ' - (正确答案)' : '';

                                    const statOption = question.stat.optionStat.find(stat => stat.seq === option.seq);
                                    const selectedRate = statOption ? Math.round(statOption.selectedRate * 100) : 'N/A';

                                    questionHtml += `<li style="margin-bottom: 5px; padding: 5px 0; border-bottom: 1px dotted #dee2e6;">选项 ${optionChar}: <strong>${selectedRate}%</strong>${correctMarkHtml}</li>`;
                                    questionPlainText += `选项 ${optionChar}: ${selectedRate}%${correctMarkPlainText}\n`;
                                });
                            } else {
                                questionHtml += `<li style="color: #6c757d;">无选项正确率数据或选项信息。</li>`;
                                questionPlainText += `无选项正确率数据或选项信息。\n`;
                            }
                            questionHtml += `</ul>`;
                        } else {
                            if (i > 0) {
                                questionHtml += '<div style="margin-top: 30px; padding-top: 20px; border-top: 1px dashed #e9ecef;"></div>';
                                questionsPlainTextParts.push(`\n---`);
                            }
                            questionHtml += `<h3 style="color: #007bff;">题目ID: ${qId}</h3><p style="color: #dc3545;">无法获取该题目详情或选项正确率数据。</p>`;
                            questionPlainText += `\n题目ID: ${qId}\n无法获取该题目详情或选项正确率数据。\n`;
                            console.warn(`Could not get details for question ID: ${qId}`);
                        }
                        questionsHtmlParts.push(questionHtml);
                        questionsPlainTextParts.push(questionPlainText); // 将纯文本加入数组
                    }

                    const fullContentHtml = questionsHtmlParts.join(''); // 不需要 initialInfoHtml,因为已经合并到 questionsHtmlParts 中
                    const fullContentPlainText = questionsPlainTextParts.join('\n'); // 组合所有纯文本部分

                    preloadedData = {
                        html: fullContentHtml, // 用于显示在弹窗的HTML内容
                        plainText: fullContentPlainText, // 用于复制的纯文本内容
                        paperName: paperName,
                        paperId: paperId,
                        error: false
                    };
                    console.log('Data preloaded successfully. HTML and plain text generated.');
                    return preloadedData;

                } catch (e) {
                    console.error('Preload: Request or parsing data failed:', e);
                    preloadedData = { error: true, message: '获取数据失败,请检查网络或Paper ID是否正确。详情请查看浏览器控制台。' };
                    return preloadedData;
                }
            })();

            return dataLoadingPromise;
        }

        button.addEventListener('click', async function() {
            console.log('Query button clicked.');
            const contentDiv = createFloatingWindow(); // 确保每次点击都创建并获取到新的弹窗内容容器

            // 显示加载提示,以防数据仍在加载中
            contentDiv.innerHTML = '<p style="text-align: center; color: #555;">正在加载数据,请稍候...</p>';

            const result = await dataLoadingPromise;

            if (result && !result.error) {
                console.log('Displaying data from preloaded/loaded source.');
                // 注意:这里仍然使用 result.html 来填充弹窗显示内容
                contentDiv.innerHTML = `
                    <p style="margin-bottom: 8px;"><strong>试卷名称:</strong> ${result.paperName}</p>
                    <p style="margin-bottom: 20px;"><strong>试卷ID:</strong> ${result.paperId}</p>
                    <div style="border-bottom: 1px dashed #e9ecef; margin-bottom: 20px;"></div>
                ` + result.html;
            } else if (result && result.error) {
                console.log('Displaying error from preloaded/loaded source.');
                contentDiv.innerHTML = `<p style="text-align: center; color: #dc3545; font-weight: bold;">${result.message}</p>`;
            } else {
                console.error('Unexpected state: dataLoadingPromise is null after click.');
                contentDiv.innerHTML = '<p style="text-align: center; color: #dc3545; font-weight: bold;">初始化数据失败,请刷新页面重试。</p>';
            }
        });
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址