VanBlog 默认的标签页面采用传统分页式布局,对于需要快速浏览高频标签的用户而言存在交互效率问题。本方案通过在作者信息卡下方集成动态标签云,实现以下改进:
graph TD
A[页面加载完成] --> B{检测侧边栏容器}
B -->|容器存在| C[发起API数据请求]
B -->|容器不存在| D[延迟500ms重试]
D --> E{重试次数<10?}
E -->|是| B
E -->|否| F[终止初始化]
C --> G[处理标签数据]
G --> H[计算动态样式]
H --> I[创建DOM结构]
I --> J[定位策略]
J --> K{是否碰撞?}
K -->|是| L[重新生成坐标]
K -->|否| M[插入DOM树]
M --> N[绑定悬浮事件]
N --> O[启动浮动动画]
P[路由变化] --> Q[销毁旧实例]
Q --> R[重新初始化]
style A fill:#4CAF50,color:white
style B fill:#2196F3,color:white
style C fill:#009688,color:white
style D fill:#FF9800,color:white
style J fill:#9C27B0,color:white
style P fill:#E91E63,color:white
javascriptconst safeInitModule = () => {
const maxRetries = 10;
let attemptCount = 0;
const checkContainer = () => {
const container = document.querySelector('.vanblog-sider');
if (container && !container.querySelector('#custom-tags-module')) {
initTagsModule();
return true;
}
return false;
};
const retry = () => {
if (attemptCount++ < maxRetries && !checkContainer()) {
setTimeout(retry, 500);
}
};
if (!checkContainer()) retry();
};
javascriptconst initTagsModule = async () => {
// 容器预处理
const existingModule = document.getElementById('custom-tags-module');
if (existingModule) existingModule.remove();
// 数据获取
const response = await fetch('/api/public/tag');
const { data: tags } = await response.json();
// 动态容器
const container = document.createElement('div');
container.style.cssText = `
height: ${200 + Object.keys(tags).length * 15}px;
position: relative;
overflow: visible;
`;
// 标签生成
Object.entries(tags).forEach(([tag, articles]) => {
const element = createTagElement(tag, articles.length);
applyRandomPosition(element, container);
container.appendChild(element);
});
// 插入DOM树
document.querySelector('#author-card')
.insertAdjacentElement('afterend', container);
};
javascriptconst createTagElement = (tag, count) => {
const element = document.createElement('a');
const baseSize = 12;
element.href = `/tag/${encodeURIComponent(tag)}`;
element.textContent = tag;
element.style.fontSize = `${baseSize + count * 1.5}px`;
element.style.opacity = Math.min(0.6 + count/20, 1);
// 悬浮效果
element.addEventListener('mouseover', () => {
element.style.transform = `scale(${1 + count/30})`;
element.style.zIndex = 100;
});
return element;
};
特性 | 实现方式 |
---|---|
大小权重 | 线性增长公式:12px + 文章数×1.5 |
颜色渐变 | 基于当前主题的RGB值插值计算 |
动态漂浮 | CSS动画相位差控制 |
/api/public/tag
的跨域访问权限!important
确保样式优先级javascript// 性能埋点
const perfMark = name => {
performance.mark(`${name}-start`);
return () => {
performance.mark(`${name}-end`);
performance.measure(name,
`${name}-start`,
`${name}-end`);
};
};
// 使用示例
const measureInit = perfMark('TagCloudInit');
await initTagsModule();
measureInit();
javaelement.addEventListener('touchstart', () => {
element.style.transform = 'scale(1.2)';
});
weight = count × e^(-0.1×age)
本方案已通过 Chrome/Firefox/Safari 最新版测试,
javascriptdocument.addEventListener("DOMContentLoaded", () => {
// 动态脚印颜色切换
const currentHour = new Date().getHours();
const footElement = document.getElementById("user-content-dynamic-footprint");
if (footElement && (currentHour >= 18 || currentHour < 8)) {
footElement.style.setProperty("filter", "invert(1)", "important");
}
// 路由监听逻辑
const originalPushState = history.pushState.bind(history);
const originalReplaceState = history.replaceState.bind(history);
history.pushState = function (...args) {
const result = originalPushState(...args);
window.dispatchEvent(new Event("pushstate"));
return result;
};
history.replaceState = function (...args) {
const result = originalReplaceState(...args);
window.dispatchEvent(new Event("replacestate"));
return result;
};
// 路由变化处理
let routeTimer = null;
const handleRouteChange = () => {
clearTimeout(routeTimer);
routeTimer = setTimeout(safeInitModule, 200);
};
// 初始化检测
let initAttempts = 0;
const safeInitModule = () => {
const siderContainer = document.querySelector(".vanblog-sider");
if (!siderContainer) {
if (initAttempts++ < 10) {
setTimeout(safeInitModule, 500);
}
return;
}
if (!siderContainer.querySelector("#custom-tags-module")) {
initTagsModule();
}
};
// 核心初始化函数
const initTagsModule = async () => {
const siderContainer = document.querySelector(".vanblog-sider");
if (!siderContainer) return;
// 清除已有模块
const existing = siderContainer.querySelector("#custom-tags-module");
if (existing) existing.remove();
// 创建模块结构
const tagsWrapper = document.createElement("div");
tagsWrapper.id = "custom-tags-module";
tagsWrapper.style.cssText = `
position: sticky;
margin-top: 20px;
z-index: 10;
`;
// 创建内容容器
const contentDiv = document.createElement("div");
contentDiv.className = "mt-4 w-full";
contentDiv.innerHTML = `
<div class="w-52 flex flex-col justify-center items-center bg-white pt-6 pb-4 card-shadow ml-2 dark:bg-dark dark:card-shadow-dark"
id="custom-tags-container"></div>
`;
tagsWrapper.appendChild(contentDiv);
// 插入到作者卡片下方
const authorCard = siderContainer.querySelector("#author-card");
if (authorCard) {
const updatePosition = () => {
tagsWrapper.style.top = `${authorCard.offsetHeight + 20}px`;
};
new ResizeObserver(updatePosition).observe(authorCard);
updatePosition();
}
authorCard?.insertAdjacentElement("afterend", tagsWrapper);
try {
const response = await fetch("https://aikuto.com/api/public/tag");
const { data: tagData } = await response.json();
const tagsContainer = tagsWrapper.querySelector("#custom-tags-container");
// 动态调整容器高度
const tagCount = Object.keys(tagData).length;
tagsContainer.style.height = `${200 + tagCount * 15}px`;
tagsContainer.style.position = "relative";
// 创建标签元素
Object.entries(tagData).forEach(([tag, articles]) => {
const tagElement = document.createElement("a");
tagElement.href = `/tag/${tag}`;
tagElement.textContent = tag;
tagElement.className =
"tag-item transition-all duration-300 hover:scale-110 dark:text-gray-300 dark:border-gray-600";
// 动态样式
const fontSize = 12 + articles.length * 1.5;
Object.assign(tagElement.style, {
fontSize: `${fontSize}px`,
borderRadius: "999px",
padding: "4px 12px",
position: "absolute",
cursor: "pointer",
backdropFilter: "blur(3px)",
transition: "transform 0.3s, box-shadow 0.3s",
});
// 随机定位
const randomPosition = () => ({
left: Math.random() * (tagsContainer.offsetWidth - 100),
top: Math.random() * (tagsContainer.offsetHeight - 40),
});
let position = randomPosition();
while (
[...tagsContainer.children].some((existing) => {
const rect = existing.getBoundingClientRect();
return (
Math.abs(rect.left - position.left) < 80 &&
Math.abs(rect.top - position.top) < 30
);
})
) {
position = randomPosition();
}
Object.assign(tagElement.style, {
left: `${position.left}px`,
top: `${position.top}px`,
});
// 悬停效果
tagElement.addEventListener("mouseenter", () => {
tagElement.style.transform = "scale(1.1)";
tagElement.style.boxShadow = "0 4px 15px rgba(153,153,153,0.5)";
});
tagElement.addEventListener("mouseleave", () => {
tagElement.style.transform = "scale(1)";
tagElement.style.boxShadow = "none";
});
tagsContainer.appendChild(tagElement);
});
// 添加浮动动画
const style = document.createElement("style");
style.textContent = `
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
.tag-item {
animation: float 4s ease-in-out infinite;
animation-delay: ${Math.random() * 2}s;
}
`;
document.head.appendChild(style);
} catch (error) {
console.error("获取标签数据时出错:", error);
}
};
// 事件监听
window.addEventListener("popstate", handleRouteChange);
window.addEventListener("pushstate", handleRouteChange);
window.addEventListener("replacestate", handleRouteChange);
// 初始化执行
setTimeout(safeInitModule, 1000);
// MutationObserver
const observer = new MutationObserver((mutations) => {
if (document.body.contains(document.querySelector(".vanblog-sider"))) {
safeInitModule();
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
});
实际部署时建议添加加载状态指示器和错误重试机制,以提升用户体验的完整性。
本文作者:Teddy
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!