您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
为 OpenMindClub 网页提供悬浮标题导航功能,支持自动主题切换
// ==UserScript== // @name OpenMindClub 标题导航器 // @namespace http://tampermonkey.net/ // @version 0.0.4 // @description 为 OpenMindClub 网页提供悬浮标题导航功能,支持自动主题切换 // @author awyugan // @match https://m.openmindclub.com/stu/*/homework* // @match https://m.openmindclub.com/stu/*/discussion // @grant GM_setValue // @grant GM_getValue // @run-at document-end // @license MIT // ==/UserScript== (function () { "use strict"; // 配置 const CONFIG = { storageKey: "omc_heading_navigator_collapsed", refreshInterval: 1500, // 2秒检查一次DOM变化 maxRetries: 30, // 最多重试30次(1分钟) }; let floatingPanel = null; let retryCount = 0; let lastHeadingCount = 0; let currentTheme = "dark"; // 获取本地存储的折叠状态 function getCollapsedState() { return GM_getValue(CONFIG.storageKey, false); } // 保存折叠状态到本地存储 function saveCollapsedState(collapsed) { GM_setValue(CONFIG.storageKey, collapsed); } // 检测页面主题 function detectTheme() { // 检测系统暗色模式偏好 const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; // 检测页面背景色 const bodyBg = window.getComputedStyle(document.body).backgroundColor; const htmlBg = window.getComputedStyle(document.documentElement).backgroundColor; // 检测页面是否有暗色类名 const hasDarkClass = document.documentElement.classList.contains("dark") || document.body.classList.contains("dark") || document.documentElement.classList.contains("theme-dark") || document.body.classList.contains("theme-dark"); // 综合判断 if (hasDarkClass) return "dark"; // 检查背景色亮度 const getBrightness = (color) => { if (color === "rgba(0, 0, 0, 0)" || color === "transparent") return null; const rgb = color.match(/\d+/g); if (!rgb) return null; return (parseInt(rgb[0]) * 299 + parseInt(rgb[1]) * 587 + parseInt(rgb[2]) * 114) / 1000; }; const bodyBrightness = getBrightness(bodyBg); const htmlBrightness = getBrightness(htmlBg); // 如果能检测到背景色且较暗,使用深色主题 if (bodyBrightness !== null && bodyBrightness < 128) return "dark"; if (htmlBrightness !== null && htmlBrightness < 128) return "dark"; // 如果检测不到明确的背景色,使用系统偏好 if (bodyBrightness === null && htmlBrightness === null && prefersDark) return "dark"; return "light"; } // 获取主题样式 function getThemeStyles(theme) { if (theme === "dark") { return { background: "#2d2d2d", border: "#404040", headerBg: "#3a3a3a", headerHover: "#434343", textColor: "#e8e8e8", itemColor: "#d0d0d0", itemHover: "rgba(255, 255, 255, 0.05)", toggleColor: "#b8b8b8", arrowColor: "#888", emptyColor: "#888", scrollTrack: "#333", scrollThumb: "#555", scrollThumbHover: "#777", }; } else { return { background: "#ffffff", border: "#e0e0e0", headerBg: "#f8f9fa", headerHover: "#f0f1f2", textColor: "#333333", itemColor: "#555555", itemHover: "rgba(0, 0, 0, 0.05)", toggleColor: "#666666", arrowColor: "#999999", emptyColor: "#6c757d", scrollTrack: "#f1f1f1", scrollThumb: "#c1c1c1", scrollThumbHover: "#a8a8a8", }; } } // 创建悬浮窗样式 function createStyles(theme = "dark") { const colors = getThemeStyles(theme); const style = document.createElement("style"); style.id = "omc-navigator-styles"; style.textContent = ` #omc-heading-navigator { position: fixed; top: 20px; right: 20px; width: 280px; max-height: 80vh; background: ${colors.background}; border: 1px solid ${colors.border}; border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, ${theme === "dark" ? "0.3" : "0.15"}); z-index: 10000; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 13px; overflow: hidden; transition: all 0.3s ease; color: ${colors.textColor}; } #omc-heading-navigator.collapsed { height: 36px !important; } .omc-nav-header { background: ${colors.headerBg}; padding: 8px 12px; border-bottom: 1px solid ${colors.border}; display: flex; justify-content: space-between; align-items: center; cursor: pointer; user-select: none; } .omc-nav-header:hover { background: ${colors.headerHover}; } .omc-nav-title { font-weight: 500; color: ${colors.textColor}; margin: 0; font-size: 13px; } .omc-nav-toggle { background: none; border: none; font-size: 12px; cursor: pointer; color: ${colors.toggleColor}; padding: 2px; transition: transform 0.3s ease; } .omc-nav-toggle.collapsed { transform: rotate(-90deg); } .omc-nav-content { max-height: calc(80vh - 50px); overflow-y: auto; padding: 6px 0; background: ${colors.background}; } .omc-nav-content.collapsed { display: none; } .omc-heading-item { padding: 6px 12px; cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; position: relative; color: ${colors.itemColor}; line-height: 1.3; } .omc-heading-item:hover { background: ${colors.itemHover}; color: ${colors.textColor}; } .omc-heading-item.has-children { cursor: pointer; } .omc-heading-toggle { width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; margin-right: 4px; color: ${colors.arrowColor}; font-size: 10px; transition: transform 0.2s ease; flex-shrink: 0; } .omc-heading-toggle.expanded { transform: rotate(90deg); } .omc-heading-toggle:hover { color: ${colors.textColor}; } .omc-heading-text { flex: 1; color: inherit; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* 层级缩进样式 */ .omc-heading-item[data-level="1"] { padding-left: 12px; } .omc-heading-item[data-level="2"] { padding-left: 28px; } .omc-heading-item[data-level="3"] { padding-left: 44px; } .omc-heading-item[data-level="4"] { padding-left: 60px; } .omc-heading-item[data-level="5"] { padding-left: 76px; } .omc-heading-item[data-level="6"] { padding-left: 92px; } /* 折叠状态 */ .omc-heading-children.collapsed { display: none; } .omc-nav-empty { padding: 20px 12px; text-align: center; color: ${colors.emptyColor}; font-style: italic; font-size: 12px; } .omc-nav-loading { padding: 15px 12px; text-align: center; color: ${colors.emptyColor}; font-size: 12px; } /* 滚动条样式 */ .omc-nav-content::-webkit-scrollbar { width: 4px; } .omc-nav-content::-webkit-scrollbar-track { background: ${colors.scrollTrack}; } .omc-nav-content::-webkit-scrollbar-thumb { background: ${colors.scrollThumb}; border-radius: 2px; } .omc-nav-content::-webkit-scrollbar-thumb:hover { background: ${colors.scrollThumbHover}; } `; // 移除旧样式 const oldStyle = document.getElementById("omc-navigator-styles"); if (oldStyle) { oldStyle.remove(); } document.head.appendChild(style); } // 更新主题 function updateTheme() { const newTheme = detectTheme(); if (newTheme !== currentTheme) { currentTheme = newTheme; createStyles(currentTheme); console.log(`OpenMindClub 标题导航器切换到${currentTheme === "dark" ? "深色" : "浅色"}主题`); } } // 创建悬浮窗HTML结构 function createFloatingPanel() { const panel = document.createElement("div"); panel.id = "omc-heading-navigator"; const collapsed = getCollapsedState(); if (collapsed) { panel.classList.add("collapsed"); } panel.innerHTML = ` <div class="omc-nav-header"> <h3 class="omc-nav-title">大纲</h3> <button class="omc-nav-toggle ${collapsed ? "collapsed" : ""}">▼</button> </div> <div class="omc-nav-content ${collapsed ? "collapsed" : ""}"> <div class="omc-nav-loading">正在加载标题...</div> </div> `; // 添加点击事件 const header = panel.querySelector(".omc-nav-header"); const content = panel.querySelector(".omc-nav-content"); const toggle = panel.querySelector(".omc-nav-toggle"); header.addEventListener("click", function () { const isCollapsed = panel.classList.toggle("collapsed"); content.classList.toggle("collapsed", isCollapsed); toggle.classList.toggle("collapsed", isCollapsed); saveCollapsedState(isCollapsed); }); document.body.appendChild(panel); return panel; } // 获取页面中的所有标题 function getHeadings() { const headings = []; const headingElements = document.querySelectorAll("h1, h2, h3, h4, h5, h6"); headingElements.forEach((heading, index) => { // 排除我们自己的导航器中的标题 if (heading.closest("#omc-heading-navigator")) { return; } const text = heading.textContent.trim(); if (text) { const level = parseInt(heading.tagName.charAt(1)); headings.push({ element: heading, level: level, tagName: heading.tagName.toLowerCase(), text: text, id: heading.id || `heading-${index}`, children: [], collapsed: GM_getValue(`collapsed_${heading.id || `heading-${index}`}`, false), }); } }); return buildHeadingTree(headings); } // 构建标题树结构 function buildHeadingTree(headings) { const tree = []; const stack = []; headings.forEach((heading) => { // 找到合适的父级 while (stack.length > 0 && stack[stack.length - 1].level >= heading.level) { stack.pop(); } if (stack.length === 0) { tree.push(heading); } else { stack[stack.length - 1].children.push(heading); } stack.push(heading); }); return tree; } // 渲染标题树 function renderHeadingTree(headings, level = 1) { let html = ""; headings.forEach((heading) => { const hasChildren = heading.children && heading.children.length > 0; const isCollapsed = heading.collapsed; html += ` <div class="omc-heading-item ${hasChildren ? "has-children" : ""}" data-target="${heading.id}" data-level="${level}"> ${ hasChildren ? `<span class="omc-heading-toggle ${!isCollapsed ? "expanded" : ""}" data-heading-id="${heading.id}">▶</span>` : '<span class="omc-heading-toggle"></span>' } <span class="omc-heading-text">${heading.text}</span> </div> `; if (hasChildren) { html += `<div class="omc-heading-children ${isCollapsed ? "collapsed" : ""}" data-parent="${heading.id}">`; html += renderHeadingTree(heading.children, level + 1); html += "</div>"; } }); return html; } // 更新悬浮窗内容 function updateFloatingPanel() { if (!floatingPanel) return; const headingTree = getHeadings(); const content = floatingPanel.querySelector(".omc-nav-content"); if (headingTree.length === 0) { content.innerHTML = '<div class="omc-nav-empty">暂未发现页面标题</div>'; return false; } // 计算总标题数 function countHeadings(tree) { let count = 0; tree.forEach((heading) => { count++; if (heading.children) { count += countHeadings(heading.children); } }); return count; } lastHeadingCount = countHeadings(headingTree); const html = renderHeadingTree(headingTree); content.innerHTML = html; // 添加事件监听 addEventListeners(content, headingTree); return true; } // 添加事件监听器 function addEventListeners(content, headingTree) { // 点击切换折叠状态 content.querySelectorAll(".omc-heading-toggle").forEach((toggle) => { toggle.addEventListener("click", function (e) { e.stopPropagation(); const headingId = this.getAttribute("data-heading-id"); if (!headingId) return; const childrenContainer = content.querySelector(`[data-parent="${headingId}"]`); const isCollapsed = childrenContainer.classList.contains("collapsed"); childrenContainer.classList.toggle("collapsed", !isCollapsed); this.classList.toggle("expanded", isCollapsed); // 保存折叠状态 GM_setValue(`collapsed_${headingId}`, !isCollapsed); }); }); // 点击标题跳转 content.querySelectorAll(".omc-heading-text").forEach((textElement) => { textElement.addEventListener("click", function () { const item = this.closest(".omc-heading-item"); const targetId = item.getAttribute("data-target"); // 查找目标元素 function findHeadingById(tree, id) { for (const heading of tree) { if (heading.id === id) return heading; if (heading.children) { const found = findHeadingById(heading.children, id); if (found) return found; } } return null; } const targetHeading = findHeadingById(headingTree, targetId); if (targetHeading && targetHeading.element) { targetHeading.element.scrollIntoView({ behavior: "smooth", block: "start", }); // 高亮效果 targetHeading.element.style.transition = "background-color 0.3s ease"; targetHeading.element.style.backgroundColor = "#fff3cd"; setTimeout(() => { targetHeading.element.style.backgroundColor = ""; }, 2000); } }); }); } // 检查页面内容变化 function checkForChanges() { const currentHeadingCount = document.querySelectorAll("h1, h2, h3, h4, h5, h6").length; if (currentHeadingCount !== lastHeadingCount) { updateFloatingPanel(); } // 如果还没有找到标题且重试次数未超限,继续检查 if (currentHeadingCount === 0 && retryCount < CONFIG.maxRetries) { retryCount++; setTimeout(checkForChanges, CONFIG.refreshInterval); } else if (currentHeadingCount > 0) { retryCount = 0; // 重置重试计数 // 找到标题后,定期检查变化 setTimeout(checkForChanges, CONFIG.refreshInterval); } } // 初始化脚本 function init() { // 检测并设置初始主题 currentTheme = detectTheme(); createStyles(currentTheme); // 创建悬浮窗 floatingPanel = createFloatingPanel(); // 初始更新 updateFloatingPanel(); // 开始检查变化 setTimeout(checkForChanges, 1000); // 监听主题变化 const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); mediaQuery.addListener(updateTheme); // 监听页面类名变化 const observer = new MutationObserver(() => { setTimeout(updateTheme, 100); // 延迟检测,确保样式已应用 }); observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"], }); observer.observe(document.body, { attributes: true, attributeFilter: ["class"], }); console.log(`OpenMindClub 标题导航器已启动 (${currentTheme === "dark" ? "深色" : "浅色"}主题)`); } // 等待页面完全加载后初始化 if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })();