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