feat: 项目初始化及当前全部内容提交
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
Generated
+10
@@ -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
|
||||
Generated
+6
@@ -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>
|
||||
Generated
+10
@@ -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="{"currentConversationId":"__NEW_AGENT__","conversations":{"036b95bf-0e85-468f-8841-83bb6c16c707":{"id":"036b95bf-0e85-468f-8841-83bb6c16c707","createdAtIso":"2025-07-15T08:46:03.668Z","lastInteractedAtIso":"2025-07-15T08:46:03.668Z","chatHistory":[],"feedbackStates":{},"toolUseStates":{},"draftExchange":{"request_message":"","rich_text_json_repr":{"type":"doc","content":[{"type":"paragraph"}]},"mentioned_items":[],"status":"draft"},"requestIds":[],"isPinned":false,"isShareable":false,"extraData":{"hasDirtyEdits":false},"personaType":0},"__NEW_AGENT__":{"id":"__NEW_AGENT__","createdAtIso":"2025-07-15T08:46:03.764Z","lastInteractedAtIso":"2025-07-15T08:46:03.816Z","chatHistory":[],"feedbackStates":{},"toolUseStates":{},"draftExchange":{"request_message":"","rich_text_json_repr":{"type":"doc","content":[{"type":"paragraph"}]},"mentioned_items":[],"status":"draft"},"requestIds":[],"isPinned":false,"isShareable":false,"extraData":{"hasDirtyEdits":false,"isAgentConversation":true,"baselineTimestamp":0},"personaType":0,"rootTaskUuid":"91b4236a-b9d0-4964-9424-1adb7d5a1c45"}},"agentExecutionMode":"auto","isPanelCollapsed":true,"displayedAnnouncements":[],"sortConversationsBy":"lastMessageTimestamp","sendMode":"send"}" />
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+29
@@ -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>
|
||||
Generated
+46
@@ -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>
|
||||
Generated
+17
@@ -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>
|
||||
Generated
+35
@@ -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>
|
||||
Generated
+25
@@ -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>
|
||||
Generated
+12
@@ -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>
|
||||
Generated
+15
@@ -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>
|
||||
Generated
+8
@@ -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>
|
||||
Generated
+6
@@ -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
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"java.compile.nullAnalysis.mode": "automatic",
|
||||
"dbcode.connections": []
|
||||
}
|
||||
@@ -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集成
|
||||
|
||||
---
|
||||
|
||||
以上是情绪博物馆全栈项目的完整开发指南,请在开发过程中严格遵循相关规范,确保代码质量和项目的可维护性。
|
||||
@@ -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/`
|
||||
|
||||
**🎉 恭喜!您的情绪博物馆项目已成功部署到自定义目录结构!**
|
||||
@@ -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
@@ -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. 部署步骤和配置
|
||||
|
||||
---
|
||||
|
||||
**注意**: 请确保在生产环境中修改默认密码和配置,并定期进行安全更新。
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
+146
-169
@@ -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
-6
@@ -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>
|
||||
+14
@@ -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
|
||||
@@ -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), // 30分钟到2小时
|
||||
prerequisites: index > 1 ? [UUID()] : []
|
||||
)
|
||||
}
|
||||
|
||||
private func generateAIResponse(for userMessage: String) -> String {
|
||||
let responses = [
|
||||
"我理解你的感受,这确实是一个值得思考的问题。",
|
||||
"你提到的这个情况很常见,让我们一起来分析一下。",
|
||||
"从你的描述中,我感受到了你的情绪变化。",
|
||||
"这是一个很好的观察,你有什么想法吗?",
|
||||
"我听到了你的担忧,我们可以一步步来解决。",
|
||||
"你的感受是完全可以理解的,很多人都会有类似的经历。",
|
||||
"让我们换个角度来看这个问题,可能会有新的发现。",
|
||||
"你已经很勇敢地表达了自己的想法,这很棒。",
|
||||
"我注意到你提到了一些关键词,我们可以深入探讨一下。",
|
||||
"你的情绪管理能力在不断提升,继续保持。"
|
||||
]
|
||||
return responses.randomElement() ?? "谢谢你的分享。"
|
||||
}
|
||||
|
||||
// MARK: - Content Generation Methods
|
||||
|
||||
private func generateEmotionContext() -> String {
|
||||
let contexts = [
|
||||
"工作压力让我感到疲惫",
|
||||
"和朋友聊天后心情变好了",
|
||||
"看到美丽的日落感到平静",
|
||||
"遇到挫折时感到沮丧",
|
||||
"完成任务后有成就感",
|
||||
"听音乐时情绪放松",
|
||||
"运动后感到充满活力",
|
||||
"独处时思考人生",
|
||||
"与家人团聚很温暖",
|
||||
"面对未知感到紧张"
|
||||
]
|
||||
return contexts.randomElement() ?? "日常生活中的情绪体验"
|
||||
}
|
||||
|
||||
private func generateEmotionTriggers() -> [String] {
|
||||
let allTriggers = ["工作", "人际关系", "健康", "家庭", "学习", "金钱", "未来", "过去", "天气", "音乐"]
|
||||
return Array(allTriggers.shuffled().prefix(Int.random(in: 1...3)))
|
||||
}
|
||||
|
||||
private func generateEmotionNotes() -> String? {
|
||||
let notes = [
|
||||
"今天的情绪比昨天好一些",
|
||||
"需要更多的休息时间",
|
||||
"和朋友的谈话很有帮助",
|
||||
"运动确实能改善心情",
|
||||
"要学会接受自己的情绪",
|
||||
nil, nil // 有些记录没有备注
|
||||
]
|
||||
return notes.randomElement() ?? nil
|
||||
}
|
||||
|
||||
private func generateConversationTitle(index: Int) -> String {
|
||||
let titles = [
|
||||
"今天的心情分享",
|
||||
"关于压力管理的讨论",
|
||||
"人际关系的困惑",
|
||||
"职场焦虑的缓解",
|
||||
"自我成长的反思",
|
||||
"情绪调节的方法",
|
||||
"生活目标的规划",
|
||||
"内心平静的追求",
|
||||
"人生意义的探索",
|
||||
"幸福感的提升"
|
||||
]
|
||||
return titles[index % titles.count]
|
||||
}
|
||||
|
||||
private func generateConversationTags() -> [String] {
|
||||
let allTags = ["情绪管理", "压力缓解", "人际关系", "自我成长", "生活规划", "心理健康"]
|
||||
return Array(allTags.shuffled().prefix(Int.random(in: 1...3)))
|
||||
}
|
||||
|
||||
private func generateUserMessage() -> String {
|
||||
let messages = [
|
||||
"我最近感到有些焦虑,不知道该怎么办。",
|
||||
"工作压力很大,总是担心做不好。",
|
||||
"和朋友的关系出现了一些问题。",
|
||||
"我想要改变现在的生活状态。",
|
||||
"有时候感到很孤独,需要有人倾听。",
|
||||
"对未来感到不确定,有些迷茫。",
|
||||
"今天心情不错,想分享一下。",
|
||||
"我在思考人生的意义是什么。",
|
||||
"想要培养一些新的习惯。",
|
||||
"感觉自己需要更多的自信。"
|
||||
]
|
||||
return messages.randomElement() ?? "你好"
|
||||
}
|
||||
|
||||
private func generateAIMessage() -> String {
|
||||
let messages = [
|
||||
"我理解你的感受,焦虑是很正常的情绪反应。",
|
||||
"工作压力确实会影响我们的心情,不妨试试放松技巧。",
|
||||
"人际关系需要时间和耐心来维护,你做得很好。",
|
||||
"改变需要勇气,你已经迈出了第一步。",
|
||||
"孤独感是人类共同的体验,你并不孤单。",
|
||||
"对未来的不确定感是成长的一部分。",
|
||||
"很高兴听到你今天心情不错!",
|
||||
"人生意义的探索是一个持续的过程。",
|
||||
"培养新习惯需要时间,要对自己有耐心。",
|
||||
"自信是可以通过练习来培养的。"
|
||||
]
|
||||
return messages.randomElement() ?? "谢谢你的分享"
|
||||
}
|
||||
|
||||
private func generateEmotionKeywords() -> [String] {
|
||||
let keywords = ["压力", "焦虑", "快乐", "悲伤", "希望", "困惑", "平静", "兴奋", "担忧", "满足"]
|
||||
return Array(keywords.shuffled().prefix(Int.random(in: 2...4)))
|
||||
}
|
||||
|
||||
private func generateAIInsights() -> String {
|
||||
let insights = [
|
||||
"你的情绪表达能力在不断提升,这是很好的进步。",
|
||||
"从对话中可以看出你对自我成长很有意识。",
|
||||
"你善于反思,这有助于情绪的自我调节。",
|
||||
"你的积极态度值得赞赏,继续保持。",
|
||||
"建议多关注自己的情绪变化模式。",
|
||||
"你的表达很真诚,这有助于深入的自我探索。"
|
||||
]
|
||||
return insights.randomElement() ?? "继续保持这种开放的态度"
|
||||
}
|
||||
|
||||
private func getTopicTitles(for category: TopicCategory) -> [String] {
|
||||
switch category {
|
||||
case .selfAwareness:
|
||||
return ["认识真实的自己", "探索内在价值观", "发现个人优势", "理解情绪模式"]
|
||||
case .emotionRegulation:
|
||||
return ["压力管理技巧", "愤怒情绪调节", "焦虑缓解方法", "悲伤情绪处理"]
|
||||
case .socialSkills:
|
||||
return ["有效沟通技巧", "建立良好关系", "冲突解决能力", "团队合作精神"]
|
||||
case .stressManagement:
|
||||
return ["工作压力应对", "时间管理技能", "放松训练方法", "心理韧性建设"]
|
||||
case .lifeGoals:
|
||||
return ["目标设定方法", "人生规划技巧", "价值观澄清", "意义感培养"]
|
||||
case .mindfulness:
|
||||
return ["正念冥想入门", "专注力训练", "当下觉察练习", "内心平静修炼"]
|
||||
case .relationships:
|
||||
return ["亲密关系维护", "友谊经营之道", "家庭和谐相处", "社交边界设定"]
|
||||
case .creativity:
|
||||
return ["创意思维开发", "艺术表达练习", "问题解决创新", "想象力激发"]
|
||||
}
|
||||
}
|
||||
|
||||
private func generateTopicDescription(for title: String) -> String {
|
||||
return "通过系统化的学习和练习,帮助你在\(title)方面获得提升。包含理论知识、实践练习和个人反思,让你在成长的道路上更进一步。"
|
||||
}
|
||||
|
||||
private func generateTopicRewards() -> [Reward] {
|
||||
let rewardCount = Int.random(in: 0...2)
|
||||
var rewards: [Reward] = []
|
||||
|
||||
for _ in 0..<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)
|
||||
|
||||
// 保存mapView引用到coordinator并开始定位
|
||||
context.coordinator.mapView = mapView
|
||||
context.coordinator.startLocationUpdates()
|
||||
|
||||
return mapView
|
||||
}
|
||||
|
||||
// 更新地图视图
|
||||
func updateUIView(_ mapView: MKMapView, context: Context) {
|
||||
// 检查是否需要重新请求定位权限
|
||||
context.coordinator.checkLocationPermission()
|
||||
|
||||
// 检查是否需要移动到用户位置
|
||||
if shouldMoveToUserLocation {
|
||||
context.coordinator.moveToUserLocation()
|
||||
// 重置状态
|
||||
DispatchQueue.main.async {
|
||||
shouldMoveToUserLocation = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 协调器类,用于处理地图代理事件和位置管理
|
||||
class Coordinator: NSObject, MKMapViewDelegate, CLLocationManagerDelegate {
|
||||
var parent: MapViewRepresentable
|
||||
var mapView: MKMapView?
|
||||
var locationManager: CLLocationManager
|
||||
var hasInitialLocationSet = false
|
||||
|
||||
init(_ parent: MapViewRepresentable) {
|
||||
self.parent = parent
|
||||
self.locationManager = CLLocationManager()
|
||||
super.init()
|
||||
|
||||
// 配置位置管理器
|
||||
locationManager.delegate = self
|
||||
locationManager.desiredAccuracy = kCLLocationAccuracyBest
|
||||
locationManager.distanceFilter = 10 // 移动10米以上才更新
|
||||
}
|
||||
|
||||
// 开始位置更新
|
||||
func startLocationUpdates() {
|
||||
checkLocationPermission()
|
||||
}
|
||||
|
||||
// 检查定位权限并请求权限
|
||||
func checkLocationPermission() {
|
||||
guard CLLocationManager.locationServicesEnabled() else {
|
||||
print("定位服务未启用")
|
||||
return
|
||||
}
|
||||
|
||||
let authorizationStatus = locationManager.authorizationStatus
|
||||
|
||||
switch authorizationStatus {
|
||||
case .notDetermined:
|
||||
// 首次使用,请求定位权限
|
||||
locationManager.requestWhenInUseAuthorization()
|
||||
|
||||
case .authorizedWhenInUse, .authorizedAlways:
|
||||
// 已授权,开始定位
|
||||
locationManager.startUpdatingLocation()
|
||||
|
||||
case .denied, .restricted:
|
||||
// 被拒绝或受限,显示默认位置
|
||||
print("定位权限被拒绝,显示默认位置")
|
||||
setDefaultLocation()
|
||||
|
||||
@unknown default:
|
||||
print("未知的定位权限状态")
|
||||
setDefaultLocation()
|
||||
}
|
||||
}
|
||||
|
||||
// 设置默认位置
|
||||
func setDefaultLocation() {
|
||||
DispatchQueue.main.async {
|
||||
guard let mapView = self.mapView else { return }
|
||||
|
||||
let region = MKCoordinateRegion(
|
||||
center: self.parent.defaultLocation,
|
||||
span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
|
||||
)
|
||||
mapView.setRegion(region, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CLLocationManagerDelegate
|
||||
|
||||
// 权限状态变化
|
||||
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
||||
checkLocationPermission()
|
||||
}
|
||||
|
||||
// 位置更新成功
|
||||
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||
guard let location = locations.last else { return }
|
||||
|
||||
// 只在首次获取位置时自动移动地图到用户位置
|
||||
if !hasInitialLocationSet {
|
||||
DispatchQueue.main.async {
|
||||
guard let mapView = self.mapView else { return }
|
||||
|
||||
let userRegion = MKCoordinateRegion(
|
||||
center: location.coordinate,
|
||||
span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
|
||||
)
|
||||
mapView.setRegion(userRegion, animated: true)
|
||||
self.hasInitialLocationSet = true
|
||||
|
||||
print("已定位到用户位置: \(location.coordinate)")
|
||||
}
|
||||
|
||||
// 定位成功后可以停止持续更新,节省电量
|
||||
locationManager.stopUpdatingLocation()
|
||||
}
|
||||
}
|
||||
|
||||
// 位置更新失败
|
||||
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
||||
print("定位失败: \(error.localizedDescription)")
|
||||
|
||||
// 定位失败时显示默认位置
|
||||
if !hasInitialLocationSet {
|
||||
setDefaultLocation()
|
||||
hasInitialLocationSet = true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MKMapViewDelegate
|
||||
|
||||
// 用户位置更新(地图上的蓝点)
|
||||
func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
|
||||
// 这里不自动移动地图,避免干扰用户操作
|
||||
// 用户位置的蓝点会自动显示在地图上
|
||||
}
|
||||
|
||||
// 地图区域变化
|
||||
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
|
||||
// 可以在这里处理地图区域变化事件
|
||||
}
|
||||
|
||||
// 手动回到用户位置的方法(供外部调用)
|
||||
func moveToUserLocation() {
|
||||
guard let mapView = self.mapView,
|
||||
let userLocation = mapView.userLocation.location else {
|
||||
// 如果没有用户位置,重新开始定位
|
||||
checkLocationPermission()
|
||||
return
|
||||
}
|
||||
|
||||
let userRegion = MKCoordinateRegion(
|
||||
center: userLocation.coordinate,
|
||||
span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
|
||||
)
|
||||
mapView.setRegion(userRegion, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MapView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MapView()
|
||||
}
|
||||
}
|
||||
@@ -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: - 数据模型(使用DataModels中的Achievement)
|
||||
|
||||
#Preview {
|
||||
UniverseView()
|
||||
}
|
||||
@@ -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
-1
@@ -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>
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
// EmotionMuseumUITests.swift
|
||||
// EmotionMuseumUITests
|
||||
//
|
||||
// Created by 华中敏 on 2025/5/26.
|
||||
// Created by 华中敏 on 2025/6/13.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
+1
-1
@@ -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.
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
@@ -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项目框架,含底部导航和主页面布 局,并生成页面组件文件名结构、推荐依赖包清单和初步布局代码。你要不要我开始搭建这部分? |
|
||||
|
||||
@@ -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)。
|
||||
@@ -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
@@ -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插件后通知我,我会立即开始设计工作!
|
||||
Generated
+10
@@ -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
@@ -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>
|
||||
Generated
+26
@@ -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>
|
||||
Generated
+18
@@ -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&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&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>
|
||||
Generated
+25
@@ -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>
|
||||
Generated
+25
@@ -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
@@ -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>
|
||||
Generated
+12
@@ -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>
|
||||
Generated
+6
@@ -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>
|
||||
@@ -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
Reference in New Issue
Block a user