Vue3 + TypeScript 实战:打造自适应 AI 问答与在线测验考试系统(附完整源码)
在 AI 应用爆发的今天,如何构建一个既能满足自由对话 ,又能支持结构化测验 ,且完美适配 PC 端与移动端的智能交互系统?
本文将深入剖析一套基于 Vue3 + TypeScript + Ant Design Vue 的 AI 应用架构。我们将展示如何通过策略模式 区分"标准问答"与"测验模式",如何利用响应式布局 实现从桌面分栏到移动抽屉的无缝切换,以及如何通过 SSE (Server-Sent Events) 实现流畅的流式打字机效果。
🎯 核心需求与挑战
我们的目标是一个统一的 AI 应用入口 (AIApplication),它需要支撑两种截然不同的场景:
- Standard (智能问答):自由对话,支持文件上传、多轮上下文、引用溯源。
- 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. 历史记录组件复用
通过 AIQuizHistoryComponents 和 AIChatHistoryComponents 分别处理两种模式的历史列表,但共享相同的 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. 状态同步与防抖
在快速切换会话或连续发送消息时,利用 ref 和 watch 确保 conversationId 和 mdId (模型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 助教系统、在线考试平台、智能客服。
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏!代码逻辑已详细拆解,欢迎交流~