beforeRender = array('TocPlugin_Plugin', 'injectToc'); Typecho_Plugin::factory('Widget_Archive')->footer = array('TocPlugin_Plugin', 'footer'); return _t('插件已激活'); } /** * 禁用插件 */ public static function deactivate() { return _t('插件已禁用'); } /** * 插件配置面板 * * @param Typecho_Widget_Helper_Form $form */ public static function config(Typecho_Widget_Helper_Form $form) { // 目录功能配置 // echo '

📖 目录功能配置

'; // 标题设置 $title = new Typecho_Widget_Helper_Form_Element_Text( 'title', NULL, '文章目录', _t('目录标题'), _t('目录的标题文字') ); $form->addInput($title); // 默认状态 $defaultState = new Typecho_Widget_Helper_Form_Element_Select( 'defaultState', array( 'expanded' => _t('展开'), 'collapsed' => _t('收起') ), 'expanded', _t('默认状态'), _t('目录初始状态') ); $form->addInput($defaultState); // 是否显示数字序号 $showNumbers = new Typecho_Widget_Helper_Form_Element_Radio( 'showNumbers', array( '1' => _t('显示'), '0' => _t('不显示') ), '1', _t('显示数字序号'), _t('是否在目录项前显示数字序号') ); $form->addInput($showNumbers); // 平滑滚动 $smoothScroll = new Typecho_Widget_Helper_Form_Element_Radio( 'smoothScroll', array( '1' => _t('启用'), '0' => _t('禁用') ), '1', _t('平滑滚动'), _t('点击目录时是否使用平滑滚动效果') ); $form->addInput($smoothScroll); // 只在文章页面显示 $onlyInPost = new Typecho_Widget_Helper_Form_Element_Radio( 'onlyInPost', array( '1' => _t('是'), '0' => _t('否') ), '1', _t('只在文章页面显示'), _t('是否只在文章页面显示目录,不在首页和列表页显示') ); $form->addInput($onlyInPost); // 快速导航按钮配置 // echo '

⬆️⬇️ 快速导航按钮

'; // 启用返回顶部按钮 $enableTopButton = new Typecho_Widget_Helper_Form_Element_Radio( 'enableTopButton', array( '1' => _t('开启'), '0' => _t('关闭') ), '1', _t('返回顶部按钮'), _t('是否显示返回顶部按钮') ); $form->addInput($enableTopButton); // 启用返回底部按钮 $enableBottomButton = new Typecho_Widget_Helper_Form_Element_Radio( 'enableBottomButton', array( '1' => _t('开启'), '0' => _t('关闭') ), '1', _t('返回底部按钮'), _t('是否显示返回底部按钮') ); $form->addInput($enableBottomButton); // 进度圆圈配置 //echo '

⭕️ 页面进度圆圈

'; // 启用进度圆圈 $enableProgressCircle = new Typecho_Widget_Helper_Form_Element_Radio( 'enableProgressCircle', array( '1' => _t('开启'), '0' => _t('关闭') ), '1', _t('进度圆圈'), _t('是否显示页面进度圆圈(点击可返回顶部)') ); $form->addInput($enableProgressCircle); } /** * 个人用户配置面板 * * @param Typecho_Widget_Helper_Form $form */ public static function personalConfig(Typecho_Widget_Helper_Form $form) { } /** * 获取目录结构 * * @param string $content 文章内容 * @return array 目录结构 */ private static function extractHeadings($content) { // 匹配H1-H3标题 $pattern = '/<(h[1-3])(?:\s[^>]*)?>(.*?)<\/\1>/i'; preg_match_all($pattern, $content, $matches, PREG_SET_ORDER); $toc = array(); $lastH1 = null; $lastH2 = null; foreach ($matches as $index => $match) { $tag = strtolower($match[1]); $text = strip_tags($match[2]); // 清理HTML实体 $text = html_entity_decode($text, ENT_QUOTES, 'UTF-8'); // 生成锚点ID $anchorId = 'toc-' . self::generateAnchorId($text, $index); // 创建目录项 $item = array( 'id' => $anchorId, 'tag' => $tag, 'text' => $text, 'level' => (int)substr($tag, 1), 'children' => array() ); // 构建层级结构 if ($tag == 'h1') { $lastH1 = count($toc); $lastH2 = null; $toc[] = $item; } elseif ($tag == 'h2') { $lastH2 = count($toc); if ($lastH1 !== null) { $toc[$lastH1]['children'][] = $item; } else { $toc[] = $item; } } elseif ($tag == 'h3') { if ($lastH1 !== null && isset($toc[$lastH1])) { if ($lastH2 !== null && isset($toc[$lastH1]['children'][$lastH2])) { $toc[$lastH1]['children'][$lastH2]['children'][] = $item; } else { $toc[$lastH1]['children'][] = $item; } } else { $toc[] = $item; } } } return $toc; } /** * 生成锚点ID * * @param string $text 标题文本 * @param int $index 索引 * @return string 锚点ID */ private static function generateAnchorId($text, $index) { // 移除特殊字符,只保留字母、数字、中文、横线和下划线 $text = preg_replace('/[^\p{L}\p{N}_-]/u', '', $text); // 如果清理后为空,使用索引 if (empty($text)) { $text = 'item-' . $index; } // 转换为小写 $text = strtolower($text); return $text; } /** * 生成目录HTML * * @param array $toc 目录结构 * @return string 目录HTML */ private static function generateTocHtml($toc) { $options = Helper::options()->plugin('TocPlugin'); $defaultState = $options->defaultState == 'collapsed' ? ' collapsed' : ''; $showNumbers = $options->showNumbers ? ' show-numbers' : ''; $html = '
'; // 输出CSS样式(内联在HTML中,避免FOUC) $html .= ''; $html .= '
'; $html .= '' . htmlspecialchars($options->title) . ''; $html .= '' . ($defaultState ? '▶' : '▼') . ''; $html .= '
'; $html .= '
'; if (empty($toc)) { $html .= '

本文无标题结构

'; } else { $html .= self::renderTocItems($toc); } $html .= '
'; $html .= '
'; return $html; } /** * 渲染目录项 * * @param array $items 目录项数组 * @param int $depth 当前深度 * @return string HTML */ private static function renderTocItems($items, $depth = 0) { $html = ''; return $html; } /** * 插入目录到文章 * * @param Widget_Archive $archive */ public static function injectToc($archive) { if (!$archive->is('single')) { $options = Helper::options()->plugin('TocPlugin'); if ($options->onlyInPost) { return; } } if ($archive->is('single') || !$archive->is('single')) { $content = $archive->content; // 提取标题并生成目录 $toc = self::extractHeadings($content); if (!empty($toc)) { $tocHtml = self::generateTocHtml($toc); // 为标题添加ID $content = preg_replace_callback( '/<(h[1-3])(?:\s[^>]*)?>(.*?)<\/\1>/i', function($matches) { $tag = $matches[1]; $text = strip_tags($matches[2]); $anchorId = 'toc-' . self::generateAnchorId($text, 0); return '<' . $tag . ' id="' . $anchorId . '">' . $matches[2] . ''; }, $content ); // 将目录插入到文章最前面 $archive->content = $tocHtml . $content; } } } /** * 获取CSS样式 */ private static function getTocStyles() { return ' /* 目录样式 - 原有样式完全不变 */ .toc-wrapper { background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 8px; padding: 0; margin: 18px 0 20px 0; margin-top:0px!important; float: right; width: 150px; max-width: 100%; position: relative; z-index: 100; transition: all 0.3s ease; overflow: hidden; } .toc-header { background: #e9ecef; padding: 8px 10px; display: flex; justify-content: space-between; align-items: center; cursor: pointer; transition: all 0.2s ease; } .toc-wrapper .toc-header { border-top-left-radius: 8px; border-top-right-radius: 8px; } .toc-wrapper:has(.toc-content.collapsed) .toc-header { border-bottom-left-radius: 8px; border-bottom-right-radius: 8px; } .toc-header:hover { background: #dee2e6; } .toc-title { font-weight: bold; color: #495057; font-size: 13px; } .toc-toggle-icon { font-size: 12px; line-height: 1; transition: transform 0.3s ease; color: #6c757d; } .toc-content { padding: 8px 10px; max-height: 400px; overflow-y: auto; transition: all 0.3s ease; } .toc-content.collapsed { max-height: 0; padding: 0 10px; overflow: hidden; border: none; } .toc-wrapper:has(.toc-content:not(.collapsed)) .toc-content { border-bottom-left-radius: 8px; border-bottom-right-radius: 8px; } .toc-content.show-numbers { counter-reset: toc-counter; } .toc-list { list-style: none !important; padding: 0; margin: 0; } .toc-list, .toc-list li, .toc-list ul { list-style: none !important; } .toc-list .toc-item { list-style-type: none !important; padding-left: 0; } .toc-list.toc-depth-0 > .toc-item > .toc-link:before { content: none; } .toc-content.show-numbers .toc-list.toc-depth-0 > .toc-item > .toc-link:before { content: counter(toc-counter) ". "; counter-increment: toc-counter; font-size: 11px; margin-right: 4px; display: inline-block; min-width: 18px; } .toc-content.show-numbers .toc-list.toc-depth-1 > .toc-item > .toc-link:before, .toc-content.show-numbers .toc-list.toc-depth-2 > .toc-item > .toc-link:before { content: none; } .toc-item { margin: 3px 0; position: relative; } .toc-link { display: block; padding: 4px 0; color: #495057; text-decoration: none; font-size: 12px; line-height: 1.3; transition: color 0.2s ease; padding-left: 0; } .toc-link:hover { color: #495057; background: none; } .toc-link.active { color: #0056b3; font-weight: bold; } .toc-level-1 .toc-link { font-weight: bold; font-size: 12px; } .toc-level-2 .toc-link { font-size: 11px; padding-left: 0; margin-left: 0; } .toc-level-3 .toc-link { font-size: 10px; color: #6c757d; padding-left: 0; margin-left: 0; } .toc-content.show-numbers .toc-level-1 .toc-link { padding-left: 22px; position: relative; } .toc-content ul{padding-left:0.8em!important;} .toc-item ul{padding-left:1.4em!important;} .toc-content.show-numbers .toc-level-1 .toc-link:before { position: absolute; left: 0; top: 4px; } .toc-content.show-numbers .toc-level-2 .toc-link, .toc-content.show-numbers .toc-level-3 .toc-link { padding-left: 0px; } .toc-empty { color: #6c757d; font-style: italic; margin: 0; padding: 6px 0; text-align: center; font-size: 11px; } @media (max-width: 768px) { .toc-wrapper { float: none; width: 100%; margin: 0 0 15px 0; } .toc-title { font-size: 14px; } .toc-link { font-size: 13px; } .toc-level-1 .toc-link { font-size: 13px; } .toc-level-2 .toc-link { font-size: 12px; } .toc-level-3 .toc-link { font-size: 11px; } } @media (max-width: 480px) { .toc-wrapper { width: 100%; } .toc-header { padding: 10px 12px; } .toc-content { padding: 10px 12px; } } .post-content { overflow: hidden; position: relative; } /* 深色模式适配 */ .dark .toc-wrapper { background: #1d1d1e; border: 1px solid #333; } .dark .toc-header { background: #1d1d1e; } .dark .toc-header:hover { background: #2d2d2e; } .dark .toc-title { color: rgb(156 163 175 / var(--tw-text-opacity)); } .dark .toc-toggle-icon { color: rgb(156 163 175 / var(--tw-text-opacity)); } .dark .toc-link { color: rgb(156 163 175 / var(--tw-text-opacity)); } .dark .toc-link:hover { color: rgb(156 163 175 / var(--tw-text-opacity)); background: none; } .dark .toc-link.active { color: #f15a22; } .dark .toc-level-3 .toc-link { color: #a0aec0; } .dark .toc-empty { color: #a0aec0; } '; } /** * 输出导航按钮HTML */ public static function outputNavigationButtons() { $options = Helper::options()->plugin('TocPlugin'); $output = ''; if ($options->enableTopButton) { $output .= ' '; } if ($options->enableBottomButton) { $output .= ' '; } if ($options->enableProgressCircle) { $output .= ' '; } return $output; } /** * 输出JavaScript */ public static function footer() { $options = Helper::options()->plugin('TocPlugin'); ?>