简介面向文旅知识库大模型问答场景的HTML前端设计源码以前端三件套构建页面结构、交互逻辑与视觉样式并通过Python脚本保留与知识问答后端服务的接口适合前端开发者和AI产品原型搭建者参考也可用于课程设计或技术预研。压缩包共482个文件、约25.17MB包括207个PNG、111个JPG图片素材以及网页字体、脚本与样式表等文件PNG适合透明图形JPG适合高清照片字体与样式负责界面呈现脚本用于动态行为。当前已有311人学习下载目录结构较为完整可从readme.txt快速了解项目组成与启动方式。项目内还提供.gitignore、LICENSE等工程文件便于理解版本管理规则、开源许可边界和项目初始化方式。整套源码既可作为文旅问答类前端实现的参考实例也适合从零梳理前端分层结构并掌握与后端API联动的思路。1. 基于文旅知识库大模型问答的HTML前端在设计什么把“基于文旅知识库大模型问答的HTML前端设计源码”这个标题拆开看真正需要设计的不是一张漂亮的页面而是“文旅知识库问答”这个业务场景下前端如何把大模型的能力完整、可控、可运营地交到游客和运营人员手里。文旅问答和通用ChatBot最大的差别在于答案必须可信、可溯源、不能信口开河。前端源码里如果只有聊天界面和流式打字机效果那只是Demo如果把引用来源、敏感词拦截、会话上下文、降级提示都设计进去才算对得上“文旅知识库”这四个字。另一个容易被低估的点是“HTML”这个词。很多人默认大模型前端就必须上React、Vue但文旅行业的落地场景往往是景区大屏、政务公众号内嵌页、临时活动站甚至是一个打包进安卓壳的WebView。在这些场景里原生HTML、CSS、JavaScript三件套依然是部署成本最低、最不容易被环境卡住的方案。这篇文章按“从零手写一个可直接维护的文旅知识库问答前端源码”的路线来讲覆盖页面骨架、SSE流式对接、Markdown渲染安全、会话上下文、本地部署验证。适合正在做AI应用前端的开发也适合要把大模型能力嵌入现有文旅系统的运维和全栈工程师。2. HTML页面骨架与文旅问答视觉设计2.1 先定技术选型为什么用原生HTML而不是上框架一个文旅知识库问答前端业务边界非常清晰输入问题、展示流式回答、展示引用来源、提供历史会话。这些功能用原生HTML配合少量JavaScript完全可以覆盖而且有几个现实理由让我在多数项目里优先选原生部署简单。一个index.html加上style.css和app.js扔到Nginx或任何静态服务器就能跑不需要Node构建链。便于运营修改。景区运营方经常要调标语、改主题色、加公告原生HTML里改几行CSS变量就能交付不需要重新打包。嵌入友好。文旅系统里经常要把问答窗嵌进已有的景区门户、小程序WebView或政务平台原生页面没有路由冲突和包体积压力。如果页面后续要扩展成多路由、复杂状态管理的后台再迁移到Vue或React也不迟。前期用原生HTML把业务跑通是一件收益很高的事。2.2 页面结构文档头、布局分区与消息列表页面结构上我习惯把整个问答界面拆成三个区域顶部的知识库标识区、中间的消息流区、底部的输入区。HTML源码里用语义化标签把这几个区域写清楚而不是全部用div堆。!doctype html html langzh-CN head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1 meta namedescription content文旅知识库智能问答前端源码 title文旅知识库智能问答/title link relstylesheet hrefcss/style.css /head body header classapp-header div classbrand span classbrand-logo游/span div classbrand-text h1文旅知识库智能问答/h1 p基于本地文旅知识库的大模型问答服务/p /div /div button idclearChat classbtn-clear typebutton清空会话/button /header main idchatContainer classchat-container section classwelcome idwelcomeBox h2有什么关于本地文旅的问题想了解/h2 p可尝试提问景区开放时间、门票政策、周边交通、当季活动等。/p /section div idmessageList classmessage-list/div div idtypingIndicator classtyping hidden正在查询知识库.../div /main footer classinput-area textarea idquestionInput rows2 placeholder请输入你的问题Enter 发送ShiftEnter 换行/textarea button idsendBtn classbtn-send typebutton发送/button /footer script srcjs/app.js/script /body /html文档头用langzh-CN明确页面语言meta description写上“文旅知识库智能问答前端源码”对检索和语义化都有帮助。消息列表容器#messageList专门用来动态插入问答消息#typingIndicator在请求等待期间显示提示状态。需要注意textarea比input更适合承载问题输入因为文旅类问题往往较长比如“博物馆今天几点开门怎么坐公交过去”。按钮和输入框都放在footer里不用form包裹是为了避免页面刷新打断流式请求。2.3 CSS变量与文旅配色的工程化文旅项目的视觉设计不能直接套用普通后台的蓝色系而是要从景区或文化IP的主视觉里提取色彩变量。用CSS变量管理主题色运营调色时只改:root里的几个值即可。:root { --brand-primary: #2e7d5b; --brand-primary-light: #4a9b77; --brand-bg: #f7f5f0; --text-main: #2b2b2b; --text-secondary: #6b6b6b; --msg-user-bg: var(--brand-primary); --msg-user-text: #ffffff; --msg-assistant-bg: #ffffff; --msg-border: #e5e2dc; --radius-lg: 16px; --radius-md: 10px; --shadow-card: 0 2px 8px rgba(0, 0, 0, 0.06); --max-content-width: 860px; } body { margin: 0; font-family: PingFang SC, Microsoft YaHei, Noto Sans CJK SC, sans-serif; background: var(--brand-bg); color: var(--text-main); display: flex; flex-direction: column; height: 100vh; } .chat-container { flex: 1; overflow-y: auto; max-width: var(--max-content-width); width: 100%; margin: 0 auto; box-sizing: border-box; padding: 16px; }变量名用--brand-前缀把品牌色和中性色分开管理。页面整体用flex纵向布局chat-container设置flex: 1和overflow-y: auto保证消息多时只有中间区域滚动输入区始终固定在底部。--max-content-width限制内容宽度避免大屏下文本行过长影响阅读。2.3.1 消息气泡、引用溯源和正文的排版规范问答消息的样式要区分三种信息层级用户提问、助手回答、引用来源。助手回答里常常夹杂知识库来源比如“以上信息根据『某某景区开放公告』整理”这些来源需要视觉上独立不能被当成正文内容混在一起。.msg { display: flex; margin-bottom: 20px; } .msg.user { justify-content: flex-end; } .msg-box { max-width: 78%; padding: 12px 16px; border-radius: var(--radius-lg); line-height: 1.7; font-size: 15px; word-break: break-word; } .msg.user .msg-box { background: var(--msg-user-bg); color: var(--msg-user-text); border-bottom-right-radius: 4px; } .msg.assistant .msg-box { background: var(--msg-assistant-bg); border: 1px solid var(--msg-border); box-shadow: var(--shadow-card); border-bottom-left-radius: 4px; } .source-ref { margin-top: 10px; padding-top: 8px; border-top: 1px dashed var(--msg-border); font-size: 13px; color: var(--text-secondary); } .source-ref a { color: var(--brand-primary); text-decoration: none; }.source-ref用虚线分隔线从正文中切分出来来源链接使用品牌色。用户消息右对齐、助手消息左对齐是问答界面最基础也最不会出错的布局模式。消息宽度限制在78%给长文本换行留足空间同时避免气泡撑满整个屏幕。3. 对接大模型API从HTTP到SSE流式问答的HTML实现3.1 理解大模型问答的通信链路为什么答案必须流式返回文旅知识库问答前端的核心通信链路是前端把用户问题通过HTTP POST发送给后端网关网关携带知识库检索结果和Prompt模板调用大模型模型生成的内容再以SSEServer-Sent Events形式流式推回前端。有人问为什么不能像普通接口那样一次性等完整答案返回原因有两个一是用户体验。大模型生成一段200字的文旅介绍需要数秒到十几秒如果前端一直空白等待游客大概率会以为系统卡死。SSE流式返回可以在生成过程中逐字展示用户看到内容在持续输出等待焦虑会大幅降低。二是技术风险控制。SSE基于HTTP长连接前端用浏览器的EventSource或fetch配合ReadableStream就能接收。文旅场景的网络环境不稳定流式协议天然支持断点感知前端可以及时提示“回答中断”而不是让用户对着一个转圈的图标干等。3.2 前端SSE客户端的完整实现浏览器原生EventSource只支持GET请求而大模型接口通常需要POST携带对话历史和参数所以我会用fetch加ReadableStream手动解析SSE流。这也是目前兼容性最好、可控性最高的做法。async function sendQuestion(question) { const controller new AbortController(); currentAbortController controller; const messages buildMessages(question); appendMessage(user, question); try { const response await fetch(config.apiUrl, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${config.apiKey} }, body: JSON.stringify({ model: config.model, messages: messages, stream: true, temperature: config.temperature }), signal: controller.signal }); if (!response.ok) { throw new Error(HTTP ${response.status}); } const reader response.body.getReader(); const decoder new TextDecoder(utf-8); let buffer ; let assistantMessage ; let currentSource null; while (true) { const { done, value } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); const lines buffer.split(\n); buffer lines.pop(); for (const line of lines) { const trimmed line.trim(); if (!trimmed.startsWith(data:)) continue; const data trimmed.slice(5).trim(); if (data [DONE]) continue; const parsed JSON.parse(data); handleStreamEvent(parsed); } } } catch (error) { if (error.name AbortError) { appendStatus(回答已停止); } else { appendStatus(请求失败请稍后重试); } } }这段代码有几个关键设计。AbortController用来支持用户中途停止生成这在长回答场景里很实用TextDecoder(utf-8)配合stream: true处理中文多字节字符的分包问题否则偶尔会出现半个汉字解析成乱码buffer变量缓存不完整的行等下一个数据块到达后再拼接。注意SSE的每一行数据以data:前缀开头解析时要用slice(5)去掉前缀空行才是事件结束的标志。3.2.1 SSE事件类型与处理动作文旅知识库问答的服务端通常会返回几种自定义事件类型。下面是我在实际对接中约定过的一张事件处理对照表前端按这张表逐项处理。事件名数据类型前端动作startJSON对象清空当前消息容器创建助手消息骨架tokenJSON字符串追加文本到当前消息内容sourcesJSON数组渲染引用来源形成.source-ref区块errorJSON对象停止流式解析展示错误码[DONE]字符串结束会话隐藏输入框加载态start和token拆分的好处是助手消息的DOM节点只需要创建一次后面的文本追加都发生在同一个节点上不会因为频繁创建DOM导致页面卡顿。sources单独推送是因为知识库检索结果往往在首包就返回但前端需要等回答结束后再展示引用链路。3.3 Markdown渲染与代码块高亮的安全处理大模型返回的答案通常是Markdown格式文旅知识库里也有大量表格和列表类内容比如开放时间表、交通路线列表。前端不能直接把返回内容用innerHTML塞进页面既会破坏样式也有XSS风险。常见做法是引入marked做Markdown解析再用DOMPurify做白名单过滤。function renderMarkdown(text) { const rawHtml marked.parse(text, { breaks: true, gfm: true }); const cleanHtml DOMPurify.sanitize(rawHtml, { USE_PROFILES: { html: true }, FORBID_TAGS: [style, iframe, form], FORBID_ATTR: [onerror, onclick] }); return cleanHtml; }marked的breaks: true让单换行直接生成br更符合大模型回答的排版习惯gfm: true开启GitHub风格Markdown表格语法才会正常渲染。DOMPurify.sanitize的FORBID_TAGS和FORBID_ATTR是两个必须配的参数前者禁掉会破坏页面布局的标签后者禁掉事件属性防止注入的脚本在渲染时执行。渲染时机也有讲究只有在流式输出完全结束后才做一次Markdown渲染而不是每收到一个token就渲染一次否则页面会频繁重绘输入法打字时都可能出现卡顿。3.4 错误状态与断线重连策略文旅景区的网络环境特殊游客用移动网络访问时经常出现弱网、断流问题前端必须把网络错误和业务错误区分开处理。我的做法是把错误分成三类分别映射到不同的用户提示错误类型判断依据用户提示网络断连TypeError: Failed to fetch“网络连接不稳定请检查网络后重试”接口鉴权失败HTTP 401 / 403“服务凭证无效请联系管理员”知识库无结果服务端返回no_result事件“知识库中没有找到相关内容请换个问法”大模型超时HTTP 408 / 504“回答超时建议简化问题后重新提问”断线重试不能无脑自动执行。文旅问答接口对接的是后端网关自动重试可能导致重复扣费或重复写入日志。我的策略是网络断连时自动重试一次间隔3秒其他错误都在界面上给出明确提示不自动重发。重试逻辑要放在catch分支里用计数器控制最多重试一次避免游客在信号差的区域反复触发请求。4. 会话上下文、本地交互增强与部署后的链路验证4.1 基于本地数组的会话上下文管理大模型API本身不维护状态每次请求都要把对话历史完整传过去。文旅知识库问答的场景和通用闲聊不同游客的提问往往是短链路的“今天开放吗”和“几点闭馆”这类问题依赖前面的景区选择信息。前端源码里需要在本地维护一个会话上下文数组按时间顺序保存消息并在新请求时截取最近几轮。const MAX_CONTEXT_ROUNDS 6; function buildMessages(question) { const history getHistoryMessages(); const apiMessages history.map(item ({ role: item.role, content: item.content })); apiMessages.push({ role: user, content: question }); return apiMessages; } function getHistoryMessages() { const list JSON.parse(localStorage.getItem(travel_chat_history) || []); const recent list.slice(-MAX_CONTEXT_ROUNDS * 2); return recent; }MAX_CONTEXT_ROUNDS用“轮”做单位每轮包含一问一答两条消息所以slice(-MAX_CONTEXT_ROUNDS * 2)取实际消息条数。上下文不是越长越好文旅知识库问答的核心是查当前事实模型对太久之前的对话记忆需求很低反而会占用token窗口导致单次请求变慢。历史消息持久化到localStorage游客误刷新页面后对话记录还在体感上更接近原生App。业务上如果涉及游客隐私这个本地存储应该在“清空会话”按钮里一并清除。4.2 语音播报、对讲交互与快捷键文旅问答的一个高频使用场景是游客在景区门口一边看屏幕一边问问题这时候打字不方便语音能力就很有价值。浏览器原生SpeechSynthesis接口可以免SDK实现文本播报但要注意不是所有移动端浏览器都支持良好。我一般会在设置面板里增加一个“自动播报”开关默认关闭由用户主动开启。function speakAnswer(text) { if (!(speechSynthesis in window) || !config.autoSpeak) return; const cleanText text .replace(/[#*\[\]()]/g, ) .replace(/\s/g, ); const utterance new SpeechSynthesisUtterance(cleanText); utterance.lang zh-CN; utterance.rate 0.9; speechSynthesis.cancel(); speechSynthesis.speak(utterance); }播报前用正则去掉Markdown符号否则语音引擎会把##读成“井井”。rate: 0.9比默认语速略慢更适合文旅场景的信息播报。注意在流式输出结束时调用speakAnswer不要在生成过程中逐token播报。键盘交互方面用keydown监听Enter键发送、ShiftEnter换行这个逻辑在HTML源码阶段就要写进textarea的处理函数里避免用户按回车直接触发提交刷新页面。4.3 本地部署后用三条命令验证整条链路源码设计完之后验证不能只靠浏览器里看一眼页面。推荐用Python起一个静态文件服务然后用curl分别验证静态资源和后端SSE接口。这样能快速区分是前端解析问题还是后端数据问题。# 在源码根目录启动静态服务 python3 -m http.server 8080 # 验证页面与静态资源是否正常返回 curl -I http://localhost:8080/ # 验证大模型问答接口的SSE流是否正常 curl -N -X POST http://localhost:8080/api/chat \ -H Content-Type: application/json \ -d {messages:[{role:user,content:今天景区开放吗}],stream:true}第一条命令让当前目录下的HTML、CSS、JS通过http://localhost:8080直接访问。第二条命令用-I只查看响应头确认静态服务正常。第三条命令的-N参数关闭curl的缓冲让SSE流式数据实时打印到终端直接看到服务端事件流是否按data:格式输出。如果浏览器里页面白屏优先检查浏览器Console里的跨域报错如果接口返回非2xx状态码优先检查网关配置而不是前端代码。部署到服务器时Nginx需要在location里配置正确的proxy_buffering off否则SSE数据会被Nginx缓冲前端等半天才一次性收到全部内容。最后补一个容易被漏掉的生产细节上线前在源码里把config.js中的接口地址、模型名、温度参数拆成独立配置项不要把apiKey硬编码进app.js或HTML里。文旅项目经常要对接不同景区的网关地址配置项独立后运营或现场实施人员只改一行地址就能切换环境这个习惯比任何代码优化都更能减少上线后的维护成本。本文还有配套的精品资源点击获取 SEO 优化官网定制响应式建站教育培训建站