Vue3 + TypeScript 实战:打造自适应 AI 问答与在线测验考试系统(附完整源码)

Vue3 + TypeScript 实战:打造自适应 AI 问答与在线测验考试系统(附完整源码)

在 AI 应用爆发的今天,如何构建一个既能满足自由对话 ,又能支持结构化测验 ,且完美适配 PC 端与移动端的智能交互系统?

本文将深入剖析一套基于 Vue3 + TypeScript + Ant Design Vue 的 AI 应用架构。我们将展示如何通过策略模式 区分"标准问答"与"测验模式",如何利用响应式布局 实现从桌面分栏到移动抽屉的无缝切换,以及如何通过 SSE (Server-Sent Events) 实现流畅的流式打字机效果。


🎯 核心需求与挑战

我们的目标是一个统一的 AI 应用入口 (AIApplication),它需要支撑两种截然不同的场景:

  1. Standard (智能问答):自由对话,支持文件上传、多轮上下文、引用溯源。
  2. Quiz (去测验):结构化考试,固定题目数量,自动评分,禁止随意发散。

技术挑战

  • 布局自适应:PC 端左侧历史/右侧聊天,移动端全屏覆盖 + 侧滑抽屉。
  • 状态隔离:两种模式下的会话管理、API 接口、消息渲染逻辑需完全解耦。
  • 流式体验:处理 SSE 流式返回,支持"停止生成"、"重新生成"及思考过程展示。

🏗️ 架构设计:策略驱动的双模引擎

1. 顶层容器:AIApplicationWeb.vue

这是系统的骨架,负责根据 chatVariant ('standard' | 'quiz') 和 设备类型 (isMobile) 动态组装组件。

typescript 复制代码
// 核心逻辑:计算是否使用桌面分栏布局
const useDesktopLayout = computed(() => {
  // 1. 真实 PC 端
  if (!props.isMobile) return true;
  // 2. 移动端但开启了全屏模式(如测验全屏查看)
  return store.state.qaDrawerFullscreen;
});

布局策略

  • Desktop : 左侧固定宽度的历史记录栏 (webLeftContainer) + 右侧弹性聊天区 (webRightContainer)。
  • Mobile : 默认隐藏左侧,通过 Drawer 组件覆盖展示历史记录,选择后自动关闭。
vue 复制代码
<template>
  <div class="flex flex-row ai-application-web-container">
    <!-- 动态渲染左侧历史组件 -->
    <div v-if="chatVariant === 'quiz' && useDesktopLayout" class="webLeftContainer">
      <AIQuizHistoryComponents @select="handleSelectConversation" />
    </div>
    
    <!-- 移动端抽屉 -->
    <AIQuizMobileHistoryComponents 
      v-if="chatVariant === 'quiz' && !useDesktopLayout" 
      :visible="isOpen" 
      @select="handleSelectConversationAndClose" 
    />

    <!-- 右侧核心聊天窗口 -->
    <div class="webRightContainer">
      <ChatWindow 
        :chat-variant="chatVariant" 
        :is-mobile="isMobile" 
      />
    </div>
  </div>
</template>

💬 核心聊天窗口:ChatWindow.vue

这是最复杂的组件,它集成了消息渲染、流式请求、输入控制和布局适配。

1. 双模 API 路由

根据 chatVariant 属性,动态选择后端接口和数据处理逻辑。

typescript 复制代码
const sendChatMessage = (question: string, chatFiles: UploadFile[]) => {
  if (props.chatVariant === "quiz") {
    sendQuizChatStream(question, chatFiles); // 测验模式:/quizChat
  } else {
    sendStandardChatStream(question, chatFiles); // 标准模式:/sendMessage
  }
};

2. 流式响应处理 (SSE)

使用 @microsoft/fetch-event-source 处理 SSE 流,实时拼接 AI 返回的内容和思考过程 (reasoning_content)。

typescript 复制代码
// 标准模式流式处理片段
async onmessage(event) {
  const data = JSON.parse(event.data);
  const lastAiItem = conversion.value[conversion.value.length - 1];
  
  if (data?.status === "completed") {
    // 结束状态:获取引用资源,停止 loading
    lastAiItem.creating = false;
    const res = await getMessageRefResource({ messageId: pageOption.messageId });
    lastAiItem.retrieverResources = res;
  } else {
    // 流式追加内容
    const delta = data.choices?.[0]?.delta;
    if (delta?.content) {
      lastAiItem.message = (lastAiItem.message || "") + delta.content;
    }
    // 处理深度思考过程
    if (delta?.reasoning_content) {
      lastAiItem.think = (lastAiItem.think || "") + delta.reasoning_content;
    }
  }
  scrollToBottom(); // 保持滚动到底部
}

3. 测验模式的特殊逻辑

测验模式下,进入新会话会自动触发"开始答题",且消息映射逻辑不同(根据 messageType 区分用户/AI)。

typescript 复制代码
// 自动开始答题逻辑
watch(() => props.conversationId, (newValue) => {
  if (!newValue && props.chatVariant === "quiz") {
    ensureQuizModelReady().then(() => {
      makeConversationToAi("开始智能答题", []); // 自动发起第一问
    });
  }
});

📱 响应式与交互细节

1. 头部导航栏的动态行为

左上角的菜单按钮在不同场景下表现不同:

  • PC 端:展开/收起左侧历史栏。
  • 移动端 (窄抽屉):先展开全屏,再显示历史。
  • 移动端 (全屏):直接显示历史抽屉。
typescript 复制代码
const handleHeaderMenuClick = () => {
  // 移动端测验模式特殊处理
  if (isQuizLayout.value && props.isMobile && props.page === "qatest") {
    if (!store.state.qaDrawerFullscreen) {
      store.dispatch("setQaDrawerFullscreen", true); // 先全屏
      open.value = true; // 再开历史
      return;
    }
  }
  jumpAI(1); // 常规切换
};

2. 居中约束布局

为了在宽屏显示器上获得更好的阅读体验,聊天内容区域和输入框被限制最大宽度 800px 并水平居中。

css 复制代码
/* 聊天内容区域 */
.chat-content-inner {
  width: 100%;
  max-width: 800px;
  margin-left: auto;
  margin-right: auto;
}

/* 底部输入框 */
.chat-input-wrap {
  width: calc(100% - 32px);
  max-width: 800px;
  left: 50%;
  transform: translateX(-50%);
  position: absolute;
  bottom: 15px;
}

3. 历史记录组件复用

通过 AIQuizHistoryComponentsAIChatHistoryComponents 分别处理两种模式的历史列表,但共享相同的 UI 结构(选中态、新建按钮、模型展示)。

vue 复制代码
<!-- 测验历史列表 -->
<a v-for="(item, index) in quizHistory" @click="emit('select', item.sessionId)">
  <div :class="['chat-history-item', { 'chat-history-item-select': selectKeys === index }]">
    {{ item.sessionName || "未命名会话" }}
  </div>
</a>

🔥 关键难点攻克

1. 状态同步与防抖

在快速切换会话或连续发送消息时,利用 refwatch 确保 conversationIdmdId (模型ID) 的准确同步,避免串话。

2. 移动端全屏与抽屉的平滑过渡

通过 store.state.qaDrawerFullscreen 控制 CSS 宽度和组件渲染策略,实现了从"窄浮层"到"全屏应用"的无缝体验,特别适配了 iPad 等平板设备。

3. 文件上传与预览

在标准模式下,集成文件上传功能,并在消息气泡中支持图片预览、文档下载及在线编辑入口(通过 upload-file-field 组件复用)。


📊 效果展示

  • 🖥️ PC 端:左侧清晰的历史会话树,右侧沉浸式聊天窗口,支持 Markdown 渲染、代码高亮、思维链展示。
  • 📱 移动端:紧凑的顶部导航,侧滑式历史记录,全屏化的答题体验,操作符合原生 App 直觉。
  • 📝 测验模式:自动计分、题目进度提示、严格的交互流程控制。

✅ 总结

这套架构成功地将复杂的 AI 交互逻辑封装在可复用的 Vue 组件中。通过 Props 驱动Composables 思想,我们仅用一套代码库就支撑了"智能问答"和"在线测验"两大核心业务,同时保证了多端的极致体验。

技术栈亮点

  • Vue 3 Setup Syntax:逻辑清晰,类型安全。
  • TypeScript:全链路类型定义,减少运行时错误。
  • Ant Design Vue:快速构建高质量 UI。
  • Fetch Event Source:完美的流式对话体验。
  • Responsive Design:一套代码,多端运行。

💡 提示:在生产环境中,建议增加消息本地缓存、弱网重试机制以及更细致的权限控制。


适用场景:企业知识库、AI 助教系统、在线考试平台、智能客服。

如果你觉得这篇文章对你有帮助,欢迎点赞、收藏!代码逻辑已详细拆解,欢迎交流~