// ==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 `
⚙️ 高级
💡 配置完成后点击"开始"
`;
}
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 = `
${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 = `
`;
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);
}
})();