diff --git a/.env b/.env
new file mode 100644
index 0000000..227c79c
--- /dev/null
+++ b/.env
@@ -0,0 +1,18 @@
+# 数据库配置
+MYSQL_ROOT_PASSWORD=123456
+MYSQL_DATABASE=emotion_museum
+MYSQL_USER=emotion
+MYSQL_PASSWORD=emotion123
+
+# Redis配置
+REDIS_PASSWORD=
+
+# Nacos配置
+NACOS_AUTH_ENABLE=false
+
+# 应用配置
+SPRING_PROFILES_ACTIVE=docker
+TZ=Asia/Shanghai
+
+# Coze API配置 (与开发环境一致)
+COZE_API_TOKEN=pat_GCR4qKzqpf90wMCvKsldMrB18KG3QsLDci65bZthssKsbLxu8X70BKYumleDcabO
diff --git a/.env.prod b/.env.prod
new file mode 100644
index 0000000..b2beccc
--- /dev/null
+++ b/.env.prod
@@ -0,0 +1,38 @@
+# 情绪博物馆生产环境配置文件
+# 用于Docker Compose和部署脚本
+
+# 基础配置
+TZ=Asia/Shanghai
+APP_VERSION=1.0.0
+SPRING_PROFILES_ACTIVE=prod
+
+# 服务器配置
+SERVER_HOST=47.111.10.27
+SERVER_USER=root
+SERVER_IP=47.111.10.27
+
+# 数据库配置
+MYSQL_ROOT_PASSWORD=123456
+MYSQL_DATABASE=emotion_museum
+MYSQL_USER=emotion
+MYSQL_PASSWORD=EmotionDB2024!
+MYSQL_HOST=localhost
+MYSQL_PORT=3306
+
+# Redis配置
+REDIS_HOST=localhost
+REDIS_PORT=6379
+
+# Nacos配置
+NACOS_SERVER_ADDR=localhost:8848
+NACOS_AUTH_ENABLE=false
+
+# Coze API配置
+COZE_API_TOKEN=pat_GCR4qKzqpf90wMCvKsldMrB18KG3QsLDci65bZthssKsbLxu8X70BKYumleDcabO
+
+# 目录配置
+REMOTE_BASE_DIR=/data
+REMOTE_BUILDS_DIR=/data/builds
+REMOTE_WEB_DIR=/data/www/emotion-museum/web
+REMOTE_LOGS_DIR=/data/logs/emotion-museum
+REMOTE_PROGRAMS_DIR=/data/programs
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..7bc07ec
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,10 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Environment-dependent path to Maven home directory
+/mavenHomeManager.xml
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/ApifoxUploaderProjectSetting.xml b/.idea/ApifoxUploaderProjectSetting.xml
new file mode 100644
index 0000000..247c160
--- /dev/null
+++ b/.idea/ApifoxUploaderProjectSetting.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/AugmentWebviewStateStore.xml b/.idea/AugmentWebviewStateStore.xml
new file mode 100644
index 0000000..5c45b02
--- /dev/null
+++ b/.idea/AugmentWebviewStateStore.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/EmotionMuseum.iml b/.idea/EmotionMuseum.iml
new file mode 100644
index 0000000..d9dbc8e
--- /dev/null
+++ b/.idea/EmotionMuseum.iml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..2363937
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml
new file mode 100644
index 0000000..ba94529
--- /dev/null
+++ b/.idea/dataSources.xml
@@ -0,0 +1,17 @@
+
+
+
+
+ mysql.8
+ true
+ com.mysql.cj.jdbc.Driver
+ jdbc:mysql://localhost:3306
+
+
+
+
+
+ $ProjectFileDir$
+
+
+
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 0000000..ae3bac1
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
new file mode 100644
index 0000000..fa3a980
--- /dev/null
+++ b/.idea/jarRepositories.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/material_theme_project_new.xml b/.idea/material_theme_project_new.xml
new file mode 100644
index 0000000..995635b
--- /dev/null
+++ b/.idea/material_theme_project_new.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..53b0c32
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..d7f9ab7
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml
new file mode 100644
index 0000000..91d7a08
--- /dev/null
+++ b/.idea/sqldialects.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..9818f00
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,4 @@
+{
+ "java.compile.nullAnalysis.mode": "automatic",
+ "dbcode.connections": []
+}
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..4ad19c0
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,415 @@
+# 情绪博物馆全栈项目 - Claude 开发指南
+
+## 项目概述
+
+这是一个情绪博物馆全栈项目,包含iOS客户端、Spring Boot微服务后端和Vue.js Web管理界面。项目采用现代化技术栈,实现情绪记录、AI对话、成长陪伴等核心功能。
+
+### 项目结构
+- **EmotionMuseum/**: iOS SwiftUI客户端应用
+- **backend/**: Spring Cloud Alibaba微服务后端
+- **web/**: Vue.js + Vite Web管理界面
+- **mysql_*.sql**: 数据库脚本文件
+- ***.md**: 项目文档和需求规格书
+
+## 技术栈
+
+### iOS客户端 (SwiftUI)
+- **SwiftUI**: 声明式UI框架
+- **Core Data**: 本地数据存储
+- **Combine**: 响应式编程
+- **MapKit**: 地图功能 (高德地图集成)
+- **UIKit**: 部分复杂UI组件
+
+### 后端微服务 (Spring Boot)
+- **Spring Boot**: 3.0.2 (主框架)
+- **Spring Cloud**: 2022.0.0 (微服务支持)
+- **Spring Cloud Alibaba**: 2022.0.0.0 (微服务生态)
+- **Java**: 17+ (编程语言)
+- **Maven**: 3.6+ (依赖管理)
+
+### Web前端 (Vue.js)
+- **Vue.js**: 3.x (前端框架)
+- **Vite**: 构建工具
+- **Pinia**: 状态管理
+- **Vue Router**: 路由管理
+- **SCSS**: 样式预处理器
+
+### 数据存储
+- **MySQL**: 8.0+ (主数据库)
+- **Redis**: 7.0+ (缓存)
+- **MyBatis Plus**: 3.5.3.1 (ORM框架)
+- **Druid**: 1.2.16 (数据库连接池)
+
+### 微服务基础设施
+- **Nacos**: 2.2.0+ (服务注册发现和配置中心)
+- **Gateway**: Spring Cloud Gateway (API网关)
+
+### 工具和库
+- **Lombok**: 代码简化
+- **Hutool**: 工具类库
+- **FastJSON2**: JSON处理
+- **JWT**: 认证授权
+- **Knife4j**: API文档
+- **MapStruct**: 对象映射
+
+## 微服务架构
+
+### 服务列表
+| 服务名称 | 端口 | 描述 | 包路径 |
+|---------|------|------|--------|
+| emotion-gateway | 9000 | API网关 | com.emotionmuseum.gateway |
+| emotion-user | 9001 | 用户服务 | com.emotionmuseum.user |
+| emotion-ai | 9002 | AI对话服务 | com.emotionmuseum.ai |
+| emotion-record | 9003 | 情绪记录服务 | com.emotionmuseum.record |
+| emotion-growth | 9004 | 成长课题服务 | com.emotionmuseum.growth |
+| emotion-explore | 9005 | 地图探索服务 | com.emotionmuseum.explore |
+| emotion-reward | 9006 | 成就奖励服务 | com.emotionmuseum.reward |
+| emotion-stats | 9007 | 统计分析服务 | com.emotionmuseum.stats |
+| emotion-common | - | 公共模块 | com.emotionmuseum.common |
+
+## 开发规范
+
+### 代码结构约定
+
+每个微服务模块遵循标准的MVC分层架构:
+```
+emotion-[service]/
+├── sql # 数据库脚本
+├── src/main/java/com/emotionmuseum/[service]/
+│ ├── [Service]Application.java # 启动类
+│ ├── controller/ # 控制器层
+│ ├── service/ # 服务层
+│ │ └── impl/ # 服务实现
+│ ├── mapper/ # 数据访问层
+│ ├── constants/ # 常量类
+│ ├── enums/ # 枚举类
+│ ├── entity/ # 实体类
+│ ├── request/ # 请求体
+│ ├── response/ # 响应体
+│ ├── dto/ # 数据传输对象
+│ ├── vo/ # 视图对象
+│ └── config/ # 配置类
+├── src/main/resources/
+│ ├── application.yml # 配置文件
+│ └── mapper/ # MyBatis XML文件
+└── pom.xml # 依赖配置
+```
+
+### 命名规范
+
+1. **包命名**: 全小写,使用点分隔
+2. **类命名**: 驼峰命名法,首字母大写
+3. **方法命名**: 驼峰命名法,首字母小写
+4. **常量命名**: 全大写,下划线分隔
+5. **数据库表名**: 小写,下划线分隔
+6. **字段命名**: 小写,下划线分隔
+
+### API设计规范
+
+1. **RESTful风格**: 使用标准HTTP方法(GET/POST/PUT/DELETE)
+2. **URL路径**: `/api/{service}/{resource}`
+3. **统一响应**: 使用`Result`包装所有API响应
+4. **状态码**: 使用`ResultCode`枚举定义业务状态码
+5. **分页**: 使用`PageQuery`进行分页查询
+6. **请求体**: 所有请求封装在request中
+7. **响应体**: 所有响应封装在response中
+
+### 数据库规范
+
+1. **表命名**: 使用模块前缀,如`user_info`、`ai_conversation`
+2. **字段命名**: 全小写,下划线分隔
+3. **主键**: 统一使用`id`,使用雪花算法生成,类型为varchar(36) UUID
+4. **公共字段**: 继承`BaseEntity`,包含创建时间、更新时间、逻辑删除等
+5. **逻辑删除**: 使用`deleted`字段,0=未删除,1=已删除
+
+### 异常处理
+
+1. **全局异常处理**: 在公共模块统一处理
+2. **业务异常**: 继承`RuntimeException`
+3. **异常响应**: 统一返回`Result.error()`格式
+
+## 开发工具指令
+
+### 自动化开发环境 (推荐)
+
+```bash
+# 启动所有服务并监控文件变化
+./dev-auto.sh
+
+# 查看服务状态
+./dev-auto.sh status
+
+# 停止所有服务
+./dev-auto.sh stop
+
+# 查看实时日志
+./dev-auto.sh logs
+```
+
+**自动化开发环境特性:**
+- **自动编译**: 检测Java/YAML/XML文件变化时自动重新编译
+- **热重载**: 使用Spring Boot DevTools实现代码变更后自动重启
+- **服务监控**: 实时监控所有微服务的健康状态
+- **统一日志**: 所有服务日志统一存储在`dev-logs/`目录
+- **一键管理**: 支持启动、停止、状态查看、日志查看等操作
+
+建议安装`fswatch`以获得更好的文件监控体验:
+```bash
+brew install fswatch # macOS
+apt-get install inotify-tools # Ubuntu
+```
+
+### 手动构建和启动
+
+```bash
+# 清理编译
+mvn clean compile -DskipTests
+
+# 编译所有模块
+mvn clean install -DskipTests
+
+# 启动单个服务
+cd emotion-[service] && mvn spring-boot:run
+```
+
+### 服务操作
+
+```bash
+# 健康检查
+curl http://localhost:808[x]/actuator/health
+
+# 查看服务日志 (手动启动)
+tail -f logs/emotion-[service].log
+
+# 查看服务日志 (自动化环境)
+tail -f dev-logs/emotion-[service].log
+```
+
+### iOS开发工具指令
+
+```bash
+# 在Xcode中打开项目
+open EmotionMuseum/EmotionMuseum.xcodeproj
+
+# 构建iOS应用
+xcodebuild -project EmotionMuseum/EmotionMuseum.xcodeproj -scheme EmotionMuseum -configuration Debug
+
+# 运行iOS模拟器
+xcrun simctl boot "iPhone 15 Pro"
+```
+
+### Web前端工具指令
+
+```bash
+# 进入web目录
+cd web
+
+# 安装依赖
+npm install
+
+# 启动开发服务器
+npm run dev
+
+# 构建生产版本
+npm run build
+```
+
+### 数据库操作
+
+```bash
+# 导入数据库结构
+mysql -u root -p emotion_museum < mysql_deploy_database.sql
+
+# 执行迁移脚本
+mysql -u root -p emotion_museum < [migration_file].sql
+```
+
+## 测试规范
+
+### 单元测试
+- 使用JUnit 5和Spring Boot Test
+- 测试覆盖率要求:控制器层>80%,服务层>90%
+- Mock外部依赖
+
+### 集成测试
+- 使用TestContainers进行数据库测试
+- 测试完整的API调用链路
+
+### API测试
+- 使用脚本文件测试API端点
+- 参考`test-api.sh`、`test-coze-api.sh`
+
+## 配置管理
+
+### Nacos配置
+- 环境: `emotion-dev` (开发环境)
+- 配置文件: `common-mysql.yml`、`common-redis.yml`、`coze-config.yml`
+- 各服务配置: `emotion-[service].yml`
+
+### 敏感信息
+- 数据库密码存储在Nacos配置中心
+- API密钥使用环境变量或配置中心管理
+- 生产环境配置独立管理
+
+## 代码提交规范
+
+### 提交信息格式
+```
+type(scope): description
+
+[optional body]
+
+[optional footer]
+```
+
+### 类型说明
+- `feat`: 新功能
+- `fix`: 修复bug
+- `docs`: 文档修改
+- `style`: 代码格式
+- `refactor`: 重构
+- `test`: 测试相关
+- `chore`: 构建工具
+
+### 示例
+```
+feat(user): add user registration API
+
+- Add user registration endpoint
+- Implement email validation
+- Add password encryption
+
+Closes #123
+```
+
+## 安全要求
+
+1. **认证授权**: 使用JWT token进行用户认证
+2. **数据加密**: 敏感数据加密存储
+3. **SQL注入**: 使用参数化查询防止SQL注入
+4. **XSS防护**: 对用户输入进行过滤和转义
+5. **CORS配置**: 限制跨域访问
+
+## 性能优化
+
+1. **数据库优化**: 合理使用索引,避免N+1查询
+2. **缓存策略**: 热点数据使用Redis缓存
+3. **连接池**: 合理配置数据库连接池参数
+4. **异步处理**: 耗时操作使用异步执行
+
+## 监控和日志
+
+1. **健康检查**: 所有服务提供`/actuator/health`端点
+2. **Prometheus指标**: 通过`/actuator/prometheus`暴露指标
+3. **日志级别**: 开发环境使用DEBUG,生产环境使用INFO
+4. **日志格式**: 统一使用结构化日志
+
+## 常见问题处理
+
+### 服务启动失败
+1. 检查Nacos服务是否启动
+2. 检查数据库连接配置
+3. 检查端口是否被占用
+4. 查看服务日志定位错误
+
+### 数据库问题
+1. 确认数据库服务状态
+2. 检查连接参数配置
+3. 验证数据库权限
+4. 查看慢查询日志
+
+### 缓存问题
+1. 检查Redis服务状态
+2. 验证缓存配置
+3. 清理过期缓存数据
+
+## 扩展开发
+
+### 添加新微服务
+1. 复制现有服务模块结构
+2. 修改包名和端口配置
+3. 添加到父工程pom.xml
+4. 更新启动脚本
+5. 配置Nacos注册信息
+
+### 添加新API
+1. 在对应服务的controller包下创建控制器
+2. 定义DTO和VO对象
+3. 实现服务层逻辑
+4. 添加数据访问层
+5. 编写单元测试
+6. 更新API文档
+
+## 部署说明
+
+### 开发环境
+- 本地启动:使用Maven插件
+- Docker部署:提供Dockerfile和docker-compose.yml
+
+### 生产环境
+- 容器化部署:使用Kubernetes
+- 配置管理:环境变量和配置中心
+- 服务监控:Prometheus + Grafana
+
+## Claude 开发助手指南
+
+### 任务执行原则
+
+1. **准确性第一**: 在执行任何任务前,必须先分析项目结构和现有代码,确保理解正确
+2. **规范性约束**: 严格遵循项目的代码规范、命名约定和架构模式
+3. **功能完整性**: 确保所有修改不会破坏现有功能,新增功能要完整实现
+4. **测试验证**: 重要修改后需要运行相关测试,确保代码质量
+
+### 文件操作规范
+
+1. **iOS项目 (SwiftUI)**
+ - 修改前先读取 `EmotionMuseum/EmotionMuseum.xcodeproj/project.pbxproj` 了解项目结构
+ - 遵循SwiftUI声明式编程范式
+ - 使用现有的颜色主题和组件样式
+ - 新增文件要正确添加到Xcode项目中
+
+2. **后端微服务 (Spring Boot)**
+ - 修改前检查 `backend/pom.xml` 了解模块依赖
+ - 遵循MVC分层架构
+ - 使用统一的Result响应格式
+ - 数据库操作使用MyBatis Plus
+
+3. **Web前端 (Vue.js)**
+ - 修改前检查 `web/package.json` 了解依赖版本
+ - 使用Vue 3 Composition API
+ - 遵循组件化开发模式
+ - 样式使用SCSS预处理器
+
+### 常用检查清单
+
+#### 开始任务前
+- [ ] 读取相关文档 (*.md 文件) 了解需求
+- [ ] 分析现有代码结构和实现模式
+- [ ] 确认修改范围和影响面
+- [ ] 检查是否有相关测试需要更新
+
+#### 完成任务后
+- [ ] 代码符合项目规范
+- [ ] 新增功能完整可用
+- [ ] 没有破坏现有功能
+- [ ] 相关文档已更新
+- [ ] 提交信息规范
+
+### 错误预防
+
+1. **避免盲目修改**: 不要在不了解项目结构的情况下直接修改代码
+2. **保持一致性**: 新增代码要与现有代码风格保持一致
+3. **完整实现**: 不要留下未完成的功能或空实现
+4. **谨慎删除**: 删除代码前确认没有其他地方在使用
+
+### 紧急情况处理
+
+如果在开发过程中遇到以下情况,需要立即停止并寻求确认:
+- 发现代码存在安全风险
+- 修改可能影响核心业务逻辑
+- 需要修改数据库结构
+- 涉及第三方API集成
+
+---
+
+以上是情绪博物馆全栈项目的完整开发指南,请在开发过程中严格遵循相关规范,确保代码质量和项目的可维护性。
\ No newline at end of file
diff --git a/CUSTOM_DEPLOYMENT.md b/CUSTOM_DEPLOYMENT.md
new file mode 100644
index 0000000..41ab17d
--- /dev/null
+++ b/CUSTOM_DEPLOYMENT.md
@@ -0,0 +1,342 @@
+# 情绪博物馆自定义目录部署指南
+
+## 📋 部署架构
+
+根据您的要求,本部署方案采用以下目录结构:
+
+```
+/data/
+├── www/emotion-museum/ # 前端静态文件目录
+│ ├── index.html
+│ ├── assets/
+│ └── ...
+├── builds/ # 后端JAR文件目录
+│ ├── emotion-gateway.jar
+│ ├── emotion-ai.jar
+│ └── emotion-user.jar
+└── logs/emotion-museum/ # 日志目录
+ ├── nginx/
+ ├── gateway/
+ ├── ai/
+ ├── user/
+ ├── mysql/
+ ├── redis/
+ └── nacos/
+```
+
+## 🏗️ 服务架构
+
+```
+┌─────────────┐ ┌─────────────┐ ┌─────────────┐
+│ 用户访问 │───▶│ Nginx │───▶│ 静态文件 │
+└─────────────┘ │ (80/443) │ │ /data/www/ │
+ └─────────────┘ └─────────────┘
+ │
+ ▼
+ ┌─────────────┐ ┌─────────────┐
+ │ API代理 │───▶│ Gateway │
+ │ /api/* │ │ (9000) │
+ └─────────────┘ └─────────────┘
+ │
+ ▼
+ ┌─────────────┐ ┌─────────────┐
+ │ 微服务集群 │ │ JAR文件 │
+ │ AI/User/... │ │ /data/builds│
+ └─────────────┘ └─────────────┘
+```
+
+## 🚀 快速部署
+
+### 1. 准备环境
+
+```bash
+# 确保Docker和Docker Compose已安装
+docker --version
+docker-compose --version
+
+# 创建必要目录
+sudo mkdir -p /data/www/emotion-museum
+sudo mkdir -p /data/builds
+sudo mkdir -p /data/logs/emotion-museum
+```
+
+### 2. 配置环境变量
+
+```bash
+# 编辑环境变量文件
+vim .env
+```
+
+**配置说明**:
+```bash
+# Coze API配置(已配置为与开发环境一致)
+COZE_API_TOKEN=pat_GCR4qKzqpf90wMCvKsldMrB18KG3QsLDci65bZthssKsbLxu8X70BKYumleDcabO
+
+# 数据库密码(可根据需要修改)
+MYSQL_ROOT_PASSWORD=123456
+MYSQL_PASSWORD=emotion123
+```
+
+### 3. 一键部署
+
+```bash
+# 给脚本执行权限
+chmod +x deploy-custom.sh
+
+# 执行自定义部署
+./deploy-custom.sh
+```
+
+## 📁 文件说明
+
+### 新增配置文件
+
+1. **`docker-compose.custom.yml`** - 自定义Docker配置
+ - 前端文件直接从宿主机目录提供
+ - 后端JAR文件挂载到容器
+ - 日志统一保存到指定目录
+
+2. **`deploy-custom.sh`** - 自定义部署脚本
+ - 自动检查和构建前后端
+ - 部署文件到指定目录
+ - 启动Docker服务
+
+3. **`manage-custom.sh`** - 自定义管理脚本
+ - 服务管理和监控
+ - 日志查看和健康检查
+ - 数据备份和恢复
+
+### 修改的配置文件
+
+1. **`deploy/nginx/conf.d/emotion-museum.conf`** - Nginx配置
+ - 前端文件直接从 `/data/www/emotion-museum` 提供
+ - API请求代理到Docker容器内的服务
+ - 日志保存到 `/data/logs/emotion-museum/nginx/`
+
+2. **`deploy/nginx/nginx.conf`** - Nginx主配置
+ - 更新上游服务器定义
+ - 支持容器间通信
+
+## 🔧 端口配置
+
+| 服务 | 容器端口 | 宿主机端口 | 说明 |
+|------|----------|------------|------|
+| Nginx | 80/443 | 80/443 | Web访问 |
+| Gateway | 9000 | 9000 | API网关 |
+| AI Service | 9002 | 9002 | AI服务 |
+| User Service | 9001 | 9001 | 用户服务 |
+| MySQL | 3306 | 3306 | 数据库 |
+| Redis | 6379 | 6379 | 缓存 |
+| Nacos | 8848 | 8848 | 注册中心 |
+
+## 🛠️ 管理命令
+
+### 服务管理
+```bash
+# 启动所有服务
+./manage-custom.sh start
+
+# 停止所有服务
+./manage-custom.sh stop
+
+# 重启所有服务
+./manage-custom.sh restart
+
+# 重启指定服务
+./manage-custom.sh restart emotion-ai
+
+# 查看服务状态
+./manage-custom.sh status
+```
+
+### 日志管理
+```bash
+# 查看所有日志
+./manage-custom.sh logs
+
+# 跟踪日志输出
+./manage-custom.sh logs -f
+
+# 查看指定服务日志
+./manage-custom.sh logs -s nginx
+./manage-custom.sh logs -s emotion-gateway
+```
+
+### 健康检查
+```bash
+# 执行健康检查
+./manage-custom.sh health
+
+# 实时监控
+./manage-custom.sh monitor
+```
+
+### 数据管理
+```bash
+# 备份数据
+./manage-custom.sh backup
+
+# 更新服务
+./manage-custom.sh update
+
+# 清理资源
+./manage-custom.sh clean
+```
+
+## 📊 目录详情
+
+### 前端目录 `/data/www/emotion-museum/`
+```
+/data/www/emotion-museum/
+├── index.html # 主页面
+├── assets/ # 静态资源
+│ ├── css/ # 样式文件
+│ ├── js/ # JavaScript文件
+│ └── images/ # 图片文件
+└── favicon.ico # 网站图标
+```
+
+### 后端目录 `/data/builds/`
+```
+/data/builds/
+├── emotion-gateway.jar # 网关服务JAR
+├── emotion-ai.jar # AI服务JAR
+└── emotion-user.jar # 用户服务JAR
+```
+
+### 日志目录 `/data/logs/emotion-museum/`
+```
+/data/logs/emotion-museum/
+├── nginx/ # Nginx日志
+│ ├── access.log
+│ └── error.log
+├── gateway/ # 网关服务日志
+├── ai/ # AI服务日志
+├── user/ # 用户服务日志
+├── mysql/ # MySQL日志
+├── redis/ # Redis日志
+└── nacos/ # Nacos日志
+```
+
+## 🔄 更新流程
+
+### 更新前端
+```bash
+# 1. 重新构建前端
+cd web
+npm run build
+
+# 2. 部署到目标目录
+sudo rm -rf /data/www/emotion-museum/*
+sudo cp -r dist/* /data/www/emotion-museum/
+sudo chown -R www-data:www-data /data/www/emotion-museum
+
+# 3. 重启Nginx(可选)
+./manage-custom.sh restart nginx
+```
+
+### 更新后端
+```bash
+# 1. 重新构建后端
+cd backend
+mvn clean package -DskipTests
+
+# 2. 部署JAR文件
+sudo cp emotion-gateway/target/emotion-gateway-1.0.0.jar /data/builds/emotion-gateway.jar
+sudo cp emotion-ai/target/emotion-ai-1.0.0.jar /data/builds/emotion-ai.jar
+sudo cp emotion-user/target/emotion-user-1.0.0.jar /data/builds/emotion-user.jar
+
+# 3. 重启相关服务
+./manage-custom.sh restart emotion-gateway
+./manage-custom.sh restart emotion-ai
+./manage-custom.sh restart emotion-user
+```
+
+### 一键更新
+```bash
+# 自动构建和部署
+./manage-custom.sh update
+```
+
+## 🚨 故障排除
+
+### 常见问题
+
+#### 1. 前端访问404
+```bash
+# 检查前端文件是否存在
+ls -la /data/www/emotion-museum/
+
+# 检查Nginx配置
+./manage-custom.sh logs -s nginx
+
+# 检查文件权限
+sudo chown -R www-data:www-data /data/www/emotion-museum
+```
+
+#### 2. API调用失败
+```bash
+# 检查网关服务状态
+./manage-custom.sh logs -s emotion-gateway
+
+# 检查服务健康状态
+curl http://localhost:9000/actuator/health
+```
+
+#### 3. 服务启动失败
+```bash
+# 检查JAR文件是否存在
+ls -la /data/builds/
+
+# 检查服务日志
+./manage-custom.sh logs -s emotion-ai
+
+# 检查容器状态
+docker-compose -f docker-compose.custom.yml ps
+```
+
+#### 4. 日志文件过大
+```bash
+# 清理日志文件
+sudo find /data/logs/emotion-museum -name "*.log" -size +100M -delete
+
+# 设置日志轮转
+sudo logrotate -f /etc/logrotate.conf
+```
+
+## 📞 技术支持
+
+### 快速诊断
+```bash
+# 执行健康检查
+./manage-custom.sh health
+
+# 查看服务状态
+./manage-custom.sh status
+
+# 查看实时监控
+./manage-custom.sh monitor
+```
+
+### 获取帮助
+```bash
+# 查看管理命令帮助
+./manage-custom.sh --help
+
+# 查看部署脚本帮助
+./deploy-custom.sh --help
+```
+
+---
+
+## ✅ 部署检查清单
+
+- [ ] **目录创建**: `/data/www/emotion-museum`, `/data/builds`, `/data/logs/emotion-museum`
+- [ ] **环境配置**: `COZE_API_TOKEN` 已配置为与开发环境一致
+- [ ] **前端部署**: 静态文件已复制到 `/data/www/emotion-museum/`
+- [ ] **后端部署**: JAR文件已复制到 `/data/builds/`
+- [ ] **服务启动**: 所有Docker容器正常运行
+- [ ] **访问测试**: 前端页面和API接口正常访问
+- [ ] **日志检查**: 日志文件正常生成到 `/data/logs/emotion-museum/`
+
+**🎉 恭喜!您的情绪博物馆项目已成功部署到自定义目录结构!**
diff --git a/DEPLOY.md b/DEPLOY.md
new file mode 100644
index 0000000..8aa403a
--- /dev/null
+++ b/DEPLOY.md
@@ -0,0 +1,313 @@
+# 情绪博物馆容器部署指南
+
+## 📋 概述
+
+本文档提供了情绪博物馆项目的完整容器化部署方案,支持开发环境和生产环境的快速部署。
+
+## 🏗️ 架构说明
+
+### 服务组件
+- **前端应用** (Vue3 + Ant Design) - 端口: 80/3000
+- **API网关** (Spring Cloud Gateway) - 端口: 9000
+- **AI服务** (Spring Boot + Coze API) - 端口: 9002
+- **用户服务** (Spring Boot) - 端口: 9001
+- **MySQL数据库** - 端口: 3306
+- **Redis缓存** - 端口: 6379
+- **Nacos注册中心** - 端口: 8848
+- **Nginx反向代理** - 端口: 80/443
+
+### 网络架构
+```
+Internet → Nginx → Frontend/Gateway → Microservices → Database
+```
+
+## 🚀 快速开始
+
+### 1. 系统要求
+- **操作系统**: Linux/macOS/Windows
+- **Docker**: 20.10+
+- **Docker Compose**: 1.29+
+- **内存**: 最少4GB,推荐8GB+
+- **磁盘**: 最少10GB可用空间
+
+### 2. 一键部署
+```bash
+# 克隆项目
+git clone
+cd EmotionMuseum
+
+# 快速部署(自动安装依赖)
+chmod +x quick-deploy.sh
+./quick-deploy.sh
+
+# 或者手动部署
+chmod +x deploy.sh
+./deploy.sh
+```
+
+### 3. 访问应用
+- **前端应用**: http://localhost
+- **API文档**: http://localhost:9000/doc.html
+- **Nacos控制台**: http://localhost:8848/nacos (nacos/nacos)
+
+## 📁 文件结构
+
+```
+EmotionMuseum/
+├── docker-compose.yml # 开发环境配置
+├── docker-compose.prod.yml # 生产环境配置
+├── deploy.sh # 部署脚本
+├── quick-deploy.sh # 快速部署脚本
+├── manage.sh # 管理脚本
+├── .env # 环境变量
+├── deploy/ # 部署配置
+│ ├── nginx/ # Nginx配置
+│ │ ├── nginx.conf
+│ │ ├── conf.d/
+│ │ └── ssl/
+│ ├── mysql/ # MySQL配置
+│ └── redis/ # Redis配置
+├── backend/ # 后端服务
+│ ├── emotion-gateway/
+│ │ └── Dockerfile
+│ ├── emotion-ai/
+│ │ └── Dockerfile
+│ └── emotion-user/
+│ └── Dockerfile
+└── web/ # 前端应用
+ ├── Dockerfile
+ └── nginx.conf
+```
+
+## ⚙️ 配置说明
+
+### 环境变量配置
+编辑 `.env` 文件:
+```bash
+# 数据库配置
+MYSQL_ROOT_PASSWORD=123456
+MYSQL_DATABASE=emotion_museum
+MYSQL_USER=emotion
+MYSQL_PASSWORD=emotion123
+
+# Coze API配置
+COZE_API_TOKEN=your-coze-api-token
+
+# 时区设置
+TZ=Asia/Shanghai
+```
+
+### Nginx配置
+- **主配置**: `deploy/nginx/nginx.conf`
+- **站点配置**: `deploy/nginx/conf.d/emotion-museum.conf`
+- **SSL证书**: `deploy/nginx/ssl/`
+
+### 数据库配置
+- **MySQL配置**: `deploy/mysql/conf.d/my.cnf`
+- **初始化脚本**: `backend/mysql_emotion_museum_final.sql`
+
+## 🛠️ 管理命令
+
+### 基础操作
+```bash
+# 启动所有服务
+./manage.sh start
+
+# 停止所有服务
+./manage.sh stop
+
+# 重启所有服务
+./manage.sh restart
+
+# 查看服务状态
+./manage.sh status
+```
+
+### 日志管理
+```bash
+# 查看所有服务日志
+./manage.sh logs
+
+# 跟踪日志输出
+./manage.sh logs -f
+
+# 查看特定服务日志
+./manage.sh logs -s gateway
+./manage.sh logs -s ai-service
+```
+
+### 服务管理
+```bash
+# 重启特定服务
+./manage.sh restart gateway
+./manage.sh restart ai-service
+
+# 健康检查
+./manage.sh health
+
+# 监控面板
+./manage.sh monitor
+```
+
+### 数据管理
+```bash
+# 备份数据
+./manage.sh backup
+
+# 恢复数据
+./manage.sh restore backup_file.tar.gz
+
+# 更新服务
+./manage.sh update
+
+# 清理资源
+./manage.sh clean
+```
+
+## 🔧 生产环境部署
+
+### 1. 使用生产配置
+```bash
+# 使用生产环境配置文件
+docker-compose -f docker-compose.prod.yml up -d
+```
+
+### 2. SSL证书配置
+```bash
+# 放置SSL证书
+cp your-domain.crt deploy/nginx/ssl/emotion-museum.crt
+cp your-domain.key deploy/nginx/ssl/emotion-museum.key
+
+# 修改Nginx配置启用HTTPS
+vim deploy/nginx/conf.d/emotion-museum.conf
+```
+
+### 3. 域名配置
+修改 `deploy/nginx/conf.d/emotion-museum.conf`:
+```nginx
+server_name your-domain.com www.your-domain.com;
+```
+
+### 4. 防火墙配置
+```bash
+# Ubuntu/Debian
+sudo ufw allow 80/tcp
+sudo ufw allow 443/tcp
+
+# CentOS/RHEL
+sudo firewall-cmd --permanent --add-port=80/tcp
+sudo firewall-cmd --permanent --add-port=443/tcp
+sudo firewall-cmd --reload
+```
+
+## 📊 监控和维护
+
+### 服务监控
+```bash
+# 实时监控
+./manage.sh monitor
+
+# 资源使用情况
+docker stats
+
+# 服务状态
+docker-compose ps
+```
+
+### 日志查看
+```bash
+# 应用日志
+./manage.sh logs -f
+
+# 系统日志
+tail -f logs/nginx/access.log
+tail -f logs/mysql/error.log
+```
+
+### 性能优化
+1. **数据库优化**: 调整 `deploy/mysql/conf.d/my.cnf`
+2. **Redis优化**: 调整 `deploy/redis/redis.conf`
+3. **Nginx优化**: 调整 `deploy/nginx/nginx.conf`
+4. **JVM优化**: 修改Dockerfile中的JVM参数
+
+## 🔒 安全配置
+
+### 1. 数据库安全
+- 修改默认密码
+- 限制访问IP
+- 启用SSL连接
+
+### 2. Redis安全
+- 设置密码认证
+- 绑定特定IP
+- 禁用危险命令
+
+### 3. Nginx安全
+- 启用HTTPS
+- 配置安全头
+- 限制请求频率
+
+### 4. 应用安全
+- 配置JWT密钥
+- 启用CORS限制
+- 设置API限流
+
+## 🚨 故障排除
+
+### 常见问题
+
+#### 1. 服务启动失败
+```bash
+# 查看服务日志
+./manage.sh logs -s service-name
+
+# 检查端口占用
+netstat -tlnp | grep :port
+
+# 重启服务
+./manage.sh restart service-name
+```
+
+#### 2. 数据库连接失败
+```bash
+# 检查MySQL状态
+docker-compose exec mysql mysqladmin ping -u root -p
+
+# 查看数据库日志
+./manage.sh logs -s mysql
+```
+
+#### 3. 前端访问异常
+```bash
+# 检查Nginx配置
+nginx -t
+
+# 查看Nginx日志
+./manage.sh logs -s nginx
+```
+
+#### 4. API调用失败
+```bash
+# 检查网关状态
+curl http://localhost:9000/actuator/health
+
+# 查看网关日志
+./manage.sh logs -s gateway
+```
+
+### 性能问题
+1. **内存不足**: 增加服务器内存或调整JVM参数
+2. **磁盘空间**: 清理日志文件和Docker镜像
+3. **网络延迟**: 检查服务间网络连接
+
+## 📞 技术支持
+
+如遇到问题,请:
+1. 查看相关服务日志
+2. 检查配置文件
+3. 参考故障排除指南
+4. 联系技术支持团队
+
+---
+
+**部署完成后,请及时修改默认密码和配置文件中的敏感信息!**
diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md
new file mode 100644
index 0000000..04bbd34
--- /dev/null
+++ b/DEPLOYMENT.md
@@ -0,0 +1,256 @@
+# 情绪博物馆项目部署指南
+
+## 概述
+
+本文档提供了情绪博物馆项目的完整部署指南,包括一键部署脚本的使用方法和手动部署步骤。
+
+## 系统要求
+
+### 本地开发环境
+- Java 17+
+- Maven 3.6+
+- Node.js 18+
+- SSH客户端
+
+### 服务器环境
+- CentOS 7/8 或 RHEL 7/8
+- 最小 4GB RAM,推荐 8GB+
+- 最小 50GB 磁盘空间
+- Docker 支持
+
+## 快速部署
+
+### 1. 一键部署(推荐)
+
+```bash
+# 克隆项目
+git clone
+cd EmotionMuseum
+
+# 执行一键部署
+./deploy-final.sh all
+```
+
+### 2. 分步部署
+
+```bash
+# 1. 构建项目
+./deploy-final.sh build
+
+# 2. 配置服务器环境
+./deploy-final.sh env
+
+# 3. 配置数据库
+./deploy-final.sh mysql
+
+# 4. 配置Redis
+./deploy-final.sh redis
+
+# 5. 配置Nacos
+./deploy-final.sh nacos
+
+# 6. 上传构建产物
+./deploy-final.sh upload
+
+# 7. 导入数据库
+./deploy-final.sh import-db
+
+# 8. 部署应用服务
+./deploy-final.sh deploy
+
+# 9. 配置Nginx
+./deploy-final.sh nginx
+
+# 10. 创建密码记录
+./deploy-final.sh passwords
+
+# 11. 健康检查
+./deploy-final.sh health
+```
+
+## 服务管理
+
+### 启动/停止服务
+
+```bash
+# 查看服务状态
+./deploy-final.sh status
+
+# 启动服务
+./deploy-final.sh start
+
+# 停止服务
+./deploy-final.sh stop
+
+# 重启服务
+./deploy-final.sh restart
+```
+
+### 查看日志
+
+```bash
+# 查看网关服务日志
+./deploy-final.sh logs gateway
+
+# 查看AI服务日志
+./deploy-final.sh logs ai
+
+# 查看用户服务日志
+./deploy-final.sh logs user
+```
+
+## 配置说明
+
+### 环境变量配置
+
+主要配置文件:
+- `.env.prod` - 生产环境配置
+- `web/.env.production` - 前端生产环境配置
+
+### 服务器配置
+
+默认配置:
+- 服务器IP: 47.111.10.27
+- MySQL端口: 3306
+- Redis端口: 6379
+- Nacos端口: 8848
+- 网关端口: 9000
+- AI服务端口: 9002
+- 用户服务端口: 9001
+
+### 目录结构
+
+```
+/data/
+├── builds/ # 应用JAR文件
+├── www/ # 前端文件
+│ └── emotion-museum/
+│ └── web/
+├── logs/ # 日志文件
+│ └── emotion-museum/
+│ ├── gateway/
+│ ├── ai/
+│ └── user/
+└── programs/ # 其他程序文件
+```
+
+## 访问地址
+
+部署完成后的访问地址:
+
+- **前端应用**: http://47.111.10.27/emotion-museum/
+- **API网关**: http://47.111.10.27:9000
+- **Nacos控制台**: http://47.111.10.27:8848/nacos
+
+## 故障排除
+
+### 常见问题
+
+1. **服务无法启动**
+ ```bash
+ # 检查服务状态
+ ./deploy-final.sh status
+
+ # 查看日志
+ ./deploy-final.sh logs
+ ```
+
+2. **数据库连接失败**
+ ```bash
+ # 检查MySQL容器状态
+ ssh root@47.111.10.27 "docker ps | grep mysql"
+
+ # 检查数据库连接
+ ssh root@47.111.10.27 "docker exec emotion-mysql-prod mysql -uemotion -pEmotionDB2024! -e 'SELECT 1;'"
+ ```
+
+3. **前端页面无法访问**
+ ```bash
+ # 检查Nginx状态
+ ssh root@47.111.10.27 "systemctl status nginx"
+
+ # 检查前端文件
+ ssh root@47.111.10.27 "ls -la /data/www/emotion-museum/web/"
+ ```
+
+### 日志位置
+
+- 应用日志: `/data/logs/emotion-museum/*/app.log`
+- Nginx日志: `/var/log/nginx/`
+- Docker日志: `docker logs `
+
+## 安全建议
+
+1. **修改默认密码**
+ - MySQL root密码
+ - 应用数据库密码
+ - 服务器SSH密钥
+
+2. **配置防火墙**
+ ```bash
+ # 只开放必要端口
+ firewall-cmd --permanent --add-port=80/tcp
+ firewall-cmd --permanent --add-port=8848/tcp
+ firewall-cmd --reload
+ ```
+
+3. **定期备份**
+ ```bash
+ # 数据库备份
+ docker exec emotion-mysql-prod mysqldump -uemotion -pEmotionDB2024! emotion_museum > backup.sql
+ ```
+
+## 更新部署
+
+### 应用更新
+
+```bash
+# 1. 构建新版本
+./deploy-final.sh build
+
+# 2. 停止服务
+./deploy-final.sh stop
+
+# 3. 上传新文件
+./deploy-final.sh upload
+
+# 4. 启动服务
+./deploy-final.sh start
+```
+
+### 配置更新
+
+```bash
+# 重新配置Nginx
+./deploy-final.sh nginx
+
+# 重启服务
+./deploy-final.sh restart
+```
+
+## 监控和维护
+
+### 健康检查
+
+```bash
+# 执行完整健康检查
+./deploy-final.sh health
+```
+
+### 性能监控
+
+建议使用以下工具进行监控:
+- Prometheus + Grafana
+- ELK Stack (日志分析)
+- Docker监控
+
+## 联系支持
+
+如遇到部署问题,请提供以下信息:
+1. 错误日志
+2. 系统环境信息
+3. 部署步骤和配置
+
+---
+
+**注意**: 请确保在生产环境中修改默认密码和配置,并定期进行安全更新。
diff --git a/DEPLOYMENT_GUIDE.md b/DEPLOYMENT_GUIDE.md
new file mode 100644
index 0000000..9e01895
--- /dev/null
+++ b/DEPLOYMENT_GUIDE.md
@@ -0,0 +1,371 @@
+# 情绪博物馆完整部署指南
+
+## 📦 部署包信息
+
+**包名称**: `emotion-museum-1.0.0-20250713_111829.tar.gz`
+**包大小**: 680KB
+**SHA256**: `900d585f575b1619e74296496e2fe22f2c2e71b6ad8901d7cab82634765cc10d`
+**构建时间**: 2025-07-13 11:18:29
+
+## 🎯 部署概述
+
+本部署包包含了情绪博物馆项目的完整容器化部署方案,支持:
+- ✅ 前端Vue3应用(已构建)
+- ✅ 后端微服务(Gateway、AI、User)
+- ✅ 数据库脚本(MySQL)
+- ✅ 完整的Docker配置
+- ✅ 自动化部署脚本
+- ✅ 监控和管理工具
+
+## 🏗️ 系统架构
+
+```
+┌─────────────┐ ┌─────────────┐ ┌─────────────┐
+│ 用户访问 │───▶│ Nginx │───▶│ 前端应用 │
+└─────────────┘ │ (80/443) │ │ (3000) │
+ └─────────────┘ └─────────────┘
+ │
+ ▼
+ ┌─────────────┐ ┌─────────────┐
+ │ API网关 │───▶│ 微服务集群 │
+ │ (9000) │ │ AI/User/... │
+ └─────────────┘ └─────────────┘
+ │
+ ▼
+ ┌─────────────┐ ┌─────────────┐
+ │ MySQL │ │ Redis │
+ │ (3306) │ │ (6379) │
+ └─────────────┘ └─────────────┘
+```
+
+## 🚀 快速部署(推荐)
+
+### 1. 下载和解压
+```bash
+# 下载部署包到服务器
+wget https://your-domain.com/emotion-museum-1.0.0-20250713_111829.tar.gz
+
+# 验证文件完整性
+echo "900d585f575b1619e74296496e2fe22f2c2e71b6ad8901d7cab82634765cc10d emotion-museum-1.0.0-20250713_111829.tar.gz" | sha256sum -c
+
+# 解压部署包
+tar -xzf emotion-museum-1.0.0-20250713_111829.tar.gz
+cd emotion-museum-1.0.0-20250713_111829
+```
+
+### 2. 配置环境变量
+```bash
+# 复制环境变量模板
+cp .env .env.local
+
+# 编辑配置文件
+vim .env.local
+```
+
+**必须配置的项目**:
+```bash
+# Coze API配置(必须修改)
+COZE_API_TOKEN=your-actual-coze-api-token
+
+# 数据库密码(建议修改)
+MYSQL_ROOT_PASSWORD=your-secure-password
+MYSQL_PASSWORD=your-secure-password
+
+# 时区设置
+TZ=Asia/Shanghai
+```
+
+### 3. 一键部署
+```bash
+# 给脚本执行权限
+chmod +x quick-deploy.sh
+
+# 执行一键部署(自动安装Docker等依赖)
+./quick-deploy.sh
+```
+
+### 4. 验证部署
+```bash
+# 查看服务状态
+./manage.sh status
+
+# 健康检查
+./manage.sh health
+
+# 查看日志
+./manage.sh logs
+```
+
+## 🔧 手动部署(高级用户)
+
+### 1. 环境准备
+```bash
+# 安装Docker
+curl -fsSL https://get.docker.com | sh
+sudo systemctl start docker
+sudo systemctl enable docker
+
+# 安装Docker Compose
+sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
+sudo chmod +x /usr/local/bin/docker-compose
+
+# 添加用户到docker组
+sudo usermod -aG docker $USER
+```
+
+### 2. 配置防火墙
+```bash
+# Ubuntu/Debian
+sudo ufw allow 80/tcp
+sudo ufw allow 443/tcp
+sudo ufw allow 8848/tcp # Nacos(可选)
+
+# CentOS/RHEL
+sudo firewall-cmd --permanent --add-port=80/tcp
+sudo firewall-cmd --permanent --add-port=443/tcp
+sudo firewall-cmd --permanent --add-port=8848/tcp
+sudo firewall-cmd --reload
+```
+
+### 3. 部署服务
+```bash
+# 开发环境部署
+./deploy.sh
+
+# 或生产环境部署
+docker-compose -f docker-compose.prod.yml up -d
+```
+
+## ⚙️ 配置说明
+
+### 环境变量配置
+| 变量名 | 说明 | 默认值 | 是否必须 |
+|--------|------|--------|----------|
+| `COZE_API_TOKEN` | Coze API令牌 | - | ✅ 必须 |
+| `MYSQL_ROOT_PASSWORD` | MySQL root密码 | 123456 | 🔶 建议修改 |
+| `MYSQL_PASSWORD` | MySQL用户密码 | emotion123 | 🔶 建议修改 |
+| `TZ` | 时区设置 | Asia/Shanghai | ⭕ 可选 |
+| `DOMAIN_NAME` | 域名(生产环境) | localhost | ⭕ 可选 |
+
+### 端口配置
+| 服务 | 端口 | 说明 |
+|------|------|------|
+| Nginx | 80, 443 | Web访问端口 |
+| Gateway | 9000 | API网关 |
+| AI Service | 9002 | AI服务 |
+| User Service | 9001 | 用户服务 |
+| MySQL | 3306 | 数据库 |
+| Redis | 6379 | 缓存 |
+| Nacos | 8848 | 注册中心 |
+
+## 🌐 生产环境配置
+
+### 1. HTTPS配置
+```bash
+# 1. 准备SSL证书
+mkdir -p deploy/nginx/ssl
+cp your-domain.crt deploy/nginx/ssl/emotion-museum.crt
+cp your-domain.key deploy/nginx/ssl/emotion-museum.key
+
+# 2. 修改Nginx配置
+vim deploy/nginx/conf.d/emotion-museum.conf
+# 取消HTTPS相关配置的注释
+
+# 3. 重启Nginx
+docker-compose restart nginx
+```
+
+### 2. 域名配置
+```bash
+# 修改Nginx配置中的域名
+vim deploy/nginx/conf.d/emotion-museum.conf
+# 将 localhost 替换为您的实际域名
+```
+
+### 3. 性能优化
+```bash
+# 1. 调整MySQL配置
+vim deploy/mysql/conf.d/my.cnf
+
+# 2. 调整Redis配置
+vim deploy/redis/redis.conf
+
+# 3. 调整JVM参数(在Dockerfile中)
+# -Xms512m -Xmx1024m
+```
+
+## 🛠️ 管理命令
+
+### 服务管理
+```bash
+./manage.sh start # 启动所有服务
+./manage.sh stop # 停止所有服务
+./manage.sh restart # 重启所有服务
+./manage.sh restart gateway # 重启指定服务
+./manage.sh status # 查看服务状态
+```
+
+### 日志管理
+```bash
+./manage.sh logs # 查看所有日志
+./manage.sh logs -f # 跟踪日志输出
+./manage.sh logs -s gateway # 查看网关日志
+./manage.sh logs -s ai-service # 查看AI服务日志
+```
+
+### 数据管理
+```bash
+./manage.sh backup # 备份数据
+./manage.sh restore backup.tar.gz # 恢复数据
+./manage.sh update # 更新服务
+./manage.sh clean # 清理资源
+```
+
+### 监控工具
+```bash
+./manage.sh monitor # 实时监控面板
+./manage.sh health # 健康检查
+```
+
+## 📊 访问地址
+
+部署完成后,您可以通过以下地址访问:
+
+| 服务 | 地址 | 说明 |
+|------|------|------|
+| 前端应用 | http://localhost | 主要访问入口 |
+| API文档 | http://localhost:9000/doc.html | Swagger文档 |
+| Nacos控制台 | http://localhost:8848/nacos | 服务注册中心 |
+| 网关健康检查 | http://localhost:9000/actuator/health | 服务状态 |
+
+**默认账号**:
+- Nacos: nacos / nacos
+
+## 🚨 故障排除
+
+### 常见问题
+
+#### 1. 端口冲突
+```bash
+# 检查端口占用
+netstat -tlnp | grep :80
+netstat -tlnp | grep :3306
+
+# 解决方案:修改docker-compose.yml中的端口映射
+```
+
+#### 2. 服务启动失败
+```bash
+# 查看具体错误
+./manage.sh logs -s service-name
+
+# 常见原因:
+# - 内存不足
+# - 端口被占用
+# - 配置文件错误
+# - 依赖服务未启动
+```
+
+#### 3. 数据库连接失败
+```bash
+# 检查MySQL状态
+docker-compose exec mysql mysqladmin ping -u root -p
+
+# 检查网络连接
+docker network ls
+docker network inspect emotion-network
+```
+
+#### 4. 前端访问404
+```bash
+# 检查Nginx配置
+docker-compose exec nginx nginx -t
+
+# 检查前端容器状态
+docker-compose ps web
+```
+
+#### 5. API调用失败
+```bash
+# 检查网关状态
+curl http://localhost:9000/actuator/health
+
+# 检查服务注册
+curl http://localhost:8848/nacos/v1/ns/instance/list?serviceName=emotion-ai
+```
+
+### 性能问题
+
+#### 1. 内存不足
+```bash
+# 查看内存使用
+free -h
+docker stats
+
+# 解决方案:
+# - 增加服务器内存
+# - 调整JVM参数
+# - 减少并发连接数
+```
+
+#### 2. 磁盘空间不足
+```bash
+# 查看磁盘使用
+df -h
+
+# 清理Docker资源
+./manage.sh clean
+docker system prune -a
+```
+
+#### 3. 网络延迟
+```bash
+# 检查服务间网络
+docker-compose exec gateway ping mysql
+docker-compose exec gateway ping redis
+
+# 优化网络配置
+# 使用自定义网络
+# 调整网络参数
+```
+
+## 🔒 安全建议
+
+### 1. 密码安全
+- ✅ 修改所有默认密码
+- ✅ 使用强密码策略
+- ✅ 定期更换密码
+
+### 2. 网络安全
+- ✅ 配置防火墙规则
+- ✅ 使用HTTPS加密
+- ✅ 限制不必要的端口访问
+
+### 3. 数据安全
+- ✅ 定期备份数据
+- ✅ 启用数据库SSL
+- ✅ 配置访问控制
+
+### 4. 应用安全
+- ✅ 配置JWT密钥
+- ✅ 启用API限流
+- ✅ 监控异常访问
+
+## 📞 技术支持
+
+### 获取帮助
+1. **查看文档**: 包内的 `DEPLOY.md` 和 `QUICK_START.md`
+2. **查看日志**: `./manage.sh logs -f`
+3. **健康检查**: `./manage.sh health`
+4. **查看版本**: `cat VERSION.txt`
+
+### 联系方式
+- 📧 技术支持邮箱: support@emotion-museum.com
+- 📱 技术支持QQ群: 123456789
+- 🌐 官方网站: https://emotion-museum.com
+
+---
+
+**🎉 恭喜!您已成功部署情绪博物馆项目!**
+
+**⚠️ 重要提醒:部署完成后请及时修改默认密码和敏感配置!**
diff --git a/EmotionMuseum/Assets.xcassets/AccentColor.colorset/Contents.json b/EmotionMuseum/Assets.xcassets/AccentColor.colorset/Contents.json
deleted file mode 100644
index eb87897..0000000
--- a/EmotionMuseum/Assets.xcassets/AccentColor.colorset/Contents.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "colors" : [
- {
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/EmotionMuseum/Assets.xcassets/AppIcon.appiconset/Contents.json b/EmotionMuseum/Assets.xcassets/AppIcon.appiconset/Contents.json
deleted file mode 100644
index ffdfe15..0000000
--- a/EmotionMuseum/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ /dev/null
@@ -1,85 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "platform" : "ios",
- "size" : "1024x1024"
- },
- {
- "appearances" : [
- {
- "appearance" : "luminosity",
- "value" : "dark"
- }
- ],
- "idiom" : "universal",
- "platform" : "ios",
- "size" : "1024x1024"
- },
- {
- "appearances" : [
- {
- "appearance" : "luminosity",
- "value" : "tinted"
- }
- ],
- "idiom" : "universal",
- "platform" : "ios",
- "size" : "1024x1024"
- },
- {
- "idiom" : "mac",
- "scale" : "1x",
- "size" : "16x16"
- },
- {
- "idiom" : "mac",
- "scale" : "2x",
- "size" : "16x16"
- },
- {
- "idiom" : "mac",
- "scale" : "1x",
- "size" : "32x32"
- },
- {
- "idiom" : "mac",
- "scale" : "2x",
- "size" : "32x32"
- },
- {
- "idiom" : "mac",
- "scale" : "1x",
- "size" : "128x128"
- },
- {
- "idiom" : "mac",
- "scale" : "2x",
- "size" : "128x128"
- },
- {
- "idiom" : "mac",
- "scale" : "1x",
- "size" : "256x256"
- },
- {
- "idiom" : "mac",
- "scale" : "2x",
- "size" : "256x256"
- },
- {
- "idiom" : "mac",
- "scale" : "1x",
- "size" : "512x512"
- },
- {
- "idiom" : "mac",
- "scale" : "2x",
- "size" : "512x512"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/EmotionMuseum/ContentView.swift b/EmotionMuseum/ContentView.swift
deleted file mode 100644
index 02b0075..0000000
--- a/EmotionMuseum/ContentView.swift
+++ /dev/null
@@ -1,88 +0,0 @@
-//
-// ContentView.swift
-// EmotionMuseum
-//
-// Created by 华中敏 on 2025/5/26.
-//
-
-import SwiftUI
-import CoreData
-
-struct ContentView: View {
- @Environment(\.managedObjectContext) private var viewContext
-
- @FetchRequest(
- sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
- animation: .default)
- private var items: FetchedResults-
-
- var body: some View {
- NavigationView {
- List {
- ForEach(items) { item in
- NavigationLink {
- Text("Item at \(item.timestamp!, formatter: itemFormatter)")
- } label: {
- Text(item.timestamp!, formatter: itemFormatter)
- }
- }
- .onDelete(perform: deleteItems)
- }
- .toolbar {
-#if os(iOS)
- ToolbarItem(placement: .navigationBarTrailing) {
- EditButton()
- }
-#endif
- ToolbarItem {
- Button(action: addItem) {
- Label("Add Item", systemImage: "plus")
- }
- }
- }
- Text("Select an item")
- }
- }
-
- private func addItem() {
- withAnimation {
- let newItem = Item(context: viewContext)
- newItem.timestamp = Date()
-
- do {
- try viewContext.save()
- } catch {
- // Replace this implementation with code to handle the error appropriately.
- // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
- let nsError = error as NSError
- fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
- }
- }
- }
-
- private func deleteItems(offsets: IndexSet) {
- withAnimation {
- offsets.map { items[$0] }.forEach(viewContext.delete)
-
- do {
- try viewContext.save()
- } catch {
- // Replace this implementation with code to handle the error appropriately.
- // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
- let nsError = error as NSError
- fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
- }
- }
- }
-}
-
-private let itemFormatter: DateFormatter = {
- let formatter = DateFormatter()
- formatter.dateStyle = .short
- formatter.timeStyle = .medium
- return formatter
-}()
-
-#Preview {
- ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
-}
diff --git a/EmotionMuseum.xcodeproj/project.pbxproj b/EmotionMuseum/EmotionMuseum.xcodeproj/project.pbxproj
similarity index 60%
rename from EmotionMuseum.xcodeproj/project.pbxproj
rename to EmotionMuseum/EmotionMuseum.xcodeproj/project.pbxproj
index 3460ec3..38fdd5e 100644
--- a/EmotionMuseum.xcodeproj/project.pbxproj
+++ b/EmotionMuseum/EmotionMuseum.xcodeproj/project.pbxproj
@@ -7,40 +7,40 @@
objects = {
/* Begin PBXContainerItemProxy section */
- 2F1ADEC92DE4903B0029490F /* PBXContainerItemProxy */ = {
+ 2FB3451A2DFBE273001A8A67 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
- containerPortal = 2F1ADEAD2DE490370029490F /* Project object */;
+ containerPortal = 2FB344FF2DFBE270001A8A67 /* Project object */;
proxyType = 1;
- remoteGlobalIDString = 2F1ADEB42DE490370029490F;
+ remoteGlobalIDString = 2FB345062DFBE270001A8A67;
remoteInfo = EmotionMuseum;
};
- 2F1ADED32DE4903B0029490F /* PBXContainerItemProxy */ = {
+ 2FB345242DFBE273001A8A67 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
- containerPortal = 2F1ADEAD2DE490370029490F /* Project object */;
+ containerPortal = 2FB344FF2DFBE270001A8A67 /* Project object */;
proxyType = 1;
- remoteGlobalIDString = 2F1ADEB42DE490370029490F;
+ remoteGlobalIDString = 2FB345062DFBE270001A8A67;
remoteInfo = EmotionMuseum;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
- 2F1ADEB52DE490370029490F /* EmotionMuseum.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = EmotionMuseum.app; sourceTree = BUILT_PRODUCTS_DIR; };
- 2F1ADEC82DE4903B0029490F /* EmotionMuseumTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EmotionMuseumTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
- 2F1ADED22DE4903B0029490F /* EmotionMuseumUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EmotionMuseumUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 2FB345072DFBE270001A8A67 /* EmotionMuseum.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = EmotionMuseum.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 2FB345192DFBE273001A8A67 /* EmotionMuseumTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EmotionMuseumTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 2FB345232DFBE273001A8A67 /* EmotionMuseumUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EmotionMuseumUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
- 2F1ADEB72DE490370029490F /* EmotionMuseum */ = {
+ 2FB345092DFBE270001A8A67 /* EmotionMuseum */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = EmotionMuseum;
sourceTree = "";
};
- 2F1ADECB2DE4903B0029490F /* EmotionMuseumTests */ = {
+ 2FB3451C2DFBE273001A8A67 /* EmotionMuseumTests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = EmotionMuseumTests;
sourceTree = "";
};
- 2F1ADED52DE4903B0029490F /* EmotionMuseumUITests */ = {
+ 2FB345262DFBE273001A8A67 /* EmotionMuseumUITests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = EmotionMuseumUITests;
sourceTree = "";
@@ -48,21 +48,21 @@
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
- 2F1ADEB22DE490370029490F /* Frameworks */ = {
+ 2FB345042DFBE270001A8A67 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
- 2F1ADEC52DE4903B0029490F /* Frameworks */ = {
+ 2FB345162DFBE273001A8A67 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
- 2F1ADECF2DE4903B0029490F /* Frameworks */ = {
+ 2FB345202DFBE273001A8A67 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -72,22 +72,22 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
- 2F1ADEAC2DE490370029490F = {
+ 2FB344FE2DFBE270001A8A67 = {
isa = PBXGroup;
children = (
- 2F1ADEB72DE490370029490F /* EmotionMuseum */,
- 2F1ADECB2DE4903B0029490F /* EmotionMuseumTests */,
- 2F1ADED52DE4903B0029490F /* EmotionMuseumUITests */,
- 2F1ADEB62DE490370029490F /* Products */,
+ 2FB345092DFBE270001A8A67 /* EmotionMuseum */,
+ 2FB3451C2DFBE273001A8A67 /* EmotionMuseumTests */,
+ 2FB345262DFBE273001A8A67 /* EmotionMuseumUITests */,
+ 2FB345082DFBE270001A8A67 /* Products */,
);
sourceTree = "";
};
- 2F1ADEB62DE490370029490F /* Products */ = {
+ 2FB345082DFBE270001A8A67 /* Products */ = {
isa = PBXGroup;
children = (
- 2F1ADEB52DE490370029490F /* EmotionMuseum.app */,
- 2F1ADEC82DE4903B0029490F /* EmotionMuseumTests.xctest */,
- 2F1ADED22DE4903B0029490F /* EmotionMuseumUITests.xctest */,
+ 2FB345072DFBE270001A8A67 /* EmotionMuseum.app */,
+ 2FB345192DFBE273001A8A67 /* EmotionMuseumTests.xctest */,
+ 2FB345232DFBE273001A8A67 /* EmotionMuseumUITests.xctest */,
);
name = Products;
sourceTree = "";
@@ -95,134 +95,134 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
- 2F1ADEB42DE490370029490F /* EmotionMuseum */ = {
+ 2FB345062DFBE270001A8A67 /* EmotionMuseum */ = {
isa = PBXNativeTarget;
- buildConfigurationList = 2F1ADEDC2DE4903B0029490F /* Build configuration list for PBXNativeTarget "EmotionMuseum" */;
+ buildConfigurationList = 2FB3452D2DFBE273001A8A67 /* Build configuration list for PBXNativeTarget "EmotionMuseum" */;
buildPhases = (
- 2F1ADEB12DE490370029490F /* Sources */,
- 2F1ADEB22DE490370029490F /* Frameworks */,
- 2F1ADEB32DE490370029490F /* Resources */,
+ 2FB345032DFBE270001A8A67 /* Sources */,
+ 2FB345042DFBE270001A8A67 /* Frameworks */,
+ 2FB345052DFBE270001A8A67 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
- 2F1ADEB72DE490370029490F /* EmotionMuseum */,
+ 2FB345092DFBE270001A8A67 /* EmotionMuseum */,
);
name = EmotionMuseum;
packageProductDependencies = (
);
productName = EmotionMuseum;
- productReference = 2F1ADEB52DE490370029490F /* EmotionMuseum.app */;
+ productReference = 2FB345072DFBE270001A8A67 /* EmotionMuseum.app */;
productType = "com.apple.product-type.application";
};
- 2F1ADEC72DE4903B0029490F /* EmotionMuseumTests */ = {
+ 2FB345182DFBE273001A8A67 /* EmotionMuseumTests */ = {
isa = PBXNativeTarget;
- buildConfigurationList = 2F1ADEDF2DE4903B0029490F /* Build configuration list for PBXNativeTarget "EmotionMuseumTests" */;
+ buildConfigurationList = 2FB345302DFBE273001A8A67 /* Build configuration list for PBXNativeTarget "EmotionMuseumTests" */;
buildPhases = (
- 2F1ADEC42DE4903B0029490F /* Sources */,
- 2F1ADEC52DE4903B0029490F /* Frameworks */,
- 2F1ADEC62DE4903B0029490F /* Resources */,
+ 2FB345152DFBE273001A8A67 /* Sources */,
+ 2FB345162DFBE273001A8A67 /* Frameworks */,
+ 2FB345172DFBE273001A8A67 /* Resources */,
);
buildRules = (
);
dependencies = (
- 2F1ADECA2DE4903B0029490F /* PBXTargetDependency */,
+ 2FB3451B2DFBE273001A8A67 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
- 2F1ADECB2DE4903B0029490F /* EmotionMuseumTests */,
+ 2FB3451C2DFBE273001A8A67 /* EmotionMuseumTests */,
);
name = EmotionMuseumTests;
packageProductDependencies = (
);
productName = EmotionMuseumTests;
- productReference = 2F1ADEC82DE4903B0029490F /* EmotionMuseumTests.xctest */;
+ productReference = 2FB345192DFBE273001A8A67 /* EmotionMuseumTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
- 2F1ADED12DE4903B0029490F /* EmotionMuseumUITests */ = {
+ 2FB345222DFBE273001A8A67 /* EmotionMuseumUITests */ = {
isa = PBXNativeTarget;
- buildConfigurationList = 2F1ADEE22DE4903B0029490F /* Build configuration list for PBXNativeTarget "EmotionMuseumUITests" */;
+ buildConfigurationList = 2FB345332DFBE273001A8A67 /* Build configuration list for PBXNativeTarget "EmotionMuseumUITests" */;
buildPhases = (
- 2F1ADECE2DE4903B0029490F /* Sources */,
- 2F1ADECF2DE4903B0029490F /* Frameworks */,
- 2F1ADED02DE4903B0029490F /* Resources */,
+ 2FB3451F2DFBE273001A8A67 /* Sources */,
+ 2FB345202DFBE273001A8A67 /* Frameworks */,
+ 2FB345212DFBE273001A8A67 /* Resources */,
);
buildRules = (
);
dependencies = (
- 2F1ADED42DE4903B0029490F /* PBXTargetDependency */,
+ 2FB345252DFBE273001A8A67 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
- 2F1ADED52DE4903B0029490F /* EmotionMuseumUITests */,
+ 2FB345262DFBE273001A8A67 /* EmotionMuseumUITests */,
);
name = EmotionMuseumUITests;
packageProductDependencies = (
);
productName = EmotionMuseumUITests;
- productReference = 2F1ADED22DE4903B0029490F /* EmotionMuseumUITests.xctest */;
+ productReference = 2FB345232DFBE273001A8A67 /* EmotionMuseumUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
- 2F1ADEAD2DE490370029490F /* Project object */ = {
+ 2FB344FF2DFBE270001A8A67 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
- LastSwiftUpdateCheck = 1630;
- LastUpgradeCheck = 1630;
+ LastSwiftUpdateCheck = 1640;
+ LastUpgradeCheck = 1640;
TargetAttributes = {
- 2F1ADEB42DE490370029490F = {
- CreatedOnToolsVersion = 16.3;
+ 2FB345062DFBE270001A8A67 = {
+ CreatedOnToolsVersion = 16.4;
};
- 2F1ADEC72DE4903B0029490F = {
- CreatedOnToolsVersion = 16.3;
- TestTargetID = 2F1ADEB42DE490370029490F;
+ 2FB345182DFBE273001A8A67 = {
+ CreatedOnToolsVersion = 16.4;
+ TestTargetID = 2FB345062DFBE270001A8A67;
};
- 2F1ADED12DE4903B0029490F = {
- CreatedOnToolsVersion = 16.3;
- TestTargetID = 2F1ADEB42DE490370029490F;
+ 2FB345222DFBE273001A8A67 = {
+ CreatedOnToolsVersion = 16.4;
+ TestTargetID = 2FB345062DFBE270001A8A67;
};
};
};
- buildConfigurationList = 2F1ADEB02DE490370029490F /* Build configuration list for PBXProject "EmotionMuseum" */;
+ buildConfigurationList = 2FB345022DFBE270001A8A67 /* Build configuration list for PBXProject "EmotionMuseum" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
- mainGroup = 2F1ADEAC2DE490370029490F;
+ mainGroup = 2FB344FE2DFBE270001A8A67;
minimizedProjectReferenceProxies = 1;
preferredProjectObjectVersion = 77;
- productRefGroup = 2F1ADEB62DE490370029490F /* Products */;
+ productRefGroup = 2FB345082DFBE270001A8A67 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
- 2F1ADEB42DE490370029490F /* EmotionMuseum */,
- 2F1ADEC72DE4903B0029490F /* EmotionMuseumTests */,
- 2F1ADED12DE4903B0029490F /* EmotionMuseumUITests */,
+ 2FB345062DFBE270001A8A67 /* EmotionMuseum */,
+ 2FB345182DFBE273001A8A67 /* EmotionMuseumTests */,
+ 2FB345222DFBE273001A8A67 /* EmotionMuseumUITests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
- 2F1ADEB32DE490370029490F /* Resources */ = {
+ 2FB345052DFBE270001A8A67 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
- 2F1ADEC62DE4903B0029490F /* Resources */ = {
+ 2FB345172DFBE273001A8A67 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
- 2F1ADED02DE4903B0029490F /* Resources */ = {
+ 2FB345212DFBE273001A8A67 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -232,21 +232,21 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
- 2F1ADEB12DE490370029490F /* Sources */ = {
+ 2FB345032DFBE270001A8A67 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
- 2F1ADEC42DE4903B0029490F /* Sources */ = {
+ 2FB345152DFBE273001A8A67 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
- 2F1ADECE2DE4903B0029490F /* Sources */ = {
+ 2FB3451F2DFBE273001A8A67 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -256,20 +256,20 @@
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
- 2F1ADECA2DE4903B0029490F /* PBXTargetDependency */ = {
+ 2FB3451B2DFBE273001A8A67 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
- target = 2F1ADEB42DE490370029490F /* EmotionMuseum */;
- targetProxy = 2F1ADEC92DE4903B0029490F /* PBXContainerItemProxy */;
+ target = 2FB345062DFBE270001A8A67 /* EmotionMuseum */;
+ targetProxy = 2FB3451A2DFBE273001A8A67 /* PBXContainerItemProxy */;
};
- 2F1ADED42DE4903B0029490F /* PBXTargetDependency */ = {
+ 2FB345252DFBE273001A8A67 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
- target = 2F1ADEB42DE490370029490F /* EmotionMuseum */;
- targetProxy = 2F1ADED32DE4903B0029490F /* PBXContainerItemProxy */;
+ target = 2FB345062DFBE270001A8A67 /* EmotionMuseum */;
+ targetProxy = 2FB345242DFBE273001A8A67 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
- 2F1ADEDA2DE4903B0029490F /* Debug */ = {
+ 2FB3452B2DFBE273001A8A67 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
@@ -322,16 +322,18 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 18.5;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
- 2F1ADEDB2DE4903B0029490F /* Release */ = {
+ 2FB3452C2DFBE273001A8A67 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
@@ -378,221 +380,196 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 18.5;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
+ SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
+ VALIDATE_PRODUCT = YES;
};
name = Release;
};
- 2F1ADEDD2DE4903B0029490F /* Debug */ = {
+ 2FB3452E2DFBE273001A8A67 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
- CODE_SIGN_ENTITLEMENTS = EmotionMuseum/EmotionMuseum.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = JA6T4PANZM;
- ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
- "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
- "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
- "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
- "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
- "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
- "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
- "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
- "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
+ INFOPLIST_KEY_LSApplicationQueriesSchemes = iosamap;
+ INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "需要使用您的位置信息来为您提供地图服务";
+ INFOPLIST_KEY_NSLocationAlwaysUsageDescription = "需要使用您的位置信息来为您提供地图服务";
+ INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "需要使用您的位置信息来为您提供地图服务";
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
- IPHONEOS_DEPLOYMENT_TARGET = 18.4;
- LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
- "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
- MACOSX_DEPLOYMENT_TARGET = 15.4;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.peanut.EmotionMuseum;
+ PRODUCT_BUNDLE_IDENTIFIER = com.dolphin.EmotionMuseum;
PRODUCT_NAME = "$(TARGET_NAME)";
- REGISTER_APP_GROUPS = YES;
- SDKROOT = auto;
- SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2,7";
- XROS_DEPLOYMENT_TARGET = 2.4;
+ TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
- 2F1ADEDE2DE4903B0029490F /* Release */ = {
+ 2FB3452F2DFBE273001A8A67 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
- CODE_SIGN_ENTITLEMENTS = EmotionMuseum/EmotionMuseum.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = JA6T4PANZM;
- ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
- "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
- "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
- "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
- "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
- "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
- "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
- "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
- "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
+ INFOPLIST_KEY_LSApplicationQueriesSchemes = iosamap;
+ INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "需要使用您的位置信息来为您提供地图服务";
+ INFOPLIST_KEY_NSLocationAlwaysUsageDescription = "需要使用您的位置信息来为您提供地图服务";
+ INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "需要使用您的位置信息来为您提供地图服务";
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
- IPHONEOS_DEPLOYMENT_TARGET = 18.4;
- LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
- "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
- MACOSX_DEPLOYMENT_TARGET = 15.4;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.peanut.EmotionMuseum;
+ PRODUCT_BUNDLE_IDENTIFIER = com.dolphin.EmotionMuseum;
PRODUCT_NAME = "$(TARGET_NAME)";
- REGISTER_APP_GROUPS = YES;
- SDKROOT = auto;
- SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2,7";
- XROS_DEPLOYMENT_TARGET = 2.4;
+ TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
- 2F1ADEE02DE4903B0029490F /* Debug */ = {
+ 2FB345312DFBE273001A8A67 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = JA6T4PANZM;
- GENERATE_INFOPLIST_FILE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 18.4;
- MACOSX_DEPLOYMENT_TARGET = 15.4;
+ GENERATE_INFOPLIST_FILE = NO;
+ INFOPLIST_FILE = EmotionMuseumTests/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 18.5;
MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.peanut.EmotionMuseumTests;
+ PRODUCT_BUNDLE_IDENTIFIER = com.dolphin.EmotionMuseumTests;
PRODUCT_NAME = "$(TARGET_NAME)";
- SDKROOT = auto;
- SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2,7";
+ TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/EmotionMuseum.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/EmotionMuseum";
- XROS_DEPLOYMENT_TARGET = 2.4;
};
name = Debug;
};
- 2F1ADEE12DE4903B0029490F /* Release */ = {
+ 2FB345322DFBE273001A8A67 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = JA6T4PANZM;
- GENERATE_INFOPLIST_FILE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 18.4;
- MACOSX_DEPLOYMENT_TARGET = 15.4;
+ GENERATE_INFOPLIST_FILE = NO;
+ INFOPLIST_FILE = EmotionMuseumTests/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 18.5;
MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.peanut.EmotionMuseumTests;
+ PRODUCT_BUNDLE_IDENTIFIER = com.dolphin.EmotionMuseumTests;
PRODUCT_NAME = "$(TARGET_NAME)";
- SDKROOT = auto;
- SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2,7";
+ TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/EmotionMuseum.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/EmotionMuseum";
- XROS_DEPLOYMENT_TARGET = 2.4;
};
name = Release;
};
- 2F1ADEE32DE4903B0029490F /* Debug */ = {
+ 2FB345342DFBE273001A8A67 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = JA6T4PANZM;
- GENERATE_INFOPLIST_FILE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 18.4;
- MACOSX_DEPLOYMENT_TARGET = 15.4;
+ GENERATE_INFOPLIST_FILE = NO;
+ INFOPLIST_FILE = EmotionMuseumUITests/Info.plist;
MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.peanut.EmotionMuseumUITests;
+ PRODUCT_BUNDLE_IDENTIFIER = com.dolphin.EmotionMuseumUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
- SDKROOT = auto;
- SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2,7";
+ TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = EmotionMuseum;
- XROS_DEPLOYMENT_TARGET = 2.4;
};
name = Debug;
};
- 2F1ADEE42DE4903B0029490F /* Release */ = {
+ 2FB345352DFBE273001A8A67 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = JA6T4PANZM;
- GENERATE_INFOPLIST_FILE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 18.4;
- MACOSX_DEPLOYMENT_TARGET = 15.4;
+ GENERATE_INFOPLIST_FILE = NO;
+ INFOPLIST_FILE = EmotionMuseumUITests/Info.plist;
MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.peanut.EmotionMuseumUITests;
+ PRODUCT_BUNDLE_IDENTIFIER = com.dolphin.EmotionMuseumUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
- SDKROOT = auto;
- SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2,7";
+ TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = EmotionMuseum;
- XROS_DEPLOYMENT_TARGET = 2.4;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
- 2F1ADEB02DE490370029490F /* Build configuration list for PBXProject "EmotionMuseum" */ = {
+ 2FB345022DFBE270001A8A67 /* Build configuration list for PBXProject "EmotionMuseum" */ = {
isa = XCConfigurationList;
buildConfigurations = (
- 2F1ADEDA2DE4903B0029490F /* Debug */,
- 2F1ADEDB2DE4903B0029490F /* Release */,
+ 2FB3452B2DFBE273001A8A67 /* Debug */,
+ 2FB3452C2DFBE273001A8A67 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
- 2F1ADEDC2DE4903B0029490F /* Build configuration list for PBXNativeTarget "EmotionMuseum" */ = {
+ 2FB3452D2DFBE273001A8A67 /* Build configuration list for PBXNativeTarget "EmotionMuseum" */ = {
isa = XCConfigurationList;
buildConfigurations = (
- 2F1ADEDD2DE4903B0029490F /* Debug */,
- 2F1ADEDE2DE4903B0029490F /* Release */,
+ 2FB3452E2DFBE273001A8A67 /* Debug */,
+ 2FB3452F2DFBE273001A8A67 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
- 2F1ADEDF2DE4903B0029490F /* Build configuration list for PBXNativeTarget "EmotionMuseumTests" */ = {
+ 2FB345302DFBE273001A8A67 /* Build configuration list for PBXNativeTarget "EmotionMuseumTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
- 2F1ADEE02DE4903B0029490F /* Debug */,
- 2F1ADEE12DE4903B0029490F /* Release */,
+ 2FB345312DFBE273001A8A67 /* Debug */,
+ 2FB345322DFBE273001A8A67 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
- 2F1ADEE22DE4903B0029490F /* Build configuration list for PBXNativeTarget "EmotionMuseumUITests" */ = {
+ 2FB345332DFBE273001A8A67 /* Build configuration list for PBXNativeTarget "EmotionMuseumUITests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
- 2F1ADEE32DE4903B0029490F /* Debug */,
- 2F1ADEE42DE4903B0029490F /* Release */,
+ 2FB345342DFBE273001A8A67 /* Debug */,
+ 2FB345352DFBE273001A8A67 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
- rootObject = 2F1ADEAD2DE490370029490F /* Project object */;
+ rootObject = 2FB344FF2DFBE270001A8A67 /* Project object */;
}
diff --git a/EmotionMuseum.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/EmotionMuseum/EmotionMuseum.xcodeproj/project.xcworkspace/contents.xcworkspacedata
similarity index 100%
rename from EmotionMuseum.xcodeproj/project.xcworkspace/contents.xcworkspacedata
rename to EmotionMuseum/EmotionMuseum.xcodeproj/project.xcworkspace/contents.xcworkspacedata
diff --git a/EmotionMuseum/EmotionMuseum.entitlements b/EmotionMuseum/EmotionMuseum.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
similarity index 53%
rename from EmotionMuseum/EmotionMuseum.entitlements
rename to EmotionMuseum/EmotionMuseum.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
index f2ef3ae..0c67376 100644
--- a/EmotionMuseum/EmotionMuseum.entitlements
+++ b/EmotionMuseum/EmotionMuseum.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -1,10 +1,5 @@
-
- com.apple.security.app-sandbox
-
- com.apple.security.files.user-selected.read-only
-
-
+
diff --git a/EmotionMuseum/EmotionMuseum.xcodeproj/project.xcworkspace/xcuserdata/huazhongmin.xcuserdatad/WorkspaceSettings.xcsettings b/EmotionMuseum/EmotionMuseum.xcodeproj/project.xcworkspace/xcuserdata/huazhongmin.xcuserdatad/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..bbfef02
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum.xcodeproj/project.xcworkspace/xcuserdata/huazhongmin.xcuserdatad/WorkspaceSettings.xcsettings
@@ -0,0 +1,14 @@
+
+
+
+
+ BuildLocationStyle
+ UseAppPreferences
+ CustomBuildLocationType
+ RelativeToDerivedData
+ DerivedDataLocationStyle
+ Default
+ ShowSharedSchemesAutomaticallyEnabled
+
+
+
diff --git a/EmotionMuseum.xcodeproj/xcuserdata/huazhongmin.xcuserdatad/xcschemes/xcschememanagement.plist b/EmotionMuseum/EmotionMuseum.xcodeproj/xcuserdata/huazhongmin.xcuserdatad/xcschemes/xcschememanagement.plist
similarity index 100%
rename from EmotionMuseum.xcodeproj/xcuserdata/huazhongmin.xcuserdatad/xcschemes/xcschememanagement.plist
rename to EmotionMuseum/EmotionMuseum.xcodeproj/xcuserdata/huazhongmin.xcuserdatad/xcschemes/xcschememanagement.plist
diff --git a/EmotionMuseum/EmotionMuseum/Assets.xcassets/AccentColor.colorset/Contents.json b/EmotionMuseum/EmotionMuseum/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 0000000..cf2908c
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "1.000",
+ "green" : "0.573",
+ "red" : "0.000"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "1.000",
+ "green" : "0.678",
+ "red" : "0.196"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/EmotionMuseum/EmotionMuseum/Assets.xcassets/AppIcon.appiconset/Contents.json b/EmotionMuseum/EmotionMuseum/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..2305880
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,35 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "tinted"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/EmotionMuseum/EmotionMuseum/Assets.xcassets/BackgroundColor.colorset/Contents.json b/EmotionMuseum/EmotionMuseum/Assets.xcassets/BackgroundColor.colorset/Contents.json
new file mode 100644
index 0000000..67bb90d
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Assets.xcassets/BackgroundColor.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "1.000",
+ "green" : "1.000",
+ "red" : "1.000"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.118",
+ "green" : "0.118",
+ "red" : "0.118"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
\ No newline at end of file
diff --git a/EmotionMuseum/EmotionMuseum/Assets.xcassets/BorderColor.colorset/Contents.json b/EmotionMuseum/EmotionMuseum/Assets.xcassets/BorderColor.colorset/Contents.json
new file mode 100644
index 0000000..ac0b1eb
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Assets.xcassets/BorderColor.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.700",
+ "green" : "0.700",
+ "red" : "0.700"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.600",
+ "green" : "0.600",
+ "red" : "0.600"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/EmotionMuseum/EmotionMuseum/Assets.xcassets/CardBackground.colorset/Contents.json b/EmotionMuseum/EmotionMuseum/Assets.xcassets/CardBackground.colorset/Contents.json
new file mode 100644
index 0000000..65cc941
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Assets.xcassets/CardBackground.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "1.000",
+ "green" : "1.000",
+ "red" : "1.000"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.196",
+ "green" : "0.196",
+ "red" : "0.196"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
\ No newline at end of file
diff --git a/EmotionMuseum/Assets.xcassets/Contents.json b/EmotionMuseum/EmotionMuseum/Assets.xcassets/Contents.json
similarity index 100%
rename from EmotionMuseum/Assets.xcassets/Contents.json
rename to EmotionMuseum/EmotionMuseum/Assets.xcassets/Contents.json
diff --git a/EmotionMuseum/EmotionMuseum/Assets.xcassets/DividerColor.colorset/Contents.json b/EmotionMuseum/EmotionMuseum/Assets.xcassets/DividerColor.colorset/Contents.json
new file mode 100644
index 0000000..9e7fef4
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Assets.xcassets/DividerColor.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.788",
+ "green" : "0.780",
+ "red" : "0.776"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.329",
+ "green" : "0.310",
+ "red" : "0.298"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/EmotionMuseum/EmotionMuseum/Assets.xcassets/ErrorColor.colorset/Contents.json b/EmotionMuseum/EmotionMuseum/Assets.xcassets/ErrorColor.colorset/Contents.json
new file mode 100644
index 0000000..d74a7cb
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Assets.xcassets/ErrorColor.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.300",
+ "green" : "0.200",
+ "red" : "0.900"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.400",
+ "green" : "0.300",
+ "red" : "1.000"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/EmotionMuseum/EmotionMuseum/Assets.xcassets/PrimaryColor.colorset/Contents.json b/EmotionMuseum/EmotionMuseum/Assets.xcassets/PrimaryColor.colorset/Contents.json
new file mode 100644
index 0000000..f533814
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Assets.xcassets/PrimaryColor.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.929",
+ "green" : "0.569",
+ "red" : "0.416"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.929",
+ "green" : "0.569",
+ "red" : "0.416"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
\ No newline at end of file
diff --git a/EmotionMuseum/EmotionMuseum/Assets.xcassets/PrimaryText.colorset/Contents.json b/EmotionMuseum/EmotionMuseum/Assets.xcassets/PrimaryText.colorset/Contents.json
new file mode 100644
index 0000000..ff1f1ac
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Assets.xcassets/PrimaryText.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.000",
+ "green" : "0.000",
+ "red" : "0.000"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "1.000",
+ "green" : "1.000",
+ "red" : "1.000"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
\ No newline at end of file
diff --git a/EmotionMuseum/EmotionMuseum/Assets.xcassets/SecondaryColor.colorset/Contents.json b/EmotionMuseum/EmotionMuseum/Assets.xcassets/SecondaryColor.colorset/Contents.json
new file mode 100644
index 0000000..885ed56
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Assets.xcassets/SecondaryColor.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.980",
+ "green" : "0.780",
+ "red" : "0.310"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.780",
+ "green" : "0.580",
+ "red" : "0.210"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/EmotionMuseum/EmotionMuseum/Assets.xcassets/SecondaryText.colorset/Contents.json b/EmotionMuseum/EmotionMuseum/Assets.xcassets/SecondaryText.colorset/Contents.json
new file mode 100644
index 0000000..9419960
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Assets.xcassets/SecondaryText.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.600",
+ "green" : "0.600",
+ "red" : "0.600"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.780",
+ "green" : "0.780",
+ "red" : "0.780"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/EmotionMuseum/EmotionMuseum/Assets.xcassets/SkeletonColor.colorset/Contents.json b/EmotionMuseum/EmotionMuseum/Assets.xcassets/SkeletonColor.colorset/Contents.json
new file mode 100644
index 0000000..3eeea39
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Assets.xcassets/SkeletonColor.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.925",
+ "green" : "0.925",
+ "red" : "0.925"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.294",
+ "green" : "0.294",
+ "red" : "0.294"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
\ No newline at end of file
diff --git a/EmotionMuseum/EmotionMuseum/Assets.xcassets/SkeletonHighlight.colorset/Contents.json b/EmotionMuseum/EmotionMuseum/Assets.xcassets/SkeletonHighlight.colorset/Contents.json
new file mode 100644
index 0000000..545bc7a
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Assets.xcassets/SkeletonHighlight.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.980",
+ "green" : "0.980",
+ "red" : "0.980"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.392",
+ "green" : "0.392",
+ "red" : "0.392"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/EmotionMuseum/EmotionMuseum/Assets.xcassets/SuccessColor.colorset/Contents.json b/EmotionMuseum/EmotionMuseum/Assets.xcassets/SuccessColor.colorset/Contents.json
new file mode 100644
index 0000000..47441ec
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Assets.xcassets/SuccessColor.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.380",
+ "green" : "0.780",
+ "red" : "0.200"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.480",
+ "green" : "0.880",
+ "red" : "0.300"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/EmotionMuseum/EmotionMuseum/Assets.xcassets/SurfaceBackground.colorset/Contents.json b/EmotionMuseum/EmotionMuseum/Assets.xcassets/SurfaceBackground.colorset/Contents.json
new file mode 100644
index 0000000..c72e2c4
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Assets.xcassets/SurfaceBackground.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.961",
+ "green" : "0.961",
+ "red" : "0.961"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.173",
+ "green" : "0.169",
+ "red" : "0.165"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/EmotionMuseum/EmotionMuseum/Assets.xcassets/TertiaryText.colorset/Contents.json b/EmotionMuseum/EmotionMuseum/Assets.xcassets/TertiaryText.colorset/Contents.json
new file mode 100644
index 0000000..ac0b1eb
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Assets.xcassets/TertiaryText.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.700",
+ "green" : "0.700",
+ "red" : "0.700"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.600",
+ "green" : "0.600",
+ "red" : "0.600"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/EmotionMuseum/EmotionMuseum/Assets.xcassets/WarningColor.colorset/Contents.json b/EmotionMuseum/EmotionMuseum/Assets.xcassets/WarningColor.colorset/Contents.json
new file mode 100644
index 0000000..274039c
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Assets.xcassets/WarningColor.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.200",
+ "green" : "0.700",
+ "red" : "1.000"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.300",
+ "green" : "0.800",
+ "red" : "1.000"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/EmotionMuseum/EmotionMuseum/ContentView.swift b/EmotionMuseum/EmotionMuseum/ContentView.swift
new file mode 100644
index 0000000..653d7e3
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/ContentView.swift
@@ -0,0 +1,75 @@
+//
+// ContentView.swift
+// EmotionMuseum
+//
+// Created by 华中敏 on 2025/6/13.
+//
+
+import SwiftUI
+import CoreData
+
+struct ContentView: View {
+ @EnvironmentObject var themeManager: ThemeManager
+ @EnvironmentObject var mockDataManager: MockDataManager
+ @EnvironmentObject var navigationManager: NavigationManager
+ @Environment(\.managedObjectContext) private var viewContext
+
+ var body: some View {
+ ZStack {
+ // 主要内容
+ TabView(selection: $navigationManager.currentTab) {
+ RecordView()
+ .tabItem {
+ Image(systemName: "heart.text.square")
+ Text("记录")
+ }
+ .tag(MainTab.record)
+
+ GrowthView()
+ .tabItem {
+ Image(systemName: "leaf.arrow.circlepath")
+ Text("治愈")
+ }
+ .tag(MainTab.growth)
+
+ ExploreView()
+ .tabItem {
+ Image(systemName: "map")
+ Text("探索")
+ }
+ .tag(MainTab.explore)
+
+ UniverseView()
+ .tabItem {
+ Image(systemName: "person.circle")
+ Text("我的")
+ }
+ .tag(MainTab.insight)
+ }
+ .accentColor(Color("AccentColor"))
+
+ // 全局加载覆盖层
+ if navigationManager.isLoading {
+ LoadingOverlay(message: navigationManager.loadingMessage)
+ }
+ }
+ .preferredColorScheme(themeManager.isDarkMode ? .dark : .light)
+ .onAppear {
+ // 初始化应用状态
+ setupInitialState()
+ }
+ }
+
+ private func setupInitialState() {
+ // 设置初始状态
+ navigationManager.currentTab = .record
+ }
+}
+
+#Preview {
+ ContentView()
+ .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
+ .environmentObject(ThemeManager())
+ .environmentObject(MockDataManager.shared)
+ .environmentObject(NavigationManager())
+}
diff --git a/EmotionMuseum/EmotionMuseum.xcdatamodeld/.xccurrentversion b/EmotionMuseum/EmotionMuseum/EmotionMuseum.xcdatamodeld/.xccurrentversion
similarity index 100%
rename from EmotionMuseum/EmotionMuseum.xcdatamodeld/.xccurrentversion
rename to EmotionMuseum/EmotionMuseum/EmotionMuseum.xcdatamodeld/.xccurrentversion
diff --git a/EmotionMuseum/EmotionMuseum.xcdatamodeld/EmotionMuseum.xcdatamodel/contents b/EmotionMuseum/EmotionMuseum/EmotionMuseum.xcdatamodeld/EmotionMuseum.xcdatamodel/contents
similarity index 100%
rename from EmotionMuseum/EmotionMuseum.xcdatamodeld/EmotionMuseum.xcdatamodel/contents
rename to EmotionMuseum/EmotionMuseum/EmotionMuseum.xcdatamodeld/EmotionMuseum.xcdatamodel/contents
diff --git a/EmotionMuseum/EmotionMuseum/EmotionMuseumApp.swift b/EmotionMuseum/EmotionMuseum/EmotionMuseumApp.swift
new file mode 100644
index 0000000..f3dd733
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/EmotionMuseumApp.swift
@@ -0,0 +1,31 @@
+//
+// EmotionMuseumApp.swift
+// EmotionMuseum
+//
+// Created by 华中敏 on 2025/6/13.
+//
+
+import SwiftUI
+
+@main
+struct EmotionMuseumApp: App {
+ let persistenceController = PersistenceController.shared
+ @StateObject private var navigationManager = NavigationManager()
+ @StateObject private var themeManager = ThemeManager()
+ @StateObject private var mockDataManager = MockDataManager.shared
+
+ init() {
+ // 初始化高德地图SDK
+ MapManager.shared.configure()
+ }
+
+ var body: some Scene {
+ WindowGroup {
+ ContentView()
+ .environment(\.managedObjectContext, persistenceController.container.viewContext)
+ .environmentObject(navigationManager)
+ .environmentObject(themeManager)
+ .environmentObject(mockDataManager)
+ }
+ }
+}
diff --git a/EmotionMuseum/EmotionMuseum/Models/ChakraType.swift b/EmotionMuseum/EmotionMuseum/Models/ChakraType.swift
new file mode 100644
index 0000000..26fadfd
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Models/ChakraType.swift
@@ -0,0 +1,189 @@
+//
+// ChakraType.swift
+// EmotionMuseum
+//
+// Created by 华中敏 on 2025/6/13.
+//
+
+import SwiftUI
+
+enum ChakraType: String, CaseIterable {
+ case root = "海底轮"
+ case sacral = "脐轮"
+ case solarPlexus = "太阳轮"
+ case heart = "心轮"
+ case throat = "喉轮"
+ case thirdEye = "眉心轮"
+ case crown = "顶轮"
+
+ var color: Color {
+ switch self {
+ case .root:
+ return .red
+ case .sacral:
+ return .orange
+ case .solarPlexus:
+ return .yellow
+ case .heart:
+ return .green
+ case .throat:
+ return .blue
+ case .thirdEye:
+ return .indigo
+ case .crown:
+ return .purple
+ }
+ }
+
+ var position: CGPoint {
+ switch self {
+ case .root:
+ return CGPoint(x: 0.5, y: 0.9)
+ case .sacral:
+ return CGPoint(x: 0.5, y: 0.8)
+ case .solarPlexus:
+ return CGPoint(x: 0.5, y: 0.65)
+ case .heart:
+ return CGPoint(x: 0.5, y: 0.5)
+ case .throat:
+ return CGPoint(x: 0.5, y: 0.35)
+ case .thirdEye:
+ return CGPoint(x: 0.5, y: 0.2)
+ case .crown:
+ return CGPoint(x: 0.5, y: 0.05)
+ }
+ }
+
+ var description: String {
+ switch self {
+ case .root:
+ return "安全感、稳定性、生存本能"
+ case .sacral:
+ return "创造力、性能量、情感流动"
+ case .solarPlexus:
+ return "个人力量、自信、意志力"
+ case .heart:
+ return "爱、同情心、人际关系"
+ case .throat:
+ return "沟通、表达、真实性"
+ case .thirdEye:
+ return "直觉、洞察力、智慧"
+ case .crown:
+ return "灵性连接、觉知、超越"
+ }
+ }
+
+ var audioFileName: String {
+ switch self {
+ case .root:
+ return "root_chakra_healing"
+ case .sacral:
+ return "sacral_chakra_healing"
+ case .solarPlexus:
+ return "solar_plexus_chakra_healing"
+ case .heart:
+ return "heart_chakra_healing"
+ case .throat:
+ return "throat_chakra_healing"
+ case .thirdEye:
+ return "third_eye_chakra_healing"
+ case .crown:
+ return "crown_chakra_healing"
+ }
+ }
+
+ var frequency: String {
+ switch self {
+ case .root:
+ return "396 Hz"
+ case .sacral:
+ return "417 Hz"
+ case .solarPlexus:
+ return "528 Hz"
+ case .heart:
+ return "639 Hz"
+ case .throat:
+ return "741 Hz"
+ case .thirdEye:
+ return "852 Hz"
+ case .crown:
+ return "963 Hz"
+ }
+ }
+
+ var mantra: String {
+ switch self {
+ case .root:
+ return "LAM"
+ case .sacral:
+ return "VAM"
+ case .solarPlexus:
+ return "RAM"
+ case .heart:
+ return "YAM"
+ case .throat:
+ return "HAM"
+ case .thirdEye:
+ return "OM"
+ case .crown:
+ return "AH"
+ }
+ }
+
+ var element: String {
+ switch self {
+ case .root:
+ return "土"
+ case .sacral:
+ return "水"
+ case .solarPlexus:
+ return "火"
+ case .heart:
+ return "风"
+ case .throat:
+ return "空"
+ case .thirdEye:
+ return "光"
+ case .crown:
+ return "思想"
+ }
+ }
+
+ var keywords: [String] {
+ switch self {
+ case .root:
+ return ["安全感", "稳定", "生存", "根基", "物质"]
+ case .sacral:
+ return ["创造力", "性能量", "情感", "流动", "享受"]
+ case .solarPlexus:
+ return ["自信", "力量", "意志", "控制", "个性"]
+ case .heart:
+ return ["爱", "同情", "宽恕", "连接", "和谐"]
+ case .throat:
+ return ["表达", "沟通", "真实", "创意", "声音"]
+ case .thirdEye:
+ return ["直觉", "洞察", "智慧", "想象", "觉知"]
+ case .crown:
+ return ["灵性", "觉醒", "超越", "统一", "神圣"]
+ }
+ }
+
+ var healingBenefits: [String] {
+ switch self {
+ case .root:
+ return ["增强安全感", "改善焦虑", "提升专注力", "增强体力"]
+ case .sacral:
+ return ["激发创造力", "改善人际关系", "增强活力", "平衡情绪"]
+ case .solarPlexus:
+ return ["提升自信", "增强意志力", "改善消化", "释放压力"]
+ case .heart:
+ return ["开放心扉", "增强同理心", "改善关系", "释放怨恨"]
+ case .throat:
+ return ["提升表达能力", "增强创造力", "改善沟通", "释放恐惧"]
+ case .thirdEye:
+ return ["增强直觉", "提升洞察力", "改善专注", "开发智慧"]
+ case .crown:
+ return ["提升觉知", "增强灵性连接", "获得内在平静", "超越自我"]
+ }
+ }
+}
\ No newline at end of file
diff --git a/EmotionMuseum/EmotionMuseum/Models/DataModels.swift b/EmotionMuseum/EmotionMuseum/Models/DataModels.swift
new file mode 100644
index 0000000..eb18881
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Models/DataModels.swift
@@ -0,0 +1,1081 @@
+//
+// DataModels.swift
+// EmotionMuseum
+//
+// Created by 华中敏 on 2025/6/13.
+//
+
+import Foundation
+import CoreLocation
+import SwiftUI
+
+// MARK: - 用户相关模型
+
+struct User: Identifiable, Codable {
+ let id: UUID
+ let username: String
+ let email: String
+ let avatar: String?
+ var profile: UserProfile
+ let createdAt: Date
+ var lastActiveAt: Date
+
+ init(id: UUID = UUID(), username: String, email: String, avatar: String? = nil, profile: UserProfile, createdAt: Date = Date(), lastActiveAt: Date = Date()) {
+ self.id = id
+ self.username = username
+ self.email = email
+ self.avatar = avatar
+ self.profile = profile
+ self.createdAt = createdAt
+ self.lastActiveAt = lastActiveAt
+ }
+}
+
+struct UserProfile: Codable {
+ var nickname: String
+ var birthDate: Date?
+ var location: String?
+ var bio: String?
+ var memberLevel: MemberLevel
+ var totalDays: Int
+ var growthStats: GrowthStats
+
+ init(nickname: String, birthDate: Date? = nil, location: String? = nil, bio: String? = nil, memberLevel: MemberLevel = .free, totalDays: Int = 0, growthStats: GrowthStats = GrowthStats()) {
+ self.nickname = nickname
+ self.birthDate = birthDate
+ self.location = location
+ self.bio = bio
+ self.memberLevel = memberLevel
+ self.totalDays = totalDays
+ self.growthStats = growthStats
+ }
+}
+
+enum MemberLevel: String, Codable, CaseIterable {
+ case free = "免费会员"
+ case premium = "高级会员"
+ case vip = "VIP会员"
+
+ var color: Color {
+ switch self {
+ case .free: return .gray
+ case .premium: return .blue
+ case .vip: return .orange
+ }
+ }
+}
+
+struct GrowthStats: Codable {
+ var selfAwareness: Float // 自我感知 0-100
+ var emotionalResilience: Float // 情绪韧性 0-100
+ var actionPower: Float // 行动力 0-100
+ var empathy: Float // 共情力 0-100
+ var lifeEnthusiasm: Float // 生活热度 0-100
+
+ init(selfAwareness: Float = 50, emotionalResilience: Float = 50, actionPower: Float = 50, empathy: Float = 50, lifeEnthusiasm: Float = 50) {
+ self.selfAwareness = selfAwareness
+ self.emotionalResilience = emotionalResilience
+ self.actionPower = actionPower
+ self.empathy = empathy
+ self.lifeEnthusiasm = lifeEnthusiasm
+ }
+
+ var average: Float {
+ (selfAwareness + emotionalResilience + actionPower + empathy + lifeEnthusiasm) / 5
+ }
+
+ var dataPoints: [(String, Float)] {
+ [
+ ("自我感知", selfAwareness),
+ ("情绪韧性", emotionalResilience),
+ ("行动力", actionPower),
+ ("共情力", empathy),
+ ("生活热度", lifeEnthusiasm)
+ ]
+ }
+}
+
+// MARK: - 对话系统模型
+
+struct Conversation: Identifiable, Codable {
+ let id: UUID
+ let userId: UUID
+ var title: String
+ var messages: [Message]
+ let startTime: Date
+ var endTime: Date?
+ var emotionAnalysis: EmotionAnalysis?
+ var summary: String?
+ var tags: [String]
+
+ init(id: UUID = UUID(), userId: UUID, title: String, messages: [Message] = [], startTime: Date = Date(), endTime: Date? = nil, emotionAnalysis: EmotionAnalysis? = nil, summary: String? = nil, tags: [String] = []) {
+ self.id = id
+ self.userId = userId
+ self.title = title
+ self.messages = messages
+ self.startTime = startTime
+ self.endTime = endTime
+ self.emotionAnalysis = emotionAnalysis
+ self.summary = summary
+ self.tags = tags
+ }
+
+ var duration: TimeInterval? {
+ guard let endTime = endTime else { return nil }
+ return endTime.timeIntervalSince(startTime)
+ }
+
+ var lastMessage: Message? {
+ messages.last
+ }
+
+ var messageCount: Int {
+ messages.count
+ }
+}
+
+struct Message: Identifiable, Codable {
+ let id: UUID
+ let conversationId: UUID
+ let content: String
+ let type: MessageType
+ let sender: MessageSender
+ let timestamp: Date
+ var emotionScore: Float?
+ var isRead: Bool
+
+ init(id: UUID = UUID(), conversationId: UUID, content: String, type: MessageType, sender: MessageSender, timestamp: Date = Date(), emotionScore: Float? = nil, isRead: Bool = true) {
+ self.id = id
+ self.conversationId = conversationId
+ self.content = content
+ self.type = type
+ self.sender = sender
+ self.timestamp = timestamp
+ self.emotionScore = emotionScore
+ self.isRead = isRead
+ }
+}
+
+enum MessageType: String, Codable, CaseIterable {
+ case text = "文字"
+ case voice = "语音"
+ case image = "图片"
+ case system = "系统"
+
+ var icon: String {
+ switch self {
+ case .text: return "text.bubble"
+ case .voice: return "mic.circle"
+ case .image: return "photo"
+ case .system: return "gear"
+ }
+ }
+}
+
+enum MessageSender: String, Codable {
+ case user = "用户"
+ case ai = "AI助手"
+}
+
+struct EmotionAnalysis: Codable {
+ let primaryEmotion: EmotionType
+ let emotionIntensity: Float // 0-1
+ let emotionTrend: EmotionTrend
+ let keywords: [String]
+ let aiInsights: String
+ let confidence: Float // 分析置信度 0-1
+
+ init(primaryEmotion: EmotionType, emotionIntensity: Float, emotionTrend: EmotionTrend, keywords: [String], aiInsights: String, confidence: Float = 0.8) {
+ self.primaryEmotion = primaryEmotion
+ self.emotionIntensity = emotionIntensity
+ self.emotionTrend = emotionTrend
+ self.keywords = keywords
+ self.aiInsights = aiInsights
+ self.confidence = confidence
+ }
+}
+
+enum EmotionType: String, Codable, CaseIterable {
+ case joy = "喜悦"
+ case sadness = "悲伤"
+ case anger = "愤怒"
+ case fear = "恐惧"
+ case surprise = "惊讶"
+ case neutral = "平静"
+ case anxiety = "焦虑"
+ case excitement = "兴奋"
+ case contentment = "满足"
+ case confusion = "困惑"
+ case melancholy = "忧郁" // 添加缺失的枚举值
+
+ var emoji: String {
+ switch self {
+ case .joy: return "😊"
+ case .sadness: return "😢"
+ case .anger: return "😡"
+ case .fear: return "😨"
+ case .surprise: return "😲"
+ case .neutral: return "😐"
+ case .anxiety: return "😰"
+ case .excitement: return "🤩"
+ case .contentment: return "😌"
+ case .confusion: return "🤔"
+ case .melancholy: return "😔"
+ }
+ }
+
+ var color: Color {
+ switch self {
+ case .joy: return .yellow
+ case .sadness: return .blue
+ case .anger: return .red
+ case .fear: return .purple
+ case .surprise: return .orange
+ case .neutral: return .gray
+ case .anxiety: return .pink
+ case .excitement: return .green
+ case .contentment: return .mint
+ case .confusion: return .brown
+ case .melancholy: return .indigo
+ }
+ }
+
+ var displayName: String { // 添加displayName属性
+ rawValue
+ }
+}
+
+enum EmotionTrend: String, Codable, CaseIterable {
+ case improving = "改善中"
+ case stable = "稳定"
+ case declining = "下降中"
+ case fluctuating = "波动中"
+
+ var icon: String {
+ switch self {
+ case .improving: return "arrow.up.circle"
+ case .stable: return "minus.circle"
+ case .declining: return "arrow.down.circle"
+ case .fluctuating: return "arrow.up.arrow.down.circle"
+ }
+ }
+
+ var color: Color {
+ switch self {
+ case .improving: return .green
+ case .stable: return .blue
+ case .declining: return .red
+ case .fluctuating: return .orange
+ }
+ }
+}
+
+// MARK: - 情绪记录模型
+
+struct EmotionRecord: Identifiable, Codable {
+ let id: UUID
+ let userId: UUID
+ let date: Date
+ let emotionType: EmotionType
+ let intensity: Float // 0-1
+ let context: String
+ let triggers: [String] // 触发因素
+ let location: String?
+ let weather: String?
+ let notes: String?
+
+ init(id: UUID = UUID(), userId: UUID, date: Date = Date(), emotionType: EmotionType, intensity: Float, context: String, triggers: [String] = [], location: String? = nil, weather: String? = nil, notes: String? = nil) {
+ self.id = id
+ self.userId = userId
+ self.date = date
+ self.emotionType = emotionType
+ self.intensity = intensity
+ self.context = context
+ self.triggers = triggers
+ self.location = location
+ self.weather = weather
+ self.notes = notes
+ }
+}
+
+// MARK: - 成长课题模型
+
+struct GrowthTopic: Identifiable, Codable, Hashable {
+ let id: UUID
+ let title: String
+ let description: String
+ let category: TopicCategory
+ let difficulty: Difficulty
+ var progress: Float // 0-1
+ var level: Int
+ let totalLevels: Int
+ var isUnlocked: Bool
+ var completedAt: Date?
+ var rewards: [Reward]
+ var interactions: [TopicInteraction]
+ let estimatedDuration: TimeInterval // 预估完成时间(秒)
+ let prerequisites: [UUID] // 前置课题ID
+ var content: TopicContent // 添加内容属性
+
+ init(id: UUID = UUID(), title: String, description: String, category: TopicCategory, difficulty: Difficulty, progress: Float = 0, level: Int = 1, totalLevels: Int = 5, isUnlocked: Bool = true, completedAt: Date? = nil, rewards: [Reward] = [], interactions: [TopicInteraction] = [], estimatedDuration: TimeInterval = 3600, prerequisites: [UUID] = [], content: TopicContent? = nil) {
+ self.id = id
+ self.title = title
+ self.description = description
+ self.category = category
+ self.difficulty = difficulty
+ self.progress = progress
+ self.level = level
+ self.totalLevels = totalLevels
+ self.isUnlocked = isUnlocked
+ self.completedAt = completedAt
+ self.rewards = rewards
+ self.interactions = interactions
+ self.estimatedDuration = estimatedDuration
+ self.prerequisites = prerequisites
+ self.content = content ?? TopicContent() // 提供默认值
+ }
+
+ var isCompleted: Bool {
+ progress >= 1.0
+ }
+
+ var progressPercentage: Int {
+ Int(progress * 100)
+ }
+
+ static func == (lhs: GrowthTopic, rhs: GrowthTopic) -> Bool {
+ lhs.id == rhs.id
+ }
+
+ func hash(into hasher: inout Hasher) {
+ hasher.combine(id)
+ }
+}
+
+enum TopicCategory: String, Codable, CaseIterable {
+ case selfAwareness = "自我认知"
+ case emotionRegulation = "情绪调节"
+ case socialSkills = "社交技能"
+ case stressManagement = "压力管理"
+ case lifeGoals = "人生目标"
+ case mindfulness = "正念冥想"
+ case relationships = "人际关系"
+ case creativity = "创造力"
+
+ var icon: String {
+ switch self {
+ case .selfAwareness: return "person.circle"
+ case .emotionRegulation: return "heart.circle"
+ case .socialSkills: return "person.2.circle"
+ case .stressManagement: return "leaf.circle"
+ case .lifeGoals: return "target"
+ case .mindfulness: return "brain.head.profile"
+ case .relationships: return "heart.text.square"
+ case .creativity: return "paintbrush.pointed"
+ }
+ }
+
+ var color: Color {
+ switch self {
+ case .selfAwareness: return .blue
+ case .emotionRegulation: return .pink
+ case .socialSkills: return .green
+ case .stressManagement: return .purple
+ case .lifeGoals: return .orange
+ case .mindfulness: return .indigo
+ case .relationships: return .red
+ case .creativity: return .yellow
+ }
+ }
+}
+
+enum Difficulty: String, Codable, CaseIterable {
+ case beginner = "初级"
+ case intermediate = "中级"
+ case advanced = "高级"
+ case expert = "专家"
+
+ var color: Color {
+ switch self {
+ case .beginner: return .green
+ case .intermediate: return .blue
+ case .advanced: return .orange
+ case .expert: return .red
+ }
+ }
+
+ var stars: Int {
+ switch self {
+ case .beginner: return 1
+ case .intermediate: return 2
+ case .advanced: return 3
+ case .expert: return 4
+ }
+ }
+}
+
+struct TopicInteraction: Identifiable, Codable {
+ let id: UUID
+ let topicId: UUID
+ let type: InteractionType
+ let title: String
+ let content: String
+ let completedAt: Date?
+ var reward: Reward?
+ let duration: TimeInterval?
+ var rating: Int? // 1-5星评分
+
+ init(id: UUID = UUID(), topicId: UUID, type: InteractionType, title: String, content: String, completedAt: Date? = nil, reward: Reward? = nil, duration: TimeInterval? = nil, rating: Int? = nil) {
+ self.id = id
+ self.topicId = topicId
+ self.type = type
+ self.title = title
+ self.content = content
+ self.completedAt = completedAt
+ self.reward = reward
+ self.duration = duration
+ self.rating = rating
+ }
+
+ var isCompleted: Bool {
+ completedAt != nil
+ }
+}
+
+enum InteractionType: String, Codable, CaseIterable {
+ case aiChat = "AI对话"
+ case article = "知识文章"
+ case exercise = "练习活动"
+ case reflection = "反思日记"
+ case meditation = "冥想练习"
+ case quiz = "小测验"
+
+ var icon: String {
+ switch self {
+ case .aiChat: return "message.circle"
+ case .article: return "doc.text"
+ case .exercise: return "figure.walk"
+ case .reflection: return "book.circle"
+ case .meditation: return "leaf"
+ case .quiz: return "questionmark.circle"
+ }
+ }
+
+ var color: Color {
+ switch self {
+ case .aiChat: return .blue
+ case .article: return .green
+ case .exercise: return .orange
+ case .reflection: return .purple
+ case .meditation: return .mint
+ case .quiz: return .pink
+ }
+ }
+}
+
+// MARK: - 奖励系统模型
+
+struct Reward: Identifiable, Codable {
+ let id: UUID
+ let type: RewardType
+ let title: String
+ let description: String
+ let value: Int // 积分值或其他数值
+ let rarity: RewardRarity
+ let earnedAt: Date
+ var isNew: Bool // 是否为新获得
+
+ init(id: UUID = UUID(), type: RewardType, title: String, description: String, value: Int, rarity: RewardRarity, earnedAt: Date = Date(), isNew: Bool = true) {
+ self.id = id
+ self.type = type
+ self.title = title
+ self.description = description
+ self.value = value
+ self.rarity = rarity
+ self.earnedAt = earnedAt
+ self.isNew = isNew
+ }
+}
+
+enum RewardType: String, Codable, CaseIterable {
+ case points = "积分"
+ case badge = "徽章"
+ case title = "称号"
+ case skin = "皮肤"
+ case item = "道具"
+
+ var icon: String {
+ switch self {
+ case .points: return "star.circle"
+ case .badge: return "shield.circle"
+ case .title: return "crown.circle"
+ case .skin: return "paintpalette.circle"
+ case .item: return "gift.circle"
+ }
+ }
+}
+
+enum RewardRarity: String, Codable, CaseIterable {
+ case common = "普通"
+ case rare = "稀有"
+ case epic = "史诗"
+ case legendary = "传说"
+
+ var color: Color {
+ switch self {
+ case .common: return .gray
+ case .rare: return .blue
+ case .epic: return .purple
+ case .legendary: return .orange
+ }
+ }
+}
+
+// MARK: - 地图和位置模型
+
+struct Coordinate: Codable {
+ let latitude: Double
+ let longitude: Double
+
+ var clLocationCoordinate2D: CLLocationCoordinate2D {
+ CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
+ }
+
+ init(latitude: Double, longitude: Double) {
+ self.latitude = latitude
+ self.longitude = longitude
+ }
+
+ init(from coordinate: CLLocationCoordinate2D) {
+ self.latitude = coordinate.latitude
+ self.longitude = coordinate.longitude
+ }
+}
+
+struct LocationPin: Identifiable, Codable, Hashable {
+ let id: UUID
+ let coordinate: Coordinate
+ let title: String
+ let description: String
+ let type: LocationType
+ let emotionTags: [EmotionType]
+ let photos: [String] // 图片URL数组
+ let createdBy: UUID?
+ let createdAt: Date
+ var likes: Int
+ var visits: Int
+ let address: String?
+ let category: LocationCategory
+ var isBookmarked: Bool
+
+ // 添加缺失的属性
+ var name: String { title } // 提供name属性作为title的别名
+ var tags: [String] { emotionTags.map { $0.rawValue } } // 提供tags属性
+ var visitCount: Int { visits } // 提供visitCount属性作为visits的别名
+ var lastVisitAt: Date? // 最后访问时间
+ var emotion: EmotionType { emotionTags.first ?? .neutral } // 主要情绪
+
+ init(id: UUID = UUID(), coordinate: Coordinate, title: String, description: String, type: LocationType, emotionTags: [EmotionType] = [], photos: [String] = [], createdBy: UUID? = nil, createdAt: Date = Date(), likes: Int = 0, visits: Int = 0, address: String? = nil, category: LocationCategory = .other, isBookmarked: Bool = false, lastVisitAt: Date? = nil) {
+ self.id = id
+ self.coordinate = coordinate
+ self.title = title
+ self.description = description
+ self.type = type
+ self.emotionTags = emotionTags
+ self.photos = photos
+ self.createdBy = createdBy
+ self.createdAt = createdAt
+ self.likes = likes
+ self.visits = visits
+ self.address = address
+ self.category = category
+ self.isBookmarked = isBookmarked
+ self.lastVisitAt = lastVisitAt
+ }
+
+ static func == (lhs: LocationPin, rhs: LocationPin) -> Bool {
+ lhs.id == rhs.id
+ }
+
+ func hash(into hasher: inout Hasher) {
+ hasher.combine(id)
+ }
+}
+
+enum LocationType: String, Codable, CaseIterable {
+ case personal = "个人收藏"
+ case aiRecommended = "AI推荐"
+ case community = "社区分享"
+ case popular = "热门地点"
+
+ var icon: String {
+ switch self {
+ case .personal: return "heart.fill"
+ case .aiRecommended: return "brain.head.profile"
+ case .community: return "person.3.fill"
+ case .popular: return "flame.fill"
+ }
+ }
+
+ var color: Color {
+ switch self {
+ case .personal: return .red
+ case .aiRecommended: return .blue
+ case .community: return .green
+ case .popular: return .orange
+ }
+ }
+}
+
+enum LocationCategory: String, Codable, CaseIterable {
+ case park = "公园"
+ case cafe = "咖啡厅"
+ case museum = "博物馆"
+ case library = "图书馆"
+ case beach = "海滩"
+ case mountain = "山景"
+ case temple = "寺庙"
+ case garden = "花园"
+ case lake = "湖泊"
+ case other = "其他"
+ // 添加缺失的枚举值
+ case restaurant = "餐厅"
+ case gym = "健身房"
+ case lookout = "观景台"
+ case bookstore = "书店"
+ case bar = "酒吧"
+ case shop = "商店"
+ case home = "家"
+ case work = "工作"
+ case school = "学校"
+ case shopping = "购物"
+ case travel = "旅行"
+ case nature = "自然"
+ case entertainment = "娱乐"
+
+ var icon: String {
+ switch self {
+ case .park: return "tree"
+ case .cafe: return "cup.and.saucer"
+ case .museum: return "building.columns"
+ case .library: return "books.vertical"
+ case .beach: return "water.waves"
+ case .mountain: return "mountain.2"
+ case .temple: return "building"
+ case .garden: return "leaf"
+ case .lake: return "drop"
+ case .other: return "mappin"
+ case .restaurant: return "fork.knife"
+ case .gym: return "dumbbell"
+ case .lookout: return "binoculars"
+ case .bookstore: return "book"
+ case .bar: return "wineglass"
+ case .shop: return "bag"
+ case .home: return "house"
+ case .work: return "briefcase"
+ case .school: return "graduationcap"
+ case .shopping: return "cart"
+ case .travel: return "airplane"
+ case .nature: return "tree.fill"
+ case .entertainment: return "tv"
+ }
+ }
+}
+
+// MARK: - 社区模型
+
+struct CommunityPost: Identifiable, Codable, Hashable {
+ let id: UUID
+ let userId: UUID
+ let locationId: UUID?
+ let content: String
+ let photos: [String]
+ let tags: [String]
+ var likes: Int
+ var comments: [Comment]
+ let createdAt: Date
+ var isPrivate: Bool
+ var viewCount: Int
+ let type: PostType
+ var isLikedByCurrentUser: Bool
+ var authorName: String // 添加作者名称属性
+
+ init(id: UUID = UUID(), userId: UUID, locationId: UUID? = nil, content: String, photos: [String] = [], tags: [String] = [], likes: Int = 0, comments: [Comment] = [], createdAt: Date = Date(), isPrivate: Bool = false, viewCount: Int = 0, type: PostType = .general, isLikedByCurrentUser: Bool = false, authorName: String = "匿名用户") {
+ self.id = id
+ self.userId = userId
+ self.locationId = locationId
+ self.content = content
+ self.photos = photos
+ self.tags = tags
+ self.likes = likes
+ self.comments = comments
+ self.createdAt = createdAt
+ self.isPrivate = isPrivate
+ self.viewCount = viewCount
+ self.type = type
+ self.isLikedByCurrentUser = isLikedByCurrentUser
+ self.authorName = authorName
+ }
+
+ var commentCount: Int {
+ comments.count
+ }
+
+ static func == (lhs: CommunityPost, rhs: CommunityPost) -> Bool {
+ lhs.id == rhs.id
+ }
+
+ func hash(into hasher: inout Hasher) {
+ hasher.combine(id)
+ }
+}
+
+enum PostType: String, Codable, CaseIterable {
+ case general = "一般分享"
+ case emotion = "情绪记录"
+ case growth = "成长感悟"
+ case location = "地点推荐"
+ case achievement = "成就展示"
+
+ var icon: String {
+ switch self {
+ case .general: return "text.bubble"
+ case .emotion: return "heart"
+ case .growth: return "arrow.up.circle"
+ case .location: return "mappin"
+ case .achievement: return "trophy"
+ }
+ }
+
+ var color: Color {
+ switch self {
+ case .general: return .blue
+ case .emotion: return .pink
+ case .growth: return .green
+ case .location: return .orange
+ case .achievement: return .yellow
+ }
+ }
+}
+
+struct Comment: Identifiable, Codable {
+ let id: UUID
+ let postId: UUID
+ let userId: UUID
+ let content: String
+ let createdAt: Date
+ var likes: Int
+ let replyToId: UUID? // 回复的评论ID
+ var isLikedByCurrentUser: Bool
+
+ init(id: UUID = UUID(), postId: UUID, userId: UUID, content: String, createdAt: Date = Date(), likes: Int = 0, replyToId: UUID? = nil, isLikedByCurrentUser: Bool = false) {
+ self.id = id
+ self.postId = postId
+ self.userId = userId
+ self.content = content
+ self.createdAt = createdAt
+ self.likes = likes
+ self.replyToId = replyToId
+ self.isLikedByCurrentUser = isLikedByCurrentUser
+ }
+}
+
+// MARK: - 成就系统模型
+
+struct Achievement: Identifiable, Codable {
+ let id: UUID
+ let title: String
+ let description: String
+ let category: AchievementCategory
+ let icon: String
+ let rarity: RewardRarity
+ let requirement: AchievementRequirement
+ var progress: Int
+ let targetValue: Int
+ var unlockedAt: Date?
+ var isHidden: Bool // 是否为隐藏成就
+
+ init(id: UUID = UUID(), title: String, description: String, category: AchievementCategory, icon: String, rarity: RewardRarity, requirement: AchievementRequirement, progress: Int = 0, targetValue: Int, unlockedAt: Date? = nil, isHidden: Bool = false) {
+ self.id = id
+ self.title = title
+ self.description = description
+ self.category = category
+ self.icon = icon
+ self.rarity = rarity
+ self.requirement = requirement
+ self.progress = progress
+ self.targetValue = targetValue
+ self.unlockedAt = unlockedAt
+ self.isHidden = isHidden
+ }
+
+ var isUnlocked: Bool {
+ unlockedAt != nil
+ }
+
+ var progressPercentage: Float {
+ Float(progress) / Float(targetValue)
+ }
+}
+
+enum AchievementCategory: String, Codable, CaseIterable {
+ case conversation = "对话交流"
+ case emotion = "情绪管理"
+ case growth = "个人成长"
+ case social = "社交互动"
+ case exploration = "探索发现"
+ case consistency = "坚持习惯"
+ case milestone = "里程碑"
+ case special = "特殊成就"
+ case all = "全部"
+
+ var icon: String {
+ switch self {
+ case .conversation: return "message.circle"
+ case .emotion: return "heart.circle"
+ case .growth: return "arrow.up.circle"
+ case .social: return "person.2.circle"
+ case .exploration: return "map.circle"
+ case .consistency: return "calendar.circle"
+ case .milestone: return "flag.circle"
+ case .special: return "star.circle"
+ case .all: return "circle.grid.3x3"
+ }
+ }
+}
+
+enum AchievementRequirement: Codable {
+ case conversationCount(Int)
+ case emotionRecordCount(Int)
+ case topicCompletion(Int)
+ case socialInteraction(Int)
+ case locationVisit(Int)
+ case consecutiveDays(Int)
+ case totalPoints(Int)
+ case special(String)
+
+ var description: String {
+ switch self {
+ case .conversationCount(let count):
+ return "完成\(count)次对话"
+ case .emotionRecordCount(let count):
+ return "记录\(count)次情绪"
+ case .topicCompletion(let count):
+ return "完成\(count)个成长课题"
+ case .socialInteraction(let count):
+ return "进行\(count)次社交互动"
+ case .locationVisit(let count):
+ return "访问\(count)个地点"
+ case .consecutiveDays(let days):
+ return "连续使用\(days)天"
+ case .totalPoints(let points):
+ return "获得\(points)积分"
+ case .special(let desc):
+ return desc
+ }
+ }
+}
+
+// MARK: - 统计数据模型
+
+struct UserStats: Codable {
+ var totalConversations: Int
+ var totalMessages: Int
+ var totalEmotionRecords: Int
+ var completedTopics: Int
+ var totalPoints: Int
+ var consecutiveDays: Int
+ var maxConsecutiveDays: Int
+ var socialInteractions: Int
+ var locationsVisited: Int
+ var postsCreated: Int
+ var likesReceived: Int
+ var commentsReceived: Int
+
+ init() {
+ self.totalConversations = 0
+ self.totalMessages = 0
+ self.totalEmotionRecords = 0
+ self.completedTopics = 0
+ self.totalPoints = 0
+ self.consecutiveDays = 0
+ self.maxConsecutiveDays = 0
+ self.socialInteractions = 0
+ self.locationsVisited = 0
+ self.postsCreated = 0
+ self.likesReceived = 0
+ self.commentsReceived = 0
+ }
+}
+
+struct WeeklyStats: Codable {
+ let weekStartDate: Date
+ var emotionRecords: [EmotionRecord]
+ var conversations: [Conversation]
+ var topicInteractions: [TopicInteraction]
+ var moodTrend: EmotionTrend
+ var averageMoodScore: Float
+ var mostActiveDay: Date?
+ var dominantEmotion: EmotionType?
+
+ init(weekStartDate: Date) {
+ self.weekStartDate = weekStartDate
+ self.emotionRecords = []
+ self.conversations = []
+ self.topicInteractions = []
+ self.moodTrend = .stable
+ self.averageMoodScore = 0.5
+ self.mostActiveDay = nil
+ self.dominantEmotion = nil
+ }
+
+ var weekEndDate: Date {
+ Calendar.current.date(byAdding: .day, value: 6, to: weekStartDate) ?? weekStartDate
+ }
+}
+
+// MARK: - 扩展方法
+
+extension Date {
+ var timeAgo: String {
+ let formatter = RelativeDateTimeFormatter()
+ formatter.unitsStyle = .short
+ formatter.locale = Locale(identifier: "zh_CN")
+ return formatter.localizedString(for: self, relativeTo: Date())
+ }
+
+ var shortFormat: String {
+ let formatter = DateFormatter()
+ formatter.dateStyle = .short
+ formatter.timeStyle = .short
+ formatter.locale = Locale(identifier: "zh_CN")
+ return formatter.string(from: self)
+ }
+
+ var dayFormat: String {
+ let formatter = DateFormatter()
+ formatter.dateFormat = "MM月dd日"
+ formatter.locale = Locale(identifier: "zh_CN")
+ return formatter.string(from: self)
+ }
+}
+
+extension Array where Element == EmotionRecord {
+ func averageIntensity() -> Float {
+ guard !isEmpty else { return 0 }
+ let sum = reduce(0) { $0 + $1.intensity }
+ return sum / Float(count)
+ }
+
+ func dominantEmotion() -> EmotionType? {
+ guard !isEmpty else { return nil }
+ let emotionCounts = Dictionary(grouping: self, by: { $0.emotionType })
+ return emotionCounts.max(by: { $0.value.count < $1.value.count })?.key
+ }
+}
+
+extension Array where Element == GrowthTopic {
+ func averageProgress() -> Float {
+ guard !isEmpty else { return 0 }
+ let sum = reduce(0) { $0 + $1.progress }
+ return sum / Float(count)
+ }
+
+ func completedCount() -> Int {
+ filter { $0.isCompleted }.count
+ }
+}
+
+// MARK: - 筛选和排序枚举
+
+enum ConversationFilter: String, CaseIterable {
+ case all = "全部"
+ case recent = "最近"
+ case emotional = "情绪相关"
+ case growth = "成长相关"
+ case unread = "未读"
+
+ var icon: String {
+ switch self {
+ case .all: return "list.bullet"
+ case .recent: return "clock"
+ case .emotional: return "heart"
+ case .growth: return "arrow.up"
+ case .unread: return "circle.fill"
+ }
+ }
+}
+
+enum PostSortType: String, CaseIterable {
+ case latest = "最新"
+ case popular = "热门"
+ case nearby = "附近"
+ case liked = "点赞最多"
+
+ var icon: String {
+ switch self {
+ case .latest: return "clock"
+ case .popular: return "flame"
+ case .nearby: return "location"
+ case .liked: return "heart"
+ }
+ }
+}
+
+// MARK: - 文章和行动建议模型
+
+struct Article: Identifiable, Codable {
+ let id: UUID
+ let title: String
+ let content: String
+ let readTime: String
+ let tags: [String]
+ let difficulty: Difficulty
+ let createdAt: Date
+
+ init(id: UUID = UUID(), title: String, content: String, readTime: String, tags: [String] = [], difficulty: Difficulty = .beginner, createdAt: Date = Date()) {
+ self.id = id
+ self.title = title
+ self.content = content
+ self.readTime = readTime
+ self.tags = tags
+ self.difficulty = difficulty
+ self.createdAt = createdAt
+ }
+}
+
+struct ActionSuggestion: Identifiable, Codable {
+ let id: UUID
+ let title: String
+ let description: String
+ let duration: String
+ let difficulty: String
+ let category: String
+ var isCompleted: Bool
+ let createdAt: Date
+
+ init(id: UUID = UUID(), title: String, description: String, duration: String, difficulty: String, category: String, isCompleted: Bool = false, createdAt: Date = Date()) {
+ self.id = id
+ self.title = title
+ self.description = description
+ self.duration = duration
+ self.difficulty = difficulty
+ self.category = category
+ self.isCompleted = isCompleted
+ self.createdAt = createdAt
+ }
+}
+
+struct TopicContent: Codable {
+ let knowledgeArticles: [Article]
+ let actionSuggestions: [ActionSuggestion]
+ let aiGuidance: String
+
+ init(knowledgeArticles: [Article] = [], actionSuggestions: [ActionSuggestion] = [], aiGuidance: String = "") {
+ self.knowledgeArticles = knowledgeArticles
+ self.actionSuggestions = actionSuggestions
+ self.aiGuidance = aiGuidance
+ }
+}
+
+
\ No newline at end of file
diff --git a/EmotionMuseum/EmotionMuseum/Models/MapManager.swift b/EmotionMuseum/EmotionMuseum/Models/MapManager.swift
new file mode 100644
index 0000000..78abc06
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Models/MapManager.swift
@@ -0,0 +1,23 @@
+import Foundation
+// import AMapFoundationKit // 临时注释,需要安装CocoaPods依赖
+import CoreLocation
+
+/// 地图管理器
+/// @Author huazhongmin
+/// @Time 2024-03-24
+/// @Description 管理高德地图SDK的配置和初始化
+class MapManager {
+ static let shared = MapManager()
+
+ private init() {}
+
+ func configure() {
+ // TODO: 安装CocoaPods依赖后取消注释
+ // 设置高德地图的AppKey
+ // AMapServices.shared().apiKey = "bb63ae64d651624f3673d61b47b45435"
+
+ // 配置定位权限说明
+ let locationManager = CLLocationManager()
+ locationManager.requestWhenInUseAuthorization()
+ }
+}
\ No newline at end of file
diff --git a/EmotionMuseum/Persistence.swift b/EmotionMuseum/EmotionMuseum/Persistence.swift
similarity index 98%
rename from EmotionMuseum/Persistence.swift
rename to EmotionMuseum/EmotionMuseum/Persistence.swift
index 08019fa..2526695 100644
--- a/EmotionMuseum/Persistence.swift
+++ b/EmotionMuseum/EmotionMuseum/Persistence.swift
@@ -2,7 +2,7 @@
// Persistence.swift
// EmotionMuseum
//
-// Created by 华中敏 on 2025/5/26.
+// Created by 华中敏 on 2025/6/13.
//
import CoreData
diff --git a/EmotionMuseum/EmotionMuseum/Podfile b/EmotionMuseum/EmotionMuseum/Podfile
new file mode 100644
index 0000000..572db27
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Podfile
@@ -0,0 +1,11 @@
+platform :ios, '14.0'
+
+target 'EmotionMuseum' do
+ use_frameworks!
+
+ # 高德地图SDK
+ pod 'AMap3DMap'
+ pod 'AMapLocation'
+ pod 'AMapSearch'
+
+end
\ No newline at end of file
diff --git a/EmotionMuseum/EmotionMuseum/Services/AIService.swift b/EmotionMuseum/EmotionMuseum/Services/AIService.swift
new file mode 100644
index 0000000..c942041
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Services/AIService.swift
@@ -0,0 +1,549 @@
+//
+// AIService.swift
+// EmotionMuseum
+//
+// Created by 华中敏 on 2025/6/13.
+//
+
+import Foundation
+import Combine
+
+// MARK: - AI服务协议
+protocol AIServiceProtocol {
+ func sendMessage(_ message: String, userId: UUID) async throws -> AIResponse
+ func analyzeEmotion(_ text: String) async throws -> EmotionAnalysis
+ func generateGrowthSuggestions(for stats: GrowthStats) async throws -> [GrowthTopic]
+}
+
+// MARK: - AI响应模型
+struct AIResponse: Codable {
+ let messageId: UUID
+ let content: String
+ let emotionAnalysis: EmotionAnalysis
+ let suggestions: [String]
+ let followUpQuestions: [String]
+ let confidence: Float
+ let processingTime: TimeInterval
+
+ struct EmotionAnalysis: Codable {
+ let detectedEmotion: EmotionType
+ let intensity: Float
+ let triggers: [String]
+ let context: String
+ let recommendations: [String]
+ }
+}
+
+// MARK: - AI服务实现
+class AIService: AIServiceProtocol, ObservableObject {
+ static let shared = AIService()
+
+ private let baseURL = "https://api.openai.com/v1"
+ private let apiKey: String
+
+ @Published var isLoading = false
+ @Published var lastError: Error?
+
+ private init() {
+ // 在实际应用中,应该从安全配置文件或环境变量中读取API密钥
+ self.apiKey = Bundle.main.infoDictionary?["OPENAI_API_KEY"] as? String ?? ""
+ }
+
+ // MARK: - 发送消息
+ func sendMessage(_ message: String, userId: UUID) async throws -> AIResponse {
+ let startTime = Date()
+ isLoading = true
+ defer { isLoading = false }
+
+ let prompt = buildEmotionAnalysisPrompt(message: message)
+ let requestBody = OpenAIRequest(
+ model: "gpt-4",
+ messages: [
+ OpenAIMessage(role: "system", content: getSystemPrompt()),
+ OpenAIMessage(role: "user", content: prompt)
+ ],
+ temperature: 0.7,
+ maxTokens: 500
+ )
+
+ do {
+ let response = try await sendOpenAIRequest(requestBody)
+ let processingTime = Date().timeIntervalSince(startTime)
+
+ return try parseAIResponse(response, messageId: UUID(), processingTime: processingTime)
+ } catch {
+ lastError = error
+ throw error
+ }
+ }
+
+ // MARK: - 情绪分析
+ func analyzeEmotion(_ text: String) async throws -> EmotionAnalysis {
+ let prompt = """
+ 分析以下文本的情绪:"\(text)"
+
+ 请返回JSON格式的分析结果,包含:
+ - summary: 简要总结
+ - keywords: 关键词数组
+ - suggestions: 建议数组
+ - moodPattern: 情绪模式
+ - confidence: 置信度(0-1)
+ """
+
+ let requestBody = OpenAIRequest(
+ model: "gpt-4",
+ messages: [
+ OpenAIMessage(role: "system", content: "你是一个专业的情绪分析师,请用中文回答。"),
+ OpenAIMessage(role: "user", content: prompt)
+ ],
+ temperature: 0.3,
+ maxTokens: 300
+ )
+
+ let response = try await sendOpenAIRequest(requestBody)
+ return try parseEmotionAnalysis(response)
+ }
+
+ // MARK: - 生成成长建议
+ func generateGrowthSuggestions(for stats: GrowthStats) async throws -> [GrowthTopic] {
+ let prompt = """
+ 基于用户的五维人格画像生成个性化成长建议:
+ - 自我感知: \(stats.selfAwareness)
+ - 情绪韧性: \(stats.emotionalResilience)
+ - 行动力: \(stats.actionPower)
+ - 共情力: \(stats.empathy)
+ - 生活热度: \(stats.lifeEnthusiasm)
+
+ 请推荐3个最适合的成长课题,返回JSON格式。
+ """
+
+ let requestBody = OpenAIRequest(
+ model: "gpt-4",
+ messages: [
+ OpenAIMessage(role: "system", content: "你是一个专业的心理成长顾问。"),
+ OpenAIMessage(role: "user", content: prompt)
+ ],
+ temperature: 0.5,
+ maxTokens: 400
+ )
+
+ let response = try await sendOpenAIRequest(requestBody)
+ return try parseGrowthTopics(response)
+ }
+
+ // MARK: - 私有方法
+
+ private func getSystemPrompt() -> String {
+ return """
+ 你是情绪博物馆的AI情绪陪伴师,具有以下特质:
+
+ 1. 专业且温暖:具备心理学知识,但表达温和亲切
+ 2. 情绪敏感:能准确识别和回应用户的情绪状态
+ 3. 个性化关怀:根据用户的话语提供针对性建议
+ 4. 积极导向:引导用户朝着更健康的情绪状态发展
+ 5. 边界清晰:不提供医疗建议,必要时建议寻求专业帮助
+
+ 请用中文回答,保持对话自然流畅。
+ """
+ }
+
+ private func buildEmotionAnalysisPrompt(message: String) -> String {
+ return """
+ 用户消息:"\(message)"
+
+ 请分析这条消息的情绪状态,并提供适当的回应。包含:
+ 1. 检测到的主要情绪
+ 2. 情绪强度(0-1)
+ 3. 可能的触发因素
+ 4. 温暖的回应内容
+ 5. 具体的情绪调节建议
+ 6. 后续探索问题
+
+ 请以JSON格式返回分析结果。
+ """
+ }
+
+ private func sendOpenAIRequest(_ request: OpenAIRequest) async throws -> OpenAIResponse {
+ guard !apiKey.isEmpty else {
+ throw AIServiceError.missingAPIKey
+ }
+
+ let url = URL(string: "\(baseURL)/chat/completions")!
+ var urlRequest = URLRequest(url: url)
+ urlRequest.httpMethod = "POST"
+ urlRequest.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
+ urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
+
+ let encoder = JSONEncoder()
+ urlRequest.httpBody = try encoder.encode(request)
+
+ let (data, response) = try await URLSession.shared.data(for: urlRequest)
+
+ guard let httpResponse = response as? HTTPURLResponse,
+ httpResponse.statusCode == 200 else {
+ throw AIServiceError.apiError("请求失败")
+ }
+
+ let decoder = JSONDecoder()
+ return try decoder.decode(OpenAIResponse.self, from: data)
+ }
+
+ private func parseAIResponse(_ response: OpenAIResponse, messageId: UUID, processingTime: TimeInterval) throws -> AIResponse {
+ guard let choice = response.choices.first else {
+ throw AIServiceError.parseError("无法解析AI响应")
+ }
+
+ let content = choice.message.content
+
+ // 尝试解析JSON格式的响应
+ if let jsonData = content.data(using: .utf8),
+ let parsed = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any] {
+
+ let emotion = parseEmotionFromString(parsed["emotion"] as? String ?? "neutral")
+ let intensity = parsed["intensity"] as? Float ?? 0.5
+ let triggers = parsed["triggers"] as? [String] ?? []
+ let context = parsed["context"] as? String ?? ""
+ let recommendations = parsed["recommendations"] as? [String] ?? []
+ let suggestions = parsed["suggestions"] as? [String] ?? []
+ let followUpQuestions = parsed["followUpQuestions"] as? [String] ?? []
+ let responseContent = parsed["response"] as? String ?? content
+
+ let emotionAnalysis = AIResponse.EmotionAnalysis(
+ detectedEmotion: emotion,
+ intensity: intensity,
+ triggers: triggers,
+ context: context,
+ recommendations: recommendations
+ )
+
+ return AIResponse(
+ messageId: messageId,
+ content: responseContent,
+ emotionAnalysis: emotionAnalysis,
+ suggestions: suggestions,
+ followUpQuestions: followUpQuestions,
+ confidence: 0.8,
+ processingTime: processingTime
+ )
+ } else {
+ // 如果不是JSON格式,返回基础响应
+ return AIResponse(
+ messageId: messageId,
+ content: content,
+ emotionAnalysis: AIResponse.EmotionAnalysis(
+ detectedEmotion: .neutral,
+ intensity: 0.5,
+ triggers: [],
+ context: "基础对话",
+ recommendations: ["继续分享您的感受"]
+ ),
+ suggestions: ["继续对话"],
+ followUpQuestions: ["还有什么想分享的吗?"],
+ confidence: 0.6,
+ processingTime: processingTime
+ )
+ }
+ }
+
+ private func parseEmotionAnalysis(_ response: OpenAIResponse) throws -> EmotionAnalysis {
+ guard response.choices.first != nil else {
+ throw AIServiceError.parseError("无法解析情绪分析响应")
+ }
+
+ // 模拟解析,实际应用中需要更复杂的JSON解析
+ return EmotionAnalysis(
+ primaryEmotion: .neutral,
+ emotionIntensity: 0.5,
+ emotionTrend: .stable,
+ keywords: ["关键词1", "关键词2"],
+ aiInsights: "情绪分析摘要",
+ confidence: 0.75
+ )
+ }
+
+ private func parseGrowthTopics(_ response: OpenAIResponse) throws -> [GrowthTopic] {
+ // 模拟返回成长课题,实际应用中需要解析AI响应
+ return [
+ GrowthTopic(
+ title: "增强自我认知",
+ description: "通过反思练习提高自我觉察能力",
+ category: .selfAwareness,
+ difficulty: .beginner,
+ progress: 0.0
+ ),
+ GrowthTopic(
+ title: "情绪调节技巧",
+ description: "学习有效的情绪管理方法",
+ category: .emotionRegulation,
+ difficulty: .intermediate,
+ progress: 0.0
+ ),
+ GrowthTopic(
+ title: "提升沟通能力",
+ description: "改善人际关系和沟通技巧",
+ category: .relationships,
+ difficulty: .beginner,
+ progress: 0.0
+ )
+ ]
+ }
+
+ private func parseEmotionFromString(_ emotionString: String) -> EmotionType {
+ switch emotionString.lowercased() {
+ case "joy", "happy", "开心", "喜悦": return .joy
+ case "sad", "sadness", "悲伤", "难过": return .sadness
+ case "angry", "anger", "愤怒", "生气": return .anger
+ case "fear", "scared", "恐惧", "害怕": return .fear
+ case "surprise", "surprised", "惊讶", "意外": return .surprise
+ default: return .neutral
+ }
+ }
+}
+
+// MARK: - OpenAI API模型
+private struct OpenAIRequest: Codable {
+ let model: String
+ let messages: [OpenAIMessage]
+ let temperature: Float
+ let maxTokens: Int?
+
+ enum CodingKeys: String, CodingKey {
+ case model, messages, temperature
+ case maxTokens = "max_tokens"
+ }
+}
+
+private struct OpenAIMessage: Codable {
+ let role: String
+ let content: String
+}
+
+private struct OpenAIResponse: Codable {
+ let choices: [OpenAIChoice]
+}
+
+private struct OpenAIChoice: Codable {
+ let message: OpenAIMessage
+}
+
+// MARK: - 错误类型
+enum AIServiceError: LocalizedError {
+ case missingAPIKey
+ case apiError(String)
+ case parseError(String)
+ case networkError(Error)
+
+ var errorDescription: String? {
+ switch self {
+ case .missingAPIKey:
+ return "缺少API密钥"
+ case .apiError(let message):
+ return "API错误: \(message)"
+ case .parseError(let message):
+ return "解析错误: \(message)"
+ case .networkError(let error):
+ return "网络错误: \(error.localizedDescription)"
+ }
+ }
+}
+
+// MARK: - 模拟AI服务(用于开发和测试)
+class MockAIService: AIServiceProtocol, ObservableObject {
+ @Published var isLoading = false
+
+ func sendMessage(_ message: String, userId: UUID) async throws -> AIResponse {
+ isLoading = true
+
+ // 模拟网络延迟
+ try await Task.sleep(nanoseconds: 1_000_000_000) // 1秒
+
+ isLoading = false
+
+ let emotion = analyzeEmotionFromMessage(message)
+ let response = generateMockResponse(for: emotion, message: message)
+
+ return AIResponse(
+ messageId: UUID(),
+ content: response,
+ emotionAnalysis: AIResponse.EmotionAnalysis(
+ detectedEmotion: emotion,
+ intensity: Float.random(in: 0.3...0.9),
+ triggers: extractTriggers(from: message),
+ context: "日常对话",
+ recommendations: generateRecommendations(for: emotion)
+ ),
+ suggestions: generateSuggestions(for: emotion),
+ followUpQuestions: generateFollowUpQuestions(for: emotion),
+ confidence: Float.random(in: 0.6...0.9),
+ processingTime: 1.0
+ )
+ }
+
+ func analyzeEmotion(_ text: String) async throws -> EmotionAnalysis {
+ try await Task.sleep(nanoseconds: 500_000_000) // 0.5秒
+
+ return EmotionAnalysis(
+ primaryEmotion: analyzeEmotionFromMessage(text),
+ emotionIntensity: Float.random(in: 0.3...0.9),
+ emotionTrend: .stable,
+ keywords: ["情绪", "感受", "心情"],
+ aiInsights: "检测到\(analyzeEmotionFromMessage(text).rawValue)情绪",
+ confidence: Float.random(in: 0.7...0.95)
+ )
+ }
+
+ func generateGrowthSuggestions(for stats: GrowthStats) async throws -> [GrowthTopic] {
+ try await Task.sleep(nanoseconds: 800_000_000) // 0.8秒
+
+ return [
+ GrowthTopic(
+ title: "自我认知提升",
+ description: "通过日记和反思提高自我觉察",
+ category: .selfAwareness,
+ difficulty: .beginner,
+ progress: 0.0
+ ),
+ GrowthTopic(
+ title: "情绪管理训练",
+ description: "学习情绪调节和压力缓解技巧",
+ category: .emotionRegulation,
+ difficulty: .intermediate,
+ progress: 0.0
+ )
+ ]
+ }
+
+ // MARK: - 私有辅助方法
+
+ private func analyzeEmotionFromMessage(_ message: String) -> EmotionType {
+ let lowerMessage = message.lowercased()
+
+ if lowerMessage.contains("开心") || lowerMessage.contains("高兴") || lowerMessage.contains("快乐") {
+ return .joy
+ } else if lowerMessage.contains("难过") || lowerMessage.contains("悲伤") || lowerMessage.contains("伤心") {
+ return .sadness
+ } else if lowerMessage.contains("生气") || lowerMessage.contains("愤怒") || lowerMessage.contains("烦躁") {
+ return .anger
+ } else if lowerMessage.contains("害怕") || lowerMessage.contains("恐惧") || lowerMessage.contains("紧张") {
+ return .fear
+ } else if lowerMessage.contains("惊讶") || lowerMessage.contains("意外") {
+ return .surprise
+ } else {
+ return .neutral
+ }
+ }
+
+ private func generateMockResponse(for emotion: EmotionType, message: String) -> String {
+ switch emotion {
+ case .joy:
+ return "我能感受到你的开心!这种积极的情绪很珍贵,记得把这份快乐分享给身边的人。是什么让你感到如此愉悦呢?"
+ case .sadness:
+ return "我理解你现在的感受,难过是正常的情绪反应。允许自己感受这些情绪,同时也要温柔地照顾自己。你愿意分享更多吗?"
+ case .anger:
+ return "我能察觉到你的愤怒情绪。愤怒往往是其他情绪的表达,比如受伤或失望。深呼吸一下,我们一起探索这种感受的根源。"
+ case .fear:
+ return "恐惧感让人不安,这是很自然的反应。记住,你有能力面对困难。让我们一起分析一下你担心的事情,也许并不像想象中那么可怕。"
+ case .surprise:
+ return "意外的事情总是让人印象深刻!无论是好的惊喜还是让人措手不及的情况,都是生活的一部分。告诉我更多细节吧。"
+ case .neutral:
+ return "我在聆听你的分享。有时候平静也是一种很好的状态。如果你想深入探讨任何感受或想法,我都愿意陪伴你。"
+ case .anxiety:
+ return "我能感受到你的焦虑情绪。焦虑是很常见的感受,让我们一起找到缓解的方法。"
+ case .excitement:
+ return "你的兴奋情绪很有感染力!这种充满活力的状态真的很棒。告诉我是什么让你如此激动,我也想分享你的喜悦!"
+ case .contentment:
+ return "你的满足感让我很欣慰。这种内心的平和与充实是生活中最珍贵的状态之一。珍惜这份宁静,它会给你力量。"
+ case .confusion:
+ return "我感受到你内心的困惑。困惑是思考和成长的开始,让我们一起理清思路。"
+ case .melancholy:
+ return "我感受到你内心的忧郁。这种淡淡的愁绪有时候也是一种美,它让我们更深刻地感受生活。愿意和我分享你的思绪吗?"
+ }
+ }
+
+ private func extractTriggers(from message: String) -> [String] {
+ // 简单的关键词提取
+ let keywords = ["工作", "家庭", "朋友", "健康", "学习", "感情", "压力", "变化"]
+ return keywords.filter { message.contains($0) }
+ }
+
+ private func generateRecommendations(for emotion: EmotionType) -> [String] {
+ switch emotion {
+ case .joy:
+ return ["记录这个美好时刻", "分享你的快乐", "感恩当下"]
+ case .sadness:
+ return ["允许自己哭泣", "寻求朋友支持", "进行轻柔运动"]
+ case .anger:
+ return ["深呼吸放松", "写下愤怒的原因", "进行体力活动"]
+ case .fear:
+ return ["面对恐惧", "寻求专业建议", "制定应对计划"]
+ case .surprise:
+ return ["接受变化", "保持开放心态", "记录感受"]
+ case .neutral:
+ return ["享受平静", "进行自我反思", "设定新目标"]
+ case .anxiety:
+ return ["深呼吸练习", "寻求支持", "制定应对计划"]
+ case .excitement:
+ return ["合理安排时间", "保持专注", "与他人分享喜悦"]
+ case .contentment:
+ return ["珍惜当下", "保持感恩心", "分享你的平和"]
+ case .confusion:
+ return ["整理思路", "寻求建议", "分步骤思考"]
+ case .melancholy:
+ return ["接受这种情绪", "寻找美好的事物", "与朋友交流"]
+ }
+ }
+
+ private func generateSuggestions(for emotion: EmotionType) -> [String] {
+ switch emotion {
+ case .joy:
+ return ["继续保持积极心态", "做些让你快乐的事"]
+ case .sadness:
+ return ["给自己一些时间", "考虑专业帮助"]
+ case .anger:
+ return ["找到健康的发泄方式", "思考问题的解决方案"]
+ case .fear:
+ return ["一步步面对恐惧", "建立支持系统"]
+ case .surprise:
+ return ["适应新情况", "保持灵活性"]
+ case .neutral:
+ return ["探索新的兴趣", "建立日常习惯"]
+ case .anxiety:
+ return ["学习放松技巧", "寻求专业帮助"]
+ case .excitement:
+ return ["制定行动计划", "保持理性思考"]
+ case .contentment:
+ return ["维持内心平衡", "继续当前的生活方式"]
+ case .confusion:
+ return ["寻找清晰的方向", "与他人交流想法"]
+ case .melancholy:
+ return ["接受当下的感受", "寻找内心的平静"]
+ }
+ }
+
+ private func generateFollowUpQuestions(for emotion: EmotionType) -> [String] {
+ switch emotion {
+ case .joy:
+ return ["是什么特别的事情让你如此开心?", "你想如何延续这种快乐?"]
+ case .sadness:
+ return ["这种感受持续多久了?", "有什么可以帮助你感觉好一些?"]
+ case .anger:
+ return ["什么事情触发了这种愤怒?", "你通常如何处理愤怒情绪?"]
+ case .fear:
+ return ["你最担心的是什么?", "有什么可以让你感到更安全?"]
+ case .surprise:
+ return ["这个意外对你意味着什么?", "你如何适应这个变化?"]
+ case .neutral:
+ return ["最近有什么新的想法吗?", "有什么目标想要实现?"]
+ case .anxiety:
+ return ["这种焦虑从什么时候开始的?", "有什么特别担心的事情吗?"]
+ case .excitement:
+ return ["是什么让你如此兴奋?", "你计划如何行动?"]
+ case .contentment:
+ return ["是什么让你感到如此满足?", "这种状态对你意味着什么?"]
+ case .confusion:
+ return ["什么让你感到困惑?", "需要帮助理清哪些思路?"]
+ case .melancholy:
+ return ["这种忧郁感从何而来?", "有什么特别的回忆或想法吗?"]
+ }
+ }
+}
\ No newline at end of file
diff --git a/EmotionMuseum/EmotionMuseum/Services/MockDataManager.swift b/EmotionMuseum/EmotionMuseum/Services/MockDataManager.swift
new file mode 100644
index 0000000..422ad9c
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Services/MockDataManager.swift
@@ -0,0 +1,701 @@
+//
+// MockDataManager.swift
+// EmotionMuseum
+//
+// Created by 华中敏 on 2025/7/5.
+//
+
+import Foundation
+import SwiftUI
+
+class MockDataManager: ObservableObject {
+ static let shared = MockDataManager()
+
+ // MARK: - Published Properties
+ @Published var currentUser: User
+ @Published var conversations: [Conversation] = []
+ @Published var growthTopics: [GrowthTopic] = []
+ @Published var locationPins: [LocationPin] = []
+ @Published var communityPosts: [CommunityPost] = []
+ @Published var emotionRecords: [EmotionRecord] = []
+ @Published var achievements: [Achievement] = []
+ @Published var userStats: UserStats = UserStats()
+ @Published var weeklyStats: WeeklyStats
+
+ private init() {
+ // 初始化当前用户
+ self.currentUser = User(
+ username: "emotion_explorer",
+ email: "user@example.com",
+ profile: UserProfile(
+ nickname: "情绪探索者",
+ birthDate: Calendar.current.date(byAdding: .year, value: -25, to: Date()),
+ location: "北京市",
+ bio: "在情绪的海洋中寻找内心的平静",
+ memberLevel: .premium,
+ totalDays: 127,
+ growthStats: GrowthStats(
+ selfAwareness: 78.5,
+ emotionalResilience: 65.2,
+ actionPower: 72.8,
+ empathy: 85.3,
+ lifeEnthusiasm: 69.7
+ )
+ ),
+ createdAt: Calendar.current.date(byAdding: .day, value: -127, to: Date()) ?? Date()
+ )
+
+ // 初始化本周统计
+ let weekStart = Calendar.current.dateInterval(of: .weekOfYear, for: Date())?.start ?? Date()
+ self.weeklyStats = WeeklyStats(weekStartDate: weekStart)
+
+ // 生成所有模拟数据
+ generateAllMockData()
+ }
+
+ // MARK: - Public Methods
+
+ func generateAllMockData() {
+ generateMockEmotionRecords()
+ generateMockConversations()
+ generateMockGrowthTopics()
+ generateMockLocationPins()
+ generateMockCommunityPosts()
+ generateMockAchievements()
+ updateUserStats()
+ updateWeeklyStats()
+ }
+
+ func refreshData() async {
+ await MainActor.run {
+ generateAllMockData()
+ }
+ }
+
+ // MARK: - Conversation Methods
+
+ func addMessage(to conversationId: UUID, content: String, sender: MessageSender) {
+ if let index = conversations.firstIndex(where: { $0.id == conversationId }) {
+ let message = Message(
+ conversationId: conversationId,
+ content: content,
+ type: .text,
+ sender: sender,
+ emotionScore: sender == .user ? Float.random(in: 0.3...0.9) : nil
+ )
+ conversations[index].messages.append(message)
+
+ if sender == .user {
+ // 模拟AI回复
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
+ let aiResponse = self.generateAIResponse(for: content)
+ self.addMessage(to: conversationId, content: aiResponse, sender: .ai)
+ }
+ }
+ }
+ }
+
+ func createNewConversation() -> Conversation {
+ let conversation = Conversation(
+ userId: currentUser.id,
+ title: "新对话 \(Date().shortFormat)"
+ )
+ conversations.insert(conversation, at: 0)
+ return conversation
+ }
+
+ // MARK: - Growth Topic Methods
+
+ func updateTopicProgress(_ topicId: UUID, progress: Float) {
+ if let index = growthTopics.firstIndex(where: { $0.id == topicId }) {
+ growthTopics[index].progress = min(1.0, progress)
+ if growthTopics[index].progress >= 1.0 {
+ growthTopics[index].completedAt = Date()
+ // 解锁奖励
+ let reward = Reward(
+ type: .points,
+ title: "课题完成",
+ description: "完成了\(growthTopics[index].title)",
+ value: 100,
+ rarity: .common
+ )
+ growthTopics[index].rewards.append(reward)
+ }
+ }
+ }
+
+ func addTopicInteraction(_ topicId: UUID, type: InteractionType, title: String, content: String) {
+ if let index = growthTopics.firstIndex(where: { $0.id == topicId }) {
+ let interaction = TopicInteraction(
+ topicId: topicId,
+ type: type,
+ title: title,
+ content: content,
+ completedAt: Date(),
+ duration: TimeInterval.random(in: 300...1800)
+ )
+ growthTopics[index].interactions.append(interaction)
+
+ // 更新进度
+ let progressIncrease: Float = 0.2
+ updateTopicProgress(topicId, progress: growthTopics[index].progress + progressIncrease)
+ }
+ }
+
+ // MARK: - Location Methods
+
+ func toggleLocationBookmark(_ locationId: UUID) {
+ if let index = locationPins.firstIndex(where: { $0.id == locationId }) {
+ locationPins[index].isBookmarked.toggle()
+ }
+ }
+
+ func addLocationVisit(_ locationId: UUID) {
+ if let index = locationPins.firstIndex(where: { $0.id == locationId }) {
+ locationPins[index].visits += 1
+ }
+ }
+
+ // MARK: - Community Methods
+
+ func togglePostLike(_ postId: UUID) {
+ if let index = communityPosts.firstIndex(where: { $0.id == postId }) {
+ communityPosts[index].isLikedByCurrentUser.toggle()
+ if communityPosts[index].isLikedByCurrentUser {
+ communityPosts[index].likes += 1
+ } else {
+ communityPosts[index].likes = max(0, communityPosts[index].likes - 1)
+ }
+ }
+ }
+
+ func addComment(to postId: UUID, content: String) {
+ if let index = communityPosts.firstIndex(where: { $0.id == postId }) {
+ let comment = Comment(
+ postId: postId,
+ userId: currentUser.id,
+ content: content
+ )
+ communityPosts[index].comments.append(comment)
+ }
+ }
+
+ // MARK: - Private Data Generation Methods
+
+ private func generateMockEmotionRecords() {
+ emotionRecords.removeAll()
+
+ // 生成过去30天的情绪记录
+ for i in 0..<30 {
+ let date = Calendar.current.date(byAdding: .day, value: -i, to: Date()) ?? Date()
+ let recordCount = Int.random(in: 1...3) // 每天1-3条记录
+
+ for _ in 0.. $1.date }
+ }
+
+ private func generateMockConversations() {
+ conversations.removeAll()
+
+ // 生成过去30天的对话记录
+ for i in 0..<15 {
+ let startDate = Calendar.current.date(byAdding: .day, value: -i*2, to: Date()) ?? Date()
+ let conversation = createMockConversation(for: startDate, index: i)
+ conversations.append(conversation)
+ }
+
+ conversations.sort { $0.startTime > $1.startTime }
+ }
+
+ private func generateMockGrowthTopics() {
+ growthTopics.removeAll()
+
+ // 为每个分类生成3-4个课题
+ for category in TopicCategory.allCases {
+ for i in 1...4 {
+ let topic = createMockTopic(category: category, index: i)
+ growthTopics.append(topic)
+ }
+ }
+ }
+
+ private func generateMockLocationPins() {
+ locationPins.removeAll()
+
+ // 北京地区的知名地点
+ let beijingLocations = [
+ (39.9042, 116.4074, "天安门广场", "北京的心脏,见证历史的地方"),
+ (39.9163, 116.3972, "故宫博物院", "明清两代的皇家宫殿,文化瑰宝"),
+ (40.0031, 116.3272, "颐和园", "清代皇家园林,山水如画"),
+ (39.8844, 116.5564, "798艺术区", "现代艺术的聚集地,创意无限"),
+ (39.9389, 116.3467, "什刹海", "老北京的韵味,夜晚格外美丽"),
+ (39.9056, 116.3913, "南锣鼓巷", "胡同文化的代表,小资情调"),
+ (40.0090, 116.2755, "香山公园", "秋天赏红叶的绝佳去处"),
+ (39.8838, 116.4649, "朝阳公园", "都市中的绿洲,休闲好去处"),
+ (39.9280, 116.3835, "后海", "酒吧一条街,夜生活的天堂"),
+ (39.9170, 116.3970, "景山公园", "俯瞰紫禁城的最佳位置")
+ ]
+
+ for (_, location) in beijingLocations.enumerated() {
+ let pin = LocationPin(
+ coordinate: Coordinate(latitude: location.0, longitude: location.1),
+ title: location.2,
+ description: location.3,
+ type: LocationType.allCases.randomElement() ?? .community,
+ emotionTags: Array(EmotionType.allCases.shuffled().prefix(Int.random(in: 1...3))),
+ photos: generateLocationPhotos(),
+ createdBy: Bool.random() ? currentUser.id : UUID(),
+ createdAt: Calendar.current.date(byAdding: .day, value: -Int.random(in: 1...90), to: Date()) ?? Date(),
+ likes: Int.random(in: 5...200),
+ visits: Int.random(in: 10...500),
+ address: "北京市" + ["东城区", "西城区", "朝阳区", "海淀区"].randomElement()!,
+ category: LocationCategory.allCases.randomElement() ?? .other,
+ isBookmarked: Bool.random()
+ )
+ locationPins.append(pin)
+ }
+ }
+
+ private func generateMockCommunityPosts() {
+ communityPosts.removeAll()
+
+ for _ in 0..<20 {
+ let post = CommunityPost(
+ userId: Bool.random() ? currentUser.id : UUID(),
+ locationId: locationPins.randomElement()?.id,
+ content: generatePostContent(),
+ photos: generatePostPhotos(),
+ tags: generatePostTags(),
+ likes: Int.random(in: 0...100),
+ comments: generatePostComments(),
+ createdAt: Calendar.current.date(byAdding: .hour, value: -Int.random(in: 1...168), to: Date()) ?? Date(),
+ isPrivate: Bool.random(),
+ viewCount: Int.random(in: 10...1000),
+ type: PostType.allCases.randomElement() ?? .general,
+ isLikedByCurrentUser: Bool.random()
+ )
+ communityPosts.append(post)
+ }
+
+ communityPosts.sort { $0.createdAt > $1.createdAt }
+ }
+
+ private func generateMockAchievements() {
+ achievements.removeAll()
+
+ let achievementData: [(String, String, AchievementCategory, String, RewardRarity, AchievementRequirement, Int, Bool)] = [
+ ("初次对话", "完成第一次AI对话", .conversation, "message.circle", .common, .conversationCount(1), 1, false),
+ ("话痨达人", "累计对话100次", .conversation, "message.badge", .rare, .conversationCount(100), 45, false),
+ ("情绪记录者", "记录50次情绪", .emotion, "heart.circle", .common, .emotionRecordCount(50), 32, false),
+ ("成长新手", "完成第一个成长课题", .growth, "arrow.up.circle", .common, .topicCompletion(1), 1, false),
+ ("社交达人", "获得100个点赞", .social, "heart.badge", .rare, .socialInteraction(100), 67, false),
+ ("探索者", "访问10个地点", .exploration, "map.circle", .common, .locationVisit(10), 8, false),
+ ("坚持不懈", "连续使用30天", .consistency, "calendar.badge", .epic, .consecutiveDays(30), 25, false),
+ ("积分大户", "累计获得1000积分", .milestone, "star.badge", .rare, .totalPoints(1000), 750, false),
+ ("神秘成就", "发现隐藏彩蛋", .special, "sparkles", .legendary, .special("发现应用中的隐藏彩蛋"), 0, true)
+ ]
+
+ for data in achievementData {
+ let achievement = Achievement(
+ title: data.0,
+ description: data.1,
+ category: data.2,
+ icon: data.3,
+ rarity: data.4,
+ requirement: data.5,
+ progress: data.6,
+ targetValue: getTargetValue(for: data.5),
+ unlockedAt: data.6 >= getTargetValue(for: data.5) ? Date() : nil,
+ isHidden: data.7
+ )
+ achievements.append(achievement)
+ }
+ }
+
+ // MARK: - Helper Methods
+
+ private func createMockConversation(for date: Date, index: Int) -> Conversation {
+ let conversation = Conversation(
+ userId: currentUser.id,
+ title: generateConversationTitle(index: index),
+ startTime: date,
+ endTime: Calendar.current.date(byAdding: .minute, value: Int.random(in: 5...60), to: date),
+ tags: generateConversationTags()
+ )
+
+ // 添加消息
+ var messages: [Message] = []
+ let messageCount = Int.random(in: 4...12)
+
+ for i in 0.. GrowthTopic {
+ let topicTitles = getTopicTitles(for: category)
+ let title = topicTitles[min(index - 1, topicTitles.count - 1)]
+
+ return GrowthTopic(
+ title: title,
+ description: generateTopicDescription(for: title),
+ category: category,
+ difficulty: Difficulty.allCases.randomElement() ?? .beginner,
+ progress: Float.random(in: 0...1),
+ level: Int.random(in: 1...5),
+ totalLevels: 5,
+ isUnlocked: index <= 2 || Bool.random(), // 前两个总是解锁的
+ completedAt: Float.random(in: 0...1) > 0.7 ? Date() : nil,
+ rewards: generateTopicRewards(),
+ interactions: generateTopicInteractions(),
+ estimatedDuration: TimeInterval.random(in: 1800...7200), // 30分钟到2小时
+ prerequisites: index > 1 ? [UUID()] : []
+ )
+ }
+
+ private func generateAIResponse(for userMessage: String) -> String {
+ let responses = [
+ "我理解你的感受,这确实是一个值得思考的问题。",
+ "你提到的这个情况很常见,让我们一起来分析一下。",
+ "从你的描述中,我感受到了你的情绪变化。",
+ "这是一个很好的观察,你有什么想法吗?",
+ "我听到了你的担忧,我们可以一步步来解决。",
+ "你的感受是完全可以理解的,很多人都会有类似的经历。",
+ "让我们换个角度来看这个问题,可能会有新的发现。",
+ "你已经很勇敢地表达了自己的想法,这很棒。",
+ "我注意到你提到了一些关键词,我们可以深入探讨一下。",
+ "你的情绪管理能力在不断提升,继续保持。"
+ ]
+ return responses.randomElement() ?? "谢谢你的分享。"
+ }
+
+ // MARK: - Content Generation Methods
+
+ private func generateEmotionContext() -> String {
+ let contexts = [
+ "工作压力让我感到疲惫",
+ "和朋友聊天后心情变好了",
+ "看到美丽的日落感到平静",
+ "遇到挫折时感到沮丧",
+ "完成任务后有成就感",
+ "听音乐时情绪放松",
+ "运动后感到充满活力",
+ "独处时思考人生",
+ "与家人团聚很温暖",
+ "面对未知感到紧张"
+ ]
+ return contexts.randomElement() ?? "日常生活中的情绪体验"
+ }
+
+ private func generateEmotionTriggers() -> [String] {
+ let allTriggers = ["工作", "人际关系", "健康", "家庭", "学习", "金钱", "未来", "过去", "天气", "音乐"]
+ return Array(allTriggers.shuffled().prefix(Int.random(in: 1...3)))
+ }
+
+ private func generateEmotionNotes() -> String? {
+ let notes = [
+ "今天的情绪比昨天好一些",
+ "需要更多的休息时间",
+ "和朋友的谈话很有帮助",
+ "运动确实能改善心情",
+ "要学会接受自己的情绪",
+ nil, nil // 有些记录没有备注
+ ]
+ return notes.randomElement() ?? nil
+ }
+
+ private func generateConversationTitle(index: Int) -> String {
+ let titles = [
+ "今天的心情分享",
+ "关于压力管理的讨论",
+ "人际关系的困惑",
+ "职场焦虑的缓解",
+ "自我成长的反思",
+ "情绪调节的方法",
+ "生活目标的规划",
+ "内心平静的追求",
+ "人生意义的探索",
+ "幸福感的提升"
+ ]
+ return titles[index % titles.count]
+ }
+
+ private func generateConversationTags() -> [String] {
+ let allTags = ["情绪管理", "压力缓解", "人际关系", "自我成长", "生活规划", "心理健康"]
+ return Array(allTags.shuffled().prefix(Int.random(in: 1...3)))
+ }
+
+ private func generateUserMessage() -> String {
+ let messages = [
+ "我最近感到有些焦虑,不知道该怎么办。",
+ "工作压力很大,总是担心做不好。",
+ "和朋友的关系出现了一些问题。",
+ "我想要改变现在的生活状态。",
+ "有时候感到很孤独,需要有人倾听。",
+ "对未来感到不确定,有些迷茫。",
+ "今天心情不错,想分享一下。",
+ "我在思考人生的意义是什么。",
+ "想要培养一些新的习惯。",
+ "感觉自己需要更多的自信。"
+ ]
+ return messages.randomElement() ?? "你好"
+ }
+
+ private func generateAIMessage() -> String {
+ let messages = [
+ "我理解你的感受,焦虑是很正常的情绪反应。",
+ "工作压力确实会影响我们的心情,不妨试试放松技巧。",
+ "人际关系需要时间和耐心来维护,你做得很好。",
+ "改变需要勇气,你已经迈出了第一步。",
+ "孤独感是人类共同的体验,你并不孤单。",
+ "对未来的不确定感是成长的一部分。",
+ "很高兴听到你今天心情不错!",
+ "人生意义的探索是一个持续的过程。",
+ "培养新习惯需要时间,要对自己有耐心。",
+ "自信是可以通过练习来培养的。"
+ ]
+ return messages.randomElement() ?? "谢谢你的分享"
+ }
+
+ private func generateEmotionKeywords() -> [String] {
+ let keywords = ["压力", "焦虑", "快乐", "悲伤", "希望", "困惑", "平静", "兴奋", "担忧", "满足"]
+ return Array(keywords.shuffled().prefix(Int.random(in: 2...4)))
+ }
+
+ private func generateAIInsights() -> String {
+ let insights = [
+ "你的情绪表达能力在不断提升,这是很好的进步。",
+ "从对话中可以看出你对自我成长很有意识。",
+ "你善于反思,这有助于情绪的自我调节。",
+ "你的积极态度值得赞赏,继续保持。",
+ "建议多关注自己的情绪变化模式。",
+ "你的表达很真诚,这有助于深入的自我探索。"
+ ]
+ return insights.randomElement() ?? "继续保持这种开放的态度"
+ }
+
+ private func getTopicTitles(for category: TopicCategory) -> [String] {
+ switch category {
+ case .selfAwareness:
+ return ["认识真实的自己", "探索内在价值观", "发现个人优势", "理解情绪模式"]
+ case .emotionRegulation:
+ return ["压力管理技巧", "愤怒情绪调节", "焦虑缓解方法", "悲伤情绪处理"]
+ case .socialSkills:
+ return ["有效沟通技巧", "建立良好关系", "冲突解决能力", "团队合作精神"]
+ case .stressManagement:
+ return ["工作压力应对", "时间管理技能", "放松训练方法", "心理韧性建设"]
+ case .lifeGoals:
+ return ["目标设定方法", "人生规划技巧", "价值观澄清", "意义感培养"]
+ case .mindfulness:
+ return ["正念冥想入门", "专注力训练", "当下觉察练习", "内心平静修炼"]
+ case .relationships:
+ return ["亲密关系维护", "友谊经营之道", "家庭和谐相处", "社交边界设定"]
+ case .creativity:
+ return ["创意思维开发", "艺术表达练习", "问题解决创新", "想象力激发"]
+ }
+ }
+
+ private func generateTopicDescription(for title: String) -> String {
+ return "通过系统化的学习和练习,帮助你在\(title)方面获得提升。包含理论知识、实践练习和个人反思,让你在成长的道路上更进一步。"
+ }
+
+ private func generateTopicRewards() -> [Reward] {
+ let rewardCount = Int.random(in: 0...2)
+ var rewards: [Reward] = []
+
+ for _ in 0.. [TopicInteraction] {
+ let interactionCount = Int.random(in: 0...3)
+ var interactions: [TopicInteraction] = []
+
+ for i in 0.. [String] {
+ let photoCount = Int.random(in: 1...4)
+ return Array(repeating: "location_photo", count: photoCount)
+ }
+
+ private func generatePostContent() -> String {
+ let contents = [
+ "今天在这个美丽的地方找到了内心的平静 ✨",
+ "和朋友一起度过了愉快的下午时光 😊",
+ "这里的风景让我想起了童年的回忆",
+ "在这个安静的角落里思考人生的意义",
+ "发现了一个治愈心灵的好地方",
+ "阳光透过树叶洒下来,心情瞬间明亮了",
+ "和陌生人的一次偶遇,让我对生活有了新的感悟",
+ "在这里感受到了城市中难得的宁静",
+ "每次来这里都能获得新的启发",
+ "分享一个让我感到温暖的瞬间"
+ ]
+ return contents.randomElement() ?? "分享今天的美好时光"
+ }
+
+ private func generatePostPhotos() -> [String] {
+ let photoCount = Int.random(in: 0...3)
+ return Array(repeating: "post_photo", count: photoCount)
+ }
+
+ private func generatePostTags() -> [String] {
+ let allTags = ["治愈", "美好", "分享", "生活", "感悟", "风景", "友谊", "成长", "平静", "温暖"]
+ return Array(allTags.shuffled().prefix(Int.random(in: 1...3)))
+ }
+
+ private func generatePostComments() -> [Comment] {
+ let commentCount = Int.random(in: 0...5)
+ var comments: [Comment] = []
+
+ let commentContents = [
+ "太美了!",
+ "我也想去这个地方",
+ "感谢分享 ❤️",
+ "很有感触",
+ "下次一起去吧",
+ "照片拍得很棒",
+ "这个地方我也去过",
+ "很治愈的分享"
+ ]
+
+ for _ in 0.. Int {
+ switch requirement {
+ case .conversationCount(let count): return count
+ case .emotionRecordCount(let count): return count
+ case .topicCompletion(let count): return count
+ case .socialInteraction(let count): return count
+ case .locationVisit(let count): return count
+ case .consecutiveDays(let days): return days
+ case .totalPoints(let points): return points
+ case .special(_): return 1
+ }
+ }
+
+ private func updateUserStats() {
+ userStats.totalConversations = conversations.count
+ userStats.totalMessages = conversations.reduce(0) { $0 + $1.messageCount }
+ userStats.totalEmotionRecords = emotionRecords.count
+ userStats.completedTopics = growthTopics.filter { $0.isCompleted }.count
+ userStats.totalPoints = achievements.reduce(0) { $0 + ($1.isUnlocked ? 100 : 0) }
+ userStats.consecutiveDays = 25 // 模拟连续使用天数
+ userStats.maxConsecutiveDays = 30
+ userStats.socialInteractions = communityPosts.reduce(0) { $0 + $1.likes + $1.commentCount }
+ userStats.locationsVisited = locationPins.filter { $0.visits > 0 }.count
+ userStats.postsCreated = communityPosts.filter { $0.userId == currentUser.id }.count
+ userStats.likesReceived = communityPosts.filter { $0.userId == currentUser.id }.reduce(0) { $0 + $1.likes }
+ userStats.commentsReceived = communityPosts.filter { $0.userId == currentUser.id }.reduce(0) { $0 + $1.commentCount }
+ }
+
+ private func updateWeeklyStats() {
+ let weekStart = weeklyStats.weekStartDate
+ let weekEnd = Calendar.current.date(byAdding: .day, value: 6, to: weekStart) ?? weekStart
+
+ // 本周情绪记录
+ weeklyStats.emotionRecords = emotionRecords.filter { record in
+ record.date >= weekStart && record.date <= weekEnd
+ }
+
+ // 本周对话
+ weeklyStats.conversations = conversations.filter { conversation in
+ conversation.startTime >= weekStart && conversation.startTime <= weekEnd
+ }
+
+ // 计算平均情绪分数
+ if !weeklyStats.emotionRecords.isEmpty {
+ weeklyStats.averageMoodScore = weeklyStats.emotionRecords.averageIntensity()
+ weeklyStats.dominantEmotion = weeklyStats.emotionRecords.dominantEmotion()
+ }
+
+ // 确定情绪趋势
+ weeklyStats.moodTrend = EmotionTrend.allCases.randomElement() ?? .stable
+
+ // 最活跃的一天
+ let dailyActivity = Dictionary(grouping: weeklyStats.emotionRecords + weeklyStats.conversations.map { conversation in
+ EmotionRecord(userId: currentUser.id, date: conversation.startTime, emotionType: .neutral, intensity: 0.5, context: "对话活动")
+ }, by: { Calendar.current.startOfDay(for: $0.date) })
+
+ weeklyStats.mostActiveDay = dailyActivity.max(by: { $0.value.count < $1.value.count })?.key
+ }
+}
\ No newline at end of file
diff --git a/EmotionMuseum/EmotionMuseum/Services/NavigationManager.swift b/EmotionMuseum/EmotionMuseum/Services/NavigationManager.swift
new file mode 100644
index 0000000..0cdee7d
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Services/NavigationManager.swift
@@ -0,0 +1,411 @@
+//
+// NavigationManager.swift
+// EmotionMuseum
+//
+// Created by 华中敏 on 2025/7/5.
+//
+
+import Foundation
+import SwiftUI
+
+class NavigationManager: ObservableObject {
+ // MARK: - Tab Navigation
+ @Published var currentTab: MainTab = .record
+
+ // MARK: - Navigation Paths for each tab
+ @Published var recordNavigation = NavigationPath()
+ @Published var growthNavigation = NavigationPath()
+ @Published var exploreNavigation = NavigationPath()
+ @Published var insightNavigation = NavigationPath()
+
+ // MARK: - Global Modal States
+ @Published var showingChatHistory = false
+ @Published var showingFullScreenChat = false
+ @Published var showingSettings = false
+ @Published var showingProfile = false
+ @Published var showingThemeSettings = false
+ @Published var showingAddLocation = false
+ @Published var showingTopicDetail = false
+ @Published var showingLocationDetail = false
+ @Published var showingPostDetail = false
+ @Published var showingAchievements = false
+ @Published var showingMemberCenter = false
+
+ // MARK: - Current Selection States
+ @Published var selectedConversation: Conversation?
+ @Published var selectedTopic: GrowthTopic?
+ @Published var selectedLocation: LocationPin?
+ @Published var selectedPost: CommunityPost?
+ @Published var selectedAchievement: Achievement?
+
+ // MARK: - Chat States
+ @Published var currentChatConversation: Conversation?
+ @Published var isVoiceMode = false
+ @Published var chatInputText = ""
+
+ // MARK: - Loading States
+ @Published var isLoading = false
+ @Published var loadingMessage = ""
+
+ // MARK: - Navigation Methods
+
+ func navigateToTab(_ tab: MainTab) {
+ withAnimation(.easeInOut(duration: 0.3)) {
+ currentTab = tab
+ }
+ }
+
+ func navigateToChat(conversation: Conversation? = nil) {
+ selectedConversation = conversation
+ currentChatConversation = conversation
+ showingFullScreenChat = true
+ }
+
+ func navigateToTopic(_ topic: GrowthTopic) {
+ selectedTopic = topic
+ showingTopicDetail = true
+ }
+
+ func navigateToLocation(_ location: LocationPin) {
+ selectedLocation = location
+ showingLocationDetail = true
+ }
+
+ func navigateToPost(_ post: CommunityPost) {
+ selectedPost = post
+ showingPostDetail = true
+ }
+
+ func navigateToProfile() {
+ showingProfile = true
+ }
+
+ func navigateToSettings() {
+ showingSettings = true
+ }
+
+ func navigateToThemeSettings() {
+ showingThemeSettings = true
+ }
+
+ func navigateToAchievements() {
+ showingAchievements = true
+ }
+
+ func navigateToMemberCenter() {
+ showingMemberCenter = true
+ }
+
+ func showAddLocation() {
+ showingAddLocation = true
+ }
+
+ func showChatHistory() {
+ showingChatHistory = true
+ }
+
+ // MARK: - Dismiss Methods
+
+ func dismissAllModals() {
+ showingChatHistory = false
+ showingFullScreenChat = false
+ showingSettings = false
+ showingProfile = false
+ showingThemeSettings = false
+ showingAddLocation = false
+ showingTopicDetail = false
+ showingLocationDetail = false
+ showingPostDetail = false
+ showingAchievements = false
+ showingMemberCenter = false
+
+ selectedConversation = nil
+ selectedTopic = nil
+ selectedLocation = nil
+ selectedPost = nil
+ selectedAchievement = nil
+ currentChatConversation = nil
+ }
+
+ func dismissCurrentModal() {
+ if showingFullScreenChat {
+ showingFullScreenChat = false
+ currentChatConversation = nil
+ } else if showingChatHistory {
+ showingChatHistory = false
+ } else if showingSettings {
+ showingSettings = false
+ } else if showingProfile {
+ showingProfile = false
+ } else if showingThemeSettings {
+ showingThemeSettings = false
+ } else if showingAddLocation {
+ showingAddLocation = false
+ } else if showingTopicDetail {
+ showingTopicDetail = false
+ selectedTopic = nil
+ } else if showingLocationDetail {
+ showingLocationDetail = false
+ selectedLocation = nil
+ } else if showingPostDetail {
+ showingPostDetail = false
+ selectedPost = nil
+ } else if showingAchievements {
+ showingAchievements = false
+ } else if showingMemberCenter {
+ showingMemberCenter = false
+ }
+ }
+
+ // MARK: - Deep Link Methods
+
+ func handleDeepLink(url: URL) {
+ // 处理深度链接,例如从通知或分享链接打开特定页面
+ guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return }
+
+ switch components.host {
+ case "conversation":
+ if let conversationId = components.queryItems?.first(where: { $0.name == "id" })?.value,
+ let uuid = UUID(uuidString: conversationId) {
+ // 查找并打开对应的对话
+ let conversation = MockDataManager.shared.conversations.first { $0.id == uuid }
+ navigateToChat(conversation: conversation)
+ }
+
+ case "topic":
+ if let topicId = components.queryItems?.first(where: { $0.name == "id" })?.value,
+ let uuid = UUID(uuidString: topicId) {
+ // 查找并打开对应的成长课题
+ let topic = MockDataManager.shared.growthTopics.first { $0.id == uuid }
+ if let topic = topic {
+ navigateToTab(.growth)
+ navigateToTopic(topic)
+ }
+ }
+
+ case "location":
+ if let locationId = components.queryItems?.first(where: { $0.name == "id" })?.value,
+ let uuid = UUID(uuidString: locationId) {
+ // 查找并打开对应的地点
+ let location = MockDataManager.shared.locationPins.first { $0.id == uuid }
+ if let location = location {
+ navigateToTab(.explore)
+ navigateToLocation(location)
+ }
+ }
+
+ case "post":
+ if let postId = components.queryItems?.first(where: { $0.name == "id" })?.value,
+ let uuid = UUID(uuidString: postId) {
+ // 查找并打开对应的帖子
+ let post = MockDataManager.shared.communityPosts.first { $0.id == uuid }
+ if let post = post {
+ navigateToTab(.explore)
+ navigateToPost(post)
+ }
+ }
+
+ default:
+ break
+ }
+ }
+
+ // MARK: - Loading Management
+
+ func showLoading(_ message: String = "加载中...") {
+ loadingMessage = message
+ withAnimation(.easeInOut) {
+ isLoading = true
+ }
+ }
+
+ func hideLoading() {
+ withAnimation(.easeInOut) {
+ isLoading = false
+ }
+ loadingMessage = ""
+ }
+
+ // MARK: - Chat Management
+
+ func sendMessage(_ content: String, to conversation: Conversation? = nil) {
+ guard !content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
+
+ let targetConversation = conversation ?? currentChatConversation
+
+ if let conv = targetConversation {
+ MockDataManager.shared.addMessage(to: conv.id, content: content, sender: .user)
+ } else {
+ // 创建新对话
+ let newConversation = MockDataManager.shared.createNewConversation()
+ currentChatConversation = newConversation
+ MockDataManager.shared.addMessage(to: newConversation.id, content: content, sender: .user)
+ }
+
+ chatInputText = ""
+ }
+
+ func toggleVoiceMode() {
+ withAnimation(.easeInOut(duration: 0.2)) {
+ isVoiceMode.toggle()
+ }
+ }
+
+ // MARK: - Utility Methods
+
+ func clearNavigationPath(for tab: MainTab) {
+ switch tab {
+ case .record:
+ recordNavigation = NavigationPath()
+ case .growth:
+ growthNavigation = NavigationPath()
+ case .explore:
+ exploreNavigation = NavigationPath()
+ case .insight:
+ insightNavigation = NavigationPath()
+ }
+ }
+
+ func popToRoot(for tab: MainTab) {
+ clearNavigationPath(for: tab)
+ dismissAllModals()
+ }
+
+ func canGoBack(for tab: MainTab) -> Bool {
+ switch tab {
+ case .record:
+ return !recordNavigation.isEmpty
+ case .growth:
+ return !growthNavigation.isEmpty
+ case .explore:
+ return !exploreNavigation.isEmpty
+ case .insight:
+ return !insightNavigation.isEmpty
+ }
+ }
+
+ func goBack(for tab: MainTab) {
+ switch tab {
+ case .record:
+ if !recordNavigation.isEmpty {
+ recordNavigation.removeLast()
+ }
+ case .growth:
+ if !growthNavigation.isEmpty {
+ growthNavigation.removeLast()
+ }
+ case .explore:
+ if !exploreNavigation.isEmpty {
+ exploreNavigation.removeLast()
+ }
+ case .insight:
+ if !insightNavigation.isEmpty {
+ insightNavigation.removeLast()
+ }
+ }
+ }
+}
+
+// MARK: - Main Tab Enum
+
+enum MainTab: String, CaseIterable {
+ case record = "记录"
+ case growth = "治愈"
+ case explore = "探索"
+ case insight = "我的"
+
+ var title: String {
+ return self.rawValue
+ }
+
+ var icon: String {
+ switch self {
+ case .record: return "brain.head.profile"
+ case .growth: return "heart"
+ case .explore: return "map"
+ case .insight: return "person"
+ }
+ }
+
+ var selectedIcon: String {
+ switch self {
+ case .record: return "brain.head.profile.fill"
+ case .growth: return "heart.fill"
+ case .explore: return "map.fill"
+ case .insight: return "person.fill"
+ }
+ }
+
+ var color: Color {
+ switch self {
+ case .record: return .blue
+ case .growth: return .pink
+ case .explore: return .green
+ case .insight: return .orange
+ }
+ }
+}
+
+// MARK: - Navigation Destination Types
+
+enum NavigationDestination: Hashable {
+ case conversation(Conversation)
+ case topic(GrowthTopic)
+ case location(LocationPin)
+ case post(CommunityPost)
+ case achievement(Achievement)
+ case settings
+ case profile
+ case chatHistory
+ case addLocation
+
+ static func == (lhs: NavigationDestination, rhs: NavigationDestination) -> Bool {
+ switch (lhs, rhs) {
+ case (.conversation(let lhsConv), .conversation(let rhsConv)):
+ return lhsConv.id == rhsConv.id
+ case (.topic(let lhsTopic), .topic(let rhsTopic)):
+ return lhsTopic.id == rhsTopic.id
+ case (.location(let lhsLoc), .location(let rhsLoc)):
+ return lhsLoc.id == rhsLoc.id
+ case (.post(let lhsPost), .post(let rhsPost)):
+ return lhsPost.id == rhsPost.id
+ case (.achievement(let lhsAch), .achievement(let rhsAch)):
+ return lhsAch.id == rhsAch.id
+ case (.settings, .settings),
+ (.profile, .profile),
+ (.chatHistory, .chatHistory),
+ (.addLocation, .addLocation):
+ return true
+ default:
+ return false
+ }
+ }
+
+ func hash(into hasher: inout Hasher) {
+ switch self {
+ case .conversation(let conv):
+ hasher.combine("conversation")
+ hasher.combine(conv.id)
+ case .topic(let topic):
+ hasher.combine("topic")
+ hasher.combine(topic.id)
+ case .location(let location):
+ hasher.combine("location")
+ hasher.combine(location.id)
+ case .post(let post):
+ hasher.combine("post")
+ hasher.combine(post.id)
+ case .achievement(let achievement):
+ hasher.combine("achievement")
+ hasher.combine(achievement.id)
+ case .settings:
+ hasher.combine("settings")
+ case .profile:
+ hasher.combine("profile")
+ case .chatHistory:
+ hasher.combine("chatHistory")
+ case .addLocation:
+ hasher.combine("addLocation")
+ }
+ }
+}
\ No newline at end of file
diff --git a/EmotionMuseum/EmotionMuseum/Views/AnimationComponents.swift b/EmotionMuseum/EmotionMuseum/Views/AnimationComponents.swift
new file mode 100644
index 0000000..d20a8ce
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Views/AnimationComponents.swift
@@ -0,0 +1,565 @@
+//
+// AnimationComponents.swift
+// EmotionMuseum
+//
+// Created by 华中敏 on 2025/6/13.
+//
+
+import SwiftUI
+
+// MARK: - 动画配置
+struct AnimationConfig {
+ static let springy = Animation.spring(response: 0.6, dampingFraction: 0.8)
+ static let bouncy = Animation.spring(response: 0.4, dampingFraction: 0.6)
+ static let smooth = Animation.easeInOut(duration: 0.3)
+ static let quick = Animation.easeOut(duration: 0.2)
+ static let slow = Animation.easeInOut(duration: 0.8)
+ static let elastic = Animation.interpolatingSpring(stiffness: 300, damping: 15)
+}
+
+// MARK: - 动画视图修饰符
+struct AnimatedAppearance: ViewModifier {
+ @State private var isVisible = false
+ let delay: Double
+ let animation: Animation
+
+ init(delay: Double = 0, animation: Animation = AnimationConfig.smooth) {
+ self.delay = delay
+ self.animation = animation
+ }
+
+ func body(content: Content) -> some View {
+ content
+ .scaleEffect(isVisible ? 1 : 0.8)
+ .opacity(isVisible ? 1 : 0)
+ .offset(y: isVisible ? 0 : 20)
+ .onAppear {
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
+ withAnimation(animation) {
+ isVisible = true
+ }
+ }
+ }
+ }
+}
+
+struct AnimatedCounter: ViewModifier {
+ @State private var currentValue: Int = 0
+ let targetValue: Int
+ let duration: Double
+
+ init(targetValue: Int, duration: Double = 1.0) {
+ self.targetValue = targetValue
+ self.duration = duration
+ }
+
+ func body(content: Content) -> some View {
+ Text("\(currentValue)")
+ .onAppear {
+ animateCounter()
+ }
+ .onChange(of: targetValue) { _ in
+ animateCounter()
+ }
+ }
+
+ private func animateCounter() {
+ currentValue = 0
+ let stepDuration = duration / Double(targetValue)
+
+ for i in 1...targetValue {
+ DispatchQueue.main.asyncAfter(deadline: .now() + stepDuration * Double(i)) {
+ withAnimation(.easeOut(duration: 0.1)) {
+ currentValue = i
+ }
+ }
+ }
+ }
+}
+
+// MARK: - 动画按钮
+struct AnimatedButton: View {
+ let action: () -> Void
+ let content: () -> Content
+ @State private var isPressed = false
+ @State private var scale: CGFloat = 1.0
+
+ init(action: @escaping () -> Void, @ViewBuilder content: @escaping () -> Content) {
+ self.action = action
+ self.content = content
+ }
+
+ var body: some View {
+ Button(action: {
+ impactFeedback()
+ action()
+ }) {
+ content()
+ }
+ .scaleEffect(scale)
+ .onLongPressGesture(minimumDuration: 0, maximumDistance: .infinity) { isPressing in
+ withAnimation(AnimationConfig.quick) {
+ scale = isPressing ? 0.95 : 1.0
+ isPressed = isPressing
+ }
+ } perform: {
+ // 长按完成时的动作
+ }
+ }
+
+ private func impactFeedback() {
+ let impactFeedback = UIImpactFeedbackGenerator(style: .light)
+ impactFeedback.impactOccurred()
+ }
+}
+
+// MARK: - 波纹动画
+struct RippleEffect: View {
+ @State private var animationAmount = 1.0
+ let color: Color
+ let size: CGFloat
+
+ init(color: Color = .blue, size: CGFloat = 100) {
+ self.color = color
+ self.size = size
+ }
+
+ var body: some View {
+ Circle()
+ .fill(color.opacity(0.3))
+ .frame(width: size, height: size)
+ .scaleEffect(animationAmount)
+ .opacity(2 - animationAmount)
+ .animation(
+ Animation.easeOut(duration: 1.5)
+ .repeatForever(autoreverses: false),
+ value: animationAmount
+ )
+ .onAppear {
+ animationAmount = 2
+ }
+ }
+}
+
+// MARK: - 脉冲动画
+struct PulseEffect: ViewModifier {
+ @State private var isAnimating = false
+ let color: Color
+ let size: CGFloat
+
+ init(color: Color = .blue, size: CGFloat = 1.2) {
+ self.color = color
+ self.size = size
+ }
+
+ func body(content: Content) -> some View {
+ content
+ .scaleEffect(isAnimating ? size : 1.0)
+ .animation(
+ Animation.easeInOut(duration: 1.0)
+ .repeatForever(autoreverses: true),
+ value: isAnimating
+ )
+ .onAppear {
+ isAnimating = true
+ }
+ }
+}
+
+// MARK: - 摇摆动画
+struct ShakeEffect: ViewModifier {
+ @State private var shakeOffset: CGFloat = 0
+ let intensity: CGFloat
+
+ init(intensity: CGFloat = 10) {
+ self.intensity = intensity
+ }
+
+ func body(content: Content) -> some View {
+ content
+ .offset(x: shakeOffset)
+ .onAppear {
+ withAnimation(
+ Animation.easeInOut(duration: 0.1)
+ .repeatCount(4, autoreverses: true)
+ ) {
+ shakeOffset = intensity
+ }
+ }
+ }
+}
+
+// MARK: - 流体动画背景
+struct FluidBackground: View {
+ @State private var animationOffset = 0.0
+ let colors: [Color]
+
+ init(colors: [Color] = [.blue, .purple, .pink]) {
+ self.colors = colors
+ }
+
+ var body: some View {
+ GeometryReader { geometry in
+ ZStack {
+ ForEach(0..: View {
+ let content: () -> Content
+ @State private var isVisible = false
+ let index: Int
+
+ init(index: Int, @ViewBuilder content: @escaping () -> Content) {
+ self.index = index
+ self.content = content
+ }
+
+ var body: some View {
+ content()
+ .scaleEffect(isVisible ? 1 : 0.8)
+ .opacity(isVisible ? 1 : 0)
+ .offset(x: isVisible ? 0 : -50)
+ .onAppear {
+ DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.1) {
+ withAnimation(AnimationConfig.bouncy) {
+ isVisible = true
+ }
+ }
+ }
+ }
+}
+
+// MARK: - 卡片翻转动画
+struct FlipCard: View {
+ let front: () -> Front
+ let back: () -> Back
+ @State private var isFlipped = false
+ @State private var rotation = 0.0
+
+ init(@ViewBuilder front: @escaping () -> Front, @ViewBuilder back: @escaping () -> Back) {
+ self.front = front
+ self.back = back
+ }
+
+ var body: some View {
+ ZStack {
+ if !isFlipped {
+ front()
+ } else {
+ back()
+ .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
+ }
+ }
+ .rotation3DEffect(.degrees(rotation), axis: (x: 0, y: 1, z: 0))
+ .onTapGesture {
+ withAnimation(.easeInOut(duration: 0.6)) {
+ rotation += 180
+ isFlipped.toggle()
+ }
+ }
+ }
+}
+
+// MARK: - 进度条动画
+struct AnimatedProgressBar: View {
+ let progress: Double
+ let height: CGFloat
+ let backgroundColor: Color
+ let fillColor: Color
+ @State private var animatedProgress: Double = 0
+
+ init(
+ progress: Double,
+ height: CGFloat = 8,
+ backgroundColor: Color = Color.gray.opacity(0.3),
+ fillColor: Color = .blue
+ ) {
+ self.progress = progress
+ self.height = height
+ self.backgroundColor = backgroundColor
+ self.fillColor = fillColor
+ }
+
+ var body: some View {
+ GeometryReader { geometry in
+ ZStack(alignment: .leading) {
+ Rectangle()
+ .fill(backgroundColor)
+ .frame(height: height)
+ .cornerRadius(height / 2)
+
+ Rectangle()
+ .fill(fillColor)
+ .frame(
+ width: geometry.size.width * animatedProgress,
+ height: height
+ )
+ .cornerRadius(height / 2)
+ .animation(AnimationConfig.smooth, value: animatedProgress)
+ }
+ }
+ .frame(height: height)
+ .onAppear {
+ withAnimation(.easeOut(duration: 1.0)) {
+ animatedProgress = progress
+ }
+ }
+ .onChange(of: progress) { newValue in
+ withAnimation(AnimationConfig.smooth) {
+ animatedProgress = newValue
+ }
+ }
+ }
+}
+
+// MARK: - 浮动动作按钮
+struct FloatingActionButton: View {
+ let action: () -> Void
+ let icon: String
+ let color: Color
+ @State private var isPressed = false
+ @State private var rotation = 0.0
+
+ init(icon: String, color: Color = .blue, action: @escaping () -> Void) {
+ self.icon = icon
+ self.color = color
+ self.action = action
+ }
+
+ var body: some View {
+ Button(action: {
+ withAnimation(AnimationConfig.bouncy) {
+ rotation += 360
+ }
+ action()
+ }) {
+ Image(systemName: icon)
+ .font(.title2)
+ .foregroundColor(.white)
+ .frame(width: 56, height: 56)
+ .background(color)
+ .clipShape(Circle())
+ .shadow(color: color.opacity(0.4), radius: 8, x: 0, y: 4)
+ }
+ .rotationEffect(.degrees(rotation))
+ .scaleEffect(isPressed ? 0.9 : 1.0)
+ .onLongPressGesture(minimumDuration: 0, maximumDistance: .infinity) { isPressing in
+ withAnimation(AnimationConfig.quick) {
+ isPressed = isPressing
+ }
+ } perform: {}
+ }
+}
+
+// MARK: - 粒子动画
+struct ParticleSystem: View {
+ @State private var particles: [AnimationParticle] = []
+ let particleCount: Int
+ let colors: [Color]
+
+ init(particleCount: Int = 20, colors: [Color] = [.blue, .purple, .pink]) {
+ self.particleCount = particleCount
+ self.colors = colors
+ }
+
+ var body: some View {
+ GeometryReader { geometry in
+ ZStack {
+ ForEach(particles) { particle in
+ Circle()
+ .fill(particle.color)
+ .frame(width: particle.size, height: particle.size)
+ .position(particle.position)
+ .opacity(particle.opacity)
+ }
+ }
+ }
+ .onAppear {
+ createParticles()
+ animateParticles()
+ }
+ }
+
+ private func createParticles() {
+ particles = (0.. some View {
+ modifier(AnimatedAppearance(delay: delay, animation: animation))
+ }
+
+ func pulseEffect(color: Color = .blue, size: CGFloat = 1.2) -> some View {
+ modifier(PulseEffect(color: color, size: size))
+ }
+
+ func shakeEffect(intensity: CGFloat = 10) -> some View {
+ modifier(ShakeEffect(intensity: intensity))
+ }
+
+ func bounceOnTap(scale: CGFloat = 0.95) -> some View {
+ scaleEffect(1.0)
+ .onTapGesture {
+ withAnimation(AnimationConfig.bouncy) {
+ // 实现点击反弹效果
+ }
+ }
+ }
+
+ func cardTransition() -> some View {
+ transition(
+ .asymmetric(
+ insertion: .scale(scale: 0.8).combined(with: .opacity),
+ removal: .scale(scale: 1.2).combined(with: .opacity)
+ )
+ )
+ }
+
+ func slideTransition(edge: Edge = .bottom) -> some View {
+ transition(.move(edge: edge).combined(with: .opacity))
+ }
+
+ func rotateTransition() -> some View {
+ transition(.scale(scale: 0.1).combined(with: .opacity))
+ }
+}
+
+// MARK: - 页面过渡动画
+struct PageTransition: View {
+ let content: () -> Content
+ @State private var isVisible = false
+ let transitionType: TransitionType
+
+ enum TransitionType {
+ case slide, fade, scale, rotate
+ }
+
+ init(type: TransitionType = .fade, @ViewBuilder content: @escaping () -> Content) {
+ self.transitionType = type
+ self.content = content
+ }
+
+ var body: some View {
+ content()
+ .opacity(isVisible ? 1 : 0)
+ .scaleEffect(transitionType == .scale ? (isVisible ? 1 : 0.8) : 1)
+ .offset(y: transitionType == .slide ? (isVisible ? 0 : 50) : 0)
+ .rotationEffect(.degrees(transitionType == .rotate ? (isVisible ? 0 : 90) : 0))
+ .onAppear {
+ withAnimation(AnimationConfig.smooth) {
+ isVisible = true
+ }
+ }
+ }
+}
+
+// MARK: - 预览
+#Preview("动画组件") {
+ VStack(spacing: 20) {
+ AnimatedGradientText("情绪博物馆")
+
+ AnimatedProgressBar(progress: 0.7)
+ .frame(height: 10)
+
+ FloatingActionButton(icon: "plus") {
+ print("FAB tapped")
+ }
+
+ RippleEffect(color: .blue, size: 60)
+ }
+ .padding()
+}
\ No newline at end of file
diff --git a/EmotionMuseum/EmotionMuseum/Views/ChakraHealingView.swift b/EmotionMuseum/EmotionMuseum/Views/ChakraHealingView.swift
new file mode 100644
index 0000000..eaf0bf6
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Views/ChakraHealingView.swift
@@ -0,0 +1,395 @@
+//
+// ChakraHealingView.swift
+// EmotionMuseum
+//
+// Created by 华中敏 on 2025/6/13.
+//
+
+import SwiftUI
+import AVFoundation
+
+struct ChakraHealingView: View {
+ let chakra: ChakraType
+ let onComplete: () -> Void
+
+ @State private var isPlaying = false
+ @State private var progress: Double = 0.0
+ @State private var timer: Timer? = nil
+ @State private var breathingPhase: BreathingPhase = .inhale
+ @State private var breathCount = 0
+ @State private var beforeScore: Int? = nil
+ @State private var afterScore: Int? = nil
+ @State private var showingCompletionSheet = false
+
+ // 粒子系统状态
+ @State private var particles: [Particle] = []
+
+ var body: some View {
+ ZStack {
+ // 背景色彩
+ chakra.color.opacity(0.2)
+ .ignoresSafeArea()
+
+ // 动态背景
+ ZStack {
+ // 呼吸引导圆圈
+ Circle()
+ .fill(
+ RadialGradient(
+ gradient: Gradient(colors: [chakra.color, chakra.color.opacity(0.5)]),
+ center: .center,
+ startRadius: 10,
+ endRadius: 150
+ )
+ )
+ .frame(width: breathingPhase == .inhale ? 200 : 150,
+ height: breathingPhase == .inhale ? 200 : 150)
+ .opacity(0.7)
+ .animation(.easeInOut(duration: breathingPhase == .inhale ? 4 : 4), value: breathingPhase)
+
+ // 粒子效果
+ ForEach(particles) { particle in
+ Circle()
+ .fill(chakra.color.opacity(particle.opacity))
+ .frame(width: particle.size, height: particle.size)
+ .position(particle.position)
+ }
+ }
+
+ // 内容
+ VStack(spacing: 30) {
+ // 顶部信息
+ VStack(spacing: 8) {
+ Text(chakra.rawValue)
+ .font(.largeTitle)
+ .fontWeight(.bold)
+ .foregroundColor(chakra.color)
+
+ Text(chakra.description)
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+ }
+ .padding(.top, 40)
+
+ Spacer()
+
+ // 呼吸引导文字
+ Text(breathingPhase == .inhale ? "吸气..." : "呼气...")
+ .font(.title)
+ .fontWeight(.medium)
+ .foregroundColor(chakra.color)
+ .opacity(isPlaying ? 1 : 0)
+
+ Spacer()
+
+ // 控制区域
+ VStack(spacing: 20) {
+ // 进度条
+ ProgressView(value: progress)
+ .progressViewStyle(LinearProgressViewStyle(tint: chakra.color))
+ .padding(.horizontal)
+
+ // 播放控制
+ HStack(spacing: 40) {
+ Button(action: {
+ if beforeScore == nil {
+ // 如果还没开始,先评分
+ showBeforeScorePrompt()
+ } else {
+ togglePlayback()
+ }
+ }) {
+ Image(systemName: isPlaying ? "pause.circle.fill" : "play.circle.fill")
+ .font(.system(size: 60))
+ .foregroundColor(chakra.color)
+ }
+
+ Button(action: {
+ completeSession()
+ }) {
+ Image(systemName: "xmark.circle.fill")
+ .font(.system(size: 40))
+ .foregroundColor(.gray)
+ }
+ }
+ .padding(.bottom, 30)
+ }
+ .background(
+ Rectangle()
+ .fill(Color(.systemBackground))
+ .cornerRadius(30, corners: [.topLeft, .topRight])
+ .shadow(color: Color.black.opacity(0.1), radius: 10, x: 0, y: -5)
+ )
+ }
+ }
+ .onAppear {
+ setupParticles()
+ }
+ .onDisappear {
+ stopSession()
+ }
+ .sheet(isPresented: $showingCompletionSheet) {
+ SessionCompletionView(chakra: chakra, beforeScore: beforeScore ?? 5, afterScore: afterScore ?? 5) {
+ onComplete()
+ }
+ }
+ }
+
+ // MARK: - 私有方法
+
+ private func setupParticles() {
+ // 创建初始粒子
+ for _ in 0..<20 {
+ particles.append(Particle.random(in: UIScreen.main.bounds, color: chakra.color))
+ }
+ }
+
+ private func togglePlayback() {
+ isPlaying.toggle()
+
+ if isPlaying {
+ startSession()
+ } else {
+ pauseSession()
+ }
+ }
+
+ private func startSession() {
+ // 开始呼吸动画
+ startBreathingAnimation()
+
+ // 开始粒子动画
+ animateParticles()
+
+ // 模拟进度
+ timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
+ if progress < 1.0 {
+ progress += 0.0005 // 大约需要30分钟完成
+ } else {
+ completeSession()
+ }
+ }
+ }
+
+ private func pauseSession() {
+ timer?.invalidate()
+ }
+
+ private func stopSession() {
+ timer?.invalidate()
+ timer = nil
+ }
+
+ private func startBreathingAnimation() {
+ // 呼吸动画循环
+ Timer.scheduledTimer(withTimeInterval: 4, repeats: true) { _ in
+ withAnimation {
+ breathingPhase = breathingPhase == .inhale ? .exhale : .inhale
+ }
+
+ breathCount += 1
+ if breathCount >= 100 {
+ completeSession()
+ }
+ }
+ }
+
+ private func animateParticles() {
+ // 粒子动画
+ Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
+ for i in 0.. Particle {
+ let position = CGPoint(
+ x: CGFloat.random(in: rect.minX...rect.maxX),
+ y: CGFloat.random(in: rect.minY...rect.maxY)
+ )
+
+ let angle = CGFloat.random(in: 0...(2 * .pi))
+ let direction = CGVector(
+ dx: cos(angle),
+ dy: sin(angle)
+ )
+
+ return Particle(
+ position: position,
+ direction: direction,
+ speed: CGFloat.random(in: 0.5...2.0),
+ size: CGFloat.random(in: 3...8),
+ opacity: Double.random(in: 0.3...0.7)
+ )
+ }
+}
+
+// MARK: - 会话完成视图
+struct SessionCompletionView: View {
+ let chakra: ChakraType
+ let beforeScore: Int
+ let afterScore: Int
+ let onDismiss: () -> Void
+
+ var body: some View {
+ VStack(spacing: 20) {
+ // 标题
+ Text("疗愈完成")
+ .font(.title)
+ .fontWeight(.bold)
+
+ // 脉轮信息
+ HStack(spacing: 12) {
+ Circle()
+ .fill(chakra.color)
+ .frame(width: 30, height: 30)
+
+ Text(chakra.rawValue)
+ .font(.headline)
+ }
+
+ Divider()
+
+ // 疗愈效果
+ VStack(alignment: .leading, spacing: 16) {
+ Text("疗愈效果")
+ .font(.headline)
+
+ HStack(spacing: 30) {
+ VStack {
+ Text("疗愈前")
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+
+ Text("\(beforeScore)")
+ .font(.title)
+ .fontWeight(.bold)
+ }
+
+ Image(systemName: "arrow.right")
+ .font(.title2)
+ .foregroundColor(.secondary)
+
+ VStack {
+ Text("疗愈后")
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+
+ Text("\(afterScore)")
+ .font(.title)
+ .fontWeight(.bold)
+ .foregroundColor(.green)
+ }
+ }
+ .padding()
+ .background(Color(.systemGray6))
+ .cornerRadius(12)
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.horizontal)
+
+ // 反馈信息
+ VStack(alignment: .leading, spacing: 12) {
+ Text("反馈")
+ .font(.headline)
+
+ Text("你的\(chakra.rawValue)能量已得到提升,情绪状态有所改善。建议每天进行一次疗愈,保持能量平衡。")
+ .font(.body)
+ .foregroundColor(.secondary)
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.horizontal)
+
+ Spacer()
+
+ // 按钮
+ Button(action: {
+ onDismiss()
+ }) {
+ Text("返回")
+ .font(.headline)
+ .foregroundColor(.white)
+ .frame(maxWidth: .infinity)
+ .padding()
+ .background(chakra.color)
+ .cornerRadius(12)
+ }
+ .padding(.horizontal)
+ .padding(.bottom)
+ }
+ .padding(.top, 40)
+ }
+}
+
+// MARK: - 扩展
+extension View {
+ func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
+ clipShape(RoundedCorner(radius: radius, corners: corners))
+ }
+}
+
+struct RoundedCorner: Shape {
+ var radius: CGFloat = .infinity
+ var corners: UIRectCorner = .allCorners
+
+ func path(in rect: CGRect) -> Path {
+ let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
+ return Path(path.cgPath)
+ }
+}
+
+#Preview {
+ ChakraHealingView(chakra: .heart, onComplete: {})
+}
\ No newline at end of file
diff --git a/EmotionMuseum/EmotionMuseum/Views/ConversationPreviewCard.swift b/EmotionMuseum/EmotionMuseum/Views/ConversationPreviewCard.swift
new file mode 100644
index 0000000..0af25e2
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Views/ConversationPreviewCard.swift
@@ -0,0 +1,115 @@
+//
+// ConversationPreviewCard.swift
+// EmotionMuseum
+//
+// Created by 华中敏 on 2025/7/5.
+//
+
+import SwiftUI
+
+struct ConversationPreviewCard: View {
+ let conversation: Conversation
+ let onTap: () -> Void
+
+ var body: some View {
+ Button(action: onTap) {
+ HStack(spacing: 12) {
+ // 对话类型图标
+ Image(systemName: "message.circle.fill")
+ .font(.title2)
+ .foregroundColor(Color("AccentColor"))
+
+ VStack(alignment: .leading, spacing: 4) {
+ // 对话标题
+ Text(conversation.title)
+ .font(.headline)
+ .foregroundColor(Color("PrimaryText"))
+ .lineLimit(1)
+
+ // 最后一条消息或摘要
+ if let lastMessage = conversation.lastMessage {
+ Text(lastMessage.content)
+ .font(.subheadline)
+ .foregroundColor(Color("SecondaryText"))
+ .lineLimit(2)
+ } else if let summary = conversation.summary {
+ Text(summary)
+ .font(.subheadline)
+ .foregroundColor(Color("SecondaryText"))
+ .lineLimit(2)
+ } else {
+ Text("点击查看对话详情")
+ .font(.subheadline)
+ .foregroundColor(Color("TertiaryText"))
+ }
+
+ // 时间和消息数量
+ HStack {
+ Text(conversation.startTime.timeAgo)
+ .font(.caption)
+ .foregroundColor(Color("TertiaryText"))
+
+ Spacer()
+
+ Text("\(conversation.messageCount) 条消息")
+ .font(.caption)
+ .foregroundColor(Color("TertiaryText"))
+ }
+ }
+
+ Spacer()
+
+ // 情绪分析指示器
+ if let emotion = conversation.emotionAnalysis?.primaryEmotion {
+ VStack {
+ Text(emotion.emoji)
+ .font(.title2)
+
+ Text(emotion.rawValue)
+ .font(.caption2)
+ .foregroundColor(emotion.color)
+ }
+ }
+ }
+ .padding()
+ .background(
+ RoundedRectangle(cornerRadius: 12)
+ .fill(Color("CardBackground"))
+ .shadow(
+ color: Color.black.opacity(0.05),
+ radius: 3,
+ x: 0,
+ y: 2
+ )
+ )
+ }
+ .buttonStyle(PlainButtonStyle())
+ }
+}
+
+#Preview {
+ ConversationPreviewCard(
+ conversation: Conversation(
+ userId: UUID(),
+ title: "今天的心情分享",
+ messages: [
+ Message(
+ conversationId: UUID(),
+ content: "我今天感觉有点焦虑,不知道该怎么办。",
+ type: .text,
+ sender: .user
+ )
+ ],
+ emotionAnalysis: EmotionAnalysis(
+ primaryEmotion: .anxiety,
+ emotionIntensity: 0.7,
+ emotionTrend: .stable,
+ keywords: ["焦虑", "压力"],
+ aiInsights: "用户表现出一定程度的焦虑情绪"
+ )
+ )
+ ) {
+ print("Tapped conversation")
+ }
+ .padding()
+}
\ No newline at end of file
diff --git a/EmotionMuseum/EmotionMuseum/Views/ExploreView.swift b/EmotionMuseum/EmotionMuseum/Views/ExploreView.swift
new file mode 100644
index 0000000..676c54b
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Views/ExploreView.swift
@@ -0,0 +1,226 @@
+//
+// ExploreView.swift
+// EmotionMuseum
+//
+// Created by 华中敏 on 2025/6/13.
+//
+
+import SwiftUI
+import MapKit
+import CoreLocation
+
+// MARK: - 探索页面主视图
+struct ExploreView: View {
+ @EnvironmentObject var mockDataManager: MockDataManager
+ @EnvironmentObject var navigationManager: NavigationManager
+ @State private var selectedLocation: LocationPin?
+ @State private var showingLocationDetail = false
+ @State private var showingCommunityFeed = false
+ @State private var savedLocations: [LocationPin] = []
+ @State private var showingMapView = true
+ @State private var showingLocationPicker = false
+ @State private var shouldMoveToLocationPin = false
+
+ var body: some View {
+ NavigationView {
+ VStack(spacing: 0) {
+ // 顶部切换按钮
+ HStack {
+ Button(action: {
+ showingMapView = true
+ showingCommunityFeed = false
+ }) {
+ HStack {
+ Image(systemName: "map")
+ Text("情绪地图")
+ }
+ .foregroundColor(showingMapView ? .white : .primary)
+ .padding(.horizontal, 16)
+ .padding(.vertical, 8)
+ .background(showingMapView ? Color.blue : Color.clear)
+ .cornerRadius(20)
+ }
+
+ Spacer()
+
+ Button(action: {
+ showingMapView = false
+ showingCommunityFeed = true
+ }) {
+ HStack {
+ Image(systemName: "person.3")
+ Text("社区分享")
+ }
+ .foregroundColor(showingCommunityFeed ? .white : .primary)
+ .padding(.horizontal, 16)
+ .padding(.vertical, 8)
+ .background(showingCommunityFeed ? Color.green : Color.clear)
+ .cornerRadius(20)
+ }
+ }
+ .padding()
+ .background(Color(.systemGray6))
+
+ // 主要内容区域
+ if showingMapView {
+ mapViewSection
+ } else {
+ communityFeedSection
+ }
+ }
+ .navigationTitle("探索")
+ .navigationBarTitleDisplayMode(.large)
+ .onAppear {
+ loadSavedLocations()
+ }
+ }
+ .sheet(isPresented: $showingLocationDetail) {
+ if let location = selectedLocation {
+ LocationDetailView(location: location)
+ }
+ }
+ }
+
+ // MARK: - 地图视图部分
+ private var mapViewSection: some View {
+ VStack {
+ // 地图区域
+ ZStack {
+ MapView()
+ .frame(height: 300)
+ .cornerRadius(12)
+ .padding()
+ }
+ // 推荐位置列表
+ VStack(alignment: .leading, spacing: 12) {
+ HStack {
+ Text("推荐地点")
+ .font(.headline)
+ Spacer()
+ Button("查看全部") {
+ // 查看全部推荐地点
+ }
+ .font(.caption)
+ .foregroundColor(.blue)
+ }
+ ScrollView(.horizontal, showsIndicators: false) {
+ HStack(spacing: 12) {
+ ForEach(Array(savedLocations.prefix(5))) { location in
+ LocationCard(location: location) {
+ selectedLocation = location
+ showingLocationDetail = true
+ }
+ }
+ }
+ .padding(.horizontal)
+ }
+ }
+ .padding()
+ Spacer()
+ }
+ }
+
+ // MARK: - 社区动态部分
+ private var communityFeedSection: some View {
+ VStack {
+ ScrollView {
+ LazyVStack(spacing: 16) {
+ ForEach(mockDataManager.communityPosts) { post in
+ CommunityPostCard(post: post)
+ }
+ }
+ .padding()
+ }
+ }
+ }
+
+ // MARK: - 私有方法
+ private func loadSavedLocations() {
+ // 创建一些示例位置数据
+ savedLocations = [
+ LocationPin(
+ coordinate: Coordinate(latitude: 39.9042, longitude: 116.4074),
+ title: "天安门广场",
+ description: "庄严肃穆的历史地标",
+ type: .popular,
+ emotionTags: [.neutral],
+ category: .other
+ ),
+ LocationPin(
+ coordinate: Coordinate(latitude: 39.9163, longitude: 116.3972),
+ title: "故宫博物院",
+ description: "深厚的历史文化底蕴",
+ type: .aiRecommended,
+ emotionTags: [.surprise],
+ category: .museum
+ ),
+ LocationPin(
+ coordinate: Coordinate(latitude: 39.9925, longitude: 116.3135),
+ title: "颐和园",
+ description: "宁静优美的皇家园林",
+ type: .personal,
+ emotionTags: [.contentment],
+ category: .garden
+ ),
+ LocationPin(
+ coordinate: Coordinate(latitude: 40.0090, longitude: 116.3348),
+ title: "圆明园",
+ description: "历史的见证与思考",
+ type: .community,
+ emotionTags: [.sadness],
+ category: .park
+ ),
+ LocationPin(
+ coordinate: Coordinate(latitude: 39.9059, longitude: 116.3913),
+ title: "北海公园",
+ description: "古典园林的宁静之美",
+ type: .popular,
+ emotionTags: [.joy],
+ category: .park
+ )
+ ]
+ }
+}
+
+// MARK: - 位置卡片组件
+struct LocationCard: View {
+ let location: LocationPin
+ let onTap: () -> Void
+
+ var body: some View {
+ Button(action: onTap) {
+ VStack(alignment: .leading, spacing: 8) {
+ // 位置图片占位符
+ Rectangle()
+ .fill(Color(.systemGray5))
+ .frame(width: 120, height: 80)
+ .overlay(
+ Image(systemName: location.category.icon)
+ .font(.title)
+ .foregroundColor(.gray)
+ )
+ .cornerRadius(8)
+
+ VStack(alignment: .leading, spacing: 4) {
+ Text(location.title)
+ .font(.caption)
+ .fontWeight(.medium)
+ .lineLimit(1)
+
+ Text(location.description)
+ .font(.caption2)
+ .foregroundColor(.secondary)
+ .lineLimit(2)
+ }
+ .frame(width: 120, alignment: .leading)
+ }
+ }
+ .buttonStyle(PlainButtonStyle())
+ }
+}
+
+#Preview {
+ ExploreView()
+ .environmentObject(MockDataManager.shared)
+ .environmentObject(NavigationManager())
+}
\ No newline at end of file
diff --git a/EmotionMuseum/EmotionMuseum/Views/GrowthView.swift b/EmotionMuseum/EmotionMuseum/Views/GrowthView.swift
new file mode 100644
index 0000000..a395c66
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Views/GrowthView.swift
@@ -0,0 +1,808 @@
+//
+// GrowthView.swift
+// EmotionMuseum
+//
+// Created by 华中敏 on 2025/6/13.
+//
+
+import SwiftUI
+
+// MARK: - 治愈页面主视图
+struct GrowthView: View {
+ @StateObject private var themeManager = ThemeManager()
+ @State private var showingTopicDetail = false
+ @State private var selectedTopic: GrowthTopic?
+ @State private var showingInsights = false
+ @State private var showingRadarChart = false
+ @State private var loadingState: LoadingState = .idle
+ @State private var isInitialLoading = true
+
+ var body: some View {
+ NavigationView {
+ LoadingStateView(loadingState: isInitialLoading ? .loading : .loaded) {
+ ScrollView {
+ VStack(spacing: 24) {
+ // 情绪洞察卡片
+ EmotionalInsightCard {
+ showingInsights = true
+ }
+ .transition(.scale(scale: 0.8).combined(with: .opacity))
+
+ // 用户画像五维图
+ UserProfileRadarCard {
+ showingRadarChart = true
+ }
+ .transition(.scale(scale: 0.8).combined(with: .opacity))
+
+ // 成长课题系统
+ GrowthTopicsSection { topic in
+ selectedTopic = topic
+ showingTopicDetail = true
+ }
+ .transition(.scale(scale: 0.8).combined(with: .opacity))
+
+ // 今日推荐行动
+ TodayActionCard()
+ .transition(.scale(scale: 0.8).combined(with: .opacity))
+
+ // 成长历程
+ GrowthTimelineCard()
+ .transition(.scale(scale: 0.8).combined(with: .opacity))
+ }
+ .padding(.horizontal)
+ .padding(.vertical)
+ }
+ .refreshable {
+ await refreshData()
+ }
+ } loadingView: {
+ AnyView(growthViewSkeleton)
+ }
+ .navigationTitle("成长治愈")
+ .navigationBarTitleDisplayMode(.large)
+ }
+ .environmentObject(themeManager)
+ .preferredColorScheme(themeManager.systemFollowsDeviceTheme ? nil : (themeManager.isDarkMode ? .dark : .light))
+ .sheet(isPresented: $showingTopicDetail) {
+ if let topic = selectedTopic {
+ TopicDetailView(topic: topic)
+ .environmentObject(themeManager)
+ }
+ }
+ .sheet(isPresented: $showingInsights) {
+ EmotionalInsightsView()
+ .environmentObject(themeManager)
+ }
+ .sheet(isPresented: $showingRadarChart) {
+ NavigationView {
+ VStack {
+ Text("用户画像雷达图")
+ .font(.title)
+ .padding()
+
+ Text("这里显示用户的多维度能力雷达图")
+ .foregroundColor(.secondary)
+ .padding()
+
+ Spacer()
+ }
+ .navigationTitle("用户画像")
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .navigationBarTrailing) {
+ Button("关闭") {
+ showingRadarChart = false
+ }
+ }
+ }
+ }
+ .environmentObject(themeManager)
+ }
+ .onAppear {
+ simulateInitialLoading()
+ }
+ }
+
+ // MARK: - 私有方法
+ private func simulateInitialLoading() {
+ loadingState = .loading
+
+ // 模拟异步加载过程
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) {
+ withAnimation(.easeOut(duration: 0.8)) {
+ isInitialLoading = false
+ loadingState = .loaded
+ }
+ }
+ }
+
+ private func refreshData() async {
+ loadingState = .loading
+
+ // 模拟数据刷新
+ try? await Task.sleep(nanoseconds: 800_000_000) // 0.8秒
+
+ DispatchQueue.main.async {
+ withAnimation(.easeOut(duration: 0.6)) {
+ loadingState = .loaded
+ }
+ }
+ }
+
+ // MARK: - 骨架屏视图
+ private var growthViewSkeleton: some View {
+ ScrollView {
+ VStack(spacing: 24) {
+ // 情绪洞察卡片骨架屏
+ insightCardSkeleton
+
+ // 用户画像卡片骨架屏
+ radarCardSkeleton
+
+ // 成长课题骨架屏
+ topicsGridSkeleton
+
+ // 今日推荐骨架屏
+ actionCardSkeleton
+
+ // 成长历程骨架屏
+ timelineCardSkeleton
+ }
+ .padding(.horizontal)
+ .padding(.vertical)
+ }
+ .background(Color.theme.background)
+ }
+
+ private var insightCardSkeleton: some View {
+ VStack(alignment: .leading, spacing: 16) {
+ HStack {
+ VStack(alignment: .leading, spacing: 4) {
+ SkeletonView(width: 80, height: 20, cornerRadius: 6)
+ SkeletonView(width: 140, height: 12, cornerRadius: 3)
+ }
+ Spacer()
+ SkeletonView(width: 24, height: 24, cornerRadius: 12)
+ }
+
+ VStack(spacing: 12) {
+ ForEach(0..<3, id: \.self) { _ in
+ HStack(spacing: 12) {
+ SkeletonView(width: 20, height: 16, cornerRadius: 4)
+ SkeletonView(width: 80, height: 14, cornerRadius: 3)
+ Spacer()
+ SkeletonView(width: 60, height: 14, cornerRadius: 3)
+ }
+ }
+ }
+
+ HStack {
+ SkeletonView(width: 100, height: 12, cornerRadius: 3)
+ Spacer()
+ SkeletonView(width: 12, height: 12, cornerRadius: 3)
+ }
+ }
+ .padding()
+ .background(Color.theme.cardBackground)
+ .cornerRadius(16)
+ }
+
+ private var radarCardSkeleton: some View {
+ VStack(spacing: 16) {
+ HStack {
+ SkeletonView(width: 80, height: 20, cornerRadius: 6)
+ Spacer()
+ SkeletonView(width: 24, height: 24, cornerRadius: 12)
+ }
+
+ VStack(spacing: 8) {
+ ForEach(0..<5, id: \.self) { _ in
+ HStack {
+ SkeletonView(width: 60, height: 12, cornerRadius: 3)
+ SkeletonView(height: 8, cornerRadius: 4)
+ SkeletonView(width: 40, height: 12, cornerRadius: 3)
+ }
+ }
+ }
+
+ HStack {
+ SkeletonView(width: 120, height: 12, cornerRadius: 3)
+ Spacer()
+ SkeletonView(width: 12, height: 12, cornerRadius: 3)
+ }
+ }
+ .padding()
+ .background(Color.theme.cardBackground)
+ .cornerRadius(16)
+ }
+
+ private var topicsGridSkeleton: some View {
+ VStack(alignment: .leading, spacing: 16) {
+ HStack {
+ SkeletonView(width: 80, height: 20, cornerRadius: 6)
+ Spacer()
+ SkeletonView(width: 60, height: 12, cornerRadius: 3)
+ }
+
+ LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 12) {
+ ForEach(0..<4, id: \.self) { _ in
+ VStack(alignment: .leading, spacing: 12) {
+ HStack {
+ SkeletonView(width: 20, height: 20, cornerRadius: 10)
+ Spacer()
+ SkeletonView(width: 30, height: 12, cornerRadius: 3)
+ }
+
+ VStack(alignment: .leading, spacing: 4) {
+ SkeletonView(width: 100, height: 14, cornerRadius: 3)
+ SkeletonView(width: 120, height: 12, cornerRadius: 3)
+ SkeletonView(width: 80, height: 12, cornerRadius: 3)
+ }
+
+ VStack(spacing: 4) {
+ HStack {
+ SkeletonView(width: 30, height: 10, cornerRadius: 2)
+ Spacer()
+ SkeletonView(width: 30, height: 10, cornerRadius: 2)
+ }
+ SkeletonView(height: 4, cornerRadius: 2)
+ }
+ }
+ .padding()
+ .background(Color.theme.cardBackground)
+ .cornerRadius(12)
+ }
+ }
+ }
+ }
+
+ private var actionCardSkeleton: some View {
+ VStack(alignment: .leading, spacing: 16) {
+ HStack {
+ SkeletonView(width: 80, height: 20, cornerRadius: 6)
+ Spacer()
+ SkeletonView(width: 24, height: 24, cornerRadius: 12)
+ }
+
+ VStack(spacing: 12) {
+ ForEach(0..<3, id: \.self) { _ in
+ HStack(spacing: 12) {
+ SkeletonView(width: 24, height: 24, cornerRadius: 12)
+
+ VStack(alignment: .leading, spacing: 2) {
+ SkeletonView(width: 160, height: 14, cornerRadius: 3)
+ HStack(spacing: 8) {
+ SkeletonView(width: 60, height: 16, cornerRadius: 8)
+ SkeletonView(width: 40, height: 12, cornerRadius: 3)
+ }
+ }
+ Spacer()
+ }
+ }
+ }
+ }
+ .padding()
+ .background(Color.theme.cardBackground)
+ .cornerRadius(16)
+ }
+
+ private var timelineCardSkeleton: some View {
+ VStack(alignment: .leading, spacing: 16) {
+ SkeletonView(width: 80, height: 20, cornerRadius: 6)
+
+ VStack(alignment: .leading, spacing: 12) {
+ ForEach(0..<4, id: \.self) { _ in
+ HStack(spacing: 12) {
+ VStack {
+ SkeletonView(width: 8, height: 8, cornerRadius: 4)
+ SkeletonView(width: 1, height: 20, cornerRadius: 1)
+ }
+
+ VStack(alignment: .leading, spacing: 2) {
+ HStack {
+ SkeletonView(width: 100, height: 14, cornerRadius: 3)
+ Spacer()
+ SkeletonView(width: 40, height: 12, cornerRadius: 3)
+ }
+ SkeletonView(width: 180, height: 12, cornerRadius: 3)
+ }
+ }
+ }
+ }
+ }
+ .padding()
+ .background(Color.theme.cardBackground)
+ .cornerRadius(16)
+ }
+}
+
+// MARK: - 情绪洞察卡片
+struct EmotionalInsightCard: View {
+ let onTap: () -> Void
+
+ var body: some View {
+ Button(action: onTap) {
+ VStack(alignment: .leading, spacing: 16) {
+ HStack {
+ VStack(alignment: .leading, spacing: 4) {
+ Text("情绪洞察")
+ .font(.headline)
+ .fontWeight(.semibold)
+ .foregroundColor(Color.theme.primaryText)
+
+ Text("基于你的对话记录生成")
+ .font(.caption)
+ .foregroundColor(Color.theme.secondaryText)
+ }
+
+ Spacer()
+
+ Image(systemName: "brain.head.profile")
+ .font(.title2)
+ .foregroundColor(.purple)
+ }
+
+ VStack(alignment: .leading, spacing: 12) {
+ InsightRow(
+ icon: "heart.fill",
+ title: "主要情绪状态",
+ value: "平静自省",
+ color: .blue
+ )
+
+ InsightRow(
+ icon: "target",
+ title: "成长焦点",
+ value: "人际关系",
+ color: .green
+ )
+
+ InsightRow(
+ icon: "chart.line.uptrend.xyaxis",
+ title: "进步指数",
+ value: "↗️ 稳步提升",
+ color: .orange
+ )
+ }
+
+ HStack {
+ Text("点击查看详细分析")
+ .font(.caption)
+ .foregroundColor(Color.theme.secondaryText)
+
+ Spacer()
+
+ Image(systemName: "chevron.right")
+ .font(.caption)
+ .foregroundColor(Color.theme.secondaryText)
+ }
+ }
+ .padding()
+ .background(
+ RoundedRectangle(cornerRadius: 16)
+ .fill(Color(.systemGray6))
+ )
+ }
+ .buttonStyle(PlainButtonStyle())
+ }
+}
+
+struct InsightRow: View {
+ let icon: String
+ let title: String
+ let value: String
+ let color: Color
+
+ var body: some View {
+ HStack(spacing: 12) {
+ Image(systemName: icon)
+ .font(.subheadline)
+ .foregroundColor(color)
+ .frame(width: 20)
+
+ Text(title)
+ .font(.subheadline)
+ .foregroundColor(Color.theme.secondaryText)
+
+ Spacer()
+
+ Text(value)
+ .font(.subheadline)
+ .fontWeight(.medium)
+ }
+ }
+}
+
+// MARK: - 用户画像雷达图卡片
+struct UserProfileRadarCard: View {
+ let onTap: () -> Void
+
+ var body: some View {
+ Button(action: onTap) {
+ VStack(spacing: 16) {
+ HStack {
+ Text("成长画像")
+ .font(.headline)
+ .fontWeight(.semibold)
+
+ Spacer()
+
+ Image(systemName: "person.crop.circle.badge.checkmark")
+ .font(.title2)
+ .foregroundColor(.green)
+ }
+
+ // 简化的五维显示
+ VStack(spacing: 8) {
+ ProfileDimensionRow(name: "自我感知", value: 0.8, color: .blue)
+ ProfileDimensionRow(name: "情绪韧性", value: 0.7, color: .purple)
+ ProfileDimensionRow(name: "行动力", value: 0.6, color: .orange)
+ ProfileDimensionRow(name: "共情力", value: 0.9, color: .green)
+ ProfileDimensionRow(name: "生活热度", value: 0.7, color: .red)
+ }
+
+ HStack {
+ Text("点击查看完整雷达图")
+ .font(.caption)
+ .foregroundColor(.secondary)
+
+ Spacer()
+
+ Image(systemName: "chevron.right")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ }
+ .padding()
+ .background(
+ RoundedRectangle(cornerRadius: 16)
+ .fill(Color(.systemGray6))
+ )
+ }
+ .buttonStyle(PlainButtonStyle())
+ }
+}
+
+struct ProfileDimensionRow: View {
+ let name: String
+ let value: Double
+ let color: Color
+
+ var body: some View {
+ HStack {
+ Text(name)
+ .font(.caption)
+ .foregroundColor(.primary)
+ .frame(width: 60, alignment: .leading)
+
+ ProgressView(value: value)
+ .progressViewStyle(LinearProgressViewStyle(tint: color))
+
+ Text("\(Int(value * 100))%")
+ .font(.caption)
+ .fontWeight(.medium)
+ .foregroundColor(color)
+ .frame(width: 40, alignment: .trailing)
+ }
+ }
+}
+
+// MARK: - 成长课题系统
+struct GrowthTopicsSection: View {
+ let onTopicTap: (GrowthTopic) -> Void
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 16) {
+ HStack {
+ Text("成长课题")
+ .font(.headline)
+ .fontWeight(.semibold)
+
+ Spacer()
+
+ Text("3个进行中")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+
+ LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 12) {
+ ForEach(sampleGrowthTopics) { topic in
+ TopicCard(topic: topic) {
+ onTopicTap(topic)
+ }
+ }
+ }
+ }
+ }
+}
+
+struct TopicCard: View {
+ let topic: GrowthTopic
+ let onTap: () -> Void
+
+ var body: some View {
+ Button(action: onTap) {
+ VStack(alignment: .leading, spacing: 12) {
+ HStack {
+ Image(systemName: topic.icon)
+ .font(.title3)
+ .foregroundColor(topic.color)
+
+ Spacer()
+
+ Text("Lv.\(topic.level)")
+ .font(.caption)
+ .fontWeight(.bold)
+ .foregroundColor(topic.color)
+ }
+
+ VStack(alignment: .leading, spacing: 4) {
+ Text(topic.title)
+ .font(.subheadline)
+ .fontWeight(.medium)
+ .multilineTextAlignment(.leading)
+
+ Text(topic.description)
+ .font(.caption)
+ .foregroundColor(.secondary)
+ .lineLimit(2)
+ .multilineTextAlignment(.leading)
+ }
+
+ VStack(spacing: 4) {
+ HStack {
+ Text("进度")
+ .font(.caption2)
+ .foregroundColor(.secondary)
+
+ Spacer()
+
+ Text("\(Int(topic.progress * 100))%")
+ .font(.caption2)
+ .fontWeight(.medium)
+ }
+
+ ProgressView(value: topic.progress)
+ .progressViewStyle(LinearProgressViewStyle(tint: topic.color))
+ }
+ }
+ .padding()
+ .background(
+ RoundedRectangle(cornerRadius: 12)
+ .fill(Color(.systemGray6))
+ )
+ }
+ .buttonStyle(PlainButtonStyle())
+ }
+}
+
+// MARK: - 今日推荐行动
+struct TodayActionCard: View {
+ var body: some View {
+ VStack(alignment: .leading, spacing: 16) {
+ HStack {
+ Text("今日推荐")
+ .font(.headline)
+ .fontWeight(.semibold)
+
+ Spacer()
+
+ Image(systemName: "lightbulb.fill")
+ .font(.title3)
+ .foregroundColor(.yellow)
+ }
+
+ VStack(spacing: 12) {
+ ActionItemView(
+ title: "与朋友分享一件开心的事",
+ category: "人际关系",
+ duration: "5分钟",
+ color: .blue
+ )
+
+ ActionItemView(
+ title: "写下今天的三个感恩点",
+ category: "自我觉察",
+ duration: "10分钟",
+ color: .green
+ )
+
+ ActionItemView(
+ title: "深呼吸练习",
+ category: "情绪调节",
+ duration: "3分钟",
+ color: .purple
+ )
+ }
+ }
+ .padding()
+ .background(
+ RoundedRectangle(cornerRadius: 16)
+ .fill(Color(.systemGray6))
+ )
+ }
+}
+
+struct ActionItemView: View {
+ let title: String
+ let category: String
+ let duration: String
+ let color: Color
+ @State private var isCompleted = false
+
+ var body: some View {
+ HStack(spacing: 12) {
+ Button(action: { isCompleted.toggle() }) {
+ Image(systemName: isCompleted ? "checkmark.circle.fill" : "circle")
+ .font(.title3)
+ .foregroundColor(isCompleted ? color : .gray)
+ }
+
+ VStack(alignment: .leading, spacing: 2) {
+ Text(title)
+ .font(.subheadline)
+ .fontWeight(.medium)
+ .strikethrough(isCompleted)
+
+ HStack(spacing: 8) {
+ Text(category)
+ .font(.caption)
+ .padding(.horizontal, 8)
+ .padding(.vertical, 2)
+ .background(color.opacity(0.2))
+ .foregroundColor(color)
+ .cornerRadius(8)
+
+ Text(duration)
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ }
+
+ Spacer()
+ }
+ .opacity(isCompleted ? 0.6 : 1.0)
+ .animation(.easeInOut(duration: 0.2), value: isCompleted)
+ }
+}
+
+// MARK: - 成长历程
+struct GrowthTimelineCard: View {
+ var body: some View {
+ VStack(alignment: .leading, spacing: 16) {
+ Text("成长历程")
+ .font(.headline)
+ .fontWeight(.semibold)
+
+ VStack(alignment: .leading, spacing: 12) {
+ TimelineItem(
+ date: "今天",
+ title: "完成情绪记录",
+ description: "记录了平静的心情状态",
+ color: .blue,
+ isRecent: true
+ )
+
+ TimelineItem(
+ date: "昨天",
+ title: "课题升级",
+ description: "人际关系课题提升到Lv.2",
+ color: .green,
+ isRecent: true
+ )
+
+ TimelineItem(
+ date: "3天前",
+ title: "解锁新课题",
+ description: "开始学习情绪调节技巧",
+ color: .purple,
+ isRecent: false
+ )
+
+ TimelineItem(
+ date: "1周前",
+ title: "达成里程碑",
+ description: "连续7天完成情绪记录",
+ color: .orange,
+ isRecent: false
+ )
+ }
+ }
+ .padding()
+ .background(
+ RoundedRectangle(cornerRadius: 16)
+ .fill(Color(.systemGray6))
+ )
+ }
+}
+
+struct TimelineItem: View {
+ let date: String
+ let title: String
+ let description: String
+ let color: Color
+ let isRecent: Bool
+
+ var body: some View {
+ HStack(spacing: 12) {
+ VStack {
+ Circle()
+ .fill(color)
+ .frame(width: 8, height: 8)
+
+ if !isRecent {
+ Rectangle()
+ .fill(Color(.systemGray4))
+ .frame(width: 1, height: 20)
+ }
+ }
+
+ VStack(alignment: .leading, spacing: 2) {
+ HStack {
+ Text(title)
+ .font(.subheadline)
+ .fontWeight(.medium)
+
+ Spacer()
+
+ Text(date)
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+
+ Text(description)
+ .font(.caption)
+ .foregroundColor(.secondary)
+ .lineLimit(2)
+ }
+ }
+ .opacity(isRecent ? 1.0 : 0.7)
+ }
+}
+
+// MARK: - GrowthTopic UI扩展
+extension GrowthTopic {
+ var icon: String {
+ category.icon
+ }
+
+ var color: Color {
+ category.color
+ }
+}
+
+// MARK: - 模拟数据
+let sampleGrowthTopics = [
+ GrowthTopic(
+ title: "人际关系边界",
+ description: "学习建立健康的人际关系边界",
+ category: .relationships,
+ difficulty: .intermediate,
+ progress: 0.7,
+ level: 2
+ ),
+ GrowthTopic(
+ title: "情绪调节技能",
+ description: "掌握情绪识别和调节的核心技能",
+ category: .emotionRegulation,
+ difficulty: .beginner,
+ progress: 0.3,
+ level: 1
+ ),
+ GrowthTopic(
+ title: "深度自我认知",
+ description: "提升对内在世界的认知和理解",
+ category: .selfAwareness,
+ difficulty: .advanced,
+ progress: 0.9,
+ level: 3
+ ),
+ GrowthTopic(
+ title: "行动力提升",
+ description: "培养执行力和目标达成能力",
+ category: .lifeGoals,
+ difficulty: .beginner,
+ progress: 0.4,
+ level: 1
+ )
+]
\ No newline at end of file
diff --git a/EmotionMuseum/EmotionMuseum/Views/HealingView.swift b/EmotionMuseum/EmotionMuseum/Views/HealingView.swift
new file mode 100644
index 0000000..010fd05
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Views/HealingView.swift
@@ -0,0 +1,311 @@
+//
+// HealingView.swift
+// EmotionMuseum
+//
+// Created by 华中敏 on 2025/6/13.
+//
+
+import SwiftUI
+import AVFoundation
+
+struct HealingView: View {
+ @State private var selectedChakra: ChakraType? = nil
+ @State private var showingHealingSession = false
+ @State private var chakraStates: [ChakraType: ChakraState] = [:]
+
+ var body: some View {
+ NavigationView {
+ ZStack {
+ // 背景渐变
+ LinearGradient(
+ gradient: Gradient(colors: [.purple.opacity(0.1), .blue.opacity(0.1)]),
+ startPoint: .topLeading,
+ endPoint: .bottomTrailing
+ )
+ .ignoresSafeArea()
+
+ ScrollView {
+ VStack(spacing: 30) {
+ // 标题和说明
+ VStack(spacing: 8) {
+ Text("脉轮疗愈")
+ .font(.largeTitle)
+ .fontWeight(.bold)
+
+ Text("点击脉轮区域开始疗愈之旅")
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+ }
+ .padding(.top)
+
+ // 人形脉轮图
+ VStack(spacing: 0) {
+ // 顶轮
+ ChakraButton(
+ chakra: .crown,
+ state: chakraStates[.crown] ?? .normal,
+ action: { selectChakra(.crown) }
+ )
+ .offset(y: 10)
+
+ Spacer().frame(height: 20)
+
+ // 眉心轮
+ ChakraButton(
+ chakra: .thirdEye,
+ state: chakraStates[.thirdEye] ?? .normal,
+ action: { selectChakra(.thirdEye) }
+ )
+
+ Spacer().frame(height: 30)
+
+ // 喉轮
+ ChakraButton(
+ chakra: .throat,
+ state: chakraStates[.throat] ?? .weak,
+ action: { selectChakra(.throat) }
+ )
+
+ Spacer().frame(height: 40)
+
+ // 心轮
+ ChakraButton(
+ chakra: .heart,
+ state: chakraStates[.heart] ?? .normal,
+ action: { selectChakra(.heart) }
+ )
+
+ Spacer().frame(height: 40)
+
+ // 太阳轮
+ ChakraButton(
+ chakra: .solarPlexus,
+ state: chakraStates[.solarPlexus] ?? .normal,
+ action: { selectChakra(.solarPlexus) }
+ )
+
+ Spacer().frame(height: 40)
+
+ // 脐轮
+ ChakraButton(
+ chakra: .sacral,
+ state: chakraStates[.sacral] ?? .weak,
+ action: { selectChakra(.sacral) }
+ )
+
+ Spacer().frame(height: 40)
+
+ // 海底轮
+ ChakraButton(
+ chakra: .root,
+ state: chakraStates[.root] ?? .normal,
+ action: { selectChakra(.root) }
+ )
+ }
+ .frame(maxWidth: 200)
+
+ // 脉轮状态说明
+ VStack(alignment: .leading, spacing: 12) {
+ Text("脉轮状态说明")
+ .font(.headline)
+
+ HStack(spacing: 16) {
+ HStack {
+ Circle()
+ .fill(Color.green)
+ .frame(width: 12, height: 12)
+ Text("健康")
+ .font(.caption)
+ }
+
+ HStack {
+ Circle()
+ .fill(Color.orange.opacity(0.6))
+ .frame(width: 12, height: 12)
+ Text("疲弱")
+ .font(.caption)
+ }
+
+ HStack {
+ Circle()
+ .fill(Color.red.opacity(0.6))
+ .frame(width: 12, height: 12)
+ Text("受阻")
+ .font(.caption)
+ }
+ }
+ }
+ .padding()
+ .background(Color(.systemGray6))
+ .cornerRadius(12)
+ .padding(.horizontal)
+
+ // 快速疗愈选项
+ VStack(alignment: .leading, spacing: 12) {
+ Text("快速疗愈")
+ .font(.headline)
+
+ LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 12) {
+ QuickHealingCard(title: "全身平衡", icon: "figure.mind.and.body", color: .purple)
+ QuickHealingCard(title: "情绪释放", icon: "heart.fill", color: .pink)
+ QuickHealingCard(title: "能量充电", icon: "bolt.fill", color: .yellow)
+ QuickHealingCard(title: "深度放松", icon: "moon.fill", color: .blue)
+ }
+ }
+ .padding()
+ .background(Color(.systemGray6))
+ .cornerRadius(12)
+ .padding(.horizontal)
+ }
+ .padding(.bottom, 30)
+ }
+ }
+ .navigationBarHidden(true)
+ }
+ .sheet(item: $selectedChakra) { chakra in
+ ChakraHealingView(chakra: chakra) {
+ // 疗愈完成回调
+ updateChakraState(chakra, newState: .normal)
+ }
+ }
+ .onAppear {
+ initializeChakraStates()
+ }
+ }
+
+ private func selectChakra(_ chakra: ChakraType) {
+ selectedChakra = chakra
+ }
+
+ private func initializeChakraStates() {
+ // 初始化脉轮状态(模拟数据)
+ chakraStates = [
+ .root: .normal,
+ .sacral: .weak,
+ .solarPlexus: .normal,
+ .heart: .normal,
+ .throat: .weak,
+ .thirdEye: .normal,
+ .crown: .normal
+ ]
+ }
+
+ private func updateChakraState(_ chakra: ChakraType, newState: ChakraState) {
+ withAnimation {
+ chakraStates[chakra] = newState
+ }
+ }
+}
+
+// MARK: - 脉轮类型扩展
+extension ChakraType: Identifiable {
+ var id: String { rawValue }
+}
+
+// MARK: - 脉轮状态枚举
+enum ChakraState {
+ case normal // 健康
+ case weak // 疲弱
+ case blocked // 受阻
+
+ var opacity: Double {
+ switch self {
+ case .normal: return 1.0
+ case .weak: return 0.6
+ case .blocked: return 0.3
+ }
+ }
+}
+
+// MARK: - 脉轮按钮组件
+struct ChakraButton: View {
+ let chakra: ChakraType
+ let state: ChakraState
+ let action: () -> Void
+
+ @State private var isAnimating = false
+
+ var body: some View {
+ Button(action: action) {
+ ZStack {
+ // 外圈光晕效果
+ Circle()
+ .fill(
+ RadialGradient(
+ gradient: Gradient(colors: [chakra.color.opacity(0.3), Color.clear]),
+ center: .center,
+ startRadius: 20,
+ endRadius: 40
+ )
+ )
+ .frame(width: 80, height: 80)
+ .scaleEffect(isAnimating ? 1.2 : 1.0)
+ .opacity(state == .weak ? 0.8 : 1.0)
+
+ // 主圆圈
+ Circle()
+ .fill(
+ RadialGradient(
+ gradient: Gradient(colors: [chakra.color, chakra.color.opacity(0.7)]),
+ center: .center,
+ startRadius: 5,
+ endRadius: 25
+ )
+ )
+ .frame(width: 50, height: 50)
+ .opacity(state.opacity)
+ .overlay(
+ Circle()
+ .stroke(Color.white.opacity(0.8), lineWidth: 2)
+ )
+
+ // 脉轮名称
+ Text(chakra.rawValue)
+ .font(.caption2)
+ .fontWeight(.semibold)
+ .foregroundColor(.white)
+ .shadow(radius: 1)
+ }
+ }
+ .onAppear {
+ if state == .weak {
+ withAnimation(Animation.easeInOut(duration: 2).repeatForever(autoreverses: true)) {
+ isAnimating = true
+ }
+ }
+ }
+ }
+}
+
+// MARK: - 快速疗愈卡片
+struct QuickHealingCard: View {
+ let title: String
+ let icon: String
+ let color: Color
+
+ var body: some View {
+ Button(action: {
+ // 快速疗愈功能
+ }) {
+ VStack(spacing: 8) {
+ Image(systemName: icon)
+ .font(.title2)
+ .foregroundColor(color)
+
+ Text(title)
+ .font(.subheadline)
+ .fontWeight(.medium)
+ .foregroundColor(.primary)
+ }
+ .frame(maxWidth: .infinity)
+ .padding()
+ .background(Color(.systemBackground))
+ .cornerRadius(12)
+ .shadow(color: color.opacity(0.3), radius: 4, x: 0, y: 2)
+ }
+ }
+}
+
+#Preview {
+ HealingView()
+}
\ No newline at end of file
diff --git a/EmotionMuseum/EmotionMuseum/Views/InsightView.swift b/EmotionMuseum/EmotionMuseum/Views/InsightView.swift
new file mode 100644
index 0000000..6c523c3
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Views/InsightView.swift
@@ -0,0 +1,1306 @@
+//
+// InsightView.swift
+// EmotionMuseum
+//
+// Created by EmotionMuseum on 2024/01/01.
+//
+
+import SwiftUI
+import Foundation
+import AVFoundation
+#if canImport(UIKit)
+import UIKit
+#endif
+#if canImport(AppKit)
+import AppKit
+#endif
+
+// MARK: - 主题颜色配置
+struct ThemeColors {
+ static let wechatGreen = Color(red: 0.1, green: 0.7, blue: 0.3)
+ static let lightBackground = Color(red: 0.95, green: 0.95, blue: 0.97)
+ static let darkBackground = Color(red: 0.1, green: 0.1, blue: 0.12)
+
+ #if canImport(UIKit)
+ static let cardBackground = Color(UIColor.systemBackground)
+ static let secondaryBackground = Color(UIColor.secondarySystemBackground)
+ static let textPrimary = Color(UIColor.label)
+ static let textSecondary = Color(UIColor.secondaryLabel)
+ #elseif canImport(AppKit)
+ static let cardBackground = Color(NSColor.controlBackgroundColor)
+ static let secondaryBackground = Color(NSColor.unemphasizedSelectedContentBackgroundColor)
+ static let textPrimary = Color(NSColor.labelColor)
+ static let textSecondary = Color(NSColor.secondaryLabelColor)
+ #else
+ static let cardBackground = Color.white
+ static let secondaryBackground = Color.gray.opacity(0.1)
+ static let textPrimary = Color.primary
+ static let textSecondary = Color.secondary
+ #endif
+
+ static let accent = Color.accentColor
+}
+
+// MARK: - 心情数据模型
+struct MoodData: Identifiable {
+ let id = UUID()
+ let emoji: String
+ let name: String
+ let color: Color
+}
+
+// MARK: - AI对话记录模型
+struct AIConversation: Identifiable {
+ let id = UUID()
+ let date: Date
+ let userMessage: String
+ let aiResponse: String
+ let mood: String
+ let tags: [String]
+}
+
+// MARK: - 心情选择弹框
+struct MoodPickerSheet: View {
+ @Binding var selectedMood: String
+ @Binding var isPresented: Bool
+ let selectedDate: Date
+
+ let moods: [MoodData] = [
+ MoodData(emoji: "😊", name: "开心", color: .yellow),
+ MoodData(emoji: "😢", name: "难过", color: .blue),
+ MoodData(emoji: "😡", name: "愤怒", color: .red),
+ MoodData(emoji: "😴", name: "疲惫", color: .gray),
+ MoodData(emoji: "🤔", name: "思考", color: .purple),
+ MoodData(emoji: "😍", name: "兴奋", color: .pink),
+ MoodData(emoji: "😰", name: "焦虑", color: .orange),
+ MoodData(emoji: "😌", name: "平静", color: .green)
+ ]
+
+ var body: some View {
+ NavigationView {
+ VStack(spacing: 24) {
+ VStack(spacing: 8) {
+ Text(DateFormatter.fullDate.string(from: selectedDate))
+ .font(.title2)
+ .fontWeight(.semibold)
+ .foregroundColor(ThemeColors.textPrimary)
+
+ Text("选择今日心情")
+ .font(.subheadline)
+ .foregroundColor(ThemeColors.textSecondary)
+ }
+ .padding(.top)
+
+ LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 16) {
+ ForEach(moods) { mood in
+ Button(action: {
+ selectedMood = mood.emoji
+ isPresented = false
+ }) {
+ VStack(spacing: 12) {
+ Text(mood.emoji)
+ .font(.system(size: 40))
+
+ Text(mood.name)
+ .font(.headline)
+ .foregroundColor(ThemeColors.textPrimary)
+ }
+ .frame(maxWidth: .infinity)
+ .padding(20)
+ .background(mood.color.opacity(0.1))
+ .cornerRadius(16)
+ .overlay(
+ RoundedRectangle(cornerRadius: 16)
+ .stroke(mood.color.opacity(0.3), lineWidth: 1)
+ )
+ }
+ .buttonStyle(PlainButtonStyle())
+ }
+ }
+ .padding(.horizontal)
+
+ Spacer()
+ }
+ .background(ThemeColors.lightBackground.ignoresSafeArea())
+ .navigationTitle("")
+ #if os(iOS)
+ .navigationBarTitleDisplayMode(.inline)
+ #endif
+ .toolbar {
+ #if os(iOS)
+ ToolbarItem(placement: .topBarTrailing) {
+ Button("取消") {
+ isPresented = false
+ }
+ .foregroundColor(ThemeColors.wechatGreen)
+ }
+ #else
+ ToolbarItem(placement: .primaryAction) {
+ Button("取消") {
+ isPresented = false
+ }
+ .foregroundColor(ThemeColors.wechatGreen)
+ }
+ #endif
+ }
+ }
+ }
+}
+
+// MARK: - 单行日历
+struct SingleRowCalendar: View {
+ @Binding var selectedDate: Date
+ @State private var currentWeekOffset = 0
+ @State private var isExpanded = false
+ let onDateTap: (Date) -> Void
+
+ private var calendar: Calendar {
+ Calendar.current
+ }
+
+ private var weekDays: [Date] {
+ if isExpanded {
+ // 展开时显示最近一个月的日期
+ let today = Date()
+ let startOfMonth = calendar.date(byAdding: .day, value: -30, to: today) ?? today
+ return (0..<31).compactMap {
+ calendar.date(byAdding: .day, value: $0, to: startOfMonth)
+ }
+ } else {
+ // 收起时显示当前周
+ let startOfWeek = calendar.dateInterval(of: .weekOfYear, for: selectedDate)?.start ?? Date()
+ return (0..<7).compactMap {
+ calendar.date(byAdding: .day, value: $0 + currentWeekOffset * 7, to: startOfWeek)
+ }
+ }
+ }
+
+ var body: some View {
+ VStack(spacing: 12) {
+ HStack {
+ if !isExpanded {
+ Button(action: { currentWeekOffset -= 1 }) {
+ Image(systemName: "chevron.left")
+ .foregroundColor(ThemeColors.wechatGreen)
+ }
+ }
+
+ Spacer()
+
+ Text(DateFormatter.monthYear.string(from: selectedDate))
+ .font(.headline)
+ .foregroundColor(ThemeColors.textPrimary)
+
+ Spacer()
+
+ if !isExpanded {
+ Button(action: { currentWeekOffset += 1 }) {
+ Image(systemName: "chevron.right")
+ .foregroundColor(ThemeColors.wechatGreen)
+ }
+ }
+
+ Button(action: {
+ withAnimation(.spring()) {
+ isExpanded.toggle()
+ }
+ }) {
+ Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
+ .foregroundColor(ThemeColors.wechatGreen)
+ .padding(.leading, 8)
+ }
+ }
+
+ if isExpanded {
+ // 展开时显示月份视图
+ LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 7), spacing: 8) {
+ // 星期标题
+ ForEach(["日", "一", "二", "三", "四", "五", "六"], id: \.self) { weekday in
+ Text(weekday)
+ .font(.caption)
+ .foregroundColor(ThemeColors.textSecondary)
+ }
+
+ // 日期格子
+ ForEach(weekDays, id: \.self) { date in
+ VStack(spacing: 4) {
+ Button(action: {
+ selectedDate = date
+ onDateTap(date)
+ // 选择日期后自动收起
+ withAnimation(.spring()) {
+ isExpanded = false
+ }
+ }) {
+ ZStack {
+ Circle()
+ .fill(calendar.isDate(date, inSameDayAs: selectedDate) ?
+ ThemeColors.wechatGreen : Color.clear)
+ .frame(width: 36, height: 36)
+
+ Text("\(calendar.component(.day, from: date))")
+ .font(.system(size: 16, weight: .medium))
+ .foregroundColor(
+ calendar.isDate(date, inSameDayAs: selectedDate) ?
+ .white : ThemeColors.textPrimary
+ )
+ }
+ }
+
+ // 心情图标占位
+ Text(moodIconForDate(date))
+ .font(.caption)
+ }
+ }
+ }
+ } else {
+ // 收起时显示周视图
+ HStack(spacing: 0) {
+ ForEach(weekDays, id: \.self) { date in
+ VStack(spacing: 4) {
+ Text(DateFormatter.weekday.string(from: date))
+ .font(.caption)
+ .foregroundColor(ThemeColors.textSecondary)
+
+ Button(action: {
+ selectedDate = date
+ onDateTap(date)
+ }) {
+ ZStack {
+ Circle()
+ .fill(calendar.isDate(date, inSameDayAs: selectedDate) ?
+ ThemeColors.wechatGreen : Color.clear)
+ .frame(width: 36, height: 36)
+
+ Text("\(calendar.component(.day, from: date))")
+ .font(.system(size: 16, weight: .medium))
+ .foregroundColor(
+ calendar.isDate(date, inSameDayAs: selectedDate) ?
+ .white : ThemeColors.textPrimary
+ )
+ }
+ }
+
+ // 心情图标占位
+ Text(moodIconForDate(date))
+ .font(.caption)
+ }
+ .frame(maxWidth: .infinity)
+ }
+ }
+ }
+ }
+ .padding()
+ .background(ThemeColors.cardBackground)
+ .cornerRadius(16)
+ .shadow(color: .black.opacity(0.05), radius: 8, x: 0, y: 2)
+ .animation(.spring(), value: currentWeekOffset)
+ }
+
+ private func moodIconForDate(_ date: Date) -> String {
+ // 这里可以根据实际数据返回对应日期的心情图标
+ let day = Calendar.current.component(.day, from: date)
+ let moods = ["😊", "😢", "😡", "😴", "🤔", "😍", "😰"]
+ return day % 3 == 0 ? moods[day % moods.count] : ""
+ }
+}
+
+// MARK: - AI对话卡片
+struct AIConversationCard: View {
+ let conversation: AIConversation
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 12) {
+ HStack {
+ Text(DateFormatter.conversationDate.string(from: conversation.date))
+ .font(.caption)
+ .foregroundColor(ThemeColors.textSecondary)
+ Spacer()
+ Text(conversation.mood)
+ .font(.title3)
+ }
+
+ VStack(alignment: .leading, spacing: 8) {
+ Text("我: \(conversation.userMessage)")
+ .font(.body)
+ .foregroundColor(ThemeColors.textPrimary)
+ .padding(12)
+ .background(ThemeColors.secondaryBackground)
+ .cornerRadius(12)
+
+ Text("AI: \(conversation.aiResponse)")
+ .font(.body)
+ .foregroundColor(ThemeColors.textPrimary)
+ .padding(12)
+ .background(ThemeColors.wechatGreen.opacity(0.1))
+ .cornerRadius(12)
+ }
+
+ if !conversation.tags.isEmpty {
+ ScrollView(.horizontal, showsIndicators: false) {
+ HStack {
+ ForEach(conversation.tags, id: \.self) { tag in
+ Text(tag)
+ .font(.caption)
+ .padding(.horizontal, 8)
+ .padding(.vertical, 4)
+ .background(ThemeColors.accent.opacity(0.2))
+ .cornerRadius(8)
+ }
+ }
+ .padding(.horizontal)
+ }
+ }
+ }
+ .padding()
+ .background(ThemeColors.cardBackground)
+ .cornerRadius(16)
+ .shadow(color: .black.opacity(0.05), radius: 8, x: 0, y: 2)
+ }
+}
+
+// MARK: - 音频消息管理器
+class AudioManager: NSObject, ObservableObject, AVAudioRecorderDelegate {
+ static let shared = AudioManager()
+
+ @Published var isRecording = false
+ @Published var recordingDuration: TimeInterval = 0
+ @Published var currentTime: TimeInterval = 0
+
+ private var audioRecorder: AVAudioRecorder?
+ private var audioPlayer: AVAudioPlayer?
+ private var displayLink: CADisplayLink?
+ private var recordingStartTime: Date?
+ private var durationTimer: Timer?
+
+ override init() {
+ super.init()
+ setupAudioSession()
+ }
+
+ private func setupAudioSession() {
+ let session = AVAudioSession.sharedInstance()
+ do {
+ try session.setCategory(.playAndRecord, mode: .default)
+ try session.setActive(true)
+ } catch {
+ print("音频会话设置失败: \(error)")
+ }
+ }
+
+ func startRecording() {
+ let audioFilename = getDocumentsDirectory().appendingPathComponent("\(UUID().uuidString).m4a")
+
+ let settings = [
+ AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
+ AVSampleRateKey: 12000,
+ AVNumberOfChannelsKey: 1,
+ AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
+ ]
+
+ do {
+ audioRecorder = try AVAudioRecorder(url: audioFilename, settings: settings)
+ audioRecorder?.delegate = self
+ audioRecorder?.record()
+ isRecording = true
+ recordingStartTime = Date()
+
+ // 开始计时
+ startDurationTimer()
+ } catch {
+ print("录音失败: \(error)")
+ }
+ }
+
+ func stopRecording() -> (URL, TimeInterval)? {
+ audioRecorder?.stop()
+ let url = audioRecorder?.url
+ let duration = recordingDuration
+ audioRecorder = nil
+ isRecording = false
+
+ // 停止计时
+ stopDurationTimer()
+ recordingDuration = 0
+
+ if let url = url {
+ return (url, duration)
+ }
+ return nil
+ }
+
+ private func startDurationTimer() {
+ durationTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
+ guard let self = self else { return }
+ if let startTime = self.recordingStartTime {
+ self.recordingDuration = Date().timeIntervalSince(startTime)
+ }
+ }
+ }
+
+ private func stopDurationTimer() {
+ durationTimer?.invalidate()
+ durationTimer = nil
+ recordingStartTime = nil
+ }
+
+ func playAudio(url: URL, completion: @escaping () -> Void) {
+ stopCurrentAudio()
+
+ do {
+ audioPlayer = try AVAudioPlayer(contentsOf: url)
+ audioPlayer?.delegate = self
+ audioPlayer?.play()
+
+ // 开始更新播放进度
+ startPlaybackTimer()
+
+ // 设置播放完成回调
+ DispatchQueue.main.asyncAfter(deadline: .now() + (audioPlayer?.duration ?? 0)) {
+ completion()
+ }
+ } catch {
+ print("音频播放失败: \(error)")
+ }
+ }
+
+ func stopCurrentAudio() {
+ audioPlayer?.stop()
+ audioPlayer = nil
+ stopPlaybackTimer()
+ currentTime = 0
+ }
+
+ private func startPlaybackTimer() {
+ displayLink = CADisplayLink(target: self, selector: #selector(updatePlaybackTime))
+ displayLink?.add(to: .main, forMode: .common)
+ }
+
+ private func stopPlaybackTimer() {
+ displayLink?.invalidate()
+ displayLink = nil
+ }
+
+ @objc private func updatePlaybackTime() {
+ currentTime = audioPlayer?.currentTime ?? 0
+ }
+
+ private func getDocumentsDirectory() -> URL {
+ FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
+ }
+}
+
+extension AudioManager: AVAudioPlayerDelegate {
+ func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
+ stopCurrentAudio()
+ }
+}
+
+// MARK: - 音频消息视图
+struct AudioMessageView: View {
+ let url: URL
+ let duration: TimeInterval
+ @StateObject private var audioManager = AudioManager.shared
+ @State private var isPlaying = false
+ @State private var progress: CGFloat = 0
+ @State private var isAnimating = false
+
+ var body: some View {
+ Button(action: {
+ if isPlaying {
+ audioManager.stopCurrentAudio()
+ isPlaying = false
+ } else {
+ isPlaying = true
+ audioManager.playAudio(url: url) {
+ isPlaying = false
+ }
+ }
+ }) {
+ HStack(spacing: 8) {
+ // 播放/暂停按钮
+ Image(systemName: isPlaying ? "pause.circle.fill" : "play.circle.fill")
+ .font(.title2)
+ .foregroundColor(ThemeColors.wechatGreen)
+
+ // 音频波形动画
+ HStack(spacing: 2) {
+ ForEach(0..<5) { index in
+ RoundedRectangle(cornerRadius: 1)
+ .frame(width: 2, height: isPlaying ? 15 : 8)
+ .animation(
+ Animation.easeInOut(duration: 0.5)
+ .repeatForever()
+ .delay(Double(index) * 0.1),
+ value: isPlaying
+ )
+ }
+ }
+ .frame(width: 80)
+
+ // 时长显示
+ Text(formatDuration(duration))
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ .padding()
+ .background(Color.secondary.opacity(0.1))
+ .cornerRadius(12)
+ }
+ }
+
+ private func formatDuration(_ duration: TimeInterval) -> String {
+ let minutes = Int(duration) / 60
+ let seconds = Int(duration) % 60
+ return String(format: "%d:%02d", minutes, seconds)
+ }
+}
+
+// MARK: - 聊天消息模型
+struct ChatMessage: Identifiable {
+ let id: UUID
+ let content: String
+ let isUser: Bool
+ let timestamp: Date
+ let messageType: MessageType
+ let audioURL: URL?
+ let audioDuration: TimeInterval
+
+ enum MessageType {
+ case text
+ case audio
+ }
+
+ init(id: UUID = UUID(), content: String, isUser: Bool, timestamp: Date, messageType: MessageType, audioURL: URL? = nil, audioDuration: TimeInterval = 0) {
+ self.id = id
+ self.content = content
+ self.isUser = isUser
+ self.timestamp = timestamp
+ self.messageType = messageType
+ self.audioURL = audioURL
+ self.audioDuration = audioDuration
+ }
+}
+
+// MARK: - 聊天消息视图
+struct ChatMessageView: View {
+ let message: ChatMessage
+
+ var body: some View {
+ HStack {
+ if message.isUser {
+ Spacer()
+ }
+
+ VStack(alignment: message.isUser ? .trailing : .leading, spacing: 4) {
+ if message.messageType == .audio {
+ if let audioURL = message.audioURL {
+ AudioMessageView(url: audioURL, duration: message.audioDuration)
+ }
+ } else {
+ Text(message.content)
+ .padding(12)
+ .background(
+ message.isUser ?
+ ThemeColors.wechatGreen : ThemeColors.secondaryBackground
+ )
+ .foregroundColor(message.isUser ? .white : ThemeColors.textPrimary)
+ .cornerRadius(16)
+ }
+
+ Text(DateFormatter.conversationDate.string(from: message.timestamp))
+ .font(.caption2)
+ .foregroundColor(ThemeColors.textSecondary)
+ }
+
+ if !message.isUser {
+ Spacer()
+ }
+ }
+ }
+}
+
+// MARK: - AI对话视图
+struct AIChatView: View {
+ @Binding var isPresented: Bool
+ @State private var isVoiceMode = false
+ @State private var inputText = ""
+ @State private var chatMessages: [ChatMessage] = []
+ @StateObject private var audioManager = AudioManager.shared
+ let initialMessage: String
+ @State private var recordingFeedback = false
+
+ private let aiResponses = [
+ "我理解你现在的感受。让我们一起探讨这个问题,看看有什么可以帮助你的方式。",
+ "你说得很有道理。这种情况下,我建议你可以试着换个角度来看待这个问题。",
+ "听起来这确实是个令人困扰的情况。不过别担心,我们可以一步一步地来解决它。",
+ "你的感受是完全正常的。在这种情况下,很多人都会有类似的反应。",
+ "谢谢你愿意跟我分享这些。让我们一起来分析一下,看看有什么可以改善的地方。"
+ ]
+
+ var body: some View {
+ NavigationView {
+ VStack(spacing: 0) {
+ // 聊天消息列表
+ ScrollView {
+ LazyVStack(spacing: 16) {
+ ForEach(chatMessages) { message in
+ ChatMessageView(message: message)
+ }
+ }
+ .padding()
+ }
+
+ // 输入区域
+ VStack(spacing: 12) {
+ if isVoiceMode {
+ // 语音输入按钮
+ voiceButton
+ } else {
+ // 文字输入框
+ HStack(spacing: 12) {
+ TextField("输入消息...", text: $inputText)
+ .textFieldStyle(RoundedBorderTextFieldStyle())
+ .padding(.vertical, 8)
+
+ Button(action: sendTextMessage) {
+ Image(systemName: "arrow.up.circle.fill")
+ .font(.title2)
+ .foregroundColor(ThemeColors.wechatGreen)
+ }
+ .disabled(inputText.isEmpty)
+ }
+ .padding()
+ }
+ }
+ .background(Color(UIColor.systemBackground))
+ .shadow(color: .black.opacity(0.05), radius: 8, y: -4)
+ }
+ .navigationTitle("AI疗愈师")
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .navigationBarLeading) {
+ Button(action: { isVoiceMode.toggle() }) {
+ Image(systemName: isVoiceMode ? "keyboard" : "mic")
+ .foregroundColor(ThemeColors.wechatGreen)
+ }
+ }
+
+ ToolbarItem(placement: .navigationBarTrailing) {
+ Button(action: { isPresented = false }) {
+ Image(systemName: "chevron.down")
+ .foregroundColor(ThemeColors.textSecondary)
+ }
+ }
+ }
+ .onAppear {
+ if !initialMessage.isEmpty {
+ let userMessage = ChatMessage(
+ id: UUID(),
+ content: initialMessage,
+ isUser: true,
+ timestamp: Date(),
+ messageType: .text,
+ audioURL: nil,
+ audioDuration: 0
+ )
+ chatMessages.append(userMessage)
+ sendAIResponse()
+ }
+ }
+ }
+ }
+
+ private var voiceButton: some View {
+ Button(action: handleVoiceButton) {
+ ZStack {
+ Circle()
+ .fill(audioManager.isRecording ? Color.red : ThemeColors.wechatGreen)
+ .frame(width: 60, height: 60)
+ .scaleEffect(recordingFeedback ? 1.2 : 1.0)
+ .animation(.spring(response: 0.3), value: recordingFeedback)
+
+ Image(systemName: audioManager.isRecording ? "stop.fill" : "mic.fill")
+ .foregroundColor(.white)
+ .font(.title2)
+ }
+ }
+ .padding(.vertical)
+ .overlay(
+ Group {
+ if audioManager.isRecording {
+ VStack {
+ Text(formatDuration(audioManager.recordingDuration))
+ .font(.caption)
+ .foregroundColor(.secondary)
+ .padding(.vertical, 4)
+
+ // 录音波形动画
+ HStack(spacing: 4) {
+ ForEach(0..<3) { index in
+ Circle()
+ .fill(Color.red)
+ .frame(width: 6, height: 6)
+ .scaleEffect(recordingFeedback ? 1 : 0.5)
+ .animation(
+ Animation.easeInOut(duration: 0.5)
+ .repeatForever()
+ .delay(Double(index) * 0.2),
+ value: recordingFeedback
+ )
+ }
+ }
+ }
+ .offset(y: -40)
+ }
+ }
+ )
+ }
+
+ private func handleVoiceButton() {
+ if audioManager.isRecording {
+ if let (audioURL, duration) = audioManager.stopRecording() {
+ let message = ChatMessage(
+ id: UUID(),
+ content: "",
+ isUser: true,
+ timestamp: Date(),
+ messageType: .audio,
+ audioURL: audioURL,
+ audioDuration: duration
+ )
+ chatMessages.append(message)
+ sendAIResponse()
+ }
+ recordingFeedback = false
+ } else {
+ audioManager.startRecording()
+ recordingFeedback = true
+ }
+ }
+
+ private func sendTextMessage() {
+ guard !inputText.isEmpty else { return }
+
+ let message = ChatMessage(
+ id: UUID(),
+ content: inputText,
+ isUser: true,
+ timestamp: Date(),
+ messageType: .text,
+ audioURL: nil,
+ audioDuration: 0
+ )
+ chatMessages.append(message)
+ inputText = ""
+ sendAIResponse()
+ }
+
+ private func sendAIResponse() {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
+ let aiMessage = ChatMessage(
+ id: UUID(),
+ content: generateAIResponse(),
+ isUser: false,
+ timestamp: Date(),
+ messageType: .text,
+ audioURL: nil,
+ audioDuration: 0
+ )
+ chatMessages.append(aiMessage)
+ }
+ }
+
+ private func generateAIResponse() -> String {
+ return aiResponses.randomElement() ?? aiResponses[0]
+ }
+
+ private func formatDuration(_ duration: TimeInterval) -> String {
+ let minutes = Int(duration) / 60
+ let seconds = Int(duration) % 60
+ return String(format: "%d:%02d", minutes, seconds)
+ }
+}
+
+// MARK: - 主视图
+struct InsightView: View {
+ @State private var selectedDate = Date()
+ @State private var selectedMood = ""
+ @State private var emotionText = ""
+ @State private var emotionScore = 5.0
+ @State private var selectedTags: Set = []
+ @State private var showingAIAnalysis = false
+ @State private var isAnalyzing = false
+ @State private var aiResponse = ""
+ @State private var showingSettings = false
+ @State private var showingMoodPicker = false
+ @State private var conversations: [AIConversation] = []
+ @State private var showingConversationHistory = false
+ @State private var showingAIChat = false
+
+ let availableTags = ["工作", "学习", "家庭", "朋友", "健康", "爱情", "财务", "娱乐"]
+
+ var body: some View {
+ NavigationView {
+ mainContent
+ }
+ .sheet(isPresented: $showingSettings) {
+ InsightSettingsView()
+ }
+ .sheet(isPresented: $showingMoodPicker) {
+ MoodPickerSheet(
+ selectedMood: $selectedMood,
+ isPresented: $showingMoodPicker,
+ selectedDate: selectedDate
+ )
+ }
+ .sheet(isPresented: $showingConversationHistory) {
+ NavigationView {
+ ScrollView {
+ ConversationHistoryView(conversations: conversations)
+ }
+ .navigationTitle("AI对话历史")
+ .toolbar {
+ ToolbarItem(placement: .cancellationAction) {
+ Button("关闭") {
+ showingConversationHistory = false
+ }
+ }
+ }
+ }
+ }
+ .fullScreenCover(isPresented: $showingAIChat) {
+ AIChatView(isPresented: $showingAIChat, initialMessage: emotionText)
+ }
+ }
+
+ private var mainContent: some View {
+ ScrollView {
+ VStack(spacing: 16) {
+ // 导航栏
+ navigationHeader
+ .padding(.horizontal)
+
+ // 单行日历
+ calendarSection
+ .padding(.horizontal)
+
+ // 标签选择
+ tagsSection
+ .padding(.horizontal)
+
+ // 情感输入
+ emotionInputSection
+ .padding(.horizontal)
+
+ // AI分析按钮
+ aiAnalysisButton
+ .padding(.horizontal)
+
+ // 分析结果
+ if showingAIAnalysis {
+ aiAnalysisResultSection
+ .padding(.horizontal)
+ }
+
+ Spacer(minLength: 20)
+ }
+ }
+ .background(ThemeColors.lightBackground.ignoresSafeArea())
+ }
+
+ private var calendarSection: some View {
+ SingleRowCalendar(selectedDate: $selectedDate) { date in
+ selectedDate = date
+ showingMoodPicker = true
+ }
+ }
+
+ // MARK: - 导航栏
+ private var navigationHeader: some View {
+ HStack {
+ Button(action: { showingConversationHistory = true }) {
+ Image(systemName: "book.fill")
+ .font(.title2)
+ .foregroundColor(ThemeColors.wechatGreen)
+ }
+
+ Spacer()
+
+ Text("情感洞察")
+ .font(.title2)
+ .fontWeight(.semibold)
+ .foregroundColor(ThemeColors.textPrimary)
+
+ Spacer()
+
+ Button(action: { showingSettings = true }) {
+ Image(systemName: "gearshape.fill")
+ .font(.title2)
+ .foregroundColor(ThemeColors.textSecondary)
+ }
+ }
+ .padding(.horizontal)
+ }
+
+ // MARK: - 标签选择
+ private var tagsSection: some View {
+ VStack(alignment: .leading, spacing: 12) {
+ Text("相关标签(AI疗愈师3D IP)")
+ .font(.headline)
+ .foregroundColor(ThemeColors.textPrimary)
+
+ LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 4), spacing: 8) {
+ ForEach(availableTags, id: \.self) { tag in
+ Button(action: {
+ if selectedTags.contains(tag) {
+ selectedTags.remove(tag)
+ } else {
+ selectedTags.insert(tag)
+ }
+ }) {
+ Text(tag)
+ .font(.caption)
+ .padding(.horizontal, 12)
+ .padding(.vertical, 6)
+ .background(
+ selectedTags.contains(tag) ?
+ ThemeColors.wechatGreen : ThemeColors.secondaryBackground
+ )
+ .foregroundColor(
+ selectedTags.contains(tag) ? .white : ThemeColors.textPrimary
+ )
+ .cornerRadius(12)
+ }
+ }
+ }
+ }
+ .padding()
+ .background(ThemeColors.cardBackground)
+ .cornerRadius(16)
+ .shadow(color: .black.opacity(0.05), radius: 8, x: 0, y: 2)
+ }
+
+ // MARK: - 情感输入
+ private var emotionInputSection: some View {
+ VStack(alignment: .leading, spacing: 12) {
+ Text("描述你的感受")
+ .font(.headline)
+ .foregroundColor(ThemeColors.textPrimary)
+
+ TextEditor(text: $emotionText)
+ .frame(height: 240)
+ .padding(8)
+ .background(ThemeColors.secondaryBackground)
+ .cornerRadius(12)
+ .overlay(
+ RoundedRectangle(cornerRadius: 12)
+ .stroke(ThemeColors.wechatGreen.opacity(0.3), lineWidth: 1)
+ )
+ }
+ .padding()
+ .background(ThemeColors.cardBackground)
+ .cornerRadius(16)
+ .shadow(color: .black.opacity(0.05), radius: 8, x: 0, y: 2)
+ }
+
+ // MARK: - AI分析按钮
+ private var aiAnalysisButton: some View {
+ Button(action: {
+ if !emotionText.isEmpty {
+ showingAIChat = true
+ }
+ }) {
+ HStack {
+ Image(systemName: "brain.head.profile")
+ .font(.title3)
+ Text("AI情感分析")
+ .font(.headline)
+ .fontWeight(.semibold)
+ }
+ .foregroundColor(.white)
+ .frame(maxWidth: .infinity)
+ .frame(height: 50)
+ .background(
+ LinearGradient(
+ colors: [ThemeColors.wechatGreen, ThemeColors.wechatGreen.opacity(0.8)],
+ startPoint: .leading,
+ endPoint: .trailing
+ )
+ )
+ .cornerRadius(25)
+ .shadow(color: ThemeColors.wechatGreen.opacity(0.3), radius: 8, x: 0, y: 4)
+ }
+ .disabled(emotionText.isEmpty)
+ .opacity(emotionText.isEmpty ? 0.6 : 1.0)
+ }
+
+ // MARK: - AI分析结果
+ private var aiAnalysisResultSection: some View {
+ VStack(alignment: .leading, spacing: 12) {
+ HStack {
+ Image(systemName: "sparkles")
+ .foregroundColor(ThemeColors.wechatGreen)
+ Text("AI分析结果")
+ .font(.headline)
+ .foregroundColor(ThemeColors.textPrimary)
+ }
+
+ Text(aiResponse)
+ .font(.body)
+ .foregroundColor(ThemeColors.textPrimary)
+ .padding()
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .background(ThemeColors.wechatGreen.opacity(0.1))
+ .cornerRadius(12)
+
+ Button("保存记录") {
+ saveEmotionRecord()
+ }
+ .font(.subheadline)
+ .foregroundColor(ThemeColors.wechatGreen)
+ .frame(maxWidth: .infinity)
+ .frame(height: 44)
+ .background(ThemeColors.wechatGreen.opacity(0.1))
+ .cornerRadius(12)
+ }
+ .padding()
+ .background(ThemeColors.cardBackground)
+ .cornerRadius(16)
+ .shadow(color: .black.opacity(0.05), radius: 8, x: 0, y: 2)
+ }
+
+ // MARK: - 方法
+ private func getRandomGreeting() -> String {
+ let greetings = [
+ "今天的心情如何?",
+ "让我们一起探索内心世界",
+ "记录此刻的感受",
+ "你的情感值得被倾听",
+ "每一种情绪都有它的意义"
+ ]
+ return greetings.randomElement() ?? greetings[0]
+ }
+
+ private func analyzeEmotion() {
+ guard !emotionText.isEmpty else { return }
+
+ isAnalyzing = true
+
+ // 模拟AI分析过程
+ DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
+ aiResponse = generateAIResponse()
+ showingAIAnalysis = true
+ isAnalyzing = false
+ }
+ }
+
+ private func generateAIResponse() -> String {
+ let responses = [
+ "从你的描述中,我感受到了复杂的情感层次。这种感受是完全正常的,建议你可以通过深呼吸来缓解当前的情绪状态。",
+ "你的情感表达很真诚。每个人都会经历情绪的起伏,重要的是学会接纳和理解自己的感受。",
+ "我注意到你提到的几个关键词,这些都反映了你内心的真实想法。建议你可以尝试写日记来进一步整理思绪。",
+ "你的情感很丰富,这说明你是一个敏感且有深度的人。建议适当的运动和音乐可以帮助调节情绪。"
+ ]
+ return responses.randomElement() ?? responses[0]
+ }
+
+ private func saveEmotionRecord() {
+ let newConversation = AIConversation(
+ date: selectedDate,
+ userMessage: emotionText,
+ aiResponse: aiResponse,
+ mood: selectedMood,
+ tags: Array(selectedTags)
+ )
+
+ conversations.insert(newConversation, at: 0)
+
+ // 重置输入
+ emotionText = ""
+ selectedMood = ""
+ selectedTags.removeAll()
+ emotionScore = 5.0
+ showingAIAnalysis = false
+ aiResponse = ""
+ }
+}
+
+// MARK: - 设置页面
+struct InsightSettingsView: View {
+ @Environment(\.dismiss) private var dismiss
+ @State private var selectedTheme = 0
+ @State private var musicVolume = 0.5
+ @State private var soundEffectVolume = 0.7
+
+ let themes = ["自动", "浅色", "深色"]
+
+ var body: some View {
+ NavigationView {
+ List {
+ Section("主题设置") {
+ SettingRow(title: "界面主题", value: themes[selectedTheme]) {
+ // 主题选择逻辑
+ }
+ }
+
+ Section("音频设置") {
+ VStack {
+ SettingRow(title: "背景音乐", value: "\(Int(musicVolume * 100))%") {}
+ Slider(value: $musicVolume, in: 0...1)
+ .accentColor(ThemeColors.wechatGreen)
+ }
+
+ VStack {
+ SettingRow(title: "音效音量", value: "\(Int(soundEffectVolume * 100))%") {}
+ Slider(value: $soundEffectVolume, in: 0...1)
+ .accentColor(ThemeColors.wechatGreen)
+ }
+ }
+ }
+ .navigationTitle("设置")
+ #if os(iOS)
+ .navigationBarTitleDisplayMode(.inline)
+ #endif
+ .toolbar {
+ ToolbarItem(placement: {
+ #if os(iOS)
+ return .topBarTrailing
+ #elseif os(macOS)
+ return .automatic
+ #else
+ return .automatic
+ #endif
+ }()) {
+ Button("完成") {
+ dismiss()
+ }
+ .foregroundColor(ThemeColors.wechatGreen)
+ }
+ }
+ }
+ }
+}
+
+// MARK: - 设置行组件
+struct SettingRow: View {
+ let title: String
+ let value: String
+ let action: () -> Void
+
+ var body: some View {
+ HStack {
+ Text(title)
+ .foregroundColor(ThemeColors.textPrimary)
+ Spacer()
+ Text(value)
+ .foregroundColor(ThemeColors.textSecondary)
+ Image(systemName: "chevron.right")
+ .font(.caption)
+ .foregroundColor(ThemeColors.textSecondary)
+ }
+ .contentShape(Rectangle())
+ .onTapGesture(perform: action)
+ }
+}
+
+// MARK: - 情感记录卡片
+struct EmotionRecordCard: View {
+ let record: AIConversation
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 8) {
+ HStack {
+ Text(record.mood)
+ .font(.title2)
+ Spacer()
+ Text(DateFormatter.shortDate.string(from: record.date))
+ .font(.caption)
+ .foregroundColor(ThemeColors.textSecondary)
+ }
+
+ Text(record.userMessage)
+ .font(.body)
+ .foregroundColor(ThemeColors.textPrimary)
+ .lineLimit(3)
+ }
+ .padding()
+ .background(ThemeColors.cardBackground)
+ .cornerRadius(12)
+ .shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 2)
+ }
+}
+
+// MARK: - 日期格式化扩展
+extension DateFormatter {
+ static let monthYear: DateFormatter = {
+ let formatter = DateFormatter()
+ formatter.dateFormat = "yyyy年M月"
+ return formatter
+ }()
+
+ static let weekday: DateFormatter = {
+ let formatter = DateFormatter()
+ formatter.dateFormat = "E"
+ return formatter
+ }()
+
+ static let conversationDate: DateFormatter = {
+ let formatter = DateFormatter()
+ formatter.dateFormat = "M月d日 HH:mm"
+ return formatter
+ }()
+
+ static let fullDate: DateFormatter = {
+ let formatter = DateFormatter()
+ formatter.dateFormat = "yyyy年M月d日"
+ return formatter
+ }()
+
+ static let shortDate: DateFormatter = {
+ let formatter = DateFormatter()
+ formatter.dateFormat = "M/d"
+ return formatter
+ }()
+}
+
+// MARK: - 预览
+struct InsightView_Previews: PreviewProvider {
+ static var previews: some View {
+ InsightView()
+ .preferredColorScheme(.light)
+
+ InsightView()
+ .preferredColorScheme(.dark)
+ }
+}
+
+// MARK: - 对话历史视图
+struct ConversationHistoryView: View {
+ let conversations: [AIConversation]
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 12) {
+ if conversations.isEmpty {
+ VStack {
+ Image(systemName: "message.circle")
+ .font(.largeTitle)
+ .foregroundColor(ThemeColors.textSecondary)
+ Text("暂无对话记录")
+ .font(.subheadline)
+ .foregroundColor(ThemeColors.textSecondary)
+ }
+ .frame(maxWidth: .infinity)
+ .padding(40)
+ } else {
+ ForEach(conversations) { conversation in
+ AIConversationCard(conversation: conversation)
+ }
+ }
+ }
+ .padding()
+ .background(ThemeColors.cardBackground)
+ .cornerRadius(16)
+ .shadow(color: .black.opacity(0.05), radius: 8, x: 0, y: 2)
+ }
+}
diff --git a/EmotionMuseum/EmotionMuseum/Views/LoadingComponents.swift b/EmotionMuseum/EmotionMuseum/Views/LoadingComponents.swift
new file mode 100644
index 0000000..663a53e
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Views/LoadingComponents.swift
@@ -0,0 +1,438 @@
+//
+// LoadingComponents.swift
+// EmotionMuseum
+//
+// Created by 华中敏 on 2025/6/13.
+//
+
+import SwiftUI
+
+// MARK: - 加载状态枚举
+enum LoadingState {
+ case idle
+ case loading
+ case loaded
+ case error(String)
+}
+
+// MARK: - 主题管理器
+class ThemeManager: ObservableObject {
+ @Published var isDarkMode: Bool = false
+ @Published var systemFollowsDeviceTheme: Bool = true
+
+ init() {
+ // 从用户偏好设置中读取主题设置
+ self.isDarkMode = UserDefaults.standard.bool(forKey: "darkMode")
+ self.systemFollowsDeviceTheme = UserDefaults.standard.bool(forKey: "systemFollowsDeviceTheme")
+ }
+
+ func toggleTheme() {
+ isDarkMode.toggle()
+ UserDefaults.standard.set(isDarkMode, forKey: "darkMode")
+ }
+
+ func setSystemFollowing(_ follows: Bool) {
+ systemFollowsDeviceTheme = follows
+ UserDefaults.standard.set(follows, forKey: "systemFollowsDeviceTheme")
+ }
+}
+
+// MARK: - 主题颜色扩展
+extension Color {
+ static let theme = ColorTheme()
+}
+
+struct ColorTheme {
+ // 主色调
+ let primary = Color("PrimaryColor")
+ let secondary = Color("SecondaryColor")
+ let accent = Color("AccentColor")
+
+ // 背景色
+ let background = Color("BackgroundColor")
+ let cardBackground = Color("CardBackground")
+ let surfaceBackground = Color("SurfaceBackground")
+
+ // 文字颜色
+ let primaryText = Color("PrimaryText")
+ let secondaryText = Color("SecondaryText")
+ let tertiaryText = Color("TertiaryText")
+
+ // 边框颜色
+ let border = Color("BorderColor")
+ let divider = Color("DividerColor")
+
+ // 状态颜色
+ let success = Color("SuccessColor")
+ let warning = Color("WarningColor")
+ let error = Color("ErrorColor")
+
+ // 骨架屏颜色
+ let skeleton = Color("SkeletonColor")
+ let skeletonHighlight = Color("SkeletonHighlight")
+}
+
+// MARK: - 基础骨架屏组件
+struct SkeletonView: View {
+ @State private var isAnimating = false
+ let width: CGFloat?
+ let height: CGFloat
+ let cornerRadius: CGFloat
+
+ init(width: CGFloat? = nil, height: CGFloat = 20, cornerRadius: CGFloat = 8) {
+ self.width = width
+ self.height = height
+ self.cornerRadius = cornerRadius
+ }
+
+ var body: some View {
+ Rectangle()
+ .fill(
+ LinearGradient(
+ colors: [
+ Color.theme.skeleton,
+ Color.theme.skeletonHighlight,
+ Color.theme.skeleton
+ ],
+ startPoint: .leading,
+ endPoint: .trailing
+ )
+ .opacity(isAnimating ? 0.6 : 1.0)
+ )
+ .frame(width: width, height: height)
+ .cornerRadius(cornerRadius)
+ .onAppear {
+ withAnimation(
+ Animation
+ .easeInOut(duration: 1.2)
+ .repeatForever(autoreverses: true)
+ ) {
+ isAnimating = true
+ }
+ }
+ }
+}
+
+// MARK: - 文本骨架屏
+struct SkeletonText: View {
+ let lineCount: Int
+ let spacing: CGFloat
+
+ init(lineCount: Int = 3, spacing: CGFloat = 8) {
+ self.lineCount = lineCount
+ self.spacing = spacing
+ }
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: spacing) {
+ ForEach(0..: View {
+ let loadingState: LoadingState
+ let content: () -> Content
+ let loadingView: (() -> AnyView)?
+ let errorView: ((String) -> AnyView)?
+
+ init(
+ loadingState: LoadingState,
+ @ViewBuilder content: @escaping () -> Content,
+ loadingView: (() -> AnyView)? = nil,
+ errorView: ((String) -> AnyView)? = nil
+ ) {
+ self.loadingState = loadingState
+ self.content = content
+ self.loadingView = loadingView
+ self.errorView = errorView
+ }
+
+ var body: some View {
+ switch loadingState {
+ case .idle:
+ Color.clear
+ .onAppear {
+ // 可以在这里触发初始加载
+ }
+
+ case .loading:
+ if let loadingView = loadingView {
+ loadingView()
+ } else {
+ defaultLoadingView
+ }
+
+ case .loaded:
+ content()
+ .transition(.opacity.combined(with: .scale(scale: 0.95)))
+
+ case .error(let message):
+ if let errorView = errorView {
+ errorView(message)
+ } else {
+ defaultErrorView(message: message)
+ }
+ }
+ }
+
+ private var defaultLoadingView: some View {
+ VStack(spacing: 20) {
+ ProgressView()
+ .scaleEffect(1.5)
+ .progressViewStyle(CircularProgressViewStyle(tint: Color.theme.accent))
+
+ Text("加载中...")
+ .font(.subheadline)
+ .foregroundColor(Color.theme.secondaryText)
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .background(Color.theme.background)
+ }
+
+ private func defaultErrorView(message: String) -> some View {
+ VStack(spacing: 16) {
+ Image(systemName: "exclamationmark.triangle")
+ .font(.system(size: 48))
+ .foregroundColor(Color.theme.error)
+
+ Text("加载失败")
+ .font(.headline)
+ .foregroundColor(Color.theme.primaryText)
+
+ Text(message)
+ .font(.subheadline)
+ .foregroundColor(Color.theme.secondaryText)
+ .multilineTextAlignment(.center)
+
+ Button("重试") {
+ // 重试逻辑需要由父视图处理
+ }
+ .buttonStyle(PrimaryButtonStyle())
+ }
+ .padding(32)
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .background(Color.theme.background)
+ }
+}
+
+// MARK: - 按钮样式
+struct PrimaryButtonStyle: ButtonStyle {
+ func makeBody(configuration: Configuration) -> some View {
+ configuration.label
+ .padding(.horizontal, 24)
+ .padding(.vertical, 12)
+ .background(Color.theme.accent)
+ .foregroundColor(.white)
+ .cornerRadius(12)
+ .scaleEffect(configuration.isPressed ? 0.95 : 1.0)
+ .animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
+ }
+}
+
+// MARK: - 列表加载视图
+struct LoadingList: View {
+ let itemCount: Int
+ let showAvatar: Bool
+ let showTrailing: Bool
+
+ init(itemCount: Int = 5, showAvatar: Bool = true, showTrailing: Bool = true) {
+ self.itemCount = itemCount
+ self.showAvatar = showAvatar
+ self.showTrailing = showTrailing
+ }
+
+ var body: some View {
+ LazyVStack(spacing: 0) {
+ ForEach(0..: View {
+ @State private var isRefreshing = false
+ let onRefresh: () async -> Void
+ let content: () -> Content
+
+ init(
+ onRefresh: @escaping () async -> Void,
+ @ViewBuilder content: @escaping () -> Content
+ ) {
+ self.onRefresh = onRefresh
+ self.content = content
+ }
+
+ var body: some View {
+ ScrollView {
+ content()
+ }
+ .refreshable {
+ await onRefresh()
+ }
+ }
+}
+
+// MARK: - 加载更多组件
+struct LoadMoreView: View {
+ @State private var isLoading = false
+ let onLoadMore: () async -> Void
+ let hasMore: Bool
+
+ init(hasMore: Bool = true, onLoadMore: @escaping () async -> Void) {
+ self.hasMore = hasMore
+ self.onLoadMore = onLoadMore
+ }
+
+ var body: some View {
+ HStack {
+ Spacer()
+
+ if hasMore {
+ if isLoading {
+ HStack(spacing: 8) {
+ ProgressView()
+ .scaleEffect(0.8)
+ Text("加载中...")
+ .font(.caption)
+ .foregroundColor(Color.theme.secondaryText)
+ }
+ } else {
+ Button("加载更多") {
+ Task {
+ isLoading = true
+ await onLoadMore()
+ isLoading = false
+ }
+ }
+ .font(.caption)
+ .foregroundColor(Color.theme.accent)
+ }
+ } else {
+ Text("没有更多内容了")
+ .font(.caption)
+ .foregroundColor(Color.theme.tertiaryText)
+ }
+
+ Spacer()
+ }
+ .padding(.vertical, 16)
+ }
+}
+
+// MARK: - 预览
+#Preview("骨架屏组件") {
+ VStack(spacing: 20) {
+ SkeletonCard()
+ SkeletonListItem()
+ SkeletonText()
+ }
+ .padding()
+ .background(Color.theme.background)
+}
\ No newline at end of file
diff --git a/EmotionMuseum/EmotionMuseum/Views/LoadingOverlay.swift b/EmotionMuseum/EmotionMuseum/Views/LoadingOverlay.swift
new file mode 100644
index 0000000..a255486
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Views/LoadingOverlay.swift
@@ -0,0 +1,55 @@
+//
+// LoadingOverlay.swift
+// EmotionMuseum
+//
+// Created by 华中敏 on 2025/7/5.
+//
+
+import SwiftUI
+
+struct LoadingOverlay: View {
+ let message: String
+ @State private var rotationAngle: Double = 0
+
+ var body: some View {
+ ZStack {
+ // 半透明背景
+ Color.black
+ .opacity(0.3)
+ .ignoresSafeArea()
+
+ // 加载内容
+ VStack(spacing: 20) {
+ // 旋转的加载指示器
+ Image(systemName: "brain.head.profile")
+ .font(.system(size: 40))
+ .foregroundColor(Color("AccentColor"))
+ .rotationEffect(.degrees(rotationAngle))
+ .onAppear {
+ withAnimation(.linear(duration: 2).repeatForever(autoreverses: false)) {
+ rotationAngle = 360
+ }
+ }
+
+ // 加载文字
+ Text(message)
+ .font(.subheadline)
+ .foregroundColor(Color("PrimaryText"))
+ .multilineTextAlignment(.center)
+ }
+ .padding(30)
+ .background(
+ RoundedRectangle(cornerRadius: 16)
+ .fill(Color("CardBackground"))
+ .shadow(color: .black.opacity(0.1), radius: 10, x: 0, y: 5)
+ )
+ }
+ .transition(.opacity)
+ .zIndex(999) // 确保在最顶层
+ }
+}
+
+#Preview {
+ LoadingOverlay(message: "正在加载数据...")
+ .background(Color.gray.opacity(0.3))
+}
\ No newline at end of file
diff --git a/EmotionMuseum/EmotionMuseum/Views/MapView.swift b/EmotionMuseum/EmotionMuseum/Views/MapView.swift
new file mode 100644
index 0000000..dab3955
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Views/MapView.swift
@@ -0,0 +1,232 @@
+import SwiftUI
+// import AMapFoundationKit // 临时注释,需要安装CocoaPods依赖
+// import MAMapKit // 临时注释,需要安装CocoaPods依赖
+import UIKit
+import MapKit
+import CoreLocation
+
+/// 地图视图
+/// @Author huazhongmin
+/// @Time 2024-03-24
+/// @Description 使用系统地图展示地图内容,默认定位到用户当前位置
+struct MapView: View {
+ @Binding var shouldMoveToUserLocation: Bool
+
+ init(shouldMoveToUserLocation: Binding = .constant(false)) {
+ self._shouldMoveToUserLocation = shouldMoveToUserLocation
+ }
+
+ var body: some View {
+ MapViewRepresentable(shouldMoveToUserLocation: $shouldMoveToUserLocation)
+ .edgesIgnoringSafeArea(.all)
+ }
+}
+
+/// 系统地图SwiftUI包装器
+struct MapViewRepresentable: UIViewRepresentable {
+ @Binding var shouldMoveToUserLocation: Bool
+
+ // 默认位置(天安门坐标)
+ let defaultLocation = CLLocationCoordinate2D(
+ latitude: 39.908823,
+ longitude: 116.397470
+ )
+
+ // 创建地图视图的协调器
+ func makeCoordinator() -> Coordinator {
+ Coordinator(self)
+ }
+
+ // 创建地图视图
+ func makeUIView(context: Context) -> MKMapView {
+ let mapView = MKMapView(frame: .zero)
+ mapView.delegate = context.coordinator
+
+ // 显示用户位置
+ mapView.showsUserLocation = true
+ mapView.userTrackingMode = .none // 不自动跟踪,手动控制
+
+ // 设置地图类型和控件
+ mapView.mapType = .standard
+ mapView.showsCompass = true
+ mapView.showsScale = true
+ mapView.showsTraffic = false
+
+ // 允许缩放和滚动
+ mapView.isZoomEnabled = true
+ mapView.isScrollEnabled = true
+ mapView.isRotateEnabled = true
+ mapView.isPitchEnabled = true
+
+ // 设置初始区域(在获取用户位置前显示默认位置)
+ let initialRegion = MKCoordinateRegion(
+ center: defaultLocation,
+ span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
+ )
+ mapView.setRegion(initialRegion, animated: false)
+
+ // 保存mapView引用到coordinator并开始定位
+ context.coordinator.mapView = mapView
+ context.coordinator.startLocationUpdates()
+
+ return mapView
+ }
+
+ // 更新地图视图
+ func updateUIView(_ mapView: MKMapView, context: Context) {
+ // 检查是否需要重新请求定位权限
+ context.coordinator.checkLocationPermission()
+
+ // 检查是否需要移动到用户位置
+ if shouldMoveToUserLocation {
+ context.coordinator.moveToUserLocation()
+ // 重置状态
+ DispatchQueue.main.async {
+ shouldMoveToUserLocation = false
+ }
+ }
+ }
+
+ // 协调器类,用于处理地图代理事件和位置管理
+ class Coordinator: NSObject, MKMapViewDelegate, CLLocationManagerDelegate {
+ var parent: MapViewRepresentable
+ var mapView: MKMapView?
+ var locationManager: CLLocationManager
+ var hasInitialLocationSet = false
+
+ init(_ parent: MapViewRepresentable) {
+ self.parent = parent
+ self.locationManager = CLLocationManager()
+ super.init()
+
+ // 配置位置管理器
+ locationManager.delegate = self
+ locationManager.desiredAccuracy = kCLLocationAccuracyBest
+ locationManager.distanceFilter = 10 // 移动10米以上才更新
+ }
+
+ // 开始位置更新
+ func startLocationUpdates() {
+ checkLocationPermission()
+ }
+
+ // 检查定位权限并请求权限
+ func checkLocationPermission() {
+ guard CLLocationManager.locationServicesEnabled() else {
+ print("定位服务未启用")
+ return
+ }
+
+ let authorizationStatus = locationManager.authorizationStatus
+
+ switch authorizationStatus {
+ case .notDetermined:
+ // 首次使用,请求定位权限
+ locationManager.requestWhenInUseAuthorization()
+
+ case .authorizedWhenInUse, .authorizedAlways:
+ // 已授权,开始定位
+ locationManager.startUpdatingLocation()
+
+ case .denied, .restricted:
+ // 被拒绝或受限,显示默认位置
+ print("定位权限被拒绝,显示默认位置")
+ setDefaultLocation()
+
+ @unknown default:
+ print("未知的定位权限状态")
+ setDefaultLocation()
+ }
+ }
+
+ // 设置默认位置
+ func setDefaultLocation() {
+ DispatchQueue.main.async {
+ guard let mapView = self.mapView else { return }
+
+ let region = MKCoordinateRegion(
+ center: self.parent.defaultLocation,
+ span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
+ )
+ mapView.setRegion(region, animated: true)
+ }
+ }
+
+ // MARK: - CLLocationManagerDelegate
+
+ // 权限状态变化
+ func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
+ checkLocationPermission()
+ }
+
+ // 位置更新成功
+ func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
+ guard let location = locations.last else { return }
+
+ // 只在首次获取位置时自动移动地图到用户位置
+ if !hasInitialLocationSet {
+ DispatchQueue.main.async {
+ guard let mapView = self.mapView else { return }
+
+ let userRegion = MKCoordinateRegion(
+ center: location.coordinate,
+ span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
+ )
+ mapView.setRegion(userRegion, animated: true)
+ self.hasInitialLocationSet = true
+
+ print("已定位到用户位置: \(location.coordinate)")
+ }
+
+ // 定位成功后可以停止持续更新,节省电量
+ locationManager.stopUpdatingLocation()
+ }
+ }
+
+ // 位置更新失败
+ func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
+ print("定位失败: \(error.localizedDescription)")
+
+ // 定位失败时显示默认位置
+ if !hasInitialLocationSet {
+ setDefaultLocation()
+ hasInitialLocationSet = true
+ }
+ }
+
+ // MARK: - MKMapViewDelegate
+
+ // 用户位置更新(地图上的蓝点)
+ func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
+ // 这里不自动移动地图,避免干扰用户操作
+ // 用户位置的蓝点会自动显示在地图上
+ }
+
+ // 地图区域变化
+ func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
+ // 可以在这里处理地图区域变化事件
+ }
+
+ // 手动回到用户位置的方法(供外部调用)
+ func moveToUserLocation() {
+ guard let mapView = self.mapView,
+ let userLocation = mapView.userLocation.location else {
+ // 如果没有用户位置,重新开始定位
+ checkLocationPermission()
+ return
+ }
+
+ let userRegion = MKCoordinateRegion(
+ center: userLocation.coordinate,
+ span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
+ )
+ mapView.setRegion(userRegion, animated: true)
+ }
+ }
+}
+
+struct MapView_Previews: PreviewProvider {
+ static var previews: some View {
+ MapView()
+ }
+}
\ No newline at end of file
diff --git a/EmotionMuseum/EmotionMuseum/Views/RecordView.swift b/EmotionMuseum/EmotionMuseum/Views/RecordView.swift
new file mode 100644
index 0000000..45f9946
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Views/RecordView.swift
@@ -0,0 +1,662 @@
+//
+// RecordView.swift
+// EmotionMuseum
+//
+// Created by 华中敏 on 2025/6/13.
+//
+
+import SwiftUI
+
+// MARK: - 记录页面主视图
+struct RecordView: View {
+ @EnvironmentObject var navigationManager: NavigationManager
+ @EnvironmentObject var themeManager: ThemeManager
+ @EnvironmentObject var mockData: MockDataManager
+ @StateObject private var aiService = MockAIService()
+ @State private var selectedDate = Date()
+ @State private var inputText = ""
+ @State private var showingMoodPicker = false
+ @State private var selectedMood = ""
+ @State private var loadingState: LoadingState = .idle
+ @State private var isInitialLoading = true
+
+ var body: some View {
+ LoadingStateView(loadingState: isInitialLoading ? .loading : .loaded) {
+ VStack(spacing: 0) {
+ // 可滚动的主要内容区域
+ ScrollView {
+ LazyVStack(spacing: 16) {
+ // 顶部导航栏
+ topNavigationBar
+ .padding(.horizontal, 16)
+ .padding(.top, 8)
+ .transition(.move(edge: .top).combined(with: .opacity))
+
+ // 日历组件
+ emotionCalendar
+ .padding(.horizontal, 16)
+ .transition(.scale(scale: 0.8).combined(with: .opacity))
+
+ // AI助手头像
+ aiAvatarSection
+ .padding(.horizontal, 16)
+ .transition(.scale(scale: 0.9).combined(with: .opacity))
+
+ // 聊天区域
+ chatArea
+ .padding(.horizontal, 16)
+ .transition(.opacity.combined(with: .slide))
+ }
+ .padding(.bottom, 10) // 为输入区域留出空间
+ }
+ .refreshable {
+ await simulateRefresh()
+ }
+
+ // 固定在底部的输入区域
+ inputArea
+ .background(Color.theme.cardBackground)
+ .shadow(color: .black.opacity(0.1), radius: 8, y: -4)
+ .transition(.move(edge: .bottom).combined(with: .opacity))
+ }
+ } loadingView: {
+ AnyView(recordViewSkeleton)
+ }
+ .background(Color.theme.background)
+ .environmentObject(themeManager)
+ .preferredColorScheme(themeManager.systemFollowsDeviceTheme ? nil : (themeManager.isDarkMode ? .dark : .light))
+
+ .ignoresSafeArea(.keyboard, edges: .bottom) // 键盘出现时不影响布局
+ .sheet(isPresented: $showingMoodPicker) {
+ MoodPickerView(
+ selectedDate: selectedDate,
+ selectedMood: $selectedMood,
+ isPresented: $showingMoodPicker
+ )
+ }
+ .onAppear {
+ simulateInitialLoading()
+ }
+ }
+
+ // MARK: - 顶部导航栏
+ private var topNavigationBar: some View {
+ HStack {
+ // 左上角 - 聊天记录图标
+ Button(action: { navigationManager.showChatHistory() }) {
+ Image(systemName: "bubble.left.and.bubble.right.fill")
+ .font(.title3)
+ .foregroundColor(.blue)
+ }
+
+ Spacer()
+
+ // 中间 - 页面标题
+ Text("情绪记录")
+ .font(.title2)
+ .fontWeight(.semibold)
+ .foregroundColor(.primary)
+
+ Spacer()
+
+ // 右上角 - 设置图标
+ Button(action: { navigationManager.navigateToSettings() }) {
+ Image(systemName: "gearshape.fill")
+ .font(.title3)
+ .foregroundColor(.gray)
+ }
+ }
+ }
+
+ // MARK: - 情绪日历
+ private var emotionCalendar: some View {
+ VStack(spacing: 12) {
+ HStack {
+ Text(selectedDate.formatted(.dateTime.month(.wide)))
+ .font(.headline)
+ .foregroundColor(.primary)
+
+ Spacer()
+
+ Button(action: { showingMoodPicker = true }) {
+ HStack(spacing: 4) {
+ Text(selectedMood.isEmpty ? "记录心情" : selectedMood)
+ .font(.caption)
+ Image(systemName: "plus.circle")
+ .font(.caption)
+ }
+ .foregroundColor(.blue)
+ }
+ }
+
+ // 7天日历视图
+ ScrollView(.horizontal, showsIndicators: false) {
+ HStack(spacing: 16) {
+ ForEach(-3...3, id: \.self) { dayOffset in
+ let date = Calendar.current.date(byAdding: .day, value: dayOffset, to: selectedDate) ?? selectedDate
+ let isToday = Calendar.current.isDate(date, inSameDayAs: Date())
+ let isSelected = Calendar.current.isDate(date, inSameDayAs: selectedDate)
+
+ VStack(spacing: 4) {
+ Text(DateFormatter.weekdayShort.string(from: date))
+ .font(.caption2)
+ .foregroundColor(.secondary)
+
+ Button(action: { selectedDate = date }) {
+ VStack(spacing: 2) {
+ Text("\(Calendar.current.component(.day, from: date))")
+ .font(.system(size: 16, weight: .medium))
+ .foregroundColor(isSelected ? .white : .primary)
+
+ // 情绪点
+ Circle()
+ .fill(emotionColorForDate(date))
+ .frame(width: 6, height: 6)
+ .opacity(hasEmotionRecord(for: date) ? 1 : 0)
+ }
+ .frame(width: 40, height: 44)
+ .background(
+ RoundedRectangle(cornerRadius: 12)
+ .fill(isSelected ? Color.blue : Color.clear)
+ )
+ .overlay(
+ RoundedRectangle(cornerRadius: 12)
+ .stroke(isToday ? Color.blue : Color.clear, lineWidth: 1)
+ )
+ }
+ }
+ }
+ }
+ .padding(.horizontal, 16)
+ }
+ }
+ .padding()
+ .background(Color.theme.cardBackground)
+ .cornerRadius(16)
+ }
+
+
+
+ // MARK: - AI助手头像区域
+ private var aiAvatarSection: some View {
+ HStack {
+ Spacer()
+
+ // AI助手形象 - 紧凑版本
+ ZStack {
+ // 背景渐变
+ Circle()
+ .fill(
+ LinearGradient(
+ colors: [Color.blue.opacity(0.1), Color.purple.opacity(0.1)],
+ startPoint: .topLeading,
+ endPoint: .bottomTrailing
+ )
+ )
+ .frame(width: 80, height: 80)
+
+ // AI助手图标
+ Image(systemName: "brain.head.profile")
+ .font(.system(size: 35))
+ .foregroundColor(.blue)
+ .scaleEffect(aiService.isLoading ? 1.1 : 1.0)
+ .animation(.easeInOut(duration: 1).repeatForever(), value: aiService.isLoading)
+ }
+
+ Spacer()
+ }
+ .padding(.vertical, 10)
+ }
+
+ // MARK: - 聊天区域
+ private var chatArea: some View {
+ VStack(spacing: 0) {
+ if mockData.conversations.isEmpty {
+ // 默认状态:显示问候语和快捷回复
+ defaultChatContent
+ } else {
+ // 有聊天记录时显示对话列表
+ chatMessagesList
+ }
+ }
+ .frame(minHeight: 300) // 设置最小高度确保有足够的聊天空间
+ }
+
+ // MARK: - 默认聊天内容
+ private var defaultChatContent: some View {
+ VStack(spacing: 20) {
+ // AI问候消息气泡
+ HStack {
+ // AI头像
+ Circle()
+ .fill(Color.blue.opacity(0.1))
+ .frame(width: 32, height: 32)
+ .overlay(
+ Image(systemName: "brain.head.profile")
+ .font(.system(size: 16))
+ .foregroundColor(.blue)
+ )
+
+ // 问候消息气泡
+ VStack(alignment: .leading, spacing: 8) {
+ Text("你好!我是你的情绪陪伴师")
+ .font(.system(size: 16, weight: .medium))
+ .foregroundColor(Color.theme.primaryText)
+
+ Text(getGreetingText())
+ .font(.system(size: 14))
+ .foregroundColor(Color.theme.secondaryText)
+ }
+ .padding(.horizontal, 16)
+ .padding(.vertical, 12)
+ .background(
+ RoundedRectangle(cornerRadius: 18)
+ .fill(Color.theme.surfaceBackground)
+ .shadow(color: Color.black.opacity(0.05), radius: 2, x: 0, y: 1)
+ )
+
+ Spacer()
+ }
+ .padding(.horizontal, 4)
+
+ // 快捷回复提示
+ HStack {
+ Spacer()
+ Text("你可以这样开始对话")
+ .font(.caption)
+ .foregroundColor(Color.theme.tertiaryText)
+ Spacer()
+ }
+ .padding(.top, 10)
+
+ // 快捷回复卡片
+ chatQuickReplyCards
+ }
+ .padding(.vertical, 20)
+ }
+
+ // MARK: - 聊天消息列表
+ private var chatMessagesList: some View {
+ LazyVStack(spacing: 12) {
+ ForEach(mockData.conversations.prefix(3), id: \.id) { conversation in
+ ConversationPreviewCard(conversation: conversation) {
+ navigationManager.navigateToChat(conversation: conversation)
+ }
+ }
+ }
+ .padding(.vertical, 10)
+ }
+
+ // MARK: - 聊天样式的快捷回复卡片
+ private var chatQuickReplyCards: some View {
+ LazyVGrid(columns: [
+ GridItem(.flexible(), spacing: 8),
+ GridItem(.flexible(), spacing: 8)
+ ], spacing: 12) {
+ ForEach(quickReplies, id: \.self) { reply in
+ Button(action: { sendQuickReply(reply) }) {
+ Text(reply)
+ .font(.system(size: 14, weight: .medium))
+ .foregroundColor(Color("AccentColor"))
+ .multilineTextAlignment(.center)
+ .lineLimit(2)
+ .padding(.horizontal, 12)
+ .padding(.vertical, 14)
+ .frame(maxWidth: .infinity)
+ .frame(minHeight: 70)
+ .background(
+ RoundedRectangle(cornerRadius: 12)
+ .fill(Color.theme.cardBackground)
+ .overlay(
+ RoundedRectangle(cornerRadius: 12)
+ .stroke(Color("AccentColor").opacity(0.3), lineWidth: 1)
+ )
+ .shadow(
+ color: Color.black.opacity(0.05),
+ radius: 3,
+ x: 0,
+ y: 2
+ )
+ )
+ }
+ .buttonStyle(PlainButtonStyle())
+ .scaleEffect(1.0)
+ .animation(.spring(response: 0.3, dampingFraction: 0.8), value: false)
+ }
+ }
+ }
+
+
+
+
+
+ private let quickReplies = [
+ "我今天感觉有点焦虑",
+ "想和你聊聊最近的压力",
+ "今天发生了一些开心的事",
+ "我需要一些建议"
+ ]
+
+ // MARK: - 输入区域
+ private var inputArea: some View {
+ VStack(spacing: 0) {
+ // 渐变分隔线
+ LinearGradient(
+ colors: [Color.theme.divider.opacity(0), Color.theme.divider, Color.theme.divider.opacity(0)],
+ startPoint: .leading,
+ endPoint: .trailing
+ )
+ .frame(height: 1)
+
+ // 紧凑的输入区域
+ HStack(spacing: 12) {
+ // 图片按钮
+ Button(action: { }) {
+ Image(systemName: "photo.circle.fill")
+ .font(.title2)
+ .foregroundColor(Color.theme.secondaryText)
+ }
+
+ // 主输入框容器
+ HStack(spacing: 8) {
+ TextField("说说你的感受...", text: $inputText)
+ .textFieldStyle(PlainTextFieldStyle())
+ .foregroundColor(Color.theme.primaryText)
+ .padding(.vertical, 12)
+ .padding(.leading, 16)
+
+ // 语音输入按钮
+ Button(action: { }) {
+ Image(systemName: "mic.circle.fill")
+ .font(.title2)
+ .foregroundColor(Color("AccentColor"))
+ }
+ .padding(.trailing, 8)
+ }
+ .background(
+ RoundedRectangle(cornerRadius: 24)
+ .fill(Color.theme.surfaceBackground)
+ .shadow(
+ color: Color.black.opacity(0.05),
+ radius: 2,
+ x: 0,
+ y: 1
+ )
+ )
+
+ // 发送按钮
+ Button(action: sendMessage) {
+ Image(systemName: inputText.isEmpty ? "arrow.up.circle" : "arrow.up.circle.fill")
+ .font(.title2)
+ .foregroundColor(inputText.isEmpty ? Color.theme.secondaryText : Color("AccentColor"))
+ .scaleEffect(inputText.isEmpty ? 0.9 : 1.0)
+ .animation(.spring(response: 0.3, dampingFraction: 0.8), value: inputText.isEmpty)
+ }
+ .disabled(inputText.isEmpty)
+ }
+ .padding(.horizontal, 16)
+ .padding(.top, 12)
+ .padding(.bottom, 12) // 适中的底部间距
+ .background(
+ Color.theme.cardBackground
+ .overlay(
+ // 顶部高光效果
+ LinearGradient(
+ colors: [
+ Color.white.opacity(themeManager.isDarkMode ? 0.05 : 0.4),
+ Color.clear
+ ],
+ startPoint: .top,
+ endPoint: .bottom
+ )
+ .frame(height: 1),
+ alignment: .top
+ )
+ )
+
+ // AI状态指示器(如果需要的话)
+ if aiService.isLoading {
+ HStack(spacing: 4) {
+ ProgressView()
+ .scaleEffect(0.7)
+ .tint(Color("AccentColor"))
+ Text("AI思考中...")
+ .font(.caption2)
+ .foregroundColor(Color.theme.secondaryText)
+ }
+ .padding(.horizontal, 16)
+ .padding(.bottom, 12)
+ .background(Color.theme.cardBackground)
+ }
+ }
+ }
+
+ // MARK: - 私有方法
+
+ private func getGreetingText() -> String {
+ let hour = Calendar.current.component(.hour, from: Date())
+ switch hour {
+ case 5..<12:
+ return "早上好!新的一天,新的开始。今天感觉怎么样?"
+ case 12..<17:
+ return "下午好!工作辛苦了,有什么想聊的吗?"
+ case 17..<22:
+ return "晚上好!一天结束了,让我们聊聊今天的感受吧。"
+ default:
+ return "夜深了,如果睡不着,我可以陪你聊聊。"
+ }
+ }
+
+ private func emotionColorForDate(_ date: Date) -> Color {
+ // 模拟数据,实际应该从数据库获取
+ let day = Calendar.current.component(.day, from: date)
+ switch day % 6 {
+ case 0: return .red
+ case 1: return .blue
+ case 2: return .green
+ case 3: return .yellow
+ case 4: return .purple
+ default: return .orange
+ }
+ }
+
+ private func hasEmotionRecord(for date: Date) -> Bool {
+ // 模拟数据,实际应该查询数据库
+ let day = Calendar.current.component(.day, from: date)
+ return day % 3 != 0
+ }
+
+ private func sendQuickReply(_ reply: String) {
+ inputText = reply
+ sendMessage()
+ }
+
+ private func sendMessage() {
+ guard !inputText.isEmpty else { return }
+
+ let messageContent = inputText
+ inputText = ""
+
+ // 使用导航管理器发送消息
+ navigationManager.sendMessage(messageContent)
+
+ // 如果没有当前对话,自动进入全屏聊天模式
+ if navigationManager.currentChatConversation == nil {
+ navigationManager.navigateToChat()
+ }
+ }
+
+ private func loadConversations() {
+ // 从数据库或本地存储加载对话历史
+ // mockData.conversations = [] // 如果需要清空对话历史
+ }
+
+ private func simulateInitialLoading() {
+ loadingState = .loading
+
+ // 模拟异步加载过程
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
+ withAnimation(.easeOut(duration: 0.6)) {
+ isInitialLoading = false
+ loadingState = .loaded
+ }
+ loadConversations()
+ }
+ }
+
+ private func simulateRefresh() async {
+ // 模拟刷新延迟
+ try? await Task.sleep(nanoseconds: 1_000_000_000)
+ await MainActor.run {
+ loadConversations()
+ }
+ }
+
+ // MARK: - 骨架屏视图
+ private var recordViewSkeleton: some View {
+ VStack(spacing: 0) {
+ // 顶部导航栏骨架屏
+ HStack {
+ SkeletonView(width: 24, height: 24, cornerRadius: 12)
+ Spacer()
+ SkeletonView(width: 100, height: 20, cornerRadius: 6)
+ Spacer()
+ SkeletonView(width: 24, height: 24, cornerRadius: 12)
+ }
+ .padding(.horizontal, 16)
+ .padding(.top, 8)
+
+ // 日历组件骨架屏
+ VStack(spacing: 12) {
+ HStack {
+ SkeletonView(width: 80, height: 20, cornerRadius: 6)
+ Spacer()
+ SkeletonView(width: 60, height: 16, cornerRadius: 4)
+ }
+
+ HStack(spacing: 16) {
+ ForEach(0..<7, id: \.self) { _ in
+ VStack(spacing: 4) {
+ SkeletonView(width: 20, height: 12, cornerRadius: 3)
+ SkeletonView(width: 40, height: 44, cornerRadius: 12)
+ }
+ }
+ }
+ }
+ .padding()
+ .background(Color.theme.cardBackground)
+ .cornerRadius(16)
+ .padding(.horizontal, 16)
+ .padding(.vertical, 8)
+
+ Spacer()
+
+ // AI助手区域骨架屏
+ VStack(spacing: 24) {
+ SkeletonView(width: 200, height: 200, cornerRadius: 100)
+
+ VStack(spacing: 8) {
+ SkeletonView(width: 200, height: 20, cornerRadius: 6)
+ SkeletonView(width: 250, height: 16, cornerRadius: 4)
+ SkeletonView(width: 180, height: 16, cornerRadius: 4)
+ }
+
+ VStack(spacing: 8) {
+ SkeletonView(width: 120, height: 12, cornerRadius: 3)
+ ForEach(0..<3, id: \.self) { _ in
+ SkeletonView(width: 160, height: 32, cornerRadius: 16)
+ }
+ }
+ }
+ .padding(.horizontal, 24)
+
+ Spacer()
+
+ // 输入区域骨架屏
+ VStack(spacing: 12) {
+ HStack(spacing: 12) {
+ SkeletonView(height: 48, cornerRadius: 24)
+ SkeletonView(width: 48, height: 48, cornerRadius: 24)
+ }
+
+ HStack(spacing: 24) {
+ ForEach(0..<2, id: \.self) { _ in
+ VStack(spacing: 4) {
+ SkeletonView(width: 24, height: 24, cornerRadius: 12)
+ SkeletonView(width: 30, height: 12, cornerRadius: 3)
+ }
+ }
+ Spacer()
+ }
+ }
+ .padding(.horizontal, 16)
+ .padding(.top, 16)
+ .padding(.bottom, 24)
+ .background(Color.theme.cardBackground)
+ }
+ .background(Color.theme.background)
+ }
+}
+
+// MARK: - 辅助视图
+
+// 心情选择视图
+struct MoodPickerView: View {
+ let selectedDate: Date
+ @Binding var selectedMood: String
+ @Binding var isPresented: Bool
+
+ let moods = [
+ ("😊", "开心"), ("😢", "难过"), ("😡", "愤怒"),
+ ("😰", "焦虑"), ("😌", "平静"), ("🤔", "思考")
+ ]
+
+ var body: some View {
+ NavigationView {
+ VStack(spacing: 24) {
+ Text("选择今日心情")
+ .font(.title2)
+ .fontWeight(.semibold)
+
+ LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 3), spacing: 16) {
+ ForEach(moods, id: \.0) { emoji, name in
+ Button(action: {
+ selectedMood = emoji
+ isPresented = false
+ }) {
+ VStack(spacing: 8) {
+ Text(emoji)
+ .font(.system(size: 40))
+ Text(name)
+ .font(.caption)
+ .foregroundColor(.primary)
+ }
+ .frame(width: 80, height: 80)
+ .background(Color(.systemGray6))
+ .cornerRadius(16)
+ }
+ }
+ }
+ .padding()
+
+ Spacer()
+ }
+ .navigationTitle("心情记录")
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .cancellationAction) {
+ Button("取消") { isPresented = false }
+ }
+ }
+ }
+ }
+}
+
+// MARK: - 日期格式化扩展
+extension DateFormatter {
+ static let weekdayShort: DateFormatter = {
+ let formatter = DateFormatter()
+ formatter.dateFormat = "E"
+ return formatter
+ }()
+}
\ No newline at end of file
diff --git a/EmotionMuseum/EmotionMuseum/Views/SupportViews.swift b/EmotionMuseum/EmotionMuseum/Views/SupportViews.swift
new file mode 100644
index 0000000..7e7797e
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Views/SupportViews.swift
@@ -0,0 +1,875 @@
+//
+// SupportViews.swift
+// EmotionMuseum
+//
+// Created by huazhongmin on 2024/01/01.
+//
+
+import SwiftUI
+import MapKit
+
+// MARK: - 占星分析视图
+struct AstroAnalysisView: View {
+ @State private var selectedDate = Date()
+ @State private var selectedTime = Date()
+ @State private var birthLocation = ""
+
+ var body: some View {
+ NavigationView {
+ ScrollView {
+ VStack(spacing: 24) {
+ Text("通过占星学了解你的内在特质")
+ .font(.headline)
+ .multilineTextAlignment(.center)
+ .padding()
+
+ VStack(spacing: 16) {
+ DatePicker("出生日期", selection: $selectedDate, displayedComponents: .date)
+ DatePicker("出生时间", selection: $selectedTime, displayedComponents: .hourAndMinute)
+ TextField("出生地点", text: $birthLocation)
+ .textFieldStyle(RoundedBorderTextFieldStyle())
+ }
+ .padding()
+
+ Button("生成星盘分析") {
+ // 分析逻辑
+ }
+ .buttonStyle(.borderedProminent)
+ .padding()
+ }
+ }
+ .navigationTitle("占星分析")
+ }
+ }
+}
+
+// MARK: - 地点标记卡片
+struct LocationMarkerCard: View {
+ let location: LocationPin
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 8) {
+ HStack {
+ Image(systemName: location.category.icon)
+ .foregroundColor(location.emotion.color)
+ Text(location.name)
+ .font(.headline)
+ Spacer()
+ Text(location.emotion.emoji)
+ .font(.title2)
+ }
+
+ Text(location.description)
+ .font(.caption)
+ .foregroundColor(.secondary)
+ .lineLimit(2)
+ }
+ .padding()
+ .background(Color(.systemBackground))
+ .cornerRadius(12)
+ .shadow(radius: 2)
+ }
+}
+
+// MARK: - 推荐地点卡片
+struct RecommendedLocationCard: View {
+ let location: LocationPin
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 12) {
+ HStack {
+ VStack(alignment: .leading, spacing: 4) {
+ Text(location.name)
+ .font(.headline)
+ .fontWeight(.semibold)
+
+ Text(location.category.rawValue)
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+
+ Spacer()
+
+ VStack(spacing: 4) {
+ Text(location.emotion.emoji)
+ .font(.title)
+
+ Text(location.emotion.displayName)
+ .font(.caption2)
+ .foregroundColor(location.emotion.color)
+ }
+ }
+
+ Text(location.description)
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+ .lineLimit(2)
+
+ HStack {
+ Label("\(location.visitCount)", systemImage: "person.2")
+ .font(.caption)
+ .foregroundColor(.secondary)
+
+ Spacer()
+
+ if !location.tags.isEmpty {
+ Text("#\(location.tags.first ?? "")")
+ .font(.caption)
+ .foregroundColor(.blue)
+ .padding(.horizontal, 8)
+ .padding(.vertical, 2)
+ .background(Color.blue.opacity(0.1))
+ .cornerRadius(4)
+ }
+ }
+ }
+ .padding()
+ .background(Color(.systemBackground))
+ .cornerRadius(16)
+ .shadow(color: .black.opacity(0.05), radius: 8, y: 2)
+ }
+}
+
+// MARK: - 社区动态卡片
+struct CommunityPostCard: View {
+ let post: CommunityPost
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 12) {
+ HStack {
+ Circle()
+ .fill(Color.blue)
+ .frame(width: 40, height: 40)
+ .overlay(
+ Text(String(post.authorName.prefix(1)))
+ .foregroundColor(.white)
+ .fontWeight(.semibold)
+ )
+
+ VStack(alignment: .leading, spacing: 2) {
+ Text(post.authorName)
+ .font(.subheadline)
+ .fontWeight(.medium)
+
+ Text(DateFormatter.shortRelative.string(from: post.createdAt))
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+
+ Spacer()
+
+ Text("💭")
+ .font(.title2)
+ }
+
+ Text(post.content)
+ .font(.body)
+ .lineLimit(3)
+
+ if !post.tags.isEmpty {
+ ScrollView(.horizontal, showsIndicators: false) {
+ HStack(spacing: 8) {
+ ForEach(post.tags, id: \.self) { tag in
+ Text("#\(tag)")
+ .font(.caption)
+ .foregroundColor(.blue)
+ .padding(.horizontal, 8)
+ .padding(.vertical, 4)
+ .background(Color.blue.opacity(0.1))
+ .cornerRadius(8)
+ }
+ }
+ .padding(.horizontal, 4)
+ }
+ }
+
+ HStack {
+ Button(action: {}) {
+ HStack(spacing: 4) {
+ Image(systemName: post.isLikedByCurrentUser ? "heart.fill" : "heart")
+ .foregroundColor(post.isLikedByCurrentUser ? .red : .gray)
+ Text("\(post.likes)")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ }
+
+ Button(action: {}) {
+ HStack(spacing: 4) {
+ Image(systemName: "message")
+ .foregroundColor(.gray)
+ Text("\(post.comments.count)")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ }
+
+ Spacer()
+
+ Button(action: {}) {
+ Image(systemName: "square.and.arrow.up")
+ .foregroundColor(.gray)
+ }
+ }
+ }
+ .padding()
+ .background(Color(.systemBackground))
+ .cornerRadius(16)
+ .shadow(color: .black.opacity(0.05), radius: 8, y: 2)
+ }
+}
+
+// MARK: - 所有地点视图
+struct AllLocationsView: View {
+ @EnvironmentObject var dataManager: MockDataManager
+ @State private var searchText = ""
+ @State private var selectedCategory: LocationCategory?
+ @State private var sortOption: SortOption = .recent
+
+ enum SortOption: String, CaseIterable {
+ case recent = "最近访问"
+ case popular = "最受欢迎"
+ case nearby = "距离最近"
+ case alphabetical = "按名称"
+ }
+
+ var filteredLocations: [LocationPin] {
+ let filtered = dataManager.locationPins.filter { location in
+ let matchesSearch = searchText.isEmpty ||
+ location.name.localizedCaseInsensitiveContains(searchText) ||
+ location.description.localizedCaseInsensitiveContains(searchText) ||
+ location.tags.contains { $0.localizedCaseInsensitiveContains(searchText) }
+
+ let matchesCategory = selectedCategory == nil || location.category == selectedCategory
+
+ return matchesSearch && matchesCategory
+ }
+
+ switch sortOption {
+ case .recent:
+ return filtered.sorted { ($0.lastVisitAt ?? Date.distantPast) > ($1.lastVisitAt ?? Date.distantPast) }
+ case .popular:
+ return filtered.sorted { $0.visitCount > $1.visitCount }
+ case .nearby:
+ return filtered // 实际应用中需要根据用户位置排序
+ case .alphabetical:
+ return filtered.sorted { $0.name < $1.name }
+ }
+ }
+
+ var body: some View {
+ NavigationView {
+ VStack(spacing: 0) {
+ // 搜索栏
+ SearchBar(text: $searchText)
+ .padding()
+
+ // 筛选和排序
+ ScrollView(.horizontal, showsIndicators: false) {
+ HStack(spacing: 12) {
+ // 分类筛选
+ Button(action: { selectedCategory = nil }) {
+ Text("全部")
+ .font(.caption)
+ .foregroundColor(selectedCategory == nil ? .white : .primary)
+ .padding(.horizontal, 12)
+ .padding(.vertical, 6)
+ .background(selectedCategory == nil ? Color.blue : Color(.systemGray6))
+ .cornerRadius(16)
+ }
+
+ ForEach(LocationCategory.allCases, id: \.self) { category in
+ Button(action: { selectedCategory = category }) {
+ Text(category.rawValue)
+ .font(.caption)
+ .foregroundColor(selectedCategory == category ? .white : .primary)
+ .padding(.horizontal, 12)
+ .padding(.vertical, 6)
+ .background(selectedCategory == category ? Color.blue : Color(.systemGray6))
+ .cornerRadius(16)
+ }
+ }
+ }
+ .padding(.horizontal)
+ }
+ .padding(.bottom)
+
+ // 排序选项
+ Picker("排序", selection: $sortOption) {
+ ForEach(SortOption.allCases, id: \.self) { option in
+ Text(option.rawValue).tag(option)
+ }
+ }
+ .pickerStyle(SegmentedPickerStyle())
+ .padding(.horizontal)
+
+ // 地点列表
+ ScrollView {
+ LazyVStack(spacing: 16) {
+ ForEach(filteredLocations) { location in
+ AllLocationCard(location: location)
+ }
+ }
+ .padding()
+ }
+ }
+ .navigationTitle("所有地点")
+ }
+ }
+}
+
+// MARK: - 搜索栏
+struct SearchBar: View {
+ @Binding var text: String
+
+ var body: some View {
+ HStack {
+ Image(systemName: "magnifyingglass")
+ .foregroundColor(.gray)
+
+ TextField("搜索地点、标签...", text: $text)
+ .textFieldStyle(PlainTextFieldStyle())
+
+ if !text.isEmpty {
+ Button(action: { text = "" }) {
+ Image(systemName: "xmark.circle.fill")
+ .foregroundColor(.gray)
+ }
+ }
+ }
+ .padding(.horizontal, 12)
+ .padding(.vertical, 8)
+ .background(Color(.systemGray6))
+ .cornerRadius(10)
+ }
+}
+
+// MARK: - 地点卡片
+struct AllLocationCard: View {
+ let location: LocationPin
+
+ var body: some View {
+ HStack(spacing: 16) {
+ // 地点图标和情绪
+ VStack(spacing: 8) {
+ Image(systemName: location.category.icon)
+ .font(.title2)
+ .foregroundColor(location.emotion.color)
+ .frame(width: 40, height: 40)
+ .background(location.emotion.color.opacity(0.2))
+ .cornerRadius(12)
+
+ Text(location.emotion.emoji)
+ .font(.title3)
+ }
+
+ // 地点信息
+ VStack(alignment: .leading, spacing: 4) {
+ Text(location.name)
+ .font(.headline)
+ .fontWeight(.semibold)
+
+ Text(location.description)
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+ .lineLimit(2)
+
+ HStack {
+ Label("\(location.visitCount)次访问", systemImage: "clock")
+ .font(.caption)
+ .foregroundColor(.secondary)
+
+ Spacer()
+
+ if let lastVisit = location.lastVisitAt {
+ Text(DateFormatter.shortRelative.string(from: lastVisit))
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ }
+ }
+
+ Spacer()
+
+ // 操作按钮
+ VStack(spacing: 8) {
+ Button(action: {}) {
+ Image(systemName: location.isBookmarked ? "bookmark.fill" : "bookmark")
+ .foregroundColor(location.isBookmarked ? .blue : .gray)
+ }
+
+ Button(action: {}) {
+ Image(systemName: "square.and.arrow.up")
+ .foregroundColor(.gray)
+ }
+ }
+ }
+ .padding()
+ .background(Color(.systemBackground))
+ .cornerRadius(16)
+ .shadow(color: .black.opacity(0.05), radius: 8, y: 2)
+ }
+}
+
+// MARK: - 创建分享视图
+struct CreatePostView: View {
+ let selectedLocation: LocationPin?
+ @Environment(\.dismiss) private var dismiss
+ @State private var postContent = ""
+ @State private var selectedEmotion: EmotionType = .neutral
+ @State private var selectedTags: Set = []
+
+ let availableTags = ["推荐", "美食", "风景", "心情", "感悟", "治愈", "安静", "热闹"]
+
+ var body: some View {
+ NavigationView {
+ ScrollView {
+ VStack(spacing: 20) {
+ // 内容输入
+ VStack(alignment: .leading, spacing: 8) {
+ Text("分享你的感受")
+ .font(.headline)
+
+ TextEditor(text: $postContent)
+ .frame(height: 120)
+ .padding(8)
+ .background(Color(.systemGray6))
+ .cornerRadius(12)
+ }
+
+ // 情绪选择
+ VStack(alignment: .leading, spacing: 8) {
+ Text("当前情绪")
+ .font(.headline)
+
+ ScrollView(.horizontal, showsIndicators: false) {
+ HStack(spacing: 12) {
+ ForEach(EmotionType.allCases, id: \.self) { emotion in
+ Button(action: { selectedEmotion = emotion }) {
+ VStack(spacing: 4) {
+ Text(emotion.emoji)
+ .font(.title2)
+ Text(emotion.rawValue)
+ .font(.caption)
+ .foregroundColor(.primary)
+ }
+ .padding(8)
+ .background(
+ RoundedRectangle(cornerRadius: 12)
+ .fill(selectedEmotion == emotion ? emotion.color.opacity(0.3) : Color(.systemGray6))
+ )
+ }
+ }
+ }
+ .padding(.horizontal, 4)
+ }
+ }
+
+ // 标签选择
+ VStack(alignment: .leading, spacing: 8) {
+ Text("添加标签")
+ .font(.headline)
+
+ LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 4), spacing: 8) {
+ ForEach(availableTags, id: \.self) { tag in
+ Button(action: {
+ if selectedTags.contains(tag) {
+ selectedTags.remove(tag)
+ } else {
+ selectedTags.insert(tag)
+ }
+ }) {
+ Text("#\(tag)")
+ .font(.caption)
+ .foregroundColor(selectedTags.contains(tag) ? .white : .primary)
+ .padding(.horizontal, 8)
+ .padding(.vertical, 4)
+ .background(
+ RoundedRectangle(cornerRadius: 8)
+ .fill(selectedTags.contains(tag) ? Color.blue : Color(.systemGray6))
+ )
+ }
+ }
+ }
+ }
+
+ // 位置信息
+ if let location = selectedLocation {
+ VStack(alignment: .leading, spacing: 8) {
+ Text("位置")
+ .font(.headline)
+
+ HStack(spacing: 12) {
+ Image(systemName: location.category.icon)
+ .foregroundColor(.blue)
+
+ VStack(alignment: .leading, spacing: 2) {
+ Text(location.name)
+ .font(.subheadline)
+ .fontWeight(.medium)
+
+ if let address = location.address {
+ Text(address)
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ }
+
+ Spacer()
+ }
+ .padding()
+ .background(Color(.systemGray6))
+ .cornerRadius(12)
+ }
+ }
+ }
+ .padding()
+ }
+ .navigationTitle("分享动态")
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .cancellationAction) {
+ Button("取消") { dismiss() }
+ }
+
+ ToolbarItem(placement: .confirmationAction) {
+ Button("发布") {
+ // 发布逻辑
+ dismiss()
+ }
+ .disabled(postContent.isEmpty)
+ }
+ }
+ }
+ }
+}
+
+// MARK: - 地点详情视图
+struct LocationDetailView: View {
+ let location: LocationPin
+ @Environment(\.dismiss) private var dismiss
+ @State private var showingShareView = false
+
+ var body: some View {
+ NavigationView {
+ ScrollView {
+ VStack(spacing: 24) {
+ // 头部信息
+ VStack(spacing: 16) {
+ HStack(spacing: 20) {
+ Image(systemName: location.category.icon)
+ .font(.system(size: 48))
+ .foregroundColor(location.emotion.color)
+ .frame(width: 80, height: 80)
+ .background(location.emotion.color.opacity(0.2))
+ .cornerRadius(20)
+
+ VStack(spacing: 8) {
+ Text(location.emotion.emoji)
+ .font(.system(size: 60))
+
+ Text(location.emotion.displayName)
+ .font(.headline)
+ .foregroundColor(location.emotion.color)
+ }
+ }
+
+ Text(location.name)
+ .font(.title)
+ .fontWeight(.bold)
+ .multilineTextAlignment(.center)
+
+ Text(location.description)
+ .font(.body)
+ .foregroundColor(.secondary)
+ .multilineTextAlignment(.center)
+ .padding(.horizontal)
+ }
+ .padding(.vertical, 20)
+ .frame(maxWidth: .infinity)
+ .background(
+ LinearGradient(
+ colors: [location.emotion.color.opacity(0.1), location.emotion.color.opacity(0.05)],
+ startPoint: .topLeading,
+ endPoint: .bottomTrailing
+ )
+ )
+ .cornerRadius(20)
+
+ // 基本信息
+ VStack(alignment: .leading, spacing: 12) {
+ Text("基本信息")
+ .font(.headline)
+
+ InfoRow(icon: "location", title: "地址", value: location.address ?? "未设置")
+ InfoRow(icon: "list.bullet", title: "类别", value: location.category.rawValue)
+ InfoRow(icon: "number", title: "访问次数", value: "\(location.visitCount) 次")
+ }
+ .padding()
+ .background(Color(.systemBackground))
+ .cornerRadius(16)
+ .shadow(radius: 2)
+
+ // 操作按钮
+ VStack(spacing: 12) {
+ Button(action: { showingShareView = true }) {
+ HStack {
+ Image(systemName: "square.and.pencil")
+ Text("在这里分享心情")
+ }
+ .font(.headline)
+ .foregroundColor(.white)
+ .frame(maxWidth: .infinity)
+ .padding()
+ .background(location.emotion.color)
+ .cornerRadius(12)
+ }
+
+ HStack(spacing: 12) {
+ Button(action: {}) {
+ HStack {
+ Image(systemName: "bookmark")
+ Text("收藏")
+ }
+ .frame(maxWidth: .infinity)
+ .padding()
+ .background(Color(.systemGray6))
+ .cornerRadius(12)
+ }
+
+ Button(action: {}) {
+ HStack {
+ Image(systemName: "square.and.arrow.up")
+ Text("分享")
+ }
+ .frame(maxWidth: .infinity)
+ .padding()
+ .background(Color(.systemGray6))
+ .cornerRadius(12)
+ }
+ }
+ }
+ .padding(.horizontal)
+ }
+ .padding()
+ }
+ .navigationTitle("")
+ .navigationBarTitleDisplayMode(.inline)
+ .navigationBarBackButtonHidden(true)
+ .toolbar {
+ ToolbarItem(placement: .navigationBarLeading) {
+ Button(action: { dismiss() }) {
+ HStack(spacing: 4) {
+ Image(systemName: "xmark.circle.fill")
+ .foregroundColor(.gray)
+ Text("关闭")
+ .foregroundColor(.primary)
+ }
+ }
+ }
+ }
+ }
+ .sheet(isPresented: $showingShareView) {
+ CreatePostView(selectedLocation: location)
+ }
+ }
+}
+
+// MARK: - 信息行组件
+struct InfoRow: View {
+ let icon: String
+ let title: String
+ let value: String
+
+ var body: some View {
+ HStack {
+ Image(systemName: icon)
+ .foregroundColor(.blue)
+ .frame(width: 20)
+
+ Text(title)
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+
+ Spacer()
+
+ Text(value)
+ .font(.subheadline)
+ .fontWeight(.medium)
+ }
+ }
+}
+
+// MARK: - 访问记录模型和视图
+struct VisitRecord: Identifiable, Codable {
+ let id: UUID
+ let date: Date
+ let emotion: EmotionType
+ let notes: String
+
+ init(id: UUID = UUID(), date: Date, emotion: EmotionType, notes: String) {
+ self.id = id
+ self.date = date
+ self.emotion = emotion
+ self.notes = notes
+ }
+}
+
+struct VisitHistoryRow: View {
+ let visit: VisitRecord
+ let isLatest: Bool
+
+ var body: some View {
+ HStack(spacing: 12) {
+ VStack(spacing: 4) {
+ Text(visit.emotion.emoji)
+ .font(.title3)
+
+ if isLatest {
+ Circle()
+ .fill(Color.green)
+ .frame(width: 6, height: 6)
+ } else {
+ Circle()
+ .fill(Color.gray.opacity(0.3))
+ .frame(width: 4, height: 4)
+ }
+ }
+
+ VStack(alignment: .leading, spacing: 4) {
+ HStack {
+ Text(visit.emotion.displayName)
+ .font(.subheadline)
+ .fontWeight(.medium)
+ .foregroundColor(visit.emotion.color)
+
+ Spacer()
+
+ Text(DateFormatter.localizedDate.string(from: visit.date))
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+
+ Text(visit.notes)
+ .font(.caption)
+ .foregroundColor(.secondary)
+ .lineLimit(2)
+ }
+ }
+ .padding(.vertical, 4)
+ }
+}
+
+// MARK: - 日期格式化扩展
+extension DateFormatter {
+ static let localizedDate: DateFormatter = {
+ let formatter = DateFormatter()
+ formatter.dateStyle = .medium
+ formatter.timeStyle = .none
+ return formatter
+ }()
+
+ static let shortRelative: DateFormatter = {
+ let formatter = DateFormatter()
+ formatter.dateStyle = .short
+ formatter.timeStyle = .none
+ return formatter
+ }()
+}
+
+// MARK: - 生成访问记录的辅助函数
+func generateVisitNote(for location: LocationPin, emotion: EmotionType) -> String {
+ let notes = [
+ "在这里度过了美好的时光",
+ "心情得到了很好的调节",
+ "这个地方让我感到平静",
+ "和朋友一起来的,很开心",
+ "独自一人,享受安静的时光",
+ "记录了这次特别的体验"
+ ]
+ return notes.randomElement() ?? "记录了这次访问"
+}
+
+// MARK: - 简化的成长相关视图
+struct GuidedSelectionView: View {
+ var body: some View {
+ VStack {
+ Text("引导选择")
+ .font(.title)
+ .padding()
+
+ Text("这里是引导用户进行选择的界面")
+ .foregroundColor(.secondary)
+ .padding()
+
+ Spacer()
+ }
+ }
+}
+
+struct AstroAnalysisInputView: View {
+ var body: some View {
+ VStack {
+ Text("占星分析输入")
+ .font(.title)
+ .padding()
+
+ Text("这里是占星分析的输入界面")
+ .foregroundColor(.secondary)
+ .padding()
+
+ Spacer()
+ }
+ }
+}
+
+// MARK: - 其他支持视图的简化版本
+struct TopicDetailView: View {
+ let topic: GrowthTopic
+
+ var body: some View {
+ VStack {
+ Text(topic.title)
+ .font(.title)
+ .padding()
+
+ Text(topic.description)
+ .foregroundColor(.secondary)
+ .padding()
+
+ Spacer()
+ }
+ }
+}
+
+struct EmotionalInsightsView: View {
+ var body: some View {
+ VStack {
+ Text("情绪洞察")
+ .font(.title)
+ .padding()
+
+ Text("这里显示用户的情绪分析和洞察")
+ .foregroundColor(.secondary)
+ .padding()
+
+ Spacer()
+ }
+ }
+}
+
+struct ChatHistoryView: View {
+ var body: some View {
+ VStack {
+ Text("对话历史")
+ .font(.title)
+ .padding()
+
+ Text("这里显示与AI的对话历史")
+ .foregroundColor(.secondary)
+ .padding()
+
+ Spacer()
+ }
+ }
+}
\ No newline at end of file
diff --git a/EmotionMuseum/EmotionMuseum/Views/ThemeAdapter.swift b/EmotionMuseum/EmotionMuseum/Views/ThemeAdapter.swift
new file mode 100644
index 0000000..13171e2
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Views/ThemeAdapter.swift
@@ -0,0 +1,415 @@
+//
+// ThemeAdapter.swift
+// EmotionMuseum
+//
+// Created by 华中敏 on 2025/6/13.
+//
+
+import SwiftUI
+
+// MARK: - 主题适配器
+struct ThemedCard: View {
+ let content: () -> Content
+ let padding: CGFloat
+ let cornerRadius: CGFloat
+ let shadowEnabled: Bool
+
+ init(
+ padding: CGFloat = 16,
+ cornerRadius: CGFloat = 16,
+ shadowEnabled: Bool = true,
+ @ViewBuilder content: @escaping () -> Content
+ ) {
+ self.padding = padding
+ self.cornerRadius = cornerRadius
+ self.shadowEnabled = shadowEnabled
+ self.content = content
+ }
+
+ var body: some View {
+ content()
+ .padding(padding)
+ .background(Color.theme.cardBackground)
+ .cornerRadius(cornerRadius)
+ .shadow(
+ color: shadowEnabled ? Color.black.opacity(0.05) : Color.clear,
+ radius: shadowEnabled ? 8 : 0,
+ x: 0,
+ y: shadowEnabled ? 2 : 0
+ )
+ }
+}
+
+// MARK: - 主题文本组件
+struct ThemedText: View {
+ let text: String
+ let style: TextStyle
+ let alignment: TextAlignment
+
+ enum TextStyle {
+ case title, headline, subheadline, body, caption
+ case primary, secondary, tertiary
+
+ var font: Font {
+ switch self {
+ case .title: return .title
+ case .headline: return .headline
+ case .subheadline: return .subheadline
+ case .body: return .body
+ case .caption: return .caption
+ case .primary, .secondary, .tertiary: return .body
+ }
+ }
+
+ var color: Color {
+ switch self {
+ case .title, .headline, .subheadline, .body, .primary:
+ return Color.theme.primaryText
+ case .secondary:
+ return Color.theme.secondaryText
+ case .tertiary, .caption:
+ return Color.theme.tertiaryText
+ }
+ }
+ }
+
+ init(_ text: String, style: TextStyle = .body, alignment: TextAlignment = .leading) {
+ self.text = text
+ self.style = style
+ self.alignment = alignment
+ }
+
+ var body: some View {
+ Text(text)
+ .font(style.font)
+ .foregroundColor(style.color)
+ .multilineTextAlignment(alignment)
+ }
+}
+
+// MARK: - 主题按钮
+struct ThemedButton: View {
+ let title: String
+ let style: ButtonStyle
+ let size: ButtonSize
+ let action: () -> Void
+
+ enum ButtonStyle {
+ case primary, secondary, outline, text
+
+ var backgroundColor: Color {
+ switch self {
+ case .primary: return Color.theme.accent
+ case .secondary: return Color.theme.secondary
+ case .outline, .text: return Color.clear
+ }
+ }
+
+ var foregroundColor: Color {
+ switch self {
+ case .primary: return .white
+ case .secondary: return Color.theme.primaryText
+ case .outline: return Color.theme.accent
+ case .text: return Color.theme.accent
+ }
+ }
+
+ var borderColor: Color {
+ switch self {
+ case .outline: return Color.theme.accent
+ default: return Color.clear
+ }
+ }
+ }
+
+ enum ButtonSize {
+ case small, medium, large
+
+ var padding: EdgeInsets {
+ switch self {
+ case .small: return EdgeInsets(top: 8, leading: 12, bottom: 8, trailing: 12)
+ case .medium: return EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16)
+ case .large: return EdgeInsets(top: 16, leading: 24, bottom: 16, trailing: 24)
+ }
+ }
+
+ var cornerRadius: CGFloat {
+ switch self {
+ case .small: return 8
+ case .medium: return 12
+ case .large: return 16
+ }
+ }
+ }
+
+ init(_ title: String, style: ButtonStyle = .primary, size: ButtonSize = .medium, action: @escaping () -> Void) {
+ self.title = title
+ self.style = style
+ self.size = size
+ self.action = action
+ }
+
+ var body: some View {
+ Button(action: action) {
+ Text(title)
+ .font(.subheadline)
+ .fontWeight(.medium)
+ .foregroundColor(style.foregroundColor)
+ .padding(size.padding)
+ .background(style.backgroundColor)
+ .overlay(
+ RoundedRectangle(cornerRadius: size.cornerRadius)
+ .stroke(style.borderColor, lineWidth: style == .outline ? 1 : 0)
+ )
+ .cornerRadius(size.cornerRadius)
+ }
+ }
+}
+
+// MARK: - 主题分隔线
+struct ThemedDivider: View {
+ let thickness: CGFloat
+ let color: Color?
+
+ init(thickness: CGFloat = 1, color: Color? = nil) {
+ self.thickness = thickness
+ self.color = color
+ }
+
+ var body: some View {
+ Rectangle()
+ .fill(color ?? Color.theme.divider)
+ .frame(height: thickness)
+ }
+}
+
+// MARK: - 主题进度条
+struct ThemedProgressView: View {
+ let value: Double
+ let total: Double
+ let height: CGFloat
+ let backgroundColor: Color?
+ let foregroundColor: Color?
+
+ init(
+ value: Double,
+ total: Double = 1.0,
+ height: CGFloat = 8,
+ backgroundColor: Color? = nil,
+ foregroundColor: Color? = nil
+ ) {
+ self.value = value
+ self.total = total
+ self.height = height
+ self.backgroundColor = backgroundColor
+ self.foregroundColor = foregroundColor
+ }
+
+ var body: some View {
+ ProgressView(value: value, total: total)
+ .progressViewStyle(
+ ThemedLinearProgressViewStyle(
+ height: height,
+ backgroundColor: backgroundColor ?? Color.theme.skeleton,
+ foregroundColor: foregroundColor ?? Color.theme.accent
+ )
+ )
+ }
+}
+
+struct ThemedLinearProgressViewStyle: ProgressViewStyle {
+ let height: CGFloat
+ let backgroundColor: Color
+ let foregroundColor: Color
+
+ func makeBody(configuration: Configuration) -> some View {
+ GeometryReader { geometry in
+ ZStack(alignment: .leading) {
+ Rectangle()
+ .fill(backgroundColor)
+ .frame(height: height)
+ .cornerRadius(height / 2)
+
+ Rectangle()
+ .fill(foregroundColor)
+ .frame(
+ width: geometry.size.width * CGFloat(configuration.fractionCompleted ?? 0),
+ height: height
+ )
+ .cornerRadius(height / 2)
+ }
+ }
+ .frame(height: height)
+ }
+}
+
+// MARK: - 主题输入框
+struct ThemedTextField: View {
+ let placeholder: String
+ @Binding var text: String
+ let style: TextFieldStyle
+
+ enum TextFieldStyle {
+ case standard, rounded, outline
+
+ var backgroundColor: Color {
+ switch self {
+ case .standard: return Color.clear
+ case .rounded: return Color.theme.surfaceBackground
+ case .outline: return Color.theme.background
+ }
+ }
+
+ var borderColor: Color {
+ switch self {
+ case .outline: return Color.theme.border
+ default: return Color.clear
+ }
+ }
+
+ var cornerRadius: CGFloat {
+ switch self {
+ case .rounded: return 12
+ case .outline: return 8
+ default: return 0
+ }
+ }
+ }
+
+ init(_ placeholder: String, text: Binding, style: TextFieldStyle = .standard) {
+ self.placeholder = placeholder
+ self._text = text
+ self.style = style
+ }
+
+ var body: some View {
+ TextField(placeholder, text: $text)
+ .padding(.horizontal, style == .standard ? 0 : 12)
+ .padding(.vertical, style == .standard ? 0 : 10)
+ .background(style.backgroundColor)
+ .foregroundColor(Color.theme.primaryText)
+ .overlay(
+ RoundedRectangle(cornerRadius: style.cornerRadius)
+ .stroke(style.borderColor, lineWidth: style == .outline ? 1 : 0)
+ )
+ .cornerRadius(style.cornerRadius)
+ }
+}
+
+// MARK: - 主题图标
+struct ThemedIcon: View {
+ let systemName: String
+ let style: IconStyle
+ let size: IconSize
+
+ enum IconStyle {
+ case primary, secondary, accent, custom(Color)
+
+ var color: Color {
+ switch self {
+ case .primary: return Color.theme.primaryText
+ case .secondary: return Color.theme.secondaryText
+ case .accent: return Color.theme.accent
+ case .custom(let color): return color
+ }
+ }
+ }
+
+ enum IconSize {
+ case small, medium, large, custom(CGFloat)
+
+ var font: Font {
+ switch self {
+ case .small: return .caption
+ case .medium: return .body
+ case .large: return .title2
+ case .custom(let size): return .system(size: size)
+ }
+ }
+ }
+
+ init(_ systemName: String, style: IconStyle = .primary, size: IconSize = .medium) {
+ self.systemName = systemName
+ self.style = style
+ self.size = size
+ }
+
+ var body: some View {
+ Image(systemName: systemName)
+ .font(size.font)
+ .foregroundColor(style.color)
+ }
+}
+
+// MARK: - 主题列表行
+struct ThemedListRow: View {
+ let content: () -> Content
+ let showSeparator: Bool
+ let padding: EdgeInsets
+
+ init(
+ showSeparator: Bool = true,
+ padding: EdgeInsets = EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16),
+ @ViewBuilder content: @escaping () -> Content
+ ) {
+ self.showSeparator = showSeparator
+ self.padding = padding
+ self.content = content
+ }
+
+ var body: some View {
+ VStack(spacing: 0) {
+ content()
+ .padding(padding)
+ .background(Color.theme.cardBackground)
+
+ if showSeparator {
+ ThemedDivider()
+ .padding(.leading, padding.leading)
+ }
+ }
+ }
+}
+
+// MARK: - 视图扩展
+extension View {
+ func themedCard(
+ padding: CGFloat = 16,
+ cornerRadius: CGFloat = 16,
+ shadowEnabled: Bool = true
+ ) -> some View {
+ ThemedCard(
+ padding: padding,
+ cornerRadius: cornerRadius,
+ shadowEnabled: shadowEnabled
+ ) {
+ self
+ }
+ }
+
+ func themedBackground() -> some View {
+ background(Color.theme.background)
+ }
+
+ func themedSurface() -> some View {
+ background(Color.theme.surfaceBackground)
+ }
+}
+
+// MARK: - 预览
+#Preview("主题组件") {
+ VStack(spacing: 20) {
+ ThemedText("主标题", style: .title)
+ ThemedText("副标题文本", style: .secondary)
+
+ ThemedButton("主要按钮") {}
+ ThemedButton("次要按钮", style: .secondary) {}
+
+ ThemedProgressView(value: 0.6)
+ .frame(height: 8)
+
+ ThemedTextField("请输入内容", text: .constant(""))
+ }
+ .padding()
+ .themedBackground()
+}
\ No newline at end of file
diff --git a/EmotionMuseum/EmotionMuseum/Views/ThemeSettingsView.swift b/EmotionMuseum/EmotionMuseum/Views/ThemeSettingsView.swift
new file mode 100644
index 0000000..737d524
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Views/ThemeSettingsView.swift
@@ -0,0 +1,367 @@
+//
+// ThemeSettingsView.swift
+// EmotionMuseum
+//
+// Created by 华中敏 on 2025/6/13.
+//
+
+import SwiftUI
+
+// MARK: - 主题设置页面
+struct ThemeSettingsView: View {
+ @EnvironmentObject var themeManager: ThemeManager
+ @Environment(\.dismiss) var dismiss
+
+ var body: some View {
+ NavigationView {
+ List {
+ // 主题选择区域
+ themeSelectionSection
+
+ // 外观预览
+ previewSection
+
+ // 高级设置
+ advancedSettingsSection
+ }
+ .listStyle(InsetGroupedListStyle())
+ .navigationTitle("主题设置")
+ .navigationBarTitleDisplayMode(.large)
+ .toolbar {
+ ToolbarItem(placement: .topBarTrailing) {
+ Button("完成") {
+ dismiss()
+ }
+ .foregroundColor(Color.theme.accent)
+ }
+ }
+ }
+ .themedBackground()
+ .preferredColorScheme(themeManager.systemFollowsDeviceTheme ? nil : (themeManager.isDarkMode ? .dark : .light))
+ }
+
+ // MARK: - 主题选择区域
+ private var themeSelectionSection: some View {
+ Section {
+ // 跟随系统设置
+ ThemedListRow {
+ HStack {
+ ThemedIcon("gear.circle.fill", style: .accent, size: .medium)
+
+ VStack(alignment: .leading, spacing: 2) {
+ ThemedText("跟随系统", style: .headline)
+ ThemedText("自动适应系统的深色模式设置", style: .secondary)
+ }
+
+ Spacer()
+
+ Toggle("", isOn: $themeManager.systemFollowsDeviceTheme)
+ .toggleStyle(SwitchToggleStyle(tint: Color.theme.accent))
+ }
+ }
+
+ if !themeManager.systemFollowsDeviceTheme {
+ // 浅色模式
+ ThemeOptionRow(
+ title: "浅色模式",
+ description: "明亮清新的视觉体验",
+ icon: "sun.max.fill",
+ isSelected: !themeManager.isDarkMode
+ ) {
+ withAnimation(AnimationConfig.smooth) {
+ themeManager.isDarkMode = false
+ }
+ }
+
+ // 深色模式
+ ThemeOptionRow(
+ title: "深色模式",
+ description: "舒适护眼的暗色调体验",
+ icon: "moon.fill",
+ isSelected: themeManager.isDarkMode
+ ) {
+ withAnimation(AnimationConfig.smooth) {
+ themeManager.isDarkMode = true
+ }
+ }
+ }
+ } header: {
+ ThemedText("外观模式", style: .caption)
+ .foregroundColor(Color.theme.secondaryText)
+ }
+ }
+
+ // MARK: - 预览区域
+ private var previewSection: some View {
+ Section {
+ VStack(spacing: 16) {
+ // 预览卡片
+ PreviewCard()
+
+ // 色彩预览
+ ColorPreviewGrid()
+ }
+ .padding(.vertical, 8)
+ } header: {
+ ThemedText("预览效果", style: .caption)
+ .foregroundColor(Color.theme.secondaryText)
+ }
+ }
+
+ // MARK: - 高级设置
+ private var advancedSettingsSection: some View {
+ Section {
+ // 重置主题设置
+ ThemedListRow {
+ Button(action: resetThemeSettings) {
+ HStack {
+ ThemedIcon("arrow.clockwise.circle.fill", style: .custom(.orange), size: .medium)
+ ThemedText("重置主题设置", style: .headline)
+ Spacer()
+ }
+ }
+ }
+
+ // 关于主题
+ ThemedListRow(showSeparator: false) {
+ NavigationLink {
+ ThemeInfoView()
+ } label: {
+ HStack {
+ ThemedIcon("info.circle.fill", style: .accent, size: .medium)
+ ThemedText("关于主题", style: .headline)
+ Spacer()
+ ThemedIcon("chevron.right", style: .secondary, size: .small)
+ }
+ }
+ }
+ } header: {
+ ThemedText("高级选项", style: .caption)
+ .foregroundColor(Color.theme.secondaryText)
+ }
+ }
+
+ private func resetThemeSettings() {
+ withAnimation(AnimationConfig.smooth) {
+ themeManager.systemFollowsDeviceTheme = true
+ themeManager.isDarkMode = false
+ }
+
+ // 触觉反馈
+ let impactFeedback = UIImpactFeedbackGenerator(style: .medium)
+ impactFeedback.impactOccurred()
+ }
+}
+
+// MARK: - 主题选项行
+struct ThemeOptionRow: View {
+ let title: String
+ let description: String
+ let icon: String
+ let isSelected: Bool
+ let action: () -> Void
+
+ var body: some View {
+ ThemedListRow {
+ Button(action: action) {
+ HStack(spacing: 12) {
+ ThemedIcon(icon, style: .accent, size: .medium)
+
+ VStack(alignment: .leading, spacing: 2) {
+ ThemedText(title, style: .headline)
+ ThemedText(description, style: .secondary)
+ }
+
+ Spacer()
+
+ if isSelected {
+ ThemedIcon("checkmark.circle.fill", style: .custom(.green), size: .medium)
+ .transition(.scale.combined(with: .opacity))
+ }
+ }
+ }
+ .buttonStyle(PlainButtonStyle())
+ }
+ }
+}
+
+// MARK: - 预览卡片
+struct PreviewCard: View {
+ var body: some View {
+ VStack(alignment: .leading, spacing: 12) {
+ HStack {
+ ThemedText("示例卡片", style: .headline)
+ Spacer()
+ ThemedIcon("heart.fill", style: .custom(.red), size: .medium)
+ }
+
+ ThemedText("这是一个预览卡片,展示当前主题的效果。文本清晰度和对比度都经过精心调校。", style: .secondary)
+
+ HStack {
+ ThemedButton("主要按钮", size: .small) {}
+ ThemedButton("次要按钮", style: .secondary, size: .small) {}
+ Spacer()
+ }
+
+ ThemedProgressView(value: 0.6)
+ .frame(height: 6)
+ }
+ .themedCard()
+ }
+}
+
+// MARK: - 色彩预览网格
+struct ColorPreviewGrid: View {
+ let colors: [(String, Color)] = [
+ ("主色调", Color.theme.accent),
+ ("成功", Color.theme.success),
+ ("警告", Color.theme.warning),
+ ("错误", Color.theme.error)
+ ]
+
+ var body: some View {
+ LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 4), spacing: 12) {
+ ForEach(colors, id: \.0) { name, color in
+ VStack(spacing: 6) {
+ Circle()
+ .fill(color)
+ .frame(width: 32, height: 32)
+
+ ThemedText(name, style: .caption)
+ .lineLimit(1)
+ }
+ }
+ }
+ .themedCard(padding: 12)
+ }
+}
+
+// MARK: - 主题信息页面
+struct ThemeInfoView: View {
+ @Environment(\.dismiss) var dismiss
+
+ var body: some View {
+ ScrollView {
+ VStack(alignment: .leading, spacing: 24) {
+ // 标题区域
+ VStack(alignment: .leading, spacing: 8) {
+ ThemedText("关于主题系统", style: .title)
+ ThemedText("智能适配,呵护双眼", style: .secondary)
+ }
+
+ // 特性介绍
+ VStack(alignment: .leading, spacing: 16) {
+ FeatureRow(
+ icon: "eye.fill",
+ title: "护眼设计",
+ description: "精心调校的颜色对比度,长时间使用不疲劳"
+ )
+
+ FeatureRow(
+ icon: "paintbrush.fill",
+ title: "精美配色",
+ description: "专业设计师打造的色彩方案,视觉体验更佳"
+ )
+
+ FeatureRow(
+ icon: "gear.badge.checkmark",
+ title: "智能适配",
+ description: "可跟随系统设置自动切换,也可手动调节"
+ )
+
+ FeatureRow(
+ icon: "moon.stars.fill",
+ title: "深色模式",
+ description: "夜间使用更舒适,有效减少蓝光刺激"
+ )
+ }
+
+ Spacer(minLength: 32)
+
+ // 版本信息
+ VStack(spacing: 8) {
+ ThemedText("主题系统 v1.0", style: .secondary)
+ ThemedText("情绪博物馆团队制作", style: .tertiary)
+ }
+ .frame(maxWidth: .infinity)
+ }
+ .padding(.horizontal, 20)
+ .padding(.vertical, 24)
+ }
+ .themedBackground()
+ .navigationTitle("主题信息")
+ .navigationBarTitleDisplayMode(.inline)
+ }
+}
+
+// MARK: - 特性行
+struct FeatureRow: View {
+ let icon: String
+ let title: String
+ let description: String
+
+ var body: some View {
+ HStack(alignment: .top, spacing: 12) {
+ ThemedIcon(icon, style: .accent, size: .medium)
+ .frame(width: 24, height: 24)
+
+ VStack(alignment: .leading, spacing: 4) {
+ ThemedText(title, style: .headline)
+ ThemedText(description, style: .secondary)
+ }
+ }
+ }
+}
+
+// MARK: - 快速主题切换组件
+struct QuickThemeToggle: View {
+ @EnvironmentObject var themeManager: ThemeManager
+
+ var body: some View {
+ Button(action: toggleTheme) {
+ HStack(spacing: 8) {
+ ThemedIcon(
+ themeManager.isDarkMode ? "moon.fill" : "sun.max.fill",
+ style: .accent,
+ size: .medium
+ )
+
+ ThemedText(
+ themeManager.isDarkMode ? "深色" : "浅色",
+ style: .secondary
+ )
+ }
+ .padding(.horizontal, 12)
+ .padding(.vertical, 6)
+ .background(Color.theme.surfaceBackground)
+ .cornerRadius(16)
+ }
+ .buttonStyle(PlainButtonStyle())
+ }
+
+ private func toggleTheme() {
+ withAnimation(AnimationConfig.smooth) {
+ if themeManager.systemFollowsDeviceTheme {
+ themeManager.setSystemFollowing(false)
+ }
+ themeManager.toggleTheme()
+ }
+
+ // 触觉反馈
+ let impactFeedback = UIImpactFeedbackGenerator(style: .light)
+ impactFeedback.impactOccurred()
+ }
+}
+
+// MARK: - 预览
+#Preview("主题设置") {
+ ThemeSettingsView()
+ .environmentObject(ThemeManager())
+}
+
+#Preview("快速切换") {
+ QuickThemeToggle()
+ .environmentObject(ThemeManager())
+ .padding()
+ .themedBackground()
+}
\ No newline at end of file
diff --git a/EmotionMuseum/EmotionMuseum/Views/UniverseView.swift b/EmotionMuseum/EmotionMuseum/Views/UniverseView.swift
new file mode 100644
index 0000000..2f2e66a
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseum/Views/UniverseView.swift
@@ -0,0 +1,622 @@
+//
+// UniverseView.swift
+// EmotionMuseum
+//
+// Created by 华中敏 on 2025/6/13.
+//
+
+import SwiftUI
+
+struct UniverseView: View {
+ @State private var showingProfile = false
+ @State private var showingSettings = false
+ @State private var showingAchievements = false
+ @State private var showingDataExport = false
+
+ var body: some View {
+ NavigationView {
+ ScrollView {
+ VStack(spacing: 24) {
+ // 用户信息卡片
+ UserProfileCard(onEditProfile: {
+ showingProfile = true
+ })
+
+ // 成长数据概览
+ GrowthOverviewCard()
+
+ // 功能菜单
+ VStack(spacing: 16) {
+ MenuSection(title: "我的成长") {
+ VStack(spacing: 12) {
+ MenuRow(
+ icon: "trophy.fill",
+ title: "成就徽章",
+ subtitle: "查看你的成长里程碑",
+ color: .yellow,
+ action: { showingAchievements = true }
+ )
+
+ MenuRow(
+ icon: "chart.line.uptrend.xyaxis",
+ title: "成长报告",
+ subtitle: "详细的成长数据分析",
+ color: .blue,
+ action: { /* 跳转到成长报告 */ }
+ )
+
+ MenuRow(
+ icon: "calendar",
+ title: "情绪日历",
+ subtitle: "回顾你的情绪历程",
+ color: .green,
+ action: { /* 跳转到情绪日历 */ }
+ )
+ }
+ }
+
+ MenuSection(title: "数据管理") {
+ VStack(spacing: 12) {
+ MenuRow(
+ icon: "square.and.arrow.up",
+ title: "导出数据",
+ subtitle: "导出你的个人数据",
+ color: .purple,
+ action: { showingDataExport = true }
+ )
+
+ MenuRow(
+ icon: "icloud.and.arrow.up",
+ title: "云端同步",
+ subtitle: "同步到iCloud",
+ color: .cyan,
+ action: { /* 云端同步 */ }
+ )
+ }
+ }
+
+ MenuSection(title: "设置") {
+ VStack(spacing: 12) {
+ MenuRow(
+ icon: "bell.fill",
+ title: "通知设置",
+ subtitle: "管理提醒和通知",
+ color: .orange,
+ action: { /* 通知设置 */ }
+ )
+
+ MenuRow(
+ icon: "lock.fill",
+ title: "隐私设置",
+ subtitle: "数据隐私和安全",
+ color: .red,
+ action: { /* 隐私设置 */ }
+ )
+
+ MenuRow(
+ icon: "gearshape.fill",
+ title: "应用设置",
+ subtitle: "个性化设置",
+ color: .gray,
+ action: { showingSettings = true }
+ )
+ }
+ }
+
+ MenuSection(title: "帮助与支持") {
+ VStack(spacing: 12) {
+ MenuRow(
+ icon: "questionmark.circle.fill",
+ title: "使用帮助",
+ subtitle: "常见问题和使用指南",
+ color: .blue,
+ action: { /* 帮助页面 */ }
+ )
+
+ MenuRow(
+ icon: "envelope.fill",
+ title: "联系我们",
+ subtitle: "反馈和建议",
+ color: .green,
+ action: { /* 联系我们 */ }
+ )
+
+ MenuRow(
+ icon: "star.fill",
+ title: "评价应用",
+ subtitle: "在App Store评价",
+ color: .yellow,
+ action: { /* 跳转App Store */ }
+ )
+ }
+ }
+ }
+
+ // 版本信息
+ VStack(spacing: 8) {
+ Text("情绪博物馆")
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+
+ Text("版本 1.0.0")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ .padding(.top, 20)
+ }
+ .padding(.horizontal)
+ .padding(.vertical)
+ }
+ .navigationTitle("我的宇宙")
+ .navigationBarTitleDisplayMode(.large)
+ }
+ .sheet(isPresented: $showingProfile) {
+ ProfileEditView()
+ }
+ .sheet(isPresented: $showingSettings) {
+ SettingsView()
+ }
+ .sheet(isPresented: $showingAchievements) {
+ AchievementsView()
+ }
+ .sheet(isPresented: $showingDataExport) {
+ DataExportView()
+ }
+ }
+}
+
+// MARK: - 用户信息卡片
+struct UserProfileCard: View {
+ let onEditProfile: () -> Void
+
+ var body: some View {
+ VStack(spacing: 16) {
+ HStack {
+ // 头像
+ Button(action: onEditProfile) {
+ ZStack {
+ Circle()
+ .fill(LinearGradient(
+ colors: [.purple, .blue],
+ startPoint: .topLeading,
+ endPoint: .bottomTrailing
+ ))
+ .frame(width: 80, height: 80)
+
+ Text("华")
+ .font(.title)
+ .fontWeight(.bold)
+ .foregroundColor(.white)
+ }
+ }
+
+ VStack(alignment: .leading, spacing: 4) {
+ Text("华中敏")
+ .font(.title2)
+ .fontWeight(.bold)
+
+ Text("成长探索者")
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+
+ HStack(spacing: 4) {
+ Image(systemName: "calendar")
+ .font(.caption)
+ Text("加入 30 天")
+ .font(.caption)
+ }
+ .foregroundColor(.secondary)
+ }
+
+ Spacer()
+
+ Button(action: onEditProfile) {
+ Image(systemName: "pencil")
+ .font(.title3)
+ .foregroundColor(.blue)
+ }
+ }
+
+ // 成长等级
+ VStack(spacing: 8) {
+ HStack {
+ Text("成长等级")
+ .font(.subheadline)
+ .fontWeight(.medium)
+
+ Spacer()
+
+ Text("Lv.5")
+ .font(.subheadline)
+ .fontWeight(.bold)
+ .foregroundColor(.purple)
+ }
+
+ ProgressView(value: 0.7)
+ .progressViewStyle(LinearProgressViewStyle(tint: .purple))
+
+ HStack {
+ Text("距离下一级还需 150 经验")
+ .font(.caption)
+ .foregroundColor(.secondary)
+
+ Spacer()
+ }
+ }
+ }
+ .padding()
+ .background(
+ RoundedRectangle(cornerRadius: 16)
+ .fill(Color(UIColor.systemGray6))
+ )
+ }
+}
+
+// MARK: - 成长数据概览
+struct GrowthOverviewCard: View {
+ var body: some View {
+ VStack(alignment: .leading, spacing: 16) {
+ Text("本周成长数据")
+ .font(.headline)
+ .fontWeight(.semibold)
+
+ HStack(spacing: 16) {
+ GrowthMetricView(
+ title: "情绪记录",
+ value: "12",
+ unit: "次",
+ color: .blue,
+ icon: "heart.fill"
+ )
+
+ GrowthMetricView(
+ title: "疗愈时长",
+ value: "45",
+ unit: "分钟",
+ color: .purple,
+ icon: "timer.circle.fill"
+ )
+ }
+
+ HStack(spacing: 16) {
+ GrowthMetricView(
+ title: "课题进展",
+ value: "3",
+ unit: "个",
+ color: .green,
+ icon: "checkmark.circle.fill"
+ )
+
+ GrowthMetricView(
+ title: "连续天数",
+ value: "7",
+ unit: "天",
+ color: .orange,
+ icon: "flame.fill"
+ )
+ }
+ }
+ .padding()
+ .background(
+ RoundedRectangle(cornerRadius: 16)
+ .fill(Color(.systemGray6))
+ )
+ }
+}
+
+struct GrowthMetricView: View {
+ let title: String
+ let value: String
+ let unit: String
+ let color: Color
+ let icon: String
+
+ var body: some View {
+ VStack(spacing: 8) {
+ HStack {
+ Image(systemName: icon)
+ .font(.title3)
+ .foregroundColor(color)
+
+ Spacer()
+ }
+
+ VStack(alignment: .leading, spacing: 2) {
+ HStack(alignment: .bottom, spacing: 2) {
+ Text(value)
+ .font(.title2)
+ .fontWeight(.bold)
+
+ Text(unit)
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+
+ Text(title)
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+ .padding()
+ .background(
+ RoundedRectangle(cornerRadius: 12)
+ .fill(Color(.systemBackground))
+ )
+ }
+}
+
+// MARK: - 菜单组件
+struct MenuSection: View {
+ let title: String
+ let content: Content
+
+ init(title: String, @ViewBuilder content: () -> Content) {
+ self.title = title
+ self.content = content()
+ }
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 12) {
+ Text(title)
+ .font(.headline)
+ .fontWeight(.semibold)
+
+ content
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+}
+
+struct MenuRow: View {
+ let icon: String
+ let title: String
+ let subtitle: String
+ let color: Color
+ let action: () -> Void
+
+ var body: some View {
+ Button(action: action) {
+ HStack(spacing: 16) {
+ ZStack {
+ RoundedRectangle(cornerRadius: 8)
+ .fill(color.opacity(0.1))
+ .frame(width: 40, height: 40)
+
+ Image(systemName: icon)
+ .font(.title3)
+ .foregroundColor(color)
+ }
+
+ VStack(alignment: .leading, spacing: 2) {
+ Text(title)
+ .font(.subheadline)
+ .fontWeight(.medium)
+ .foregroundColor(.primary)
+
+ Text(subtitle)
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+
+ Spacer()
+
+ Image(systemName: "chevron.right")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ .padding()
+ .background(
+ RoundedRectangle(cornerRadius: 12)
+ .fill(Color(.systemGray6))
+ )
+ }
+ .buttonStyle(PlainButtonStyle())
+ }
+}
+
+// MARK: - 子页面视图(占位符)
+struct ProfileEditView: View {
+ @Environment(\.dismiss) private var dismiss
+
+ var body: some View {
+ NavigationView {
+ VStack {
+ Text("个人资料编辑")
+ .font(.title)
+
+ Text("这里是个人资料编辑页面")
+ .foregroundColor(.secondary)
+ }
+ .navigationTitle("编辑资料")
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .navigationBarLeading) {
+ Button("取消") {
+ dismiss()
+ }
+ }
+
+ ToolbarItem(placement: .navigationBarTrailing) {
+ Button("保存") {
+ dismiss()
+ }
+ }
+ }
+ }
+ }
+}
+
+struct SettingsView: View {
+ @Environment(\.dismiss) private var dismiss
+
+ var body: some View {
+ NavigationView {
+ VStack {
+ Text("应用设置")
+ .font(.title)
+
+ Text("这里是应用设置页面")
+ .foregroundColor(.secondary)
+ }
+ .navigationTitle("设置")
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .navigationBarTrailing) {
+ Button("完成") {
+ dismiss()
+ }
+ }
+ }
+ }
+ }
+}
+
+struct AchievementsView: View {
+ @Environment(\.dismiss) private var dismiss
+
+ var body: some View {
+ NavigationView {
+ ScrollView {
+ LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 16) {
+ ForEach(0..<6) { index in
+ AchievementCard(achievement: sampleAchievements[index])
+ }
+ }
+ .padding()
+ }
+ .navigationTitle("成就徽章")
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .navigationBarTrailing) {
+ Button("完成") {
+ dismiss()
+ }
+ }
+ }
+ }
+ }
+
+ private var sampleAchievements: [Achievement] {
+ [
+ Achievement(title: "初心者", description: "完成第一次情绪记录", category: .milestone, icon: "star.fill", rarity: .common, requirement: .conversationCount(1), targetValue: 1, unlockedAt: Date()),
+ Achievement(title: "坚持者", description: "连续7天记录情绪", category: .consistency, icon: "flame.fill", rarity: .rare, requirement: .consecutiveDays(7), targetValue: 7, unlockedAt: Date()),
+ Achievement(title: "探索者", description: "开始第一个课题", category: .growth, icon: "map.fill", rarity: .common, requirement: .topicCompletion(1), targetValue: 1, unlockedAt: Date()),
+ Achievement(title: "疗愈师", description: "完成10次脉轮疗愈", category: .emotion, icon: "heart.fill", rarity: .epic, requirement: .emotionRecordCount(10), targetValue: 10),
+ Achievement(title: "成长者", description: "完成一个完整课题", category: .growth, icon: "trophy.fill", rarity: .rare, requirement: .topicCompletion(5), targetValue: 5),
+ Achievement(title: "大师", description: "达到10级成长等级", category: .milestone, icon: "crown.fill", rarity: .legendary, requirement: .totalPoints(10000), targetValue: 10000)
+ ]
+ }
+}
+
+struct DataExportView: View {
+ @Environment(\.dismiss) private var dismiss
+
+ var body: some View {
+ NavigationView {
+ VStack(spacing: 20) {
+ Text("数据导出")
+ .font(.title)
+
+ Text("选择要导出的数据类型")
+ .foregroundColor(.secondary)
+
+ VStack(spacing: 12) {
+ ExportOptionRow(title: "情绪记录", description: "所有的情绪记录数据")
+ ExportOptionRow(title: "疗愈记录", description: "脉轮疗愈会话记录")
+ ExportOptionRow(title: "课题进展", description: "成长课题和进展数据")
+ ExportOptionRow(title: "成就数据", description: "解锁的成就和里程碑")
+ }
+
+ Spacer()
+
+ Button("导出数据") {
+ // 导出逻辑
+ }
+ .buttonStyle(.borderedProminent)
+ }
+ .padding()
+ .navigationTitle("导出数据")
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .navigationBarTrailing) {
+ Button("取消") {
+ dismiss()
+ }
+ }
+ }
+ }
+ }
+}
+
+struct ExportOptionRow: View {
+ let title: String
+ let description: String
+ @State private var isSelected = false
+
+ var body: some View {
+ HStack {
+ VStack(alignment: .leading, spacing: 4) {
+ Text(title)
+ .font(.subheadline)
+ .fontWeight(.medium)
+
+ Text(description)
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+
+ Spacer()
+
+ Toggle("", isOn: $isSelected)
+ }
+ .padding()
+ .background(
+ RoundedRectangle(cornerRadius: 12)
+ .fill(Color(.systemGray6))
+ )
+ }
+}
+
+struct AchievementCard: View {
+ let achievement: Achievement
+
+ var body: some View {
+ VStack(spacing: 12) {
+ ZStack {
+ Circle()
+ .fill(achievement.isUnlocked ? achievement.rarity.color.opacity(0.2) : Color.gray.opacity(0.2))
+ .frame(width: 60, height: 60)
+
+ Image(systemName: achievement.icon)
+ .font(.title2)
+ .foregroundColor(achievement.isUnlocked ? achievement.rarity.color : .gray)
+ }
+
+ VStack(spacing: 4) {
+ Text(achievement.title)
+ .font(.subheadline)
+ .fontWeight(.medium)
+ .foregroundColor(achievement.isUnlocked ? .primary : .secondary)
+
+ Text(achievement.description)
+ .font(.caption)
+ .foregroundColor(.secondary)
+ .multilineTextAlignment(.center)
+ .lineLimit(2)
+ }
+ }
+ .padding()
+ .background(
+ RoundedRectangle(cornerRadius: 12)
+ .fill(Color(.systemGray6))
+ )
+ .opacity(achievement.isUnlocked ? 1.0 : 0.6)
+ }
+}
+
+// MARK: - 数据模型(使用DataModels中的Achievement)
+
+#Preview {
+ UniverseView()
+}
\ No newline at end of file
diff --git a/EmotionMuseum/EmotionMuseumApp.swift b/EmotionMuseum/EmotionMuseumApp.swift
deleted file mode 100644
index bdef7bb..0000000
--- a/EmotionMuseum/EmotionMuseumApp.swift
+++ /dev/null
@@ -1,20 +0,0 @@
-//
-// EmotionMuseumApp.swift
-// EmotionMuseum
-//
-// Created by 华中敏 on 2025/5/26.
-//
-
-import SwiftUI
-
-@main
-struct EmotionMuseumApp: App {
- let persistenceController = PersistenceController.shared
-
- var body: some Scene {
- WindowGroup {
- ContentView()
- .environment(\.managedObjectContext, persistenceController.container.viewContext)
- }
- }
-}
diff --git a/EmotionMuseumTests/EmotionMuseumTests.swift b/EmotionMuseum/EmotionMuseumTests/EmotionMuseumTests.swift
similarity index 78%
rename from EmotionMuseumTests/EmotionMuseumTests.swift
rename to EmotionMuseum/EmotionMuseumTests/EmotionMuseumTests.swift
index 62627b4..b3a3f73 100644
--- a/EmotionMuseumTests/EmotionMuseumTests.swift
+++ b/EmotionMuseum/EmotionMuseumTests/EmotionMuseumTests.swift
@@ -2,10 +2,11 @@
// EmotionMuseumTests.swift
// EmotionMuseumTests
//
-// Created by 华中敏 on 2025/5/26.
+// Created by 华中敏 on 2025/6/13.
//
import Testing
+@testable import EmotionMuseum
struct EmotionMuseumTests {
diff --git a/EmotionMuseum/EmotionMuseumTests/Info.plist b/EmotionMuseum/EmotionMuseumTests/Info.plist
new file mode 100644
index 0000000..86b8cc9
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseumTests/Info.plist
@@ -0,0 +1,22 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+
+
\ No newline at end of file
diff --git a/EmotionMuseumUITests/EmotionMuseumUITests.swift b/EmotionMuseum/EmotionMuseumUITests/EmotionMuseumUITests.swift
similarity index 97%
rename from EmotionMuseumUITests/EmotionMuseumUITests.swift
rename to EmotionMuseum/EmotionMuseumUITests/EmotionMuseumUITests.swift
index 1e92f95..b190676 100644
--- a/EmotionMuseumUITests/EmotionMuseumUITests.swift
+++ b/EmotionMuseum/EmotionMuseumUITests/EmotionMuseumUITests.swift
@@ -2,7 +2,7 @@
// EmotionMuseumUITests.swift
// EmotionMuseumUITests
//
-// Created by 华中敏 on 2025/5/26.
+// Created by 华中敏 on 2025/6/13.
//
import XCTest
diff --git a/EmotionMuseumUITests/EmotionMuseumUITestsLaunchTests.swift b/EmotionMuseum/EmotionMuseumUITests/EmotionMuseumUITestsLaunchTests.swift
similarity index 95%
rename from EmotionMuseumUITests/EmotionMuseumUITestsLaunchTests.swift
rename to EmotionMuseum/EmotionMuseumUITests/EmotionMuseumUITestsLaunchTests.swift
index fdb713b..d96a541 100644
--- a/EmotionMuseumUITests/EmotionMuseumUITestsLaunchTests.swift
+++ b/EmotionMuseum/EmotionMuseumUITests/EmotionMuseumUITestsLaunchTests.swift
@@ -2,7 +2,7 @@
// EmotionMuseumUITestsLaunchTests.swift
// EmotionMuseumUITests
//
-// Created by 华中敏 on 2025/5/26.
+// Created by 华中敏 on 2025/6/13.
//
import XCTest
diff --git a/EmotionMuseum/EmotionMuseumUITests/Info.plist b/EmotionMuseum/EmotionMuseumUITests/Info.plist
new file mode 100644
index 0000000..86b8cc9
--- /dev/null
+++ b/EmotionMuseum/EmotionMuseumUITests/Info.plist
@@ -0,0 +1,22 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+
+
\ No newline at end of file
diff --git a/EmotionMuseum/LocalPods/AMap_iOS_Foundation_Lib_V1.8.2.zip b/EmotionMuseum/LocalPods/AMap_iOS_Foundation_Lib_V1.8.2.zip
new file mode 100644
index 0000000..dd09d88
Binary files /dev/null and b/EmotionMuseum/LocalPods/AMap_iOS_Foundation_Lib_V1.8.2.zip differ
diff --git a/EmotionMuseum/MVP功能.pdf b/EmotionMuseum/MVP功能.pdf
new file mode 100644
index 0000000..fc8818e
Binary files /dev/null and b/EmotionMuseum/MVP功能.pdf differ
diff --git a/EmotionMuseum/images/65149df3-ab7e-457c-bfa2-bb74e2fed54c.svg b/EmotionMuseum/images/65149df3-ab7e-457c-bfa2-bb74e2fed54c.svg
new file mode 100644
index 0000000..56a9582
--- /dev/null
+++ b/EmotionMuseum/images/65149df3-ab7e-457c-bfa2-bb74e2fed54c.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/EmotionMuseum/images/WechatIMG374.jpg b/EmotionMuseum/images/WechatIMG374.jpg
new file mode 100644
index 0000000..f4318a5
Binary files /dev/null and b/EmotionMuseum/images/WechatIMG374.jpg differ
diff --git a/EmotionMuseum功能完善实施计划.md b/EmotionMuseum功能完善实施计划.md
new file mode 100644
index 0000000..a9f2690
--- /dev/null
+++ b/EmotionMuseum功能完善实施计划.md
@@ -0,0 +1,1173 @@
+# 情绪博物馆功能完善实施计划
+
+**创建时间**: 2025-07-05
+**项目状态**: 基础框架已完成,需要功能完善
+**开发框架**: SwiftUI + iOS 18.5
+
+---
+
+## 📋 当前状态分析
+
+### ✅ 已完成功能
+- [x] 基础TabView导航框架(记录、治愈、探索、个人)
+- [x] 主题系统(深色模式支持)
+- [x] 加载状态和骨架屏系统
+- [x] 动画过渡效果
+- [x] RecordView聊天化改造
+- [x] AI对话基础框架
+- [x] 情绪日历基础版
+
+### ❌ 待完善功能
+- [ ] 完整的AI对话系统
+- [ ] 治愈页面(成长课题系统)
+- [ ] 探索页面(地图+社区)
+- [ ] 个人页面(用户信息+成就)
+- [ ] 数据模型和存储
+- [ ] 模拟数据系统
+- [ ] 页面间跳转逻辑
+- [ ] 所有弹窗页面的关闭按钮
+
+---
+
+## 🎯 第一阶段:数据模型和模拟数据系统
+
+### 1.1 核心数据模型设计
+
+#### 用户模型
+```swift
+struct User {
+ let id: UUID
+ let username: String
+ let email: String
+ let avatar: String?
+ let profile: UserProfile
+ let createdAt: Date
+ let lastActiveAt: Date
+}
+
+struct UserProfile {
+ let nickname: String
+ let birthDate: Date?
+ let location: String?
+ let bio: String?
+ let memberLevel: MemberLevel
+ let totalDays: Int
+ let growthStats: GrowthStats
+}
+
+struct GrowthStats {
+ let selfAwareness: Float // 自我感知 0-100
+ let emotionalResilience: Float // 情绪韧性 0-100
+ let actionPower: Float // 行动力 0-100
+ let empathy: Float // 共情力 0-100
+ let lifeEnthusiasm: Float // 生活热度 0-100
+}
+```
+
+#### 对话系统模型
+```swift
+struct Conversation {
+ let id: UUID
+ let userId: UUID
+ let title: String
+ let messages: [Message]
+ let startTime: Date
+ let endTime: Date?
+ let emotionAnalysis: EmotionAnalysis?
+ let summary: String?
+}
+
+struct Message {
+ let id: UUID
+ let conversationId: UUID
+ let content: String
+ let type: MessageType
+ let sender: MessageSender
+ let timestamp: Date
+ let emotionScore: Float?
+}
+
+enum MessageType {
+ case text, voice, image, system
+}
+
+enum MessageSender {
+ case user, ai
+}
+
+struct EmotionAnalysis {
+ let primaryEmotion: EmotionType
+ let emotionIntensity: Float
+ let emotionTrend: EmotionTrend
+ let keywords: [String]
+ let aiInsights: String
+}
+```
+
+#### 成长课题模型
+```swift
+struct GrowthTopic {
+ let id: UUID
+ let title: String
+ let description: String
+ let category: TopicCategory
+ let difficulty: Difficulty
+ let progress: Float
+ let level: Int
+ let totalLevels: Int
+ let isUnlocked: Bool
+ let completedAt: Date?
+ let rewards: [Reward]
+ let interactions: [TopicInteraction]
+}
+
+enum TopicCategory {
+ case selfAwareness // 自我认知
+ case emotionRegulation // 情绪调节
+ case socialSkills // 社交技能
+ case stressManagement // 压力管理
+ case lifeGoals // 人生目标
+}
+
+struct TopicInteraction {
+ let id: UUID
+ let topicId: UUID
+ let type: InteractionType
+ let content: String
+ let completedAt: Date
+ let reward: Reward?
+}
+
+enum InteractionType {
+ case aiChat, article, exercise, reflection
+}
+```
+
+#### 地图和社区模型
+```swift
+struct LocationPin {
+ let id: UUID
+ let coordinate: Coordinate
+ let title: String
+ let description: String
+ let type: LocationType
+ let emotionTags: [EmotionType]
+ let photos: [String]
+ let createdBy: UUID?
+ let createdAt: Date
+ let likes: Int
+ let visits: Int
+}
+
+enum LocationType {
+ case personal // 个人收藏
+ case aiRecommended // AI推荐
+ case community // 社区分享
+}
+
+struct CommunityPost {
+ let id: UUID
+ let userId: UUID
+ let locationId: UUID?
+ let content: String
+ let photos: [String]
+ let tags: [String]
+ let likes: Int
+ let comments: [Comment]
+ let createdAt: Date
+ let isPrivate: Bool
+}
+
+struct Comment {
+ let id: UUID
+ let postId: UUID
+ let userId: UUID
+ let content: String
+ let createdAt: Date
+ let likes: Int
+}
+```
+
+### 1.2 模拟数据管理器
+
+创建 `MockDataManager.swift`:
+```swift
+class MockDataManager: ObservableObject {
+ static let shared = MockDataManager()
+
+ @Published var currentUser: User
+ @Published var conversations: [Conversation] = []
+ @Published var growthTopics: [GrowthTopic] = []
+ @Published var locationPins: [LocationPin] = []
+ @Published var communityPosts: [CommunityPost] = []
+ @Published var emotionRecords: [EmotionRecord] = []
+
+ private init() {
+ // 初始化模拟数据
+ }
+
+ // 生成丰富的模拟数据方法
+ func generateMockConversations() { }
+ func generateMockGrowthTopics() { }
+ func generateMockLocationPins() { }
+ func generateMockEmotionRecords() { }
+}
+```
+
+---
+
+## 🎯 第二阶段:RecordView功能完善
+
+### 2.1 聊天记录入口页面
+
+创建 `ChatHistoryView.swift`:
+```swift
+struct ChatHistoryView: View {
+ @Environment(\.dismiss) private var dismiss
+ @ObservedObject var mockData = MockDataManager.shared
+ @State private var searchText = ""
+ @State private var selectedFilter: ConversationFilter = .all
+
+ var body: some View {
+ NavigationView {
+ VStack(spacing: 0) {
+ // 搜索栏
+ searchBar
+
+ // 筛选器
+ filterTabs
+
+ // 对话列表
+ conversationList
+ }
+ .navigationTitle("聊天记录")
+ .navigationBarTitleDisplayMode(.large)
+ .toolbar {
+ ToolbarItem(placement: .navigationBarTrailing) {
+ Button("关闭") { dismiss() }
+ }
+ }
+ }
+ }
+
+ private var searchBar: some View { /* 搜索功能 */ }
+ private var filterTabs: some View { /* 筛选标签 */ }
+ private var conversationList: some View { /* 对话列表 */ }
+}
+```
+
+### 2.2 全屏对话页面
+
+创建 `FullScreenChatView.swift`:
+```swift
+struct FullScreenChatView: View {
+ @Environment(\.dismiss) private var dismiss
+ @StateObject private var chatViewModel = ChatViewModel()
+ @State private var inputText = ""
+ @State private var isVoiceMode = false
+
+ var body: some View {
+ VStack(spacing: 0) {
+ // 顶部导航栏
+ chatNavigationBar
+
+ // 消息列表
+ messagesList
+
+ // 输入区域
+ chatInputArea
+ }
+ .background(Color.theme.background)
+ .ignoresSafeArea(.keyboard, edges: .bottom)
+ }
+
+ private var chatNavigationBar: some View {
+ HStack {
+ Button("收起") { dismiss() }
+ Spacer()
+ Text("AI助手")
+ Spacer()
+ Button(isVoiceMode ? "文字" : "语音") {
+ isVoiceMode.toggle()
+ }
+ }
+ .padding()
+ }
+
+ private var messagesList: some View { /* 消息列表实现 */ }
+ private var chatInputArea: some View { /* 输入区域实现 */ }
+}
+```
+
+### 2.3 设置页面
+
+创建 `SettingsView.swift`:
+```swift
+struct SettingsView: View {
+ @Environment(\.dismiss) private var dismiss
+ @StateObject private var themeManager = ThemeManager()
+ @State private var musicVolume: Float = 0.5
+ @State private var soundVolume: Float = 0.7
+
+ var body: some View {
+ NavigationView {
+ List {
+ // 主题设置
+ themeSection
+
+ // 音效设置
+ audioSection
+
+ // 隐私设置
+ privacySection
+
+ // 关于设置
+ aboutSection
+ }
+ .navigationTitle("设置")
+ .navigationBarTitleDisplayMode(.large)
+ .toolbar {
+ ToolbarItem(placement: .navigationBarTrailing) {
+ Button("关闭") { dismiss() }
+ }
+ }
+ }
+ }
+
+ private var themeSection: some View { /* 主题设置 */ }
+ private var audioSection: some View { /* 音效设置 */ }
+ private var privacySection: some View { /* 隐私设置 */ }
+ private var aboutSection: some View { /* 关于设置 */ }
+}
+```
+
+---
+
+## 🎯 第三阶段:GrowthView(治愈页面)完善
+
+### 3.1 成长课题主页
+
+完善 `GrowthView.swift`:
+```swift
+struct GrowthView: View {
+ @StateObject private var themeManager = ThemeManager()
+ @ObservedObject var mockData = MockDataManager.shared
+ @State private var selectedCategory: TopicCategory = .selfAwareness
+ @State private var showingTopicDetail = false
+ @State private var selectedTopic: GrowthTopic?
+
+ var body: some View {
+ NavigationView {
+ ScrollView {
+ LazyVStack(spacing: 20) {
+ // 个人成长概览
+ growthOverviewCard
+
+ // 五维雷达图
+ growthRadarChart
+
+ // 课题分类标签
+ categoryTabs
+
+ // 课题列表
+ topicsList
+
+ // 成就展示
+ achievementsSection
+ }
+ .padding(.horizontal, 16)
+ }
+ .navigationTitle("治愈")
+ .navigationBarTitleDisplayMode(.large)
+ .refreshable {
+ await refreshGrowthData()
+ }
+ }
+ .environmentObject(themeManager)
+ .sheet(item: $selectedTopic) { topic in
+ TopicDetailView(topic: topic)
+ }
+ }
+
+ private var growthOverviewCard: some View { /* 成长概览卡片 */ }
+ private var growthRadarChart: some View { /* 五维雷达图 */ }
+ private var categoryTabs: some View { /* 分类标签 */ }
+ private var topicsList: some View { /* 课题列表 */ }
+ private var achievementsSection: some View { /* 成就展示 */ }
+}
+```
+
+### 3.2 课题详情页面
+
+创建 `TopicDetailView.swift`:
+```swift
+struct TopicDetailView: View {
+ @Environment(\.dismiss) private var dismiss
+ let topic: GrowthTopic
+ @State private var showingInteraction = false
+ @State private var selectedInteractionType: InteractionType = .aiChat
+
+ var body: some View {
+ NavigationView {
+ ScrollView {
+ VStack(spacing: 24) {
+ // 课题头部信息
+ topicHeader
+
+ // 进度展示
+ progressSection
+
+ // 互动选项
+ interactionOptions
+
+ // 已完成的互动
+ completedInteractions
+
+ // 相关推荐
+ relatedTopics
+ }
+ .padding()
+ }
+ .navigationTitle(topic.title)
+ .navigationBarTitleDisplayMode(.large)
+ .toolbar {
+ ToolbarItem(placement: .navigationBarTrailing) {
+ Button("关闭") { dismiss() }
+ }
+ }
+ }
+ .sheet(isPresented: $showingInteraction) {
+ TopicInteractionView(
+ topic: topic,
+ interactionType: selectedInteractionType
+ )
+ }
+ }
+
+ private var topicHeader: some View { /* 课题头部 */ }
+ private var progressSection: some View { /* 进度展示 */ }
+ private var interactionOptions: some View { /* 互动选项 */ }
+ private var completedInteractions: some View { /* 已完成互动 */ }
+ private var relatedTopics: some View { /* 相关推荐 */ }
+}
+```
+
+### 3.3 课题互动页面
+
+创建 `TopicInteractionView.swift`:
+```swift
+struct TopicInteractionView: View {
+ @Environment(\.dismiss) private var dismiss
+ let topic: GrowthTopic
+ let interactionType: InteractionType
+ @State private var interactionContent = ""
+ @State private var isCompleted = false
+
+ var body: some View {
+ NavigationView {
+ VStack {
+ switch interactionType {
+ case .aiChat:
+ TopicChatView(topic: topic)
+ case .article:
+ TopicArticleView(topic: topic)
+ case .exercise:
+ TopicExerciseView(topic: topic)
+ case .reflection:
+ TopicReflectionView(topic: topic)
+ }
+ }
+ .navigationTitle(interactionType.title)
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .navigationBarLeading) {
+ Button("关闭") { dismiss() }
+ }
+ ToolbarItem(placement: .navigationBarTrailing) {
+ Button("完成") {
+ completeInteraction()
+ }
+ .disabled(!isCompleted)
+ }
+ }
+ }
+ }
+
+ private func completeInteraction() {
+ // 完成互动逻辑
+ dismiss()
+ }
+}
+```
+
+---
+
+## 🎯 第四阶段:ExploreView(探索页面)完善
+
+### 4.1 地图主页
+
+完善 `ExploreView.swift`:
+```swift
+import MapKit
+
+struct ExploreView: View {
+ @StateObject private var themeManager = ThemeManager()
+ @StateObject private var mapManager = MapManager()
+ @ObservedObject var mockData = MockDataManager.shared
+ @State private var viewMode: ExploreViewMode = .map
+ @State private var selectedPin: LocationPin?
+ @State private var showingPinDetail = false
+ @State private var showingAddPin = false
+
+ var body: some View {
+ NavigationView {
+ ZStack {
+ if viewMode == .map {
+ mapView
+ } else {
+ communityView
+ }
+
+ // 浮动控件
+ floatingControls
+ }
+ .navigationTitle("探索")
+ .navigationBarTitleDisplayMode(.large)
+ .toolbar {
+ ToolbarItem(placement: .navigationBarTrailing) {
+ Button(viewMode == .map ? "社区" : "地图") {
+ withAnimation {
+ viewMode = viewMode == .map ? .community : .map
+ }
+ }
+ }
+ }
+ }
+ .environmentObject(themeManager)
+ .sheet(item: $selectedPin) { pin in
+ LocationDetailView(pin: pin)
+ }
+ .sheet(isPresented: $showingAddPin) {
+ AddLocationView()
+ }
+ }
+
+ private var mapView: some View {
+ Map(coordinateRegion: $mapManager.region, annotationItems: mockData.locationPins) { pin in
+ MapAnnotation(coordinate: pin.coordinate.clLocationCoordinate2D) {
+ LocationPinView(pin: pin) {
+ selectedPin = pin
+ showingPinDetail = true
+ }
+ }
+ }
+ .ignoresSafeArea()
+ }
+
+ private var communityView: some View {
+ CommunityFeedView()
+ }
+
+ private var floatingControls: some View { /* 浮动控件 */ }
+}
+
+enum ExploreViewMode {
+ case map, community
+}
+```
+
+### 4.2 地点详情页面
+
+创建 `LocationDetailView.swift`:
+```swift
+struct LocationDetailView: View {
+ @Environment(\.dismiss) private var dismiss
+ let pin: LocationPin
+ @State private var showingComments = false
+ @State private var isLiked = false
+ @State private var showingNavigation = false
+
+ var body: some View {
+ NavigationView {
+ ScrollView {
+ VStack(spacing: 20) {
+ // 地点图片轮播
+ locationPhotos
+
+ // 地点信息
+ locationInfo
+
+ // 情绪标签
+ emotionTags
+
+ // 操作按钮
+ actionButtons
+
+ // 相关笔记
+ relatedPosts
+
+ // 评论区域
+ commentsSection
+ }
+ .padding()
+ }
+ .navigationTitle(pin.title)
+ .navigationBarTitleDisplayMode(.large)
+ .toolbar {
+ ToolbarItem(placement: .navigationBarTrailing) {
+ Button("关闭") { dismiss() }
+ }
+ }
+ }
+ .sheet(isPresented: $showingNavigation) {
+ NavigationView(destination: pin.coordinate)
+ }
+ }
+
+ private var locationPhotos: some View { /* 图片轮播 */ }
+ private var locationInfo: some View { /* 地点信息 */ }
+ private var emotionTags: some View { /* 情绪标签 */ }
+ private var actionButtons: some View { /* 操作按钮 */ }
+ private var relatedPosts: some View { /* 相关笔记 */ }
+ private var commentsSection: some View { /* 评论区域 */ }
+}
+```
+
+### 4.3 社区动态页面
+
+创建 `CommunityFeedView.swift`:
+```swift
+struct CommunityFeedView: View {
+ @ObservedObject var mockData = MockDataManager.shared
+ @State private var refreshing = false
+ @State private var selectedPost: CommunityPost?
+
+ var body: some View {
+ ScrollView {
+ LazyVStack(spacing: 16) {
+ ForEach(mockData.communityPosts) { post in
+ CommunityPostCard(post: post) {
+ selectedPost = post
+ }
+ }
+ }
+ .padding(.horizontal, 16)
+ }
+ .refreshable {
+ await refreshFeed()
+ }
+ .sheet(item: $selectedPost) { post in
+ PostDetailView(post: post)
+ }
+ }
+
+ private func refreshFeed() async {
+ // 刷新动态逻辑
+ }
+}
+```
+
+### 4.4 添加地点页面
+
+创建 `AddLocationView.swift`:
+```swift
+struct AddLocationView: View {
+ @Environment(\.dismiss) private var dismiss
+ @State private var locationName = ""
+ @State private var locationDescription = ""
+ @State private var selectedEmotions: [EmotionType] = []
+ @State private var selectedPhotos: [UIImage] = []
+ @State private var selectedCoordinate: Coordinate?
+
+ var body: some View {
+ NavigationView {
+ Form {
+ Section("基本信息") {
+ TextField("地点名称", text: $locationName)
+ TextField("地点描述", text: $locationDescription, axis: .vertical)
+ .lineLimit(3...6)
+ }
+
+ Section("位置信息") {
+ LocationPickerView(selectedCoordinate: $selectedCoordinate)
+ }
+
+ Section("情绪标签") {
+ EmotionTagPicker(selectedEmotions: $selectedEmotions)
+ }
+
+ Section("添加照片") {
+ PhotoPickerView(selectedPhotos: $selectedPhotos)
+ }
+ }
+ .navigationTitle("添加地点")
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .navigationBarLeading) {
+ Button("取消") { dismiss() }
+ }
+ ToolbarItem(placement: .navigationBarTrailing) {
+ Button("保存") {
+ saveLocation()
+ }
+ .disabled(locationName.isEmpty)
+ }
+ }
+ }
+ }
+
+ private func saveLocation() {
+ // 保存地点逻辑
+ dismiss()
+ }
+}
+```
+
+---
+
+## 🎯 第五阶段:InsightView(个人页面)完善
+
+### 5.1 个人主页
+
+完善 `InsightView.swift`:
+```swift
+struct InsightView: View {
+ @StateObject private var themeManager = ThemeManager()
+ @ObservedObject var mockData = MockDataManager.shared
+ @State private var showingProfile = false
+ @State private var showingSettings = false
+ @State private var showingAchievements = false
+
+ var body: some View {
+ NavigationView {
+ ScrollView {
+ LazyVStack(spacing: 20) {
+ // 用户头像和基本信息
+ userProfileCard
+
+ // 本周数据统计
+ weeklyStatsCard
+
+ // 成就展示
+ achievementsCard
+
+ // 社交数据
+ socialStatsCard
+
+ // 快捷功能
+ quickActionsCard
+
+ // 会员中心
+ membershipCard
+ }
+ .padding(.horizontal, 16)
+ }
+ .navigationTitle("我的")
+ .navigationBarTitleDisplayMode(.large)
+ .toolbar {
+ ToolbarItem(placement: .navigationBarTrailing) {
+ Button(action: { showingSettings = true }) {
+ Image(systemName: "gearshape")
+ }
+ }
+ }
+ }
+ .environmentObject(themeManager)
+ .sheet(isPresented: $showingProfile) {
+ UserProfileView()
+ }
+ .sheet(isPresented: $showingSettings) {
+ SettingsView()
+ }
+ .sheet(isPresented: $showingAchievements) {
+ AchievementsView()
+ }
+ }
+
+ private var userProfileCard: some View { /* 用户资料卡片 */ }
+ private var weeklyStatsCard: some View { /* 本周统计 */ }
+ private var achievementsCard: some View { /* 成就展示 */ }
+ private var socialStatsCard: some View { /* 社交数据 */ }
+ private var quickActionsCard: some View { /* 快捷功能 */ }
+ private var membershipCard: some View { /* 会员中心 */ }
+}
+```
+
+### 5.2 用户资料页面
+
+创建 `UserProfileView.swift`:
+```swift
+struct UserProfileView: View {
+ @Environment(\.dismiss) private var dismiss
+ @ObservedObject var mockData = MockDataManager.shared
+ @State private var isEditing = false
+ @State private var editedProfile: UserProfile
+
+ init() {
+ _editedProfile = State(initialValue: MockDataManager.shared.currentUser.profile)
+ }
+
+ var body: some View {
+ NavigationView {
+ Form {
+ Section("基本信息") {
+ profileImageSection
+
+ if isEditing {
+ TextField("昵称", text: $editedProfile.nickname)
+ DatePicker("生日", selection: Binding(
+ get: { editedProfile.birthDate ?? Date() },
+ set: { editedProfile.birthDate = $0 }
+ ), displayedComponents: .date)
+ TextField("所在地", text: Binding(
+ get: { editedProfile.location ?? "" },
+ set: { editedProfile.location = $0 }
+ ))
+ TextField("个人简介", text: Binding(
+ get: { editedProfile.bio ?? "" },
+ set: { editedProfile.bio = $0 }
+ ), axis: .vertical)
+ .lineLimit(3...6)
+ } else {
+ profileDisplaySection
+ }
+ }
+
+ Section("成长数据") {
+ growthStatsSection
+ }
+
+ Section("账户信息") {
+ accountInfoSection
+ }
+ }
+ .navigationTitle("个人资料")
+ .navigationBarTitleDisplayMode(.large)
+ .toolbar {
+ ToolbarItem(placement: .navigationBarLeading) {
+ Button("关闭") { dismiss() }
+ }
+ ToolbarItem(placement: .navigationBarTrailing) {
+ Button(isEditing ? "保存" : "编辑") {
+ if isEditing {
+ saveProfile()
+ }
+ isEditing.toggle()
+ }
+ }
+ }
+ }
+ }
+
+ private var profileImageSection: some View { /* 头像编辑 */ }
+ private var profileDisplaySection: some View { /* 资料展示 */ }
+ private var growthStatsSection: some View { /* 成长数据 */ }
+ private var accountInfoSection: some View { /* 账户信息 */ }
+
+ private func saveProfile() {
+ // 保存用户资料
+ }
+}
+```
+
+### 5.3 成就页面
+
+创建 `AchievementsView.swift`:
+```swift
+struct AchievementsView: View {
+ @Environment(\.dismiss) private var dismiss
+ @ObservedObject var mockData = MockDataManager.shared
+ @State private var selectedCategory: AchievementCategory = .all
+
+ var body: some View {
+ NavigationView {
+ VStack(spacing: 0) {
+ // 成就统计概览
+ achievementOverview
+
+ // 分类筛选
+ categoryFilter
+
+ // 成就列表
+ achievementsList
+ }
+ .navigationTitle("我的成就")
+ .navigationBarTitleDisplayMode(.large)
+ .toolbar {
+ ToolbarItem(placement: .navigationBarTrailing) {
+ Button("关闭") { dismiss() }
+ }
+ }
+ }
+ }
+
+ private var achievementOverview: some View { /* 成就概览 */ }
+ private var categoryFilter: some View { /* 分类筛选 */ }
+ private var achievementsList: some View { /* 成就列表 */ }
+}
+```
+
+---
+
+## 🎯 第六阶段:页面跳转逻辑完善
+
+### 6.1 导航状态管理
+
+创建 `NavigationManager.swift`:
+```swift
+class NavigationManager: ObservableObject {
+ @Published var currentTab: MainTab = .record
+ @Published var recordNavigation = NavigationPath()
+ @Published var growthNavigation = NavigationPath()
+ @Published var exploreNavigation = NavigationPath()
+ @Published var insightNavigation = NavigationPath()
+
+ // 全局弹窗状态
+ @Published var showingChatHistory = false
+ @Published var showingFullScreenChat = false
+ @Published var showingSettings = false
+ @Published var showingProfile = false
+
+ func navigateToChat(conversation: Conversation?) {
+ showingFullScreenChat = true
+ }
+
+ func navigateToTopic(topic: GrowthTopic) {
+ growthNavigation.append(topic)
+ }
+
+ func navigateToLocation(pin: LocationPin) {
+ exploreNavigation.append(pin)
+ }
+}
+
+enum MainTab: CaseIterable {
+ case record, growth, explore, insight
+
+ var title: String {
+ switch self {
+ case .record: return "记录"
+ case .growth: return "治愈"
+ case .explore: return "探索"
+ case .insight: return "我的"
+ }
+ }
+
+ var icon: String {
+ switch self {
+ case .record: return "brain.head.profile"
+ case .growth: return "heart"
+ case .explore: return "map"
+ case .insight: return "person"
+ }
+ }
+}
+```
+
+### 6.2 更新ContentView
+
+更新主视图以支持完整的导航:
+```swift
+struct ContentView: View {
+ @StateObject private var navigationManager = NavigationManager()
+ @StateObject private var themeManager = ThemeManager()
+ @StateObject private var mockData = MockDataManager.shared
+
+ var body: some View {
+ TabView(selection: $navigationManager.currentTab) {
+ ForEach(MainTab.allCases, id: \.self) { tab in
+ NavigationStack(path: bindingForTab(tab)) {
+ viewForTab(tab)
+ .navigationDestination(for: GrowthTopic.self) { topic in
+ TopicDetailView(topic: topic)
+ }
+ .navigationDestination(for: LocationPin.self) { pin in
+ LocationDetailView(pin: pin)
+ }
+ .navigationDestination(for: CommunityPost.self) { post in
+ PostDetailView(post: post)
+ }
+ }
+ .tabItem {
+ Image(systemName: tab.icon)
+ Text(tab.title)
+ }
+ .tag(tab)
+ }
+ }
+ .environmentObject(navigationManager)
+ .environmentObject(themeManager)
+ .environmentObject(mockData)
+ .onAppear {
+ configureTabBarAppearance()
+ }
+ }
+
+ @ViewBuilder
+ private func viewForTab(_ tab: MainTab) -> some View {
+ switch tab {
+ case .record:
+ RecordView()
+ case .growth:
+ GrowthView()
+ case .explore:
+ ExploreView()
+ case .insight:
+ InsightView()
+ }
+ }
+
+ private func bindingForTab(_ tab: MainTab) -> Binding {
+ switch tab {
+ case .record:
+ return $navigationManager.recordNavigation
+ case .growth:
+ return $navigationManager.growthNavigation
+ case .explore:
+ return $navigationManager.exploreNavigation
+ case .insight:
+ return $navigationManager.insightNavigation
+ }
+ }
+}
+```
+
+---
+
+## 🎯 第七阶段:模拟数据生成
+
+### 7.1 丰富的模拟数据
+
+在 `MockDataManager.swift` 中添加:
+```swift
+extension MockDataManager {
+ func generateRichMockData() {
+ generateMockUser()
+ generateMockConversations()
+ generateMockGrowthTopics()
+ generateMockLocationPins()
+ generateMockCommunityPosts()
+ generateMockEmotionRecords()
+ generateMockAchievements()
+ }
+
+ private func generateMockUser() {
+ currentUser = User(
+ id: UUID(),
+ username: "emotion_explorer",
+ email: "user@example.com",
+ avatar: nil,
+ profile: UserProfile(
+ nickname: "情绪探索者",
+ birthDate: Calendar.current.date(byAdding: .year, value: -25, to: Date()),
+ location: "北京市",
+ bio: "在情绪的海洋中寻找内心的平静",
+ memberLevel: .premium,
+ totalDays: 127,
+ growthStats: GrowthStats(
+ selfAwareness: 78.5,
+ emotionalResilience: 65.2,
+ actionPower: 72.8,
+ empathy: 85.3,
+ lifeEnthusiasm: 69.7
+ )
+ ),
+ createdAt: Calendar.current.date(byAdding: .day, value: -127, to: Date()) ?? Date(),
+ lastActiveAt: Date()
+ )
+ }
+
+ private func generateMockConversations() {
+ // 生成30天的对话记录
+ for i in 0..<30 {
+ let date = Calendar.current.date(byAdding: .day, value: -i, to: Date()) ?? Date()
+ let conversation = createMockConversation(for: date)
+ conversations.append(conversation)
+ }
+ }
+
+ private func generateMockGrowthTopics() {
+ // 为每个分类生成3-5个课题
+ TopicCategory.allCases.forEach { category in
+ for i in 1...4 {
+ let topic = createMockTopic(category: category, index: i)
+ growthTopics.append(topic)
+ }
+ }
+ }
+
+ private func generateMockLocationPins() {
+ // 生成北京地区的模拟地点
+ let beijingCoordinates = [
+ (39.9042, 116.4074), // 天安门
+ (39.9163, 116.3972), // 故宫
+ (40.0031, 116.3272), // 颐和园
+ (39.8844, 116.5564), // 798艺术区
+ (39.9389, 116.3467), // 什刹海
+ ]
+
+ beijingCoordinates.enumerated().forEach { index, coord in
+ let pin = createMockLocationPin(
+ coordinate: Coordinate(latitude: coord.0, longitude: coord.1),
+ index: index
+ )
+ locationPins.append(pin)
+ }
+ }
+
+ // 具体的创建方法...
+}
+```
+
+---
+
+## 📅 实施时间表
+
+### Week 1: 数据模型和基础功能
+- [ ] Day 1-2: 创建完整的数据模型
+- [ ] Day 3-4: 实现MockDataManager和模拟数据
+- [ ] Day 5-7: 完善RecordView的所有子页面
+
+### Week 2: 治愈页面开发
+- [ ] Day 1-3: 实现GrowthView主页和课题系统
+- [ ] Day 4-5: 开发课题详情和互动页面
+- [ ] Day 6-7: 实现五维雷达图和成就系统
+
+### Week 3: 探索页面开发
+- [ ] Day 1-3: 集成地图SDK和地点功能
+- [ ] Day 4-5: 实现社区动态和分享功能
+- [ ] Day 6-7: 完善地点详情和导航功能
+
+### Week 4: 个人页面和完善
+- [ ] Day 1-2: 实现个人资料和设置页面
+- [ ] Day 3-4: 完善成就系统和统计功能
+- [ ] Day 5-7: 全面测试和优化
+
+---
+
+## ✅ 验收标准
+
+### 功能完整性
+- [ ] 所有页面都有关闭按钮
+- [ ] 页面间跳转逻辑完整
+- [ ] 模拟数据丰富真实
+- [ ] 支持深色模式
+- [ ] 动画流畅自然
+
+### 用户体验
+- [ ] 界面美观一致
+- [ ] 交互反馈及时
+- [ ] 加载状态友好
+- [ ] 错误处理完善
+- [ ] 无功能死角
+
+### 技术质量
+- [ ] 代码结构清晰
+- [ ] 组件复用性好
+- [ ] 性能表现良好
+- [ ] 内存使用合理
+- [ ] 无崩溃和卡顿
+
+---
+
+这个实施计划将指导我们按步骤完善整个应用,确保每个功能都得到充分实现。接下来我将按照这个计划开始实施第一阶段的工作。
\ No newline at end of file
diff --git a/MVP功能需求文档.md b/MVP功能需求文档.md
new file mode 100644
index 0000000..a5a5cfb
--- /dev/null
+++ b/MVP功能需求文档.md
@@ -0,0 +1,267 @@
+# MVP功能需求文档
+
+**生成时间**: 2025-07-03 11:31:17
+
+**源文件**: MVP功能.pdf
+
+**总页数**: 7
+
+MVP功能
+
+你已经为MVP构想出一个⾮常完整、用户体验导向的原型了!下面我将按照你提出的四个页面结构,
+
+把功能要点进行**模块化梳理**,方便你用来指引设计开发,也方便你在Cursor⾥逐步实现每个部分
+
+的前端/后端逻辑。
+
+### ✅整体结构:底部导航栏4个主页面
+
+导航项:聊天|开心||探索|我的
+
+## 📘页面一:记录(情绪主页+聊天入⼝)
+
+### ✅核心功能:
+
+- 左上⻆入⼝:聊天内容回顾页(情绪对话记录索引页)。
+- 右上⻆设置(主题切换)
+- ⽇历组件(单行,可展开):支持情绪记录、历史回顾标记。
+- 中心展示AI疗愈师3DIP(静态MVP阶段可用插图代替)
+- 底部对话框输入:
+- 支持语⾳、文字、图⽚输入
+- 点发送后进入全屏对话模式(AI聊天),AIIP缩为头像
+
+**技术建议:**
+
+| MVP功能 你已经为MVP构想出一个⾮常完整、用户体验导向的原型了!下面我将按照你提出的四个页面结构, 把功能要点进行**模块化梳理**,方便你用来指引设计开发,也方便你在Cursor⾥逐步实现每个部分 的前端/后端逻辑。 |
+| --- |
+| ✅整体结构:底部导航栏4个主页面 导航项:聊天|开心||探索|我的 |
+| 📘页面一:记录(情绪主页+聊天入⼝) ✅核心功能: • 左上⻆入⼝:聊天内容回顾页(情绪对话记录索引页)。 • 右上⻆设置(主题切换) • ⽇历组件(单行,可展开):支持情绪记录、历史回顾标记。 • 中心展示AI疗愈师3DIP(静态MVP阶段可用插图代替) • 底部对话框输入: • 支持语⾳、文字、图⽚输入 • 点发送后进入全屏对话模式(AI聊天),AIIP缩为头像 技术建议: |
+
+
+---
+- 可用 做语⾳转文本MVP
+react-speech-recognition
+
+- 全屏聊天页面:使用GPT-4或现成情绪对话接⼝+表情动画反馈
+①左上⻆
+
+②右上⻆
+
+(⽇记本图 ③单行⽇历 折叠/展开 选择⽇期
+
+(设置图标)
+
+标)
+
+可滚动页面 ⽇历数字变 弹出可选择的 主题设置
+
+AI对话记录 为心情图标 选择心情图标 心情图标 ⾳乐设置
+
+⾳效设置
+
+选择记录 选择主题
+
+调整⾳量
+
+聊天记录
+
+调整⾳量
+
+随机打招呼文案
+
+跳转聊天详情 界面主题变化
+
+⾳乐⾳量变化
+
+⾳效⾳量变化
+
+AI总结
+
+查看课题 表情动作变化
+
+点击产生交互 ⽓泡文案变化
+
+(跳转课题系统)
+
+语⾳跟随⽓泡
+
+跟随手指移动
+
++ 图⽚
+
+语⾳
+
+输入文字...
+
+⻨克⻛
+
+记录 聊天
+
+AI 探索 发现 我的
+
+分析心情 反馈⾄③,⽇历数字变为心情图标
+
+AI智能分析
+
+记录 内容收录⾄①
+
+(A)
+
+反馈⾄①,未读提示
+
+分析内容
+
+反馈⾄课题系统(若有))
+
+语⾳ 聊天已收录
+
+AI智能分析 顶部 反馈⾄页面2
+
+聊天
+
+(A) 弹窗 未读提示
+
+解锁新课题
+
+文字
+
+课题进度有更新
+
+
+---
+语⾳聊天 文字聊天
+
+切换全屏 切换全屏
+
+收起 X
+
+切换文字聊天 收起
+
+可滚动页面
+
++ 图⽚
+
+语⾳
+
+挂断 文本输入框
+
+⻨克⻛
+
+## 📗页面二:治愈(个人成长档案)
+
+### ✅模块结构:
+
+A.情绪洞察与成长课题板块
+
+- 来自:
+1. ⽇常聊天自动总结
+2. 主动探索测试
+3. 命盘(出生数据生成)
+B.课题标签系统
+
+- 每个课题一个标签,包含:
+- 当前等级/进度
+| 语⾳聊天 文字聊天 切换全屏 切换全屏 收起 X 切换文字聊天 收起 可滚动页面 + 图⽚ 语⾳ 挂断 文本输入框 ⻨克⻛ |
+| --- |
+| 📗页面二:治愈(个人成长档案) ✅模块结构: A.情绪洞察与成长课题板块 • 来自: 1. ⽇常聊天自动总结 2. 主动探索测试 3. 命盘(出生数据生成) B.课题标签系统 • 每个课题一个标签,包含: • 当前等级/进度 |
+
+
+---
+- 可点入:AI对话、知识文章、行动建议
+- 完成一次互动:课题升级/积分/⽪肤/称号掉落
+C.用户画像五维图(或视觉更优形式)
+
+- 根据用户的成长路径动态生成
+- 示例:自我感知|情绪韧性|行动力|共情力|生活热度
+
+**技术建议:**
+
+- 使用radarchart(五边图)或Tag系统管理成长维度
+- Tag模块可作为数据库中的 表维护
+UserGrowthTopic
+
+| • 可点入:AI对话、知识文章、行动建议 • 完成一次互动:课题升级/积分/⽪肤/称号掉落 C.用户画像五维图(或视觉更优形式) • 根据用户的成长路径动态生成 • 示例:自我感知|情绪韧性|行动力|共情力|生活热度 技术建议: • 使用radarchart(五边图)或Tag系统管理成长维度 • Tag模块可作为数据库中的 表维护 UserGrowthTopic |
+| --- |
+| |
+| |
+| |
+
+
+### 页面图片信息
+
+- 图片 1: X163
+
+
+---
+## 📙页面三:探索(情绪地图+笔记分享)
+
+### ✅地图展示+推荐逻辑
+
+- 地图上两类地标颜⾊:
+1. 用户自⼰收藏/计划出行地
+2. AI根据情绪推荐场景(匹配情绪+地点)
+- 地标弹窗内容:
+- 场景图⽚
+- 评论(系统/用户)
+- 社区笔记
+- 收藏/加入计划
+### ✅分享视图切换
+
+- 地图左上⻆切换为**社区模式**(美好⻆落分享)
+- 用户上传图⽚+文字标记城市疗愈⻆落
+- 可点赞、评论、转发
+
+---
+
+**技术建议:**
+
+- 使用Leaflet/Mapbox/⾼德地图开放平台
+- 笔记内容结构可参考小红书feed设计(图文瀑布流)
+## 📕页面四:我的
+
+### ✅用户信息模块
+
+- 用户基础资料(注册天数)
+- 会员中心(MVP可为静态页)
+- 邀请好友模块(可做邀请返积分)
+### ✅成就总结
+
+- 本周:
+| |
+| --- |
+| |
+| 技术建议: • 使用Leaflet/Mapbox/⾼德地图开放平台 • 笔记内容结构可参考小红书feed设计(图文瀑布流) |
+| 📕页面四:我的 ✅用户信息模块 • 用户基础资料(注册天数) • 会员中心(MVP可为静态页) • 邀请好友模块(可做邀请返积分) ✅成就总结 • 本周: |
+
+
+### 页面图片信息
+
+- 图片 1: X176
+
+
+---
+- 心情指数
+- 聊天次数
+- 成长轨迹
+- 打卡记录:
+- 疗愈地标打卡数
+- 分享数
+- 他人访问你分享的次数
+#### 🧠额外建议(冷启动优先级)
+
+
+**MVP推荐优先搭建模块顺序:**
+
+1. 记录页(对话+⽇历)
+2. 治愈页(课题总结+标签互动)
+3. 探索页(地图基础+笔记系统)
+4. 我的页(基础资料+成就统计)
+如你愿意,我可以现在就帮你用Cursor建一个最小结构的React项目框架,含底部导航和主页面布
+
+局,并生成页面组件文件名结构、推荐依赖包清单和初步布局代码。你要不要我开始搭建这部分?
+
+| • 心情指数 • 聊天次数 • 成长轨迹 • 打卡记录: • 疗愈地标打卡数 • 分享数 • 他人访问你分享的次数 |
+| --- |
+| 🧠额外建议(冷启动优先级) MVP推荐优先搭建模块顺序: 1. 记录页(对话+⽇历) 2. 治愈页(课题总结+标签互动) 3. 探索页(地图基础+笔记系统) 4. 我的页(基础资料+成就统计) |
+| 如你愿意,我可以现在就帮你用Cursor建一个最小结构的React项目框架,含底部导航和主页面布 局,并生成页面组件文件名结构、推荐依赖包清单和初步布局代码。你要不要我开始搭建这部分? |
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e7e3301
--- /dev/null
+++ b/README.md
@@ -0,0 +1,336 @@
+# 情绪博物馆 (Emotion Museum)
+
+一个基于Spring Cloud Alibaba微服务架构的情绪管理和AI对话平台。
+
+## 项目概述
+
+情绪博物馆是一个创新的情绪健康管理平台,通过AI对话、情绪记录、数据分析等功能,帮助用户更好地理解和管理自己的情绪状态。
+
+## 🚀 快速部署
+
+### 一键部署(推荐)
+
+```bash
+# 克隆项目
+git clone
+cd EmotionMuseum
+
+# 执行一键部署到阿里云服务器
+./deploy-final.sh all
+```
+
+### 分步部署
+
+```bash
+# 1. 构建项目
+./deploy-final.sh build
+
+# 2. 配置服务器环境
+./deploy-final.sh env
+
+# 3. 部署数据库和中间件
+./deploy-final.sh mysql
+./deploy-final.sh redis
+./deploy-final.sh nacos
+
+# 4. 部署应用
+./deploy-final.sh upload
+./deploy-final.sh import-db
+./deploy-final.sh deploy
+
+# 5. 配置Web服务器
+./deploy-final.sh nginx
+
+# 6. 健康检查
+./deploy-final.sh health
+```
+
+### 服务管理
+
+```bash
+# 查看服务状态
+./deploy-final.sh status
+
+# 启动/停止/重启服务
+./deploy-final.sh start
+./deploy-final.sh stop
+./deploy-final.sh restart
+
+# 查看服务日志
+./deploy-final.sh logs gateway
+./deploy-final.sh logs ai
+./deploy-final.sh logs user
+```
+
+## 📋 部署后访问地址
+
+- **前端应用**: http://47.111.10.27/emotion-museum/
+- **API网关**: http://47.111.10.27:9000
+- **Nacos控制台**: http://47.111.10.27:8848/nacos
+
+## 🏗️ 技术架构
+
+### 后端技术栈
+- **框架**: Spring Boot 3.0.2 + Spring Cloud Alibaba
+- **数据库**: MySQL 8.0
+- **缓存**: Redis 7.0
+- **注册中心**: Nacos 2.2.0
+- **网关**: Spring Cloud Gateway
+- **ORM**: MyBatis Plus
+- **AI集成**: Spring AI + Coze平台
+
+### 前端技术栈
+- **框架**: Vue 3 + Vite
+- **UI组件**: Ant Design Vue
+- **状态管理**: Pinia
+- **路由**: Vue Router
+- **HTTP客户端**: Axios
+
+## 📁 项目结构
+
+```
+EmotionMuseum/
+├── backend/ # 后端微服务
+│ ├── emotion-common/ # 公共模块
+│ ├── emotion-gateway/ # API网关
+│ ├── emotion-user/ # 用户服务
+│ ├── emotion-ai/ # AI对话服务
+│ ├── emotion-record/ # 情绪记录服务
+│ ├── emotion-growth/ # 成长分析服务
+│ ├── emotion-explore/ # 地图探索服务
+│ ├── emotion-reward/ # 奖励系统服务
+│ └── emotion-stats/ # 统计分析服务
+├── web/ # 前端应用
+├── deploy-final.sh # 一键部署脚本
+├── docker-compose.prod.yml # 生产环境Docker配置
+├── .env.prod # 生产环境配置
+├── DEPLOYMENT.md # 详细部署指南
+└── README.md # 项目说明
+```
+
+## 🛠️ 本地开发
+
+### 环境要求
+- Java 17+
+- Maven 3.6+
+- Node.js 18+
+- MySQL 8.0+
+- Redis 7.0+
+
+### 开发环境启动
+
+1. **克隆项目**
+```bash
+git clone
+cd EmotionMuseum
+```
+
+2. **启动基础服务**
+```bash
+# 使用Docker Compose启动MySQL、Redis、Nacos
+docker-compose up -d
+```
+
+3. **导入数据库**
+```bash
+# 导入数据库结构和初始数据
+mysql -u root -p < backend/mysql_emotion_museum_final.sql
+```
+
+4. **构建并启动后端服务**
+```bash
+cd backend
+mvn clean package -DskipTests
+
+# 启动网关
+java -jar emotion-gateway/target/emotion-gateway-1.0.0.jar
+
+# 启动用户服务
+java -jar emotion-user/target/emotion-user-1.0.0.jar
+
+# 启动AI服务
+java -jar emotion-ai/target/emotion-ai-1.0.0.jar
+```
+
+5. **启动前端应用**
+```bash
+cd web
+npm install
+npm run dev
+```
+
+## 🔧 配置说明
+
+### 生产环境配置
+
+主要配置文件:
+- `.env.prod` - 生产环境变量配置
+- `web/.env.production` - 前端生产环境配置
+- `backend/*/src/main/resources/application-prod.yml` - 后端生产配置
+
+### 关键配置项
+
+```bash
+# 服务器配置
+SERVER_HOST=47.111.10.27
+SERVER_USER=root
+
+# 数据库配置
+MYSQL_HOST=localhost
+MYSQL_PORT=3306
+MYSQL_DATABASE=emotion_museum
+MYSQL_USERNAME=emotion
+MYSQL_PASSWORD=EmotionDB2024!
+
+# Redis配置
+REDIS_HOST=localhost
+REDIS_PORT=6379
+
+# Nacos配置
+NACOS_SERVER_ADDR=localhost:8848
+
+# Coze API配置
+COZE_API_TOKEN=your_token_here
+```
+
+## 🎯 核心功能
+
+### 1. AI智能对话
+- 基于Coze平台的智能对话
+- 情绪识别和分析
+- 个性化建议和指导
+
+### 2. 情绪记录
+- 多维度情绪数据记录
+- 情绪变化趋势分析
+- 情绪触发因素识别
+
+### 3. 成长分析
+- 个人成长轨迹追踪
+- 情绪健康评估
+- 目标设定和进度跟踪
+
+### 4. 地图探索
+- 情绪地图可视化
+- 情绪热点区域分析
+- 社区情绪趋势
+
+### 5. 奖励系统
+- 成就系统
+- 积分奖励
+- 等级提升
+
+## 📚 API文档
+
+### 用户服务 API
+- `POST /api/user/register` - 用户注册
+- `POST /api/user/login` - 用户登录
+- `GET /api/user/profile` - 获取用户信息
+
+### AI服务 API
+- `POST /api/ai/chat` - AI对话
+- `GET /api/ai/history` - 对话历史
+- `POST /api/ai/analyze` - 情绪分析
+
+### 记录服务 API
+- `POST /api/record/emotion` - 记录情绪
+- `GET /api/record/list` - 获取记录列表
+- `GET /api/record/stats` - 统计数据
+
+## 🔍 故障排除
+
+### 常见问题
+
+1. **服务无法启动**
+ ```bash
+ # 检查服务状态
+ ./deploy-final.sh status
+
+ # 查看日志
+ ./deploy-final.sh logs
+ ```
+
+2. **数据库连接失败**
+ ```bash
+ # 检查MySQL容器状态
+ ssh root@47.111.10.27 "docker ps | grep mysql"
+ ```
+
+3. **前端页面无法访问**
+ ```bash
+ # 检查Nginx状态
+ ssh root@47.111.10.27 "systemctl status nginx"
+ ```
+
+### 日志位置
+- 应用日志: `/data/logs/emotion-museum/*/app.log`
+- Nginx日志: `/var/log/nginx/`
+- Docker日志: `docker logs `
+
+## 🔒 安全建议
+
+1. **修改默认密码**
+ - MySQL root密码
+ - 应用数据库密码
+ - 服务器SSH密钥
+
+2. **配置防火墙**
+ ```bash
+ # 只开放必要端口
+ firewall-cmd --permanent --add-port=80/tcp
+ firewall-cmd --permanent --add-port=8848/tcp
+ firewall-cmd --reload
+ ```
+
+3. **定期备份**
+ ```bash
+ # 数据库备份
+ docker exec emotion-mysql-prod mysqldump -uemotion -pEmotionDB2024! emotion_museum > backup.sql
+ ```
+
+## 📈 监控和运维
+
+### 健康检查
+```bash
+# 执行完整健康检查
+./deploy-final.sh health
+```
+
+### 性能监控
+- Spring Boot Actuator端点
+- JVM性能监控
+- 数据库连接池监控
+- Redis连接监控
+
+## 🤝 贡献指南
+
+1. Fork 项目
+2. 创建功能分支 (`git checkout -b feature/AmazingFeature`)
+3. 提交代码 (`git commit -m 'Add some AmazingFeature'`)
+4. 推送到分支 (`git push origin feature/AmazingFeature`)
+5. 创建 Pull Request
+
+### 提交规范
+```
+feat: 新功能
+fix: 修复bug
+docs: 文档更新
+style: 代码格式调整
+refactor: 代码重构
+test: 测试相关
+chore: 构建过程或辅助工具的变动
+```
+
+## 📄 许可证
+
+本项目采用 MIT 许可证 - 详见 [LICENSE](LICENSE) 文件
+
+## 📞 联系我们
+
+- 项目主页: [GitHub Repository]
+- 问题反馈: [Issues]
+- 邮箱: support@emotionmuseum.com
+
+---
+
+**注意**: 请确保在生产环境中修改默认密码和敏感配置信息。详细部署说明请参考 [DEPLOYMENT.md](DEPLOYMENT.md)。
diff --git a/SERVER_DEPLOYMENT_CHECKLIST.md b/SERVER_DEPLOYMENT_CHECKLIST.md
new file mode 100644
index 0000000..3c36bdf
--- /dev/null
+++ b/SERVER_DEPLOYMENT_CHECKLIST.md
@@ -0,0 +1,175 @@
+# 服务器部署检查清单
+
+## 📋 部署前检查
+
+### 🖥️ 服务器要求
+- [ ] **操作系统**: Linux (Ubuntu 18.04+, CentOS 7+, Debian 9+)
+- [ ] **CPU**: 2核心以上
+- [ ] **内存**: 4GB以上(推荐8GB)
+- [ ] **磁盘**: 20GB以上可用空间
+- [ ] **网络**: 稳定的互联网连接
+
+### 🔧 软件环境
+- [ ] **Docker**: 20.10+ 已安装
+- [ ] **Docker Compose**: 1.29+ 已安装
+- [ ] **Git**: 已安装(可选)
+- [ ] **Curl/Wget**: 已安装
+
+### 🌐 网络配置
+- [ ] **端口开放**: 80, 443 已开放
+- [ ] **防火墙**: 已配置允许HTTP/HTTPS流量
+- [ ] **域名解析**: 域名已正确解析到服务器IP(如有)
+- [ ] **SSL证书**: 已准备好SSL证书文件(生产环境)
+
+## 📦 部署包准备
+
+### 📁 文件检查
+- [ ] **部署包**: `emotion-museum-1.0.0-20250713_111829.tar.gz` 已下载
+- [ ] **校验和**: SHA256校验通过
+- [ ] **解压**: 部署包已成功解压
+- [ ] **权限**: 脚本文件已设置执行权限
+
+### ⚙️ 配置文件
+- [ ] **环境变量**: `.env` 文件已配置
+- [ ] **Coze API**: API Token 已设置
+- [ ] **数据库密码**: 已修改默认密码
+- [ ] **域名配置**: Nginx配置中的域名已更新(生产环境)
+
+## 🚀 部署执行
+
+### 🔄 部署步骤
+- [ ] **1. 环境检查**: `./quick-deploy.sh` 环境检查通过
+- [ ] **2. Docker安装**: Docker和Docker Compose安装成功
+- [ ] **3. 配置生成**: 配置文件生成成功
+- [ ] **4. 镜像构建**: Docker镜像构建成功
+- [ ] **5. 服务启动**: 所有容器启动成功
+
+### 📊 服务状态
+- [ ] **MySQL**: 容器运行正常,数据库连接成功
+- [ ] **Redis**: 容器运行正常,缓存服务可用
+- [ ] **Nacos**: 容器运行正常,注册中心可访问
+- [ ] **Gateway**: 容器运行正常,网关服务可用
+- [ ] **AI Service**: 容器运行正常,AI服务可用
+- [ ] **User Service**: 容器运行正常,用户服务可用
+- [ ] **Frontend**: 容器运行正常,前端应用可访问
+- [ ] **Nginx**: 容器运行正常,反向代理工作正常
+
+## ✅ 部署验证
+
+### 🌐 访问测试
+- [ ] **前端首页**: http://your-domain.com 可正常访问
+- [ ] **API网关**: http://your-domain.com:9000/actuator/health 返回正常
+- [ ] **Nacos控制台**: http://your-domain.com:8848/nacos 可正常登录
+- [ ] **API文档**: http://your-domain.com:9000/doc.html 可正常访问
+
+### 🔍 功能测试
+- [ ] **用户注册**: 新用户注册功能正常
+- [ ] **用户登录**: 用户登录功能正常
+- [ ] **AI对话**: AI聊天功能正常
+- [ ] **数据存储**: 对话记录正常保存
+- [ ] **情绪分析**: 情绪分析功能正常
+
+### 📈 性能测试
+- [ ] **响应时间**: 页面加载时间 < 3秒
+- [ ] **API响应**: API接口响应时间 < 1秒
+- [ ] **并发测试**: 支持预期的并发用户数
+- [ ] **资源使用**: CPU和内存使用率在合理范围
+
+## 🔒 安全配置
+
+### 🛡️ 基础安全
+- [ ] **默认密码**: 所有默认密码已修改
+- [ ] **防火墙**: 只开放必要端口
+- [ ] **用户权限**: 使用非root用户运行服务
+- [ ] **文件权限**: 敏感文件权限设置正确
+
+### 🔐 HTTPS配置(生产环境)
+- [ ] **SSL证书**: 证书文件已正确放置
+- [ ] **Nginx配置**: HTTPS配置已启用
+- [ ] **HTTP重定向**: HTTP自动重定向到HTTPS
+- [ ] **证书验证**: SSL证书验证通过
+
+### 🔑 API安全
+- [ ] **JWT配置**: JWT密钥已设置
+- [ ] **CORS配置**: 跨域配置正确
+- [ ] **限流配置**: API限流规则已启用
+- [ ] **访问日志**: 访问日志记录正常
+
+## 📊 监控配置
+
+### 📈 服务监控
+- [ ] **健康检查**: `./manage.sh health` 所有服务健康
+- [ ] **日志收集**: 日志文件正常生成
+- [ ] **资源监控**: `./manage.sh monitor` 监控面板正常
+- [ ] **告警配置**: 异常告警机制已配置(可选)
+
+### 💾 数据备份
+- [ ] **备份脚本**: `./manage.sh backup` 备份功能正常
+- [ ] **备份策略**: 定期备份计划已制定
+- [ ] **恢复测试**: 数据恢复功能已测试
+- [ ] **备份存储**: 备份文件存储位置已确定
+
+## 📝 文档记录
+
+### 📋 部署记录
+- [ ] **部署时间**: 记录部署完成时间
+- [ ] **版本信息**: 记录部署的版本号
+- [ ] **配置信息**: 记录重要配置参数
+- [ ] **访问信息**: 记录访问地址和账号
+
+### 📖 运维文档
+- [ ] **管理命令**: 熟悉 `./manage.sh` 各项命令
+- [ ] **故障排除**: 了解常见问题解决方案
+- [ ] **更新流程**: 了解服务更新流程
+- [ ] **联系方式**: 记录技术支持联系方式
+
+## 🎯 部署后任务
+
+### 🔧 优化配置
+- [ ] **性能调优**: 根据实际负载调整配置
+- [ ] **缓存策略**: 优化Redis缓存配置
+- [ ] **数据库优化**: 调整MySQL配置参数
+- [ ] **网络优化**: 优化Nginx配置
+
+### 📊 监控设置
+- [ ] **日志轮转**: 配置日志文件轮转
+- [ ] **磁盘清理**: 设置定期清理任务
+- [ ] **性能监控**: 配置性能监控工具
+- [ ] **告警通知**: 设置异常告警通知
+
+### 🔄 维护计划
+- [ ] **更新计划**: 制定定期更新计划
+- [ ] **备份计划**: 制定数据备份计划
+- [ ] **安全审计**: 制定安全审计计划
+- [ ] **容量规划**: 制定容量扩展计划
+
+## ⚠️ 注意事项
+
+### 🚨 重要提醒
+1. **Coze API Token**: 必须配置正确的API Token,否则AI功能无法使用
+2. **数据库密码**: 生产环境必须修改默认密码
+3. **防火墙配置**: 确保只开放必要的端口
+4. **SSL证书**: 生产环境强烈建议使用HTTPS
+5. **定期备份**: 重要数据必须定期备份
+
+### 📞 紧急联系
+- **技术支持**: support@emotion-museum.com
+- **紧急热线**: 400-xxx-xxxx
+- **在线文档**: https://docs.emotion-museum.com
+
+---
+
+## ✅ 部署完成确认
+
+**部署工程师**: ________________
+**部署时间**: ________________
+**版本号**: emotion-museum-1.0.0-20250713_111829
+**服务器IP**: ________________
+**域名**: ________________
+
+**签名确认**: ________________
+**日期**: ________________
+
+---
+
+**🎉 恭喜完成部署!请妥善保存此检查清单作为部署记录。**
diff --git a/Spring Cloud Alibaba微服务架构设计.md b/Spring Cloud Alibaba微服务架构设计.md
new file mode 100644
index 0000000..7a57e89
--- /dev/null
+++ b/Spring Cloud Alibaba微服务架构设计.md
@@ -0,0 +1,285 @@
+# 情绪博物馆Spring Cloud Alibaba微服务架构设计
+
+**版本**: v1.0
+**创建时间**: 2025-07-12
+**技术栈**: Spring Cloud Alibaba 2022.0.0.0
+**Spring Boot版本**: 3.0.2
+**JDK版本**: 17+
+
+---
+
+## 📋 架构分析
+
+### 业务模块分析
+基于功能需求文档分析,情绪博物馆包含以下核心业务模块:
+
+1. **用户认证模块** - 账号、密码、手机号登录
+2. **AI对话模块** - 智能对话、情绪分析
+3. **情绪记录模块** - 情绪日历、记录管理
+4. **成长课题模块** - 课题系统、互动记录
+5. **地图探索模块** - 地点标记、社区分享
+6. **成就奖励模块** - 成就系统、积分奖励
+7. **用户统计模块** - 数据统计、分析报告
+
+### 技术架构选型
+
+#### Spring Cloud Alibaba 2022.0.0.0 组件
+- **Nacos**: 服务注册发现 + 配置中心
+- **Sentinel**: 流量控制、熔断降级
+- **Seata**: 分布式事务
+- **Gateway**: API网关
+- **OpenFeign**: 服务间调用
+- **Dubbo**: RPC通信(可选)
+
+#### 基础设施组件
+- **MySQL 8.0**: 主数据库
+- **Redis 7.0**: 缓存 + 分布式锁
+- **RocketMQ**: 消息队列
+- **Elasticsearch**: 搜索引擎
+- **MinIO**: 对象存储
+
+---
+
+## 🏗️ 微服务架构设计
+
+### 服务拆分策略
+
+```mermaid
+graph TB
+ A[API Gateway] --> B[用户服务]
+ A --> C[AI对话服务]
+ A --> D[情绪记录服务]
+ A --> E[成长课题服务]
+ A --> F[地图探索服务]
+ A --> G[成就奖励服务]
+ A --> H[统计分析服务]
+
+ B --> I[MySQL-用户库]
+ C --> J[MySQL-对话库]
+ D --> K[MySQL-情绪库]
+ E --> L[MySQL-成长库]
+ F --> M[MySQL-地图库]
+ G --> N[MySQL-奖励库]
+ H --> O[MySQL-统计库]
+
+ P[Nacos] --> A
+ P --> B
+ P --> C
+ P --> D
+ P --> E
+ P --> F
+ P --> G
+ P --> H
+
+ Q[Redis] --> B
+ Q --> C
+ Q --> D
+ Q --> E
+ Q --> F
+ Q --> G
+ Q --> H
+```
+
+### 微服务清单
+
+| 服务名称 | 端口 | 职责 | 数据库 |
+|---------|------|------|--------|
+| emotion-gateway | 8080 | API网关、路由、鉴权 | - |
+| emotion-user | 8081 | 用户认证、资料管理 | user, user_stats |
+| emotion-ai | 8082 | AI对话、情绪分析 | conversation, message |
+| emotion-record | 8083 | 情绪记录、日历管理 | emotion_record |
+| emotion-growth | 8084 | 成长课题、互动记录 | growth_topic, topic_interaction |
+| emotion-explore | 8085 | 地图探索、社区分享 | location_pin, community_post, comment |
+| emotion-reward | 8086 | 成就奖励、积分管理 | achievement, reward |
+| emotion-stats | 8087 | 数据统计、分析报告 | 跨库查询 |
+
+---
+
+## 📦 技术版本选择
+
+### Spring Cloud Alibaba 2022.0.0.0
+这是当前最稳定的版本,具有以下优势:
+- ✅ 与Spring Boot 3.0.x完美兼容
+- ✅ 支持JDK 17+
+- ✅ 生产环境验证充分
+- ✅ 社区活跃,文档完善
+- ✅ 阿里云原生支持
+
+### 版本依赖关系
+```xml
+
+ 17
+ 3.0.2
+ 2022.0.0
+ 2022.0.0.0
+ 8.0.33
+ 7.0.8
+ 3.5.3.1
+
+```
+
+---
+
+## 🔧 核心配置设计
+
+### Nacos配置中心
+```yaml
+# application-dev.yml
+spring:
+ cloud:
+ nacos:
+ discovery:
+ server-addr: localhost:8848
+ namespace: emotion-dev
+ group: DEFAULT_GROUP
+ config:
+ server-addr: localhost:8848
+ namespace: emotion-dev
+ group: DEFAULT_GROUP
+ file-extension: yml
+ shared-configs:
+ - data-id: common-mysql.yml
+ group: DEFAULT_GROUP
+ refresh: true
+ - data-id: common-redis.yml
+ group: DEFAULT_GROUP
+ refresh: true
+```
+
+### 数据库配置
+```yaml
+# common-mysql.yml
+spring:
+ datasource:
+ driver-class-name: com.mysql.cj.jdbc.Driver
+ url: jdbc:mysql://localhost:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
+ username: ${DB_USERNAME:root}
+ password: ${DB_PASSWORD:123456}
+ hikari:
+ minimum-idle: 5
+ maximum-pool-size: 20
+ idle-timeout: 600000
+ max-lifetime: 1800000
+ connection-timeout: 30000
+
+mybatis-plus:
+ configuration:
+ map-underscore-to-camel-case: true
+ log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
+ global-config:
+ db-config:
+ id-type: assign_uuid
+ logic-delete-field: deleted
+ logic-delete-value: 1
+ logic-not-delete-value: 0
+```
+
+### Redis配置
+```yaml
+# common-redis.yml
+spring:
+ data:
+ redis:
+ host: localhost
+ port: 6379
+ password: ${REDIS_PASSWORD:}
+ database: 0
+ timeout: 3000ms
+ lettuce:
+ pool:
+ max-active: 20
+ max-idle: 10
+ min-idle: 5
+ max-wait: 3000ms
+```
+
+---
+
+## 🚀 实施计划
+
+### Phase 1: 基础设施搭建 (1周)
+1. **父工程创建**
+ - Maven多模块项目结构
+ - 版本依赖管理
+ - 公共组件抽取
+
+2. **注册中心部署**
+ - Nacos Server安装配置
+ - 命名空间和分组设置
+ - 配置文件管理
+
+3. **数据库初始化**
+ - MySQL数据库创建
+ - 表结构和索引创建
+ - 初始数据导入
+
+### Phase 2: 核心服务开发 (2-3周)
+1. **网关服务** (emotion-gateway)
+ - 路由配置
+ - 统一鉴权
+ - 限流熔断
+
+2. **用户服务** (emotion-user)
+ - 用户注册登录
+ - JWT Token管理
+ - 用户资料CRUD
+
+3. **AI对话服务** (emotion-ai)
+ - 对话管理
+ - 消息处理
+ - AI接口集成
+
+### Phase 3: 业务服务开发 (3-4周)
+1. **情绪记录服务** (emotion-record)
+2. **成长课题服务** (emotion-growth)
+3. **地图探索服务** (emotion-explore)
+4. **成就奖励服务** (emotion-reward)
+
+### Phase 4: 数据服务开发 (1-2周)
+1. **统计分析服务** (emotion-stats)
+2. **监控告警配置**
+3. **性能优化调试**
+
+---
+
+## 📊 服务间通信设计
+
+### API调用关系
+```
+emotion-gateway
+├── emotion-user (用户认证)
+├── emotion-ai (AI对话)
+│ └── emotion-user (用户信息)
+├── emotion-record (情绪记录)
+│ └── emotion-user (用户验证)
+├── emotion-growth (成长课题)
+│ ├── emotion-user (用户信息)
+│ └── emotion-reward (奖励发放)
+├── emotion-explore (地图探索)
+│ └── emotion-user (用户信息)
+├── emotion-reward (成就奖励)
+│ └── emotion-user (用户信息)
+└── emotion-stats (统计分析)
+ ├── emotion-user (用户数据)
+ ├── emotion-ai (对话数据)
+ ├── emotion-record (情绪数据)
+ ├── emotion-growth (成长数据)
+ └── emotion-explore (探索数据)
+```
+
+### 消息队列设计
+```yaml
+# RocketMQ Topic设计
+topics:
+ - emotion-user-events # 用户事件
+ - emotion-conversation # 对话事件
+ - emotion-record-events # 情绪记录事件
+ - emotion-growth-events # 成长事件
+ - emotion-explore-events # 探索事件
+ - emotion-reward-events # 奖励事件
+ - emotion-stats-events # 统计事件
+```
+
+---
+
+*接下来将按照此架构设计逐步创建各个微服务模块*
diff --git a/UI设计实施指南.md b/UI设计实施指南.md
new file mode 100644
index 0000000..f985319
--- /dev/null
+++ b/UI设计实施指南.md
@@ -0,0 +1,352 @@
+# 情绪博物馆 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插件后通知我,我会立即开始设计工作!
diff --git a/backend/.idea/.gitignore b/backend/.idea/.gitignore
new file mode 100644
index 0000000..7bc07ec
--- /dev/null
+++ b/backend/.idea/.gitignore
@@ -0,0 +1,10 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Environment-dependent path to Maven home directory
+/mavenHomeManager.xml
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/backend/.idea/ApifoxUploaderProjectSetting.xml b/backend/.idea/ApifoxUploaderProjectSetting.xml
new file mode 100644
index 0000000..247c160
--- /dev/null
+++ b/backend/.idea/ApifoxUploaderProjectSetting.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/backend/.idea/compiler.xml b/backend/.idea/compiler.xml
new file mode 100644
index 0000000..57653b2
--- /dev/null
+++ b/backend/.idea/compiler.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/backend/.idea/dataSources.xml b/backend/.idea/dataSources.xml
new file mode 100644
index 0000000..ed76b43
--- /dev/null
+++ b/backend/.idea/dataSources.xml
@@ -0,0 +1,18 @@
+
+
+
+
+ mysql.8
+ true
+ true
+ com.mysql.cj.jdbc.Driver
+ jdbc:mysql://localhost:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT
+
+
+
+
+
+ $ProjectFileDir$
+
+
+
\ No newline at end of file
diff --git a/backend/.idea/encodings.xml b/backend/.idea/encodings.xml
new file mode 100644
index 0000000..21ec8eb
--- /dev/null
+++ b/backend/.idea/encodings.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/backend/.idea/jarRepositories.xml b/backend/.idea/jarRepositories.xml
new file mode 100644
index 0000000..fa3a980
--- /dev/null
+++ b/backend/.idea/jarRepositories.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/backend/.idea/material_theme_project_new.xml b/backend/.idea/material_theme_project_new.xml
new file mode 100644
index 0000000..995635b
--- /dev/null
+++ b/backend/.idea/material_theme_project_new.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/backend/.idea/misc.xml b/backend/.idea/misc.xml
new file mode 100644
index 0000000..67e1e61
--- /dev/null
+++ b/backend/.idea/misc.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/backend/.idea/vcs.xml b/backend/.idea/vcs.xml
new file mode 100644
index 0000000..6c0b863
--- /dev/null
+++ b/backend/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/backend/README.md b/backend/README.md
new file mode 100644
index 0000000..4216880
--- /dev/null
+++ b/backend/README.md
@@ -0,0 +1,364 @@
+# 情绪博物馆后端微服务
+
+基于Spring Cloud Alibaba 2022.0.0.0的微服务架构,为情绪博物馆iOS应用提供后端API服务。
+
+## 🏗️ 架构概览
+
+### 技术栈
+- **Spring Boot**: 3.0.2
+- **Spring Cloud**: 2022.0.0
+- **Spring Cloud Alibaba**: 2022.0.0.0
+- **JDK**: 17+
+- **MySQL**: 8.0+
+- **Redis**: 7.0+
+- **Nacos**: 2.2.0+
+
+### 微服务列表
+| 服务名称 | 端口 | 描述 | 状态 |
+|---------|------|------|------|
+| emotion-gateway | 8080 | API网关 | ✅ 已实现 |
+| emotion-user | 8081 | 用户服务 | ✅ 已实现 |
+| emotion-ai | 8082 | AI对话服务 | ✅ 已实现 |
+| emotion-record | 8083 | 情绪记录服务 | ✅ 已实现 |
+| emotion-growth | 8084 | 成长课题服务 | ✅ 已实现 |
+| emotion-explore | 8085 | 地图探索服务 | ✅ 已实现 |
+| emotion-reward | 8086 | 成就奖励服务 | ✅ 已实现 |
+| emotion-stats | 8087 | 统计分析服务 | ✅ 已实现 |
+
+## 🚀 快速开始
+
+### 环境要求
+- JDK 17+
+- Maven 3.6+
+- MySQL 8.0+
+- Redis 7.0+
+- Nacos 2.2.0+
+
+### 1. 环境准备
+
+#### 启动Nacos
+```bash
+# 下载Nacos 2.2.0
+wget https://github.com/alibaba/nacos/releases/download/2.2.0/nacos-server-2.2.0.tar.gz
+tar -xzf nacos-server-2.2.0.tar.gz
+cd nacos/bin
+
+# 单机模式启动
+sh startup.sh -m standalone
+
+# 访问控制台: http://localhost:8848/nacos
+# 默认用户名/密码: nacos/nacos
+```
+
+#### 启动MySQL
+```bash
+# 创建数据库
+mysql -u root -p
+CREATE DATABASE emotion_museum DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+
+# 导入数据库结构
+mysql -u root -p emotion_museum < ../mysql_deploy_database.sql
+```
+
+#### 启动Redis
+```bash
+redis-server
+```
+
+### 2. 配置Nacos
+
+访问 http://localhost:8848/nacos,创建以下配置:
+
+#### 命名空间
+- 命名空间ID: `emotion-dev`
+- 命名空间名: `情绪博物馆开发环境`
+
+#### 配置文件
+
+**common-mysql.yml**
+```yaml
+spring:
+ datasource:
+ driver-class-name: com.mysql.cj.jdbc.Driver
+ url: jdbc:mysql://localhost:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
+ username: root
+ password: 123456
+ hikari:
+ minimum-idle: 5
+ maximum-pool-size: 20
+ idle-timeout: 600000
+ max-lifetime: 1800000
+ connection-timeout: 30000
+
+mybatis-plus:
+ configuration:
+ map-underscore-to-camel-case: true
+ log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
+ global-config:
+ db-config:
+ id-type: assign_uuid
+ logic-delete-field: deleted
+ logic-delete-value: 1
+ logic-not-delete-value: 0
+```
+
+**common-redis.yml**
+```yaml
+spring:
+ data:
+ redis:
+ host: localhost
+ port: 6379
+ password:
+ database: 0
+ timeout: 3000ms
+ lettuce:
+ pool:
+ max-active: 20
+ max-idle: 10
+ min-idle: 5
+ max-wait: 3000ms
+```
+
+**coze-config.yml**
+```yaml
+coze:
+ base-url: https://api.coze.cn
+ api-key: your-coze-api-key
+ bot-id: your-bot-id
+ user-id: emotion-museum-user
+ timeout: 60
+ max-retries: 3
+ stream: false
+ model:
+ temperature: 0.7
+ max-tokens: 1000
+ top-p: 0.9
+ frequency-penalty: 0.0
+ presence-penalty: 0.0
+```
+
+### 3. 启动微服务
+
+#### 方式一:使用启动脚本(推荐)
+```bash
+# 启动所有服务
+./start-services.sh
+
+# 停止所有服务
+./stop-services.sh
+```
+
+#### 方式二:手动启动
+```bash
+# 编译项目
+mvn clean compile -DskipTests
+
+# 启动网关服务
+cd emotion-gateway
+mvn spring-boot:run &
+
+# 启动用户服务
+cd ../emotion-user
+mvn spring-boot:run &
+```
+
+### 4. 验证服务状态
+
+#### 方式一:使用测试脚本(推荐)
+```bash
+# 运行完整测试
+./test-services.sh
+```
+
+#### 方式二:手动验证
+```bash
+# 健康检查
+curl http://localhost:8080/actuator/health # 网关服务
+curl http://localhost:8081/actuator/health # 用户服务
+curl http://localhost:8082/actuator/health # AI对话服务
+curl http://localhost:8083/actuator/health # 情绪记录服务
+curl http://localhost:8084/actuator/health # 成长课题服务
+curl http://localhost:8085/actuator/health # 地图探索服务
+curl http://localhost:8086/actuator/health # 成就奖励服务
+curl http://localhost:8087/actuator/health # 统计分析服务
+
+# Nacos服务列表
+curl http://localhost:8848/nacos/v1/ns/service/list?pageNo=1&pageSize=10
+```
+
+## 📡 API文档
+
+### 用户服务API
+
+#### 用户注册
+```bash
+curl -X POST http://localhost:8080/api/user/register \
+ -H "Content-Type: application/json" \
+ -d '{
+ "account": "test_user",
+ "password": "123456",
+ "username": "测试用户",
+ "email": "test@example.com",
+ "phone": "13800138000",
+ "nickname": "小测试"
+ }'
+```
+
+#### 用户登录
+```bash
+curl -X POST http://localhost:8080/api/user/login \
+ -H "Content-Type: application/json" \
+ -d '{
+ "account": "test_user",
+ "password": "123456"
+ }'
+```
+
+#### 获取用户信息
+```bash
+curl -X GET http://localhost:8080/api/user/info/{userId} \
+ -H "Authorization: Bearer {token}"
+```
+
+### Coze AI服务API
+
+#### 健康检查
+```bash
+curl http://localhost:8080/api/ai/coze/health
+```
+
+#### 测试AI对话
+```bash
+curl -X POST http://localhost:8080/api/ai/coze/test/message \
+ -H "Content-Type: application/json" \
+ -d "message=你好,我今天感觉有点焦虑&userId=test_user"
+```
+
+#### 测试情绪分析
+```bash
+curl -X POST http://localhost:8080/api/ai/coze/test/emotion \
+ -H "Content-Type: application/json" \
+ -d "text=我今天心情很好,阳光明媚"
+```
+
+#### 测试完整对话流程
+```bash
+curl -X POST http://localhost:8080/api/ai/coze/test/full-chat \
+ -H "Content-Type: application/json" \
+ -d "userMessage=我最近工作压力很大,感觉很累&userId=test_user"
+```
+
+## 🔧 开发指南
+
+### 项目结构
+```
+backend/
+├── emotion-common/ # 公共模块
+│ ├── src/main/java/
+│ │ └── com/emotionmuseum/common/
+│ │ ├── entity/ # 基础实体
+│ │ ├── result/ # 统一响应
+│ │ └── util/ # 工具类
+├── emotion-gateway/ # 网关服务
+├── emotion-user/ # 用户服务
+├── emotion-ai/ # AI对话服务(待实现)
+├── emotion-record/ # 情绪记录服务(待实现)
+├── emotion-growth/ # 成长课题服务(待实现)
+├── emotion-explore/ # 地图探索服务(待实现)
+├── emotion-reward/ # 成就奖励服务(待实现)
+├── emotion-stats/ # 统计分析服务(待实现)
+├── start-services.sh # 启动脚本
+├── stop-services.sh # 停止脚本
+└── pom.xml # 父工程POM
+```
+
+### 添加新微服务
+
+1. **创建模块**
+```bash
+mkdir emotion-new-service
+cd emotion-new-service
+```
+
+2. **创建pom.xml**
+```xml
+
+ com.emotionmuseum
+ backend
+ 1.0.0
+
+emotion-new-service
+```
+
+3. **添加到父工程**
+```xml
+
+ emotion-new-service
+
+```
+
+4. **创建启动类**
+```java
+@SpringBootApplication(scanBasePackages = {"com.emotionmuseum"})
+@EnableDiscoveryClient
+@MapperScan("com.emotionmuseum.newservice.mapper")
+public class NewServiceApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(NewServiceApplication.class, args);
+ }
+}
+```
+
+## 🐛 故障排除
+
+### 常见问题
+
+1. **Nacos连接失败**
+ - 检查Nacos是否启动:`curl http://localhost:8848/nacos/v1/ns/operator/metrics`
+ - 检查命名空间是否创建
+ - 检查配置文件是否正确
+
+2. **数据库连接失败**
+ - 检查MySQL是否启动:`mysqladmin ping`
+ - 检查数据库是否创建
+ - 检查用户名密码是否正确
+
+3. **Redis连接失败**
+ - 检查Redis是否启动:`redis-cli ping`
+ - 检查端口是否正确
+
+4. **服务启动失败**
+ - 查看日志文件:`tail -f logs/emotion-*.log`
+ - 检查端口是否被占用:`lsof -i :8080`
+
+### 日志查看
+```bash
+# 查看所有服务日志
+tail -f logs/*.log
+
+# 查看特定服务日志
+tail -f logs/emotion-user.log
+```
+
+## 📊 监控
+
+### 健康检查端点
+- 网关: http://localhost:8080/actuator/health
+- 用户服务: http://localhost:8081/actuator/health
+
+### Prometheus指标
+- 网关: http://localhost:8080/actuator/prometheus
+- 用户服务: http://localhost:8081/actuator/prometheus
+
+## 🤝 贡献指南
+
+1. Fork项目
+2. 创建功能分支:`git checkout -b feature/new-feature`
+3. 提交更改:`git commit -am 'Add new feature'`
+4. 推送分支:`git push origin feature/new-feature`
+5. 提交Pull Request
+
+## 📄 许可证
+
+本项目采用MIT许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。
diff --git a/backend/dev-auto.sh b/backend/dev-auto.sh
new file mode 100755
index 0000000..9ba3cf8
--- /dev/null
+++ b/backend/dev-auto.sh
@@ -0,0 +1,209 @@
+#!/bin/bash
+
+# ============================================================================
+# 情绪博物馆自动化开发脚本
+# 自动编译、启动、监控文件变化并重启服务
+# ============================================================================
+
+# 颜色定义
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+# 配置
+PROJECT_ROOT=$(pwd)
+SERVICES=("emotion-gateway" "emotion-user" "emotion-ai" "emotion-record" "emotion-growth" "emotion-explore" "emotion-reward" "emotion-stats")
+PID_FILE="$PROJECT_ROOT/.dev-pids"
+LOG_DIR="$PROJECT_ROOT/dev-logs"
+
+# 创建日志目录
+mkdir -p "$LOG_DIR"
+
+echo -e "${BLUE}===========================================${NC}"
+echo -e "${BLUE}情绪博物馆自动化开发环境${NC}"
+echo -e "${BLUE}===========================================${NC}"
+
+# 清理函数
+cleanup() {
+ echo -e "\n${YELLOW}正在停止所有服务...${NC}"
+ if [ -f "$PID_FILE" ]; then
+ while read line; do
+ if [ ! -z "$line" ]; then
+ echo "停止进程: $line"
+ kill -9 $line 2>/dev/null || true
+ fi
+ done < "$PID_FILE"
+ rm -f "$PID_FILE"
+ fi
+
+ # 清理Maven进程
+ pkill -f "mvn.*spring-boot:run" 2>/dev/null || true
+
+ echo -e "${GREEN}✅ 清理完成${NC}"
+ exit 0
+}
+
+# 捕获退出信号
+trap cleanup SIGINT SIGTERM
+
+# 编译项目
+compile_project() {
+ echo -e "${YELLOW}🔄 编译项目...${NC}"
+ mvn clean compile -DskipTests -q
+ if [ $? -eq 0 ]; then
+ echo -e "${GREEN}✅ 编译成功${NC}"
+ return 0
+ else
+ echo -e "${RED}❌ 编译失败${NC}"
+ return 1
+ fi
+}
+
+# 启动单个服务
+start_service() {
+ local service=$1
+ local port=$2
+
+ echo -e "${BLUE}🚀 启动 $service (端口: $port)...${NC}"
+
+ cd "$PROJECT_ROOT/$service"
+
+ # 启动服务并获取PID
+ nohup mvn spring-boot:run -Dspring-boot.run.jvmArguments="-Dserver.port=$port" \
+ > "$LOG_DIR/$service.log" 2>&1 &
+
+ local pid=$!
+ echo "$pid" >> "$PID_FILE"
+
+ echo -e "${GREEN}✅ $service 已启动 (PID: $pid)${NC}"
+
+ cd "$PROJECT_ROOT"
+}
+
+# 检查服务健康状态
+check_service_health() {
+ local service=$1
+ local port=$2
+ local max_attempts=30
+ local attempt=0
+
+ echo -e "${YELLOW}⏳ 等待 $service 启动...${NC}"
+
+ while [ $attempt -lt $max_attempts ]; do
+ if curl -s "http://localhost:$port/actuator/health" > /dev/null 2>&1; then
+ echo -e "${GREEN}✅ $service 健康检查通过${NC}"
+ return 0
+ fi
+
+ attempt=$((attempt + 1))
+ sleep 2
+ echo -n "."
+ done
+
+ echo -e "\n${RED}❌ $service 健康检查失败${NC}"
+ return 1
+}
+
+# 监控文件变化
+monitor_changes() {
+ echo -e "${BLUE}👀 开始监控文件变化...${NC}"
+ echo -e "${YELLOW}按 Ctrl+C 停止监控${NC}"
+
+ # 使用fswatch监控文件变化 (需要安装: brew install fswatch)
+ if command -v fswatch > /dev/null; then
+ fswatch -o -r --exclude="target" --exclude=".git" --exclude="node_modules" \
+ --include=".*\\.java$" --include=".*\\.yml$" --include=".*\\.xml$" \
+ "$PROJECT_ROOT" | while read f; do
+ echo -e "${YELLOW}🔄 检测到文件变化,重新编译...${NC}"
+ if compile_project; then
+ echo -e "${GREEN}📝 代码已更新,DevTools将自动重启服务${NC}"
+ fi
+ done
+ else
+ # 如果没有fswatch,使用简单的循环检查
+ echo -e "${YELLOW}⚠️ 建议安装 fswatch 以获得更好的文件监控体验:brew install fswatch${NC}"
+ while true; do
+ sleep 5
+ # 简单的时间戳检查(这里可以根据需要扩展)
+ echo -e "${BLUE}💓 服务运行中... ($(date))${NC}"
+ done
+ fi
+}
+
+# 显示服务状态
+show_status() {
+ echo -e "${BLUE}===========================================${NC}"
+ echo -e "${BLUE}服务状态${NC}"
+ echo -e "${BLUE}===========================================${NC}"
+
+ local ports=(8080 8081 8082 8083 8084 8085 8086 8087)
+ local i=0
+
+ for service in "${SERVICES[@]}"; do
+ local port=${ports[$i]}
+ if curl -s "http://localhost:$port/actuator/health" > /dev/null 2>&1; then
+ echo -e "${GREEN}✅ $service (端口: $port) - 运行中${NC}"
+ else
+ echo -e "${RED}❌ $service (端口: $port) - 停止${NC}"
+ fi
+ i=$((i + 1))
+ done
+
+ echo -e "${BLUE}===========================================${NC}"
+ echo -e "${YELLOW}📋 日志文件位置: $LOG_DIR/${NC}"
+ echo -e "${YELLOW}🌐 API网关: http://localhost:9000${NC}"
+ echo -e "${YELLOW}📚 API文档: http://localhost:9000/doc.html${NC}"
+ echo -e "${BLUE}===========================================${NC}"
+}
+
+# 主函数
+main() {
+ # 清理之前的进程
+ cleanup 2>/dev/null || true
+
+ # 编译项目
+ if ! compile_project; then
+ echo -e "${RED}❌ 编译失败,请检查代码错误${NC}"
+ exit 1
+ fi
+
+ # 启动服务
+ echo -e "${BLUE}🚀 启动所有微服务...${NC}"
+
+ local ports=(9000 9001 9002 9003 9004 9005 9006 9007)
+ local i=0
+
+ for service in "${SERVICES[@]}"; do
+ start_service "$service" "${ports[$i]}"
+ sleep 3 # 给服务一些启动时间
+ i=$((i + 1))
+ done
+
+ # 等待所有服务启动
+ sleep 10
+
+ # 显示状态
+ show_status
+
+ # 开始监控
+ monitor_changes
+}
+
+# 检查参数
+case "${1:-}" in
+ "status")
+ show_status
+ ;;
+ "stop")
+ cleanup
+ ;;
+ "logs")
+ echo -e "${BLUE}📋 实时日志 (按 Ctrl+C 退出):${NC}"
+ tail -f "$LOG_DIR"/*.log
+ ;;
+ *)
+ main
+ ;;
+esac
\ No newline at end of file
diff --git a/backend/dev-start.sh b/backend/dev-start.sh
new file mode 100755
index 0000000..663bf7a
--- /dev/null
+++ b/backend/dev-start.sh
@@ -0,0 +1,129 @@
+#!/bin/bash
+
+# 情绪博物馆微服务开发启动脚本
+# 适用于本地开发环境,可以直接看到日志输出
+# 作者: emotion-museum
+# 日期: 2025-07-13
+
+# 颜色定义
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+PURPLE='\033[0;35m'
+CYAN='\033[0;36m'
+NC='\033[0m' # No Color
+
+echo -e "${BLUE}=========================================="
+echo -e "情绪博物馆微服务开发启动脚本"
+echo -e "适用于本地开发环境 - 实时日志输出"
+echo -e "==========================================${NC}"
+
+# 检查Java环境
+if ! command -v java &> /dev/null; then
+ echo -e "${RED}❌ 错误: 未找到Java环境,请安装JDK 17+${NC}"
+ exit 1
+fi
+
+# 检查Maven环境
+if ! command -v mvn &> /dev/null; then
+ echo -e "${RED}❌ 错误: 未找到Maven环境,请安装Maven 3.6+${NC}"
+ exit 1
+fi
+
+echo -e "${GREEN}✅ Java环境检查通过${NC}"
+echo -e "${GREEN}✅ Maven环境检查通过${NC}"
+
+# 显示菜单
+show_menu() {
+ echo ""
+ echo -e "${CYAN}请选择要启动的服务:${NC}"
+ echo -e "${YELLOW}1.${NC} 启动网关服务 (emotion-gateway:9000)"
+ echo -e "${YELLOW}2.${NC} 启动用户服务 (emotion-user:9001)"
+ echo -e "${YELLOW}3.${NC} 启动AI对话服务 (emotion-ai:9002)"
+ echo -e "${YELLOW}4.${NC} 启动情绪记录服务 (emotion-record:9003)"
+ echo -e "${YELLOW}5.${NC} 启动成长课题服务 (emotion-growth:9004)"
+ echo -e "${YELLOW}6.${NC} 启动地图探索服务 (emotion-explore:9005)"
+ echo -e "${YELLOW}7.${NC} 启动成就奖励服务 (emotion-reward:9006)"
+ echo -e "${YELLOW}8.${NC} 启动统计分析服务 (emotion-stats:9007)"
+ echo -e "${YELLOW}9.${NC} 编译所有项目"
+ echo -e "${YELLOW}0.${NC} 退出"
+ echo ""
+}
+
+# 编译项目
+compile_project() {
+ echo -e "${BLUE}🔨 开始编译项目...${NC}"
+ mvn clean compile -DskipTests
+
+ if [ $? -eq 0 ]; then
+ echo -e "${GREEN}✅ 项目编译成功!${NC}"
+ return 0
+ else
+ echo -e "${RED}❌ 项目编译失败!${NC}"
+ return 1
+ fi
+}
+
+# 启动单个服务
+start_service() {
+ local service_name=$1
+ local service_port=$2
+ local service_desc=$3
+
+ echo -e "${BLUE}🚀 启动 ${service_desc} (${service_name}:${service_port})...${NC}"
+ echo -e "${YELLOW}💡 提示: 按 Ctrl+C 停止服务${NC}"
+ echo -e "${PURPLE}📋 日志输出开始:${NC}"
+ echo "----------------------------------------"
+
+ cd $service_name
+ mvn spring-boot:run -Dspring-boot.run.jvmArguments="-Dserver.port=$service_port"
+ cd ..
+}
+
+# 主循环
+while true; do
+ show_menu
+ read -p "请输入选择 (0-9): " choice
+
+ case $choice in
+ 1)
+ start_service "emotion-gateway" 9000 "网关服务"
+ ;;
+ 2)
+ start_service "emotion-user" 9001 "用户服务"
+ ;;
+ 3)
+ start_service "emotion-ai" 9002 "AI对话服务"
+ ;;
+ 4)
+ start_service "emotion-record" 9003 "情绪记录服务"
+ ;;
+ 5)
+ start_service "emotion-growth" 9004 "成长课题服务"
+ ;;
+ 6)
+ start_service "emotion-explore" 9005 "地图探索服务"
+ ;;
+ 7)
+ start_service "emotion-reward" 9006 "成就奖励服务"
+ ;;
+ 8)
+ start_service "emotion-stats" 9007 "统计分析服务"
+ ;;
+ 9)
+ compile_project
+ ;;
+ 0)
+ echo -e "${GREEN}👋 退出开发启动脚本${NC}"
+ exit 0
+ ;;
+ *)
+ echo -e "${RED}❌ 无效选择,请输入 0-9${NC}"
+ ;;
+ esac
+
+ echo ""
+ echo -e "${YELLOW}按任意键继续...${NC}"
+ read -n 1
+done
diff --git a/backend/docker-compose-local.yml b/backend/docker-compose-local.yml
new file mode 100644
index 0000000..d5d37ce
--- /dev/null
+++ b/backend/docker-compose-local.yml
@@ -0,0 +1,259 @@
+version: '3.8'
+
+services:
+ # MySQL数据库
+ mysql:
+ image: mysql:8.0
+ container_name: emotion-mysql
+ restart: always
+ environment:
+ MYSQL_ROOT_PASSWORD: 123456
+ MYSQL_DATABASE: emotion_museum
+ MYSQL_CHARACTER_SET_SERVER: utf8mb4
+ MYSQL_COLLATION_SERVER: utf8mb4_unicode_ci
+ ports:
+ - "3306:3306"
+ volumes:
+ - mysql_data:/var/lib/mysql
+ - ./mysql_emotion_museum_final.sql:/docker-entrypoint-initdb.d/init.sql
+ command: --default-authentication-plugin=mysql_native_password
+ networks:
+ - emotion-network
+
+ # Redis缓存
+ redis:
+ image: redis:7-alpine
+ container_name: emotion-redis
+ restart: always
+ ports:
+ - "6379:6379"
+ volumes:
+ - redis_data:/data
+ networks:
+ - emotion-network
+
+ # Nacos注册中心
+ nacos:
+ image: nacos/nacos-server:v2.2.3
+ container_name: emotion-nacos
+ restart: always
+ environment:
+ MODE: standalone
+ SPRING_DATASOURCE_PLATFORM: mysql
+ MYSQL_SERVICE_HOST: mysql
+ MYSQL_SERVICE_PORT: 3306
+ MYSQL_SERVICE_DB_NAME: nacos
+ MYSQL_SERVICE_USER: root
+ MYSQL_SERVICE_PASSWORD: 123456
+ MYSQL_SERVICE_DB_PARAM: characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true
+ ports:
+ - "8848:8848"
+ - "9848:9848"
+ depends_on:
+ - mysql
+ volumes:
+ - nacos_logs:/home/nacos/logs
+ networks:
+ - emotion-network
+
+ # 用户服务
+ emotion-user:
+ build:
+ context: .
+ dockerfile: emotion-user/Dockerfile
+ container_name: emotion-user
+ restart: always
+ ports:
+ - "19001:19001"
+ environment:
+ SPRING_PROFILES_ACTIVE: local
+ SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
+ SPRING_DATASOURCE_USERNAME: root
+ SPRING_DATASOURCE_PASSWORD: 123456
+ SPRING_DATA_REDIS_HOST: redis
+ SPRING_CLOUD_NACOS_DISCOVERY_SERVER_ADDR: nacos:8848
+ SPRING_CLOUD_NACOS_CONFIG_SERVER_ADDR: nacos:8848
+ depends_on:
+ - mysql
+ - redis
+ - nacos
+ networks:
+ - emotion-network
+
+ # AI服务
+ emotion-ai:
+ build:
+ context: .
+ dockerfile: emotion-ai/Dockerfile
+ container_name: emotion-ai
+ restart: always
+ ports:
+ - "19002:19002"
+ environment:
+ SPRING_PROFILES_ACTIVE: local
+ SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
+ SPRING_DATASOURCE_USERNAME: root
+ SPRING_DATASOURCE_PASSWORD: 123456
+ SPRING_DATA_REDIS_HOST: redis
+ SPRING_CLOUD_NACOS_DISCOVERY_SERVER_ADDR: nacos:8848
+ SPRING_CLOUD_NACOS_CONFIG_SERVER_ADDR: nacos:8848
+ depends_on:
+ - mysql
+ - redis
+ - nacos
+ networks:
+ - emotion-network
+
+ # 记录服务
+ emotion-record:
+ build:
+ context: .
+ dockerfile: emotion-record/Dockerfile
+ container_name: emotion-record
+ restart: always
+ ports:
+ - "19003:19003"
+ environment:
+ SPRING_PROFILES_ACTIVE: local
+ SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
+ SPRING_DATASOURCE_USERNAME: root
+ SPRING_DATASOURCE_PASSWORD: 123456
+ SPRING_DATA_REDIS_HOST: redis
+ SPRING_CLOUD_NACOS_DISCOVERY_SERVER_ADDR: nacos:8848
+ SPRING_CLOUD_NACOS_CONFIG_SERVER_ADDR: nacos:8848
+ depends_on:
+ - mysql
+ - redis
+ - nacos
+ networks:
+ - emotion-network
+
+ # 成长服务
+ emotion-growth:
+ build:
+ context: .
+ dockerfile: emotion-growth/Dockerfile
+ container_name: emotion-growth
+ restart: always
+ ports:
+ - "19004:19004"
+ environment:
+ SPRING_PROFILES_ACTIVE: local
+ SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
+ SPRING_DATASOURCE_USERNAME: root
+ SPRING_DATASOURCE_PASSWORD: 123456
+ SPRING_DATA_REDIS_HOST: redis
+ SPRING_CLOUD_NACOS_DISCOVERY_SERVER_ADDR: nacos:8848
+ SPRING_CLOUD_NACOS_CONFIG_SERVER_ADDR: nacos:8848
+ depends_on:
+ - mysql
+ - redis
+ - nacos
+ networks:
+ - emotion-network
+
+ # 探索服务
+ emotion-explore:
+ build:
+ context: .
+ dockerfile: emotion-explore/Dockerfile
+ container_name: emotion-explore
+ restart: always
+ ports:
+ - "19005:19005"
+ environment:
+ SPRING_PROFILES_ACTIVE: local
+ SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
+ SPRING_DATASOURCE_USERNAME: root
+ SPRING_DATASOURCE_PASSWORD: 123456
+ SPRING_DATA_REDIS_HOST: redis
+ SPRING_CLOUD_NACOS_DISCOVERY_SERVER_ADDR: nacos:8848
+ SPRING_CLOUD_NACOS_CONFIG_SERVER_ADDR: nacos:8848
+ depends_on:
+ - mysql
+ - redis
+ - nacos
+ networks:
+ - emotion-network
+
+ # 奖励服务
+ emotion-reward:
+ build:
+ context: .
+ dockerfile: emotion-reward/Dockerfile
+ container_name: emotion-reward
+ restart: always
+ ports:
+ - "19006:19006"
+ environment:
+ SPRING_PROFILES_ACTIVE: local
+ SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
+ SPRING_DATASOURCE_USERNAME: root
+ SPRING_DATASOURCE_PASSWORD: 123456
+ SPRING_DATA_REDIS_HOST: redis
+ SPRING_CLOUD_NACOS_DISCOVERY_SERVER_ADDR: nacos:8848
+ SPRING_CLOUD_NACOS_CONFIG_SERVER_ADDR: nacos:8848
+ depends_on:
+ - mysql
+ - redis
+ - nacos
+ networks:
+ - emotion-network
+
+ # 统计服务
+ emotion-stats:
+ build:
+ context: .
+ dockerfile: emotion-stats/Dockerfile
+ container_name: emotion-stats
+ restart: always
+ ports:
+ - "19007:19007"
+ environment:
+ SPRING_PROFILES_ACTIVE: local
+ SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
+ SPRING_DATASOURCE_USERNAME: root
+ SPRING_DATASOURCE_PASSWORD: 123456
+ SPRING_DATA_REDIS_HOST: redis
+ SPRING_CLOUD_NACOS_DISCOVERY_SERVER_ADDR: nacos:8848
+ SPRING_CLOUD_NACOS_CONFIG_SERVER_ADDR: nacos:8848
+ depends_on:
+ - mysql
+ - redis
+ - nacos
+ networks:
+ - emotion-network
+
+ # 网关服务
+ emotion-gateway:
+ build:
+ context: .
+ dockerfile: emotion-gateway/Dockerfile
+ container_name: emotion-gateway
+ restart: always
+ ports:
+ - "19000:19000"
+ environment:
+ SPRING_PROFILES_ACTIVE: local
+ SPRING_DATA_REDIS_HOST: redis
+ SPRING_CLOUD_NACOS_DISCOVERY_SERVER_ADDR: nacos:8848
+ SPRING_CLOUD_NACOS_CONFIG_SERVER_ADDR: nacos:8848
+ depends_on:
+ - emotion-user
+ - emotion-ai
+ - emotion-record
+ - emotion-growth
+ - emotion-explore
+ - emotion-reward
+ - emotion-stats
+ networks:
+ - emotion-network
+
+volumes:
+ mysql_data:
+ redis_data:
+ nacos_logs:
+
+networks:
+ emotion-network:
+ driver: bridge
diff --git a/backend/emotion-ai/Dockerfile b/backend/emotion-ai/Dockerfile
new file mode 100644
index 0000000..1dd80b5
--- /dev/null
+++ b/backend/emotion-ai/Dockerfile
@@ -0,0 +1,48 @@
+# AI服务Dockerfile
+FROM openjdk:17-jdk-alpine
+
+# 设置工作目录
+WORKDIR /app
+
+# 安装必要的工具
+RUN apk add --no-cache curl tzdata && \
+ cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
+ echo "Asia/Shanghai" > /etc/timezone
+
+# 复制Maven构建文件
+COPY pom.xml ./
+COPY emotion-common ./emotion-common
+COPY emotion-ai ./emotion-ai
+
+# 安装Maven
+RUN apk add --no-cache maven
+
+# 构建应用
+RUN mvn clean package -DskipTests -pl emotion-ai -am
+
+# 创建运行用户
+RUN addgroup -g 1000 emotion && \
+ adduser -D -s /bin/sh -u 1000 -G emotion emotion
+
+# 复制jar文件
+RUN cp emotion-ai/target/emotion-ai-*.jar app.jar
+
+# 设置文件权限
+RUN chown -R emotion:emotion /app
+
+# 切换到非root用户
+USER emotion
+
+# 健康检查
+HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
+ CMD curl -f http://localhost:19002/actuator/health || exit 1
+
+# 暴露端口
+EXPOSE 19002
+
+# 启动命令
+ENTRYPOINT ["java", "-jar", \
+ "-Xms512m", "-Xmx1024m", \
+ "-Djava.security.egd=file:/dev/./urandom", \
+ "-Dspring.profiles.active=local", \
+ "app.jar"]
diff --git a/backend/emotion-ai/pom.xml b/backend/emotion-ai/pom.xml
new file mode 100644
index 0000000..860560d
--- /dev/null
+++ b/backend/emotion-ai/pom.xml
@@ -0,0 +1,101 @@
+
+
+ 4.0.0
+
+
+ com.emotionmuseum
+ backend
+ 1.0.0
+
+
+ emotion-ai
+ emotion-ai
+ AI对话服务
+
+
+
+
+ com.emotionmuseum
+ emotion-common
+
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-bootstrap
+
+
+
+ com.alibaba.cloud
+ spring-cloud-starter-alibaba-nacos-discovery
+
+
+
+ com.alibaba.cloud
+ spring-cloud-starter-alibaba-nacos-config
+
+
+
+
+ org.springframework.boot
+ spring-boot-devtools
+ runtime
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-openfeign
+
+
+
+
+ mysql
+ mysql-connector-java
+
+
+
+
+ com.alibaba
+ druid-spring-boot-starter
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+
+
+ io.micrometer
+ micrometer-registry-prometheus
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
\ No newline at end of file
diff --git a/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/AiApplication.java b/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/AiApplication.java
new file mode 100644
index 0000000..35adeb2
--- /dev/null
+++ b/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/AiApplication.java
@@ -0,0 +1,24 @@
+package com.emotionmuseum.ai;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+
+/**
+ * AI对话服务启动类
+ *
+ * @author emotion-museum
+ * @since 2025-07-12
+ */
+@SpringBootApplication(scanBasePackages = {"com.emotionmuseum"})
+@EnableDiscoveryClient
+@EnableFeignClients
+@MapperScan("com.emotionmuseum.ai.mapper")
+public class AiApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(AiApplication.class, args);
+ }
+}
diff --git a/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/config/AiConfig.java b/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/config/AiConfig.java
new file mode 100644
index 0000000..1078536
--- /dev/null
+++ b/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/config/AiConfig.java
@@ -0,0 +1,35 @@
+package com.emotionmuseum.ai.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.reactive.function.client.WebClient;
+
+/**
+ * AI配置类
+ * 配置Coze平台HTTP客户端
+ *
+ * @author emotion-museum
+ * @since 2025-07-12
+ */
+@Configuration
+public class AiConfig {
+
+ @Value("${coze.base-url:https://api.coze.cn}")
+ private String baseUrl;
+
+ @Value("${coze.token}")
+ private String token;
+
+ /**
+ * 配置Coze API客户端
+ */
+ @Bean
+ public WebClient cozeWebClient() {
+ return WebClient.builder()
+ .baseUrl(baseUrl)
+ .defaultHeader("Authorization", "Bearer " + token)
+ .defaultHeader("Content-Type", "application/json")
+ .build();
+ }
+}
diff --git a/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/config/FeatureConfig.java b/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/config/FeatureConfig.java
new file mode 100644
index 0000000..bec1a14
--- /dev/null
+++ b/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/config/FeatureConfig.java
@@ -0,0 +1,53 @@
+package com.emotionmuseum.ai.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 功能开关配置
+ *
+ * @author emotion-museum
+ * @since 2025-07-13
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "features")
+public class FeatureConfig {
+
+ /**
+ * 情绪分析功能配置
+ */
+ private EmotionAnalysis emotionAnalysis = new EmotionAnalysis();
+
+ /**
+ * 聊天功能配置
+ */
+ private Chat chat = new Chat();
+
+ @Data
+ public static class EmotionAnalysis {
+ /**
+ * 是否启用情绪分析功能
+ */
+ private boolean enabled = false;
+
+ /**
+ * 是否自动进行情绪分析
+ */
+ private boolean autoAnalyze = false;
+ }
+
+ @Data
+ public static class Chat {
+ /**
+ * 是否启用聊天功能
+ */
+ private boolean enabled = true;
+
+ /**
+ * 是否启用流式聊天
+ */
+ private boolean stream = false;
+ }
+}
diff --git a/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/controller/AiChatController.java b/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/controller/AiChatController.java
new file mode 100644
index 0000000..f4a419f
--- /dev/null
+++ b/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/controller/AiChatController.java
@@ -0,0 +1,234 @@
+package com.emotionmuseum.ai.controller;
+
+import com.emotionmuseum.ai.dto.*;
+import com.emotionmuseum.ai.entity.Conversation;
+import com.emotionmuseum.ai.entity.Message;
+import com.emotionmuseum.ai.service.AiChatService;
+import com.emotionmuseum.ai.service.ConversationDbService;
+import com.emotionmuseum.common.dto.PageQuery;
+import com.emotionmuseum.common.result.Result;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import jakarta.validation.Valid;
+import java.util.List;
+
+/**
+ * AI聊天控制器
+ *
+ * @author emotion-museum
+ * @since 2025-07-12
+ */
+@Slf4j
+@RestController
+@RequestMapping("/api/ai/chat")
+@RequiredArgsConstructor
+@Validated
+@Tag(name = "AI聊天", description = "AI聊天相关接口")
+public class AiChatController {
+
+ private final AiChatService aiChatService;
+ private final ConversationDbService conversationDbService;
+
+ @Operation(summary = "创建会话")
+ @PostMapping("/conversation/create")
+ public Result createConversation(
+ @Valid @RequestBody CreateConversationRequest request) {
+ log.info("收到创建会话请求: userId={}, title={}", request.getUserId(), request.getTitle());
+
+ CreateConversationResponse response = aiChatService.createConversation(request);
+ return Result.success(response);
+ }
+
+ @Operation(summary = "发送聊天消息")
+ @PostMapping("/send")
+ public Result sendMessage(@Valid @RequestBody ChatRequest request) {
+ log.info("收到聊天请求: userId={}, message={}", request.getUserId(), request.getMessage());
+
+ ChatResponse response = aiChatService.chat(request);
+ return Result.success(response);
+ }
+
+ @Operation(summary = "情绪分析")
+ @PostMapping("/emotion/analyze")
+ public Result analyzeEmotion(@Valid @RequestBody EmotionAnalysisRequest request) {
+ log.info("收到情绪分析请求: userId={}, text={}", request.getUserId(), request.getText());
+
+ EmotionAnalysisResponse response = aiChatService.analyzeEmotion(request);
+ return Result.success(response);
+ }
+
+ @Operation(summary = "流式聊天")
+ @PostMapping("/stream")
+ public Result streamChat(@Valid @RequestBody ChatRequest request) {
+ log.info("收到流式聊天请求: userId={}", request.getUserId());
+
+ String response = aiChatService.streamChat(request);
+ return Result.success(response);
+ }
+
+ @Operation(summary = "健康检查")
+ @GetMapping("/health")
+ public Result healthCheck() {
+ log.info("AI服务健康检查");
+
+ boolean isHealthy = aiChatService.healthCheck();
+ return Result.success(isHealthy);
+ }
+
+ @Operation(summary = "获取AI服务信息")
+ @GetMapping("/info")
+ public Result