// ==UserScript== // @name 智慧职教全能助手 // @namespace http://tampermonkey.net/ // @version 2.0.1 // @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 = { /** * 延时函数 * @param ms - 延时毫秒数 */ sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }, /** * 格式化时间(秒转为 MM:SS 格式) * @param seconds - 秒数 */ 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")}`; }, /** * 防抖函数 * @param fn - 要防抖的函数 * @param delay - 延时毫秒数 */ debounce(fn, delay = 300) { let timer = null; return function(...args) { if (timer) clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; }, /** * 节流函数 * @param fn - 要节流的函数 * @param 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); } return void 0; }; }, /** * 安全获取单个 DOM 元素 * @param selector - CSS 选择器 * @param parent - 父元素 */ $(selector, parent = document) { return parent.querySelector(selector); }, /** * 安全获取多个 DOM 元素 * @param selector - CSS 选择器 * @param parent - 父元素 */ $$(selector, parent = document) { return Array.from(parent.querySelectorAll(selector)); }, /** * 带重试的异步操作 * @param fn - 要执行的异步函数 * @param maxRetries - 最大重试次数 * @param delay - 重试间隔毫秒数 */ 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); } } throw new Error("Retry failed"); }, /** * 更新进度条(带智能标签定位) * @param progressBarId - 进度条元素 ID * @param percentage - 百分比 (0-100) */ updateProgressBar(progressBarId, percentage) { const progressBar = document.getElementById(progressBarId); if (!progressBar) return; percentage = Math.max(0, Math.min(100, percentage)); const roundedPercentage = Math.round(percentage); progressBar.style.width = `${percentage}%`; progressBar.setAttribute("data-progress", `${roundedPercentage}%`); if (percentage > 70) { progressBar.classList.add("progress-label-inside"); } else { progressBar.classList.remove("progress-label-inside"); } }, /** * 重置进度条 * @param progressBarId - 进度条元素 ID */ resetProgressBar(progressBarId) { const progressBar = document.getElementById(progressBarId); if (!progressBar) return; progressBar.style.width = "0%"; progressBar.setAttribute("data-progress", "0%"); progressBar.classList.remove("progress-label-inside"); } }; 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 = { type: level, time: 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 window.updateLogCount === "function") { window.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) { if (container.firstChild) { container.removeChild(container.firstChild); } } container.scrollTop = container.scrollHeight; }, _createLogElement(logEntry) { const div = document.createElement("div"); div.className = `log-entry log-${logEntry.type}`; div.dataset.type = logEntry.type; div.dataset.message = logEntry.message.toLowerCase(); div.innerHTML = ` ${logEntry.time} ${this._getLogIcon(logEntry.type)} ${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 window.updateLogCount === "function") { window.updateLogCount(); } }, /** * 导出日志为文本 */ exportLogs() { const header = `智慧职教助手 - 日志导出 导出时间: ${(/* @__PURE__ */ new Date()).toLocaleString()} 共 ${this._logs.length} 条记录 ${"=".repeat(50)} `; const logContent = this._logs.map((log) => { const typeLabel = { info: "[信息]", success: "[成功]", warn: "[警告]", error: "[错误]" }[log.type] || "[未知]"; return `${log.time} ${typeLabel} ${log.message}`; }).join("\n"); return header + logContent; }, /** * 下载日志文件 */ downloadLogs() { const content = this.exportLogs(); const blob = new Blob([content], { type: "text/plain;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `icve-helper-logs-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); this.success("日志已导出"); }, /** * 按类型筛选日志 */ filterLogs(type) { const container = document.getElementById("page-log-container"); if (!container) return; const entries = container.querySelectorAll(".log-entry"); let visibleCount = 0; entries.forEach((entry) => { const entryType = entry.dataset.type; if (type === "all" || entryType === type) { entry.classList.remove("filtered"); visibleCount++; } else { entry.classList.add("filtered"); } }); const countEl = document.getElementById("log-count"); if (countEl) { if (type === "all") { countEl.textContent = `${this._logs.length} 条记录`; } else { countEl.textContent = `${visibleCount} / ${this._logs.length} 条记录`; } } const noResults = container.querySelector(".log-no-results"); if (visibleCount === 0 && this._logs.length > 0) { if (!noResults) { const tip = document.createElement("div"); tip.className = "log-no-results"; tip.textContent = "没有匹配的日志"; container.appendChild(tip); } } else if (noResults) { noResults.remove(); } }, /** * 搜索日志 */ searchLogs(keyword) { const container = document.getElementById("page-log-container"); if (!container) return; const entries = container.querySelectorAll(".log-entry"); const lowerKeyword = keyword.toLowerCase().trim(); entries.forEach((entry) => { const message = entry.dataset.message || ""; const isFiltered = entry.classList.contains("filtered"); if (!isFiltered) { if (!lowerKeyword || message.includes(lowerKeyword)) { entry.classList.remove("search-hidden"); if (lowerKeyword) { entry.classList.add("highlight"); } else { entry.classList.remove("highlight"); } } else { entry.classList.add("search-hidden"); entry.classList.remove("highlight"); } } }); const style = document.getElementById("log-search-style"); if (!style) { const styleEl = document.createElement("style"); styleEl.id = "log-search-style"; styleEl.textContent = ".log-entry.search-hidden { display: none; }"; document.head.appendChild(styleEl); } }, 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 = { xinliu: { name: "心流", baseURL: "https://apis.iflow.cn/v1", model: "qwen3-max", defaultKey: "", keyPlaceholder: "sk-xxx" }, openai: { name: "OpenAI", baseURL: "https://api.openai.com/v1", model: "gpt-4o-mini", defaultKey: "", keyPlaceholder: "sk-xxx" }, claude: { name: "Claude", baseURL: "https://api.anthropic.com/v1", model: "claude-3-5-sonnet-20241022", defaultKey: "", keyPlaceholder: "sk-ant-xxx" }, gemini: { name: "Google Gemini", baseURL: "https://generativelanguage.googleapis.com/v1beta", model: "gemini-2.0-flash-exp", defaultKey: "", keyPlaceholder: "AIzaSyxxx" }, deepseek: { name: "DeepSeek", baseURL: "https://api.deepseek.com/v1", model: "deepseek-chat", defaultKey: "", keyPlaceholder: "sk-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: "xinliu" } }, get(category, key) { const categoryKeys = this.keys[category]; const categoryDefaults = this.defaults[category]; const storageKey = categoryKeys == null ? void 0 : categoryKeys[key]; const defaultValue = categoryDefaults == null ? void 0 : categoryDefaults[key]; if (storageKey) { return GM_getValue(storageKey, defaultValue); } return defaultValue; }, saveAll(config) { if (config.learning) { const learningKeys = this.keys.learning; Object.keys(learningKeys).forEach((key) => { var _a; const value = (_a = config.learning) == null ? void 0 : _a[key]; if (value !== void 0) { GM_setValue(learningKeys[key], value); } }); } if (config.exam) { const examKeys = this.keys.exam; Object.keys(examKeys).forEach((key) => { var _a; const value = (_a = config.exam) == null ? void 0 : _a[key]; if (value !== void 0) { GM_setValue(examKeys[key], value); } }); } if (config.theme) { localStorage.setItem("icve_theme_mode", config.theme); } }, getAIConfig(aiType) { const preset = AI_PRESETS[aiType] || AI_PRESETS.custom; 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) }; } }; class ReactiveStateManager { constructor() { this._listeners = /* @__PURE__ */ new Map(); this._state = null; this._initialized = false; } /** * 初始化状态 */ init() { if (this._initialized && this._state) { return this._state; } const initialState = { 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 } }; this._state = this._createReactiveObject(initialState, ""); this._initialized = true; return this._state; } /** * 创建响应式对象 */ _createReactiveObject(obj, path) { if (obj === null || typeof obj !== "object" || obj instanceof Set || obj instanceof Map) { return obj; } const self = this; for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { const value = obj[key]; if (typeof value === "object" && value !== null && !(value instanceof Set) && !(value instanceof Map)) { obj[key] = this._createReactiveObject( value, path ? `${path}.${key}` : key ); } } } return new Proxy(obj, { set(target, property, value) { const oldValue = target[property]; if (oldValue === value) { return true; } if (typeof value === "object" && value !== null && !(value instanceof Set) && !(value instanceof Map)) { value = self._createReactiveObject( value, path ? `${path}.${String(property)}` : String(property) ); } target[property] = value; const fullPath = path ? `${path}.${String(property)}` : String(property); self._notify(fullPath, value, oldValue); return true; }, get(target, property) { return target[property]; } }); } /** * 通知所有相关监听器 */ _notify(path, newValue, oldValue) { const listeners = this._listeners.get(path); if (listeners) { listeners.forEach((listener) => { try { listener(path, newValue, oldValue); } catch (e) { console.error("[ReactiveState] Listener error:", e); } }); } const pathParts = path.split("."); for (let i = pathParts.length - 1; i >= 0; i--) { const parentPath = pathParts.slice(0, i).join("."); if (parentPath) { const parentListeners = this._listeners.get(parentPath + ".*"); if (parentListeners) { parentListeners.forEach((listener) => { try { listener(path, newValue, oldValue); } catch (e) { console.error("[ReactiveState] Listener error:", e); } }); } } } const globalListeners = this._listeners.get("*"); if (globalListeners) { globalListeners.forEach((listener) => { try { listener(path, newValue, oldValue); } catch (e) { console.error("[ReactiveState] Listener error:", e); } }); } } /** * 监听状态变化 */ watch(path, listener) { if (!this._listeners.has(path)) { this._listeners.set(path, /* @__PURE__ */ new Set()); } this._listeners.get(path).add(listener); return () => { const listeners = this._listeners.get(path); if (listeners) { listeners.delete(listener); if (listeners.size === 0) { this._listeners.delete(path); } } }; } /** * 批量更新状态 */ batch(updater) { const tempListeners = this._listeners; this._listeners = /* @__PURE__ */ new Map(); try { if (this._state) { updater(this._state); } } finally { this._listeners = tempListeners; this._notify("*", this._state, null); } } /** * 获取状态快照 */ getSnapshot() { return JSON.parse(JSON.stringify(this._state, (key, value) => { if (value instanceof Set) { return Array.from(value); } return value; })); } } const stateManager = new ReactiveStateManager(); const state = stateManager.init(); 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); } const CONFIG_VERSION = "1.0"; function exportConfig() { const aiConfigs = {}; Object.keys(AI_PRESETS).forEach((aiType) => { aiConfigs[aiType] = ConfigManager.getAIConfig(aiType); }); const exportData = { version: CONFIG_VERSION, exportedAt: Date.now(), learning: { ...CONFIG.learning }, exam: { ...CONFIG.exam }, theme: CONFIG.theme, aiConfigs }; return JSON.stringify(exportData, null, 2); } function downloadConfig() { const configJson = exportConfig(); const blob = new Blob([configJson], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `icve-helper-config-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); Logger.success("配置已导出"); } function importConfig(jsonString) { try { const data = JSON.parse(jsonString); if (!data.version) { return { success: false, message: "无效的配置文件格式" }; } if (data.learning) { Object.assign(CONFIG.learning, data.learning); } if (data.exam) { Object.assign(CONFIG.exam, data.exam); } if (data.theme) { CONFIG.theme = data.theme; } if (data.aiConfigs) { Object.entries(data.aiConfigs).forEach(([aiType, config]) => { if (config.apiKey) { GM_setValue(`ai_key_${aiType}`, config.apiKey); } if (config.baseURL) { GM_setValue(`ai_baseurl_${aiType}`, config.baseURL); } if (config.model) { GM_setValue(`ai_model_${aiType}`, config.model); } }); } saveConfig(); Logger.success("配置导入成功"); return { success: true, message: "配置导入成功,页面将刷新以应用更改" }; } catch (error) { Logger.error("配置导入失败:", error); return { success: false, message: "配置文件解析失败,请检查文件格式" }; } } function resetToDefault() { CONFIG.learning = { ...ConfigManager.defaults.learning }; CONFIG.exam = { ...ConfigManager.defaults.exam }; CONFIG.theme = "light"; saveConfig(); Object.keys(AI_PRESETS).forEach((aiType) => { GM_setValue(`ai_key_${aiType}`, ""); GM_setValue(`ai_baseurl_${aiType}`, AI_PRESETS[aiType].baseURL); GM_setValue(`ai_model_${aiType}`, AI_PRESETS[aiType].model); }); Logger.warn("配置已重置为默认值"); } function createFileInput(onImport) { const input = document.createElement("input"); input.type = "file"; input.accept = ".json"; input.style.display = "none"; input.addEventListener("change", (e) => { var _a; const file = (_a = e.target.files) == null ? void 0 : _a[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { var _a2; const content = (_a2 = event.target) == null ? void 0 : _a2.result; const result = importConfig(content); onImport(result); }; reader.readAsText(file); }); return input; } function createConfigManagementSection() { return `
🔧 配置管理
`; } function getConfigManagementStyles() { return ` /* 配置管理区域 */ .config-management-details { margin-top: 12px; border: 1px solid var(--border-color); border-radius: 6px; overflow: hidden; } .config-management-details summary { padding: 8px 12px; background: var(--bg-secondary); cursor: pointer; font-size: 13px; font-weight: 500; color: var(--text-secondary); transition: background 0.2s; } .config-management-details summary:hover { background: var(--bg-hover); } .config-management-details[open] summary { border-bottom: 1px solid var(--border-color); } .config-management-body { padding: 12px; display: flex; flex-direction: column; gap: 8px; } .config-action-row { display: flex; gap: 8px; } .config-action-row .btn { flex: 1; } .btn-danger { color: #ef4444 !important; border-color: #fca5a5 !important; } .btn-danger:hover { background: #fef2f2 !important; } .dark-theme .btn-danger:hover { background: rgba(239, 68, 68, 0.1) !important; } `; } function createLearningTab() { return `
停止中 📊 0/0 0
📖 等待开始...
等待开始...

⚙️ 学习配置

${createConfigManagementSection()}
`; } 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
⚙️ 高级
💡 配置完成后点击"开始"
`; } function createLogTab() { return `
暂无日志记录
`; } function setCurrentSearch(search) { } function getLogToolbarStyles() { return ` /* 日志工具栏 */ .log-toolbar { display: flex; gap: 8px; padding: 8px; background: var(--bg-secondary); border-bottom: 1px solid var(--border-color); flex-wrap: wrap; } .log-filter-group { display: flex; gap: 4px; } .log-filter-btn { padding: 4px 8px; font-size: 12px; border: 1px solid var(--border-color); background: var(--bg-primary); color: var(--text-secondary); border-radius: 4px; cursor: pointer; transition: all 0.2s; } .log-filter-btn:hover { background: var(--bg-hover); } .log-filter-btn.active { background: var(--primary-color); color: white; border-color: var(--primary-color); } .log-search-wrapper { flex: 1; min-width: 120px; position: relative; } .log-search-input { width: 100%; padding: 4px 28px 4px 8px; font-size: 12px; border: 1px solid var(--border-color); border-radius: 4px; background: var(--bg-primary); color: var(--text-primary); } .log-search-input:focus { outline: none; border-color: var(--primary-color); box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2); } .log-search-clear { position: absolute; right: 4px; top: 50%; transform: translateY(-50%); background: none; border: none; color: var(--text-secondary); cursor: pointer; padding: 2px 4px; font-size: 12px; opacity: 0; transition: opacity 0.2s; } .log-search-input:not(:placeholder-shown) + .log-search-clear { opacity: 0.6; } .log-search-clear:hover { opacity: 1; } /* 日志底部工具栏 */ .log-footer { display: flex; justify-content: space-between; align-items: center; padding: 8px; background: var(--bg-secondary); border-top: 1px solid var(--border-color); } .log-actions { display: flex; gap: 6px; } .log-actions .btn-sm { padding: 4px 12px; font-size: 12px; white-space: nowrap; } /* 日志条目过滤动画 */ .log-entry.filtered { display: none; } .log-entry.highlight { background: rgba(251, 191, 36, 0.2) !important; } /* 无匹配结果提示 */ .log-no-results { padding: 20px; text-align: center; color: var(--text-secondary); font-size: 13px; } `; } const cssVariables = ` /* ==================== 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-panel-width: 400px; --icve-panel-padding: 20px; } /* 小屏幕适配 */ @media (max-width: 480px) { :root { --icve-panel-width: calc(100vw - 32px); --icve-panel-padding: 14px; } } @media (min-width: 481px) and (max-width: 768px) { :root { --icve-panel-width: 360px; --icve-panel-padding: 16px; } } `; const baseStyles = ` /* ==================== 字体定义 ==================== */ /* 使用系统字体栈作为回退,确保国内用户体验 */ @font-face { font-family: 'Outfit'; font-style: normal; font-weight: 300 800; font-display: swap; src: local('Outfit'), url('https://fonts.loli.net/css2?family=Outfit:wght@300;400;500;600;700;800&display=swap') format('woff2'), url('https://cdn.jsdelivr.net/npm/@fontsource/outfit@5.0.8/files/outfit-latin-400-normal.woff2') format('woff2'); } @font-face { font-family: 'JetBrains Mono'; font-style: normal; font-weight: 400 600; font-display: swap; src: local('JetBrains Mono'), url('https://fonts.loli.net/css2?family=JetBrains+Mono:wght@400;500;600&display=swap') format('woff2'), url('https://cdn.jsdelivr.net/npm/@fontsource/jetbrains-mono@5.0.18/files/jetbrains-mono-latin-400-normal.woff2') format('woff2'); } /* 字体回退栈 */ .icve-font-sans { font-family: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; } .icve-font-mono { font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', 'Consolas', 'Monaco', monospace; } /* ==================== 基础面板样式 ==================== */ #icve-tabbed-panel { position: fixed; top: 24px; right: 24px; width: var(--icve-panel-width); max-width: calc(100vw - 32px); max-height: 92vh; z-index: 999999; font-family: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', 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); } } /* 小屏幕位置调整 */ @media (max-width: 480px) { #icve-tabbed-panel { top: 16px; right: 16px; max-height: 85vh; } } .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); } /* 小屏幕圆角调整 */ @media (max-width: 480px) { .panel-container { border-radius: 18px; max-height: 85vh; } } /* ==================== 头部样式 ==================== */ .panel-header { padding: 18px var(--icve-panel-padding); 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); } } /* 小屏幕标题字号 */ @media (max-width: 480px) { .panel-title { font-size: 14px; } } .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; } /* 小屏幕按钮尺寸 */ @media (max-width: 480px) { .theme-toggle, .panel-toggle { width: 32px; height: 32px; font-size: 14px; } .header-controls { gap: 8px; } } /* ==================== 标签页导航 ==================== */ .tab-nav { display: flex; background: var(--icve-bg-sunken); padding: 8px 12px 0; gap: 4px; position: relative; z-index: 1; } .tab-nav.collapsed { display: none; } .tab-btn { flex: 1; padding: 14px 16px; background: transparent; border: none; cursor: pointer; font-family: 'Outfit', -apple-system, BlinkMacSystemFont, 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; } } /* 小屏幕标签页 */ @media (max-width: 480px) { .tab-nav { padding: 6px 8px 0; } .tab-btn { padding: 10px 8px; font-size: 12px; } } /* ==================== 标签页内容 ==================== */ .tab-content-wrapper { overflow-y: auto; max-height: calc(92vh - 140px); scrollbar-width: thin; scrollbar-color: rgba(139, 92, 246, 0.3) transparent; scrollbar-gutter: stable; 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; } /* 小屏幕滚动区域 */ @media (max-width: 480px) { .tab-content-wrapper { max-height: calc(85vh - 120px); } } .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: var(--icve-panel-padding); } /* 小屏幕内边距 */ @media (max-width: 480px) { .tab-inner { padding: 14px; } } `; const componentStyles = ` /* ==================== 状态卡片 ==================== */ .status-card-compact, .learning-status-section, .exam-status-compact { 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); 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); } .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); } /* 小屏幕卡片调整 */ @media (max-width: 480px) { .status-card-compact, .learning-status-section, .exam-status-compact { padding: 12px; border-radius: 12px; margin-bottom: 12px; } } /* ==================== 状态行与状态项 ==================== */ .status-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; gap: 10px; flex-wrap: wrap; } .status-item { display: flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 600; color: var(--icve-text-primary); } /* 小屏幕状态行 */ @media (max-width: 480px) { .status-row { margin-bottom: 10px; gap: 8px; } .status-item { font-size: 12px; gap: 4px; } } /* 状态指示点 */ .status-dot { width: 8px; height: 8px; border-radius: 50%; background: #94a3b8; transition: all 0.3s ease; flex-shrink: 0; } .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; box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.2); } .status-dot.ready { background: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2); } .status-dot.error { background: #ef4444; box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.2); } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 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', 'SF Mono', 'Consolas', 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; right: -8px; top: 50%; transform: translate(100%, -50%); font-size: 11px; font-weight: 700; font-family: 'JetBrains Mono', 'SF Mono', 'Consolas', monospace; color: var(--icve-primary-via); background: var(--icve-bg-elevated); padding: 3px 8px; border-radius: 6px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); border: 1px solid var(--icve-border-subtle); white-space: nowrap; z-index: 10; opacity: 0; transition: opacity 0.3s ease; } /* 只有在有进度时才显示标签 */ .progress-bar[data-progress]:not([data-progress="0%"])::before { opacity: 1; } /* 当进度超过 70% 时,标签显示在进度条内部左侧 */ .progress-bar[style*="width: 7"], .progress-bar[style*="width: 8"], .progress-bar[style*="width: 9"], .progress-bar[style*="width: 100"] { /* 这些选择器无法精确匹配,使用 JS 来动态添加类 */ } /* 进度条内部标签样式(通过 JS 添加 .progress-label-inside 类) */ .progress-bar.progress-label-inside::before { right: auto; left: 8px; transform: translate(0, -50%); color: white; background: rgba(0, 0, 0, 0.3); border: none; backdrop-filter: blur(4px); } /* 进度条光效动画 */ .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; border-radius: 10px; } @keyframes progressShimmer { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } } /* 小屏幕进度条标签 */ @media (max-width: 480px) { .progress-bar::before { font-size: 10px; padding: 2px 6px; } } /* ==================== 当前节点显示 ==================== */ .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; flex-shrink: 0; } .node-text { flex: 1; color: var(--icve-text-secondary); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* ==================== 按钮系统 ==================== */ .btn { padding: 12px 16px; border: none; border-radius: 14px; cursor: pointer; font-family: 'Outfit', -apple-system, BlinkMacSystemFont, 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-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; } /* 开始按钮 */ .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); } .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; } /* 次要按钮 */ .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-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-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-group { display: flex; gap: 8px; } .btn-group .btn { flex: 1; padding: 10px 12px; font-size: 13px; } .primary-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; } .secondary-actions { display: flex; gap: 8px; } .control-buttons-group { margin-bottom: 16px; } /* 小屏幕按钮调整 */ @media (max-width: 480px) { .btn { padding: 10px 14px; font-size: 13px; border-radius: 12px; } .btn-primary { height: 44px; font-size: 14px; } .btn-large { padding: 12px 16px; font-size: 14px; } .btn-group { gap: 6px; } .btn-group .btn { padding: 8px 10px; font-size: 12px; } .primary-actions { gap: 10px; } } /* 切换按钮标签 */ .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; } /* 切换按钮 */ .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; } /* ==================== 输入框与选择器 ==================== */ .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', -apple-system, BlinkMacSystemFont, 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); } /* 带单位输入框 */ .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', 'SF Mono', 'Consolas', 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-mini, .input-mini { flex: 1; height: 32px; padding: 4px 8px; font-size: 13px; border-radius: 8px; } .select-mini { font-weight: 600; color: var(--icve-primary-via); } .input-mini { font-family: 'JetBrains Mono', 'SF Mono', 'Consolas', monospace; } /* 小屏幕输入框 */ @media (max-width: 480px) { .select-control, .input-control { padding: 8px 10px; font-size: 13px; } .input-with-unit { padding: 2px 2px 2px 10px; } .input-with-unit input { padding: 6px 4px; 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, .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; } /* 小屏幕设置区域 */ @media (max-width: 480px) { .settings-section { padding: 12px; border-radius: 12px; margin-bottom: 12px; } .section-header h3 { font-size: 13px; margin-bottom: 10px; } .settings-grid, .settings-grid-compact { gap: 10px; } .setting-label { font-size: 11px; } } /* 单列设置布局(超小屏幕) */ @media (max-width: 360px) { .settings-grid, .settings-grid-compact { grid-template-columns: 1fr; } } /* ==================== 状态消息 ==================== */ .status-message, .status-msg-mini { 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; 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); } .status-msg-mini { padding: 8px 10px; border-radius: 8px; font-size: 12px; } /* 小屏幕状态消息 */ @media (max-width: 480px) { .status-message { padding: 10px 12px; font-size: 12px; border-radius: 10px; } .status-msg-mini { padding: 6px 8px; font-size: 11px; } } /* ==================== 开关切换 ==================== */ .switch-toggle { position: relative; display: inline-block; width: 48px; height: 26px; } .switch-toggle input[type="checkbox"] { opacity: 0; width: 0; height: 0; } .switch-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background: #cbd5e1; border-radius: 26px; transition: all var(--icve-duration-normal) var(--icve-ease-out-expo); } .switch-slider::before { content: ''; position: absolute; height: 20px; width: 20px; left: 3px; bottom: 3px; background: white; border-radius: 50%; transition: all var(--icve-duration-normal) var(--icve-ease-spring); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); } .switch-toggle input:checked + .switch-slider { background: linear-gradient(135deg, var(--icve-primary-from), var(--icve-primary-to)); } .switch-toggle input:checked + .switch-slider::before { transform: translateX(22px); } .switch-toggle:hover .switch-slider { box-shadow: 0 0 8px rgba(139, 92, 246, 0.3); } /* 迷你开关 */ .switch-mini { position: relative; display: inline-block; width: 42px; height: 24px; } .switch-mini input[type="checkbox"] { opacity: 0; width: 0; height: 0; } .slider-mini { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background: #cbd5e1; border-radius: 24px; transition: all 0.3s; } .slider-mini::before { content: ''; position: absolute; height: 18px; width: 18px; left: 3px; bottom: 3px; background: white; border-radius: 50%; transition: all 0.3s; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); } .switch-mini input:checked + .slider-mini { background: linear-gradient(135deg, var(--icve-primary-from), var(--icve-primary-to)); } .switch-mini input:checked + .slider-mini::before { transform: translateX(18px); } `; const learningStyles = ` /* ==================== 学习控制区 ==================== */ .learning-controls { display: flex; flex-direction: column; gap: 10px; margin-bottom: 14px; } /* 小屏幕学习控制区 */ @media (max-width: 480px) { .learning-controls { gap: 8px; margin-bottom: 12px; } } `; const examStyles = ` /* ==================== 状态行 ==================== */ .status-line { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } .status-left { display: flex; align-items: center; gap: 8px; } .status-text { font-size: 13px; font-weight: 600; color: var(--icve-text-primary); } .progress-mini { font-size: 12px; font-weight: 700; color: var(--icve-text-secondary); font-family: 'JetBrains Mono', 'SF Mono', 'Consolas', monospace; } /* ==================== 配置区 ==================== */ .exam-config-compact { background: var(--icve-bg-glass); backdrop-filter: blur(8px); border-radius: 12px; padding: 10px; margin-bottom: 10px; border: 1px solid var(--icve-border-subtle); display: flex; flex-direction: column; gap: 8px; } .config-row { display: flex; align-items: center; gap: 8px; } .config-row.config-key { background: linear-gradient(135deg, rgba(16, 185, 129, 0.05), rgba(52, 211, 153, 0.05)); padding: 6px 8px; border-radius: 8px; border: 1px solid rgba(16, 185, 129, 0.15); } .row-label { font-size: 12px; font-weight: 600; color: var(--icve-text-secondary); white-space: nowrap; min-width: 80px; display: inline-flex; align-items: center; } .config-row-dual { display: flex; gap: 8px; } .config-col { flex: 1; display: flex; flex-direction: column; gap: 4px; } .row-label-sm { font-size: 11px; font-weight: 600; color: var(--icve-text-tertiary); text-transform: uppercase; letter-spacing: 0.3px; } .input-unit-mini { display: flex; align-items: center; gap: 4px; background: var(--icve-bg-elevated); border-radius: 6px; padding: 4px 6px; border: 2px solid var(--icve-border-default); height: 32px; } .input-mini-num { flex: 1; border: none; background: transparent; padding: 0; font-family: 'JetBrains Mono', 'SF Mono', 'Consolas', monospace; font-size: 13px; font-weight: 600; outline: none; color: var(--icve-text-primary); text-align: center; } .unit-sm { font-size: 11px; color: var(--icve-text-tertiary); font-weight: 600; } /* 小屏幕配置区 */ @media (max-width: 480px) { .exam-config-compact { padding: 8px; gap: 6px; } .row-label { font-size: 11px; min-width: 60px; } .config-row-dual { flex-direction: column; gap: 6px; } } /* ==================== 按钮区 ==================== */ .exam-buttons-compact { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 10px; } .exam-buttons-compact .btn { height: 42px; font-size: 14px; } .exam-action-buttons { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 16px; } .exam-action-buttons .btn { height: 50px; display: flex; align-items: center; justify-content: center; gap: 8px; } .btn-icon { font-size: 16px; line-height: 1; } .btn-text { font-size: 15px; font-weight: 700; } /* 小屏幕按钮区 */ @media (max-width: 480px) { .exam-buttons-compact { gap: 6px; } .exam-buttons-compact .btn { height: 38px; font-size: 13px; } .exam-action-buttons { gap: 8px; margin-bottom: 12px; } .exam-action-buttons .btn { height: 44px; } } /* ==================== 高级配置 ==================== */ .advanced-mini { margin-bottom: 10px; border: 1px solid var(--icve-border-default); border-radius: 10px; overflow: hidden; } .advanced-mini summary { padding: 8px 12px; background: var(--icve-bg-sunken); cursor: pointer; font-size: 12px; font-weight: 600; color: var(--icve-text-secondary); user-select: none; list-style: none; } .advanced-mini summary::-webkit-details-marker { display: none; } .advanced-mini[open] { border-color: var(--icve-primary-via); } .advanced-mini summary:hover { background: var(--icve-bg-elevated); color: var(--icve-primary-via); } .advanced-body { padding: 10px; display: flex; flex-direction: column; gap: 8px; background: var(--icve-bg-glass); } .advanced-row { display: flex; flex-direction: column; gap: 6px; } .advanced-row label { font-size: 11px; font-weight: 600; color: var(--icve-text-secondary); } /* 高级设置详情 */ .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; } /* ==================== AI 配置相关 ==================== */ .exam-ai-selector { margin-bottom: 16px; padding: 14px; background: var(--icve-bg-glass); backdrop-filter: blur(8px); border-radius: 14px; border: 1px solid var(--icve-border-subtle); transition: all var(--icve-duration-normal) var(--icve-ease-out-expo); } .exam-ai-selector:hover { border-color: var(--icve-border-default); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06); } .selector-label { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; color: var(--icve-text-secondary); margin-bottom: 10px; } .exam-api-config { margin-bottom: 16px; padding: 14px; background: linear-gradient(135deg, rgba(16, 185, 129, 0.05), rgba(52, 211, 153, 0.05)); backdrop-filter: blur(8px); border-radius: 14px; border: 1px solid rgba(16, 185, 129, 0.15); transition: all var(--icve-duration-normal) var(--icve-ease-out-expo); } .exam-api-config:hover { border-color: rgba(16, 185, 129, 0.25); box-shadow: 0 4px 16px rgba(16, 185, 129, 0.08); } .config-header { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; } .header-icon { font-size: 18px; line-height: 1; } .header-label { flex: 1; font-size: 14px; font-weight: 700; color: var(--icve-text-primary); } .required-badge { padding: 3px 8px; background: linear-gradient(135deg, #ef4444, #f87171); color: white; font-size: 11px; font-weight: 700; border-radius: 6px; text-transform: uppercase; letter-spacing: 0.5px; } /* API Key 输入 */ .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', 'SF Mono', 'Consolas', 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); } /* 小屏幕 API 配置 */ @media (max-width: 480px) { .exam-ai-selector, .exam-api-config, .api-key-section { padding: 12px; border-radius: 12px; margin-bottom: 12px; } .input-api-key { padding: 10px 12px; font-size: 13px; } } /* ==================== 设置行 ==================== */ .exam-settings-compact { margin-bottom: 16px; padding: 14px; background: var(--icve-bg-glass); backdrop-filter: blur(8px); border-radius: 14px; border: 1px solid var(--icve-border-subtle); display: flex; flex-direction: column; gap: 12px; } .setting-row { display: flex; justify-content: space-between; align-items: center; padding: 10px 12px; background: var(--icve-bg-elevated); border-radius: 10px; border: 1px solid var(--icve-border-subtle); transition: all var(--icve-duration-normal) var(--icve-ease-out-expo); } .setting-row:hover { border-color: var(--icve-primary-via); box-shadow: 0 4px 12px rgba(139, 92, 246, 0.08); } .setting-label-compact { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; color: var(--icve-text-primary); } /* AI 模型显示 */ .ai-model-display { display: flex; align-items: center; gap: 6px; padding: 6px 12px; background: linear-gradient(135deg, rgba(139, 92, 246, 0.1), rgba(217, 70, 239, 0.1)); border-radius: 10px; border: 1px solid rgba(139, 92, 246, 0.2); } .model-icon { font-size: 16px; line-height: 1; } .model-name { font-size: 13px; font-weight: 700; color: var(--icve-primary-via); } /* 进度区域 */ .progress-section { background: var(--icve-bg-elevated); border-radius: 12px; padding: 12px; border: 1px solid var(--icve-border-subtle); } .progress-info { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } .progress-label { display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; color: var(--icve-text-tertiary); text-transform: uppercase; letter-spacing: 0.5px; } .progress-icon { font-size: 16px; line-height: 1; } .progress-count { font-size: 14px; font-weight: 700; color: var(--icve-text-primary); font-family: 'JetBrains Mono', 'SF Mono', 'Consolas', monospace; } `; const logStyles = ` /* ==================== 日志标签页容器 ==================== */ .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', 'SF Mono', 'Consolas', 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; } /* 小屏幕日志容器 */ @media (max-width: 480px) { .log-tab-container { max-height: calc(85vh - 120px); } .log-container { padding: 10px; font-size: 11px; min-height: 200px; } } /* ==================== 日志底栏 ==================== */ .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; } /* 小屏幕底栏 */ @media (max-width: 480px) { .log-footer { padding: 10px 12px; } .log-count-text { font-size: 12px; } .btn-clear-log { height: 30px; padding: 0 12px; font-size: 12px; } } /* ==================== 日志条目 ==================== */ .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; flex-shrink: 0; } .log-icon { font-size: 14px; line-height: 1; flex-shrink: 0; } .log-message { flex: 1; color: var(--icve-text-primary); word-break: break-word; line-height: 1.4; } /* 小屏幕日志条目 */ @media (max-width: 480px) { .log-entry { gap: 6px; padding: 6px 0; } .log-time { min-width: 55px; font-size: 10px; } .log-icon { font-size: 12px; } } /* 日志类型颜色 */ .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; } /* 小屏幕占位符 */ @media (max-width: 480px) { .log-placeholder { padding: 30px 16px; font-size: 12px; } } `; const darkThemeStyles = ` /* ==================== 深色主题 ==================== */ #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 .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, #icve-tabbed-panel.dark-theme .input-with-unit-inline, #icve-tabbed-panel.dark-theme .input-api-key, #icve-tabbed-panel.dark-theme .input-unit-mini { 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, #icve-tabbed-panel.dark-theme .advanced-mini summary { background: var(--icve-bg-base); } #icve-tabbed-panel.dark-theme .advanced-settings summary:hover, #icve-tabbed-panel.dark-theme .advanced-mini summary:hover { background: var(--icve-bg-elevated); } /* 深色主题 - 答题页面 */ #icve-tabbed-panel.dark-theme .exam-status-compact, #icve-tabbed-panel.dark-theme .exam-config-compact { background: rgba(30, 41, 59, 0.6); } #icve-tabbed-panel.dark-theme .config-row.config-key { background: linear-gradient(135deg, rgba(16, 185, 129, 0.08), rgba(52, 211, 153, 0.08)); border-color: rgba(16, 185, 129, 0.2); } #icve-tabbed-panel.dark-theme .slider-mini { background: #475569; } #icve-tabbed-panel.dark-theme .advanced-body { background: rgba(30, 41, 59, 0.6); } #icve-tabbed-panel.dark-theme .status-msg-mini { background: rgba(30, 41, 59, 0.6); } `; const legacyStyles = ` /* ==================== 兼容旧样式 ==================== */ .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:last-child { margin-bottom: 0; } .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', 'SF Mono', 'Consolas', 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; } .switches { display: flex; flex-direction: column; gap: 8px; } .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); } .setting-row-full { display: flex; flex-direction: column; gap: 8px; 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', 'SF Mono', 'Consolas', 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', 'SF Mono', 'Consolas', monospace; } /* 快速配置(答题页旧样式兼容) */ .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); flex-wrap: wrap; } .config-item { flex: 1; display: flex; align-items: center; gap: 8px; min-width: 100px; } .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', 'SF Mono', 'Consolas', 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', 'SF Mono', 'Consolas', 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; } /* 状态概览(兼容旧版) */ .exam-status-overview { background: var(--icve-bg-glass); backdrop-filter: blur(12px); border-radius: 16px; padding: 16px; margin-bottom: 16px; border: 1px solid var(--icve-border-subtle); box-shadow: var(--icve-shadow-ambient); transition: all var(--icve-duration-normal) var(--icve-ease-out-expo); } .exam-status-overview:hover { box-shadow: 0 12px 32px rgba(15, 23, 42, 0.1); border-color: var(--icve-border-default); } .status-header-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; } .status-indicator { display: flex; align-items: center; gap: 10px; } .summary-icon { font-size: 16px; line-height: 1; } .summary-badge { margin-left: auto; padding: 3px 8px; background: rgba(139, 92, 246, 0.1); color: var(--icve-primary-via); font-size: 11px; font-weight: 700; border-radius: 6px; text-transform: uppercase; letter-spacing: 0.5px; } .message-icon { font-size: 16px; line-height: 1; margin-right: 6px; } /* 小屏幕兼容样式 */ @media (max-width: 480px) { .quick-config { padding: 10px; gap: 8px; } .config-item { min-width: 80px; } .select-compact, .input-compact { height: 32px; font-size: 12px; } } @media (max-width: 360px) { .quick-config { flex-direction: column; } .config-item { width: 100%; } } `; function addStyles() { const style = document.createElement("style"); style.id = "icve-helper-styles"; style.textContent = [ cssVariables, baseStyles, componentStyles, learningStyles, examStyles, logStyles, darkThemeStyles, legacyStyles ].join("\n"); document.head.appendChild(style); } const GUIDE_STORAGE_KEY = "icve_guide_completed"; const GUIDE_VERSION = "1.0"; const guideSteps = [ { title: "👋 欢迎使用智慧职教全能助手", content: "本助手可以帮助您自动学习课程和智能答题。让我们快速了解一下主要功能!" }, { title: "📚 自动学习功能", content: '在学习页面,点击"开始学习"按钮即可自动播放视频、浏览文档。支持调整播放倍速(最高16倍速)和静音模式。', target: "#tab-learning" }, { title: "🤖 AI智能答题", content: '在答题页面,配置您的AI API密钥后,点击"开始"即可自动答题。支持多种AI模型(心流、OpenAI、Claude等)。', target: "#tab-exam" }, { title: "📋 日志查看", content: "日志面板会记录所有操作,支持按类型筛选、搜索和导出,方便追踪学习和答题进度。", target: "#tab-log" }, { title: "⚙️ 小技巧", content: "• 拖动标题栏可移动面板位置\n• 点击右上角按钮可切换深色主题\n• 所有配置会自动保存\n• 面板位置和折叠状态也会记住" }, { title: "✅ 开始使用", content: "现在您已经了解了基本功能,开始您的学习之旅吧!如有问题请查看GitHub项目页面。" } ]; function shouldShowGuide() { try { const stored = localStorage.getItem(GUIDE_STORAGE_KEY); if (!stored) return true; const data = JSON.parse(stored); return data.version !== GUIDE_VERSION; } catch { return true; } } function markGuideCompleted() { localStorage.setItem(GUIDE_STORAGE_KEY, JSON.stringify({ version: GUIDE_VERSION, completedAt: Date.now() })); } function resetGuide() { localStorage.removeItem(GUIDE_STORAGE_KEY); } function createGuideModal() { const modal = document.createElement("div"); modal.id = "icve-guide-modal"; modal.className = "guide-modal"; modal.innerHTML = `
1 / ${guideSteps.length}

${guideSteps[0].title}

${guideSteps[0].content.replace(/\n/g, "
")}

`; return modal; } function showGuide() { var _a, _b, _c, _d; if (!shouldShowGuide()) return; const modal = createGuideModal(); document.body.appendChild(modal); let currentStep = 0; const updateStep = (step) => { currentStep = step; const stepData = guideSteps[step]; const title = modal.querySelector(".guide-title"); const text = modal.querySelector(".guide-text"); const indicator = modal.querySelector(".guide-step-indicator"); const prevBtn = modal.querySelector(".guide-prev"); const nextBtn = modal.querySelector(".guide-next"); const dots = modal.querySelectorAll(".guide-dot"); if (title) title.textContent = stepData.title; if (text) text.innerHTML = stepData.content.replace(/\n/g, "
"); if (indicator) indicator.textContent = `${step + 1} / ${guideSteps.length}`; if (prevBtn) prevBtn.disabled = step === 0; if (nextBtn) { nextBtn.textContent = step === guideSteps.length - 1 ? "完成" : "下一步"; } dots.forEach((dot, i) => { dot.classList.toggle("active", i === step); }); highlightTarget(stepData.target); }; const highlightTarget = (selector) => { document.querySelectorAll(".guide-highlight").forEach((el) => { el.classList.remove("guide-highlight"); }); if (selector) { const target = document.querySelector(selector); if (target) { target.classList.add("guide-highlight"); if (selector.startsWith("#tab-")) { const tabName = selector.replace("#tab-", ""); const tabBtn = document.querySelector(`[data-tab="${tabName}"]`); if (tabBtn) tabBtn.click(); } } } }; const closeGuide = () => { document.querySelectorAll(".guide-highlight").forEach((el) => { el.classList.remove("guide-highlight"); }); modal.remove(); markGuideCompleted(); }; (_a = modal.querySelector(".guide-close")) == null ? void 0 : _a.addEventListener("click", closeGuide); (_b = modal.querySelector(".guide-overlay")) == null ? void 0 : _b.addEventListener("click", closeGuide); (_c = modal.querySelector(".guide-prev")) == null ? void 0 : _c.addEventListener("click", () => { if (currentStep > 0) updateStep(currentStep - 1); }); (_d = modal.querySelector(".guide-next")) == null ? void 0 : _d.addEventListener("click", () => { if (currentStep < guideSteps.length - 1) { updateStep(currentStep + 1); } else { closeGuide(); } }); modal.querySelectorAll(".guide-dot").forEach((dot) => { dot.addEventListener("click", (e) => { const step = parseInt(e.target.dataset.step || "0"); updateStep(step); }); }); const handleKeydown = (e) => { if (e.key === "Escape") { closeGuide(); } else if (e.key === "ArrowRight" || e.key === "Enter") { if (currentStep < guideSteps.length - 1) { updateStep(currentStep + 1); } else { closeGuide(); } } else if (e.key === "ArrowLeft") { if (currentStep > 0) updateStep(currentStep - 1); } }; document.addEventListener("keydown", handleKeydown); const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.removedNodes.forEach((node) => { if (node === modal) { document.removeEventListener("keydown", handleKeydown); observer.disconnect(); } }); }); }); observer.observe(document.body, { childList: true }); } function getGuideStyles() { return ` /* 引导模态框 */ .guide-modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 100001; display: flex; align-items: center; justify-content: center; } .guide-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(2px); } .guide-content { position: relative; background: white; border-radius: 12px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); width: 90%; max-width: 420px; overflow: hidden; animation: guideSlideIn 0.3s ease; } @keyframes guideSlideIn { from { opacity: 0; transform: translateY(-20px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } } .guide-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; } .guide-step-indicator { font-size: 13px; opacity: 0.9; } .guide-close { background: none; border: none; color: white; font-size: 18px; cursor: pointer; opacity: 0.8; transition: opacity 0.2s; padding: 4px 8px; } .guide-close:hover { opacity: 1; } .guide-body { padding: 24px; } .guide-title { margin: 0 0 12px 0; font-size: 18px; color: #1f2937; } .guide-text { margin: 0; font-size: 14px; color: #4b5563; line-height: 1.6; } .guide-footer { display: flex; justify-content: space-between; align-items: center; padding: 16px 24px; background: #f9fafb; border-top: 1px solid #e5e7eb; } .guide-dots { display: flex; gap: 8px; } .guide-dot { width: 8px; height: 8px; border-radius: 50%; background: #d1d5db; cursor: pointer; transition: all 0.2s; } .guide-dot:hover { background: #9ca3af; } .guide-dot.active { background: #667eea; transform: scale(1.2); } .guide-prev:disabled { opacity: 0.5; cursor: not-allowed; } /* 高亮目标元素 */ .guide-highlight { position: relative; z-index: 100000; box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.5), 0 0 20px rgba(102, 126, 234, 0.3); border-radius: 8px; } /* 深色主题适配 */ .dark-theme .guide-content { background: #1e293b; } .dark-theme .guide-title { color: #f1f5f9; } .dark-theme .guide-text { color: #94a3b8; } .dark-theme .guide-footer { background: #0f172a; border-top-color: #334155; } .dark-theme .guide-dot { background: #475569; } .dark-theme .guide-dot.active { background: #818cf8; } `; } function showConfirmDialog(options) { return new Promise((resolve) => { var _a, _b; const dialog = document.createElement("div"); dialog.className = "confirm-dialog-overlay"; dialog.innerHTML = `

${options.title}

${options.message}

`; const close = (result) => { dialog.classList.add("closing"); setTimeout(() => { dialog.remove(); resolve(result); }, 200); }; (_a = dialog.querySelector(".confirm-dialog-cancel")) == null ? void 0 : _a.addEventListener("click", () => close(false)); (_b = dialog.querySelector(".confirm-dialog-confirm")) == null ? void 0 : _b.addEventListener("click", () => close(true)); dialog.addEventListener("click", (e) => { if (e.target === dialog) close(false); }); const handleEsc = (e) => { if (e.key === "Escape") { close(false); document.removeEventListener("keydown", handleEsc); } }; document.addEventListener("keydown", handleEsc); document.body.appendChild(dialog); requestAnimationFrame(() => dialog.classList.add("visible")); }); } function showToast(message, type = "info", duration = 3e3) { const toast = document.createElement("div"); toast.className = `toast toast-${type}`; const icons = { success: "✅", error: "❌", info: "ℹ️", warning: "⚠️" }; toast.innerHTML = ` ${icons[type]} ${message} `; let container = document.getElementById("toast-container"); if (!container) { container = document.createElement("div"); container.id = "toast-container"; document.body.appendChild(container); } container.appendChild(toast); requestAnimationFrame(() => toast.classList.add("visible")); setTimeout(() => { toast.classList.remove("visible"); setTimeout(() => toast.remove(), 300); }, duration); } function getUIUtilsStyles() { return ` /* 按钮加载状态 */ .btn-loading { position: relative; pointer-events: none; } .btn-spinner { display: inline-block; width: 12px; height: 12px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: btnSpin 0.6s linear infinite; vertical-align: middle; margin-right: 4px; } @keyframes btnSpin { to { transform: rotate(360deg); } } /* 确认对话框 */ .confirm-dialog-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 100002; opacity: 0; transition: opacity 0.2s; } .confirm-dialog-overlay.visible { opacity: 1; } .confirm-dialog-overlay.closing { opacity: 0; } .confirm-dialog { background: white; border-radius: 8px; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2); width: 90%; max-width: 360px; transform: scale(0.95); transition: transform 0.2s; } .confirm-dialog-overlay.visible .confirm-dialog { transform: scale(1); } .confirm-dialog-header { padding: 16px; border-bottom: 1px solid #e5e7eb; } .confirm-dialog-title { margin: 0; font-size: 16px; font-weight: 600; color: #1f2937; } .confirm-dialog-body { padding: 16px; } .confirm-dialog-message { margin: 0; font-size: 14px; color: #4b5563; line-height: 1.5; } .confirm-dialog-footer { padding: 12px 16px; background: #f9fafb; border-top: 1px solid #e5e7eb; border-radius: 0 0 8px 8px; display: flex; justify-content: flex-end; gap: 8px; } /* Toast 提示 */ #toast-container { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); z-index: 100003; display: flex; flex-direction: column; gap: 8px; pointer-events: none; } .toast { background: white; padding: 10px 16px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); display: flex; align-items: center; gap: 8px; opacity: 0; transform: translateY(-10px); transition: all 0.3s ease; pointer-events: auto; } .toast.visible { opacity: 1; transform: translateY(0); } .toast-icon { font-size: 16px; } .toast-message { font-size: 13px; color: #1f2937; } .toast-success { border-left: 3px solid #10b981; } .toast-error { border-left: 3px solid #ef4444; } .toast-info { border-left: 3px solid #3b82f6; } .toast-warning { border-left: 3px solid #f59e0b; } /* 深色主题适配 */ .dark-theme .confirm-dialog { background: #1e293b; } .dark-theme .confirm-dialog-header { border-bottom-color: #334155; } .dark-theme .confirm-dialog-title { color: #f1f5f9; } .dark-theme .confirm-dialog-message { color: #94a3b8; } .dark-theme .confirm-dialog-footer { background: #0f172a; border-top-color: #334155; } .dark-theme .toast { background: #1e293b; } .dark-theme .toast-message { color: #f1f5f9; } `; } const DOMCache = { _cache: /* @__PURE__ */ new Map(), _idCache: /* @__PURE__ */ new Map(), _maxAge: 5e3, _debug: false, /** * 通过选择器获取元素(带缓存) */ get(selector, forceRefresh = false) { const now = Date.now(); const cached = this._cache.get(selector); if (!forceRefresh && cached && now - cached.time < this._maxAge) { if (this._debug) { console.log(`[DOMCache] Hit: ${selector}`); } if (cached.element && document.contains(cached.element)) { return cached.element; } } const element = document.querySelector(selector); this._cache.set(selector, { element, time: now }); if (this._debug) { console.log(`[DOMCache] Miss: ${selector}`, element ? "found" : "not found"); } return element; }, /** * 通过 ID 获取元素(永久缓存) */ getById(id, forceRefresh = false) { if (!forceRefresh && this._idCache.has(id)) { const cached = this._idCache.get(id); if (document.contains(cached)) { return cached; } } const element = document.getElementById(id); if (element) { this._idCache.set(id, element); } else { this._idCache.delete(id); } return element; }, /** * 批量获取多个元素 */ getMultiple(selectors) { const result = {}; for (const [name, selector] of Object.entries(selectors)) { result[name] = selector.startsWith("#") ? this.getById(selector.slice(1)) : this.get(selector); } return result; }, /** * 通过选择器获取所有匹配元素 */ getAll(selector) { return Array.from(document.querySelectorAll(selector)); }, /** * 安全设置元素文本内容 */ setText(id, text) { const element = this.getById(id); if (element) { element.textContent = text; return true; } return false; }, /** * 安全设置元素 HTML 内容 */ setHTML(id, html) { const element = this.getById(id); if (element) { element.innerHTML = html; return true; } return false; }, /** * 安全设置元素样式 */ setStyle(id, styles) { const element = this.getById(id); if (element) { Object.assign(element.style, styles); return true; } return false; }, /** * 安全切换元素类名 */ toggleClass(id, className, force) { const element = this.getById(id); if (element) { element.classList.toggle(className, force); return true; } return false; }, /** * 安全设置元素属性 */ setAttribute(id, attr, value) { const element = this.getById(id); if (element) { element.setAttribute(attr, value); return true; } return false; }, /** * 安全设置元素禁用状态 */ setDisabled(id, disabled) { const element = this.getById(id); if (element && "disabled" in element) { element.disabled = disabled; return true; } return false; }, /** * 清除指定选择器的缓存 */ invalidate(selector) { this._cache.delete(selector); }, /** * 清除指定 ID 的缓存 */ invalidateById(id) { this._idCache.delete(id); }, /** * 清除所有缓存 */ clear() { this._cache.clear(); this._idCache.clear(); }, /** * 清除过期缓存 */ cleanup() { const now = Date.now(); for (const [selector, cached] of this._cache.entries()) { if (now - cached.time >= this._maxAge) { this._cache.delete(selector); } } }, /** * 设置缓存过期时间 */ setMaxAge(ms) { this._maxAge = ms; }, /** * 启用或禁用调试模式 */ setDebug(enabled) { this._debug = enabled; }, /** * 获取缓存统计信息 */ getStats() { return { selectorCacheSize: this._cache.size, idCacheSize: this._idCache.size }; } }; setInterval(() => { DOMCache.cleanup(); }, 3e4); const ErrorType = { NETWORK: "NETWORK", API: "API", PARSE: "PARSE", DOM: "DOM", CONFIG: "CONFIG", UNKNOWN: "UNKNOWN" }; class AppError extends Error { constructor(message, type = ErrorType.UNKNOWN, cause = null, context = {}) { super(message); this.name = "AppError"; this.type = type; this.cause = cause; this.context = context; this.timestamp = (/* @__PURE__ */ new Date()).toISOString(); } /** * 获取用户友好的错误消息 */ getUserMessage() { const messages = { [ErrorType.NETWORK]: "网络连接失败,请检查网络后重试", [ErrorType.API]: "API 请求失败,请检查配置", [ErrorType.PARSE]: "数据解析失败", [ErrorType.DOM]: "页面元素加载异常,请刷新页面", [ErrorType.CONFIG]: "配置错误,请检查设置", [ErrorType.TIMEOUT]: "操作超时,请重试", [ErrorType.VALIDATION]: "输入验证失败", [ErrorType.UNKNOWN]: "发生未知错误" }; return messages[this.type] || this.message; } } const ErrorHandler = { /** * 处理错误并记录日志 */ handle(error, context = "", silent = false) { const appError = this.normalize(error, context); Logger.error(`[${appError.type}] ${context ? context + ": " : ""}${appError.message}`); if (this._isDebugMode()) { console.error("[ICVE Helper Error]", { type: appError.type, message: appError.message, context: appError.context, cause: appError.cause, stack: appError.stack }); } if (!silent) { this._showUserNotification(appError); } return appError; }, /** * 将普通错误转换为 AppError */ normalize(error, context = "") { if (error instanceof AppError) { return error; } let type = ErrorType.UNKNOWN; let message = error.message || "未知错误"; if (error.name === "TypeError" && message.includes("fetch")) { type = ErrorType.NETWORK; message = "网络请求失败"; } else if (error.name === "SyntaxError" || message.includes("JSON")) { type = ErrorType.PARSE; message = "JSON 解析失败"; } else if (message.includes("timeout") || message.includes("超时")) { type = ErrorType.TIMEOUT; } else if (message.includes("API") || message.includes("401") || message.includes("403")) { type = ErrorType.API; } else if (error.name === "TypeError" && (message.includes("null") || message.includes("undefined"))) { type = ErrorType.DOM; } return new AppError(message, type, error, { originalContext: context }); }, /** * 创建 API 错误 */ createAPIError(status, message = "") { const statusMessages = { 400: "请求参数错误", 401: "API Key 无效或已过期", 403: "没有访问权限", 404: "API 地址不存在", 429: "请求过于频繁,请稍后重试", 500: "API 服务器内部错误", 502: "API 网关错误", 503: "API 服务暂时不可用" }; const errorMessage = message || statusMessages[status] || `API 错误 (${status})`; return new AppError(errorMessage, ErrorType.API, null, { status }); }, /** * 创建网络错误 */ createNetworkError(message = "网络连接失败") { return new AppError(message, ErrorType.NETWORK); }, /** * 创建超时错误 */ createTimeoutError(timeout) { const message = timeout ? `请求超时(${timeout / 1e3}秒)` : "请求超时"; return new AppError(message, ErrorType.TIMEOUT, null, { timeout }); }, /** * 安全执行函数,捕获错误 */ safeExecute(fn, defaultValue = null, context = "") { try { return fn(); } catch (error) { this.handle(error, context, true); return defaultValue; } }, /** * 安全执行异步函数 */ async safeExecuteAsync(fn, defaultValue = null, context = "") { try { return await fn(); } catch (error) { this.handle(error, context, true); return defaultValue; } }, /** * 检查是否是调试模式 */ _isDebugMode() { return localStorage.getItem("icve_debug_mode") === "true"; }, /** * 显示用户通知 */ _showUserNotification(error) { const examMessage = document.getElementById("exam-message"); const learningProgressText = document.getElementById("learning-progress-text"); const userMessage = `❌ ${error.getUserMessage()}`; if (examMessage) { examMessage.textContent = userMessage; examMessage.style.color = "#ef4444"; } if (learningProgressText) { learningProgressText.textContent = userMessage; } } }; const MEDIA_PROGRESS_THROTTLE = 500; function isExamNode(nodeElement) { var _a; const examButton = nodeElement.querySelector(".li_action .btn_dt"); if (examButton) { const btnText = ((_a = examButton.textContent) == null ? void 0 : _a.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) => { var _a; const nodeElement = node; const titleElement = nodeElement.querySelector(".title"); const statusIcon = nodeElement.querySelector(".jd"); const title = titleElement ? ((_a = titleElement.textContent) == null ? void 0 : _a.trim()) || `节点${index + 1}` : `节点${index + 1}`; const id = nodeElement.id; const isCompleted = statusIcon && statusIcon.classList.contains("wc") || state.learning.processedNodes.has(id); const isExam = isExamNode(nodeElement); if (isExam) { state.learning.examCount++; } state.learning.allNodes.push({ element: nodeElement, 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 = DOMCache.getById("learning-progress"); if (progressElement) { progressElement.textContent = progressText; progressElement.title = state.learning.examCount > 0 ? `跳过 ${state.learning.examCount} 个考试/测验节点` : ""; } DOMCache.setText("learning-processed", String(state.learning.processedNodes.size)); const currentElement = DOMCache.getById("learning-current"); if (currentElement) { 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; currentElement.textContent = shortTitle; currentElement.title = state.learning.currentNode.title; } else { currentElement.textContent = "无"; currentElement.title = ""; } } } function applyPlaybackRate() { const mediaElements = [ ...Array.from(document.querySelectorAll("audio")), ...Array.from(document.querySelectorAll("video")) ]; mediaElements.forEach((media) => { media.playbackRate = CONFIG.learning.playbackRate; }); } function applyMuteToCurrentMedia() { const mediaElements = [ ...Array.from(document.querySelectorAll("audio")), ...Array.from(document.querySelectorAll("video")) ]; mediaElements.forEach((media) => { media.muted = CONFIG.learning.muteMedia; }); } function resetLearning() { 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) { Logger.warn("无法获取课程参数,跳过 API 预加载"); 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) { Logger.warn(`章节内容 API 返回 ${response.status},将使用点击方式展开`); return null; } const data = await response.json(); return data; } catch (error) { ErrorHandler.handle(error, "获取章节内容", true); return null; } } async function expandNextUncompletedSection() { var _a; updateLearningProgressText("🔍 正在查找下一个章节..."); const sections = document.querySelectorAll(".one > .draggablebox > span > .collapse-panel"); for (const section of Array.from(sections)) { const sectionElement = section; const panelTitle = sectionElement.querySelector(".panel-title"); const panelContent = sectionElement.querySelector(".panel-content"); if (!panelTitle || !panelContent) continue; if (panelContent.style.display !== "none") { const nodes = sectionElement.querySelectorAll(".panelList .node"); if (nodes.length > 0) { const allCompleted = Array.from(nodes).every((node) => { const nodeElement = node; const statusIcon = nodeElement.querySelector(".jd"); const id = nodeElement.id; const isExam = isExamNode(nodeElement); return statusIcon && statusIcon.classList.contains("wc") || state.learning.processedNodes.has(id) || isExam; }); if (allCompleted) { const chapterId = sectionElement.id; if (!state.learning.completedChapters) { state.learning.completedChapters = /* @__PURE__ */ new Set(); } state.learning.completedChapters.add(chapterId); saveLearningProgress(); continue; } else { return false; } } } else { const chapterId = sectionElement.id; if (state.learning.completedChapters && state.learning.completedChapters.has(chapterId)) { continue; } const titleText = (((_a = panelTitle.textContent) == null ? void 0 : _a.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 = sectionElement.querySelectorAll(".panelList .node"); let retryCount = 0; const maxRetries = 5; while (nodes.length === 0 && retryCount < maxRetries) { await Utils.sleep(1500); nodes = sectionElement.querySelectorAll(".panelList .node"); retryCount++; if (nodes.length === 0 && retryCount === 2) { const retryArrow = panelTitle.querySelector(".jiantou"); if (retryArrow) { retryArrow.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() { var _a; const pageDiv = document.querySelector(".page"); if (!pageDiv) return null; const match = (_a = pageDiv.textContent) == null ? void 0 : _a.match(/(\d+)\s*\/\s*(\d+)/); if (match) { return { current: parseInt(match[1]), total: parseInt(match[2]) }; } return null; } function clickNextPage() { var _a; const buttons = document.querySelectorAll(".page button"); for (const btn of Array.from(buttons)) { const span = btn.querySelector("span"); if (span && ((_a = span.textContent) == null ? void 0 : _a.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; Utils.updateProgressBar("learning-progress-bar", 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(() => { Utils.resetProgressBar("learning-progress-bar"); }, 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 (const dialog of Array.from(dialogs)) { const dialogElement = dialog; if (dialogElement.style.display === "none") continue; const dialogText = (dialogElement.textContent || "").replace(/\s+/g, " "); if (dialogText.includes("继续播放") || dialogText.includes("是否继续") && dialogText.includes("播放")) { dialogElement.style.display = "none"; Logger.info('已隐藏"继续播放"提示框'); return true; } } return false; } function playMedia(mediaElements) { mediaElements.forEach((media) => { 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}播放中...`); let lastUpdateTime = 0; const throttledProgressUpdate = () => { const now = Date.now(); if (now - lastUpdateTime < MEDIA_PROGRESS_THROTTLE) return; lastUpdateTime = now; if (media.duration > 0) { const current = media.currentTime; const total = media.duration; const percentage = current / total * 100; Utils.updateProgressBar("learning-progress-bar", percentage); updateLearningProgressText(`${mediaType}: ${Utils.formatTime(current)} / ${Utils.formatTime(total)}`); } }; media.addEventListener("timeupdate", throttledProgressUpdate); 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(); } Utils.resetProgressBar("learning-progress-bar"); 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() { var _a; const examButton = document.querySelector(".li_action .btn_dt, .btn_dt"); if (examButton) { const btnText = ((_a = examButton.textContent) == null ? void 0 : _a.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 = [ ...Array.from(document.querySelectorAll("audio")), ...Array.from(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); } function safeClick(element) { try { const clickEvent = new MouseEvent("click", { bubbles: true, cancelable: true, view: window }); element.dispatchEvent(clickEvent); return true; } catch { try { element.click(); return true; } catch { return false; } } } async function clickNode(nodeInfo) { state.learning.currentNode = nodeInfo; updateLearningStatus(); Utils.resetProgressBar("learning-progress-bar"); updateLearningProgressText("正在加载内容..."); const shortTitle = nodeInfo.title.length > 25 ? nodeInfo.title.substring(0, 25) + "..." : nodeInfo.title; Logger.info(`开始学习: ${shortTitle}`); let targetElement = null; if (nodeInfo.id) { targetElement = document.getElementById(nodeInfo.id); } if (!targetElement && nodeInfo.element) { try { if (nodeInfo.element.isConnected) { targetElement = nodeInfo.element; } } catch { } } if (targetElement) { if (safeClick(targetElement)) { setTimeout(() => { detectContentType(); }, 3e3); return; } } Logger.warn("无法点击节点,重新扫描"); scanLearningNodes(); setTimeout(() => { goToNextNode(); }, 1e3); } 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; const startBtn = document.getElementById("learning-start"); if (startBtn) startBtn.disabled = false; const statusEl = document.getElementById("learning-status"); if (statusEl) statusEl.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; const startBtn = document.getElementById("learning-start"); if (startBtn) startBtn.disabled = true; const statusEl = document.getElementById("learning-status"); if (statusEl) statusEl.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() { var _a; 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 ? ((_a = titleEl.textContent) == null ? void 0 : _a.trim()) || "" : ""; const options = []; const optionEls = questionEl.querySelectorAll(".ivu-radio-wrapper, .ivu-checkbox-wrapper"); optionEls.forEach((optionEl, index) => { var _a2; const optionLabel = String.fromCharCode(65 + index); const optionTextEl = optionEl.querySelector("span:last-child"); const optionText = optionTextEl ? ((_a2 = optionTextEl.textContent) == null ? void 0 : _a2.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("请求超时,请检查网络连接")); }, 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, _b, _c; clearTimeout(timeoutId); try { if (response.status === 401) { reject(new Error("API Key 无效或已过期,请检查配置")); return; } if (response.status === 403) { reject(new Error("API Key 权限不足或账户余额不足")); return; } if (response.status === 429) { reject(new Error("请求频率过高,请稍后再试")); return; } if (response.status === 500 || response.status === 502 || response.status === 503) { reject(new Error("AI 服务暂时不可用,请稍后再试")); return; } if (response.status !== 200) { let errorMsg = `API 错误 (${response.status})`; try { const errorData = JSON.parse(response.responseText); if ((_a = errorData.error) == null ? void 0 : _a.message) { errorMsg = errorData.error.message; } } catch { } reject(new Error(errorMsg)); return; } const data = JSON.parse(response.responseText); if (data.choices && data.choices.length > 0 && ((_b = data.choices[0].message) == null ? void 0 : _b.content)) { 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 返回数据异常,请检查 API 配置")); } } catch (error) { Logger.error("解析响应失败:", (_c = response.responseText) == null ? void 0 : _c.substring(0, 200)); reject(new Error("解析 AI 响应失败,请检查 API 地址是否正确")); } }, onerror: (err) => { clearTimeout(timeoutId); Logger.error("网络错误:", err); reject(new Error("网络请求失败,请检查网络连接和 API 地址")); }, 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 { return false; } } function clickNextButton() { const nextBtn = Array.from(document.querySelectorAll("button")).find((btn) => { var _a; return (_a = btn.textContent) == null ? void 0 : _a.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) => { var _a; return (_a = btn.textContent) == null ? void 0 : _a.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) => { var _a; return (_a = btn.textContent) == null ? void 0 : _a.includes("确认提交"); }); if (!confirmBtn) { const footer = document.querySelector(".ivu-modal-confirm-footer"); if (footer) confirmBtn = footer.querySelector(".ivu-btn-primary") ?? void 0; } if (!confirmBtn) { const modal = document.querySelector(".ivu-modal-confirm"); if (modal) confirmBtn = modal.querySelector(".ivu-btn-primary") ?? void 0; } 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() { var _a, _b; for (let i = 0; i < 15; i++) { let closeBtn = Array.from(document.querySelectorAll("button")).find( (btn) => { var _a2, _b2; return ((_a2 = btn.textContent) == null ? void 0 : _a2.includes("关闭页面")) || ((_b2 = btn.textContent) == null ? void 0 : _b2.includes("关闭")); } ); if (!closeBtn) { const footer = document.querySelector(".ivu-modal-confirm-footer"); if (footer) { const primaryBtn = footer.querySelector(".ivu-btn-primary"); if (primaryBtn && (((_a = primaryBtn.textContent) == null ? void 0 : _a.includes("关闭")) || ((_b = primaryBtn.textContent) == null ? void 0 : _b.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}`); const selected = await selectAnswer(question, answer); if (selected) { updateExamMessage(`✅ 第 ${state.exam.currentQuestionIndex} 题已完成`, "#10b981"); } else { Logger.error(`第${state.exam.currentQuestionIndex}题答案格式不匹配,已暂停`); updateExamMessage(`⚠️ 第 ${state.exam.currentQuestionIndex} 题答案格式不匹配,已暂停`, "#ef4444"); stopExam(); return; } } else { Logger.error(`第${state.exam.currentQuestionIndex}题未获取到答案,已暂停`); updateExamMessage(`❌ 第 ${state.exam.currentQuestionIndex} 题查询失败,已暂停`, "#ef4444"); stopExam(); return; } 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"); stopExam(); return; } } state.exam.isRunning = false; const startBtn = document.getElementById("exam-start"); const stopBtn = document.getElementById("exam-stop"); if (startBtn) startBtn.disabled = false; if (stopBtn) stopBtn.disabled = true; const statusText = document.getElementById("exam-status"); const statusDot = document.getElementById("exam-status-dot"); if (statusText) statusText.textContent = "已完成"; if (statusDot) { statusDot.className = "status-dot completed"; } 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(); const startBtn = document.getElementById("exam-start"); const stopBtn = document.getElementById("exam-stop"); if (startBtn) startBtn.disabled = true; if (stopBtn) stopBtn.disabled = false; const statusText = document.getElementById("exam-status"); const statusDot = document.getElementById("exam-status-dot"); if (statusText) statusText.textContent = "运行中"; if (statusDot) { statusDot.className = "status-dot running"; } updateExamMessage(`开始AI答题(使用 ${AI_PRESETS[CONFIG.exam.currentAI].name})...`, "#10b981"); updateExamProgress(); await answerQuestions(); } function stopExam() { state.exam.isRunning = false; const startBtn = document.getElementById("exam-start"); const stopBtn = document.getElementById("exam-stop"); if (startBtn) startBtn.disabled = false; if (stopBtn) stopBtn.disabled = true; const statusText = document.getElementById("exam-status"); const statusDot = document.getElementById("exam-status-dot"); if (statusText) statusText.textContent = "已停止"; if (statusDot) { statusDot.className = "status-dot"; } updateExamMessage("已停止答题", "#f59e0b"); } function getTotalQuestions() { const answerCard = DOMCache.get(".topic-zpx-list"); if (answerCard) { const questionSpans = answerCard.querySelectorAll(".topic-zpx-main span"); return questionSpans.length; } return 0; } function updateExamProgress() { DOMCache.setText("exam-progress", `${state.exam.currentQuestionIndex}/${state.exam.totalQuestions}`); const percentage = state.exam.totalQuestions > 0 ? state.exam.currentQuestionIndex / state.exam.totalQuestions * 100 : 0; Utils.updateProgressBar("exam-progress-bar", percentage); } function updateExamMessage(text, color = "#64748b") { DOMCache.setText("exam-message", text); DOMCache.setStyle("exam-message", { 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(); addExtraStyles(); document.body.appendChild(panel); bindEvents(); applyTheme(CONFIG.theme); restorePanelState(); switchTab(defaultTab); loadLearningProgress(); setTimeout(() => { showGuide(); }, 500); } function addExtraStyles() { const style = document.createElement("style"); style.textContent = getGuideStyles() + getLogToolbarStyles() + getConfigManagementStyles() + getUIUtilsStyles(); document.head.appendChild(style); } function bindEvents() { const panel = document.getElementById("icve-tabbed-panel"); if (!panel) return; makeDraggable(); panel.addEventListener("click", handlePanelClick); panel.addEventListener("change", Utils.throttle(handlePanelChange, 150)); bindLogEvents(); bindDetailsToggle(); Logger.info("事件绑定完成"); } function bindLogEvents() { document.querySelectorAll(".log-filter-btn").forEach((btn) => { btn.addEventListener("click", () => { const filter = btn.dataset.filter; document.querySelectorAll(".log-filter-btn").forEach((b) => b.classList.remove("active")); btn.classList.add("active"); Logger.filterLogs(filter); }); }); const searchInput = document.getElementById("log-search"); const searchClear = document.getElementById("log-search-clear"); if (searchInput) { searchInput.addEventListener("input", Utils.debounce(() => { setCurrentSearch(searchInput.value); Logger.searchLogs(searchInput.value); }, 200)); } if (searchClear && searchInput) { searchClear.addEventListener("click", () => { searchInput.value = ""; Logger.searchLogs(""); }); } } async 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": handleResetLearning, "exam-start": startExam, "exam-stop": stopExam, "clear-page-log": handleClearLog, "export-log": () => Logger.downloadLogs(), "export-config": downloadConfig, "import-config": handleImportConfig, "reset-config": handleResetConfig, "show-guide": () => { resetGuide(); showGuide(); } }; if (id && actionMap[id]) { await actionMap[id](); return; } const tabBtn = target.closest(".tab-btn"); if (tabBtn == null ? void 0 : tabBtn.dataset.tab) { switchTab(tabBtn.dataset.tab); } const filterBtn = target.closest(".log-filter-btn"); if (filterBtn == null ? void 0 : filterBtn.dataset.filter) { const filter = filterBtn.dataset.filter; document.querySelectorAll(".log-filter-btn").forEach((b) => b.classList.remove("active")); filterBtn.classList.add("active"); Logger.filterLogs(filter); } } async function handleResetLearning() { const confirmed = await showConfirmDialog({ title: "重置学习进度", message: "确定要清空所有已处理节点的记录吗?此操作不可恢复。", confirmText: "确认重置", cancelText: "取消", danger: true }); if (confirmed) { resetLearning(); showToast("学习进度已重置", "success"); } } async function handleClearLog() { if (Logger._logs.length === 0) { showToast("日志已经是空的", "info"); return; } const confirmed = await showConfirmDialog({ title: "清空日志", message: `确定要清空所有 ${Logger._logs.length} 条日志记录吗?`, confirmText: "清空", cancelText: "取消", danger: true }); if (confirmed) { Logger.clearPageLog(); showToast("日志已清空", "success"); } } function handleImportConfig() { const fileInput = createFileInput((result) => { if (result.success) { showToast(result.message, "success"); setTimeout(() => { window.location.reload(); }, 1500); } else { showToast(result.message, "error"); } }); fileInput.click(); } async function handleResetConfig() { const confirmed = await showConfirmDialog({ title: "恢复默认配置", message: "确定要将所有配置重置为默认值吗?包括AI密钥等配置都将被清除。", confirmText: "确认重置", cancelText: "取消", danger: true }); if (confirmed) { resetToDefault(); showToast("配置已重置,页面将刷新", "success"); setTimeout(() => { window.location.reload(); }, 1500); } } 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 = DOMCache.getById("icve-tabbed-panel"); const themeBtn = DOMCache.getById("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 = DOMCache.getById("tab-content-wrapper"); const tabNav = DOMCache.get(".tab-nav"); const toggleBtn = DOMCache.getById("panel-toggle"); if (!wrapper || !toggleBtn) return; 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 = DOMCache.getById("tab-content-wrapper"); const tabNav = DOMCache.get(".tab-nav"); const toggleBtn = DOMCache.getById("panel-toggle"); if (wrapper) wrapper.classList.add("collapsed"); if (tabNav) tabNav.classList.add("collapsed"); if (toggleBtn) toggleBtn.textContent = "+"; } } function makeDraggable() { const panel = DOMCache.getById("icve-tabbed-panel"); const header = DOMCache.getById("panel-header"); if (!panel || !header) return; let isDragging = false; let initialX, initialY; let hasMoved = false; restorePanelPosition(); header.addEventListener("mousedown", (e) => { if (e.target.closest("button")) return; const rect = panel.getBoundingClientRect(); initialX = e.clientX - rect.left; initialY = e.clientY - rect.top; isDragging = true; hasMoved = false; panel.style.transition = "none"; }); document.addEventListener("mousemove", (e) => { if (!isDragging) return; e.preventDefault(); hasMoved = true; const panelRect = panel.getBoundingClientRect(); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; let newX = e.clientX - initialX; let newY = e.clientY - initialY; const minVisible = 50; const maxX = viewportWidth - minVisible; const maxY = viewportHeight - minVisible; const minX = minVisible - panelRect.width; const minY = 0; newX = Math.max(minX, Math.min(newX, maxX)); newY = Math.max(minY, Math.min(newY, maxY)); panel.style.left = newX + "px"; panel.style.top = newY + "px"; panel.style.right = "auto"; }); document.addEventListener("mouseup", () => { if (isDragging && hasMoved) { savePanelPosition(); panel.style.transition = ""; } isDragging = false; }); window.addEventListener("resize", Utils.debounce(() => { ensurePanelInViewport(); }, 200)); } function savePanelPosition() { const panel = DOMCache.getById("icve-tabbed-panel"); if (!panel) return; const rect = panel.getBoundingClientRect(); const position = { left: rect.left, top: rect.top, timestamp: Date.now() }; localStorage.setItem("icve_panel_position", JSON.stringify(position)); } function restorePanelPosition() { const panel = DOMCache.getById("icve-tabbed-panel"); if (!panel) return; try { const saved = localStorage.getItem("icve_panel_position"); if (!saved) return; const position = JSON.parse(saved); if (Date.now() - position.timestamp > 7 * 24 * 60 * 60 * 1e3) { localStorage.removeItem("icve_panel_position"); return; } panel.style.left = position.left + "px"; panel.style.top = position.top + "px"; panel.style.right = "auto"; requestAnimationFrame(() => { ensurePanelInViewport(); }); } catch { } } function ensurePanelInViewport() { const panel = DOMCache.getById("icve-tabbed-panel"); if (!panel) return; const rect = panel.getBoundingClientRect(); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const minVisible = 50; let needsUpdate = false; let newLeft = rect.left; let newTop = rect.top; if (rect.left > viewportWidth - minVisible) { newLeft = viewportWidth - minVisible; needsUpdate = true; } if (rect.right < minVisible) { newLeft = minVisible - rect.width; needsUpdate = true; } if (rect.top > viewportHeight - minVisible) { newTop = viewportHeight - minVisible; needsUpdate = true; } if (rect.top < 0) { newTop = 0; needsUpdate = true; } if (needsUpdate) { panel.style.left = newLeft + "px"; panel.style.top = newTop + "px"; panel.style.right = "auto"; savePanelPosition(); } } const DETAILS_STORAGE_KEY = "icve_details_state"; function bindDetailsToggle() { const panel = DOMCache.getById("icve-tabbed-panel"); if (!panel) return; const detailsElements = panel.querySelectorAll("details[id]"); detailsElements.forEach((details) => { restoreDetailsState(details); details.addEventListener("toggle", () => { saveDetailsState(details); }); }); } function saveDetailsState(details) { if (!details.id) return; try { const saved = localStorage.getItem(DETAILS_STORAGE_KEY); const states = saved ? JSON.parse(saved) : {}; states[details.id] = details.open; localStorage.setItem(DETAILS_STORAGE_KEY, JSON.stringify(states)); } catch { } } function restoreDetailsState(details) { if (!details.id) return; try { const saved = localStorage.getItem(DETAILS_STORAGE_KEY); if (!saved) return; const states = JSON.parse(saved); if (typeof states[details.id] === "boolean") { details.open = states[details.id]; } } catch { } } 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); } })();