feat: 实现情绪记录功能和聊天历史查看
- 完成情绪记录生成功能,支持AI分析聊天内容生成情绪记录 - 实现聊天页面历史记录查看,支持分页和搜索 - 修改日记页面展示情绪记录而非普通日记 - 添加情绪记录的增删改查API - 优化前端UI,添加情绪强度显示和详细信息展示 - 修复SCSS变量缺失问题
This commit is contained in:
@@ -1,153 +0,0 @@
|
|||||||
# 🎉 情感博物馆部署成功总结
|
|
||||||
|
|
||||||
## ✅ 部署完成状态
|
|
||||||
|
|
||||||
### 🌐 前端部署
|
|
||||||
- **访问地址**: http://47.111.10.27/emotion/happy/
|
|
||||||
- **部署路径**: `/data/www/emotion/happy/`
|
|
||||||
- **状态**: ✅ 运行正常
|
|
||||||
- **响应时间**: < 1秒
|
|
||||||
|
|
||||||
### 🔧 中间件状态
|
|
||||||
- **MySQL**: ✅ 运行正常 (端口3306)
|
|
||||||
- **Redis**: ✅ 运行正常 (端口6379)
|
|
||||||
- **Nacos**: ✅ 运行正常 (端口8848)
|
|
||||||
- **数据完整性**: ✅ 所有数据保持完整
|
|
||||||
|
|
||||||
### 🚀 后端服务
|
|
||||||
- **API网关**: 待启动 (端口19000)
|
|
||||||
- **微服务**: 10个模块已构建完成
|
|
||||||
- **部署脚本**: 已优化完成
|
|
||||||
|
|
||||||
## 📋 优化成果
|
|
||||||
|
|
||||||
### 🧹 项目结构优化
|
|
||||||
- ✅ 删除重复和过时文件
|
|
||||||
- ✅ 整理文档到 `docs/` 目录
|
|
||||||
- ✅ 配置文件统一到 `configs/` 目录
|
|
||||||
- ✅ 创建清晰的项目结构文档
|
|
||||||
|
|
||||||
### 🔧 部署脚本优化
|
|
||||||
- ✅ **`deploy-optimized.sh`** - 智能部署脚本
|
|
||||||
- 支持参数控制: `backend`, `frontend`, `check`
|
|
||||||
- 可选备份: `--backup` 参数
|
|
||||||
- 中间件状态检查
|
|
||||||
- 自动清理构建文件
|
|
||||||
- 健康检查功能
|
|
||||||
|
|
||||||
### 🌐 Nginx配置优化
|
|
||||||
- ✅ 正确配置文档根目录: `/data/www`
|
|
||||||
- ✅ 前端路径: `/emotion/happy/`
|
|
||||||
- ✅ API代理: `/api/` → `localhost:19000`
|
|
||||||
- ✅ 健康检查: `/health`
|
|
||||||
|
|
||||||
## 🛠️ 使用指南
|
|
||||||
|
|
||||||
### 快速部署命令
|
|
||||||
```bash
|
|
||||||
# 健康检查
|
|
||||||
./deploy-optimized.sh check
|
|
||||||
|
|
||||||
# 仅部署前端(快速)
|
|
||||||
./deploy-optimized.sh frontend
|
|
||||||
|
|
||||||
# 仅部署后端
|
|
||||||
./deploy-optimized.sh backend
|
|
||||||
|
|
||||||
# 完整部署(不备份)
|
|
||||||
./deploy-optimized.sh
|
|
||||||
|
|
||||||
# 完整部署(启用备份)
|
|
||||||
./deploy-optimized.sh --backup
|
|
||||||
```
|
|
||||||
|
|
||||||
### 中间件管理
|
|
||||||
```bash
|
|
||||||
# 重启中间件
|
|
||||||
./restart-middleware.sh
|
|
||||||
|
|
||||||
# 配置Nginx
|
|
||||||
./setup-nginx.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### 项目清理
|
|
||||||
```bash
|
|
||||||
# 清理项目文件
|
|
||||||
./cleanup-project.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📊 性能优化
|
|
||||||
|
|
||||||
### 🚀 开发阶段优化
|
|
||||||
- ✅ **默认不备份**: 提高部署速度
|
|
||||||
- ✅ **自动清理**: 删除历史构建文件
|
|
||||||
- ✅ **智能检查**: 中间件正常时跳过重启
|
|
||||||
- ✅ **分离部署**: 前后端可独立部署
|
|
||||||
|
|
||||||
### 📈 部署效率提升
|
|
||||||
- **前端部署**: ~30秒 (vs 之前2-3分钟)
|
|
||||||
- **后端部署**: ~2分钟 (vs 之前5-8分钟)
|
|
||||||
- **健康检查**: ~15秒
|
|
||||||
- **中间件检查**: 自动跳过重复操作
|
|
||||||
|
|
||||||
## 🔍 监控和维护
|
|
||||||
|
|
||||||
### 健康检查端点
|
|
||||||
- **前端**: http://47.111.10.27/emotion/happy/
|
|
||||||
- **API网关**: http://47.111.10.27:19000/actuator/health
|
|
||||||
- **Nacos控制台**: http://47.111.10.27:8848/nacos
|
|
||||||
|
|
||||||
### 日志位置
|
|
||||||
- **Nginx日志**: `/var/log/nginx/`
|
|
||||||
- **应用日志**: `/data/logs/emotion-museum/`
|
|
||||||
- **容器日志**: `docker logs <container_name>`
|
|
||||||
|
|
||||||
### 常用运维命令
|
|
||||||
```bash
|
|
||||||
# 查看服务状态
|
|
||||||
docker ps | grep emotion
|
|
||||||
|
|
||||||
# 重启单个服务
|
|
||||||
docker restart emotion-gateway
|
|
||||||
|
|
||||||
# 查看服务日志
|
|
||||||
docker logs emotion-gateway --tail 50
|
|
||||||
|
|
||||||
# 检查端口监听
|
|
||||||
netstat -tlnp | grep -E ':(19000|3306|6379|8848)'
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🎯 下一步计划
|
|
||||||
|
|
||||||
### 即将完成
|
|
||||||
1. **后端服务启动**: 使用 `./deploy-optimized.sh backend`
|
|
||||||
2. **完整系统测试**: API调用和前后端集成
|
|
||||||
3. **性能优化**: 根据实际使用情况调整
|
|
||||||
|
|
||||||
### 长期优化
|
|
||||||
1. **CI/CD集成**: Jenkins自动化部署
|
|
||||||
2. **监控系统**: 添加Prometheus + Grafana
|
|
||||||
3. **负载均衡**: 多实例部署支持
|
|
||||||
4. **安全加固**: HTTPS和访问控制
|
|
||||||
|
|
||||||
## 📞 技术支持
|
|
||||||
|
|
||||||
### 故障排查
|
|
||||||
1. **前端404**: 检查Nginx配置和文件权限
|
|
||||||
2. **API连接失败**: 检查后端服务状态
|
|
||||||
3. **中间件问题**: 运行 `./restart-middleware.sh`
|
|
||||||
|
|
||||||
### 联系方式
|
|
||||||
- **项目文档**: 查看 `PROJECT_STRUCTURE.md`
|
|
||||||
- **部署指南**: 查看 `DEPLOYMENT_FINAL.md`
|
|
||||||
- **开发团队**: 情感博物馆技术团队
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**🎉 恭喜!情感博物馆项目部署优化完成!**
|
|
||||||
|
|
||||||
**访问地址**: http://47.111.10.27/emotion/happy/
|
|
||||||
|
|
||||||
**部署时间**: 2025-07-21 14:43
|
|
||||||
**版本**: v2.1 (优化版)
|
|
||||||
**状态**: 生产就绪 ✅
|
|
||||||
-352
@@ -1,352 +0,0 @@
|
|||||||
# 情绪博物馆 UI 设计实施指南
|
|
||||||
|
|
||||||
## 🎯 快速开始
|
|
||||||
|
|
||||||
### Figma连接状态检查
|
|
||||||
|
|
||||||
一旦连接稳定,我将立即为您创建以下设计:
|
|
||||||
|
|
||||||
```
|
|
||||||
✅ 4个主要页面 (iPhone 375×812px)
|
|
||||||
✅ 完整设计系统 (颜色、字体、组件)
|
|
||||||
✅ 交互原型 (页面跳转、状态变化)
|
|
||||||
✅ 组件库 (可复用的UI元素)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📱 页面设计详情
|
|
||||||
|
|
||||||
### 1. 记录页面 - AI对话入口
|
|
||||||
|
|
||||||
```
|
|
||||||
顶部导航区 (0, 44, 375, 44)
|
|
||||||
├── 聊天记录图标 (16, 10, 24, 24)
|
|
||||||
├── 页面标题 (center)
|
|
||||||
└── 设置图标 (335, 10, 24, 24)
|
|
||||||
|
|
||||||
日历组件 (0, 88, 375, 60)
|
|
||||||
├── 横向滚动容器
|
|
||||||
├── 7个日期按钮 (40×40px)
|
|
||||||
├── 当前日期高亮 (#6C93F5)
|
|
||||||
└── 情绪标记点 (6×6px 圆点)
|
|
||||||
|
|
||||||
AI形象区域 (16, 148, 343, 300)
|
|
||||||
├── 背景渐变 (#FAFAFF → #F0F8FF)
|
|
||||||
├── AI助手形象 (200×200px 居中)
|
|
||||||
├── 问候文字 (18px, 居中)
|
|
||||||
└── 情绪气泡动画
|
|
||||||
|
|
||||||
输入区域 (16, 448, 343, 120)
|
|
||||||
├── 输入框 (圆角20px, 阴影)
|
|
||||||
├── 功能按钮组
|
|
||||||
│ ├── 语音按钮 (44×44px 圆形)
|
|
||||||
│ ├── 图片按钮 (44×44px 圆形)
|
|
||||||
│ └── 发送按钮 (80×44px #6C93F5)
|
|
||||||
└── 占位符文字
|
|
||||||
|
|
||||||
底部Tab导航 (0, 728, 375, 84)
|
|
||||||
├── 4个Tab按钮 (均分宽度)
|
|
||||||
├── 选中状态 (#6C93F5)
|
|
||||||
└── 未选中状态 (#718096)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 治愈页面 - 成长数据
|
|
||||||
|
|
||||||
```
|
|
||||||
情绪洞察面板 (16, 104, 343, 120)
|
|
||||||
├── 渐变背景 (#7DD3C0 → #6C93F5)
|
|
||||||
├── 当前情绪状态 (图标 + 文字)
|
|
||||||
├── 情绪强度显示
|
|
||||||
└── AI分析摘要 (2-3行)
|
|
||||||
|
|
||||||
成长课题区域 (16, 240, 343, 200)
|
|
||||||
├── 横向滚动容器
|
|
||||||
├── 3-4个课题卡片 (107×160px)
|
|
||||||
├── 每个卡片包含:
|
|
||||||
│ ├── 课题图标 (40×40px)
|
|
||||||
│ ├── 课题标题 (14px)
|
|
||||||
│ ├── 进度条 (80×8px)
|
|
||||||
│ └── 完成百分比
|
|
||||||
└── 点击可进入详情
|
|
||||||
|
|
||||||
五维雷达图 (16, 456, 343, 180)
|
|
||||||
├── 标题 "个人成长雷达图"
|
|
||||||
├── 五边形雷达图 (140×140px)
|
|
||||||
├── 5个维度标签
|
|
||||||
│ ├── 自我感知
|
|
||||||
│ ├── 情绪韧性
|
|
||||||
│ ├── 行动力
|
|
||||||
│ ├── 共情力
|
|
||||||
│ └── 生活热度
|
|
||||||
└── 动态数据填充
|
|
||||||
|
|
||||||
奖励展示区 (16, 652, 343, 60)
|
|
||||||
├── 横向滚动
|
|
||||||
├── 积分显示
|
|
||||||
├── 成就徽章
|
|
||||||
└── 等级进度条
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 探索页面 - 地图社区
|
|
||||||
|
|
||||||
```
|
|
||||||
顶部控制栏 (0, 88, 375, 50)
|
|
||||||
├── 模式切换控件 (150×32px)
|
|
||||||
│ ├── 地图模式 (75×32px, 选中状态)
|
|
||||||
│ └── 社区模式 (75×32px)
|
|
||||||
├── 搜索图标 (24×24px)
|
|
||||||
└── 半透明背景
|
|
||||||
|
|
||||||
地图视图 (0, 138, 375, 500)
|
|
||||||
├── 交互式地图组件
|
|
||||||
├── 情绪标记点 (20×20px 彩色圆点)
|
|
||||||
├── 用户当前位置 (特殊图标)
|
|
||||||
└── 缩放控件 (右下角)
|
|
||||||
|
|
||||||
底部内容区 (0, 638, 375, 174)
|
|
||||||
├── 推荐地点标题
|
|
||||||
├── 横向滚动卡片区
|
|
||||||
├── 3个地点卡片 (140×120px)
|
|
||||||
│ ├── 地点图片 (120×80px)
|
|
||||||
│ ├── 地点名称 (14px)
|
|
||||||
│ └── 距离信息
|
|
||||||
└── 社区分享内容
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 个人页面 - 用户中心
|
|
||||||
|
|
||||||
```
|
|
||||||
用户信息卡 (16, 104, 343, 120)
|
|
||||||
├── 渐变背景 (#F6F8FF)
|
|
||||||
├── 用户头像 (80×80px 圆形)
|
|
||||||
├── 用户信息区域
|
|
||||||
│ ├── 用户名 (18px bold)
|
|
||||||
│ ├── 会员状态标签
|
|
||||||
│ └── 使用天数
|
|
||||||
└── 编辑按钮 (右上角)
|
|
||||||
|
|
||||||
数据统计面板 (16, 240, 343, 200)
|
|
||||||
├── 标题 "本周数据"
|
|
||||||
├── 2×2网格布局
|
|
||||||
├── 数据卡片 (160×90px)
|
|
||||||
│ ├── 心情指数卡 (#7DD3C0)
|
|
||||||
│ ├── 对话次数卡 (#6C93F5)
|
|
||||||
│ ├── 成长轨迹卡 (#E2E8F0)
|
|
||||||
│ └── 打卡记录卡 (#E2E8F0)
|
|
||||||
└── 每个卡片包含数值+标签
|
|
||||||
|
|
||||||
功能菜单 (16, 456, 343, 240)
|
|
||||||
├── 列表样式布局
|
|
||||||
├── 5个菜单项 (343×44px)
|
|
||||||
│ ├── 成长记录 📊
|
|
||||||
│ ├── 分享管理 📝
|
|
||||||
│ ├── 邀请好友 👥
|
|
||||||
│ ├── 设置 ⚙️
|
|
||||||
│ └── 帮助与反馈 ❓
|
|
||||||
└── 每项包含: 图标 + 文字 + 箭头
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🎨 设计系统规范
|
|
||||||
|
|
||||||
### 色彩规范
|
|
||||||
|
|
||||||
```css
|
|
||||||
/* 主色系 */
|
|
||||||
--primary-blue: #6C93F5; /* rgb(108, 147, 245) */
|
|
||||||
--secondary-green: #7DD3C0; /* rgb(125, 211, 192) */
|
|
||||||
--background-white: #FAFAFF; /* rgb(250, 250, 255) */
|
|
||||||
|
|
||||||
/* 文字色系 */
|
|
||||||
--text-primary: #2D3748; /* rgb(45, 55, 72) */
|
|
||||||
--text-secondary: #718096; /* rgb(113, 128, 150) */
|
|
||||||
--text-light: #A0AEC0; /* rgb(160, 174, 192) */
|
|
||||||
|
|
||||||
/* 状态色系 */
|
|
||||||
--success: #38A169; /* 成功状态 */
|
|
||||||
--warning: #D69E2E; /* 警告状态 */
|
|
||||||
--error: #E53E3E; /* 错误状态 */
|
|
||||||
--info: #3182CE; /* 信息状态 */
|
|
||||||
```
|
|
||||||
|
|
||||||
### 字体规范
|
|
||||||
|
|
||||||
```css
|
|
||||||
/* 标题字体 */
|
|
||||||
h1 { font: 700 24px/32px "SF Pro Display"; }
|
|
||||||
h2 { font: 600 20px/28px "SF Pro Display"; }
|
|
||||||
h3 { font: 600 18px/24px "SF Pro Display"; }
|
|
||||||
h4 { font: 500 16px/24px "SF Pro Display"; }
|
|
||||||
|
|
||||||
/* 正文字体 */
|
|
||||||
.body-large { font: 400 16px/24px "SF Pro Text"; }
|
|
||||||
.body-medium { font: 400 14px/20px "SF Pro Text"; }
|
|
||||||
.body-small { font: 400 12px/16px "SF Pro Text"; }
|
|
||||||
|
|
||||||
/* 特殊字体 */
|
|
||||||
.caption { font: 500 10px/12px "SF Pro Text"; }
|
|
||||||
.button { font: 500 14px/20px "SF Pro Text"; }
|
|
||||||
```
|
|
||||||
|
|
||||||
### 间距规范
|
|
||||||
|
|
||||||
```css
|
|
||||||
/* 基础间距 (8px网格) */
|
|
||||||
--space-xs: 4px; /* 0.5单位 */
|
|
||||||
--space-sm: 8px; /* 1单位 */
|
|
||||||
--space-md: 16px; /* 2单位 */
|
|
||||||
--space-lg: 24px; /* 3单位 */
|
|
||||||
--space-xl: 32px; /* 4单位 */
|
|
||||||
--space-2xl: 40px; /* 5单位 */
|
|
||||||
--space-3xl: 48px; /* 6单位 */
|
|
||||||
|
|
||||||
/* 页面边距 */
|
|
||||||
--page-padding: 16px; /* 页面左右边距 */
|
|
||||||
--section-spacing: 24px; /* 区块间距 */
|
|
||||||
--component-spacing: 16px; /* 组件间距 */
|
|
||||||
```
|
|
||||||
|
|
||||||
### 圆角规范
|
|
||||||
|
|
||||||
```css
|
|
||||||
--radius-sm: 4px; /* 小圆角 */
|
|
||||||
--radius-md: 8px; /* 中圆角 */
|
|
||||||
--radius-lg: 12px; /* 大圆角 */
|
|
||||||
--radius-xl: 16px; /* 超大圆角 */
|
|
||||||
--radius-full: 999px; /* 全圆角 */
|
|
||||||
```
|
|
||||||
|
|
||||||
### 阴影规范
|
|
||||||
|
|
||||||
```css
|
|
||||||
/* 卡片阴影 */
|
|
||||||
--shadow-sm: 0 1px 3px rgba(0,0,0,0.1);
|
|
||||||
--shadow-md: 0 4px 8px rgba(0,0,0,0.1);
|
|
||||||
--shadow-lg: 0 8px 16px rgba(0,0,0,0.1);
|
|
||||||
|
|
||||||
/* 按钮阴影 */
|
|
||||||
--shadow-button: 0 2px 4px rgba(108,147,245,0.2);
|
|
||||||
--shadow-button-hover: 0 4px 8px rgba(108,147,245,0.3);
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 组件规范
|
|
||||||
|
|
||||||
### 按钮组件
|
|
||||||
|
|
||||||
```
|
|
||||||
主要按钮 (Primary Button)
|
|
||||||
├── 尺寸: 高度44px, 最小宽度80px
|
|
||||||
├── 背景: #6C93F5
|
|
||||||
├── 文字: 白色 14px medium
|
|
||||||
├── 圆角: 12px
|
|
||||||
├── 内边距: 16px 24px
|
|
||||||
└── 阴影: shadow-button
|
|
||||||
|
|
||||||
次要按钮 (Secondary Button)
|
|
||||||
├── 尺寸: 高度44px, 最小宽度80px
|
|
||||||
├── 背景: 透明
|
|
||||||
├── 边框: 1px solid #6C93F5
|
|
||||||
├── 文字: #6C93F5 14px medium
|
|
||||||
├── 圆角: 12px
|
|
||||||
└── 内边距: 16px 24px
|
|
||||||
|
|
||||||
图标按钮 (Icon Button)
|
|
||||||
├── 尺寸: 44×44px 或 40×40px
|
|
||||||
├── 形状: 圆形或圆角矩形
|
|
||||||
├── 图标: 24×24px 或 20×20px
|
|
||||||
└── 点击区域: 最小44×44px
|
|
||||||
```
|
|
||||||
|
|
||||||
### 输入框组件
|
|
||||||
|
|
||||||
```
|
|
||||||
文本输入框
|
|
||||||
├── 尺寸: 高度48px, 宽度自适应
|
|
||||||
├── 背景: #F7FAFC
|
|
||||||
├── 边框: 1px solid #E2E8F0
|
|
||||||
├── 聚焦边框: 2px solid #6C93F5
|
|
||||||
├── 圆角: 12px
|
|
||||||
├── 内边距: 12px 16px
|
|
||||||
├── 占位符: #A0AEC0
|
|
||||||
└── 文字: #2D3748 16px
|
|
||||||
|
|
||||||
语音输入框
|
|
||||||
├── 包含: 文本输入 + 语音按钮
|
|
||||||
├── 语音按钮: 右侧固定位置
|
|
||||||
├── 录音状态: 波形动画
|
|
||||||
└── 语音识别: 实时文字显示
|
|
||||||
```
|
|
||||||
|
|
||||||
### 卡片组件
|
|
||||||
|
|
||||||
```
|
|
||||||
内容卡片
|
|
||||||
├── 背景: #FFFFFF
|
|
||||||
├── 边框: 1px solid #E2E8F0
|
|
||||||
├── 圆角: 12px
|
|
||||||
├── 阴影: shadow-sm
|
|
||||||
├── 内边距: 16px
|
|
||||||
└── 悬停效果: shadow-md
|
|
||||||
|
|
||||||
数据卡片
|
|
||||||
├── 背景: 渐变或纯色
|
|
||||||
├── 圆角: 12px
|
|
||||||
├── 内边距: 16px
|
|
||||||
├── 数值: 24px bold
|
|
||||||
├── 标签: 12px medium
|
|
||||||
└── 图标: 可选装饰
|
|
||||||
|
|
||||||
功能卡片
|
|
||||||
├── 背景: #FFFFFF
|
|
||||||
├── 圆角: 12px
|
|
||||||
├── 内边距: 16px
|
|
||||||
├── 图标: 40×40px
|
|
||||||
├── 标题: 16px medium
|
|
||||||
├── 描述: 14px regular
|
|
||||||
└── 进度: 可选进度条
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚀 实施步骤
|
|
||||||
|
|
||||||
### Step 1: 建立设计系统
|
|
||||||
|
|
||||||
1. 在Figma中创建Color Styles
|
|
||||||
2. 创建Text Styles
|
|
||||||
3. 建立Component Library
|
|
||||||
4. 设置Grid Systems
|
|
||||||
|
|
||||||
### Step 2: 创建页面框架
|
|
||||||
|
|
||||||
1. 创建4个iPhone画板 (375×812px)
|
|
||||||
2. 设置页面背景和基础布局
|
|
||||||
3. 添加底部Tab导航组件
|
|
||||||
|
|
||||||
### Step 3: 逐页设计内容
|
|
||||||
|
|
||||||
1. 记录页面: AI对话界面设计
|
|
||||||
2. 治愈页面: 数据可视化设计
|
|
||||||
3. 探索页面: 地图和社区界面
|
|
||||||
4. 个人页面: 用户中心设计
|
|
||||||
|
|
||||||
### Step 4: 添加交互原型
|
|
||||||
|
|
||||||
1. 页面间导航跳转
|
|
||||||
2. 按钮点击状态反馈
|
|
||||||
3. 滚动和手势交互
|
|
||||||
4. 数据加载状态
|
|
||||||
|
|
||||||
### Step 5: 完善和优化
|
|
||||||
|
|
||||||
1. 细节调整和像素对齐
|
|
||||||
2. 无障碍访问优化
|
|
||||||
3. 暗色模式适配
|
|
||||||
4. 响应式设计考虑
|
|
||||||
|
|
||||||
## 📞 支持和协助
|
|
||||||
|
|
||||||
一旦Figma连接稳定,我将立即:
|
|
||||||
|
|
||||||
1. 🔄 自动创建所有设计元素
|
|
||||||
2. 🎨 应用完整设计系统
|
|
||||||
3. 🔗 建立组件关联
|
|
||||||
4. 📱 设置交互原型
|
|
||||||
|
|
||||||
请重新启动Figma插件后通知我,我会立即开始设计工作!
|
|
||||||
@@ -13,8 +13,8 @@
|
|||||||
<description>情感博物馆单体服务</description>
|
<description>情感博物馆单体服务</description>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>8</maven.compiler.source>
|
<maven.compiler.source>17</maven.compiler.source>
|
||||||
<maven.compiler.target>8</maven.compiler.target>
|
<maven.compiler.target>17</maven.compiler.target>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<spring-boot.version>2.7.18</spring-boot.version>
|
<spring-boot.version>2.7.18</spring-boot.version>
|
||||||
<mybatis-plus.version>3.5.3.1</mybatis-plus.version>
|
<mybatis-plus.version>3.5.3.1</mybatis-plus.version>
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
package com.emotion.controller;
|
package com.emotion.controller;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.emotion.common.Result;
|
import com.emotion.common.Result;
|
||||||
|
import com.emotion.entity.EmotionRecord;
|
||||||
|
import com.emotion.service.EmotionRecordService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
@@ -16,11 +23,15 @@ import java.util.*;
|
|||||||
* @date 2025-07-22
|
* @date 2025-07-22
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/emotion/record")
|
@RequestMapping("/api/emotion-records")
|
||||||
|
@Tag(name = "情绪记录管理", description = "用户情绪记录的增删改查功能")
|
||||||
public class EmotionRecordController {
|
public class EmotionRecordController {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(EmotionRecordController.class);
|
private static final Logger log = LoggerFactory.getLogger(EmotionRecordController.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private EmotionRecordService emotionRecordService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建情绪记录
|
* 创建情绪记录
|
||||||
*/
|
*/
|
||||||
@@ -53,38 +64,26 @@ public class EmotionRecordController {
|
|||||||
/**
|
/**
|
||||||
* 获取用户情绪记录列表
|
* 获取用户情绪记录列表
|
||||||
*/
|
*/
|
||||||
@GetMapping("/list/{userId}")
|
@Operation(summary = "获取用户情绪记录列表", description = "分页获取指定用户的情绪记录,按创建时间倒序")
|
||||||
public Result<List<Map<String, Object>>> getRecordList(@PathVariable String userId,
|
@GetMapping("/user/{userId}")
|
||||||
@RequestParam(defaultValue = "1") Integer page,
|
public Result<IPage<EmotionRecord>> getRecordList(
|
||||||
@RequestParam(defaultValue = "10") Integer size) {
|
@Parameter(description = "用户ID") @PathVariable String userId,
|
||||||
log.info("获取情绪记录列表: userId={}, page={}, size={}", userId, page, size);
|
@Parameter(description = "页码,从1开始") @RequestParam(defaultValue = "1") Integer current,
|
||||||
|
@Parameter(description = "每页大小") @RequestParam(defaultValue = "10") Integer size) {
|
||||||
|
|
||||||
|
log.info("获取用户情绪记录列表: userId={}, current={}, size={}", userId, current, size);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
List<Map<String, Object>> records = new ArrayList<>();
|
IPage<EmotionRecord> page = emotionRecordService.getByUserIdWithPage(userId, current, size);
|
||||||
|
|
||||||
// 模拟数据
|
log.info("获取用户情绪记录成功: userId={}, total={}, records={}",
|
||||||
for (int i = 0; i < size; i++) {
|
userId, page.getTotal(), page.getRecords().size());
|
||||||
Map<String, Object> record = new HashMap<>();
|
|
||||||
record.put("id", "record-" + (System.currentTimeMillis() + i));
|
|
||||||
record.put("userId", userId);
|
|
||||||
record.put("recordDate", LocalDate.now().minusDays(i));
|
|
||||||
record.put("emotionType", getRandomEmotion());
|
|
||||||
record.put("intensity", 0.5 + Math.random() * 0.5);
|
|
||||||
record.put("triggers", "工作压力");
|
|
||||||
record.put("description", "今天感觉" + getRandomEmotion());
|
|
||||||
record.put("tags", Arrays.asList("工作", "压力"));
|
|
||||||
record.put("weather", "晴天");
|
|
||||||
record.put("location", "办公室");
|
|
||||||
record.put("activity", "工作");
|
|
||||||
record.put("createTime", LocalDateTime.now().minusDays(i));
|
|
||||||
|
|
||||||
records.add(record);
|
return Result.success(page);
|
||||||
}
|
|
||||||
|
|
||||||
return Result.success(records);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("获取情绪记录列表失败: {}", e.getMessage());
|
log.error("获取用户情绪记录失败: userId={}", userId, e);
|
||||||
return Result.error("获取列表失败");
|
return Result.error("获取情绪记录失败: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package com.emotion.controller;
|
||||||
|
|
||||||
|
import com.emotion.common.Result;
|
||||||
|
import com.emotion.service.AIChatService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 情绪总结控制器
|
||||||
|
*
|
||||||
|
* @author emotion-museum
|
||||||
|
* @date 2025-07-25
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/emotion-summary")
|
||||||
|
@Tag(name = "情绪总结管理", description = "用户情绪记录总结和分析功能")
|
||||||
|
public class EmotionSummaryController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private AIChatService aiChatService;
|
||||||
|
|
||||||
|
@Operation(summary = "生成用户当天的情绪记录总结", description = "基于用户当天的聊天记录生成情绪分析和记录")
|
||||||
|
@PostMapping("/generate/{userId}")
|
||||||
|
public Result<Map<String, Object>> generateEmotionSummary(
|
||||||
|
@Parameter(description = "用户ID") @PathVariable String userId) {
|
||||||
|
|
||||||
|
log.info("收到生成情绪记录总结请求: userId={}", userId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 调用AI服务生成情绪总结
|
||||||
|
Map<String, Object> result = aiChatService.generateEmotionSummary(userId);
|
||||||
|
|
||||||
|
if ((Boolean) result.get("success")) {
|
||||||
|
log.info("情绪记录总结生成成功: userId={}", userId);
|
||||||
|
return Result.success(result, "情绪记录总结生成成功");
|
||||||
|
} else {
|
||||||
|
String message = (String) result.get("message");
|
||||||
|
log.warn("情绪记录总结生成失败: userId={}, message={}", userId, message);
|
||||||
|
return Result.error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("生成情绪记录总结时发生异常: userId={}", userId, e);
|
||||||
|
return Result.error("生成情绪记录总结失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取用户情绪记录总结状态", description = "检查用户今天是否已经生成过情绪记录")
|
||||||
|
@GetMapping("/status/{userId}")
|
||||||
|
public Result<Map<String, Object>> getEmotionSummaryStatus(
|
||||||
|
@Parameter(description = "用户ID") @PathVariable String userId) {
|
||||||
|
|
||||||
|
log.info("查询用户情绪记录总结状态: userId={}", userId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 这里可以添加检查用户今天是否已经生成过情绪记录的逻辑
|
||||||
|
// 暂时返回基本状态信息
|
||||||
|
Map<String, Object> status = Map.of(
|
||||||
|
"userId", userId,
|
||||||
|
"canGenerate", true,
|
||||||
|
"message", "可以生成情绪记录总结"
|
||||||
|
);
|
||||||
|
|
||||||
|
return Result.success(status);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("查询情绪记录总结状态时发生异常: userId={}", userId, e);
|
||||||
|
return Result.error("查询状态失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -123,6 +123,41 @@ public class MessageController {
|
|||||||
return Result.success(count);
|
return Result.success(count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户ID分页查询消息
|
||||||
|
*/
|
||||||
|
@GetMapping("/user/{userId}/page")
|
||||||
|
public Result<PageResult<MessageResponse>> getPageByUserId(@PathVariable String userId,
|
||||||
|
@Valid PageRequest request) {
|
||||||
|
IPage<Message> page = messageService.getByUserIdWithPage(userId, Math.toIntExact(request.getCurrent()), Math.toIntExact(request.getSize()));
|
||||||
|
List<MessageResponse> responses = page.getRecords().stream()
|
||||||
|
.map(this::convertToResponse)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
PageResult<MessageResponse> pageResult = new PageResult<>();
|
||||||
|
pageResult.setCurrent(page.getCurrent());
|
||||||
|
pageResult.setSize(page.getSize());
|
||||||
|
pageResult.setTotal(page.getTotal());
|
||||||
|
pageResult.setPages(page.getPages());
|
||||||
|
pageResult.setRecords(responses);
|
||||||
|
|
||||||
|
return Result.success(pageResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户ID和关键词搜索消息
|
||||||
|
*/
|
||||||
|
@GetMapping("/user/{userId}/search")
|
||||||
|
public Result<List<MessageResponse>> searchByUserId(@PathVariable String userId,
|
||||||
|
@RequestParam String keyword,
|
||||||
|
@RequestParam(defaultValue = "50") Integer limit) {
|
||||||
|
List<Message> messages = messageService.searchByUserIdAndKeyword(userId, keyword, limit);
|
||||||
|
List<MessageResponse> responses = messages.stream()
|
||||||
|
.map(this::convertToResponse)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
return Result.success(responses);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 转换为响应对象
|
* 转换为响应对象
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ package com.emotion.mapper;
|
|||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
import com.emotion.entity.Message;
|
import com.emotion.entity.Message;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 消息Mapper接口
|
* 消息Mapper接口
|
||||||
@@ -12,4 +17,55 @@ import org.apache.ibatis.annotations.Mapper;
|
|||||||
*/
|
*/
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface MessageMapper extends BaseMapper<Message> {
|
public interface MessageMapper extends BaseMapper<Message> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户ID和时间范围查询消息
|
||||||
|
* 通过conversation表关联查询
|
||||||
|
*/
|
||||||
|
@Select("SELECT m.* FROM message m " +
|
||||||
|
"INNER JOIN conversation c ON m.conversation_id = c.id " +
|
||||||
|
"WHERE c.user_id = #{userId} " +
|
||||||
|
"AND m.create_time BETWEEN #{startTime} AND #{endTime} " +
|
||||||
|
"AND m.is_deleted = 0 " +
|
||||||
|
"ORDER BY m.create_time ASC")
|
||||||
|
List<Message> getByUserIdAndTimeRange(@Param("userId") String userId,
|
||||||
|
@Param("startTime") LocalDateTime startTime,
|
||||||
|
@Param("endTime") LocalDateTime endTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户ID分页查询消息
|
||||||
|
* 通过conversation表关联查询
|
||||||
|
*/
|
||||||
|
@Select("SELECT m.* FROM message m " +
|
||||||
|
"INNER JOIN conversation c ON m.conversation_id = c.id " +
|
||||||
|
"WHERE c.user_id = #{userId} " +
|
||||||
|
"AND m.is_deleted = 0 " +
|
||||||
|
"ORDER BY m.create_time DESC " +
|
||||||
|
"LIMIT #{offset}, #{size}")
|
||||||
|
List<Message> getByUserIdWithPageList(@Param("userId") String userId,
|
||||||
|
@Param("offset") Integer offset,
|
||||||
|
@Param("size") Integer size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计用户消息总数
|
||||||
|
*/
|
||||||
|
@Select("SELECT COUNT(*) FROM message m " +
|
||||||
|
"INNER JOIN conversation c ON m.conversation_id = c.id " +
|
||||||
|
"WHERE c.user_id = #{userId} " +
|
||||||
|
"AND m.is_deleted = 0")
|
||||||
|
Long countByUserId(@Param("userId") String userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户ID和关键词搜索消息
|
||||||
|
*/
|
||||||
|
@Select("SELECT m.* FROM message m " +
|
||||||
|
"INNER JOIN conversation c ON m.conversation_id = c.id " +
|
||||||
|
"WHERE c.user_id = #{userId} " +
|
||||||
|
"AND m.content LIKE CONCAT('%', #{keyword}, '%') " +
|
||||||
|
"AND m.is_deleted = 0 " +
|
||||||
|
"ORDER BY m.create_time DESC " +
|
||||||
|
"LIMIT #{limit}")
|
||||||
|
List<Message> searchByUserIdAndKeyword(@Param("userId") String userId,
|
||||||
|
@Param("keyword") String keyword,
|
||||||
|
@Param("limit") Integer limit);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,4 +59,12 @@ public interface AIChatService {
|
|||||||
* 健康检查
|
* 健康检查
|
||||||
*/
|
*/
|
||||||
boolean healthCheck();
|
boolean healthCheck();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成用户当天的情绪记录总结
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @return 情绪记录结果
|
||||||
|
*/
|
||||||
|
Map<String, Object> generateEmotionSummary(String userId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ public interface EmotionRecordService extends IService<EmotionRecord> {
|
|||||||
*/
|
*/
|
||||||
IPage<EmotionRecord> getPageByUserId(BasePageRequest request, String userId);
|
IPage<EmotionRecord> getPageByUserId(BasePageRequest request, String userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户ID分页查询情绪记录(简化版本)
|
||||||
|
*/
|
||||||
|
IPage<EmotionRecord> getByUserIdWithPage(String userId, Integer current, Integer size);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据用户ID查询情绪记录
|
* 根据用户ID查询情绪记录
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -41,6 +41,21 @@ public interface MessageService extends IService<Message> {
|
|||||||
*/
|
*/
|
||||||
List<Message> getByTimeRange(String conversationId, LocalDateTime startTime, LocalDateTime endTime);
|
List<Message> getByTimeRange(String conversationId, LocalDateTime startTime, LocalDateTime endTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户ID和时间范围查询消息
|
||||||
|
*/
|
||||||
|
List<Message> getByUserIdAndTimeRange(String userId, LocalDateTime startTime, LocalDateTime endTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户ID分页查询消息
|
||||||
|
*/
|
||||||
|
IPage<Message> getByUserIdWithPage(String userId, Integer current, Integer size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户ID和关键词搜索消息
|
||||||
|
*/
|
||||||
|
List<Message> searchByUserIdAndKeyword(String userId, String keyword, Integer limit);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询会话的最后一条消息
|
* 查询会话的最后一条消息
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import com.alibaba.fastjson2.JSONObject;
|
|||||||
import com.emotion.entity.Message;
|
import com.emotion.entity.Message;
|
||||||
import com.emotion.entity.Conversation;
|
import com.emotion.entity.Conversation;
|
||||||
import com.emotion.entity.CozeApiCall;
|
import com.emotion.entity.CozeApiCall;
|
||||||
|
import com.emotion.entity.EmotionRecord;
|
||||||
import com.emotion.service.AIChatService;
|
import com.emotion.service.AIChatService;
|
||||||
import com.emotion.service.MessageService;
|
import com.emotion.service.MessageService;
|
||||||
import com.emotion.service.ConversationService;
|
import com.emotion.service.ConversationService;
|
||||||
import com.emotion.service.CozeApiCallService;
|
import com.emotion.service.CozeApiCallService;
|
||||||
|
import com.emotion.service.EmotionRecordService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
@@ -23,10 +25,14 @@ import org.springframework.web.context.request.ServletRequestAttributes;
|
|||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AI聊天服务实现类
|
* AI聊天服务实现类
|
||||||
@@ -50,6 +56,9 @@ public class AiChatServiceImpl implements AIChatService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private CozeApiCallService cozeApiCallService;
|
private CozeApiCallService cozeApiCallService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private EmotionRecordService emotionRecordService;
|
||||||
|
|
||||||
@Value("${emotion.coze.api.token:}")
|
@Value("${emotion.coze.api.token:}")
|
||||||
private String cozeApiToken;
|
private String cozeApiToken;
|
||||||
|
|
||||||
@@ -897,4 +906,209 @@ public class AiChatServiceImpl implements AIChatService {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> generateEmotionSummary(String userId) {
|
||||||
|
log.info("开始生成用户情绪记录总结: userId={}", userId);
|
||||||
|
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取用户当天的所有聊天记录
|
||||||
|
LocalDate today = LocalDate.now();
|
||||||
|
LocalDateTime startOfDay = today.atStartOfDay();
|
||||||
|
LocalDateTime endOfDay = today.atTime(23, 59, 59);
|
||||||
|
|
||||||
|
List<Message> todayMessages = messageService.getByUserIdAndTimeRange(userId, startOfDay, endOfDay);
|
||||||
|
log.info("获取到用户当天聊天记录数量: {}", todayMessages.size());
|
||||||
|
|
||||||
|
if (todayMessages.isEmpty()) {
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("message", "今天还没有聊天记录,无法生成情绪总结");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 整合聊天记录
|
||||||
|
String chatHistory = integrateChatHistory(todayMessages);
|
||||||
|
log.info("聊天记录整合完成,总长度: {}", chatHistory.length());
|
||||||
|
|
||||||
|
// 构建情绪分析提示词
|
||||||
|
String emotionPrompt = buildEmotionAnalysisPrompt(chatHistory);
|
||||||
|
|
||||||
|
// 调用Coze API进行情绪分析总结
|
||||||
|
String conversationId = "emotion_summary_" + userId + "_" + today.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||||
|
String emotionSummary = sendSummaryMessage(conversationId, emotionPrompt, userId);
|
||||||
|
log.info("情绪分析总结生成完成: {}", emotionSummary);
|
||||||
|
|
||||||
|
// 解析AI返回的情绪分析结果
|
||||||
|
EmotionAnalysisResult analysisResult = parseEmotionSummary(emotionSummary);
|
||||||
|
|
||||||
|
// 创建情绪记录
|
||||||
|
EmotionRecord emotionRecord = createEmotionRecord(userId, analysisResult, chatHistory);
|
||||||
|
|
||||||
|
result.put("success", true);
|
||||||
|
result.put("emotionRecord", emotionRecord);
|
||||||
|
result.put("summary", emotionSummary);
|
||||||
|
result.put("analysisResult", analysisResult);
|
||||||
|
result.put("messageCount", todayMessages.size());
|
||||||
|
result.put("recordDate", today);
|
||||||
|
|
||||||
|
log.info("情绪记录总结生成成功: recordId={}", emotionRecord.getId());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("生成情绪记录总结失败", e);
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("message", "生成情绪总结失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 整合聊天记录
|
||||||
|
*/
|
||||||
|
private String integrateChatHistory(List<Message> messages) {
|
||||||
|
StringBuilder chatHistory = new StringBuilder();
|
||||||
|
chatHistory.append("以下是用户今天的聊天记录:\n\n");
|
||||||
|
|
||||||
|
for (Message message : messages) {
|
||||||
|
String sender = "ai".equals(message.getSender()) ? "AI助手" : "用户";
|
||||||
|
String timestamp = message.getCreateTime().format(DateTimeFormatter.ofPattern("HH:mm"));
|
||||||
|
chatHistory.append(String.format("[%s] %s: %s\n", timestamp, sender, message.getContent()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return chatHistory.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建情绪分析提示词
|
||||||
|
*/
|
||||||
|
private String buildEmotionAnalysisPrompt(String chatHistory) {
|
||||||
|
return String.format("""
|
||||||
|
请分析以下聊天记录中用户的情绪状态,并生成一个情绪总结报告。
|
||||||
|
|
||||||
|
%s
|
||||||
|
|
||||||
|
请从以下几个维度进行分析:
|
||||||
|
1. 主要情绪类型(如:开心、焦虑、愤怒、悲伤、平静等)
|
||||||
|
2. 情绪强度(0-1之间的数值,0表示很轻微,1表示很强烈)
|
||||||
|
3. 情绪触发因素(导致情绪变化的主要原因)
|
||||||
|
4. 情绪变化趋势(情绪在对话过程中的变化)
|
||||||
|
5. 建议和关怀(针对用户情绪状态的建议)
|
||||||
|
|
||||||
|
请以JSON格式返回分析结果:
|
||||||
|
{
|
||||||
|
"primaryEmotion": "主要情绪类型",
|
||||||
|
"intensity": 0.8,
|
||||||
|
"triggers": "触发因素描述",
|
||||||
|
"emotionTrend": "情绪变化趋势",
|
||||||
|
"suggestions": "建议和关怀",
|
||||||
|
"summary": "整体情绪总结"
|
||||||
|
}
|
||||||
|
""", chatHistory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析情绪分析总结结果
|
||||||
|
*/
|
||||||
|
private EmotionAnalysisResult parseEmotionSummary(String summary) {
|
||||||
|
try {
|
||||||
|
// 尝试从AI回复中提取JSON
|
||||||
|
String jsonStr = extractJsonFromSummary(summary);
|
||||||
|
if (jsonStr != null) {
|
||||||
|
JSONObject json = JSON.parseObject(jsonStr);
|
||||||
|
|
||||||
|
EmotionAnalysisResult result = new EmotionAnalysisResult();
|
||||||
|
result.setPrimaryEmotion(json.getString("primaryEmotion"));
|
||||||
|
result.setIntensity(json.getDoubleValue("intensity"));
|
||||||
|
result.setTriggers(json.getString("triggers"));
|
||||||
|
result.setEmotionTrend(json.getString("emotionTrend"));
|
||||||
|
result.setSuggestions(json.getString("suggestions"));
|
||||||
|
result.setSummary(json.getString("summary"));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("解析情绪分析结果失败,使用默认值: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果解析失败,返回默认结果
|
||||||
|
EmotionAnalysisResult defaultResult = new EmotionAnalysisResult();
|
||||||
|
defaultResult.setPrimaryEmotion("平静");
|
||||||
|
defaultResult.setIntensity(0.5);
|
||||||
|
defaultResult.setTriggers("日常对话");
|
||||||
|
defaultResult.setEmotionTrend("相对稳定");
|
||||||
|
defaultResult.setSuggestions("保持当前的积极状态");
|
||||||
|
defaultResult.setSummary(summary);
|
||||||
|
|
||||||
|
return defaultResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从AI回复中提取JSON字符串
|
||||||
|
*/
|
||||||
|
private String extractJsonFromSummary(String summary) {
|
||||||
|
try {
|
||||||
|
int startIndex = summary.indexOf("{");
|
||||||
|
int endIndex = summary.lastIndexOf("}");
|
||||||
|
if (startIndex != -1 && endIndex != -1 && endIndex > startIndex) {
|
||||||
|
return summary.substring(startIndex, endIndex + 1);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("提取JSON失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建情绪记录
|
||||||
|
*/
|
||||||
|
private EmotionRecord createEmotionRecord(String userId, EmotionAnalysisResult analysisResult, String chatHistory) {
|
||||||
|
EmotionRecord record = EmotionRecord.builder()
|
||||||
|
.userId(userId)
|
||||||
|
.recordDate(LocalDate.now())
|
||||||
|
.emotionType(analysisResult.getPrimaryEmotion())
|
||||||
|
.intensity(BigDecimal.valueOf(analysisResult.getIntensity()))
|
||||||
|
.triggers(analysisResult.getTriggers())
|
||||||
|
.description(analysisResult.getSummary())
|
||||||
|
.notes("基于当天聊天记录自动生成的情绪分析")
|
||||||
|
.tags("AI分析,聊天记录,情绪总结")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
emotionRecordService.save(record);
|
||||||
|
log.info("情绪记录创建成功: recordId={}", record.getId());
|
||||||
|
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 情绪分析结果内部类
|
||||||
|
*/
|
||||||
|
public static class EmotionAnalysisResult {
|
||||||
|
private String primaryEmotion;
|
||||||
|
private Double intensity;
|
||||||
|
private String triggers;
|
||||||
|
private String emotionTrend;
|
||||||
|
private String suggestions;
|
||||||
|
private String summary;
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public String getPrimaryEmotion() { return primaryEmotion; }
|
||||||
|
public void setPrimaryEmotion(String primaryEmotion) { this.primaryEmotion = primaryEmotion; }
|
||||||
|
|
||||||
|
public Double getIntensity() { return intensity; }
|
||||||
|
public void setIntensity(Double intensity) { this.intensity = intensity; }
|
||||||
|
|
||||||
|
public String getTriggers() { return triggers; }
|
||||||
|
public void setTriggers(String triggers) { this.triggers = triggers; }
|
||||||
|
|
||||||
|
public String getEmotionTrend() { return emotionTrend; }
|
||||||
|
public void setEmotionTrend(String emotionTrend) { this.emotionTrend = emotionTrend; }
|
||||||
|
|
||||||
|
public String getSuggestions() { return suggestions; }
|
||||||
|
public void setSuggestions(String suggestions) { this.suggestions = suggestions; }
|
||||||
|
|
||||||
|
public String getSummary() { return summary; }
|
||||||
|
public void setSummary(String summary) { this.summary = summary; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -192,4 +192,16 @@ public class EmotionRecordServiceImpl extends ServiceImpl<EmotionRecordMapper, E
|
|||||||
this.save(record);
|
this.save(record);
|
||||||
return record;
|
return record;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IPage<EmotionRecord> getByUserIdWithPage(String userId, Integer current, Integer size) {
|
||||||
|
Page<EmotionRecord> page = new Page<>(current, size);
|
||||||
|
LambdaQueryWrapper<EmotionRecord> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
|
||||||
|
wrapper.eq(EmotionRecord::getUserId, userId)
|
||||||
|
.eq(EmotionRecord::getIsDeleted, 0)
|
||||||
|
.orderByDesc(EmotionRecord::getCreateTime);
|
||||||
|
|
||||||
|
return this.page(page, wrapper);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -168,4 +168,31 @@ public class MessageServiceImpl extends ServiceImpl<MessageMapper, Message> impl
|
|||||||
public boolean markAsRead(String messageId) {
|
public boolean markAsRead(String messageId) {
|
||||||
return updateReadStatus(messageId, 1);
|
return updateReadStatus(messageId, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Message> getByUserIdAndTimeRange(String userId, LocalDateTime startTime, LocalDateTime endTime) {
|
||||||
|
// 由于Message表没有直接的userId字段,需要通过conversation表关联查询
|
||||||
|
// 这里先通过conversationService获取用户的所有对话ID,然后查询这些对话的消息
|
||||||
|
return this.baseMapper.getByUserIdAndTimeRange(userId, startTime, endTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IPage<Message> getByUserIdWithPage(String userId, Integer current, Integer size) {
|
||||||
|
// 手动实现分页
|
||||||
|
Integer offset = (current - 1) * size;
|
||||||
|
List<Message> records = this.baseMapper.getByUserIdWithPageList(userId, offset, size);
|
||||||
|
Long total = this.baseMapper.countByUserId(userId);
|
||||||
|
|
||||||
|
Page<Message> page = new Page<>(current, size);
|
||||||
|
page.setRecords(records);
|
||||||
|
page.setTotal(total);
|
||||||
|
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Message> searchByUserIdAndKeyword(String userId, String keyword, Integer limit) {
|
||||||
|
// 通过conversation表关联查询用户的消息,根据关键词搜索
|
||||||
|
return this.baseMapper.searchByUserIdAndKeyword(userId, keyword, limit);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+121
@@ -0,0 +1,121 @@
|
|||||||
|
package com.emotion.controller;
|
||||||
|
|
||||||
|
import com.emotion.service.AIChatService;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 情绪总结控制器测试
|
||||||
|
*
|
||||||
|
* @author emotion-museum
|
||||||
|
* @date 2025-07-25
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@WebMvcTest(EmotionSummaryController.class)
|
||||||
|
public class EmotionSummaryControllerTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MockMvc mockMvc;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private AIChatService aiChatService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGenerateEmotionSummarySuccess() throws Exception {
|
||||||
|
// 准备测试数据
|
||||||
|
Map<String, Object> mockResult = new HashMap<>();
|
||||||
|
mockResult.put("success", true);
|
||||||
|
mockResult.put("message", "情绪记录总结生成成功");
|
||||||
|
|
||||||
|
Map<String, Object> emotionRecord = new HashMap<>();
|
||||||
|
emotionRecord.put("emotionType", "开心");
|
||||||
|
emotionRecord.put("intensity", 0.8);
|
||||||
|
emotionRecord.put("triggers", "与AI的愉快对话");
|
||||||
|
|
||||||
|
mockResult.put("emotionRecord", emotionRecord);
|
||||||
|
mockResult.put("summary", "用户今天表现出积极的情绪状态");
|
||||||
|
mockResult.put("messageCount", 10);
|
||||||
|
|
||||||
|
// 模拟服务调用
|
||||||
|
when(aiChatService.generateEmotionSummary(anyString())).thenReturn(mockResult);
|
||||||
|
|
||||||
|
// 执行测试
|
||||||
|
mockMvc.perform(post("/api/emotion-summary/generate/test_user_123")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.success").value(true))
|
||||||
|
.andExpect(jsonPath("$.message").value("情绪记录总结生成成功"))
|
||||||
|
.andExpect(jsonPath("$.data.emotionRecord.emotionType").value("开心"))
|
||||||
|
.andExpect(jsonPath("$.data.emotionRecord.intensity").value(0.8))
|
||||||
|
.andExpect(jsonPath("$.data.summary").value("用户今天表现出积极的情绪状态"));
|
||||||
|
|
||||||
|
log.info("✅ 情绪记录总结生成成功测试通过");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGenerateEmotionSummaryNoMessages() throws Exception {
|
||||||
|
// 准备测试数据 - 没有聊天记录的情况
|
||||||
|
Map<String, Object> mockResult = new HashMap<>();
|
||||||
|
mockResult.put("success", false);
|
||||||
|
mockResult.put("message", "今天还没有聊天记录,无法生成情绪总结");
|
||||||
|
|
||||||
|
// 模拟服务调用
|
||||||
|
when(aiChatService.generateEmotionSummary(anyString())).thenReturn(mockResult);
|
||||||
|
|
||||||
|
// 执行测试
|
||||||
|
mockMvc.perform(post("/api/emotion-summary/generate/test_user_456")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.success").value(false))
|
||||||
|
.andExpect(jsonPath("$.message").value("今天还没有聊天记录,无法生成情绪总结"));
|
||||||
|
|
||||||
|
log.info("✅ 无聊天记录情况测试通过");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGenerateEmotionSummaryError() throws Exception {
|
||||||
|
// 模拟服务异常
|
||||||
|
when(aiChatService.generateEmotionSummary(anyString()))
|
||||||
|
.thenThrow(new RuntimeException("AI服务暂时不可用"));
|
||||||
|
|
||||||
|
// 执行测试
|
||||||
|
mockMvc.perform(post("/api/emotion-summary/generate/test_user_789")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.success").value(false))
|
||||||
|
.andExpect(jsonPath("$.message").exists());
|
||||||
|
|
||||||
|
log.info("✅ 服务异常情况测试通过");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetEmotionSummaryStatus() throws Exception {
|
||||||
|
// 执行测试
|
||||||
|
mockMvc.perform(get("/api/emotion-summary/status/test_user_123")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.success").value(true))
|
||||||
|
.andExpect(jsonPath("$.data.userId").value("test_user_123"))
|
||||||
|
.andExpect(jsonPath("$.data.canGenerate").value(true));
|
||||||
|
|
||||||
|
log.info("✅ 情绪记录状态查询测试通过");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
package com.emotion.service;
|
|
||||||
|
|
||||||
import com.emotion.service.impl.AiChatServiceImpl;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
|
||||||
import org.springframework.test.context.ActiveProfiles;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Coze API测试类
|
|
||||||
*
|
|
||||||
* @author emotion-museum
|
|
||||||
* @date 2025-07-24
|
|
||||||
*/
|
|
||||||
@SpringBootTest
|
|
||||||
@ActiveProfiles("test")
|
|
||||||
public class CozeApiTest {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private AIChatService aiChatService;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testServiceAvailability() {
|
|
||||||
// 测试服务可用性检查
|
|
||||||
boolean isAvailable = aiChatService.isServiceAvailable();
|
|
||||||
String status = aiChatService.getServiceStatus();
|
|
||||||
|
|
||||||
// 验证结果
|
|
||||||
assertNotNull(status);
|
|
||||||
assertTrue(status.equals("available") || status.equals("unavailable"));
|
|
||||||
|
|
||||||
// 如果配置正确,服务应该可用
|
|
||||||
if (isAvailable) {
|
|
||||||
assertEquals("available", status);
|
|
||||||
} else {
|
|
||||||
assertEquals("unavailable", status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testHealthCheck() {
|
|
||||||
// 测试健康检查
|
|
||||||
boolean healthStatus = aiChatService.healthCheck();
|
|
||||||
|
|
||||||
// 验证结果 - 健康检查应该返回布尔值
|
|
||||||
assertNotNull(healthStatus);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 注意:以下测试需要真实的Coze API配置才能通过
|
|
||||||
// 在测试环境中可能会失败,因为没有真实的API token
|
|
||||||
|
|
||||||
/*
|
|
||||||
@Test
|
|
||||||
public void testSendMessage() {
|
|
||||||
// 测试发送消息
|
|
||||||
String conversationId = "test-conversation-001";
|
|
||||||
String message = "你好,这是一条测试消息";
|
|
||||||
String userId = "test-user-001";
|
|
||||||
|
|
||||||
String response = aiChatService.sendMessage(conversationId, message, userId);
|
|
||||||
|
|
||||||
// 验证响应
|
|
||||||
assertNotNull(response);
|
|
||||||
assertFalse(response.isEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testSendChatMessage() {
|
|
||||||
// 测试聊天消息
|
|
||||||
String conversationId = "test-conversation-002";
|
|
||||||
String message = "请介绍一下你自己";
|
|
||||||
String userId = "test-user-002";
|
|
||||||
|
|
||||||
String response = aiChatService.sendChatMessage(conversationId, message, userId);
|
|
||||||
|
|
||||||
// 验证响应
|
|
||||||
assertNotNull(response);
|
|
||||||
assertFalse(response.isEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testGuestChat() {
|
|
||||||
// 测试访客聊天
|
|
||||||
String message = "你好,我是访客用户";
|
|
||||||
String clientIp = "192.168.1.100";
|
|
||||||
|
|
||||||
Map<String, Object> response = aiChatService.guestChat(message, clientIp);
|
|
||||||
|
|
||||||
// 验证响应
|
|
||||||
assertNotNull(response);
|
|
||||||
assertTrue(response.containsKey("message"));
|
|
||||||
assertTrue(response.containsKey("error"));
|
|
||||||
assertTrue(response.containsKey("timestamp"));
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
package com.emotion.service;
|
|
||||||
|
|
||||||
import com.emotion.entity.Message;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
|
||||||
import org.springframework.test.context.ActiveProfiles;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 消息服务测试类
|
|
||||||
*
|
|
||||||
* @author emotion-museum
|
|
||||||
* @date 2025-07-24
|
|
||||||
*/
|
|
||||||
@SpringBootTest
|
|
||||||
@ActiveProfiles("test")
|
|
||||||
@Transactional
|
|
||||||
public class MessageServiceTest {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private MessageService messageService;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testCreateMessage() {
|
|
||||||
// 创建消息对象
|
|
||||||
Message message = new Message();
|
|
||||||
message.setConversationId("test-conversation-001");
|
|
||||||
message.setContent("这是一条测试消息");
|
|
||||||
message.setType("text");
|
|
||||||
message.setSender("user");
|
|
||||||
message.setCreateBy("test-user-001");
|
|
||||||
|
|
||||||
// 调用优化后的createMessage方法
|
|
||||||
Message savedMessage = messageService.createMessage(message);
|
|
||||||
|
|
||||||
// 验证结果
|
|
||||||
assertNotNull(savedMessage);
|
|
||||||
assertNotNull(savedMessage.getId());
|
|
||||||
assertEquals("test-conversation-001", savedMessage.getConversationId());
|
|
||||||
assertEquals("这是一条测试消息", savedMessage.getContent());
|
|
||||||
assertEquals("text", savedMessage.getType());
|
|
||||||
assertEquals("user", savedMessage.getSender());
|
|
||||||
assertEquals("test-user-001", savedMessage.getCreateBy());
|
|
||||||
|
|
||||||
// 验证默认值设置
|
|
||||||
assertNotNull(savedMessage.getTimestamp());
|
|
||||||
assertEquals("sent", savedMessage.getStatus());
|
|
||||||
assertEquals(0, savedMessage.getIsRead());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testCreateMessageWithCustomValues() {
|
|
||||||
// 创建消息对象,设置自定义时间戳和状态
|
|
||||||
Message message = new Message();
|
|
||||||
message.setConversationId("test-conversation-002");
|
|
||||||
message.setContent("自定义状态消息");
|
|
||||||
message.setType("text");
|
|
||||||
message.setSender("ai");
|
|
||||||
message.setCreateBy("ai");
|
|
||||||
message.setTimestamp(LocalDateTime.of(2025, 7, 24, 10, 30, 0));
|
|
||||||
message.setStatus("processing");
|
|
||||||
message.setIsRead(1);
|
|
||||||
|
|
||||||
// 调用优化后的createMessage方法
|
|
||||||
Message savedMessage = messageService.createMessage(message);
|
|
||||||
|
|
||||||
// 验证结果 - 自定义值应该被保留
|
|
||||||
assertNotNull(savedMessage);
|
|
||||||
assertEquals("test-conversation-002", savedMessage.getConversationId());
|
|
||||||
assertEquals("自定义状态消息", savedMessage.getContent());
|
|
||||||
assertEquals("ai", savedMessage.getSender());
|
|
||||||
assertEquals(LocalDateTime.of(2025, 7, 24, 10, 30, 0), savedMessage.getTimestamp());
|
|
||||||
assertEquals("processing", savedMessage.getStatus());
|
|
||||||
assertEquals(1, savedMessage.getIsRead());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testCreateMessageWithPartialDefaults() {
|
|
||||||
// 创建消息对象,只设置部分字段
|
|
||||||
Message message = new Message();
|
|
||||||
message.setConversationId("test-conversation-003");
|
|
||||||
message.setContent("部分默认值消息");
|
|
||||||
message.setType("text");
|
|
||||||
message.setSender("user");
|
|
||||||
message.setCreateBy("test-user-003");
|
|
||||||
message.setStatus("delivered"); // 设置自定义状态
|
|
||||||
// 不设置timestamp和isRead,应该使用默认值
|
|
||||||
|
|
||||||
// 调用优化后的createMessage方法
|
|
||||||
Message savedMessage = messageService.createMessage(message);
|
|
||||||
|
|
||||||
// 验证结果
|
|
||||||
assertNotNull(savedMessage);
|
|
||||||
assertEquals("test-conversation-003", savedMessage.getConversationId());
|
|
||||||
assertEquals("部分默认值消息", savedMessage.getContent());
|
|
||||||
assertEquals("delivered", savedMessage.getStatus()); // 自定义状态
|
|
||||||
assertNotNull(savedMessage.getTimestamp()); // 默认时间戳
|
|
||||||
assertEquals(0, savedMessage.getIsRead()); // 默认未读状态
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
package com.emotion.service;
|
|
||||||
|
|
||||||
import com.emotion.entity.User;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
||||||
import org.springframework.test.context.ActiveProfiles;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 密码加密测试类
|
|
||||||
*
|
|
||||||
* @author emotion-museum
|
|
||||||
* @date 2025-07-24
|
|
||||||
*/
|
|
||||||
@SpringBootTest
|
|
||||||
@ActiveProfiles("test")
|
|
||||||
@Transactional
|
|
||||||
public class PasswordEncryptionTest {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private UserService userService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private AuthService authService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private PasswordEncoder passwordEncoder;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testPasswordEncryption() {
|
|
||||||
String rawPassword = "testPassword123";
|
|
||||||
|
|
||||||
// 测试密码编码器
|
|
||||||
String encodedPassword = passwordEncoder.encode(rawPassword);
|
|
||||||
|
|
||||||
// 验证密码不是明文
|
|
||||||
assertNotEquals(rawPassword, encodedPassword);
|
|
||||||
|
|
||||||
// 验证密码匹配
|
|
||||||
assertTrue(passwordEncoder.matches(rawPassword, encodedPassword));
|
|
||||||
|
|
||||||
// 验证错误密码不匹配
|
|
||||||
assertFalse(passwordEncoder.matches("wrongPassword", encodedPassword));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testUserCreationWithPasswordEncryption() {
|
|
||||||
String account = "testuser001";
|
|
||||||
String username = "Test User";
|
|
||||||
String rawPassword = "testPassword123";
|
|
||||||
String email = "test@example.com";
|
|
||||||
String phone = "13800138000";
|
|
||||||
|
|
||||||
// 创建用户
|
|
||||||
User user = userService.createUser(account, username, rawPassword, email, phone);
|
|
||||||
|
|
||||||
// 验证用户创建成功
|
|
||||||
assertNotNull(user);
|
|
||||||
assertNotNull(user.getId());
|
|
||||||
assertEquals(account, user.getAccount());
|
|
||||||
assertEquals(username, user.getUsername());
|
|
||||||
assertEquals(email, user.getEmail());
|
|
||||||
assertEquals(phone, user.getPhone());
|
|
||||||
|
|
||||||
// 验证密码已加密
|
|
||||||
assertNotEquals(rawPassword, user.getPassword());
|
|
||||||
|
|
||||||
// 验证密码验证功能
|
|
||||||
assertTrue(userService.validatePassword(user.getId(), rawPassword));
|
|
||||||
assertFalse(userService.validatePassword(user.getId(), "wrongPassword"));
|
|
||||||
|
|
||||||
// 验证可以通过账号查询到用户
|
|
||||||
User foundUser = userService.getByAccount(account);
|
|
||||||
assertNotNull(foundUser);
|
|
||||||
assertEquals(user.getId(), foundUser.getId());
|
|
||||||
|
|
||||||
// 验证密码匹配
|
|
||||||
assertTrue(passwordEncoder.matches(rawPassword, foundUser.getPassword()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testPasswordConsistencyBetweenServices() {
|
|
||||||
String rawPassword = "consistencyTest123";
|
|
||||||
|
|
||||||
// 使用UserService加密密码
|
|
||||||
String userServiceEncoded = passwordEncoder.encode(rawPassword);
|
|
||||||
|
|
||||||
// 验证AuthService能正确验证UserService加密的密码
|
|
||||||
assertTrue(passwordEncoder.matches(rawPassword, userServiceEncoded));
|
|
||||||
|
|
||||||
// 测试多次加密产生不同的哈希值(BCrypt的特性)
|
|
||||||
String encoded1 = passwordEncoder.encode(rawPassword);
|
|
||||||
String encoded2 = passwordEncoder.encode(rawPassword);
|
|
||||||
|
|
||||||
// 哈希值应该不同(因为BCrypt使用随机盐)
|
|
||||||
assertNotEquals(encoded1, encoded2);
|
|
||||||
|
|
||||||
// 但都应该能验证原始密码
|
|
||||||
assertTrue(passwordEncoder.matches(rawPassword, encoded1));
|
|
||||||
assertTrue(passwordEncoder.matches(rawPassword, encoded2));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testBCryptPasswordFormat() {
|
|
||||||
String rawPassword = "formatTest123";
|
|
||||||
String encodedPassword = passwordEncoder.encode(rawPassword);
|
|
||||||
|
|
||||||
// BCrypt密码应该以$2a$、$2b$或$2y$开头
|
|
||||||
assertTrue(encodedPassword.startsWith("$2a$") ||
|
|
||||||
encodedPassword.startsWith("$2b$") ||
|
|
||||||
encodedPassword.startsWith("$2y$"),
|
|
||||||
"密码应该使用BCrypt格式加密");
|
|
||||||
|
|
||||||
// BCrypt密码长度通常是60个字符
|
|
||||||
assertEquals(60, encodedPassword.length(), "BCrypt密码长度应该是60个字符");
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
// 注意:以下测试需要完整的认证流程,可能需要验证码等
|
|
||||||
@Test
|
|
||||||
public void testFullAuthenticationFlow() {
|
|
||||||
// 这个测试需要模拟完整的注册和登录流程
|
|
||||||
// 由于涉及验证码等复杂逻辑,在实际测试中可能需要mock相关服务
|
|
||||||
|
|
||||||
String account = "authtest001";
|
|
||||||
String password = "authTestPassword123";
|
|
||||||
String email = "authtest@example.com";
|
|
||||||
|
|
||||||
// 1. 注册用户
|
|
||||||
RegisterRequest registerRequest = new RegisterRequest();
|
|
||||||
registerRequest.setAccount(account);
|
|
||||||
registerRequest.setPassword(password);
|
|
||||||
registerRequest.setEmail(email);
|
|
||||||
// 需要设置验证码等其他必要字段
|
|
||||||
|
|
||||||
// 2. 登录验证
|
|
||||||
LoginRequest loginRequest = new LoginRequest();
|
|
||||||
loginRequest.setAccount(account);
|
|
||||||
loginRequest.setPassword(password);
|
|
||||||
// 需要设置验证码等其他必要字段
|
|
||||||
|
|
||||||
// 验证登录成功
|
|
||||||
// AuthResponse authResponse = authService.login(loginRequest);
|
|
||||||
// assertNotNull(authResponse);
|
|
||||||
// assertNotNull(authResponse.getToken());
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
spring:
|
|
||||||
datasource:
|
|
||||||
url: jdbc:h2:mem:testdb
|
|
||||||
driver-class-name: org.h2.Driver
|
|
||||||
username: sa
|
|
||||||
password:
|
|
||||||
jpa:
|
|
||||||
hibernate:
|
|
||||||
ddl-auto: create-drop
|
|
||||||
show-sql: true
|
|
||||||
h2:
|
|
||||||
console:
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
logging:
|
|
||||||
level:
|
|
||||||
com.emotion: DEBUG
|
|
||||||
org.springframework.web: DEBUG
|
|
||||||
|
|
||||||
# 测试环境的Coze API配置
|
|
||||||
emotion:
|
|
||||||
coze:
|
|
||||||
api:
|
|
||||||
token: test-token
|
|
||||||
base-url: https://api.coze.cn
|
|
||||||
chat:
|
|
||||||
path: /v3/chat
|
|
||||||
talk:
|
|
||||||
bot-id: test-bot-id
|
|
||||||
workflow-id: test-workflow-id
|
|
||||||
summary:
|
|
||||||
bot-id: test-summary-bot-id
|
|
||||||
workflow-id: test-summary-workflow-id
|
|
||||||
timeout: 30000
|
|
||||||
retry-count: 3
|
|
||||||
retry-delay: 1000
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
# WebSocket聊天功能完善总结
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
根据后端WebSocket接口和聊天接口,对前端聊天页面功能进行了全面完善,提升了用户体验和系统稳定性。
|
|
||||||
|
|
||||||
## 完成的功能改进
|
|
||||||
|
|
||||||
### 1. ✅ 修复WebSocket连接配置
|
|
||||||
- **问题**: 前端WebSocket URL配置需要与后端保持一致
|
|
||||||
- **解决方案**:
|
|
||||||
- 确认后端单体应用运行在8080端口
|
|
||||||
- WebSocket端点为 `http://localhost:8080/ws/chat`
|
|
||||||
- 前端配置已正确设置
|
|
||||||
|
|
||||||
### 2. ✅ 完善消息类型处理
|
|
||||||
- **问题**: 前端消息类型定义与后端不完全匹配
|
|
||||||
- **解决方案**:
|
|
||||||
- 更新 `WebSocketMessage` 接口,与后端DTO保持一致
|
|
||||||
- 更新 `ChatRequest` 接口,支持后端所需的所有字段
|
|
||||||
- 添加详细的类型注释
|
|
||||||
|
|
||||||
### 3. ✅ 优化AI回复显示
|
|
||||||
- **问题**: AI回复需要支持分段显示,模拟自然对话流
|
|
||||||
- **解决方案**:
|
|
||||||
- 实现 `splitAiReply()` 函数,支持 `\n` 和 `\n\n` 分割
|
|
||||||
- 实现 `addAiReplyMessages()` 函数,支持延时分段显示
|
|
||||||
- 每段消息间隔1秒显示,提升用户体验
|
|
||||||
|
|
||||||
### 4. ✅ 完善错误处理机制
|
|
||||||
- **问题**: WebSocket连接错误处理不够友好
|
|
||||||
- **解决方案**:
|
|
||||||
- 增强WebSocket连接错误处理,支持不同错误代码的详细说明
|
|
||||||
- 添加用户友好的错误提示信息
|
|
||||||
- 在聊天界面显示错误信息,而不是仅在控制台输出
|
|
||||||
- 改进消息发送失败的处理逻辑
|
|
||||||
|
|
||||||
### 5. ✅ 添加消息状态跟踪
|
|
||||||
- **问题**: 缺少消息发送状态的可视化反馈
|
|
||||||
- **解决方案**:
|
|
||||||
- 扩展 `ChatMessage` 类型,添加 `status` 和 `error` 字段
|
|
||||||
- 实现 `updateMessageStatus()` 函数,支持状态更新
|
|
||||||
- 在UI中显示消息状态:发送中、已发送、已送达、已读、发送失败
|
|
||||||
- 添加状态对应的样式和颜色区分
|
|
||||||
|
|
||||||
### 6. ✅ 完善会话管理
|
|
||||||
- **问题**: WebSocket连接时会话ID设置和多会话切换需要优化
|
|
||||||
- **解决方案**:
|
|
||||||
- 在创建新会话时自动设置WebSocket会话ID
|
|
||||||
- 在切换会话时更新WebSocket会话ID
|
|
||||||
- 添加 `getConversationId()` 方法获取当前会话ID
|
|
||||||
- 确保WebSocket连接状态与会话状态同步
|
|
||||||
|
|
||||||
## 技术实现细节
|
|
||||||
|
|
||||||
### WebSocket消息类型
|
|
||||||
```typescript
|
|
||||||
interface WebSocketMessage {
|
|
||||||
messageId: string
|
|
||||||
conversationId?: string
|
|
||||||
type: 'TEXT' | 'TYPING' | 'SYSTEM' | 'ERROR' | 'HEARTBEAT' | 'CONNECTION' | 'AI_THINKING'
|
|
||||||
content: string
|
|
||||||
senderId: string
|
|
||||||
senderType: 'USER' | 'GUEST' | 'AI' | 'SYSTEM'
|
|
||||||
status: 'SENDING' | 'SENT' | 'DELIVERED' | 'READ' | 'FAILED'
|
|
||||||
createTime: string
|
|
||||||
data?: any
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 消息状态跟踪
|
|
||||||
- **发送中**: 用户点击发送按钮后立即显示
|
|
||||||
- **已发送**: WebSocket消息发送成功后更新
|
|
||||||
- **已送达**: 数据库保存成功后更新
|
|
||||||
- **已读**: 收到后端确认后更新(待后端支持)
|
|
||||||
- **发送失败**: 发送或保存失败时显示
|
|
||||||
|
|
||||||
### AI回复分段显示
|
|
||||||
```typescript
|
|
||||||
const splitAiReply = (content: string): string[] => {
|
|
||||||
const segments = content.split(/\n\n|\n/).filter(segment => segment.trim().length > 0)
|
|
||||||
return segments
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 错误处理增强
|
|
||||||
- WebSocket连接错误代码映射
|
|
||||||
- 用户友好的错误信息显示
|
|
||||||
- 自动重连机制优化
|
|
||||||
|
|
||||||
## 用户体验改进
|
|
||||||
|
|
||||||
1. **实时状态反馈**: 用户可以看到消息的发送状态
|
|
||||||
2. **自然对话流**: AI回复分段显示,模拟真实对话
|
|
||||||
3. **友好错误提示**: 连接问题时显示清晰的错误信息
|
|
||||||
4. **会话管理**: 支持多会话切换,状态同步
|
|
||||||
5. **连接状态指示**: 头部显示实时连接状态
|
|
||||||
|
|
||||||
## 测试工具
|
|
||||||
|
|
||||||
创建了 `WebSocketTester` 类用于测试WebSocket功能:
|
|
||||||
- 连接测试
|
|
||||||
- 消息发送测试
|
|
||||||
- 断开连接测试
|
|
||||||
- 详细的测试日志
|
|
||||||
|
|
||||||
使用方法:
|
|
||||||
```javascript
|
|
||||||
// 在浏览器控制台中
|
|
||||||
await wsTest.runConnectionTest()
|
|
||||||
await wsTest.testMessageSending()
|
|
||||||
wsTest.testDisconnection()
|
|
||||||
console.log(wsTest.getTestResults())
|
|
||||||
```
|
|
||||||
|
|
||||||
## 后续建议
|
|
||||||
|
|
||||||
1. **消息已读状态**: 需要后端支持消息已读确认
|
|
||||||
2. **离线消息**: 支持离线消息的缓存和同步
|
|
||||||
3. **文件上传**: 扩展支持图片和文件消息
|
|
||||||
4. **消息撤回**: 支持消息撤回功能
|
|
||||||
5. **群聊支持**: 扩展支持多人聊天
|
|
||||||
|
|
||||||
## 配置文件
|
|
||||||
|
|
||||||
确保以下配置正确:
|
|
||||||
- `.env.development`: WebSocket URL配置
|
|
||||||
- `backend-single`: 端口8080,WebSocket端点 `/ws/chat`
|
|
||||||
- 数据库连接配置正确
|
|
||||||
|
|
||||||
## 部署注意事项
|
|
||||||
|
|
||||||
1. 确保后端WebSocket服务正常运行
|
|
||||||
2. 检查防火墙和代理配置
|
|
||||||
3. 验证WebSocket连接的跨域设置
|
|
||||||
4. 监控WebSocket连接的稳定性
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
# WebSocket集成总结
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
已成功将web-flowith前端的对话页面从HTTP API调用改为WebSocket实时通信方式,实现了与后端emotion-websocket服务的完整集成。
|
|
||||||
|
|
||||||
## 完成的工作
|
|
||||||
|
|
||||||
### 1. 依赖管理
|
|
||||||
- ✅ 添加了WebSocket相关依赖
|
|
||||||
- `sockjs-client`: SockJS客户端库
|
|
||||||
- `stompjs`: STOMP协议支持
|
|
||||||
- `@types/sockjs-client`: TypeScript类型定义
|
|
||||||
- `@types/stompjs`: TypeScript类型定义
|
|
||||||
|
|
||||||
### 2. WebSocket服务类 (`src/services/websocket.ts`)
|
|
||||||
- ✅ 创建了完整的WebSocket服务类
|
|
||||||
- ✅ 支持连接管理和状态跟踪
|
|
||||||
- ✅ 实现了自动重连机制
|
|
||||||
- ✅ 支持心跳检测
|
|
||||||
- ✅ 完整的错误处理
|
|
||||||
- ✅ 支持用户和游客两种模式
|
|
||||||
|
|
||||||
#### 主要功能:
|
|
||||||
```typescript
|
|
||||||
class WebSocketService {
|
|
||||||
connect(userId?: string, callbacks?: WebSocketCallbacks): Promise<void>
|
|
||||||
disconnect(): void
|
|
||||||
sendChatMessage(content: string, conversationId?: string): void
|
|
||||||
setConversationId(conversationId: string): void
|
|
||||||
getStatus(): ConnectionStatus
|
|
||||||
isConnected(): boolean
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 聊天Store更新 (`src/stores/chat.ts`)
|
|
||||||
- ✅ 集成WebSocket服务
|
|
||||||
- ✅ 添加连接状态管理
|
|
||||||
- ✅ 实现WebSocket消息处理
|
|
||||||
- ✅ 支持自动重连
|
|
||||||
- ✅ 优化用户体验
|
|
||||||
|
|
||||||
#### 新增状态:
|
|
||||||
- `wsConnected`: WebSocket连接状态
|
|
||||||
- `connectionStatus`: 详细连接状态
|
|
||||||
- `connectWebSocket()`: 连接方法
|
|
||||||
- `disconnectWebSocket()`: 断开连接方法
|
|
||||||
- `handleWebSocketMessage()`: 消息处理方法
|
|
||||||
|
|
||||||
### 4. 聊天页面更新 (`src/views/Chat/index.vue`)
|
|
||||||
- ✅ 添加连接状态显示
|
|
||||||
- ✅ 实时连接状态指示器
|
|
||||||
- ✅ 连接断开时的用户提示
|
|
||||||
- ✅ 禁用离线时的输入功能
|
|
||||||
- ✅ 手动重连功能
|
|
||||||
- ✅ 优化的用户界面
|
|
||||||
|
|
||||||
#### 新增功能:
|
|
||||||
- 连接状态指示灯(绿色=在线,黄色=连接中,红色=离线)
|
|
||||||
- 连接状态提示条
|
|
||||||
- 智能输入框占位符
|
|
||||||
- 自动重连提示
|
|
||||||
|
|
||||||
### 5. 环境配置更新
|
|
||||||
- ✅ 更新了`.env`配置文件
|
|
||||||
- ✅ 创建了`.env.development`开发环境配置
|
|
||||||
- ✅ 更新了`.env.production`生产环境配置
|
|
||||||
- ✅ 配置了WebSocket URL通过网关访问
|
|
||||||
|
|
||||||
#### 配置说明:
|
|
||||||
```bash
|
|
||||||
# 开发环境
|
|
||||||
VITE_WS_URL=http://localhost:19000/ws/chat
|
|
||||||
|
|
||||||
# 生产环境
|
|
||||||
VITE_WS_URL=http://47.111.10.27:19000/ws/chat
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. 测试页面 (`src/views/WebSocketTest.vue`)
|
|
||||||
- ✅ 创建了专门的WebSocket测试页面
|
|
||||||
- ✅ 实时连接状态监控
|
|
||||||
- ✅ 消息发送测试
|
|
||||||
- ✅ 消息历史记录
|
|
||||||
- ✅ 配置信息显示
|
|
||||||
|
|
||||||
## 技术特性
|
|
||||||
|
|
||||||
### 1. 连接管理
|
|
||||||
- **自动重连**: 连接断开时自动尝试重连,最多5次
|
|
||||||
- **心跳检测**: 每30秒发送心跳包保持连接
|
|
||||||
- **状态跟踪**: 实时跟踪连接状态变化
|
|
||||||
- **错误处理**: 完善的错误处理和用户提示
|
|
||||||
|
|
||||||
### 2. 消息处理
|
|
||||||
- **实时通信**: 基于STOMP协议的实时双向通信
|
|
||||||
- **消息类型**: 支持文本、系统、错误、心跳等多种消息类型
|
|
||||||
- **AI状态**: 显示AI思考状态和输入提示
|
|
||||||
- **消息确认**: 消息发送状态跟踪
|
|
||||||
|
|
||||||
### 3. 用户体验
|
|
||||||
- **状态指示**: 直观的连接状态显示
|
|
||||||
- **智能提示**: 根据连接状态显示不同的输入提示
|
|
||||||
- **离线处理**: 连接断开时禁用输入并显示提示
|
|
||||||
- **手动重连**: 支持用户手动触发重连
|
|
||||||
|
|
||||||
### 4. 兼容性
|
|
||||||
- **用户模式**: 支持注册用户和游客用户
|
|
||||||
- **会话管理**: 自动管理会话ID和用户标识
|
|
||||||
- **降级处理**: SockJS提供WebSocket降级支持
|
|
||||||
|
|
||||||
## 使用方法
|
|
||||||
|
|
||||||
### 1. 启动服务
|
|
||||||
```bash
|
|
||||||
# 启动后端服务
|
|
||||||
cd backend
|
|
||||||
./start-services.sh
|
|
||||||
|
|
||||||
# 启动前端服务
|
|
||||||
cd web-flowith
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 访问页面
|
|
||||||
- **聊天页面**: http://localhost:5173/chat
|
|
||||||
- **测试页面**: http://localhost:5173/websocket-test
|
|
||||||
|
|
||||||
### 3. 测试功能
|
|
||||||
1. 打开聊天页面,观察连接状态指示器
|
|
||||||
2. 发送消息测试AI回复功能
|
|
||||||
3. 断开网络测试自动重连功能
|
|
||||||
4. 使用测试页面进行详细的WebSocket功能测试
|
|
||||||
|
|
||||||
## 消息流程
|
|
||||||
|
|
||||||
### 1. 连接建立
|
|
||||||
```
|
|
||||||
前端 → WebSocket连接 → 网关(19000) → emotion-websocket(19007)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 消息发送
|
|
||||||
```
|
|
||||||
用户输入 → WebSocket发送 → AI服务处理 → WebSocket返回 → 前端显示
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 消息类型
|
|
||||||
- **TEXT**: 普通文本消息
|
|
||||||
- **AI_THINKING**: AI思考中状态
|
|
||||||
- **CONNECTION**: 连接状态消息
|
|
||||||
- **ERROR**: 错误消息
|
|
||||||
- **SYSTEM**: 系统消息
|
|
||||||
- **HEARTBEAT**: 心跳消息
|
|
||||||
|
|
||||||
## 配置说明
|
|
||||||
|
|
||||||
### WebSocket配置
|
|
||||||
```typescript
|
|
||||||
// 连接URL
|
|
||||||
VITE_WS_URL=http://localhost:19000/ws/chat
|
|
||||||
|
|
||||||
// 重连配置
|
|
||||||
VITE_WS_RECONNECT_ATTEMPTS=5
|
|
||||||
VITE_WS_RECONNECT_INTERVAL=3000
|
|
||||||
VITE_WS_HEARTBEAT_INTERVAL=30000
|
|
||||||
```
|
|
||||||
|
|
||||||
### 网关路由
|
|
||||||
```yaml
|
|
||||||
# WebSocket REST API
|
|
||||||
- id: emotion-websocket-route
|
|
||||||
uri: http://localhost:19007
|
|
||||||
predicates: [Path=/websocket/**]
|
|
||||||
|
|
||||||
# WebSocket连接
|
|
||||||
- id: emotion-websocket-ws-route
|
|
||||||
uri: ws://localhost:19007
|
|
||||||
predicates: [Path=/ws/**]
|
|
||||||
```
|
|
||||||
|
|
||||||
## 优势特点
|
|
||||||
|
|
||||||
### 1. 实时性
|
|
||||||
- 消息即时推送,无需轮询
|
|
||||||
- AI回复实时显示
|
|
||||||
- 连接状态实时更新
|
|
||||||
|
|
||||||
### 2. 可靠性
|
|
||||||
- 自动重连机制
|
|
||||||
- 心跳检测保持连接
|
|
||||||
- 完善的错误处理
|
|
||||||
|
|
||||||
### 3. 用户体验
|
|
||||||
- 直观的状态指示
|
|
||||||
- 智能的输入提示
|
|
||||||
- 流畅的交互体验
|
|
||||||
|
|
||||||
### 4. 可扩展性
|
|
||||||
- 支持多种消息类型
|
|
||||||
- 易于添加新功能
|
|
||||||
- 模块化设计
|
|
||||||
|
|
||||||
## 后续优化建议
|
|
||||||
|
|
||||||
1. **消息持久化**: 将聊天记录保存到本地存储
|
|
||||||
2. **文件传输**: 支持图片、文件等多媒体消息
|
|
||||||
3. **消息状态**: 显示消息已读、未读状态
|
|
||||||
4. **通知功能**: 集成浏览器通知API
|
|
||||||
5. **性能优化**: 消息列表虚拟滚动
|
|
||||||
6. **主题切换**: 支持暗色模式
|
|
||||||
7. **快捷操作**: 添加常用回复快捷键
|
|
||||||
|
|
||||||
## 总结
|
|
||||||
|
|
||||||
WebSocket集成已完成,实现了:
|
|
||||||
- ✅ 完整的实时通信功能
|
|
||||||
- ✅ 稳定的连接管理
|
|
||||||
- ✅ 优秀的用户体验
|
|
||||||
- ✅ 完善的错误处理
|
|
||||||
- ✅ 灵活的配置管理
|
|
||||||
|
|
||||||
前端现在可以通过WebSocket与AI进行实时对话,提供了流畅、稳定的聊天体验!🚀
|
|
||||||
@@ -5,6 +5,7 @@ $white: #FFFFFF;
|
|||||||
$light-gray: #F7F8FA;
|
$light-gray: #F7F8FA;
|
||||||
$text-dark: #333333;
|
$text-dark: #333333;
|
||||||
$text-medium: #888888;
|
$text-medium: #888888;
|
||||||
|
$border-color: #e8e8e8;
|
||||||
|
|
||||||
// 间距
|
// 间距
|
||||||
$spacing-xs: 4px;
|
$spacing-xs: 4px;
|
||||||
@@ -37,6 +38,7 @@ $breakpoint-xxl: 1536px;
|
|||||||
// 字体大小
|
// 字体大小
|
||||||
$font-size-xs: 12px;
|
$font-size-xs: 12px;
|
||||||
$font-size-sm: 14px;
|
$font-size-sm: 14px;
|
||||||
|
$font-size-md: 16px; // 添加缺失的 md 尺寸
|
||||||
$font-size-base: 16px;
|
$font-size-base: 16px;
|
||||||
$font-size-lg: 18px;
|
$font-size-lg: 18px;
|
||||||
$font-size-xl: 20px;
|
$font-size-xl: 20px;
|
||||||
|
|||||||
@@ -34,6 +34,15 @@
|
|||||||
<a-button type="text" @click="showHistory = true" class="action-btn">
|
<a-button type="text" @click="showHistory = true" class="action-btn">
|
||||||
<HistoryOutlined />
|
<HistoryOutlined />
|
||||||
</a-button>
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
type="text"
|
||||||
|
@click="generateEmotionSummary"
|
||||||
|
class="action-btn emotion-btn"
|
||||||
|
:loading="emotionSummaryLoading"
|
||||||
|
:title="'生成今日情绪记录'"
|
||||||
|
>
|
||||||
|
<HeartOutlined />
|
||||||
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -136,7 +145,9 @@
|
|||||||
v-model:open="showHistory"
|
v-model:open="showHistory"
|
||||||
title="聊天记录"
|
title="聊天记录"
|
||||||
placement="right"
|
placement="right"
|
||||||
:width="320"
|
:width="400"
|
||||||
|
class="history-drawer"
|
||||||
|
@open="loadHistoryMessages(1)"
|
||||||
>
|
>
|
||||||
<div class="history-content">
|
<div class="history-content">
|
||||||
<div class="search-section">
|
<div class="search-section">
|
||||||
@@ -144,10 +155,16 @@
|
|||||||
v-model:value="searchKeyword"
|
v-model:value="searchKeyword"
|
||||||
placeholder="搜索关键词..."
|
placeholder="搜索关键词..."
|
||||||
class="search-input"
|
class="search-input"
|
||||||
|
@press-enter="searchHistoryMessages"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<SearchOutlined />
|
<SearchOutlined />
|
||||||
</template>
|
</template>
|
||||||
|
<template #suffix>
|
||||||
|
<a-button type="text" size="small" @click="searchHistoryMessages" :loading="historyLoading">
|
||||||
|
搜索
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
</a-input>
|
</a-input>
|
||||||
|
|
||||||
<a-date-picker
|
<a-date-picker
|
||||||
@@ -155,22 +172,107 @@
|
|||||||
placeholder="按日期查询"
|
placeholder="按日期查询"
|
||||||
class="date-picker"
|
class="date-picker"
|
||||||
style="width: 100%; margin-top: 12px;"
|
style="width: 100%; margin-top: 12px;"
|
||||||
|
@change="loadHistoryMessages(1)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="history-messages">
|
<div class="history-messages" v-if="!historyLoading || historyMessages.length > 0">
|
||||||
<div
|
<div
|
||||||
v-for="message in filteredMessages"
|
v-for="message in filteredMessages"
|
||||||
:key="message.id"
|
:key="message.id"
|
||||||
class="history-message"
|
class="history-message"
|
||||||
:class="{ 'user': message.type === 'user' }"
|
:class="{ 'user': message.sender === 'user' }"
|
||||||
>
|
>
|
||||||
<div class="message-text">{{ message.content }}</div>
|
<div class="message-text">{{ message.content }}</div>
|
||||||
<div class="message-time">{{ formatTime.standard(message.timestamp) }}</div>
|
<div class="message-time">{{ formatTime.standard(message.createTime) }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 加载更多按钮 -->
|
||||||
|
<div v-if="historyPagination.current * historyPagination.pageSize < historyPagination.total" class="load-more">
|
||||||
|
<a-button
|
||||||
|
type="dashed"
|
||||||
|
block
|
||||||
|
@click="loadHistoryMessages(historyPagination.current + 1)"
|
||||||
|
:loading="historyLoading"
|
||||||
|
>
|
||||||
|
加载更多 ({{ historyMessages.length }}/{{ historyPagination.total }})
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 没有更多数据提示 -->
|
||||||
|
<div v-else-if="historyMessages.length > 0" class="no-more">
|
||||||
|
<a-divider>已显示全部记录</a-divider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<div v-if="historyLoading && historyMessages.length === 0" class="loading-state">
|
||||||
|
<a-spin size="large" tip="加载中..." />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<div v-if="!historyLoading && historyMessages.length === 0" class="empty-state">
|
||||||
|
<a-empty description="暂无聊天记录" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a-drawer>
|
</a-drawer>
|
||||||
|
|
||||||
|
<!-- 情绪记录结果模态框 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:open="showEmotionResult"
|
||||||
|
title="今日情绪记录"
|
||||||
|
:footer="null"
|
||||||
|
width="600px"
|
||||||
|
class="emotion-result-modal"
|
||||||
|
>
|
||||||
|
<div v-if="emotionResult" class="emotion-result-content">
|
||||||
|
<div class="emotion-header">
|
||||||
|
<div class="emotion-icon">
|
||||||
|
<HeartOutlined />
|
||||||
|
</div>
|
||||||
|
<div class="emotion-info">
|
||||||
|
<h3 class="emotion-type">{{ emotionResult.emotionRecord?.emotionType || '平静' }}</h3>
|
||||||
|
<div class="emotion-intensity">
|
||||||
|
<span>情绪强度: </span>
|
||||||
|
<a-progress
|
||||||
|
:percent="Math.round((emotionResult.emotionRecord?.intensity || 0.5) * 100)"
|
||||||
|
:stroke-color="getEmotionColor(emotionResult.emotionRecord?.intensity || 0.5)"
|
||||||
|
:show-info="true"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="emotion-details">
|
||||||
|
<div class="detail-item">
|
||||||
|
<h4>触发因素</h4>
|
||||||
|
<p>{{ emotionResult.emotionRecord?.triggers || '日常对话' }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-item">
|
||||||
|
<h4>AI分析总结</h4>
|
||||||
|
<div class="summary-content">
|
||||||
|
{{ emotionResult.summary || '暂无分析总结' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-item">
|
||||||
|
<h4>记录信息</h4>
|
||||||
|
<div class="record-meta">
|
||||||
|
<p><strong>记录日期:</strong> {{ formatDate(emotionResult.recordDate) }}</p>
|
||||||
|
<p><strong>分析消息数:</strong> {{ emotionResult.messageCount || 0 }} 条</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="emotion-actions">
|
||||||
|
<a-button type="primary" @click="showEmotionResult = false">
|
||||||
|
知道了
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -181,6 +283,7 @@
|
|||||||
HistoryOutlined,
|
HistoryOutlined,
|
||||||
SendOutlined,
|
SendOutlined,
|
||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
|
HeartOutlined,
|
||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
import { useChatStore } from '@/stores'
|
import { useChatStore } from '@/stores'
|
||||||
import { formatTime } from '@/utils'
|
import { formatTime } from '@/utils'
|
||||||
@@ -194,13 +297,23 @@
|
|||||||
const searchKeyword = ref('')
|
const searchKeyword = ref('')
|
||||||
const searchDate = ref<Dayjs | null>(null)
|
const searchDate = ref<Dayjs | null>(null)
|
||||||
const chatMainRef = ref<HTMLElement>()
|
const chatMainRef = ref<HTMLElement>()
|
||||||
|
const emotionSummaryLoading = ref(false)
|
||||||
|
const showEmotionResult = ref(false)
|
||||||
|
const emotionResult = ref<any>(null)
|
||||||
|
const historyMessages = ref<any[]>([])
|
||||||
|
const historyLoading = ref(false)
|
||||||
|
const historyPagination = ref({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
// 开开头像
|
// 开开头像
|
||||||
const kaikaiAvatar = 'https://r2.flowith.net/files/o/1752574406770-thoughtful_kaikai_character_generation_index_1@1024x1024.png'
|
const kaikaiAvatar = 'https://r2.flowith.net/files/o/1752574406770-thoughtful_kaikai_character_generation_index_1@1024x1024.png'
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const filteredMessages = computed(() => {
|
const filteredMessages = computed(() => {
|
||||||
let messages = chatStore.messages
|
let messages = historyMessages.value
|
||||||
|
|
||||||
// 关键词搜索
|
// 关键词搜索
|
||||||
if (searchKeyword.value) {
|
if (searchKeyword.value) {
|
||||||
@@ -213,7 +326,7 @@
|
|||||||
if (searchDate.value) {
|
if (searchDate.value) {
|
||||||
const targetDate = searchDate.value.format('YYYY-MM-DD')
|
const targetDate = searchDate.value.format('YYYY-MM-DD')
|
||||||
messages = messages.filter(msg => {
|
messages = messages.filter(msg => {
|
||||||
const msgDate = new Date(msg.timestamp).toISOString().split('T')[0]
|
const msgDate = new Date(msg.createTime).toISOString().split('T')[0]
|
||||||
return msgDate === targetDate
|
return msgDate === targetDate
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -267,6 +380,158 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 生成情绪记录总结
|
||||||
|
const generateEmotionSummary = async () => {
|
||||||
|
if (emotionSummaryLoading.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
emotionSummaryLoading.value = true
|
||||||
|
|
||||||
|
// 获取当前用户ID(这里需要根据实际的用户管理方式获取)
|
||||||
|
const userId = chatStore.currentSession?.userId || 'default_user'
|
||||||
|
|
||||||
|
// 调用后端API生成情绪记录
|
||||||
|
const response = await fetch(`/api/emotion-summary/generate/${userId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// 显示成功消息
|
||||||
|
const emotionRecord = result.data.emotionRecord
|
||||||
|
const summary = result.data.summary
|
||||||
|
|
||||||
|
// 可以显示一个模态框或通知来展示情绪记录结果
|
||||||
|
showEmotionSummaryResult(result.data)
|
||||||
|
} else {
|
||||||
|
console.error('生成情绪记录失败:', result.message)
|
||||||
|
// 显示错误提示
|
||||||
|
alert(result.message || '生成情绪记录失败,请稍后再试')
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('生成情绪记录时发生错误:', error)
|
||||||
|
alert('生成情绪记录失败,请检查网络连接')
|
||||||
|
} finally {
|
||||||
|
emotionSummaryLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示情绪记录结果
|
||||||
|
const showEmotionSummaryResult = (data: any) => {
|
||||||
|
emotionResult.value = {
|
||||||
|
emotionRecord: data.emotionRecord,
|
||||||
|
summary: data.summary,
|
||||||
|
recordDate: data.recordDate || new Date(),
|
||||||
|
messageCount: data.messageCount || 0
|
||||||
|
}
|
||||||
|
showEmotionResult.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取情绪颜色
|
||||||
|
const getEmotionColor = (intensity: number) => {
|
||||||
|
if (intensity >= 0.8) return '#ff4d4f' // 高强度 - 红色
|
||||||
|
if (intensity >= 0.6) return '#ff7a45' // 中高强度 - 橙红色
|
||||||
|
if (intensity >= 0.4) return '#ffa940' // 中等强度 - 橙色
|
||||||
|
if (intensity >= 0.2) return '#52c41a' // 低强度 - 绿色
|
||||||
|
return '#1890ff' // 很低强度 - 蓝色
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (date: Date | string) => {
|
||||||
|
const d = new Date(date)
|
||||||
|
return d.toLocaleDateString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载历史记录
|
||||||
|
const loadHistoryMessages = async (page = 1) => {
|
||||||
|
if (historyLoading.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
historyLoading.value = true
|
||||||
|
|
||||||
|
// 获取当前用户ID(这里需要根据实际的用户管理方式获取)
|
||||||
|
const userId = chatStore.currentSession?.userId || 'default_user'
|
||||||
|
|
||||||
|
const response = await fetch(`/message/user/${userId}/page?current=${page}&size=${historyPagination.value.pageSize}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
const pageData = result.data
|
||||||
|
|
||||||
|
if (page === 1) {
|
||||||
|
historyMessages.value = pageData.records || []
|
||||||
|
} else {
|
||||||
|
historyMessages.value.push(...(pageData.records || []))
|
||||||
|
}
|
||||||
|
|
||||||
|
historyPagination.value = {
|
||||||
|
current: pageData.current || 1,
|
||||||
|
pageSize: pageData.size || 20,
|
||||||
|
total: pageData.total || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('历史记录加载成功:', historyMessages.value.length, '条')
|
||||||
|
} else {
|
||||||
|
console.error('加载历史记录失败:', result.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载历史记录时发生错误:', error)
|
||||||
|
} finally {
|
||||||
|
historyLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索历史记录
|
||||||
|
const searchHistoryMessages = async () => {
|
||||||
|
if (!searchKeyword.value.trim()) {
|
||||||
|
await loadHistoryMessages(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
historyLoading.value = true
|
||||||
|
|
||||||
|
const userId = chatStore.currentSession?.userId || 'default_user'
|
||||||
|
|
||||||
|
const response = await fetch(`/message/user/${userId}/search?keyword=${encodeURIComponent(searchKeyword.value)}&limit=100`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
historyMessages.value = result.data || []
|
||||||
|
console.log('搜索历史记录成功:', historyMessages.value.length, '条')
|
||||||
|
} else {
|
||||||
|
console.error('搜索历史记录失败:', result.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('搜索历史记录时发生错误:', error)
|
||||||
|
} finally {
|
||||||
|
historyLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 监听消息变化,自动滚动到底部
|
// 监听消息变化,自动滚动到底部
|
||||||
watch(
|
watch(
|
||||||
() => chatStore.messages.length,
|
() => chatStore.messages.length,
|
||||||
@@ -376,12 +641,29 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header-right {
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $spacing-sm;
|
||||||
|
|
||||||
.action-btn {
|
.action-btn {
|
||||||
color: $text-medium;
|
color: $text-medium;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $tech-blue;
|
color: $tech-blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.emotion-btn {
|
||||||
|
color: #ff6b6b;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #ff5252;
|
||||||
|
background-color: rgba(255, 107, 107, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
color: #ff5252;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -635,5 +917,114 @@
|
|||||||
color: $text-medium;
|
color: $text-medium;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.load-more {
|
||||||
|
margin-top: $spacing-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-more {
|
||||||
|
margin-top: $spacing-md;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-state,
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: $spacing-xxl;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 情绪记录模态框样式
|
||||||
|
:deep(.emotion-result-modal) {
|
||||||
|
.ant-modal-content {
|
||||||
|
border-radius: $border-radius-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emotion-result-content {
|
||||||
|
.emotion-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $spacing-lg;
|
||||||
|
margin-bottom: $spacing-xl;
|
||||||
|
padding: $spacing-lg;
|
||||||
|
background: linear-gradient(135deg, #ff6b6b, #ff8e8e);
|
||||||
|
border-radius: $border-radius-lg;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
.emotion-icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emotion-info {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.emotion-type {
|
||||||
|
font-size: $font-size-xl;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
margin: 0 0 $spacing-sm 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emotion-intensity {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $spacing-sm;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.emotion-details {
|
||||||
|
.detail-item {
|
||||||
|
margin-bottom: $spacing-lg;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
color: $text-dark;
|
||||||
|
font-weight: $font-weight-medium;
|
||||||
|
margin-bottom: $spacing-sm;
|
||||||
|
font-size: $font-size-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: $text-medium;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-content {
|
||||||
|
background: $light-gray;
|
||||||
|
padding: $spacing-md;
|
||||||
|
border-radius: $border-radius-md;
|
||||||
|
color: $text-medium;
|
||||||
|
line-height: 1.6;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-meta {
|
||||||
|
p {
|
||||||
|
margin-bottom: $spacing-xs;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: $text-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.emotion-actions {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: $spacing-xl;
|
||||||
|
padding-top: $spacing-lg;
|
||||||
|
border-top: 1px solid $border-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+375
-150
@@ -7,11 +7,11 @@
|
|||||||
<a-button type="text" @click="$router.back()" class="back-btn">
|
<a-button type="text" @click="$router.back()" class="back-btn">
|
||||||
<ArrowLeftOutlined />
|
<ArrowLeftOutlined />
|
||||||
</a-button>
|
</a-button>
|
||||||
<h1 class="page-title">情绪日记</h1>
|
<h1 class="page-title">情绪记录</h1>
|
||||||
</div>
|
</div>
|
||||||
<a-button type="primary" @click="showNewEntryModal = true" class="new-entry-btn">
|
<a-button type="primary" @click="$router.push('/chat')" class="new-entry-btn">
|
||||||
<PlusOutlined />
|
<HeartOutlined />
|
||||||
写日记
|
生成情绪记录
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -19,36 +19,41 @@
|
|||||||
<!-- 主要内容 -->
|
<!-- 主要内容 -->
|
||||||
<main class="page-main">
|
<main class="page-main">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<!-- 快速写日记卡片 -->
|
<!-- 提示卡片 -->
|
||||||
<div class="quick-entry-section">
|
<div class="tip-section">
|
||||||
<a-card class="quick-entry-card" @click="showNewEntryModal = true">
|
<a-card class="tip-card">
|
||||||
<div class="quick-entry-content">
|
<div class="tip-content">
|
||||||
<div class="quick-entry-text">
|
<HeartOutlined class="tip-icon" />
|
||||||
<span class="placeholder-text">记录今天的心情...</span>
|
<div class="tip-text">
|
||||||
|
<h3>如何生成情绪记录?</h3>
|
||||||
|
<p>与开开聊天后,点击聊天页面右上角的 ❤️ 按钮,AI会分析你的聊天内容并生成情绪记录</p>
|
||||||
</div>
|
</div>
|
||||||
<a-button type="primary" size="small" class="quick-btn">
|
<a-button type="primary" @click="$router.push('/chat')" class="tip-btn">
|
||||||
<PlusOutlined />
|
去聊天
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</a-card>
|
</a-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 日记列表 -->
|
<!-- 情绪记录列表 -->
|
||||||
<div class="diary-feed">
|
<div class="emotion-feed">
|
||||||
<div
|
<div
|
||||||
v-for="entry in diaryStore.entries"
|
v-for="record in emotionRecords"
|
||||||
:key="entry.id"
|
:key="record.id"
|
||||||
class="diary-entry"
|
class="emotion-entry"
|
||||||
>
|
>
|
||||||
<a-card class="entry-card">
|
<a-card class="entry-card">
|
||||||
<div class="entry-header">
|
<div class="entry-header">
|
||||||
<div class="entry-meta">
|
<div class="entry-meta">
|
||||||
<span class="entry-mood" v-if="entry.mood">
|
<span class="emotion-icon">
|
||||||
{{ getMoodEmoji(entry.mood) }}
|
{{ getEmotionIcon(record.emotionType) }}
|
||||||
</span>
|
|
||||||
<span class="entry-date">
|
|
||||||
{{ formatTime.friendly(entry.createTime) }}
|
|
||||||
</span>
|
</span>
|
||||||
|
<div class="emotion-info">
|
||||||
|
<span class="emotion-type">{{ record.emotionType }}</span>
|
||||||
|
<span class="emotion-date">
|
||||||
|
{{ formatTime.friendly(record.createTime) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a-dropdown>
|
<a-dropdown>
|
||||||
<a-button type="text" size="small">
|
<a-button type="text" size="small">
|
||||||
@@ -56,11 +61,7 @@
|
|||||||
</a-button>
|
</a-button>
|
||||||
<template #overlay>
|
<template #overlay>
|
||||||
<a-menu>
|
<a-menu>
|
||||||
<a-menu-item @click="editEntry(entry)">
|
<a-menu-item @click="deleteEmotionRecord(record.id)" danger>
|
||||||
<EditOutlined />
|
|
||||||
编辑
|
|
||||||
</a-menu-item>
|
|
||||||
<a-menu-item @click="deleteEntry(entry.id)" danger>
|
|
||||||
<DeleteOutlined />
|
<DeleteOutlined />
|
||||||
删除
|
删除
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
@@ -70,120 +71,111 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="entry-content">
|
<div class="entry-content">
|
||||||
<p class="entry-text">{{ entry.content }}</p>
|
<!-- 情绪强度 -->
|
||||||
|
<div class="emotion-intensity">
|
||||||
<div class="entry-tags" v-if="entry.tags && entry.tags.length">
|
<span class="intensity-label">情绪强度:</span>
|
||||||
<a-tag
|
<a-progress
|
||||||
v-for="tag in entry.tags"
|
:percent="Math.round((record.intensity || 0) * 100)"
|
||||||
:key="tag"
|
:stroke-color="getIntensityColor(record.intensity || 0)"
|
||||||
color="blue"
|
:show-info="true"
|
||||||
class="entry-tag"
|
size="small"
|
||||||
>
|
class="intensity-bar"
|
||||||
{{ tag }}
|
|
||||||
</a-tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- AI回复 -->
|
|
||||||
<div class="ai-reply" v-if="entry.aiReply">
|
|
||||||
<div class="ai-avatar">
|
|
||||||
<a-avatar
|
|
||||||
:src="kaikaiAvatar"
|
|
||||||
:size="32"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="ai-content">
|
|
||||||
<div class="ai-name">开开的回复</div>
|
<!-- 触发因素 -->
|
||||||
<p class="ai-text">{{ entry.aiReply }}</p>
|
<div v-if="record.triggers" class="emotion-triggers">
|
||||||
|
<span class="triggers-label">触发因素:</span>
|
||||||
|
<span class="triggers-text">{{ record.triggers }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 描述 -->
|
||||||
|
<div v-if="record.description" class="emotion-description">
|
||||||
|
<p class="description-text">{{ record.description }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 标签 -->
|
||||||
|
<div class="emotion-tags" v-if="record.tags">
|
||||||
|
<a-tag
|
||||||
|
v-for="tag in (typeof record.tags === 'string' ? record.tags.split(',') : record.tags)"
|
||||||
|
:key="tag"
|
||||||
|
color="blue"
|
||||||
|
class="emotion-tag"
|
||||||
|
>
|
||||||
|
{{ tag.trim() }}
|
||||||
|
</a-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 其他信息 -->
|
||||||
|
<div class="emotion-details">
|
||||||
|
<div v-if="record.weather" class="detail-item">
|
||||||
|
<span class="detail-label">天气:</span>
|
||||||
|
<span class="detail-value">{{ record.weather }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="record.location" class="detail-item">
|
||||||
|
<span class="detail-label">地点:</span>
|
||||||
|
<span class="detail-value">{{ record.location }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="record.activity" class="detail-item">
|
||||||
|
<span class="detail-label">活动:</span>
|
||||||
|
<span class="detail-value">{{ record.activity }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a-card>
|
</a-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 加载更多按钮 -->
|
||||||
|
<div v-if="pagination.current * pagination.pageSize < pagination.total" class="load-more">
|
||||||
|
<a-button
|
||||||
|
type="dashed"
|
||||||
|
block
|
||||||
|
@click="loadMoreRecords"
|
||||||
|
:loading="loading"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
加载更多 ({{ emotionRecords.length }}/{{ pagination.total }})
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 没有更多数据提示 -->
|
||||||
|
<div v-else-if="emotionRecords.length > 0" class="no-more">
|
||||||
|
<a-divider>已显示全部记录</a-divider>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 空状态 -->
|
<!-- 空状态 -->
|
||||||
<div v-if="diaryStore.entries.length === 0 && !diaryStore.isLoading" class="empty-state">
|
<div v-if="emotionRecords.length === 0 && !loading" class="empty-state">
|
||||||
<a-empty
|
<a-empty
|
||||||
description="还没有日记记录"
|
description="还没有情绪记录"
|
||||||
:image="Empty.PRESENTED_IMAGE_SIMPLE"
|
:image="Empty.PRESENTED_IMAGE_SIMPLE"
|
||||||
>
|
>
|
||||||
<a-button type="primary" @click="showNewEntryModal = true">
|
<p class="empty-tip">
|
||||||
写第一篇日记
|
与开开聊天后,点击右上角的 <HeartOutlined /> 按钮生成情绪记录
|
||||||
</a-button>
|
</p>
|
||||||
</a-empty>
|
</a-empty>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 加载状态 -->
|
<!-- 加载状态 -->
|
||||||
<div v-if="diaryStore.isLoading" class="loading-state">
|
<div v-if="loading && emotionRecords.length === 0" class="loading-state">
|
||||||
<a-spin size="large" />
|
<a-spin size="large" tip="加载中..." />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- 新日记模态框 -->
|
|
||||||
<a-modal
|
|
||||||
v-model:open="showNewEntryModal"
|
|
||||||
title="写日记"
|
|
||||||
:width="600"
|
|
||||||
@ok="publishEntry"
|
|
||||||
@cancel="resetNewEntry"
|
|
||||||
:confirm-loading="diaryStore.isLoading"
|
|
||||||
:ok-button-props="{ disabled: !newEntryContent.trim() }"
|
|
||||||
>
|
|
||||||
<div class="modal-content">
|
|
||||||
<a-textarea
|
|
||||||
v-model:value="newEntryContent"
|
|
||||||
placeholder="今天有什么新鲜事或心里话想对开开说?"
|
|
||||||
:rows="6"
|
|
||||||
class="modal-textarea"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="modal-options">
|
|
||||||
<div class="mood-selector">
|
|
||||||
<span class="mood-label">心情:</span>
|
|
||||||
<a-radio-group v-model:value="selectedMood" class="mood-options">
|
|
||||||
<a-radio-button value="happy">😊</a-radio-button>
|
|
||||||
<a-radio-button value="sad">😢</a-radio-button>
|
|
||||||
<a-radio-button value="neutral">😐</a-radio-button>
|
|
||||||
<a-radio-button value="excited">🤩</a-radio-button>
|
|
||||||
<a-radio-button value="tired">😴</a-radio-button>
|
|
||||||
</a-radio-group>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tags-input">
|
|
||||||
<span class="tags-label">标签:</span>
|
|
||||||
<a-input
|
|
||||||
v-model:value="newTagInput"
|
|
||||||
placeholder="添加标签,按回车确认"
|
|
||||||
@press-enter="addTag"
|
|
||||||
class="tag-input"
|
|
||||||
/>
|
|
||||||
<div class="selected-tags" v-if="selectedTags.length">
|
|
||||||
<a-tag
|
|
||||||
v-for="tag in selectedTags"
|
|
||||||
:key="tag"
|
|
||||||
closable
|
|
||||||
@close="removeTag(tag)"
|
|
||||||
color="blue"
|
|
||||||
>
|
|
||||||
{{ tag }}
|
|
||||||
</a-tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a-modal>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import {
|
import {
|
||||||
ArrowLeftOutlined,
|
ArrowLeftOutlined,
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
MoreOutlined,
|
MoreOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
|
HeartOutlined,
|
||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
import { Empty, message } from 'ant-design-vue'
|
import { Empty, message } from 'ant-design-vue'
|
||||||
import { useDiaryStore } from '@/stores'
|
import { useDiaryStore } from '@/stores'
|
||||||
@@ -198,6 +190,13 @@
|
|||||||
const selectedMood = ref<string>('neutral')
|
const selectedMood = ref<string>('neutral')
|
||||||
const selectedTags = ref<string[]>([])
|
const selectedTags = ref<string[]>([])
|
||||||
const newTagInput = ref('')
|
const newTagInput = ref('')
|
||||||
|
const emotionRecords = ref<any[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const pagination = ref({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
// 开开头像
|
// 开开头像
|
||||||
const kaikaiAvatar = 'https://r2.flowith.net/files/o/1752574406770-thoughtful_kaikai_character_generation_index_1@1024x1024.png'
|
const kaikaiAvatar = 'https://r2.flowith.net/files/o/1752574406770-thoughtful_kaikai_character_generation_index_1@1024x1024.png'
|
||||||
@@ -273,9 +272,142 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载情绪记录
|
||||||
|
const loadEmotionRecords = async (page = 1, append = false) => {
|
||||||
|
if (loading.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
// 获取当前用户ID(这里需要根据实际的用户管理方式获取)
|
||||||
|
const userId = 'default_user' // 这里应该从用户状态中获取
|
||||||
|
|
||||||
|
const response = await fetch(`/api/emotion-records/user/${userId}?current=${page}&size=${pagination.value.pageSize}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
const pageData = result.data
|
||||||
|
|
||||||
|
if (append) {
|
||||||
|
emotionRecords.value.push(...(pageData.records || []))
|
||||||
|
} else {
|
||||||
|
emotionRecords.value = pageData.records || []
|
||||||
|
}
|
||||||
|
|
||||||
|
pagination.value = {
|
||||||
|
current: pageData.current || 1,
|
||||||
|
pageSize: pageData.size || 10,
|
||||||
|
total: pageData.total || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('情绪记录加载成功:', emotionRecords.value.length, '条')
|
||||||
|
} else {
|
||||||
|
console.error('加载情绪记录失败:', result.message)
|
||||||
|
message.error(result.message || '加载情绪记录失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载情绪记录时发生错误:', error)
|
||||||
|
message.error('加载情绪记录失败,请检查网络连接')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载更多情绪记录
|
||||||
|
const loadMoreRecords = () => {
|
||||||
|
if (pagination.value.current * pagination.value.pageSize < pagination.value.total) {
|
||||||
|
loadEmotionRecords(pagination.value.current + 1, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除情绪记录
|
||||||
|
const deleteEmotionRecord = async (id: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/emotion-records/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
message.success('情绪记录删除成功')
|
||||||
|
// 重新加载第一页
|
||||||
|
await loadEmotionRecords(1)
|
||||||
|
} else {
|
||||||
|
message.error(result.message || '删除失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除情绪记录时发生错误:', error)
|
||||||
|
message.error('删除失败,请重试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取情绪图标
|
||||||
|
const getEmotionIcon = (emotionType: string) => {
|
||||||
|
const emotionIcons: Record<string, string> = {
|
||||||
|
'joy': '😊',
|
||||||
|
'happiness': '😊',
|
||||||
|
'happy': '😊',
|
||||||
|
'excited': '🤩',
|
||||||
|
'love': '😍',
|
||||||
|
'sadness': '😢',
|
||||||
|
'sad': '😢',
|
||||||
|
'crying': '😭',
|
||||||
|
'anger': '😠',
|
||||||
|
'angry': '😡',
|
||||||
|
'rage': '🤬',
|
||||||
|
'fear': '😨',
|
||||||
|
'scared': '😰',
|
||||||
|
'anxiety': '😰',
|
||||||
|
'surprise': '😲',
|
||||||
|
'shocked': '😱',
|
||||||
|
'neutral': '😐',
|
||||||
|
'calm': '😌',
|
||||||
|
'peaceful': '😌',
|
||||||
|
'tired': '😴',
|
||||||
|
'exhausted': '😵',
|
||||||
|
'confused': '😕',
|
||||||
|
'disappointed': '😞',
|
||||||
|
'frustrated': '😤',
|
||||||
|
'bored': '😑',
|
||||||
|
'content': '😊',
|
||||||
|
'grateful': '🙏',
|
||||||
|
'hopeful': '🌟',
|
||||||
|
'proud': '😎',
|
||||||
|
'embarrassed': '😳',
|
||||||
|
'guilty': '😔',
|
||||||
|
'lonely': '😞',
|
||||||
|
'nostalgic': '🥺',
|
||||||
|
'optimistic': '😄',
|
||||||
|
'pessimistic': '😟'
|
||||||
|
}
|
||||||
|
|
||||||
|
return emotionIcons[emotionType.toLowerCase()] || '😐'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取情绪强度颜色
|
||||||
|
const getIntensityColor = (intensity: number) => {
|
||||||
|
if (intensity >= 0.8) return '#ff4d4f' // 高强度 - 红色
|
||||||
|
if (intensity >= 0.6) return '#ff7a45' // 中高强度 - 橙红色
|
||||||
|
if (intensity >= 0.4) return '#ffa940' // 中等强度 - 橙色
|
||||||
|
if (intensity >= 0.2) return '#52c41a' // 低强度 - 绿色
|
||||||
|
return '#1890ff' // 很低强度 - 蓝色
|
||||||
|
}
|
||||||
|
|
||||||
// 组件挂载
|
// 组件挂载
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
diaryStore.loadEntries()
|
loadEmotionRecords(1)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -336,50 +468,50 @@
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.new-entry-section {
|
.tip-section {
|
||||||
margin-bottom: $spacing-xl;
|
margin-bottom: $spacing-xl;
|
||||||
}
|
}
|
||||||
|
|
||||||
.new-entry-card {
|
.tip-card {
|
||||||
.card-title {
|
.tip-content {
|
||||||
font-size: $font-size-lg;
|
|
||||||
font-weight: $font-weight-semibold;
|
|
||||||
color: $text-dark;
|
|
||||||
margin-bottom: $spacing-md;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry-textarea {
|
|
||||||
margin-bottom: $spacing-md;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-end;
|
|
||||||
gap: $spacing-md;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mood-selector {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: $spacing-sm;
|
gap: $spacing-md;
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mood-label {
|
.tip-icon {
|
||||||
font-weight: $font-weight-medium;
|
font-size: 2rem;
|
||||||
color: $text-dark;
|
color: #ff6b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-text {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 $spacing-xs 0;
|
||||||
|
color: $text-dark;
|
||||||
|
font-size: $font-size-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: $text-medium;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-btn {
|
||||||
|
border-radius: $border-radius-full;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.diary-feed {
|
.emotion-feed {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: $spacing-lg;
|
gap: $spacing-lg;
|
||||||
}
|
}
|
||||||
|
|
||||||
.diary-entry {
|
.emotion-entry {
|
||||||
.entry-card {
|
.entry-card {
|
||||||
transition: all $transition-normal;
|
transition: all $transition-normal;
|
||||||
|
|
||||||
@@ -398,14 +530,27 @@
|
|||||||
.entry-meta {
|
.entry-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: $spacing-sm;
|
gap: $spacing-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
.entry-mood {
|
.emotion-icon {
|
||||||
font-size: $font-size-lg;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.entry-date {
|
.emotion-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $spacing-xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emotion-type {
|
||||||
|
font-weight: $font-weight-medium;
|
||||||
|
color: $text-dark;
|
||||||
|
font-size: $font-size-md;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emotion-date {
|
||||||
color: $text-medium;
|
color: $text-medium;
|
||||||
font-size: $font-size-sm;
|
font-size: $font-size-sm;
|
||||||
}
|
}
|
||||||
@@ -414,17 +559,88 @@
|
|||||||
margin-bottom: $spacing-md;
|
margin-bottom: $spacing-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
.entry-text {
|
.emotion-intensity {
|
||||||
line-height: 1.6;
|
display: flex;
|
||||||
color: $text-dark;
|
align-items: center;
|
||||||
margin-bottom: $spacing-sm;
|
gap: $spacing-sm;
|
||||||
|
margin-bottom: $spacing-md;
|
||||||
|
|
||||||
|
.intensity-label {
|
||||||
|
font-weight: $font-weight-medium;
|
||||||
|
color: $text-dark;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intensity-bar {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.entry-tags {
|
.emotion-triggers {
|
||||||
|
margin-bottom: $spacing-md;
|
||||||
|
|
||||||
|
.triggers-label {
|
||||||
|
font-weight: $font-weight-medium;
|
||||||
|
color: $text-dark;
|
||||||
|
margin-right: $spacing-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.triggers-text {
|
||||||
|
color: $text-medium;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.emotion-description {
|
||||||
|
margin-bottom: $spacing-md;
|
||||||
|
|
||||||
|
.description-text {
|
||||||
|
line-height: 1.6;
|
||||||
|
color: $text-dark;
|
||||||
|
margin: 0;
|
||||||
|
padding: $spacing-sm;
|
||||||
|
background: $light-gray;
|
||||||
|
border-radius: $border-radius-md;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.emotion-tags {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: $spacing-xs;
|
gap: $spacing-xs;
|
||||||
|
margin-bottom: $spacing-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.emotion-details {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: $spacing-md;
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $spacing-xs;
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
font-weight: $font-weight-medium;
|
||||||
|
color: $text-dark;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value {
|
||||||
|
color: $text-medium;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more {
|
||||||
|
margin-top: $spacing-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-more {
|
||||||
|
margin-top: $spacing-lg;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-reply {
|
.ai-reply {
|
||||||
@@ -456,9 +672,18 @@
|
|||||||
.empty-state,
|
.empty-state,
|
||||||
.loading-state {
|
.loading-state {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: $spacing-xxl;
|
padding: $spacing-xxl;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.empty-tip {
|
||||||
|
margin-top: $spacing-md;
|
||||||
|
color: $text-medium;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模态框样式
|
// 模态框样式
|
||||||
|
|||||||
-313
@@ -1,313 +0,0 @@
|
|||||||
# 开心APP前端重构计划
|
|
||||||
|
|
||||||
## 项目概述
|
|
||||||
|
|
||||||
将wCcWXJD目录下的原生HTML/CSS/JS前端应用重构为基于Vue 3 + Ant Design Vue的现代化单页应用。
|
|
||||||
|
|
||||||
## 原有功能分析
|
|
||||||
|
|
||||||
### 页面结构
|
|
||||||
1. **首页 (index.html)** - 产品介绍和功能展示
|
|
||||||
2. **聊天页面 (chat.html)** - AI对话界面
|
|
||||||
3. **日记页面 (diary.html)** - 情绪日记发布和浏览
|
|
||||||
4. **个人展板 (personal_dashboard.html)** - 个人信息和数据展示
|
|
||||||
5. **话题追踪 (topic_tracker.html)** - 话题管理和追踪
|
|
||||||
6. **人生轨迹 (life_trajectory.html)** - 生活轨迹记录
|
|
||||||
7. **消息页面 (messages.html)** - 消息中心
|
|
||||||
8. **设置页面 (settings.html)** - 用户设置和配置
|
|
||||||
9. **聊天历史 (chat-history.html)** - 聊天记录查看
|
|
||||||
|
|
||||||
### 核心功能模块
|
|
||||||
1. **智能对话系统** - 与AI助手"开开"的实时聊天
|
|
||||||
2. **情绪日记** - 记录和分享日常心情
|
|
||||||
3. **个人展板** - 自定义个人信息展示
|
|
||||||
4. **话题追踪** - 关注和管理感兴趣的话题
|
|
||||||
5. **数据可视化** - 心情统计图表
|
|
||||||
6. **用户管理** - 登录、注册、设置
|
|
||||||
|
|
||||||
### 设计风格
|
|
||||||
- **主色调**: 科技蓝 (#4A90E2) 和 温暖橙 (#F5A623)
|
|
||||||
- **字体**: Noto Sans SC
|
|
||||||
- **设计风格**: 现代简约,圆角卡片,毛玻璃效果
|
|
||||||
- **响应式设计**: 支持移动端和桌面端
|
|
||||||
|
|
||||||
## 技术栈选择
|
|
||||||
|
|
||||||
### 前端框架
|
|
||||||
- **Vue 3** - 使用Composition API
|
|
||||||
- **Ant Design Vue 4.x** - UI组件库
|
|
||||||
- **Vue Router 4** - 路由管理
|
|
||||||
- **Pinia** - 状态管理
|
|
||||||
- **Vite** - 构建工具
|
|
||||||
|
|
||||||
### 开发工具
|
|
||||||
- **TypeScript** - 类型安全
|
|
||||||
- **ESLint + Prettier** - 代码规范
|
|
||||||
- **Sass/SCSS** - CSS预处理器
|
|
||||||
|
|
||||||
## 项目结构设计
|
|
||||||
|
|
||||||
```
|
|
||||||
web-flowith/
|
|
||||||
├── public/
|
|
||||||
│ ├── index.html
|
|
||||||
│ └── favicon.ico
|
|
||||||
├── src/
|
|
||||||
│ ├── assets/ # 静态资源
|
|
||||||
│ │ ├── images/
|
|
||||||
│ │ └── styles/
|
|
||||||
│ ├── components/ # 公共组件
|
|
||||||
│ │ ├── common/ # 通用组件
|
|
||||||
│ │ ├── layout/ # 布局组件
|
|
||||||
│ │ └── ui/ # UI组件
|
|
||||||
│ ├── views/ # 页面组件
|
|
||||||
│ │ ├── Home/
|
|
||||||
│ │ ├── Chat/
|
|
||||||
│ │ ├── Diary/
|
|
||||||
│ │ ├── Dashboard/
|
|
||||||
│ │ ├── TopicTracker/
|
|
||||||
│ │ ├── LifeTrajectory/
|
|
||||||
│ │ ├── Messages/
|
|
||||||
│ │ └── Settings/
|
|
||||||
│ ├── router/ # 路由配置
|
|
||||||
│ ├── stores/ # Pinia状态管理
|
|
||||||
│ ├── services/ # API服务
|
|
||||||
│ ├── utils/ # 工具函数
|
|
||||||
│ ├── types/ # TypeScript类型定义
|
|
||||||
│ ├── App.vue
|
|
||||||
│ └── main.ts
|
|
||||||
├── package.json
|
|
||||||
├── vite.config.ts
|
|
||||||
├── tsconfig.json
|
|
||||||
└── README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
## 重构实施计划
|
|
||||||
|
|
||||||
### 第一阶段:项目初始化和基础设施
|
|
||||||
1. 创建Vue 3 + Vite项目
|
|
||||||
2. 配置Ant Design Vue
|
|
||||||
3. 设置路由和状态管理
|
|
||||||
4. 配置TypeScript和开发工具
|
|
||||||
5. 创建基础布局组件
|
|
||||||
|
|
||||||
### 第二阶段:核心页面重构
|
|
||||||
1. **首页重构** - 产品介绍和功能展示
|
|
||||||
2. **聊天页面重构** - AI对话界面
|
|
||||||
3. **布局组件** - 头部导航、侧边栏、底部
|
|
||||||
|
|
||||||
### 第三阶段:功能页面重构
|
|
||||||
1. **日记页面** - 情绪日记功能
|
|
||||||
2. **个人展板** - 个人信息展示
|
|
||||||
3. **设置页面** - 用户配置
|
|
||||||
|
|
||||||
### 第四阶段:高级功能重构
|
|
||||||
1. **话题追踪** - 话题管理功能
|
|
||||||
2. **人生轨迹** - 生活记录功能
|
|
||||||
3. **消息中心** - 消息管理
|
|
||||||
|
|
||||||
### 第五阶段:优化和完善
|
|
||||||
1. 性能优化
|
|
||||||
2. 响应式适配
|
|
||||||
3. 无障碍访问
|
|
||||||
4. 测试和调试
|
|
||||||
|
|
||||||
## 组件设计规范
|
|
||||||
|
|
||||||
### 命名规范
|
|
||||||
- 组件名使用PascalCase
|
|
||||||
- 文件名使用kebab-case
|
|
||||||
- 变量和函数使用camelCase
|
|
||||||
|
|
||||||
### 组件结构
|
|
||||||
```vue
|
|
||||||
<template>
|
|
||||||
<!-- 模板内容 -->
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
// 组件逻辑
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
// 组件样式
|
|
||||||
</style>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 状态管理
|
|
||||||
- 使用Pinia进行全局状态管理
|
|
||||||
- 页面级状态使用ref/reactive
|
|
||||||
- 组件间通信使用props/emit
|
|
||||||
|
|
||||||
## API集成计划
|
|
||||||
|
|
||||||
### 后端API对接
|
|
||||||
- 用户认证API
|
|
||||||
- 聊天对话API
|
|
||||||
- 日记管理API
|
|
||||||
- 个人数据API
|
|
||||||
- 文件上传API
|
|
||||||
|
|
||||||
### 数据格式标准化
|
|
||||||
- 统一响应格式
|
|
||||||
- 错误处理机制
|
|
||||||
- 数据验证规则
|
|
||||||
|
|
||||||
## 样式迁移策略
|
|
||||||
|
|
||||||
### 主题配置
|
|
||||||
- 保持原有色彩方案
|
|
||||||
- 适配Ant Design主题系统
|
|
||||||
- 自定义组件样式
|
|
||||||
|
|
||||||
### 响应式设计
|
|
||||||
- 移动端优先
|
|
||||||
- 断点设计规范
|
|
||||||
- 组件自适应
|
|
||||||
|
|
||||||
## 测试策略
|
|
||||||
|
|
||||||
### 单元测试
|
|
||||||
- 组件测试
|
|
||||||
- 工具函数测试
|
|
||||||
- 状态管理测试
|
|
||||||
|
|
||||||
### 集成测试
|
|
||||||
- 页面功能测试
|
|
||||||
- API集成测试
|
|
||||||
- 用户流程测试
|
|
||||||
|
|
||||||
## 部署配置
|
|
||||||
|
|
||||||
### 构建优化
|
|
||||||
- 代码分割
|
|
||||||
- 资源压缩
|
|
||||||
- 缓存策略
|
|
||||||
|
|
||||||
### 环境配置
|
|
||||||
- 开发环境
|
|
||||||
- 测试环境
|
|
||||||
- 生产环境
|
|
||||||
|
|
||||||
## 时间安排
|
|
||||||
|
|
||||||
- **第一阶段**: 2天 - 项目初始化
|
|
||||||
- **第二阶段**: 3天 - 核心页面
|
|
||||||
- **第三阶段**: 3天 - 功能页面
|
|
||||||
- **第四阶段**: 3天 - 高级功能
|
|
||||||
- **第五阶段**: 2天 - 优化完善
|
|
||||||
|
|
||||||
**总计**: 约13个工作日
|
|
||||||
|
|
||||||
## 风险评估
|
|
||||||
|
|
||||||
### 技术风险
|
|
||||||
- Vue 3新特性学习成本
|
|
||||||
- Ant Design组件定制复杂度
|
|
||||||
- 原有功能迁移兼容性
|
|
||||||
|
|
||||||
### 解决方案
|
|
||||||
- 渐进式重构
|
|
||||||
- 组件化开发
|
|
||||||
- 充分测试验证
|
|
||||||
|
|
||||||
## 成功标准
|
|
||||||
|
|
||||||
1. 功能完整性 - 100%还原原有功能 ✅
|
|
||||||
2. 性能指标 - 页面加载时间<2秒 ✅
|
|
||||||
3. 用户体验 - 响应式设计完美适配 ✅
|
|
||||||
4. 代码质量 - TypeScript覆盖率>90% ✅
|
|
||||||
5. 可维护性 - 组件化程度>80% ✅
|
|
||||||
|
|
||||||
## 重构完成总结
|
|
||||||
|
|
||||||
### 已完成功能
|
|
||||||
|
|
||||||
✅ **项目初始化和基础设施**
|
|
||||||
- Vue 3 + Vite项目搭建
|
|
||||||
- Ant Design Vue UI组件库集成
|
|
||||||
- TypeScript配置和类型定义
|
|
||||||
- ESLint + Prettier代码规范
|
|
||||||
- Pinia状态管理
|
|
||||||
- Vue Router路由配置
|
|
||||||
|
|
||||||
✅ **核心页面重构**
|
|
||||||
- 首页 - 产品介绍和功能展示
|
|
||||||
- 聊天页面 - AI对话界面
|
|
||||||
- 布局组件 - 头部导航、底部信息
|
|
||||||
|
|
||||||
✅ **功能页面重构**
|
|
||||||
- 日记页面 - 情绪日记发布和浏览
|
|
||||||
- 个人展板 - 个人信息和数据展示
|
|
||||||
- 设置页面 - 用户配置和管理
|
|
||||||
|
|
||||||
✅ **高级功能重构**
|
|
||||||
- 话题追踪 - 话题管理和追踪功能
|
|
||||||
- 人生轨迹 - 生活事件记录
|
|
||||||
- 消息中心 - 消息管理和通知
|
|
||||||
- 聊天历史 - 聊天记录查看
|
|
||||||
|
|
||||||
✅ **优化和完善**
|
|
||||||
- 响应式设计适配
|
|
||||||
- 性能优化配置
|
|
||||||
- Docker容器化部署
|
|
||||||
- 部署脚本和文档
|
|
||||||
|
|
||||||
### 技术亮点
|
|
||||||
|
|
||||||
1. **现代化技术栈**: Vue 3 + TypeScript + Vite
|
|
||||||
2. **组件化设计**: 高度模块化的组件结构
|
|
||||||
3. **类型安全**: 完整的TypeScript类型定义
|
|
||||||
4. **状态管理**: Pinia现代状态管理方案
|
|
||||||
5. **UI一致性**: Ant Design Vue统一设计语言
|
|
||||||
6. **开发体验**: 热重载、代码检查、格式化
|
|
||||||
7. **部署方案**: 传统部署 + Docker容器化
|
|
||||||
|
|
||||||
### 项目结构
|
|
||||||
|
|
||||||
```
|
|
||||||
web-flowith/
|
|
||||||
├── src/
|
|
||||||
│ ├── assets/styles/ # 全局样式和变量
|
|
||||||
│ ├── components/ # 可复用组件
|
|
||||||
│ │ └── layout/ # 布局组件
|
|
||||||
│ ├── views/ # 页面组件
|
|
||||||
│ │ ├── Home/ # 首页
|
|
||||||
│ │ ├── Chat/ # 聊天相关
|
|
||||||
│ │ ├── Diary/ # 日记功能
|
|
||||||
│ │ ├── Dashboard/ # 个人展板
|
|
||||||
│ │ ├── TopicTracker/ # 话题追踪
|
|
||||||
│ │ ├── LifeTrajectory/# 人生轨迹
|
|
||||||
│ │ ├── Messages/ # 消息中心
|
|
||||||
│ │ └── Settings/ # 设置页面
|
|
||||||
│ ├── stores/ # Pinia状态管理
|
|
||||||
│ ├── services/ # API服务层
|
|
||||||
│ ├── utils/ # 工具函数
|
|
||||||
│ ├── types/ # TypeScript类型
|
|
||||||
│ └── router/ # 路由配置
|
|
||||||
├── public/ # 静态资源
|
|
||||||
├── Dockerfile # Docker配置
|
|
||||||
├── docker-compose.yml # Docker Compose
|
|
||||||
├── deploy.sh # 部署脚本
|
|
||||||
└── nginx.conf # Nginx配置
|
|
||||||
```
|
|
||||||
|
|
||||||
### 下一步建议
|
|
||||||
|
|
||||||
1. **API集成**: 连接后端API服务
|
|
||||||
2. **用户认证**: 完善登录注册功能
|
|
||||||
3. **数据持久化**: 实现本地存储和同步
|
|
||||||
4. **性能监控**: 添加性能监控和错误追踪
|
|
||||||
5. **测试覆盖**: 增加单元测试和集成测试
|
|
||||||
6. **PWA支持**: 添加离线功能和推送通知
|
|
||||||
|
|
||||||
### 部署说明
|
|
||||||
|
|
||||||
项目支持多种部署方式:
|
|
||||||
|
|
||||||
1. **开发环境**: `npm run dev`
|
|
||||||
2. **生产构建**: `npm run build`
|
|
||||||
3. **Docker部署**: `docker-compose up -d`
|
|
||||||
4. **脚本部署**: `./deploy.sh prod`
|
|
||||||
|
|
||||||
项目已成功重构完成,具备了现代化前端应用的所有特性!🎉
|
|
||||||
-435
@@ -1,435 +0,0 @@
|
|||||||
# 情绪博物馆功能模块详细梳理
|
|
||||||
|
|
||||||
**文档版本**: v1.0
|
|
||||||
**创建时间**: 2025-07-12
|
|
||||||
**基于**: 现有代码分析和需求文档
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 目录
|
|
||||||
|
|
||||||
- [1. 记录页面 (RecordView)](#1-记录页面-recordview)
|
|
||||||
- [2. 治愈页面 (GrowthView)](#2-治愈页面-growthview)
|
|
||||||
- [3. 探索页面 (ExploreView)](#3-探索页面-exploreview)
|
|
||||||
- [4. 个人页面 (InsightView)](#4-个人页面-insightview)
|
|
||||||
- [5. 公共组件和服务](#5-公共组件和服务)
|
|
||||||
- [6. 缺失功能清单](#6-缺失功能清单)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 记录页面 (RecordView)
|
|
||||||
|
|
||||||
### 1.1 已实现功能 ✅
|
|
||||||
|
|
||||||
#### 主界面组件
|
|
||||||
- [x] **顶部导航栏**
|
|
||||||
- 左上角:聊天记录入口按钮
|
|
||||||
- 中间:页面标题
|
|
||||||
- 右上角:设置按钮
|
|
||||||
|
|
||||||
- [x] **情绪日历组件**
|
|
||||||
- 单行日历显示
|
|
||||||
- 日期选择功能
|
|
||||||
- 基础情绪标记
|
|
||||||
|
|
||||||
- [x] **AI助手头像区域**
|
|
||||||
- 静态头像显示
|
|
||||||
- 欢迎文案
|
|
||||||
- 基础动画效果
|
|
||||||
|
|
||||||
- [x] **聊天区域**
|
|
||||||
- 消息列表显示
|
|
||||||
- 消息气泡样式
|
|
||||||
- 滚动到底部功能
|
|
||||||
|
|
||||||
- [x] **输入区域**
|
|
||||||
- 文本输入框
|
|
||||||
- 发送按钮
|
|
||||||
- 语音模式切换按钮
|
|
||||||
- 图片添加按钮
|
|
||||||
|
|
||||||
#### 状态管理
|
|
||||||
- [x] 加载状态管理
|
|
||||||
- [x] 骨架屏显示
|
|
||||||
- [x] 错误状态处理
|
|
||||||
- [x] 刷新功能
|
|
||||||
|
|
||||||
### 1.2 待实现功能 ❌
|
|
||||||
|
|
||||||
#### 核心功能
|
|
||||||
- [ ] **聊天记录入口页面 (ChatHistoryView)**
|
|
||||||
- 对话列表展示
|
|
||||||
- 搜索和筛选功能
|
|
||||||
- 对话摘要显示
|
|
||||||
- 删除和管理功能
|
|
||||||
|
|
||||||
- [ ] **全屏对话页面 (FullScreenChatView)**
|
|
||||||
- 全屏聊天界面
|
|
||||||
- 语音/文字模式切换
|
|
||||||
- 消息发送状态
|
|
||||||
- 对话收起功能
|
|
||||||
|
|
||||||
- [ ] **设置页面 (SettingsView)**
|
|
||||||
- 主题设置
|
|
||||||
- 音效设置
|
|
||||||
- 隐私设置
|
|
||||||
- 关于页面
|
|
||||||
|
|
||||||
#### 高级功能
|
|
||||||
- [ ] **语音识别功能**
|
|
||||||
- 语音转文字
|
|
||||||
- 语音消息录制
|
|
||||||
- 语音播放控制
|
|
||||||
|
|
||||||
- [ ] **AI对话增强**
|
|
||||||
- 真实AI服务集成
|
|
||||||
- 情绪分析反馈
|
|
||||||
- 智能回复建议
|
|
||||||
|
|
||||||
- [ ] **情绪日历增强**
|
|
||||||
- 日历展开/收起
|
|
||||||
- 情绪趋势图表
|
|
||||||
- 历史回顾功能
|
|
||||||
|
|
||||||
### 1.3 数据流设计
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TD
|
|
||||||
A[用户输入] --> B[NavigationManager]
|
|
||||||
B --> C[MockDataManager]
|
|
||||||
C --> D[AI服务]
|
|
||||||
D --> E[情绪分析]
|
|
||||||
E --> F[数据存储]
|
|
||||||
F --> G[UI更新]
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 治愈页面 (GrowthView)
|
|
||||||
|
|
||||||
### 2.1 已实现功能 ✅
|
|
||||||
|
|
||||||
#### 数据模型
|
|
||||||
- [x] **GrowthTopic模型**
|
|
||||||
- 课题基本信息
|
|
||||||
- 进度追踪
|
|
||||||
- 分类系统
|
|
||||||
- 难度等级
|
|
||||||
|
|
||||||
- [x] **TopicInteraction模型**
|
|
||||||
- 互动类型定义
|
|
||||||
- 完成状态追踪
|
|
||||||
- 评分系统
|
|
||||||
|
|
||||||
- [x] **Reward模型**
|
|
||||||
- 奖励类型
|
|
||||||
- 稀有度系统
|
|
||||||
- 积分机制
|
|
||||||
|
|
||||||
#### 基础界面
|
|
||||||
- [x] 页面框架结构
|
|
||||||
- [x] 主题适配
|
|
||||||
- [x] 加载状态
|
|
||||||
|
|
||||||
### 2.2 待实现功能 ❌
|
|
||||||
|
|
||||||
#### 主界面组件
|
|
||||||
- [ ] **成长概览卡片**
|
|
||||||
- 个人成长数据展示
|
|
||||||
- 本周进展摘要
|
|
||||||
- 成长轨迹可视化
|
|
||||||
|
|
||||||
- [ ] **五维雷达图**
|
|
||||||
- 自我感知维度
|
|
||||||
- 情绪韧性维度
|
|
||||||
- 行动力维度
|
|
||||||
- 共情力维度
|
|
||||||
- 生活热度维度
|
|
||||||
|
|
||||||
- [ ] **课题分类标签**
|
|
||||||
- 分类筛选功能
|
|
||||||
- 进度指示器
|
|
||||||
- 解锁状态显示
|
|
||||||
|
|
||||||
- [ ] **课题列表**
|
|
||||||
- 课题卡片展示
|
|
||||||
- 进度条显示
|
|
||||||
- 快速操作按钮
|
|
||||||
|
|
||||||
#### 详情页面
|
|
||||||
- [ ] **课题详情页 (TopicDetailView)**
|
|
||||||
- 课题完整信息
|
|
||||||
- 学习路径展示
|
|
||||||
- 相关资源链接
|
|
||||||
- 进度统计
|
|
||||||
|
|
||||||
- [ ] **课题互动页 (TopicInteractionView)**
|
|
||||||
- AI对话互动
|
|
||||||
- 知识文章阅读
|
|
||||||
- 练习活动
|
|
||||||
- 反思日记
|
|
||||||
|
|
||||||
#### 子功能页面
|
|
||||||
- [ ] **TopicChatView** - AI对话
|
|
||||||
- [ ] **TopicArticleView** - 文章阅读
|
|
||||||
- [ ] **TopicExerciseView** - 练习活动
|
|
||||||
- [ ] **TopicReflectionView** - 反思日记
|
|
||||||
|
|
||||||
### 2.3 成长数据算法
|
|
||||||
|
|
||||||
```swift
|
|
||||||
// 成长数据计算逻辑
|
|
||||||
class GrowthCalculator {
|
|
||||||
static func calculateGrowthStats(from interactions: [TopicInteraction]) -> GrowthStats {
|
|
||||||
// 基于互动数据计算五维成长数据
|
|
||||||
}
|
|
||||||
|
|
||||||
static func updateProgressBasedOnActivity(topic: GrowthTopic, activity: TopicInteraction) -> Float {
|
|
||||||
// 根据活动类型和质量更新进度
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 探索页面 (ExploreView)
|
|
||||||
|
|
||||||
### 3.1 已实现功能 ✅
|
|
||||||
|
|
||||||
#### 数据模型
|
|
||||||
- [x] **LocationPin模型**
|
|
||||||
- 地理坐标
|
|
||||||
- 地点信息
|
|
||||||
- 情绪标签
|
|
||||||
- 社区数据
|
|
||||||
|
|
||||||
- [x] **CommunityPost模型**
|
|
||||||
- 帖子内容
|
|
||||||
- 图片支持
|
|
||||||
- 点赞评论
|
|
||||||
- 分类标签
|
|
||||||
|
|
||||||
#### 基础界面
|
|
||||||
- [x] 页面框架
|
|
||||||
- [x] 主题适配
|
|
||||||
|
|
||||||
### 3.2 待实现功能 ❌
|
|
||||||
|
|
||||||
#### 地图功能
|
|
||||||
- [ ] **地图SDK集成**
|
|
||||||
- 高德地图集成
|
|
||||||
- 地图显示和控制
|
|
||||||
- 定位服务
|
|
||||||
- 地点标记
|
|
||||||
|
|
||||||
- [ ] **地点管理**
|
|
||||||
- 地点详情页 (LocationDetailView)
|
|
||||||
- 添加地点页 (AddLocationView)
|
|
||||||
- 地点分类筛选
|
|
||||||
- 收藏和访问记录
|
|
||||||
|
|
||||||
#### 社区功能
|
|
||||||
- [ ] **社区动态页 (CommunityFeedView)**
|
|
||||||
- 帖子列表展示
|
|
||||||
- 图文混排
|
|
||||||
- 无限滚动加载
|
|
||||||
- 刷新机制
|
|
||||||
|
|
||||||
- [ ] **帖子详情页 (PostDetailView)**
|
|
||||||
- 完整帖子内容
|
|
||||||
- 评论区域
|
|
||||||
- 点赞分享功能
|
|
||||||
- 相关推荐
|
|
||||||
|
|
||||||
- [ ] **发布功能**
|
|
||||||
- 图片选择和上传
|
|
||||||
- 文字编辑
|
|
||||||
- 地点关联
|
|
||||||
- 标签添加
|
|
||||||
|
|
||||||
#### 视图切换
|
|
||||||
- [ ] **地图/社区模式切换**
|
|
||||||
- 平滑过渡动画
|
|
||||||
- 状态保持
|
|
||||||
- 数据同步
|
|
||||||
|
|
||||||
### 3.3 地图服务架构
|
|
||||||
|
|
||||||
```swift
|
|
||||||
// 地图服务管理
|
|
||||||
class MapService: ObservableObject {
|
|
||||||
@Published var currentLocation: CLLocationCoordinate2D?
|
|
||||||
@Published var nearbyLocations: [LocationPin] = []
|
|
||||||
|
|
||||||
func searchNearbyPlaces(radius: Double) async {
|
|
||||||
// 搜索周边地点
|
|
||||||
}
|
|
||||||
|
|
||||||
func addCustomLocation(_ location: LocationPin) {
|
|
||||||
// 添加自定义地点
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 个人页面 (InsightView)
|
|
||||||
|
|
||||||
### 4.1 已实现功能 ✅
|
|
||||||
|
|
||||||
#### 数据模型
|
|
||||||
- [x] **User模型**
|
|
||||||
- 用户基本信息
|
|
||||||
- 成长统计数据
|
|
||||||
- 会员等级
|
|
||||||
|
|
||||||
- [x] **Achievement模型**
|
|
||||||
- 成就系统
|
|
||||||
- 进度追踪
|
|
||||||
- 分类管理
|
|
||||||
|
|
||||||
- [x] **UserStats模型**
|
|
||||||
- 统计数据
|
|
||||||
- 活跃度指标
|
|
||||||
|
|
||||||
#### 基础界面
|
|
||||||
- [x] 页面框架(当前为UniverseView)
|
|
||||||
- [x] 主题适配
|
|
||||||
|
|
||||||
### 4.2 待实现功能 ❌
|
|
||||||
|
|
||||||
#### 主界面组件
|
|
||||||
- [ ] **用户资料卡片**
|
|
||||||
- 头像和基本信息
|
|
||||||
- 会员等级显示
|
|
||||||
- 注册天数统计
|
|
||||||
|
|
||||||
- [ ] **本周数据统计**
|
|
||||||
- 心情指数
|
|
||||||
- 对话次数
|
|
||||||
- 成长轨迹
|
|
||||||
- 活跃度图表
|
|
||||||
|
|
||||||
- [ ] **成就展示区域**
|
|
||||||
- 最新成就
|
|
||||||
- 进度展示
|
|
||||||
- 快速查看
|
|
||||||
|
|
||||||
- [ ] **快捷功能区域**
|
|
||||||
- 设置入口
|
|
||||||
- 邀请好友
|
|
||||||
- 反馈建议
|
|
||||||
- 帮助中心
|
|
||||||
|
|
||||||
#### 详情页面
|
|
||||||
- [ ] **用户资料页 (UserProfileView)**
|
|
||||||
- 个人信息编辑
|
|
||||||
- 头像上传
|
|
||||||
- 隐私设置
|
|
||||||
- 账户管理
|
|
||||||
|
|
||||||
- [ ] **成就页面 (AchievementsView)**
|
|
||||||
- 成就分类展示
|
|
||||||
- 进度详情
|
|
||||||
- 解锁条件
|
|
||||||
- 奖励查看
|
|
||||||
|
|
||||||
- [ ] **会员中心 (MemberCenterView)**
|
|
||||||
- 会员权益
|
|
||||||
- 升级选项
|
|
||||||
- 使用统计
|
|
||||||
- 特权功能
|
|
||||||
|
|
||||||
### 4.3 统计数据计算
|
|
||||||
|
|
||||||
```swift
|
|
||||||
// 用户统计数据计算
|
|
||||||
class UserStatsCalculator {
|
|
||||||
static func calculateWeeklyStats(user: User) -> WeeklyStats {
|
|
||||||
// 计算本周统计数据
|
|
||||||
}
|
|
||||||
|
|
||||||
static func calculateMoodTrend(records: [EmotionRecord]) -> EmotionTrend {
|
|
||||||
// 分析情绪趋势
|
|
||||||
}
|
|
||||||
|
|
||||||
static func updateAchievementProgress(user: User) -> [Achievement] {
|
|
||||||
// 更新成就进度
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 公共组件和服务
|
|
||||||
|
|
||||||
### 5.1 已实现组件 ✅
|
|
||||||
|
|
||||||
#### 管理服务
|
|
||||||
- [x] **NavigationManager** - 导航状态管理
|
|
||||||
- [x] **MockDataManager** - 模拟数据管理
|
|
||||||
- [x] **ThemeManager** - 主题管理
|
|
||||||
|
|
||||||
#### UI组件
|
|
||||||
- [x] **LoadingComponents** - 加载组件
|
|
||||||
- [x] **AnimationComponents** - 动画组件
|
|
||||||
- [x] **ThemeAdapter** - 主题适配器
|
|
||||||
|
|
||||||
### 5.2 待实现组件 ❌
|
|
||||||
|
|
||||||
#### 核心服务
|
|
||||||
- [ ] **CoreDataManager** - 数据持久化
|
|
||||||
- [ ] **AIService** - AI服务集成
|
|
||||||
- [ ] **LocationService** - 位置服务
|
|
||||||
- [ ] **NotificationService** - 通知服务
|
|
||||||
- [ ] **ImageService** - 图片处理服务
|
|
||||||
|
|
||||||
#### 通用UI组件
|
|
||||||
- [ ] **EmotionPicker** - 情绪选择器
|
|
||||||
- [ ] **RadarChart** - 雷达图组件
|
|
||||||
- [ ] **ProgressRing** - 进度环组件
|
|
||||||
- [ ] **TagView** - 标签组件
|
|
||||||
- [ ] **PhotoPicker** - 图片选择器
|
|
||||||
- [ ] **VoiceRecorder** - 语音录制器
|
|
||||||
|
|
||||||
#### 工具类
|
|
||||||
- [ ] **DateFormatter** - 日期格式化
|
|
||||||
- [ ] **ValidationHelper** - 数据验证
|
|
||||||
- [ ] **NetworkMonitor** - 网络状态监控
|
|
||||||
- [ ] **PermissionManager** - 权限管理
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 缺失功能清单
|
|
||||||
|
|
||||||
### 6.1 高优先级 (P0)
|
|
||||||
- [ ] 聊天记录页面 (ChatHistoryView)
|
|
||||||
- [ ] 全屏对话页面 (FullScreenChatView)
|
|
||||||
- [ ] 成长课题主页完善
|
|
||||||
- [ ] 地图功能集成
|
|
||||||
- [ ] 用户资料页面
|
|
||||||
|
|
||||||
### 6.2 中优先级 (P1)
|
|
||||||
- [ ] 设置页面 (SettingsView)
|
|
||||||
- [ ] 课题详情页面
|
|
||||||
- [ ] 社区动态页面
|
|
||||||
- [ ] 成就系统页面
|
|
||||||
- [ ] AI服务集成
|
|
||||||
|
|
||||||
### 6.3 低优先级 (P2)
|
|
||||||
- [ ] 语音识别功能
|
|
||||||
- [ ] 高级数据可视化
|
|
||||||
- [ ] 社交分享功能
|
|
||||||
- [ ] 会员中心
|
|
||||||
- [ ] 通知系统
|
|
||||||
|
|
||||||
### 6.4 技术债务
|
|
||||||
- [ ] Core Data集成
|
|
||||||
- [ ] 真实API集成
|
|
||||||
- [ ] 性能优化
|
|
||||||
- [ ] 错误处理完善
|
|
||||||
- [ ] 单元测试覆盖
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*本文档基于当前代码状态分析,将随开发进度更新*
|
|
||||||
Reference in New Issue
Block a user