feat: 项目初始化及当前全部内容提交

This commit is contained in:
2025-07-15 17:37:50 +08:00
parent ec817067f1
commit e78f192d34
622 changed files with 75174 additions and 383 deletions
+18
View File
@@ -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
+38
View File
@@ -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
+10
View File
@@ -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
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ApifoxUploaderProjectSetting">
<option name="apiAccessToken" value="APS-lcuiwXuAQ9Ef4NCzeBgangmOvLg2KEjr" />
</component>
</project>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AugmentWebviewStateStore">
<option name="stateMap">
<map>
<entry key="CHAT_STATE" value="{&quot;currentConversationId&quot;:&quot;__NEW_AGENT__&quot;,&quot;conversations&quot;:{&quot;036b95bf-0e85-468f-8841-83bb6c16c707&quot;:{&quot;id&quot;:&quot;036b95bf-0e85-468f-8841-83bb6c16c707&quot;,&quot;createdAtIso&quot;:&quot;2025-07-15T08:46:03.668Z&quot;,&quot;lastInteractedAtIso&quot;:&quot;2025-07-15T08:46:03.668Z&quot;,&quot;chatHistory&quot;:[],&quot;feedbackStates&quot;:{},&quot;toolUseStates&quot;:{},&quot;draftExchange&quot;:{&quot;request_message&quot;:&quot;&quot;,&quot;rich_text_json_repr&quot;:{&quot;type&quot;:&quot;doc&quot;,&quot;content&quot;:[{&quot;type&quot;:&quot;paragraph&quot;}]},&quot;mentioned_items&quot;:[],&quot;status&quot;:&quot;draft&quot;},&quot;requestIds&quot;:[],&quot;isPinned&quot;:false,&quot;isShareable&quot;:false,&quot;extraData&quot;:{&quot;hasDirtyEdits&quot;:false},&quot;personaType&quot;:0},&quot;__NEW_AGENT__&quot;:{&quot;id&quot;:&quot;__NEW_AGENT__&quot;,&quot;createdAtIso&quot;:&quot;2025-07-15T08:46:03.764Z&quot;,&quot;lastInteractedAtIso&quot;:&quot;2025-07-15T08:46:03.816Z&quot;,&quot;chatHistory&quot;:[],&quot;feedbackStates&quot;:{},&quot;toolUseStates&quot;:{},&quot;draftExchange&quot;:{&quot;request_message&quot;:&quot;&quot;,&quot;rich_text_json_repr&quot;:{&quot;type&quot;:&quot;doc&quot;,&quot;content&quot;:[{&quot;type&quot;:&quot;paragraph&quot;}]},&quot;mentioned_items&quot;:[],&quot;status&quot;:&quot;draft&quot;},&quot;requestIds&quot;:[],&quot;isPinned&quot;:false,&quot;isShareable&quot;:false,&quot;extraData&quot;:{&quot;hasDirtyEdits&quot;:false,&quot;isAgentConversation&quot;:true,&quot;baselineTimestamp&quot;:0},&quot;personaType&quot;:0,&quot;rootTaskUuid&quot;:&quot;91b4236a-b9d0-4964-9424-1adb7d5a1c45&quot;}},&quot;agentExecutionMode&quot;:&quot;auto&quot;,&quot;isPanelCollapsed&quot;:true,&quot;displayedAnnouncements&quot;:[],&quot;sortConversationsBy&quot;:&quot;lastMessageTimestamp&quot;,&quot;sendMode&quot;:&quot;send&quot;}" />
</map>
</option>
</component>
</project>
+29
View File
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/server/backend/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/server/backend/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/server/common/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/server/customer/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/server/customer/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/server/service/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/server/service/src/main/resources" type="java-resource" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot:2.7.18" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-autoconfigure:2.7.18" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-autoconfigure:3.5.0" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot:3.5.0" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-autoconfigure:3.2.7" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot:3.2.7" level="project" />
<orderEntry type="library" name="Maven: org.projectlombok:lombok:1.18.30" level="project" />
<orderEntry type="library" name="Maven: com.baomidou:mybatis-plus-annotation:3.5.4" level="project" />
<orderEntry type="library" name="Maven: com.baomidou:mybatis-plus-core:3.5.4" level="project" />
<orderEntry type="library" name="Maven: com.baomidou:mybatis-plus-extension:3.5.4" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-context:6.1.1" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-core:6.1.1" level="project" />
</component>
</module>
+46
View File
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile default="true" name="Default" enabled="true" />
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
</profile>
<profile name="Annotation profile for backend" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<processorPath useClasspath="false">
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.30/lombok-1.18.30.jar" />
<entry name="$MAVEN_REPOSITORY$/org/mapstruct/mapstruct-processor/1.5.3.Final/mapstruct-processor-1.5.3.Final.jar" />
<entry name="$MAVEN_REPOSITORY$/org/mapstruct/mapstruct/1.5.3.Final/mapstruct-1.5.3.Final.jar" />
</processorPath>
<module name="emotion-stats" />
<module name="emotion-ai" />
<module name="emotion-reward" />
<module name="emotion-gateway" />
<module name="emotion-user" />
<module name="emotion-explore" />
<module name="emotion-common" />
<module name="emotion-growth" />
<module name="emotion-record" />
</profile>
</annotationProcessing>
<bytecodeTargetLevel>
<module name="common" target="17" />
<module name="customer" target="17" />
<module name="emotion-museum-server" target="17" />
<module name="server" target="1.5" />
<module name="service" target="17" />
</bytecodeTargetLevel>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="backend" options="" />
<module name="common" options="" />
<module name="customer" options="" />
</option>
</component>
</project>
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="@localhost" uuid="5d3e10c1-409f-42d2-98e4-fb21f0c9102b">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://localhost:3306</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>
+35
View File
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/backend/emotion-ai/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-ai/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-common/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-common/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-explore/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-explore/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-gateway/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-gateway/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-growth/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-growth/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-record/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-record/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-reward/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-reward/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-stats/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-stats/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-user/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-user/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/backend/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/backend/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/common/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/common/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/customer/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/customer/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/service/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/service/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/src/main/resources" charset="UTF-8" />
</component>
</project>
+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="aliyun-maven" />
<option name="name" value="Aliyun Maven Repository" />
<option name="url" value="https://maven.aliyun.com/repository/public" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://maven.aliyun.com/nexus/content/groups/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
</component>
</project>
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MaterialThemeProjectNewConfig">
<option name="metadata">
<MTProjectMetadataState>
<option name="migrated" value="true" />
<option name="pristineConfig" value="false" />
<option name="userId" value="-37f43507:191123ac8a3:-7ffe" />
</MTProjectMetadataState>
</option>
</component>
</project>
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/server/pom.xml" />
<option value="$PROJECT_DIR$/backend/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/EmotionMuseum.iml" filepath="$PROJECT_DIR$/.idea/EmotionMuseum.iml" />
</modules>
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/backend/mysql_emotion_museum_final.sql" dialect="MySQL" />
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
+4
View File
@@ -0,0 +1,4 @@
{
"java.compile.nullAnalysis.mode": "automatic",
"dbcode.connections": []
}
+415
View File
@@ -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<T>`包装所有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集成
---
以上是情绪博物馆全栈项目的完整开发指南,请在开发过程中严格遵循相关规范,确保代码质量和项目的可维护性。
+342
View File
@@ -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/`
**🎉 恭喜!您的情绪博物馆项目已成功部署到自定义目录结构!**
+313
View File
@@ -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 <repository-url>
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. 联系技术支持团队
---
**部署完成后,请及时修改默认密码和配置文件中的敏感信息!**
+256
View File
@@ -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 <repository-url>
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 <service>
```
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 <container-name>`
## 安全建议
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. 部署步骤和配置
---
**注意**: 请确保在生产环境中修改默认密码和配置,并定期进行安全更新。
+371
View File
@@ -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
---
**🎉 恭喜!您已成功部署情绪博物馆项目!**
**⚠️ 重要提醒:部署完成后请及时修改默认密码和敏感配置!**
@@ -1,11 +0,0 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -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
}
}
-88
View File
@@ -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<Item>
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)
}
@@ -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 = "<group>";
};
2F1ADECB2DE4903B0029490F /* EmotionMuseumTests */ = {
2FB3451C2DFBE273001A8A67 /* EmotionMuseumTests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = EmotionMuseumTests;
sourceTree = "<group>";
};
2F1ADED52DE4903B0029490F /* EmotionMuseumUITests */ = {
2FB345262DFBE273001A8A67 /* EmotionMuseumUITests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = EmotionMuseumUITests;
sourceTree = "<group>";
@@ -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 = "<group>";
};
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 = "<group>";
@@ -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 */;
}
@@ -1,10 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict>
<dict/>
</plist>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildLocationStyle</key>
<string>UseAppPreferences</string>
<key>CustomBuildLocationType</key>
<string>RelativeToDerivedData</string>
<key>DerivedDataLocationStyle</key>
<string>Default</string>
<key>ShowSharedSchemesAutomaticallyEnabled</key>
<true/>
</dict>
</plist>
@@ -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
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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())
}
@@ -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)
}
}
}
@@ -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 ["提升觉知", "增强灵性连接", "获得内在平静", "超越自我"]
}
}
}
File diff suppressed because it is too large Load Diff
@@ -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()
}
}
@@ -2,7 +2,7 @@
// Persistence.swift
// EmotionMuseum
//
// Created by on 2025/5/26.
// Created by on 2025/6/13.
//
import CoreData
+11
View File
@@ -0,0 +1,11 @@
platform :ios, '14.0'
target 'EmotionMuseum' do
use_frameworks!
# 高德地图SDK
pod 'AMap3DMap'
pod 'AMapLocation'
pod 'AMapSearch'
end
@@ -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 ["这种忧郁感从何而来?", "有什么特别的回忆或想法吗?"]
}
}
}
@@ -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..<recordCount {
let record = EmotionRecord(
userId: currentUser.id,
date: date,
emotionType: EmotionType.allCases.randomElement() ?? .neutral,
intensity: Float.random(in: 0.2...0.9),
context: generateEmotionContext(),
triggers: generateEmotionTriggers(),
location: ["家里", "公司", "咖啡厅", "公园", "地铁上"].randomElement(),
weather: ["晴天", "阴天", "雨天", "雪天"].randomElement(),
notes: generateEmotionNotes()
)
emotionRecords.append(record)
}
}
emotionRecords.sort { $0.date > $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..<messageCount {
let isUserMessage = i % 2 == 0
let messageTime = Calendar.current.date(byAdding: .minute, value: i * 2, to: date) ?? date
let message = Message(
conversationId: conversation.id,
content: isUserMessage ? generateUserMessage() : generateAIMessage(),
type: .text,
sender: isUserMessage ? .user : .ai,
timestamp: messageTime,
emotionScore: isUserMessage ? Float.random(in: 0.2...0.9) : nil
)
messages.append(message)
}
var updatedConversation = conversation
updatedConversation.messages = messages
//
if !messages.isEmpty {
updatedConversation.emotionAnalysis = EmotionAnalysis(
primaryEmotion: EmotionType.allCases.randomElement() ?? .neutral,
emotionIntensity: Float.random(in: 0.3...0.8),
emotionTrend: EmotionTrend.allCases.randomElement() ?? .stable,
keywords: generateEmotionKeywords(),
aiInsights: generateAIInsights()
)
}
return updatedConversation
}
private func createMockTopic(category: TopicCategory, index: Int) -> 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), // 302
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..<rewardCount {
let reward = Reward(
type: RewardType.allCases.randomElement() ?? .points,
title: "课题奖励",
description: "完成课题获得的奖励",
value: Int.random(in: 50...200),
rarity: RewardRarity.allCases.randomElement() ?? .common
)
rewards.append(reward)
}
return rewards
}
private func generateTopicInteractions() -> [TopicInteraction] {
let interactionCount = Int.random(in: 0...3)
var interactions: [TopicInteraction] = []
for i in 0..<interactionCount {
let interaction = TopicInteraction(
topicId: UUID(),
type: InteractionType.allCases.randomElement() ?? .aiChat,
title: "互动\(i+1)",
content: "这是一个有意义的互动内容",
completedAt: Bool.random() ? Date() : nil,
duration: TimeInterval.random(in: 300...1800),
rating: Bool.random() ? Int.random(in: 3...5) : nil
)
interactions.append(interaction)
}
return interactions
}
private func generateLocationPhotos() -> [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..<commentCount {
let comment = Comment(
postId: UUID(),
userId: UUID(),
content: commentContents.randomElement() ?? "很棒的分享",
createdAt: Calendar.current.date(byAdding: .hour, value: -Int.random(in: 1...24), to: Date()) ?? Date(),
likes: Int.random(in: 0...10),
isLikedByCurrentUser: Bool.random()
)
comments.append(comment)
}
return comments
}
private func getTargetValue(for requirement: AchievementRequirement) -> 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
}
}
@@ -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")
}
}
}
@@ -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<Content: View>: 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..<colors.count, id: \.self) { index in
Circle()
.fill(colors[index].opacity(0.3))
.frame(width: geometry.size.width * 0.8)
.offset(
x: sin(animationOffset + Double(index) * 2) * 50,
y: cos(animationOffset + Double(index) * 1.5) * 30
)
.blur(radius: 20)
}
}
}
.onAppear {
withAnimation(
Animation.linear(duration: 20)
.repeatForever(autoreverses: false)
) {
animationOffset = .pi * 2
}
}
}
}
// MARK: -
struct BounceListItem<Content: View>: 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<Front: View, Back: View>: 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..<particleCount).map { _ in
AnimationParticle(
id: UUID(),
position: CGPoint(
x: CGFloat.random(in: 0...UIScreen.main.bounds.width),
y: CGFloat.random(in: 0...UIScreen.main.bounds.height)
),
size: CGFloat.random(in: 2...8),
color: colors.randomElement() ?? .blue,
opacity: Double.random(in: 0.3...1.0)
)
}
}
private func animateParticles() {
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
withAnimation(.linear(duration: 0.1)) {
for i in particles.indices {
particles[i].position.x += CGFloat.random(in: -2...2)
particles[i].position.y += CGFloat.random(in: -2...2)
particles[i].opacity = Double.random(in: 0.3...1.0)
}
}
}
}
}
struct AnimationParticle: Identifiable {
let id: UUID
var position: CGPoint
var size: CGFloat
var color: Color
var opacity: Double
}
// MARK: -
struct AnimatedGradientText: View {
let text: String
let colors: [Color]
@State private var animationOffset = 0.0
init(_ text: String, colors: [Color] = [.blue, .purple, .pink]) {
self.text = text
self.colors = colors
}
var body: some View {
Text(text)
.font(.title)
.fontWeight(.bold)
.foregroundColor(.primary)
.background(
LinearGradient(
colors: colors,
startPoint: .leading,
endPoint: .trailing
)
.rotationEffect(.degrees(animationOffset))
.mask(
Text(text)
.font(.title)
.fontWeight(.bold)
)
)
.onAppear {
withAnimation(
Animation.linear(duration: 3)
.repeatForever(autoreverses: false)
) {
animationOffset = 360
}
}
}
}
// MARK: -
extension View {
func animatedAppearance(delay: Double = 0, animation: Animation = AnimationConfig.smooth) -> 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<Content: View>: 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()
}
@@ -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..<particles.count {
if i < particles.count {
particles[i].move()
//
if !UIScreen.main.bounds.contains(particles[i].position) {
particles[i] = Particle.random(in: UIScreen.main.bounds, color: chakra.color)
}
}
}
}
}
private func showBeforeScorePrompt() {
//
//
beforeScore = 5
togglePlayback()
}
private func completeSession() {
stopSession()
isPlaying = false
//
afterScore = 8
//
showingCompletionSheet = true
}
}
// MARK: -
enum BreathingPhase {
case inhale
case exhale
}
// MARK: -
struct Particle: Identifiable {
let id = UUID()
var position: CGPoint
var direction: CGVector
var speed: CGFloat
var size: CGFloat
var opacity: Double
mutating func move() {
position.x += direction.dx * speed
position.y += direction.dy * speed
opacity = max(0, opacity - 0.001)
if opacity <= 0.1 {
opacity = Double.random(in: 0.3...0.7)
}
}
static func random(in rect: CGRect, color: Color) -> 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: {})
}
@@ -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()
}
@@ -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())
}
@@ -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
)
]
@@ -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()
}
File diff suppressed because it is too large Load Diff
@@ -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..<lineCount, id: \.self) { index in
SkeletonView(
width: index == lineCount - 1 ? CGFloat.random(in: 100...200) : nil,
height: 16,
cornerRadius: 4
)
}
}
}
}
// MARK: -
struct SkeletonCard: View {
let showImage: Bool
let showButton: Bool
init(showImage: Bool = true, showButton: Bool = false) {
self.showImage = showImage
self.showButton = showButton
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
if showImage {
SkeletonView(height: 120, cornerRadius: 12)
}
VStack(alignment: .leading, spacing: 8) {
SkeletonView(height: 20, cornerRadius: 6)
SkeletonText(lineCount: 2, spacing: 6)
if showButton {
HStack {
Spacer()
SkeletonView(width: 80, height: 32, cornerRadius: 16)
}
}
}
.padding(.horizontal, showImage ? 0 : 16)
}
.padding(16)
.background(Color.theme.cardBackground)
.cornerRadius(16)
.shadow(color: Color.black.opacity(0.05), radius: 8, x: 0, y: 2)
}
}
// MARK: -
struct SkeletonListItem: View {
let showAvatar: Bool
let showTrailing: Bool
init(showAvatar: Bool = true, showTrailing: Bool = true) {
self.showAvatar = showAvatar
self.showTrailing = showTrailing
}
var body: some View {
HStack(spacing: 12) {
if showAvatar {
SkeletonView(width: 50, height: 50, cornerRadius: 25)
}
VStack(alignment: .leading, spacing: 6) {
SkeletonView(width: 120, height: 16, cornerRadius: 4)
SkeletonView(width: 200, height: 14, cornerRadius: 4)
}
Spacer()
if showTrailing {
SkeletonView(width: 60, height: 20, cornerRadius: 6)
}
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
}
}
// MARK: -
struct LoadingStateView<Content: View>: 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..<itemCount, id: \.self) { _ in
SkeletonListItem(showAvatar: showAvatar, showTrailing: showTrailing)
Divider()
.background(Color.theme.divider)
}
}
}
}
// MARK: -
struct LoadingCardGrid: View {
let columns: Int
let itemCount: Int
let showImage: Bool
let showButton: Bool
init(columns: Int = 2, itemCount: Int = 6, showImage: Bool = true, showButton: Bool = false) {
self.columns = columns
self.itemCount = itemCount
self.showImage = showImage
self.showButton = showButton
}
var body: some View {
LazyVGrid(
columns: Array(repeating: GridItem(.flexible(), spacing: 16), count: columns),
spacing: 16
) {
ForEach(0..<itemCount, id: \.self) { _ in
SkeletonCard(showImage: showImage, showButton: showButton)
}
}
.padding(.horizontal, 16)
}
}
// MARK: -
struct SmartRefreshView<Content: View>: 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)
}
@@ -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))
}
@@ -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<Bool> = .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)
// mapViewcoordinator
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()
}
}
@@ -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
}()
}
@@ -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<String> = []
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()
}
}
}
@@ -0,0 +1,415 @@
//
// ThemeAdapter.swift
// EmotionMuseum
//
// Created by on 2025/6/13.
//
import SwiftUI
// MARK: -
struct ThemedCard<Content: View>: 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<String>, 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<Content: View>: 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()
}
@@ -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()
}
@@ -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<Content: View>: 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: - 使DataModelsAchievement
#Preview {
UniverseView()
}
-20
View File
@@ -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)
}
}
}
@@ -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 {
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>
@@ -2,7 +2,7 @@
// EmotionMuseumUITests.swift
// EmotionMuseumUITests
//
// Created by on 2025/5/26.
// Created by on 2025/6/13.
//
import XCTest
@@ -2,7 +2,7 @@
// EmotionMuseumUITestsLaunchTests.swift
// EmotionMuseumUITests
//
// Created by on 2025/5/26.
// Created by on 2025/6/13.
//
import XCTest
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>
Binary file not shown.
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 KiB

File diff suppressed because it is too large Load Diff
+267
View File
@@ -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项目框架,含底部导航和主页面布 局,并生成页面组件文件名结构、推荐依赖包清单和初步布局代码。你要不要我开始搭建这部分? |
+336
View File
@@ -0,0 +1,336 @@
# 情绪博物馆 (Emotion Museum)
一个基于Spring Cloud Alibaba微服务架构的情绪管理和AI对话平台。
## 项目概述
情绪博物馆是一个创新的情绪健康管理平台,通过AI对话、情绪记录、数据分析等功能,帮助用户更好地理解和管理自己的情绪状态。
## 🚀 快速部署
### 一键部署(推荐)
```bash
# 克隆项目
git clone <repository-url>
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 <repository-url>
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 <service>
```
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 <container-name>`
## 🔒 安全建议
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)。
+175
View File
@@ -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**: ________________
**域名**: ________________
**签名确认**: ________________
**日期**: ________________
---
**🎉 恭喜完成部署!请妥善保存此检查清单作为部署记录。**
@@ -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
<properties>
<java.version>17</java.version>
<spring-boot.version>3.0.2</spring-boot.version>
<spring-cloud.version>2022.0.0</spring-cloud.version>
<spring-cloud-alibaba.version>2022.0.0.0</spring-cloud-alibaba.version>
<mysql.version>8.0.33</mysql.version>
<redis.version>7.0.8</redis.version>
<mybatis-plus.version>3.5.3.1</mybatis-plus.version>
</properties>
```
---
## 🔧 核心配置设计
### 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 # 统计事件
```
---
*接下来将按照此架构设计逐步创建各个微服务模块*
+352
View File
@@ -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插件后通知我,我会立即开始设计工作!
+10
View File
@@ -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
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ApifoxUploaderProjectSetting">
<option name="apiAccessToken" value="APS-lcuiwXuAQ9Ef4NCzeBgangmOvLg2KEjr" />
</component>
</project>
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile name="Annotation profile for emotion-museum-backend" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<processorPath useClasspath="false">
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.30/lombok-1.18.30.jar" />
<entry name="$MAVEN_REPOSITORY$/org/mapstruct/mapstruct-processor/1.5.3.Final/mapstruct-processor-1.5.3.Final.jar" />
<entry name="$MAVEN_REPOSITORY$/org/mapstruct/mapstruct/1.5.3.Final/mapstruct-1.5.3.Final.jar" />
</processorPath>
<module name="emotion-stats" />
<module name="emotion-ai" />
<module name="emotion-reward" />
<module name="emotion-user" />
<module name="emotion-gateway" />
<module name="emotion-explore" />
<module name="emotion-common" />
<module name="emotion-growth" />
<module name="emotion-record" />
</profile>
</annotationProcessing>
</component>
</project>
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="emotion_museum" uuid="9d7864ea-853a-4143-81b8-f434fbf28caf">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://localhost:3306/emotion_museum?useUnicode=true&amp;characterEncoding=utf8&amp;zeroDateTimeBehavior=convertToNull&amp;useSSL=true&amp;serverTimezone=GMT</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>
+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/emotion-ai/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/emotion-ai/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/emotion-common/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/emotion-common/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/emotion-explore/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/emotion-explore/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/emotion-gateway/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/emotion-gateway/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/emotion-growth/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/emotion-growth/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/emotion-record/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/emotion-record/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/emotion-reward/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/emotion-reward/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/emotion-stats/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/emotion-stats/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/emotion-user/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/emotion-user/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>
+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="aliyun-maven" />
<option name="name" value="Aliyun Maven Repository" />
<option name="url" value="https://maven.aliyun.com/repository/public" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://maven.aliyun.com/nexus/content/groups/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
</component>
</project>
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MaterialThemeProjectNewConfig">
<option name="metadata">
<MTProjectMetadataState>
<option name="migrated" value="true" />
<option name="pristineConfig" value="false" />
<option name="userId" value="-37f43507:191123ac8a3:-7ffe" />
</MTProjectMetadataState>
</option>
</component>
</project>
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK" />
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>
+364
View File
@@ -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
<parent>
<groupId>com.emotionmuseum</groupId>
<artifactId>backend</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>emotion-new-service</artifactId>
```
3. **添加到父工程**
```xml
<modules>
<module>emotion-new-service</module>
</modules>
```
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) 文件了解详情。

Some files were not shown because too many files have changed in this diff Show More