











在移动互联网时代,用户期待的不再是一个简单的关键词匹配机器人,而是一个能理解上下文、支持富文本展示、甚至能解答数学难题的智能对话助手。
本文将基于真实的 Vue3 + UniApp 项目代码,深入剖析如何构建一个企业级智能问答 H5/小程序应用。我们将重点讲解会话状态管理、Markdown 与 LaTeX 公式渲染、多模式推荐算法以及附件预览下载等核心功能的实现细节。
本系统不仅是一个聊天界面,它解决的是复杂知识表达与流畅交互体验的痛点:
markdown-it 与 katex,完美支持富文本、代码高亮及复杂的数学公式渲染。对话列表采用经典的“左问右答”布局,通过时间轴串联整个会话过程。系统内置严谨的状态机,处理新建会话、追加提问、超时结束等逻辑。
利用 uni.setStorageSync 缓存 sessionId,用户刷新页面或重新进入时,自动恢复上次会话上下文。
const CACHE_KEY = 'intelligent_qa_session_id';
const CACHE_EXPIRE_TIME = 60 * 60 * 1000; // 1 小时过期
// 从本地缓存获取 sessionId
const getSessionFromCache = (): number | null => {
try {
const cacheData = uni.getStorageSync(CACHE_KEY);
if (!cacheData) return null;
const parsed = JSON.parse(cacheData);
// 检查缓存是否过期
if (Date.now() - parsed.timestamp > CACHE_EXPIRE_TIME) {
uni.removeStorageSync(CACHE_KEY);
return null;
}
return parsed.sessionId;
} catch (error) {
return null;
}
};
// 发送消息时自动创建或复用会话
const handleSend = async () => {
if (data.sessionId) {
// 复用现有会话
await AddSessionQARecord({ question: data.message, sessionId: data.sessionId });
} else {
// 创建新会话
const res = await AddSessionInfo({ question: data.message });
data.sessionId = res.data.sessionId;
saveSessionToCache(res.data.sessionId); // 缓存 ID
// 启动定时器轮询会话状态(判断是否超时)
addSessionTimer = setInterval(findSessionDetailById, 30000);
}
await findSessionQARecordList(); // 刷新列表并滚动到底部
};
后端返回 endTime 后,前端停止轮询并显示“会话已结束”提示,禁止继续发送消息。
<template>
<div v-if="data.sessionEndTime" class="question-answer-end-list">
<div class="question-answer-end-list-title">
客户会话超时,本次会话已经结束 {{ moment(data.sessionEndTime).calendar() }}
</div>
</div>
<!-- 发送前校验 -->
<div @click="handleRecommendClick(answer)">
<!-- 若已超时,点击推荐问题也无效 -->
<script>
if(data.sessionEndTime){
message.warning('客户会话超时,本次会话已经结束');
return;
}
</script>
</div>
</template>
针对教育、科研等场景,系统必须支持复杂的数学公式和富文本。我们使用 markdown-it 配合 markdown-it-katex 插件实现。
import MarkdownIt from 'markdown-it';
import markdownItKatex from 'markdown-it-katex';
// 初始化渲染器
const md = new MarkdownIt();
md.use(markdownItKatex); // 启用 LaTeX 支持
// 渲染函数
const renderedMarkdown = (item: any) => {
const content = item.answer;
// 移除模型模式下的特殊标记(可选)
const cleanContent = data.systemSessionModel === '1' ? content.replace(/<[^>]+>/g, '') : content;
return md.render(cleanContent);
};
UI 展示:
v-html 将渲染后的 HTML 插入页面。katex.min.css 确保公式样式正确。$E=mc^2$ 和块级公式 $$...$$。<div class="question-answer-content-item-content-text">
<!-- 模型优化模式:直接渲染 answer -->
<span v-if="data.systemSessionModel==='1'" v-html="renderedMarkdown(item)"></span>
<!-- 知识匹配模式:渲染第一个匹配知识的答案 -->
<span v-else-if="item?.answerList?.length===1" v-html="renderedMarkdownHtml(item.answerList[0])"></span>
</div>
系统支持两种核心应答模式,通过后端配置动态切换:
AI 直接生成最终答案,适合通用问答。
item.answer 字段。当问题模糊或存在多个相关知识时,系统返回候选列表,引导用户精准提问。
<!-- 推荐列表展示 -->
<div class="question-answer-recommend-section" v-if="data.systemSessionModel==='2' && item.answerList.length > 1">
<div class="question-answer-recommend-title">您是否要咨询以下问题呢:</div>
<div class="question-answer-recommend-list">
<div
v-for="(answer, idx) in item.answerList"
:key="answer.knowledgeId"
class="question-answer-recommend-item"
@click="handleRecommendClick(answer)"
>
{{ answer.question }}
</div>
</div>
</div>
<!-- 无匹配结果兜底 -->
<div v-else-if="data.systemSessionModel==='2' && item.answerList.length === 0">
<div class="question-answer-recommend-title">
很抱歉,目前我没有这个问题的相关答案...
</div>
</div>
交互逻辑:
点击推荐问题后,自动将该问题填入输入框并模拟发送 (simulateAddSessionQARecord),形成闭环。
支持在答案中携带附件(PDF、Word、图片等),提供预览和下载功能。
<div class="question-answer-attachment-section" v-if="item.answerList[0]?.annexList?.length > 0">
<div class="question-answer-attachment-header">
<span>附件</span><span class="question-answer-attachment-line"></span>
</div>
<div class="question-answer-attachment-list">
<div v-for="annex in item.answerList[0].annexList" :key="annex.id" class="question-answer-attachment-item">
<img src="@/static/images/common-type-file.svg" />
<!-- 文件名截断显示 -->
<span class="question-answer-attachment-name" @click="handlePreview(annex)">
{{ annex.name.length > 40 ? annex.name.substring(0, 40) + '...' : annex.name }}
</span>
<!-- 下载按钮 -->
<img src="@/static/images/Download.svg" @click.stop="handleDownload(annex)" />
</div>
</div>
</div>
核心方法:
onlinePreviewPath + Base64 编码的文件路径,在新窗口打开。file-saver 库,通过 fetch 获取 Blob 流并触发浏览器下载。const handleDownload = async (item: any) => {
const prefixPath = data.prefixPath || '';
try {
const response = await fetch(`${prefixPath}${item.path}`);
const blob = await response.blob();
saveAs(blob, item.name); // 触发下载
} catch (error) {
// 降级处理:直接跳转链接
window.open(`${prefixPath}${item.path}`);
}
};
// 评价逻辑
const handleResolve = async (item: any) => {
item.evaluate = item.evaluate === 1 ? null : 1; // 切换状态
await EvaluateSessionQARecord({
sessionId: data.sessionId,
qaRecordId: item.id,
evaluate: item.evaluate,
});
message.success('感谢您的反馈!');
};
scrollIntoView({ behavior: 'smooth' }) 自动滚动到底部,提升阅读体验。uni.showLoading,防止重复提交;输入框限制 500 字并实时提示。isFullScreen / isModal),动态计算容器高度。这套智能问答系统不仅实现了基础的对话功能,更在内容渲染能力、交互逻辑严密性和用户反馈闭环上做了深度打磨。它适用于高校教务咨询、企业技术支持、图书馆参考咨询等多种场景,是构建现代化智能客服体系的优秀范本。
💡 未来展望:后续可引入语音输入/输出、多轮对话上下文记忆、以及基于用户画像的个性化推荐,让助手更加“懂你”。
技术栈:Vue3 | UniApp | TypeScript | Markdown-it | Katex | File-saver
适用场景:智能客服 | 教育问答 | 技术支持 | 知识库检索
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏!有任何关于 Markdown 渲染优化或会话状态管理的问题,欢迎在评论区交流~
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。