// ==UserScript== // @name 智慧职教全能助手 // @namespace http://tampermonkey.net/ // @version 1.4.3 // @author caokun // @description 智慧职教MOOC学习助手:仅支持智慧职教MOOC平台,集成自动学习和AI智能答题功能 // @license MIT // @icon https://www.icve.com.cn/favicon.ico // @homepageURL https://github.com/hearthealt/Smart-Vocational-Education // @supportURL https://github.com/hearthealt/Smart-Vocational-Education/issues // @match https://*.icve.com.cn/excellent-study/* // @match https://*.icve.com.cn/preview-exam/* // @connect * // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @run-at document-idle // ==/UserScript== (function () { 'use strict'; const Utils = { // 延时函数 sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)), // 格式化时间(秒转为 MM:SS 格式) formatTime: (seconds) => { if (!seconds || isNaN(seconds) || seconds === Infinity) return "0:00"; const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, "0")}`; }, // 防抖函数 debounce: (fn, delay = 300) => { let timer = null; return function(...args) { if (timer) clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; }, // 节流函数 throttle: (fn, delay = 300) => { let lastTime = 0; return function(...args) { const now = Date.now(); if (now - lastTime >= delay) { lastTime = now; return fn.apply(this, args); } }; }, // 安全获取DOM元素 $(selector, parent = document) { return parent.querySelector(selector); }, // 安全获取多个DOM元素 $$(selector, parent = document) { return Array.from(parent.querySelectorAll(selector)); }, // 带重试的异步操作 async retry(fn, maxRetries = 3, delay = 1e3) { for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (error) { if (i === maxRetries - 1) throw error; await this.sleep(delay); } } } }; const Logger = { _prefix: "[智慧职教助手]", _maxLogs: 100, // 最大日志条数 _logs: [], // 存储日志数据 _log(level, ...args) { const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString(); this._addPageLog(level, timestamp, args); }, _addPageLog(level, timestamp, args) { const message = args.map( (arg) => typeof arg === "object" ? JSON.stringify(arg) : String(arg) ).join(" "); const logEntry = { level, timestamp, message, id: Date.now() + Math.random() }; this._logs.push(logEntry); if (this._logs.length > this._maxLogs) { this._logs.shift(); } this._updatePageLog(logEntry); if (typeof updateLogCount === "function") { updateLogCount(); } }, _updatePageLog(logEntry) { const container = document.getElementById("page-log-container"); if (!container) return; const placeholder = container.querySelector(".log-placeholder"); if (placeholder) { placeholder.remove(); } const logElement = this._createLogElement(logEntry); container.appendChild(logElement); while (container.children.length > this._maxLogs) { container.removeChild(container.firstChild); } container.scrollTop = container.scrollHeight; }, _createLogElement(logEntry) { const div = document.createElement("div"); div.className = `log-entry log-${logEntry.level}`; div.innerHTML = ` ${logEntry.timestamp} ${this._getLogIcon(logEntry.level)} ${logEntry.message} `; return div; }, _getLogIcon(level) { const icons = { info: "ℹ️", success: "✅", warn: "⚠️", error: "❌" }; return icons[level] || "ℹ️"; }, clearPageLog() { this._logs = []; const container = document.getElementById("page-log-container"); if (container) { container.innerHTML = '
暂无日志记录
'; } if (typeof updateLogCount === "function") { updateLogCount(); } }, info(...args) { this._log("info", ...args); }, success(...args) { this._log("success", ...args); }, warn(...args) { this._log("warn", ...args); }, error(...args) { this._log("error", ...args); } }; const AI_PRESETS = { qwen: { name: "通义千问", baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1", model: "qwen-max", defaultKey: "", keyPlaceholder: "sk-xxx" }, gpt: { name: "OpenAI GPT", baseURL: "https://api.openai.com/v1", model: "gpt-4o-mini", defaultKey: "", keyPlaceholder: "sk-xxx" }, deepseek: { name: "DeepSeek", baseURL: "https://api.deepseek.com/v1", model: "deepseek-chat", defaultKey: "", keyPlaceholder: "sk-xxx" }, kimi: { name: "Moonshot AI (Kimi)", baseURL: "https://api.moonshot.cn/v1", model: "moonshot-v1-8k", defaultKey: "", keyPlaceholder: "sk-xxx" }, zhipu: { name: "智谱AI (GLM)", baseURL: "https://open.bigmodel.cn/api/paas/v4", model: "glm-4", defaultKey: "", keyPlaceholder: "xxx.xxx" }, custom: { name: "自定义", baseURL: "", model: "", defaultKey: "", keyPlaceholder: "your-api-key" } }; const ConfigManager = { // 配置键名映射 keys: { learning: { playbackRate: "learning_playbackRate", waitTimeAfterComplete: "learning_waitTime", documentPageInterval: "learning_docInterval", expandDelay: "learning_expandDelay", muteMedia: "learning_muteMedia" }, exam: { delay: "exam_delay", autoSubmit: "exam_autoSubmit", currentAI: "exam_currentAI" }, progress: { processedNodes: "learning_processedNodes", completedChapters: "learning_completedChapters" } }, // 默认值 defaults: { learning: { playbackRate: 1, waitTimeAfterComplete: 2, documentPageInterval: 1, expandDelay: 3, muteMedia: false }, exam: { delay: 3e3, autoSubmit: false, currentAI: "qwen" } }, // 获取配置值 get(category, key) { var _a, _b; const storageKey = (_a = this.keys[category]) == null ? void 0 : _a[key]; const defaultValue = (_b = this.defaults[category]) == null ? void 0 : _b[key]; if (storageKey) { return GM_getValue(storageKey, defaultValue); } return defaultValue; }, // 批量保存配置 saveAll(config) { Object.keys(this.keys.learning).forEach((key) => { if (config.learning && config.learning[key] !== void 0) { GM_setValue(this.keys.learning[key], config.learning[key]); } }); Object.keys(this.keys.exam).forEach((key) => { if (config.exam && config.exam[key] !== void 0) { GM_setValue(this.keys.exam[key], config.exam[key]); } }); if (config.theme) { localStorage.setItem("icve_theme_mode", config.theme); } }, // 获取AI配置 getAIConfig(aiType) { const preset = AI_PRESETS[aiType]; return { apiKey: GM_getValue(`ai_key_${aiType}`, preset.defaultKey), baseURL: GM_getValue(`ai_baseurl_${aiType}`, preset.baseURL), model: GM_getValue(`ai_model_${aiType}`, preset.model) }; } }; const state = { // 学习模式状态 learning: { isRunning: false, currentNode: null, allNodes: [], completedCount: 0, totalCount: 0, examCount: 0, processedNodes: new Set(GM_getValue(ConfigManager.keys.progress.processedNodes, [])), completedChapters: new Set(GM_getValue(ConfigManager.keys.progress.completedChapters, [])), currentPage: 1, totalPages: 1, isDocument: false, mediaWatching: false }, // 答题模式状态 exam: { isRunning: false, currentQuestionIndex: 0, totalQuestions: 0 } }; function saveLearningProgress() { GM_setValue(ConfigManager.keys.progress.processedNodes, Array.from(state.learning.processedNodes)); GM_setValue(ConfigManager.keys.progress.completedChapters, Array.from(state.learning.completedChapters)); } function loadLearningProgress() { const processedNodes = GM_getValue(ConfigManager.keys.progress.processedNodes, []); const completedChapters = GM_getValue(ConfigManager.keys.progress.completedChapters, []); state.learning.processedNodes = new Set(processedNodes); state.learning.completedChapters = new Set(completedChapters); } const CONFIG = { learning: { playbackRate: ConfigManager.get("learning", "playbackRate"), waitTimeAfterComplete: ConfigManager.get("learning", "waitTimeAfterComplete"), documentPageInterval: ConfigManager.get("learning", "documentPageInterval"), expandDelay: ConfigManager.get("learning", "expandDelay"), muteMedia: ConfigManager.get("learning", "muteMedia") }, exam: { delay: ConfigManager.get("exam", "delay"), autoSubmit: ConfigManager.get("exam", "autoSubmit"), currentAI: ConfigManager.get("exam", "currentAI") }, theme: localStorage.getItem("icve_theme_mode") || "light", currentTab: "learning" }; function saveConfig() { ConfigManager.saveAll(CONFIG); } function createLearningTab() { return `
停止中 📊 0/0 0
📖 等待开始...
等待开始...

⚙️ 学习配置

`; } function getAIConfig$1() { const preset = AI_PRESETS[CONFIG.exam.currentAI]; return { apiKey: GM_getValue(`ai_key_${CONFIG.exam.currentAI}`, preset.defaultKey), baseURL: GM_getValue(`ai_baseurl_${CONFIG.exam.currentAI}`, preset.baseURL), model: GM_getValue(`ai_model_${CONFIG.exam.currentAI}`, preset.model) }; } function createExamTab() { let aiOptions = ""; for (const [key, preset] of Object.entries(AI_PRESETS)) { const selected = CONFIG.exam.currentAI === key ? "selected" : ""; aiOptions += ``; } const aiConfig = getAIConfig$1(); return `
🎯 就绪 📊 0/0 🤖 ${AI_PRESETS[CONFIG.exam.currentAI].name}
🔑 API Key 需要密钥才能使用AI答题
⚙️ 高级配置(可选)
默认使用官方地址,如需使用代理可修改
默认使用推荐模型,高级用户可自定义
💡 配置完成后点击"开始答题"
`; } function createLogTab() { return `
暂无日志记录
`; } function addStyles() { const style = document.createElement("style"); style.textContent = ` /* ==================== 导入字体 ==================== */ @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap'); /* ==================== CSS 变量系统 ==================== */ :root { /* 主色调 - 极光渐变系 */ --icve-primary-from: #6366f1; --icve-primary-via: #8b5cf6; --icve-primary-to: #d946ef; --icve-primary-glow: rgba(139, 92, 246, 0.4); /* 功能色 */ --icve-success-from: #10b981; --icve-success-to: #34d399; --icve-success-glow: rgba(16, 185, 129, 0.35); --icve-warning-from: #f59e0b; --icve-warning-to: #fbbf24; --icve-warning-glow: rgba(245, 158, 11, 0.35); --icve-info-from: #0ea5e9; --icve-info-to: #38bdf8; --icve-info-glow: rgba(14, 165, 233, 0.35); --icve-danger-from: #ef4444; --icve-danger-to: #f87171; --icve-danger-glow: rgba(239, 68, 68, 0.35); /* 浅色主题 */ --icve-bg-base: #f8fafc; --icve-bg-elevated: #ffffff; --icve-bg-sunken: #f1f5f9; --icve-bg-glass: rgba(255, 255, 255, 0.72); --icve-bg-glass-strong: rgba(255, 255, 255, 0.88); --icve-border-subtle: rgba(148, 163, 184, 0.2); --icve-border-default: rgba(148, 163, 184, 0.35); --icve-text-primary: #0f172a; --icve-text-secondary: #475569; --icve-text-tertiary: #94a3b8; --icve-text-inverted: #ffffff; --icve-shadow-ambient: 0 8px 32px rgba(15, 23, 42, 0.08); --icve-shadow-elevated: 0 24px 48px rgba(15, 23, 42, 0.12); --icve-shadow-glow: 0 0 60px rgba(139, 92, 246, 0.15); /* 动画 */ --icve-ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); --icve-ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); --icve-duration-fast: 0.2s; --icve-duration-normal: 0.35s; --icve-duration-slow: 0.5s; } /* ==================== 基础面板样式 ==================== */ #icve-tabbed-panel { position: fixed; top: 24px; right: 24px; width: 400px; max-height: 92vh; z-index: 999999; font-family: 'Outfit', -apple-system, BlinkMacSystemFont, sans-serif; animation: icvePanelEnter 0.7s var(--icve-ease-out-expo); } @keyframes icvePanelEnter { 0% { opacity: 0; transform: translateX(80px) scale(0.92) rotateY(-8deg); filter: blur(8px); } 100% { opacity: 1; transform: translateX(0) scale(1) rotateY(0); filter: blur(0); } } .panel-container { background: var(--icve-bg-glass); backdrop-filter: blur(24px) saturate(180%); -webkit-backdrop-filter: blur(24px) saturate(180%); border-radius: 24px; border: 1px solid var(--icve-border-subtle); box-shadow: var(--icve-shadow-elevated), var(--icve-shadow-glow), inset 0 1px 1px rgba(255, 255, 255, 0.6); overflow: hidden; display: flex; flex-direction: column; max-height: 92vh; transition: all var(--icve-duration-normal) var(--icve-ease-out-expo); position: relative; } /* 面板光晕背景 */ .panel-container::before { content: ''; position: absolute; top: -50%; left: -50%; width: 200%; height: 200%; background: radial-gradient( ellipse at 30% 20%, rgba(99, 102, 241, 0.08) 0%, transparent 50% ), radial-gradient( ellipse at 70% 80%, rgba(217, 70, 239, 0.06) 0%, transparent 50% ); pointer-events: none; z-index: 0; } .panel-container:hover { box-shadow: 0 32px 64px rgba(15, 23, 42, 0.16), 0 0 80px rgba(139, 92, 246, 0.2), inset 0 1px 1px rgba(255, 255, 255, 0.6); } /* ==================== 头部样式 ==================== */ .panel-header { padding: 18px 20px; background: linear-gradient( 135deg, var(--icve-primary-from) 0%, var(--icve-primary-via) 50%, var(--icve-primary-to) 100% ); cursor: move; display: flex; justify-content: space-between; align-items: center; user-select: none; position: relative; z-index: 1; overflow: hidden; } /* 头部动态光效 */ .panel-header::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient( 90deg, transparent 0%, rgba(255, 255, 255, 0.2) 50%, transparent 100% ); animation: headerShine 4s ease-in-out infinite; } @keyframes headerShine { 0%, 100% { left: -100%; } 50% { left: 100%; } } /* 头部底部渐变阴影 */ .panel-header::after { content: ''; position: absolute; bottom: 0; left: 0; right: 0; height: 1px; background: linear-gradient( 90deg, transparent 0%, rgba(255, 255, 255, 0.4) 50%, transparent 100% ); } .panel-title { font-weight: 700; font-size: 16px; color: var(--icve-text-inverted); text-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); letter-spacing: 0.5px; position: relative; z-index: 1; display: flex; align-items: center; gap: 8px; } .panel-title::before { content: ''; width: 8px; height: 8px; background: var(--icve-text-inverted); border-radius: 50%; box-shadow: 0 0 12px rgba(255, 255, 255, 0.6); animation: titlePulse 2s ease-in-out infinite; } @keyframes titlePulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.6; transform: scale(0.8); } } .header-controls { display: flex; gap: 10px; position: relative; z-index: 1; } /* 头部控制按钮动画优化 */ .theme-toggle, .panel-toggle { background: rgba(255, 255, 255, 0.18); border: 1px solid rgba(255, 255, 255, 0.25); color: white; width: 36px; height: 36px; border-radius: 12px; cursor: pointer; font-size: 16px; transition: all var(--icve-duration-normal) var(--icve-ease-spring); display: flex; align-items: center; justify-content: center; backdrop-filter: blur(8px); position: relative; overflow: hidden; } .theme-toggle::before, .panel-toggle::before { content: ''; position: absolute; inset: 0; background: rgba(255, 255, 255, 0); transition: background var(--icve-duration-fast) ease; } .theme-toggle:hover, .panel-toggle:hover { background: rgba(255, 255, 255, 0.28); border-color: rgba(255, 255, 255, 0.4); transform: scale(1.08) rotate(6deg); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2); } .theme-toggle:hover::before, .panel-toggle:hover::before { background: rgba(255, 255, 255, 0.1); } .theme-toggle:active, .panel-toggle:active { transform: scale(0.96) rotate(0deg); transition: transform 0.1s ease; } /* ==================== 标签页导航 ==================== */ .tab-nav { display: flex; background: var(--icve-bg-sunken); padding: 8px 12px 0; gap: 4px; position: relative; z-index: 1; } .tab-btn { flex: 1; padding: 14px 16px; background: transparent; border: none; cursor: pointer; font-family: 'Outfit', sans-serif; font-size: 14px; font-weight: 600; color: var(--icve-text-tertiary); transition: all var(--icve-duration-normal) var(--icve-ease-out-expo); position: relative; border-radius: 14px 14px 0 0; letter-spacing: 0.3px; } .tab-btn:hover { color: var(--icve-primary-via); background: rgba(139, 92, 246, 0.08); } .tab-btn.active { color: var(--icve-primary-via); background: var(--icve-bg-glass-strong); box-shadow: 0 -4px 16px rgba(139, 92, 246, 0.1); } .tab-btn.active::after { content: ''; position: absolute; bottom: 0; left: 16px; right: 16px; height: 3px; background: linear-gradient( 90deg, var(--icve-primary-from), var(--icve-primary-via), var(--icve-primary-to) ); border-radius: 3px 3px 0 0; animation: tabIndicator 0.4s var(--icve-ease-spring); } @keyframes tabIndicator { from { transform: scaleX(0); opacity: 0; } to { transform: scaleX(1); opacity: 1; } } /* ==================== 标签页内容 ==================== */ .tab-content-wrapper { overflow-y: auto; max-height: calc(92vh - 140px); scrollbar-width: thin; scrollbar-color: rgba(139, 92, 246, 0.3) transparent; position: relative; z-index: 1; } .tab-content-wrapper::-webkit-scrollbar { width: 6px; } .tab-content-wrapper::-webkit-scrollbar-track { background: transparent; } .tab-content-wrapper::-webkit-scrollbar-thumb { background: linear-gradient( 180deg, var(--icve-primary-from), var(--icve-primary-to) ); border-radius: 10px; } .tab-content-wrapper::-webkit-scrollbar-thumb:hover { background: linear-gradient( 180deg, var(--icve-primary-via), var(--icve-primary-to) ); } .tab-content-wrapper.collapsed { display: none; } .tab-nav.collapsed { display: none; } .tab-pane { display: none; background: var(--icve-bg-glass-strong); animation: tabPaneFade 0.5s var(--icve-ease-out-expo); } @keyframes tabPaneFade { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } } .tab-pane.active { display: block; } .tab-inner { padding: 20px; } /* ==================== 状态卡片 - 玻璃拟态 ==================== */ /* ==================== 学习页面样式 ==================== */ .learning-status-section { background: var(--icve-bg-glass); backdrop-filter: blur(12px); border-radius: 16px; padding: 14px 16px; margin-bottom: 14px; border: 1px solid var(--icve-border-subtle); } .status-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; } .status-item { display: flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 600; color: var(--icve-text-primary); } .status-dot { width: 8px; height: 8px; border-radius: 50%; background: #94a3b8; transition: all 0.3s ease; } .status-dot.running { background: #10b981; box-shadow: 0 0 8px rgba(16, 185, 129, 0.6); animation: pulse 1.5s infinite; } .status-dot.completed { background: #8b5cf6; } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } .learning-controls { display: flex; flex-direction: column; gap: 10px; margin-bottom: 14px; } .btn-large { width: 100%; padding: 14px 20px; font-size: 15px; background: linear-gradient( 135deg, var(--icve-success-from) 0%, var(--icve-success-to) 100% ); color: white !important; box-shadow: 0 4px 16px var(--icve-success-glow); } .btn-large:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 6px 24px var(--icve-success-glow); } .btn-large:disabled { background: linear-gradient(135deg, #9ca3af, #6b7280); box-shadow: none; cursor: not-allowed; opacity: 0.7; } .btn-group { display: flex; gap: 8px; } .btn-group .btn { flex: 1; padding: 10px 12px; font-size: 13px; } .btn-outline { background: var(--icve-bg-elevated) !important; color: #374151 !important; border: 1px solid var(--icve-border-subtle) !important; } .btn-outline:hover { background: var(--icve-bg-glass) !important; border-color: var(--icve-primary-from) !important; } .btn-toggle-label { cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 4px; color: #374151 !important; } .btn-toggle-label:has(input:checked) { background: linear-gradient(135deg, var(--icve-primary-from), var(--icve-primary-to)) !important; color: white !important; border-color: transparent !important; } .settings-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } .status-card-compact { background: var(--icve-bg-glass); backdrop-filter: blur(12px); border-radius: 18px; padding: 16px; margin-bottom: 16px; border: 1px solid var(--icve-border-subtle); box-shadow: var(--icve-shadow-ambient), inset 0 1px 0 rgba(255, 255, 255, 0.5); transition: all var(--icve-duration-normal) var(--icve-ease-out-expo); position: relative; overflow: hidden; } .status-card-compact::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 1px; background: linear-gradient( 90deg, transparent, rgba(255, 255, 255, 0.8), transparent ); } /* 状态卡片hover效果优化 */ .status-card-compact:hover { transform: translateY(-2px); box-shadow: 0 12px 32px rgba(15, 23, 42, 0.1), 0 0 32px rgba(139, 92, 246, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.5); } .status-inline { display: flex; gap: 10px; margin-bottom: 14px; } .status-badge { flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px; padding: 10px 12px; background: var(--icve-bg-elevated); border-radius: 12px; border: 1px solid var(--icve-border-subtle); transition: all var(--icve-duration-normal) var(--icve-ease-spring); cursor: default; } .status-badge:hover { transform: translateY(-1px) scale(1.01); box-shadow: 0 6px 18px rgba(0, 0, 0, 0.06); border-color: var(--icve-primary-via); } .badge-icon { font-size: 16px; line-height: 1; } .badge-value { font-size: 14px; font-weight: 700; color: var(--icve-text-primary); font-family: 'JetBrains Mono', monospace; } /* ==================== 进度条 - 渐变发光 ==================== */ .progress-bar-wrapper { height: 8px; background: var(--icve-bg-sunken); border-radius: 10px; overflow: visible; box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.08); margin-bottom: 14px; position: relative; } .progress-bar { height: 100%; background: linear-gradient( 90deg, var(--icve-primary-from), var(--icve-primary-via), var(--icve-primary-to) ); width: 0%; transition: width 0.8s var(--icve-ease-out-expo); border-radius: 10px; position: relative; box-shadow: 0 0 20px var(--icve-primary-glow), 0 0 40px rgba(139, 92, 246, 0.2); } .progress-bar::before { content: attr(data-progress); position: absolute; top: 50%; transform: translateY(-50%); font-size: 11px; font-weight: 700; font-family: 'JetBrains Mono', monospace; color: var(--icve-primary-via); background: var(--icve-bg-elevated); padding: 4px 8px; border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); border: 1px solid var(--icve-border-subtle); } .progress-bar::after { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient( 90deg, transparent 0%, rgba(255, 255, 255, 0.4) 50%, transparent 100% ); animation: progressShimmer 2s ease-in-out infinite; } @keyframes progressShimmer { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } } .current-node { display: flex; align-items: center; gap: 10px; padding: 10px 14px; background: var(--icve-bg-elevated); border-radius: 10px; font-size: 12px; border: 1px solid var(--icve-border-subtle); } .node-icon { font-size: 16px; line-height: 1; } .node-text { flex: 1; color: var(--icve-text-secondary); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* ==================== 按钮系统 ==================== */ .control-buttons-group { margin-bottom: 16px; } .primary-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; } .btn { padding: 12px 16px; border: none; border-radius: 14px; cursor: pointer; font-family: 'Outfit', sans-serif; font-size: 14px; font-weight: 700; color: var(--icve-text-inverted); transition: all var(--icve-duration-normal) var(--icve-ease-spring); position: relative; overflow: hidden; letter-spacing: 0.3px; } .btn::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 50%; background: linear-gradient( 180deg, rgba(255, 255, 255, 0.2) 0%, transparent 100% ); pointer-events: none; } .btn::after { content: ''; position: absolute; top: 50%; left: 50%; width: 0; height: 0; border-radius: 50%; background: rgba(255, 255, 255, 0.3); transform: translate(-50%, -50%); transition: width 0.6s ease, height 0.6s ease; } .btn:active::after { width: 400px; height: 400px; } .btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none !important; filter: grayscale(0.3); } .btn-primary { height: 50px; font-size: 15px; } .btn-start { background: linear-gradient( 135deg, var(--icve-success-from) 0%, var(--icve-success-to) 100% ); box-shadow: 0 4px 16px var(--icve-success-glow), inset 0 1px 0 rgba(255, 255, 255, 0.2); } /* 按钮hoer效果优化 */ .btn-start:hover:not(:disabled) { transform: translateY(-2px) scale(1.01); box-shadow: 0 6px 24px var(--icve-success-glow), 0 0 32px rgba(16, 185, 129, 0.18), inset 0 1px 0 rgba(255, 255, 255, 0.2); } .btn-start:active:not(:disabled) { transform: translateY(0) scale(0.98); transition: transform 0.1s ease; } .btn-stop { background: linear-gradient( 135deg, var(--icve-warning-from) 0%, var(--icve-warning-to) 100% ); box-shadow: 0 4px 16px var(--icve-warning-glow), inset 0 1px 0 rgba(255, 255, 255, 0.2); } .btn-stop:hover:not(:disabled) { transform: translateY(-2px) scale(1.01); box-shadow: 0 6px 24px var(--icve-warning-glow), 0 0 32px rgba(245, 158, 11, 0.18), inset 0 1px 0 rgba(255, 255, 255, 0.2); } .btn-stop:active:not(:disabled) { transform: translateY(0) scale(0.98); transition: transform 0.1s ease; } .secondary-actions { display: flex; gap: 8px; } .btn-secondary { flex: 1; height: 40px; font-size: 13px; font-weight: 600; background: linear-gradient( 135deg, #64748b 0%, #475569 100% ); box-shadow: 0 3px 12px rgba(71, 85, 105, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.15); } .btn-secondary:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 4px 16px rgba(71, 85, 105, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.15); } .btn-secondary:active:not(:disabled) { transform: translateY(0); transition: transform 0.1s ease; } .btn-scan { background: linear-gradient( 135deg, var(--icve-info-from) 0%, var(--icve-info-to) 100% ); box-shadow: 0 3px 12px var(--icve-info-glow), inset 0 1px 0 rgba(255, 255, 255, 0.15); } .btn-scan:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 5px 18px var(--icve-info-glow), inset 0 1px 0 rgba(255, 255, 255, 0.15); } .btn-reset { background: linear-gradient( 135deg, var(--icve-primary-via) 0%, var(--icve-primary-to) 100% ); box-shadow: 0 3px 12px var(--icve-primary-glow), inset 0 1px 0 rgba(255, 255, 255, 0.15); } .btn-reset:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 5px 18px var(--icve-primary-glow), inset 0 1px 0 rgba(255, 255, 255, 0.15); } /* 静音切换按钮优化 */ .btn-toggle { flex: 1; height: 40px; display: flex; align-items: center; justify-content: center; gap: 6px; background: var(--icve-bg-elevated); border-radius: 12px; cursor: pointer; transition: all var(--icve-duration-normal) var(--icve-ease-spring); font-size: 13px; font-weight: 600; color: var(--icve-text-secondary); border: 2px solid var(--icve-border-default); padding: 0 12px; } .btn-toggle:hover { border-color: var(--icve-primary-via); color: var(--icve-primary-via); box-shadow: 0 3px 12px rgba(139, 92, 246, 0.12); transform: translateY(-1px); } .btn-toggle:active { transform: translateY(0); transition: transform 0.1s ease; } .btn-toggle input[type="checkbox"] { display: none; } .toggle-icon { font-size: 16px; transition: transform var(--icve-duration-normal) var(--icve-ease-spring); } .btn-toggle:hover .toggle-icon { transform: scale(1.2); } .toggle-text { font-size: 13px; } /* ==================== 设置区域 ==================== */ .settings-section { margin-bottom: 16px; padding: 16px; border-radius: 16px; background: var(--icve-bg-glass); backdrop-filter: blur(8px); border: 1px solid var(--icve-border-subtle); transition: all var(--icve-duration-normal) var(--icve-ease-out-expo); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.4); } .settings-section:hover { box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06), inset 0 1px 0 rgba(255, 255, 255, 0.4); border-color: var(--icve-border-default); } .settings-section:last-child { margin-bottom: 0; } .section-header h3 { margin: 0 0 12px 0; font-size: 14px; font-weight: 700; color: var(--icve-text-primary); letter-spacing: 0.3px; display: flex; align-items: center; gap: 8px; } .settings-grid-compact { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; } .setting-item { display: flex; flex-direction: column; gap: 8px; } .setting-label { font-size: 12px; font-weight: 600; color: var(--icve-text-tertiary); letter-spacing: 0.3px; text-transform: uppercase; } .input-with-unit { display: flex; align-items: center; background: var(--icve-bg-elevated); border-radius: 10px; padding: 4px 4px 4px 12px; border: 2px solid var(--icve-border-default); transition: all var(--icve-duration-normal) var(--icve-ease-out-expo); } .input-with-unit:focus-within { border-color: var(--icve-primary-via); box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.1); } .input-with-unit input { flex: 1; border: none; background: transparent; padding: 8px 4px; font-family: 'JetBrains Mono', monospace; font-size: 14px; font-weight: 600; color: var(--icve-text-primary); outline: none; } .input-with-unit .unit { font-size: 12px; font-weight: 600; color: var(--icve-text-tertiary); padding: 0 10px; white-space: nowrap; } .select-control, .input-control { width: 100%; padding: 10px 12px; border: 2px solid var(--icve-border-default); border-radius: 10px; background: var(--icve-bg-elevated); color: var(--icve-text-primary); font-family: 'Outfit', sans-serif; font-size: 14px; font-weight: 500; outline: none; transition: all var(--icve-duration-normal) var(--icve-ease-out-expo); cursor: pointer; } .select-control:hover, .input-control:hover { border-color: var(--icve-border-default); background: var(--icve-bg-sunken); } .select-control:focus, .input-control:focus { border-color: var(--icve-primary-via); background: var(--icve-bg-elevated); box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.1); } /* ==================== 答题页配置 ==================== */ .quick-config { display: flex; gap: 10px; margin-bottom: 16px; padding: 14px; background: var(--icve-bg-glass); backdrop-filter: blur(8px); border-radius: 14px; border: 1px solid var(--icve-border-subtle); } .config-item { flex: 1; display: flex; align-items: center; gap: 8px; } .config-item.config-ai { flex: 1.6; min-width: 0; } .config-item.config-delay { flex: 1.4; min-width: 0; } .config-item.config-submit { flex: 1; min-width: 0; } .config-label { font-size: 18px; line-height: 1; } .select-compact, .input-compact { flex: 1; min-width: 0; height: 36px; padding: 6px 10px; font-size: 13px; border-radius: 8px; } .config-ai .select-compact { font-weight: 700; color: var(--icve-primary-via); } .config-delay .input-compact { text-align: center; font-weight: 600; font-family: 'JetBrains Mono', monospace; } .input-with-unit-inline { flex: 1; display: flex; align-items: center; gap: 4px; background: var(--icve-bg-elevated); border-radius: 8px; padding: 4px 8px; border: 2px solid var(--icve-border-default); } .input-with-unit-inline input { flex: 1; border: none; background: transparent; padding: 4px; font-family: 'JetBrains Mono', monospace; font-size: 13px; font-weight: 600; outline: none; color: var(--icve-text-primary); } .input-with-unit-inline .unit { font-size: 11px; color: var(--icve-text-tertiary); font-weight: 600; } .switch-item-inline { display: flex; align-items: center; gap: 6px; cursor: pointer; } .switch-item-inline input[type="checkbox"] { width: 18px; height: 18px; cursor: pointer; accent-color: var(--icve-primary-via); } .switch-label-inline { font-size: 13px; font-weight: 600; color: var(--icve-text-secondary); cursor: pointer; white-space: nowrap; } /* ==================== API 密钥区域 ==================== */ .api-key-section { margin-bottom: 16px; padding: 16px; background: var(--icve-bg-glass); backdrop-filter: blur(8px); border-radius: 14px; border: 1px solid var(--icve-border-subtle); } .api-key-header { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; } .api-icon { font-size: 18px; line-height: 1; } .api-label { font-size: 14px; font-weight: 700; color: var(--icve-text-primary); } .api-hint { margin-left: auto; font-size: 11px; color: var(--icve-text-tertiary); } .input-api-key { width: 100%; padding: 12px 14px; font-size: 14px; font-family: 'JetBrains Mono', monospace; font-weight: 500; background: var(--icve-bg-elevated); border: 2px solid var(--icve-border-default); border-radius: 10px; transition: all var(--icve-duration-normal) var(--icve-ease-out-expo); color: var(--icve-text-primary); } .input-api-key:focus { border-color: var(--icve-primary-via); box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.12); outline: none; } .input-api-key::placeholder { color: var(--icve-text-tertiary); } /* ==================== 状态消息 ==================== */ .status-message { padding: 12px 16px; background: var(--icve-bg-glass); backdrop-filter: blur(8px); border-radius: 12px; font-size: 13px; color: var(--icve-text-secondary); text-align: center; margin-bottom: 12px; border: 1px solid var(--icve-border-subtle); transition: all var(--icve-duration-normal) var(--icve-ease-out-expo); font-weight: 500; } .status-message:hover { border-color: var(--icve-border-default); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.05); } /* ==================== 高级设置 ==================== */ .advanced-settings { margin: 12px 0; border: 2px solid var(--icve-border-default); border-radius: 12px; overflow: hidden; transition: all var(--icve-duration-normal) var(--icve-ease-out-expo); } .advanced-settings:hover { border-color: var(--icve-border-default); } .advanced-settings[open] { border-color: var(--icve-primary-via); } .advanced-settings summary { padding: 12px 16px; background: var(--icve-bg-sunken); cursor: pointer; font-size: 13px; font-weight: 600; color: var(--icve-text-secondary); user-select: none; transition: all var(--icve-duration-normal) var(--icve-ease-out-expo); list-style: none; display: flex; align-items: center; gap: 8px; } .advanced-settings summary::-webkit-details-marker { display: none; } .advanced-settings summary::after { content: '▸'; margin-left: auto; transition: transform var(--icve-duration-normal) var(--icve-ease-out-expo); } .advanced-settings[open] summary::after { transform: rotate(90deg); } .advanced-settings summary:hover { background: var(--icve-bg-elevated); color: var(--icve-primary-via); } .advanced-content { padding: 16px; display: flex; flex-direction: column; gap: 12px; background: var(--icve-bg-glass); animation: advancedSlide 0.4s var(--icve-ease-out-expo); } @keyframes advancedSlide { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: translateY(0); } } .advanced-item { display: flex; flex-direction: column; gap: 8px; } .advanced-item label { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; color: var(--icve-text-secondary); } .label-icon { font-size: 16px; line-height: 1; } .hint { font-size: 11px; color: var(--icve-text-tertiary); margin-top: 4px; font-weight: 500; letter-spacing: 0.2px; } /* ==================== 深色主题 ==================== */ #icve-tabbed-panel.dark-theme { --icve-bg-base: #0f172a; --icve-bg-elevated: #1e293b; --icve-bg-sunken: #0c1322; --icve-bg-glass: rgba(30, 41, 59, 0.8); --icve-bg-glass-strong: rgba(30, 41, 59, 0.92); --icve-border-subtle: rgba(148, 163, 184, 0.12); --icve-border-default: rgba(148, 163, 184, 0.2); --icve-text-primary: #f1f5f9; --icve-text-secondary: #94a3b8; --icve-text-tertiary: #64748b; --icve-shadow-ambient: 0 8px 32px rgba(0, 0, 0, 0.3); --icve-shadow-elevated: 0 24px 48px rgba(0, 0, 0, 0.4); --icve-shadow-glow: 0 0 60px rgba(139, 92, 246, 0.25); } #icve-tabbed-panel.dark-theme .panel-container { box-shadow: var(--icve-shadow-elevated), var(--icve-shadow-glow), inset 0 1px 1px rgba(255, 255, 255, 0.08); } #icve-tabbed-panel.dark-theme .panel-container::before { background: radial-gradient( ellipse at 30% 20%, rgba(99, 102, 241, 0.15) 0%, transparent 50% ), radial-gradient( ellipse at 70% 80%, rgba(217, 70, 239, 0.12) 0%, transparent 50% ); } #icve-tabbed-panel.dark-theme .panel-container:hover { box-shadow: 0 32px 64px rgba(0, 0, 0, 0.5), 0 0 80px rgba(139, 92, 246, 0.3), inset 0 1px 1px rgba(255, 255, 255, 0.08); } #icve-tabbed-panel.dark-theme .status-card-compact::before { background: linear-gradient( 90deg, transparent, rgba(255, 255, 255, 0.15), transparent ); } #icve-tabbed-panel.dark-theme .status-badge { background: var(--icve-bg-base); } #icve-tabbed-panel.dark-theme .progress-bar::before { background: var(--icve-bg-base); } #icve-tabbed-panel.dark-theme .tab-content-wrapper::-webkit-scrollbar-thumb { background: linear-gradient( 180deg, rgba(99, 102, 241, 0.6), rgba(217, 70, 239, 0.6) ); } #icve-tabbed-panel.dark-theme .settings-section { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08); } #icve-tabbed-panel.dark-theme .btn-toggle { background: var(--icve-bg-base); } #icve-tabbed-panel.dark-theme .input-with-unit { background: var(--icve-bg-base); } #icve-tabbed-panel.dark-theme .input-with-unit-inline { background: var(--icve-bg-base); } #icve-tabbed-panel.dark-theme .input-api-key { background: var(--icve-bg-base); } #icve-tabbed-panel.dark-theme .select-control, #icve-tabbed-panel.dark-theme .input-control { background: var(--icve-bg-base); } #icve-tabbed-panel.dark-theme .select-control:hover, #icve-tabbed-panel.dark-theme .input-control:hover { background: var(--icve-bg-elevated); } #icve-tabbed-panel.dark-theme .select-control:focus, #icve-tabbed-panel.dark-theme .input-control:focus { background: var(--icve-bg-base); } #icve-tabbed-panel.dark-theme .advanced-settings summary { background: var(--icve-bg-base); } #icve-tabbed-panel.dark-theme .advanced-settings summary:hover { background: var(--icve-bg-elevated); } /* ==================== 兼容旧样式 ==================== */ .status-card { background: var(--icve-bg-glass); backdrop-filter: blur(12px); border-radius: 18px; padding: 16px; margin-bottom: 16px; border: 1px solid var(--icve-border-subtle); box-shadow: var(--icve-shadow-ambient); } .status-row { display: flex; gap: 10px; margin-bottom: 8px; } .status-row:last-child { margin-bottom: 0; } .status-item { flex: 1; display: flex; justify-content: space-between; align-items: center; font-size: 12px; padding: 8px 12px; background: var(--icve-bg-elevated); border-radius: 10px; } .label { color: var(--icve-text-tertiary); font-weight: 600; } .value { font-weight: 700; font-size: 14px; color: var(--icve-text-primary); font-family: 'JetBrains Mono', monospace; } .value.short { max-width: 130px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .control-buttons { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-bottom: 16px; } .progress-section { background: var(--icve-bg-glass); backdrop-filter: blur(12px); border-radius: 16px; padding: 16px; border: 1px solid var(--icve-border-subtle); } .progress-label { display: flex; align-items: center; gap: 6px; margin-bottom: 10px; font-size: 13px; color: var(--icve-text-secondary); font-weight: 600; } .progress-icon { font-size: 16px; line-height: 1; } .switches { display: flex; flex-direction: column; gap: 8px; } /* ==================== 页面日志样式 ==================== */ .log-tab-container { display: flex; flex-direction: column; height: 100%; max-height: calc(92vh - 140px); } .log-container { flex: 1; overflow-y: auto; padding: 12px; background: var(--icve-bg-elevated); font-family: 'JetBrains Mono', monospace; font-size: 12px; scrollbar-width: thin; scrollbar-color: rgba(139, 92, 246, 0.3) transparent; min-height: 280px; } .log-container::-webkit-scrollbar { width: 6px; } .log-container::-webkit-scrollbar-track { background: transparent; } .log-container::-webkit-scrollbar-thumb { background: linear-gradient( 180deg, var(--icve-primary-from), var(--icve-primary-to) ); border-radius: 3px; } .log-footer { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; background: var(--icve-bg-glass); border-top: 1px solid var(--icve-border-subtle); } .log-count-text { font-size: 13px; font-weight: 600; color: var(--icve-text-secondary); } .btn-clear-log { height: 34px; padding: 0 16px; font-size: 13px; } .log-entry { display: flex; align-items: flex-start; gap: 8px; padding: 8px 0; border-bottom: 1px solid var(--icve-border-subtle); animation: logEntryEnter 0.3s ease-out; } @keyframes logEntryEnter { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } } .log-entry:last-child { border-bottom: none; } .log-time { color: var(--icve-text-tertiary); font-weight: 500; min-width: 70px; } .log-icon { font-size: 14px; line-height: 1; } .log-message { flex: 1; color: var(--icve-text-primary); word-break: break-word; line-height: 1.4; } .log-info .log-icon { color: var(--icve-info-from); } .log-success .log-icon { color: var(--icve-success-from); } .log-warn .log-icon { color: var(--icve-warning-from); } .log-error .log-icon { color: var(--icve-danger-from); } .log-info .log-message { color: var(--icve-text-primary); } .log-success .log-message { color: var(--icve-success-from); } .log-warn .log-message { color: var(--icve-warning-from); } .log-error .log-message { color: var(--icve-danger-from); font-weight: 600; } .log-placeholder { text-align: center; color: var(--icve-text-tertiary); padding: 40px 20px; font-style: italic; } .switch-item { display: flex; align-items: center; padding: 12px 14px; background: var(--icve-bg-elevated); border: 2px solid var(--icve-border-default); border-radius: 12px; cursor: pointer; transition: all var(--icve-duration-normal) var(--icve-ease-spring); } .switch-item:hover { border-color: var(--icve-primary-via); transform: translateX(4px); box-shadow: 0 4px 16px rgba(139, 92, 246, 0.15); } .switch-item input[type="checkbox"] { margin-right: 10px; width: 18px; height: 18px; cursor: pointer; accent-color: var(--icve-primary-via); } .switch-label { font-size: 14px; font-weight: 600; color: var(--icve-text-primary); } .settings-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; } .setting-row, .setting-row-full { display: flex; flex-direction: column; gap: 8px; } .setting-row-full { grid-column: span 2; } .setting-row label, .setting-row-full label { font-size: 12px; font-weight: 600; color: var(--icve-text-tertiary); text-transform: uppercase; letter-spacing: 0.3px; } .hint-info { color: var(--icve-info-from); } .hint-box { padding: 12px 14px; background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); border: 2px solid #fcd34d; border-radius: 12px; font-size: 13px; color: #92400e; margin-top: 8px; font-weight: 500; box-shadow: 0 4px 12px rgba(252, 211, 77, 0.25); } .ai-model-selector, .api-key-input, .exam-delay-setting { margin-bottom: 12px; } .model-label { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; color: var(--icve-text-primary); margin-bottom: 8px; } .select-large, .input-large { font-size: 14px; padding: 12px 14px; } .select-large { font-weight: 600; color: var(--icve-primary-via); } .input-large { font-family: 'JetBrains Mono', monospace; } .input-with-suffix { display: flex; align-items: center; gap: 10px; } .input-with-suffix .input-control { flex: 1; min-width: 0; } .input-suffix { font-size: 13px; font-weight: 600; color: var(--icve-text-tertiary); white-space: nowrap; } .question-bank-stats { margin-top: 16px; padding: 14px 16px; background: var(--icve-bg-glass); border-radius: 12px; border: 1px solid var(--icve-border-subtle); } .stats-row { display: flex; justify-content: space-between; align-items: center; padding: 6px 0; font-size: 13px; } .stats-label { color: var(--icve-text-secondary); font-weight: 600; } .stats-value { color: var(--icve-primary-via); font-weight: 700; font-size: 14px; font-family: 'JetBrains Mono', monospace; } `; document.head.appendChild(style); } function isExamNode(nodeElement) { const examButton = nodeElement.querySelector(".li_action .btn_dt"); if (examButton) { const btnText = examButton.textContent.trim(); if (btnText.includes("开始答题") || btnText.includes("答题") || btnText.includes("考试") || btnText.includes("测验")) { return true; } } return false; } function scanLearningNodes() { const nodes = document.querySelectorAll(".panelList .node"); state.learning.allNodes = []; state.learning.completedCount = 0; state.learning.examCount = 0; state.learning.totalCount = nodes.length; nodes.forEach((node, index) => { const titleElement = node.querySelector(".title"); const statusIcon = node.querySelector(".jd"); const title = titleElement ? titleElement.textContent.trim() : `节点${index + 1}`; const id = node.id; const isCompleted = statusIcon && statusIcon.classList.contains("wc") || state.learning.processedNodes.has(id); const isExam = isExamNode(node); if (isExam) { state.learning.examCount++; } state.learning.allNodes.push({ element: node, id, title, isCompleted, isExam, index }); if (isCompleted) { state.learning.completedCount++; } }); const uncompletedCount = state.learning.totalCount - state.learning.completedCount; Logger.info(`扫描完成: 共${state.learning.totalCount}个节点, 已完成${state.learning.completedCount}个, 待学习${uncompletedCount}个`); if (state.learning.examCount > 0) { Logger.info(`发现${state.learning.examCount}个考试节点(将自动跳过)`); } updateLearningStatus(); } function updateLearningStatus() { const progressText = `${state.learning.completedCount}/${state.learning.totalCount}`; const progressElement = document.getElementById("learning-progress"); if (progressElement) { if (state.learning.examCount > 0) { progressElement.textContent = progressText; progressElement.title = `跳过 ${state.learning.examCount} 个考试/测验节点`; } else { progressElement.textContent = progressText; progressElement.title = ""; } } document.getElementById("learning-processed").textContent = state.learning.processedNodes.size; if (state.learning.currentNode && state.learning.currentNode.title) { const shortTitle = state.learning.currentNode.title.length > 18 ? state.learning.currentNode.title.substring(0, 18) + "..." : state.learning.currentNode.title; document.getElementById("learning-current").textContent = shortTitle; document.getElementById("learning-current").title = state.learning.currentNode.title; } else { document.getElementById("learning-current").textContent = "无"; document.getElementById("learning-current").title = ""; } } function applyPlaybackRate() { const mediaElements = [ ...document.querySelectorAll("audio"), ...document.querySelectorAll("video") ]; mediaElements.forEach((media) => { media.playbackRate = CONFIG.learning.playbackRate; }); } function applyMuteToCurrentMedia() { const mediaElements = [ ...document.querySelectorAll("audio"), ...document.querySelectorAll("video") ]; mediaElements.forEach((media) => { media.muted = CONFIG.learning.muteMedia; }); } function resetLearning() { if (confirm("确定要清空所有已处理节点的记录吗?")) { state.learning.processedNodes.clear(); if (state.learning.completedChapters) { state.learning.completedChapters.clear(); } saveLearningProgress(); scanLearningNodes(); Logger.warn("已重置所有学习进度"); } } function updateLearningProgressText(text) { const progressText = document.getElementById("learning-progress-text"); if (progressText) { progressText.textContent = text; } } async function fetchChapterContentByAPI(chapterId) { try { const urlParams = new URLSearchParams(window.location.search); const courseInfoId = urlParams.get("courseInfoId"); const courseId = urlParams.get("courseId"); if (!courseInfoId || !courseId) { return null; } const apiUrl = `https://ai.icve.com.cn/prod-api/course/courseDesign/getCellList?courseInfoId=${courseInfoId}&courseId=${courseId}&parentId=${chapterId}`; const response = await fetch(apiUrl, { method: "GET", headers: { "Accept": "application/json, text/plain, */*", "Content-Type": "application/json" }, credentials: "include" }); if (!response.ok) { return null; } const data = await response.json(); return data; } catch (error) { return null; } } async function expandNextUncompletedSection() { updateLearningProgressText("🔍 正在查找下一个章节..."); const sections = document.querySelectorAll(".one > .draggablebox > span > .collapse-panel"); for (let section of sections) { const panelTitle = section.querySelector(".panel-title"); const panelContent = section.querySelector(".panel-content"); if (!panelTitle || !panelContent) continue; if (panelContent.style.display !== "none") { const nodes = section.querySelectorAll(".panelList .node"); if (nodes.length > 0) { const allCompleted = Array.from(nodes).every((node) => { const statusIcon = node.querySelector(".jd"); const id = node.id; const isExam = isExamNode(node); return statusIcon && statusIcon.classList.contains("wc") || state.learning.processedNodes.has(id) || isExam; }); if (allCompleted) { const chapterId = section.id; if (!state.learning.completedChapters) { state.learning.completedChapters = /* @__PURE__ */ new Set(); } state.learning.completedChapters.add(chapterId); saveLearningProgress(); continue; } else { return false; } } } else { const chapterId = section.id; if (state.learning.completedChapters && state.learning.completedChapters.has(chapterId)) { continue; } const titleText = panelTitle.textContent.trim().substring(0, 40); updateLearningProgressText(`📂 正在展开新章节:${titleText}...`); await fetchChapterContentByAPI(chapterId); await Utils.sleep(500); const arrow = panelTitle.querySelector(".jiantou"); if (arrow) { arrow.click(); await Utils.sleep(800); } await Utils.sleep(2e3); let nodes = section.querySelectorAll(".panelList .node"); let retryCount = 0; const maxRetries = 5; while (nodes.length === 0 && retryCount < maxRetries) { await Utils.sleep(1500); nodes = section.querySelectorAll(".panelList .node"); retryCount++; if (nodes.length === 0 && retryCount === 2) { const arrow2 = panelTitle.querySelector(".jiantou"); if (arrow2) { arrow2.click(); await Utils.sleep(1e3); } } } updateLearningProgressText(`✅ 章节展开成功,发现 ${nodes.length} 个节点`); Logger.info(`展开新章节: 发现${nodes.length}个节点`); if (nodes.length > 0) { return true; } else { if (!state.learning.completedChapters) { state.learning.completedChapters = /* @__PURE__ */ new Set(); } state.learning.completedChapters.add(chapterId); saveLearningProgress(); continue; } } } return false; } function getDocumentPageInfo() { const pageDiv = document.querySelector(".page"); if (!pageDiv) return null; const match = pageDiv.textContent.match(/(\d+)\s*\/\s*(\d+)/); if (match) { return { current: parseInt(match[1]), total: parseInt(match[2]) }; } return null; } function clickNextPage() { const buttons = document.querySelectorAll(".page button"); for (let btn of buttons) { const span = btn.querySelector("span"); if (span && span.textContent.includes("下一页")) { btn.click(); return true; } } return false; } function handleDocument() { const pageInfo = getDocumentPageInfo(); if (pageInfo) { state.learning.currentPage = pageInfo.current; state.learning.totalPages = pageInfo.total; const percentage = pageInfo.current / pageInfo.total * 100; const progressBar = document.getElementById("learning-progress-bar"); if (progressBar) { progressBar.style.width = `${percentage}%`; progressBar.setAttribute("data-progress", `${Math.round(percentage)}%`); } updateLearningProgressText(`文档: 第 ${pageInfo.current}/${pageInfo.total} 页`); if (pageInfo.current < pageInfo.total) { setTimeout(() => { if (clickNextPage()) { setTimeout(() => { handleDocument(); }, 2e3); } }, CONFIG.learning.documentPageInterval * 1e3); } else { updateLearningProgressText("文档已浏览完成"); Logger.success(`文档浏览完成(共${pageInfo.total}页)`); state.learning.isDocument = false; setTimeout(() => { const progressBar2 = document.getElementById("learning-progress-bar"); if (progressBar2) { progressBar2.style.width = "0%"; } }, 1e3); if (state.learning.currentNode && state.learning.currentNode.id) { state.learning.processedNodes.add(state.learning.currentNode.id); saveLearningProgress(); updateLearningStatus(); } if (state.learning.isRunning) { setTimeout(() => { goToNextNode(); }, CONFIG.learning.waitTimeAfterComplete * 1e3); } } } else { updateLearningProgressText("单页文档已浏览"); Logger.success("单页文档浏览完成"); state.learning.isDocument = false; if (state.learning.currentNode && state.learning.currentNode.id) { state.learning.processedNodes.add(state.learning.currentNode.id); saveLearningProgress(); updateLearningStatus(); } if (state.learning.isRunning) { setTimeout(() => { goToNextNode(); }, CONFIG.learning.waitTimeAfterComplete * 1e3); } } } function hideContinuePlayDialog() { const dialogs = document.querySelectorAll(".el-message-box__wrapper"); for (let dialog of dialogs) { if (dialog.style.display === "none") continue; const dialogText = (dialog.textContent || "").replace(/\s+/g, " "); if (dialogText.includes("继续播放") || dialogText.includes("是否继续") && dialogText.includes("播放")) { dialog.style.display = "none"; Logger.info('已隐藏"继续播放"提示框'); return true; } } return false; } function playMedia(mediaElements) { mediaElements.forEach((media, index) => { if (media.dataset.processed) return; media.dataset.processed = "true"; const mediaType = media.tagName.toLowerCase() === "video" ? "视频" : "音频"; media.playbackRate = CONFIG.learning.playbackRate; media.muted = CONFIG.learning.muteMedia; updateLearningProgressText(`${mediaType}播放中...`); media.addEventListener("timeupdate", () => { if (media.duration > 0) { const current = media.currentTime; const total = media.duration; const percentage = current / total * 100; const progressBar = document.getElementById("learning-progress-bar"); if (progressBar) { progressBar.style.width = `${percentage}%`; progressBar.setAttribute("data-progress", `${Math.round(percentage)}%`); } updateLearningProgressText(`${mediaType}: ${Utils.formatTime(current)} / ${Utils.formatTime(total)}`); } }); media.addEventListener("ended", () => { state.learning.mediaWatching = false; Logger.success(`${mediaType}播放完成`); if (state.learning.currentNode && state.learning.currentNode.id) { state.learning.processedNodes.add(state.learning.currentNode.id); saveLearningProgress(); updateLearningStatus(); } const progressBar = document.getElementById("learning-progress-bar"); if (progressBar) { progressBar.style.width = "0%"; } updateLearningProgressText(`${mediaType}已完成`); if (state.learning.isRunning) { setTimeout(() => { goToNextNode(); }, CONFIG.learning.waitTimeAfterComplete * 1e3); } }); state.learning.mediaWatching = true; media.play().catch((err) => { state.learning.mediaWatching = false; Logger.error("媒体播放失败: " + err.message); }); }); } function detectContentType() { const examButton = document.querySelector(".li_action .btn_dt, .btn_dt"); if (examButton) { const btnText = examButton.textContent.trim(); if (btnText.includes("开始答题") || btnText.includes("答题") || btnText.includes("考试") || btnText.includes("测验")) { updateLearningProgressText("⏭️ 检测到考试页面,已跳过"); Logger.warn("跳过考试节点"); if (state.learning.currentNode && state.learning.currentNode.id) { state.learning.processedNodes.add(state.learning.currentNode.id); saveLearningProgress(); updateLearningStatus(); } if (state.learning.isRunning) { setTimeout(() => { goToNextNode(); }, 1e3); } return; } } const mediaElements = [ ...document.querySelectorAll("audio"), ...document.querySelectorAll("video") ]; if (mediaElements.length === 0) { updateLearningProgressText("检测到文档,准备浏览..."); Logger.info("检测到文档类型内容"); state.learning.isDocument = true; setTimeout(() => { handleDocument(); }, 1e3); return; } state.learning.isDocument = false; const mediaType = mediaElements[0].tagName.toLowerCase() === "video" ? "视频" : "音频"; Logger.info(`检测到${mediaType}内容,开始播放`); playMedia(mediaElements); } async function clickNode(nodeInfo) { state.learning.currentNode = nodeInfo; updateLearningStatus(); const progressBar = document.getElementById("learning-progress-bar"); if (progressBar) { progressBar.style.width = "0%"; } updateLearningProgressText("正在加载内容..."); const shortTitle = nodeInfo.title.length > 25 ? nodeInfo.title.substring(0, 25) + "..." : nodeInfo.title; Logger.info(`开始学习: ${shortTitle}`); if (nodeInfo.element) { nodeInfo.element.click(); setTimeout(() => { detectContentType(); }, 3e3); } } async function goToNextNode() { scanLearningNodes(); const uncompletedNodes = state.learning.allNodes.filter((n) => !n.isCompleted); if (uncompletedNodes.length === 0) { updateLearningProgressText("🎯 当前章节已完成,正在查找下一章节..."); const foundNewSection = await expandNextUncompletedSection(); if (foundNewSection) { scanLearningNodes(); const newUncompletedNodes = state.learning.allNodes.filter((n) => !n.isCompleted); if (newUncompletedNodes.length > 0) { const nextNode2 = newUncompletedNodes[0]; if (nextNode2.isExam) { updateLearningProgressText(`⏭️ 跳过考试节点:${nextNode2.title.substring(0, 20)}...`); state.learning.processedNodes.add(nextNode2.id); saveLearningProgress(); updateLearningStatus(); setTimeout(() => { goToNextNode(); }, 500); return; } setTimeout(() => { clickNode(nextNode2); }, 1e3); } else { setTimeout(() => { goToNextNode(); }, 1e3); } } else { updateLearningProgressText("🎉 所有章节已完成!"); Logger.success("所有学习内容已完成!"); state.learning.isRunning = false; document.getElementById("learning-start").disabled = false; document.getElementById("learning-status").textContent = "已完成"; const statusDot = document.getElementById("learning-status-dot"); if (statusDot) { statusDot.classList.remove("running"); statusDot.classList.add("completed"); } } return; } const nextNode = uncompletedNodes[0]; if (nextNode.isExam) { updateLearningProgressText(`⏭️ 跳过考试节点:${nextNode.title.substring(0, 20)}...`); state.learning.processedNodes.add(nextNode.id); saveLearningProgress(); updateLearningStatus(); setTimeout(() => { goToNextNode(); }, 500); return; } setTimeout(() => { clickNode(nextNode); }, 1e3); } function startLearning() { if (state.learning.isRunning) return; state.learning.isRunning = true; document.getElementById("learning-start").disabled = true; document.getElementById("learning-status").textContent = "运行中"; const statusDot = document.getElementById("learning-status-dot"); if (statusDot) statusDot.classList.add("running"); setTimeout(() => { hideContinuePlayDialog(); }, 500); Logger.info("开始自动学习"); scanLearningNodes(); setTimeout(() => { goToNextNode(); }, 1e3); } function getAIConfig() { return ConfigManager.getAIConfig(CONFIG.exam.currentAI); } function getCurrentQuestion() { const questionEl = document.querySelector(".single, .multiple, .judge, .fill, .completion"); if (!questionEl) return null; const typeMap = { "single": "单选题", "multiple": "多选题", "judge": "判断题", "fill": "填空题", "completion": "填空题" }; let questionType = "未知"; for (const [cls, type] of Object.entries(typeMap)) { if (questionEl.classList.contains(cls)) { questionType = type; break; } } const titleEl = questionEl.querySelector(".single-title-content, .multiple-title-content, .judge-title-content, .fill-title-content, .completion-title-content"); const questionText = titleEl ? titleEl.textContent.trim() : ""; const options = []; const optionEls = questionEl.querySelectorAll(".ivu-radio-wrapper, .ivu-checkbox-wrapper"); optionEls.forEach((optionEl, index) => { const optionLabel = String.fromCharCode(65 + index); const optionTextEl = optionEl.querySelector("span:last-child"); const optionText = optionTextEl ? optionTextEl.textContent.trim() : ""; options.push({ label: optionLabel, text: optionText, element: optionEl }); }); let fillInputs = []; if (questionType === "填空题") { fillInputs = Array.from(questionEl.querySelectorAll('input[type="text"], textarea, .ivu-input')); } return { type: questionType, text: questionText, options, fillInputs, element: questionEl }; } function buildPrompt(question) { let prompt = ""; if (question.type === "单选题") { prompt = `这是一道单选题,请仔细分析后选择正确答案。 题目:${question.text} 选项: `; question.options.forEach((opt) => { prompt += `${opt.label}. ${opt.text} `; }); prompt += ` 请直接回答选项字母(如:A 或 B 或 C 或 D),不要有其他内容。`; } else if (question.type === "多选题") { prompt = `这是一道多选题,请仔细分析后选择所有正确答案。 题目:${question.text} 选项: `; question.options.forEach((opt) => { prompt += `${opt.label}. ${opt.text} `; }); prompt += ` 请直接回答选项字母,多个答案用逗号分隔(如:A,C,D),不要有其他内容。`; } else if (question.type === "判断题") { prompt = `这是一道判断题,请判断对错。 题目:${question.text} `; if (question.options.length > 0) { prompt += `选项: `; question.options.forEach((opt) => { prompt += `${opt.label}. ${opt.text} `; }); prompt += ` 请直接回答选项字母(如:A 或 B),不要有其他内容。`; } else { prompt += ` 请直接回答"对"或"错",不要有其他内容。`; } } else if (question.type === "填空题") { prompt = `这是一道填空题,请给出准确答案。 题目:${question.text} `; if (question.options && question.options.length > 0) { prompt += `参考选项: `; question.options.forEach((opt) => { prompt += `${opt.label}. ${opt.text} `; }); prompt += ` `; } const blankCount = question.fillInputs.length; if (blankCount > 1) { prompt += `注意:这道题有 ${blankCount} 个空需要填写。 `; prompt += `请按顺序给出所有空的答案,每个答案之间用分号(;)分隔。 例如:答案1;答案2;答案3 `; } prompt += `要求: 1. 只返回答案内容,不要有任何解释或其他文字 2. 如果有多个空,务必用分号(;)分隔 3. 答案要准确简洁`; } return prompt; } function askAI(question) { return new Promise((resolve, reject) => { const aiConfig = getAIConfig(); const prompt = buildPrompt(question); Logger.info(`正在请求AI...`); const requestBody = { model: aiConfig.model, messages: [ { role: "system", content: "你是一个专业的答题助手。你需要根据题目内容,给出准确的答案。请严格按照要求的格式返回答案。" }, { role: "user", content: prompt } ], temperature: 0.1, max_tokens: 500 }; const timeoutId = setTimeout(() => { reject(new Error("请求超时(30秒)")); }, 3e4); GM_xmlhttpRequest({ method: "POST", url: `${aiConfig.baseURL}/chat/completions`, headers: { "Content-Type": "application/json", "Authorization": `Bearer ${aiConfig.apiKey}` }, data: JSON.stringify(requestBody), timeout: 3e4, onload: function(response) { var _a; clearTimeout(timeoutId); try { if (response.status !== 200) { let errorMsg = `API错误(${response.status})`; try { const errorData = JSON.parse(response.responseText); errorMsg = ((_a = errorData.error) == null ? void 0 : _a.message) || errorData.message || errorMsg; } catch (e) { errorMsg = `API返回错误: ${response.status} ${response.statusText}`; } Logger.error("API错误:", errorMsg); reject(new Error(errorMsg)); return; } const data = JSON.parse(response.responseText); if (data.choices && data.choices.length > 0) { const answer = data.choices[0].message.content.trim(); resolve(answer); } else if (data.error) { reject(new Error(data.error.message || "API返回错误")); } else { reject(new Error("AI返回数据格式错误")); } } catch (error) { Logger.error("解析响应失败:", error); reject(new Error("解析AI返回数据失败")); } }, onerror: (err) => { clearTimeout(timeoutId); Logger.error("网络错误:", err); reject(new Error("网络请求失败")); }, ontimeout: () => { clearTimeout(timeoutId); reject(new Error("请求超时")); } }); }); } async function searchAnswer(question) { try { const aiConfig = getAIConfig(); if (!aiConfig.apiKey || aiConfig.apiKey === "") { updateExamMessage("请先配置API Key", "#ef4444"); return null; } updateExamMessage(`📡 正在使用 ${AI_PRESETS[CONFIG.exam.currentAI].name} 查询...`, "#2196F3"); const answer = await Utils.retry( () => askAI(question), 2, // 最多重试2次 1500 // 重试间隔1.5秒 ); return answer; } catch (error) { Logger.error("查询失败:", error.message); updateExamMessage("❌ 查询失败: " + error.message, "#ef4444"); return null; } } async function selectAnswer(question, answer) { if (!answer) { updateExamMessage("未找到答案,跳过此题", "#f59e0b"); return false; } try { if (question.type === "单选题" || question.type === "判断题") { const matchedOption = question.options.find((opt) => { return answer.includes(opt.label) || answer.includes(opt.text) || opt.text.includes(answer); }); if (matchedOption) { const radioInput = matchedOption.element.querySelector('input[type="radio"]'); if (radioInput) { radioInput.click(); updateExamMessage(`已选择答案:${matchedOption.label}`, "#10b981"); return true; } } } else if (question.type === "多选题") { const answerLabels = answer.match(/[A-Z]/g) || []; let selectedCount = 0; for (let i = 0; i < answerLabels.length; i++) { const label = answerLabels[i]; const matchedOption = question.options.find((opt) => opt.label === label); if (matchedOption) { let checkboxInput = matchedOption.element.querySelector('input[type="checkbox"]'); if (!checkboxInput) { checkboxInput = matchedOption.element.querySelector(".ivu-checkbox-input"); } if (!checkboxInput) { matchedOption.element.click(); selectedCount++; } else if (!checkboxInput.checked) { checkboxInput.click(); selectedCount++; } await Utils.sleep(200); } } if (selectedCount > 0) { updateExamMessage(`已选择答案:${answerLabels.join(", ")}`, "#10b981"); return true; } } else if (question.type === "填空题") { if (question.fillInputs.length > 0) { const answers = answer.split(/[;;]/).map((a) => a.trim()).filter((a) => a); let filledCount = 0; question.fillInputs.forEach((input, index) => { if (answers[index]) { input.value = answers[index]; input.dispatchEvent(new Event("input", { bubbles: true })); input.dispatchEvent(new Event("change", { bubbles: true })); input.dispatchEvent(new Event("blur", { bubbles: true })); filledCount++; } }); if (filledCount > 0) { updateExamMessage(`已填入 ${filledCount} 个答案`, "#10b981"); return true; } } } updateExamMessage("答案格式不匹配,跳过此题", "#f59e0b"); return false; } catch (error) { return false; } } function clickNextButton() { const nextBtn = Array.from(document.querySelectorAll("button")).find((btn) => btn.textContent.includes("下一题")); if (nextBtn && !nextBtn.disabled) { setTimeout(() => { nextBtn.click(); updateExamMessage("已点击下一题", "#2196F3"); }, 500); return true; } return false; } async function clickSubmitButton() { const submitBtn = Array.from(document.querySelectorAll("button")).find((btn) => btn.textContent.includes("交卷")); if (submitBtn && !submitBtn.disabled) { if (CONFIG.exam.autoSubmit) { updateExamMessage("正在自动交卷...", "#10b981"); await Utils.sleep(1e3); submitBtn.click(); await Utils.sleep(1500); const confirmed = await clickConfirmSubmit(); if (confirmed) { updateExamMessage("已自动确认提交", "#10b981"); } } else { updateExamMessage("所有题目已完成,请手动交卷", "#10b981"); } return true; } return false; } async function clickConfirmSubmit() { for (let i = 0; i < 10; i++) { let confirmBtn = Array.from(document.querySelectorAll("button")).find((btn) => btn.textContent.includes("确认提交")); if (!confirmBtn) { const footer = document.querySelector(".ivu-modal-confirm-footer"); if (footer) confirmBtn = footer.querySelector(".ivu-btn-primary"); } if (!confirmBtn) { const modal = document.querySelector(".ivu-modal-confirm"); if (modal) confirmBtn = modal.querySelector(".ivu-btn-primary"); } if (confirmBtn) { await Utils.sleep(500); confirmBtn.click(); await Utils.sleep(2e3); await clickClosePage(); return true; } await Utils.sleep(100); } return false; } async function clickClosePage() { for (let i = 0; i < 15; i++) { let closeBtn = Array.from(document.querySelectorAll("button")).find( (btn) => btn.textContent.includes("关闭页面") || btn.textContent.includes("关闭") ); if (!closeBtn) { const footer = document.querySelector(".ivu-modal-confirm-footer"); if (footer) { const primaryBtn = footer.querySelector(".ivu-btn-primary"); if (primaryBtn && (primaryBtn.textContent.includes("关闭") || primaryBtn.textContent.includes("确定"))) { closeBtn = primaryBtn; } } } if (closeBtn) { await Utils.sleep(500); closeBtn.click(); updateExamMessage("已完成并关闭页面", "#10b981"); return true; } await Utils.sleep(200); } return false; } async function answerQuestions() { while (state.exam.isRunning) { try { const question = getCurrentQuestion(); if (!question || !question.text) { const submitted = await clickSubmitButton(); if (submitted) break; await Utils.sleep(2e3); break; } state.exam.currentQuestionIndex++; updateExamProgress(); const shortQuestion = question.text.length > 50 ? question.text.substring(0, 50) + "..." : question.text; Logger.info(`【第${state.exam.currentQuestionIndex}题-${question.type}】${shortQuestion}`); if (question.options.length > 0) { const optionsText = question.options.map((opt) => `${opt.label}.${opt.text}`).join(" | "); const shortOptions = optionsText.length > 80 ? optionsText.substring(0, 80) + "..." : optionsText; Logger.info(`选项: ${shortOptions}`); } updateExamMessage(`正在处理第 ${state.exam.currentQuestionIndex} 题 (${question.type})...`, "#2196F3"); const answer = await searchAnswer(question); if (answer) { Logger.success(`AI答案: ${answer}`); await selectAnswer(question, answer); updateExamMessage(`✅ 第 ${state.exam.currentQuestionIndex} 题已完成`, "#10b981"); } else { Logger.warn(`第${state.exam.currentQuestionIndex}题未获取到答案`); updateExamMessage(`⚠️ 第 ${state.exam.currentQuestionIndex} 题未找到答案,跳过`, "#f59e0b"); } await Utils.sleep(CONFIG.exam.delay); const hasNext = clickNextButton(); if (!hasNext) { await Utils.sleep(1e3); await clickSubmitButton(); break; } await Utils.sleep(1e3); } catch (error) { Logger.error("答题出错:", error); updateExamMessage(`❌ 第 ${state.exam.currentQuestionIndex} 题出错: ${error.message}`, "#ef4444"); await Utils.sleep(2e3); const hasNext = clickNextButton(); if (!hasNext) break; await Utils.sleep(1e3); } } state.exam.isRunning = false; document.getElementById("exam-start").disabled = false; document.getElementById("exam-stop").disabled = true; Logger.info("答题完成"); } async function startExam() { if (state.exam.isRunning) return; const aiConfig = getAIConfig(); if (!aiConfig.apiKey || aiConfig.apiKey === "") { updateExamMessage("❌ 请先配置API Key", "#ef4444"); return; } state.exam.isRunning = true; state.exam.currentQuestionIndex = 0; state.exam.totalQuestions = getTotalQuestions(); document.getElementById("exam-start").disabled = true; document.getElementById("exam-stop").disabled = false; document.getElementById("exam-status").textContent = "🟢 运行中"; updateExamMessage(`开始AI答题(使用 ${AI_PRESETS[CONFIG.exam.currentAI].name})...`, "#10b981"); updateExamProgress(); await answerQuestions(); } function stopExam() { state.exam.isRunning = false; document.getElementById("exam-start").disabled = false; document.getElementById("exam-stop").disabled = true; document.getElementById("exam-status").textContent = "⏸️ 已停止"; updateExamMessage("已停止答题", "#f59e0b"); } function getTotalQuestions() { const answerCard = document.querySelector(".topic-zpx-list"); if (answerCard) { const questionSpans = answerCard.querySelectorAll(".topic-zpx-main span"); return questionSpans.length; } return 0; } function updateExamProgress() { document.getElementById("exam-progress").textContent = `${state.exam.currentQuestionIndex}/${state.exam.totalQuestions}`; const percentage = state.exam.totalQuestions > 0 ? state.exam.currentQuestionIndex / state.exam.totalQuestions * 100 : 0; const progressBar = document.getElementById("exam-progress-bar"); if (progressBar) { progressBar.style.width = `${percentage}%`; progressBar.setAttribute("data-progress", `${Math.round(percentage)}%`); } } function updateExamMessage(text, color = "#64748b") { const msg = document.getElementById("exam-message"); if (msg) { msg.textContent = text; msg.style.color = color; } } function getPageType() { const url = window.location.href; if (url.includes("/excellent-study/")) { return "learning"; } else if (url.includes("/preview-exam/")) { return "exam"; } return "all"; } function createPanel() { const panel = document.createElement("div"); panel.id = "icve-tabbed-panel"; const pageType = getPageType(); const showLearning = pageType === "learning" || pageType === "all"; const showExam = pageType === "exam" || pageType === "all"; const defaultTab = pageType === "exam" ? "exam" : "learning"; panel.innerHTML = `
🎓 智慧职教全能助手
${showLearning ? `` : ""} ${showExam ? `` : ""}
${showLearning ? `
${createLearningTab()}
` : ""} ${showExam ? `
${createExamTab()}
` : ""}
${createLogTab()}
`; addStyles(); document.body.appendChild(panel); bindEvents(); applyTheme(CONFIG.theme); restorePanelState(); switchTab(defaultTab); loadLearningProgress(); } function bindEvents() { const panel = document.getElementById("icve-tabbed-panel"); if (!panel) return; makeDraggable(); panel.addEventListener("click", Utils.debounce(handlePanelClick, 100)); panel.addEventListener("change", Utils.throttle(handlePanelChange, 300)); Logger.info("事件绑定完成"); } function handlePanelClick(e) { var _a; const target = e.target; const id = target.id || ((_a = target.closest("[id]")) == null ? void 0 : _a.id); const actionMap = { "theme-toggle": toggleTheme, "panel-toggle": togglePanel, "learning-start": startLearning, "learning-scan": scanLearningNodes, "learning-reset": resetLearning, "exam-start": startExam, "exam-stop": stopExam, "clear-page-log": () => Logger.clearPageLog() }; if (actionMap[id]) { actionMap[id](); return; } const tabBtn = target.closest(".tab-btn"); if (tabBtn == null ? void 0 : tabBtn.dataset.tab) { switchTab(tabBtn.dataset.tab); } } function handlePanelChange(e) { const target = e.target; const id = target.id; const value = target.type === "checkbox" ? target.checked : target.value; switch (id) { case "learning-playback-rate": CONFIG.learning.playbackRate = parseFloat(value); applyPlaybackRate(); saveConfig(); Logger.info(`播放倍速: ${CONFIG.learning.playbackRate}x`); break; case "learning-wait-time": CONFIG.learning.waitTimeAfterComplete = parseInt(value); saveConfig(); Logger.info(`完成等待时间: ${value}秒`); break; case "learning-doc-interval": CONFIG.learning.documentPageInterval = parseInt(value); saveConfig(); Logger.info(`文档翻页间隔: ${value}秒`); break; case "learning-expand-delay": CONFIG.learning.expandDelay = parseFloat(value); saveConfig(); Logger.info(`展开延迟: ${value}秒`); break; case "learning-mute-media": CONFIG.learning.muteMedia = value; applyMuteToCurrentMedia(); saveConfig(); const toggleIcon = document.querySelector(".btn-toggle-label .toggle-icon"); if (toggleIcon) { toggleIcon.textContent = value ? "🔇" : "🔊"; } Logger.info(`静音模式: ${value ? "开启" : "关闭"}`); break; case "exam-ai-model": CONFIG.exam.currentAI = value; const preset = AI_PRESETS[CONFIG.exam.currentAI]; const aiConfig = getAIConfig(); const apiKeyInput = document.getElementById("exam-api-key"); const apiUrlInput = document.getElementById("exam-api-url"); const modelInput = document.getElementById("exam-api-model-name"); if (apiKeyInput) { apiKeyInput.value = aiConfig.apiKey; apiKeyInput.placeholder = preset.keyPlaceholder; } if (apiUrlInput) apiUrlInput.value = aiConfig.baseURL; if (modelInput) modelInput.value = aiConfig.model; updateExamMessage(`已切换到 ${preset.name}`, "#10b981"); setTimeout(() => { updateExamMessage(`就绪(使用 ${preset.name})`, "#64748b"); }, 2e3); saveConfig(); Logger.info(`AI模型: ${preset.name}`); break; case "exam-api-key": GM_setValue(`ai_key_${CONFIG.exam.currentAI}`, value.trim()); updateExamMessage("API Key已保存", "#10b981"); setTimeout(() => { updateExamMessage(`就绪(使用 ${AI_PRESETS[CONFIG.exam.currentAI].name})`, "#64748b"); }, 2e3); Logger.info("API Key已更新"); break; case "exam-api-url": GM_setValue(`ai_baseurl_${CONFIG.exam.currentAI}`, value.trim()); updateExamMessage("API地址已保存", "#10b981"); setTimeout(() => { updateExamMessage(`就绪(使用 ${AI_PRESETS[CONFIG.exam.currentAI].name})`, "#64748b"); }, 2e3); Logger.info(`API地址已更新`); break; case "exam-api-model-name": GM_setValue(`ai_model_${CONFIG.exam.currentAI}`, value.trim()); updateExamMessage("模型名称已保存", "#10b981"); setTimeout(() => { updateExamMessage(`就绪(使用 ${AI_PRESETS[CONFIG.exam.currentAI].name})`, "#64748b"); }, 2e3); Logger.info(`模型名称: ${value.trim()}`); break; case "exam-delay": CONFIG.exam.delay = parseInt(value) * 1e3; saveConfig(); Logger.info(`答题间隔: ${value}秒`); break; case "exam-auto-submit": CONFIG.exam.autoSubmit = value; saveConfig(); Logger.info(`自动交卷: ${value ? "开启" : "关闭"}`); break; } } function switchTab(tabName) { var _a, _b; document.querySelectorAll(".tab-btn").forEach((btn) => { btn.classList.remove("active"); }); (_a = document.querySelector(`[data-tab="${tabName}"]`)) == null ? void 0 : _a.classList.add("active"); document.querySelectorAll(".tab-pane").forEach((pane) => { pane.classList.remove("active"); }); (_b = document.getElementById(`tab-${tabName}`)) == null ? void 0 : _b.classList.add("active"); CONFIG.currentTab = tabName; saveConfig(); } function toggleTheme() { CONFIG.theme = CONFIG.theme === "light" ? "dark" : "light"; applyTheme(CONFIG.theme); saveConfig(); } function applyTheme(theme) { const panel = document.getElementById("icve-tabbed-panel"); const themeBtn = document.getElementById("theme-toggle"); if (panel) { if (theme === "dark") { panel.classList.add("dark-theme"); } else { panel.classList.remove("dark-theme"); } } if (themeBtn) { themeBtn.textContent = theme === "dark" ? "☀️" : "🌙"; themeBtn.title = theme === "dark" ? "切换到浅色模式" : "切换到深色模式"; } } function togglePanel() { const wrapper = document.getElementById("tab-content-wrapper"); const tabNav = document.querySelector(".tab-nav"); const toggleBtn = document.getElementById("panel-toggle"); if (wrapper.classList.contains("collapsed")) { wrapper.classList.remove("collapsed"); if (tabNav) tabNav.classList.remove("collapsed"); toggleBtn.textContent = "−"; localStorage.setItem("icve_panel_collapsed", "false"); } else { wrapper.classList.add("collapsed"); if (tabNav) tabNav.classList.add("collapsed"); toggleBtn.textContent = "+"; localStorage.setItem("icve_panel_collapsed", "true"); } } function restorePanelState() { const isCollapsed = localStorage.getItem("icve_panel_collapsed") === "true"; if (isCollapsed) { const wrapper = document.getElementById("tab-content-wrapper"); const tabNav = document.querySelector(".tab-nav"); const toggleBtn = document.getElementById("panel-toggle"); if (wrapper) wrapper.classList.add("collapsed"); if (tabNav) tabNav.classList.add("collapsed"); if (toggleBtn) toggleBtn.textContent = "+"; } } function makeDraggable() { const panel = document.getElementById("icve-tabbed-panel"); const header = document.getElementById("panel-header"); let isDragging = false; let currentX, currentY, initialX, initialY; header.addEventListener("mousedown", (e) => { initialX = e.clientX - panel.offsetLeft; initialY = e.clientY - panel.offsetTop; isDragging = true; }); document.addEventListener("mousemove", (e) => { if (isDragging) { e.preventDefault(); currentX = e.clientX - initialX; currentY = e.clientY - initialY; panel.style.left = currentX + "px"; panel.style.top = currentY + "px"; panel.style.right = "auto"; } }); document.addEventListener("mouseup", () => { isDragging = false; }); } window.updateLogCount = function() { const logCountElement = document.getElementById("log-count"); if (logCountElement) { logCountElement.textContent = `${Logger._logs.length} 条记录`; } }; function init() { createPanel(); Logger.info("智慧职教全能助手已加载"); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => { setTimeout(init, 1e3); }); } else { setTimeout(init, 1e3); } })();