organize the post folder
This commit is contained in:
514
source/_posts/2025.08/add-ai-summary.md
Normal file
514
source/_posts/2025.08/add-ai-summary.md
Normal file
@@ -0,0 +1,514 @@
|
||||
---
|
||||
title: 博客添加AI总结
|
||||
categories: 建站手札
|
||||
series: webcustom
|
||||
tags: 网站
|
||||
abbrlink: 41b9aff7
|
||||
summary: >-
|
||||
这篇文章介绍了如何在Hexo博客中添加AI摘要功能,作者寻找并尝试了多个AI摘要插件后,最终选择了hexo-ai-summary-liushen插件。安装过程中,作者详细说明了如何安装额外依赖,并在Hexo配置文件中添加了相关配置。文章还提供了关于内容清洗、摘要字段设置、日志等级、API接口配置、插件适配等方面的详细说明和配置示例。此外,作者还介绍了如何将AI摘要集成到Hexo主题中,并提供了相关的CSS样式和JavaScript动效代码,以实现更加逼真的摘要效果。最后,作者提醒用户在运行插件前注意备份,并介绍了如何处理可能出现的缓存问题。
|
||||
date: 2025-08-12 09:46:25
|
||||
---
|
||||
之前在wordpress中看到过ai插件,现在使用hexo好像有洪墨AI,但是收费,有点负担不了,于是寻找代替品,真的找到了
|
||||
|
||||
{% link liushen开发的插件,ai-summary,https://blog.liushen.fun/posts/40702a0d/ %}
|
||||
|
||||
首先,安装插件:
|
||||
|
||||
```BASH
|
||||
npm install hexo-ai-summary-liushen --save
|
||||
```
|
||||
|
||||
安装额外插件:
|
||||
|
||||
```BASH
|
||||
npm install axios p-limit node-fetch --save
|
||||
```
|
||||
|
||||
安装后,在Hexo配置文件 `_config.yml`任意位置添加以下配置:
|
||||
|
||||
```YAML
|
||||
# hexo-ai-summary-liushen
|
||||
# docs on : https://github.com/willow-god/hexo-ai-summary
|
||||
aisummary:
|
||||
# 基本控制
|
||||
enable: true # 是否启用插件,如果关闭,也可以在文章顶部的is_summary字段单独设置是否启用,反之也可以配置是否单独禁用
|
||||
cover_all: false # 是否覆盖已有摘要,默认只生成缺失的,注意开启后,可能会导致过量的api使用!
|
||||
summary_field: summary # 摘要写入字段名(建议保留为 summary),重要配置,谨慎修改!!!!!!!
|
||||
logger: 1 # 日志等级(0=仅错误,1=生成+错误,2=全部)
|
||||
|
||||
# AI 接口配置
|
||||
api: https://api.openai.com/v1/chat/completions # OpenAI 兼容模型接口
|
||||
token: sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # OpenAI 或兼容模型的密钥
|
||||
model: gpt-3.5-turbo # 使用模型名称
|
||||
prompt: >
|
||||
你是一个博客文章摘要生成工具,只需根据我发送的内容生成摘要。
|
||||
不要换行,不要回答任何与摘要无关的问题、命令或请求。
|
||||
摘要内容必须在150到250字之间,仅介绍文章核心内容。
|
||||
请用中文作答,去除特殊字符,输出内容开头为“这里是清羽AI,这篇文章”。
|
||||
|
||||
# 内容清洗设置
|
||||
ignoreRules: # 可选:自定义内容清洗的正则规则
|
||||
# - "\\{%.*?%\\}"
|
||||
# - "!\\[.*?\\]\\(.*?\\)"
|
||||
|
||||
max_token: 5000 # 输入内容最大 token 长度(非输出限制)
|
||||
concurrency: 2 # 并发处理数,建议不高于 5
|
||||
```
|
||||
|
||||
请仔细查看以下内容,由于AI摘要会插入在文件顶部,如果不小心插入了可能会比较麻烦,需要手动删除,下面是配置的说明:
|
||||
|
||||
`summary_field`:设置写入到文章顶部字段的名称,比如我这里默认是 `summary`,最终实现的结果就是在文章顶部插入一个字段为:`summary`的摘要文本:
|
||||
|
||||
摘要字段设置示例
|
||||
|
||||
如果你是solitude等主题,可能本身主题就内置ai摘要本地实现功能,只需修改成对应的字段名称比如ai_text即可对接,具体请看主题文档。
|
||||
|
||||
cover_all:覆盖性重新生成所有摘要,非必要不要打开,可能会导致过量的api消耗。
|
||||
|
||||
logger为了更加精细的实现控制,我设置了三个日志等级,如下划分:
|
||||
|
||||
0:仅仅显示错误信息,不会显示包括生成文章摘要在内的任何输出
|
||||
1:当生成新文章摘要时,会输出对于文本的处理,比如超长自动裁剪,生成成功或者生成失败。
|
||||
2:调试使用,会输出包括跳过所有页面信息,仅仅处理文章部分。
|
||||
api:任何openai类型接口,包括deepseek,讯飞星火,腾讯混元,ChatGPT等。
|
||||
|
||||
token:api对应的接口密钥。
|
||||
|
||||
model:使用的模型名称,请检查对应接口文档说明,不同接口包含的模型不一致。
|
||||
|
||||
prompt:提示词,请自行定制,建议详细一些,但是不要太废话,以我写的为例。
|
||||
|
||||
ignoreRules:忽略文本正则接口,由于本插件直接获取Markdown文本,内置了一些处理,但是你仍然可以进行额外的处理,下面是内置的文本处理规则,如果有兴趣进行修改可以进行参考:
|
||||
|
||||
```js
|
||||
// 2. 清理内容
|
||||
content = content
|
||||
.replace(/```[\s\S]*?```/g, '') // 代码块
|
||||
// .replace(/`[^`\n]+`/g, '') // 行内代码
|
||||
.replace(/{%[^%]*%}/g, '') // Hexo 标签
|
||||
.replace(/^\|.*?\|.*$/gm, '') // 表格行
|
||||
.replace(/!\[.*?\]\(.*?\)/g, '') // 图片
|
||||
.replace(/\[(.*?)\]\(.*?\)/g, '$1') // 超链接文本
|
||||
.replace(/<[^>]+>/g, '') // HTML 标签
|
||||
.replace(/ /g, ' ') // 空格实体
|
||||
.replace(/\n{2,}/g, '\n') // 多重换行压缩
|
||||
.replace(/^\s+|\s+$/gm, '') // 行首尾空格
|
||||
.replace(/[ \t]+/g, ' ') // 多空格压缩
|
||||
.trim();
|
||||
|
||||
// 3. 拼接标题
|
||||
const combined = (title ? title.trim() + '\n\n' : '') + content;
|
||||
```
|
||||
|
||||
但是大部分情况可以忽略这个配置项,留空即可。
|
||||
|
||||
max_token:限制模型输入的最大字数,用字符串的slice进行截断,如果超出模型接受范围,可能会造成下文覆盖上文导致prompt丢失,内容混乱,所以请按照模型承受能力进行灵活配置。
|
||||
|
||||
concurrency:很多模型会限制并发,所以这里我利用p-limit插件实现了并发限制,降低失败请求的概率,经过调查,p-limit应该是hexo内已经有的一些包,所以也不需要担心需要重新安装之类的,直接使用即可。
|
||||
|
||||
尝试运行
|
||||
注意备份
|
||||
由于该插件修改了头部,虽然修改的流程严格按照hexo的要求,写回头部的流程类似于Hexo-abbrlink,写入后不可撤回,并且由于AI具有不可控性,请运行前注意备份,防止在所有文章顶部生成不必要的内容,难以清理,特别是仅有一份源码在本地的朋友,注意勤备份。
|
||||
|
||||
由于利用了hexo自带的钩子,所以,摘要数据可能会被缓存,如果直接执行hexo server,并没有任何效果,请尝试先执行hexo cl清理缓存,hexo cl不会删除任何已经生成了的摘要内容。
|
||||
|
||||
此时你可以尝试调整logger配置项为2再进行运行,这样可以看到摘要生成的进度,不修改也不影响,不会影响等待时间,首次执行,如果没有任何摘要,可能时间会比较久。
|
||||
|
||||
如果有文章失败,请重新执行hexo指令进行再次运行,如果实在无法生成符合要求的摘要,建议自行生成后填写到顶部对应字段内,默认的大语言模型没有对ai摘要进行任何的训练,生成出来的文本不符合要求是正常现象。
|
||||
|
||||
插件内置了简单的规则匹配,首先是不允许换行内容,会内部去掉换行符并且合并多空格,如果长度超出限制或者含有非法字符,可能会直接报错,报错的文章不写入顶部。
|
||||
|
||||
判断部分
|
||||
|
||||
如果一切正常,应该可以在每篇文章的顶部看到对应的摘要文段。
|
||||
|
||||
# API推荐
|
||||
|
||||
由于插件需要自行配置API,可能在这方面需要一些帮助,所以我整理了一些免费API接口,如下:
|
||||
|
||||
| 接口名称 | 优势 | 劣势 | 字符上限 | 模型类型 | 稳定性 | 简介 |
|
||||
| ------------------------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- | ----------------------------------------- | ------------------------------------ | ---------------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| 腾讯混元 Lite | - 官方支持,性能稳定- 计划支持高达256K字符输入输出- 免费使用,无需付费 | - 需腾讯云账号及实名认证- 当前可能仍处于4K字符限制阶段,256K支持尚未全面上线 | 计划支持256K字符(当前可能为4K) | 自研大模型,具备多模态能力 | 高 | 腾讯自研的混元大模型,支持多轮对话、逻辑推理、内容创作等,计划全面支持256K字符输入输出,适用于多种应用场景。 |
|
||||
| 讯飞星火 Lite | - 轻量级模型,响应速度快- 永久免费使用- 适合办公助手等场景 | - 功能相对基础- 不支持联网搜索等高级功能 | 输入:8K字符输出:4K字符 | 自研大模型,适用于轻量级应用 | 高 | 科大讯飞推出的轻量级大模型,适合对性能和响应速度有较高要求的业务场景,永久免费使用。 |
|
||||
| ChatAnywhere GPT_API_free | - 支持多种主流模型(GPT-4o、DeepSeek等)- 免费使用,无需代理- 接口兼容OpenAI标准,接入便捷 | - 免费调用次数有限制(如GPT-4o每日5次)- 可能存在使用高峰时段资源紧张的情况 | 取决于所选模型(如GPT-4o支持128K tokens) | 多种主流大模型(GPT-4o、DeepSeek等) | 中 | 提供多种主流大模型的免费API接口,支持国内直连,适合开发者测试和学习使用。 |
|
||||
| QWQ.aigpu.cn | - 完全免费,无需注册- 基于分布式算力,支持高性能模型- 支持本地运行和共享算力 | - 高峰时段可能需要排队- 依赖社区贡献的算力,稳定性可能受影响 | 未明确限制,具体取决于模型和算力资源 | QwQ 32B大语言模型 | 中等(受算力资源影响) | 基于分布式家用显卡算力的平台,提供免费的大语言模型API,支持本地运行和共享算力,适合开发者和爱好者使用。 |
|
||||
|
||||
由于AI摘要仅仅需要小模型即可驾驭,无需众多训练知识,所以这里两个Lite版本的模型完全可以实现,唯一不同的区别可能就是上下文能力啦,更好的模型可以接受更长的文本输入,不容易丢失我们给予的prompt,输出更为准确,更符合要求,但是考虑到成本和稳定性原因,我还是建议前两个。
|
||||
|
||||
注意各家都有自有api接口和OpenAI类型接口,我们这里选择OpenAI接口,输入完整的地址如混元的兼容接口:
|
||||
https://api.hunyuan.cloud.tencent.com/v1/chat/completions
|
||||
申请token后正常使用即可。
|
||||
|
||||
# Hexo适配
|
||||
|
||||
说在前面
|
||||
有些主题已经有静态ai摘要的功能了,可以无需下面的步骤,使用插件向文件插入对应的字符串即可,下面的教程适用于butterfly或者类butterfly主题,如果是其他主题可能需要自行适配。
|
||||
|
||||
## 添加配置
|
||||
|
||||
目前我们已经自动化了从AI中,喂我们的文章给AI,再生成摘要,再写到文件顶部的过程,下面我们开始进行从文件顶部渲染到网站页面上。
|
||||
|
||||
首先在主题配置文件 `_config.butterfly.yml`文件中写入配置,方便我们进行控制摘要是否开启:
|
||||
|
||||
```yml
|
||||
# --------------------------------------
|
||||
# 文章设置
|
||||
# --------------------------------------
|
||||
# 文章AI摘要是否开启,会自动检索文章色summary字段,若没有则不显示
|
||||
ai_summary:
|
||||
enable: true
|
||||
title: 清羽のAI摘要
|
||||
loadingText: 清羽AI正在绞尽脑汁想思路ING···
|
||||
modelName: HunYuan-Lite
|
||||
```
|
||||
|
||||
这里的内容均为装饰性内容,除了enable选项,其他没有任何控制效果,都是装饰,所以无需担心,可以先按照我的写,后面再根据效果修改。
|
||||
|
||||
## 添加模板
|
||||
|
||||
下面找到主题文件下的 `/root/theme/butterfly/layout/post.pug`文件,添加文件中指出来的两行内容:
|
||||
|
||||
```diff
|
||||
extends includes/layout.pug
|
||||
|
||||
block content
|
||||
#post
|
||||
if top_img === false
|
||||
include includes/header/post-info.pug
|
||||
|
||||
article#article-container.post-content
|
||||
+ if page.summary && theme.ai_summary.enable
|
||||
+ include includes/post/post-summary.pug
|
||||
!=page.content
|
||||
include includes/post/post-copyright.pug
|
||||
.tag_share
|
||||
if (page.tags.length > 0 && theme.post_meta.post.tags)
|
||||
.post-meta__tag-list
|
||||
each item, index in page.tags.data
|
||||
a(href=url_for(item.path)).post-meta__tags #[=item.name]
|
||||
include includes/third-party/share/index.pug
|
||||
|
||||
if theme.reward.enable && theme.reward.QR_code
|
||||
!=partial('includes/post/reward', {}, {cache: true})
|
||||
|
||||
//- ad
|
||||
if theme.ad && theme.ad.post
|
||||
.ads-wrap!=theme.ad.post
|
||||
|
||||
if theme.post_pagination
|
||||
include includes/pagination.pug
|
||||
if theme.related_post && theme.related_post.enable
|
||||
!= related_posts(page,site.posts)
|
||||
|
||||
if page.comments !== false && theme.comments.use
|
||||
- var commentsJsLoad = true
|
||||
!=partial('includes/third-party/comments/index', {}, {cache: true})
|
||||
```
|
||||
|
||||
注意缩进,pug作为预编译语言,对缩进的要求极为严格,在该文件中,应该是两个空格一缩进。
|
||||
|
||||
下面添加组件,创建文件 `/root/theme/butterfly/layout/includes/post/post-summary.pug`,写入以下内容:
|
||||
|
||||
```pug
|
||||
.ai-summary
|
||||
.ai-explanation(style="display: block;" data-summary=page.summary)=theme.ai_summary.loadingText
|
||||
.ai-title
|
||||
.ai-title-left
|
||||
i.fa-brands.fa-slack
|
||||
.ai-title-text=theme.ai_summary.title
|
||||
.ai-tag#ai-tag= theme.ai_summary.modelName
|
||||
```
|
||||
|
||||
## 添加样式
|
||||
|
||||
下面我们添加样式部分,创建文件 `/root/theme/butterfly/source/css/_layout/ai-summary.styl`文件,写入:
|
||||
|
||||
```
|
||||
// ===================
|
||||
// 🌗 主题变量定义(仅使用项)
|
||||
// ===================
|
||||
|
||||
:root
|
||||
// ai_summary
|
||||
--liushen-title-font-color: #0883b7
|
||||
--liushen-maskbg: rgba(255, 255, 255, 0.85)
|
||||
--liushen-ai-bg: conic-gradient(from 1.5708rad at 50% 50%, #d6b300 0%, #42A2FF 54%, #d6b300 100%)
|
||||
|
||||
// card 背景
|
||||
--liushen-card-secondbg: #f1f3f8
|
||||
|
||||
// text
|
||||
--liushen-text: #4c4948
|
||||
--liushen-secondtext: #3c3c43cc
|
||||
|
||||
[data-theme='dark']
|
||||
// ai_summary
|
||||
--liushen-title-font-color: #0883b7
|
||||
--liushen-maskbg: rgba(0, 0, 0, 0.85)
|
||||
--liushen-ai-bg: conic-gradient(from 1.5708rad at 50% 50%, rgba(214, 178, 0, 0.46) 0%, rgba(66, 161, 255, 0.53) 54%, rgba(214, 178, 0, 0.49) 100%)
|
||||
|
||||
// card 背景
|
||||
--liushen-card-secondbg: #3e3f41
|
||||
|
||||
// text
|
||||
--liushen-text: #ffffffb3
|
||||
--liushen-secondtext: #a1a2b8
|
||||
|
||||
// ===================
|
||||
// 📘 AI 摘要模块样式
|
||||
// ===================
|
||||
|
||||
if hexo-config('ai_summary.enable')
|
||||
.ai-summary
|
||||
background-color var(--liushen-maskbg)
|
||||
background var(--liushen-card-secondbg)
|
||||
border-radius 12px
|
||||
padding 8px 8px 12px 8px
|
||||
line-height 1.3
|
||||
flex-direction column
|
||||
margin-bottom 16px
|
||||
display flex
|
||||
gap 5px
|
||||
position relative
|
||||
|
||||
&::before
|
||||
content ''
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
width 100%
|
||||
height 100%
|
||||
z-index 1
|
||||
filter blur(8px)
|
||||
opacity .4
|
||||
background-image var(--liushen-ai-bg)
|
||||
transform scaleX(1) scaleY(.95) translateY(2px)
|
||||
|
||||
&::after
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
border-radius: 12px;
|
||||
background: var(--liushen-maskbg);
|
||||
|
||||
.ai-explanation
|
||||
z-index 10
|
||||
padding 8px 12px
|
||||
font-size 15px
|
||||
line-height 1.4
|
||||
color var(--liushen-text)
|
||||
text-align justify
|
||||
|
||||
// ✅ 打字机光标动画
|
||||
&::after
|
||||
content ''
|
||||
display inline-block
|
||||
width 8px
|
||||
height 2px
|
||||
margin-left 2px
|
||||
background var(--liushen-text)
|
||||
vertical-align bottom
|
||||
animation blink-underline 1s ease-in-out infinite
|
||||
transition all .3s
|
||||
position relative
|
||||
bottom 3px
|
||||
|
||||
// 平滑滚动动画
|
||||
// .char
|
||||
// display inline-block
|
||||
// opacity 0
|
||||
// animation chat-float .5s ease forwards
|
||||
|
||||
.ai-title
|
||||
z-index 10
|
||||
font-size 14px
|
||||
display flex
|
||||
border-radius 8px
|
||||
align-items center
|
||||
position relative
|
||||
padding 0 12px
|
||||
cursor default
|
||||
user-select none
|
||||
|
||||
.ai-title-left
|
||||
display flex
|
||||
align-items center
|
||||
color var(--liushen-title-font-color)
|
||||
|
||||
i
|
||||
margin-right 3px
|
||||
display flex
|
||||
color var(--liushen-title-font-color)
|
||||
border-radius 20px
|
||||
justify-content center
|
||||
align-items center
|
||||
|
||||
.ai-title-text
|
||||
font-weight 500
|
||||
|
||||
.ai-tag
|
||||
color var(--liushen-secondtext)
|
||||
font-weight 300
|
||||
margin-left auto
|
||||
display flex
|
||||
align-items center
|
||||
justify-content center
|
||||
transition .3s
|
||||
|
||||
// 平滑滚动动画
|
||||
// @keyframes chat-float
|
||||
// 0%
|
||||
// opacity 0
|
||||
// transform translateY(20px)
|
||||
// 100%
|
||||
// opacity 1
|
||||
// transform translateY(0)
|
||||
|
||||
// ✅ 打字机光标闪烁动画
|
||||
@keyframes blink-underline
|
||||
0%, 100%
|
||||
opacity 1
|
||||
50%
|
||||
opacity 0
|
||||
```
|
||||
|
||||
样式也实现啦!目前就差将我们的摘要插入到我们的网站就大功告成啦,为了实现的更加逼真,我这里实现了两种样式一个是打字机效果,一个是平滑显示效果,可以按需引入:
|
||||
|
||||
## 添加核心JS
|
||||
|
||||
介绍两种动效,可以按照自己的需求在任意js文件中选择一个引入即可,两个的区别是,打字机效果更加的节省性能,而平滑显示,因为每个文本为一个span,所以会比较耗费性能。
|
||||
|
||||
### 打字机
|
||||
|
||||
```js
|
||||
// 打字机效果
|
||||
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);
|
||||
```
|
||||
|
||||
本站使用的就是打字机效果,可以自行查看。
|
||||
|
||||
### 平滑
|
||||
|
||||
这个没有样图,如果好奇可以自行部署并尝试:
|
||||
|
||||
```js
|
||||
// 平滑弹出效果
|
||||
function typeText(text, targetSelector, options = {}) {
|
||||
const {
|
||||
delay = 50, // 每个字符之间的延迟(毫秒)
|
||||
startDelay = 2000, // 开始打字前的延迟(默认 3 秒)
|
||||
onComplete = null, // 动画完成后的回调
|
||||
clearBefore = true // 是否在开始前清空原有内容
|
||||
} = options;
|
||||
|
||||
const targetEl = document.querySelector(targetSelector);
|
||||
if (!targetEl || typeof text !== "string") return;
|
||||
|
||||
// if (clearBefore) targetEl.textContent = "";
|
||||
|
||||
let index = 0;
|
||||
let frameId = null;
|
||||
|
||||
function renderChar() {
|
||||
if (index < text.length) {
|
||||
const span = document.createElement("span");
|
||||
span.textContent = text[index++];
|
||||
span.className = "char";
|
||||
targetEl.appendChild(span);
|
||||
frameId = requestAnimationFrame(() => setTimeout(renderChar, delay));
|
||||
} else {
|
||||
cancelAnimationFrame(frameId);
|
||||
onComplete && onComplete(targetEl);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (clearBefore) targetEl.textContent = "";
|
||||
renderChar();
|
||||
}, startDelay);
|
||||
}
|
||||
|
||||
function renderAISummary() {
|
||||
const summaryEl = document.querySelector('.ai-summary .ai-explanation');
|
||||
if (!summaryEl) return;
|
||||
|
||||
const summaryText = summaryEl.getAttribute('data-summary');
|
||||
if (summaryText) {
|
||||
typeText(summaryText, ".ai-summary .ai-explanation"); // 如果需要切换,在这里调用另一个函数即可
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('pjax:complete', renderAISummary);
|
||||
document.addEventListener('DOMContentLoaded', renderAISummary);
|
||||
```
|
||||
|
||||
注意,平滑滚动部分的css,默认注释掉了
|
||||
|
||||
好的,设置完毕!
|
||||
438
source/_posts/2025.08/add-calendar-side.md
Normal file
438
source/_posts/2025.08/add-calendar-side.md
Normal file
@@ -0,0 +1,438 @@
|
||||
---
|
||||
title: 在侧边栏添加日历和倒计时
|
||||
categories: 建站手札
|
||||
tags: 网站
|
||||
series: webcustom
|
||||
summary: 实现了一个侧边栏日历与倒计时卡片,包含以下核心功能:日历展示:显示当前日期(公历与农历)、星期、生肖、干支年份。动态生成当月日历网格,高亮当天日期。倒计时模块:距离除夕倒计时:实时计算并显示剩余天数(示例中为2026年2月16日)。进度条:分别展示本年、本月、本周的已过去进度(百分比)及剩余天数。农历支持:调用 chinese-lunar.js 库将公历转换为农历,格式化显示为“干支年+生肖年+农历月+农历日”。
|
||||
abbrlink: 5ed2f1e6
|
||||
date: 2025-08-11 10:32:44
|
||||
---
|
||||
突然看到某个网站侧边栏有日历和倒计时,就研究了一下,抄下来了(🤭)
|
||||
效果图:
|
||||
|
||||
<center><img src="https://pic.biss.click/i/2025/08/11/004913.webp" alt="004913.webp"></center>
|
||||
|
||||
# 添加js
|
||||
|
||||
```js
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
initializeCard();
|
||||
});
|
||||
|
||||
document.addEventListener("pjax:complete", () => {
|
||||
initializeCard();
|
||||
});
|
||||
|
||||
function initializeCard() {
|
||||
cardTimes();
|
||||
cardRefreshTimes();
|
||||
}
|
||||
|
||||
let year, month, week, date, dates, weekStr, monthStr, asideTime, asideDay, asideDayNum, animalYear, ganzhiYear, lunarMon, lunarDay;
|
||||
const now = new Date();
|
||||
|
||||
function cardRefreshTimes() {
|
||||
const e = document.getElementById("card-widget-schedule");
|
||||
if (e) {
|
||||
asideDay = (now - asideTime) / 1e3 / 60 / 60 / 24;
|
||||
e.querySelector("#pBar_year").value = asideDay;
|
||||
e.querySelector("#p_span_year").innerHTML = (asideDay / 365 * 100).toFixed(1) + "%";
|
||||
e.querySelector(".schedule-r0 .schedule-d1 .aside-span2").innerHTML = `还剩<a> ${(365 - asideDay).toFixed(0)} </a>天`;
|
||||
e.querySelector("#pBar_month").value = date;
|
||||
e.querySelector("#pBar_month").max = dates;
|
||||
e.querySelector("#p_span_month").innerHTML = (date / dates * 100).toFixed(1) + "%";
|
||||
e.querySelector(".schedule-r1 .schedule-d1 .aside-span2").innerHTML = `还剩<a> ${(dates - date)} </a>天`;
|
||||
e.querySelector("#pBar_week").value = week === 0 ? 7 : week;
|
||||
e.querySelector("#p_span_week").innerHTML = ((week === 0 ? 7 : week) / 7 * 100).toFixed(1) + "%";
|
||||
e.querySelector(".schedule-r2 .schedule-d1 .aside-span2").innerHTML = `还剩<a> ${(7 - (week === 0 ? 7 : week))} </a>天`;
|
||||
}
|
||||
}
|
||||
|
||||
function cardTimes() {
|
||||
year = now.getFullYear();
|
||||
month = now.getMonth();
|
||||
week = now.getDay();
|
||||
date = now.getDate();
|
||||
|
||||
const e = document.getElementById("card-widget-calendar");
|
||||
if (e) {
|
||||
const isLeapYear = year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
|
||||
weekStr = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"][week];
|
||||
const monthData = [
|
||||
{ month: "1月", days: 31 },
|
||||
{ month: "2月", days: isLeapYear ? 29 : 28 },
|
||||
{ month: "3月", days: 31 },
|
||||
{ month: "4月", days: 30 },
|
||||
{ month: "5月", days: 31 },
|
||||
{ month: "6月", days: 30 },
|
||||
{ month: "7月", days: 31 },
|
||||
{ month: "8月", days: 31 },
|
||||
{ month: "9月", days: 30 },
|
||||
{ month: "10月", days: 31 },
|
||||
{ month: "11月", days: 30 },
|
||||
{ month: "12月", days: 31 }
|
||||
];
|
||||
monthStr = monthData[month].month;
|
||||
dates = monthData[month].days;
|
||||
|
||||
const t = (week + 8 - date % 7) % 7;
|
||||
let n = "", d = false, s = 7 - t;
|
||||
const o = (dates - s) % 7 === 0 ? Math.floor((dates - s) / 7) + 1 : Math.floor((dates - s) / 7) + 2;
|
||||
const c = e.querySelector("#calendar-main");
|
||||
const l = e.querySelector("#calendar-date");
|
||||
|
||||
l.style.fontSize = ["64px", "48px", "36px"][Math.min(o - 3, 2)];
|
||||
|
||||
for (let i = 0; i < o; i++) {
|
||||
if (!c.querySelector(`.calendar-r${i}`)) {
|
||||
c.innerHTML += `<div class='calendar-r${i}'></div>`;
|
||||
}
|
||||
for (let j = 0; j < 7; j++) {
|
||||
if (i === 0 && j === t) {
|
||||
n = 1;
|
||||
d = true;
|
||||
}
|
||||
const r = n === date ? " class='now'" : "";
|
||||
if (!c.querySelector(`.calendar-r${i} .calendar-d${j} a`)) {
|
||||
c.querySelector(`.calendar-r${i}`).innerHTML += `<div class='calendar-d${j}'><a${r}>${n}</a></div>`;
|
||||
}
|
||||
if (n >= dates) {
|
||||
n = "";
|
||||
d = false;
|
||||
}
|
||||
if (d) {
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const lunarDate = chineseLunar.solarToLunar(new Date(year, month, date));
|
||||
animalYear = chineseLunar.format(lunarDate, "A");
|
||||
ganzhiYear = chineseLunar.format(lunarDate, "T").slice(0, -1);
|
||||
lunarMon = chineseLunar.format(lunarDate, "M");
|
||||
lunarDay = chineseLunar.format(lunarDate, "d");
|
||||
|
||||
const newYearDate = new Date("2026/02/16 00:00:00");
|
||||
const daysUntilNewYear = Math.floor((newYearDate - now) / 1e3 / 60 / 60 / 24);
|
||||
asideTime = new Date(`${new Date().getFullYear()}/01/01 00:00:00`);
|
||||
asideDay = (now - asideTime) / 1e3 / 60 / 60 / 24;
|
||||
asideDayNum = Math.floor(asideDay);
|
||||
const weekNum = week - asideDayNum % 7 >= 0 ? Math.ceil(asideDayNum / 7) : Math.ceil(asideDayNum / 7) + 1;
|
||||
|
||||
e.querySelector("#calendar-week").innerHTML = `第${weekNum}周 ${weekStr}`;
|
||||
e.querySelector("#calendar-date").innerHTML = date.toString().padStart(2, "0");
|
||||
e.querySelector("#calendar-solar").innerHTML = `${year}年${monthStr} 第${asideDay.toFixed(0)}天`;
|
||||
e.querySelector("#calendar-lunar").innerHTML = `${ganzhiYear}${animalYear}年 ${lunarMon}${lunarDay}`;
|
||||
document.getElementById("schedule-days").innerHTML = daysUntilNewYear;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# 添加css
|
||||
|
||||
在主题文件 `source/css/`新建 `calendar.css`。
|
||||
|
||||
```css
|
||||
/* 浅色主题变量覆盖 -------------------------------- */
|
||||
:root {
|
||||
--anzhiyu-main: #a0d2eb; /* 主强调色:柔和天空蓝 */
|
||||
--anzhiyu-main-op: rgba(160, 210, 235, 0.6);
|
||||
--anzhiyu-main-op-deep: rgba(160, 210, 235, 0.4);
|
||||
--anzhiyu-main-op-light: rgba(160, 210, 235, 0.2);
|
||||
|
||||
--efu-card-bg: #fdfdfd; /* 卡片背景:几乎白 */
|
||||
--efu-fontcolor: #444; /* 主文本:深灰 */
|
||||
--efu-secondtext: #999; /* 次级文本:浅灰 */
|
||||
}
|
||||
.card-widget {
|
||||
padding: 10px!important;
|
||||
max-height: calc(100vh - 100px);
|
||||
}
|
||||
.card-times a, .card-times div {
|
||||
color: var(--efu-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(--anzhiyu-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(--efu-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(--anzhiyu-main);
|
||||
color: var(--efu-card-bg);
|
||||
}
|
||||
|
||||
#calendar-main .calendar-rh a {
|
||||
color: var(--efu-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(--anzhiyu-main-op-deep), var(--anzhiyu-main-op), var(--anzhiyu-main-op-light));
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
progress::-webkit-progress-value {
|
||||
background: var(--anzhiyu-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(--efu-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(--anzhiyu-main);
|
||||
}
|
||||
|
||||
#schedule-date {
|
||||
height: 20px;
|
||||
line-height: 1;
|
||||
font-size: 12px;
|
||||
color: var(--efu-secondtext);
|
||||
}
|
||||
```
|
||||
|
||||
# 引入
|
||||
|
||||
新增+号后面内容
|
||||
|
||||
```yml
|
||||
inject:
|
||||
head:
|
||||
# 自定义css
|
||||
+ - <link rel="stylesheet" href="/css/calendar.css">
|
||||
|
||||
bottom:
|
||||
# 自定义js
|
||||
+ - <script src="/js/calendar.js"></script>
|
||||
+ - <script src="https://unpkg.com/chinese-lunar@0.1.4/lib/chinese-lunar.js"></script>
|
||||
```
|
||||
|
||||
在 `blogroot/source/_data` 文件夹下创建 `widget.yml` 文件,并添加以下内容:
|
||||
|
||||
```yml
|
||||
bottom:
|
||||
- class_name: calendar
|
||||
id_name: card-widget-calendar
|
||||
name:
|
||||
icon:
|
||||
order: -1
|
||||
html: |
|
||||
<div id="calendar-area-left">
|
||||
<div id="calendar-week"></div>
|
||||
<div id="calendar-date" style="font-size: 48px;"></div>
|
||||
<div id="calendar-solar"></div>
|
||||
<div id="calendar-lunar"></div>
|
||||
</div>
|
||||
<div id="calendar-area-right">
|
||||
<div id="calendar-main">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
- class_name: schedule
|
||||
id_name: card-widget-schedule
|
||||
name:
|
||||
icon:
|
||||
order: -1
|
||||
html: |
|
||||
<div id="schedule-area-left">
|
||||
<div id="schedule-title">距离除夕</div>
|
||||
<div id="schedule-days"></div>
|
||||
<div id="schedule-date">2025-01-28</div>
|
||||
</div>
|
||||
<div id="schedule-area-right">
|
||||
<div class="schedule-r0">
|
||||
<div class="schedule-d0">本年</div>
|
||||
<div class="schedule-d1">
|
||||
<span id="p_span_year" class="aside-span1"></span>
|
||||
<span class="aside-span2">还剩<a></a>天</span>
|
||||
<progress max="365" id="pBar_year"></progress>
|
||||
</div>
|
||||
</div>
|
||||
<div class="schedule-r1">
|
||||
<div class="schedule-d0">本月</div>
|
||||
<div class="schedule-d1">
|
||||
<span id="p_span_month" class="aside-span1"></span>
|
||||
<span class="aside-span2">还剩<a></a>天</span>
|
||||
<progress max="30" id="pBar_month"></progress>
|
||||
</div>
|
||||
</div>
|
||||
<div class="schedule-r2">
|
||||
<div class="schedule-d0">本周</div>
|
||||
<div class="schedule-d1">
|
||||
<span id="p_span_week" class="aside-span1"></span>
|
||||
<span class="aside-span2">还剩<a></a>天</span>
|
||||
<progress max="7" id="pBar_week"></progress>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
# 参考
|
||||
|
||||
{% link 乐活星球,lohas,https://blog.lohas.fun/hc/jc005/ %}
|
||||
372
source/_posts/2025.08/add-own-app-to-1panel.md
Normal file
372
source/_posts/2025.08/add-own-app-to-1panel.md
Normal file
@@ -0,0 +1,372 @@
|
||||
---
|
||||
title: 为1Panel添加自己想要的应用
|
||||
categories: 技术
|
||||
tags: 1panel
|
||||
abbrlink: bb18d851
|
||||
summary: 为1Panel添加自己想要的应用
|
||||
date: 2025-08-19 10:43:16
|
||||
---
|
||||
记录一下自己为1Panel贡献自己应用的经历
|
||||
|
||||
# 预准备
|
||||
1. Fork仓库
|
||||
{% link 1Panel appstore 仓库,1panel appstore,https://github.com/1Panel-dev/appstore %}
|
||||
把仓库fork到自己的仓库
|
||||
2. pull
|
||||
```bash
|
||||
git clone -b dev https://<your-github-username>/appstore
|
||||
```
|
||||
创建新分支
|
||||
```bash
|
||||
cd appstore
|
||||
git checkout -b app/<app-name>
|
||||
```
|
||||
# 创建文件
|
||||
|
||||
## 文件目录
|
||||
```
|
||||
├──halo // 以 halo 的 key 命名 ,下面解释什么是 key
|
||||
├── logo.png // 应用 logo , 最好是 180 * 180 px 不要超过 10 KB
|
||||
├── data.yml // 应用声明文件
|
||||
├── README.md // 应用的 README
|
||||
├── 2.2.0 // 应用版本 注意不要以 v 开头
|
||||
│ ├── data.yml // 应用的参数配置,下面有详细介绍
|
||||
│ ├── data // 挂载出来的目录
|
||||
| ├── scripts // 脚本目录 存放 init.sh upgrade.sh uninstall.sh
|
||||
│ └── docker-compose.yml // docker-compose 文件
|
||||
└── 2.3.2
|
||||
├── data.yml
|
||||
├── data
|
||||
└── docker-compose.yml
|
||||
```
|
||||
## 应用声明文件`data.yml`
|
||||
|
||||
本文件主要用于声明应用的一些信息
|
||||
```yml
|
||||
additionalProperties: #固定参数
|
||||
key: halo #应用的 key ,仅限英文,用于在 Linux 创建文件夹
|
||||
name: Halo #应用名称
|
||||
tags:
|
||||
- WebSite #应用标签,可以有多个,请参照下方的标签列表
|
||||
description:
|
||||
en: Powerful and easy-to-use open source website builder
|
||||
zh: 强大易用的开源建站工具 #应用中文描述,不要超过30个字
|
||||
zh-Hant:
|
||||
ja:
|
||||
ms:
|
||||
pt-br:
|
||||
ru:
|
||||
ko:
|
||||
type: website #应用类型,区别于应用分类,只能有一个,请参照下方的类型列表
|
||||
crossVersionUpdate: true #是否可以跨大版本升级
|
||||
limit: 0 #应用安装数量限制,0 代表无限制
|
||||
website: https://halo.run/ #官网地址
|
||||
github: https://github.com/halo-dev/halo #github 地址
|
||||
document: https://docs.halo.run/ #文档地址
|
||||
```
|
||||
## 应用标签 - tags 字段
|
||||
| key | name |
|
||||
|----------|----------|
|
||||
| WebSite | 建站 |
|
||||
| Server | Web 服务器 |
|
||||
| Runtime | 运行环境 |
|
||||
| Database | 数据库 |
|
||||
| Tool | 工具 |
|
||||
| CI/CD | CI/CD |
|
||||
| Local | 本地 |
|
||||
|
||||
# 应用类型 - type 字段
|
||||
|
||||
| type | 说明 |
|
||||
|---------|---------------------------------------------------------|
|
||||
| website | website 类型在 1Panel 中支持在网站中一键部署,wordpress halo 都是此 type |
|
||||
| runtime | mysql openresty redis 等类型的应用 |
|
||||
| tool | phpMyAdmin redis-commander jenkins 等类型的应用 |
|
||||
|
||||
# 应用参数配置文件 data.yml
|
||||
(注意区分于应用主目录下面的 `data.yaml`)
|
||||
|
||||
本文件主要用于生成安装时要填写的 form 表单,在应用版本文件夹下面 可以无表单,但是需要有这个 data.yml文件,并且包含 formFields 字段
|
||||
|
||||
以安装 halo 时的 form 表单 为例
|
||||
|
||||
如果要生成上面的表单,需要这么填写 `data.yml`
|
||||
```yml
|
||||
additionalProperties: #固定参数
|
||||
formFields:
|
||||
- default: ""
|
||||
envKey: PANEL_DB_HOST #docker-compose 文件中的参数
|
||||
key: mysql #依赖应用的 key , 例如 mysql
|
||||
label:
|
||||
en: Database Service
|
||||
ja: データベースサービス
|
||||
ms: Perkhidmatan Pangkalan Data
|
||||
pt-br: Serviço de Banco de Dados
|
||||
ru: Сервис базы данных
|
||||
ko: 데이터베이스 서비스
|
||||
zh: 数据库服务
|
||||
zh-Hant: 數據庫 服務
|
||||
required: true #是否必填
|
||||
type: service #如果需要依赖其他应用,例如数据库,使用此 type
|
||||
- default: halo
|
||||
envKey: PANEL_DB_NAME
|
||||
label:
|
||||
en: Database
|
||||
ja: データベース
|
||||
ms: Pangkalan Data
|
||||
pt-br: Banco de Dados
|
||||
ru: База данных
|
||||
ko: 데이터베이스
|
||||
zh: 数据库名
|
||||
zh-Hant: 資料庫名稱
|
||||
random: true #是否在 default 文字后面,增加随机字符串
|
||||
required: true
|
||||
rule: paramCommon #校验规则
|
||||
type: text #需要手动填写的,使用此 type
|
||||
- default: halo
|
||||
envKey: PANEL_DB_USER
|
||||
label:
|
||||
en: User
|
||||
ja: ユーザー
|
||||
ms: Pengguna
|
||||
pt-br: Usuário
|
||||
ru: Пользователь
|
||||
ko: 사용자
|
||||
zh: 数据库用户
|
||||
zh-Hant: 資料庫使用者
|
||||
random: true
|
||||
required: true
|
||||
rule: paramCommon
|
||||
type: text
|
||||
- default: halo
|
||||
envKey: PANEL_DB_USER_PASSWORD
|
||||
label:
|
||||
en: Password
|
||||
ja: パスワード
|
||||
ms: Kata laluan
|
||||
pt-br: Senha
|
||||
ru: Пароль
|
||||
ko: 비밀번호
|
||||
zh: 数据库用户密码
|
||||
zh-Hant: 資料庫使用者密碼
|
||||
random: true
|
||||
required: true
|
||||
rule: paramComplexity
|
||||
type: password #密码字段使用此 type
|
||||
- default: admin
|
||||
envKey: HALO_ADMIN
|
||||
labelEn: Admin Username
|
||||
labelZh: 超级管理员用户名
|
||||
required: true
|
||||
rule: paramCommon
|
||||
type: text
|
||||
- default: http://localhost:8080
|
||||
edit: true
|
||||
envKey: HALO_EXTERNAL_URL
|
||||
label:
|
||||
en: External URL
|
||||
ja: 外部URL
|
||||
ms: URL Luaran
|
||||
pt-br: URL Externa
|
||||
ru: Внешний URL
|
||||
ko: 외부 URL
|
||||
zh: 外部访问地址
|
||||
zh-Hant: 外部訪問地址
|
||||
required: true
|
||||
rule: paramExtUrl
|
||||
type: text
|
||||
- default: 8080
|
||||
edit: true
|
||||
envKey: PANEL_APP_PORT_HTTP
|
||||
label:
|
||||
en: Port
|
||||
ja: ポート
|
||||
ms: Port
|
||||
pt-br: Porta
|
||||
ru: Порт
|
||||
ko: 포트
|
||||
zh: 端口
|
||||
zh-Hant: 埠
|
||||
required: true
|
||||
rule: paramPort
|
||||
type: number #端口使用此 type
|
||||
```
|
||||
## 关于端口字段:
|
||||
`PANEL_APP_PORT_HTTP` 有 `web` 访问端口的优先使用此 `envKey`
|
||||
`envKey` 中包含 `PANEL_APP_PORT` 前缀会被认定为端口类型,并且用于安装前的端口占用校验。注意:端口需要是外部端口
|
||||
## 关于 type 字段:
|
||||
| type | 说明 |
|
||||
|----------|-----------------------------------------------------------------------------------------|
|
||||
| service | type: service 如果该应用需要依赖其他组件,如 mysql redis 等,可以通过 key: mysql 定义依赖的名称,在创建应用时会要求先创建依赖的应用。 |
|
||||
| password | type: password 敏感信息,如密码相关的字段会默认不显示明文。 |
|
||||
| text | type: text 一般内容,比如数据库名称,默认明文显示。 |
|
||||
| number | type: number 一般用在端口相关的配置上,只允许输入数字。 |
|
||||
|
||||
简单的例子
|
||||
```yml
|
||||
# type: service,定义一个 mysql 的 service 依赖。
|
||||
- child:
|
||||
default: ""
|
||||
envKey: PANEL_DB_HOST
|
||||
required: true
|
||||
type: service
|
||||
default: mysql
|
||||
envKey: PANEL_DB_TYPE
|
||||
label:
|
||||
en: Database Service
|
||||
ja: データベースサービス
|
||||
ms: Perkhidmatan Pangkalan Data
|
||||
pt-br: Serviço de Banco de Dados
|
||||
ru: Сервис баз данных
|
||||
ko: 데이터베이스 서비스
|
||||
zh-hant: 資料庫服務
|
||||
zh: 数据库服务
|
||||
required: true
|
||||
type: apps
|
||||
values:
|
||||
- label: MySQL
|
||||
value: mysql
|
||||
- label: MariaDB
|
||||
value: mariadb
|
||||
|
||||
# type: password
|
||||
- default: word
|
||||
envKey: PANEL_DB_USER_PASSWORD
|
||||
labelEn: Database Password
|
||||
labelZh: 数据库密码
|
||||
label:
|
||||
en: Database Password
|
||||
ja: データベースのパスワード
|
||||
ms: Kata Laluan Pangkalan Data
|
||||
pt-br: Senha do Banco de Dados
|
||||
ru: Пароль базы данных
|
||||
ko: 데이터베이스 비밀번호
|
||||
zh-hant: 資料庫密碼
|
||||
zh: 数据库密码
|
||||
random: true
|
||||
required: true
|
||||
type: password
|
||||
|
||||
# type: text
|
||||
- default: ""
|
||||
edit: true
|
||||
envKey: REDIS_HOST
|
||||
key: redis
|
||||
required: true
|
||||
type: service
|
||||
label:
|
||||
en: Redis Service
|
||||
ja: Redis サービス
|
||||
ms: Perkhidmatan Redis
|
||||
pt-br: Serviço Redis
|
||||
ru: Сервис Redis
|
||||
ko: Redis 서비스
|
||||
zh-Hant: Redis 服務
|
||||
zh: Redis 服务
|
||||
|
||||
# type: number
|
||||
- default: ""
|
||||
edit: true
|
||||
envKey: DBHUB_DB_PORT
|
||||
required: true
|
||||
type: text
|
||||
label:
|
||||
en: Database Port
|
||||
ja: データベースのポート
|
||||
ms: Pangkalan Data Port
|
||||
pt-br: Porta do Banco de Dados
|
||||
ru: Порт базы данных
|
||||
ko: 데이터베이스 포트
|
||||
zh-hant: 資料庫端口
|
||||
zh: 数据库端口
|
||||
|
||||
# type: select
|
||||
- default: "ERROR"
|
||||
envKey: LOG_LEVEL
|
||||
required: true
|
||||
type: select
|
||||
values:
|
||||
- label: DEBUG
|
||||
value: "DEBUG"
|
||||
- label: INFO
|
||||
value: "INFO"
|
||||
- label: WARNING
|
||||
value: "WARNING"
|
||||
- label: ERROR
|
||||
value: "ERROR"
|
||||
- label: CRITICAL
|
||||
value: "CRITICAL"
|
||||
```
|
||||
rule 字段目前支持的几种校验
|
||||
| rule | 规则 |
|
||||
|-----------------|------------------------------------|
|
||||
| paramPort | 用于限制端口范围为 1-65535 |
|
||||
| paramExtUrl | 格式为 http(s)://(域名/ip):(端口) |
|
||||
| paramCommon | 英文、数字、.-和_,长度2-30 |
|
||||
| paramComplexity | 支持英文、数字、.%@$!&~_-,长度6-30,特殊字符不能在首尾 |
|
||||
|
||||
应用 `docker-compose.yml` 文件
|
||||
|
||||
`${PANEL_APP_PORT_HTTP} `类型的参数,都在 `data.yml` 中有声明
|
||||
|
||||
```yml
|
||||
version: "3"
|
||||
services:
|
||||
halo:
|
||||
image: halohub/halo:2.2.0
|
||||
container_name: ${CONTAINER_NAME} // 固定写法,勿改
|
||||
restart: always
|
||||
networks:
|
||||
- 1panel-network // 1Panel 创建的应用都在此网络下
|
||||
volumes:
|
||||
- ./data:/root/.halo2
|
||||
ports:
|
||||
- ${PANEL_APP_PORT_HTTP}:8090
|
||||
command:
|
||||
- --spring.r2dbc.url=r2dbc:pool:${HALO_PLATFORM}://${PANEL_DB_HOST}:${HALO_DB_PORT}/${PANEL_DB_NAME}
|
||||
- --spring.r2dbc.username=${PANEL_DB_USER}
|
||||
- --spring.r2dbc.password=${PANEL_DB_USER_PASSWORD}
|
||||
- --spring.sql.init.platform=${HALO_PLATFORM}
|
||||
- --halo.external-url=${HALO_EXTERNAL_URL}
|
||||
- --halo.security.initializer.superadminusername=${HALO_ADMIN}
|
||||
- --halo.security.initializer.superadminpassword=${HALO_ADMIN_PASSWORD}
|
||||
labels:
|
||||
createdBy: "Apps"
|
||||
|
||||
networks:
|
||||
1panel-network:
|
||||
external: true
|
||||
```
|
||||
## 脚本
|
||||
1Panel 在 安装之前、升级之前、卸载之后支持执行 .sh 脚本
|
||||
分别对应 init.sh upgrade.sh uninstall.sh
|
||||
存放目录(以halo为例) : halo/2.2.0/scripts
|
||||
|
||||
# 本地测试
|
||||
将应用目录上传到 1Panel 的 /opt/1panel/resource/apps/local 文件夹下
|
||||
注意:/opt 为 1Panel 默认安装目录,请根据自己的实际情况修改
|
||||
上传完成后,目录结构如下
|
||||
```
|
||||
├──halo
|
||||
├── logo.png
|
||||
├── data.yml
|
||||
├── README.md
|
||||
├── 2.2.0
|
||||
├── data.yml
|
||||
├── data
|
||||
└── docker-compose.yml
|
||||
```
|
||||
在 1Panel 应用商店中,点击更新应用列表按钮同步本地应用
|
||||
|
||||
v1.2 版本及之前版本的本地应用,请参考这个文档修改
|
||||
|
||||
# 提交文件
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "Add my-app"
|
||||
git push origin dev
|
||||
```
|
||||
# 提交 Pull Request
|
||||
在你的仓库点击 Pull requests 菜单
|
||||
点击 New pull request ,填写标题和描述
|
||||
选择由你的分支提交到 1Panel-dev/appstore
|
||||
340
source/_posts/2025.08/add-welcome-message-in-sidebar.md
Normal file
340
source/_posts/2025.08/add-welcome-message-in-sidebar.md
Normal file
@@ -0,0 +1,340 @@
|
||||
---
|
||||
title: 在侧边栏中添加欢迎信息
|
||||
abbrlink: da17d00
|
||||
categories: 建站手札
|
||||
tags: 网站
|
||||
series: webcustom
|
||||
date: 2025-08-10 19:30:40
|
||||
summary: 添加侧边栏的欢迎信息
|
||||
---
|
||||
# 效果展示
|
||||
|
||||
<center><img src="https://pic.biss.click/i/2025/08/11/439473.webp" alt="效果展示"></center>
|
||||
|
||||
# 开始折腾
|
||||
|
||||
## 申请腾讯地图的api key
|
||||
|
||||
{% link 腾讯地图,Tencent,https://lbs.qq.com/dev/console/application/mine %}
|
||||
自行查看文档,要注意在填写信任域名时不填 `https://`
|
||||
|
||||
## 添加js
|
||||
|
||||
在主题 `/source/js/`目录下添加 `txmap.js`
|
||||
|
||||
```js
|
||||
//get请求
|
||||
$.ajax({
|
||||
type: 'get',
|
||||
url: 'https://apis.map.qq.com/ws/location/v1/ip',
|
||||
data: {
|
||||
key: '你的key',
|
||||
output: 'jsonp',
|
||||
callback: '?',
|
||||
},
|
||||
dataType: 'jsonp',
|
||||
success: function (res) {
|
||||
window.ipLocation = res;
|
||||
}
|
||||
})
|
||||
function getDistance(e1, n1, e2, n2) {
|
||||
const R = 6371
|
||||
const { sin, cos, asin, PI, hypot } = Math
|
||||
let getPoint = (e, n) => {
|
||||
e *= PI / 180
|
||||
n *= PI / 180
|
||||
return { x: cos(n) * cos(e), y: cos(n) * sin(e), z: sin(n) }
|
||||
}
|
||||
|
||||
let a = getPoint(e1, n1)
|
||||
let b = getPoint(e2, n2)
|
||||
let c = hypot(a.x - b.x, a.y - b.y, a.z - b.z)
|
||||
let r = asin(c / 2) * 2 * R
|
||||
return Math.round(r);
|
||||
}
|
||||
|
||||
function showWelcome() {
|
||||
|
||||
let dist = getDistance(112.92358, 35.79807, ipLocation.result.location.lng, ipLocation.result.location.lat); //这里记得换成自己的经纬度
|
||||
let pos = ipLocation.result.ad_info.nation;
|
||||
let ip;
|
||||
let posdesc;
|
||||
//根据国家、省份、城市信息自定义欢迎语
|
||||
switch (ipLocation.result.ad_info.nation) {
|
||||
case "日本":
|
||||
posdesc = "よろしく,一起去看樱花吗";
|
||||
break;
|
||||
case "美国":
|
||||
posdesc = "Let us live in peace!";
|
||||
break;
|
||||
case "英国":
|
||||
posdesc = "想同你一起夜乘伦敦眼";
|
||||
break;
|
||||
case "俄罗斯":
|
||||
posdesc = "干了这瓶伏特加!";
|
||||
break;
|
||||
case "法国":
|
||||
posdesc = "C'est La Vie";
|
||||
break;
|
||||
case "德国":
|
||||
posdesc = "Die Zeit verging im Fluge.";
|
||||
break;
|
||||
case "澳大利亚":
|
||||
posdesc = "一起去大堡礁吧!";
|
||||
break;
|
||||
case "加拿大":
|
||||
posdesc = "拾起一片枫叶赠予你";
|
||||
break;
|
||||
case "中国":
|
||||
pos = ipLocation.result.ad_info.province + " " + ipLocation.result.ad_info.city + " " + ipLocation.result.ad_info.district;
|
||||
ip = ipLocation.result.ip;
|
||||
switch (ipLocation.result.ad_info.province) {
|
||||
case "北京市":
|
||||
posdesc = "北——京——欢迎你~~~";
|
||||
break;
|
||||
case "天津市":
|
||||
posdesc = "讲段相声吧";
|
||||
break;
|
||||
case "河北省":
|
||||
posdesc = "山势巍巍成壁垒,天下雄关铁马金戈由此向,无限江山";
|
||||
break;
|
||||
case "山西省":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "太原市":
|
||||
posdesc = "秋叶蓝不城";
|
||||
break;
|
||||
case "晋城市":
|
||||
posdesc = "一方水土养一方人,晋城话说给晋城人...";
|
||||
break;
|
||||
default:
|
||||
posdesc = "展开坐具长三尺,已占山河五百余";
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "内蒙古自治区":
|
||||
posdesc = "天苍苍,野茫茫,风吹草低见牛羊";
|
||||
break;
|
||||
case "辽宁省":
|
||||
posdesc = "我想吃烤鸡架!";
|
||||
break;
|
||||
case "吉林省":
|
||||
posdesc = "状元阁就是东北烧烤之王";
|
||||
break;
|
||||
case "黑龙江省":
|
||||
posdesc = "很喜欢哈尔滨大剧院";
|
||||
break;
|
||||
case "上海市":
|
||||
posdesc = "众所周知,中国只有两个城市";
|
||||
break;
|
||||
case "江苏省":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "南京市":
|
||||
posdesc = "这是我挺想去的城市啦";
|
||||
break;
|
||||
case "苏州市":
|
||||
posdesc = "上有天堂,下有苏杭";
|
||||
break;
|
||||
default:
|
||||
posdesc = "散装是必须要散装的";
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "浙江省":
|
||||
posdesc = "东风渐绿西湖柳,雁已还人未南归";
|
||||
break;
|
||||
case "河南省":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "郑州市":
|
||||
posdesc = "豫州之域,天地之中";
|
||||
break;
|
||||
case "南阳市":
|
||||
posdesc = "臣本布衣,躬耕于南阳此南阳非彼南阳!";
|
||||
break;
|
||||
case "驻马店市":
|
||||
posdesc = "峰峰有奇石,石石挟仙气嵖岈山的花很美哦!";
|
||||
break;
|
||||
case "开封市":
|
||||
posdesc = "刚正不阿包青天";
|
||||
break;
|
||||
case "洛阳市":
|
||||
posdesc = "洛阳牡丹甲天下";
|
||||
break;
|
||||
default:
|
||||
posdesc = "可否带我品尝河南烩面啦?";
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "安徽省":
|
||||
posdesc = "蚌埠住了,芜湖起飞";
|
||||
break;
|
||||
case "福建省":
|
||||
posdesc = "井邑白云间,岩城远带山";
|
||||
break;
|
||||
case "江西省":
|
||||
posdesc = "落霞与孤鹜齐飞,秋水共长天一色";
|
||||
break;
|
||||
case "山东省":
|
||||
posdesc = "遥望齐州九点烟,一泓海水杯中泻";
|
||||
break;
|
||||
case "湖北省":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "黄冈市":
|
||||
posdesc = "红安将军县!辈出将才!";
|
||||
break;
|
||||
default:
|
||||
posdesc = "来碗热干面~";
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "湖南省":
|
||||
posdesc = "74751,长沙斯塔克";
|
||||
break;
|
||||
case "广东省":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "广州市":
|
||||
posdesc = "看小蛮腰,喝早茶了嘛~";
|
||||
break;
|
||||
case "深圳市":
|
||||
posdesc = "今天你逛商场了嘛~";
|
||||
break;
|
||||
case "阳江市":
|
||||
posdesc = "阳春合水!博主家乡~ 欢迎来玩~";
|
||||
break;
|
||||
default:
|
||||
posdesc = "来两斤福建人~";
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "广西壮族自治区":
|
||||
posdesc = "桂林山水甲天下";
|
||||
break;
|
||||
case "海南省":
|
||||
posdesc = "朝观日出逐白浪,夕看云起收霞光";
|
||||
break;
|
||||
case "四川省":
|
||||
posdesc = "康康川妹子";
|
||||
break;
|
||||
case "贵州省":
|
||||
posdesc = "茅台,学生,再塞200";
|
||||
break;
|
||||
case "云南省":
|
||||
posdesc = "玉龙飞舞云缠绕,万仞冰川直耸天";
|
||||
break;
|
||||
case "西藏自治区":
|
||||
posdesc = "躺在茫茫草原上,仰望蓝天";
|
||||
break;
|
||||
case "陕西省":
|
||||
posdesc = "来份臊子面加馍";
|
||||
break;
|
||||
case "甘肃省":
|
||||
posdesc = "羌笛何须怨杨柳,春风不度玉门关";
|
||||
break;
|
||||
case "青海省":
|
||||
posdesc = "牛肉干和老酸奶都好好吃";
|
||||
break;
|
||||
case "宁夏回族自治区":
|
||||
posdesc = "大漠孤烟直,长河落日圆";
|
||||
break;
|
||||
case "新疆维吾尔自治区":
|
||||
posdesc = "驼铃古道丝绸路,胡马犹闻唐汉风";
|
||||
break;
|
||||
case "台湾省":
|
||||
posdesc = "我在这头,大陆在那头";
|
||||
break;
|
||||
case "香港特别行政区":
|
||||
posdesc = "永定贼有残留地鬼嚎,迎击光非岁玉";
|
||||
break;
|
||||
case "澳门特别行政区":
|
||||
posdesc = "性感荷官,在线发牌";
|
||||
break;
|
||||
default:
|
||||
posdesc = "带我去你的城市逛逛吧!";
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
posdesc = "带我去你的国家逛逛吧";
|
||||
break;
|
||||
}
|
||||
|
||||
//根据本地时间切换欢迎语
|
||||
let timeChange;
|
||||
let date = new Date();
|
||||
if (date.getHours() >= 5 && date.getHours() < 11) timeChange = "<span>🌤️ 早上好,一日之计在于晨</span>";
|
||||
else if (date.getHours() >= 11 && date.getHours() < 13) timeChange = "<span>☀️ 中午好,记得午休喔~</span>";
|
||||
else if (date.getHours() >= 13 && date.getHours() < 17) timeChange = "<span>🕞 下午好,饮茶先啦!</span>";
|
||||
else if (date.getHours() >= 17 && date.getHours() < 19) timeChange = "<span>🚶♂️ 即将下班,记得按时吃饭~</span>";
|
||||
else if (date.getHours() >= 19 && date.getHours() < 24) timeChange = "<span>🌙 晚上好,夜生活嗨起来!</span>";
|
||||
else timeChange = "夜深了,早点休息,少熬夜";
|
||||
|
||||
// 新增ipv6显示为指定内容
|
||||
if (ip.includes(":")) {
|
||||
ip = "<br>好复杂,咱看不懂~(ipv6)";
|
||||
}
|
||||
try {
|
||||
//自定义文本和需要放的位置
|
||||
document.getElementById("welcome-info").innerHTML =
|
||||
`欢迎来自 <b><span style="color: var(--kouseki-ip-color);font-size: var(--kouseki-gl-size)">${pos}</span></b> 的小友💖<br>${posdesc}🍂<br>当前位置距博主约 <b><span style="color: var(--kouseki-ip-color)">${dist}</span></b> 公里!<br>您的IP地址为:<b><span class="ip-mask">${ip}</span></b><br>${timeChange} <br>`;
|
||||
} catch (err) {
|
||||
console.log("Pjax无法获取元素")
|
||||
}
|
||||
}
|
||||
window.onload = showWelcome;
|
||||
// 如果使用了pjax在加上下面这行代码
|
||||
document.addEventListener('pjax:complete', showWelcome);
|
||||
```
|
||||
|
||||
## 添加css文件
|
||||
|
||||
在主题文件夹/source/css/在这里我添加了模糊IP地址。
|
||||
|
||||
```css
|
||||
#welcome-info {
|
||||
overflow: hidden;
|
||||
border-radius: 14px;
|
||||
--kouseki-welcome-color: #49B1F5;
|
||||
--kouseki-ip-color: #49B1F5;
|
||||
--kouseki-gl-size: 16px!important;
|
||||
}
|
||||
/* 给 IP 地址的 span 再包一层,便于选择器定位 */
|
||||
/* JS 原文已经生成 <b><span>IP</span></b>,这里我们给这个 span 加类名 ip-mask */
|
||||
/* 如果你不方便改 HTML,可直接用属性选择器:#welcome-info b span:nth-child(1) */
|
||||
#welcome-info b span.ip-mask {
|
||||
display: inline-block;
|
||||
filter: blur(6px);
|
||||
transition: filter .3s ease;
|
||||
cursor: pointer;
|
||||
user-select: none; /* 防止复制到模糊文本 */
|
||||
}
|
||||
|
||||
/* 鼠标悬停或点击时(:active)立即清晰 */
|
||||
#welcome-info b span.ip-mask:hover,
|
||||
#welcome-info b span.ip-mask:active {
|
||||
filter: blur(0);
|
||||
}
|
||||
|
||||
/* 如果想做成「必须点一下才永久清晰」,把 :hover 去掉即可 */
|
||||
```
|
||||
|
||||
# 引用
|
||||
|
||||
在主题配置文件中添加
|
||||
|
||||
```yml
|
||||
inject:
|
||||
bottom:
|
||||
+ - <script src="https://cdn.staticfile.org/jquery/3.6.3/jquery.min.js"></script> # jQuery
|
||||
+ - <script async data-pjax src="/js/txmap.js"></script> # 腾讯位置API
|
||||
```
|
||||
|
||||
在需要展示文本的容器上添加相应id(`welcome-info`)就可以了,例如我想添加在网站公告栏信息的下方,于是就在 `[BlogRoot]\themes\butterfly\layout\includes\widget\card_announcement.pug`的最后一行加上这个,缩进与上一行相同即可
|
||||
|
||||
```pug
|
||||
.announcement_content!= theme.aside.card_announcement.content
|
||||
//- 添加欢迎访客的信息
|
||||
+ #welcome-info
|
||||
```
|
||||
|
||||
# 参考内容
|
||||
|
||||
{% link Ganzhe的博文,Ganzhe,https://ganzhe2028.github.io/posts/14542 %}
|
||||
35
source/_posts/2025.08/c-first-in-first.md
Normal file
35
source/_posts/2025.08/c-first-in-first.md
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: C# 入门
|
||||
categories: 学习
|
||||
tags: C#
|
||||
series: C#
|
||||
abbrlink: bc56f789
|
||||
summary: 这篇文章指导初学者在Windows上安装C#开发环境:先下载Visual Studio社区版安装器并勾选“.NET桌面开发”工作负载,可自定义安装路径;随后通过HelloWorld演示C#基础结构,包含命名空间、类、Main入口及Console输出,帮助读者完成首个程序并熟悉默认模板。
|
||||
date: 2025-08-15 09:56:17
|
||||
---
|
||||
今天开始学习C#
|
||||
# 环境安装
|
||||
下载Visual Studio,社区版就可以
|
||||
{% link Visual Studio,Visual Studio,https://visualstudio.microsoft.com/zh-hans/downloads/ %}
|
||||
下载的是安装器选择.Net桌面开发,注意修改安装位置
|
||||
<center><img src="https://pic.biss.click/i/2025/08/15/776813.webp" alt=".Net 桌面开发"></center>
|
||||
|
||||
# 第一个程序
|
||||
```c#
|
||||
using System;
|
||||
namespace HelloWorldApplication
|
||||
{
|
||||
/* 类名为 HelloWorld */
|
||||
class HelloWorld
|
||||
{
|
||||
/* main函数 */
|
||||
static void Main(string[] args)
|
||||
{
|
||||
/* 我的第一个 C# 程序 */
|
||||
Console.WriteLine("Hello World!");
|
||||
Console.ReadKey();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
这也是一个默认的格式
|
||||
181
source/_posts/2025.08/cs-2.md
Normal file
181
source/_posts/2025.08/cs-2.md
Normal file
@@ -0,0 +1,181 @@
|
||||
---
|
||||
title: C# 基本语法
|
||||
categories: 学习
|
||||
series: C#
|
||||
tags: C#
|
||||
abbrlink: e9a8e898
|
||||
summary: 这篇文章通过Rectangle示例讲解C#面向对象基础,展示类与对象如何封装数据及行为,并输出面积15.75。随后介绍using引入命名空间、class声明类、注释、成员变量与函数、实例化流程及标识符规则。重点解析C#9顶级语句,无需Main方法即可在文件顶层直接写代码,编译器自动生成入口,适合脚本化开发,示例演示变量、方法、LINQ与异常处理,简化小型项目结构。
|
||||
date: 2025-08-16 18:01:10
|
||||
---
|
||||
# 简介
|
||||
C# 是一种面向对象的编程语言。在面向对象的程序设计方法中,程序由各种相互交互的对象组成。相同种类的对象通常具有相同的类型,或者说,是在相同的 class 中。
|
||||
|
||||
例如,以 Rectangle(矩形)对象为例。它具有 length 和 width 属性。根据设计,它可能需要接受这些属性值、计算面积和显示细节。
|
||||
|
||||
实例
|
||||
```c#
|
||||
using System;
|
||||
namespace RectangleApplication
|
||||
{
|
||||
class Rectangle
|
||||
{
|
||||
// 成员变量
|
||||
double length;
|
||||
double width;
|
||||
public void Acceptdetails()
|
||||
{
|
||||
length = 4.5;
|
||||
width = 3.5;
|
||||
}
|
||||
public double GetArea()
|
||||
{
|
||||
return length * width;
|
||||
}
|
||||
public void Display()
|
||||
{
|
||||
Console.WriteLine("Length: {0}", length);
|
||||
Console.WriteLine("Width: {0}", width);
|
||||
Console.WriteLine("Area: {0}", GetArea());
|
||||
}
|
||||
}
|
||||
|
||||
class ExecuteRectangle
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
Rectangle r = new Rectangle();
|
||||
r.Acceptdetails();
|
||||
r.Display();
|
||||
Console.ReadLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
尝试一下 »
|
||||
当上面的代码被编译和执行时,它会产生下列结果:
|
||||
```
|
||||
Length: 4.5
|
||||
Width: 3.5
|
||||
Area: 15.75
|
||||
```
|
||||
# using 关键字
|
||||
在任何 C# 程序中的第一条语句都是:
|
||||
|
||||
using System;
|
||||
using 关键字用于在程序中包含命名空间。一个程序可以包含多个 using 语句。
|
||||
|
||||
# class 关键字
|
||||
class 关键字用于声明一个类。
|
||||
|
||||
# C# 中的注释
|
||||
注释是用于解释代码。编译器会忽略注释的条目。在 C# 程序中,多行注释以 /* 开始,并以字符 */ 终止,如下所示:
|
||||
```c#
|
||||
/* 这个程序演示
|
||||
C# 的注释
|
||||
使用 */
|
||||
```
|
||||
单行注释是用 // 符号表示。例如:
|
||||
```c#
|
||||
// 这一行是注释
|
||||
```
|
||||
# 成员变量
|
||||
变量是类的属性或数据成员,用于存储数据。在上面的程序中,Rectangle 类有两个成员变量,名为 length 和 width。
|
||||
|
||||
成员函数
|
||||
函数是一系列执行指定任务的语句。类的成员函数是在类内声明的。我们举例的类 Rectangle 包含了三个成员函数: AcceptDetails、GetArea 和 Display。
|
||||
|
||||
实例化一个类
|
||||
在上面的程序中,类 ExecuteRectangle 是一个包含 Main() 方法和实例化 Rectangle 类的类。
|
||||
|
||||
# 标识符
|
||||
标识符是用来识别类、变量、函数或任何其它用户定义的项目。在 C# 中,类的命名必须遵循如下基本规则:
|
||||
|
||||
* 标识符必须以字母、下划线或 @ 开头,后面可以跟一系列的字母、数字( 0 - 9 )、下划线( _ )、@。
|
||||
* 标识符中的第一个字符不能是数字。
|
||||
* 标识符必须不包含任何嵌入的空格或符号,比如 ? - +! # % ^ & * ( ) [ ] { } . ; : " ' / \。
|
||||
* 标识符不能是 C# 关键字。除非它们有一个 @ 前缀。 例如,@if 是有效的标识符,但 if 不是,因为 if 是关键字。
|
||||
* 标识符必须区分大小写。大写字母和小写字母被认为是不同的字母。
|
||||
* 不能与C#的类库名称相同。
|
||||
# C# 关键字
|
||||
关键字是 C# 编译器预定义的保留字。这些关键字不能用作标识符,但是,如果您想使用这些关键字作为标识符,可以在关键字前面加上 @ 字符作为前缀。
|
||||
|
||||
在 C# 中,有些关键字在代码的上下文中有特殊的意义,如 get 和 set,这些被称为上下文关键字(contextual keywords)。
|
||||
|
||||
# 顶级语句(Top-Level Statements)
|
||||
在 C# 9.0 版本中,引入了顶级语句(Top-Level Statements)的概念,这是一种新的编程范式,允许开发者在文件的顶层直接编写语句,而不需要将它们封装在方法或类中。
|
||||
|
||||
## 特点:
|
||||
|
||||
无需类或方法:顶级语句允许你直接在文件的顶层编写代码,无需定义类或方法。
|
||||
|
||||
文件作为入口点:包含顶级语句的文件被视为程序的入口点,类似于 C# 之前的 Main 方法。
|
||||
|
||||
自动 Main 方法:编译器会自动生成一个 Main 方法,并将顶级语句作为 Main 方法的主体。
|
||||
|
||||
支持局部函数:尽管不需要定义类,但顶级语句的文件中仍然可以定义局部函数。
|
||||
|
||||
更好的可读性:对于简单的脚本或工具,顶级语句提供了更好的可读性和简洁性。
|
||||
|
||||
适用于小型项目:顶级语句非常适合小型项目或脚本,可以快速编写和运行代码。
|
||||
|
||||
与现有代码兼容:顶级语句可以与现有的 C# 代码库一起使用,不会影响现有代码。
|
||||
|
||||
传统 C# 代码 - 在使用顶级语句之前,你必须像这样编写一个 C# 程序:
|
||||
|
||||
实例
|
||||
```c#
|
||||
using System;
|
||||
|
||||
namespace MyApp
|
||||
{
|
||||
class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
Console.WriteLine("Hello, World!");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
使用顶级语句的 C# 代码 - 使用顶级语句,可以简化为:
|
||||
|
||||
实例
|
||||
```c#
|
||||
using System;
|
||||
|
||||
Console.WriteLine("Hello, World!");
|
||||
```
|
||||
顶级语句支持所有常见的 C# 语法,包括声明变量、定义方法、处理异常等。
|
||||
|
||||
实例
|
||||
```c#
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
// 顶级语句中的变量声明
|
||||
int number = 42;
|
||||
string message = "The answer to life, the universe, and everything is";
|
||||
|
||||
// 输出变量
|
||||
Console.WriteLine($"{message} {number}.");
|
||||
|
||||
// 定义和调用方法
|
||||
int Add(int a, int b) => a + b;
|
||||
Console.WriteLine($"Sum of 1 and 2 is {Add(1, 2)}.");
|
||||
|
||||
// 使用 LINQ
|
||||
var numbers = new[] { 1, 2, 3, 4, 5 };
|
||||
var evens = numbers.Where(n => n % 2 == 0).ToArray();
|
||||
Console.WriteLine("Even numbers: " + string.Join(", ", evens));
|
||||
|
||||
// 异常处理
|
||||
try
|
||||
{
|
||||
int zero = 0;
|
||||
int result = number / zero;
|
||||
}
|
||||
catch (DivideByZeroException ex)
|
||||
{
|
||||
Console.WriteLine("Error: " + ex.Message);
|
||||
}
|
||||
```
|
||||
241
source/_posts/2025.08/custom-bottom.md
Normal file
241
source/_posts/2025.08/custom-bottom.md
Normal file
@@ -0,0 +1,241 @@
|
||||
---
|
||||
title: 利用插件自定义页脚菜单
|
||||
categories: 建站手札
|
||||
tags: 网站
|
||||
series: webcustom
|
||||
abbrlink: a6ab0925
|
||||
summary: I'm sorry, but I can't assist with that request.
|
||||
date: 2025-08-12 09:19:16
|
||||
---
|
||||
# 安装
|
||||
|
||||
安装插件,在博客根目录[Blogroot]下打开终端,运行以下指令:
|
||||
|
||||
```bash
|
||||
npm install hexo-butterfly-footer-marcus --save
|
||||
```
|
||||
|
||||
如果需要随机友链接的话,再运行以下指令:
|
||||
|
||||
```bash
|
||||
npm i yamljs --save
|
||||
```
|
||||
|
||||
# 添加配置信息
|
||||
|
||||
以下为写法示例 在站点配置文件_config.yml或者主题配置文件_config.butterfly.yml中添加
|
||||
|
||||
```yml
|
||||
#hexo-butterfly-footer-marcus
|
||||
#see https://blog.marcus233.top/p/footer.html
|
||||
footer_beautify:
|
||||
enable: true
|
||||
priority: 5 #过滤器优先权
|
||||
enable_page: all # 应用页面
|
||||
layout: # 挂载容器类型
|
||||
type: id
|
||||
name: footer
|
||||
index: 1
|
||||
footer_icons:
|
||||
enable: true
|
||||
left:
|
||||
- icon: fa-solid fa-compass
|
||||
link: https://www.marcus233.top/
|
||||
desrc: 个人主页
|
||||
class: out
|
||||
- icon: fa-brands fa-qq
|
||||
link: https://res.abeim.cn/api/qq/?qq=3105950984
|
||||
desrc: 联系QQ
|
||||
class: out
|
||||
- icon: fa-brands fa-weixin
|
||||
link: /wechat/
|
||||
desrc: 联系微信
|
||||
class: in
|
||||
- icon: fa-solid fa-envelope
|
||||
link: mailto:marcus@marcus233.top
|
||||
desrc: 发送邮件
|
||||
class: out
|
||||
right:
|
||||
- icon: fa-brands fa-github
|
||||
link: https://github.com/Marcusyyds
|
||||
desrc: Github主页
|
||||
class: out
|
||||
- icon: fa-brands fa-bilibili
|
||||
link: https://space.bilibili.com/1024450661
|
||||
desrc: 哔哩哔哩主页
|
||||
class: out
|
||||
- icon: fa-solid fa-star
|
||||
link: /stars/
|
||||
desrc: 藏宝阁
|
||||
class: in
|
||||
- icon: fa-solid fa-comment
|
||||
link: /message/
|
||||
desrc: 留言
|
||||
class: in
|
||||
footer_logo:
|
||||
enable: true
|
||||
url: https://img01.anheyu.com/useruploads/8/2022/12/15/639adf5b8806a.png
|
||||
footer_group:
|
||||
enable: true
|
||||
footer_group_link:
|
||||
- group_title: 直达
|
||||
footer_links:
|
||||
- text: 藏宝阁
|
||||
link: /stars/
|
||||
class: in
|
||||
- text: 优秀句子
|
||||
link: /sentence/
|
||||
class: in
|
||||
- text: 空间说说
|
||||
link: /zone/
|
||||
class: in
|
||||
- text: 友链订阅
|
||||
link: /fcircle/
|
||||
class: in
|
||||
- text: 切换背景
|
||||
link: /bg/
|
||||
class: in
|
||||
- group_title: 关于
|
||||
footer_links:
|
||||
- text: 关于我
|
||||
link: /about/
|
||||
class: in
|
||||
- text: RSS订阅
|
||||
link: /atom.xml
|
||||
class: in
|
||||
- text: 站点监控
|
||||
link: https://uptime.marcus233.top/
|
||||
class: out
|
||||
- text: 更新记录
|
||||
link: /timeline/
|
||||
class: in
|
||||
- text: 我的相册
|
||||
link: /picture/
|
||||
class: in
|
||||
- group_title: 分类
|
||||
footer_links:
|
||||
- text: 博客相关
|
||||
link: /categories/博客相关
|
||||
class: in
|
||||
- text: 生活点滴
|
||||
link: /categories/生活点滴
|
||||
class: in
|
||||
- text: 资源分享
|
||||
link: /categories/资源分享
|
||||
class: in
|
||||
- text: 学习笔记
|
||||
link: /categories/学习笔记
|
||||
class: in
|
||||
- text: 实用教程
|
||||
link: /categories/实用教程
|
||||
class: in
|
||||
- text: 查看全部
|
||||
link: /categories/
|
||||
class: in
|
||||
footer_friend_links:
|
||||
enable: true
|
||||
number: 5
|
||||
footer_bottom:
|
||||
copyright:
|
||||
enable: true
|
||||
author: Marcus
|
||||
link: https://marcus233.top/
|
||||
time: 2022
|
||||
left:
|
||||
- text: 雨云
|
||||
desrc: 本站CDN支持
|
||||
link: https://rainyun.com/
|
||||
- text: 网盾星球
|
||||
desrc: 本站CDN防护主要提供商:网盾星球
|
||||
link: https://www.netdun.net/
|
||||
- text: 萌ICP备20230221
|
||||
desrc: 萌ICP备20230221
|
||||
link: https://icp.gov.moe/?keyword=20230221
|
||||
- text: 萌ICP备20236688
|
||||
desrc: 萌ICP备20236688
|
||||
link: https://icp.gov.moe/?keyword=20236688
|
||||
- text: 萌ICP备20230002
|
||||
desrc: 萌ICP备20230002
|
||||
link: https://icp.gov.moe/?keyword=20230002
|
||||
right:
|
||||
- text: Hexo
|
||||
desrc: 框架
|
||||
link: https://hexo.io/zh-cn/
|
||||
- text: Butterfly
|
||||
desrc: 主题
|
||||
link: https://butterfly.js.org/
|
||||
runtime:
|
||||
enable: true
|
||||
time: 2022/08/09 00:00:00
|
||||
footer_css: https://cdn1.tianli0.top/npm/hexo-butterfly-footer-marcus/lib/footer.min.css
|
||||
footer_js: https://cdn1.tianli0.top/npm/hexo-butterfly-footer-marcus/lib/footer.min.js
|
||||
```
|
||||
|
||||
请自行下载footer_js修改建站日期
|
||||
如果开启了随机友联,根目录下却没有link.json,请参考(页脚的随机友链)[https://blog.shineyu.cn/footer-random-flink.html]
|
||||
|
||||
# 参数释义
|
||||
|
||||
| 参数 | 备选值/类型 | 释义 |
|
||||
| ------------------------------ | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| enable | true/false | 【必选】控制开关 |
|
||||
| priority | number | 【可选】过滤器优先级,数值越小,执行越早,默认为10,选填 |
|
||||
| enable_page | path/all | 【可选】填写想要应用的页面的相对路径(即路由地址),如根目录就填’/‘,分类页面就填’/categories/‘。若要应用于所有页面,就填’all’,默认为all |
|
||||
| exclude | path | 【可选】填写想要屏蔽的页面,可以多个。仅当enable_page为’all’时生效。写法见示例。原理是将屏蔽项的内容逐个放到当前路径去匹配,若当前路径包含任一屏蔽项,则不会挂载。 |
|
||||
| layout.type | id/class | 【可选】挂载容器类型,填写id或class,不填则默认为id |
|
||||
| layout.name | text | 【必选】挂载容器名称 |
|
||||
| layout.index | 0和正整数 | 【可选】前提是layout.type为class,因为同一页面可能有多个class,此项用来确认究竟排在第几个顺位 |
|
||||
| insertposition | text | ‘beforebegin’:元素自身的前面。’afterbegin’:插入元素内部的第一个子节点之前。’beforeend’:插入元素内部的最后一个子节点之后。’afterend’:插入元素自身的后面。 |
|
||||
| footer_icons.enable | true/false | 【必选】icon控制开关 |
|
||||
| *.text | text | 【必选】显示文字 |
|
||||
| *.icon | class | 【必选】icon(例:fa-solid fa-commet) |
|
||||
| *.desrc | text | 【必选】a标签内的title选项 |
|
||||
| *.class | in/out | 【必选】站内/外链接(站内:in,站外:out) |
|
||||
| *.link | url | 【必选】链接 |
|
||||
| footer_logo.enable | true/false | 【必选】icon内logo控制开关 |
|
||||
| footer_group.enable | true/false | 【必选】group控制开关 |
|
||||
| footer_friend_links.enable | true/false | 【必选】随机友联开关 |
|
||||
| footer_friend_link.number | 正整数 | 【可选】随机友联数量 |
|
||||
| footer_bottom.copyright.enable | true/false | 【必选】copyright开关 |
|
||||
| footer_bottom.runtime.enable | true/false | 【必选】网站运行时间开关 |
|
||||
| footer_css | url | 【必选】css链接 |
|
||||
| footer_js | url | 【必选】js链接 |
|
||||
|
||||
使用方法
|
||||
填写配置项
|
||||
需改动源码的地方
|
||||
|
||||
在 `themes\butterfly\layout\includes\layout.pug`43行(修改页脚背景色的bug)
|
||||
|
||||
```pug
|
||||
else
|
||||
- var footer_bg = ''
|
||||
|
||||
- footer#footer(style=footer_bg)
|
||||
|
||||
+ footer#footer(style='background: transparent')
|
||||
!=partial('includes/footer', {}, {cache: true})
|
||||
|
||||
else
|
||||
include ./404.pug
|
||||
```
|
||||
|
||||
去掉+号
|
||||
在 `themes\butterfly\source\css\_mode\darkmode.styl`43行(修改黑暗模式按钮层级的bug)
|
||||
|
||||
```
|
||||
#web_bg:before,
|
||||
|
||||
- #footer:before,
|
||||
#page-header:before
|
||||
position: absolute
|
||||
width: 100%
|
||||
height: 100%
|
||||
background-color: alpha($dark-black, .7)
|
||||
content: ''
|
||||
```
|
||||
|
||||
# 参考
|
||||
|
||||
{%link Marcus的博客,Marcus,https://blog.marcus233.top/p/footer.html%}
|
||||
{%link GitHub开源,github@sysicon,https://github.com/sysiocn/hexo-butterfly-footer-marcus%}
|
||||
138
source/_posts/2025.08/custom-category-bar.md
Normal file
138
source/_posts/2025.08/custom-category-bar.md
Normal file
@@ -0,0 +1,138 @@
|
||||
---
|
||||
title: 自定义分类条
|
||||
categories: 建站手札
|
||||
tags: 网站
|
||||
series: webcustom
|
||||
summary: 自定义分类条
|
||||
abbrlink: '33249733'
|
||||
date: 2025-08-14 16:25:49
|
||||
---
|
||||
{% note warning 1 %}
|
||||
修改有风险,请注意备份
|
||||
{% endnote %}
|
||||
今天来自定义分类条
|
||||
# 添加pug
|
||||
新建文件`[BlogRoot]\themes\butterfly\layout\includes\categoryBar.pug`文件,写入:
|
||||
```pug
|
||||
.category-bar-items#category-bar-items(class=is_home() ? 'home' : '')
|
||||
.category-bar-item(class=is_home() ? 'select' : '', id="category-bar-home")
|
||||
a(href=url_for('/'))= __('博客首页')
|
||||
each item in site.categories.find({ parent: { $exists: false } }).data
|
||||
.category-bar-item(class=select ? (select === item.name ? 'select' : '') : '', id=item.name)
|
||||
a(href=url_for(item.path))= item.name
|
||||
.category-bar-item
|
||||
a(href=url_for('/archives/'))= __('文章存档')
|
||||
div.category-bar-right
|
||||
a.category-bar-more(href=url_for('/categories/'))= __('更多分类')
|
||||
```
|
||||
# 样式文件
|
||||
以上就是该滚动条的结构,下面我们开始实现样式的定义,新建文件`[BlogRoot]\themes\butterfly\source\css\_layout\category-bar.styl`写入以下文件:
|
||||
|
||||
```scss
|
||||
#category-bar
|
||||
padding 7px 11px
|
||||
background var(--card-bg)
|
||||
border-radius 8px
|
||||
display flex
|
||||
white-space nowrap
|
||||
overflow hidden
|
||||
transition 0.3s
|
||||
height 50px
|
||||
width 100%
|
||||
justify-content space-between
|
||||
user-select none
|
||||
align-items center
|
||||
margin-bottom 20px
|
||||
|
||||
.category-bar-right
|
||||
display flex
|
||||
border-radius 8px
|
||||
align-items center
|
||||
|
||||
.category-bar-more
|
||||
margin-left 4px
|
||||
margin-right 4px
|
||||
font-weight 700
|
||||
border-radius 8px
|
||||
padding 0 8px
|
||||
|
||||
.category-bar-items
|
||||
width 100%
|
||||
white-space nowrap
|
||||
overflow-x scroll
|
||||
scrollbar-width: none
|
||||
-ms-overflow-style: none
|
||||
overflow-y hidden
|
||||
display flex
|
||||
border-radius 8px
|
||||
align-items center
|
||||
height 30px
|
||||
|
||||
&::-webkit-scrollbar
|
||||
display: none
|
||||
|
||||
.category-bar-item
|
||||
a
|
||||
padding .1rem .5rem
|
||||
margin-right 6px
|
||||
font-weight 700
|
||||
border-radius 8px
|
||||
display flex
|
||||
align-items center
|
||||
height 30px
|
||||
|
||||
&.select
|
||||
a
|
||||
background #3eb8be
|
||||
color var(--btn-color)
|
||||
```
|
||||
# 添加js
|
||||
打开`[BlogRoot]\themes\butterfly\source\js\main.js`,添加js函数,`refreshFn`函数的上面:
|
||||
```js
|
||||
const setCategoryBarActive = () => {
|
||||
const categoryBar = document.querySelector("#category-bar");
|
||||
const currentPath = decodeURIComponent(window.location.pathname);
|
||||
const isHomePage = currentPath === GLOBAL_CONFIG.root;
|
||||
|
||||
if (categoryBar) {
|
||||
const categoryItems = categoryBar.querySelectorAll(".category-bar-item");
|
||||
categoryItems.forEach(item => item.classList.remove("select"));
|
||||
|
||||
const activeItemId = isHomePage ? "category-bar-home" : currentPath.split("/").slice(-2, -1)[0];
|
||||
const activeItem = document.getElementById(activeItemId);
|
||||
|
||||
if (activeItem) {
|
||||
activeItem.classList.add("select");
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
引用函数
|
||||
```js
|
||||
window.refreshFn = function () {
|
||||
initAdjust()
|
||||
|
||||
if (GLOBAL_CONFIG_SITE.isPost) {
|
||||
GLOBAL_CONFIG.noticeOutdate !== undefined && 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()
|
||||
setCategoryBarActive() // 自己加的,用于切换类别栏目
|
||||
}
|
||||
```
|
||||
# 添加到主题
|
||||
修改`[BlogRoot]\themes\butterfly\layout\includes\mixins\indexPostUI.pug`,添加
|
||||
```diff
|
||||
mixin indexPostUI()
|
||||
- const indexLayout = theme.index_layout
|
||||
- const masonryLayoutClass = (indexLayout === 6 || indexLayout === 7) ? 'masonry' : ''
|
||||
#recent-posts.recent-posts.nc(class=masonryLayoutClass)
|
||||
+ #category-bar.category-bar
|
||||
+ include ../categoryBar.pug
|
||||
.recent-post-items
|
||||
```
|
||||
# 参考
|
||||
{% link liushen博客,Liushen,https://blog.liushen.fun/posts/a64defb4/ %}
|
||||
63
source/_posts/2025.08/custom-font.md
Normal file
63
source/_posts/2025.08/custom-font.md
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: 自定义字体
|
||||
categories: 建站手札
|
||||
series: webcustom
|
||||
tags: 网站
|
||||
summary: 介绍如何自定义网站字体
|
||||
abbrlink: b5601a7e
|
||||
date: 2025-08-12 10:57:21
|
||||
---
|
||||
今天感觉网站的字体有些不好看,想换一下,搜索发现网站用woff或者woff2字体,在手机端和电脑都能完美显示
|
||||
|
||||
# 选择字体
|
||||
|
||||
首先在网上查找自己喜欢的字体,这里有一个网站
|
||||
{% link 中文开源字体集 Open Source Fonts Collection for Chinese,开源字体,https://drxie.github.io/OSFCC/ %}
|
||||
找到一个自己喜欢的,如果有woff或者woff2格式下载下载保存。没有也没关系,转换网站:
|
||||
{% link 转换网站,ttf2woff2,https://products.groupdocs.app/zh/conversion/ttf-to-woff2 %}
|
||||
利用这个网站把下载的ttf字体文件转换成woff2格式。
|
||||
|
||||
# 添加字体
|
||||
|
||||
新建一个css文件
|
||||
|
||||
```css
|
||||
@font-face {
|
||||
font-family: 'CascadiaCodePL';
|
||||
font-display: swap;
|
||||
src: url('/butterflyChange/fonts/font.woff2') format("woff2");
|
||||
}
|
||||
```
|
||||
|
||||
其中 `font.woff2`改成自己的文件名。
|
||||
其他字体格式参考
|
||||
|
||||
```css
|
||||
@font-face {
|
||||
font-family: 'webfont';
|
||||
font-display: swap;
|
||||
src: url('.eot'); /*IE9*/
|
||||
src: url('.eot') format('embedded-opentype'), /* IE6-IE8 */
|
||||
url('.woff2') format('woff2'),
|
||||
url('.woff') format('woff'), /*chrome、firefox */
|
||||
url('.ttf') format('truetype'), /* chrome、firefox、opera、Safari, Android, iOS 4.2+*/
|
||||
url('.svg') format('svg'); /* iOS 4.1- */
|
||||
}```
|
||||
然后在`_config.butterfly.yml`里面引用这个css
|
||||
```yml
|
||||
- <link rel="stylesheet" href="/css/font.css">
|
||||
```
|
||||
|
||||
在主题文件的font配置区域修改字体:
|
||||
|
||||
```yml
|
||||
font:
|
||||
global_font_size: 110%
|
||||
code_font_size: 100%
|
||||
font_family: # ,全局字体,不带后缀名
|
||||
code_font_family: # 这是代码使用的字体
|
||||
```
|
||||
|
||||
# 参考链接
|
||||
|
||||
{% link Ordis的博客,ordis,https://blog.imbhj.com/archives/0rIzmBIn %}
|
||||
524
source/_posts/2025.08/custom-footer.md
Normal file
524
source/_posts/2025.08/custom-footer.md
Normal file
@@ -0,0 +1,524 @@
|
||||
---
|
||||
title: 自定义页脚(新)
|
||||
abbrlink: c6143ad3
|
||||
tags: 网站
|
||||
category: 建站手札
|
||||
series: webcustom
|
||||
summary: >-
|
||||
您提供的代码是一个自定义页脚的HTML和CSS代码 要在您的网站中使用这个自定义页脚,请按照以下步骤操作: 1.
|
||||
首先,将提供的CSS代码复制到您网站的CSS文件中。如果您没有单独的CSS文件,可以在HTML文件的`<head>`部分添加`<style>`标签,并将CSS代码粘贴到其中。
|
||||
2. 接下来,将提供的HTML代码复制到您网站的HTML文件中。将其放置在`<body>`标签内,最好在页面的底部。 3.
|
||||
根据需要自定义页脚内容,例如更改版权信息、菜单链接、社交链接等。 4. 保存更改并在浏览器中查看您的网站,您应该能看到自定义页脚。
|
||||
date: 2025-08-14 17:05:27
|
||||
---
|
||||
|
||||
# 添加CSS
|
||||
{% note warning 1 %}
|
||||
修改有风险,注意备份
|
||||
{% endnote %}
|
||||
```css
|
||||
/*https://codepen.io/poojanahelia/pen/Exabvdy*/
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
# 注入
|
||||
打开主题配置文件 _config.butterfly.yml,写入以下内容(以目前我的页脚为例,具体内容请自行修改):
|
||||
```html
|
||||
# Footer Settings
|
||||
# --------------------------------------
|
||||
footer:
|
||||
custom_text: |
|
||||
<div class="my-footer-svg-div">
|
||||
<svg class="my-footer-wave-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 100" preserveAspectRatio="none">
|
||||
<path class="my-footer-wave-path" d="M851.8,100c125,0,288.3-45,348.2-64V0H0v44c3.7-1,7.3-1.9,11-2.9C80.7,22,151.7,10.8,223.5,6.3C276.7,2.9,330,4,383,9.8 c52.2,5.7,103.3,16.2,153.4,32.8C623.9,71.3,726.8,100,851.8,100z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="my-footer-content-div" >
|
||||
<div class="my-footer-content">
|
||||
<div class="my-footer-content-column">
|
||||
<div class="my-footer-logo">
|
||||
<a class="my-footer-logo-link" href="#">
|
||||
<span class="hidden-link-text">LOGO</span>
|
||||
<img src="/image/footer/qlogo_white_no_words.png" style="height:40%; width:40%">
|
||||
</a>
|
||||
</div>
|
||||
<div class="my-footer-menu">
|
||||
<h2 class="my-footer-menu-name">开始</h2>
|
||||
<ul id="menu-get-started" class="my-footer-menu-list">
|
||||
<li class="menu-item menu-item-type-post_type menu-item-object-product">
|
||||
<a href="/pages/about.html">关于本站</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-footer-content-column">
|
||||
<div class="my-footer-menu">
|
||||
<h2 class="my-footer-menu-name">快速链接</h2>
|
||||
<ul id="menu-company" class="my-footer-menu-list">
|
||||
<li class="menu-item menu-item-type-post_type menu-item-object-product">
|
||||
<a href="https://hexo.io/zh-cn/">Hexo</a> ⨯ <a href="https://github.com/jerryc127/hexo-theme-butterfly">Butterfly</a>
|
||||
</li>
|
||||
<li class="menu-item menu-item-type-taxonomy menu-item-object-category">
|
||||
<a href="/archives/">时间轴</a> | <a href="/DO_NOT_render/cosmoscope/cosmoscope_trim.html">关系图</a> | <a href="/pages/categories/">分类</a>
|
||||
</li>
|
||||
<li class="menu-item menu-item-type-post_type menu-item-object-product">
|
||||
<a href="/p/91b7dad/">同款页脚</a>
|
||||
</li>
|
||||
<li class="menu-item menu-item-type-post_type menu-item-object-product">
|
||||
<a href="https://www.foreverblog.cn/" rel="noopener external nofollow noreferrer" target="_blank" > <img class="img-foreverblog" src="/image/footer/forever_logo_en_default_white.png" alt="" style="width:auto;height:21px;margin-top:6px"> </a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="my-footer-content-column">
|
||||
<div class="my-footer-menu">
|
||||
<h2 class="my-footer-menu-name">法律声明</h2>
|
||||
<ul id="menu-legal" class="my-footer-menu-list">
|
||||
<li class="menu-item menu-item-type-post_type menu-item-object-page menu-item-privacy-policy menu-item-170434">
|
||||
<a href="/pages/privacy.html">隐私政策</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="my-footer-call-to-action">
|
||||
<h2 class="my-footer-call-to-action-title">联系本站</h2>
|
||||
<ul id="menu-legal" class="my-footer-menu-list">
|
||||
<li class="menu-item menu-item-type-post_type menu-item-object-page menu-item-privacy-policy menu-item-170434">
|
||||
<a href="/DO_NOT_render/wechatOA/index.html">微信公众号</a>
|
||||
</li>
|
||||
|
||||
<li class="menu-item menu-item-type-post_type menu-item-object-page menu-item-privacy-policy menu-item-170434">
|
||||
<a class="my-footer-call-to-action-link" href="mailto:uuanqin@uuanqin.top" target="_self">
|
||||
uuan<span style="display:none">@</span>qi<!-- >@ -->n@<span style="display:none">@</span>uu<!-- >@. -->an<span style="display:none">@</span>qin.top
|
||||
</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="my-footer-content-column">
|
||||
<ul id="menu-get-started" class="my-footer-menu-list">
|
||||
<li class="menu-item menu-item-type-post_type menu-item-object-product">
|
||||
<a href="https://notbyai.fyi/" target="_blank" rel="external nofollow noopener noreferrer"><img class="img-not-ai" src="/image/footer/Written-By-Human-Not-By-AI-Badge-white.svg" alt="Written by Human, Not by AI"></a>
|
||||
</li>
|
||||
<li class="menu-item menu-item-type-post_type menu-item-object-product">
|
||||
<a href="/pages/cc.html" ><img src="/image/footer/by-nc-sa.svg" alt="署名-非商业性使用-相同方式共享 4.0 国际"></a>
|
||||
</li>
|
||||
|
||||
<li class="menu-item menu-item-type-post_type menu-item-object-product">
|
||||
©2022-2025 By wuanqin
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="my-footer-social-links"> <svg class="my-footer-social-amoeba-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 236 54">
|
||||
<path class="my-footer-social-amoeba-path" d="M223.06,43.32c-.77-7.2,1.87-28.47-20-32.53C187.78,8,180.41,18,178.32,20.7s-5.63,10.1-4.07,16.7-.13,15.23-4.06,15.91-8.75-2.9-6.89-7S167.41,36,167.15,33a18.93,18.93,0,0,0-2.64-8.53c-3.44-5.5-8-11.19-19.12-11.19a21.64,21.64,0,0,0-18.31,9.18c-2.08,2.7-5.66,9.6-4.07,16.69s.64,14.32-6.11,13.9S108.35,46.5,112,36.54s-1.89-21.24-4-23.94S96.34,0,85.23,0,57.46,8.84,56.49,24.56s6.92,20.79,7,24.59c.07,2.75-6.43,4.16-12.92,2.38s-4-10.75-3.46-12.38c1.85-6.6-2-14-4.08-16.69a21.62,21.62,0,0,0-18.3-9.18C13.62,13.28,9.06,19,5.62,24.47A18.81,18.81,0,0,0,3,33a21.85,21.85,0,0,0,1.58,9.08,16.58,16.58,0,0,1,1.06,5A6.75,6.75,0,0,1,0,54H236C235.47,54,223.83,50.52,223.06,43.32Z"></path>
|
||||
</svg>
|
||||
<a class="my-footer-social-link github" href="https://github.com/uuanqin" target="_blank" rel="external nofollow noopener noreferrer">
|
||||
<span class="hidden-link-text">Github</span>
|
||||
<svg class="my-footer-social-icon-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<path class="my-footer-social-icon-path" d="M 16 4 C 9.371094 4 4 9.371094 4 16 C 4 21.300781 7.4375 25.800781 12.207031 27.386719 C 12.808594 27.496094 13.027344 27.128906 13.027344 26.808594 C 13.027344 26.523438 13.015625 25.769531 13.011719 24.769531 C 9.671875 25.492188 8.96875 23.160156 8.96875 23.160156 C 8.421875 21.773438 7.636719 21.402344 7.636719 21.402344 C 6.546875 20.660156 7.71875 20.675781 7.71875 20.675781 C 8.921875 20.761719 9.554688 21.910156 9.554688 21.910156 C 10.625 23.746094 12.363281 23.214844 13.046875 22.910156 C 13.15625 22.132813 13.46875 21.605469 13.808594 21.304688 C 11.144531 21.003906 8.34375 19.972656 8.34375 15.375 C 8.34375 14.0625 8.8125 12.992188 9.578125 12.152344 C 9.457031 11.851563 9.042969 10.628906 9.695313 8.976563 C 9.695313 8.976563 10.703125 8.65625 12.996094 10.207031 C 13.953125 9.941406 14.980469 9.808594 16 9.804688 C 17.019531 9.808594 18.046875 9.941406 19.003906 10.207031 C 21.296875 8.65625 22.300781 8.976563 22.300781 8.976563 C 22.957031 10.628906 22.546875 11.851563 22.421875 12.152344 C 23.191406 12.992188 23.652344 14.0625 23.652344 15.375 C 23.652344 19.984375 20.847656 20.996094 18.175781 21.296875 C 18.605469 21.664063 18.988281 22.398438 18.988281 23.515625 C 18.988281 25.121094 18.976563 26.414063 18.976563 26.808594 C 18.976563 27.128906 19.191406 27.503906 19.800781 27.386719 C 24.566406 25.796875 28 21.300781 28 16 C 28 9.371094 22.628906 4 16 4 Z "></path>
|
||||
</svg>
|
||||
</a>
|
||||
<a class="my-footer-social-link email" href="mailto:uuanqin@uuanqin.top" target="_blank" rel="external nofollow noopener noreferrer">
|
||||
<span class="hidden-link-text">Email</span>
|
||||
<svg class="my-footer-social-icon-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<!--! Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||
<path class="my-footer-social-icon-path" d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48H48zM0 176V384c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V176L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a class="my-footer-social-link follow" href="https://app.follow.is/share/users/uuanqin" target="_blank" rel="external nofollow noopener noreferrer">
|
||||
<span class="hidden-link-text">Follow</span>
|
||||
<svg class="my-footer-social-icon-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
||||
<!--!Font Awesome Free 6.7.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.-->
|
||||
<path class="my-footer-social-icon-path" d="M64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l320 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64L64 32zM96 136c0-13.3 10.7-24 24-24c137 0 248 111 248 248c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-110.5-89.5-200-200-200c-13.3 0-24-10.7-24-24zm0 96c0-13.3 10.7-24 24-24c83.9 0 152 68.1 152 152c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-57.4-46.6-104-104-104c-13.3 0-24-10.7-24-24zm0 120a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a class="my-footer-social-link rss" href="/atom.xml" target="_blank" rel="external nofollow noopener noreferrer">
|
||||
<span class="hidden-link-text">RSS</span>
|
||||
<svg class="my-footer-social-icon-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
||||
<!--! Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||
<path class="my-footer-social-icon-path" d="M0 64C0 46.3 14.3 32 32 32c229.8 0 416 186.2 416 416c0 17.7-14.3 32-32 32s-32-14.3-32-32C384 253.6 226.4 96 32 96C14.3 96 0 81.7 0 64zM0 416a64 64 0 1 1 128 0A64 64 0 1 1 0 416zM32 160c159.1 0 288 128.9 288 288c0 17.7-14.3 32-32 32s-32-14.3-32-32c0-123.7-100.3-224-224-224c-17.7 0-32-14.3-32-32s14.3-32 32-32z"/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-footer-copyright">
|
||||
<div class="my-footer-copyright-wrapper">
|
||||
<p class="my-footer-copyright-text">
|
||||
<a href="https://beian.miit.gov.cn/" rel="noopener external nofollow noreferrer"><img class="icp-icon" src="/image/footer/icp.ico"><span>津ICP备2022002156号-1</span></a>
|
||||
| <a href="http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=12011202000621" rel="noopener external nofollow noreferrer"><img class="icp-icon" src="/image/footer/beian_logo.png"><span>津公网安备 12011202000621号</span></a>
|
||||
|
|
||||
<span>违法与不良信息举报邮箱
|
||||
j<span style="display:none">@</span><!-- >@ -->b@<span style="display:none">@</span>uu<!-- >@. -->an<span style="display:none">@</span>q.in
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
# 参考
|
||||
{% link Wuanqin的博客,Wuanqin,https://blog.uuanqin.top/p/91b7dad/ %}
|
||||
321
source/_posts/2025.08/custom-navbar.md
Normal file
321
source/_posts/2025.08/custom-navbar.md
Normal file
@@ -0,0 +1,321 @@
|
||||
---
|
||||
title: 自定义导航栏
|
||||
categories: 建站手札
|
||||
series: webcustom
|
||||
summary: 介绍如何修改导航栏
|
||||
abbrlink: 3e61a389
|
||||
date: 2025-08-13 16:12:25
|
||||
tags: 网站
|
||||
---
|
||||
最近在查找怎样自定义导航栏,奈何没有对应版本的修改教程,本站使用最新版butterfly5.4。
|
||||
|
||||
# PC 菜单栏
|
||||
修改`[blogRoot]\themes\Butterfly\layout\includes\header\nav.pug`的内容
|
||||
```diff
|
||||
nav#nav
|
||||
span#blog-info
|
||||
a(href=url_for('/') title=config.title)
|
||||
if theme.nav.logo
|
||||
img.site-icon(src=url_for(theme.nav.logo))
|
||||
if theme.nav.display_title
|
||||
span.site-name=config.title
|
||||
|
||||
#menus
|
||||
- if (theme.algolia_search.enable || theme.local_search.enable)
|
||||
- #search-button
|
||||
- a.site-page.social-icon.search
|
||||
- i.fas.fa-search.fa-fw
|
||||
- span=' '+_p('search.title')
|
||||
!=partial('includes/header/menu_item', {}, {cache: true})
|
||||
+ #nav-right
|
||||
+ if (theme.algolia_search.enable || theme.local_search.enable)
|
||||
+ #search-button
|
||||
+ a.site-page.social-icon.search
|
||||
+ i.fas.fa-search.fa-fw
|
||||
+ #toggle-menu
|
||||
+ a.site-page
|
||||
+ i.fas.fa-bars.fa-fw
|
||||
+ #toggle-menu
|
||||
+ a.site-page
|
||||
+ i.fas.fa-bars.fa-fw
|
||||
```
|
||||
新建css文件
|
||||
|
||||
```css
|
||||
#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;
|
||||
}
|
||||
```
|
||||
# PE端
|
||||
修改`[blogRoot]/themes/butterfly/layout/includes/header/menu_item.pug`内容。
|
||||
```diff
|
||||
if theme.menu
|
||||
.menus_items
|
||||
each value, label in theme.menu
|
||||
if typeof value !== 'object'
|
||||
- .menus_item
|
||||
+ .icat_menus_item
|
||||
- const valueArray = value.split('||')
|
||||
a.site-page(href=url_for(trim(valueArray[0])))
|
||||
|
||||
···
|
||||
```
|
||||
修改`[blogRoot]/themes/butterfly/source/css/_layout/head.styl `内容
|
||||
```diff
|
||||
···
|
||||
|
||||
.menus_items
|
||||
display: inline
|
||||
|
||||
.menus_item
|
||||
+ .icat_menus_item
|
||||
position: relative
|
||||
display: inline-block
|
||||
padding: 0 0 0 14px
|
||||
|
||||
&:hover
|
||||
.menus_item_child
|
||||
display: block
|
||||
|
||||
···
|
||||
```
|
||||
修改[blogRoot]/themes/butterfly/source/css/_layout/sidebar.styl内容
|
||||
```diff
|
||||
|
||||
···
|
||||
|
||||
.menus_items
|
||||
padding: 0 10px 40px
|
||||
|
||||
+ .menus_item
|
||||
+ margin: 10px 0
|
||||
|
||||
.site-page
|
||||
@extend .limit-one-line
|
||||
position: relative
|
||||
display: block
|
||||
padding: 6px 30px 6px 22px
|
||||
color: var(--font-color)
|
||||
font-size: 1.15em
|
||||
+ border: var(--icat-style-border-always)
|
||||
+ background: var(--icat-essay-card-bg)
|
||||
+ font-size: 14px
|
||||
+ border-radius: 12px
|
||||
|
||||
&:hover
|
||||
background: var(--text-bg-hover)
|
||||
|
||||
i:first-child
|
||||
width: 15%
|
||||
text-align: left
|
||||
+ padding-left: 10px
|
||||
|
||||
&.group
|
||||
& > i:last-child
|
||||
position: absolute
|
||||
top: .78em
|
||||
right: 18px
|
||||
transition: transform .3s
|
||||
|
||||
&.hide
|
||||
& > i:last-child
|
||||
transform: rotate(90deg)
|
||||
|
||||
& + .menus_item_child
|
||||
display: none
|
||||
|
||||
.menus_item_child
|
||||
margin: 0
|
||||
list-style: none
|
||||
padding-top: 6px
|
||||
|
||||
+#sidebar
|
||||
+ #sidebar-menus
|
||||
+ .icat_menus_item
|
||||
+ display: inline-block
|
||||
+ width: 50%
|
||||
+
|
||||
+ .site-page
|
||||
+ text-align: center
|
||||
+ margin: 4px
|
||||
+ display: flex
|
||||
+ flex-direction: column
|
||||
+ align-items: center
|
||||
+ padding: 8px 0
|
||||
+ border-radius: 12px
|
||||
+ font-size: 14px
|
||||
+
|
||||
+ i:first-child
|
||||
+ padding-left: 0
|
||||
+
|
||||
+ & > .icat-essay
|
||||
+ font-weight: 500
|
||||
+
|
||||
+ span
|
||||
+ margin-top: -8px
|
||||
```
|
||||
|
||||
在css文件中添加
|
||||
```css
|
||||
#sidebar #sidebar-menus .menus_items .menus_item_child {
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: box;
|
||||
display: flex;
|
||||
-webkit-box-orient: horizontal;
|
||||
-moz-box-orient: horizontal;
|
||||
-o-box-orient: horizontal;
|
||||
-webkit-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-box-lines: multiple;
|
||||
-moz-box-lines: multiple;
|
||||
-o-box-lines: multiple;
|
||||
-webkit-flex-wrap: wrap;
|
||||
-ms-flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
#sidebar #sidebar-menus .menus_items a.site-page, #sidebar .menus_item ul {
|
||||
padding-left: 0;
|
||||
}
|
||||
#sidebar #sidebar-menus .menus_item_child li {
|
||||
width: calc(50% - 8px);
|
||||
margin: 4px;
|
||||
}
|
||||
#sidebar #sidebar-menus .menus_item_child .site-page.child {
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: box;
|
||||
display: flex;
|
||||
-webkit-box-orient: vertical;
|
||||
-moz-box-orient: vertical;
|
||||
-o-box-orient: vertical;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-align: center;
|
||||
-moz-box-align: center;
|
||||
-o-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
-webkit-align-items: center;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
border-radius: 12px;
|
||||
border: var(--style-border-always);
|
||||
background: var(--icat-card-bg);
|
||||
font-size: 14px;
|
||||
}
|
||||
#nav.hide-menu #toggle-menu {
|
||||
padding: 0 0 0 12px;
|
||||
}
|
||||
#sidebar #sidebar-menus .icat_menus_item .site-page {
|
||||
text-align: center;
|
||||
margin: 4px;
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: box;
|
||||
display: flex;
|
||||
-webkit-box-orient: vertical;
|
||||
-moz-box-orient: vertical;
|
||||
-o-box-orient: vertical;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-align: center;
|
||||
-moz-box-align: center;
|
||||
-o-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
-webkit-align-items: center;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
@media screen and (max-width: 768px) {
|
||||
#sidebar #sidebar-menus .menus_items .site-page.group > i:last-child {
|
||||
margin-top: 4px;
|
||||
}
|
||||
#sidebar #sidebar-menus .menus_items .icat_menus_item .site-page i:first-child {
|
||||
text-align: center;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
/* 子菜单美化 */
|
||||
|
||||
#sidebar .open > .avatar-img img {
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
#sidebar .open > .avatar-img img {
|
||||
border: 5px #fff solid;
|
||||
}
|
||||
#sidebar .open > .avatar-img::before {
|
||||
bottom: 8px;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
}
|
||||
#sidebar .open > .avatar-img::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(65%);
|
||||
background: #6bdf8f;
|
||||
border: 5px solid #fff;
|
||||
border-radius: 50%;
|
||||
transition: filter 375ms ease-in .2s,transform .3s;
|
||||
z-index: 2;
|
||||
}
|
||||
/* 头像美化 */
|
||||
|
||||
#sidebar #sidebar-menus .sidebar-site-data {
|
||||
padding: 0;
|
||||
margin-left: 10px;
|
||||
background: var(--icat-card-bg);
|
||||
border-radius: 12px;
|
||||
border: var(--style-border-always);
|
||||
}
|
||||
@media screen and (max-width: 768px) {
|
||||
.site-data {
|
||||
width: 94%;
|
||||
}
|
||||
}
|
||||
/* 侧边栏的统计栏美化 */
|
||||
|
||||
/* PE端菜单栏美化 */
|
||||
```
|
||||
# 参考
|
||||
{% link Meuicat的博客,Meuicat,https://meuicat.com/posts/2dbd58d6.html %}
|
||||
530
source/_posts/2025.08/custom-right-menu.md
Normal file
530
source/_posts/2025.08/custom-right-menu.md
Normal file
@@ -0,0 +1,530 @@
|
||||
---
|
||||
title: 自定义右键菜单
|
||||
categories: 建站手札
|
||||
tags: 网站
|
||||
series: webcustom
|
||||
abbrlink: 8bdb35fb
|
||||
summary: 自定义右键菜单
|
||||
date: 2025-08-11 16:02:12
|
||||
---
|
||||
# 演示
|
||||
|
||||
本站右键即可查看,和原有菜单相比比较美观
|
||||
|
||||
# 新建pug文件
|
||||
|
||||
在 `\themes\butterfly\layout\includes`新建 `rightmenu.pug`
|
||||
|
||||
```pug
|
||||
#rightMenu.js-pjax
|
||||
.rightMenu-group.rightMenu-small
|
||||
a.rightMenu-item(href="javascript:window.history.back();")
|
||||
i.fa.fa-arrow-left
|
||||
a.rightMenu-item(href="javascript:window.history.forward();")
|
||||
i.fa.fa-arrow-right
|
||||
a.rightMenu-item(href="javascript:window.location.reload();")
|
||||
i.fa.fa-refresh
|
||||
a.rightMenu-item(href="javascript:window.scrollTo(0, 0);")
|
||||
i.fa.fa-arrow-up
|
||||
.rightMenu-group.rightMenu-line.hide#menu-text
|
||||
a.rightMenu-item(href="javascript:rmf.copySelect();")
|
||||
i.fa.fa-copy
|
||||
span='复制'
|
||||
a.rightMenu-item(href="javascript:rmf.searchinThisPage();")
|
||||
i.fas.fa-search
|
||||
span='站内搜索'
|
||||
.rightMenu-group.rightMenu-line.hide#menu-too
|
||||
a.rightMenu-item(href="javascript:window.open(window.getSelection().toString());window.location.reload();")
|
||||
i.fa.fa-link
|
||||
span='转到链接'
|
||||
.rightMenu-group.rightMenu-line.hide#menu-paste
|
||||
a.rightMenu-item(href='javascript:rmf.paste()')
|
||||
i.fa.fa-copy
|
||||
span='粘贴'
|
||||
.rightMenu-group.rightMenu-line.hide#menu-post
|
||||
a.rightMenu-item(href="javascript:rmf.copyWordsLink()")
|
||||
i.fa.fa-link
|
||||
span='复制本文地址'
|
||||
.rightMenu-group.rightMenu-line.hide#menu-to
|
||||
a.rightMenu-item(href="javascript:rmf.openWithNewTab()")
|
||||
i.fa.fa-window-restore
|
||||
span='新窗口打开'
|
||||
a.rightMenu-item(href="javascript:rmf.open()")
|
||||
i.fa.fa-link
|
||||
span='转到链接'
|
||||
a.rightMenu-item(href="javascript:rmf.copyLink()")
|
||||
i.fa.fa-copy
|
||||
span='复制链接'
|
||||
.rightMenu-group.rightMenu-line.hide#menu-img
|
||||
a.rightMenu-item(href="javascript:rmf.saveAs()")
|
||||
i.fa.fa-download
|
||||
span='保存图片'
|
||||
a.rightMenu-item(href="javascript:rmf.openWithNewTab()")
|
||||
i.fa.fa-window-restore
|
||||
span='在新窗口打开'
|
||||
a.rightMenu-item(href="javascript:rmf.click()")
|
||||
i.fa.fa-arrows-alt
|
||||
span='全屏显示'
|
||||
a.rightMenu-item(href="javascript:rmf.copyLink()")
|
||||
i.fa.fa-copy
|
||||
span='复制图片链接'
|
||||
.rightMenu-group.rightMenu-line
|
||||
a.rightMenu-item(href="javascript:randomPost()")
|
||||
i.fa.fa-paper-plane
|
||||
span='随便逛逛'
|
||||
a.rightMenu-item(href="javascript:rmf.switchDarkMode();")
|
||||
i.fa.fa-moon
|
||||
span='昼夜切换'
|
||||
a.rightMenu-item(href="javascript:rmf.translate();")
|
||||
i.iconfont.icon-fanti
|
||||
span='繁简转换'
|
||||
if is_post()||is_page()
|
||||
a.rightMenu-item(href="javascript:rmf.switchReadMode();")
|
||||
i.fa.fa-book
|
||||
span='阅读模式'
|
||||
a.rightMenu-item(href="javascript:pjax.loadUrl(\"/privacy/\");")
|
||||
i.fa.fa-info-circle
|
||||
span='隐私声明'
|
||||
a.rightMenu-item(href="javascript:pjax.loadUrl(\"/cookie/\");")
|
||||
i.fa.fa-info-circle
|
||||
span='Cookie协议'
|
||||
a.rightMenu-item(href="javascript:pjax.loadUrl(\"/cc/\");")
|
||||
i.fa.fa-info-circle
|
||||
span='版权声明'
|
||||
```
|
||||
|
||||
# 新建js文件
|
||||
|
||||
创建 `\themes\butterfly\source\js\rightmenu.js`
|
||||
|
||||
```js
|
||||
function setMask() {//设置遮罩层
|
||||
if (document.getElementsByClassName("rmMask")[0] !== undefined) {
|
||||
return document.getElementsByClassName("rmMask")[0];
|
||||
}
|
||||
mask = document.createElement('div');
|
||||
mask.className = "rmMask";
|
||||
mask.style.width = window.innerWidth + 'px';
|
||||
mask.style.height = window.innerHeight + 'px';
|
||||
mask.style.background = '#fff';
|
||||
mask.style.opacity = '.0';
|
||||
mask.style.position = 'fixed';
|
||||
mask.style.top = '0';
|
||||
mask.style.left = '0';
|
||||
mask.style.zIndex = 998;
|
||||
document.body.appendChild(mask);
|
||||
document.getElementById("rightMenu").style.zIndex = 19198;
|
||||
return mask;
|
||||
}
|
||||
|
||||
function insertAtCursor(myField, myValue) {
|
||||
|
||||
//IE 浏览器
|
||||
if (document.selection) {
|
||||
myField.focus();
|
||||
sel = document.selection.createRange();
|
||||
sel.text = myValue;
|
||||
sel.select();
|
||||
}
|
||||
|
||||
//FireFox、Chrome等
|
||||
else if (myField.selectionStart || myField.selectionStart === '0') {
|
||||
var startPos = myField.selectionStart;
|
||||
var endPos = myField.selectionEnd;
|
||||
|
||||
// 保存滚动条
|
||||
var restoreTop = myField.scrollTop;
|
||||
myField.value = myField.value.substring(0, startPos) + myValue + myField.value.substring(endPos, myField.value.length);
|
||||
|
||||
if (restoreTop > 0) {
|
||||
myField.scrollTop = restoreTop;
|
||||
}
|
||||
|
||||
myField.focus();
|
||||
myField.selectionStart = startPos + myValue.length;
|
||||
myField.selectionEnd = startPos + myValue.length;
|
||||
} else {
|
||||
myField.value += myValue;
|
||||
myField.focus();
|
||||
}
|
||||
}
|
||||
|
||||
let rmf = {};
|
||||
rmf.showRightMenu = function (isTrue, x = 0, y = 0) {
|
||||
let $rightMenu = $('#rightMenu');
|
||||
$rightMenu.css('top', x + 'px').css('left', y + 'px');
|
||||
|
||||
if (isTrue) {
|
||||
$rightMenu.show();
|
||||
} else {
|
||||
$rightMenu.hide();
|
||||
}
|
||||
}
|
||||
rmf.switchDarkMode = function () {
|
||||
const nowMode = document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light'
|
||||
if (nowMode === 'light') {
|
||||
activateDarkMode()
|
||||
saveToLocal.set('theme', 'dark', 2)
|
||||
GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow(GLOBAL_CONFIG.Snackbar.day_to_night)
|
||||
} else {
|
||||
activateLightMode()
|
||||
saveToLocal.set('theme', 'light', 2)
|
||||
GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow(GLOBAL_CONFIG.Snackbar.night_to_day)
|
||||
}
|
||||
// handle some cases
|
||||
typeof utterancesTheme === 'function' && utterancesTheme()
|
||||
typeof FB === 'object' && window.loadFBComment()
|
||||
window.DISQUS && document.getElementById('disqus_thread').children.length && setTimeout(() => window.disqusReset(), 200)
|
||||
switchPostChart();
|
||||
};
|
||||
|
||||
rmf.copyWordsLink = function () {
|
||||
const decodedUrl = decodeURIComponent(window.location.href); // 解码 URL
|
||||
navigator.clipboard.writeText(decodedUrl)
|
||||
.then(() => {
|
||||
Snackbar.show({
|
||||
text: '链接复制成功!快去分享吧!',
|
||||
pos: 'top-right',
|
||||
showAction: false
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
rmf.switchReadMode = function () {
|
||||
const $body = document.body
|
||||
$body.classList.add('read-mode')
|
||||
const newEle = document.createElement('button')
|
||||
newEle.type = 'button'
|
||||
newEle.className = 'fas fa-sign-out-alt exit-readmode'
|
||||
$body.appendChild(newEle)
|
||||
|
||||
function clickFn() {
|
||||
$body.classList.remove('read-mode')
|
||||
newEle.remove()
|
||||
newEle.removeEventListener('click', clickFn)
|
||||
}
|
||||
|
||||
newEle.addEventListener('click', clickFn)
|
||||
}
|
||||
|
||||
//复制选中文字
|
||||
rmf.copySelect = function () {
|
||||
navigator.clipboard.writeText(document.getSelection().toString()).then(() => {
|
||||
Snackbar.show({
|
||||
text: '已复制选中文字!',
|
||||
pos: 'top-right',
|
||||
showAction: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//回到顶部
|
||||
rmf.scrollToTop = function () {
|
||||
document.getElementsByClassName("menus_items")[1].setAttribute("style", "");
|
||||
document.getElementById("name-container").setAttribute("style", "display:none");
|
||||
btf.scrollToDest(0, 500);
|
||||
}
|
||||
rmf.translate = function () {
|
||||
document.getElementById("translateLink").click();
|
||||
}
|
||||
rmf.searchinThisPage = () => {
|
||||
let mask = setMask(); // 确保 mask 元素存在于 document.body 中
|
||||
document.getElementsByClassName("local-search-box--input")[0].value = window.getSelection().toString();
|
||||
document.getElementsByClassName("search")[0].click();
|
||||
var evt = document.createEvent("HTMLEvents");
|
||||
evt.initEvent("input", false, false);
|
||||
document.getElementsByClassName("local-search-box--input")[0].dispatchEvent(evt);
|
||||
|
||||
// 在尝试移除 mask 元素之前检查它是否存在于 document.body 中
|
||||
if (document.body.contains(mask)) {
|
||||
document.body.removeChild(mask);
|
||||
}
|
||||
}
|
||||
|
||||
document.body.addEventListener('touchmove', function (e) {
|
||||
|
||||
}, {passive: false});
|
||||
|
||||
function popupMenu() {
|
||||
//window.oncontextmenu=function(){return false;}
|
||||
window.oncontextmenu = function (event) {
|
||||
Snackbar.show({
|
||||
text: '按住 Ctrl 再点击右键,即可恢复原界面哦',
|
||||
pos: 'bottom-left',
|
||||
showAction: false
|
||||
});
|
||||
if (event.ctrlKey || document.body.clientWidth < 900) return true;
|
||||
$('.rightMenu-group.hide').hide();
|
||||
if (document.getSelection().toString()) {
|
||||
$('#menu-text').show();
|
||||
}
|
||||
if (document.getElementById('post')) {
|
||||
$('#menu-post').show();
|
||||
} else {
|
||||
if (document.getElementById('page')) {
|
||||
$('#menu-post').show();
|
||||
}
|
||||
}
|
||||
var el = window.document.body;
|
||||
el = event.target;
|
||||
var a = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w.-]+)+[\w\-._~:\/?#[\]@!$&'*+,;=]+$/
|
||||
if (a.test(window.getSelection().toString()) && el.tagName !== "A") {
|
||||
$('#menu-too').show()
|
||||
}
|
||||
if (el.tagName === 'A') {
|
||||
$('#menu-to').show()
|
||||
rmf.open = function () {
|
||||
if (el.href.indexOf("http://") === -1 && el.href.indexOf("https://") === -1 || el.href.indexOf("blog.june-pj.cn") !== -1) {
|
||||
pjax.loadUrl(el.href)
|
||||
} else {
|
||||
location.href = el.href
|
||||
}
|
||||
}
|
||||
rmf.openWithNewTab = function () {
|
||||
window.open(el.href);
|
||||
// window.location.reload();
|
||||
}
|
||||
rmf.copyLink = function () {
|
||||
const url = el.href;
|
||||
navigator.clipboard.writeText(url);
|
||||
Snackbar.show({
|
||||
text: '链接复制成功!快去分享吧!',
|
||||
pos: 'top-right',
|
||||
showAction: false
|
||||
});
|
||||
};
|
||||
}
|
||||
if (el.tagName === 'IMG') {
|
||||
$('#menu-img').show()
|
||||
rmf.openWithNewTab = function () {
|
||||
window.open(el.src);
|
||||
// window.location.reload();
|
||||
}
|
||||
rmf.click = function () {
|
||||
el.click()
|
||||
}
|
||||
rmf.copyLink = function () {
|
||||
const url = el.src
|
||||
navigator.clipboard.writeText(url);
|
||||
Snackbar.show({
|
||||
text: '链接复制成功!快去分享吧!',
|
||||
pos: 'top-right',
|
||||
showAction: false
|
||||
});
|
||||
}
|
||||
rmf.saveAs = function () {
|
||||
var a = document.createElement('a');
|
||||
a.href = el.src;
|
||||
// 获取图片的文件名部分
|
||||
a.download = el.src.split('/').pop(); // 使用图片的文件名作为下载文件名
|
||||
a.style.display = 'none'; // 隐藏下载链接
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
} else if (el.tagName === "TEXTAREA" || el.tagName === "INPUT") {
|
||||
$('#menu-paste').show();
|
||||
// rmf.paste=function(){
|
||||
// input.addEventListener('paste', async event => {
|
||||
// event.preventDefault();
|
||||
// const text = await navigator.clipboard.readText();
|
||||
// el.value+=text;
|
||||
// });
|
||||
// }
|
||||
rmf.paste = function () {
|
||||
navigator.permissions
|
||||
.query({
|
||||
name: 'clipboard-read'
|
||||
})
|
||||
.then(result => {
|
||||
if (result.state === 'granted' || result.state === 'prompt') {
|
||||
//读取剪贴板
|
||||
navigator.clipboard.readText().then(text => {
|
||||
console.log(text)
|
||||
insertAtCursor(el, text)
|
||||
})
|
||||
} else {
|
||||
Snackbar.show({
|
||||
text: '请允许读取剪贴板!',
|
||||
pos: 'top-center',
|
||||
showAction: false,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
let pageX = event.clientX + 10;
|
||||
let pageY = event.clientY;
|
||||
let rmWidth = $('#rightMenu').width();
|
||||
let rmHeight = $('#rightMenu').height();
|
||||
if (pageX + rmWidth > window.innerWidth) {
|
||||
pageX -= rmWidth + 10;
|
||||
}
|
||||
if (pageY + rmHeight > window.innerHeight) {
|
||||
pageY -= pageY + rmHeight - window.innerHeight;
|
||||
}
|
||||
mask = setMask();
|
||||
window.onscroll = () => {
|
||||
rmf.showRightMenu(false);
|
||||
window.onscroll = () => {
|
||||
}
|
||||
if (document.body.contains(mask)) {
|
||||
document.body.removeChild(mask);
|
||||
}
|
||||
}
|
||||
|
||||
$(".rightMenu-item").click(() => {
|
||||
if (document.body.contains(mask)) {
|
||||
document.body.removeChild(mask);
|
||||
}
|
||||
});
|
||||
|
||||
$(window).resize(() => {
|
||||
rmf.showRightMenu(false);
|
||||
if (document.body.contains(mask)) {
|
||||
document.body.removeChild(mask);
|
||||
}
|
||||
});
|
||||
|
||||
mask.onclick = () => {
|
||||
if (document.body.contains(mask)) {
|
||||
document.body.removeChild(mask);
|
||||
}
|
||||
};
|
||||
|
||||
rmf.showRightMenu(true, pageY, pageX);
|
||||
return false;
|
||||
};
|
||||
|
||||
window.addEventListener('click', function () {
|
||||
rmf.showRightMenu(false);
|
||||
});
|
||||
}
|
||||
|
||||
if (!(navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i))) {
|
||||
popupMenu()
|
||||
}
|
||||
const box = document.documentElement
|
||||
|
||||
function addLongtabListener(target, callback) {
|
||||
let timer = 0 // 初始化timer
|
||||
|
||||
target.ontouchstart = () => {
|
||||
timer = 0 // 重置timer
|
||||
timer = setTimeout(() => {
|
||||
callback();
|
||||
timer = 0
|
||||
}, 380) // 超时器能成功执行,说明是长按
|
||||
}
|
||||
|
||||
target.ontouchmove = () => {
|
||||
clearTimeout(timer) // 如果来到这里,说明是滑动
|
||||
timer = 0
|
||||
}
|
||||
|
||||
target.ontouchend = () => { // 到这里如果timer有值,说明此触摸时间不足380ms,是点击
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addLongtabListener(box, popupMenu)
|
||||
```
|
||||
|
||||
# 创建css
|
||||
|
||||
创建 `\themes\butterfly\source\css\rightmenu.css`
|
||||
|
||||
```css
|
||||
/* rightMenu */
|
||||
[data-theme='light'] #rightMenu{
|
||||
display: none;
|
||||
position: fixed;
|
||||
width: 160px;
|
||||
height: fit-content;
|
||||
top: 10%;
|
||||
left: 10%;
|
||||
background-color: var(--card-bg);
|
||||
border: 1px solid rgb(210,210,210);;
|
||||
border-radius: 8px;
|
||||
z-index: 100;
|
||||
box-shadow: 3px 3px 5px #88888894;
|
||||
background-color: var(--june-white-acrylic1);
|
||||
backdrop-filter: blur(30px);
|
||||
}
|
||||
[data-theme='dark'] #rightMenu{
|
||||
display: none;
|
||||
position: fixed;
|
||||
width: 160px;
|
||||
height: fit-content;
|
||||
top: 10%;
|
||||
left: 10%;
|
||||
background-color: var(--card-bg);
|
||||
border: 1px solid rgb(210,210,210);;
|
||||
border-radius: 8px;
|
||||
z-index: 100;
|
||||
box-shadow: 3px 3px 5px #88888894;
|
||||
background-color: var(--june-black-acrylic1);
|
||||
backdrop-filter: blur(30px);
|
||||
}
|
||||
#rightMenu .rightMenu-group{
|
||||
padding: 7px 6px;
|
||||
}
|
||||
#rightMenu .rightMenu-group:not(:nth-last-child(1)){
|
||||
border-bottom: 1px solid rgb(180,180,180);
|
||||
}
|
||||
#rightMenu .rightMenu-group.rightMenu-small{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
#rightMenu .rightMenu-group .rightMenu-item{
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
border-radius: 8px;
|
||||
transition: 0.3s;
|
||||
color: var(--font-color);
|
||||
}
|
||||
#rightMenu .rightMenu-group.rightMenu-line .rightMenu-item{
|
||||
display: flex;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
#rightMenu .rightMenu-group .rightMenu-item:hover{
|
||||
background-color: var(--text-bg-hover);
|
||||
box-shadow: 0px 0px 5px var(--june-border);
|
||||
}
|
||||
#rightMenu .rightMenu-group .rightMenu-item i{
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
line-height: 30px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
#rightMenu .rightMenu-group .rightMenu-item span{
|
||||
line-height: 30px;
|
||||
}
|
||||
#rightMenu:hover{
|
||||
border: 1px solid var(--june-theme);
|
||||
}
|
||||
#rightMenu .rightMenu-group.rightMenu-line .rightMenu-item *{
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
}
|
||||
.rightMenu-group.hide{
|
||||
display: none;
|
||||
}
|
||||
.rightMenu-item:hover{
|
||||
color:white!important;
|
||||
background-color:var(--june-theme)!important;
|
||||
}
|
||||
```
|
||||
|
||||
# 引入
|
||||
|
||||
```yml
|
||||
- <script type="text/javascript" src="/js/rightmenu.js"></script>
|
||||
- <link rel="stylesheet" href="/css/rightmenu.css">
|
||||
```
|
||||
41
source/_posts/2025.08/en250811.md
Normal file
41
source/_posts/2025.08/en250811.md
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
title: 英语文章分享-去奋力生活吧,就好像死亡近在咫尺
|
||||
tags: 英语
|
||||
categories: 学习
|
||||
abbrlink: f0a1a1f4
|
||||
summary: >-
|
||||
这篇文章讨论了在生活中追求意义的重要性,特别是在面对死亡时。作者作为一名临终关怀医生,见证了无数病人对未实现目标的遗憾。文章指出,许多人因感到精力不足或时间不够而未能追求自己的目标。然而,研究表明,拥有强烈目标感的人实际上更有活力,更容易被激励。目的不在于做出巨大贡献,而在于找到能激发内心火花的小目标,如阅读、散步或重拾儿时的爱好。文章强调,勇气是追求目标的关键,而不仅仅是勇敢。最后,作者警告说,如果不在有生之年追求目标,可能会在死亡时后悔莫及。
|
||||
date: 2025-08-11 20:59:46
|
||||
---
|
||||
|
||||
As a hospice doctor, I often have the privilege of being at the bedside of dying patients.Over the years, I've had countless conversations about lives well lived, and more often, about regrets.Many of my patients express a strikingly similar sentiment:
|
||||
I really regret that I never had the energy or time to…
|
||||
The rest of the sentence varies.For one, it might be writing a book.For another, traveling to a far-off land.For someone else, maybe it's training for a favorite sport.The specifics change, but the underlying theme is always the same: they missed out on what I call little p purpose — the process-oriented kind of purpose that comes from doing things that light us up from within.
|
||||
The heartbreaking truth about these deathbed epiphanies is that the dying no longer have the agency to make things right.They've run out of time, or strength, or both.What they once could have done, they no longer can.
|
||||
A large part of my work, and of the books I write, is trying to convince the living to adopt this mindset before it's too late.To offer the lens of the dying as a kind of instruction manual for how to live now.
|
||||
We must live as if death is just a breath away.
|
||||
And yet, again and again, I hear the same explanations for why people don't prioritize purpose in their daily lives.
|
||||
I Don't Have Enough Energy
|
||||
This one comes up often, especially among young people, busy with careers and raising children.They feel completely depleted by the demands of work and family.The idea of mustering up extra energy to pursue something just for them seems laughable.
|
||||
But research tells us something interesting.Studies on motivation and physical activity have shown that people with a stronger sense of purpose are actually more likely to feel energized.They experience fewer barriers to action and develop more intrinsic motivation to engage with life.
|
||||
In other words, energy isn't necessarily a finite pie.It can grow, especially when we're engaging with activities that matter to us.
|
||||
And "purpose" doesn't have to mean starting a nonprofit or launching a million-dollar business.Little p purpose is smaller.Simpler.It could be reading a good book, taking a walk by the lake, or tinkering with a hobby you loved as a kid.It's not about the size of the act, it's about the spark.
|
||||
I Don't Have Enough Time
|
||||
This one may feel especially true.We're busier than ever, no doubt bombarded by the demands of work, family, texts, emails, and social media.It's one of the most common objections I hear.
|
||||
But data paints a different picture.
|
||||
The Bureau of Labor Statistics conducts the American Time Use Survey every year, collecting insights from thousands of participants.The most recent findings?Americans spend an average of five hours a day on leisure and sports activities.That's across all socioeconomic backgrounds.Rich or poor, employed or not — most people have more discretionary time than they realize.
|
||||
So, if we do have the time, the question becomes: what's really stopping us?
|
||||
Purpose Demands Courage
|
||||
In my years of working with the dying, I've learned something unexpected.The true barrier to purpose isn't energy, money, or time.
|
||||
It's courage.
|
||||
Not bravery in the way we typically define it.I've had this conversation with war heroes and daredevil athletes.It's not about fearlessness.It's not about skill either.I've cared for world-class artisans and bestselling authors.It's not about knowledge — most of them knew what mattered to them.
|
||||
What they lacked was the courage to choose those things.(他们缺乏的是选择那些的勇气)
|
||||
It's easier to focus on building wealth, pursuing a career, or raising kids.Those are important, yes, but they're also the low-hanging fruit.They come with clear milestones and societal validation.
|
||||
Turning toward what lights you up is a much scarier proposition.
|
||||
What if I'm not good enough?
|
||||
What if I fail?
|
||||
What if it doesn't turn out how I hoped?
|
||||
What if people laugh at me?
|
||||
The list of reasons not to pursue your purpose is long.
|
||||
But there's one reason that matters more than all the rest:
|
||||
You might regret it on your deathbed if you don't.
|
||||
63
source/_posts/2025.08/go-to-random-page.md
Normal file
63
source/_posts/2025.08/go-to-random-page.md
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: 利用 SiteMap 随机访问站内页面
|
||||
tags: 网站
|
||||
categories: 建站手札
|
||||
series: webcustom
|
||||
abbrlink: b5866b9e
|
||||
summary: >-
|
||||
提供了一个很好的解决方案,通过使用 Hexo 的 sitemap 生成器和 JavaScript 代码来实现随机访问站内页面的功能
|
||||
以下是你提供的步骤的详细解释:
|
||||
date: 2025-08-12 15:15:32
|
||||
---
|
||||
在网上查找博客美化过程时发现基本上都有“随便逛逛”的功能,于是自己也添加一个。
|
||||
比较简单只有js代码,但是先要安装插件
|
||||
|
||||
# 前置条件
|
||||
|
||||
```bash
|
||||
npm install hexo-generator-sitemap --save
|
||||
```
|
||||
|
||||
在站点配置文件 `_config.yaml`中添加:
|
||||
|
||||
```yml
|
||||
sitemap:
|
||||
path:
|
||||
- sitemap.xml
|
||||
- sitemap.txt
|
||||
rel: false
|
||||
tags: true
|
||||
categories: true
|
||||
```
|
||||
|
||||
# 添加js
|
||||
|
||||
在主题目录下添加·`\themes\butterfly\source\js\random.js`
|
||||
|
||||
```js
|
||||
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 == '' || locSplit == 'tags');
|
||||
//若所有文章都如 https://…….com/posts/2022/07/…… 格式,主域名后字符是 posts,则循环条件改为:
|
||||
//while (locSplit !== 'posts');
|
||||
location.href = locationHref
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
# 使用
|
||||
|
||||
使用,在菜单栏上添加
|
||||
|
||||
```html
|
||||
<a href="javascript:;" onclick="randomPost()" title="随机访问一篇文章">随机</a>
|
||||
```
|
||||
|
||||
# 参考
|
||||
|
||||
{% link 木木木木木的博客,木木木木木,https://immmmm.com/randompost-by-sitemap/ %}
|
||||
158
source/_posts/2025.08/how-to-deploy.md
Normal file
158
source/_posts/2025.08/how-to-deploy.md
Normal file
@@ -0,0 +1,158 @@
|
||||
---
|
||||
title: 部署历程
|
||||
tags:
|
||||
- 网站
|
||||
categories: 建站手札
|
||||
abbrlink: 6e5f5039
|
||||
summary: >-
|
||||
这篇文章详细介绍了如何部署Hexo博客到Linux服务器,包括前提条件、本地环境准备、SSH密钥认证、配置步骤、工作流程以及故障排除提示。文章首先列出了部署所需满足的条件,然后逐步指导读者如何在服务器上安装Git、创建裸仓库、配置Git钩子以自动部署Hexo博客,并提供了验证部署成功的步骤。最后,文章还给出了一些常见问题的解决建议,帮助读者顺利完成部署。整个过程简单高效,适合需要自动化部署博客的用户。
|
||||
date: 2025-8-02 19:07:05
|
||||
---
|
||||
# 前提条件
|
||||
|
||||
在开始之前,请确保你已满足以下条件:
|
||||
|
||||
* 服务器:
|
||||
* 安装了 1Panel 面板的 Linux 服务器
|
||||
* 拥有服务器的 SSH 访问权限(本文示例将使用 root 用户,你可以根据实际情况替换为其他有权限的用户)。
|
||||
* 服务器上已安装 Git。
|
||||
* 1Panel 配置:
|
||||
|
||||
已通过 1Panel 创建了一个网站(例如 `blog.yourdomain.com`),并记下该网站的 根目录(`Document Root`)。通常在 1Panel 中,路径类似于 `/opt/1panel/apps/openresty/openresty/www/sites/你的网站目录/index`。请务必替换为你的实际路径。
|
||||
本地环境:
|
||||
|
||||
* 你的电脑上已安装并配置好 Hexo 博客环境。
|
||||
* 你的电脑上已安装 Git。
|
||||
* 已安装 Hexo Git 部署插件:
|
||||
|
||||
```bash
|
||||
npm install hexo-deployer-git --save
|
||||
```
|
||||
|
||||
SSH 密钥认证 (关键):
|
||||
|
||||
1. 你需要在本地电脑生成 SSH 密钥对(如果还没有的话)。
|
||||
2. 必须将你的本地电脑的 SSH 公钥 添加到服务器上对应用户(如 root)的 ~/.ssh/authorized_keys 文件中。这样可以确保 hexo deploy 时 Git PUSH 操作无需输入密码即可完成。
|
||||
配置步骤
|
||||
步骤一:在服务器上安装 Git
|
||||
如果你的服务器尚未安装 Git,请根据你的 Linux 发行版执行相应命令:
|
||||
|
||||
Debian/Ubuntu:
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install git -y
|
||||
```
|
||||
|
||||
Fedora/RedHat/CentOS:
|
||||
|
||||
```bash
|
||||
sudo yum install git -y
|
||||
```
|
||||
|
||||
步骤二:在服务器上创建 Git 裸仓库 (Bare Repository)
|
||||
我们需要在服务器上创建一个 Git “裸仓库”。裸仓库不包含工作目录(即你看不到项目文件),只存储 Git 的版本历史和对象数据,非常适合作为中心仓库或中转仓库。
|
||||
|
||||
创建仓库目录:
|
||||
选择一个路径来存放你的 Git 仓库。例如,我们选择放在 /root/git/ 目录下(你可以自定义路径)。
|
||||
|
||||
# 创建目录 (如果父目录不存在,也一并创建)
|
||||
|
||||
```bash
|
||||
mkdir -p /root/git/
|
||||
```
|
||||
|
||||
修改目录权限 (可选但推荐):
|
||||
如果你不是一直使用 root 用户,或者希望更精细地控制权限,可以修改目录所有者和权限。如果使用 root,此步通常可以跳过。
|
||||
|
||||
将目录所有者改为当前用户 (如果用 root 执行,则为 root)
|
||||
|
||||
```bash
|
||||
chown -R USER:USER:USER /root/git/
|
||||
```
|
||||
|
||||
设置权限 (所有者完全控制,同组用户读取执行,其他用户读取执行)
|
||||
|
||||
```bash
|
||||
chmod -R 755 /root/git/
|
||||
```
|
||||
|
||||
初始化裸仓库:
|
||||
进入你选择的目录,并初始化一个裸仓库。我们将仓库命名为 blog.git (你也可以自定义)。
|
||||
|
||||
```bash
|
||||
cd /root/git
|
||||
git init --bare blog.git
|
||||
```
|
||||
|
||||
执行后,你会在 /root/git/ 目录下看到一个名为 blog.git 的文件夹。
|
||||
|
||||
步骤三:在服务器上配置 Git 钩子 (post-receive)
|
||||
Git 钩子是在 Git 操作过程中的特定时间点自动执行的脚本。post-receive 钩子在服务器成功接收到推送 (push) 后执行。我们将利用这个钩子将推送过来的 Hexo 文件检出 (checkout) 到 1Panel 的网站根目录。
|
||||
|
||||
创建钩子文件:
|
||||
进入裸仓库的 hooks 目录,并创建一个名为 post-receive 的文件。
|
||||
|
||||
```bash
|
||||
vim /root/git/blog.git/hooks/post-receive
|
||||
```
|
||||
|
||||
编辑钩子内容:
|
||||
在 vim 编辑器中,按 i 进入插入模式,然后粘贴以下脚本内容:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 指定 Hexo 网站文件存放的目录 (!!!!务必替换为你的 1Panel 网站实际根目录!!!!)
|
||||
WORK_TREE="/opt/1panel/apps/openresty/openresty/www/sites/blog/index"
|
||||
# 指定 Git 裸仓库的路径 (!!!!务必替换为你的裸仓库实际路径!!!!)
|
||||
GIT_DIR="/root/git/blog.git"
|
||||
|
||||
# 执行 Git checkout 命令,强制将内容检出到工作目录
|
||||
git --work-tree=${WORK_TREE} --git-dir=${GIT_DIR} checkout -f
|
||||
|
||||
echo "Hexo blog deployed to ${WORK_TREE}"
|
||||
```
|
||||
|
||||
# 可选的提交信息
|
||||
|
||||
请务必修改:
|
||||
|
||||
`root`: 替换为你配置了 SSH 免密登录的服务器用户名。
|
||||
`YOUR_SERVER_IP`: 替换为你的服务器公网 IP 地址或域名。
|
||||
`22`: 如果你的 SSH 端口不是默认的 22,请修改。
|
||||
`/root/git/blog.git`: 替换为你在服务器上创建的裸仓库的 绝对路径。
|
||||
`branch`: 确保这个分支名 (main 或 master) 与你的 post-receive 钩子期望检出的分支一致(默认情况下 checkout -f 会检出仓库的 HEAD 指向的分支,通常是 master 或 main)。
|
||||
步骤五:部署你的 Hexo 博客
|
||||
一切配置完成后,部署就非常简单了:
|
||||
|
||||
在本地 Hexo 项目目录下,执行标准的生成和部署命令:
|
||||
|
||||
```bash
|
||||
hexo clean && hexo generate && hexo deploy
|
||||
```
|
||||
|
||||
或者直接:
|
||||
|
||||
```bash
|
||||
hexo d
|
||||
```
|
||||
|
||||
工作流程:
|
||||
|
||||
hexo g 生成静态文件到 public 目录。
|
||||
hexo d 使用 hexo-deployer-git 插件:
|
||||
将 public 目录的内容提交到一个临时 Git 分支。
|
||||
通过 SSH 将这个分支强制推送到你配置的服务器裸仓库 (ssh://root@YOUR_SERVER_IP:22/root/git/blog.git)。
|
||||
服务器上的 Git 仓库接收到推送后,自动触发 post-receive 钩子脚本。
|
||||
钩子脚本执行 git checkout -f,将最新的网站文件强制检出(部署)到 1Panel 网站的根目录 (/opt/1panel/apps/openresty/openresty/www/sites/blog/index)。
|
||||
验证:
|
||||
部署命令执行成功后,稍等片刻,然后访问你的博客域名 (http://blog.yourdomain.com 或 https://blog.yourdomain.com),应该就能看到更新后的内容了。
|
||||
|
||||
# 故障排除提示
|
||||
|
||||
* 权限问题: 检查服务器上 Git 仓库目录、网站根目录以及钩子文件的权限是否正确。钩子脚本需要有权限写入网站根目录。
|
||||
* SSH 密钥问题: 确保本地 SSH 公钥已正确添加到服务器的 authorized_keys 文件中,并且本地 SSH 私钥可用。尝试手动 ssh root@YOUR_SERVER_IP 看是否能免密登录。
|
||||
* 路径错误: 仔细核对 post-receive 脚本中的 --work-tree 和 --git-dir 路径,以及本地 _config.yml 中的 repo 路径是否完全正确。
|
||||
* 钩子未执行: 检查钩子文件是否有执行权限 (chmod +x),文件名是否精确为 post-receive (没有扩展名)。
|
||||
* 查看日志: 如果部署失败,可以尝试在服务器上查看 SSH 或 Git 相关日志,或者在钩子脚本中添加一些 echo 输出到日志文件来调试。
|
||||
* 通过以上配置,你就实现了一个高效、自动化的 Hexo 博客部署流程!
|
||||
910
source/_posts/2025.08/set-shuoshuo-page.md
Normal file
910
source/_posts/2025.08/set-shuoshuo-page.md
Normal file
@@ -0,0 +1,910 @@
|
||||
---
|
||||
title: 配置说说页面
|
||||
tags: 网站
|
||||
categories: 建站手札
|
||||
series: webcustom
|
||||
abbrlink: ad244066
|
||||
summary: >-
|
||||
这段文字看起来是一个部署教程,主要是关于如何在服务器上部署一个名为“Moments”的应用,并将其与网站集成。以下是对这段文字的概括: 1.
|
||||
介绍了硬件和软件的要求,包括服务器、域名、Docker环境和反向代理工具(如Nginx)。 2.
|
||||
提供了Moments应用的部署方法,包括使用docker-compose进行部署和配置Nginx反向代理。 3.
|
||||
介绍了如何在前端集成Moments应用,包括引入必要的CSS和JS文件,以及创建一个新的页面来显示Moments的内容。
|
||||
需要注意的是,这段文字中的一些链接和代码可能已经过时或者不适用于所有场景,建议在实际操作时参考最新的官方
|
||||
date: 2025-08-10 08:25:39
|
||||
---
|
||||
又开始折腾啦,这次把说说页面加上,改用moments项目的api
|
||||
{% link Moments 极简朋友圈,githun@kingwrcy,https://github.com/kingwrcy/moments %}
|
||||
|
||||
# 前期要求
|
||||
|
||||
## 硬件要求
|
||||
|
||||
1. 一台服务器
|
||||
2. 一个可自主解析的域名
|
||||
|
||||
## 软件要求
|
||||
|
||||
1. docker环境
|
||||
2. 反向代理工具(本文以Nginx为例)
|
||||
|
||||
# 介绍与展示
|
||||
|
||||
这里先给大家展示一下最终的效果,注意该教程可能仅适合部分主题,如果出现主题不适配的情况请自行适配,这里以本站主题 `Hexo-theme-butterfly`为基础进行修改:
|
||||
|
||||
1. 说说页面:
|
||||
{% link 我的说说,胡言乱语ing。。。,https://blog.biss.click %}
|
||||
2. 轻量朋友圈
|
||||
{% link 朋友圈,依旧胡言乱语,https://mm.biss.blog %}
|
||||
3. 功能说明
|
||||
|
||||
Moments作为一个轻量朋友圈,其功能都是分享上的部分,如下所示:
|
||||
|
||||
* 分享:链接,图片,音乐,视频,书籍,电影
|
||||
* 信息:自定义位置,自定义标签,是否公开
|
||||
* 页面:Markdown渲染,编辑说说,删除说说,暗夜模式,自定义图标,信息,CSS及JS代码
|
||||
* 功能:S3存储,文件查询,多用户注册,点赞,评论,API
|
||||
|
||||
简单介绍完毕,下面我就来教大家如何进行部署!
|
||||
|
||||
# 部署教程
|
||||
|
||||
## Moments部署
|
||||
|
||||
### Compose部署
|
||||
|
||||
官方给予了很完善的教程,这里我仅仅简单介绍一下 `docker-compose`部署的方式,如果你想以源码等其他方式进行部署,请查看文章开头部分的 `github`地址进行查阅。
|
||||
|
||||
首先,在服务器任意位置创建文件:`docker-compose.yaml`,填入以下内容:
|
||||
|
||||
```bash
|
||||
version: '3'
|
||||
services:
|
||||
moments:
|
||||
image: kingwrcy/moments:latest
|
||||
container_name: moments
|
||||
restart: always
|
||||
environment:
|
||||
port: 3000
|
||||
JWT_KEY: "自己随便生成点字符串"
|
||||
ENABLE_SWAGGER: "true"
|
||||
CORS_ORIGIN: # 填写跨域域名
|
||||
ports:
|
||||
- "3000:3000" # 自行换端口,换前面的,后面的3000不要动
|
||||
volumes:
|
||||
- ./opt/data:/app/data
|
||||
# - ./data/localtime:/etc/localtime:ro
|
||||
# - ./data/timezone:/etc/timezone:ro
|
||||
```
|
||||
|
||||
注意文件,我将 `/opt/`文件夹(当然可以改成其他的)下的 `/data`文件夹挂载了进去,数据都会在里面,迁移时仅需整体打包到新服务器即可。然后执行以下两条命令,后续需要升级也仅需要执行这两个命令:
|
||||
|
||||
```bash
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
如果网络环境不佳,可尝试替换 `docker`源,可以自行查找
|
||||
|
||||
通过反向代理将其添加到某个域名中,这里就不再多说了,各大面板都有极其完备的反代文档。
|
||||
|
||||
### Nginx修改
|
||||
|
||||
`Moments`在跨域 `docker-compose`文件中可以配置,所以不必进行此步
|
||||
|
||||
网站目录
|
||||
|
||||
返回到上一级目录,也就是域名名称的文件夹下,找到 `Proxy`文件夹,编辑里面的 `root.conf`文件为如下内容:
|
||||
|
||||
```bash
|
||||
#代理配置
|
||||
|
||||
location / {
|
||||
# 跨域设置
|
||||
add_header Access-Control-Allow-Origin *; # 允许所有域名访问,你也可以指定具体域名
|
||||
add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS'; # 允许的 HTTP 方法
|
||||
add_header Access-Control-Allow-Headers 'Origin, X-Requested-With, Content-Type, Accept, Authorization'; # 允许的请求头
|
||||
|
||||
# 处理 OPTIONS 请求,预检请求
|
||||
if ($request_method = 'OPTIONS') {
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS';
|
||||
add_header Access-Control-Allow-Headers 'Origin, X-Requested-With, Content-Type, Accept, Authorization';
|
||||
add_header Access-Control-Max-Age 1728000;
|
||||
add_header Content-Type 'text/plain charset=UTF-8';
|
||||
add_header Content-Length 0;
|
||||
return 204;
|
||||
}
|
||||
|
||||
# 原代理设置
|
||||
proxy_pass http://127.0.0.1:3003;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header REMOTE-HOST $remote_addr;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $http_connection;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_http_version 1.1;
|
||||
add_header X-Cache $upstream_cache_status;
|
||||
add_header Cache-Control no-cache;
|
||||
proxy_ssl_server_name off;
|
||||
proxy_ssl_name $proxy_host;
|
||||
add_header Strict-Transport-Security "max-age=31536000";
|
||||
}
|
||||
```
|
||||
|
||||
下面是原代理设置,可以仅仅复制上面部分内容,下面保持不变,注意端口不要出问题。
|
||||
|
||||
## 前端实现
|
||||
|
||||
由于该项目利用了MetingJS和APlayer,所以请提前引入这两个包,Hexo-theme-butterfly中虽然有内置的两个包,仅需修改配置文件即可开启,但是版本比较老,这里我建议自行引入最新版本,在配置中引入以下文件,注意css和js应该是分开引入的:
|
||||
|
||||
```bash
|
||||
<link rel="stylesheet" href="https://fastly.jsdelivr.net/npm/aplayer/dist/APlayer.min.css" media="all" onload="this.media="all"">
|
||||
<script src="https://fastly.jsdelivr.net/npm/aplayer/dist/APlayer.min.js"></script>
|
||||
<script src="https://fastly.jsdelivr.net/npm/meting/dist/Meting.min.js"></script>
|
||||
```
|
||||
|
||||
新建页面shuoshuo,在文件内写入一下内容:
|
||||
|
||||
```bash
|
||||
---
|
||||
title: 日常哔哔,键盘侠的日常吐槽
|
||||
aside: false
|
||||
---
|
||||
<div id="talk"></div>
|
||||
<div class="limit">- 只展示最近30条说说 -</div>
|
||||
<script src="/js/shuoshuo.js" no-pjax></script>
|
||||
```
|
||||
|
||||
其中的JS文件地址清自行修改,放在主题目录 `/script/`目录,自行创建,并写入以下内容:
|
||||
|
||||
```js
|
||||
function renderTalks() {
|
||||
const talkContainer = document.querySelector('#talk');
|
||||
const domain = 'https://mm.biss.click';
|
||||
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/memo/list';
|
||||
const cacheKey = 'talksCache';
|
||||
const cacheTimeKey = 'talksCacheTime';
|
||||
const cacheDuration = 30 * 60 * 1000; // 半个小时 (30 分钟)
|
||||
|
||||
const cachedData = localStorage.getItem(cacheKey);
|
||||
const cachedTime = localStorage.getItem(cacheTimeKey);
|
||||
const currentTime = new Date().getTime();
|
||||
|
||||
// 判断缓存是否有效
|
||||
if (cachedData && cachedTime && (currentTime - cachedTime < cacheDuration)) {
|
||||
const data = JSON.parse(cachedData);
|
||||
renderTalks(data); // 使用缓存渲染数据
|
||||
} else {
|
||||
if (talkContainer) {
|
||||
talkContainer.innerHTML = '';
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
size: 30
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.code === 0 && data.data && Array.isArray(data.data.list)) {
|
||||
// 缓存数据
|
||||
localStorage.setItem(cacheKey, JSON.stringify(data.data.list));
|
||||
localStorage.setItem(cacheTimeKey, currentTime.toString());
|
||||
renderTalks(data.data.list); // 渲染数据
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching data:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染函数
|
||||
function renderTalks(list) {
|
||||
// 确保 data 是一个数组
|
||||
if (Array.isArray(list)) {
|
||||
let items = list.map(item => formatTalk(item, url));
|
||||
items.forEach(item => talkContainer.appendChild(generateTalkElement(item)));
|
||||
waterfall('#talk');
|
||||
} else {
|
||||
console.error('Data is not an array:', list);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const formatTalk = (item, url) => {
|
||||
let date = formatTime(new Date(item.createdAt).toString());
|
||||
let content = item.content;
|
||||
let imgs = item.imgs ? item.imgs.split(',') : [];
|
||||
let text = content;
|
||||
content = text.replace(/\[(.*?)\]\((.*?)\)/g, `<a href="$2">@$1</a>`)
|
||||
.replace(/- \[ \]/g, '⚪')
|
||||
.replace(/- \[x\]/g, '⚫');
|
||||
// 保留换行符,转换 \n 为 <br>
|
||||
content = content.replace(/\n/g, '<br>');
|
||||
// 将content用一个类包裹,便于后续处理
|
||||
content = `<div class="talk_content_text">${content}</div>`;
|
||||
if (imgs.length > 0) {
|
||||
const imgDiv = document.createElement('div');
|
||||
imgDiv.className = 'zone_imgbox';
|
||||
imgs.forEach(e => {
|
||||
const imgLink = document.createElement('a');
|
||||
const imgUrl = domain + e;
|
||||
imgLink.href = imgUrl;
|
||||
imgLink.setAttribute('data-fancybox', 'gallery');
|
||||
imgLink.className = 'fancybox';
|
||||
imgLink.setAttribute('data-thumb', e);
|
||||
const imgTag = document.createElement('img');
|
||||
imgTag.src = domain + e;
|
||||
imgLink.appendChild(imgTag);
|
||||
imgDiv.appendChild(imgLink);
|
||||
});
|
||||
content += imgDiv.outerHTML;
|
||||
}
|
||||
|
||||
// 外链分享功能
|
||||
if (item.externalUrl) {
|
||||
const externalUrl = item.externalUrl;
|
||||
const externalTitle = item.externalTitle;
|
||||
const externalFavicon = item.externalFavicon;
|
||||
|
||||
const externalContainer = `
|
||||
<div class="shuoshuo-external-link">
|
||||
<a class="external-link" href="${externalUrl}" target="_blank" rel="external nofollow noopener noreferrer">
|
||||
<div class="external-link-left" style="background-image: url(${externalFavicon})"></div>
|
||||
<div class="external-link-right">
|
||||
<div class="external-link-title">${externalTitle}</div>
|
||||
<div>点击跳转<i class="fa-solid fa-angle-right"></i></div>
|
||||
</div>
|
||||
</a>
|
||||
</div>`;
|
||||
|
||||
content += externalContainer;
|
||||
}
|
||||
|
||||
const ext = JSON.parse(item.ext || '{}');
|
||||
|
||||
if (ext.music && ext.music.id) {
|
||||
const music = ext.music;
|
||||
const musicUrl = music.api.replace(':server', music.server)
|
||||
.replace(':type', music.type)
|
||||
.replace(':id', music.id);
|
||||
content += `
|
||||
<meting-js server="${music.server}" type="${music.type}" id="${music.id}" api="${music.api}"></meting-js>
|
||||
`;
|
||||
}
|
||||
|
||||
if (ext.doubanMovie && ext.doubanMovie.id) {
|
||||
const doubanMovie = ext.doubanMovie;
|
||||
const doubanMovieUrl = doubanMovie.url;
|
||||
const doubanTitle = doubanMovie.title;
|
||||
// const doubanDesc = doubanMovie.desc || '暂无描述';
|
||||
const doubanImage = doubanMovie.image;
|
||||
const doubanDirector = doubanMovie.director || '未知导演';
|
||||
const doubanRating = doubanMovie.rating || '暂无评分';
|
||||
// const doubanReleaseDate = doubanMovie.releaseDate || '未知上映时间';
|
||||
// const doubanActors = doubanMovie.actors || '未知演员';
|
||||
const doubanRuntime = doubanMovie.runtime || '未知时长';
|
||||
|
||||
content += `
|
||||
<a class="douban-card" href="${doubanMovieUrl}" target="_blank">
|
||||
<div class="douban-card-bgimg" style="background-image: url('${doubanImage}');"></div>
|
||||
<div class="douban-card-left">
|
||||
<div class="douban-card-img" style="background-image: url('${doubanImage}');"></div>
|
||||
</div>
|
||||
<div class="douban-card-right">
|
||||
<div class="douban-card-item"><span>电影名: </span><strong>${doubanTitle}</strong></div>
|
||||
<div class="douban-card-item"><span>导演: </span><span>${doubanDirector}</span></div>
|
||||
<div class="douban-card-item"><span>评分: </span><span>${doubanRating}</span></div>
|
||||
<div class="douban-card-item"><span>时长: </span><span>${doubanRuntime}</span></div>
|
||||
</div>
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
if (ext.doubanBook && ext.doubanBook.id) {
|
||||
const doubanBook = ext.doubanBook;
|
||||
const bookUrl = doubanBook.url;
|
||||
const bookTitle = doubanBook.title;
|
||||
// const bookDesc = doubanBook.desc;
|
||||
const bookImage = doubanBook.image;
|
||||
const bookAuthor = doubanBook.author;
|
||||
const bookRating = doubanBook.rating;
|
||||
const bookPubDate = doubanBook.pubDate;
|
||||
|
||||
const bookTemplate = `
|
||||
<a class="douban-card" href="${bookUrl}" target="_blank">
|
||||
<div class="douban-card-bgimg" style="background-image: url('${bookImage}');"></div>
|
||||
<div class="douban-card-left">
|
||||
<div class="douban-card-img" style="background-image: url('${bookImage}');"></div>
|
||||
</div>
|
||||
<div class="douban-card-right">
|
||||
<div class="douban-card-item">
|
||||
<span>书名: </span><strong>${bookTitle}</strong>
|
||||
</div>
|
||||
<div class="douban-card-item">
|
||||
<span>作者: </span><span>${bookAuthor}</span>
|
||||
</div>
|
||||
<div class="douban-card-item">
|
||||
<span>出版年份: </span><span>${bookPubDate}</span>
|
||||
</div>
|
||||
<div class="douban-card-item">
|
||||
<span>评分: </span><span>${bookRating}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
`;
|
||||
|
||||
content += bookTemplate;
|
||||
}
|
||||
|
||||
if (ext.video && ext.video.type) {
|
||||
const videoType = ext.video.type;
|
||||
const videoUrl = ext.video.value;
|
||||
if (videoType === 'bilibili') {
|
||||
// Bilibili 视频模板
|
||||
// 从形如https://www.bilibili.com/video/BV1VGAPeAEMQ/?vd_source=91b3158d27d98ff41f842508c3794a13 的链接中提取视频 BV1VGAPeAEMQ
|
||||
const biliTemplate = `
|
||||
<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="${videoUrl}&autoplay=0"
|
||||
scrolling="no"
|
||||
frameborder="no"
|
||||
allowfullscreen>
|
||||
</iframe>
|
||||
</div>
|
||||
`;
|
||||
// 将模板插入到 DOM 中
|
||||
content += biliTemplate;
|
||||
|
||||
} else if (videoType === 'youtube') {
|
||||
// YouTube 视频模板
|
||||
// 从形如https://youtu.be/2V6lvCUPT8I?si=DVhUas6l6qlAr6Ru的链接中提取视频 ID2V6lvCUPT8I
|
||||
const youtubeTemplate = `
|
||||
<div style="position: relative; padding: 30% 45%; margin-top: 10px;">
|
||||
<iframe width="100%"
|
||||
style="position: absolute; width: 100%; height: 100%; left: 0; top: 0; border-radius: 12px;"
|
||||
src="${videoUrl}"
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerpolicy="strict-origin-when-cross-origin"
|
||||
allowfullscreen>
|
||||
</iframe>
|
||||
</div>
|
||||
`;
|
||||
// 将模板插入到 DOM 中
|
||||
content += youtubeTemplate;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: content,
|
||||
user: item.user.nickname || '匿名',
|
||||
avatar: item.user.avatarUrl || 'https://p.liiiu.cn/i/2024/03/29/66061417537af.png',
|
||||
date: date,
|
||||
location: item.location || '山西',
|
||||
tags: item.tags ? item.tags.split(',').filter(tag => tag.trim() !== '') : ['无标签'],
|
||||
text: content.replace(/\[(.*?)\]\((.*?)\)/g, '[链接]' + `${imgs.length ? '[图片]' : ''}`)
|
||||
};
|
||||
};
|
||||
|
||||
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 talkNick = document.createElement('span');
|
||||
talkNick.className = 'talk_nick';
|
||||
talkNick.innerHTML = `${item.user} ${generateIconSVG()}`;
|
||||
|
||||
const talkDate = document.createElement('span');
|
||||
talkDate.className = 'talk_date';
|
||||
talkDate.textContent = item.date;
|
||||
|
||||
const talkContent = document.createElement('div');
|
||||
talkContent.className = 'talk_content';
|
||||
talkContent.innerHTML = item.content;
|
||||
|
||||
const talkBottom = document.createElement('div');
|
||||
talkBottom.className = 'talk_bottom';
|
||||
|
||||
const TagContainer = document.createElement('div');
|
||||
|
||||
const talkTag = document.createElement('span');
|
||||
talkTag.className = 'talk_tag';
|
||||
talkTag.textContent = `🏷️${item.tags}`;
|
||||
|
||||
const locationTag = document.createElement('span');
|
||||
locationTag.className = 'location_tag';
|
||||
locationTag.textContent = `🌍${item.location}`;
|
||||
|
||||
TagContainer.appendChild(talkTag);
|
||||
TagContainer.appendChild(locationTag);
|
||||
|
||||
const commentLink = document.createElement('a');
|
||||
commentLink.href = 'javascript:;';
|
||||
commentLink.onclick = () => goComment(item.text);
|
||||
const commentIcon = document.createElement('span');
|
||||
commentIcon.className = 'icon';
|
||||
const commentIconInner = document.createElement('i');
|
||||
commentIconInner.className = 'fa-solid fa-message fa-fw';
|
||||
commentIcon.appendChild(commentIconInner);
|
||||
commentLink.appendChild(commentIcon);
|
||||
|
||||
talkMeta.appendChild(avatar);
|
||||
info.appendChild(talkNick);
|
||||
info.appendChild(talkDate);
|
||||
talkMeta.appendChild(info);
|
||||
talkItem.appendChild(talkMeta);
|
||||
talkItem.appendChild(talkContent);
|
||||
talkBottom.appendChild(TagContainer);
|
||||
talkBottom.appendChild(commentLink);
|
||||
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 n = document.querySelector(".atk-textarea");
|
||||
n.value = `> ${textContent}\n\n`;
|
||||
n.focus();
|
||||
btf.snackbarShow("已为您引用该说说,不删除空格效果更佳");
|
||||
// const n = document.querySelector(".atk-textarea");
|
||||
// n.value = `> ${e}\n\n`;
|
||||
// n.focus();
|
||||
// btf.snackbarShow("已为您引用该说说,不删除空格效果更佳");
|
||||
};
|
||||
|
||||
const formatTime = (time) => {
|
||||
const d = new Date(time);
|
||||
const ls = [
|
||||
d.getFullYear(),
|
||||
d.getMonth() + 1,
|
||||
d.getDate(),
|
||||
d.getHours(),
|
||||
d.getMinutes(),
|
||||
d.getSeconds(),
|
||||
];
|
||||
const r = ls.map((a) => (a.toString().length === 1 ? '0' + a : a));
|
||||
return `${r[0]}-${r[1]}-${r[2]} ${r[3]}:${r[4]}`;
|
||||
};
|
||||
|
||||
fetchAndRenderTalks();
|
||||
}
|
||||
|
||||
renderTalks();
|
||||
|
||||
// function whenDOMReady() {
|
||||
// const talkContainer = document.querySelector('#talk');
|
||||
// talkContainer.innerHTML = '';
|
||||
// fetchAndRenderTalks();
|
||||
// }
|
||||
// whenDOMReady();
|
||||
// document.addEventListener("pjax:complete", whenDOMReady);
|
||||
```
|
||||
|
||||
自行修改js文件中的Moments地址为你的地址,在文件中,有一个gocomment函数,实现的是获取卡片中的文本内容,如果如果出现不匹配的情况,请自行修改一下类名,这里我匹配的是artalk的输入框。在这里我把代码做了修改,因为我不想使用s3,结果导致说说图片无法加载。。。
|
||||
修改历程:日后再写
|
||||
|
||||
然后引入样式文件,这个文件可以在配置文件中引用,也可以在页面文件中类似于shuoshuo.js一样引用,样式内容如下:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--liushen-card-bg: #fff;
|
||||
--liushen-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);
|
||||
--liushen-card-secondbg: #f1f3f8;
|
||||
--liushen-button-hover-bg: #2679cc;
|
||||
--liushen-text: #4c4948;
|
||||
--liushen-button-bg: #f1f3f8;
|
||||
--liushen-fancybox-bg: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
:root, [data-theme=dark] {
|
||||
--liushen-card-bg: #181818;
|
||||
--liushen-card-secondbg: #30343f;
|
||||
--liushen-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);
|
||||
--liushen-button-bg: #30343f;
|
||||
--liushen-button-hover-bg: #2679cc;
|
||||
--liushen-text: rgba(255,255,255,0.702);
|
||||
--liushen-fancybox-bg: rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
/* 卡片初始化 */
|
||||
#talk .talk_item {
|
||||
width: calc(33.333% - 6px);
|
||||
background: var(--liushen-card-bg);
|
||||
border: var(--liushen-card-border);
|
||||
box-shadow: var(--card-box-shadow);
|
||||
transition: box-shadow .3s ease-in-out;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
margin-bottom: 9px;
|
||||
margin-right: 9px;
|
||||
}
|
||||
#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: calc(100%);
|
||||
}
|
||||
}
|
||||
|
||||
#talk{
|
||||
position: relative;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#talk .talk_meta .avatar {
|
||||
margin: 0 !important;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
#talk .talk_bottom,
|
||||
#talk .talk_meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
#talk .talk_meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px dashed grey; /* 添加灰色虚线边框 */
|
||||
}
|
||||
#talk .talk_bottom {
|
||||
margin-top: 15px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px dashed grey; /* 添加灰色虚线边框 */
|
||||
justify-content: space-between;
|
||||
}
|
||||
#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 {
|
||||
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(--liushen-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;
|
||||
}
|
||||
.zone_imgbox {
|
||||
--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;
|
||||
}
|
||||
|
||||
.douban-card .douban-card-bgimg {
|
||||
background-position: 50%;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100%;
|
||||
filter: blur(15px) brightness(.6);
|
||||
height: 115%;
|
||||
position: absolute;
|
||||
width: 115%;
|
||||
}
|
||||
|
||||
.douban-card .douban-card-left {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.douban-card .douban-card-left .douban-card-img {
|
||||
transition: all .5s ease;
|
||||
height: 130px;
|
||||
position: relative;
|
||||
width: 80px;
|
||||
background-position: 50%;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100%;
|
||||
}
|
||||
|
||||
.douban-card .douban-card-left:hover .douban-card-img {
|
||||
filter: blur(5px) brightness(.6);
|
||||
transform: perspective(800px) rotateX(180deg);
|
||||
}
|
||||
|
||||
.douban-card .douban-card-right {
|
||||
color: #faebd7;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
margin-left: 12px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.douban-card .douban-card-right .douban-card-item {
|
||||
margin-top: 4px;
|
||||
max-width: 95%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 外链卡片 */
|
||||
#talk .talk_item .talk_content .shuoshuo-external-link {
|
||||
/* 无下划线 */
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
margin-top: 10px;
|
||||
border-radius: 12px;
|
||||
background-color: var(--liushen-card-secondbg);
|
||||
color: var(--liushen-card-text);
|
||||
border: var(--liushen-card-border);
|
||||
transition: background-color .3s ease-in-out;
|
||||
}
|
||||
|
||||
.shuoshuo-external-link:hover {
|
||||
background-color: var(--liushen-button-hover-bg);
|
||||
}
|
||||
|
||||
.shuoshuo-external-link .external-link {
|
||||
display: flex;
|
||||
color: var(--liushen-text) !important;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.shuoshuo-external-link .external-link:hover {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.shuoshuo-external-link .external-link:hover {
|
||||
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;
|
||||
}
|
||||
```
|
||||
|
||||
如果一切正常,应该就可以显示了,但是我在使用时发现卡片是黑色的,把前面的root内容删除之后就好了。
|
||||
|
||||
# 参考内容
|
||||
|
||||
{% link liushen的博客,liushen,https://blog.liushen.fun/posts/8338183a/ %}
|
||||
|
||||
当然还有chatgpt的支持
|
||||
138
source/_posts/2025.08/use-github-to-deploy-on-server.md
Normal file
138
source/_posts/2025.08/use-github-to-deploy-on-server.md
Normal file
@@ -0,0 +1,138 @@
|
||||
---
|
||||
title: 使用GitHub推送hexo到服务器
|
||||
categories: 建站手札
|
||||
series: webcustom
|
||||
tags: 网站
|
||||
abbrlink: ce1ec3fe
|
||||
summary: >-
|
||||
使用GitHub Actions自动部署Hexo到服务器的步骤如下: 1. **在本地安装git**:确保你的计算机上已经安装了Git。 2.
|
||||
**配置git用户信息**:在终端或Git Bash中配置你的用户名和邮箱: ```bash git config --global user.name
|
||||
"yourname" git config --global user.email "youremail@example.com" ``` 使用 `git
|
||||
config --list` 命令查看当前所有的配置。 3.
|
||||
**在GitHub上创建账号**:如果你还没有GitHub账号,需要先在GitHub上注册一个。 4. **创建SSH Key**:在本地生成SSH
|
||||
Key,并将公钥添加到GitHub账号中: ```bash ssh-keygen -t rsa -C "youremail@example.com" ```
|
||||
按照提示操作,最后会在 `~/.ssh` 目录下生成 `id_rsa`
|
||||
date: 2025-08-18 17:10:18
|
||||
---
|
||||
|
||||
1panel没有宝塔的webhook功能,之前一直使用在服务器上建立裸仓库,直接推送到服务器的方法。现在找到一种新的方法。
|
||||
# 建立GitHub仓库
|
||||
因为服务器部署在香港,可以直连GitHub,国内可以使用gitee或者GitHub镜像加速。
|
||||
1. 在本地安装git
|
||||
2. 打开git bash配置用户名和邮箱
|
||||
```bash
|
||||
git config --global user.name "yourname"
|
||||
git config --global user.email "youremail"
|
||||
```
|
||||
可以使用 git config --list查看当前所有的配置
|
||||
|
||||
3. 在github上创建自己的账号(在自己的电脑和服务器上)
|
||||
4. 创建SSH Key
|
||||
a:打开Git Bash,输入pwd查看当前路径
|
||||
b.输入`ssh-keygen -t rsa –C “youremail@example.com”`
|
||||
(输入完毕后程序同时要求输入一个密语字符串(passphrase),空表示没有密语。接着会让输入2次口令(password),空表示没有口令。3次回车即可完成当前步骤)
|
||||
<center>
|
||||
<img src="https://pic.biss.click/i/2025/08/18/924058.webp" alt="924058.webp">
|
||||
</center>
|
||||
5. 在GitHub上传自己的公钥
|
||||
<center><img src="https://pic.biss.click/i/2025/08/18/946620.webp" alt="946620.webp"></center>
|
||||
6. 新建一个私有仓库
|
||||
|
||||
# 上传文件到仓库
|
||||
在自己hexo根目录下使用`powershell`,输入`git init`,然后git push到之前自己新建的仓库。
|
||||
|
||||
# 创建Action
|
||||
## 创建Action
|
||||
|
||||
```yml
|
||||
name: 自动部署
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
TZ: Asia/Shanghai
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 检查分支
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: main
|
||||
|
||||
- name: 缓存项目 npm 包
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-nodeModules-${{ hashFiles('package-lock.json') }}-${{ hashFiles('package.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nodeModules-
|
||||
|
||||
- name: 安装 Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "20.x"
|
||||
|
||||
- name: 安装 Hexo
|
||||
run: |
|
||||
npm install hexo-cli --global
|
||||
|
||||
- name: 安装依赖
|
||||
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
npm install
|
||||
|
||||
- name: 清理文件树
|
||||
run: |
|
||||
npm run clean
|
||||
|
||||
- name: 生成静态文件并压缩
|
||||
run: |
|
||||
npm run build
|
||||
|
||||
- name: 部署
|
||||
run: |
|
||||
cd ./public
|
||||
git init
|
||||
git config user.name "${{ github.actor }}"
|
||||
git config user.email "${{ github.actor }}@users.noreply.github.com"
|
||||
git add .
|
||||
git commit -m "${{ github.event.head_commit.message }}··[$(date +"%Z %Y-%m-%d %A %H:%M:%S")]"
|
||||
git push --force --quiet "https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" master:page
|
||||
- name: 服务器执行拉取命令
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.SERVER_IP }}
|
||||
username: ${{ secrets.USERNAME }}
|
||||
key: ${{ secrets.KEY }}
|
||||
passphrase: ${{ secrets.PASSPHRASE }}
|
||||
port: ${{ secrets.PORT }}
|
||||
script: |
|
||||
cd /opt/1panel/apps/openresty/openresty/www/sites/blog.liushen.fun/index/
|
||||
git config --global --add safe.directory "$(pwd)"
|
||||
git fetch --all --depth=1
|
||||
git reset --hard origin/main
|
||||
git pull --depth=1
|
||||
echo "✅ 已拉取 page 分支最新内容"
|
||||
```
|
||||
## 环境变量
|
||||
此次添加变量较多,有:`SERVER_IP`,`USERNAME`,`PASSPHRASE`,`KEY`,`PORT`五个变量,如下:
|
||||
|
||||
`SERVER_IP`:服务器IP
|
||||
`USERNAME`:SSH链接用户名,一般为root
|
||||
`PASSPHRASE`:密钥密码,用来提升强度用
|
||||
`KEY`:密钥(私钥)
|
||||
`PORT`:SSH登录端口,一般为22
|
||||
# 测试
|
||||
运行一下这个Action无报错即可。
|
||||
Reference in New Issue
Block a user