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

This commit is contained in:
2025-07-15 17:37:50 +08:00
parent ec817067f1
commit e78f192d34
622 changed files with 75174 additions and 383 deletions
@@ -1,11 +0,0 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -1,85 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
-88
View File
@@ -1,88 +0,0 @@
//
// ContentView.swift
// EmotionMuseum
//
// Created by on 2025/5/26.
//
import SwiftUI
import CoreData
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink {
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
} label: {
Text(item.timestamp!, formatter: itemFormatter)
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
#if os(iOS)
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
#endif
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
Text("Select an item")
}
}
private func addItem() {
withAnimation {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { items[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter
}()
#Preview {
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
@@ -0,0 +1,575 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objects = {
/* Begin PBXContainerItemProxy section */
2FB3451A2DFBE273001A8A67 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 2FB344FF2DFBE270001A8A67 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 2FB345062DFBE270001A8A67;
remoteInfo = EmotionMuseum;
};
2FB345242DFBE273001A8A67 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 2FB344FF2DFBE270001A8A67 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 2FB345062DFBE270001A8A67;
remoteInfo = EmotionMuseum;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
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 */
2FB345092DFBE270001A8A67 /* EmotionMuseum */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = EmotionMuseum;
sourceTree = "<group>";
};
2FB3451C2DFBE273001A8A67 /* EmotionMuseumTests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = EmotionMuseumTests;
sourceTree = "<group>";
};
2FB345262DFBE273001A8A67 /* EmotionMuseumUITests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = EmotionMuseumUITests;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
2FB345042DFBE270001A8A67 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
2FB345162DFBE273001A8A67 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
2FB345202DFBE273001A8A67 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
2FB344FE2DFBE270001A8A67 = {
isa = PBXGroup;
children = (
2FB345092DFBE270001A8A67 /* EmotionMuseum */,
2FB3451C2DFBE273001A8A67 /* EmotionMuseumTests */,
2FB345262DFBE273001A8A67 /* EmotionMuseumUITests */,
2FB345082DFBE270001A8A67 /* Products */,
);
sourceTree = "<group>";
};
2FB345082DFBE270001A8A67 /* Products */ = {
isa = PBXGroup;
children = (
2FB345072DFBE270001A8A67 /* EmotionMuseum.app */,
2FB345192DFBE273001A8A67 /* EmotionMuseumTests.xctest */,
2FB345232DFBE273001A8A67 /* EmotionMuseumUITests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
2FB345062DFBE270001A8A67 /* EmotionMuseum */ = {
isa = PBXNativeTarget;
buildConfigurationList = 2FB3452D2DFBE273001A8A67 /* Build configuration list for PBXNativeTarget "EmotionMuseum" */;
buildPhases = (
2FB345032DFBE270001A8A67 /* Sources */,
2FB345042DFBE270001A8A67 /* Frameworks */,
2FB345052DFBE270001A8A67 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
2FB345092DFBE270001A8A67 /* EmotionMuseum */,
);
name = EmotionMuseum;
packageProductDependencies = (
);
productName = EmotionMuseum;
productReference = 2FB345072DFBE270001A8A67 /* EmotionMuseum.app */;
productType = "com.apple.product-type.application";
};
2FB345182DFBE273001A8A67 /* EmotionMuseumTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 2FB345302DFBE273001A8A67 /* Build configuration list for PBXNativeTarget "EmotionMuseumTests" */;
buildPhases = (
2FB345152DFBE273001A8A67 /* Sources */,
2FB345162DFBE273001A8A67 /* Frameworks */,
2FB345172DFBE273001A8A67 /* Resources */,
);
buildRules = (
);
dependencies = (
2FB3451B2DFBE273001A8A67 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
2FB3451C2DFBE273001A8A67 /* EmotionMuseumTests */,
);
name = EmotionMuseumTests;
packageProductDependencies = (
);
productName = EmotionMuseumTests;
productReference = 2FB345192DFBE273001A8A67 /* EmotionMuseumTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
2FB345222DFBE273001A8A67 /* EmotionMuseumUITests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 2FB345332DFBE273001A8A67 /* Build configuration list for PBXNativeTarget "EmotionMuseumUITests" */;
buildPhases = (
2FB3451F2DFBE273001A8A67 /* Sources */,
2FB345202DFBE273001A8A67 /* Frameworks */,
2FB345212DFBE273001A8A67 /* Resources */,
);
buildRules = (
);
dependencies = (
2FB345252DFBE273001A8A67 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
2FB345262DFBE273001A8A67 /* EmotionMuseumUITests */,
);
name = EmotionMuseumUITests;
packageProductDependencies = (
);
productName = EmotionMuseumUITests;
productReference = 2FB345232DFBE273001A8A67 /* EmotionMuseumUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
2FB344FF2DFBE270001A8A67 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1640;
LastUpgradeCheck = 1640;
TargetAttributes = {
2FB345062DFBE270001A8A67 = {
CreatedOnToolsVersion = 16.4;
};
2FB345182DFBE273001A8A67 = {
CreatedOnToolsVersion = 16.4;
TestTargetID = 2FB345062DFBE270001A8A67;
};
2FB345222DFBE273001A8A67 = {
CreatedOnToolsVersion = 16.4;
TestTargetID = 2FB345062DFBE270001A8A67;
};
};
};
buildConfigurationList = 2FB345022DFBE270001A8A67 /* Build configuration list for PBXProject "EmotionMuseum" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 2FB344FE2DFBE270001A8A67;
minimizedProjectReferenceProxies = 1;
preferredProjectObjectVersion = 77;
productRefGroup = 2FB345082DFBE270001A8A67 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
2FB345062DFBE270001A8A67 /* EmotionMuseum */,
2FB345182DFBE273001A8A67 /* EmotionMuseumTests */,
2FB345222DFBE273001A8A67 /* EmotionMuseumUITests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
2FB345052DFBE270001A8A67 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
2FB345172DFBE273001A8A67 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
2FB345212DFBE273001A8A67 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
2FB345032DFBE270001A8A67 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
2FB345152DFBE273001A8A67 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
2FB3451F2DFBE273001A8A67 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
2FB3451B2DFBE273001A8A67 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 2FB345062DFBE270001A8A67 /* EmotionMuseum */;
targetProxy = 2FB3451A2DFBE273001A8A67 /* PBXContainerItemProxy */;
};
2FB345252DFBE273001A8A67 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 2FB345062DFBE270001A8A67 /* EmotionMuseum */;
targetProxy = 2FB345242DFBE273001A8A67 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
2FB3452B2DFBE273001A8A67 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = JA6T4PANZM;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
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;
};
2FB3452C2DFBE273001A8A67 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = JA6T4PANZM;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
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;
};
2FB3452E2DFBE273001A8A67 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = JA6T4PANZM;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
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";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.dolphin.EmotionMuseum;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
2FB3452F2DFBE273001A8A67 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = JA6T4PANZM;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
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";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.dolphin.EmotionMuseum;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
2FB345312DFBE273001A8A67 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = JA6T4PANZM;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = EmotionMuseumTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.dolphin.EmotionMuseumTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/EmotionMuseum.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/EmotionMuseum";
};
name = Debug;
};
2FB345322DFBE273001A8A67 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = JA6T4PANZM;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = EmotionMuseumTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.dolphin.EmotionMuseumTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/EmotionMuseum.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/EmotionMuseum";
};
name = Release;
};
2FB345342DFBE273001A8A67 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = JA6T4PANZM;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = EmotionMuseumUITests/Info.plist;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.dolphin.EmotionMuseumUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = EmotionMuseum;
};
name = Debug;
};
2FB345352DFBE273001A8A67 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = JA6T4PANZM;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = EmotionMuseumUITests/Info.plist;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.dolphin.EmotionMuseumUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = EmotionMuseum;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
2FB345022DFBE270001A8A67 /* Build configuration list for PBXProject "EmotionMuseum" */ = {
isa = XCConfigurationList;
buildConfigurations = (
2FB3452B2DFBE273001A8A67 /* Debug */,
2FB3452C2DFBE273001A8A67 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
2FB3452D2DFBE273001A8A67 /* Build configuration list for PBXNativeTarget "EmotionMuseum" */ = {
isa = XCConfigurationList;
buildConfigurations = (
2FB3452E2DFBE273001A8A67 /* Debug */,
2FB3452F2DFBE273001A8A67 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
2FB345302DFBE273001A8A67 /* Build configuration list for PBXNativeTarget "EmotionMuseumTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
2FB345312DFBE273001A8A67 /* Debug */,
2FB345322DFBE273001A8A67 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
2FB345332DFBE273001A8A67 /* Build configuration list for PBXNativeTarget "EmotionMuseumUITests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
2FB345342DFBE273001A8A67 /* Debug */,
2FB345352DFBE273001A8A67 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 2FB344FF2DFBE270001A8A67 /* Project object */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +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/>
</plist>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildLocationStyle</key>
<string>UseAppPreferences</string>
<key>CustomBuildLocationType</key>
<string>RelativeToDerivedData</string>
<key>DerivedDataLocationStyle</key>
<string>Default</string>
<key>ShowSharedSchemesAutomaticallyEnabled</key>
<true/>
</dict>
</plist>
@@ -2,9 +2,13 @@
<!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/>
<key>SchemeUserState</key>
<dict>
<key>EmotionMuseum.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
</dict>
</dict>
</plist>
@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "0.573",
"red" : "0.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "0.678",
"red" : "0.196"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,35 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.118",
"green" : "0.118",
"red" : "0.118"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.700",
"green" : "0.700",
"red" : "0.700"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.600",
"green" : "0.600",
"red" : "0.600"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.196",
"green" : "0.196",
"red" : "0.196"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.788",
"green" : "0.780",
"red" : "0.776"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.329",
"green" : "0.310",
"red" : "0.298"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.300",
"green" : "0.200",
"red" : "0.900"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.400",
"green" : "0.300",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.929",
"green" : "0.569",
"red" : "0.416"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.929",
"green" : "0.569",
"red" : "0.416"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.000",
"green" : "0.000",
"red" : "0.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.980",
"green" : "0.780",
"red" : "0.310"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.780",
"green" : "0.580",
"red" : "0.210"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.600",
"green" : "0.600",
"red" : "0.600"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.780",
"green" : "0.780",
"red" : "0.780"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.925",
"green" : "0.925",
"red" : "0.925"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.294",
"green" : "0.294",
"red" : "0.294"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.980",
"green" : "0.980",
"red" : "0.980"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.392",
"green" : "0.392",
"red" : "0.392"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.380",
"green" : "0.780",
"red" : "0.200"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.480",
"green" : "0.880",
"red" : "0.300"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.961",
"green" : "0.961",
"red" : "0.961"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.173",
"green" : "0.169",
"red" : "0.165"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.700",
"green" : "0.700",
"red" : "0.700"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.600",
"green" : "0.600",
"red" : "0.600"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.200",
"green" : "0.700",
"red" : "1.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.300",
"green" : "0.800",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,75 @@
//
// ContentView.swift
// EmotionMuseum
//
// Created by on 2025/6/13.
//
import SwiftUI
import CoreData
struct ContentView: View {
@EnvironmentObject var themeManager: ThemeManager
@EnvironmentObject var mockDataManager: MockDataManager
@EnvironmentObject var navigationManager: NavigationManager
@Environment(\.managedObjectContext) private var viewContext
var body: some View {
ZStack {
//
TabView(selection: $navigationManager.currentTab) {
RecordView()
.tabItem {
Image(systemName: "heart.text.square")
Text("记录")
}
.tag(MainTab.record)
GrowthView()
.tabItem {
Image(systemName: "leaf.arrow.circlepath")
Text("治愈")
}
.tag(MainTab.growth)
ExploreView()
.tabItem {
Image(systemName: "map")
Text("探索")
}
.tag(MainTab.explore)
UniverseView()
.tabItem {
Image(systemName: "person.circle")
Text("我的")
}
.tag(MainTab.insight)
}
.accentColor(Color("AccentColor"))
//
if navigationManager.isLoading {
LoadingOverlay(message: navigationManager.loadingMessage)
}
}
.preferredColorScheme(themeManager.isDarkMode ? .dark : .light)
.onAppear {
//
setupInitialState()
}
}
private func setupInitialState() {
//
navigationManager.currentTab = .record
}
}
#Preview {
ContentView()
.environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
.environmentObject(ThemeManager())
.environmentObject(MockDataManager.shared)
.environmentObject(NavigationManager())
}
@@ -0,0 +1,31 @@
//
// EmotionMuseumApp.swift
// EmotionMuseum
//
// Created by on 2025/6/13.
//
import SwiftUI
@main
struct EmotionMuseumApp: App {
let persistenceController = PersistenceController.shared
@StateObject private var navigationManager = NavigationManager()
@StateObject private var themeManager = ThemeManager()
@StateObject private var mockDataManager = MockDataManager.shared
init() {
// SDK
MapManager.shared.configure()
}
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.environmentObject(navigationManager)
.environmentObject(themeManager)
.environmentObject(mockDataManager)
}
}
}
@@ -0,0 +1,189 @@
//
// ChakraType.swift
// EmotionMuseum
//
// Created by on 2025/6/13.
//
import SwiftUI
enum ChakraType: String, CaseIterable {
case root = "海底轮"
case sacral = "脐轮"
case solarPlexus = "太阳轮"
case heart = "心轮"
case throat = "喉轮"
case thirdEye = "眉心轮"
case crown = "顶轮"
var color: Color {
switch self {
case .root:
return .red
case .sacral:
return .orange
case .solarPlexus:
return .yellow
case .heart:
return .green
case .throat:
return .blue
case .thirdEye:
return .indigo
case .crown:
return .purple
}
}
var position: CGPoint {
switch self {
case .root:
return CGPoint(x: 0.5, y: 0.9)
case .sacral:
return CGPoint(x: 0.5, y: 0.8)
case .solarPlexus:
return CGPoint(x: 0.5, y: 0.65)
case .heart:
return CGPoint(x: 0.5, y: 0.5)
case .throat:
return CGPoint(x: 0.5, y: 0.35)
case .thirdEye:
return CGPoint(x: 0.5, y: 0.2)
case .crown:
return CGPoint(x: 0.5, y: 0.05)
}
}
var description: String {
switch self {
case .root:
return "安全感、稳定性、生存本能"
case .sacral:
return "创造力、性能量、情感流动"
case .solarPlexus:
return "个人力量、自信、意志力"
case .heart:
return "爱、同情心、人际关系"
case .throat:
return "沟通、表达、真实性"
case .thirdEye:
return "直觉、洞察力、智慧"
case .crown:
return "灵性连接、觉知、超越"
}
}
var audioFileName: String {
switch self {
case .root:
return "root_chakra_healing"
case .sacral:
return "sacral_chakra_healing"
case .solarPlexus:
return "solar_plexus_chakra_healing"
case .heart:
return "heart_chakra_healing"
case .throat:
return "throat_chakra_healing"
case .thirdEye:
return "third_eye_chakra_healing"
case .crown:
return "crown_chakra_healing"
}
}
var frequency: String {
switch self {
case .root:
return "396 Hz"
case .sacral:
return "417 Hz"
case .solarPlexus:
return "528 Hz"
case .heart:
return "639 Hz"
case .throat:
return "741 Hz"
case .thirdEye:
return "852 Hz"
case .crown:
return "963 Hz"
}
}
var mantra: String {
switch self {
case .root:
return "LAM"
case .sacral:
return "VAM"
case .solarPlexus:
return "RAM"
case .heart:
return "YAM"
case .throat:
return "HAM"
case .thirdEye:
return "OM"
case .crown:
return "AH"
}
}
var element: String {
switch self {
case .root:
return ""
case .sacral:
return ""
case .solarPlexus:
return ""
case .heart:
return ""
case .throat:
return ""
case .thirdEye:
return ""
case .crown:
return "思想"
}
}
var keywords: [String] {
switch self {
case .root:
return ["安全感", "稳定", "生存", "根基", "物质"]
case .sacral:
return ["创造力", "性能量", "情感", "流动", "享受"]
case .solarPlexus:
return ["自信", "力量", "意志", "控制", "个性"]
case .heart:
return ["", "同情", "宽恕", "连接", "和谐"]
case .throat:
return ["表达", "沟通", "真实", "创意", "声音"]
case .thirdEye:
return ["直觉", "洞察", "智慧", "想象", "觉知"]
case .crown:
return ["灵性", "觉醒", "超越", "统一", "神圣"]
}
}
var healingBenefits: [String] {
switch self {
case .root:
return ["增强安全感", "改善焦虑", "提升专注力", "增强体力"]
case .sacral:
return ["激发创造力", "改善人际关系", "增强活力", "平衡情绪"]
case .solarPlexus:
return ["提升自信", "增强意志力", "改善消化", "释放压力"]
case .heart:
return ["开放心扉", "增强同理心", "改善关系", "释放怨恨"]
case .throat:
return ["提升表达能力", "增强创造力", "改善沟通", "释放恐惧"]
case .thirdEye:
return ["增强直觉", "提升洞察力", "改善专注", "开发智慧"]
case .crown:
return ["提升觉知", "增强灵性连接", "获得内在平静", "超越自我"]
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,23 @@
import Foundation
// import AMapFoundationKit // CocoaPods
import CoreLocation
///
/// @Author huazhongmin
/// @Time 2024-03-24
/// @Description SDK
class MapManager {
static let shared = MapManager()
private init() {}
func configure() {
// TODO: CocoaPods
// AppKey
// AMapServices.shared().apiKey = "bb63ae64d651624f3673d61b47b45435"
//
let locationManager = CLLocationManager()
locationManager.requestWhenInUseAuthorization()
}
}
@@ -2,7 +2,7 @@
// Persistence.swift
// EmotionMuseum
//
// Created by on 2025/5/26.
// Created by on 2025/6/13.
//
import CoreData
+11
View File
@@ -0,0 +1,11 @@
platform :ios, '14.0'
target 'EmotionMuseum' do
use_frameworks!
# 高德地图SDK
pod 'AMap3DMap'
pod 'AMapLocation'
pod 'AMapSearch'
end
@@ -0,0 +1,549 @@
//
// AIService.swift
// EmotionMuseum
//
// Created by on 2025/6/13.
//
import Foundation
import Combine
// MARK: - AI
protocol AIServiceProtocol {
func sendMessage(_ message: String, userId: UUID) async throws -> AIResponse
func analyzeEmotion(_ text: String) async throws -> EmotionAnalysis
func generateGrowthSuggestions(for stats: GrowthStats) async throws -> [GrowthTopic]
}
// MARK: - AI
struct AIResponse: Codable {
let messageId: UUID
let content: String
let emotionAnalysis: EmotionAnalysis
let suggestions: [String]
let followUpQuestions: [String]
let confidence: Float
let processingTime: TimeInterval
struct EmotionAnalysis: Codable {
let detectedEmotion: EmotionType
let intensity: Float
let triggers: [String]
let context: String
let recommendations: [String]
}
}
// MARK: - AI
class AIService: AIServiceProtocol, ObservableObject {
static let shared = AIService()
private let baseURL = "https://api.openai.com/v1"
private let apiKey: String
@Published var isLoading = false
@Published var lastError: Error?
private init() {
// API
self.apiKey = Bundle.main.infoDictionary?["OPENAI_API_KEY"] as? String ?? ""
}
// MARK: -
func sendMessage(_ message: String, userId: UUID) async throws -> AIResponse {
let startTime = Date()
isLoading = true
defer { isLoading = false }
let prompt = buildEmotionAnalysisPrompt(message: message)
let requestBody = OpenAIRequest(
model: "gpt-4",
messages: [
OpenAIMessage(role: "system", content: getSystemPrompt()),
OpenAIMessage(role: "user", content: prompt)
],
temperature: 0.7,
maxTokens: 500
)
do {
let response = try await sendOpenAIRequest(requestBody)
let processingTime = Date().timeIntervalSince(startTime)
return try parseAIResponse(response, messageId: UUID(), processingTime: processingTime)
} catch {
lastError = error
throw error
}
}
// MARK: -
func analyzeEmotion(_ text: String) async throws -> EmotionAnalysis {
let prompt = """
分析以下文本的情绪:"\(text)"
请返回JSON格式的分析结果,包含:
- summary: 简要总结
- keywords: 关键词数组
- suggestions: 建议数组
- moodPattern: 情绪模式
- confidence: 置信度(0-1)
"""
let requestBody = OpenAIRequest(
model: "gpt-4",
messages: [
OpenAIMessage(role: "system", content: "你是一个专业的情绪分析师,请用中文回答。"),
OpenAIMessage(role: "user", content: prompt)
],
temperature: 0.3,
maxTokens: 300
)
let response = try await sendOpenAIRequest(requestBody)
return try parseEmotionAnalysis(response)
}
// MARK: -
func generateGrowthSuggestions(for stats: GrowthStats) async throws -> [GrowthTopic] {
let prompt = """
基于用户的五维人格画像生成个性化成长建议:
- 自我感知: \(stats.selfAwareness)
- 情绪韧性: \(stats.emotionalResilience)
- 行动力: \(stats.actionPower)
- 共情力: \(stats.empathy)
- 生活热度: \(stats.lifeEnthusiasm)
请推荐3个最适合的成长课题,返回JSON格式。
"""
let requestBody = OpenAIRequest(
model: "gpt-4",
messages: [
OpenAIMessage(role: "system", content: "你是一个专业的心理成长顾问。"),
OpenAIMessage(role: "user", content: prompt)
],
temperature: 0.5,
maxTokens: 400
)
let response = try await sendOpenAIRequest(requestBody)
return try parseGrowthTopics(response)
}
// MARK: -
private func getSystemPrompt() -> String {
return """
你是情绪博物馆的AI情绪陪伴师,具有以下特质:
1. 专业且温暖:具备心理学知识,但表达温和亲切
2. 情绪敏感:能准确识别和回应用户的情绪状态
3. 个性化关怀:根据用户的话语提供针对性建议
4. 积极导向:引导用户朝着更健康的情绪状态发展
5. 边界清晰:不提供医疗建议,必要时建议寻求专业帮助
请用中文回答,保持对话自然流畅。
"""
}
private func buildEmotionAnalysisPrompt(message: String) -> String {
return """
用户消息:"\(message)"
请分析这条消息的情绪状态,并提供适当的回应。包含:
1. 检测到的主要情绪
2. 情绪强度(0-1)
3. 可能的触发因素
4. 温暖的回应内容
5. 具体的情绪调节建议
6. 后续探索问题
请以JSON格式返回分析结果。
"""
}
private func sendOpenAIRequest(_ request: OpenAIRequest) async throws -> OpenAIResponse {
guard !apiKey.isEmpty else {
throw AIServiceError.missingAPIKey
}
let url = URL(string: "\(baseURL)/chat/completions")!
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
let encoder = JSONEncoder()
urlRequest.httpBody = try encoder.encode(request)
let (data, response) = try await URLSession.shared.data(for: urlRequest)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw AIServiceError.apiError("请求失败")
}
let decoder = JSONDecoder()
return try decoder.decode(OpenAIResponse.self, from: data)
}
private func parseAIResponse(_ response: OpenAIResponse, messageId: UUID, processingTime: TimeInterval) throws -> AIResponse {
guard let choice = response.choices.first else {
throw AIServiceError.parseError("无法解析AI响应")
}
let content = choice.message.content
// JSON
if let jsonData = content.data(using: .utf8),
let parsed = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any] {
let emotion = parseEmotionFromString(parsed["emotion"] as? String ?? "neutral")
let intensity = parsed["intensity"] as? Float ?? 0.5
let triggers = parsed["triggers"] as? [String] ?? []
let context = parsed["context"] as? String ?? ""
let recommendations = parsed["recommendations"] as? [String] ?? []
let suggestions = parsed["suggestions"] as? [String] ?? []
let followUpQuestions = parsed["followUpQuestions"] as? [String] ?? []
let responseContent = parsed["response"] as? String ?? content
let emotionAnalysis = AIResponse.EmotionAnalysis(
detectedEmotion: emotion,
intensity: intensity,
triggers: triggers,
context: context,
recommendations: recommendations
)
return AIResponse(
messageId: messageId,
content: responseContent,
emotionAnalysis: emotionAnalysis,
suggestions: suggestions,
followUpQuestions: followUpQuestions,
confidence: 0.8,
processingTime: processingTime
)
} else {
// JSON
return AIResponse(
messageId: messageId,
content: content,
emotionAnalysis: AIResponse.EmotionAnalysis(
detectedEmotion: .neutral,
intensity: 0.5,
triggers: [],
context: "基础对话",
recommendations: ["继续分享您的感受"]
),
suggestions: ["继续对话"],
followUpQuestions: ["还有什么想分享的吗?"],
confidence: 0.6,
processingTime: processingTime
)
}
}
private func parseEmotionAnalysis(_ response: OpenAIResponse) throws -> EmotionAnalysis {
guard response.choices.first != nil else {
throw AIServiceError.parseError("无法解析情绪分析响应")
}
// JSON
return EmotionAnalysis(
primaryEmotion: .neutral,
emotionIntensity: 0.5,
emotionTrend: .stable,
keywords: ["关键词1", "关键词2"],
aiInsights: "情绪分析摘要",
confidence: 0.75
)
}
private func parseGrowthTopics(_ response: OpenAIResponse) throws -> [GrowthTopic] {
// AI
return [
GrowthTopic(
title: "增强自我认知",
description: "通过反思练习提高自我觉察能力",
category: .selfAwareness,
difficulty: .beginner,
progress: 0.0
),
GrowthTopic(
title: "情绪调节技巧",
description: "学习有效的情绪管理方法",
category: .emotionRegulation,
difficulty: .intermediate,
progress: 0.0
),
GrowthTopic(
title: "提升沟通能力",
description: "改善人际关系和沟通技巧",
category: .relationships,
difficulty: .beginner,
progress: 0.0
)
]
}
private func parseEmotionFromString(_ emotionString: String) -> EmotionType {
switch emotionString.lowercased() {
case "joy", "happy", "开心", "喜悦": return .joy
case "sad", "sadness", "悲伤", "难过": return .sadness
case "angry", "anger", "愤怒", "生气": return .anger
case "fear", "scared", "恐惧", "害怕": return .fear
case "surprise", "surprised", "惊讶", "意外": return .surprise
default: return .neutral
}
}
}
// MARK: - OpenAI API
private struct OpenAIRequest: Codable {
let model: String
let messages: [OpenAIMessage]
let temperature: Float
let maxTokens: Int?
enum CodingKeys: String, CodingKey {
case model, messages, temperature
case maxTokens = "max_tokens"
}
}
private struct OpenAIMessage: Codable {
let role: String
let content: String
}
private struct OpenAIResponse: Codable {
let choices: [OpenAIChoice]
}
private struct OpenAIChoice: Codable {
let message: OpenAIMessage
}
// MARK: -
enum AIServiceError: LocalizedError {
case missingAPIKey
case apiError(String)
case parseError(String)
case networkError(Error)
var errorDescription: String? {
switch self {
case .missingAPIKey:
return "缺少API密钥"
case .apiError(let message):
return "API错误: \(message)"
case .parseError(let message):
return "解析错误: \(message)"
case .networkError(let error):
return "网络错误: \(error.localizedDescription)"
}
}
}
// MARK: - AI
class MockAIService: AIServiceProtocol, ObservableObject {
@Published var isLoading = false
func sendMessage(_ message: String, userId: UUID) async throws -> AIResponse {
isLoading = true
//
try await Task.sleep(nanoseconds: 1_000_000_000) // 1
isLoading = false
let emotion = analyzeEmotionFromMessage(message)
let response = generateMockResponse(for: emotion, message: message)
return AIResponse(
messageId: UUID(),
content: response,
emotionAnalysis: AIResponse.EmotionAnalysis(
detectedEmotion: emotion,
intensity: Float.random(in: 0.3...0.9),
triggers: extractTriggers(from: message),
context: "日常对话",
recommendations: generateRecommendations(for: emotion)
),
suggestions: generateSuggestions(for: emotion),
followUpQuestions: generateFollowUpQuestions(for: emotion),
confidence: Float.random(in: 0.6...0.9),
processingTime: 1.0
)
}
func analyzeEmotion(_ text: String) async throws -> EmotionAnalysis {
try await Task.sleep(nanoseconds: 500_000_000) // 0.5
return EmotionAnalysis(
primaryEmotion: analyzeEmotionFromMessage(text),
emotionIntensity: Float.random(in: 0.3...0.9),
emotionTrend: .stable,
keywords: ["情绪", "感受", "心情"],
aiInsights: "检测到\(analyzeEmotionFromMessage(text).rawValue)情绪",
confidence: Float.random(in: 0.7...0.95)
)
}
func generateGrowthSuggestions(for stats: GrowthStats) async throws -> [GrowthTopic] {
try await Task.sleep(nanoseconds: 800_000_000) // 0.8
return [
GrowthTopic(
title: "自我认知提升",
description: "通过日记和反思提高自我觉察",
category: .selfAwareness,
difficulty: .beginner,
progress: 0.0
),
GrowthTopic(
title: "情绪管理训练",
description: "学习情绪调节和压力缓解技巧",
category: .emotionRegulation,
difficulty: .intermediate,
progress: 0.0
)
]
}
// MARK: -
private func analyzeEmotionFromMessage(_ message: String) -> EmotionType {
let lowerMessage = message.lowercased()
if lowerMessage.contains("开心") || lowerMessage.contains("高兴") || lowerMessage.contains("快乐") {
return .joy
} else if lowerMessage.contains("难过") || lowerMessage.contains("悲伤") || lowerMessage.contains("伤心") {
return .sadness
} else if lowerMessage.contains("生气") || lowerMessage.contains("愤怒") || lowerMessage.contains("烦躁") {
return .anger
} else if lowerMessage.contains("害怕") || lowerMessage.contains("恐惧") || lowerMessage.contains("紧张") {
return .fear
} else if lowerMessage.contains("惊讶") || lowerMessage.contains("意外") {
return .surprise
} else {
return .neutral
}
}
private func generateMockResponse(for emotion: EmotionType, message: String) -> String {
switch emotion {
case .joy:
return "我能感受到你的开心!这种积极的情绪很珍贵,记得把这份快乐分享给身边的人。是什么让你感到如此愉悦呢?"
case .sadness:
return "我理解你现在的感受,难过是正常的情绪反应。允许自己感受这些情绪,同时也要温柔地照顾自己。你愿意分享更多吗?"
case .anger:
return "我能察觉到你的愤怒情绪。愤怒往往是其他情绪的表达,比如受伤或失望。深呼吸一下,我们一起探索这种感受的根源。"
case .fear:
return "恐惧感让人不安,这是很自然的反应。记住,你有能力面对困难。让我们一起分析一下你担心的事情,也许并不像想象中那么可怕。"
case .surprise:
return "意外的事情总是让人印象深刻!无论是好的惊喜还是让人措手不及的情况,都是生活的一部分。告诉我更多细节吧。"
case .neutral:
return "我在聆听你的分享。有时候平静也是一种很好的状态。如果你想深入探讨任何感受或想法,我都愿意陪伴你。"
case .anxiety:
return "我能感受到你的焦虑情绪。焦虑是很常见的感受,让我们一起找到缓解的方法。"
case .excitement:
return "你的兴奋情绪很有感染力!这种充满活力的状态真的很棒。告诉我是什么让你如此激动,我也想分享你的喜悦!"
case .contentment:
return "你的满足感让我很欣慰。这种内心的平和与充实是生活中最珍贵的状态之一。珍惜这份宁静,它会给你力量。"
case .confusion:
return "我感受到你内心的困惑。困惑是思考和成长的开始,让我们一起理清思路。"
case .melancholy:
return "我感受到你内心的忧郁。这种淡淡的愁绪有时候也是一种美,它让我们更深刻地感受生活。愿意和我分享你的思绪吗?"
}
}
private func extractTriggers(from message: String) -> [String] {
//
let keywords = ["工作", "家庭", "朋友", "健康", "学习", "感情", "压力", "变化"]
return keywords.filter { message.contains($0) }
}
private func generateRecommendations(for emotion: EmotionType) -> [String] {
switch emotion {
case .joy:
return ["记录这个美好时刻", "分享你的快乐", "感恩当下"]
case .sadness:
return ["允许自己哭泣", "寻求朋友支持", "进行轻柔运动"]
case .anger:
return ["深呼吸放松", "写下愤怒的原因", "进行体力活动"]
case .fear:
return ["面对恐惧", "寻求专业建议", "制定应对计划"]
case .surprise:
return ["接受变化", "保持开放心态", "记录感受"]
case .neutral:
return ["享受平静", "进行自我反思", "设定新目标"]
case .anxiety:
return ["深呼吸练习", "寻求支持", "制定应对计划"]
case .excitement:
return ["合理安排时间", "保持专注", "与他人分享喜悦"]
case .contentment:
return ["珍惜当下", "保持感恩心", "分享你的平和"]
case .confusion:
return ["整理思路", "寻求建议", "分步骤思考"]
case .melancholy:
return ["接受这种情绪", "寻找美好的事物", "与朋友交流"]
}
}
private func generateSuggestions(for emotion: EmotionType) -> [String] {
switch emotion {
case .joy:
return ["继续保持积极心态", "做些让你快乐的事"]
case .sadness:
return ["给自己一些时间", "考虑专业帮助"]
case .anger:
return ["找到健康的发泄方式", "思考问题的解决方案"]
case .fear:
return ["一步步面对恐惧", "建立支持系统"]
case .surprise:
return ["适应新情况", "保持灵活性"]
case .neutral:
return ["探索新的兴趣", "建立日常习惯"]
case .anxiety:
return ["学习放松技巧", "寻求专业帮助"]
case .excitement:
return ["制定行动计划", "保持理性思考"]
case .contentment:
return ["维持内心平衡", "继续当前的生活方式"]
case .confusion:
return ["寻找清晰的方向", "与他人交流想法"]
case .melancholy:
return ["接受当下的感受", "寻找内心的平静"]
}
}
private func generateFollowUpQuestions(for emotion: EmotionType) -> [String] {
switch emotion {
case .joy:
return ["是什么特别的事情让你如此开心?", "你想如何延续这种快乐?"]
case .sadness:
return ["这种感受持续多久了?", "有什么可以帮助你感觉好一些?"]
case .anger:
return ["什么事情触发了这种愤怒?", "你通常如何处理愤怒情绪?"]
case .fear:
return ["你最担心的是什么?", "有什么可以让你感到更安全?"]
case .surprise:
return ["这个意外对你意味着什么?", "你如何适应这个变化?"]
case .neutral:
return ["最近有什么新的想法吗?", "有什么目标想要实现?"]
case .anxiety:
return ["这种焦虑从什么时候开始的?", "有什么特别担心的事情吗?"]
case .excitement:
return ["是什么让你如此兴奋?", "你计划如何行动?"]
case .contentment:
return ["是什么让你感到如此满足?", "这种状态对你意味着什么?"]
case .confusion:
return ["什么让你感到困惑?", "需要帮助理清哪些思路?"]
case .melancholy:
return ["这种忧郁感从何而来?", "有什么特别的回忆或想法吗?"]
}
}
}
@@ -0,0 +1,701 @@
//
// MockDataManager.swift
// EmotionMuseum
//
// Created by on 2025/7/5.
//
import Foundation
import SwiftUI
class MockDataManager: ObservableObject {
static let shared = MockDataManager()
// MARK: - Published Properties
@Published var currentUser: User
@Published var conversations: [Conversation] = []
@Published var growthTopics: [GrowthTopic] = []
@Published var locationPins: [LocationPin] = []
@Published var communityPosts: [CommunityPost] = []
@Published var emotionRecords: [EmotionRecord] = []
@Published var achievements: [Achievement] = []
@Published var userStats: UserStats = UserStats()
@Published var weeklyStats: WeeklyStats
private init() {
//
self.currentUser = User(
username: "emotion_explorer",
email: "user@example.com",
profile: UserProfile(
nickname: "情绪探索者",
birthDate: Calendar.current.date(byAdding: .year, value: -25, to: Date()),
location: "北京市",
bio: "在情绪的海洋中寻找内心的平静",
memberLevel: .premium,
totalDays: 127,
growthStats: GrowthStats(
selfAwareness: 78.5,
emotionalResilience: 65.2,
actionPower: 72.8,
empathy: 85.3,
lifeEnthusiasm: 69.7
)
),
createdAt: Calendar.current.date(byAdding: .day, value: -127, to: Date()) ?? Date()
)
//
let weekStart = Calendar.current.dateInterval(of: .weekOfYear, for: Date())?.start ?? Date()
self.weeklyStats = WeeklyStats(weekStartDate: weekStart)
//
generateAllMockData()
}
// MARK: - Public Methods
func generateAllMockData() {
generateMockEmotionRecords()
generateMockConversations()
generateMockGrowthTopics()
generateMockLocationPins()
generateMockCommunityPosts()
generateMockAchievements()
updateUserStats()
updateWeeklyStats()
}
func refreshData() async {
await MainActor.run {
generateAllMockData()
}
}
// MARK: - Conversation Methods
func addMessage(to conversationId: UUID, content: String, sender: MessageSender) {
if let index = conversations.firstIndex(where: { $0.id == conversationId }) {
let message = Message(
conversationId: conversationId,
content: content,
type: .text,
sender: sender,
emotionScore: sender == .user ? Float.random(in: 0.3...0.9) : nil
)
conversations[index].messages.append(message)
if sender == .user {
// AI
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
let aiResponse = self.generateAIResponse(for: content)
self.addMessage(to: conversationId, content: aiResponse, sender: .ai)
}
}
}
}
func createNewConversation() -> Conversation {
let conversation = Conversation(
userId: currentUser.id,
title: "新对话 \(Date().shortFormat)"
)
conversations.insert(conversation, at: 0)
return conversation
}
// MARK: - Growth Topic Methods
func updateTopicProgress(_ topicId: UUID, progress: Float) {
if let index = growthTopics.firstIndex(where: { $0.id == topicId }) {
growthTopics[index].progress = min(1.0, progress)
if growthTopics[index].progress >= 1.0 {
growthTopics[index].completedAt = Date()
//
let reward = Reward(
type: .points,
title: "课题完成",
description: "完成了\(growthTopics[index].title)",
value: 100,
rarity: .common
)
growthTopics[index].rewards.append(reward)
}
}
}
func addTopicInteraction(_ topicId: UUID, type: InteractionType, title: String, content: String) {
if let index = growthTopics.firstIndex(where: { $0.id == topicId }) {
let interaction = TopicInteraction(
topicId: topicId,
type: type,
title: title,
content: content,
completedAt: Date(),
duration: TimeInterval.random(in: 300...1800)
)
growthTopics[index].interactions.append(interaction)
//
let progressIncrease: Float = 0.2
updateTopicProgress(topicId, progress: growthTopics[index].progress + progressIncrease)
}
}
// MARK: - Location Methods
func toggleLocationBookmark(_ locationId: UUID) {
if let index = locationPins.firstIndex(where: { $0.id == locationId }) {
locationPins[index].isBookmarked.toggle()
}
}
func addLocationVisit(_ locationId: UUID) {
if let index = locationPins.firstIndex(where: { $0.id == locationId }) {
locationPins[index].visits += 1
}
}
// MARK: - Community Methods
func togglePostLike(_ postId: UUID) {
if let index = communityPosts.firstIndex(where: { $0.id == postId }) {
communityPosts[index].isLikedByCurrentUser.toggle()
if communityPosts[index].isLikedByCurrentUser {
communityPosts[index].likes += 1
} else {
communityPosts[index].likes = max(0, communityPosts[index].likes - 1)
}
}
}
func addComment(to postId: UUID, content: String) {
if let index = communityPosts.firstIndex(where: { $0.id == postId }) {
let comment = Comment(
postId: postId,
userId: currentUser.id,
content: content
)
communityPosts[index].comments.append(comment)
}
}
// MARK: - Private Data Generation Methods
private func generateMockEmotionRecords() {
emotionRecords.removeAll()
// 30
for i in 0..<30 {
let date = Calendar.current.date(byAdding: .day, value: -i, to: Date()) ?? Date()
let recordCount = Int.random(in: 1...3) // 1-3
for _ in 0..<recordCount {
let record = EmotionRecord(
userId: currentUser.id,
date: date,
emotionType: EmotionType.allCases.randomElement() ?? .neutral,
intensity: Float.random(in: 0.2...0.9),
context: generateEmotionContext(),
triggers: generateEmotionTriggers(),
location: ["家里", "公司", "咖啡厅", "公园", "地铁上"].randomElement(),
weather: ["晴天", "阴天", "雨天", "雪天"].randomElement(),
notes: generateEmotionNotes()
)
emotionRecords.append(record)
}
}
emotionRecords.sort { $0.date > $1.date }
}
private func generateMockConversations() {
conversations.removeAll()
// 30
for i in 0..<15 {
let startDate = Calendar.current.date(byAdding: .day, value: -i*2, to: Date()) ?? Date()
let conversation = createMockConversation(for: startDate, index: i)
conversations.append(conversation)
}
conversations.sort { $0.startTime > $1.startTime }
}
private func generateMockGrowthTopics() {
growthTopics.removeAll()
// 3-4
for category in TopicCategory.allCases {
for i in 1...4 {
let topic = createMockTopic(category: category, index: i)
growthTopics.append(topic)
}
}
}
private func generateMockLocationPins() {
locationPins.removeAll()
//
let beijingLocations = [
(39.9042, 116.4074, "天安门广场", "北京的心脏,见证历史的地方"),
(39.9163, 116.3972, "故宫博物院", "明清两代的皇家宫殿,文化瑰宝"),
(40.0031, 116.3272, "颐和园", "清代皇家园林,山水如画"),
(39.8844, 116.5564, "798艺术区", "现代艺术的聚集地,创意无限"),
(39.9389, 116.3467, "什刹海", "老北京的韵味,夜晚格外美丽"),
(39.9056, 116.3913, "南锣鼓巷", "胡同文化的代表,小资情调"),
(40.0090, 116.2755, "香山公园", "秋天赏红叶的绝佳去处"),
(39.8838, 116.4649, "朝阳公园", "都市中的绿洲,休闲好去处"),
(39.9280, 116.3835, "后海", "酒吧一条街,夜生活的天堂"),
(39.9170, 116.3970, "景山公园", "俯瞰紫禁城的最佳位置")
]
for (_, location) in beijingLocations.enumerated() {
let pin = LocationPin(
coordinate: Coordinate(latitude: location.0, longitude: location.1),
title: location.2,
description: location.3,
type: LocationType.allCases.randomElement() ?? .community,
emotionTags: Array(EmotionType.allCases.shuffled().prefix(Int.random(in: 1...3))),
photos: generateLocationPhotos(),
createdBy: Bool.random() ? currentUser.id : UUID(),
createdAt: Calendar.current.date(byAdding: .day, value: -Int.random(in: 1...90), to: Date()) ?? Date(),
likes: Int.random(in: 5...200),
visits: Int.random(in: 10...500),
address: "北京市" + ["东城区", "西城区", "朝阳区", "海淀区"].randomElement()!,
category: LocationCategory.allCases.randomElement() ?? .other,
isBookmarked: Bool.random()
)
locationPins.append(pin)
}
}
private func generateMockCommunityPosts() {
communityPosts.removeAll()
for _ in 0..<20 {
let post = CommunityPost(
userId: Bool.random() ? currentUser.id : UUID(),
locationId: locationPins.randomElement()?.id,
content: generatePostContent(),
photos: generatePostPhotos(),
tags: generatePostTags(),
likes: Int.random(in: 0...100),
comments: generatePostComments(),
createdAt: Calendar.current.date(byAdding: .hour, value: -Int.random(in: 1...168), to: Date()) ?? Date(),
isPrivate: Bool.random(),
viewCount: Int.random(in: 10...1000),
type: PostType.allCases.randomElement() ?? .general,
isLikedByCurrentUser: Bool.random()
)
communityPosts.append(post)
}
communityPosts.sort { $0.createdAt > $1.createdAt }
}
private func generateMockAchievements() {
achievements.removeAll()
let achievementData: [(String, String, AchievementCategory, String, RewardRarity, AchievementRequirement, Int, Bool)] = [
("初次对话", "完成第一次AI对话", .conversation, "message.circle", .common, .conversationCount(1), 1, false),
("话痨达人", "累计对话100次", .conversation, "message.badge", .rare, .conversationCount(100), 45, false),
("情绪记录者", "记录50次情绪", .emotion, "heart.circle", .common, .emotionRecordCount(50), 32, false),
("成长新手", "完成第一个成长课题", .growth, "arrow.up.circle", .common, .topicCompletion(1), 1, false),
("社交达人", "获得100个点赞", .social, "heart.badge", .rare, .socialInteraction(100), 67, false),
("探索者", "访问10个地点", .exploration, "map.circle", .common, .locationVisit(10), 8, false),
("坚持不懈", "连续使用30天", .consistency, "calendar.badge", .epic, .consecutiveDays(30), 25, false),
("积分大户", "累计获得1000积分", .milestone, "star.badge", .rare, .totalPoints(1000), 750, false),
("神秘成就", "发现隐藏彩蛋", .special, "sparkles", .legendary, .special("发现应用中的隐藏彩蛋"), 0, true)
]
for data in achievementData {
let achievement = Achievement(
title: data.0,
description: data.1,
category: data.2,
icon: data.3,
rarity: data.4,
requirement: data.5,
progress: data.6,
targetValue: getTargetValue(for: data.5),
unlockedAt: data.6 >= getTargetValue(for: data.5) ? Date() : nil,
isHidden: data.7
)
achievements.append(achievement)
}
}
// MARK: - Helper Methods
private func createMockConversation(for date: Date, index: Int) -> Conversation {
let conversation = Conversation(
userId: currentUser.id,
title: generateConversationTitle(index: index),
startTime: date,
endTime: Calendar.current.date(byAdding: .minute, value: Int.random(in: 5...60), to: date),
tags: generateConversationTags()
)
//
var messages: [Message] = []
let messageCount = Int.random(in: 4...12)
for i in 0..<messageCount {
let isUserMessage = i % 2 == 0
let messageTime = Calendar.current.date(byAdding: .minute, value: i * 2, to: date) ?? date
let message = Message(
conversationId: conversation.id,
content: isUserMessage ? generateUserMessage() : generateAIMessage(),
type: .text,
sender: isUserMessage ? .user : .ai,
timestamp: messageTime,
emotionScore: isUserMessage ? Float.random(in: 0.2...0.9) : nil
)
messages.append(message)
}
var updatedConversation = conversation
updatedConversation.messages = messages
//
if !messages.isEmpty {
updatedConversation.emotionAnalysis = EmotionAnalysis(
primaryEmotion: EmotionType.allCases.randomElement() ?? .neutral,
emotionIntensity: Float.random(in: 0.3...0.8),
emotionTrend: EmotionTrend.allCases.randomElement() ?? .stable,
keywords: generateEmotionKeywords(),
aiInsights: generateAIInsights()
)
}
return updatedConversation
}
private func createMockTopic(category: TopicCategory, index: Int) -> GrowthTopic {
let topicTitles = getTopicTitles(for: category)
let title = topicTitles[min(index - 1, topicTitles.count - 1)]
return GrowthTopic(
title: title,
description: generateTopicDescription(for: title),
category: category,
difficulty: Difficulty.allCases.randomElement() ?? .beginner,
progress: Float.random(in: 0...1),
level: Int.random(in: 1...5),
totalLevels: 5,
isUnlocked: index <= 2 || Bool.random(), //
completedAt: Float.random(in: 0...1) > 0.7 ? Date() : nil,
rewards: generateTopicRewards(),
interactions: generateTopicInteractions(),
estimatedDuration: TimeInterval.random(in: 1800...7200), // 302
prerequisites: index > 1 ? [UUID()] : []
)
}
private func generateAIResponse(for userMessage: String) -> String {
let responses = [
"我理解你的感受,这确实是一个值得思考的问题。",
"你提到的这个情况很常见,让我们一起来分析一下。",
"从你的描述中,我感受到了你的情绪变化。",
"这是一个很好的观察,你有什么想法吗?",
"我听到了你的担忧,我们可以一步步来解决。",
"你的感受是完全可以理解的,很多人都会有类似的经历。",
"让我们换个角度来看这个问题,可能会有新的发现。",
"你已经很勇敢地表达了自己的想法,这很棒。",
"我注意到你提到了一些关键词,我们可以深入探讨一下。",
"你的情绪管理能力在不断提升,继续保持。"
]
return responses.randomElement() ?? "谢谢你的分享。"
}
// MARK: - Content Generation Methods
private func generateEmotionContext() -> String {
let contexts = [
"工作压力让我感到疲惫",
"和朋友聊天后心情变好了",
"看到美丽的日落感到平静",
"遇到挫折时感到沮丧",
"完成任务后有成就感",
"听音乐时情绪放松",
"运动后感到充满活力",
"独处时思考人生",
"与家人团聚很温暖",
"面对未知感到紧张"
]
return contexts.randomElement() ?? "日常生活中的情绪体验"
}
private func generateEmotionTriggers() -> [String] {
let allTriggers = ["工作", "人际关系", "健康", "家庭", "学习", "金钱", "未来", "过去", "天气", "音乐"]
return Array(allTriggers.shuffled().prefix(Int.random(in: 1...3)))
}
private func generateEmotionNotes() -> String? {
let notes = [
"今天的情绪比昨天好一些",
"需要更多的休息时间",
"和朋友的谈话很有帮助",
"运动确实能改善心情",
"要学会接受自己的情绪",
nil, nil //
]
return notes.randomElement() ?? nil
}
private func generateConversationTitle(index: Int) -> String {
let titles = [
"今天的心情分享",
"关于压力管理的讨论",
"人际关系的困惑",
"职场焦虑的缓解",
"自我成长的反思",
"情绪调节的方法",
"生活目标的规划",
"内心平静的追求",
"人生意义的探索",
"幸福感的提升"
]
return titles[index % titles.count]
}
private func generateConversationTags() -> [String] {
let allTags = ["情绪管理", "压力缓解", "人际关系", "自我成长", "生活规划", "心理健康"]
return Array(allTags.shuffled().prefix(Int.random(in: 1...3)))
}
private func generateUserMessage() -> String {
let messages = [
"我最近感到有些焦虑,不知道该怎么办。",
"工作压力很大,总是担心做不好。",
"和朋友的关系出现了一些问题。",
"我想要改变现在的生活状态。",
"有时候感到很孤独,需要有人倾听。",
"对未来感到不确定,有些迷茫。",
"今天心情不错,想分享一下。",
"我在思考人生的意义是什么。",
"想要培养一些新的习惯。",
"感觉自己需要更多的自信。"
]
return messages.randomElement() ?? "你好"
}
private func generateAIMessage() -> String {
let messages = [
"我理解你的感受,焦虑是很正常的情绪反应。",
"工作压力确实会影响我们的心情,不妨试试放松技巧。",
"人际关系需要时间和耐心来维护,你做得很好。",
"改变需要勇气,你已经迈出了第一步。",
"孤独感是人类共同的体验,你并不孤单。",
"对未来的不确定感是成长的一部分。",
"很高兴听到你今天心情不错!",
"人生意义的探索是一个持续的过程。",
"培养新习惯需要时间,要对自己有耐心。",
"自信是可以通过练习来培养的。"
]
return messages.randomElement() ?? "谢谢你的分享"
}
private func generateEmotionKeywords() -> [String] {
let keywords = ["压力", "焦虑", "快乐", "悲伤", "希望", "困惑", "平静", "兴奋", "担忧", "满足"]
return Array(keywords.shuffled().prefix(Int.random(in: 2...4)))
}
private func generateAIInsights() -> String {
let insights = [
"你的情绪表达能力在不断提升,这是很好的进步。",
"从对话中可以看出你对自我成长很有意识。",
"你善于反思,这有助于情绪的自我调节。",
"你的积极态度值得赞赏,继续保持。",
"建议多关注自己的情绪变化模式。",
"你的表达很真诚,这有助于深入的自我探索。"
]
return insights.randomElement() ?? "继续保持这种开放的态度"
}
private func getTopicTitles(for category: TopicCategory) -> [String] {
switch category {
case .selfAwareness:
return ["认识真实的自己", "探索内在价值观", "发现个人优势", "理解情绪模式"]
case .emotionRegulation:
return ["压力管理技巧", "愤怒情绪调节", "焦虑缓解方法", "悲伤情绪处理"]
case .socialSkills:
return ["有效沟通技巧", "建立良好关系", "冲突解决能力", "团队合作精神"]
case .stressManagement:
return ["工作压力应对", "时间管理技能", "放松训练方法", "心理韧性建设"]
case .lifeGoals:
return ["目标设定方法", "人生规划技巧", "价值观澄清", "意义感培养"]
case .mindfulness:
return ["正念冥想入门", "专注力训练", "当下觉察练习", "内心平静修炼"]
case .relationships:
return ["亲密关系维护", "友谊经营之道", "家庭和谐相处", "社交边界设定"]
case .creativity:
return ["创意思维开发", "艺术表达练习", "问题解决创新", "想象力激发"]
}
}
private func generateTopicDescription(for title: String) -> String {
return "通过系统化的学习和练习,帮助你在\(title)方面获得提升。包含理论知识、实践练习和个人反思,让你在成长的道路上更进一步。"
}
private func generateTopicRewards() -> [Reward] {
let rewardCount = Int.random(in: 0...2)
var rewards: [Reward] = []
for _ in 0..<rewardCount {
let reward = Reward(
type: RewardType.allCases.randomElement() ?? .points,
title: "课题奖励",
description: "完成课题获得的奖励",
value: Int.random(in: 50...200),
rarity: RewardRarity.allCases.randomElement() ?? .common
)
rewards.append(reward)
}
return rewards
}
private func generateTopicInteractions() -> [TopicInteraction] {
let interactionCount = Int.random(in: 0...3)
var interactions: [TopicInteraction] = []
for i in 0..<interactionCount {
let interaction = TopicInteraction(
topicId: UUID(),
type: InteractionType.allCases.randomElement() ?? .aiChat,
title: "互动\(i+1)",
content: "这是一个有意义的互动内容",
completedAt: Bool.random() ? Date() : nil,
duration: TimeInterval.random(in: 300...1800),
rating: Bool.random() ? Int.random(in: 3...5) : nil
)
interactions.append(interaction)
}
return interactions
}
private func generateLocationPhotos() -> [String] {
let photoCount = Int.random(in: 1...4)
return Array(repeating: "location_photo", count: photoCount)
}
private func generatePostContent() -> String {
let contents = [
"今天在这个美丽的地方找到了内心的平静 ✨",
"和朋友一起度过了愉快的下午时光 😊",
"这里的风景让我想起了童年的回忆",
"在这个安静的角落里思考人生的意义",
"发现了一个治愈心灵的好地方",
"阳光透过树叶洒下来,心情瞬间明亮了",
"和陌生人的一次偶遇,让我对生活有了新的感悟",
"在这里感受到了城市中难得的宁静",
"每次来这里都能获得新的启发",
"分享一个让我感到温暖的瞬间"
]
return contents.randomElement() ?? "分享今天的美好时光"
}
private func generatePostPhotos() -> [String] {
let photoCount = Int.random(in: 0...3)
return Array(repeating: "post_photo", count: photoCount)
}
private func generatePostTags() -> [String] {
let allTags = ["治愈", "美好", "分享", "生活", "感悟", "风景", "友谊", "成长", "平静", "温暖"]
return Array(allTags.shuffled().prefix(Int.random(in: 1...3)))
}
private func generatePostComments() -> [Comment] {
let commentCount = Int.random(in: 0...5)
var comments: [Comment] = []
let commentContents = [
"太美了!",
"我也想去这个地方",
"感谢分享 ❤️",
"很有感触",
"下次一起去吧",
"照片拍得很棒",
"这个地方我也去过",
"很治愈的分享"
]
for _ in 0..<commentCount {
let comment = Comment(
postId: UUID(),
userId: UUID(),
content: commentContents.randomElement() ?? "很棒的分享",
createdAt: Calendar.current.date(byAdding: .hour, value: -Int.random(in: 1...24), to: Date()) ?? Date(),
likes: Int.random(in: 0...10),
isLikedByCurrentUser: Bool.random()
)
comments.append(comment)
}
return comments
}
private func getTargetValue(for requirement: AchievementRequirement) -> Int {
switch requirement {
case .conversationCount(let count): return count
case .emotionRecordCount(let count): return count
case .topicCompletion(let count): return count
case .socialInteraction(let count): return count
case .locationVisit(let count): return count
case .consecutiveDays(let days): return days
case .totalPoints(let points): return points
case .special(_): return 1
}
}
private func updateUserStats() {
userStats.totalConversations = conversations.count
userStats.totalMessages = conversations.reduce(0) { $0 + $1.messageCount }
userStats.totalEmotionRecords = emotionRecords.count
userStats.completedTopics = growthTopics.filter { $0.isCompleted }.count
userStats.totalPoints = achievements.reduce(0) { $0 + ($1.isUnlocked ? 100 : 0) }
userStats.consecutiveDays = 25 // 使
userStats.maxConsecutiveDays = 30
userStats.socialInteractions = communityPosts.reduce(0) { $0 + $1.likes + $1.commentCount }
userStats.locationsVisited = locationPins.filter { $0.visits > 0 }.count
userStats.postsCreated = communityPosts.filter { $0.userId == currentUser.id }.count
userStats.likesReceived = communityPosts.filter { $0.userId == currentUser.id }.reduce(0) { $0 + $1.likes }
userStats.commentsReceived = communityPosts.filter { $0.userId == currentUser.id }.reduce(0) { $0 + $1.commentCount }
}
private func updateWeeklyStats() {
let weekStart = weeklyStats.weekStartDate
let weekEnd = Calendar.current.date(byAdding: .day, value: 6, to: weekStart) ?? weekStart
//
weeklyStats.emotionRecords = emotionRecords.filter { record in
record.date >= weekStart && record.date <= weekEnd
}
//
weeklyStats.conversations = conversations.filter { conversation in
conversation.startTime >= weekStart && conversation.startTime <= weekEnd
}
//
if !weeklyStats.emotionRecords.isEmpty {
weeklyStats.averageMoodScore = weeklyStats.emotionRecords.averageIntensity()
weeklyStats.dominantEmotion = weeklyStats.emotionRecords.dominantEmotion()
}
//
weeklyStats.moodTrend = EmotionTrend.allCases.randomElement() ?? .stable
//
let dailyActivity = Dictionary(grouping: weeklyStats.emotionRecords + weeklyStats.conversations.map { conversation in
EmotionRecord(userId: currentUser.id, date: conversation.startTime, emotionType: .neutral, intensity: 0.5, context: "对话活动")
}, by: { Calendar.current.startOfDay(for: $0.date) })
weeklyStats.mostActiveDay = dailyActivity.max(by: { $0.value.count < $1.value.count })?.key
}
}
@@ -0,0 +1,411 @@
//
// NavigationManager.swift
// EmotionMuseum
//
// Created by on 2025/7/5.
//
import Foundation
import SwiftUI
class NavigationManager: ObservableObject {
// MARK: - Tab Navigation
@Published var currentTab: MainTab = .record
// MARK: - Navigation Paths for each tab
@Published var recordNavigation = NavigationPath()
@Published var growthNavigation = NavigationPath()
@Published var exploreNavigation = NavigationPath()
@Published var insightNavigation = NavigationPath()
// MARK: - Global Modal States
@Published var showingChatHistory = false
@Published var showingFullScreenChat = false
@Published var showingSettings = false
@Published var showingProfile = false
@Published var showingThemeSettings = false
@Published var showingAddLocation = false
@Published var showingTopicDetail = false
@Published var showingLocationDetail = false
@Published var showingPostDetail = false
@Published var showingAchievements = false
@Published var showingMemberCenter = false
// MARK: - Current Selection States
@Published var selectedConversation: Conversation?
@Published var selectedTopic: GrowthTopic?
@Published var selectedLocation: LocationPin?
@Published var selectedPost: CommunityPost?
@Published var selectedAchievement: Achievement?
// MARK: - Chat States
@Published var currentChatConversation: Conversation?
@Published var isVoiceMode = false
@Published var chatInputText = ""
// MARK: - Loading States
@Published var isLoading = false
@Published var loadingMessage = ""
// MARK: - Navigation Methods
func navigateToTab(_ tab: MainTab) {
withAnimation(.easeInOut(duration: 0.3)) {
currentTab = tab
}
}
func navigateToChat(conversation: Conversation? = nil) {
selectedConversation = conversation
currentChatConversation = conversation
showingFullScreenChat = true
}
func navigateToTopic(_ topic: GrowthTopic) {
selectedTopic = topic
showingTopicDetail = true
}
func navigateToLocation(_ location: LocationPin) {
selectedLocation = location
showingLocationDetail = true
}
func navigateToPost(_ post: CommunityPost) {
selectedPost = post
showingPostDetail = true
}
func navigateToProfile() {
showingProfile = true
}
func navigateToSettings() {
showingSettings = true
}
func navigateToThemeSettings() {
showingThemeSettings = true
}
func navigateToAchievements() {
showingAchievements = true
}
func navigateToMemberCenter() {
showingMemberCenter = true
}
func showAddLocation() {
showingAddLocation = true
}
func showChatHistory() {
showingChatHistory = true
}
// MARK: - Dismiss Methods
func dismissAllModals() {
showingChatHistory = false
showingFullScreenChat = false
showingSettings = false
showingProfile = false
showingThemeSettings = false
showingAddLocation = false
showingTopicDetail = false
showingLocationDetail = false
showingPostDetail = false
showingAchievements = false
showingMemberCenter = false
selectedConversation = nil
selectedTopic = nil
selectedLocation = nil
selectedPost = nil
selectedAchievement = nil
currentChatConversation = nil
}
func dismissCurrentModal() {
if showingFullScreenChat {
showingFullScreenChat = false
currentChatConversation = nil
} else if showingChatHistory {
showingChatHistory = false
} else if showingSettings {
showingSettings = false
} else if showingProfile {
showingProfile = false
} else if showingThemeSettings {
showingThemeSettings = false
} else if showingAddLocation {
showingAddLocation = false
} else if showingTopicDetail {
showingTopicDetail = false
selectedTopic = nil
} else if showingLocationDetail {
showingLocationDetail = false
selectedLocation = nil
} else if showingPostDetail {
showingPostDetail = false
selectedPost = nil
} else if showingAchievements {
showingAchievements = false
} else if showingMemberCenter {
showingMemberCenter = false
}
}
// MARK: - Deep Link Methods
func handleDeepLink(url: URL) {
//
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return }
switch components.host {
case "conversation":
if let conversationId = components.queryItems?.first(where: { $0.name == "id" })?.value,
let uuid = UUID(uuidString: conversationId) {
//
let conversation = MockDataManager.shared.conversations.first { $0.id == uuid }
navigateToChat(conversation: conversation)
}
case "topic":
if let topicId = components.queryItems?.first(where: { $0.name == "id" })?.value,
let uuid = UUID(uuidString: topicId) {
//
let topic = MockDataManager.shared.growthTopics.first { $0.id == uuid }
if let topic = topic {
navigateToTab(.growth)
navigateToTopic(topic)
}
}
case "location":
if let locationId = components.queryItems?.first(where: { $0.name == "id" })?.value,
let uuid = UUID(uuidString: locationId) {
//
let location = MockDataManager.shared.locationPins.first { $0.id == uuid }
if let location = location {
navigateToTab(.explore)
navigateToLocation(location)
}
}
case "post":
if let postId = components.queryItems?.first(where: { $0.name == "id" })?.value,
let uuid = UUID(uuidString: postId) {
//
let post = MockDataManager.shared.communityPosts.first { $0.id == uuid }
if let post = post {
navigateToTab(.explore)
navigateToPost(post)
}
}
default:
break
}
}
// MARK: - Loading Management
func showLoading(_ message: String = "加载中...") {
loadingMessage = message
withAnimation(.easeInOut) {
isLoading = true
}
}
func hideLoading() {
withAnimation(.easeInOut) {
isLoading = false
}
loadingMessage = ""
}
// MARK: - Chat Management
func sendMessage(_ content: String, to conversation: Conversation? = nil) {
guard !content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
let targetConversation = conversation ?? currentChatConversation
if let conv = targetConversation {
MockDataManager.shared.addMessage(to: conv.id, content: content, sender: .user)
} else {
//
let newConversation = MockDataManager.shared.createNewConversation()
currentChatConversation = newConversation
MockDataManager.shared.addMessage(to: newConversation.id, content: content, sender: .user)
}
chatInputText = ""
}
func toggleVoiceMode() {
withAnimation(.easeInOut(duration: 0.2)) {
isVoiceMode.toggle()
}
}
// MARK: - Utility Methods
func clearNavigationPath(for tab: MainTab) {
switch tab {
case .record:
recordNavigation = NavigationPath()
case .growth:
growthNavigation = NavigationPath()
case .explore:
exploreNavigation = NavigationPath()
case .insight:
insightNavigation = NavigationPath()
}
}
func popToRoot(for tab: MainTab) {
clearNavigationPath(for: tab)
dismissAllModals()
}
func canGoBack(for tab: MainTab) -> Bool {
switch tab {
case .record:
return !recordNavigation.isEmpty
case .growth:
return !growthNavigation.isEmpty
case .explore:
return !exploreNavigation.isEmpty
case .insight:
return !insightNavigation.isEmpty
}
}
func goBack(for tab: MainTab) {
switch tab {
case .record:
if !recordNavigation.isEmpty {
recordNavigation.removeLast()
}
case .growth:
if !growthNavigation.isEmpty {
growthNavigation.removeLast()
}
case .explore:
if !exploreNavigation.isEmpty {
exploreNavigation.removeLast()
}
case .insight:
if !insightNavigation.isEmpty {
insightNavigation.removeLast()
}
}
}
}
// MARK: - Main Tab Enum
enum MainTab: String, CaseIterable {
case record = "记录"
case growth = "治愈"
case explore = "探索"
case insight = "我的"
var title: String {
return self.rawValue
}
var icon: String {
switch self {
case .record: return "brain.head.profile"
case .growth: return "heart"
case .explore: return "map"
case .insight: return "person"
}
}
var selectedIcon: String {
switch self {
case .record: return "brain.head.profile.fill"
case .growth: return "heart.fill"
case .explore: return "map.fill"
case .insight: return "person.fill"
}
}
var color: Color {
switch self {
case .record: return .blue
case .growth: return .pink
case .explore: return .green
case .insight: return .orange
}
}
}
// MARK: - Navigation Destination Types
enum NavigationDestination: Hashable {
case conversation(Conversation)
case topic(GrowthTopic)
case location(LocationPin)
case post(CommunityPost)
case achievement(Achievement)
case settings
case profile
case chatHistory
case addLocation
static func == (lhs: NavigationDestination, rhs: NavigationDestination) -> Bool {
switch (lhs, rhs) {
case (.conversation(let lhsConv), .conversation(let rhsConv)):
return lhsConv.id == rhsConv.id
case (.topic(let lhsTopic), .topic(let rhsTopic)):
return lhsTopic.id == rhsTopic.id
case (.location(let lhsLoc), .location(let rhsLoc)):
return lhsLoc.id == rhsLoc.id
case (.post(let lhsPost), .post(let rhsPost)):
return lhsPost.id == rhsPost.id
case (.achievement(let lhsAch), .achievement(let rhsAch)):
return lhsAch.id == rhsAch.id
case (.settings, .settings),
(.profile, .profile),
(.chatHistory, .chatHistory),
(.addLocation, .addLocation):
return true
default:
return false
}
}
func hash(into hasher: inout Hasher) {
switch self {
case .conversation(let conv):
hasher.combine("conversation")
hasher.combine(conv.id)
case .topic(let topic):
hasher.combine("topic")
hasher.combine(topic.id)
case .location(let location):
hasher.combine("location")
hasher.combine(location.id)
case .post(let post):
hasher.combine("post")
hasher.combine(post.id)
case .achievement(let achievement):
hasher.combine("achievement")
hasher.combine(achievement.id)
case .settings:
hasher.combine("settings")
case .profile:
hasher.combine("profile")
case .chatHistory:
hasher.combine("chatHistory")
case .addLocation:
hasher.combine("addLocation")
}
}
}
@@ -0,0 +1,565 @@
//
// AnimationComponents.swift
// EmotionMuseum
//
// Created by on 2025/6/13.
//
import SwiftUI
// MARK: -
struct AnimationConfig {
static let springy = Animation.spring(response: 0.6, dampingFraction: 0.8)
static let bouncy = Animation.spring(response: 0.4, dampingFraction: 0.6)
static let smooth = Animation.easeInOut(duration: 0.3)
static let quick = Animation.easeOut(duration: 0.2)
static let slow = Animation.easeInOut(duration: 0.8)
static let elastic = Animation.interpolatingSpring(stiffness: 300, damping: 15)
}
// MARK: -
struct AnimatedAppearance: ViewModifier {
@State private var isVisible = false
let delay: Double
let animation: Animation
init(delay: Double = 0, animation: Animation = AnimationConfig.smooth) {
self.delay = delay
self.animation = animation
}
func body(content: Content) -> some View {
content
.scaleEffect(isVisible ? 1 : 0.8)
.opacity(isVisible ? 1 : 0)
.offset(y: isVisible ? 0 : 20)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
withAnimation(animation) {
isVisible = true
}
}
}
}
}
struct AnimatedCounter: ViewModifier {
@State private var currentValue: Int = 0
let targetValue: Int
let duration: Double
init(targetValue: Int, duration: Double = 1.0) {
self.targetValue = targetValue
self.duration = duration
}
func body(content: Content) -> some View {
Text("\(currentValue)")
.onAppear {
animateCounter()
}
.onChange(of: targetValue) { _ in
animateCounter()
}
}
private func animateCounter() {
currentValue = 0
let stepDuration = duration / Double(targetValue)
for i in 1...targetValue {
DispatchQueue.main.asyncAfter(deadline: .now() + stepDuration * Double(i)) {
withAnimation(.easeOut(duration: 0.1)) {
currentValue = i
}
}
}
}
}
// MARK: -
struct AnimatedButton<Content: View>: View {
let action: () -> Void
let content: () -> Content
@State private var isPressed = false
@State private var scale: CGFloat = 1.0
init(action: @escaping () -> Void, @ViewBuilder content: @escaping () -> Content) {
self.action = action
self.content = content
}
var body: some View {
Button(action: {
impactFeedback()
action()
}) {
content()
}
.scaleEffect(scale)
.onLongPressGesture(minimumDuration: 0, maximumDistance: .infinity) { isPressing in
withAnimation(AnimationConfig.quick) {
scale = isPressing ? 0.95 : 1.0
isPressed = isPressing
}
} perform: {
//
}
}
private func impactFeedback() {
let impactFeedback = UIImpactFeedbackGenerator(style: .light)
impactFeedback.impactOccurred()
}
}
// MARK: -
struct RippleEffect: View {
@State private var animationAmount = 1.0
let color: Color
let size: CGFloat
init(color: Color = .blue, size: CGFloat = 100) {
self.color = color
self.size = size
}
var body: some View {
Circle()
.fill(color.opacity(0.3))
.frame(width: size, height: size)
.scaleEffect(animationAmount)
.opacity(2 - animationAmount)
.animation(
Animation.easeOut(duration: 1.5)
.repeatForever(autoreverses: false),
value: animationAmount
)
.onAppear {
animationAmount = 2
}
}
}
// MARK: -
struct PulseEffect: ViewModifier {
@State private var isAnimating = false
let color: Color
let size: CGFloat
init(color: Color = .blue, size: CGFloat = 1.2) {
self.color = color
self.size = size
}
func body(content: Content) -> some View {
content
.scaleEffect(isAnimating ? size : 1.0)
.animation(
Animation.easeInOut(duration: 1.0)
.repeatForever(autoreverses: true),
value: isAnimating
)
.onAppear {
isAnimating = true
}
}
}
// MARK: -
struct ShakeEffect: ViewModifier {
@State private var shakeOffset: CGFloat = 0
let intensity: CGFloat
init(intensity: CGFloat = 10) {
self.intensity = intensity
}
func body(content: Content) -> some View {
content
.offset(x: shakeOffset)
.onAppear {
withAnimation(
Animation.easeInOut(duration: 0.1)
.repeatCount(4, autoreverses: true)
) {
shakeOffset = intensity
}
}
}
}
// MARK: -
struct FluidBackground: View {
@State private var animationOffset = 0.0
let colors: [Color]
init(colors: [Color] = [.blue, .purple, .pink]) {
self.colors = colors
}
var body: some View {
GeometryReader { geometry in
ZStack {
ForEach(0..<colors.count, id: \.self) { index in
Circle()
.fill(colors[index].opacity(0.3))
.frame(width: geometry.size.width * 0.8)
.offset(
x: sin(animationOffset + Double(index) * 2) * 50,
y: cos(animationOffset + Double(index) * 1.5) * 30
)
.blur(radius: 20)
}
}
}
.onAppear {
withAnimation(
Animation.linear(duration: 20)
.repeatForever(autoreverses: false)
) {
animationOffset = .pi * 2
}
}
}
}
// MARK: -
struct BounceListItem<Content: View>: View {
let content: () -> Content
@State private var isVisible = false
let index: Int
init(index: Int, @ViewBuilder content: @escaping () -> Content) {
self.index = index
self.content = content
}
var body: some View {
content()
.scaleEffect(isVisible ? 1 : 0.8)
.opacity(isVisible ? 1 : 0)
.offset(x: isVisible ? 0 : -50)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.1) {
withAnimation(AnimationConfig.bouncy) {
isVisible = true
}
}
}
}
}
// MARK: -
struct FlipCard<Front: View, Back: View>: View {
let front: () -> Front
let back: () -> Back
@State private var isFlipped = false
@State private var rotation = 0.0
init(@ViewBuilder front: @escaping () -> Front, @ViewBuilder back: @escaping () -> Back) {
self.front = front
self.back = back
}
var body: some View {
ZStack {
if !isFlipped {
front()
} else {
back()
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
}
}
.rotation3DEffect(.degrees(rotation), axis: (x: 0, y: 1, z: 0))
.onTapGesture {
withAnimation(.easeInOut(duration: 0.6)) {
rotation += 180
isFlipped.toggle()
}
}
}
}
// MARK: -
struct AnimatedProgressBar: View {
let progress: Double
let height: CGFloat
let backgroundColor: Color
let fillColor: Color
@State private var animatedProgress: Double = 0
init(
progress: Double,
height: CGFloat = 8,
backgroundColor: Color = Color.gray.opacity(0.3),
fillColor: Color = .blue
) {
self.progress = progress
self.height = height
self.backgroundColor = backgroundColor
self.fillColor = fillColor
}
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
Rectangle()
.fill(backgroundColor)
.frame(height: height)
.cornerRadius(height / 2)
Rectangle()
.fill(fillColor)
.frame(
width: geometry.size.width * animatedProgress,
height: height
)
.cornerRadius(height / 2)
.animation(AnimationConfig.smooth, value: animatedProgress)
}
}
.frame(height: height)
.onAppear {
withAnimation(.easeOut(duration: 1.0)) {
animatedProgress = progress
}
}
.onChange(of: progress) { newValue in
withAnimation(AnimationConfig.smooth) {
animatedProgress = newValue
}
}
}
}
// MARK: -
struct FloatingActionButton: View {
let action: () -> Void
let icon: String
let color: Color
@State private var isPressed = false
@State private var rotation = 0.0
init(icon: String, color: Color = .blue, action: @escaping () -> Void) {
self.icon = icon
self.color = color
self.action = action
}
var body: some View {
Button(action: {
withAnimation(AnimationConfig.bouncy) {
rotation += 360
}
action()
}) {
Image(systemName: icon)
.font(.title2)
.foregroundColor(.white)
.frame(width: 56, height: 56)
.background(color)
.clipShape(Circle())
.shadow(color: color.opacity(0.4), radius: 8, x: 0, y: 4)
}
.rotationEffect(.degrees(rotation))
.scaleEffect(isPressed ? 0.9 : 1.0)
.onLongPressGesture(minimumDuration: 0, maximumDistance: .infinity) { isPressing in
withAnimation(AnimationConfig.quick) {
isPressed = isPressing
}
} perform: {}
}
}
// MARK: -
struct ParticleSystem: View {
@State private var particles: [AnimationParticle] = []
let particleCount: Int
let colors: [Color]
init(particleCount: Int = 20, colors: [Color] = [.blue, .purple, .pink]) {
self.particleCount = particleCount
self.colors = colors
}
var body: some View {
GeometryReader { geometry in
ZStack {
ForEach(particles) { particle in
Circle()
.fill(particle.color)
.frame(width: particle.size, height: particle.size)
.position(particle.position)
.opacity(particle.opacity)
}
}
}
.onAppear {
createParticles()
animateParticles()
}
}
private func createParticles() {
particles = (0..<particleCount).map { _ in
AnimationParticle(
id: UUID(),
position: CGPoint(
x: CGFloat.random(in: 0...UIScreen.main.bounds.width),
y: CGFloat.random(in: 0...UIScreen.main.bounds.height)
),
size: CGFloat.random(in: 2...8),
color: colors.randomElement() ?? .blue,
opacity: Double.random(in: 0.3...1.0)
)
}
}
private func animateParticles() {
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
withAnimation(.linear(duration: 0.1)) {
for i in particles.indices {
particles[i].position.x += CGFloat.random(in: -2...2)
particles[i].position.y += CGFloat.random(in: -2...2)
particles[i].opacity = Double.random(in: 0.3...1.0)
}
}
}
}
}
struct AnimationParticle: Identifiable {
let id: UUID
var position: CGPoint
var size: CGFloat
var color: Color
var opacity: Double
}
// MARK: -
struct AnimatedGradientText: View {
let text: String
let colors: [Color]
@State private var animationOffset = 0.0
init(_ text: String, colors: [Color] = [.blue, .purple, .pink]) {
self.text = text
self.colors = colors
}
var body: some View {
Text(text)
.font(.title)
.fontWeight(.bold)
.foregroundColor(.primary)
.background(
LinearGradient(
colors: colors,
startPoint: .leading,
endPoint: .trailing
)
.rotationEffect(.degrees(animationOffset))
.mask(
Text(text)
.font(.title)
.fontWeight(.bold)
)
)
.onAppear {
withAnimation(
Animation.linear(duration: 3)
.repeatForever(autoreverses: false)
) {
animationOffset = 360
}
}
}
}
// MARK: -
extension View {
func animatedAppearance(delay: Double = 0, animation: Animation = AnimationConfig.smooth) -> some View {
modifier(AnimatedAppearance(delay: delay, animation: animation))
}
func pulseEffect(color: Color = .blue, size: CGFloat = 1.2) -> some View {
modifier(PulseEffect(color: color, size: size))
}
func shakeEffect(intensity: CGFloat = 10) -> some View {
modifier(ShakeEffect(intensity: intensity))
}
func bounceOnTap(scale: CGFloat = 0.95) -> some View {
scaleEffect(1.0)
.onTapGesture {
withAnimation(AnimationConfig.bouncy) {
//
}
}
}
func cardTransition() -> some View {
transition(
.asymmetric(
insertion: .scale(scale: 0.8).combined(with: .opacity),
removal: .scale(scale: 1.2).combined(with: .opacity)
)
)
}
func slideTransition(edge: Edge = .bottom) -> some View {
transition(.move(edge: edge).combined(with: .opacity))
}
func rotateTransition() -> some View {
transition(.scale(scale: 0.1).combined(with: .opacity))
}
}
// MARK: -
struct PageTransition<Content: View>: View {
let content: () -> Content
@State private var isVisible = false
let transitionType: TransitionType
enum TransitionType {
case slide, fade, scale, rotate
}
init(type: TransitionType = .fade, @ViewBuilder content: @escaping () -> Content) {
self.transitionType = type
self.content = content
}
var body: some View {
content()
.opacity(isVisible ? 1 : 0)
.scaleEffect(transitionType == .scale ? (isVisible ? 1 : 0.8) : 1)
.offset(y: transitionType == .slide ? (isVisible ? 0 : 50) : 0)
.rotationEffect(.degrees(transitionType == .rotate ? (isVisible ? 0 : 90) : 0))
.onAppear {
withAnimation(AnimationConfig.smooth) {
isVisible = true
}
}
}
}
// MARK: -
#Preview("动画组件") {
VStack(spacing: 20) {
AnimatedGradientText("情绪博物馆")
AnimatedProgressBar(progress: 0.7)
.frame(height: 10)
FloatingActionButton(icon: "plus") {
print("FAB tapped")
}
RippleEffect(color: .blue, size: 60)
}
.padding()
}
@@ -0,0 +1,395 @@
//
// ChakraHealingView.swift
// EmotionMuseum
//
// Created by on 2025/6/13.
//
import SwiftUI
import AVFoundation
struct ChakraHealingView: View {
let chakra: ChakraType
let onComplete: () -> Void
@State private var isPlaying = false
@State private var progress: Double = 0.0
@State private var timer: Timer? = nil
@State private var breathingPhase: BreathingPhase = .inhale
@State private var breathCount = 0
@State private var beforeScore: Int? = nil
@State private var afterScore: Int? = nil
@State private var showingCompletionSheet = false
//
@State private var particles: [Particle] = []
var body: some View {
ZStack {
//
chakra.color.opacity(0.2)
.ignoresSafeArea()
//
ZStack {
//
Circle()
.fill(
RadialGradient(
gradient: Gradient(colors: [chakra.color, chakra.color.opacity(0.5)]),
center: .center,
startRadius: 10,
endRadius: 150
)
)
.frame(width: breathingPhase == .inhale ? 200 : 150,
height: breathingPhase == .inhale ? 200 : 150)
.opacity(0.7)
.animation(.easeInOut(duration: breathingPhase == .inhale ? 4 : 4), value: breathingPhase)
//
ForEach(particles) { particle in
Circle()
.fill(chakra.color.opacity(particle.opacity))
.frame(width: particle.size, height: particle.size)
.position(particle.position)
}
}
//
VStack(spacing: 30) {
//
VStack(spacing: 8) {
Text(chakra.rawValue)
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(chakra.color)
Text(chakra.description)
.font(.subheadline)
.foregroundColor(.secondary)
}
.padding(.top, 40)
Spacer()
//
Text(breathingPhase == .inhale ? "吸气..." : "呼气...")
.font(.title)
.fontWeight(.medium)
.foregroundColor(chakra.color)
.opacity(isPlaying ? 1 : 0)
Spacer()
//
VStack(spacing: 20) {
//
ProgressView(value: progress)
.progressViewStyle(LinearProgressViewStyle(tint: chakra.color))
.padding(.horizontal)
//
HStack(spacing: 40) {
Button(action: {
if beforeScore == nil {
//
showBeforeScorePrompt()
} else {
togglePlayback()
}
}) {
Image(systemName: isPlaying ? "pause.circle.fill" : "play.circle.fill")
.font(.system(size: 60))
.foregroundColor(chakra.color)
}
Button(action: {
completeSession()
}) {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 40))
.foregroundColor(.gray)
}
}
.padding(.bottom, 30)
}
.background(
Rectangle()
.fill(Color(.systemBackground))
.cornerRadius(30, corners: [.topLeft, .topRight])
.shadow(color: Color.black.opacity(0.1), radius: 10, x: 0, y: -5)
)
}
}
.onAppear {
setupParticles()
}
.onDisappear {
stopSession()
}
.sheet(isPresented: $showingCompletionSheet) {
SessionCompletionView(chakra: chakra, beforeScore: beforeScore ?? 5, afterScore: afterScore ?? 5) {
onComplete()
}
}
}
// MARK: -
private func setupParticles() {
//
for _ in 0..<20 {
particles.append(Particle.random(in: UIScreen.main.bounds, color: chakra.color))
}
}
private func togglePlayback() {
isPlaying.toggle()
if isPlaying {
startSession()
} else {
pauseSession()
}
}
private func startSession() {
//
startBreathingAnimation()
//
animateParticles()
//
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
if progress < 1.0 {
progress += 0.0005 // 30
} else {
completeSession()
}
}
}
private func pauseSession() {
timer?.invalidate()
}
private func stopSession() {
timer?.invalidate()
timer = nil
}
private func startBreathingAnimation() {
//
Timer.scheduledTimer(withTimeInterval: 4, repeats: true) { _ in
withAnimation {
breathingPhase = breathingPhase == .inhale ? .exhale : .inhale
}
breathCount += 1
if breathCount >= 100 {
completeSession()
}
}
}
private func animateParticles() {
//
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
for i in 0..<particles.count {
if i < particles.count {
particles[i].move()
//
if !UIScreen.main.bounds.contains(particles[i].position) {
particles[i] = Particle.random(in: UIScreen.main.bounds, color: chakra.color)
}
}
}
}
}
private func showBeforeScorePrompt() {
//
//
beforeScore = 5
togglePlayback()
}
private func completeSession() {
stopSession()
isPlaying = false
//
afterScore = 8
//
showingCompletionSheet = true
}
}
// MARK: -
enum BreathingPhase {
case inhale
case exhale
}
// MARK: -
struct Particle: Identifiable {
let id = UUID()
var position: CGPoint
var direction: CGVector
var speed: CGFloat
var size: CGFloat
var opacity: Double
mutating func move() {
position.x += direction.dx * speed
position.y += direction.dy * speed
opacity = max(0, opacity - 0.001)
if opacity <= 0.1 {
opacity = Double.random(in: 0.3...0.7)
}
}
static func random(in rect: CGRect, color: Color) -> Particle {
let position = CGPoint(
x: CGFloat.random(in: rect.minX...rect.maxX),
y: CGFloat.random(in: rect.minY...rect.maxY)
)
let angle = CGFloat.random(in: 0...(2 * .pi))
let direction = CGVector(
dx: cos(angle),
dy: sin(angle)
)
return Particle(
position: position,
direction: direction,
speed: CGFloat.random(in: 0.5...2.0),
size: CGFloat.random(in: 3...8),
opacity: Double.random(in: 0.3...0.7)
)
}
}
// MARK: -
struct SessionCompletionView: View {
let chakra: ChakraType
let beforeScore: Int
let afterScore: Int
let onDismiss: () -> Void
var body: some View {
VStack(spacing: 20) {
//
Text("疗愈完成")
.font(.title)
.fontWeight(.bold)
//
HStack(spacing: 12) {
Circle()
.fill(chakra.color)
.frame(width: 30, height: 30)
Text(chakra.rawValue)
.font(.headline)
}
Divider()
//
VStack(alignment: .leading, spacing: 16) {
Text("疗愈效果")
.font(.headline)
HStack(spacing: 30) {
VStack {
Text("疗愈前")
.font(.subheadline)
.foregroundColor(.secondary)
Text("\(beforeScore)")
.font(.title)
.fontWeight(.bold)
}
Image(systemName: "arrow.right")
.font(.title2)
.foregroundColor(.secondary)
VStack {
Text("疗愈后")
.font(.subheadline)
.foregroundColor(.secondary)
Text("\(afterScore)")
.font(.title)
.fontWeight(.bold)
.foregroundColor(.green)
}
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(12)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal)
//
VStack(alignment: .leading, spacing: 12) {
Text("反馈")
.font(.headline)
Text("你的\(chakra.rawValue)能量已得到提升,情绪状态有所改善。建议每天进行一次疗愈,保持能量平衡。")
.font(.body)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal)
Spacer()
//
Button(action: {
onDismiss()
}) {
Text("返回")
.font(.headline)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding()
.background(chakra.color)
.cornerRadius(12)
}
.padding(.horizontal)
.padding(.bottom)
}
.padding(.top, 40)
}
}
// MARK: -
extension View {
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
clipShape(RoundedCorner(radius: radius, corners: corners))
}
}
struct RoundedCorner: Shape {
var radius: CGFloat = .infinity
var corners: UIRectCorner = .allCorners
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
return Path(path.cgPath)
}
}
#Preview {
ChakraHealingView(chakra: .heart, onComplete: {})
}
@@ -0,0 +1,115 @@
//
// ConversationPreviewCard.swift
// EmotionMuseum
//
// Created by on 2025/7/5.
//
import SwiftUI
struct ConversationPreviewCard: View {
let conversation: Conversation
let onTap: () -> Void
var body: some View {
Button(action: onTap) {
HStack(spacing: 12) {
//
Image(systemName: "message.circle.fill")
.font(.title2)
.foregroundColor(Color("AccentColor"))
VStack(alignment: .leading, spacing: 4) {
//
Text(conversation.title)
.font(.headline)
.foregroundColor(Color("PrimaryText"))
.lineLimit(1)
//
if let lastMessage = conversation.lastMessage {
Text(lastMessage.content)
.font(.subheadline)
.foregroundColor(Color("SecondaryText"))
.lineLimit(2)
} else if let summary = conversation.summary {
Text(summary)
.font(.subheadline)
.foregroundColor(Color("SecondaryText"))
.lineLimit(2)
} else {
Text("点击查看对话详情")
.font(.subheadline)
.foregroundColor(Color("TertiaryText"))
}
//
HStack {
Text(conversation.startTime.timeAgo)
.font(.caption)
.foregroundColor(Color("TertiaryText"))
Spacer()
Text("\(conversation.messageCount) 条消息")
.font(.caption)
.foregroundColor(Color("TertiaryText"))
}
}
Spacer()
//
if let emotion = conversation.emotionAnalysis?.primaryEmotion {
VStack {
Text(emotion.emoji)
.font(.title2)
Text(emotion.rawValue)
.font(.caption2)
.foregroundColor(emotion.color)
}
}
}
.padding()
.background(
RoundedRectangle(cornerRadius: 12)
.fill(Color("CardBackground"))
.shadow(
color: Color.black.opacity(0.05),
radius: 3,
x: 0,
y: 2
)
)
}
.buttonStyle(PlainButtonStyle())
}
}
#Preview {
ConversationPreviewCard(
conversation: Conversation(
userId: UUID(),
title: "今天的心情分享",
messages: [
Message(
conversationId: UUID(),
content: "我今天感觉有点焦虑,不知道该怎么办。",
type: .text,
sender: .user
)
],
emotionAnalysis: EmotionAnalysis(
primaryEmotion: .anxiety,
emotionIntensity: 0.7,
emotionTrend: .stable,
keywords: ["焦虑", "压力"],
aiInsights: "用户表现出一定程度的焦虑情绪"
)
)
) {
print("Tapped conversation")
}
.padding()
}
@@ -0,0 +1,226 @@
//
// ExploreView.swift
// EmotionMuseum
//
// Created by on 2025/6/13.
//
import SwiftUI
import MapKit
import CoreLocation
// MARK: -
struct ExploreView: View {
@EnvironmentObject var mockDataManager: MockDataManager
@EnvironmentObject var navigationManager: NavigationManager
@State private var selectedLocation: LocationPin?
@State private var showingLocationDetail = false
@State private var showingCommunityFeed = false
@State private var savedLocations: [LocationPin] = []
@State private var showingMapView = true
@State private var showingLocationPicker = false
@State private var shouldMoveToLocationPin = false
var body: some View {
NavigationView {
VStack(spacing: 0) {
//
HStack {
Button(action: {
showingMapView = true
showingCommunityFeed = false
}) {
HStack {
Image(systemName: "map")
Text("情绪地图")
}
.foregroundColor(showingMapView ? .white : .primary)
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(showingMapView ? Color.blue : Color.clear)
.cornerRadius(20)
}
Spacer()
Button(action: {
showingMapView = false
showingCommunityFeed = true
}) {
HStack {
Image(systemName: "person.3")
Text("社区分享")
}
.foregroundColor(showingCommunityFeed ? .white : .primary)
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(showingCommunityFeed ? Color.green : Color.clear)
.cornerRadius(20)
}
}
.padding()
.background(Color(.systemGray6))
//
if showingMapView {
mapViewSection
} else {
communityFeedSection
}
}
.navigationTitle("探索")
.navigationBarTitleDisplayMode(.large)
.onAppear {
loadSavedLocations()
}
}
.sheet(isPresented: $showingLocationDetail) {
if let location = selectedLocation {
LocationDetailView(location: location)
}
}
}
// MARK: -
private var mapViewSection: some View {
VStack {
//
ZStack {
MapView()
.frame(height: 300)
.cornerRadius(12)
.padding()
}
//
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("推荐地点")
.font(.headline)
Spacer()
Button("查看全部") {
//
}
.font(.caption)
.foregroundColor(.blue)
}
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(Array(savedLocations.prefix(5))) { location in
LocationCard(location: location) {
selectedLocation = location
showingLocationDetail = true
}
}
}
.padding(.horizontal)
}
}
.padding()
Spacer()
}
}
// MARK: -
private var communityFeedSection: some View {
VStack {
ScrollView {
LazyVStack(spacing: 16) {
ForEach(mockDataManager.communityPosts) { post in
CommunityPostCard(post: post)
}
}
.padding()
}
}
}
// MARK: -
private func loadSavedLocations() {
//
savedLocations = [
LocationPin(
coordinate: Coordinate(latitude: 39.9042, longitude: 116.4074),
title: "天安门广场",
description: "庄严肃穆的历史地标",
type: .popular,
emotionTags: [.neutral],
category: .other
),
LocationPin(
coordinate: Coordinate(latitude: 39.9163, longitude: 116.3972),
title: "故宫博物院",
description: "深厚的历史文化底蕴",
type: .aiRecommended,
emotionTags: [.surprise],
category: .museum
),
LocationPin(
coordinate: Coordinate(latitude: 39.9925, longitude: 116.3135),
title: "颐和园",
description: "宁静优美的皇家园林",
type: .personal,
emotionTags: [.contentment],
category: .garden
),
LocationPin(
coordinate: Coordinate(latitude: 40.0090, longitude: 116.3348),
title: "圆明园",
description: "历史的见证与思考",
type: .community,
emotionTags: [.sadness],
category: .park
),
LocationPin(
coordinate: Coordinate(latitude: 39.9059, longitude: 116.3913),
title: "北海公园",
description: "古典园林的宁静之美",
type: .popular,
emotionTags: [.joy],
category: .park
)
]
}
}
// MARK: -
struct LocationCard: View {
let location: LocationPin
let onTap: () -> Void
var body: some View {
Button(action: onTap) {
VStack(alignment: .leading, spacing: 8) {
//
Rectangle()
.fill(Color(.systemGray5))
.frame(width: 120, height: 80)
.overlay(
Image(systemName: location.category.icon)
.font(.title)
.foregroundColor(.gray)
)
.cornerRadius(8)
VStack(alignment: .leading, spacing: 4) {
Text(location.title)
.font(.caption)
.fontWeight(.medium)
.lineLimit(1)
Text(location.description)
.font(.caption2)
.foregroundColor(.secondary)
.lineLimit(2)
}
.frame(width: 120, alignment: .leading)
}
}
.buttonStyle(PlainButtonStyle())
}
}
#Preview {
ExploreView()
.environmentObject(MockDataManager.shared)
.environmentObject(NavigationManager())
}
@@ -0,0 +1,808 @@
//
// GrowthView.swift
// EmotionMuseum
//
// Created by on 2025/6/13.
//
import SwiftUI
// MARK: -
struct GrowthView: View {
@StateObject private var themeManager = ThemeManager()
@State private var showingTopicDetail = false
@State private var selectedTopic: GrowthTopic?
@State private var showingInsights = false
@State private var showingRadarChart = false
@State private var loadingState: LoadingState = .idle
@State private var isInitialLoading = true
var body: some View {
NavigationView {
LoadingStateView(loadingState: isInitialLoading ? .loading : .loaded) {
ScrollView {
VStack(spacing: 24) {
//
EmotionalInsightCard {
showingInsights = true
}
.transition(.scale(scale: 0.8).combined(with: .opacity))
//
UserProfileRadarCard {
showingRadarChart = true
}
.transition(.scale(scale: 0.8).combined(with: .opacity))
//
GrowthTopicsSection { topic in
selectedTopic = topic
showingTopicDetail = true
}
.transition(.scale(scale: 0.8).combined(with: .opacity))
//
TodayActionCard()
.transition(.scale(scale: 0.8).combined(with: .opacity))
//
GrowthTimelineCard()
.transition(.scale(scale: 0.8).combined(with: .opacity))
}
.padding(.horizontal)
.padding(.vertical)
}
.refreshable {
await refreshData()
}
} loadingView: {
AnyView(growthViewSkeleton)
}
.navigationTitle("成长治愈")
.navigationBarTitleDisplayMode(.large)
}
.environmentObject(themeManager)
.preferredColorScheme(themeManager.systemFollowsDeviceTheme ? nil : (themeManager.isDarkMode ? .dark : .light))
.sheet(isPresented: $showingTopicDetail) {
if let topic = selectedTopic {
TopicDetailView(topic: topic)
.environmentObject(themeManager)
}
}
.sheet(isPresented: $showingInsights) {
EmotionalInsightsView()
.environmentObject(themeManager)
}
.sheet(isPresented: $showingRadarChart) {
NavigationView {
VStack {
Text("用户画像雷达图")
.font(.title)
.padding()
Text("这里显示用户的多维度能力雷达图")
.foregroundColor(.secondary)
.padding()
Spacer()
}
.navigationTitle("用户画像")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("关闭") {
showingRadarChart = false
}
}
}
}
.environmentObject(themeManager)
}
.onAppear {
simulateInitialLoading()
}
}
// MARK: -
private func simulateInitialLoading() {
loadingState = .loading
//
DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) {
withAnimation(.easeOut(duration: 0.8)) {
isInitialLoading = false
loadingState = .loaded
}
}
}
private func refreshData() async {
loadingState = .loading
//
try? await Task.sleep(nanoseconds: 800_000_000) // 0.8
DispatchQueue.main.async {
withAnimation(.easeOut(duration: 0.6)) {
loadingState = .loaded
}
}
}
// MARK: -
private var growthViewSkeleton: some View {
ScrollView {
VStack(spacing: 24) {
//
insightCardSkeleton
//
radarCardSkeleton
//
topicsGridSkeleton
//
actionCardSkeleton
//
timelineCardSkeleton
}
.padding(.horizontal)
.padding(.vertical)
}
.background(Color.theme.background)
}
private var insightCardSkeleton: some View {
VStack(alignment: .leading, spacing: 16) {
HStack {
VStack(alignment: .leading, spacing: 4) {
SkeletonView(width: 80, height: 20, cornerRadius: 6)
SkeletonView(width: 140, height: 12, cornerRadius: 3)
}
Spacer()
SkeletonView(width: 24, height: 24, cornerRadius: 12)
}
VStack(spacing: 12) {
ForEach(0..<3, id: \.self) { _ in
HStack(spacing: 12) {
SkeletonView(width: 20, height: 16, cornerRadius: 4)
SkeletonView(width: 80, height: 14, cornerRadius: 3)
Spacer()
SkeletonView(width: 60, height: 14, cornerRadius: 3)
}
}
}
HStack {
SkeletonView(width: 100, height: 12, cornerRadius: 3)
Spacer()
SkeletonView(width: 12, height: 12, cornerRadius: 3)
}
}
.padding()
.background(Color.theme.cardBackground)
.cornerRadius(16)
}
private var radarCardSkeleton: some View {
VStack(spacing: 16) {
HStack {
SkeletonView(width: 80, height: 20, cornerRadius: 6)
Spacer()
SkeletonView(width: 24, height: 24, cornerRadius: 12)
}
VStack(spacing: 8) {
ForEach(0..<5, id: \.self) { _ in
HStack {
SkeletonView(width: 60, height: 12, cornerRadius: 3)
SkeletonView(height: 8, cornerRadius: 4)
SkeletonView(width: 40, height: 12, cornerRadius: 3)
}
}
}
HStack {
SkeletonView(width: 120, height: 12, cornerRadius: 3)
Spacer()
SkeletonView(width: 12, height: 12, cornerRadius: 3)
}
}
.padding()
.background(Color.theme.cardBackground)
.cornerRadius(16)
}
private var topicsGridSkeleton: some View {
VStack(alignment: .leading, spacing: 16) {
HStack {
SkeletonView(width: 80, height: 20, cornerRadius: 6)
Spacer()
SkeletonView(width: 60, height: 12, cornerRadius: 3)
}
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 12) {
ForEach(0..<4, id: \.self) { _ in
VStack(alignment: .leading, spacing: 12) {
HStack {
SkeletonView(width: 20, height: 20, cornerRadius: 10)
Spacer()
SkeletonView(width: 30, height: 12, cornerRadius: 3)
}
VStack(alignment: .leading, spacing: 4) {
SkeletonView(width: 100, height: 14, cornerRadius: 3)
SkeletonView(width: 120, height: 12, cornerRadius: 3)
SkeletonView(width: 80, height: 12, cornerRadius: 3)
}
VStack(spacing: 4) {
HStack {
SkeletonView(width: 30, height: 10, cornerRadius: 2)
Spacer()
SkeletonView(width: 30, height: 10, cornerRadius: 2)
}
SkeletonView(height: 4, cornerRadius: 2)
}
}
.padding()
.background(Color.theme.cardBackground)
.cornerRadius(12)
}
}
}
}
private var actionCardSkeleton: some View {
VStack(alignment: .leading, spacing: 16) {
HStack {
SkeletonView(width: 80, height: 20, cornerRadius: 6)
Spacer()
SkeletonView(width: 24, height: 24, cornerRadius: 12)
}
VStack(spacing: 12) {
ForEach(0..<3, id: \.self) { _ in
HStack(spacing: 12) {
SkeletonView(width: 24, height: 24, cornerRadius: 12)
VStack(alignment: .leading, spacing: 2) {
SkeletonView(width: 160, height: 14, cornerRadius: 3)
HStack(spacing: 8) {
SkeletonView(width: 60, height: 16, cornerRadius: 8)
SkeletonView(width: 40, height: 12, cornerRadius: 3)
}
}
Spacer()
}
}
}
}
.padding()
.background(Color.theme.cardBackground)
.cornerRadius(16)
}
private var timelineCardSkeleton: some View {
VStack(alignment: .leading, spacing: 16) {
SkeletonView(width: 80, height: 20, cornerRadius: 6)
VStack(alignment: .leading, spacing: 12) {
ForEach(0..<4, id: \.self) { _ in
HStack(spacing: 12) {
VStack {
SkeletonView(width: 8, height: 8, cornerRadius: 4)
SkeletonView(width: 1, height: 20, cornerRadius: 1)
}
VStack(alignment: .leading, spacing: 2) {
HStack {
SkeletonView(width: 100, height: 14, cornerRadius: 3)
Spacer()
SkeletonView(width: 40, height: 12, cornerRadius: 3)
}
SkeletonView(width: 180, height: 12, cornerRadius: 3)
}
}
}
}
}
.padding()
.background(Color.theme.cardBackground)
.cornerRadius(16)
}
}
// MARK: -
struct EmotionalInsightCard: View {
let onTap: () -> Void
var body: some View {
Button(action: onTap) {
VStack(alignment: .leading, spacing: 16) {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text("情绪洞察")
.font(.headline)
.fontWeight(.semibold)
.foregroundColor(Color.theme.primaryText)
Text("基于你的对话记录生成")
.font(.caption)
.foregroundColor(Color.theme.secondaryText)
}
Spacer()
Image(systemName: "brain.head.profile")
.font(.title2)
.foregroundColor(.purple)
}
VStack(alignment: .leading, spacing: 12) {
InsightRow(
icon: "heart.fill",
title: "主要情绪状态",
value: "平静自省",
color: .blue
)
InsightRow(
icon: "target",
title: "成长焦点",
value: "人际关系",
color: .green
)
InsightRow(
icon: "chart.line.uptrend.xyaxis",
title: "进步指数",
value: "↗️ 稳步提升",
color: .orange
)
}
HStack {
Text("点击查看详细分析")
.font(.caption)
.foregroundColor(Color.theme.secondaryText)
Spacer()
Image(systemName: "chevron.right")
.font(.caption)
.foregroundColor(Color.theme.secondaryText)
}
}
.padding()
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color(.systemGray6))
)
}
.buttonStyle(PlainButtonStyle())
}
}
struct InsightRow: View {
let icon: String
let title: String
let value: String
let color: Color
var body: some View {
HStack(spacing: 12) {
Image(systemName: icon)
.font(.subheadline)
.foregroundColor(color)
.frame(width: 20)
Text(title)
.font(.subheadline)
.foregroundColor(Color.theme.secondaryText)
Spacer()
Text(value)
.font(.subheadline)
.fontWeight(.medium)
}
}
}
// MARK: -
struct UserProfileRadarCard: View {
let onTap: () -> Void
var body: some View {
Button(action: onTap) {
VStack(spacing: 16) {
HStack {
Text("成长画像")
.font(.headline)
.fontWeight(.semibold)
Spacer()
Image(systemName: "person.crop.circle.badge.checkmark")
.font(.title2)
.foregroundColor(.green)
}
//
VStack(spacing: 8) {
ProfileDimensionRow(name: "自我感知", value: 0.8, color: .blue)
ProfileDimensionRow(name: "情绪韧性", value: 0.7, color: .purple)
ProfileDimensionRow(name: "行动力", value: 0.6, color: .orange)
ProfileDimensionRow(name: "共情力", value: 0.9, color: .green)
ProfileDimensionRow(name: "生活热度", value: 0.7, color: .red)
}
HStack {
Text("点击查看完整雷达图")
.font(.caption)
.foregroundColor(.secondary)
Spacer()
Image(systemName: "chevron.right")
.font(.caption)
.foregroundColor(.secondary)
}
}
.padding()
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color(.systemGray6))
)
}
.buttonStyle(PlainButtonStyle())
}
}
struct ProfileDimensionRow: View {
let name: String
let value: Double
let color: Color
var body: some View {
HStack {
Text(name)
.font(.caption)
.foregroundColor(.primary)
.frame(width: 60, alignment: .leading)
ProgressView(value: value)
.progressViewStyle(LinearProgressViewStyle(tint: color))
Text("\(Int(value * 100))%")
.font(.caption)
.fontWeight(.medium)
.foregroundColor(color)
.frame(width: 40, alignment: .trailing)
}
}
}
// MARK: -
struct GrowthTopicsSection: View {
let onTopicTap: (GrowthTopic) -> Void
var body: some View {
VStack(alignment: .leading, spacing: 16) {
HStack {
Text("成长课题")
.font(.headline)
.fontWeight(.semibold)
Spacer()
Text("3个进行中")
.font(.caption)
.foregroundColor(.secondary)
}
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 12) {
ForEach(sampleGrowthTopics) { topic in
TopicCard(topic: topic) {
onTopicTap(topic)
}
}
}
}
}
}
struct TopicCard: View {
let topic: GrowthTopic
let onTap: () -> Void
var body: some View {
Button(action: onTap) {
VStack(alignment: .leading, spacing: 12) {
HStack {
Image(systemName: topic.icon)
.font(.title3)
.foregroundColor(topic.color)
Spacer()
Text("Lv.\(topic.level)")
.font(.caption)
.fontWeight(.bold)
.foregroundColor(topic.color)
}
VStack(alignment: .leading, spacing: 4) {
Text(topic.title)
.font(.subheadline)
.fontWeight(.medium)
.multilineTextAlignment(.leading)
Text(topic.description)
.font(.caption)
.foregroundColor(.secondary)
.lineLimit(2)
.multilineTextAlignment(.leading)
}
VStack(spacing: 4) {
HStack {
Text("进度")
.font(.caption2)
.foregroundColor(.secondary)
Spacer()
Text("\(Int(topic.progress * 100))%")
.font(.caption2)
.fontWeight(.medium)
}
ProgressView(value: topic.progress)
.progressViewStyle(LinearProgressViewStyle(tint: topic.color))
}
}
.padding()
.background(
RoundedRectangle(cornerRadius: 12)
.fill(Color(.systemGray6))
)
}
.buttonStyle(PlainButtonStyle())
}
}
// MARK: -
struct TodayActionCard: View {
var body: some View {
VStack(alignment: .leading, spacing: 16) {
HStack {
Text("今日推荐")
.font(.headline)
.fontWeight(.semibold)
Spacer()
Image(systemName: "lightbulb.fill")
.font(.title3)
.foregroundColor(.yellow)
}
VStack(spacing: 12) {
ActionItemView(
title: "与朋友分享一件开心的事",
category: "人际关系",
duration: "5分钟",
color: .blue
)
ActionItemView(
title: "写下今天的三个感恩点",
category: "自我觉察",
duration: "10分钟",
color: .green
)
ActionItemView(
title: "深呼吸练习",
category: "情绪调节",
duration: "3分钟",
color: .purple
)
}
}
.padding()
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color(.systemGray6))
)
}
}
struct ActionItemView: View {
let title: String
let category: String
let duration: String
let color: Color
@State private var isCompleted = false
var body: some View {
HStack(spacing: 12) {
Button(action: { isCompleted.toggle() }) {
Image(systemName: isCompleted ? "checkmark.circle.fill" : "circle")
.font(.title3)
.foregroundColor(isCompleted ? color : .gray)
}
VStack(alignment: .leading, spacing: 2) {
Text(title)
.font(.subheadline)
.fontWeight(.medium)
.strikethrough(isCompleted)
HStack(spacing: 8) {
Text(category)
.font(.caption)
.padding(.horizontal, 8)
.padding(.vertical, 2)
.background(color.opacity(0.2))
.foregroundColor(color)
.cornerRadius(8)
Text(duration)
.font(.caption)
.foregroundColor(.secondary)
}
}
Spacer()
}
.opacity(isCompleted ? 0.6 : 1.0)
.animation(.easeInOut(duration: 0.2), value: isCompleted)
}
}
// MARK: -
struct GrowthTimelineCard: View {
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("成长历程")
.font(.headline)
.fontWeight(.semibold)
VStack(alignment: .leading, spacing: 12) {
TimelineItem(
date: "今天",
title: "完成情绪记录",
description: "记录了平静的心情状态",
color: .blue,
isRecent: true
)
TimelineItem(
date: "昨天",
title: "课题升级",
description: "人际关系课题提升到Lv.2",
color: .green,
isRecent: true
)
TimelineItem(
date: "3天前",
title: "解锁新课题",
description: "开始学习情绪调节技巧",
color: .purple,
isRecent: false
)
TimelineItem(
date: "1周前",
title: "达成里程碑",
description: "连续7天完成情绪记录",
color: .orange,
isRecent: false
)
}
}
.padding()
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color(.systemGray6))
)
}
}
struct TimelineItem: View {
let date: String
let title: String
let description: String
let color: Color
let isRecent: Bool
var body: some View {
HStack(spacing: 12) {
VStack {
Circle()
.fill(color)
.frame(width: 8, height: 8)
if !isRecent {
Rectangle()
.fill(Color(.systemGray4))
.frame(width: 1, height: 20)
}
}
VStack(alignment: .leading, spacing: 2) {
HStack {
Text(title)
.font(.subheadline)
.fontWeight(.medium)
Spacer()
Text(date)
.font(.caption)
.foregroundColor(.secondary)
}
Text(description)
.font(.caption)
.foregroundColor(.secondary)
.lineLimit(2)
}
}
.opacity(isRecent ? 1.0 : 0.7)
}
}
// MARK: - GrowthTopic UI
extension GrowthTopic {
var icon: String {
category.icon
}
var color: Color {
category.color
}
}
// MARK: -
let sampleGrowthTopics = [
GrowthTopic(
title: "人际关系边界",
description: "学习建立健康的人际关系边界",
category: .relationships,
difficulty: .intermediate,
progress: 0.7,
level: 2
),
GrowthTopic(
title: "情绪调节技能",
description: "掌握情绪识别和调节的核心技能",
category: .emotionRegulation,
difficulty: .beginner,
progress: 0.3,
level: 1
),
GrowthTopic(
title: "深度自我认知",
description: "提升对内在世界的认知和理解",
category: .selfAwareness,
difficulty: .advanced,
progress: 0.9,
level: 3
),
GrowthTopic(
title: "行动力提升",
description: "培养执行力和目标达成能力",
category: .lifeGoals,
difficulty: .beginner,
progress: 0.4,
level: 1
)
]
@@ -0,0 +1,311 @@
//
// HealingView.swift
// EmotionMuseum
//
// Created by on 2025/6/13.
//
import SwiftUI
import AVFoundation
struct HealingView: View {
@State private var selectedChakra: ChakraType? = nil
@State private var showingHealingSession = false
@State private var chakraStates: [ChakraType: ChakraState] = [:]
var body: some View {
NavigationView {
ZStack {
//
LinearGradient(
gradient: Gradient(colors: [.purple.opacity(0.1), .blue.opacity(0.1)]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.ignoresSafeArea()
ScrollView {
VStack(spacing: 30) {
//
VStack(spacing: 8) {
Text("脉轮疗愈")
.font(.largeTitle)
.fontWeight(.bold)
Text("点击脉轮区域开始疗愈之旅")
.font(.subheadline)
.foregroundColor(.secondary)
}
.padding(.top)
//
VStack(spacing: 0) {
//
ChakraButton(
chakra: .crown,
state: chakraStates[.crown] ?? .normal,
action: { selectChakra(.crown) }
)
.offset(y: 10)
Spacer().frame(height: 20)
//
ChakraButton(
chakra: .thirdEye,
state: chakraStates[.thirdEye] ?? .normal,
action: { selectChakra(.thirdEye) }
)
Spacer().frame(height: 30)
//
ChakraButton(
chakra: .throat,
state: chakraStates[.throat] ?? .weak,
action: { selectChakra(.throat) }
)
Spacer().frame(height: 40)
//
ChakraButton(
chakra: .heart,
state: chakraStates[.heart] ?? .normal,
action: { selectChakra(.heart) }
)
Spacer().frame(height: 40)
//
ChakraButton(
chakra: .solarPlexus,
state: chakraStates[.solarPlexus] ?? .normal,
action: { selectChakra(.solarPlexus) }
)
Spacer().frame(height: 40)
//
ChakraButton(
chakra: .sacral,
state: chakraStates[.sacral] ?? .weak,
action: { selectChakra(.sacral) }
)
Spacer().frame(height: 40)
//
ChakraButton(
chakra: .root,
state: chakraStates[.root] ?? .normal,
action: { selectChakra(.root) }
)
}
.frame(maxWidth: 200)
//
VStack(alignment: .leading, spacing: 12) {
Text("脉轮状态说明")
.font(.headline)
HStack(spacing: 16) {
HStack {
Circle()
.fill(Color.green)
.frame(width: 12, height: 12)
Text("健康")
.font(.caption)
}
HStack {
Circle()
.fill(Color.orange.opacity(0.6))
.frame(width: 12, height: 12)
Text("疲弱")
.font(.caption)
}
HStack {
Circle()
.fill(Color.red.opacity(0.6))
.frame(width: 12, height: 12)
Text("受阻")
.font(.caption)
}
}
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(12)
.padding(.horizontal)
//
VStack(alignment: .leading, spacing: 12) {
Text("快速疗愈")
.font(.headline)
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 12) {
QuickHealingCard(title: "全身平衡", icon: "figure.mind.and.body", color: .purple)
QuickHealingCard(title: "情绪释放", icon: "heart.fill", color: .pink)
QuickHealingCard(title: "能量充电", icon: "bolt.fill", color: .yellow)
QuickHealingCard(title: "深度放松", icon: "moon.fill", color: .blue)
}
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(12)
.padding(.horizontal)
}
.padding(.bottom, 30)
}
}
.navigationBarHidden(true)
}
.sheet(item: $selectedChakra) { chakra in
ChakraHealingView(chakra: chakra) {
//
updateChakraState(chakra, newState: .normal)
}
}
.onAppear {
initializeChakraStates()
}
}
private func selectChakra(_ chakra: ChakraType) {
selectedChakra = chakra
}
private func initializeChakraStates() {
//
chakraStates = [
.root: .normal,
.sacral: .weak,
.solarPlexus: .normal,
.heart: .normal,
.throat: .weak,
.thirdEye: .normal,
.crown: .normal
]
}
private func updateChakraState(_ chakra: ChakraType, newState: ChakraState) {
withAnimation {
chakraStates[chakra] = newState
}
}
}
// MARK: -
extension ChakraType: Identifiable {
var id: String { rawValue }
}
// MARK: -
enum ChakraState {
case normal //
case weak //
case blocked //
var opacity: Double {
switch self {
case .normal: return 1.0
case .weak: return 0.6
case .blocked: return 0.3
}
}
}
// MARK: -
struct ChakraButton: View {
let chakra: ChakraType
let state: ChakraState
let action: () -> Void
@State private var isAnimating = false
var body: some View {
Button(action: action) {
ZStack {
//
Circle()
.fill(
RadialGradient(
gradient: Gradient(colors: [chakra.color.opacity(0.3), Color.clear]),
center: .center,
startRadius: 20,
endRadius: 40
)
)
.frame(width: 80, height: 80)
.scaleEffect(isAnimating ? 1.2 : 1.0)
.opacity(state == .weak ? 0.8 : 1.0)
//
Circle()
.fill(
RadialGradient(
gradient: Gradient(colors: [chakra.color, chakra.color.opacity(0.7)]),
center: .center,
startRadius: 5,
endRadius: 25
)
)
.frame(width: 50, height: 50)
.opacity(state.opacity)
.overlay(
Circle()
.stroke(Color.white.opacity(0.8), lineWidth: 2)
)
//
Text(chakra.rawValue)
.font(.caption2)
.fontWeight(.semibold)
.foregroundColor(.white)
.shadow(radius: 1)
}
}
.onAppear {
if state == .weak {
withAnimation(Animation.easeInOut(duration: 2).repeatForever(autoreverses: true)) {
isAnimating = true
}
}
}
}
}
// MARK: -
struct QuickHealingCard: View {
let title: String
let icon: String
let color: Color
var body: some View {
Button(action: {
//
}) {
VStack(spacing: 8) {
Image(systemName: icon)
.font(.title2)
.foregroundColor(color)
Text(title)
.font(.subheadline)
.fontWeight(.medium)
.foregroundColor(.primary)
}
.frame(maxWidth: .infinity)
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(color: color.opacity(0.3), radius: 4, x: 0, y: 2)
}
}
}
#Preview {
HealingView()
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,438 @@
//
// LoadingComponents.swift
// EmotionMuseum
//
// Created by on 2025/6/13.
//
import SwiftUI
// MARK: -
enum LoadingState {
case idle
case loading
case loaded
case error(String)
}
// MARK: -
class ThemeManager: ObservableObject {
@Published var isDarkMode: Bool = false
@Published var systemFollowsDeviceTheme: Bool = true
init() {
//
self.isDarkMode = UserDefaults.standard.bool(forKey: "darkMode")
self.systemFollowsDeviceTheme = UserDefaults.standard.bool(forKey: "systemFollowsDeviceTheme")
}
func toggleTheme() {
isDarkMode.toggle()
UserDefaults.standard.set(isDarkMode, forKey: "darkMode")
}
func setSystemFollowing(_ follows: Bool) {
systemFollowsDeviceTheme = follows
UserDefaults.standard.set(follows, forKey: "systemFollowsDeviceTheme")
}
}
// MARK: -
extension Color {
static let theme = ColorTheme()
}
struct ColorTheme {
//
let primary = Color("PrimaryColor")
let secondary = Color("SecondaryColor")
let accent = Color("AccentColor")
//
let background = Color("BackgroundColor")
let cardBackground = Color("CardBackground")
let surfaceBackground = Color("SurfaceBackground")
//
let primaryText = Color("PrimaryText")
let secondaryText = Color("SecondaryText")
let tertiaryText = Color("TertiaryText")
//
let border = Color("BorderColor")
let divider = Color("DividerColor")
//
let success = Color("SuccessColor")
let warning = Color("WarningColor")
let error = Color("ErrorColor")
//
let skeleton = Color("SkeletonColor")
let skeletonHighlight = Color("SkeletonHighlight")
}
// MARK: -
struct SkeletonView: View {
@State private var isAnimating = false
let width: CGFloat?
let height: CGFloat
let cornerRadius: CGFloat
init(width: CGFloat? = nil, height: CGFloat = 20, cornerRadius: CGFloat = 8) {
self.width = width
self.height = height
self.cornerRadius = cornerRadius
}
var body: some View {
Rectangle()
.fill(
LinearGradient(
colors: [
Color.theme.skeleton,
Color.theme.skeletonHighlight,
Color.theme.skeleton
],
startPoint: .leading,
endPoint: .trailing
)
.opacity(isAnimating ? 0.6 : 1.0)
)
.frame(width: width, height: height)
.cornerRadius(cornerRadius)
.onAppear {
withAnimation(
Animation
.easeInOut(duration: 1.2)
.repeatForever(autoreverses: true)
) {
isAnimating = true
}
}
}
}
// MARK: -
struct SkeletonText: View {
let lineCount: Int
let spacing: CGFloat
init(lineCount: Int = 3, spacing: CGFloat = 8) {
self.lineCount = lineCount
self.spacing = spacing
}
var body: some View {
VStack(alignment: .leading, spacing: spacing) {
ForEach(0..<lineCount, id: \.self) { index in
SkeletonView(
width: index == lineCount - 1 ? CGFloat.random(in: 100...200) : nil,
height: 16,
cornerRadius: 4
)
}
}
}
}
// MARK: -
struct SkeletonCard: View {
let showImage: Bool
let showButton: Bool
init(showImage: Bool = true, showButton: Bool = false) {
self.showImage = showImage
self.showButton = showButton
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
if showImage {
SkeletonView(height: 120, cornerRadius: 12)
}
VStack(alignment: .leading, spacing: 8) {
SkeletonView(height: 20, cornerRadius: 6)
SkeletonText(lineCount: 2, spacing: 6)
if showButton {
HStack {
Spacer()
SkeletonView(width: 80, height: 32, cornerRadius: 16)
}
}
}
.padding(.horizontal, showImage ? 0 : 16)
}
.padding(16)
.background(Color.theme.cardBackground)
.cornerRadius(16)
.shadow(color: Color.black.opacity(0.05), radius: 8, x: 0, y: 2)
}
}
// MARK: -
struct SkeletonListItem: View {
let showAvatar: Bool
let showTrailing: Bool
init(showAvatar: Bool = true, showTrailing: Bool = true) {
self.showAvatar = showAvatar
self.showTrailing = showTrailing
}
var body: some View {
HStack(spacing: 12) {
if showAvatar {
SkeletonView(width: 50, height: 50, cornerRadius: 25)
}
VStack(alignment: .leading, spacing: 6) {
SkeletonView(width: 120, height: 16, cornerRadius: 4)
SkeletonView(width: 200, height: 14, cornerRadius: 4)
}
Spacer()
if showTrailing {
SkeletonView(width: 60, height: 20, cornerRadius: 6)
}
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
}
}
// MARK: -
struct LoadingStateView<Content: View>: View {
let loadingState: LoadingState
let content: () -> Content
let loadingView: (() -> AnyView)?
let errorView: ((String) -> AnyView)?
init(
loadingState: LoadingState,
@ViewBuilder content: @escaping () -> Content,
loadingView: (() -> AnyView)? = nil,
errorView: ((String) -> AnyView)? = nil
) {
self.loadingState = loadingState
self.content = content
self.loadingView = loadingView
self.errorView = errorView
}
var body: some View {
switch loadingState {
case .idle:
Color.clear
.onAppear {
//
}
case .loading:
if let loadingView = loadingView {
loadingView()
} else {
defaultLoadingView
}
case .loaded:
content()
.transition(.opacity.combined(with: .scale(scale: 0.95)))
case .error(let message):
if let errorView = errorView {
errorView(message)
} else {
defaultErrorView(message: message)
}
}
}
private var defaultLoadingView: some View {
VStack(spacing: 20) {
ProgressView()
.scaleEffect(1.5)
.progressViewStyle(CircularProgressViewStyle(tint: Color.theme.accent))
Text("加载中...")
.font(.subheadline)
.foregroundColor(Color.theme.secondaryText)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.theme.background)
}
private func defaultErrorView(message: String) -> some View {
VStack(spacing: 16) {
Image(systemName: "exclamationmark.triangle")
.font(.system(size: 48))
.foregroundColor(Color.theme.error)
Text("加载失败")
.font(.headline)
.foregroundColor(Color.theme.primaryText)
Text(message)
.font(.subheadline)
.foregroundColor(Color.theme.secondaryText)
.multilineTextAlignment(.center)
Button("重试") {
//
}
.buttonStyle(PrimaryButtonStyle())
}
.padding(32)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.theme.background)
}
}
// MARK: -
struct PrimaryButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.padding(.horizontal, 24)
.padding(.vertical, 12)
.background(Color.theme.accent)
.foregroundColor(.white)
.cornerRadius(12)
.scaleEffect(configuration.isPressed ? 0.95 : 1.0)
.animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
}
}
// MARK: -
struct LoadingList: View {
let itemCount: Int
let showAvatar: Bool
let showTrailing: Bool
init(itemCount: Int = 5, showAvatar: Bool = true, showTrailing: Bool = true) {
self.itemCount = itemCount
self.showAvatar = showAvatar
self.showTrailing = showTrailing
}
var body: some View {
LazyVStack(spacing: 0) {
ForEach(0..<itemCount, id: \.self) { _ in
SkeletonListItem(showAvatar: showAvatar, showTrailing: showTrailing)
Divider()
.background(Color.theme.divider)
}
}
}
}
// MARK: -
struct LoadingCardGrid: View {
let columns: Int
let itemCount: Int
let showImage: Bool
let showButton: Bool
init(columns: Int = 2, itemCount: Int = 6, showImage: Bool = true, showButton: Bool = false) {
self.columns = columns
self.itemCount = itemCount
self.showImage = showImage
self.showButton = showButton
}
var body: some View {
LazyVGrid(
columns: Array(repeating: GridItem(.flexible(), spacing: 16), count: columns),
spacing: 16
) {
ForEach(0..<itemCount, id: \.self) { _ in
SkeletonCard(showImage: showImage, showButton: showButton)
}
}
.padding(.horizontal, 16)
}
}
// MARK: -
struct SmartRefreshView<Content: View>: View {
@State private var isRefreshing = false
let onRefresh: () async -> Void
let content: () -> Content
init(
onRefresh: @escaping () async -> Void,
@ViewBuilder content: @escaping () -> Content
) {
self.onRefresh = onRefresh
self.content = content
}
var body: some View {
ScrollView {
content()
}
.refreshable {
await onRefresh()
}
}
}
// MARK: -
struct LoadMoreView: View {
@State private var isLoading = false
let onLoadMore: () async -> Void
let hasMore: Bool
init(hasMore: Bool = true, onLoadMore: @escaping () async -> Void) {
self.hasMore = hasMore
self.onLoadMore = onLoadMore
}
var body: some View {
HStack {
Spacer()
if hasMore {
if isLoading {
HStack(spacing: 8) {
ProgressView()
.scaleEffect(0.8)
Text("加载中...")
.font(.caption)
.foregroundColor(Color.theme.secondaryText)
}
} else {
Button("加载更多") {
Task {
isLoading = true
await onLoadMore()
isLoading = false
}
}
.font(.caption)
.foregroundColor(Color.theme.accent)
}
} else {
Text("没有更多内容了")
.font(.caption)
.foregroundColor(Color.theme.tertiaryText)
}
Spacer()
}
.padding(.vertical, 16)
}
}
// MARK: -
#Preview("骨架屏组件") {
VStack(spacing: 20) {
SkeletonCard()
SkeletonListItem()
SkeletonText()
}
.padding()
.background(Color.theme.background)
}
@@ -0,0 +1,55 @@
//
// LoadingOverlay.swift
// EmotionMuseum
//
// Created by on 2025/7/5.
//
import SwiftUI
struct LoadingOverlay: View {
let message: String
@State private var rotationAngle: Double = 0
var body: some View {
ZStack {
//
Color.black
.opacity(0.3)
.ignoresSafeArea()
//
VStack(spacing: 20) {
//
Image(systemName: "brain.head.profile")
.font(.system(size: 40))
.foregroundColor(Color("AccentColor"))
.rotationEffect(.degrees(rotationAngle))
.onAppear {
withAnimation(.linear(duration: 2).repeatForever(autoreverses: false)) {
rotationAngle = 360
}
}
//
Text(message)
.font(.subheadline)
.foregroundColor(Color("PrimaryText"))
.multilineTextAlignment(.center)
}
.padding(30)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color("CardBackground"))
.shadow(color: .black.opacity(0.1), radius: 10, x: 0, y: 5)
)
}
.transition(.opacity)
.zIndex(999) //
}
}
#Preview {
LoadingOverlay(message: "正在加载数据...")
.background(Color.gray.opacity(0.3))
}
@@ -0,0 +1,232 @@
import SwiftUI
// import AMapFoundationKit // CocoaPods
// import MAMapKit // CocoaPods
import UIKit
import MapKit
import CoreLocation
///
/// @Author huazhongmin
/// @Time 2024-03-24
/// @Description 使
struct MapView: View {
@Binding var shouldMoveToUserLocation: Bool
init(shouldMoveToUserLocation: Binding<Bool> = .constant(false)) {
self._shouldMoveToUserLocation = shouldMoveToUserLocation
}
var body: some View {
MapViewRepresentable(shouldMoveToUserLocation: $shouldMoveToUserLocation)
.edgesIgnoringSafeArea(.all)
}
}
/// SwiftUI
struct MapViewRepresentable: UIViewRepresentable {
@Binding var shouldMoveToUserLocation: Bool
//
let defaultLocation = CLLocationCoordinate2D(
latitude: 39.908823,
longitude: 116.397470
)
//
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
//
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView(frame: .zero)
mapView.delegate = context.coordinator
//
mapView.showsUserLocation = true
mapView.userTrackingMode = .none //
//
mapView.mapType = .standard
mapView.showsCompass = true
mapView.showsScale = true
mapView.showsTraffic = false
//
mapView.isZoomEnabled = true
mapView.isScrollEnabled = true
mapView.isRotateEnabled = true
mapView.isPitchEnabled = true
//
let initialRegion = MKCoordinateRegion(
center: defaultLocation,
span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
)
mapView.setRegion(initialRegion, animated: false)
// mapViewcoordinator
context.coordinator.mapView = mapView
context.coordinator.startLocationUpdates()
return mapView
}
//
func updateUIView(_ mapView: MKMapView, context: Context) {
//
context.coordinator.checkLocationPermission()
//
if shouldMoveToUserLocation {
context.coordinator.moveToUserLocation()
//
DispatchQueue.main.async {
shouldMoveToUserLocation = false
}
}
}
//
class Coordinator: NSObject, MKMapViewDelegate, CLLocationManagerDelegate {
var parent: MapViewRepresentable
var mapView: MKMapView?
var locationManager: CLLocationManager
var hasInitialLocationSet = false
init(_ parent: MapViewRepresentable) {
self.parent = parent
self.locationManager = CLLocationManager()
super.init()
//
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.distanceFilter = 10 // 10
}
//
func startLocationUpdates() {
checkLocationPermission()
}
//
func checkLocationPermission() {
guard CLLocationManager.locationServicesEnabled() else {
print("定位服务未启用")
return
}
let authorizationStatus = locationManager.authorizationStatus
switch authorizationStatus {
case .notDetermined:
// 使
locationManager.requestWhenInUseAuthorization()
case .authorizedWhenInUse, .authorizedAlways:
//
locationManager.startUpdatingLocation()
case .denied, .restricted:
//
print("定位权限被拒绝,显示默认位置")
setDefaultLocation()
@unknown default:
print("未知的定位权限状态")
setDefaultLocation()
}
}
//
func setDefaultLocation() {
DispatchQueue.main.async {
guard let mapView = self.mapView else { return }
let region = MKCoordinateRegion(
center: self.parent.defaultLocation,
span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
)
mapView.setRegion(region, animated: true)
}
}
// MARK: - CLLocationManagerDelegate
//
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
checkLocationPermission()
}
//
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
//
if !hasInitialLocationSet {
DispatchQueue.main.async {
guard let mapView = self.mapView else { return }
let userRegion = MKCoordinateRegion(
center: location.coordinate,
span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
)
mapView.setRegion(userRegion, animated: true)
self.hasInitialLocationSet = true
print("已定位到用户位置: \(location.coordinate)")
}
//
locationManager.stopUpdatingLocation()
}
}
//
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("定位失败: \(error.localizedDescription)")
//
if !hasInitialLocationSet {
setDefaultLocation()
hasInitialLocationSet = true
}
}
// MARK: - MKMapViewDelegate
//
func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
//
//
}
//
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
//
}
//
func moveToUserLocation() {
guard let mapView = self.mapView,
let userLocation = mapView.userLocation.location else {
//
checkLocationPermission()
return
}
let userRegion = MKCoordinateRegion(
center: userLocation.coordinate,
span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
)
mapView.setRegion(userRegion, animated: true)
}
}
}
struct MapView_Previews: PreviewProvider {
static var previews: some View {
MapView()
}
}
@@ -0,0 +1,662 @@
//
// RecordView.swift
// EmotionMuseum
//
// Created by on 2025/6/13.
//
import SwiftUI
// MARK: -
struct RecordView: View {
@EnvironmentObject var navigationManager: NavigationManager
@EnvironmentObject var themeManager: ThemeManager
@EnvironmentObject var mockData: MockDataManager
@StateObject private var aiService = MockAIService()
@State private var selectedDate = Date()
@State private var inputText = ""
@State private var showingMoodPicker = false
@State private var selectedMood = ""
@State private var loadingState: LoadingState = .idle
@State private var isInitialLoading = true
var body: some View {
LoadingStateView(loadingState: isInitialLoading ? .loading : .loaded) {
VStack(spacing: 0) {
//
ScrollView {
LazyVStack(spacing: 16) {
//
topNavigationBar
.padding(.horizontal, 16)
.padding(.top, 8)
.transition(.move(edge: .top).combined(with: .opacity))
//
emotionCalendar
.padding(.horizontal, 16)
.transition(.scale(scale: 0.8).combined(with: .opacity))
// AI
aiAvatarSection
.padding(.horizontal, 16)
.transition(.scale(scale: 0.9).combined(with: .opacity))
//
chatArea
.padding(.horizontal, 16)
.transition(.opacity.combined(with: .slide))
}
.padding(.bottom, 10) //
}
.refreshable {
await simulateRefresh()
}
//
inputArea
.background(Color.theme.cardBackground)
.shadow(color: .black.opacity(0.1), radius: 8, y: -4)
.transition(.move(edge: .bottom).combined(with: .opacity))
}
} loadingView: {
AnyView(recordViewSkeleton)
}
.background(Color.theme.background)
.environmentObject(themeManager)
.preferredColorScheme(themeManager.systemFollowsDeviceTheme ? nil : (themeManager.isDarkMode ? .dark : .light))
.ignoresSafeArea(.keyboard, edges: .bottom) //
.sheet(isPresented: $showingMoodPicker) {
MoodPickerView(
selectedDate: selectedDate,
selectedMood: $selectedMood,
isPresented: $showingMoodPicker
)
}
.onAppear {
simulateInitialLoading()
}
}
// MARK: -
private var topNavigationBar: some View {
HStack {
// -
Button(action: { navigationManager.showChatHistory() }) {
Image(systemName: "bubble.left.and.bubble.right.fill")
.font(.title3)
.foregroundColor(.blue)
}
Spacer()
// -
Text("情绪记录")
.font(.title2)
.fontWeight(.semibold)
.foregroundColor(.primary)
Spacer()
// -
Button(action: { navigationManager.navigateToSettings() }) {
Image(systemName: "gearshape.fill")
.font(.title3)
.foregroundColor(.gray)
}
}
}
// MARK: -
private var emotionCalendar: some View {
VStack(spacing: 12) {
HStack {
Text(selectedDate.formatted(.dateTime.month(.wide)))
.font(.headline)
.foregroundColor(.primary)
Spacer()
Button(action: { showingMoodPicker = true }) {
HStack(spacing: 4) {
Text(selectedMood.isEmpty ? "记录心情" : selectedMood)
.font(.caption)
Image(systemName: "plus.circle")
.font(.caption)
}
.foregroundColor(.blue)
}
}
// 7
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 16) {
ForEach(-3...3, id: \.self) { dayOffset in
let date = Calendar.current.date(byAdding: .day, value: dayOffset, to: selectedDate) ?? selectedDate
let isToday = Calendar.current.isDate(date, inSameDayAs: Date())
let isSelected = Calendar.current.isDate(date, inSameDayAs: selectedDate)
VStack(spacing: 4) {
Text(DateFormatter.weekdayShort.string(from: date))
.font(.caption2)
.foregroundColor(.secondary)
Button(action: { selectedDate = date }) {
VStack(spacing: 2) {
Text("\(Calendar.current.component(.day, from: date))")
.font(.system(size: 16, weight: .medium))
.foregroundColor(isSelected ? .white : .primary)
//
Circle()
.fill(emotionColorForDate(date))
.frame(width: 6, height: 6)
.opacity(hasEmotionRecord(for: date) ? 1 : 0)
}
.frame(width: 40, height: 44)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(isSelected ? Color.blue : Color.clear)
)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(isToday ? Color.blue : Color.clear, lineWidth: 1)
)
}
}
}
}
.padding(.horizontal, 16)
}
}
.padding()
.background(Color.theme.cardBackground)
.cornerRadius(16)
}
// MARK: - AI
private var aiAvatarSection: some View {
HStack {
Spacer()
// AI -
ZStack {
//
Circle()
.fill(
LinearGradient(
colors: [Color.blue.opacity(0.1), Color.purple.opacity(0.1)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(width: 80, height: 80)
// AI
Image(systemName: "brain.head.profile")
.font(.system(size: 35))
.foregroundColor(.blue)
.scaleEffect(aiService.isLoading ? 1.1 : 1.0)
.animation(.easeInOut(duration: 1).repeatForever(), value: aiService.isLoading)
}
Spacer()
}
.padding(.vertical, 10)
}
// MARK: -
private var chatArea: some View {
VStack(spacing: 0) {
if mockData.conversations.isEmpty {
//
defaultChatContent
} else {
//
chatMessagesList
}
}
.frame(minHeight: 300) //
}
// MARK: -
private var defaultChatContent: some View {
VStack(spacing: 20) {
// AI
HStack {
// AI
Circle()
.fill(Color.blue.opacity(0.1))
.frame(width: 32, height: 32)
.overlay(
Image(systemName: "brain.head.profile")
.font(.system(size: 16))
.foregroundColor(.blue)
)
//
VStack(alignment: .leading, spacing: 8) {
Text("你好!我是你的情绪陪伴师")
.font(.system(size: 16, weight: .medium))
.foregroundColor(Color.theme.primaryText)
Text(getGreetingText())
.font(.system(size: 14))
.foregroundColor(Color.theme.secondaryText)
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
.background(
RoundedRectangle(cornerRadius: 18)
.fill(Color.theme.surfaceBackground)
.shadow(color: Color.black.opacity(0.05), radius: 2, x: 0, y: 1)
)
Spacer()
}
.padding(.horizontal, 4)
//
HStack {
Spacer()
Text("你可以这样开始对话")
.font(.caption)
.foregroundColor(Color.theme.tertiaryText)
Spacer()
}
.padding(.top, 10)
//
chatQuickReplyCards
}
.padding(.vertical, 20)
}
// MARK: -
private var chatMessagesList: some View {
LazyVStack(spacing: 12) {
ForEach(mockData.conversations.prefix(3), id: \.id) { conversation in
ConversationPreviewCard(conversation: conversation) {
navigationManager.navigateToChat(conversation: conversation)
}
}
}
.padding(.vertical, 10)
}
// MARK: -
private var chatQuickReplyCards: some View {
LazyVGrid(columns: [
GridItem(.flexible(), spacing: 8),
GridItem(.flexible(), spacing: 8)
], spacing: 12) {
ForEach(quickReplies, id: \.self) { reply in
Button(action: { sendQuickReply(reply) }) {
Text(reply)
.font(.system(size: 14, weight: .medium))
.foregroundColor(Color("AccentColor"))
.multilineTextAlignment(.center)
.lineLimit(2)
.padding(.horizontal, 12)
.padding(.vertical, 14)
.frame(maxWidth: .infinity)
.frame(minHeight: 70)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(Color.theme.cardBackground)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color("AccentColor").opacity(0.3), lineWidth: 1)
)
.shadow(
color: Color.black.opacity(0.05),
radius: 3,
x: 0,
y: 2
)
)
}
.buttonStyle(PlainButtonStyle())
.scaleEffect(1.0)
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: false)
}
}
}
private let quickReplies = [
"我今天感觉有点焦虑",
"想和你聊聊最近的压力",
"今天发生了一些开心的事",
"我需要一些建议"
]
// MARK: -
private var inputArea: some View {
VStack(spacing: 0) {
// 线
LinearGradient(
colors: [Color.theme.divider.opacity(0), Color.theme.divider, Color.theme.divider.opacity(0)],
startPoint: .leading,
endPoint: .trailing
)
.frame(height: 1)
//
HStack(spacing: 12) {
//
Button(action: { }) {
Image(systemName: "photo.circle.fill")
.font(.title2)
.foregroundColor(Color.theme.secondaryText)
}
//
HStack(spacing: 8) {
TextField("说说你的感受...", text: $inputText)
.textFieldStyle(PlainTextFieldStyle())
.foregroundColor(Color.theme.primaryText)
.padding(.vertical, 12)
.padding(.leading, 16)
//
Button(action: { }) {
Image(systemName: "mic.circle.fill")
.font(.title2)
.foregroundColor(Color("AccentColor"))
}
.padding(.trailing, 8)
}
.background(
RoundedRectangle(cornerRadius: 24)
.fill(Color.theme.surfaceBackground)
.shadow(
color: Color.black.opacity(0.05),
radius: 2,
x: 0,
y: 1
)
)
//
Button(action: sendMessage) {
Image(systemName: inputText.isEmpty ? "arrow.up.circle" : "arrow.up.circle.fill")
.font(.title2)
.foregroundColor(inputText.isEmpty ? Color.theme.secondaryText : Color("AccentColor"))
.scaleEffect(inputText.isEmpty ? 0.9 : 1.0)
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: inputText.isEmpty)
}
.disabled(inputText.isEmpty)
}
.padding(.horizontal, 16)
.padding(.top, 12)
.padding(.bottom, 12) //
.background(
Color.theme.cardBackground
.overlay(
//
LinearGradient(
colors: [
Color.white.opacity(themeManager.isDarkMode ? 0.05 : 0.4),
Color.clear
],
startPoint: .top,
endPoint: .bottom
)
.frame(height: 1),
alignment: .top
)
)
// AI
if aiService.isLoading {
HStack(spacing: 4) {
ProgressView()
.scaleEffect(0.7)
.tint(Color("AccentColor"))
Text("AI思考中...")
.font(.caption2)
.foregroundColor(Color.theme.secondaryText)
}
.padding(.horizontal, 16)
.padding(.bottom, 12)
.background(Color.theme.cardBackground)
}
}
}
// MARK: -
private func getGreetingText() -> String {
let hour = Calendar.current.component(.hour, from: Date())
switch hour {
case 5..<12:
return "早上好!新的一天,新的开始。今天感觉怎么样?"
case 12..<17:
return "下午好!工作辛苦了,有什么想聊的吗?"
case 17..<22:
return "晚上好!一天结束了,让我们聊聊今天的感受吧。"
default:
return "夜深了,如果睡不着,我可以陪你聊聊。"
}
}
private func emotionColorForDate(_ date: Date) -> Color {
//
let day = Calendar.current.component(.day, from: date)
switch day % 6 {
case 0: return .red
case 1: return .blue
case 2: return .green
case 3: return .yellow
case 4: return .purple
default: return .orange
}
}
private func hasEmotionRecord(for date: Date) -> Bool {
//
let day = Calendar.current.component(.day, from: date)
return day % 3 != 0
}
private func sendQuickReply(_ reply: String) {
inputText = reply
sendMessage()
}
private func sendMessage() {
guard !inputText.isEmpty else { return }
let messageContent = inputText
inputText = ""
// 使
navigationManager.sendMessage(messageContent)
//
if navigationManager.currentChatConversation == nil {
navigationManager.navigateToChat()
}
}
private func loadConversations() {
//
// mockData.conversations = [] //
}
private func simulateInitialLoading() {
loadingState = .loading
//
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
withAnimation(.easeOut(duration: 0.6)) {
isInitialLoading = false
loadingState = .loaded
}
loadConversations()
}
}
private func simulateRefresh() async {
//
try? await Task.sleep(nanoseconds: 1_000_000_000)
await MainActor.run {
loadConversations()
}
}
// MARK: -
private var recordViewSkeleton: some View {
VStack(spacing: 0) {
//
HStack {
SkeletonView(width: 24, height: 24, cornerRadius: 12)
Spacer()
SkeletonView(width: 100, height: 20, cornerRadius: 6)
Spacer()
SkeletonView(width: 24, height: 24, cornerRadius: 12)
}
.padding(.horizontal, 16)
.padding(.top, 8)
//
VStack(spacing: 12) {
HStack {
SkeletonView(width: 80, height: 20, cornerRadius: 6)
Spacer()
SkeletonView(width: 60, height: 16, cornerRadius: 4)
}
HStack(spacing: 16) {
ForEach(0..<7, id: \.self) { _ in
VStack(spacing: 4) {
SkeletonView(width: 20, height: 12, cornerRadius: 3)
SkeletonView(width: 40, height: 44, cornerRadius: 12)
}
}
}
}
.padding()
.background(Color.theme.cardBackground)
.cornerRadius(16)
.padding(.horizontal, 16)
.padding(.vertical, 8)
Spacer()
// AI
VStack(spacing: 24) {
SkeletonView(width: 200, height: 200, cornerRadius: 100)
VStack(spacing: 8) {
SkeletonView(width: 200, height: 20, cornerRadius: 6)
SkeletonView(width: 250, height: 16, cornerRadius: 4)
SkeletonView(width: 180, height: 16, cornerRadius: 4)
}
VStack(spacing: 8) {
SkeletonView(width: 120, height: 12, cornerRadius: 3)
ForEach(0..<3, id: \.self) { _ in
SkeletonView(width: 160, height: 32, cornerRadius: 16)
}
}
}
.padding(.horizontal, 24)
Spacer()
//
VStack(spacing: 12) {
HStack(spacing: 12) {
SkeletonView(height: 48, cornerRadius: 24)
SkeletonView(width: 48, height: 48, cornerRadius: 24)
}
HStack(spacing: 24) {
ForEach(0..<2, id: \.self) { _ in
VStack(spacing: 4) {
SkeletonView(width: 24, height: 24, cornerRadius: 12)
SkeletonView(width: 30, height: 12, cornerRadius: 3)
}
}
Spacer()
}
}
.padding(.horizontal, 16)
.padding(.top, 16)
.padding(.bottom, 24)
.background(Color.theme.cardBackground)
}
.background(Color.theme.background)
}
}
// MARK: -
//
struct MoodPickerView: View {
let selectedDate: Date
@Binding var selectedMood: String
@Binding var isPresented: Bool
let moods = [
("😊", "开心"), ("😢", "难过"), ("😡", "愤怒"),
("😰", "焦虑"), ("😌", "平静"), ("🤔", "思考")
]
var body: some View {
NavigationView {
VStack(spacing: 24) {
Text("选择今日心情")
.font(.title2)
.fontWeight(.semibold)
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 3), spacing: 16) {
ForEach(moods, id: \.0) { emoji, name in
Button(action: {
selectedMood = emoji
isPresented = false
}) {
VStack(spacing: 8) {
Text(emoji)
.font(.system(size: 40))
Text(name)
.font(.caption)
.foregroundColor(.primary)
}
.frame(width: 80, height: 80)
.background(Color(.systemGray6))
.cornerRadius(16)
}
}
}
.padding()
Spacer()
}
.navigationTitle("心情记录")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("取消") { isPresented = false }
}
}
}
}
}
// MARK: -
extension DateFormatter {
static let weekdayShort: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "E"
return formatter
}()
}
@@ -0,0 +1,875 @@
//
// SupportViews.swift
// EmotionMuseum
//
// Created by huazhongmin on 2024/01/01.
//
import SwiftUI
import MapKit
// MARK: -
struct AstroAnalysisView: View {
@State private var selectedDate = Date()
@State private var selectedTime = Date()
@State private var birthLocation = ""
var body: some View {
NavigationView {
ScrollView {
VStack(spacing: 24) {
Text("通过占星学了解你的内在特质")
.font(.headline)
.multilineTextAlignment(.center)
.padding()
VStack(spacing: 16) {
DatePicker("出生日期", selection: $selectedDate, displayedComponents: .date)
DatePicker("出生时间", selection: $selectedTime, displayedComponents: .hourAndMinute)
TextField("出生地点", text: $birthLocation)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
.padding()
Button("生成星盘分析") {
//
}
.buttonStyle(.borderedProminent)
.padding()
}
}
.navigationTitle("占星分析")
}
}
}
// MARK: -
struct LocationMarkerCard: View {
let location: LocationPin
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Image(systemName: location.category.icon)
.foregroundColor(location.emotion.color)
Text(location.name)
.font(.headline)
Spacer()
Text(location.emotion.emoji)
.font(.title2)
}
Text(location.description)
.font(.caption)
.foregroundColor(.secondary)
.lineLimit(2)
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(radius: 2)
}
}
// MARK: -
struct RecommendedLocationCard: View {
let location: LocationPin
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(location.name)
.font(.headline)
.fontWeight(.semibold)
Text(location.category.rawValue)
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
VStack(spacing: 4) {
Text(location.emotion.emoji)
.font(.title)
Text(location.emotion.displayName)
.font(.caption2)
.foregroundColor(location.emotion.color)
}
}
Text(location.description)
.font(.subheadline)
.foregroundColor(.secondary)
.lineLimit(2)
HStack {
Label("\(location.visitCount)", systemImage: "person.2")
.font(.caption)
.foregroundColor(.secondary)
Spacer()
if !location.tags.isEmpty {
Text("#\(location.tags.first ?? "")")
.font(.caption)
.foregroundColor(.blue)
.padding(.horizontal, 8)
.padding(.vertical, 2)
.background(Color.blue.opacity(0.1))
.cornerRadius(4)
}
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(16)
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
}
}
// MARK: -
struct CommunityPostCard: View {
let post: CommunityPost
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Circle()
.fill(Color.blue)
.frame(width: 40, height: 40)
.overlay(
Text(String(post.authorName.prefix(1)))
.foregroundColor(.white)
.fontWeight(.semibold)
)
VStack(alignment: .leading, spacing: 2) {
Text(post.authorName)
.font(.subheadline)
.fontWeight(.medium)
Text(DateFormatter.shortRelative.string(from: post.createdAt))
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
Text("💭")
.font(.title2)
}
Text(post.content)
.font(.body)
.lineLimit(3)
if !post.tags.isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
ForEach(post.tags, id: \.self) { tag in
Text("#\(tag)")
.font(.caption)
.foregroundColor(.blue)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Color.blue.opacity(0.1))
.cornerRadius(8)
}
}
.padding(.horizontal, 4)
}
}
HStack {
Button(action: {}) {
HStack(spacing: 4) {
Image(systemName: post.isLikedByCurrentUser ? "heart.fill" : "heart")
.foregroundColor(post.isLikedByCurrentUser ? .red : .gray)
Text("\(post.likes)")
.font(.caption)
.foregroundColor(.secondary)
}
}
Button(action: {}) {
HStack(spacing: 4) {
Image(systemName: "message")
.foregroundColor(.gray)
Text("\(post.comments.count)")
.font(.caption)
.foregroundColor(.secondary)
}
}
Spacer()
Button(action: {}) {
Image(systemName: "square.and.arrow.up")
.foregroundColor(.gray)
}
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(16)
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
}
}
// MARK: -
struct AllLocationsView: View {
@EnvironmentObject var dataManager: MockDataManager
@State private var searchText = ""
@State private var selectedCategory: LocationCategory?
@State private var sortOption: SortOption = .recent
enum SortOption: String, CaseIterable {
case recent = "最近访问"
case popular = "最受欢迎"
case nearby = "距离最近"
case alphabetical = "按名称"
}
var filteredLocations: [LocationPin] {
let filtered = dataManager.locationPins.filter { location in
let matchesSearch = searchText.isEmpty ||
location.name.localizedCaseInsensitiveContains(searchText) ||
location.description.localizedCaseInsensitiveContains(searchText) ||
location.tags.contains { $0.localizedCaseInsensitiveContains(searchText) }
let matchesCategory = selectedCategory == nil || location.category == selectedCategory
return matchesSearch && matchesCategory
}
switch sortOption {
case .recent:
return filtered.sorted { ($0.lastVisitAt ?? Date.distantPast) > ($1.lastVisitAt ?? Date.distantPast) }
case .popular:
return filtered.sorted { $0.visitCount > $1.visitCount }
case .nearby:
return filtered //
case .alphabetical:
return filtered.sorted { $0.name < $1.name }
}
}
var body: some View {
NavigationView {
VStack(spacing: 0) {
//
SearchBar(text: $searchText)
.padding()
//
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
//
Button(action: { selectedCategory = nil }) {
Text("全部")
.font(.caption)
.foregroundColor(selectedCategory == nil ? .white : .primary)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(selectedCategory == nil ? Color.blue : Color(.systemGray6))
.cornerRadius(16)
}
ForEach(LocationCategory.allCases, id: \.self) { category in
Button(action: { selectedCategory = category }) {
Text(category.rawValue)
.font(.caption)
.foregroundColor(selectedCategory == category ? .white : .primary)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(selectedCategory == category ? Color.blue : Color(.systemGray6))
.cornerRadius(16)
}
}
}
.padding(.horizontal)
}
.padding(.bottom)
//
Picker("排序", selection: $sortOption) {
ForEach(SortOption.allCases, id: \.self) { option in
Text(option.rawValue).tag(option)
}
}
.pickerStyle(SegmentedPickerStyle())
.padding(.horizontal)
//
ScrollView {
LazyVStack(spacing: 16) {
ForEach(filteredLocations) { location in
AllLocationCard(location: location)
}
}
.padding()
}
}
.navigationTitle("所有地点")
}
}
}
// MARK: -
struct SearchBar: View {
@Binding var text: String
var body: some View {
HStack {
Image(systemName: "magnifyingglass")
.foregroundColor(.gray)
TextField("搜索地点、标签...", text: $text)
.textFieldStyle(PlainTextFieldStyle())
if !text.isEmpty {
Button(action: { text = "" }) {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.gray)
}
}
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(Color(.systemGray6))
.cornerRadius(10)
}
}
// MARK: -
struct AllLocationCard: View {
let location: LocationPin
var body: some View {
HStack(spacing: 16) {
//
VStack(spacing: 8) {
Image(systemName: location.category.icon)
.font(.title2)
.foregroundColor(location.emotion.color)
.frame(width: 40, height: 40)
.background(location.emotion.color.opacity(0.2))
.cornerRadius(12)
Text(location.emotion.emoji)
.font(.title3)
}
//
VStack(alignment: .leading, spacing: 4) {
Text(location.name)
.font(.headline)
.fontWeight(.semibold)
Text(location.description)
.font(.subheadline)
.foregroundColor(.secondary)
.lineLimit(2)
HStack {
Label("\(location.visitCount)次访问", systemImage: "clock")
.font(.caption)
.foregroundColor(.secondary)
Spacer()
if let lastVisit = location.lastVisitAt {
Text(DateFormatter.shortRelative.string(from: lastVisit))
.font(.caption)
.foregroundColor(.secondary)
}
}
}
Spacer()
//
VStack(spacing: 8) {
Button(action: {}) {
Image(systemName: location.isBookmarked ? "bookmark.fill" : "bookmark")
.foregroundColor(location.isBookmarked ? .blue : .gray)
}
Button(action: {}) {
Image(systemName: "square.and.arrow.up")
.foregroundColor(.gray)
}
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(16)
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
}
}
// MARK: -
struct CreatePostView: View {
let selectedLocation: LocationPin?
@Environment(\.dismiss) private var dismiss
@State private var postContent = ""
@State private var selectedEmotion: EmotionType = .neutral
@State private var selectedTags: Set<String> = []
let availableTags = ["推荐", "美食", "风景", "心情", "感悟", "治愈", "安静", "热闹"]
var body: some View {
NavigationView {
ScrollView {
VStack(spacing: 20) {
//
VStack(alignment: .leading, spacing: 8) {
Text("分享你的感受")
.font(.headline)
TextEditor(text: $postContent)
.frame(height: 120)
.padding(8)
.background(Color(.systemGray6))
.cornerRadius(12)
}
//
VStack(alignment: .leading, spacing: 8) {
Text("当前情绪")
.font(.headline)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(EmotionType.allCases, id: \.self) { emotion in
Button(action: { selectedEmotion = emotion }) {
VStack(spacing: 4) {
Text(emotion.emoji)
.font(.title2)
Text(emotion.rawValue)
.font(.caption)
.foregroundColor(.primary)
}
.padding(8)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(selectedEmotion == emotion ? emotion.color.opacity(0.3) : Color(.systemGray6))
)
}
}
}
.padding(.horizontal, 4)
}
}
//
VStack(alignment: .leading, spacing: 8) {
Text("添加标签")
.font(.headline)
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 4), spacing: 8) {
ForEach(availableTags, id: \.self) { tag in
Button(action: {
if selectedTags.contains(tag) {
selectedTags.remove(tag)
} else {
selectedTags.insert(tag)
}
}) {
Text("#\(tag)")
.font(.caption)
.foregroundColor(selectedTags.contains(tag) ? .white : .primary)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(selectedTags.contains(tag) ? Color.blue : Color(.systemGray6))
)
}
}
}
}
//
if let location = selectedLocation {
VStack(alignment: .leading, spacing: 8) {
Text("位置")
.font(.headline)
HStack(spacing: 12) {
Image(systemName: location.category.icon)
.foregroundColor(.blue)
VStack(alignment: .leading, spacing: 2) {
Text(location.name)
.font(.subheadline)
.fontWeight(.medium)
if let address = location.address {
Text(address)
.font(.caption)
.foregroundColor(.secondary)
}
}
Spacer()
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(12)
}
}
}
.padding()
}
.navigationTitle("分享动态")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("取消") { dismiss() }
}
ToolbarItem(placement: .confirmationAction) {
Button("发布") {
//
dismiss()
}
.disabled(postContent.isEmpty)
}
}
}
}
}
// MARK: -
struct LocationDetailView: View {
let location: LocationPin
@Environment(\.dismiss) private var dismiss
@State private var showingShareView = false
var body: some View {
NavigationView {
ScrollView {
VStack(spacing: 24) {
//
VStack(spacing: 16) {
HStack(spacing: 20) {
Image(systemName: location.category.icon)
.font(.system(size: 48))
.foregroundColor(location.emotion.color)
.frame(width: 80, height: 80)
.background(location.emotion.color.opacity(0.2))
.cornerRadius(20)
VStack(spacing: 8) {
Text(location.emotion.emoji)
.font(.system(size: 60))
Text(location.emotion.displayName)
.font(.headline)
.foregroundColor(location.emotion.color)
}
}
Text(location.name)
.font(.title)
.fontWeight(.bold)
.multilineTextAlignment(.center)
Text(location.description)
.font(.body)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal)
}
.padding(.vertical, 20)
.frame(maxWidth: .infinity)
.background(
LinearGradient(
colors: [location.emotion.color.opacity(0.1), location.emotion.color.opacity(0.05)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.cornerRadius(20)
//
VStack(alignment: .leading, spacing: 12) {
Text("基本信息")
.font(.headline)
InfoRow(icon: "location", title: "地址", value: location.address ?? "未设置")
InfoRow(icon: "list.bullet", title: "类别", value: location.category.rawValue)
InfoRow(icon: "number", title: "访问次数", value: "\(location.visitCount)")
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(16)
.shadow(radius: 2)
//
VStack(spacing: 12) {
Button(action: { showingShareView = true }) {
HStack {
Image(systemName: "square.and.pencil")
Text("在这里分享心情")
}
.font(.headline)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding()
.background(location.emotion.color)
.cornerRadius(12)
}
HStack(spacing: 12) {
Button(action: {}) {
HStack {
Image(systemName: "bookmark")
Text("收藏")
}
.frame(maxWidth: .infinity)
.padding()
.background(Color(.systemGray6))
.cornerRadius(12)
}
Button(action: {}) {
HStack {
Image(systemName: "square.and.arrow.up")
Text("分享")
}
.frame(maxWidth: .infinity)
.padding()
.background(Color(.systemGray6))
.cornerRadius(12)
}
}
}
.padding(.horizontal)
}
.padding()
}
.navigationTitle("")
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(true)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: { dismiss() }) {
HStack(spacing: 4) {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.gray)
Text("关闭")
.foregroundColor(.primary)
}
}
}
}
}
.sheet(isPresented: $showingShareView) {
CreatePostView(selectedLocation: location)
}
}
}
// MARK: -
struct InfoRow: View {
let icon: String
let title: String
let value: String
var body: some View {
HStack {
Image(systemName: icon)
.foregroundColor(.blue)
.frame(width: 20)
Text(title)
.font(.subheadline)
.foregroundColor(.secondary)
Spacer()
Text(value)
.font(.subheadline)
.fontWeight(.medium)
}
}
}
// MARK: - 访
struct VisitRecord: Identifiable, Codable {
let id: UUID
let date: Date
let emotion: EmotionType
let notes: String
init(id: UUID = UUID(), date: Date, emotion: EmotionType, notes: String) {
self.id = id
self.date = date
self.emotion = emotion
self.notes = notes
}
}
struct VisitHistoryRow: View {
let visit: VisitRecord
let isLatest: Bool
var body: some View {
HStack(spacing: 12) {
VStack(spacing: 4) {
Text(visit.emotion.emoji)
.font(.title3)
if isLatest {
Circle()
.fill(Color.green)
.frame(width: 6, height: 6)
} else {
Circle()
.fill(Color.gray.opacity(0.3))
.frame(width: 4, height: 4)
}
}
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(visit.emotion.displayName)
.font(.subheadline)
.fontWeight(.medium)
.foregroundColor(visit.emotion.color)
Spacer()
Text(DateFormatter.localizedDate.string(from: visit.date))
.font(.caption)
.foregroundColor(.secondary)
}
Text(visit.notes)
.font(.caption)
.foregroundColor(.secondary)
.lineLimit(2)
}
}
.padding(.vertical, 4)
}
}
// MARK: -
extension DateFormatter {
static let localizedDate: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .none
return formatter
}()
static let shortRelative: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .none
return formatter
}()
}
// MARK: - 访
func generateVisitNote(for location: LocationPin, emotion: EmotionType) -> String {
let notes = [
"在这里度过了美好的时光",
"心情得到了很好的调节",
"这个地方让我感到平静",
"和朋友一起来的,很开心",
"独自一人,享受安静的时光",
"记录了这次特别的体验"
]
return notes.randomElement() ?? "记录了这次访问"
}
// MARK: -
struct GuidedSelectionView: View {
var body: some View {
VStack {
Text("引导选择")
.font(.title)
.padding()
Text("这里是引导用户进行选择的界面")
.foregroundColor(.secondary)
.padding()
Spacer()
}
}
}
struct AstroAnalysisInputView: View {
var body: some View {
VStack {
Text("占星分析输入")
.font(.title)
.padding()
Text("这里是占星分析的输入界面")
.foregroundColor(.secondary)
.padding()
Spacer()
}
}
}
// MARK: -
struct TopicDetailView: View {
let topic: GrowthTopic
var body: some View {
VStack {
Text(topic.title)
.font(.title)
.padding()
Text(topic.description)
.foregroundColor(.secondary)
.padding()
Spacer()
}
}
}
struct EmotionalInsightsView: View {
var body: some View {
VStack {
Text("情绪洞察")
.font(.title)
.padding()
Text("这里显示用户的情绪分析和洞察")
.foregroundColor(.secondary)
.padding()
Spacer()
}
}
}
struct ChatHistoryView: View {
var body: some View {
VStack {
Text("对话历史")
.font(.title)
.padding()
Text("这里显示与AI的对话历史")
.foregroundColor(.secondary)
.padding()
Spacer()
}
}
}
@@ -0,0 +1,415 @@
//
// ThemeAdapter.swift
// EmotionMuseum
//
// Created by on 2025/6/13.
//
import SwiftUI
// MARK: -
struct ThemedCard<Content: View>: View {
let content: () -> Content
let padding: CGFloat
let cornerRadius: CGFloat
let shadowEnabled: Bool
init(
padding: CGFloat = 16,
cornerRadius: CGFloat = 16,
shadowEnabled: Bool = true,
@ViewBuilder content: @escaping () -> Content
) {
self.padding = padding
self.cornerRadius = cornerRadius
self.shadowEnabled = shadowEnabled
self.content = content
}
var body: some View {
content()
.padding(padding)
.background(Color.theme.cardBackground)
.cornerRadius(cornerRadius)
.shadow(
color: shadowEnabled ? Color.black.opacity(0.05) : Color.clear,
radius: shadowEnabled ? 8 : 0,
x: 0,
y: shadowEnabled ? 2 : 0
)
}
}
// MARK: -
struct ThemedText: View {
let text: String
let style: TextStyle
let alignment: TextAlignment
enum TextStyle {
case title, headline, subheadline, body, caption
case primary, secondary, tertiary
var font: Font {
switch self {
case .title: return .title
case .headline: return .headline
case .subheadline: return .subheadline
case .body: return .body
case .caption: return .caption
case .primary, .secondary, .tertiary: return .body
}
}
var color: Color {
switch self {
case .title, .headline, .subheadline, .body, .primary:
return Color.theme.primaryText
case .secondary:
return Color.theme.secondaryText
case .tertiary, .caption:
return Color.theme.tertiaryText
}
}
}
init(_ text: String, style: TextStyle = .body, alignment: TextAlignment = .leading) {
self.text = text
self.style = style
self.alignment = alignment
}
var body: some View {
Text(text)
.font(style.font)
.foregroundColor(style.color)
.multilineTextAlignment(alignment)
}
}
// MARK: -
struct ThemedButton: View {
let title: String
let style: ButtonStyle
let size: ButtonSize
let action: () -> Void
enum ButtonStyle {
case primary, secondary, outline, text
var backgroundColor: Color {
switch self {
case .primary: return Color.theme.accent
case .secondary: return Color.theme.secondary
case .outline, .text: return Color.clear
}
}
var foregroundColor: Color {
switch self {
case .primary: return .white
case .secondary: return Color.theme.primaryText
case .outline: return Color.theme.accent
case .text: return Color.theme.accent
}
}
var borderColor: Color {
switch self {
case .outline: return Color.theme.accent
default: return Color.clear
}
}
}
enum ButtonSize {
case small, medium, large
var padding: EdgeInsets {
switch self {
case .small: return EdgeInsets(top: 8, leading: 12, bottom: 8, trailing: 12)
case .medium: return EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16)
case .large: return EdgeInsets(top: 16, leading: 24, bottom: 16, trailing: 24)
}
}
var cornerRadius: CGFloat {
switch self {
case .small: return 8
case .medium: return 12
case .large: return 16
}
}
}
init(_ title: String, style: ButtonStyle = .primary, size: ButtonSize = .medium, action: @escaping () -> Void) {
self.title = title
self.style = style
self.size = size
self.action = action
}
var body: some View {
Button(action: action) {
Text(title)
.font(.subheadline)
.fontWeight(.medium)
.foregroundColor(style.foregroundColor)
.padding(size.padding)
.background(style.backgroundColor)
.overlay(
RoundedRectangle(cornerRadius: size.cornerRadius)
.stroke(style.borderColor, lineWidth: style == .outline ? 1 : 0)
)
.cornerRadius(size.cornerRadius)
}
}
}
// MARK: - 线
struct ThemedDivider: View {
let thickness: CGFloat
let color: Color?
init(thickness: CGFloat = 1, color: Color? = nil) {
self.thickness = thickness
self.color = color
}
var body: some View {
Rectangle()
.fill(color ?? Color.theme.divider)
.frame(height: thickness)
}
}
// MARK: -
struct ThemedProgressView: View {
let value: Double
let total: Double
let height: CGFloat
let backgroundColor: Color?
let foregroundColor: Color?
init(
value: Double,
total: Double = 1.0,
height: CGFloat = 8,
backgroundColor: Color? = nil,
foregroundColor: Color? = nil
) {
self.value = value
self.total = total
self.height = height
self.backgroundColor = backgroundColor
self.foregroundColor = foregroundColor
}
var body: some View {
ProgressView(value: value, total: total)
.progressViewStyle(
ThemedLinearProgressViewStyle(
height: height,
backgroundColor: backgroundColor ?? Color.theme.skeleton,
foregroundColor: foregroundColor ?? Color.theme.accent
)
)
}
}
struct ThemedLinearProgressViewStyle: ProgressViewStyle {
let height: CGFloat
let backgroundColor: Color
let foregroundColor: Color
func makeBody(configuration: Configuration) -> some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
Rectangle()
.fill(backgroundColor)
.frame(height: height)
.cornerRadius(height / 2)
Rectangle()
.fill(foregroundColor)
.frame(
width: geometry.size.width * CGFloat(configuration.fractionCompleted ?? 0),
height: height
)
.cornerRadius(height / 2)
}
}
.frame(height: height)
}
}
// MARK: -
struct ThemedTextField: View {
let placeholder: String
@Binding var text: String
let style: TextFieldStyle
enum TextFieldStyle {
case standard, rounded, outline
var backgroundColor: Color {
switch self {
case .standard: return Color.clear
case .rounded: return Color.theme.surfaceBackground
case .outline: return Color.theme.background
}
}
var borderColor: Color {
switch self {
case .outline: return Color.theme.border
default: return Color.clear
}
}
var cornerRadius: CGFloat {
switch self {
case .rounded: return 12
case .outline: return 8
default: return 0
}
}
}
init(_ placeholder: String, text: Binding<String>, style: TextFieldStyle = .standard) {
self.placeholder = placeholder
self._text = text
self.style = style
}
var body: some View {
TextField(placeholder, text: $text)
.padding(.horizontal, style == .standard ? 0 : 12)
.padding(.vertical, style == .standard ? 0 : 10)
.background(style.backgroundColor)
.foregroundColor(Color.theme.primaryText)
.overlay(
RoundedRectangle(cornerRadius: style.cornerRadius)
.stroke(style.borderColor, lineWidth: style == .outline ? 1 : 0)
)
.cornerRadius(style.cornerRadius)
}
}
// MARK: -
struct ThemedIcon: View {
let systemName: String
let style: IconStyle
let size: IconSize
enum IconStyle {
case primary, secondary, accent, custom(Color)
var color: Color {
switch self {
case .primary: return Color.theme.primaryText
case .secondary: return Color.theme.secondaryText
case .accent: return Color.theme.accent
case .custom(let color): return color
}
}
}
enum IconSize {
case small, medium, large, custom(CGFloat)
var font: Font {
switch self {
case .small: return .caption
case .medium: return .body
case .large: return .title2
case .custom(let size): return .system(size: size)
}
}
}
init(_ systemName: String, style: IconStyle = .primary, size: IconSize = .medium) {
self.systemName = systemName
self.style = style
self.size = size
}
var body: some View {
Image(systemName: systemName)
.font(size.font)
.foregroundColor(style.color)
}
}
// MARK: -
struct ThemedListRow<Content: View>: View {
let content: () -> Content
let showSeparator: Bool
let padding: EdgeInsets
init(
showSeparator: Bool = true,
padding: EdgeInsets = EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16),
@ViewBuilder content: @escaping () -> Content
) {
self.showSeparator = showSeparator
self.padding = padding
self.content = content
}
var body: some View {
VStack(spacing: 0) {
content()
.padding(padding)
.background(Color.theme.cardBackground)
if showSeparator {
ThemedDivider()
.padding(.leading, padding.leading)
}
}
}
}
// MARK: -
extension View {
func themedCard(
padding: CGFloat = 16,
cornerRadius: CGFloat = 16,
shadowEnabled: Bool = true
) -> some View {
ThemedCard(
padding: padding,
cornerRadius: cornerRadius,
shadowEnabled: shadowEnabled
) {
self
}
}
func themedBackground() -> some View {
background(Color.theme.background)
}
func themedSurface() -> some View {
background(Color.theme.surfaceBackground)
}
}
// MARK: -
#Preview("主题组件") {
VStack(spacing: 20) {
ThemedText("主标题", style: .title)
ThemedText("副标题文本", style: .secondary)
ThemedButton("主要按钮") {}
ThemedButton("次要按钮", style: .secondary) {}
ThemedProgressView(value: 0.6)
.frame(height: 8)
ThemedTextField("请输入内容", text: .constant(""))
}
.padding()
.themedBackground()
}
@@ -0,0 +1,367 @@
//
// ThemeSettingsView.swift
// EmotionMuseum
//
// Created by on 2025/6/13.
//
import SwiftUI
// MARK: -
struct ThemeSettingsView: View {
@EnvironmentObject var themeManager: ThemeManager
@Environment(\.dismiss) var dismiss
var body: some View {
NavigationView {
List {
//
themeSelectionSection
//
previewSection
//
advancedSettingsSection
}
.listStyle(InsetGroupedListStyle())
.navigationTitle("主题设置")
.navigationBarTitleDisplayMode(.large)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("完成") {
dismiss()
}
.foregroundColor(Color.theme.accent)
}
}
}
.themedBackground()
.preferredColorScheme(themeManager.systemFollowsDeviceTheme ? nil : (themeManager.isDarkMode ? .dark : .light))
}
// MARK: -
private var themeSelectionSection: some View {
Section {
//
ThemedListRow {
HStack {
ThemedIcon("gear.circle.fill", style: .accent, size: .medium)
VStack(alignment: .leading, spacing: 2) {
ThemedText("跟随系统", style: .headline)
ThemedText("自动适应系统的深色模式设置", style: .secondary)
}
Spacer()
Toggle("", isOn: $themeManager.systemFollowsDeviceTheme)
.toggleStyle(SwitchToggleStyle(tint: Color.theme.accent))
}
}
if !themeManager.systemFollowsDeviceTheme {
//
ThemeOptionRow(
title: "浅色模式",
description: "明亮清新的视觉体验",
icon: "sun.max.fill",
isSelected: !themeManager.isDarkMode
) {
withAnimation(AnimationConfig.smooth) {
themeManager.isDarkMode = false
}
}
//
ThemeOptionRow(
title: "深色模式",
description: "舒适护眼的暗色调体验",
icon: "moon.fill",
isSelected: themeManager.isDarkMode
) {
withAnimation(AnimationConfig.smooth) {
themeManager.isDarkMode = true
}
}
}
} header: {
ThemedText("外观模式", style: .caption)
.foregroundColor(Color.theme.secondaryText)
}
}
// MARK: -
private var previewSection: some View {
Section {
VStack(spacing: 16) {
//
PreviewCard()
//
ColorPreviewGrid()
}
.padding(.vertical, 8)
} header: {
ThemedText("预览效果", style: .caption)
.foregroundColor(Color.theme.secondaryText)
}
}
// MARK: -
private var advancedSettingsSection: some View {
Section {
//
ThemedListRow {
Button(action: resetThemeSettings) {
HStack {
ThemedIcon("arrow.clockwise.circle.fill", style: .custom(.orange), size: .medium)
ThemedText("重置主题设置", style: .headline)
Spacer()
}
}
}
//
ThemedListRow(showSeparator: false) {
NavigationLink {
ThemeInfoView()
} label: {
HStack {
ThemedIcon("info.circle.fill", style: .accent, size: .medium)
ThemedText("关于主题", style: .headline)
Spacer()
ThemedIcon("chevron.right", style: .secondary, size: .small)
}
}
}
} header: {
ThemedText("高级选项", style: .caption)
.foregroundColor(Color.theme.secondaryText)
}
}
private func resetThemeSettings() {
withAnimation(AnimationConfig.smooth) {
themeManager.systemFollowsDeviceTheme = true
themeManager.isDarkMode = false
}
//
let impactFeedback = UIImpactFeedbackGenerator(style: .medium)
impactFeedback.impactOccurred()
}
}
// MARK: -
struct ThemeOptionRow: View {
let title: String
let description: String
let icon: String
let isSelected: Bool
let action: () -> Void
var body: some View {
ThemedListRow {
Button(action: action) {
HStack(spacing: 12) {
ThemedIcon(icon, style: .accent, size: .medium)
VStack(alignment: .leading, spacing: 2) {
ThemedText(title, style: .headline)
ThemedText(description, style: .secondary)
}
Spacer()
if isSelected {
ThemedIcon("checkmark.circle.fill", style: .custom(.green), size: .medium)
.transition(.scale.combined(with: .opacity))
}
}
}
.buttonStyle(PlainButtonStyle())
}
}
}
// MARK: -
struct PreviewCard: View {
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
ThemedText("示例卡片", style: .headline)
Spacer()
ThemedIcon("heart.fill", style: .custom(.red), size: .medium)
}
ThemedText("这是一个预览卡片,展示当前主题的效果。文本清晰度和对比度都经过精心调校。", style: .secondary)
HStack {
ThemedButton("主要按钮", size: .small) {}
ThemedButton("次要按钮", style: .secondary, size: .small) {}
Spacer()
}
ThemedProgressView(value: 0.6)
.frame(height: 6)
}
.themedCard()
}
}
// MARK: -
struct ColorPreviewGrid: View {
let colors: [(String, Color)] = [
("主色调", Color.theme.accent),
("成功", Color.theme.success),
("警告", Color.theme.warning),
("错误", Color.theme.error)
]
var body: some View {
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 4), spacing: 12) {
ForEach(colors, id: \.0) { name, color in
VStack(spacing: 6) {
Circle()
.fill(color)
.frame(width: 32, height: 32)
ThemedText(name, style: .caption)
.lineLimit(1)
}
}
}
.themedCard(padding: 12)
}
}
// MARK: -
struct ThemeInfoView: View {
@Environment(\.dismiss) var dismiss
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 24) {
//
VStack(alignment: .leading, spacing: 8) {
ThemedText("关于主题系统", style: .title)
ThemedText("智能适配,呵护双眼", style: .secondary)
}
//
VStack(alignment: .leading, spacing: 16) {
FeatureRow(
icon: "eye.fill",
title: "护眼设计",
description: "精心调校的颜色对比度,长时间使用不疲劳"
)
FeatureRow(
icon: "paintbrush.fill",
title: "精美配色",
description: "专业设计师打造的色彩方案,视觉体验更佳"
)
FeatureRow(
icon: "gear.badge.checkmark",
title: "智能适配",
description: "可跟随系统设置自动切换,也可手动调节"
)
FeatureRow(
icon: "moon.stars.fill",
title: "深色模式",
description: "夜间使用更舒适,有效减少蓝光刺激"
)
}
Spacer(minLength: 32)
//
VStack(spacing: 8) {
ThemedText("主题系统 v1.0", style: .secondary)
ThemedText("情绪博物馆团队制作", style: .tertiary)
}
.frame(maxWidth: .infinity)
}
.padding(.horizontal, 20)
.padding(.vertical, 24)
}
.themedBackground()
.navigationTitle("主题信息")
.navigationBarTitleDisplayMode(.inline)
}
}
// MARK: -
struct FeatureRow: View {
let icon: String
let title: String
let description: String
var body: some View {
HStack(alignment: .top, spacing: 12) {
ThemedIcon(icon, style: .accent, size: .medium)
.frame(width: 24, height: 24)
VStack(alignment: .leading, spacing: 4) {
ThemedText(title, style: .headline)
ThemedText(description, style: .secondary)
}
}
}
}
// MARK: -
struct QuickThemeToggle: View {
@EnvironmentObject var themeManager: ThemeManager
var body: some View {
Button(action: toggleTheme) {
HStack(spacing: 8) {
ThemedIcon(
themeManager.isDarkMode ? "moon.fill" : "sun.max.fill",
style: .accent,
size: .medium
)
ThemedText(
themeManager.isDarkMode ? "深色" : "浅色",
style: .secondary
)
}
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(Color.theme.surfaceBackground)
.cornerRadius(16)
}
.buttonStyle(PlainButtonStyle())
}
private func toggleTheme() {
withAnimation(AnimationConfig.smooth) {
if themeManager.systemFollowsDeviceTheme {
themeManager.setSystemFollowing(false)
}
themeManager.toggleTheme()
}
//
let impactFeedback = UIImpactFeedbackGenerator(style: .light)
impactFeedback.impactOccurred()
}
}
// MARK: -
#Preview("主题设置") {
ThemeSettingsView()
.environmentObject(ThemeManager())
}
#Preview("快速切换") {
QuickThemeToggle()
.environmentObject(ThemeManager())
.padding()
.themedBackground()
}
@@ -0,0 +1,622 @@
//
// UniverseView.swift
// EmotionMuseum
//
// Created by on 2025/6/13.
//
import SwiftUI
struct UniverseView: View {
@State private var showingProfile = false
@State private var showingSettings = false
@State private var showingAchievements = false
@State private var showingDataExport = false
var body: some View {
NavigationView {
ScrollView {
VStack(spacing: 24) {
//
UserProfileCard(onEditProfile: {
showingProfile = true
})
//
GrowthOverviewCard()
//
VStack(spacing: 16) {
MenuSection(title: "我的成长") {
VStack(spacing: 12) {
MenuRow(
icon: "trophy.fill",
title: "成就徽章",
subtitle: "查看你的成长里程碑",
color: .yellow,
action: { showingAchievements = true }
)
MenuRow(
icon: "chart.line.uptrend.xyaxis",
title: "成长报告",
subtitle: "详细的成长数据分析",
color: .blue,
action: { /* */ }
)
MenuRow(
icon: "calendar",
title: "情绪日历",
subtitle: "回顾你的情绪历程",
color: .green,
action: { /* */ }
)
}
}
MenuSection(title: "数据管理") {
VStack(spacing: 12) {
MenuRow(
icon: "square.and.arrow.up",
title: "导出数据",
subtitle: "导出你的个人数据",
color: .purple,
action: { showingDataExport = true }
)
MenuRow(
icon: "icloud.and.arrow.up",
title: "云端同步",
subtitle: "同步到iCloud",
color: .cyan,
action: { /* */ }
)
}
}
MenuSection(title: "设置") {
VStack(spacing: 12) {
MenuRow(
icon: "bell.fill",
title: "通知设置",
subtitle: "管理提醒和通知",
color: .orange,
action: { /* */ }
)
MenuRow(
icon: "lock.fill",
title: "隐私设置",
subtitle: "数据隐私和安全",
color: .red,
action: { /* */ }
)
MenuRow(
icon: "gearshape.fill",
title: "应用设置",
subtitle: "个性化设置",
color: .gray,
action: { showingSettings = true }
)
}
}
MenuSection(title: "帮助与支持") {
VStack(spacing: 12) {
MenuRow(
icon: "questionmark.circle.fill",
title: "使用帮助",
subtitle: "常见问题和使用指南",
color: .blue,
action: { /* */ }
)
MenuRow(
icon: "envelope.fill",
title: "联系我们",
subtitle: "反馈和建议",
color: .green,
action: { /* */ }
)
MenuRow(
icon: "star.fill",
title: "评价应用",
subtitle: "在App Store评价",
color: .yellow,
action: { /* App Store */ }
)
}
}
}
//
VStack(spacing: 8) {
Text("情绪博物馆")
.font(.subheadline)
.foregroundColor(.secondary)
Text("版本 1.0.0")
.font(.caption)
.foregroundColor(.secondary)
}
.padding(.top, 20)
}
.padding(.horizontal)
.padding(.vertical)
}
.navigationTitle("我的宇宙")
.navigationBarTitleDisplayMode(.large)
}
.sheet(isPresented: $showingProfile) {
ProfileEditView()
}
.sheet(isPresented: $showingSettings) {
SettingsView()
}
.sheet(isPresented: $showingAchievements) {
AchievementsView()
}
.sheet(isPresented: $showingDataExport) {
DataExportView()
}
}
}
// MARK: -
struct UserProfileCard: View {
let onEditProfile: () -> Void
var body: some View {
VStack(spacing: 16) {
HStack {
//
Button(action: onEditProfile) {
ZStack {
Circle()
.fill(LinearGradient(
colors: [.purple, .blue],
startPoint: .topLeading,
endPoint: .bottomTrailing
))
.frame(width: 80, height: 80)
Text("")
.font(.title)
.fontWeight(.bold)
.foregroundColor(.white)
}
}
VStack(alignment: .leading, spacing: 4) {
Text("华中敏")
.font(.title2)
.fontWeight(.bold)
Text("成长探索者")
.font(.subheadline)
.foregroundColor(.secondary)
HStack(spacing: 4) {
Image(systemName: "calendar")
.font(.caption)
Text("加入 30 天")
.font(.caption)
}
.foregroundColor(.secondary)
}
Spacer()
Button(action: onEditProfile) {
Image(systemName: "pencil")
.font(.title3)
.foregroundColor(.blue)
}
}
//
VStack(spacing: 8) {
HStack {
Text("成长等级")
.font(.subheadline)
.fontWeight(.medium)
Spacer()
Text("Lv.5")
.font(.subheadline)
.fontWeight(.bold)
.foregroundColor(.purple)
}
ProgressView(value: 0.7)
.progressViewStyle(LinearProgressViewStyle(tint: .purple))
HStack {
Text("距离下一级还需 150 经验")
.font(.caption)
.foregroundColor(.secondary)
Spacer()
}
}
}
.padding()
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color(UIColor.systemGray6))
)
}
}
// MARK: -
struct GrowthOverviewCard: View {
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("本周成长数据")
.font(.headline)
.fontWeight(.semibold)
HStack(spacing: 16) {
GrowthMetricView(
title: "情绪记录",
value: "12",
unit: "",
color: .blue,
icon: "heart.fill"
)
GrowthMetricView(
title: "疗愈时长",
value: "45",
unit: "分钟",
color: .purple,
icon: "timer.circle.fill"
)
}
HStack(spacing: 16) {
GrowthMetricView(
title: "课题进展",
value: "3",
unit: "",
color: .green,
icon: "checkmark.circle.fill"
)
GrowthMetricView(
title: "连续天数",
value: "7",
unit: "",
color: .orange,
icon: "flame.fill"
)
}
}
.padding()
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color(.systemGray6))
)
}
}
struct GrowthMetricView: View {
let title: String
let value: String
let unit: String
let color: Color
let icon: String
var body: some View {
VStack(spacing: 8) {
HStack {
Image(systemName: icon)
.font(.title3)
.foregroundColor(color)
Spacer()
}
VStack(alignment: .leading, spacing: 2) {
HStack(alignment: .bottom, spacing: 2) {
Text(value)
.font(.title2)
.fontWeight(.bold)
Text(unit)
.font(.caption)
.foregroundColor(.secondary)
}
Text(title)
.font(.caption)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding()
.background(
RoundedRectangle(cornerRadius: 12)
.fill(Color(.systemBackground))
)
}
}
// MARK: -
struct MenuSection<Content: View>: View {
let title: String
let content: Content
init(title: String, @ViewBuilder content: () -> Content) {
self.title = title
self.content = content()
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text(title)
.font(.headline)
.fontWeight(.semibold)
content
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
struct MenuRow: View {
let icon: String
let title: String
let subtitle: String
let color: Color
let action: () -> Void
var body: some View {
Button(action: action) {
HStack(spacing: 16) {
ZStack {
RoundedRectangle(cornerRadius: 8)
.fill(color.opacity(0.1))
.frame(width: 40, height: 40)
Image(systemName: icon)
.font(.title3)
.foregroundColor(color)
}
VStack(alignment: .leading, spacing: 2) {
Text(title)
.font(.subheadline)
.fontWeight(.medium)
.foregroundColor(.primary)
Text(subtitle)
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
Image(systemName: "chevron.right")
.font(.caption)
.foregroundColor(.secondary)
}
.padding()
.background(
RoundedRectangle(cornerRadius: 12)
.fill(Color(.systemGray6))
)
}
.buttonStyle(PlainButtonStyle())
}
}
// MARK: -
struct ProfileEditView: View {
@Environment(\.dismiss) private var dismiss
var body: some View {
NavigationView {
VStack {
Text("个人资料编辑")
.font(.title)
Text("这里是个人资料编辑页面")
.foregroundColor(.secondary)
}
.navigationTitle("编辑资料")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("取消") {
dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("保存") {
dismiss()
}
}
}
}
}
}
struct SettingsView: View {
@Environment(\.dismiss) private var dismiss
var body: some View {
NavigationView {
VStack {
Text("应用设置")
.font(.title)
Text("这里是应用设置页面")
.foregroundColor(.secondary)
}
.navigationTitle("设置")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("完成") {
dismiss()
}
}
}
}
}
}
struct AchievementsView: View {
@Environment(\.dismiss) private var dismiss
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 16) {
ForEach(0..<6) { index in
AchievementCard(achievement: sampleAchievements[index])
}
}
.padding()
}
.navigationTitle("成就徽章")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("完成") {
dismiss()
}
}
}
}
}
private var sampleAchievements: [Achievement] {
[
Achievement(title: "初心者", description: "完成第一次情绪记录", category: .milestone, icon: "star.fill", rarity: .common, requirement: .conversationCount(1), targetValue: 1, unlockedAt: Date()),
Achievement(title: "坚持者", description: "连续7天记录情绪", category: .consistency, icon: "flame.fill", rarity: .rare, requirement: .consecutiveDays(7), targetValue: 7, unlockedAt: Date()),
Achievement(title: "探索者", description: "开始第一个课题", category: .growth, icon: "map.fill", rarity: .common, requirement: .topicCompletion(1), targetValue: 1, unlockedAt: Date()),
Achievement(title: "疗愈师", description: "完成10次脉轮疗愈", category: .emotion, icon: "heart.fill", rarity: .epic, requirement: .emotionRecordCount(10), targetValue: 10),
Achievement(title: "成长者", description: "完成一个完整课题", category: .growth, icon: "trophy.fill", rarity: .rare, requirement: .topicCompletion(5), targetValue: 5),
Achievement(title: "大师", description: "达到10级成长等级", category: .milestone, icon: "crown.fill", rarity: .legendary, requirement: .totalPoints(10000), targetValue: 10000)
]
}
}
struct DataExportView: View {
@Environment(\.dismiss) private var dismiss
var body: some View {
NavigationView {
VStack(spacing: 20) {
Text("数据导出")
.font(.title)
Text("选择要导出的数据类型")
.foregroundColor(.secondary)
VStack(spacing: 12) {
ExportOptionRow(title: "情绪记录", description: "所有的情绪记录数据")
ExportOptionRow(title: "疗愈记录", description: "脉轮疗愈会话记录")
ExportOptionRow(title: "课题进展", description: "成长课题和进展数据")
ExportOptionRow(title: "成就数据", description: "解锁的成就和里程碑")
}
Spacer()
Button("导出数据") {
//
}
.buttonStyle(.borderedProminent)
}
.padding()
.navigationTitle("导出数据")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("取消") {
dismiss()
}
}
}
}
}
}
struct ExportOptionRow: View {
let title: String
let description: String
@State private var isSelected = false
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.subheadline)
.fontWeight(.medium)
Text(description)
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
Toggle("", isOn: $isSelected)
}
.padding()
.background(
RoundedRectangle(cornerRadius: 12)
.fill(Color(.systemGray6))
)
}
}
struct AchievementCard: View {
let achievement: Achievement
var body: some View {
VStack(spacing: 12) {
ZStack {
Circle()
.fill(achievement.isUnlocked ? achievement.rarity.color.opacity(0.2) : Color.gray.opacity(0.2))
.frame(width: 60, height: 60)
Image(systemName: achievement.icon)
.font(.title2)
.foregroundColor(achievement.isUnlocked ? achievement.rarity.color : .gray)
}
VStack(spacing: 4) {
Text(achievement.title)
.font(.subheadline)
.fontWeight(.medium)
.foregroundColor(achievement.isUnlocked ? .primary : .secondary)
Text(achievement.description)
.font(.caption)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.lineLimit(2)
}
}
.padding()
.background(
RoundedRectangle(cornerRadius: 12)
.fill(Color(.systemGray6))
)
.opacity(achievement.isUnlocked ? 1.0 : 0.6)
}
}
// MARK: - 使DataModelsAchievement
#Preview {
UniverseView()
}
-20
View File
@@ -1,20 +0,0 @@
//
// EmotionMuseumApp.swift
// EmotionMuseum
//
// Created by on 2025/5/26.
//
import SwiftUI
@main
struct EmotionMuseumApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}
@@ -0,0 +1,17 @@
//
// EmotionMuseumTests.swift
// EmotionMuseumTests
//
// Created by on 2025/6/13.
//
import Testing
@testable import EmotionMuseum
struct EmotionMuseumTests {
@Test func example() async throws {
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
}
}
@@ -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>
@@ -0,0 +1,41 @@
//
// EmotionMuseumUITests.swift
// EmotionMuseumUITests
//
// Created by on 2025/6/13.
//
import XCTest
final class EmotionMuseumUITests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
// In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false
// In UI tests its important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
@MainActor
func testExample() throws {
// UI tests must launch the application that they test.
let app = XCUIApplication()
app.launch()
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
@MainActor
func testLaunchPerformance() throws {
// This measures how long it takes to launch your application.
measure(metrics: [XCTApplicationLaunchMetric()]) {
XCUIApplication().launch()
}
}
}
@@ -0,0 +1,33 @@
//
// EmotionMuseumUITestsLaunchTests.swift
// EmotionMuseumUITests
//
// Created by on 2025/6/13.
//
import XCTest
final class EmotionMuseumUITestsLaunchTests: XCTestCase {
override class var runsForEachTargetApplicationUIConfiguration: Bool {
true
}
override func setUpWithError() throws {
continueAfterFailure = false
}
@MainActor
func testLaunch() throws {
let app = XCUIApplication()
app.launch()
// Insert steps here to perform after app launch but before taking a screenshot,
// such as logging into a test account or navigating somewhere in the app
let attachment = XCTAttachment(screenshot: app.screenshot())
attachment.name = "Launch Screen"
attachment.lifetime = .keepAlways
add(attachment)
}
}
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>
Binary file not shown.
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 KiB