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