// ==UserScript== // @name (测试)隐藏/显示超星学习通作业答案 // @namespace http://tampermonkey.net/ // @version 2.3.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 = { // ========== DOM 选择器配置 ========== selectors: { answerBlock: 'div.mark_answer', // 答案块的选择器 container: 'div.topicNumber', // 题目容器的选择器 questionItem: 'div.mark_item' // 题目项的选择器 }, // ========== 延迟配置 ========== delays: { initialization: 800 // 脚本初始化延迟时间(毫秒),确保页面加载完成 }, // ========== 单个答案控制按钮配置 ========== answerButton: { // --- 按钮位置配置 --- position: { marginLeft: '10px', // 按钮左外边距 marginRight: '0px', // 按钮右外边距 marginTop: '10px', // 按钮上外边距 marginBottom: '0px', // 按钮下外边距 verticalAlign: 'middle' // 垂直对齐方式(top/middle/bottom) }, // --- 按钮样式配置 --- style: { fontSize: '12px', // 字体大小 padding: '2px 8px', // 内边距(上下 左右) borderRadius: '3px', // 圆角半径 border: 'none', // 边框样式 fontWeight: 'normal', // 字体粗细(normal/bold/100-900) cursor: 'pointer', // 鼠标样式 transition: 'background 0.2s' // 过渡动画 }, // --- 按钮颜色配置 --- colors: { showBackground: '#4299e1', // "显示答案"按钮背景色(蓝色) hideBackground: '#9f7aea', // "隐藏答案"按钮背景色(紫色) textColor: 'white', // 按钮文字颜色 hoverOpacity: '0.8' // 鼠标悬停时的透明度 }, // --- 按钮文字配置 --- text: { show: '显示答案', // "显示答案"按钮文字 hide: '隐藏答案' // "隐藏答案"按钮文字 } }, // ========== 笔记控制按钮配置 ========== noteButton: { // --- 按钮位置配置 --- position: { marginLeft: '5px', // 按钮左外边距(与答案按钮的间距) marginRight: '0px', // 按钮右外边距 marginTop: '10px', // 按钮上外边距 marginBottom: '0px', // 按钮下外边距 verticalAlign: 'middle' // 垂直对齐方式 }, // --- 按钮样式配置 --- style: { fontSize: '12px', // 字体大小 padding: '2px 8px', // 内边距(上下 左右) borderRadius: '3px', // 圆角半径 border: 'none', // 边框样式 fontWeight: 'normal', // 字体粗细 cursor: 'pointer', // 鼠标样式 transition: 'background 0.2s' // 过渡动画 }, // --- 按钮颜色配置 --- colors: { showBackground: '#48bb78', // "显示笔记"按钮背景色(绿色) hideBackground: '#9f7aea', // "隐藏笔记"按钮背景色(紫色) textColor: 'white', // 按钮文字颜色 hoverOpacity: '0.8' // 鼠标悬停时的透明度 }, // --- 按钮文字配置 --- text: { show: '显示笔记', // "显示笔记"按钮文字 hide: '隐藏笔记' // "隐藏笔记"按钮文字 } }, // ========== 保存笔记按钮配置 ========== saveNoteButton: { // --- 按钮位置配置 --- position: { marginLeft: '5px', // 按钮左外边距(与笔记按钮的间距) marginRight: '0px', // 按钮右外边距 marginTop: '10px', // 按钮上外边距 marginBottom: '0px', // 按钮下外边距 verticalAlign: 'middle' // 垂直对齐方式 }, // --- 按钮样式配置 --- style: { fontSize: '12px', // 字体大小 padding: '2px 8px', // 内边距(上下 左右) borderRadius: '3px', // 圆角半径 border: 'none', // 边框样式 fontWeight: 'normal', // 字体粗细 cursor: 'pointer', // 鼠标样式 transition: 'background 0.2s' // 过渡动画 }, // --- 按钮颜色配置 --- colors: { background: '#38b2ac', // 按钮背景色(青色) textColor: 'white', // 按钮文字颜色 hoverOpacity: '0.8' // 鼠标悬停时的透明度 }, // --- 按钮文字配置 --- text: '💾 保存' // 保存按钮文字 }, // ========== 全局控制按钮配置 ========== globalButton: { // --- 按钮位置配置 --- position: { top: '8px', // 距离容器顶部的距离 right: '8px', // 距离容器右侧的距离 zIndex: '9999' // 层级(确保在最上层) }, // --- 按钮样式配置 --- style: { fontSize: '12px', // 字体大小 padding: '3px 10px', // 内边距(上下 左右) borderRadius: '4px', // 圆角半径 border: 'none', // 边框样式 fontWeight: 'normal', // 字体粗细 cursor: 'pointer', // 鼠标样式 transition: 'background 0.2s' // 过渡动画 }, // --- 按钮颜色配置 --- colors: { showAllBackground: '#4299e1', // "显示全部答案"按钮背景色(蓝色) hideAllBackground: '#9f7aea', // "隐藏全部答案"按钮背景色(紫色) textColor: 'white', // 按钮文字颜色 hoverOpacity: '0.8' // 鼠标悬停时的透明度 }, // --- 按钮文字配置 --- text: { showAll: '显示全部答案', // "显示全部答案"按钮文字 hideAll: '隐藏全部答案' // "隐藏全部答案"按钮文字 } }, // ========== 笔记编辑器配置 ========== noteEditor: { placeholder: '在这里记录你的笔记...', // 编辑器占位符文字 width: '110%', // 编辑器宽度 minHeight: '60px', // 编辑器最小高度 maxHeight: '400px', // 编辑器最大高度(超出滚动) fontSize: '14px', // 编辑器字体大小 padding: '10px', // 编辑器内边距 marginTop: '10px', // 编辑器上外边距 marginBottom: '10px', // 编辑器下外边距 borderRadius: '4px', // 编辑器圆角半径 borderWidth: '1px', // 编辑器边框宽度 borderStyle: 'solid', // 编辑器边框样式 borderColor: '#cbd5e0', // 编辑器边框颜色(默认) focusBorderColor: '#4299e1', // 编辑器获得焦点时的边框颜色 backgroundColor: '#f7fafc', // 编辑器背景颜色 textColor: '#2d3748', // 编辑器文字颜色 fontFamily: 'inherit', // 编辑器字体(继承父元素) resize: 'vertical' // 调整大小方式(none/vertical/horizontal/both) }, // ========== 用户设置默认值 ========== settings: { autoSave: false, // 是否开启自动保存(默认关闭) autoSaveDelay: 5000 // 自动保存延迟时间(毫秒) }, // ========== 控制面板按钮配置 ========== manageButton: { // --- 按钮位置配置 --- position: { top: '35px', // 距离容器顶部的距离(在全局按钮下方) right: '8px', // 距离容器右侧的距离 zIndex: '9999' // 层级(确保在最上层) }, // --- 按钮样式配置 --- style: { fontSize: '12px', // 字体大小 padding: '3px 10px', // 内边距(上下 左右) borderRadius: '4px', // 圆角半径 border: 'none', // 边框样式 fontWeight: 'normal', // 字体粗细 cursor: 'pointer', // 鼠标样式 transition: 'background 0.2s' // 过渡动画 }, // --- 按钮颜色配置 --- colors: { background: '#ed8936', // 按钮背景色(橙色) textColor: 'white', // 按钮文字颜色 hoverOpacity: '0.8' // 鼠标悬停时的透明度 }, // --- 按钮文字配置 --- text: '⚙️ 控制面板' // 控制面板按钮文字 }, // ========== 控制面板保存按钮配置 ========== panelSaveButton: { // --- 按钮样式配置 --- style: { padding: '10px 24px', // 内边距(上下 左右) borderRadius: '6px', // 圆角半径 border: 'none', // 边框样式 fontSize: '14px', // 字体大小 fontWeight: '600', // 字体粗细 cursor: 'pointer', // 鼠标样式 transition: 'all 0.2s' // 过渡动画 }, // --- 按钮颜色配置 --- colors: { background: '#4299e1', // 按钮背景色(蓝色) hoverBackground: '#3182ce', // 悬停时背景色 textColor: 'white', // 按钮文字颜色 successBackground: '#48bb78', // 保存成功背景色(绿色) errorBackground: '#f56565', // 保存失败背景色(红色) boxShadow: '0 2px 4px rgba(66, 153, 225, 0.3)', // 默认阴影 hoverBoxShadow: '0 4px 6px rgba(66, 153, 225, 0.4)' // 悬停阴影 }, // --- 按钮文字配置 --- text: { save: '💾 保存设置', // 默认文字 success: '✅ 保存成功', // 保存成功文字 error: '❌ 保存失败' // 保存失败文字 } }, // ========== 数据库配置 ========== database: { name: 'ChaoxingNotesDB', // IndexedDB 数据库名称 version: 3, // 数据库版本号(v3:添加设置存储) stores: { notes: 'notes', // 笔记存储名称 attachments: 'attachments', // 附件存储名称 settings: 'settings' // 用户设置存储名称 } }, // ========== 提示消息配置 ========== messages: { noAnswerBlocks: 'ℹ️ 未找到答案块(可能页面未完全加载,可刷新重试)', noContainer: 'ℹ️ 未找到容器模块,仅启用单个答案块隐藏功能', success: '✅ 超星作业答案块隐藏工具执行完成!', hiddenCount: (count) => `- 已隐藏 ${count} 个答案内容块,每个块已添加独立显示按钮`, globalButton: (hasContainer) => `- ${hasContainer ? '已在容器右上角添加全局控制按钮' : '未找到容器模块,未添加全局按钮'}`, noteSaved: '💾 笔记已自动保存', noteLoadError: '⚠️ 加载笔记失败' } }; 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); } } // ===================== URL 解析器 ===================== class URLParser { static parseWorkInfo() { const url = new URL(window.location.href); return { courseId: url.searchParams.get('courseId') || '', classId: url.searchParams.get('classId') || '', workId: url.searchParams.get('workId') || '' }; } static getWorkKey() { const { courseId, classId, workId } = this.parseWorkInfo(); return `${courseId}_${classId}_${workId}`; } } // ===================== IndexedDB 管理器 ===================== class DatabaseManager { constructor(config) { this.config = config; this.db = null; } async init() { return new Promise((resolve, reject) => { const request = indexedDB.open( this.config.get('database.name'), this.config.get('database.version') ); request.onerror = () => reject(request.error); request.onsuccess = () => { this.db = request.result; resolve(this.db); }; request.onupgradeneeded = (event) => { const db = event.target.result; const oldVersion = event.oldVersion; // 创建或升级笔记存储 if (!db.objectStoreNames.contains(this.config.get('database.stores.notes'))) { const notesStore = db.createObjectStore( this.config.get('database.stores.notes'), { keyPath: 'id' } ); notesStore.createIndex('workKey', 'workKey', { unique: false }); notesStore.createIndex('questionId', 'questionId', { unique: false }); notesStore.createIndex('timestamp', 'timestamp', { unique: false }); } // v2: 创建附件存储(为未来图片等附件做准备) if (oldVersion < 2 && !db.objectStoreNames.contains(this.config.get('database.stores.attachments'))) { const attachmentsStore = db.createObjectStore( this.config.get('database.stores.attachments'), { keyPath: 'id' } ); attachmentsStore.createIndex('noteId', 'noteId', { unique: false }); attachmentsStore.createIndex('workKey', 'workKey', { unique: false }); attachmentsStore.createIndex('type', 'type', { unique: false }); attachmentsStore.createIndex('timestamp', 'timestamp', { unique: false }); } // v3: 创建设置存储 if (oldVersion < 3 && !db.objectStoreNames.contains(this.config.get('database.stores.settings'))) { db.createObjectStore( this.config.get('database.stores.settings'), { keyPath: 'key' } ); } }; }); } async saveNote(workKey, questionId, content) { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction( [this.config.get('database.stores.notes')], 'readwrite' ); const objectStore = transaction.objectStore(this.config.get('database.stores.notes')); const id = `${workKey}_${questionId}`; const data = { id, workKey, questionId, content, contentType: 'text', // 内容类型:text, html等 hasAttachments: false, // 是否有附件 attachmentCount: 0, // 附件数量 timestamp: Date.now(), updatedAt: Date.now() }; const request = objectStore.put(data); request.onsuccess = () => resolve(data); request.onerror = () => reject(request.error); }); } async getNote(workKey, questionId) { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction( [this.config.get('database.stores.notes')], 'readonly' ); const objectStore = transaction.objectStore(this.config.get('database.stores.notes')); const id = `${workKey}_${questionId}`; const request = objectStore.get(id); request.onsuccess = () => resolve(request.result?.content || ''); request.onerror = () => reject(request.error); }); } async getAllNotes(workKey) { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction( [this.config.get('database.stores.notes')], 'readonly' ); const objectStore = transaction.objectStore(this.config.get('database.stores.notes')); const index = objectStore.index('workKey'); const request = index.getAll(workKey); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } /** * 获取整个域名下的所有笔记 */ async getAllDomainNotes() { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction( [this.config.get('database.stores.notes')], 'readonly' ); const objectStore = transaction.objectStore(this.config.get('database.stores.notes')); const request = objectStore.getAll(); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } async deleteNote(workKey, questionId) { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction( [this.config.get('database.stores.notes')], 'readwrite' ); const objectStore = transaction.objectStore(this.config.get('database.stores.notes')); const id = `${workKey}_${questionId}`; const request = objectStore.delete(id); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }); } /** * 批量删除笔记 * @param {Array} noteIds - 笔记ID数组 */ async deleteNotes(noteIds) { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction( [this.config.get('database.stores.notes')], 'readwrite' ); const objectStore = transaction.objectStore(this.config.get('database.stores.notes')); let completedCount = 0; const totalCount = noteIds.length; if (totalCount === 0) { resolve(0); return; } noteIds.forEach(id => { const request = objectStore.delete(id); request.onsuccess = () => { completedCount++; if (completedCount === totalCount) { resolve(completedCount); } }; request.onerror = () => { Logger.error(`删除笔记失败: ${id}`, request.error); completedCount++; if (completedCount === totalCount) { resolve(completedCount); } }; }); }); } /** * 获取数据库统计信息 */ async getStatistics() { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction( [this.config.get('database.stores.notes')], 'readonly' ); const objectStore = transaction.objectStore(this.config.get('database.stores.notes')); const countRequest = objectStore.count(); countRequest.onsuccess = () => { resolve({ totalNotes: countRequest.result, databaseName: this.config.get('database.name'), version: this.config.get('database.version') }); }; countRequest.onerror = () => reject(countRequest.error); }); } /** * 保存设置 * @param {string} key - 设置键 * @param {any} value - 设置值 */ async saveSetting(key, value) { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction( [this.config.get('database.stores.settings')], 'readwrite' ); const objectStore = transaction.objectStore(this.config.get('database.stores.settings')); const data = { key, value, updatedAt: Date.now() }; const request = objectStore.put(data); request.onsuccess = () => resolve(data); request.onerror = () => reject(request.error); }); } /** * 获取设置 * @param {string} key - 设置键 * @param {any} defaultValue - 默认值 */ async getSetting(key, defaultValue = null) { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction( [this.config.get('database.stores.settings')], 'readonly' ); const objectStore = transaction.objectStore(this.config.get('database.stores.settings')); const request = objectStore.get(key); request.onsuccess = () => { const result = request.result; resolve(result ? result.value : defaultValue); }; request.onerror = () => reject(request.error); }); } /** * 获取所有设置 */ async getAllSettings() { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction( [this.config.get('database.stores.settings')], 'readonly' ); const objectStore = transaction.objectStore(this.config.get('database.stores.settings')); const request = objectStore.getAll(); request.onsuccess = () => { const settings = {}; request.result.forEach(item => { settings[item.key] = item.value; }); resolve(settings); }; request.onerror = () => reject(request.error); }); } } // ===================== 笔记编辑器组件 ===================== class NoteEditor { constructor(questionId, workKey, dbManager, config, styleGenerator) { this.questionId = questionId; this.workKey = workKey; this.dbManager = dbManager; this.config = config; this.styleGenerator = styleGenerator; this.editor = null; this.saveTimer = null; this.isVisible = false; } async create() { const noteConfig = this.config.get('noteEditor'); this.editor = DOMHelper.createElement('textarea', { placeholder: noteConfig.placeholder, style: this.styleGenerator.getNoteEditorStyle() }); // 加载已保存的笔记 try { const savedContent = await this.dbManager.getNote(this.workKey, this.questionId); if (savedContent) { this.editor.value = savedContent; this._adjustHeight(); } } catch (error) { Logger.error(this.config.get('messages.noteLoadError'), error); } // 监听输入事件,自动调整高度和保存 this.editor.addEventListener('input', () => { this._adjustHeight(); this._scheduleAutoSave(); }); // 获得焦点时改变边框颜色 this.editor.addEventListener('focus', () => { this.editor.style.borderColor = this.config.get('noteEditor.focusBorderColor'); }); this.editor.addEventListener('blur', () => { this.editor.style.borderColor = this.config.get('noteEditor.borderColor'); }); return this.editor; } _adjustHeight() { // 重置高度以获取正确的 scrollHeight this.editor.style.height = 'auto'; const noteConfig = this.config.get('noteEditor'); const minHeight = parseInt(noteConfig.minHeight); const maxHeight = parseInt(noteConfig.maxHeight); const newHeight = Math.min(Math.max(this.editor.scrollHeight, minHeight), maxHeight); this.editor.style.height = `${newHeight}px`; } _scheduleAutoSave() { // 检查自动保存是否启用 this.dbManager.getSetting('autoSave', this.config.get('settings.autoSave')) .then(autoSaveEnabled => { if (!autoSaveEnabled) return; if (this.saveTimer) { clearTimeout(this.saveTimer); } this.dbManager.getSetting('autoSaveDelay', this.config.get('settings.autoSaveDelay')) .then(delay => { this.saveTimer = setTimeout(async () => { await this.save(); }, delay); }); }); } async save() { try { const content = this.editor.value.trim(); await this.dbManager.saveNote(this.workKey, this.questionId, content); } catch (error) { Logger.error('保存笔记失败', error); } } show() { this.editor.style.display = 'block'; this.isVisible = true; this._adjustHeight(); } hide() { this.editor.style.display = 'none'; this.isVisible = false; } toggle() { if (this.isVisible) { this.hide(); } else { this.show(); } } getElement() { return this.editor; } } // ===================== 控制面板UI组件 ===================== class ControlPanelUI { constructor(dbManager, workKey, config) { this.dbManager = dbManager; this.workKey = workKey; this.config = config; this.modal = null; this.notesList = []; this.selectedNotes = new Set(); this.notesScope = 'current'; // 'current', 'course', 'class', 'domain' this.currentTab = 'settings'; // 'settings', 'notes', 'styles' this.settings = {}; this.notesMenuExpanded = false; // 管理笔记子菜单是否展开 // 解析 workKey 获取 courseId, classId, workId const parts = workKey.split('_'); this.courseId = parts[0] || ''; this.classId = parts[1] || ''; this.workId = parts[2] || ''; } /** * 显示控制面板 */ async show() { await this._loadSettings(); await this._loadNotes(); this._createModal(); this._renderContent(); } /** * 加载用户设置 */ async _loadSettings() { try { this.settings = await this.dbManager.getAllSettings(); // 填充默认值 if (!('autoSave' in this.settings)) { this.settings.autoSave = this.config.get('settings.autoSave'); } if (!('autoSaveDelay' in this.settings)) { this.settings.autoSaveDelay = this.config.get('settings.autoSaveDelay'); } } catch (error) { Logger.error('加载设置失败', error); this.settings = { autoSave: this.config.get('settings.autoSave'), autoSaveDelay: this.config.get('settings.autoSaveDelay') }; } } /** * 加载笔记数据 */ async _loadNotes() { try { const allNotes = await this.dbManager.getAllDomainNotes(); switch (this.notesScope) { case 'current': // 当前页面:完全匹配 workKey this.notesList = allNotes.filter(note => note.workKey === this.workKey); break; case 'course': // 当前课程:courseId 相同 this.notesList = allNotes.filter(note => { const parts = note.workKey.split('_'); return parts[0] === this.courseId; }); break; case 'class': // 当前班级:courseId 和 classId 都相同 this.notesList = allNotes.filter(note => { const parts = note.workKey.split('_'); return parts[0] === this.courseId && parts[1] === this.classId; }); break; case 'domain': // 整个域名:所有笔记 this.notesList = allNotes; break; default: this.notesList = allNotes.filter(note => note.workKey === this.workKey); } this.notesList.sort((a, b) => b.timestamp - a.timestamp); } catch (error) { Logger.error('加载笔记失败', error); this.notesList = []; } } /** * 创建模态框 */ _createModal() { // 创建遮罩层 const overlay = DOMHelper.createElement('div', { style: { position: 'fixed', top: '0', left: '0', width: '100%', height: '100%', backgroundColor: 'rgba(0, 0, 0, 0.5)', zIndex: '99999', display: 'flex', justifyContent: 'center', alignItems: 'center' } }); // 创建主容器 const mainContainer = DOMHelper.createElement('div', { style: { backgroundColor: 'white', borderRadius: '12px', width: '90%', maxWidth: '900px', height: '85vh', display: 'flex', boxShadow: '0 10px 40px rgba(0, 0, 0, 0.2)', overflow: 'hidden' } }); // 创建左侧边栏 const sidebar = this._createSidebar(); mainContainer.appendChild(sidebar); // 创建右侧内容区 const contentArea = DOMHelper.createElement('div', { id: 'panel-content-area', style: { flex: '1', display: 'flex', flexDirection: 'column', backgroundColor: '#f7fafc' } }); // 创建内容区标题栏 const contentHeader = DOMHelper.createElement('div', { id: 'panel-content-header', style: { padding: '20px 30px', borderBottom: '1px solid #e2e8f0', backgroundColor: 'white', display: 'flex', justifyContent: 'space-between', alignItems: 'center' } }); const headerTitle = DOMHelper.createElement('h2', { id: 'panel-header-title', innerText: '⚙️ 设置', style: { margin: '0', fontSize: '20px', fontWeight: 'bold', color: '#2d3748' } }); const closeBtn = DOMHelper.createElement('button', { innerText: '✕', style: { background: 'none', border: 'none', fontSize: '24px', cursor: 'pointer', color: '#718096', padding: '0', width: '30px', height: '30px', lineHeight: '30px', textAlign: 'center', borderRadius: '50%', transition: 'background 0.2s' } }); closeBtn.addEventListener('mouseenter', () => { closeBtn.style.backgroundColor = '#e2e8f0'; }); closeBtn.addEventListener('mouseleave', () => { closeBtn.style.backgroundColor = 'transparent'; }); closeBtn.addEventListener('click', () => this._close()); contentHeader.appendChild(headerTitle); contentHeader.appendChild(closeBtn); contentArea.appendChild(contentHeader); // 创建内容主体 const contentBody = DOMHelper.createElement('div', { id: 'panel-content-body', style: { flex: '1', overflow: 'auto', padding: '30px' } }); contentArea.appendChild(contentBody); mainContainer.appendChild(contentArea); overlay.appendChild(mainContainer); // 点击遮罩层关闭 overlay.addEventListener('click', (e) => { if (e.target === overlay) { this._close(); } }); this.modal = overlay; document.body.appendChild(overlay); } /** * 创建左侧边栏 */ _createSidebar() { const sidebar = DOMHelper.createElement('div', { style: { width: '220px', backgroundColor: '#2d3748', display: 'flex', flexDirection: 'column', padding: '20px 0' } }); // 标题 const title = DOMHelper.createElement('div', { innerText: '控制面板', style: { padding: '0 20px 20px', fontSize: '18px', fontWeight: 'bold', color: 'white', borderBottom: '1px solid rgba(255, 255, 255, 0.1)', marginBottom: '10px' } }); sidebar.appendChild(title); // 菜单项 const menuItems = [ { id: 'settings', icon: '⚙️', text: '设置' }, { id: 'notes', icon: '📝', text: '管理笔记', hasSubmenu: true, submenu: [ { id: 'notes-current', icon: '📄', text: '当前页面', scope: 'current' }, { id: 'notes-course', icon: '📚', text: '当前课程', scope: 'course' }, { id: 'notes-class', icon: '👥', text: '当前班级', scope: 'class' }, { id: 'notes-domain', icon: '🌐', text: '整个域名', scope: 'domain' } ] }, { id: 'styles', icon: '🎨', text: '样式管理' } ]; menuItems.forEach(item => { const menuItem = this._createMenuItem(item); sidebar.appendChild(menuItem); }); return sidebar; } /** * 创建菜单项(支持子菜单) */ _createMenuItem(item) { const container = DOMHelper.createElement('div'); // 主菜单项 const menuItem = DOMHelper.createElement('div', { dataset: { tab: item.id }, style: { padding: '12px 20px', cursor: 'pointer', color: this.currentTab === item.id ? 'white' : '#a0aec0', backgroundColor: this.currentTab === item.id ? '#4a5568' : 'transparent', borderLeft: this.currentTab === item.id ? '3px solid #4299e1' : '3px solid transparent', fontWeight: this.currentTab === item.id ? 'bold' : 'normal', transition: 'all 0.2s', display: 'flex', alignItems: 'center', gap: '10px', justifyContent: 'space-between' } }); const leftContent = DOMHelper.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '10px' } }); const iconSpan = DOMHelper.createElement('span', { innerText: item.icon, style: { fontSize: '16px' } }); const textSpan = DOMHelper.createElement('span', { innerText: item.text, style: { fontSize: '14px' } }); leftContent.appendChild(iconSpan); leftContent.appendChild(textSpan); menuItem.appendChild(leftContent); // 如果有子菜单,添加展开图标 if (item.hasSubmenu) { const expandIcon = DOMHelper.createElement('span', { innerText: '▼', style: { fontSize: '10px', transition: 'transform 0.2s', transform: this.notesMenuExpanded ? 'rotate(0deg)' : 'rotate(-90deg)' } }); menuItem.appendChild(expandIcon); // 创建子菜单容器 const submenuContainer = DOMHelper.createElement('div', { style: { display: this.notesMenuExpanded ? 'block' : 'none', backgroundColor: '#1a202c' } }); item.submenu.forEach(subItem => { const subMenuItem = this._createSubMenuItem(subItem); submenuContainer.appendChild(subMenuItem); }); menuItem.addEventListener('click', () => { this.notesMenuExpanded = !this.notesMenuExpanded; expandIcon.style.transform = this.notesMenuExpanded ? 'rotate(0deg)' : 'rotate(-90deg)'; submenuContainer.style.display = this.notesMenuExpanded ? 'block' : 'none'; }); container.appendChild(menuItem); container.appendChild(submenuContainer); } else { // 无子菜单的普通菜单项 menuItem.addEventListener('mouseenter', () => { if (this.currentTab !== item.id) { menuItem.style.backgroundColor = '#4a5568'; menuItem.style.color = '#e2e8f0'; } }); menuItem.addEventListener('mouseleave', () => { if (this.currentTab !== item.id) { menuItem.style.backgroundColor = 'transparent'; menuItem.style.color = '#a0aec0'; } }); menuItem.addEventListener('click', () => { this.currentTab = item.id; this._updateSidebarState(); this._renderContent(); }); container.appendChild(menuItem); } return container; } /** * 创建子菜单项 */ _createSubMenuItem(subItem) { const isActive = this.currentTab === 'notes' && this.notesScope === subItem.scope; const subMenuItem = DOMHelper.createElement('div', { dataset: { scope: subItem.scope }, style: { padding: '10px 20px 10px 50px', cursor: 'pointer', color: isActive ? '#4299e1' : '#718096', backgroundColor: isActive ? '#2d3748' : 'transparent', fontSize: '13px', transition: 'all 0.2s', display: 'flex', alignItems: 'center', gap: '8px' } }); const icon = DOMHelper.createElement('span', { innerText: subItem.icon, style: { fontSize: '14px' } }); const text = DOMHelper.createElement('span', { innerText: subItem.text }); subMenuItem.appendChild(icon); subMenuItem.appendChild(text); subMenuItem.addEventListener('mouseenter', () => { if (!(this.currentTab === 'notes' && this.notesScope === subItem.scope)) { subMenuItem.style.backgroundColor = '#2d3748'; subMenuItem.style.color = '#a0aec0'; } }); subMenuItem.addEventListener('mouseleave', () => { if (!(this.currentTab === 'notes' && this.notesScope === subItem.scope)) { subMenuItem.style.backgroundColor = 'transparent'; subMenuItem.style.color = '#718096'; } }); subMenuItem.addEventListener('click', async () => { this.currentTab = 'notes'; this.notesScope = subItem.scope; this.selectedNotes.clear(); await this._loadNotes(); this._updateSidebarState(); this._renderContent(); }); return subMenuItem; } /** * 更新侧边栏状态 */ _updateSidebarState() { const menuItems = this.modal.querySelectorAll('[data-tab]'); menuItems.forEach(item => { const isActive = item.dataset.tab === this.currentTab; item.style.color = isActive ? 'white' : '#a0aec0'; item.style.backgroundColor = isActive ? '#4a5568' : 'transparent'; item.style.borderLeft = isActive ? '3px solid #4299e1' : '3px solid transparent'; item.style.fontWeight = isActive ? 'bold' : 'normal'; }); } /** * 渲染内容区 */ _renderContent() { const headerTitle = document.getElementById('panel-header-title'); const contentBody = document.getElementById('panel-content-body'); if (this.currentTab === 'settings') { headerTitle.innerText = '⚙️ 设置'; this._renderSettingsPanel(contentBody); } else if (this.currentTab === 'notes') { headerTitle.innerText = '📝 管理笔记'; this._renderNotesPanel(contentBody); } else if (this.currentTab === 'styles') { headerTitle.innerText = '🎨 样式管理'; this._renderStylesPanel(contentBody); } } /** * 渲染设置面板 */ _renderSettingsPanel(container) { container.innerHTML = ''; const settingsContainer = DOMHelper.createElement('div', { style: { backgroundColor: 'white', borderRadius: '8px', padding: '24px', boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)', marginBottom: '20px' } }); // 自动保存开关 const autoSaveSection = this._createSettingItem( '自动保存', '开启后会在输入停止一段时间后自动保存笔记', 'checkbox', 'autoSave', this.settings.autoSave ); settingsContainer.appendChild(autoSaveSection); // 自动保存延迟时间 const delaySection = this._createSettingItem( '自动保存延迟', '输入停止后多久开始保存(毫秒)', 'number', 'autoSaveDelay', this.settings.autoSaveDelay ); settingsContainer.appendChild(delaySection); container.appendChild(settingsContainer); // 添加保存按钮 const saveButtonContainer = DOMHelper.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: '10px' } }); const buttonConfig = this.config.get('panelSaveButton'); const saveButton = DOMHelper.createElement('button', { innerText: buttonConfig.text.save, style: { padding: buttonConfig.style.padding, border: buttonConfig.style.border, borderRadius: buttonConfig.style.borderRadius, backgroundColor: buttonConfig.colors.background, color: buttonConfig.colors.textColor, fontSize: buttonConfig.style.fontSize, fontWeight: buttonConfig.style.fontWeight, cursor: buttonConfig.style.cursor, transition: buttonConfig.style.transition, boxShadow: buttonConfig.colors.boxShadow } }); saveButton.addEventListener('mouseenter', () => { saveButton.style.backgroundColor = buttonConfig.colors.hoverBackground; saveButton.style.transform = 'translateY(-1px)'; saveButton.style.boxShadow = buttonConfig.colors.hoverBoxShadow; }); saveButton.addEventListener('mouseleave', () => { if (saveButton.innerText === buttonConfig.text.save) { saveButton.style.backgroundColor = buttonConfig.colors.background; saveButton.style.transform = 'translateY(0)'; saveButton.style.boxShadow = buttonConfig.colors.boxShadow; } }); saveButton.addEventListener('click', async () => { try { // 保存所有设置 await this.dbManager.saveSetting('autoSave', this.settings.autoSave); await this.dbManager.saveSetting('autoSaveDelay', this.settings.autoSaveDelay); // 显示成功提示 saveButton.innerText = buttonConfig.text.success; saveButton.style.backgroundColor = buttonConfig.colors.successBackground; setTimeout(() => { saveButton.innerText = buttonConfig.text.save; saveButton.style.backgroundColor = buttonConfig.colors.background; }, 2000); Logger.success('设置已保存'); } catch (error) { Logger.error('保存设置失败', error); saveButton.innerText = buttonConfig.text.error; saveButton.style.backgroundColor = buttonConfig.colors.errorBackground; setTimeout(() => { saveButton.innerText = buttonConfig.text.save; saveButton.style.backgroundColor = buttonConfig.colors.background; }, 2000); } }); saveButtonContainer.appendChild(saveButton); container.appendChild(saveButtonContainer); } /** * 创建设置项 */ _createSettingItem(label, description, type, key, value) { const item = DOMHelper.createElement('div', { style: { marginBottom: '24px', paddingBottom: '24px', borderBottom: '1px solid #e2e8f0' } }); const labelEl = DOMHelper.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' } }); const labelText = DOMHelper.createElement('span', { innerText: label, style: { fontSize: '16px', fontWeight: '600', color: '#2d3748' } }); let input; if (type === 'checkbox') { input = DOMHelper.createElement('input', { type: 'checkbox', checked: value, style: { width: '20px', height: '20px', cursor: 'pointer' } }); input.addEventListener('change', () => { this.settings[key] = input.checked; }); } else if (type === 'number') { input = DOMHelper.createElement('input', { type: 'number', value: value, style: { width: '120px', padding: '6px 12px', border: '1px solid #cbd5e0', borderRadius: '4px', fontSize: '14px' } }); input.addEventListener('change', () => { const numValue = parseInt(input.value); if (numValue > 0) { this.settings[key] = numValue; } }); } labelEl.appendChild(labelText); labelEl.appendChild(input); const desc = DOMHelper.createElement('div', { innerText: description, style: { fontSize: '13px', color: '#718096', marginTop: '4px' } }); item.appendChild(labelEl); item.appendChild(desc); return item; } /** * 渲染笔记管理面板 */ _renderNotesPanel(container) { container.innerHTML = ''; container.style.padding = '0'; if (this.notesList.length === 0) { const emptyMsg = DOMHelper.createElement('div', { innerText: '📭 暂无笔记', style: { textAlign: 'center', color: '#a0aec0', padding: '60px 20px', fontSize: '16px' } }); container.appendChild(emptyMsg); return; } // 操作栏 const toolbar = DOMHelper.createElement('div', { style: { padding: '15px 30px', backgroundColor: 'white', borderBottom: '1px solid #e2e8f0', display: 'flex', justifyContent: 'space-between', alignItems: 'center' } }); const info = DOMHelper.createElement('span', { id: 'notes-info-text', innerText: `共 ${this.notesList.length} 条笔记`, style: { fontSize: '14px', color: '#718096' } }); const actions = DOMHelper.createElement('div', { style: { display: 'flex', gap: '10px' } }); const selectAllBtn = DOMHelper.createElement('button', { innerText: '全选', style: { padding: '6px 14px', border: '1px solid #cbd5e0', borderRadius: '4px', backgroundColor: 'white', cursor: 'pointer', fontSize: '13px', fontWeight: '500', transition: 'all 0.2s' } }); const deleteBtn = DOMHelper.createElement('button', { innerText: '删除选中', style: { padding: '6px 14px', border: 'none', borderRadius: '4px', backgroundColor: '#f56565', color: 'white', cursor: 'pointer', fontSize: '13px', fontWeight: '500', transition: 'all 0.2s' } }); selectAllBtn.addEventListener('click', () => this._toggleSelectAll()); deleteBtn.addEventListener('click', () => this._deleteSelected()); actions.appendChild(selectAllBtn); actions.appendChild(deleteBtn); toolbar.appendChild(info); toolbar.appendChild(actions); // 笔记列表 const notesList = DOMHelper.createElement('div', { id: 'notes-list-content', style: { padding: '20px 30px', overflow: 'auto', flex: '1' } }); if (this.notesScope === 'current') { // 当前页面:直接显示笔记列表 this.notesList.forEach(note => { const noteItem = this._createNoteItem(note); notesList.appendChild(noteItem); }); } else { // 其他范围:按 workKey 分组显示 const groupedNotes = this._groupNotesByWorkKey(this.notesList); Object.entries(groupedNotes).forEach(([workKey, notes]) => { const group = this._createNotesGroup(workKey, notes); notesList.appendChild(group); }); } container.appendChild(toolbar); container.appendChild(notesList); } /** * 创建笔记项 */ _createNoteItem(note) { const item = DOMHelper.createElement('div', { style: { padding: '16px', marginBottom: '12px', border: '1px solid #e2e8f0', borderRadius: '8px', backgroundColor: this.selectedNotes.has(note.id) ? '#ebf8ff' : 'white', cursor: 'pointer', transition: 'all 0.2s' } }); item.addEventListener('mouseenter', () => { if (!this.selectedNotes.has(note.id)) { item.style.backgroundColor = '#f7fafc'; } }); item.addEventListener('mouseleave', () => { if (!this.selectedNotes.has(note.id)) { item.style.backgroundColor = 'white'; } }); const header = DOMHelper.createElement('div', { style: { display: 'flex', alignItems: 'center', marginBottom: '10px', gap: '10px' } }); const checkbox = DOMHelper.createElement('input', { type: 'checkbox', checked: this.selectedNotes.has(note.id), style: { width: '16px', height: '16px', cursor: 'pointer' } }); checkbox.addEventListener('change', (e) => { e.stopPropagation(); if (checkbox.checked) { this.selectedNotes.add(note.id); item.style.backgroundColor = '#ebf8ff'; } else { this.selectedNotes.delete(note.id); item.style.backgroundColor = 'white'; } this._updateNotesInfo(); }); const questionId = DOMHelper.createElement('span', { innerText: note.questionId, style: { fontSize: '14px', fontWeight: '600', color: '#4299e1', flex: '1' } }); const time = DOMHelper.createElement('span', { innerText: new Date(note.timestamp).toLocaleString('zh-CN'), style: { fontSize: '12px', color: '#a0aec0' } }); header.appendChild(checkbox); header.appendChild(questionId); header.appendChild(time); const content = DOMHelper.createElement('div', { innerText: note.content || '(空笔记)', style: { fontSize: '14px', color: note.content ? '#2d3748' : '#a0aec0', lineHeight: '1.6', maxHeight: '80px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'pre-wrap' } }); item.appendChild(header); item.appendChild(content); item.addEventListener('click', (e) => { if (e.target !== checkbox) { checkbox.checked = !checkbox.checked; checkbox.dispatchEvent(new Event('change')); } }); return item; } /** * 切换全选 */ _toggleSelectAll() { if (this.selectedNotes.size === this.notesList.length) { this.selectedNotes.clear(); } else { this.notesList.forEach(note => this.selectedNotes.add(note.id)); } this._renderContent(); } /** * 删除选中的笔记 */ async _deleteSelected() { if (this.selectedNotes.size === 0) { alert('请先选择要删除的笔记'); return; } if (!confirm(`确定要删除选中的 ${this.selectedNotes.size} 条笔记吗?\n此操作不可恢复!`)) { return; } try { const noteIds = Array.from(this.selectedNotes); await this.dbManager.deleteNotes(noteIds); Logger.success(`已删除 ${noteIds.length} 条笔记`); this.selectedNotes.clear(); await this._loadNotes(); this._renderContent(); } catch (error) { Logger.error('删除笔记失败', error); alert('删除笔记失败,请查看控制台了解详情'); } } /** * 更新笔记信息 */ _updateNotesInfo() { const info = document.getElementById('notes-info-text'); if (info) { const selectedText = this.selectedNotes.size > 0 ? `,已选中 ${this.selectedNotes.size} 条` : ''; info.innerText = `共 ${this.notesList.length} 条笔记${selectedText}`; } } /** * 按 workKey 分组笔记 */ _groupNotesByWorkKey(notes) { const groups = {}; notes.forEach(note => { if (!groups[note.workKey]) { groups[note.workKey] = []; } groups[note.workKey].push(note); }); // 按时间戳排序每个组 Object.keys(groups).forEach(key => { groups[key].sort((a, b) => b.timestamp - a.timestamp); }); return groups; } /** * 创建笔记组(用于域名模式) */ _createNotesGroup(workKey, notes) { const group = DOMHelper.createElement('div', { style: { marginBottom: '30px' } }); // 组标题 const groupHeader = DOMHelper.createElement('div', { style: { padding: '12px 16px', backgroundColor: '#e3f2fd', borderRadius: '8px', marginBottom: '12px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer', transition: 'all 0.2s' } }); const headerLeft = DOMHelper.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '10px' } }); const collapseIcon = DOMHelper.createElement('span', { innerText: '▼', style: { fontSize: '12px', color: '#1976d2', transition: 'transform 0.2s' } }); const groupTitle = DOMHelper.createElement('span', { innerText: `📄 ${workKey}`, style: { fontSize: '14px', fontWeight: '600', color: '#1976d2' } }); const groupCount = DOMHelper.createElement('span', { innerText: `(${notes.length} 条)`, style: { fontSize: '13px', color: '#64b5f6', marginLeft: '8px' } }); headerLeft.appendChild(collapseIcon); headerLeft.appendChild(groupTitle); headerLeft.appendChild(groupCount); // 全选此组的按钮 const selectGroupBtn = DOMHelper.createElement('button', { innerText: '全选', style: { padding: '4px 10px', border: '1px solid #2196f3', borderRadius: '4px', backgroundColor: 'white', color: '#2196f3', cursor: 'pointer', fontSize: '12px', fontWeight: '500', transition: 'all 0.2s' } }); selectGroupBtn.addEventListener('click', (e) => { e.stopPropagation(); const allSelected = notes.every(note => this.selectedNotes.has(note.id)); if (allSelected) { notes.forEach(note => this.selectedNotes.delete(note.id)); selectGroupBtn.innerText = '全选'; } else { notes.forEach(note => this.selectedNotes.add(note.id)); selectGroupBtn.innerText = '取消'; } this._renderContent(); }); groupHeader.appendChild(headerLeft); groupHeader.appendChild(selectGroupBtn); // 笔记列表容器 const notesContainer = DOMHelper.createElement('div', { style: { display: 'block', paddingLeft: '20px' } }); notes.forEach(note => { const noteItem = this._createNoteItem(note); notesContainer.appendChild(noteItem); }); // 折叠/展开功能 let isCollapsed = false; groupHeader.addEventListener('click', (e) => { if (e.target === selectGroupBtn) return; isCollapsed = !isCollapsed; notesContainer.style.display = isCollapsed ? 'none' : 'block'; collapseIcon.style.transform = isCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)'; }); group.appendChild(groupHeader); group.appendChild(notesContainer); return group; } /** * 渲染样式管理面板 */ async _renderStylesPanel(container) { container.innerHTML = ''; // 样式配置的分类 const styleCategories = [ { title: '答案按钮样式', key: 'answerButton', fields: [ { name: 'fontSize', label: '字体大小', type: 'text', path: 'style.fontSize' }, { name: 'padding', label: '内边距', type: 'text', path: 'style.padding' }, { name: 'borderRadius', label: '圆角半径', type: 'text', path: 'style.borderRadius' }, { name: 'showBackground', label: '显示按钮背景色', type: 'color', path: 'colors.showBackground' }, { name: 'hideBackground', label: '隐藏按钮背景色', type: 'color', path: 'colors.hideBackground' } ] }, { title: '笔记按钮样式', key: 'noteButton', fields: [ { name: 'fontSize', label: '字体大小', type: 'text', path: 'style.fontSize' }, { name: 'padding', label: '内边距', type: 'text', path: 'style.padding' }, { name: 'showBackground', label: '显示按钮背景色', type: 'color', path: 'colors.showBackground' }, { name: 'hideBackground', label: '隐藏按钮背景色', type: 'color', path: 'colors.hideBackground' } ] }, { title: '保存笔记按钮样式', key: 'saveNoteButton', fields: [ { name: 'fontSize', label: '字体大小', type: 'text', path: 'style.fontSize' }, { name: 'padding', label: '内边距', type: 'text', path: 'style.padding' }, { name: 'background', label: '背景色', type: 'color', path: 'colors.background' } ] }, { title: '笔记编辑器样式', key: 'noteEditor', fields: [ { name: 'width', label: '宽度', type: 'text', path: 'width' }, { name: 'minHeight', label: '最小高度', type: 'text', path: 'minHeight' }, { name: 'maxHeight', label: '最大高度', type: 'text', path: 'maxHeight' }, { name: 'fontSize', label: '字体大小', type: 'text', path: 'fontSize' }, { name: 'backgroundColor', label: '背景色', type: 'color', path: 'backgroundColor' }, { name: 'borderColor', label: '边框颜色', type: 'color', path: 'borderColor' } ] } ]; // 加载已保存的样式配置 const savedStyles = await this.dbManager.getSetting('customStyles', {}); // 创建滚动容器 const scrollContainer = DOMHelper.createElement('div', { style: { overflow: 'auto', padding: '20px' } }); // 为每个分类创建配置区块 styleCategories.forEach(category => { const section = this._createStyleSection(category, savedStyles); scrollContainer.appendChild(section); }); container.appendChild(scrollContainer); // 添加保存和重置按钮 const buttonContainer = DOMHelper.createElement('div', { style: { padding: '20px', borderTop: '1px solid #e2e8f0', display: 'flex', justifyContent: 'space-between', backgroundColor: 'white' } }); const resetButton = DOMHelper.createElement('button', { innerText: '🔄 重置为默认', style: { padding: '10px 20px', border: '1px solid #cbd5e0', borderRadius: '6px', backgroundColor: 'white', color: '#718096', fontSize: '14px', fontWeight: '600', cursor: 'pointer', transition: 'all 0.2s' } }); resetButton.addEventListener('click', async () => { if (confirm('确定要重置所有样式为默认值吗?')) { await this.dbManager.saveSetting('customStyles', {}); Logger.success('样式已重置'); this._renderStylesPanel(container); } }); const buttonConfig = this.config.get('panelSaveButton'); const saveButton = DOMHelper.createElement('button', { innerText: buttonConfig.text.save, style: { padding: buttonConfig.style.padding, border: buttonConfig.style.border, borderRadius: buttonConfig.style.borderRadius, backgroundColor: buttonConfig.colors.background, color: buttonConfig.colors.textColor, fontSize: buttonConfig.style.fontSize, fontWeight: buttonConfig.style.fontWeight, cursor: buttonConfig.style.cursor, transition: buttonConfig.style.transition, boxShadow: buttonConfig.colors.boxShadow } }); saveButton.addEventListener('click', async () => { try { const customStyles = {}; // 收集所有表单数据 styleCategories.forEach(category => { category.fields.forEach(field => { const input = document.getElementById(`style-${category.key}-${field.name}`); if (input && input.value) { if (!customStyles[category.key]) { customStyles[category.key] = {}; } // 设置嵌套属性 const pathParts = field.path.split('.'); let target = customStyles[category.key]; for (let i = 0; i < pathParts.length - 1; i++) { if (!target[pathParts[i]]) { target[pathParts[i]] = {}; } target = target[pathParts[i]]; } target[pathParts[pathParts.length - 1]] = input.value; } }); }); await this.dbManager.saveSetting('customStyles', customStyles); saveButton.innerText = buttonConfig.text.success; saveButton.style.backgroundColor = buttonConfig.colors.successBackground; setTimeout(() => { saveButton.innerText = buttonConfig.text.save; saveButton.style.backgroundColor = buttonConfig.colors.background; }, 2000); Logger.success('样式已保存,刷新页面后生效'); } catch (error) { Logger.error('保存样式失败', error); saveButton.innerText = buttonConfig.text.error; saveButton.style.backgroundColor = buttonConfig.colors.errorBackground; setTimeout(() => { saveButton.innerText = buttonConfig.text.save; saveButton.style.backgroundColor = buttonConfig.colors.background; }, 2000); } }); buttonContainer.appendChild(resetButton); buttonContainer.appendChild(saveButton); container.appendChild(buttonContainer); } /** * 创建样式配置区块 */ _createStyleSection(category, savedStyles) { const section = DOMHelper.createElement('div', { style: { backgroundColor: 'white', borderRadius: '8px', padding: '20px', marginBottom: '20px', boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)' } }); const title = DOMHelper.createElement('h3', { innerText: category.title, style: { margin: '0 0 16px 0', fontSize: '16px', fontWeight: '600', color: '#2d3748', borderBottom: '2px solid #4299e1', paddingBottom: '8px' } }); section.appendChild(title); category.fields.forEach(field => { const fieldGroup = DOMHelper.createElement('div', { style: { marginBottom: '16px', display: 'flex', alignItems: 'center', justifyContent: 'space-between' } }); const label = DOMHelper.createElement('label', { innerText: field.label, style: { fontSize: '14px', color: '#4a5568', fontWeight: '500', flex: '1' } }); // 获取当前值(优先使用保存的值,否则使用默认配置值) let currentValue; if (savedStyles[category.key]) { const pathParts = field.path.split('.'); let value = savedStyles[category.key]; for (let part of pathParts) { value = value?.[part]; } currentValue = value; } if (!currentValue) { const pathParts = field.path.split('.'); let value = this.config.get(category.key); for (let part of pathParts) { value = value?.[part]; } currentValue = value || ''; } const input = DOMHelper.createElement('input', { type: field.type, value: currentValue, id: `style-${category.key}-${field.name}`, style: { width: field.type === 'color' ? '60px' : '150px', padding: '6px 10px', border: '1px solid #cbd5e0', borderRadius: '4px', fontSize: '13px' } }); fieldGroup.appendChild(label); fieldGroup.appendChild(input); section.appendChild(fieldGroup); }); return section; } /** * 关闭模态框 */ _close() { if (this.modal && this.modal.parentNode) { document.body.removeChild(this.modal); this.modal = null; } } } // ===================== 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; } /** * 获取单个答案按钮的样式 * @param {boolean} isHidden - 是否为隐藏状态 * @returns {Object} 样式对象 */ getAnswerButtonStyle(isHidden = true) { const position = this.config.get('answerButton.position'); const style = this.config.get('answerButton.style'); const colors = this.config.get('answerButton.colors'); return { marginLeft: position.marginLeft, marginRight: position.marginRight, marginTop: position.marginTop, marginBottom: position.marginBottom, verticalAlign: position.verticalAlign, padding: style.padding, border: style.border, borderRadius: style.borderRadius, background: isHidden ? colors.showBackground : colors.hideBackground, color: colors.textColor, fontSize: style.fontSize, fontWeight: style.fontWeight, cursor: style.cursor, transition: style.transition, display: 'inline-block' }; } /** * 获取笔记按钮的样式 * @param {boolean} isVisible - 笔记是否可见 * @returns {Object} 样式对象 */ getNoteButtonStyle(isVisible = false) { const position = this.config.get('noteButton.position'); const style = this.config.get('noteButton.style'); const colors = this.config.get('noteButton.colors'); return { marginLeft: position.marginLeft, marginRight: position.marginRight, marginTop: position.marginTop, marginBottom: position.marginBottom, verticalAlign: position.verticalAlign, padding: style.padding, border: style.border, borderRadius: style.borderRadius, background: isVisible ? colors.hideBackground : colors.showBackground, color: colors.textColor, fontSize: style.fontSize, fontWeight: style.fontWeight, cursor: style.cursor, transition: style.transition, display: 'inline-block' }; } /** * 获取保存笔记按钮的样式 * @returns {Object} 样式对象 */ getSaveNoteButtonStyle() { const position = this.config.get('saveNoteButton.position'); const style = this.config.get('saveNoteButton.style'); const colors = this.config.get('saveNoteButton.colors'); return { marginLeft: position.marginLeft, marginRight: position.marginRight, marginTop: position.marginTop, marginBottom: position.marginBottom, verticalAlign: position.verticalAlign, padding: style.padding, border: style.border, borderRadius: style.borderRadius, background: colors.background, color: colors.textColor, fontSize: style.fontSize, fontWeight: style.fontWeight, cursor: style.cursor, transition: style.transition, display: 'inline-block' }; } /** * 获取全局按钮的样式 * @param {boolean} isHidden - 是否为全部隐藏状态 * @returns {Object} 样式对象 */ getGlobalButtonStyle(isHidden = true) { const position = this.config.get('globalButton.position'); const style = this.config.get('globalButton.style'); const colors = this.config.get('globalButton.colors'); return { position: 'absolute', top: position.top, right: position.right, zIndex: position.zIndex, border: style.border, borderRadius: style.borderRadius, padding: style.padding, fontSize: style.fontSize, fontWeight: style.fontWeight, color: colors.textColor, cursor: style.cursor, transition: style.transition, background: isHidden ? colors.showAllBackground : colors.hideAllBackground }; } /** * 获取笔记编辑器的样式 * @returns {Object} 样式对象 */ getNoteEditorStyle() { const noteConfig = this.config.get('noteEditor'); return { width: noteConfig.width || '100%', minHeight: noteConfig.minHeight, maxHeight: noteConfig.maxHeight, padding: noteConfig.padding, marginTop: noteConfig.marginTop, marginBottom: noteConfig.marginBottom, fontSize: noteConfig.fontSize, border: `${noteConfig.borderWidth} ${noteConfig.borderStyle} ${noteConfig.borderColor}`, borderRadius: noteConfig.borderRadius, backgroundColor: noteConfig.backgroundColor, color: noteConfig.textColor, resize: noteConfig.resize, fontFamily: noteConfig.fontFamily, outline: 'none', display: 'none', transition: 'border-color 0.2s', boxSizing: 'border-box' }; } /** * 获取管理按钮的样式 * @returns {Object} 样式对象 */ getManageButtonStyle() { const position = this.config.get('manageButton.position'); const style = this.config.get('manageButton.style'); const colors = this.config.get('manageButton.colors'); return { position: 'absolute', top: position.top, right: position.right, zIndex: position.zIndex, border: style.border, borderRadius: style.borderRadius, padding: style.padding, fontSize: style.fontSize, fontWeight: style.fontWeight, color: colors.textColor, cursor: style.cursor, transition: style.transition, background: colors.background }; } } // ===================== 答案块控制器 ===================== class AnswerBlockController { constructor(block, config, styleGenerator, dbManager, workKey) { this.block = block; this.config = config; this.styleGenerator = styleGenerator; this.dbManager = dbManager; this.workKey = workKey; this.parent = block.parentNode; this.nextSibling = block.nextSibling; this.originalHTML = block.outerHTML; this.toggleButton = null; this.noteButton = null; this.saveNoteButton = null; this.noteEditor = null; this.buttonContainer = null; this.currentAnswerBlock = null; // 跟踪当前显示的答案块 this.isHidden = false; this.questionId = this._extractQuestionId(); } _extractQuestionId() { // 从父元素中查找包含 question 的 id let element = this.block; while (element && element !== document.body) { if (element.id && element.id.startsWith('question')) { return element.id; } element = element.parentElement; } // 如果没找到,生成一个唯一标识 return `question_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } async initialize() { this._hideBlockInitial(); await this._createButtons(); await this._createNoteEditor(); return this.buttonContainer; } _hideBlockInitial() { // 初始化时删除原始答案块 DOMHelper.removeElement(this.block); this.currentAnswerBlock = null; this.isHidden = true; } async _createButtons() { // 创建按钮容器 this.buttonContainer = DOMHelper.createElement('div', { style: { display: 'inline-block', marginLeft: this.config.get('answerButton.position.marginLeft'), marginTop: this.config.get('answerButton.position.marginTop'), verticalAlign: this.config.get('answerButton.position.verticalAlign') } }); // 创建答案切换按钮 this._createAnswerToggleButton(); // 创建笔记切换按钮 this._createNoteToggleButton(); // 创建保存笔记按钮 this._createSaveNoteButton(); // 插入按钮容器 DOMHelper.insertElement(this.buttonContainer, this.parent, this.nextSibling); } _createAnswerToggleButton() { const buttonText = this.config.get('answerButton.text'); this.toggleButton = DOMHelper.createElement('button', { innerText: buttonText.show, style: this.styleGenerator.getAnswerButtonStyle(true), title: '点击显示/隐藏当前答案块', dataset: { isHidden: 'true', originalHTML: this.originalHTML } }); this.toggleButton.addEventListener('click', () => this._handleAnswerToggle()); this.buttonContainer.appendChild(this.toggleButton); } _createNoteToggleButton() { const buttonText = this.config.get('noteButton.text'); this.noteButton = DOMHelper.createElement('button', { innerText: buttonText.show, style: this.styleGenerator.getNoteButtonStyle(false), title: '点击显示/隐藏笔记编辑器', dataset: { isVisible: 'false' } }); this.noteButton.addEventListener('click', () => this._handleNoteToggle()); this.buttonContainer.appendChild(this.noteButton); } _createSaveNoteButton() { const buttonText = this.config.get('saveNoteButton.text'); this.saveNoteButton = DOMHelper.createElement('button', { innerText: buttonText, style: this.styleGenerator.getSaveNoteButtonStyle(), title: '手动保存当前笔记' }); this.saveNoteButton.addEventListener('click', async () => { await this.noteEditor.save(); Logger.success('💾 笔记已保存'); }); this.buttonContainer.appendChild(this.saveNoteButton); } async _createNoteEditor() { this.noteEditor = new NoteEditor( this.questionId, this.workKey, this.dbManager, this.config, this.styleGenerator ); const editorElement = await this.noteEditor.create(); // 将编辑器插入到按钮容器之后 DOMHelper.insertElement(editorElement, this.parent, this.buttonContainer.nextSibling); } _handleAnswerToggle() { if (this.isHidden) { this._showBlock(); } else { this._hideBlock(); } this._updateAnswerButtonState(); } _showBlock() { // 如果已经有显示的答案块,先删除它(防止重复) if (this.currentAnswerBlock && this.currentAnswerBlock.parentNode) { DOMHelper.removeElement(this.currentAnswerBlock); } const tempContainer = document.createElement('div'); tempContainer.innerHTML = this.originalHTML; const restoredBlock = tempContainer.firstChild; // 保存对新创建的答案块的引用 this.currentAnswerBlock = restoredBlock; // 插入到笔记编辑器之后(如果可见)或按钮容器之后 const insertAfter = this.noteEditor.isVisible ? this.noteEditor.getElement().nextSibling : this.buttonContainer.nextSibling; DOMHelper.insertElement(restoredBlock, this.parent, insertAfter); this.isHidden = false; } _hideBlock() { // 删除当前显示的答案块 if (this.currentAnswerBlock && this.currentAnswerBlock.parentNode) { DOMHelper.removeElement(this.currentAnswerBlock); this.currentAnswerBlock = null; } this.isHidden = true; } _updateAnswerButtonState() { const buttonText = this.config.get('answerButton.text'); const colors = this.config.get('answerButton.colors'); this.toggleButton.innerText = this.isHidden ? buttonText.show : buttonText.hide; this.toggleButton.style.background = this.isHidden ? colors.showBackground : colors.hideBackground; this.toggleButton.dataset.isHidden = String(this.isHidden); } _handleNoteToggle() { this.noteEditor.toggle(); this._updateNoteButtonState(); } _updateNoteButtonState() { const buttonText = this.config.get('noteButton.text'); const colors = this.config.get('noteButton.colors'); this.noteButton.innerText = this.noteEditor.isVisible ? buttonText.hide : buttonText.show; this.noteButton.style.background = this.noteEditor.isVisible ? colors.hideBackground : colors.showBackground; this.noteButton.dataset.isVisible = String(this.noteEditor.isVisible); } toggle() { this._handleAnswerToggle(); } getState() { return this.isHidden; } } // ===================== 全局控制器 ===================== class GlobalController { constructor(container, controllers, config, styleGenerator, dbManager, workKey) { this.container = container; this.controllers = controllers; this.config = config; this.styleGenerator = styleGenerator; this.dbManager = dbManager; this.workKey = workKey; this.globalButton = null; this.manageButton = null; } initialize() { if (!this.container) return null; DOMHelper.ensureRelativePosition(this.container); this._createGlobalButton(); this._createManageButton(); return this.globalButton; } _createGlobalButton() { const buttonText = this.config.get('globalButton.text'); this.globalButton = DOMHelper.createElement('button', { innerText: buttonText.showAll, style: this.styleGenerator.getGlobalButtonStyle(true), title: '点击一键显示/隐藏所有答案块' }); this.globalButton.addEventListener('click', () => this._handleGlobalToggle()); this.container.appendChild(this.globalButton); } _createManageButton() { const buttonText = this.config.get('manageButton.text'); this.manageButton = DOMHelper.createElement('button', { innerText: buttonText, style: this.styleGenerator.getManageButtonStyle(), title: '打开控制面板:设置和笔记管理' }); this.manageButton.addEventListener('click', () => this._handleManageClick()); this.container.appendChild(this.manageButton); } _handleManageClick() { const controlPanel = new ControlPanelUI(this.dbManager, this.workKey, this.config); controlPanel.show(); } _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('globalButton.text'); const colors = this.config.get('globalButton.colors'); this.globalButton.innerText = allHidden ? buttonText.showAll : buttonText.hideAll; this.globalButton.style.background = allHidden ? colors.showAllBackground : colors.hideAllBackground; } } // ===================== 主应用类 ===================== class ChaoxingAnswerHider { constructor(customConfig = {}) { this.config = new Config(customConfig); this.styleGenerator = new StyleGenerator(this.config); this.dbManager = new DatabaseManager(this.config); this.answerControllers = []; this.globalController = null; this.workKey = URLParser.getWorkKey(); } async initialize() { try { // 初始化数据库 await this.dbManager.init(); Logger.success('数据库初始化成功'); // 加载自定义样式配置 await this._loadCustomStyles(); await this._waitForPageLoad(); const elements = this._findElements(); if (!this._validateElements(elements)) { return; } await this._initializeAnswerBlocks(elements.answerBlocks); this._initializeGlobalControl(elements.container); this._logSuccess(elements.answerBlocks.length, !!elements.container); } catch (error) { Logger.error('初始化失败', error); } } async _loadCustomStyles() { try { const customStyles = await this.dbManager.getSetting('customStyles', {}); if (customStyles && Object.keys(customStyles).length > 0) { // 将自定义样式合并到配置中 this.config = new Config(this.config._deepMerge(this.config.config, customStyles)); this.styleGenerator = new StyleGenerator(this.config); Logger.log('✨ 已加载自定义样式配置'); } } 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; } async _initializeAnswerBlocks(blocks) { for (const block of blocks) { const controller = new AnswerBlockController( block, this.config, this.styleGenerator, this.dbManager, this.workKey ); await controller.initialize(); this.answerControllers.push(controller); } } _initializeGlobalControl(container) { this.globalController = new GlobalController( container, this.answerControllers, this.config, this.styleGenerator, this.dbManager, this.workKey ); 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)); Logger.log(`📝 笔记功能已启用,数据存储标识: ${this.workKey}`); } } // ===================== 启动应用 ===================== const app = new ChaoxingAnswerHider(); app.initialize(); })();