feat: 添加Swift Package管理和API功能模块
新增Package.swift和Package.resolved文件以支持Swift Package管理,创建API相关文件(API.swift、APICaller.swift、APIConstants.swift、APIEndpoints.swift、APIService.swift、APILogger.swift、APIModels.swift、Integration-Guide.md)以实现API请求管理和网络交互功能,增强项目的功能性和可扩展性。同时更新.gitignore以排除构建文件和临时文件。
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -2,3 +2,9 @@ Pods
|
|||||||
.vscode
|
.vscode
|
||||||
yana.xcworkspace/xcuserdata/edwinqqq.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
|
yana.xcworkspace/xcuserdata/edwinqqq.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
|
||||||
*.xcbkptlist
|
*.xcbkptlist
|
||||||
|
.build/checkouts
|
||||||
|
.build/index-build
|
||||||
|
.build
|
||||||
|
.cursor
|
||||||
|
.swiftpm
|
||||||
|
yana.xcworkspace/xcuserdata/edwinqqq.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
|
||||||
|
131
Package.resolved
Normal file
131
Package.resolved
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
{
|
||||||
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "combine-schedulers",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/combine-schedulers",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "5928286acce13def418ec36d05a001a9641086f2",
|
||||||
|
"version" : "1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-case-paths",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-case-paths",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "41b89b8b68d8c56c622dbb7132258f1a3e638b25",
|
||||||
|
"version" : "1.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-clocks",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-clocks",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "cc46202b53476d64e824e0b6612da09d84ffde8e",
|
||||||
|
"version" : "1.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-collections",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-collections",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "c1805596154bb3a265fd91b8ac0c4433b4348fb0",
|
||||||
|
"version" : "1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-composable-architecture",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-composable-architecture",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "6574de2396319a58e86e2178577268cb4aeccc30",
|
||||||
|
"version" : "1.20.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-concurrency-extras",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "82a4ae7170d98d8538ec77238b7eb8e7199ef2e8",
|
||||||
|
"version" : "1.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-custom-dump",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-custom-dump",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1",
|
||||||
|
"version" : "1.3.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-dependencies",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-dependencies",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "4c90d6b2b9bf0911af87b103bb40f41771891596",
|
||||||
|
"version" : "1.9.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-identified-collections",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-identified-collections",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "322d9ffeeba85c9f7c4984b39422ec7cc3c56597",
|
||||||
|
"version" : "1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-navigation",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-navigation",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "db6bc9dbfed001f21e6728fd36413d9342c235b4",
|
||||||
|
"version" : "2.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-perception",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-perception",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "d924c62a70fca5f43872f286dbd7cef0957f1c01",
|
||||||
|
"version" : "1.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-sharing",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-sharing",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "75e846ee3159dc75b3a29bfc24b6ce5a557ddca9",
|
||||||
|
"version" : "2.5.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-syntax",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/swiftlang/swift-syntax",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2",
|
||||||
|
"version" : "601.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "xctest-dynamic-overlay",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "39de59b2d47f7ef3ca88a039dff3084688fe27f4",
|
||||||
|
"version" : "1.5.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 2
|
||||||
|
}
|
32
Package.swift
Normal file
32
Package.swift
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// swift-tools-version: 5.9
|
||||||
|
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
name: "yana",
|
||||||
|
platforms: [
|
||||||
|
.iOS(.v17),
|
||||||
|
.macOS(.v14)
|
||||||
|
],
|
||||||
|
products: [
|
||||||
|
.library(
|
||||||
|
name: "yana",
|
||||||
|
targets: ["yana"]
|
||||||
|
),
|
||||||
|
],
|
||||||
|
dependencies: [
|
||||||
|
.package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "1.8.0"),
|
||||||
|
],
|
||||||
|
targets: [
|
||||||
|
.target(
|
||||||
|
name: "yana",
|
||||||
|
dependencies: [
|
||||||
|
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.testTarget(
|
||||||
|
name: "yanaTests",
|
||||||
|
dependencies: ["yana"]
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
2
Podfile
2
Podfile
@@ -1,5 +1,5 @@
|
|||||||
# Uncomment the next line to define a global platform for your project
|
# Uncomment the next line to define a global platform for your project
|
||||||
platform :ios, '15.6'
|
platform :ios, '13.0'
|
||||||
|
|
||||||
target 'yana' do
|
target 'yana' do
|
||||||
# Comment the next line if you don't want to use dynamic frameworks
|
# Comment the next line if you don't want to use dynamic frameworks
|
||||||
|
@@ -122,6 +122,6 @@ SPEC CHECKSUMS:
|
|||||||
YXAlog: 6fdd73102ba0a16933dd7bef426d6011d913c041
|
YXAlog: 6fdd73102ba0a16933dd7bef426d6011d913c041
|
||||||
YXArtemis_XCFramework: d298161285aa9cf0c99800b17847dc99aef60617
|
YXArtemis_XCFramework: d298161285aa9cf0c99800b17847dc99aef60617
|
||||||
|
|
||||||
PODFILE CHECKSUM: 4034a059527d37196c5dca32d338b37b71e31488
|
PODFILE CHECKSUM: 1d74a8886888ebdfb5a6d41769a74dd0a3026dec
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
@@ -7,6 +7,9 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
4C4C91092DE85A3E00384527 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C4C91082DE85A3E00384527 /* SystemConfiguration.framework */; };
|
||||||
|
4C4C910B2DE85A4F00384527 /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C4C910A2DE85A4F00384527 /* CoreFoundation.framework */; };
|
||||||
|
4C4C91302DE864F000384527 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = 4C4C912F2DE864F000384527 /* ComposableArchitecture */; };
|
||||||
856EF8A28776CEF6CE595B76 /* Pods_yana.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D8529F57AF9337F626C670ED /* Pods_yana.framework */; };
|
856EF8A28776CEF6CE595B76 /* Pods_yana.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D8529F57AF9337F626C670ED /* Pods_yana.framework */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
@@ -24,6 +27,8 @@
|
|||||||
0977E1E6E883533DD125CAD4 /* Pods-yana.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-yana.debug.xcconfig"; path = "Target Support Files/Pods-yana/Pods-yana.debug.xcconfig"; sourceTree = "<group>"; };
|
0977E1E6E883533DD125CAD4 /* Pods-yana.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-yana.debug.xcconfig"; path = "Target Support Files/Pods-yana/Pods-yana.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
4C3E651F2DB61F7A00E5A455 /* yana.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = yana.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
4C3E651F2DB61F7A00E5A455 /* yana.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = yana.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
4C4C8FBD2DE5AF9200384527 /* yanaAPITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = yanaAPITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
4C4C8FBD2DE5AF9200384527 /* yanaAPITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = yanaAPITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
4C4C91082DE85A3E00384527 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; };
|
||||||
|
4C4C910A2DE85A4F00384527 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; };
|
||||||
977CD030E95CB064179F3A1B /* Pods-yana.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-yana.release.xcconfig"; path = "Target Support Files/Pods-yana/Pods-yana.release.xcconfig"; sourceTree = "<group>"; };
|
977CD030E95CB064179F3A1B /* Pods-yana.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-yana.release.xcconfig"; path = "Target Support Files/Pods-yana/Pods-yana.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
D8529F57AF9337F626C670ED /* Pods_yana.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_yana.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
D8529F57AF9337F626C670ED /* Pods_yana.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_yana.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
@@ -60,7 +65,10 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
4C4C91092DE85A3E00384527 /* SystemConfiguration.framework in Frameworks */,
|
||||||
856EF8A28776CEF6CE595B76 /* Pods_yana.framework in Frameworks */,
|
856EF8A28776CEF6CE595B76 /* Pods_yana.framework in Frameworks */,
|
||||||
|
4C4C91302DE864F000384527 /* ComposableArchitecture in Frameworks */,
|
||||||
|
4C4C910B2DE85A4F00384527 /* CoreFoundation.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -105,6 +113,8 @@
|
|||||||
556C2003CCDA5AC2C56882D0 /* Frameworks */ = {
|
556C2003CCDA5AC2C56882D0 /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
4C4C910A2DE85A4F00384527 /* CoreFoundation.framework */,
|
||||||
|
4C4C91082DE85A3E00384527 /* SystemConfiguration.framework */,
|
||||||
D8529F57AF9337F626C670ED /* Pods_yana.framework */,
|
D8529F57AF9337F626C670ED /* Pods_yana.framework */,
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
@@ -204,6 +214,9 @@
|
|||||||
);
|
);
|
||||||
mainGroup = 4C3E65162DB61F7A00E5A455;
|
mainGroup = 4C3E65162DB61F7A00E5A455;
|
||||||
minimizedProjectReferenceProxies = 1;
|
minimizedProjectReferenceProxies = 1;
|
||||||
|
packageReferences = (
|
||||||
|
4C4C912E2DE864F000384527 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */,
|
||||||
|
);
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = 4C3E65202DB61F7A00E5A455 /* Products */;
|
productRefGroup = 4C3E65202DB61F7A00E5A455 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
@@ -431,6 +444,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = yana/yana.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = EKM7RAGNA6;
|
DEVELOPMENT_TEAM = EKM7RAGNA6;
|
||||||
@@ -463,6 +477,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 20.20.61;
|
MARKETING_VERSION = 20.20.61;
|
||||||
|
OTHER_LIBTOOLFLAGS = "-framework \"SystemConfiguration\"";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.stupidmonkey.yana.yana;
|
PRODUCT_BUNDLE_IDENTIFIER = com.stupidmonkey.yana.yana;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
@@ -482,6 +497,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = yana/yana.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = EKM7RAGNA6;
|
DEVELOPMENT_TEAM = EKM7RAGNA6;
|
||||||
@@ -514,6 +530,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 20.20.61;
|
MARKETING_VERSION = 20.20.61;
|
||||||
|
OTHER_LIBTOOLFLAGS = "-framework \"SystemConfiguration\"";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.stupidmonkey.yana.yana;
|
PRODUCT_BUNDLE_IDENTIFIER = com.stupidmonkey.yana.yana;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
@@ -604,6 +621,25 @@
|
|||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
|
4C4C912E2DE864F000384527 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/pointfreeco/swift-composable-architecture";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 1.20.2;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
4C4C912F2DE864F000384527 /* ComposableArchitecture */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 4C4C912E2DE864F000384527 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */;
|
||||||
|
productName = ComposableArchitecture;
|
||||||
|
};
|
||||||
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = 4C3E65172DB61F7A00E5A455 /* Project object */;
|
rootObject = 4C3E65172DB61F7A00E5A455 /* Project object */;
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,132 @@
|
|||||||
|
{
|
||||||
|
"originHash" : "e7024a9cd3fa1c40fa4003b3b2b186c00ba720c787de8ba274caf8fc530677e8",
|
||||||
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "combine-schedulers",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/combine-schedulers",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "5928286acce13def418ec36d05a001a9641086f2",
|
||||||
|
"version" : "1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-case-paths",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-case-paths",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "41b89b8b68d8c56c622dbb7132258f1a3e638b25",
|
||||||
|
"version" : "1.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-clocks",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-clocks",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "cc46202b53476d64e824e0b6612da09d84ffde8e",
|
||||||
|
"version" : "1.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-collections",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-collections",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "c1805596154bb3a265fd91b8ac0c4433b4348fb0",
|
||||||
|
"version" : "1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-composable-architecture",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-composable-architecture",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "6574de2396319a58e86e2178577268cb4aeccc30",
|
||||||
|
"version" : "1.20.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-concurrency-extras",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "82a4ae7170d98d8538ec77238b7eb8e7199ef2e8",
|
||||||
|
"version" : "1.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-custom-dump",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-custom-dump",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1",
|
||||||
|
"version" : "1.3.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-dependencies",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-dependencies",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "4c90d6b2b9bf0911af87b103bb40f41771891596",
|
||||||
|
"version" : "1.9.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-identified-collections",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-identified-collections",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "322d9ffeeba85c9f7c4984b39422ec7cc3c56597",
|
||||||
|
"version" : "1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-navigation",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-navigation",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "db6bc9dbfed001f21e6728fd36413d9342c235b4",
|
||||||
|
"version" : "2.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-perception",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-perception",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "d924c62a70fca5f43872f286dbd7cef0957f1c01",
|
||||||
|
"version" : "1.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-sharing",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-sharing",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "75e846ee3159dc75b3a29bfc24b6ce5a557ddca9",
|
||||||
|
"version" : "2.5.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-syntax",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/swiftlang/swift-syntax",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2",
|
||||||
|
"version" : "601.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "xctest-dynamic-overlay",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "39de59b2d47f7ef3ca88a039dff3084688fe27f4",
|
||||||
|
"version" : "1.5.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 3
|
||||||
|
}
|
132
yana.xcworkspace/xcshareddata/swiftpm/Package.resolved
Normal file
132
yana.xcworkspace/xcshareddata/swiftpm/Package.resolved
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
{
|
||||||
|
"originHash" : "e7024a9cd3fa1c40fa4003b3b2b186c00ba720c787de8ba274caf8fc530677e8",
|
||||||
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "combine-schedulers",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/combine-schedulers",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "5928286acce13def418ec36d05a001a9641086f2",
|
||||||
|
"version" : "1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-case-paths",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-case-paths",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "41b89b8b68d8c56c622dbb7132258f1a3e638b25",
|
||||||
|
"version" : "1.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-clocks",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-clocks",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "cc46202b53476d64e824e0b6612da09d84ffde8e",
|
||||||
|
"version" : "1.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-collections",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-collections",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "c1805596154bb3a265fd91b8ac0c4433b4348fb0",
|
||||||
|
"version" : "1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-composable-architecture",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-composable-architecture",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "6574de2396319a58e86e2178577268cb4aeccc30",
|
||||||
|
"version" : "1.20.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-concurrency-extras",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "82a4ae7170d98d8538ec77238b7eb8e7199ef2e8",
|
||||||
|
"version" : "1.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-custom-dump",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-custom-dump",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1",
|
||||||
|
"version" : "1.3.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-dependencies",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-dependencies",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "4c90d6b2b9bf0911af87b103bb40f41771891596",
|
||||||
|
"version" : "1.9.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-identified-collections",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-identified-collections",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "322d9ffeeba85c9f7c4984b39422ec7cc3c56597",
|
||||||
|
"version" : "1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-navigation",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-navigation",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "db6bc9dbfed001f21e6728fd36413d9342c235b4",
|
||||||
|
"version" : "2.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-perception",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-perception",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "d924c62a70fca5f43872f286dbd7cef0957f1c01",
|
||||||
|
"version" : "1.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-sharing",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-sharing",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "75e846ee3159dc75b3a29bfc24b6ce5a557ddca9",
|
||||||
|
"version" : "2.5.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-syntax",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/swiftlang/swift-syntax",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2",
|
||||||
|
"version" : "601.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "xctest-dynamic-overlay",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "39de59b2d47f7ef3ca88a039dff3084688fe27f4",
|
||||||
|
"version" : "1.5.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 3
|
||||||
|
}
|
@@ -68,38 +68,6 @@
|
|||||||
landmarkType = "7">
|
landmarkType = "7">
|
||||||
</BreakpointContent>
|
</BreakpointContent>
|
||||||
</BreakpointProxy>
|
</BreakpointProxy>
|
||||||
<BreakpointProxy
|
|
||||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
|
||||||
<BreakpointContent
|
|
||||||
uuid = "1268A929-970C-4C74-B3E5-09976D796C5E"
|
|
||||||
shouldBeEnabled = "No"
|
|
||||||
ignoreCount = "0"
|
|
||||||
continueAfterRunningActions = "No"
|
|
||||||
filePath = "yana/Managers/NetworkManager.swift"
|
|
||||||
startingColumnNumber = "9223372036854775807"
|
|
||||||
endingColumnNumber = "9223372036854775807"
|
|
||||||
startingLineNumber = "54"
|
|
||||||
endingLineNumber = "54"
|
|
||||||
landmarkName = "String"
|
|
||||||
landmarkType = "21">
|
|
||||||
</BreakpointContent>
|
|
||||||
</BreakpointProxy>
|
|
||||||
<BreakpointProxy
|
|
||||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
|
||||||
<BreakpointContent
|
|
||||||
uuid = "DB8B4E7E-87FD-4E45-86F7-D21CF24F3B08"
|
|
||||||
shouldBeEnabled = "No"
|
|
||||||
ignoreCount = "0"
|
|
||||||
continueAfterRunningActions = "No"
|
|
||||||
filePath = "yana/Configs/ClientConfig.swift"
|
|
||||||
startingColumnNumber = "9223372036854775807"
|
|
||||||
endingColumnNumber = "9223372036854775807"
|
|
||||||
startingLineNumber = "14"
|
|
||||||
endingLineNumber = "14"
|
|
||||||
landmarkName = "initializeClient()"
|
|
||||||
landmarkType = "7">
|
|
||||||
</BreakpointContent>
|
|
||||||
</BreakpointProxy>
|
|
||||||
<BreakpointProxy
|
<BreakpointProxy
|
||||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||||
<BreakpointContent
|
<BreakpointContent
|
||||||
@@ -110,10 +78,10 @@
|
|||||||
filePath = "yana/Managers/NetworkManager.swift"
|
filePath = "yana/Managers/NetworkManager.swift"
|
||||||
startingColumnNumber = "9223372036854775807"
|
startingColumnNumber = "9223372036854775807"
|
||||||
endingColumnNumber = "9223372036854775807"
|
endingColumnNumber = "9223372036854775807"
|
||||||
startingLineNumber = "519"
|
startingLineNumber = "521"
|
||||||
endingLineNumber = "519"
|
endingLineNumber = "521"
|
||||||
landmarkName = "request(_:didValidateRequest:response:data:)"
|
landmarkName = "unknown"
|
||||||
landmarkType = "7">
|
landmarkType = "0">
|
||||||
</BreakpointContent>
|
</BreakpointContent>
|
||||||
</BreakpointProxy>
|
</BreakpointProxy>
|
||||||
<BreakpointProxy
|
<BreakpointProxy
|
||||||
@@ -126,25 +94,57 @@
|
|||||||
filePath = "yana/Managers/NetworkManager.swift"
|
filePath = "yana/Managers/NetworkManager.swift"
|
||||||
startingColumnNumber = "9223372036854775807"
|
startingColumnNumber = "9223372036854775807"
|
||||||
endingColumnNumber = "9223372036854775807"
|
endingColumnNumber = "9223372036854775807"
|
||||||
startingLineNumber = "326"
|
startingLineNumber = "328"
|
||||||
endingLineNumber = "326"
|
endingLineNumber = "328"
|
||||||
landmarkName = "enhancedRequest(path:method:queryItems:bodyParameters:responseType:completion:)"
|
landmarkName = "unknown"
|
||||||
landmarkType = "7">
|
landmarkType = "0">
|
||||||
</BreakpointContent>
|
</BreakpointContent>
|
||||||
</BreakpointProxy>
|
</BreakpointProxy>
|
||||||
<BreakpointProxy
|
<BreakpointProxy
|
||||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||||
<BreakpointContent
|
<BreakpointContent
|
||||||
uuid = "198A1AE8-A7A4-4A66-A4D3-DF86D873E2AE"
|
uuid = "198A1AE8-A7A4-4A66-A4D3-DF86D873E2AE"
|
||||||
|
shouldBeEnabled = "No"
|
||||||
|
ignoreCount = "0"
|
||||||
|
continueAfterRunningActions = "No"
|
||||||
|
filePath = "yana/Managers/NetworkManager.swift"
|
||||||
|
startingColumnNumber = "9223372036854775807"
|
||||||
|
endingColumnNumber = "9223372036854775807"
|
||||||
|
startingLineNumber = "363"
|
||||||
|
endingLineNumber = "363"
|
||||||
|
landmarkName = "unknown"
|
||||||
|
landmarkType = "0">
|
||||||
|
</BreakpointContent>
|
||||||
|
</BreakpointProxy>
|
||||||
|
<BreakpointProxy
|
||||||
|
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||||
|
<BreakpointContent
|
||||||
|
uuid = "E026A08A-FE1E-4C73-A2EC-9CCA3F2FB9C1"
|
||||||
shouldBeEnabled = "Yes"
|
shouldBeEnabled = "Yes"
|
||||||
ignoreCount = "0"
|
ignoreCount = "0"
|
||||||
continueAfterRunningActions = "No"
|
continueAfterRunningActions = "No"
|
||||||
filePath = "yana/Managers/NetworkManager.swift"
|
filePath = "yana/Managers/NetworkManager.swift"
|
||||||
startingColumnNumber = "9223372036854775807"
|
startingColumnNumber = "9223372036854775807"
|
||||||
endingColumnNumber = "9223372036854775807"
|
endingColumnNumber = "9223372036854775807"
|
||||||
startingLineNumber = "361"
|
startingLineNumber = "314"
|
||||||
endingLineNumber = "361"
|
endingLineNumber = "314"
|
||||||
landmarkName = "enhancedRequest(path:method:queryItems:bodyParameters:responseType:completion:)"
|
landmarkName = "unknown"
|
||||||
|
landmarkType = "0">
|
||||||
|
</BreakpointContent>
|
||||||
|
</BreakpointProxy>
|
||||||
|
<BreakpointProxy
|
||||||
|
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||||
|
<BreakpointContent
|
||||||
|
uuid = "2591B697-A3D2-4AFB-8144-67EC0ADE3C6B"
|
||||||
|
shouldBeEnabled = "Yes"
|
||||||
|
ignoreCount = "0"
|
||||||
|
continueAfterRunningActions = "No"
|
||||||
|
filePath = "Pods/Alamofire/Source/Core/Session.swift"
|
||||||
|
startingColumnNumber = "9223372036854775807"
|
||||||
|
endingColumnNumber = "9223372036854775807"
|
||||||
|
startingLineNumber = "287"
|
||||||
|
endingLineNumber = "287"
|
||||||
|
landmarkName = "request(_:method:parameters:encoding:headers:interceptor:requestModifier:)"
|
||||||
landmarkType = "7">
|
landmarkType = "7">
|
||||||
</BreakpointContent>
|
</BreakpointContent>
|
||||||
</BreakpointProxy>
|
</BreakpointProxy>
|
||||||
|
@@ -1,59 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import Alamofire
|
|
||||||
|
|
||||||
enum HttpRequestMethod: String {
|
|
||||||
case get = "GET"
|
|
||||||
case post = "POST"
|
|
||||||
// 可扩展其他方法
|
|
||||||
}
|
|
||||||
|
|
||||||
typealias HttpRequestCompletion = (Result<Data, Error>) -> Void
|
|
||||||
|
|
||||||
class API {
|
|
||||||
// 通用请求方法
|
|
||||||
static func makeRequest(
|
|
||||||
route: String,
|
|
||||||
method: HttpRequestMethod,
|
|
||||||
params: [String: Any],
|
|
||||||
completion: @escaping HttpRequestCompletion
|
|
||||||
) {
|
|
||||||
let httpMethod: HTTPMethod = {
|
|
||||||
switch method {
|
|
||||||
case .get: return .get
|
|
||||||
case .post: return .post
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
NetworkManager.shared.request(route, method: httpMethod, parameters: params) { (result: Result<Data, NetworkError>) in
|
|
||||||
switch result {
|
|
||||||
case .success(let data):
|
|
||||||
completion(.success(data))
|
|
||||||
case .failure(let error):
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 具体接口方法示例
|
|
||||||
static func getUserInfo(uid: String, completion: @escaping HttpRequestCompletion) {
|
|
||||||
let route = "user/get"
|
|
||||||
let params = ["uid": uid]
|
|
||||||
makeRequest(route: route, method: .get, params: params, completion: completion)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func phoneSmsCode(mobile: String, type: String, phoneAreaCode: String, completion: @escaping HttpRequestCompletion) {
|
|
||||||
let route = "sms/getCode"
|
|
||||||
let params = ["mobile": mobile, "type": type, "phoneAreaCode": phoneAreaCode]
|
|
||||||
makeRequest(route: route, method: .post, params: params, completion: completion)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension API //ClientConfig
|
|
||||||
{
|
|
||||||
static func clientInit(completion: @escaping HttpRequestCompletion) {
|
|
||||||
makeRequest(route: "client/init",
|
|
||||||
method: .get,
|
|
||||||
params: [:],
|
|
||||||
completion: completion)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,8 +0,0 @@
|
|||||||
//
|
|
||||||
// APICaller.swift
|
|
||||||
// yana
|
|
||||||
//
|
|
||||||
// Created by P on 2025/5/29.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
26
yana/APIs/APIConstants.swift
Normal file
26
yana/APIs/APIConstants.swift
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum APIConstants {
|
||||||
|
// MARK: - Base URLs
|
||||||
|
static let baseURL = "http://beta.api.molistar.xyz"
|
||||||
|
|
||||||
|
// MARK: - Common Headers
|
||||||
|
static let defaultHeaders: [String: String] = [
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
"platform": "ios",
|
||||||
|
"version": "1.0.0"
|
||||||
|
]
|
||||||
|
|
||||||
|
// MARK: - Endpoints
|
||||||
|
enum Endpoints {
|
||||||
|
static let clientInit = "/client/config"
|
||||||
|
static let login = "/user/login"
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Common Parameters
|
||||||
|
static let commonParameters: [String: String] = [:
|
||||||
|
// "platform": "ios",
|
||||||
|
// "version": "1.0.0"
|
||||||
|
]
|
||||||
|
}
|
38
yana/APIs/APIEndpoints.swift
Normal file
38
yana/APIs/APIEndpoints.swift
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
// MARK: - API Endpoints
|
||||||
|
enum APIEndpoint: String, CaseIterable {
|
||||||
|
case config = "/client/config"
|
||||||
|
case login = "/auth/login"
|
||||||
|
// 可以继续添加其他端点
|
||||||
|
|
||||||
|
var path: String {
|
||||||
|
return self.rawValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - API Configuration
|
||||||
|
struct APIConfiguration {
|
||||||
|
static let baseURL = "http://beta.api.molistar.xyz"
|
||||||
|
static let timeout: TimeInterval = 30.0
|
||||||
|
static let maxDataSize: Int = 50 * 1024 * 1024 // 50MB 限制,防止资源超限
|
||||||
|
|
||||||
|
// 默认请求头
|
||||||
|
static let defaultHeaders: [String: String] = [
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
// "User-Agent": "yana-iOS/\(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0")"
|
||||||
|
"User-Agent": "YuMi/20.20.61 (iPhone; iOS 18.3.1; Scale/3.00)",
|
||||||
|
"Accept-Language": "zh-Hant",
|
||||||
|
"Accept-Encoding": "gzip, br"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Request Models
|
||||||
|
struct LoginRequest: Codable {
|
||||||
|
let username: String
|
||||||
|
let password: String
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Empty Request
|
||||||
|
struct EmptyRequest: Codable {}
|
214
yana/APIs/APILogger.swift
Normal file
214
yana/APIs/APILogger.swift
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
// MARK: - API Logger
|
||||||
|
class APILogger {
|
||||||
|
enum LogLevel {
|
||||||
|
case none
|
||||||
|
case basic
|
||||||
|
case detailed
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
static var logLevel: LogLevel = .detailed
|
||||||
|
#else
|
||||||
|
static var logLevel: LogLevel = .none
|
||||||
|
#endif
|
||||||
|
|
||||||
|
private static let dateFormatter: DateFormatter = {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateFormat = "HH:mm:ss.SSS"
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
|
||||||
|
// MARK: - Request Logging
|
||||||
|
static func logRequest<T: APIRequestProtocol>(_ request: T, url: URL, body: Data?, finalHeaders: [String: String]? = nil) {
|
||||||
|
guard logLevel != .none else { return }
|
||||||
|
|
||||||
|
let timestamp = dateFormatter.string(from: Date())
|
||||||
|
|
||||||
|
print("\n🚀 [API Request] [\(timestamp)] ==================")
|
||||||
|
print("📍 Endpoint: \(request.endpoint)")
|
||||||
|
print("🔗 Full URL: \(url.absoluteString)")
|
||||||
|
print("📝 Method: \(request.method.rawValue)")
|
||||||
|
print("⏰ Timeout: \(request.timeout)s")
|
||||||
|
|
||||||
|
// 显示最终的完整 headers(包括默认 headers 和自定义 headers)
|
||||||
|
if let headers = finalHeaders, !headers.isEmpty {
|
||||||
|
if logLevel == .detailed {
|
||||||
|
print("📋 Final Headers (包括默认 + 自定义):")
|
||||||
|
for (key, value) in headers.sorted(by: { $0.key < $1.key }) {
|
||||||
|
print(" \(key): \(value)")
|
||||||
|
}
|
||||||
|
} else if logLevel == .basic {
|
||||||
|
print("📋 Headers: \(headers.count) 个 headers")
|
||||||
|
// 只显示重要的 headers
|
||||||
|
let importantHeaders = ["Content-Type", "Accept", "User-Agent", "Authorization"]
|
||||||
|
for key in importantHeaders {
|
||||||
|
if let value = headers[key] {
|
||||||
|
print(" \(key): \(value)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let customHeaders = request.headers, !customHeaders.isEmpty {
|
||||||
|
print("📋 Custom Headers:")
|
||||||
|
for (key, value) in customHeaders.sorted(by: { $0.key < $1.key }) {
|
||||||
|
print(" \(key): \(value)")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print("📋 Headers: 使用默认 headers")
|
||||||
|
}
|
||||||
|
|
||||||
|
if let queryParams = request.queryParameters, !queryParams.isEmpty {
|
||||||
|
print("🔍 Query Parameters:")
|
||||||
|
for (key, value) in queryParams.sorted(by: { $0.key < $1.key }) {
|
||||||
|
print(" \(key): \(value)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if logLevel == .detailed {
|
||||||
|
if let body = body {
|
||||||
|
print("📦 Request Body (\(body.count) bytes):")
|
||||||
|
if let jsonObject = try? JSONSerialization.jsonObject(with: body, options: []),
|
||||||
|
let prettyData = try? JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted),
|
||||||
|
let prettyString = String(data: prettyData, encoding: .utf8) {
|
||||||
|
print(prettyString)
|
||||||
|
} else if let rawString = String(data: body, encoding: .utf8) {
|
||||||
|
print(rawString)
|
||||||
|
} else {
|
||||||
|
print("Binary data")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print("📦 Request Body: No body")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示基础参数信息(仅详细模式)
|
||||||
|
if request.includeBaseParameters {
|
||||||
|
print("📱 Base Parameters: 自动注入设备和应用信息")
|
||||||
|
let baseParams = BaseRequest()
|
||||||
|
print(" Device: \(baseParams.model), OS: \(baseParams.os) \(baseParams.osVersion)")
|
||||||
|
print(" App: \(baseParams.app) v\(baseParams.appVersion)")
|
||||||
|
print(" Language: \(baseParams.acceptLanguage)")
|
||||||
|
}
|
||||||
|
} else if logLevel == .basic {
|
||||||
|
if let body = body {
|
||||||
|
print("📦 Request Body: \(formatBytes(body.count))")
|
||||||
|
} else {
|
||||||
|
print("📦 Request Body: No body")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基础模式也显示是否包含基础参数
|
||||||
|
if request.includeBaseParameters {
|
||||||
|
print("📱 Base Parameters: 已自动注入")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print("=====================================")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Response Logging
|
||||||
|
static func logResponse(data: Data, response: HTTPURLResponse, duration: TimeInterval) {
|
||||||
|
guard logLevel != .none else { return }
|
||||||
|
|
||||||
|
let timestamp = dateFormatter.string(from: Date())
|
||||||
|
let statusEmoji = response.statusCode < 400 ? "✅" : "❌"
|
||||||
|
|
||||||
|
print("\n\(statusEmoji) [API Response] [\(timestamp)] ===================")
|
||||||
|
print("⏱️ Duration: \(String(format: "%.3f", duration))s")
|
||||||
|
print("📊 Status Code: \(response.statusCode)")
|
||||||
|
print("🔗 URL: \(response.url?.absoluteString ?? "Unknown")")
|
||||||
|
print("📏 Data Size: \(formatBytes(data.count))")
|
||||||
|
|
||||||
|
if logLevel == .detailed {
|
||||||
|
print("📋 Response Headers:")
|
||||||
|
for (key, value) in response.allHeaderFields.sorted(by: { "\($0.key)" < "\($1.key)" }) {
|
||||||
|
print(" \(key): \(value)")
|
||||||
|
}
|
||||||
|
|
||||||
|
print("📦 Response Data:")
|
||||||
|
if data.isEmpty {
|
||||||
|
print(" Empty response")
|
||||||
|
} else if let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []),
|
||||||
|
let prettyData = try? JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted),
|
||||||
|
let prettyString = String(data: prettyData, encoding: .utf8) {
|
||||||
|
print(prettyString)
|
||||||
|
} else if let rawString = String(data: data, encoding: .utf8) {
|
||||||
|
print(rawString)
|
||||||
|
} else {
|
||||||
|
print(" Binary data (\(data.count) bytes)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print("=====================================")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Error Logging
|
||||||
|
static func logError(_ error: Error, url: URL?, duration: TimeInterval) {
|
||||||
|
guard logLevel != .none else { return }
|
||||||
|
|
||||||
|
let timestamp = dateFormatter.string(from: Date())
|
||||||
|
|
||||||
|
print("\n❌ [API Error] [\(timestamp)] ======================")
|
||||||
|
print("⏱️ Duration: \(String(format: "%.3f", duration))s")
|
||||||
|
if let url = url {
|
||||||
|
print("🔗 URL: \(url.absoluteString)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if let apiError = error as? APIError {
|
||||||
|
print("🚨 API Error: \(apiError.localizedDescription)")
|
||||||
|
} else {
|
||||||
|
print("🚨 System Error: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if logLevel == .detailed {
|
||||||
|
if let urlError = error as? URLError {
|
||||||
|
print("🔍 URLError Code: \(urlError.code.rawValue)")
|
||||||
|
print("🔍 URLError Localized: \(urlError.localizedDescription)")
|
||||||
|
|
||||||
|
// 详细的网络错误分析
|
||||||
|
switch urlError.code {
|
||||||
|
case .timedOut:
|
||||||
|
print("💡 建议:检查网络连接或增加超时时间")
|
||||||
|
case .notConnectedToInternet:
|
||||||
|
print("💡 建议:检查网络连接")
|
||||||
|
case .cannotConnectToHost:
|
||||||
|
print("💡 建议:检查服务器地址和端口")
|
||||||
|
case .resourceUnavailable:
|
||||||
|
print("💡 建议:检查 API 端点是否正确")
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print("🔍 Full Error: \(error)")
|
||||||
|
}
|
||||||
|
|
||||||
|
print("=====================================\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Decoded Response Logging
|
||||||
|
static func logDecodedResponse<T>(_ response: T, type: T.Type) {
|
||||||
|
guard logLevel == .detailed else { return }
|
||||||
|
|
||||||
|
let timestamp = dateFormatter.string(from: Date())
|
||||||
|
print("🎯 [Decoded Response] [\(timestamp)] Type: \(type)")
|
||||||
|
print("=====================================\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helper Methods
|
||||||
|
private static func formatBytes(_ bytes: Int) -> String {
|
||||||
|
let formatter = ByteCountFormatter()
|
||||||
|
formatter.allowedUnits = [.useKB, .useMB]
|
||||||
|
formatter.countStyle = .file
|
||||||
|
return formatter.string(fromByteCount: Int64(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Performance Logging
|
||||||
|
static func logPerformanceWarning(duration: TimeInterval, threshold: TimeInterval = 5.0) {
|
||||||
|
guard logLevel != .none && duration > threshold else { return }
|
||||||
|
|
||||||
|
let timestamp = dateFormatter.string(from: Date())
|
||||||
|
print("\n⚠️ [Performance Warning] [\(timestamp)] ============")
|
||||||
|
print("🐌 Request took \(String(format: "%.3f", duration))s (threshold: \(threshold)s)")
|
||||||
|
print("💡 建议:检查网络条件或优化 API 响应")
|
||||||
|
print("================================================\n")
|
||||||
|
}
|
||||||
|
}
|
137
yana/APIs/APIModels.swift
Normal file
137
yana/APIs/APIModels.swift
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import Foundation
|
||||||
|
import ComposableArchitecture
|
||||||
|
|
||||||
|
// MARK: - HTTP Method
|
||||||
|
enum HTTPMethod: String, CaseIterable {
|
||||||
|
case GET = "GET"
|
||||||
|
case POST = "POST"
|
||||||
|
case PUT = "PUT"
|
||||||
|
case DELETE = "DELETE"
|
||||||
|
case PATCH = "PATCH"
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - API Error Types
|
||||||
|
enum APIError: Error, Equatable {
|
||||||
|
case invalidURL
|
||||||
|
case noData
|
||||||
|
case decodingError(String)
|
||||||
|
case networkError(String)
|
||||||
|
case httpError(statusCode: Int, message: String?)
|
||||||
|
case timeout
|
||||||
|
case resourceTooLarge
|
||||||
|
case unknown(String)
|
||||||
|
|
||||||
|
var localizedDescription: String {
|
||||||
|
switch self {
|
||||||
|
case .invalidURL:
|
||||||
|
return "无效的 URL"
|
||||||
|
case .noData:
|
||||||
|
return "没有收到数据"
|
||||||
|
case .decodingError(let message):
|
||||||
|
return "数据解析失败: \(message)"
|
||||||
|
case .networkError(let message):
|
||||||
|
return "网络错误: \(message)"
|
||||||
|
case .httpError(let statusCode, let message):
|
||||||
|
return "HTTP 错误 \(statusCode): \(message ?? "未知错误")"
|
||||||
|
case .timeout:
|
||||||
|
return "请求超时"
|
||||||
|
case .resourceTooLarge:
|
||||||
|
return "响应数据过大"
|
||||||
|
case .unknown(let message):
|
||||||
|
return "未知错误: \(message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Base Request Parameters
|
||||||
|
struct BaseRequest: Codable {
|
||||||
|
let acceptLanguage: String
|
||||||
|
let os: String = "iOS"
|
||||||
|
let osVersion: String
|
||||||
|
let ispType: String
|
||||||
|
let channel: String = "molistar_enterprise"
|
||||||
|
let model: String
|
||||||
|
let deviceId: String
|
||||||
|
let appVersion: String
|
||||||
|
let app: String = "youmi"
|
||||||
|
let mcc: String?
|
||||||
|
let spType: String?
|
||||||
|
let pubSign: String
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case acceptLanguage = "Accept-Language"
|
||||||
|
case appVersion = "appVersion"
|
||||||
|
case os, osVersion, ispType, channel, model, deviceId
|
||||||
|
case app, mcc, spType
|
||||||
|
case pubSign = "pub_sign"
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// 获取系统首选语言
|
||||||
|
self.acceptLanguage = Locale.current.languageCode ?? "en"
|
||||||
|
|
||||||
|
// 获取系统版本
|
||||||
|
self.osVersion = UIDevice.current.systemVersion
|
||||||
|
|
||||||
|
// 获取设备型号
|
||||||
|
self.model = UIDevice.current.model
|
||||||
|
|
||||||
|
// 生成设备ID (这里使用 identifierForVendor,实际项目中可能需要更稳定的方案)
|
||||||
|
self.deviceId = UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString
|
||||||
|
|
||||||
|
// 获取应用版本
|
||||||
|
self.appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
|
||||||
|
|
||||||
|
// 运营商相关信息(简化处理)
|
||||||
|
self.ispType = "65535"
|
||||||
|
self.mcc = nil
|
||||||
|
self.spType = nil
|
||||||
|
|
||||||
|
// 生成签名(这里使用时间戳的 MD5,实际项目中需要根据具体签名规则)
|
||||||
|
let timestamp = String(Int(Date().timeIntervalSince1970))
|
||||||
|
self.pubSign = timestamp.md5()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - API Request Protocol
|
||||||
|
protocol APIRequestProtocol {
|
||||||
|
associatedtype Response: Codable
|
||||||
|
|
||||||
|
var endpoint: String { get }
|
||||||
|
var method: HTTPMethod { get }
|
||||||
|
var queryParameters: [String: String]? { get }
|
||||||
|
var bodyParameters: [String: Any]? { get }
|
||||||
|
var headers: [String: String]? { get }
|
||||||
|
var timeout: TimeInterval { get }
|
||||||
|
var includeBaseParameters: Bool { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension APIRequestProtocol {
|
||||||
|
var timeout: TimeInterval { 30.0 }
|
||||||
|
var includeBaseParameters: Bool { true }
|
||||||
|
var headers: [String: String]? { nil }
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Generic API Response
|
||||||
|
struct APIResponse<T: Codable>: Codable {
|
||||||
|
let data: T?
|
||||||
|
let status: String?
|
||||||
|
let message: String?
|
||||||
|
let code: Int?
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - String MD5 Extension
|
||||||
|
extension String {
|
||||||
|
func md5() -> String {
|
||||||
|
let data = Data(self.utf8)
|
||||||
|
let hash = data.withUnsafeBytes { bytes -> [UInt8] in
|
||||||
|
var hash = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))
|
||||||
|
CC_MD5(bytes.baseAddress, CC_LONG(data.count), &hash)
|
||||||
|
return hash
|
||||||
|
}
|
||||||
|
return hash.map { String(format: "%02x", $0) }.joined()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 需要导入 CommonCrypto
|
||||||
|
import CommonCrypto
|
244
yana/APIs/APIService.swift
Normal file
244
yana/APIs/APIService.swift
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
import Foundation
|
||||||
|
import ComposableArchitecture
|
||||||
|
|
||||||
|
// MARK: - API Service Protocol
|
||||||
|
protocol APIServiceProtocol {
|
||||||
|
func request<T: APIRequestProtocol>(_ request: T) async throws -> T.Response
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Live API Service Implementation
|
||||||
|
struct LiveAPIService: APIServiceProtocol {
|
||||||
|
private let session: URLSession
|
||||||
|
private let baseURL: String
|
||||||
|
|
||||||
|
init(baseURL: String = APIConfiguration.baseURL) {
|
||||||
|
self.baseURL = baseURL
|
||||||
|
|
||||||
|
// 配置 URLSession 以防止资源超限问题
|
||||||
|
let config = URLSessionConfiguration.default
|
||||||
|
config.timeoutIntervalForRequest = APIConfiguration.timeout
|
||||||
|
config.timeoutIntervalForResource = APIConfiguration.timeout * 2
|
||||||
|
config.waitsForConnectivity = true
|
||||||
|
config.allowsCellularAccess = true
|
||||||
|
|
||||||
|
// 设置数据大小限制
|
||||||
|
config.urlCache = URLCache(memoryCapacity: 0, diskCapacity: 0, diskPath: nil)
|
||||||
|
|
||||||
|
self.session = URLSession(configuration: config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func request<T: APIRequestProtocol>(_ request: T) async throws -> T.Response {
|
||||||
|
let startTime = Date()
|
||||||
|
|
||||||
|
// 构建 URL
|
||||||
|
guard let url = buildURL(for: request) else {
|
||||||
|
throw APIError.invalidURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建 URLRequest
|
||||||
|
var urlRequest = URLRequest(url: url)
|
||||||
|
urlRequest.httpMethod = request.method.rawValue
|
||||||
|
urlRequest.timeoutInterval = request.timeout
|
||||||
|
|
||||||
|
// 设置请求头
|
||||||
|
var headers = APIConfiguration.defaultHeaders
|
||||||
|
if let customHeaders = request.headers {
|
||||||
|
headers.merge(customHeaders) { _, new in new }
|
||||||
|
}
|
||||||
|
|
||||||
|
for (key, value) in headers {
|
||||||
|
urlRequest.setValue(value, forHTTPHeaderField: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理请求体
|
||||||
|
var requestBody: Data? = nil
|
||||||
|
if request.method != .GET, let bodyParams = request.bodyParameters {
|
||||||
|
do {
|
||||||
|
// 如果需要包含基础参数,则合并
|
||||||
|
var finalBody = bodyParams
|
||||||
|
if request.includeBaseParameters {
|
||||||
|
let baseParams = BaseRequest()
|
||||||
|
let baseDict = try baseParams.toDictionary()
|
||||||
|
finalBody.merge(baseDict) { existing, _ in existing }
|
||||||
|
}
|
||||||
|
|
||||||
|
requestBody = try JSONSerialization.data(withJSONObject: finalBody, options: [])
|
||||||
|
urlRequest.httpBody = requestBody
|
||||||
|
} catch {
|
||||||
|
throw APIError.decodingError("请求体编码失败: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录请求日志,传递完整的 headers 信息
|
||||||
|
APILogger.logRequest(request, url: url, body: requestBody, finalHeaders: headers)
|
||||||
|
|
||||||
|
do {
|
||||||
|
// 发起请求
|
||||||
|
let (data, response) = try await session.data(for: urlRequest)
|
||||||
|
let duration = Date().timeIntervalSince(startTime)
|
||||||
|
|
||||||
|
// 检查响应
|
||||||
|
guard let httpResponse = response as? HTTPURLResponse else {
|
||||||
|
throw APIError.networkError("无效的响应类型")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查数据大小
|
||||||
|
if data.count > APIConfiguration.maxDataSize {
|
||||||
|
APILogger.logError(APIError.resourceTooLarge, url: url, duration: duration)
|
||||||
|
throw APIError.resourceTooLarge
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录响应日志
|
||||||
|
APILogger.logResponse(data: data, response: httpResponse, duration: duration)
|
||||||
|
|
||||||
|
// 性能警告
|
||||||
|
APILogger.logPerformanceWarning(duration: duration)
|
||||||
|
|
||||||
|
// 检查 HTTP 状态码
|
||||||
|
guard 200...299 ~= httpResponse.statusCode else {
|
||||||
|
let errorMessage = extractErrorMessage(from: data)
|
||||||
|
throw APIError.httpError(statusCode: httpResponse.statusCode, message: errorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查数据是否为空
|
||||||
|
guard !data.isEmpty else {
|
||||||
|
throw APIError.noData
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析响应数据
|
||||||
|
do {
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
let decodedResponse = try decoder.decode(T.Response.self, from: data)
|
||||||
|
APILogger.logDecodedResponse(decodedResponse, type: T.Response.self)
|
||||||
|
return decodedResponse
|
||||||
|
} catch {
|
||||||
|
throw APIError.decodingError("响应解析失败: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch let error as APIError {
|
||||||
|
let duration = Date().timeIntervalSince(startTime)
|
||||||
|
APILogger.logError(error, url: url, duration: duration)
|
||||||
|
throw error
|
||||||
|
} catch {
|
||||||
|
let duration = Date().timeIntervalSince(startTime)
|
||||||
|
let apiError = mapSystemError(error)
|
||||||
|
APILogger.logError(apiError, url: url, duration: duration)
|
||||||
|
throw apiError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private Helper Methods
|
||||||
|
|
||||||
|
private func buildURL<T: APIRequestProtocol>(for request: T) -> URL? {
|
||||||
|
guard var urlComponents = URLComponents(string: baseURL + request.endpoint) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理查询参数
|
||||||
|
var queryItems: [URLQueryItem] = []
|
||||||
|
|
||||||
|
// 对于 GET 请求,将基础参数添加到查询参数中
|
||||||
|
if request.method == .GET && request.includeBaseParameters {
|
||||||
|
do {
|
||||||
|
let baseParams = BaseRequest()
|
||||||
|
let baseDict = try baseParams.toDictionary()
|
||||||
|
for (key, value) in baseDict {
|
||||||
|
queryItems.append(URLQueryItem(name: key, value: "\(value)"))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("警告:无法添加基础参数到查询字符串")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加自定义查询参数
|
||||||
|
if let customParams = request.queryParameters {
|
||||||
|
for (key, value) in customParams {
|
||||||
|
queryItems.append(URLQueryItem(name: key, value: value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !queryItems.isEmpty {
|
||||||
|
urlComponents.queryItems = queryItems
|
||||||
|
}
|
||||||
|
|
||||||
|
return urlComponents.url
|
||||||
|
}
|
||||||
|
|
||||||
|
private func extractErrorMessage(from data: Data) -> String? {
|
||||||
|
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试多种可能的错误消息字段
|
||||||
|
if let message = json["message"] as? String {
|
||||||
|
return message
|
||||||
|
} else if let error = json["error"] as? String {
|
||||||
|
return error
|
||||||
|
} else if let msg = json["msg"] as? String {
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func mapSystemError(_ error: Error) -> APIError {
|
||||||
|
if let urlError = error as? URLError {
|
||||||
|
switch urlError.code {
|
||||||
|
case .timedOut:
|
||||||
|
return .timeout
|
||||||
|
case .cannotConnectToHost, .notConnectedToInternet:
|
||||||
|
return .networkError(urlError.localizedDescription)
|
||||||
|
case .dataLengthExceedsMaximum:
|
||||||
|
return .resourceTooLarge
|
||||||
|
default:
|
||||||
|
return .networkError(urlError.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return .unknown(error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Mock API Service (for testing)
|
||||||
|
struct MockAPIService: APIServiceProtocol {
|
||||||
|
private var mockResponses: [String: Any] = [:]
|
||||||
|
|
||||||
|
mutating func setMockResponse<T>(for endpoint: String, response: T) {
|
||||||
|
mockResponses[endpoint] = response
|
||||||
|
}
|
||||||
|
|
||||||
|
func request<T: APIRequestProtocol>(_ request: T) async throws -> T.Response {
|
||||||
|
// 模拟网络延迟
|
||||||
|
try await Task.sleep(nanoseconds: 500_000_000) // 0.5 秒
|
||||||
|
|
||||||
|
if let mockResponse = mockResponses[request.endpoint] as? T.Response {
|
||||||
|
return mockResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
throw APIError.noData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - TCA Dependency Integration
|
||||||
|
private enum APIServiceKey: DependencyKey {
|
||||||
|
static let liveValue: APIServiceProtocol = LiveAPIService()
|
||||||
|
static let testValue: APIServiceProtocol = MockAPIService()
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DependencyValues {
|
||||||
|
var apiService: APIServiceProtocol {
|
||||||
|
get { self[APIServiceKey.self] }
|
||||||
|
set { self[APIServiceKey.self] = newValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - BaseRequest Dictionary Conversion
|
||||||
|
extension BaseRequest {
|
||||||
|
func toDictionary() throws -> [String: Any] {
|
||||||
|
let data = try JSONEncoder().encode(self)
|
||||||
|
guard let dictionary = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
|
||||||
|
throw APIError.decodingError("无法转换基础参数为字典")
|
||||||
|
}
|
||||||
|
return dictionary
|
||||||
|
}
|
||||||
|
}
|
196
yana/APIs/Integration-Guide.md
Normal file
196
yana/APIs/Integration-Guide.md
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
# APIClient 集成指南
|
||||||
|
|
||||||
|
## 🔧 在其他文件中使用 APIClient
|
||||||
|
|
||||||
|
### 方法一:直接实例化 (推荐用于测试)
|
||||||
|
|
||||||
|
```swift
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ContentView: View {
|
||||||
|
@State private var isLoading = false
|
||||||
|
@State private var result = ""
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
Button("测试 API") {
|
||||||
|
testAPI()
|
||||||
|
}
|
||||||
|
.disabled(isLoading)
|
||||||
|
|
||||||
|
if isLoading {
|
||||||
|
ProgressView("请求中...")
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func testAPI() {
|
||||||
|
isLoading = true
|
||||||
|
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
// 创建 API 客户端实例
|
||||||
|
let apiClient = LiveAPIClient(
|
||||||
|
baseURL: URL(string: "https://your-api.com")!
|
||||||
|
)
|
||||||
|
|
||||||
|
// 发起请求
|
||||||
|
let response: YourResponseModel = try await apiClient.get(
|
||||||
|
path: "/your/endpoint",
|
||||||
|
queryParameters: ["key": "value"]
|
||||||
|
)
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
result = "成功: \(response)"
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
await MainActor.run {
|
||||||
|
result = "错误: \(error.localizedDescription)"
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方法二:使用单例模式
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// 在需要使用的文件中
|
||||||
|
private func makeAPIRequest() async {
|
||||||
|
do {
|
||||||
|
let response: SomeModel = try await APIClientManager.shared.get(
|
||||||
|
path: "/endpoint"
|
||||||
|
)
|
||||||
|
// 处理响应
|
||||||
|
} catch {
|
||||||
|
// 处理错误
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方法三:TCA 集成 (当 TCA 可用时)
|
||||||
|
|
||||||
|
```swift
|
||||||
|
import ComposableArchitecture
|
||||||
|
|
||||||
|
@Reducer
|
||||||
|
struct MyFeature {
|
||||||
|
@ObservableState
|
||||||
|
struct State: Equatable {
|
||||||
|
var data: [Item] = []
|
||||||
|
var isLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Action: Equatable {
|
||||||
|
case loadData
|
||||||
|
case dataLoaded([Item])
|
||||||
|
case loadingFailed(String)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Dependency(\.apiClient) var apiClient
|
||||||
|
|
||||||
|
var body: some ReducerOf<Self> {
|
||||||
|
Reduce { state, action in
|
||||||
|
switch action {
|
||||||
|
case .loadData:
|
||||||
|
state.isLoading = true
|
||||||
|
|
||||||
|
return .run { send in
|
||||||
|
do {
|
||||||
|
let items: [Item] = try await apiClient.get(path: "/items")
|
||||||
|
await send(.dataLoaded(items))
|
||||||
|
} catch {
|
||||||
|
await send(.loadingFailed(error.localizedDescription))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case let .dataLoaded(items):
|
||||||
|
state.isLoading = false
|
||||||
|
state.data = items
|
||||||
|
return .none
|
||||||
|
|
||||||
|
case let .loadingFailed(error):
|
||||||
|
state.isLoading = false
|
||||||
|
// 处理错误
|
||||||
|
return .none
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚨 常见问题解决
|
||||||
|
|
||||||
|
### 1. "Cannot find 'APIClientManager' in scope"
|
||||||
|
|
||||||
|
**原因**: Swift 模块系统问题,需要确保正确导入
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
```swift
|
||||||
|
// 方案 A: 直接实例化
|
||||||
|
let apiClient = LiveAPIClient(baseURL: URL(string: "https://api.com")!)
|
||||||
|
|
||||||
|
// 方案 B: 确保文件在同一个 target 中
|
||||||
|
// 检查 Xcode 项目设置,确保 APICaller.swift 和使用文件在同一个 target
|
||||||
|
|
||||||
|
// 方案 C: 使用原生 URLSession (临时方案)
|
||||||
|
let (data, _) = try await URLSession.shared.data(for: request)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. TCA 导入问题
|
||||||
|
|
||||||
|
**原因**: ComposableArchitecture 模块配置问题
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
1. 检查 Package Dependencies 是否正确添加
|
||||||
|
2. 确保 target 正确链接了 TCA
|
||||||
|
3. Clean Build Folder (⌘+Shift+K)
|
||||||
|
4. 重新构建项目
|
||||||
|
|
||||||
|
### 3. 网络请求失败
|
||||||
|
|
||||||
|
**常见错误处理**:
|
||||||
|
```swift
|
||||||
|
do {
|
||||||
|
let response = try await apiClient.get(path: "/endpoint")
|
||||||
|
// 成功处理
|
||||||
|
} catch let urlError as URLError {
|
||||||
|
switch urlError.code {
|
||||||
|
case .notConnectedToInternet:
|
||||||
|
print("网络不可用")
|
||||||
|
case .timedOut:
|
||||||
|
print("请求超时")
|
||||||
|
case .cannotFindHost:
|
||||||
|
print("无法找到服务器")
|
||||||
|
default:
|
||||||
|
print("网络错误: \(urlError.localizedDescription)")
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("其他错误: \(error)")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 最佳实践
|
||||||
|
|
||||||
|
1. **错误处理**: 始终使用 do-catch 处理异步请求
|
||||||
|
2. **主线程更新**: 使用 `await MainActor.run` 更新 UI
|
||||||
|
3. **取消请求**: 在适当时候取消不需要的请求
|
||||||
|
4. **重试机制**: 对于网络错误实现重试逻辑
|
||||||
|
5. **缓存策略**: 考虑实现适当的缓存机制
|
||||||
|
|
||||||
|
## 🔄 迁移步骤
|
||||||
|
|
||||||
|
从原生 URLSession 迁移到 APIClient:
|
||||||
|
|
||||||
|
1. **保留现有代码** - 确保功能正常
|
||||||
|
2. **逐步替换** - 一个接口一个接口地迁移
|
||||||
|
3. **测试验证** - 每次迁移后进行测试
|
||||||
|
4. **清理代码** - 移除不需要的原生实现
|
||||||
|
|
||||||
|
这样可以确保平滑的迁移过程,避免破坏现有功能。
|
@@ -3,7 +3,32 @@ import NIMSDK
|
|||||||
|
|
||||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||||
NIMConfigurationManager.setupNimSDK()
|
|
||||||
|
|
||||||
|
// 开启网络监控
|
||||||
|
// NetworkManager.shared.networkStatusChanged = { status in
|
||||||
|
// print("🌍 网络状态更新:\(status)")
|
||||||
|
// }
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
// 网络诊断
|
||||||
|
let testURL = URL(string: "http://beta.api.molistar.xyz/client/init")!
|
||||||
|
let request = URLRequest(url: testURL)
|
||||||
|
|
||||||
|
print("🛠 原生URLSession测试开始")
|
||||||
|
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||||
|
print("""
|
||||||
|
=== 网络诊断结果 ===
|
||||||
|
响应状态码: \((response as? HTTPURLResponse)?.statusCode ?? -1)
|
||||||
|
错误信息: \(error?.localizedDescription ?? "无")
|
||||||
|
原始数据: \(data?.count ?? 0) bytes
|
||||||
|
==================
|
||||||
|
""")
|
||||||
|
}.resume()
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// NIMConfigurationManager.setupNimSDK()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -33,4 +33,30 @@ struct AppConfig {
|
|||||||
static func switchEnvironment(to env: Environment) {
|
static func switchEnvironment(to env: Environment) {
|
||||||
current = env
|
current = env
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加调试配置
|
||||||
|
static var enableNetworkDebug: Bool {
|
||||||
|
#if DEBUG
|
||||||
|
return true
|
||||||
|
#else
|
||||||
|
return false
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加服务器信任配置
|
||||||
|
static var serverTrustPolicies: [String: ServerTrustEvaluating] {
|
||||||
|
#if DEBUG
|
||||||
|
return ["beta.api.molistar.xyz": DisabledTrustEvaluator()]
|
||||||
|
#else
|
||||||
|
return ["api.hfighting.com": PublicKeysTrustEvaluator()]
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static var networkDebugEnabled: Bool {
|
||||||
|
#if DEBUG
|
||||||
|
return true
|
||||||
|
#else
|
||||||
|
return false
|
||||||
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
@@ -1,32 +1,51 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import UIKit // 用于设备信息
|
||||||
|
@_exported import Alamofire // 全局导入
|
||||||
|
|
||||||
final class ClientConfig {
|
final class ClientConfig {
|
||||||
static let shared = ClientConfig()
|
static let shared = ClientConfig()
|
||||||
private init() {}
|
private init() {}
|
||||||
|
|
||||||
func initializeClient() {
|
func initializeClient() {
|
||||||
print("开始初始化客户端")
|
print("✅ 开始初始化客户端 - URL: \(AppConfig.baseURL)/client/init")
|
||||||
|
callClientInitAPI() // 调用新方法
|
||||||
|
}
|
||||||
|
|
||||||
|
func callClientInitAPI() {
|
||||||
|
print("🆕 使用GET方法调用初始化接口")
|
||||||
|
|
||||||
NetworkManager.shared.enhancedRequest(
|
// let queryParams = [
|
||||||
path: "client/init",
|
// "debug": "1",
|
||||||
method: .get,
|
// "platform": "ios",
|
||||||
responseType: Data.self
|
// "timestamp": String(Int(Date().timeIntervalSince1970))
|
||||||
) { result in
|
// ]
|
||||||
switch result {
|
|
||||||
case .success(let response):
|
// NetworkManager.shared.get(
|
||||||
print("初始化成功,状态码:\(response.statusCode)")
|
// path: "client/init",
|
||||||
if let data = response.data {
|
// queryItems: [:]
|
||||||
do {
|
// ) { (result: Result<Data, NetworkError>) in
|
||||||
let json = try JSONSerialization.jsonObject(with: data)
|
// switch result {
|
||||||
print("响应数据:\(json)")
|
// case .success(let data):
|
||||||
} catch {
|
// if let response = NetworkManager.lastResponse {
|
||||||
print("JSON解析失败:\(error)")
|
// print("✅ 请求成功 | 状态码: \(response.statusCode) | 数据长度: \(data.count) bytes")
|
||||||
}
|
// }
|
||||||
}
|
// if let json = try? JSONSerialization.jsonObject(with: data) {
|
||||||
|
// print("📊 响应数据:\(json)")
|
||||||
case .failure(let error):
|
// }
|
||||||
print("初始化失败:\(error.localizedDescription)")
|
// case .failure(let error):
|
||||||
}
|
// let statusCode: Int
|
||||||
}
|
// switch error {
|
||||||
|
// case .requestFailed(let code, _):
|
||||||
|
// statusCode = code
|
||||||
|
// case .unauthorized:
|
||||||
|
// statusCode = 401
|
||||||
|
// case .rateLimited:
|
||||||
|
// statusCode = 429
|
||||||
|
// default:
|
||||||
|
// statusCode = -1
|
||||||
|
// }
|
||||||
|
// print("❌ 请求失败 | 状态码: \(statusCode) | 错误: \(error.localizedDescription)")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,75 +6,220 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import ComposableArchitecture
|
||||||
|
|
||||||
|
// MARK: - API Response Models
|
||||||
|
struct InitResponse: Codable, Equatable {
|
||||||
|
let status: String
|
||||||
|
let message: String?
|
||||||
|
let data: InitData?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct InitData: Codable, Equatable {
|
||||||
|
let version: String?
|
||||||
|
let timestamp: Int?
|
||||||
|
let config: [String: String]?
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Local UI Log Level Enum
|
||||||
|
enum UILogLevel: String, CaseIterable {
|
||||||
|
case none = "无日志"
|
||||||
|
case basic = "基础日志"
|
||||||
|
case detailed = "详细日志"
|
||||||
|
}
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
@State private var account = ""
|
let store: StoreOf<LoginFeature>
|
||||||
@State private var password = ""
|
let initStore: StoreOf<InitFeature>
|
||||||
|
let configStore: StoreOf<ConfigFeature>
|
||||||
#if DEBUG
|
@State private var selectedLogLevel: APILogger.LogLevel = APILogger.logLevel
|
||||||
init() {
|
@State private var selectedTab = 0
|
||||||
_account = State(initialValue: "3184")
|
|
||||||
_password = State(initialValue: "a0d5da073d14731cc7a01ecaa17b9174")
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
@State private var isLoading = false
|
|
||||||
@State private var loginError: String?
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
TabView(selection: $selectedTab) {
|
||||||
// 新增测试按钮
|
// 原有登录界面
|
||||||
Button("测试初始化") {
|
VStack {
|
||||||
ClientConfig.shared.initializeClient()
|
// 日志级别选择器
|
||||||
}
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
.padding(.top, 20)
|
Text("日志级别:")
|
||||||
|
.font(.headline)
|
||||||
TextField("账号", text: $account)
|
.foregroundColor(.primary)
|
||||||
.textFieldStyle(.roundedBorder)
|
|
||||||
.padding()
|
Picker("日志级别", selection: $selectedLogLevel) {
|
||||||
|
Text("无日志").tag(APILogger.LogLevel.none)
|
||||||
SecureField("密码", text: $password)
|
Text("基础日志").tag(APILogger.LogLevel.basic)
|
||||||
.textFieldStyle(.roundedBorder)
|
Text("详细日志").tag(APILogger.LogLevel.detailed)
|
||||||
.padding()
|
}
|
||||||
|
.pickerStyle(SegmentedPickerStyle())
|
||||||
Button(action: handleLogin) {
|
|
||||||
if isLoading {
|
|
||||||
ProgressView()
|
|
||||||
} else {
|
|
||||||
Text("登录")
|
|
||||||
}
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color.gray.opacity(0.1))
|
||||||
|
.cornerRadius(10)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
Text("yana")
|
||||||
|
.font(.largeTitle)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
|
||||||
|
VStack(spacing: 15) {
|
||||||
|
WithViewStore(store, observe: { $0 }) { viewStore in
|
||||||
|
TextField("账号", text: viewStore.binding(
|
||||||
|
get: \.account,
|
||||||
|
send: { LoginFeature.Action.updateAccount($0) }
|
||||||
|
))
|
||||||
|
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||||
|
.autocorrectionDisabled(true)
|
||||||
|
|
||||||
|
SecureField("密码", text: viewStore.binding(
|
||||||
|
get: \.password,
|
||||||
|
send: { LoginFeature.Action.updatePassword($0) }
|
||||||
|
))
|
||||||
|
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
WithViewStore(store, observe: { $0 }) { viewStore in
|
||||||
|
if let error = viewStore.error {
|
||||||
|
Text(error)
|
||||||
|
.foregroundColor(.red)
|
||||||
|
.font(.caption)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(spacing: 10) {
|
||||||
|
Button(action: {
|
||||||
|
viewStore.send(.login)
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
if viewStore.isLoading {
|
||||||
|
ProgressView()
|
||||||
|
.scaleEffect(0.8)
|
||||||
|
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||||
|
}
|
||||||
|
Text(viewStore.isLoading ? "登录中..." : "登录")
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding()
|
||||||
|
.background(viewStore.isLoading ? Color.gray : Color.blue)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.cornerRadius(10)
|
||||||
|
}
|
||||||
|
.disabled(viewStore.isLoading || viewStore.account.isEmpty || viewStore.password.isEmpty)
|
||||||
|
|
||||||
|
WithViewStore(initStore, observe: { $0 }) { initViewStore in
|
||||||
|
Button(action: {
|
||||||
|
initViewStore.send(.initialize)
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
if initViewStore.isLoading {
|
||||||
|
ProgressView()
|
||||||
|
.scaleEffect(0.8)
|
||||||
|
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||||
|
}
|
||||||
|
Text(initViewStore.isLoading ? "测试中..." : "测试初始化")
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding()
|
||||||
|
.background(initViewStore.isLoading ? Color.gray : Color.green)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.cornerRadius(10)
|
||||||
|
}
|
||||||
|
.disabled(initViewStore.isLoading)
|
||||||
|
|
||||||
|
// API 测试结果显示区域
|
||||||
|
if let response = initViewStore.response {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
HStack {
|
||||||
|
Text("API 测试结果:")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text("状态: \(response.status)")
|
||||||
|
if let message = response.message {
|
||||||
|
Text("消息: \(message)")
|
||||||
|
}
|
||||||
|
if let data = response.data {
|
||||||
|
Text("版本: \(data.version ?? "未知")")
|
||||||
|
Text("时间戳: \(data.timestamp ?? 0)")
|
||||||
|
if let config = data.config {
|
||||||
|
Text("配置:")
|
||||||
|
ForEach(Array(config.keys), id: \.self) { key in
|
||||||
|
Text(" \(key): \(config[key] ?? "")")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(.system(.caption, design: .monospaced))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding(8)
|
||||||
|
.background(Color.gray.opacity(0.1))
|
||||||
|
.cornerRadius(8)
|
||||||
|
}
|
||||||
|
.frame(maxHeight: 200)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color.gray.opacity(0.05))
|
||||||
|
.cornerRadius(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let error = initViewStore.error {
|
||||||
|
Text(error)
|
||||||
|
.foregroundColor(.red)
|
||||||
|
.font(.caption)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
}
|
}
|
||||||
.disabled(isLoading)
|
.padding()
|
||||||
.alert("登录错误", isPresented: .constant(loginError != nil)) {
|
.tabItem {
|
||||||
Button("确定") { loginError = nil }
|
Label("登录", systemImage: "person.circle")
|
||||||
} message: {
|
|
||||||
Text(loginError ?? "")
|
|
||||||
}
|
}
|
||||||
|
.tag(0)
|
||||||
|
|
||||||
Image(systemName: "globe")
|
// 新的 API 配置测试界面
|
||||||
.imageScale(.large)
|
ConfigView(store: configStore)
|
||||||
.foregroundStyle(.tint)
|
.tabItem {
|
||||||
Text("Hello, yana!")
|
Label("API 测试", systemImage: "network")
|
||||||
|
}
|
||||||
|
.tag(1)
|
||||||
|
}
|
||||||
|
.onChange(of: selectedLogLevel) { newValue in
|
||||||
|
APILogger.logLevel = newValue
|
||||||
}
|
}
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleLogin() {
|
|
||||||
isLoading = true
|
|
||||||
NIMSessionManager.shared
|
|
||||||
.autoLogin(account: account, token: password) { error in
|
|
||||||
if let error = error {
|
|
||||||
loginError = error.localizedDescription
|
|
||||||
} else {
|
|
||||||
// 登录成功处理
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// NIMSessionManager.shared.login(account: account, token: password) { error in
|
|
||||||
// isLoading = false
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
ContentView()
|
ContentView(
|
||||||
|
store: Store(
|
||||||
|
initialState: LoginFeature.State()
|
||||||
|
) {
|
||||||
|
LoginFeature()
|
||||||
|
},
|
||||||
|
initStore: Store(
|
||||||
|
initialState: InitFeature.State()
|
||||||
|
) {
|
||||||
|
InitFeature()
|
||||||
|
},
|
||||||
|
configStore: Store(
|
||||||
|
initialState: ConfigFeature.State()
|
||||||
|
) {
|
||||||
|
ConfigFeature()
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
95
yana/Features/ConfigFeature.swift
Normal file
95
yana/Features/ConfigFeature.swift
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import Foundation
|
||||||
|
import ComposableArchitecture
|
||||||
|
|
||||||
|
// MARK: - Config Response Model
|
||||||
|
struct ConfigResponse: Codable, Equatable {
|
||||||
|
let status: String?
|
||||||
|
let message: String?
|
||||||
|
let data: ConfigData?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ConfigData: Codable, Equatable {
|
||||||
|
let version: String?
|
||||||
|
let features: [String]?
|
||||||
|
let settings: ConfigSettings?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ConfigSettings: Codable, Equatable {
|
||||||
|
let enableDebug: Bool?
|
||||||
|
let apiTimeout: Int?
|
||||||
|
let maxRetries: Int?
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Config API Request
|
||||||
|
struct ConfigRequest: APIRequestProtocol {
|
||||||
|
typealias Response = ConfigResponse
|
||||||
|
|
||||||
|
var endpoint: String { APIEndpoint.config.path }
|
||||||
|
var method: HTTPMethod { .GET }
|
||||||
|
var queryParameters: [String: String]? { nil }
|
||||||
|
var bodyParameters: [String: Any]? { nil }
|
||||||
|
var timeout: TimeInterval { 15.0 } // Config 请求使用较短超时
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Config Feature
|
||||||
|
@Reducer
|
||||||
|
struct ConfigFeature {
|
||||||
|
@ObservableState
|
||||||
|
struct State: Equatable {
|
||||||
|
var isLoading = false
|
||||||
|
var configData: ConfigData?
|
||||||
|
var errorMessage: String?
|
||||||
|
var lastUpdated: Date?
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Action: Equatable {
|
||||||
|
case loadConfig
|
||||||
|
case configResponse(TaskResult<ConfigResponse>)
|
||||||
|
case clearError
|
||||||
|
}
|
||||||
|
|
||||||
|
@Dependency(\.apiService) var apiService
|
||||||
|
|
||||||
|
var body: some ReducerOf<Self> {
|
||||||
|
Reduce { state, action in
|
||||||
|
switch action {
|
||||||
|
case .loadConfig:
|
||||||
|
state.isLoading = true
|
||||||
|
state.errorMessage = nil
|
||||||
|
|
||||||
|
return .run { send in
|
||||||
|
let request = ConfigRequest()
|
||||||
|
await send(.configResponse(
|
||||||
|
TaskResult {
|
||||||
|
try await apiService.request(request)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
case let .configResponse(.success(response)):
|
||||||
|
state.isLoading = false
|
||||||
|
state.lastUpdated = Date()
|
||||||
|
|
||||||
|
if response.status == "success" {
|
||||||
|
state.configData = response.data
|
||||||
|
} else {
|
||||||
|
state.errorMessage = response.message ?? "配置加载失败"
|
||||||
|
}
|
||||||
|
return .none
|
||||||
|
|
||||||
|
case let .configResponse(.failure(error)):
|
||||||
|
state.isLoading = false
|
||||||
|
if let apiError = error as? APIError {
|
||||||
|
state.errorMessage = apiError.localizedDescription
|
||||||
|
} else {
|
||||||
|
state.errorMessage = error.localizedDescription
|
||||||
|
}
|
||||||
|
return .none
|
||||||
|
|
||||||
|
case .clearError:
|
||||||
|
state.errorMessage = nil
|
||||||
|
return .none
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
190
yana/Features/ConfigView.swift
Normal file
190
yana/Features/ConfigView.swift
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import ComposableArchitecture
|
||||||
|
|
||||||
|
struct ConfigView: View {
|
||||||
|
let store: StoreOf<ConfigFeature>
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
WithViewStore(self.store, observe: { $0 }) { viewStore in
|
||||||
|
NavigationView {
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
// 标题
|
||||||
|
Text("API 配置测试")
|
||||||
|
.font(.largeTitle)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
.padding(.top)
|
||||||
|
|
||||||
|
// 状态显示
|
||||||
|
Group {
|
||||||
|
if viewStore.isLoading {
|
||||||
|
VStack {
|
||||||
|
ProgressView()
|
||||||
|
.scaleEffect(1.5)
|
||||||
|
Text("正在加载配置...")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.padding(.top, 8)
|
||||||
|
}
|
||||||
|
.frame(height: 100)
|
||||||
|
} else if let errorMessage = viewStore.errorMessage {
|
||||||
|
VStack {
|
||||||
|
Image(systemName: "exclamationmark.triangle.fill")
|
||||||
|
.font(.system(size: 40))
|
||||||
|
.foregroundColor(.red)
|
||||||
|
|
||||||
|
Text("错误")
|
||||||
|
.font(.headline)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
|
||||||
|
Text(errorMessage)
|
||||||
|
.font(.body)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
Button("清除错误") {
|
||||||
|
viewStore.send(.clearError)
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.padding(.top)
|
||||||
|
}
|
||||||
|
.frame(maxHeight: .infinity)
|
||||||
|
} else if let configData = viewStore.configData {
|
||||||
|
// 配置数据显示
|
||||||
|
ScrollView {
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
|
||||||
|
if let version = configData.version {
|
||||||
|
InfoRow(title: "版本", value: version)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let features = configData.features, !features.isEmpty {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("功能列表")
|
||||||
|
.font(.headline)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
|
||||||
|
ForEach(features, id: \.self) { feature in
|
||||||
|
HStack {
|
||||||
|
Circle()
|
||||||
|
.fill(Color.green)
|
||||||
|
.frame(width: 6, height: 6)
|
||||||
|
Text(feature)
|
||||||
|
.font(.body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color(.systemGray6))
|
||||||
|
.cornerRadius(12)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let settings = configData.settings {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("设置")
|
||||||
|
.font(.headline)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
|
||||||
|
if let enableDebug = settings.enableDebug {
|
||||||
|
InfoRow(title: "调试模式", value: enableDebug ? "启用" : "禁用")
|
||||||
|
}
|
||||||
|
|
||||||
|
if let apiTimeout = settings.apiTimeout {
|
||||||
|
InfoRow(title: "API 超时", value: "\(apiTimeout)秒")
|
||||||
|
}
|
||||||
|
|
||||||
|
if let maxRetries = settings.maxRetries {
|
||||||
|
InfoRow(title: "最大重试次数", value: "\(maxRetries)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color(.systemGray6))
|
||||||
|
.cornerRadius(12)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let lastUpdated = viewStore.lastUpdated {
|
||||||
|
Text("最后更新: \(lastUpdated, style: .time)")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
VStack {
|
||||||
|
Image(systemName: "gear")
|
||||||
|
.font(.system(size: 40))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
Text("点击下方按钮加载配置")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.frame(maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
// 操作按钮
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
Button(action: {
|
||||||
|
viewStore.send(.loadConfig)
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
if viewStore.isLoading {
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||||
|
.scaleEffect(0.8)
|
||||||
|
} else {
|
||||||
|
Image(systemName: "arrow.clockwise")
|
||||||
|
}
|
||||||
|
Text(viewStore.isLoading ? "加载中..." : "加载配置")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.disabled(viewStore.isLoading)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.frame(height: 50)
|
||||||
|
|
||||||
|
Text("使用新的 TCA API 组件")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationBarHidden(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helper Views
|
||||||
|
struct InfoRow: View {
|
||||||
|
let title: String
|
||||||
|
let value: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
Text(title)
|
||||||
|
.font(.body)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text(value)
|
||||||
|
.font(.body)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Preview
|
||||||
|
#Preview {
|
||||||
|
ConfigView(
|
||||||
|
store: Store(initialState: ConfigFeature.State()) {
|
||||||
|
ConfigFeature()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
63
yana/Features/InitFeature.swift
Normal file
63
yana/Features/InitFeature.swift
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import Foundation
|
||||||
|
import ComposableArchitecture
|
||||||
|
|
||||||
|
@Reducer
|
||||||
|
struct InitFeature {
|
||||||
|
@ObservableState
|
||||||
|
struct State: Equatable {
|
||||||
|
var isLoading = false
|
||||||
|
var response: InitResponse?
|
||||||
|
var error: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Action: Equatable {
|
||||||
|
case initialize
|
||||||
|
case initializeResponse(TaskResult<InitResponse>)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Dependency(\.apiService) var apiService
|
||||||
|
|
||||||
|
var body: some ReducerOf<Self> {
|
||||||
|
Reduce { state, action in
|
||||||
|
switch action {
|
||||||
|
case .initialize:
|
||||||
|
state.isLoading = true
|
||||||
|
state.error = nil
|
||||||
|
|
||||||
|
return .run { send in
|
||||||
|
let request = InitRequest()
|
||||||
|
await send(.initializeResponse(
|
||||||
|
TaskResult {
|
||||||
|
try await apiService.request(request)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
case let .initializeResponse(.success(response)):
|
||||||
|
state.isLoading = false
|
||||||
|
state.response = response
|
||||||
|
return .none
|
||||||
|
|
||||||
|
case let .initializeResponse(.failure(error)):
|
||||||
|
state.isLoading = false
|
||||||
|
if let apiError = error as? APIError {
|
||||||
|
state.error = apiError.localizedDescription
|
||||||
|
} else {
|
||||||
|
state.error = error.localizedDescription
|
||||||
|
}
|
||||||
|
return .none
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Init API Request
|
||||||
|
struct InitRequest: APIRequestProtocol {
|
||||||
|
typealias Response = InitResponse
|
||||||
|
|
||||||
|
var endpoint: String { APIEndpoint.config.path }
|
||||||
|
var method: HTTPMethod { .GET }
|
||||||
|
var queryParameters: [String: String]? { nil }
|
||||||
|
var bodyParameters: [String: Any]? { nil }
|
||||||
|
var timeout: TimeInterval { 15.0 }
|
||||||
|
}
|
83
yana/Features/LoginFeature.swift
Normal file
83
yana/Features/LoginFeature.swift
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import Foundation
|
||||||
|
import ComposableArchitecture
|
||||||
|
|
||||||
|
struct LoginResponse: Codable, Equatable {
|
||||||
|
let status: String
|
||||||
|
let message: String?
|
||||||
|
let token: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
@Reducer
|
||||||
|
struct LoginFeature {
|
||||||
|
@ObservableState
|
||||||
|
struct State: Equatable {
|
||||||
|
var account: String = ""
|
||||||
|
var password: String = ""
|
||||||
|
var isLoading = false
|
||||||
|
var error: String?
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
init() {
|
||||||
|
self.account = "3184"
|
||||||
|
self.password = "a0d5da073d14731cc7a01ecaa17b9174"
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Action: Equatable {
|
||||||
|
case updateAccount(String)
|
||||||
|
case updatePassword(String)
|
||||||
|
case login
|
||||||
|
case loginResponse(TaskResult<LoginResponse>)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some ReducerOf<Self> {
|
||||||
|
Reduce { state, action in
|
||||||
|
switch action {
|
||||||
|
case let .updateAccount(account):
|
||||||
|
state.account = account
|
||||||
|
return .none
|
||||||
|
|
||||||
|
case let .updatePassword(password):
|
||||||
|
state.password = password
|
||||||
|
return .none
|
||||||
|
|
||||||
|
case .login:
|
||||||
|
state.isLoading = true
|
||||||
|
state.error = nil
|
||||||
|
|
||||||
|
let loginBody = [
|
||||||
|
"account": state.account,
|
||||||
|
"password": state.password
|
||||||
|
]
|
||||||
|
|
||||||
|
return .run { send in
|
||||||
|
do {
|
||||||
|
let response: LoginResponse = try await APIClientManager.shared.post(
|
||||||
|
path: APIConstants.Endpoints.login,
|
||||||
|
body: loginBody,
|
||||||
|
headers: APIConstants.defaultHeaders
|
||||||
|
)
|
||||||
|
await send(.loginResponse(.success(response)))
|
||||||
|
} catch {
|
||||||
|
await send(.loginResponse(.failure(error)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case let .loginResponse(.success(response)):
|
||||||
|
state.isLoading = false
|
||||||
|
if response.status == "success" {
|
||||||
|
// TODO: 处理登录成功,保存 token 等
|
||||||
|
} else {
|
||||||
|
state.error = response.message ?? "登录失败"
|
||||||
|
}
|
||||||
|
return .none
|
||||||
|
|
||||||
|
case let .loginResponse(.failure(error)):
|
||||||
|
state.isLoading = false
|
||||||
|
state.error = error.localizedDescription
|
||||||
|
return .none
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -7,5 +7,7 @@
|
|||||||
<key>NSAllowsArbitraryLoads</key>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>NSWiFiUsageDescription</key>
|
||||||
|
<string>应用需要访问 Wi-Fi 信息以提供网络相关功能</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@@ -21,7 +21,9 @@ struct NIMConfigurationManager {
|
|||||||
static func setupChatSDK(with option: NIMSDKOption) {
|
static func setupChatSDK(with option: NIMSDKOption) {
|
||||||
let v2Option = V2NIMSDKOption()
|
let v2Option = V2NIMSDKOption()
|
||||||
v2Option.enableV2CloudConversation = false
|
v2Option.enableV2CloudConversation = false
|
||||||
IMKitClient.instance.setupIM2(option, v2Option)
|
// TODO: 修复 IMKitClient API 调用
|
||||||
|
// IMKitClient.shared.setupIM2(option, v2Option)
|
||||||
|
print("⚠️ NIM SDK 配置暂时被注释,需要修复 IMKitClient API")
|
||||||
}
|
}
|
||||||
|
|
||||||
static func configureNIMSDKOption() -> NIMSDKOption {
|
static func configureNIMSDKOption() -> NIMSDKOption {
|
||||||
|
@@ -1,667 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import Alamofire
|
|
||||||
import CoreTelephony
|
|
||||||
import UIKit
|
|
||||||
import Darwin // 用于 utsname 结构体
|
|
||||||
import CommonCrypto
|
|
||||||
|
|
||||||
// 配置类
|
|
||||||
//enum AppConfig {
|
|
||||||
// static let baseURL = "https://api.example.com" // 请替换为实际的 API 基础 URL
|
|
||||||
//}
|
|
||||||
|
|
||||||
// 网络状态枚举
|
|
||||||
enum NetworkStatus: Int {
|
|
||||||
case notReachable = 0
|
|
||||||
case reachableViaWWAN = 1
|
|
||||||
case reachableViaWiFi = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
// 扩展错误类型
|
|
||||||
enum NetworkError: Error {
|
|
||||||
case invalidURL
|
|
||||||
case requestFailed(statusCode: Int, message: String?)
|
|
||||||
case invalidResponse
|
|
||||||
case decodingFailed
|
|
||||||
case networkUnavailable
|
|
||||||
case serverError(message: String)
|
|
||||||
case unauthorized
|
|
||||||
case rateLimited
|
|
||||||
|
|
||||||
var localizedDescription: String {
|
|
||||||
switch self {
|
|
||||||
case .invalidURL:
|
|
||||||
return "无效的 URL"
|
|
||||||
case .requestFailed(let statusCode, let message):
|
|
||||||
return "请求失败: \(statusCode), \(message ?? "未知错误")"
|
|
||||||
case .invalidResponse:
|
|
||||||
return "无效的响应"
|
|
||||||
case .decodingFailed:
|
|
||||||
return "数据解析失败"
|
|
||||||
case .networkUnavailable:
|
|
||||||
return "网络不可用"
|
|
||||||
case .serverError(let message):
|
|
||||||
return "服务器错误: \(message)"
|
|
||||||
case .unauthorized:
|
|
||||||
return "未授权访问"
|
|
||||||
case .rateLimited:
|
|
||||||
return "请求过于频繁"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - MD5 加密扩展
|
|
||||||
extension String {
|
|
||||||
func md5() -> String {
|
|
||||||
let str = self.cString(using: .utf8)
|
|
||||||
let strLen = CUnsignedInt(self.lengthOfBytes(using: .utf8))
|
|
||||||
let digestLen = Int(CC_MD5_DIGEST_LENGTH)
|
|
||||||
let result = UnsafeMutablePointer<UInt8>.allocate(capacity: digestLen)
|
|
||||||
|
|
||||||
CC_MD5(str!, strLen, result)
|
|
||||||
|
|
||||||
let hash = NSMutableString()
|
|
||||||
for i in 0..<digestLen {
|
|
||||||
hash.appendFormat("%02x", result[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
result.deallocate()
|
|
||||||
return hash as String
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 基础参数结构体
|
|
||||||
struct BaseParameters: Encodable {
|
|
||||||
let acceptLanguage: String
|
|
||||||
let os: String = "iOS"
|
|
||||||
let osVersion: String
|
|
||||||
let ispType: String
|
|
||||||
let channel: String
|
|
||||||
let model: String
|
|
||||||
let deviceId: String
|
|
||||||
let appVersion: String
|
|
||||||
let app: String
|
|
||||||
let mcc: String?
|
|
||||||
let pub_sign: String
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case acceptLanguage = "Accept-Language"
|
|
||||||
case os, osVersion, ispType, channel, model, deviceId
|
|
||||||
case appVersion, app, mcc
|
|
||||||
case pub_sign
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
// 获取系统首选语言(使用新的语言管理器)
|
|
||||||
self.acceptLanguage = LanguageManager.getCurrentLanguage()
|
|
||||||
|
|
||||||
// 获取系统版本
|
|
||||||
self.osVersion = UIDevice.current.systemVersion
|
|
||||||
|
|
||||||
// 获取运营商信息
|
|
||||||
let networkInfo = CTTelephonyNetworkInfo()
|
|
||||||
var ispType = "65535"
|
|
||||||
var mcc: String? = nil // 初始化 mcc 变量
|
|
||||||
|
|
||||||
if #available(iOS 12.0, *) {
|
|
||||||
// 使用新的 API
|
|
||||||
if let carriers = networkInfo.serviceSubscriberCellularProviders,
|
|
||||||
let carrier = carriers.values.first {
|
|
||||||
ispType = (
|
|
||||||
carrier.mobileNetworkCode != nil ? carrier.mobileNetworkCode : ""
|
|
||||||
) ?? "65535"
|
|
||||||
mcc = carrier.mobileCountryCode
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 兼容旧版本
|
|
||||||
if let carrier = networkInfo.subscriberCellularProvider {
|
|
||||||
ispType = (
|
|
||||||
carrier.mobileNetworkCode != nil ? carrier.mobileNetworkCode : ""
|
|
||||||
) ?? "65535"
|
|
||||||
mcc = carrier.mobileCountryCode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.ispType = ispType
|
|
||||||
self.mcc = mcc // 确保在所有路径中都设置了 mcc
|
|
||||||
|
|
||||||
// 获取渠道信息
|
|
||||||
self.channel = ChannelManager.getCurrentChannel()
|
|
||||||
|
|
||||||
// 获取设备型号
|
|
||||||
self.model = DeviceManager.getDeviceModel()
|
|
||||||
|
|
||||||
// 获取设备唯一标识
|
|
||||||
self.deviceId = UIDevice.current.identifierForVendor?.uuidString ?? ""
|
|
||||||
|
|
||||||
// 获取应用版本
|
|
||||||
self.appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
|
|
||||||
|
|
||||||
// 获取应用名称
|
|
||||||
self.app = Bundle.main.infoDictionary?["CFBundleName"] as? String ?? ""
|
|
||||||
|
|
||||||
// 生成 pub_sign
|
|
||||||
let key = "rpbs6us1m8r2j9g6u06ff2bo18orwaya"
|
|
||||||
let signString = "key=\(key)"
|
|
||||||
self.pub_sign = signString.md5().uppercased()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class NetworkManager {
|
|
||||||
static let shared = NetworkManager()
|
|
||||||
|
|
||||||
// 网络响应结构体
|
|
||||||
struct NetworkResponse<T> {
|
|
||||||
let statusCode: Int
|
|
||||||
let data: T?
|
|
||||||
let headers: [AnyHashable: Any]
|
|
||||||
let metrics: URLSessionTaskMetrics?
|
|
||||||
|
|
||||||
var isSuccessful: Bool {
|
|
||||||
return (200...299).contains(statusCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private let reachability = NetworkReachabilityManager()
|
|
||||||
private var isNetworkReachable = false
|
|
||||||
private let baseURL = AppConfig.baseURL
|
|
||||||
private let session: Session
|
|
||||||
private let retryLimit = 2
|
|
||||||
|
|
||||||
// 网络状态监听回调
|
|
||||||
var networkStatusChanged: ((NetworkStatus) -> Void)?
|
|
||||||
|
|
||||||
init() {
|
|
||||||
let configuration = URLSessionConfiguration.af.default
|
|
||||||
configuration.httpShouldSetCookies = true
|
|
||||||
configuration.httpCookieAcceptPolicy = .always
|
|
||||||
configuration.timeoutIntervalForRequest = 60
|
|
||||||
configuration.timeoutIntervalForResource = 60
|
|
||||||
configuration.httpMaximumConnectionsPerHost = 10
|
|
||||||
|
|
||||||
// 支持的内容类型
|
|
||||||
configuration.httpAdditionalHeaders = [
|
|
||||||
"Accept": "application/json, text/json, text/javascript, text/html, text/plain, image/jpeg, image/png",
|
|
||||||
"Accept-Encoding": "gzip, deflate, br",
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
]
|
|
||||||
|
|
||||||
// 强制 TLS 1.2+ 并禁用 HTTP/1.1
|
|
||||||
configuration.tlsMinimumSupportedProtocolVersion = .TLSv12
|
|
||||||
configuration.httpShouldUsePipelining = true
|
|
||||||
|
|
||||||
// 重试策略
|
|
||||||
let retrier = RetryPolicy(retryLimit: UInt(retryLimit))
|
|
||||||
|
|
||||||
session = Session(
|
|
||||||
configuration: configuration,
|
|
||||||
interceptor: retrier,
|
|
||||||
eventMonitors: [AlamofireLogger()]
|
|
||||||
)
|
|
||||||
|
|
||||||
// 添加网络可达性监听
|
|
||||||
setupReachability()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setupReachability() {
|
|
||||||
reachability?.startListening { [weak self] status in
|
|
||||||
guard let self = self else { return }
|
|
||||||
|
|
||||||
switch status {
|
|
||||||
case .reachable(.ethernetOrWiFi):
|
|
||||||
self.isNetworkReachable = true
|
|
||||||
self.networkStatusChanged?(.reachableViaWiFi)
|
|
||||||
case .reachable(.cellular):
|
|
||||||
self.isNetworkReachable = true
|
|
||||||
self.networkStatusChanged?(.reachableViaWWAN)
|
|
||||||
case .notReachable:
|
|
||||||
self.isNetworkReachable = false
|
|
||||||
self.networkStatusChanged?(.notReachable)
|
|
||||||
case .unknown:
|
|
||||||
self.isNetworkReachable = false
|
|
||||||
self.networkStatusChanged?(.notReachable)
|
|
||||||
case .reachable(_):
|
|
||||||
self.isNetworkReachable = true
|
|
||||||
self.networkStatusChanged?(.reachableViaWiFi)
|
|
||||||
@unknown default:
|
|
||||||
fatalError("未知的网络状态")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 便利方法
|
|
||||||
|
|
||||||
/// 发送 GET 请求
|
|
||||||
func get<T: Decodable>(
|
|
||||||
path: String,
|
|
||||||
queryItems: [String: String]? = nil,
|
|
||||||
completion: @escaping (Result<T, NetworkError>) -> Void
|
|
||||||
) {
|
|
||||||
enhancedRequest(
|
|
||||||
path: path,
|
|
||||||
method: .get,
|
|
||||||
queryItems: queryItems,
|
|
||||||
responseType: T.self
|
|
||||||
) { result in
|
|
||||||
switch result {
|
|
||||||
case .success(let response):
|
|
||||||
if let data = response.data as? T {
|
|
||||||
completion(.success(data))
|
|
||||||
} else {
|
|
||||||
completion(.failure(.decodingFailed))
|
|
||||||
}
|
|
||||||
case .failure(let error):
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 发送 POST 请求
|
|
||||||
func post<T: Decodable, P: Encodable>(
|
|
||||||
path: String,
|
|
||||||
parameters: P,
|
|
||||||
completion: @escaping (Result<T, NetworkError>) -> Void
|
|
||||||
) {
|
|
||||||
enhancedRequest(
|
|
||||||
path: path,
|
|
||||||
method: .post,
|
|
||||||
bodyParameters: parameters,
|
|
||||||
responseType: T.self
|
|
||||||
) { result in
|
|
||||||
switch result {
|
|
||||||
case .success(let response):
|
|
||||||
if let data = response.data as? T {
|
|
||||||
completion(.success(data))
|
|
||||||
} else {
|
|
||||||
completion(.failure(.decodingFailed))
|
|
||||||
}
|
|
||||||
case .failure(let error):
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 核心请求方法
|
|
||||||
func enhancedRequest<T>(
|
|
||||||
path: String,
|
|
||||||
method: HTTPMethod = .get,
|
|
||||||
queryItems: [String: String]? = nil,
|
|
||||||
bodyParameters: Encodable? = nil,
|
|
||||||
responseType: T.Type = Data.self,
|
|
||||||
completion: @escaping (Result<NetworkResponse<T>, NetworkError>) -> Void
|
|
||||||
) {
|
|
||||||
// 前置网络检查
|
|
||||||
guard isNetworkReachable else {
|
|
||||||
completion(.failure(.networkUnavailable))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let baseURL = URL(string: baseURL) else {
|
|
||||||
completion(.failure(.invalidURL))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var urlComponents = URLComponents(url: baseURL.appendingPathComponent(path), resolvingAgainstBaseURL: true)
|
|
||||||
urlComponents?.queryItems = queryItems?.map { URLQueryItem(name: $0.key, value: $0.value) }
|
|
||||||
|
|
||||||
guard let finalURL = urlComponents?.url else {
|
|
||||||
completion(.failure(.invalidURL))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 合并基础参数和自定义参数
|
|
||||||
// TODO: 补充加密验参:pub_sign
|
|
||||||
let baseParams = BaseParameters()
|
|
||||||
var parameters: Parameters = baseParams.dictionary ?? [:]
|
|
||||||
|
|
||||||
if let customParams = bodyParameters {
|
|
||||||
if let dict = try? customParams.asDictionary() {
|
|
||||||
parameters.merge(dict) { (_, new) in new }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
session.request(finalURL, method: method, parameters: parameters, encoding: JSONEncoding.default, headers: commonHeaders)
|
|
||||||
.validate()
|
|
||||||
.responseData { [weak self] response in
|
|
||||||
guard let self = self else { return }
|
|
||||||
|
|
||||||
let statusCode = response.response?.statusCode ?? -1
|
|
||||||
let headers = response.response?.allHeaderFields ?? [:]
|
|
||||||
let metrics = response.metrics
|
|
||||||
|
|
||||||
switch response.result {
|
|
||||||
case .success(let decodedData):
|
|
||||||
do {
|
|
||||||
let resultData: T
|
|
||||||
if T.self == Data.self {
|
|
||||||
resultData = decodedData as! T
|
|
||||||
} else if let decodableData = decodedData as? T {
|
|
||||||
resultData = decodableData
|
|
||||||
} else {
|
|
||||||
throw AFError.responseSerializationFailed(reason: .decodingFailed(error: NetworkError.decodingFailed))
|
|
||||||
}
|
|
||||||
|
|
||||||
let networkResponse = NetworkResponse(
|
|
||||||
statusCode: statusCode,
|
|
||||||
data: resultData,
|
|
||||||
headers: headers,
|
|
||||||
metrics: metrics
|
|
||||||
)
|
|
||||||
|
|
||||||
if networkResponse.isSuccessful {
|
|
||||||
completion(.success(networkResponse))
|
|
||||||
} else {
|
|
||||||
self.handleErrorResponse(statusCode: statusCode, completion: completion)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
completion(.failure(.decodingFailed))
|
|
||||||
}
|
|
||||||
|
|
||||||
case .failure(let error):
|
|
||||||
self.handleRequestError(error, statusCode: statusCode, completion: completion)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 错误处理
|
|
||||||
|
|
||||||
private func handleErrorResponse<T>(
|
|
||||||
statusCode: Int,
|
|
||||||
completion: (Result<NetworkResponse<T>, NetworkError>) -> Void
|
|
||||||
) {
|
|
||||||
switch statusCode {
|
|
||||||
case 401:
|
|
||||||
completion(.failure(.unauthorized))
|
|
||||||
case 429:
|
|
||||||
completion(.failure(.rateLimited))
|
|
||||||
case 500...599:
|
|
||||||
completion(.failure(.serverError(message: "服务器错误 \(statusCode)")))
|
|
||||||
default:
|
|
||||||
completion(.failure(.requestFailed(statusCode: statusCode, message: "请求失败")))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleRequestError<T>(
|
|
||||||
_ error: AFError,
|
|
||||||
statusCode: Int,
|
|
||||||
completion: (Result<NetworkResponse<T>, NetworkError>) -> Void
|
|
||||||
) {
|
|
||||||
if let underlyingError = error.underlyingError as? URLError {
|
|
||||||
switch underlyingError.code {
|
|
||||||
case .notConnectedToInternet:
|
|
||||||
completion(.failure(.networkUnavailable))
|
|
||||||
default:
|
|
||||||
completion(.failure(.requestFailed(
|
|
||||||
statusCode: statusCode,
|
|
||||||
message: underlyingError.localizedDescription
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
completion(.failure(.invalidResponse))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 公共头信息
|
|
||||||
private var commonHeaders: HTTPHeaders {
|
|
||||||
var headers = HTTPHeaders()
|
|
||||||
|
|
||||||
// 公共头信息
|
|
||||||
if let language = Locale.preferredLanguages.first {
|
|
||||||
headers.add(name: "Accept-Language", value: language)
|
|
||||||
}
|
|
||||||
headers.add(name: "App-Version", value: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0")
|
|
||||||
|
|
||||||
// 登录相关头信息
|
|
||||||
let uid = "" //AccountInfoStorage.instance?.getUid() ?? ""
|
|
||||||
let ticket = "" //AccountInfoStorage.instance?.getTicket() ?? ""
|
|
||||||
headers.add(name: "pub_uid", value: uid)
|
|
||||||
headers.add(name: "pub_ticket", value: ticket)
|
|
||||||
|
|
||||||
return headers
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 公共请求方法
|
|
||||||
func request<T: Decodable, P: Encodable>(
|
|
||||||
_ path: String,
|
|
||||||
method: HTTPMethod = .get,
|
|
||||||
parameters: P? = nil,
|
|
||||||
completion: @escaping (Result<T, NetworkError>) -> Void
|
|
||||||
) {
|
|
||||||
enhancedRequest(
|
|
||||||
path: path,
|
|
||||||
method: method,
|
|
||||||
bodyParameters: parameters,
|
|
||||||
responseType: T.self
|
|
||||||
) { result in
|
|
||||||
switch result {
|
|
||||||
case .success(let response):
|
|
||||||
if let data = response.data {
|
|
||||||
completion(.success(data))
|
|
||||||
} else {
|
|
||||||
completion(.failure(.decodingFailed))
|
|
||||||
}
|
|
||||||
case .failure(let error):
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加一个便利方法,用于处理字典参数
|
|
||||||
func request<T: Decodable>(
|
|
||||||
_ path: String,
|
|
||||||
method: HTTPMethod = .get,
|
|
||||||
parameters: [String: Any]? = nil,
|
|
||||||
completion: @escaping (Result<T, NetworkError>) -> Void
|
|
||||||
) {
|
|
||||||
// 将字典转换为 Data,然后再解码为 [String: String]
|
|
||||||
if let params = parameters {
|
|
||||||
do {
|
|
||||||
let jsonData = try JSONSerialization.data(withJSONObject: params)
|
|
||||||
let decoder = JSONDecoder()
|
|
||||||
let encodableParams = try decoder.decode([String: String].self, from: jsonData)
|
|
||||||
|
|
||||||
request(path, method: method, parameters: encodableParams, completion: completion)
|
|
||||||
} catch {
|
|
||||||
completion(.failure(.decodingFailed))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 如果没有参数,使用空字典
|
|
||||||
request(path, method: method, parameters: [String: String](), completion: completion)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 语言管理
|
|
||||||
func getCurrentLanguage() -> String {
|
|
||||||
return LanguageManager.getCurrentLanguage()
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateLanguage(_ language: String) {
|
|
||||||
LanguageManager.updateLanguage(language)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Logger
|
|
||||||
final class AlamofireLogger: EventMonitor {
|
|
||||||
func requestDidResume(_ request: Request) {
|
|
||||||
let allHeaders = request.request?.allHTTPHeaderFields ?? [:]
|
|
||||||
let relevantHeaders = allHeaders.filter { !$0.key.contains("Authorization") }
|
|
||||||
|
|
||||||
print("🚀 Request Started: \(request.description)")
|
|
||||||
print("📝 Headers: \(relevantHeaders)")
|
|
||||||
|
|
||||||
if let httpBody = request.request?.httpBody,
|
|
||||||
let parameters = try? JSONSerialization.jsonObject(with: httpBody) {
|
|
||||||
print("📦 Parameters: \(parameters)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func request(_ request: DataRequest, didValidateRequest urlRequest: URLRequest?, response: HTTPURLResponse, data: Data?) {
|
|
||||||
print("📥 Response Status: \(response.statusCode)")
|
|
||||||
|
|
||||||
if let data = data,
|
|
||||||
let json = try? JSONSerialization.jsonObject(with: data) {
|
|
||||||
print("📄 Response Data: \(json)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Encodable Extension
|
|
||||||
private extension Encodable {
|
|
||||||
var dictionary: [String: Any]? {
|
|
||||||
guard let data = try? JSONEncoder().encode(self) else { return nil }
|
|
||||||
return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any] }
|
|
||||||
}
|
|
||||||
|
|
||||||
func asDictionary() throws -> [String: Any] {
|
|
||||||
let data = try JSONEncoder().encode(self)
|
|
||||||
guard let dictionary = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else {
|
|
||||||
throw NetworkError.decodingFailed
|
|
||||||
}
|
|
||||||
return dictionary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 语言管理
|
|
||||||
enum LanguageManager {
|
|
||||||
static let languageKey = "UserSelectedLanguage"
|
|
||||||
|
|
||||||
// 映射语言代码
|
|
||||||
static func mapLanguage(_ language: String) -> String {
|
|
||||||
// 处理完整的语言代码,如 "zh-Hans"、"zh-Hant"、"zh-HK" 等
|
|
||||||
if language.hasPrefix("zh-Hans") || language.hasPrefix("zh-CN") {
|
|
||||||
return "zh-Hant" // 简体中文也返回繁体
|
|
||||||
} else if language.hasPrefix("zh") {
|
|
||||||
return "zh-Hant" // 其他中文变体都返回繁体
|
|
||||||
} else if language.hasPrefix("ar") {
|
|
||||||
return "ar" // 阿拉伯语
|
|
||||||
} else if language.hasPrefix("tr") {
|
|
||||||
return "tr" // 土耳其语
|
|
||||||
} else {
|
|
||||||
return "en" // 默认英文
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取当前语言
|
|
||||||
static func getCurrentLanguage() -> String {
|
|
||||||
// 先从 UserDefaults 获取
|
|
||||||
if let savedLanguage = UserDefaults.standard.string(forKey: languageKey) {
|
|
||||||
return savedLanguage
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取系统首选语言
|
|
||||||
let preferredLanguages = Locale.preferredLanguages.first
|
|
||||||
// let systemLanguage = preferredLanguages.first ?? Locale.current.languageCode ?? "en"
|
|
||||||
|
|
||||||
// 映射并保存语言设置
|
|
||||||
let mappedLanguage = mapLanguage(preferredLanguages ?? "en")
|
|
||||||
UserDefaults.standard.set(mappedLanguage, forKey: languageKey)
|
|
||||||
UserDefaults.standard.synchronize()
|
|
||||||
|
|
||||||
return mappedLanguage
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新语言设置
|
|
||||||
static func updateLanguage(_ language: String) {
|
|
||||||
let mappedLanguage = mapLanguage(language)
|
|
||||||
UserDefaults.standard.set(mappedLanguage, forKey: languageKey)
|
|
||||||
UserDefaults.standard.synchronize()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取系统语言代码(调试用)
|
|
||||||
static func getSystemLanguageInfo() -> (preferred: [String], current: String?) {
|
|
||||||
return (
|
|
||||||
Bundle.main.preferredLocalizations,
|
|
||||||
Locale.current.languageCode
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 设备型号管理
|
|
||||||
enum DeviceManager {
|
|
||||||
// 获取设备标识符
|
|
||||||
static func getDeviceIdentifier() -> String {
|
|
||||||
var systemInfo = utsname()
|
|
||||||
uname(&systemInfo)
|
|
||||||
let machineMirror = Mirror(reflecting: systemInfo.machine)
|
|
||||||
let identifier = machineMirror.children.reduce("") { identifier, element in
|
|
||||||
guard let value = element.value as? Int8, value != 0 else { return identifier }
|
|
||||||
return identifier + String(UnicodeScalar(UInt8(value)))
|
|
||||||
}
|
|
||||||
return identifier
|
|
||||||
}
|
|
||||||
|
|
||||||
// 映射设备标识符到具体型号
|
|
||||||
static func mapDeviceModel(_ identifier: String) -> String {
|
|
||||||
switch identifier {
|
|
||||||
// iPhone
|
|
||||||
case "iPhone13,1": return "iPhone 12 mini"
|
|
||||||
case "iPhone13,2": return "iPhone 12"
|
|
||||||
case "iPhone13,3": return "iPhone 12 Pro"
|
|
||||||
case "iPhone13,4": return "iPhone 12 Pro Max"
|
|
||||||
case "iPhone14,4": return "iPhone 13 mini"
|
|
||||||
case "iPhone14,5": return "iPhone 13"
|
|
||||||
case "iPhone14,2": return "iPhone 13 Pro"
|
|
||||||
case "iPhone14,3": return "iPhone 13 Pro Max"
|
|
||||||
case "iPhone14,7": return "iPhone 14"
|
|
||||||
case "iPhone14,8": return "iPhone 14 Plus"
|
|
||||||
case "iPhone15,2": return "iPhone 14 Pro"
|
|
||||||
case "iPhone15,3": return "iPhone 14 Pro Max"
|
|
||||||
case "iPhone15,4": return "iPhone 15"
|
|
||||||
case "iPhone15,5": return "iPhone 15 Plus"
|
|
||||||
case "iPhone16,1": return "iPhone 15 Pro"
|
|
||||||
case "iPhone16,2": return "iPhone 15 Pro Max"
|
|
||||||
|
|
||||||
// iPad
|
|
||||||
case "iPad13,1", "iPad13,2": return "iPad Air (4th generation)"
|
|
||||||
case "iPad13,4", "iPad13,5", "iPad13,6", "iPad13,7": return "iPad Pro 12.9-inch (5th generation)"
|
|
||||||
case "iPad13,8", "iPad13,9", "iPad13,10", "iPad13,11": return "iPad Pro 12.9-inch (6th generation)"
|
|
||||||
|
|
||||||
// iPod
|
|
||||||
case "iPod9,1": return "iPod touch (7th generation)"
|
|
||||||
|
|
||||||
// 模拟器
|
|
||||||
case "i386", "x86_64", "arm64": return "Simulator \(mapDeviceModel(ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "iOS"))"
|
|
||||||
|
|
||||||
default: return identifier // 如果找不到对应的型号,返回原始标识符
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取具体的设备型号
|
|
||||||
static func getDeviceModel() -> String {
|
|
||||||
let identifier = getDeviceIdentifier()
|
|
||||||
return mapDeviceModel(identifier)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 渠道管理
|
|
||||||
enum ChannelManager {
|
|
||||||
static let enterpriseBundleId = "com.stupidmonkey.yana.yana"//"com.hflighting.yumi"
|
|
||||||
|
|
||||||
enum ChannelType: String {
|
|
||||||
case enterprise = "enterprise"
|
|
||||||
case testflight = "testflight"
|
|
||||||
case appstore = "appstore"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 判断是否企业版
|
|
||||||
static func isEnterprise() -> Bool {
|
|
||||||
let bundleId = Bundle.main.bundleIdentifier ?? ""
|
|
||||||
return bundleId == enterpriseBundleId
|
|
||||||
}
|
|
||||||
|
|
||||||
// 判断是否 TestFlight 版本
|
|
||||||
static func isTestFlight() -> Bool {
|
|
||||||
return Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取当前渠道
|
|
||||||
static func getCurrentChannel() -> String {
|
|
||||||
if isEnterprise() {
|
|
||||||
return ChannelType.enterprise.rawValue
|
|
||||||
} else if isTestFlight() {
|
|
||||||
return ChannelType.testflight.rawValue
|
|
||||||
} else {
|
|
||||||
return ChannelType.appstore.rawValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,5 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict/>
|
<dict>
|
||||||
|
<key>com.apple.external-accessory.wireless-configuration</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import NIMSDK
|
import ComposableArchitecture
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct yanaApp: App {
|
struct yanaApp: App {
|
||||||
@@ -14,7 +14,23 @@ struct yanaApp: App {
|
|||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView()
|
ContentView(
|
||||||
|
store: Store(
|
||||||
|
initialState: LoginFeature.State()
|
||||||
|
) {
|
||||||
|
LoginFeature()
|
||||||
|
},
|
||||||
|
initStore: Store(
|
||||||
|
initialState: InitFeature.State()
|
||||||
|
) {
|
||||||
|
InitFeature()
|
||||||
|
},
|
||||||
|
configStore: Store(
|
||||||
|
initialState: ConfigFeature.State()
|
||||||
|
) {
|
||||||
|
ConfigFeature()
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user