<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <id>https://seasir.top/</id>
  <title type="text">Hyde Blog</title>
  <subtitle type="text">人心中的成见是一座大山</subtitle>
  <updated>2026-08-02T00:00:00.000Z</updated>
  <author><name>Hyde</name></author>
  <link rel="alternate" href="https://seasir.top/"/>
  <link rel="self" href="https://seasir.top/atom.xml"/>
  <generator uri="https://github.com/CuteLeaf/Firefly">Firefly v6.16.8</generator>
    <entry>
      <id>https://seasir.top/posts/todo/</id>
      <title type="text">待办</title>
      <published>2026-05-09T00:00:00.000Z</published>
      <updated>2026-05-09T00:00:00.000Z</updated>
      <author><name>Hyde</name></author>
      <link rel="alternate" href="https://seasir.top/posts/todo/"/>
      <summary type="text">最近又在折腾我的 Astro 博客啦，顺手整理了一份接下来的开发小计划。想给博客加个朋友圈和友链申请，顺便修一修之前遇到的小 Bug。这里也记了几个好用的工具链接当备忘录，大家一起来看看我最近又搞了哪些新花样吧。</summary>
      <content type="html"><![CDATA[<section><h2>功能<a href="#功能"><span>#</span></a></h2><ul>
<li>
<p> <a href="https://www.zzyang.top/posts/ech0/" target="_blank">Astro 集成 ech0 实现朋友圈</a></p>
</li>
<li>
<p> <a href="https://astro.cchaoka.cn/friends/" target="_blank">Astro 集成 友链自助申请按钮添加</a></p>
</li>
<li>
<p> <a href="https://tbmiao.dpdns.org/posts/ustat-tongji/" target="_blank">添加ustat统计</a></p>
</li>
<li>
<p> <a href="https://qwq.sigrika.cc/comic-events/" target="_blank">漫展日程</a></p>
</li>
<li>
<p> <a href="https://blog.xuwupiaomiao.cn/" target="_blank">足迹1</a></p>
</li>
<li>
<p> <a href="https://blog.huchao.vip/blog/" target="_blank">足迹2</a></p>
</li>
<li>
<p> <a href="https://blog.tsh520.cn/posts/%E5%8D%9A%E5%AE%A2%E6%8C%87%E5%8D%97/ai%E6%91%98%E8%A6%81%E6%89%93%E5%AD%97%E6%9C%BA%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97/" target="_blank">Astro 集成 AI文章摘要</a></p>
</li>
<li>
<p> <a href="https://uapis.cn/docs/api-reference/get-misc-weather" target="_blank">天气组件</a></p>
</li>
<li>
<p> <a href="https://tbmiao.dpdns.org/posts/blogdevtoolswarning/" target="_blank">给你的Astro博客添加开发者工具提示信息功能</a></p>
</li>
<li>
<p> <a href="https://seasir.top/music/">音乐页面</a></p>
</li>
</ul></section>
<section><h2>BUG<a href="#bug"><span>#</span></a></h2><ul>
<li> 修复生产环境下顶部滚动条刷新页面不显示</li>
<li> 自定义Footer页面生产环境不显示主题版本号，本地环境显示</li>
<li> 浏览量一直加载中…</li>
<li> <a href="https://seasir.top/moments/">朋友圈远程说说首次不加载</a></li>
</ul></section>
<section><h2>优化<a href="#优化"><span>#</span></a></h2><ul>
<li> 右侧滚动条优化，和<a href="https://teek.seasir.top/" target="_blank">teek</a>保持一致</li>
</ul></section>
<section><h2>其他<a href="#其他"><span>#</span></a></h2><p>获取歌单网址：<a href="https://163.hyc.moe/" target="_blank">https://163.hyc.moe/</a></p><p>高德地图坐标拾取器（推荐，GCJ-02）: <a href="https://lbs.amap.com/tools/picker" target="_blank">https://lbs.amap.com/tools/picker</a></p></section>]]></content>
    </entry>
    <entry>
      <id>https://seasir.top/posts/pnpm-workspace/</id>
      <title type="text">PNPM 11 适配 CNB 构建安全兼容方案</title>
      <published>2026-08-02T00:00:00.000Z</published>
      <updated>2026-08-02T00:00:00.000Z</updated>
      <author><name>Hyde</name></author>
      <link rel="alternate" href="https://seasir.top/posts/pnpm-workspace/"/>
      <summary type="text">解决PNPM 11+ 版本 升级安全策略后，CNB（Cloud Native Buildpacks）云构建环境依赖安装失败 的问题，同时明确临时兼容方案、长期安全最优方案、团队使用规范，兼顾项目构建可用性与供应链安全。
适用场景：使用 PNPM 11+、CNB 流水线构建、项目存在带构建脚本的第三方依赖/私有依赖
适配环境：本地开发环境、CNB 容器构建环境</summary>
      <content type="html"><![CDATA[<section><h2>问题根因<a href="#问题根因"><span>#</span></a></h2><p>PNPM 11 进行了重大安全策略升级，默认开启两大防护机制，与无交互的 CNB 构建环境冲突，导致安装依赖失败</p><section><h3>构建脚本严格校验（核心报错）<a href="#构建脚本严格校验核心报错"><span>#</span></a></h3><p>PNPM 11 默认开启 <code>strictDepBuilds: true</code>，禁止所有第三方依赖自动执行 <code>postinstall/preinstall</code> 构建脚本。
本地开发可通过交互式命令 <code>pnpm approve-builds</code> 手动授权白名单，但 CNB 构建环境无 TTY 交互终端，无法手动确认，直接抛出错误：<code>ERR_PNPM_IGNORED_BUILDS</code>，构建中断</p></section><section><h3>冷却机制校验<a href="#冷却机制校验"><span>#</span></a></h3><p>PNPM 11 默认开启 minimumReleaseAge: 1440（24小时发布冷却），禁止安装发布时间不足24小时的新版本包。
项目迭代中经常会更新内部私有包、最新开源依赖，这类包发布时间短，会触发冷却校验报错：<code>ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION</code>，导致 CNB 构建失败。</p></section></section>
<section><h2>兼容方案<a href="#兼容方案"><span>#</span></a></h2><p>在项目根目录 pnpm-workspace.yaml 中添加以下配置：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 临时兼容CNB无交互构建：放开所有依赖构建脚本</span></div></div><div><div><div>2</div></div><div><span>dangerouslyAllowAllBuilds</span><span>: </span><span>true</span></div></div><div><div><div>3</div></div><div><span># 临时关闭新版本包24小时冷却校验，适配私有包高频迭代</span></div></div><div><div><div>4</div></div><div><span>minimumReleaseAge</span><span>: </span><span>0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><strong>dangerouslyAllowAllBuilds: true</strong>：绕过脚本白名单机制，允许所有第三方依赖自动执行构建脚本，解决 CNB 交互阻塞报错</li>
<li><strong>minimumReleaseAge: 0</strong>：关闭包发布冷却安全策略，允许安装刚发布的新版私有包、开源依赖</li>
</ul><section><h3>明确风险<a href="#明确风险"><span>#</span></a></h3><p>该配置会临时关闭 PNPM11 两大核心供应链安全防护：</p><ul>
<li>放开所有依赖 postinstall 恶意脚本执行风险</li>
<li>无法拦截账号劫持后新发的恶意版本包，无24小时安全缓冲窗口期
使用边界：仅用于内部业务项目临时过渡，<strong><code>禁止长期永久使用</code></strong>，需尽快迁移至安全方案。</li>
</ul></section></section>
<section><h2>长期安全方案<a href="#长期安全方案"><span>#</span></a></h2><p>摒弃高危全局降级配置，采用精准白名单+局部豁免机制，保留 PNPM 核心安全能力，同时适配 CNB 构建</p><p><strong>操作步骤:</strong></p><ol>
<li>
<p>本地有交互终端执行，自动生成可信脚本白名单
pnpm approve-builds
按需授权项目所需的构建依赖（如 esbuild、sharp、@parcel/watcher 等），自动写入配置文件</p>
</li>
<li>
<p>私有包豁免冷却校验：不全局关闭冷却，仅放行内部可信包</p>
</li>
<li>
<p>提交更新后的 pnpm-workspace.yaml 到代码仓库，CNB 直接无感知构建</p>
</li>
</ol><p><strong>最终安全配置模板</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 开启严格脚本校验（保留安全能力）</span></div></div><div><div><div>2</div></div><div><span>strictDepBuilds</span><span>: </span><span>true</span></div></div><div><div><div>3</div></div><div><span># 仅放行项目可信的带构建脚本依赖（精准白名单）</span></div></div><div><div><div>4</div></div><div><span>allowBuilds</span><span>:</span></div></div><div><div><div>5</div></div><div><span>  </span><span>esbuild</span><span>: </span><span>true</span></div></div><div><div><div>6</div></div><div><span>  </span><span>sharp</span><span>: </span><span>true</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>@</span><span>parcel/watcher</span><span>: </span><span>true</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span># 保留公共包24小时冷却防护</span></div></div><div><div><div>10</div></div><div><span>minimumReleaseAge</span><span>: </span><span>1440</span></div></div><div><div><div>11</div></div><div><span># 仅内部私有包豁免冷却，适配高频迭代</span></div></div><div><div><div>12</div></div><div><span>minimumReleaseAgeExclude</span><span>:</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>- </span><span>"@你的组织/*"</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>CNB 构建命令规范<a href="#cnb-构建命令规范"><span>#</span></a></h2><p>无论临时方案还是安全方案，CNB 流水线统一使用安全构建命令，杜绝版本浮动风险：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 锁定锁文件，禁止自动更新依赖版本</span></div></div><div><div><div>2</div></div><div><span>pnpm</span><span> </span><span>install</span><span> </span><span>--frozen-lockfile</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>同时在项目 package.json 锁定 PNPM 版本，避免环境策略不一致：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"packageManager"</span><span>: </span><span>"pnpm@11.4.0"</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>]]></content>
    </entry>
    <entry>
      <id>https://seasir.top/posts/ai-summary-tutorial/</id>
      <title type="text">AI 摘要实现</title>
      <published>2026-07-27T00:00:00.000Z</published>
      <updated>2026-07-27T00:00:00.000Z</updated>
      <author><name>Hyde</name></author>
      <link rel="alternate" href="https://seasir.top/posts/ai-summary-tutorial/"/>
      <summary type="text">最近给博客折腾了个 AI 摘要功能，效果挺有意思。今天就把我在 Firefly 项目里摸爬滚打总结的实战经验全盘托出。从挑大模型、写构建脚本到搞定打字机动画，顺便分享几个我踩过的坑，希望能帮想给博客加 AI 元素的你少走点弯路。</summary>
      <content type="html"><![CDATA[<p>AI 摘要提前在构建阶段生成并存入文章元数据，前端页面读取后带动画展示，页面访问无需实时调用 AI，无额外接口开销。</p>
<section><h2>架构概览<a href="#架构概览"><span>#</span></a></h2><p>Firefly 博客的 AI 摘要功能分为两部分：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>┌─────────────────────────────────────────────────────┐</span></div></div><div><div><div>2</div></div><div><span>│                   构建时 (Build Time)                │</span></div></div><div><div><div>3</div></div><div><span>│                                                     │</span></div></div><div><div><div>4</div></div><div><span>│  scripts/fill-descriptions/index.ts                 │</span></div></div><div><div><div>5</div></div><div><span>│  ├── 扫描 src/content/posts/ 下所有 .md/.mdx         │</span></div></div><div><div><div>6</div></div><div><span>│  ├── 跳过已有 description 的文章                      │</span></div></div><div><div><div>7</div></div><div><span>│  ├── 调用千问 API 生成摘要                            │</span></div></div><div><div><div>8</div></div><div><span>│  └── 写回 frontmatter (description + descriptionSource) │</span></div></div><div><div><div>9</div></div><div><span>└─────────────────────────────────────────────────────┘</span></div></div><div><div><div>10</div></div><div><span><span>                          </span></span><span>↓</span></div></div><div><div><div>11</div></div><div><span>┌─────────────────────────────────────────────────────┐</span></div></div><div><div><div>12</div></div><div><span>│                   运行时 (Runtime)                   │</span></div></div><div><div><div>13</div></div><div><span>│                                                     │</span></div></div><div><div><div>14</div></div><div><span>│  src/components/widget/AiSummary.astro              │</span></div></div><div><div><div>15</div></div><div><span>│  ├── 读取 description 和 descriptionSource           │</span></div></div><div><div><div>16</div></div><div><span>│  ├── IntersectionObserver 监听滚动进入视口            │</span></div></div><div><div><div>17</div></div><div><span>│  ├── 逐字打字机动画，标点处自动停顿                    │</span></div></div><div><div><div>18</div></div><div><span>│  └── astro:page-load 支持 Swup 站内导航              │</span></div></div><div><div><div>19</div></div><div><span>└─────────────────────────────────────────────────────┘</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>文件目录<a href="#文件目录"><span>#</span></a></h2>

<table><thead><tr><th>文件路径</th><th>作用</th></tr></thead><tbody><tr><td><code>src/components/widget/AiSummary.astro</code></td><td>摘要卡片 + 打字机动画</td></tr><tr><td><code>src/pages/posts/[...slug].astro</code></td><td>文章页条件渲染摘要组件</td></tr><tr><td><code>src/content.config.ts</code></td><td><code>description</code> / <code>descriptionSource</code> 字段校验</td></tr><tr><td><code>scripts/fill-descriptions/index.ts</code></td><td>千问 API 批量补全缺失摘要</td></tr><tr><td><code>.env</code></td><td>存放 <code>QWEN_API_KEY</code>（<code>勿提交 Git</code>）</td></tr></tbody></table><hr /></section>
<section><h2>配置 API 密钥<a href="#配置-api-密钥"><span>#</span></a></h2><p>在项目根目录创建 <code>.env</code> 文件：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 千问 API 配置</span></div></div><div><div><div>2</div></div><div><span>QWEN_API_KEY=你的API密钥</span></div></div><div><div><div>3</div></div><div><span>QWEN_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1</span></div></div><div><div><div>4</div></div><div><span>QWEN_MODEL=qwen-plus</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div></div><div>注意</div></div><div><p>📌 将 <code>.env</code> 添加到 <code>.gitignore</code>，避免泄露密钥！</p></div></div><p>密钥在 <a href="https://dashscope.console.aliyun.com/" target="_blank">阿里云 DashScope 控制台</a> 申请。脚本通过 <code>import.meta.url</code> 定位项目根目录并读取 <code>.env</code> 文件（兼容 Windows <code>\r\n</code> 换行），也可临时用环境变量：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>QWEN_API_KEY</span><span>=</span><span>sk-xxx</span><span> </span><span>pnpm</span><span> </span><span>fill-descriptions</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可选环境变量：</p>

<table><thead><tr><th>变量</th><th>默认值</th><th>说明</th></tr></thead><tbody><tr><td><code>QWEN_API_KEY</code></td><td>（必填）</td><td>千问 API 密钥</td></tr><tr><td><code>QWEN_MODEL</code></td><td><code>qwen-plus</code></td><td>文本模型；<strong>不要用</strong> <code>qwen-math-turbo</code> 等数学/代码模型</td></tr><tr><td><code>QWEN_BASE_URL</code></td><td>DashScope 兼容端点</td><td>使用自定义 MaaS 端点时再改</td></tr></tbody></table></section>
<section><h2>构建时脚本详解<a href="#构建时脚本详解"><span>#</span></a></h2><p>脚本路径：<code>scripts/fill-descriptions/index.ts</code></p><section><h3>核心配置<a href="#核心配置"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>const</span><span> </span><span>QWEN_BASE_URL</span><span> </span><span>=</span><span><span> </span><span>process</span><span>.</span></span><span>env</span><span>.</span><span>QWEN_BASE_URL</span><span> </span><span>||</span><span> </span><span>"https://dashscope.aliyuncs.com/compatible-mode/v1"</span><span>;</span></div></div><div><div><div>2</div></div><div><span>const</span><span> </span><span>QWEN_MODEL</span><span> </span><span>=</span><span><span> </span><span>process</span><span>.</span></span><span>env</span><span>.</span><span>QWEN_MODEL</span><span> </span><span>||</span><span> </span><span>"qwen-plus"</span><span>;</span></div></div><div><div><div>3</div></div><div><span>const</span><span> </span><span>QWEN_API_KEY</span><span> </span><span>=</span><span><span> </span><span>process</span><span>.</span></span><span>env</span><span>.</span><span>QWEN_API_KEY</span><span> </span><span>||</span><span> </span><span>""</span><span>;</span></div></div><div><div><div>4</div></div><div><span>const</span><span> </span><span>POSTS_DIR</span><span> </span><span>=</span><span><span> </span><span>path</span><span>.</span></span><span>join</span><span>(</span><span>PROJECT_ROOT</span><span>, </span><span>"src/content/posts"</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>脚本做了什么<a href="#脚本做了什么"><span>#</span></a></h3><ul>
<li>递归扫描 src/content/posts/ 下所有 .md / .mdx</li>
<li>仅在 frontmatter 区块检测是否存在 description，不会误判正文代码块内的示例 <code>description:</code></li>
<li>提取文章标题 + 正文前2600字作为生成上下文</li>
<li>调用千问 API，使用优化提示词自动生成文章摘要</li>
<li>校验摘要输出质量（长度校验、过滤**等无效内容），不合格自动重试生成</li>
<li>重写文件 frontmatter，写入 <code>description</code> 与 <code>descriptionSource: ai</code></li>
<li>仅修改 frontmatter 区域，文章正文内容完全不改动</li>
</ul></section><section><h3>扫描与过滤<a href="#扫描与过滤"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/**</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>* 判断文章是否已有 description（只检查 frontmatter 区块内，且值不能为空）</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>*/</span></div></div><div><div><div>4</div></div><div><span>function</span><span> </span><span>hasDescription</span><span><span>(</span><span>raw</span></span><span>:</span><span> </span><span>string</span><span>)</span><span>:</span><span> </span><span>boolean</span><span> {</span></div></div><div><div><div>5</div></div><div><span>  </span><span>const</span><span> </span><span>parsed</span><span> </span><span>=</span><span> </span><span>parseFrontmatter</span><span><span>(</span><span>raw</span><span>);</span></span></div></div><div><div><div>6</div></div><div><span>  </span><span>if</span><span> (</span><span>!</span><span><span>parsed</span><span>) </span></span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>  </span><span>const</span><span> </span><span>descMatch</span><span> </span><span>=</span><span><span> </span><span>parsed</span><span>.</span></span><span>frontmatter</span><span>.</span><span>match</span><span>(</span><span>/</span><span>^</span><span>description</span><span>\s</span><span><span>*</span><span>:</span></span><span>\s</span><span><span>*</span><span>(</span></span><span>.</span><span><span>+</span><span>)</span></span><span>$</span><span>/</span><span>m</span><span>);</span></div></div><div><div><div>9</div></div><div><span>  </span><span>if</span><span> (</span><span>!</span><span><span>descMatch</span><span>) </span></span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>  </span><span>let</span><span><span> </span><span>value</span><span> </span></span><span>=</span><span><span> </span><span>descMatch</span><span>[</span></span><span>1</span><span>].</span><span>trim</span><span>();</span></div></div><div><div><div>12</div></div><div><span>  </span><span>// 去掉引号</span></div></div><div><div><div>13</div></div><div><span>  </span><span>if</span><span><span> ((</span><span>value</span><span>.</span></span><span>startsWith</span><span>(</span><span>'"'</span><span>) </span><span>&amp;&amp;</span><span><span> </span><span>value</span><span>.</span></span><span>endsWith</span><span>(</span><span>'"'</span><span>)) </span><span>||</span><span><span> (</span><span>value</span><span>.</span></span><span>startsWith</span><span>(</span><span>"'"</span><span>) </span><span>&amp;&amp;</span><span><span> </span><span>value</span><span>.</span></span><span>endsWith</span><span>(</span><span>"'"</span><span>))) {</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>value</span><span> </span><span>=</span><span><span> </span><span>value</span><span>.</span></span><span>slice</span><span>(</span><span>1</span><span>, </span><span>-</span><span>1</span><span>);</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>  </span><span>return</span><span><span> </span><span>value</span><span>.</span></span><span>length</span><span> </span><span>&gt;</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div></div><div>注意</div></div><div><p>⚠️ <strong>新手陷阱</strong>：若用整篇文件匹配 <code>description:</code>，在正文里展示 frontmatter 示例的文章会被误判为”已有描述”。务必限定在 frontmatter 区块内检测！</p></div></div></section><section><h3>上下文提取<a href="#上下文提取"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>function</span><span> </span><span>extractContext</span><span><span>(</span><span>body</span></span><span>:</span><span> </span><span>string</span><span><span>, </span><span>maxChars</span></span><span>:</span><span> </span><span>number</span><span>)</span><span>:</span><span> </span><span>string</span><span> {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>const</span><span> </span><span>cleaned</span><span> </span><span>=</span><span><span> </span><span>body</span></span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>.</span><span>replace</span><span>(</span><span>/</span><span>^</span><span>---</span><span><span>[</span><span>\s\S</span><span>]</span></span><span><span>*?</span><span>---</span></span><span>\n</span><span><span>?</span><span>/</span></span><span>, </span><span>""</span><span>)           </span><span>// 去掉 frontmatter</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>.</span><span>replace</span><span>(</span><span><span>/#</span><span>{1,6}</span></span><span>\s</span><span><span>+</span><span>/</span></span><span>g</span><span>, </span><span>""</span><span>)                   </span><span>// 去掉标题标记</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>.</span><span>replace</span><span>(</span><span>/```</span><span><span>[</span><span>\s\S</span><span>]</span></span><span><span>*?</span><span>```/</span></span><span>g</span><span>, </span><span>"[代码块]"</span><span>)       </span><span>// 代码块替换</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>.</span><span>replace</span><span>(</span><span>/`</span><span>[</span><span>^</span><span>`]</span><span><span>+</span><span>`/</span></span><span>g</span><span>, </span><span>"[代码]"</span><span>)               </span><span>// 行内代码替换</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>.</span><span>replace</span><span>(</span><span><span>/!</span><span>\[</span></span><span>.</span><span><span>*?</span><span>\]\(</span></span><span>.</span><span><span>*?</span><span>\)</span><span>/</span></span><span>g</span><span>, </span><span>""</span><span>)             </span><span>// 去掉图片</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>.</span><span>replace</span><span>(</span><span><span>/</span><span>\[</span><span>(</span></span><span>[</span><span><span>^</span><span>\]</span></span><span>]</span><span><span>*</span><span>)</span><span>\]\(</span></span><span>.</span><span><span>*?</span><span>\)</span><span>/</span></span><span>g</span><span>, </span><span>"$1"</span><span>)       </span><span>// 保留链接文字</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>.</span><span>replace</span><span>(</span><span>/</span><span>\n</span><span><span>{3,}</span><span>/</span></span><span>g</span><span>, </span><span>"</span><span>\n\n</span><span>"</span><span>)                  </span><span>// 压缩空行</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>.</span><span>trim</span><span>();</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>  </span><span>return</span><span><span> </span><span>cleaned</span><span>.</span></span><span>length</span><span> </span><span>&gt;</span><span><span> </span><span>maxChars</span></span></div></div><div><div><div>13</div></div><div><span>    </span><span>?</span><span> </span><span>`</span><span>${</span><span>cleaned</span><span>.</span><span>slice</span><span>(</span><span>0</span><span>,</span><span> </span><span><span>maxChars</span><span>)</span></span><span>}</span><span>...`</span></div></div><div><div><div>14</div></div><div><span>    </span><span>:</span><span><span> </span><span>cleaned</span><span>;</span></span></div></div><div><div><div>15</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>提示词设计<a href="#提示词设计"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>const</span><span> </span><span>SYSTEM_PROMPT</span><span> </span><span>=</span><span> </span><span>`你是一个以第一视角写作的个人博客作者。你的博客记录技术学习、日常生活和真实感悟。</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>你的任务是：读完一篇博客文章后，为它写一段友好、自然、像博客导语一样的"文章摘要"。</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>核心规则：</span></div></div><div><div><div>6</div></div><div><span>1. 输出只要一段摘要文字，不要标题、不要列表、不要"本文""这篇文章""总之"之类的套话。</span></div></div><div><div><div>7</div></div><div><span>2. 表达要自然、口语化，像一个真实的博主在跟读者打招呼或做开场铺垫，有一点"人味"。</span></div></div><div><div><div>8</div></div><div><span>3. 不要堆砌概念、不要写得像说明书或提纲总结。</span></div></div><div><div><div>9</div></div><div><span>4. 贴近原文真实内容，保留原作者的情绪和语气。</span></div></div><div><div><div>10</div></div><div><span>5. 技术文章保持清晰但不要生硬，生活/感悟类文章语气柔和一些。</span></div></div><div><div><div>11</div></div><div><span>6. 字数控制在 60～120 字左右，越短、越准越好，不要啰嗦。</span></div></div><div><div><div>12</div></div><div><span>7. 纯正文内容输出（不带任何前缀或说明）。`</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>API 调用与重试<a href="#api-调用与重试"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>const</span><span> </span><span>resp</span><span> </span><span>=</span><span> </span><span>await</span><span> </span><span>fetch</span><span>(</span><span>`</span><span>${</span><span>QWEN_BASE_URL</span><span>}</span><span>/chat/completions`</span><span>, {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>method</span><span>:</span><span> </span><span>"POST"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>headers</span><span>:</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>"Content-Type"</span><span>:</span><span> </span><span>"application/json"</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>Authorization</span><span>:</span><span> </span><span>`Bearer </span><span>${</span><span>QWEN_API_KEY</span><span>}</span><span>`</span><span>,</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>},</span></div></div><div><div><div>7</div></div><div><span>  </span><span>body</span><span>:</span><span> </span><span>JSON</span><span>.</span><span>stringify</span><span>({</span></div></div><div><div><div>8</div></div><div><span>    </span><span>model</span><span>:</span><span> </span><span>QWEN_MODEL</span><span>,</span></div></div><div><div><div>9</div></div><div><span>    </span><span>messages</span><span>:</span><span> [</span></div></div><div><div><div>10</div></div><div><span><span>      </span></span><span>{ </span><span>role</span><span>:</span><span> </span><span>"system"</span><span>, </span><span>content</span><span>:</span><span> </span><span>SYSTEM_PROMPT</span><span> },</span></div></div><div><div><div>11</div></div><div><span><span>      </span></span><span>{ </span><span>role</span><span>:</span><span> </span><span>"user"</span><span>, </span><span>content</span><span>:</span><span><span> </span><span>userMsg</span><span> },</span></span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>],</span></div></div><div><div><div>13</div></div><div><span>    </span><span>temperature</span><span>:</span><span> </span><span>0.75</span><span>,</span></div></div><div><div><div>14</div></div><div><span>    </span><span>max_tokens</span><span>:</span><span> </span><span>256</span><span>,</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>}),</span></div></div><div><div><div>16</div></div><div><span>});</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>运行脚本<a href="#运行脚本"><span>#</span></a></h3><p>完整脚本获取：<a href="https://cnb.cool/W3C/Hyde/Firefly-hyde/-/blob/main/fill-descriptions/index.ts" target="_blank">fill-descriptions/index.ts</a></p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># package.json</span></div></div><div><div><div>2</div></div><div><span>{</span></div></div><div><div><div>3</div></div><div><span>  </span><span>"scripts"</span><span>:</span><span> </span><span>{</span></div></div><div><div><div>4</div></div><div><span>    </span><span>"fill-descriptions"</span><span>:</span><span> </span><span>"npx tsx scripts/fill-descriptions/index.ts"</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>},</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 终端运行脚本</span></div></div><div><div><div>2</div></div><div><span>pnpm</span><span> </span><span>fill-descriptions</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>构建前自动补全（可选）<a href="#构建前自动补全可选"><span>#</span></a></h3><p>希望每次部署前自动补全新文章摘要，可在 package.json 的 build 前加上脚本：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"scripts"</span><span>:</span><span> </span><span>{</span></div></div><div><div><div>3</div></div><div><span>    </span><span>"build"</span><span>:</span><span> </span><span>"pnpm fill-descriptions &amp;&amp; node scripts/generate-icons.js &amp;&amp; npx tsx scripts/generate-lqips.ts &amp;&amp; astro build &amp;&amp; pagefind --site dist"</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>注意：每次 build 都会扫描并可能调用 API，会消耗额度。文章量大时建议只在本地或 CI 需要时手动跑 <code>pnpm fill-descriptions</code>。</p></section></section>
<section><h2>运行时组件详解<a href="#运行时组件详解"><span>#</span></a></h2><p>组件路径：<code>src/components/widget/AiSummary.astro</code></p><p>完整组件获取：<a href="https://cnb.cool/W3C/Hyde/Firefly-hyde/-/blob/main/src/components/widget/AiSummary.astro" target="_blank">AiSummary.astro</a></p><section><h3>Props 定义<a href="#props-定义"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>interface</span><span> </span><span>Props</span><span> {</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>description</span><span>:</span><span> </span><span>string</span><span>;</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>descriptionSource</span><span><span>?</span><span>:</span></span><span> </span><span>"manual"</span><span> </span><span>|</span><span> </span><span>"ai"</span><span> </span><span>|</span><span> </span><span>string</span><span>;</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>模板设计<a href="#模板设计"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"ai-summary-wrapper rounded-xl mb-6"</span><span>&gt;</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"ai-summary"</span><span>&gt;</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"ai-summary-inner"</span><span>&gt;</span></div></div><div><div><div>4</div></div><div><span><span>      </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"ai-summary-header"</span><span>&gt;</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"ai-summary-icon"</span><span>&gt;</span></div></div><div><div><div>6</div></div><div><span><span>          </span></span><span>&lt;</span><span>Icon</span><span> </span><span>name</span><span>=</span><span>{</span><span>iconName</span><span>}</span><span> </span><span>class</span><span>=</span><span>"text-base"</span><span> /&gt;</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>&lt;</span><span>span</span><span> </span><span>class</span><span>=</span><span>"ai-summary-label"</span><span>&gt;</span><span>{</span><span>sourceLabel</span><span>}</span><span>&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>9</div></div><div><span><span>      </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>10</div></div><div><span><span>      </span></span><span>&lt;</span><span>p</span></div></div><div><div><div>11</div></div><div><span>        </span><span>class</span><span>=</span><span>"ai-summary-text js-ai-summary-text"</span></div></div><div><div><div>12</div></div><div><span>        </span><span>data-full-text</span><span>=</span><span>{</span><span>description</span><span>}</span></div></div><div><div><div>13</div></div><div><span><span>      </span></span><span>&gt;&lt;/</span><span>p</span><span>&gt;</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>16</div></div><div><span>&lt;/</span><span>div</span><span>&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div></div><div>提示</div></div><div><p>⚠️使用 class 而非固定 id，避免 Swup 多页残留冲突；用 <code>data-typing-init</code> 防止重复初始化。</p></div></div></section><section><h3>打字机动画核心<a href="#打字机动画核心"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(</span><span>function</span><span> </span><span>initAiSummaryTypewriter</span><span>() {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>function</span><span> </span><span>run</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>const</span><span> </span><span>el</span><span> </span><span>=</span><span><span> </span><span>document</span><span>.</span></span><span>querySelector</span><span>(</span><span>".js-ai-summary-text"</span><span>);</span></div></div><div><div><div>4</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span><span>el</span><span> </span></span><span>||</span><span><span> </span><span>el</span><span>.</span></span><span>dataset</span><span>.</span><span>typingInit</span><span> </span><span>===</span><span> </span><span>"1"</span><span>) </span><span>return</span><span>;</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>el</span><span>.</span><span>dataset</span><span>.</span><span>typingInit</span><span> </span><span>=</span><span> </span><span>"1"</span><span>;</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span>const</span><span> </span><span>fullText</span><span> </span><span>=</span><span><span> </span><span>el</span><span>.</span></span><span>getAttribute</span><span>(</span><span>"data-full-text"</span><span>) </span><span>||</span><span> </span><span>""</span><span>;</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>    </span><span>// 无障碍：减少动态效果</span></div></div><div><div><div>10</div></div><div><span>    </span><span>if</span><span><span> (</span><span>window</span><span>.</span></span><span>matchMedia</span><span>(</span><span>"(prefers-reduced-motion: reduce)"</span><span>).</span><span>matches</span><span>) {</span></div></div><div><div><div>11</div></div><div><span><span>      </span></span><span>el</span><span>.</span><span>textContent</span><span> </span><span>=</span><span><span> </span><span>fullText</span><span>;</span></span></div></div><div><div><div>12</div></div><div><span>      </span><span>return</span><span>;</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>    </span><span>let</span><span><span> </span><span>hasRun</span><span> </span></span><span>=</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>16</div></div><div><span>    </span><span>const</span><span> </span><span>speed</span><span> </span><span>=</span><span> </span><span>45</span><span>;</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>    </span><span>const</span><span> </span><span>observer</span><span> </span><span>=</span><span> </span><span>new</span><span> </span><span>IntersectionObserver</span><span>(</span></div></div><div><div><div>19</div></div><div><span><span>      </span></span><span>(</span><span>entries</span><span>) </span><span>=&gt;</span><span> {</span></div></div><div><div><div>20</div></div><div><span>        </span><span>for</span><span> (</span><span>const</span><span> </span><span>entry</span><span> </span><span>of</span><span><span> </span><span>entries</span><span>) {</span></span></div></div><div><div><div>21</div></div><div><span>          </span><span>if</span><span><span> (</span><span>entry</span><span>.</span></span><span>isIntersecting</span><span> </span><span>&amp;&amp;</span><span> </span><span>!</span><span><span>hasRun</span><span>) {</span></span></div></div><div><div><div>22</div></div><div><span><span>            </span></span><span>hasRun</span><span> </span><span>=</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>23</div></div><div><span><span>            </span></span><span>observer</span><span>.</span><span>unobserve</span><span><span>(</span><span>el</span><span>);</span></span></div></div><div><div><div>24</div></div><div><span>            </span><span>startTyping</span><span><span>(</span><span>el</span><span>, </span><span>fullText</span><span>, </span><span>speed</span><span>);</span></span></div></div><div><div><div>25</div></div><div><span><span>          </span></span><span>}</span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>27</div></div><div><span><span>      </span></span><span>},</span></div></div><div><div><div>28</div></div><div><span><span>      </span></span><span>{ </span><span>threshold</span><span>:</span><span> </span><span>0.3</span><span> },</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>);</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>observer</span><span>.</span><span>observe</span><span><span>(</span><span>el</span><span>);</span></span></div></div><div><div><div>32</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>  </span><span>run</span><span>();</span></div></div><div><div><div>35</div></div><div><span><span>  </span></span><span>document</span><span>.</span><span>addEventListener</span><span>(</span><span>"astro:page-load"</span><span><span>, </span><span>run</span><span>);</span></span></div></div><div><div><div>36</div></div><div><span>})();</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>标点停顿设计<a href="#标点停顿设计"><span>#</span></a></h3>

<table><thead><tr><th>字符</th><th>延迟</th><th>说明</th></tr></thead><tbody><tr><td>普通字</td><td>45ms</td><td>基础速度</td></tr><tr><td>，、；:</td><td>90ms（2×）</td><td>短停顿</td></tr><tr><td>。！？…</td><td>135ms（3×）</td><td>长停顿</td></tr></tbody></table></section><section><h3>文章页集成<a href="#文章页集成"><span>#</span></a></h3><p>在 <code>src/pages/posts/[...slug].astro</code> 中添加：
组件获取：<a href="https://cnb.cool/W3C/Hyde/Firefly-hyde/-/blob/main/src/pages/posts/%5B...slug%5D.astro" target="_blank">[…slug].astro</a></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import AiSummary from "@/components/widget/AiSummary.astro";</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>&lt;!-- AI 摘要 - 打字机动画 --&gt;</span></div></div><div><div><div>4</div></div><div><span>{</span></div></div><div><div><div>5</div></div><div><span>  </span><span>entry</span><span>.</span><span>data</span><span>.</span><span>description</span><span> </span><span>&amp;&amp;</span><span> (</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>&lt;</span><span>AiSummary</span></div></div><div><div><div>7</div></div><div><span>      </span><span>description</span><span><span>=</span><span>{</span></span><span>entry</span><span>.</span><span>data</span><span>.</span><span>description</span><span>}</span></div></div><div><div><div>8</div></div><div><span>      </span><span>descriptionSource</span><span><span>=</span><span>{</span></span><span>entry</span><span>.</span><span>data</span><span>.</span><span>descriptionSource</span><span>}</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>/&gt;</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section>
<section><h2>Content Schema 配置<a href="#content-schema-配置"><span>#</span></a></h2><p>在 <code>src/content.config.ts</code> 中添加：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>type</span><span> </span><span>PostData</span><span> </span><span>=</span><span> {</span></div></div><div><div><div>2</div></div><div><span>descriptionSource</span><span><span>?</span><span>:</span></span><span> </span><span>"manual"</span><span> </span><span>|</span><span> </span><span>"ai"</span><span>;</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>const</span><span> </span><span>postsCollection</span><span>:</span><span> </span><span>ContentCollection</span><span>&lt;</span><span>PostData</span><span>&gt; </span><span>=</span><span> </span><span>defineCollection</span><span>({</span></div></div><div><div><div>6</div></div><div><span>  </span><span>schema</span><span>:</span><span><span> </span><span>z</span><span>.</span></span><span>object</span><span>({</span></div></div><div><div><div>7</div></div><div><span>    </span><span>descriptionSource</span><span>:</span><span> </span><span>z</span><span>.</span><span>enum</span><span>([</span><span>"manual"</span><span>, </span><span>"ai"</span><span>]).</span><span>optional</span><span>(),</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>}),</span></div></div><div><div><div>9</div></div><div><span>});</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section>
<section><h2>优化技巧<a href="#优化技巧"><span>#</span></a></h2>

<table><thead><tr><th>参数</th><th>作用</th><th>推荐值</th><th>说明</th></tr></thead><tbody><tr><td><code>temperature</code></td><td>控制随机性</td><td>0.75</td><td>略有个性，避免死板</td></tr><tr><td><code>max_tokens</code></td><td>最大输出长度</td><td>256</td><td>摘要短，足够</td></tr><tr><td><code>top_p</code></td><td>核采样</td><td>0.9</td><td>控制生成多样性</td></tr></tbody></table><section><h3>输入优化<a href="#输入优化"><span>#</span></a></h3><ul>
<li>去掉无关的格式标记</li>
<li>代码块用占位符替代（节省 token）</li>
<li>控制输入长度（推荐 2000-3000 字）</li>
</ul><hr /></section></section>
<section><h2>陷阱清单<a href="#陷阱清单"><span>#</span></a></h2>

<table><thead><tr><th>陷阱</th><th>说明</th><th>规避方法</th></tr></thead><tbody><tr><td><strong>API Key 泄露</strong></td><td>将密钥硬编码到代码中</td><td>使用 .env 文件，加入 .gitignore</td></tr><tr><td><strong>用整篇文件匹配 description</strong></td><td>正文代码块里的示例 description 会被误识别为已存在摘要</td><td>只在 frontmatter 区块内检测字段</td></tr><tr><td><strong>Swup 兼容</strong></td><td>页面切换后打字机动画无法重新触发，组件失效</td><td>组件监听 <code>astro:page-load</code>，站内切页后打字机仍会正常初始化</td></tr><tr><td><strong>模型选择</strong></td><td>选用数学、代码专用模型生成文章摘要，输出内容质量差、不符合文案需求</td><td>默认 <code>qwen-plus</code>；数学/代码专用模型不适合写摘要，脚本启动时会给出警告</td></tr><tr><td><strong>教程类文章</strong></td><td>教程正文代码块中存在 <code>description:</code> 示例文本，旧脚本会误判文章已有摘要，跳过生成</td><td>当前版本仅在 frontmatter 区域检测字段，不受正文代码示例干扰，可正常补全摘要</td></tr><tr><td><strong>请求无间隔</strong></td><td>循环批量调用千问API，请求频率过高触发平台限流，脚本中断报错</td><td>添加 600ms 以上的请求间隔</td></tr><tr><td><strong>输入内容过长</strong></td><td>全文无限制传入API，消耗大量token，同时超长文本会降低AI摘要精准度</td><td>正文内容截断到 2000-3000 字后再传入模型</td></tr><tr><td><strong>忽略编码问题</strong></td><td>Windows系统读写md文件时默认编码非UTF-8，中文摘要出现乱码</td><td>读写文件时强制指定 UTF-8 编码</td></tr><tr><td><strong>未校验输出</strong></td><td>AI返回空内容、仅符号、无意义文本等无效摘要，直接写入文件</td><td>使用 sanitizeDescription 函数校验、清洗AI输出内容</td></tr><tr><td><strong>提示词过于复杂</strong></td><td>提示词冗余、逻辑混乱，AI无法准确理解摘要生成要求，产出效果差</td><td>提示词清晰简洁，分点明确告知AI生成规则与要求</td></tr><tr><td><strong>使用固定 id</strong></td><td>多页面共用相同DOM id，Swup页面缓存残留，打字机组件渲染冲突</td><td>使用 class 选择器替代固定id</td></tr><tr><td><strong>空字符串误判</strong></td><td>frontmatter 内 <code>description: ""</code> 空字符串被判定为已有摘要，跳过自动补全</td><td>使用 hasDescription 函数精准判断字段是否存在有效文本</td></tr></tbody></table></section>
<section><h2>快速检查清单<a href="#快速检查清单"><span>#</span></a></h2><ul>
<li> <code>AiSummary.astro</code> 组件文件已创建</li>
<li> <code>[...slug].astro</code> 文章页面已引入摘要组件</li>
<li> <code>content/content.config.ts</code> 配置文件包含 descriptionSource 字段校验</li>
<li> <code>.env</code> 环境变量文件已配置 <code>QWEN_API_KEY</code></li>
<li> 执行过命令 <code>pnpm fill-descriptions</code> 批量生成摘要，或手动填写过 description</li>
<li> 本地启动 <code>pnpm dev</code>，打开带有摘要的文章页面，滚动至摘要区域，验证打字机动画正常展示</li>
</ul></section>]]></content>
    </entry>
    <entry>
      <id>https://seasir.top/posts/friends-apply/</id>
      <title type="text">自助友链申请</title>
      <published>2026-07-27T00:00:00.000Z</published>
      <updated>2026-07-27T00:00:00.000Z</updated>
      <author><name>Hyde</name></author>
      <link rel="alternate" href="https://seasir.top/posts/friends-apply/"/>
      <summary type="text">最近折腾了一下，终于把站里的自助友链申请功能搞定了！对比一圈后，我选了 Supabase 搭配 EdgeOne 边缘函数。从数据库配置、前端改造到函数部署，完整流程都整理好了。想给博客加个友链入口的朋友，直接来抄作业吧！</summary>
      <content type="html"><![CDATA[<section><h2>存储类型区别优势劣势对比<a href="#存储类型区别优势劣势对比"><span>#</span></a></h2>

<table><thead><tr><th>存储类型</th><th>速度</th><th>收费模式</th><th>Pages 函数限制</th><th>核心优势</th><th>核心劣势</th><th>适用场景</th></tr></thead><tbody><tr><td><code>S3 存储</code></td><td>中等</td><td>免费额度可用</td><td>不限制函数</td><td>S3 通用标准，生态工具丰富，免费额度充足，全函数兼容</td><td>免费超限后计费，无内置数据库权限，鉴权需自行开发</td><td>静态网站资源、文件备份、跨云存储、通用文件托管</td></tr><tr><td><code>KV 存储</code></td><td>极快</td><td>免费额度可用</td><td>仅 pages-edge 函数可用</td><td>内存级低延迟，API 简单，免费额度满足小型缓存，适配边缘函数</td><td>仅支持主键查询，无法存大文件，普通 Pages 函数无法调用</td><td>网站缓存、会话存储、计数器、配置文件、边缘临时热点数据</td></tr><tr><td><code>Blob 存储</code></td><td>普通</td><td>免费额度可用</td><td>支持 pages 任意函数</td><td>针对二进制大文件优化，全类型 Pages 函数无限制，冷归档低价</td><td>访问速度一般，不兼容标准 S3，跨云迁移适配差</td><td>微软云配套业务、视频备份、虚拟机快照、文档附件存储</td></tr><tr><td><code>Supabase 存储</code></td><td>最快</td><td>免费额度可用</td><td>不限制函数</td><td>存储 + PostgreSQL 一体化，自带 RLS 行级权限，配套用户认证、向量存储</td><td>免费项目数量有限，闲置项目会暂停，底层依赖第三方存储</td><td>全栈 Web/App 快速开发、用户上传素材、AI 向量知识库、小型后端项目</td></tr><tr><td><code>COS 存储</code></td><td>最快</td><td>按量收费，无永久免费</td><td>不限制函数</td><td>国内节点多，国内访问速度顶尖，深度集成腾讯 CDN，媒体处理功能完善</td><td>无免费额度，存储 / 流量 / 请求均收费，海外访问延迟偏高</td><td>国内小程序 / 网站静态资源、电商图片、短视频素材、企业数据备份</td></tr></tbody></table></section>
<section><h2>选择存储类型<a href="#选择存储类型"><span>#</span></a></h2><p>这里博主选择的是 <a href="https://spb-ut25fs7km7012zlr.supabase.opentrust.net/" target="_blank">Supabase</a> 存储，因为<code>最快免费额度可用不限制函数</code>，符合博主的需求。</p></section>
<section><h2>配置 Supabase<a href="#配置-supabase"><span>#</span></a></h2><ul>
<li>
<p>进入<a href="https://gpdbnext.console.aliyun.com/gpdb/cn-shenzhen/supabase" target="_blank">阿里云云原生数据仓库</a>，左上角选择离你最近的区域。这里我选择的是深圳区域</p>
</li>
<li>
<p>点击<code>创建项目</code>，选择<code>免费测试创建</code>，点击<code>创建</code>，填写<code>项目名称</code>项目名称需要英文的！输入<code>数据库账号</code>和<code>数据库密码</code></p>
</li>
<li>
<p>首次创建需要配置<code>专有网络</code>和<code>专有网络交换机</code>，若当前区域未创建专有网络，请前往<a href="https://vpc.console.aliyun.com/vpc/cn-shenzhen/switches" target="_blank">阿里云VPC控制台</a>创建专有网络实例</p>
</li>
<li>
<p>进入<code>专有网络</code>页面，点击<code>创建专有网络</code>，地域选择需要和刚刚<code>创建项目-可用区</code>一致！填写<code>名称</code>，点击<code>创建</code>，IPv4网段选择<code>手动输入 IPv4 地址段</code>，输入IPv4网段随便选一个，这里我选的是<code>192.168.0.0/16</code>，IPv6网段选择<code>系统分配的 IPv6 地址段</code></p>
</li>
<li>
<p>输入交换机名称和可用区选择和刚刚<code>创建项目-可用区</code>一致！这里我选择的是<code>深圳 可用区 F</code></p>
</li>
<li>
<p>是否开放白名单限制，这里我选择的是<code>否</code>！</p>
</li>
<li>
<p><code>IP 白名单</code>，这里我选择的是<code>0.0.0.0/0</code>！然后点击<code>确定</code>，即可完成配置。</p>
</li>
<li>
<p>创建好后把<code>修改数据库白名单</code>，添加<code>0.0.0.0/0</code>，即可从任何 IP访问数据库。</p>
</li>
</ul></section>
<section><h2>登录 Supabase<a href="#登录-supabase"><span>#</span></a></h2><ul>
<li>点击<code>登录 Dashboard</code>，会弹出<code>登录账号</code>和<code>登录密码</code>复制<code>数据库账号</code>和<code>数据库密码</code>，即可登录（如果设置了白名单，需要在白名单中添加当前 IP 才能登录）</li>
</ul></section>
<section><h2>SQL Editor<a href="#sql-editor"><span>#</span></a></h2><ul>
<li>登录后，左侧菜单点击<code>SQL Editor</code>，即可进入 SQL 编辑器，在 SQL 编辑器中执行 SQL 语句</li>
</ul><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>INSERT INTO</span><span> </span><span>"public"</span><span>.</span><span>"friends"</span><span> (</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"id"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>    </span><span>"title"</span><span>,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>"desc"</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"siteUrl"</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>"imgUrl"</span><span>,</span></div></div><div><div><div>7</div></div><div><span>    </span><span>"tags"</span><span>,</span></div></div><div><div><div>8</div></div><div><span>    </span><span>"status"</span><span>,</span></div></div><div><div><div>9</div></div><div><span>    </span><span>"ip"</span><span>,</span></div></div><div><div><div>10</div></div><div><span>    </span><span>"email"</span><span>,</span></div></div><div><div><div>11</div></div><div><span>    </span><span>"message"</span><span>,</span></div></div><div><div><div>12</div></div><div><span>    </span><span>"updatedAt"</span><span>,</span></div></div><div><div><div>13</div></div><div><span>    </span><span>"createdAt"</span><span>,</span></div></div><div><div><div>14</div></div><div><span>    </span><span>"weight"</span><span> </span><span>-- 新增int4权重字段</span></div></div><div><div><div>15</div></div><div><span>) </span><span>VALUES</span></div></div><div><div><div>16</div></div><div><span>(</span></div></div><div><div><div>17</div></div><div><span>    </span><span>'64f3b4ef-490d-4440-ad8e-5dc73034ccb9'</span><span>,</span></div></div><div><div><div>18</div></div><div><span>    </span><span>'云原生构建'</span><span>,</span></div></div><div><div><div>19</div></div><div><span>    </span><span>'会Fork就会CNB'</span><span>,</span></div></div><div><div><div>20</div></div><div><span>    </span><span>'https://cnb.cool/'</span><span>,</span></div></div><div><div><div>21</div></div><div><span>    </span><span>'https://cnb.cool/cnb/-/logos/s'</span><span>,</span></div></div><div><div><div>22</div></div><div><span>    </span><span>'["Blog","Astro"]'</span><span>::jsonb,</span></div></div><div><div><div>23</div></div><div><span>    </span><span>'rejected'</span><span>,</span></div></div><div><div><div>24</div></div><div><span>    </span><span>'unknown'</span><span>,</span></div></div><div><div><div>25</div></div><div><span>    </span><span>NULL</span><span>,</span></div></div><div><div><div>26</div></div><div><span>    </span><span>NULL</span><span>,</span></div></div><div><div><div>27</div></div><div><span>    </span><span>'1785103334148'</span><span>,</span></div></div><div><div><div>28</div></div><div><span>    </span><span>'1784148550477'</span><span>,</span></div></div><div><div><div>29</div></div><div><span>    </span><span>0</span></div></div><div><div><div>30</div></div><div><span>),</span></div></div><div><div><div>31</div></div><div><span>(</span></div></div><div><div><div>32</div></div><div><span>    </span><span>'80a0e1dc-7f96-42df-be62-3d0edabc77ac'</span><span>,</span></div></div><div><div><div>33</div></div><div><span>    </span><span>'启源'</span><span>,</span></div></div><div><div><div>34</div></div><div><span>    </span><span>'技术丶生活'</span><span>,</span></div></div><div><div><div>35</div></div><div><span>    </span><span>'https://czxcly.cn/'</span><span>,</span></div></div><div><div><div>36</div></div><div><span>    </span><span>'https://personal-biz.czxcly.cn/profile/avatar/2026/05/18/594c922986f34225aa6917bb0b2908ca.jpg'</span><span>,</span></div></div><div><div><div>37</div></div><div><span>    </span><span>'["Blog","Astro"]'</span><span>::jsonb,</span></div></div><div><div><div>38</div></div><div><span>    </span><span>'approved'</span><span>,</span></div></div><div><div><div>39</div></div><div><span>    </span><span>'129.204.149.174'</span><span>,</span></div></div><div><div><div>40</div></div><div><span>    </span><span>NULL</span><span>,</span></div></div><div><div><div>41</div></div><div><span>    </span><span>NULL</span><span>,</span></div></div><div><div><div>42</div></div><div><span>    </span><span>'1785102151599'</span><span>,</span></div></div><div><div><div>43</div></div><div><span>    </span><span>'1783301022476'</span><span>,</span></div></div><div><div><div>44</div></div><div><span>    </span><span>0</span></div></div><div><div><div>45</div></div><div><span>),</span></div></div><div><div><div>46</div></div><div><span>(</span></div></div><div><div><div>47</div></div><div><span>    </span><span>'8f652adc-0d92-4c53-9e5a-5088048b76e9'</span><span>,</span></div></div><div><div><div>48</div></div><div><span>    </span><span>'ZhiJing''s Blog'</span><span>,</span></div></div><div><div><div>49</div></div><div><span>    </span><span>'Go with the flow.'</span><span>,</span></div></div><div><div><div>50</div></div><div><span>    </span><span>'https://iwexe.top'</span><span>,</span></div></div><div><div><div>51</div></div><div><span>    </span><span>'https://iwexe.top/avatar.svg'</span><span>,</span></div></div><div><div><div>52</div></div><div><span>    </span><span>'["Blog","Astro"]'</span><span>::jsonb,</span></div></div><div><div><div>53</div></div><div><span>    </span><span>'approved'</span><span>,</span></div></div><div><div><div>54</div></div><div><span>    </span><span>'43.139.234.199'</span><span>,</span></div></div><div><div><div>55</div></div><div><span>    </span><span>NULL</span><span>,</span></div></div><div><div><div>56</div></div><div><span>    </span><span>NULL</span><span>,</span></div></div><div><div><div>57</div></div><div><span>    </span><span>'1785102152667'</span><span>,</span></div></div><div><div><div>58</div></div><div><span>    </span><span>'1783185799746'</span><span>,</span></div></div><div><div><div>59</div></div><div><span>    </span><span>0</span></div></div><div><div><div>60</div></div><div><span>),</span></div></div><div><div><div>61</div></div><div><span>(</span></div></div><div><div><div>62</div></div><div><span>    </span><span>'a1b2c3d4-e5f6-7890-abcd-ef1234567890'</span><span>,</span></div></div><div><div><div>63</div></div><div><span>    </span><span>'One'</span><span>,</span></div></div><div><div><div>64</div></div><div><span>    </span><span>'明心见性，爱自己'</span><span>,</span></div></div><div><div><div>65</div></div><div><span>    </span><span>'https://onedayxyy.cn/'</span><span>,</span></div></div><div><div><div>66</div></div><div><span>    </span><span>'https://img.onedayxyy.cn/images/Teek/Teekwebsite/xyy-logo.avif'</span><span>,</span></div></div><div><div><div>67</div></div><div><span>    </span><span>'["Blog","Astro"]'</span><span>::jsonb,</span></div></div><div><div><div>68</div></div><div><span>    </span><span>'approved'</span><span>,</span></div></div><div><div><div>69</div></div><div><span>    </span><span>'unknown'</span><span>,</span></div></div><div><div><div>70</div></div><div><span>    </span><span>NULL</span><span>,</span></div></div><div><div><div>71</div></div><div><span>    </span><span>NULL</span><span>,</span></div></div><div><div><div>72</div></div><div><span>    </span><span>'1785102169329'</span><span>,</span></div></div><div><div><div>73</div></div><div><span>    </span><span>'1783815691854'</span><span>,</span></div></div><div><div><div>74</div></div><div><span>    </span><span>0</span></div></div><div><div><div>75</div></div><div><span>),</span></div></div><div><div><div>76</div></div><div><span>(</span></div></div><div><div><div>77</div></div><div><span>    </span><span>'a56878d9-4649-439d-b16d-abb4fa87ec7f'</span><span>,</span></div></div><div><div><div>78</div></div><div><span>    </span><span>'gc的小站'</span><span>,</span></div></div><div><div><div>79</div></div><div><span>    </span><span>'综合性小站,希望能帮到你!'</span><span>,</span></div></div><div><div><div>80</div></div><div><span>    </span><span>'https://gcweb.cc'</span><span>,</span></div></div><div><div><div>81</div></div><div><span>    </span><span>'https://www.gcweb.cc/static/img/pig.jpg'</span><span>,</span></div></div><div><div><div>82</div></div><div><span>    </span><span>'["Blog","Astro"]'</span><span>::jsonb,</span></div></div><div><div><div>83</div></div><div><span>    </span><span>'approved'</span><span>,</span></div></div><div><div><div>84</div></div><div><span>    </span><span>'223.104.116.184'</span><span>,</span></div></div><div><div><div>85</div></div><div><span>    </span><span>NULL</span><span>,</span></div></div><div><div><div>86</div></div><div><span>    </span><span>NULL</span><span>,</span></div></div><div><div><div>87</div></div><div><span>    </span><span>'1785102168900'</span><span>,</span></div></div><div><div><div>88</div></div><div><span>    </span><span>'1783462756928'</span><span>,</span></div></div><div><div><div>89</div></div><div><span>    </span><span>0</span></div></div><div><div><div>90</div></div><div><span>),</span></div></div><div><div><div>91</div></div><div><span>(</span></div></div><div><div><div>92</div></div><div><span>    </span><span>'b2c3d4e5-f6a7-8901-bcde-f23456789012'</span><span>,</span></div></div><div><div><div>93</div></div><div><span>    </span><span>'Hyde'</span><span>,</span></div></div><div><div><div>94</div></div><div><span>    </span><span>'人心中的成见是一座大山'</span><span>,</span></div></div><div><div><div>95</div></div><div><span>    </span><span>'https://seasir.top/'</span><span>,</span></div></div><div><div><div>96</div></div><div><span>    </span><span>'https://seasir.top/_astro/avatar.B2RZ_bo3_JV0dW.webp'</span><span>,</span></div></div><div><div><div>97</div></div><div><span>    </span><span>'["Blog","Astro"]'</span><span>::jsonb,</span></div></div><div><div><div>98</div></div><div><span>    </span><span>'approved'</span><span>,</span></div></div><div><div><div>99</div></div><div><span>    </span><span>'unknown'</span><span>,</span></div></div><div><div><div>100</div></div><div><span>    </span><span>NULL</span><span>,</span></div></div><div><div><div>101</div></div><div><span>    </span><span>NULL</span><span>,</span></div></div><div><div><div>102</div></div><div><span>    </span><span>'1785102168100'</span><span>,</span></div></div><div><div><div>103</div></div><div><span>    </span><span>'1783462753394'</span><span>,</span></div></div><div><div><div>104</div></div><div><span>    </span><span>0</span></div></div><div><div><div>105</div></div><div><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>执行后可以在<code>SQL Editor</code>中查看执行的示例数据</p></section>
<section><h2>前端页面改造<a href="#前端页面改造"><span>#</span></a></h2><p>文件目录：</p>

<table><thead><tr><th>文件路径</th><th>作用</th></tr></thead><tbody><tr><td><code>src/config/friendsConfig.ts</code></td><td>友链配置文件</td></tr><tr><td><code>src/content/spec/friends.mdx</code></td><td>友链列表页面自定义内容，已弃用</td></tr><tr><td><code>src/pages/friends.astro</code></td><td>友链列表页面组件</td></tr><tr><td><code>src/types/friendsConfig.ts</code></td><td>友链配置文件类型定义</td></tr></tbody></table><p>相关文件在<a href="https://cnb.cool/W3C/Hyde/Firefly-hyde" target="_blank">源码</a>中获取，记得点个<code>Star</code>！将上面的文件替换掉您项目对应文件即可。</p></section>
<section><h2>部署edgeone边缘函数<a href="#部署edgeone边缘函数"><span>#</span></a></h2><ul>
<li>
<p>登录进入<a href="https://console.cloud.tencent.com/edgeone/zones" target="_blank">Edgeone</a>控制台,新增站点-进入站点详情页</p>
</li>
<li>
<p>在左侧菜单找到<code>函数管理</code>-<code>新建函数</code>，随便选一个模板点击下一步</p>
</li>
<li>
<p>填写<code>函数名称</code>，这里我选的是<code>创建 Hello World</code>，黏贴<a href="https://cnb.cool/W3C/Hyde/Firefly-hyde/-/tree/main/scripts/hyde-Supabase-friends.js" target="_blank">函数代码</a>,点击<code>创建并部署</code>等待创建完成，通常10s左右，刷新页面即可完成部署</p>
</li>
</ul></section>
<section><h2>配置变量<a href="#配置变量"><span>#</span></a></h2><ul>
<li>在<code>函数管理</code>-<code>函数详情</code>-<code>环境变量与密钥</code>中，添加以下环境变量</li>
</ul>

<table><thead><tr><th>变量名</th><th>变量值</th><th>变量类型</th><th>变量描述</th><th>获取方式</th></tr></thead><tbody><tr><td><code>SUPABASE_URL</code></td><td><code>https://spb-xxxxxxxx.supabase.opentrust.net</code></td><td><code>string</code></td><td><code>Supabase URL</code></td><td>从<a href="https://spb-ut25fs7km7012zlr.supabase.opentrust.net" target="_blank">Supabase</a>控制台左上角-<code>Connect</code>-<code>App Frameworks</code>获取</td></tr><tr><td><code>SUPABASE_KEY</code></td><td><code>eyJ0exxxxxxx.xxxxxxxxxxxxxx.xxxxxxxxxx</code></td><td><code>string</code></td><td><code>Supabase Key</code></td><td>从<a href="https://spb-ut25fs7km7012zlr.supabase.opentrust.net" target="_blank">Supabase</a>控制台左上角-<code>Connect</code>-<code>App Frameworks</code>获取</td></tr><tr><td><code>ADMIN_USER</code></td><td><code>admin</code></td><td><code>string</code></td><td><code>登录admin后台用户名</code></td><td>自定义设置</td></tr><tr><td><code>ADMIN_PASS</code></td><td><code>admin</code></td><td><code>string</code></td><td><code>登录admin后台密码</code></td><td>自定义设置</td></tr><tr><td><code>PUSH_SWITCH</code></td><td><code>on</code></td><td><code>string</code></td><td><code>是否开启推送通知</code></td><td>用于开启或关闭推送通知功能</td></tr><tr><td><code>PUSH_PLATFORM</code></td><td><code>feishu</code></td><td><code>string</code></td><td><code>推送平台</code></td><td>例如：<code>feishu</code>、<code>dingtalk</code>等</td></tr><tr><td><code>PUSH_TOKEN</code></td><td><code>https://open.feishu.cn/open-apis/bot/v2/hook/xxx</code></td><td><code>string</code></td><td><code>飞书/钉钉webhook推送token地址</code></td><td><a href="https://open.feishu.cn/document/feishu-cards/quick-start/send-message-cards-with-custom-bot" target="_blank">飞书</a> 、<a href="https://open.dingtalk.com/document/dingstart/custom-bot-creation-and-installation" target="_blank">钉钉</a>webhook地址获取</td></tr><tr><td><code>PUSH_SECRET</code></td><td><code>SEC9xxxxxxxxxx</code></td><td><code>string</code></td><td><code>钉钉平台加签密钥</code></td><td><a href="https://open.dingtalk.com/document/dingstart/customize-robot-security-settings" target="_blank">钉钉加签获取</a></td></tr></tbody></table><p>填写完成后，一定要点击<code>部署</code>按钮，否则环境变量不会生效</p></section>
<section><h2>配置域名<a href="#配置域名"><span>#</span></a></h2><section><h3>配置主域名（推荐）<a href="#配置主域名推荐"><span>#</span></a></h3><ul>
<li>
<p>进入<a href="https://console.cloud.tencent.com/edgeone/makers" target="_blank">edgeone控制台</a>，点击<code>makers</code>找到您部署的 <a href="https://github.com/CuteLeaf/Firefly" target="_blank">Firefly</a>的项目，进入<code>项目设置</code>-<code>域名管理</code>-<code>添加自定义域名</code>，填写您的二级域名，例如：<code>firefly.seasir.top</code>，填写后去您的域名平台填写<code>canme解析</code>，申请免费证书，等待部署证书生效</p>
</li>
<li>
<p>进入<code>服务总览</code>-<code>网站安全加速</code>-进入您的<code>站点</code>，找到<code>域名服务</code>-<code>域名管理</code>，点击<code>添加域名</code></p>
</li>
</ul><p><img src="https://cfimg.seasir.top/file/1785298203851_PixPin_2026-07-29_12-09-38.jpg" alt="" /></p><p>然后域名平台填写<code>canme解析</code>，申请免费证书，等待部署证书生效</p></section><section><h3>配置二级域名（2选1）<a href="#配置二级域名2选1"><span>#</span></a></h3><ul>
<li>
<p>进入主站-域名管理-添加域名</p>
</li>
<li>
<p>填写二级域名，例如：<code>api.seasir.top</code></p>
</li>
<li>
<p>IP/域名填写边缘函数部署生成的<code>默认访问域名</code>，例如：<code>friends-zone-3du2b1i5fnma-1312627337.eo-edgefunctions.com</code></p>
</li>
<li>
<p>推荐模板选择不使用模板，点击<code>下一步</code></p>
</li>
<li>
<p>去您域名后台管理平台解析canme类型的域名，申请免费证书，等待部署证书生效</p>
</li>
</ul></section></section>
<section><h2>配置函数触发规则<a href="#配置函数触发规则"><span>#</span></a></h2><ul>
<li>在<code>函数管理</code>-<code>函数详情</code>-<code>新增触发规则</code>中，添加以下触发规则</li>
</ul><p>触发条件：</p>

<table><thead><tr><th>匹配类型</th><th>运算符</th><th>值</th></tr></thead><tbody><tr><td><code>HOST</code></td><td><code>等于</code></td><td><code>seasir.top</code> （选择你配置的域名）</td></tr><tr><td><code>URL path</code></td><td><code>等于</code></td><td><code>/api*</code></td></tr></tbody></table><p>执行函数：</p>

<table><thead><tr><th>执行方式</th><th>执行函数</th></tr></thead><tbody><tr><td><code>执行指定函数</code></td><td>friends-zone-3du2b1i5fnma-1312627337</td></tr></tbody></table><p></p><figure><img src="https://cfimg.seasir.top/file/1785299145901_PixPin_2026-07-29_12-25-33.jpg" alt="函数触发规则" /><figcaption>函数触发规则</figcaption></figure><p></p></section>
<section><h2>配置后端请求地址<a href="#配置后端请求地址"><span>#</span></a></h2><p>路径：<code>src/pages/friends.astro</code></p><ul>
<li>搜索<a href="https://seasir.top/">seasir.top</a>替换您上面配置好的二级域名，有<code>两处</code>要替换</li>
</ul></section>
<section><h2>访问后台<a href="#访问后台"><span>#</span></a></h2><ul>
<li>
<p>访问<code>https://你的域名/api/admin</code>，登录后即可查看友友链审核管理后台页面</p>
</li>
<li>
<p>登录用户名密码：您环境变量中的<code>ADMIN_USER</code>和<code>ADMIN_PASS</code>里面的值，变量值中就是您后台的账号和密码</p>
</li>
</ul></section>]]></content>
    </entry>
    <entry>
      <id>https://seasir.top/posts/moments/</id>
      <title type="text">集成朋友圈与笔记本功能</title>
      <published>2026-06-21T00:00:00.000Z</published>
      <updated>2026-06-21T00:00:00.000Z</updated>
      <author><name>Hyde</name></author>
      <link rel="alternate" href="https://seasir.top/posts/moments/"/>
      <summary type="text">最近折腾博客，参考大佬方案把朋友圈和笔记本功能安排上了。这篇算是我的实操复盘，补充了不少配置细节。从前端组件到后台管理，一步步带你用 GitHub Gist 零成本搞定这两个模块，想给博客加点料的朋友直接来抄作业啦。</summary>
      <content type="html"><![CDATA[<div><div><div></div><div>提示</div></div><div><p>本文是在博主 <a href="https://blog.tsh520.cn/" target="_blank">团子和蛋糕</a> 文章：<a href="https://blog.tsh520.cn/posts/%E6%8A%80%E6%9C%AF%E5%88%86%E4%BA%AB/gist-backend/" target="_blank">用 GitHub Gist 做博客后端：零成本的说说与笔记方案</a> 基础上增加了自己实践过程的一些细节，转载无需和我联系，但请注明文章来源。如果侵权之处，请联系博主进行删除，谢谢~</p></div></div>
<section><h2>目录结构<a href="#目录结构"><span>#</span></a></h2><p>以下是朋友圈和笔记本功能相关的核心目录结构：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Firefly-hyde/</span></div></div><div><div><div>2</div></div><div><span>├── astro.config.mjs                                    # 修改 - 添加路径别名</span></div></div><div><div><div>3</div></div><div><span>│</span></div></div><div><div><div>4</div></div><div><span>├── src/</span></div></div><div><div><div>5</div></div><div><span>│   ├── components/</span></div></div><div><div><div>6</div></div><div><span>│   │   └── moments/                                    # 新增目录</span></div></div><div><div><div>7</div></div><div><span>│   │       ├── MomentCard.astro                       # 新增 - 朋友圈卡片组件</span></div></div><div><div><div>8</div></div><div><span>│   │       └── MomentsCover.astro                     # 新增 - 朋友圈封面组件</span></div></div><div><div><div>9</div></div><div><span>│   │</span></div></div><div><div><div>10</div></div><div><span>│   ├── config/</span></div></div><div><div><div>11</div></div><div><span>│   │   ├── externalMomentsConfig.ts                   # 新增 - 朋友圈配置</span></div></div><div><div><div>12</div></div><div><span>│   │   ├── externalNotebooksConfig.ts                 # 新增 - 笔记本配置</span></div></div><div><div><div>13</div></div><div><span>│   │   ├── navBarConfig.ts                            # 修改 - 添加导航链接</span></div></div><div><div><div>14</div></div><div><span>│   │   └── siteConfig.ts                              # 修改 - 添加封面配置</span></div></div><div><div><div>15</div></div><div><span>│   │</span></div></div><div><div><div>16</div></div><div><span>│   ├── content/</span></div></div><div><div><div>17</div></div><div><span>│   │   ├── config.ts                                  # 修改 - 添加moments集合</span></div></div><div><div><div>18</div></div><div><span>│   │   └── moments/                                    # 新增目录</span></div></div><div><div><div>19</div></div><div><span>│   │       └── *.md                                   # 新增 - 本地朋友圈内容文件</span></div></div><div><div><div>20</div></div><div><span>│   │</span></div></div><div><div><div>21</div></div><div><span>│   ├── data/</span></div></div><div><div><div>22</div></div><div><span>│   │   └── moments.ts                                 # 新增 - 获取远程朋友圈数据脚本</span></div></div><div><div><div>23</div></div><div><span>│   │</span></div></div><div><div><div>24</div></div><div><span>│   ├── pages/</span></div></div><div><div><div>25</div></div><div><span>│   │   ├── admin/                                     # 新增目录</span></div></div><div><div><div>26</div></div><div><span>│   │   │   ├── index.astro                           # 新增 - 登录页</span></div></div><div><div><div>27</div></div><div><span>│   │   │   ├── moments.astro                         # 新增 - 朋友圈管理</span></div></div><div><div><div>28</div></div><div><span>│   │   │   └── notebooks.astro                       # 新增 - 笔记本管理</span></div></div><div><div><div>29</div></div><div><span>│   │   ├── life/notebooks/</span></div></div><div><div><div>30</div></div><div><span>│   │   │   └── remote-entry.astro                    # 新增 - 远程笔记页</span></div></div><div><div><div>31</div></div><div><span>│   │   ├── moments.astro                              # 新增 - 朋友圈主页</span></div></div><div><div><div>32</div></div><div><span>│   │   └── moments/</span></div></div><div><div><div>33</div></div><div><span>│   │       └── pinned.astro                           # 新增 - 精选朋友圈</span></div></div><div><div><div>34</div></div><div><span>│   │</span></div></div><div><div><div>35</div></div><div><span>│   ├── types/</span></div></div><div><div><div>36</div></div><div><span>│   │   └── siteConfig.ts                              # 修改 - 添加momentsCover类型</span></div></div><div><div><div>37</div></div><div><span>│   │</span></div></div><div><div><div>38</div></div><div><span>│   └── utils/</span></div></div><div><div><div>39</div></div><div><span>│       ├── content-utils.ts                           # 修改 - 添加数据获取</span></div></div><div><div><div>40</div></div><div><span>│       └── gist-api.ts                                # 新增 - Gist API工具</span></div></div><div><div><div>41</div></div><div><span>└──</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><hr /></section>
<section><h2>新增朋友圈/封面卡片组件<a href="#新增朋友圈封面卡片组件"><span>#</span></a></h2><p>文件路径：<code>src/components/moments/MomentCard.astro和src/components/moments/MomentsCover.astro</code></p><p><strong>完整代码：</strong><a href="https://cnb.cool/W3C/Hyde/Firefly-hyde/-/blob/main/src/components/moments/MomentCard.astro" target="_blank">MomentCard.astro</a></p><p><strong>完整代码：</strong><a href="https://cnb.cool/W3C/Hyde/Firefly-hyde/-/blob/main/src/components/moments/MomentsCover.astro" target="_blank">MomentsCover.astro</a></p></section>
<section><h2>创建 Gist API 工具文件<a href="#创建-gist-api-工具文件"><span>#</span></a></h2><p><strong>文件路径</strong>: <code>src/utils/gist-api.ts</code></p><p><strong>完整代码</strong>: <a href="https://cnb.cool/W3C/Hyde/Firefly-hyde/-/blob/main/src/utils/gist-api.ts" target="_blank">gist-api.ts</a></p><hr /></section>
<section><h2>创建朋友圈配置<a href="#创建朋友圈配置"><span>#</span></a></h2><p><strong>文件路径</strong>: <code>src/config/externalMomentsConfig.ts</code></p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// 外部朋友圈数据源配置（基于 GitHub Gist，完全免费）</span></div></div><div><div><div>2</div></div><div><span>// 数据存储在 GitHub Gist 中，通过 GitHub API 读写</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>export</span><span> </span><span>const</span><span> </span><span>externalMomentsConfig</span><span> </span><span>=</span><span> {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>// 是否启用外部朋友圈数据源</span></div></div><div><div><div>6</div></div><div><span>    </span><span>enable</span><span>:</span><span> </span><span>true</span><span>,</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span>// GitHub Gist ID（创建 Gist 后从 URL 中获取）</span></div></div><div><div><div>9</div></div><div><span>    </span><span>// 替换为你的 Gist ID。https://gist.github.com/Seasir-Hyde/xxxx，其中 xxxx 即为 Gist ID</span></div></div><div><div><div>10</div></div><div><span>    </span><span>gistId</span><span>:</span><span> </span><span>"替换为你的 Gist ID"</span><span>,</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span>// Gist 中的文件名</span></div></div><div><div><div>13</div></div><div><span>    </span><span>fileName</span><span>:</span><span> </span><span>"moments.json"</span><span>,</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>    </span><span>// 默认作者信息</span></div></div><div><div><div>16</div></div><div><span>    </span><span>defaultAuthor</span><span>:</span><span> </span><span>"Hyde"</span><span>,</span></div></div><div><div><div>17</div></div><div><span>    </span><span>defaultAvatar</span><span>:</span><span> </span><span>"https://你的头像URL.jpg"</span><span>,</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>    </span><span>// 后台登录密码的 SHA-256 哈希</span></div></div><div><div><div>20</div></div><div><span>    </span><span>// 生成方式：在线工具:https://www.strongpasswordgenerator.org/zh-cn/sha256-hash-generator/</span></div></div><div><div><div>21</div></div><div><span>    </span><span>adminPasswordHash</span><span>:</span></div></div><div><div><div>22</div></div><div><span>        </span><span>"b5d9ded2ab2812e8653f06fc4d3246a4b13642e8371bc74a7f1003eb4fb4e637"</span><span>,</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>    </span><span>// GitHub Token（优先从环境变量 GITHUB_TOKEN 读取）</span></div></div><div><div><div>25</div></div><div><span>    </span><span>githubToken</span><span>:</span><span><span> </span><span>process</span><span>.</span></span><span>env</span><span>.</span><span>GITHUB_TOKEN</span><span> </span><span>||</span><span> </span><span>""</span><span>,</span></div></div><div><div><div>26</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>创建笔记本配置<a href="#创建笔记本配置"><span>#</span></a></h2><p><strong>文件路径</strong>: <code>src/config/externalNotebooksConfig.ts</code></p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// 外部笔记数据源配置（基于 GitHub Gist）</span></div></div><div><div><div>2</div></div><div><span>// 每个笔记本有独立的 Gist 仓库</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>export</span><span> </span><span>const</span><span> </span><span>externalNotebooksConfig</span><span> </span><span>=</span><span> {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>// 是否启用外部笔记数据源</span></div></div><div><div><div>6</div></div><div><span>    </span><span>enable</span><span>:</span><span> </span><span>true</span><span>,</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span>// 每个笔记本对应的 Gist ID</span></div></div><div><div><div>9</div></div><div><span>    </span><span>// 在 https://gist.github.com 创建 Secret Gist，文件名 notebooks-entries.json，内容 []</span></div></div><div><div><div>10</div></div><div><span>    </span><span>notebookGists</span><span>:</span><span> {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>"每日总结"</span><span>:</span><span> </span><span>"your-gist-id-here"</span><span>,</span></div></div><div><div><div>12</div></div><div><span>        </span><span>"日记"</span><span>:</span><span> </span><span>"your-gist-id-here"</span><span>,</span></div></div><div><div><div>13</div></div><div><span>        </span><span>"日常琐记"</span><span>:</span><span> </span><span>"your-gist-id-here"</span><span>,</span></div></div><div><div><div>14</div></div><div><span>        </span><span>"学习笔记"</span><span>:</span><span> </span><span>"your-gist-id-here"</span><span>,</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>} </span><span>as</span><span> </span><span>Record</span><span><span>&lt;</span><span>string</span><span>, </span><span>string</span><span>&gt;,</span></span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span>// 笔记模板（Admin 页面快速选择）</span></div></div><div><div><div>18</div></div><div><span>    </span><span>// {name} 会被替换为今天的日期</span></div></div><div><div><div>19</div></div><div><span>    </span><span>templates</span><span>:</span><span> [</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>{</span></div></div><div><div><div>21</div></div><div><span>            </span><span>id</span><span>:</span><span> </span><span>"daily"</span><span>,</span></div></div><div><div><div>22</div></div><div><span>            </span><span>icon</span><span>:</span><span> </span><span>"📝"</span><span>,</span></div></div><div><div><div>23</div></div><div><span>            </span><span>name</span><span>:</span><span> </span><span>"每日总结"</span><span>,</span></div></div><div><div><div>24</div></div><div><span>            </span><span>title</span><span>:</span><span> </span><span>"{name} 每日总结"</span><span>,</span></div></div><div><div><div>25</div></div><div><span>            </span><span>content</span><span>:</span><span> </span><span>"📍今天做了什么：</span><span>\n</span><span>📅今日感想：</span><span>\n</span><span>🔮明日计划："</span><span>,</span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>},</span></div></div><div><div><div>27</div></div><div><span><span>        </span></span><span>{</span></div></div><div><div><div>28</div></div><div><span>            </span><span>id</span><span>:</span><span> </span><span>"diary"</span><span>,</span></div></div><div><div><div>29</div></div><div><span>            </span><span>icon</span><span>:</span><span> </span><span>"📔"</span><span>,</span></div></div><div><div><div>30</div></div><div><span>            </span><span>name</span><span>:</span><span> </span><span>"日记"</span><span>,</span></div></div><div><div><div>31</div></div><div><span>            </span><span>title</span><span>:</span><span> </span><span>"{name}"</span><span>,</span></div></div><div><div><div>32</div></div><div><span>            </span><span>content</span><span>:</span><span> </span><span>"## 天气</span><span>\n\n</span><span>## 今天发生了什么</span><span>\n\n</span><span>## 心情</span><span>\n\n</span><span>"</span><span>,</span></div></div><div><div><div>33</div></div><div><span><span>        </span></span><span>},</span></div></div><div><div><div>34</div></div><div><span><span>        </span></span><span>{</span></div></div><div><div><div>35</div></div><div><span>            </span><span>id</span><span>:</span><span> </span><span>"reading"</span><span>,</span></div></div><div><div><div>36</div></div><div><span>            </span><span>icon</span><span>:</span><span> </span><span>"📚"</span><span>,</span></div></div><div><div><div>37</div></div><div><span>            </span><span>name</span><span>:</span><span> </span><span>"读书笔记"</span><span>,</span></div></div><div><div><div>38</div></div><div><span>            </span><span>title</span><span>:</span><span> </span><span>""</span><span>,</span></div></div><div><div><div>39</div></div><div><span>            </span><span>content</span><span>:</span><span> </span><span>"## 📚 书籍信息</span><span>\n\n</span><span>- 书名：</span><span>\n</span><span>- 作者：</span><span>\n\n</span><span>## 核心观点</span><span>\n\n</span><span>## 我的感想</span><span>\n\n</span><span>"</span><span>,</span></div></div><div><div><div>40</div></div><div><span><span>        </span></span><span>},</span></div></div><div><div><div>41</div></div><div><span><span>        </span></span><span>{</span></div></div><div><div><div>42</div></div><div><span>            </span><span>id</span><span>:</span><span> </span><span>"free"</span><span>,</span></div></div><div><div><div>43</div></div><div><span>            </span><span>icon</span><span>:</span><span> </span><span>"📄"</span><span>,</span></div></div><div><div><div>44</div></div><div><span>            </span><span>name</span><span>:</span><span> </span><span>"空白"</span><span>,</span></div></div><div><div><div>45</div></div><div><span>            </span><span>title</span><span>:</span><span> </span><span>""</span><span>,</span></div></div><div><div><div>46</div></div><div><span>            </span><span>content</span><span>:</span><span> </span><span>""</span><span>,</span></div></div><div><div><div>47</div></div><div><span><span>        </span></span><span>},</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>] </span><span>as</span><span> </span><span>Array</span><span>&lt;{</span></div></div><div><div><div>49</div></div><div><span><span>        </span></span><span>id</span><span>:</span><span><span> </span><span>string</span><span>;</span></span></div></div><div><div><div>50</div></div><div><span><span>        </span></span><span>icon</span><span>:</span><span><span> </span><span>string</span><span>;</span></span></div></div><div><div><div>51</div></div><div><span><span>        </span></span><span>name</span><span>:</span><span><span> </span><span>string</span><span>;</span></span></div></div><div><div><div>52</div></div><div><span><span>        </span></span><span>title</span><span>:</span><span><span> </span><span>string</span><span>;</span></span></div></div><div><div><div>53</div></div><div><span><span>        </span></span><span>content</span><span>:</span><span><span> </span><span>string</span><span>;</span></span></div></div><div><div><div>54</div></div><div><span><span>    </span></span><span>}&gt;,</span></div></div><div><div><div>55</div></div><div>
</div></div><div><div><div>56</div></div><div><span>    </span><span>// 后台登录密码的 SHA-256 哈希（与朋友圈后台共用同一密码）</span></div></div><div><div><div>57</div></div><div><span>    </span><span>adminPasswordHash</span><span>:</span></div></div><div><div><div>58</div></div><div><span>        </span><span>"b5d9ded2ab2812e8653f06fc4d3246a4b13642e8371bc74a7f1003eb4fb4e637"</span><span>,</span></div></div><div><div><div>59</div></div><div>
</div></div><div><div><div>60</div></div><div><span>    </span><span>// GitHub Token</span></div></div><div><div><div>61</div></div><div><span>    </span><span>githubToken</span><span>:</span><span><span> </span><span>process</span><span>.</span></span><span>env</span><span>.</span><span>GITHUB_TOKEN</span><span> </span><span>||</span><span> </span><span>""</span><span>,</span></div></div><div><div><div>62</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>修改站点配置<a href="#修改站点配置"><span>#</span></a></h2><p><strong>文件路径</strong>: <code>src/config/siteConfig.ts</code></p><p><strong>操作</strong>: 添加朋友圈封面配置</p><p><strong>插入位置</strong>: 在 <code>defaultAvatar</code> 配置之后</p><p><strong>添加代码</strong>:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// 朋友圈页面封面配置（微信朋友圈风格）</span></div></div><div><div><div>2</div></div><div><span><span>momentsCover</span><span>: {</span></span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>enable</span><span>: </span><span>true</span><span>,</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>image</span><span>: </span><span>"https://你的封面图片URL.jpg"</span><span>,</span></div></div><div><div><div>5</div></div><div><span>},</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>修改类型定义<a href="#修改类型定义"><span>#</span></a></h2><p><strong>文件路径</strong>: <code>src/types/siteConfig.ts</code></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// 朋友圈页面封面配置</span></div></div><div><div><div>2</div></div><div><span><span>momentsCover</span><span>: {</span></span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>enable</span><span>: </span><span>boolean</span><span>;</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>image</span><span>?:</span><span><span> </span><span>string</span><span>;</span></span></div></div><div><div><div>5</div></div><div><span>},</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>修改导航栏配置<a href="#修改导航栏配置"><span>#</span></a></h2><p><strong>文件路径</strong>: <code>src/config/navBarConfig.ts</code></p><p><strong>修改1 - 添加朋友圈链接</strong>:</p><p>在导航链接列表中添加：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>LinkPresets</span><span>.</span></span><span>Moments</span><span>,</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>修改2 - 定义链接预设</strong>:</p><p>在 <code>LinkPresets</code> 对象中添加：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>Moments</span><span>: {</span></span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>name</span><span>: </span><span>"朋友圈"</span><span>,</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>url</span><span>: </span><span>"/moments/"</span><span>,</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>icon</span><span>: </span><span>"mdi:wechat"</span><span>,</span></div></div><div><div><div>5</div></div><div><span>},</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>修改3 - 添加登录链接</strong>:</p><p>文件路径：src/components/layout/Navbar.astro</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&lt;</span><span>LightDarkSwitch</span><span> </span><span>client:load</span><span>&gt;&lt;/</span><span>LightDarkSwitch</span><span>&gt;</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>&lt;</span><span>a</span><span> </span><span>aria-label</span><span>=</span><span>"Admin"</span><span> </span><span>href</span><span>=</span><span>"/admin/"</span><span> </span><span>class</span><span>=</span><span>"btn-plain scale-animation rounded-lg h-11 w-11 active:scale-90"</span><span>&gt;</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>&lt;</span><span>Icon</span><span> </span><span>is:inline</span><span> </span><span>name</span><span>=</span><span>"material-symbols:account-circle"</span><span> </span><span>class</span><span>=</span><span>"text-[1.25rem]"</span><span>&gt;&lt;/</span><span>Icon</span><span>&gt;</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>&lt;/</span><span>a</span><span>&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>修改内容配置<a href="#修改内容配置"><span>#</span></a></h2><p><strong>文件路径</strong>: <code>src/content.config.ts</code></p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// 朋友圈集合</span></div></div><div><div><div>2</div></div><div><span>const</span><span> </span><span>momentsCollection</span><span> </span><span>=</span><span> </span><span>defineCollection</span><span>({</span></div></div><div><div><div>3</div></div><div><span>    </span><span>loader</span><span>:</span><span> </span><span>glob</span><span>({ </span><span>pattern</span><span>:</span><span> </span><span>"**/*.{md,mdx}"</span><span>, </span><span>base</span><span>:</span><span> </span><span>"./src/content/moments"</span><span> }),</span></div></div><div><div><div>4</div></div><div><span>    </span><span>schema</span><span>:</span><span><span> ({ </span><span>image</span><span> }) </span></span><span>=&gt;</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>z</span><span>.</span><span>object</span><span>({</span></div></div><div><div><div>6</div></div><div><span>            </span><span>author</span><span>:</span><span><span> </span><span>z</span><span>.</span></span><span>string</span><span>().</span><span>optional</span><span>().</span><span>default</span><span>(</span><span>""</span><span>),</span></div></div><div><div><div>7</div></div><div><span>            </span><span>avatar</span><span>:</span><span><span> </span><span>z</span><span>.</span></span><span>string</span><span>().</span><span>optional</span><span>().</span><span>default</span><span>(</span><span>""</span><span>),</span></div></div><div><div><div>8</div></div><div><span>            </span><span>pinned</span><span>:</span><span><span> </span><span>z</span><span>.</span></span><span>boolean</span><span>().</span><span>optional</span><span>().</span><span>default</span><span>(</span><span>false</span><span>),</span></div></div><div><div><div>9</div></div><div><span>            </span><span>published</span><span>:</span><span><span> </span><span>z</span><span>.</span></span><span>date</span><span>(),</span></div></div><div><div><div>10</div></div><div><span>            </span><span>images</span><span>:</span><span><span> </span><span>z</span></span></div></div><div><div><div>11</div></div><div><span><span>                </span></span><span>.</span><span>array</span><span>(</span><span>image</span><span>().</span><span>or</span><span><span>(</span><span>z</span><span>.</span></span><span>string</span><span>()))</span></div></div><div><div><div>12</div></div><div><span><span>                </span></span><span>.</span><span>or</span><span><span>(</span><span>z</span><span>.</span></span><span>string</span><span>())</span></div></div><div><div><div>13</div></div><div><span><span>                </span></span><span>.</span><span>optional</span><span>()</span></div></div><div><div><div>14</div></div><div><span><span>                </span></span><span>.</span><span>default</span><span>([]),</span></div></div><div><div><div>15</div></div><div><span>            </span><span>tags</span><span>:</span><span><span> </span><span>z</span><span>.</span></span><span>array</span><span><span>(</span><span>z</span><span>.</span></span><span>string</span><span>()).</span><span>optional</span><span>().</span><span>default</span><span>([]),</span></div></div><div><div><div>16</div></div><div><span>            </span><span>location</span><span>:</span><span><span> </span><span>z</span><span>.</span></span><span>string</span><span>().</span><span>optional</span><span>().</span><span>default</span><span>(</span><span>""</span><span>),</span></div></div><div><div><div>17</div></div><div><span>            </span><span>device</span><span>:</span><span><span> </span><span>z</span><span>.</span></span><span>string</span><span>().</span><span>optional</span><span>().</span><span>default</span><span>(</span><span>""</span><span>),</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>}),</span></div></div><div><div><div>19</div></div><div><span>});</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>// 导出朋友圈集合</span></div></div><div><div><div>22</div></div><div><span>export</span><span> </span><span>const</span><span> </span><span>collections</span><span> </span><span>=</span><span> {</span></div></div><div><div><div>23</div></div><div><span>    </span><span>posts</span><span>:</span><span><span> </span><span>postsCollection</span><span>,</span></span></div></div><div><div><div>24</div></div><div><span>    </span><span>spec</span><span>:</span><span><span> </span><span>specCollection</span><span>,</span></span></div></div><div><div><div>25</div></div><div><span>    </span><span>ziyuan</span><span>:</span><span><span> </span><span>ziyuanCollection</span><span>,</span></span></div></div><div><div><div>26</div></div><div><span>    </span><span>moments</span><span>:</span><span><span> </span><span>momentsCollection</span><span>,  </span></span><span>// 朋友圈集合</span></div></div><div><div><div>27</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><hr /></section>
<section><h2>创建朋友圈数据获取脚本<a href="#创建朋友圈数据获取脚本"><span>#</span></a></h2><p><strong>文件路径</strong>: <code>src/data/moments.ts</code></p><div><div><div></div><div>提示</div></div><div><p>此脚本从布局文件抽离，便于维护。负责从 GitHub Gist 加载朋友圈数据并动态插入页面。</p></div></div><p><strong>完整代码</strong>: <a href="https://cnb.cool/W3C/Hyde/Firefly-hyde/-/blob/main/src/data/moments.ts" target="_blank">moments.ts</a></p></section>
<section><h2>创建精选朋友圈页面<a href="#创建精选朋友圈页面"><span>#</span></a></h2><p><strong>文件路径</strong>: <code>src/pages/moments/pinned.astro</code></p><p><strong>完整代码</strong>: <a href="https://cnb.cool/W3C/Hyde/Firefly-hyde/-/blob/main/src/pages/moments/pinned.astro" target="_blank">pinned.astro</a></p></section>
<section><h2>Admin 管理后台<a href="#admin-管理后台"><span>#</span></a></h2><section><h3>创建登录页<a href="#创建登录页"><span>#</span></a></h3><p><strong>文件路径</strong>: <code>src/pages/admin/index.astro</code></p><p><strong>核心功能</strong>: SHA-256 密码验证，登录成功后存储 token 到 localStorage</p><p><strong>完整代码</strong>: <a href="https://cnb.cool/W3C/Hyde/Firefly-hyde/-/blob/main/src/pages/admin/index.astro" target="_blank">index.astro</a></p></section><section><h3>创建朋友圈管理页<a href="#创建朋友圈管理页"><span>#</span></a></h3><p><strong>文件路径</strong>: <code>src/pages/admin/moments.astro</code></p><p><strong>核心功能</strong>:</p><ul>
<li>从 Gist 读取朋友圈数据</li>
<li>新增/编辑/删除朋友圈</li>
<li>支持图片上传（转为 base64）</li>
<li>支持置顶、标签、位置</li>
</ul><p><strong>完整代码</strong>: <a href="https://cnb.cool/W3C/Hyde/Firefly-hyde/-/blob/main/src/pages/admin/moments.astro" target="_blank">moments.astro</a></p></section><section><h3>创建笔记本管理页<a href="#创建笔记本管理页"><span>#</span></a></h3><p><strong>文件路径</strong>: <code>src/pages/admin/notebooks.astro</code></p><p><strong>核心功能</strong>:</p><ul>
<li>多笔记本支持</li>
<li>笔记模板选择</li>
<li>Markdown 编辑</li>
<li>时间线展示</li>
</ul><p><strong>完整代码</strong>: <a href="https://cnb.cool/W3C/Hyde/Firefly-hyde/-/blob/main/src/pages/admin/notebooks.astro" target="_blank">notebooks.astro</a></p></section></section>
<section><h2>环境变量配置<a href="#环境变量配置"><span>#</span></a></h2><p><strong>文件路径</strong>: <code>.env</code></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># GitHub Token（用于 Gist API 认证）</span></div></div><div><div><div>2</div></div><div><span># 获取方式：https://github.com/settings/tokens</span></div></div><div><div><div>3</div></div><div><span># 需要 gist 权限</span></div></div><div><div><div>4</div></div><div><span>GITHUB_TOKEN=your_github_token_here</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section>
<section><h2>路径别名配置<a href="#路径别名配置"><span>#</span></a></h2></section>
<section><h2>修改 Astro 配置<a href="#修改-astro-配置"><span>#</span></a></h2><p><strong>文件路径</strong>: <code>astro.config.mjs</code></p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> { </span><span>defineConfig</span><span> } </span><span>from</span><span> </span><span>'astro/config'</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>export</span><span> </span><span>default</span><span> </span><span>defineConfig</span><span>({</span></div></div><div><div><div>4</div></div><div><span>    </span><span>vite</span><span>:</span><span> {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>resolve</span><span>:</span><span> {</span></div></div><div><div><div>6</div></div><div><span>            </span><span>alias</span><span>:</span><span> {</span></div></div><div><div><div>7</div></div><div><span>                </span><span>"@"</span><span>:</span><span> </span><span>"./src"</span><span>,</span></div></div><div><div><div>8</div></div><div><span>                </span><span>"@components"</span><span>:</span><span> </span><span>"./src/components"</span><span>,</span></div></div><div><div><div>9</div></div><div><span>                </span><span>"@layouts"</span><span>:</span><span> </span><span>"./src/layouts"</span><span>,</span></div></div><div><div><div>10</div></div><div><span>                </span><span>"@config"</span><span>:</span><span> </span><span>"./src/config"</span><span>,</span></div></div><div><div><div>11</div></div><div><span>                </span><span>"@utils"</span><span>:</span><span> </span><span>"./src/utils"</span><span>,</span></div></div><div><div><div>12</div></div><div><span>                </span><span>"@data"</span><span>:</span><span> </span><span>"./src/data"</span><span>,</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>},</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>},</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>16</div></div><div><span>});</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><hr /></section>
<section><h2>文件清单<a href="#文件清单"><span>#</span></a></h2>

<table><thead><tr><th>文件路径</th><th>操作</th><th>说明</th></tr></thead><tbody><tr><td><code>src/utils/gist-api.ts</code></td><td>新建</td><td>Gist API 工具函数</td></tr><tr><td><code>src/config/externalMomentsConfig.ts</code></td><td>新建</td><td>朋友圈配置</td></tr><tr><td><code>src/config/externalNotebooksConfig.ts</code></td><td>新建</td><td>笔记本配置</td></tr><tr><td><code>src/config/siteConfig.ts</code></td><td>修改</td><td>添加封面配置</td></tr><tr><td><code>src/config/navBarConfig.ts</code></td><td>修改</td><td>添加导航链接</td></tr><tr><td><code>src/types/siteConfig.ts</code></td><td>修改</td><td>添加类型定义</td></tr><tr><td><code>src/content.config.ts</code></td><td>修改</td><td>添加 moments 集合</td></tr><tr><td><code>src/data/moments.ts</code></td><td>新建</td><td>朋友圈数据获取脚本</td></tr><tr><td><code>src/utils/content-utils.ts</code></td><td>修改</td><td>添加数据获取</td></tr><tr><td><code>src/components/moments/MomentCard.astro</code></td><td>新建</td><td>朋友圈卡片组件</td></tr><tr><td><code>src/components/moments/MomentsCover.astro</code></td><td>新建</td><td>朋友圈封面组件</td></tr><tr><td><code>src/pages/moments.astro</code></td><td>新建</td><td>朋友圈主页</td></tr><tr><td><code>src/pages/moments/pinned.astro</code></td><td>新建</td><td>精选朋友圈</td></tr><tr><td><code>src/pages/admin/index.astro</code></td><td>新建</td><td>登录页</td></tr><tr><td><code>src/pages/admin/moments.astro</code></td><td>新建</td><td>朋友圈管理</td></tr><tr><td><code>src/pages/admin/notebooks.astro</code></td><td>新建</td><td>笔记本管理</td></tr><tr><td><code>src/pages/life/notebooks/remote-entry.astro</code></td><td>新建</td><td>远程笔记页</td></tr><tr><td><code>.env</code></td><td>新建</td><td>环境变量配置</td></tr><tr><td><code>astro.config.mjs</code></td><td>修改</td><td>添加路径别名</td></tr></tbody></table></section>]]></content>
    </entry>
    <entry>
      <id>https://seasir.top/posts/HideCoverImage/</id>
      <title type="text">隐藏封面图</title>
      <published>2026-06-03T00:00:00.000Z</published>
      <updated>2026-06-03T00:00:00.000Z</updated>
      <author><name>Hyde</name></author>
      <link rel="alternate" href="https://seasir.top/posts/HideCoverImage/"/>
      <summary type="text">最近在折腾博客的封面图显示逻辑，加了个「隐藏封面图」的开关功能——从类型定义、配置项、工具函数到 UI 控件和样式，一步步把整个流程补全了。代码分散在多个文件里，但核心就一件事：让用户能一键收起或展开文章封面，既保持页面简洁，又不丢失视觉层次。顺手还做了多语言支持，中文、英文切换也一起配上了。</summary>
      <content type="html"><![CDATA[<section><h2>添加类型定义<a href="#添加类型定义"><span>#</span></a></h2><p>文件路径：<code>src/types/config.ts</code></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// 文章列表布局配置</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>postListLayout</span><span>: {</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>allowCoverSwitch</span><span>?:</span><span><span> </span><span>boolean</span><span>; </span></span><span>// 是否允许用户切换文章封面图显示</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>showCover</span><span>?:</span><span><span> </span><span>boolean</span><span>; </span></span><span>// 默认是否显示文章封面图</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>添加配置项<a href="#添加配置项"><span>#</span></a></h2><p>文件路径：<code>src/config/siteConfig.ts</code></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>postListLayout</span><span>: {</span></span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>allowCoverSwitch</span><span>: </span><span>true</span><span>,  </span><span>// 新增：允许切换封面图显示</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>showCover</span><span>: </span><span>true</span><span>,         </span><span>// 新增：默认显示封面图</span></div></div><div><div><div>4</div></div><div><span>},</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>设置工具函数<a href="#设置工具函数"><span>#</span></a></h2><p>文件路径：<code>src/utils/setting-utils.ts</code></p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// 获取默认封面图显示状态</span></div></div><div><div><div>2</div></div><div><span>export</span><span> </span><span>function</span><span> </span><span>getDefaultPostCoverImageEnabled</span><span>()</span><span>:</span><span> </span><span>boolean</span><span> {</span></div></div><div><div><div>3</div></div><div><span>  </span><span>return</span><span><span> </span><span>siteConfig</span><span>.</span></span><span>postListLayout</span><span>.</span><span>showCover</span><span> </span><span>??</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>// 从localStorage读取封面图显示状态</span></div></div><div><div><div>7</div></div><div><span>export</span><span> </span><span>function</span><span> </span><span>getStoredPostCoverImageEnabled</span><span>()</span><span>:</span><span> </span><span>boolean</span><span> {</span></div></div><div><div><div>8</div></div><div><span>  </span><span>if</span><span> (</span></div></div><div><div><div>9</div></div><div><span>    </span><span>typeof</span><span><span> </span><span>localStorage</span><span> </span></span><span>===</span><span> </span><span>"undefined"</span><span> </span><span>||</span></div></div><div><div><div>10</div></div><div><span>    </span><span>typeof</span><span><span> </span><span>localStorage</span><span>.</span></span><span>getItem</span><span> </span><span>!==</span><span> </span><span>"function"</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>) {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>return</span><span> </span><span>getDefaultPostCoverImageEnabled</span><span>();</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span>  </span><span>const</span><span> </span><span>stored</span><span> </span><span>=</span><span><span> </span><span>localStorage</span><span>.</span></span><span>getItem</span><span>(</span><span>"postCoverImageEnabled"</span><span>);</span></div></div><div><div><div>15</div></div><div><span>  </span><span>if</span><span><span> (</span><span>stored</span><span> </span></span><span>===</span><span> </span><span>null</span><span>) {</span></div></div><div><div><div>16</div></div><div><span>    </span><span>return</span><span> </span><span>getDefaultPostCoverImageEnabled</span><span>();</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span>  </span><span>return</span><span><span> </span><span>stored</span><span> </span></span><span>===</span><span> </span><span>"true"</span><span>;</span></div></div><div><div><div>19</div></div><div><span>}</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>// 设置封面图显示状态</span></div></div><div><div><div>22</div></div><div><span>export</span><span> </span><span>function</span><span> </span><span>setPostCoverImageEnabled</span><span><span>(</span><span>enabled</span></span><span>:</span><span> </span><span>boolean</span><span>)</span><span>:</span><span> </span><span>void</span><span> {</span></div></div><div><div><div>23</div></div><div><span>  </span><span>if</span><span> (</span></div></div><div><div><div>24</div></div><div><span>    </span><span>typeof</span><span><span> </span><span>localStorage</span><span> </span></span><span>===</span><span> </span><span>"undefined"</span><span> </span><span>||</span></div></div><div><div><div>25</div></div><div><span>    </span><span>typeof</span><span><span> </span><span>localStorage</span><span>.</span></span><span>setItem</span><span> </span><span>!==</span><span> </span><span>"function"</span></div></div><div><div><div>26</div></div><div><span><span>  </span></span><span>) {</span></div></div><div><div><div>27</div></div><div><span>    </span><span>return</span><span>;</span></div></div><div><div><div>28</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>29</div></div><div><span><span>  </span></span><span>localStorage</span><span>.</span><span>setItem</span><span>(</span><span>"postCoverImageEnabled"</span><span>, </span><span>String</span><span><span>(</span><span>enabled</span><span>));</span></span></div></div><div><div><div>30</div></div><div><span>  </span><span>applyPostCoverImageEnabledToDocument</span><span><span>(</span><span>enabled</span><span>);</span></span></div></div><div><div><div>31</div></div><div><span>  </span><span>if</span><span> (</span><span>typeof</span><span><span> </span><span>window</span><span> </span></span><span>!==</span><span> </span><span>"undefined"</span><span>) {</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>window</span><span>.</span><span>dispatchEvent</span><span>(</span></div></div><div><div><div>33</div></div><div><span>      </span><span>new</span><span> </span><span>CustomEvent</span><span>(</span><span>"postCoverImageChange"</span><span>, {</span></div></div><div><div><div>34</div></div><div><span>        </span><span>detail</span><span>:</span><span><span> { </span><span>enabled</span><span> },</span></span></div></div><div><div><div>35</div></div><div><span><span>      </span></span><span>}),</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>);</span></div></div><div><div><div>37</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>38</div></div><div><span>}</span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>// 将封面图状态应用到文档</span></div></div><div><div><div>41</div></div><div><span>export</span><span> </span><span>function</span><span> </span><span>applyPostCoverImageEnabledToDocument</span><span><span>(</span><span>enabled</span></span><span>:</span><span> </span><span>boolean</span><span>)</span><span>:</span><span> </span><span>void</span><span> {</span></div></div><div><div><div>42</div></div><div><span>  </span><span>if</span><span> (</span><span>typeof</span><span><span> </span><span>document</span><span> </span></span><span>===</span><span> </span><span>"undefined"</span><span>) {</span></div></div><div><div><div>43</div></div><div><span>    </span><span>return</span><span>;</span></div></div><div><div><div>44</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>45</div></div><div><span><span>  </span></span><span>document</span><span>.</span><span>documentElement</span><span>.</span><span>setAttribute</span><span>(</span></div></div><div><div><div>46</div></div><div><span>    </span><span>"data-post-cover-enabled"</span><span>,</span></div></div><div><div><div>47</div></div><div><span>    </span><span>String</span><span><span>(</span><span>enabled</span><span>),</span></span></div></div><div><div><div>48</div></div><div><span><span>  </span></span><span>);</span></div></div><div><div><div>49</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>添加设置面板开关<a href="#添加设置面板开关"><span>#</span></a></h2><p>文件路径：<code>src/components/controls/DisplaySettingsIntegrated.svelte</code></p><section><h3>导入相关函数<a href="#导入相关函数"><span>#</span></a></h3><ul>
<li>在文件顶部导入新添加的工具函数：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>// ... 其他导入</span></div></div><div><div><div>3</div></div><div><span>  </span><span>getDefaultPostCoverImageEnabled</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>getStoredPostCoverImageEnabled</span><span>,</span></div></div><div><div><div>5</div></div><div><span>  </span><span>setPostCoverImageEnabled</span><span>,</span></div></div><div><div><div>6</div></div><div><span>  </span><span>applyPostCoverImageEnabledToDocument</span><span>,</span></div></div><div><div><div>7</div></div><div><span>} </span><span>from</span><span> </span><span>"@utils/setting-utils"</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>添加状态变量<a href="#添加状态变量"><span>#</span></a></h3><ul>
<li>在脚本部分添加以下状态变量：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>const</span><span> </span><span>defaultPostCoverImageEnabled</span><span> </span><span>=</span><span> </span><span>getDefaultPostCoverImageEnabled</span><span>();</span></div></div><div><div><div>2</div></div><div><span>const</span><span> </span><span>isPostCoverImageSwitchable</span><span> </span><span>=</span><span><span> </span><span>siteConfig</span><span>.</span></span><span>postListLayout</span><span>.</span><span>allowCoverSwitch</span><span> </span><span>??</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>3</div></div><div><span>let</span><span><span> </span><span>postCoverImageEnabled</span><span> </span></span><span>=</span><span> </span><span>$state</span><span>(</span><span>true</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>添加切换函数<a href="#添加切换函数"><span>#</span></a></h3><ul>
<li>添加切换封面图状态的函数：数：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>function</span><span> </span><span>togglePostCoverImageEnabled</span><span>() {</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>postCoverImageEnabled</span><span> </span><span>=</span><span> </span><span>!</span><span><span>postCoverImageEnabled</span><span>;</span></span></div></div><div><div><div>3</div></div><div><span>  </span><span>setPostCoverImageEnabled</span><span><span>(</span><span>postCoverImageEnabled</span><span>);</span></span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>在 onMount 中初始化状态<a href="#在-onmount-中初始化状态"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// 从localStorage读取文章封面图状态</span></div></div><div><div><div>2</div></div><div><span><span>postCoverImageEnabled</span><span> </span></span><span>=</span><span> </span><span>getStoredPostCoverImageEnabled</span><span>();</span></div></div><div><div><div>3</div></div><div><span>// 将状态应用到文档</span></div></div><div><div><div>4</div></div><div><span>applyPostCoverImageEnabledToDocument</span><span><span>(</span><span>postCoverImageEnabled</span><span>);</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>添加开关 UI<a href="#添加开关-ui"><span>#</span></a></h3><ul>
<li>在 HTML 模板部分（布局切换区域后）添加：</li>
</ul><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&lt;!-- Post Cover Image Switch Section --&gt;</span></div></div><div><div><div>2</div></div><div><span>{#</span><span>if</span><span><span> </span><span>isPostCoverImageSwitchable</span><span>}</span></span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"mt-2 mb-2"</span><span>&gt;</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"flex gap-2 font-bold text-lg text-neutral-900 dark:text-neutral-100 transition relative ml-3 mb-2</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>before:w-1 before:h-4 before:rounded-md before:bg-(--primary)</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>before:absolute before:-left-3 before:top-1/2 before:-translate-y-1/2"</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>&gt;</span></div></div><div><div><div>8</div></div><div><span>      </span><span>{</span><span>i18n</span><span><span>(</span><span>I18nKey</span><span>.</span></span><span>postCoverImage</span><span>)</span><span>}</span></div></div><div><div><div>9</div></div><div><span><span>      </span></span><span>&lt;</span><span>button</span><span> </span><span>aria-label</span><span>=</span><span>"Reset to Default"</span><span> </span><span>class</span><span>=</span><span>"btn-regular w-7 h-7 rounded-md active:scale-90"</span></div></div><div><div><div>10</div></div><div><span>              </span><span>class</span><span>:</span><span>opacity-0</span><span>=</span><span>{</span><span><span>postCoverImageEnabled</span><span> </span></span><span>===</span><span><span> </span><span>defaultPostCoverImageEnabled</span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>              </span><span>class</span><span>:</span><span>pointer-events-none</span><span>=</span><span>{</span><span><span>postCoverImageEnabled</span><span> </span></span><span>===</span><span><span> </span><span>defaultPostCoverImageEnabled</span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>              </span><span>onclick</span><span>=</span><span>{</span><span>() </span><span>=&gt;</span><span> {</span></div></div><div><div><div>13</div></div><div><span><span>                </span></span><span>postCoverImageEnabled</span><span> </span><span>=</span><span><span> </span><span>defaultPostCoverImageEnabled</span><span>;</span></span></div></div><div><div><div>14</div></div><div><span>                </span><span>setPostCoverImageEnabled</span><span><span>(</span><span>defaultPostCoverImageEnabled</span><span>);</span></span></div></div><div><div><div>15</div></div><div><span><span>              </span></span><span>}</span><span>}</span><span>&gt;</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"text-(--btn-content)"</span><span>&gt;</span></div></div><div><div><div>17</div></div><div><span><span>          </span></span><span>&lt;</span><span>Icon</span><span> </span><span>icon</span><span>=</span><span>"fa7-solid:arrow-rotate-left"</span><span> </span><span>class</span><span>=</span><span>"text-[0.875rem]"</span><span>&gt;&lt;/</span><span>Icon</span><span>&gt;</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>19</div></div><div><span><span>      </span></span><span>&lt;/</span><span>button</span><span>&gt;</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"space-y-1"</span><span>&gt;</span></div></div><div><div><div>22</div></div><div><span><span>      </span></span><span>&lt;</span><span>button</span></div></div><div><div><div>23</div></div><div><span>        </span><span>class</span><span>=</span><span>"w-full btn-regular rounded-md py-2 px-3 flex items-center gap-3 text-left active:scale-95 transition-all relative overflow-hidden"</span></div></div><div><div><div>24</div></div><div><span>        </span><span>class</span><span><span>:</span><span>bg-</span><span>(--btn-regular-bg-hover)={</span></span><span>postCoverImageEnabled</span><span>}</span></div></div><div><div><div>25</div></div><div><span>        </span><span>onclick</span><span>=</span><span>{</span><span>togglePostCoverImageEnabled</span><span>}</span></div></div><div><div><div>26</div></div><div><span><span>      </span></span><span>&gt;</span></div></div><div><div><div>27</div></div><div><span><span>        </span></span><span>&lt;</span><span>Icon</span><span> </span><span>icon</span><span>=</span><span>"material-symbols:image"</span><span> </span><span>class</span><span>=</span><span>"text-[1.25rem] shrink-0"</span><span>&gt;&lt;/</span><span>Icon</span><span>&gt;</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>&lt;</span><span>span</span><span> </span><span>class</span><span>=</span><span>"text-sm flex-1"</span><span>&gt;</span><span>{</span><span>i18n</span><span><span>(</span><span>I18nKey</span><span>.</span></span><span>postCoverImage</span><span>)</span><span>}</span><span>&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"w-10 h-5 rounded-full transition-all duration-200 relative"</span></div></div><div><div><div>30</div></div><div><span>             </span><span>class</span><span><span>:</span><span>bg-</span><span>(--primary)={</span></span><span>postCoverImageEnabled</span><span>}</span></div></div><div><div><div>31</div></div><div><span>             </span><span>class</span><span><span>:</span><span>bg-</span><span>(--btn-regular-bg-active)={!</span></span><span>postCoverImageEnabled</span><span>}&gt;</span></div></div><div><div><div>32</div></div><div><span><span>          </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"absolute top-0.5 w-4 h-4 bg-white rounded-full shadow transition-all duration-200"</span></div></div><div><div><div>33</div></div><div><span>               </span><span>class</span><span><span>:</span><span>left-0</span><span>.5={!</span></span><span>postCoverImageEnabled</span><span>}</span></div></div><div><div><div>34</div></div><div><span>               </span><span>class</span><span>:</span><span>left-5</span><span>=</span><span>{</span><span>postCoverImageEnabled</span><span>}</span><span>&gt;&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>35</div></div><div><span><span>        </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>36</div></div><div><span><span>      </span></span><span>&lt;/</span><span>button</span><span>&gt;</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>38</div></div><div><span><span>  </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>39</div></div><div><span>{/</span><span>if</span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section></section>
<section><h2>添加样式控制<a href="#添加样式控制"><span>#</span></a></h2><p>文件路径：<code>src/components/layout/PostCard.astro</code></p><ul>
<li>在 
</li></ul><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/* === Hide Post Cover Image === */</span></div></div><div><div><div>2</div></div><div><span>/* 当 data-post-cover-enabled="false" 时隐藏封面图 */</span></div></div><div><div><div>3</div></div><div><span>:global([</span><span>data-post-cover-enabled</span><span>=</span><span>"false"</span><span>]</span><span>) </span><span>.post-card-image</span><span> {</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>display: </span><span>none</span><span> </span><span>!important</span><span>;</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>:global([</span><span>data-post-cover-enabled</span><span>=</span><span>"false"</span><span>]</span><span>) </span><span>.has-cover</span><span> {</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>&amp;.post-card-wrapper {</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>.post-card-content {</span></div></div><div><div><div>10</div></div><div><span><span>      </span></span><span>width: </span><span>calc</span><span>(</span><span><span>100</span><span>%</span></span><span><span> </span><span>-</span><span> </span></span><span><span>52</span><span>px</span></span><span><span> </span><span>-</span><span> </span></span><span><span>12</span><span>px</span></span><span>) </span><span>!important</span><span>;</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>:global([</span><span>data-post-cover-enabled</span><span>=</span><span>"false"</span><span>]</span><span>) </span><span>.has-cover</span><span> </span><span>.post-card-enter-btn</span><span> {</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>display: </span><span>flex</span><span> </span><span>!important</span><span>;</span></div></div><div><div><div>17</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>语言配置<a href="#语言配置"><span>#</span></a></h2><ul>
<li>文件路径：<code>src/i18n/i18nKey.ts</code></li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>postCoverImage</span><span> </span></span><span>=</span><span> </span><span>"postCoverImage"</span><span>,</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>文件路径：<code>src/i18n/languages/en.ts</code></li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>[</span><span>Key</span><span>.</span></span><span>postCoverImage</span><span>]: </span><span>"Post Cover Image"</span><span>,</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>文件路径：<code>src/i18n/languages/ja.ts</code></li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>[</span><span>Key</span><span>.</span></span><span>postCoverImage</span><span>]: </span><span>"記事のカバー画像"</span><span>,</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>文件路径：<code>src/i18n/languages/ru.ts</code></li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>[</span><span>Key</span><span>.</span></span><span>postCoverImage</span><span>]: </span><span>"Обложка поста"</span><span>,</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>文件路径：<code>src/i18n/languages/zh_CN.ts</code></li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>[</span><span>Key</span><span>.</span></span><span>postCoverImage</span><span>]: </span><span>"文章封面图"</span><span>,</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>文件路径：<code>src/i18n/languages/zh_TW.ts</code></li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>[</span><span>Key</span><span>.</span></span><span>postCoverImage</span><span>]: </span><span>"文章封面圖"</span><span>,</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>]]></content>
    </entry>
    <entry>
      <id>https://seasir.top/posts/ArchiveStatistics/</id>
      <title type="text">归档统计</title>
      <published>2026-06-02T00:00:00.000Z</published>
      <updated>2026-06-02T00:00:00.000Z</updated>
      <author><name>Hyde</name></author>
      <link rel="alternate" href="https://seasir.top/posts/ArchiveStatistics/"/>
      <summary type="text">最近在折腾博客的归档统计功能，从获取归档列表开始，一路写到更新内容的工具函数、类型定义，再到多语言配置的拆分与管理——每一处改动都为了让归档页更清晰、更易维护，也顺便理清了自己代码里的逻辑脉络。</summary>
      <content type="html"><![CDATA[<section><h2>获取归档列表<a href="#获取归档列表"><span>#</span></a></h2><p>文件路径：<code>src/pages/archive.astro</code></p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>---</span></div></div><div><div><div>2</div></div><div><span>import</span><span> { </span><span>getSortedPostsList</span><span> } </span><span>from</span><span> </span><span>"@/utils/content-utils"</span><span>;  </span><span>// [!code --]</span></div></div><div><div><div>3</div></div><div><span>const</span><span> </span><span>sortedPostsList</span><span> </span><span>=</span><span> </span><span>await</span><span> </span><span>getSortedPostsList</span><span>();     </span><span>// [!code --]</span></div></div><div><div><div>4</div></div><div><span>&lt;</span><span><span>ArchivePanel</span><span> </span><span>sortedPosts</span></span><span>=</span><span><span>{</span><span>sortedPostsList</span><span>} </span><span>client</span><span>:</span><span>only</span></span><span>=</span><span>"svelte"</span><span> </span><span>/&gt;</span><span> </span><span>// [!code --]</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>import</span><span> { </span><span>getArchiveList</span><span> } </span><span>from</span><span> </span><span>"@/utils/content-utils"</span><span>;</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>// 获取归档列表（posts + moments + bangumi + life）</span></div></div><div><div><div>9</div></div><div><span>const</span><span> </span><span>archiveList</span><span> </span><span>=</span><span> </span><span>await</span><span> </span><span>getArchiveList</span><span>();</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>// 计算各类型数量</span></div></div><div><div><div>12</div></div><div><span>const</span><span> </span><span>postsCount</span><span> </span><span>=</span><span> </span><span>archiveList</span><span>.</span><span>filter</span><span>(</span><span>item</span><span> </span><span>=&gt;</span><span> </span><span>item</span><span>.</span><span>type</span><span> </span><span>===</span><span> </span><span>"post"</span><span>).</span><span>length</span><span>;</span></div></div><div><div><div>13</div></div><div><span>const</span><span> </span><span>momentsCount</span><span> </span><span>=</span><span> </span><span>archiveList</span><span>.</span><span>filter</span><span>(</span><span>item</span><span> </span><span>=&gt;</span><span> </span><span>item</span><span>.</span><span>type</span><span> </span><span>===</span><span> </span><span>"moment"</span><span>).</span><span>length</span><span>;</span></div></div><div><div><div>14</div></div><div><span>const</span><span> </span><span>bangumiCount</span><span> </span><span>=</span><span> </span><span>archiveList</span><span>.</span><span>filter</span><span>(</span><span>item</span><span> </span><span>=&gt;</span><span> </span><span>item</span><span>.</span><span>type</span><span> </span><span>===</span><span> </span><span>"bangumi"</span><span>).</span><span>length</span><span>;</span></div></div><div><div><div>15</div></div><div><span>const</span><span> </span><span>lifeCount</span><span> </span><span>=</span><span> </span><span>archiveList</span><span>.</span><span>filter</span><span>(</span><span>item</span><span> </span><span>=&gt;</span><span> </span><span>item</span><span>.</span><span>type</span><span> </span><span>===</span><span> </span><span>"life"</span><span>).</span><span>length</span><span>;</span></div></div><div><div><div>16</div></div><div><span>const</span><span> </span><span>totalCount</span><span> </span><span>=</span><span> </span><span>postsCount</span><span> </span><span>+</span><span> </span><span>momentsCount</span><span> </span><span>+</span><span> </span><span>bangumiCount</span><span> </span><span>+</span><span> </span><span>lifeCount</span><span>;</span></div></div><div><div><div>17</div></div><div><span>---</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>&lt;</span><span>MainGridLayout</span><span> </span><span>title</span><span>=</span><span>{</span><span>i18n</span><span>(</span><span>I18nKey</span><span>.</span><span>archive</span><span>)</span><span>}</span><span>&gt;</span></div></div><div><div><div>20</div></div><div><span>      </span><span>&lt;!-- 统计信息 --&gt;</span></div></div><div><div><div>21</div></div><div><span><span><span>    </span></span><span>&lt;</span></span><span>div</span><span> </span><span>class</span><span>=</span><span>"card-base px-8 py-6 mb-6"</span><span>&gt;</span></div></div><div><div><div>22</div></div><div><span><span><span>        </span></span><span>&lt;</span></span><span>div</span><span> </span><span>class</span><span>=</span><span>"flex flex-wrap gap-6 justify-center text-center"</span><span>&gt;</span></div></div><div><div><div>23</div></div><div><span><span><span>            </span></span><span>&lt;</span></span><span>div</span><span> </span><span>class</span><span>=</span><span>"flex flex-col"</span><span>&gt;</span></div></div><div><div><div>24</div></div><div><span><span><span>                </span></span><span>&lt;</span></span><span>span</span><span> </span><span>class</span><span>=</span><span>"text-3xl font-bold text-amber-500"</span><span>&gt;</span><span>{</span><span>postsCount</span><span>}</span><span>&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>25</div></div><div><span><span><span>                </span></span><span>&lt;</span></span><span>span</span><span> </span><span>class</span><span>=</span><span>"text-sm text-30 mt-1"</span><span>&gt;</span><span>{</span><span>i18n</span><span>(</span><span>I18nKey</span><span>.</span><span>postList</span><span>)</span><span>}</span><span>&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>26</div></div><div><span><span><span>            </span></span><span>&lt;/</span></span><span>div</span><span>&gt;</span></div></div><div><div><div>27</div></div><div><span><span><span>            </span></span><span>&lt;</span></span><span>div</span><span> </span><span>class</span><span>=</span><span>"w-px bg-neutral-200 dark:bg-neutral-700"</span><span>&gt;&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>28</div></div><div><span><span><span>            </span></span><span>&lt;</span></span><span>div</span><span> </span><span>class</span><span>=</span><span>"flex flex-col"</span><span>&gt;</span></div></div><div><div><div>29</div></div><div><span><span><span>                </span></span><span>&lt;</span></span><span>span</span><span> </span><span>class</span><span>=</span><span>"text-3xl font-bold text-(--primary)"</span><span>&gt;</span><span>{</span><span>momentsCount</span><span>}</span><span>&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>30</div></div><div><span><span><span>                </span></span><span>&lt;</span></span><span>span</span><span> </span><span>class</span><span>=</span><span>"text-sm text-30 mt-1"</span><span>&gt;</span><span>{</span><span>i18n</span><span>(</span><span>I18nKey</span><span>.</span><span>moments</span><span>)</span><span>}</span><span>&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>31</div></div><div><span><span><span>            </span></span><span>&lt;/</span></span><span>div</span><span>&gt;</span></div></div><div><div><div>32</div></div><div><span><span><span>            </span></span><span>&lt;</span></span><span>div</span><span> </span><span>class</span><span>=</span><span>"w-px bg-neutral-200 dark:bg-neutral-700"</span><span>&gt;&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>33</div></div><div><span><span><span>            </span></span><span>&lt;</span></span><span>div</span><span> </span><span>class</span><span>=</span><span>"flex flex-col"</span><span>&gt;</span></div></div><div><div><div>34</div></div><div><span><span><span>                </span></span><span>&lt;</span></span><span>span</span><span> </span><span>class</span><span>=</span><span>"text-3xl font-bold text-red-500 dark:text-red-400"</span><span>&gt;</span><span>{</span><span>bangumiCount</span><span>}</span><span>&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>35</div></div><div><span><span><span>                </span></span><span>&lt;</span></span><span>span</span><span> </span><span>class</span><span>=</span><span>"text-sm text-30 mt-1"</span><span>&gt;</span><span>{</span><span>i18n</span><span>(</span><span>I18nKey</span><span>.</span><span>bangumi</span><span>)</span><span>}</span><span>&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>36</div></div><div><span><span><span>            </span></span><span>&lt;/</span></span><span>div</span><span>&gt;</span></div></div><div><div><div>37</div></div><div><span><span><span>            </span></span><span>&lt;</span></span><span>div</span><span> </span><span>class</span><span>=</span><span>"w-px bg-neutral-200 dark:bg-neutral-700"</span><span>&gt;&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>38</div></div><div><span><span><span>            </span></span><span>&lt;</span></span><span>div</span><span> </span><span>class</span><span>=</span><span>"flex flex-col"</span><span>&gt;</span></div></div><div><div><div>39</div></div><div><span><span><span>                </span></span><span>&lt;</span></span><span>span</span><span> </span><span>class</span><span>=</span><span>"text-3xl font-bold text-emerald-600 dark:text-emerald-400"</span><span>&gt;</span><span>{</span><span>lifeCount</span><span>}</span><span>&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>40</div></div><div><span><span><span>                </span></span><span>&lt;</span></span><span>span</span><span> </span><span>class</span><span>=</span><span>"text-sm text-30 mt-1"</span><span>&gt;生活&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>41</div></div><div><span><span><span>            </span></span><span>&lt;/</span></span><span>div</span><span>&gt;</span></div></div><div><div><div>42</div></div><div><span><span><span>            </span></span><span>&lt;</span></span><span>div</span><span> </span><span>class</span><span>=</span><span>"w-px bg-neutral-200 dark:bg-neutral-700"</span><span>&gt;&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>43</div></div><div><span><span><span>            </span></span><span>&lt;</span></span><span>div</span><span> </span><span>class</span><span>=</span><span>"flex flex-col"</span><span>&gt;</span></div></div><div><div><div>44</div></div><div><span><span><span>                </span></span><span>&lt;</span></span><span>span</span><span> </span><span>class</span><span>=</span><span>"text-3xl font-bold text-(--primary)"</span><span>&gt;</span><span>{</span><span>totalCount</span><span>}</span><span>&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>45</div></div><div><span><span><span>                </span></span><span>&lt;</span></span><span>span</span><span> </span><span>class</span><span>=</span><span>"text-sm text-30 mt-1"</span><span>&gt;总计&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>46</div></div><div><span><span><span>            </span></span><span>&lt;/</span></span><span>div</span><span>&gt;</span></div></div><div><div><div>47</div></div><div><span><span><span>        </span></span><span>&lt;/</span></span><span>div</span><span>&gt;</span></div></div><div><div><div>48</div></div><div><span><span><span>    </span></span><span>&lt;/</span></span><span>div</span><span>&gt;</span></div></div><div><div><div>49</div></div><div><span><span><span>  </span></span><span>&lt;</span></span><span>ArchivePanel</span><span> </span><span>sortedPosts</span><span>=</span><span>{</span><span>archiveList</span><span>}</span><span> </span><span>client:only</span><span>=</span><span>"svelte"</span><span> /&gt;</span></div></div><div><div><div>50</div></div><div><span>&lt;/</span><span>MainGridLayout</span><span>&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>更新内容工具函数文件<a href="#更新内容工具函数文件"><span>#</span></a></h2><p>文件路径：<code>src/utils/content-utils.ts</code></p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>---</span></div></div><div><div><div>2</div></div><div><span>import</span><span> { </span><span>getCategoryUrl</span><span>, </span><span>getPostUrlBySlug</span><span> } </span><span>from</span><span> </span><span>"@utils/url-utils"</span><span>;</span></div></div><div><div><div>3</div></div><div><span>import</span><span> { </span><span>getDiaryList</span><span> } </span><span>from</span><span> </span><span>"@/data/diary"</span></div></div><div><div><div>4</div></div><div><span>---</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>export</span><span> </span><span>interface</span><span> </span><span>ArchiveItem</span><span> {</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>id</span><span>:</span><span> </span><span>string</span><span>;</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>type</span><span>:</span><span> </span><span>"post"</span><span> </span><span>|</span><span> </span><span>"moment"</span><span> </span><span>|</span><span> </span><span>"bangumi"</span><span> </span><span>|</span><span> </span><span>"life"</span><span>;</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>link</span><span><span>?</span><span>:</span></span><span> </span><span>string</span><span>;</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>data</span><span>:</span><span> {</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>title</span><span>:</span><span> </span><span>string</span><span>;</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>published</span><span>:</span><span> </span><span>Date</span><span>;</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>tags</span><span>:</span><span> </span><span>string</span><span>[];</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>category</span><span><span>?</span><span>:</span></span><span> </span><span>string</span><span> </span><span>|</span><span> </span><span>null</span><span>;</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>};</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>export</span><span> </span><span>async</span><span> </span><span>function</span><span> </span><span>getArchiveList</span><span>()</span><span>:</span><span> </span><span>Promise</span><span>&lt;</span><span>ArchiveItem</span><span>[]&gt; {</span></div></div><div><div><div>19</div></div><div><span>  </span><span>const</span><span> </span><span>posts</span><span> </span><span>=</span><span> </span><span>await</span><span> </span><span>getCollection</span><span>(</span><span>"posts"</span><span><span>, ({ </span><span>data</span><span> }) </span></span><span>=&gt;</span><span> {</span></div></div><div><div><div>20</div></div><div><span>    </span><span>return</span><span> </span><span>import</span><span>.</span><span>meta</span><span>.</span><span>env</span><span>.</span><span>PROD</span><span> </span><span>?</span><span><span> </span><span>data</span><span>.</span></span><span>draft</span><span> </span><span>!==</span><span> </span><span>true</span><span> </span><span>:</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>});</span></div></div><div><div><div>22</div></div><div><span>  </span><span>const</span><span> </span><span>diaryList</span><span> </span><span>=</span><span> </span><span>getDiaryList</span><span>();</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>  </span><span>const</span><span> </span><span>postItems</span><span>:</span><span> </span><span>ArchiveItem</span><span>[] </span><span>=</span><span><span> </span><span>posts</span><span>.</span></span><span>map</span><span><span>((</span><span>post</span><span>) </span></span><span>=&gt;</span><span> ({</span></div></div><div><div><div>25</div></div><div><span>    </span><span>id</span><span>:</span><span><span> </span><span>post</span><span>.</span></span><span>id</span><span>,</span></div></div><div><div><div>26</div></div><div><span>    </span><span>type</span><span>:</span><span> </span><span>"post"</span><span>,</span></div></div><div><div><div>27</div></div><div><span>    </span><span>link</span><span>:</span><span> </span><span>getPostUrlBySlug</span><span><span>(</span><span>post</span><span>.</span></span><span>id</span><span>),</span></div></div><div><div><div>28</div></div><div><span>    </span><span>data</span><span>:</span><span> {</span></div></div><div><div><div>29</div></div><div><span>      </span><span>title</span><span>:</span><span><span> </span><span>post</span><span>.</span></span><span>data</span><span>.</span><span>title</span><span>,</span></div></div><div><div><div>30</div></div><div><span>      </span><span>published</span><span>:</span><span><span> </span><span>post</span><span>.</span></span><span>data</span><span>.</span><span>published</span><span>,</span></div></div><div><div><div>31</div></div><div><span>      </span><span>tags</span><span>:</span><span><span> </span><span>post</span><span>.</span></span><span>data</span><span>.</span><span>tags</span><span>,</span></div></div><div><div><div>32</div></div><div><span>      </span><span>category</span><span>:</span><span><span> </span><span>post</span><span>.</span></span><span>data</span><span>.</span><span>category</span><span> </span><span>||</span><span> </span><span>null</span><span>,</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>34</div></div><div><span><span>  </span></span><span>}));</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>  </span><span>// 将日记数据转换为归档项</span></div></div><div><div><div>37</div></div><div><span>  </span><span>const</span><span> </span><span>momentItems</span><span>:</span><span> </span><span>ArchiveItem</span><span>[] </span><span>=</span><span><span> </span><span>diaryList</span><span>.</span></span><span>map</span><span><span>((</span><span>diary</span><span>) </span></span><span>=&gt;</span><span> {</span></div></div><div><div><div>38</div></div><div><span>    </span><span>let</span><span><span> </span><span>title</span><span> </span></span><span>=</span><span><span> </span><span>diary</span><span>.</span></span><span>content</span><span> </span><span>||</span><span> </span><span>""</span><span>;</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>title</span><span> </span><span>=</span><span><span> </span><span>title</span><span>.</span></span><span>replace</span><span>(</span><span>/</span><span>[#*`]</span><span>/</span><span>g</span><span>, </span><span>""</span><span>).</span><span>trim</span><span>();</span></div></div><div><div><div>40</div></div><div><span>    </span><span>if</span><span><span> (</span><span>title</span><span>.</span></span><span>length</span><span> </span><span>&gt;</span><span> </span><span>50</span><span><span>) </span><span>title</span><span> </span></span><span>=</span><span> </span><span>`</span><span>${</span><span>title</span><span>.</span><span>substring</span><span>(</span><span>0</span><span>,</span><span> </span><span>50</span><span>)</span><span>}</span><span>...`</span><span>;</span></div></div><div><div><div>41</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span><span>title</span><span>) </span><span>title</span><span> </span></span><span>=</span><span> </span><span>i18n</span><span><span>(</span><span>I18nKey</span><span>.</span></span><span>moments</span><span>) </span><span>||</span><span> </span><span>"日常动态"</span><span>;</span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span>    </span><span>return</span><span> {</span></div></div><div><div><div>44</div></div><div><span>      </span><span>id</span><span>:</span><span> </span><span>String</span><span><span>(</span><span>diary</span><span>.</span></span><span>id</span><span>),</span></div></div><div><div><div>45</div></div><div><span>      </span><span>type</span><span>:</span><span> </span><span>"moment"</span><span>,</span></div></div><div><div><div>46</div></div><div><span>      </span><span>link</span><span>:</span><span> </span><span>"/diary/"</span><span>,</span></div></div><div><div><div>47</div></div><div><span>      </span><span>data</span><span>:</span><span> {</span></div></div><div><div><div>48</div></div><div><span>        </span><span>title</span><span>:</span><span><span> </span><span>title</span><span>,</span></span></div></div><div><div><div>49</div></div><div><span>        </span><span>published</span><span>:</span><span> </span><span>new</span><span> </span><span>Date</span><span><span>(</span><span>diary</span><span>.</span></span><span>date</span><span>),</span></div></div><div><div><div>50</div></div><div><span>        </span><span>tags</span><span>:</span><span><span> </span><span>diary</span><span>.</span></span><span>tags</span><span> </span><span>||</span><span> [],</span></div></div><div><div><div>51</div></div><div><span>        </span><span>category</span><span>:</span><span> </span><span>null</span><span>,</span></div></div><div><div><div>52</div></div><div><span><span>      </span></span><span>},</span></div></div><div><div><div>53</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>54</div></div><div><span><span>  </span></span><span>});</span></div></div><div><div><div>55</div></div><div>
</div></div><div><div><div>56</div></div><div><span>  </span><span>// 空数组，用于保持接口兼容性</span></div></div><div><div><div>57</div></div><div><span>  </span><span>const</span><span> </span><span>bangumiItems</span><span>:</span><span> </span><span>ArchiveItem</span><span>[] </span><span>=</span><span> [];</span></div></div><div><div><div>58</div></div><div><span>  </span><span>const</span><span> </span><span>lifeItems</span><span>:</span><span> </span><span>ArchiveItem</span><span>[] </span><span>=</span><span> [];</span></div></div><div><div><div>59</div></div><div>
</div></div><div><div><div>60</div></div><div><span>  </span><span>return</span><span> [</span><span>...</span><span><span>postItems</span><span>, </span></span><span>...</span><span><span>momentItems</span><span>, </span></span><span>...</span><span><span>bangumiItems</span><span>, </span></span><span>...</span><span><span>lifeItems</span><span>].</span></span><span>sort</span><span><span>((</span><span>a</span><span>, </span><span>b</span><span>) </span></span><span>=&gt;</span><span> {</span></div></div><div><div><div>61</div></div><div><span>    </span><span>const</span><span> </span><span>timeA</span><span> </span><span>=</span><span><span> </span><span>a</span><span>.</span></span><span>data</span><span>.</span><span>published</span><span>.</span><span>getTime</span><span>();</span></div></div><div><div><div>62</div></div><div><span>    </span><span>const</span><span> </span><span>timeB</span><span> </span><span>=</span><span><span> </span><span>b</span><span>.</span></span><span>data</span><span>.</span><span>published</span><span>.</span><span>getTime</span><span>();</span></div></div><div><div><div>63</div></div><div><span>    </span><span>return</span><span><span> </span><span>timeB</span><span> </span></span><span>-</span><span><span> </span><span>timeA</span><span>;</span></span></div></div><div><div><div>64</div></div><div><span><span>  </span></span><span>});</span></div></div><div><div><div>65</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>定义类型<a href="#定义类型"><span>#</span></a></h2><p>文件路径：<code>src/components/controls/ArchivePanel.svelte</code></p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>interface</span><span> </span><span>Post</span><span> {</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>id</span><span>:</span><span> </span><span>string</span><span>;</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>type</span><span><span>?</span><span>:</span></span><span> </span><span>"post"</span><span> </span><span>|</span><span> </span><span>"moment"</span><span> </span><span>|</span><span> </span><span>"bangumi"</span><span> </span><span>|</span><span> </span><span>"life"</span><span>;</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>link</span><span><span>?</span><span>:</span></span><span> </span><span>string</span><span>;</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>data</span><span>:</span><span> {</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>title</span><span>:</span><span> </span><span>string</span><span>;</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>tags</span><span>:</span><span> </span><span>string</span><span>[];</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>category</span><span><span>?</span><span>:</span></span><span> </span><span>string</span><span> </span><span>|</span><span> </span><span>null</span><span>;</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>published</span><span>:</span><span> </span><span>Date</span><span>;</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>};</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>{@</span><span>const</span><span> </span><span>postLink</span><span> </span><span>=</span><span> </span><span>post</span><span>.</span><span>link</span><span> </span><span>||</span><span> </span><span>getPostUrlBySlug</span><span>(</span><span>post</span><span>.</span><span>id</span><span>)}</span></div></div><div><div><div>14</div></div><div><span>&lt;</span><span>a</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>href</span><span>=</span><span>{</span><span>getPostUrlBySlug</span><span>(post.id)}  </span><span>// [!code --]</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>href</span><span>=</span><span>{</span><span>postLink</span><span>}  </span><span>// [!code ++]</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>aria</span><span>-</span><span>label</span><span>=</span><span><span>{post.data.</span><span>title</span><span>}</span></span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>class</span><span>=</span><span>"group btn-plain block! h-10 w-full rounded-lg hover:text-[initial]"</span></div></div><div><div><div>19</div></div><div><span>&gt;</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>&lt;</span><span>div</span></div></div><div><div><div>22</div></div><div><span><span>                </span></span><span>class</span><span>=</span><span>"w-[70%] md:max-w-[65%] md:w-[65%] text-left font-bol</span><span>d</span></div></div><div><div><div>23</div></div><div><span><span>                     </span></span><span>group</span><span>-</span><span><span>hover</span><span>:</span><span>translate</span></span><span>-</span><span>x</span><span>-</span><span>1</span><span><span> </span><span>transition</span></span><span>-</span><span><span>all</span><span> </span><span>group</span></span><span>-</span><span><span>hover</span><span>:</span><span>text</span></span><span>-</span><span>(</span><span>--</span><span><span>primary</span><span>)</span></span></div></div><div><div><div>24</div></div><div><span><span>                     </span></span><span>text</span><span>-</span><span>75</span><span><span> </span><span>pr</span></span><span>-</span><span>8</span><span><span> </span><span>whitespace</span></span><span>-</span><span><span>nowrap</span><span> </span><span>text</span></span><span>-</span><span><span>ellipsis</span><span> </span><span>overflow</span></span><span>-</span><span><span>hidden</span><span> </span><span>flex</span><span> </span><span>items</span></span><span>-</span><span><span>center</span><span> </span><span>gap</span></span><span>-</span><span>2</span><span>"</span></div></div><div><div><div>25</div></div><div><span>            </span><span>&gt;</span></div></div><div><div><div>26</div></div><div><span><span><span>              </span></span><span>{#</span></span><span>if</span><span> </span><span>post</span><span>.</span><span>type</span><span> </span><span>===</span><span> </span><span>'post'</span><span>}</span></div></div><div><div><div>27</div></div><div><span>                </span><span>&lt;</span><span>span</span><span> </span><span>class</span><span>=</span><span>"px-1.5 py-0.5 text-[10px] bg-amber-500 text-white rounded-md shrink-0 uppercase tracking-wider opacity-80 font-normal"</span><span>&gt;</span></div></div><div><div><div>28</div></div><div><span><span>                  </span></span><span>文章</span></div></div><div><div><div>29</div></div><div><span>                </span><span>&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>30</div></div><div><span><span><span>              </span></span><span>{:</span></span><span>else</span><span> </span><span>if</span><span> </span><span>post</span><span>.</span><span>type</span><span> </span><span>===</span><span> </span><span>'moment'</span><span>}</span></div></div><div><div><div>31</div></div><div><span>                </span><span>&lt;</span><span>span</span><span> </span><span>class</span><span>=</span><span>"px-1.5 py-0.5 text-[10px] bg-(--primary) text-white rounded-md shrink-0 uppercase tracking-wider opacity-80 font-normal"</span><span>&gt;</span></div></div><div><div><div>32</div></div><div><span><span><span>                  </span></span><span>{</span></span><span>i18n</span><span>(</span><span>I18nKey</span><span>.</span><span>moments</span><span>) </span><span>||</span><span> </span><span>'Moment'</span><span>}</span></div></div><div><div><div>33</div></div><div><span>                </span><span>&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>34</div></div><div><span><span><span>              </span></span><span>{:</span></span><span>else</span><span> </span><span>if</span><span> </span><span>post</span><span>.</span><span>type</span><span> </span><span>===</span><span> </span><span>'bangumi'</span><span>}</span></div></div><div><div><div>35</div></div><div><span>                </span><span>&lt;</span><span>span</span><span> </span><span>class</span><span>=</span><span>"px-1.5 py-0.5 text-[10px] bg-red-500 text-white rounded-md shrink-0 uppercase tracking-wider opacity-80 font-normal"</span><span>&gt;</span></div></div><div><div><div>36</div></div><div><span><span><span>                  </span></span><span>{</span></span><span>i18n</span><span>(</span><span>I18nKey</span><span>.</span><span>bangumi</span><span>) </span><span>||</span><span> </span><span>'Record'</span><span>}</span></div></div><div><div><div>37</div></div><div><span>                </span><span>&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>38</div></div><div><span><span><span>              </span></span><span>{:</span></span><span>else</span><span> </span><span>if</span><span> </span><span>post</span><span>.</span><span>type</span><span> </span><span>===</span><span> </span><span>'life'</span><span>}</span></div></div><div><div><div>39</div></div><div><span>                </span><span>&lt;</span><span>span</span><span> </span><span>class</span><span>=</span><span>"px-1.5 py-0.5 text-[10px] bg-emerald-600 text-white rounded-md shrink-0 uppercase tracking-wider opacity-80 font-normal"</span><span>&gt;</span></div></div><div><div><div>40</div></div><div><span><span>                  </span></span><span>生活</span></div></div><div><div><div>41</div></div><div><span>                </span><span>&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>42</div></div><div><span><span><span>              </span></span><span>{</span></span><span>/</span><span>if</span><span>}</span></div></div><div><div><div>43</div></div><div><span>              </span><span>&lt;</span><span>span</span><span> </span><span>class</span><span>=</span><span>"truncate"</span><span>&gt;</span><span>{post.data.</span><span>title</span><span>}</span><span>&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>44</div></div><div><span>            </span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>语言配置<a href="#语言配置"><span>#</span></a></h2><ul>
<li>文件路径：<code>src/i18n/i18nKey.ts</code></li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>moments</span><span> </span></span><span>=</span><span> </span><span>"moments"</span><span>,</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>文件路径：<code>src/i18n/languages/en.ts</code></li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>[</span><span>Key</span><span>.</span></span><span>moments</span><span>]: </span><span>"Dynamic"</span><span>,</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>文件路径：<code>src/i18n/languages/ja.ts</code></li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>[</span><span>Key</span><span>.</span></span><span>moments</span><span>]: </span><span>"ダイナミック"</span><span>,</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>文件路径：<code>src/i18n/languages/ru.ts</code></li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>[</span><span>Key</span><span>.</span></span><span>moments</span><span>]: </span><span>"Динамика"</span><span>,</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>文件路径：<code>src/i18n/languages/zh_CN.ts</code></li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>[</span><span>Key</span><span>.</span></span><span>moments</span><span>]: </span><span>"动态"</span><span>,</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>文件路径：<code>src/i18n/languages/zh_TW.ts</code></li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>[</span><span>Key</span><span>.</span></span><span>moments</span><span>]: </span><span>"動態"</span><span>,</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>]]></content>
    </entry>
    <entry>
      <id>https://seasir.top/posts/categories/</id>
      <title type="text">给博客增加单个和全部分类页面</title>
      <published>2026-06-02T00:00:00.000Z</published>
      <updated>2026-06-02T00:00:00.000Z</updated>
      <author><name>Hyde</name></author>
      <link rel="alternate" href="https://seasir.top/posts/categories/"/>
      <summary type="text">最近给博客加了个小功能：点击分类名就能跳转到该分类下的所有文章，还能一键查看全部分类列表——整个过程其实挺顺的，从创建页面、写 URL 工具函数，到更新导航栏和多语言支持，一步步配下来，连分类链接都自动带上了本地化路径，现在逛自己博客像逛图书馆一样清爽 😄</summary>
      <content type="html"><![CDATA[<section><h2>文件目录<a href="#文件目录"><span>#</span></a></h2><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>src/</span></div></div><div><div><div>2</div></div><div><span>├── components/</span></div></div><div><div><div>3</div></div><div><span>│   └── layout/</span></div></div><div><div><div>4</div></div><div><span>│       ├── CategoryBar.astro          </span><span>[</span><span>分类栏组件：显示主页、归档、分类按钮，支持横向滚动</span><span>]</span></div></div><div><div><div>5</div></div><div><span>├── config/</span></div></div><div><div><div>6</div></div><div><span>│   └── navBarConfig.ts                </span><span>[</span><span>导航栏配置：动态生成导航链接，添加分类菜单项</span><span>]</span></div></div><div><div><div>7</div></div><div><span>├── i18n/</span></div></div><div><div><div>8</div></div><div><span>│   ├── i18nKey.ts                     [国际化键定义：新增 allCategories 和 searchAlbums 键]</span></div></div><div><div><div>9</div></div><div><span>│   └── languages/</span></div></div><div><div><div>10</div></div><div><span>│       ├── en.ts                      </span><span>[</span><span>英文翻译文件</span><span>]</span></div></div><div><div><div>11</div></div><div><span>│       ├── ja.ts                      </span><span>[</span><span>日文翻译文件</span><span>]</span></div></div><div><div><div>12</div></div><div><span>│       ├── ru.ts                      </span><span>[</span><span>俄文翻译文件</span><span>]</span></div></div><div><div><div>13</div></div><div><span>│       ├── zh_CN.ts                   </span><span>[</span><span>简体中文翻译文件</span><span>]</span></div></div><div><div><div>14</div></div><div><span>│       └── zh_TW.ts                   </span><span>[</span><span>繁体中文翻译文件</span><span>]</span></div></div><div><div><div>15</div></div><div><span>├── pages/</span></div></div><div><div><div>16</div></div><div><span>│   ├── categories.astro               </span><span>[</span><span>全部分类页面：显示所有分类卡片列表（新增）</span><span>]</span></div></div><div><div><div>17</div></div><div><span>│   └── categories/</span></div></div><div><div><div>18</div></div><div><span>│       └── </span><span>[</span><span>category</span><span>]</span><span>.astro           </span><span>[</span><span>单个分类详情页：显示该分类文章和标签筛选（新增）</span><span>]</span></div></div><div><div><div>19</div></div><div><span>└── utils/</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>└── url-utils.ts                   </span><span>[</span><span>URL工具函数：新增分类相关URL生成函数</span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>创建全部分类页面<a href="#创建全部分类页面"><span>#</span></a></h2><p>文件路径：<code>src/pages/categories.astro</code></p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>---</span></div></div><div><div><div>2</div></div><div><span>import</span><span> { </span><span>Icon</span><span> } </span><span>from</span><span> </span><span>"astro-icon/components"</span><span>;</span></div></div><div><div><div>3</div></div><div><span>import</span><span> </span><span>MainGridLayout</span><span> </span><span>from</span><span> </span><span>"@layouts/MainGridLayout.astro"</span><span>;</span></div></div><div><div><div>4</div></div><div><span>import</span><span> { </span><span>getCategoryList</span><span> } </span><span>from</span><span> </span><span>"@/utils/content-utils"</span><span>;</span></div></div><div><div><div>5</div></div><div><span>import</span><span> </span><span>I18nKey</span><span> </span><span>from</span><span> </span><span>"@i18n/i18nKey"</span><span>;</span></div></div><div><div><div>6</div></div><div><span>import</span><span> { </span><span>i18n</span><span> } </span><span>from</span><span> </span><span>"@i18n/translation"</span><span>;</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>const</span><span> </span><span>categories</span><span> </span><span>=</span><span> </span><span>await</span><span> </span><span>getCategoryList</span><span>();</span></div></div><div><div><div>9</div></div><div><span>const</span><span> </span><span>totalPosts</span><span> </span><span>=</span><span><span> </span><span>categories</span><span>.</span></span><span>reduce</span><span><span>((</span><span>sum</span><span>, </span><span>cat</span><span>) </span></span><span>=&gt;</span><span><span> </span><span>sum</span><span> </span></span><span>+</span><span><span> </span><span>cat</span><span>.</span></span><span>count</span><span>, </span><span>0</span><span>);</span></div></div><div><div><div>10</div></div><div><span>---</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>&lt;</span><span>MainGridLayout</span><span> </span><span>title</span><span>=</span><span>{</span><span>i18n</span><span>(</span><span>I18nKey</span><span>.</span><span>categories</span><span>)</span><span>}</span><span>&gt;</span></div></div><div><div><div>13</div></div><div><span>    </span><span>&lt;!-- 页面标题 --&gt;</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"card-base px-8 py-6 mb-6"</span><span>&gt;</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>&lt;</span><span>h1</span><span> </span><span>class</span><span>=</span><span>"text-2xl font-bold text-(--primary) mb-2"</span><span>&gt;</span></div></div><div><div><div>16</div></div><div><span>            </span><span>{</span><span>i18n</span><span>(</span><span>I18nKey</span><span>.</span><span>categories</span><span>)</span><span>}</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>&lt;/</span><span>h1</span><span>&gt;</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>&lt;</span><span>p</span><span> </span><span>class</span><span>=</span><span>"text-30 text-sm"</span><span>&gt;</span></div></div><div><div><div>19</div></div><div><span>            </span><span>{</span><span>i18n</span><span>(</span><span>I18nKey</span><span>.</span><span>allCategories</span><span>)</span><span>}</span><span> · </span><span>{</span><span>totalPosts</span><span>}</span><span> </span><span>{</span><span>i18n</span><span>(</span><span>I18nKey</span><span>.</span><span>postsCount</span><span>)</span><span>}</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>&lt;/</span><span>p</span><span>&gt;</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>    </span><span>&lt;!-- 分类卡片列表 --&gt;</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6"</span><span>&gt;</span></div></div><div><div><div>25</div></div><div><span>        </span><span>{</span></div></div><div><div><div>26</div></div><div><span>            </span><span>categories</span><span>.</span><span>map</span><span><span>((</span><span>category</span><span>, </span><span>index</span><span>) </span></span><span>=&gt;</span><span> (</span></div></div><div><div><div>27</div></div><div><span><span>                </span></span><span>&lt;</span><span>a</span></div></div><div><div><div>28</div></div><div><span>                    </span><span>href</span><span><span>=</span><span>{</span></span><span>category</span><span>.</span><span>url</span><span>}</span></div></div><div><div><div>29</div></div><div><span>                    </span><span>class</span><span>=</span><span>"card-base p-6 flex items-center gap-4 transition-all duration-300</span></div></div><div><div><div>30</div></div><div><span><span>                           </span></span><span>hover:scale-[1.02] hover:shadow-lg active:scale-[0.98]</span></div></div><div><div><div>31</div></div><div><span><span>                           </span></span><span>onload-animation"</span></div></div><div><div><div>32</div></div><div><span>                    </span><span>style</span><span><span>=</span><span>{</span></span><span><span>`animation-delay: calc(var(--content-delay) + </span><span>${</span></span><span>index</span><span> </span><span>*</span><span> </span><span>50</span><span><span>}</span><span>ms);`</span></span><span>}</span></div></div><div><div><div>33</div></div><div><span><span>                </span></span><span>&gt;</span></div></div><div><div><div>34</div></div><div><span><span>                    </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"flex-shrink-0 w-12 h-12 rounded-full bg-(--primary)/10 flex items-center justify-center"</span><span>&gt;</span></div></div><div><div><div>35</div></div><div><span><span>                        </span></span><span>&lt;</span><span>Icon</span></div></div><div><div><div>36</div></div><div><span>                            </span><span>name</span><span>=</span><span>"material-symbols:folder-open"</span></div></div><div><div><div>37</div></div><div><span>                            </span><span>class</span><span>=</span><span>"text-2xl text-(--primary)"</span></div></div><div><div><div>38</div></div><div><span><span>                        </span></span><span>/&gt;</span></div></div><div><div><div>39</div></div><div><span><span>                    </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>40</div></div><div><span><span>                    </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"flex-1 min-w-0"</span><span>&gt;</span></div></div><div><div><div>41</div></div><div><span><span>                        </span></span><span>&lt;</span><span>h2</span><span> </span><span>class</span><span>=</span><span>"text-lg font-bold text-90 truncate"</span><span>&gt;</span></div></div><div><div><div>42</div></div><div><span><span>                            </span></span><span>{</span><span>category</span><span>.</span><span>name</span><span>}</span></div></div><div><div><div>43</div></div><div><span><span>                        </span></span><span>&lt;/</span><span>h2</span><span>&gt;</span></div></div><div><div><div>44</div></div><div><span><span>                        </span></span><span>&lt;</span><span>p</span><span> </span><span>class</span><span>=</span><span>"text-sm text-30 mt-1"</span><span>&gt;</span></div></div><div><div><div>45</div></div><div><span><span>                            </span></span><span>{</span><span>category</span><span>.</span><span>count</span><span><span>}</span><span> </span><span>{</span></span><span>i18n</span><span>(</span><span>I18nKey</span><span>.</span><span>postsCount</span><span><span>)</span><span>}</span></span></div></div><div><div><div>46</div></div><div><span><span>                        </span></span><span>&lt;/</span><span>p</span><span>&gt;</span></div></div><div><div><div>47</div></div><div><span><span>                    </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>48</div></div><div><span><span>                    </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"flex-shrink-0"</span><span>&gt;</span></div></div><div><div><div>49</div></div><div><span><span>                        </span></span><span>&lt;</span><span>Icon</span></div></div><div><div><div>50</div></div><div><span>                            </span><span>name</span><span>=</span><span>"material-symbols:chevron-right-rounded"</span></div></div><div><div><div>51</div></div><div><span>                            </span><span>class</span><span>=</span><span>"text-2xl text-30 transition-transform duration-200 group-hover:translate-x-1"</span></div></div><div><div><div>52</div></div><div><span><span>                        </span></span><span>/&gt;</span></div></div><div><div><div>53</div></div><div><span><span>                    </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>54</div></div><div><span><span>                </span></span><span>&lt;/</span><span>a</span><span>&gt;</span></div></div><div><div><div>55</div></div><div><span><span>            </span></span><span>))</span></div></div><div><div><div>56</div></div><div><span>        </span><span>}</span></div></div><div><div><div>57</div></div><div><span><span>    </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>58</div></div><div>
</div></div><div><div><div>59</div></div><div><span>    </span><span>&lt;!-- 空状态 --&gt;</span></div></div><div><div><div>60</div></div><div><span>    </span><span>{</span></div></div><div><div><div>61</div></div><div><span>        </span><span>categories</span><span>.</span><span>length</span><span> </span><span>===</span><span> </span><span>0</span><span> </span><span>&amp;&amp;</span><span> (</span></div></div><div><div><div>62</div></div><div><span><span>            </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"card-base px-8 py-12 text-center"</span><span>&gt;</span></div></div><div><div><div>63</div></div><div><span><span>                </span></span><span>&lt;</span><span>Icon</span></div></div><div><div><div>64</div></div><div><span>                    </span><span>name</span><span>=</span><span>"material-symbols:folder-off"</span></div></div><div><div><div>65</div></div><div><span>                    </span><span>class</span><span>=</span><span>"text-5xl text-30 mx-auto mb-4"</span></div></div><div><div><div>66</div></div><div><span><span>                </span></span><span>/&gt;</span></div></div><div><div><div>67</div></div><div><span><span>                </span></span><span>&lt;</span><span>p</span><span> </span><span>class</span><span>=</span><span>"text-30"</span><span><span>&gt;</span><span>{</span></span><span>i18n</span><span>(</span><span>I18nKey</span><span>.</span><span>bangumiNoData</span><span><span>)</span><span>}</span><span>&lt;/</span></span><span>p</span><span>&gt;</span></div></div><div><div><div>68</div></div><div><span><span>            </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>69</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>70</div></div><div><span>    </span><span>}</span></div></div><div><div><div>71</div></div><div><span>&lt;/</span><span>MainGridLayout</span><span>&gt;</span></div></div><div><div><div>72</div></div><div>
</div></div><div><div><div>73</div></div><div><span>&lt;</span><span>style</span><span>&gt;</span></div></div><div><div><div>74</div></div><div><span>    </span><span>.card-base</span><span> {</span></div></div><div><div><div>75</div></div><div><span><span>        </span></span><span>background: </span><span>var</span><span>(</span><span>--card-bg</span><span>);</span></div></div><div><div><div>76</div></div><div><span><span>        </span></span><span>border: </span><span><span>1</span><span>px</span></span><span><span> </span><span>solid</span><span> </span></span><span>var</span><span>(</span><span>--card-border</span><span>);</span></div></div><div><div><div>77</div></div><div><span><span>        </span></span><span>border-radius: </span><span>var</span><span>(</span><span>--radius-large</span><span>);</span></div></div><div><div><div>78</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>79</div></div><div>
</div></div><div><div><div>80</div></div><div><span>    </span><span>a</span><span><span>.card-base</span><span>:hover</span></span><span> {</span></div></div><div><div><div>81</div></div><div><span><span>        </span></span><span>border-color: </span><span>var</span><span>(</span><span>--primary</span><span>);</span></div></div><div><div><div>82</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>83</div></div><div>
</div></div><div><div><div>84</div></div><div><span>    </span><span>a</span><span><span>.card-base</span><span>:active</span></span><span> {</span></div></div><div><div><div>85</div></div><div><span><span>        </span></span><span>background: </span><span>var</span><span>(</span><span>--card-bg-active</span><span>);</span></div></div><div><div><div>86</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>87</div></div><div><span>&lt;/</span><span>style</span><span>&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>创建单个分类详情页<a href="#创建单个分类详情页"><span>#</span></a></h2><p>文件路径：<code>src/pages/categories/[category].astro</code></p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>---</span></div></div><div><div><div>2</div></div><div><span>import</span><span> </span><span>type</span><span> { </span><span>GetStaticPaths</span><span> } </span><span>from</span><span> </span><span>"astro"</span><span>;</span></div></div><div><div><div>3</div></div><div><span>import</span><span> </span><span>type</span><span> { </span><span>CollectionEntry</span><span> } </span><span>from</span><span> </span><span>"astro:content"</span><span>;</span></div></div><div><div><div>4</div></div><div><span>import</span><span> { </span><span>getCollection</span><span> } </span><span>from</span><span> </span><span>"astro:content"</span><span>;</span></div></div><div><div><div>5</div></div><div><span>import</span><span> { </span><span>Icon</span><span> } </span><span>from</span><span> </span><span>"astro-icon/components"</span><span>;</span></div></div><div><div><div>6</div></div><div><span>import</span><span> </span><span>PostCard</span><span> </span><span>from</span><span> </span><span>"@/components/layout/PostCard.astro"</span><span>;</span></div></div><div><div><div>7</div></div><div><span>import</span><span> </span><span>MainGridLayout</span><span> </span><span>from</span><span> </span><span>"@layouts/MainGridLayout.astro"</span><span>;</span></div></div><div><div><div>8</div></div><div><span>import</span><span> { </span><span>getCategoryList</span><span> } </span><span>from</span><span> </span><span>"@/utils/content-utils"</span><span>;</span></div></div><div><div><div>9</div></div><div><span>import</span><span> { </span><span>getTagUrl</span><span>, </span><span>getCategoryPageUrl</span><span>, </span><span>getPostUrlBySlug</span><span> } </span><span>from</span><span> </span><span>"@/utils/url-utils"</span><span>;</span></div></div><div><div><div>10</div></div><div><span>import</span><span> </span><span>I18nKey</span><span> </span><span>from</span><span> </span><span>"@i18n/i18nKey"</span><span>;</span></div></div><div><div><div>11</div></div><div><span>import</span><span> { </span><span>i18n</span><span> } </span><span>from</span><span> </span><span>"@i18n/translation"</span><span>;</span></div></div><div><div><div>12</div></div><div><span>import</span><span> { </span><span>siteConfig</span><span> } </span><span>from</span><span> </span><span>"@/config"</span><span>;</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>type</span><span> </span><span>PostEntry</span><span> </span><span>=</span><span> </span><span>CollectionEntry</span><span>&lt;</span><span>"posts"</span><span>&gt;;</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>interface</span><span> </span><span>Props</span><span> {</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>categoryName</span><span>:</span><span> </span><span>string</span><span>;</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>posts</span><span>:</span><span> </span><span>PostEntry</span><span>[];</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>totalPosts</span><span>:</span><span> </span><span>number</span><span>;</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>tags</span><span>:</span><span><span> { </span><span>name</span></span><span>:</span><span> </span><span>string</span><span><span>; </span><span>count</span></span><span>:</span><span> </span><span>number</span><span> }[];</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>export</span><span> </span><span>const</span><span> </span><span>getStaticPaths</span><span> </span><span>=</span><span> (</span><span>async</span><span> () </span><span>=&gt;</span><span> {</span></div></div><div><div><div>24</div></div><div><span>  </span><span>const</span><span> </span><span>categories</span><span> </span><span>=</span><span> </span><span>await</span><span> </span><span>getCategoryList</span><span>();</span></div></div><div><div><div>25</div></div><div><span>  </span><span>const</span><span> </span><span>allPosts</span><span> </span><span>=</span><span> </span><span>await</span><span> </span><span>getCollection</span><span>(</span><span>"posts"</span><span><span>, ({ </span><span>data</span><span> }) </span></span><span>=&gt;</span><span> {</span></div></div><div><div><div>26</div></div><div><span>    </span><span>return</span><span> </span><span>import</span><span>.</span><span>meta</span><span>.</span><span>env</span><span>.</span><span>PROD</span><span> </span><span>?</span><span><span> </span><span>data</span><span>.</span></span><span>draft</span><span> </span><span>!==</span><span> </span><span>true</span><span> </span><span>:</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>}) </span><span>as</span><span> </span><span>PostEntry</span><span>[];</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>  </span><span>const</span><span> </span><span>paths</span><span>:</span><span> </span><span>any</span><span>[] </span><span>=</span><span> [];</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>  </span><span>for</span><span> (</span><span>const</span><span> </span><span>category</span><span> </span><span>of</span><span><span> </span><span>categories</span><span>) {</span></span></div></div><div><div><div>32</div></div><div><span>    </span><span>// 过滤该分类下的文章</span></div></div><div><div><div>33</div></div><div><span>    </span><span>const</span><span> </span><span>categoryPosts</span><span> </span><span>=</span><span><span> </span><span>allPosts</span><span>.</span></span><span>filter</span><span><span>((</span><span>post</span><span>) </span></span><span>=&gt;</span><span> {</span></div></div><div><div><div>34</div></div><div><span>      </span><span>const</span><span> </span><span>postCategory</span><span> </span><span>=</span><span><span> </span><span>post</span><span>.</span></span><span>data</span><span>.</span><span>category</span><span>?.</span><span>trim</span><span>() </span><span>||</span><span> </span><span>""</span><span>;</span></div></div><div><div><div>35</div></div><div><span>      </span><span>const</span><span> </span><span>uncategorized</span><span> </span><span>=</span><span> </span><span>i18n</span><span><span>(</span><span>I18nKey</span><span>.</span></span><span>uncategorized</span><span>).</span><span>toLowerCase</span><span>();</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>      </span><span>if</span><span> (</span><span>!</span><span><span>category</span><span>.</span></span><span>name</span><span> </span><span>||</span><span><span> </span><span>category</span><span>.</span></span><span>name</span><span>.</span><span>toLowerCase</span><span>() </span><span>===</span><span><span> </span><span>uncategorized</span><span>) {</span></span></div></div><div><div><div>38</div></div><div><span>        </span><span>// 未分类：没有分类或分类为空/未分类</span></div></div><div><div><div>39</div></div><div><span>        </span><span>return</span><span> </span><span>!</span><span><span>postCategory</span><span> </span></span><span>||</span><span><span> </span><span>postCategory</span><span>.</span></span><span>toLowerCase</span><span>() </span><span>===</span><span><span> </span><span>uncategorized</span><span>;</span></span></div></div><div><div><div>40</div></div><div><span><span>      </span></span><span>}</span></div></div><div><div><div>41</div></div><div><span>      </span><span>return</span><span><span> </span><span>postCategory</span><span> </span></span><span>===</span><span><span> </span><span>category</span><span>.</span></span><span>name</span><span>;</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>});</span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div><span>    </span><span>// 按日期排序</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>categoryPosts</span><span>.</span><span>sort</span><span><span>((</span><span>a</span><span>, </span><span>b</span><span>) </span></span><span>=&gt;</span><span> {</span></div></div><div><div><div>46</div></div><div><span>      </span><span>if</span><span><span> (</span><span>a</span><span>.</span></span><span>data</span><span>.</span><span>pinned</span><span> </span><span>&amp;&amp;</span><span> </span><span>!</span><span><span>b</span><span>.</span></span><span>data</span><span>.</span><span>pinned</span><span>) </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>47</div></div><div><span>      </span><span>if</span><span> (</span><span>!</span><span><span>a</span><span>.</span></span><span>data</span><span>.</span><span>pinned</span><span> </span><span>&amp;&amp;</span><span><span> </span><span>b</span><span>.</span></span><span>data</span><span>.</span><span>pinned</span><span>) </span><span>return</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>48</div></div><div><span>      </span><span>return</span><span> </span><span>new</span><span> </span><span>Date</span><span><span>(</span><span>b</span><span>.</span></span><span>data</span><span>.</span><span>published</span><span>).</span><span>getTime</span><span>() </span><span>-</span><span> </span><span>new</span><span> </span><span>Date</span><span><span>(</span><span>a</span><span>.</span></span><span>data</span><span>.</span><span>published</span><span>).</span><span>getTime</span><span>();</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>});</span></div></div><div><div><div>50</div></div><div>
</div></div><div><div><div>51</div></div><div><span>    </span><span>// 收集该分类下所有标签</span></div></div><div><div><div>52</div></div><div><span>    </span><span>const</span><span> </span><span>tagCountMap</span><span>:</span><span> </span><span>Record</span><span><span>&lt;</span><span>string</span><span>, </span><span>number</span><span>&gt; </span></span><span>=</span><span> {};</span></div></div><div><div><div>53</div></div><div><span><span>    </span></span><span>categoryPosts</span><span>.</span><span>forEach</span><span><span>((</span><span>post</span><span>) </span></span><span>=&gt;</span><span> {</span></div></div><div><div><div>54</div></div><div><span><span>      </span></span><span>(</span><span>post</span><span>.</span><span>data</span><span>.</span><span>tags</span><span> </span><span>||</span><span> []).</span><span>forEach</span><span><span>((</span><span>tag</span></span><span>:</span><span> </span><span>string</span><span>) </span><span>=&gt;</span><span> {</span></div></div><div><div><div>55</div></div><div><span><span>        </span></span><span>tagCountMap</span><span>[</span><span>tag</span><span>] </span><span>=</span><span><span> (</span><span>tagCountMap</span><span>[</span><span>tag</span><span>] </span></span><span>||</span><span> </span><span>0</span><span>) </span><span>+</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>56</div></div><div><span><span>      </span></span><span>});</span></div></div><div><div><div>57</div></div><div><span><span>    </span></span><span>});</span></div></div><div><div><div>58</div></div><div><span>    </span><span>const</span><span> </span><span>tags</span><span> </span><span>=</span><span><span> </span><span>Object</span><span>.</span></span><span>entries</span><span><span>(</span><span>tagCountMap</span><span>)</span></span></div></div><div><div><div>59</div></div><div><span><span>      </span></span><span>.</span><span>sort</span><span><span>((</span><span>a</span><span>, </span><span>b</span><span>) </span></span><span>=&gt;</span><span><span> </span><span>b</span><span>[</span></span><span>1</span><span>] </span><span>-</span><span><span> </span><span>a</span><span>[</span></span><span>1</span><span>])</span></div></div><div><div><div>60</div></div><div><span><span>      </span></span><span>.</span><span>map</span><span><span>(([</span><span>name</span><span>, </span><span>count</span><span>]) </span></span><span>=&gt;</span><span><span> ({ </span><span>name</span><span>, </span><span>count</span><span> }));</span></span></div></div><div><div><div>61</div></div><div>
</div></div><div><div><div>62</div></div><div><span><span>    </span></span><span>paths</span><span>.</span><span>push</span><span>({</span></div></div><div><div><div>63</div></div><div><span>      </span><span>params</span><span>:</span><span> { </span><span>category</span><span>:</span><span><span> </span><span>category</span><span>.</span></span><span>name</span><span> },</span></div></div><div><div><div>64</div></div><div><span>      </span><span>props</span><span>:</span><span> {</span></div></div><div><div><div>65</div></div><div><span>        </span><span>categoryName</span><span>:</span><span><span> </span><span>category</span><span>.</span></span><span>name</span><span>,</span></div></div><div><div><div>66</div></div><div><span>        </span><span>posts</span><span>:</span><span><span> </span><span>categoryPosts</span><span>,</span></span></div></div><div><div><div>67</div></div><div><span>        </span><span>totalPosts</span><span>:</span><span><span> </span><span>categoryPosts</span><span>.</span></span><span>length</span><span>,</span></div></div><div><div><div>68</div></div><div><span><span>        </span></span><span>tags</span><span>,</span></div></div><div><div><div>69</div></div><div><span><span>      </span></span><span>},</span></div></div><div><div><div>70</div></div><div><span><span>    </span></span><span>});</span></div></div><div><div><div>71</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>72</div></div><div>
</div></div><div><div><div>73</div></div><div><span>  </span><span>return</span><span><span> </span><span>paths</span><span>;</span></span></div></div><div><div><div>74</div></div><div><span>}) </span><span>satisfies</span><span> </span><span>GetStaticPaths</span><span>;</span></div></div><div><div><div>75</div></div><div>
</div></div><div><div><div>76</div></div><div><span>const</span><span> { </span><span>categoryName</span><span>, </span><span>posts</span><span>, </span><span>totalPosts</span><span>, </span><span>tags</span><span> } </span><span>=</span><span><span> </span><span>Astro</span><span>.</span></span><span>props</span><span>;</span></div></div><div><div><div>77</div></div><div>
</div></div><div><div><div>78</div></div><div><span>// 解码分类名称</span></div></div><div><div><div>79</div></div><div><span>const</span><span> </span><span>decodedCategory</span><span> </span><span>=</span><span> </span><span>decodeURIComponent</span><span><span>(</span><span>categoryName</span><span>);</span></span></div></div><div><div><div>80</div></div><div>
</div></div><div><div><div>81</div></div><div><span>let</span><span><span> </span><span>delay</span><span> </span></span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>82</div></div><div><span>const</span><span> </span><span>interval</span><span> </span><span>=</span><span> </span><span>50</span><span>;</span></div></div><div><div><div>83</div></div><div>
</div></div><div><div><div>84</div></div><div><span>const</span><span> </span><span>masonryEnabled</span><span> </span><span>=</span><span><span> </span><span>siteConfig</span><span>.</span></span><span>postListLayout</span><span>.</span><span>grid</span><span>.</span><span>masonry</span><span>;</span></div></div><div><div><div>85</div></div><div><span>const</span><span> </span><span>columnWidth</span><span> </span><span>=</span><span><span> </span><span>siteConfig</span><span>.</span></span><span>postListLayout</span><span>.</span><span>grid</span><span>.</span><span>columnWidth</span><span> </span><span>||</span><span> </span><span>280</span><span>;</span></div></div><div><div><div>86</div></div><div>
</div></div><div><div><div>87</div></div><div><span>const</span><span> </span><span>defaultLayout</span><span> </span><span>=</span><span><span> </span><span>siteConfig</span><span>.</span></span><span>postListLayout</span><span>.</span><span>defaultMode</span><span> </span><span>||</span><span> </span><span>"list"</span><span>;</span></div></div><div><div><div>88</div></div><div><span>const</span><span> </span><span>mobileDefaultLayout</span><span> </span><span>=</span></div></div><div><div><div>89</div></div><div><span><span>  </span></span><span>siteConfig</span><span>.</span><span>postListLayout</span><span>.</span><span>mobileDefaultMode</span><span> </span><span>||</span><span><span> </span><span>defaultLayout</span><span>;</span></span></div></div><div><div><div>90</div></div><div><span>const</span><span> </span><span>initialLayoutClass</span><span> </span><span>=</span></div></div><div><div><div>91</div></div><div><span><span>  </span></span><span>defaultLayout</span><span> </span><span>===</span><span> </span><span>"grid"</span></div></div><div><div><div>92</div></div><div><span>    </span><span>?</span><span> </span><span>"post-grid-auto grid-mode"</span></div></div><div><div><div>93</div></div><div><span>    </span><span>:</span><span> </span><span>"flex flex-col gap-4 md:gap-4 list-mode"</span><span>;</span></div></div><div><div><div>94</div></div><div><span>---</span></div></div><div><div><div>95</div></div><div>
</div></div><div><div><div>96</div></div><div><span>&lt;</span><span>MainGridLayout</span><span> </span><span>title</span><span>=</span><span>{</span><span><span>`</span><span>${</span></span><span>decodedCategory</span><span><span>}</span><span> - </span><span>${</span></span><span>i18n</span><span>(</span><span>I18nKey</span><span>.</span><span>categories</span><span>)</span><span><span>}</span><span>`</span></span><span>}</span><span>&gt;</span></div></div><div><div><div>97</div></div><div><span>    </span><span>&lt;!-- 面包屑导航 --&gt;</span></div></div><div><div><div>98</div></div><div><span><span>    </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"card-base px-6 py-4 mb-6"</span><span>&gt;</span></div></div><div><div><div>99</div></div><div><span><span>        </span></span><span>&lt;</span><span>nav</span><span> </span><span>class</span><span>=</span><span>"flex items-center gap-2 text-sm text-30"</span><span>&gt;</span></div></div><div><div><div>100</div></div><div><span><span>            </span></span><span>&lt;</span><span>a</span><span> </span><span>href</span><span>=</span><span>"/"</span><span> </span><span>class</span><span>=</span><span>"hover:text-(--primary) transition-colors"</span><span>&gt;</span></div></div><div><div><div>101</div></div><div><span><span>                </span></span><span>&lt;</span><span>Icon</span><span> </span><span>name</span><span>=</span><span>"material-symbols:home"</span><span> </span><span>class</span><span>=</span><span>"inline-block align-middle"</span><span> /&gt;</span></div></div><div><div><div>102</div></div><div><span><span>            </span></span><span>&lt;/</span><span>a</span><span>&gt;</span></div></div><div><div><div>103</div></div><div><span><span>            </span></span><span>&lt;</span><span>span</span><span> </span><span>class</span><span>=</span><span>"opacity-50"</span><span>&gt;/&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>104</div></div><div><span><span>            </span></span><span>&lt;</span><span>a</span><span> </span><span>href</span><span>=</span><span>{</span><span>getCategoryPageUrl</span><span>()</span><span>}</span><span> </span><span>class</span><span>=</span><span>"hover:text-(--primary) transition-colors"</span><span>&gt;</span></div></div><div><div><div>105</div></div><div><span>                </span><span>{</span><span>i18n</span><span>(</span><span>I18nKey</span><span>.</span><span>categories</span><span>)</span><span>}</span></div></div><div><div><div>106</div></div><div><span><span>            </span></span><span>&lt;/</span><span>a</span><span>&gt;</span></div></div><div><div><div>107</div></div><div><span><span>            </span></span><span>&lt;</span><span>span</span><span> </span><span>class</span><span>=</span><span>"opacity-50"</span><span>&gt;/&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>108</div></div><div><span><span>            </span></span><span>&lt;</span><span>span</span><span> </span><span>class</span><span>=</span><span>"text-90 font-medium"</span><span>&gt;</span><span>{</span><span>decodedCategory</span><span>}</span><span>&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>109</div></div><div><span><span>        </span></span><span>&lt;/</span><span>nav</span><span>&gt;</span></div></div><div><div><div>110</div></div><div><span><span>    </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>111</div></div><div>
</div></div><div><div><div>112</div></div><div><span>    </span><span>&lt;!-- 分类标题 --&gt;</span></div></div><div><div><div>113</div></div><div><span><span>    </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"card-base px-8 py-6 mb-6"</span><span>&gt;</span></div></div><div><div><div>114</div></div><div><span><span>        </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"flex items-center gap-4"</span><span>&gt;</span></div></div><div><div><div>115</div></div><div><span><span>            </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"w-14 h-14 rounded-full bg-(--primary)/10 flex items-center justify-center flex-shrink-0"</span><span>&gt;</span></div></div><div><div><div>116</div></div><div><span><span>                </span></span><span>&lt;</span><span>Icon</span><span> </span><span>name</span><span>=</span><span>"material-symbols:folder-open"</span><span> </span><span>class</span><span>=</span><span>"text-3xl text-(--primary)"</span><span> /&gt;</span></div></div><div><div><div>117</div></div><div><span><span>            </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>118</div></div><div><span><span>            </span></span><span>&lt;</span><span>div</span><span>&gt;</span></div></div><div><div><div>119</div></div><div><span><span>                </span></span><span>&lt;</span><span>h1</span><span> </span><span>class</span><span>=</span><span>"text-2xl font-bold text-90"</span><span>&gt;</span><span>{</span><span>decodedCategory</span><span>}</span><span>&lt;/</span><span>h1</span><span>&gt;</span></div></div><div><div><div>120</div></div><div><span><span>                </span></span><span>&lt;</span><span>p</span><span> </span><span>class</span><span>=</span><span>"text-sm text-30 mt-1"</span><span>&gt;</span></div></div><div><div><div>121</div></div><div><span>                    </span><span>{</span><span>totalPosts</span><span>}</span><span> </span><span>{</span><span>i18n</span><span>(</span><span>I18nKey</span><span>.</span><span>postsCount</span><span>)</span><span>}</span></div></div><div><div><div>122</div></div><div><span><span>                </span></span><span>&lt;/</span><span>p</span><span>&gt;</span></div></div><div><div><div>123</div></div><div><span><span>            </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>124</div></div><div><span><span>        </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>125</div></div><div><span><span>    </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>126</div></div><div>
</div></div><div><div><div>127</div></div><div><span>    </span><span>&lt;!-- 标签筛选区域 --&gt;</span></div></div><div><div><div>128</div></div><div><span>    </span><span>{</span></div></div><div><div><div>129</div></div><div><span>        </span><span>tags</span><span>.</span><span>length</span><span> </span><span>&gt;</span><span> </span><span>0</span><span> </span><span>&amp;&amp;</span><span> (</span></div></div><div><div><div>130</div></div><div><span><span>            </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"card-base px-6 py-5 mb-6"</span><span>&gt;</span></div></div><div><div><div>131</div></div><div><span><span>                </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"flex items-center gap-3 mb-3"</span><span>&gt;</span></div></div><div><div><div>132</div></div><div><span><span>                    </span></span><span>&lt;</span><span>Icon</span><span> </span><span>name</span><span>=</span><span>"material-symbols:label"</span><span> </span><span>class</span><span>=</span><span>"text-lg text-(--primary)"</span><span> /&gt;</span></div></div><div><div><div>133</div></div><div><span><span>                    </span></span><span>&lt;</span><span>span</span><span> </span><span>class</span><span>=</span><span>"text-sm font-medium text-90"</span><span><span>&gt;</span><span>{</span></span><span>i18n</span><span>(</span><span>I18nKey</span><span>.</span><span>tags</span><span><span>)</span><span>}</span><span>&lt;/</span></span><span>span</span><span>&gt;</span></div></div><div><div><div>134</div></div><div><span><span>                    </span></span><span>&lt;</span><span>span</span><span> </span><span>class</span><span>=</span><span>"text-xs text-30"</span><span><span>&gt;(</span><span>{</span></span><span>tags</span><span>.</span><span>length</span><span><span>}</span><span>)&lt;/</span></span><span>span</span><span>&gt;</span></div></div><div><div><div>135</div></div><div><span><span>                </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>136</div></div><div><span><span>                </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"flex flex-wrap gap-2"</span><span>&gt;</span></div></div><div><div><div>137</div></div><div><span><span>                    </span></span><span>{</span><span>tags</span><span>.</span><span>map</span><span><span>((</span><span>tag</span><span>: { </span><span>name</span><span>: </span></span><span>string</span><span><span>; </span><span>count</span><span>: </span></span><span>number</span><span> }) </span><span>=&gt;</span><span> (</span></div></div><div><div><div>138</div></div><div><span><span>                        </span></span><span>&lt;</span><span>a</span></div></div><div><div><div>139</div></div><div><span>                            </span><span>href</span><span><span>=</span><span>{</span></span><span>getTagUrl</span><span>(</span><span>tag</span><span>.</span><span>name</span><span><span>)</span><span>}</span></span></div></div><div><div><div>140</div></div><div><span>                            </span><span>class</span><span>=</span><span>"btn-regular px-3 py-1.5 text-sm rounded-md</span></div></div><div><div><div>141</div></div><div><span><span>                                     </span></span><span>transition-all duration-200</span></div></div><div><div><div>142</div></div><div><span><span>                                     </span></span><span>hover:scale-105 hover:bg-(--primary)/20</span></div></div><div><div><div>143</div></div><div><span><span>                                     </span></span><span>active:scale-95"</span></div></div><div><div><div>144</div></div><div><span><span>                        </span></span><span>&gt;</span></div></div><div><div><div>145</div></div><div><span><span>                            </span></span><span>#</span><span>{</span><span>tag</span><span>.</span><span>name</span><span>}</span></div></div><div><div><div>146</div></div><div><span><span>                            </span></span><span>&lt;</span><span>span</span><span> </span><span>class</span><span>=</span><span>"ml-1 text-xs opacity-60"</span><span><span>&gt;(</span><span>{</span></span><span>tag</span><span>.</span><span>count</span><span><span>}</span><span>)&lt;/</span></span><span>span</span><span>&gt;</span></div></div><div><div><div>147</div></div><div><span><span>                        </span></span><span>&lt;/</span><span>a</span><span>&gt;</span></div></div><div><div><div>148</div></div><div><span><span>                    </span></span><span>))</span><span>}</span></div></div><div><div><div>149</div></div><div><span><span>                </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>150</div></div><div><span><span>            </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>151</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>152</div></div><div><span>    </span><span>}</span></div></div><div><div><div>153</div></div><div>
</div></div><div><div><div>154</div></div><div><span>    </span><span>&lt;!-- 文章列表 --&gt;</span></div></div><div><div><div>155</div></div><div><span><span>    </span></span><span>&lt;</span><span>div</span></div></div><div><div><div>156</div></div><div><span>        </span><span>id</span><span>=</span><span>"post-list-container"</span></div></div><div><div><div>157</div></div><div><span>        </span><span>class</span><span>=</span><span>{</span><span><span>`transition-all duration-500 ease-in-out mb-4 </span><span>${</span></span><span>initialLayoutClass</span><span><span>}</span><span>`</span></span><span>}</span></div></div><div><div><div>158</div></div><div><span>        </span><span>data-default-layout</span><span>=</span><span>{</span><span>defaultLayout</span><span>}</span></div></div><div><div><div>159</div></div><div><span>        </span><span>data-mobile-default-layout</span><span>=</span><span>{</span><span>mobileDefaultLayout</span><span>}</span></div></div><div><div><div>160</div></div><div><span>        </span><span>data-masonry-enabled</span><span>=</span><span>{</span><span>masonryEnabled</span><span>}</span></div></div><div><div><div>161</div></div><div><span>        </span><span>data-column-width</span><span>=</span><span>{</span><span>columnWidth</span><span>}</span></div></div><div><div><div>162</div></div><div><span>        </span><span>style</span><span>=</span><span>{</span><span><span>`--post-card-min-width: </span><span>${</span></span><span>columnWidth</span><span><span>}</span><span>px;`</span></span><span>}</span></div></div><div><div><div>163</div></div><div><span><span>    </span></span><span>&gt;</span></div></div><div><div><div>164</div></div><div><span>        </span><span>{</span></div></div><div><div><div>165</div></div><div><span>            </span><span>posts</span><span>.</span><span>length</span><span> </span><span>&gt;</span><span> </span><span>0</span><span> </span><span>?</span><span> (</span></div></div><div><div><div>166</div></div><div><span>                </span><span>posts</span><span>.</span><span>map</span><span><span>((</span><span>entry</span></span><span>:</span><span> </span><span>PostEntry</span><span><span>, </span><span>index</span></span><span>:</span><span> </span><span>number</span><span>) </span><span>=&gt;</span><span> (</span></div></div><div><div><div>167</div></div><div><span><span>                    </span></span><span>&lt;</span><span>PostCard</span></div></div><div><div><div>168</div></div><div><span>                        </span><span>entry</span><span><span>=</span><span>{</span><span>entry</span><span>}</span></span></div></div><div><div><div>169</div></div><div><span>                        </span><span>title</span><span><span>=</span><span>{</span></span><span>entry</span><span>.</span><span>data</span><span>.</span><span>title</span><span>}</span></div></div><div><div><div>170</div></div><div><span>                        </span><span>tags</span><span><span>=</span><span>{</span></span><span>entry</span><span>.</span><span>data</span><span>.</span><span>tags</span><span>}</span></div></div><div><div><div>171</div></div><div><span>                        </span><span>category</span><span><span>=</span><span>{</span></span><span>entry</span><span>.</span><span>data</span><span>.</span><span>category</span><span>}</span></div></div><div><div><div>172</div></div><div><span>                        </span><span>published</span><span><span>=</span><span>{</span></span><span>entry</span><span>.</span><span>data</span><span>.</span><span>published</span><span>}</span></div></div><div><div><div>173</div></div><div><span>                        </span><span>updated</span><span><span>=</span><span>{</span></span><span>entry</span><span>.</span><span>data</span><span>.</span><span>updated</span><span>}</span></div></div><div><div><div>174</div></div><div><span>                        </span><span>url</span><span><span>=</span><span>{</span></span><span>getPostUrlBySlug</span><span>(</span><span>entry</span><span>.</span><span>id</span><span><span>)</span><span>}</span></span></div></div><div><div><div>175</div></div><div><span>                        </span><span>image</span><span><span>=</span><span>{</span></span><span>entry</span><span>.</span><span>data</span><span>.</span><span>image</span><span>}</span></div></div><div><div><div>176</div></div><div><span>                        </span><span>description</span><span><span>=</span><span>{</span></span><span>entry</span><span>.</span><span>data</span><span>.</span><span>description</span><span>}</span></div></div><div><div><div>177</div></div><div><span>                        </span><span>draft</span><span><span>=</span><span>{</span></span><span>entry</span><span>.</span><span>data</span><span>.</span><span>draft</span><span>}</span></div></div><div><div><div>178</div></div><div><span>                        </span><span>pinned</span><span><span>=</span><span>{</span></span><span>entry</span><span>.</span><span>data</span><span>.</span><span>pinned</span><span>}</span></div></div><div><div><div>179</div></div><div><span>                        </span><span>password</span><span><span>=</span><span>{</span><span>!!</span></span><span>entry</span><span>.</span><span>data</span><span>.</span><span>password</span><span>}</span></div></div><div><div><div>180</div></div><div><span>                        </span><span>loading</span><span><span>=</span><span>{</span><span>index</span><span> </span><span>&lt;</span><span> </span></span><span>2</span><span><span> </span><span>?</span><span> </span></span><span>"eager"</span><span><span> </span><span>:</span><span> </span></span><span>"lazy"</span><span>}</span></div></div><div><div><div>181</div></div><div><span>                        </span><span>class</span><span>:</span><span>list</span><span>=</span><span>"onload-animation post-card-item"</span></div></div><div><div><div>182</div></div><div><span>                        </span><span>style</span><span><span>=</span><span>{</span></span><span><span>`animation-delay: calc(var(--content-delay) + </span><span>${</span></span><span><span>delay</span><span>++</span></span><span> </span><span>*</span><span> </span><span>interval</span><span><span>}</span><span>ms);`</span></span><span>}</span></div></div><div><div><div>183</div></div><div><span><span>                    </span></span><span>/&gt;</span></div></div><div><div><div>184</div></div><div><span><span>                </span></span><span>))</span></div></div><div><div><div>185</div></div><div><span><span>            </span></span><span>) </span><span>:</span><span> (</span></div></div><div><div><div>186</div></div><div><span><span>                </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"card-base px-8 py-12 text-center w-full"</span><span>&gt;</span></div></div><div><div><div>187</div></div><div><span><span>                    </span></span><span>&lt;</span><span>Icon</span><span> </span><span>name</span><span>=</span><span>"material-symbols:article"</span><span> </span><span>class</span><span>=</span><span>"text-5xl text-30 mx-auto mb-4"</span><span> /&gt;</span></div></div><div><div><div>188</div></div><div><span><span>                    </span></span><span>&lt;</span><span>p</span><span> </span><span>class</span><span>=</span><span>"text-30"</span><span><span>&gt;</span><span>{</span></span><span>i18n</span><span>(</span><span>I18nKey</span><span>.</span><span>bangumiNoData</span><span><span>)</span><span>}</span><span>&lt;/</span></span><span>p</span><span>&gt;</span></div></div><div><div><div>189</div></div><div><span><span>                </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>190</div></div><div><span><span>            </span></span><span>)</span></div></div><div><div><div>191</div></div><div><span>        </span><span>}</span></div></div><div><div><div>192</div></div><div><span><span>    </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>193</div></div><div><span>&lt;/</span><span>MainGridLayout</span><span>&gt;</span></div></div><div><div><div>194</div></div><div>
</div></div><div><div><div>195</div></div><div><span>&lt;</span><span>script</span><span> </span><span>is:inline</span><span> </span><span>define:vars</span><span>=</span><span>{</span><span><span>{ </span><span>defaultLayout</span><span>, </span><span>mobileDefaultLayout</span><span>, </span><span>columnWidth</span><span> }</span></span><span>}</span><span>&gt;</span></div></div><div><div><div>196</div></div><div><span><span>    </span></span><span>(</span><span>function</span><span>() {</span></div></div><div><div><div>197</div></div><div><span>        </span><span>const</span><span> </span><span>savedLayout</span><span> </span><span>=</span><span><span> </span><span>localStorage</span><span>.</span></span><span>getItem</span><span>(</span><span>'postListLayout'</span><span>);</span></div></div><div><div><div>198</div></div><div><span>        </span><span>const</span><span> </span><span>effectiveDefault</span><span> </span><span>=</span><span><span> </span><span>window</span><span>.</span></span><span>innerWidth</span><span> </span><span>&lt;</span><span> </span><span>780</span><span> </span><span>?</span><span><span> </span><span>mobileDefaultLayout</span><span> </span></span><span>:</span><span><span> </span><span>defaultLayout</span><span>;</span></span></div></div><div><div><div>199</div></div><div><span>        </span><span>const</span><span> </span><span>effectiveLayout</span><span> </span><span>=</span><span><span> </span><span>window</span><span>.</span></span><span>innerWidth</span><span> </span><span>&lt;</span><span> </span><span>380</span><span> </span><span>?</span><span> </span><span>'grid'</span><span> </span><span>:</span><span><span> (</span><span>savedLayout</span><span> </span></span><span>||</span><span><span> </span><span>effectiveDefault</span><span>);</span></span></div></div><div><div><div>200</div></div><div>
</div></div><div><div><div>201</div></div><div><span>        </span><span>if</span><span><span> (</span><span>effectiveLayout</span><span> </span></span><span>!==</span><span><span> </span><span>defaultLayout</span><span>) {</span></span></div></div><div><div><div>202</div></div><div><span>            </span><span>const</span><span> </span><span>container</span><span> </span><span>=</span><span><span> </span><span>document</span><span>.</span></span><span>getElementById</span><span>(</span><span>'post-list-container'</span><span>);</span></div></div><div><div><div>203</div></div><div>
</div></div><div><div><div>204</div></div><div><span>            </span><span>if</span><span><span> (</span><span>container</span><span>) {</span></span></div></div><div><div><div>205</div></div><div><span><span>                </span></span><span>container</span><span>.</span><span>style</span><span>.</span><span>transition</span><span> </span><span>=</span><span> </span><span>'none'</span><span>;</span></div></div><div><div><div>206</div></div><div><span><span>                </span></span><span>container</span><span>.</span><span>classList</span><span>.</span><span>remove</span><span>(</span><span>'list-mode'</span><span>, </span><span>'grid-mode'</span><span>, </span><span>'post-grid-auto'</span><span>, </span><span>'flex'</span><span>, </span><span>'flex-col'</span><span>, </span><span>'gap-4'</span><span>, </span><span>'md:gap-4'</span><span>);</span></div></div><div><div><div>207</div></div><div>
</div></div><div><div><div>208</div></div><div><span>                </span><span>if</span><span><span> (</span><span>effectiveLayout</span><span> </span></span><span>===</span><span> </span><span>'grid'</span><span>) {</span></div></div><div><div><div>209</div></div><div><span><span>                    </span></span><span>container</span><span>.</span><span>classList</span><span>.</span><span>add</span><span>(</span><span>'grid-mode'</span><span>, </span><span>'post-grid-auto'</span><span>);</span></div></div><div><div><div>210</div></div><div><span><span>                </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>211</div></div><div><span><span>                    </span></span><span>container</span><span>.</span><span>classList</span><span>.</span><span>add</span><span>(</span><span>'list-mode'</span><span>, </span><span>'flex'</span><span>, </span><span>'flex-col'</span><span>, </span><span>'gap-4'</span><span>, </span><span>'md:gap-4'</span><span>);</span></div></div><div><div><div>212</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>213</div></div><div>
</div></div><div><div><div>214</div></div><div><span><span>                </span></span><span>container</span><span>.</span><span>offsetHeight</span><span>;</span></div></div><div><div><div>215</div></div><div><span><span>                </span></span><span>container</span><span>.</span><span>style</span><span>.</span><span>transition</span><span> </span><span>=</span><span> </span><span>''</span><span>;</span></div></div><div><div><div>216</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>217</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>218</div></div><div><span><span>    </span></span><span>})();</span></div></div><div><div><div>219</div></div><div><span>&lt;/</span><span>script</span><span>&gt;</span></div></div><div><div><div>220</div></div><div>
</div></div><div><div><div>221</div></div><div><span>&lt;</span><span>script</span><span>&gt;</span></div></div><div><div><div>222</div></div><div><span>    </span><span>function</span><span> </span><span>initLayout</span><span>() {</span></div></div><div><div><div>223</div></div><div><span>        </span><span>const</span><span> </span><span>postListContainer</span><span> </span><span>=</span><span><span> </span><span>document</span><span>.</span></span><span>getElementById</span><span>(</span><span>"post-list-container"</span><span>);</span></div></div><div><div><div>224</div></div><div><span>        </span><span>if</span><span> (</span><span>!</span><span><span>postListContainer</span><span>) </span></span><span>return</span><span>;</span></div></div><div><div><div>225</div></div><div>
</div></div><div><div><div>226</div></div><div><span>        </span><span>const</span><span> </span><span>savedLayout</span><span> </span><span>=</span><span><span> </span><span>localStorage</span><span>.</span></span><span>getItem</span><span>(</span><span>"postListLayout"</span><span>);</span></div></div><div><div><div>227</div></div><div><span>        </span><span>const</span><span> </span><span>defaultLayout</span><span> </span><span>=</span></div></div><div><div><div>228</div></div><div><span><span>            </span></span><span>postListContainer</span><span>.</span><span>getAttribute</span><span>(</span><span>"data-default-layout"</span><span>) </span><span>||</span><span> </span><span>"list"</span><span>;</span></div></div><div><div><div>229</div></div><div><span>        </span><span>const</span><span> </span><span>mobileDefaultLayout</span><span> </span><span>=</span></div></div><div><div><div>230</div></div><div><span><span>            </span></span><span>postListContainer</span><span>.</span><span>getAttribute</span><span>(</span><span>"data-mobile-default-layout"</span><span>) </span><span>||</span><span><span> </span><span>defaultLayout</span><span>;</span></span></div></div><div><div><div>231</div></div><div>
</div></div><div><div><div>232</div></div><div><span>        </span><span>const</span><span> </span><span>effectiveDefault</span><span> </span><span>=</span><span><span> </span><span>window</span><span>.</span></span><span>innerWidth</span><span> </span><span>&lt;</span><span> </span><span>780</span><span> </span><span>?</span><span><span> </span><span>mobileDefaultLayout</span><span> </span></span><span>:</span><span><span> </span><span>defaultLayout</span><span>;</span></span></div></div><div><div><div>233</div></div><div><span>        </span><span>let</span><span><span> </span><span>currentLayout</span><span> </span></span><span>=</span><span><span> </span><span>savedLayout</span><span> </span></span><span>||</span><span><span> </span><span>effectiveDefault</span><span>;</span></span></div></div><div><div><div>234</div></div><div>
</div></div><div><div><div>235</div></div><div><span>        </span><span>if</span><span><span> (</span><span>window</span><span>.</span></span><span>innerWidth</span><span> </span><span>&lt;</span><span> </span><span>380</span><span>) {</span></div></div><div><div><div>236</div></div><div><span><span>            </span></span><span>currentLayout</span><span> </span><span>=</span><span> </span><span>"grid"</span><span>;</span></div></div><div><div><div>237</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>238</div></div><div>
</div></div><div><div><div>239</div></div><div><span>        </span><span>updatePostListLayout</span><span><span>(</span><span>currentLayout</span><span>);</span></span></div></div><div><div><div>240</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>241</div></div><div>
</div></div><div><div><div>242</div></div><div><span>    </span><span>function</span><span> </span><span>updatePostListLayout</span><span><span>(</span><span>layout</span></span><span>:</span><span> </span><span>string</span><span>) {</span></div></div><div><div><div>243</div></div><div><span>        </span><span>const</span><span> </span><span>postListContainer</span><span> </span><span>=</span><span><span> </span><span>document</span><span>.</span></span><span>getElementById</span><span>(</span><span>"post-list-container"</span><span>);</span></div></div><div><div><div>244</div></div><div><span>        </span><span>if</span><span> (</span><span>!</span><span><span>postListContainer</span><span>) </span></span><span>return</span><span>;</span></div></div><div><div><div>245</div></div><div>
</div></div><div><div><div>246</div></div><div><span>        </span><span>const</span><span> </span><span>isCurrentGrid</span><span> </span><span>=</span><span><span> </span><span>postListContainer</span><span>.</span></span><span>classList</span><span>.</span><span>contains</span><span>(</span><span>"grid-mode"</span><span>);</span></div></div><div><div><div>247</div></div><div><span>        </span><span>const</span><span> </span><span>isCurrentList</span><span> </span><span>=</span><span><span> </span><span>postListContainer</span><span>.</span></span><span>classList</span><span>.</span><span>contains</span><span>(</span><span>"list-mode"</span><span>);</span></div></div><div><div><div>248</div></div><div><span>        </span><span>const</span><span> </span><span>currentLayout</span><span> </span><span>=</span><span><span> </span><span>isCurrentGrid</span><span> </span></span><span>?</span><span> </span><span>"grid"</span><span> </span><span>:</span><span><span> (</span><span>isCurrentList</span><span> </span></span><span>?</span><span> </span><span>"list"</span><span> </span><span>:</span><span> </span><span>null</span><span>);</span></div></div><div><div><div>249</div></div><div>
</div></div><div><div><div>250</div></div><div><span>        </span><span>const</span><span> </span><span>masonryEnabled</span><span> </span><span>=</span><span><span> </span><span>postListContainer</span><span>.</span></span><span>getAttribute</span><span>(</span><span>"data-masonry-enabled"</span><span>) </span><span>===</span><span> </span><span>"true"</span><span>;</span></div></div><div><div><div>251</div></div><div>
</div></div><div><div><div>252</div></div><div><span>        </span><span>const</span><span> </span><span>applyClasses</span><span> </span><span>=</span><span> () </span><span>=&gt;</span><span> {</span></div></div><div><div><div>253</div></div><div><span><span>            </span></span><span>postListContainer</span><span>.</span><span>classList</span><span>.</span><span>remove</span><span>(</span><span>"list-mode"</span><span>, </span><span>"grid-mode"</span><span>, </span><span>"post-grid-auto"</span><span>);</span></div></div><div><div><div>254</div></div><div><span>            </span><span>if</span><span><span> (</span><span>layout</span><span> </span></span><span>===</span><span> </span><span>"grid"</span><span>) {</span></div></div><div><div><div>255</div></div><div><span><span>                </span></span><span>postListContainer</span><span>.</span><span>classList</span><span>.</span><span>add</span><span>(</span><span>"grid-mode"</span><span>);</span></div></div><div><div><div>256</div></div><div><span><span>                </span></span><span>postListContainer</span><span>.</span><span>classList</span><span>.</span><span>remove</span><span>(</span><span>"flex"</span><span>, </span><span>"flex-col"</span><span>);</span></div></div><div><div><div>257</div></div><div><span>                </span><span>if</span><span><span> (</span><span>masonryEnabled</span><span>) {</span></span></div></div><div><div><div>258</div></div><div><span><span>                    </span></span><span>postListContainer</span><span>.</span><span>classList</span><span>.</span><span>remove</span><span>(</span><span>"post-grid-auto"</span><span>);</span></div></div><div><div><div>259</div></div><div><span>                    </span><span>applyMasonryLayout</span><span>();</span></div></div><div><div><div>260</div></div><div><span><span>                </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>261</div></div><div><span><span>                    </span></span><span>postListContainer</span><span>.</span><span>classList</span><span>.</span><span>add</span><span>(</span><span>"post-grid-auto"</span><span>);</span></div></div><div><div><div>262</div></div><div><span>                    </span><span>resetMasonryLayout</span><span>();</span></div></div><div><div><div>263</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>264</div></div><div><span><span>            </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>265</div></div><div><span><span>                </span></span><span>postListContainer</span><span>.</span><span>classList</span><span>.</span><span>add</span><span>(</span><span>"list-mode"</span><span>);</span></div></div><div><div><div>266</div></div><div><span><span>                </span></span><span>postListContainer</span><span>.</span><span>classList</span><span>.</span><span>add</span><span>(</span><span>"flex"</span><span>, </span><span>"flex-col"</span><span>, </span><span>"gap-4"</span><span>, </span><span>"md:gap-4"</span><span>);</span></div></div><div><div><div>267</div></div><div><span><span>                </span></span><span>postListContainer</span><span>.</span><span>classList</span><span>.</span><span>remove</span><span>(</span><span>"post-grid-auto"</span><span>);</span></div></div><div><div><div>268</div></div><div><span>                </span><span>resetMasonryLayout</span><span>();</span></div></div><div><div><div>269</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>270</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>271</div></div><div>
</div></div><div><div><div>272</div></div><div><span>        </span><span>if</span><span> (</span><span>!</span><span><span>currentLayout</span><span>) {</span></span></div></div><div><div><div>273</div></div><div><span>            </span><span>applyClasses</span><span>();</span></div></div><div><div><div>274</div></div><div><span>            </span><span>return</span><span>;</span></div></div><div><div><div>275</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>276</div></div><div>
</div></div><div><div><div>277</div></div><div><span>        </span><span>if</span><span><span> (</span><span>currentLayout</span><span> </span></span><span>===</span><span><span> </span><span>layout</span><span>) {</span></span></div></div><div><div><div>278</div></div><div><span>            </span><span>if</span><span><span> (</span><span>layout</span><span> </span></span><span>===</span><span> </span><span>"grid"</span><span> </span><span>&amp;&amp;</span><span><span> </span><span>masonryEnabled</span><span>) {</span></span></div></div><div><div><div>279</div></div><div><span>                </span><span>applyMasonryLayout</span><span>();</span></div></div><div><div><div>280</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>281</div></div><div><span>            </span><span>return</span><span>;</span></div></div><div><div><div>282</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>283</div></div><div>
</div></div><div><div><div>284</div></div><div><span><span>        </span></span><span>postListContainer</span><span>.</span><span>classList</span><span>.</span><span>add</span><span>(</span><span>"layout-switching"</span><span>);</span></div></div><div><div><div>285</div></div><div>
</div></div><div><div><div>286</div></div><div><span>        </span><span>setTimeout</span><span>(() </span><span>=&gt;</span><span> {</span></div></div><div><div><div>287</div></div><div><span>            </span><span>applyClasses</span><span>();</span></div></div><div><div><div>288</div></div><div>
</div></div><div><div><div>289</div></div><div><span>            </span><span>requestAnimationFrame</span><span>(() </span><span>=&gt;</span><span> {</span></div></div><div><div><div>290</div></div><div><span><span>                </span></span><span>postListContainer</span><span>.</span><span>classList</span><span>.</span><span>remove</span><span>(</span><span>"layout-switching"</span><span>);</span></div></div><div><div><div>291</div></div><div><span><span>            </span></span><span>});</span></div></div><div><div><div>292</div></div><div><span><span>        </span></span><span>}, </span><span>200</span><span>);</span></div></div><div><div><div>293</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>294</div></div><div>
</div></div><div><div><div>295</div></div><div><span>    </span><span>function</span><span> </span><span>resetMasonryLayout</span><span>() {</span></div></div><div><div><div>296</div></div><div><span>        </span><span>const</span><span> </span><span>container</span><span> </span><span>=</span><span><span> </span><span>document</span><span>.</span></span><span>getElementById</span><span>(</span><span>"post-list-container"</span><span>);</span></div></div><div><div><div>297</div></div><div><span>        </span><span>if</span><span> (</span><span>!</span><span><span>container</span><span>) </span></span><span>return</span><span>;</span></div></div><div><div><div>298</div></div><div>
</div></div><div><div><div>299</div></div><div><span><span>        </span></span><span>container</span><span>.</span><span>style</span><span>.</span><span>height</span><span> </span><span>=</span><span> </span><span>""</span><span>;</span></div></div><div><div><div>300</div></div><div><span><span>        </span></span><span>container</span><span>.</span><span>style</span><span>.</span><span>position</span><span> </span><span>=</span><span> </span><span>""</span><span>;</span></div></div><div><div><div>301</div></div><div><span><span>        </span></span><span>container</span><span>.</span><span>style</span><span>.</span><span>display</span><span> </span><span>=</span><span> </span><span>""</span><span>;</span></div></div><div><div><div>302</div></div><div>
</div></div><div><div><div>303</div></div><div><span>        </span><span>const</span><span> </span><span>items</span><span> </span><span>=</span><span><span> </span><span>container</span><span>.</span></span><span>querySelectorAll</span><span>(</span><span>".post-card-item"</span><span>);</span></div></div><div><div><div>304</div></div><div><span><span>        </span></span><span>items</span><span>.</span><span>forEach</span><span><span>((</span><span>item</span><span>) </span></span><span>=&gt;</span><span> {</span></div></div><div><div><div>305</div></div><div><span>            </span><span>// @ts-ignore</span></div></div><div><div><div>306</div></div><div><span><span>            </span></span><span>item</span><span>.</span><span>style</span><span>.</span><span>position</span><span> </span><span>=</span><span> </span><span>""</span><span>;</span></div></div><div><div><div>307</div></div><div><span>            </span><span>// @ts-ignore</span></div></div><div><div><div>308</div></div><div><span><span>            </span></span><span>item</span><span>.</span><span>style</span><span>.</span><span>top</span><span> </span><span>=</span><span> </span><span>""</span><span>;</span></div></div><div><div><div>309</div></div><div><span>            </span><span>// @ts-ignore</span></div></div><div><div><div>310</div></div><div><span><span>            </span></span><span>item</span><span>.</span><span>style</span><span>.</span><span>left</span><span> </span><span>=</span><span> </span><span>""</span><span>;</span></div></div><div><div><div>311</div></div><div><span>            </span><span>// @ts-ignore</span></div></div><div><div><div>312</div></div><div><span><span>            </span></span><span>item</span><span>.</span><span>style</span><span>.</span><span>width</span><span> </span><span>=</span><span> </span><span>""</span><span>;</span></div></div><div><div><div>313</div></div><div><span><span>        </span></span><span>});</span></div></div><div><div><div>314</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>315</div></div><div>
</div></div><div><div><div>316</div></div><div><span>    </span><span>function</span><span> </span><span>applyMasonryLayout</span><span>() {</span></div></div><div><div><div>317</div></div><div><span>        </span><span>const</span><span> </span><span>container</span><span> </span><span>=</span><span><span> </span><span>document</span><span>.</span></span><span>getElementById</span><span>(</span><span>"post-list-container"</span><span>);</span></div></div><div><div><div>318</div></div><div><span>        </span><span>if</span><span> (</span><span>!</span><span><span>container</span><span>) </span></span><span>return</span><span>;</span></div></div><div><div><div>319</div></div><div>
</div></div><div><div><div>320</div></div><div><span>        </span><span>const</span><span> </span><span>masonryEnabled</span><span> </span><span>=</span><span><span> </span><span>container</span><span>.</span></span><span>getAttribute</span><span>(</span><span>"data-masonry-enabled"</span><span>) </span><span>===</span><span> </span><span>"true"</span><span>;</span></div></div><div><div><div>321</div></div><div><span>        </span><span>if</span><span> (</span><span>!</span><span><span>masonryEnabled</span><span>) </span></span><span>return</span><span>;</span></div></div><div><div><div>322</div></div><div>
</div></div><div><div><div>323</div></div><div><span>        </span><span>if</span><span> (</span><span>!</span><span><span>container</span><span>.</span></span><span>classList</span><span>.</span><span>contains</span><span>(</span><span>"grid-mode"</span><span>)) </span><span>return</span><span>;</span></div></div><div><div><div>324</div></div><div>
</div></div><div><div><div>325</div></div><div><span>        </span><span>const</span><span> </span><span>items</span><span> </span><span>=</span><span><span> </span><span>Array</span><span>.</span></span><span>from</span><span><span>(</span><span>container</span><span>.</span></span><span>querySelectorAll</span><span>(</span><span>".post-card-item"</span><span>));</span></div></div><div><div><div>326</div></div><div><span>        </span><span>if</span><span><span> (</span><span>items</span><span>.</span></span><span>length</span><span> </span><span>===</span><span> </span><span>0</span><span>) </span><span>return</span><span>;</span></div></div><div><div><div>327</div></div><div>
</div></div><div><div><div>328</div></div><div><span>        </span><span>const</span><span> </span><span>gap</span><span> </span><span>=</span><span> </span><span>16</span><span>;</span></div></div><div><div><div>329</div></div><div><span>        </span><span>const</span><span> </span><span>columnWidth</span><span> </span><span>=</span><span> </span><span>parseInt</span><span><span>(</span><span>container</span><span>.</span></span><span>getAttribute</span><span>(</span><span>"data-column-width"</span><span>) </span><span>||</span><span> </span><span>"280"</span><span>);</span></div></div><div><div><div>330</div></div><div>
</div></div><div><div><div>331</div></div><div><span><span>        </span></span><span>container</span><span>.</span><span>style</span><span>.</span><span>position</span><span> </span><span>=</span><span> </span><span>"relative"</span><span>;</span></div></div><div><div><div>332</div></div><div><span><span>        </span></span><span>container</span><span>.</span><span>style</span><span>.</span><span>display</span><span> </span><span>=</span><span> </span><span>"block"</span><span>;</span></div></div><div><div><div>333</div></div><div>
</div></div><div><div><div>334</div></div><div><span>        </span><span>const</span><span> </span><span>containerWidth</span><span> </span><span>=</span><span><span> </span><span>container</span><span>.</span></span><span>offsetWidth</span><span>;</span></div></div><div><div><div>335</div></div><div><span>        </span><span>const</span><span> </span><span>colCount</span><span> </span><span>=</span><span><span> </span><span>Math</span><span>.</span></span><span>max</span><span>(</span><span>1</span><span><span>, </span><span>Math</span><span>.</span></span><span>floor</span><span><span>((</span><span>containerWidth</span><span> </span></span><span>+</span><span><span> </span><span>gap</span><span>) </span></span><span>/</span><span><span> (</span><span>columnWidth</span><span> </span></span><span>+</span><span><span> </span><span>gap</span><span>)));</span></span></div></div><div><div><div>336</div></div><div><span>        </span><span>const</span><span> </span><span>itemWidth</span><span> </span><span>=</span><span><span> (</span><span>containerWidth</span><span> </span></span><span>-</span><span><span> (</span><span>colCount</span><span> </span></span><span>-</span><span> </span><span>1</span><span>) </span><span>*</span><span><span> </span><span>gap</span><span>) </span></span><span>/</span><span><span> </span><span>colCount</span><span>;</span></span></div></div><div><div><div>337</div></div><div>
</div></div><div><div><div>338</div></div><div><span>        </span><span>const</span><span> </span><span>colHeights</span><span> </span><span>=</span><span> </span><span>new</span><span> </span><span>Array</span><span><span>(</span><span>colCount</span><span>).</span></span><span>fill</span><span>(</span><span>0</span><span>);</span></div></div><div><div><div>339</div></div><div>
</div></div><div><div><div>340</div></div><div><span><span>        </span></span><span>items</span><span>.</span><span>forEach</span><span><span>((</span><span>item</span><span>) </span></span><span>=&gt;</span><span> {</span></div></div><div><div><div>341</div></div><div><span>            </span><span>const</span><span> </span><span>colIndex</span><span> </span><span>=</span><span><span> </span><span>colHeights</span><span>.</span></span><span>indexOf</span><span><span>(</span><span>Math</span><span>.</span></span><span>min</span><span>(</span><span>...</span><span><span>colHeights</span><span>));</span></span></div></div><div><div><div>342</div></div><div><span>            </span><span>// @ts-ignore</span></div></div><div><div><div>343</div></div><div><span><span>            </span></span><span>item</span><span>.</span><span>style</span><span>.</span><span>position</span><span> </span><span>=</span><span> </span><span>"absolute"</span><span>;</span></div></div><div><div><div>344</div></div><div><span>            </span><span>// @ts-ignore</span></div></div><div><div><div>345</div></div><div><span><span>            </span></span><span>item</span><span>.</span><span>style</span><span>.</span><span>width</span><span> </span><span>=</span><span> </span><span>`</span><span>${</span><span>itemWidth</span><span>}</span><span>px`</span><span>;</span></div></div><div><div><div>346</div></div><div><span>            </span><span>// @ts-ignore</span></div></div><div><div><div>347</div></div><div><span><span>            </span></span><span>item</span><span>.</span><span>style</span><span>.</span><span>setProperty</span><span>(</span><span>'height'</span><span>, </span><span>'auto'</span><span>, </span><span>'important'</span><span>);</span></div></div><div><div><div>348</div></div><div>
</div></div><div><div><div>349</div></div><div><span>            </span><span>// @ts-ignore</span></div></div><div><div><div>350</div></div><div><span>            </span><span>const</span><span> </span><span>height</span><span> </span><span>=</span><span><span> </span><span>item</span><span>.</span></span><span>offsetHeight</span><span>;</span></div></div><div><div><div>351</div></div><div><span>            </span><span>const</span><span> </span><span>top</span><span> </span><span>=</span><span><span> </span><span>colHeights</span><span>[</span><span>colIndex</span><span>];</span></span></div></div><div><div><div>352</div></div><div><span>            </span><span>const</span><span> </span><span>left</span><span> </span><span>=</span><span><span> </span><span>colIndex</span><span> </span></span><span>*</span><span><span> (</span><span>itemWidth</span><span> </span></span><span>+</span><span><span> </span><span>gap</span><span>);</span></span></div></div><div><div><div>353</div></div><div>
</div></div><div><div><div>354</div></div><div><span>            </span><span>// @ts-ignore</span></div></div><div><div><div>355</div></div><div><span><span>            </span></span><span>item</span><span>.</span><span>style</span><span>.</span><span>top</span><span> </span><span>=</span><span> </span><span>`</span><span>${</span><span>top</span><span>}</span><span>px`</span><span>;</span></div></div><div><div><div>356</div></div><div><span>            </span><span>// @ts-ignore</span></div></div><div><div><div>357</div></div><div><span><span>            </span></span><span>item</span><span>.</span><span>style</span><span>.</span><span>left</span><span> </span><span>=</span><span> </span><span>`</span><span>${</span><span>left</span><span>}</span><span>px`</span><span>;</span></div></div><div><div><div>358</div></div><div>
</div></div><div><div><div>359</div></div><div><span><span>            </span></span><span>colHeights</span><span>[</span><span>colIndex</span><span>] </span><span>+=</span><span><span> </span><span>height</span><span> </span></span><span>+</span><span><span> </span><span>gap</span><span>;</span></span></div></div><div><div><div>360</div></div><div><span><span>        </span></span><span>});</span></div></div><div><div><div>361</div></div><div>
</div></div><div><div><div>362</div></div><div><span><span>        </span></span><span>container</span><span>.</span><span>style</span><span>.</span><span>height</span><span> </span><span>=</span><span> </span><span>`</span><span>${</span><span>Math</span><span>.</span><span>max</span><span>(</span><span>...</span><span><span>colHeights</span><span>)</span></span><span>}</span><span>px`</span><span>;</span></div></div><div><div><div>363</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>364</div></div><div>
</div></div><div><div><div>365</div></div><div><span><span>    </span></span><span>document</span><span>.</span><span>addEventListener</span><span>(</span><span>"DOMContentLoaded"</span><span>, </span><span>function</span><span> () {</span></div></div><div><div><div>366</div></div><div><span>        </span><span>setTimeout</span><span><span>(</span><span>initLayout</span><span>, </span></span><span>50</span><span>);</span></div></div><div><div><div>367</div></div><div>
</div></div><div><div><div>368</div></div><div><span>        </span><span>const</span><span> </span><span>imgs</span><span> </span><span>=</span><span><span> </span><span>document</span><span>.</span></span><span>querySelectorAll</span><span>(</span><span>'#post-list-container img'</span><span>);</span></div></div><div><div><div>369</div></div><div><span><span>        </span></span><span>imgs</span><span>.</span><span>forEach</span><span><span>(</span><span>img</span><span> </span></span><span>=&gt;</span><span> {</span></div></div><div><div><div>370</div></div><div><span>            </span><span>// @ts-ignore</span></div></div><div><div><div>371</div></div><div><span>            </span><span>if</span><span><span>(</span><span>img</span><span>.</span></span><span>complete</span><span>) </span><span>return</span><span>;</span></div></div><div><div><div>372</div></div><div><span><span>            </span></span><span>img</span><span>.</span><span>addEventListener</span><span>(</span><span>'load'</span><span>, () </span><span>=&gt;</span><span> {</span></div></div><div><div><div>373</div></div><div><span>                </span><span>applyMasonryLayout</span><span>();</span></div></div><div><div><div>374</div></div><div><span><span>            </span></span><span>});</span></div></div><div><div><div>375</div></div><div><span><span>        </span></span><span>});</span></div></div><div><div><div>376</div></div><div><span><span>    </span></span><span>});</span></div></div><div><div><div>377</div></div><div>
</div></div><div><div><div>378</div></div><div><span><span>    </span></span><span>document</span><span>.</span><span>addEventListener</span><span>(</span><span>"visibilitychange"</span><span>, </span><span>function</span><span> () {</span></div></div><div><div><div>379</div></div><div><span>        </span><span>if</span><span> (</span><span>!</span><span><span>document</span><span>.</span></span><span>hidden</span><span>) {</span></div></div><div><div><div>380</div></div><div><span>            </span><span>setTimeout</span><span><span>(</span><span>initLayout</span><span>, </span></span><span>100</span><span>);</span></div></div><div><div><div>381</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>382</div></div><div><span><span>    </span></span><span>});</span></div></div><div><div><div>383</div></div><div>
</div></div><div><div><div>384</div></div><div><span><span>    </span></span><span>window</span><span>.</span><span>addEventListener</span><span>(</span><span>"layoutChange"</span><span>, </span><span>function</span><span><span> (</span><span>event</span><span>) {</span></span></div></div><div><div><div>385</div></div><div><span>        </span><span>// @ts-ignore</span></div></div><div><div><div>386</div></div><div><span>        </span><span>const</span><span> </span><span>newLayout</span><span> </span><span>=</span><span><span> </span><span>event</span><span>.</span></span><span>detail</span><span>.</span><span>layout</span><span>;</span></div></div><div><div><div>387</div></div><div><span>        </span><span>const</span><span> </span><span>postListContainer</span><span> </span><span>=</span><span><span> </span><span>document</span><span>.</span></span><span>getElementById</span><span>(</span><span>"post-list-container"</span><span>);</span></div></div><div><div><div>388</div></div><div><span>        </span><span>if</span><span> (</span><span>!</span><span><span>postListContainer</span><span>) </span></span><span>return</span><span>;</span></div></div><div><div><div>389</div></div><div>
</div></div><div><div><div>390</div></div><div><span>        </span><span>updatePostListLayout</span><span><span>(</span><span>newLayout</span><span>);</span></span></div></div><div><div><div>391</div></div><div><span><span>    </span></span><span>});</span></div></div><div><div><div>392</div></div><div>
</div></div><div><div><div>393</div></div><div><span>    </span><span>let</span><span><span> </span><span>resizeTimeout</span></span><span>:</span><span> </span><span>any</span><span>;</span></div></div><div><div><div>394</div></div><div><span><span>    </span></span><span>window</span><span>.</span><span>addEventListener</span><span>(</span><span>"resize"</span><span>, </span><span>function</span><span> () {</span></div></div><div><div><div>395</div></div><div><span>        </span><span>clearTimeout</span><span><span>(</span><span>resizeTimeout</span><span>);</span></span></div></div><div><div><div>396</div></div><div><span><span>        </span></span><span>resizeTimeout</span><span> </span><span>=</span><span> </span><span>setTimeout</span><span>(</span><span>function</span><span> () {</span></div></div><div><div><div>397</div></div><div><span>            </span><span>initLayout</span><span>();</span></div></div><div><div><div>398</div></div><div><span><span>        </span></span><span>}, </span><span>250</span><span>);</span></div></div><div><div><div>399</div></div><div><span><span>    </span></span><span>});</span></div></div><div><div><div>400</div></div><div>
</div></div><div><div><div>401</div></div><div><span><span>    </span></span><span>document</span><span>.</span><span>addEventListener</span><span>(</span><span>"astro:page-load"</span><span>, </span><span>function</span><span> () {</span></div></div><div><div><div>402</div></div><div><span>        </span><span>setTimeout</span><span><span>(</span><span>initLayout</span><span>, </span></span><span>50</span><span>);</span></div></div><div><div><div>403</div></div><div>
</div></div><div><div><div>404</div></div><div><span>        </span><span>const</span><span> </span><span>imgs</span><span> </span><span>=</span><span><span> </span><span>document</span><span>.</span></span><span>querySelectorAll</span><span>(</span><span>'#post-list-container img'</span><span>);</span></div></div><div><div><div>405</div></div><div><span><span>        </span></span><span>imgs</span><span>.</span><span>forEach</span><span><span>(</span><span>img</span><span> </span></span><span>=&gt;</span><span> {</span></div></div><div><div><div>406</div></div><div><span>            </span><span>// @ts-ignore</span></div></div><div><div><div>407</div></div><div><span>            </span><span>if</span><span><span>(</span><span>img</span><span>.</span></span><span>complete</span><span>) </span><span>return</span><span>;</span></div></div><div><div><div>408</div></div><div><span><span>            </span></span><span>img</span><span>.</span><span>addEventListener</span><span>(</span><span>'load'</span><span>, () </span><span>=&gt;</span><span> {</span></div></div><div><div><div>409</div></div><div><span>                </span><span>applyMasonryLayout</span><span>();</span></div></div><div><div><div>410</div></div><div><span><span>            </span></span><span>});</span></div></div><div><div><div>411</div></div><div><span><span>        </span></span><span>});</span></div></div><div><div><div>412</div></div><div><span><span>    </span></span><span>});</span></div></div><div><div><div>413</div></div><div>
</div></div><div><div><div>414</div></div><div><span><span>    </span></span><span>document</span><span>.</span><span>addEventListener</span><span>(</span><span>"astro:after-swap"</span><span>, </span><span>function</span><span> () {</span></div></div><div><div><div>415</div></div><div><span>        </span><span>setTimeout</span><span><span>(</span><span>initLayout</span><span>, </span></span><span>50</span><span>);</span></div></div><div><div><div>416</div></div><div><span><span>    </span></span><span>});</span></div></div><div><div><div>417</div></div><div>
</div></div><div><div><div>418</div></div><div><span>    </span><span>setTimeout</span><span><span>(</span><span>initLayout</span><span>, </span></span><span>0</span><span>);</span></div></div><div><div><div>419</div></div><div><span>&lt;/</span><span>script</span><span>&gt;</span></div></div><div><div><div>420</div></div><div>
</div></div><div><div><div>421</div></div><div><span>&lt;</span><span>style</span><span> </span><span>is:global</span><span>&gt;</span></div></div><div><div><div>422</div></div><div><span>    </span><span>.post-grid-auto</span><span> {</span></div></div><div><div><div>423</div></div><div><span><span>        </span></span><span>display: </span><span>grid</span><span>;</span></div></div><div><div><div>424</div></div><div><span><span>        </span></span><span>grid-template-columns: </span><span>repeat</span><span><span>(</span><span>auto-fill</span><span>, </span></span><span>minmax</span><span>(</span><span>var</span><span>(</span><span>--post-card-min-width</span><span>, </span><span><span>320</span><span>px</span></span><span>), </span><span><span>1</span><span>fr</span></span><span>));</span></div></div><div><div><div>425</div></div><div><span><span>        </span></span><span>gap: </span><span><span>1</span><span>rem</span></span><span>;</span></div></div><div><div><div>426</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>427</div></div><div>
</div></div><div><div><div>428</div></div><div><span>    </span><span>@media</span><span> (max-width: </span><span><span>767</span><span>px</span></span><span>) {</span></div></div><div><div><div>429</div></div><div><span>        </span><span>.list-mode</span><span> </span><span>.post-meta-root</span><span> {</span></div></div><div><div><div>430</div></div><div><span><span>            </span></span><span>transform: </span><span>scale</span><span>(</span><span>0.72</span><span>) </span><span>!important</span><span>;</span></div></div><div><div><div>431</div></div><div><span><span>            </span></span><span>transform-origin: </span><span>top</span><span> </span><span>left</span><span> </span><span>!important</span><span>;</span></div></div><div><div><div>432</div></div><div><span><span>            </span></span><span>margin-bottom: </span><span><span>0.25</span><span>rem</span></span><span> </span><span>!important</span><span>;</span></div></div><div><div><div>433</div></div><div><span><span>            </span></span><span>width: </span><span><span>139</span><span>%</span></span><span> </span><span>!important</span><span>;</span></div></div><div><div><div>434</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>435</div></div><div><span>        </span><span>.list-mode</span><span> :global(</span><span>.post-meta</span><span>) {</span></div></div><div><div><div>436</div></div><div><span><span>            </span></span><span>margin-bottom: </span><span><span>0.25</span><span>rem</span></span><span> </span><span>!important</span><span>;</span></div></div><div><div><div>437</div></div><div><span><span>            </span></span><span>gap: </span><span><span>0.25</span><span>rem</span></span><span> </span><span>!important</span><span>;</span></div></div><div><div><div>438</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>439</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>440</div></div><div><span>&lt;/</span><span>style</span><span>&gt;</span></div></div><div><div><div>441</div></div><div>
</div></div><div><div><div>442</div></div><div><span>&lt;</span><span>style</span><span>&gt;</span></div></div><div><div><div>443</div></div><div><span>    </span><span>#post-list-container</span><span> {</span></div></div><div><div><div>444</div></div><div><span><span>        </span></span><span>transition: opacity </span><span><span>0.5</span><span>s</span></span><span> </span><span>cubic-bezier</span><span>(</span><span>0.4</span><span>, </span><span>0</span><span>, </span><span>0.2</span><span>, </span><span>1</span><span>),</span></div></div><div><div><div>445</div></div><div><span><span>                    </span></span><span>transform </span><span><span>0.5</span><span>s</span></span><span> </span><span>cubic-bezier</span><span>(</span><span>0.4</span><span>, </span><span>0</span><span>, </span><span>0.2</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>446</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>447</div></div><div>
</div></div><div><div><div>448</div></div><div><span>    </span><span>#post-list-container</span><span> </span><span>&gt;</span><span> :global(</span><span>*</span><span>) {</span></div></div><div><div><div>449</div></div><div><span><span>        </span></span><span>transition: opacity </span><span><span>0.4</span><span>s</span></span><span> </span><span>cubic-bezier</span><span>(</span><span>0.4</span><span>, </span><span>0</span><span>, </span><span>0.2</span><span>, </span><span>1</span><span>),</span></div></div><div><div><div>450</div></div><div><span><span>                    </span></span><span>transform </span><span><span>0.4</span><span>s</span></span><span> </span><span>cubic-bezier</span><span>(</span><span>0.4</span><span>, </span><span>0</span><span>, </span><span>0.2</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>451</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>452</div></div><div>
</div></div><div><div><div>453</div></div><div><span>    </span><span>#post-list-container</span><span> {</span></div></div><div><div><div>454</div></div><div><span><span>        </span></span><span>transition: opacity </span><span><span>0.2</span><span>s</span></span><span><span> </span><span>ease-out</span><span>, transform </span></span><span><span>0.2</span><span>s</span></span><span><span> </span><span>ease-out</span><span>;</span></span></div></div><div><div><div>455</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>456</div></div><div>
</div></div><div><div><div>457</div></div><div><span>    </span><span>#post-list-container</span><span>.layout-switching</span><span> {</span></div></div><div><div><div>458</div></div><div><span><span>        </span></span><span>opacity: </span><span>0</span><span>;</span></div></div><div><div><div>459</div></div><div><span><span>        </span></span><span>transform: </span><span>translateY</span><span>(</span><span><span>10</span><span>px</span></span><span>);</span></div></div><div><div><div>460</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>461</div></div><div>
</div></div><div><div><div>462</div></div><div><span>    </span><span>#post-list-container</span><span>.grid-mode</span><span> :global(</span><span>.post-card-wrapper</span><span>) {</span></div></div><div><div><div>463</div></div><div><span><span>        </span></span><span>flex-direction: </span><span>column-reverse</span><span> </span><span>!important</span><span>;</span></div></div><div><div><div>464</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>465</div></div><div>
</div></div><div><div><div>466</div></div><div><span>    </span><span>#post-list-container</span><span>.grid-mode</span><span> :global(</span><span>.post-card-image</span><span>) {</span></div></div><div><div><div>467</div></div><div><span><span>        </span></span><span>width: </span><span><span>100</span><span>%</span></span><span> </span><span>!important</span><span>;</span></div></div><div><div><div>468</div></div><div><span><span>        </span></span><span>position: </span><span>relative</span><span> </span><span>!important</span><span>;</span></div></div><div><div><div>469</div></div><div><span><span>        </span></span><span>top: </span><span>auto</span><span> </span><span>!important</span><span>;</span></div></div><div><div><div>470</div></div><div><span><span>        </span></span><span>right: </span><span>auto</span><span> </span><span>!important</span><span>;</span></div></div><div><div><div>471</div></div><div><span><span>        </span></span><span>bottom: </span><span>auto</span><span> </span><span>!important</span><span>;</span></div></div><div><div><div>472</div></div><div><span><span>        </span></span><span>border-radius: </span><span>var</span><span>(</span><span>--radius-large</span><span>) </span><span>var</span><span>(</span><span>--radius-large</span><span>) </span><span>0</span><span> </span><span>0</span><span> </span><span>!important</span><span>;</span></div></div><div><div><div>473</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>474</div></div><div>
</div></div><div><div><div>475</div></div><div><span>    </span><span>#post-list-container</span><span>.grid-mode</span><span> :global(</span><span>.post-card-content</span><span>) {</span></div></div><div><div><div>476</div></div><div><span><span>        </span></span><span>width: </span><span><span>100</span><span>%</span></span><span> </span><span>!important</span><span>;</span></div></div><div><div><div>477</div></div><div><span><span>        </span></span><span>padding: </span><span><span>1</span><span>rem</span></span><span> </span><span>!important</span><span>;</span></div></div><div><div><div>478</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>479</div></div><div>
</div></div><div><div><div>480</div></div><div><span>    </span><span>#post-list-container</span><span>.grid-mode</span><span> :global(</span><span>.no-cover</span><span> </span><span>.post-card-content</span><span>) {</span></div></div><div><div><div>481</div></div><div><span><span>        </span></span><span>padding-right: </span><span><span>4.5</span><span>rem</span></span><span> </span><span>!important</span><span>;</span></div></div><div><div><div>482</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>483</div></div><div>
</div></div><div><div><div>484</div></div><div><span>    </span><span>#post-list-container</span><span>.grid-mode</span><span> :global(</span><span>.post-card-title</span><span>) {</span></div></div><div><div><div>485</div></div><div><span><span>        </span></span><span>font-size: </span><span><span>1.35</span><span>rem</span></span><span> </span><span>!important</span><span>;</span></div></div><div><div><div>486</div></div><div><span><span>        </span></span><span>line-height: </span><span><span>1.75</span><span>rem</span></span><span> </span><span>!important</span><span>;</span></div></div><div><div><div>487</div></div><div><span><span>        </span></span><span>margin-bottom: </span><span><span>0.5</span><span>rem</span></span><span> </span><span>!important</span><span>;</span></div></div><div><div><div>488</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>489</div></div><div>
</div></div><div><div><div>490</div></div><div><span>    </span><span>#post-list-container</span><span>.grid-mode</span><span> :global(</span><span><span>.post-card-title</span><span>::before</span></span><span>) {</span></div></div><div><div><div>491</div></div><div><span><span>        </span></span><span>display: </span><span>none</span><span> </span><span>!important</span><span>;</span></div></div><div><div><div>492</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>493</div></div><div>
</div></div><div><div><div>494</div></div><div><span>    </span><span>#post-list-container</span><span>.grid-mode</span><span> :global(</span><span>.post-meta</span><span>) {</span></div></div><div><div><div>495</div></div><div><span><span>        </span></span><span>margin-bottom: </span><span><span>0.5</span><span>rem</span></span><span> </span><span>!important</span><span>;</span></div></div><div><div><div>496</div></div><div><span><span>        </span></span><span>gap: </span><span><span>0.5</span><span>rem</span></span><span> </span><span>!important</span><span>;</span></div></div><div><div><div>497</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>498</div></div><div>
</div></div><div><div><div>499</div></div><div><span>    </span><span>#post-list-container</span><span>.grid-mode</span><span> :global(</span><span>.post-meta</span><span> </span><span>.text-xl</span><span>) {</span></div></div><div><div><div>500</div></div><div><span><span>        </span></span><span>font-size: </span><span><span>1</span><span>rem</span></span><span> </span><span>!important</span><span>;</span></div></div><div><div><div>501</div></div><div><span><span>        </span></span><span>line-height: </span><span><span>1.25</span><span>rem</span></span><span> </span><span>!important</span><span>;</span></div></div><div><div><div>502</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>503</div></div><div>
</div></div><div><div><div>504</div></div><div><span>    </span><span>#post-list-container</span><span>.grid-mode</span><span> :global(</span><span>.meta-icon</span><span>) {</span></div></div><div><div><div>505</div></div><div><span><span>        </span></span><span>width: </span><span><span>1.5</span><span>rem</span></span><span> </span><span>!important</span><span>;</span></div></div><div><div><div>506</div></div><div><span><span>        </span></span><span>height: </span><span><span>1.5</span><span>rem</span></span><span> </span><span>!important</span><span>;</span></div></div><div><div><div>507</div></div><div><span><span>        </span></span><span>margin-right: </span><span><span>0.25</span><span>rem</span></span><span> </span><span>!important</span><span>;</span></div></div><div><div><div>508</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>509</div></div><div>
</div></div><div><div><div>510</div></div><div><span>    </span><span>#post-list-container</span><span>.grid-mode</span><span> :global(</span><span>.post-meta</span><span> </span><span>.text-sm</span><span>) {</span></div></div><div><div><div>511</div></div><div><span><span>        </span></span><span>font-size: </span><span><span>0.75</span><span>rem</span></span><span> </span><span>!important</span><span>;</span></div></div><div><div><div>512</div></div><div><span><span>        </span></span><span>line-height: </span><span><span>1</span><span>rem</span></span><span> </span><span>!important</span><span>;</span></div></div><div><div><div>513</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>514</div></div><div>
</div></div><div><div><div>515</div></div><div><span>    </span><span>#post-list-container</span><span>.grid-mode</span><span> :global(</span><span>.post-meta</span><span> </span><span>.pinned-btn</span><span>) {</span></div></div><div><div><div>516</div></div><div><span><span>        </span></span><span>padding: </span><span><span>0.25</span><span>rem</span></span><span> </span><span><span>0.375</span><span>rem</span></span><span> </span><span>!important</span><span>;</span></div></div><div><div><div>517</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>518</div></div><div><span>&lt;/</span><span>style</span><span>&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>创建URL工具函数<a href="#创建url工具函数"><span>#</span></a></h2><p>文件路径：<code>src/utils/url-utils.ts</code></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>export</span><span> </span><span>function</span><span> </span><span>getCategoryUrl</span><span><span>(</span><span>category</span></span><span>:</span><span> </span><span>string</span><span> </span><span>|</span><span> </span><span>null</span><span>)</span><span>:</span><span> </span><span>string</span><span> {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>if</span><span> (</span></div></div><div><div><div>3</div></div><div><span>    </span><span>!</span><span><span>category</span><span> </span></span><span>||</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>category</span><span>.</span><span>trim</span><span>() </span><span>===</span><span> </span><span>""</span><span> </span><span>||</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>category</span><span>.</span><span>trim</span><span>().</span><span>toLowerCase</span><span>() </span><span>===</span><span> </span><span>i18n</span><span><span>(</span><span>I18nKey</span><span>.</span></span><span>uncategorized</span><span>).</span><span>toLowerCase</span><span>()</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>7</div></div><div><span>    </span><span>return</span><span> </span><span>url</span><span>(</span><span>"/categories/uncategorized/"</span><span>);</span></div></div><div><div><div>8</div></div><div><span>  </span><span>return</span><span> </span><span>url</span><span>(</span><span>`/categories/</span><span>${</span><span>encodeURIComponent</span><span><span>(</span><span>category</span></span><span>.</span><span>trim</span><span>())</span><span>}</span><span>/`</span><span>);</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>export</span><span> </span><span>function</span><span> </span><span>getCategoryPageUrl</span><span>()</span><span>:</span><span> </span><span>string</span><span> {</span></div></div><div><div><div>12</div></div><div><span>  </span><span>return</span><span> </span><span>url</span><span>(</span><span>"/categories/"</span><span>);</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>更新分类栏组件<a href="#更新分类栏组件"><span>#</span></a></h2><p>文件路径：<code>src/components/layout/CategoryBar.astro</code></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>---</span></div></div><div><div><div>2</div></div><div><span>import</span><span> { </span><span>url</span><span>, </span><span>getCategoryPageUrl</span><span> } </span><span>from</span><span> </span><span>"@/utils/url-utils"</span><span>;</span></div></div><div><div><div>3</div></div><div><span>const</span><span> </span><span>categoriesUrl</span><span> </span><span>=</span><span> </span><span>getCategoryPageUrl</span><span>();</span></div></div><div><div><div>4</div></div><div><span>---</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>&lt;</span><span>a</span></div></div><div><div><div>7</div></div><div><span>      </span><span>href</span><span>=</span><span>{</span><span>categoriesUrl</span><span>}</span></div></div><div><div><div>8</div></div><div><span>      </span><span>class</span><span>=</span><span>"category-pill text-sm px-3 py-1 rounded-lg whitespace-nowrap flex-shrink-0</span></div></div><div><div><div>9</div></div><div><span><span>             </span></span><span>transition-colors duration-200 flex items-center justify-center"</span></div></div><div><div><div>10</div></div><div><span>      </span><span>data-category-name</span><span>=</span><span>"__categories__"</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>&gt;</span></div></div><div><div><div>12</div></div><div><span>      </span><span>{</span><span>i18n</span><span>(</span><span>I18nKey</span><span>.</span><span>categories</span><span>)</span><span>}</span></div></div><div><div><div>13</div></div><div><span><span>      </span></span><span>&lt;</span><span>span</span><span> </span><span>class</span><span>=</span><span>"text-xs opacity-60 ml-1"</span><span>&gt;</span><span>{</span><span>categories</span><span>.</span><span>length</span><span>}</span><span>&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>14</div></div><div><span>&lt;/</span><span>a</span><span>&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>导航栏配置<a href="#导航栏配置"><span>#</span></a></h2><p>文件路径：<code>src/config/navBarConfig.ts</code></p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// 根据页面开关动态生成导航栏配置</span></div></div><div><div><div>2</div></div><div><span>const</span><span> </span><span>getDynamicNavBarConfig</span><span> </span><span>=</span><span> ()</span><span>:</span><span> </span><span>NavBarConfig</span><span> </span><span>=&gt;</span><span> {</span></div></div><div><div><div>3</div></div><div><span>  </span><span>// 基础导航栏链接</span></div></div><div><div><div>4</div></div><div><span>  </span><span>const</span><span> </span><span>links</span><span>:</span><span> (</span><span>NavBarLink</span><span> </span><span>|</span><span> </span><span>LinkPreset</span><span>)[] </span><span>=</span><span> [</span></div></div><div><div><div>5</div></div><div><span>    </span><span>// 主页</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>LinkPreset</span><span>.</span><span>Home</span><span>,</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span>// 文章分类</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>{</span></div></div><div><div><div>10</div></div><div><span>      </span><span>name</span><span>:</span><span> </span><span>"分类"</span><span>,</span></div></div><div><div><div>11</div></div><div><span>      </span><span>url</span><span>:</span><span> </span><span>"/categories/"</span><span>,</span></div></div><div><div><div>12</div></div><div><span>      </span><span>icon</span><span>:</span><span> </span><span>"material-symbols:folder-open"</span><span>,</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>    </span><span>// 归档</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>LinkPreset</span><span>.</span><span>Archive</span><span>,</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>];</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>语言配置<a href="#语言配置"><span>#</span></a></h2><ul>
<li>文件路径：<code>src/i18n/i18nKey.ts</code></li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>allCategories</span><span> </span></span><span>=</span><span> </span><span>"allCategories"</span><span>,</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>文件路径：<code>src/i18n/languages/en.ts</code></li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>[</span><span>Key</span><span>.</span></span><span>allCategories</span><span>]: </span><span>"All Categories"</span><span>,</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>文件路径：<code>src/i18n/languages/ja.ts</code></li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>[</span><span>Key</span><span>.</span></span><span>allCategories</span><span>]: </span><span>"すべてのカテゴリ"</span><span>,</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>文件路径：<code>src/i18n/languages/ru.ts</code></li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>[</span><span>Key</span><span>.</span></span><span>allCategories</span><span>]: </span><span>"Все категории"</span><span>,</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>文件路径：<code>src/i18n/languages/zh_CN.ts</code></li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>  </span></span><span>[</span><span>Key</span><span>.</span><span>allCategories</span><span>]: </span><span>"全部分类"</span><span>,</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>文件路径：<code>src/i18n/languages/zh_TW.ts</code></li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>  </span></span><span>[</span><span>Key</span><span>.</span><span>allCategories</span><span>]: </span><span>"全部分類"</span><span>,</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>]]></content>
    </entry>
    <entry>
      <id>https://seasir.top/posts/RelationshipTimer/</id>
      <title type="text">恋爱计时组件</title>
      <published>2026-06-02T00:00:00.000Z</published>
      <updated>2026-06-02T00:00:00.000Z</updated>
      <author><name>Hyde</name></author>
      <link rel="alternate" href="https://seasir.top/posts/RelationshipTimer/"/>
      <summary type="text">最近给博客加了个小彩蛋——「恋爱计时组件」，从零撸了一个记录纪念日的小工具。写了组件代码、配了全局注册、还兼顾了桌面端侧边栏和移动端的显示逻辑，连 TypeScript 类型定义和双方昵称/日期的配置项都琢磨得挺细。虽然只是个轻量小功能，但每次看到倒计时跳动，心里都悄悄甜一下 😊</summary>
      <content type="html"><![CDATA[<section><h2>创建组件<a href="#创建组件"><span>#</span></a></h2><p>文件路径：<code>src/components/widget/RelationshipTimer.astro</code></p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&lt;!-- 恋爱倒计时组件 --&gt;</span></div></div><div><div><div>2</div></div><div><span>---</span></div></div><div><div><div>3</div></div><div><span>import WidgetLayout from "@/components/common/WidgetLayout.astro";</span></div></div><div><div><div>4</div></div><div><span>import </span><span>{</span><span><span> </span><span>relationshipConfig</span><span> </span></span><span>}</span><span> from "@/config/relationshipConfig";</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>const config = relationshipConfig;</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>interface Props </span><span>{</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>class</span><span>?:</span><span><span> </span><span>string</span><span>;</span></span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>style</span><span>?:</span><span><span> </span><span>string</span><span>;</span></span></div></div><div><div><div>11</div></div><div><span>}</span></div></div><div><div><div>12</div></div><div><span>const className = Astro.props.class;</span></div></div><div><div><div>13</div></div><div><span>const style = Astro.props.style;</span></div></div><div><div><div>14</div></div><div><span>---</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>&lt;</span><span>WidgetLayout</span></div></div><div><div><div>17</div></div><div><span>  </span><span>name</span><span>=</span><span>{</span><span>config</span><span>.</span><span>title</span><span> </span><span>||</span><span> </span><span>"恋爱时光"</span><span>}</span></div></div><div><div><div>18</div></div><div><span>  </span><span>id</span><span>=</span><span>"relationship"</span></div></div><div><div><div>19</div></div><div><span>  </span><span>class</span><span>=</span><span>{</span><span>className</span><span>}</span></div></div><div><div><div>20</div></div><div><span>  </span><span>style</span><span>=</span><span>{</span><span>style</span><span>}</span></div></div><div><div><div>21</div></div><div><span>&gt;</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"flex flex-col items-center p-2"</span><span>&gt;</span></div></div><div><div><div>23</div></div><div><span>    </span><span>&lt;!-- 顶部名称栏 --&gt;</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"flex items-center justify-center gap-2 mb-4 text-sm font-medium text-neutral-600 dark:text-neutral-400"</span><span>&gt;</span></div></div><div><div><div>25</div></div><div><span><span>      </span></span><span>&lt;</span><span>span</span><span>&gt;</span><span>{</span><span>config</span><span>.</span><span>name1</span><span>}</span><span>&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>26</div></div><div><span><span>      </span></span><span>&lt;</span><span>span</span><span> </span><span>class</span><span>=</span><span>"text-red-500 animate-pulse"</span><span>&gt;❤️&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>27</div></div><div><span><span>      </span></span><span>&lt;</span><span>span</span><span>&gt;</span><span>{</span><span>config</span><span>.</span><span>name2</span><span>}</span><span>&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>    </span><span>&lt;!-- 头像和爱心 --&gt;</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"flex items-center justify-center gap-4 mb-6"</span><span>&gt;</span></div></div><div><div><div>32</div></div><div><span><span>      </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"w-16 h-16 rounded-full overflow-hidden border-2 border-white dark:border-neutral-800 shadow-md transition-transform hover:scale-110"</span><span>&gt;</span></div></div><div><div><div>33</div></div><div><span><span>        </span></span><span>&lt;</span><span>img</span><span> </span><span>src</span><span>=</span><span>{</span><span>config</span><span>.</span><span>avatar1</span><span>}</span><span> </span><span>alt</span><span>=</span><span>{</span><span>config</span><span>.</span><span>name1</span><span>}</span><span> </span><span>class</span><span>=</span><span>"w-full h-full object-cover"</span><span> /&gt;</span></div></div><div><div><div>34</div></div><div><span><span>      </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>35</div></div><div><span><span>      </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"text-2xl text-red-500 transform transition-all hover:scale-125 hover:rotate-12 cursor-pointer"</span><span>&gt;</span></div></div><div><div><div>36</div></div><div><span><span>        </span></span><span>❤️</span></div></div><div><div><div>37</div></div><div><span><span>      </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>38</div></div><div><span><span>      </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"w-16 h-16 rounded-full overflow-hidden border-2 border-white dark:border-neutral-800 shadow-md transition-transform hover:scale-110"</span><span>&gt;</span></div></div><div><div><div>39</div></div><div><span><span>        </span></span><span>&lt;</span><span>img</span><span> </span><span>src</span><span>=</span><span>{</span><span>config</span><span>.</span><span>avatar2</span><span>}</span><span> </span><span>alt</span><span>=</span><span>{</span><span>config</span><span>.</span><span>name2</span><span>}</span><span> </span><span>class</span><span>=</span><span>"w-full h-full object-cover"</span><span> /&gt;</span></div></div><div><div><div>40</div></div><div><span><span>      </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span>    </span><span>&lt;!-- 计时器 --&gt;</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"relationship-timer flex items-center justify-center gap-x-1 text-sm whitespace-nowrap overflow-x-auto"</span><span>&gt;</span></div></div><div><div><div>45</div></div><div><span><span>      </span></span><span>&lt;</span><span>span</span><span> </span><span>class</span><span>=</span><span>"timer-num"</span><span> </span><span>data-timer-years</span><span>&gt;0&lt;/</span><span>span</span><span>&gt;&lt;</span><span>span</span><span> </span><span>class</span><span>=</span><span>"timer-unit bg-red-400"</span><span>&gt;年&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>46</div></div><div><span><span>      </span></span><span>&lt;</span><span>span</span><span> </span><span>class</span><span>=</span><span>"timer-num"</span><span> </span><span>data-timer-months</span><span>&gt;0&lt;/</span><span>span</span><span>&gt;&lt;</span><span>span</span><span> </span><span>class</span><span>=</span><span>"timer-unit bg-red-300"</span><span>&gt;月&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>47</div></div><div><span><span>      </span></span><span>&lt;</span><span>span</span><span> </span><span>class</span><span>=</span><span>"timer-num"</span><span> </span><span>data-timer-days</span><span>&gt;0&lt;/</span><span>span</span><span>&gt;&lt;</span><span>span</span><span> </span><span>class</span><span>=</span><span>"timer-unit bg-blue-400"</span><span>&gt;天&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>48</div></div><div><span><span>      </span></span><span>&lt;</span><span>span</span><span> </span><span>class</span><span>=</span><span>"timer-num"</span><span> </span><span>data-timer-hours</span><span>&gt;0&lt;/</span><span>span</span><span>&gt;&lt;</span><span>span</span><span> </span><span>class</span><span>=</span><span>"timer-unit bg-orange-400"</span><span>&gt;时&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>49</div></div><div><span><span>      </span></span><span>&lt;</span><span>span</span><span> </span><span>class</span><span>=</span><span>"timer-num"</span><span> </span><span>data-timer-minutes</span><span>&gt;0&lt;/</span><span>span</span><span>&gt;&lt;</span><span>span</span><span> </span><span>class</span><span>=</span><span>"timer-unit bg-blue-500"</span><span>&gt;分&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>50</div></div><div><span><span>      </span></span><span>&lt;</span><span>span</span><span> </span><span>class</span><span>=</span><span>"timer-num"</span><span> </span><span>data-timer-seconds</span><span>&gt;00&lt;/</span><span>span</span><span>&gt;&lt;</span><span>span</span><span> </span><span>class</span><span>=</span><span>"timer-unit bg-purple-400"</span><span>&gt;秒&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>51</div></div><div><span><span>    </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>52</div></div><div><span><span>  </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>53</div></div><div><span>&lt;/</span><span>WidgetLayout</span><span>&gt;</span></div></div><div><div><div>54</div></div><div>
</div></div><div><div><div>55</div></div><div><span>&lt;</span><span>style</span><span>&gt;</span></div></div><div><div><div>56</div></div><div><span>  </span><span>@reference</span><span> "tailwindcss";</span></div></div><div><div><div>57</div></div><div>
</div></div><div><div><div>58</div></div><div><span>  </span><span>.timer-num</span><span> {</span></div></div><div><div><div>59</div></div><div><span><span>    </span></span><span>@apply font-bold text-neutral-700 dark:text-neutral-200 min-w-[</span><span><span>0</span><span>rem</span></span><span>] text-center;</span></div></div><div><div><div>60</div></div><div><span><span>    </span></span><span>color: </span><span>var</span><span>(</span><span>--primary</span><span>);</span></div></div><div><div><div>61</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>62</div></div><div><span>  </span><span>.timer-unit</span><span> {</span></div></div><div><div><div>63</div></div><div><span><span>    </span></span><span>@apply px-1 py-0.5 rounded text-[10px] text-white font-medium shrink-0;</span></div></div><div><div><div>64</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>65</div></div><div><span>&lt;/</span><span>style</span><span>&gt;</span></div></div><div><div><div>66</div></div><div>
</div></div><div><div><div>67</div></div><div><span>&lt;</span><span>script</span><span> </span><span>is:inline</span><span> </span><span>define:vars</span><span>=</span><span>{</span><span><span>{ </span><span>startDateStr</span></span><span>:</span><span> </span><span>config</span><span>.</span><span>startDate</span><span> }</span><span>}</span><span>&gt;</span></div></div><div><div><div>68</div></div><div><span>  </span><span>function</span><span> </span><span>updateRelationshipTimer</span><span>() {</span></div></div><div><div><div>69</div></div><div><span>    </span><span>const</span><span> </span><span>startDate</span><span> </span><span>=</span><span> </span><span>new</span><span> </span><span>Date</span><span><span>(</span><span>startDateStr</span><span>);</span></span></div></div><div><div><div>70</div></div><div><span>    </span><span>const</span><span> </span><span>now</span><span> </span><span>=</span><span> </span><span>new</span><span> </span><span>Date</span><span>();</span></div></div><div><div><div>71</div></div><div>
</div></div><div><div><div>72</div></div><div><span>    </span><span>let</span><span><span> </span><span>diff</span><span> </span></span><span>=</span><span><span> </span><span>now</span><span>.</span></span><span>getTime</span><span>() </span><span>-</span><span><span> </span><span>startDate</span><span>.</span></span><span>getTime</span><span>();</span></div></div><div><div><div>73</div></div><div>
</div></div><div><div><div>74</div></div><div><span>    </span><span>if</span><span><span> (</span><span>diff</span><span> </span></span><span>&lt;</span><span> </span><span>0</span><span>) </span><span>return</span><span>;</span></div></div><div><div><div>75</div></div><div>
</div></div><div><div><div>76</div></div><div><span>    </span><span>// 计算年月日时分秒的差值</span></div></div><div><div><div>77</div></div><div><span>    </span><span>let</span><span><span> </span><span>years</span><span> </span></span><span>=</span><span><span> </span><span>now</span><span>.</span></span><span>getFullYear</span><span>() </span><span>-</span><span><span> </span><span>startDate</span><span>.</span></span><span>getFullYear</span><span>();</span></div></div><div><div><div>78</div></div><div><span>    </span><span>let</span><span><span> </span><span>months</span><span> </span></span><span>=</span><span><span> </span><span>now</span><span>.</span></span><span>getMonth</span><span>() </span><span>-</span><span><span> </span><span>startDate</span><span>.</span></span><span>getMonth</span><span>();</span></div></div><div><div><div>79</div></div><div><span>    </span><span>let</span><span><span> </span><span>days</span><span> </span></span><span>=</span><span><span> </span><span>now</span><span>.</span></span><span>getDate</span><span>() </span><span>-</span><span><span> </span><span>startDate</span><span>.</span></span><span>getDate</span><span>();</span></div></div><div><div><div>80</div></div><div><span>    </span><span>let</span><span><span> </span><span>hours</span><span> </span></span><span>=</span><span><span> </span><span>now</span><span>.</span></span><span>getHours</span><span>() </span><span>-</span><span><span> </span><span>startDate</span><span>.</span></span><span>getHours</span><span>();</span></div></div><div><div><div>81</div></div><div><span>    </span><span>let</span><span><span> </span><span>minutes</span><span> </span></span><span>=</span><span><span> </span><span>now</span><span>.</span></span><span>getMinutes</span><span>() </span><span>-</span><span><span> </span><span>startDate</span><span>.</span></span><span>getMinutes</span><span>();</span></div></div><div><div><div>82</div></div><div><span>    </span><span>let</span><span><span> </span><span>seconds</span><span> </span></span><span>=</span><span><span> </span><span>now</span><span>.</span></span><span>getSeconds</span><span>() </span><span>-</span><span><span> </span><span>startDate</span><span>.</span></span><span>getSeconds</span><span>();</span></div></div><div><div><div>83</div></div><div>
</div></div><div><div><div>84</div></div><div><span>    </span><span>// 处理借位逻辑</span></div></div><div><div><div>85</div></div><div><span>    </span><span>if</span><span><span> (</span><span>seconds</span><span> </span></span><span>&lt;</span><span> </span><span>0</span><span>) {</span></div></div><div><div><div>86</div></div><div><span><span>      </span></span><span>seconds</span><span> </span><span>+=</span><span> </span><span>60</span><span>;</span></div></div><div><div><div>87</div></div><div><span><span>      </span></span><span>minutes</span><span>--</span><span>;</span></div></div><div><div><div>88</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>89</div></div><div><span>    </span><span>if</span><span><span> (</span><span>minutes</span><span> </span></span><span>&lt;</span><span> </span><span>0</span><span>) {</span></div></div><div><div><div>90</div></div><div><span><span>      </span></span><span>minutes</span><span> </span><span>+=</span><span> </span><span>60</span><span>;</span></div></div><div><div><div>91</div></div><div><span><span>      </span></span><span>hours</span><span>--</span><span>;</span></div></div><div><div><div>92</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>93</div></div><div><span>    </span><span>if</span><span><span> (</span><span>hours</span><span> </span></span><span>&lt;</span><span> </span><span>0</span><span>) {</span></div></div><div><div><div>94</div></div><div><span><span>      </span></span><span>hours</span><span> </span><span>+=</span><span> </span><span>24</span><span>;</span></div></div><div><div><div>95</div></div><div><span><span>      </span></span><span>days</span><span>--</span><span>;</span></div></div><div><div><div>96</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>97</div></div><div><span>    </span><span>if</span><span><span> (</span><span>days</span><span> </span></span><span>&lt;</span><span> </span><span>0</span><span>) {</span></div></div><div><div><div>98</div></div><div><span>      </span><span>const</span><span> </span><span>lastMonth</span><span> </span><span>=</span><span> </span><span>new</span><span> </span><span>Date</span><span><span>(</span><span>now</span><span>.</span></span><span>getFullYear</span><span><span>(), </span><span>now</span><span>.</span></span><span>getMonth</span><span>(), </span><span>0</span><span>);</span></div></div><div><div><div>99</div></div><div><span><span>      </span></span><span>days</span><span> </span><span>+=</span><span><span> </span><span>lastMonth</span><span>.</span></span><span>getDate</span><span>();</span></div></div><div><div><div>100</div></div><div><span><span>      </span></span><span>months</span><span>--</span><span>;</span></div></div><div><div><div>101</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>102</div></div><div><span>    </span><span>if</span><span><span> (</span><span>months</span><span> </span></span><span>&lt;</span><span> </span><span>0</span><span>) {</span></div></div><div><div><div>103</div></div><div><span><span>      </span></span><span>months</span><span> </span><span>+=</span><span> </span><span>12</span><span>;</span></div></div><div><div><div>104</div></div><div><span><span>      </span></span><span>years</span><span>--</span><span>;</span></div></div><div><div><div>105</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>106</div></div><div>
</div></div><div><div><div>107</div></div><div><span>    </span><span>// 更新所有实例，避免移动端和电脑端 ID 冲突</span></div></div><div><div><div>108</div></div><div><span>    </span><span>const</span><span> </span><span>update</span><span> </span><span>=</span><span><span> (</span><span>attr</span><span>, </span><span>value</span><span>) </span></span><span>=&gt;</span><span> {</span></div></div><div><div><div>109</div></div><div><span>      </span><span>const</span><span> </span><span>elements</span><span> </span><span>=</span><span><span> </span><span>document</span><span>.</span></span><span>querySelectorAll</span><span>(</span><span>`[data-timer-</span><span>${</span><span>attr</span><span>}</span><span>]`</span><span>);</span></div></div><div><div><div>110</div></div><div><span><span>      </span></span><span>elements</span><span>.</span><span>forEach</span><span><span>(</span><span>el</span><span> </span></span><span>=&gt;</span><span> {</span></div></div><div><div><div>111</div></div><div><span><span>        </span></span><span>el</span><span>.</span><span>innerText</span><span> </span><span>=</span><span><span> </span><span>value</span><span>;</span></span></div></div><div><div><div>112</div></div><div><span><span>      </span></span><span>});</span></div></div><div><div><div>113</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>114</div></div><div>
</div></div><div><div><div>115</div></div><div><span>    </span><span>update</span><span>(</span><span>'years'</span><span><span>, </span><span>years</span><span>);</span></span></div></div><div><div><div>116</div></div><div><span>    </span><span>update</span><span>(</span><span>'months'</span><span><span>, </span><span>months</span><span>);</span></span></div></div><div><div><div>117</div></div><div><span>    </span><span>update</span><span>(</span><span>'days'</span><span><span>, </span><span>days</span><span>);</span></span></div></div><div><div><div>118</div></div><div><span>    </span><span>update</span><span>(</span><span>'hours'</span><span><span>, </span><span>hours</span><span>);</span></span></div></div><div><div><div>119</div></div><div><span>    </span><span>update</span><span>(</span><span>'minutes'</span><span><span>, </span><span>minutes</span><span>);</span></span></div></div><div><div><div>120</div></div><div><span>    </span><span>update</span><span>(</span><span>'seconds'</span><span><span>, </span><span>seconds</span><span>.</span></span><span>toString</span><span>().</span><span>padStart</span><span>(</span><span>2</span><span>, </span><span>'0'</span><span>));</span></div></div><div><div><div>121</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>122</div></div><div>
</div></div><div><div><div>123</div></div><div><span>  </span><span>// 立即运行并每秒更新</span></div></div><div><div><div>124</div></div><div><span>  </span><span>updateRelationshipTimer</span><span>();</span></div></div><div><div><div>125</div></div><div><span>  </span><span>if</span><span> (</span><span>!</span><span><span>window</span><span>.</span></span><span>relationshipTimerInterval</span><span>) {</span></div></div><div><div><div>126</div></div><div><span><span>    </span></span><span>window</span><span>.</span><span>relationshipTimerInterval</span><span> </span><span>=</span><span> </span><span>setInterval</span><span><span>(</span><span>updateRelationshipTimer</span><span>, </span></span><span>1000</span><span>);</span></div></div><div><div><div>127</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>128</div></div><div>
</div></div><div><div><div>129</div></div><div><span>  </span><span>// 监听 Swup 页面切换</span></div></div><div><div><div>130</div></div><div><span><span>  </span></span><span>document</span><span>.</span><span>addEventListener</span><span>(</span><span>'swup:content:replace'</span><span><span>, </span><span>updateRelationshipTimer</span><span>);</span></span></div></div><div><div><div>131</div></div><div><span>&lt;/</span><span>script</span><span>&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>注册组件<a href="#注册组件"><span>#</span></a></h2><p>文件路径：<code>src/components/layout/SideBar.astro</code></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>---</span></div></div><div><div><div>2</div></div><div><span>import</span><span> </span><span>RelationshipTimer</span><span> </span><span>from</span><span> </span><span>"@/components/widget/RelationshipTimer.astro"</span><span>;  </span><span>// 引入恋爱计时组件</span></div></div><div><div><div>3</div></div><div><span>---</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>// 组件映射表</span></div></div><div><div><div>6</div></div><div><span>const componentMap = </span><span>{</span></div></div><div><div><div>7</div></div><div><span>    </span><span>&lt;!--</span><span><span> </span><span>恋爱计时组件</span><span> </span></span><span>--&gt;</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>relationship</span><span>: </span><span>RelationshipTimer</span><span>,</span></div></div><div><div><div>9</div></div><div><span>}</span><span> satisfies Record&lt;</span><span>WidgetComponentType</span><span>, </span><span>typeof</span><span> </span><span>Profile</span><span>&gt;;</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>启用组件<a href="#启用组件"><span>#</span></a></h2><p>文件路径：<code>src/config/sidebarConfig.ts</code></p><ul>
<li>右侧边栏配置</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>type</span><span>: </span><span>"relationship"</span><span>,    </span><span>// 组件类型，与 componentMap 中的 key 对应</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>enable</span><span>: </span><span>true</span><span>,            </span><span>// 是否启用</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>position</span><span>: </span><span>"top"</span><span>,         </span><span>// 组件位置：top（固定顶部）或 sticky（粘性定位）</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>showOnPostPage</span><span>: </span><span>true</span><span>,    </span><span>// 是否在文章详情页显示</span></div></div><div><div><div>6</div></div><div><span>}</span><span>,</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>移动端配置</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>// 组件类型：恋爱计时小组件</span></div></div><div><div><div>3</div></div><div><span><span>type</span><span>: </span></span><span>"relationship"</span><span>,</span></div></div><div><div><div>4</div></div><div><span>// 是否启用该组件</span></div></div><div><div><div>5</div></div><div><span><span>enable</span><span>: </span></span><span>true</span><span>,</span></div></div><div><div><div>6</div></div><div><span>// 是否在文章详情页显示</span></div></div><div><div><div>7</div></div><div><span><span>showOnPostPage</span><span>: </span></span><span>true</span><span>,</span></div></div><div><div><div>8</div></div><div><span>}</span><span>,</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>配置类型定义<a href="#配置类型定义"><span>#</span></a></h2><p>文件路径：<code>src/types/config.ts</code></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// 组件配置类型定义</span></div></div><div><div><div>2</div></div><div><span>export</span><span> </span><span>type</span><span> </span><span>WidgetComponentType</span><span> </span><span>=</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span><span> </span><span>"relationship"</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>双方信息配置<a href="#双方信息配置"><span>#</span></a></h2><p>文件路径：<code>src/config/relationshipConfig.ts</code></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> </span><span>type</span><span> { </span><span>RelationshipConfig</span><span> } </span><span>from</span><span> </span><span>"../types/config"</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>export</span><span> </span><span>const</span><span> </span><span>relationshipConfig</span><span>:</span><span> </span><span>RelationshipConfig</span><span> </span><span>=</span><span> {</span></div></div><div><div><div>4</div></div><div><span>  </span><span>// 恋爱开始日期</span></div></div><div><div><div>5</div></div><div><span>  </span><span>startDate</span><span>:</span><span> </span><span>"2026-01-06"</span><span>,</span></div></div><div><div><div>6</div></div><div><span>  </span><span>// 双方名称</span></div></div><div><div><div>7</div></div><div><span>  </span><span>name1</span><span>:</span><span> </span><span>"---------TSH"</span><span>,</span></div></div><div><div><div>8</div></div><div><span>  </span><span>name2</span><span>:</span><span> </span><span>"CXY---------"</span><span>,</span></div></div><div><div><div>9</div></div><div><span>  </span><span>// 双方头像</span></div></div><div><div><div>10</div></div><div><span>  </span><span>avatar1</span><span>:</span><span> </span><span>"https://re.tsh520.cn/zl/tsh.jpg"</span><span>,</span></div></div><div><div><div>11</div></div><div><span>  </span><span>avatar2</span><span>:</span><span> </span><span>"https://re.tsh520.cn/zl/cxy.jpg"</span><span>,</span></div></div><div><div><div>12</div></div><div><span>  </span><span>// 小组件标题</span></div></div><div><div><div>13</div></div><div><span>  </span><span>title</span><span>:</span><span> </span><span>"我和宝宝在一起已经"</span><span>,</span></div></div><div><div><div>14</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>]]></content>
    </entry>
    <entry>
      <id>https://seasir.top/posts/UmamiStats/</id>
      <title type="text">Umami 访问统计卡片</title>
      <published>2026-06-02T00:00:00.000Z</published>
      <updated>2026-06-02T00:00:00.000Z</updated>
      <author><name>Hyde</name></author>
      <link rel="alternate" href="https://seasir.top/posts/UmamiStats/"/>
      <summary type="text">最近给博客侧边栏加了个小彩蛋——Umami 访问统计卡片，不用后端、不破风格，数据实时还带点击跳转。整个过程就是在 Firefly 主题里新建一个组件，填入 Umami 的分享链接，系统自动解析 API 和 Token，再配个备用数据兜底。连统计周期都能按需调，比如改成最近 30 天，改个毫秒数就完事～现在每次打开博客，侧边栏都默默告诉我：“今天又被看了好多眼”。</summary>
      <content type="html"><![CDATA[<div><div><div></div><div>提示</div></div><div><p>本文是在博主 <a href="https://blog.tianhw.top/" target="_blank">THW</a> 文章：<a href="https://blog.tianhw.top/posts/fuwari-umami-stats/" target="_blank">为 Fuwari 添加 Umami 访问统计卡片</a> 基础上增加了自己实践过程的一些细节，转载无需和我联系，但请注明文章来源。如果侵权之处，请联系博主进行删除，谢谢~</p></div></div>
<section><h2>开始<a href="#开始"><span>#</span></a></h2><p>对于静态博客而言，了解访客来源与流量趋势是很有必要的。在不自建服务器的前提下，配置简单，功能强大的 Umami 往往是我们的首选。</p><p>然而，直接挂载 Umami 的分享外链不够直观且破坏页面一致性。本文将教你如何将 Umami 的统计数据以原生组件的形式集成到 Firefly  主题的侧边栏中，让你的博客实时展示访问数据。</p><p></p><figure><img src="https://image.tianhw.top/fuwari-umami-stats/card.webp" alt="Umami 访问统计卡片" /><figcaption>Umami 访问统计卡片</figcaption></figure><p></p></section>
<section><h2>准备工作<a href="#准备工作"><span>#</span></a></h2><ul>
<li>一个已部署好的Firefly博客</li>
<li>无服务器部署Umami统计教程：<a href="https://teek.seasir.top/Teek/Umami#vercel-%E9%83%A8%E7%BD%B2" target="_blank">Vercel 部署</a></li>
<li>启用Umami统计的站点</li>
</ul></section>
<section><h2>获取数据<a href="#获取数据"><span>#</span></a></h2><p>为了获取到图中的这些数据，我们需要启用 Umami 统计的 分享 URL。</p><p>你应该会得到一个类似 <a href="https://umami.seasir.top/share/xxxxxxxxxxxx" target="_blank">https://umami.seasir.top/share/xxxxxxxxxxxx</a> 格式的链接。记下这个链接，现在我们只需要它就能完成所有配置。</p><p></p><figure><img src="https://image.tianhw.top/fuwari-umami-stats/umami.webp" alt="alt text" /><figcaption>alt text</figcaption></figure><p></p></section>
<section><h2>创建组件<a href="#创建组件"><span>#</span></a></h2><p>文件路径：<code>src/components/widget/UmamiStats.astro</code></p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>---</span></div></div><div><div><div>2</div></div><div><span>import</span><span> </span><span>WidgetLayout</span><span> </span><span>from</span><span> </span><span>"@/components/common/WidgetLayout.astro"</span><span>;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>interface</span><span> </span><span>Props</span><span> {</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>class</span><span><span>?</span><span>:</span></span><span> </span><span>string</span><span>;</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>style</span><span><span>?</span><span>:</span></span><span> </span><span>string</span><span>;</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div><div><div><div>8</div></div><div><span>const</span><span> { </span><span>class</span><span>: </span><span>className</span><span>, </span><span>style</span><span> } </span><span>=</span><span><span> </span><span>Astro</span><span>.</span></span><span>props</span><span>;</span></div></div><div><div><div>9</div></div><div><span>---</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>&lt;</span><span>WidgetLayout</span><span> </span><span>id</span><span>=</span><span>"umami-stats"</span><span> </span><span>name</span><span>=</span><span>"统计"</span><span> </span><span>class:list</span><span>=</span><span>{</span><span>[</span><span>"umami-stats-container"</span><span><span>, </span><span>className</span><span>, </span></span><span>"cursor-pointer transition-opacity active:scale-95"</span><span>]</span><span>}</span><span><span> {</span><span>style</span><span>}&gt;</span></span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>&lt;</span><span>a</span><span> </span><span>target</span><span>=</span><span>"_blank"</span><span> </span><span>rel</span><span>=</span><span>"noopener noreferrer"</span><span> </span><span>class</span><span>=</span><span>"block umami-link"</span><span>&gt;</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"text-center py-2"</span><span>&gt;</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"text-3xl font-bold text-neutral-900 dark:text-neutral-100 umami-total-pageviews"</span><span>&gt;-&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"text-sm text-neutral-500 dark:text-neutral-400"</span><span>&gt;总浏览量&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"grid grid-cols-2 divide-x divide-neutral-200 dark:divide-neutral-700 text-center pt-2"</span><span>&gt;</span></div></div><div><div><div>18</div></div><div><span><span>            </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"px-2"</span><span>&gt;</span></div></div><div><div><div>19</div></div><div><span><span>                </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"text-xl font-bold text-neutral-900 dark:text-neutral-100 umami-total-visits"</span><span>&gt;-&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>20</div></div><div><span><span>                </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"text-sm text-neutral-500 dark:text-neutral-400"</span><span>&gt;访问数&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>21</div></div><div><span><span>            </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>22</div></div><div><span><span>            </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"px-2"</span><span>&gt;</span></div></div><div><div><div>23</div></div><div><span><span>                </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"text-xl font-bold text-neutral-900 dark:text-neutral-100 umami-total-visitors"</span><span>&gt;-&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>24</div></div><div><span><span>                </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"text-sm text-neutral-500 dark:text-neutral-400"</span><span>&gt;游客数&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>25</div></div><div><span><span>            </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>&lt;/</span><span>a</span><span>&gt;</span></div></div><div><div><div>28</div></div><div><span>&lt;/</span><span>WidgetLayout</span><span>&gt;</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>&lt;</span><span>script</span><span>&gt;</span></div></div><div><div><div>31</div></div><div><span>const</span><span> </span><span>UMAMI_CONFIG</span><span> </span><span>=</span><span> {</span></div></div><div><div><div>32</div></div><div><span>    </span><span>shareUrl</span><span>:</span><span> </span><span>'https://umami.seasir.top/share/xxxxxxxxxxxx'</span><span>,</span></div></div><div><div><div>33</div></div><div><span>};</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>let</span><span><span> </span><span>__UMAMI_INTERNAL</span><span> </span></span><span>=</span><span> {</span></div></div><div><div><div>36</div></div><div><span>    </span><span>baseUrl</span><span>:</span><span> </span><span>''</span><span>,</span></div></div><div><div><div>37</div></div><div><span>    </span><span>websiteId</span><span>:</span><span> </span><span>''</span><span>,</span></div></div><div><div><div>38</div></div><div><span>    </span><span>shareToken</span><span>:</span><span> </span><span>''</span><span>,</span></div></div><div><div><div>39</div></div><div><span>    </span><span>shareId</span><span>:</span><span> </span><span>''</span><span>,</span></div></div><div><div><div>40</div></div><div><span>    </span><span>isReady</span><span>:</span><span> </span><span>false</span></div></div><div><div><div>41</div></div><div><span>};</span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span>const</span><span> </span><span>FALLBACK_STATS</span><span> </span><span>=</span><span> {</span></div></div><div><div><div>44</div></div><div><span>    </span><span>pageviews</span><span>:</span><span> </span><span>1000</span><span>,</span></div></div><div><div><div>45</div></div><div><span>    </span><span>visits</span><span>:</span><span> </span><span>1000</span><span>,</span></div></div><div><div><div>46</div></div><div><span>    </span><span>visitors</span><span>:</span><span> </span><span>1000</span><span>,</span></div></div><div><div><div>47</div></div><div><span>};</span></div></div><div><div><div>48</div></div><div>
</div></div><div><div><div>49</div></div><div><span>async</span><span> </span><span>function</span><span> </span><span>initUmamiConfig</span><span>() {</span></div></div><div><div><div>50</div></div><div><span>    </span><span>try</span><span> {</span></div></div><div><div><div>51</div></div><div><span>        </span><span>const</span><span> </span><span>sharePath</span><span> </span><span>=</span><span> </span><span>UMAMI_CONFIG</span><span>.</span><span>shareUrl</span><span>.</span><span>split</span><span>(</span><span>'/share/'</span><span>)[</span><span>1</span><span>];</span></div></div><div><div><div>52</div></div><div><span>        </span><span>if</span><span> (</span><span>!</span><span><span>sharePath</span><span>) </span></span><span>throw</span><span> </span><span>new</span><span> </span><span>Error</span><span>(</span><span>'Invalid Umami Share URL'</span><span>);</span></div></div><div><div><div>53</div></div><div>
</div></div><div><div><div>54</div></div><div><span>        </span><span>let</span><span><span> </span><span>apiBase</span><span> </span></span><span>=</span><span> </span><span>''</span><span>;</span></div></div><div><div><div>55</div></div><div><span>        </span><span>if</span><span> (</span><span>UMAMI_CONFIG</span><span>.</span><span>shareUrl</span><span>.</span><span>includes</span><span>(</span><span>'cloud.umami.is'</span><span>) </span><span>||</span><span> </span><span>UMAMI_CONFIG</span><span>.</span><span>shareUrl</span><span>.</span><span>includes</span><span>(</span><span>'analytics.umami.is'</span><span>)) {</span></div></div><div><div><div>56</div></div><div><span>            </span><span>const</span><span> </span><span>region</span><span> </span><span>=</span><span> </span><span>UMAMI_CONFIG</span><span>.</span><span>shareUrl</span><span>.</span><span>includes</span><span>(</span><span>'/analytics/eu/'</span><span>) </span><span>?</span><span> </span><span>'eu'</span><span> </span><span>:</span><span> </span><span>'us'</span><span>;</span></div></div><div><div><div>57</div></div><div><span><span>            </span></span><span>apiBase</span><span> </span><span>=</span><span> </span><span>`https://cloud.umami.is/analytics/</span><span>${</span><span>region</span><span>}</span><span>/api`</span><span>;</span></div></div><div><div><div>58</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>59</div></div><div><span>            </span><span>const</span><span> </span><span>urlObj</span><span> </span><span>=</span><span> </span><span>new</span><span> </span><span>URL</span><span>(</span><span>UMAMI_CONFIG</span><span>.</span><span>shareUrl</span><span>);</span></div></div><div><div><div>60</div></div><div><span><span>            </span></span><span>apiBase</span><span> </span><span>=</span><span> </span><span>`</span><span>${</span><span>urlObj</span><span>.</span><span>origin</span><span>}</span><span>/api`</span><span>;</span></div></div><div><div><div>61</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>62</div></div><div>
</div></div><div><div><div>63</div></div><div><span>        </span><span>const</span><span> </span><span>res</span><span> </span><span>=</span><span> </span><span>await</span><span> </span><span>fetch</span><span>(</span><span>`</span><span>${</span><span>apiBase</span><span>}</span><span>/share/</span><span>${</span><span>sharePath</span><span>}</span><span>`</span><span>);</span></div></div><div><div><div>64</div></div><div><span>        </span><span>if</span><span> (</span><span>!</span><span><span>res</span><span>.</span></span><span>ok</span><span>) </span><span>throw</span><span> </span><span>new</span><span> </span><span>Error</span><span>(</span><span>`Failed to fetch share config: </span><span>${</span><span>res</span><span>.</span><span>status</span><span>}</span><span>`</span><span>);</span></div></div><div><div><div>65</div></div><div><span>        </span><span>const</span><span> </span><span>data</span><span> </span><span>=</span><span> </span><span>await</span><span><span> </span><span>res</span><span>.</span></span><span>json</span><span>();</span></div></div><div><div><div>66</div></div><div>
</div></div><div><div><div>67</div></div><div><span><span>        </span></span><span>__UMAMI_INTERNAL</span><span> </span><span>=</span><span> {</span></div></div><div><div><div>68</div></div><div><span>            </span><span>baseUrl</span><span>:</span><span><span> </span><span>apiBase</span><span>,</span></span></div></div><div><div><div>69</div></div><div><span>            </span><span>websiteId</span><span>:</span><span><span> </span><span>data</span><span>.</span></span><span>websiteId</span><span>,</span></div></div><div><div><div>70</div></div><div><span>            </span><span>shareToken</span><span>:</span><span><span> </span><span>data</span><span>.</span></span><span>token</span><span>,</span></div></div><div><div><div>71</div></div><div><span>            </span><span>shareId</span><span>:</span><span><span> </span><span>data</span><span>.</span></span><span>shareId</span><span>,</span></div></div><div><div><div>72</div></div><div><span>            </span><span>isReady</span><span>:</span><span> </span><span>true</span></div></div><div><div><div>73</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>74</div></div><div>
</div></div><div><div><div>75</div></div><div><span>        </span><span>const</span><span> </span><span>links</span><span> </span><span>=</span><span><span> </span><span>document</span><span>.</span></span><span>querySelectorAll</span><span>(</span><span>'.umami-link'</span><span>);</span></div></div><div><div><div>76</div></div><div><span><span>        </span></span><span>links</span><span>.</span><span>forEach</span><span><span>(</span><span>link</span><span> </span></span><span>=&gt;</span><span><span> </span><span>link</span><span>.</span></span><span>setAttribute</span><span>(</span><span>'href'</span><span>, </span><span>UMAMI_CONFIG</span><span>.</span><span>shareUrl</span><span>));</span></div></div><div><div><div>77</div></div><div>
</div></div><div><div><div>78</div></div><div><span><span>    </span></span><span>} </span><span>catch</span><span><span> (</span><span>e</span><span>) {</span></span></div></div><div><div><div>79</div></div><div><span><span>        </span></span><span>console</span><span>.</span><span>error</span><span>(</span><span>'Umami Config Init Failed:'</span><span><span>, </span><span>e</span><span>);</span></span></div></div><div><div><div>80</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>81</div></div><div><span>}</span></div></div><div><div><div>82</div></div><div>
</div></div><div><div><div>83</div></div><div><span>function</span><span> </span><span>formatNumber</span><span><span>(</span><span>num</span></span><span>:</span><span> </span><span>number</span><span>)</span><span>:</span><span> </span><span>string</span><span> {</span></div></div><div><div><div>84</div></div><div><span>    </span><span>if</span><span><span> (</span><span>num</span><span> </span></span><span>&gt;=</span><span> </span><span>1000000</span><span>) {</span></div></div><div><div><div>85</div></div><div><span>        </span><span>return</span><span><span> (</span><span>num</span><span> </span></span><span>/</span><span> </span><span>1000000</span><span>).</span><span>toFixed</span><span>(</span><span>1</span><span>) </span><span>+</span><span> </span><span>'M'</span><span>;</span></div></div><div><div><div>86</div></div><div><span><span>    </span></span><span>} </span><span>else</span><span> </span><span>if</span><span><span> (</span><span>num</span><span> </span></span><span>&gt;=</span><span> </span><span>1000</span><span>) {</span></div></div><div><div><div>87</div></div><div><span>        </span><span>return</span><span><span> (</span><span>num</span><span> </span></span><span>/</span><span> </span><span>1000</span><span>).</span><span>toFixed</span><span>(</span><span>1</span><span>) </span><span>+</span><span> </span><span>'K'</span><span>;</span></div></div><div><div><div>88</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>89</div></div><div><span>    </span><span>return</span><span><span> </span><span>Math</span><span>.</span></span><span>round</span><span><span>(</span><span>num</span><span>).</span></span><span>toString</span><span>();</span></div></div><div><div><div>90</div></div><div><span>}</span></div></div><div><div><div>91</div></div><div>
</div></div><div><div><div>92</div></div><div><span>function</span><span> </span><span>setStats</span><span><span>(</span><span>values</span></span><span>:</span><span><span> { </span><span>pageviews</span></span><span>:</span><span> </span><span>number</span><span><span>; </span><span>visits</span></span><span>:</span><span> </span><span>number</span><span><span>; </span><span>visitors</span></span><span>:</span><span> </span><span>number</span><span> }) {</span></div></div><div><div><div>93</div></div><div><span>    </span><span>const</span><span> </span><span>pageviewsElements</span><span> </span><span>=</span><span><span> </span><span>document</span><span>.</span></span><span>querySelectorAll</span><span>(</span><span>'.umami-total-pageviews'</span><span>);</span></div></div><div><div><div>94</div></div><div><span>    </span><span>const</span><span> </span><span>visitsElements</span><span> </span><span>=</span><span><span> </span><span>document</span><span>.</span></span><span>querySelectorAll</span><span>(</span><span>'.umami-total-visits'</span><span>);</span></div></div><div><div><div>95</div></div><div><span>    </span><span>const</span><span> </span><span>visitorsElements</span><span> </span><span>=</span><span><span> </span><span>document</span><span>.</span></span><span>querySelectorAll</span><span>(</span><span>'.umami-total-visitors'</span><span>);</span></div></div><div><div><div>96</div></div><div>
</div></div><div><div><div>97</div></div><div><span>    </span><span>const</span><span> </span><span>easeOutCubic</span><span> </span><span>=</span><span><span> (</span><span>t</span></span><span>:</span><span> </span><span>number</span><span>) </span><span>=&gt;</span><span> </span><span>1</span><span> </span><span>-</span><span><span> </span><span>Math</span><span>.</span></span><span>pow</span><span>(</span><span>1</span><span> </span><span>-</span><span><span> </span><span>t</span><span>, </span></span><span>3</span><span>);</span></div></div><div><div><div>98</div></div><div><span>    </span><span>const</span><span> </span><span>animHandles</span><span> </span><span>=</span><span> </span><span>new</span><span> </span><span>Map</span><span>&lt;</span><span>HTMLElement</span><span><span>, </span><span>number</span><span>&gt;();</span></span></div></div><div><div><div>99</div></div><div>
</div></div><div><div><div>100</div></div><div><span>    </span><span>const</span><span> </span><span>animateStat</span><span> </span><span>=</span><span><span> (</span><span>el</span></span><span>:</span><span> </span><span>HTMLElement</span><span> </span><span>|</span><span> </span><span>null</span><span><span>, </span><span>to</span></span><span>:</span><span> </span><span>number</span><span><span>, </span><span>duration</span><span> </span></span><span>=</span><span> </span><span>2000</span><span>) </span><span>=&gt;</span><span> {</span></div></div><div><div><div>101</div></div><div><span>        </span><span>if</span><span> (</span><span>!</span><span><span>el</span><span>) </span></span><span>return</span><span>;</span></div></div><div><div><div>102</div></div><div>
</div></div><div><div><div>103</div></div><div><span>        </span><span>const</span><span> </span><span>prev</span><span> </span><span>=</span><span><span> </span><span>animHandles</span><span>.</span></span><span>get</span><span><span>(</span><span>el</span><span>);</span></span></div></div><div><div><div>104</div></div><div><span>        </span><span>if</span><span><span> (</span><span>prev</span><span>) </span></span><span>cancelAnimationFrame</span><span><span>(</span><span>prev</span><span>);</span></span></div></div><div><div><div>105</div></div><div>
</div></div><div><div><div>106</div></div><div><span>        </span><span>const</span><span> </span><span>from</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>107</div></div><div><span>        </span><span>const</span><span> </span><span>startTime</span><span> </span><span>=</span><span><span> </span><span>performance</span><span>.</span></span><span>now</span><span>();</span></div></div><div><div><div>108</div></div><div>
</div></div><div><div><div>109</div></div><div><span>        </span><span>const</span><span> </span><span>tick</span><span> </span><span>=</span><span><span> (</span><span>now</span></span><span>:</span><span> </span><span>number</span><span>) </span><span>=&gt;</span><span> {</span></div></div><div><div><div>110</div></div><div><span>            </span><span>const</span><span> </span><span>elapsed</span><span> </span><span>=</span><span><span> </span><span>now</span><span> </span></span><span>-</span><span><span> </span><span>startTime</span><span>;</span></span></div></div><div><div><div>111</div></div><div><span>            </span><span>const</span><span> </span><span>progress</span><span> </span><span>=</span><span><span> </span><span>Math</span><span>.</span></span><span>min</span><span>(</span><span>1</span><span><span>, </span><span>elapsed</span><span> </span></span><span>/</span><span><span> </span><span>duration</span><span>);</span></span></div></div><div><div><div>112</div></div><div><span>            </span><span>const</span><span> </span><span>easedProgress</span><span> </span><span>=</span><span> </span><span>easeOutCubic</span><span><span>(</span><span>progress</span><span>);</span></span></div></div><div><div><div>113</div></div><div>
</div></div><div><div><div>114</div></div><div><span>            </span><span>const</span><span> </span><span>current</span><span> </span><span>=</span><span><span> </span><span>from</span><span> </span></span><span>+</span><span><span> (</span><span>to</span><span> </span></span><span>-</span><span><span> </span><span>from</span><span>) </span></span><span>*</span><span><span> </span><span>easedProgress</span><span>;</span></span></div></div><div><div><div>115</div></div><div><span><span>            </span></span><span>el</span><span>.</span><span>textContent</span><span> </span><span>=</span><span> </span><span>formatNumber</span><span><span>(</span><span>current</span><span>);</span></span></div></div><div><div><div>116</div></div><div>
</div></div><div><div><div>117</div></div><div><span>            </span><span>if</span><span><span> (</span><span>progress</span><span> </span></span><span>&lt;</span><span> </span><span>1</span><span>) {</span></div></div><div><div><div>118</div></div><div><span><span>                </span></span><span>animHandles</span><span>.</span><span>set</span><span><span>(</span><span>el</span><span>, </span></span><span>requestAnimationFrame</span><span><span>(</span><span>tick</span><span>));</span></span></div></div><div><div><div>119</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>120</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>121</div></div><div><span><span>        </span></span><span>animHandles</span><span>.</span><span>set</span><span><span>(</span><span>el</span><span>, </span></span><span>requestAnimationFrame</span><span><span>(</span><span>tick</span><span>));</span></span></div></div><div><div><div>122</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>123</div></div><div>
</div></div><div><div><div>124</div></div><div><span><span>    </span></span><span>pageviewsElements</span><span>.</span><span>forEach</span><span><span>(</span><span>el</span><span> </span></span><span>=&gt;</span><span> </span><span>animateStat</span><span><span>(</span><span>el</span><span> </span><span>as</span><span> </span></span><span>HTMLElement</span><span><span>, </span><span>values</span><span>.</span></span><span>pageviews</span><span>));</span></div></div><div><div><div>125</div></div><div><span><span>    </span></span><span>visitsElements</span><span>.</span><span>forEach</span><span><span>(</span><span>el</span><span> </span></span><span>=&gt;</span><span> </span><span>animateStat</span><span><span>(</span><span>el</span><span> </span><span>as</span><span> </span></span><span>HTMLElement</span><span><span>, </span><span>values</span><span>.</span></span><span>visits</span><span>));</span></div></div><div><div><div>126</div></div><div><span><span>    </span></span><span>visitorsElements</span><span>.</span><span>forEach</span><span><span>(</span><span>el</span><span> </span></span><span>=&gt;</span><span> </span><span>animateStat</span><span><span>(</span><span>el</span><span> </span><span>as</span><span> </span></span><span>HTMLElement</span><span><span>, </span><span>values</span><span>.</span></span><span>visitors</span><span>));</span></div></div><div><div><div>127</div></div><div><span>}</span></div></div><div><div><div>128</div></div><div>
</div></div><div><div><div>129</div></div><div><span>async</span><span> </span><span>function</span><span> </span><span>fetchUmamiStats</span><span>() {</span></div></div><div><div><div>130</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span><span>__UMAMI_INTERNAL</span><span>.</span></span><span>isReady</span><span>) {</span></div></div><div><div><div>131</div></div><div><span>        </span><span>await</span><span> </span><span>initUmamiConfig</span><span>();</span></div></div><div><div><div>132</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>133</div></div><div>
</div></div><div><div><div>134</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span><span>__UMAMI_INTERNAL</span><span>.</span></span><span>isReady</span><span>) {</span></div></div><div><div><div>135</div></div><div><span>        </span><span>setStats</span><span>(</span><span>FALLBACK_STATS</span><span>);</span></div></div><div><div><div>136</div></div><div><span>        </span><span>return</span><span>;</span></div></div><div><div><div>137</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>138</div></div><div>
</div></div><div><div><div>139</div></div><div><span>    </span><span>try</span><span> {</span></div></div><div><div><div>140</div></div><div><span>        </span><span>const</span><span> </span><span>endAt</span><span> </span><span>=</span><span><span> </span><span>Date</span><span>.</span></span><span>now</span><span>();</span></div></div><div><div><div>141</div></div><div><span>        </span><span>const</span><span> </span><span>startAt</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>142</div></div><div><span>        </span><span>const</span><span> </span><span>url</span><span> </span><span>=</span><span> </span><span>`</span><span>${</span><span>__UMAMI_INTERNAL</span><span>.</span><span>baseUrl</span><span>}</span><span>/websites/</span><span>${</span><span>__UMAMI_INTERNAL</span><span>.</span><span>websiteId</span><span>}</span><span>/stats?startAt=</span><span>${</span><span>startAt</span><span>}</span><span>&amp;endAt=</span><span>${</span><span>endAt</span><span>}</span><span>&amp;unit=hour&amp;timezone=Asia%2FShanghai`</span><span>;</span></div></div><div><div><div>143</div></div><div>
</div></div><div><div><div>144</div></div><div><span>        </span><span>const</span><span> </span><span>response</span><span> </span><span>=</span><span> </span><span>await</span><span> </span><span>fetch</span><span><span>(</span><span>url</span><span>, {</span></span></div></div><div><div><div>145</div></div><div><span>            </span><span>headers</span><span>:</span><span> {</span></div></div><div><div><div>146</div></div><div><span>                </span><span>'x-umami-share-context'</span><span>:</span><span> </span><span>'1'</span><span>,</span></div></div><div><div><div>147</div></div><div><span>                </span><span>'x-umami-share-token'</span><span>:</span><span><span> </span><span>__UMAMI_INTERNAL</span><span>.</span></span><span>shareToken</span></div></div><div><div><div>148</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>149</div></div><div><span><span>        </span></span><span>});</span></div></div><div><div><div>150</div></div><div>
</div></div><div><div><div>151</div></div><div><span>        </span><span>if</span><span> (</span><span>!</span><span><span>response</span><span>.</span></span><span>ok</span><span>) </span><span>throw</span><span> </span><span>new</span><span> </span><span>Error</span><span>(</span><span>`HTTP </span><span>${</span><span>response</span><span>.</span><span>status</span><span>}</span><span>`</span><span>);</span></div></div><div><div><div>152</div></div><div><span>        </span><span>const</span><span> </span><span>data</span><span> </span><span>=</span><span> </span><span>await</span><span><span> </span><span>response</span><span>.</span></span><span>json</span><span>();</span></div></div><div><div><div>153</div></div><div><span>        </span><span>const</span><span> </span><span>getValue</span><span> </span><span>=</span><span><span> (</span><span>field</span></span><span>:</span><span> </span><span>any</span><span>) </span><span>=&gt;</span><span> (</span><span>typeof</span><span><span> </span><span>field</span><span> </span></span><span>===</span><span> </span><span>'object'</span><span> </span><span>?</span><span><span> </span><span>field</span><span>?.</span></span><span>value</span><span> </span><span>:</span><span><span> </span><span>field</span><span>) </span></span><span>||</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>154</div></div><div>
</div></div><div><div><div>155</div></div><div><span>        </span><span>setStats</span><span>({</span></div></div><div><div><div>156</div></div><div><span>            </span><span>pageviews</span><span>:</span><span> </span><span>getValue</span><span><span>(</span><span>data</span><span>.</span></span><span>pageviews</span><span>),</span></div></div><div><div><div>157</div></div><div><span>            </span><span>visits</span><span>:</span><span> </span><span>getValue</span><span><span>(</span><span>data</span><span>.</span></span><span>visits</span><span>),</span></div></div><div><div><div>158</div></div><div><span>            </span><span>visitors</span><span>:</span><span> </span><span>getValue</span><span><span>(</span><span>data</span><span>.</span></span><span>visitors</span><span>),</span></div></div><div><div><div>159</div></div><div><span><span>        </span></span><span>});</span></div></div><div><div><div>160</div></div><div>
</div></div><div><div><div>161</div></div><div><span><span>    </span></span><span>} </span><span>catch</span><span><span> (</span><span>error</span><span>) {</span></span></div></div><div><div><div>162</div></div><div><span><span>        </span></span><span>console</span><span>.</span><span>error</span><span>(</span><span>'Umami Fetch Failed:'</span><span><span>, </span><span>error</span><span>);</span></span></div></div><div><div><div>163</div></div><div><span>        </span><span>setStats</span><span>(</span><span>FALLBACK_STATS</span><span>);</span></div></div><div><div><div>164</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>165</div></div><div><span>}</span></div></div><div><div><div>166</div></div><div>
</div></div><div><div><div>167</div></div><div><span>let</span><span><span> </span><span>__umamiStatsStarted</span><span> </span></span><span>=</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>168</div></div><div><span>function</span><span> </span><span>startUmamiStats</span><span>() {</span></div></div><div><div><div>169</div></div><div><span>    </span><span>if</span><span><span> (</span><span>__umamiStatsStarted</span><span>) </span></span><span>return</span><span>;</span></div></div><div><div><div>170</div></div><div><span><span>    </span></span><span>__umamiStatsStarted</span><span> </span><span>=</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>171</div></div><div><span>    </span><span>fetchUmamiStats</span><span>();</span></div></div><div><div><div>172</div></div><div><span>}</span></div></div><div><div><div>173</div></div><div>
</div></div><div><div><div>174</div></div><div><span>function</span><span> </span><span>initUmamiStatsVisibility</span><span>() {</span></div></div><div><div><div>175</div></div><div><span>    </span><span>const</span><span> </span><span>containers</span><span> </span><span>=</span><span><span> </span><span>document</span><span>.</span></span><span>querySelectorAll</span><span>(</span><span>'.umami-stats-container'</span><span>);</span></div></div><div><div><div>176</div></div><div><span>    </span><span>const</span><span> </span><span>io</span><span> </span><span>=</span><span> </span><span>new</span><span> </span><span>IntersectionObserver</span><span><span>((</span><span>entries</span><span>) </span></span><span>=&gt;</span><span> {</span></div></div><div><div><div>177</div></div><div><span>        </span><span>let</span><span><span> </span><span>isAnyVisible</span><span> </span></span><span>=</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>178</div></div><div><span><span>        </span></span><span>entries</span><span>.</span><span>forEach</span><span><span>(</span><span>entry</span><span> </span></span><span>=&gt;</span><span> {</span></div></div><div><div><div>179</div></div><div><span>            </span><span>if</span><span><span> (</span><span>entry</span><span>.</span></span><span>isIntersecting</span><span><span>) </span><span>isAnyVisible</span><span> </span></span><span>=</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>180</div></div><div><span><span>        </span></span><span>});</span></div></div><div><div><div>181</div></div><div>
</div></div><div><div><div>182</div></div><div><span>        </span><span>if</span><span><span> (</span><span>isAnyVisible</span><span>) {</span></span></div></div><div><div><div>183</div></div><div><span>            </span><span>startUmamiStats</span><span>();</span></div></div><div><div><div>184</div></div><div><span><span>            </span></span><span>io</span><span>.</span><span>disconnect</span><span>();</span></div></div><div><div><div>185</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>186</div></div><div><span><span>    </span></span><span>}, { </span><span>threshold</span><span>:</span><span> </span><span>0.1</span><span> });</span></div></div><div><div><div>187</div></div><div>
</div></div><div><div><div>188</div></div><div><span><span>    </span></span><span>containers</span><span>.</span><span>forEach</span><span><span>(</span><span>container</span><span> </span></span><span>=&gt;</span><span><span> </span><span>io</span><span>.</span></span><span>observe</span><span><span>(</span><span>container</span><span>));</span></span></div></div><div><div><div>189</div></div><div><span>}</span></div></div><div><div><div>190</div></div><div>
</div></div><div><div><div>191</div></div><div><span>initUmamiStatsVisibility</span><span>();</span></div></div><div><div><div>192</div></div><div>
</div></div><div><div><div>193</div></div><div><span>if</span><span><span> (</span><span>window</span><span>.</span></span><span>swup</span><span>) {</span></div></div><div><div><div>194</div></div><div><span><span>    </span></span><span>window</span><span>.</span><span>swup</span><span>.</span><span>hooks</span><span>.</span><span>on</span><span>(</span><span>'page:view'</span><span>, () </span><span>=&gt;</span><span> {</span></div></div><div><div><div>195</div></div><div><span><span>        </span></span><span>__umamiStatsStarted</span><span> </span><span>=</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>196</div></div><div><span>        </span><span>initUmamiStatsVisibility</span><span>();</span></div></div><div><div><div>197</div></div><div><span><span>    </span></span><span>});</span></div></div><div><div><div>198</div></div><div><span>}</span></div></div><div><div><div>199</div></div><div><span>&lt;/</span><span>script</span><span>&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>配置参数<a href="#配置参数"><span>#</span></a></h2><p>在代码文件的 script 部分，填入你的分享链接：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>const</span><span> </span><span>UMAMI_CONFIG</span><span> </span><span>=</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>shareUrl</span><span>:</span><span> </span><span>'https://umami.seasir.top/share/xxxxxxxxxxxx'</span><span>,  </span><span>// ← 这里改成你自己的</span></div></div><div><div><div>3</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>系统会自动解析 API 路径、Website ID 和 验证 Token。</p></section>
<section><h2>注册组件<a href="#注册组件"><span>#</span></a></h2><p>文件路径：<code>src/components/layout/SideBar.astro</code></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>---</span></div></div><div><div><div>2</div></div><div><span>import</span><span> </span><span>UmamiStats</span><span> </span><span>from</span><span> </span><span>"@/components/widget/UmamiStats.astro"</span><span>;  </span><span>// 引入 Umami 组件</span></div></div><div><div><div>3</div></div><div><span>---</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>// 组件映射表</span></div></div><div><div><div>6</div></div><div><span>const componentMap = </span><span>{</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>umamiStats</span><span>: </span><span>UmamiStats</span><span>,</span></div></div><div><div><div>8</div></div><div><span>}</span><span> satisfies Record&lt;</span><span>WidgetComponentType</span><span>, </span><span>typeof</span><span> </span><span>Profile</span><span>&gt;;</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>配置侧边栏<a href="#配置侧边栏"><span>#</span></a></h2><p>文件路径：<code>src/config/sidebarConfig.ts</code></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>    </span><span>// 组件类型：Umami 统计组件</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>type</span><span>: </span><span>"umamiStats"</span><span>,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>// 是否启用该组件</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>enable</span><span>: </span><span>true</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>// 组件位置</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>position</span><span>: </span><span>"top"</span><span>,</span></div></div><div><div><div>8</div></div><div><span>    </span><span>// 是否在文章详情页显示</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>showOnPostPage</span><span>: </span><span>true</span><span>,</span></div></div><div><div><div>10</div></div><div><span>},</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>修改统计周期<a href="#修改统计周期"><span>#</span></a></h2><p>默认为 所有时间 ，如果你想要调整浏览量统计的时间范围，可以修改 fetchUmamiStats 函数内的 startAt 参数。</p><p>常用时间周期公式（单位：毫秒）：</p><p>24小时：24 * 60 * 60 * 1000 = 86400000</p><p>最近30天：30 * 24 * 60 * 60 * 1000 = 2592000000</p><p>最近90天：90 * 24 * 60 * 60 * 1000 = 7776000000</p><p>修改示例（例如改为最近 30 天）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>try</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>const</span><span> </span><span>endAt</span><span> </span><span>=</span><span><span> </span><span>Date</span><span>.</span></span><span>now</span><span>();</span></div></div><div><div><div>3</div></div><div><span>    </span><span>const</span><span> </span><span>startAt</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>// [!code --]</span></div></div><div><div><div>4</div></div><div><span>    </span><span>const</span><span> </span><span>startAt</span><span> </span><span>=</span><span> </span><span>Date</span><span>.</span><span>now</span><span>() </span><span>-</span><span> </span><span>2592000000</span><span>; </span><span>//[!code ++]</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这将从 30 天前开始统计浏览量。</p><p>##配置备用数据</p><p>如果遇到 API 请求失败，全新的组件会显示你配置的备用数据以保持 UI 美观。你可以在 FALLBACK_STATS 对象中修改这些数值（默认为1000）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>const</span><span> </span><span>FALLBACK_STATS</span><span> </span><span>=</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>pageviews</span><span>:</span><span> </span><span>1000</span><span>, </span><span>// 备用总浏览量</span></div></div><div><div><div>3</div></div><div><span>    </span><span>visits</span><span>:</span><span> </span><span>1000</span><span>,    </span><span>// 备用访问数</span></div></div><div><div><div>4</div></div><div><span>    </span><span>visitors</span><span>:</span><span> </span><span>1000</span><span>,  </span><span>// 备用游客数</span></div></div><div><div><div>5</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>结尾<a href="#结尾"><span>#</span></a></h2><p>通过以上步骤，你就成功为 Fuwari 添加了具有丰富交互感、自动解析配置且支持点击查看详情的 Umami 统计卡片。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://seasir.top/posts/WelcomePrompt/</id>
      <title type="text">欢迎提示组件</title>
      <published>2026-06-02T00:00:00.000Z</published>
      <updated>2026-06-02T00:00:00.000Z</updated>
      <author><name>Hyde</name></author>
      <link rel="alternate" href="https://seasir.top/posts/WelcomePrompt/"/>
      <summary type="text">最近在折腾 Firefly 主题的欢迎提示组件，基于年华博主的 WelcomeToast 分析做了些小调整和实践补充——比如让弹窗只在首页刷新时出现、避免重复打扰访客，还整理了创建、引入和配置显示逻辑的完整步骤，代码都贴出来了，方便自己回头查，也希望能帮到同样想定制欢迎体验的朋友～</summary>
      <content type="html"><![CDATA[<div><div><div></div><div>提示</div></div><div><p>本文是在博主 <a href="https://blog.520781.xyz/" target="_blank">年华</a> 文章：<a href="https://blog.520781.xyz/posts/firefly_welcometoast/" target="_blank">Firefly 主题 WelcomeToast 组件分析</a> 基础上增加了自己实践过程的一些细节，转载无需和我联系，但请注明文章来源。如果侵权之处，请联系博主进行删除，谢谢~</p></div></div>
<p></p><figure><img src="https://blog.520781.xyz/_astro/welcome.CqPgWiqc_SONnY.webp" alt="欢迎提示组件" /><figcaption>欢迎提示组件</figcaption></figure><p></p>
<section><h2>创建组件<a href="#创建组件"><span>#</span></a></h2><p>文件路径：<code>src/components/widget/WelcomeToast.astro</code></p><div><div><div><figure><figcaption><span>src/components/WelcomeToast.astro</span><span>展开</span><span>收起</span></figcaption><pre><code><div><div><div>1</div></div><div><span>&lt;</span><span>script</span><span>&gt;</span></div></div><div><div><div>2</div></div><div><span>    </span><span>// const VISIT_SESSION_KEY = 'blog_visit_flag';</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span><span> </span><span>hasShownToast</span><span> </span></span><span>=</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>    </span><span>function</span><span> </span><span>createWelcomeToast</span><span>() {</span></div></div><div><div><div>6</div></div><div><span>        </span><span>const</span><span> </span><span>toast</span><span> </span><span>=</span><span><span> </span><span>document</span><span>.</span></span><span>createElement</span><span>(</span><span>'div'</span><span>);</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>toast</span><span>.</span><span>id</span><span> </span><span>=</span><span> </span><span>'welcome-toast'</span><span>;</span></div></div><div><div><div>8</div></div><div><span>        </span><span>// Tailwind 样式控制：自动适配深色模式</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>toast</span><span>.</span><span>className</span><span> </span><span>=</span><span> </span><span>'fixed bottom-4 right-4 z-50 transform translate-y-full opacity-0 transition-all duration-500 ease-out'</span><span>;</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>        </span><span>//使用 bg-white dark:bg-zinc-800 强制适配颜色</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>toast</span><span>.</span><span>innerHTML</span><span> </span><span>=</span><span> </span><span>`</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>&lt;div class="bg-white dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-lg shadow-lg p-4 max-w-xs"&gt;</span></div></div><div><div><div>14</div></div><div><span><span>                </span></span><span>&lt;div class="flex items-start gap-3"&gt;</span></div></div><div><div><div>15</div></div><div><span><span>                    </span></span><span>&lt;div class="flex-shrink-0 text-2xl"&gt;👋&lt;/div&gt;</span></div></div><div><div><div>16</div></div><div><span><span>                    </span></span><span>&lt;div class="flex-1 min-w-0"&gt;</span></div></div><div><div><div>17</div></div><div><span><span>                        </span></span><span>&lt;p id="welcome-message" class="text-sm font-semibold text-gray-900 dark:text-gray-100"&gt;</span></div></div><div><div><div>18</div></div><div><span><span>                            </span></span><span>正在加载...</span></div></div><div><div><div>19</div></div><div><span><span>                        </span></span><span>&lt;/p&gt;</span></div></div><div><div><div>20</div></div><div><span><span>                        </span></span><span>&lt;p class="text-xs text-gray-600 dark:text-gray-400 mt-2 italic"&gt;</span></div></div><div><div><div>21</div></div><div><span><span>                            </span></span><span>欢迎来到我的博客</span></div></div><div><div><div>22</div></div><div><span><span>                        </span></span><span>&lt;/p&gt;</span></div></div><div><div><div>23</div></div><div><span><span>                    </span></span><span>&lt;/div&gt;</span></div></div><div><div><div>24</div></div><div><span><span>                    </span></span><span>&lt;button</span></div></div><div><div><div>25</div></div><div><span><span>                        </span></span><span>onclick="closeWelcomeToast()"</span></div></div><div><div><div>26</div></div><div><span><span>                        </span></span><span>class="flex-shrink-0 text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 transition-colors"</span></div></div><div><div><div>27</div></div><div><span><span>                        </span></span><span>aria-label="关闭"</span></div></div><div><div><div>28</div></div><div><span><span>                    </span></span><span>&gt;</span></div></div><div><div><div>29</div></div><div><span><span>                        </span></span><span>&lt;svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"&gt;</span></div></div><div><div><div>30</div></div><div><span><span>                            </span></span><span>&lt;path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"&gt;&lt;/path&gt;</span></div></div><div><div><div>31</div></div><div><span><span>                        </span></span><span>&lt;/svg&gt;</span></div></div><div><div><div>32</div></div><div><span><span>                    </span></span><span>&lt;/button&gt;</span></div></div><div><div><div>33</div></div><div><span><span>                </span></span><span>&lt;/div&gt;</span></div></div><div><div><div>34</div></div><div><span><span>            </span></span><span>&lt;/div&gt;</span></div></div><div><div><div>35</div></div><div><span><span>        </span></span><span>`</span><span>;</span></div></div><div><div><div>36</div></div><div><span><span>        </span></span><span>document</span><span>.</span><span>body</span><span>.</span><span>appendChild</span><span><span>(</span><span>toast</span><span>);</span></span></div></div><div><div><div>37</div></div><div><span>        </span><span>return</span><span><span> </span><span>toast</span><span>;</span></span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>    </span><span>function</span><span> </span><span>closeWelcomeToast</span><span>() {</span></div></div><div><div><div>41</div></div><div><span>        </span><span>const</span><span> </span><span>toast</span><span> </span><span>=</span><span><span> </span><span>document</span><span>.</span></span><span>getElementById</span><span>(</span><span>'welcome-toast'</span><span>);</span></div></div><div><div><div>42</div></div><div><span>        </span><span>if</span><span><span> (</span><span>toast</span><span>) {</span></span></div></div><div><div><div>43</div></div><div><span><span>            </span></span><span>toast</span><span>.</span><span>classList</span><span>.</span><span>add</span><span>(</span><span>'translate-y-full'</span><span>, </span><span>'opacity-0'</span><span>);</span></div></div><div><div><div>44</div></div><div><span><span>            </span></span><span>toast</span><span>.</span><span>classList</span><span>.</span><span>remove</span><span>(</span><span>'translate-y-0'</span><span>, </span><span>'opacity-100'</span><span>);</span></div></div><div><div><div>45</div></div><div><span>            </span><span>setTimeout</span><span>(() </span><span>=&gt;</span><span><span> { </span><span>toast</span><span>.</span></span><span>remove</span><span>(); }, </span><span>500</span><span>);</span></div></div><div><div><div>46</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>48</div></div><div>
</div></div><div><div><div>49</div></div><div><span>    </span><span>async</span><span> </span><span>function</span><span> </span><span>fetchLocation</span><span>() {</span></div></div><div><div><div>50</div></div><div><span>        </span><span>try</span><span> {</span></div></div><div><div><div>51</div></div><div><span>            </span><span>const</span><span> </span><span>response</span><span> </span><span>=</span><span> </span><span>await</span><span> </span><span>fetch</span><span>(</span><span>'https://v2.xxapi.cn/api/ip'</span><span>);</span></div></div><div><div><div>52</div></div><div><span>            </span><span>const</span><span> </span><span>data</span><span> </span><span>=</span><span> </span><span>await</span><span><span> </span><span>response</span><span>.</span></span><span>json</span><span>();</span></div></div><div><div><div>53</div></div><div><span>            </span><span>if</span><span><span> (</span><span>data</span><span>.</span></span><span>code</span><span> </span><span>===</span><span> </span><span>200</span><span> </span><span>&amp;&amp;</span><span><span> </span><span>data</span><span>.</span></span><span>data</span><span>) {</span></div></div><div><div><div>54</div></div><div><span>                </span><span>const</span><span> </span><span>locationText</span><span> </span><span>=</span><span> </span><span>`你好，来自</span><span>${</span><span>data</span><span>.</span><span>data</span><span>.</span><span>address</span><span>}</span><span>的朋友`</span><span>;</span></div></div><div><div><div>55</div></div><div><span>                </span><span>const</span><span> </span><span>messageEl</span><span> </span><span>=</span><span><span> </span><span>document</span><span>.</span></span><span>getElementById</span><span>(</span><span>'welcome-message'</span><span>);</span></div></div><div><div><div>56</div></div><div><span>                </span><span>if</span><span><span> (</span><span>messageEl</span><span>) </span><span>messageEl</span><span>.</span></span><span>textContent</span><span> </span><span>=</span><span><span> </span><span>locationText</span><span>;</span></span></div></div><div><div><div>57</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>58</div></div><div><span><span>        </span></span><span>} </span><span>catch</span><span><span> (</span><span>error</span><span>) {</span></span></div></div><div><div><div>59</div></div><div><span>            </span><span>const</span><span> </span><span>messageEl</span><span> </span><span>=</span><span><span> </span><span>document</span><span>.</span></span><span>getElementById</span><span>(</span><span>'welcome-message'</span><span>);</span></div></div><div><div><div>60</div></div><div><span>            </span><span>if</span><span><span> (</span><span>messageEl</span><span>) </span><span>messageEl</span><span>.</span></span><span>textContent</span><span> </span><span>=</span><span> </span><span>'你好，欢迎来到我的博客'</span><span>;</span></div></div><div><div><div>61</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>62</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>63</div></div><div>
</div></div><div><div><div>64</div></div><div><span>    </span><span>async</span><span> </span><span>function</span><span> </span><span>showWelcomeToast</span><span>() {</span></div></div><div><div><div>65</div></div><div><span>        </span><span>if</span><span><span> (</span><span>hasShownToast</span><span>) </span></span><span>return</span><span>;</span></div></div><div><div><div>66</div></div><div><span>        </span><span>const</span><span> </span><span>toast</span><span> </span><span>=</span><span> </span><span>createWelcomeToast</span><span>();</span></div></div><div><div><div>67</div></div><div><span>        </span><span>setTimeout</span><span>(() </span><span>=&gt;</span><span> {</span></div></div><div><div><div>68</div></div><div><span><span>            </span></span><span>toast</span><span>.</span><span>classList</span><span>.</span><span>remove</span><span>(</span><span>'translate-y-full'</span><span>, </span><span>'opacity-0'</span><span>);</span></div></div><div><div><div>69</div></div><div><span><span>            </span></span><span>toast</span><span>.</span><span>classList</span><span>.</span><span>add</span><span>(</span><span>'translate-y-0'</span><span>, </span><span>'opacity-100'</span><span>);</span></div></div><div><div><div>70</div></div><div><span><span>        </span></span><span>}, </span><span>100</span><span>);</span></div></div><div><div><div>71</div></div><div><span><span>        </span></span><span>hasShownToast</span><span> </span><span>=</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>72</div></div><div><span>        </span><span>await</span><span> </span><span>fetchLocation</span><span>();</span></div></div><div><div><div>73</div></div><div><span>        </span><span>setTimeout</span><span>(() </span><span>=&gt;</span><span> { </span><span>closeWelcomeToast</span><span>(); }, </span><span>5000</span><span>);</span></div></div><div><div><div>74</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>75</div></div><div>
</div></div><div><div><div>76</div></div><div><span>    </span><span>function</span><span> </span><span>initWelcome</span><span>() {</span></div></div><div><div><div>77</div></div><div><span>        </span><span>// 检查是否已显示过欢迎弹窗</span></div></div><div><div><div>78</div></div><div><span>        </span><span>// if (!sessionStorage.getItem(VISIT_SESSION_KEY)) {</span></div></div><div><div><div>79</div></div><div><span>        </span><span>//     sessionStorage.setItem(VISIT_SESSION_KEY, 'true');</span></div></div><div><div><div>80</div></div><div><span>        </span><span>//     showWelcomeToast();</span></div></div><div><div><div>81</div></div><div><span>        </span><span>// }</span></div></div><div><div><div>82</div></div><div><span>        </span><span>// 显示欢迎弹窗</span></div></div><div><div><div>83</div></div><div><span>        </span><span>showWelcomeToast</span><span>();</span></div></div><div><div><div>84</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>85</div></div><div>
</div></div><div><div><div>86</div></div><div><span>    </span><span>if</span><span><span> (</span><span>document</span><span>.</span></span><span>readyState</span><span> </span><span>===</span><span> </span><span>'loading'</span><span>) {</span></div></div><div><div><div>87</div></div><div><span><span>        </span></span><span>document</span><span>.</span><span>addEventListener</span><span>(</span><span>'DOMContentLoaded'</span><span><span>, </span><span>initWelcome</span><span>);</span></span></div></div><div><div><div>88</div></div><div><span><span>        </span></span><span>document</span><span>.</span><span>addEventListener</span><span>(</span><span>'astro:page-load'</span><span><span>, </span><span>initWelcome</span><span>);</span></span></div></div><div><div><div>89</div></div><div><span><span>    </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>90</div></div><div><span>        </span><span>initWelcome</span><span>();</span></div></div><div><div><div>91</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>92</div></div><div>
</div></div><div><div><div>93</div></div><div><span><span>    </span></span><span>(</span><span>window</span><span> </span><span>as</span><span> </span><span>any</span><span>).</span><span>closeWelcomeToast</span><span> </span><span>=</span><span><span> </span><span>closeWelcomeToast</span><span>;</span></span></div></div><div><div><div>94</div></div><div><span>&lt;/</span><span>script</span><span>&gt;</span></div></div><div><div><div>95</div></div><div>
</div></div><div><div><div>96</div></div><div><span>&lt;</span><span>style</span><span>&gt;</span></div></div><div><div><div>97</div></div><div><span>    </span><span>/* 样式保持不变 */</span></div></div><div><div><div>98</div></div><div><span>    </span><span>#welcome-toast</span><span> { max-width: </span><span>calc</span><span>(</span><span><span>100</span><span>vw</span></span><span><span> </span><span>-</span><span> </span></span><span><span>2</span><span>rem</span></span><span>); }</span></div></div><div><div><div>99</div></div><div><span>    </span><span>@media</span><span> (max-width: </span><span><span>640</span><span>px</span></span><span>) {</span></div></div><div><div><div>100</div></div><div><span>        </span><span>#welcome-toast</span><span> {</span></div></div><div><div><div>101</div></div><div><span><span>            </span></span><span>top: </span><span>auto</span><span>; bottom: </span><span><span>1</span><span>rem</span></span><span>; left: </span><span><span>50</span><span>%</span></span><span><span>; right: </span><span>auto</span><span>;</span></span></div></div><div><div><div>102</div></div><div><span><span>            </span></span><span>transform: </span><span>translateX</span><span>(</span><span><span>-50</span><span>%</span></span><span>) </span><span>translateY</span><span>(</span><span><span>100</span><span>%</span></span><span>);</span></div></div><div><div><div>103</div></div><div><span><span>            </span></span><span>width: </span><span><span>90</span><span>%</span></span><span><span>; max-width: </span><span>none</span><span>;</span></span></div></div><div><div><div>104</div></div><div><span><span>            </span></span><span>transition: transform </span><span><span>0.5</span><span>s</span></span><span><span> </span><span>ease-out</span><span>, opacity </span></span><span><span>0.5</span><span>s</span></span><span><span> </span><span>ease-out</span><span>;</span></span></div></div><div><div><div>105</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>106</div></div><div><span>        </span><span>#welcome-toast</span><span>.translate-y-0</span><span> { transform: </span><span>translateX</span><span>(</span><span><span>-50</span><span>%</span></span><span>) </span><span>translateY</span><span>(</span><span>0</span><span>); }</span></div></div><div><div><div>107</div></div><div><span>        </span><span>#welcome-toast</span><span>.translate-y-full</span><span> { transform: </span><span>translateX</span><span>(</span><span><span>-50</span><span>%</span></span><span>) </span><span>translateY</span><span>(</span><span><span>100</span><span>%</span></span><span>); }</span></div></div><div><div><div>108</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>109</div></div><div><span>&lt;/</span><span>style</span><span>&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>使用组件<a href="#使用组件"><span>#</span></a></h2><p>文件路径：<code>src/layouts/MainGridLayout.astro</code></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>---</span></div></div><div><div><div>2</div></div><div><span>import</span><span> </span><span>WelcomeToast</span><span> </span><span>from</span><span> </span><span>"@components/widget/WelcomeToast.astro"</span><span>; </span><span>//欢迎提示组件</span></div></div><div><div><div>3</div></div><div><span>---</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>&lt;!-- 欢迎提示组件 --&gt;</span></div></div><div><div><div>6</div></div><div><span>&lt;</span><span>WelcomeToast</span><span>&gt;&lt;/</span><span>WelcomeToast</span><span>&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>显示逻辑配置<a href="#显示逻辑配置"><span>#</span></a></h2><ul>
<li>访客只在首次访问时显示欢迎弹窗（原作者）</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>function</span><span> </span><span>initWelcome</span><span>() {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>// 检查是否已显示过欢迎弹窗</span></div></div><div><div><div>3</div></div><div><span>        </span><span>if</span><span> (</span><span>!</span><span><span>sessionStorage</span><span>.</span></span><span>getItem</span><span>(</span><span>VISIT_SESSION_KEY</span><span>)) {</span></div></div><div><div><div>4</div></div><div><span><span>            </span></span><span>sessionStorage</span><span>.</span><span>setItem</span><span>(</span><span>VISIT_SESSION_KEY</span><span>, </span><span>'true'</span><span>);</span></div></div><div><div><div>5</div></div><div><span>            </span><span>showWelcomeToast</span><span>(); </span><span>// 显示欢迎弹窗</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>访客只会在首页刷新时显示，非首页刷新不会显示（正在使用）</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>function</span><span> </span><span>initWelcome</span><span>() {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>// 检查是否为首页（路径为 "/" 或 "/index.html"）</span></div></div><div><div><div>3</div></div><div><span>        </span><span>const</span><span> </span><span>isHomePage</span><span> </span><span>=</span><span> </span><span>window</span><span>.</span><span>location</span><span>.</span><span>pathname</span><span> </span><span>===</span><span> </span><span>'/'</span><span> </span><span>||</span><span> </span><span>window</span><span>.</span><span>location</span><span>.</span><span>pathname</span><span> </span><span>===</span><span> </span><span>'/index.html'</span><span>;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>        </span><span>// 只在首页显示欢迎弹窗</span></div></div><div><div><div>6</div></div><div><span>        </span><span>if</span><span><span> (</span><span>isHomePage</span><span>) {</span></span></div></div><div><div><div>7</div></div><div><span>            </span><span>showWelcomeToast</span><span>();</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>访客每次刷新页面时，欢迎提示组件会重新显示，不限于文章页。</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>function</span><span> </span><span>initWelcome</span><span>() {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>// 显示欢迎弹窗</span></div></div><div><div><div>3</div></div><div><span>        </span><span>showWelcomeToast</span><span>();</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>]]></content>
    </entry>
    <entry>
      <id>https://seasir.top/posts/quoteoftheday/</id>
      <title type="text">实现今日一言功能</title>
      <published>2026-05-30T00:00:00.000Z</published>
      <updated>2026-05-30T00:00:00.000Z</updated>
      <author><name>Hyde</name></author>
      <link rel="alternate" href="https://seasir.top/posts/quoteoftheday/"/>
      <summary type="text">基于 Astro 框架搭建了今日一言小组件，配置数据源、开发页面组件并完成全局注册与布局设置，组件会按日期每日展示不同名言，全端样式与交互效果均已适配完成。</summary>
      <content type="html"><![CDATA[<section><h2>创建数据文件<a href="#创建数据文件"><span>#</span></a></h2><p>文件路径：<code>src/content/ziyuan/</code> 目录并添加文件：<code>quote.md</code></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>---</span></div></div><div><div><div>2</div></div><div><span>title</span><span>: </span><span>"每日一言"</span></div></div><div><div><div>3</div></div><div><span>quotes</span><span>:</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>- </span><span>text</span><span>: </span><span>"生活不是等待暴风雨过去，而是学会在雨中跳舞。"</span></div></div><div><div><div>5</div></div><div><span>    </span><span>author</span><span>: </span><span>"塞维涅夫人"</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>- </span><span>text</span><span>: </span><span>"世界上只有一种英雄主义，就是在认清生活真相之后依然热爱生活。"</span></div></div><div><div><div>7</div></div><div><span>    </span><span>author</span><span>: </span><span>"罗曼·罗兰"</span></div></div><div><div><div>8</div></div><div><span>---</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>创建组件文件<a href="#创建组件文件"><span>#</span></a></h2><p>文件路径：<code>src/components/widget/QuoteOfTheDay.astro</code></p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>---</span></div></div><div><div><div>2</div></div><div><span>import</span><span> </span><span>WidgetLayout</span><span> </span><span>from</span><span> </span><span>"@/components/common/WidgetLayout.astro"</span><span>;</span></div></div><div><div><div>3</div></div><div><span>import</span><span> { </span><span>getCollection</span><span> } </span><span>from</span><span> </span><span>"astro:content"</span><span>;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>// 从 content/ziyuan/quote.md 读取名言数据</span></div></div><div><div><div>6</div></div><div><span>const</span><span> </span><span>ziyuanEntries</span><span> </span><span>=</span><span> </span><span>await</span><span> </span><span>getCollection</span><span>(</span><span>"ziyuan"</span><span>);</span></div></div><div><div><div>7</div></div><div><span>const</span><span> </span><span>quoteEntry</span><span> </span><span>=</span><span><span> </span><span>ziyuanEntries</span><span>.</span></span><span>find</span><span><span>((</span><span>e</span><span>) </span></span><span>=&gt;</span><span><span> </span><span>e</span><span>.</span></span><span>id</span><span> </span><span>===</span><span> </span><span>"quote"</span><span>);</span></div></div><div><div><div>8</div></div><div><span>const</span><span> </span><span>quoteData</span><span> </span><span>=</span><span><span> </span><span>quoteEntry</span><span>?.</span></span><span>data</span><span><span> </span><span>as</span><span> </span></span><span>any</span><span>;</span></div></div><div><div><div>9</div></div><div><span>const</span><span> </span><span>quotes</span><span> </span><span>=</span><span><span> </span><span>quoteData</span><span>?.</span></span><span>quotes</span><span> </span><span>||</span><span> [];</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>interface</span><span> </span><span>Props</span><span> {</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>class</span><span><span>?</span><span>:</span></span><span> </span><span>string</span><span>;</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>style</span><span><span>?</span><span>:</span></span><span> </span><span>string</span><span>;</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>const</span><span> { </span><span>class</span><span>: </span><span>className</span><span>, </span><span>style</span><span> } </span><span>=</span><span><span> </span><span>Astro</span><span>.</span></span><span>props</span><span>;</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>// 使用日期作为随机种子，确保同一天显示相同的名言</span></div></div><div><div><div>19</div></div><div><span>const</span><span> </span><span>today</span><span> </span><span>=</span><span> </span><span>new</span><span> </span><span>Date</span><span>();</span></div></div><div><div><div>20</div></div><div><span>const</span><span> </span><span>dateStr</span><span> </span><span>=</span><span><span> </span><span>today</span><span>.</span></span><span>toISOString</span><span>().</span><span>split</span><span>(</span><span>"T"</span><span>)[</span><span>0</span><span>];</span></div></div><div><div><div>21</div></div><div><span>const</span><span> </span><span>seed</span><span> </span><span>=</span><span><span> </span><span>dateStr</span><span>.</span></span><span>split</span><span>(</span><span>""</span><span>).</span><span>reduce</span><span><span>((</span><span>acc</span><span>, </span><span>char</span><span>) </span></span><span>=&gt;</span><span><span> </span><span>acc</span><span> </span></span><span>+</span><span><span> </span><span>char</span><span>.</span></span><span>charCodeAt</span><span>(</span><span>0</span><span>), </span><span>0</span><span>);</span></div></div><div><div><div>22</div></div><div><span>const</span><span> </span><span>randomIndex</span><span> </span><span>=</span><span><span> </span><span>seed</span><span> </span></span><span>%</span><span><span> </span><span>quotes</span><span>.</span></span><span>length</span><span>;</span></div></div><div><div><div>23</div></div><div><span>const</span><span> </span><span>quote</span><span> </span><span>=</span><span><span> </span><span>quotes</span><span>[</span><span>randomIndex</span><span>] </span></span><span>||</span><span> { </span><span>text</span><span>:</span><span> </span><span>"暂无名言"</span><span>, </span><span>author</span><span>:</span><span> </span><span>""</span><span> };</span></div></div><div><div><div>24</div></div><div><span>---</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>&lt;</span><span>WidgetLayout</span><span> </span><span>name</span><span>=</span><span>"✨ 今日一言"</span><span> </span><span>id</span><span>=</span><span>"quote-of-the-day"</span><span> </span><span>class</span><span>=</span><span>{</span><span>className</span><span>}</span><span> </span><span>style</span><span>=</span><span>{</span><span>style</span><span>}</span><span>&gt;</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>&lt;</span><span>div</span><span> </span><span>class</span><span>=</span><span>"quote-content"</span><span>&gt;</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>&lt;</span><span>blockquote</span><span> </span><span>class</span><span>=</span><span>"quote-text"</span><span>&gt;</span></div></div><div><div><div>29</div></div><div><span><span>      </span></span><span>&lt;</span><span>span</span><span> </span><span>class</span><span>=</span><span>"quote-mark"</span><span>&gt;"&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>30</div></div><div><span>      </span><span>{</span><span>quote</span><span>.</span><span>text</span><span>}</span></div></div><div><div><div>31</div></div><div><span><span>      </span></span><span>&lt;</span><span>span</span><span> </span><span>class</span><span>=</span><span>"quote-mark"</span><span>&gt;"&lt;/</span><span>span</span><span>&gt;</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>&lt;/</span><span>blockquote</span><span>&gt;</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>&lt;</span><span>cite</span><span> </span><span>class</span><span>=</span><span>"quote-author"</span><span>&gt;—— </span><span>{</span><span>quote</span><span>.</span><span>author</span><span>}</span><span>&lt;/</span><span>cite</span><span>&gt;</span></div></div><div><div><div>34</div></div><div><span><span>  </span></span><span>&lt;/</span><span>div</span><span>&gt;</span></div></div><div><div><div>35</div></div><div><span>&lt;/</span><span>WidgetLayout</span><span>&gt;</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>&lt;</span><span>style</span><span>&gt;</span></div></div><div><div><div>38</div></div><div><span>  </span><span>.quote-content</span><span> {</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>display: </span><span>flex</span><span>;</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>flex-direction: </span><span>column</span><span>;</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>gap: </span><span><span>0.75</span><span>rem</span></span><span>;</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>padding: </span><span><span>0.5</span><span>rem</span></span><span> </span><span>0</span><span>;</span></div></div><div><div><div>43</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>44</div></div><div>
</div></div><div><div><div>45</div></div><div><span>  </span><span>.quote-text</span><span> {</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>position: </span><span>relative</span><span>;</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>font-style: </span><span>italic</span><span>;</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>font-size: </span><span><span>0.9</span><span>rem</span></span><span>;</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>line-height: </span><span>1.6</span><span>;</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>color: </span><span>var</span><span>(</span><span>--text-color</span><span>);</span></div></div><div><div><div>51</div></div><div><span><span>    </span></span><span>padding: </span><span>0</span><span> </span><span><span>0.5</span><span>rem</span></span><span>;</span></div></div><div><div><div>52</div></div><div><span><span>    </span></span><span>margin: </span><span>0</span><span>;</span></div></div><div><div><div>53</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>54</div></div><div>
</div></div><div><div><div>55</div></div><div><span>  </span><span>.quote-mark</span><span> {</span></div></div><div><div><div>56</div></div><div><span><span>    </span></span><span>font-size: </span><span><span>1.5</span><span>rem</span></span><span>;</span></div></div><div><div><div>57</div></div><div><span><span>    </span></span><span>line-height: </span><span>0</span><span>;</span></div></div><div><div><div>58</div></div><div><span><span>    </span></span><span>vertical-align: </span><span>middle</span><span>;</span></div></div><div><div><div>59</div></div><div><span><span>    </span></span><span>color: </span><span>var</span><span>(</span><span>--primary</span><span>);</span></div></div><div><div><div>60</div></div><div><span><span>    </span></span><span>opacity: </span><span>0.6</span><span>;</span></div></div><div><div><div>61</div></div><div><span><span>    </span></span><span>font-family: </span><span>Georgia</span><span>, </span><span>serif</span><span>;</span></div></div><div><div><div>62</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>63</div></div><div>
</div></div><div><div><div>64</div></div><div><span>  </span><span>.quote-author</span><span> {</span></div></div><div><div><div>65</div></div><div><span><span>    </span></span><span>display: </span><span>block</span><span>;</span></div></div><div><div><div>66</div></div><div><span><span>    </span></span><span>text-align: </span><span>right</span><span>;</span></div></div><div><div><div>67</div></div><div><span><span>    </span></span><span>font-size: </span><span><span>0.75</span><span>rem</span></span><span>;</span></div></div><div><div><div>68</div></div><div><span><span>    </span></span><span>color: </span><span>var</span><span>(</span><span>--content-meta</span><span>);</span></div></div><div><div><div>69</div></div><div><span><span>    </span></span><span>font-style: </span><span>normal</span><span>;</span></div></div><div><div><div>70</div></div><div><span><span>    </span></span><span>margin-top: </span><span><span>0.25</span><span>rem</span></span><span>;</span></div></div><div><div><div>71</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>72</div></div><div>
</div></div><div><div><div>73</div></div><div><span>  </span><span>/* 深色模式优化 */</span></div></div><div><div><div>74</div></div><div><span>  </span><span>:global(</span><span>html</span><span>.dark</span><span>) </span><span>.quote-text</span><span> {</span></div></div><div><div><div>75</div></div><div><span><span>    </span></span><span>color: </span><span>var</span><span>(</span><span>--text-color</span><span>);</span></div></div><div><div><div>76</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>77</div></div><div>
</div></div><div><div><div>78</div></div><div><span>  </span><span>/* 悬停效果 */</span></div></div><div><div><div>79</div></div><div><span>  </span><span>:global(</span><span><span>.quote-content</span><span>:hover</span></span><span> </span><span>.quote-text</span><span>) {</span></div></div><div><div><div>80</div></div><div><span><span>    </span></span><span>color: </span><span>var</span><span>(</span><span>--primary</span><span>);</span></div></div><div><div><div>81</div></div><div><span><span>    </span></span><span>transition: </span><span>color</span><span> </span><span><span>0.2</span><span>s</span></span><span><span> </span><span>ease</span><span>;</span></span></div></div><div><div><div>82</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>83</div></div><div><span>&lt;/</span><span>style</span><span>&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>配置数据加载方式<a href="#配置数据加载方式"><span>#</span></a></h2><p>在 <code>src/content.config.ts </code> 配置数据加载方式：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>const ziyuanCollection = defineCollection({</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>loader: glob({ pattern: "**/*.md", base: "./src/content/ziyuan" }),</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>schema: z.union([</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>z.object({</span></div></div><div><div><div>5</div></div><div><span><span>      </span></span><span>title: z.string(),</span></div></div><div><div><div>6</div></div><div><span><span>      </span></span><span>content: z.string(),</span></div></div><div><div><div>7</div></div><div><span><span>      </span></span><span>closable: z.boolean().optional().default(true),</span></div></div><div><div><div>8</div></div><div><span><span>      </span></span><span>link: z</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>.object({</span></div></div><div><div><div>10</div></div><div><span><span>          </span></span><span>enable: z.boolean().optional().default(true),</span></div></div><div><div><div>11</div></div><div><span><span>          </span></span><span>text: z.string(),</span></div></div><div><div><div>12</div></div><div><span><span>          </span></span><span>url: z.string(),</span></div></div><div><div><div>13</div></div><div><span><span>          </span></span><span>external: z.boolean().optional().default(false),</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>})</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>.optional(),</span></div></div><div><div><div>16</div></div><div><span><span>      </span></span><span>quotes: z.undefined().optional(),</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>}),</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>z.object({</span></div></div><div><div><div>19</div></div><div><span><span>      </span></span><span>title: z.string(),</span></div></div><div><div><div>20</div></div><div><span><span>      </span></span><span>quotes: z.array(</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>z.object({</span></div></div><div><div><div>22</div></div><div><span><span>          </span></span><span>text: z.string(),</span></div></div><div><div><div>23</div></div><div><span><span>          </span></span><span>author: z.string(),</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>})</span></div></div><div><div><div>25</div></div><div><span><span>      </span></span><span>),</span></div></div><div><div><div>26</div></div><div><span><span>      </span></span><span>content: z.undefined().optional(),</span></div></div><div><div><div>27</div></div><div><span><span>      </span></span><span>closable: z.undefined().optional(),</span></div></div><div><div><div>28</div></div><div><span><span>      </span></span><span>link: z.undefined().optional(),</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>}),</span></div></div><div><div><div>30</div></div><div><span><span>  </span></span><span>]),</span></div></div><div><div><div>31</div></div><div><span>});</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>export const collections = {</span></div></div><div><div><div>34</div></div><div><span><span>  </span></span><span>ziyuan: ziyuanCollection,</span></div></div><div><div><div>35</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>注册组件<a href="#注册组件"><span>#</span></a></h2><p>文件路径：<code>src/components/layout/SideBar.astro</code></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import QuoteOfTheDay from "@/components/widget/QuoteOfTheDay.astro";</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>添加到组件映射表：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>const componentMap = </span><span>{</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>quoteOfTheDay</span><span>: </span><span>QuoteOfTheDay</span><span>,  </span><span>// 添加这一行</span></div></div><div><div><div>3</div></div><div><span>}</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>侧边栏配置<a href="#侧边栏配置"><span>#</span></a></h2><p>文件路径：<code>src/config/sidebarConfig.ts</code></p><ul>
<li>添加到右侧边栏</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>rightComponents</span><span>: [</span></span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>{</span></div></div><div><div><div>3</div></div><div><span>    </span><span>type</span><span>:</span><span> </span><span>"quoteOfTheDay"</span><span>,  </span><span>// 必须与 componentMap 的 key 一致</span></div></div><div><div><div>4</div></div><div><span>    </span><span>enable</span><span>:</span><span> </span><span>true</span><span>,           </span><span>// true = 启用，false = 禁用</span></div></div><div><div><div>5</div></div><div><span>    </span><span>position</span><span>:</span><span> </span><span>"sticky"</span><span>,        </span><span>// "top" = 固定顶部，"sticky" = 粘性定位</span></div></div><div><div><div>6</div></div><div><span>    </span><span>showOnPostPage</span><span>:</span><span> </span><span>true</span><span>,   </span><span>// 是否在文章详情页显示</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>},</span></div></div><div><div><div>8</div></div><div><span>  </span><span>// ... 其他组件</span></div></div><div><div><div>9</div></div><div><span>],</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>添加到移动端底部</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>mobileBottomComponents</span><span>: [</span></span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>{</span></div></div><div><div><div>3</div></div><div><span>      </span><span>// 组件类型：今日一言</span></div></div><div><div><div>4</div></div><div><span>      </span><span>type</span><span>:</span><span> </span><span>"quoteOfTheDay"</span><span>,</span></div></div><div><div><div>5</div></div><div><span>      </span><span>// 是否启用该组件</span></div></div><div><div><div>6</div></div><div><span>      </span><span>enable</span><span>:</span><span> </span><span>true</span><span>,</span></div></div><div><div><div>7</div></div><div><span>      </span><span>// 是否在文章详情页显示</span></div></div><div><div><div>8</div></div><div><span>      </span><span>showOnPostPage</span><span>:</span><span> </span><span>true</span><span>,</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>},</span></div></div><div><div><div>10</div></div><div><span>  </span><span>// ... 其他组件</span></div></div><div><div><div>11</div></div><div><span>],</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>类型定义<a href="#类型定义"><span>#</span></a></h2><p>文件路径：<code>src/types/config.ts</code></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>export</span><span> </span><span>type</span><span> </span><span>WidgetComponentType</span><span> </span><span>=</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>|</span><span> </span><span>"profile"</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span><span> </span><span>"announcement"</span></div></div><div><div><div>4</div></div><div><span>  </span><span>// ... 其他类型</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|</span><span> </span><span>"quoteOfTheDay"</span><span>;  </span><span>// 确保这一行存在</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>]]></content>
    </entry>
    <entry>
      <id>https://seasir.top/posts/travel/</id>
      <title type="text">年轻的时候是旅游重要还是攒钱重要？</title>
      <published>2026-04-30T00:00:00.000Z</published>
      <updated>2026-04-30T00:00:00.000Z</updated>
      <author><name>Hyde</name></author>
      <link rel="alternate" href="https://seasir.top/posts/travel/"/>
      <summary type="text">年轻时攒钱和旅行，哪样更重要？其实答案早藏在绿皮火车硬座上熬过的夜、青旅里聊梦想时眼睛里的光、还有那场说走就走却再难复制的少年远行里——体力可以练，钱可以再赚，但那种不顾一切奔向远方的心气，只属于那个夏天。人生不是非此即彼的选择题，而是：趁热，出发。</summary>
      <content type="html"><![CDATA[<section><h2>独特价值<a href="#独特价值"><span>#</span></a></h2><p>18岁时，我们揣着打工兼职存下的钱，就能在绿皮火车的硬座上熬一整夜</p><p>住青旅像回到大学宿舍般自在，和萍水相逢的陌生人聊起梦想，眼睛里都闪着光</p><p>那时的快乐很简单：有耗不尽的体力，特种兵旅行一整天，到第二天还有精力继续开启旅行</p><p>少年心气是不可再生之物，一场说走就走的旅行，一次毫不犹豫的出发，都是青春最滚烫的印记</p><p>人生有无数个夏天，却只有一次少年的烟火人间</p><p>我们只来一次，不如真诚一点，过得肆意，活成自己并永远热烈且自由</p></section>
<section><h2>旅行心境<a href="#旅行心境"><span>#</span></a></h2><p>或许到了30多岁，我们终于有能力选择喝着咖啡在沙滩边看日出日落、睡一觉就能抵达异国他乡的飞机，去往年轻时日思夜想的雪山</p><p>可站在山脚下的那一刻，却少了当年的激动，多了几分家里的柴米油盐和下班的疲惫</p><p>不再忙着打卡拍照发朋友圈，只想独自坐在山顶晒晒太阳发着呆</p><p>原来，最好的旅行从来不是在准备和计划中，而是在出发的那一刻</p></section>
<section><h2>感悟人生<a href="#感悟人生"><span>#</span></a></h2><p>大家常说：“欲买桂花同载酒，终不似少年游”</p><p>可也别忘了，“莫道桑榆晚，为霞尚满天”</p><p>人生还很长，一程有一程的风景</p><p>20岁没有完成的事情，30岁去完成也没关系；</p><p>就算是朝花夕拾，你觉得没有意义了，那也无所谓</p><p>人生于天地间，若白驹过隙，忽然而已</p><p>所以岁月不待人，及时当勉励，重在的是”诗酒趁年华”</p></section>
<section><h2>遗憾错过<a href="#遗憾错过"><span>#</span></a></h2><p>你一生追求的东西，其实一开始就有</p><p>18岁的你想去旅行，可你囊中羞涩，止于心动</p><p>20岁的你，朋友都叫你出去玩，可你说学业要紧，止于行动</p><p>我们总说来日方长，却忘了人只活这一次</p><p>很多事情错过一次，可能就再也没有机会了；很多远方，错过一次就再也去不成了</p><p>所有的错过，最终都成为这辈子难以忘怀的遗憾；</p><p>或许等我老了，躺在床上回想起这一生，要是因为一些事而一次次错过自己的梦想与决定，那我该多懊悔啊！</p></section>
<section><h2>出发勇气<a href="#出发勇气"><span>#</span></a></h2><p>很多幸福和快乐都是后知后觉，当我们身在其中的时候，或许觉得并没有什么，但过了很久以后，就会开始怀念</p><p>在所有不确定的时间里，好好珍惜当下才是最珍贵的</p><p>愿你我永远有出发的勇气和说走就走的行动力，并一直朝着梦想远航</p></section>]]></content>
    </entry>
</feed>
