Compare commits
9 Commits
573c9a42ae
...
codex/push
| Author | SHA1 | Date | |
|---|---|---|---|
| 413a9603a4 | |||
| f87b13cc05 | |||
| 0e0415eec4 | |||
| 7ccbc5568d | |||
| 787a166263 | |||
| 74f7370aab | |||
| 1b2637f0ee | |||
| caeeb9a1cf | |||
| e7f1d777dd |
19
Podfile
19
Podfile
@ -1,13 +1,23 @@
|
|||||||
platform :ios, '16.0'
|
platform :ios, '16.0'
|
||||||
|
|
||||||
|
project 'suixinkan.xcodeproj',
|
||||||
|
'Debug' => :debug,
|
||||||
|
'SimulatorDebug' => :debug,
|
||||||
|
'Release' => :release
|
||||||
|
|
||||||
target 'suixinkan' do
|
target 'suixinkan' do
|
||||||
use_frameworks!
|
use_frameworks!
|
||||||
pod 'AMapLocation-NO-IDFA'
|
# 高德 SDK 未提供 arm64-simulator slice,仅在真机配置中接入。
|
||||||
pod 'AMap3DMap-NO-IDFA'
|
pod 'AMapFoundation-NO-IDFA', :configurations => ['Debug', 'Release']
|
||||||
pod 'AMapSearch-NO-IDFA'
|
pod 'AMapLocation-NO-IDFA', :configurations => ['Debug', 'Release']
|
||||||
|
pod 'AMap3DMap-NO-IDFA', :configurations => ['Debug', 'Release']
|
||||||
|
pod 'AMapSearch-NO-IDFA', :configurations => ['Debug', 'Release']
|
||||||
pod 'UMCommon'
|
pod 'UMCommon'
|
||||||
pod 'UMDevice'
|
pod 'UMDevice'
|
||||||
pod 'UMAPM'
|
pod 'UMAPM'
|
||||||
|
# 极光推送:固定无 IDFA 的 JCore,避免引入 ATT 授权。
|
||||||
|
pod 'JCore', '5.4.2-noidfa'
|
||||||
|
pod 'JPush', '6.1.0'
|
||||||
pod 'IQKeyboardManagerSwift'
|
pod 'IQKeyboardManagerSwift'
|
||||||
# 微信 Open SDK:仅接入分享能力,不调用支付 API。
|
# 微信 Open SDK:仅接入分享能力,不调用支付 API。
|
||||||
pod 'WechatOpenSDK-XCFramework', '~> 2.0.4'
|
pod 'WechatOpenSDK-XCFramework', '~> 2.0.4'
|
||||||
@ -21,6 +31,9 @@ post_install do |installer|
|
|||||||
installer.pods_project.targets.each do |target|
|
installer.pods_project.targets.each do |target|
|
||||||
target.build_configurations.each do |config|
|
target.build_configurations.each do |config|
|
||||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '16.0'
|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '16.0'
|
||||||
|
if target.name == 'Pods-suixinkanTests' && config.name == 'SimulatorDebug'
|
||||||
|
config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = ''
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
12
Podfile.lock
12
Podfile.lock
@ -46,6 +46,9 @@ PODS:
|
|||||||
- IQKeyboardCore
|
- IQKeyboardCore
|
||||||
- IQTextView (1.0.5):
|
- IQTextView (1.0.5):
|
||||||
- IQKeyboardToolbar/Placeholderable
|
- IQKeyboardToolbar/Placeholderable
|
||||||
|
- JCore (5.4.2-noidfa)
|
||||||
|
- JPush (6.1.0):
|
||||||
|
- JCore (> 5.1.1)
|
||||||
- UMAPM (2.0.7):
|
- UMAPM (2.0.7):
|
||||||
- UMCommon
|
- UMCommon
|
||||||
- UMCommon (7.5.11):
|
- UMCommon (7.5.11):
|
||||||
@ -55,9 +58,12 @@ PODS:
|
|||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- AMap3DMap-NO-IDFA
|
- AMap3DMap-NO-IDFA
|
||||||
|
- AMapFoundation-NO-IDFA
|
||||||
- AMapLocation-NO-IDFA
|
- AMapLocation-NO-IDFA
|
||||||
- AMapSearch-NO-IDFA
|
- AMapSearch-NO-IDFA
|
||||||
- IQKeyboardManagerSwift
|
- IQKeyboardManagerSwift
|
||||||
|
- JCore (= 5.4.2-noidfa)
|
||||||
|
- JPush (= 6.1.0)
|
||||||
- UMAPM
|
- UMAPM
|
||||||
- UMCommon
|
- UMCommon
|
||||||
- UMDevice
|
- UMDevice
|
||||||
@ -77,6 +83,8 @@ SPEC REPOS:
|
|||||||
- IQKeyboardToolbarManager
|
- IQKeyboardToolbarManager
|
||||||
- IQTextInputViewNotification
|
- IQTextInputViewNotification
|
||||||
- IQTextView
|
- IQTextView
|
||||||
|
- JCore
|
||||||
|
- JPush
|
||||||
- UMAPM
|
- UMAPM
|
||||||
- UMCommon
|
- UMCommon
|
||||||
- UMDevice
|
- UMDevice
|
||||||
@ -95,11 +103,13 @@ SPEC CHECKSUMS:
|
|||||||
IQKeyboardToolbarManager: 242e858cc74e06e00aa989bea1355b24d4397854
|
IQKeyboardToolbarManager: 242e858cc74e06e00aa989bea1355b24d4397854
|
||||||
IQTextInputViewNotification: 3b9fb27a16e7ee8958cc9092cfb07a1a9e1fd559
|
IQTextInputViewNotification: 3b9fb27a16e7ee8958cc9092cfb07a1a9e1fd559
|
||||||
IQTextView: ae13b4922f22e6f027f62c557d9f4f236b19d5c7
|
IQTextView: ae13b4922f22e6f027f62c557d9f4f236b19d5c7
|
||||||
|
JCore: 696e0ac22f417bb0ac4729fc41bc2941aef14e2b
|
||||||
|
JPush: 891c497687be788b094bde368d51289355ee1112
|
||||||
UMAPM: ba9fbebe8dd2048b251c332ab0afc86e1ba5ea63
|
UMAPM: ba9fbebe8dd2048b251c332ab0afc86e1ba5ea63
|
||||||
UMCommon: d652b3b372a801b36db203f5b546ab3e15676898
|
UMCommon: d652b3b372a801b36db203f5b546ab3e15676898
|
||||||
UMDevice: 20b7b3c37a36b2c5c7ca8b5a54386ff9ed2b3b19
|
UMDevice: 20b7b3c37a36b2c5c7ca8b5a54386ff9ed2b3b19
|
||||||
WechatOpenSDK-XCFramework: ff342ae616bb86df3d236aca38059dfd4bc4a949
|
WechatOpenSDK-XCFramework: ff342ae616bb86df3d236aca38059dfd4bc4a949
|
||||||
|
|
||||||
PODFILE CHECKSUM: 0fd38f741cbaae39c8e7a70fa233f651d3b34d0f
|
PODFILE CHECKSUM: 5abf70ade4a9c4b66b64877fdea91f1d086bcaf5
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
1
Pods/JCore/README.md
generated
Normal file
1
Pods/JCore/README.md
generated
Normal file
@ -0,0 +1 @@
|
|||||||
|
# jcore-sdk
|
||||||
44
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/Info.plist
generated
Normal file
44
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/Info.plist
generated
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>AvailableLibraries</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>HeadersPath</key>
|
||||||
|
<string>Headers</string>
|
||||||
|
<key>LibraryIdentifier</key>
|
||||||
|
<string>ios-arm64</string>
|
||||||
|
<key>LibraryPath</key>
|
||||||
|
<string>libJCore.a</string>
|
||||||
|
<key>SupportedArchitectures</key>
|
||||||
|
<array>
|
||||||
|
<string>arm64</string>
|
||||||
|
</array>
|
||||||
|
<key>SupportedPlatform</key>
|
||||||
|
<string>ios</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>HeadersPath</key>
|
||||||
|
<string>Headers</string>
|
||||||
|
<key>LibraryIdentifier</key>
|
||||||
|
<string>ios-arm64_x86_64-simulator</string>
|
||||||
|
<key>LibraryPath</key>
|
||||||
|
<string>libJCore.a</string>
|
||||||
|
<key>SupportedArchitectures</key>
|
||||||
|
<array>
|
||||||
|
<string>arm64</string>
|
||||||
|
<string>x86_64</string>
|
||||||
|
</array>
|
||||||
|
<key>SupportedPlatform</key>
|
||||||
|
<string>ios</string>
|
||||||
|
<key>SupportedPlatformVariant</key>
|
||||||
|
<string>simulator</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>XFWK</string>
|
||||||
|
<key>XCFrameworkFormatVersion</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
19
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64/Headers/JGCOREAPI.h
generated
Normal file
19
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64/Headers/JGCOREAPI.h
generated
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
//
|
||||||
|
// JGCOREService.h
|
||||||
|
// JCore
|
||||||
|
//
|
||||||
|
// Created by Shuni Huang on 2024/3/19.
|
||||||
|
// Copyright © 2024 jiguang. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@interface JGCOREAPI : NSObject
|
||||||
|
|
||||||
|
+ (void)ci:(BOOL)enable;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
27
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64/Headers/JGInforCollectionAuth.h
generated
Normal file
27
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64/Headers/JGInforCollectionAuth.h
generated
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
//
|
||||||
|
// JGInforCollectionAuth.h
|
||||||
|
// JCore
|
||||||
|
//
|
||||||
|
// Created by 豆瓣 on 2021/10/27.
|
||||||
|
// Copyright © 2021 jiguang. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
/// 合规接口,是否授权极光采集一定的设备信息
|
||||||
|
@interface JGInforCollectionAuthItems : NSObject
|
||||||
|
/// 是否授权,默认YES
|
||||||
|
@property(nonatomic,assign)BOOL isAuth;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@interface JGInforCollectionAuth : NSObject
|
||||||
|
|
||||||
|
/// 设备信息采集授权接口(合规接口)
|
||||||
|
/// 请务必在调用初始化、功能性接口前调用此接口进行合规授权
|
||||||
|
/// @param authBlock auth:YES 则极光认为您同意极光采集一定的设备信息
|
||||||
|
+(void)JCollectionAuth:(void(^_Nullable)(JGInforCollectionAuthItems *authInfo))authBlock;
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
64
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64/PrivacyInfo.xcprivacy
generated
Normal file
64
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64/PrivacyInfo.xcprivacy
generated
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyTrackingDomains</key>
|
||||||
|
<array/>
|
||||||
|
<key>NSPrivacyCollectedDataTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
|
<string>NSPrivacyCollectedDataTypeCrashData</string>
|
||||||
|
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||||
|
<array>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
|
<string>NSPrivacyCollectedDataTypePreciseLocation</string>
|
||||||
|
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||||
|
<array>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyTracking</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyAccessedAPITypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>E174.1</string>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>CA92.1</string>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>C617.1</string>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
BIN
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64/libJCore.a
generated
Normal file
BIN
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64/libJCore.a
generated
Normal file
Binary file not shown.
19
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64_x86_64-simulator/Headers/JGCOREAPI.h
generated
Normal file
19
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64_x86_64-simulator/Headers/JGCOREAPI.h
generated
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
//
|
||||||
|
// JGCOREService.h
|
||||||
|
// JCore
|
||||||
|
//
|
||||||
|
// Created by Shuni Huang on 2024/3/19.
|
||||||
|
// Copyright © 2024 jiguang. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@interface JGCOREAPI : NSObject
|
||||||
|
|
||||||
|
+ (void)ci:(BOOL)enable;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
//
|
||||||
|
// JGInforCollectionAuth.h
|
||||||
|
// JCore
|
||||||
|
//
|
||||||
|
// Created by 豆瓣 on 2021/10/27.
|
||||||
|
// Copyright © 2021 jiguang. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
/// 合规接口,是否授权极光采集一定的设备信息
|
||||||
|
@interface JGInforCollectionAuthItems : NSObject
|
||||||
|
/// 是否授权,默认YES
|
||||||
|
@property(nonatomic,assign)BOOL isAuth;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@interface JGInforCollectionAuth : NSObject
|
||||||
|
|
||||||
|
/// 设备信息采集授权接口(合规接口)
|
||||||
|
/// 请务必在调用初始化、功能性接口前调用此接口进行合规授权
|
||||||
|
/// @param authBlock auth:YES 则极光认为您同意极光采集一定的设备信息
|
||||||
|
+(void)JCollectionAuth:(void(^_Nullable)(JGInforCollectionAuthItems *authInfo))authBlock;
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
64
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64_x86_64-simulator/PrivacyInfo.xcprivacy
generated
Normal file
64
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64_x86_64-simulator/PrivacyInfo.xcprivacy
generated
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyTrackingDomains</key>
|
||||||
|
<array/>
|
||||||
|
<key>NSPrivacyCollectedDataTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
|
<string>NSPrivacyCollectedDataTypeCrashData</string>
|
||||||
|
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||||
|
<array>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
|
<string>NSPrivacyCollectedDataTypePreciseLocation</string>
|
||||||
|
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||||
|
<array>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyTracking</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyAccessedAPITypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>E174.1</string>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>CA92.1</string>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>C617.1</string>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
BIN
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64_x86_64-simulator/libJCore.a
generated
Normal file
BIN
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64_x86_64-simulator/libJCore.a
generated
Normal file
Binary file not shown.
44
Pods/JPush/jpush-ios-6.1.0.xcframework/Info.plist
generated
Normal file
44
Pods/JPush/jpush-ios-6.1.0.xcframework/Info.plist
generated
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>AvailableLibraries</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>HeadersPath</key>
|
||||||
|
<string>Headers</string>
|
||||||
|
<key>LibraryIdentifier</key>
|
||||||
|
<string>ios-arm64_x86_64-simulator</string>
|
||||||
|
<key>LibraryPath</key>
|
||||||
|
<string>libJPush.a</string>
|
||||||
|
<key>SupportedArchitectures</key>
|
||||||
|
<array>
|
||||||
|
<string>arm64</string>
|
||||||
|
<string>x86_64</string>
|
||||||
|
</array>
|
||||||
|
<key>SupportedPlatform</key>
|
||||||
|
<string>ios</string>
|
||||||
|
<key>SupportedPlatformVariant</key>
|
||||||
|
<string>simulator</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>HeadersPath</key>
|
||||||
|
<string>Headers</string>
|
||||||
|
<key>LibraryIdentifier</key>
|
||||||
|
<string>ios-arm64</string>
|
||||||
|
<key>LibraryPath</key>
|
||||||
|
<string>libJPush.a</string>
|
||||||
|
<key>SupportedArchitectures</key>
|
||||||
|
<array>
|
||||||
|
<string>arm64</string>
|
||||||
|
</array>
|
||||||
|
<key>SupportedPlatform</key>
|
||||||
|
<string>ios</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>XFWK</string>
|
||||||
|
<key>XCFrameworkFormatVersion</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
1023
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64/Headers/JPUSHService.h
generated
Normal file
1023
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64/Headers/JPUSHService.h
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64/PrivacyInfo.xcprivacy
generated
Normal file
23
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64/PrivacyInfo.xcprivacy
generated
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyTrackingDomains</key>
|
||||||
|
<array/>
|
||||||
|
<key>NSPrivacyCollectedDataTypes</key>
|
||||||
|
<array/>
|
||||||
|
<key>NSPrivacyTracking</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyAccessedAPITypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>CA92.1</string>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
BIN
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64/libJPush.a
generated
Normal file
BIN
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64/libJPush.a
generated
Normal file
Binary file not shown.
1023
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64_x86_64-simulator/Headers/JPUSHService.h
generated
Normal file
1023
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64_x86_64-simulator/Headers/JPUSHService.h
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64_x86_64-simulator/PrivacyInfo.xcprivacy
generated
Normal file
23
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64_x86_64-simulator/PrivacyInfo.xcprivacy
generated
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyTrackingDomains</key>
|
||||||
|
<array/>
|
||||||
|
<key>NSPrivacyCollectedDataTypes</key>
|
||||||
|
<array/>
|
||||||
|
<key>NSPrivacyTracking</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyAccessedAPITypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>CA92.1</string>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
BIN
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64_x86_64-simulator/libJPush.a
generated
Normal file
BIN
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64_x86_64-simulator/libJPush.a
generated
Normal file
Binary file not shown.
12
Pods/Manifest.lock
generated
12
Pods/Manifest.lock
generated
@ -46,6 +46,9 @@ PODS:
|
|||||||
- IQKeyboardCore
|
- IQKeyboardCore
|
||||||
- IQTextView (1.0.5):
|
- IQTextView (1.0.5):
|
||||||
- IQKeyboardToolbar/Placeholderable
|
- IQKeyboardToolbar/Placeholderable
|
||||||
|
- JCore (5.4.2-noidfa)
|
||||||
|
- JPush (6.1.0):
|
||||||
|
- JCore (> 5.1.1)
|
||||||
- UMAPM (2.0.7):
|
- UMAPM (2.0.7):
|
||||||
- UMCommon
|
- UMCommon
|
||||||
- UMCommon (7.5.11):
|
- UMCommon (7.5.11):
|
||||||
@ -55,9 +58,12 @@ PODS:
|
|||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- AMap3DMap-NO-IDFA
|
- AMap3DMap-NO-IDFA
|
||||||
|
- AMapFoundation-NO-IDFA
|
||||||
- AMapLocation-NO-IDFA
|
- AMapLocation-NO-IDFA
|
||||||
- AMapSearch-NO-IDFA
|
- AMapSearch-NO-IDFA
|
||||||
- IQKeyboardManagerSwift
|
- IQKeyboardManagerSwift
|
||||||
|
- JCore (= 5.4.2-noidfa)
|
||||||
|
- JPush (= 6.1.0)
|
||||||
- UMAPM
|
- UMAPM
|
||||||
- UMCommon
|
- UMCommon
|
||||||
- UMDevice
|
- UMDevice
|
||||||
@ -77,6 +83,8 @@ SPEC REPOS:
|
|||||||
- IQKeyboardToolbarManager
|
- IQKeyboardToolbarManager
|
||||||
- IQTextInputViewNotification
|
- IQTextInputViewNotification
|
||||||
- IQTextView
|
- IQTextView
|
||||||
|
- JCore
|
||||||
|
- JPush
|
||||||
- UMAPM
|
- UMAPM
|
||||||
- UMCommon
|
- UMCommon
|
||||||
- UMDevice
|
- UMDevice
|
||||||
@ -95,11 +103,13 @@ SPEC CHECKSUMS:
|
|||||||
IQKeyboardToolbarManager: 242e858cc74e06e00aa989bea1355b24d4397854
|
IQKeyboardToolbarManager: 242e858cc74e06e00aa989bea1355b24d4397854
|
||||||
IQTextInputViewNotification: 3b9fb27a16e7ee8958cc9092cfb07a1a9e1fd559
|
IQTextInputViewNotification: 3b9fb27a16e7ee8958cc9092cfb07a1a9e1fd559
|
||||||
IQTextView: ae13b4922f22e6f027f62c557d9f4f236b19d5c7
|
IQTextView: ae13b4922f22e6f027f62c557d9f4f236b19d5c7
|
||||||
|
JCore: 696e0ac22f417bb0ac4729fc41bc2941aef14e2b
|
||||||
|
JPush: 891c497687be788b094bde368d51289355ee1112
|
||||||
UMAPM: ba9fbebe8dd2048b251c332ab0afc86e1ba5ea63
|
UMAPM: ba9fbebe8dd2048b251c332ab0afc86e1ba5ea63
|
||||||
UMCommon: d652b3b372a801b36db203f5b546ab3e15676898
|
UMCommon: d652b3b372a801b36db203f5b546ab3e15676898
|
||||||
UMDevice: 20b7b3c37a36b2c5c7ca8b5a54386ff9ed2b3b19
|
UMDevice: 20b7b3c37a36b2c5c7ca8b5a54386ff9ed2b3b19
|
||||||
WechatOpenSDK-XCFramework: ff342ae616bb86df3d236aca38059dfd4bc4a949
|
WechatOpenSDK-XCFramework: ff342ae616bb86df3d236aca38059dfd4bc4a949
|
||||||
|
|
||||||
PODFILE CHECKSUM: 0fd38f741cbaae39c8e7a70fa233f651d3b34d0f
|
PODFILE CHECKSUM: 5abf70ade4a9c4b66b64877fdea91f1d086bcaf5
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
7001
Pods/Pods.xcodeproj/project.pbxproj
generated
7001
Pods/Pods.xcodeproj/project.pbxproj
generated
File diff suppressed because it is too large
Load Diff
2
Pods/Target Support Files/JCore/JCore-xcframeworks-input-files.xcfilelist
generated
Normal file
2
Pods/Target Support Files/JCore/JCore-xcframeworks-input-files.xcfilelist
generated
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
${PODS_ROOT}/Target Support Files/JCore/JCore-xcframeworks.sh
|
||||||
|
${PODS_ROOT}/JCore/jcore-noidfa-ios-5.4.2.xcframework
|
||||||
1
Pods/Target Support Files/JCore/JCore-xcframeworks-output-files.xcfilelist
generated
Normal file
1
Pods/Target Support Files/JCore/JCore-xcframeworks-output-files.xcfilelist
generated
Normal file
@ -0,0 +1 @@
|
|||||||
|
${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore/jcore-noidfa-ios-5.4.2.framework
|
||||||
121
Pods/Target Support Files/JCore/JCore-xcframeworks.sh
generated
Executable file
121
Pods/Target Support Files/JCore/JCore-xcframeworks.sh
generated
Executable file
@ -0,0 +1,121 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
set -u
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
function on_error {
|
||||||
|
echo "$(realpath -mq "${0}"):$1: error: Unexpected failure"
|
||||||
|
}
|
||||||
|
trap 'on_error $LINENO' ERR
|
||||||
|
|
||||||
|
|
||||||
|
# This protects against multiple targets copying the same framework dependency at the same time. The solution
|
||||||
|
# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html
|
||||||
|
RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????")
|
||||||
|
|
||||||
|
|
||||||
|
variant_for_slice()
|
||||||
|
{
|
||||||
|
case "$1" in
|
||||||
|
"jcore-noidfa-ios-5.4.2.xcframework/ios-arm64")
|
||||||
|
echo ""
|
||||||
|
;;
|
||||||
|
"jcore-noidfa-ios-5.4.2.xcframework/ios-arm64_x86_64-simulator")
|
||||||
|
echo "simulator"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
archs_for_slice()
|
||||||
|
{
|
||||||
|
case "$1" in
|
||||||
|
"jcore-noidfa-ios-5.4.2.xcframework/ios-arm64")
|
||||||
|
echo "arm64"
|
||||||
|
;;
|
||||||
|
"jcore-noidfa-ios-5.4.2.xcframework/ios-arm64_x86_64-simulator")
|
||||||
|
echo "arm64 x86_64"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
copy_dir()
|
||||||
|
{
|
||||||
|
local source="$1"
|
||||||
|
local destination="$2"
|
||||||
|
|
||||||
|
# Use filter instead of exclude so missing patterns don't throw errors.
|
||||||
|
echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" \"${source}*\" \"${destination}\""
|
||||||
|
rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" "${source}"/* "${destination}"
|
||||||
|
}
|
||||||
|
|
||||||
|
SELECT_SLICE_RETVAL=""
|
||||||
|
|
||||||
|
select_slice() {
|
||||||
|
local xcframework_name="$1"
|
||||||
|
xcframework_name="${xcframework_name##*/}"
|
||||||
|
local paths=("${@:2}")
|
||||||
|
# Locate the correct slice of the .xcframework for the current architectures
|
||||||
|
local target_path=""
|
||||||
|
|
||||||
|
# Split archs on space so we can find a slice that has all the needed archs
|
||||||
|
local target_archs=$(echo $ARCHS | tr " " "\n")
|
||||||
|
|
||||||
|
local target_variant=""
|
||||||
|
if [[ "$PLATFORM_NAME" == *"simulator" ]]; then
|
||||||
|
target_variant="simulator"
|
||||||
|
fi
|
||||||
|
if [[ ! -z ${EFFECTIVE_PLATFORM_NAME+x} && "$EFFECTIVE_PLATFORM_NAME" == *"maccatalyst" ]]; then
|
||||||
|
target_variant="maccatalyst"
|
||||||
|
fi
|
||||||
|
for i in ${!paths[@]}; do
|
||||||
|
local matched_all_archs="1"
|
||||||
|
local slice_archs="$(archs_for_slice "${xcframework_name}/${paths[$i]}")"
|
||||||
|
local slice_variant="$(variant_for_slice "${xcframework_name}/${paths[$i]}")"
|
||||||
|
for target_arch in $target_archs; do
|
||||||
|
if ! [[ "${slice_variant}" == "$target_variant" ]]; then
|
||||||
|
matched_all_archs="0"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! echo "${slice_archs}" | tr " " "\n" | grep -F -q -x "$target_arch"; then
|
||||||
|
matched_all_archs="0"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$matched_all_archs" == "1" ]]; then
|
||||||
|
# Found a matching slice
|
||||||
|
echo "Selected xcframework slice ${paths[$i]}"
|
||||||
|
SELECT_SLICE_RETVAL=${paths[$i]}
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
install_xcframework() {
|
||||||
|
local basepath="$1"
|
||||||
|
local name="$2"
|
||||||
|
local package_type="$3"
|
||||||
|
local paths=("${@:4}")
|
||||||
|
|
||||||
|
# Locate the correct slice of the .xcframework for the current architectures
|
||||||
|
select_slice "${basepath}" "${paths[@]}"
|
||||||
|
local target_path="$SELECT_SLICE_RETVAL"
|
||||||
|
if [[ -z "$target_path" ]]; then
|
||||||
|
echo "warning: [CP] $(basename ${basepath}): Unable to find matching slice in '${paths[@]}' for the current build architectures ($ARCHS) and platform (${EFFECTIVE_PLATFORM_NAME-${PLATFORM_NAME}})."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
local source="$basepath/$target_path"
|
||||||
|
|
||||||
|
local destination="${PODS_XCFRAMEWORKS_BUILD_DIR}/${name}"
|
||||||
|
|
||||||
|
if [ ! -d "$destination" ]; then
|
||||||
|
mkdir -p "$destination"
|
||||||
|
fi
|
||||||
|
|
||||||
|
copy_dir "$source/" "$destination"
|
||||||
|
echo "Copied $source to $destination"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_xcframework "${PODS_ROOT}/JCore/jcore-noidfa-ios-5.4.2.xcframework" "JCore" "library" "ios-arm64" "ios-arm64_x86_64-simulator"
|
||||||
|
|
||||||
17
Pods/Target Support Files/JCore/JCore.debug.xcconfig
generated
Normal file
17
Pods/Target Support Files/JCore/JCore.debug.xcconfig
generated
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||||
|
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/JCore
|
||||||
|
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/JCore"
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||||
|
HEADER_SEARCH_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore/Headers"
|
||||||
|
LIBRARY_SEARCH_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore"
|
||||||
|
OTHER_LDFLAGS = $(inherited) -l"JCore" -l"resolv" -l"z" -framework "CFNetwork" -framework "CoreFoundation" -framework "CoreGraphics" -framework "CoreTelephony" -framework "Foundation" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "WebKit" -weak_framework "Network" -weak_framework "UserNotifications"
|
||||||
|
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||||
|
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||||
|
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
|
||||||
|
PODS_ROOT = ${SRCROOT}
|
||||||
|
PODS_TARGET_SRCROOT = ${PODS_ROOT}/JCore
|
||||||
|
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||||
|
SKIP_INSTALL = YES
|
||||||
|
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||||
|
VALID_ARCHS = x86_64 arm64
|
||||||
17
Pods/Target Support Files/JCore/JCore.release.xcconfig
generated
Normal file
17
Pods/Target Support Files/JCore/JCore.release.xcconfig
generated
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||||
|
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/JCore
|
||||||
|
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/JCore"
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||||
|
HEADER_SEARCH_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore/Headers"
|
||||||
|
LIBRARY_SEARCH_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore"
|
||||||
|
OTHER_LDFLAGS = $(inherited) -l"JCore" -l"resolv" -l"z" -framework "CFNetwork" -framework "CoreFoundation" -framework "CoreGraphics" -framework "CoreTelephony" -framework "Foundation" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "WebKit" -weak_framework "Network" -weak_framework "UserNotifications"
|
||||||
|
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||||
|
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||||
|
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
|
||||||
|
PODS_ROOT = ${SRCROOT}
|
||||||
|
PODS_TARGET_SRCROOT = ${PODS_ROOT}/JCore
|
||||||
|
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||||
|
SKIP_INSTALL = YES
|
||||||
|
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||||
|
VALID_ARCHS = x86_64 arm64
|
||||||
2
Pods/Target Support Files/JPush/JPush-xcframeworks-input-files.xcfilelist
generated
Normal file
2
Pods/Target Support Files/JPush/JPush-xcframeworks-input-files.xcfilelist
generated
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
${PODS_ROOT}/Target Support Files/JPush/JPush-xcframeworks.sh
|
||||||
|
${PODS_ROOT}/JPush/jpush-ios-6.1.0.xcframework
|
||||||
1
Pods/Target Support Files/JPush/JPush-xcframeworks-output-files.xcfilelist
generated
Normal file
1
Pods/Target Support Files/JPush/JPush-xcframeworks-output-files.xcfilelist
generated
Normal file
@ -0,0 +1 @@
|
|||||||
|
${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush/jpush-ios-6.1.0.framework
|
||||||
121
Pods/Target Support Files/JPush/JPush-xcframeworks.sh
generated
Executable file
121
Pods/Target Support Files/JPush/JPush-xcframeworks.sh
generated
Executable file
@ -0,0 +1,121 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
set -u
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
function on_error {
|
||||||
|
echo "$(realpath -mq "${0}"):$1: error: Unexpected failure"
|
||||||
|
}
|
||||||
|
trap 'on_error $LINENO' ERR
|
||||||
|
|
||||||
|
|
||||||
|
# This protects against multiple targets copying the same framework dependency at the same time. The solution
|
||||||
|
# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html
|
||||||
|
RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????")
|
||||||
|
|
||||||
|
|
||||||
|
variant_for_slice()
|
||||||
|
{
|
||||||
|
case "$1" in
|
||||||
|
"jpush-ios-6.1.0.xcframework/ios-arm64")
|
||||||
|
echo ""
|
||||||
|
;;
|
||||||
|
"jpush-ios-6.1.0.xcframework/ios-arm64_x86_64-simulator")
|
||||||
|
echo "simulator"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
archs_for_slice()
|
||||||
|
{
|
||||||
|
case "$1" in
|
||||||
|
"jpush-ios-6.1.0.xcframework/ios-arm64")
|
||||||
|
echo "arm64"
|
||||||
|
;;
|
||||||
|
"jpush-ios-6.1.0.xcframework/ios-arm64_x86_64-simulator")
|
||||||
|
echo "arm64 x86_64"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
copy_dir()
|
||||||
|
{
|
||||||
|
local source="$1"
|
||||||
|
local destination="$2"
|
||||||
|
|
||||||
|
# Use filter instead of exclude so missing patterns don't throw errors.
|
||||||
|
echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" \"${source}*\" \"${destination}\""
|
||||||
|
rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" "${source}"/* "${destination}"
|
||||||
|
}
|
||||||
|
|
||||||
|
SELECT_SLICE_RETVAL=""
|
||||||
|
|
||||||
|
select_slice() {
|
||||||
|
local xcframework_name="$1"
|
||||||
|
xcframework_name="${xcframework_name##*/}"
|
||||||
|
local paths=("${@:2}")
|
||||||
|
# Locate the correct slice of the .xcframework for the current architectures
|
||||||
|
local target_path=""
|
||||||
|
|
||||||
|
# Split archs on space so we can find a slice that has all the needed archs
|
||||||
|
local target_archs=$(echo $ARCHS | tr " " "\n")
|
||||||
|
|
||||||
|
local target_variant=""
|
||||||
|
if [[ "$PLATFORM_NAME" == *"simulator" ]]; then
|
||||||
|
target_variant="simulator"
|
||||||
|
fi
|
||||||
|
if [[ ! -z ${EFFECTIVE_PLATFORM_NAME+x} && "$EFFECTIVE_PLATFORM_NAME" == *"maccatalyst" ]]; then
|
||||||
|
target_variant="maccatalyst"
|
||||||
|
fi
|
||||||
|
for i in ${!paths[@]}; do
|
||||||
|
local matched_all_archs="1"
|
||||||
|
local slice_archs="$(archs_for_slice "${xcframework_name}/${paths[$i]}")"
|
||||||
|
local slice_variant="$(variant_for_slice "${xcframework_name}/${paths[$i]}")"
|
||||||
|
for target_arch in $target_archs; do
|
||||||
|
if ! [[ "${slice_variant}" == "$target_variant" ]]; then
|
||||||
|
matched_all_archs="0"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! echo "${slice_archs}" | tr " " "\n" | grep -F -q -x "$target_arch"; then
|
||||||
|
matched_all_archs="0"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$matched_all_archs" == "1" ]]; then
|
||||||
|
# Found a matching slice
|
||||||
|
echo "Selected xcframework slice ${paths[$i]}"
|
||||||
|
SELECT_SLICE_RETVAL=${paths[$i]}
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
install_xcframework() {
|
||||||
|
local basepath="$1"
|
||||||
|
local name="$2"
|
||||||
|
local package_type="$3"
|
||||||
|
local paths=("${@:4}")
|
||||||
|
|
||||||
|
# Locate the correct slice of the .xcframework for the current architectures
|
||||||
|
select_slice "${basepath}" "${paths[@]}"
|
||||||
|
local target_path="$SELECT_SLICE_RETVAL"
|
||||||
|
if [[ -z "$target_path" ]]; then
|
||||||
|
echo "warning: [CP] $(basename ${basepath}): Unable to find matching slice in '${paths[@]}' for the current build architectures ($ARCHS) and platform (${EFFECTIVE_PLATFORM_NAME-${PLATFORM_NAME}})."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
local source="$basepath/$target_path"
|
||||||
|
|
||||||
|
local destination="${PODS_XCFRAMEWORKS_BUILD_DIR}/${name}"
|
||||||
|
|
||||||
|
if [ ! -d "$destination" ]; then
|
||||||
|
mkdir -p "$destination"
|
||||||
|
fi
|
||||||
|
|
||||||
|
copy_dir "$source/" "$destination"
|
||||||
|
echo "Copied $source to $destination"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_xcframework "${PODS_ROOT}/JPush/jpush-ios-6.1.0.xcframework" "JPush" "library" "ios-arm64" "ios-arm64_x86_64-simulator"
|
||||||
|
|
||||||
15
Pods/Target Support Files/JPush/JPush.debug.xcconfig
generated
Normal file
15
Pods/Target Support Files/JPush/JPush.debug.xcconfig
generated
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||||
|
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/JPush
|
||||||
|
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/JCore" "${PODS_ROOT}/JPush"
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||||
|
HEADER_SEARCH_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush/Headers"
|
||||||
|
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||||
|
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||||
|
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
|
||||||
|
PODS_ROOT = ${SRCROOT}
|
||||||
|
PODS_TARGET_SRCROOT = ${PODS_ROOT}/JPush
|
||||||
|
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||||
|
SKIP_INSTALL = YES
|
||||||
|
SWIFT_INCLUDE_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore"
|
||||||
|
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||||
15
Pods/Target Support Files/JPush/JPush.release.xcconfig
generated
Normal file
15
Pods/Target Support Files/JPush/JPush.release.xcconfig
generated
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||||
|
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/JPush
|
||||||
|
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/JCore" "${PODS_ROOT}/JPush"
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||||
|
HEADER_SEARCH_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush/Headers"
|
||||||
|
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||||
|
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||||
|
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
|
||||||
|
PODS_ROOT = ${SRCROOT}
|
||||||
|
PODS_TARGET_SRCROOT = ${PODS_ROOT}/JPush
|
||||||
|
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||||
|
SKIP_INSTALL = YES
|
||||||
|
SWIFT_INCLUDE_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore"
|
||||||
|
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||||
@ -219,6 +219,14 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
|
## JCore
|
||||||
|
|
||||||
|
Copyright jpush.cn
|
||||||
|
|
||||||
|
## JPush
|
||||||
|
|
||||||
|
Copyright jiguang.cn
|
||||||
|
|
||||||
## UMAPM
|
## UMAPM
|
||||||
|
|
||||||
Copyright 2011 - 2020 umeng.com. All rights reserved.
|
Copyright 2011 - 2020 umeng.com. All rights reserved.
|
||||||
|
|||||||
@ -302,6 +302,26 @@ SOFTWARE.
|
|||||||
<key>Type</key>
|
<key>Type</key>
|
||||||
<string>PSGroupSpecifier</string>
|
<string>PSGroupSpecifier</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>FooterText</key>
|
||||||
|
<string>Copyright jpush.cn</string>
|
||||||
|
<key>License</key>
|
||||||
|
<string>Copyright</string>
|
||||||
|
<key>Title</key>
|
||||||
|
<string>JCore</string>
|
||||||
|
<key>Type</key>
|
||||||
|
<string>PSGroupSpecifier</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>FooterText</key>
|
||||||
|
<string>Copyright jiguang.cn</string>
|
||||||
|
<key>License</key>
|
||||||
|
<string>Copyright</string>
|
||||||
|
<key>Title</key>
|
||||||
|
<string>JPush</string>
|
||||||
|
<key>Type</key>
|
||||||
|
<string>PSGroupSpecifier</string>
|
||||||
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>FooterText</key>
|
<key>FooterText</key>
|
||||||
<string>Copyright 2011 - 2020 umeng.com. All rights reserved.
|
<string>Copyright 2011 - 2020 umeng.com. All rights reserved.
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-frameworks.sh
|
||||||
|
${BUILT_PRODUCTS_DIR}/IQKeyboardCore/IQKeyboardCore.framework
|
||||||
|
${BUILT_PRODUCTS_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework
|
||||||
|
${BUILT_PRODUCTS_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework
|
||||||
|
${BUILT_PRODUCTS_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework
|
||||||
|
${BUILT_PRODUCTS_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework
|
||||||
|
${BUILT_PRODUCTS_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework
|
||||||
|
${BUILT_PRODUCTS_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework
|
||||||
|
${BUILT_PRODUCTS_DIR}/IQTextView/IQTextView.framework
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardCore.framework
|
||||||
|
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardManagerSwift.framework
|
||||||
|
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardNotification.framework
|
||||||
|
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardReturnManager.framework
|
||||||
|
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardToolbar.framework
|
||||||
|
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardToolbarManager.framework
|
||||||
|
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQTextInputViewNotification.framework
|
||||||
|
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQTextView.framework
|
||||||
@ -195,6 +195,16 @@ if [[ "$CONFIGURATION" == "Release" ]]; then
|
|||||||
install_framework "${BUILT_PRODUCTS_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework"
|
install_framework "${BUILT_PRODUCTS_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework"
|
||||||
install_framework "${BUILT_PRODUCTS_DIR}/IQTextView/IQTextView.framework"
|
install_framework "${BUILT_PRODUCTS_DIR}/IQTextView/IQTextView.framework"
|
||||||
fi
|
fi
|
||||||
|
if [[ "$CONFIGURATION" == "SimulatorDebug" ]]; then
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardCore/IQKeyboardCore.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/IQTextView/IQTextView.framework"
|
||||||
|
fi
|
||||||
if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then
|
if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then
|
||||||
wait
|
wait
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-resources.sh
|
||||||
@ -3,13 +3,13 @@ ARCHS = $(ARCHS_STANDARD)
|
|||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO
|
ENABLE_USER_SCRIPT_SANDBOXING = NO
|
||||||
EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64
|
EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64
|
||||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_ROOT}/AMap3DMap-NO-IDFA" "${PODS_ROOT}/AMapFoundation-NO-IDFA" "${PODS_ROOT}/AMapLocation-NO-IDFA" "${PODS_ROOT}/AMapSearch-NO-IDFA" "${PODS_ROOT}/UMAPM/UMAPM_2.0.7" "${PODS_ROOT}/UMCommon/UMCommon_7.5.11" "${PODS_ROOT}/UMDevice/UMDevice_3.6.0" "${PODS_ROOT}/WechatOpenSDK-XCFramework" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMAPM" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMCommon" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMDevice" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_ROOT}/AMap3DMap-NO-IDFA" "${PODS_ROOT}/AMapFoundation-NO-IDFA" "${PODS_ROOT}/AMapLocation-NO-IDFA" "${PODS_ROOT}/AMapSearch-NO-IDFA" "${PODS_ROOT}/JCore" "${PODS_ROOT}/JPush" "${PODS_ROOT}/UMAPM/UMAPM_2.0.7" "${PODS_ROOT}/UMCommon/UMCommon_7.5.11" "${PODS_ROOT}/UMDevice/UMDevice_3.6.0" "${PODS_ROOT}/WechatOpenSDK-XCFramework" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMAPM" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMCommon" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMDevice" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers"
|
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush/Headers"
|
||||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks'
|
LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks'
|
||||||
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
LIBRARY_SEARCH_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush" "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
||||||
OTHER_LDFLAGS = $(inherited) -ObjC -l"c++" -l"sqlite3" -l"sqlite3.0" -l"z" -framework "AMapFoundationKit" -framework "AMapLocationKit" -framework "AMapSearchKit" -framework "Combine" -framework "CoreGraphics" -framework "CoreLocation" -framework "CoreTelephony" -framework "CoreText" -framework "ExternalAccessory" -framework "GLKit" -framework "IQKeyboardCore" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "MAMapKit" -framework "OpenGLES" -framework "QuartzCore" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "UMAPM" -framework "UMCommon" -framework "UMDevice" -framework "WebKit" -framework "WechatOpenSDK"
|
OTHER_LDFLAGS = $(inherited) -ObjC -l"JCore" -l"JPush" -l"c++" -l"resolv" -l"sqlite3" -l"sqlite3.0" -l"z" -framework "AMapFoundationKit" -framework "AMapLocationKit" -framework "AMapSearchKit" -framework "CFNetwork" -framework "Combine" -framework "CoreFoundation" -framework "CoreGraphics" -framework "CoreLocation" -framework "CoreTelephony" -framework "CoreText" -framework "ExternalAccessory" -framework "Foundation" -framework "GLKit" -framework "IQKeyboardCore" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "MAMapKit" -framework "OpenGLES" -framework "QuartzCore" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "UMAPM" -framework "UMCommon" -framework "UMDevice" -framework "WebKit" -framework "WechatOpenSDK" -weak_framework "Network" -weak_framework "UserNotifications"
|
||||||
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/AMap3DMap-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapFoundation-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapLocation-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapSearch-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMAPM" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMCommon" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMDevice" "-F${PODS_CONFIGURATION_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/AMap3DMap-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapFoundation-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapLocation-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapSearch-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "-F${PODS_CONFIGURATION_BUILD_DIR}/JCore" "-F${PODS_CONFIGURATION_BUILD_DIR}/JPush" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMAPM" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMCommon" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMDevice" "-F${PODS_CONFIGURATION_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||||
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||||
|
|||||||
@ -3,13 +3,13 @@ ARCHS = $(ARCHS_STANDARD)
|
|||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO
|
ENABLE_USER_SCRIPT_SANDBOXING = NO
|
||||||
EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64
|
EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64
|
||||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_ROOT}/AMap3DMap-NO-IDFA" "${PODS_ROOT}/AMapFoundation-NO-IDFA" "${PODS_ROOT}/AMapLocation-NO-IDFA" "${PODS_ROOT}/AMapSearch-NO-IDFA" "${PODS_ROOT}/UMAPM/UMAPM_2.0.7" "${PODS_ROOT}/UMCommon/UMCommon_7.5.11" "${PODS_ROOT}/UMDevice/UMDevice_3.6.0" "${PODS_ROOT}/WechatOpenSDK-XCFramework" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMAPM" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMCommon" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMDevice" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_ROOT}/AMap3DMap-NO-IDFA" "${PODS_ROOT}/AMapFoundation-NO-IDFA" "${PODS_ROOT}/AMapLocation-NO-IDFA" "${PODS_ROOT}/AMapSearch-NO-IDFA" "${PODS_ROOT}/JCore" "${PODS_ROOT}/JPush" "${PODS_ROOT}/UMAPM/UMAPM_2.0.7" "${PODS_ROOT}/UMCommon/UMCommon_7.5.11" "${PODS_ROOT}/UMDevice/UMDevice_3.6.0" "${PODS_ROOT}/WechatOpenSDK-XCFramework" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMAPM" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMCommon" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMDevice" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers"
|
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush/Headers"
|
||||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks'
|
LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks'
|
||||||
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
LIBRARY_SEARCH_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush" "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
||||||
OTHER_LDFLAGS = $(inherited) -ObjC -l"c++" -l"sqlite3" -l"sqlite3.0" -l"z" -framework "AMapFoundationKit" -framework "AMapLocationKit" -framework "AMapSearchKit" -framework "Combine" -framework "CoreGraphics" -framework "CoreLocation" -framework "CoreTelephony" -framework "CoreText" -framework "ExternalAccessory" -framework "GLKit" -framework "IQKeyboardCore" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "MAMapKit" -framework "OpenGLES" -framework "QuartzCore" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "UMAPM" -framework "UMCommon" -framework "UMDevice" -framework "WebKit" -framework "WechatOpenSDK"
|
OTHER_LDFLAGS = $(inherited) -ObjC -l"JCore" -l"JPush" -l"c++" -l"resolv" -l"sqlite3" -l"sqlite3.0" -l"z" -framework "AMapFoundationKit" -framework "AMapLocationKit" -framework "AMapSearchKit" -framework "CFNetwork" -framework "Combine" -framework "CoreFoundation" -framework "CoreGraphics" -framework "CoreLocation" -framework "CoreTelephony" -framework "CoreText" -framework "ExternalAccessory" -framework "Foundation" -framework "GLKit" -framework "IQKeyboardCore" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "MAMapKit" -framework "OpenGLES" -framework "QuartzCore" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "UMAPM" -framework "UMCommon" -framework "UMDevice" -framework "WebKit" -framework "WechatOpenSDK" -weak_framework "Network" -weak_framework "UserNotifications"
|
||||||
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/AMap3DMap-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapFoundation-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapLocation-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapSearch-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMAPM" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMCommon" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMDevice" "-F${PODS_CONFIGURATION_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/AMap3DMap-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapFoundation-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapLocation-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapSearch-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "-F${PODS_CONFIGURATION_BUILD_DIR}/JCore" "-F${PODS_CONFIGURATION_BUILD_DIR}/JPush" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMAPM" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMCommon" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMDevice" "-F${PODS_CONFIGURATION_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||||
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||||
|
|||||||
18
Pods/Target Support Files/Pods-suixinkan/Pods-suixinkan.simulatordebug.xcconfig
generated
Normal file
18
Pods/Target Support Files/Pods-suixinkan/Pods-suixinkan.simulatordebug.xcconfig
generated
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO
|
||||||
|
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_ROOT}/JCore" "${PODS_ROOT}/JPush" "${PODS_ROOT}/UMAPM/UMAPM_2.0.7" "${PODS_ROOT}/UMCommon/UMCommon_7.5.11" "${PODS_ROOT}/UMDevice/UMDevice_3.6.0" "${PODS_ROOT}/WechatOpenSDK-XCFramework" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMAPM" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMCommon" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMDevice" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||||
|
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush/Headers"
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks'
|
||||||
|
LIBRARY_SEARCH_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush" "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
||||||
|
OTHER_LDFLAGS = $(inherited) -ObjC -l"JCore" -l"JPush" -l"c++" -l"resolv" -l"sqlite3" -l"sqlite3.0" -l"z" -framework "CFNetwork" -framework "Combine" -framework "CoreFoundation" -framework "CoreGraphics" -framework "CoreTelephony" -framework "Foundation" -framework "IQKeyboardCore" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "UMAPM" -framework "UMCommon" -framework "UMDevice" -framework "WebKit" -framework "WechatOpenSDK" -weak_framework "Network" -weak_framework "UserNotifications"
|
||||||
|
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "-F${PODS_CONFIGURATION_BUILD_DIR}/JCore" "-F${PODS_CONFIGURATION_BUILD_DIR}/JPush" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMAPM" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMCommon" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMDevice" "-F${PODS_CONFIGURATION_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||||
|
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||||
|
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||||
|
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||||
|
PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
|
||||||
|
PODS_ROOT = ${SRCROOT}/Pods
|
||||||
|
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||||
|
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||||
|
VALID_ARCHS = arm64 x86_64
|
||||||
@ -2,10 +2,10 @@ ARCHS = $(ARCHS_STANDARD)
|
|||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO
|
ENABLE_USER_SCRIPT_SANDBOXING = NO
|
||||||
EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64
|
EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64
|
||||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_ROOT}/AMap3DMap-NO-IDFA" "${PODS_ROOT}/AMapFoundation-NO-IDFA" "${PODS_ROOT}/AMapLocation-NO-IDFA" "${PODS_ROOT}/AMapSearch-NO-IDFA" "${PODS_ROOT}/UMAPM/UMAPM_2.0.7" "${PODS_ROOT}/UMCommon/UMCommon_7.5.11" "${PODS_ROOT}/UMDevice/UMDevice_3.6.0" "${PODS_ROOT}/WechatOpenSDK-XCFramework" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMAPM" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMCommon" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMDevice" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_ROOT}/AMap3DMap-NO-IDFA" "${PODS_ROOT}/AMapFoundation-NO-IDFA" "${PODS_ROOT}/AMapLocation-NO-IDFA" "${PODS_ROOT}/AMapSearch-NO-IDFA" "${PODS_ROOT}/JCore" "${PODS_ROOT}/JPush" "${PODS_ROOT}/UMAPM/UMAPM_2.0.7" "${PODS_ROOT}/UMCommon/UMCommon_7.5.11" "${PODS_ROOT}/UMDevice/UMDevice_3.6.0" "${PODS_ROOT}/WechatOpenSDK-XCFramework" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMAPM" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMCommon" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMDevice" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers"
|
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush/Headers"
|
||||||
OTHER_LDFLAGS = $(inherited) -l"c++" -l"sqlite3" -l"sqlite3.0" -l"z" -framework "Combine" -framework "CoreGraphics" -framework "CoreLocation" -framework "CoreTelephony" -framework "CoreText" -framework "ExternalAccessory" -framework "GLKit" -framework "IQKeyboardCore" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "OpenGLES" -framework "QuartzCore" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "WebKit"
|
OTHER_LDFLAGS = $(inherited) -l"c++" -l"resolv" -l"sqlite3" -l"sqlite3.0" -l"z" -framework "CFNetwork" -framework "Combine" -framework "CoreFoundation" -framework "CoreGraphics" -framework "CoreLocation" -framework "CoreTelephony" -framework "CoreText" -framework "ExternalAccessory" -framework "Foundation" -framework "GLKit" -framework "IQKeyboardCore" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "OpenGLES" -framework "QuartzCore" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "WebKit" -weak_framework "Network" -weak_framework "UserNotifications"
|
||||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||||
PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
|
PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
|
||||||
|
|||||||
@ -2,10 +2,10 @@ ARCHS = $(ARCHS_STANDARD)
|
|||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO
|
ENABLE_USER_SCRIPT_SANDBOXING = NO
|
||||||
EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64
|
EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64
|
||||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_ROOT}/AMap3DMap-NO-IDFA" "${PODS_ROOT}/AMapFoundation-NO-IDFA" "${PODS_ROOT}/AMapLocation-NO-IDFA" "${PODS_ROOT}/AMapSearch-NO-IDFA" "${PODS_ROOT}/UMAPM/UMAPM_2.0.7" "${PODS_ROOT}/UMCommon/UMCommon_7.5.11" "${PODS_ROOT}/UMDevice/UMDevice_3.6.0" "${PODS_ROOT}/WechatOpenSDK-XCFramework" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMAPM" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMCommon" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMDevice" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_ROOT}/AMap3DMap-NO-IDFA" "${PODS_ROOT}/AMapFoundation-NO-IDFA" "${PODS_ROOT}/AMapLocation-NO-IDFA" "${PODS_ROOT}/AMapSearch-NO-IDFA" "${PODS_ROOT}/JCore" "${PODS_ROOT}/JPush" "${PODS_ROOT}/UMAPM/UMAPM_2.0.7" "${PODS_ROOT}/UMCommon/UMCommon_7.5.11" "${PODS_ROOT}/UMDevice/UMDevice_3.6.0" "${PODS_ROOT}/WechatOpenSDK-XCFramework" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMAPM" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMCommon" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMDevice" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers"
|
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush/Headers"
|
||||||
OTHER_LDFLAGS = $(inherited) -l"c++" -l"sqlite3" -l"sqlite3.0" -l"z" -framework "Combine" -framework "CoreGraphics" -framework "CoreLocation" -framework "CoreTelephony" -framework "CoreText" -framework "ExternalAccessory" -framework "GLKit" -framework "IQKeyboardCore" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "OpenGLES" -framework "QuartzCore" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "WebKit"
|
OTHER_LDFLAGS = $(inherited) -l"c++" -l"resolv" -l"sqlite3" -l"sqlite3.0" -l"z" -framework "CFNetwork" -framework "Combine" -framework "CoreFoundation" -framework "CoreGraphics" -framework "CoreLocation" -framework "CoreTelephony" -framework "CoreText" -framework "ExternalAccessory" -framework "Foundation" -framework "GLKit" -framework "IQKeyboardCore" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "OpenGLES" -framework "QuartzCore" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "WebKit" -weak_framework "Network" -weak_framework "UserNotifications"
|
||||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||||
PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
|
PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
|
||||||
|
|||||||
15
Pods/Target Support Files/Pods-suixinkanTests/Pods-suixinkanTests.simulatordebug.xcconfig
generated
Normal file
15
Pods/Target Support Files/Pods-suixinkanTests/Pods-suixinkanTests.simulatordebug.xcconfig
generated
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
ARCHS = $(ARCHS_STANDARD)
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO
|
||||||
|
EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64
|
||||||
|
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_ROOT}/JCore" "${PODS_ROOT}/JPush" "${PODS_ROOT}/UMAPM/UMAPM_2.0.7" "${PODS_ROOT}/UMCommon/UMCommon_7.5.11" "${PODS_ROOT}/UMDevice/UMDevice_3.6.0" "${PODS_ROOT}/WechatOpenSDK-XCFramework" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMAPM" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMCommon" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMDevice" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||||
|
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush/Headers"
|
||||||
|
OTHER_LDFLAGS = $(inherited) -l"c++" -l"resolv" -l"sqlite3" -l"sqlite3.0" -l"z" -framework "CFNetwork" -framework "Combine" -framework "CoreFoundation" -framework "CoreGraphics" -framework "CoreTelephony" -framework "Foundation" -framework "IQKeyboardCore" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "WebKit" -weak_framework "Network" -weak_framework "UserNotifications"
|
||||||
|
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||||
|
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||||
|
PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
|
||||||
|
PODS_ROOT = ${SRCROOT}/Pods
|
||||||
|
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||||
|
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||||
|
VALID_ARCHS = arm64 x86_64
|
||||||
484
docs/后端消息推送格式.md
Normal file
484
docs/后端消息推送格式.md
Normal file
@ -0,0 +1,484 @@
|
|||||||
|
# 后端消息推送格式
|
||||||
|
|
||||||
|
## 1. 文档说明
|
||||||
|
|
||||||
|
- 本文整理后端经极光推送发送到 Android 和 iOS 的通知消息格式。
|
||||||
|
- 当前内容基于截至 2026-07-23 的 Android、iOS 实际收包日志、两端客户端现有解析逻辑,以及后端开发人员提供的 `PushMsg` 类型定义。
|
||||||
|
- 后端当前定义了 `1`~`13`、`100`、`999` 共 15 个业务消息类型;目前已取得 `type = 1`(付款成功)、`type = 2`(退款成功)、`type = 6`(扫码成功)和 `type = 13`(新获客线索)的收包样本。
|
||||||
|
- 本文描述的是推送协议,不是普通 HTTP 接口响应格式。
|
||||||
|
|
||||||
|
## 2. 统一业务结构
|
||||||
|
|
||||||
|
极光在 Android 和 iOS 上交付的原始结构不同,但当前消息都可以归一化为以下业务结构:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"title": "通知标题",
|
||||||
|
"content": "通知正文",
|
||||||
|
"msg_id": 1858,
|
||||||
|
"type": 6,
|
||||||
|
"data": {
|
||||||
|
"order_type": 14
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.1 公共业务字段
|
||||||
|
|
||||||
|
| 字段 | 当前类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `title` | string | 通知标题。Android 为 `notificationTitle`,iOS 为 `aps.alert.title`。 |
|
||||||
|
| `content` | string | 通知正文。Android 为 `notificationContent`,iOS 为 `aps.alert.body`。 |
|
||||||
|
| `msg_id` | int | 后端业务消息 ID。它与极光消息 ID 不是同一个字段。 |
|
||||||
|
| `type` | int | 业务消息类型,是客户端业务分发的主要依据。 |
|
||||||
|
| `data` | object | 随消息类型变化的业务数据。 |
|
||||||
|
|
||||||
|
> “当前类型”表示现有样本中观察到的 JSON 类型。后端应保持字段类型稳定,避免同一个字段在不同推送中交替使用数字、字符串或 `null`。
|
||||||
|
|
||||||
|
## 3. Android 收包格式
|
||||||
|
|
||||||
|
Android 通过极光 `JPushMessageReceiver.onNotifyMessageArrived` 收到通知。标题、正文和附加字段分别由 SDK 提供,其中 `extras` 是 JSON 字符串:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[onNotifyMessageArrived] msgId=<极光消息ID>, title=<标题>, content=<正文>, extras=<业务JSON字符串>
|
||||||
|
```
|
||||||
|
|
||||||
|
`extras` 的当前结构为:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {},
|
||||||
|
"msg_id": 0,
|
||||||
|
"type": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.1 扫码成功原始日志
|
||||||
|
|
||||||
|
```text
|
||||||
|
[onNotifyMessageArrived] msgId=18103321643292031, title=扫码成功通知, content=扫码成功, extras={"data":{"order_type":14},"msg_id":1852,"type":6}
|
||||||
|
```
|
||||||
|
|
||||||
|
对应的 `extras`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"order_type": 14
|
||||||
|
},
|
||||||
|
"msg_id": 1852,
|
||||||
|
"type": 6
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 付款成功原始日志
|
||||||
|
|
||||||
|
```text
|
||||||
|
[onNotifyMessageArrived] msgId=18103321644290274, title=付款成功通知, content=您的订单 #260722128005 已支付成功,支付金额 ¥0.01, extras={"data":{"order_amount":"0.01","order_number":"260722128005","order_type":14,"pay_time":1784711036,"remark":""},"msg_id":1853,"type":1}
|
||||||
|
```
|
||||||
|
|
||||||
|
对应的 `extras`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"order_amount": "0.01",
|
||||||
|
"order_number": "260722128005",
|
||||||
|
"order_type": 14,
|
||||||
|
"pay_time": 1784711036,
|
||||||
|
"remark": ""
|
||||||
|
},
|
||||||
|
"msg_id": 1853,
|
||||||
|
"type": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 退款成功原始日志
|
||||||
|
|
||||||
|
```text
|
||||||
|
[onNotifyMessageArrived] msgId=18103321800527159, title=退款成功通知, content=您的订单 #260722128002 已成功退款,退款金额 ¥0.01, extras={"data":{},"msg_id":1867,"type":2}
|
||||||
|
```
|
||||||
|
|
||||||
|
对应的 `extras`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {},
|
||||||
|
"msg_id": 1867,
|
||||||
|
"type": 2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. iOS 收包格式
|
||||||
|
|
||||||
|
iOS 通过 APNs/极光收到 `userInfo` 字典。与 Android 相比:
|
||||||
|
|
||||||
|
- 标题和正文位于 `aps.alert`。
|
||||||
|
- Android `extras` 中的 `msg_id`、`type`、`data` 在 iOS 中位于 payload 顶层。
|
||||||
|
- `_j_*` 为极光传输层内部字段,不应作为业务分发依据。
|
||||||
|
|
||||||
|
通用结构如下:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"_j_business": 1,
|
||||||
|
"_j_data": "{\"data_msgtype\":1,\"push_type\":8,\"is_vip\":0}",
|
||||||
|
"_j_msgid": 18103321726881995,
|
||||||
|
"_j_uid": 86399144805,
|
||||||
|
"aps": {
|
||||||
|
"alert": {
|
||||||
|
"body": "通知正文",
|
||||||
|
"title": "通知标题"
|
||||||
|
},
|
||||||
|
"sound": "default"
|
||||||
|
},
|
||||||
|
"data": {},
|
||||||
|
"msg_id": 0,
|
||||||
|
"type": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 原始日志中该内部字段显示为 `*j\_data*`。本文按极光其他内部字段的命名方式记为 `_j_data`;后续联调时应以设备收到的真实字典 key 为准。该字段目前不参与业务解析。
|
||||||
|
|
||||||
|
### 4.1 扫码成功 payload
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"_j_business": 1,
|
||||||
|
"_j_data": "{\"data_msgtype\":1,\"push_type\":8,\"is_vip\":0}",
|
||||||
|
"_j_msgid": 18103321726881995,
|
||||||
|
"_j_uid": 86399144805,
|
||||||
|
"aps": {
|
||||||
|
"alert": {
|
||||||
|
"body": "扫码成功",
|
||||||
|
"title": "扫码成功通知"
|
||||||
|
},
|
||||||
|
"sound": "default"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"order_type": 14
|
||||||
|
},
|
||||||
|
"msg_id": 1858,
|
||||||
|
"type": 6
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 付款成功 payload
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"_j_business": 1,
|
||||||
|
"_j_data": "{\"data_msgtype\":1,\"push_type\":8,\"is_vip\":0}",
|
||||||
|
"_j_msgid": 18103321720587147,
|
||||||
|
"_j_uid": 86399144805,
|
||||||
|
"aps": {
|
||||||
|
"alert": {
|
||||||
|
"body": "您的订单 #260722128006 已支付成功,支付金额 ¥0.01",
|
||||||
|
"title": "付款成功通知"
|
||||||
|
},
|
||||||
|
"sound": "default"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"order_amount": "0.01",
|
||||||
|
"order_number": "260722128006",
|
||||||
|
"order_type": 14,
|
||||||
|
"pay_time": 1784712671,
|
||||||
|
"remark": ""
|
||||||
|
},
|
||||||
|
"msg_id": 1856,
|
||||||
|
"type": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 退款成功 payload
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"_j_business": 1,
|
||||||
|
"_j_data": "{\"data_msgtype\":1,\"push_type\":8,\"is_vip\":0}",
|
||||||
|
"_j_msgid": 18103321792603316,
|
||||||
|
"_j_uid": 86399144805,
|
||||||
|
"aps": {
|
||||||
|
"alert": {
|
||||||
|
"body": "您的订单 #260722128005 已成功退款,退款金额 ¥0.01",
|
||||||
|
"title": "退款成功通知"
|
||||||
|
},
|
||||||
|
"sound": "default"
|
||||||
|
},
|
||||||
|
"data": {},
|
||||||
|
"msg_id": 1862,
|
||||||
|
"type": 2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 新获客线索 payload
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"_j_business": 1,
|
||||||
|
"_j_data": "{\"data_msgtype\":1,\"push_type\":8,\"is_vip\":0}",
|
||||||
|
"_j_msgid": 18103323696468714,
|
||||||
|
"_j_uid": 86399144805,
|
||||||
|
"aps": {
|
||||||
|
"alert": {
|
||||||
|
"body": "获客员哦啦啦提交了一条新线索,请及时查看并跟进",
|
||||||
|
"title": "新获客线索通知"
|
||||||
|
},
|
||||||
|
"sound": "default"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"lead_id": 75,
|
||||||
|
"sale_user_id": 6,
|
||||||
|
"saler_name": "哦啦啦"
|
||||||
|
},
|
||||||
|
"msg_id": 1869,
|
||||||
|
"type": 13
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Android 与 iOS 字段对应关系
|
||||||
|
|
||||||
|
| 语义 | Android | iOS | 是否用于业务 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 极光消息 ID | SDK `message.msgId` | `_j_msgid` | 否,仅用于推送链路日志与排查 |
|
||||||
|
| 通知标题 | SDK `message.notificationTitle` | `aps.alert.title` | 是,用于展示 |
|
||||||
|
| 通知正文 | SDK `message.notificationContent` | `aps.alert.body` | 是,用于展示 |
|
||||||
|
| 后端业务消息 ID | `extras.msg_id` | 顶层 `msg_id` | 是 |
|
||||||
|
| 业务消息类型 | `extras.type` | 顶层 `type` | 是,作为业务分发主键 |
|
||||||
|
| 业务数据 | `extras.data` | 顶层 `data` | 是,结构由 `type` 决定 |
|
||||||
|
| 极光业务标记 | SDK 内部处理 | `_j_business` | 否 |
|
||||||
|
| 极光附加元数据 | SDK 内部处理 | `_j_data` | 否 |
|
||||||
|
| 极光用户 ID | SDK 内部处理 | `_j_uid` | 否 |
|
||||||
|
| APNs 展示配置 | 不适用 | `aps` | 仅 iOS 系统使用 |
|
||||||
|
|
||||||
|
### 5.1 两种消息 ID 不可混用
|
||||||
|
|
||||||
|
- `msgId` / `_j_msgid`:极光生成的传输层消息 ID,当前样本为 17 位数字。
|
||||||
|
- `msg_id`:后端生成的业务消息 ID,当前样本为 `1852`、`1853`、`1856`、`1858`。
|
||||||
|
- 客户端日志、去重或上报时必须明确需要哪一种 ID,不能仅用名称相近而互相替代。
|
||||||
|
- 极光消息 ID 已超过 JavaScript 可安全表示的整数范围;若需要经过 JavaScript、WebView 或其他只能安全处理 53 位整数的链路,应按字符串传递。
|
||||||
|
|
||||||
|
## 6. 消息类型清单
|
||||||
|
|
||||||
|
### 6.1 后端 `PushMsg` 完整类型定义
|
||||||
|
|
||||||
|
定义位置:后端 `PushMsg.php` 第 49 行附近。
|
||||||
|
|
||||||
|
| `type` | 消息类型 | 后端备注 | 当前收包样本 |
|
||||||
|
| ---: | --- | --- | --- |
|
||||||
|
| `1` | 付款成功通知 | 上文实际示例即为此类型 | Android、iOS 均已取得 |
|
||||||
|
| `2` | 退款成功通知 | — | Android、iOS 均已取得 |
|
||||||
|
| `3` | 下单成功通知 | App 无需推送,当前未使用 | 暂无 |
|
||||||
|
| `4` | 签到成功通知 | — | 暂无 |
|
||||||
|
| `5` | 付尾款成功通知 | — | 暂无 |
|
||||||
|
| `6` | 扫码成功通知 | — | Android、iOS 均已取得 |
|
||||||
|
| `7` | 付定金成功通知 | — | 暂无 |
|
||||||
|
| `8` | 订单核销成功通知 | — | 暂无 |
|
||||||
|
| `9` | 剪辑完成通知 | — | 暂无 |
|
||||||
|
| `10` | 修图完成通知 | — | 暂无 |
|
||||||
|
| `11` | 跟拍签到通知 | — | 暂无 |
|
||||||
|
| `12` | 剪辑师新任务通知 | 类型名称映射中漏配 | 暂无 |
|
||||||
|
| `13` | 新获客线索通知 | 获客员提交新线索 | iOS 已取得,Android 暂无 |
|
||||||
|
| `100` | 用户登出通知 | 账号在其他设备登录 | 暂无 |
|
||||||
|
| `999` | 系统消息 | 当前未使用 | 暂无 |
|
||||||
|
|
||||||
|
后端特别说明:
|
||||||
|
|
||||||
|
- `type` 表示业务消息类型,不代表推送渠道。
|
||||||
|
- 推送渠道的取值另有定义:`1` 为极光推送,`2` 为微信小程序,`999` 为不推送。
|
||||||
|
- `type = 12` 虽然已经定义,但尚未加入后端 `MAP_TYPE_NAMES`。后端调用 `getTypeName(12)` 时会返回“未知消息类型”,需要后端补充映射。
|
||||||
|
- “暂无收包样本”只表示当前文档还没有该类型的完整 payload,不能据此认为该类型未启用。收到后应继续补充其标题、正文、`data` 字段和点击行为。
|
||||||
|
|
||||||
|
### 6.2 `type = 1`:付款成功
|
||||||
|
|
||||||
|
| 项目 | 当前值 |
|
||||||
|
| --- | --- |
|
||||||
|
| 标题示例 | `付款成功通知` |
|
||||||
|
| 正文示例 | `您的订单 #260722128006 已支付成功,支付金额 ¥0.01` |
|
||||||
|
| 客户端点击行为 | Android、iOS 均进入收款记录页 |
|
||||||
|
|
||||||
|
`data` 字段:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 当前样本 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `order_amount` | string | `"0.01"` | 支付金额。使用十进制字符串,避免浮点精度问题。 |
|
||||||
|
| `order_number` | string | `"260722128006"` | 业务订单号。 |
|
||||||
|
| `order_type` | int | `14` | 订单类型;当前值表示“摄影师跟拍线下扫码”。 |
|
||||||
|
| `pay_time` | int | `1784712671` | 支付时间。当前样本为 Unix 秒级时间戳;Android 现有代码同时兼容秒和毫秒。 |
|
||||||
|
| `remark` | string | `""` | 付款备注;没有备注时当前返回空字符串。 |
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"msg_id": 1856,
|
||||||
|
"data": {
|
||||||
|
"order_amount": "0.01",
|
||||||
|
"order_number": "260722128006",
|
||||||
|
"order_type": 14,
|
||||||
|
"pay_time": 1784712671,
|
||||||
|
"remark": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 `type = 2`:退款成功
|
||||||
|
|
||||||
|
| 项目 | 当前值 |
|
||||||
|
| --- | --- |
|
||||||
|
| 标题示例 | `退款成功通知` |
|
||||||
|
| 正文示例 | `您的订单 #260722128005 已成功退款,退款金额 ¥0.01` |
|
||||||
|
| 客户端点击行为 | Android、iOS 均进入消息中心 |
|
||||||
|
|
||||||
|
`data` 字段:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 当前样本 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| — | object | `{}` | 当前 Android、iOS 样本均为空对象。 |
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": 2,
|
||||||
|
"msg_id": 1862,
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 当前订单号和退款金额只存在于展示正文中,没有以结构化字段放入 `data`。客户端如果需要可靠地刷新指定订单、展示退款详情或执行金额计算,应由后端在 `data` 中提供字段,不应解析自然语言正文。
|
||||||
|
|
||||||
|
### 6.4 `type = 6`:扫码成功
|
||||||
|
|
||||||
|
| 项目 | 当前值 |
|
||||||
|
| --- | --- |
|
||||||
|
| 标题示例 | `扫码成功通知` |
|
||||||
|
| 正文示例 | `扫码成功` |
|
||||||
|
| 客户端点击行为 | Android、iOS 均进入收款详情页 |
|
||||||
|
|
||||||
|
`data` 字段:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 当前样本 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `order_type` | int | `14` | 订单类型;`14` 在 Android 现有代码中表示“摄影师跟拍线下扫码”。 |
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": 6,
|
||||||
|
"msg_id": 1858,
|
||||||
|
"data": {
|
||||||
|
"order_type": 14
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.5 `type = 13`:新获客线索
|
||||||
|
|
||||||
|
| 项目 | 当前值 |
|
||||||
|
| --- | --- |
|
||||||
|
| 标题示例 | `新获客线索通知` |
|
||||||
|
| 正文示例 | `获客员哦啦啦提交了一条新线索,请及时查看并跟进` |
|
||||||
|
| 客户端点击行为 | Android、iOS 均进入消息中心 |
|
||||||
|
|
||||||
|
`data` 字段:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 当前样本 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `lead_id` | int | `75` | 新增获客线索的唯一 ID。 |
|
||||||
|
| `sale_user_id` | int | `6` | 提交线索的获客员 ID。 |
|
||||||
|
| `saler_name` | string | `"哦啦啦"` | 获客员名称,与通知正文中的名称一致。 |
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": 13,
|
||||||
|
"msg_id": 1869,
|
||||||
|
"data": {
|
||||||
|
"lead_id": 75,
|
||||||
|
"sale_user_id": 6,
|
||||||
|
"saler_name": "哦啦啦"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 当前点击路由只根据 `type` 判断,`type = 13` 属于默认分支,因此进入消息中心;`data` 中的线索字段不会改变跳转页面。
|
||||||
|
|
||||||
|
## 7. 客户端解析约定
|
||||||
|
|
||||||
|
1. Android、iOS 的通知点击路由只根据业务字段 `type` 判断,不读取标题、正文、`data`、`route`、`uri`、`action` 或极光内部字段决定页面。
|
||||||
|
2. `type = 1`:进入收款记录页。
|
||||||
|
3. `type = 6`:进入收款详情页。
|
||||||
|
4. 其他任意 `type`、缺少 `type`、类型无法解析或 payload 损坏:进入消息中心。
|
||||||
|
5. Android 从 `notificationExtras` 或厂商通道包装字段中提取 `type`;iOS 优先读取 payload 顶层 `type`,并兼容常见推送包装层中的 `type`。
|
||||||
|
6. `data` 仍可按消息类型用于页面内容刷新,但不参与通知点击路由。新增字段应保持向后兼容,客户端应忽略当前版本不认识的额外字段。
|
||||||
|
7. 金额继续使用字符串;时间戳建议统一为 Unix 秒级整数,并在协议中固定单位。
|
||||||
|
8. `remark` 等可空业务字段建议固定返回空字符串或明确约定 `null`,不要在多种表示之间切换。
|
||||||
|
|
||||||
|
## 8. 新增消息类型记录模板
|
||||||
|
|
||||||
|
后续收到新类型时,复制以下内容追加到“消息类型清单”:
|
||||||
|
|
||||||
|
````markdown
|
||||||
|
### 6.x `type = <类型值>`:<消息名称>
|
||||||
|
|
||||||
|
| 项目 | 当前值 |
|
||||||
|
| --- | --- |
|
||||||
|
| 首次发现日期 | `YYYY-MM-DD` |
|
||||||
|
| 标题示例 | `<title>` |
|
||||||
|
| 正文示例 | `<content>` |
|
||||||
|
| 客户端业务行为 | `<展示、刷新或跳转行为>` |
|
||||||
|
|
||||||
|
`data` 字段:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必有 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `<field>` | `<type>` | `<是/否/待确认>` | `<说明>` |
|
||||||
|
|
||||||
|
Android `extras` 示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {},
|
||||||
|
"msg_id": 0,
|
||||||
|
"type": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
iOS 业务字段示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {},
|
||||||
|
"msg_id": 0,
|
||||||
|
"type": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
````
|
||||||
|
|
||||||
|
记录新类型时至少保留以下信息:
|
||||||
|
|
||||||
|
- Android 和 iOS 各一份原始收包日志。
|
||||||
|
- `type`、`msg_id` 和完整 `data`。
|
||||||
|
- 标题、正文以及消息点击后的期望页面。
|
||||||
|
- 每个字段的 JSON 类型、是否必有、空值表示方式和时间/金额单位。
|
||||||
|
- 同一业务事件在两端的字段是否一致。
|
||||||
|
|
||||||
|
## 9. 待后端确认项
|
||||||
|
|
||||||
|
| 项目 | 当前观察 | 需要确认 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `msg_id` | 与极光消息 ID 不同 | 是否对应消息中心记录 ID,以及是否用于已读/送达状态上报 |
|
||||||
|
| `data` | `type = 1`、`2`、`6`、`13` 的样本中都存在;`type = 2` 为 `{}` | 所有业务推送是否保证存在;无业务字段时是否统一返回 `{}` |
|
||||||
|
| 退款成功的 `data` | 当前为空对象,订单号和退款金额只在正文中 | 是否补充 `order_number`、`refund_amount`、`refund_time`、`order_type` 等结构化字段 |
|
||||||
|
| 其他类型的 `data` | 后端已定义消息名称,但尚未提供 payload | 分别确认 `type = 3`~`5`、`7`~`12`、`100`、`999` 的字段结构、必填项及点击行为 |
|
||||||
|
| `pay_time` | 当前为秒级时间戳 | 后端是否统一保证使用 Unix 秒,且时区语义固定为 UTC 时间戳 |
|
||||||
|
| `remark` | 无备注时为空字符串 | 是否可能返回 `null` 或省略 |
|
||||||
|
| `order_type` | 当前为 `14` | 完整枚举和值含义由哪份接口文档维护 |
|
||||||
|
| 标题与正文 | 当前均存在 | 是否为所有通知消息的必有字段 |
|
||||||
|
| 推送渠道字段 | 后端已给出渠道取值 `1`、`2`、`999` | payload 或发送接口中对应字段的准确名称 |
|
||||||
|
| `type = 12` | `MAP_TYPE_NAMES` 中漏配 | 后端补充映射,避免 `getTypeName(12)` 返回“未知消息类型” |
|
||||||
|
| 新增 `type` | 当前定义维护在 `PushMsg.php` | 后端新增类型前是否可同步类型值、字段结构和客户端行为 |
|
||||||
204
docs/消息中心未读数量与全部已读接口需求.md
Normal file
204
docs/消息中心未读数量与全部已读接口需求.md
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
# 消息中心未读数量与全部已读接口需求
|
||||||
|
|
||||||
|
## 1. 需求背景
|
||||||
|
|
||||||
|
iOS App 需要实现以下功能:
|
||||||
|
|
||||||
|
1. 首页展示消息中心入口,有未读消息时显示红点。
|
||||||
|
2. 获取最新未读消息数量后,同步更新 App 桌面图标右上角的角标数量。
|
||||||
|
3. 用户点击某条未读消息并成功标记已读后,立即更新最新未读数量和桌面角标。
|
||||||
|
4. 消息中心导航栏右上角提供“全部已读”按钮。
|
||||||
|
5. 全部标记已读成功后,更新首页红点,并将桌面角标更新为服务端返回的最新未读数量。
|
||||||
|
|
||||||
|
## 2. 当前已有接口
|
||||||
|
|
||||||
|
### 2.1 获取消息列表
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/app/msg/list
|
||||||
|
```
|
||||||
|
|
||||||
|
当前参数:
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `last_id` | int | 否 | 分页游标,首次请求传 `0` |
|
||||||
|
| `limit` | int | 否 | 每页数量 |
|
||||||
|
| `unread` | int | 否 | `0` 获取全部消息,`1` 只获取未读消息 |
|
||||||
|
|
||||||
|
该接口仅返回分页消息,没有返回未读消息总数。客户端若通过分页遍历全部未读消息来统计数量,请求次数和数据流量都会随着未读消息数量增长,因此不适合作为桌面角标的常规数据来源。
|
||||||
|
|
||||||
|
### 2.2 标记单条消息已读
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/app/msg/read?id={message_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
当前接口可以标记单条消息已读,但成功响应没有返回操作后的最新未读数量。
|
||||||
|
|
||||||
|
## 3. 需要新增或调整的接口
|
||||||
|
|
||||||
|
### 3.1 获取未读消息总数
|
||||||
|
|
||||||
|
建议新增:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/app/msg/unread-count
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 请求说明
|
||||||
|
|
||||||
|
- 不需要业务请求参数。
|
||||||
|
- 根据当前登录 Token 识别用户及业务账号。
|
||||||
|
- 返回当前账号下尚未读取的消息总数。
|
||||||
|
|
||||||
|
#### 成功响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 100000,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"unread_count": 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必有 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `unread_count` | int | 是 | 当前账号的最新未读消息数量,最小值为 `0` |
|
||||||
|
|
||||||
|
### 3.2 全部标记为已读
|
||||||
|
|
||||||
|
建议新增:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/app/msg/read-all
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 请求说明
|
||||||
|
|
||||||
|
- 不需要业务请求参数和请求体。
|
||||||
|
- 根据当前登录 Token 识别用户及业务账号。
|
||||||
|
- 将当前账号下请求执行时已经存在的所有未读消息标记为已读。
|
||||||
|
- 接口需要支持幂等调用;没有未读消息时仍返回成功。
|
||||||
|
|
||||||
|
#### 成功响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 100000,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"updated_count": 12,
|
||||||
|
"unread_count": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必有 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `updated_count` | int | 是 | 本次实际被更新为已读的消息数量 |
|
||||||
|
| `unread_count` | int | 是 | 操作完成后的最新未读消息数量 |
|
||||||
|
|
||||||
|
> 客户端应以响应中的 `unread_count` 为准,不应直接假定其一定为 `0`。例如“全部已读”执行期间如果有新消息到达,操作完成后仍可能存在新的未读消息。
|
||||||
|
|
||||||
|
### 3.3 调整单条已读接口响应
|
||||||
|
|
||||||
|
保留现有接口:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/app/msg/read?id={message_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
建议成功响应增加最新未读数量:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 100000,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"unread_count": 11
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必有 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `unread_count` | int | 是 | 单条消息标记已读后的最新未读消息数量 |
|
||||||
|
|
||||||
|
#### 幂等要求
|
||||||
|
|
||||||
|
- 对已经处于已读状态的消息重复调用时,接口仍返回成功和最新 `unread_count`。
|
||||||
|
- 消息不存在或不属于当前账号时,返回明确的业务错误,不能修改其他账号的消息。
|
||||||
|
|
||||||
|
## 4. 统一约定
|
||||||
|
|
||||||
|
### 4.1 账号范围
|
||||||
|
|
||||||
|
- 所有接口必须根据登录 Token 限定当前用户及当前业务账号的数据范围。
|
||||||
|
- 景区账号、门店账号等不同业务账号之间的未读数量不能互相污染。
|
||||||
|
- 切换账号后,客户端会重新调用未读数量接口。
|
||||||
|
|
||||||
|
### 4.2 数值约束
|
||||||
|
|
||||||
|
- `unread_count` 和 `updated_count` 必须返回 JSON 整数。
|
||||||
|
- 数值不得为负数。
|
||||||
|
- 即使数量为 `0`,字段也必须存在,不能返回 `null`、空字符串或省略字段。
|
||||||
|
|
||||||
|
### 4.3 一致性与并发
|
||||||
|
|
||||||
|
- 单条已读或全部已读操作及其响应中的 `unread_count` 应基于同一账号范围。
|
||||||
|
- `read-all` 应尽量在事务内完成批量更新和剩余未读数统计。
|
||||||
|
- 如果操作期间有新消息写入,响应应返回操作完成时数据库中的真实 `unread_count`。
|
||||||
|
- 客户端始终以最近一次成功接口响应中的 `unread_count` 更新首页红点和桌面角标。
|
||||||
|
|
||||||
|
### 4.4 错误响应
|
||||||
|
|
||||||
|
沿用现有统一业务响应结构,例如:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 100001,
|
||||||
|
"msg": "消息不存在或无权操作",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
建议至少区分以下情况:
|
||||||
|
|
||||||
|
- Token 无效或登录已过期。
|
||||||
|
- 单条消息不存在。
|
||||||
|
- 单条消息不属于当前账号。
|
||||||
|
- 数据库更新失败。
|
||||||
|
|
||||||
|
## 5. 客户端调用时机
|
||||||
|
|
||||||
|
| 场景 | 调用接口 | 客户端处理 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 登录后首次进入首页 | `GET /msg/unread-count` | 更新首页红点和桌面角标 |
|
||||||
|
| App 回到前台 | `GET /msg/unread-count` | 校准最新未读数量 |
|
||||||
|
| 前台收到新消息推送 | `GET /msg/unread-count` | 更新首页红点和桌面角标 |
|
||||||
|
| 点击一条未读消息 | `POST /msg/read` | 使用返回的 `unread_count` 更新红点和桌面角标 |
|
||||||
|
| 点击“全部已读” | `POST /msg/read-all` | 刷新消息列表,使用返回的 `unread_count` 更新红点和桌面角标 |
|
||||||
|
| 切换业务账号 | `GET /msg/unread-count` | 使用新账号的数量覆盖旧账号角标 |
|
||||||
|
| 退出登录 | 无 | 客户端将桌面角标清零 |
|
||||||
|
|
||||||
|
## 6. 验收标准
|
||||||
|
|
||||||
|
1. 未读数量为 `0` 时,接口稳定返回 `{"unread_count": 0}`。
|
||||||
|
2. 存在多页未读消息时,未读数量接口返回完整总数,而不是当前页数量。
|
||||||
|
3. 单条未读消息标记成功后,返回的 `unread_count` 与数据库实际剩余未读数一致。
|
||||||
|
4. 对同一消息重复调用单条已读接口不会重复扣减未读数量。
|
||||||
|
5. 全部已读接口可一次处理当前账号的所有未读消息,并返回实际更新数量。
|
||||||
|
6. 重复调用全部已读接口仍成功,返回 `updated_count = 0`。
|
||||||
|
7. 不同业务账号的未读数量相互隔离。
|
||||||
|
8. 并发到达新消息时,响应中的 `unread_count` 能反映操作完成时的真实剩余数量。
|
||||||
|
9. 所有接口继续使用项目现有统一鉴权、业务状态码和响应信封格式。
|
||||||
|
|
||||||
@ -36,6 +36,8 @@
|
|||||||
93FF17C52FFB84950033C304 /* suixinkan.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = suixinkan.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
93FF17C52FFB84950033C304 /* suixinkan.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = suixinkan.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
A1B2C3D02FFB84950033C305 /* suixinkanTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = suixinkanTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
A1B2C3D02FFB84950033C305 /* suixinkanTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = suixinkanTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
C69C08A144C2D5D4A448B7D2 /* Pods_suixinkan.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_suixinkan.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
C69C08A144C2D5D4A448B7D2 /* Pods_suixinkan.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_suixinkan.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
DC152A95F1705B826BE8A56A /* Pods-suixinkanTests.simulatordebug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-suixinkanTests.simulatordebug.xcconfig"; path = "Target Support Files/Pods-suixinkanTests/Pods-suixinkanTests.simulatordebug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
ECE6D74459339B5075CF14AD /* Pods-suixinkan.simulatordebug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-suixinkan.simulatordebug.xcconfig"; path = "Target Support Files/Pods-suixinkan/Pods-suixinkan.simulatordebug.xcconfig"; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
@ -59,11 +61,15 @@
|
|||||||
};
|
};
|
||||||
9A01B8E22FFB84950033C402 /* Vendor */ = {
|
9A01B8E22FFB84950033C402 /* Vendor */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
exceptions = (
|
||||||
|
);
|
||||||
path = Vendor;
|
path = Vendor;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
A1B2C3D42FFB84950033C305 /* suixinkanTests */ = {
|
A1B2C3D42FFB84950033C305 /* suixinkanTests */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
exceptions = (
|
||||||
|
);
|
||||||
path = suixinkanTests;
|
path = suixinkanTests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@ -111,6 +117,8 @@
|
|||||||
1060D5E389506965D83EA1CC /* Pods-suixinkan.release.xcconfig */,
|
1060D5E389506965D83EA1CC /* Pods-suixinkan.release.xcconfig */,
|
||||||
83445E7B4CCCF42C6FD481D3 /* Pods-suixinkanTests.debug.xcconfig */,
|
83445E7B4CCCF42C6FD481D3 /* Pods-suixinkanTests.debug.xcconfig */,
|
||||||
6CB3E1DA4E97AA4961B25733 /* Pods-suixinkanTests.release.xcconfig */,
|
6CB3E1DA4E97AA4961B25733 /* Pods-suixinkanTests.release.xcconfig */,
|
||||||
|
ECE6D74459339B5075CF14AD /* Pods-suixinkan.simulatordebug.xcconfig */,
|
||||||
|
DC152A95F1705B826BE8A56A /* Pods-suixinkanTests.simulatordebug.xcconfig */,
|
||||||
);
|
);
|
||||||
path = Pods;
|
path = Pods;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -208,6 +216,11 @@
|
|||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
93FF17C42FFB84950033C304 = {
|
93FF17C42FFB84950033C304 = {
|
||||||
CreatedOnToolsVersion = 26.6;
|
CreatedOnToolsVersion = 26.6;
|
||||||
|
SystemCapabilities = {
|
||||||
|
com.apple.Push = {
|
||||||
|
enabled = 1;
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
A1B2C3D32FFB84950033C305 = {
|
A1B2C3D32FFB84950033C305 = {
|
||||||
CreatedOnToolsVersion = 26.6;
|
CreatedOnToolsVersion = 26.6;
|
||||||
@ -356,14 +369,10 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
inputPaths = (
|
|
||||||
);
|
|
||||||
name = "[CP] Embed Pods Frameworks";
|
name = "[CP] Embed Pods Frameworks";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
outputPaths = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-frameworks.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-frameworks.sh\"\n";
|
||||||
@ -377,14 +386,10 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-resources-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
inputPaths = (
|
|
||||||
);
|
|
||||||
name = "[CP] Copy Pods Resources";
|
name = "[CP] Copy Pods Resources";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-resources-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
outputPaths = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-resources.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-resources.sh\"\n";
|
||||||
@ -422,6 +427,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 5BBB56C2281879245220A9F3 /* Pods-suixinkan.debug.xcconfig */;
|
baseConfigurationReference = 5BBB56C2281879245220A9F3 /* Pods-suixinkan.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
APS_ENVIRONMENT = development;
|
||||||
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 = suixinkan/suixinkan.entitlements;
|
CODE_SIGN_ENTITLEMENTS = suixinkan/suixinkan.entitlements;
|
||||||
@ -487,6 +493,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 1060D5E389506965D83EA1CC /* Pods-suixinkan.release.xcconfig */;
|
baseConfigurationReference = 1060D5E389506965D83EA1CC /* Pods-suixinkan.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
APS_ENVIRONMENT = production;
|
||||||
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 = suixinkan/suixinkan.entitlements;
|
CODE_SIGN_ENTITLEMENTS = suixinkan/suixinkan.entitlements;
|
||||||
@ -721,6 +728,150 @@
|
|||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
|
D1A2B3C42FFB84950033C401 /* SimulatorDebug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = ECE6D74459339B5075CF14AD /* Pods-suixinkan.simulatordebug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
APS_ENVIRONMENT = development;
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = suixinkan/suixinkan.entitlements;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1010402;
|
||||||
|
DEVELOPMENT_TEAM = 56GVN5RNVN;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = suixinkan/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = "随心瞰商家版";
|
||||||
|
INFOPLIST_KEY_NSCameraUsageDescription = "需要访问相机以拍摄任务素材、扫描订单核销二维码,并连接有线相机进行旅拍相册传输";
|
||||||
|
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "需要获取您的位置信息,用于位置上报、景区距离排序及多点位核销打卡";
|
||||||
|
INFOPLIST_KEY_NSMicrophoneUsageDescription = "需要访问麦克风以在拍摄素材视频时录制声音";
|
||||||
|
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "需要保存收款二维码、排队打卡点小程序码和云盘图片视频到相册";
|
||||||
|
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "需要访问相册以选择头像、身份证、银行卡照片和云盘上传素材";
|
||||||
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
|
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 16;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 1.1.4;
|
||||||
|
OTHER_LDFLAGS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"-ObjC",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.yuanzhixiang.suixinkan;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = iphonesimulator;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
SUPPORTED_PLATFORMS = iphonesimulator;
|
||||||
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_DEFAULT_ACTOR_ISOLATION = nonisolated;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "suixinkan/suixinkan-Bridging-Header.h";
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
|
};
|
||||||
|
name = SimulatorDebug;
|
||||||
|
};
|
||||||
|
D1A2B3C52FFB84950033C402 /* SimulatorDebug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
DEVELOPMENT_TEAM = 56GVN5RNVN;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_TESTABILITY = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = 0;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"DEBUG=1",
|
||||||
|
"$(inherited)",
|
||||||
|
);
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 26.5;
|
||||||
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
SDKROOT = iphonesimulator;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
};
|
||||||
|
name = SimulatorDebug;
|
||||||
|
};
|
||||||
|
D1A2B3C62FFB84950033C403 /* SimulatorDebug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = DC152A95F1705B826BE8A56A /* Pods-suixinkanTests.simulatordebug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = 56GVN5RNVN;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 16;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.yuanzhixiang.suixinkanTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = iphonesimulator;
|
||||||
|
SUPPORTED_PLATFORMS = iphonesimulator;
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_DEFAULT_ACTOR_ISOLATION = nonisolated;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/suixinkan.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/suixinkan";
|
||||||
|
};
|
||||||
|
name = SimulatorDebug;
|
||||||
|
};
|
||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
@ -728,6 +879,7 @@
|
|||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
93FF17DB2FFB84950033C304 /* Debug */,
|
93FF17DB2FFB84950033C304 /* Debug */,
|
||||||
|
D1A2B3C52FFB84950033C402 /* SimulatorDebug */,
|
||||||
93FF17DC2FFB84950033C304 /* Release */,
|
93FF17DC2FFB84950033C304 /* Release */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
@ -737,6 +889,7 @@
|
|||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
93FF17D92FFB84950033C304 /* Debug */,
|
93FF17D92FFB84950033C304 /* Debug */,
|
||||||
|
D1A2B3C42FFB84950033C401 /* SimulatorDebug */,
|
||||||
93FF17DA2FFB84950033C304 /* Release */,
|
93FF17DA2FFB84950033C304 /* Release */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
@ -746,6 +899,7 @@
|
|||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
A1B2C3DC2FFB84950033C305 /* Debug */,
|
A1B2C3DC2FFB84950033C305 /* Debug */,
|
||||||
|
D1A2B3C62FFB84950033C403 /* SimulatorDebug */,
|
||||||
A1B2C3DA2FFB84950033C305 /* Release */,
|
A1B2C3DA2FFB84950033C305 /* Release */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
|
|||||||
@ -0,0 +1,100 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "2660"
|
||||||
|
version = "1.7">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "NO"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "93FF17C42FFB84950033C304"
|
||||||
|
BuildableName = "suixinkan.app"
|
||||||
|
BlueprintName = "suixinkan"
|
||||||
|
ReferencedContainer = "container:suixinkan.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "SimulatorDebug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "93FF17C42FFB84950033C304"
|
||||||
|
BuildableName = "suixinkan.app"
|
||||||
|
BlueprintName = "suixinkan"
|
||||||
|
ReferencedContainer = "container:suixinkan.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO"
|
||||||
|
parallelizable = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "A1B2C3D32FFB84950033C305"
|
||||||
|
BuildableName = "suixinkanTests.xctest"
|
||||||
|
BlueprintName = "suixinkanTests"
|
||||||
|
ReferencedContainer = "container:suixinkan.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "SimulatorDebug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "93FF17C42FFB84950033C304"
|
||||||
|
BuildableName = "suixinkan.app"
|
||||||
|
BlueprintName = "suixinkan"
|
||||||
|
ReferencedContainer = "container:suixinkan.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "SimulatorDebug"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "93FF17C42FFB84950033C304"
|
||||||
|
BuildableName = "suixinkan.app"
|
||||||
|
BlueprintName = "suixinkan"
|
||||||
|
ReferencedContainer = "container:suixinkan.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "SimulatorDebug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@ -29,6 +29,7 @@ final class NetworkServices {
|
|||||||
let liveAPI: LiveAPI
|
let liveAPI: LiveAPI
|
||||||
let cloudDriveAPI: CloudDriveAPI
|
let cloudDriveAPI: CloudDriveAPI
|
||||||
let messageCenterAPI: MessageCenterAPI
|
let messageCenterAPI: MessageCenterAPI
|
||||||
|
let pushAPI: PushAPI
|
||||||
let wildPhotographerReportAPI: WildPhotographerReportAPI
|
let wildPhotographerReportAPI: WildPhotographerReportAPI
|
||||||
let uploadAPI: UploadAPI
|
let uploadAPI: UploadAPI
|
||||||
let ossUploadService: OSSUploadService
|
let ossUploadService: OSSUploadService
|
||||||
@ -54,6 +55,7 @@ final class NetworkServices {
|
|||||||
liveAPI = LiveAPI(client: client)
|
liveAPI = LiveAPI(client: client)
|
||||||
cloudDriveAPI = CloudDriveAPI(client: client)
|
cloudDriveAPI = CloudDriveAPI(client: client)
|
||||||
messageCenterAPI = MessageCenterAPI(client: client)
|
messageCenterAPI = MessageCenterAPI(client: client)
|
||||||
|
pushAPI = PushAPI(client: client)
|
||||||
wildPhotographerReportAPI = WildPhotographerReportAPI(client: client)
|
wildPhotographerReportAPI = WildPhotographerReportAPI(client: client)
|
||||||
uploadAPI = UploadAPI(client: client)
|
uploadAPI = UploadAPI(client: client)
|
||||||
ossUploadService = OSSUploadService(configService: uploadAPI)
|
ossUploadService = OSSUploadService(configService: uploadAPI)
|
||||||
@ -84,6 +86,7 @@ final class NetworkServices {
|
|||||||
liveAPI = LiveAPI(client: apiClient)
|
liveAPI = LiveAPI(client: apiClient)
|
||||||
cloudDriveAPI = CloudDriveAPI(client: apiClient)
|
cloudDriveAPI = CloudDriveAPI(client: apiClient)
|
||||||
messageCenterAPI = MessageCenterAPI(client: apiClient)
|
messageCenterAPI = MessageCenterAPI(client: apiClient)
|
||||||
|
pushAPI = PushAPI(client: apiClient)
|
||||||
wildPhotographerReportAPI = WildPhotographerReportAPI(client: apiClient)
|
wildPhotographerReportAPI = WildPhotographerReportAPI(client: apiClient)
|
||||||
uploadAPI = UploadAPI(client: apiClient)
|
uploadAPI = UploadAPI(client: apiClient)
|
||||||
ossUploadService = OSSUploadService(configService: uploadAPI)
|
ossUploadService = OSSUploadService(configService: uploadAPI)
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
import IQKeyboardManagerSwift
|
import IQKeyboardManagerSwift
|
||||||
import IQKeyboardToolbarManager
|
import IQKeyboardToolbarManager
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import UserNotifications
|
||||||
|
|
||||||
@main
|
@main
|
||||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
@ -18,9 +19,36 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
if AppStore.shared.session.privacyAgreementAccepted, !AppStore.shared.session.token.isEmpty {
|
if AppStore.shared.session.privacyAgreementAccepted, !AppStore.shared.session.token.isEmpty {
|
||||||
AMapBootstrap.configureIfNeeded()
|
AMapBootstrap.configureIfNeeded()
|
||||||
}
|
}
|
||||||
|
PushNotificationManager.shared.initializeIfPrivacyAccepted(launchOptions: launchOptions)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// APNs 注册成功后将 Device Token 交给极光。
|
||||||
|
func application(
|
||||||
|
_ application: UIApplication,
|
||||||
|
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
|
||||||
|
) {
|
||||||
|
PushNotificationManager.shared.handleDeviceToken(deviceToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// APNs 注册失败不阻断应用启动,仅交由推送管理器记录。
|
||||||
|
func application(
|
||||||
|
_ application: UIApplication,
|
||||||
|
didFailToRegisterForRemoteNotificationsWithError error: Error
|
||||||
|
) {
|
||||||
|
PushNotificationManager.shared.handleRegistrationError(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 处理后台静默通知并完成极光到达统计。
|
||||||
|
func application(
|
||||||
|
_ application: UIApplication,
|
||||||
|
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
|
||||||
|
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
|
||||||
|
) {
|
||||||
|
PushNotificationManager.shared.handleRemoteNotification(userInfo)
|
||||||
|
completionHandler(.newData)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: UISceneSession Lifecycle
|
// MARK: UISceneSession Lifecycle
|
||||||
|
|
||||||
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||||
|
|||||||
6
suixinkan/Assets.xcassets/Payment/Contents.json
Normal file
6
suixinkan/Assets.xcassets/Payment/Contents.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
21
suixinkan/Assets.xcassets/Payment/payment_nalati_background.imageset/Contents.json
vendored
Normal file
21
suixinkan/Assets.xcassets/Payment/payment_nalati_background.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "payment_nalati_background.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
suixinkan/Assets.xcassets/Payment/payment_nalati_background.imageset/payment_nalati_background.png
vendored
Normal file
BIN
suixinkan/Assets.xcassets/Payment/payment_nalati_background.imageset/payment_nalati_background.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
21
suixinkan/Assets.xcassets/Payment/payment_nalati_logo.imageset/Contents.json
vendored
Normal file
21
suixinkan/Assets.xcassets/Payment/payment_nalati_logo.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "payment_nalati_logo.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
suixinkan/Assets.xcassets/Payment/payment_nalati_logo.imageset/payment_nalati_logo.png
vendored
Normal file
BIN
suixinkan/Assets.xcassets/Payment/payment_nalati_logo.imageset/payment_nalati_logo.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 89 KiB |
30
suixinkan/Core/Push/PushAPI.swift
Normal file
30
suixinkan/Core/Push/PushAPI.swift
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// 极光 Registration ID 上报能力,允许推送管理器注入测试替身。
|
||||||
|
@MainActor
|
||||||
|
protocol PushRegistrationServing: AnyObject {
|
||||||
|
/// 将当前设备的极光 Registration ID 绑定到已登录业务账号。
|
||||||
|
func registerJPushID(_ registrationID: String) async throws
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 推送 API,复用 Android 当前使用的 Registration ID 上报接口。
|
||||||
|
@MainActor
|
||||||
|
final class PushAPI: PushRegistrationServing {
|
||||||
|
private let client: APIClient
|
||||||
|
|
||||||
|
/// 使用共享网络客户端创建推送 API。
|
||||||
|
init(client: APIClient) {
|
||||||
|
self.client = client
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 上报极光 Registration ID;后端字段名沿用 `jpush_reg_id`。
|
||||||
|
func registerJPushID(_ registrationID: String) async throws {
|
||||||
|
let _: EmptyPayload = try await client.send(
|
||||||
|
APIRequest(
|
||||||
|
method: .post,
|
||||||
|
path: "/api/app/user/register-jpush-id",
|
||||||
|
queryItems: [URLQueryItem(name: "jpush_reg_id", value: registrationID)]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
86
suixinkan/Core/Push/PushModels.swift
Normal file
86
suixinkan/Core/Push/PushModels.swift
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// 推送点击后可到达的业务页面。
|
||||||
|
enum PushDestination: Sendable, Equatable {
|
||||||
|
case paymentRecord
|
||||||
|
case paymentDetails
|
||||||
|
case messageCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 极光/APNs payload 解析结果,仅提取业务消息类型用于点击路由。
|
||||||
|
struct PushPayload: Sendable, Equatable {
|
||||||
|
private let type: String
|
||||||
|
|
||||||
|
/// 从系统通知 userInfo 提取顶层或推送包装层中的业务消息类型。
|
||||||
|
nonisolated init(userInfo: [AnyHashable: Any]) {
|
||||||
|
type = Self.extractType(from: userInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从可发送的字符串字典创建 payload,仅保留业务消息类型。
|
||||||
|
nonisolated init(values: [String: String]) {
|
||||||
|
type = Self.normalizedType(values["type"])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 可发送的规范化字段快照。
|
||||||
|
nonisolated var normalizedValues: [String: String] {
|
||||||
|
type.isEmpty ? [:] : ["type": type]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 仅根据后端业务消息类型解析目标页面。
|
||||||
|
nonisolated var destination: PushDestination {
|
||||||
|
switch type {
|
||||||
|
case "1":
|
||||||
|
return .paymentRecord
|
||||||
|
case "6":
|
||||||
|
return .paymentDetails
|
||||||
|
default:
|
||||||
|
return .messageCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private nonisolated static func extractType(
|
||||||
|
from dictionary: [AnyHashable: Any],
|
||||||
|
depth: Int = 0
|
||||||
|
) -> String {
|
||||||
|
guard depth <= 4 else { return "" }
|
||||||
|
let directType = normalizedType(dictionary["type"])
|
||||||
|
guard directType.isEmpty else { return directType }
|
||||||
|
|
||||||
|
for key in nestedTypeContainerKeys {
|
||||||
|
guard let value = dictionary[key] else { continue }
|
||||||
|
if let nested = value as? [AnyHashable: Any] {
|
||||||
|
let nestedType = extractType(from: nested, depth: depth + 1)
|
||||||
|
if !nestedType.isEmpty { return nestedType }
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let text = value as? String,
|
||||||
|
let data = text.data(using: .utf8),
|
||||||
|
let object = try? JSONSerialization.jsonObject(with: data),
|
||||||
|
let nested = object as? [String: Any]
|
||||||
|
else { continue }
|
||||||
|
|
||||||
|
let bridged = Dictionary(uniqueKeysWithValues: nested.map {
|
||||||
|
(AnyHashable($0.key), $0.value)
|
||||||
|
})
|
||||||
|
let nestedType = extractType(from: bridged, depth: depth + 1)
|
||||||
|
if !nestedType.isEmpty { return nestedType }
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
private nonisolated static func normalizedType(_ value: Any?) -> String {
|
||||||
|
switch value {
|
||||||
|
case let string as String:
|
||||||
|
return string.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
case let number as NSNumber:
|
||||||
|
return number.stringValue
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private nonisolated static let nestedTypeContainerKeys = [
|
||||||
|
"extras", "extra", "JMessageExtra", "n_extras",
|
||||||
|
]
|
||||||
|
}
|
||||||
422
suixinkan/Core/Push/PushNotificationManager.swift
Normal file
422
suixinkan/Core/Push/PushNotificationManager.swift
Normal file
@ -0,0 +1,422 @@
|
|||||||
|
import UIKit
|
||||||
|
import UserNotifications
|
||||||
|
|
||||||
|
/// App 桌面图标角标设置能力,便于业务同步未读消息数量并进行测试替换。
|
||||||
|
@MainActor
|
||||||
|
protocol ApplicationIconBadgeSetting: AnyObject {
|
||||||
|
/// 将桌面图标角标更新为指定非负数量。
|
||||||
|
func setBadgeCount(_ count: Int) async
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 使用 UserNotifications 更新 App 桌面图标角标的系统实现。
|
||||||
|
@MainActor
|
||||||
|
final class SystemApplicationIconBadgeSetter: ApplicationIconBadgeSetting {
|
||||||
|
/// 全局角标设置器。
|
||||||
|
static let shared = SystemApplicationIconBadgeSetter()
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
func setBadgeCount(_ count: Int) async {
|
||||||
|
do {
|
||||||
|
try await UNUserNotificationCenter.current().setBadgeCount(max(count, 0))
|
||||||
|
} catch {
|
||||||
|
#if DEBUG
|
||||||
|
print("Update application icon badge failed: \(error.localizedDescription)")
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 极光 SDK 的最小能力边界,便于推送生命周期逻辑使用测试替身。
|
||||||
|
@MainActor
|
||||||
|
protocol PushSDKProviding: AnyObject {
|
||||||
|
/// 初始化极光长连接与 APNs 代理服务。
|
||||||
|
func initialize(launchOptions: [UIApplication.LaunchOptionsKey: Any]?, isProduction: Bool)
|
||||||
|
|
||||||
|
/// 请求系统通知权限并注册极光通知代理。
|
||||||
|
func requestAuthorization(delegate: JPUSHRegisterDelegate)
|
||||||
|
|
||||||
|
/// 将 APNs Device Token 注册给极光。
|
||||||
|
func registerDeviceToken(_ deviceToken: Data)
|
||||||
|
|
||||||
|
/// 获取极光 Registration ID。
|
||||||
|
func fetchRegistrationID(completion: @escaping (Int32, String?) -> Void)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 跨越极光 `@Sendable` 回调边界的只读闭包容器。
|
||||||
|
private final class RegistrationIDCompletionBox: @unchecked Sendable {
|
||||||
|
let completion: (Int32, String?) -> Void
|
||||||
|
|
||||||
|
init(completion: @escaping (Int32, String?) -> Void) {
|
||||||
|
self.completion = completion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 生产环境极光 SDK 适配器,集中保存 AppKey 和渠道配置。
|
||||||
|
@MainActor
|
||||||
|
final class JPushSDKAdapter: PushSDKProviding {
|
||||||
|
private static let appKey = "194a633c2a6b79c2f1033c40"
|
||||||
|
private static let channel = "App Store"
|
||||||
|
|
||||||
|
/// 初始化极光 SDK;Debug 输出详细日志,Release 只保留告警和错误。
|
||||||
|
func initialize(launchOptions: [UIApplication.LaunchOptionsKey: Any]?, isProduction: Bool) {
|
||||||
|
JPUSHService.setup(
|
||||||
|
withOption: launchOptions,
|
||||||
|
appKey: Self.appKey,
|
||||||
|
channel: Self.channel,
|
||||||
|
apsForProduction: isProduction
|
||||||
|
)
|
||||||
|
#if DEBUG
|
||||||
|
JPUSHService.setDebugMode()
|
||||||
|
#else
|
||||||
|
JPUSHService.setLogOFF()
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 请求 alert、badge、sound 三类通知权限。
|
||||||
|
func requestAuthorization(delegate: JPUSHRegisterDelegate) {
|
||||||
|
let entity = JPUSHRegisterEntity()
|
||||||
|
entity.types = 1 | 2 | 4
|
||||||
|
JPUSHService.register(forRemoteNotificationConfig: entity, delegate: delegate)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 将 APNs Device Token 交给极光。
|
||||||
|
func registerDeviceToken(_ deviceToken: Data) {
|
||||||
|
JPUSHService.registerDeviceToken(deviceToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 异步获取当前设备 Registration ID。
|
||||||
|
func fetchRegistrationID(completion: @escaping (Int32, String?) -> Void) {
|
||||||
|
let box = RegistrationIDCompletionBox(completion: completion)
|
||||||
|
JPUSHService.registrationIDCompletionHandler { resultCode, registrationID in
|
||||||
|
box.completion(resultCode, registrationID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 推送生命周期管理器,负责隐私门禁、权限、Registration ID 绑定及通知点击分发。
|
||||||
|
@MainActor
|
||||||
|
final class PushNotificationManager: NSObject {
|
||||||
|
/// 全局推送管理器。
|
||||||
|
static let shared = PushNotificationManager()
|
||||||
|
|
||||||
|
/// 推送相关的本地持久化键。
|
||||||
|
private enum Key {
|
||||||
|
static let registrationID = "key_jpush_registration_id"
|
||||||
|
static let uploadedRegistrationIDSuffix = "jpush_uploaded_registration_id"
|
||||||
|
}
|
||||||
|
|
||||||
|
private let sdk: any PushSDKProviding
|
||||||
|
private let api: any PushRegistrationServing
|
||||||
|
private let appStore: AppStore
|
||||||
|
private let defaults: UserDefaults
|
||||||
|
private let router: any PushRouting
|
||||||
|
private let applicationIconBadgeSetter: any ApplicationIconBadgeSetting
|
||||||
|
|
||||||
|
private var isInitialized = false
|
||||||
|
private var didRequestAuthorization = false
|
||||||
|
private var uploadTask: Task<Void, Never>?
|
||||||
|
private var queuedForcedUpload = false
|
||||||
|
private var isFetchingRegistrationID = false
|
||||||
|
private var queuedForcedFetch = false
|
||||||
|
|
||||||
|
/// 创建推送管理器;生产环境使用共享依赖,测试可注入替身。
|
||||||
|
init(
|
||||||
|
sdk: (any PushSDKProviding)? = nil,
|
||||||
|
api: (any PushRegistrationServing)? = nil,
|
||||||
|
appStore: AppStore = .shared,
|
||||||
|
defaults: UserDefaults = .standard,
|
||||||
|
router: (any PushRouting)? = nil,
|
||||||
|
applicationIconBadgeSetter: (any ApplicationIconBadgeSetting)? = nil
|
||||||
|
) {
|
||||||
|
self.sdk = sdk ?? JPushSDKAdapter()
|
||||||
|
self.api = api ?? NetworkServices.shared.pushAPI
|
||||||
|
self.appStore = appStore
|
||||||
|
self.defaults = defaults
|
||||||
|
self.router = router ?? PushRouteCoordinator.shared
|
||||||
|
self.applicationIconBadgeSetter = applicationIconBadgeSetter ?? SystemApplicationIconBadgeSetter.shared
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 绑定当前 Scene 的主窗口。
|
||||||
|
func attach(window: UIWindow) {
|
||||||
|
router.attach(window: window)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 仅在用户已同意隐私协议后初始化极光 SDK。
|
||||||
|
func initializeIfPrivacyAccepted(
|
||||||
|
launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
|
||||||
|
) {
|
||||||
|
guard appStore.session.privacyAgreementAccepted, !isInitialized else { return }
|
||||||
|
isInitialized = true
|
||||||
|
sdk.initialize(launchOptions: launchOptions, isProduction: Self.isProductionBuild)
|
||||||
|
observeJPushNetworkLogin()
|
||||||
|
refreshRegistrationID(forceUpload: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 登录成功后请求通知权限,并强制绑定当前账号。
|
||||||
|
func handleLoginCompleted() {
|
||||||
|
initializeIfPrivacyAccepted()
|
||||||
|
guard isInitialized, appStore.session.isLoggedIn else { return }
|
||||||
|
if !didRequestAuthorization {
|
||||||
|
didRequestAuthorization = true
|
||||||
|
sdk.requestAuthorization(delegate: self)
|
||||||
|
}
|
||||||
|
bindCurrentAccount()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 账号切换后把同一设备重新绑定到新的业务账号。
|
||||||
|
func handleAccountSwitched() {
|
||||||
|
guard appStore.session.isLoggedIn else { return }
|
||||||
|
bindCurrentAccount()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 退出登录时停止账号上报并清除未完成的业务路由。
|
||||||
|
func handleLogout() {
|
||||||
|
uploadTask?.cancel()
|
||||||
|
uploadTask = nil
|
||||||
|
queuedForcedUpload = false
|
||||||
|
router.resetPendingRoute()
|
||||||
|
Task { await updateApplicationIconBadgeCount(0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 将桌面 App Icon 角标更新为最新未读消息数量。
|
||||||
|
func updateApplicationIconBadgeCount(_ count: Int) async {
|
||||||
|
await applicationIconBadgeSetter.setBadgeCount(max(count, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// App 回到前台时补偿失败或尚未完成的 Registration ID 上报。
|
||||||
|
func retryPendingRegistrationUpload() {
|
||||||
|
guard appStore.session.isLoggedIn else { return }
|
||||||
|
uploadCachedRegistrationID(force: false)
|
||||||
|
refreshRegistrationID(forceUpload: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 登录根页面建立后继续执行通知点击暂存的路由。
|
||||||
|
func routePendingNotificationIfPossible() {
|
||||||
|
router.routePendingIfPossible()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// APNs 注册成功后把 Device Token 注册给极光。
|
||||||
|
func handleDeviceToken(_ deviceToken: Data) {
|
||||||
|
guard isInitialized else { return }
|
||||||
|
sdk.registerDeviceToken(deviceToken)
|
||||||
|
refreshRegistrationID(forceUpload: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// APNs 注册失败只记录调试信息,不阻断登录和主业务。
|
||||||
|
func handleRegistrationError(_ error: Error) {
|
||||||
|
#if DEBUG
|
||||||
|
print("JPush APNs registration failed: \(error.localizedDescription)")
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 处理冷启动或运行中通知点击。
|
||||||
|
func handleNotificationTap(
|
||||||
|
payload: PushPayload,
|
||||||
|
requestIdentifier: String
|
||||||
|
) {
|
||||||
|
router.handle(
|
||||||
|
destination: payload.destination,
|
||||||
|
requestIdentifier: requestIdentifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 处理 Scene 冷启动携带的系统通知响应,并向极光上报点击。
|
||||||
|
func handleNotificationResponse(_ response: UNNotificationResponse) {
|
||||||
|
let userInfo = response.notification.request.content.userInfo
|
||||||
|
Self.logReceivedPush(userInfo, source: "tap(cold-start)")
|
||||||
|
JPUSHService.handleRemoteNotification(userInfo)
|
||||||
|
handleNotificationTap(
|
||||||
|
payload: PushPayload(userInfo: userInfo),
|
||||||
|
requestIdentifier: response.notification.request.identifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 处理后台静默通知并向极光上报到达,不触发页面跳转。
|
||||||
|
func handleRemoteNotification(_ userInfo: [AnyHashable: Any]) {
|
||||||
|
Self.logReceivedPush(userInfo, source: "background")
|
||||||
|
JPUSHService.handleRemoteNotification(userInfo)
|
||||||
|
NotificationCenter.default.post(name: NotificationName.unreadMessageCountDidChange, object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 将收到的推送 payload 打印到控制台,便于联调排查。
|
||||||
|
private nonisolated static func logReceivedPush(
|
||||||
|
_ userInfo: [AnyHashable: Any],
|
||||||
|
source: String
|
||||||
|
) {
|
||||||
|
#if DEBUG
|
||||||
|
let payloadText: String
|
||||||
|
let normalized = Dictionary(uniqueKeysWithValues: userInfo.compactMap { key, value in
|
||||||
|
(key as? String).map { ($0, value) }
|
||||||
|
})
|
||||||
|
if JSONSerialization.isValidJSONObject(normalized),
|
||||||
|
let data = try? JSONSerialization.data(
|
||||||
|
withJSONObject: normalized,
|
||||||
|
options: [.prettyPrinted, .sortedKeys]
|
||||||
|
),
|
||||||
|
let text = String(data: data, encoding: .utf8) {
|
||||||
|
payloadText = text
|
||||||
|
} else {
|
||||||
|
payloadText = String(describing: userInfo)
|
||||||
|
}
|
||||||
|
print("[Push] received (\(source)):\n\(payloadText)")
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private func observeJPushNetworkLogin() {
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(handleJPushNetworkLogin),
|
||||||
|
name: .jpfNetworkDidLogin,
|
||||||
|
object: nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func handleJPushNetworkLogin() {
|
||||||
|
refreshRegistrationID(forceUpload: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func refreshRegistrationID(forceUpload: Bool) {
|
||||||
|
guard isInitialized else { return }
|
||||||
|
queuedForcedFetch = queuedForcedFetch || forceUpload
|
||||||
|
guard !isFetchingRegistrationID else { return }
|
||||||
|
isFetchingRegistrationID = true
|
||||||
|
sdk.fetchRegistrationID { [weak self] resultCode, registrationID in
|
||||||
|
Task { @MainActor in
|
||||||
|
guard let self else { return }
|
||||||
|
let shouldForceUpload = self.queuedForcedFetch
|
||||||
|
self.queuedForcedFetch = false
|
||||||
|
self.isFetchingRegistrationID = false
|
||||||
|
guard resultCode == 0 else { return }
|
||||||
|
let normalized = registrationID?
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||||
|
guard !normalized.isEmpty else { return }
|
||||||
|
self.defaults.set(normalized, forKey: Key.registrationID)
|
||||||
|
self.upload(registrationID: normalized, force: shouldForceUpload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func uploadCachedRegistrationID(force: Bool) {
|
||||||
|
let registrationID = cachedRegistrationID
|
||||||
|
guard !registrationID.isEmpty else { return }
|
||||||
|
upload(registrationID: registrationID, force: force)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func bindCurrentAccount() {
|
||||||
|
if cachedRegistrationID.isEmpty {
|
||||||
|
refreshRegistrationID(forceUpload: true)
|
||||||
|
} else {
|
||||||
|
uploadCachedRegistrationID(force: true)
|
||||||
|
refreshRegistrationID(forceUpload: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var cachedRegistrationID: String {
|
||||||
|
defaults.string(forKey: Key.registrationID)?
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
private func upload(registrationID: String, force: Bool) {
|
||||||
|
guard appStore.session.isLoggedIn,
|
||||||
|
let uploadedKey = appStore.session.accountScopedKey(Key.uploadedRegistrationIDSuffix)
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
if !force, defaults.string(forKey: uploadedKey) == registrationID {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if uploadTask != nil {
|
||||||
|
queuedForcedUpload = queuedForcedUpload || force
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let accountScope = appStore.session.accountCachePrefix
|
||||||
|
uploadTask = Task { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
var succeeded = false
|
||||||
|
do {
|
||||||
|
try await self.api.registerJPushID(registrationID)
|
||||||
|
if self.appStore.session.accountCachePrefix == accountScope {
|
||||||
|
self.defaults.set(registrationID, forKey: uploadedKey)
|
||||||
|
}
|
||||||
|
succeeded = true
|
||||||
|
} catch is CancellationError {
|
||||||
|
// 退出登录时取消,不视为失败重试。
|
||||||
|
} catch {
|
||||||
|
#if DEBUG
|
||||||
|
print("JPush Registration ID upload failed: \(error.localizedDescription)")
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
self.uploadTask = nil
|
||||||
|
let shouldForceAgain = self.queuedForcedUpload
|
||||||
|
self.queuedForcedUpload = false
|
||||||
|
if succeeded, shouldForceAgain {
|
||||||
|
self.uploadCachedRegistrationID(force: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private nonisolated static var isProductionBuild: Bool {
|
||||||
|
#if DEBUG
|
||||||
|
false
|
||||||
|
#else
|
||||||
|
true
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PushNotificationManager: JPUSHRegisterDelegate {
|
||||||
|
/// 前台通知只展示系统 UI,不自动执行业务路由。
|
||||||
|
nonisolated func jpushNotificationCenter(
|
||||||
|
_ center: UNUserNotificationCenter,
|
||||||
|
willPresent notification: UNNotification,
|
||||||
|
withCompletionHandler completionHandler: @escaping (Int) -> Void
|
||||||
|
) {
|
||||||
|
let userInfo = notification.request.content.userInfo
|
||||||
|
Self.logReceivedPush(userInfo, source: "foreground")
|
||||||
|
JPUSHService.handleRemoteNotification(userInfo)
|
||||||
|
Task { @MainActor in
|
||||||
|
NotificationCenter.default.post(name: NotificationName.unreadMessageCountDidChange, object: nil)
|
||||||
|
}
|
||||||
|
let options: UNNotificationPresentationOptions = [.banner, .list, .sound, .badge]
|
||||||
|
completionHandler(Int(options.rawValue))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 用户点击通知后上报极光统计,并在主 Actor 执行业务路由。
|
||||||
|
nonisolated func jpushNotificationCenter(
|
||||||
|
_ center: UNUserNotificationCenter,
|
||||||
|
didReceive response: UNNotificationResponse,
|
||||||
|
withCompletionHandler completionHandler: @escaping () -> Void
|
||||||
|
) {
|
||||||
|
let userInfo = response.notification.request.content.userInfo
|
||||||
|
Self.logReceivedPush(userInfo, source: "tap")
|
||||||
|
JPUSHService.handleRemoteNotification(userInfo)
|
||||||
|
let payload = PushPayload(userInfo: userInfo)
|
||||||
|
let requestIdentifier = response.notification.request.identifier
|
||||||
|
Task { @MainActor [weak self] in
|
||||||
|
self?.handleNotificationTap(
|
||||||
|
payload: payload,
|
||||||
|
requestIdentifier: requestIdentifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
completionHandler()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 系统通知设置入口回调,首期无需额外应用内页面。
|
||||||
|
nonisolated func jpushNotificationCenter(
|
||||||
|
_ center: UNUserNotificationCenter,
|
||||||
|
openSettingsFor notification: UNNotification
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/// 极光通知授权状态回调,权限变化由系统设置管理。
|
||||||
|
nonisolated func jpushNotificationAuthorization(
|
||||||
|
_ status: JPAuthorizationStatus,
|
||||||
|
withInfo info: [AnyHashable: Any]?
|
||||||
|
) {}
|
||||||
|
}
|
||||||
98
suixinkan/Core/Push/PushRouteCoordinator.swift
Normal file
98
suixinkan/Core/Push/PushRouteCoordinator.swift
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// 推送点击路由能力,允许生命周期管理器与 UIKit 导航解耦并支持测试替身。
|
||||||
|
@MainActor
|
||||||
|
protocol PushRouting: AnyObject {
|
||||||
|
/// 绑定当前 Scene 的主窗口。
|
||||||
|
func attach(window: UIWindow)
|
||||||
|
|
||||||
|
/// 处理一次通知点击;未登录时暂存到登录成功后继续。
|
||||||
|
func handle(destination: PushDestination, requestIdentifier: String)
|
||||||
|
|
||||||
|
/// 主界面建立后执行尚未完成的通知路由。
|
||||||
|
func routePendingIfPossible()
|
||||||
|
|
||||||
|
/// 清除退出登录前遗留的待处理路由。
|
||||||
|
func resetPendingRoute()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 集中式推送路由器,负责冷启动去重、登录等待和跨 Tab 导航。
|
||||||
|
@MainActor
|
||||||
|
final class PushRouteCoordinator: PushRouting {
|
||||||
|
/// 全局推送路由器。
|
||||||
|
static let shared = PushRouteCoordinator()
|
||||||
|
|
||||||
|
/// 等待登录或主界面建立后执行的一次性推送路由。
|
||||||
|
private struct PendingRoute {
|
||||||
|
let destination: PushDestination
|
||||||
|
let requestIdentifier: String
|
||||||
|
}
|
||||||
|
|
||||||
|
private weak var window: UIWindow?
|
||||||
|
private var pendingRoute: PendingRoute?
|
||||||
|
private var handledRequestIdentifiers: [String] = []
|
||||||
|
private let appStore: AppStore
|
||||||
|
|
||||||
|
/// 创建路由器并注入应用状态。
|
||||||
|
init(appStore: AppStore = .shared) {
|
||||||
|
self.appStore = appStore
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 绑定当前 Scene 的主窗口。
|
||||||
|
func attach(window: UIWindow) {
|
||||||
|
self.window = window
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 处理通知点击;相同 request identifier 只执行一次。
|
||||||
|
func handle(destination: PushDestination, requestIdentifier: String) {
|
||||||
|
let identifier = requestIdentifier.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if !identifier.isEmpty, handledRequestIdentifiers.contains(identifier) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
markHandled(identifier)
|
||||||
|
|
||||||
|
let route = PendingRoute(destination: destination, requestIdentifier: identifier)
|
||||||
|
guard appStore.session.isLoggedIn, mainTabController != nil else {
|
||||||
|
pendingRoute = route
|
||||||
|
return
|
||||||
|
}
|
||||||
|
perform(route)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 登录完成、根页面切换后继续执行最后一次待处理路由。
|
||||||
|
func routePendingIfPossible() {
|
||||||
|
guard appStore.session.isLoggedIn,
|
||||||
|
mainTabController != nil,
|
||||||
|
let route = pendingRoute
|
||||||
|
else { return }
|
||||||
|
pendingRoute = nil
|
||||||
|
perform(route)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 用户退出登录时丢弃之前账号尚未执行的跳转。
|
||||||
|
func resetPendingRoute() {
|
||||||
|
pendingRoute = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private var mainTabController: MainTabBarController? {
|
||||||
|
window?.rootViewController as? MainTabBarController
|
||||||
|
}
|
||||||
|
|
||||||
|
private func perform(_ route: PendingRoute) {
|
||||||
|
guard let mainTabController else {
|
||||||
|
pendingRoute = route
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mainTabController.dismiss(animated: false)
|
||||||
|
mainTabController.openPushDestination(route.destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func markHandled(_ identifier: String) {
|
||||||
|
guard !identifier.isEmpty else { return }
|
||||||
|
handledRequestIdentifiers.append(identifier)
|
||||||
|
if handledRequestIdentifiers.count > 50 {
|
||||||
|
handledRequestIdentifiers.removeFirst(handledRequestIdentifiers.count - 50)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
//
|
||||||
|
// CommissionRateLogViewModel.swift
|
||||||
|
// suixinkan
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// 获客员分成比例修改记录 ViewModel,负责刷新、分页与重复项合并。
|
||||||
|
final class CommissionRateLogViewModel {
|
||||||
|
private enum Constants {
|
||||||
|
static let pageSize = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
private(set) var items: [CommissionRateLogEntity] = []
|
||||||
|
private(set) var isRefreshing = false
|
||||||
|
private(set) var isLoadingMore = false
|
||||||
|
private(set) var initialLoading = true
|
||||||
|
private(set) var canLoadMore = false
|
||||||
|
|
||||||
|
var onStateChange: (() -> Void)?
|
||||||
|
var onShowMessage: ((String) -> Void)?
|
||||||
|
|
||||||
|
private let saleUserId: Int
|
||||||
|
private var total = 0
|
||||||
|
private var lastLoadedPage = 0
|
||||||
|
private var isLoading = false
|
||||||
|
|
||||||
|
init(saleUserId: Int) {
|
||||||
|
self.saleUserId = saleUserId
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 重新加载第一页修改记录。
|
||||||
|
func refresh(api: OrderAPI) async {
|
||||||
|
await load(api: api, page: 1, append: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 在存在下一页时加载更多修改记录。
|
||||||
|
func loadMore(api: OrderAPI) async {
|
||||||
|
guard canLoadMore, !isRefreshing, !isLoadingMore else { return }
|
||||||
|
await load(api: api, page: lastLoadedPage + 1, append: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func load(api: OrderAPI, page: Int, append: Bool) async {
|
||||||
|
guard saleUserId > 0 else {
|
||||||
|
initialLoading = false
|
||||||
|
onShowMessage?("获客员信息无效")
|
||||||
|
notifyStateChange()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard !isLoading else { return }
|
||||||
|
isLoading = true
|
||||||
|
if append {
|
||||||
|
isLoadingMore = true
|
||||||
|
} else {
|
||||||
|
isRefreshing = true
|
||||||
|
}
|
||||||
|
notifyStateChange()
|
||||||
|
|
||||||
|
defer {
|
||||||
|
isLoading = false
|
||||||
|
initialLoading = false
|
||||||
|
if append {
|
||||||
|
isLoadingMore = false
|
||||||
|
} else {
|
||||||
|
isRefreshing = false
|
||||||
|
}
|
||||||
|
notifyStateChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let response = try await api.saleUserCommissionRateLogs(
|
||||||
|
saleUserId: saleUserId,
|
||||||
|
page: page,
|
||||||
|
pageSize: Constants.pageSize
|
||||||
|
)
|
||||||
|
if append {
|
||||||
|
let existingIDs = Set(items.map(\.id))
|
||||||
|
items.append(contentsOf: response.list.filter { !existingIDs.contains($0.id) })
|
||||||
|
} else {
|
||||||
|
var seenIDs = Set<Int>()
|
||||||
|
items = response.list.filter { seenIDs.insert($0.id).inserted }
|
||||||
|
}
|
||||||
|
total = max(0, response.total)
|
||||||
|
lastLoadedPage = page
|
||||||
|
canLoadMore = items.count < total && !response.list.isEmpty
|
||||||
|
} catch {
|
||||||
|
onShowMessage?(error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func notifyStateChange() {
|
||||||
|
onStateChange?()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,10 +9,40 @@ import Foundation
|
|||||||
struct RolePermissionResponse: Codable, Equatable, Sendable {
|
struct RolePermissionResponse: Codable, Equatable, Sendable {
|
||||||
let role: RoleInfo
|
let role: RoleInfo
|
||||||
let scenic: [ScenicInfo]
|
let scenic: [ScenicInfo]
|
||||||
|
let store: [RolePermissionStoreInfo]
|
||||||
|
|
||||||
init(role: RoleInfo, scenic: [ScenicInfo] = []) {
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case role
|
||||||
|
case scenic
|
||||||
|
case store
|
||||||
|
}
|
||||||
|
|
||||||
|
init(
|
||||||
|
role: RoleInfo,
|
||||||
|
scenic: [ScenicInfo] = [],
|
||||||
|
store: [RolePermissionStoreInfo] = []
|
||||||
|
) {
|
||||||
self.role = role
|
self.role = role
|
||||||
self.scenic = scenic
|
self.scenic = scenic
|
||||||
|
self.store = store
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
role = try container.decode(RoleInfo.self, forKey: .role)
|
||||||
|
scenic = try container.decodeIfPresent([ScenicInfo].self, forKey: .scenic) ?? []
|
||||||
|
store = try container.decodeIfPresent([RolePermissionStoreInfo].self, forKey: .store) ?? []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 当前角色账号可使用的店铺摘要,来源于 `role-permission` 的 `store` 字段。
|
||||||
|
struct RolePermissionStoreInfo: Codable, Equatable, Sendable {
|
||||||
|
let id: Int
|
||||||
|
let name: String
|
||||||
|
|
||||||
|
init(id: Int = 0, name: String = "") {
|
||||||
|
self.id = id
|
||||||
|
self.name = name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,18 +17,23 @@ final class HomeCommonMenuStore {
|
|||||||
/// 读取已保存的常用 URI;无记录时返回空数组。
|
/// 读取已保存的常用 URI;无记录时返回空数组。
|
||||||
func savedCommonURIs(accountScope: String, roleCode: String) -> [String] {
|
func savedCommonURIs(accountScope: String, roleCode: String) -> [String] {
|
||||||
guard let key = storageKey(accountScope: accountScope, roleCode: roleCode) else { return [] }
|
guard let key = storageKey(accountScope: accountScope, roleCode: roleCode) else { return [] }
|
||||||
return defaults.stringArray(forKey: key) ?? []
|
let saved = defaults.stringArray(forKey: key) ?? []
|
||||||
|
let sanitized = saved.filter(HomeMenuCatalog.isCustomizable(uri:))
|
||||||
|
if sanitized != saved {
|
||||||
|
defaults.set(sanitized, forKey: key)
|
||||||
|
}
|
||||||
|
return sanitized
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 保存常用 URI 列表。
|
/// 保存常用 URI 列表。
|
||||||
func saveCommonURIs(_ uris: [String], accountScope: String, roleCode: String) {
|
func saveCommonURIs(_ uris: [String], accountScope: String, roleCode: String) {
|
||||||
guard let key = storageKey(accountScope: accountScope, roleCode: roleCode) else { return }
|
guard let key = storageKey(accountScope: accountScope, roleCode: roleCode) else { return }
|
||||||
defaults.set(uris, forKey: key)
|
defaults.set(uris.filter(HomeMenuCatalog.isCustomizable(uri:)), forKey: key)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 根据权限生成默认常用 URI(最多 4 个)。
|
/// 根据权限生成默认常用 URI(最多 4 个)。
|
||||||
static func defaultCommonURIs(from permissions: [HomePermissionItem]) -> [String] {
|
static func defaultCommonURIs(from permissions: [HomePermissionItem]) -> [String] {
|
||||||
let uris = permissions.map(\.uri)
|
let uris = permissions.map(\.uri).filter(HomeMenuCatalog.isCustomizable(uri:))
|
||||||
return uris.count > 4 ? Array(uris.prefix(4)) : uris
|
return uris.count > 4 ? Array(uris.prefix(4)) : uris
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,8 +56,9 @@ final class HomeCommonMenuStore {
|
|||||||
commonURIs: [String]
|
commonURIs: [String]
|
||||||
) -> (common: [HomeMenuItem], more: [HomeMenuItem]) {
|
) -> (common: [HomeMenuItem], more: [HomeMenuItem]) {
|
||||||
let commonSet = Set(commonURIs)
|
let commonSet = Set(commonURIs)
|
||||||
let common = allMenus.filter { commonSet.contains($0.uri) }
|
let customizableMenus = allMenus.filter { HomeMenuCatalog.isCustomizable(uri: $0.uri) }
|
||||||
let more = allMenus.filter { !commonSet.contains($0.uri) }
|
let common = customizableMenus.filter { commonSet.contains($0.uri) }
|
||||||
|
let more = customizableMenus.filter { !commonSet.contains($0.uri) }
|
||||||
return (common, more)
|
return (common, more)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import Foundation
|
|||||||
/// 首页菜单静态 catalog,与服务端 permission URI 取交集后展示。
|
/// 首页菜单静态 catalog,与服务端 permission URI 取交集后展示。
|
||||||
enum HomeMenuCatalog {
|
enum HomeMenuCatalog {
|
||||||
|
|
||||||
|
private static let fixedHomeEntryURIs: Set<String> = ["message_center"]
|
||||||
|
|
||||||
/// 全部已知菜单项,对齐 Android `Constants.menuList`。
|
/// 全部已知菜单项,对齐 Android `Constants.menuList`。
|
||||||
static let allItems: [HomeMenuItem] = [
|
static let allItems: [HomeMenuItem] = [
|
||||||
HomeMenuItem(uri: "space_settings", title: "空间设置", iconName: "home_menu_space"),
|
HomeMenuItem(uri: "space_settings", title: "空间设置", iconName: "home_menu_space"),
|
||||||
@ -45,6 +47,7 @@ enum HomeMenuCatalog {
|
|||||||
/// 将服务端权限映射为可展示菜单,保持服务端顺序。
|
/// 将服务端权限映射为可展示菜单,保持服务端顺序。
|
||||||
static func visibleMenus(from permissions: [HomePermissionItem]) -> [HomeMenuItem] {
|
static func visibleMenus(from permissions: [HomePermissionItem]) -> [HomeMenuItem] {
|
||||||
permissions.compactMap { permission in
|
permissions.compactMap { permission in
|
||||||
|
guard isCustomizable(uri: permission.uri) else { return nil }
|
||||||
guard let catalog = allItems.first(where: { $0.uri == permission.uri }) else { return nil }
|
guard let catalog = allItems.first(where: { $0.uri == permission.uri }) else { return nil }
|
||||||
return HomeMenuItem(
|
return HomeMenuItem(
|
||||||
uri: catalog.uri,
|
uri: catalog.uri,
|
||||||
@ -59,4 +62,9 @@ enum HomeMenuCatalog {
|
|||||||
static func item(for uri: String) -> HomeMenuItem? {
|
static func item(for uri: String) -> HomeMenuItem? {
|
||||||
allItems.first { $0.uri == uri }
|
allItems.first { $0.uri == uri }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 判断入口是否允许出现在首页常用应用及其自定义页面中。
|
||||||
|
static func isCustomizable(uri: String) -> Bool {
|
||||||
|
!fixedHomeEntryURIs.contains(uri)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,21 +16,24 @@ enum HomeMenuIconFactory {
|
|||||||
|
|
||||||
private static let imageCache = NSCache<NSString, UIImage>()
|
private static let imageCache = NSCache<NSString, UIImage>()
|
||||||
|
|
||||||
/// 优先加载 Assets 资源图;否则加载 SF Symbol,并统一渲染到固定画布。
|
/// 优先加载 Assets 资源图并归一化画布;SF Symbol 保留系统符号表示与分层渲染。
|
||||||
static func image(named iconName: String) -> UIImage? {
|
static func image(named iconName: String) -> UIImage? {
|
||||||
let cacheKey = iconName as NSString
|
let cacheKey = iconName as NSString
|
||||||
if let cachedImage = imageCache.object(forKey: cacheKey) {
|
if let cachedImage = imageCache.object(forKey: cacheKey) {
|
||||||
return cachedImage
|
return cachedImage
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let sourceImage = UIImage(named: iconName)
|
let image: UIImage?
|
||||||
?? UIImage(systemName: iconName, withConfiguration: symbolConfiguration) else {
|
if let assetImage = UIImage(named: iconName) {
|
||||||
return nil
|
image = renderOnFixedCanvas(assetImage)
|
||||||
|
} else {
|
||||||
|
image = UIImage(systemName: iconName, withConfiguration: symbolConfiguration)
|
||||||
}
|
}
|
||||||
|
|
||||||
let normalizedImage = renderOnFixedCanvas(sourceImage)
|
if let image {
|
||||||
imageCache.setObject(normalizedImage, forKey: cacheKey)
|
imageCache.setObject(image, forKey: cacheKey)
|
||||||
return normalizedImage
|
}
|
||||||
|
return image
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func renderOnFixedCanvas(_ image: UIImage) -> UIImage {
|
private static func renderOnFixedCanvas(_ image: UIImage) -> UIImage {
|
||||||
|
|||||||
@ -48,6 +48,7 @@ final class AllFunctionsViewModel {
|
|||||||
|
|
||||||
/// 将菜单加入常用应用。
|
/// 将菜单加入常用应用。
|
||||||
func addToCommon(_ menu: HomeMenuItem) {
|
func addToCommon(_ menu: HomeMenuItem) {
|
||||||
|
guard HomeMenuCatalog.isCustomizable(uri: menu.uri) else { return }
|
||||||
guard !commonMenus.contains(where: { $0.uri == menu.uri }) else { return }
|
guard !commonMenus.contains(where: { $0.uri == menu.uri }) else { return }
|
||||||
commonMenus.append(menu)
|
commonMenus.append(menu)
|
||||||
moreMenus.removeAll { $0.uri == menu.uri }
|
moreMenus.removeAll { $0.uri == menu.uri }
|
||||||
|
|||||||
@ -25,6 +25,7 @@ final class HomeViewModel {
|
|||||||
private(set) var reportTimeText = ""
|
private(set) var reportTimeText = ""
|
||||||
private(set) var isLocationReporting = false
|
private(set) var isLocationReporting = false
|
||||||
private(set) var needsPermissionReload = false
|
private(set) var needsPermissionReload = false
|
||||||
|
private(set) var unreadMessageCount = 0
|
||||||
|
|
||||||
var onStateChange: (() -> Void)?
|
var onStateChange: (() -> Void)?
|
||||||
var onShowMessage: ((String) -> Void)?
|
var onShowMessage: ((String) -> Void)?
|
||||||
@ -65,6 +66,7 @@ final class HomeViewModel {
|
|||||||
var isOnline: Bool { locationStateStore.isOnline }
|
var isOnline: Bool { locationStateStore.isOnline }
|
||||||
var countdownDisplayText: String { locationStateStore.countdownDisplayText }
|
var countdownDisplayText: String { locationStateStore.countdownDisplayText }
|
||||||
var reminderMinutes: Int { locationStateStore.reminderMinutes }
|
var reminderMinutes: Int { locationStateStore.reminderMinutes }
|
||||||
|
var hasUnreadMessages: Bool { unreadMessageCount > 0 }
|
||||||
|
|
||||||
/// 首次进入首页时加载权限并恢复在线状态。
|
/// 首次进入首页时加载权限并恢复在线状态。
|
||||||
func initialize(api: HomeAPI) async {
|
func initialize(api: HomeAPI) async {
|
||||||
@ -76,6 +78,28 @@ final class HomeViewModel {
|
|||||||
/// 账号切换后标记需要重新拉取权限。
|
/// 账号切换后标记需要重新拉取权限。
|
||||||
func markNeedsPermissionReload() {
|
func markNeedsPermissionReload() {
|
||||||
needsPermissionReload = true
|
needsPermissionReload = true
|
||||||
|
unreadMessageCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 查询未读消息总数,成功时更新首页状态并返回 `true`。
|
||||||
|
func refreshUnreadMessageStatus(api: any MessageCenterServing) async -> Bool {
|
||||||
|
do {
|
||||||
|
let response = try await api.unreadCount()
|
||||||
|
unreadMessageCount = response.unreadCount
|
||||||
|
notifyStateChange()
|
||||||
|
return true
|
||||||
|
} catch is CancellationError {
|
||||||
|
return false
|
||||||
|
} catch {
|
||||||
|
// 查询失败时保留上一次状态,避免网络抖动导致红点错误消失。
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 使用已知的最新未读消息数量立即刷新首页状态。
|
||||||
|
func updateUnreadMessageCount(_ count: Int) {
|
||||||
|
unreadMessageCount = max(count, 0)
|
||||||
|
notifyStateChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 查询待审核记录并决定进入申请页或审核状态页。
|
/// 查询待审核记录并决定进入申请页或审核状态页。
|
||||||
|
|||||||
@ -20,6 +20,7 @@ enum AMapBootstrap {
|
|||||||
defer { lock.unlock() }
|
defer { lock.unlock() }
|
||||||
guard !isConfigured else { return true }
|
guard !isConfigured else { return true }
|
||||||
|
|
||||||
|
#if !targetEnvironment(simulator)
|
||||||
AMapLocationManager.updatePrivacyShow(.didShow, privacyInfo: .didContain)
|
AMapLocationManager.updatePrivacyShow(.didShow, privacyInfo: .didContain)
|
||||||
AMapLocationManager.updatePrivacyAgree(.didAgree)
|
AMapLocationManager.updatePrivacyAgree(.didAgree)
|
||||||
MAMapView.updatePrivacyShow(.didShow, privacyInfo: .didContain)
|
MAMapView.updatePrivacyShow(.didShow, privacyInfo: .didContain)
|
||||||
@ -29,6 +30,7 @@ enum AMapBootstrap {
|
|||||||
|
|
||||||
AMapServices.shared().apiKey = AMapConfig.apiKey
|
AMapServices.shared().apiKey = AMapConfig.apiKey
|
||||||
AMapServices.shared().enableHTTPS = true
|
AMapServices.shared().enableHTTPS = true
|
||||||
|
#endif
|
||||||
isConfigured = true
|
isConfigured = true
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,16 +3,28 @@
|
|||||||
// suixinkan
|
// suixinkan
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import CoreLocation
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// 高德逆地理编码封装,对齐 Android `GeocodeSearch`。
|
/// 地址逆地理编码能力,供平台定位实现注入。
|
||||||
@MainActor
|
@MainActor
|
||||||
final class LocationGeocoder: NSObject, AMapSearchDelegate {
|
protocol LocationAddressGeocoding: AnyObject {
|
||||||
|
/// 将坐标解析为可读地址,失败时返回空字符串。
|
||||||
|
func reverseGeocode(latitude: Double, longitude: Double) async -> String
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 平台逆地理编码封装;真机使用高德,模拟器使用 Core Location。
|
||||||
|
@MainActor
|
||||||
|
final class LocationGeocoder: NSObject, LocationAddressGeocoding {
|
||||||
|
|
||||||
nonisolated static let shared = LocationGeocoder()
|
nonisolated static let shared = LocationGeocoder()
|
||||||
|
|
||||||
|
#if targetEnvironment(simulator)
|
||||||
|
private let geocoder = CLGeocoder()
|
||||||
|
#else
|
||||||
private var searchAPI: AMapSearchAPI?
|
private var searchAPI: AMapSearchAPI?
|
||||||
private var continuation: CheckedContinuation<String, Never>?
|
private var continuation: CheckedContinuation<String, Never>?
|
||||||
|
#endif
|
||||||
|
|
||||||
nonisolated private override init() {
|
nonisolated private override init() {
|
||||||
super.init()
|
super.init()
|
||||||
@ -20,6 +32,26 @@ final class LocationGeocoder: NSObject, AMapSearchDelegate {
|
|||||||
|
|
||||||
/// 将坐标解析为可读地址。
|
/// 将坐标解析为可读地址。
|
||||||
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
|
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
|
||||||
|
#if targetEnvironment(simulator)
|
||||||
|
let location = CLLocation(latitude: latitude, longitude: longitude)
|
||||||
|
guard let placemark = try? await geocoder.reverseGeocodeLocation(location).first else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if let name = placemark.name?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||||
|
!name.isEmpty {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
placemark.administrativeArea,
|
||||||
|
placemark.locality,
|
||||||
|
placemark.subLocality,
|
||||||
|
placemark.thoroughfare,
|
||||||
|
placemark.subThoroughfare,
|
||||||
|
]
|
||||||
|
.compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||||
|
.filter { !$0.isEmpty }
|
||||||
|
.joined()
|
||||||
|
#else
|
||||||
do {
|
do {
|
||||||
try AMapBootstrap.requireConfigured()
|
try AMapBootstrap.requireConfigured()
|
||||||
} catch {
|
} catch {
|
||||||
@ -40,8 +72,10 @@ final class LocationGeocoder: NSObject, AMapSearchDelegate {
|
|||||||
request.radius = 200
|
request.radius = 200
|
||||||
api.aMapReGoecodeSearch(request)
|
api.aMapReGoecodeSearch(request)
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !targetEnvironment(simulator)
|
||||||
func onReGeocodeSearchDone(_ request: AMapReGeocodeSearchRequest!, response: AMapReGeocodeSearchResponse!) {
|
func onReGeocodeSearchDone(_ request: AMapReGeocodeSearchRequest!, response: AMapReGeocodeSearchResponse!) {
|
||||||
let address = response?.regeocode?.formattedAddress ?? ""
|
let address = response?.regeocode?.formattedAddress ?? ""
|
||||||
continuation?.resume(returning: address)
|
continuation?.resume(returning: address)
|
||||||
@ -62,4 +96,9 @@ final class LocationGeocoder: NSObject, AMapSearchDelegate {
|
|||||||
searchAPI = api
|
searchAPI = api
|
||||||
return api
|
return api
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !targetEnvironment(simulator)
|
||||||
|
extension LocationGeocoder: AMapSearchDelegate {}
|
||||||
|
#endif
|
||||||
|
|||||||
@ -6,7 +6,165 @@
|
|||||||
import CoreLocation
|
import CoreLocation
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// 统一定位服务,封装高德 `AMapLocationManager`,对齐 Android `LocationProvider`。
|
/// Core Location 管理器最小接口,便于模拟器定位实现注入测试替身。
|
||||||
|
@MainActor
|
||||||
|
protocol CoreLocationManaging: AnyObject {
|
||||||
|
var authorizationStatus: CLAuthorizationStatus { get }
|
||||||
|
var desiredAccuracy: CLLocationAccuracy { get set }
|
||||||
|
var delegate: CLLocationManagerDelegate? { get set }
|
||||||
|
|
||||||
|
/// 请求使用期间定位权限。
|
||||||
|
func requestWhenInUseAuthorization()
|
||||||
|
/// 请求一次当前位置。
|
||||||
|
func requestLocation()
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CLLocationManager: CoreLocationManaging {}
|
||||||
|
|
||||||
|
/// Core Location 定位实现,供模拟器运行并支持依赖注入测试。
|
||||||
|
@MainActor
|
||||||
|
final class CoreLocationProvider: NSObject, LocationProviding, CLLocationManagerDelegate {
|
||||||
|
|
||||||
|
nonisolated static let shared = CoreLocationProvider()
|
||||||
|
|
||||||
|
private var appStore: AppStore = .shared
|
||||||
|
private var manager: any CoreLocationManaging = CLLocationManager()
|
||||||
|
private var geocoder: any LocationAddressGeocoding = LocationGeocoder.shared
|
||||||
|
private var permissionContinuation: CheckedContinuation<Void, Error>?
|
||||||
|
private var locationContinuation: CheckedContinuation<CLLocationCoordinate2D, Error>?
|
||||||
|
|
||||||
|
nonisolated override init() {
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 使用可控依赖创建 Core Location 定位服务。
|
||||||
|
init(
|
||||||
|
appStore: AppStore,
|
||||||
|
manager: any CoreLocationManaging,
|
||||||
|
geocoder: any LocationAddressGeocoding
|
||||||
|
) {
|
||||||
|
self.appStore = appStore
|
||||||
|
self.manager = manager
|
||||||
|
self.geocoder = geocoder
|
||||||
|
super.init()
|
||||||
|
manager.delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 请求坐标及逆地理编码地址。
|
||||||
|
func requestSnapshot(
|
||||||
|
desiredAccuracy: CLLocationAccuracy = kCLLocationAccuracyBest
|
||||||
|
) async throws -> HomeLocationSnapshot {
|
||||||
|
let coordinate = try await requestCoordinate(desiredAccuracy: desiredAccuracy)
|
||||||
|
let address = await geocoder.reverseGeocode(
|
||||||
|
latitude: coordinate.latitude,
|
||||||
|
longitude: coordinate.longitude
|
||||||
|
)
|
||||||
|
return HomeLocationSnapshot(
|
||||||
|
latitude: coordinate.latitude,
|
||||||
|
longitude: coordinate.longitude,
|
||||||
|
address: address
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 请求一次当前位置坐标。
|
||||||
|
func requestCoordinate(
|
||||||
|
desiredAccuracy: CLLocationAccuracy
|
||||||
|
) async throws -> CLLocationCoordinate2D {
|
||||||
|
try requirePrivacyAgreement()
|
||||||
|
try await ensureLocationPermission()
|
||||||
|
guard locationContinuation == nil else {
|
||||||
|
throw LocationProviderError.locationFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.delegate = self
|
||||||
|
manager.desiredAccuracy = desiredAccuracy
|
||||||
|
return try await withCheckedThrowingContinuation { continuation in
|
||||||
|
locationContinuation = continuation
|
||||||
|
manager.requestLocation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 使用系统服务将坐标解析为地址。
|
||||||
|
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
|
||||||
|
guard appStore.session.privacyAgreementAccepted else { return "" }
|
||||||
|
return await geocoder.reverseGeocode(latitude: latitude, longitude: longitude)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 请求系统定位权限,等待用户完成首次授权。
|
||||||
|
func ensureLocationPermission() async throws {
|
||||||
|
manager.delegate = self
|
||||||
|
switch manager.authorizationStatus {
|
||||||
|
case .authorizedAlways, .authorizedWhenInUse:
|
||||||
|
return
|
||||||
|
case .denied, .restricted:
|
||||||
|
throw LocationProviderError.permissionDenied
|
||||||
|
case .notDetermined:
|
||||||
|
guard permissionContinuation == nil else {
|
||||||
|
throw LocationProviderError.locationFailed
|
||||||
|
}
|
||||||
|
try await withCheckedThrowingContinuation { continuation in
|
||||||
|
permissionContinuation = continuation
|
||||||
|
manager.requestWhenInUseAuthorization()
|
||||||
|
}
|
||||||
|
@unknown default:
|
||||||
|
throw LocationProviderError.permissionDenied
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
||||||
|
resolvePermission(status: self.manager.authorizationStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||||
|
guard let continuation = locationContinuation else { return }
|
||||||
|
locationContinuation = nil
|
||||||
|
guard let coordinate = locations.last?.coordinate,
|
||||||
|
CLLocationCoordinate2DIsValid(coordinate),
|
||||||
|
coordinate.latitude != 0 || coordinate.longitude != 0 else {
|
||||||
|
continuation.resume(throwing: LocationProviderError.locationFailed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
continuation.resume(returning: coordinate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
||||||
|
guard let continuation = locationContinuation else { return }
|
||||||
|
locationContinuation = nil
|
||||||
|
continuation.resume(throwing: LocationProviderError.locationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requirePrivacyAgreement() throws {
|
||||||
|
guard appStore.session.privacyAgreementAccepted else {
|
||||||
|
throw LocationProviderError.privacyNotAccepted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resolvePermission(status: CLAuthorizationStatus) {
|
||||||
|
guard let continuation = permissionContinuation else { return }
|
||||||
|
switch status {
|
||||||
|
case .authorizedAlways, .authorizedWhenInUse:
|
||||||
|
permissionContinuation = nil
|
||||||
|
continuation.resume()
|
||||||
|
case .denied, .restricted:
|
||||||
|
permissionContinuation = nil
|
||||||
|
continuation.resume(throwing: LocationProviderError.permissionDenied)
|
||||||
|
case .notDetermined:
|
||||||
|
break
|
||||||
|
@unknown default:
|
||||||
|
permissionContinuation = nil
|
||||||
|
continuation.resume(throwing: LocationProviderError.permissionDenied)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if targetEnvironment(simulator)
|
||||||
|
|
||||||
|
/// 模拟器默认定位服务,使用 Core Location 与系统逆地理编码。
|
||||||
|
typealias LocationProvider = CoreLocationProvider
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
/// 真机统一定位服务,封装高德 `AMapLocationManager`,对齐 Android `LocationProvider`。
|
||||||
@MainActor
|
@MainActor
|
||||||
final class LocationProvider: NSObject, LocationProviding {
|
final class LocationProvider: NSObject, LocationProviding {
|
||||||
|
|
||||||
@ -19,7 +177,7 @@ final class LocationProvider: NSObject, LocationProviding {
|
|||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
/// 请求坐标及高德逆地理编码地址。
|
||||||
func requestSnapshot(desiredAccuracy: CLLocationAccuracy = kCLLocationAccuracyBest) async throws -> HomeLocationSnapshot {
|
func requestSnapshot(desiredAccuracy: CLLocationAccuracy = kCLLocationAccuracyBest) async throws -> HomeLocationSnapshot {
|
||||||
try AMapBootstrap.requireConfigured()
|
try AMapBootstrap.requireConfigured()
|
||||||
try await ensureLocationPermission()
|
try await ensureLocationPermission()
|
||||||
@ -51,7 +209,7 @@ final class LocationProvider: NSObject, LocationProviding {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
/// 请求一次当前位置坐标。
|
||||||
func requestCoordinate(desiredAccuracy: CLLocationAccuracy) async throws -> CLLocationCoordinate2D {
|
func requestCoordinate(desiredAccuracy: CLLocationAccuracy) async throws -> CLLocationCoordinate2D {
|
||||||
try AMapBootstrap.requireConfigured()
|
try AMapBootstrap.requireConfigured()
|
||||||
try await ensureLocationPermission()
|
try await ensureLocationPermission()
|
||||||
@ -78,7 +236,7 @@ final class LocationProvider: NSObject, LocationProviding {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
/// 使用高德服务将坐标解析为地址。
|
||||||
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
|
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
|
||||||
await geocoder.reverseGeocode(latitude: latitude, longitude: longitude)
|
await geocoder.reverseGeocode(latitude: latitude, longitude: longitude)
|
||||||
}
|
}
|
||||||
@ -110,3 +268,5 @@ final class LocationProvider: NSObject, LocationProviding {
|
|||||||
return manager
|
return manager
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|||||||
@ -16,20 +16,25 @@ struct HomeLocationSnapshot: Sendable, Equatable {
|
|||||||
/// 定位服务协议,便于 ViewModel 注入与单元测试 mock。
|
/// 定位服务协议,便于 ViewModel 注入与单元测试 mock。
|
||||||
protocol LocationProviding: Sendable {
|
protocol LocationProviding: Sendable {
|
||||||
/// 按指定精度请求当前坐标与地址。
|
/// 按指定精度请求当前坐标与地址。
|
||||||
|
@MainActor
|
||||||
func requestSnapshot(desiredAccuracy: CLLocationAccuracy) async throws -> HomeLocationSnapshot
|
func requestSnapshot(desiredAccuracy: CLLocationAccuracy) async throws -> HomeLocationSnapshot
|
||||||
/// 按指定精度请求当前坐标。
|
/// 按指定精度请求当前坐标。
|
||||||
|
@MainActor
|
||||||
func requestCoordinate(desiredAccuracy: CLLocationAccuracy) async throws -> CLLocationCoordinate2D
|
func requestCoordinate(desiredAccuracy: CLLocationAccuracy) async throws -> CLLocationCoordinate2D
|
||||||
/// 逆地理编码。
|
/// 逆地理编码。
|
||||||
|
@MainActor
|
||||||
func reverseGeocode(latitude: Double, longitude: Double) async -> String
|
func reverseGeocode(latitude: Double, longitude: Double) async -> String
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LocationProviding {
|
extension LocationProviding {
|
||||||
/// 请求当前坐标与地址,默认使用最高精度以保持既有业务行为。
|
/// 请求当前坐标与地址,默认使用最高精度以保持既有业务行为。
|
||||||
|
@MainActor
|
||||||
func requestSnapshot() async throws -> HomeLocationSnapshot {
|
func requestSnapshot() async throws -> HomeLocationSnapshot {
|
||||||
try await requestSnapshot(desiredAccuracy: kCLLocationAccuracyBest)
|
try await requestSnapshot(desiredAccuracy: kCLLocationAccuracyBest)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 请求当前坐标,默认使用最高精度以保持既有业务行为。
|
/// 请求当前坐标,默认使用最高精度以保持既有业务行为。
|
||||||
|
@MainActor
|
||||||
func requestCoordinate() async throws -> CLLocationCoordinate2D {
|
func requestCoordinate() async throws -> CLLocationCoordinate2D {
|
||||||
try await requestCoordinate(desiredAccuracy: kCLLocationAccuracyBest)
|
try await requestCoordinate(desiredAccuracy: kCLLocationAccuracyBest)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,8 +11,14 @@ protocol MessageCenterServing {
|
|||||||
/// 拉取消息列表。
|
/// 拉取消息列表。
|
||||||
func list(lastId: Int, limit: Int, unread: Int) async throws -> MessageListResponse
|
func list(lastId: Int, limit: Int, unread: Int) async throws -> MessageListResponse
|
||||||
|
|
||||||
/// 标记消息已读。
|
/// 获取当前账号未读消息总数。
|
||||||
func markAsRead(messageId: Int) async throws
|
func unreadCount() async throws -> MessageUnreadCountResponse
|
||||||
|
|
||||||
|
/// 标记消息已读并返回最新未读数量。
|
||||||
|
func markAsRead(messageId: Int) async throws -> MessageUnreadCountResponse
|
||||||
|
|
||||||
|
/// 将当前账号全部消息标记已读。
|
||||||
|
func markAllAsRead() async throws -> MessageReadAllResponse
|
||||||
|
|
||||||
/// 删除消息。
|
/// 删除消息。
|
||||||
func delete(messageId: Int) async throws
|
func delete(messageId: Int) async throws
|
||||||
@ -43,9 +49,16 @@ final class MessageCenterAPI: MessageCenterServing {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// GET /api/app/msg/unread-count
|
||||||
|
func unreadCount() async throws -> MessageUnreadCountResponse {
|
||||||
|
try await client.send(
|
||||||
|
APIRequest(method: .get, path: "/api/app/msg/unread-count")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// POST /api/app/msg/read
|
/// POST /api/app/msg/read
|
||||||
func markAsRead(messageId: Int) async throws {
|
func markAsRead(messageId: Int) async throws -> MessageUnreadCountResponse {
|
||||||
let _: EmptyPayload = try await client.send(
|
try await client.send(
|
||||||
APIRequest(
|
APIRequest(
|
||||||
method: .post,
|
method: .post,
|
||||||
path: "/api/app/msg/read",
|
path: "/api/app/msg/read",
|
||||||
@ -54,6 +67,13 @@ final class MessageCenterAPI: MessageCenterServing {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// POST /api/app/msg/read-all
|
||||||
|
func markAllAsRead() async throws -> MessageReadAllResponse {
|
||||||
|
try await client.send(
|
||||||
|
APIRequest(method: .post, path: "/api/app/msg/read-all")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// POST /api/app/msg/delete
|
/// POST /api/app/msg/delete
|
||||||
func delete(messageId: Int) async throws {
|
func delete(messageId: Int) async throws {
|
||||||
let _: EmptyPayload = try await client.send(
|
let _: EmptyPayload = try await client.send(
|
||||||
|
|||||||
@ -32,6 +32,46 @@ enum MessageJSONValue: Decodable, Hashable, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 消息未读数量响应,用于同步首页红点和桌面图标角标。
|
||||||
|
struct MessageUnreadCountResponse: Decodable, Equatable, Sendable {
|
||||||
|
let unreadCount: Int
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case unreadCount = "unread_count"
|
||||||
|
}
|
||||||
|
|
||||||
|
init(unreadCount: Int = 0) {
|
||||||
|
self.unreadCount = max(unreadCount, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
unreadCount = max(try container.decodeLossyInt(forKey: .unreadCount) ?? 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 全部标记已读响应,包含本次更新数量和操作后的未读数量。
|
||||||
|
struct MessageReadAllResponse: Decodable, Equatable, Sendable {
|
||||||
|
let updatedCount: Int
|
||||||
|
let unreadCount: Int
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case updatedCount = "updated_count"
|
||||||
|
case unreadCount = "unread_count"
|
||||||
|
}
|
||||||
|
|
||||||
|
init(updatedCount: Int = 0, unreadCount: Int = 0) {
|
||||||
|
self.updatedCount = max(updatedCount, 0)
|
||||||
|
self.unreadCount = max(unreadCount, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
updatedCount = max(try container.decodeLossyInt(forKey: .updatedCount) ?? 0, 0)
|
||||||
|
unreadCount = max(try container.decodeLossyInt(forKey: .unreadCount) ?? 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 消息中心列表响应,对齐 Android `MsgResponse`。
|
/// 消息中心列表响应,对齐 Android `MsgResponse`。
|
||||||
struct MessageListResponse: Decodable, Equatable, Sendable {
|
struct MessageListResponse: Decodable, Equatable, Sendable {
|
||||||
let hasMore: Bool
|
let hasMore: Bool
|
||||||
|
|||||||
@ -11,11 +11,13 @@ final class MessageCenterViewModel {
|
|||||||
private(set) var isLoading = false
|
private(set) var isLoading = false
|
||||||
private(set) var isRefreshing = false
|
private(set) var isRefreshing = false
|
||||||
private(set) var isLoadingMore = false
|
private(set) var isLoadingMore = false
|
||||||
|
private(set) var isMarkingAllAsRead = false
|
||||||
private(set) var canLoadMore = false
|
private(set) var canLoadMore = false
|
||||||
|
|
||||||
var onStateChange: (() -> Void)?
|
var onStateChange: (() -> Void)?
|
||||||
var onShowMessage: ((String) -> Void)?
|
var onShowMessage: ((String) -> Void)?
|
||||||
var onOpenDetail: ((MessageItem) -> Void)?
|
var onOpenDetail: ((MessageItem) -> Void)?
|
||||||
|
var onUnreadMessageCountChange: ((Int) -> Void)?
|
||||||
|
|
||||||
private let pageSize = 20
|
private let pageSize = 20
|
||||||
private var lastId = 0
|
private var lastId = 0
|
||||||
@ -55,9 +57,10 @@ final class MessageCenterViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try await api.markAsRead(messageId: id)
|
let response = try await api.markAsRead(messageId: id)
|
||||||
let readItem = item.markedRead()
|
let readItem = item.markedRead()
|
||||||
items = items.map { $0.id == id ? readItem : $0 }
|
items = items.map { $0.id == id ? readItem : $0 }
|
||||||
|
notifyUnreadMessageCountChanged(response.unreadCount)
|
||||||
onOpenDetail?(readItem)
|
onOpenDetail?(readItem)
|
||||||
} catch is CancellationError {
|
} catch is CancellationError {
|
||||||
return
|
return
|
||||||
@ -66,6 +69,30 @@ final class MessageCenterViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 将当前账号的全部消息标记已读,成功时返回服务端最新统计。
|
||||||
|
func markAllAsRead(api: any MessageCenterServing) async -> MessageReadAllResponse? {
|
||||||
|
guard !isLoading, !isMarkingAllAsRead else { return nil }
|
||||||
|
isMarkingAllAsRead = true
|
||||||
|
notifyStateChange()
|
||||||
|
defer {
|
||||||
|
isMarkingAllAsRead = false
|
||||||
|
notifyStateChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let response = try await api.markAllAsRead()
|
||||||
|
items = items.map { $0.isRead ? $0 : $0.markedRead() }
|
||||||
|
notifyUnreadMessageCountChanged(response.unreadCount)
|
||||||
|
onShowMessage?(response.updatedCount > 0 ? "已全部标记为已读" : "暂无未读消息")
|
||||||
|
return response
|
||||||
|
} catch is CancellationError {
|
||||||
|
return nil
|
||||||
|
} catch {
|
||||||
|
onShowMessage?("全部标记已读失败,请稍后重试")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 删除后从本地列表移除消息。
|
/// 删除后从本地列表移除消息。
|
||||||
func removeMessage(id: Int) {
|
func removeMessage(id: Int) {
|
||||||
items.removeAll { $0.id == id }
|
items.removeAll { $0.id == id }
|
||||||
@ -110,6 +137,16 @@ final class MessageCenterViewModel {
|
|||||||
private func notifyStateChange() {
|
private func notifyStateChange() {
|
||||||
onStateChange?()
|
onStateChange?()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func notifyUnreadMessageCountChanged(_ count: Int) {
|
||||||
|
let normalizedCount = max(count, 0)
|
||||||
|
onUnreadMessageCountChange?(normalizedCount)
|
||||||
|
NotificationCenter.default.post(
|
||||||
|
name: NotificationName.unreadMessageCountDidChange,
|
||||||
|
object: nil,
|
||||||
|
userInfo: [NotificationUserInfoKey.unreadCount: normalizedCount]
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 消息详情页 ViewModel,负责删除当前消息。
|
/// 消息详情页 ViewModel,负责删除当前消息。
|
||||||
|
|||||||
@ -293,6 +293,28 @@ final class OrderAPI {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获取指定获客员的分成比例修改记录。
|
||||||
|
func saleUserCommissionRateLogs(
|
||||||
|
saleUserId: Int,
|
||||||
|
page: Int = 1,
|
||||||
|
pageSize: Int = 10
|
||||||
|
) async throws -> CommissionRateLogListResponse {
|
||||||
|
guard saleUserId > 0 else {
|
||||||
|
throw APIError.networkFailed("获客员信息无效")
|
||||||
|
}
|
||||||
|
return try await client.send(
|
||||||
|
APIRequest(
|
||||||
|
method: .get,
|
||||||
|
path: "/api/yf-handset-app/photog/sale-user/commission-rate/logs",
|
||||||
|
queryItems: [
|
||||||
|
URLQueryItem(name: "sale_user_id", value: String(saleUserId)),
|
||||||
|
URLQueryItem(name: "page", value: String(max(1, page))),
|
||||||
|
URLQueryItem(name: "page_size", value: String(min(max(1, pageSize), 50))),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func shootHistory(orderNumber: String) async throws -> MultiTravelShootHistoryResponse {
|
func shootHistory(orderNumber: String) async throws -> MultiTravelShootHistoryResponse {
|
||||||
try await client.send(
|
try await client.send(
|
||||||
APIRequest(
|
APIRequest(
|
||||||
|
|||||||
@ -875,6 +875,93 @@ struct CooperativeSalerEntity: Decodable, Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获客员分成比例修改记录分页响应。
|
||||||
|
struct CommissionRateLogListResponse: Decodable, Equatable {
|
||||||
|
let list: [CommissionRateLogEntity]
|
||||||
|
let total: Int
|
||||||
|
let page: Int
|
||||||
|
let pageSize: Int
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case list
|
||||||
|
case total
|
||||||
|
case page
|
||||||
|
case pageSize = "page_size"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 单条获客员分成比例修改记录。
|
||||||
|
struct CommissionRateLogEntity: Decodable, Equatable, Hashable {
|
||||||
|
let id: Int
|
||||||
|
let bindingId: Int
|
||||||
|
let saleUserId: Int
|
||||||
|
let storeUserId: Int
|
||||||
|
let beforeRate: Int
|
||||||
|
let beforeRateLabel: String
|
||||||
|
let afterRate: Int
|
||||||
|
let afterRateLabel: String
|
||||||
|
let operatorType: String
|
||||||
|
let operatorId: Int
|
||||||
|
let operatorName: String
|
||||||
|
let remark: String
|
||||||
|
let createdAt: String
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case id
|
||||||
|
case bindingId = "binding_id"
|
||||||
|
case saleUserId = "sale_user_id"
|
||||||
|
case storeUserId = "store_user_id"
|
||||||
|
case beforeRate = "before_rate"
|
||||||
|
case beforeRateLabel = "before_rate_label"
|
||||||
|
case afterRate = "after_rate"
|
||||||
|
case afterRateLabel = "after_rate_label"
|
||||||
|
case operatorType = "operator_type"
|
||||||
|
case operatorId = "operator_id"
|
||||||
|
case operatorName = "operator_name"
|
||||||
|
case remark
|
||||||
|
case createdAt = "created_at"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 修改前比例展示文本,优先使用服务端 label。
|
||||||
|
var displayBeforeRate: String {
|
||||||
|
let label = beforeRateLabel.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
return label.isEmpty ? "\(beforeRate)%" : label
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 修改后比例展示文本,优先使用服务端 label。
|
||||||
|
var displayAfterRate: String {
|
||||||
|
let label = afterRateLabel.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
return label.isEmpty ? "\(afterRate)%" : label
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 修改时间展示文本。
|
||||||
|
var metadataLine: String {
|
||||||
|
formattedCreatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 去除首尾空白后的备注;空备注不展示。
|
||||||
|
var displayRemark: String? {
|
||||||
|
let value = remark.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
return value.isEmpty ? nil : value
|
||||||
|
}
|
||||||
|
|
||||||
|
private var formattedCreatedAt: String {
|
||||||
|
let value = createdAt.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !value.isEmpty else { return "—" }
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||||
|
formatter.timeZone = TimeZone(identifier: "Asia/Shanghai")
|
||||||
|
for format in ["yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm"] {
|
||||||
|
formatter.dateFormat = format
|
||||||
|
if let date = formatter.date(from: value) {
|
||||||
|
formatter.dateFormat = "yyyy-MM-dd HH:mm"
|
||||||
|
return formatter.string(from: date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ReferralOrderListResponse: Decodable {
|
struct ReferralOrderListResponse: Decodable {
|
||||||
let total: Int
|
let total: Int
|
||||||
let list: [ReferralOrderEntity]
|
let list: [ReferralOrderEntity]
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
// suixinkan
|
// suixinkan
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import CoreLocation
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
/// 主 Tab 中间扫码按钮结果处理,对齐 Android `MainTabViewModel.signIn`。
|
/// 主 Tab 中间扫码按钮结果处理,对齐 Android `MainTabViewModel.signIn`。
|
||||||
|
|||||||
@ -25,6 +25,17 @@ final class PaymentAPI {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 拉取当前景区收款页的背景、Logo 与标题配置。
|
||||||
|
func payPageConfig(scenicId: Int) async throws -> PayPageConfigResponse {
|
||||||
|
try await client.send(
|
||||||
|
APIRequest(
|
||||||
|
method: .get,
|
||||||
|
path: "/api/yf-handset-app/photog/pay-page-config",
|
||||||
|
queryItems: [URLQueryItem(name: "scenic_id", value: String(scenicId))]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// 拉取近 7 天收款记录。
|
/// 拉取近 7 天收款记录。
|
||||||
func collectionRecord(scenicId: Int) async throws -> RepaymentCollectionRecordResponse {
|
func collectionRecord(scenicId: Int) async throws -> RepaymentCollectionRecordResponse {
|
||||||
try await client.send(
|
try await client.send(
|
||||||
|
|||||||
@ -5,6 +5,83 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
/// 收款页展示配置接口响应。
|
||||||
|
struct PayPageConfigResponse: Decodable, Sendable, Equatable {
|
||||||
|
let background: String
|
||||||
|
let logo: String
|
||||||
|
let title: String
|
||||||
|
let subtitle: String
|
||||||
|
|
||||||
|
init(background: String, logo: String, title: String, subtitle: String) {
|
||||||
|
self.background = background
|
||||||
|
self.logo = logo
|
||||||
|
self.title = title
|
||||||
|
self.subtitle = subtitle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 收款页最终可展示并可持久化的品牌配置。
|
||||||
|
struct PayPageConfig: Codable, Sendable, Equatable {
|
||||||
|
let background: String
|
||||||
|
let logo: String
|
||||||
|
let title: String
|
||||||
|
let subtitle: String
|
||||||
|
|
||||||
|
/// 那拉提景区随包默认配置。
|
||||||
|
static let nalatiDefault = PayPageConfig(
|
||||||
|
background: "",
|
||||||
|
logo: "",
|
||||||
|
title: "那拉提旅拍管理收费平台",
|
||||||
|
subtitle: "那拉提景区"
|
||||||
|
)
|
||||||
|
|
||||||
|
/// 将服务端非空有效字段合并到当前可用配置。
|
||||||
|
func merging(_ response: PayPageConfigResponse) -> PayPageConfig {
|
||||||
|
PayPageConfig(
|
||||||
|
background: Self.validRemoteImageURL(response.background) ?? background,
|
||||||
|
logo: Self.validRemoteImageURL(response.logo) ?? logo,
|
||||||
|
title: response.title.trimmedNonEmpty ?? title,
|
||||||
|
subtitle: response.subtitle.trimmedNonEmpty ?? subtitle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func validRemoteImageURL(_ value: String) -> String? {
|
||||||
|
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard let url = URL(string: trimmed),
|
||||||
|
url.scheme?.lowercased() == "https",
|
||||||
|
url.host != nil else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 收款页品牌化景区判定策略。
|
||||||
|
enum PayPageBrandingPolicy {
|
||||||
|
/// 那拉提景区正式 ID。
|
||||||
|
static let nalatiScenicId = 128
|
||||||
|
|
||||||
|
/// 判断当前景区是否启用品牌收款页。
|
||||||
|
static func isNalati(scenicId: Int) -> Bool {
|
||||||
|
scenicId == nalatiScenicId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 收款页当前账号信息的展示文本格式化规则。
|
||||||
|
enum PaymentAccountDisplayFormatter {
|
||||||
|
/// 解析当前登录摄影师名称,昵称为空时使用实名,仍为空时显示占位符。
|
||||||
|
static func photographerName(userName: String, realName: String) -> String {
|
||||||
|
userName.trimmedNonEmpty ?? realName.trimmedNonEmpty ?? "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 去除空值与重复项后,合并当前账号的全部店铺名称。
|
||||||
|
static func storeNames(_ names: [String]) -> String {
|
||||||
|
var seen = Set<String>()
|
||||||
|
let resolved = names.compactMap(\.trimmedNonEmpty).filter { seen.insert($0).inserted }
|
||||||
|
return resolved.isEmpty ? "-" : resolved.joined(separator: "、")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 摄影师收款码响应,包含静态与动态支付 URL。
|
/// 摄影师收款码响应,包含静态与动态支付 URL。
|
||||||
struct PayCodeResponse: Decodable, Sendable, Equatable {
|
struct PayCodeResponse: Decodable, Sendable, Equatable {
|
||||||
let staticPayURL: String
|
let staticPayURL: String
|
||||||
@ -101,3 +178,10 @@ struct PaymentMerchantInfo: Sendable, Equatable {
|
|||||||
company: "扬州元智享网络科技有限公司"
|
company: "扬州元智享网络科技有限公司"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension String {
|
||||||
|
var trimmedNonEmpty: String? {
|
||||||
|
let value = trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
return value.isEmpty ? nil : value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
51
suixinkan/Features/Payment/Services/PayPageConfigCache.swift
Normal file
51
suixinkan/Features/Payment/Services/PayPageConfigCache.swift
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
//
|
||||||
|
// PayPageConfigCache.swift
|
||||||
|
// suixinkan
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// 收款页品牌配置缓存协议,供 ViewModel 注入持久化实现或测试替身。
|
||||||
|
protocol PayPageConfigCaching {
|
||||||
|
/// 读取指定景区的品牌配置。
|
||||||
|
func config(scenicId: Int) -> PayPageConfig?
|
||||||
|
|
||||||
|
/// 保存指定景区的完整品牌配置。
|
||||||
|
func save(_ config: PayPageConfig, scenicId: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 使用 UserDefaults 按接口环境与景区隔离的收款页品牌配置缓存。
|
||||||
|
final class PayPageConfigCache: PayPageConfigCaching {
|
||||||
|
private static let version = "v1"
|
||||||
|
|
||||||
|
private let defaults: UserDefaults
|
||||||
|
private let environmentNamespace: String
|
||||||
|
private let encoder = JSONEncoder()
|
||||||
|
private let decoder = JSONDecoder()
|
||||||
|
|
||||||
|
/// 使用指定持久化容器和 API 环境创建缓存。
|
||||||
|
init(
|
||||||
|
defaults: UserDefaults = .standard,
|
||||||
|
environment: APIEnvironment = .current
|
||||||
|
) {
|
||||||
|
self.defaults = defaults
|
||||||
|
environmentNamespace = environment.baseURL.host ?? environment.baseURL.absoluteString
|
||||||
|
}
|
||||||
|
|
||||||
|
func config(scenicId: Int) -> PayPageConfig? {
|
||||||
|
guard scenicId > 0,
|
||||||
|
let data = defaults.data(forKey: key(scenicId: scenicId)) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return try? decoder.decode(PayPageConfig.self, from: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func save(_ config: PayPageConfig, scenicId: Int) {
|
||||||
|
guard scenicId > 0, let data = try? encoder.encode(config) else { return }
|
||||||
|
defaults.set(data, forKey: key(scenicId: scenicId))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func key(scenicId: Int) -> String {
|
||||||
|
"payment.pay_page_config.\(Self.version).\(environmentNamespace).\(scenicId)"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,6 +17,9 @@ final class PaymentCollectionDetailsViewModel {
|
|||||||
private(set) var showAmountDialog = false
|
private(set) var showAmountDialog = false
|
||||||
private(set) var amount = ""
|
private(set) var amount = ""
|
||||||
private(set) var remark = ""
|
private(set) var remark = ""
|
||||||
|
private(set) var scenicId = 0
|
||||||
|
private(set) var payPageConfig = PayPageConfig.nalatiDefault
|
||||||
|
private(set) var brandingRefreshVersion = 0
|
||||||
|
|
||||||
let merchantInfo = PaymentMerchantInfo.defaultInfo
|
let merchantInfo = PaymentMerchantInfo.defaultInfo
|
||||||
|
|
||||||
@ -26,9 +29,21 @@ final class PaymentCollectionDetailsViewModel {
|
|||||||
private var staticPayURL = ""
|
private var staticPayURL = ""
|
||||||
private var dynamicPayURL = ""
|
private var dynamicPayURL = ""
|
||||||
private let appStore: AppStore
|
private let appStore: AppStore
|
||||||
|
private let payPageConfigCache: PayPageConfigCaching
|
||||||
|
|
||||||
init(appStore: AppStore = .shared) {
|
init(
|
||||||
|
appStore: AppStore = .shared,
|
||||||
|
payPageConfigCache: PayPageConfigCaching = PayPageConfigCache()
|
||||||
|
) {
|
||||||
self.appStore = appStore
|
self.appStore = appStore
|
||||||
|
self.payPageConfigCache = payPageConfigCache
|
||||||
|
scenicId = appStore.session.currentScenicId
|
||||||
|
scenicName = appStore.session.currentScenicName
|
||||||
|
staffId = appStore.session.userId
|
||||||
|
isVoiceBroadcastOpen = appStore.payment.isOpenReceiveVoice
|
||||||
|
if PayPageBrandingPolicy.isNalati(scenicId: scenicId) {
|
||||||
|
payPageConfig = payPageConfigCache.config(scenicId: scenicId) ?? .nalatiDefault
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasPayCode: Bool {
|
var hasPayCode: Bool {
|
||||||
@ -39,10 +54,32 @@ final class PaymentCollectionDetailsViewModel {
|
|||||||
scenicName.isEmpty ? "暂无景区" : scenicName
|
scenicName.isEmpty ? "暂无景区" : scenicName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var usesNalatiBranding: Bool {
|
||||||
|
PayPageBrandingPolicy.isNalati(scenicId: scenicId)
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayPhotographerName: String {
|
||||||
|
PaymentAccountDisplayFormatter.photographerName(
|
||||||
|
userName: appStore.session.userName,
|
||||||
|
realName: appStore.session.realName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayStoreNames: String {
|
||||||
|
let names = appStore.permissions.rolePermissionList().flatMap(\.store).map(\.name)
|
||||||
|
return PaymentAccountDisplayFormatter.storeNames(names)
|
||||||
|
}
|
||||||
|
|
||||||
func refreshLocalState() {
|
func refreshLocalState() {
|
||||||
|
scenicId = appStore.session.currentScenicId
|
||||||
scenicName = appStore.session.currentScenicName
|
scenicName = appStore.session.currentScenicName
|
||||||
staffId = appStore.session.userId
|
staffId = appStore.session.userId
|
||||||
isVoiceBroadcastOpen = appStore.payment.isOpenReceiveVoice
|
isVoiceBroadcastOpen = appStore.payment.isOpenReceiveVoice
|
||||||
|
if usesNalatiBranding {
|
||||||
|
payPageConfig = payPageConfigCache.config(scenicId: scenicId) ?? .nalatiDefault
|
||||||
|
} else {
|
||||||
|
payPageConfig = .nalatiDefault
|
||||||
|
}
|
||||||
notifyStateChange()
|
notifyStateChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,7 +93,7 @@ final class PaymentCollectionDetailsViewModel {
|
|||||||
notifyStateChange()
|
notifyStateChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
let scenicId = appStore.session.currentScenicId
|
let scenicId = self.scenicId
|
||||||
guard scenicId > 0 else {
|
guard scenicId > 0 else {
|
||||||
clearPayCode()
|
clearPayCode()
|
||||||
return
|
return
|
||||||
@ -72,8 +109,27 @@ final class PaymentCollectionDetailsViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadPayPageConfig(api: PaymentAPI) async {
|
||||||
|
let requestedScenicId = appStore.session.currentScenicId
|
||||||
|
guard PayPageBrandingPolicy.isNalati(scenicId: requestedScenicId) else { return }
|
||||||
|
|
||||||
|
do {
|
||||||
|
let response = try await api.payPageConfig(scenicId: requestedScenicId)
|
||||||
|
guard appStore.session.currentScenicId == requestedScenicId else { return }
|
||||||
|
let resolved = payPageConfig.merging(response)
|
||||||
|
payPageConfigCache.save(resolved, scenicId: requestedScenicId)
|
||||||
|
payPageConfig = resolved
|
||||||
|
brandingRefreshVersion += 1
|
||||||
|
notifyStateChange()
|
||||||
|
} catch {
|
||||||
|
// 品牌配置失败时保留缓存或随包默认值,不影响收款码与页面交互。
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func refresh(api: PaymentAPI) async {
|
func refresh(api: PaymentAPI) async {
|
||||||
await loadPayCode(api: api)
|
async let payCode: Void = loadPayCode(api: api)
|
||||||
|
async let pageConfig: Void = loadPayPageConfig(api: api)
|
||||||
|
_ = await (payCode, pageConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
func showSetAmountDialog() {
|
func showSetAmountDialog() {
|
||||||
|
|||||||
@ -42,5 +42,9 @@
|
|||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>remote-notification</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@ -24,7 +24,18 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||||||
self.window = window
|
self.window = window
|
||||||
(UIApplication.shared.delegate as? AppDelegate)?.window = window
|
(UIApplication.shared.delegate as? AppDelegate)?.window = window
|
||||||
|
|
||||||
|
PushNotificationManager.shared.attach(window: window)
|
||||||
|
|
||||||
registerNotifications()
|
registerNotifications()
|
||||||
|
|
||||||
|
if AppStore.shared.session.isLoggedIn {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
PushNotificationManager.shared.handleLoginCompleted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let response = connectionOptions.notificationResponse {
|
||||||
|
PushNotificationManager.shared.handleNotificationResponse(response)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sceneDidDisconnect(_ scene: UIScene) {
|
func sceneDidDisconnect(_ scene: UIScene) {
|
||||||
@ -43,6 +54,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||||||
_ = WeChatManager.shared.handleContinueUserActivity(userActivity)
|
_ = WeChatManager.shared.handleContinueUserActivity(userActivity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sceneDidBecomeActive(_ scene: UIScene) {
|
||||||
|
PushNotificationManager.shared.retryPendingRegistrationUpload()
|
||||||
|
}
|
||||||
|
|
||||||
private func registerNotifications() {
|
private func registerNotifications() {
|
||||||
NotificationCenter.default.addObserver(
|
NotificationCenter.default.addObserver(
|
||||||
self,
|
self,
|
||||||
@ -62,19 +77,35 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||||||
name: NotificationName.userDidLogin,
|
name: NotificationName.userDidLogin,
|
||||||
object: nil
|
object: nil
|
||||||
)
|
)
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(handleAccountDidSwitch),
|
||||||
|
name: NotificationName.accountDidSwitch,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func handleSessionDidExpire() {
|
@objc private func handleSessionDidExpire() {
|
||||||
|
PushNotificationManager.shared.handleLogout()
|
||||||
AppStore.shared.logout()
|
AppStore.shared.logout()
|
||||||
AppRouter.setRoot(.login, on: window)
|
AppRouter.setRoot(.login, on: window)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func handleUserDidLogout() {
|
@objc private func handleUserDidLogout() {
|
||||||
|
PushNotificationManager.shared.handleLogout()
|
||||||
AppStore.shared.logout()
|
AppStore.shared.logout()
|
||||||
AppRouter.setRoot(.login, on: window)
|
AppRouter.setRoot(.login, on: window)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func handleUserDidLogin() {
|
@objc private func handleUserDidLogin() {
|
||||||
AppRouter.setRoot(.mainTab, on: window)
|
AppRouter.setRoot(.mainTab, on: window)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
PushNotificationManager.shared.handleLoginCompleted()
|
||||||
|
PushNotificationManager.shared.routePendingNotificationIfPossible()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func handleAccountDidSwitch() {
|
||||||
|
PushNotificationManager.shared.handleAccountSwitched()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,284 @@
|
|||||||
|
//
|
||||||
|
// CommissionRateLogViewController.swift
|
||||||
|
// suixinkan
|
||||||
|
//
|
||||||
|
|
||||||
|
import SnapKit
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// 获客员分成比例修改记录页,以时间轴形式展示历史变更。
|
||||||
|
final class CommissionRateLogViewController: BaseViewController {
|
||||||
|
private let viewModel: CommissionRateLogViewModel
|
||||||
|
private let orderAPI: OrderAPI
|
||||||
|
private let navigationTitle: String
|
||||||
|
|
||||||
|
private let tableView = UITableView(frame: .zero, style: .plain)
|
||||||
|
private let emptyLabel = UILabel()
|
||||||
|
private let loadingIndicator = UIActivityIndicatorView(style: .medium)
|
||||||
|
private let footerLabel = UILabel()
|
||||||
|
private var dataSource: UITableViewDiffableDataSource<Int, CommissionRateLogEntity>!
|
||||||
|
|
||||||
|
init(saleUserId: Int, acquirerName: String, orderAPI: OrderAPI? = nil) {
|
||||||
|
viewModel = CommissionRateLogViewModel(saleUserId: saleUserId)
|
||||||
|
let trimmedName = acquirerName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
navigationTitle = trimmedName.isEmpty ? "修改记录" : "给\(trimmedName)的分成记录"
|
||||||
|
self.orderAPI = orderAPI ?? NetworkServices.shared.orderAPI
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
Task { await viewModel.refresh(api: orderAPI) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override func setupNavigationBar() {
|
||||||
|
title = navigationTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
override func setupUI() {
|
||||||
|
view.backgroundColor = AppColor.pageBackground
|
||||||
|
|
||||||
|
tableView.backgroundColor = AppColor.pageBackground
|
||||||
|
tableView.separatorStyle = .none
|
||||||
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
|
tableView.estimatedRowHeight = 132
|
||||||
|
tableView.delegate = self
|
||||||
|
tableView.contentInset.top = AppSpacing.sm
|
||||||
|
tableView.register(
|
||||||
|
CommissionRateLogTimelineCell.self,
|
||||||
|
forCellReuseIdentifier: CommissionRateLogTimelineCell.reuseIdentifier
|
||||||
|
)
|
||||||
|
tableView.refreshControl = UIRefreshControl()
|
||||||
|
tableView.refreshControl?.addTarget(self, action: #selector(refreshPulled), for: .valueChanged)
|
||||||
|
|
||||||
|
dataSource = UITableViewDiffableDataSource<Int, CommissionRateLogEntity>(
|
||||||
|
tableView: tableView
|
||||||
|
) { tableView, indexPath, item in
|
||||||
|
guard let cell = tableView.dequeueReusableCell(
|
||||||
|
withIdentifier: CommissionRateLogTimelineCell.reuseIdentifier,
|
||||||
|
for: indexPath
|
||||||
|
) as? CommissionRateLogTimelineCell else {
|
||||||
|
return UITableViewCell()
|
||||||
|
}
|
||||||
|
cell.configure(with: item, isLatest: indexPath.item == 0)
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyLabel.text = "暂无修改记录"
|
||||||
|
emptyLabel.font = .systemFont(ofSize: 14)
|
||||||
|
emptyLabel.textColor = AppColor.textTertiary
|
||||||
|
emptyLabel.textAlignment = .center
|
||||||
|
emptyLabel.isHidden = true
|
||||||
|
|
||||||
|
loadingIndicator.color = AppColor.primary
|
||||||
|
|
||||||
|
footerLabel.font = .systemFont(ofSize: 13)
|
||||||
|
footerLabel.textColor = AppColor.textTertiary
|
||||||
|
footerLabel.textAlignment = .center
|
||||||
|
|
||||||
|
view.addSubview(tableView)
|
||||||
|
view.addSubview(emptyLabel)
|
||||||
|
view.addSubview(loadingIndicator)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func setupConstraints() {
|
||||||
|
tableView.snp.makeConstraints { make in
|
||||||
|
make.edges.equalTo(view.safeAreaLayoutGuide)
|
||||||
|
}
|
||||||
|
emptyLabel.snp.makeConstraints { make in
|
||||||
|
make.center.equalTo(tableView)
|
||||||
|
}
|
||||||
|
loadingIndicator.snp.makeConstraints { make in
|
||||||
|
make.center.equalTo(tableView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func bindActions() {
|
||||||
|
viewModel.onStateChange = { [weak self] in
|
||||||
|
Task { @MainActor in self?.applyState() }
|
||||||
|
}
|
||||||
|
viewModel.onShowMessage = { [weak self] message in
|
||||||
|
Task { @MainActor in self?.showToast(message) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyState() {
|
||||||
|
if !viewModel.isRefreshing {
|
||||||
|
tableView.refreshControl?.endRefreshing()
|
||||||
|
}
|
||||||
|
if viewModel.initialLoading && viewModel.items.isEmpty {
|
||||||
|
loadingIndicator.startAnimating()
|
||||||
|
} else {
|
||||||
|
loadingIndicator.stopAnimating()
|
||||||
|
}
|
||||||
|
emptyLabel.isHidden = viewModel.initialLoading || viewModel.isRefreshing || !viewModel.items.isEmpty
|
||||||
|
|
||||||
|
var snapshot = NSDiffableDataSourceSnapshot<Int, CommissionRateLogEntity>()
|
||||||
|
snapshot.appendSections([0])
|
||||||
|
snapshot.appendItems(viewModel.items)
|
||||||
|
dataSource.apply(snapshot, animatingDifferences: true)
|
||||||
|
|
||||||
|
guard !viewModel.items.isEmpty else {
|
||||||
|
tableView.tableFooterView = UIView()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
footerLabel.text = viewModel.isLoadingMore ? "加载中…" : (viewModel.canLoadMore ? "" : "没有更多")
|
||||||
|
footerLabel.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: 52)
|
||||||
|
tableView.tableFooterView = footerLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func refreshPulled() {
|
||||||
|
Task { await viewModel.refresh(api: orderAPI) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CommissionRateLogViewController: UITableViewDelegate {
|
||||||
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
guard !viewModel.items.isEmpty,
|
||||||
|
scrollView.contentOffset.y > scrollView.contentSize.height - scrollView.bounds.height - 180 else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Task { await viewModel.loadMore(api: orderAPI) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 分成比例修改记录的时间轴 Cell。
|
||||||
|
final class CommissionRateLogTimelineCell: UITableViewCell {
|
||||||
|
static let reuseIdentifier = "CommissionRateLogTimelineCell"
|
||||||
|
|
||||||
|
private let railView = UIView()
|
||||||
|
private let dotView = UIView()
|
||||||
|
private let cardView = UIView()
|
||||||
|
private let metadataLabel = UILabel()
|
||||||
|
private let latestBadgeLabel = UILabel()
|
||||||
|
private let beforeRateLabel = UILabel()
|
||||||
|
private let arrowImageView = UIImageView(image: UIImage(systemName: "arrow.right"))
|
||||||
|
private let afterRateLabel = UILabel()
|
||||||
|
private let remarkLabel = UILabel()
|
||||||
|
private let contentStack = UIStackView()
|
||||||
|
|
||||||
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
|
selectionStyle = .none
|
||||||
|
backgroundColor = AppColor.pageBackground
|
||||||
|
contentView.backgroundColor = AppColor.pageBackground
|
||||||
|
|
||||||
|
railView.backgroundColor = AppColor.primary.withAlphaComponent(0.16)
|
||||||
|
dotView.backgroundColor = AppColor.primary
|
||||||
|
dotView.layer.cornerRadius = 4
|
||||||
|
|
||||||
|
cardView.backgroundColor = .white
|
||||||
|
cardView.layer.cornerRadius = AppRadius.md
|
||||||
|
cardView.layer.shadowColor = UIColor.black.cgColor
|
||||||
|
cardView.layer.shadowOpacity = 0.04
|
||||||
|
cardView.layer.shadowOffset = CGSize(width: 0, height: 1)
|
||||||
|
cardView.layer.shadowRadius = 2
|
||||||
|
|
||||||
|
metadataLabel.font = .systemFont(ofSize: 12)
|
||||||
|
metadataLabel.textColor = AppColor.textSecondary
|
||||||
|
metadataLabel.numberOfLines = 2
|
||||||
|
|
||||||
|
latestBadgeLabel.text = "最新"
|
||||||
|
latestBadgeLabel.font = .systemFont(ofSize: 11, weight: .semibold)
|
||||||
|
latestBadgeLabel.textColor = AppColor.primary
|
||||||
|
latestBadgeLabel.textAlignment = .center
|
||||||
|
latestBadgeLabel.backgroundColor = AppColor.primary.withAlphaComponent(0.10)
|
||||||
|
latestBadgeLabel.layer.cornerRadius = 9
|
||||||
|
latestBadgeLabel.clipsToBounds = true
|
||||||
|
latestBadgeLabel.isHidden = true
|
||||||
|
latestBadgeLabel.setContentHuggingPriority(.required, for: .horizontal)
|
||||||
|
latestBadgeLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||||
|
latestBadgeLabel.snp.makeConstraints { make in
|
||||||
|
make.width.equalTo(38)
|
||||||
|
make.height.equalTo(18)
|
||||||
|
}
|
||||||
|
|
||||||
|
let headerStack = UIStackView(arrangedSubviews: [metadataLabel, latestBadgeLabel])
|
||||||
|
headerStack.axis = .horizontal
|
||||||
|
headerStack.alignment = .top
|
||||||
|
headerStack.spacing = AppSpacing.sm
|
||||||
|
|
||||||
|
[beforeRateLabel, afterRateLabel].forEach { label in
|
||||||
|
label.font = .systemFont(ofSize: 17, weight: .semibold)
|
||||||
|
label.textColor = AppColor.textPrimary
|
||||||
|
label.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||||
|
}
|
||||||
|
afterRateLabel.textColor = AppColor.primary
|
||||||
|
arrowImageView.tintColor = AppColor.textTertiary
|
||||||
|
arrowImageView.contentMode = .scaleAspectFit
|
||||||
|
|
||||||
|
let rateStack = UIStackView(arrangedSubviews: [beforeRateLabel, arrowImageView, afterRateLabel, UIView()])
|
||||||
|
rateStack.axis = .horizontal
|
||||||
|
rateStack.alignment = .center
|
||||||
|
rateStack.spacing = AppSpacing.sm
|
||||||
|
arrowImageView.snp.makeConstraints { make in
|
||||||
|
make.size.equalTo(16)
|
||||||
|
}
|
||||||
|
|
||||||
|
remarkLabel.font = .systemFont(ofSize: 13)
|
||||||
|
remarkLabel.textColor = AppColor.textSecondary
|
||||||
|
remarkLabel.numberOfLines = 0
|
||||||
|
|
||||||
|
contentStack.axis = .vertical
|
||||||
|
contentStack.spacing = AppSpacing.sm
|
||||||
|
contentStack.addArrangedSubview(headerStack)
|
||||||
|
contentStack.addArrangedSubview(rateStack)
|
||||||
|
contentStack.addArrangedSubview(remarkLabel)
|
||||||
|
|
||||||
|
contentView.addSubview(railView)
|
||||||
|
contentView.addSubview(dotView)
|
||||||
|
contentView.addSubview(cardView)
|
||||||
|
cardView.addSubview(contentStack)
|
||||||
|
|
||||||
|
railView.snp.makeConstraints { make in
|
||||||
|
make.leading.equalToSuperview().offset(24)
|
||||||
|
make.top.bottom.equalToSuperview()
|
||||||
|
make.width.equalTo(1.5)
|
||||||
|
}
|
||||||
|
dotView.snp.makeConstraints { make in
|
||||||
|
make.centerX.equalTo(railView)
|
||||||
|
make.top.equalToSuperview().offset(20)
|
||||||
|
make.size.equalTo(8)
|
||||||
|
}
|
||||||
|
cardView.snp.makeConstraints { make in
|
||||||
|
make.leading.equalTo(railView.snp.trailing).offset(16)
|
||||||
|
make.trailing.equalToSuperview().inset(14)
|
||||||
|
make.top.equalToSuperview().offset(4)
|
||||||
|
make.bottom.equalToSuperview().inset(8)
|
||||||
|
}
|
||||||
|
contentStack.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview().inset(15)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 绑定比例变化、时间及可选备注。
|
||||||
|
func configure(with item: CommissionRateLogEntity, isLatest: Bool) {
|
||||||
|
metadataLabel.text = item.metadataLine
|
||||||
|
latestBadgeLabel.isHidden = !isLatest
|
||||||
|
beforeRateLabel.text = item.displayBeforeRate
|
||||||
|
afterRateLabel.text = item.displayAfterRate
|
||||||
|
if let remark = item.displayRemark {
|
||||||
|
remarkLabel.text = "备注:\(remark)"
|
||||||
|
remarkLabel.isHidden = false
|
||||||
|
} else {
|
||||||
|
remarkLabel.text = nil
|
||||||
|
remarkLabel.isHidden = true
|
||||||
|
}
|
||||||
|
accessibilityLabel = [
|
||||||
|
isLatest ? "最新记录" : nil,
|
||||||
|
item.metadataLine,
|
||||||
|
"\(item.displayBeforeRate)修改为\(item.displayAfterRate)",
|
||||||
|
item.displayRemark.map { "备注\($0)" },
|
||||||
|
].compactMap { $0 }.joined(separator: ",")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -31,12 +31,13 @@ final class CooperationAcquirerViewController: BaseViewController, UITableViewDa
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func setupUI() {
|
override func setupUI() {
|
||||||
|
view.backgroundColor = UIColor(hex: 0xF6F7FA)
|
||||||
tableView.backgroundColor = .clear
|
tableView.backgroundColor = .clear
|
||||||
tableView.separatorStyle = .none
|
tableView.separatorStyle = .none
|
||||||
tableView.rowHeight = UITableView.automaticDimension
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
tableView.estimatedRowHeight = 142
|
tableView.estimatedRowHeight = 224
|
||||||
tableView.contentInset.top = AppSpacing.sm
|
tableView.contentInset = UIEdgeInsets(top: AppSpacing.sm, left: 0, bottom: AppSpacing.md, right: 0)
|
||||||
tableView.verticalScrollIndicatorInsets.top = AppSpacing.sm
|
tableView.verticalScrollIndicatorInsets = UIEdgeInsets(top: AppSpacing.sm, left: 0, bottom: AppSpacing.md, right: 0)
|
||||||
tableView.dataSource = self
|
tableView.dataSource = self
|
||||||
tableView.delegate = self
|
tableView.delegate = self
|
||||||
tableView.register(CooperationAcquirerCell.self, forCellReuseIdentifier: CooperationAcquirerCell.reuseIdentifier)
|
tableView.register(CooperationAcquirerCell.self, forCellReuseIdentifier: CooperationAcquirerCell.reuseIdentifier)
|
||||||
@ -245,6 +246,17 @@ final class CooperationAcquirerViewController: BaseViewController, UITableViewDa
|
|||||||
cell.onEditCommission = { [weak self] in
|
cell.onEditCommission = { [weak self] in
|
||||||
self?.presentCommissionDialog(for: acquirer)
|
self?.presentCommissionDialog(for: acquirer)
|
||||||
}
|
}
|
||||||
|
cell.onViewCommissionLogs = { [weak self] in
|
||||||
|
guard acquirer.saleUserId > 0 else {
|
||||||
|
self?.showToast("获客员信息无效")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let controller = CommissionRateLogViewController(
|
||||||
|
saleUserId: acquirer.saleUserId,
|
||||||
|
acquirerName: acquirer.displayName
|
||||||
|
)
|
||||||
|
self?.navigationController?.pushViewController(controller, animated: true)
|
||||||
|
}
|
||||||
cell.onCall = {
|
cell.onCall = {
|
||||||
let phone = acquirer.displayPhone.trimmingCharacters(in: .whitespacesAndNewlines)
|
let phone = acquirer.displayPhone.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
guard !phone.isEmpty, let url = URL(string: "tel://\(phone)") else { return }
|
guard !phone.isEmpty, let url = URL(string: "tel://\(phone)") else { return }
|
||||||
|
|||||||
@ -445,22 +445,25 @@ final class CooperationAcquirerCell: UITableViewCell {
|
|||||||
|
|
||||||
var onEditRemark: (() -> Void)?
|
var onEditRemark: (() -> Void)?
|
||||||
var onEditCommission: (() -> Void)?
|
var onEditCommission: (() -> Void)?
|
||||||
|
var onViewCommissionLogs: (() -> Void)?
|
||||||
var onCall: (() -> Void)?
|
var onCall: (() -> Void)?
|
||||||
|
|
||||||
private let cardView = UIView()
|
private let cardView = UIView()
|
||||||
private let avatarContainer = UIView()
|
private let avatarContainer = UIView()
|
||||||
private let avatarIconView = UIImageView(image: UIImage(systemName: "person.fill"))
|
private let avatarIconView = UIImageView(image: UIImage(systemName: "person.fill"))
|
||||||
private let nameTitleLabel = UILabel()
|
|
||||||
private let nameValueLabel = UILabel()
|
private let nameValueLabel = UILabel()
|
||||||
private let editRemarkButton = UIButton(type: .system)
|
|
||||||
private let phoneTitleLabel = UILabel()
|
|
||||||
private let phoneValueLabel = UILabel()
|
private let phoneValueLabel = UILabel()
|
||||||
private let callButton = UIButton(type: .system)
|
private let callButton = UIButton(type: .system)
|
||||||
private let bindTimeTitleLabel = UILabel()
|
private let commissionContainer = UIView()
|
||||||
private let bindTimeValueLabel = UILabel()
|
|
||||||
private let commissionTitleLabel = UILabel()
|
private let commissionTitleLabel = UILabel()
|
||||||
private let commissionValueLabel = UILabel()
|
private let commissionValueLabel = UILabel()
|
||||||
|
private let bindTimeIconView = UIImageView(image: UIImage(systemName: "calendar"))
|
||||||
|
private let bindTimeValueLabel = UILabel()
|
||||||
|
private let dividerView = UIView()
|
||||||
|
private let editRemarkButton = UIButton(type: .system)
|
||||||
private let editCommissionButton = UIButton(type: .system)
|
private let editCommissionButton = UIButton(type: .system)
|
||||||
|
private let commissionLogButton = UIButton(type: .system)
|
||||||
|
private let actionStack = UIStackView()
|
||||||
|
|
||||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
@ -469,135 +472,180 @@ final class CooperationAcquirerCell: UITableViewCell {
|
|||||||
contentView.backgroundColor = .clear
|
contentView.backgroundColor = .clear
|
||||||
|
|
||||||
cardView.backgroundColor = .white
|
cardView.backgroundColor = .white
|
||||||
cardView.layer.cornerRadius = AppRadius.md
|
cardView.layer.cornerRadius = AppRadius.xl
|
||||||
|
cardView.layer.borderWidth = 0.5
|
||||||
|
cardView.layer.borderColor = AppColor.cardOutline.cgColor
|
||||||
cardView.layer.shadowColor = UIColor.black.cgColor
|
cardView.layer.shadowColor = UIColor.black.cgColor
|
||||||
cardView.layer.shadowOpacity = 0.04
|
cardView.layer.shadowOpacity = 0.04
|
||||||
cardView.layer.shadowOffset = CGSize(width: 0, height: 1)
|
cardView.layer.shadowOffset = CGSize(width: 0, height: 3)
|
||||||
cardView.layer.shadowRadius = 2
|
cardView.layer.shadowRadius = 8
|
||||||
|
cardView.accessibilityIdentifier = "cooperation_acquirer_card"
|
||||||
|
|
||||||
avatarContainer.backgroundColor = AppColor.primary.withAlphaComponent(0.08)
|
avatarContainer.backgroundColor = AppColor.primary.withAlphaComponent(0.08)
|
||||||
avatarContainer.layer.cornerRadius = AppRadius.sm
|
avatarContainer.layer.cornerRadius = 24
|
||||||
avatarContainer.clipsToBounds = true
|
avatarContainer.clipsToBounds = true
|
||||||
|
|
||||||
avatarIconView.tintColor = AppColor.primary
|
avatarIconView.tintColor = AppColor.primary
|
||||||
avatarIconView.contentMode = .scaleAspectFit
|
avatarIconView.contentMode = .scaleAspectFit
|
||||||
|
|
||||||
configureTitleLabel(nameTitleLabel, text: "获客员名称")
|
nameValueLabel.font = .systemFont(ofSize: 17, weight: .semibold)
|
||||||
configureTitleLabel(phoneTitleLabel, text: "手机号")
|
|
||||||
configureTitleLabel(bindTimeTitleLabel, text: "绑定时间")
|
|
||||||
configureTitleLabel(commissionTitleLabel, text: "分成比例")
|
|
||||||
|
|
||||||
nameValueLabel.font = .systemFont(ofSize: 15, weight: .bold)
|
|
||||||
nameValueLabel.textColor = AppColor.textPrimary
|
nameValueLabel.textColor = AppColor.textPrimary
|
||||||
nameValueLabel.numberOfLines = 1
|
nameValueLabel.numberOfLines = 1
|
||||||
nameValueLabel.lineBreakMode = .byTruncatingTail
|
nameValueLabel.lineBreakMode = .byTruncatingTail
|
||||||
|
nameValueLabel.accessibilityIdentifier = "cooperation_acquirer_name"
|
||||||
|
|
||||||
[phoneValueLabel, bindTimeValueLabel, commissionValueLabel].forEach { label in
|
phoneValueLabel.font = .systemFont(ofSize: 14)
|
||||||
label.font = .systemFont(ofSize: 13)
|
phoneValueLabel.textColor = AppColor.textTertiary
|
||||||
label.textColor = AppColor.textSecondary
|
phoneValueLabel.numberOfLines = 1
|
||||||
label.numberOfLines = 1
|
phoneValueLabel.lineBreakMode = .byTruncatingTail
|
||||||
label.lineBreakMode = .byTruncatingTail
|
phoneValueLabel.accessibilityIdentifier = "cooperation_acquirer_phone"
|
||||||
}
|
|
||||||
commissionValueLabel.font = .systemFont(ofSize: 13, weight: .medium)
|
|
||||||
|
|
||||||
editRemarkButton.setTitle("修改备注", for: .normal)
|
|
||||||
editRemarkButton.titleLabel?.font = .systemFont(ofSize: 13)
|
|
||||||
editRemarkButton.setTitleColor(AppColor.primary, for: .normal)
|
|
||||||
editRemarkButton.setConfigurationContentInsets(
|
|
||||||
NSDirectionalEdgeInsets(top: 16, leading: 14, bottom: 16, trailing: 14)
|
|
||||||
)
|
|
||||||
editRemarkButton.addTarget(self, action: #selector(editRemarkTapped), for: .touchUpInside)
|
|
||||||
|
|
||||||
callButton.setImage(UIImage(systemName: "phone.fill"), for: .normal)
|
callButton.setImage(UIImage(systemName: "phone.fill"), for: .normal)
|
||||||
callButton.tintColor = AppColor.primary
|
callButton.tintColor = AppColor.primary
|
||||||
callButton.backgroundColor = AppColor.primaryLight
|
callButton.backgroundColor = AppColor.primaryLight
|
||||||
callButton.layer.cornerRadius = 15
|
callButton.layer.cornerRadius = AppSpacing.minTouchTarget / 2
|
||||||
|
callButton.layer.borderWidth = 0.5
|
||||||
|
callButton.layer.borderColor = AppColor.primary.withAlphaComponent(0.12).cgColor
|
||||||
callButton.imageView?.contentMode = .scaleAspectFit
|
callButton.imageView?.contentMode = .scaleAspectFit
|
||||||
|
callButton.accessibilityLabel = "拨打获客员电话"
|
||||||
|
callButton.accessibilityIdentifier = "cooperation_acquirer_call"
|
||||||
callButton.addTarget(self, action: #selector(callTapped), for: .touchUpInside)
|
callButton.addTarget(self, action: #selector(callTapped), for: .touchUpInside)
|
||||||
|
|
||||||
editCommissionButton.setTitle("修改比例", for: .normal)
|
commissionContainer.backgroundColor = AppColor.primary.withAlphaComponent(0.06)
|
||||||
editCommissionButton.titleLabel?.font = .systemFont(ofSize: 13)
|
commissionContainer.layer.cornerRadius = AppRadius.lg
|
||||||
editCommissionButton.setTitleColor(AppColor.primary, for: .normal)
|
|
||||||
editCommissionButton.setConfigurationContentInsets(
|
commissionTitleLabel.text = "分成比例"
|
||||||
NSDirectionalEdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8)
|
commissionTitleLabel.font = .systemFont(ofSize: 12)
|
||||||
|
commissionTitleLabel.textColor = AppColor.textTertiary
|
||||||
|
|
||||||
|
commissionValueLabel.font = .systemFont(ofSize: 24, weight: .bold)
|
||||||
|
commissionValueLabel.textColor = AppColor.primary
|
||||||
|
commissionValueLabel.numberOfLines = 1
|
||||||
|
commissionValueLabel.adjustsFontSizeToFitWidth = true
|
||||||
|
commissionValueLabel.minimumScaleFactor = 0.75
|
||||||
|
commissionValueLabel.accessibilityIdentifier = "cooperation_acquirer_commission_rate"
|
||||||
|
|
||||||
|
bindTimeIconView.tintColor = AppColor.textTertiary
|
||||||
|
bindTimeIconView.contentMode = .scaleAspectFit
|
||||||
|
|
||||||
|
bindTimeValueLabel.font = .systemFont(ofSize: 13)
|
||||||
|
bindTimeValueLabel.textColor = AppColor.textTertiary
|
||||||
|
bindTimeValueLabel.numberOfLines = 2
|
||||||
|
bindTimeValueLabel.lineBreakMode = .byTruncatingTail
|
||||||
|
bindTimeValueLabel.accessibilityIdentifier = "cooperation_acquirer_bind_time"
|
||||||
|
|
||||||
|
dividerView.backgroundColor = AppColor.cardOutline
|
||||||
|
|
||||||
|
configureActionButton(
|
||||||
|
editRemarkButton,
|
||||||
|
title: "修改备注",
|
||||||
|
isPrimary: false,
|
||||||
|
accessibilityIdentifier: "cooperation_acquirer_edit_remark"
|
||||||
|
)
|
||||||
|
editRemarkButton.addTarget(self, action: #selector(editRemarkTapped), for: .touchUpInside)
|
||||||
|
|
||||||
|
configureActionButton(
|
||||||
|
commissionLogButton,
|
||||||
|
title: "修改记录",
|
||||||
|
isPrimary: false,
|
||||||
|
accessibilityIdentifier: "cooperation_acquirer_commission_logs"
|
||||||
|
)
|
||||||
|
commissionLogButton.accessibilityLabel = "查看分成比例修改记录"
|
||||||
|
commissionLogButton.addTarget(self, action: #selector(commissionLogTapped), for: .touchUpInside)
|
||||||
|
|
||||||
|
configureActionButton(
|
||||||
|
editCommissionButton,
|
||||||
|
title: "修改比例",
|
||||||
|
isPrimary: true,
|
||||||
|
accessibilityIdentifier: "cooperation_acquirer_edit_commission"
|
||||||
)
|
)
|
||||||
editCommissionButton.addTarget(self, action: #selector(editCommissionTapped), for: .touchUpInside)
|
editCommissionButton.addTarget(self, action: #selector(editCommissionTapped), for: .touchUpInside)
|
||||||
|
|
||||||
|
actionStack.axis = .horizontal
|
||||||
|
actionStack.alignment = .fill
|
||||||
|
actionStack.distribution = .fillEqually
|
||||||
|
actionStack.spacing = AppSpacing.xs
|
||||||
|
actionStack.addArrangedSubview(editRemarkButton)
|
||||||
|
actionStack.addArrangedSubview(commissionLogButton)
|
||||||
|
actionStack.addArrangedSubview(editCommissionButton)
|
||||||
|
|
||||||
contentView.addSubview(cardView)
|
contentView.addSubview(cardView)
|
||||||
cardView.addSubview(avatarContainer)
|
cardView.addSubview(avatarContainer)
|
||||||
avatarContainer.addSubview(avatarIconView)
|
avatarContainer.addSubview(avatarIconView)
|
||||||
cardView.addSubview(nameTitleLabel)
|
|
||||||
cardView.addSubview(nameValueLabel)
|
cardView.addSubview(nameValueLabel)
|
||||||
cardView.addSubview(editRemarkButton)
|
|
||||||
cardView.addSubview(phoneTitleLabel)
|
|
||||||
cardView.addSubview(phoneValueLabel)
|
cardView.addSubview(phoneValueLabel)
|
||||||
cardView.addSubview(callButton)
|
cardView.addSubview(callButton)
|
||||||
cardView.addSubview(bindTimeTitleLabel)
|
cardView.addSubview(commissionContainer)
|
||||||
|
commissionContainer.addSubview(commissionTitleLabel)
|
||||||
|
commissionContainer.addSubview(commissionValueLabel)
|
||||||
|
cardView.addSubview(bindTimeIconView)
|
||||||
cardView.addSubview(bindTimeValueLabel)
|
cardView.addSubview(bindTimeValueLabel)
|
||||||
cardView.addSubview(commissionTitleLabel)
|
cardView.addSubview(dividerView)
|
||||||
cardView.addSubview(commissionValueLabel)
|
cardView.addSubview(actionStack)
|
||||||
cardView.addSubview(editCommissionButton)
|
|
||||||
|
|
||||||
cardView.snp.makeConstraints { make in
|
cardView.snp.makeConstraints { make in
|
||||||
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 14, bottom: AppSpacing.sm, right: 14))
|
make.edges.equalToSuperview().inset(
|
||||||
|
UIEdgeInsets(top: 0, left: AppSpacing.md, bottom: AppSpacing.sm, right: AppSpacing.md)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
avatarContainer.snp.makeConstraints { make in
|
avatarContainer.snp.makeConstraints { make in
|
||||||
make.leading.equalToSuperview().offset(14)
|
make.top.leading.equalToSuperview().inset(AppSpacing.md)
|
||||||
make.centerY.equalToSuperview()
|
make.size.equalTo(48)
|
||||||
make.size.equalTo(44)
|
|
||||||
}
|
}
|
||||||
avatarIconView.snp.makeConstraints { make in
|
avatarIconView.snp.makeConstraints { make in
|
||||||
make.center.equalToSuperview()
|
make.center.equalToSuperview()
|
||||||
make.size.equalTo(22)
|
make.size.equalTo(24)
|
||||||
}
|
|
||||||
nameTitleLabel.snp.makeConstraints { make in
|
|
||||||
make.top.equalToSuperview().offset(AppSpacing.md)
|
|
||||||
make.leading.equalTo(avatarContainer.snp.trailing).offset(AppSpacing.sm)
|
|
||||||
}
|
}
|
||||||
nameValueLabel.snp.makeConstraints { make in
|
nameValueLabel.snp.makeConstraints { make in
|
||||||
make.centerY.equalTo(nameTitleLabel)
|
make.top.equalTo(avatarContainer).offset(2)
|
||||||
make.leading.equalTo(nameTitleLabel.snp.trailing).offset(AppSpacing.sm)
|
make.leading.equalTo(avatarContainer.snp.trailing).offset(AppSpacing.sm)
|
||||||
make.trailing.lessThanOrEqualTo(editRemarkButton.snp.leading).offset(-AppSpacing.xs)
|
make.trailing.lessThanOrEqualTo(callButton.snp.leading).offset(-AppSpacing.sm)
|
||||||
}
|
|
||||||
editRemarkButton.snp.makeConstraints { make in
|
|
||||||
make.top.trailing.equalToSuperview()
|
|
||||||
}
|
|
||||||
phoneTitleLabel.snp.makeConstraints { make in
|
|
||||||
make.top.equalTo(nameTitleLabel.snp.bottom).offset(AppSpacing.xs)
|
|
||||||
make.leading.equalTo(nameTitleLabel)
|
|
||||||
}
|
}
|
||||||
phoneValueLabel.snp.makeConstraints { make in
|
phoneValueLabel.snp.makeConstraints { make in
|
||||||
make.centerY.equalTo(phoneTitleLabel)
|
make.top.equalTo(nameValueLabel.snp.bottom).offset(AppSpacing.xxs)
|
||||||
make.leading.equalTo(phoneTitleLabel.snp.trailing).offset(AppSpacing.sm)
|
make.leading.equalTo(nameValueLabel)
|
||||||
make.trailing.lessThanOrEqualTo(callButton.snp.leading).offset(-AppSpacing.xs)
|
make.trailing.lessThanOrEqualTo(callButton.snp.leading).offset(-AppSpacing.sm)
|
||||||
}
|
}
|
||||||
callButton.snp.makeConstraints { make in
|
callButton.snp.makeConstraints { make in
|
||||||
make.trailing.equalToSuperview().inset(14)
|
make.top.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||||
make.centerY.equalTo(phoneTitleLabel)
|
make.size.equalTo(AppSpacing.minTouchTarget)
|
||||||
make.size.equalTo(30)
|
|
||||||
}
|
}
|
||||||
bindTimeTitleLabel.snp.makeConstraints { make in
|
commissionContainer.snp.makeConstraints { make in
|
||||||
make.top.equalTo(phoneTitleLabel.snp.bottom).offset(AppSpacing.xs)
|
make.top.equalTo(avatarContainer.snp.bottom).offset(14)
|
||||||
make.leading.equalTo(nameTitleLabel)
|
make.leading.equalToSuperview().inset(AppSpacing.md)
|
||||||
}
|
make.width.equalTo(120)
|
||||||
bindTimeValueLabel.snp.makeConstraints { make in
|
make.height.equalTo(64)
|
||||||
make.centerY.equalTo(bindTimeTitleLabel)
|
|
||||||
make.leading.equalTo(bindTimeTitleLabel.snp.trailing).offset(AppSpacing.sm)
|
|
||||||
make.trailing.lessThanOrEqualToSuperview().inset(14)
|
|
||||||
}
|
}
|
||||||
commissionTitleLabel.snp.makeConstraints { make in
|
commissionTitleLabel.snp.makeConstraints { make in
|
||||||
make.top.equalTo(bindTimeTitleLabel.snp.bottom).offset(AppSpacing.xs)
|
make.top.equalToSuperview().offset(AppSpacing.xs)
|
||||||
make.leading.equalTo(nameTitleLabel)
|
make.centerX.equalToSuperview()
|
||||||
make.bottom.equalToSuperview().inset(AppSpacing.md)
|
|
||||||
}
|
}
|
||||||
commissionValueLabel.snp.makeConstraints { make in
|
commissionValueLabel.snp.makeConstraints { make in
|
||||||
make.centerY.equalTo(commissionTitleLabel)
|
make.top.equalTo(commissionTitleLabel.snp.bottom).offset(2)
|
||||||
make.leading.equalTo(commissionTitleLabel.snp.trailing).offset(AppSpacing.sm)
|
make.centerX.equalToSuperview()
|
||||||
make.trailing.lessThanOrEqualTo(editCommissionButton.snp.leading).offset(-AppSpacing.xs)
|
make.leading.greaterThanOrEqualToSuperview().offset(AppSpacing.xs)
|
||||||
|
make.trailing.lessThanOrEqualToSuperview().inset(AppSpacing.xs)
|
||||||
}
|
}
|
||||||
editCommissionButton.snp.makeConstraints { make in
|
bindTimeIconView.snp.makeConstraints { make in
|
||||||
make.centerY.equalTo(commissionTitleLabel)
|
make.leading.equalTo(commissionContainer.snp.trailing).offset(AppSpacing.md)
|
||||||
make.trailing.equalToSuperview().inset(AppSpacing.xs)
|
make.centerY.equalTo(commissionContainer)
|
||||||
|
make.size.equalTo(18)
|
||||||
|
}
|
||||||
|
bindTimeValueLabel.snp.makeConstraints { make in
|
||||||
|
make.leading.equalTo(bindTimeIconView.snp.trailing).offset(AppSpacing.xs)
|
||||||
|
make.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||||
|
make.centerY.equalTo(bindTimeIconView)
|
||||||
|
}
|
||||||
|
dividerView.snp.makeConstraints { make in
|
||||||
|
make.top.equalTo(commissionContainer.snp.bottom).offset(14)
|
||||||
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||||
|
make.height.equalTo(0.5)
|
||||||
|
}
|
||||||
|
actionStack.snp.makeConstraints { make in
|
||||||
|
make.top.equalTo(dividerView.snp.bottom).offset(AppSpacing.sm)
|
||||||
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||||
|
make.height.equalTo(AppSpacing.minTouchTarget)
|
||||||
|
make.bottom.equalToSuperview().inset(14)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -610,26 +658,52 @@ final class CooperationAcquirerCell: UITableViewCell {
|
|||||||
super.prepareForReuse()
|
super.prepareForReuse()
|
||||||
onEditRemark = nil
|
onEditRemark = nil
|
||||||
onEditCommission = nil
|
onEditCommission = nil
|
||||||
|
onViewCommissionLogs = nil
|
||||||
onCall = nil
|
onCall = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func configure(with acquirer: CooperativeSalerEntity) {
|
func configure(with acquirer: CooperativeSalerEntity) {
|
||||||
nameValueLabel.text = acquirer.displayName.isEmpty ? "—" : acquirer.displayName
|
nameValueLabel.text = acquirer.displayName.isEmpty ? "—" : acquirer.displayName
|
||||||
phoneValueLabel.text = acquirer.displayPhone.isEmpty ? "—" : CooperationOrderPhoneMask.mask(acquirer.displayPhone)
|
phoneValueLabel.text = acquirer.displayPhone.isEmpty ? "—" : CooperationOrderPhoneMask.mask(acquirer.displayPhone)
|
||||||
bindTimeValueLabel.text = acquirer.displayBindTime.isEmpty ? "—" : acquirer.displayBindTime
|
let bindTime = acquirer.displayBindTime.isEmpty ? "—" : Self.minutePrecisionTime(acquirer.displayBindTime)
|
||||||
|
bindTimeValueLabel.text = "绑定于 \(bindTime)"
|
||||||
commissionValueLabel.text = acquirer.displayCommissionRate.isEmpty ? "—" : acquirer.displayCommissionRate
|
commissionValueLabel.text = acquirer.displayCommissionRate.isEmpty ? "—" : acquirer.displayCommissionRate
|
||||||
}
|
}
|
||||||
|
|
||||||
private func configureTitleLabel(_ label: UILabel, text: String) {
|
private func configureActionButton(
|
||||||
label.text = text
|
_ button: UIButton,
|
||||||
label.font = .systemFont(ofSize: 13)
|
title: String,
|
||||||
label.textColor = AppColor.textTertiary
|
isPrimary: Bool,
|
||||||
label.setContentHuggingPriority(.required, for: .horizontal)
|
accessibilityIdentifier: String
|
||||||
label.setContentCompressionResistancePriority(.required, for: .horizontal)
|
) {
|
||||||
|
var configuration = UIButton.Configuration.plain()
|
||||||
|
configuration.title = title
|
||||||
|
configuration.baseForegroundColor = isPrimary ? AppColor.primary : UIColor(hex: 0x5F6673)
|
||||||
|
configuration.background.backgroundColor = isPrimary ? AppColor.primaryLight : .white
|
||||||
|
configuration.background.strokeColor = isPrimary
|
||||||
|
? AppColor.primary.withAlphaComponent(0.35)
|
||||||
|
: UIColor(hex: 0xD9DEE7)
|
||||||
|
configuration.background.strokeWidth = 1
|
||||||
|
configuration.background.cornerRadius = AppRadius.md
|
||||||
|
configuration.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 6, bottom: 0, trailing: 6)
|
||||||
|
configuration.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { attributes in
|
||||||
|
var updated = attributes
|
||||||
|
updated.font = .systemFont(ofSize: 12, weight: .medium)
|
||||||
|
return updated
|
||||||
|
}
|
||||||
|
button.configuration = configuration
|
||||||
|
button.accessibilityIdentifier = accessibilityIdentifier
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func minutePrecisionTime(_ value: String) -> String {
|
||||||
|
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard trimmed.count == 19, trimmed.dropFirst(16).first == ":" else { return trimmed }
|
||||||
|
return String(trimmed.prefix(16))
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func editRemarkTapped() { onEditRemark?() }
|
@objc private func editRemarkTapped() { onEditRemark?() }
|
||||||
@objc private func editCommissionTapped() { onEditCommission?() }
|
@objc private func editCommissionTapped() { onEditCommission?() }
|
||||||
|
@objc private func commissionLogTapped() { onViewCommissionLogs?() }
|
||||||
@objc private func callTapped() { onCall?() }
|
@objc private func callTapped() { onCall?() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ final class HomeViewController: BaseViewController {
|
|||||||
|
|
||||||
private let viewModel = HomeViewModel()
|
private let viewModel = HomeViewModel()
|
||||||
private let homeAPI = NetworkServices.shared.homeAPI
|
private let homeAPI = NetworkServices.shared.homeAPI
|
||||||
|
private let messageCenterAPI = NetworkServices.shared.messageCenterAPI
|
||||||
|
|
||||||
private let scenicHeaderView = HomeScenicHeaderView()
|
private let scenicHeaderView = HomeScenicHeaderView()
|
||||||
private var collectionView: UICollectionView!
|
private var collectionView: UICollectionView!
|
||||||
@ -74,6 +75,7 @@ final class HomeViewController: BaseViewController {
|
|||||||
Task { @MainActor in self?.showToast(message) }
|
Task { @MainActor in self?.showToast(message) }
|
||||||
}
|
}
|
||||||
scenicHeaderView.onTap = { [weak self] in self?.presentScenicSelection() }
|
scenicHeaderView.onTap = { [weak self] in self?.presentScenicSelection() }
|
||||||
|
scenicHeaderView.onMessageTap = { [weak self] in self?.openMessageCenter() }
|
||||||
collectionView.delegate = self
|
collectionView.delegate = self
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(
|
NotificationCenter.default.addObserver(
|
||||||
@ -88,11 +90,26 @@ final class HomeViewController: BaseViewController {
|
|||||||
name: NotificationName.scenicDidChange,
|
name: NotificationName.scenicDidChange,
|
||||||
object: nil
|
object: nil
|
||||||
)
|
)
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(handleUnreadMessageCountDidChange(_:)),
|
||||||
|
name: NotificationName.unreadMessageCountDidChange,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(handleUnreadMessageCountDidChange(_:)),
|
||||||
|
name: UIApplication.didBecomeActiveNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
navigationController?.setNavigationBarHidden(true, animated: animated)
|
navigationController?.setNavigationBarHidden(true, animated: animated)
|
||||||
|
if hasInitialized {
|
||||||
|
Task { await refreshUnreadMessageStatus() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
@ -272,6 +289,7 @@ final class HomeViewController: BaseViewController {
|
|||||||
private func initializeHome() async {
|
private func initializeHome() async {
|
||||||
showLoading()
|
showLoading()
|
||||||
await viewModel.initialize(api: homeAPI)
|
await viewModel.initialize(api: homeAPI)
|
||||||
|
await refreshUnreadMessageStatus()
|
||||||
hideLoading()
|
hideLoading()
|
||||||
|
|
||||||
applyViewModel()
|
applyViewModel()
|
||||||
@ -287,7 +305,10 @@ final class HomeViewController: BaseViewController {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func applyViewModel() {
|
private func applyViewModel() {
|
||||||
scenicHeaderView.apply(scenicName: viewModel.currentScenicName)
|
scenicHeaderView.apply(
|
||||||
|
scenicName: viewModel.currentScenicName,
|
||||||
|
hasUnreadMessages: viewModel.hasUnreadMessages
|
||||||
|
)
|
||||||
|
|
||||||
let showWorkBlock = !viewModel.isMinimalTopRole
|
let showWorkBlock = !viewModel.isMinimalTopRole
|
||||||
let showStore = viewModel.currentAppRole == .storeAdmin && viewModel.storeItem != nil
|
let showStore = viewModel.currentAppRole == .storeAdmin && viewModel.storeItem != nil
|
||||||
@ -499,6 +520,10 @@ final class HomeViewController: BaseViewController {
|
|||||||
pushScenicSelection()
|
pushScenicSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func openMessageCenter() {
|
||||||
|
navigationController?.pushViewController(MessageCenterViewController(), animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
private func pushScenicSelection() {
|
private func pushScenicSelection() {
|
||||||
navigationController?.setNavigationBarHidden(false, animated: true)
|
navigationController?.setNavigationBarHidden(false, animated: true)
|
||||||
let controller = ScenicSelectionViewController()
|
let controller = ScenicSelectionViewController()
|
||||||
@ -529,6 +554,7 @@ final class HomeViewController: BaseViewController {
|
|||||||
viewModel.markNeedsPermissionReload()
|
viewModel.markNeedsPermissionReload()
|
||||||
Task {
|
Task {
|
||||||
await viewModel.reloadIfNeeded(api: homeAPI)
|
await viewModel.reloadIfNeeded(api: homeAPI)
|
||||||
|
await refreshUnreadMessageStatus()
|
||||||
applyViewModel()
|
applyViewModel()
|
||||||
await evaluateDialogsWithDelay()
|
await evaluateDialogsWithDelay()
|
||||||
}
|
}
|
||||||
@ -541,6 +567,21 @@ final class HomeViewController: BaseViewController {
|
|||||||
applyViewModel()
|
applyViewModel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func handleUnreadMessageCountDidChange(_ notification: Notification) {
|
||||||
|
guard hasInitialized else { return }
|
||||||
|
if let count = notification.userInfo?[NotificationUserInfoKey.unreadCount] as? Int {
|
||||||
|
viewModel.updateUnreadMessageCount(count)
|
||||||
|
Task { await PushNotificationManager.shared.updateApplicationIconBadgeCount(count) }
|
||||||
|
} else {
|
||||||
|
Task { await refreshUnreadMessageStatus() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func refreshUnreadMessageStatus() async {
|
||||||
|
guard await viewModel.refreshUnreadMessageStatus(api: messageCenterAPI) else { return }
|
||||||
|
await PushNotificationManager.shared.updateApplicationIconBadgeCount(viewModel.unreadMessageCount)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension HomeViewController: UICollectionViewDelegate {
|
extension HomeViewController: UICollectionViewDelegate {
|
||||||
|
|||||||
@ -9,10 +9,16 @@ import UIKit
|
|||||||
/// 首页顶部景区选择条。
|
/// 首页顶部景区选择条。
|
||||||
final class HomeScenicHeaderView: UIView {
|
final class HomeScenicHeaderView: UIView {
|
||||||
|
|
||||||
|
/// 点击景区名称时触发。
|
||||||
var onTap: (() -> Void)?
|
var onTap: (() -> Void)?
|
||||||
|
|
||||||
|
/// 点击消息入口时触发。
|
||||||
|
var onMessageTap: (() -> Void)?
|
||||||
|
|
||||||
private let titleLabel = UILabel()
|
private let titleLabel = UILabel()
|
||||||
private let arrowView = UIImageView(image: UIImage(systemName: "chevron.down"))
|
private let arrowView = UIImageView(image: UIImage(systemName: "chevron.down"))
|
||||||
|
private let messageButton = UIButton(type: .system)
|
||||||
|
private let unreadDotView = UIView()
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
@ -25,8 +31,24 @@ final class HomeScenicHeaderView: UIView {
|
|||||||
arrowView.contentMode = .scaleAspectFit
|
arrowView.contentMode = .scaleAspectFit
|
||||||
arrowView.setContentCompressionResistancePriority(.required, for: .horizontal)
|
arrowView.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||||
|
|
||||||
|
var messageConfiguration = UIButton.Configuration.plain()
|
||||||
|
messageConfiguration.image = UIImage(systemName: "bell")
|
||||||
|
messageConfiguration.baseForegroundColor = AppColor.textPrimary
|
||||||
|
messageConfiguration.contentInsets = .zero
|
||||||
|
messageButton.configuration = messageConfiguration
|
||||||
|
messageButton.accessibilityIdentifier = "home_message_button"
|
||||||
|
messageButton.addTarget(self, action: #selector(handleMessageTap), for: .touchUpInside)
|
||||||
|
|
||||||
|
unreadDotView.backgroundColor = AppColor.danger
|
||||||
|
unreadDotView.layer.cornerRadius = 4
|
||||||
|
unreadDotView.isUserInteractionEnabled = false
|
||||||
|
unreadDotView.isHidden = true
|
||||||
|
unreadDotView.accessibilityIdentifier = "home_message_unread_dot"
|
||||||
|
|
||||||
addSubview(titleLabel)
|
addSubview(titleLabel)
|
||||||
addSubview(arrowView)
|
addSubview(arrowView)
|
||||||
|
addSubview(messageButton)
|
||||||
|
messageButton.addSubview(unreadDotView)
|
||||||
|
|
||||||
titleLabel.snp.makeConstraints { make in
|
titleLabel.snp.makeConstraints { make in
|
||||||
make.leading.equalTo(safeAreaLayoutGuide).offset(AppSpacing.screenHorizontalInset)
|
make.leading.equalTo(safeAreaLayoutGuide).offset(AppSpacing.screenHorizontalInset)
|
||||||
@ -34,12 +56,23 @@ final class HomeScenicHeaderView: UIView {
|
|||||||
}
|
}
|
||||||
arrowView.snp.makeConstraints { make in
|
arrowView.snp.makeConstraints { make in
|
||||||
make.leading.equalTo(titleLabel.snp.trailing).offset(AppSpacing.xs)
|
make.leading.equalTo(titleLabel.snp.trailing).offset(AppSpacing.xs)
|
||||||
make.trailing.lessThanOrEqualTo(safeAreaLayoutGuide).offset(-AppSpacing.screenHorizontalInset)
|
make.trailing.lessThanOrEqualTo(messageButton.snp.leading).offset(-AppSpacing.xs)
|
||||||
make.centerY.equalTo(safeAreaLayoutGuide)
|
make.centerY.equalTo(safeAreaLayoutGuide)
|
||||||
make.width.height.equalTo(16)
|
make.width.height.equalTo(16)
|
||||||
}
|
}
|
||||||
|
messageButton.snp.makeConstraints { make in
|
||||||
|
make.trailing.equalTo(safeAreaLayoutGuide).offset(-AppSpacing.screenHorizontalInset)
|
||||||
|
make.centerY.equalTo(safeAreaLayoutGuide)
|
||||||
|
make.width.height.equalTo(44)
|
||||||
|
}
|
||||||
|
unreadDotView.snp.makeConstraints { make in
|
||||||
|
make.centerX.equalTo(messageButton.snp.centerX).offset(9)
|
||||||
|
make.centerY.equalTo(messageButton.snp.centerY).offset(-9)
|
||||||
|
make.width.height.equalTo(8)
|
||||||
|
}
|
||||||
|
|
||||||
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
|
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
|
||||||
|
tap.delegate = self
|
||||||
addGestureRecognizer(tap)
|
addGestureRecognizer(tap)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,12 +81,26 @@ final class HomeScenicHeaderView: UIView {
|
|||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func apply(scenicName: String) {
|
/// 更新景区名称与未读消息状态。
|
||||||
|
func apply(scenicName: String, hasUnreadMessages: Bool) {
|
||||||
let trimmed = scenicName.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = scenicName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
titleLabel.text = trimmed.isEmpty ? "请选择景区" : trimmed
|
titleLabel.text = trimmed.isEmpty ? "请选择景区" : trimmed
|
||||||
|
unreadDotView.isHidden = !hasUnreadMessages
|
||||||
|
messageButton.accessibilityLabel = hasUnreadMessages ? "消息中心,有未读消息" : "消息中心"
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func handleTap() {
|
@objc private func handleTap() {
|
||||||
onTap?()
|
onTap?()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func handleMessageTap() {
|
||||||
|
onMessageTap?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension HomeScenicHeaderView: UIGestureRecognizerDelegate {
|
||||||
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
||||||
|
guard let touchedView = touch.view else { return true }
|
||||||
|
return touchedView !== messageButton && !touchedView.isDescendant(of: messageButton)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,10 @@ import CoreLocation
|
|||||||
import SnapKit
|
import SnapKit
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
|
#if targetEnvironment(simulator)
|
||||||
|
import MapKit
|
||||||
|
#endif
|
||||||
|
|
||||||
/// 地图缩放/定位控件。
|
/// 地图缩放/定位控件。
|
||||||
final class LocationReportMapControlStack: UIView {
|
final class LocationReportMapControlStack: UIView {
|
||||||
|
|
||||||
@ -295,7 +299,112 @@ final class LocationReportBottomActionView: UIControl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 地图容器,封装高德 `MAMapView`。
|
/// 位置上报地图容器,模拟器使用 MapKit。
|
||||||
|
#if targetEnvironment(simulator)
|
||||||
|
final class LocationReportMapView: UIView, MKMapViewDelegate {
|
||||||
|
|
||||||
|
var onMapTap: ((CLLocationCoordinate2D) -> Void)?
|
||||||
|
|
||||||
|
let mapView: MKMapView
|
||||||
|
private var markerAnnotation: MKPointAnnotation?
|
||||||
|
private var shouldCenterOnNextUserLocation = false
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
mapView = MKMapView(frame: .zero)
|
||||||
|
super.init(frame: frame)
|
||||||
|
mapView.delegate = self
|
||||||
|
mapView.showsUserLocation = true
|
||||||
|
mapView.showsCompass = false
|
||||||
|
addSubview(mapView)
|
||||||
|
mapView.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(mapTapped(_:)))
|
||||||
|
tapGesture.cancelsTouchesInView = false
|
||||||
|
mapView.addGestureRecognizer(tapGesture)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 进入页面后,在首次拿到用户位置时将地图居中。
|
||||||
|
func enableInitialUserLocationCentering() {
|
||||||
|
shouldCenterOnNextUserLocation = true
|
||||||
|
centerOnUserLocationIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
func centerOnUserLocation() {
|
||||||
|
guard let coordinate = validUserCoordinate else { return }
|
||||||
|
setCenter(coordinate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setCenter(_ coordinate: CLLocationCoordinate2D, animated: Bool = true) {
|
||||||
|
let region = MKCoordinateRegion(
|
||||||
|
center: coordinate,
|
||||||
|
span: MKCoordinateSpan(latitudeDelta: 0.012, longitudeDelta: 0.012)
|
||||||
|
)
|
||||||
|
mapView.setRegion(region, animated: animated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func zoomIn() {
|
||||||
|
updateZoom(scale: 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
func zoomOut() {
|
||||||
|
updateZoom(scale: 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateMarker(latitude: Double?, longitude: Double?) {
|
||||||
|
if let markerAnnotation {
|
||||||
|
mapView.removeAnnotation(markerAnnotation)
|
||||||
|
self.markerAnnotation = nil
|
||||||
|
}
|
||||||
|
guard let latitude, let longitude else { return }
|
||||||
|
let annotation = MKPointAnnotation()
|
||||||
|
annotation.coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
|
||||||
|
markerAnnotation = annotation
|
||||||
|
mapView.addAnnotation(annotation)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
|
||||||
|
centerOnUserLocationIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func mapTapped(_ gesture: UITapGestureRecognizer) {
|
||||||
|
guard gesture.state == .ended else { return }
|
||||||
|
let point = gesture.location(in: mapView)
|
||||||
|
onMapTap?(mapView.convert(point, toCoordinateFrom: mapView))
|
||||||
|
}
|
||||||
|
|
||||||
|
private var validUserCoordinate: CLLocationCoordinate2D? {
|
||||||
|
guard let coordinate = mapView.userLocation.location?.coordinate,
|
||||||
|
CLLocationCoordinate2DIsValid(coordinate),
|
||||||
|
coordinate.latitude != 0 || coordinate.longitude != 0 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return coordinate
|
||||||
|
}
|
||||||
|
|
||||||
|
private func centerOnUserLocationIfNeeded() {
|
||||||
|
guard shouldCenterOnNextUserLocation, let coordinate = validUserCoordinate else { return }
|
||||||
|
setCenter(coordinate, animated: false)
|
||||||
|
shouldCenterOnNextUserLocation = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateZoom(scale: Double) {
|
||||||
|
let current = mapView.region
|
||||||
|
let span = MKCoordinateSpan(
|
||||||
|
latitudeDelta: min(max(current.span.latitudeDelta * scale, 0.0005), 120),
|
||||||
|
longitudeDelta: min(max(current.span.longitudeDelta * scale, 0.0005), 120)
|
||||||
|
)
|
||||||
|
mapView.setRegion(MKCoordinateRegion(center: current.center, span: span), animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
/// 位置上报地图容器,真机封装高德 `MAMapView`。
|
||||||
final class LocationReportMapView: UIView, MAMapViewDelegate {
|
final class LocationReportMapView: UIView, MAMapViewDelegate {
|
||||||
|
|
||||||
var onMapTap: ((CLLocationCoordinate2D) -> Void)?
|
var onMapTap: ((CLLocationCoordinate2D) -> Void)?
|
||||||
@ -381,3 +490,4 @@ final class LocationReportMapView: UIView, MAMapViewDelegate {
|
|||||||
shouldCenterOnNextUserLocation = false
|
shouldCenterOnNextUserLocation = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@ -65,6 +65,29 @@ final class MainTabBarController: UITabBarController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 响应推送点击,切换到目标 Tab 并打开对应业务页面。
|
||||||
|
func openPushDestination(_ destination: PushDestination) {
|
||||||
|
let targetTab = AppTab.home
|
||||||
|
|
||||||
|
selectTab(targetTab)
|
||||||
|
guard let navigationController = tabNavigationControllers[targetTab] else { return }
|
||||||
|
_ = navigationController.popToRootViewController(animated: false)
|
||||||
|
|
||||||
|
let controller: UIViewController?
|
||||||
|
switch destination {
|
||||||
|
case .paymentRecord:
|
||||||
|
controller = PaymentCollectionRecordViewController()
|
||||||
|
case .paymentDetails:
|
||||||
|
controller = PaymentCollectionDetailsViewController()
|
||||||
|
case .messageCenter:
|
||||||
|
controller = MessageCenterViewController()
|
||||||
|
}
|
||||||
|
|
||||||
|
if let controller {
|
||||||
|
navigationController.pushViewController(controller, animated: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func configureTabBarAppearance() {
|
private func configureTabBarAppearance() {
|
||||||
tabBar.tintColor = AppColor.primary
|
tabBar.tintColor = AppColor.primary
|
||||||
tabBar.unselectedItemTintColor = AppColor.textTabInactive
|
tabBar.unselectedItemTintColor = AppColor.textTabInactive
|
||||||
|
|||||||
@ -19,6 +19,12 @@ final class MessageCenterViewController: BaseViewController, UITableViewDelegate
|
|||||||
private let loadingIndicator = UIActivityIndicatorView(style: .medium)
|
private let loadingIndicator = UIActivityIndicatorView(style: .medium)
|
||||||
private let footerSpinner = UIActivityIndicatorView(style: .medium)
|
private let footerSpinner = UIActivityIndicatorView(style: .medium)
|
||||||
private let emptyLabel = UILabel()
|
private let emptyLabel = UILabel()
|
||||||
|
private lazy var markAllReadButton = UIBarButtonItem(
|
||||||
|
title: "全部已读",
|
||||||
|
style: .plain,
|
||||||
|
target: self,
|
||||||
|
action: #selector(markAllReadTapped)
|
||||||
|
)
|
||||||
|
|
||||||
private var dataSource: UITableViewDiffableDataSource<Section, MessageItem>!
|
private var dataSource: UITableViewDiffableDataSource<Section, MessageItem>!
|
||||||
|
|
||||||
@ -46,6 +52,7 @@ final class MessageCenterViewController: BaseViewController, UITableViewDelegate
|
|||||||
|
|
||||||
override func setupNavigationBar() {
|
override func setupNavigationBar() {
|
||||||
title = "消息中心"
|
title = "消息中心"
|
||||||
|
navigationItem.rightBarButtonItem = markAllReadButton
|
||||||
}
|
}
|
||||||
|
|
||||||
override func setupUI() {
|
override func setupUI() {
|
||||||
@ -79,7 +86,8 @@ final class MessageCenterViewController: BaseViewController, UITableViewDelegate
|
|||||||
|
|
||||||
override func setupConstraints() {
|
override func setupConstraints() {
|
||||||
tableView.snp.makeConstraints { make in
|
tableView.snp.makeConstraints { make in
|
||||||
make.edges.equalTo(view.safeAreaLayoutGuide)
|
make.top.equalTo(view.safeAreaLayoutGuide)
|
||||||
|
make.leading.trailing.bottom.equalToSuperview()
|
||||||
}
|
}
|
||||||
loadingIndicator.snp.makeConstraints { make in
|
loadingIndicator.snp.makeConstraints { make in
|
||||||
make.center.equalToSuperview()
|
make.center.equalToSuperview()
|
||||||
@ -103,6 +111,11 @@ final class MessageCenterViewController: BaseViewController, UITableViewDelegate
|
|||||||
self?.openDetail(message)
|
self?.openDetail(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
viewModel.onUnreadMessageCountChange = { count in
|
||||||
|
Task { @MainActor in
|
||||||
|
await PushNotificationManager.shared.updateApplicationIconBadgeCount(count)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
@ -125,6 +138,12 @@ final class MessageCenterViewController: BaseViewController, UITableViewDelegate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func markAllReadTapped() {
|
||||||
|
Task { @MainActor in
|
||||||
|
await viewModel.markAllAsRead(api: api)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func configureDataSource() {
|
private func configureDataSource() {
|
||||||
dataSource = UITableViewDiffableDataSource<Section, MessageItem>(tableView: tableView) { tableView, indexPath, item in
|
dataSource = UITableViewDiffableDataSource<Section, MessageItem>(tableView: tableView) { tableView, indexPath, item in
|
||||||
let cell = tableView.dequeueReusableCell(
|
let cell = tableView.dequeueReusableCell(
|
||||||
@ -154,6 +173,7 @@ final class MessageCenterViewController: BaseViewController, UITableViewDelegate
|
|||||||
}
|
}
|
||||||
|
|
||||||
tableView.tableFooterView = footerView()
|
tableView.tableFooterView = footerView()
|
||||||
|
markAllReadButton.isEnabled = !viewModel.isLoading && !viewModel.isMarkingAllAsRead
|
||||||
}
|
}
|
||||||
|
|
||||||
private func applySnapshot(animated: Bool) {
|
private func applySnapshot(animated: Bool) {
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
// suixinkan
|
// suixinkan
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import Kingfisher
|
||||||
import SnapKit
|
import SnapKit
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@ -13,10 +14,18 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
|||||||
private let paymentAPI = NetworkServices.shared.paymentAPI
|
private let paymentAPI = NetworkServices.shared.paymentAPI
|
||||||
|
|
||||||
private let scrollView = UIScrollView()
|
private let scrollView = UIScrollView()
|
||||||
|
private let contentContainerView = UIView()
|
||||||
private let contentStack = UIStackView()
|
private let contentStack = UIStackView()
|
||||||
|
|
||||||
|
private let backgroundImageView = UIImageView()
|
||||||
|
private let brandHeaderStack = UIStackView()
|
||||||
|
private let brandLogoImageView = UIImageView()
|
||||||
|
private let brandTitleLabel = UILabel()
|
||||||
|
private let brandSubtitleLabel = UILabel()
|
||||||
|
|
||||||
private let qrCardView = UIView()
|
private let qrCardView = UIView()
|
||||||
private let scenicNameLabel = UILabel()
|
private let scenicNameLabel = UILabel()
|
||||||
|
private let qrContainerView = UIView()
|
||||||
private let qrImageView = UIImageView()
|
private let qrImageView = UIImageView()
|
||||||
private let qrPlaceholderView = UIView()
|
private let qrPlaceholderView = UIView()
|
||||||
private let qrErrorLabel = UILabel()
|
private let qrErrorLabel = UILabel()
|
||||||
@ -29,26 +38,75 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
|||||||
private let scenicRow = PaymentInfoRowView(title: "景区名称")
|
private let scenicRow = PaymentInfoRowView(title: "景区名称")
|
||||||
private let merchantRow = PaymentInfoRowView(title: "收款方")
|
private let merchantRow = PaymentInfoRowView(title: "收款方")
|
||||||
private let staffRow = PaymentInfoRowView(title: "员工 ID")
|
private let staffRow = PaymentInfoRowView(title: "员工 ID")
|
||||||
|
private let photographerRow = PaymentInfoRowView(title: "摄影师名称")
|
||||||
|
private let storeRow = PaymentInfoRowView(title: "店铺名称")
|
||||||
|
|
||||||
private let recordRow = PaymentNavigationRowView(title: "收款记录")
|
private let recordRow = PaymentNavigationRowView(title: "收款记录")
|
||||||
|
|
||||||
|
private let settingsCardView = UIView()
|
||||||
private let voiceCardView = UIView()
|
private let voiceCardView = UIView()
|
||||||
private let voiceTitleLabel = UILabel()
|
private let voiceTitleLabel = UILabel()
|
||||||
private let voiceSwitch = UISwitch()
|
private let voiceSwitch = UISwitch()
|
||||||
|
|
||||||
private var amountDialog: PaymentSetAmountDialogView?
|
private var amountDialog: PaymentSetAmountDialogView?
|
||||||
|
private var appliedBrandConfig: PayPageConfig?
|
||||||
|
private var appliedBrandingRefreshVersion = -1
|
||||||
|
|
||||||
|
private var previousStandardAppearance: UINavigationBarAppearance?
|
||||||
|
private var previousScrollEdgeAppearance: UINavigationBarAppearance?
|
||||||
|
private var previousCompactAppearance: UINavigationBarAppearance?
|
||||||
|
private var previousNavigationTintColor: UIColor?
|
||||||
|
private var previousNavigationTranslucency = false
|
||||||
|
|
||||||
|
private var brandScreenHeight: CGFloat {
|
||||||
|
UIScreen.main.bounds.height
|
||||||
|
}
|
||||||
|
|
||||||
|
private var brandLogoHeight: CGFloat {
|
||||||
|
if brandScreenHeight <= 700 { return 82 }
|
||||||
|
if brandScreenHeight <= 820 { return 96 }
|
||||||
|
return 112
|
||||||
|
}
|
||||||
|
|
||||||
|
private var brandQRContainerSize: CGFloat {
|
||||||
|
if brandScreenHeight <= 700 { return 170 }
|
||||||
|
if brandScreenHeight <= 820 { return 190 }
|
||||||
|
return 210
|
||||||
|
}
|
||||||
|
|
||||||
|
private var brandQRImageSize: CGFloat {
|
||||||
|
if brandScreenHeight <= 700 { return 146 }
|
||||||
|
if brandScreenHeight <= 820 { return 166 }
|
||||||
|
return 182
|
||||||
|
}
|
||||||
|
|
||||||
|
private var brandSectionSpacing: CGFloat {
|
||||||
|
if brandScreenHeight <= 700 { return 8 }
|
||||||
|
if brandScreenHeight <= 820 { return 10 }
|
||||||
|
return 12
|
||||||
|
}
|
||||||
|
|
||||||
|
private var brandHeaderToQRSpacing: CGFloat {
|
||||||
|
if brandScreenHeight <= 700 { return 16 }
|
||||||
|
if brandScreenHeight <= 820 { return 18 }
|
||||||
|
return 20
|
||||||
|
}
|
||||||
|
|
||||||
override func setupNavigationBar() {
|
override func setupNavigationBar() {
|
||||||
title = "收款详情"
|
title = viewModel.usesNalatiBranding ? nil : "收款详情"
|
||||||
}
|
}
|
||||||
|
|
||||||
override func setupUI() {
|
override func setupUI() {
|
||||||
view.backgroundColor = AppColor.pageBackground
|
let usesBranding = viewModel.usesNalatiBranding
|
||||||
|
view.backgroundColor = usesBranding ? .clear : AppColor.pageBackground
|
||||||
|
scrollView.showsVerticalScrollIndicator = !usesBranding
|
||||||
|
scrollView.isScrollEnabled = !usesBranding
|
||||||
|
scrollView.alwaysBounceVertical = false
|
||||||
|
|
||||||
contentStack.axis = .vertical
|
contentStack.axis = .vertical
|
||||||
contentStack.spacing = AppSpacing.md
|
contentStack.spacing = usesBranding ? brandSectionSpacing : AppSpacing.md
|
||||||
|
|
||||||
qrCardView.backgroundColor = .white
|
qrCardView.backgroundColor = usesBranding ? .clear : .white
|
||||||
qrCardView.layer.cornerRadius = AppRadius.lg
|
qrCardView.layer.cornerRadius = AppRadius.lg
|
||||||
|
|
||||||
scenicNameLabel.font = .systemFont(ofSize: 16, weight: .medium)
|
scenicNameLabel.font = .systemFont(ofSize: 16, weight: .medium)
|
||||||
@ -57,9 +115,16 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
|||||||
|
|
||||||
qrImageView.contentMode = .scaleAspectFit
|
qrImageView.contentMode = .scaleAspectFit
|
||||||
qrImageView.isHidden = true
|
qrImageView.isHidden = true
|
||||||
|
if usesBranding {
|
||||||
|
qrContainerView.backgroundColor = .white
|
||||||
|
qrContainerView.layer.cornerRadius = 10
|
||||||
|
qrContainerView.clipsToBounds = true
|
||||||
|
qrContainerView.isHidden = true
|
||||||
|
qrContainerView.addSubview(qrImageView)
|
||||||
|
}
|
||||||
|
|
||||||
qrPlaceholderView.backgroundColor = AppColor.inputBackground
|
qrPlaceholderView.backgroundColor = AppColor.inputBackground
|
||||||
qrPlaceholderView.layer.cornerRadius = 24
|
qrPlaceholderView.layer.cornerRadius = usesBranding ? 10 : 24
|
||||||
qrPlaceholderView.isHidden = true
|
qrPlaceholderView.isHidden = true
|
||||||
|
|
||||||
qrErrorLabel.text = "未选景区或网络问题"
|
qrErrorLabel.text = "未选景区或网络问题"
|
||||||
@ -73,19 +138,29 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
|||||||
|
|
||||||
actionRow.axis = .horizontal
|
actionRow.axis = .horizontal
|
||||||
actionRow.alignment = .center
|
actionRow.alignment = .center
|
||||||
actionRow.spacing = AppSpacing.xl
|
actionRow.spacing = usesBranding ? AppSpacing.lg : AppSpacing.xl
|
||||||
|
if usesBranding {
|
||||||
|
actionRow.backgroundColor = UIColor.white.withAlphaComponent(0.88)
|
||||||
|
actionRow.layer.cornerRadius = 24
|
||||||
|
actionRow.isLayoutMarginsRelativeArrangement = true
|
||||||
|
actionRow.directionalLayoutMargins = NSDirectionalEdgeInsets(
|
||||||
|
top: 10,
|
||||||
|
leading: 24,
|
||||||
|
bottom: 10,
|
||||||
|
trailing: 24
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
configureLinkButton(setAmountButton, title: "设置金额")
|
configureLinkButton(setAmountButton, title: "设置金额")
|
||||||
configureLinkButton(saveQRButton, title: "保存二维码")
|
configureLinkButton(saveQRButton, title: "保存二维码")
|
||||||
|
|
||||||
infoCardView.backgroundColor = .white
|
configureCard(infoCardView, branded: usesBranding)
|
||||||
infoCardView.layer.cornerRadius = AppRadius.lg
|
|
||||||
|
|
||||||
recordRow.backgroundColor = .white
|
recordRow.backgroundColor = usesBranding ? .clear : .white
|
||||||
recordRow.layer.cornerRadius = AppRadius.lg
|
recordRow.layer.cornerRadius = usesBranding ? 0 : AppRadius.lg
|
||||||
|
|
||||||
voiceCardView.backgroundColor = .white
|
voiceCardView.backgroundColor = usesBranding ? .clear : .white
|
||||||
voiceCardView.layer.cornerRadius = AppRadius.lg
|
voiceCardView.layer.cornerRadius = usesBranding ? 0 : AppRadius.lg
|
||||||
|
|
||||||
voiceTitleLabel.text = "收款到账语音提醒"
|
voiceTitleLabel.text = "收款到账语音提醒"
|
||||||
voiceTitleLabel.font = .systemFont(ofSize: 14)
|
voiceTitleLabel.font = .systemFont(ofSize: 14)
|
||||||
@ -93,49 +168,112 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
|||||||
|
|
||||||
voiceSwitch.onTintColor = AppColor.primary
|
voiceSwitch.onTintColor = AppColor.primary
|
||||||
|
|
||||||
view.addSubview(scrollView)
|
if usesBranding {
|
||||||
scrollView.addSubview(contentStack)
|
configureBrandHeader()
|
||||||
|
backgroundImageView.contentMode = .scaleAspectFill
|
||||||
|
backgroundImageView.clipsToBounds = true
|
||||||
|
backgroundImageView.image = UIImage(named: "payment_nalati_background")
|
||||||
|
view.addSubview(backgroundImageView)
|
||||||
|
}
|
||||||
|
|
||||||
|
view.addSubview(scrollView)
|
||||||
|
scrollView.addSubview(contentContainerView)
|
||||||
|
contentContainerView.addSubview(contentStack)
|
||||||
|
|
||||||
|
if usesBranding {
|
||||||
|
contentStack.addArrangedSubview(brandHeaderStack)
|
||||||
|
contentStack.setCustomSpacing(brandHeaderToQRSpacing, after: brandHeaderStack)
|
||||||
|
}
|
||||||
contentStack.addArrangedSubview(qrCardView)
|
contentStack.addArrangedSubview(qrCardView)
|
||||||
contentStack.addArrangedSubview(infoCardView)
|
contentStack.addArrangedSubview(infoCardView)
|
||||||
|
if usesBranding {
|
||||||
|
configureCard(settingsCardView, branded: true)
|
||||||
|
contentStack.addArrangedSubview(settingsCardView)
|
||||||
|
} else {
|
||||||
contentStack.addArrangedSubview(recordRow)
|
contentStack.addArrangedSubview(recordRow)
|
||||||
contentStack.addArrangedSubview(voiceCardView)
|
contentStack.addArrangedSubview(voiceCardView)
|
||||||
|
}
|
||||||
|
|
||||||
|
let qrDisplayView = usesBranding ? qrContainerView : qrImageView
|
||||||
let qrContentStack = UIStackView(arrangedSubviews: [
|
let qrContentStack = UIStackView(arrangedSubviews: [
|
||||||
scenicNameLabel,
|
scenicNameLabel,
|
||||||
qrImageView,
|
qrDisplayView,
|
||||||
qrPlaceholderView,
|
qrPlaceholderView,
|
||||||
actionRow,
|
actionRow,
|
||||||
])
|
])
|
||||||
qrContentStack.axis = .vertical
|
qrContentStack.axis = .vertical
|
||||||
qrContentStack.alignment = .center
|
qrContentStack.alignment = .center
|
||||||
qrContentStack.spacing = AppSpacing.lg
|
qrContentStack.spacing = usesBranding ? brandSectionSpacing : AppSpacing.lg
|
||||||
qrCardView.addSubview(qrContentStack)
|
qrCardView.addSubview(qrContentStack)
|
||||||
|
|
||||||
qrPlaceholderView.addSubview(qrErrorLabel)
|
qrPlaceholderView.addSubview(qrErrorLabel)
|
||||||
qrPlaceholderView.addSubview(refreshButton)
|
qrPlaceholderView.addSubview(refreshButton)
|
||||||
|
|
||||||
actionRow.addArrangedSubview(setAmountButton)
|
actionRow.addArrangedSubview(setAmountButton)
|
||||||
|
if usesBranding {
|
||||||
|
let divider = UIView()
|
||||||
|
divider.backgroundColor = UIColor(hex: 0xD6E1EE)
|
||||||
|
actionRow.addArrangedSubview(divider)
|
||||||
|
divider.snp.makeConstraints { make in
|
||||||
|
make.width.equalTo(1)
|
||||||
|
make.height.equalTo(24)
|
||||||
|
}
|
||||||
|
}
|
||||||
actionRow.addArrangedSubview(saveQRButton)
|
actionRow.addArrangedSubview(saveQRButton)
|
||||||
|
|
||||||
let infoStack = UIStackView(arrangedSubviews: [scenicRow, merchantRow, staffRow])
|
let infoRows = usesBranding
|
||||||
|
? [photographerRow, storeRow]
|
||||||
|
: [scenicRow, merchantRow, staffRow]
|
||||||
|
let infoStack = UIStackView(arrangedSubviews: infoRows)
|
||||||
infoStack.axis = .vertical
|
infoStack.axis = .vertical
|
||||||
infoCardView.addSubview(infoStack)
|
infoCardView.addSubview(infoStack)
|
||||||
|
|
||||||
voiceCardView.addSubview(voiceTitleLabel)
|
voiceCardView.addSubview(voiceTitleLabel)
|
||||||
voiceCardView.addSubview(voiceSwitch)
|
voiceCardView.addSubview(voiceSwitch)
|
||||||
|
|
||||||
qrContentStack.snp.makeConstraints { make in
|
if usesBranding {
|
||||||
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 32, left: 16, bottom: 32, right: 16))
|
let settingsStack = UIStackView(arrangedSubviews: [recordRow, voiceCardView])
|
||||||
|
settingsStack.axis = .vertical
|
||||||
|
settingsCardView.addSubview(settingsStack)
|
||||||
|
settingsStack.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let divider = UIView()
|
||||||
|
divider.backgroundColor = UIColor(hex: 0xD9E2EA).withAlphaComponent(0.75)
|
||||||
|
settingsCardView.addSubview(divider)
|
||||||
|
divider.snp.makeConstraints { make in
|
||||||
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||||
|
make.centerY.equalToSuperview()
|
||||||
|
make.height.equalTo(0.5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qrContentStack.snp.makeConstraints { make in
|
||||||
|
let verticalInset: CGFloat = usesBranding ? 0 : 32
|
||||||
|
make.edges.equalToSuperview().inset(
|
||||||
|
UIEdgeInsets(top: verticalInset, left: 16, bottom: verticalInset, right: 16)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
scenicNameLabel.isHidden = usesBranding
|
||||||
scenicNameLabel.snp.makeConstraints { make in
|
scenicNameLabel.snp.makeConstraints { make in
|
||||||
make.leading.trailing.equalToSuperview()
|
make.leading.trailing.equalToSuperview()
|
||||||
}
|
}
|
||||||
|
if usesBranding {
|
||||||
|
qrContainerView.snp.makeConstraints { make in
|
||||||
|
make.width.height.equalTo(brandQRContainerSize)
|
||||||
|
}
|
||||||
|
qrImageView.snp.makeConstraints { make in
|
||||||
|
make.center.equalToSuperview()
|
||||||
|
make.width.height.equalTo(brandQRImageSize)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
qrImageView.snp.makeConstraints { make in
|
qrImageView.snp.makeConstraints { make in
|
||||||
make.width.height.equalTo(182)
|
make.width.height.equalTo(182)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
qrPlaceholderView.snp.makeConstraints { make in
|
qrPlaceholderView.snp.makeConstraints { make in
|
||||||
make.width.height.equalTo(182)
|
make.width.height.equalTo(usesBranding ? brandQRContainerSize : 182)
|
||||||
}
|
}
|
||||||
qrErrorLabel.snp.makeConstraints { make in
|
qrErrorLabel.snp.makeConstraints { make in
|
||||||
make.top.equalToSuperview().offset(56)
|
make.top.equalToSuperview().offset(56)
|
||||||
@ -162,12 +300,36 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func setupConstraints() {
|
override func setupConstraints() {
|
||||||
|
if viewModel.usesNalatiBranding {
|
||||||
|
backgroundImageView.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
scrollView.snp.makeConstraints { make in
|
scrollView.snp.makeConstraints { make in
|
||||||
|
if viewModel.usesNalatiBranding {
|
||||||
|
make.leading.trailing.equalToSuperview()
|
||||||
|
make.top.equalTo(view.safeAreaLayoutGuide.snp.top).offset(-44)
|
||||||
|
make.bottom.equalTo(view.safeAreaLayoutGuide)
|
||||||
|
} else {
|
||||||
make.edges.equalTo(view.safeAreaLayoutGuide)
|
make.edges.equalTo(view.safeAreaLayoutGuide)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
contentContainerView.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
|
make.width.equalTo(scrollView.snp.width)
|
||||||
|
if viewModel.usesNalatiBranding {
|
||||||
|
make.height.equalTo(scrollView.snp.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
contentStack.snp.makeConstraints { make in
|
contentStack.snp.makeConstraints { make in
|
||||||
make.edges.equalToSuperview().inset(AppSpacing.screenHorizontalInset)
|
|
||||||
make.width.equalTo(scrollView.snp.width).offset(-AppSpacing.screenHorizontalInset * 2)
|
make.width.equalTo(scrollView.snp.width).offset(-AppSpacing.screenHorizontalInset * 2)
|
||||||
|
if viewModel.usesNalatiBranding {
|
||||||
|
make.centerX.centerY.equalToSuperview()
|
||||||
|
make.top.greaterThanOrEqualToSuperview()
|
||||||
|
make.bottom.lessThanOrEqualToSuperview()
|
||||||
|
} else {
|
||||||
|
make.edges.equalToSuperview().inset(AppSpacing.screenHorizontalInset)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,13 +350,30 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
|||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
applyViewModel()
|
||||||
Task { await loadData() }
|
Task { await loadData() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
applyBrandNavigationAppearanceIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
|
super.viewWillDisappear(animated)
|
||||||
|
restoreNavigationAppearanceIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||||
|
viewModel.usesNalatiBranding ? .darkContent : .default
|
||||||
|
}
|
||||||
|
|
||||||
private func loadData() async {
|
private func loadData() async {
|
||||||
|
async let pageConfig: Void = viewModel.loadPayPageConfig(api: paymentAPI)
|
||||||
showLoading()
|
showLoading()
|
||||||
await viewModel.loadPayCode(api: paymentAPI)
|
await viewModel.loadPayCode(api: paymentAPI)
|
||||||
hideLoading()
|
hideLoading()
|
||||||
|
await pageConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@ -203,15 +382,20 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
|||||||
scenicRow.setValue(viewModel.displayScenicName)
|
scenicRow.setValue(viewModel.displayScenicName)
|
||||||
merchantRow.setValue(viewModel.merchantInfo.company)
|
merchantRow.setValue(viewModel.merchantInfo.company)
|
||||||
staffRow.setValue(viewModel.staffId.isEmpty ? "-" : viewModel.staffId)
|
staffRow.setValue(viewModel.staffId.isEmpty ? "-" : viewModel.staffId)
|
||||||
|
photographerRow.setValue(viewModel.displayPhotographerName)
|
||||||
|
storeRow.setValue(viewModel.displayStoreNames)
|
||||||
voiceSwitch.isOn = viewModel.isVoiceBroadcastOpen
|
voiceSwitch.isOn = viewModel.isVoiceBroadcastOpen
|
||||||
|
applyBrandConfigIfNeeded()
|
||||||
|
|
||||||
if let image = viewModel.qrImage {
|
if let image = viewModel.qrImage {
|
||||||
qrImageView.image = image
|
qrImageView.image = image
|
||||||
qrImageView.isHidden = false
|
qrImageView.isHidden = false
|
||||||
|
qrContainerView.isHidden = !viewModel.usesNalatiBranding
|
||||||
qrPlaceholderView.isHidden = true
|
qrPlaceholderView.isHidden = true
|
||||||
actionRow.isHidden = false
|
actionRow.isHidden = false
|
||||||
} else {
|
} else {
|
||||||
qrImageView.isHidden = true
|
qrImageView.isHidden = true
|
||||||
|
qrContainerView.isHidden = true
|
||||||
qrPlaceholderView.isHidden = false
|
qrPlaceholderView.isHidden = false
|
||||||
actionRow.isHidden = true
|
actionRow.isHidden = true
|
||||||
}
|
}
|
||||||
@ -254,6 +438,181 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
|||||||
button.titleLabel?.font = .systemFont(ofSize: 14)
|
button.titleLabel?.font = .systemFont(ofSize: 14)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func configureCard(_ view: UIView, branded: Bool) {
|
||||||
|
view.backgroundColor = branded ? UIColor.white.withAlphaComponent(0.9) : .white
|
||||||
|
view.layer.cornerRadius = branded ? 18 : AppRadius.lg
|
||||||
|
view.clipsToBounds = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureBrandHeader() {
|
||||||
|
brandHeaderStack.axis = .vertical
|
||||||
|
brandHeaderStack.alignment = .center
|
||||||
|
brandHeaderStack.spacing = brandScreenHeight <= 700 ? 6 : 8
|
||||||
|
|
||||||
|
brandLogoImageView.image = UIImage(named: "payment_nalati_logo")
|
||||||
|
brandLogoImageView.contentMode = .scaleAspectFit
|
||||||
|
brandLogoImageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||||
|
|
||||||
|
brandTitleLabel.font = .systemFont(
|
||||||
|
ofSize: brandScreenHeight <= 700 ? 21 : 23,
|
||||||
|
weight: .bold
|
||||||
|
)
|
||||||
|
brandTitleLabel.textColor = UIColor(hex: 0x102D54)
|
||||||
|
brandTitleLabel.textAlignment = .center
|
||||||
|
brandTitleLabel.numberOfLines = 1
|
||||||
|
brandTitleLabel.adjustsFontSizeToFitWidth = true
|
||||||
|
brandTitleLabel.minimumScaleFactor = 0.8
|
||||||
|
|
||||||
|
brandSubtitleLabel.font = .systemFont(
|
||||||
|
ofSize: brandScreenHeight <= 700 ? 15 : 16,
|
||||||
|
weight: .medium
|
||||||
|
)
|
||||||
|
brandSubtitleLabel.textColor = UIColor(hex: 0x183A61)
|
||||||
|
brandSubtitleLabel.textAlignment = .center
|
||||||
|
|
||||||
|
let subtitleRow = UIStackView()
|
||||||
|
subtitleRow.axis = .horizontal
|
||||||
|
subtitleRow.alignment = .center
|
||||||
|
subtitleRow.spacing = 9
|
||||||
|
|
||||||
|
let leftLine = makeBrandLine()
|
||||||
|
let leftDiamond = makeBrandDiamond()
|
||||||
|
let rightDiamond = makeBrandDiamond()
|
||||||
|
let rightLine = makeBrandLine()
|
||||||
|
[leftLine, leftDiamond, brandSubtitleLabel, rightDiamond, rightLine].forEach(
|
||||||
|
subtitleRow.addArrangedSubview
|
||||||
|
)
|
||||||
|
|
||||||
|
brandHeaderStack.addArrangedSubview(brandLogoImageView)
|
||||||
|
brandHeaderStack.addArrangedSubview(brandTitleLabel)
|
||||||
|
brandHeaderStack.addArrangedSubview(subtitleRow)
|
||||||
|
brandHeaderStack.setCustomSpacing(
|
||||||
|
brandScreenHeight <= 700 ? 8 : 10,
|
||||||
|
after: brandLogoImageView
|
||||||
|
)
|
||||||
|
|
||||||
|
brandLogoImageView.snp.makeConstraints { make in
|
||||||
|
make.width.equalToSuperview().inset(8)
|
||||||
|
make.height.equalTo(brandLogoHeight)
|
||||||
|
}
|
||||||
|
brandTitleLabel.snp.makeConstraints { make in
|
||||||
|
make.leading.trailing.equalToSuperview().inset(8)
|
||||||
|
}
|
||||||
|
[leftLine, rightLine].forEach { line in
|
||||||
|
line.snp.makeConstraints { make in
|
||||||
|
make.width.equalTo(42)
|
||||||
|
make.height.equalTo(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[leftDiamond, rightDiamond].forEach { diamond in
|
||||||
|
diamond.snp.makeConstraints { make in
|
||||||
|
make.width.height.equalTo(6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeBrandLine() -> UIView {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = UIColor(hex: 0x27496D).withAlphaComponent(0.75)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeBrandDiamond() -> UIView {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = UIColor(hex: 0x27496D)
|
||||||
|
view.transform = CGAffineTransform(rotationAngle: .pi / 4)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func applyBrandConfigIfNeeded() {
|
||||||
|
guard viewModel.usesNalatiBranding else { return }
|
||||||
|
let config = viewModel.payPageConfig
|
||||||
|
brandTitleLabel.text = config.title
|
||||||
|
brandSubtitleLabel.text = config.subtitle
|
||||||
|
|
||||||
|
let versionChanged = appliedBrandingRefreshVersion != viewModel.brandingRefreshVersion
|
||||||
|
guard appliedBrandConfig != config || versionChanged else { return }
|
||||||
|
let forceRefresh = appliedBrandingRefreshVersion >= 0 && versionChanged
|
||||||
|
setBrandImage(
|
||||||
|
backgroundImageView,
|
||||||
|
urlString: config.background,
|
||||||
|
fallbackName: "payment_nalati_background",
|
||||||
|
forceRefresh: forceRefresh
|
||||||
|
)
|
||||||
|
setBrandImage(
|
||||||
|
brandLogoImageView,
|
||||||
|
urlString: config.logo,
|
||||||
|
fallbackName: "payment_nalati_logo",
|
||||||
|
forceRefresh: forceRefresh
|
||||||
|
)
|
||||||
|
appliedBrandConfig = config
|
||||||
|
appliedBrandingRefreshVersion = viewModel.brandingRefreshVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setBrandImage(
|
||||||
|
_ imageView: UIImageView,
|
||||||
|
urlString: String,
|
||||||
|
fallbackName: String,
|
||||||
|
forceRefresh: Bool
|
||||||
|
) {
|
||||||
|
let fallback = UIImage(named: fallbackName)
|
||||||
|
let trimmed = urlString.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard let url = URL(string: trimmed),
|
||||||
|
url.scheme?.lowercased() == "https",
|
||||||
|
url.host != nil else {
|
||||||
|
imageView.image = fallback
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var options: KingfisherOptionsInfo = [
|
||||||
|
.keepCurrentImageWhileLoading,
|
||||||
|
.cacheOriginalImage,
|
||||||
|
.transition(.fade(0.2)),
|
||||||
|
]
|
||||||
|
if forceRefresh {
|
||||||
|
options.append(.forceRefresh)
|
||||||
|
}
|
||||||
|
imageView.kf.setImage(with: url, placeholder: fallback, options: options)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyBrandNavigationAppearanceIfNeeded() {
|
||||||
|
guard viewModel.usesNalatiBranding, let navigationBar = navigationController?.navigationBar else { return }
|
||||||
|
previousStandardAppearance = navigationBar.standardAppearance
|
||||||
|
previousScrollEdgeAppearance = navigationBar.scrollEdgeAppearance
|
||||||
|
previousCompactAppearance = navigationBar.compactAppearance
|
||||||
|
previousNavigationTintColor = navigationBar.tintColor
|
||||||
|
previousNavigationTranslucency = navigationBar.isTranslucent
|
||||||
|
|
||||||
|
let appearance = UINavigationBarAppearance()
|
||||||
|
appearance.configureWithTransparentBackground()
|
||||||
|
appearance.shadowColor = .clear
|
||||||
|
appearance.titleTextAttributes = [
|
||||||
|
.foregroundColor: UIColor(hex: 0x111827),
|
||||||
|
.font: UIFont.app(.title),
|
||||||
|
]
|
||||||
|
navigationBar.standardAppearance = appearance
|
||||||
|
navigationBar.scrollEdgeAppearance = appearance
|
||||||
|
navigationBar.compactAppearance = appearance
|
||||||
|
navigationBar.tintColor = UIColor(hex: 0x111827)
|
||||||
|
navigationBar.isTranslucent = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func restoreNavigationAppearanceIfNeeded() {
|
||||||
|
guard viewModel.usesNalatiBranding, let navigationBar = navigationController?.navigationBar else { return }
|
||||||
|
if let previousStandardAppearance {
|
||||||
|
navigationBar.standardAppearance = previousStandardAppearance
|
||||||
|
}
|
||||||
|
navigationBar.scrollEdgeAppearance = previousScrollEdgeAppearance
|
||||||
|
navigationBar.compactAppearance = previousCompactAppearance
|
||||||
|
navigationBar.tintColor = previousNavigationTintColor
|
||||||
|
navigationBar.isTranslucent = previousNavigationTranslucency
|
||||||
|
previousStandardAppearance = nil
|
||||||
|
previousScrollEdgeAppearance = nil
|
||||||
|
previousCompactAppearance = nil
|
||||||
|
previousNavigationTintColor = nil
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func setAmountTapped() {
|
@objc private func setAmountTapped() {
|
||||||
viewModel.showSetAmountDialog()
|
viewModel.showSetAmountDialog()
|
||||||
}
|
}
|
||||||
@ -310,7 +669,7 @@ private final class PaymentInfoRowView: UIView {
|
|||||||
valueLabel.font = .systemFont(ofSize: 14)
|
valueLabel.font = .systemFont(ofSize: 14)
|
||||||
valueLabel.textColor = AppColor.textPrimary
|
valueLabel.textColor = AppColor.textPrimary
|
||||||
valueLabel.textAlignment = .right
|
valueLabel.textAlignment = .right
|
||||||
valueLabel.numberOfLines = 2
|
valueLabel.numberOfLines = 0
|
||||||
|
|
||||||
divider.backgroundColor = UIColor(hex: 0xE5E7EB)
|
divider.backgroundColor = UIColor(hex: 0xE5E7EB)
|
||||||
|
|
||||||
|
|||||||
@ -3,11 +3,16 @@
|
|||||||
// suixinkan
|
// suixinkan
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import CoreLocation
|
||||||
import Kingfisher
|
import Kingfisher
|
||||||
import Photos
|
import Photos
|
||||||
import SnapKit
|
import SnapKit
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
|
#if targetEnvironment(simulator)
|
||||||
|
import MapKit
|
||||||
|
#endif
|
||||||
|
|
||||||
/// 打卡点详情页,对齐 Android `PunchPointDetailScreen`。
|
/// 打卡点详情页,对齐 Android `PunchPointDetailScreen`。
|
||||||
final class PunchPointDetailViewController: BaseViewController {
|
final class PunchPointDetailViewController: BaseViewController {
|
||||||
private let viewModel: PunchPointDetailViewModel
|
private let viewModel: PunchPointDetailViewModel
|
||||||
@ -447,7 +452,192 @@ final class PunchPointDetailViewController: BaseViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 高德地图容器,供打卡点详情与表单复用。
|
/// 高德地图容器,供打卡点详情与表单复用。
|
||||||
/// 打卡点地图视图,封装高德地图 marker、缩放、定位与点击选点行为。
|
/// 打卡点地图视图,模拟器使用 MapKit 提供 marker、缩放、定位与点击选点行为。
|
||||||
|
#if targetEnvironment(simulator)
|
||||||
|
final class PunchPointMapView: UIView, MKMapViewDelegate {
|
||||||
|
var onMapTap: ((CLLocationCoordinate2D) -> Void)?
|
||||||
|
|
||||||
|
private let mapView: MKMapView
|
||||||
|
private var punchPointAnnotation: MKPointAnnotation?
|
||||||
|
private var selectedAnnotation: MKPointAnnotation?
|
||||||
|
private var punchPointImageURL: URL?
|
||||||
|
private var fitWithUserLocation = false
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
mapView = MKMapView(frame: .zero)
|
||||||
|
super.init(frame: frame)
|
||||||
|
mapView.delegate = self
|
||||||
|
mapView.showsUserLocation = true
|
||||||
|
mapView.showsCompass = false
|
||||||
|
mapView.showsScale = false
|
||||||
|
mapView.pointOfInterestFilter = .includingAll
|
||||||
|
addSubview(mapView)
|
||||||
|
mapView.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(mapTapped(_:)))
|
||||||
|
tapGesture.cancelsTouchesInView = false
|
||||||
|
mapView.addGestureRecognizer(tapGesture)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新原打卡点标记,可使用首张图片作为圆角 Marker。
|
||||||
|
func updatePunchPointMarker(
|
||||||
|
coordinate: CLLocationCoordinate2D?,
|
||||||
|
imageURL: String? = nil,
|
||||||
|
fitWithUserLocation: Bool = false
|
||||||
|
) {
|
||||||
|
if let punchPointAnnotation {
|
||||||
|
mapView.removeAnnotation(punchPointAnnotation)
|
||||||
|
self.punchPointAnnotation = nil
|
||||||
|
}
|
||||||
|
punchPointImageURL = imageURL.flatMap(URL.init(string:))
|
||||||
|
self.fitWithUserLocation = fitWithUserLocation
|
||||||
|
guard let coordinate, CLLocationCoordinate2DIsValid(coordinate) else { return }
|
||||||
|
let annotation = MKPointAnnotation()
|
||||||
|
annotation.coordinate = coordinate
|
||||||
|
annotation.title = "punch_point_original"
|
||||||
|
punchPointAnnotation = annotation
|
||||||
|
mapView.addAnnotation(annotation)
|
||||||
|
if fitWithUserLocation {
|
||||||
|
fitPunchPointAndUserLocation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 兼容仅需更新普通 Marker 的调用。
|
||||||
|
func updateMarker(coordinate: CLLocationCoordinate2D?) {
|
||||||
|
updatePunchPointMarker(coordinate: coordinate)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新地图点击产生的新位置 Marker。
|
||||||
|
func updateSelectedMarker(coordinate: CLLocationCoordinate2D?) {
|
||||||
|
if let selectedAnnotation {
|
||||||
|
mapView.removeAnnotation(selectedAnnotation)
|
||||||
|
self.selectedAnnotation = nil
|
||||||
|
}
|
||||||
|
guard let coordinate, CLLocationCoordinate2DIsValid(coordinate) else { return }
|
||||||
|
let annotation = MKPointAnnotation()
|
||||||
|
annotation.coordinate = coordinate
|
||||||
|
annotation.title = "punch_point_selected"
|
||||||
|
selectedAnnotation = annotation
|
||||||
|
mapView.addAnnotation(annotation)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置地图中心点。
|
||||||
|
func setCenter(_ coordinate: CLLocationCoordinate2D, zoomLevel: Double = 15, animated: Bool = true) {
|
||||||
|
let delta = min(max(0.012 * pow(2, 15 - zoomLevel), 0.0005), 120)
|
||||||
|
let region = MKCoordinateRegion(
|
||||||
|
center: coordinate,
|
||||||
|
span: MKCoordinateSpan(latitudeDelta: delta, longitudeDelta: delta)
|
||||||
|
)
|
||||||
|
mapView.setRegion(region, animated: animated)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 放大地图。
|
||||||
|
func zoomIn() {
|
||||||
|
updateZoom(scale: 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 缩小地图。
|
||||||
|
func zoomOut() {
|
||||||
|
updateZoom(scale: 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 居中到用户当前位置。
|
||||||
|
func centerOnUserLocation() {
|
||||||
|
guard let coordinate = mapView.userLocation.location?.coordinate,
|
||||||
|
CLLocationCoordinate2DIsValid(coordinate),
|
||||||
|
coordinate.latitude != 0 || coordinate.longitude != 0 else { return }
|
||||||
|
setCenter(coordinate)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 同时展示原打卡点、用户位置和新选位置。
|
||||||
|
func fitAllMarkers(edgePadding: UIEdgeInsets = UIEdgeInsets(top: 80, left: 56, bottom: 80, right: 56)) {
|
||||||
|
var annotations: [any MKAnnotation] = []
|
||||||
|
if let punchPointAnnotation { annotations.append(punchPointAnnotation) }
|
||||||
|
if let selectedAnnotation { annotations.append(selectedAnnotation) }
|
||||||
|
if mapView.userLocation.location != nil { annotations.append(mapView.userLocation) }
|
||||||
|
guard !annotations.isEmpty else { return }
|
||||||
|
if annotations.count == 1, let annotation = annotations.first {
|
||||||
|
setCenter(annotation.coordinate, zoomLevel: 16)
|
||||||
|
} else {
|
||||||
|
var mapRect = MKMapRect.null
|
||||||
|
for annotation in annotations {
|
||||||
|
let point = MKMapPoint(annotation.coordinate)
|
||||||
|
mapRect = mapRect.union(MKMapRect(
|
||||||
|
origin: point,
|
||||||
|
size: MKMapSize(width: 1, height: 1)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
mapView.setVisibleMapRect(mapRect, edgePadding: edgePadding, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapView(_ mapView: MKMapView, viewFor annotation: any MKAnnotation) -> MKAnnotationView? {
|
||||||
|
if annotation is MKUserLocation { return nil }
|
||||||
|
let isOriginal = (annotation as AnyObject) === punchPointAnnotation
|
||||||
|
let reuseIdentifier = isOriginal ? "PunchPointImageMarker" : "PunchPointSelectedMarker"
|
||||||
|
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier)
|
||||||
|
?? MKAnnotationView(annotation: annotation, reuseIdentifier: reuseIdentifier)
|
||||||
|
annotationView.annotation = annotation
|
||||||
|
annotationView.canShowCallout = false
|
||||||
|
annotationView.centerOffset = CGPoint(x: 0, y: -12)
|
||||||
|
|
||||||
|
if isOriginal, let punchPointImageURL {
|
||||||
|
annotationView.image = UIImage(named: "punch_point_marker")
|
||||||
|
KingfisherManager.shared.retrieveImage(with: punchPointImageURL) { result in
|
||||||
|
guard case let .success(value) = result else { return }
|
||||||
|
Task { @MainActor in
|
||||||
|
guard annotationView.annotation === annotation else { return }
|
||||||
|
annotationView.image = Self.roundedMarkerImage(value.image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
annotationView.image = UIImage(named: "punch_point_marker")
|
||||||
|
}
|
||||||
|
return annotationView
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
|
||||||
|
guard userLocation.location != nil else { return }
|
||||||
|
fitPunchPointAndUserLocation()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func fitPunchPointAndUserLocation() {
|
||||||
|
guard fitWithUserLocation else { return }
|
||||||
|
fitAllMarkers(edgePadding: UIEdgeInsets(top: 80, left: 50, bottom: 80, right: 50))
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func roundedMarkerImage(_ source: UIImage) -> UIImage {
|
||||||
|
let size = CGSize(width: 40, height: 40)
|
||||||
|
return UIGraphicsImageRenderer(size: size).image { _ in
|
||||||
|
UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 6).addClip()
|
||||||
|
source.draw(in: CGRect(origin: .zero, size: size))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func mapTapped(_ gesture: UITapGestureRecognizer) {
|
||||||
|
guard gesture.state == .ended else { return }
|
||||||
|
let point = gesture.location(in: mapView)
|
||||||
|
onMapTap?(mapView.convert(point, toCoordinateFrom: mapView))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateZoom(scale: Double) {
|
||||||
|
let current = mapView.region
|
||||||
|
let span = MKCoordinateSpan(
|
||||||
|
latitudeDelta: min(max(current.span.latitudeDelta * scale, 0.0005), 120),
|
||||||
|
longitudeDelta: min(max(current.span.longitudeDelta * scale, 0.0005), 120)
|
||||||
|
)
|
||||||
|
mapView.setRegion(MKCoordinateRegion(center: current.center, span: span), animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
/// 打卡点地图视图,真机封装高德地图 marker、缩放、定位与点击选点行为。
|
||||||
final class PunchPointMapView: UIView, MAMapViewDelegate {
|
final class PunchPointMapView: UIView, MAMapViewDelegate {
|
||||||
var onMapTap: ((CLLocationCoordinate2D) -> Void)?
|
var onMapTap: ((CLLocationCoordinate2D) -> Void)?
|
||||||
|
|
||||||
@ -614,6 +804,7 @@ final class PunchPointMapView: UIView, MAMapViewDelegate {
|
|||||||
onMapTap?(poi.coordinate)
|
onMapTap?(poi.coordinate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
/// 打卡点地图控制按钮。
|
/// 打卡点地图控制按钮。
|
||||||
/// 地图悬浮控制按钮。
|
/// 地图悬浮控制按钮。
|
||||||
|
|||||||
@ -20,7 +20,9 @@ final class TravelAlbumCodeDialogViewController: UIViewController {
|
|||||||
private let albumNameLabel = UILabel()
|
private let albumNameLabel = UILabel()
|
||||||
private let qrImageView = UIImageView()
|
private let qrImageView = UIImageView()
|
||||||
private let loadingIndicator = UIActivityIndicatorView(style: .medium)
|
private let loadingIndicator = UIActivityIndicatorView(style: .medium)
|
||||||
private let hintLabel = UILabel()
|
private let hintLabel = TravelAlbumCodeHintLabel(
|
||||||
|
contentInsets: UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16)
|
||||||
|
)
|
||||||
private let downloadButton = UIButton(type: .system)
|
private let downloadButton = UIButton(type: .system)
|
||||||
|
|
||||||
init(state: TravelAlbumEntryViewModel.AlbumCodeState) {
|
init(state: TravelAlbumEntryViewModel.AlbumCodeState) {
|
||||||
@ -117,8 +119,8 @@ final class TravelAlbumCodeDialogViewController: UIViewController {
|
|||||||
hintLabel.snp.makeConstraints { make in
|
hintLabel.snp.makeConstraints { make in
|
||||||
make.top.equalTo(qrImageView.snp.bottom).offset(18)
|
make.top.equalTo(qrImageView.snp.bottom).offset(18)
|
||||||
make.centerX.equalToSuperview()
|
make.centerX.equalToSuperview()
|
||||||
make.height.equalTo(34)
|
|
||||||
make.leading.greaterThanOrEqualToSuperview().offset(20)
|
make.leading.greaterThanOrEqualToSuperview().offset(20)
|
||||||
|
make.trailing.lessThanOrEqualToSuperview().offset(-20)
|
||||||
}
|
}
|
||||||
downloadButton.snp.makeConstraints { make in
|
downloadButton.snp.makeConstraints { make in
|
||||||
make.top.equalTo(hintLabel.snp.bottom).offset(20)
|
make.top.equalTo(hintLabel.snp.bottom).offset(20)
|
||||||
@ -160,3 +162,30 @@ final class TravelAlbumCodeDialogViewController: UIViewController {
|
|||||||
onDownload?()
|
onDownload?()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 为相册码提示文案提供与 Android 弹窗一致的内容留白。
|
||||||
|
private final class TravelAlbumCodeHintLabel: UILabel {
|
||||||
|
private let contentInsets: UIEdgeInsets
|
||||||
|
|
||||||
|
init(contentInsets: UIEdgeInsets) {
|
||||||
|
self.contentInsets = contentInsets
|
||||||
|
super.init(frame: .zero)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func drawText(in rect: CGRect) {
|
||||||
|
super.drawText(in: rect.inset(by: contentInsets))
|
||||||
|
}
|
||||||
|
|
||||||
|
override var intrinsicContentSize: CGSize {
|
||||||
|
let size = super.intrinsicContentSize
|
||||||
|
return CGSize(
|
||||||
|
width: size.width + contentInsets.left + contentInsets.right,
|
||||||
|
height: size.height + contentInsets.top + contentInsets.bottom
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -184,7 +184,8 @@ extension TravelAlbumEntryViewController: UITableViewDelegate {
|
|||||||
/// 新建相册任务蓝色入口卡片。
|
/// 新建相册任务蓝色入口卡片。
|
||||||
private final class TravelAlbumHeroCard: UIControl {
|
private final class TravelAlbumHeroCard: UIControl {
|
||||||
private let gradientLayer = CAGradientLayer()
|
private let gradientLayer = CAGradientLayer()
|
||||||
private let plusView = UIImageView(image: UIImage(systemName: "plus"))
|
private let plusView = UIView()
|
||||||
|
private let plusImageView = UIImageView(image: UIImage(systemName: "plus"))
|
||||||
private let titleLabel = UILabel()
|
private let titleLabel = UILabel()
|
||||||
private let subtitleLabel = UILabel()
|
private let subtitleLabel = UILabel()
|
||||||
private let arrowLabel = UILabel()
|
private let arrowLabel = UILabel()
|
||||||
@ -202,10 +203,11 @@ private final class TravelAlbumHeroCard: UIControl {
|
|||||||
gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
|
gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
|
||||||
layer.insertSublayer(gradientLayer, at: 0)
|
layer.insertSublayer(gradientLayer, at: 0)
|
||||||
|
|
||||||
plusView.tintColor = AppColor.primary
|
|
||||||
plusView.contentMode = .scaleAspectFit
|
|
||||||
plusView.backgroundColor = .white
|
plusView.backgroundColor = .white
|
||||||
plusView.layer.cornerRadius = 17
|
plusView.layer.cornerRadius = 17
|
||||||
|
plusView.clipsToBounds = true
|
||||||
|
plusImageView.tintColor = AppColor.primary
|
||||||
|
plusImageView.contentMode = .scaleAspectFit
|
||||||
|
|
||||||
titleLabel.text = "新建相册任务"
|
titleLabel.text = "新建相册任务"
|
||||||
titleLabel.font = .systemFont(ofSize: 19, weight: .medium)
|
titleLabel.font = .systemFont(ofSize: 19, weight: .medium)
|
||||||
@ -218,6 +220,7 @@ private final class TravelAlbumHeroCard: UIControl {
|
|||||||
arrowLabel.font = .systemFont(ofSize: 36, weight: .light)
|
arrowLabel.font = .systemFont(ofSize: 36, weight: .light)
|
||||||
|
|
||||||
addSubview(plusView)
|
addSubview(plusView)
|
||||||
|
plusView.addSubview(plusImageView)
|
||||||
addSubview(titleLabel)
|
addSubview(titleLabel)
|
||||||
addSubview(subtitleLabel)
|
addSubview(subtitleLabel)
|
||||||
addSubview(arrowLabel)
|
addSubview(arrowLabel)
|
||||||
@ -226,6 +229,10 @@ private final class TravelAlbumHeroCard: UIControl {
|
|||||||
make.centerY.equalToSuperview()
|
make.centerY.equalToSuperview()
|
||||||
make.size.equalTo(34)
|
make.size.equalTo(34)
|
||||||
}
|
}
|
||||||
|
plusImageView.snp.makeConstraints { make in
|
||||||
|
make.center.equalToSuperview()
|
||||||
|
make.size.equalTo(26)
|
||||||
|
}
|
||||||
titleLabel.snp.makeConstraints { make in
|
titleLabel.snp.makeConstraints { make in
|
||||||
make.top.equalToSuperview().offset(30)
|
make.top.equalToSuperview().offset(30)
|
||||||
make.leading.equalTo(plusView.snp.trailing).offset(16)
|
make.leading.equalTo(plusView.snp.trailing).offset(16)
|
||||||
|
|||||||
@ -4,10 +4,15 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Kingfisher
|
import Kingfisher
|
||||||
import MAMapKit
|
|
||||||
import SnapKit
|
import SnapKit
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
|
#if targetEnvironment(simulator)
|
||||||
|
import MapKit
|
||||||
|
#else
|
||||||
|
import MAMapKit
|
||||||
|
#endif
|
||||||
|
|
||||||
/// 我的举报列表页,支持按状态筛选并进入举报详情。
|
/// 我的举报列表页,支持按状态筛选并进入举报详情。
|
||||||
final class WildPhotographerReportListViewController: BaseViewController {
|
final class WildPhotographerReportListViewController: BaseViewController {
|
||||||
private let viewModel: WildPhotographerReportListViewModel
|
private let viewModel: WildPhotographerReportListViewModel
|
||||||
@ -1797,6 +1802,49 @@ private final class WildReportDetailMediaThumbnailView: UIView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 举报详情页地图预览,绘制简化道路、路径和定位点。
|
/// 举报详情页地图预览,绘制简化道路、路径和定位点。
|
||||||
|
#if targetEnvironment(simulator)
|
||||||
|
private final class WildReportDetailMapPreviewView: UIView {
|
||||||
|
private let mapView: MKMapView
|
||||||
|
|
||||||
|
init(scenicName: String, detailAddress: String, coordinate: CLLocationCoordinate2D?) {
|
||||||
|
mapView = MKMapView(frame: .zero)
|
||||||
|
super.init(frame: .zero)
|
||||||
|
backgroundColor = AppColor.pageBackgroundSoft
|
||||||
|
layer.cornerRadius = 10
|
||||||
|
clipsToBounds = true
|
||||||
|
|
||||||
|
mapView.showsCompass = false
|
||||||
|
mapView.showsScale = false
|
||||||
|
mapView.isScrollEnabled = false
|
||||||
|
mapView.isZoomEnabled = false
|
||||||
|
mapView.isRotateEnabled = false
|
||||||
|
mapView.isPitchEnabled = false
|
||||||
|
addSubview(mapView)
|
||||||
|
mapView.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let coordinate else { return }
|
||||||
|
let annotation = MKPointAnnotation()
|
||||||
|
annotation.coordinate = coordinate
|
||||||
|
annotation.title = scenicName
|
||||||
|
annotation.subtitle = detailAddress
|
||||||
|
mapView.addAnnotation(annotation)
|
||||||
|
mapView.setRegion(
|
||||||
|
MKCoordinateRegion(
|
||||||
|
center: coordinate,
|
||||||
|
span: MKCoordinateSpan(latitudeDelta: 0.006, longitudeDelta: 0.006)
|
||||||
|
),
|
||||||
|
animated: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
private final class WildReportDetailMapPreviewView: UIView {
|
private final class WildReportDetailMapPreviewView: UIView {
|
||||||
private let mapView: MAMapView
|
private let mapView: MAMapView
|
||||||
|
|
||||||
@ -1834,6 +1882,7 @@ private final class WildReportDetailMapPreviewView: UIView {
|
|||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
/// 举报详情页处理进度时间线单行。
|
/// 举报详情页处理进度时间线单行。
|
||||||
private final class WildReportDetailTimelineRowView: UIView {
|
private final class WildReportDetailTimelineRowView: UIView {
|
||||||
|
|||||||
@ -3,25 +3,29 @@
|
|||||||
// suixinkan
|
// suixinkan
|
||||||
//
|
//
|
||||||
|
|
||||||
#if __has_include(<AMapFoundationKit/AMapFoundationKit.h>)
|
#if !TARGET_OS_SIMULATOR && __has_include(<AMapFoundationKit/AMapFoundationKit.h>)
|
||||||
#import <AMapFoundationKit/AMapFoundationKit.h>
|
#import <AMapFoundationKit/AMapFoundationKit.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if __has_include(<AMapLocationKit/AMapLocationKit.h>)
|
#if !TARGET_OS_SIMULATOR && __has_include(<AMapLocationKit/AMapLocationKit.h>)
|
||||||
#import <AMapLocationKit/AMapLocationKit.h>
|
#import <AMapLocationKit/AMapLocationKit.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if __has_include(<MAMapKit/MAMapKit.h>)
|
#if !TARGET_OS_SIMULATOR && __has_include(<MAMapKit/MAMapKit.h>)
|
||||||
#import <MAMapKit/MAMapKit.h>
|
#import <MAMapKit/MAMapKit.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if __has_include(<AMapSearchKit/AMapSearchKit.h>)
|
#if !TARGET_OS_SIMULATOR && __has_include(<AMapSearchKit/AMapSearchKit.h>)
|
||||||
#import <AMapSearchKit/AMapSearchKit.h>
|
#import <AMapSearchKit/AMapSearchKit.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if __has_include(<UMCommon/UMCommon.h>)
|
#if __has_include(<UMCommon/UMCommon.h>)
|
||||||
#import <UMCommon/UMCommon.h>
|
#import <UMCommon/UMCommon.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if __has_include(<JPUSHService.h>)
|
||||||
|
#import <JPUSHService.h>
|
||||||
|
#endif
|
||||||
#import "Features/ScenicQueue/Services/AliyunNuiTTSBridge.h"
|
#import "Features/ScenicQueue/Services/AliyunNuiTTSBridge.h"
|
||||||
|
|
||||||
#if __has_include(<UMAPM/UMAPM.h>)
|
#if __has_include(<UMAPM/UMAPM.h>)
|
||||||
|
|||||||
@ -2,6 +2,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>aps-environment</key>
|
||||||
|
<string>$(APS_ENVIRONMENT)</string>
|
||||||
<key>com.apple.developer.associated-domains</key>
|
<key>com.apple.developer.associated-domains</key>
|
||||||
<array>
|
<array>
|
||||||
<string>applinks:www.zhifly.cn</string>
|
<string>applinks:www.zhifly.cn</string>
|
||||||
|
|||||||
@ -29,7 +29,7 @@ final class AllFunctionsViewModelTests: XCTestCase {
|
|||||||
super.tearDown()
|
super.tearDown()
|
||||||
}
|
}
|
||||||
|
|
||||||
func testLoadFunctionsUsesDefaultFirstFourWhenNoSavedURIs() {
|
func testLoadFunctionsUsesDefaultFirstFourAndExcludesMessageCenter() {
|
||||||
appStore.permissions.savePermissionItems([
|
appStore.permissions.savePermissionItems([
|
||||||
HomePermissionItem(id: "1", name: "钱包", uri: "wallet"),
|
HomePermissionItem(id: "1", name: "钱包", uri: "wallet"),
|
||||||
HomePermissionItem(id: "2", name: "云盘", uri: "cloud_management"),
|
HomePermissionItem(id: "2", name: "云盘", uri: "cloud_management"),
|
||||||
@ -44,7 +44,7 @@ final class AllFunctionsViewModelTests: XCTestCase {
|
|||||||
XCTAssertEqual(viewModel.commonMenus.map(\.uri), [
|
XCTAssertEqual(viewModel.commonMenus.map(\.uri), [
|
||||||
"wallet", "cloud_management", "task_management", "system_settings",
|
"wallet", "cloud_management", "task_management", "system_settings",
|
||||||
])
|
])
|
||||||
XCTAssertEqual(viewModel.moreMenus.map(\.uri), ["message_center"])
|
XCTAssertTrue(viewModel.moreMenus.isEmpty)
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
menuStore.savedCommonURIs(
|
menuStore.savedCommonURIs(
|
||||||
accountScope: appStore.session.accountCachePrefix,
|
accountScope: appStore.session.accountCachePrefix,
|
||||||
|
|||||||
277
suixinkanTests/CommissionRateLogTests.swift
Normal file
277
suixinkanTests/CommissionRateLogTests.swift
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
//
|
||||||
|
// CommissionRateLogTests.swift
|
||||||
|
// suixinkanTests
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import XCTest
|
||||||
|
@testable import suixinkan
|
||||||
|
|
||||||
|
/// 获客员分成比例修改记录模型、接口与分页行为测试。
|
||||||
|
@MainActor
|
||||||
|
final class CommissionRateLogTests: XCTestCase {
|
||||||
|
func testModelDecodesCompleteRecordAndBuildsBusinessDisplayText() throws {
|
||||||
|
let data = """
|
||||||
|
{
|
||||||
|
"list": [{
|
||||||
|
"id": 1,
|
||||||
|
"binding_id": 10,
|
||||||
|
"sale_user_id": 456,
|
||||||
|
"store_user_id": 732,
|
||||||
|
"before_rate": 10,
|
||||||
|
"before_rate_label": "10%(旧)",
|
||||||
|
"after_rate": 20,
|
||||||
|
"after_rate_label": "20%(新)",
|
||||||
|
"operator_type": "store_user",
|
||||||
|
"operator_id": 732,
|
||||||
|
"operator_name": "李师傅",
|
||||||
|
"remark": " 调整合作比例 ",
|
||||||
|
"created_at": "2026-07-23 11:00:00"
|
||||||
|
}],
|
||||||
|
"total": 1,
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 10
|
||||||
|
}
|
||||||
|
""".data(using: .utf8)!
|
||||||
|
|
||||||
|
let response = try JSONDecoder().decode(CommissionRateLogListResponse.self, from: data)
|
||||||
|
let item = try XCTUnwrap(response.list.first)
|
||||||
|
|
||||||
|
XCTAssertEqual(response.pageSize, 10)
|
||||||
|
XCTAssertEqual(item.bindingId, 10)
|
||||||
|
XCTAssertEqual(item.saleUserId, 456)
|
||||||
|
XCTAssertEqual(item.storeUserId, 732)
|
||||||
|
XCTAssertEqual(item.operatorType, "store_user")
|
||||||
|
XCTAssertEqual(item.operatorId, 732)
|
||||||
|
XCTAssertEqual(item.displayBeforeRate, "10%(旧)")
|
||||||
|
XCTAssertEqual(item.displayAfterRate, "20%(新)")
|
||||||
|
XCTAssertEqual(item.metadataLine, "2026-07-23 11:00")
|
||||||
|
XCTAssertEqual(item.displayRemark, "调整合作比例")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDisplayFallsBackToNumericRateUnknownOperatorAndRawTime() throws {
|
||||||
|
let data = """
|
||||||
|
{
|
||||||
|
"list": [{
|
||||||
|
"id": 2,
|
||||||
|
"binding_id": 0,
|
||||||
|
"sale_user_id": 456,
|
||||||
|
"store_user_id": 0,
|
||||||
|
"before_rate": 5,
|
||||||
|
"before_rate_label": "",
|
||||||
|
"after_rate": 15,
|
||||||
|
"after_rate_label": " ",
|
||||||
|
"operator_type": "",
|
||||||
|
"operator_id": 0,
|
||||||
|
"operator_name": "",
|
||||||
|
"remark": " ",
|
||||||
|
"created_at": "服务端时间"
|
||||||
|
}],
|
||||||
|
"total": 1,
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 10
|
||||||
|
}
|
||||||
|
""".data(using: .utf8)!
|
||||||
|
|
||||||
|
let item = try JSONDecoder().decode(CommissionRateLogListResponse.self, from: data).list[0]
|
||||||
|
|
||||||
|
XCTAssertEqual(item.displayBeforeRate, "5%")
|
||||||
|
XCTAssertEqual(item.displayAfterRate, "15%")
|
||||||
|
XCTAssertEqual(item.metadataLine, "服务端时间")
|
||||||
|
XCTAssertNil(item.displayRemark)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAPIBuildsPathAndClampsPaginationParameters() async throws {
|
||||||
|
let session = MockURLSession(responses: [envelope(list: [], total: 0, page: 1)])
|
||||||
|
let api = OrderAPI(client: APIClient(environment: .testing, session: session))
|
||||||
|
|
||||||
|
_ = try await api.saleUserCommissionRateLogs(saleUserId: 456, page: 0, pageSize: 99)
|
||||||
|
|
||||||
|
let request = try XCTUnwrap(session.requests.first)
|
||||||
|
XCTAssertEqual(request.httpMethod, "GET")
|
||||||
|
XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/sale-user/commission-rate/logs")
|
||||||
|
let query = URLComponents(url: try XCTUnwrap(request.url), resolvingAgainstBaseURL: false)?.queryItems
|
||||||
|
XCTAssertEqual(query?.first { $0.name == "sale_user_id" }?.value, "456")
|
||||||
|
XCTAssertEqual(query?.first { $0.name == "page" }?.value, "1")
|
||||||
|
XCTAssertEqual(query?.first { $0.name == "page_size" }?.value, "50")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testViewModelRetriesFailedPageAndDeduplicatesItems() async throws {
|
||||||
|
let page1 = envelope(
|
||||||
|
list: [
|
||||||
|
record(id: 1, before: 10, after: 20),
|
||||||
|
record(id: 1, before: 10, after: 20),
|
||||||
|
record(id: 2, before: 20, after: 30),
|
||||||
|
],
|
||||||
|
total: 3,
|
||||||
|
page: 1
|
||||||
|
)
|
||||||
|
let page2 = envelope(
|
||||||
|
list: [
|
||||||
|
record(id: 2, before: 20, after: 30),
|
||||||
|
record(id: 3, before: 30, after: 40),
|
||||||
|
],
|
||||||
|
total: 3,
|
||||||
|
page: 2
|
||||||
|
)
|
||||||
|
let response = HTTPURLResponse(
|
||||||
|
url: URL(string: "https://api-test.zhifly.cn/mock")!,
|
||||||
|
statusCode: 200,
|
||||||
|
httpVersion: nil,
|
||||||
|
headerFields: nil
|
||||||
|
)!
|
||||||
|
let session = MockURLSession(results: [
|
||||||
|
.success((page1, response)),
|
||||||
|
.failure(URLError(.timedOut)),
|
||||||
|
.success((page2, response)),
|
||||||
|
])
|
||||||
|
let api = OrderAPI(client: APIClient(environment: .testing, session: session))
|
||||||
|
let viewModel = CommissionRateLogViewModel(saleUserId: 456)
|
||||||
|
|
||||||
|
await viewModel.refresh(api: api)
|
||||||
|
await viewModel.loadMore(api: api)
|
||||||
|
|
||||||
|
XCTAssertEqual(viewModel.items.map(\.id), [1, 2])
|
||||||
|
XCTAssertTrue(viewModel.canLoadMore)
|
||||||
|
XCTAssertFalse(viewModel.isLoadingMore)
|
||||||
|
|
||||||
|
await viewModel.loadMore(api: api)
|
||||||
|
|
||||||
|
XCTAssertEqual(viewModel.items.map(\.id), [1, 2, 3])
|
||||||
|
XCTAssertFalse(viewModel.canLoadMore)
|
||||||
|
let requestedPages = session.requests.compactMap { request in
|
||||||
|
URLComponents(url: request.url!, resolvingAgainstBaseURL: false)?
|
||||||
|
.queryItems?
|
||||||
|
.first { $0.name == "page" }?
|
||||||
|
.value
|
||||||
|
}
|
||||||
|
XCTAssertEqual(requestedPages, ["1", "2", "2"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTimelineCellOnlyMarksLatestRecord() {
|
||||||
|
let item = CommissionRateLogEntity(
|
||||||
|
id: 1,
|
||||||
|
bindingId: 10,
|
||||||
|
saleUserId: 456,
|
||||||
|
storeUserId: 732,
|
||||||
|
beforeRate: 10,
|
||||||
|
beforeRateLabel: "10%",
|
||||||
|
afterRate: 20,
|
||||||
|
afterRateLabel: "20%",
|
||||||
|
operatorType: "store_user",
|
||||||
|
operatorId: 732,
|
||||||
|
operatorName: "李师傅",
|
||||||
|
remark: "",
|
||||||
|
createdAt: "2026-07-23 11:00:00"
|
||||||
|
)
|
||||||
|
let cell = CommissionRateLogTimelineCell(style: .default, reuseIdentifier: nil)
|
||||||
|
|
||||||
|
cell.configure(with: item, isLatest: true)
|
||||||
|
XCTAssertTrue(cell.accessibilityLabel?.contains("最新记录") == true)
|
||||||
|
XCTAssertFalse(cell.accessibilityLabel?.contains("李师傅") == true)
|
||||||
|
|
||||||
|
cell.configure(with: item, isLatest: false)
|
||||||
|
XCTAssertFalse(cell.accessibilityLabel?.contains("最新记录") == true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPageTitleUsesAcquirerNameAndFallsBackWhenEmpty() {
|
||||||
|
let namedController = CommissionRateLogViewController(
|
||||||
|
saleUserId: 456,
|
||||||
|
acquirerName: " 获客员小王 "
|
||||||
|
)
|
||||||
|
namedController.setupNavigationBar()
|
||||||
|
XCTAssertEqual(namedController.title, "给获客员小王的分成记录")
|
||||||
|
|
||||||
|
let fallbackController = CommissionRateLogViewController(
|
||||||
|
saleUserId: 456,
|
||||||
|
acquirerName: " "
|
||||||
|
)
|
||||||
|
fallbackController.setupNavigationBar()
|
||||||
|
XCTAssertEqual(fallbackController.title, "修改记录")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAcquirerCardUsesUnifiedActionsAndBusinessHierarchy() throws {
|
||||||
|
let cell = CooperationAcquirerCell(style: .default, reuseIdentifier: nil)
|
||||||
|
cell.configure(
|
||||||
|
with: CooperativeSalerEntity(
|
||||||
|
saleUserId: 456,
|
||||||
|
name: "获客员小王",
|
||||||
|
salerPhone: "13800138000",
|
||||||
|
commissionRate: 20,
|
||||||
|
commissionRateLabel: "20%",
|
||||||
|
bindTime: "2026-07-23 11:00:00"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
let fittingSize = cell.contentView.systemLayoutSizeFitting(
|
||||||
|
CGSize(width: 390, height: UIView.layoutFittingCompressedSize.height),
|
||||||
|
withHorizontalFittingPriority: .required,
|
||||||
|
verticalFittingPriority: .fittingSizeLevel
|
||||||
|
)
|
||||||
|
cell.frame = CGRect(x: 0, y: 0, width: 390, height: fittingSize.height)
|
||||||
|
cell.layoutIfNeeded()
|
||||||
|
|
||||||
|
let remarkButton = try XCTUnwrap(
|
||||||
|
subview(in: cell, identifier: "cooperation_acquirer_edit_remark") as? UIButton
|
||||||
|
)
|
||||||
|
let logButton = try XCTUnwrap(
|
||||||
|
subview(in: cell, identifier: "cooperation_acquirer_commission_logs") as? UIButton
|
||||||
|
)
|
||||||
|
let commissionButton = try XCTUnwrap(
|
||||||
|
subview(in: cell, identifier: "cooperation_acquirer_edit_commission") as? UIButton
|
||||||
|
)
|
||||||
|
let bindTimeLabel = try XCTUnwrap(
|
||||||
|
subview(in: cell, identifier: "cooperation_acquirer_bind_time") as? UILabel
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(remarkButton.bounds.height, AppSpacing.minTouchTarget, accuracy: 0.5)
|
||||||
|
XCTAssertEqual(logButton.bounds.height, remarkButton.bounds.height, accuracy: 0.5)
|
||||||
|
XCTAssertEqual(commissionButton.bounds.height, remarkButton.bounds.height, accuracy: 0.5)
|
||||||
|
XCTAssertEqual(logButton.bounds.width, remarkButton.bounds.width, accuracy: 0.5)
|
||||||
|
XCTAssertEqual(commissionButton.bounds.width, remarkButton.bounds.width, accuracy: 0.5)
|
||||||
|
XCTAssertEqual(bindTimeLabel.text, "绑定于 2026-07-23 11:00")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func envelope(list: [[String: Any]], total: Int, page: Int) -> Data {
|
||||||
|
let object: [String: Any] = [
|
||||||
|
"code": 100000,
|
||||||
|
"msg": "success",
|
||||||
|
"data": [
|
||||||
|
"list": list,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"page_size": 10,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
return try! JSONSerialization.data(withJSONObject: object)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func record(id: Int, before: Int, after: Int) -> [String: Any] {
|
||||||
|
[
|
||||||
|
"id": id,
|
||||||
|
"binding_id": 10,
|
||||||
|
"sale_user_id": 456,
|
||||||
|
"store_user_id": 732,
|
||||||
|
"before_rate": before,
|
||||||
|
"before_rate_label": "\(before)%",
|
||||||
|
"after_rate": after,
|
||||||
|
"after_rate_label": "\(after)%",
|
||||||
|
"operator_type": "store_user",
|
||||||
|
"operator_id": 732,
|
||||||
|
"operator_name": "李师傅",
|
||||||
|
"remark": "",
|
||||||
|
"created_at": "2026-07-23 11:00:00",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private func subview(in root: UIView, identifier: String) -> UIView? {
|
||||||
|
if root.accessibilityIdentifier == identifier {
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
for child in root.subviews {
|
||||||
|
if let match = subview(in: child, identifier: identifier) {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
204
suixinkanTests/CoreLocationProviderTests.swift
Normal file
204
suixinkanTests/CoreLocationProviderTests.swift
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
//
|
||||||
|
// CoreLocationProviderTests.swift
|
||||||
|
// suixinkanTests
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreLocation
|
||||||
|
import XCTest
|
||||||
|
@testable import suixinkan
|
||||||
|
|
||||||
|
/// Core Location fallback 的授权、定位与逆地理编码测试。
|
||||||
|
@MainActor
|
||||||
|
final class CoreLocationProviderTests: XCTestCase {
|
||||||
|
|
||||||
|
func testPrivacyAgreementIsRequiredBeforeLocation() async {
|
||||||
|
let context = makeContext(privacyAccepted: false)
|
||||||
|
|
||||||
|
await assertLocationError(.privacyNotAccepted) {
|
||||||
|
_ = try await context.provider.requestCoordinate()
|
||||||
|
}
|
||||||
|
XCTAssertEqual(context.manager.requestLocationCallCount, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDeniedAuthorizationReturnsPermissionError() async {
|
||||||
|
let context = makeContext(authorizationStatus: .denied)
|
||||||
|
|
||||||
|
await assertLocationError(.permissionDenied) {
|
||||||
|
_ = try await context.provider.requestCoordinate()
|
||||||
|
}
|
||||||
|
XCTAssertEqual(context.manager.requestLocationCallCount, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNotDeterminedAuthorizationContinuesAfterGrant() async throws {
|
||||||
|
let coordinate = CLLocationCoordinate2D(latitude: 30.2741, longitude: 120.1551)
|
||||||
|
let context = makeContext(
|
||||||
|
authorizationStatus: .notDetermined,
|
||||||
|
requestedAuthorizationStatus: .authorizedWhenInUse,
|
||||||
|
locationResult: .success(CLLocation(
|
||||||
|
latitude: coordinate.latitude,
|
||||||
|
longitude: coordinate.longitude
|
||||||
|
))
|
||||||
|
)
|
||||||
|
|
||||||
|
let result = try await context.provider.requestCoordinate(
|
||||||
|
desiredAccuracy: kCLLocationAccuracyHundredMeters
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(context.manager.requestAuthorizationCallCount, 1)
|
||||||
|
XCTAssertEqual(context.manager.requestLocationCallCount, 1)
|
||||||
|
XCTAssertEqual(context.manager.desiredAccuracy, kCLLocationAccuracyHundredMeters)
|
||||||
|
XCTAssertEqual(result.latitude, coordinate.latitude, accuracy: 0.000_001)
|
||||||
|
XCTAssertEqual(result.longitude, coordinate.longitude, accuracy: 0.000_001)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLocationFailureUsesExistingBusinessError() async {
|
||||||
|
let context = makeContext(locationResult: .failure)
|
||||||
|
|
||||||
|
await assertLocationError(.locationFailed) {
|
||||||
|
_ = try await context.provider.requestCoordinate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSnapshotContainsReverseGeocodedAddress() async throws {
|
||||||
|
let context = makeContext(
|
||||||
|
locationResult: .success(CLLocation(latitude: 31.2304, longitude: 121.4737)),
|
||||||
|
geocodedAddress: "上海市黄浦区"
|
||||||
|
)
|
||||||
|
|
||||||
|
let snapshot = try await context.provider.requestSnapshot()
|
||||||
|
|
||||||
|
XCTAssertEqual(snapshot.latitude, 31.2304, accuracy: 0.000_001)
|
||||||
|
XCTAssertEqual(snapshot.longitude, 121.4737, accuracy: 0.000_001)
|
||||||
|
XCTAssertEqual(snapshot.address, "上海市黄浦区")
|
||||||
|
XCTAssertEqual(context.geocoder.requestedCoordinates.count, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSnapshotFallsBackToEmptyAddressWhenGeocoderHasNoResult() async throws {
|
||||||
|
let context = makeContext(
|
||||||
|
locationResult: .success(CLLocation(latitude: 39.9042, longitude: 116.4074)),
|
||||||
|
geocodedAddress: ""
|
||||||
|
)
|
||||||
|
|
||||||
|
let snapshot = try await context.provider.requestSnapshot()
|
||||||
|
|
||||||
|
XCTAssertEqual(snapshot.address, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeContext(
|
||||||
|
privacyAccepted: Bool = true,
|
||||||
|
authorizationStatus: CLAuthorizationStatus = .authorizedWhenInUse,
|
||||||
|
requestedAuthorizationStatus: CLAuthorizationStatus = .authorizedWhenInUse,
|
||||||
|
locationResult: FakeCoreLocationManager.LocationResult = .success(
|
||||||
|
CLLocation(latitude: 39.9042, longitude: 116.4074)
|
||||||
|
),
|
||||||
|
geocodedAddress: String = "北京市东城区"
|
||||||
|
) -> TestContext {
|
||||||
|
let suiteName = "CoreLocationProviderTests.\(UUID().uuidString)"
|
||||||
|
let defaults = UserDefaults(suiteName: suiteName)!
|
||||||
|
defaults.removePersistentDomain(forName: suiteName)
|
||||||
|
let appStore = AppStore(defaults: defaults)
|
||||||
|
appStore.session.privacyAgreementAccepted = privacyAccepted
|
||||||
|
|
||||||
|
let manager = FakeCoreLocationManager(
|
||||||
|
authorizationStatus: authorizationStatus,
|
||||||
|
requestedAuthorizationStatus: requestedAuthorizationStatus,
|
||||||
|
locationResult: locationResult
|
||||||
|
)
|
||||||
|
let geocoder = FakeLocationAddressGeocoder(address: geocodedAddress)
|
||||||
|
let provider = CoreLocationProvider(
|
||||||
|
appStore: appStore,
|
||||||
|
manager: manager,
|
||||||
|
geocoder: geocoder
|
||||||
|
)
|
||||||
|
return TestContext(provider: provider, manager: manager, geocoder: geocoder)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func assertLocationError(
|
||||||
|
_ expected: LocationProviderError,
|
||||||
|
operation: () async throws -> Void
|
||||||
|
) async {
|
||||||
|
do {
|
||||||
|
try await operation()
|
||||||
|
XCTFail("Expected \(expected)")
|
||||||
|
} catch {
|
||||||
|
XCTAssertEqual(error as? LocationProviderError, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Core Location fallback 测试依赖集合。
|
||||||
|
@MainActor
|
||||||
|
private struct TestContext {
|
||||||
|
let provider: CoreLocationProvider
|
||||||
|
let manager: FakeCoreLocationManager
|
||||||
|
let geocoder: FakeLocationAddressGeocoder
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 可控的 `CLLocationManager` 替身。
|
||||||
|
@MainActor
|
||||||
|
private final class FakeCoreLocationManager: CoreLocationManaging {
|
||||||
|
|
||||||
|
/// 单次定位请求的模拟结果。
|
||||||
|
enum LocationResult {
|
||||||
|
case success(CLLocation)
|
||||||
|
case failure
|
||||||
|
}
|
||||||
|
|
||||||
|
var authorizationStatus: CLAuthorizationStatus
|
||||||
|
var desiredAccuracy: CLLocationAccuracy = kCLLocationAccuracyBest
|
||||||
|
weak var delegate: CLLocationManagerDelegate?
|
||||||
|
private(set) var requestAuthorizationCallCount = 0
|
||||||
|
private(set) var requestLocationCallCount = 0
|
||||||
|
|
||||||
|
private let requestedAuthorizationStatus: CLAuthorizationStatus
|
||||||
|
private let locationResult: LocationResult
|
||||||
|
|
||||||
|
init(
|
||||||
|
authorizationStatus: CLAuthorizationStatus,
|
||||||
|
requestedAuthorizationStatus: CLAuthorizationStatus,
|
||||||
|
locationResult: LocationResult
|
||||||
|
) {
|
||||||
|
self.authorizationStatus = authorizationStatus
|
||||||
|
self.requestedAuthorizationStatus = requestedAuthorizationStatus
|
||||||
|
self.locationResult = locationResult
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestWhenInUseAuthorization() {
|
||||||
|
requestAuthorizationCallCount += 1
|
||||||
|
authorizationStatus = requestedAuthorizationStatus
|
||||||
|
delegate?.locationManagerDidChangeAuthorization?(CLLocationManager())
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestLocation() {
|
||||||
|
requestLocationCallCount += 1
|
||||||
|
let callbackManager = CLLocationManager()
|
||||||
|
switch locationResult {
|
||||||
|
case let .success(location):
|
||||||
|
delegate?.locationManager?(callbackManager, didUpdateLocations: [location])
|
||||||
|
case .failure:
|
||||||
|
delegate?.locationManager?(
|
||||||
|
callbackManager,
|
||||||
|
didFailWithError: CLError(.locationUnknown)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 可控的逆地理编码替身。
|
||||||
|
@MainActor
|
||||||
|
private final class FakeLocationAddressGeocoder: LocationAddressGeocoding {
|
||||||
|
private(set) var requestedCoordinates: [CLLocationCoordinate2D] = []
|
||||||
|
private let address: String
|
||||||
|
|
||||||
|
init(address: String) {
|
||||||
|
self.address = address
|
||||||
|
}
|
||||||
|
|
||||||
|
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
|
||||||
|
requestedCoordinates.append(CLLocationCoordinate2D(
|
||||||
|
latitude: latitude,
|
||||||
|
longitude: longitude
|
||||||
|
))
|
||||||
|
return address
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user