AI 摘要

本摘要由 AI 生成,仅供参考,内容请以原文为准。

VitePress 文章页 AI 摘要实现详解

前段时间给博客文章页加了一个 AI 摘要 模块。它的目标很直接:当文章比较长时,先给读者一个几秒钟就能看完的摘要,决定要不要继续读全文。

这次实现的重点不在于"在线调用大模型",而在于把现有文章摘要以更自然的方式呈现在文章页里。也就是说,摘要内容本身来自文章 frontmatter,页面负责渲染、动画和交互体验。

整体思路

这套实现可以拆成三步:

  1. 在文章 frontmatter 中提供摘要文本
  2. 在文章页正文前插入一个摘要组件
  3. 给组件补上打字机动画、重播按钮和降级逻辑

这样做的好处是结构简单、没有服务端依赖,也不会在页面加载时额外请求 AI 接口。

第一步:设计摘要数据来源

最简单的做法,就是直接复用文章已有的 summary 字段,同时兼容一个可选的 articleGPT 字段。

组件内部的核心逻辑大致是这样:

js
const summaryText = computed(() => {
  const value = frontmatter.value.summary || frontmatter.value.articleGPT;
  if (Array.isArray(value)) {
    return value.join(" ").trim();
  }
  return typeof value === "string" ? value.trim() : "";
});

这样设计有几个好处:

  • 兼容现有文章,很多文章本来就已经有 summary
  • 如果以后想把 AI 摘要单独存到 articleGPT,也不用改模板结构
  • 支持字符串和数组两种格式,便于后续扩展

文章里只需要这样写元数据:

yaml
---
title: 示例文章
description: 这是一篇示例文章
date: 2026-06-16
categories: [开发教程]
tags: [VitePress, AI 摘要]
summary: "这里放摘要内容,文章页会自动渲染 AI 摘要卡片。"
---

第二步:在文章页挂载摘要组件

文章页模板在 blog/.vitepress/theme/views/Post.vue。实现时直接把摘要组件插入正文内容前面:

vue
<article class="post-article s-card">
  <div class="expired s-card" v-if="postMetaData?.expired >= 180">
    本文发表于<strong>{{ postMetaData?.expired }}</strong> 天前,其中的信息可能已经时过境迁
  </div>

  <ArticleGPT />

  <Content id="page-content" class="markdown-main-style" />
</article>

这样所有文章页都会自动尝试渲染摘要模块,但真正显示与否由组件自身控制。只要当前文章没有 summaryarticleGPT,组件就不会输出任何内容。

这种写法比在每篇文章里手动插入组件更省事,也更适合博客这种批量内容场景。

第三步:实现摘要卡片组件

摘要卡片组件位于 blog/.vitepress/theme/components/Aside/Widgets/ArticleGPT.vue

它的模板结构并不复杂,主要由三部分组成:

  • 标题区域:机器人图标、"AI 摘要"标题、重播按钮
  • 内容区域:显示逐字输出的摘要文本
  • 提示区域:说明这是 AI 摘要,仅供参考

组件通过 v-if="summaryText" 控制显示,这一步很关键:

vue
<div v-if="summaryText" ref="containerRef" class="article-gpt s-card">

这样可以避免空摘要时渲染出一个没内容的卡片。

打字机动画怎么做

这次动画没有用第三方库,而是直接用 requestAnimationFrame 自己控制逐字输出。

核心思路是:

  • 先把完整摘要拆成字符数组
  • 每一帧根据时间差决定是否追加一个字符
  • 遇到标点符号时拉长停顿时间,让阅读节奏更自然

关键代码类似这样:

js
const chars = Array.from(text);
const baseDelay = 28;
const punctuationDelay = 130;

const delay = /[,。!?;:,.!?]/.test(currentChar)
  ? punctuationDelay
  : baseDelay;

这样处理后,摘要不会像机器刷屏一样一股脑蹦出来,而是更接近"读一句、停一下"的感觉。

为什么要配合 IntersectionObserver

如果页面一加载就开始动画,用户还没滚到正文区域,动画可能已经播完了。这样摘要区的存在感会很弱。

所以这里又加了一层 IntersectionObserver

  • 先监听摘要卡片是否进入视口
  • 真正进入视口后,再延迟一点点启动打字动画

对应的好处是:

  • 首屏性能更稳,不需要一上来就执行动画
  • 用户滚动到摘要区域时,反馈更明显
  • 视觉体验更符合阅读流程

无障碍和降级处理

做这类动画组件时,不能只顾效果,还要考虑"动画不适合所有人"。

这里做了两个降级:

1. 服务端渲染时直接输出最终文本

如果当前环境没有 window,说明代码正在 SSR 阶段执行,这时直接显示完整摘要:

js
if (typeof window === "undefined") {
  renderedText.value = summaryText.value;
  return;
}

2. 用户偏好减少动画时跳过打字效果

通过 prefers-reduced-motion 检测系统设置,如果用户更倾向少动画,就直接完整显示内容:

js
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;

这一步能避免为了视觉效果牺牲可访问性。

重播按钮的作用

摘要卡片右上角放了一个 Moretti AI 按钮,点击后会重新执行 initializeAnimation()

这个交互虽然简单,但体验上很有用:

  • 用户第一次可能没认真看
  • 页面切换回来后想再看一遍摘要
  • 可以让组件更像一个"可交互的信息块",而不只是静态提示

同时按钮在动画执行中会被禁用,避免重复点击导致状态混乱。

样式层的几个处理

这个组件的视觉层主要做了几件事:

  • 使用渐变背景,和普通卡片拉开层次
  • color-mix 给边框和阴影增加一点主题色
  • 光标闪烁配合逐字输出,强化打字机感
  • 在移动端缩小间距和字号,保证不挤占正文空间

因为它本质上是正文前的引导信息,所以样式上不能太重,否则会喧宾夺主;但也不能太弱,否则读者注意不到。

这套方案的边界

需要说明的是,这次实现严格来说更像是AI 摘要展示层,而不是"前端实时生成 AI 摘要"。

也就是说:

  • 页面不会在浏览器里调用大模型接口
  • 摘要内容需要提前写进 frontmatter
  • 组件负责的是读取、展示、动画和交互

这么做的优点是稳定、便宜、易维护,尤其适合静态博客。

如果后面想进一步自动化,可以继续往前走一步:在构建阶段写脚本,批量调用 AI 生成摘要,再回填到每篇文章的 summaryarticleGPT 字段里。这样仍然能保持前端零运行时依赖。

总结

这次 AI 摘要的实现,其实没有引入复杂架构,核心只是把数据来源、页面挂载点和动效逻辑拆清楚:

  • summary / articleGPT 负责提供内容
  • Post.vue 负责把组件放到合适的位置
  • ArticleGPT.vue 负责展示、动画和交互细节

对于 VitePress 博客来说,这是一种投入不大、收益很直接的增强方式。尤其是文章偏长、教程类内容较多时,给读者一个先看摘要再决定是否深入的入口,体验会好很多。

赞赏博主
评论 隐私政策