INIT (I swear I will never re-init this repo again)
This commit is contained in:
commit
3db85006cc
105 changed files with 16927 additions and 0 deletions
46
content/posts/百草园/Hugo博客迁移日志/Hugo博客迁移日志(1).md
Normal file
46
content/posts/百草园/Hugo博客迁移日志/Hugo博客迁移日志(1).md
Normal file
|
@ -0,0 +1,46 @@
|
|||
---
|
||||
title: Hugo博客迁移日志(1)
|
||||
date: 2024-06-29
|
||||
summary: 关于为什么又双叒叕搬家的一些狡辩(划掉)解释
|
||||
description: 本文介绍了作者关于从 NotionNext 再一次迁移到 Hugo 的一些原因的叙述,主要原因是喜欢 Hugo Landscape 主题的外观和构建速度。
|
||||
categories: ["百草园"]
|
||||
series: Hugo博客迁移日志
|
||||
tags:
|
||||
- 博客
|
||||
- Hugo
|
||||
- Efímero
|
||||
- 折腾
|
||||
featuredImage: https://img.viento.cc/cover/migrating-to-hugo-1-cover.webp
|
||||
draft: false
|
||||
slug: migrating-to-hugo-1
|
||||
---
|
||||
|
||||
芜湖,各位老友们好,我是 Chlorine。几天不见,我又回来水文章了。
|
||||
|
||||
本期开一个新坑,主要讲我从 NotionNext 又回到 Hugo 的原因。
|
||||
|
||||
## 前言——从 Fuwari 讲起
|
||||
|
||||
行吧,我也不废话了,就是因为颜值。
|
||||
|
||||
NotionNext 当然好看,尤其是 Hexo 和 Heo 主题。但是,有一天我在 GitHub 上闲逛的时候,偶然发现了这个框架:
|
||||
|
||||
{{< github repo="saicaca/fuwari" >}}
|
||||
|
||||
当时我的反应就是:**惊为天人**。
|
||||
|
||||
这世界上怎么会有这么丝滑的博客框架啊!
|
||||
|
||||
于是我可耻地动心了。不过可惜,Fuwari 基于 Astro,但是我对 Astro 一窍不通。于是我就没继续,转而继续折腾我的 Hugo Blowfish。
|
||||
|
||||
可就在我的 Blowfish 即将大功告成之际,我发现,[恐咖兵糖](https://www.ftls.xyz/)大佬居然已经写了一个类 Fuwari 的 Hugo 主题,名为 Landscape。
|
||||
|
||||
{{< github repo="kkbt0/Hugo-Landscape" >}}
|
||||
|
||||
好的,我是小丑。
|
||||
|
||||
但是 Blowfish 折腾了这么久,扔是不可能的。于是,我开启了漫长的魔改之路。最终兜兜转转,可算是有了个雏形。由于自我感觉添加了不少元素,因此厚颜无耻地将之作为了一个新的主题,取名 Efímero,中文名「浮光」。
|
||||
|
||||
总体而言,浮光不算一个很沉的主题,构建时间大概十几秒。虽说比不过 Virgo 的不超过 10s,但是比 NotionNext 的将近一分半还是强多了。
|
||||
|
||||
本篇比较短,主要是讲一讲前言。明天在高铁上更具体经过(的第一篇)。
|
539
content/posts/百草园/Hugo博客迁移日志/Hugo博客迁移日志(2).md
Normal file
539
content/posts/百草园/Hugo博客迁移日志/Hugo博客迁移日志(2).md
Normal file
|
@ -0,0 +1,539 @@
|
|||
---
|
||||
title: Hugo博客迁移日志(2)
|
||||
date: 2024-06-29T21:30:00+08:00
|
||||
summary: 真·迁移日志(1)
|
||||
description: 本文介绍了作者对于从NotionNext迁移到Hugo的具体过程的简要叙述,包含对项目的总览、友链页面的构建、说说页面的构建等
|
||||
categories: ["百草园"]
|
||||
series: Hugo博客迁移日志
|
||||
tags:
|
||||
- 博客
|
||||
- Hugo
|
||||
- 浮光
|
||||
- 折腾
|
||||
featuredImage: https://img.viento.cc/cover/migrating-to-hugo-2-cover.webp
|
||||
draft: false
|
||||
slug: migrating-to-hugo-2
|
||||
---
|
||||
|
||||
芜湖,各位老友们好啊,我是 Chlorine。接着上一回,咱们开始讲我从 NotionNext 迁移到 Hugo 的旅程。
|
||||
|
||||
## 项目整体结构
|
||||
|
||||
Hugo Landscape 的项目结构没有什么佶屈聱牙的地方,很正常的 Hugo 主题结构……除了 `layouts` 下面有一个单独的 `posts/single.html`?我也不知道这个算不算特别。
|
||||
|
||||
这个主题最经典的地方在于:**它使用的是 UnoCSS**。啥是 UnoCSS?行吧我也不是很懂,似乎就是一个高度原子化、依赖于 HTML 对象的 CSS。反正挺 OOP 的。
|
||||
|
||||
那就先安装个 UnoCSS 吧。直接在主题文件夹下:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
完事。就安装好了一大堆依赖。
|
||||
|
||||
*其实我一开始的时候没有做这个,后续我会讲具体经过。*
|
||||
|
||||
为了后续开发方便,我使用 OOP 课程的知识,写了个简单的 Makefile:
|
||||
|
||||
```makefile
|
||||
# 默认目标
|
||||
all: theme hugo
|
||||
|
||||
# Hugo构建
|
||||
hugo:
|
||||
hugo server -D
|
||||
|
||||
# 主题构建
|
||||
theme:
|
||||
cd themes/efimero && npm run build && cd ../..
|
||||
|
||||
# 清理构建文件
|
||||
clean:
|
||||
rm -rf public
|
||||
rm -rf resources/_gen
|
||||
cd themes/efimero && rm -rf node_modules
|
||||
cd ../..
|
||||
|
||||
# 帮助信息
|
||||
help:
|
||||
@echo "可用的make目标:"
|
||||
@echo " all: 默认目标,构建 Hugo 和主题"
|
||||
@echo " hugo: 构建 Hugo"
|
||||
@echo " theme: 构建主题"
|
||||
```
|
||||
|
||||
这样每次更新功能要预览的时候,直接 `make` 就完事了。
|
||||
|
||||
## 友链页面
|
||||
|
||||
没错,在不细品项目的情况下,直接开冲,主打一个勇。
|
||||
|
||||
~~就算是品了我也品不懂,毕竟我对前端一窍不通。~~
|
||||
|
||||
友链页面是我的必需品。还是老办法,用一个 `JSON` 文件存储信息,放在 `主题/static/jsons/friends.json`。格式大概是:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "名字",
|
||||
"note": "注释",
|
||||
"url": "网址",
|
||||
"md5": "邮箱的 MD5 值,用来从 Cravatar 获取头像",
|
||||
"des": "描述/签名",
|
||||
"ava": "/avatars/头像.webp,在没有 MD5 的情况下用这个"
|
||||
}
|
||||
```
|
||||
|
||||
然后搬出我的祖传 shortcode:
|
||||
|
||||
```html
|
||||
{{ $friends := getJSON "themes/efimero/static/jsons/friends.json" }}
|
||||
<div class="friend-link-container">
|
||||
{{ range $friends.friend}}
|
||||
<div class="friend-link" style="background-image: url('{{ .avatar }}');">
|
||||
<a href="{{ .url }}" target="_blank">
|
||||
<img src="{{ with .ava }}{{ if ne . "" }}{{ . }}{{ else }}https://cravatar.cn/avatar/{{ .md5 }}{{ end }}{{ else }}https://cravatar.cn/avatar/{{ .md5 }}{{ end }}"
|
||||
alt="{{ .name }}" class="friend-link-avatar">
|
||||
</a>
|
||||
<div class="friend-link-content">
|
||||
<h3><a href="{{ .url }}" target="_blank">{{ .note }}({{ .name }})</a></h3>
|
||||
<p>{{ .des }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
```
|
||||
|
||||
既然是换新主题了,肯定得写个好看的样式。`主题/assets/css` 下新建一个 `addon.css`,扔进去:
|
||||
|
||||
```css
|
||||
.friend-link-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.friend-link {
|
||||
width: calc(50% - 10px);
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
height: auto;
|
||||
max-height: 150px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.friend-link {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.friend-link-avatar {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
border-radius: 50%;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.friend-link-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.friend-link-content h3 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.friend-link-content p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
```
|
||||
|
||||
当然,不能忘了包含这个 CSS。Landscape 采用的是 `css.html` 专门包含 CSS,嗯,我很喜欢。
|
||||
|
||||
最终出来的效果还是很好看的,详见我的友链页面。哦,别忘了新建一个 Markdown 文件,包含这个短代码。
|
||||
|
||||
## 说说
|
||||
|
||||
关于怎么搞说说,我属实是折腾了 N 多天,最终还是靠着[恐咖兵糖](https://www.ftls.xyz/)大佬的方案,经过一顿折腾之后得到的还算满意的 solution。
|
||||
|
||||
首先,让我们科普一个概念:**联邦宇宙**。
|
||||
|
||||
搬运 Wiki:
|
||||
|
||||
> 联邦宇宙(英语:Fediverse,简称Fedi)在英文中是“联邦”(Federation)和“宇宙”(Universe)的混成词。联邦宇宙由一系列自由软件组成,有一组互联的服务器(用户自建或第三方托管),一起提供网络发布(如社交媒体、微博、博客或者网站)或者文件托管功能。虽然各个服务器是独立运行的,且各个实例繁多,内容多样, 但服务器之间可以彼此互通。在不同的服务器(实例)上,用户可以创建不同帐号。这些帐号能够跨越实例边界而通信,因为服务器上运行的软件支持一种或多种遵循开放标准的通信协议。 用户通过联邦宇宙中的帐号,可以发布文本或者其他媒体文件,也可以关注其他用户。在某些情况下,用户可以公布或分享数据(如音频、视频、文本文件等),使其对所有或部分人开放并允许他们共同编辑内容(例如日历和黄页)。
|
||||
>
|
||||
> 联邦宇宙的目的是建立在网络社交巨头公司之外, 提供另一种交流方式。与在单一服务器上运行的传统社交网络相比,联邦宇宙的运行方式更开放。 其服务器的分散性,使联邦宇宙更安全可靠。
|
||||
|
||||
简单来说,就是一系列去中心化(准确来说应该算是联邦化或者多中心化)的自由软件组成的庞大社交网络。我对去中心化网络非常感兴趣,后续如果系统学习,会把心得分享出来(`画大饼.webp`)。
|
||||
|
||||
联邦宇宙主要由四大通讯协议支持:
|
||||
|
||||
- ActivityPub
|
||||
- Diaspora Network
|
||||
- OStatus
|
||||
- Zot & Zot/6
|
||||
|
||||
我们要用的是第一个。ActivityPub 协议的代表软件是 [Mastodon](https://mastodon.social/)(中文名:长毛象/乳齿象),简单来说就是联邦宇宙的 Twitter(𝕏)。Mastodon 虽然成熟,但是比较笨重,不利于自托管(虽然说咱们也不用自托管就是了),而且国内可用的实例比较少。所以,我们选择 ActivityPub 协议的另外一个实践者—— [GoToSocial](https://gotosocial.org/)。
|
||||
|
||||
GoToSocial 不多介绍。直接上解决方案:
|
||||
|
||||
1. 注册一个国内的 GTS 实例,例如我用的 [https://scg.owu.one](https://scg.owu.one)。
|
||||
2. 获取鉴权 Token,直接在[这里](https://takahashim.github.io/mastodon-access-token/)操作即可。
|
||||
|
||||
好的,下面开始爆改。具体的操作步骤等我专门写一个说明文档(或许是主题的说明文档?)。
|
||||
|
||||
上一个 shortcode:
|
||||
|
||||
```html
|
||||
<style>
|
||||
.toots-container {
|
||||
margin: 0 auto;
|
||||
max-height: fit-content;
|
||||
}
|
||||
|
||||
.toot {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.toot .avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.toot-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.toot-stats {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
/* 将元素靠右对齐 */
|
||||
}
|
||||
|
||||
.toot-stats i {
|
||||
margin-right: 3rem;
|
||||
}
|
||||
|
||||
.basic-field-status {
|
||||
border: 1px solid #2d97bd86;
|
||||
border-radius: 30px;
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.basic-avatar img {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
object-fit: cover;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
left: calc(50% - 50px);
|
||||
}
|
||||
|
||||
.basic-avatar img {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.basic-avatar img:hover {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.basic-avatar {
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
/* Media Query for Mobile Devices */
|
||||
@media only screen and (max-width: 600px) {
|
||||
.header {
|
||||
height: 150px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.toots-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.toot .avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.basic-info {
|
||||
margin-left: 2px;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 430px) {
|
||||
|
||||
.basic-avatar::before,
|
||||
.basic-avatar::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.basic-text {
|
||||
margin-top: 100px;
|
||||
margin-left: -25px;
|
||||
}
|
||||
|
||||
.basic-avatar img {
|
||||
margin-right: 10px;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.mdi--reply {
|
||||
display: inline-block;
|
||||
width: 1.3em;
|
||||
height: 1.3em;
|
||||
--svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M10 9V5l-7 7l7 7v-4.1c5 0 8.5 1.6 11 5.1c-1-5-4-10-11-11'/%3E%3C/svg%3E");
|
||||
background-color: currentColor;
|
||||
-webkit-mask-image: var(--svg);
|
||||
mask-image: var(--svg);
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-size: 100% 100%;
|
||||
mask-size: 100% 100%;
|
||||
}
|
||||
|
||||
.mdi--star {
|
||||
display: inline-block;
|
||||
width: 1.3em;
|
||||
height: 1.3em;
|
||||
--svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.62L12 2L9.19 8.62L2 9.24l5.45 4.73L5.82 21z'/%3E%3C/svg%3E");
|
||||
background-color: currentColor;
|
||||
-webkit-mask-image: var(--svg);
|
||||
mask-image: var(--svg);
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-size: 100% 100%;
|
||||
mask-size: 100% 100%;
|
||||
}
|
||||
|
||||
.mdi--twitter-retweet {
|
||||
display: inline-block;
|
||||
width: 1.3em;
|
||||
height: 1.3em;
|
||||
--svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M6 5.75L10.25 10H7v6h6.5l2 2H7a2 2 0 0 1-2-2v-6H1.75zm12 12.5L13.75 14H17V8h-6.5l-2-2H17a2 2 0 0 1 2 2v6h3.25z'/%3E%3C/svg%3E");
|
||||
background-color: currentColor;
|
||||
-webkit-mask-image: var(--svg);
|
||||
mask-image: var(--svg);
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-size: 100% 100%;
|
||||
mask-size: 100% 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
<br>
|
||||
<div id="toots-content" class="toots-container">
|
||||
<div class="toot" id="toots">
|
||||
</div>
|
||||
<i id="toots-loading" class="fa fa-spinner fa-pulse fa-3x fa-fw" style="display: none;place-items: center;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="5em" height="5em" viewBox="0 0 256 256">
|
||||
<path fill="currentColor"
|
||||
d="M128 24a104 104 0 1 0 104 104A104.11 104.11 0 0 0 128 24m39.11 25.19C170.24 83.71 155 99.44 135 113.61c-2.25-24.48-8.44-49.8-38.37-67.82a87.89 87.89 0 0 1 70.5 3.4ZM40.18 133.54c28.34-20 49.57-14.68 71.87-4.39c-20.05 14.19-38.86 32.21-39.53 67.11a87.92 87.92 0 0 1-32.34-62.72m136.5 67.73c-31.45-14.55-37.47-35.58-39.71-60c12.72 5.86 26.31 10.75 41.3 10.75c11.33 0 23.46-2.8 36.63-10.08a88.2 88.2 0 0 1-38.22 59.33" />
|
||||
</svg>
|
||||
</i>
|
||||
<button id="toots-moreButton" onclick="tootsShowMore()"><a>更多</a></button>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script type="text/javascript" src="/js/time-fmt.min.js"></script>
|
||||
<script>
|
||||
let maxId = null; // 初始值为 null,表示第一页
|
||||
let isFirst = true; // 首次加载
|
||||
const tootsDiv = document.getElementById('toots');
|
||||
const tootsMoreButton = document.getElementById('toots-moreButton');
|
||||
const tootsLoading = document.getElementById('toots-loading');
|
||||
const urlObject = new URL(window.location.href);
|
||||
const idValue = urlObject.searchParams.get("id");
|
||||
|
||||
// 获取 Mastodon 用户公开Toots 限制条数 默认5 排除回复 toot
|
||||
async function getPublicToots() {
|
||||
let limit = "{{ .Get 2 | default 5 }}";
|
||||
|
||||
if (idValue != null && isFirst) {
|
||||
isFirst = false;
|
||||
const response = await fetch("{{ .Site.Params.Whisper.instance }}/api/v1/statuses/" + idValue, {
|
||||
headers: {
|
||||
'Authorization': "Bearer {{ .Site.Params.bot_token }}"
|
||||
}
|
||||
})
|
||||
const toot = await response.json();
|
||||
return [toot];
|
||||
}
|
||||
|
||||
const queryParams = maxId ? (`?limit=${limit}&max_id=${maxId}`) : "?limit=" + limit;
|
||||
const response = await fetch("{{ .Site.Params.Whisper.instance }}/api/v1/accounts/{{ .Site.Params.Whisper.user_id }}/statuses" + queryParams + "&exclude_replies=true", {
|
||||
headers: {
|
||||
'Authorization': "Bearer {{ .Site.Params.Whisper.bot_token }}"
|
||||
}
|
||||
})
|
||||
const toots = await response.json();
|
||||
return toots;
|
||||
}
|
||||
|
||||
// 解析ULID
|
||||
function parseULID(ulid) {
|
||||
const base32Chars = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
||||
const timestamp = parseInt(ulid.slice(0, 10).split('').map(char => base32Chars.indexOf(char)).map(index => index.toString(2).padStart(5, '0')).join(''), 2);
|
||||
const randomPart = ulid.slice(10);
|
||||
|
||||
return {
|
||||
timestamp: new Date(timestamp),
|
||||
randomPart: randomPart
|
||||
};
|
||||
}
|
||||
|
||||
// 将Toots显示在页面上
|
||||
async function displayToots() {
|
||||
try {
|
||||
tootsLoading.style.display = "grid";
|
||||
tootsMoreButton.style.display = 'none';
|
||||
const toots = await getPublicToots();
|
||||
if (toots && toots.length > 0) {
|
||||
displayBioProfile(toots[0]);
|
||||
toots.forEach(toot => {
|
||||
// console.log(parseULID(toot.id)); // 解析 ULID
|
||||
const tootDiv = document.createElement("div");
|
||||
|
||||
tootDiv.classList.add("toot");
|
||||
|
||||
const tootInfoDiv = document.createElement("div");
|
||||
tootInfoDiv.classList.add("toot-info");
|
||||
|
||||
const tootAvatar = document.createElement("div");
|
||||
tootAvatar.classList.add("toot-avatar");
|
||||
|
||||
const profileImage = document.createElement("img");
|
||||
profileImage.src = "{{ .Site.Params.Author.avatar }}";
|
||||
profileImage.classList.add("avatar");
|
||||
profileImage.alt = toot.account.display_name;
|
||||
|
||||
const tootProfileDiv = document.createElement("div");
|
||||
tootProfileDiv.innerHTML = `<strong>${toot.account.display_name}</strong> <a href="${toot.url}" target="_blank">@${toot.account.acct}</a><br><small>${formatTime(toot.created_at)}</small>`;
|
||||
|
||||
tootAvatar.appendChild(profileImage);
|
||||
tootInfoDiv.appendChild(profileImage);
|
||||
tootInfoDiv.appendChild(tootProfileDiv);
|
||||
|
||||
const contentDiv = document.createElement("div");
|
||||
contentDiv.classList.add("toot-content");
|
||||
contentDiv.innerHTML = toot.content.replace(/<img/g, '<img loading="lazy" class="toot-img"');
|
||||
// contentDiv.innerHTML = toot.content;
|
||||
|
||||
// media loading="lazy"
|
||||
for (let i = 0; i < toot.media_attachments.length; i++) {
|
||||
const media = toot.media_attachments[i];
|
||||
contentDiv.innerHTML += `<img loading="lazy" src="${media.url}">`;
|
||||
}
|
||||
|
||||
const tootStats = document.createElement("a");
|
||||
tootStats.href = toot.url;
|
||||
tootStats.target = "_blank";
|
||||
{
|
||||
{/* tootStats.className = "toot-stats";
|
||||
if (toot.replies_count + toot.favourites_count + toot.reblogs_count != 0) {
|
||||
tootStats.innerHTML += `<span class="mdi--reply"></span> ${toot.replies_count} `;
|
||||
tootStats.innerHTML += `<span class="mdi--star"></span> ${toot.favourites_count} `;
|
||||
tootStats.innerHTML += `<span class="mdi--twitter-retweet"></span> ${toot.reblogs_count}`;
|
||||
} */}
|
||||
}
|
||||
|
||||
const statsDiv = document.createElement('div');
|
||||
statsDiv.classList.add('toot-stats');
|
||||
statsDiv.innerHTML = `
|
||||
<span class="mdi--reply"></span> ${toot.replies_count}
|
||||
<span class="mdi--star"></span> ${toot.favourites_count}
|
||||
<span class="mdi--twitter-retweet"></span> ${toot.reblogs_count}
|
||||
`;
|
||||
|
||||
const hr = document.createElement("hr");
|
||||
hr.style = "margin: 0.4rem 0;"
|
||||
|
||||
// 评论锚点
|
||||
// const commentAnchor = document.createElement("div");
|
||||
|
||||
tootDiv.appendChild(tootInfoDiv);
|
||||
tootDiv.appendChild(contentDiv);
|
||||
tootDiv.appendChild(tootStats);
|
||||
tootDiv.appendChild(statsDiv);
|
||||
tootDiv.appendChild(hr);
|
||||
|
||||
tootsDiv.appendChild(tootDiv);
|
||||
maxId = toot.id; // 更新最大 ID
|
||||
// 如果 只有一个 自动打开评论区
|
||||
if (toots.length == 1) {
|
||||
initArtalk(commentAnchor, toot);
|
||||
}
|
||||
});
|
||||
tootsMoreButton.style.display = 'block';
|
||||
} else {
|
||||
tootsMoreButton.style.display = 'none';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取 Toots 时出错:', error);
|
||||
tootsDiv.innerHTML += error.message;
|
||||
}
|
||||
tootsLoading.style.display = "none";
|
||||
}
|
||||
|
||||
function tootsShowMore() {
|
||||
displayToots();
|
||||
}
|
||||
|
||||
function displayBioProfile(statuse) {
|
||||
}
|
||||
|
||||
displayToots();
|
||||
// 页面加载时调用显示Toots函数
|
||||
// window.onload = displayToots;
|
||||
window.ViewImage && ViewImage.init('.toot-img');
|
||||
</script>
|
||||
```
|
||||
|
||||
在站点配置 TOML 中加入相关数据:
|
||||
|
||||
```toml
|
||||
# 联邦宇宙的说说参数
|
||||
[params.whisper]
|
||||
instance = "你的实例名称"
|
||||
user_id = "你的 ID"
|
||||
bot_token = "你的 Token"
|
||||
```
|
||||
|
||||
开一个 Markdown,加入短代码,完事。
|
||||
|
||||
别看我现在说得轻巧,当初折腾的时候不知道费了多少劲。
|
||||
|
||||
今天先说这两个,剩下的明天再说。
|
71
content/posts/百草园/Hugo博客迁移日志/Hugo博客迁移日志(3).md
Normal file
71
content/posts/百草园/Hugo博客迁移日志/Hugo博客迁移日志(3).md
Normal file
|
@ -0,0 +1,71 @@
|
|||
---
|
||||
title: Hugo博客迁移日志(3)
|
||||
date: 2024-06-30
|
||||
summary: 真·迁移日志(2)
|
||||
description: 作者对于从NotionNext迁移到Hugo的具体过程的简要叙述,包含GitHub、GitLab、Codeberg短代码的构建与Callout块的处理。
|
||||
categories: ["百草园"]
|
||||
series: Hugo博客迁移日志
|
||||
tags:
|
||||
- 博客
|
||||
- Hugo
|
||||
- 浮光
|
||||
- 折腾
|
||||
slug: migrating-to-hugo-3
|
||||
featuredImage: https://img.viento.cc/cover/migrating-to-hugo-3-cover.webp
|
||||
draft: false
|
||||
---
|
||||
|
||||
芜湖,各位老友们好啊,我是 Chlorine。接着上一回,继续为您说。
|
||||
|
||||
## GitHub/GitLab/Codeberg 短代码
|
||||
|
||||
Blowfish 有一个很好的短代码,就是 GitHub/GitLab/Codeberg 项目卡片。本着拿来主义的原则,直接拿过来。
|
||||
|
||||
Blowfish 的短代码需要使用 icon 短代码和 SVG 图标,但是我表示根本不需要。用 carbon icons 就可以。
|
||||
|
||||
大手一挥改完,然后加载本地服务器……
|
||||
|
||||
不是,我图标呢?!
|
||||
|
||||
连夜查了一下,中间也不知道怎么改的,反正折腾了半天,可算是搞出来了。
|
||||
|
||||
然后……怎么 403 了?
|
||||
|
||||
再去查。哦,这个短代码用的 GitHub API 获取信息,而匿名的 API 有调用限制。所以如果有高频的调用需求,可能需要去 GitHub 申请个 API,反正不花钱。
|
||||
|
||||
最终效果如下:
|
||||
|
||||
{{< github repo="kkbt0/Hugo-Landscape" >}}
|
||||
|
||||
GitLab 和 Codeberg 也差不多。
|
||||
|
||||
## Callout 块
|
||||
|
||||
Callout 块是一个很难办的事情。
|
||||
|
||||
其实如果不考虑使用的便利性,最好的办法就是使用 Shortcode。甚至 GitHub 上已经有现成的 notice 短代码了。
|
||||
|
||||
但是,作为强迫症的我,希望能够直接使用 GitHub 风格(Obsidian 风格)的语法进行书写,也就是:
|
||||
|
||||
```md
|
||||
> [!Callout-type]
|
||||
> Content
|
||||
```
|
||||
|
||||
去给 Hugo 官方提了 issue,但是官方表示,他们感觉这并不是一个好的实现,推荐我使用这样的语法,然后写 Markdown render hook:
|
||||
|
||||
```txt
|
||||
callout {level="warning" }
|
||||
This is a warning.
|
||||
```
|
||||
|
||||
……行吧。
|
||||
|
||||
那我还不如写 shortcode 呢!
|
||||
|
||||
于是我决定,使用 shortcode 加上 GitHub publisher 的正则替换。很可惜,由于我不会写正则,还是得用 shortcode 的原始语法,就很难评。
|
||||
|
||||
下面是一个例子:
|
||||
|
||||
> [!TIP]
|
||||
> 这是一段 Callout。
|
493
content/posts/百草园/Hugo博客迁移日志/Hugo博客迁移日志(4).md
Normal file
493
content/posts/百草园/Hugo博客迁移日志/Hugo博客迁移日志(4).md
Normal file
|
@ -0,0 +1,493 @@
|
|||
---
|
||||
title: Hugo博客迁移日志(4)
|
||||
date: 2024-07-01
|
||||
summary: 真·迁移日志(3)
|
||||
description: 本文详细介绍了Hugo主题定制过程中的四个主要组件:公告、目录、简历和糖果雨特效。通过具体的代码示例和配置方法,文章指导读者如何实现这些组件的添加和功能扩展。包括了HTML模板的编辑、TOML配置文件的编写、CSS样式的调整以及JavaScript特效的实现。适合希望深入了解Hugo主题开发和自定义功能的读者。
|
||||
categories: ["百草园"]
|
||||
series: Hugo博客迁移日志
|
||||
tags:
|
||||
- Hugo
|
||||
- 博客
|
||||
- Efímero
|
||||
- 折腾
|
||||
slug: migrating-to-hugo-4
|
||||
featuredImage: https://img.viento.cc/cover/migrating-to-hugo-4-cover.webp
|
||||
draft: false
|
||||
---
|
||||
|
||||
芜湖,各位老友们好啊,我是 Chlorine。接着上一回,继续为您说。
|
||||
|
||||
## 公告组件
|
||||
|
||||
下面的目标是为主题添加侧边栏组件。经过观察,这部分代码位于 `主题/layouts/partials/sidebar` 和 `主题/layouts/partials/sidebar.html` 中。不过使用循环来简化代码似乎有那么亿点点麻烦,所以还是先不管可扩展性,直接硬上吧。
|
||||
|
||||
在 `sidebar` 文件夹下添加一个 `announcement.html`:
|
||||
|
||||
```html
|
||||
{{ $announcement := .Site.GetPage "announcement" }}
|
||||
{{ with $announcement }}
|
||||
<div class="markdown-body text-neutral-900 dark:text-neutral-100 text-center">
|
||||
{{ .Content | safeHTML }}
|
||||
</div>
|
||||
{{ end }}
|
||||
```
|
||||
|
||||
在 `sidebar.html` 下添加:
|
||||
|
||||
```html
|
||||
{{ if .Site.Params.Basic.announcement }}
|
||||
<widget-layout id="announcement-widget" class="pb-4 card-base">
|
||||
<div
|
||||
class="font-bold transition text-lg text-neutral-900 dark:text-neutral-100 relative ml-8 mt-4 mb-2 before:content-[''] before:w-1 before:h-4 before:rounded-md before:bg-[var(--primary)] before:absolute before:left-[-16px] before:top-[5.5px]">
|
||||
公告
|
||||
</div>
|
||||
<div id="announcement-content" class="collapse-wrapper px-4 overflow-hidden">
|
||||
{{ partial "sidebar/announcement.html" . }}
|
||||
</div>
|
||||
</widget-layout>
|
||||
{{ end }}
|
||||
```
|
||||
|
||||
这样直接编辑 `announcement.md` 就可以编辑公告了。当然,还需要配置 `.Site.Params.Basic.announcement` 参数。
|
||||
|
||||
## 目录组件
|
||||
|
||||
目录和公告大差不差:
|
||||
|
||||
```html
|
||||
<div id="toc-container" class="
|
||||
toc text-neutral-900 dark:text-neutral-100
|
||||
p-4 rounded-lg shadow-lg overflow-auto max-h-96
|
||||
transition-all duration-300 ease-in-out hover:shadow-2xl
|
||||
">
|
||||
<div id="toc-content" class="transition-all duration-500 ease-in-out">
|
||||
{{ .Page.TableOfContents }}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
就是这个样式不大好看,凑合用吧。
|
||||
|
||||
## 简历组件
|
||||
|
||||
这个相对麻烦。我不想在 HTML 里硬编码,于是想了半天后,决定使用 TOML 配置,大概就是:
|
||||
|
||||
```toml
|
||||
[[params.social]]
|
||||
name = "Home"
|
||||
url = ""
|
||||
icon = "i-carbon-home"
|
||||
enable = true
|
||||
[[params.social]]
|
||||
name = "Email"
|
||||
url = "mailto:your@email.com"
|
||||
icon = "i-carbon-email"
|
||||
enable = true
|
||||
```
|
||||
|
||||
然后改一下 profile 的逻辑:
|
||||
|
||||
```html
|
||||
<div class="card-base">
|
||||
<a aria-label="Go to About Page" href="{{ relURL "about/" }}" class="group block relative mx-auto mt-4 lg:mx-3 lg:mt-3 mb-3
|
||||
max-w-[240px] lg:max-w-none overflow-hidden rounded-xl active:scale-95">
|
||||
<div class="absolute transition pointer-events-none group-hover:bg-black/30 group-active:bg-black/50
|
||||
w-full h-full z-50 flex items-center justify-center">
|
||||
<div
|
||||
class="transition opacity-0 group-hover:opacity-100 text-white text-5xl i-mdi-card-account-details-outline">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx-auto lg:w-full h-full lg:mt-0 overflow-hidden relative">
|
||||
<div class="transition absolute inset-0 dark:bg-black/10 bg-opacity-50 pointer-events-none"></div>
|
||||
<img src="{{ relURL "/img/avatar.webp" }}" alt="Profile Image of the Author"
|
||||
class="w-full h-full object-center object-cover mx-auto lg:w-full h-full lg:mt-0" />
|
||||
</div>
|
||||
</a>
|
||||
<div class="font-bold text-xl text-center mb-1 dark:text-neutral-50 transition">{{ .Site.Params.Author.name }}</div>
|
||||
<div class="h-1 w-5 bg-[var(--primary)] mx-auto rounded-full mb-2 transition"></div>
|
||||
<div class="text-center text-neutral-400 mb-2.5 transition">{{ .Site.Params.Author.description }}</div>
|
||||
{{/* <div class="flex gap-2 mx-2 justify-center mb-4">
|
||||
<a aria-label="Home" href="" target="_blank" class="btn-regular rounded-lg h-10 w-10 active:scale-90">
|
||||
<div class="i-carbon-home text-xl"></div>
|
||||
</a>
|
||||
|
||||
</div> */}}
|
||||
<div class="icons-container">
|
||||
{{ range .Site.Params.social }}
|
||||
{{ if .enable }}
|
||||
<a aria-label="{{ .name }}" href="{{ .url }}" target="_blank"
|
||||
class="btn-regular rounded-lg h-10 w-10 active:scale-90">
|
||||
<div class="{{ .icon }} text-xl"></div>
|
||||
</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
```
|
||||
|
||||
再写点美化的 CSS:
|
||||
|
||||
```css
|
||||
.icon svg {
|
||||
height: 1em;
|
||||
width: 1em
|
||||
}
|
||||
|
||||
.icons-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icons-container a {
|
||||
margin: 5px;
|
||||
/* 调整图标之间的间距 */
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 移动端样式 */
|
||||
@media (max-width: 600px) {
|
||||
.icons-container a {
|
||||
margin: 4px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.card-base {
|
||||
flex-direction: column;
|
||||
padding: 0 1rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
就可以渲染社交图标了。
|
||||
|
||||
当初整这个的时候费了不少劲,主要的问题居然是我不知道图标不经过重新 UnoCSS 构建是不生效的(因此我安装了 UnoCSS 🤣)。以及,UnoCSS 不能检测没有在 HTML 中直接使用的图标,因此开了个 `utility.html` 来专门引入图标。
|
||||
|
||||
## 糖果雨特效
|
||||
|
||||
这个我在网上找了许多代码,最终在 Claude 等 AI 伙伴的帮助下写了出来。
|
||||
|
||||
新建一个 `candy.js`:
|
||||
|
||||
```js
|
||||
// 糖果雨效果,一定要在 footer 中引入,否则由于页面未加载完成,无法获取到 body 元素
|
||||
|
||||
class Circle {
|
||||
constructor ({ origin, speed, color, angle, context }) {
|
||||
this.origin = origin
|
||||
this.position = { ...this.origin }
|
||||
this.color = color
|
||||
this.speed = speed
|
||||
this.angle = angle
|
||||
this.context = context
|
||||
this.renderCount = 0
|
||||
}
|
||||
|
||||
draw() {
|
||||
this.context.fillStyle = this.color
|
||||
this.context.beginPath()
|
||||
this.context.arc(this.position.x, this.position.y, 2, 0, Math.PI * 2)
|
||||
this.context.fill()
|
||||
}
|
||||
|
||||
move() {
|
||||
this.position.x = (Math.sin(this.angle) * this.speed) + this.position.x
|
||||
this.position.y = (Math.cos(this.angle) * this.speed) + this.position.y + (this.renderCount * 0.3)
|
||||
this.renderCount++
|
||||
}
|
||||
}
|
||||
|
||||
class Boom {
|
||||
constructor ({ origin, context, circleCount = 10, area }) {
|
||||
this.origin = origin
|
||||
this.context = context
|
||||
this.circleCount = circleCount
|
||||
this.area = area
|
||||
this.stop = false
|
||||
this.circles = []
|
||||
}
|
||||
|
||||
randomArray(range) {
|
||||
const length = range.length
|
||||
const randomIndex = Math.floor(length * Math.random())
|
||||
return range[randomIndex]
|
||||
}
|
||||
|
||||
randomColor() {
|
||||
const range = ['8', '9', 'A', 'B', 'C', 'D', 'E', 'F']
|
||||
return '#' + this.randomArray(range) + this.randomArray(range) + this.randomArray(range) + this.randomArray(range) + this.randomArray(range) + this.randomArray(range)
|
||||
}
|
||||
|
||||
randomRange(start, end) {
|
||||
return (end - start) * Math.random() + start
|
||||
}
|
||||
|
||||
init() {
|
||||
for (let i = 0; i < this.circleCount; i++) {
|
||||
const circle = new Circle({
|
||||
context: this.context,
|
||||
origin: this.origin,
|
||||
color: this.randomColor(),
|
||||
angle: this.randomRange(Math.PI - 1, Math.PI + 1),
|
||||
speed: this.randomRange(1, 6)
|
||||
})
|
||||
this.circles.push(circle)
|
||||
}
|
||||
}
|
||||
|
||||
move() {
|
||||
this.circles.forEach((circle, index) => {
|
||||
if (circle.position.x > this.area.width || circle.position.y > this.area.height) {
|
||||
return this.circles.splice(index, 1)
|
||||
}
|
||||
circle.move()
|
||||
})
|
||||
if (this.circles.length == 0) {
|
||||
this.stop = true
|
||||
}
|
||||
}
|
||||
|
||||
draw() {
|
||||
this.circles.forEach(circle => circle.draw())
|
||||
}
|
||||
}
|
||||
|
||||
class CursorSpecialEffects {
|
||||
constructor () {
|
||||
this.computerCanvas = document.createElement('canvas')
|
||||
this.renderCanvas = document.createElement('canvas')
|
||||
|
||||
this.computerContext = this.computerCanvas.getContext('2d')
|
||||
this.renderContext = this.renderCanvas.getContext('2d')
|
||||
|
||||
this.globalWidth = window.innerWidth
|
||||
this.globalHeight = window.innerHeight
|
||||
|
||||
this.booms = []
|
||||
this.running = false
|
||||
}
|
||||
|
||||
handleMouseDown(e) {
|
||||
const boom = new Boom({
|
||||
origin: { x: e.clientX, y: e.clientY },
|
||||
context: this.computerContext,
|
||||
area: {
|
||||
width: this.globalWidth,
|
||||
height: this.globalHeight
|
||||
}
|
||||
})
|
||||
boom.init()
|
||||
this.booms.push(boom)
|
||||
this.running || this.run()
|
||||
}
|
||||
|
||||
handlePageHide() {
|
||||
this.booms = []
|
||||
this.running = false
|
||||
}
|
||||
|
||||
init() {
|
||||
const style = this.renderCanvas.style
|
||||
style.position = 'fixed'
|
||||
style.top = style.left = 0
|
||||
style.zIndex = '999999999999999999999999999999999999999999'
|
||||
style.pointerEvents = 'none'
|
||||
|
||||
style.width = this.renderCanvas.width = this.computerCanvas.width = this.globalWidth
|
||||
style.height = this.renderCanvas.height = this.computerCanvas.height = this.globalHeight
|
||||
|
||||
document.body.append(this.renderCanvas)
|
||||
|
||||
window.addEventListener('mousedown', this.handleMouseDown.bind(this))
|
||||
window.addEventListener('pagehide', this.handlePageHide.bind(this))
|
||||
}
|
||||
|
||||
run() {
|
||||
this.running = true
|
||||
if (this.booms.length == 0) {
|
||||
return this.running = false
|
||||
}
|
||||
|
||||
requestAnimationFrame(this.run.bind(this))
|
||||
|
||||
this.computerContext.clearRect(0, 0, this.globalWidth, this.globalHeight)
|
||||
this.renderContext.clearRect(0, 0, this.globalWidth, this.globalHeight)
|
||||
|
||||
this.booms.forEach((boom, index) => {
|
||||
if (boom.stop) {
|
||||
return this.booms.splice(index, 1)
|
||||
}
|
||||
boom.move()
|
||||
boom.draw()
|
||||
})
|
||||
this.renderContext.drawImage(this.computerCanvas, 0, 0, this.globalWidth, this.globalHeight)
|
||||
}
|
||||
}
|
||||
|
||||
const cursorSpecialEffects = new CursorSpecialEffects()
|
||||
cursorSpecialEffects.init()
|
||||
```
|
||||
|
||||
再添加 CSS:
|
||||
|
||||
```css
|
||||
.candy {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: #f00;
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
}
|
||||
```
|
||||
|
||||
引入即可。以及,点名批评 Hugo 奇葩的构建机制,JavaScript 就摆在那,愣是不加到 `public` 里,我也是没招了。
|
||||
|
||||
## 代码一键复制
|
||||
|
||||
这个属实是给我整得心力交瘁。好在最后在强大的 Claude 3.5 的帮助下写出来了。
|
||||
|
||||
开一个 `clickcopy.js`:
|
||||
|
||||
```js
|
||||
(function () {
|
||||
'use strict';
|
||||
if (!navigator.clipboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
function createSVGIcon(iconName) {
|
||||
const svgNS = "http://www.w3.org/2000/svg";
|
||||
const svg = document.createElementNS(svgNS, "svg");
|
||||
svg.setAttribute("viewBox", "0 0 32 32");
|
||||
|
||||
const path = document.createElementNS(svgNS, "path");
|
||||
if (iconName === "copy") {
|
||||
path.setAttribute("d", "M28,10V28H10V10H28m0-2H10a2,2,0,0,0-2,2V28a2,2,0,0,0,2,2H28a2,2,0,0,0,2-2V10a2,2,0,0,0-2-2Z");
|
||||
const path2 = document.createElementNS(svgNS, "path");
|
||||
path2.setAttribute("d", "M4,18H2V4A2,2,0,0,1,4,2H18V4H4Z");
|
||||
svg.appendChild(path2);
|
||||
} else if (iconName === "checkmark") {
|
||||
path.setAttribute("d", "M13 24L4 15 5.414 13.586 13 21.171 26.586 7.586 28 9 13 24z");
|
||||
}
|
||||
svg.appendChild(path);
|
||||
return svg;
|
||||
}
|
||||
|
||||
function flashCopyMessage(el, msg, iconName) {
|
||||
var iconContainer = el.querySelector('.copy-icon');
|
||||
var msgContainer = el.querySelector('.copy-msg');
|
||||
|
||||
// 更新图标
|
||||
iconContainer.innerHTML = '';
|
||||
iconContainer.appendChild(createSVGIcon(iconName));
|
||||
|
||||
// 更新消息
|
||||
msgContainer.textContent = msg;
|
||||
|
||||
setTimeout(function () {
|
||||
// 2秒后清除消息文本和图标变化
|
||||
msgContainer.textContent = '';
|
||||
iconContainer.innerHTML = '';
|
||||
iconContainer.appendChild(createSVGIcon('copy'));
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function addCopyButton(containerEl) {
|
||||
if (containerEl.querySelector('.highlight-copy-btn')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var copyBtn = document.createElement("button");
|
||||
copyBtn.className = "highlight-copy-btn";
|
||||
copyBtn.setAttribute("aria-label", "Copy to clipboard");
|
||||
copyBtn.innerHTML = '<span class="copy-icon"></span><span class="copy-msg"></span>';
|
||||
copyBtn.querySelector('.copy-icon').appendChild(createSVGIcon('copy'));
|
||||
copyBtn.style.display = "none";
|
||||
|
||||
var codeEl = containerEl.querySelector('code') || containerEl;
|
||||
copyBtn.addEventListener('click', function () {
|
||||
navigator.clipboard.writeText(codeEl.innerText).then(function () {
|
||||
flashCopyMessage(copyBtn, 'Copied!', 'checkmark');
|
||||
}, function (err) {
|
||||
console.error('Unable to copy: ', err);
|
||||
flashCopyMessage(copyBtn, 'Failed :\'(', 'copy');
|
||||
});
|
||||
});
|
||||
|
||||
containerEl.appendChild(copyBtn);
|
||||
containerEl.style.position = 'relative';
|
||||
|
||||
containerEl.addEventListener('mouseenter', function () {
|
||||
copyBtn.style.display = "block";
|
||||
});
|
||||
containerEl.addEventListener('mouseleave', function () {
|
||||
copyBtn.style.display = "none";
|
||||
});
|
||||
}
|
||||
|
||||
// 添加复制按钮到所有代码块
|
||||
var codeBlocks = document.querySelectorAll('pre');
|
||||
Array.prototype.forEach.call(codeBlocks, addCopyButton);
|
||||
})();
|
||||
```
|
||||
|
||||
写 CSS:
|
||||
|
||||
```css
|
||||
.highlight-copy-btn {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 7px;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
padding: 5px;
|
||||
font-size: 0.8em;
|
||||
line-height: 1;
|
||||
background-color: transparent;
|
||||
/* 移除背景色 */
|
||||
color: inherit;
|
||||
/* 继承父元素的颜色 */
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.highlight-copy-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.copy-icon {
|
||||
display: inline-flex;
|
||||
/* 使用 flex 布局以更好地控制图标 */
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.copy-icon svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: currentColor;
|
||||
/* 使用当前文本颜色填充SVG */
|
||||
}
|
||||
|
||||
.copy-msg {
|
||||
margin-left: 5px;
|
||||
font-size: 12px;
|
||||
}
|
||||
```
|
||||
|
||||
然后引入就完事了。不过这个功能有时候需要刷新才能启动。
|
||||
|
||||
以及复制之后的按钮和文本有点偏移,但是我没力气调了。
|
Loading…
Add table
Add a link
Reference in a new issue