项目简介 ​

MentalHealthy 是一个面向心理健康场景的 Web 平台,把 AI 心理咨询对话、情绪日记、心理健康知识库 放在同一个应用里,同时提供一个管理后台,用来查看平台数据、管理知识库文章和回溯咨询记录。

前端由我独立完成。本仓库是平台的前端部分,基于 Vue 3 + Vite 构建;后端接口是项目自带的,前端通过 /api 前缀调用。

🔗 在线演示:后端是课程提供的服务器,AI 咨询、知识库等内容需要登录后才能查看。

功能详解 ​

平台有两类用户:普通用户使用 AI 咨询、情绪日记和知识库;管理员进入后台查看数据、管理内容。两类用户登录后会被路由守卫分别带到各自的界面。

用户端首页:「一次温暖的对话,化孤独为慰藉」,两个按钮分别进入 AI 咨询和情绪日记用户端首页:「一次温暖的对话,化孤独为慰藉」,两个按钮分别进入 AI 咨询和情绪日记

页面一览 ​

端页面做什么
用户端首页一句温暖的标语和「开始倾诉」「记录心情」两个入口
用户端AI 心理咨询和 AI 流式对话,右侧「情绪花园」实时展示情绪状态和风险等级
用户端情绪日记记录当天的情绪评分、主要情绪、睡眠、压力和触发因素
用户端知识库按分类浏览心理健康文章,分页阅读文章详情
管理端数据驾驶舱平台概览卡片和三张趋势图
管理端情绪日志查看、删除用户的情绪记录,查看单条记录的详情和风险描述
管理端咨询记录按会话列出咨询记录,点开回溯用户和 AI 的完整对话
管理端知识库管理文章的新建、编辑、发布、下线和删除
公共登录 / 注册登录后按用户类型进入用户端或管理端

1. AI 心理咨询 ​

  • 流式对话:AI 回复像打字一样逐字出现,支持 Markdown 格式(代码块、粗体、标题、列表、链接)。AI 还在输出时不能再次发送,出错时会提示重试。
  • 会话管理:侧边栏里有最近的会话列表,可以切换查看历史对话,也可以删除会话。点击「新建会话」不会马上在后端建会话,等用户发出第一条消息时才真正创建,会话标题自动生成。
  • 情绪花园:每轮回复结束后,重新拉取这次会话的情绪分析结果,在侧边栏展示当前情绪状态、风险等级(正常 → 关注 → 预警 → 危机)、一句小建议和几条治愈行动;情绪为负面且风险达到「预警」及以上时,还会额外弹出风险提示。

AI 心理咨询页:左侧是「宁渡 AI 助手」和情绪花园(情绪状态、风险等级、小建议、治愈小行动),右侧是对话区,右上角的 + 号用来新建会话AI 心理咨询页:左侧是「宁渡 AI 助手」和情绪花园(情绪状态、风险等级、小建议、治愈小行动),右侧是对话区,右上角的 + 号用来新建会话

2. 情绪日记 ​

情绪评分用 10 颗星表示,每一档都配有文字(从「绝望崩溃」到「愉悦舒心」);主要情绪用一组带表情图片的卡片来选(开心、平静、焦虑……);再加上睡眠质量(很差 → 优秀)、压力水平(很低 → 很高)、情绪触发因素和日记正文,一次提交。

情绪日记页:用 10 颗星给今天的情绪打分,再从开心、平静、焦虑、悲伤、兴奋、疲惫、惊讶、困惑 8 种情绪卡片里选出主要情绪情绪日记页:用 10 颗星给今天的情绪打分,再从开心、平静、焦虑、悲伤、兴奋、疲惫、惊讶、困惑 8 种情绪卡片里选出主要情绪

3. 知识库 ​

用户端按卡片列表展示文章,每篇标注所属分类,支持分页,点击进入文章详情页阅读。管理端的文章增删改查见下文第 5 节。

4. 数据驾驶舱(管理端) ​

顶部是用户总数等平台概览卡片,下面三张 ECharts 图:

  • 情绪趋势分析:平均情绪评分和记录数量的折线图;
  • 咨询活动统计:会话数量和参与用户数的柱状图;
  • 用户活跃度趋势:新增用户、活跃用户、写日记的用户、做咨询的用户四条折线。

5. 内容与记录管理(管理端) ​

  • 知识库管理:文章表格支持发布、下线和删除,操作前都有二次确认。新建和编辑共用一个弹窗:标题、所属分类、摘要、标签(可多选,也可以输入新标签)、封面图片上传和富文本正文;编辑时自动回显原内容。
  • 情绪日志管理:表格列出用户的情绪记录,点开可以看到情绪评分、生活指标、触发因素、日记正文、专业建议、改善建议和风险描述,可以删除记录。
  • 咨询记录:按会话列出用户和 AI 的对话,点开查看完整的消息记录。

技术栈 ​

分类技术用在哪里
框架Vue 3(组合式 API)、Vue Router、Pinia用户端 / 管理端 / 登录注册三套布局,按布局组织路由
构建Vite开发服务器代理 /api 到后端
UIElement Plus、SCSS表单、表格、对话框、消息提示
可视化ECharts管理端数据驾驶舱
网络Axios、fetch-event-sourceAxios 统一封装普通请求;fetch-event-source 处理 AI 对话的 SSE 流式响应
编辑器wangEditor知识库文章的富文本编辑

项目结构 ​

前端按「用户端 / 管理端 / 登录注册」三套布局组织路由:

text
/            用户端(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,用于预览和保存。
评论
我要评论