Greasy Fork

Greasy Fork is available in English.

隐藏/显示超星学习通作业答案

一键隐藏超星学习通作业页面中所有 div.mark_answer 答案块,支持单个控制和全局控制,可直接在控制台执行或安装为油猴脚本使用。

当前为 2025-11-09 提交的版本,查看 最新版本

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

You will need to install an extension such as Tampermonkey to install this script.

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         隐藏/显示超星学习通作业答案
// @namespace    http://tampermonkey.net/
// @version      2.0.0
// @description  一键隐藏超星学习通作业页面中所有 div.mark_answer 答案块,支持单个控制和全局控制,可直接在控制台执行或安装为油猴脚本使用。
// @author       You
// @match        https://*.chaoxing.com/mooc-ans/mooc2/work/view*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=chaoxing.com
// @grant        none
// @run-at       document-end
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // ===================== 配置管理模块 =====================
    class Config {
        static DEFAULT = {
            selectors: {
                answerBlock: 'div.mark_answer',
                container: 'div.topicNumber'
            },
            delays: {
                initialization: 800
            },
            button: {
                position: {
                    marginLeft: '20px',
                    marginRight: '0px',
                    marginTop: '10px',
                    marginBottom: '0px',
                    verticalAlign: 'middle'
                },
                style: {
                    fontSize: '12px',
                    padding: '3px 10px',
                    borderRadius: '4px',
                    primaryColor: '#4299e1',   // 显示按钮颜色
                    secondaryColor: '#9f7aea'  // 隐藏按钮颜色
                },
                text: {
                    show: '显示答案',
                    hide: '隐藏答案',
                    showAll: '显示全部答案',
                    hideAll: '隐藏全部答案'
                }
            },
            messages: {
                noAnswerBlocks: 'ℹ️ 未找到答案块(可能页面未完全加载,可刷新重试)',
                noContainer: 'ℹ️ 未找到容器模块,仅启用单个答案块隐藏功能',
                success: '✅ 超星作业答案块隐藏工具执行完成!',
                hiddenCount: (count) => `- 已隐藏 ${count} 个答案内容块,每个块已添加独立显示按钮`,
                globalButton: (hasContainer) => `- ${hasContainer ? '已在容器右上角添加全局控制按钮' : '未找到容器模块,未添加全局按钮'}`
            }
        };

        constructor(customConfig = {}) {
            this.config = this._deepMerge(Config.DEFAULT, customConfig);
        }

        get(path) {
            return path.split('.').reduce((obj, key) => obj?.[key], this.config);
        }

        _deepMerge(target, source) {
            const result = { ...target };
            for (const key in source) {
                if (source[key] instanceof Object && key in target) {
                    result[key] = this._deepMerge(target[key], source[key]);
                } else {
                    result[key] = source[key];
                }
            }
            return result;
        }
    }

    // ===================== 日志管理模块 =====================
    class Logger {
        static log(message, type = 'info') {
            const prefix = type === 'error' ? '❌' : type === 'warn' ? '⚠️' : 'ℹ️';
            console.log(`${prefix} ${message}`);
        }

        static success(message) {
            console.log(`✅ ${message}`);
        }

        static error(message, error) {
            console.error(`❌ ${message}`, error);
        }
    }

    // ===================== DOM 工具类 =====================
    class DOMHelper {
        static createElement(tag, attributes = {}) {
            const element = document.createElement(tag);
            Object.entries(attributes).forEach(([key, value]) => {
                if (key === 'style' && typeof value === 'object') {
                    Object.assign(element.style, value);
                } else if (key === 'dataset' && typeof value === 'object') {
                    Object.entries(value).forEach(([dataKey, dataValue]) => {
                        element.dataset[dataKey] = dataValue;
                    });
                } else {
                    element[key] = value;
                }
            });
            return element;
        }

        static insertElement(element, parent, nextSibling = null) {
            if (nextSibling) {
                parent.insertBefore(element, nextSibling);
            } else {
                parent.appendChild(element);
            }
        }

        static removeElement(element) {
            element?.parentNode?.removeChild(element);
        }

        static ensureRelativePosition(element) {
            if (getComputedStyle(element).position === 'static') {
                element.style.position = 'relative';
            }
        }
    }

    // ===================== 样式生成器 =====================
    class StyleGenerator {
        constructor(config) {
            this.config = config;
        }

        getSingleButtonStyle() {
            const { position, style } = this.config.get('button');
            return {
                marginLeft: position.marginLeft,
                marginRight: position.marginRight,
                marginTop: position.marginTop,
                marginBottom: position.marginBottom,
                verticalAlign: position.verticalAlign,
                padding: '2px 8px',
                border: 'none',
                borderRadius: '3px',
                background: style.primaryColor,
                color: 'white',
                fontSize: '12px',
                cursor: 'pointer',
                transition: 'background 0.2s',
                display: 'inline-block'
            };
        }

        getGlobalButtonStyle() {
            const { style } = this.config.get('button');
            return {
                position: 'absolute',
                top: '8px',
                right: '8px',
                border: 'none',
                borderRadius: style.borderRadius,
                padding: style.padding,
                fontSize: style.fontSize,
                color: 'white',
                cursor: 'pointer',
                transition: 'background 0.2s',
                zIndex: '9999',
                background: style.primaryColor
            };
        }
    }

    // ===================== 答案块控制器 =====================
    class AnswerBlockController {
        constructor(block, config, styleGenerator) {
            this.block = block;
            this.config = config;
            this.styleGenerator = styleGenerator;
            this.parent = block.parentNode;
            this.nextSibling = block.nextSibling;
            this.originalHTML = block.outerHTML;
            this.toggleButton = null;
            this.isHidden = false;
        }

        initialize() {
            this._hideBlock();
            this._createToggleButton();
            return this.toggleButton;
        }

        _hideBlock() {
            DOMHelper.removeElement(this.block);
            this.isHidden = true;
        }

        _createToggleButton() {
            const buttonText = this.config.get('button.text');
            this.toggleButton = DOMHelper.createElement('button', {
                innerText: buttonText.show,
                style: this.styleGenerator.getSingleButtonStyle(),
                title: '点击显示/隐藏当前答案块',
                dataset: {
                    isHidden: 'true',
                    originalHTML: this.originalHTML
                }
            });

            this.toggleButton.addEventListener('click', () => this._handleToggle());
            DOMHelper.insertElement(this.toggleButton, this.parent, this.nextSibling);
        }

        _handleToggle() {
            if (this.isHidden) {
                this._showBlock();
            } else {
                this._hideBlock();
            }
            this._updateButtonState();
        }

        _showBlock() {
            const tempContainer = document.createElement('div');
            tempContainer.innerHTML = this.originalHTML;
            const restoredBlock = tempContainer.firstChild;
            DOMHelper.insertElement(restoredBlock, this.parent, this.toggleButton.nextSibling);
            this.isHidden = false;
        }

        _updateButtonState() {
            const buttonText = this.config.get('button.text');
            const colors = this.config.get('button.style');
            
            this.toggleButton.innerText = this.isHidden ? buttonText.show : buttonText.hide;
            this.toggleButton.style.background = this.isHidden ? colors.primaryColor : colors.secondaryColor;
            this.toggleButton.dataset.isHidden = String(this.isHidden);
        }

        toggle() {
            this._handleToggle();
        }

        getState() {
            return this.isHidden;
        }
    }

    // ===================== 全局控制器 =====================
    class GlobalController {
        constructor(container, controllers, config, styleGenerator) {
            this.container = container;
            this.controllers = controllers;
            this.config = config;
            this.styleGenerator = styleGenerator;
            this.globalButton = null;
        }

        initialize() {
            if (!this.container) return null;

            DOMHelper.ensureRelativePosition(this.container);
            this._createGlobalButton();
            return this.globalButton;
        }

        _createGlobalButton() {
            const buttonText = this.config.get('button.text');
            this.globalButton = DOMHelper.createElement('button', {
                innerText: buttonText.showAll,
                style: this.styleGenerator.getGlobalButtonStyle(),
                title: '点击一键显示/隐藏所有答案块'
            });

            this.globalButton.addEventListener('click', () => this._handleGlobalToggle());
            this.container.appendChild(this.globalButton);
        }

        _handleGlobalToggle() {
            const allHidden = this.controllers.every(ctrl => ctrl.getState());
            
            this.controllers.forEach(controller => {
                const shouldToggle = allHidden ? controller.getState() : !controller.getState();
                if (shouldToggle) {
                    controller.toggle();
                }
            });

            this._updateGlobalButtonState(!allHidden);
        }

        _updateGlobalButtonState(allHidden) {
            const buttonText = this.config.get('button.text');
            const colors = this.config.get('button.style');
            
            this.globalButton.innerText = allHidden ? buttonText.showAll : buttonText.hideAll;
            this.globalButton.style.background = allHidden ? colors.primaryColor : colors.secondaryColor;
        }
    }

    // ===================== 主应用类 =====================
    class ChaoxingAnswerHider {
        constructor(customConfig = {}) {
            this.config = new Config(customConfig);
            this.styleGenerator = new StyleGenerator(this.config);
            this.answerControllers = [];
            this.globalController = null;
        }

        async initialize() {
            try {
                await this._waitForPageLoad();
                const elements = this._findElements();
                
                if (!this._validateElements(elements)) {
                    return;
                }

                this._initializeAnswerBlocks(elements.answerBlocks);
                this._initializeGlobalControl(elements.container);
                this._logSuccess(elements.answerBlocks.length, !!elements.container);
            } catch (error) {
                Logger.error('初始化失败', error);
            }
        }

        _waitForPageLoad() {
            const delay = this.config.get('delays.initialization');
            return new Promise(resolve => setTimeout(resolve, delay));
        }

        _findElements() {
            return {
                container: document.querySelector(this.config.get('selectors.container')),
                answerBlocks: document.querySelectorAll(this.config.get('selectors.answerBlock'))
            };
        }

        _validateElements({ container, answerBlocks }) {
            if (answerBlocks.length === 0) {
                Logger.log(this.config.get('messages.noAnswerBlocks'));
                return false;
            }
            
            if (!container) {
                Logger.log(this.config.get('messages.noContainer'), 'warn');
            }
            
            return true;
        }

        _initializeAnswerBlocks(blocks) {
            blocks.forEach(block => {
                const controller = new AnswerBlockController(block, this.config, this.styleGenerator);
                controller.initialize();
                this.answerControllers.push(controller);
            });
        }

        _initializeGlobalControl(container) {
            this.globalController = new GlobalController(
                container,
                this.answerControllers,
                this.config,
                this.styleGenerator
            );
            this.globalController.initialize();
        }

        _logSuccess(count, hasContainer) {
            Logger.success(this.config.get('messages.success'));
            Logger.log(this.config.get('messages.hiddenCount')(count));
            Logger.log(this.config.get('messages.globalButton')(hasContainer));
        }
    }

    // ===================== 启动应用 =====================
    const app = new ChaoxingAnswerHider();
    app.initialize();
})();