云计算百科
云计算领域专业知识百科平台

文章详情页 AI 快速阅读功能实现

文章目录

  • 文章详情页 AI 快速阅读功能实现
    • 一、功能需求
    • 二、技术选型
    • 三、后端实现
      • 1. 添加依赖
      • 2. 配置文件
      • 3. 配置属性类
      • 4. AI 服务类
      • 5. 控制器
    • 四、前端实现
      • 1. API 函数
      • 2. 文章详情页添加快速阅读卡片
    • 五、使用效果
    • 六、遇到的问题
      • 1. AI 返回格式问题
      • 2. 文章内容过长
      • 3. 用户体验
    • 七、总结
    • 八、完整代码文件清单

文章详情页 AI 快速阅读功能实现

最近给博客加了个小功能:用户进入文章详情页时,自动生成一段不超过 200 字的概要,用浅紫色半透明卡片展示。这样用户不用看完整个文章,就能快速了解文章的核心内容。

一、功能需求

  • 进入文章详情页时,自动触发 AI 生成概要
  • 概要不超过 200 字
  • 用浅紫色半透明卡片展示,带 AI 图标
  • 支持关闭和重试

二、技术选型

  • 后端:Spring Boot + OkHttp + DeepSeek/硅基流动 API
  • 前端:Vue 3 + TypeScript + Axios

三、后端实现

1. 添加依赖

如果你的项目还没有 OkHttp 依赖,需要在 pom.xml 中添加:

<!– OkHttp (用于 AI 流式请求) –>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>

注意:快速阅读功能用的是同步调用,不需要 SSE 依赖。但如果你已经有 AI 对话功能,应该已经添加了。

2. 配置文件

在 application-dev.yml 或 application.yml 中添加 AI 配置:

# AI 对话配置
ai:
deepseek:
# API Key(从 https://siliconflow.cn 或 https://platform.deepseek.com 获取)
api-key: ${AI_API_KEY:sk你的API密钥}
# API 地址(硅基流动免费,DeepSeek 需要付费)
api-url: ${AI_API_URL:https://api.siliconflow.cn/v1/chat/completions}
# 模型名称
model: ${AI_MODEL:deepseekai/DeepSeekV30324}
# 系统提示词
system-prompt: ${AI_SYSTEM_PROMPT:你是一个博客智能助手,帮助用户解答技术问题。请用简洁、专业的中文回答,支持 Markdown 格式。}

获取 API Key:

  • 硅基流动(推荐,免费):https://siliconflow.cn 注册账号,创建 API Key
  • DeepSeek:https://platform.deepseek.com 注册账号,充值后创建 API Key

3. 配置属性类

创建 DeepSeekProperties.java(如果还没有的话):

package com.ican.config.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
* DeepSeek AI 配置属性
*
* @author ican
*/

@Data
@Component
@ConfigurationProperties(prefix = "ai.deepseek")
public class DeepSeekProperties {

/**
* API Key
*/

private String apiKey;

/**
* API 地址
*/

private String apiUrl = "https://api.deepseek.com/chat/completions";

/**
* 模型名称
*/

private String model = "deepseek-chat";

/**
* 系统提示词
*/

private String systemPrompt = "你是一个博客智能助手,帮助用户解答技术问题。请用简洁、专业的中文回答,支持 Markdown 格式。";
}

4. AI 服务类

在 AiService.java 中添加快速阅读方法:

package com.ican.service;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.ican.config.properties.DeepSeekProperties;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
public class AiService {

@Autowired
private DeepSeekProperties deepSeekProperties;

private final OkHttpClient httpClient = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();

/**
* 快速阅读(200字概要)
*
* @param content 文章内容
* @return 200字左右的概要
*/

public String quickRead(String content) {
// 截取前 4000 字,避免 token 过长
String truncated = content.length() > 4000 ? content.substring(0, 4000) : content;

JSONArray messagesArray = new JSONArray();

// 系统提示词
JSONObject systemMsg = new JSONObject();
systemMsg.put("role", "system");
systemMsg.put("content", "你是一个文章速读助手。请根据文章内容,生成一段不超过200字的中文概要。" +
"要求:抓住文章核心要点,语言简洁流畅,不要使用 Markdown 格式,不要加前缀,直接输出概要。");
messagesArray.add(systemMsg);

// 用户消息
JSONObject userMsg = new JSONObject();
userMsg.put("role", "user");
userMsg.put("content", "请为以下文章生成200字以内的快速阅读概要:\\n\\n" + truncated);
messagesArray.add(userMsg);

JSONObject requestBody = new JSONObject();
requestBody.put("model", deepSeekProperties.getModel());
requestBody.put("messages", messagesArray);
requestBody.put("stream", false); // 非流式,同步返回
requestBody.put("temperature", 0.3); // 降低随机性,概要更稳定
requestBody.put("max_tokens", 400); // 限制输出长度,约 200 字

Request request = new Request.Builder()
.url(deepSeekProperties.getApiUrl())
.addHeader("Authorization", "Bearer " + deepSeekProperties.getApiKey())
.addHeader("Content-Type", "application/json")
.post(RequestBody.create(requestBody.toJSONString(), MediaType.parse("application/json")))
.build();

try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
String body = response.body() != null ? response.body().string() : "";
log.error("AI 快速阅读失败: {} {}", response.code(), body);
throw new RuntimeException("AI 快速阅读失败: " + response.code());
}
String responseBody = response.body().string();
JSONObject jsonResponse = JSON.parseObject(responseBody);
JSONArray choices = jsonResponse.getJSONArray("choices");
if (choices != null && !choices.isEmpty()) {
JSONObject message = choices.getJSONObject(0).getJSONObject("message");
if (message != null) {
String result = message.getString("content");
return result != null ? result.trim() : "";
}
}
return "";
} catch (IOException e) {
log.error("AI 快速阅读异常", e);
throw new RuntimeException("AI 快速阅读异常: " + e.getMessage());
}
}
}

关键点说明:

  • stream: false:非流式调用,同步返回结果
  • temperature: 0.3:降低随机性,概要更稳定
  • max_tokens: 400:限制输出长度,约 200 字
  • 截取前 4000 字:避免文章太长导致 token 超限
  • 系统提示词明确要求:不要 Markdown,不要前缀,直接输出概要

5. 控制器

在 AiController.java 中添加接口:

package com.ican.controller;

import com.ican.model.vo.Result;
import com.ican.service.AiService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

/**
* AI 对话控制器
*
* @author ican
*/

@Api(tags = "AI 模块")
@RestController
public class AiController {

@Autowired
private AiService aiService;

/**
* AI 快速阅读(200字概要,前台文章详情页使用)
*
* @param body 请求体,包含 content 字段(文章内容)
* @return 200字左右的概要
*/

@ApiOperation(value = "AI 快速阅读")
@PostMapping("/ai/quick-read")
public Result<String> quickRead(@RequestBody Map<String, String> body) {
String content = body.get("content");
if (content == null || content.trim().isEmpty()) {
return Result.fail("文章内容不能为空");
}
return Result.success(aiService.quickRead(content));
}
}

接口放在 /ai/ 路径下,不需要登录(前台功能)。

四、前端实现

1. API 函数

在 api/article/index.ts 中添加:

import { PageQuery, PageResult, Result } from "@/model";
import request from "@/utils/request";
import { AxiosPromise } from "axios";
import { Article, ArticleInfo, ArticleRank, ArticleRecommend, ArticleSearch } from "./types";

/**
* AI 快速阅读
* @param content 文章内容
* @returns 200字概要
*/

export function quickReadArticle(content: string): AxiosPromise<Result<string>> {
return request({
url: "/ai/quick-read",
method: "post",
timeout: 60000, // AI 调用可能需要较长时间,设置 60 秒超时
data: { content },
});
}

注意设置了 timeout: 60000(60 秒),因为 AI 调用可能需要几秒到十几秒。

2. 文章详情页添加快速阅读卡片

在 views/Article/Article.vue 中:

<template>
<div class="bg">
<div class="main-container" v-if="article">
<div class="left-container" :class="app.sideFlag ? 'w-full' : ''">
<!– AI 快速阅读 –>
<div class="quick-read-card" v-if="quickReadVisible">
<div class="quick-read-header">
<div class="quick-read-title">
<svg-icon icon-class="ai" size="1.2rem" style="margin-right: 0.4rem"></svg-icon>
<span>AI 快速阅读</span>
</div>
<button class="quick-read-close" @click="quickReadVisible = false">
<svg-icon icon-class="close" size="0.8rem"></svg-icon>
</button>
</div>
<div class="quick-read-body">
<div v-if="quickReadLoading" class="quick-read-loading">
<span class="dot-loading"></span>
AI 正在阅读文章…
</div>
<div v-else-if="quickReadError" class="quick-read-error">
{{ quickReadError }}
<button class="retry-btn" @click="fetchQuickRead">重试</button>
</div>
<p v-else class="quick-read-content">{{ quickReadText }}</p>
</div>
</div>
<!– 文章内容 –>
<div class="article-container">
<!– … 原有文章内容 … –>
</div>
</div>
</div>
</div>
</template>

<script setup lang="ts">
import { getArticle, likeArticle, quickReadArticle } from "@/api/article";
import { ArticleInfo, ArticlePagination } from "@/api/article/types";
import { useAppStore, useBlogStore, useUserStore } from "@/store";
import { formatDate } from "@/utils/date";

const user = useUserStore();
const app = useAppStore();
const blog = useBlogStore();
const route = useRoute();

// 文章数据
const article = ref<ArticleInfo | null>(null);

// AI 快速阅读
const quickReadVisible = ref(true);
const quickReadLoading = ref(false);
const quickReadText = ref("");
const quickReadError = ref("");

const fetchQuickRead = () => {
if (!article.value?.articleContent) return;
quickReadLoading.value = true;
quickReadError.value = "";
quickReadArticle(article.value.articleContent)
.then(({ data }) => {
if (data.flag && data.data) {
quickReadText.value = data.data;
} else {
quickReadError.value = data.msg || "生成失败";
}
})
.catch(() => {
quickReadError.value = "AI 服务暂时不可用,请稍后重试";
})
.finally(() => {
quickReadLoading.value = false;
});
};

onMounted(() => {
getArticle(Number(route.params.id)).then(({ data }) => {
article.value = data.data;
// … 其他逻辑 …
// 自动触发 AI 快速阅读
fetchQuickRead();
});
});
</script>

<style lang="scss" scoped>
/* AI 快速阅读卡片 */
.quick-read-card {
margin-bottom: 1rem;
border-radius: 0.75rem;
background: rgba(147, 112, 219, 0.12);
backdrop-filter: blur(10px);
border: 1px solid rgba(147, 112, 219, 0.25);
overflow: hidden;
box-shadow: 0 2px 12px rgba(147, 112, 219, 0.1);
animation: fadeInDown 0.5s ease;
}

@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

.quick-read-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
border-bottom: 1px solid rgba(147, 112, 219, 0.15);
}

.quick-read-title {
display: flex;
align-items: center;
font-size: 0.95rem;
font-weight: 600;
color: #6a3fbf;
}

.quick-read-close {
background: none;
border: none;
cursor: pointer;
padding: 0.25rem;
border-radius: 50%;
color: var(–grey-5);
transition: all 0.2s;

&:hover {
background: rgba(0, 0, 0, 0.06);
color: var(–grey-7);
}
}

.quick-read-body {
padding: 1rem 1.25rem;
}

.quick-read-content {
margin: 0;
font-size: 0.9rem;
line-height: 1.7;
color: var(–grey-7);
}

.quick-read-loading {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
color: #6a3fbf;
}

.dot-loading {
display: inline-block;
width: 1.5rem;
height: 0.5rem;

&::after {
content: "…";
animation: dotAnim 1.5s steps(3, end) infinite;
font-size: 1.2rem;
letter-spacing: 2px;
}
}

@keyframes dotAnim {
0% { content: "."; }
33% { content: ".."; }
66% { content: "…"; }
}

.quick-read-error {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
color: #e85d5d;
}

.retry-btn {
background: rgba(147, 112, 219, 0.15);
border: 1px solid rgba(147, 112, 219, 0.3);
border-radius: 0.3rem;
padding: 0.15rem 0.5rem;
color: #6a3fbf;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.2s;

&:hover {
background: rgba(147, 112, 219, 0.25);
}
}
</style>

五、使用效果

  • 用户进入文章详情页
  • 页面加载完成后,自动触发 AI 快速阅读
  • 顶部显示浅紫色半透明卡片,显示「AI 正在阅读文章…」
  • 几秒后显示生成的概要(不超过 200 字)
  • 用户可以关闭卡片,也可以点击重试
  • 六、遇到的问题

    1. AI 返回格式问题

    问题:AI 可能返回带 Markdown 格式的文本,或者带前缀「概要:」。

    解决:在系统提示词中明确要求「不要使用 Markdown 格式,不要加任何前缀,直接输出概要」。后端再做一些清理。

    2. 文章内容过长

    问题:文章可能有几万字,全部传给 AI 会超出 token 限制。

    解决:后端截取前 4000 字,足够生成概要,也避免 token 超限。

    3. 用户体验

    问题:AI 调用可能需要几秒到十几秒,用户等待时需要有反馈。

    解决:显示 loading 状态和加载动画,失败时显示错误信息和重试按钮。

    七、总结

    这个功能实现起来不算复杂:

  • 后端:新增一个同步调用的 AI 接口,专门用于生成快速阅读概要
  • 前端:在文章详情页顶部添加卡片,进入页面时自动调用接口
  • 关键是:

    • 系统提示词要明确(不要 Markdown,不要前缀)
    • 设置合适的 temperature 和 max_tokens
    • 前端设置足够的超时时间
    • 处理错误情况,给用户友好提示(loading、重试)

    现在用户进入文章页时,可以快速了解文章内容,不用看完整个文章。如果生成的不满意,也可以关闭卡片,不影响正常阅读。

    八、完整代码文件清单

    后端:

    • pom.xml – 添加 OkHttp 依赖
    • application-dev.yml – 添加 AI 配置
    • DeepSeekProperties.java – 配置属性类
    • AiService.java – 添加 quickRead 方法
    • AiController.java – 添加 /ai/quick-read 接口

    前端:

    • api/article/index.ts – 添加 quickReadArticle 函数
    • views/Article/Article.vue – 添加快速阅读卡片和逻辑

    按照上面的步骤,就可以在自己的项目中实现这个功能了。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 文章详情页 AI 快速阅读功能实现
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!