项目简介
MentalHealthy 是一个面向心理健康场景的 Web 平台,把 AI 心理咨询对话、情绪日记、心理健康知识库 放在同一个应用里,同时提供一个管理后台,用来查看平台数据、管理知识库文章和回溯咨询记录。
前端由我独立完成。本仓库是平台的前端部分,基于 Vue 3 + Vite 构建;后端接口是项目自带的,前端通过 /api 前缀调用。
🔗 在线演示:后端是课程提供的服务器,AI 咨询、知识库等内容需要登录后才能查看。
功能详解
平台有两类用户:普通用户使用 AI 咨询、情绪日记和知识库;管理员进入后台查看数据、管理内容。两类用户登录后会被路由守卫分别带到各自的界面。
用户端首页:「一次温暖的对话,化孤独为慰藉」,两个按钮分别进入 AI 咨询和情绪日记
页面一览
| 端 | 页面 | 做什么 |
|---|---|---|
| 用户端 | 首页 | 一句温暖的标语和「开始倾诉」「记录心情」两个入口 |
| 用户端 | AI 心理咨询 | 和 AI 流式对话,右侧「情绪花园」实时展示情绪状态和风险等级 |
| 用户端 | 情绪日记 | 记录当天的情绪评分、主要情绪、睡眠、压力和触发因素 |
| 用户端 | 知识库 | 按分类浏览心理健康文章,分页阅读文章详情 |
| 管理端 | 数据驾驶舱 | 平台概览卡片和三张趋势图 |
| 管理端 | 情绪日志 | 查看、删除用户的情绪记录,查看单条记录的详情和风险描述 |
| 管理端 | 咨询记录 | 按会话列出咨询记录,点开回溯用户和 AI 的完整对话 |
| 管理端 | 知识库管理 | 文章的新建、编辑、发布、下线和删除 |
| 公共 | 登录 / 注册 | 登录后按用户类型进入用户端或管理端 |
1. AI 心理咨询
- 流式对话:AI 回复像打字一样逐字出现,支持 Markdown 格式(代码块、粗体、标题、列表、链接)。AI 还在输出时不能再次发送,出错时会提示重试。
- 会话管理:侧边栏里有最近的会话列表,可以切换查看历史对话,也可以删除会话。点击「新建会话」不会马上在后端建会话,等用户发出第一条消息时才真正创建,会话标题自动生成。
- 情绪花园:每轮回复结束后,重新拉取这次会话的情绪分析结果,在侧边栏展示当前情绪状态、风险等级(正常 → 关注 → 预警 → 危机)、一句小建议和几条治愈行动;情绪为负面且风险达到「预警」及以上时,还会额外弹出风险提示。
AI 心理咨询页:左侧是「宁渡 AI 助手」和情绪花园(情绪状态、风险等级、小建议、治愈小行动),右侧是对话区,右上角的 + 号用来新建会话
2. 情绪日记
情绪评分用 10 颗星表示,每一档都配有文字(从「绝望崩溃」到「愉悦舒心」);主要情绪用一组带表情图片的卡片来选(开心、平静、焦虑……);再加上睡眠质量(很差 → 优秀)、压力水平(很低 → 很高)、情绪触发因素和日记正文,一次提交。
情绪日记页:用 10 颗星给今天的情绪打分,再从开心、平静、焦虑、悲伤、兴奋、疲惫、惊讶、困惑 8 种情绪卡片里选出主要情绪
3. 知识库
用户端按卡片列表展示文章,每篇标注所属分类,支持分页,点击进入文章详情页阅读。管理端的文章增删改查见下文第 5 节。
4. 数据驾驶舱(管理端)
顶部是用户总数等平台概览卡片,下面三张 ECharts 图:
- 情绪趋势分析:平均情绪评分和记录数量的折线图;
- 咨询活动统计:会话数量和参与用户数的柱状图;
- 用户活跃度趋势:新增用户、活跃用户、写日记的用户、做咨询的用户四条折线。
5. 内容与记录管理(管理端)
- 知识库管理:文章表格支持发布、下线和删除,操作前都有二次确认。新建和编辑共用一个弹窗:标题、所属分类、摘要、标签(可多选,也可以输入新标签)、封面图片上传和富文本正文;编辑时自动回显原内容。
- 情绪日志管理:表格列出用户的情绪记录,点开可以看到情绪评分、生活指标、触发因素、日记正文、专业建议、改善建议和风险描述,可以删除记录。
- 咨询记录:按会话列出用户和 AI 的对话,点开查看完整的消息记录。
技术栈
| 分类 | 技术 | 用在哪里 |
|---|---|---|
| 框架 | Vue 3(组合式 API)、Vue Router、Pinia | 用户端 / 管理端 / 登录注册三套布局,按布局组织路由 |
| 构建 | Vite | 开发服务器代理 /api 到后端 |
| UI | Element Plus、SCSS | 表单、表格、对话框、消息提示 |
| 可视化 | ECharts | 管理端数据驾驶舱 |
| 网络 | Axios、fetch-event-source | Axios 统一封装普通请求;fetch-event-source 处理 AI 对话的 SSE 流式响应 |
| 编辑器 | wangEditor | 知识库文章的富文本编辑 |
项目结构
前端按「用户端 / 管理端 / 登录注册」三套布局组织路由:
/ 用户端(FrontendLayout):首页、AI 咨询、情绪日记、知识库、文章详情
/back 管理端(BackendLayout):数据驾驶舱、知识库、咨询记录、情绪日志
/auth 登录、注册(AuthLayout)我负责的部分
前端部分由我独立完成,包括:
- 管理端(先做):侧边栏和路由框架、知识库文章的增删改查与发布下线(富文本编辑、封面上传、对话框回显)、情绪日志管理、咨询记录、数据驾驶舱。
- 用户端(后做):首页、AI 心理咨询(流式对话、会话管理、情绪和风险展示)、情绪日记、知识库阅读。
- 公共部分:登录注册页面、按用户类型分流的路由守卫、Axios 请求封装、简易 Markdown 渲染组件。
难点与解决
1. AI 回复的流式输出
问题:AI 回复要逐字显示。浏览器原生的 EventSource 只支持 GET 请求,也不能加自定义请求头,而对话接口是 POST,请求体里要带会话 ID 和消息内容,请求头里还要带 Token。
解决:改用 @microsoft/fetch-event-source,它基于 fetch,可以发 POST、带自定义请求头,同时能逐条接收服务端推送的事件。具体处理:
- 校验响应类型:在
onopen里检查响应是不是流式数据。一开始用相等判断Content-Type,后来改成判断是否包含text/event-stream,因为响应头里可能还带有字符集等参数,相等判断会把正常的流误判成错误。 - 拼接回复:先在消息列表里插一条空的 AI 消息占位,每收到一段数据就解析 JSON,把内容追加到这条消息上,页面就呈现出逐字输出的效果。
- 结束和中断:收到
done事件后用AbortController主动断开连接,并重新拉取这次会话的情绪状态。 - 出错不重连:fetch-event-source 默认在出错后会自动重连,对聊天来说就是把同一条消息再发一遍。所以在
onerror里把错误重新抛出,让它停止重试。 - 防重复和出错:AI 还在输出时禁止再次发送;出错时把占位消息改成「AI 回复失败,请重试」,不会留下一条空消息。
2. 新对话什么时候在后端创建
问题:如果用户一点「新对话」就调接口创建会话,只要点了但没说话,后端就会留下一堆空会话,会话列表也会被刷满。另外,会话列表接口返回的是数字 ID,而流式对话和情绪分析接口要求的是带 session_ 前缀的 ID,两边的格式不一样。
解决:
- 点「新对话」时只在前端创建一个临时会话(状态标为
TEMP);用户发出第一条消息时,才调接口创建真正的会话,并把这条消息作为开场白一起传过去。创建成功后,用后端返回的会话信息替换掉临时会话,刷新列表,再开始流式请求。 - 统一处理会话 ID:从列表点进历史会话时补上
session_前缀;请求情绪分析时,如果 ID 已经带了前缀就不再重复添加。
3. AI 回复的 Markdown 渲染与 XSS 防护
问题:AI 回复里常带有代码块、粗体、标题、列表等 Markdown 格式,需要渲染成 HTML 用 v-html 显示。但如果回复内容里混进了 HTML 标签,直接渲染就有 XSS 风险。
解决:自己写了一个简易的 Markdown 渲染组件:先把内容里的 <、> 全部转义,再用正则依次处理代码块、行内代码、粗体、斜体、标题、链接和列表;生成的链接统一加上 target="_blank" 和 rel="noopener noreferrer"。
4. 两类用户的路由分流和登录过期
问题:普通用户和管理员共用一套前端,但能进入的页面完全不同;每个页面还都要处理「登录过期」,分散写容易漏,而且登录接口本身返回错误时,也不应该再跳一次登录页。
解决:
- 路由守卫:在全局
beforeEach里根据本地保存的用户类型分流:管理员只能访问/back下的后台页面,访问其他页面会被带回数据驾驶舱;普通用户访问后台或登录页会被带回首页;没登录的人访问后台会被带到登录页。 - 请求封装:Axios 请求拦截器统一带上 Token;响应拦截器统一判断业务状态码:成功时直接返回
data,登录过期时清除本地的 Token 和用户信息并跳转到登录页(请求的是登录接口时除外,避免死循环),其他错误统一弹出提示。页面里只需要处理成功的结果。
5. 富文本编辑器的封装
问题:知识库文章要用富文本编辑,wangEditor 本身不是 Vue 组件,直接在页面里使用会遇到几个问题:编辑器实例如果放进普通的响应式对象,会被 Vue 深度代理,导致异常;弹窗关闭后编辑器不销毁,会留下内存泄漏;父组件还希望能像普通输入框一样用 v-model 绑定内容。
解决:封装成一个 RichTextEditor 组件:编辑器实例用 shallowRef 保存,避免深度代理;组件卸载前调用 destroy() 销毁编辑器;内容通过 computed 的 get / set 对接 modelValue 和 update:modelValue,外部直接用 v-model 就行。字号、字体、颜色这些菜单也在组件里统一配置好。
6. 文章弹窗的回显和提交
问题:新建和编辑文章共用一个弹窗,容易出现几类问题:编辑时表单里要显示原来的内容,但后端的标签是逗号分隔的字符串,而多选框需要的是数组;提交时如果直接在表单对象上把数组改成字符串,多选框绑定的值就被改坏了;封面图片还要单独上传。
解决:
- 监听传进弹窗的编辑数据,有数据时逐项回显,把标签字符串
split(',')成数组;关闭弹窗时resetFields()清空表单,新建和编辑互不干扰。 - 提交时不改原表单,而是用展开运算符复制出一份新对象,在副本上把标签
join(',')成字符串。 - 封面用
el-upload的自定义上传:先校验文件类型,再用FormData以二进制方式上传,同时用crypto.randomUUID()生成业务 ID 一起提交,上传成功后把返回的地址拼成完整 URL,用于预览和保存。
