Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6129a1632f | |||
| 7ccbc5568d | |||
| 787a166263 | |||
| 74f7370aab | |||
| 1b2637f0ee | |||
| caeeb9a1cf | |||
| e7f1d777dd |
3
Podfile
@ -8,6 +8,9 @@ target 'suixinkan' do
|
||||
pod 'UMCommon'
|
||||
pod 'UMDevice'
|
||||
pod 'UMAPM'
|
||||
# 极光推送:固定无 IDFA 的 JCore,避免引入 ATT 授权。
|
||||
pod 'JCore', '5.4.2-noidfa'
|
||||
pod 'JPush', '6.1.0'
|
||||
pod 'IQKeyboardManagerSwift'
|
||||
# 微信 Open SDK:仅接入分享能力,不调用支付 API。
|
||||
pod 'WechatOpenSDK-XCFramework', '~> 2.0.4'
|
||||
|
||||
11
Podfile.lock
@ -46,6 +46,9 @@ PODS:
|
||||
- IQKeyboardCore
|
||||
- IQTextView (1.0.5):
|
||||
- IQKeyboardToolbar/Placeholderable
|
||||
- JCore (5.4.2-noidfa)
|
||||
- JPush (6.1.0):
|
||||
- JCore (> 5.1.1)
|
||||
- UMAPM (2.0.7):
|
||||
- UMCommon
|
||||
- UMCommon (7.5.11):
|
||||
@ -58,6 +61,8 @@ DEPENDENCIES:
|
||||
- AMapLocation-NO-IDFA
|
||||
- AMapSearch-NO-IDFA
|
||||
- IQKeyboardManagerSwift
|
||||
- JCore (= 5.4.2-noidfa)
|
||||
- JPush (= 6.1.0)
|
||||
- UMAPM
|
||||
- UMCommon
|
||||
- UMDevice
|
||||
@ -77,6 +82,8 @@ SPEC REPOS:
|
||||
- IQKeyboardToolbarManager
|
||||
- IQTextInputViewNotification
|
||||
- IQTextView
|
||||
- JCore
|
||||
- JPush
|
||||
- UMAPM
|
||||
- UMCommon
|
||||
- UMDevice
|
||||
@ -95,11 +102,13 @@ SPEC CHECKSUMS:
|
||||
IQKeyboardToolbarManager: 242e858cc74e06e00aa989bea1355b24d4397854
|
||||
IQTextInputViewNotification: 3b9fb27a16e7ee8958cc9092cfb07a1a9e1fd559
|
||||
IQTextView: ae13b4922f22e6f027f62c557d9f4f236b19d5c7
|
||||
JCore: 696e0ac22f417bb0ac4729fc41bc2941aef14e2b
|
||||
JPush: 891c497687be788b094bde368d51289355ee1112
|
||||
UMAPM: ba9fbebe8dd2048b251c332ab0afc86e1ba5ea63
|
||||
UMCommon: d652b3b372a801b36db203f5b546ab3e15676898
|
||||
UMDevice: 20b7b3c37a36b2c5c7ca8b5a54386ff9ed2b3b19
|
||||
WechatOpenSDK-XCFramework: ff342ae616bb86df3d236aca38059dfd4bc4a949
|
||||
|
||||
PODFILE CHECKSUM: 0fd38f741cbaae39c8e7a70fa233f651d3b34d0f
|
||||
PODFILE CHECKSUM: e7971a58b5d46ffc03f36ef716b922b6a75a6412
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
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
@ -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
@ -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
@ -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
@ -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
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
@ -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
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
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
1023
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64_x86_64-simulator/Headers/JPUSHService.h
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
11
Pods/Manifest.lock
generated
@ -46,6 +46,9 @@ PODS:
|
||||
- IQKeyboardCore
|
||||
- IQTextView (1.0.5):
|
||||
- IQKeyboardToolbar/Placeholderable
|
||||
- JCore (5.4.2-noidfa)
|
||||
- JPush (6.1.0):
|
||||
- JCore (> 5.1.1)
|
||||
- UMAPM (2.0.7):
|
||||
- UMCommon
|
||||
- UMCommon (7.5.11):
|
||||
@ -58,6 +61,8 @@ DEPENDENCIES:
|
||||
- AMapLocation-NO-IDFA
|
||||
- AMapSearch-NO-IDFA
|
||||
- IQKeyboardManagerSwift
|
||||
- JCore (= 5.4.2-noidfa)
|
||||
- JPush (= 6.1.0)
|
||||
- UMAPM
|
||||
- UMCommon
|
||||
- UMDevice
|
||||
@ -77,6 +82,8 @@ SPEC REPOS:
|
||||
- IQKeyboardToolbarManager
|
||||
- IQTextInputViewNotification
|
||||
- IQTextView
|
||||
- JCore
|
||||
- JPush
|
||||
- UMAPM
|
||||
- UMCommon
|
||||
- UMDevice
|
||||
@ -95,11 +102,13 @@ SPEC CHECKSUMS:
|
||||
IQKeyboardToolbarManager: 242e858cc74e06e00aa989bea1355b24d4397854
|
||||
IQTextInputViewNotification: 3b9fb27a16e7ee8958cc9092cfb07a1a9e1fd559
|
||||
IQTextView: ae13b4922f22e6f027f62c557d9f4f236b19d5c7
|
||||
JCore: 696e0ac22f417bb0ac4729fc41bc2941aef14e2b
|
||||
JPush: 891c497687be788b094bde368d51289355ee1112
|
||||
UMAPM: ba9fbebe8dd2048b251c332ab0afc86e1ba5ea63
|
||||
UMCommon: d652b3b372a801b36db203f5b546ab3e15676898
|
||||
UMDevice: 20b7b3c37a36b2c5c7ca8b5a54386ff9ed2b3b19
|
||||
WechatOpenSDK-XCFramework: ff342ae616bb86df3d236aca38059dfd4bc4a949
|
||||
|
||||
PODFILE CHECKSUM: 0fd38f741cbaae39c8e7a70fa233f651d3b34d0f
|
||||
PODFILE CHECKSUM: e7971a58b5d46ffc03f36ef716b922b6a75a6412
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
5216
Pods/Pods.xcodeproj/project.pbxproj
generated
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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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.
|
||||
|
||||
|
||||
## JCore
|
||||
|
||||
Copyright jpush.cn
|
||||
|
||||
## JPush
|
||||
|
||||
Copyright jiguang.cn
|
||||
|
||||
## UMAPM
|
||||
|
||||
Copyright 2011 - 2020 umeng.com. All rights reserved.
|
||||
|
||||
@ -302,6 +302,26 @@ SOFTWARE.
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</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>
|
||||
<key>FooterText</key>
|
||||
<string>Copyright 2011 - 2020 umeng.com. All rights reserved.
|
||||
|
||||
@ -3,13 +3,13 @@ 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}/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
|
||||
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'
|
||||
LIBRARY_SEARCH_PATHS = $(inherited) "${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_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"
|
||||
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 "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}/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)
|
||||
|
||||
@ -3,13 +3,13 @@ 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}/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
|
||||
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'
|
||||
LIBRARY_SEARCH_PATHS = $(inherited) "${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_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"
|
||||
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 "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}/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)
|
||||
|
||||
@ -2,10 +2,10 @@ 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}/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
|
||||
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"
|
||||
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"
|
||||
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 "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_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
|
||||
|
||||
@ -2,10 +2,10 @@ 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}/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
|
||||
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"
|
||||
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"
|
||||
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 "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_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
|
||||
|
||||
47
docs/assets/travel-album-ai/01-photo-preview-flow.svg
Normal file
@ -0,0 +1,47 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="430" viewBox="0 0 1200 430">
|
||||
<defs>
|
||||
<marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
|
||||
<path d="M0,0 L0,6 L9,3 z" fill="#4D7CFE"/>
|
||||
</marker>
|
||||
<style>
|
||||
.title { font: 700 24px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #182230; }
|
||||
.label { font: 600 17px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #182230; }
|
||||
.small { font: 14px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #5D6B7C; }
|
||||
.box { fill: #FFFFFF; stroke: #B9C8DA; stroke-width: 2; }
|
||||
.active { fill: #EEF4FF; stroke: #4D7CFE; stroke-width: 2.5; }
|
||||
.action { fill: #F6F8FB; stroke: #D5DEE9; stroke-width: 2; }
|
||||
.line { fill: none; stroke: #4D7CFE; stroke-width: 2.5; marker-end: url(#arrow); }
|
||||
</style>
|
||||
</defs>
|
||||
<rect width="1200" height="430" rx="24" fill="#F7F9FC"/>
|
||||
<text x="42" y="52" class="title">照片预览与左右切换</text>
|
||||
<rect x="42" y="92" width="205" height="104" rx="16" class="box"/>
|
||||
<text x="144" y="126" text-anchor="middle" class="label">相册管理缩略图</text>
|
||||
<text x="144" y="158" text-anchor="middle" class="small">点击任意照片</text>
|
||||
<path d="M247 144 H326" class="line"/>
|
||||
<rect x="326" y="78" width="300" height="132" rx="18" class="active"/>
|
||||
<text x="476" y="115" text-anchor="middle" class="label">单一返回按钮 + 文件信息</text>
|
||||
<text x="476" y="146" text-anchor="middle" class="small">文件名 / 文件大小 / 当前照片</text>
|
||||
<text x="476" y="176" text-anchor="middle" class="small">底部:AI修图、删除、刷新</text>
|
||||
<path d="M626 144 H706" class="line"/>
|
||||
<rect x="706" y="78" width="205" height="132" rx="18" class="box"/>
|
||||
<text x="808" y="115" text-anchor="middle" class="label">左右滑动</text>
|
||||
<text x="808" y="146" text-anchor="middle" class="small">切换上一张 / 下一张</text>
|
||||
<text x="808" y="176" text-anchor="middle" class="small">首尾不循环</text>
|
||||
<path d="M911 144 H990" class="line"/>
|
||||
<rect x="990" y="92" width="168" height="104" rx="16" class="box"/>
|
||||
<text x="1074" y="126" text-anchor="middle" class="label">同步刷新信息</text>
|
||||
<text x="1074" y="158" text-anchor="middle" class="small">名称、大小、结果状态</text>
|
||||
<rect x="42" y="258" width="344" height="112" rx="16" class="action"/>
|
||||
<text x="214" y="292" text-anchor="middle" class="label">删除当前照片</text>
|
||||
<text x="214" y="323" text-anchor="middle" class="small">删除后定位到相邻照片</text>
|
||||
<text x="214" y="349" text-anchor="middle" class="small">最后一张删除后返回相册管理</text>
|
||||
<rect x="428" y="258" width="344" height="112" rx="16" class="action"/>
|
||||
<text x="600" y="292" text-anchor="middle" class="label">刷新当前照片</text>
|
||||
<text x="600" y="323" text-anchor="middle" class="small">重新加载原图或最新修图结果</text>
|
||||
<text x="600" y="349" text-anchor="middle" class="small">加载失败保留当前可见内容</text>
|
||||
<rect x="814" y="258" width="344" height="112" rx="16" class="action"/>
|
||||
<text x="986" y="292" text-anchor="middle" class="label">预览底部操作</text>
|
||||
<text x="986" y="323" text-anchor="middle" class="small">保留 AI修图、删除、刷新</text>
|
||||
<text x="986" y="349" text-anchor="middle" class="small">不展示打印按钮</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
44
docs/assets/travel-album-ai/02-multi-select-flow.svg
Normal file
@ -0,0 +1,44 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="430" viewBox="0 0 1200 430">
|
||||
<defs>
|
||||
<marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
|
||||
<path d="M0,0 L0,6 L9,3 z" fill="#4D7CFE"/>
|
||||
</marker>
|
||||
<style>
|
||||
.title { font: 700 24px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #182230; }
|
||||
.label { font: 600 17px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #182230; }
|
||||
.small { font: 14px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #5D6B7C; }
|
||||
.box { fill: #FFFFFF; stroke: #B9C8DA; stroke-width: 2; }
|
||||
.active { fill: #EEF4FF; stroke: #4D7CFE; stroke-width: 2.5; }
|
||||
.danger { fill: #FFF1F1; stroke: #E35D6A; stroke-width: 2; }
|
||||
.line { fill: none; stroke: #4D7CFE; stroke-width: 2.5; marker-end: url(#arrow); }
|
||||
</style>
|
||||
</defs>
|
||||
<rect width="1200" height="430" rx="24" fill="#F7F9FC"/>
|
||||
<text x="42" y="52" class="title">相册管理多选操作栏</text>
|
||||
<rect x="42" y="92" width="215" height="110" rx="16" class="box"/>
|
||||
<text x="149" y="127" text-anchor="middle" class="label">普通浏览状态</text>
|
||||
<text x="149" y="160" text-anchor="middle" class="small">底部显示“上传照片”</text>
|
||||
<path d="M257 147 H337" class="line"/>
|
||||
<rect x="337" y="92" width="215" height="110" rx="16" class="active"/>
|
||||
<text x="444" y="127" text-anchor="middle" class="label">点击右上角选择</text>
|
||||
<text x="444" y="160" text-anchor="middle" class="small">缩略图出现单选圆圈</text>
|
||||
<path d="M552 147 H632" class="line"/>
|
||||
<rect x="632" y="92" width="225" height="110" rx="16" class="active"/>
|
||||
<text x="744" y="127" text-anchor="middle" class="label">进入多选状态</text>
|
||||
<text x="744" y="160" text-anchor="middle" class="small">上传按钮隐藏</text>
|
||||
<text x="744" y="184" text-anchor="middle" class="small">显示 AI修图、删除</text>
|
||||
<path d="M857 147 H937" class="line"/>
|
||||
<rect x="937" y="92" width="221" height="110" rx="16" class="box"/>
|
||||
<text x="1048" y="127" text-anchor="middle" class="label">选择至少 1 张</text>
|
||||
<text x="1048" y="160" text-anchor="middle" class="small">两个操作按钮恢复可用</text>
|
||||
<rect x="140" y="268" width="310" height="110" rx="16" class="active"/>
|
||||
<text x="295" y="304" text-anchor="middle" class="label">点击 AI修图</text>
|
||||
<text x="295" y="336" text-anchor="middle" class="small">进入批量 AI 修图弹窗</text>
|
||||
<text x="295" y="360" text-anchor="middle" class="small">包含处理中照片时按钮置灰</text>
|
||||
<rect x="750" y="268" width="310" height="110" rx="16" class="danger"/>
|
||||
<text x="905" y="304" text-anchor="middle" class="label">点击删除</text>
|
||||
<text x="905" y="336" text-anchor="middle" class="small">二次确认后删除选中素材</text>
|
||||
<text x="905" y="360" text-anchor="middle" class="small">取消确认则保持原选择</text>
|
||||
<path d="M744 202 V235 H295 V268" class="line"/>
|
||||
<path d="M744 202 V235 H905 V268" class="line"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
31
docs/assets/travel-album-ai/02-upload-transfer-mode-flow.svg
Normal file
@ -0,0 +1,31 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="300" viewBox="0 0 1080 300">
|
||||
<defs>
|
||||
<marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto">
|
||||
<path d="M0,0 L0,6 L9,3 z" fill="#7A8699"/>
|
||||
</marker>
|
||||
<style>
|
||||
.title { font: 600 18px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #1F2937; }
|
||||
.detail { font: 14px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #667085; }
|
||||
.box { fill: #FFFFFF; stroke: #D9E2EF; stroke-width: 2; rx: 16; }
|
||||
.active { fill: #EEF5FF; stroke: #1677FF; stroke-width: 2; rx: 16; }
|
||||
</style>
|
||||
</defs>
|
||||
<rect width="1080" height="300" fill="#F7F9FC"/>
|
||||
<rect class="box" x="40" y="104" width="180" height="92"/>
|
||||
<text class="title" x="130" y="142" text-anchor="middle">相册管理</text>
|
||||
<text class="detail" x="130" y="170" text-anchor="middle">点击上传照片</text>
|
||||
<line x1="220" y1="150" x2="292" y2="150" stroke="#7A8699" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<rect class="active" x="305" y="70" width="230" height="160"/>
|
||||
<text class="title" x="420" y="108" text-anchor="middle">选择传输模式</text>
|
||||
<text class="detail" x="420" y="142" text-anchor="middle">边拍边传</text>
|
||||
<text class="detail" x="420" y="174" text-anchor="middle">拍后传输</text>
|
||||
<text class="detail" x="420" y="206" text-anchor="middle">取消:留在当前页</text>
|
||||
<line x1="535" y1="118" x2="622" y2="78" stroke="#7A8699" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<line x1="535" y1="182" x2="622" y2="222" stroke="#7A8699" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<rect class="box" x="635" y="28" width="400" height="100"/>
|
||||
<text class="title" x="835" y="68" text-anchor="middle">有线传输 · 边拍边传</text>
|
||||
<text class="detail" x="835" y="96" text-anchor="middle">新照片传入后自动上传当前相册</text>
|
||||
<rect class="box" x="635" y="172" width="400" height="100"/>
|
||||
<text class="title" x="835" y="212" text-anchor="middle">有线传输 · 拍后传输</text>
|
||||
<text class="detail" x="835" y="240" text-anchor="middle">拍摄完成后选择照片批量上传</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
56
docs/assets/travel-album-ai/03-ai-edit-sheet-flow.svg
Normal file
@ -0,0 +1,56 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="470" viewBox="0 0 1200 470">
|
||||
<defs>
|
||||
<marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
|
||||
<path d="M0,0 L0,6 L9,3 z" fill="#4D7CFE"/>
|
||||
</marker>
|
||||
<style>
|
||||
.title { font: 700 24px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #182230; }
|
||||
.label { font: 600 17px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #182230; }
|
||||
.small { font: 14px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #5D6B7C; }
|
||||
.box { fill: #FFFFFF; stroke: #B9C8DA; stroke-width: 2; }
|
||||
.active { fill: #EEF4FF; stroke: #4D7CFE; stroke-width: 2.5; }
|
||||
.optional { fill: #FFF8E8; stroke: #F2A93B; stroke-width: 2; }
|
||||
.line { fill: none; stroke: #4D7CFE; stroke-width: 2.5; marker-end: url(#arrow); }
|
||||
</style>
|
||||
</defs>
|
||||
<rect width="1200" height="470" rx="24" fill="#F7F9FC"/>
|
||||
<text x="42" y="52" class="title">AI 修图弹窗:原图精修、氛围感与结果组合</text>
|
||||
<rect x="42" y="92" width="190" height="116" rx="16" class="box"/>
|
||||
<text x="137" y="127" text-anchor="middle" class="label">打开 AI修图</text>
|
||||
<text x="137" y="158" text-anchor="middle" class="small">显示已选照片数量</text>
|
||||
<text x="137" y="183" text-anchor="middle" class="small">内容区域可纵向滚动</text>
|
||||
<path d="M232 150 H272" class="line"/>
|
||||
<rect x="272" y="78" width="230" height="144" rx="18" class="active"/>
|
||||
<text x="387" y="116" text-anchor="middle" class="label">原图精修</text>
|
||||
<text x="387" y="148" text-anchor="middle" class="small">预设文字 + 对应效果图</text>
|
||||
<text x="387" y="174" text-anchor="middle" class="small">横向滚动,始终单选</text>
|
||||
<text x="387" y="200" text-anchor="middle" class="small">生成基础“精修后”结果</text>
|
||||
<path d="M502 150 H542" class="line"/>
|
||||
<rect x="542" y="78" width="230" height="144" rx="18" class="optional"/>
|
||||
<text x="657" y="116" text-anchor="middle" class="label">氛围感修图(选填)</text>
|
||||
<text x="657" y="148" text-anchor="middle" class="small">横向滚动,多样式单选</text>
|
||||
<text x="657" y="174" text-anchor="middle" class="small">默认不选,重复点击可取消</text>
|
||||
<text x="657" y="200" text-anchor="middle" class="small">额外生成独立氛围感结果</text>
|
||||
<path d="M772 150 H812" class="line"/>
|
||||
<rect x="812" y="92" width="170" height="116" rx="16" class="box"/>
|
||||
<text x="897" y="127" text-anchor="middle" class="label">只读额度</text>
|
||||
<text x="897" y="158" text-anchor="middle" class="small">显示剩余 9 张</text>
|
||||
<text x="897" y="183" text-anchor="middle" class="small">不提供勾选操作</text>
|
||||
<path d="M982 150 H1014" class="line"/>
|
||||
<rect x="1014" y="92" width="144" height="116" rx="16" class="active"/>
|
||||
<text x="1086" y="127" text-anchor="middle" class="label">点击确定</text>
|
||||
<text x="1086" y="158" text-anchor="middle" class="small">提交样式组合</text>
|
||||
<text x="1086" y="183" text-anchor="middle" class="small">开始处理</text>
|
||||
<rect x="42" y="286" width="344" height="120" rx="16" class="box"/>
|
||||
<text x="214" y="322" text-anchor="middle" class="label">点击取消 / 下拉关闭</text>
|
||||
<text x="214" y="354" text-anchor="middle" class="small">不启动任务</text>
|
||||
<text x="214" y="380" text-anchor="middle" class="small">不改变原图、旧结果或额度状态</text>
|
||||
<rect x="428" y="286" width="344" height="120" rx="16" class="box"/>
|
||||
<text x="600" y="322" text-anchor="middle" class="label">效果图预览</text>
|
||||
<text x="600" y="354" text-anchor="middle" class="small">各样式卡片显示“预览”按钮</text>
|
||||
<text x="600" y="380" text-anchor="middle" class="small">全屏查看,不改变当前选中项</text>
|
||||
<rect x="814" y="286" width="344" height="120" rx="16" class="box"/>
|
||||
<text x="986" y="322" text-anchor="middle" class="label">结果独立保存</text>
|
||||
<text x="986" y="354" text-anchor="middle" class="small">原图 / 精修后 / 氛围感分别查看</text>
|
||||
<text x="986" y="380" text-anchor="middle" class="small">再次确认后以新组合覆盖旧结果</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
@ -0,0 +1,31 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="300" viewBox="0 0 1080 300">
|
||||
<defs>
|
||||
<marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto">
|
||||
<path d="M0,0 L0,6 L9,3 z" fill="#7A8699"/>
|
||||
</marker>
|
||||
<style>
|
||||
.title { font: 600 18px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #1F2937; }
|
||||
.detail { font: 14px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #667085; }
|
||||
.box { fill: #FFFFFF; stroke: #D9E2EF; stroke-width: 2; rx: 16; }
|
||||
.active { fill: #EEF5FF; stroke: #1677FF; stroke-width: 2; rx: 16; }
|
||||
</style>
|
||||
</defs>
|
||||
<rect width="1080" height="300" fill="#F7F9FC"/>
|
||||
<rect class="box" x="40" y="104" width="210" height="92"/>
|
||||
<text class="title" x="145" y="142" text-anchor="middle">有线传输照片列表</text>
|
||||
<text class="detail" x="145" y="170" text-anchor="middle">点击任意缩略图</text>
|
||||
<line x1="250" y1="150" x2="320" y2="150" stroke="#7A8699" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<rect class="active" x="333" y="70" width="260" height="160"/>
|
||||
<text class="title" x="463" y="108" text-anchor="middle">多图全屏预览</text>
|
||||
<text class="detail" x="463" y="142" text-anchor="middle">从被点击照片开始</text>
|
||||
<text class="detail" x="463" y="174" text-anchor="middle">页码显示 当前张 / 总张数</text>
|
||||
<text class="detail" x="463" y="206" text-anchor="middle">不可预览照片自动跳过</text>
|
||||
<line x1="593" y1="112" x2="668" y2="74" stroke="#7A8699" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<line x1="593" y1="188" x2="668" y2="226" stroke="#7A8699" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<rect class="box" x="681" y="28" width="354" height="100"/>
|
||||
<text class="title" x="858" y="68" text-anchor="middle">向右滑动</text>
|
||||
<text class="detail" x="858" y="96" text-anchor="middle">查看上一张,第一张处停止</text>
|
||||
<rect class="box" x="681" y="172" width="354" height="100"/>
|
||||
<text class="title" x="858" y="212" text-anchor="middle">向左滑动</text>
|
||||
<text class="detail" x="858" y="240" text-anchor="middle">查看下一张,最后一张处停止</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
49
docs/assets/travel-album-ai/04-ai-task-state-flow.svg
Normal file
@ -0,0 +1,49 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="500" viewBox="0 0 1200 500">
|
||||
<defs>
|
||||
<marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
|
||||
<path d="M0,0 L0,6 L9,3 z" fill="#4D7CFE"/>
|
||||
</marker>
|
||||
<style>
|
||||
.title { font: 700 24px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #182230; }
|
||||
.label { font: 600 17px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #182230; }
|
||||
.small { font: 14px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #5D6B7C; }
|
||||
.box { fill: #FFFFFF; stroke: #B9C8DA; stroke-width: 2; }
|
||||
.processing { fill: #EEF4FF; stroke: #4D7CFE; stroke-width: 2.5; }
|
||||
.success { fill: #EEFAF3; stroke: #37A86B; stroke-width: 2.5; }
|
||||
.failed { fill: #FFF1F1; stroke: #E35D6A; stroke-width: 2.5; }
|
||||
.line { fill: none; stroke: #4D7CFE; stroke-width: 2.5; marker-end: url(#arrow); }
|
||||
.retry { fill: none; stroke: #E35D6A; stroke-width: 2.5; marker-end: url(#arrow); }
|
||||
</style>
|
||||
</defs>
|
||||
<rect width="1200" height="500" rx="24" fill="#F7F9FC"/>
|
||||
<text x="42" y="52" class="title">AI 修图任务生命周期与覆盖规则</text>
|
||||
<rect x="42" y="104" width="205" height="116" rx="16" class="box"/>
|
||||
<text x="144" y="140" text-anchor="middle" class="label">可发起</text>
|
||||
<text x="144" y="172" text-anchor="middle" class="small">未修图或已有旧结果</text>
|
||||
<text x="144" y="198" text-anchor="middle" class="small">AI修图按钮可点击</text>
|
||||
<path d="M247 162 H337" class="line"/>
|
||||
<rect x="337" y="104" width="230" height="116" rx="16" class="processing"/>
|
||||
<text x="452" y="140" text-anchor="middle" class="label">修图中</text>
|
||||
<text x="452" y="172" text-anchor="middle" class="small">按钮置灰,禁止重复提交</text>
|
||||
<text x="452" y="198" text-anchor="middle" class="small">离开页面后任务继续完成</text>
|
||||
<path d="M567 142 H692" class="line"/>
|
||||
<path d="M567 182 H692" class="retry"/>
|
||||
<rect x="692" y="76" width="230" height="116" rx="16" class="success"/>
|
||||
<text x="807" y="112" text-anchor="middle" class="label">成功</text>
|
||||
<text x="807" y="144" text-anchor="middle" class="small">分别保存精修与氛围感结果</text>
|
||||
<text x="807" y="170" text-anchor="middle" class="small">默认展示“精修后”</text>
|
||||
<rect x="692" y="228" width="230" height="116" rx="16" class="failed"/>
|
||||
<text x="807" y="264" text-anchor="middle" class="label">失败</text>
|
||||
<text x="807" y="296" text-anchor="middle" class="small">显示“修图失败”</text>
|
||||
<text x="807" y="322" text-anchor="middle" class="small">恢复 AI修图按钮,可重试</text>
|
||||
<path d="M922 134 H1012" class="line"/>
|
||||
<rect x="1012" y="76" width="146" height="116" rx="16" class="box"/>
|
||||
<text x="1085" y="112" text-anchor="middle" class="label">再次修图</text>
|
||||
<text x="1085" y="144" text-anchor="middle" class="small">新组合完成后</text>
|
||||
<text x="1085" y="170" text-anchor="middle" class="small">整组覆盖旧结果</text>
|
||||
<path d="M1085 192 V405 H144 V220" class="line"/>
|
||||
<path d="M807 344 V405 H452 V220" class="retry"/>
|
||||
<rect x="337" y="380" width="230" height="82" rx="16" class="box"/>
|
||||
<text x="452" y="414" text-anchor="middle" class="label">还原为原图</text>
|
||||
<text x="452" y="440" text-anchor="middle" class="small">清除精修、氛围感、封面及失败状态</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
52
docs/assets/travel-album-ai/05-cover-template-flow.svg
Normal file
@ -0,0 +1,52 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="500" viewBox="0 0 1200 500">
|
||||
<defs>
|
||||
<marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
|
||||
<path d="M0,0 L0,6 L9,3 z" fill="#4D7CFE"/>
|
||||
</marker>
|
||||
<style>
|
||||
.title { font: 700 24px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #182230; }
|
||||
.label { font: 600 17px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #182230; }
|
||||
.small { font: 14px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #5D6B7C; }
|
||||
.box { fill: #FFFFFF; stroke: #B9C8DA; stroke-width: 2; }
|
||||
.active { fill: #EEF4FF; stroke: #4D7CFE; stroke-width: 2.5; }
|
||||
.free { fill: #FFF8E8; stroke: #F2A93B; stroke-width: 2.5; }
|
||||
.cover { fill: #FFF4E8; stroke: #F59E0B; stroke-width: 2.5; }
|
||||
.line { fill: none; stroke: #4D7CFE; stroke-width: 2.5; marker-end: url(#arrow); }
|
||||
</style>
|
||||
</defs>
|
||||
<rect width="1200" height="500" rx="24" fill="#F7F9FC"/>
|
||||
<text x="42" y="52" class="title">多图三类风格选择与封面生成</text>
|
||||
<rect x="42" y="92" width="220" height="116" rx="16" class="box"/>
|
||||
<text x="152" y="128" text-anchor="middle" class="label">统计选中照片数</text>
|
||||
<text x="152" y="160" text-anchor="middle" class="small">进入批量 AI 修图前判断</text>
|
||||
<text x="152" y="186" text-anchor="middle" class="small">阈值以当前需求为准</text>
|
||||
<path d="M262 150 H342" class="line"/>
|
||||
<rect x="342" y="76" width="230" height="148" rx="18" class="active"/>
|
||||
<text x="457" y="112" text-anchor="middle" class="label">数量是否 ≥ 4 张?</text>
|
||||
<text x="457" y="147" text-anchor="middle" class="small">否:显示精修 + 氛围感</text>
|
||||
<text x="457" y="175" text-anchor="middle" class="small">是:再显示封面模板</text>
|
||||
<text x="457" y="201" text-anchor="middle" class="small">弹窗自动使用更高高度</text>
|
||||
<path d="M572 150 H652" class="line"/>
|
||||
<rect x="652" y="76" width="250" height="148" rx="18" class="free"/>
|
||||
<text x="777" y="112" text-anchor="middle" class="label">显示三个风格模块</text>
|
||||
<text x="777" y="147" text-anchor="middle" class="small">原图精修:单选基础预设</text>
|
||||
<text x="777" y="175" text-anchor="middle" class="small">氛围感:横向单选,可取消</text>
|
||||
<text x="777" y="201" text-anchor="middle" class="small">封面:横向单选,可全屏预览</text>
|
||||
<path d="M902 150 H982" class="line"/>
|
||||
<rect x="982" y="92" width="176" height="116" rx="16" class="cover"/>
|
||||
<text x="1070" y="128" text-anchor="middle" class="label">确认并生成</text>
|
||||
<text x="1070" y="160" text-anchor="middle" class="small">每张保存独立结果</text>
|
||||
<text x="1070" y="186" text-anchor="middle" class="small">第一张另作封面</text>
|
||||
<rect x="42" y="300" width="344" height="126" rx="16" class="box"/>
|
||||
<text x="214" y="336" text-anchor="middle" class="label">当前免费阶段</text>
|
||||
<text x="214" y="368" text-anchor="middle" class="small">封面 quotaCost = 0,不减少额度</text>
|
||||
<text x="214" y="394" text-anchor="middle" class="small">弹窗显示“赠送 · 不占精修张数”</text>
|
||||
<rect x="428" y="300" width="344" height="126" rx="16" class="box"/>
|
||||
<text x="600" y="336" text-anchor="middle" class="label">未来收费阶段</text>
|
||||
<text x="600" y="368" text-anchor="middle" class="small">服务端将 quotaCost 配置为正数</text>
|
||||
<text x="600" y="394" text-anchor="middle" class="small">确认前展示实际消耗,成功后再扣减</text>
|
||||
<rect x="814" y="300" width="344" height="126" rx="16" class="box"/>
|
||||
<text x="986" y="336" text-anchor="middle" class="label">重复选择封面模板</text>
|
||||
<text x="986" y="368" text-anchor="middle" class="small">新封面效果覆盖旧封面效果</text>
|
||||
<text x="986" y="394" text-anchor="middle" class="small">失败时不把结果标记为已生成</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
51
docs/assets/travel-album-ai/06-thumbnail-state-flow.svg
Normal file
@ -0,0 +1,51 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="520" viewBox="0 0 1200 520">
|
||||
<defs>
|
||||
<marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
|
||||
<path d="M0,0 L0,6 L9,3 z" fill="#7A8CA5"/>
|
||||
</marker>
|
||||
<style>
|
||||
.title { font: 700 24px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #182230; }
|
||||
.label { font: 700 17px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #182230; }
|
||||
.small { font: 14px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #5D6B7C; }
|
||||
.card { fill: #FFFFFF; stroke: #D5DEE9; stroke-width: 2; }
|
||||
.line { fill: none; stroke: #7A8CA5; stroke-width: 2.3; marker-end: url(#arrow); }
|
||||
.badge { font: 700 15px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #FFFFFF; }
|
||||
</style>
|
||||
</defs>
|
||||
<rect width="1200" height="520" rx="24" fill="#F7F9FC"/>
|
||||
<text x="42" y="52" class="title">相册缩略图结果类型与状态优先级</text>
|
||||
<rect x="42" y="98" width="190" height="148" rx="18" class="card"/>
|
||||
<rect x="58" y="114" width="70" height="28" rx="8" fill="#34C759"/>
|
||||
<text x="93" y="134" text-anchor="middle" class="badge">已上传</text>
|
||||
<text x="137" y="178" text-anchor="middle" class="label">普通原图</text>
|
||||
<text x="137" y="210" text-anchor="middle" class="small">显示服务端缩略图</text>
|
||||
<path d="M232 172 H282" class="line"/>
|
||||
<rect x="282" y="98" width="190" height="148" rx="18" class="card"/>
|
||||
<rect x="298" y="114" width="70" height="28" rx="8" fill="#4D7CFE"/>
|
||||
<text x="333" y="134" text-anchor="middle" class="badge">修图中</text>
|
||||
<text x="377" y="178" text-anchor="middle" class="label">处理中</text>
|
||||
<text x="377" y="210" text-anchor="middle" class="small">AI入口同步置灰</text>
|
||||
<path d="M472 172 H522" class="line"/>
|
||||
<rect x="522" y="98" width="190" height="148" rx="18" class="card"/>
|
||||
<rect x="538" y="114" width="70" height="28" rx="8" fill="#6750A4"/>
|
||||
<text x="573" y="134" text-anchor="middle" class="badge">AI已修</text>
|
||||
<text x="617" y="178" text-anchor="middle" class="label">普通修图结果</text>
|
||||
<text x="617" y="210" text-anchor="middle" class="small">缩略图展示最新结果</text>
|
||||
<path d="M712 172 H762" class="line"/>
|
||||
<rect x="762" y="98" width="190" height="148" rx="18" class="card"/>
|
||||
<rect x="778" y="114" width="78" height="28" rx="8" fill="#F59E0B"/>
|
||||
<text x="817" y="134" text-anchor="middle" class="badge">AI封面</text>
|
||||
<text x="857" y="178" text-anchor="middle" class="label">封面结果</text>
|
||||
<text x="857" y="210" text-anchor="middle" class="small">显示封面模板排版图</text>
|
||||
<path d="M952 172 H1002" class="line"/>
|
||||
<rect x="1002" y="98" width="156" height="148" rx="18" class="card"/>
|
||||
<rect x="1018" y="114" width="88" height="28" rx="8" fill="#E53935"/>
|
||||
<text x="1062" y="134" text-anchor="middle" class="badge">修图失败</text>
|
||||
<text x="1080" y="178" text-anchor="middle" class="label">失败结果</text>
|
||||
<text x="1080" y="210" text-anchor="middle" class="small">可再次修图</text>
|
||||
<rect x="42" y="310" width="1116" height="150" rx="18" fill="#FFFFFF" stroke="#D5DEE9" stroke-width="2"/>
|
||||
<text x="70" y="346" class="label">状态显示优先级</text>
|
||||
<text x="70" y="382" class="small">修图中 > 修图失败 > AI封面 > AI已修 > 已上传。高优先级状态先展示,避免处理中或失败被旧成功结果遮盖。</text>
|
||||
<text x="70" y="416" class="small">成功:缩略图切换为最新修图图或封面图;再次成功:覆盖旧结果;还原原图:回到“已上传”;删除素材:同步清除本地结果。</text>
|
||||
<text x="70" y="446" class="small">当前 AI 修图、封面结果及角标为本地演示数据,App 进程重启后会清空;服务端接入后应返回结果类型、状态、结果图地址和模板 ID。</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
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
@ -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. 所有接口继续使用项目现有统一鉴权、业务状态码和响应信封格式。
|
||||
|
||||
261
docs/相册管理与AI修图功能说明.md
Normal file
@ -0,0 +1,261 @@
|
||||
# 相册管理与 AI 修图功能说明
|
||||
|
||||
> 文档日期:2026-07-29
|
||||
> 适用范围:新增相册 → 相册任务 → 相册管理 → 照片上传与传输 → 照片预览与 AI 修图
|
||||
> 数据说明:相册和照片列表、有线相机连接及照片传输继续使用项目已有真实数据能力;传输模式选择保存在本地。原图精修结果、氛围感结果、封面模板、剩余 9 张额度及状态角标目前为本地可演示数据,尚未接入服务端持久化,App 进程重启后会清空。
|
||||
|
||||
## 1. 照片预览、左右滑动与底部操作
|
||||
|
||||
### 功能说明
|
||||
|
||||
用户在“相册管理”照片网格中点击任意缩略图,进入单张照片预览页。页面只保留一个返回按钮,导航区域展示当前照片文件名和文件大小;照片区域支持左右滑动查看同一相册内的上一张或下一张照片。
|
||||
|
||||
预览页统一使用黑色沉浸式自定义头部,不再混用系统导航栏:左侧为圆形返回按钮,中间展示文件名和文件大小,右侧展示“当前张数/总张数”。切换照片时三处信息同步更新。照片预览与原图精修、氛围感、封面模板预览都直接使用同一个全屏模态容器,不进入项目导航栈,因此不会叠加系统白色导航栏或出现两个返回按钮;关闭后回到进入前的相册管理页或修图弹窗。
|
||||
|
||||
底部固定展示以下三个操作,不展示打印按钮:
|
||||
|
||||
| 操作 | 点击结果 | 操作期间或异常处理 |
|
||||
| --- | --- | --- |
|
||||
| AI修图 | 打开当前照片的 AI 修图弹窗 | 当前照片处于“修图中”时按钮置灰,避免重复提交 |
|
||||
| 删除 | 二次确认后删除当前照片 | 删除成功后自动定位到相邻照片;删除最后一张后返回相册管理 |
|
||||
| 刷新 | 重新加载当前照片及最新修图结果 | 刷新失败时不清空当前已显示内容,可再次点击 |
|
||||
|
||||
左右滑动时同步刷新文件名、文件大小、原图/修图后状态和底部按钮状态。第一张继续向右滑、最后一张继续向左滑时保持当前照片,不循环跳转。
|
||||
|
||||
页面底部图标使用项目内 `TravelAlbumPreviewIcons` SVG 资源,确保 AI 修图、删除、刷新三个入口视觉一致。
|
||||
|
||||
### 状态流转图
|
||||
|
||||

|
||||
|
||||
### 本功能验收
|
||||
|
||||
- [ ] 从任意缩略图进入时只显示一个返回入口。
|
||||
- [ ] 文件名和文件大小与当前照片一致。
|
||||
- [ ] 导航头部保持黑色沉浸样式,右侧张数随照片切换同步变化。
|
||||
- [ ] 左右滑动后照片和文件信息同步变化。
|
||||
- [ ] 首尾照片不会越界或循环。
|
||||
- [ ] 底部仅显示 AI修图、删除、刷新,不显示打印。
|
||||
- [ ] 删除当前照片后能正确定位或退出预览。
|
||||
|
||||
## 2. 上传照片与传输模式选择
|
||||
|
||||
### 功能说明
|
||||
|
||||
相册管理处于普通状态时,用户点击底部“上传照片”,先从底部弹出“选择传输模式”,展示“边拍边传”和“拍后传输”两个选项。点击任一选项后弹窗关闭,并进入当前相册的有线传输页面;点击“取消”或向下关闭弹窗时停留在相册管理,不改变现有照片和选择状态。
|
||||
|
||||
| 传输模式 | 弹窗说明 | 下一页初始状态 | 后续行为 |
|
||||
| --- | --- | --- | --- |
|
||||
| 边拍边传 | 相机拍摄后,照片自动传输并上传到当前相册 | 传输模式显示“边拍边传” | 新传入 App 的照片自动上传 |
|
||||
| 拍后传输 | 拍摄完成后,再选择相机照片批量传输 | 传输模式显示“拍后传输” | 新照片先保留,用户后续选择并上传 |
|
||||
|
||||
本次弹窗选择优先于本机上一次保存的传输模式,并同步成为新的最近使用模式,确保下一页展示和实际传输行为一致。相册 ID、相册名称、联系电话及照片数据继续沿用相册管理页面的真实数据,不创建新的相册或假照片。
|
||||
|
||||
进入有线传输页面后,非批量选择状态下点击任意照片缩略图,预览页会使用当前 Tab 筛选后的照片顺序,并从被点击照片开始展示。用户可左右滑动查看上一张或下一张,顶部页码随照片同步变化;没有可用预览地址的照片不进入预览序列,也不占用页码。第一张继续向右滑、最后一张继续向左滑时均停留在边界,不循环跳转。处于批量上传选择状态时,点击照片仍执行选择或取消选择,不打开预览。
|
||||
|
||||
### 状态流转图
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### 本功能验收
|
||||
|
||||
- [ ] 点击“上传照片”后先显示底部传输模式弹窗,不直接进入下一页。
|
||||
- [ ] 弹窗同时显示“边拍边传”和“拍后传输”及对应说明。
|
||||
- [ ] 选择“边拍边传”后,下一页模式显示为“边拍边传”。
|
||||
- [ ] 选择“拍后传输”后,下一页模式显示为“拍后传输”。
|
||||
- [ ] 本次选择不会被上一次保存的模式覆盖。
|
||||
- [ ] 取消或关闭弹窗后仍停留在相册管理。
|
||||
- [ ] 非批量选择状态点击缩略图后,从被点击照片进入全屏预览。
|
||||
- [ ] 预览可左右滑动查看当前 Tab 中的上一张和下一张,页码同步变化且首尾不循环。
|
||||
- [ ] 批量选择状态点击照片只改变选择状态,不打开预览。
|
||||
|
||||
## 3. 相册管理多选与底部操作栏切换
|
||||
|
||||
### 功能说明
|
||||
|
||||
相册管理页普通状态下,底部显示“上传照片”。用户点击右上角选择按钮后进入多选状态,每张缩略图右上角显示选择圆圈,底部“上传照片”隐藏,替换为“AI修图”和“删除”。
|
||||
|
||||
| 多选状态 | 页面表现 | 可用操作 | 下一状态 |
|
||||
| --- | --- | --- | --- |
|
||||
| 未进入多选 | 不显示选择圆圈,底部显示上传照片 | 点击照片进入预览;点击右上角选择 | 进入多选 |
|
||||
| 已进入但未选照片 | 显示未选圆圈,AI修图和删除置灰 | 选择照片;再次点击右上角选择退出 | 已选择或退出 |
|
||||
| 已选择照片 | 选中照片显示勾选,AI修图和删除可用 | 发起批量修图或批量删除 | 弹窗、处理中或删除结果 |
|
||||
| 选中项含修图中的照片 | 保留当前选择,AI修图置灰 | 等待任务完成;删除仍按现有删除规则处理 | 任务结束后恢复 |
|
||||
| 退出多选 | 清空本次选择,恢复上传照片 | 上传或进入照片预览 | 普通状态 |
|
||||
|
||||
批量删除必须二次确认;用户取消时不删除任何照片并保留当前选择。批量 AI 修图复用与单张预览相同的预设、任务状态和结果覆盖逻辑。
|
||||
|
||||
### 状态流转图
|
||||
|
||||

|
||||
|
||||
### 本功能验收
|
||||
|
||||
- [ ] 进入多选后上传按钮消失,AI修图和删除出现。
|
||||
- [ ] 未选择照片时两个批量按钮不可点击。
|
||||
- [ ] 选择至少一张后两个批量按钮恢复可用。
|
||||
- [ ] 包含修图中照片时不能再次发起 AI 修图。
|
||||
- [ ] 退出多选后选择状态清空并恢复上传按钮。
|
||||
|
||||
## 4. AI 修图弹窗、原图精修、氛围感与按张收费模式
|
||||
|
||||
### 功能说明
|
||||
|
||||
用户从单张预览或相册多选操作栏点击“AI修图”后,以同一个底部弹窗展示 AI 修图配置。标题与弹窗顶部保留足够间距,标题下方展示“已选择 N 张照片”。弹窗内容可纵向滚动,取消和确定按钮固定在底部,照片较多并出现封面模块时也不会遮挡选择项。
|
||||
|
||||
“原图精修”是基础修图模块,使用横向滚动单选列表,每个预设同时展示名称和对应效果图:
|
||||
|
||||
| 预设 | 当前效果用途 |
|
||||
| --- | --- |
|
||||
| 还原为原图 | 清除该照片已有修图结果和封面类型 |
|
||||
| 写真-简约肖像 | 低饱和、提亮的人像效果 |
|
||||
| 写真-清冷古风 | 清冷复古效果 |
|
||||
| 旅拍-锦绣 | 提升饱和度和对比度 |
|
||||
| 旅拍-远山 | 低饱和、柔和远景效果 |
|
||||
| 旅拍-薄雾 | 提亮并降低对比度 |
|
||||
| 油画-夏日 | 偏暖夏日效果 |
|
||||
| 油画-浓郁 | 高饱和、高对比效果 |
|
||||
|
||||
原图精修默认选中“还原为原图”,用户点击其他预设后仅保留一个选中项。没有可用原图时,效果图位置显示稳定占位图,不影响弹窗选择。每张样式效果图右下角都有“预览”按钮,点击后全屏查看当前照片应用该样式的效果,不改变当前选中项。
|
||||
|
||||
“氛围感修图”位于原图精修下方,并明确标记为“选填”。该模块与原图精修保持一致,使用横向滚动效果图单选,当前本地演示包含日落暖阳、复古胶片、清透蓝调、森系薄雾、浓郁故事五种样式。默认不选中;点击一种样式后仅保留该项,再次点击当前样式即可取消。每个样式同样提供“预览”按钮。选中后不是把氛围效果叠加并合并到精修图中,而是为每张照片额外生成一张可独立查看的“氛围感”结果。
|
||||
|
||||
原图精修、氛围感、封面模板和“查看高清图”统一进入同一个沉浸式大图预览页。大图页不再套用系统导航栏,只显示一个返回按钮、当前样式名称和“双指缩放查看细节”提示;关闭后回到原弹窗或照片页,并保留关闭前的选择状态。
|
||||
|
||||
弹窗内的 Tips 会随当前组合实时变化:
|
||||
|
||||
| 当前选择 | 确认后的结果 |
|
||||
| --- | --- |
|
||||
| 仅原图精修 | 每张照片生成 1 个“精修后”结果 |
|
||||
| 原图精修 + 氛围感修图 | 每张照片生成“精修后 + 氛围感”2 个独立结果 |
|
||||
| 有封面、未选氛围感 | 每张照片生成精修结果,第一张选中照片额外生成封面 |
|
||||
| 有封面、已选氛围感 | 每张照片生成精修和氛围感 2 个结果,第一张选中照片再额外生成封面 |
|
||||
|
||||
“修图模式”区域只读展示“AI精修 · 按张收费”和“剩余 9 张”,不再显示勾选圆圈,也不能点击选择。当前剩余数量为本地演示数据,不会真实扣减额度;未来接入服务端时由接口返回剩余额度,并以任务创建成功或服务端明确返回的扣减结果为准。
|
||||
|
||||
点击“确定”提交当前原图精修、氛围感和封面组合并关闭弹窗;点击“取消”或关闭弹窗时不创建任务、不改变旧修图结果,也不改变额度。
|
||||
|
||||
### 状态流转图
|
||||
|
||||

|
||||
|
||||
### 本功能验收
|
||||
|
||||
- [ ] 标题与弹窗顶部间距自然,不贴近顶部。
|
||||
- [ ] 已选照片数量准确。
|
||||
- [ ] 原图精修预设同时展示文字和效果图,并可横向滚动、保持单选。
|
||||
- [ ] 原图精修、氛围感和封面模板效果图均可通过小按钮全屏预览,预览不改变选择。
|
||||
- [ ] 所有大图预览使用同一黑色头部、单一返回入口,并支持双指缩放。
|
||||
- [ ] 氛围感修图横向展示多个效果图,默认不选中、保持单选,可再次点击当前样式取消。
|
||||
- [ ] 同时选择原图精修和氛围感时,每张照片生成两个独立结果。
|
||||
- [ ] Tips 能根据氛围感和封面选择动态说明实际输出结果。
|
||||
- [ ] 修图模式只展示按张收费与剩余张数,不提供选择操作。
|
||||
- [ ] 取消弹窗不会创建任务或覆盖旧结果。
|
||||
|
||||
## 5. AI 修图任务状态、失败重试与结果覆盖
|
||||
|
||||
### 功能说明
|
||||
|
||||
用户确认修图后,所选照片立即进入“修图中”。任务未结束前,相关 AI 修图入口置灰;即使用户离开照片预览页,本地演示任务仍会继续完成,并通过共享结果状态刷新相册缩略图。
|
||||
|
||||
| 任务状态 | 进入条件 | 页面表现 | 可用操作 | 后续状态 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 可发起 | 未修图、已有成功结果或上次任务已结束 | AI修图按钮可用 | 选择预设并确认 | 修图中 |
|
||||
| 修图中 | 已提交预设,等待结果 | 按钮置灰;缩略图显示“修图中” | 查看其他照片、离开页面 | 成功或失败 |
|
||||
| 成功 | 获取原图并完成本地效果处理 | 分别保存精修、氛围感和封面结果;默认展示“精修后”;按钮恢复 | 在已有结果间切换、再次修图、还原原图 | 新任务或已上传 |
|
||||
| 失败 | 原图无法获取或处理失败 | 缩略图显示“修图失败”;按钮恢复 | 再次修图 | 修图中 |
|
||||
|
||||
成功后,照片预览页至少出现“原图 / 精修后”切换;本次选择了氛围感时增加“氛围感”,存在封面结果时增加“封面”。各结果互相独立,不把两个效果合成同一张图;有修图结果时还可以点击“查看高清图”。再次修图时,旧结果在新任务处理期间继续保留,新任务成功后整组替换为本次最新组合:本次未选择氛围感时会清除旧氛围感结果;新任务失败时保留旧成功图片,但状态优先显示“修图失败”,提醒用户本次任务未成功。
|
||||
|
||||
选择“还原为原图”会同时清除修图结果、失败状态和封面类型,缩略图恢复为“已上传”。删除照片时同步清理该照片的本地修图记录,避免已删除照片的结果残留。
|
||||
|
||||
### 状态流转图
|
||||
|
||||

|
||||
|
||||
### 本功能验收
|
||||
|
||||
- [ ] 修图中不能重复点击 AI修图。
|
||||
- [ ] 离开预览页后任务仍能结束并刷新缩略图。
|
||||
- [ ] 成功后可切换查看原图、精修后以及本次实际生成的氛围感或封面。
|
||||
- [ ] 同选原图精修和氛围感时两个结果均可独立查看。
|
||||
- [ ] 再次修图成功后以本次结果组合覆盖旧结果。
|
||||
- [ ] 失败后按钮恢复并允许重试。
|
||||
- [ ] 还原原图后所有本地修图类型和结果被清除。
|
||||
|
||||
## 6. 选中 4 张及以上时的封面模板选择
|
||||
|
||||
### 功能说明
|
||||
|
||||
从相册管理多选照片并点击“AI修图”时,系统根据选中照片数量决定是否展示“封面风格模板”:
|
||||
|
||||
| 选中数量 | 弹窗表现 |
|
||||
| --- | --- |
|
||||
| 1~3 张 | 展示“原图精修 + 氛围感修图(选填)”两个模块,不展示封面模板 |
|
||||
| ≥ 4 张 | 展示“原图精修 + 氛围感修图(选填)+ 封面模板”三个模块,并提高弹窗高度 |
|
||||
|
||||
封面模板为横向滚动单选,默认选中“旅行纪念册”。每个模板效果图右下角提供“预览”按钮,点击后全屏查看完整排版,不改变当前模板选择。当前提供:
|
||||
|
||||
- 旅行纪念册
|
||||
- 简约留白
|
||||
- 景区故事
|
||||
- 电影胶片
|
||||
|
||||
每个模板展示名称和以第一张选中照片生成的排版效果图。用户确认后,所有选中照片先生成原图精修结果;选择了氛围感时,所有照片再各自生成一个氛围感结果;第一张选中照片另外生成独立封面并作为封面承载图。封面不会替代精修或氛围感结果,用户进入预览后可分别切换查看。
|
||||
|
||||
当前所有封面模板 `quotaCost = 0`,弹窗显示“赠送 · 不占精修张数”,不会减少剩余修图额度。后续收费时保留同一选择流程,由服务端为模板返回大于 0 的 `quotaCost`,客户端在确认前显示实际消耗,并以服务端成功结果为准扣减;收费能力目前尚未实现。
|
||||
|
||||
再次对不少于 4 张照片选择新封面模板并修图时,新封面效果覆盖旧封面效果。封面承载图处理失败时不标记为封面成功,缩略图显示“修图失败”并允许重试。选择“还原为原图”时清除封面效果和封面类型。
|
||||
|
||||
### 状态流转图
|
||||
|
||||

|
||||
|
||||
### 本功能验收
|
||||
|
||||
- [ ] 选中 3 张时不显示封面模板。
|
||||
- [ ] 选中 4 张或更多时显示封面模板。
|
||||
- [ ] 有封面模块时,弹窗同时完整显示原图精修、氛围感和封面三个风格模块。
|
||||
- [ ] 封面模板可横向滚动且始终单选。
|
||||
- [ ] 每个封面模板均可独立预览,关闭预览后保留原选择。
|
||||
- [ ] 默认选中旅行纪念册。
|
||||
- [ ] 当前显示赠送并且不消耗精修额度。
|
||||
- [ ] 成功后第一张选中照片成为封面承载图。
|
||||
- [ ] 再次生成封面时以新结果覆盖旧结果。
|
||||
|
||||
## 7. 相册缩略图结果类型与显示优先级
|
||||
|
||||
### 功能说明
|
||||
|
||||
相册管理缩略图左上角复用同一状态位,根据照片当前结果显示不同类型,让用户无需进入预览即可区分未修、处理中、已修、封面和失败照片。
|
||||
|
||||
| 缩略图状态 | 角标文案与颜色 | 缩略图内容 | 进入条件 | 可用操作 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 普通原图 | 绿色“已上传” | 服务端原图或缩略图 | 没有本地修图结果 | 预览、选择、AI修图、删除 |
|
||||
| 处理中 | 蓝色“修图中” | 有旧成功结果时继续显示旧结果,否则显示原图 | AI 任务未结束 | 预览、删除;不能再次修图 |
|
||||
| 普通修图结果 | 紫色“AI已修” | 最新精修图片 | 原图精修成功,可同时存在氛围感结果 | 预览原图/精修后/氛围感、再次修图、还原 |
|
||||
| 封面结果 | 橙色“AI封面” | 所选封面模板排版图 | 封面承载图生成成功 | 预览、再次修图、重新生成封面、还原 |
|
||||
| 修图失败 | 红色“修图失败” | 有旧成功结果时保留旧图,否则显示原图 | 最近一次任务失败 | 再次修图、删除 |
|
||||
|
||||
状态显示优先级为:
|
||||
|
||||
`修图中 > 修图失败 > AI封面 > AI已修 > 已上传`
|
||||
|
||||
该优先级保证处理中或失败状态不会被历史成功结果遮盖。普通修图成功后显示“AI已修”;封面模板成功后,第一张选中照片显示封面排版图和“AI封面”,其他照片继续显示“AI已修”。再次修图成功后缩略图切换为最新结果;还原原图后回到“已上传”;删除照片后对应本地状态一起删除。
|
||||
|
||||
当前状态和结果由内存数据源在相册管理与照片预览之间共享,尚不支持 App 重启后的恢复。服务端接入时至少需要返回素材 ID、任务状态、结果类型、结果图地址、原图精修预设 ID、是否生成氛围感、封面模板 ID、失败原因和额度消耗,客户端才能恢复完全一致的缩略图状态。
|
||||
|
||||
### 状态流转图
|
||||
|
||||

|
||||
|
||||
### 本功能验收
|
||||
|
||||
- [ ] 未修图照片显示“已上传”。
|
||||
- [ ] 修图中照片显示“修图中”且不能重复提交。
|
||||
- [ ] 普通成功结果显示“AI已修”和最新效果图。
|
||||
- [ ] 封面成功结果显示“AI封面”和封面排版图。
|
||||
- [ ] 最近一次任务失败时优先显示“修图失败”。
|
||||
- [ ] 还原、覆盖和删除后缩略图状态同步更新。
|
||||
@ -208,6 +208,11 @@
|
||||
TargetAttributes = {
|
||||
93FF17C42FFB84950033C304 = {
|
||||
CreatedOnToolsVersion = 26.6;
|
||||
SystemCapabilities = {
|
||||
com.apple.Push = {
|
||||
enabled = 1;
|
||||
};
|
||||
};
|
||||
};
|
||||
A1B2C3D32FFB84950033C305 = {
|
||||
CreatedOnToolsVersion = 26.6;
|
||||
@ -422,6 +427,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 5BBB56C2281879245220A9F3 /* Pods-suixinkan.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
APS_ENVIRONMENT = development;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = suixinkan/suixinkan.entitlements;
|
||||
@ -487,6 +493,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 1060D5E389506965D83EA1CC /* Pods-suixinkan.release.xcconfig */;
|
||||
buildSettings = {
|
||||
APS_ENVIRONMENT = production;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = suixinkan/suixinkan.entitlements;
|
||||
|
||||
@ -29,6 +29,7 @@ final class NetworkServices {
|
||||
let liveAPI: LiveAPI
|
||||
let cloudDriveAPI: CloudDriveAPI
|
||||
let messageCenterAPI: MessageCenterAPI
|
||||
let pushAPI: PushAPI
|
||||
let wildPhotographerReportAPI: WildPhotographerReportAPI
|
||||
let uploadAPI: UploadAPI
|
||||
let ossUploadService: OSSUploadService
|
||||
@ -54,6 +55,7 @@ final class NetworkServices {
|
||||
liveAPI = LiveAPI(client: client)
|
||||
cloudDriveAPI = CloudDriveAPI(client: client)
|
||||
messageCenterAPI = MessageCenterAPI(client: client)
|
||||
pushAPI = PushAPI(client: client)
|
||||
wildPhotographerReportAPI = WildPhotographerReportAPI(client: client)
|
||||
uploadAPI = UploadAPI(client: client)
|
||||
ossUploadService = OSSUploadService(configService: uploadAPI)
|
||||
@ -84,6 +86,7 @@ final class NetworkServices {
|
||||
liveAPI = LiveAPI(client: apiClient)
|
||||
cloudDriveAPI = CloudDriveAPI(client: apiClient)
|
||||
messageCenterAPI = MessageCenterAPI(client: apiClient)
|
||||
pushAPI = PushAPI(client: apiClient)
|
||||
wildPhotographerReportAPI = WildPhotographerReportAPI(client: apiClient)
|
||||
uploadAPI = UploadAPI(client: apiClient)
|
||||
ossUploadService = OSSUploadService(configService: uploadAPI)
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
import IQKeyboardManagerSwift
|
||||
import IQKeyboardToolbarManager
|
||||
import UIKit
|
||||
import UserNotifications
|
||||
|
||||
@main
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
@ -18,9 +19,36 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
if AppStore.shared.session.privacyAgreementAccepted, !AppStore.shared.session.token.isEmpty {
|
||||
AMapBootstrap.configureIfNeeded()
|
||||
}
|
||||
PushNotificationManager.shared.initializeIfPrivacyAccepted(launchOptions: launchOptions)
|
||||
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
|
||||
|
||||
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||
|
||||
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
@ -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
|
After Width: | Height: | Size: 1.6 MiB |
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
|
After Width: | Height: | Size: 89 KiB |
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "travel_album_preview_ai_edit.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 -128 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#FFFFFF" d="M764 824q-40 40 -97 40t-97 -40l-544 -542v-320q0 -25 17.5 -43t42.5 -18h340l528 538q41 40 41 97t-41 97zM338 -29h-179q-26 0 -44.5 18.5t-18.5 45.5v196q12 -3 31 -9t65 -29.5t77 -53.5q29 -27 63.5 -86t38.5 -82h-33zM885 463l-435 -438q-73 119 -124 171q-15 15 -39.5 32t-48 31t-45.5 26t-35 19l-13 6l448 446q24 24 60.5 24t60.5 -25l171 -171q25 -26 25 -61.5t-25 -59.5zM611 315l-43 43l32 31l42 -42zM692 397l-42 42l126 127l43 -43z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 541 B |
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "travel_album_preview_delete.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 -128 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#FFFFFF" d="M368 440v-279q0 -13 8.5 -22t20.5 -9t20 8q9 10 9 23v279q0 13 -8.5 22.5t-20.5 9.5t-20.5 -9.5t-8.5 -22.5zM569 440v-279q0 -13 8.5 -22t20.5 -9t20.5 9t8.5 22v279q0 13 -8.5 22.5t-20.5 9.5t-20.5 -9.5t-8.5 -22.5zM283 -109h442q57 0 97 42t40 102v606h95q12 0 20.5 9.5t8.5 22.5t-8.5 22t-20.5 9h-259l-1 25q0 53 -35 101t-81 48h-154q-45 0 -80 -48t-36 -101v-25h-251q-12 0 -20.5 -9t-8.5 -22t8.5 -22.5t20.5 -9.5h87v-606q0 -60 40 -102t96 -42zM369 730q1 27 18.5 55t37.5 30h2h154q21 -2 39 -30t19 -55l1 -26h-272zM205 641h599v-606q0 -34 -23 -57.5t-56 -23.5h-442q-33 0 -55.5 23.5t-22.5 57.5v606v0z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 700 B |
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "travel_album_preview_refresh.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 -128 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#FFFFFF" d="M505.5 -127q-102.5 0 -196.5 39q-90 39 -159.5 108.5t-108.5 159.5q-39 94 -39 196.5t39 195.5q39 90 108.5 160t159.5 108q94 40 196.5 40t195.5 -40q90 -38 160 -108t108 -160q40 -93 40 -195.5t-40 -196.5q-38 -90 -108 -159.5t-160 -108.5q-93 -39 -195.5 -39zM505 819q-90 0 -172 -35q-80 -33 -141 -94.5t-95 -140.5q-35 -83 -35 -173t35 -172q34 -80 95 -141t141 -95q82 -35 172 -35t173 35q79 34 140.5 95t94.5 141q35 82 35 172t-35 173q-33 79 -94.5 140.5t-140.5 94.5q-83 35 -173 35zM595 472h-240l62 62q9 9 9 21.5t-9 21.5t-21.5 9t-21.5 -9l-113 -114q-9 -9 -9 -21.5t9 -21.5l113 -113q9 -9 21.5 -9t21.5 9t9 21.5t-9 21.5l-62 61h240q41 0 69.5 -28t28.5 -68t-28.5 -68.5t-69.5 -28.5h-221q-12 0 -21 -9t-9 -21.5t9 -21.5t21 -9h221q66 0 112 46.5t46 111.5t-46 111t-112 46z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 863 B |
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
21
suixinkan/Assets.xcassets/TravelAlbumPurchasedDemo/purchased_alpine_lake.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "purchased_alpine_lake.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 970 KiB |
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "purchased_forest_waterfall.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.2 MiB |
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "purchased_grassland_path.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.1 MiB |
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "purchased_lakeside_flowers.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.1 MiB |
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
@ -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
@ -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
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -9,10 +9,40 @@ import Foundation
|
||||
struct RolePermissionResponse: Codable, Equatable, Sendable {
|
||||
let role: RoleInfo
|
||||
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.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;无记录时返回空数组。
|
||||
func savedCommonURIs(accountScope: String, roleCode: String) -> [String] {
|
||||
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 列表。
|
||||
func saveCommonURIs(_ uris: [String], accountScope: String, roleCode: String) {
|
||||
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 个)。
|
||||
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
|
||||
}
|
||||
|
||||
@ -51,8 +56,9 @@ final class HomeCommonMenuStore {
|
||||
commonURIs: [String]
|
||||
) -> (common: [HomeMenuItem], more: [HomeMenuItem]) {
|
||||
let commonSet = Set(commonURIs)
|
||||
let common = allMenus.filter { commonSet.contains($0.uri) }
|
||||
let more = allMenus.filter { !commonSet.contains($0.uri) }
|
||||
let customizableMenus = allMenus.filter { HomeMenuCatalog.isCustomizable(uri: $0.uri) }
|
||||
let common = customizableMenus.filter { commonSet.contains($0.uri) }
|
||||
let more = customizableMenus.filter { !commonSet.contains($0.uri) }
|
||||
return (common, more)
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,8 @@ import Foundation
|
||||
/// 首页菜单静态 catalog,与服务端 permission URI 取交集后展示。
|
||||
enum HomeMenuCatalog {
|
||||
|
||||
private static let fixedHomeEntryURIs: Set<String> = ["message_center"]
|
||||
|
||||
/// 全部已知菜单项,对齐 Android `Constants.menuList`。
|
||||
static let allItems: [HomeMenuItem] = [
|
||||
HomeMenuItem(uri: "space_settings", title: "空间设置", iconName: "home_menu_space"),
|
||||
@ -45,6 +47,7 @@ enum HomeMenuCatalog {
|
||||
/// 将服务端权限映射为可展示菜单,保持服务端顺序。
|
||||
static func visibleMenus(from permissions: [HomePermissionItem]) -> [HomeMenuItem] {
|
||||
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 }
|
||||
return HomeMenuItem(
|
||||
uri: catalog.uri,
|
||||
@ -59,4 +62,9 @@ enum HomeMenuCatalog {
|
||||
static func item(for uri: String) -> HomeMenuItem? {
|
||||
allItems.first { $0.uri == uri }
|
||||
}
|
||||
|
||||
/// 判断入口是否允许出现在首页常用应用及其自定义页面中。
|
||||
static func isCustomizable(uri: String) -> Bool {
|
||||
!fixedHomeEntryURIs.contains(uri)
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,6 +48,7 @@ final class AllFunctionsViewModel {
|
||||
|
||||
/// 将菜单加入常用应用。
|
||||
func addToCommon(_ menu: HomeMenuItem) {
|
||||
guard HomeMenuCatalog.isCustomizable(uri: menu.uri) else { return }
|
||||
guard !commonMenus.contains(where: { $0.uri == menu.uri }) else { return }
|
||||
commonMenus.append(menu)
|
||||
moreMenus.removeAll { $0.uri == menu.uri }
|
||||
|
||||
@ -25,6 +25,7 @@ final class HomeViewModel {
|
||||
private(set) var reportTimeText = ""
|
||||
private(set) var isLocationReporting = false
|
||||
private(set) var needsPermissionReload = false
|
||||
private(set) var unreadMessageCount = 0
|
||||
|
||||
var onStateChange: (() -> Void)?
|
||||
var onShowMessage: ((String) -> Void)?
|
||||
@ -65,6 +66,7 @@ final class HomeViewModel {
|
||||
var isOnline: Bool { locationStateStore.isOnline }
|
||||
var countdownDisplayText: String { locationStateStore.countdownDisplayText }
|
||||
var reminderMinutes: Int { locationStateStore.reminderMinutes }
|
||||
var hasUnreadMessages: Bool { unreadMessageCount > 0 }
|
||||
|
||||
/// 首次进入首页时加载权限并恢复在线状态。
|
||||
func initialize(api: HomeAPI) async {
|
||||
@ -76,6 +78,28 @@ final class HomeViewModel {
|
||||
/// 账号切换后标记需要重新拉取权限。
|
||||
func markNeedsPermissionReload() {
|
||||
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()
|
||||
}
|
||||
|
||||
/// 查询待审核记录并决定进入申请页或审核状态页。
|
||||
|
||||
@ -11,8 +11,14 @@ protocol MessageCenterServing {
|
||||
/// 拉取消息列表。
|
||||
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
|
||||
@ -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
|
||||
func markAsRead(messageId: Int) async throws {
|
||||
let _: EmptyPayload = try await client.send(
|
||||
func markAsRead(messageId: Int) async throws -> MessageUnreadCountResponse {
|
||||
try await client.send(
|
||||
APIRequest(
|
||||
method: .post,
|
||||
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
|
||||
func delete(messageId: Int) async throws {
|
||||
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`。
|
||||
struct MessageListResponse: Decodable, Equatable, Sendable {
|
||||
let hasMore: Bool
|
||||
|
||||
@ -11,11 +11,13 @@ final class MessageCenterViewModel {
|
||||
private(set) var isLoading = false
|
||||
private(set) var isRefreshing = false
|
||||
private(set) var isLoadingMore = false
|
||||
private(set) var isMarkingAllAsRead = false
|
||||
private(set) var canLoadMore = false
|
||||
|
||||
var onStateChange: (() -> Void)?
|
||||
var onShowMessage: ((String) -> Void)?
|
||||
var onOpenDetail: ((MessageItem) -> Void)?
|
||||
var onUnreadMessageCountChange: ((Int) -> Void)?
|
||||
|
||||
private let pageSize = 20
|
||||
private var lastId = 0
|
||||
@ -55,9 +57,10 @@ final class MessageCenterViewModel {
|
||||
}
|
||||
|
||||
do {
|
||||
try await api.markAsRead(messageId: id)
|
||||
let response = try await api.markAsRead(messageId: id)
|
||||
let readItem = item.markedRead()
|
||||
items = items.map { $0.id == id ? readItem : $0 }
|
||||
notifyUnreadMessageCountChanged(response.unreadCount)
|
||||
onOpenDetail?(readItem)
|
||||
} catch is CancellationError {
|
||||
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) {
|
||||
items.removeAll { $0.id == id }
|
||||
@ -110,6 +137,16 @@ final class MessageCenterViewModel {
|
||||
private func notifyStateChange() {
|
||||
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,负责删除当前消息。
|
||||
|
||||
@ -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 天收款记录。
|
||||
func collectionRecord(scenicId: Int) async throws -> RepaymentCollectionRecordResponse {
|
||||
try await client.send(
|
||||
|
||||
@ -5,6 +5,83 @@
|
||||
|
||||
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。
|
||||
struct PayCodeResponse: Decodable, Sendable, Equatable {
|
||||
let staticPayURL: String
|
||||
@ -101,3 +178,10 @@ struct PaymentMerchantInfo: Sendable, Equatable {
|
||||
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
@ -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 amount = ""
|
||||
private(set) var remark = ""
|
||||
private(set) var scenicId = 0
|
||||
private(set) var payPageConfig = PayPageConfig.nalatiDefault
|
||||
private(set) var brandingRefreshVersion = 0
|
||||
|
||||
let merchantInfo = PaymentMerchantInfo.defaultInfo
|
||||
|
||||
@ -26,9 +29,21 @@ final class PaymentCollectionDetailsViewModel {
|
||||
private var staticPayURL = ""
|
||||
private var dynamicPayURL = ""
|
||||
private let appStore: AppStore
|
||||
private let payPageConfigCache: PayPageConfigCaching
|
||||
|
||||
init(appStore: AppStore = .shared) {
|
||||
init(
|
||||
appStore: AppStore = .shared,
|
||||
payPageConfigCache: PayPageConfigCaching = PayPageConfigCache()
|
||||
) {
|
||||
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 {
|
||||
@ -39,10 +54,32 @@ final class PaymentCollectionDetailsViewModel {
|
||||
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() {
|
||||
scenicId = appStore.session.currentScenicId
|
||||
scenicName = appStore.session.currentScenicName
|
||||
staffId = appStore.session.userId
|
||||
isVoiceBroadcastOpen = appStore.payment.isOpenReceiveVoice
|
||||
if usesNalatiBranding {
|
||||
payPageConfig = payPageConfigCache.config(scenicId: scenicId) ?? .nalatiDefault
|
||||
} else {
|
||||
payPageConfig = .nalatiDefault
|
||||
}
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
@ -56,7 +93,7 @@ final class PaymentCollectionDetailsViewModel {
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
let scenicId = appStore.session.currentScenicId
|
||||
let scenicId = self.scenicId
|
||||
guard scenicId > 0 else {
|
||||
clearPayCode()
|
||||
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 {
|
||||
await loadPayCode(api: api)
|
||||
async let payCode: Void = loadPayCode(api: api)
|
||||
async let pageConfig: Void = loadPayPageConfig(api: api)
|
||||
_ = await (payCode, pageConfig)
|
||||
}
|
||||
|
||||
func showSetAmountDialog() {
|
||||
|
||||
@ -116,6 +116,8 @@ struct TravelAlbumAvailableOrder: Decodable, Sendable, Equatable, Hashable {
|
||||
|
||||
/// 旅拍相册素材实体,对齐 Android `TravelAlbumMaterialEntity`。
|
||||
struct TravelAlbumMaterial: Decodable, Sendable, Equatable, Hashable, Identifiable {
|
||||
private static let bundledImageURLPrefix = "asset://"
|
||||
|
||||
let id: Int
|
||||
let userEquityTravelId: Int
|
||||
let status: Int
|
||||
@ -175,6 +177,18 @@ struct TravelAlbumMaterial: Decodable, Sendable, Equatable, Hashable, Identifiab
|
||||
self.createdAt = createdAt
|
||||
self.updatedAt = updatedAt
|
||||
}
|
||||
|
||||
/// 将工程内图片资源转换为素材可识别的本地地址,仅供无接口数据的演示素材使用。
|
||||
static func bundledImageURL(assetName: String) -> String {
|
||||
bundledImageURLPrefix + assetName
|
||||
}
|
||||
|
||||
/// 返回本地演示素材对应的图片资源名;真实网络素材返回 `nil`。
|
||||
var bundledImageAssetName: String? {
|
||||
guard fileUrl.hasPrefix(Self.bundledImageURLPrefix) else { return nil }
|
||||
let assetName = String(fileUrl.dropFirst(Self.bundledImageURLPrefix.count))
|
||||
return assetName.isEmpty ? nil : assetName
|
||||
}
|
||||
}
|
||||
|
||||
/// 旅拍相册创建请求体,对齐 Android `TravelAlbumCreateRequest`。
|
||||
|
||||
@ -0,0 +1,316 @@
|
||||
//
|
||||
// TravelAlbumPhotoPreviewModels.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 单张照片的 AI 修图状态,控制处理中禁用、结果对比以及再次修图覆盖。
|
||||
struct TravelAlbumAIEditResultState: Equatable, Sendable {
|
||||
/// 相册缩略图需要展示的 AI 修图状态。
|
||||
enum ThumbnailStatus: Equatable, Sendable {
|
||||
case none
|
||||
case processing
|
||||
case edited
|
||||
case cover
|
||||
case failed
|
||||
}
|
||||
|
||||
/// 当前预览展示的版本。
|
||||
enum DisplayMode: Equatable, Sendable {
|
||||
case original
|
||||
case edited
|
||||
case atmosphere
|
||||
case cover
|
||||
}
|
||||
|
||||
private(set) var isProcessing = false
|
||||
private(set) var didFailLastAttempt = false
|
||||
private(set) var appliedPresetID: String?
|
||||
private(set) var appliedAtmosphereOptionID: String?
|
||||
private(set) var appliedCoverTemplateID: String?
|
||||
private(set) var displayMode: DisplayMode = .original
|
||||
|
||||
/// 已经存在可查看的修图结果。
|
||||
var hasEditedResult: Bool {
|
||||
appliedPresetID != nil
|
||||
}
|
||||
|
||||
/// 处理期间禁止再次发起 AI 修图。
|
||||
var canStartEditing: Bool {
|
||||
!isProcessing
|
||||
}
|
||||
|
||||
/// 当前结果是否为封面模板生成的封面图。
|
||||
var isCover: Bool {
|
||||
appliedCoverTemplateID != nil
|
||||
}
|
||||
|
||||
/// 当前是否已经生成可独立查看的氛围感修图结果。
|
||||
var hasAtmosphereResult: Bool {
|
||||
appliedAtmosphereOptionID != nil
|
||||
}
|
||||
|
||||
/// 根据修图任务状态生成缩略图角标,处理中和失败优先于历史成功结果。
|
||||
var thumbnailStatus: ThumbnailStatus {
|
||||
if isProcessing {
|
||||
return .processing
|
||||
}
|
||||
if didFailLastAttempt {
|
||||
return .failed
|
||||
}
|
||||
if isCover {
|
||||
return .cover
|
||||
}
|
||||
return hasEditedResult ? .edited : .none
|
||||
}
|
||||
|
||||
/// 开始修图;已有结果保留到新结果完成,避免处理中画面回退。
|
||||
mutating func startProcessing() {
|
||||
guard !isProcessing else { return }
|
||||
isProcessing = true
|
||||
didFailLastAttempt = false
|
||||
}
|
||||
|
||||
/// 完成修图并覆盖旧预设结果,默认展示新的修图后版本。
|
||||
mutating func complete(
|
||||
presetID: String,
|
||||
atmosphereOptionID: String? = nil,
|
||||
coverTemplateID: String? = nil
|
||||
) {
|
||||
isProcessing = false
|
||||
didFailLastAttempt = false
|
||||
appliedPresetID = presetID
|
||||
appliedAtmosphereOptionID = atmosphereOptionID
|
||||
appliedCoverTemplateID = coverTemplateID
|
||||
displayMode = coverTemplateID == nil ? .edited : .cover
|
||||
}
|
||||
|
||||
/// 处理失败时恢复入口;已有成功结果继续保留。
|
||||
mutating func failProcessing() {
|
||||
isProcessing = false
|
||||
didFailLastAttempt = true
|
||||
}
|
||||
|
||||
/// 选择还原原图后清除历史修图结果和失败状态。
|
||||
mutating func restoreOriginal() {
|
||||
isProcessing = false
|
||||
didFailLastAttempt = false
|
||||
appliedPresetID = nil
|
||||
appliedAtmosphereOptionID = nil
|
||||
appliedCoverTemplateID = nil
|
||||
displayMode = .original
|
||||
}
|
||||
|
||||
/// 在已有结果时切换原图或修图后预览。
|
||||
mutating func selectDisplayMode(_ mode: DisplayMode) {
|
||||
guard hasEditedResult else {
|
||||
displayMode = .original
|
||||
return
|
||||
}
|
||||
if mode == .atmosphere, !hasAtmosphereResult {
|
||||
displayMode = .edited
|
||||
return
|
||||
}
|
||||
if mode == .cover, !isCover {
|
||||
displayMode = .edited
|
||||
return
|
||||
}
|
||||
displayMode = mode
|
||||
}
|
||||
}
|
||||
|
||||
/// 相册管理底部操作栏状态,用于在上传与多选操作之间切换。
|
||||
struct TravelAlbumSelectionToolbarState: Equatable, Sendable {
|
||||
let isSelectionMode: Bool
|
||||
let selectedCount: Int
|
||||
|
||||
/// 普通状态显示上传照片。
|
||||
var showsUpload: Bool {
|
||||
!isSelectionMode
|
||||
}
|
||||
|
||||
/// 多选状态显示 AI 修图和删除。
|
||||
var showsSelectionActions: Bool {
|
||||
isSelectionMode
|
||||
}
|
||||
|
||||
/// 至少选中一张照片后才允许执行批量操作。
|
||||
var selectionActionsEnabled: Bool {
|
||||
isSelectionMode && selectedCount > 0
|
||||
}
|
||||
}
|
||||
|
||||
/// 相册照片预览状态,维护当前照片位置并处理左右切换。
|
||||
struct TravelAlbumPhotoPreviewState: Equatable, Sendable {
|
||||
private(set) var materials: [TravelAlbumMaterial]
|
||||
private(set) var currentIndex: Int
|
||||
|
||||
init(materials: [TravelAlbumMaterial], initialIndex: Int) {
|
||||
self.materials = materials
|
||||
currentIndex = min(max(initialIndex, 0), max(materials.count - 1, 0))
|
||||
}
|
||||
|
||||
/// 当前正在预览的照片。
|
||||
var currentMaterial: TravelAlbumMaterial? {
|
||||
materials.indices.contains(currentIndex) ? materials[currentIndex] : nil
|
||||
}
|
||||
|
||||
/// 切换至下一张;已经是最后一张时保持不变。
|
||||
@discardableResult
|
||||
mutating func moveNext() -> Bool {
|
||||
guard currentIndex + 1 < materials.count else { return false }
|
||||
currentIndex += 1
|
||||
return true
|
||||
}
|
||||
|
||||
/// 切换至上一张;已经是第一张时保持不变。
|
||||
@discardableResult
|
||||
mutating func movePrevious() -> Bool {
|
||||
guard currentIndex > 0 else { return false }
|
||||
currentIndex -= 1
|
||||
return true
|
||||
}
|
||||
|
||||
/// 删除当前照片,并自动定位到相邻照片。
|
||||
@discardableResult
|
||||
mutating func removeCurrent() -> Bool {
|
||||
guard materials.indices.contains(currentIndex) else { return false }
|
||||
materials.remove(at: currentIndex)
|
||||
if currentIndex >= materials.count {
|
||||
currentIndex = max(materials.count - 1, 0)
|
||||
}
|
||||
return !materials.isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
/// 相册照片 AI 修图预设,当前为本地演示数据,后续可替换为服务端下发的预设列表。
|
||||
struct TravelAlbumEditPreset: Hashable, Sendable, Identifiable {
|
||||
/// 图像效果类型,用于在预览页生成对应效果图。
|
||||
enum Effect: String, Hashable, Sendable {
|
||||
case original
|
||||
case portrait
|
||||
case vintage
|
||||
case brocade
|
||||
case distantMountain
|
||||
case mist
|
||||
case summer
|
||||
case rich
|
||||
}
|
||||
|
||||
let id: String
|
||||
let title: String
|
||||
let effect: Effect
|
||||
|
||||
/// AI 修图底部弹窗展示的默认预设,暂无接口时用于完整演示选择流程。
|
||||
static let defaultOptions: [TravelAlbumEditPreset] = [
|
||||
TravelAlbumEditPreset(id: "original", title: "还原为原图", effect: .original),
|
||||
TravelAlbumEditPreset(id: "portrait", title: "写真-简约肖像", effect: .portrait),
|
||||
TravelAlbumEditPreset(id: "vintage", title: "写真-清冷古风", effect: .vintage),
|
||||
TravelAlbumEditPreset(id: "brocade", title: "旅拍-锦绣", effect: .brocade),
|
||||
TravelAlbumEditPreset(id: "distant_mountain", title: "旅拍-远山", effect: .distantMountain),
|
||||
TravelAlbumEditPreset(id: "mist", title: "旅拍-薄雾", effect: .mist),
|
||||
TravelAlbumEditPreset(id: "summer", title: "油画-夏日", effect: .summer),
|
||||
TravelAlbumEditPreset(id: "rich", title: "油画-浓郁", effect: .rich),
|
||||
]
|
||||
}
|
||||
|
||||
/// 可选的氛围感修图样式;选中后在原图精修之外额外生成一张独立结果。
|
||||
struct TravelAlbumAtmosphereEditOption: Hashable, Sendable, Identifiable {
|
||||
let id: String
|
||||
let title: String
|
||||
let subtitle: String
|
||||
let effect: TravelAlbumEditPreset.Effect
|
||||
|
||||
/// 暂无接口时用于演示横向滚动单选的氛围感样式。
|
||||
static let defaultOptions: [TravelAlbumAtmosphereEditOption] = [
|
||||
TravelAlbumAtmosphereEditOption(
|
||||
id: "warm_sun",
|
||||
title: "日落暖阳",
|
||||
subtitle: "温暖柔和",
|
||||
effect: .summer
|
||||
),
|
||||
TravelAlbumAtmosphereEditOption(
|
||||
id: "retro_film",
|
||||
title: "复古胶片",
|
||||
subtitle: "怀旧颗粒感",
|
||||
effect: .vintage
|
||||
),
|
||||
TravelAlbumAtmosphereEditOption(
|
||||
id: "clear_blue",
|
||||
title: "清透蓝调",
|
||||
subtitle: "通透轻盈",
|
||||
effect: .mist
|
||||
),
|
||||
TravelAlbumAtmosphereEditOption(
|
||||
id: "forest_mist",
|
||||
title: "森系薄雾",
|
||||
subtitle: "低饱和自然感",
|
||||
effect: .distantMountain
|
||||
),
|
||||
TravelAlbumAtmosphereEditOption(
|
||||
id: "vivid_story",
|
||||
title: "浓郁故事",
|
||||
subtitle: "高饱和电影感",
|
||||
effect: .rich
|
||||
),
|
||||
]
|
||||
|
||||
/// 兼容现有结果测试与默认演示数据的首个氛围感样式。
|
||||
static let defaultOption = defaultOptions[0]
|
||||
}
|
||||
|
||||
/// 多图 AI 修图使用的封面风格模板,当前为本地演示数据,后续可替换为服务端配置。
|
||||
struct TravelAlbumCoverTemplate: Hashable, Sendable, Identifiable {
|
||||
/// 封面预览的排版样式,用于生成可辨识的本地效果图。
|
||||
enum PreviewStyle: String, Hashable, Sendable {
|
||||
case travelMemoir
|
||||
case minimal
|
||||
case scenicStory
|
||||
case film
|
||||
}
|
||||
|
||||
/// 触发展示封面模板选择的最少照片数量。
|
||||
static let minimumPhotoCount = 4
|
||||
|
||||
let id: String
|
||||
let title: String
|
||||
let previewStyle: PreviewStyle
|
||||
/// 当前消耗的精修额度;免费阶段为 0,后续收费可由服务端改为正数。
|
||||
let quotaCost: Int
|
||||
|
||||
/// 当前模板是否为赠送且不占用精修张数。
|
||||
var isFreeGift: Bool {
|
||||
quotaCost == 0
|
||||
}
|
||||
|
||||
/// 判断本次多选是否需要展示封面模板模块。
|
||||
static func shouldShow(for selectedPhotoCount: Int) -> Bool {
|
||||
selectedPhotoCount >= minimumPhotoCount
|
||||
}
|
||||
|
||||
/// 暂无接口时用于完整演示单选流程的封面模板。
|
||||
static let defaultOptions: [TravelAlbumCoverTemplate] = [
|
||||
TravelAlbumCoverTemplate(id: "travel_memoir", title: "旅行纪念册", previewStyle: .travelMemoir, quotaCost: 0),
|
||||
TravelAlbumCoverTemplate(id: "minimal", title: "简约留白", previewStyle: .minimal, quotaCost: 0),
|
||||
TravelAlbumCoverTemplate(id: "scenic_story", title: "景区故事", previewStyle: .scenicStory, quotaCost: 0),
|
||||
TravelAlbumCoverTemplate(id: "film", title: "电影胶片", previewStyle: .film, quotaCost: 0),
|
||||
]
|
||||
}
|
||||
|
||||
/// AI 修图弹窗最终提交的样式组合,统一承载原图精修、氛围感和封面模板。
|
||||
struct TravelAlbumAIEditSelection: Equatable, Sendable {
|
||||
let refinedPreset: TravelAlbumEditPreset
|
||||
let atmosphereOption: TravelAlbumAtmosphereEditOption?
|
||||
let coverTemplate: TravelAlbumCoverTemplate?
|
||||
|
||||
/// 每张普通照片需要生成的修图结果数量,不包含原图。
|
||||
var resultCountPerPhoto: Int {
|
||||
atmosphereOption == nil ? 1 : 2
|
||||
}
|
||||
|
||||
/// 弹窗当前展示并提交的样式类别数量。
|
||||
var selectedStyleCount: Int {
|
||||
1 + (atmosphereOption == nil ? 0 : 1) + (coverTemplate == nil ? 0 : 1)
|
||||
}
|
||||
}
|
||||
@ -52,6 +52,12 @@ struct TravelAlbumOTGPhotoSection: Hashable, Sendable {
|
||||
let photos: [TravelAlbumOTGPhotoItem]
|
||||
}
|
||||
|
||||
/// 有线传输照片预览上下文,保存可预览照片顺序和点击照片位置。
|
||||
struct TravelAlbumOTGPhotoPreviewContext: Equatable, Sendable {
|
||||
let photos: [TravelAlbumOTGPhotoItem]
|
||||
let initialIndex: Int
|
||||
}
|
||||
|
||||
/// OTG 传输页 Tab。
|
||||
enum TravelAlbumOTGTransferTab: Int, CaseIterable, Sendable {
|
||||
case all
|
||||
@ -80,6 +86,16 @@ enum TravelAlbumOTGTransferMode: String, CaseIterable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
/// 模式选择弹窗中的业务说明。
|
||||
var detailText: String {
|
||||
switch self {
|
||||
case .liveUpload:
|
||||
return "相机拍摄后,照片自动传输并上传到当前相册"
|
||||
case .postTransfer:
|
||||
return "拍摄完成后,再选择照片批量传输"
|
||||
}
|
||||
}
|
||||
|
||||
/// 是否在新照片导入 App 后自动上传。
|
||||
var shouldAutoUploadNewImports: Bool {
|
||||
self == .liveUpload
|
||||
@ -113,6 +129,18 @@ extension TravelAlbumOTGPhotoItem {
|
||||
}
|
||||
|
||||
extension Array where Element == TravelAlbumOTGPhotoItem {
|
||||
/// 按当前列表顺序生成预览上下文,不可预览照片不会占用左右滑动页码。
|
||||
func previewContext(startingWith photoID: String) -> TravelAlbumOTGPhotoPreviewContext? {
|
||||
let previewablePhotos = filter { $0.thumbnailURL != nil }
|
||||
guard let initialIndex = previewablePhotos.firstIndex(where: { $0.id == photoID }) else {
|
||||
return nil
|
||||
}
|
||||
return TravelAlbumOTGPhotoPreviewContext(
|
||||
photos: previewablePhotos,
|
||||
initialIndex: initialIndex
|
||||
)
|
||||
}
|
||||
|
||||
/// 按 Android OTG 页规则构造半小时照片分段。
|
||||
func buildPhotoSections() -> [TravelAlbumOTGPhotoSection] {
|
||||
let pairs = compactMap { photo -> (slotStart: Date, photo: TravelAlbumOTGPhotoItem)? in
|
||||
|
||||
@ -0,0 +1,308 @@
|
||||
//
|
||||
// TravelAlbumAIEditResultStore.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import CoreImage
|
||||
import UIKit
|
||||
|
||||
/// AI 修图结果的内存数据源,在相册管理与照片预览之间共享本地演示结果。
|
||||
@MainActor
|
||||
final class TravelAlbumAIEditResultStore {
|
||||
/// 单张照片的修图记录。
|
||||
struct Record {
|
||||
var state: TravelAlbumAIEditResultState
|
||||
var editedImage: UIImage?
|
||||
var atmosphereImage: UIImage?
|
||||
var coverImage: UIImage?
|
||||
}
|
||||
|
||||
static let shared = TravelAlbumAIEditResultStore()
|
||||
static let didChangeNotification = Notification.Name("TravelAlbumAIEditResultStore.didChange")
|
||||
static let materialIDUserInfoKey = "materialID"
|
||||
|
||||
private var recordsByMaterialID: [Int: Record] = [:]
|
||||
|
||||
private init() {}
|
||||
|
||||
/// 获取指定照片的修图状态。
|
||||
func state(for materialID: Int) -> TravelAlbumAIEditResultState {
|
||||
recordsByMaterialID[materialID]?.state ?? TravelAlbumAIEditResultState()
|
||||
}
|
||||
|
||||
/// 获取指定照片最新的修图结果。
|
||||
func editedImage(for materialID: Int) -> UIImage? {
|
||||
recordsByMaterialID[materialID]?.editedImage
|
||||
}
|
||||
|
||||
/// 获取指定照片最新的氛围感修图结果。
|
||||
func atmosphereImage(for materialID: Int) -> UIImage? {
|
||||
recordsByMaterialID[materialID]?.atmosphereImage
|
||||
}
|
||||
|
||||
/// 获取指定照片最新的封面模板结果。
|
||||
func coverImage(for materialID: Int) -> UIImage? {
|
||||
recordsByMaterialID[materialID]?.coverImage
|
||||
}
|
||||
|
||||
/// 将照片标记为修图中,已有结果继续保留到新结果完成。
|
||||
func startProcessing(materialIDs: [Int]) {
|
||||
materialIDs.forEach { materialID in
|
||||
var record = recordsByMaterialID[materialID]
|
||||
?? Record(
|
||||
state: TravelAlbumAIEditResultState(),
|
||||
editedImage: nil,
|
||||
atmosphereImage: nil,
|
||||
coverImage: nil
|
||||
)
|
||||
record.state.startProcessing()
|
||||
recordsByMaterialID[materialID] = record
|
||||
notifyChange(materialID: materialID)
|
||||
}
|
||||
}
|
||||
|
||||
/// 保存最新结果并覆盖同一照片之前的修图结果。
|
||||
func complete(
|
||||
materialID: Int,
|
||||
presetID: String,
|
||||
atmosphereOptionID: String? = nil,
|
||||
coverTemplateID: String? = nil,
|
||||
editedImage: UIImage,
|
||||
atmosphereImage: UIImage? = nil,
|
||||
coverImage: UIImage? = nil
|
||||
) {
|
||||
var record = recordsByMaterialID[materialID]
|
||||
?? Record(
|
||||
state: TravelAlbumAIEditResultState(),
|
||||
editedImage: nil,
|
||||
atmosphereImage: nil,
|
||||
coverImage: nil
|
||||
)
|
||||
record.state.complete(
|
||||
presetID: presetID,
|
||||
atmosphereOptionID: atmosphereOptionID,
|
||||
coverTemplateID: coverTemplateID
|
||||
)
|
||||
record.editedImage = editedImage
|
||||
record.atmosphereImage = atmosphereImage
|
||||
record.coverImage = coverImage
|
||||
recordsByMaterialID[materialID] = record
|
||||
notifyChange(materialID: materialID)
|
||||
}
|
||||
|
||||
/// 修图失败时恢复按钮可用状态,并保留之前成功的结果。
|
||||
func failProcessing(materialID: Int) {
|
||||
guard var record = recordsByMaterialID[materialID] else { return }
|
||||
record.state.failProcessing()
|
||||
recordsByMaterialID[materialID] = record
|
||||
notifyChange(materialID: materialID)
|
||||
}
|
||||
|
||||
/// 还原为原图时清除该照片的本地修图结果。
|
||||
func restoreOriginal(materialID: Int) {
|
||||
guard var record = recordsByMaterialID[materialID] else { return }
|
||||
record.state.restoreOriginal()
|
||||
record.editedImage = nil
|
||||
record.atmosphereImage = nil
|
||||
record.coverImage = nil
|
||||
recordsByMaterialID[materialID] = record
|
||||
notifyChange(materialID: materialID)
|
||||
}
|
||||
|
||||
/// 切换指定照片当前展示的原图或修图后版本。
|
||||
func selectDisplayMode(_ mode: TravelAlbumAIEditResultState.DisplayMode, materialID: Int) {
|
||||
guard var record = recordsByMaterialID[materialID] else { return }
|
||||
record.state.selectDisplayMode(mode)
|
||||
recordsByMaterialID[materialID] = record
|
||||
notifyChange(materialID: materialID)
|
||||
}
|
||||
|
||||
/// 删除照片时同步清理其本地修图结果。
|
||||
func remove(materialID: Int) {
|
||||
recordsByMaterialID.removeValue(forKey: materialID)
|
||||
notifyChange(materialID: materialID)
|
||||
}
|
||||
|
||||
private func notifyChange(materialID: Int) {
|
||||
NotificationCenter.default.post(
|
||||
name: Self.didChangeNotification,
|
||||
object: self,
|
||||
userInfo: [Self.materialIDUserInfoKey: materialID]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// 本地 AI 修图效果生成器;后续接入服务端时可替换为结果图片下载。
|
||||
@MainActor
|
||||
enum TravelAlbumAIEditImageProcessor {
|
||||
private static let context = CIContext()
|
||||
|
||||
/// 根据预设生成演示用修图图片。
|
||||
static func render(
|
||||
effect: TravelAlbumEditPreset.Effect,
|
||||
source: UIImage
|
||||
) -> UIImage {
|
||||
guard effect != .original, let ciImage = CIImage(image: source) else { return source }
|
||||
let output: CIImage
|
||||
switch effect {
|
||||
case .original:
|
||||
output = ciImage
|
||||
case .portrait:
|
||||
output = ciImage.applyingFilter("CIColorControls", parameters: [
|
||||
kCIInputSaturationKey: 0.86,
|
||||
kCIInputBrightnessKey: 0.07,
|
||||
kCIInputContrastKey: 1.08,
|
||||
])
|
||||
case .vintage:
|
||||
output = ciImage.applyingFilter("CISepiaTone", parameters: [kCIInputIntensityKey: 0.45])
|
||||
case .brocade:
|
||||
output = ciImage.applyingFilter("CIColorControls", parameters: [
|
||||
kCIInputSaturationKey: 1.22,
|
||||
kCIInputBrightnessKey: 0.04,
|
||||
kCIInputContrastKey: 1.1,
|
||||
])
|
||||
case .distantMountain:
|
||||
output = ciImage.applyingFilter("CIColorControls", parameters: [
|
||||
kCIInputSaturationKey: 0.72,
|
||||
kCIInputBrightnessKey: 0.1,
|
||||
kCIInputContrastKey: 0.9,
|
||||
])
|
||||
case .mist:
|
||||
output = ciImage.applyingFilter("CIColorControls", parameters: [
|
||||
kCIInputSaturationKey: 0.7,
|
||||
kCIInputBrightnessKey: 0.14,
|
||||
kCIInputContrastKey: 0.82,
|
||||
])
|
||||
case .summer:
|
||||
output = ciImage.applyingFilter("CISepiaTone", parameters: [kCIInputIntensityKey: 0.24])
|
||||
case .rich:
|
||||
output = ciImage.applyingFilter("CIColorControls", parameters: [
|
||||
kCIInputSaturationKey: 1.38,
|
||||
kCIInputBrightnessKey: -0.02,
|
||||
kCIInputContrastKey: 1.2,
|
||||
])
|
||||
}
|
||||
guard let cgImage = context.createCGImage(output, from: output.extent) else { return source }
|
||||
return UIImage(cgImage: cgImage, scale: source.scale, orientation: source.imageOrientation)
|
||||
}
|
||||
}
|
||||
|
||||
/// 本地封面模板效果生成器,让封面结果在缩略图和预览页中与普通照片明显区分。
|
||||
@MainActor
|
||||
enum TravelAlbumCoverTemplateImageProcessor {
|
||||
/// 将照片渲染为指定封面排版;无原图时使用稳定的演示背景。
|
||||
static func render(
|
||||
template: TravelAlbumCoverTemplate,
|
||||
source: UIImage?,
|
||||
targetSize: CGSize = CGSize(width: 1200, height: 900)
|
||||
) -> UIImage {
|
||||
UIGraphicsImageRenderer(size: targetSize).image { context in
|
||||
let bounds = CGRect(origin: .zero, size: targetSize)
|
||||
UIColor(hex: 0xCAD8E8).setFill()
|
||||
context.cgContext.fill(bounds)
|
||||
if let source {
|
||||
source.draw(in: aspectFillRect(imageSize: source.size, bounds: bounds))
|
||||
}
|
||||
|
||||
let unit = min(targetSize.width / 232, targetSize.height / 208)
|
||||
switch template.previewStyle {
|
||||
case .travelMemoir:
|
||||
UIColor.black.withAlphaComponent(0.24).setFill()
|
||||
context.cgContext.fill(bounds)
|
||||
UIColor.white.setStroke()
|
||||
let oval = UIBezierPath(ovalIn: CGRect(
|
||||
x: bounds.midX - 50 * unit,
|
||||
y: 28 * unit,
|
||||
width: 100 * unit,
|
||||
height: 100 * unit
|
||||
))
|
||||
oval.lineWidth = 9 * unit
|
||||
oval.stroke()
|
||||
drawCaption(
|
||||
"TRAVEL\nMEMOIR",
|
||||
in: CGRect(x: 30 * unit, y: bounds.height - 62 * unit, width: bounds.width - 60 * unit, height: 46 * unit),
|
||||
alignment: .center,
|
||||
fontSize: 17 * unit
|
||||
)
|
||||
case .minimal:
|
||||
UIColor.white.withAlphaComponent(0.9).setFill()
|
||||
context.cgContext.fill(CGRect(x: bounds.width * 0.55, y: 0, width: bounds.width * 0.45, height: bounds.height))
|
||||
drawCaption(
|
||||
"LESS\nIS MORE",
|
||||
in: CGRect(x: bounds.width * 0.62, y: bounds.midY - 27 * unit, width: bounds.width * 0.3, height: 54 * unit),
|
||||
alignment: .left,
|
||||
color: .black,
|
||||
fontSize: 17 * unit
|
||||
)
|
||||
case .scenicStory:
|
||||
UIColor.black.withAlphaComponent(0.2).setFill()
|
||||
context.cgContext.fill(bounds)
|
||||
UIColor.white.setStroke()
|
||||
let inset = 20 * unit
|
||||
let frame = UIBezierPath(rect: bounds.insetBy(dx: inset, dy: inset))
|
||||
frame.lineWidth = 4 * unit
|
||||
frame.stroke()
|
||||
drawCaption(
|
||||
"SCENIC STORY",
|
||||
in: CGRect(x: 34 * unit, y: bounds.height - 58 * unit, width: bounds.width - 68 * unit, height: 24 * unit),
|
||||
alignment: .center,
|
||||
fontSize: 17 * unit
|
||||
)
|
||||
case .film:
|
||||
let stripHeight = 24 * unit
|
||||
UIColor.black.withAlphaComponent(0.72).setFill()
|
||||
context.cgContext.fill(CGRect(x: 0, y: 0, width: bounds.width, height: stripHeight))
|
||||
context.cgContext.fill(CGRect(x: 0, y: bounds.height - stripHeight, width: bounds.width, height: stripHeight))
|
||||
UIColor.white.withAlphaComponent(0.9).setFill()
|
||||
var x = 10 * unit
|
||||
while x <= bounds.width - 16 * unit {
|
||||
context.cgContext.fill(CGRect(x: x, y: 6 * unit, width: 14 * unit, height: 10 * unit))
|
||||
context.cgContext.fill(CGRect(
|
||||
x: x,
|
||||
y: bounds.height - 16 * unit,
|
||||
width: 14 * unit,
|
||||
height: 10 * unit
|
||||
))
|
||||
x += 30 * unit
|
||||
}
|
||||
drawCaption(
|
||||
"THE JOURNEY",
|
||||
in: CGRect(x: 28 * unit, y: bounds.height - 60 * unit, width: bounds.width - 56 * unit, height: 24 * unit),
|
||||
alignment: .center,
|
||||
fontSize: 17 * unit
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func aspectFillRect(imageSize: CGSize, bounds: CGRect) -> CGRect {
|
||||
guard imageSize.width > 0, imageSize.height > 0 else { return bounds }
|
||||
let scale = max(bounds.width / imageSize.width, bounds.height / imageSize.height)
|
||||
let drawSize = CGSize(width: imageSize.width * scale, height: imageSize.height * scale)
|
||||
return CGRect(
|
||||
x: bounds.midX - drawSize.width / 2,
|
||||
y: bounds.midY - drawSize.height / 2,
|
||||
width: drawSize.width,
|
||||
height: drawSize.height
|
||||
)
|
||||
}
|
||||
|
||||
private static func drawCaption(
|
||||
_ text: String,
|
||||
in rect: CGRect,
|
||||
alignment: NSTextAlignment,
|
||||
color: UIColor = .white,
|
||||
fontSize: CGFloat
|
||||
) {
|
||||
let paragraph = NSMutableParagraphStyle()
|
||||
paragraph.alignment = alignment
|
||||
text.draw(
|
||||
in: rect,
|
||||
withAttributes: [
|
||||
.font: UIFont.systemFont(ofSize: fontSize, weight: .bold),
|
||||
.foregroundColor: color,
|
||||
.paragraphStyle: paragraph,
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -36,6 +36,7 @@ final class TravelAlbumDetailViewModel {
|
||||
private(set) var sortOption: SortOption = .createdDesc
|
||||
private(set) var isLoading = true
|
||||
private(set) var isRefreshing = false
|
||||
private(set) var isUsingPurchasedDemoData = false
|
||||
private(set) var isSelectionMode = false
|
||||
private(set) var selectedMaterialIds: Set<Int> = []
|
||||
|
||||
@ -126,6 +127,7 @@ final class TravelAlbumDetailViewModel {
|
||||
if reset {
|
||||
currentPage = 1
|
||||
canLoadMore = false
|
||||
isUsingPurchasedDemoData = false
|
||||
} else {
|
||||
guard canLoadMore, !isLoadingMore else { return }
|
||||
isLoadingMore = true
|
||||
@ -140,8 +142,15 @@ final class TravelAlbumDetailViewModel {
|
||||
orderBy: sortOption.rawValue,
|
||||
isPurchased: selectedTab == .purchased ? 1 : nil
|
||||
)
|
||||
materials = reset ? response.list : materials + response.list
|
||||
canLoadMore = materials.count < response.total
|
||||
if reset,
|
||||
selectedTab == .purchased,
|
||||
response.total == 0,
|
||||
response.list.isEmpty {
|
||||
applyPurchasedDemoMaterials()
|
||||
} else {
|
||||
materials = reset ? response.list : materials + response.list
|
||||
canLoadMore = materials.count < response.total
|
||||
}
|
||||
if selectedTab == .all {
|
||||
allPhotoCount = response.total
|
||||
}
|
||||
@ -154,6 +163,12 @@ final class TravelAlbumDetailViewModel {
|
||||
currentPage -= 1
|
||||
}
|
||||
isLoadingMore = false
|
||||
if reset, selectedTab == .purchased {
|
||||
// 已购接口暂不可用时继续提供稳定的本地演示数据,保证产品流程可演示。
|
||||
applyPurchasedDemoMaterials()
|
||||
notifyStateChange()
|
||||
return
|
||||
}
|
||||
onShowMessage?(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
@ -225,6 +240,44 @@ final class TravelAlbumDetailViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// 已购照片接口无数据时生成稳定的本地素材;真实接口一旦返回数据即不再使用。
|
||||
private func applyPurchasedDemoMaterials() {
|
||||
let demoItems: [(fileName: String, fileSize: Int, assetName: String, createdAt: String)] = [
|
||||
("IMG_0724_0001.jpeg", 2_684_928, "purchased_alpine_lake", "2026-07-24 11:25:18"),
|
||||
("IMG_0724_0002.jpeg", 3_215_360, "purchased_forest_waterfall", "2026-07-24 11:27:46"),
|
||||
("IMG_0724_0003.jpeg", 2_936_832, "purchased_grassland_path", "2026-07-24 11:31:09"),
|
||||
("IMG_0724_0004.jpeg", 3_481_600, "purchased_lakeside_flowers", "2026-07-24 11:34:22"),
|
||||
]
|
||||
var materials = demoItems.enumerated().map { index, item in
|
||||
TravelAlbumMaterial(
|
||||
id: -((albumId * 10) + index + 1),
|
||||
userEquityTravelId: albumId,
|
||||
status: 2,
|
||||
fileName: item.fileName,
|
||||
fileType: 1,
|
||||
fileUrl: TravelAlbumMaterial.bundledImageURL(assetName: item.assetName),
|
||||
fileSize: item.fileSize,
|
||||
coverUrl: TravelAlbumMaterial.bundledImageURL(assetName: item.assetName),
|
||||
isPurchased: true,
|
||||
createdAt: item.createdAt,
|
||||
updatedAt: item.createdAt
|
||||
)
|
||||
}
|
||||
switch sortOption {
|
||||
case .createdAsc:
|
||||
materials.sort { $0.createdAt < $1.createdAt }
|
||||
case .createdDesc:
|
||||
materials.sort { $0.createdAt > $1.createdAt }
|
||||
case .fileNameAsc:
|
||||
materials.sort { $0.fileName.localizedStandardCompare($1.fileName) == .orderedAscending }
|
||||
case .fileNameDesc:
|
||||
materials.sort { $0.fileName.localizedStandardCompare($1.fileName) == .orderedDescending }
|
||||
}
|
||||
self.materials = materials
|
||||
canLoadMore = false
|
||||
isUsingPurchasedDemoData = true
|
||||
}
|
||||
|
||||
private func notifyStateChange() {
|
||||
onStateChange?()
|
||||
}
|
||||
|
||||
@ -94,6 +94,7 @@ final class WiredCameraTransferViewModel {
|
||||
albumTitle: String,
|
||||
headerPhone: String,
|
||||
scenicSpotLabel: String? = nil,
|
||||
initialTransferMode: TravelAlbumOTGTransferMode? = nil,
|
||||
connectionManager: (any WiredCameraConnectionManaging)? = nil,
|
||||
storage: TravelAlbumOTGPhotoStore = TravelAlbumOTGPhotoStore(),
|
||||
uploader: (any TravelAlbumOTGUploading)? = nil,
|
||||
@ -112,7 +113,11 @@ final class WiredCameraTransferViewModel {
|
||||
self.api = api ?? NetworkServices.shared.travelAlbumAPI
|
||||
self.appStore = appStore
|
||||
self.userDefaults = userDefaults
|
||||
self.transferMode = Self.persistedTransferMode(in: userDefaults)
|
||||
// 从相册管理选择模式进入时,以本次选择为准;其他旧入口继续沿用上次记录。
|
||||
self.transferMode = initialTransferMode ?? Self.persistedTransferMode(in: userDefaults)
|
||||
if let initialTransferMode {
|
||||
userDefaults.set(initialTransferMode.rawValue, forKey: Self.transferModeDefaultsKey)
|
||||
}
|
||||
}
|
||||
|
||||
/// 相机状态文案。
|
||||
|
||||
@ -42,5 +42,9 @@
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@ -24,7 +24,18 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
self.window = window
|
||||
(UIApplication.shared.delegate as? AppDelegate)?.window = window
|
||||
|
||||
PushNotificationManager.shared.attach(window: window)
|
||||
|
||||
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) {
|
||||
@ -43,6 +54,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
_ = WeChatManager.shared.handleContinueUserActivity(userActivity)
|
||||
}
|
||||
|
||||
func sceneDidBecomeActive(_ scene: UIScene) {
|
||||
PushNotificationManager.shared.retryPendingRegistrationUpload()
|
||||
}
|
||||
|
||||
private func registerNotifications() {
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
@ -62,19 +77,35 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
name: NotificationName.userDidLogin,
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleAccountDidSwitch),
|
||||
name: NotificationName.accountDidSwitch,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
@objc private func handleSessionDidExpire() {
|
||||
PushNotificationManager.shared.handleLogout()
|
||||
AppStore.shared.logout()
|
||||
AppRouter.setRoot(.login, on: window)
|
||||
}
|
||||
|
||||
@objc private func handleUserDidLogout() {
|
||||
PushNotificationManager.shared.handleLogout()
|
||||
AppStore.shared.logout()
|
||||
AppRouter.setRoot(.login, on: window)
|
||||
}
|
||||
|
||||
@objc private func handleUserDidLogin() {
|
||||
AppRouter.setRoot(.mainTab, on: window)
|
||||
DispatchQueue.main.async {
|
||||
PushNotificationManager.shared.handleLoginCompleted()
|
||||
PushNotificationManager.shared.routePendingNotificationIfPossible()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleAccountDidSwitch() {
|
||||
PushNotificationManager.shared.handleAccountSwitched()
|
||||
}
|
||||
}
|
||||
|
||||