Typecho通用悬浮目录插件
侧边栏壁纸
  • 累计撰写 5 篇文章
  • 累计收到 0 条评论

Typecho通用悬浮目录插件

Evan
2026-03-31 / 0 评论 / 1 阅读

前言

我折腾过太多博客系统了,hugo,hexo,WordPress等,最终还是选择了typecho,它的好处很明显,非常的轻量,搭配SQLite用起来真的很爽,而且备份起来也很方便,所有文件一打包就好了,可以说非常使,而且有后端,不会像hugo,hexo这类静态博客系统一样换一台机器就要本地重新配置,WordPress很强大,但是对我们这种个人小博客来说太重了。但由于typecho现在用的人确实不多了,好用的主题和插件也不多,我现在用的是Joe这个主题,其他都挺好的,就是没有目录功能,如果是长文的话对读者非常不友好。
在现在这个AI的时候,我就vibe coding了一个悬浮目录的插件,也就是你们现在看到的这个右侧的目录,可能有点小瑕疵,待我后续优化一下

使用方法

  1. 本地建一个Plugin.php的文件
  2. 将下方的代码copy到这个文件中
  3. 在你typecho的usr/plugins目录下建一个SmartTOC的目录,这个名字必须要对应上
  4. 把本地的Plugin.php文件上传到SmartTOC目录下,即可在博客后台看到这个插件了

后续我把它打包一下放到github上去,到时候导入起来会方便一些

插件代码

<?php
/**
 * SmartTOC - 智能悬浮目录插件
 * 
 * @package SmartTOC
 * @author Evan
 * @version 1.0.0
 * @link https://www.evantalk.com
 */

if (!defined('__TYPECHO_ROOT_DIR__')) exit;

class SmartTOC_Plugin implements Typecho_Plugin_Interface
{
    public static function activate()
    {
        Typecho_Plugin::factory('Widget_Archive')->footer = array('SmartTOC_Plugin', 'render');
        return '插件已激活';
    }

    public static function deactivate()
    {
        return '插件已禁用';
    }

    public static function config(Typecho_Widget_Helper_Form $form)
    {
        $name = 'offset';
        $default = '80';
        $label = '顶部偏移量 (px)';
        $desc = '点击标题跳转时,距离顶部的像素值(建议设置为:导航栏高度 + 20)';
        $offset = new Typecho_Widget_Helper_Form_Element_Text($name, NULL, $default, $label, $desc);
        $form->addInput($offset);
    }

    public static function personalConfig(Typecho_Widget_Helper_Form $form){}

    public static function render($archive)
    {
        if (!$archive->is('single')) {
            return;
        }

        $offsetVal = 80;
        try {
            $options = Typecho_Widget::widget('Widget_Options')->plugin('SmartTOC');
            if (isset($options->offset) && is_numeric($options->offset)) {
                $offsetVal = intval($options->offset);
            }
        } catch (Exception $e) {
            $offsetVal = 80;
        }

        echo '<style>
            /* 容器基础样式 */
            #smart-toc-container { position: fixed; bottom: 50px; right: 20px; width: 220px; background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.15); z-index: 9999; font-size: 14px; color: #333; border: 1px solid #eee; max-height: 400px; display: flex; flex-direction: column; user-select: none; transition: width 0.3s, height 0.3s, border-radius 0.3s; }
            html[data-theme="dark"] #smart-toc-container, body.dark #smart-toc-container { background: rgba(40, 40, 40, 0.95); color: #eee; border-color: #444; }
            
            /* 头部样式 */
            #smart-toc-header { padding: 12px 15px; cursor: move; background: #f9f9f9; border-bottom: 1px solid #eee; border-radius: 8px 8px 0 0; font-weight: bold; display: flex; justify-content: space-between; align-items: center; }
            html[data-theme="dark"] #smart-toc-header, body.dark #smart-toc-header { background: #333; border-bottom-color: #444; }
            #smart-toc-minimize { cursor: pointer; padding: 0 5px; font-size: 18px; line-height: 1; }
            
            /* 列表滚动区 */
            #smart-toc-list { padding: 8px 0; overflow-y: auto; margin: 0; list-style: none; scrollbar-width: thin; }
            #smart-toc-list::-webkit-scrollbar { width: 4px; }
            #smart-toc-list::-webkit-scrollbar-thumb { background: #ccc; border-radius: 2px; }
            #smart-toc-list li { margin: 0; line-height: 1.4; padding: 0; }
            
            /* --- 核心链接样式 & 缩进系统 --- */
            #smart-toc-list a { 
                text-decoration: none; 
                color: inherit; 
                display: block; 
                /* 稍微减小上下间距,使列表更紧凑 */
                padding: 5px 12px 5px 0; 
                transition: background 0.2s, color 0.2s; 
                white-space: nowrap; 
                overflow: hidden; 
                text-overflow: ellipsis; 
                font-size: 13.5px; 
                border-left: 3px solid transparent; 
                position: relative;
            }
            
            #smart-toc-list a:hover { background: rgba(0,0,0,0.05); color: #409eff; }
            html[data-theme="dark"] #smart-toc-list a:hover { background: rgba(255,255,255,0.1); }
            
            /* 激活状态 */
            #smart-toc-list a.active { background: rgba(64, 158, 255, 0.1); color: #409eff; border-left: 3px solid #409eff; font-weight: 600; }

            /* --- 强制缩进规则 (padding-left) --- */
            
            /* H1 (极少用) */
            #smart-toc-list a.stoc-h1 { padding-left: 10px; font-weight: 700; }
            
            /* H2: 基础缩进 15px */
            #smart-toc-list a.stoc-h2 { 
                padding-left: 15px; 
                font-weight: 600; /* 加粗 */
                color: #333;
            }
            
            /* H3: 缩进 30px (比H2多15px) */
            #smart-toc-list a.stoc-h3 { 
                padding-left: 30px; 
                font-size: 13px;
                color: #666;
            }
            
            /* H4: 缩进 45px (比H3多15px) */
            #smart-toc-list a.stoc-h4 { 
                padding-left: 45px; 
                font-size: 12.5px;
                color: #888;
            }
            
            /* H5/H6: 如果有,缩进更深 */
            #smart-toc-list a.stoc-h5 { padding-left: 60px; font-size: 12px; color: #999; }
            
            /* 暗黑模式文字颜色适配 */
            html[data-theme="dark"] #smart-toc-list a.stoc-h2 { color: #eee; }
            html[data-theme="dark"] #smart-toc-list a.stoc-h3 { color: #aaa; }
            html[data-theme="dark"] #smart-toc-list a.stoc-h4 { color: #888; }

            /* 最小化圆球 */
            #smart-toc-container.minimized { width: 44px; height: 44px; border-radius: 50%; overflow: hidden; cursor: pointer; border: none; box-shadow: 0 4px 10px rgba(0,0,0,0.2); }
            #smart-toc-container.minimized #smart-toc-header { padding: 0; height: 100%; justify-content: center; background: #409eff; color: white; }
            #smart-toc-container.minimized .header-title, #smart-toc-container.minimized #smart-toc-list { display: none; }
        </style>';

        $js = '<script>
        (function() {
            var USER_OFFSET = ' . $offsetVal . ';
            
            function initSmartTOC() {
                var oldContainer = document.getElementById("smart-toc-container");
                if (oldContainer) oldContainer.remove();

                var contentSelectors = [".joe_detail__content", ".typecho-post-content", ".entry-content", ".post-content", "article", "#content"];
                var contentEl = null;
                for (var i = 0; i < contentSelectors.length; i++) {
                    var el = document.querySelector(contentSelectors[i]);
                    if (el) { contentEl = el; break; }
                }
                if (!contentEl) return;

                // 扩大抓取范围,支持到 H6
                var headers = contentEl.querySelectorAll("h1, h2, h3, h4, h5, h6");
                if (headers.length < 2) return; 

                var container = document.createElement("div");
                container.id = "smart-toc-container";
                
                var header = document.createElement("div");
                header.id = "smart-toc-header";
                header.innerHTML = "<span class=\'header-title\'>目录</span><span id=\'smart-toc-minimize\'>-</span>";
                
                var list = document.createElement("ul");
                list.id = "smart-toc-list";

                headers.forEach(function(h, index) {
                    if (!h.id) h.id = "stoc-" + index;
                    var li = document.createElement("li");
                    var a = document.createElement("a");
                    a.href = "#" + h.id;
                    a.innerText = h.innerText;
                    // JS 自动分配类名:stoc-h2, stoc-h3...
                    a.className = "stoc-" + h.tagName.toLowerCase();
                    
                    a.addEventListener("click", function(e) {
                        e.preventDefault();
                        var target = document.getElementById(h.id);
                        if(target) {
                            var elementPosition = target.getBoundingClientRect().top;
                            var offsetPosition = elementPosition + window.pageYOffset - USER_OFFSET;
                            window.scrollTo({ top: offsetPosition, behavior: "smooth" });
                            if(window.innerWidth < 768) container.classList.add("minimized");
                        }
                    });
                    li.appendChild(a);
                    list.appendChild(li);
                });

                container.appendChild(header);
                container.appendChild(list);
                document.body.appendChild(container);

                var isDragging = false, startX, startY, initialLeft, initialTop;
                header.addEventListener("mousedown", dragStart);
                header.addEventListener("touchstart", dragStart, {passive: false});

                function dragStart(e) {
                    if (e.target.id === "smart-toc-minimize") return;
                    isDragging = true;
                    var rect = container.getBoundingClientRect();
                    container.style.right = "auto"; container.style.bottom = "auto";
                    container.style.left = rect.left + "px"; container.style.top = rect.top + "px";
                    
                    if (e.type === "touchstart") { startX = e.touches[0].clientX; startY = e.touches[0].clientY; } 
                    else { startX = e.clientX; startY = e.clientY; }
                    initialLeft = rect.left; initialTop = rect.top;
                    document.addEventListener("mousemove", dragMove); document.addEventListener("mouseup", dragEnd);
                    document.addEventListener("touchmove", dragMove, {passive: false}); document.addEventListener("touchend", dragEnd);
                }

                function dragMove(e) {
                    if (!isDragging) return;
                    e.preventDefault();
                    var clientX = (e.type === "touchmove") ? e.touches[0].clientX : e.clientX;
                    var clientY = (e.type === "touchmove") ? e.touches[0].clientY : e.clientY;
                    container.style.left = (initialLeft + (clientX - startX)) + "px";
                    container.style.top = (initialTop + (clientY - startY)) + "px";
                }

                function dragEnd() {
                    isDragging = false;
                    document.removeEventListener("mousemove", dragMove); document.removeEventListener("mouseup", dragEnd);
                    document.removeEventListener("touchmove", dragMove); document.removeEventListener("touchend", dragEnd);
                }

                var minBtn = document.getElementById("smart-toc-minimize");
                minBtn.addEventListener("click", function(e) { e.stopPropagation(); toggleMinimize(); });
                container.addEventListener("click", function(e) { if(container.classList.contains("minimized")) toggleMinimize(); });

                function toggleMinimize() {
                    container.classList.toggle("minimized");
                    var isMin = container.classList.contains("minimized");
                    if (isMin) { header.innerHTML = "<span style=\'font-size:20px; line-height:44px;\'>≡</span>"; minBtn.textContent = ""; } 
                    else { 
                        header.innerHTML = "<span class=\'header-title\'>目录</span><span id=\'smart-toc-minimize\'>-</span>";
                        document.getElementById("smart-toc-minimize").addEventListener("click", function(ev){ ev.stopPropagation(); toggleMinimize(); });
                        header.addEventListener("mousedown", dragStart); header.addEventListener("touchstart", dragStart, {passive: false});
                    }
                }

                if ("IntersectionObserver" in window) {
                    var observer = new IntersectionObserver(function(entries) {
                        entries.forEach(function(entry) {
                            if (entry.isIntersecting) {
                                list.querySelectorAll(".active").forEach(function(a) { a.classList.remove("active"); });
                                var activeLink = list.querySelector("a[href=\'#" + entry.target.id + "\']");
                                if (activeLink) {
                                    activeLink.classList.add("active");
                                    var linkRect = activeLink.getBoundingClientRect(); var listRect = list.getBoundingClientRect();
                                    if (linkRect.bottom > listRect.bottom || linkRect.top < listRect.top) activeLink.scrollIntoView({ block: "nearest" });
                                }
                            }
                        });
                    }, { rootMargin: "-" + USER_OFFSET + "px 0px -70% 0px" });
                    headers.forEach(function(h) { observer.observe(h); });
                }
            }

            document.addEventListener("DOMContentLoaded", initSmartTOC);
            document.addEventListener("pjax:complete", initSmartTOC); 
            document.addEventListener("pjax:end", initSmartTOC);
            var lastUrl = location.href;
            setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(initSmartTOC, 500); } }, 1000);
        })();
        </script>';

        echo $js;
    }
}
0

评论 (0)

取消