update hook

··[CST 2026-02-18 Wednesday 15:25:54]
This commit is contained in:
biss
2026-02-18 15:25:54 +08:00
commit a39e3f25bb
107 changed files with 54767 additions and 0 deletions

412
404.html Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
C5EC3CAE4FD14AD69AECF56219B47A0A

414
about/index.html Normal file

File diff suppressed because one or more lines are too long

558
archives/2025/08/index.html Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

558
archives/2025/09/index.html Normal file

File diff suppressed because one or more lines are too long

558
archives/2025/10/index.html Normal file

File diff suppressed because one or more lines are too long

558
archives/2025/index.html Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

558
archives/2026/01/index.html Normal file

File diff suppressed because one or more lines are too long

558
archives/2026/02/index.html Normal file

File diff suppressed because one or more lines are too long

558
archives/2026/index.html Normal file

File diff suppressed because one or more lines are too long

558
archives/index.html Normal file

File diff suppressed because one or more lines are too long

558
archives/page/2/index.html Normal file

File diff suppressed because one or more lines are too long

558
archives/page/3/index.html Normal file

File diff suppressed because one or more lines are too long

547
atom.xml Normal file
View File

@@ -0,0 +1,547 @@
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Bi&#39;s Blog</title>
<icon>https://blog.biss.click/icon.png</icon>
<link href="https://blog.biss.click/atom.xml" rel="self"/>
<link href="https://blog.biss.click/"/>
<updated>2026-02-18T07:25:26.766Z</updated>
<id>https://blog.biss.click/</id>
<author>
<name>biss</name>
</author>
<generator uri="https://hexo.io/">Hexo</generator>
<entry>
<title>新年快乐!</title>
<link href="https://blog.biss.click/posts/5785bd01/"/>
<id>https://blog.biss.click/posts/5785bd01/</id>
<published>2026-02-13T22:56:18.000Z</published>
<updated>2026-02-18T07:25:26.766Z</updated>
<summary type="html">&lt;p&gt;各位亲朋好友、合作伙伴及屏幕前的你:&lt;/p&gt;
&lt;p&gt;值此二〇二六年蛇年来临之际,为贯彻落实“快乐至上”的核心价值观,进一步提升全体人员的幸福指数,现将有关事项通知如下:&lt;/p&gt;
&lt;p&gt;一、各单位要切实做好“吃好喝好”保障工作严禁在假期期间进行任何形式的emo。&lt;/p&gt;
&lt;</summary>
<category term="生活" scheme="https://blog.biss.click/tags/life/"/>
</entry>
<entry>
<title>添加网站左上角菜单</title>
<link href="https://blog.biss.click/posts/7e921903/"/>
<id>https://blog.biss.click/posts/7e921903/</id>
<published>2026-02-09T23:11:14.000Z</published>
<updated>2026-02-18T07:25:26.766Z</updated>
<summary type="html">&lt;p&gt;看到柳神的网站有这种菜单,但是没有写魔改教程,只好自己慢慢摸索了。&lt;/p&gt;
&lt;div class=&#39;liushen-tag-link&#39;&gt;&lt;a class=&quot;tag-Link&quot; target=&quot;_blank&quot;</summary>
<category term="建站手札" scheme="https://blog.biss.click/categories/website/"/>
<category term="网站" scheme="https://blog.biss.click/tags/web/"/>
</entry>
<entry>
<title>将博客仓库转移到gitea</title>
<link href="https://blog.biss.click/posts/d2c8521/"/>
<id>https://blog.biss.click/posts/d2c8521/</id>
<published>2026-02-07T04:30:39.000Z</published>
<updated>2026-02-18T07:25:26.766Z</updated>
<summary type="html">&lt;p&gt;在上一篇文章中已经完成了gitea的安装&lt;br&gt;那么博客源码迁移倒是没问题,直接&lt;code&gt;git remote add origin&lt;/code&gt;就行但是action文件就有些变更。&lt;br&gt;这是我修改的action文件&lt;/p&gt;
&lt;figure</summary>
<category term="技术" scheme="https://blog.biss.click/categories/technology/"/>
<category term="gitea" scheme="https://blog.biss.click/tags/gitea/"/>
</entry>
<entry>
<title>安装gitea</title>
<link href="https://blog.biss.click/posts/34725d47/"/>
<id>https://blog.biss.click/posts/34725d47/</id>
<published>2026-02-06T22:32:04.000Z</published>
<updated>2026-02-18T07:25:26.766Z</updated>
<summary type="html">&lt;p&gt;今天想把网站的源码转移到自建git仓所以先来安装gitea吧gitlab过于庞大服务器配置不够&lt;br&gt;PS:我的服务器为2C2G&lt;/p&gt;
&lt;h1 id=&quot;安装gitea&quot;&gt;&lt;a href=&quot;#安装gitea&quot; class=&quot;headerlink&quot;</summary>
<category term="技术" scheme="https://blog.biss.click/categories/technology/"/>
<category term="gitea" scheme="https://blog.biss.click/tags/gitea/"/>
</entry>
<entry>
<title>添加typesense搜索</title>
<link href="https://blog.biss.click/posts/f287c563/"/>
<id>https://blog.biss.click/posts/f287c563/</id>
<published>2026-02-05T05:14:16.000Z</published>
<updated>2026-02-18T07:25:26.766Z</updated>
<summary type="html">&lt;p&gt;最近在构建班级博客,用&lt;code&gt;ghost cms&lt;/code&gt;在构建搜索时发现了typesense所以把他移植到这个博客上。&lt;/p&gt;
&lt;h1 id=&quot;安装typesense&quot;&gt;&lt;a href=&quot;#安装typesense&quot; class=&quot;headerlink&quot;</summary>
<category term="技术" scheme="https://blog.biss.click/categories/technology/"/>
<category term="建站手札" scheme="https://blog.biss.click/categories/technology/website/"/>
</entry>
<entry>
<title>自定义字体</title>
<link href="https://blog.biss.click/posts/b5601a7e/"/>
<id>https://blog.biss.click/posts/b5601a7e/</id>
<published>2026-01-15T23:47:15.000Z</published>
<updated>2026-02-18T07:25:26.762Z</updated>
<summary type="html">&lt;p&gt;今天感觉网站的字体有些不好看想换一下搜索发现网站用woff或者woff2字体在手机端和电脑都能完美显示&lt;/p&gt;
&lt;div class=&quot;note no-icon</summary>
<category term="建站手札" scheme="https://blog.biss.click/categories/website/"/>
<category term="网站" scheme="https://blog.biss.click/tags/web/"/>
</entry>
<entry>
<title>在侧边栏添加日历和倒计时</title>
<link href="https://blog.biss.click/posts/5ed2f1e6/"/>
<id>https://blog.biss.click/posts/5ed2f1e6/</id>
<published>2026-01-15T23:47:07.000Z</published>
<updated>2026-02-18T07:25:26.762Z</updated>
<summary type="html">&lt;p&gt;突然看到某个网站侧边栏有日历和倒计时,就研究了一下,抄下来了(🤭)&lt;br&gt;效果图:&lt;/p&gt;
&lt;center&gt;&lt;img src=&quot;https://pic.biss.click/image/b9819d72-3496-4204-9b7d-07d82e2e2f1b.webp&quot;</summary>
<category term="建站手札" scheme="https://blog.biss.click/categories/website/"/>
<category term="网站" scheme="https://blog.biss.click/tags/web/"/>
</entry>
<entry>
<title>和朋友们的故事</title>
<link href="https://blog.biss.click/posts/a31d95d9/"/>
<id>https://blog.biss.click/posts/a31d95d9/</id>
<published>2026-01-05T05:14:16.000Z</published>
<updated>2026-02-18T07:25:26.766Z</updated>
<summary type="html">&lt;p&gt;最近一直忙于学习(&lt;del&gt;虽然也没怎么好好学&lt;/del&gt;),所以一直忘了网站的更新;现在闲下来了,又不知道该写点什么;就写写我和朋友们的故事吧。&lt;br&gt;因为一次偶然的突发奇想,解锁了挂号信这种奇奇怪怪的通信方式,虽然现在网络发达,但感觉这种方式有一种独特的魅力。所以从去年</summary>
<category term="杂谈" scheme="https://blog.biss.click/categories/miscellaneous/"/>
<category term="生活" scheme="https://blog.biss.click/tags/life/"/>
</entry>
<entry>
<title>Adguard和Openclash共存</title>
<link href="https://blog.biss.click/posts/66e66374/"/>
<id>https://blog.biss.click/posts/66e66374/</id>
<published>2025-10-05T07:18:00.000Z</published>
<updated>2026-02-18T07:25:26.766Z</updated>
<summary type="html">&lt;p&gt;在之前已经安装了&lt;code&gt;Openwrt&lt;/code&gt;系统,并且也配置了&lt;code&gt;OpenClash&lt;/code&gt;&lt;code&gt;AdguardHome&lt;/code&gt;,两者的原理简单看都是劫持&lt;code&gt;DNS&lt;/code&gt;,所以两者要同时运行,必须经过一定的配置。&lt;br</summary>
<category term="技术" scheme="https://blog.biss.click/categories/technology/"/>
<category term="Openwrt" scheme="https://blog.biss.click/tags/Openwrt/"/>
<category term="AdguardHome" scheme="https://blog.biss.click/tags/AdguardHome/"/>
<category term="Openclash" scheme="https://blog.biss.click/tags/Openclash/"/>
</entry>
<entry>
<title>Adguard规则分享</title>
<link href="https://blog.biss.click/posts/69b16001/"/>
<id>https://blog.biss.click/posts/69b16001/</id>
<published>2025-10-05T04:18:00.000Z</published>
<updated>2026-02-18T07:25:26.766Z</updated>
<summary type="html">&lt;p&gt;众所周知,&lt;code&gt;Adguardhome&lt;/code&gt;是用于拦截广告的工具,搭配好的规则,拦截效果才会更好,下面来分享一些规则:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一个综合的过滤规则 &lt;div class=&#39;liushen-tag-link&#39;&gt;&lt;a</summary>
<category term="技术" scheme="https://blog.biss.click/categories/technology/"/>
<category term="Openwrt" scheme="https://blog.biss.click/tags/Openwrt/"/>
<category term="AdguardHome" scheme="https://blog.biss.click/tags/AdguardHome/"/>
</entry>
<entry>
<title>在Openwrt上安装AdguardHome</title>
<link href="https://blog.biss.click/posts/b57500e9/"/>
<id>https://blog.biss.click/posts/b57500e9/</id>
<published>2025-09-28T09:10:18.000Z</published>
<updated>2026-02-18T07:25:26.766Z</updated>
<summary type="html">&lt;p&gt;接续前言,&lt;code&gt;AdguardHome&lt;/code&gt;是一款广告拦截软件,有了一台小软路由后就开始折腾了。&lt;/p&gt;
&lt;h1 id=&quot;安装&quot;&gt;&lt;a href=&quot;#安装&quot; class=&quot;headerlink&quot;</summary>
<category term="技术" scheme="https://blog.biss.click/categories/technology/"/>
<category term="Openwrt" scheme="https://blog.biss.click/tags/Openwrt/"/>
<category term="AdguardHome" scheme="https://blog.biss.click/tags/AdguardHome/"/>
</entry>
<entry>
<title>在Openwrt中安装OpenClash</title>
<link href="https://blog.biss.click/posts/2b2fb1a7/"/>
<id>https://blog.biss.click/posts/2b2fb1a7/</id>
<published>2025-09-02T13:00:00.000Z</published>
<updated>2026-02-18T07:25:26.766Z</updated>
<summary type="html">&lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;因为一直想要实现从软路由上进行代理所以买到了一个Cudy Tr3000刷系统折腾。用这篇文章记录一下安装的过程。&lt;/p&gt;
&lt;h1</summary>
<category term="技术" scheme="https://blog.biss.click/categories/technology/"/>
<category term="Openwrt" scheme="https://blog.biss.click/tags/Openwrt/"/>
<category term="Openclash" scheme="https://blog.biss.click/tags/Openclash/"/>
</entry>
<entry>
<title>Cudy TR3000刷入Openwrt系统</title>
<link href="https://blog.biss.click/posts/8802e397/"/>
<id>https://blog.biss.click/posts/8802e397/</id>
<published>2025-08-31T12:15:00.000Z</published>
<updated>2026-02-18T07:25:26.766Z</updated>
<summary type="html">&lt;p&gt;最近搞到了一台&lt;code&gt;Cudy-TR3000&lt;/code&gt;路由器,用来进行科学上网(小猫咪),先记录一下刷写&lt;code&gt;Openwrt&lt;/code&gt;系统的过程。&lt;/p&gt;
&lt;h1 id=&quot;下载固件&quot;&gt;&lt;a href=&quot;#下载固件&quot; class=&quot;headerlink&quot;</summary>
<category term="技术" scheme="https://blog.biss.click/categories/technology/"/>
<category term="Cudy-TR3000" scheme="https://blog.biss.click/tags/Cudy-TR3000/"/>
</entry>
<entry>
<title>搭建Owncloud并集成Onlyoffice</title>
<link href="https://blog.biss.click/posts/59c8572a/"/>
<id>https://blog.biss.click/posts/59c8572a/</id>
<published>2025-08-26T06:27:00.000Z</published>
<updated>2026-02-18T07:25:26.762Z</updated>
<summary type="html">&lt;h1 id=&quot;引言&quot;&gt;&lt;a href=&quot;#引言&quot; class=&quot;headerlink&quot; title=&quot;引言&quot;&gt;&lt;/a&gt;引言&lt;/h1&gt;&lt;p&gt;因为正好有云存储的需求恰好又有服务器所以决定自建一个Owncloud网盘服务集成一个Onlyoffice。&lt;/p&gt;
&lt;h1</summary>
<category term="技术" scheme="https://blog.biss.click/categories/technology/"/>
<category term="网站" scheme="https://blog.biss.click/tags/web/"/>
</entry>
<entry>
<title>为博客添加CMS系统</title>
<link href="https://blog.biss.click/posts/3a26a97a/"/>
<id>https://blog.biss.click/posts/3a26a97a/</id>
<published>2025-08-25T14:03:00.000Z</published>
<updated>2025-08-26T00:01:00.000Z</updated>
<summary type="html">&lt;h1 id=&quot;序言&quot;&gt;&lt;a href=&quot;#序言&quot; class=&quot;headerlink&quot;</summary>
<category term="建站手札" scheme="https://blog.biss.click/categories/website/"/>
<category term="网站" scheme="https://blog.biss.click/tags/web/"/>
</entry>
<entry>
<title>为1Panel添加自己想要的应用</title>
<link href="https://blog.biss.click/posts/bb18d851/"/>
<id>https://blog.biss.click/posts/bb18d851/</id>
<published>2025-08-19T02:43:16.000Z</published>
<updated>2026-02-18T07:25:26.762Z</updated>
<summary type="html">&lt;p&gt;记录一下自己为1Panel贡献自己应用的经历&lt;/p&gt;
&lt;h1 id=&quot;预准备&quot;&gt;&lt;a href=&quot;#预准备&quot; class=&quot;headerlink&quot; title=&quot;预准备&quot;&gt;&lt;/a&gt;预准备&lt;/h1&gt;&lt;ol&gt;
&lt;li&gt;Fork仓库 &lt;div</summary>
<category term="技术" scheme="https://blog.biss.click/categories/technology/"/>
<category term="1panel" scheme="https://blog.biss.click/tags/1panel/"/>
</entry>
<entry>
<title>使用GitHub推送Hexo到服务器</title>
<link href="https://blog.biss.click/posts/ce1ec3fe/"/>
<id>https://blog.biss.click/posts/ce1ec3fe/</id>
<published>2025-08-18T09:10:18.000Z</published>
<updated>2026-02-18T07:25:26.766Z</updated>
<summary type="html">&lt;p&gt;1panel没有宝塔的webhook功能之前一直使用在服务器上建立裸仓库直接推送到服务器的方法。现在找到一种新的方法。&lt;/p&gt;
&lt;h1 id=&quot;建立GitHub仓库&quot;&gt;&lt;a href=&quot;#建立GitHub仓库&quot; class=&quot;headerlink&quot;</summary>
<category term="建站手札" scheme="https://blog.biss.click/categories/website/"/>
<category term="网站" scheme="https://blog.biss.click/tags/web/"/>
</entry>
<entry>
<title>C# 基本语法</title>
<link href="https://blog.biss.click/posts/e9a8e898/"/>
<id>https://blog.biss.click/posts/e9a8e898/</id>
<published>2025-08-16T10:01:10.000Z</published>
<updated>2026-02-18T07:25:26.762Z</updated>
<summary type="html">&lt;h1 id=&quot;简介&quot;&gt;&lt;a href=&quot;#简介&quot; class=&quot;headerlink&quot; title=&quot;简介&quot;&gt;&lt;/a&gt;简介&lt;/h1&gt;&lt;p&gt;C#</summary>
<category term="学习" scheme="https://blog.biss.click/categories/learning/"/>
<category term="C#" scheme="https://blog.biss.click/tags/C/"/>
</entry>
<entry>
<title>C# 入门</title>
<link href="https://blog.biss.click/posts/bc56f789/"/>
<id>https://blog.biss.click/posts/bc56f789/</id>
<published>2025-08-15T01:56:17.000Z</published>
<updated>2026-02-18T07:25:26.762Z</updated>
<summary type="html">&lt;p&gt;今天开始学习C#&lt;/p&gt;
&lt;h1 id=&quot;环境安装&quot;&gt;&lt;a href=&quot;#环境安装&quot; class=&quot;headerlink&quot; title=&quot;环境安装&quot;&gt;&lt;/a&gt;环境安装&lt;/h1&gt;&lt;p&gt;下载Visual Studio,社区版就可以&lt;/p&gt;
&lt;div</summary>
<category term="学习" scheme="https://blog.biss.click/categories/learning/"/>
<category term="C#" scheme="https://blog.biss.click/tags/C/"/>
</entry>
<entry>
<title>自定义页脚(新)</title>
<link href="https://blog.biss.click/posts/c6143ad3/"/>
<id>https://blog.biss.click/posts/c6143ad3/</id>
<published>2025-08-14T09:05:27.000Z</published>
<updated>2026-02-18T07:25:26.762Z</updated>
<summary type="html">&lt;h1 id=&quot;添加CSS&quot;&gt;&lt;a href=&quot;#添加CSS&quot; class=&quot;headerlink&quot; title=&quot;添加CSS&quot;&gt;&lt;/a&gt;添加CSS&lt;/h1&gt;&lt;div class=&quot;note warning 1</summary>
<category term="建站手札" scheme="https://blog.biss.click/categories/website/"/>
<category term="网站" scheme="https://blog.biss.click/tags/web/"/>
</entry>
</feed>

487
categories/index.html Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

441
cc/index.html Normal file

File diff suppressed because one or more lines are too long

495
cookie/index.html Normal file

File diff suppressed because one or more lines are too long

222
css/calendar.css Normal file
View File

@@ -0,0 +1,222 @@
/* 浅色主题变量覆盖 -------------------------------- */
:root {
--main: #a0d2eb; /* 主强调色:柔和天空蓝 */
--main-op: rgba(160, 210, 235, 0.6);
--main-op-deep: rgba(160, 210, 235, 0.4);
--main-op-light: rgba(160, 210, 235, 0.2);
--card-bg: #fdfdfd; /* 卡片背景:几乎白 */
--fontcolor: #444; /* 主文本:深灰 */
--secondtext: #999; /* 次级文本:浅灰 */
}
.card-widget {
padding: 10px!important;
max-height: calc(100vh - 100px);
}
.card-times a, .card-times div {
color: var(--fontcolor);
}
#card-widget-calendar .item-content {
display: flex;
}
#calendar-area-left {
width: 45%;
}
#calendar-area-right {
width: 55%;
}
#calendar-area-left, #calendar-area-right {
height: 100%;
padding: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
}
#calendar-main {
width: 100%;
}
#calendar-week {
height: 1.2rem;
font-size: 14px;
letter-spacing: 1px;
font-weight: 700;
align-items: center;
display: flex;
}
#calendar-date {
height: 3rem;
line-height: 1.3;
font-size: 64px;
letter-spacing: 3px;
color: var(--main);
font-weight: 700;
align-items: center;
display: flex;
position: relative;
top: calc(50% - 2.1rem);
}
#calendar-lunar, #calendar-solar {
height: 1rem;
font-size: 12px;
align-items: center;
display: flex;
position: absolute;
}
#calendar-solar {
bottom: 2.1rem;
}
#calendar-lunar {
bottom: 1rem;
color: var(--secondtext);
}
#calendar-main a {
height: 1rem;
width: 1rem;
border-radius: 50%;
font-size: 12px;
line-height: 12px;
display: flex;
justify-content: center;
align-items: center;
}
#calendar-main a.now {
background: var(--main);
color: var(--card-bg);
}
#calendar-main .calendar-rh a {
color: var(--secondtext);
}
.calendar-r0, .calendar-r1, .calendar-r2, .calendar-r3, .calendar-r4, .calendar-r5, .calendar-rh {
height: 1.2rem;
display: flex;
}
.calendar-d0, .calendar-d1, .calendar-d2, .calendar-d3, .calendar-d4, .calendar-d5, .calendar-d6 {
width: calc(100% / 7);
display: flex;
justify-content: center;
align-items: center;
}
#card-widget-schedule .item-content {
display: flex;
}
#schedule-area-left, #schedule-area-right {
height: 100px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
#schedule-area-left {
width: 30%;
}
#schedule-area-right {
width: 70%;
padding: 0 5px;
}
.schedule-r0, .schedule-r1, .schedule-r2 {
height: 2rem;
width: 100%;
align-items: center;
display: flex;
}
.schedule-d0 {
width: 30px;
margin-right: 5px;
text-align: center;
font-size: 12px;
}
.schedule-d1 {
width: calc(100% - 35px);
height: 1.5rem;
align-items: center;
display: flex;
}
progress::-webkit-progress-bar {
background: linear-gradient(to right, var(--main-op-deep), var(--main-op), var(--main-op-light));
border-radius: 5px;
overflow: hidden;
}
progress::-webkit-progress-value {
background: var(--main);
border-radius: 5px;
}
.aside-span1, .aside-span2 {
height: 1rem;
font-size: 12px;
z-index: 1;
display: flex;
align-items: center;
position: absolute;
}
.aside-span1 {
margin-left: 5px;
}
.aside-span2 {
right: 20px;
color: var(--secondtext);
}
.aside-span2 a {
margin: 0 3px;
}
#pBar_month, #pBar_week, #pBar_year {
width: 100%;
border-radius: 5px;
height: 100%;
}
#schedule-date, #schedule-days, #schedule-title {
display: flex;
align-items: center;
}
#schedule-title {
height: 25px;
line-height: 1;
font-size: 14px;
font-weight: 700;
}
#schedule-days {
height: 40px;
line-height: 1;
font-size: 30px;
font-weight: 900;
color: var(--main);
}
#schedule-date {
height: 20px;
line-height: 1;
font-size: 12px;
color: var(--secondtext);
}

8804
css/index.css Normal file

File diff suppressed because it is too large Load Diff

203
css/nav.css Normal file
View File

@@ -0,0 +1,203 @@
#nav-right{
flex:1 1 auto;
justify-content: flex-end;
margin-left: auto;
display: flex;
flex-wrap:nowrap;
}
/* 导航栏居中 */
#sidebar #sidebar-menus .menus_items .menus_item {
margin: 10px 0;
}
#sidebar #sidebar-menus .menus_items a.site-page {
padding-left: 0;
}
#sidebar #sidebar-menus .menus_items .site-page {
position: relative;
display: block;
padding: 6px 30px 6px 22px;
color: var(--font-color);
font-size: 1.15em;
border: var(--style-border-always);
background: var(--icat-card-bg);
font-size: 14px;
border-radius: 12px;
}
#sidebar #sidebar-menus .menus_items .site-page i:first-child {
text-align: left;
padding-left: 10px;
}
#nav #menus {
display: flex;
justify-content: center;
width: 100%;
position: absolute;
left: 0;
margin: 0;
transform: translateZ(0);
}
#nav #blog-info {
flex-wrap: nowrap;
display: flex;
align-items: center;
z-index: 102;
max-width: fit-content;
}
@media screen and (max-width: 900px) {
#nav {
padding: 0 15px;
}
#nav-group {
padding: 0 0.2rem;
}
#rightside {
right: -42px;
}
}
/* IPAD菜单栏调整 */
/* 1. 容器溢出穿透 */
#nav, #blog-info, #nav-group {
overflow: visible !important;
}
#ls-menu-container {
position: relative !important;
display: inline-flex !important;
align-items: center;
height: 100%;
padding: 0 15px;
cursor: pointer !important;
z-index: 2000 !important;
}
/* 2. 悬浮面板:宽大且高不透明度 */
#ls-menu-panel {
position: absolute !important;
top: 100% !important;
left: 0 !important;
margin-top: 15px !important;
width: 420px;
padding: 24px;
border-radius: 18px;
/* 高不透明度磨砂玻璃 (0.9) */
background: rgba(255, 255, 255, 0.95) !important;
backdrop-filter: blur(20px) saturate(180%) !important;
-webkit-backdrop-filter: blur(20px) saturate(180%) !important;
border: 1px solid rgba(255, 255, 255, 0.5) !important;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.18) !important;
opacity: 0 !important;
visibility: hidden !important;
transform: translateY(12px) !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
z-index: 999999 !important;
cursor: default;
}
/* 3. 透明感应桥梁 */
#ls-menu-panel::before {
content: "";
position: absolute;
top: -20px;
left: 0;
right: 0;
height: 25px;
background: transparent !important;
}
/* 4. 触发效果 */
#ls-menu-container:hover #ls-menu-panel {
opacity: 1 !important;
visibility: visible !important;
transform: translateY(0) !important;
}
#ls-menu-container:hover i.fa-fingerprint {
color: #49b1f5;
transform: scale(1.1);
}
/* 5. 内部网格:保持两列 */
.ls-section { margin-bottom: 22px; }
.ls-section:last-child { margin-bottom: 0; }
.ls-title {
color: #333;
font-weight: 800;
font-size: 15px;
margin-bottom: 12px;
opacity: 0.9;
}
.ls-grid {
display: grid !important;
grid-template-columns: repeat(2, 1fr); /* 恢复为两列 */
gap: 10px;
}
.ls-grid a {
color: #444 !important;
font-size: 14px !important;
padding: 10px 12px;
border-radius: 12px;
display: flex;
align-items: center;
transition: all 0.2s ease;
background: rgba(0, 0, 0, 0.02);
}
.ls-grid a:hover {
background: #49b1f5 !important;
color: #fff !important;
transform: translateX(4px); /* 悬停时轻微右移,增加动感 */
}
.ls-grid a i {
margin-right: 12px;
width: 20px;
text-align: center;
font-size: 15px;
}
/* 6. 深色模式适配 */
[data-theme='dark'] #ls-menu-panel {
background: rgba(30, 30, 30, 0.95) !important;
border-color: rgba(255, 255, 255, 0.1) !important;
}
[data-theme='dark'] .ls-title { color: #eee; }
[data-theme='dark'] .ls-grid a {
color: #ccc !important;
background: rgba(255, 255, 255, 0.05);
}
#nav.show-title .nav-page-title {
display: flex !important;
opacity: 1 !important;
visibility: visible !important;
position: absolute !important;
left: 50% !important;
transform: translateX(-50%) !important;
white-space: nowrap !important;
z-index: 1000 !important;
}
#nav.show-title #menus,
#nav.show-title .nav-site-title {
opacity: 0 !important;
visibility: hidden !important;
pointer-events: none !important;
}
#nav .nav-page-title {
display: none;
transition: opacity 0.3s ease;
}
#nav #blog-info {
overflow: visible !important;
}

67
css/poem.css Normal file
View File

@@ -0,0 +1,67 @@
[data-theme=light] {
--fontcolor: #363636;
--text: rgba(60, 60, 67, 0.6);
--bg: #edf0f7;
}
[data-theme=dark] {
--fontcolor: #F7F7FA;
--text: #a1a2b8;
--bg: #30343f;
}
div#poem_sentence{
text-align: center;
font-family: serif,cursive;
line-height: 1.4;
margin-bottom: 0.5rem;
padding: 1rem;
border-radius: 12px;
background: var(--bg);
min-height: 62px;
}
div#poem_info{
display: flex;
color: var(--text);
font-size: 100%;
justify-content: center;
flex-wrap: wrap;
}
div#poem_author{
order: 1;
padding: 2px;
margin-left: 8px;
}
div#poem_dynasty{
order: 0;
padding: 2px 4px 2px 6px;
background: var(-btn-bg);
color: var(--fontcolor);
border-radius: 8px;
}
#card-poem{
display: flex;
flex-direction: column;
padding: 0.5rem!important;
min-height: 130px;
background: linear-gradient(-45deg, rgba(255, 255, 255, .7),
rgba(255, 255, 255, .8),
rgba(255, 255, 255, .8),
rgba(255, 255, 255, .7))!important;
backdrop-filter: blur(10px);
}
[data-theme=dark] #card-poem{
display: flex;
flex-direction: column;
padding: 0.5rem!important;
min-height: 130px;
background: linear-gradient(-45deg, rgba(24, 40, 72, .7),
rgba(35, 37, 58, .8),
rgba(35, 37, 58, .8),
rgba(24, 40, 72, .7))!important;
backdrop-filter: blur(10px);
}

300
css/shuoshuo.css Normal file
View File

@@ -0,0 +1,300 @@
/* ================= 主题变量 ================= */
:root {
--card-bg: #fff;
--card-border: 1px solid #e3e8f7;
--card-box-shadow: 0 3px 8px 6px rgba(7,17,27,0.09);
--card-hover-box-shadow: 0 3px 8px 6px rgba(7,17,27,0.2);
--card-secondbg: #f1f3f8;
--button-hover-bg: #2679cc;
--text: #4c4948;
--button-bg: #f1f3f8;
--fancybox-bg: rgba(255,255,255,0.5);
}
[data-theme=dark] {
--card-bg: #181818;
--card-secondbg: #30343f;
--card-border: 1px solid #42444a;
--card-box-shadow: 0 3px 8px 6px rgba(7,17,27,0.09);
--card-hover-box-shadow: 0 3px 8px 6px rgba(7,17,27,0.2);
--button-bg: #30343f;
--button-hover-bg: #2679cc;
--text: rgba(255,255,255,0.702);
--fancybox-bg: rgba(0,0,0,0.5);
}
/* ================= 卡片布局 ================= */
#talk {
position: relative;
width: 100%;
box-sizing: border-box;
display: flex;
flex-wrap: wrap; /* 允许换行 */
align-items: flex-start; /* 防止高度拉伸导致变形 */
}
#talk .talk_item {
width: calc(33.333% - 6px);
background: var(--card-bg);
border: var(--card-border);
box-shadow: var(--card-box-shadow);
border-radius: 12px;
padding: 20px;
margin-right: 9px;
margin-bottom: 9px;
display: flex;
flex-direction: column;
transition: box-shadow .3s ease-in-out;
}
#talk .talk_item:hover {
box-shadow: var(--card-hover-box-shadow);
}
@media (max-width: 900px) {
#talk .talk_item {
width: calc(50% - 5px);
}
}
@media (max-width: 450px) {
#talk .talk_item {
width: 100%;
}
}
/* 给 Meting 播放器预留约 80px 的固定高度 */
meting-js {
display: block;
min-height: 100px;
margin-top: 10px;
}
/* 如果有视频,也预留一下比例高度 */
.talk_content iframe {
min-height: 150px;
}
/* ================= 头部信息 ================= */
#talk .talk_meta,
#talk .talk_bottom {
display: flex;
align-items: center;
}
#talk .talk_meta {
width: 100%;
padding-bottom: 10px;
border-bottom: 1px dashed grey;
}
#talk .talk_meta .avatar {
margin: 0 !important;
width: 60px;
height: 60px;
border-radius: 12px;
}
#talk .talk_meta .info {
display: flex;
flex-direction: column;
margin-left: 10px;
}
#talk .talk_meta .info .talk_nick {
color: #6dbdc3;
font-size: 1.2rem;
}
#talk .talk_meta .info svg.is-badge.icon {
width: 15px;
padding-top: 3px;
}
#talk .talk_meta .info span.talk_date {
opacity: .6;
}
/* ================= 内容区域 ================= */
#talk .talk_item .talk_content {
margin-top: 10px;
}
#talk .talk_item .talk_content .zone_imgbox {
display: flex;
flex-wrap: wrap;
--w: calc(25% - 8px);
gap: 10px;
margin-top: 10px;
}
#talk .talk_item .talk_content .zone_imgbox a {
display: block;
border-radius: 12px;
width: var(--w);
aspect-ratio: 1/1;
position: relative;
}
#talk .talk_item .talk_content .zone_imgbox a:first-child {
width: 100%;
aspect-ratio: 1.8;
}
#talk .talk_item .talk_content .zone_imgbox img {
border-radius: 10px;
width: 100%;
height: 100%;
margin: 0 !important;
object-fit: cover;
}
/* ================= 底部信息 ================= */
#talk .talk_item .talk_bottom {
margin-top: 15px;
padding-top: 10px;
border-top: 1px dashed grey;
justify-content: space-between;
opacity: .9;
}
#talk .talk_item .talk_bottom .icon {
float: right;
transition: all .3s;
}
#talk .talk_item .talk_bottom .icon:hover {
color: #49b1f5;
}
#talk .talk_item .talk_bottom span.talk_tag,
#talk .talk_item .talk_bottom span.location_tag {
font-size: 14px;
background-color: var(--card-secondbg);
border-radius: 12px;
padding: 3px 15px 3px 10px;
transition: box-shadow 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
#talk .talk_item .talk_bottom span.location_tag {
margin-left: 5px;
}
#talk .talk_item .talk_bottom span.talk_tag:hover,
#talk .talk_item .talk_bottom span.location_tag:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
/* ================= 文本链接 ================= */
#talk .talk_item .talk_content > a {
margin: 0 3px;
color: #ff7d73 !important;
}
#talk .talk_item .talk_content > a:hover {
text-decoration: none !important;
color: #ff5143 !important;
}
/* ================= 响应式 ================= */
@media screen and (max-width: 900px) {
#talk .talk_item .talk_content .zone_imgbox {
--w: calc(33% - 5px);
}
#talk .talk_item #post-comment {
margin: 0 3px;
}
}
@media screen and (max-width: 768px) {
.zone_imgbox {
gap: 6px;
--w: calc(50% - 3px);
}
span.talk_date {
font-size: 14px;
}
}
/* ================= 豆瓣卡片 ================= */
#talk .talk_item .talk_content .douban-card {
margin-top: 10px !important;
text-decoration: none;
align-items: center;
border-radius: 12px;
color: #faebd7;
display: flex;
justify-content: center;
margin: 10px;
max-width: 400px;
overflow: hidden;
padding: 15px;
position: relative;
}
/* ================= 外链卡片 ================= */
#talk .talk_item .talk_content .shuoshuo-external-link {
width: 100%;
height: 80px;
margin-top: 10px;
border-radius: 12px;
background-color: var(--card-secondbg);
color: var(--card-text);
border: var(--card-border);
transition: background-color .3s ease-in-out;
}
.shuoshuo-external-link:hover {
background-color: var(--button-hover-bg);
}
.shuoshuo-external-link .external-link {
display: flex;
color: var(--text) !important;
width: 100%;
height: 100%;
}
.shuoshuo-external-link .external-link:hover {
color: white !important;
text-decoration: none !important;
}
.shuoshuo-external-link .external-link-left {
width: 60px;
height: 60px;
margin: 10px;
border-radius: 12px;
background-size: cover;
background-position: center;
}
.shuoshuo-external-link .external-link-right {
display: flex;
flex-direction: column;
justify-content: center;
width: calc(100% - 80px);
padding: 10px;
}
.shuoshuo-external-link .external-link-right .external-link-title {
font-size: 1.0rem;
font-weight: 800;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.shuoshuo-external-link .external-link-right i {
margin-left: 5px;
}
/* ================= 结尾 ================= */
.limit {
width: 100%;
text-align: center;
margin-top: 30px;
}

140
css/shuoshuoshouye.css Normal file
View File

@@ -0,0 +1,140 @@
/* maintop */
#main_top {
display: flex;
justify-content: center;
z-index: 1;
max-width: 1400px;
margin: 20px auto;
width: 100%;
padding: 0 15px;
margin-top: 40px;
margin-bottom: 0px;
}
.hide-aside #main_top {
width: 80%;
}
.hide-aside #main_top #bber-talk {
max-width: 936px;
}
@media screen and (min-width: 2000px) {
.hide-aside #main_top #bber-talk {
max-width: 80%;
}
#main_top {
max-width: 70%;
}
}
@media screen and (max-width: 1210px) {
.hide-aside #main_top {
padding: 0 12px;
}
}
@media screen and (max-width: 900px) {
.hide-aside #main_top {
width: 100%;
padding: 0 15px;
}
}
@media screen and (max-width: 768px) {
.hide-aside #main_top {
padding: 0 5px;
}
div#main_top {
margin-top: 20px;
padding: 0 5px;
}
}
/* 重点修改bber-talk 的磨砂玻璃背景 */
#bber-talk {
/* 统一磨砂玻璃效果 */
background: linear-gradient(-45deg, rgba(255, 255, 255, .7), rgba(255, 255, 255, .8), rgba(255, 255, 255, .8), rgba(255, 255, 255, .7)) !important;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 12px; /* 给哔哔栏加一点圆角,更符合玻璃质感 */
border: 1px solid rgba(255, 255, 255, 0.3); /* 增加边缘高光 */
box-sizing: border-box;
cursor: pointer;
width: 100%;
min-height: 50px;
padding: .5rem 1rem;
display: flex;
align-items: center;
overflow: hidden;
font-weight: 700;
transition: all .3s ease-in-out;
}
/* 暗黑模式适配 */
[data-theme=dark] #bber-talk {
background: linear-gradient(-45deg, rgba(24, 40, 72, .7), rgba(35, 37, 58, .8), rgba(35, 37, 58, .8), rgba(24, 40, 72, .7)) !important;
border: 1px solid rgba(255, 255, 255, 0.1);
}
#bber-talk:hover {
transform: translateY(-2px); /* 悬停微动,增加交互感 */
box-shadow: 0 8px 20px rgba(0,0,0,0.1);
}
#bber-talk,
#bber-talk a {
color: var(--font-color);
}
#bber-talk svg.icon {
width: 1em;
height: 1em;
vertical-align: -.15em;
fill: currentColor;
overflow: hidden;
font-size: 20px;
}
#bber-talk .item i {
margin-left: 5px;
}
#bber-talk > i {
font-size: 1.1rem;
}
#bber-talk .talk-list {
flex: 1;
max-height: 32px;
font-size: 16px;
padding: 0;
margin: 0;
overflow: hidden;
}
#bber-talk .talk-list:hover {
color: var(--default-bg-color);
transition: all .2s ease-in-out;
}
#bber-talk .talk-list li {
list-style: none;
width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
margin-left: 10px;
}
@media screen and (min-width: 770px) {
#bber-talk .talk-list {
text-align: center;
margin-right: 20px;
}
}

699
css/style.css Normal file
View File

@@ -0,0 +1,699 @@
/* #card-newest-comments img {border-radius: 10px; /* 设置最新评论圆角半径为10px可以根据需要调整} */
/* 侧边栏个人信息卡片动态渐变色 */
#aside-content>.card-widget.card-info {
position: relative;
z-index: 1; /* 确保原内容在上层显示 */
background: linear-gradient(-45deg, rgba(255, 255, 255, .7),
rgba(255, 255, 255, .8),
rgba(255, 255, 255, .8),
rgba(255, 255, 255, .7));
backdrop-filter: blur(10px);
/* -webkit-backdrop-filter: blur(10px);兼容性前缀,适用于一些旧版本的浏览器 */
}
#aside-content > .card-widget.card-info::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(to bottom,
rgba(255, 255, 255, 0.0),
rgba(255, 255, 255, 0.3),
rgba(255, 255, 255, 0.6),
rgba(255, 255, 255, 0.7)); /* 鼠标悬停时显示的背景图片 */
background-size: cover;
background-repeat: no-repeat;
opacity: 0.5;
transition: opacity 0.8s; /* 过渡效果持续时间 */
z-index: -1;
}
[data-theme=dark] #aside-content > .card-widget.card-info::before {
background: linear-gradient(to bottom,
rgba(24, 40, 72, .1),
rgba(35, 37, 58, .3),
rgba(35, 37, 58, .6),
rgba(24, 40, 72, .7)); /* 鼠标悬停时显示的背景图片 */
background-size: cover;
background-repeat: no-repeat;
}
#aside-content > .card-widget.card-info:hover::before {
opacity: 1;
}
/* 卡片动态效果,好好看哈哈 */
#aside-content div.card-widget.card-info .avatar-img {
transition: opacity 0.3s ease; /* 添加渐变效果 */
}
#aside-content div.card-widget.card-info .avatar-img:hover {
opacity: 0; /* 鼠标移上去时完全透明 */
}
#card-info-btn:hover, #card-info-btn:hover .icon, #card-info-btn:hover span {
transform: scale(1.1); /* 鼠标悬停时放大到110% */
}
[data-theme=dark] #aside-content>.card-widget.card-info {
background: linear-gradient(-45deg, rgba(24, 40, 72, .6),
rgba(35, 37, 58, .5),
rgba(35, 37, 58, .5),
rgba(24, 40, 72, .6));
backdrop-filter: blur(10px);
/* -webkit-backdrop-filter: blur(10px);兼容性前缀,适用于一些旧版本的浏览器 */
}
/* 侧边栏公告栏卡片渐变色 */
#aside-content>.card-widget.card-announcement {
background: linear-gradient(-45deg, rgba(255, 255, 255, .7),
rgba(255, 255, 255, .8),
rgba(255, 255, 255, .8),
rgba(255, 255, 255, .7));
backdrop-filter: blur(10px);
/* -webkit-backdrop-filter: blur(10px);兼容性前缀,适用于一些旧版本的浏览器 */
}
[data-theme=dark] #aside-content>.card-widget.card-announcement {
background: linear-gradient(-45deg, rgba(24, 40, 72, .7),
rgba(35, 37, 58, .8),
rgba(35, 37, 58, .8),
rgba(24, 40, 72, .7));
backdrop-filter: blur(10px);
/* -webkit-backdrop-filter: blur(10px);兼容性前缀,适用于一些旧版本的浏览器 */
}
/* 侧边栏目录最新文章卡片渐变色 */
#aside-content>.sticky_layout>.card-widget {
background: linear-gradient(-45deg, rgba(255, 255, 255, .7),
rgba(255, 255, 255, .8),
rgba(255, 255, 255, .8),
rgba(255, 255, 255, .7));
backdrop-filter: blur(10px);
/* -webkit-backdrop-filter: blur(10px);兼容性前缀,适用于一些旧版本的浏览器 */
}
[data-theme=dark] #aside-content>.sticky_layout>.card-widget {
background: linear-gradient(-45deg, rgba(24, 40, 72, .7),
rgba(35, 37, 58, .8),
rgba(35, 37, 58, .8),
rgba(24, 40, 72, .7));
backdrop-filter: blur(10px);
/* -webkit-backdrop-filter: blur(10px);兼容性前缀,适用于一些旧版本的浏览器 */
}
/* 侧边栏欢迎卡片渐变色 */
#aside-content>.card-widget.welcomeBoxClass {
background: linear-gradient(-45deg, rgba(255, 255, 255, .7),
rgba(255, 255, 255, .8),
rgba(255, 255, 255, .8),
rgba(255, 255, 255, .7));
backdrop-filter: blur(10px);
/* -webkit-backdrop-filter: blur(10px);兼容性前缀,适用于一些旧版本的浏览器 */
}
[data-theme=dark] #aside-content>.card-widget.welcomeBoxClass {
background: linear-gradient(-45deg, rgba(24, 40, 72, .7),
rgba(35, 37, 58, .8),
rgba(35, 37, 58, .8),
rgba(24, 40, 72, .7));
backdrop-filter: blur(10px);
/* -webkit-backdrop-filter: blur(10px);兼容性前缀,适用于一些旧版本的浏览器 */
}
/* 公告卡片渐变色 */
#recent-posts>#noticeList {
background: linear-gradient(-45deg, rgba(255, 255, 255, .7),
rgba(255, 255, 255, .8),
rgba(255, 255, 255, .8),
rgba(255, 255, 255, .7));
backdrop-filter: blur(10px);
/* -webkit-backdrop-filter: blur(10px);兼容性前缀,适用于一些旧版本的浏览器 */
}
[data-theme=dark] #recent-posts>#noticeList {
background: linear-gradient(-45deg, rgba(24, 40, 72, .7),
rgba(35, 37, 58, .8),
rgba(35, 37, 58, .8),
rgba(24, 40, 72, .7));
backdrop-filter: blur(10px);
/* -webkit-backdrop-filter: blur(10px);兼容性前缀,适用于一些旧版本的浏览器 */
}
/* 个人信息Follow me按钮 */
#aside-content>.card-widget.card-info>#card-info-btn {
background-color: #3eb8be;
border-radius: 10px;
}
/*文章页面*/
.layout>#post {
background: linear-gradient(-45deg, rgba(255, 255, 255, .7),
rgba(255, 255, 255, .8),
rgba(255, 255, 255, .8),
rgba(255, 255, 255, .7));
backdrop-filter: blur(10px);
/* -webkit-backdrop-filter: blur(10px);兼容性前缀,适用于一些旧版本的浏览器 */
}
[data-theme=dark] .layout>#post {
background: linear-gradient(-45deg, rgba(24, 40, 72, .7),
rgba(35, 37, 58, .8),
rgba(35, 37, 58, .8),
rgba(24, 40, 72, .7));
backdrop-filter: blur(10px);
/* -webkit-backdrop-filter: blur(10px);兼容性前缀,适用于一些旧版本的浏览器 */
}
/*分类条*/
#recent-posts>.category-bar {
background: linear-gradient(-45deg, rgba(255, 255, 255, .7),
rgba(255, 255, 255, .8),
rgba(255, 255, 255, .8),
rgba(255, 255, 255, .7))!important;
backdrop-filter: blur(10px)!important;
/* -webkit-backdrop-filter: blur(10px);兼容性前缀,适用于一些旧版本的浏览器 */
}
[data-theme=dark] #recent-posts>.category-bar {
background: linear-gradient(-45deg, rgba(24, 40, 72, .7),
rgba(35, 37, 58, .8),
rgba(35, 37, 58, .8),
rgba(24, 40, 72, .7))!important;
backdrop-filter: blur(10px)!important;
/* -webkit-backdrop-filter: blur(10px);兼容性前缀,适用于一些旧版本的浏览器 */
}
/*主页文章预览页面*/
#recent-posts>.recent-post-items>.recent-post-item {
background: linear-gradient(-45deg, rgba(255, 255, 255, .7),
rgba(255, 255, 255, .8),
rgba(255, 255, 255, .8),
rgba(255, 255, 255, .7))!important;
backdrop-filter: blur(10px)!important;
/* -webkit-backdrop-filter: blur(10px);兼容性前缀,适用于一些旧版本的浏览器 */
}
[data-theme=dark] #recent-posts>.recent-post-items>.recent-post-item {
background: linear-gradient(-45deg, rgba(24, 40, 72, .7),
rgba(35, 37, 58, .8),
rgba(35, 37, 58, .8),
rgba(24, 40, 72, .7))!important;
backdrop-filter: blur(10px)!important;
/* -webkit-backdrop-filter: blur(10px);兼容性前缀,适用于一些旧版本的浏览器 */
}
/*分类页面*/
.layout>#page {
background: linear-gradient(-45deg, rgba(255, 255, 255, .7),
rgba(255, 255, 255, .8),
rgba(255, 255, 255, .8),
rgba(255, 255, 255, .7));
backdrop-filter: blur(10px);
/* -webkit-backdrop-filter: blur(10px); 兼容性前缀,适用于一些旧版本的浏览器 */
}
[data-theme=dark] .layout>#page {
background: linear-gradient(-45deg, rgba(24, 40, 72, .7),
rgba(35, 37, 58, .8),
rgba(35, 37, 58, .8),
rgba(24, 40, 72, .7));
backdrop-filter: blur(10px);
/* -webkit-backdrop-filter: blur(10px); 兼容性前缀,适用于一些旧版本的浏览器 */
}
/*时间轴页面*/
.layout>#archive {
background: linear-gradient(-45deg, rgba(255, 255, 255, .7),
rgba(255, 255, 255, .8),
rgba(255, 255, 255, .8),
rgba(255, 255, 255, .7));
backdrop-filter: blur(10px);
/* -webkit-backdrop-filter: blur(10px);兼容性前缀,适用于一些旧版本的浏览器 */
}
[data-theme=dark] .layout>#archive {
background: linear-gradient(-45deg, rgba(24, 40, 72, .7),
rgba(35, 37, 58, .8),
rgba(35, 37, 58, .8),
rgba(24, 40, 72, .7));
backdrop-filter: blur(10px);
/* -webkit-backdrop-filter: blur(10px);兼容性前缀,适用于一些旧版本的浏览器 */
}
/*分类点进去的页面*/
.layout>#category {
background: linear-gradient(-45deg, rgba(255, 255, 255, .7),
rgba(255, 255, 255, .8),
rgba(255, 255, 255, .8),
rgba(255, 255, 255, .7));
backdrop-filter: blur(10px);
/* -webkit-backdrop-filter: blur(10px);兼容性前缀,适用于一些旧版本的浏览器 */
}
[data-theme=dark] .layout>#category {
background: linear-gradient(-45deg, rgba(24, 40, 72, .7),
rgba(35, 37, 58, .8),
rgba(35, 37, 58, .8),
rgba(24, 40, 72, .7));
backdrop-filter: blur(10px);
/* -webkit-backdrop-filter: blur(10px);兼容性前缀,适用于一些旧版本的浏览器 */
}
/*标签点进去的页面*/
.layout>#tag {
background: linear-gradient(-45deg, rgba(255, 255, 255, .7),
rgba(255, 255, 255, .8),
rgba(255, 255, 255, .8),
rgba(255, 255, 255, .7));
backdrop-filter: blur(10px);
/* -webkit-backdrop-filter: blur(10px);兼容性前缀,适用于一些旧版本的浏览器 */
}
[data-theme=dark] .layout>#tag {
background: linear-gradient(-45deg, rgba(24, 40, 72, .7),
rgba(35, 37, 58, .8),
rgba(35, 37, 58, .8),
rgba(24, 40, 72, .7));
backdrop-filter: blur(10px);
/* -webkit-backdrop-filter: blur(10px);兼容性前缀,适用于一些旧版本的浏览器 */
}
.layout.hide-aside {
max-width: 1200px;
}
/* 设置黑夜的时候,社交按钮为白色 */
[data-theme=dark] .social-icon i {
color: rgba(188, 188, 188) !important; /* 设置为白色 */
}
.layout{
width: 100%;
max-width:1400px;
}
/* 重新定义宽度 */
.layout > div:first-child {
width: 100%;
}
#post {
width: 78%;
}
.aside-content {
max-width: 100%;
min-width: 300px;
}
.layout.hide-aside {
max-width: 1400px;
}
/* 定义是否侧边栏的宽度 */
#recent-posts > .recent-post-item {
height: 19em;
border: var(--style-border);
}
#recent-posts > .recent-post-item >.recent-post-info {
padding: 0 56px;
width: 64%;
}
@media screen and (max-width: 768px) {
#recent-posts > .recent-post-item {
height: auto;
}
#recent-posts > .recent-post-item >.recent-post-info {
padding: 20px 20px 30px;
width: 100%;
text-align: center;
}
}
/* 主页文章列表图片宽度 */
.loading-img {
background: url(https://free.picui.cn/free/2025/08/10/689845496a283.png) no-repeat center center;
background-size: cover;
}
#footer {
background-color: rgba(0, 0, 0, 0); /* 修改透明色 */
color: #fff;
padding: 0;
}
#footer .footer-other {
padding: 0;
}
.footer-copyright{
padding: 0;
}
.my-footer-logo {
color: white;
}
.my-footer-wave-svg {
height: 50px;
width: 100%;
transform: scale(-1, -1) translateY(-10px); /*;*/
}
.my-footer-wave-path {
fill: #177ecd;
}
.my-footer-content {
margin-left: auto;
margin-right: auto;
max-width: 1230px;
padding: 40px 15px 450px;
position: relative;
}
.my-footer-content-column {
box-sizing: border-box;
float: left;
padding-left: 15px;
padding-right: 15px;
width: 100%;
color: #fff;
}
.my-footer-content-column ul li a {
color: #fff;
text-decoration: none;
}
.my-footer-logo-link {
display: inline-block;
}
.my-footer-menu {
margin-top: 15px;
}
.my-footer-menu-name {
color: #fffff2;
font-size: 15px;
font-weight: 900;
letter-spacing: .1em;
line-height: 18px;
margin-bottom: 0;
margin-top: 0;
text-transform: uppercase;
}
.my-footer-menu-list {
list-style: none;
margin-bottom: 0;
margin-top: 10px;
padding-left: 0;
}
.my-footer-menu-list li {
margin-top: 5px;
}
.my-footer-call-to-action-description {
color: #fffff2;
margin-top: 10px;
margin-bottom: 20px;
}
.my-footer-call-to-action-button:hover {
background-color: #fffff2;
color: #00bef0;
}
.button:last-of-type {
margin-right: 0;
}
.my-footer-call-to-action-button {
background-color: #03708c;
border-radius: 21px;
color: #fffff2;
display: inline-block;
font-size: 11px;
font-weight: 900;
letter-spacing: .1em;
line-height: 18px;
padding: 12px 30px;
margin: 0 10px 10px 0;
text-decoration: none;
text-transform: uppercase;
transition: background-color .2s;
cursor: pointer;
position: relative;
}
.my-footer-call-to-action {
margin-top: 17px;
}
.my-footer-call-to-action-title {
color: #fffff2;
font-size: 14px;
font-weight: 900;
letter-spacing: .1em;
line-height: 18px;
margin-bottom: 0;
margin-top: 0;
text-transform: uppercase;
}
.my-footer-call-to-action-link-wrapper {
margin-bottom: 0;
margin-top: 10px;
color: #fff;
text-decoration: none;
}
.my-footer-call-to-action-link-wrapper a {
color: #fff;
text-decoration: none;
}
.my-footer-social-links {
bottom: -1px;
height: 54px;
position: absolute;
right: 0;
width: 236px;
}
.my-footer-social-amoeba-svg {
height: 54px;
left: 0;
display: block;
position: absolute;
top: 0;
width: 236px;
}
.my-footer-social-amoeba-path {
fill: #03708c;
}
.my-footer-social-link.email {
height: 41px;
left: 5px;
top: 14px;
width: 41px;
}
.my-footer-social-link {
display: block;
padding: 10px;
position: absolute;
}
.hidden-link-text {
position: absolute;
clip: rect(1px 1px 1px 1px);
clip: rect(1px, 1px, 1px, 1px);
-webkit-clip-path: inset(0px 0px 99.9% 99.9%);
clip-path: inset(0px 0px 99.9% 99.9%);
overflow: hidden;
height: 1px;
width: 1px;
padding: 0;
border: 0;
top: 50%;
}
.my-footer-social-icon-svg {
display: block;
}
.my-footer-social-icon-path {
fill: #fffff2;
transition: fill .2s;
}
.my-footer-social-link.follow {
height: 42px;
left: 124px;
top: 13px;
width: 42px;
}
.my-footer-social-link.rss {
height: 43px;
left: 178px;
top: 10px;
width: 43px;
}
.my-footer-social-link.github {
height: 62px;
left: 54px;
top: -4px;
width: 62px;
}
.my-footer-copyright {
background-color: #03708c;
color: #fff;
padding: 15px 30px;
text-align: center;
}
.my-footer-copyright-wrapper {
margin-left: auto;
margin-right: auto;
max-width: 1200px;
}
.my-footer-copyright-text {
color: #fff;
font-size: 13px;
font-weight: 400;
line-height: 18px;
margin-bottom: 0;
margin-top: 0;
}
.my-footer-copyright-link {
color: #fff;
text-decoration: none;
}
.my-footer-content-div {
background: #177ecd
}
.my-footer-svg-div {
width: 100%;
max-height: 200px;
}
/* Media Query For different screens */
@media (min-width: 320px) and (max-width: 479px) {
/* smartphones, portrait iPhone, portrait 480x320 phones (Android) */
.my-footer-content {
margin-left: auto;
margin-right: auto;
max-width: 1230px;
padding: 40px 15px 649px;
position: relative;
}
/* 不展示logo */
.my-footer-logo {
display: none;
}
}
@media (min-width: 480px) and (max-width: 599px) {
/* smartphones, Android phones, landscape iPhone */
.my-footer-content {
margin-left: auto;
margin-right: auto;
max-width: 1230px;
padding: 40px 15px 738px;
position: relative;
}
.my-footer-logo {
padding-left: 177px; /* Qlogo 稍微偏移一点 */
padding-right: 170px;
}
}
@media (min-width: 600px) and (max-width: 800px) {
/* portrait tablets, portrait iPad, e-readers (Nook/Kindle), landscape 800x480 phones (Android) */
.my-footer-content {
margin-left: auto;
margin-right: auto;
max-width: 1230px;
padding: 40px 15px 758px;
position: relative;
}
}
@media (min-width: 801px) {
/* tablet, landscape iPad, lo-res laptops ands desktops */
}
@media (min-width: 1025px) {
/* big landscape tablets, laptops, and desktops */
}
@media (min-width: 1281px) {
/* hi-res laptops and desktops */
}
@media (min-width: 760px) {
.my-footer-content {
margin-left: auto;
margin-right: auto;
max-width: 1230px;
padding: 10px 15px 237px;
position: relative;
}
.my-footer-content-column {
/*五列的话 19.99 %*/
width: 24.99%;
}
}
@media (min-width: 568px) {
}
@media (min-width: 600px) and (max-width: 760px) {
/* 在这里编写适用于小屏幕的样式 */
.my-footer-logo {
padding-left: 212px; /* Qlogo 稍微偏移一点 */
padding-right: 204px;
}
}
/* 页脚波浪的颜色 */
[data-theme='dark'] .my-footer-wave-path {
fill: #0f3858;
}
[data-theme='dark'] .my-footer-content-div {
background-color: #0f3858;
}
/* 页脚海底的颜色 */
[data-theme='dark'] .my-footer-copyright {
background-color: #2b3f49;
}
[data-theme='dark'] .my-footer-social-amoeba-path {
fill: #2b3f49;
}

138
css/swiper.css Normal file
View File

@@ -0,0 +1,138 @@
/* 1. 强制清空外层容器的背景、边框和阴影 */
#swiperBox,
#swiper_container,
.blog-slider,
.swiper-container {
padding: 0 !important;
margin: 0 auto !important;
background: none !important;
border: none !important;
box-shadow: none !important;
width: 100% !important;
overflow: hidden !important;
}
/* 2. 强力锁定比例,防止 JS 动态修改高度 */
div#swiper_container {
width: 100% !important;
/* 使用 vw 单位根据屏幕宽度动态计算高度 */
/* 16:9 比例下,高度 = 宽度 * 56.25% */
height: 56.25vw !important;
/* 设定最大高度,防止在 PC 端显得过大(可根据需求调整,比如 400px */
max-height: 450px !important;
/* 设定最小高度,防止在极小屏幕下太扁 */
min-height: 180px !important;
aspect-ratio: 16 / 9 !important;
display: block !important;
border-radius: 12px;
position: relative !important;
}
/* 强制内部所有幻灯片跟随父容器高度 */
.swiper-wrapper, .swiper-slide, .blog-slider__item {
height: 100% !important;
}
/* 3. 彻底隐藏分页圆点或橙色元素 */
.blog-slider__pagination, .swiper-pagination {
display: none !important;
}
.blog-slider__pagination .swiper-pagination-bullet,
.swiper-pagination-bullet,
.swiper-button-next,
.swiper-button-prev {
outline: none !important;
}
/* 4. 图片填充修复:确保铺满不留白 */
.blog-slider__item {
width: 100% !important;
height: 100% !important;
background-size: cover !important;
background-position: center !important;
margin: 0 !important;
padding: 0 !important;
}
/* 5. 打包内容块并整体居中 */
.blog-slider__content {
background: rgba(0, 0, 0, 0.45) !important;
width: 100% !important;
height: 100% !important;
position: absolute !important;
top: 0 !important;
left: 0 !important;
margin: 0 !important;
padding: 0 !important;
}
/* 核心:将内容容器设为居中参考点 */
.blog-slider__content .blog-slider__item_content_wrapper, /* 尝试抓取可能的内层包裹 */
.blog-slider__content > div:first-child, /* 备选抓取 */
.blog-slider__content {
display: flex !important;
flex-direction: column !important;
justify-content: center !important;
align-items: center !important;
}
/* 针对标题和描述的统一样式 */
.blog-slider__title,
.blog-slider__text {
position: relative !important; /* 放弃绝对定位,回归正常流,防止重叠 */
top: 0 !important;
left: 0 !important;
transform: none !important;
width: 85% !important;
text-align: center !important;
margin: 5px auto !important; /* 自动上下留白 */
color: #ffffff !important;
}
a.blog-slider__title {
font-size: 1.4rem !important;
font-weight: bold !important;
line-height: 1.3 !important;
text-decoration: none !important;
}
.blog-slider__text {
font-size: 0.9rem !important;
opacity: 0.9;
/* 限制描述文字行数防止在16:9中撑破容器 */
display: -webkit-box !important;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* 针对手机端的极小屏幕适配 */
@media screen and (max-width: 768px) {
a.blog-slider__title {
font-size: 1.1rem !important;
}
.blog-slider__text {
font-size: 0.8rem !important;
-webkit-line-clamp: 1; /* 手机端只留一行描述,防止空间拥挤 */
}
}
/* 6. 移动端微调:防止文字过大或间距过宽 */
@media screen and (max-width: 768px) {
.blog-slider__content {
padding: 0 15px !important;
}
a.blog-slider__title {
font-size: 1.1rem !important;
}
}
/* 7. 左右箭头样式:白色且无背景 */
.swiper-button-next::after, .swiper-button-prev::after {
font-size: 1.2rem !important;
color: #fff !important;
background: none !important;
}

0
css/var.css Normal file
View File

1
go.html Normal file

File diff suppressed because one or more lines are too long

BIN
images/Bi.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
images/background.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 KiB

BIN
img/404.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
img/butterfly-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

BIN
img/error-page.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
img/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
img/friend_404.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

514
index.html Normal file

File diff suppressed because one or more lines are too long

1
indexnow.txt Normal file
View File

@@ -0,0 +1 @@
https://blog.biss.click/posts/8802e397/

65
js/ai-summary.js Normal file
View File

@@ -0,0 +1,65 @@
// 打字机效果
function typeTextMachineStyle(text, targetSelector, options = {}) {
const {
delay = 50,
startDelay = 2000,
onComplete = null,
clearBefore = true,
eraseBefore = true, // 新增:是否以打字机方式清除原文本
eraseDelay = 30, // 新增:删除每个字符的间隔
} = options;
const el = document.querySelector(targetSelector);
if (!el || typeof text !== "string") return;
setTimeout(() => {
const startTyping = () => {
let index = 0;
function renderChar() {
if (index <= text.length) {
el.textContent = text.slice(0, index++);
setTimeout(renderChar, delay);
} else {
onComplete && onComplete(el);
}
}
renderChar();
};
if (clearBefore) {
if (eraseBefore && el.textContent.length > 0) {
let currentText = el.textContent;
let eraseIndex = currentText.length;
function eraseChar() {
if (eraseIndex > 0) {
el.textContent = currentText.slice(0, --eraseIndex);
setTimeout(eraseChar, eraseDelay);
} else {
startTyping(); // 删除完毕后开始打字
}
}
eraseChar();
} else {
el.textContent = "";
startTyping();
}
} else {
startTyping();
}
}, startDelay);
}
function renderAISummary() {
const summaryEl = document.querySelector('.ai-summary .ai-explanation');
if (!summaryEl) return;
const summaryText = summaryEl.getAttribute('data-summary');
if (summaryText) {
typeTextMachineStyle(summaryText, ".ai-summary .ai-explanation"); // 如果需要切换,在这里调用另一个函数即可
}
}
document.addEventListener('pjax:complete', renderAISummary);
document.addEventListener('DOMContentLoaded', renderAISummary);

397
js/chart.js Normal file
View File

@@ -0,0 +1,397 @@
const cheerio = require('cheerio')
const moment = require('moment')
hexo.extend.filter.register('after_render:html', function (locals) {
const $ = cheerio.load(locals)
const post = $('#posts-chart')
const tag = $('#tags-chart')
const category = $('#categories-chart')
const htmlEncode = false
if (post.length > 0 || tag.length > 0 || category.length > 0) {
if (post.length > 0 && $('#postsChart').length === 0) {
if (post.attr('data-encode') === 'true') htmlEncode = true
post.after(postsChart(post.attr('data-start')))
}
if (tag.length > 0 && $('#tagsChart').length === 0) {
if (tag.attr('data-encode') === 'true') htmlEncode = true
tag.after(tagsChart(tag.attr('data-length')))
}
if (category.length > 0 && $('#categoriesChart').length === 0) {
if (category.attr('data-encode') === 'true') htmlEncode = true
category.after(categoriesChart(category.attr('data-parent')))
}
if (htmlEncode) {
return $.root().html().replace(/&amp;#/g, '&#')
} else {
return $.root().html()
}
} else {
return locals
}
}, 15)
function postsChart (startMonth) {
const startDate = moment(startMonth || '2020-01')
const endDate = moment()
const monthMap = new Map()
const dayTime = 3600 * 24 * 1000
for (let time = startDate; time <= endDate; time += dayTime) {
const month = moment(time).format('YYYY-MM')
if (!monthMap.has(month)) {
monthMap.set(month, 0)
}
}
hexo.locals.get('posts').forEach(function (post) {
const month = post.date.format('YYYY-MM')
if (monthMap.has(month)) {
monthMap.set(month, monthMap.get(month) + 1)
}
})
const monthArr = JSON.stringify([...monthMap.keys()])
const monthValueArr = JSON.stringify([...monthMap.values()])
return `
<script id="postsChart">
var color = document.documentElement.getAttribute('data-theme') === 'light' ? '#4c4948' : 'rgba(255,255,255,0.7)'
var postsChart = echarts.init(document.getElementById('posts-chart'), 'light');
var postsOption = {
title: {
text: '文章发布统计图',
x: 'center',
textStyle: {
color: color
}
},
tooltip: {
trigger: 'axis'
},
xAxis: {
name: '日期',
type: 'category',
boundaryGap: false,
nameTextStyle: {
color: color
},
axisTick: {
show: false
},
axisLabel: {
show: true,
color: color
},
axisLine: {
show: true,
lineStyle: {
color: color
}
},
data: ${monthArr}
},
yAxis: {
name: '文章篇数',
type: 'value',
nameTextStyle: {
color: color
},
splitLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
show: true,
color: color
},
axisLine: {
show: true,
lineStyle: {
color: color
}
}
},
series: [{
name: '文章篇数',
type: 'line',
smooth: true,
lineStyle: {
width: 0
},
showSymbol: false,
itemStyle: {
opacity: 1,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: 'rgba(128, 255, 165)'
},
{
offset: 1,
color: 'rgba(1, 191, 236)'
}])
},
areaStyle: {
opacity: 1,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: 'rgba(128, 255, 165)'
}, {
offset: 1,
color: 'rgba(1, 191, 236)'
}])
},
data: ${monthValueArr},
markLine: {
data: [{
name: '平均值',
type: 'average',
label: {
color: color
}
}]
}
}]
};
postsChart.setOption(postsOption);
window.addEventListener('resize', () => {
postsChart.resize();
});
postsChart.on('click', 'series', (event) => {
if (event.componentType === 'series') window.location.href = '/archives/' + event.name.replace('-', '/');
});
</script>`
}
function tagsChart (len) {
const tagArr = []
hexo.locals.get('tags').map(function (tag) {
tagArr.push({ name: tag.name, value: tag.length, path: tag.path })
})
tagArr.sort((a, b) => { return b.value - a.value })
const dataLength = Math.min(tagArr.length, len) || tagArr.length
const tagNameArr = []
for (let i = 0; i < dataLength; i++) {
tagNameArr.push(tagArr[i].name)
}
const tagNameArrJson = JSON.stringify(tagNameArr)
const tagArrJson = JSON.stringify(tagArr)
return `
<script id="tagsChart">
var color = document.documentElement.getAttribute('data-theme') === 'light' ? '#4c4948' : 'rgba(255,255,255,0.7)'
var tagsChart = echarts.init(document.getElementById('tags-chart'), 'light');
var tagsOption = {
title: {
text: 'Top ${dataLength} 标签统计图',
x: 'center',
textStyle: {
color: color
}
},
tooltip: {},
xAxis: {
name: '标签',
type: 'category',
nameTextStyle: {
color: color
},
axisTick: {
show: false
},
axisLabel: {
show: true,
color: color,
interval: 0
},
axisLine: {
show: true,
lineStyle: {
color: color
}
},
data: ${tagNameArrJson}
},
yAxis: {
name: '文章篇数',
type: 'value',
splitLine: {
show: false
},
nameTextStyle: {
color: color
},
axisTick: {
show: false
},
axisLabel: {
show: true,
color: color
},
axisLine: {
show: true,
lineStyle: {
color: color
}
}
},
series: [{
name: '文章篇数',
type: 'bar',
data: ${tagArrJson},
itemStyle: {
borderRadius: [5, 5, 0, 0],
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: 'rgba(128, 255, 165)'
},
{
offset: 1,
color: 'rgba(1, 191, 236)'
}])
},
emphasis: {
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: 'rgba(128, 255, 195)'
},
{
offset: 1,
color: 'rgba(1, 211, 255)'
}])
}
},
markLine: {
data: [{
name: '平均值',
type: 'average',
label: {
color: color
}
}]
}
}]
};
tagsChart.setOption(tagsOption);
window.addEventListener('resize', () => {
tagsChart.resize();
});
tagsChart.on('click', 'series', (event) => {
if(event.data.path) window.location.href = '/' + event.data.path;
});
</script>`
}
function categoriesChart (dataParent) {
const categoryArr = []
let categoryParentFlag = false
hexo.locals.get('categories').map(function (category) {
if (category.parent) categoryParentFlag = true
categoryArr.push({
name: category.name,
value: category.length,
path: category.path,
id: category._id,
parentId: category.parent || '0'
})
})
categoryParentFlag = categoryParentFlag && dataParent === 'true'
categoryArr.sort((a, b) => { return b.value - a.value })
function translateListToTree (data, parent) {
let tree = []
let temp
data.forEach((item, index) => {
if (data[index].parentId == parent) {
let obj = data[index];
temp = translateListToTree(data, data[index].id);
if (temp.length > 0) {
obj.children = temp
}
if (tree.indexOf())
tree.push(obj)
}
})
return tree
}
const categoryNameJson = JSON.stringify(categoryArr.map(function (category) { return category.name }))
const categoryArrJson = JSON.stringify(categoryArr)
const categoryArrParentJson = JSON.stringify(translateListToTree(categoryArr, '0'))
return `
<script id="categoriesChart">
var color = document.documentElement.getAttribute('data-theme') === 'light' ? '#4c4948' : 'rgba(255,255,255,0.7)'
var categoriesChart = echarts.init(document.getElementById('categories-chart'), 'light');
var categoryParentFlag = ${categoryParentFlag}
var categoriesOption = {
title: {
text: '文章分类统计图',
x: 'center',
textStyle: {
color: color
}
},
legend: {
top: 'bottom',
data: ${categoryNameJson},
textStyle: {
color: color
}
},
tooltip: {
trigger: 'item'
},
series: []
};
categoriesOption.series.push(
categoryParentFlag ?
{
nodeClick :false,
name: '文章篇数',
type: 'sunburst',
radius: ['15%', '90%'],
center: ['50%', '55%'],
sort: 'desc',
data: ${categoryArrParentJson},
itemStyle: {
borderColor: '#fff',
borderWidth: 2,
emphasis: {
focus: 'ancestor',
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(255, 255, 255, 0.5)'
}
}
}
:
{
name: '文章篇数',
type: 'pie',
radius: [30, 80],
roseType: 'area',
label: {
color: color,
formatter: '{b} : {c} ({d}%)'
},
data: ${categoryArrJson},
itemStyle: {
emphasis: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(255, 255, 255, 0.5)'
}
}
}
)
categoriesChart.setOption(categoriesOption);
window.addEventListener('resize', () => {
categoriesChart.resize();
});
categoriesChart.on('click', 'series', (event) => {
if(event.data.path) window.location.href = '/' + event.data.path;
});
</script>`
}

66
js/footer.js Normal file
View File

@@ -0,0 +1,66 @@
//版权图标动态显示
document.addEventListener('DOMContentLoaded', function() {
const currentYear = new Date().getFullYear();
const copyrightElement = document.querySelector('.copyright');
if (copyrightElement) {
copyrightElement.innerHTML = `©2025 - ${currentYear} <i class="fa-solid fa-heart "></i> By Bi`;
}
});
// 运行时间动态显示
function showDateTime() {
const timeDisplay = document.getElementById('span_dt_dt');
if (!timeDisplay) return;
const startTime = new Date("2025-07-05T15:41:23");
const now = new Date();
const elapsedMilliseconds = now - startTime;
const seconds = Math.floor(elapsedMilliseconds / 1000);
const oneYearInSeconds = 365 * 24 * 60 * 60;
if (seconds < oneYearInSeconds) {
const days = Math.floor(seconds / (24 * 60 * 60));
const remainingSecondsAfterDays = seconds % (24 * 60 * 60);
const hours = Math.floor(remainingSecondsAfterDays / (60 * 60));
const remainingSecondsAfterHours = remainingSecondsAfterDays % (60 * 60);
const minutes = Math.floor(remainingSecondsAfterHours / 60);
const sec = remainingSecondsAfterHours % 60;
timeDisplay.innerHTML = `
<span style="color:#ffff00">${days}</span> 天
<span style="color:#ffff00">${hours}</span> 时
<span style="color:#ffff00">${minutes}</span> 分
<span style="color:#ffff00">${sec}</span> 秒
`;
} else {
const years = Math.floor(seconds / oneYearInSeconds);
const remainingSecondsAfterYears = seconds % oneYearInSeconds;
const days = Math.floor(remainingSecondsAfterYears / (24 * 60 * 60));
const remainingSecondsAfterDays = remainingSecondsAfterYears % (24 * 60 * 60);
const hours = Math.floor(remainingSecondsAfterDays / (60 * 60));
const remainingSecondsAfterHours = remainingSecondsAfterDays % (60 * 60);
const minutes = Math.floor(remainingSecondsAfterHours / 60);
const sec = remainingSecondsAfterHours % 60;
timeDisplay.innerHTML = `
<span style="color:#ffff00">${years}</span> 年
<span style="color:#ffff00">${days}</span> 天
<span style="color:#ffff00">${hours}</span> 时
<span style="color:#ffff00">${minutes}</span> 分
<span style="color:#ffff00">${sec}</span> 秒
`;
}
setTimeout(showDateTime, 1000);
}
document.addEventListener('DOMContentLoaded', () => {
const frameworkInfo = document.querySelector('.framework-info');
if (frameworkInfo) {
frameworkInfo.innerHTML = '本站已运行<span id="span_dt_dt"></span>';
}
showDateTime();
});

987
js/main.js Normal file
View File

@@ -0,0 +1,987 @@
document.addEventListener('DOMContentLoaded', () => {
let headerContentWidth, $nav
let mobileSidebarOpen = false
const adjustMenu = init => {
const getAllWidth = ele => Array.from(ele).reduce((width, i) => width + i.offsetWidth, 0)
if (init) {
const blogInfoWidth = getAllWidth(document.querySelector('#blog-info > a').children)
const menusWidth = getAllWidth(document.getElementById('menus').children)
headerContentWidth = blogInfoWidth + menusWidth
$nav = document.getElementById('nav')
}
const hideMenuIndex = window.innerWidth <= 768 || headerContentWidth > $nav.offsetWidth - 120
$nav.classList.toggle('hide-menu', hideMenuIndex)
}
// 初始化header
const initAdjust = () => {
adjustMenu(true)
$nav.classList.add('show')
}
// sidebar menus
const sidebarFn = {
open: () => {
btf.overflowPaddingR.add()
btf.animateIn(document.getElementById('menu-mask'), 'to_show 0.5s')
document.getElementById('sidebar-menus').classList.add('open')
mobileSidebarOpen = true
},
close: () => {
btf.overflowPaddingR.remove()
btf.animateOut(document.getElementById('menu-mask'), 'to_hide 0.5s')
document.getElementById('sidebar-menus').classList.remove('open')
mobileSidebarOpen = false
}
}
/**
* 首頁top_img底下的箭頭
*/
const scrollDownInIndex = () => {
const handleScrollToDest = () => {
btf.scrollToDest(document.getElementById('content-inner').offsetTop, 300)
}
const $scrollDownEle = document.getElementById('scroll-down')
$scrollDownEle && btf.addEventListenerPjax($scrollDownEle, 'click', handleScrollToDest)
}
/**
* 代碼
* 只適用於Hexo默認的代碼渲染
*/
const addHighlightTool = () => {
const highLight = GLOBAL_CONFIG.highlight
if (!highLight) return
const { highlightCopy, highlightLang, highlightHeightLimit, highlightFullpage, highlightMacStyle, plugin } = highLight
const isHighlightShrink = GLOBAL_CONFIG_SITE.isHighlightShrink
const isShowTool = highlightCopy || highlightLang || isHighlightShrink !== undefined || highlightFullpage || highlightMacStyle
const isNotHighlightJs = plugin !== 'highlight.js'
const isPrismjs = plugin === 'prismjs'
const $figureHighlight = isNotHighlightJs
? Array.from(document.querySelectorAll('code[class*="language-"]')).map(code => code.parentElement)
: document.querySelectorAll('figure.highlight')
if (!((isShowTool || highlightHeightLimit) && $figureHighlight.length)) return
const highlightShrinkClass = isHighlightShrink === true ? 'closed' : ''
const highlightShrinkEle = isHighlightShrink !== undefined ? '<i class="fas fa-angle-down expand"></i>' : ''
const highlightCopyEle = highlightCopy ? '<i class="fas fa-paste copy-button"></i>' : ''
const highlightMacStyleEle = '<div class="macStyle"><div class="mac-close"></div><div class="mac-minimize"></div><div class="mac-maximize"></div></div>'
const highlightFullpageEle = highlightFullpage ? '<i class="fa-solid fa-up-right-and-down-left-from-center fullpage-button"></i>' : ''
const alertInfo = (ele, text) => {
if (GLOBAL_CONFIG.Snackbar !== undefined) {
btf.snackbarShow(text)
} else {
const newEle = document.createElement('div')
newEle.className = 'copy-notice'
newEle.textContent = text
document.body.appendChild(newEle)
const buttonRect = ele.getBoundingClientRect()
const scrollTop = window.pageYOffset || document.documentElement.scrollTop
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft
// X-axis boundary check
const halfWidth = newEle.offsetWidth / 2
const centerLeft = buttonRect.left + scrollLeft + buttonRect.width / 2
const finalLeft = Math.max(halfWidth + 10, Math.min(window.innerWidth - halfWidth - 10, centerLeft))
// Show tooltip below button if too close to top
const normalTop = buttonRect.top + scrollTop - 40
const shouldShowBelow = buttonRect.top < 60 || normalTop < 10
const topValue = shouldShowBelow ? buttonRect.top + scrollTop + buttonRect.height + 10 : normalTop
newEle.style.cssText = `
top: ${topValue + 10}px;
left: ${finalLeft}px;
transform: translateX(-50%);
opacity: 0;
transition: opacity 0.3s ease, top 0.3s ease;
`
requestAnimationFrame(() => {
newEle.style.opacity = '1'
newEle.style.top = `${topValue}px`
})
setTimeout(() => {
newEle.style.opacity = '0'
newEle.style.top = `${topValue + 10}px`
setTimeout(() => {
newEle?.remove()
}, 300)
}, 800)
}
}
const copy = async (text, ctx) => {
try {
await navigator.clipboard.writeText(text)
alertInfo(ctx, GLOBAL_CONFIG.copy.success)
} catch (err) {
console.error('Failed to copy: ', err)
alertInfo(ctx, GLOBAL_CONFIG.copy.noSupport)
}
}
// click events
const highlightCopyFn = (ele, clickEle) => {
const $buttonParent = ele.parentNode
$buttonParent.classList.add('copy-true')
const preCodeSelector = isNotHighlightJs ? 'pre code' : 'table .code pre'
const codeElement = $buttonParent.querySelector(preCodeSelector)
if (!codeElement) return
copy(codeElement.innerText, clickEle)
$buttonParent.classList.remove('copy-true')
}
const highlightShrinkFn = ele => ele.classList.toggle('closed')
const codeFullpage = (item, clickEle) => {
const wrapEle = item.closest('figure.highlight')
const isFullpage = wrapEle.classList.toggle('code-fullpage')
document.body.style.overflow = isFullpage ? 'hidden' : ''
clickEle.classList.toggle('fa-down-left-and-up-right-to-center', isFullpage)
clickEle.classList.toggle('fa-up-right-and-down-left-from-center', !isFullpage)
}
const highlightToolsFn = e => {
const $target = e.target.classList
const currentElement = e.currentTarget
if ($target.contains('expand')) highlightShrinkFn(currentElement)
else if ($target.contains('copy-button')) highlightCopyFn(currentElement, e.target)
else if ($target.contains('fullpage-button')) codeFullpage(currentElement, e.target)
}
const expandCode = e => e.currentTarget.classList.toggle('expand-done')
// 獲取隱藏狀態下元素的真實高度
const getActualHeight = item => {
if (item.offsetHeight > 0) return item.offsetHeight
const hiddenElements = new Map()
const fix = () => {
let current = item
while (current !== document.body && current != null) {
if (window.getComputedStyle(current).display === 'none') {
hiddenElements.set(current, current.getAttribute('style') || '')
}
current = current.parentNode
}
const style = 'visibility: hidden !important; display: block !important;'
hiddenElements.forEach((originalStyle, elem) => {
elem.setAttribute('style', originalStyle ? originalStyle + ';' + style : style)
})
}
const restore = () => {
hiddenElements.forEach((originalStyle, elem) => {
if (originalStyle === '') elem.removeAttribute('style')
else elem.setAttribute('style', originalStyle)
})
}
fix()
const height = item.offsetHeight
restore()
return height
}
const createEle = (lang, item) => {
const fragment = document.createDocumentFragment()
if (isShowTool) {
const hlTools = document.createElement('div')
hlTools.className = `highlight-tools ${highlightShrinkClass}`
hlTools.innerHTML = highlightMacStyleEle + highlightShrinkEle + lang + highlightCopyEle + highlightFullpageEle
btf.addEventListenerPjax(hlTools, 'click', highlightToolsFn)
fragment.appendChild(hlTools)
}
if (highlightHeightLimit && getActualHeight(item) > highlightHeightLimit + 30) {
const ele = document.createElement('div')
ele.className = 'code-expand-btn'
ele.innerHTML = '<i class="fas fa-angle-double-down"></i>'
btf.addEventListenerPjax(ele, 'click', expandCode)
fragment.appendChild(ele)
}
isNotHighlightJs ? item.parentNode.insertBefore(fragment, item) : item.insertBefore(fragment, item.firstChild)
}
$figureHighlight.forEach(item => {
let langName = ''
if (isNotHighlightJs) {
const newClassName = isPrismjs ? 'prismjs' : 'default'
btf.wrap(item, 'figure', { class: `highlight ${newClassName}` })
}
if (!highlightLang) {
createEle('', item)
return
}
if (isNotHighlightJs) {
langName = isPrismjs ? item.getAttribute('data-language') || 'Code' : item.querySelector('code').getAttribute('class').replace('language-', '')
} else {
langName = item.getAttribute('class').split(' ')[1]
if (langName === 'plain' || langName === undefined) langName = 'Code'
}
createEle(`<div class="code-lang">${langName}</div>`, item)
})
}
/**
* PhotoFigcaption
*/
const addPhotoFigcaption = () => {
if (!GLOBAL_CONFIG.isPhotoFigcaption) return
document.querySelectorAll('#article-container img').forEach(item => {
const altValue = item.title || item.alt
if (!altValue) return
const ele = document.createElement('div')
ele.className = 'img-alt text-center'
ele.textContent = altValue
item.insertAdjacentElement('afterend', ele)
})
}
/**
* Lightbox
*/
const runLightbox = () => {
btf.loadLightbox(document.querySelectorAll('#article-container img:not(.no-lightbox)'))
}
/**
* justified-gallery 圖庫排版
*/
const fetchUrl = async url => {
try {
const response = await fetch(url)
return await response.json()
} catch (error) {
console.error('Failed to fetch URL:', error)
return []
}
}
const runJustifiedGallery = (container, data, config) => {
const { isButton, limit, firstLimit, tabs } = config
const dataLength = data.length
const maxGroupKey = Math.ceil((dataLength - firstLimit) / limit + 1)
// Gallery configuration
const igConfig = {
gap: 5,
isConstantSize: true,
sizeRange: [150, 600],
// useResizeObserver: true,
// observeChildren: true,
useTransform: true
// useRecycle: false
}
const ig = new InfiniteGrid.JustifiedInfiniteGrid(container, igConfig)
let isLayoutHidden = false
// Utility functions
const sanitizeString = str => (str && str.replace(/"/g, '&quot;')) || ''
const createImageItem = item => {
const alt = item.alt ? `alt="${sanitizeString(item.alt)}"` : ''
const title = item.title ? `title="${sanitizeString(item.title)}"` : ''
return `<div class="item">
<img src="${item.url}" data-grid-maintained-target="true" ${alt} ${title} />
</div>`
}
const getItems = (nextGroupKey, count, isFirst = false) => {
const startIndex = isFirst ? (nextGroupKey - 1) * count : (nextGroupKey - 2) * count + firstLimit
return data.slice(startIndex, startIndex + count).map(createImageItem)
}
// Load more button
const addLoadMoreButton = container => {
const button = document.createElement('button')
button.innerHTML = `${GLOBAL_CONFIG.infinitegrid.buttonText}<i class="fa-solid fa-arrow-down"></i>`
button.addEventListener('click', () => {
button.remove()
btf.setLoading.add(container)
appendItems(ig.getGroups().length + 1, limit)
}, { once: true })
container.insertAdjacentElement('afterend', button)
}
const appendItems = (nextGroupKey, count, isFirst) => {
ig.append(getItems(nextGroupKey, count, isFirst), nextGroupKey)
}
// Event handlers
const handleRenderComplete = e => {
if (tabs) {
const parentNode = container.parentNode
if (isLayoutHidden) {
parentNode.style.visibility = 'visible'
}
if (container.offsetHeight === 0) {
parentNode.style.visibility = 'hidden'
isLayoutHidden = true
}
}
const { updated, isResize, mounted } = e
if (!updated.length || !mounted.length || isResize) return
btf.loadLightbox(container.querySelectorAll('img:not(.medium-zoom-image)'))
if (ig.getGroups().length === maxGroupKey) {
btf.setLoading.remove(container)
!tabs && ig.off('renderComplete', handleRenderComplete)
return
}
if (isButton) {
btf.setLoading.remove(container)
addLoadMoreButton(container)
}
}
const handleRequestAppend = btf.debounce(e => {
const nextGroupKey = (+e.groupKey || 0) + 1
if (nextGroupKey === 1) appendItems(nextGroupKey, firstLimit, true)
else appendItems(nextGroupKey, limit)
if (nextGroupKey === maxGroupKey) ig.off('requestAppend', handleRequestAppend)
}, 300)
btf.setLoading.add(container)
ig.on('renderComplete', handleRenderComplete)
if (isButton) {
appendItems(1, firstLimit, true)
} else {
ig.on('requestAppend', handleRequestAppend)
ig.renderItems()
}
btf.addGlobalFn('pjaxSendOnce', () => ig.destroy())
}
const addJustifiedGallery = async (elements, tabs = false) => {
if (!elements.length) return
const initGallery = async () => {
for (const element of elements) {
if (btf.isHidden(element) || element.classList.contains('loaded')) continue
const config = {
isButton: element.getAttribute('data-button') === 'true',
limit: parseInt(element.getAttribute('data-limit'), 10),
firstLimit: parseInt(element.getAttribute('data-first'), 10),
tabs
}
const container = element.firstElementChild
const content = container.textContent
container.textContent = ''
element.classList.add('loaded')
try {
const data = element.getAttribute('data-type') === 'url' ? await fetchUrl(content) : JSON.parse(content)
runJustifiedGallery(container, data, config)
} catch (error) {
console.error('Gallery data parsing failed:', error)
}
}
}
if (typeof InfiniteGrid === 'function') {
await initGallery()
} else {
await btf.getScript(GLOBAL_CONFIG.infinitegrid.js)
await initGallery()
}
}
/**
* rightside scroll percent
*/
const rightsideScrollPercent = currentTop => {
const scrollPercent = btf.getScrollPercent(currentTop, document.body)
const goUpElement = document.getElementById('go-up')
if (scrollPercent < 95) {
goUpElement.classList.add('show-percent')
goUpElement.querySelector('.scroll-percent').textContent = scrollPercent
} else {
goUpElement.classList.remove('show-percent')
}
}
/**
* 滾動處理
*/
const scrollFn = () => {
const $rightside = document.getElementById('rightside')
const innerHeight = window.innerHeight + 56
let initTop = 0
const $header = document.getElementById('page-header')
const isChatBtn = typeof chatBtn !== 'undefined'
const isShowPercent = GLOBAL_CONFIG.percent.rightside
// 檢查文檔高度是否小於視窗高度
const checkDocumentHeight = () => {
if (document.body.scrollHeight <= innerHeight) {
$rightside.classList.add('rightside-show')
return true
}
return false
}
// 如果文檔高度小於視窗高度,直接返回
if (checkDocumentHeight()) return
// find the scroll direction
const scrollDirection = currentTop => {
const result = currentTop > initTop // true is down & false is up
initTop = currentTop
return result
}
let flag = ''
const scrollTask = btf.throttle(() => {
const currentTop = window.scrollY || document.documentElement.scrollTop
const isDown = scrollDirection(currentTop)
if (currentTop > 56) {
if (flag === '') {
$header.classList.add('nav-fixed')
$rightside.classList.add('rightside-show')
}
if (isDown) {
if (flag !== 'down') {
$header.classList.remove('nav-visible')
isChatBtn && window.chatBtn.hide()
flag = 'down'
}
} else {
if (flag !== 'up') {
$header.classList.add('nav-visible')
isChatBtn && window.chatBtn.show()
flag = 'up'
}
}
} else {
flag = ''
if (currentTop === 0) {
$header.classList.remove('nav-fixed', 'nav-visible')
}
$rightside.classList.remove('rightside-show')
}
isShowPercent && rightsideScrollPercent(currentTop)
checkDocumentHeight()
}, 300)
btf.addEventListenerPjax(window, 'scroll', scrollTask, { passive: true })
}
/**
* toc,anchor
*/
const scrollFnToDo = () => {
const isToc = GLOBAL_CONFIG_SITE.isToc
const isAnchor = GLOBAL_CONFIG.isAnchor
const $article = document.getElementById('article-container')
if (!($article && (isToc || isAnchor))) return
let $tocLink, $cardToc, autoScrollToc, $tocPercentage, isExpand
if (isToc) {
const $cardTocLayout = document.getElementById('card-toc')
$cardToc = $cardTocLayout.querySelector('.toc-content')
$tocLink = $cardToc.querySelectorAll('.toc-link')
$tocPercentage = $cardTocLayout.querySelector('.toc-percentage')
isExpand = $cardToc.classList.contains('is-expand')
// toc元素點擊
const tocItemClickFn = e => {
const target = e.target.closest('.toc-link')
if (!target) return
e.preventDefault()
btf.scrollToDest(btf.getEleTop(document.getElementById(decodeURI(target.getAttribute('href')).replace('#', ''))), 300)
if (window.innerWidth < 900) {
$cardTocLayout.classList.remove('open')
}
}
btf.addEventListenerPjax($cardToc, 'click', tocItemClickFn)
autoScrollToc = item => {
const sidebarHeight = $cardToc.clientHeight
const itemOffsetTop = item.offsetTop
const itemHeight = item.clientHeight
const scrollTop = $cardToc.scrollTop
const offset = itemOffsetTop - scrollTop
const middlePosition = (sidebarHeight - itemHeight) / 2
if (offset !== middlePosition) {
$cardToc.scrollTop = scrollTop + (offset - middlePosition)
}
}
// 處理 hexo-blog-encrypt 事件
$cardToc.style.display = 'block'
}
// find head position & add active class
const $articleList = $article.querySelectorAll('h1,h2,h3,h4,h5,h6')
let detectItem = ''
// Optimization: Cache header positions
let headerList = []
const updateHeaderPositions = () => {
headerList = Array.from($articleList).map(ele => ({
ele,
top: btf.getEleTop(ele),
id: ele.id
}))
}
updateHeaderPositions()
btf.addEventListenerPjax(window, 'resize', btf.throttle(updateHeaderPositions, 200))
const findHeadPosition = top => {
if (top === 0) return false
let currentId = ''
let currentIndex = ''
for (let i = 0; i < headerList.length; i++) {
const item = headerList[i]
if (top > item.top - 80) {
currentId = item.id ? '#' + encodeURI(item.id) : ''
currentIndex = i
} else {
break
}
}
if (detectItem === currentIndex) return
if (isAnchor) btf.updateAnchor(currentId)
detectItem = currentIndex
if (isToc) {
$cardToc.querySelectorAll('.active').forEach(i => i.classList.remove('active'))
if (currentId) {
const currentActive = $tocLink[currentIndex]
currentActive.classList.add('active')
setTimeout(() => autoScrollToc(currentActive), 0)
if (!isExpand) {
let parent = currentActive.parentNode
while (!parent.matches('.toc')) {
if (parent.matches('li')) parent.classList.add('active')
parent = parent.parentNode
}
}
}
}
}
// main of scroll
const tocScrollFn = btf.throttle(() => {
const currentTop = window.scrollY || document.documentElement.scrollTop
if (isToc && GLOBAL_CONFIG.percent.toc) {
$tocPercentage.textContent = btf.getScrollPercent(currentTop, $article)
}
findHeadPosition(currentTop)
}, 100)
btf.addEventListenerPjax(window, 'scroll', tocScrollFn, { passive: true })
}
const handleThemeChange = mode => {
const globalFn = window.globalFn || {}
const themeChange = globalFn.themeChange || {}
if (!themeChange) {
return
}
Object.keys(themeChange).forEach(key => {
const themeChangeFn = themeChange[key]
if (['disqus', 'disqusjs'].includes(key)) {
setTimeout(() => themeChangeFn(mode), 300)
} else {
themeChangeFn(mode)
}
})
}
/**
* Rightside
*/
const rightSideFn = {
readmode: () => { // read mode
const $body = document.body
const newEle = document.createElement('button')
const exitReadMode = () => {
$body.classList.remove('read-mode')
newEle.remove()
newEle.removeEventListener('click', exitReadMode)
}
$body.classList.add('read-mode')
newEle.type = 'button'
newEle.className = 'exit-readmode'
newEle.innerHTML = '<i class="fas fa-sign-out-alt"></i>'
newEle.addEventListener('click', exitReadMode)
$body.appendChild(newEle)
},
darkmode: () => { // switch between light and dark mode
const willChangeMode = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'
if (willChangeMode === 'dark') {
btf.activateDarkMode()
GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow(GLOBAL_CONFIG.Snackbar.day_to_night)
} else {
btf.activateLightMode()
GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow(GLOBAL_CONFIG.Snackbar.night_to_day)
}
btf.saveToLocal.set('theme', willChangeMode, 2)
handleThemeChange(willChangeMode)
},
'rightside-config': item => { // Show or hide rightside-hide-btn
const hideLayout = item.firstElementChild
if (hideLayout.classList.contains('show')) {
hideLayout.classList.add('status')
setTimeout(() => {
hideLayout.classList.remove('status')
}, 300)
}
hideLayout.classList.toggle('show')
},
'go-up': () => { // Back to top
btf.scrollToDest(0, 500)
},
'hide-aside-btn': () => { // Hide aside
const $htmlDom = document.documentElement.classList
const saveStatus = $htmlDom.contains('hide-aside') ? 'show' : 'hide'
btf.saveToLocal.set('aside-status', saveStatus, 2)
$htmlDom.toggle('hide-aside')
},
'mobile-toc-button': (p, item) => { // Show mobile toc
const tocEle = document.getElementById('card-toc')
tocEle.style.transition = 'transform 0.3s ease-in-out'
const tocEleHeight = tocEle.clientHeight
const btData = item.getBoundingClientRect()
const tocEleBottom = window.innerHeight - btData.bottom - 30
if (tocEleHeight > tocEleBottom) {
tocEle.style.transformOrigin = `right ${tocEleHeight - tocEleBottom - btData.height / 2}px`
}
tocEle.classList.toggle('open')
tocEle.addEventListener('transitionend', () => {
tocEle.style.cssText = ''
}, { once: true })
},
'chat-btn': () => { // Show chat
window.chatBtnFn()
},
translateLink: () => { // switch between traditional and simplified chinese
window.translateFn.translatePage()
}
}
document.getElementById('rightside').addEventListener('click', e => {
const $target = e.target.closest('[id]')
if ($target && rightSideFn[$target.id]) {
rightSideFn[$target.id](e.currentTarget, $target)
}
})
/**
* menu
* 側邊欄sub-menu 展開/收縮
*/
const clickFnOfSubMenu = () => {
const handleClickOfSubMenu = e => {
const target = e.target.closest('.site-page.group')
if (!target) return
target.classList.toggle('hide')
}
const menusItems = document.querySelector('#sidebar-menus .menus_items')
menusItems && menusItems.addEventListener('click', handleClickOfSubMenu)
}
/**
* 手机端目录点击
*/
const openMobileMenu = () => {
const toggleMenu = document.getElementById('toggle-menu')
if (!toggleMenu) return
btf.addEventListenerPjax(toggleMenu, 'click', () => { sidebarFn.open() })
}
/**
* 複製時加上版權信息
*/
const addCopyright = () => {
const { limitCount, languages } = GLOBAL_CONFIG.copyright
const handleCopy = e => {
e.preventDefault()
const copyFont = window.getSelection(0).toString()
let textFont = copyFont
if (copyFont.length > limitCount) {
textFont = `${copyFont}\n\n\n${languages.author}\n${languages.link}${window.location.href}\n${languages.source}\n${languages.info}`
}
if (e.clipboardData) {
return e.clipboardData.setData('text', textFont)
} else {
return window.clipboardData.setData('text', textFont)
}
}
document.body.addEventListener('copy', handleCopy)
}
/**
* 網頁運行時間
*/
const addRuntime = () => {
const $runtimeCount = document.getElementById('runtimeshow')
if ($runtimeCount) {
const publishDate = $runtimeCount.getAttribute('data-publishDate')
$runtimeCount.textContent = `${btf.diffDate(publishDate)} ${GLOBAL_CONFIG.runtime}`
}
}
/**
* 最後一次更新時間
*/
const addLastPushDate = () => {
const $lastPushDateItem = document.getElementById('last-push-date')
if ($lastPushDateItem) {
const lastPushDate = $lastPushDateItem.getAttribute('data-lastPushDate')
$lastPushDateItem.textContent = btf.diffDate(lastPushDate, true)
}
}
/**
* table overflow
*/
const addTableWrap = () => {
const $table = document.querySelectorAll('#article-container table')
if (!$table.length) return
$table.forEach(item => {
if (!item.closest('.highlight')) {
btf.wrap(item, 'div', { class: 'table-wrap' })
}
})
}
/**
* tag-hide
*/
const clickFnOfTagHide = () => {
const hideButtons = document.querySelectorAll('#article-container .hide-button')
if (!hideButtons.length) return
hideButtons.forEach(item => item.addEventListener('click', e => {
const currentTarget = e.currentTarget
currentTarget.classList.add('open')
addJustifiedGallery(currentTarget.nextElementSibling.querySelectorAll('.gallery-container'))
}, { once: true }))
}
const tabsFn = () => {
const navTabsElements = document.querySelectorAll('#article-container .tabs')
if (!navTabsElements.length) return
const setActiveClass = (elements, activeIndex) => {
elements.forEach((el, index) => {
el.classList.toggle('active', index === activeIndex)
})
}
const handleNavClick = e => {
const target = e.target.closest('button')
if (!target || target.classList.contains('active')) return
const navItems = [...e.currentTarget.children]
const tabContents = [...e.currentTarget.nextElementSibling.children]
const indexOfButton = navItems.indexOf(target)
setActiveClass(navItems, indexOfButton)
e.currentTarget.classList.remove('no-default')
setActiveClass(tabContents, indexOfButton)
addJustifiedGallery(tabContents[indexOfButton].querySelectorAll('.gallery-container'), true)
}
const handleToTopClick = tabElement => e => {
if (e.target.closest('button')) {
btf.scrollToDest(btf.getEleTop(tabElement), 300)
}
}
navTabsElements.forEach(tabElement => {
btf.addEventListenerPjax(tabElement.firstElementChild, 'click', handleNavClick)
btf.addEventListenerPjax(tabElement.lastElementChild, 'click', handleToTopClick(tabElement))
})
}
const toggleCardCategory = () => {
const cardCategory = document.querySelector('#aside-cat-list.expandBtn')
if (!cardCategory) return
const handleToggleBtn = e => {
const target = e.target
if (target.nodeName === 'I') {
e.preventDefault()
target.parentNode.classList.toggle('expand')
}
}
btf.addEventListenerPjax(cardCategory, 'click', handleToggleBtn, true)
}
const addPostOutdateNotice = () => {
const ele = document.getElementById('post-outdate-notice')
if (!ele) return
const { limitDay, messagePrev, messageNext, postUpdate } = JSON.parse(ele.getAttribute('data'))
const diffDay = btf.diffDate(postUpdate)
if (diffDay >= limitDay) {
ele.textContent = `${messagePrev} ${diffDay} ${messageNext}`
ele.hidden = false
}
}
const lazyloadImg = () => {
window.lazyLoadInstance = new LazyLoad({
elements_selector: 'img',
threshold: 0,
data_src: 'lazy-src'
})
btf.addGlobalFn('pjaxComplete', () => {
window.lazyLoadInstance.update()
}, 'lazyload')
}
const relativeDate = selector => {
selector.forEach(item => {
item.textContent = btf.diffDate(item.getAttribute('datetime'), true)
item.style.display = 'inline'
})
}
const justifiedIndexPostUI = () => {
const recentPostsElement = document.getElementById('recent-posts')
if (!(recentPostsElement && recentPostsElement.classList.contains('masonry'))) return
const init = () => {
const masonryItem = new InfiniteGrid.MasonryInfiniteGrid('.recent-post-items', {
gap: { horizontal: 10, vertical: 20 },
useTransform: true,
useResizeObserver: true
})
masonryItem.renderItems()
btf.addGlobalFn('pjaxCompleteOnce', () => { masonryItem.destroy() }, 'removeJustifiedIndexPostUI')
}
typeof InfiniteGrid === 'function' ? init() : btf.getScript(`${GLOBAL_CONFIG.infinitegrid.js}`).then(init)
}
const unRefreshFn = () => {
window.addEventListener('resize', () => {
adjustMenu(false)
mobileSidebarOpen && btf.isHidden(document.getElementById('toggle-menu')) && sidebarFn.close()
})
const menuMask = document.getElementById('menu-mask')
menuMask && menuMask.addEventListener('click', () => { sidebarFn.close() })
clickFnOfSubMenu()
GLOBAL_CONFIG.islazyloadPlugin && lazyloadImg()
GLOBAL_CONFIG.copyright !== undefined && addCopyright()
if (GLOBAL_CONFIG.autoDarkmode) {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
if (btf.saveToLocal.get('theme') !== undefined) return
e.matches ? handleThemeChange('dark') : handleThemeChange('light')
})
}
}
const forPostFn = () => {
addHighlightTool()
addPhotoFigcaption()
addJustifiedGallery(document.querySelectorAll('#article-container .gallery-container'))
runLightbox()
scrollFnToDo()
addTableWrap()
clickFnOfTagHide()
tabsFn()
}
const refreshFn = () => {
initAdjust()
justifiedIndexPostUI()
if (GLOBAL_CONFIG_SITE.pageType === 'post') {
addPostOutdateNotice()
GLOBAL_CONFIG.relativeDate.post && relativeDate(document.querySelectorAll('#post-meta time'))
} else {
GLOBAL_CONFIG.relativeDate.homepage && relativeDate(document.querySelectorAll('#recent-posts time'))
GLOBAL_CONFIG.runtime && addRuntime()
addLastPushDate()
toggleCardCategory()
}
GLOBAL_CONFIG_SITE.pageType === 'home' && scrollDownInIndex()
scrollFn()
forPostFn()
GLOBAL_CONFIG_SITE.pageType !== 'shuoshuo' && btf.switchComments(document)
openMobileMenu()
}
btf.addGlobalFn('pjaxComplete', refreshFn, 'refreshFn')
refreshFn()
unRefreshFn()
// 處理 hexo-blog-encrypt 事件
window.addEventListener('hexo-blog-decrypt', e => {
forPostFn()
window.translateFn.translateInitialization()
Object.values(window.globalFn.encrypt).forEach(fn => {
fn()
})
})
})

13
js/random.js Normal file
View File

@@ -0,0 +1,13 @@
function randomPost() {
fetch('/sitemap.xml').then(res => res.text()).then(str => (new window.DOMParser()).parseFromString(str, "text/xml")).then(data => {
let ls = data.querySelectorAll('url loc');
let locationHref,locSplit;
do {
locationHref = ls[Math.floor(Math.random() * ls.length)].innerHTML
locSplit = locationHref.split('/')[3] || ''
} while (locSplit !== 'posts');
//若所有文章都如 https://…….com/posts/2022/07/…… 格式,主域名后字符是 posts则循环条件改为
//while (locSplit !== 'posts');
location.href = locationHref
})
}

562
js/search/algolia.js Normal file
View File

@@ -0,0 +1,562 @@
window.addEventListener('load', () => {
const { algolia } = GLOBAL_CONFIG
const { appId, apiKey, indexName, hitsPerPage = 5, languages } = algolia
if (!appId || !apiKey || !indexName) {
return console.error('Algolia setting is invalid!')
}
const $searchMask = document.getElementById('search-mask')
const $searchDialog = document.querySelector('#algolia-search .search-dialog')
const animateElements = show => {
const action = show ? 'animateIn' : 'animateOut'
const maskAnimation = show ? 'to_show 0.5s' : 'to_hide 0.5s'
const dialogAnimation = show ? 'titleScale 0.5s' : 'search_close .5s'
btf[action]($searchMask, maskAnimation)
btf[action]($searchDialog, dialogAnimation)
}
const fixSafariHeight = () => {
if (window.innerWidth < 768) {
$searchDialog.style.setProperty('--search-height', `${window.innerHeight}px`)
}
}
const openSearch = () => {
btf.overflowPaddingR.add()
animateElements(true)
showLoading(false)
setTimeout(() => {
const searchInput = document.querySelector('#algolia-search-input .ais-SearchBox-input')
if (searchInput) searchInput.focus()
}, 100)
const handleEscape = event => {
if (event.code === 'Escape') {
closeSearch()
document.removeEventListener('keydown', handleEscape)
}
}
document.addEventListener('keydown', handleEscape)
fixSafariHeight()
window.addEventListener('resize', fixSafariHeight)
}
const closeSearch = () => {
btf.overflowPaddingR.remove()
animateElements(false)
window.removeEventListener('resize', fixSafariHeight)
}
const searchClickFn = () => {
btf.addEventListenerPjax(document.querySelector('#search-button > .search'), 'click', openSearch)
}
const searchFnOnce = () => {
$searchMask.addEventListener('click', closeSearch)
document.querySelector('#algolia-search .search-close-button').addEventListener('click', closeSearch)
}
const cutContent = content => {
if (!content) return ''
let contentStr = ''
if (typeof content === 'string') {
contentStr = content.trim()
} else if (typeof content === 'object') {
if (content.value !== undefined) {
contentStr = String(content.value).trim()
if (!contentStr) return ''
} else if (content.matchedWords || content.matchLevel || content.fullyHighlighted !== undefined) {
return ''
} else {
try {
contentStr = JSON.stringify(content).trim()
if (contentStr === '{}' || contentStr === '[]' || contentStr === '""') {
return ''
}
} catch (e) {
return ''
}
}
} else if (content.toString && typeof content.toString === 'function') {
contentStr = content.toString().trim()
if (contentStr === '[object Object]' || contentStr === '[object Array]') {
return ''
}
} else {
return ''
}
const firstOccur = contentStr.indexOf('<mark>')
let start = firstOccur - 30
let end = firstOccur + 120
let pre = ''
let post = ''
if (start <= 0) {
start = 0
end = 140
} else {
pre = '...'
}
if (end > contentStr.length) {
end = contentStr.length
} else {
post = '...'
}
// Ensure we don't cut off HTML tags in the middle
let substr = contentStr.substring(start, end)
// Handle tag completeness
// Check for incomplete opening tags at the beginning
const firstCloseBracket = substr.indexOf('>')
const firstOpenBracket = substr.indexOf('<')
// If there's a closing bracket but no opening bracket before it, we've cut a tag
if (firstCloseBracket !== -1 && (firstOpenBracket === -1 || firstCloseBracket < firstOpenBracket)) {
substr = substr.substring(firstCloseBracket + 1)
}
// Check for incomplete closing tags at the end
const lastOpenBracket = substr.lastIndexOf('<')
const lastCloseBracket = substr.lastIndexOf('>')
// If there's an opening bracket after the last closing bracket, we've cut a tag
if (lastOpenBracket !== -1 && lastOpenBracket > lastCloseBracket) {
substr = substr.substring(0, lastOpenBracket)
}
// Balance tags in the substring
const tagStack = []
let balancedStr = ''
let i = 0
while (i < substr.length) {
if (substr[i] === '<') {
// Check if it's a closing tag
if (substr[i + 1] === '/') {
const closeTagEnd = substr.indexOf('>', i)
if (closeTagEnd !== -1) {
const closeTagName = substr.substring(i + 2, closeTagEnd)
// Remove matching opening tag from stack
for (let j = tagStack.length - 1; j >= 0; j--) {
if (tagStack[j] === closeTagName) {
tagStack.splice(j, 1)
break
}
}
balancedStr += substr.substring(i, closeTagEnd + 1)
i = closeTagEnd + 1
continue
}
} else if (substr.substr(i, 2) === '<!' || (substr.indexOf('/>', i) !== -1 && substr.indexOf('/>', i) < substr.indexOf('>', i))) {
const tagEnd = substr.indexOf('>', i)
if (tagEnd !== -1) {
balancedStr += substr.substring(i, tagEnd + 1)
i = tagEnd + 1
continue
}
} else {
const tagEnd = substr.indexOf('>', i)
if (tagEnd !== -1) {
const tagName = substr.substring(i + 1, (substr.indexOf(' ', i) > -1 && substr.indexOf(' ', i) < tagEnd)
? substr.indexOf(' ', i)
: tagEnd).split(/\s/)[0]
tagStack.push(tagName)
balancedStr += substr.substring(i, tagEnd + 1)
i = tagEnd + 1
continue
}
}
}
balancedStr += substr[i]
i++
}
// Close any unclosed tags
while (tagStack.length > 0) {
const tagName = tagStack.pop()
balancedStr += `</${tagName}>`
}
// If we removed content from the beginning, add prefix
if (start > 0 || pre) {
const actualFirstOpenBracket = contentStr.indexOf('<', start > 0 ? start - 30 : 0)
const actualFirstMark = contentStr.indexOf('<mark>', start > 0 ? start - 30 : 0)
if (actualFirstOpenBracket !== -1 &&
(actualFirstMark === -1 || actualFirstOpenBracket < actualFirstMark)) {
pre = '...'
}
}
substr = balancedStr
return `${pre}${substr}${post}`
}
// Helper function to handle Algolia highlight results
const extractHighlightValue = highlightObj => {
if (!highlightObj) return ''
if (typeof highlightObj === 'string') {
return highlightObj.trim()
}
if (typeof highlightObj === 'object' && highlightObj.value !== undefined) {
return String(highlightObj.value).trim()
}
return ''
}
// Initialize Algolia client
let searchClient
if (window['algoliasearch/lite'] && typeof window['algoliasearch/lite'].liteClient === 'function') {
searchClient = window['algoliasearch/lite'].liteClient(appId, apiKey)
} else if (typeof window.algoliasearch === 'function') {
searchClient = window.algoliasearch(appId, apiKey)
} else {
return console.error('Algolia search client not found!')
}
if (!searchClient) {
return console.error('Failed to initialize Algolia search client')
}
// Search state
let currentQuery = ''
// Show loading state
const showLoading = show => {
const loadingIndicator = document.getElementById('loading-status')
if (loadingIndicator) {
loadingIndicator.hidden = !show
}
}
// Cache frequently used elements
const elements = {
get searchInput () { return document.querySelector('#algolia-search-input .ais-SearchBox-input') },
get hits () { return document.getElementById('algolia-hits') },
get hitsEmpty () { return document.getElementById('algolia-hits-empty') },
get hitsList () { return document.querySelector('#algolia-hits .ais-Hits-list') },
get hitsWrapper () { return document.querySelector('#algolia-hits .ais-Hits') },
get pagination () { return document.getElementById('algolia-pagination') },
get paginationList () { return document.querySelector('#algolia-pagination .ais-Pagination-list') },
get stats () { return document.querySelector('#algolia-info .ais-Stats-text') },
}
// Show/hide search results area
const toggleResultsVisibility = hasResults => {
elements.pagination.style.display = hasResults ? '' : 'none'
elements.stats.style.display = hasResults ? '' : 'none'
}
// Render search results
const renderHits = (hits, query, page = 0) => {
if (hits.length === 0 && query) {
elements.hitsEmpty.textContent = languages.hits_empty.replace(/\$\{query}/, query)
elements.hitsEmpty.style.display = ''
elements.hitsWrapper.style.display = 'none'
elements.stats.style.display = 'none'
return
}
elements.hitsEmpty.style.display = 'none'
const hitsHTML = hits.map((hit, index) => {
const itemNumber = page * hitsPerPage + index + 1
const link = hit.permalink || (GLOBAL_CONFIG.root + hit.path)
const result = hit._highlightResult || hit
// Content extraction
let content = ''
try {
if (result.contentStripTruncate) {
content = cutContent(result.contentStripTruncate)
} else if (result.contentStrip) {
content = cutContent(result.contentStrip)
} else if (result.content) {
content = cutContent(result.content)
} else if (hit.contentStripTruncate) {
content = cutContent(hit.contentStripTruncate)
} else if (hit.contentStrip) {
content = cutContent(hit.contentStrip)
} else if (hit.content) {
content = cutContent(hit.content)
}
} catch (error) {
content = ''
}
// Title handling
let title = 'no-title'
try {
if (result.title) {
title = extractHighlightValue(result.title) || 'no-title'
} else if (hit.title) {
title = extractHighlightValue(hit.title) || 'no-title'
}
if (!title || title === 'no-title') {
if (typeof hit.title === 'string' && hit.title.trim()) {
title = hit.title.trim()
} else if (hit.title && typeof hit.title === 'object' && hit.title.value) {
title = String(hit.title.value).trim() || 'no-title'
} else {
title = 'no-title'
}
}
} catch (error) {
title = 'no-title'
}
return `
<li class="ais-Hits-item" value="${itemNumber}">
<a href="${link}" class="algolia-hit-item-link">
<span class="algolia-hits-item-title">${title}</span>
${content ? `<div class="algolia-hit-item-content">${content}</div>` : ''}
</a>
</li>`
}).join('')
elements.hitsList.innerHTML = hitsHTML
elements.hitsWrapper.style.display = query ? '' : 'none'
if (hits.length > 0) {
elements.stats.style.display = ''
}
}
// Render pagination
const renderPagination = (page, nbPages) => {
if (nbPages <= 1) {
elements.pagination.style.display = 'none'
elements.paginationList.innerHTML = ''
return
}
elements.pagination.style.display = 'block'
const isFirstPage = page === 0
const isLastPage = page === nbPages - 1
// Responsive page display
const isMobile = window.innerWidth < 768
const maxVisiblePages = isMobile ? 3 : 5
let startPage = Math.max(0, page - Math.floor(maxVisiblePages / 2))
const endPage = Math.min(nbPages - 1, startPage + maxVisiblePages - 1)
// Adjust starting page to maintain max visible pages
if (endPage - startPage + 1 < maxVisiblePages) {
startPage = Math.max(0, endPage - maxVisiblePages + 1)
}
let pagesHTML = ''
// Only add ellipsis and first page when there are many pages
if (nbPages > maxVisiblePages && startPage > 0) {
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--page">
<a class="ais-Pagination-link" aria-label="Page 1" href="#" data-page="0">1</a>
</li>`
if (startPage > 1) {
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--ellipsis">
<span class="ais-Pagination-link">...</span>
</li>`
}
}
// Add middle page numbers
for (let i = startPage; i <= endPage; i++) {
const isSelected = i === page
if (isSelected) {
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--page ais-Pagination-item--selected">
<span class="ais-Pagination-link" aria-label="Page ${i + 1}">${i + 1}</span>
</li>`
} else {
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--page">
<a class="ais-Pagination-link" aria-label="Page ${i + 1}" href="#" data-page="${i}">${i + 1}</a>
</li>`
}
}
// Only add ellipsis and last page when there are many pages
if (nbPages > maxVisiblePages && endPage < nbPages - 1) {
if (endPage < nbPages - 2) {
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--ellipsis">
<span class="ais-Pagination-link">...</span>
</li>`
}
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--page">
<a class="ais-Pagination-link" aria-label="Page ${nbPages}" href="#" data-page="${nbPages - 1}">${nbPages}</a>
</li>`
}
if (nbPages > 1) {
elements.paginationList.innerHTML = `
<li class="ais-Pagination-item ais-Pagination-item--previousPage ${isFirstPage ? 'ais-Pagination-item--disabled' : ''}">
${isFirstPage
? '<span class="ais-Pagination-link ais-Pagination-link--disabled" aria-label="Previous Page"><i class="fas fa-angle-left"></i></span>'
: `<a class="ais-Pagination-link" aria-label="Previous Page" href="#" data-page="${page - 1}"><i class="fas fa-angle-left"></i></a>`
}
</li>
${pagesHTML}
<li class="ais-Pagination-item ais-Pagination-item--nextPage ${isLastPage ? 'ais-Pagination-item--disabled' : ''}">
${isLastPage
? '<span class="ais-Pagination-link ais-Pagination-link--disabled" aria-label="Next Page"><i class="fas fa-angle-right"></i></span>'
: `<a class="ais-Pagination-link" aria-label="Next Page" href="#" data-page="${page + 1}"><i class="fas fa-angle-right"></i></a>`
}
</li>`
elements.pagination.style.display = currentQuery ? '' : 'none'
} else {
elements.pagination.style.display = 'none'
}
}
// Render statistics
const renderStats = (nbHits, processingTimeMS, query) => {
if (query) {
const stats = languages.hits_stats
.replace(/\$\{hits}/, nbHits)
.replace(/\$\{time}/, processingTimeMS)
elements.stats.innerHTML = `<hr>${stats}`
elements.stats.style.display = ''
} else {
elements.stats.style.display = 'none'
}
}
// Perform search
const performSearch = async (query, page = 0) => {
if (!query.trim()) {
currentQuery = ''
renderHits([], '', 0)
renderPagination(0, 0)
renderStats(0, 0, '')
toggleResultsVisibility(false)
return
}
showLoading(true)
currentQuery = query
try {
let result
if (searchClient && typeof searchClient.search === 'function') {
// v5 multi-index search
const searchResult = await searchClient.search([{
indexName,
query,
params: {
page,
hitsPerPage,
highlightPreTag: '<mark>',
highlightPostTag: '</mark>',
attributesToHighlight: ['title', 'content', 'contentStrip', 'contentStripTruncate']
}
}])
result = searchResult.results[0]
} else if (searchClient && typeof searchClient.initIndex === 'function') {
// v4 single-index search
const index = searchClient.initIndex(indexName)
result = await index.search(query, {
page,
hitsPerPage,
highlightPreTag: '<mark>',
highlightPostTag: '</mark>',
attributesToHighlight: ['title', 'content', 'contentStrip', 'contentStripTruncate']
})
} else {
throw new Error('Algolia: No compatible search method available')
}
renderHits(result.hits || [], query, page)
const actualNbPages = result.nbHits <= hitsPerPage ? 1 : (result.nbPages || 0)
renderPagination(page, actualNbPages)
renderStats(result.nbHits || 0, result.processingTimeMS || 0, query)
const hasResults = result.hits && result.hits.length > 0
toggleResultsVisibility(hasResults)
// Refresh Pjax links
if (window.pjax) {
window.pjax.refresh(document.getElementById('algolia-hits'))
}
} catch (error) {
console.error('Algolia search error:', error)
renderHits([], query, page)
renderPagination(0, 0)
renderStats(0, 0, query)
} finally {
showLoading(false)
}
}
// Debounced search
let searchTimeout
const debouncedSearch = (query, delay = 300) => {
clearTimeout(searchTimeout)
searchTimeout = setTimeout(() => performSearch(query), delay)
}
// Initialize search box and events
const initializeSearch = () => {
showLoading(false)
if (elements.searchInput) {
elements.searchInput.addEventListener('input', e => {
const query = e.target.value
debouncedSearch(query)
})
}
const searchForm = document.querySelector('#algolia-search-input .ais-SearchBox-form')
if (searchForm) {
searchForm.addEventListener('submit', e => {
e.preventDefault()
const query = elements.searchInput.value
performSearch(query)
})
}
// Pagination event delegation
elements.pagination.addEventListener('click', e => {
e.preventDefault()
const link = e.target.closest('a[data-page]')
if (link) {
const page = parseInt(link.dataset.page, 10)
if (!isNaN(page) && currentQuery) {
performSearch(currentQuery, page)
}
}
})
// Initial state
toggleResultsVisibility(false)
}
// Initialize
initializeSearch()
searchClickFn()
searchFnOnce()
window.addEventListener('pjax:complete', () => {
if (!btf.isHidden($searchMask)) closeSearch()
searchClickFn()
})
})

567
js/search/local-search.js Normal file
View File

@@ -0,0 +1,567 @@
/**
* Refer to hexo-generator-searchdb
* https://github.com/next-theme/hexo-generator-searchdb/blob/main/dist/search.js
* Modified by hexo-theme-butterfly
*/
class LocalSearch {
constructor ({
path = '',
unescape = false,
top_n_per_article = 1
}) {
this.path = path
this.unescape = unescape
this.top_n_per_article = top_n_per_article
this.isfetched = false
this.datas = null
}
getIndexByWord (words, text, caseSensitive = false) {
const index = []
const included = new Set()
if (!caseSensitive) {
text = text.toLowerCase()
}
words.forEach(word => {
if (this.unescape) {
const div = document.createElement('div')
div.innerText = word
word = div.innerHTML
}
const wordLen = word.length
if (wordLen === 0) return
let startPosition = 0
let position = -1
if (!caseSensitive) {
word = word.toLowerCase()
}
while ((position = text.indexOf(word, startPosition)) > -1) {
index.push({ position, word })
included.add(word)
startPosition = position + wordLen
}
})
// Sort index by position of keyword
index.sort((left, right) => {
if (left.position !== right.position) {
return left.position - right.position
}
return right.word.length - left.word.length
})
return [index, included]
}
// Merge hits into slices
mergeIntoSlice (start, end, index) {
let item = index[0]
let { position, word } = item
const hits = []
const count = new Set()
while (position + word.length <= end && index.length !== 0) {
count.add(word)
hits.push({
position,
length: word.length
})
const wordEnd = position + word.length
// Move to next position of hit
index.shift()
while (index.length !== 0) {
item = index[0]
position = item.position
word = item.word
if (wordEnd > position) {
index.shift()
} else {
break
}
}
}
return {
hits,
start,
end,
count: count.size
}
}
// Highlight title and content
highlightKeyword (val, slice) {
let result = ''
let index = slice.start
for (const { position, length } of slice.hits) {
result += val.substring(index, position)
index = position + length
result += `<mark class="search-keyword">${val.substr(position, length)}</mark>`
}
result += val.substring(index, slice.end)
return result
}
getResultItems (keywords) {
const resultItems = []
this.datas.forEach(({ title, content, url }) => {
// The number of different keywords included in the article.
const [indexOfTitle, keysOfTitle] = this.getIndexByWord(keywords, title)
const [indexOfContent, keysOfContent] = this.getIndexByWord(keywords, content)
const includedCount = new Set([...keysOfTitle, ...keysOfContent]).size
// Show search results
const hitCount = indexOfTitle.length + indexOfContent.length
if (hitCount === 0) return
const slicesOfTitle = []
if (indexOfTitle.length !== 0) {
slicesOfTitle.push(this.mergeIntoSlice(0, title.length, indexOfTitle))
}
let slicesOfContent = []
while (indexOfContent.length !== 0) {
const item = indexOfContent[0]
const { position } = item
// Cut out 120 characters. The maxlength of .search-input is 80.
const start = Math.max(0, position - 20)
const end = Math.min(content.length, position + 100)
slicesOfContent.push(this.mergeIntoSlice(start, end, indexOfContent))
}
// Sort slices in content by included keywords' count and hits' count
slicesOfContent.sort((left, right) => {
if (left.count !== right.count) {
return right.count - left.count
} else if (left.hits.length !== right.hits.length) {
return right.hits.length - left.hits.length
}
return left.start - right.start
})
// Select top N slices in content
const upperBound = parseInt(this.top_n_per_article, 10)
if (upperBound >= 0) {
slicesOfContent = slicesOfContent.slice(0, upperBound)
}
let resultItem = ''
url = new URL(url, location.origin)
url.searchParams.append('highlight', keywords.join(' '))
if (slicesOfTitle.length !== 0) {
resultItem += `<li class="local-search-hit-item"><a href="${url.href}"><span class="search-result-title">${this.highlightKeyword(title, slicesOfTitle[0])}</span>`
} else {
resultItem += `<li class="local-search-hit-item"><a href="${url.href}"><span class="search-result-title">${title}</span>`
}
slicesOfContent.forEach(slice => {
resultItem += `<p class="search-result">${this.highlightKeyword(content, slice)}...</p>`
})
resultItem += '</a></li>'
resultItems.push({
item: resultItem,
id: resultItems.length,
hitCount,
includedCount
})
})
return resultItems
}
fetchData () {
const isXml = !this.path.endsWith('json')
fetch(this.path)
.then(response => response.text())
.then(res => {
// Get the contents from search data
this.isfetched = true
this.datas = isXml
? [...new DOMParser().parseFromString(res, 'text/xml').querySelectorAll('entry')].map(element => ({
title: element.querySelector('title').textContent,
content: element.querySelector('content').textContent,
url: element.querySelector('url').textContent
}))
: JSON.parse(res)
// Only match articles with non-empty titles
this.datas = this.datas.filter(data => data.title).map(data => {
data.title = data.title.trim()
data.content = data.content ? data.content.trim().replace(/<[^>]+>/g, '') : ''
data.url = decodeURIComponent(data.url).replace(/\/{2,}/g, '/')
return data
})
// Remove loading animation
window.dispatchEvent(new Event('search:loaded'))
})
}
// Highlight by wrapping node in mark elements with the given class name
highlightText (node, slice, className) {
const val = node.nodeValue
let index = slice.start
const children = []
for (const { position, length } of slice.hits) {
const text = document.createTextNode(val.substring(index, position))
index = position + length
const mark = document.createElement('mark')
mark.className = className
mark.appendChild(document.createTextNode(val.substr(position, length)))
children.push(text, mark)
}
node.nodeValue = val.substring(index, slice.end)
children.forEach(element => {
node.parentNode.insertBefore(element, node)
})
}
// Highlight the search words provided in the url in the text
highlightSearchWords (body) {
const params = new URL(location.href).searchParams.get('highlight')
const keywords = params ? params.split(' ') : []
if (!keywords.length || !body) return
const walk = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, null)
const allNodes = []
while (walk.nextNode()) {
if (!walk.currentNode.parentNode.matches('button, select, textarea, .mermaid')) allNodes.push(walk.currentNode)
}
allNodes.forEach(node => {
const [indexOfNode] = this.getIndexByWord(keywords, node.nodeValue)
if (!indexOfNode.length) return
const slice = this.mergeIntoSlice(0, node.nodeValue.length, indexOfNode)
this.highlightText(node, slice, 'search-keyword')
})
}
}
window.addEventListener('load', () => {
// Search
const { path, top_n_per_article, unescape, languages, pagination } = GLOBAL_CONFIG.localSearch
const enablePagination = pagination && pagination.enable
const localSearch = new LocalSearch({
path,
top_n_per_article,
unescape
})
const input = document.querySelector('.local-search-input input')
const statsItem = document.getElementById('local-search-stats')
const $loadingStatus = document.getElementById('loading-status')
const isXml = !path.endsWith('json')
// Pagination variables (only initialize if pagination is enabled)
let currentPage = 0
const hitsPerPage = pagination.hitsPerPage || 10
let currentResultItems = []
if (!enablePagination) {
// If pagination is disabled, we don't need these variables
currentPage = undefined
currentResultItems = undefined
}
// Cache frequently used elements
const elements = {
get pagination () { return document.getElementById('local-search-pagination') },
get paginationList () { return document.querySelector('#local-search-pagination .ais-Pagination-list') }
}
// Show/hide search results area
const toggleResultsVisibility = hasResults => {
if (enablePagination) {
elements.pagination.style.display = hasResults ? '' : 'none'
} else {
elements.pagination.style.display = 'none'
}
}
// Render search results for current page
const renderResults = (searchText, resultItems) => {
const container = document.getElementById('local-search-results')
// Determine items to display based on pagination mode
const itemsToDisplay = enablePagination
? currentResultItems.slice(currentPage * hitsPerPage, (currentPage + 1) * hitsPerPage)
: resultItems
// Handle empty page in pagination mode
if (enablePagination && itemsToDisplay.length === 0 && currentResultItems.length > 0) {
currentPage = 0
renderResults(searchText, resultItems)
return
}
// Add numbering to items
const numberedItems = itemsToDisplay.map((result, index) => {
const itemNumber = enablePagination
? currentPage * hitsPerPage + index + 1
: index + 1
return result.item.replace(
'<li class="local-search-hit-item">',
`<li class="local-search-hit-item" value="${itemNumber}">`
)
})
container.innerHTML = `<ol class="search-result-list">${numberedItems.join('')}</ol>`
// Update stats
const displayCount = enablePagination ? currentResultItems.length : resultItems.length
const stats = languages.hits_stats.replace(/\$\{hits}/, displayCount)
statsItem.innerHTML = `<hr><div class="search-result-stats">${stats}</div>`
// Handle pagination
if (enablePagination) {
const nbPages = Math.ceil(currentResultItems.length / hitsPerPage)
renderPagination(currentPage, nbPages, searchText)
}
const hasResults = resultItems.length > 0
toggleResultsVisibility(hasResults)
window.pjax && window.pjax.refresh(container)
}
// Render pagination
const renderPagination = (page, nbPages, query) => {
if (nbPages <= 1) {
elements.pagination.style.display = 'none'
elements.paginationList.innerHTML = ''
return
}
elements.pagination.style.display = 'block'
const isFirstPage = page === 0
const isLastPage = page === nbPages - 1
// Responsive page display
const isMobile = window.innerWidth < 768
const maxVisiblePages = isMobile ? 3 : 5
let startPage = Math.max(0, page - Math.floor(maxVisiblePages / 2))
const endPage = Math.min(nbPages - 1, startPage + maxVisiblePages - 1)
// Adjust starting page to maintain max visible pages
if (endPage - startPage + 1 < maxVisiblePages) {
startPage = Math.max(0, endPage - maxVisiblePages + 1)
}
let pagesHTML = ''
// Only add ellipsis and first page when there are many pages
if (nbPages > maxVisiblePages && startPage > 0) {
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--page">
<a class="ais-Pagination-link" aria-label="Page 1" href="#" data-page="0">1</a>
</li>`
if (startPage > 1) {
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--ellipsis">
<span class="ais-Pagination-link">...</span>
</li>`
}
}
// Add middle page numbers
for (let i = startPage; i <= endPage; i++) {
const isSelected = i === page
if (isSelected) {
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--page ais-Pagination-item--selected">
<span class="ais-Pagination-link" aria-label="Page ${i + 1}">${i + 1}</span>
</li>`
} else {
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--page">
<a class="ais-Pagination-link" aria-label="Page ${i + 1}" href="#" data-page="${i}">${i + 1}</a>
</li>`
}
}
// Only add ellipsis and last page when there are many pages
if (nbPages > maxVisiblePages && endPage < nbPages - 1) {
if (endPage < nbPages - 2) {
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--ellipsis">
<span class="ais-Pagination-link">...</span>
</li>`
}
pagesHTML += `
<li class="ais-Pagination-item ais-Pagination-item--page">
<a class="ais-Pagination-link" aria-label="Page ${nbPages}" href="#" data-page="${nbPages - 1}">${nbPages}</a>
</li>`
}
if (nbPages > 1) {
elements.paginationList.innerHTML = `
<li class="ais-Pagination-item ais-Pagination-item--previousPage ${isFirstPage ? 'ais-Pagination-item--disabled' : ''}">
${isFirstPage
? '<span class="ais-Pagination-link ais-Pagination-link--disabled" aria-label="Previous Page"><i class="fas fa-angle-left"></i></span>'
: `<a class="ais-Pagination-link" aria-label="Previous Page" href="#" data-page="${page - 1}"><i class="fas fa-angle-left"></i></a>`
}
</li>
${pagesHTML}
<li class="ais-Pagination-item ais-Pagination-item--nextPage ${isLastPage ? 'ais-Pagination-item--disabled' : ''}">
${isLastPage
? '<span class="ais-Pagination-link ais-Pagination-link--disabled" aria-label="Next Page"><i class="fas fa-angle-right"></i></span>'
: `<a class="ais-Pagination-link" aria-label="Next Page" href="#" data-page="${page + 1}"><i class="fas fa-angle-right"></i></a>`
}
</li>`
} else {
elements.pagination.style.display = 'none'
}
}
// Clear search results and stats
const clearSearchResults = () => {
const container = document.getElementById('local-search-results')
container.textContent = ''
statsItem.textContent = ''
toggleResultsVisibility(false)
if (enablePagination) {
currentResultItems = []
currentPage = 0
}
}
// Show no results message
const showNoResults = searchText => {
const container = document.getElementById('local-search-results')
container.textContent = ''
const statsDiv = document.createElement('div')
statsDiv.className = 'search-result-stats'
statsDiv.textContent = languages.hits_empty.replace(/\$\{query}/, searchText)
statsItem.innerHTML = statsDiv.outerHTML
toggleResultsVisibility(false)
if (enablePagination) {
currentResultItems = []
currentPage = 0
}
}
const inputEventFunction = () => {
if (!localSearch.isfetched) return
let searchText = input.value.trim().toLowerCase()
isXml && (searchText = searchText.replace(/</g, '&lt;').replace(/>/g, '&gt;'))
if (searchText !== '') $loadingStatus.hidden = false
const keywords = searchText.split(/[-\s]+/)
let resultItems = []
if (searchText.length > 0) {
resultItems = localSearch.getResultItems(keywords)
}
if (keywords.length === 1 && keywords[0] === '') {
clearSearchResults()
} else if (resultItems.length === 0) {
showNoResults(searchText)
} else {
// Sort results by relevance
resultItems.sort((left, right) => {
if (left.includedCount !== right.includedCount) {
return right.includedCount - left.includedCount
} else if (left.hitCount !== right.hitCount) {
return right.hitCount - left.hitCount
}
return right.id - left.id
})
if (enablePagination) {
currentResultItems = resultItems
currentPage = 0
}
renderResults(searchText, resultItems)
}
$loadingStatus.hidden = true
}
let loadFlag = false
const $searchMask = document.getElementById('search-mask')
const $searchDialog = document.querySelector('#local-search .search-dialog')
// fix safari
const fixSafariHeight = () => {
if (window.innerWidth < 768) {
$searchDialog.style.setProperty('--search-height', window.innerHeight + 'px')
}
}
const openSearch = () => {
btf.overflowPaddingR.add()
btf.animateIn($searchMask, 'to_show 0.5s')
btf.animateIn($searchDialog, 'titleScale 0.5s')
setTimeout(() => { input.focus() }, 300)
if (!loadFlag) {
!localSearch.isfetched && localSearch.fetchData()
input.addEventListener('input', inputEventFunction)
loadFlag = true
}
// shortcut: ESC
document.addEventListener('keydown', function f (event) {
if (event.code === 'Escape') {
closeSearch()
document.removeEventListener('keydown', f)
}
})
fixSafariHeight()
window.addEventListener('resize', fixSafariHeight)
}
const closeSearch = () => {
btf.overflowPaddingR.remove()
btf.animateOut($searchDialog, 'search_close .5s')
btf.animateOut($searchMask, 'to_hide 0.5s')
window.removeEventListener('resize', fixSafariHeight)
}
const searchClickFn = () => {
btf.addEventListenerPjax(document.querySelector('#search-button > .search'), 'click', openSearch)
}
const searchFnOnce = () => {
document.querySelector('#local-search .search-close-button').addEventListener('click', closeSearch)
$searchMask.addEventListener('click', closeSearch)
if (GLOBAL_CONFIG.localSearch.preload) {
localSearch.fetchData()
}
localSearch.highlightSearchWords(document.getElementById('article-container'))
// Pagination event delegation - only add if pagination is enabled
if (enablePagination) {
elements.pagination.addEventListener('click', e => {
e.preventDefault()
const link = e.target.closest('a[data-page]')
if (link) {
const page = parseInt(link.dataset.page, 10)
if (!isNaN(page) && currentResultItems.length > 0) {
currentPage = page
renderResults(input.value.trim().toLowerCase(), currentResultItems)
}
}
})
}
// Initial state
toggleResultsVisibility(false)
}
window.addEventListener('search:loaded', () => {
const $loadDataItem = document.getElementById('loading-database')
$loadDataItem.nextElementSibling.style.visibility = 'visible'
$loadDataItem.remove()
})
searchClickFn()
searchFnOnce()
// pjax
window.addEventListener('pjax:complete', () => {
!btf.isHidden($searchMask) && closeSearch()
localSearch.highlightSearchWords(document.getElementById('article-container'))
searchClickFn()
})
})

321
js/shuoshuo.js Normal file
View File

@@ -0,0 +1,321 @@
function renderTalks() {
const talkContainer = document.querySelector('#talk');
if (!talkContainer) return;
talkContainer.innerHTML = '';
const generateIconSVG = () => {
return `<svg viewBox="0 0 512 512"xmlns="http://www.w3.org/2000/svg"class="is-badge icon"><path d="m512 268c0 17.9-4.3 34.5-12.9 49.7s-20.1 27.1-34.6 35.4c.4 2.7.6 6.9.6 12.6 0 27.1-9.1 50.1-27.1 69.1-18.1 19.1-39.9 28.6-65.4 28.6-11.4 0-22.3-2.1-32.6-6.3-8 16.4-19.5 29.6-34.6 39.7-15 10.2-31.5 15.2-49.4 15.2-18.3 0-34.9-4.9-49.7-14.9-14.9-9.9-26.3-23.2-34.3-40-10.3 4.2-21.1 6.3-32.6 6.3-25.5 0-47.4-9.5-65.7-28.6-18.3-19-27.4-42.1-27.4-69.1 0-3 .4-7.2 1.1-12.6-14.5-8.4-26-20.2-34.6-35.4-8.5-15.2-12.8-31.8-12.8-49.7 0-19 4.8-36.5 14.3-52.3s22.3-27.5 38.3-35.1c-4.2-11.4-6.3-22.9-6.3-34.3 0-27 9.1-50.1 27.4-69.1s40.2-28.6 65.7-28.6c11.4 0 22.3 2.1 32.6 6.3 8-16.4 19.5-29.6 34.6-39.7 15-10.1 31.5-15.2 49.4-15.2s34.4 5.1 49.4 15.1c15 10.1 26.6 23.3 34.6 39.7 10.3-4.2 21.1-6.3 32.6-6.3 25.5 0 47.3 9.5 65.4 28.6s27.1 42.1 27.1 69.1c0 12.6-1.9 24-5.7 34.3 16 7.6 28.8 19.3 38.3 35.1 9.5 15.9 14.3 33.4 14.3 52.4zm-266.9 77.1 105.7-158.3c2.7-4.2 3.5-8.8 2.6-13.7-1-4.9-3.5-8.8-7.7-11.4-4.2-2.7-8.8-3.6-13.7-2.9-5 .8-9 3.2-12 7.4l-93.1 140-42.9-42.8c-3.8-3.8-8.2-5.6-13.1-5.4-5 .2-9.3 2-13.1 5.4-3.4 3.4-5.1 7.7-5.1 12.9 0 5.1 1.7 9.4 5.1 12.9l58.9 58.9 2.9 2.3c3.4 2.3 6.9 3.4 10.3 3.4 6.7-.1 11.8-2.9 15.2-8.7z"fill="#1da1f2"></path></svg>`;
}
const waterfall = (a) => {
function b(a, b) {
var c = window.getComputedStyle(b);
return parseFloat(c["margin" + a]) || 0
}
function c(a) {
return a + "px"
}
function d(a) {
return parseFloat(a.style.top)
}
function e(a) {
return parseFloat(a.style.left)
}
function f(a) {
return a.clientWidth
}
function g(a) {
return a.clientHeight
}
function h(a) {
return d(a) + g(a) + b("Bottom", a)
}
function i(a) {
return e(a) + f(a) + b("Right", a)
}
function j(a) {
a = a.sort(function (a, b) {
return h(a) === h(b) ? e(b) - e(a) : h(b) - h(a)
})
}
function k(b) {
f(a) != t && (b.target.removeEventListener(b.type, arguments.callee), waterfall(a))
}
"string" == typeof a && (a = document.querySelector(a));
var l = [].map.call(a.children, function (a) {
return a.style.position = "absolute", a
});
a.style.position = "relative";
var m = [];
l.length && (l[0].style.top = "0px", l[0].style.left = c(b("Left", l[0])), m.push(l[0]));
for (var n = 1; n < l.length; n++) {
var o = l[n - 1],
p = l[n],
q = i(o) + f(p) <= f(a);
if (!q) break;
p.style.top = o.style.top, p.style.left = c(i(o) + b("Left", p)), m.push(p)
}
for (; n < l.length; n++) {
j(m);
var p = l[n],
r = m.pop();
p.style.top = c(h(r) + b("Top", p)), p.style.left = c(e(r)), m.push(p)
}
j(m);
var s = m[0];
a.style.height = c(h(s) + b("Bottom", s));
var t = f(a);
window.addEventListener ? window.addEventListener("resize", k) : document.body.onresize = k
};
const fetchAndRenderTalks = () => {
const url = 'https://mm.biss.click/api/echo/page';
const cacheKey = 'talksCache';
const cacheTimeKey = 'talksCacheTime';
const cacheDuration = 30 * 60 * 1000;
const cachedData = localStorage.getItem(cacheKey);
const cachedTime = localStorage.getItem(cacheTimeKey);
const now = Date.now();
if (cachedData && cachedTime && (now - cachedTime < cacheDuration)) {
renderTalksList(JSON.parse(cachedData));
} else {
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ page: 1, pageSize: 30 })
})
.then(res => res.json())
.then(data => {
if (data.code === 1 && data.data && Array.isArray(data.data.items)) {
localStorage.setItem(cacheKey, JSON.stringify(data.data.items));
localStorage.setItem(cacheTimeKey, now.toString());
renderTalksList(data.data.items);
}
})
.catch(err => console.error('Error fetching:', err));
}
};
const renderTalksList = (list) => {
list.map(formatTalk).forEach(item => talkContainer.appendChild(generateTalkElement(item)));
waterfall('#talk');
};
const formatTalk = (item) => {
const date = formatTime(item.created_at);
let content = item.content || '';
content = content.replace(/\[(.*?)\]\((.*?)\)/g, `<a href="$2" target="_blank" rel="nofollow noopener">@$1</a>`)
.replace(/- \[ \]/g, '⚪')
.replace(/- \[x\]/g, '⚫')
.replace(/\n/g, '<br>');
content = `<div class="talk_content_text">${content}</div>`;
// 图片
if (Array.isArray(item.images) && item.images.length > 0) {
const imgDiv = document.createElement('div');
imgDiv.className = 'zone_imgbox';
item.images.forEach(img => {
const link = document.createElement('a');
link.href = img.image_url + "?fmt=webp&q=75";
link.setAttribute('data-fancybox', 'gallery');
link.className = 'fancybox';
const imgTag = document.createElement('img');
imgTag.src = img.image_url + "?fmt=webp&q=75";
link.appendChild(imgTag);
imgDiv.appendChild(link);
});
content += imgDiv.outerHTML;
}
// 外链 / GitHub 项目
// 外链 / GitHub 项目
if (['WEBSITE', 'GITHUBPROJ'].includes(item.extension_type)) {
let siteUrl = '', title = '';
let extensionBack = "https://pic.biss.click/image/1971bdc1-4349-4bb9-b683-20404f5da7d7.webp";
// 解析 extension 字段
try {
const extObj = typeof item.extension === 'string' ? JSON.parse(item.extension) : item.extension;
siteUrl = extObj.site || extObj.url || item.extension;
title = extObj.title || siteUrl;
} catch {
siteUrl = item.extension;
title = siteUrl;
}
// 特殊处理 GitHub 项目
if (item.extension_type === 'GITHUBPROJ') {
extensionBack = "https://pic.biss.click/image/ed410d4e-d3f8-4b26-8840-50dd58f7dc4e.webp";
// 提取 GitHub 项目名
const match = siteUrl.match(/^https?:\/\/github\.com\/[^/]+\/([^/?#]+)/i);
if (match) {
title = match[1]; // 获取仓库名
} else {
// fallback从最后一个路径段提取
try {
const parts = new URL(siteUrl).pathname.split('/').filter(Boolean);
title = parts.pop() || siteUrl;
} catch {
// 如果 URL 无效则保留原始
}
}
}
// 输出 HTML 结构
content += `
<div class="shuoshuo-external-link">
<a class="external-link" href="${siteUrl}" target="_blank" rel="nofollow noopener">
<div class="external-link-left" style="background-image:url(${extensionBack})"></div>
<div class="external-link-right">
<div class="external-link-title">${title}</div>
<div>点击跳转<i class="fa-solid fa-angle-right"></i></div>
</div>
</a>
</div>`;
}
// 音乐
if (item.extension_type === 'MUSIC' && item.extension) {
const link = item.extension;
let server = '';
if (link.includes('music.163.com')) server = 'netease';
else if (link.includes('y.qq.com')) server = 'tencent';
const idMatch = link.match(/id=(\d+)/);
const id = idMatch ? idMatch[1] : '';
if (server && id) {
content += `<meting-js server="${server}" type="song" id="${id}" api="https://meting.qjqq.cn/?server=:server&type=:type&id=:id&auth=:auth&r=:r"></meting-js>`;
}
}
// 视频
if (item.extension_type === 'VIDEO' && item.extension) {
const video = item.extension;
if (video.startsWith('BV')) {
const bilibiliUrl = `https://www.bilibili.com/blackboard/html5mobileplayer.html?bvid=${video}&as_wide=1&high_quality=1&danmaku=0`;
content += `
<div style="position: relative; padding: 30% 45%; margin-top: 10px;">
<iframe style="position:absolute;width:100%;height:100%;left:0;top:0;border-radius:12px;"
src="${bilibiliUrl}"
frameborder="no"
allowfullscreen="true"
loading="lazy"></iframe>
</div>`;
} else {
const youtubeUrl = `https://www.youtube.com/embed/${video}`;
content += `
<div style="position: relative; padding: 30% 45%; margin-top: 10px;">
<iframe style="position:absolute;width:100%;height:100%;left:0;top:0;border-radius:12px;"
src="${youtubeUrl}"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen></iframe>
</div>`;
}
}
return {
content,
user: item.username || '匿名',
avatar: 'https://free.picui.cn/free/2025/08/10/689845496a283.png',
date,
location: '',
tags: Array.isArray(item.tags) && item.tags.length ? item.tags.map(t => t.name) : ['无标签'],
text: content.replace(/\[(.*?)\]\((.*?)\)/g, '[链接]')
};
};
const generateTalkElement = (item) => {
const talkItem = document.createElement('div');
talkItem.className = 'talk_item';
const talkMeta = document.createElement('div');
talkMeta.className = 'talk_meta';
const avatar = document.createElement('img');
avatar.className = 'no-lightbox avatar';
avatar.src = item.avatar;
const info = document.createElement('div');
info.className = 'info';
const nick = document.createElement('span');
nick.className = 'talk_nick';
nick.innerHTML = `${item.user} ${generateIconSVG()}`;
const date = document.createElement('span');
date.className = 'talk_date';
date.textContent = item.date;
info.appendChild(nick);
info.appendChild(date);
talkMeta.appendChild(avatar);
talkMeta.appendChild(info);
const talkContent = document.createElement('div');
talkContent.className = 'talk_content';
talkContent.innerHTML = item.content;
const talkBottom = document.createElement('div');
talkBottom.className = 'talk_bottom';
const tags = document.createElement('div');
const tag = document.createElement('span');
tag.className = 'talk_tag';
tag.textContent = `🏷️${item.tags}`;
//const loc = document.createElement('span');
//loc.className = 'location_tag';
//loc.textContent = `🌍${item.location}`;
tags.appendChild(tag);
//tags.appendChild(loc);
const commentLink = document.createElement('a');
commentLink.href = 'javascript:;';
commentLink.onclick = () => goComment(item.text);
const icon = document.createElement('span');
icon.className = 'icon';
icon.innerHTML = '<i class="fa-solid fa-message fa-fw"></i>';
commentLink.appendChild(icon);
talkBottom.appendChild(tags);
talkBottom.appendChild(commentLink);
talkItem.appendChild(talkMeta);
talkItem.appendChild(talkContent);
talkItem.appendChild(talkBottom);
return talkItem;
};
const goComment = (e) => {
const match = e.match(/<div class="talk_content_text">([\s\S]*?)<\/div>/);
const textContent = match ? match[1] : "";
const textarea = document.querySelector("#twikoo .el-textarea__inner");
textarea.value = `> ${textContent}\n\n`;
textarea.focus();
btf.snackbarShow("已为您引用该说说,不删除空格效果更佳");
};
const formatTime = (time) => {
const d = new Date(time);
const pad = (n) => n.toString().padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
};
fetchAndRenderTalks();
}
renderTalks();
// function whenDOMReady() {
// const talkContainer = document.querySelector('#talk');
// talkContainer.innerHTML = '';
// fetchAndRenderTalks();
// }
// whenDOMReady();
// document.addEventListener("pjax:complete", whenDOMReady);

97
js/shuoshuoshouye.js Normal file
View File

@@ -0,0 +1,97 @@
let talkTimer = null;
const cacheKey = 'talksCache';
const cacheTimeKey = 'talksCacheTime';
const cacheDuration = 30 * 60 * 1000; // 缓存有效期 30分钟
function indexTalk() {
if (talkTimer) {
clearInterval(talkTimer);
talkTimer = null;
}
if (!document.getElementById('bber-talk')) return;
function toText(ls) {
return ls.map(item => {
let c = item.content || '';
const hasImg = /\!\[.*?\]\(.*?\)/.test(c);
const hasLink = /\[.*?\]\(.*?\)/.test(c);
c = c
.replace(/#(.*?)\s/g, '')
.replace(/\{.*?\}/g, '')
.replace(/\!\[.*?\]\(.*?\)/g, '<i class="fa-solid fa-image"></i>')
.replace(/\[.*?\]\(.*?\)/g, '<i class="fa-solid fa-link"></i>');
const icons = [];
if (item.images?.length && !hasImg) icons.push('fa-solid fa-image');
if (item.extension_type === 'VIDEO') icons.push('fa-solid fa-video');
if (item.extension_type === 'MUSIC') icons.push('fa-solid fa-music');
if (item.extension_type === 'WEBSITE' && !hasLink) icons.push('fa-solid fa-link');
if (item.extension_type === 'GITHUBPROJ' && !hasLink) icons.push('fab fa-github');
if (icons.length) c += ' ' + icons.map(i => `<i class="${i}"></i>`).join(' ');
return c;
});
}
// 渲染与轮播
function talk(ls) {
let html = '';
ls.forEach((item, i) => {
html += `<li class="item item-${i + 1}">${item}</li>`;
});
let box = document.querySelector("#bber-talk .talk-list");
if (!box) return;
box.innerHTML = html;
talkTimer = setInterval(() => {
if (box.children.length > 0) {
box.appendChild(box.children[0]);
}
}, 3000);
}
const cachedData = localStorage.getItem(cacheKey);
const cachedTime = localStorage.getItem(cacheTimeKey);
const currentTime = new Date().getTime();
// 判断缓存是否有效
if (cachedData && cachedTime && (currentTime - cachedTime < cacheDuration)) {
const data = toText(JSON.parse(cachedData));
talk(data.slice(0, 6)); // 使用缓存渲染数据
} else {
fetch('https://mm.biss.click/api/echo/page', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ page: 1, pageSize: 30 })
})
.then(res => res.json())
.then(data => {
// 适配新版结构code=1 且 data.items 存在
if (data.code === 1 && data.data && Array.isArray(data.data.items)) {
localStorage.setItem(cacheKey, JSON.stringify(data.data.items));
localStorage.setItem(cacheTimeKey, currentTime.toString());
const formattedData = toText(data.data.items);
talk(formattedData.slice(0, 6));
} else {
console.warn('Unexpected API response format:', data);
}
})
.catch(error => console.error('Error fetching data:', error));
}
}
// pjax 支持
function whenDOMReady() {
indexTalk();
}
whenDOMReady();
document.addEventListener("pjax:complete", whenDOMReady);

11
js/statistic.js Normal file
View File

@@ -0,0 +1,11 @@
var _paq = window._paq = window._paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="https://statistic.biss.click/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', '1']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();

117
js/tw_cn.js Normal file

File diff suppressed because one or more lines are too long

545
js/typesense-search.js Normal file
View File

@@ -0,0 +1,545 @@
(function () {
'use strict';
// ============================================================================
// 配置区域 - 请根据实际情况修改
// ============================================================================
const CONFIG = {
apiKey: "VramSTWKUAggeZ5viQw8SlCwXQqmGCmA", // ⚠️ 建议使用 Search-Only API Key
server: {
host: "typesense.biss.click",
port: "443",
protocol: "https"
},
indexName: "blogs",
searchParams: {
query_by: "title,content",
highlight_full_fields: "title,content",
per_page: 8,
num_typos: 1,
typo_tokens_threshold: 1,
prefix: true
},
ui: {
maxRetries: 10,
retryDelay: 100,
animationDuration: 300
}
};
// ============================================================================
// 状态管理
// ============================================================================
let searchInstance = null;
let isInitialized = false;
let isSearchOpen = false;
let initRetryCount = 0;
const MAX_INIT_RETRIES = 30; // 最多重试30次 (3秒)
// ============================================================================
// 错误提示函数
// ============================================================================
function showErrorMessage() {
const hitsContainer = document.getElementById('hits');
if (!hitsContainer) return;
hitsContainer.innerHTML =
'<div class="ts-empty">' +
'<div style="color: #f44336;"><i class="fas fa-exclamation-triangle" style="font-size: 3rem;"></i></div>' +
'<div style="font-size: 1.1rem; font-weight: bold; margin: 15px 0;">搜索服务加载失败</div>' +
'<div style="font-size: 0.9rem; color: #666; line-height: 1.8;">' +
'<p>依赖库未能正确加载,请检查以下配置:</p>' +
'<ol style="text-align: left; max-width: 500px; margin: 15px auto;">' +
'<li>确认已在 <code>_config.butterfly.yml</code> 中正确引入依赖</li>' +
'<li>检查 JS 文件加载顺序(先 instantsearch.js再 adapter</li>' +
'<li>尝试更换 CDN 或使用本地文件</li>' +
'<li>打开浏览器控制台查看详细错误信息</li>' +
'</ol>' +
'</div>' +
'<div style="margin-top: 20px;">' +
'<button onclick="location.reload()" style="padding: 10px 20px; background: #49b1f5; color: white; border: none; border-radius: 5px; cursor: pointer;">重新加载页面</button>' +
'</div>' +
'</div>';
}
// ============================================================================
// 1. 动态插入 HTML 结构
// ============================================================================
const searchHTML = `
<div id="typesense-search-mask" class="ts-mask" style="display:none;">
<div id="typesense-search-container" class="ts-container">
<div class="ts-header">
<span class="ts-title">
<i class="fas fa-search"></i> 本站搜索
</span>
<span id="close-typesense" class="ts-close" aria-label="关闭搜索">&times;</span>
</div>
<div id="searchbox"></div>
<div id="stats" class="ts-stats"></div>
<div id="hits" class="ts-hits"></div>
<div id="pagination" class="ts-pagination"></div>
<div class="ts-footer">
<small>Search powered by <strong>Typesense</strong></small>
</div>
</div>
</div>
<style>
.ts-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 10000;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
opacity: 0;
transition: opacity 300ms ease;
}
.ts-mask.active { opacity: 1; }
.ts-container {
margin: 5% auto;
width: 90%;
max-width: 650px;
background: var(--search-bg, var(--card-bg, #fff));
padding: 25px;
border-radius: 12px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.3);
position: relative;
z-index: 10001;
transform: translateY(-50px);
opacity: 0;
transition: all 300ms ease;
}
.ts-mask.active .ts-container {
transform: translateY(0);
opacity: 1;
}
.ts-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
border-bottom: 2px solid var(--text-highlight-color, #49b1f5);
padding-bottom: 10px;
}
.ts-title {
font-size: 1.2rem;
font-weight: bold;
color: var(--text-highlight-color, #49b1f5);
}
.ts-close {
cursor: pointer;
font-size: 28px;
color: var(--font-color, #333);
line-height: 1;
transition: color 0.2s, transform 0.2s;
}
.ts-close:hover {
color: var(--text-highlight-color, #49b1f5);
transform: scale(1.1);
}
.ais-SearchBox-input {
position: relative;
z-index: 10002;
cursor: text;
padding: 12px 40px 12px 15px !important;
border-radius: 8px !important;
border: 2px solid #eee !important;
width: 100%;
outline: none;
transition: border-color 0.3s, box-shadow 0.3s;
background: var(--card-bg, #fff);
color: var(--font-color, #333);
font-size: 1rem;
}
.ais-SearchBox-input:focus {
border-color: var(--text-highlight-color, #49b1f5) !important;
box-shadow: 0 0 0 3px rgba(73, 177, 245, 0.1);
}
.ts-stats {
margin: 10px 0;
font-size: 0.85rem;
color: var(--font-color, #666);
opacity: 0.8;
}
.ts-hits {
max-height: 55vh;
overflow-y: auto;
margin-top: 15px;
padding-right: 5px;
}
.ts-hits::-webkit-scrollbar { width: 6px; }
.ts-hits::-webkit-scrollbar-track {
background: var(--card-bg, #f1f1f1);
border-radius: 10px;
}
.ts-hits::-webkit-scrollbar-thumb {
background: var(--text-highlight-color, #49b1f5);
border-radius: 10px;
}
.ts-empty {
text-align: center;
padding: 40px 20px;
color: var(--font-color, #999);
}
.ts-empty i {
font-size: 3rem;
margin-bottom: 15px;
opacity: 0.3;
}
.ts-empty code {
background: #f5f5f5;
padding: 2px 6px;
border-radius: 3px;
font-family: monospace;
color: #e91e63;
}
.ts-empty ol {
padding-left: 20px;
}
.ts-empty li {
margin: 8px 0;
}
.ts-result-item {
border-radius: 8px;
transition: all 0.2s ease;
margin-bottom: 10px;
padding: 15px;
border: 1px solid transparent;
text-decoration: none;
display: block;
background: var(--card-bg, #fff);
}
.ts-result-item:hover {
background: var(--text-bg-hover, rgba(73, 177, 245, 0.05));
border-color: var(--text-highlight-color, #49b1f5);
transform: translateX(5px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.ts-result-title {
font-weight: bold;
color: var(--text-highlight-color, #49b1f5);
font-size: 1.1rem;
margin-bottom: 8px;
display: block;
line-height: 1.4;
}
.ts-result-content {
font-size: 0.9rem;
color: var(--font-color, #666);
line-height: 1.6;
opacity: 0.85;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.ts-result-item mark {
background: #ffeb3b;
color: #000;
padding: 2px 4px;
border-radius: 3px;
font-weight: 500;
}
.ts-pagination {
margin-top: 20px;
display: flex;
justify-content: center;
gap: 5px;
}
.ais-Pagination-list {
display: flex;
list-style: none;
padding: 0;
margin: 0;
gap: 5px;
}
.ais-Pagination-link {
display: block;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 5px;
color: var(--font-color, #333);
text-decoration: none;
transition: all 0.2s;
background: var(--card-bg, #fff);
}
.ais-Pagination-link:hover {
background: var(--text-highlight-color, #49b1f5);
color: #fff;
}
.ais-Pagination-item--selected .ais-Pagination-link {
background: var(--text-highlight-color, #49b1f5);
color: #fff;
}
.ts-footer {
text-align: right;
margin-top: 15px;
border-top: 1px solid var(--border-color, #eee);
padding-top: 10px;
}
@media (max-width: 768px) {
.ts-container {
margin: 10px;
width: calc(100% - 20px);
padding: 20px 15px;
}
.ts-hits { max-height: 50vh; }
}
[data-theme="dark"] .ts-mask,
.dark-mode .ts-mask {
background: rgba(0, 0, 0, 0.85);
}
</style>
`;
document.body.insertAdjacentHTML('beforeend', searchHTML);
const mask = document.getElementById('typesense-search-mask');
const closeBtn = document.getElementById('close-typesense');
const container = document.getElementById('typesense-search-container');
// ============================================================================
// 搜索控制
// ============================================================================
function openSearch() {
if (isSearchOpen) return;
isSearchOpen = true;
mask.style.display = 'block';
void mask.offsetWidth;
mask.classList.add('active');
document.body.style.overflow = 'hidden';
if (!isInitialized) {
initTypesense();
}
focusSearchInput();
}
function closeSearch() {
if (!isSearchOpen) return;
isSearchOpen = false;
mask.classList.remove('active');
setTimeout(function() {
mask.style.display = 'none';
document.body.style.overflow = '';
}, CONFIG.ui.animationDuration);
}
function focusSearchInput(retryCount) {
retryCount = retryCount || 0;
const input = document.querySelector('.ais-SearchBox-input');
if (input) {
input.focus();
input.select();
} else if (retryCount < CONFIG.ui.maxRetries) {
setTimeout(function() {
focusSearchInput(retryCount + 1);
}, CONFIG.ui.retryDelay);
}
}
// ============================================================================
// 事件监听
// ============================================================================
document.addEventListener('click', function(e) {
if (e.target.closest('.search-typesense-trigger')) {
e.preventDefault();
openSearch();
}
});
closeBtn.addEventListener('click', closeSearch);
mask.addEventListener('click', function(e) {
if (e.target === mask) closeSearch();
});
container.addEventListener('click', function(e) {
e.stopPropagation();
});
window.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && isSearchOpen) {
closeSearch();
}
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
openSearch();
}
});
// ============================================================================
// Typesense 初始化(带重试限制)
// ============================================================================
function initTypesense() {
if (isInitialized || searchInstance) {
console.warn('Typesense 搜索已初始化');
return;
}
var instantsearchLoaded = typeof instantsearch !== 'undefined';
var adapterLoaded = typeof TypesenseInstantSearchAdapter !== 'undefined' ||
typeof window.TypesenseInstantSearchAdapter !== 'undefined';
console.log('📦 依赖库检查 (' + (initRetryCount + 1) + '/' + MAX_INIT_RETRIES + '):');
console.log(' instantsearch.js:', instantsearchLoaded ? '✅ 已加载' : '❌ 未加载');
console.log(' TypesenseAdapter:', adapterLoaded ? '✅ 已加载' : '❌ 未加载');
if (!instantsearchLoaded || !adapterLoaded) {
initRetryCount++;
if (initRetryCount >= MAX_INIT_RETRIES) {
console.error('');
console.error('❌ Typesense 依赖库加载失败!');
console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.error('');
console.error('🔧 请检查 _config.butterfly.yml 配置:');
console.error('');
console.error('inject:');
console.error(' bottom: # ⚠️ 使用 bottom 而不是 head');
console.error(' - <script src="https://cdn.jsdelivr.net/npm/instantsearch.js@4.56.0"></script>');
console.error(' - <script src="https://cdn.jsdelivr.net/npm/typesense-instantsearch-adapter@2.7.0/dist/typesense-instantsearch-adapter.min.js"></script>');
console.error(' - <script src="/js/typesense-search-fixed.js"></script>');
console.error('');
console.error('💡 或在控制台手动检查:');
console.error(' typeof instantsearch');
console.error(' typeof TypesenseInstantSearchAdapter');
console.error('');
console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
showErrorMessage();
return;
}
console.warn('⏳ 100ms 后重试...');
setTimeout(initTypesense, 100);
return;
}
initRetryCount = 0;
console.log('🚀 开始初始化 Typesense...');
try {
const typesenseAdapter = new TypesenseInstantSearchAdapter({
server: {
apiKey: CONFIG.apiKey,
nodes: [{
host: CONFIG.server.host,
port: CONFIG.server.port,
protocol: CONFIG.server.protocol
}],
cacheSearchResultsForSeconds: 120
},
additionalSearchParameters: CONFIG.searchParams
});
searchInstance = instantsearch({
searchClient: typesenseAdapter.searchClient,
indexName: CONFIG.indexName,
routing: false
});
searchInstance.addWidgets([
instantsearch.widgets.searchBox({
container: '#searchbox',
placeholder: '输入关键词寻找故事...',
autofocus: true,
showReset: true,
showSubmit: false,
showLoadingIndicator: true
}),
instantsearch.widgets.stats({
container: '#stats',
templates: {
text: function(data) {
if (!data.query) return '';
return '找到 <strong>' + data.nbHits + '</strong> 条结果 (' + data.processingTimeMS + 'ms)';
}
}
}),
instantsearch.widgets.hits({
container: '#hits',
templates: {
empty: function(results) {
return '<div class="ts-empty">' +
'<div><i class="fas fa-search"></i></div>' +
'<div>找不到与 "<strong>' + results.query + '</strong>" 相关的内容</div>' +
'<div style="margin-top: 10px;">试试其他关键词吧 (´·ω·`)</div>' +
'</div>';
},
item: function(hit) {
// 使用 _highlightResult 获取高亮文本
var titleHighlight = hit._highlightResult && hit._highlightResult.title
? hit._highlightResult.title.value
: (hit.title || '');
var contentHighlight = hit._highlightResult && hit._highlightResult.content
? hit._highlightResult.content.value
: (hit.content || '');
// 截取内容长度
if (contentHighlight.length > 200) {
contentHighlight = contentHighlight.substring(0, 200) + '...';
}
return '<a href="' + hit.url + '" class="ts-result-item">' +
'<div class="ts-result-title">' + titleHighlight + '</div>' +
'<div class="ts-result-content">' + contentHighlight + '</div>' +
'</a>';
}
}
}),
instantsearch.widgets.pagination({
container: '#pagination',
padding: 2,
showFirst: false,
showLast: false
})
]);
searchInstance.start();
isInitialized = true;
console.log('✅ Typesense 初始化成功!');
searchInstance.on('render', function() {
if (isSearchOpen) {
const input = document.querySelector('.ais-SearchBox-input');
if (input && document.activeElement !== input) {
input.focus();
}
}
});
} catch (error) {
console.error('❌ 初始化失败:', error);
showErrorMessage();
}
}
// ============================================================================
// 初始化
// ============================================================================
function init() {
console.log('🔍 Typesense 搜索已准备就绪');
console.log('💡 快捷键: Ctrl/Cmd + K');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// ============================================================================
// 全局接口
// ============================================================================
window.TypesenseSearch = {
open: openSearch,
close: closeSearch,
isOpen: function() { return isSearchOpen; },
getInstance: function() { return searchInstance; }
};
})();

338
js/utils.js Normal file
View File

@@ -0,0 +1,338 @@
(() => {
const btfFn = {
debounce: (func, wait = 0, immediate = false) => {
let timeout
return (...args) => {
const later = () => {
timeout = null
if (!immediate) func(...args)
}
const callNow = immediate && !timeout
clearTimeout(timeout)
timeout = setTimeout(later, wait)
if (callNow) func(...args)
}
},
throttle: (func, wait, options = {}) => {
let timeout, args
let previous = 0
const later = () => {
previous = options.leading === false ? 0 : new Date().getTime()
timeout = null
func(...args)
if (!timeout) args = null
}
return (...params) => {
const now = new Date().getTime()
if (!previous && options.leading === false) previous = now
const remaining = wait - (now - previous)
args = params
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
func(...args)
if (!timeout) args = null
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining)
}
}
},
overflowPaddingR: {
add: () => {
const paddingRight = window.innerWidth - document.body.clientWidth
if (paddingRight > 0) {
document.body.style.paddingRight = `${paddingRight}px`
document.body.style.overflow = 'hidden'
const menuElement = document.querySelector('#page-header.nav-fixed #menus')
if (menuElement) {
menuElement.style.paddingRight = `${paddingRight}px`
}
}
},
remove: () => {
document.body.style.paddingRight = ''
document.body.style.overflow = ''
const menuElement = document.querySelector('#page-header.nav-fixed #menus')
if (menuElement) {
menuElement.style.paddingRight = ''
}
}
},
snackbarShow: (text, showAction = false, duration = 2000) => {
const { position, bgLight, bgDark } = GLOBAL_CONFIG.Snackbar
const bg = document.documentElement.getAttribute('data-theme') === 'light' ? bgLight : bgDark
Snackbar.show({
text,
backgroundColor: bg,
showAction,
duration,
pos: position,
customClass: 'snackbar-css'
})
},
diffDate: (inputDate, more = false) => {
const dateNow = new Date()
const datePost = new Date(inputDate)
const diffMs = dateNow - datePost
const diffSec = diffMs / 1000
const diffMin = diffSec / 60
const diffHour = diffMin / 60
const diffDay = diffHour / 24
const diffMonth = diffDay / 30
const { dateSuffix } = GLOBAL_CONFIG
if (!more) return Math.floor(diffDay)
if (diffMonth > 12) return datePost.toISOString().slice(0, 10)
if (diffMonth >= 1) return `${Math.floor(diffMonth)} ${dateSuffix.month}`
if (diffDay >= 1) return `${Math.floor(diffDay)} ${dateSuffix.day}`
if (diffHour >= 1) return `${Math.floor(diffHour)} ${dateSuffix.hour}`
if (diffMin >= 1) return `${Math.floor(diffMin)} ${dateSuffix.min}`
return dateSuffix.just
},
loadComment: (dom, callback) => {
if ('IntersectionObserver' in window) {
const observerItem = new IntersectionObserver(entries => {
if (entries[0].isIntersecting) {
callback()
observerItem.disconnect()
}
}, { threshold: [0] })
observerItem.observe(dom)
} else {
callback()
}
},
scrollToDest: (pos, time = 500) => {
const currentPos = window.scrollY
const isNavFixed = document.getElementById('page-header').classList.contains('fixed')
if (currentPos > pos || isNavFixed) pos = pos - 70
if ('scrollBehavior' in document.documentElement.style) {
window.scrollTo({
top: pos,
behavior: 'smooth'
})
return
}
const startTime = performance.now()
const animate = currentTime => {
const timeElapsed = currentTime - startTime
const progress = Math.min(timeElapsed / time, 1)
window.scrollTo(0, currentPos + (pos - currentPos) * progress)
if (progress < 1) {
requestAnimationFrame(animate)
}
}
requestAnimationFrame(animate)
},
animateIn: (ele, animation) => {
ele.style.display = 'block'
ele.style.animation = animation
},
animateOut: (ele, animation) => {
const handleAnimationEnd = () => {
ele.style.display = ''
ele.style.animation = ''
ele.removeEventListener('animationend', handleAnimationEnd)
}
ele.addEventListener('animationend', handleAnimationEnd)
ele.style.animation = animation
},
wrap: (selector, eleType, options) => {
const createEle = document.createElement(eleType)
for (const [key, value] of Object.entries(options)) {
createEle.setAttribute(key, value)
}
selector.parentNode.insertBefore(createEle, selector)
createEle.appendChild(selector)
},
isHidden: ele => ele.offsetHeight === 0 && ele.offsetWidth === 0,
getEleTop: ele => ele.getBoundingClientRect().top + window.scrollY,
loadLightbox: ele => {
const service = GLOBAL_CONFIG.lightbox
if (service === 'medium_zoom') {
mediumZoom(ele, { background: 'var(--zoom-bg)' })
return
}
if (service === 'fancybox') {
ele.forEach(i => {
if (i.parentNode.tagName !== 'A') {
const dataSrc = i.dataset.lazySrc || i.src
const dataCaption = i.title || i.alt || ''
btf.wrap(i, 'a', { href: dataSrc, 'data-fancybox': 'gallery', 'data-caption': dataCaption, 'data-thumb': dataSrc })
}
})
if (!window.fancyboxRun) {
let options = ''
if (Fancybox.version < '6') {
options = {
Hash: false,
Thumbs: {
showOnStart: false
},
Images: {
Panzoom: {
maxScale: 4
}
},
Carousel: {
transition: 'slide'
},
Toolbar: {
display: {
left: ['infobar'],
middle: [
'zoomIn',
'zoomOut',
'toggle1to1',
'rotateCCW',
'rotateCW',
'flipX',
'flipY'
],
right: ['slideshow', 'thumbs', 'close']
}
}
}
} else {
options = {
Hash: false,
Carousel: {
transition: 'slide',
Thumbs: {
showOnStart: false
},
Toolbar: {
display: {
left: ['counter'],
middle: [
'zoomIn',
'zoomOut',
'toggle1to1',
'rotateCCW',
'rotateCW',
'flipX',
'flipY',
'reset'
],
right: ['autoplay', 'thumbs', 'close']
}
},
Zoomable: {
Panzoom: {
maxScale: 4
}
}
}
}
}
Fancybox.bind('[data-fancybox]', options)
window.fancyboxRun = true
}
}
},
setLoading: {
add: ele => {
const html = `
<div class="loading-container">
<div class="loading-item">
<div></div><div></div><div></div><div></div><div></div>
</div>
</div>
`
ele.insertAdjacentHTML('afterend', html)
},
remove: ele => {
ele.nextElementSibling.remove()
}
},
updateAnchor: anchor => {
if (anchor !== window.location.hash) {
if (!anchor) anchor = location.pathname
const title = GLOBAL_CONFIG_SITE.title
window.history.replaceState({
url: location.href,
title
}, title, anchor)
}
},
getScrollPercent: (() => {
let docHeight, winHeight, headerHeight, contentMath
return (currentTop, ele) => {
if (!docHeight || ele.clientHeight !== docHeight) {
docHeight = ele.clientHeight
winHeight = window.innerHeight
headerHeight = ele.offsetTop
contentMath = Math.max(docHeight - winHeight, document.documentElement.scrollHeight - winHeight)
}
const scrollPercent = (currentTop - headerHeight) / contentMath
return Math.max(0, Math.min(100, Math.round(scrollPercent * 100)))
}
})(),
addEventListenerPjax: (ele, event, fn, option = false) => {
ele.addEventListener(event, fn, option)
btf.addGlobalFn('pjaxSendOnce', () => {
ele.removeEventListener(event, fn, option)
})
},
removeGlobalFnEvent: (key, parent = window) => {
const globalFn = parent.globalFn || {}
const keyObj = globalFn[key]
if (!keyObj) return
Object.keys(keyObj).forEach(i => keyObj[i]())
delete globalFn[key]
},
switchComments: (el = document, path) => {
const switchBtn = el.querySelector('#switch-btn')
if (!switchBtn) return
let switchDone = false
const postComment = el.querySelector('#post-comment')
const handleSwitchBtn = () => {
postComment.classList.toggle('move')
if (!switchDone && typeof loadOtherComment === 'function') {
switchDone = true
loadOtherComment(el, path)
}
}
btf.addEventListenerPjax(switchBtn, 'click', handleSwitchBtn)
}
}
window.btf = { ...window.btf, ...btfFn }
})()

594
link/index.html Normal file

File diff suppressed because one or more lines are too long

506
page/2/index.html Normal file

File diff suppressed because one or more lines are too long

556
posts/2b2fb1a7/index.html Normal file

File diff suppressed because one or more lines are too long

565
posts/33249733/index.html Normal file

File diff suppressed because one or more lines are too long

575
posts/34725d47/index.html Normal file

File diff suppressed because one or more lines are too long

578
posts/3a26a97a/index.html Normal file

File diff suppressed because one or more lines are too long

566
posts/3e61a389/index.html Normal file

File diff suppressed because one or more lines are too long

665
posts/41b9aff7/index.html Normal file

File diff suppressed because one or more lines are too long

547
posts/5785bd01/index.html Normal file

File diff suppressed because one or more lines are too long

565
posts/59c8572a/index.html Normal file

File diff suppressed because one or more lines are too long

567
posts/5ed2f1e6/index.html Normal file

File diff suppressed because one or more lines are too long

548
posts/66e66374/index.html Normal file

File diff suppressed because one or more lines are too long

571
posts/69b16001/index.html Normal file

File diff suppressed because one or more lines are too long

611
posts/6e5f5039/index.html Normal file

File diff suppressed because one or more lines are too long

567
posts/7e921903/index.html Normal file

File diff suppressed because one or more lines are too long

581
posts/8802e397/index.html Normal file

File diff suppressed because one or more lines are too long

552
posts/8bdb35fb/index.html Normal file

File diff suppressed because one or more lines are too long

548
posts/a31d95d9/index.html Normal file

File diff suppressed because one or more lines are too long

699
posts/a6ab0925/index.html Normal file

File diff suppressed because one or more lines are too long

639
posts/ad244066/index.html Normal file

File diff suppressed because one or more lines are too long

589
posts/b5601a7e/index.html Normal file

File diff suppressed because one or more lines are too long

569
posts/b57500e9/index.html Normal file

File diff suppressed because one or more lines are too long

565
posts/b5866b9e/index.html Normal file

File diff suppressed because one or more lines are too long

685
posts/bb18d851/index.html Normal file

File diff suppressed because one or more lines are too long

560
posts/bc56f789/index.html Normal file

File diff suppressed because one or more lines are too long

558
posts/c6143ad3/index.html Normal file

File diff suppressed because one or more lines are too long

567
posts/ce1ec3fe/index.html Normal file

File diff suppressed because one or more lines are too long

545
posts/d2c8521/index.html Normal file

File diff suppressed because one or more lines are too long

591
posts/da17d00/index.html Normal file

File diff suppressed because one or more lines are too long

585
posts/e9a8e898/index.html Normal file

File diff suppressed because one or more lines are too long

543
posts/f0a1a1f4/index.html Normal file

File diff suppressed because one or more lines are too long

563
posts/f287c563/index.html Normal file

File diff suppressed because one or more lines are too long

475
privacy/index.html Normal file

File diff suppressed because one or more lines are too long

1489
search.xml Normal file

File diff suppressed because one or more lines are too long

477
shuoshuo/index.html Normal file

File diff suppressed because one or more lines are too long

54
sitemap.txt Normal file
View File

@@ -0,0 +1,54 @@
https://blog.biss.click/link/index.html
https://blog.biss.click/privacy/index.html
https://blog.biss.click/tags/index.html
https://blog.biss.click/shuoshuo/index.html
https://blog.biss.click/posts/8802e397/
https://blog.biss.click/posts/ad244066/
https://blog.biss.click/posts/ce1ec3fe/
https://blog.biss.click/posts/b57500e9/
https://blog.biss.click/posts/2b2fb1a7/
https://blog.biss.click/posts/66e66374/
https://blog.biss.click/posts/7e921903/
https://blog.biss.click/posts/f287c563/
https://blog.biss.click/posts/5785bd01/
https://blog.biss.click/posts/a31d95d9/
https://blog.biss.click/posts/34725d47/
https://blog.biss.click/posts/69b16001/
https://blog.biss.click/posts/d2c8521/
https://blog.biss.click/cc/index.html
https://blog.biss.click/cookie/index.html
https://blog.biss.click/about/index.html
https://blog.biss.click/categories/index.html
https://blog.biss.click/posts/41b9aff7/
https://blog.biss.click/posts/5ed2f1e6/
https://blog.biss.click/posts/bb18d851/
https://blog.biss.click/posts/da17d00/
https://blog.biss.click/posts/bc56f789/
https://blog.biss.click/posts/a6ab0925/
https://blog.biss.click/posts/e9a8e898/
https://blog.biss.click/posts/b5601a7e/
https://blog.biss.click/posts/33249733/
https://blog.biss.click/posts/c6143ad3/
https://blog.biss.click/posts/3e61a389/
https://blog.biss.click/posts/8bdb35fb/
https://blog.biss.click/posts/f0a1a1f4/
https://blog.biss.click/posts/b5866b9e/
https://blog.biss.click/posts/59c8572a/
https://blog.biss.click/posts/6e5f5039/
https://blog.biss.click/posts/3a26a97a/
https://blog.biss.click/
https://blog.biss.click/tags/web/
https://blog.biss.click/tags/1panel/
https://blog.biss.click/tags/C/
https://blog.biss.click/tags/english/
https://blog.biss.click/tags/Cudy-TR3000/
https://blog.biss.click/tags/Openwrt/
https://blog.biss.click/tags/AdguardHome/
https://blog.biss.click/tags/Openclash/
https://blog.biss.click/tags/life/
https://blog.biss.click/tags/gitea/
https://blog.biss.click/categories/website/
https://blog.biss.click/categories/technology/
https://blog.biss.click/categories/learning/
https://blog.biss.click/categories/technology/website/
https://blog.biss.click/categories/miscellaneous/

462
sitemap.xml Normal file
View File

@@ -0,0 +1,462 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://blog.biss.click/link/index.html</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/privacy/index.html</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/tags/index.html</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/shuoshuo/index.html</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/posts/8802e397/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/posts/ad244066/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/posts/ce1ec3fe/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/posts/b57500e9/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/posts/2b2fb1a7/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/posts/66e66374/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/posts/7e921903/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/posts/f287c563/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/posts/5785bd01/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/posts/a31d95d9/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/posts/34725d47/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/posts/69b16001/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/posts/d2c8521/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/cc/index.html</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/cookie/index.html</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/about/index.html</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/categories/index.html</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/posts/41b9aff7/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/posts/5ed2f1e6/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/posts/bb18d851/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/posts/da17d00/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/posts/bc56f789/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/posts/a6ab0925/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/posts/e9a8e898/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/posts/b5601a7e/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/posts/33249733/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/posts/c6143ad3/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/posts/3e61a389/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/posts/8bdb35fb/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/posts/f0a1a1f4/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/posts/b5866b9e/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/posts/59c8572a/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/posts/6e5f5039/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/posts/3a26a97a/</loc>
<lastmod>2025-08-26</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://blog.biss.click/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://blog.biss.click/tags/web/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>weekly</changefreq>
<priority>0.2</priority>
</url>
<url>
<loc>https://blog.biss.click/tags/1panel/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>weekly</changefreq>
<priority>0.2</priority>
</url>
<url>
<loc>https://blog.biss.click/tags/C/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>weekly</changefreq>
<priority>0.2</priority>
</url>
<url>
<loc>https://blog.biss.click/tags/english/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>weekly</changefreq>
<priority>0.2</priority>
</url>
<url>
<loc>https://blog.biss.click/tags/Cudy-TR3000/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>weekly</changefreq>
<priority>0.2</priority>
</url>
<url>
<loc>https://blog.biss.click/tags/Openwrt/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>weekly</changefreq>
<priority>0.2</priority>
</url>
<url>
<loc>https://blog.biss.click/tags/AdguardHome/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>weekly</changefreq>
<priority>0.2</priority>
</url>
<url>
<loc>https://blog.biss.click/tags/Openclash/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>weekly</changefreq>
<priority>0.2</priority>
</url>
<url>
<loc>https://blog.biss.click/tags/life/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>weekly</changefreq>
<priority>0.2</priority>
</url>
<url>
<loc>https://blog.biss.click/tags/gitea/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>weekly</changefreq>
<priority>0.2</priority>
</url>
<url>
<loc>https://blog.biss.click/categories/website/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>weekly</changefreq>
<priority>0.2</priority>
</url>
<url>
<loc>https://blog.biss.click/categories/technology/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>weekly</changefreq>
<priority>0.2</priority>
</url>
<url>
<loc>https://blog.biss.click/categories/learning/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>weekly</changefreq>
<priority>0.2</priority>
</url>
<url>
<loc>https://blog.biss.click/categories/technology/website/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>weekly</changefreq>
<priority>0.2</priority>
</url>
<url>
<loc>https://blog.biss.click/categories/miscellaneous/</loc>
<lastmod>2026-02-18</lastmod>
<changefreq>weekly</changefreq>
<priority>0.2</priority>
</url>
</urlset>

450
tags/1panel/index.html Normal file

File diff suppressed because one or more lines are too long

450
tags/AdguardHome/index.html Normal file

File diff suppressed because one or more lines are too long

450
tags/C/index.html Normal file

File diff suppressed because one or more lines are too long

450
tags/Cudy-TR3000/index.html Normal file

File diff suppressed because one or more lines are too long

450
tags/Openclash/index.html Normal file

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More