仿Heo部分元素添加卡片式信息外框

进行主题修改前必看

  • 博客魔改有风险,请务必备份你的原代码

  • 因为.pug.styl以及.yml等对缩进要求较为严格,请尽量不要使用记事本等无法提供语法高亮的文本编辑器进行修改。

  • 本文涉及修改内容会以diff代码块进行标识,复制时请不要忘记删除前面的+,-符号

  • 本帖基于Anzhiyu主题进行修改方案编写,因此请读者优先掌握Anzhiyu主题官方文档的内容后再来进行魔改。


本文实现效果的样式定义与部分源码来自于Heo博客,感谢优秀的设计令人赏心悦目。

为了方便写魔改总集缓解字数繁多显得杂乱的情况,特地抽出来一篇最近写的设计实现样式,也就是本文所写的–Tooltip拓展信息框,方便各位参考以实现相同的设计样式。

卡片式信息外框
卡片式信息外框

📍特别提醒 本文涉及修改内容较多,请务必备份源代码以方便回滚

实现方式

anzhiyu\layout\includes\layout.pug 里添加如下内容,以确保页面能调用Tooltip

1
2
//- 全局 Tooltip 容器(供 Js 动态控制)
div.custom-tooltip

添加gb-tooltip.js并引用,内容如下,当然也可以自定义文件名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
function initTooltip() {
const tooltip = document.querySelector(".custom-tooltip");
if (!tooltip) return;

let hideTimer = null;
const hasHover = matchMedia("(hover: hover)").matches;

// 保证默认内联属性存在
tooltip.style.maxWidth ||= "200px";
tooltip.style.whiteSpace ||= "pre-wrap";
tooltip.style.overflowWrap ||= "break-word";
tooltip.style.opacity ||= "0";

// 当前激活的目标与冻结位置
let activeTarget = null;
let frozenPos = { top: null, left: null };

function computePosition(el) {
// 先移出视口,避免测量受当前位置影响
tooltip.style.top = "-9999px";
tooltip.style.left = "-9999px";
tooltip.classList.remove("bottom");
void tooltip.offsetHeight; // 强制 reflow

const rect = el.getBoundingClientRect();
const ttRect = tooltip.getBoundingClientRect();
const offset = 10;

let top = rect.top - ttRect.height - offset; // 默认在上方
if (rect.top <= ttRect.height + offset) {
tooltip.classList.add("bottom");
top = rect.bottom + offset; // 改为下方
}

let left = rect.left + rect.width / 2 - ttRect.width / 2;
const minPad = 8;
left = Math.max(minPad, Math.min(left, window.innerWidth - ttRect.width - minPad));

return { top: Math.round(top), left: Math.round(left) };
}

function applyFrozen() {
if (frozenPos.top == null || frozenPos.left == null) return;
tooltip.style.top = `${frozenPos.top}px`;
tooltip.style.left = `${frozenPos.left}px`;
}

function showTooltip(el, text) {
clearTimeout(hideTimer);
activeTarget = el;
tooltip.textContent = text;
tooltip.style.opacity = "1";

// 仅计算一次位置并“冻结”,后续滚动不再更新
frozenPos = computePosition(el);
applyFrozen();
}

function hideTooltip() {
hideTimer = setTimeout(() => {
tooltip.style.opacity = "0";
tooltip.classList.remove("bottom");
activeTarget = null;
frozenPos = { top: null, left: null };
}, 80);
}

function bindTargets(root = document) {
root.querySelectorAll("[gbtip], [data-tooltip]").forEach(el => {
if (el.dataset.tooltipBound) return; // 防止重复绑定
el.dataset.tooltipBound = "true";

const tipText = el.getAttribute("gbtip") || el.getAttribute("data-tooltip");
if (!tipText) return;

if (hasHover) {
el.addEventListener("mouseenter", () => showTooltip(el, tipText));
el.addEventListener("mouseleave", hideTooltip);
} else {
el.addEventListener("touchstart", () => {
showTooltip(el, tipText);
clearTimeout(hideTimer);
hideTimer = setTimeout(hideTooltip, 1500); // 触屏自动隐藏
}, { passive: true });

el.addEventListener("touchend", hideTooltip, { passive: true });
}
});
}

// 全局滚动/缩放:固定元素保持显示;普通元素移出视口则隐藏;不更新位置
function onViewportChange() {
if (!activeTarget || tooltip.style.opacity === "0" || !activeTarget.isConnected) return;

const rect = activeTarget.getBoundingClientRect();
const style = window.getComputedStyle(activeTarget);
const isFixedOrSticky = style.position === "fixed" || style.position === "sticky";

const inViewport =
rect.bottom > 0 &&
rect.right > 0 &&
rect.top < window.innerHeight &&
rect.left < window.innerWidth;

if (isFixedOrSticky || inViewport) {
// 按你的需求:位置保持不变,不做 placeTooltip
// 若你希望在 resize 时避免完全越界,可启用一次轻微“夹取”
// const minPad = 8;
// frozenPos.left = Math.max(minPad, Math.min(frozenPos.left, window.innerWidth - tooltip.offsetWidth - minPad));
// frozenPos.top = Math.max(minPad, Math.min(frozenPos.top, window.innerHeight - tooltip.offsetHeight - minPad));
applyFrozen();
} else {
hideTooltip();
}
}

// 初次绑定
bindTargets();

// 全局监听一次即可,避免为每个元素重复加监听
window.addEventListener("scroll", onViewportChange, { passive: true });
window.addEventListener("resize", onViewportChange, { passive: true });

// anzhiyu PJAX:切换前隐藏;切换后重新绑定
window.addEventListener("pjax:send", hideTooltip);
window.addEventListener("pjax:success", () => {
bindTargets(document);
});
}

document.addEventListener("DOMContentLoaded", initTooltip);

样式定义,加入CSS,可加入到Custom.css或自建文件进行调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/* ======= Tooltip 样式 ======= */
.custom-tooltip {
position: fixed;
background: rgba(255, 255, 255, 0.85);
border: var(--style-border-always);
backdrop-filter: blur(6px);
color: var(--anzhiyu-fontcolor);
padding: 8px 12px;
border-radius: 12px;
font-size: 14px;
z-index: 9999;
pointer-events: none;
/* 核心:不拦截鼠标,避免影响任何元素 hover */
opacity: 0;
transition: opacity .15s;
box-shadow: var(--anzhiyu-shadow-border);
max-width: 200px;
/* 与内联默认一致,作为兜底 */
white-space: pre-wrap;
word-wrap: break-word;
line-height: 1.3;
text-align: center;
}

[data-theme="dark"] .custom-tooltip {
background: rgba(0, 0, 0, 0.7);
color: var(--anzhiyu-white);
}

.custom-tooltip.bottom {
/* 标记位,可扩展方向样式 */
}

/* ======= 移动端适配(仅影响 tooltip 本身) ======= */
@media (hover: none) and (pointer: coarse) {
.custom-tooltip {
font-size: 16px;
padding: 10px 14px;
max-width: 80vw;
line-height: 1.4;
}
}

做完以上内容的添加,以后只要在元素上写 gbtip="xxx",JS 就会自动处理,添加上tooltip拓展框。

注: gbtip可以换为自己的定义,tip,heotip皆可,以下内容均以我自己的定义进行删改

关于实现方面,我的修改

如果你是跟着我的魔改修改下来的,以下会有部分内容与我的魔改主题相关,

以下为我的修改内容,你可以参考大部分样式。

📍特别提醒 以下涉及修改内容较多,请务必备份源代码以方便回滚

主页大体修改

footer.pug部分(路径: anzhiyu\layout\includes\footer.pug)

  • 备案组(相关文字添加gbtip可以参考后续关于仿Heo页脚添加备案组与页脚站点状态检测这篇文章,目前先占位
  • CC协议
1
a.footer-bar-link.cc(href=url_for(theme.footer.footerBar.cc.link) title="cc协议" gbtip="网站采用 署名-非商业性使用-禁止演绎 4.0 国际 标准")
  • 换一批友链
1
a.random-friends-btn#footer-random-friends-btn(href="javascript:addFriendLinksInFooter();" title="换一批友情链接" gbtip="换一批友情链接")
  • 站点头像(相关内容可以参考后续关于仿Solitude页脚站点名附带头像,目前先占位
1
a.footer-bar-link(href=url_for(authorLink) target="_blank" gbtip="前往我的主页")

其中两处相同的均需要修改

  • foot-bar-mini-logo
1
img.footer_mini_logo(title="返回顶部", alt="返回顶部" gbtip="返回顶部" onclick="anzhiyu.scrollToDest(0, 500)", src=centerImg, size="50px")
  • 无聊湾徽标
1
2
3
4
5
6
            a.footer-bar-link.badge(
href=theme.footer.footerBar.badge.link
title=theme.footer.footerBar.badge.title
+ gbtip="加入无聊湾吧🥱"
target="_blank"
style="margin-left:8px"
  • 变量名#ghbdages(安知鱼主题自带徽标
1
2
a.github-badge(target='_blank' href=item.link style='margin-inline:5px' data-title=item.message title=item.message gbtip=item.message)

  • 变量名#footer_deal(迷你logo旁边的社交媒体
1
a.deal_link(href=url_for(item.link) title=item.title gbtip=item.title)

本步也有两处相同的内容需要一起修改


top.pug部分(路径:anzhiyu\layout\includes\top\top.pug)

  • 更多推荐
1
2
3
4
.banner-button-group
.banner-button(gbtip="查看更多推荐文章" onclick='event.stopPropagation();event.preventDefault();anzhiyu.hideTodayCard();')
i.anzhiyufont.anzhiyu-icon-arrow-circle-right
span.banner-button-text 更多推荐

nav.pug部分(路径anzhiyu\layout\includes\header\nav.pug)

  • 中控台
1
label.widget(for="center-console" title=_p("中控台") gbtip="显示中控台" onclick="anzhiyu.switchConsole();")
  • 搜索
1
a.site-page.social-icon.search(href='javascript:void(0);', title='搜索🔍' gbtip="本站搜索🔍" accesskey="s")
  • 返回顶部
1
2
a.totopbtn(href='javascript:void(0);' gbtip="返回顶部⬆️")

  • 随机前往一处文章
1
a.site-page(onclick='toRandomPost()', title='随机前往一个文章', gbtip="随机前往一个文章" href='javascript:void(0);')
  • 返回主页
1
a#site-name(href=url_for('/') gbtip="返回博客主页" accesskey="h")

social.pug部分(路径:anzhiyu\layout\includes\header\social.pug)

  • 个人卡片旁的社交媒体部分
1
a.social-icon.faa-parent.animated-hover(href=url_for(trim(value.split('||')[0])) target="_blank" title=title === undefined ? '' : trim(title) gbtip=title)

bbTimeList.pug部分(路径: anzhiyu\layout\includes\bbTimeList.pug)

  • 自行对照修改
1
2
3
i.anzhiyufont.anzhiyu-icon-jike.bber-logo.fontbold(onclick=onclick_value, title="即刻短文" gbtip="即刻短文", href=href_value, aria-hidden="true")
-----
a.bber-gotobb.anzhiyufont.anzhiyu-icon-circle-arrow-right( onclick=onclick_value, href=href_value, title="查看全文" gbtip="查看全文")

categoryGroup.pug部分(路径: anzhiyu\layout\includes\categoryGroup.pug)

此处内容有进行大幅度修改,填补了分类条部分的空缺位置(主要我很少给分类

完成此部分的修改会有几处BUG:

子分类页固定项缺少信息拓展框

子分类页的布局冲突以及拓展框丢失

具体大致是由于修改了.catalog-list-item的内容作为了固定项,导致与anzhiyu主题自带的category.pug布局有冲突,因为此处的后续我有对分类条进行修改布局,所以如果你有兴趣,可以修改完以下的内容,点击跳转至拓展-首页分类条的布局修改(占位)进行下一步的修改以完善分类条的布局

你可以直接替换(categoryGroup.pug)

1
2
3
4
5
6
7
8
9
10
11
12
#categoryBar
#category-bar.category-bar
#catalog-bar
#catalog-list
.catalog-list-item(id='首页')
a(href="/" gbtip="查看所有精选文章") 精选
.catalog-list-item(id='隧道')
a(href="/archives/" gbtip="查看所有文章") 全部文章
!=catalog_list("categories")
.category-bar-next#category-bar-next(onclick="anzhiyu.scrollCategoryBarToRight()" gbtip="分类条翻页")
i.anzhiyufont.anzhiyu-icon-angle-double-right
a.catalog-more(href="/categories/" gbtip="查看所有分类")!= '更多'

补充剩余没有添加tooltip的部分

路径anzhiyu\scripts\helpers\catalog_list.js

1
2
3
4
5
6
7
8
9
10
11
12
13
hexo.extend.helper.register("catalog_list", function (type) {
let html = ``;
hexo.locals.get(type).map(function (item) {
html += `
<div class="catalog-list-item" id="/${item.path}">
<a href="/${item.path}" gbtip="前往 ${item.name} 分类的文章">
${item.name}
</a>
</div>
`;
});
return html;
});

修改完以上内容,至此主页部分结束,以下内容为文章页修改

文章页大体修改

post-info.pug部分(路径: anzhiyu\layout\includes\header\post-info.pug)

此处内容有进行大幅度修改,为了对应拓展信息框的修改,故将部分文字删去,显得简洁且不多余

  • IP属地部分
1
span.post-meta-position(title="作者IP属地为" + location gbtip="作者IP属地为" + location)
  • 字数统计(此处有大改,可自行对比源码)
1
2
3
4
5
span.post-meta-wordcount(gbtip="这篇文章有" + wordcount(page.content) + "字")
if theme.wordcount.post_wordcount
i.anzhiyufont.anzhiyu-icon-file-word.post-meta-icon
span.word-count= wordcount(page.content)
if theme.wordcount.min2read
  • 阅读时长(此处有大改,可自行对比源码)
1
2
3
4
span.post-meta-readtime(gbtip="这篇文章预计需要" + min2read(page.content,{cn: 350, en: 160}) + _p('post.min2read_unit') + "来阅读")      
if theme.wordcount.min2read
i.anzhiyufont.anzhiyu-icon-clock.post-meta-icon
span=min2read(page.content,{cn: 350, en: 160}) + _p('post.min2read_unit')
  • 阅读量(我将此处修改为热度,毕竟小博客阅读量少显得太寒碜了
1
2
3
4
5
6
7
mixin pvBlock(parent_id,parent_class,parent_title)
span.post-meta-separator
span(class=parent_class id=parent_id data-flag-title=page.title gbtip="热度")
i.anzhiyufont.anzhiyu-icon-fire.post-meta-icon
span.post-meta-label
if block
block

下方的Waline,Twikoo,Artalktitle也需要对应修改为热度

  • 评论数
1
2
3
4
5
6
mixin countBlock
span.post-meta-separator
span.post-meta-commentcount(gbtip="评论数")
i.anzhiyufont.anzhiyu-icon-comments.post-meta-icon
if block
block
  • 文章创建时间更新时间(此处有大改,可自行对比源码)

主要进行了拆分变量,原创建时间更新时间是同一个变量.post-meta-date,我在此进行了拆分,并添加了新变量.post-meta-update对应更新时间

1
2
3
4
5
6
7
8
9
10
11
if (theme.post_meta.post.date_type)
if (theme.post_meta.post.date_type === 'both')
span.post-meta-date(gbtip="本篇文章" + _p('post.created') + date(page.date))
i.anzhiyufont.anzhiyu-icon-calendar-days.post-meta-icon
span.post-meta-label= _p('post.created')
time.post-meta-date-created(itemprop="dateCreated datePublished" datetime=date_xml(page.date) title=_p('post.created') + ' ' + full_date(page.date))=date(page.date, config.date_format)
span.post-meta-separator
span.post-meta-update(gbtip="本篇文章" + _p('post.updated') + date(page.updated))
i.anzhiyufont.anzhiyu-icon-history.post-meta-icon
span.post-meta-label= _p('post.updated')
time.post-meta-date-updated(itemprop="dateCreated datePublished" datetime=date_xml(page.updated) title=_p('post.updated') + ' ' + full_date(page.updated))=date(page.updated, config.date_format)
  • 原创,转载或翻译,分类,tag部分

此处有进行原创与转载判定的修改,具体可以参考本站的anzhiyu 主题文章板块中关于原创与判定的实现逻辑问题,此处是对于当前修改的补充修改,未修改的请自行参考修改和源码几乎一模一样)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if page.copyright === true
a.post-meta-original(href="/reprint/" gbtip="该文章为转载或翻译文章,版权归原作者所有") 转载或翻译
else
a.post-meta-original(href="/copyright/" gbtip="该文章为原创文章,注意版权协议") 原创
if (theme.post_meta.post.categories && page.categories && page.categories.data.length > 0)
span.post-meta-categories
if (theme.post_meta.post.date_type)
span.post-meta-separator

each item, index in page.categories.data
i.anzhiyufont.anzhiyu-icon-inbox.post-meta-icon
a(href=url_for(item.path) itemprop="url" gbtip="查看更多 " + item.name + " 分类的文章").post-meta-categories #[=item.name]
if (index < page.categories.data.length - 1)
i.anzhiyufont.anzhiyu-icon-angle-right.post-meta-separator
if (theme.post_meta.page.tags)
span.article-meta.tags
each item, index in page.tags.data
a(href=url_for(item.path) tabindex="-1" itemprop="url" gbtip="查看更多 " + item.name + " 标签的文章").article-meta__tags
span
i.anzhiyufont.anzhiyu-icon-hashtag
=item.name

为了协同修改,可以将链接修改的后续部分(anzhiyu\layout\includes\post\post-copyright.pug)一起修改,可以参考以下内容


post-copyright.pug部分(anzhiyu\layout\includes\post\post-copyright.pug)

  • 原创,转载或翻译续修改
1
2
3
4
if page.copyright
a.post-copyright__reprint(title="该文章为转载文章,注意版权协议" gbtip="该文章为转载或翻译文章,版权归原作者所有,点击跳转原文" href=url_for(url)) 转载或翻译
else
a.post-copyright__original(title="该文章为原创文章,注意版权协议" gbtip="该文章为原创文章,注意版权协议,点击跳转版权协议" href="/copyright/") 原创
  • 头像部分
1
2
3
a.post-copyright__author_img(href=url_for(copyright_author_link) title='关于博主' gbtip="关于博主")
img.post-copyright__author_img_back(src=url_for(copyright_author_img_back) title='关于博主' alt='头像')
img.post-copyright__author_img_front(src=url_for(copyright_author_img_front) title="关于博主" alt='头像')

页面评论部分(路径: anzhiyu\layout\includes\third-party\comments\index.pug)

1
2
3
.comment-randomInfo
a(gbtip="匿名身份评论将无法收到回复" onclick="anzhiyu.addRandomCommentInfo()" href="javascript:void(0)" style=theme.visitorMail.enable ? "" : "display: none") 匿名评论
a(gbtip="查看本站的隐私政策" href=url_for('/privacy') style="margin-left: 4px") 隐私政策

根据以上的修改,大体上你就可以实现基本的样式了,大体上只要你找出元素,就可以添加,和添加标题的逻辑是相同的。

(好像不知不觉挖了好多新坑