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