Vue + Element Plus 实现AI文档解析与问答功能(含详细注释+核心逻辑解析)

本文将分享一套基于 Vue 3 + Element Plus 实现的「文档解析-会话创建-问答交互」完整方案,包含界面组件、核心逻辑、后端接口封装和本地存储设计,适合前端开发者直接复用或二次开发。

一,功能整体概述

本方案实现的核心功能:

  1. 点击按钮解析当前编辑器中的文档(获取文档URL标题
  2. 调用后端接口完成文档解析会话创建
  3. 解析后的文档列表本地持久化(localStorage),主要是`刷新之后不会消失``
  4. 文档选择联动会话ID,支持基于文档的AI流式问答
  5. 加载状态管理、重复点击防止、错误提示等用户体验优化

技术栈:Vue 3(Composition API)、Element Plus、Axios(API请求)、localStorage(本地存储)

二,界面按钮(Vue 模板)

首先是触发文档解析的核心按钮,基于 Element Plus 的 el-button 组件实现,绑定加载状态和点击事件:

javascript 复制代码
<el-button 
  size="small" 
  @click="parseCurrentDocument" //点击触发解析文档核心函数
  :loading="documentParsing" //绑定加载状态(解析中时显示加载动画,禁用点击)
  class="document-btn"
>
  解析文档
</el-button>

关键语法解析:documentParsing.value = true:loading="documentParsing"

这是 Vue 响应式状态与 Element Plus 组件的联动核心,很多开发者会疑惑其作用,这里详细拆解:

documentParsing 是什么?

documentParsing 是 Vue 3 用 ref 定义的响应式变量,用于标记「文档是否正在解析」:

javascript 复制代码
import { ref } from 'vue';
// 初始值为 false(未解析状态)
const documentParsing = ref(false);

它就像一个「状态开关」:

  • false:未解析/解析完成/解析失败(按钮正常可点击)
  • true:解析中(按钮显示加载动画,不可点击)
documentParsing.value = true 的作用

在解析函数开始执行时设置为 true,作用有两个:

  • 开启加载状态:让按钮显示转圈动画,告知用户「系统正在处理」
  • 防止重复点击:Element Plus 的 :loading="true" 会自动禁用按钮,避免用户短时间内多次点击导致重复请求
:loading="documentParsing" 的作用
  • :loadingv-bind:loading 的简写,用于将 Vue 响应式变量绑定到组件属性
  • documentParsingtrue 时,按钮自动进入加载状态(显示动画+禁用)
  • documentParsingfalse 时,按钮恢复正常状态(无动画+可点击)
联动逻辑:
javascript 复制代码
// 解析开始 → 开启加载
documentParsing.value = true;
// 解析成功/失败 → 关闭加载(finally 块确保无论结果如何都会执行)
documentParsing.value = false;

三、核心业务逻辑(Vue 脚本)

这部分是功能的核心,包含文档解析、列表管理、本地存储、状态监听等逻辑,全部基于 Vue 3 的 Composition API 实现:

javascript 复制代码
// 解析当前文档 - 核心业务函数(异步处理,避免阻塞界面)
const parseCurrentDocument = async () => {
  try {
    // 1. 开启加载状态,防止重复点击(对应上面的 loading 绑定)
    documentParsing.value = true;
    console.log('[AI Panel] 开始解析当前文档');

    // 2. 从编辑器获取当前文档的URL和标题(依赖编辑器提供的API,需根据实际编辑器适配)
    const documentUrl = await props.editorActions.getCurrentDocumentUrl(); // 获取文档在线URL
    const documentTitle = await props.editorActions.getCurrentDocumentTitle(); // 获取文档标题

    // 打印调试信息,便于开发和线上问题排查
    console.log('[AI Panel] 文档URL:', documentUrl);
    console.log('[AI Panel] 文档标题:', documentTitle);

    // 3. 校验URL合法性:无URL则抛出错误(后续catch块处理提示)
    if (!documentUrl) {
      throw new Error('无法获取当前文档URL');
    }

    // 4. 调用后端接口解析文档(传入URL和标题)
    const result = await parseDocumentFromUrl(documentUrl, documentTitle);
    console.log('[AI Panel] 文档解析成功:', result);

    // 5. 保存解析返回的文档ID到响应式变量(用于后续创建会话)
    currentDocumentId.value = result.document_id;

    // 6. 基于文档ID创建对话会话(用于后续AI问答,保持上下文连贯)
    const sessionResult = await startDocumentConversation(result.document_id);
    console.log('[AI Panel] 会话创建成功:', sessionResult);

    // 7. 保存会话ID到响应式变量(问答接口需传入该ID)
    documentSessionId.value = sessionResult.session_id;

    // 8. 将解析后的文档添加到本地文档列表(含同名文档去重逻辑)
    addParsedDocument(result.document_id, documentTitle);

    // 9. 持久化文档列表到本地存储(防止页面刷新后数据丢失)
    saveParsedDocuments();

    // 10. 解析成功提示(显示段落数量,提升用户感知)
    ElMessage.success(`文档解析成功!共${result.paragraphs_count}个段落`);
  } catch (error) {
    // 异常处理:打印错误日志(便于排查)+ 显示用户友好的错误提示
    console.error('[AI Panel] 解析文档失败:', error);
    ElMessage.error(`文档解析失败: ${error.message}`);
  } finally {
    // 无论解析成功/失败,最终都关闭加载状态(按钮恢复正常)
    documentParsing.value = false;
  }
};

// 添加解析文档到本地列表(含去重、自动选中逻辑)
// 参数:documentId-后端返回的文档唯一ID,documentTitle-文档标题(用户可见)
const addParsedDocument = (documentId, documentTitle) => {
  // 1. 检查列表中是否已存在同名文档(通过标题匹配,避免重复添加)
  const existingDocIndex = parsedDocuments.value.findIndex(doc => doc.label === documentTitle);

  // 2. 存在同名文档:直接替换(更新文档ID和会话ID,保持列表唯一性)
  if (existingDocIndex >= 0) {
    parsedDocuments.value[existingDocIndex] = {
      value: `doc_${documentId}`, // 列表选项值(格式:doc_+文档ID,用于区分其他类型选项)
      label: documentTitle, // 列表显示文本(用户可见的文档名称)
      sessionId: documentSessionId.value // 关联的会话ID(用于后续问答接口)
    };
  } else {
    // 3. 不存在同名文档:新增到列表尾部
    parsedDocuments.value.push({
      value: `doc_${documentId}`,
      label: documentTitle,
      sessionId: documentSessionId.value
    });
  }

  // 4. 持久化更新后的列表到本地存储
  saveParsedDocuments();

  // 5. 自动选中当前新增/替换的文档(提升用户体验,无需手动选择)
  selectedDocumentId.value = `doc_${documentId}`;
};

// 关键语法解析:selectedDocumentId.value = `doc_${documentId}`
很多开发者会疑惑这行代码的作用,这里详细说明:



```javascript
// 保存解析文档列表到localStorage(持久化存储,防止页面刷新后丢失)
const saveParsedDocuments = () => {
  try {
    // 将响应式数组转为JSON字符串(localStorage仅支持存储字符串)
    localStorage.setItem('ai_parsed_documents', JSON.stringify(parsedDocuments.value));
    // 注意:会话ID已嵌入文档列表项中,无需单独存储
  } catch (error) {
    // 存储失败处理(如浏览器存储容量不足):打印错误日志
    console.error('[AI Panel] 保存解析文档列表失败:', error);
  }
};

// 从localStorage恢复解析文档列表(页面初始化时调用)
const loadParsedDocuments = () => {
  try {
    // 1. 从localStorage获取存储的JSON字符串
    const savedDocuments = localStorage.getItem('ai_parsed_documents');
    
    // 2. 存在存储数据:解析为数组,赋值给响应式变量(恢复之前的解析记录)
    if (savedDocuments) {
      parsedDocuments.value = JSON.parse(savedDocuments);
    }
    // 注意:会话ID随文档列表一起恢复,无需单独处理
  } catch (error) {
    // 恢复失败处理(如数据格式损坏):打印错误日志 + 初始化空列表
    console.error('[AI Panel] 恢复解析文档列表失败:', error);
    parsedDocuments.value = [];
  }
};

// 监听选中文档ID变化 - 自动同步会话ID和文档ID(响应式联动)
// 监听目标:selectedDocumentId(用户选中的列表选项值,如doc_123456)
watch(selectedDocumentId, (newSelectedId) => {
  // 选中值存在时才处理(排除空值情况)
  if (newSelectedId) {
    // 1. 判断选中的是解析文档(通过前缀"doc_"区分其他类型选项)
    if (newSelectedId.startsWith('doc_')) {
      // 2. 在文档列表中查找当前选中的文档信息
      const selectedDoc = parsedDocuments.value.find(doc => doc.value === newSelectedId);
      
      // 3. 找到匹配文档且存在会话ID:同步到响应式变量(为问答接口提供参数)
      if (selectedDoc && selectedDoc.sessionId) {
        documentSessionId.value = selectedDoc.sessionId; // 同步会话ID
        currentDocumentId.value = newSelectedId.replace('doc_', ''); // 提取纯文档ID(去除前缀)
      } else {
        // 4. 未找到匹配文档或无会话ID:清空相关变量(避免脏数据)
        documentSessionId.value = '';
        currentDocumentId.value = '';
      }
    } else {
      // 5. 选中的是知识库等其他类型:清空文档相关变量
      documentSessionId.value = '';
      currentDocumentId.value = '';
    }

    // 6. 保存当前选中状态到本地存储(持久化用户选择)
    saveParsedDocuments();
  }
});

四、后端交互接口(API 封装)

所有与后端的交互都封装为独立函数,便于维护和复用,基于 Axios 实现(apiClient 是 Axios 实例,需提前配置):

  1. 文档解析接口(从 URL 解析文档)
javascript 复制代码
/**
 * 从URL解析文档 - 后端接口封装
 * @param {string} fileUrl - 文档的在线URL(如编辑器中文档的访问地址)
 * @param {string} fileName - 文档名称(用于后端存储和展示,默认document.docx)
 * @returns {Promise<Object>} 解析结果Promise,包含document_id等核心字段
 */
export const parseDocumentFromUrl = async (fileUrl, fileName) => {
  try {
    console.log('[AI Service] 开始解析文档:', { fileUrl, fileName });

    // 发送POST请求到后端解析接口,我的后端代理是/rr_api/,paragraphs/parse-from-url是具体接口
    const response = await apiClient.post('/rr_api/paragraphs/parse-from-url', {
      file_url: fileUrl, // 文档URL(后端接收参数名,需与后端一致)
      file_name: fileName, // 文档名称
      chunk_strategy: 'paragraph', // 切片策略:按段落切片(便于后续问答上下文理解)
      chunk_size: 1000, // 每个切片的字符数(1000字/段,可根据需求调整)
      chunk_overlap: 200 // 切片重叠字符数(避免段落拆分导致上下文丢失)
    });

    // 解析响应数据(JSON格式)
    const data = await response.json();
    console.log('[AI Service] 文档解析结果:', data);

    // 校验响应是否包含document_id(核心字段,无则视为解析失败)
    if (!data.document_id) {
      throw new Error('文档解析失败:未返回document_id');
    }

    // 返回标准化结果(便于前端统一处理,屏蔽后端返回格式差异)
    return {
      success: true,
      document_id: data.document_id, // 文档唯一ID(用于创建会话)
      file_name: data.file_name, // 文档名称(后端返回,可能与传入一致)
      paragraphs_count: data.paragraphs_count // 解析后的段落总数(用于用户提示)
    };
  } catch (error) {
    // 接口请求异常处理:打印错误日志 + 向上抛出错误(由调用方处理用户提示)
    console.error('[AI Service] 解析文档失败:', error);
    throw error;
  }
};
  1. 会话创建接口(为文档创建问答会话)
javascript 复制代码
/**
 * 开始文档对话会话 - 后端接口封装
 * 作用:为解析后的文档创建专属会话,后续问答基于该会话进行(保持上下文连贯)
 * @param {string} documentId - 解析文档返回的document_id
 * @returns {Promise<Object>} 会话创建结果,包含session_id(问答核心参数)
 */
export const startDocumentConversation = async (documentId) => {
  try {
    console.log('[AI Service] 开始文档对话会话:', documentId);

    // 发送POST请求到会话创建接口
    const response = await apiClient.post('/rr_api/conversation/start', {
      document_id: documentId, // 关联的文档ID(绑定会话与文档)
      user_id: 'user_001' // 用户ID(固定值,实际项目可替换为登录用户的真实ID)
    });

    // 解析响应数据
    const data = await response.json();
    console.log('[AI Service] 会话创建成功:', data);

    // 校验响应是否包含session_id(核心字段,无则视为创建失败)
    if (!data.session_id) {
      throw new Error('创建会话失败:未返回session_id');
    }

    // 返回标准化结果
    return {
      success: true,
      session_id: data.session_id, // 会话唯一ID(用于后续问答接口)
      document_id: data.document_id // 关联的文档ID(冗余返回,便于前端校验)
    };
  } catch (error) {
    console.error('[AI Service] 创建会话失败:', error);
    throw error;
  }
};
  1. 文档问答接口(基于会话的流式问答)
javascript 复制代码
/**
 * 文档模式AI问答 - 后端接口封装(流式响应)
 * 作用:基于已创建的会话,向AI提问并获取文档相关答案(流式返回,逐字显示)
 * @param {string} sessionId - 会话ID(startDocumentConversation返回的核心参数)
 * @param {string} userMessage - 用户输入的问题/消息
 * @returns {Promise<Object>} 后端流式响应对象(需前端处理流式数据)
 */
export const generateDocumentAnswer = async (sessionId, userMessage) => {
  console.log('[AI Service] 开始文档模式AI问答');
  console.log('[AI Service] 会话ID:', sessionId);
  console.log('[AI Service] 用户消息:', userMessage);

  // 校验会话ID是否存在(必填参数,无则抛出错误)
  if (!sessionId) {
    throw new Error("会话ID不能为空");
  }

  // 发送POST请求到流式问答接口(携带会话ID和用户消息)
  const response = await apiClient.post('/rr_api/conversation/chat/stream', {
    session_id: sessionId,
    user_message: userMessage
  }, {
    // 流式响应需配置响应类型为stream(Axios配置)
    responseType: 'stream'
  });

  console.log('[AI Service] 文档模式AI问答请求完成(开始接收流式响应)');
  return response; // 返回响应对象,由调用方处理流式数据(如逐字渲染到页面)
};

五、数据存储配置(localStorage)

解析后的文档列表通过 localStorage 持久化,避免页面刷新后数据丢失,存储结构如下:

javascript 复制代码
// localStorage存储结构说明:
// 键名:ai_parsed_documents(固定,用于区分其他本地存储数据)
// 存储内容:JSON格式的文档列表数组,每个元素包含3个核心字段
localStorage.setItem('ai_parsed_documents', JSON.stringify([
  {
    value: 'doc_文档ID', // 列表选项值(格式:doc_+文档ID,如doc_123456)
    label: '文档标题', // 列表显示文本(用户可见的文档名称,如"产品需求文档.docx")
    sessionId: '会话ID' // 关联的会话ID(如session_789,用于问答接口)
  },
  // 更多解析后的文档会按相同格式添加到数组中
]));

存储和恢复逻辑已在核心业务函数中实现(saveParsedDocuments 和 loadParsedDocuments),无需手动操作。

六、完整流程说明(步骤 + 逻辑)

#前置准备:

1)开启加载状态(documentParsing.value = true),防止重复点击

2)从编辑器获取文档 URL 和标题(依赖编辑器提供的 API)

#流程

1)用户触发操作:点击解析文档按钮 → 触发 parseCurrentDocument 函数

2)文档解析:调用 parseDocumentFromUrl 接口,传入 URL标题

后端返回 document_id(文档唯一标识)和段落数

3)会话创建:调用 startDocumentConversation 接口,传入 document_id

后端返回 session_id(会话唯一标识,用于后续问答)

4)本地存储更新:调用 addParsedDocument,将文档 ID、标题、会话 ID 存入列表(含去重)再调用 saveParsedDocuments,持久化列表到 localStorage

5)自动选中当前文档(selectedDocumentId.value = doc_${documentId})

6)文档选择联动:用户切换选中的文档 → 触发 watch(selectedDocumentId) 监听器,从列表中匹配对应的会话 ID,同步到 documentSessionId currentDocumentId

7)问答交互:

用户输入问题 → 调用 generateDocumentAnswer 接口(传入会话 ID 和问题)
接收后端流式响应 → 前端逐字渲染答案(保持上下文连贯)

页面刷新恢复

相关推荐
G***E3161 小时前
前端GraphQLAPI
前端
z***I3941 小时前
VueGraphQLAPI
前端
m0_650108242 小时前
InstructBLIP:面向通用视觉语言模型的指令微调技术解析
论文阅读·人工智能·q-former·指令微调的视觉语言大模型·零样本跨任务泛化·通用视觉语言模型
金融小师妹3 小时前
基于NLP语义解析的联储政策信号:强化学习框架下的12月降息概率回升动态建模
大数据·人工智能·深度学习·1024程序员节
S***t7143 小时前
Vue面试经验
javascript·vue.js·面试
粉末的沉淀3 小时前
css:制作带边框的气泡框
前端·javascript·css
AKAMAI4 小时前
提升 EdgeWorker 可观测性:使用 DataStream 设置日志功能
人工智能·云计算
p***h6434 小时前
JavaScript在Node.js中的异步编程
开发语言·javascript·node.js
N***73855 小时前
Vue网络编程详解
前端·javascript·vue.js