1312 lines
120 KiB
HTML
1312 lines
120 KiB
HTML
<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="description" content="这篇文章介绍了如何在Hexo博客中添加AI摘要功能,作者寻找并尝试了多个AI摘要插件后,最终选择了hexo-ai-summary-liushen插件。安装过程中,作者详细说明了如何安装额外依赖,并在Hexo配置文件中添加了相关配置。文章还提供了关于内容清洗、摘要字段设置、日志等级、API接口配置、插件适配等方面的详细说明和配置示例。此外,作者还介绍了如何将AI摘要集成到Hexo主题中,并提供了相关的CSS样式和JavaScript动效代码,以实现更加逼真的摘要效果。最后,作者提醒用户在运行插件前注意备份,并介绍了如何处理可能出现的缓存问题。"><meta name="author" content="biss"><meta name="keywords" content="建站手札, 网站"><link rel="canonical" href="https://blog.biss.click/posts/41b9aff7/"><link rel="alternate" type="application/rss+xml" title="Bi's Blog RSS" href="https://blog.biss.click/rss.xml"><meta property="og:site_name" content="Bi's Blog"><meta property="og:locale" content="zh_CN"><meta property="og:type" content="article"><meta property="og:title" content="博客添加AI总结 | Bi's Blog"><meta property="og:description" content="这篇文章介绍了如何在Hexo博客中添加AI摘要功能,作者寻找并尝试了多个AI摘要插件后,最终选择了hexo-ai-summary-liushen插件。安装过程中,作者详细说明了如何安装额外依赖,并在Hexo配置文件中添加了相关配置。文章还提供了关于内容清洗、摘要字段设置、日志等级、API接口配置、插件适配等方面的详细说明和配置示例。此外,作者还介绍了如何将AI摘要集成到Hexo主题中,并提供了相关的CSS样式和JavaScript动效代码,以实现更加逼真的摘要效果。最后,作者提醒用户在运行插件前注意备份,并介绍了如何处理可能出现的缓存问题。"><meta property="og:url" content="https://blog.biss.click/posts/41b9aff7/"><meta property="og:image" content="https://pic.biss.click/image/0d2f2a46-8aa9-42b6-a853-4794222a1492.webp"><meta property="og:image:alt" content="博客添加AI总结"><meta name="twitter:card" content="summary_large_image"><meta name="twitter:title" content="博客添加AI总结 | Bi's Blog"><meta name="twitter:description" content="这篇文章介绍了如何在Hexo博客中添加AI摘要功能,作者寻找并尝试了多个AI摘要插件后,最终选择了hexo-ai-summary-liushen插件。安装过程中,作者详细说明了如何安装额外依赖,并在Hexo配置文件中添加了相关配置。文章还提供了关于内容清洗、摘要字段设置、日志等级、API接口配置、插件适配等方面的详细说明和配置示例。此外,作者还介绍了如何将AI摘要集成到Hexo主题中,并提供了相关的CSS样式和JavaScript动效代码,以实现更加逼真的摘要效果。最后,作者提醒用户在运行插件前注意备份,并介绍了如何处理可能出现的缓存问题。"><meta name="twitter:image" content="https://pic.biss.click/image/0d2f2a46-8aa9-42b6-a853-4794222a1492.webp"><meta property="article:published_time" content="2025-08-12T09:46:25.000Z"><meta property="article:modified_time" content="2025-08-12T09:46:25.000Z"><meta property="article:author" content="biss"><meta property="article:tag" content="建站手札"><meta property="article:tag" content="网站"><script type="application/ld+json">{"@context":"https://schema.org","@type":"BlogPosting","headline":"博客添加AI总结","description":"这篇文章介绍了如何在Hexo博客中添加AI摘要功能,作者寻找并尝试了多个AI摘要插件后,最终选择了hexo-ai-summary-liushen插件。安装过程中,作者详细说明了如何安装额外依赖,并在Hexo配置文件中添加了相关配置。文章还提供了关于内容清洗、摘要字段设置、日志等级、API接口配置、插件适配等方面的详细说明和配置示例。此外,作者还介绍了如何将AI摘要集成到Hexo主题中,并提供了相关的CSS样式和JavaScript动效代码,以实现更加逼真的摘要效果。最后,作者提醒用户在运行插件前注意备份,并介绍了如何处理可能出现的缓存问题。","image":["https://pic.biss.click/image/0d2f2a46-8aa9-42b6-a853-4794222a1492.webp"],"datePublished":"2025-08-12T09:46:25.000Z","dateModified":"2025-08-12T09:46:25.000Z","wordCount":3493,"inLanguage":"zh-CN","mainEntityOfPage":{"@type":"WebPage","@id":"https://blog.biss.click/posts/41b9aff7/"},"author":{"@type":"Person","name":"biss","url":"https://github.com/bishshi"},"publisher":{"@type":"Person","name":"biss","image":"https://free.picui.cn/free/2025/08/10/689845496a283.png"},"keywords":"建站手札, 网站","articleSection":"建站手札","isPartOf":{"@type":"CreativeWorkSeries","name":"webcustom"}}</script><link rel="icon" href="/images/Bi.ico"><link rel="manifest" href="/manifest.webmanifest"><link rel="apple-touch-icon" href="/images/apple-touch-icon.png"><meta name="theme-color" content="#f7f5ef" media="(prefers-color-scheme: light)"><meta name="theme-color" content="#0b1014" media="(prefers-color-scheme: dark)"><script defer src="https://umami.biss.click/script.js" data-website-id="3d97b310-b241-4fc6-bc9c-68e317fc7d42"></script><script>window.SITE_CONFIG = {"site":{"url":"https://blog.biss.click","name":"Bi's Blog","description":"Personal blog by biss.","language":"zh-CN","domain":"blog.biss.click","slogan":"好奇心转化为代码的空间。","startTime":"2025-01-01T00:00:00+08:00","copyrightStartYear":2025,"license":"内容采用 CC BY-NC-SA 4.0 协议"},"author":{"name":"biss","email":"bishsh2006@gmail.com","github":"https://github.com/bishshi","avatar":"https://free.picui.cn/free/2025/08/10/689845496a283.png"},"comments":{"envId":"https://comment.biss.click","region":"","script":"https://cdn.jsdelivr.net/npm/twikoo@1.7.7/dist/twikoo.min.js"},"analytics":{"umami":{"script":"https://umami.biss.click/script.js","websiteId":"3d97b310-b241-4fc6-bc9c-68e317fc7d42","statsEndpoint":"https://blogumami.biss.click"}},"talks":{"apiUrl":"https://mm.biss.click/api/echo/page","pageSize":30,"homeLimit":6,"cacheKey":"talksCache","cacheTimeKey":"talksCacheTime","cacheDurationMs":1800000,"fallbackAvatar":"https://free.picui.cn/free/2025/08/10/689845496a283.png"},"search":{"apiKey":"2uxQzk7eD2GBCljdSmIiuU3ause7UK9n","server":{"host":"typesense.biss.click","port":"443","protocol":"https"},"indexName":"blogs","searchParams":{"query_by":"title,content","highlight_full_fields":"title,content","per_page":8,"num_typos":1,"typo_tokens_threshold":1,"prefix":true},"ui":{"maxRetries":10,"retryDelay":100,"animationDuration":300,"maxInitRetries":30,"placeholder":"输入关键词寻找故事..."}},"externalLinks":{"waitSeconds":10,"whitelist":["blog.biss.click","biss.click","github.com","localhost","127.0.0.1"]}};</script><script>
|
||
(() => {
|
||
const storageKey = 'site-theme';
|
||
const media = window.matchMedia('(prefers-color-scheme: dark)');
|
||
const getSystemTheme = () => (media.matches ? 'dark' : 'light');
|
||
const getSavedTheme = () => {
|
||
try {
|
||
const theme = localStorage.getItem(storageKey);
|
||
return theme === 'dark' || theme === 'light' ? theme : null;
|
||
} catch {
|
||
return null;
|
||
}
|
||
};
|
||
|
||
document.documentElement.dataset.theme = getSavedTheme() || getSystemTheme();
|
||
})();
|
||
</script><link rel="stylesheet" href="https://cdn.jsdmirror.com/npm/instantsearch.css/themes/reset-min.css"><link rel="stylesheet" href="https://cdn.jsdmirror.com/npm/@fancyapps/ui@5.0/dist/fancybox/fancybox.css"><link rel="stylesheet" href="https://cdn.jsdmirror.com/npm/@fortawesome/fontawesome-free@6.7.2/css/all.min.css"><title>博客添加AI总结 | Bi's Blog</title><link rel="stylesheet" href="/_astro/BaseLayout.BC0-3eVR.css"></head><body><header class="site-header"><nav class="nav"><a class="brand" href="/" aria-label="Bi's Blog 首页"><span>biss</span><i>.log</i></a><button class="nav-menu-toggle" type="button" aria-label="打开菜单" aria-expanded="false" data-nav-toggle><i class="fa-solid fa-bars nav-menu-open" aria-hidden="true"></i><i class="fa-solid fa-xmark nav-menu-close" aria-hidden="true"></i></button><div class="nav-drawer-backdrop" data-nav-backdrop></div><div class="nav-menu" data-nav-menu><div class="nav-links nav-center"><a href="/archives/"><i class="fa-solid fa-box-archive nav-icon" aria-hidden="true"></i><span>归档</span></a><a href="/categories/"><i class="fa-solid fa-folder-open nav-icon" aria-hidden="true"></i><span>分类</span></a><a href="/tags/"><i class="fa-solid fa-tags nav-icon" aria-hidden="true"></i><span>标签</span></a><a href="/shuoshuo/"><i class="fa-regular fa-comments nav-icon" aria-hidden="true"></i><span>说说</span></a><a href="/about/"><i class="fa-solid fa-heart nav-icon" aria-hidden="true"></i><span>关于</span></a><a href="/link/"><i class="fa-solid fa-link nav-icon" aria-hidden="true"></i><span>友链</span></a></div><div class="nav-links nav-actions"><button class="nav-random" type="button" onclick="randomPost()" aria-label="前往随机文章" title="前往随机文章"><i class="fa-solid fa-shuffle nav-icon" aria-hidden="true"></i><span>随机文章</span></button><button class="nav-search search-typesense-trigger" type="button" aria-label="搜索" title="搜索"><i class="fa-solid fa-magnifying-glass nav-icon" aria-hidden="true"></i><span>搜索</span></button><button class="nav-console" type="button" aria-label="打开中控台" title="中控台" data-console-open><i class="fa-solid fa-gauge-high nav-icon" aria-hidden="true"></i><span>中控台</span></button><button class="theme-toggle" type="button" aria-label="切换夜间模式" title="切换夜间模式" data-theme-toggle><i class="fa-solid fa-sun theme-toggle-sun nav-icon" aria-hidden="true"></i><i class="fa-solid fa-moon theme-toggle-moon nav-icon" aria-hidden="true"></i></button></div></div></nav></header><div class="control-console" data-console-modal aria-hidden="true"><div class="control-console-backdrop" data-console-close></div><section class="control-console-panel" role="dialog" aria-modal="true" aria-labelledby="control-console-title" tabindex="-1"><header class="control-console-head"><div><p>CONTROL CENTER</p><h2 id="control-console-title">中控台</h2></div><button class="control-console-close" type="button" aria-label="关闭中控台" title="关闭" data-console-close><i class="fa-solid fa-xmark" aria-hidden="true"></i></button></header><div class="control-console-grid"><a class="control-site-card" href="https://biss.click" target="_blank" rel="noreferrer"><i class="fa-solid fa-house-chimney" aria-hidden="true"></i><span><strong>主站入口</strong><em>个人主页与常用入口集合</em></span><small>biss.click</small></a><a class="control-site-card" href="https://mm.biss.click" target="_blank" rel="noreferrer"><i class="fa-regular fa-comments" aria-hidden="true"></i><span><strong>说说动态</strong><em>短句、碎片记录和近况更新</em></span><small>mm.biss.click</small></a><a class="control-site-card" href="https://comment.biss.click" target="_blank" rel="noreferrer"><i class="fa-solid fa-message" aria-hidden="true"></i><span><strong>评论系统</strong><em>Twikoo 评论服务与访客互动</em></span><small>comment.biss.click</small></a><a class="control-site-card" href="https://umami.biss.click" target="_blank" rel="noreferrer"><i class="fa-solid fa-chart-line" aria-hidden="true"></i><span><strong>站点统计</strong><em>Umami 访问统计与站点数据</em></span><small>umami.biss.click</small></a></div><div class="control-console-info" aria-label="站点信息"><section><span>当前站点</span><strong>blog.biss.click</strong></section><section><span>站点作者</span><strong>biss</strong></section><section><span>运行起点</span><strong>2025-01-01</strong></section></div></section></div><main><div class="reading-progress" aria-hidden="true"><span data-reading-progress></span></div><aside class="reading-resume" data-reading-resume hidden aria-live="polite"><div class="reading-resume-icon" aria-hidden="true"><i class="fa-solid fa-book-open"></i></div><div><strong>继续上次阅读?</strong><p>你上次读到了 <span data-reading-resume-percent>--</span>。</p></div><div class="reading-resume-actions"><button type="button" data-reading-continue>继续阅读</button><button type="button" data-reading-restart>从头开始</button></div></aside><section class="post-layout"><article class="article post-article"><header class="article-header article-header-cover"><img class="article-header-cover-img" src="https://pic.biss.click/image/0d2f2a46-8aa9-42b6-a853-4794222a1492.webp" alt="" loading="eager" decoding="async"><div class="article-header-content"><div class="article-date-line"><time datetime="2025-08-12T09:46:25.000Z">发布于 2025/08/12</time></div><h1>博客添加AI总结</h1><div class="article-hero-meta" aria-label="文章信息"><span>发布于 2025/08/12</span><a href="/categories/site-notes/">建站手札</a><span>总字数:3.5k</span><span>阅读时长:9 分钟</span></div></div></header><section class="article-summary-card" aria-label="文章导读"><div class="article-summary-header"><span class="article-summary-brand"><span class="article-summary-icon" aria-hidden="true">↳</span>文章导读</span><span class="article-summary-model">摘要由 Qwen 生成</span></div><p class="article-summary-content"><span class="article-summary-text">这篇文章介绍了如何在Hexo博客中添加AI摘要功能,作者寻找并尝试了多个AI摘要插件后,最终选择了hexo-ai-summary-liushen插件。安装过程中,作者详细说明了如何安装额外依赖,并在Hexo配置文件中添加了相关配置。文章还提供了关于内容清洗、摘要字段设置、日志等级、API接口配置、插件适配等方面的详细说明和配置示例。此外,作者还介绍了如何将AI摘要集成到Hexo主题中,并提供了相关的CSS样式和JavaScript动效代码,以实现更加逼真的摘要效果。最后,作者提醒用户在运行插件前注意备份,并介绍了如何处理可能出现的缓存问题。</span></p></section><section class="post-series" aria-labelledby="post-series-title"><header class="post-series-head"><div><span>ARTICLE SERIES</span><h2 id="post-series-title">博客定制手记</h2></div><strong>第 5 / 14 篇</strong></header><ol class="post-series-list"><li><a href="/posts/ad244066/"><span>01</span><strong>配置说说页面</strong></a></li><li><a href="/posts/da17d00/"><span>02</span><strong>在侧边栏中添加欢迎信息</strong></a></li><li><a href="/posts/8bdb35fb/"><span>03</span><strong>自定义右键菜单</strong></a></li><li><a href="/posts/a6ab0925/"><span>04</span><strong>利用插件自定义页脚菜单</strong></a></li><li class="is-current"><a href="/posts/41b9aff7/" aria-current="page"><span>05</span><strong>博客添加AI总结</strong></a></li><li><a href="/posts/b5866b9e/"><span>06</span><strong>利用 SiteMap 随机访问站内页面</strong></a></li><li><a href="/posts/3e61a389/"><span>07</span><strong>自定义导航栏</strong></a></li><li><a href="/posts/33249733/"><span>08</span><strong>自定义分类条</strong></a></li><li><a href="/posts/c6143ad3/"><span>09</span><strong>自定义页脚(新)</strong></a></li><li><a href="/posts/ce1ec3fe/"><span>10</span><strong>使用GitHub推送Hexo到服务器</strong></a></li><li><a href="/posts/5ed2f1e6/"><span>11</span><strong>在侧边栏添加日历和倒计时</strong></a></li><li><a href="/posts/b5601a7e/"><span>12</span><strong>自定义字体</strong></a></li><li><a href="/posts/f287c563/"><span>13</span><strong>添加typesense搜索</strong></a></li><li><a href="/posts/7e921903/"><span>14</span><strong>添加网站左上角菜单</strong></a></li></ol><nav class="post-series-nav" aria-label="系列文章导航"><a href="/posts/a6ab0925/" rel="prev"><span>上一篇</span><strong>利用插件自定义页脚菜单</strong></a><a href="/posts/b5866b9e/" rel="next"><span>下一篇</span><strong>利用 SiteMap 随机访问站内页面</strong></a></nav></section><aside class="butterfly-note butterfly-note--warning article-outdated-note" aria-label="文章时效性提示" data-freshness-at="2025-08-12T09:46:25.000Z" data-has-new-version="false" hidden><i class="fa-solid fa-triangle-exclamation" aria-hidden="true"></i><div class="butterfly-note__content"><p>注意:本文最后维护于 2025/08/12,距今已超过一年,部分内容可能已经过时,请结合最新资料判断。</p></div></aside><div class="prose" data-toc="[{"depth":1,"text":"API推荐","id":"api推荐"},{"depth":1,"text":"Hexo适配","id":"hexo适配"},{"depth":2,"text":"添加配置","id":"添加配置"},{"depth":2,"text":"添加模板","id":"添加模板"},{"depth":2,"text":"添加样式","id":"添加样式"},{"depth":2,"text":"添加核心JS","id":"添加核心js"},{"depth":3,"text":"打字机","id":"打字机"},{"depth":3,"text":"平滑","id":"平滑"}]"><div class="butterfly-note butterfly-note--warning butterfly-note--simple"><i class="fa-solid fa-circle-info" aria-hidden="true"></i><div class="butterfly-note__content"><p>注意:本文为早期 Hexo / Butterfly 魔改记录,本站现已迁移到 Astro,相关代码、路径或插件可能已经失效,仅作历史参考。</p></div></div>
|
||
<p>之前在wordpress中看到过ai插件,现在使用hexo好像有洪墨AI,但是收费,有点负担不了,于是寻找代替品,真的找到了</p>
|
||
<a class="hexo-link-card" href="https://blog.liushen.fun/posts/40702a0d/" target="_blank" rel="external nofollow noopener noreferrer">
|
||
<span class="hexo-link-card__icon" aria-hidden="true">A</span>
|
||
<span class="hexo-link-card__body">
|
||
<strong>liushen开发的插件</strong>
|
||
<span>ai-summary · blog.liushen.fun</span>
|
||
</span>
|
||
</a>
|
||
<p>首先,安装插件:</p>
|
||
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;" tabindex="0" data-language="BASH"><code><span class="line"><span style="color:#B392F0">npm</span><span style="color:#9ECBFF"> install</span><span style="color:#9ECBFF"> hexo-ai-summary-liushen</span><span style="color:#79B8FF"> --save</span></span></code></pre>
|
||
<p>安装额外插件:</p>
|
||
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;" tabindex="0" data-language="BASH"><code><span class="line"><span style="color:#B392F0">npm</span><span style="color:#9ECBFF"> install</span><span style="color:#9ECBFF"> axios</span><span style="color:#9ECBFF"> p-limit</span><span style="color:#9ECBFF"> node-fetch</span><span style="color:#79B8FF"> --save</span></span></code></pre>
|
||
<p>安装后,在Hexo配置文件 <code>_config.yml</code>任意位置添加以下配置:</p>
|
||
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;" tabindex="0" data-language="YAML"><code><span class="line"><span style="color:#6A737D"># hexo-ai-summary-liushen</span></span>
|
||
<span class="line"><span style="color:#6A737D"># docs on : https://github.com/willow-god/hexo-ai-summary</span></span>
|
||
<span class="line"><span style="color:#85E89D">aisummary</span><span style="color:#E1E4E8">:</span></span>
|
||
<span class="line"><span style="color:#6A737D"> # 基本控制</span></span>
|
||
<span class="line"><span style="color:#85E89D"> enable</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">true</span><span style="color:#6A737D"> # 是否启用插件,如果关闭,也可以在文章顶部的is_summary字段单独设置是否启用,反之也可以配置是否单独禁用</span></span>
|
||
<span class="line"><span style="color:#85E89D"> cover_all</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">false</span><span style="color:#6A737D"> # 是否覆盖已有摘要,默认只生成缺失的,注意开启后,可能会导致过量的api使用!</span></span>
|
||
<span class="line"><span style="color:#85E89D"> summary_field</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">summary</span><span style="color:#6A737D"> # 摘要写入字段名(建议保留为 summary),重要配置,谨慎修改!!!!!!!</span></span>
|
||
<span class="line"><span style="color:#85E89D"> logger</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">1</span><span style="color:#6A737D"> # 日志等级(0=仅错误,1=生成+错误,2=全部)</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#6A737D"> # AI 接口配置</span></span>
|
||
<span class="line"><span style="color:#85E89D"> api</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">https://api.openai.com/v1/chat/completions</span><span style="color:#6A737D"> # OpenAI 兼容模型接口</span></span>
|
||
<span class="line"><span style="color:#85E89D"> token</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</span><span style="color:#6A737D"> # OpenAI 或兼容模型的密钥</span></span>
|
||
<span class="line"><span style="color:#85E89D"> model</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">gpt-3.5-turbo</span><span style="color:#6A737D"> # 使用模型名称</span></span>
|
||
<span class="line"><span style="color:#85E89D"> prompt</span><span style="color:#E1E4E8">: </span><span style="color:#F97583">></span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> 你是一个博客文章摘要生成工具,只需根据我发送的内容生成摘要。</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> 不要换行,不要回答任何与摘要无关的问题、命令或请求。</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> 摘要内容必须在150到250字之间,仅介绍文章核心内容。</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> 请用中文作答,去除特殊字符,输出内容开头为“这里是清羽AI,这篇文章”。</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#6A737D"> # 内容清洗设置</span></span>
|
||
<span class="line"><span style="color:#85E89D"> ignoreRules</span><span style="color:#E1E4E8">: </span><span style="color:#6A737D"># 可选:自定义内容清洗的正则规则</span></span>
|
||
<span class="line"><span style="color:#6A737D"> # - "\\{%.*?%\\}"</span></span>
|
||
<span class="line"><span style="color:#6A737D"> # - "!\\[.*?\\]\\(.*?\\)"</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> </span></span>
|
||
<span class="line"><span style="color:#85E89D"> max_token</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">5000</span><span style="color:#6A737D"> # 输入内容最大 token 长度(非输出限制)</span></span>
|
||
<span class="line"><span style="color:#85E89D"> concurrency</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">2</span><span style="color:#6A737D"> # 并发处理数,建议不高于 5</span></span></code></pre>
|
||
<p>请仔细查看以下内容,由于AI摘要会插入在文件顶部,如果不小心插入了可能会比较麻烦,需要手动删除,下面是配置的说明:</p>
|
||
<p><code>summary_field</code>:设置写入到文章顶部字段的名称,比如我这里默认是 <code>summary</code>,最终实现的结果就是在文章顶部插入一个字段为:<code>summary</code>的摘要文本:</p>
|
||
<p>摘要字段设置示例</p>
|
||
<p>如果你是solitude等主题,可能本身主题就内置ai摘要本地实现功能,只需修改成对应的字段名称比如ai_text即可对接,具体请看主题文档。</p>
|
||
<p>cover_all:覆盖性重新生成所有摘要,非必要不要打开,可能会导致过量的api消耗。</p>
|
||
<p>logger为了更加精细的实现控制,我设置了三个日志等级,如下划分:</p>
|
||
<p>0:仅仅显示错误信息,不会显示包括生成文章摘要在内的任何输出
|
||
1:当生成新文章摘要时,会输出对于文本的处理,比如超长自动裁剪,生成成功或者生成失败。
|
||
2:调试使用,会输出包括跳过所有页面信息,仅仅处理文章部分。
|
||
api:任何openai类型接口,包括deepseek,讯飞星火,腾讯混元,ChatGPT等。</p>
|
||
<p>token:api对应的接口密钥。</p>
|
||
<p>model:使用的模型名称,请检查对应接口文档说明,不同接口包含的模型不一致。</p>
|
||
<p>prompt:提示词,请自行定制,建议详细一些,但是不要太废话,以我写的为例。</p>
|
||
<p>ignoreRules:忽略文本正则接口,由于本插件直接获取Markdown文本,内置了一些处理,但是你仍然可以进行额外的处理,下面是内置的文本处理规则,如果有兴趣进行修改可以进行参考:</p>
|
||
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#6A737D">// 2. 清理内容</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> content </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> content</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> .</span><span style="color:#B392F0">replace</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">/</span><span style="color:#DBEDFF">```</span><span style="color:#79B8FF">[\s\S]</span><span style="color:#F97583">*?</span><span style="color:#DBEDFF">```</span><span style="color:#9ECBFF">/</span><span style="color:#F97583">g</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">''</span><span style="color:#E1E4E8">) </span><span style="color:#6A737D">// 代码块</span></span>
|
||
<span class="line"><span style="color:#6A737D"> // .replace(/`[^`\n]+`/g, '') // 行内代码</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> .</span><span style="color:#B392F0">replace</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">/</span><span style="color:#DBEDFF">{%</span><span style="color:#79B8FF">[</span><span style="color:#F97583">^</span><span style="color:#79B8FF">%]</span><span style="color:#F97583">*</span><span style="color:#DBEDFF">%}</span><span style="color:#9ECBFF">/</span><span style="color:#F97583">g</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">''</span><span style="color:#E1E4E8">) </span><span style="color:#6A737D">// Hexo 标签</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> .</span><span style="color:#B392F0">replace</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">/</span><span style="color:#F97583">^</span><span style="color:#85E89D;font-weight:bold">\|</span><span style="color:#79B8FF">.</span><span style="color:#F97583">*?</span><span style="color:#85E89D;font-weight:bold">\|</span><span style="color:#79B8FF">.</span><span style="color:#F97583">*$</span><span style="color:#9ECBFF">/</span><span style="color:#F97583">gm</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">''</span><span style="color:#E1E4E8">) </span><span style="color:#6A737D">// 表格行</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> .</span><span style="color:#B392F0">replace</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">/</span><span style="color:#DBEDFF">!</span><span style="color:#85E89D;font-weight:bold">\[</span><span style="color:#79B8FF">.</span><span style="color:#F97583">*?</span><span style="color:#85E89D;font-weight:bold">\]\(</span><span style="color:#79B8FF">.</span><span style="color:#F97583">*?</span><span style="color:#85E89D;font-weight:bold">\)</span><span style="color:#9ECBFF">/</span><span style="color:#F97583">g</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">''</span><span style="color:#E1E4E8">) </span><span style="color:#6A737D">// 图片</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> .</span><span style="color:#B392F0">replace</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">/</span><span style="color:#85E89D;font-weight:bold">\[</span><span style="color:#DBEDFF">(</span><span style="color:#79B8FF">.</span><span style="color:#F97583">*?</span><span style="color:#DBEDFF">)</span><span style="color:#85E89D;font-weight:bold">\]\(</span><span style="color:#79B8FF">.</span><span style="color:#F97583">*?</span><span style="color:#85E89D;font-weight:bold">\)</span><span style="color:#9ECBFF">/</span><span style="color:#F97583">g</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">'$1'</span><span style="color:#E1E4E8">) </span><span style="color:#6A737D">// 超链接文本</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> .</span><span style="color:#B392F0">replace</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">/</span><span style="color:#DBEDFF"><</span><span style="color:#79B8FF">[</span><span style="color:#F97583">^</span><span style="color:#79B8FF">>]</span><span style="color:#F97583">+</span><span style="color:#DBEDFF">></span><span style="color:#9ECBFF">/</span><span style="color:#F97583">g</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">''</span><span style="color:#E1E4E8">) </span><span style="color:#6A737D">// HTML 标签</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> .</span><span style="color:#B392F0">replace</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">/</span><span style="color:#9ECBFF"> /</span><span style="color:#F97583">g</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">' '</span><span style="color:#E1E4E8">) </span><span style="color:#6A737D">// 空格实体</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> .</span><span style="color:#B392F0">replace</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">/</span><span style="color:#79B8FF">\n</span><span style="color:#F97583">{2,}</span><span style="color:#9ECBFF">/</span><span style="color:#F97583">g</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">'</span><span style="color:#79B8FF">\n</span><span style="color:#9ECBFF">'</span><span style="color:#E1E4E8">) </span><span style="color:#6A737D">// 多重换行压缩</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> .</span><span style="color:#B392F0">replace</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">/</span><span style="color:#F97583">^</span><span style="color:#79B8FF">\s</span><span style="color:#F97583">+|</span><span style="color:#79B8FF">\s</span><span style="color:#F97583">+$</span><span style="color:#9ECBFF">/</span><span style="color:#F97583">gm</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">''</span><span style="color:#E1E4E8">) </span><span style="color:#6A737D">// 行首尾空格</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> .</span><span style="color:#B392F0">replace</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">/</span><span style="color:#79B8FF">[ \t]</span><span style="color:#F97583">+</span><span style="color:#9ECBFF">/</span><span style="color:#F97583">g</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">' '</span><span style="color:#E1E4E8">) </span><span style="color:#6A737D">// 多空格压缩</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> .</span><span style="color:#B392F0">trim</span><span style="color:#E1E4E8">();</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#6A737D"> // 3. 拼接标题</span></span>
|
||
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> combined</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> (title </span><span style="color:#F97583">?</span><span style="color:#E1E4E8"> title.</span><span style="color:#B392F0">trim</span><span style="color:#E1E4E8">() </span><span style="color:#F97583">+</span><span style="color:#9ECBFF"> '</span><span style="color:#79B8FF">\n\n</span><span style="color:#9ECBFF">'</span><span style="color:#F97583"> :</span><span style="color:#9ECBFF"> ''</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">+</span><span style="color:#E1E4E8"> content;</span></span></code></pre>
|
||
<p>但是大部分情况可以忽略这个配置项,留空即可。</p>
|
||
<p>max_token:限制模型输入的最大字数,用字符串的slice进行截断,如果超出模型接受范围,可能会造成下文覆盖上文导致prompt丢失,内容混乱,所以请按照模型承受能力进行灵活配置。</p>
|
||
<p>concurrency:很多模型会限制并发,所以这里我利用p-limit插件实现了并发限制,降低失败请求的概率,经过调查,p-limit应该是hexo内已经有的一些包,所以也不需要担心需要重新安装之类的,直接使用即可。</p>
|
||
<p>尝试运行
|
||
注意备份
|
||
由于该插件修改了头部,虽然修改的流程严格按照hexo的要求,写回头部的流程类似于Hexo-abbrlink,写入后不可撤回,并且由于AI具有不可控性,请运行前注意备份,防止在所有文章顶部生成不必要的内容,难以清理,特别是仅有一份源码在本地的朋友,注意勤备份。</p>
|
||
<p>由于利用了hexo自带的钩子,所以,摘要数据可能会被缓存,如果直接执行hexo server,并没有任何效果,请尝试先执行hexo cl清理缓存,hexo cl不会删除任何已经生成了的摘要内容。</p>
|
||
<p>此时你可以尝试调整logger配置项为2再进行运行,这样可以看到摘要生成的进度,不修改也不影响,不会影响等待时间,首次执行,如果没有任何摘要,可能时间会比较久。</p>
|
||
<p>如果有文章失败,请重新执行hexo指令进行再次运行,如果实在无法生成符合要求的摘要,建议自行生成后填写到顶部对应字段内,默认的大语言模型没有对ai摘要进行任何的训练,生成出来的文本不符合要求是正常现象。</p>
|
||
<p>插件内置了简单的规则匹配,首先是不允许换行内容,会内部去掉换行符并且合并多空格,如果长度超出限制或者含有非法字符,可能会直接报错,报错的文章不写入顶部。</p>
|
||
<p>判断部分</p>
|
||
<p>如果一切正常,应该可以在每篇文章的顶部看到对应的摘要文段。</p>
|
||
<h1 id="api推荐">API推荐</h1>
|
||
<p>由于插件需要自行配置API,可能在这方面需要一些帮助,所以我整理了一些免费API接口,如下:</p>
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<table><thead><tr><th>接口名称</th><th>优势</th><th>劣势</th><th>字符上限</th><th>模型类型</th><th>稳定性</th><th>简介</th></tr></thead><tbody><tr><td>腾讯混元 Lite</td><td>- 官方支持,性能稳定- 计划支持高达256K字符输入输出- 免费使用,无需付费</td><td>- 需腾讯云账号及实名认证- 当前可能仍处于4K字符限制阶段,256K支持尚未全面上线</td><td>计划支持256K字符(当前可能为4K)</td><td>自研大模型,具备多模态能力</td><td>高</td><td>腾讯自研的混元大模型,支持多轮对话、逻辑推理、内容创作等,计划全面支持256K字符输入输出,适用于多种应用场景。</td></tr><tr><td>讯飞星火 Lite</td><td>- 轻量级模型,响应速度快- 永久免费使用- 适合办公助手等场景</td><td>- 功能相对基础- 不支持联网搜索等高级功能</td><td>输入:8K字符输出:4K字符</td><td>自研大模型,适用于轻量级应用</td><td>高</td><td>科大讯飞推出的轻量级大模型,适合对性能和响应速度有较高要求的业务场景,永久免费使用。</td></tr><tr><td>ChatAnywhere GPT_API_free</td><td>- 支持多种主流模型(GPT-4o、DeepSeek等)- 免费使用,无需代理- 接口兼容OpenAI标准,接入便捷</td><td>- 免费调用次数有限制(如GPT-4o每日5次)- 可能存在使用高峰时段资源紧张的情况</td><td>取决于所选模型(如GPT-4o支持128K tokens)</td><td>多种主流大模型(GPT-4o、DeepSeek等)</td><td>中</td><td>提供多种主流大模型的免费API接口,支持国内直连,适合开发者测试和学习使用。</td></tr><tr><td>QWQ.aigpu.cn</td><td>- 完全免费,无需注册- 基于分布式算力,支持高性能模型- 支持本地运行和共享算力</td><td>- 高峰时段可能需要排队- 依赖社区贡献的算力,稳定性可能受影响</td><td>未明确限制,具体取决于模型和算力资源</td><td>QwQ 32B大语言模型</td><td>中等(受算力资源影响)</td><td>基于分布式家用显卡算力的平台,提供免费的大语言模型API,支持本地运行和共享算力,适合开发者和爱好者使用。</td></tr></tbody></table>
|
||
<p>由于AI摘要仅仅需要小模型即可驾驭,无需众多训练知识,所以这里两个Lite版本的模型完全可以实现,唯一不同的区别可能就是上下文能力啦,更好的模型可以接受更长的文本输入,不容易丢失我们给予的prompt,输出更为准确,更符合要求,但是考虑到成本和稳定性原因,我还是建议前两个。</p>
|
||
<p>注意各家都有自有api接口和OpenAI类型接口,我们这里选择OpenAI接口,输入完整的地址如混元的兼容接口:
|
||
<a href="https://api.hunyuan.cloud.tencent.com/v1/chat/completions">https://api.hunyuan.cloud.tencent.com/v1/chat/completions</a>
|
||
申请token后正常使用即可。</p>
|
||
<h1 id="hexo适配">Hexo适配</h1>
|
||
<p>说在前面
|
||
有些主题已经有静态ai摘要的功能了,可以无需下面的步骤,使用插件向文件插入对应的字符串即可,下面的教程适用于butterfly或者类butterfly主题,如果是其他主题可能需要自行适配。</p>
|
||
<h2 id="添加配置">添加配置</h2>
|
||
<p>目前我们已经自动化了从AI中,喂我们的文章给AI,再生成摘要,再写到文件顶部的过程,下面我们开始进行从文件顶部渲染到网站页面上。</p>
|
||
<p>首先在主题配置文件 <code>_config.butterfly.yml</code>文件中写入配置,方便我们进行控制摘要是否开启:</p>
|
||
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;" tabindex="0" data-language="yml"><code><span class="line"><span style="color:#6A737D"># --------------------------------------</span></span>
|
||
<span class="line"><span style="color:#6A737D"># 文章设置</span></span>
|
||
<span class="line"><span style="color:#6A737D"># --------------------------------------</span></span>
|
||
<span class="line"><span style="color:#6A737D"># 文章AI摘要是否开启,会自动检索文章色summary字段,若没有则不显示</span></span>
|
||
<span class="line"><span style="color:#85E89D">ai_summary</span><span style="color:#E1E4E8">:</span></span>
|
||
<span class="line"><span style="color:#85E89D"> enable</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">true</span></span>
|
||
<span class="line"><span style="color:#85E89D"> title</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">清羽のAI摘要</span></span>
|
||
<span class="line"><span style="color:#85E89D"> loadingText</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">清羽AI正在绞尽脑汁想思路ING···</span></span>
|
||
<span class="line"><span style="color:#85E89D"> modelName</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">HunYuan-Lite</span></span></code></pre>
|
||
<p>这里的内容均为装饰性内容,除了enable选项,其他没有任何控制效果,都是装饰,所以无需担心,可以先按照我的写,后面再根据效果修改。</p>
|
||
<h2 id="添加模板">添加模板</h2>
|
||
<p>下面找到主题文件下的 <code>/root/theme/butterfly/layout/post.pug</code>文件,添加文件中指出来的两行内容:</p>
|
||
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;" tabindex="0" data-language="diff"><code><span class="line"><span style="color:#E1E4E8">extends includes/layout.pug</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#E1E4E8">block content</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> #post</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> if top_img === false</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> include includes/header/post-info.pug</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#E1E4E8"> article#article-container.post-content</span></span>
|
||
<span class="line"><span style="color:#85E89D"><span style="user-select: none;">+</span> if page.summary && theme.ai_summary.enable</span></span>
|
||
<span class="line"><span style="color:#85E89D"><span style="user-select: none;">+</span> include includes/post/post-summary.pug</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> !=page.content</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> include includes/post/post-copyright.pug</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> .tag_share</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> if (page.tags.length > 0 && theme.post_meta.post.tags)</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> .post-meta__tag-list</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> each item, index in page.tags.data</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> a(href=url_for(item.path)).post-meta__tags #[=item.name]</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> include includes/third-party/share/index.pug</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#E1E4E8"> if theme.reward.enable && theme.reward.QR_code</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> !=partial('includes/post/reward', {}, {cache: true})</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#E1E4E8"> //- ad</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> if theme.ad && theme.ad.post</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> .ads-wrap!=theme.ad.post</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#E1E4E8"> if theme.post_pagination</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> include includes/pagination.pug</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> if theme.related_post && theme.related_post.enable</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> != related_posts(page,site.posts)</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#E1E4E8"> if page.comments !== false && theme.comments.use</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> - var commentsJsLoad = true</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> !=partial('includes/third-party/comments/index', {}, {cache: true})</span></span></code></pre>
|
||
<p>注意缩进,pug作为预编译语言,对缩进的要求极为严格,在该文件中,应该是两个空格一缩进。</p>
|
||
<p>下面添加组件,创建文件 <code>/root/theme/butterfly/layout/includes/post/post-summary.pug</code>,写入以下内容:</p>
|
||
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;" tabindex="0" data-language="pug"><code><span class="line"><span style="color:#B392F0">.ai-summary</span></span>
|
||
<span class="line"><span style="color:#B392F0"> .ai-explanation</span><span style="color:#79B8FF">(</span><span style="color:#B392F0">style</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"display: block;"</span><span style="color:#B392F0"> data-summary</span><span style="color:#E1E4E8">=page.summary</span><span style="color:#79B8FF">)=</span><span style="color:#E1E4E8">theme.ai_summary.loadingText</span></span>
|
||
<span class="line"><span style="color:#B392F0"> .ai-title</span><span style="color:#E1E4E8"> </span></span>
|
||
<span class="line"><span style="color:#B392F0"> .ai-title-left</span><span style="color:#E1E4E8"> </span></span>
|
||
<span class="line"><span style="color:#85E89D"> i</span><span style="color:#B392F0">.fa-brands.fa-slack</span></span>
|
||
<span class="line"><span style="color:#B392F0"> .ai-title-text</span><span style="color:#79B8FF">=</span><span style="color:#E1E4E8">theme.ai_summary.title</span></span>
|
||
<span class="line"><span style="color:#B392F0"> .ai-tag#ai-tag</span><span style="color:#79B8FF">=</span><span style="color:#E1E4E8"> theme.ai_summary.modelName</span></span></code></pre>
|
||
<h2 id="添加样式">添加样式</h2>
|
||
<p>下面我们添加样式部分,创建文件 <code>/root/theme/butterfly/source/css/_layout/ai-summary.styl</code>文件,写入:</p>
|
||
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;" tabindex="0" data-language="plaintext"><code><span class="line"><span>// ===================</span></span>
|
||
<span class="line"><span>// 🌗 主题变量定义(仅使用项)</span></span>
|
||
<span class="line"><span>// ===================</span></span>
|
||
<span class="line"><span></span></span>
|
||
<span class="line"><span>:root</span></span>
|
||
<span class="line"><span> // ai_summary</span></span>
|
||
<span class="line"><span> --liushen-title-font-color: #0883b7</span></span>
|
||
<span class="line"><span> --liushen-maskbg: rgba(255, 255, 255, 0.85)</span></span>
|
||
<span class="line"><span> --liushen-ai-bg: conic-gradient(from 1.5708rad at 50% 50%, #d6b300 0%, #42A2FF 54%, #d6b300 100%)</span></span>
|
||
<span class="line"><span></span></span>
|
||
<span class="line"><span> // card 背景</span></span>
|
||
<span class="line"><span> --liushen-card-secondbg: #f1f3f8</span></span>
|
||
<span class="line"><span></span></span>
|
||
<span class="line"><span> // text</span></span>
|
||
<span class="line"><span> --liushen-text: #4c4948</span></span>
|
||
<span class="line"><span> --liushen-secondtext: #3c3c43cc</span></span>
|
||
<span class="line"><span></span></span>
|
||
<span class="line"><span>[data-theme='dark']</span></span>
|
||
<span class="line"><span> // ai_summary</span></span>
|
||
<span class="line"><span> --liushen-title-font-color: #0883b7</span></span>
|
||
<span class="line"><span> --liushen-maskbg: rgba(0, 0, 0, 0.85)</span></span>
|
||
<span class="line"><span> --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%)</span></span>
|
||
<span class="line"><span></span></span>
|
||
<span class="line"><span> // card 背景</span></span>
|
||
<span class="line"><span> --liushen-card-secondbg: #3e3f41</span></span>
|
||
<span class="line"><span></span></span>
|
||
<span class="line"><span> // text</span></span>
|
||
<span class="line"><span> --liushen-text: #ffffffb3</span></span>
|
||
<span class="line"><span> --liushen-secondtext: #a1a2b8</span></span>
|
||
<span class="line"><span></span></span>
|
||
<span class="line"><span>// ===================</span></span>
|
||
<span class="line"><span>// 📘 AI 摘要模块样式</span></span>
|
||
<span class="line"><span>// ===================</span></span>
|
||
<span class="line"><span></span></span>
|
||
<span class="line"><span>if hexo-config('ai_summary.enable')</span></span>
|
||
<span class="line"><span> .ai-summary</span></span>
|
||
<span class="line"><span> background-color var(--liushen-maskbg)</span></span>
|
||
<span class="line"><span> background var(--liushen-card-secondbg)</span></span>
|
||
<span class="line"><span> border-radius 12px</span></span>
|
||
<span class="line"><span> padding 8px 8px 12px 8px</span></span>
|
||
<span class="line"><span> line-height 1.3</span></span>
|
||
<span class="line"><span> flex-direction column</span></span>
|
||
<span class="line"><span> margin-bottom 16px</span></span>
|
||
<span class="line"><span> display flex</span></span>
|
||
<span class="line"><span> gap 5px</span></span>
|
||
<span class="line"><span> position relative</span></span>
|
||
<span class="line"><span></span></span>
|
||
<span class="line"><span> &::before</span></span>
|
||
<span class="line"><span> content ''</span></span>
|
||
<span class="line"><span> position absolute</span></span>
|
||
<span class="line"><span> top 0</span></span>
|
||
<span class="line"><span> left 0</span></span>
|
||
<span class="line"><span> width 100%</span></span>
|
||
<span class="line"><span> height 100%</span></span>
|
||
<span class="line"><span> z-index 1</span></span>
|
||
<span class="line"><span> filter blur(8px)</span></span>
|
||
<span class="line"><span> opacity .4</span></span>
|
||
<span class="line"><span> background-image var(--liushen-ai-bg)</span></span>
|
||
<span class="line"><span> transform scaleX(1) scaleY(.95) translateY(2px)</span></span>
|
||
<span class="line"><span> </span></span>
|
||
<span class="line"><span> &::after</span></span>
|
||
<span class="line"><span> content: '';</span></span>
|
||
<span class="line"><span> position: absolute;</span></span>
|
||
<span class="line"><span> top: 0;</span></span>
|
||
<span class="line"><span> left: 0;</span></span>
|
||
<span class="line"><span> width: 100%;</span></span>
|
||
<span class="line"><span> height: 100%;</span></span>
|
||
<span class="line"><span> z-index: 2;</span></span>
|
||
<span class="line"><span> border-radius: 12px;</span></span>
|
||
<span class="line"><span> background: var(--liushen-maskbg);</span></span>
|
||
<span class="line"><span></span></span>
|
||
<span class="line"><span> .ai-explanation</span></span>
|
||
<span class="line"><span> z-index 10</span></span>
|
||
<span class="line"><span> padding 8px 12px</span></span>
|
||
<span class="line"><span> font-size 15px</span></span>
|
||
<span class="line"><span> line-height 1.4</span></span>
|
||
<span class="line"><span> color var(--liushen-text)</span></span>
|
||
<span class="line"><span> text-align justify</span></span>
|
||
<span class="line"><span></span></span>
|
||
<span class="line"><span> // ✅ 打字机光标动画</span></span>
|
||
<span class="line"><span> &::after</span></span>
|
||
<span class="line"><span> content ''</span></span>
|
||
<span class="line"><span> display inline-block</span></span>
|
||
<span class="line"><span> width 8px</span></span>
|
||
<span class="line"><span> height 2px</span></span>
|
||
<span class="line"><span> margin-left 2px</span></span>
|
||
<span class="line"><span> background var(--liushen-text)</span></span>
|
||
<span class="line"><span> vertical-align bottom</span></span>
|
||
<span class="line"><span> animation blink-underline 1s ease-in-out infinite</span></span>
|
||
<span class="line"><span> transition all .3s</span></span>
|
||
<span class="line"><span> position relative</span></span>
|
||
<span class="line"><span> bottom 3px</span></span>
|
||
<span class="line"><span></span></span>
|
||
<span class="line"><span> // 平滑滚动动画</span></span>
|
||
<span class="line"><span> // .char</span></span>
|
||
<span class="line"><span> // display inline-block</span></span>
|
||
<span class="line"><span> // opacity 0</span></span>
|
||
<span class="line"><span> // animation chat-float .5s ease forwards</span></span>
|
||
<span class="line"><span> </span></span>
|
||
<span class="line"><span> .ai-title</span></span>
|
||
<span class="line"><span> z-index 10</span></span>
|
||
<span class="line"><span> font-size 14px</span></span>
|
||
<span class="line"><span> display flex</span></span>
|
||
<span class="line"><span> border-radius 8px</span></span>
|
||
<span class="line"><span> align-items center</span></span>
|
||
<span class="line"><span> position relative</span></span>
|
||
<span class="line"><span> padding 0 12px</span></span>
|
||
<span class="line"><span> cursor default</span></span>
|
||
<span class="line"><span> user-select none</span></span>
|
||
<span class="line"><span></span></span>
|
||
<span class="line"><span> .ai-title-left</span></span>
|
||
<span class="line"><span> display flex</span></span>
|
||
<span class="line"><span> align-items center</span></span>
|
||
<span class="line"><span> color var(--liushen-title-font-color)</span></span>
|
||
<span class="line"><span></span></span>
|
||
<span class="line"><span> i</span></span>
|
||
<span class="line"><span> margin-right 3px</span></span>
|
||
<span class="line"><span> display flex</span></span>
|
||
<span class="line"><span> color var(--liushen-title-font-color)</span></span>
|
||
<span class="line"><span> border-radius 20px</span></span>
|
||
<span class="line"><span> justify-content center</span></span>
|
||
<span class="line"><span> align-items center</span></span>
|
||
<span class="line"><span></span></span>
|
||
<span class="line"><span> .ai-title-text</span></span>
|
||
<span class="line"><span> font-weight 500</span></span>
|
||
<span class="line"><span></span></span>
|
||
<span class="line"><span> .ai-tag</span></span>
|
||
<span class="line"><span> color var(--liushen-secondtext)</span></span>
|
||
<span class="line"><span> font-weight 300</span></span>
|
||
<span class="line"><span> margin-left auto</span></span>
|
||
<span class="line"><span> display flex</span></span>
|
||
<span class="line"><span> align-items center</span></span>
|
||
<span class="line"><span> justify-content center</span></span>
|
||
<span class="line"><span> transition .3s</span></span>
|
||
<span class="line"><span></span></span>
|
||
<span class="line"><span> // 平滑滚动动画</span></span>
|
||
<span class="line"><span> // @keyframes chat-float</span></span>
|
||
<span class="line"><span> // 0% </span></span>
|
||
<span class="line"><span> // opacity 0</span></span>
|
||
<span class="line"><span> // transform translateY(20px)</span></span>
|
||
<span class="line"><span> // 100% </span></span>
|
||
<span class="line"><span> // opacity 1</span></span>
|
||
<span class="line"><span> // transform translateY(0)</span></span>
|
||
<span class="line"><span> </span></span>
|
||
<span class="line"><span> // ✅ 打字机光标闪烁动画</span></span>
|
||
<span class="line"><span> @keyframes blink-underline</span></span>
|
||
<span class="line"><span> 0%, 100%</span></span>
|
||
<span class="line"><span> opacity 1</span></span>
|
||
<span class="line"><span> 50%</span></span>
|
||
<span class="line"><span> opacity 0</span></span></code></pre>
|
||
<p>样式也实现啦!目前就差将我们的摘要插入到我们的网站就大功告成啦,为了实现的更加逼真,我这里实现了两种样式一个是打字机效果,一个是平滑显示效果,可以按需引入:</p>
|
||
<h2 id="添加核心js">添加核心JS</h2>
|
||
<p>介绍两种动效,可以按照自己的需求在任意js文件中选择一个引入即可,两个的区别是,打字机效果更加的节省性能,而平滑显示,因为每个文本为一个span,所以会比较耗费性能。</p>
|
||
<h3 id="打字机">打字机</h3>
|
||
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#6A737D">// 打字机效果</span></span>
|
||
<span class="line"><span style="color:#F97583">function</span><span style="color:#B392F0"> typeTextMachineStyle</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">text</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">targetSelector</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">options</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> {}) {</span></span>
|
||
<span class="line"><span style="color:#F97583"> const</span><span style="color:#E1E4E8"> {</span></span>
|
||
<span class="line"><span style="color:#79B8FF"> delay</span><span style="color:#F97583"> =</span><span style="color:#79B8FF"> 50</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#79B8FF"> startDelay</span><span style="color:#F97583"> =</span><span style="color:#79B8FF"> 2000</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#79B8FF"> onComplete</span><span style="color:#F97583"> =</span><span style="color:#79B8FF"> null</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#79B8FF"> clearBefore</span><span style="color:#F97583"> =</span><span style="color:#79B8FF"> true</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#79B8FF"> eraseBefore</span><span style="color:#F97583"> =</span><span style="color:#79B8FF"> true</span><span style="color:#E1E4E8">, </span><span style="color:#6A737D">// 新增:是否以打字机方式清除原文本</span></span>
|
||
<span class="line"><span style="color:#79B8FF"> eraseDelay</span><span style="color:#F97583"> =</span><span style="color:#79B8FF"> 30</span><span style="color:#E1E4E8">, </span><span style="color:#6A737D">// 新增:删除每个字符的间隔</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> } </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> options;</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> el</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> document.</span><span style="color:#B392F0">querySelector</span><span style="color:#E1E4E8">(targetSelector);</span></span>
|
||
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">el </span><span style="color:#F97583">||</span><span style="color:#F97583"> typeof</span><span style="color:#E1E4E8"> text </span><span style="color:#F97583">!==</span><span style="color:#9ECBFF"> "string"</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">return</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#B392F0"> setTimeout</span><span style="color:#E1E4E8">(() </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
|
||
<span class="line"><span style="color:#F97583"> const</span><span style="color:#B392F0"> startTyping</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> () </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
|
||
<span class="line"><span style="color:#F97583"> let</span><span style="color:#E1E4E8"> index </span><span style="color:#F97583">=</span><span style="color:#79B8FF"> 0</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#F97583"> function</span><span style="color:#B392F0"> renderChar</span><span style="color:#E1E4E8">() {</span></span>
|
||
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (index </span><span style="color:#F97583"><=</span><span style="color:#E1E4E8"> text.</span><span style="color:#79B8FF">length</span><span style="color:#E1E4E8">) {</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> el.textContent </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> text.</span><span style="color:#B392F0">slice</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">0</span><span style="color:#E1E4E8">, index</span><span style="color:#F97583">++</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#B392F0"> setTimeout</span><span style="color:#E1E4E8">(renderChar, delay);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> } </span><span style="color:#F97583">else</span><span style="color:#E1E4E8"> {</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> onComplete </span><span style="color:#F97583">&&</span><span style="color:#B392F0"> onComplete</span><span style="color:#E1E4E8">(el);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"><span style="color:#B392F0"> renderChar</span><span style="color:#E1E4E8">();</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> };</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (clearBefore) {</span></span>
|
||
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (eraseBefore </span><span style="color:#F97583">&&</span><span style="color:#E1E4E8"> el.textContent.</span><span style="color:#79B8FF">length</span><span style="color:#F97583"> ></span><span style="color:#79B8FF"> 0</span><span style="color:#E1E4E8">) {</span></span>
|
||
<span class="line"><span style="color:#F97583"> let</span><span style="color:#E1E4E8"> currentText </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> el.textContent;</span></span>
|
||
<span class="line"><span style="color:#F97583"> let</span><span style="color:#E1E4E8"> eraseIndex </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> currentText.</span><span style="color:#79B8FF">length</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#F97583"> function</span><span style="color:#B392F0"> eraseChar</span><span style="color:#E1E4E8">() {</span></span>
|
||
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (eraseIndex </span><span style="color:#F97583">></span><span style="color:#79B8FF"> 0</span><span style="color:#E1E4E8">) {</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> el.textContent </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> currentText.</span><span style="color:#B392F0">slice</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">0</span><span style="color:#E1E4E8">, </span><span style="color:#F97583">--</span><span style="color:#E1E4E8">eraseIndex);</span></span>
|
||
<span class="line"><span style="color:#B392F0"> setTimeout</span><span style="color:#E1E4E8">(eraseChar, eraseDelay);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> } </span><span style="color:#F97583">else</span><span style="color:#E1E4E8"> {</span></span>
|
||
<span class="line"><span style="color:#B392F0"> startTyping</span><span style="color:#E1E4E8">(); </span><span style="color:#6A737D">// 删除完毕后开始打字</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#B392F0"> eraseChar</span><span style="color:#E1E4E8">();</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> } </span><span style="color:#F97583">else</span><span style="color:#E1E4E8"> {</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> el.textContent </span><span style="color:#F97583">=</span><span style="color:#9ECBFF"> ""</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#B392F0"> startTyping</span><span style="color:#E1E4E8">();</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> } </span><span style="color:#F97583">else</span><span style="color:#E1E4E8"> {</span></span>
|
||
<span class="line"><span style="color:#B392F0"> startTyping</span><span style="color:#E1E4E8">();</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }, startDelay);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8">}</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#F97583">function</span><span style="color:#B392F0"> renderAISummary</span><span style="color:#E1E4E8">() {</span></span>
|
||
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> summaryEl</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> document.</span><span style="color:#B392F0">querySelector</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'.ai-summary .ai-explanation'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">summaryEl) </span><span style="color:#F97583">return</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> summaryText</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> summaryEl.</span><span style="color:#B392F0">getAttribute</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'data-summary'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (summaryText) {</span></span>
|
||
<span class="line"><span style="color:#B392F0"> typeTextMachineStyle</span><span style="color:#E1E4E8">(summaryText, </span><span style="color:#9ECBFF">".ai-summary .ai-explanation"</span><span style="color:#E1E4E8">); </span><span style="color:#6A737D">// 如果需要切换,在这里调用另一个函数即可</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"><span style="color:#E1E4E8">}</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#E1E4E8">document.</span><span style="color:#B392F0">addEventListener</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'pjax:complete'</span><span style="color:#E1E4E8">, renderAISummary);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8">document.</span><span style="color:#B392F0">addEventListener</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'DOMContentLoaded'</span><span style="color:#E1E4E8">, renderAISummary);</span></span></code></pre>
|
||
<p>本站使用的就是打字机效果,可以自行查看。</p>
|
||
<h3 id="平滑">平滑</h3>
|
||
<p>这个没有样图,如果好奇可以自行部署并尝试:</p>
|
||
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#6A737D"> // 平滑弹出效果</span></span>
|
||
<span class="line"><span style="color:#F97583"> function</span><span style="color:#B392F0"> typeText</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">text</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">targetSelector</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">options</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> {}) {</span></span>
|
||
<span class="line"><span style="color:#F97583"> const</span><span style="color:#E1E4E8"> {</span></span>
|
||
<span class="line"><span style="color:#79B8FF"> delay</span><span style="color:#F97583"> =</span><span style="color:#79B8FF"> 50</span><span style="color:#E1E4E8">, </span><span style="color:#6A737D">// 每个字符之间的延迟(毫秒)</span></span>
|
||
<span class="line"><span style="color:#79B8FF"> startDelay</span><span style="color:#F97583"> =</span><span style="color:#79B8FF"> 2000</span><span style="color:#E1E4E8">, </span><span style="color:#6A737D">// 开始打字前的延迟(默认 3 秒)</span></span>
|
||
<span class="line"><span style="color:#79B8FF"> onComplete</span><span style="color:#F97583"> =</span><span style="color:#79B8FF"> null</span><span style="color:#E1E4E8">, </span><span style="color:#6A737D">// 动画完成后的回调</span></span>
|
||
<span class="line"><span style="color:#79B8FF"> clearBefore</span><span style="color:#F97583"> =</span><span style="color:#79B8FF"> true</span><span style="color:#6A737D"> // 是否在开始前清空原有内容</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> } </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> options;</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> targetEl</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> document.</span><span style="color:#B392F0">querySelector</span><span style="color:#E1E4E8">(targetSelector);</span></span>
|
||
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">targetEl </span><span style="color:#F97583">||</span><span style="color:#F97583"> typeof</span><span style="color:#E1E4E8"> text </span><span style="color:#F97583">!==</span><span style="color:#9ECBFF"> "string"</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">return</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#6A737D"> // if (clearBefore) targetEl.textContent = "";</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#F97583"> let</span><span style="color:#E1E4E8"> index </span><span style="color:#F97583">=</span><span style="color:#79B8FF"> 0</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#F97583"> let</span><span style="color:#E1E4E8"> frameId </span><span style="color:#F97583">=</span><span style="color:#79B8FF"> null</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#F97583"> function</span><span style="color:#B392F0"> renderChar</span><span style="color:#E1E4E8">() {</span></span>
|
||
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (index </span><span style="color:#F97583"><</span><span style="color:#E1E4E8"> text.</span><span style="color:#79B8FF">length</span><span style="color:#E1E4E8">) {</span></span>
|
||
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> span</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> document.</span><span style="color:#B392F0">createElement</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"span"</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> span.textContent </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> text[index</span><span style="color:#F97583">++</span><span style="color:#E1E4E8">];</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> span.className </span><span style="color:#F97583">=</span><span style="color:#9ECBFF"> "char"</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> targetEl.</span><span style="color:#B392F0">appendChild</span><span style="color:#E1E4E8">(span);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> frameId </span><span style="color:#F97583">=</span><span style="color:#B392F0"> requestAnimationFrame</span><span style="color:#E1E4E8">(() </span><span style="color:#F97583">=></span><span style="color:#B392F0"> setTimeout</span><span style="color:#E1E4E8">(renderChar, delay));</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> } </span><span style="color:#F97583">else</span><span style="color:#E1E4E8"> {</span></span>
|
||
<span class="line"><span style="color:#B392F0"> cancelAnimationFrame</span><span style="color:#E1E4E8">(frameId);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> onComplete </span><span style="color:#F97583">&&</span><span style="color:#B392F0"> onComplete</span><span style="color:#E1E4E8">(targetEl);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#B392F0"> setTimeout</span><span style="color:#E1E4E8">(() </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
|
||
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (clearBefore) targetEl.textContent </span><span style="color:#F97583">=</span><span style="color:#9ECBFF"> ""</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#B392F0"> renderChar</span><span style="color:#E1E4E8">();</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }, startDelay);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#F97583">function</span><span style="color:#B392F0"> renderAISummary</span><span style="color:#E1E4E8">() {</span></span>
|
||
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> summaryEl</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> document.</span><span style="color:#B392F0">querySelector</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'.ai-summary .ai-explanation'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">summaryEl) </span><span style="color:#F97583">return</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> summaryText</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> summaryEl.</span><span style="color:#B392F0">getAttribute</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'data-summary'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (summaryText) {</span></span>
|
||
<span class="line"><span style="color:#B392F0"> typeText</span><span style="color:#E1E4E8">(summaryText, </span><span style="color:#9ECBFF">".ai-summary .ai-explanation"</span><span style="color:#E1E4E8">); </span><span style="color:#6A737D">// 如果需要切换,在这里调用另一个函数即可</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"><span style="color:#E1E4E8">}</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#E1E4E8">document.</span><span style="color:#B392F0">addEventListener</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'pjax:complete'</span><span style="color:#E1E4E8">, renderAISummary);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8">document.</span><span style="color:#B392F0">addEventListener</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'DOMContentLoaded'</span><span style="color:#E1E4E8">, renderAISummary);</span></span></code></pre>
|
||
<p>注意,平滑滚动部分的css,默认注释掉了</p>
|
||
<p>好的,设置完毕!</p></div><section class="post-copyright-card" aria-label="文章版权信息"><div class="post-copyright-mark" aria-hidden="true"><span>CC</span></div><div class="post-copyright-main"><h2>博客添加AI总结</h2><a class="post-copyright-link" href="/posts/41b9aff7/">/posts/41b9aff7/</a><dl class="post-copyright-meta"><div><dt>作者</dt><dd>biss</dd></div><div><dt>发布于</dt><dd><time datetime="2025-08-12T09:46:25.000Z">2025/08/12</time></dd></div><div><dt>许可协议</dt><dd><a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank" rel="noreferrer">CC BY-NC-SA 4.0</a></dd></div></dl></div><div class="post-copyright-footer"><div class="post-copyright-tags" aria-label="文章标签"><a href="/tags/%E7%BD%91%E7%AB%99/">网站</a></div><div class="post-copyright-actions" aria-label="文章操作"><button class="post-copyright-action is-primary post-share-button" type="button" data-share-title="博客添加AI总结" data-share-url="https://blog.biss.click/posts/41b9aff7/" title="分享文章"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M4 12v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7"></path><path d="M12 16V4"></path><path d="m7 9 5-5 5 5"></path></svg><span>分享</span></button><a class="post-copyright-action" href="/rss.xml" title="订阅 RSS"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M5 5a14 14 0 0 1 14 14"></path><path d="M5 11a8 8 0 0 1 8 8"></path><path d="M6 18h.01"></path></svg><span>订阅</span></a><a class="post-copyright-action" href="/about/" title="打赏作者"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M7 8h10"></path><path d="M8 8V5h8v3"></path><path d="M6 8h12l-1.5 11h-9L6 8Z"></path><path d="M10 12h4"></path></svg><span>打赏</span></a><a class="post-copyright-icon" href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fblog.biss.click%2Fposts%2F41b9aff7%2F&text=%E5%8D%9A%E5%AE%A2%E6%B7%BB%E5%8A%A0AI%E6%80%BB%E7%BB%93" target="_blank" rel="noreferrer" title="分享到 X"><span aria-hidden="true">x</span></a><a class="post-copyright-icon" href="https://service.weibo.com/share/share.php?url=https%3A%2F%2Fblog.biss.click%2Fposts%2F41b9aff7%2F&title=%E5%8D%9A%E5%AE%A2%E6%B7%BB%E5%8A%A0AI%E6%80%BB%E7%BB%93" target="_blank" rel="noreferrer" title="分享到微博"><span aria-hidden="true">微</span></a></div></div></section><script>
|
||
(() => {
|
||
document.querySelectorAll('.post-share-button').forEach((button) => {
|
||
button.addEventListener('click', async () => {
|
||
const title = button.dataset.shareTitle || document.title;
|
||
const url = button.dataset.shareUrl || window.location.href;
|
||
const label = button.querySelector('span');
|
||
const originalText = label?.textContent || '分享';
|
||
|
||
try {
|
||
if (navigator.share) {
|
||
await navigator.share({ title, url });
|
||
return;
|
||
}
|
||
|
||
await navigator.clipboard.writeText(url);
|
||
if (label) label.textContent = '已复制';
|
||
} catch {
|
||
if (label) label.textContent = '复制失败';
|
||
} finally {
|
||
window.setTimeout(() => {
|
||
if (label) label.textContent = originalText;
|
||
}, 1800);
|
||
}
|
||
});
|
||
});
|
||
})();
|
||
</script><section id="post-comment" class="twikoo-comments" aria-label="Comments" data-env-id="https://comment.biss.click" data-region data-path="/posts/41b9aff7/" data-script="https://cdn.jsdelivr.net/npm/twikoo@1.7.7/dist/twikoo.min.js"><div class="twikoo-comments-head"><span class="twikoo-comments-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M21 12a8 8 0 0 1-8 8H7l-4 3v-5a8 8 0 1 1 18-6Z"></path></svg></span><div><h2>Comments</h2><p>博客添加AI总结</p></div></div><div id="twikoo-wrap" class="twikoo-wrap"></div></section><script>
|
||
(() => {
|
||
const root = document.getElementById('post-comment');
|
||
const wrap = document.getElementById('twikoo-wrap');
|
||
if (!root || !wrap || root.dataset.twikooLoaded === 'true') return;
|
||
|
||
const envId = root.dataset.envId;
|
||
const path = root.dataset.path || window.location.pathname;
|
||
const region = root.dataset.region || undefined;
|
||
if (!envId) return;
|
||
|
||
root.dataset.twikooLoaded = 'true';
|
||
|
||
const initTwikoo = () => {
|
||
if (!window.twikoo || typeof window.twikoo.init !== 'function') return;
|
||
|
||
window.twikoo.init({
|
||
el: wrap,
|
||
envId,
|
||
region,
|
||
path,
|
||
});
|
||
};
|
||
|
||
if (window.twikoo) {
|
||
initTwikoo();
|
||
return;
|
||
}
|
||
|
||
const script = document.createElement('script');
|
||
script.src = root.dataset.script;
|
||
script.defer = true;
|
||
script.onload = initTwikoo;
|
||
script.onerror = () => {
|
||
root.dataset.twikooLoaded = 'false';
|
||
};
|
||
document.head.appendChild(script);
|
||
})();
|
||
</script></article><aside class="article-sidebar" aria-label="文章侧边栏"><section class="sidebar-card article-author-card"><img class="profile-avatar" src="https://free.picui.cn/free/2025/08/10/689845496a283.png" alt="biss" loading="lazy"><div class="profile-name">biss</div><div class="article-author-meta"><span>2025/08/12</span><a href="/categories/site-notes/">建站手札</a></div></section><section class="sidebar-card article-tags-card"><h2><span aria-hidden="true">#</span>文章标签<em>1</em></h2><div class="article-tag-list" aria-label="文章标签"><a href="/tags/%E7%BD%91%E7%AB%99/">#网站</a></div></section><div class="article-sticky-sidebar"><section class="sidebar-card toc-card"><h2><span aria-hidden="true">☰</span>文章目录<em>8</em></h2><nav class="toc-list toc-tree" aria-label="文章目录" data-toc-tree><div class="toc-group is-open" data-toc-group data-heading-id="api推荐"><a class="toc-link toc-link-main" href="#api推荐" data-toc-link data-toc-top><span class="toc-marker" aria-hidden="true"></span><span>API推荐</span></a></div><div class="toc-group" data-toc-group data-heading-id="hexo适配"><a class="toc-link toc-link-main" href="#hexo适配" data-toc-link data-toc-top><span class="toc-marker" aria-hidden="true"></span><span>Hexo适配</span></a><div class="toc-children" data-toc-children><a class="toc-link toc-link-child toc-depth-2" href="#添加配置" data-toc-link><span>添加配置</span></a><a class="toc-link toc-link-child toc-depth-2" href="#添加模板" data-toc-link><span>添加模板</span></a><a class="toc-link toc-link-child toc-depth-2" href="#添加样式" data-toc-link><span>添加样式</span></a><a class="toc-link toc-link-child toc-depth-2" href="#添加核心js" data-toc-link><span>添加核心JS</span></a><a class="toc-link toc-link-child toc-depth-3" href="#打字机" data-toc-link><span>打字机</span></a><a class="toc-link toc-link-child toc-depth-3" href="#平滑" data-toc-link><span>平滑</span></a></div></div></nav></section><section class="sidebar-card same-category-card"><h2><span aria-hidden="true">▣</span>同类文章<em>8</em></h2><div class="same-category-list"><a href="/posts/8f3b9d21/"><strong>网站迁移日志</strong><time datetime="2026-06-19T16:45:00.000Z">2026/06/20</time></a><a href="/posts/4c91a8f2/"><strong>用 Umami 分享链接给网站信息卡提供统计数据</strong><time datetime="2026-06-19T08:58:00.000Z">2026/06/19</time></a><a href="/posts/7e921903/"><strong>添加网站左上角菜单</strong><time datetime="2026-02-10T07:11:14.000Z">2026/02/10</time></a><a href="/posts/f287c563/"><strong>添加typesense搜索</strong><time datetime="2026-02-05T13:14:16.000Z">2026/02/05</time></a><a href="/posts/b5601a7e/"><strong>自定义字体</strong><time datetime="2026-01-16T07:47:15.000Z">2026/01/16</time></a><a href="/posts/5ed2f1e6/"><strong>在侧边栏添加日历和倒计时</strong><time datetime="2026-01-16T07:47:07.000Z">2026/01/16</time></a><a href="/posts/3a26a97a/"><strong>为博客添加CMS系统</strong><time datetime="2025-08-25T22:03:00.000Z">2025/08/26</time></a><a href="/posts/ce1ec3fe/"><strong>使用GitHub推送Hexo到服务器</strong><time datetime="2025-08-18T17:10:18.000Z">2025/08/19</time></a></div></section><section class="sidebar-card site-info-card" data-site-info-card data-stats-endpoint="https://blogumami.biss.click"><h2><i class="fa-solid fa-chart-line" aria-hidden="true"></i> 网站信息</h2><dl class="site-info-list"><div><dt>文章数目</dt><dd>45</dd></div><div><dt>本站总字数</dt><dd>38,514</dd></div><div><dt>本站访客数</dt><dd data-stat-key="visitors">--</dd></div><div><dt>本站访问量</dt><dd data-stat-key="pageviews">--</dd></div><div><dt>最后更新时间</dt><dd>2026/08/28</dd></div></dl></section><script>
|
||
(() => {
|
||
const cacheDuration = 10 * 60 * 1000;
|
||
const formatter = new Intl.NumberFormat('en-US');
|
||
const cards = [...document.querySelectorAll('[data-site-info-card]')];
|
||
const endpoint = cards.find((card) => card.dataset.statsEndpoint)?.dataset.statsEndpoint;
|
||
|
||
if (!endpoint) return;
|
||
|
||
const cacheKey = `site-info-stats:${endpoint}`;
|
||
|
||
const updateCards = (stats) => {
|
||
const visitors = Number(stats?.visitors);
|
||
const pageviews = Number(stats?.pageviews);
|
||
|
||
cards.forEach((card) => {
|
||
const visitorsEl = card.querySelector('[data-stat-key="visitors"]');
|
||
const pageviewsEl = card.querySelector('[data-stat-key="pageviews"]');
|
||
if (visitorsEl && Number.isFinite(visitors)) visitorsEl.textContent = formatter.format(visitors);
|
||
if (pageviewsEl && Number.isFinite(pageviews)) pageviewsEl.textContent = formatter.format(pageviews);
|
||
});
|
||
};
|
||
|
||
try {
|
||
const cached = JSON.parse(localStorage.getItem(cacheKey) || 'null');
|
||
if (cached?.time && Date.now() - cached.time < cacheDuration) {
|
||
updateCards(cached.data);
|
||
return;
|
||
}
|
||
} catch {
|
||
localStorage.removeItem(cacheKey);
|
||
}
|
||
|
||
fetch(endpoint, { headers: { Accept: 'application/json' } })
|
||
.then((response) => {
|
||
if (!response.ok) throw new Error(`Stats request failed: ${response.status}`);
|
||
return response.json();
|
||
})
|
||
.then((data) => {
|
||
localStorage.setItem(cacheKey, JSON.stringify({ time: Date.now(), data }));
|
||
updateCards(data);
|
||
})
|
||
.catch(() => {
|
||
});
|
||
})();
|
||
</script></div></aside></section></main><footer class="site-footer" aria-label="站点页脚"><div class="footer-inner"><section class="footer-top" aria-label="站点信息"><a class="footer-brand" href="/" aria-label="Bi's Blog 首页"><img src="/images/Bi.ico" alt="" loading="lazy"><span>Bi's Blog</span></a><p class="footer-slogan">好奇心转化为代码的空间。</p></section><section class="footer-contact" aria-label="联系方式"><p>联系方式</p><div class="footer-socials"><a href="mailto:bishsh2006@gmail.com" aria-label="发送邮件" title="发送邮件"><i class="fa-solid fa-envelope" aria-hidden="true"></i></a><a href="/rss.xml" aria-label="订阅 RSS" title="订阅 RSS"><i class="fa-solid fa-rss" aria-hidden="true"></i></a><a href="https://github.com/bishshi" target="_blank" rel="noreferrer" aria-label="GitHub" title="GitHub"><i class="fa-brands fa-github" aria-hidden="true"></i></a></div><nav class="footer-contact-links" aria-label="页脚链接"><a href="/archives/">发展历程</a><span aria-hidden="true">|</span><a href="/privacy/">隐私政策</a></nav></section><section class="footer-bottom" aria-label="版权信息"><div><p>© 2025 - 2026 Bi's Blog</p><p>blog.biss.click | 内容采用 CC BY-NC-SA 4.0 协议</p></div><div class="footer-meta"><p>本站已运行:<span id="site-runtime">计算中...</span></p><p><a href="https://fastly.com/" target="_blank" rel="noreferrer">Fastly</a><span aria-hidden="true">|</span><a href="https://astro.build/" target="_blank" rel="noreferrer">ASTRO</a></p></div></section></div></footer><aside class="hitokoto-card is-collapsed" aria-label="一言" data-hitokoto-card data-astro-cid-fo5bt3gf><div class="hitokoto-card__head" data-astro-cid-fo5bt3gf><button class="hitokoto-card__toggle" type="button" aria-label="展开一言" aria-expanded="false" title="一言" data-hitokoto-toggle data-astro-cid-fo5bt3gf><i class="fa-solid fa-quote-left" aria-hidden="true" data-astro-cid-fo5bt3gf></i><span data-astro-cid-fo5bt3gf>一言</span></button><div class="hitokoto-card__actions" data-astro-cid-fo5bt3gf><button class="hitokoto-card__refresh" type="button" aria-label="换一句" title="换一句" data-hitokoto-refresh data-astro-cid-fo5bt3gf><i class="fa-solid fa-rotate" aria-hidden="true" data-astro-cid-fo5bt3gf></i></button><button class="hitokoto-card__collapse" type="button" aria-label="收起一言" title="收起" data-hitokoto-collapse data-astro-cid-fo5bt3gf><i class="fa-solid fa-chevron-down" aria-hidden="true" data-astro-cid-fo5bt3gf></i></button></div></div><a class="hitokoto-card__text" href="https://hitokoto.cn/" target="_blank" rel="noreferrer" data-hitokoto-text data-astro-cid-fo5bt3gf>获取中...</a><p class="hitokoto-card__from" data-hitokoto-from data-astro-cid-fo5bt3gf>来自 Hitokoto</p></aside><script type="module">(()=>{let e=document.querySelector(`[data-hitokoto-card]`),t=e?.querySelector(`[data-hitokoto-toggle]`),n=e?.querySelector(`[data-hitokoto-text]`),r=e?.querySelector(`[data-hitokoto-from]`),i=e?.querySelector(`[data-hitokoto-refresh]`),a=e?.querySelector(`[data-hitokoto-collapse]`);if(!(e instanceof HTMLElement)||!(t instanceof HTMLButtonElement)||!(n instanceof HTMLAnchorElement)||!(r instanceof HTMLElement))return;let o=`hitokoto-card-cache`,s=`hitokoto-card-expanded`,c={sentence:`生活明朗,万物可爱。`,source:`本地缓存`,url:`https://hitokoto.cn/`},l=[`/`,`/index.html`].includes(window.location.pathname),u=window.matchMedia(`(max-width: 720px)`);function d(){try{let e=localStorage.getItem(s);if(e===`true`)return!0;if(e===`false`)return!1}catch{}return null}function f(e){try{localStorage.setItem(s,String(e))}catch{}}function p(n,{persist:r=!1}={}){e.classList.toggle(`is-collapsed`,!n),t.setAttribute(`aria-expanded`,String(n)),t.setAttribute(`aria-label`,n?`收起一言`:`展开一言`),r&&f(n)}function m(){try{let e=localStorage.getItem(o);if(!e)return null;let t=JSON.parse(e);return!t||typeof t.expiresAt!=`number`||t.expiresAt<=Date.now()?null:t.data}catch{return null}}function h(e){try{localStorage.setItem(o,JSON.stringify({data:e,expiresAt:Date.now()+216e5}))}catch{}}function g(e){n.textContent=e.sentence,n.href=e.url||`https://hitokoto.cn/`,r.textContent=e.source?`出自 ${e.source}`:`来自 Hitokoto`}async function _({force:t=!1}={}){if(!t){let e=m();if(e){g(e);return}}e.classList.add(`is-loading`),n.textContent=`获取中...`;try{let e=await fetch(`https://yiyan.biss.click/api?encode=json`);if(!e.ok)throw Error(`Hitokoto request failed: ${e.status}`);let t=await e.json(),n=t.hitokoto||t.content||t.sentence||t.text,r={sentence:typeof n==`string`&&n.trim()?n.trim():c.sentence,source:typeof t.from==`string`&&t.from.trim()?t.from.trim():`Hitokoto`,url:t.uuid?`https://hitokoto.cn/?uuid=${encodeURIComponent(t.uuid)}`:`https://hitokoto.cn/`};h(r),g(r)}catch(e){console.error(e),g(m()||c)}finally{e.classList.remove(`is-loading`)}}p(d()??(l&&!u.matches)),t.addEventListener(`click`,()=>{p(e.classList.contains(`is-collapsed`),{persist:!0})}),i?.addEventListener(`click`,e=>{e.stopPropagation(),p(!0,{persist:!0}),_({force:!0})}),a?.addEventListener(`click`,e=>{e.stopPropagation(),p(!1,{persist:!0})}),e.addEventListener(`click`,t=>{e.classList.contains(`is-collapsed`)&&(t.target instanceof Element&&t.target.closest(`a, button`)||p(!0,{persist:!0}))}),_()})();</script><div class="custom-context-menu" data-context-menu role="menu" aria-hidden="true"><div class="context-menu-nav" aria-label="页面导航"><button type="button" data-context-action="back" aria-label="后退上一页" title="后退上一页"><i class="fa-solid fa-arrow-left" aria-hidden="true"></i></button><button type="button" data-context-action="forward" aria-label="前进下一页" title="前进下一页"><i class="fa-solid fa-arrow-right" aria-hidden="true"></i></button><button type="button" data-context-action="reload" aria-label="刷新页面" title="刷新页面"><i class="fa-solid fa-rotate-right" aria-hidden="true"></i></button><a href="/" aria-label="返回首页" title="返回首页"><i class="fa-solid fa-house" aria-hidden="true"></i></a></div><div class="context-menu-group" data-selection-group hidden><button type="button" data-context-action="copy-selection" data-selection-item><i class="fa-regular fa-copy" aria-hidden="true"></i><span>复制选中文字</span></button><button type="button" data-context-action="search-selection" data-selection-item><i class="fa-brands fa-microsoft" aria-hidden="true"></i><span>用 Bing 搜索</span></button><button type="button" data-context-action="quote-selection" data-selection-item data-comments-item><i class="fa-regular fa-comment-dots" aria-hidden="true"></i><span>引用到评论区</span></button></div><div class="context-menu-group" data-image-group hidden><button type="button" data-context-action="copy-image"><i class="fa-regular fa-image" aria-hidden="true"></i><span>复制图片</span></button><button type="button" data-context-action="open-image"><i class="fa-solid fa-up-right-from-square" aria-hidden="true"></i><span>新页面打开图片</span></button><button type="button" data-context-action="copy-image-url"><i class="fa-solid fa-link" aria-hidden="true"></i><span>复制图片地址</span></button></div><div class="context-menu-group"><a href="/archives/"><i class="fa-solid fa-box-archive" aria-hidden="true"></i><span>时间线</span></a><a href="/categories/"><i class="fa-solid fa-folder" aria-hidden="true"></i><span>分类</span></a><a href="/tags/"><i class="fa-solid fa-hashtag" aria-hidden="true"></i><span>标签</span></a><button type="button" data-context-action="random-post"><i class="fa-solid fa-shuffle" aria-hidden="true"></i><span>随机文章</span></button><button type="button" data-context-action="comments" data-comments-item><i class="fa-regular fa-comment-dots" aria-hidden="true"></i><span>跳转评论区</span></button><button type="button" data-context-action="print"><i class="fa-solid fa-print" aria-hidden="true"></i><span>打印页面</span></button><a href="/cc/"><i class="fa-solid fa-circle-info" aria-hidden="true"></i><span>网站版权</span></a></div></div><div class="context-menu-native-hint" data-context-native-hint role="note" aria-label="Ctrl 右键提示"><kbd>Ctrl</kbd> + 右键:浏览器原生菜单</div><script>
|
||
(() => {
|
||
const menu = document.querySelector('[data-context-menu]');
|
||
if (!menu) return;
|
||
|
||
const nativeHint = document.querySelector('[data-context-native-hint]');
|
||
const selectionItems = [...menu.querySelectorAll('[data-selection-item]')];
|
||
const selectionGroup = menu.querySelector('[data-selection-group]');
|
||
const commentsItems = [...menu.querySelectorAll('[data-comments-item]')];
|
||
const imageGroup = menu.querySelector('[data-image-group]');
|
||
const focusableSelector = 'a[href], button:not([disabled])';
|
||
let lastSelectionText = '';
|
||
let lastImage = null;
|
||
let nativeHintTimer = 0;
|
||
|
||
const isNativeContextTarget = (target) =>
|
||
target instanceof Element &&
|
||
Boolean(target.closest('input, textarea, select, [contenteditable="true"], [contenteditable=""]'));
|
||
|
||
const getCommentsTarget = () =>
|
||
document.querySelector('.twikoo-comments, #twikoo-wrap, #twikoo, .tk-comments-container');
|
||
|
||
const getSelectionText = () => String(window.getSelection?.() || '').trim();
|
||
|
||
const getImageTarget = (target) => {
|
||
if (!(target instanceof Element)) return null;
|
||
const image = target.closest('img');
|
||
return image instanceof HTMLImageElement ? image : null;
|
||
};
|
||
|
||
const getImageUrl = () => {
|
||
if (!lastImage) return '';
|
||
return lastImage.currentSrc || lastImage.src || lastImage.getAttribute('src') || '';
|
||
};
|
||
|
||
const copyText = async (text) => {
|
||
if (navigator.clipboard?.writeText) {
|
||
await navigator.clipboard.writeText(text);
|
||
return;
|
||
}
|
||
|
||
const textarea = document.createElement('textarea');
|
||
textarea.value = text;
|
||
textarea.setAttribute('readonly', '');
|
||
textarea.style.position = 'fixed';
|
||
textarea.style.inset = '0 auto auto -9999px';
|
||
document.body.appendChild(textarea);
|
||
textarea.select();
|
||
document.execCommand('copy');
|
||
textarea.remove();
|
||
};
|
||
|
||
const setMenuState = () => {
|
||
const hasSelection = lastSelectionText.length > 0;
|
||
const hasComments = Boolean(getCommentsTarget());
|
||
const hasImage = Boolean(getImageUrl());
|
||
|
||
selectionItems.forEach((item) => {
|
||
item.hidden = !hasSelection;
|
||
});
|
||
if (selectionGroup) selectionGroup.hidden = !hasSelection;
|
||
|
||
commentsItems.forEach((item) => {
|
||
item.hidden = !hasComments || (item.hasAttribute('data-selection-item') && !hasSelection);
|
||
});
|
||
|
||
if (imageGroup) imageGroup.hidden = !hasImage;
|
||
};
|
||
|
||
const closeMenu = () => {
|
||
menu.classList.remove('is-open');
|
||
menu.setAttribute('aria-hidden', 'true');
|
||
};
|
||
|
||
const openMenu = (x, y) => {
|
||
setMenuState();
|
||
menu.classList.add('is-open');
|
||
menu.setAttribute('aria-hidden', 'false');
|
||
|
||
const rect = menu.getBoundingClientRect();
|
||
const gap = 10;
|
||
const left = Math.min(Math.max(gap, x), window.innerWidth - rect.width - gap);
|
||
const top = Math.min(Math.max(gap, y), window.innerHeight - rect.height - gap);
|
||
|
||
menu.style.left = `${left}px`;
|
||
menu.style.top = `${top}px`;
|
||
};
|
||
|
||
const showNativeHint = () => {
|
||
if (!nativeHint) return;
|
||
window.clearTimeout(nativeHintTimer);
|
||
nativeHint.classList.add('is-visible');
|
||
nativeHintTimer = window.setTimeout(() => {
|
||
nativeHint.classList.remove('is-visible');
|
||
}, 2600);
|
||
};
|
||
|
||
const jumpToComments = () => {
|
||
const target = getCommentsTarget();
|
||
target?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
};
|
||
|
||
const quoteToComments = () => {
|
||
jumpToComments();
|
||
window.setTimeout(() => {
|
||
const textarea = document.querySelector('#twikoo textarea, .twikoo-comments textarea, .tk-input textarea');
|
||
if (!(textarea instanceof HTMLTextAreaElement) || !lastSelectionText) return;
|
||
|
||
const quote = lastSelectionText
|
||
.split(/\r?\n/)
|
||
.map((line) => `> ${line}`)
|
||
.join('\n');
|
||
const current = textarea.value.trim();
|
||
textarea.value = current ? `${current}\n\n${quote}\n` : `${quote}\n`;
|
||
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||
textarea.focus();
|
||
}, 500);
|
||
};
|
||
|
||
const searchSelection = () => {
|
||
if (!lastSelectionText) return;
|
||
const url = `https://www.bing.com/search?q=${encodeURIComponent(lastSelectionText)}`;
|
||
window.open(url, '_blank', 'noopener,noreferrer');
|
||
};
|
||
|
||
const copyImage = async () => {
|
||
const imageUrl = getImageUrl();
|
||
if (!imageUrl) return;
|
||
|
||
try {
|
||
if (!navigator.clipboard?.write || typeof ClipboardItem === 'undefined') {
|
||
throw new Error('Image clipboard is not available.');
|
||
}
|
||
|
||
const response = await fetch(imageUrl, { mode: 'cors' });
|
||
const blob = await response.blob();
|
||
await navigator.clipboard.write([
|
||
new ClipboardItem({ [blob.type || 'image/png']: blob }),
|
||
]);
|
||
} catch {
|
||
await copyText(new URL(imageUrl, window.location.href).toString());
|
||
}
|
||
};
|
||
|
||
const openImage = () => {
|
||
const imageUrl = getImageUrl();
|
||
if (!imageUrl) return;
|
||
window.open(new URL(imageUrl, window.location.href).toString(), '_blank', 'noopener,noreferrer');
|
||
};
|
||
|
||
document.addEventListener('contextmenu', (event) => {
|
||
if (event.ctrlKey) {
|
||
closeMenu();
|
||
nativeHint?.classList.remove('is-visible');
|
||
return;
|
||
}
|
||
|
||
if (isNativeContextTarget(event.target)) return;
|
||
|
||
lastSelectionText = getSelectionText();
|
||
lastImage = getImageTarget(event.target);
|
||
event.preventDefault();
|
||
openMenu(event.clientX, event.clientY);
|
||
showNativeHint();
|
||
});
|
||
|
||
document.addEventListener('click', (event) => {
|
||
const actionTarget = event.target instanceof Element ? event.target.closest('[data-context-action]') : null;
|
||
if (!actionTarget || !menu.contains(actionTarget)) {
|
||
closeMenu();
|
||
return;
|
||
}
|
||
|
||
const action = actionTarget.dataset.contextAction;
|
||
closeMenu();
|
||
|
||
if (action === 'back') history.back();
|
||
if (action === 'forward') history.forward();
|
||
if (action === 'reload') location.reload();
|
||
if (action === 'copy-selection' && lastSelectionText) copyText(lastSelectionText);
|
||
if (action === 'search-selection') searchSelection();
|
||
if (action === 'quote-selection') quoteToComments();
|
||
if (action === 'copy-image') copyImage();
|
||
if (action === 'open-image') openImage();
|
||
if (action === 'copy-image-url') copyText(new URL(getImageUrl(), window.location.href).toString());
|
||
if (action === 'random-post' && typeof window.randomPost === 'function') window.randomPost(actionTarget);
|
||
if (action === 'comments') jumpToComments();
|
||
if (action === 'print') window.print();
|
||
});
|
||
|
||
document.addEventListener('keydown', (event) => {
|
||
if (event.key === 'Escape') closeMenu();
|
||
if (event.key === 'Tab' && menu.classList.contains('is-open')) {
|
||
const items = [...menu.querySelectorAll(focusableSelector)].filter((item) => !item.closest('[hidden]'));
|
||
if (items.length === 0) return;
|
||
const first = items[0];
|
||
const last = items[items.length - 1];
|
||
|
||
if (event.shiftKey && document.activeElement === first) {
|
||
event.preventDefault();
|
||
last.focus();
|
||
} else if (!event.shiftKey && document.activeElement === last) {
|
||
event.preventDefault();
|
||
first.focus();
|
||
}
|
||
}
|
||
});
|
||
|
||
window.addEventListener('scroll', closeMenu, { passive: true });
|
||
window.addEventListener('resize', closeMenu);
|
||
})();
|
||
</script><aside class="pwa-toast" data-pwa-toast hidden role="status" aria-live="polite"><div class="pwa-toast-icon" aria-hidden="true"><i class="fa-solid fa-download"></i></div><div class="pwa-toast-copy"><strong data-pwa-title>PWA 提示</strong><p data-pwa-message></p></div><div class="pwa-toast-actions"><button class="pwa-toast-primary" type="button" data-pwa-primary>确定</button><button class="pwa-toast-close" type="button" data-pwa-close aria-label="关闭提示">稍后</button></div></aside><script src="https://cdn.jsdmirror.com/npm/instantsearch.js@4.56.0"></script><script src="https://cdn.jsdmirror.com/npm/typesense-instantsearch-adapter@2.7.0/dist/typesense-instantsearch-adapter.min.js"></script><script src="https://cdn.jsdmirror.com/npm/@fancyapps/ui@5.0/dist/fancybox/fancybox.umd.js"></script><script src="/js/random.js"></script><script src="/js/search/typesense-search.js"></script><script src="/js/external-links.js"></script><script src="/js/pwa.js"></script><script>
|
||
(() => {
|
||
const nav = document.querySelector('.nav');
|
||
const toggle = document.querySelector('[data-nav-toggle]');
|
||
const menu = document.querySelector('[data-nav-menu]');
|
||
const backdrop = document.querySelector('[data-nav-backdrop]');
|
||
if (!nav || !toggle || !menu) return;
|
||
|
||
function setMenu(open) {
|
||
nav.classList.toggle('is-open', open);
|
||
toggle.setAttribute('aria-expanded', String(open));
|
||
toggle.setAttribute('aria-label', open ? '关闭菜单' : '打开菜单');
|
||
menu.setAttribute('aria-hidden', String(!open && window.innerWidth <= 720));
|
||
document.body.classList.toggle('has-nav-drawer', open);
|
||
}
|
||
|
||
toggle.addEventListener('click', () => {
|
||
setMenu(!nav.classList.contains('is-open'));
|
||
});
|
||
|
||
backdrop?.addEventListener('click', () => setMenu(false));
|
||
|
||
menu.addEventListener('click', (event) => {
|
||
if (event.target instanceof Element && event.target.closest('a')) setMenu(false);
|
||
});
|
||
|
||
document.addEventListener('click', (event) => {
|
||
if (!nav.classList.contains('is-open')) return;
|
||
if (event.target instanceof Node && nav.contains(event.target)) return;
|
||
setMenu(false);
|
||
});
|
||
|
||
document.addEventListener('keydown', (event) => {
|
||
if (event.key === 'Escape' && nav.classList.contains('is-open')) setMenu(false);
|
||
});
|
||
|
||
window.addEventListener('resize', () => {
|
||
if (window.innerWidth > 720) setMenu(false);
|
||
else menu.setAttribute('aria-hidden', String(!nav.classList.contains('is-open')));
|
||
});
|
||
|
||
menu.setAttribute('aria-hidden', String(window.innerWidth <= 720));
|
||
})();
|
||
|
||
(() => {
|
||
const modal = document.querySelector('[data-console-modal]');
|
||
const panel = modal?.querySelector('.control-console-panel');
|
||
const openButtons = document.querySelectorAll('[data-console-open]');
|
||
const closeButtons = modal?.querySelectorAll('[data-console-close]') || [];
|
||
if (!modal || !panel || openButtons.length === 0) return;
|
||
|
||
function setConsole(open) {
|
||
modal.classList.toggle('is-open', open);
|
||
modal.setAttribute('aria-hidden', String(!open));
|
||
document.body.classList.toggle('has-control-console', open);
|
||
if (open) panel.focus();
|
||
}
|
||
|
||
openButtons.forEach((button) => {
|
||
button.addEventListener('click', () => setConsole(true));
|
||
});
|
||
|
||
closeButtons.forEach((button) => {
|
||
button.addEventListener('click', () => setConsole(false));
|
||
});
|
||
|
||
document.addEventListener('keydown', (event) => {
|
||
if (event.key === 'Escape' && modal.classList.contains('is-open')) setConsole(false);
|
||
});
|
||
})();
|
||
|
||
(() => {
|
||
const storageKey = 'site-theme';
|
||
const root = document.documentElement;
|
||
const button = document.querySelector('[data-theme-toggle]');
|
||
const media = window.matchMedia('(prefers-color-scheme: dark)');
|
||
const getSystemTheme = () => (media.matches ? 'dark' : 'light');
|
||
const getSavedTheme = () => {
|
||
try {
|
||
const theme = localStorage.getItem(storageKey);
|
||
return theme === 'dark' || theme === 'light' ? theme : null;
|
||
} catch {
|
||
return null;
|
||
}
|
||
};
|
||
const setSavedTheme = (theme) => {
|
||
try {
|
||
if (theme === getSystemTheme()) localStorage.removeItem(storageKey);
|
||
else localStorage.setItem(storageKey, theme);
|
||
} catch {
|
||
// localStorage can be unavailable in strict privacy modes.
|
||
}
|
||
};
|
||
const getTheme = () => getSavedTheme() || getSystemTheme();
|
||
|
||
function applyTheme(theme) {
|
||
root.dataset.theme = theme;
|
||
root.dataset.themeMode = getSavedTheme() ? 'manual' : 'auto';
|
||
button?.setAttribute('aria-label', theme === 'dark' ? '切换到日间模式' : '切换到夜间模式');
|
||
button?.setAttribute(
|
||
'title',
|
||
getSavedTheme()
|
||
? (theme === 'dark' ? '切换到日间模式' : '切换到夜间模式')
|
||
: `跟随系统:${theme === 'dark' ? '夜间' : '日间'}模式`
|
||
);
|
||
}
|
||
|
||
applyTheme(getTheme());
|
||
|
||
button?.addEventListener('click', () => {
|
||
const nextTheme = root.dataset.theme === 'dark' ? 'light' : 'dark';
|
||
setSavedTheme(nextTheme);
|
||
applyTheme(nextTheme);
|
||
});
|
||
|
||
const handleSystemThemeChange = () => {
|
||
if (!getSavedTheme()) applyTheme(getSystemTheme());
|
||
};
|
||
|
||
if (typeof media.addEventListener === 'function') {
|
||
media.addEventListener('change', handleSystemThemeChange);
|
||
} else if (typeof media.addListener === 'function') {
|
||
media.addListener(handleSystemThemeChange);
|
||
}
|
||
})();
|
||
|
||
(() => {
|
||
const runtime = document.getElementById('site-runtime');
|
||
if (!runtime) return;
|
||
|
||
const start = new Date(window.SITE_CONFIG.site.startTime).getTime();
|
||
const pad = (value) => String(value).padStart(2, '0');
|
||
|
||
function updateRuntime() {
|
||
const diff = Math.max(0, Date.now() - start);
|
||
const days = Math.floor(diff / 86400000);
|
||
const hours = Math.floor((diff / 3600000) % 24);
|
||
const minutes = Math.floor((diff / 60000) % 60);
|
||
const seconds = Math.floor((diff / 1000) % 60);
|
||
runtime.textContent = `${days} 天 ${pad(hours)} 时 ${pad(minutes)} 分 ${pad(seconds)} 秒`;
|
||
}
|
||
|
||
updateRuntime();
|
||
window.setInterval(updateRuntime, 1000);
|
||
})();
|
||
</script></body></html><script>(function(){const postSlug = "41b9aff7";
|
||
|
||
(() => {
|
||
const article = document.querySelector('.post-article');
|
||
const progressBar = document.querySelector('[data-reading-progress]');
|
||
const resume = document.querySelector('[data-reading-resume]');
|
||
const resumePercent = document.querySelector('[data-reading-resume-percent]');
|
||
const continueButton = document.querySelector('[data-reading-continue]');
|
||
const restartButton = document.querySelector('[data-reading-restart]');
|
||
if (!article || !progressBar || !resume) return;
|
||
|
||
const storageKey = `reading-progress:${postSlug}`;
|
||
let saved = null;
|
||
let canPersist = true;
|
||
let frame = 0;
|
||
|
||
try {
|
||
saved = JSON.parse(localStorage.getItem(storageKey) || 'null');
|
||
} catch {
|
||
saved = null;
|
||
}
|
||
|
||
const articleMetrics = () => {
|
||
const top = article.getBoundingClientRect().top + window.scrollY;
|
||
const distance = Math.max(1, article.offsetHeight - window.innerHeight);
|
||
return { top, distance };
|
||
};
|
||
|
||
const getProgress = () => {
|
||
const { top, distance } = articleMetrics();
|
||
return Math.min(1, Math.max(0, (window.scrollY - top) / distance));
|
||
};
|
||
|
||
const persist = (progress) => {
|
||
if (!canPersist) return;
|
||
try {
|
||
if (progress >= 0.97) localStorage.removeItem(storageKey);
|
||
else if (progress >= 0.02) localStorage.setItem(storageKey, JSON.stringify({ progress, savedAt: Date.now() }));
|
||
} catch {
|
||
// Storage can be unavailable in strict privacy modes.
|
||
}
|
||
};
|
||
|
||
const update = () => {
|
||
frame = 0;
|
||
const progress = getProgress();
|
||
progressBar.style.transform = `scaleX(${progress})`;
|
||
persist(progress);
|
||
};
|
||
|
||
const requestUpdate = () => {
|
||
if (!frame) frame = window.requestAnimationFrame(update);
|
||
};
|
||
|
||
const closeResume = () => {
|
||
resume.hidden = true;
|
||
canPersist = true;
|
||
};
|
||
|
||
const savedIsFresh = saved && Date.now() - Number(saved.savedAt) < 90 * 86400000;
|
||
const savedProgress = Number(saved?.progress);
|
||
if (savedIsFresh && savedProgress >= 0.05 && savedProgress < 0.97) {
|
||
canPersist = false;
|
||
if (resumePercent) resumePercent.textContent = `${Math.round(savedProgress * 100)}%`;
|
||
window.setTimeout(() => { resume.hidden = false; }, 350);
|
||
}
|
||
|
||
continueButton?.addEventListener('click', () => {
|
||
closeResume();
|
||
const { top, distance } = articleMetrics();
|
||
window.scrollTo({ top: top + distance * savedProgress, behavior: 'smooth' });
|
||
});
|
||
|
||
restartButton?.addEventListener('click', () => {
|
||
try { localStorage.removeItem(storageKey); } catch {}
|
||
closeResume();
|
||
window.scrollTo({ top: Math.max(0, articleMetrics().top - 90), behavior: 'smooth' });
|
||
});
|
||
|
||
update();
|
||
window.addEventListener('scroll', requestUpdate, { passive: true });
|
||
window.addEventListener('resize', requestUpdate);
|
||
window.addEventListener('pagehide', () => persist(getProgress()));
|
||
})();
|
||
})();</script><script>
|
||
(() => {
|
||
document.querySelectorAll('.article-outdated-note[data-freshness-at]').forEach((note) => {
|
||
const freshnessAt = new Date(note.dataset.freshnessAt || '');
|
||
const hasNewVersion = note.dataset.hasNewVersion === 'true';
|
||
if (Number.isNaN(freshnessAt.valueOf())) return;
|
||
|
||
const threshold = new Date();
|
||
threshold.setFullYear(threshold.getFullYear() - 1);
|
||
note.hidden = !hasNewVersion && freshnessAt >= threshold;
|
||
});
|
||
})();
|
||
</script><script>
|
||
(() => {
|
||
const prose = document.querySelector('.prose[data-toc]');
|
||
if (!prose) return;
|
||
|
||
let toc = [];
|
||
try {
|
||
toc = JSON.parse(prose.dataset.toc || '[]');
|
||
} catch {
|
||
toc = [];
|
||
}
|
||
|
||
prose.querySelectorAll('h1, h2, h3').forEach((heading, index) => {
|
||
const item = toc[index];
|
||
if (item?.id && !heading.id) heading.id = item.id;
|
||
});
|
||
|
||
const tocTree = document.querySelector('[data-toc-tree]');
|
||
if (tocTree && toc.length > 0) {
|
||
const links = [...tocTree.querySelectorAll('[data-toc-link]')];
|
||
const groups = [...tocTree.querySelectorAll('[data-toc-group]')];
|
||
const headings = toc
|
||
.map((item) => document.getElementById(item.id))
|
||
.filter(Boolean);
|
||
|
||
const setActiveToc = (id) => {
|
||
links.forEach((link) => {
|
||
const isActive = link.getAttribute('href') === `#${id}`;
|
||
link.classList.toggle('is-active', isActive);
|
||
if (isActive) link.setAttribute('aria-current', 'location');
|
||
else link.removeAttribute('aria-current');
|
||
});
|
||
|
||
const activeLink = tocTree.querySelector(`[data-toc-link][href="#${CSS.escape(id)}"]`);
|
||
const activeGroup = activeLink?.closest('[data-toc-group]');
|
||
groups.forEach((group) => group.classList.toggle('is-open', group === activeGroup));
|
||
};
|
||
|
||
const updateToc = () => {
|
||
const anchorOffset = 132;
|
||
let activeId = headings[0]?.id;
|
||
|
||
headings.forEach((heading) => {
|
||
if (heading.getBoundingClientRect().top <= anchorOffset) activeId = heading.id;
|
||
});
|
||
|
||
if (activeId) setActiveToc(activeId);
|
||
};
|
||
|
||
tocTree.addEventListener('click', (event) => {
|
||
const link = event.target.closest?.('[data-toc-link]');
|
||
if (!link) return;
|
||
|
||
const group = link.closest('[data-toc-group]');
|
||
groups.forEach((item) => item.classList.toggle('is-open', item === group));
|
||
});
|
||
|
||
updateToc();
|
||
window.addEventListener('scroll', updateToc, { passive: true });
|
||
window.addEventListener('resize', updateToc);
|
||
}
|
||
|
||
})();
|
||
</script><script>
|
||
(() => {
|
||
const prose = document.querySelector('.post-article .prose');
|
||
if (!prose) return;
|
||
|
||
const collapsedLineCount = 8;
|
||
const copyLabels = {
|
||
idle: '复制',
|
||
done: '已复制',
|
||
error: '复制失败',
|
||
};
|
||
|
||
const languageNames = {
|
||
bash: 'Bash',
|
||
c: 'C',
|
||
cpp: 'C++',
|
||
cs: 'C#',
|
||
css: 'CSS',
|
||
html: 'HTML',
|
||
js: 'JavaScript',
|
||
json: 'JSON',
|
||
jsx: 'JSX',
|
||
md: 'Markdown',
|
||
powershell: 'PowerShell',
|
||
py: 'Python',
|
||
shell: 'Shell',
|
||
sh: 'Shell',
|
||
ts: 'TypeScript',
|
||
tsx: 'TSX',
|
||
txt: 'Text',
|
||
yaml: 'YAML',
|
||
yml: 'YAML',
|
||
};
|
||
|
||
function getCodeLanguage(pre, code) {
|
||
const rawLanguage =
|
||
pre.dataset.language ||
|
||
code.dataset.language ||
|
||
[...pre.classList, ...code.classList]
|
||
.find((className) => className.startsWith('language-'))
|
||
?.replace(/^language-/, '') ||
|
||
'';
|
||
|
||
const normalized = rawLanguage.trim().toLowerCase();
|
||
return languageNames[normalized] || normalized.toUpperCase() || 'Code';
|
||
}
|
||
|
||
function getCodeText(code, lines) {
|
||
if (lines.length > 0) return lines.map((line) => line.textContent || '').join('\n');
|
||
return code.textContent || '';
|
||
}
|
||
|
||
prose.querySelectorAll('img').forEach((image) => {
|
||
if (image.closest('a, .article-header-cover')) return;
|
||
|
||
const src = image.currentSrc || image.getAttribute('src');
|
||
if (!src) return;
|
||
|
||
const link = document.createElement('a');
|
||
link.className = 'fancybox';
|
||
link.href = src;
|
||
link.dataset.fancybox = 'article-gallery';
|
||
link.dataset.caption = image.getAttribute('alt') || '';
|
||
image.parentNode?.insertBefore(link, image);
|
||
link.appendChild(image);
|
||
});
|
||
|
||
prose.querySelectorAll('table').forEach((table) => {
|
||
if (table.closest('.table-scroll')) return;
|
||
|
||
const wrapper = document.createElement('div');
|
||
wrapper.className = 'table-scroll';
|
||
table.parentNode?.insertBefore(wrapper, table);
|
||
wrapper.appendChild(table);
|
||
});
|
||
|
||
prose.querySelectorAll('.butterfly-hide-inline').forEach((item) => {
|
||
const button = item.querySelector('.butterfly-hide-toggle');
|
||
if (!button) return;
|
||
|
||
button.addEventListener('click', () => {
|
||
item.classList.toggle('is-open');
|
||
});
|
||
});
|
||
|
||
if (window.Fancybox) {
|
||
window.Fancybox.bind('[data-fancybox="article-gallery"]', {
|
||
compact: false,
|
||
dragToClose: true,
|
||
Images: {
|
||
zoom: true,
|
||
},
|
||
});
|
||
}
|
||
|
||
async function copyText(text) {
|
||
if (navigator.clipboard?.writeText) {
|
||
await navigator.clipboard.writeText(text);
|
||
return;
|
||
}
|
||
|
||
const textarea = document.createElement('textarea');
|
||
textarea.value = text;
|
||
textarea.setAttribute('readonly', '');
|
||
textarea.style.position = 'fixed';
|
||
textarea.style.top = '-9999px';
|
||
textarea.style.opacity = '0';
|
||
document.body.appendChild(textarea);
|
||
textarea.select();
|
||
document.execCommand('copy');
|
||
textarea.remove();
|
||
}
|
||
|
||
prose.querySelectorAll('pre').forEach((pre) => {
|
||
if (pre.closest('.code-block')) return;
|
||
|
||
const code = pre.querySelector('code');
|
||
if (!code) return;
|
||
|
||
const wrapper = document.createElement('div');
|
||
wrapper.className = 'code-block';
|
||
pre.parentNode?.insertBefore(wrapper, pre);
|
||
wrapper.appendChild(pre);
|
||
|
||
const header = document.createElement('div');
|
||
header.className = 'code-block-header';
|
||
|
||
const language = document.createElement('span');
|
||
language.className = 'code-block-language';
|
||
language.textContent = getCodeLanguage(pre, code);
|
||
|
||
const toolbar = document.createElement('div');
|
||
toolbar.className = 'code-block-toolbar';
|
||
|
||
const copyButton = document.createElement('button');
|
||
copyButton.className = 'code-block-copy';
|
||
copyButton.type = 'button';
|
||
copyButton.textContent = copyLabels.idle;
|
||
copyButton.setAttribute('aria-label', '复制代码');
|
||
|
||
const toggleButton = document.createElement('button');
|
||
toggleButton.className = 'code-block-toggle';
|
||
toggleButton.type = 'button';
|
||
toggleButton.textContent = '展开';
|
||
toggleButton.setAttribute('aria-expanded', 'false');
|
||
|
||
const expandCue = document.createElement('button');
|
||
expandCue.className = 'code-block-expand-cue';
|
||
expandCue.type = 'button';
|
||
expandCue.setAttribute('aria-label', 'Expand code block');
|
||
expandCue.setAttribute('aria-expanded', 'false');
|
||
expandCue.innerHTML = '<span aria-hidden="true"></span>';
|
||
|
||
toolbar.append(copyButton, toggleButton);
|
||
header.append(language, toolbar);
|
||
wrapper.appendChild(header);
|
||
wrapper.appendChild(expandCue);
|
||
|
||
const codeLines = [...code.querySelectorAll(':scope > .line')];
|
||
if (codeLines.length > 0) {
|
||
wrapper.classList.add('has-line-numbers');
|
||
wrapper.style.setProperty('--line-number-width', `${String(codeLines.length).length + 1}ch`);
|
||
codeLines.forEach((line, index) => {
|
||
line.dataset.line = String(index + 1);
|
||
});
|
||
}
|
||
|
||
const lineCount = codeLines.length || (code.textContent || '').split(/\r?\n/).length;
|
||
if (lineCount > collapsedLineCount) wrapper.classList.add('is-collapsible');
|
||
|
||
copyButton.addEventListener('click', async () => {
|
||
try {
|
||
await copyText(getCodeText(code, codeLines));
|
||
copyButton.textContent = copyLabels.done;
|
||
window.setTimeout(() => {
|
||
copyButton.textContent = copyLabels.idle;
|
||
}, 1400);
|
||
} catch {
|
||
copyButton.textContent = copyLabels.error;
|
||
window.setTimeout(() => {
|
||
copyButton.textContent = copyLabels.idle;
|
||
}, 1600);
|
||
}
|
||
});
|
||
|
||
function toggleExpanded() {
|
||
const isExpanded = wrapper.classList.toggle('is-expanded');
|
||
toggleButton.textContent = isExpanded ? '收起' : '展开';
|
||
toggleButton.setAttribute('aria-expanded', String(isExpanded));
|
||
expandCue.setAttribute('aria-expanded', String(isExpanded));
|
||
}
|
||
|
||
toggleButton.addEventListener('click', toggleExpanded);
|
||
expandCue.addEventListener('click', toggleExpanded);
|
||
});
|
||
})();
|
||
</script> |