Compare commits
37 Commits
98fb194718
...
Company/v0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a62183a2c | ||
|
|
681b011c99 | ||
|
|
4256e01820 | ||
|
|
9777c3de28 | ||
|
|
7b88912b37 | ||
|
|
4706f4bcc6 | ||
|
|
37e105f04f | ||
|
|
c8173bf034 | ||
|
|
6f5ab10562 | ||
|
|
a0e83658c6 | ||
|
|
90360448a1 | ||
|
|
2d0063396c | ||
|
|
3a12a18687 | ||
|
|
f60a0eef14 | ||
|
|
a8319c61d8 | ||
|
|
de8627a230 | ||
|
|
9466b65b40 | ||
|
|
955cc3622f | ||
|
|
e4f4557369 | ||
|
|
02a8335d70 | ||
|
|
809cc44ca5 | ||
|
|
26d9894830 | ||
|
|
e318aaeee4 | ||
|
|
c0441f7853 | ||
|
|
7626eb8351 | ||
|
|
ceaeb5c951 | ||
|
|
e8d59495a4 | ||
|
|
8b177e5fad | ||
|
|
49ac7efa66 | ||
|
|
12a8ef9a62 | ||
|
|
099b27ed15 | ||
|
|
03e656f209 | ||
|
|
a684c7e4f7 | ||
|
|
524c7a271b | ||
|
|
5294f32ca7 | ||
|
|
bf31ffda51 | ||
|
|
1e759ba461 |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -13,3 +13,10 @@ DerivedData/
|
||||
|
||||
# Assets (distributed separately, kept locally)
|
||||
YuMi/Assets.xcassets/
|
||||
|
||||
# Documentation files
|
||||
*.md
|
||||
error message.txt
|
||||
|
||||
# Summary and documentation folder
|
||||
Docs/
|
||||
|
||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -1,3 +1,6 @@
|
||||
{
|
||||
"git.ignoreLimitWarning": true
|
||||
"git.ignoreLimitWarning": true,
|
||||
"cSpell.words": [
|
||||
"eparti"
|
||||
]
|
||||
}
|
||||
35
Podfile
35
Podfile
@@ -6,9 +6,6 @@ target 'YuMi' do
|
||||
#pag动画
|
||||
pod 'libpag'
|
||||
pod 'Bugly'
|
||||
pod 'FBSDKLoginKit'
|
||||
pod 'FBSDKCoreKit'
|
||||
pod 'FBSDKShareKit'
|
||||
# 滑动标签栏
|
||||
pod 'JXCategoryView'
|
||||
pod 'JXPagingView/Pager'
|
||||
@@ -53,19 +50,15 @@ target 'YuMi' do
|
||||
pod 'NIMSDK_LITE', '~> 10.9.40'
|
||||
pod 'GKCycleScrollView'
|
||||
pod 'SVGAPlayer'
|
||||
pod 'GoogleSignIn'
|
||||
pod 'mob_linksdk_pro'
|
||||
pod 'mob_sharesdk'
|
||||
pod 'mob_sharesdk/ShareSDKPlatforms/Apple'
|
||||
pod 'mob_sharesdk/ShareSDKExtension'
|
||||
|
||||
pod 'UMCommon', '7.5.3'
|
||||
pod 'UMDevice'
|
||||
pod 'ZLCollectionViewFlowLayout'
|
||||
pod 'TABAnimated'
|
||||
pod 'YuMi',:path=>'yum'
|
||||
pod 'QCloudCOSXML'
|
||||
pod 'TYCyclePagerView'
|
||||
|
||||
pod 'SnapKit', '~> 5.0'
|
||||
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
@@ -82,4 +75,26 @@ post_install do |installer|
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# 🔧 自动修复 SVGAPlayer 的 OSAtomic 导入问题
|
||||
# 原因: SVGAPlayer 2.5.7 的 Svga.pbobjc.m 使用旧版 protoc 生成,
|
||||
# 代码中使用了 OSAtomicCompareAndSwapPtrBarrier 但未导入头文件
|
||||
svga_pbobjc_path = 'Pods/SVGAPlayer/Source/pbobjc/Svga.pbobjc.m'
|
||||
if File.exist?(svga_pbobjc_path)
|
||||
text = File.read(svga_pbobjc_path)
|
||||
# 检查是否已经包含 OSAtomic 导入
|
||||
unless text.include?('#import <libkern/OSAtomic.h>')
|
||||
# 在 #endif 后的第一个空行位置插入导入语句
|
||||
new_text = text.sub(
|
||||
/(#define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0\n#endif\n)/,
|
||||
"\\1\n#import <libkern/OSAtomic.h>\n"
|
||||
)
|
||||
File.write(svga_pbobjc_path, new_text)
|
||||
puts "✅ [自动修复] SVGAPlayer OSAtomic 导入问题已解决"
|
||||
else
|
||||
puts "✓ [检查通过] SVGAPlayer OSAtomic 导入已存在"
|
||||
end
|
||||
else
|
||||
puts "⚠️ [警告] 未找到 SVGAPlayer pbobjc 文件,跳过修复"
|
||||
end
|
||||
end
|
||||
|
||||
137
Podfile.lock
137
Podfile.lock
@@ -14,77 +14,37 @@ PODS:
|
||||
- AFNetworking/Serialization (4.0.1)
|
||||
- AFNetworking/UIKit (4.0.1):
|
||||
- AFNetworking/NSURLSession
|
||||
- AppAuth (1.7.6):
|
||||
- AppAuth/Core (= 1.7.6)
|
||||
- AppAuth/ExternalUserAgent (= 1.7.6)
|
||||
- AppAuth/Core (1.7.6)
|
||||
- AppAuth/ExternalUserAgent (1.7.6):
|
||||
- AppAuth/Core
|
||||
- Base64 (1.1.2)
|
||||
- Bugly (2.6.1)
|
||||
- CocoaAsyncSocket (7.6.5)
|
||||
- FBAEMKit (14.1.0):
|
||||
- FBSDKCoreKit_Basics (= 14.1.0)
|
||||
- FBSDKCoreKit (14.1.0):
|
||||
- FBAEMKit (= 14.1.0)
|
||||
- FBSDKCoreKit_Basics (= 14.1.0)
|
||||
- FBSDKCoreKit_Basics (14.1.0)
|
||||
- FBSDKLoginKit (14.1.0):
|
||||
- FBSDKCoreKit (= 14.1.0)
|
||||
- FBSDKShareKit (14.1.0):
|
||||
- FBSDKCoreKit (= 14.1.0)
|
||||
- FFPopup (1.1.5)
|
||||
- FLAnimatedImage (1.0.17)
|
||||
- FlyVerifyCSDK (1.0.7)
|
||||
- GKCycleScrollView (1.2.3)
|
||||
- GoogleSignIn (7.1.0):
|
||||
- AppAuth (< 2.0, >= 1.7.3)
|
||||
- GTMAppAuth (< 5.0, >= 4.1.1)
|
||||
- GTMSessionFetcher/Core (~> 3.3)
|
||||
- GTMAppAuth (4.1.1):
|
||||
- AppAuth/Core (~> 1.7)
|
||||
- GTMSessionFetcher/Core (< 4.0, >= 3.3)
|
||||
- GTMSessionFetcher/Core (3.5.0)
|
||||
- IQKeyboardManager (6.5.19)
|
||||
- JXCategoryView (1.6.8)
|
||||
- JXPagingView/Pager (2.1.3)
|
||||
- libpag (4.4.32)
|
||||
- MarqueeLabel (4.4.0)
|
||||
- libpag (4.5.3)
|
||||
- MarqueeLabel (4.5.3)
|
||||
- Masonry (1.1.0)
|
||||
- MBProgressHUD (1.2.0)
|
||||
- MJExtension (3.4.2)
|
||||
- MJRefresh (3.7.9)
|
||||
- mob_linksdk_pro (3.3.20):
|
||||
- MOBFoundation
|
||||
- mob_sharesdk (4.4.35):
|
||||
- mob_sharesdk/ShareSDK (= 4.4.35)
|
||||
- MOBFoundation (>= 3.2.9)
|
||||
- mob_sharesdk/ShareSDK (4.4.35):
|
||||
- MOBFoundation (>= 3.2.9)
|
||||
- mob_sharesdk/ShareSDKExtension (4.4.35):
|
||||
- mob_sharesdk/ShareSDK
|
||||
- MOBFoundation (>= 3.2.9)
|
||||
- mob_sharesdk/ShareSDKPlatforms/Apple (4.4.35):
|
||||
- mob_sharesdk/ShareSDK
|
||||
- MOBFoundation (>= 3.2.9)
|
||||
- MOBFoundation (20250528):
|
||||
- FlyVerifyCSDK (>= 0.0.7)
|
||||
- NIMSDK_LITE (10.9.42):
|
||||
- NIMSDK_LITE/NOS (= 10.9.42)
|
||||
- NIMSDK_LITE (10.9.53):
|
||||
- NIMSDK_LITE/NOS (= 10.9.53)
|
||||
- YXArtemis_XCFramework
|
||||
- NIMSDK_LITE/NOS (10.9.42):
|
||||
- NIMSDK_LITE/NOS (10.9.53):
|
||||
- YXArtemis_XCFramework
|
||||
- pop (1.0.12)
|
||||
- Protobuf (3.29.5)
|
||||
- QCloudCore (6.4.9):
|
||||
- QCloudCore/Default (= 6.4.9)
|
||||
- QCloudCore/Default (6.4.9):
|
||||
- QCloudTrack/Beacon (= 6.4.9)
|
||||
- QCloudCOSXML (6.4.9):
|
||||
- QCloudCOSXML/Default (= 6.4.9)
|
||||
- QCloudCOSXML/Default (6.4.9):
|
||||
- QCloudCore (= 6.4.9)
|
||||
- QCloudTrack/Beacon (6.4.9)
|
||||
- QCloudCore (6.5.1):
|
||||
- QCloudCore/Default (= 6.5.1)
|
||||
- QCloudCore/Default (6.5.1):
|
||||
- QCloudTrack/Beacon (= 6.5.1)
|
||||
- QCloudCOSXML (6.5.1):
|
||||
- QCloudCOSXML/Default (= 6.5.1)
|
||||
- QCloudCOSXML/Default (6.5.1):
|
||||
- QCloudCore (= 6.5.1)
|
||||
- QCloudTrack/Beacon (6.5.1)
|
||||
- QGVAPlayer (1.0.19)
|
||||
- ReactiveObjC (3.1.1)
|
||||
- SDCycleScrollView (1.82):
|
||||
@@ -95,6 +55,7 @@ PODS:
|
||||
- SDWebImageFLPlugin (0.6.0):
|
||||
- FLAnimatedImage (>= 1.0.11)
|
||||
- SDWebImage/Core (~> 5.10)
|
||||
- SnapKit (5.7.1)
|
||||
- SSKeychain (1.4.1)
|
||||
- SSZipArchive (2.4.3)
|
||||
- SVGAPlayer (2.5.7):
|
||||
@@ -107,18 +68,15 @@ PODS:
|
||||
- Protobuf (~> 3.4)
|
||||
- SZTextView (1.3.0)
|
||||
- TABAnimated (2.6.6)
|
||||
- TXLiteAVSDK_TRTC (12.6.18866):
|
||||
- TXLiteAVSDK_TRTC/TRTC (= 12.6.18866)
|
||||
- TXLiteAVSDK_TRTC/TRTC (12.6.18866)
|
||||
- TXLiteAVSDK_TRTC (12.8.19666):
|
||||
- TXLiteAVSDK_TRTC/TRTC (= 12.8.19666)
|
||||
- TXLiteAVSDK_TRTC/TRTC (12.8.19666)
|
||||
- TYCyclePagerView (1.2.0)
|
||||
- TZImagePickerController (3.8.9):
|
||||
- TZImagePickerController/Basic (= 3.8.9)
|
||||
- TZImagePickerController/Location (= 3.8.9)
|
||||
- TZImagePickerController/Basic (3.8.9)
|
||||
- TZImagePickerController/Location (3.8.9)
|
||||
- UMCommon (7.5.3):
|
||||
- UMDevice
|
||||
- UMDevice (3.4.0)
|
||||
- YuMi (0.0.1)
|
||||
- YXArtemis_XCFramework (1.1.6)
|
||||
- YYCache (1.0.4)
|
||||
@@ -136,13 +94,9 @@ DEPENDENCIES:
|
||||
- Base64
|
||||
- Bugly
|
||||
- CocoaAsyncSocket
|
||||
- FBSDKCoreKit
|
||||
- FBSDKLoginKit
|
||||
- FBSDKShareKit
|
||||
- FFPopup
|
||||
- FLAnimatedImage
|
||||
- GKCycleScrollView
|
||||
- GoogleSignIn
|
||||
- IQKeyboardManager
|
||||
- JXCategoryView
|
||||
- JXPagingView/Pager
|
||||
@@ -152,10 +106,6 @@ DEPENDENCIES:
|
||||
- MBProgressHUD
|
||||
- MJExtension (= 3.4.2)
|
||||
- MJRefresh (= 3.7.9)
|
||||
- mob_linksdk_pro
|
||||
- mob_sharesdk
|
||||
- mob_sharesdk/ShareSDKExtension
|
||||
- mob_sharesdk/ShareSDKPlatforms/Apple
|
||||
- NIMSDK_LITE (~> 10.9.40)
|
||||
- pop
|
||||
- QCloudCOSXML
|
||||
@@ -164,6 +114,7 @@ DEPENDENCIES:
|
||||
- SDCycleScrollView
|
||||
- SDWebImage (= 5.21.3)
|
||||
- SDWebImageFLPlugin
|
||||
- SnapKit (~> 5.0)
|
||||
- SSKeychain
|
||||
- SVGAPlayer
|
||||
- SZTextView
|
||||
@@ -171,8 +122,6 @@ DEPENDENCIES:
|
||||
- TXLiteAVSDK_TRTC
|
||||
- TYCyclePagerView
|
||||
- TZImagePickerController
|
||||
- UMCommon (= 7.5.3)
|
||||
- UMDevice
|
||||
- YuMi (from `yum`)
|
||||
- YYText
|
||||
- YYWebImage
|
||||
@@ -181,22 +130,12 @@ DEPENDENCIES:
|
||||
SPEC REPOS:
|
||||
https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git:
|
||||
- AFNetworking
|
||||
- AppAuth
|
||||
- Base64
|
||||
- Bugly
|
||||
- CocoaAsyncSocket
|
||||
- FBAEMKit
|
||||
- FBSDKCoreKit
|
||||
- FBSDKCoreKit_Basics
|
||||
- FBSDKLoginKit
|
||||
- FBSDKShareKit
|
||||
- FFPopup
|
||||
- FLAnimatedImage
|
||||
- FlyVerifyCSDK
|
||||
- GKCycleScrollView
|
||||
- GoogleSignIn
|
||||
- GTMAppAuth
|
||||
- GTMSessionFetcher
|
||||
- IQKeyboardManager
|
||||
- JXCategoryView
|
||||
- JXPagingView
|
||||
@@ -206,9 +145,6 @@ SPEC REPOS:
|
||||
- MBProgressHUD
|
||||
- MJExtension
|
||||
- MJRefresh
|
||||
- mob_linksdk_pro
|
||||
- mob_sharesdk
|
||||
- MOBFoundation
|
||||
- NIMSDK_LITE
|
||||
- pop
|
||||
- Protobuf
|
||||
@@ -220,6 +156,7 @@ SPEC REPOS:
|
||||
- SDCycleScrollView
|
||||
- SDWebImage
|
||||
- SDWebImageFLPlugin
|
||||
- SnapKit
|
||||
- SSKeychain
|
||||
- SSZipArchive
|
||||
- SVGAPlayer
|
||||
@@ -228,8 +165,6 @@ SPEC REPOS:
|
||||
- TXLiteAVSDK_TRTC
|
||||
- TYCyclePagerView
|
||||
- TZImagePickerController
|
||||
- UMCommon
|
||||
- UMDevice
|
||||
- YXArtemis_XCFramework
|
||||
- YYCache
|
||||
- YYImage
|
||||
@@ -243,55 +178,41 @@ EXTERNAL SOURCES:
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
AFNetworking: 3bd23d814e976cd148d7d44c3ab78017b744cd58
|
||||
AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
|
||||
Base64: cecfb41a004124895a7bcee567a89bae5a89d49b
|
||||
Bugly: 217ac2ce5f0f2626d43dbaa4f70764c953a26a31
|
||||
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
|
||||
FBAEMKit: a899515e45476027f73aef377b5cffadcd56ca3a
|
||||
FBSDKCoreKit: 24f8bc8d3b5b2a8c5c656a1329492a12e8efa792
|
||||
FBSDKCoreKit_Basics: 6e578c9bdc7aa1365dbbbde633c9ebb536bcaa98
|
||||
FBSDKLoginKit: 787de205d524c3a4b17d527916f1d066e4361660
|
||||
FBSDKShareKit: b9c1cd1fa6a320a50f0f353cf30d589049c8db77
|
||||
FFPopup: a208dcee8db3e54ec4a88fcd6481f6f5d85b7a83
|
||||
FLAnimatedImage: bbf914596368867157cc71b38a8ec834b3eeb32b
|
||||
FlyVerifyCSDK: e0a13f11d4f29aca7fb7fdcff3f27e3b7ba2de5d
|
||||
GKCycleScrollView: 8ed79d2142e62895a701973358b6f94b661b4829
|
||||
GoogleSignIn: d4281ab6cf21542b1cfaff85c191f230b399d2db
|
||||
GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de
|
||||
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
||||
IQKeyboardManager: c8665b3396bd0b79402b4c573eac345a31c7d485
|
||||
JXCategoryView: 262d503acea0b1278c79a1c25b7332ffaef4d518
|
||||
JXPagingView: afdd2e9af09c90160dd232b970d603cc6e7ddd0e
|
||||
libpag: 6e8253018ee4e7f310c8c07d9d9a89d7ae58ae27
|
||||
MarqueeLabel: d2388949ac58d587303178d56a792ba8a001b037
|
||||
libpag: c59ae60dbae9e025465e9541ee03ee96994f4c73
|
||||
MarqueeLabel: 0c57d4c6634e04a6d015af79f7c9a175b2309525
|
||||
Masonry: 678fab65091a9290e40e2832a55e7ab731aad201
|
||||
MBProgressHUD: 3ee5efcc380f6a79a7cc9b363dd669c5e1ae7406
|
||||
MJExtension: e97d164cb411aa9795cf576093a1fa208b4a8dd8
|
||||
MJRefresh: ff9e531227924c84ce459338414550a05d2aea78
|
||||
mob_linksdk_pro: d6ac555e9bb8d2743a8634032a70ea1d34119a50
|
||||
mob_sharesdk: 409503324d18f231dd27b4d26428c0c168b20c36
|
||||
MOBFoundation: a1f193058aba95440dadeb799fb398ff92cfe45e
|
||||
NIMSDK_LITE: 67f6815667acefdc8f9969f8c955b5c1fab490df
|
||||
NIMSDK_LITE: 79bc52b8ad905e6c088053d8d29e7863fb9cdb98
|
||||
pop: d582054913807fd11fd50bfe6a539d91c7e1a55a
|
||||
Protobuf: 164aea2ae380c3951abdc3e195220c01d17400e0
|
||||
QCloudCore: 0e70cda608d1ac485e039e83be1c4a1197197e6b
|
||||
QCloudCOSXML: b7f0b9cac61780a03318d40367a879f8d7eb3d86
|
||||
QCloudTrack: cc101dd57be7f87bffc3f2fb692a781d5efeda98
|
||||
QCloudCore: 6f8c67b96448472d2c6a92b9cfe1bdb5abbb1798
|
||||
QCloudCOSXML: 92f50a787b4e8d9a7cb6ea8e626775256b4840a7
|
||||
QCloudTrack: 20b79388365b4c8ed150019c82a56f1569f237f8
|
||||
QGVAPlayer: a0bca68c9bd6f1c8de5ac2d10ddf98be6038cce9
|
||||
ReactiveObjC: 011caa393aa0383245f2dcf9bf02e86b80b36040
|
||||
SDCycleScrollView: a0d74c3384caa72bdfc81470bdbc8c14b3e1fbcf
|
||||
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
|
||||
SDWebImageFLPlugin: 72efd2cfbf565bc438421abb426f4bcf7b670754
|
||||
SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a
|
||||
SSKeychain: 55cc80f66f5c73da827e3077f02e43528897db41
|
||||
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
|
||||
SVGAPlayer: 318b85a78b61292d6ae9dfcd651f3f0d1cdadd86
|
||||
SZTextView: 094dc6acc9beec537685c545d6e3e0d4975174e1
|
||||
TABAnimated: 75fece541a774193565697c7a11539d3c6f631b3
|
||||
TXLiteAVSDK_TRTC: 09552a5bb5571c85c851d8dd858064724639f55e
|
||||
TXLiteAVSDK_TRTC: b576b0c6a477fa98b5d2b33be63fa9aa7c41f0eb
|
||||
TYCyclePagerView: 2b051dade0615c70784aa34f40c646feeddb7344
|
||||
TZImagePickerController: 456f470b5dea97b37226ec7a694994a8663340b2
|
||||
UMCommon: 3b850836e8bc162b4e7f6b527d30071ed8ea75a1
|
||||
UMDevice: dcdf7ec167387837559d149fbc7d793d984faf82
|
||||
YuMi: 6c5f00f1eccbcea3304feae03cbe659025fdb9cb
|
||||
YXArtemis_XCFramework: d9a8b9439d7a6c757ed00ada53a6d2dd9b13f9c7
|
||||
YYCache: 8105b6638f5e849296c71f331ff83891a4942952
|
||||
@@ -300,6 +221,6 @@ SPEC CHECKSUMS:
|
||||
YYWebImage: 5f7f36aee2ae293f016d418c7d6ba05c4863e928
|
||||
ZLCollectionViewFlowLayout: c99024652ce9f0c57d33ab53052c9b85e4a936b7
|
||||
|
||||
PODFILE CHECKSUM: b14955816bdf61713f83a3de2cac5823a1e1449a
|
||||
PODFILE CHECKSUM: 3ef6e2b784d16a5b9d2c5cdd03f8bbf3ed3483ce
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -56,6 +56,11 @@
|
||||
value = "disable"
|
||||
isEnabled = "NO">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "SWIFT_DISABLE_SAFETY_CHECKS"
|
||||
value = "YES"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
|
||||
@@ -8,10 +8,8 @@
|
||||
#import "AppDelegate+ThirdConfig.h"
|
||||
///Third
|
||||
#import <NIMSDK/NIMSDK.h>
|
||||
#import <ShareSDK/ShareSDK.h>
|
||||
#import <UserNotifications/UNUserNotificationCenter.h>
|
||||
#import <UserNotifications/UserNotifications.h>
|
||||
#import <MOBFoundation/MobSDK+Privacy.h>
|
||||
///Tool
|
||||
#import "YUMIConstant.h"
|
||||
#import "CustomAttachmentDecoder.h"
|
||||
@@ -40,7 +38,6 @@ UIKIT_EXTERN NSString * adImageName;
|
||||
/// 初始化一些第三方配置
|
||||
- (void)initThirdConfig{
|
||||
[self setLanguage];
|
||||
[self configShareSDK];
|
||||
[self configNIMSDK];
|
||||
[self configBugly];
|
||||
[self registerNot];
|
||||
@@ -123,24 +120,6 @@ UIKIT_EXTERN NSString * adImageName;
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)configShareSDK {
|
||||
|
||||
// [PILineLoginManager registerLine];
|
||||
|
||||
[ShareSDK registPlatforms:^(SSDKRegister *platformsRegister) {
|
||||
///faceBook
|
||||
// [platformsRegister setupFacebookWithAppkey:@"1266232494209868" appSecret:@"c9b170b383f8be9cdf118823b8632821" displayName:YMLocalizedString(@"AppDelegate_ThirdConfig0")];
|
||||
[platformsRegister setupLineAuthType:SSDKAuthorizeTypeBoth];
|
||||
}];
|
||||
|
||||
NSString *isUpload = [[NSUserDefaults standardUserDefaults]valueForKey:@"kMobLinkUploadPrivacy"];
|
||||
if (isUpload == nil){
|
||||
[MobSDK uploadPrivacyPermissionStatus:YES onResult:nil];
|
||||
[[NSUserDefaults standardUserDefaults] setValue:@"YES" forKey:@"kMobLinkUploadPrivacy"];
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - 表情
|
||||
- (void)initEmojiData {
|
||||
dispatch_async(dispatch_get_global_queue(0, 0), ^{
|
||||
@@ -177,6 +156,8 @@ UIKIT_EXTERN NSString * adImageName;
|
||||
设置广告页
|
||||
*/
|
||||
- (void)setupLaunchADView {
|
||||
return;
|
||||
|
||||
NSUserDefaults * kUserDefaults = NSUserDefaults.standardUserDefaults;
|
||||
// 判断沙盒中是否存在广告图片,如果存在,直接显示
|
||||
NSString *adName = [kUserDefaults stringForKey:adImageName];
|
||||
|
||||
@@ -6,15 +6,10 @@
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <CoreData/CoreData.h>
|
||||
|
||||
@interface AppDelegate : UIResponder <UIApplicationDelegate>
|
||||
|
||||
@property (strong, nonatomic) UIWindow *window;
|
||||
@property(nonatomic,strong,readonly)NSManagedObjectContext *managedObjectContext;
|
||||
@property(nonatomic,strong,readonly)NSManagedObjectModel *managedObjectModel;
|
||||
@property(nonatomic,strong,readonly)NSPersistentStoreCoordinator *persistentStoreCoordinator;
|
||||
|
||||
- (void)saveContext;
|
||||
- (NSURL *)applicationDocumentsDirectory;
|
||||
@end
|
||||
|
||||
|
||||
@@ -7,28 +7,20 @@
|
||||
|
||||
|
||||
#import "AppDelegate.h"
|
||||
#import <UMCommon/UMCommon.h>
|
||||
#import <MobLinkPro/MobLink.h>
|
||||
#import <MobLinkPro/MLSDKScene.h>
|
||||
#import "TabbarViewController.h"
|
||||
#import "BaseNavigationController.h"
|
||||
#import "AppDelegate+ThirdConfig.h"
|
||||
#import <NIMSDK/NIMSDK.h>
|
||||
#import <AppTrackingTransparency/AppTrackingTransparency.h>
|
||||
#import "ClientConfig.h"
|
||||
#import <GoogleSignIn/GoogleSignIn.h>
|
||||
#import <GoogleSignIn/GoogleSignIn.h>
|
||||
#import "LoginViewController.h"
|
||||
#import "AccountModel.h"
|
||||
#import "YuMi-swift.h"
|
||||
#import "SessionViewController.h"
|
||||
#import "LoginFullInfoViewController.h"
|
||||
#import "UIView+VAP.h"
|
||||
#import "SocialShareManager.h"
|
||||
#import "EPSignatureColorGuideView.h"
|
||||
#import "EPEmotionColorStorage.h"
|
||||
#import "EPNIMManager.h"
|
||||
|
||||
UIKIT_EXTERN NSString * const kOpenRoomNotification;
|
||||
|
||||
@interface AppDelegate ()<IMLSDKRestoreDelegate>
|
||||
@interface AppDelegate ()
|
||||
|
||||
@end
|
||||
|
||||
@@ -63,37 +55,51 @@ void qg_VAP_Logger_handler(VAPLogLevel level, const char* file, int line, const
|
||||
self.window.rootViewController = launchScreenVC;
|
||||
[self.window makeKeyAndVisible];
|
||||
|
||||
[VAPView registerHWDLog:qg_VAP_Logger_handler];
|
||||
[self setupUIAppearance];
|
||||
|
||||
///初始化一些 sdk配置
|
||||
[self initThirdConfig];
|
||||
[self initUM:application launchOptions:launchOptions];
|
||||
|
||||
@kWeakify(self);
|
||||
[[ClientConfig shareConfig] clientConfig:^{
|
||||
@kStrongify(self);
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self loadMainPage];
|
||||
[self setupLaunchADView];
|
||||
});
|
||||
}];
|
||||
|
||||
if (@available(iOS 15, *)) {
|
||||
[[UITableView appearance] setSectionHeaderTopPadding:0];
|
||||
}
|
||||
[self setupConfig];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)initUM:(UIApplication *)application
|
||||
launchOptions:(NSDictionary *)launchOptions {
|
||||
// 只有同意过了隐私协议 才初始化
|
||||
if ([[NSUserDefaults standardUserDefaults] objectForKey:@"kYouMinumbernnagna"]) {
|
||||
///初始化友盟
|
||||
[UMConfigure initWithAppkey:@"6434c6dfd64e686139618269"
|
||||
channel:@"appstore"];
|
||||
- (void)setupUIAppearance {
|
||||
if (@available(iOS 15, *)) {
|
||||
[[UITableView appearance] setSectionHeaderTopPadding:0];
|
||||
}
|
||||
[MobLink setDelegate:self];
|
||||
}
|
||||
|
||||
- (void)setupConfig {
|
||||
// 冷启动配置:client/init → client/config,由 EPConfigManager 统一调度
|
||||
@kWeakify(self);
|
||||
[[EPConfigManager shared] startColdBootWithOnSuccess:^{
|
||||
@kStrongify(self);
|
||||
if (!self) return;
|
||||
|
||||
// 配置成功,初始化 NIMSDK 并进入主页面
|
||||
@kWeakify(self);
|
||||
[[EPNIMManager sharedManager] initializeWithCompletion:^(NSError * _Nullable error) {
|
||||
@kStrongify(self);
|
||||
if (!self) return;
|
||||
|
||||
if (error) {
|
||||
NSLog(@"[AppDelegate] NIMSDK 初始化失败: %@", error);
|
||||
} else {
|
||||
NSLog(@"[AppDelegate] NIMSDK 初始化成功");
|
||||
}
|
||||
// 无论 NIMSDK 是否成功,都进入主页面
|
||||
[self loadMainPage];
|
||||
}];
|
||||
} onFailure:^(NSString * _Nonnull errorMessage) {
|
||||
@kStrongify(self);
|
||||
if (!self) return;
|
||||
|
||||
// 配置失败,显示错误提示
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:YMLocalizedString(@"提示")
|
||||
message:errorMessage
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:YMLocalizedString(@"确定") style:UIAlertActionStyleDefault handler:nil]];
|
||||
[self.window.rootViewController presentViewController:alert animated:YES completion:nil];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)loadMainPage {
|
||||
@@ -104,36 +110,84 @@ void qg_VAP_Logger_handler(VAPLogLevel level, const char* file, int line, const
|
||||
[self toLoginPage];
|
||||
}else{
|
||||
[self toHomeTabbarPage];
|
||||
|
||||
// 延迟检查专属颜色(等待 window 初始化完成)
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.8 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
[self checkAndShowSignatureColorGuide];
|
||||
});
|
||||
}
|
||||
|
||||
[[ClientConfig shareConfig] clientInit];
|
||||
[self ignoreVAPLog];
|
||||
}
|
||||
|
||||
- (void)ignoreVAPLog {
|
||||
[VAPView registerHWDLog:qg_VAP_Logger_handler];
|
||||
}
|
||||
|
||||
/// 检查并显示专属颜色引导页
|
||||
- (void)checkAndShowSignatureColorGuide {
|
||||
UIWindow *keyWindow = kWindow;
|
||||
if (!keyWindow) return;
|
||||
|
||||
BOOL hasSignatureColor = [EPEmotionColorStorage hasUserSignatureColor];
|
||||
|
||||
#if 0
|
||||
// Debug 环境:总是显示引导页
|
||||
NSLog(@"[AppDelegate] Debug 模式:显示专属颜色引导页(已有颜色: %@)", hasSignatureColor ? @"YES" : @"NO");
|
||||
|
||||
EPSignatureColorGuideView *guideView = [[EPSignatureColorGuideView alloc] init];
|
||||
|
||||
// 设置颜色确认回调
|
||||
guideView.onColorConfirmed = ^(NSString *hexColor) {
|
||||
[EPEmotionColorStorage saveUserSignatureColor:hexColor];
|
||||
NSLog(@"[AppDelegate] 用户选择专属颜色: %@", hexColor);
|
||||
};
|
||||
|
||||
// 如果已有颜色,设置 Skip 回调
|
||||
if (hasSignatureColor) {
|
||||
guideView.onSkipTapped = ^{
|
||||
NSLog(@"[AppDelegate] 用户跳过专属颜色选择");
|
||||
};
|
||||
}
|
||||
|
||||
// 显示引导页,已有颜色时显示 Skip 按钮
|
||||
[guideView showInWindow:keyWindow showSkipButton:hasSignatureColor];
|
||||
|
||||
#else
|
||||
// Release 环境:仅在未设置专属颜色时显示
|
||||
if (!hasSignatureColor) {
|
||||
EPSignatureColorGuideView *guideView = [[EPSignatureColorGuideView alloc] init];
|
||||
guideView.onColorConfirmed = ^(NSString *hexColor) {
|
||||
[EPEmotionColorStorage saveUserSignatureColor:hexColor];
|
||||
NSLog(@"[AppDelegate] 用户选择专属颜色: %@", hexColor);
|
||||
};
|
||||
[guideView showInWindow:keyWindow];
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)toLoginPage {
|
||||
LoginViewController *lvc = [[LoginViewController alloc] init];
|
||||
BaseNavigationController * navigationController = [[BaseNavigationController alloc] initWithRootViewController:lvc];
|
||||
// 使用新的 Swift 登录页面
|
||||
EPLoginViewController *lvc = [[EPLoginViewController alloc] init];
|
||||
BaseNavigationController *navigationController =
|
||||
[[BaseNavigationController alloc] initWithRootViewController:lvc];
|
||||
navigationController.modalPresentationStyle = UIModalPresentationFullScreen;
|
||||
self.window.rootViewController = navigationController;
|
||||
}
|
||||
|
||||
- (void)toHomeTabbarPage {
|
||||
TabbarViewController *vc = [[TabbarViewController alloc] init];
|
||||
BaseNavigationController *navigationController = [[BaseNavigationController alloc] initWithRootViewController:vc];
|
||||
self.window.rootViewController = navigationController;
|
||||
}
|
||||
|
||||
- (void)IMLSDKWillRestoreScene:(MLSDKScene *)scene
|
||||
Restore:(void (^)(BOOL, RestoreStyle))restoreHandler {
|
||||
NSString *inviteCode = scene.params[@"inviteCode"];
|
||||
if (inviteCode != nil && [[AccountInfoStorage instance]getUid].length == 0){
|
||||
ClientConfig *config = [ClientConfig shareConfig];
|
||||
config.inviteCode = inviteCode;
|
||||
EPTabBarController *epTabBar = [EPTabBarController create];
|
||||
[epTabBar refreshTabBarWithIsLogin:YES];
|
||||
|
||||
UIWindow *window = kWindow;
|
||||
if (window) {
|
||||
window.rootViewController = epTabBar;
|
||||
[window makeKeyAndVisible];
|
||||
}
|
||||
restoreHandler(YES, MLDefault);
|
||||
}
|
||||
|
||||
- (void)applicationDidEnterBackground:(UIApplication *)application {
|
||||
NSInteger count = [NIMSDK sharedSDK].conversationManager.allUnreadCount;
|
||||
NSInteger count = [[EPNIMManager sharedManager] allUnreadCount];
|
||||
[[UIApplication sharedApplication] setApplicationIconBadgeNumber:count];
|
||||
}
|
||||
|
||||
@@ -169,8 +223,8 @@ void qg_VAP_Logger_handler(VAPLogLevel level, const char* file, int line, const
|
||||
}
|
||||
|
||||
- (void)application:(UIApplication *)app didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
|
||||
// 上传devicetoken至云信服务器。
|
||||
[[NIMSDK sharedSDK] updateApnsToken:deviceToken ];
|
||||
// 上传 deviceToken 至云信服务器(统一走 EPNIMManager)
|
||||
[[EPNIMManager sharedManager] updateApnsToken:deviceToken];
|
||||
}
|
||||
|
||||
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler{
|
||||
@@ -202,124 +256,9 @@ void qg_VAP_Logger_handler(VAPLogLevel level, const char* file, int line, const
|
||||
|
||||
///URL Scheme跳转
|
||||
-(BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<NSString *,id> *)options{
|
||||
// TODO: 在 EPTabbar 中补充 [SocialShareManager sharedManager] setHandleJumpToRoom
|
||||
[[SocialShareManager sharedManager] handleURL:url];
|
||||
|
||||
return [GIDSignIn.sharedInstance handleURL:url];
|
||||
}
|
||||
|
||||
//- (void)__oldApplicationOpenURLMethod:(NSURL *)url {
|
||||
// NSString *text = [url query];
|
||||
// if(text.length){
|
||||
// NSMutableDictionary *paramsDict = [NSMutableDictionary dictionary];
|
||||
// NSArray *paramArray = [text componentsSeparatedByString:@"&"];
|
||||
// for (NSString *param in paramArray) {
|
||||
// if (param && param.length) {
|
||||
// NSArray *parArr = [param componentsSeparatedByString:@"="];
|
||||
// if (parArr.count == 2) {
|
||||
// [paramsDict setObject:parArr[1] forKey:parArr[0]];
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// if(paramsDict[@"type"] != nil){
|
||||
// NSInteger type = [paramsDict[@"type"] integerValue];
|
||||
// if (type == 2) {
|
||||
// NSString *uid = [NSString stringWithFormat:@"%@",paramsDict[@"uid"]];
|
||||
// [[NSNotificationCenter defaultCenter]postNotificationName:kOpenRoomNotification object:nil userInfo:@{@"uid":uid}];
|
||||
// ClientConfig *config = [ClientConfig shareConfig];
|
||||
// config.roomId = uid;
|
||||
// }else if(type == 7){
|
||||
// NSString *uid = [NSString stringWithFormat:@"%@",paramsDict[@"uid"]];
|
||||
// [[NSNotificationCenter defaultCenter]postNotificationName:kOpenRoomNotification object:nil userInfo:@{@"type":@"kOpenChat",@"uid":uid}];
|
||||
// ClientConfig *config = [ClientConfig shareConfig];
|
||||
// config.chatId = uid;
|
||||
// }else if (type == 8){
|
||||
// NSString *inviteCode = paramsDict[@"inviteCode"];
|
||||
// if (inviteCode != nil && [[AccountInfoStorage instance]getUid].length == 0){
|
||||
// ClientConfig *config = [ClientConfig shareConfig];
|
||||
// config.inviteCode = inviteCode;
|
||||
// }
|
||||
// }
|
||||
//// return YES;
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
#pragma mark - Core Data stack
|
||||
@synthesize managedObjectContext = _managedObjectContext;
|
||||
@synthesize managedObjectModel = _managedObjectModel;
|
||||
@synthesize persistentStoreCoordinator = _persistentStoreCoordinator;
|
||||
|
||||
-(NSURL *)applicationDocumentsDirectory{
|
||||
return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
|
||||
}
|
||||
|
||||
- (NSManagedObjectModel *)managedObjectModel {
|
||||
// The managed object model for the application. It is a fatal error for the application not to be able to find and load its model.
|
||||
if (_managedObjectModel != nil) {
|
||||
return _managedObjectModel;
|
||||
}
|
||||
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"_1_______" withExtension:@"momd"];
|
||||
_managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
|
||||
return _managedObjectModel;
|
||||
}
|
||||
|
||||
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
|
||||
// The persistent store coordinator for the application. This implementation creates and returns a coordinator, having added the store for the application to it.
|
||||
if (_persistentStoreCoordinator != nil) {
|
||||
return _persistentStoreCoordinator;
|
||||
}
|
||||
|
||||
// Create the coordinator and store
|
||||
|
||||
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
|
||||
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"_1_______.sqlite"];
|
||||
NSError *error = nil;
|
||||
NSString *failureReason = @"There was an error creating or loading the application's saved data.";
|
||||
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
|
||||
// Report any error we got.
|
||||
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
|
||||
dict[NSLocalizedDescriptionKey] = @"Failed to initialize the application's saved data";
|
||||
dict[NSLocalizedFailureReasonErrorKey] = failureReason;
|
||||
dict[NSUnderlyingErrorKey] = error;
|
||||
error = [NSError errorWithDomain:@"YOUR_ERROR_DOMAIN" code:9999 userInfo:dict];
|
||||
// Replace this with code to handle the error appropriately.
|
||||
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||
// NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
|
||||
abort();
|
||||
}
|
||||
|
||||
return _persistentStoreCoordinator;
|
||||
}
|
||||
|
||||
|
||||
- (NSManagedObjectContext *)managedObjectContext {
|
||||
// Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.)
|
||||
if (_managedObjectContext != nil) {
|
||||
return _managedObjectContext;
|
||||
}
|
||||
|
||||
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
|
||||
if (!coordinator) {
|
||||
return nil;
|
||||
}
|
||||
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
|
||||
[_managedObjectContext setPersistentStoreCoordinator:coordinator];
|
||||
return _managedObjectContext;
|
||||
}
|
||||
|
||||
#pragma mark - Core Data Saving support
|
||||
|
||||
- (void)saveContext {
|
||||
NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
|
||||
if (managedObjectContext != nil) {
|
||||
NSError *error = nil;
|
||||
if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
|
||||
// Replace this implementation with code to handle the error appropriately.
|
||||
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||
// NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
|
||||
abort();
|
||||
}
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24128" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina5_9" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24063"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
@@ -16,46 +16,26 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="pi_app_logo_new_bg.png" translatesAutoresizingMaskIntoConstraints="NO" id="sON-N7-5Wv">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="355"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="355" id="BrK-cy-oiN"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Meet your exclusive voice~" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="o5T-sv-tDU">
|
||||
<rect key="frame" x="79.333333333333329" y="312" width="216.66666666666669" height="22"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="18"/>
|
||||
<color key="textColor" red="0.023529411760000001" green="0.043137254899999998" blue="0.090196078430000007" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="pi_login_new_logo.png" translatesAutoresizingMaskIntoConstraints="NO" id="v2t-MR-31f">
|
||||
<rect key="frame" x="122.66666666666669" y="140" width="130" height="148"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="130" id="mQh-M0-hFI"/>
|
||||
<constraint firstAttribute="height" constant="148" id="tX3-Va-dub"/>
|
||||
</constraints>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="ep_splash.png" translatesAutoresizingMaskIntoConstraints="NO" id="sON-N7-5Wv">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="r4O-Vu-IrR"/>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="sON-N7-5Wv" firstAttribute="leading" secondItem="r4O-Vu-IrR" secondAttribute="leading" id="CEl-rE-BeK"/>
|
||||
<constraint firstItem="o5T-sv-tDU" firstAttribute="top" secondItem="v2t-MR-31f" secondAttribute="bottom" constant="24" id="GEv-XM-qev"/>
|
||||
<constraint firstItem="sON-N7-5Wv" firstAttribute="trailing" secondItem="r4O-Vu-IrR" secondAttribute="trailing" id="MsB-m5-LHI"/>
|
||||
<constraint firstItem="sON-N7-5Wv" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="SM6-2S-etM"/>
|
||||
<constraint firstItem="v2t-MR-31f" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" constant="140" id="YA3-7E-mLb"/>
|
||||
<constraint firstItem="o5T-sv-tDU" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="Yej-IY-emP"/>
|
||||
<constraint firstItem="v2t-MR-31f" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="x8C-D7-WvQ"/>
|
||||
<constraint firstAttribute="bottom" secondItem="sON-N7-5Wv" secondAttribute="bottom" id="0zO-vt-zzT"/>
|
||||
<constraint firstItem="sON-N7-5Wv" firstAttribute="trailing" secondItem="r4O-Vu-IrR" secondAttribute="trailing" id="MAy-os-QAw"/>
|
||||
<constraint firstItem="sON-N7-5Wv" firstAttribute="leading" secondItem="r4O-Vu-IrR" secondAttribute="leading" id="Onc-xX-tha"/>
|
||||
<constraint firstItem="sON-N7-5Wv" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="vhU-0c-IHX"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="52.671755725190835" y="374.64788732394368"/>
|
||||
<point key="canvasLocation" x="52" y="374.6305418719212"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="pi_app_logo_new_bg.png" width="1125" height="273"/>
|
||||
<image name="pi_login_new_logo.png" width="486" height="96"/>
|
||||
<image name="ep_splash.png" width="1125" height="2436"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
@@ -31,8 +31,9 @@ import Foundation
|
||||
/// - Returns: 根据编译环境返回对应的域名
|
||||
@objc static func baseURL() -> String {
|
||||
#if DEBUG
|
||||
// DEV 环境:使用原有的测试域名(不变)
|
||||
return HttpRequestHelper.getHostUrl()
|
||||
// DEV 环境:临时使用硬编码(等 Bridging 修复后再改回 HttpRequestHelper)
|
||||
// TODO: 修复后改为 return HttpRequestHelper.getHostUrl()
|
||||
return getDevBaseURL()
|
||||
#else
|
||||
// RELEASE 环境:使用动态生成的新域名
|
||||
let url = decodeURL(from: releaseEncodedParts)
|
||||
@@ -47,10 +48,26 @@ import Foundation
|
||||
#endif
|
||||
}
|
||||
|
||||
/// 获取 DEV 环境域名(临时方案)
|
||||
/// - Returns: DEV 域名
|
||||
private static func getDevBaseURL() -> String {
|
||||
// 从 UserDefaults 读取(原 HttpRequestHelper 的逻辑)
|
||||
#if DEBUG
|
||||
let isProduction = UserDefaults.standard.string(forKey: "kIsProductionEnvironment")
|
||||
if isProduction == "YES" {
|
||||
return "https://api.epartylive.com" // 正式环境
|
||||
} else {
|
||||
return "https://test-api.yourdomain.com" // 测试环境(请替换为实际测试域名)
|
||||
}
|
||||
#else
|
||||
return "https://api.epartylive.com"
|
||||
#endif
|
||||
}
|
||||
|
||||
/// 备用域名(降级方案)
|
||||
/// - Returns: 原域名(仅在解密失败时使用)
|
||||
@objc static func backupURL() -> String {
|
||||
return HttpRequestHelper.getHostUrl()
|
||||
return getDevBaseURL()
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
|
||||
@@ -38,8 +38,6 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@property (nonatomic, copy) NSString *__nullable chatId;
|
||||
///用户id,推送跳转到聊天页面
|
||||
@property (nonatomic, copy) NSString *__nullable pushChatId;
|
||||
///邀请码,从外面进来会进入注册页面,并自动填写这个邀请码
|
||||
@property(nonatomic,copy) NSString *inviteCode;
|
||||
///表情---
|
||||
@property (nonatomic, copy) NSString *version;
|
||||
@property (nonatomic, copy) NSString *zipMd5;
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
//
|
||||
// YMShareModel.h
|
||||
// YUMI
|
||||
//
|
||||
// Created by YUMI on 2021/11/23.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
typedef NS_ENUM(NSUInteger, ShareType) {
|
||||
///分享房间
|
||||
ShareType_Room = 1,
|
||||
///分享h5
|
||||
ShareType_H5 = 2,
|
||||
///大转盘 目前没用到
|
||||
ShareType_User_Draw = 888,
|
||||
};
|
||||
|
||||
@interface XPShareInfoModel : PIBaseModel
|
||||
///分享的标题
|
||||
@property (nonatomic,copy) NSString *shareTitle;
|
||||
///分享的内容
|
||||
@property (nonatomic,copy) NSString *shareContent;
|
||||
///分享的地址
|
||||
@property (nonatomic,copy) NSString *shareUrl;
|
||||
///分享图片
|
||||
@property (nonatomic,copy) NSString *shareImageUrl;
|
||||
///分享图片
|
||||
@property (nonatomic,copy) UIImage *shareImage;
|
||||
///分享的类型
|
||||
@property (nonatomic,assign) ShareType type;
|
||||
///分享类型,1微信好友,2微信朋友圈,3QQ好友,4QQ空间
|
||||
@property (nonatomic,assign) NSInteger shareType;
|
||||
///分享房间的uid
|
||||
@property (nonatomic,assign) NSInteger roomUid;
|
||||
#pragma mark - 动态分享
|
||||
///被分享动态的那个人
|
||||
@property (nonatomic,copy) NSString *uid;
|
||||
///动态分享
|
||||
@property (nonatomic,copy) NSString *dynamicId;
|
||||
///话题id
|
||||
@property (nonatomic,copy) NSString *worldId;
|
||||
///封面
|
||||
@property (nonatomic,copy) NSString *imageUrl;
|
||||
///名称
|
||||
@property (nonatomic,copy) NSString *nick;
|
||||
///发布者的uid
|
||||
@property (nonatomic,copy) NSString *publishUid;
|
||||
///内容
|
||||
@property (nonatomic,copy) NSString *content;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,12 +0,0 @@
|
||||
//
|
||||
// YMShareModel.m
|
||||
// YUMI
|
||||
//
|
||||
// Created by YUMI on 2021/11/23.
|
||||
//
|
||||
|
||||
#import "XPShareInfoModel.h"
|
||||
|
||||
@implementation XPShareInfoModel
|
||||
|
||||
@end
|
||||
@@ -1,45 +0,0 @@
|
||||
//
|
||||
// YMShareItem.h
|
||||
// YUMI
|
||||
//
|
||||
// Created by YUMI on 2021/11/23.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef enum : NSUInteger {
|
||||
///微信好友
|
||||
XPShareItemTagWeChat = 1,
|
||||
///微信朋友圈
|
||||
XPShareItemTagMoments,
|
||||
///QQ好友
|
||||
XPShareItemTagQQ,
|
||||
///QQ空间
|
||||
XPShareItemTagQQZone,
|
||||
///LIne
|
||||
XPShareItemTagLine,
|
||||
///FaceBook
|
||||
XPShareItemTagFaceBook,
|
||||
///复制链接
|
||||
XPShareItemTagCopyLink,
|
||||
///应用好友
|
||||
XPShareItemTagAppFriends,
|
||||
///保存到相册
|
||||
XPShareItemTagAppSaveAlbum,
|
||||
} XPShareItemTag;
|
||||
|
||||
@interface XPShareItem : NSObject
|
||||
@property(nonatomic,assign) BOOL isShareInvite;
|
||||
@property (nonatomic, copy) NSString *inviteTitle;
|
||||
@property (nonatomic, copy) NSString *title;
|
||||
@property (nonatomic, copy) NSString *imageName;
|
||||
@property (nonatomic, copy) NSString *disableImageName;
|
||||
@property (nonatomic, assign) BOOL disable;
|
||||
@property (nonatomic, assign) XPShareItemTag type;
|
||||
|
||||
+ (instancetype)itemWitTag:(XPShareItemTag)itemTag title:(NSString *)title imageName:(NSString *)imageName disableImageName:(NSString *)disableImageName;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,23 +0,0 @@
|
||||
//
|
||||
// YMShareItem.m
|
||||
// YUMI
|
||||
//
|
||||
// Created by YUMI on 2021/11/23.
|
||||
//
|
||||
|
||||
#import "XPShareItem.h"
|
||||
|
||||
@implementation XPShareItem
|
||||
|
||||
+ (instancetype)itemWitTag:(XPShareItemTag)itemTag title:(NSString *)title imageName:(NSString *)imageName disableImageName:(NSString *)disableImageName {
|
||||
XPShareItem *item = [[self alloc] init];
|
||||
item.type = itemTag;
|
||||
item.title = title;
|
||||
item.imageName = imageName;
|
||||
item.disableImageName = disableImageName;
|
||||
item.disable = NO;
|
||||
return item;
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
@@ -1,16 +0,0 @@
|
||||
//
|
||||
// YMShareItemCell.h
|
||||
// YMRoomMoudle
|
||||
//
|
||||
// Created by YUMI on 2022/9/2.
|
||||
// Copyright © 2023 YUMI. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "XPShareItem.h"
|
||||
|
||||
@interface XPShareItemCell : UICollectionViewCell
|
||||
|
||||
@property (nonatomic, strong) XPShareItem *shareItem;
|
||||
|
||||
@end
|
||||
@@ -1,78 +0,0 @@
|
||||
//
|
||||
// YMShareItemCell.m
|
||||
// YMRoomMoudle
|
||||
//
|
||||
// Created by YUMI on 2022/9/2.
|
||||
// Copyright © 2023 YUMI. All rights reserved.
|
||||
//
|
||||
|
||||
#import "XPShareItemCell.h"
|
||||
#import "DJDKMIMOMColor.h"
|
||||
#import <Masonry/Masonry.h>
|
||||
|
||||
@interface XPShareItemCell()
|
||||
|
||||
@property (nonatomic, strong) UIImageView *iconImageView;
|
||||
@property (nonatomic, strong) UILabel *titleLabel;
|
||||
|
||||
@end
|
||||
|
||||
@implementation XPShareItemCell
|
||||
|
||||
#pragma mark - Life Style
|
||||
- (instancetype)initWithFrame:(CGRect)frame{
|
||||
if (self=[super initWithFrame:frame]) {
|
||||
[self initSubViews];
|
||||
[self initSubViewConstraints];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
#pragma mark - Private Method
|
||||
- (void)initSubViews{
|
||||
[self.contentView addSubview:self.iconImageView];
|
||||
[self.contentView addSubview:self.titleLabel];
|
||||
|
||||
}
|
||||
- (void)initSubViewConstraints{
|
||||
CGFloat wh = 30;
|
||||
[self.iconImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.width.height.equalTo(@(wh));
|
||||
make.top.equalTo(self.contentView);
|
||||
make.centerX.equalTo(self.contentView);
|
||||
}];
|
||||
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.trailing.leading.equalTo(self.contentView).inset(5);
|
||||
make.top.equalTo(self.iconImageView.mas_bottom).offset(10);
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Getters And Setters
|
||||
- (void)setShareItem:(XPShareItem *)shareItem{
|
||||
_shareItem = shareItem;
|
||||
self.userInteractionEnabled = shareItem.disable;
|
||||
if (!shareItem.disable) {
|
||||
self.iconImageView.image = [UIImage imageNamed:shareItem.disableImageName];
|
||||
}else{
|
||||
self.iconImageView.image = [UIImage imageNamed:shareItem.imageName];
|
||||
}
|
||||
self.titleLabel.text = shareItem.title;
|
||||
}
|
||||
|
||||
|
||||
- (UIImageView *)iconImageView{
|
||||
if (!_iconImageView) {
|
||||
_iconImageView = [[UIImageView alloc] init];
|
||||
}
|
||||
return _iconImageView;
|
||||
}
|
||||
- (UILabel *)titleLabel{
|
||||
if (!_titleLabel) {
|
||||
_titleLabel = [[UILabel alloc] init];
|
||||
_titleLabel.textColor = [DJDKMIMOMColor alertMessageColor];
|
||||
_titleLabel.font = [UIFont fontWithName:@"PingFang-SC-Medium" size:12];
|
||||
_titleLabel.numberOfLines = 2;
|
||||
_titleLabel.textAlignment = NSTextAlignmentCenter;
|
||||
}
|
||||
return _titleLabel;
|
||||
}
|
||||
@end
|
||||
@@ -1,34 +0,0 @@
|
||||
//
|
||||
// XCShareView.h
|
||||
// XCRoomMoudle
|
||||
//
|
||||
// Created by KevinWang on 2018/9/2.
|
||||
// Copyright © 2018年 YiZhuan. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "XPShareItem.h"
|
||||
#import "XPShareInfoModel.h"
|
||||
|
||||
@class XPShareView;
|
||||
@protocol XCShareViewDelegate <NSObject>
|
||||
@optional
|
||||
///点击保存图片到相册
|
||||
- (void)shareView:(XPShareView *)shareView savePhoto:(XPShareInfoModel *)shareInfo;
|
||||
///点了取消分享
|
||||
- (void)shareViewDidClickCancel:(XPShareView *)shareView;
|
||||
///分享成功
|
||||
- (void)shareView:(XPShareView *)shareView didSuccess:(XPShareInfoModel *)shareInfo;
|
||||
///分享失败
|
||||
- (void)shareView:(XPShareView *)shareView shareFail:(NSString *)message;
|
||||
@end;
|
||||
|
||||
@interface XPShareView : UIView
|
||||
|
||||
@property (nonatomic, weak) id<XCShareViewDelegate> delegate;
|
||||
@property (nonatomic,assign) BOOL isFromWebVeiw;
|
||||
|
||||
|
||||
- (instancetype)initWithItems:(NSArray<XPShareItem *> *)items itemSize:(CGSize)itemSize shareInfo:(XPShareInfoModel *)shareInfo;
|
||||
|
||||
@end
|
||||
@@ -1,329 +0,0 @@
|
||||
//
|
||||
// XCShareView.m
|
||||
// XCRoomMoudle
|
||||
//
|
||||
// Created by KevinWang on 2018/9/2.
|
||||
// Copyright © 2018年 YiZhuan. All rights reserved.
|
||||
//
|
||||
|
||||
#import "XPShareView.h"
|
||||
///Third
|
||||
#import <Masonry/Masonry.h>
|
||||
#import <ShareSDK/ShareSDK.h>
|
||||
#import <ShareSDKExtension/ShareSDK+Extension.h>
|
||||
#import <FBSDKShareKit/FBSDKShareKit.h>
|
||||
#import "XCCurrentVCStackManager.h"
|
||||
///Tool
|
||||
#import "TTPopup.h"
|
||||
|
||||
///View
|
||||
#import "XPShareItemCell.h"
|
||||
#import "XPMineShareViewController.h"
|
||||
#import "ClientConfig.h"
|
||||
|
||||
@interface XPShareView()<UICollectionViewDataSource,UICollectionViewDelegate,FBSDKSharingDelegate>
|
||||
///取消
|
||||
@property (nonatomic, strong) UIButton *cancleButton;
|
||||
///列表
|
||||
@property (nonatomic, strong) UICollectionView *collectionView;
|
||||
///数据源
|
||||
@property (nonatomic, strong) NSArray<XPShareItem *> *items;
|
||||
///item的大小
|
||||
@property (nonatomic,assign) CGSize itemSize;
|
||||
///分享的内容
|
||||
@property (nonatomic,strong) XPShareInfoModel *shareInfo;
|
||||
@end
|
||||
|
||||
@implementation XPShareView
|
||||
|
||||
#pragma mark - Life Style
|
||||
- (instancetype)initWithItems:(NSArray<XPShareItem *> *)items itemSize:(CGSize)itemSize shareInfo:(XPShareInfoModel *)shareInfo {
|
||||
if (self = [super init]) {
|
||||
for (XPShareItem * item in items) {
|
||||
if (item.type == XPShareItemTagAppFriends || item.type == XPShareItemTagCopyLink) {
|
||||
item.disable = YES;
|
||||
} else {
|
||||
item.disable = [self isInstallClient:[self getSharePlatformType:item.type]];
|
||||
}
|
||||
}
|
||||
self.items = [NSMutableArray arrayWithArray:items];
|
||||
self.itemSize =itemSize;
|
||||
self.shareInfo = shareInfo;
|
||||
[self initSubViews];
|
||||
[self initSubViewConstraints];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Private Method
|
||||
- (void)initSubViews {
|
||||
[self addSubview:self.collectionView];
|
||||
[self addSubview:self.cancleButton];
|
||||
}
|
||||
|
||||
- (void)initSubViewConstraints {
|
||||
|
||||
CGFloat collectionWidth = KScreenWidth - 15 * 2;
|
||||
///一行有几个
|
||||
int numberLine = collectionWidth / self.itemSize.width;
|
||||
int page = self.items.count % numberLine > 0 ? (int)self.items.count / numberLine + 1 : (int)self.items.count / numberLine;
|
||||
CGFloat collectionHeight = page * self.itemSize.height + 20 + (page-1) * 10 + 10;
|
||||
[self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.mas_equalTo(self);
|
||||
make.height.mas_equalTo(collectionHeight);
|
||||
make.leading.trailing.mas_equalTo(self).inset(15);
|
||||
}];
|
||||
|
||||
[self.cancleButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.height.mas_equalTo(45);
|
||||
make.leading.trailing.mas_equalTo(self.collectionView);
|
||||
make.top.mas_equalTo(self.collectionView.mas_bottom).offset(15);
|
||||
}];
|
||||
|
||||
[self mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.width.mas_equalTo(KScreenWidth);
|
||||
make.bottom.mas_equalTo(self.cancleButton.mas_bottom).offset(30);
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
- (BOOL)isInstallClient:(SSDKPlatformType)platform {
|
||||
return [ShareSDK isClientInstalled:platform];
|
||||
}
|
||||
|
||||
- (SSDKPlatformType)getSharePlatformType:(XPShareItemTag)itemTag {
|
||||
SSDKPlatformType type;
|
||||
switch (itemTag) {
|
||||
case XPShareItemTagFaceBook:
|
||||
type = SSDKPlatformTypeFacebook;
|
||||
break;
|
||||
case XPShareItemTagLine:
|
||||
type = SSDKPlatformTypeLine;
|
||||
break;
|
||||
default:
|
||||
type = SSDKPlatformTypeUnknown;
|
||||
break;
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
#pragma mark - UICollectionViewDelegate
|
||||
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
|
||||
return self.items.count;
|
||||
}
|
||||
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
|
||||
XPShareItemCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:NSStringFromClass([XPShareItemCell class]) forIndexPath:indexPath];
|
||||
XPShareItem * item = [self.items xpSafeObjectAtIndex:indexPath.item];
|
||||
item.disable = YES;
|
||||
cell.shareItem = item;
|
||||
return cell;
|
||||
}
|
||||
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{
|
||||
[collectionView deselectItemAtIndexPath:indexPath animated:YES];
|
||||
|
||||
NSMutableDictionary *shareParams = [NSMutableDictionary dictionary];
|
||||
NSString * title = [self.shareInfo shareTitle].length > 0 ? self.shareInfo.shareTitle : @"";
|
||||
NSString * content = self.shareInfo.shareContent.length > 0 ? self.shareInfo.shareContent : @"";
|
||||
NSString * urlString = self.shareInfo.shareUrl.length > 0 ?self.shareInfo.shareUrl : @"";
|
||||
if ([urlString containsString:@"?"]){
|
||||
urlString = [NSString stringWithFormat:@"%@&lang=%@",urlString,[NSBundle uploadLanguageText]];
|
||||
}else{
|
||||
urlString = [NSString stringWithFormat:@"%@?lang=%@",urlString,[NSBundle uploadLanguageText]];
|
||||
}
|
||||
|
||||
NSString *encodedUrl = [urlString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
|
||||
|
||||
XPShareItem * item = [self.items xpSafeObjectAtIndex:indexPath.item];
|
||||
if (item == nil){
|
||||
[TTPopup dismiss];
|
||||
return;
|
||||
};
|
||||
if (item.type == XPShareItemTagAppSaveAlbum){
|
||||
[TTPopup dismiss];
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(shareView:savePhoto:)]){
|
||||
[self.delegate shareView:self savePhoto:self.shareInfo];
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (item.type == XPShareItemTagAppFriends) {
|
||||
[TTPopup dismiss];
|
||||
XPMineShareViewController * shareVC = [[XPMineShareViewController alloc] init];
|
||||
shareVC.shareType = MineShareType_Monents;
|
||||
shareVC.shareInfo = self.shareInfo;
|
||||
[[XCCurrentVCStackManager shareManager].getCurrentVC.navigationController pushViewController:shareVC animated:YES];
|
||||
return;
|
||||
} else if(item.type == XPShareItemTagCopyLink) {
|
||||
NSString * urlString = self.shareInfo.shareUrl;
|
||||
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
|
||||
[pasteboard setString:urlString];
|
||||
[XNDJTDDLoadingTool showSuccessWithMessage:YMLocalizedString(@"XPShareView0")];
|
||||
|
||||
[TTPopup dismiss];
|
||||
return;
|
||||
}
|
||||
if([self isInstallClient:[self getSharePlatformType:item.type]] == NO){
|
||||
[XNDJTDDLoadingTool showErrorWithMessage:YMLocalizedString(@"XPShareView9")];
|
||||
[TTPopup dismiss];
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加类型安全检查,防止NSTaggedPointerString错误
|
||||
if ([self.shareInfo isKindOfClass:[XPShareInfoModel class]] && [item isKindOfClass:[XPShareItem class]]) {
|
||||
self.shareInfo.shareType = item.type;
|
||||
} else {
|
||||
NSLog(@"警告:self.shareInfo不是XPShareInfoModel类型,而是%@类型", NSStringFromClass([self.shareInfo class]));
|
||||
[TTPopup dismiss];
|
||||
return;
|
||||
}
|
||||
|
||||
SSDKPlatformType platformType = SSDKPlatformTypeCopy;
|
||||
|
||||
if (item.type == XPShareItemTagLine) {
|
||||
title = YMLocalizedString(@"XPShareView1");
|
||||
platformType = SSDKPlatformTypeLine;
|
||||
if (![ShareSDK isClientInstalled:platformType]) {
|
||||
[XNDJTDDLoadingTool showErrorWithMessage:YMLocalizedString(@"XPShareView2")];
|
||||
return;
|
||||
}
|
||||
NSString*contentKey= [encodedUrl stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet characterSetWithCharactersInString:@"`#%^{}\"[]|\\<> "].invertedSet];
|
||||
NSString*contentType =@"text";
|
||||
NSString*urlString = [NSString stringWithFormat:@"line://msg/%@/%@",contentType, contentKey];
|
||||
|
||||
[[UIApplication sharedApplication]openURL:[NSURL URLWithString:urlString] options:@{} completionHandler:^(BOOL success) {
|
||||
|
||||
}];
|
||||
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(shareView:didSuccess:)]) {
|
||||
[self.delegate shareView:self didSuccess:self.shareInfo];
|
||||
}
|
||||
return;
|
||||
|
||||
}
|
||||
if(item.type == XPShareItemTagFaceBook){
|
||||
FBSDKShareLinkContent*linkContent = [[FBSDKShareLinkContent alloc]init];
|
||||
urlString = [urlString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLFragmentAllowedCharacterSet]];
|
||||
linkContent.contentURL= [NSURL URLWithString:urlString];
|
||||
linkContent.quote = content;
|
||||
FBSDKShareDialog *shareDialog = [[FBSDKShareDialog alloc]initWithViewController:[XCCurrentVCStackManager shareManager].getCurrentVC content:linkContent delegate:self];
|
||||
// 需要指定模式,否则会调起web分享
|
||||
shareDialog.mode = FBSDKShareDialogModeNative;
|
||||
if (![shareDialog canShow]) {
|
||||
shareDialog.mode = FBSDKShareDialogModeWeb;
|
||||
}
|
||||
|
||||
|
||||
[shareDialog show];
|
||||
return;
|
||||
}
|
||||
|
||||
[ShareSDK share:platformType parameters:shareParams onStateChanged:^(SSDKResponseState state, NSDictionary *userData, SSDKContentEntity *contentEntity, NSError *error) {
|
||||
switch (state) {
|
||||
case SSDKResponseStateSuccess:
|
||||
{
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(shareView:didSuccess:)]) {
|
||||
[self.delegate shareView:self didSuccess:self.shareInfo];
|
||||
}
|
||||
}
|
||||
break;
|
||||
case SSDKResponseStateFail:
|
||||
{
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(shareView:shareFail:)]) {
|
||||
[self.delegate shareView:self shareFail:YMLocalizedString(@"XPShareView5")];
|
||||
}
|
||||
}
|
||||
break;
|
||||
case SSDKResponseStateCancel:
|
||||
{
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(shareView:shareFail:)]) {
|
||||
[self.delegate shareView:self shareFail:YMLocalizedString(@"XPShareView6")];
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}];
|
||||
}
|
||||
#pragma mark - FBSDKSharingDelegate
|
||||
/// Sent to the delegate when sharing completes without error or cancellation.
|
||||
/// @param sharer The sharer that completed.
|
||||
/// @param results The results from the sharer. This may be nil or empty.
|
||||
- (void)sharer:(id <FBSDKSharing> _Nonnull)sharer didCompleteWithResults:(NSDictionary<NSString *, id> * _Nonnull)results{
|
||||
NSString *postId = results[@"postId"];
|
||||
FBSDKShareDialog *dialog = (FBSDKShareDialog *)sharer;
|
||||
if (dialog.mode == FBSDKShareDialogModeBrowser && (postId == nil || [postId isEqualToString:@""])) {
|
||||
// 如果使用webview分享的,但postId是空的,
|
||||
// 这种情况是用户点击了『完成』按钮,并没有真的分享
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(shareView:shareFail:)]) {
|
||||
[self.delegate shareView:self shareFail:YMLocalizedString(@"XPShareView6")];
|
||||
}
|
||||
} else {
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(shareView:didSuccess:)]) {
|
||||
[self.delegate shareView:self didSuccess:self.shareInfo];
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Sent to the delegate when the sharer encounters an error.
|
||||
/// @param sharer The sharer that completed.
|
||||
/// @param error The error.
|
||||
- (void)sharer:(id <FBSDKSharing> _Nonnull)sharer didFailWithError:(NSError * _Nonnull)error{
|
||||
FBSDKShareDialog *dialog = (FBSDKShareDialog *)sharer;
|
||||
if (error == nil && dialog.mode == FBSDKShareDialogModeNative) {
|
||||
// 如果使用原生登录失败,但error为空,那是因为用户没有安装Facebook app
|
||||
// 重设dialog的mode,再次弹出对话框
|
||||
dialog.mode = FBSDKShareDialogModeBrowser;
|
||||
[dialog show];
|
||||
} else {
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(shareView:shareFail:)]) {
|
||||
[self.delegate shareView:self shareFail:YMLocalizedString(@"XPShareView5")];
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Sent to the delegate when the sharer is cancelled.
|
||||
/// @param sharer The sharer that completed.
|
||||
- (void)sharerDidCancel:(id <FBSDKSharing> _Nonnull)sharer{
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(shareView:shareFail:)]) {
|
||||
[self.delegate shareView:self shareFail:YMLocalizedString(@"XPShareView6")];
|
||||
}
|
||||
}
|
||||
#pragma mark - Event Response
|
||||
- (void)cancleButtonDidClck:(UIButton *)button{
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(shareViewDidClickCancel:)]) {
|
||||
[self.delegate shareViewDidClickCancel:self];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Getters And Setters
|
||||
- (UICollectionView *)collectionView{
|
||||
if (!_collectionView) {
|
||||
MSBaseRTLFlowLayout *layout = [[MSBaseRTLFlowLayout alloc] init];
|
||||
layout.itemSize = self.itemSize;
|
||||
layout.minimumInteritemSpacing = 0;
|
||||
layout.minimumLineSpacing = 10;
|
||||
layout.sectionInset = UIEdgeInsetsMake(20, 0, 10, 0);
|
||||
_collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
|
||||
_collectionView.backgroundColor = [UIColor whiteColor];
|
||||
_collectionView.dataSource = self;
|
||||
_collectionView.delegate = self;
|
||||
_collectionView.layer.masksToBounds = YES;
|
||||
_collectionView.layer.cornerRadius = 15;
|
||||
[_collectionView registerClass:[XPShareItemCell class] forCellWithReuseIdentifier:NSStringFromClass([XPShareItemCell class])];
|
||||
}
|
||||
return _collectionView;
|
||||
}
|
||||
|
||||
- (UIButton *)cancleButton{
|
||||
if (!_cancleButton) {
|
||||
_cancleButton = [[UIButton alloc] init];
|
||||
[_cancleButton setBackgroundColor:[UIColor whiteColor]];
|
||||
[_cancleButton setTitle:YMLocalizedString(@"XPShareView7") forState:UIControlStateNormal];
|
||||
_cancleButton.titleLabel.font = [UIFont fontWithName:@"PingFang-SC-Medium" size:15];
|
||||
_cancleButton.layer.masksToBounds = YES;
|
||||
_cancleButton.layer.cornerRadius = 45/2;
|
||||
[_cancleButton setTitleColor:[DJDKMIMOMColor textThirdColor] forState:UIControlStateNormal];
|
||||
[_cancleButton addTarget:self action:@selector(cancleButtonDidClck:) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
return _cancleButton;
|
||||
}
|
||||
|
||||
@end
|
||||
26
YuMi/E-P/Common/EPClientAPIBridge.h
Normal file
26
YuMi/E-P/Common/EPClientAPIBridge.h
Normal file
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// EPClientAPIBridge.h
|
||||
// YuMi
|
||||
//
|
||||
// Deprecated: replaced by Swift EPConfigAPI
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// Bridge to wrap existing Objective-C APIs for Swift consumers
|
||||
__attribute__((deprecated("Use EPConfigAPI (Swift) instead")))
|
||||
@interface EPClientAPIBridge : NSObject
|
||||
|
||||
/// Call Api.clientInitConfig and forward raw dictionary and status
|
||||
+ (void)clientInit:(void(^)(NSDictionary * _Nullable data, NSInteger code, NSString * _Nullable msg))completion;
|
||||
|
||||
/// Call ClientConfig.clientConfig; returns code 200 on success (no payload)
|
||||
+ (void)clientConfig:(void(^)(NSDictionary * _Nullable data, NSInteger code, NSString * _Nullable msg))completion;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
|
||||
36
YuMi/E-P/Common/EPClientAPIBridge.m
Normal file
36
YuMi/E-P/Common/EPClientAPIBridge.m
Normal file
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// EPClientAPIBridge.m
|
||||
// YuMi
|
||||
//
|
||||
// Objective-C to Swift bridge for client init/config APIs
|
||||
//
|
||||
|
||||
#import "EPClientAPIBridge.h"
|
||||
#import "Api+Main.h"
|
||||
#import "ClientConfig.h"
|
||||
#import "BaseModel.h"
|
||||
|
||||
@implementation EPClientAPIBridge
|
||||
|
||||
+ (void)clientInit:(void(^)(NSDictionary * _Nullable data, NSInteger code, NSString * _Nullable msg))completion {
|
||||
if (!completion) { return; }
|
||||
[Api clientInitConfig:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) {
|
||||
NSDictionary *payload = nil;
|
||||
if (code == 200 && data.data && [data.data isKindOfClass:[NSDictionary class]]) {
|
||||
payload = (NSDictionary *)data.data;
|
||||
}
|
||||
completion(payload, code, msg);
|
||||
}];
|
||||
}
|
||||
|
||||
+ (void)clientConfig:(void(^)(NSDictionary * _Nullable data, NSInteger code, NSString * _Nullable msg))completion {
|
||||
if (!completion) { return; }
|
||||
// ClientConfig.clientConfig only has a finish block with no parameters; treat success as code 200
|
||||
[[ClientConfig shareConfig] clientConfig:^{
|
||||
completion(nil, 200, nil);
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
37
YuMi/E-P/Common/EPConfigAPI.swift
Normal file
37
YuMi/E-P/Common/EPConfigAPI.swift
Normal file
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// EPConfigAPI.swift
|
||||
// YuMi
|
||||
//
|
||||
// Thin Swift wrapper aligning EP module naming, for client/init & client/config
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc final class EPConfigAPI: NSObject {
|
||||
|
||||
/// GET client/init — returns payload dictionary when code == 200
|
||||
@objc static func clientInit(
|
||||
completion: @escaping (_ data: [String: Any]?, _ code: Int, _ msg: String?) -> Void
|
||||
) {
|
||||
Api.clientInitConfig { baseModel, code, msg in
|
||||
var dict: [String: Any]? = nil
|
||||
if code == 200, let payload = baseModel?.data as? [String: Any] {
|
||||
dict = payload
|
||||
}
|
||||
completion(dict, Int(code), msg)
|
||||
}
|
||||
}
|
||||
|
||||
/// GET client/config — treat success as code 200 (no payload)
|
||||
@objc static func clientConfig(
|
||||
completion: @escaping (_ data: [String: Any]?, _ code: Int, _ msg: String?) -> Void
|
||||
) {
|
||||
// ClientConfig has + (instancetype)shareConfig; bridged to Swift as .share()
|
||||
// If the symbol differs, adjust to your Swift name (e.g., shareConfig()).
|
||||
ClientConfig.share().clientConfig {
|
||||
completion(nil, 200, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
133
YuMi/E-P/Common/EPConfigManager.swift
Normal file
133
YuMi/E-P/Common/EPConfigManager.swift
Normal file
@@ -0,0 +1,133 @@
|
||||
//
|
||||
// EPConfigManager.swift
|
||||
// YuMi
|
||||
//
|
||||
// Cold boot configuration manager for client/init and client/config flows
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc final class EPConfigManager: NSObject {
|
||||
@objc static let shared = EPConfigManager()
|
||||
|
||||
// MARK: - State
|
||||
@objc private(set) var isInitReady: Bool = false
|
||||
@objc private(set) var isConfigReady: Bool = false
|
||||
@objc private(set) var isUsingPersistedInit: Bool = false
|
||||
|
||||
// 原始数据(向后兼容)
|
||||
@objc private(set) var initModelRaw: [String: Any]? = nil
|
||||
@objc private(set) var configModelRaw: [String: Any]? = nil
|
||||
|
||||
// 强类型模型
|
||||
@objc private(set) var clientDataModel: ClientDataModel? = nil
|
||||
|
||||
private var hasStarted = false
|
||||
|
||||
// 回调闭包
|
||||
private var successCallback: (() -> Void)?
|
||||
private var failureCallback: ((String) -> Void)?
|
||||
|
||||
// MARK: - Public API
|
||||
@objc(startColdBootWithOnSuccess:onFailure:)
|
||||
func startColdBoot(
|
||||
onSuccess: @escaping () -> Void,
|
||||
onFailure: @escaping (String) -> Void
|
||||
) {
|
||||
guard !hasStarted else {
|
||||
// 如果已经启动过,根据当前状态直接回调
|
||||
if isInitReady && isConfigReady {
|
||||
onSuccess()
|
||||
} else if !isInitReady {
|
||||
onFailure("配置初始化失败")
|
||||
}
|
||||
return
|
||||
}
|
||||
hasStarted = true
|
||||
|
||||
// 保存回调
|
||||
self.successCallback = onSuccess
|
||||
self.failureCallback = onFailure
|
||||
|
||||
runClientInitWithRetry(maxRetry: 5, interval: 1.0)
|
||||
}
|
||||
|
||||
// MARK: - Flow
|
||||
private func runClientInitWithRetry(maxRetry: Int, interval: TimeInterval) {
|
||||
attemptClientInit(remaining: maxRetry, interval: interval)
|
||||
}
|
||||
|
||||
private func attemptClientInit(remaining: Int, interval: TimeInterval) {
|
||||
EPConfigAPI.clientInit { [weak self] data, code, msg in
|
||||
guard let self = self else { return }
|
||||
if code == 200, let dict = data {
|
||||
self.onInitSuccess(dict)
|
||||
self.runClientConfig()
|
||||
} else if remaining > 0 {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + interval) {
|
||||
self.attemptClientInit(remaining: remaining - 1, interval: interval)
|
||||
}
|
||||
} else {
|
||||
self.onInitExhausted()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func onInitSuccess(_ dict: [String: Any]) {
|
||||
// 1. 转换为强类型模型
|
||||
let model = ClientDataModel.model(withJSON: dict)
|
||||
self.clientDataModel = model
|
||||
|
||||
// 2. 更新 ClientConfig(保持现有代码兼容)
|
||||
ClientConfig.share().configInfo = model
|
||||
|
||||
// 3. 持久化原始字典(避免模型变更时数据失效)
|
||||
_ = EPConfigStorage.saveInit(dict)
|
||||
|
||||
// 4. 更新状态
|
||||
isUsingPersistedInit = false
|
||||
initModelRaw = dict
|
||||
isInitReady = true
|
||||
|
||||
// 5. 继续执行 client/config
|
||||
runClientConfig()
|
||||
}
|
||||
|
||||
private func onInitExhausted() {
|
||||
if let persistedDict = EPConfigStorage.loadInit() as? [String: Any] {
|
||||
// 使用持久化数据
|
||||
let model = ClientDataModel.model(withJSON: persistedDict)
|
||||
self.clientDataModel = model
|
||||
ClientConfig.share().configInfo = model
|
||||
|
||||
isUsingPersistedInit = true
|
||||
initModelRaw = persistedDict
|
||||
isInitReady = true
|
||||
|
||||
// 尝试获取最新配置
|
||||
runClientConfig()
|
||||
} else {
|
||||
// 无持久化数据,调用失败回调
|
||||
failureCallback?("网络异常,请稍后重新启动应用")
|
||||
}
|
||||
}
|
||||
|
||||
private func runClientConfig() {
|
||||
EPConfigAPI.clientConfig { [weak self] data, code, msg in
|
||||
guard let self = self else { return }
|
||||
if code == 200 {
|
||||
// client/config 成功,标记完成并调用成功回调
|
||||
self.isConfigReady = true
|
||||
self.successCallback?()
|
||||
} else {
|
||||
// client/config 失败,但 init 已成功,仍可继续
|
||||
self.isConfigReady = true
|
||||
self.successCallback?()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移除 Notification 扩展,不再需要
|
||||
|
||||
|
||||
27
YuMi/E-P/Common/EPConfigStorage.h
Normal file
27
YuMi/E-P/Common/EPConfigStorage.h
Normal file
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// EPConfigStorage.h
|
||||
// YuMi
|
||||
//
|
||||
// Lightweight persistence for client/init data
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface EPConfigStorage : NSObject
|
||||
|
||||
/// Save init payload dictionary to disk (Application Support)
|
||||
+ (BOOL)saveInit:(NSDictionary *)dict;
|
||||
|
||||
/// Load init payload dictionary from disk; returns nil if missing/invalid
|
||||
+ (NSDictionary * _Nullable)loadInit;
|
||||
|
||||
/// Remove persisted init payload
|
||||
+ (BOOL)clearInit;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
|
||||
62
YuMi/E-P/Common/EPConfigStorage.m
Normal file
62
YuMi/E-P/Common/EPConfigStorage.m
Normal file
@@ -0,0 +1,62 @@
|
||||
//
|
||||
// EPConfigStorage.m
|
||||
// YuMi
|
||||
//
|
||||
// Lightweight persistence for client/init data
|
||||
//
|
||||
|
||||
#import "EPConfigStorage.h"
|
||||
|
||||
@implementation EPConfigStorage
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
+ (BOOL)saveInit:(NSDictionary *)dict {
|
||||
if (![dict isKindOfClass:[NSDictionary class]]) { return NO; }
|
||||
NSMutableDictionary *wrapped = [dict mutableCopy];
|
||||
wrapped[@"_version"] = @1;
|
||||
wrapped[@"_timestamp"] = @((long long)([[NSDate date] timeIntervalSince1970]));
|
||||
NSData *data = [NSJSONSerialization dataWithJSONObject:wrapped options:0 error:nil];
|
||||
if (!data) { return NO; }
|
||||
NSString *path = [self initPath];
|
||||
NSError *error = nil;
|
||||
NSFileManager *fm = [NSFileManager defaultManager];
|
||||
NSString *dir = [path stringByDeletingLastPathComponent];
|
||||
if (![fm fileExistsAtPath:dir]) {
|
||||
[fm createDirectoryAtPath:dir withIntermediateDirectories:YES attributes:nil error:&error];
|
||||
if (error) { return NO; }
|
||||
}
|
||||
return [data writeToFile:path atomically:YES];
|
||||
}
|
||||
|
||||
+ (NSDictionary * _Nullable)loadInit {
|
||||
NSString *path = [self initPath];
|
||||
NSData *data = [NSData dataWithContentsOfFile:path];
|
||||
if (!data) { return nil; }
|
||||
id obj = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
|
||||
if (![obj isKindOfClass:[NSDictionary class]]) { return nil; }
|
||||
return (NSDictionary *)obj;
|
||||
}
|
||||
|
||||
+ (BOOL)clearInit {
|
||||
NSString *path = [self initPath];
|
||||
NSFileManager *fm = [NSFileManager defaultManager];
|
||||
if (![fm fileExistsAtPath:path]) { return YES; }
|
||||
NSError *error = nil;
|
||||
[fm removeItemAtPath:path error:&error];
|
||||
return (error == nil);
|
||||
}
|
||||
|
||||
#pragma mark - Helpers
|
||||
|
||||
+ (NSString *)initPath {
|
||||
NSArray<NSURL *> *urls = [[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask];
|
||||
NSURL *dirURL = urls.firstObject;
|
||||
if (!dirURL) { dirURL = [NSURL fileURLWithPath:[NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) firstObject]]; }
|
||||
NSURL *fileURL = [dirURL URLByAppendingPathComponent:@"ep_config_init.json"];
|
||||
return fileURL.path;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
163
YuMi/E-P/Common/EPImageUploader.swift
Normal file
163
YuMi/E-P/Common/EPImageUploader.swift
Normal file
@@ -0,0 +1,163 @@
|
||||
//
|
||||
// EPImageUploader.swift
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-10-11.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Foundation
|
||||
|
||||
/// 图片批量上传工具(纯 Swift 内部类,直接使用 QCloudCOSXML SDK)
|
||||
/// 不对外暴露,由 EPSDKManager 内部调用
|
||||
class EPImageUploader {
|
||||
|
||||
init() {}
|
||||
|
||||
/// 批量上传图片(内部方法)
|
||||
/// - Parameters:
|
||||
/// - images: 要上传的图片数组
|
||||
/// - bucket: QCloud bucket 名称
|
||||
/// - customDomain: 自定义域名
|
||||
/// - progress: 进度回调 (已上传数, 总数)
|
||||
/// - success: 成功回调
|
||||
/// - failure: 失败回调
|
||||
func performBatchUpload(
|
||||
_ images: [UIImage],
|
||||
bucket: String,
|
||||
customDomain: String,
|
||||
progress: @escaping (Int, Int) -> Void,
|
||||
success: @escaping ([[String: Any]]) -> Void,
|
||||
failure: @escaping (String) -> Void
|
||||
) {
|
||||
let total = images.count
|
||||
let queue = DispatchQueue(label: "com.yumi.imageupload", attributes: .concurrent)
|
||||
let semaphore = DispatchSemaphore(value: 3) // 最多同时上传 3 张
|
||||
var uploadedCount = 0
|
||||
var resultList: [[String: Any]] = []
|
||||
var hasError = false
|
||||
let lock = NSLock()
|
||||
|
||||
for (_, image) in images.enumerated() {
|
||||
queue.async {
|
||||
semaphore.wait()
|
||||
|
||||
// 检查是否已经失败
|
||||
lock.lock()
|
||||
if hasError {
|
||||
lock.unlock()
|
||||
semaphore.signal()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
|
||||
// 压缩图片
|
||||
guard let imageData = image.jpegData(compressionQuality: 0.5) else {
|
||||
lock.lock()
|
||||
hasError = true
|
||||
lock.unlock()
|
||||
semaphore.signal()
|
||||
DispatchQueue.main.async {
|
||||
failure(YMLocalizedString("error.image_compress_failed"))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 获取图片格式
|
||||
let format = UIImage.getImageType(withImageData: imageData) ?? "jpeg"
|
||||
|
||||
// 生成文件名
|
||||
let uuid = NSString.createUUID()
|
||||
let fileName = "image/\(uuid).\(format)"
|
||||
|
||||
// 直接使用 QCloud SDK 上传
|
||||
let request = QCloudCOSXMLUploadObjectRequest<AnyObject>()
|
||||
request.bucket = bucket
|
||||
request.object = fileName
|
||||
request.body = imageData as NSData
|
||||
|
||||
// 监听上传进度(可选)
|
||||
request.sendProcessBlock = { bytesSent, totalBytesSent, totalBytesExpectedToSend in
|
||||
// 单个文件的上传进度(当前不使用)
|
||||
}
|
||||
|
||||
// 监听上传结果
|
||||
request.finishBlock = { [weak self] result, error in
|
||||
guard let self = self else {
|
||||
semaphore.signal()
|
||||
return
|
||||
}
|
||||
|
||||
if let error = error {
|
||||
// 上传失败
|
||||
lock.lock()
|
||||
if !hasError {
|
||||
hasError = true
|
||||
lock.unlock()
|
||||
semaphore.signal()
|
||||
DispatchQueue.main.async {
|
||||
failure(error.localizedDescription)
|
||||
}
|
||||
} else {
|
||||
lock.unlock()
|
||||
semaphore.signal()
|
||||
}
|
||||
} else if let result = result as? QCloudUploadObjectResult {
|
||||
// 上传成功
|
||||
lock.lock()
|
||||
if !hasError {
|
||||
uploadedCount += 1
|
||||
|
||||
// 解析上传 URL(参考 UploadFile.m line 217-223)
|
||||
let uploadedURL = self.parseUploadURL(result.location, customDomain: customDomain)
|
||||
|
||||
let imageInfo: [String: Any] = [
|
||||
"resUrl": uploadedURL,
|
||||
"width": image.size.width,
|
||||
"height": image.size.height,
|
||||
"format": format
|
||||
]
|
||||
resultList.append(imageInfo)
|
||||
|
||||
let currentUploaded = uploadedCount
|
||||
lock.unlock()
|
||||
|
||||
// 进度回调
|
||||
DispatchQueue.main.async {
|
||||
progress(currentUploaded, total)
|
||||
}
|
||||
|
||||
// 全部完成
|
||||
if currentUploaded == total {
|
||||
DispatchQueue.main.async {
|
||||
success(resultList)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
semaphore.signal()
|
||||
} else {
|
||||
semaphore.signal()
|
||||
}
|
||||
}
|
||||
|
||||
// 执行上传
|
||||
QCloudCOSTransferMangerService.defaultCOSTransferManager().uploadObject(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析上传返回的 URL(参考 UploadFile.m line 217-223)
|
||||
/// - Parameters:
|
||||
/// - location: QCloud 返回的原始 URL
|
||||
/// - customDomain: 自定义域名
|
||||
/// - Returns: 解析后的 URL
|
||||
private func parseUploadURL(_ location: String, customDomain: String) -> String {
|
||||
let components = location.components(separatedBy: ".com/")
|
||||
if components.count == 2 {
|
||||
return "\(customDomain)/\(components[1])"
|
||||
}
|
||||
return location
|
||||
}
|
||||
}
|
||||
92
YuMi/E-P/Common/EPProgressHUD.swift
Normal file
92
YuMi/E-P/Common/EPProgressHUD.swift
Normal file
@@ -0,0 +1,92 @@
|
||||
//
|
||||
// EPProgressHUD.swift
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-10-11.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Foundation
|
||||
|
||||
/// 带进度的 Loading 组件(基于 MBProgressHUD)
|
||||
@objc class EPProgressHUD: NSObject {
|
||||
|
||||
private static var currentHUD: MBProgressHUD?
|
||||
|
||||
/// 获取当前活跃的 window(兼容 iOS 13+)
|
||||
private static var keyWindow: UIWindow? {
|
||||
if #available(iOS 13.0, *) {
|
||||
return UIApplication.shared.connectedScenes
|
||||
.compactMap { $0 as? UIWindowScene }
|
||||
.flatMap { $0.windows }
|
||||
.first { $0.isKeyWindow }
|
||||
} else {
|
||||
return UIApplication.shared.keyWindow
|
||||
}
|
||||
}
|
||||
|
||||
/// 显示上传进度
|
||||
/// - Parameters:
|
||||
/// - uploaded: 已上传数量
|
||||
/// - total: 总数量
|
||||
@objc static func showProgress(_ uploaded: Int, total: Int) {
|
||||
DispatchQueue.main.async {
|
||||
guard let window = keyWindow else { return }
|
||||
|
||||
if let hud = currentHUD {
|
||||
// 更新现有 HUD
|
||||
hud.label.text = String(format: YMLocalizedString("upload.progress_format"), uploaded, total)
|
||||
hud.progress = Float(uploaded) / Float(total)
|
||||
} else {
|
||||
// 创建新 HUD
|
||||
let hud = MBProgressHUD.showAdded(to: window, animated: true)
|
||||
hud.mode = .determinateHorizontalBar
|
||||
hud.label.text = String(format: YMLocalizedString("upload.progress_format"), uploaded, total)
|
||||
hud.progress = Float(uploaded) / Float(total)
|
||||
hud.removeFromSuperViewOnHide = true
|
||||
currentHUD = hud
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 显示错误提示
|
||||
/// - Parameter message: 错误信息
|
||||
@objc static func showError(_ message: String) {
|
||||
DispatchQueue.main.async {
|
||||
guard let window = keyWindow else { return }
|
||||
|
||||
let hud = MBProgressHUD.showAdded(to: window, animated: true)
|
||||
hud.mode = .text
|
||||
hud.label.text = message
|
||||
hud.label.numberOfLines = 0
|
||||
hud.removeFromSuperViewOnHide = true
|
||||
hud.hide(animated: true, afterDelay: 2.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// 显示成功提示
|
||||
/// - Parameter message: 成功信息
|
||||
@objc static func showSuccess(_ message: String) {
|
||||
DispatchQueue.main.async {
|
||||
guard let window = keyWindow else { return }
|
||||
|
||||
let hud = MBProgressHUD.showAdded(to: window, animated: true)
|
||||
hud.mode = .text
|
||||
hud.label.text = message
|
||||
hud.label.numberOfLines = 0
|
||||
hud.removeFromSuperViewOnHide = true
|
||||
hud.hide(animated: true, afterDelay: 2.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// 关闭 HUD
|
||||
@objc static func dismiss() {
|
||||
DispatchQueue.main.async {
|
||||
guard let hud = currentHUD else { return }
|
||||
|
||||
hud.hide(animated: true)
|
||||
currentHUD = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
56
YuMi/E-P/Common/EPQCloudConfig.swift
Normal file
56
YuMi/E-P/Common/EPQCloudConfig.swift
Normal file
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// EPQCloudConfig.swift
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-10-11.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// QCloud 配置数据模型(对应 UploadFileModel)
|
||||
struct EPQCloudConfig {
|
||||
let secretId: String
|
||||
let secretKey: String
|
||||
let sessionToken: String
|
||||
let bucket: String
|
||||
let region: String
|
||||
let customDomain: String
|
||||
let startTime: Int64
|
||||
let expireTime: Int64
|
||||
let appId: String
|
||||
let accelerate: Int
|
||||
|
||||
/// 从 API 返回的 dictionary 初始化
|
||||
/// API: GET tencent/cos/getToken
|
||||
init?(dictionary: [String: Any]) {
|
||||
// 必填字段检查
|
||||
guard let secretId = dictionary["secretId"] as? String,
|
||||
let secretKey = dictionary["secretKey"] as? String,
|
||||
let sessionToken = dictionary["sessionToken"] as? String,
|
||||
let bucket = dictionary["bucket"] as? String,
|
||||
let region = dictionary["region"] as? String,
|
||||
let customDomain = dictionary["customDomain"] as? String,
|
||||
let appId = dictionary["appId"] as? String else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.secretId = secretId
|
||||
self.secretKey = secretKey
|
||||
self.sessionToken = sessionToken
|
||||
self.bucket = bucket
|
||||
self.region = region
|
||||
self.customDomain = customDomain
|
||||
self.appId = appId
|
||||
|
||||
// 可选字段使用默认值
|
||||
self.startTime = (dictionary["startTime"] as? Int64) ?? 0
|
||||
self.expireTime = (dictionary["expireTime"] as? Int64) ?? 0
|
||||
self.accelerate = (dictionary["accelerate"] as? Int) ?? 0
|
||||
}
|
||||
|
||||
/// 检查配置是否过期
|
||||
var isExpired: Bool {
|
||||
return Date().timeIntervalSince1970 > Double(expireTime)
|
||||
}
|
||||
}
|
||||
|
||||
27
YuMi/E-P/Common/EPSDKManager+NIM.swift
Normal file
27
YuMi/E-P/Common/EPSDKManager+NIM.swift
Normal file
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// EPSDKManager+NIM.swift
|
||||
// YuMi
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc extension EPSDKManager {
|
||||
/// 初始化 NIMSDK(从 ClientConfig 获取 nimKey)
|
||||
@objc func initializeNIMSDK(completion: ((NSError?) -> Void)? = nil) {
|
||||
EPNIMManager.shared().initialize { error in
|
||||
completion?(error as NSError?)
|
||||
}
|
||||
}
|
||||
|
||||
/// 上传 APNS token 到 NIM
|
||||
@objc func updateNIMApnsToken(_ deviceToken: Data) {
|
||||
EPNIMManager.shared().updateApnsToken(deviceToken)
|
||||
}
|
||||
|
||||
/// 获取 NIM 未读数
|
||||
@objc func nimUnreadCount() -> Int {
|
||||
return Int(EPNIMManager.shared().allUnreadCount())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
253
YuMi/E-P/Common/EPSDKManager.swift
Normal file
253
YuMi/E-P/Common/EPSDKManager.swift
Normal file
@@ -0,0 +1,253 @@
|
||||
//
|
||||
// EPSDKManager.swift
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-10-11.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 第三方 SDK 统一管理器(单例)
|
||||
/// 统一入口:对外提供所有 SDK 能力
|
||||
/// 内部管理:QCloud 初始化、配置、上传等
|
||||
@objc class EPSDKManager: NSObject, QCloudSignatureProvider, QCloudCredentailFenceQueueDelegate {
|
||||
|
||||
// MARK: - Singleton
|
||||
|
||||
@objc static let shared = EPSDKManager()
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// QCloud 配置缓存
|
||||
private var qcloudConfig: EPQCloudConfig?
|
||||
|
||||
// QCloud 初始化状态
|
||||
private var isQCloudInitializing = false
|
||||
|
||||
// QCloud 初始化回调队列
|
||||
private var qcloudInitCallbacks: [(Bool, String?) -> Void] = []
|
||||
|
||||
// QCloud 凭证队列
|
||||
private var credentialFenceQueue: QCloudCredentailFenceQueue?
|
||||
|
||||
// 线程安全锁
|
||||
private let lock = NSLock()
|
||||
|
||||
// 内部图片上传器
|
||||
private let uploader = EPImageUploader()
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
private override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
// MARK: - Public API (对外统一入口)
|
||||
|
||||
/// 批量上传图片(统一入口)
|
||||
/// - Parameters:
|
||||
/// - images: 要上传的图片数组
|
||||
/// - progress: 进度回调 (已上传数, 总数)
|
||||
/// - success: 成功回调,返回图片信息数组
|
||||
/// - failure: 失败回调
|
||||
@objc func uploadImages(
|
||||
_ images: [UIImage],
|
||||
progress: @escaping (Int, Int) -> Void,
|
||||
success: @escaping ([[String: Any]]) -> Void,
|
||||
failure: @escaping (String) -> Void
|
||||
) {
|
||||
guard !images.isEmpty else {
|
||||
success([])
|
||||
return
|
||||
}
|
||||
|
||||
// 确保 QCloud 已就绪
|
||||
ensureQCloudReady { [weak self] isReady, errorMsg in
|
||||
guard let self = self, isReady else {
|
||||
DispatchQueue.main.async {
|
||||
failure(errorMsg ?? YMLocalizedString("error.qcloud_init_failed"))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 委托给内部 uploader 执行
|
||||
self.uploader.performBatchUpload(
|
||||
images,
|
||||
bucket: self.qcloudConfig?.bucket ?? "",
|
||||
customDomain: self.qcloudConfig?.customDomain ?? "",
|
||||
progress: progress,
|
||||
success: success,
|
||||
failure: failure
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查 QCloud 是否已就绪
|
||||
/// - Returns: true 表示已初始化且未过期
|
||||
@objc func isQCloudReady() -> Bool {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
|
||||
guard let config = qcloudConfig else {
|
||||
return false
|
||||
}
|
||||
return !config.isExpired
|
||||
}
|
||||
|
||||
// MARK: - Internal Methods
|
||||
|
||||
/// 确保 QCloud 已就绪(自动初始化)
|
||||
private func ensureQCloudReady(completion: @escaping (Bool, String?) -> Void) {
|
||||
if isQCloudReady() {
|
||||
completion(true, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 未初始化或已过期,重新初始化
|
||||
initializeQCloud(completion: completion)
|
||||
}
|
||||
|
||||
/// 初始化 QCloud(获取 Token 并配置 SDK)
|
||||
private func initializeQCloud(completion: @escaping (Bool, String?) -> Void) {
|
||||
lock.lock()
|
||||
|
||||
// 如果正在初始化,加入回调队列
|
||||
if isQCloudInitializing {
|
||||
qcloudInitCallbacks.append(completion)
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// 如果已初始化且未过期,直接返回
|
||||
if let config = qcloudConfig, !config.isExpired {
|
||||
lock.unlock()
|
||||
completion(true, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 开始初始化
|
||||
isQCloudInitializing = true
|
||||
qcloudInitCallbacks.append(completion)
|
||||
lock.unlock()
|
||||
|
||||
// 调用 API 获取 QCloud Token
|
||||
// API: GET tencent/cos/getToken
|
||||
Api.getQCloudInfo { [weak self] (data, code, msg) in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.lock.lock()
|
||||
|
||||
if code == 200,
|
||||
let dict = data?.data as? [String: Any],
|
||||
let config = EPQCloudConfig(dictionary: dict) {
|
||||
|
||||
// 保存配置
|
||||
self.qcloudConfig = config
|
||||
|
||||
// 配置 QCloud SDK
|
||||
self.configureQCloudSDK(with: config)
|
||||
|
||||
// 初始化完成
|
||||
self.isQCloudInitializing = false
|
||||
let callbacks = self.qcloudInitCallbacks
|
||||
self.qcloudInitCallbacks.removeAll()
|
||||
self.lock.unlock()
|
||||
|
||||
// 短暂延迟确保 SDK 配置完成
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
||||
callbacks.forEach { $0(true, nil) }
|
||||
}
|
||||
} else {
|
||||
// 初始化失败
|
||||
self.isQCloudInitializing = false
|
||||
let callbacks = self.qcloudInitCallbacks
|
||||
self.qcloudInitCallbacks.removeAll()
|
||||
self.lock.unlock()
|
||||
|
||||
let errorMsg = msg ?? YMLocalizedString("error.qcloud_config_failed")
|
||||
DispatchQueue.main.async {
|
||||
callbacks.forEach { $0(false, errorMsg) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 配置 QCloud SDK(参考 UploadFile.m line 42-64)
|
||||
private func configureQCloudSDK(with config: EPQCloudConfig) {
|
||||
let configuration = QCloudServiceConfiguration()
|
||||
configuration.appID = config.appId
|
||||
|
||||
let endpoint = QCloudCOSXMLEndPoint()
|
||||
endpoint.regionName = config.region
|
||||
endpoint.useHTTPS = true
|
||||
|
||||
// 全球加速(参考 UploadFile.m line 56-59)
|
||||
if config.accelerate == 1 {
|
||||
endpoint.suffix = "cos.accelerate.myqcloud.com"
|
||||
}
|
||||
|
||||
configuration.endpoint = endpoint
|
||||
configuration.signatureProvider = self
|
||||
|
||||
// 注册 COS 服务
|
||||
QCloudCOSXMLService.registerDefaultCOSXML(with: configuration)
|
||||
QCloudCOSTransferMangerService.registerDefaultCOSTransferManger(with: configuration)
|
||||
|
||||
// 初始化凭证队列
|
||||
credentialFenceQueue = QCloudCredentailFenceQueue()
|
||||
credentialFenceQueue?.delegate = self
|
||||
}
|
||||
|
||||
// MARK: - QCloudSignatureProvider Protocol
|
||||
|
||||
/// 提供签名(参考 UploadFile.m line 67-104)
|
||||
func signature(
|
||||
with fields: QCloudSignatureFields,
|
||||
request: QCloudBizHTTPRequest,
|
||||
urlRequest: NSMutableURLRequest,
|
||||
compelete: @escaping QCloudHTTPAuthentationContinueBlock
|
||||
) {
|
||||
guard let config = qcloudConfig else {
|
||||
let error = NSError(domain: "com.yumi.qcloud", code: -1,
|
||||
userInfo: [NSLocalizedDescriptionKey: YMLocalizedString("error.qcloud_config_not_initialized")])
|
||||
compelete(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
let credential = QCloudCredential()
|
||||
credential.secretID = config.secretId
|
||||
credential.secretKey = config.secretKey
|
||||
credential.token = config.sessionToken
|
||||
credential.startDate = Date(timeIntervalSince1970: TimeInterval(config.startTime))
|
||||
credential.expirationDate = Date(timeIntervalSince1970: TimeInterval(config.expireTime))
|
||||
|
||||
let creator = QCloudAuthentationV5Creator(credential: credential)
|
||||
let signature = creator?.signature(forData: urlRequest)
|
||||
compelete(signature, nil)
|
||||
}
|
||||
|
||||
// MARK: - QCloudCredentailFenceQueueDelegate Protocol
|
||||
|
||||
/// 管理凭证(参考 UploadFile.m line 107-133)
|
||||
func fenceQueue(
|
||||
_ queue: QCloudCredentailFenceQueue,
|
||||
requestCreatorWithContinue continueBlock: @escaping QCloudCredentailFenceQueueContinue
|
||||
) {
|
||||
guard let config = qcloudConfig else {
|
||||
let error = NSError(domain: "com.yumi.qcloud", code: -1,
|
||||
userInfo: [NSLocalizedDescriptionKey: YMLocalizedString("error.qcloud_config_not_initialized")])
|
||||
continueBlock(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
let credential = QCloudCredential()
|
||||
credential.secretID = config.secretId
|
||||
credential.secretKey = config.secretKey
|
||||
credential.token = config.sessionToken
|
||||
credential.startDate = Date(timeIntervalSince1970: TimeInterval(config.startTime))
|
||||
credential.expirationDate = Date(timeIntervalSince1970: TimeInterval(config.expireTime))
|
||||
|
||||
let creator = QCloudAuthentationV5Creator(credential: credential)
|
||||
continueBlock(creator, nil)
|
||||
}
|
||||
}
|
||||
721
YuMi/E-P/NewLogin/Controllers/EPLoginTypesViewController.swift
Normal file
721
YuMi/E-P/NewLogin/Controllers/EPLoginTypesViewController.swift
Normal file
@@ -0,0 +1,721 @@
|
||||
//
|
||||
// EPLoginTypesViewController.swift
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-01-27.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class EPLoginTypesViewController: BaseViewController {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
var displayType: EPLoginDisplayType = .id
|
||||
|
||||
private let loginService = EPLoginService()
|
||||
|
||||
private let backgroundImageView = UIImageView()
|
||||
private let titleLabel = UILabel()
|
||||
private let backButton = UIButton(type: .system)
|
||||
|
||||
private let firstInputView = EPLoginInputView()
|
||||
private let secondInputView = EPLoginInputView()
|
||||
private var thirdInputView: EPLoginInputView?
|
||||
|
||||
private let actionButton = UIButton(type: .system)
|
||||
private var forgotPasswordButton: UIButton?
|
||||
|
||||
private var hasAddedGradient = false
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setupUI()
|
||||
configureForDisplayType()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
navigationController?.setNavigationBarHidden(true, animated: false)
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
// 添加渐变背景到 actionButton(只添加一次)
|
||||
if !hasAddedGradient && actionButton.bounds.width > 0 {
|
||||
actionButton.addGradientBackground(
|
||||
with: [
|
||||
EPLoginConfig.Colors.gradientStart,
|
||||
EPLoginConfig.Colors.gradientEnd
|
||||
],
|
||||
start: CGPoint(x: 0, y: 0.5),
|
||||
end: CGPoint(x: 1, y: 0.5),
|
||||
cornerRadius: EPLoginConfig.Layout.uniformCornerRadius
|
||||
)
|
||||
hasAddedGradient = true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
private func setupUI() {
|
||||
setupBackground()
|
||||
setupNavigationBar()
|
||||
setupTitle()
|
||||
setupInputViews()
|
||||
setupActionButton()
|
||||
}
|
||||
|
||||
private func setupBackground() {
|
||||
view.addSubview(backgroundImageView)
|
||||
backgroundImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
backgroundImageView.image = kImage(EPLoginConfig.Images.background)
|
||||
backgroundImageView.contentMode = .scaleAspectFill
|
||||
|
||||
backgroundImageView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
private func setupNavigationBar() {
|
||||
view.addSubview(backButton)
|
||||
backButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
backButton.setImage(UIImage(systemName: EPLoginConfig.Images.iconBack), for: .normal)
|
||||
backButton.tintColor = EPLoginConfig.Colors.textLight
|
||||
backButton.addTarget(self, action: #selector(handleBack), for: .touchUpInside)
|
||||
|
||||
backButton.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(EPLoginConfig.Layout.compactHorizontalPadding)
|
||||
make.top.equalTo(view.safeAreaLayoutGuide).offset(8)
|
||||
make.size.equalTo(EPLoginConfig.Layout.backButtonSize)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupTitle() {
|
||||
view.addSubview(titleLabel)
|
||||
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
titleLabel.font = .systemFont(ofSize: EPLoginConfig.Layout.titleFontSize, weight: .bold)
|
||||
titleLabel.textColor = EPLoginConfig.Colors.textLight
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.centerX.equalToSuperview()
|
||||
make.centerY.equalTo(backButton) // 与返回按钮垂直居中对齐
|
||||
}
|
||||
}
|
||||
|
||||
private func setupInputViews() {
|
||||
firstInputView.translatesAutoresizingMaskIntoConstraints = false
|
||||
secondInputView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
view.addSubview(firstInputView)
|
||||
view.addSubview(secondInputView)
|
||||
|
||||
firstInputView.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(EPLoginConfig.Layout.uniformHorizontalPadding)
|
||||
make.trailing.equalToSuperview().offset(-EPLoginConfig.Layout.uniformHorizontalPadding)
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(EPLoginConfig.Layout.inputTitleSpacing)
|
||||
make.height.equalTo(EPLoginConfig.Layout.uniformHeight)
|
||||
}
|
||||
|
||||
secondInputView.snp.makeConstraints { make in
|
||||
make.leading.trailing.equalTo(firstInputView)
|
||||
make.top.equalTo(firstInputView.snp.bottom).offset(EPLoginConfig.Layout.inputVerticalSpacing)
|
||||
make.height.equalTo(EPLoginConfig.Layout.uniformHeight)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupActionButton() {
|
||||
view.addSubview(actionButton)
|
||||
actionButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
actionButton.setTitle("Login", for: .normal)
|
||||
actionButton.setTitleColor(EPLoginConfig.Colors.textLight, for: .normal)
|
||||
actionButton.layer.cornerRadius = EPLoginConfig.Layout.uniformCornerRadius
|
||||
actionButton.titleLabel?.font = .systemFont(ofSize: EPLoginConfig.Layout.buttonFontSize, weight: .semibold)
|
||||
actionButton.addTarget(self, action: #selector(handleAction), for: .touchUpInside)
|
||||
|
||||
// 初始状态:禁用按钮
|
||||
actionButton.isEnabled = false
|
||||
actionButton.alpha = 0.5
|
||||
|
||||
actionButton.snp.makeConstraints { make in
|
||||
make.leading.trailing.equalTo(firstInputView)
|
||||
make.top.equalTo(secondInputView.snp.bottom).offset(EPLoginConfig.Layout.buttonTopSpacing)
|
||||
make.height.equalTo(EPLoginConfig.Layout.uniformHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Configuration
|
||||
|
||||
private func configureForDisplayType() {
|
||||
switch displayType {
|
||||
case .id:
|
||||
titleLabel.text = YMLocalizedString("1.0.37_text_26") // ID Login
|
||||
firstInputView.configure(with: EPLoginInputConfig(
|
||||
showAreaCode: false,
|
||||
showCodeButton: false,
|
||||
isSecure: false,
|
||||
icon: "icon_login_id",
|
||||
placeholder: "Please enter ID",
|
||||
keyboardType: .numberPad // ID 使用数字键盘
|
||||
))
|
||||
firstInputView.onTextChanged = { [weak self] _ in
|
||||
self?.checkActionButtonStatus()
|
||||
}
|
||||
|
||||
secondInputView.configure(with: EPLoginInputConfig(
|
||||
showAreaCode: false,
|
||||
showCodeButton: false,
|
||||
isSecure: true,
|
||||
icon: "icon_login_id",
|
||||
placeholder: "Please enter password",
|
||||
keyboardType: .default // 密码使用默认键盘(需要字母+数字)
|
||||
))
|
||||
secondInputView.onTextChanged = { [weak self] _ in
|
||||
self?.checkActionButtonStatus()
|
||||
}
|
||||
|
||||
actionButton.setTitle("Login", for: .normal)
|
||||
|
||||
// 添加忘记密码按钮
|
||||
setupForgotPasswordButton()
|
||||
|
||||
case .email:
|
||||
titleLabel.text = YMLocalizedString("20.20.51_text_1") // Email Login
|
||||
firstInputView.configure(with: EPLoginInputConfig(
|
||||
showAreaCode: false,
|
||||
showCodeButton: false,
|
||||
isSecure: false,
|
||||
icon: "envelope",
|
||||
placeholder: "Please enter email",
|
||||
keyboardType: .emailAddress // Email 使用邮箱键盘
|
||||
))
|
||||
firstInputView.onTextChanged = { [weak self] _ in
|
||||
self?.checkActionButtonStatus()
|
||||
}
|
||||
|
||||
secondInputView.configure(with: EPLoginInputConfig(
|
||||
showAreaCode: false,
|
||||
showCodeButton: true,
|
||||
isSecure: false,
|
||||
icon: "number",
|
||||
placeholder: "Please enter verification code",
|
||||
keyboardType: .numberPad // 验证码使用数字键盘
|
||||
))
|
||||
secondInputView.onTextChanged = { [weak self] _ in
|
||||
self?.checkActionButtonStatus()
|
||||
}
|
||||
secondInputView.delegate = self
|
||||
actionButton.setTitle("Login", for: .normal)
|
||||
|
||||
case .phone:
|
||||
titleLabel.text = "Phone Login"
|
||||
firstInputView.configure(with: EPLoginInputConfig(
|
||||
showAreaCode: false,
|
||||
showCodeButton: false,
|
||||
isSecure: false,
|
||||
icon: "phone",
|
||||
placeholder: "Please enter phone",
|
||||
keyboardType: .numberPad // 手机号使用数字键盘
|
||||
))
|
||||
firstInputView.onTextChanged = { [weak self] _ in
|
||||
self?.checkActionButtonStatus()
|
||||
}
|
||||
|
||||
secondInputView.configure(with: EPLoginInputConfig(
|
||||
showAreaCode: false,
|
||||
showCodeButton: true,
|
||||
isSecure: false,
|
||||
icon: "number",
|
||||
placeholder: "Please enter verification code",
|
||||
keyboardType: .numberPad // 验证码使用数字键盘
|
||||
))
|
||||
secondInputView.onTextChanged = { [weak self] _ in
|
||||
self?.checkActionButtonStatus()
|
||||
}
|
||||
secondInputView.delegate = self
|
||||
actionButton.setTitle("Login", for: .normal)
|
||||
|
||||
case .emailReset:
|
||||
titleLabel.text = YMLocalizedString("20.20.51_text_20")
|
||||
firstInputView.configure(with: EPLoginInputConfig(
|
||||
showAreaCode: false,
|
||||
showCodeButton: false,
|
||||
isSecure: false,
|
||||
icon: "envelope",
|
||||
placeholder: "Please enter email",
|
||||
keyboardType: .emailAddress // Email 使用邮箱键盘
|
||||
))
|
||||
firstInputView.onTextChanged = { [weak self] _ in
|
||||
self?.checkActionButtonStatus()
|
||||
}
|
||||
|
||||
secondInputView.configure(with: EPLoginInputConfig(
|
||||
showAreaCode: false,
|
||||
showCodeButton: true,
|
||||
isSecure: false,
|
||||
icon: "number",
|
||||
placeholder: "Please enter verification code",
|
||||
keyboardType: .numberPad // 验证码使用数字键盘
|
||||
))
|
||||
secondInputView.onTextChanged = { [weak self] _ in
|
||||
self?.checkActionButtonStatus()
|
||||
}
|
||||
secondInputView.delegate = self
|
||||
|
||||
// 添加第三个输入框
|
||||
setupThirdInputView()
|
||||
actionButton.setTitle("Confirm", for: .normal)
|
||||
|
||||
case .phoneReset:
|
||||
titleLabel.text = YMLocalizedString("20.20.51_text_20")
|
||||
firstInputView.configure(with: EPLoginInputConfig(
|
||||
showAreaCode: false,
|
||||
showCodeButton: false,
|
||||
isSecure: false,
|
||||
icon: "phone",
|
||||
placeholder: "Please enter phone",
|
||||
keyboardType: .numberPad // 手机号使用数字键盘
|
||||
))
|
||||
firstInputView.onTextChanged = { [weak self] _ in
|
||||
self?.checkActionButtonStatus()
|
||||
}
|
||||
|
||||
secondInputView.configure(with: EPLoginInputConfig(
|
||||
showAreaCode: false,
|
||||
showCodeButton: true,
|
||||
isSecure: false,
|
||||
icon: "number",
|
||||
placeholder: "Please enter verification code",
|
||||
keyboardType: .numberPad // 验证码使用数字键盘
|
||||
))
|
||||
secondInputView.onTextChanged = { [weak self] _ in
|
||||
self?.checkActionButtonStatus()
|
||||
}
|
||||
secondInputView.delegate = self
|
||||
|
||||
// 添加第三个输入框
|
||||
setupThirdInputView()
|
||||
actionButton.setTitle("Confirm", for: .normal)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupForgotPasswordButton() {
|
||||
let button = UIButton(type: .system)
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
button.setTitle("Forgot Password?", for: .normal)
|
||||
button.setTitleColor(EPLoginConfig.Colors.textLight, for: .normal)
|
||||
button.titleLabel?.font = .systemFont(ofSize: EPLoginConfig.Layout.smallFontSize)
|
||||
button.addTarget(self, action: #selector(handleForgotPassword), for: .touchUpInside)
|
||||
|
||||
view.addSubview(button)
|
||||
|
||||
button.snp.makeConstraints { make in
|
||||
make.trailing.equalTo(secondInputView)
|
||||
make.top.equalTo(secondInputView.snp.bottom).offset(8)
|
||||
}
|
||||
|
||||
forgotPasswordButton = button
|
||||
}
|
||||
|
||||
private func setupThirdInputView() {
|
||||
let inputView = EPLoginInputView()
|
||||
inputView.translatesAutoresizingMaskIntoConstraints = false
|
||||
inputView.configure(with: EPLoginInputConfig(
|
||||
showAreaCode: false,
|
||||
showCodeButton: false,
|
||||
isSecure: true,
|
||||
icon: EPLoginConfig.Images.iconLock,
|
||||
placeholder: "6-16 Digits + English Letters",
|
||||
keyboardType: .default // 密码使用默认键盘(需要字母+数字)
|
||||
))
|
||||
inputView.onTextChanged = { [weak self] _ in
|
||||
self?.checkActionButtonStatus()
|
||||
}
|
||||
view.addSubview(inputView)
|
||||
|
||||
inputView.snp.makeConstraints { make in
|
||||
make.leading.trailing.equalTo(firstInputView)
|
||||
make.top.equalTo(secondInputView.snp.bottom).offset(EPLoginConfig.Layout.inputVerticalSpacing)
|
||||
make.height.equalTo(EPLoginConfig.Layout.uniformHeight)
|
||||
}
|
||||
|
||||
// 重新调整 actionButton 位置
|
||||
actionButton.snp.remakeConstraints { make in
|
||||
make.leading.trailing.equalTo(firstInputView)
|
||||
make.top.equalTo(inputView.snp.bottom).offset(EPLoginConfig.Layout.buttonTopSpacing)
|
||||
make.height.equalTo(EPLoginConfig.Layout.uniformHeight)
|
||||
}
|
||||
|
||||
thirdInputView = inputView
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@objc private func handleBack() {
|
||||
navigationController?.popViewController(animated: true)
|
||||
}
|
||||
|
||||
@objc private func handleAction() {
|
||||
view.endEditing(true)
|
||||
|
||||
// 执行对应类型的操作
|
||||
switch displayType {
|
||||
case .id:
|
||||
handleIDLogin()
|
||||
case .email:
|
||||
handleEmailLogin()
|
||||
case .phone:
|
||||
handlePhoneLogin()
|
||||
case .emailReset:
|
||||
handleEmailResetPassword()
|
||||
case .phoneReset:
|
||||
handlePhoneResetPassword()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleForgotPassword() {
|
||||
let vc = EPLoginTypesViewController()
|
||||
vc.displayType = .emailReset
|
||||
navigationController?.pushViewController(vc, animated: true)
|
||||
}
|
||||
|
||||
// MARK: - 登录逻辑
|
||||
|
||||
private func handleIDLogin() {
|
||||
let id = firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let password = secondInputView.text
|
||||
|
||||
// 表单验证(简化,仅检查空值)
|
||||
guard !id.isEmpty else {
|
||||
showErrorToast(YMLocalizedString("LoginPresenter0"))
|
||||
return
|
||||
}
|
||||
|
||||
guard !password.isEmpty else {
|
||||
showErrorToast(YMLocalizedString("LoginPresenter1"))
|
||||
return
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
showLoading(true)
|
||||
|
||||
loginService.loginWithID(id: id, password: password) { [weak self] (accountModel: AccountModel) in
|
||||
DispatchQueue.main.async {
|
||||
self?.showLoading(false)
|
||||
print("[EPLogin] ID登录成功: \(accountModel.uid)")
|
||||
self?.showSuccessToast(YMLocalizedString("XPLoginPhoneViewController1"))
|
||||
EPLoginManager.jumpToHome(from: self!)
|
||||
}
|
||||
} failure: { [weak self] (code: Int, msg: String) in
|
||||
DispatchQueue.main.async {
|
||||
self?.showLoading(false)
|
||||
self?.showErrorToast(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleEmailLogin() {
|
||||
let email = firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let code = secondInputView.text
|
||||
|
||||
// 表单验证(简化,仅检查空值)
|
||||
guard !email.isEmpty else {
|
||||
showErrorToast(YMLocalizedString("LoginPresenter0"))
|
||||
return
|
||||
}
|
||||
|
||||
guard !code.isEmpty else {
|
||||
showErrorToast(YMLocalizedString("LoginPresenter1"))
|
||||
return
|
||||
}
|
||||
|
||||
showLoading(true)
|
||||
|
||||
loginService.loginWithEmail(email: email, code: code) { [weak self] (accountModel: AccountModel) in
|
||||
DispatchQueue.main.async {
|
||||
self?.showLoading(false)
|
||||
print("[EPLogin] 邮箱登录成功: \(accountModel.uid)")
|
||||
self?.showSuccessToast(YMLocalizedString("XPLoginPhoneViewController1"))
|
||||
EPLoginManager.jumpToHome(from: self!)
|
||||
}
|
||||
} failure: { [weak self] (code: Int, msg: String) in
|
||||
DispatchQueue.main.async {
|
||||
self?.showLoading(false)
|
||||
self?.showErrorToast(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handlePhoneLogin() {
|
||||
let phone = firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let code = secondInputView.text
|
||||
|
||||
// 表单验证(简化,仅检查空值)
|
||||
guard !phone.isEmpty else {
|
||||
showErrorToast(YMLocalizedString("XPLoginPhoneViewController0"))
|
||||
return
|
||||
}
|
||||
|
||||
guard !code.isEmpty else {
|
||||
showErrorToast(YMLocalizedString("LoginPresenter1"))
|
||||
return
|
||||
}
|
||||
|
||||
showLoading(true)
|
||||
|
||||
loginService.loginWithPhone(phone: phone, code: code, areaCode: "+86") { [weak self] (accountModel: AccountModel) in
|
||||
DispatchQueue.main.async {
|
||||
self?.showLoading(false)
|
||||
print("[EPLogin] 手机登录成功: \(accountModel.uid)")
|
||||
self?.showSuccessToast(YMLocalizedString("XPLoginPhoneViewController1"))
|
||||
EPLoginManager.jumpToHome(from: self!)
|
||||
}
|
||||
} failure: { [weak self] (code: Int, msg: String) in
|
||||
DispatchQueue.main.async {
|
||||
self?.showLoading(false)
|
||||
self?.showErrorToast(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleEmailResetPassword() {
|
||||
guard let thirdInput = thirdInputView else { return }
|
||||
|
||||
let email = firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let code = secondInputView.text
|
||||
let newPassword = thirdInput.text
|
||||
|
||||
// 表单验证(简化,仅检查空值)
|
||||
guard !email.isEmpty else {
|
||||
showErrorToast(YMLocalizedString("LoginPresenter0"))
|
||||
return
|
||||
}
|
||||
|
||||
guard !code.isEmpty else {
|
||||
showErrorToast(YMLocalizedString("LoginPresenter1"))
|
||||
return
|
||||
}
|
||||
|
||||
guard !newPassword.isEmpty else {
|
||||
showErrorToast(YMLocalizedString("LoginPresenter1"))
|
||||
return
|
||||
}
|
||||
|
||||
showLoading(true)
|
||||
|
||||
loginService.resetEmailPassword(email: email, code: code, newPassword: newPassword) { [weak self] in
|
||||
DispatchQueue.main.async {
|
||||
self?.showLoading(false)
|
||||
self?.showSuccessToast(YMLocalizedString("XPForgetPwdViewController1"))
|
||||
self?.navigationController?.popViewController(animated: true)
|
||||
}
|
||||
} failure: { [weak self] (code: Int, msg: String) in
|
||||
DispatchQueue.main.async {
|
||||
self?.showLoading(false)
|
||||
self?.showErrorToast(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handlePhoneResetPassword() {
|
||||
guard let thirdInput = thirdInputView else { return }
|
||||
|
||||
let phone = firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let code = secondInputView.text
|
||||
let newPassword = thirdInput.text
|
||||
|
||||
// 表单验证(简化,仅检查空值)
|
||||
guard !phone.isEmpty else {
|
||||
showErrorToast(YMLocalizedString("XPLoginPhoneViewController0"))
|
||||
return
|
||||
}
|
||||
|
||||
guard !code.isEmpty else {
|
||||
showErrorToast(YMLocalizedString("LoginPresenter1"))
|
||||
return
|
||||
}
|
||||
|
||||
guard !newPassword.isEmpty else {
|
||||
showErrorToast(YMLocalizedString("LoginPresenter1"))
|
||||
return
|
||||
}
|
||||
|
||||
showLoading(true)
|
||||
|
||||
loginService.resetPhonePassword(phone: phone, code: code, areaCode: "+86", newPassword: newPassword) { [weak self] in
|
||||
DispatchQueue.main.async {
|
||||
self?.showLoading(false)
|
||||
self?.showSuccessToast(YMLocalizedString("XPForgetPwdViewController1"))
|
||||
self?.navigationController?.popViewController(animated: true)
|
||||
}
|
||||
} failure: { [weak self] (code: Int, msg: String) in
|
||||
DispatchQueue.main.async {
|
||||
self?.showLoading(false)
|
||||
self?.showErrorToast(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 验证码发送
|
||||
|
||||
private func sendEmailCode() {
|
||||
let email = firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
// 简化验证,仅检查空值
|
||||
guard !email.isEmpty else {
|
||||
secondInputView.stopCountdown()
|
||||
return
|
||||
}
|
||||
|
||||
let type = (displayType == .emailReset) ? 2 : 1 // 2=找回密码, 1=登录
|
||||
|
||||
loginService.sendEmailCode(email: email, type: type) { [weak self] in
|
||||
DispatchQueue.main.async {
|
||||
self?.secondInputView.startCountdown()
|
||||
self?.secondInputView.displayKeyboard()
|
||||
self?.showSuccessToast(YMLocalizedString("XPLoginPhoneViewController2"))
|
||||
}
|
||||
} failure: { [weak self] (code: Int, msg: String) in
|
||||
DispatchQueue.main.async {
|
||||
self?.secondInputView.stopCountdown()
|
||||
self?.showErrorToast(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func sendPhoneCode() {
|
||||
let phone = firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
// 简化验证,仅检查空值
|
||||
guard !phone.isEmpty else {
|
||||
showErrorToast(YMLocalizedString("XPLoginPhoneViewController0"))
|
||||
secondInputView.stopCountdown()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否需要人机验证
|
||||
loadCaptchaWebView { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
let type = (self.displayType == .phoneReset) ? 2 : 1 // 2=找回密码, 1=登录
|
||||
|
||||
self.loginService.sendPhoneCode(phone: phone, areaCode: "+86", type: type) { [weak self] in
|
||||
DispatchQueue.main.async {
|
||||
self?.secondInputView.startCountdown()
|
||||
self?.secondInputView.displayKeyboard()
|
||||
self?.showSuccessToast(YMLocalizedString("XPLoginPhoneViewController2"))
|
||||
}
|
||||
} failure: { [weak self] (code: Int, msg: String) in
|
||||
DispatchQueue.main.async {
|
||||
self?.secondInputView.stopCountdown()
|
||||
self?.showErrorToast(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func sendEmailResetCode() {
|
||||
sendEmailCode() // 复用邮箱验证码逻辑
|
||||
}
|
||||
|
||||
private func sendPhoneResetCode() {
|
||||
sendPhoneCode() // 复用手机验证码逻辑
|
||||
}
|
||||
|
||||
// MARK: - UI Helpers
|
||||
|
||||
private func showLoading(_ show: Bool) {
|
||||
if show {
|
||||
actionButton.isEnabled = false
|
||||
actionButton.alpha = 0.5
|
||||
actionButton.setTitle("Loading...", for: .normal)
|
||||
} else {
|
||||
switch displayType {
|
||||
case .id, .email, .phone:
|
||||
actionButton.setTitle("Login", for: .normal)
|
||||
case .emailReset, .phoneReset:
|
||||
actionButton.setTitle("Confirm", for: .normal)
|
||||
}
|
||||
checkActionButtonStatus()
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查并更新按钮启用状态
|
||||
private func checkActionButtonStatus() {
|
||||
let isEnabled: Bool
|
||||
|
||||
switch displayType {
|
||||
case .id:
|
||||
let hasId = !firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
let hasPassword = !secondInputView.text.isEmpty
|
||||
isEnabled = hasId && hasPassword
|
||||
|
||||
case .email, .phone:
|
||||
let hasAccount = !firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
let hasCode = !secondInputView.text.isEmpty
|
||||
isEnabled = hasAccount && hasCode
|
||||
|
||||
case .emailReset, .phoneReset:
|
||||
let hasAccount = !firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
let hasCode = !secondInputView.text.isEmpty
|
||||
let hasPassword = !(thirdInputView?.text.isEmpty ?? true)
|
||||
isEnabled = hasAccount && hasCode && hasPassword
|
||||
}
|
||||
|
||||
actionButton.isEnabled = isEnabled
|
||||
actionButton.alpha = isEnabled ? 1.0 : 0.5
|
||||
}
|
||||
|
||||
/// 加载人机验证 Captcha WebView
|
||||
/// - Parameter completion: 验证成功后的回调
|
||||
private func loadCaptchaWebView(completion: @escaping () -> Void) {
|
||||
guard ClientConfig.share().shouldDisplayCaptcha else {
|
||||
// 不需要验证,直接执行
|
||||
completion()
|
||||
return
|
||||
}
|
||||
|
||||
view.endEditing(true)
|
||||
|
||||
let webVC = XPWebViewController(roomUID: nil)
|
||||
webVC.view.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width * 0.8, height: UIScreen.main.bounds.width * 1.2)
|
||||
webVC.view.backgroundColor = .clear
|
||||
webVC.view.layer.cornerRadius = 12
|
||||
webVC.view.layer.masksToBounds = true
|
||||
webVC.isLoginStatus = false
|
||||
webVC.isPush = false
|
||||
webVC.hideNavigationBar()
|
||||
webVC.url = URLWithType(.captchaSwitch)
|
||||
|
||||
webVC.verifyCaptcha = { result in
|
||||
if result {
|
||||
TTPopup.dismiss()
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
TTPopup.popupView(webVC.view, style: .alert)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - EPLoginInputViewDelegate
|
||||
|
||||
extension EPLoginTypesViewController: EPLoginInputViewDelegate {
|
||||
func inputViewDidRequestCode(_ inputView: EPLoginInputView) {
|
||||
if inputView == secondInputView {
|
||||
if displayType == .email || displayType == .emailReset {
|
||||
sendEmailCode()
|
||||
} else if displayType == .phone || displayType == .phoneReset {
|
||||
sendPhoneCode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func inputViewDidSelectArea(_ inputView: EPLoginInputView) {
|
||||
// 区号选择(暂不实现)
|
||||
print("[EPLogin] Area selection - 占位,Phase 2 实现")
|
||||
}
|
||||
}
|
||||
307
YuMi/E-P/NewLogin/Controllers/EPLoginViewController.swift
Normal file
307
YuMi/E-P/NewLogin/Controllers/EPLoginViewController.swift
Normal file
@@ -0,0 +1,307 @@
|
||||
//
|
||||
// EPLoginViewController.swift
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-01-27.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@objc class EPLoginViewController: UIViewController {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private let backgroundImageView = UIImageView()
|
||||
private let logoImageView = UIImageView()
|
||||
private let epartiTitleLabel = UILabel()
|
||||
|
||||
private let idLoginButton = EPLoginButton()
|
||||
private let emailLoginButton = EPLoginButton()
|
||||
|
||||
private let agreeCheckbox = UIButton(type: .custom)
|
||||
private let policyLabel = EPPolicyLabel()
|
||||
|
||||
private let feedbackButton = UIButton(type: .custom)
|
||||
|
||||
#if DEBUG
|
||||
private let debugButton = UIButton(type: .custom)
|
||||
#endif
|
||||
|
||||
private let policySelectedKey = EPLoginConfig.Keys.policyAgreed
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
// 验证 DEBUG 编译条件
|
||||
#if DEBUG
|
||||
print("✅ [EPLogin] DEBUG 模式已激活")
|
||||
#else
|
||||
print("⚠️ [EPLogin] 当前为 Release 模式")
|
||||
#endif
|
||||
|
||||
navigationController?.setNavigationBarHidden(true, animated: false)
|
||||
setupUI()
|
||||
loadPolicyStatus()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
navigationController?.setNavigationBarHidden(true, animated: false)
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
private func setupUI() {
|
||||
setupBackground()
|
||||
setupLogo()
|
||||
setupLoginButtons()
|
||||
setupPolicyArea()
|
||||
setupNavigationBar()
|
||||
}
|
||||
|
||||
private func setupBackground() {
|
||||
view.addSubview(backgroundImageView)
|
||||
backgroundImageView.image = kImage(EPLoginConfig.Images.background)
|
||||
backgroundImageView.contentMode = .scaleAspectFill
|
||||
|
||||
backgroundImageView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
private func setupLogo() {
|
||||
view.addSubview(logoImageView)
|
||||
logoImageView.image = kImage(EPLoginConfig.Images.loginBg)
|
||||
|
||||
logoImageView.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalTo(view)
|
||||
make.height.equalTo(EPLoginConfig.Layout.logoHeight)
|
||||
}
|
||||
|
||||
// E-PARTY 标题
|
||||
view.addSubview(epartiTitleLabel)
|
||||
epartiTitleLabel.text = "E-PARTY"
|
||||
epartiTitleLabel.font = .systemFont(ofSize: EPLoginConfig.Layout.epartiTitleFontSize, weight: .bold)
|
||||
epartiTitleLabel.textColor = EPLoginConfig.Colors.textLight
|
||||
epartiTitleLabel.transform = CGAffineTransform(a: 1, b: 0, c: -0.2, d: 1, tx: 0, ty: 0) // 斜体效果
|
||||
|
||||
epartiTitleLabel.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(EPLoginConfig.Layout.epartiTitleLeading)
|
||||
make.bottom.equalTo(logoImageView.snp.bottom).offset(EPLoginConfig.Layout.epartiTitleBottomOffset)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupLoginButtons() {
|
||||
// 配置按钮
|
||||
idLoginButton.configure(
|
||||
icon: EPLoginConfig.Images.iconLoginId,
|
||||
title: YMLocalizedString(EPLoginConfig.LocalizedKeys.idLogin)
|
||||
)
|
||||
idLoginButton.delegate = self
|
||||
|
||||
emailLoginButton.configure(
|
||||
icon: EPLoginConfig.Images.iconLoginEmail,
|
||||
title: YMLocalizedString(EPLoginConfig.LocalizedKeys.emailLogin)
|
||||
)
|
||||
emailLoginButton.delegate = self
|
||||
|
||||
// StackView 布局
|
||||
let stackView = UIStackView(arrangedSubviews: [idLoginButton, emailLoginButton])
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = EPLoginConfig.Layout.loginButtonSpacing
|
||||
stackView.distribution = .fillEqually
|
||||
view.addSubview(stackView)
|
||||
|
||||
stackView.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(EPLoginConfig.Layout.loginButtonHorizontalPadding)
|
||||
make.trailing.equalToSuperview().offset(-EPLoginConfig.Layout.loginButtonHorizontalPadding)
|
||||
make.top.equalTo(logoImageView.snp.bottom)
|
||||
}
|
||||
|
||||
idLoginButton.snp.makeConstraints { make in
|
||||
make.height.equalTo(EPLoginConfig.Layout.loginButtonHeight)
|
||||
}
|
||||
|
||||
emailLoginButton.snp.makeConstraints { make in
|
||||
make.height.equalTo(EPLoginConfig.Layout.loginButtonHeight)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupPolicyArea() {
|
||||
view.addSubview(agreeCheckbox)
|
||||
view.addSubview(policyLabel)
|
||||
|
||||
agreeCheckbox.setImage(kImage("login_privace_select"), for: .selected)
|
||||
agreeCheckbox.setImage(kImage("login_privace_unselect"), for: .normal)
|
||||
agreeCheckbox.addTarget(self, action: #selector(togglePolicyCheckbox), for: .touchUpInside)
|
||||
|
||||
policyLabel.onUserAgreementTapped = { [weak self] in
|
||||
print("[EPLogin] User agreement tapped callback triggered")
|
||||
let url = self?.getUserAgreementURL() ?? ""
|
||||
print("[EPLogin] User agreement URL: \(url)")
|
||||
self?.openPolicyInExternalBrowser(url)
|
||||
}
|
||||
policyLabel.onPrivacyPolicyTapped = { [weak self] in
|
||||
print("[EPLogin] Privacy policy tapped callback triggered")
|
||||
let url = self?.getPrivacyPolicyURL() ?? ""
|
||||
print("[EPLogin] Privacy policy URL: \(url)")
|
||||
self?.openPolicyInExternalBrowser(url)
|
||||
}
|
||||
|
||||
agreeCheckbox.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(EPLoginConfig.Layout.horizontalPadding)
|
||||
make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-30)
|
||||
make.size.equalTo(EPLoginConfig.Layout.checkboxSize)
|
||||
}
|
||||
|
||||
policyLabel.snp.makeConstraints { make in
|
||||
make.leading.equalTo(agreeCheckbox.snp.trailing).offset(8)
|
||||
make.trailing.equalToSuperview().offset(-EPLoginConfig.Layout.horizontalPadding)
|
||||
make.centerY.equalTo(agreeCheckbox)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupNavigationBar() {
|
||||
#if DEBUG
|
||||
view.addSubview(feedbackButton)
|
||||
feedbackButton.setTitle(YMLocalizedString(EPLoginConfig.LocalizedKeys.feedback), for: .normal)
|
||||
feedbackButton.titleLabel?.font = .systemFont(ofSize: EPLoginConfig.Layout.smallFontSize)
|
||||
feedbackButton.backgroundColor = EPLoginConfig.Colors.backgroundTransparent
|
||||
feedbackButton.layer.cornerRadius = EPLoginConfig.Layout.feedbackButtonCornerRadius
|
||||
feedbackButton.addTarget(self, action: #selector(handleFeedback), for: .touchUpInside)
|
||||
|
||||
feedbackButton.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().offset(-EPLoginConfig.Layout.compactHorizontalPadding)
|
||||
make.top.equalTo(view.safeAreaLayoutGuide).offset(8)
|
||||
make.height.equalTo(EPLoginConfig.Layout.feedbackButtonHeight)
|
||||
}
|
||||
|
||||
view.addSubview(debugButton)
|
||||
debugButton.setTitle("切换环境", for: .normal)
|
||||
debugButton.setTitleColor(.blue, for: .normal)
|
||||
debugButton.addTarget(self, action: #selector(handleDebug), for: .touchUpInside)
|
||||
|
||||
debugButton.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(EPLoginConfig.Layout.compactHorizontalPadding)
|
||||
make.top.equalTo(view.safeAreaLayoutGuide).offset(8)
|
||||
}
|
||||
#endif // DEBUG
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
private func handleIDLogin() {
|
||||
let vc = EPLoginTypesViewController()
|
||||
vc.displayType = .id
|
||||
navigationController?.pushViewController(vc, animated: true)
|
||||
}
|
||||
|
||||
private func handleEmailLogin() {
|
||||
let vc = EPLoginTypesViewController()
|
||||
vc.displayType = .email
|
||||
navigationController?.pushViewController(vc, animated: true)
|
||||
}
|
||||
|
||||
@objc private func togglePolicyCheckbox() {
|
||||
agreeCheckbox.isSelected.toggle()
|
||||
UserDefaults.standard.set(agreeCheckbox.isSelected, forKey: policySelectedKey)
|
||||
}
|
||||
|
||||
@objc private func handleFeedback() {
|
||||
print("[EPLogin] Feedback - 占位,Phase 2 实现")
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
@objc private func handleDebug() {
|
||||
print("[EPLogin] Debug - 占位,Phase 2 实现")
|
||||
}
|
||||
#endif
|
||||
|
||||
private func openPolicyInExternalBrowser(_ urlString: String) {
|
||||
print("[EPLogin] Original URL: \(urlString)")
|
||||
|
||||
// 如果不是完整 URL,拼接域名(参考 XPWebViewController.m 第 697-698 行)
|
||||
var fullUrl = urlString
|
||||
if !urlString.hasPrefix("http") && !urlString.hasPrefix("https") {
|
||||
let hostUrl = HttpRequestHelper.getHostUrl()
|
||||
fullUrl = "\(hostUrl)/\(urlString)"
|
||||
print("[EPLogin] Added host URL, full URL: \(fullUrl)")
|
||||
}
|
||||
|
||||
print("[EPLogin] Opening URL in external browser: \(fullUrl)")
|
||||
|
||||
guard let url = URL(string: fullUrl) else {
|
||||
print("[EPLogin] ❌ Invalid URL: \(fullUrl)")
|
||||
return
|
||||
}
|
||||
|
||||
print("[EPLogin] URL object created: \(url)")
|
||||
|
||||
// 在外部浏览器中打开
|
||||
if UIApplication.shared.canOpenURL(url) {
|
||||
print("[EPLogin] ✅ Can open URL, attempting to open...")
|
||||
UIApplication.shared.open(url, options: [:]) { success in
|
||||
print("[EPLogin] Open external browser: \(success ? "✅ Success" : "❌ Failed")")
|
||||
}
|
||||
} else {
|
||||
print("[EPLogin] ❌ Cannot open URL: \(fullUrl)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func loadPolicyStatus() {
|
||||
agreeCheckbox.isSelected = UserDefaults.standard.bool(forKey: policySelectedKey)
|
||||
// 默认勾选
|
||||
if !UserDefaults.standard.bool(forKey: EPLoginConfig.Keys.hasLaunchedBefore) {
|
||||
agreeCheckbox.isSelected = true
|
||||
UserDefaults.standard.set(true, forKey: policySelectedKey)
|
||||
UserDefaults.standard.set(true, forKey: EPLoginConfig.Keys.hasLaunchedBefore)
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取用户协议 URL
|
||||
private func getUserAgreementURL() -> String {
|
||||
// kUserProtocalURL 对应枚举值 4
|
||||
let url = URLWithType(URLType(rawValue: 4)!) as String
|
||||
print("[EPLogin] User agreement URL from URLWithType: \(url)")
|
||||
return url
|
||||
}
|
||||
|
||||
/// 获取隐私政策 URL
|
||||
private func getPrivacyPolicyURL() -> String {
|
||||
// kPrivacyURL 对应枚举值 0
|
||||
let url = URLWithType(URLType(rawValue: 0)!) as String
|
||||
print("[EPLogin] Privacy policy URL from URLWithType: \(url)")
|
||||
return url
|
||||
}
|
||||
|
||||
private func checkPolicyAgreed() -> Bool {
|
||||
if !agreeCheckbox.isSelected {
|
||||
// Phase 2: 显示提示
|
||||
print("[EPLogin] Please agree to policy first")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - EPLoginButtonDelegate
|
||||
|
||||
extension EPLoginViewController: EPLoginButtonDelegate {
|
||||
func loginButtonDidTap(_ button: EPLoginButton) {
|
||||
guard checkPolicyAgreed() else { return }
|
||||
|
||||
if button == idLoginButton {
|
||||
handleIDLogin()
|
||||
} else if button == emailLoginButton {
|
||||
handleEmailLogin()
|
||||
}
|
||||
}
|
||||
}
|
||||
33
YuMi/E-P/NewLogin/Models/EPLoginBridge.swift
Normal file
33
YuMi/E-P/NewLogin/Models/EPLoginBridge.swift
Normal file
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// EPLoginBridge.swift
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-01-27.
|
||||
// 桥接 Objective-C 宏到 Swift
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// 桥接 kImage 宏
|
||||
func kImage(_ name: String) -> UIImage? {
|
||||
return UIImage(named: name)
|
||||
}
|
||||
|
||||
/// 桥接 YMLocalizedString 宏
|
||||
func YMLocalizedString(_ key: String) -> String {
|
||||
return Bundle.ymLocalizedString(forKey: key)
|
||||
}
|
||||
|
||||
/// 桥接 URLType 枚举常量
|
||||
extension URLType {
|
||||
static var captchaSwitch: URLType {
|
||||
return URLType(rawValue: 113)! // kCaptchaSwitchPath
|
||||
}
|
||||
}
|
||||
|
||||
/// DES 加密辅助函数
|
||||
func encryptDES(_ plainText: String) -> String {
|
||||
// 直接使用加密密钥(与 ObjC 版本保持一致)
|
||||
let key = "1ea53d260ecf11e7b56e00163e046a26"
|
||||
return DESEncrypt.encryptUseDES(plainText, key: key) ?? plainText
|
||||
}
|
||||
305
YuMi/E-P/NewLogin/Models/EPLoginConfig.swift
Normal file
305
YuMi/E-P/NewLogin/Models/EPLoginConfig.swift
Normal file
@@ -0,0 +1,305 @@
|
||||
//
|
||||
// EPLoginConfig.swift
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-01-27.
|
||||
// 统一配置文件 - 消除硬编码
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// 登录模块统一配置
|
||||
struct EPLoginConfig {
|
||||
|
||||
// MARK: - Layout 布局尺寸
|
||||
|
||||
struct Layout {
|
||||
/// 标准按钮宽度
|
||||
static let buttonWidth: CGFloat = 294
|
||||
/// 标准按钮高度
|
||||
static let buttonHeight: CGFloat = 46
|
||||
/// 登录按钮高度
|
||||
static let loginButtonHeight: CGFloat = 56
|
||||
/// 登录按钮间距
|
||||
static let loginButtonSpacing: CGFloat = 24
|
||||
/// 登录按钮左右边距
|
||||
static let loginButtonHorizontalPadding: CGFloat = 30
|
||||
|
||||
/// 输入框/按钮统一高度
|
||||
static let uniformHeight: CGFloat = 56
|
||||
/// 输入框/按钮统一左右边距
|
||||
static let uniformHorizontalPadding: CGFloat = 29
|
||||
/// 输入框/按钮统一圆角
|
||||
static let uniformCornerRadius: CGFloat = 28
|
||||
/// 标准圆角半径(按钮/输入框)
|
||||
static let cornerRadius: CGFloat = 23
|
||||
|
||||
/// Logo 尺寸
|
||||
static let logoHeight: CGFloat = 400
|
||||
/// Logo 距离顶部的距离
|
||||
static let logoTopOffset: CGFloat = 80
|
||||
|
||||
/// E-PARTY 标题字号
|
||||
static let epartiTitleFontSize: CGFloat = 56
|
||||
/// E-PARTY 标题距离 view leading
|
||||
static let epartiTitleLeading: CGFloat = 40
|
||||
/// E-PARTY 标题距离 logoImage bottom 的偏移(负值表示向上)
|
||||
static let epartiTitleBottomOffset: CGFloat = -30
|
||||
|
||||
/// 输入框之间的垂直间距
|
||||
static let inputVerticalSpacing: CGFloat = 16
|
||||
/// 输入框距离标题的距离
|
||||
static let inputTitleSpacing: CGFloat = 60
|
||||
|
||||
/// 按钮距离输入框的距离
|
||||
static let buttonTopSpacing: CGFloat = 40
|
||||
|
||||
/// 页面左右边距
|
||||
static let horizontalPadding: CGFloat = 40
|
||||
/// 紧凑左右边距
|
||||
static let compactHorizontalPadding: CGFloat = 16
|
||||
|
||||
/// 标题字体大小
|
||||
static let titleFontSize: CGFloat = 28
|
||||
/// 按钮字体大小
|
||||
static let buttonFontSize: CGFloat = 16
|
||||
/// 输入框字体大小
|
||||
static let inputFontSize: CGFloat = 14
|
||||
/// 小字体大小(提示文字等)
|
||||
static let smallFontSize: CGFloat = 12
|
||||
|
||||
/// 图标尺寸
|
||||
static let iconSize: CGFloat = 24
|
||||
/// 登录按钮图标尺寸
|
||||
static let loginButtonIconSize: CGFloat = 30
|
||||
/// 登录按钮图标左边距(距离白色背景)
|
||||
static let loginButtonIconLeading: CGFloat = 33
|
||||
/// 图标左边距
|
||||
static let iconLeading: CGFloat = 15
|
||||
/// 图标与文字间距
|
||||
static let iconTextSpacing: CGFloat = 12
|
||||
|
||||
/// Checkbox 尺寸
|
||||
static let checkboxSize: CGFloat = 18
|
||||
|
||||
/// 返回按钮尺寸
|
||||
static let backButtonSize: CGFloat = 44
|
||||
|
||||
/// Feedback 按钮高度
|
||||
static let feedbackButtonHeight: CGFloat = 22
|
||||
static let feedbackButtonCornerRadius: CGFloat = 10.5
|
||||
|
||||
/// 输入框高度
|
||||
static let inputHeight: CGFloat = 56
|
||||
/// 输入框圆角
|
||||
static let inputCornerRadius: CGFloat = 28
|
||||
/// 输入框左右内边距
|
||||
static let inputHorizontalPadding: CGFloat = 24
|
||||
/// 输入框 icon 尺寸
|
||||
static let inputIconSize: CGFloat = 20
|
||||
/// 输入框边框宽度
|
||||
static let inputBorderWidth: CGFloat = 1
|
||||
|
||||
/// 验证码按钮宽度
|
||||
static let codeButtonWidth: CGFloat = 102
|
||||
/// 验证码按钮高度
|
||||
static let codeButtonHeight: CGFloat = 38
|
||||
}
|
||||
|
||||
// MARK: - Colors 颜色主题
|
||||
|
||||
struct Colors {
|
||||
/// 主题色(按钮背景)
|
||||
static let primary = UIColor.systemPurple
|
||||
|
||||
/// 背景色
|
||||
static let background = UIColor.white
|
||||
static let backgroundTransparent = UIColor.white.withAlphaComponent(0.5)
|
||||
|
||||
/// 文字颜色
|
||||
static let text = UIColor.darkText
|
||||
static let textSecondary = UIColor.darkGray
|
||||
static let textLight = UIColor.white
|
||||
|
||||
/// 图标颜色
|
||||
static let icon = UIColor.darkGray
|
||||
static let iconDisabled = UIColor.gray
|
||||
|
||||
/// 输入框颜色
|
||||
static let inputBackground = UIColor.white.withAlphaComponent(0.1)
|
||||
static let inputText = UIColor(red: 0x1F/255.0, green: 0x1B/255.0, blue: 0x4F/255.0, alpha: 1.0)
|
||||
static let inputBorder = UIColor.white
|
||||
static let inputBorderFocused = UIColor.systemPurple
|
||||
|
||||
/// 渐变色(Login/Confirm按钮)
|
||||
static let gradientStart = UIColor(red: 0xF8/255.0, green: 0x54/255.0, blue: 0xFC/255.0, alpha: 1.0) // #F854FC
|
||||
static let gradientEnd = UIColor(red: 0x50/255.0, green: 0x0F/255.0, blue: 0xFF/255.0, alpha: 1.0) // #500FFF
|
||||
|
||||
/// 验证码按钮颜色
|
||||
static let codeButtonBackground = UIColor(red: 0x91/255.0, green: 0x68/255.0, blue: 0xFA/255.0, alpha: 1.0)
|
||||
|
||||
/// 按钮状态颜色
|
||||
static let buttonEnabled = UIColor.systemPurple
|
||||
static let buttonDisabled = UIColor.lightGray
|
||||
|
||||
/// 错误提示色
|
||||
static let error = UIColor.systemRed
|
||||
static let success = UIColor.systemGreen
|
||||
|
||||
/// 链接颜色
|
||||
static let link = UIColor.black
|
||||
static let linkUnderline = UIColor.black
|
||||
}
|
||||
|
||||
// MARK: - Animation 动画配置
|
||||
|
||||
struct Animation {
|
||||
/// 标准动画时长
|
||||
static let duration: TimeInterval = 0.3
|
||||
/// 短动画时长
|
||||
static let shortDuration: TimeInterval = 0.15
|
||||
/// 长动画时长
|
||||
static let longDuration: TimeInterval = 0.5
|
||||
|
||||
/// 弹簧动画阻尼
|
||||
static let springDamping: CGFloat = 0.75
|
||||
/// 弹簧动画初速度
|
||||
static let springVelocity: CGFloat = 0.5
|
||||
|
||||
/// 按钮点击缩放比例
|
||||
static let buttonPressScale: CGFloat = 0.95
|
||||
|
||||
/// 错误抖动距离
|
||||
static let shakeOffset: CGFloat = 10
|
||||
/// 错误抖动次数
|
||||
static let shakeCount: Int = 3
|
||||
}
|
||||
|
||||
// MARK: - Validation 验证规则
|
||||
|
||||
struct Validation {
|
||||
/// 密码最小长度
|
||||
static let passwordMinLength = 6
|
||||
/// 密码最大长度
|
||||
static let passwordMaxLength = 16
|
||||
|
||||
/// 验证码长度
|
||||
static let codeLength = 6
|
||||
|
||||
/// 手机号最小长度
|
||||
static let phoneMinLength = 10
|
||||
/// 手机号最大长度
|
||||
static let phoneMaxLength = 15
|
||||
|
||||
/// 邮箱正则表达式
|
||||
static let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
|
||||
/// 手机号正则表达式
|
||||
static let phoneRegex = "^[0-9]{10,15}$"
|
||||
}
|
||||
|
||||
// MARK: - Timing 时间配置
|
||||
|
||||
struct Timing {
|
||||
/// 验证码倒计时秒数
|
||||
static let codeCountdownSeconds = 60
|
||||
|
||||
/// Toast 显示时长
|
||||
static let toastDuration: TimeInterval = 2.0
|
||||
|
||||
/// 加载超时时间
|
||||
static let requestTimeout: TimeInterval = 30.0
|
||||
}
|
||||
|
||||
// MARK: - API 接口配置
|
||||
|
||||
struct API {
|
||||
/// Client Secret
|
||||
static let clientSecret = "uyzjdhds"
|
||||
/// Client ID
|
||||
static let clientId = "erban-client"
|
||||
/// Grant Type
|
||||
static let grantType = "password"
|
||||
/// 版本号
|
||||
static let version = "1"
|
||||
|
||||
/// 验证码类型:登录
|
||||
static let codeTypeLogin = 1
|
||||
/// 验证码类型:找回密码
|
||||
static let codeTypeReset = 2
|
||||
}
|
||||
|
||||
// MARK: - UserDefaults Keys
|
||||
|
||||
struct Keys {
|
||||
/// 隐私协议已同意
|
||||
static let policyAgreed = "HadAgreePrivacy"
|
||||
/// 首次启动标识
|
||||
static let hasLaunchedBefore = "HasLaunchedBefore"
|
||||
}
|
||||
|
||||
// MARK: - Images 图片资源名称
|
||||
|
||||
struct Images {
|
||||
/// 背景图
|
||||
static let background = "vc_bg"
|
||||
/// Logo 背景图
|
||||
static let loginBg = "login_bg"
|
||||
|
||||
/// 登录按钮图标 - ID
|
||||
static let iconLoginId = "icon_login_id"
|
||||
/// 登录按钮图标 - Email
|
||||
static let iconLoginEmail = "icon_login_email"
|
||||
|
||||
/// 图标 - 用户
|
||||
static let iconPerson = "person.circle"
|
||||
static let iconPersonFill = "person"
|
||||
/// 图标 - 邮箱
|
||||
static let iconEmail = "envelope.circle"
|
||||
static let iconEmailFill = "envelope"
|
||||
/// 图标 - 手机
|
||||
static let iconPhone = "phone.circle"
|
||||
static let iconPhoneFill = "phone"
|
||||
/// 图标 - Apple
|
||||
static let iconApple = "apple.logo"
|
||||
/// 图标 - 锁
|
||||
static let iconLock = "lock"
|
||||
/// 图标 - 数字
|
||||
static let iconNumber = "number"
|
||||
|
||||
/// 密码可见性图标
|
||||
static let iconPasswordSee = "icon_password_see"
|
||||
static let iconPasswordUnsee = "icon_password_unsee"
|
||||
|
||||
/// 图标 - 返回
|
||||
static let iconBack = "chevron.left"
|
||||
/// 图标 - 眼睛(隐藏)
|
||||
static let iconEyeSlash = "eye.slash"
|
||||
/// 图标 - 眼睛(显示)
|
||||
static let iconEye = "eye"
|
||||
|
||||
/// Checkbox - 未选中
|
||||
static let checkboxEmpty = "circle"
|
||||
/// Checkbox - 已选中
|
||||
static let checkboxFilled = "checkmark.circle"
|
||||
}
|
||||
|
||||
// MARK: - Localized Strings Keys
|
||||
|
||||
struct LocalizedKeys {
|
||||
/// ID 登录
|
||||
static let idLogin = "1.0.37_text_26"
|
||||
/// 邮箱登录
|
||||
static let emailLogin = "20.20.51_text_1"
|
||||
|
||||
/// 隐私协议完整文本
|
||||
static let policyFullText = "XPLoginViewController6"
|
||||
/// 用户协议
|
||||
static let userAgreement = "XPLoginViewController7"
|
||||
/// 隐私政策
|
||||
static let privacyPolicy = "XPLoginViewController9"
|
||||
|
||||
/// 反馈
|
||||
static let feedback = "XPMineFeedbackViewController0"
|
||||
}
|
||||
}
|
||||
|
||||
52
YuMi/E-P/NewLogin/Models/EPLoginState.swift
Normal file
52
YuMi/E-P/NewLogin/Models/EPLoginState.swift
Normal file
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// EPLoginState.swift
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-01-27.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 登录显示类型枚举
|
||||
enum EPLoginDisplayType {
|
||||
case id // ID + 密码
|
||||
case email // 邮箱 + 验证码
|
||||
case phone // 手机号 + 验证码
|
||||
case emailReset // 邮箱找回密码
|
||||
case phoneReset // 手机号找回密码
|
||||
}
|
||||
|
||||
/// 登录状态验证器(Phase 2 实现)
|
||||
class EPLoginValidator {
|
||||
|
||||
/// 密码强度验证:6-16位,必须包含字母+数字
|
||||
func validatePassword(_ password: String) -> Bool {
|
||||
guard password.count >= 6 && password.count <= 16 else { return false }
|
||||
|
||||
let hasLetter = password.rangeOfCharacter(from: .letters) != nil
|
||||
let hasDigit = password.rangeOfCharacter(from: .decimalDigits) != nil
|
||||
|
||||
return hasLetter && hasDigit
|
||||
}
|
||||
|
||||
/// 邮箱格式验证
|
||||
func validateEmail(_ email: String) -> Bool {
|
||||
let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
|
||||
let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailRegex)
|
||||
return emailPredicate.evaluate(with: email)
|
||||
}
|
||||
|
||||
/// 验证码格式验证(6位数字)
|
||||
func validateCode(_ code: String) -> Bool {
|
||||
guard code.count == 6 else { return false }
|
||||
return code.allSatisfy { $0.isNumber }
|
||||
}
|
||||
|
||||
/// 手机号格式验证(简单验证)
|
||||
func validatePhone(_ phone: String) -> Bool {
|
||||
let phoneRegex = "^[0-9]{10,15}$"
|
||||
let phonePredicate = NSPredicate(format: "SELF MATCHES %@", phoneRegex)
|
||||
return phonePredicate.evaluate(with: phone)
|
||||
}
|
||||
}
|
||||
|
||||
129
YuMi/E-P/NewLogin/Services/EPLoginManager.swift
Normal file
129
YuMi/E-P/NewLogin/Services/EPLoginManager.swift
Normal file
@@ -0,0 +1,129 @@
|
||||
//
|
||||
// EPLoginManager.swift
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-01-27.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// 登录管理器(Swift 版本)
|
||||
/// 替代 PILoginManager,处理登录成功后的路由和初始化
|
||||
@objc class EPLoginManager: NSObject {
|
||||
|
||||
// MARK: - Login Success Navigation
|
||||
|
||||
/// 登录成功后跳转首页
|
||||
/// - Parameter viewController: 当前视图控制器
|
||||
static func jumpToHome(from viewController: UIViewController) {
|
||||
|
||||
// 1. 获取当前账号信息
|
||||
guard let accountModel = AccountInfoStorage.instance().getCurrentAccountInfo() else {
|
||||
print("[EPLoginManager] 账号信息不完整,无法继续")
|
||||
return
|
||||
}
|
||||
|
||||
let accessToken = accountModel.access_token
|
||||
guard !accessToken.isEmpty else {
|
||||
print("[EPLoginManager] access_token 为空,无法继续")
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 请求 ticket
|
||||
let loginService = EPLoginService()
|
||||
loginService.requestTicket(accessToken: accessToken) { ticket in
|
||||
|
||||
// 3. 保存 ticket
|
||||
AccountInfoStorage.instance().saveTicket(ticket)
|
||||
|
||||
// 4. 切换到 EPTabBarController
|
||||
DispatchQueue.main.async {
|
||||
let epTabBar = EPTabBarController.create()
|
||||
epTabBar.refreshTabBarWithIsLogin(true)
|
||||
|
||||
// 设置为根控制器(统一从 ObjC inline 函数获取)
|
||||
if let window = kGetKeyWindow() {
|
||||
window.rootViewController = epTabBar
|
||||
window.makeKeyAndVisible()
|
||||
|
||||
// 延迟检查专属颜色(登录成功后引导)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
|
||||
Self.checkAndShowSignatureColorGuide(in: window)
|
||||
}
|
||||
}
|
||||
|
||||
print("[EPLoginManager] 登录成功,已切换到 EPTabBarController")
|
||||
}
|
||||
|
||||
} failure: { code, msg in
|
||||
print("[EPLoginManager] 请求 Ticket 失败: \(code) - \(msg)")
|
||||
|
||||
// Ticket 请求失败,仍然跳转到首页(保持原有行为)
|
||||
DispatchQueue.main.async {
|
||||
let epTabBar = EPTabBarController.create()
|
||||
epTabBar.refreshTabBarWithIsLogin(true)
|
||||
|
||||
if let window = kGetKeyWindow() {
|
||||
window.rootViewController = epTabBar
|
||||
window.makeKeyAndVisible()
|
||||
|
||||
// 延迟检查专属颜色(登录成功后引导)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
|
||||
Self.checkAndShowSignatureColorGuide(in: window)
|
||||
}
|
||||
}
|
||||
|
||||
print("[EPLoginManager] Ticket 请求失败,仍跳转到首页")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Apple Login 接口占位(不实现)
|
||||
/// - Parameter viewController: 当前视图控制器
|
||||
static func loginWithApple(from viewController: UIViewController) {
|
||||
print("[EPLoginManager] Apple Login - 占位,Phase 2 实现")
|
||||
// 占位,打印 log
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
// Swift 侧不再重复实现 keyWindow 获取,统一调用 ObjC inline 函数 kGetKeyWindow()
|
||||
|
||||
/// 检查并显示专属颜色引导页
|
||||
private static func checkAndShowSignatureColorGuide(in window: UIWindow) {
|
||||
let hasSignatureColor = EPEmotionColorStorage.hasUserSignatureColor()
|
||||
|
||||
// #if DEBUG
|
||||
print("[EPLoginManager] Debug 模式:显示专属颜色引导页(已有颜色: \(hasSignatureColor))")
|
||||
|
||||
let guideView = EPSignatureColorGuideView()
|
||||
|
||||
// 设置颜色确认回调
|
||||
guideView.onColorConfirmed = { (hexColor: String) in
|
||||
EPEmotionColorStorage.saveUserSignatureColor(hexColor)
|
||||
print("[EPLoginManager] 用户选择专属颜色: \(hexColor)")
|
||||
}
|
||||
|
||||
// 如果已有颜色,设置 Skip 回调
|
||||
if hasSignatureColor {
|
||||
guideView.onSkipTapped = {
|
||||
print("[EPLoginManager] 用户跳过专属颜色选择")
|
||||
}
|
||||
}
|
||||
|
||||
// 显示引导页,已有颜色时显示 Skip 按钮
|
||||
guideView.show(in: window, showSkipButton: hasSignatureColor)
|
||||
|
||||
// #else
|
||||
// // Release 环境:仅在未设置专属颜色时显示
|
||||
// if !hasSignatureColor {
|
||||
// let guideView = EPSignatureColorGuideView()
|
||||
// guideView.onColorConfirmed = { (hexColor: String) in
|
||||
// EPEmotionColorStorage.saveUserSignatureColor(hexColor)
|
||||
// }
|
||||
// guideView.show(in: window)
|
||||
// }
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
|
||||
303
YuMi/E-P/NewLogin/Services/EPLoginService.swift
Normal file
303
YuMi/E-P/NewLogin/Services/EPLoginService.swift
Normal file
@@ -0,0 +1,303 @@
|
||||
//
|
||||
// EPLoginService.swift
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-01-27.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 登录服务封装(Swift 现代化版本)
|
||||
/// 统一封装所有登录相关 API,完全替代 OC 版本的 LoginPresenter
|
||||
@objc class EPLoginService: NSObject {
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
private let clientSecret = EPLoginConfig.API.clientSecret
|
||||
private let clientId = EPLoginConfig.API.clientId
|
||||
private let version = EPLoginConfig.API.version
|
||||
|
||||
// MARK: - Private Helper Methods
|
||||
|
||||
/// 解析并保存 AccountModel
|
||||
/// - Parameters:
|
||||
/// - data: API 返回的数据
|
||||
/// - code: 状态码
|
||||
/// - completion: 成功回调
|
||||
/// - failure: 失败回调
|
||||
private func parseAndSaveAccount(data: BaseModel?,
|
||||
code: Int64,
|
||||
completion: @escaping (AccountModel) -> Void,
|
||||
failure: @escaping (Int, String) -> Void) {
|
||||
if code == 200 {
|
||||
if let accountDict = data?.data as? NSDictionary,
|
||||
let accountModel = AccountModel.mj_object(withKeyValues: accountDict) {
|
||||
// 保存账号信息
|
||||
AccountInfoStorage.instance().saveAccountInfo(accountModel)
|
||||
completion(accountModel)
|
||||
} else {
|
||||
failure(Int(code), YMLocalizedString("error.account_parse_failed"))
|
||||
}
|
||||
} else {
|
||||
failure(Int(code), YMLocalizedString("error.operation_failed"))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Request Ticket
|
||||
|
||||
/// 请求 Ticket(登录成功后调用)
|
||||
/// - Parameters:
|
||||
/// - accessToken: 访问令牌
|
||||
/// - completion: 成功回调 (ticket)
|
||||
/// - failure: 失败回调 (错误码, 错误信息)
|
||||
@objc func requestTicket(accessToken: String,
|
||||
completion: @escaping (String) -> Void,
|
||||
failure: @escaping (Int, String) -> Void) {
|
||||
|
||||
Api.requestTicket({ (data, code, msg) in
|
||||
if code == 200, let dict = data?.data as? NSDictionary {
|
||||
if let tickets = dict["tickets"] as? NSArray,
|
||||
let firstTicket = tickets.firstObject as? NSDictionary,
|
||||
let ticket = firstTicket["ticket"] as? String {
|
||||
completion(ticket)
|
||||
} else {
|
||||
failure(Int(code), YMLocalizedString("error.ticket_parse_failed"))
|
||||
}
|
||||
} else {
|
||||
failure(Int(code), msg ?? YMLocalizedString("error.request_ticket_failed"))
|
||||
}
|
||||
}, access_token: accessToken, issue_type: "multi")
|
||||
}
|
||||
|
||||
// MARK: - Send Verification Code
|
||||
|
||||
/// 发送邮箱验证码
|
||||
/// - Parameters:
|
||||
/// - email: 邮箱地址
|
||||
/// - type: 类型 (1=登录, 2=找回密码)
|
||||
/// - completion: 成功回调
|
||||
/// - failure: 失败回调
|
||||
@objc func sendEmailCode(email: String,
|
||||
type: Int,
|
||||
completion: @escaping () -> Void,
|
||||
failure: @escaping (Int, String) -> Void) {
|
||||
|
||||
// 🔐 DES 加密邮箱
|
||||
let encryptedEmail = encryptDES(email)
|
||||
|
||||
Api.emailGetCode({ (data, code, msg) in
|
||||
if code == 200 {
|
||||
completion()
|
||||
} else {
|
||||
failure(Int(code), msg ?? YMLocalizedString("error.send_email_code_failed"))
|
||||
}
|
||||
}, emailAddress: encryptedEmail, type: NSNumber(value: type))
|
||||
}
|
||||
|
||||
/// 发送手机验证码
|
||||
/// - Parameters:
|
||||
/// - phone: 手机号
|
||||
/// - areaCode: 区号
|
||||
/// - type: 类型 (1=登录, 2=找回密码)
|
||||
/// - completion: 成功回调
|
||||
/// - failure: 失败回调
|
||||
@objc func sendPhoneCode(phone: String,
|
||||
areaCode: String,
|
||||
type: Int,
|
||||
completion: @escaping () -> Void,
|
||||
failure: @escaping (Int, String) -> Void) {
|
||||
|
||||
// 🔐 DES 加密手机号
|
||||
let encryptedPhone = encryptDES(phone)
|
||||
|
||||
Api.phoneSmsCode({ (data, code, msg) in
|
||||
if code == 200 {
|
||||
completion()
|
||||
} else {
|
||||
failure(Int(code), msg ?? YMLocalizedString("error.send_phone_code_failed"))
|
||||
}
|
||||
}, mobile: encryptedPhone, type: String(type), phoneAreaCode: areaCode)
|
||||
}
|
||||
|
||||
// MARK: - Login Methods
|
||||
|
||||
/// ID + 密码登录
|
||||
/// - Parameters:
|
||||
/// - id: 用户 ID
|
||||
/// - password: 密码
|
||||
/// - completion: 成功回调 (AccountModel)
|
||||
/// - failure: 失败回调
|
||||
@objc func loginWithID(id: String,
|
||||
password: String,
|
||||
completion: @escaping (AccountModel) -> Void,
|
||||
failure: @escaping (Int, String) -> Void) {
|
||||
|
||||
// 🔐 DES 加密 ID 和密码
|
||||
let encryptedId = encryptDES(id)
|
||||
let encryptedPassword = encryptDES(password)
|
||||
|
||||
Api.login(password: { [weak self] (data, code, msg) in
|
||||
self?.parseAndSaveAccount(
|
||||
data: data,
|
||||
code: Int64(code),
|
||||
completion: completion,
|
||||
failure: { errorCode, _ in
|
||||
failure(errorCode, msg ?? YMLocalizedString("error.login_failed"))
|
||||
})
|
||||
},
|
||||
phone: encryptedId,
|
||||
password: encryptedPassword,
|
||||
client_secret: clientSecret,
|
||||
version: version,
|
||||
client_id: clientId,
|
||||
grant_type: "password")
|
||||
}
|
||||
|
||||
/// 邮箱 + 验证码登录
|
||||
/// - Parameters:
|
||||
/// - email: 邮箱地址
|
||||
/// - code: 验证码
|
||||
/// - completion: 成功回调 (AccountModel)
|
||||
/// - failure: 失败回调
|
||||
@objc func loginWithEmail(email: String,
|
||||
code: String,
|
||||
completion: @escaping (AccountModel) -> Void,
|
||||
failure: @escaping (Int, String) -> Void) {
|
||||
|
||||
// 🔐 DES 加密邮箱
|
||||
let encryptedEmail = encryptDES(email)
|
||||
|
||||
Api.login(code: { [weak self] (data, code, msg) in
|
||||
self?.parseAndSaveAccount(
|
||||
data: data,
|
||||
code: Int64(code),
|
||||
completion: completion,
|
||||
failure: { errorCode, _ in
|
||||
failure(errorCode, msg ?? YMLocalizedString("error.login_failed"))
|
||||
})
|
||||
},
|
||||
email: encryptedEmail,
|
||||
code: code,
|
||||
client_secret: clientSecret,
|
||||
version: version,
|
||||
client_id: clientId,
|
||||
grant_type: "email")
|
||||
}
|
||||
|
||||
/// 手机号 + 验证码登录
|
||||
/// - Parameters:
|
||||
/// - phone: 手机号
|
||||
/// - code: 验证码
|
||||
/// - areaCode: 区号
|
||||
/// - completion: 成功回调 (AccountModel)
|
||||
/// - failure: 失败回调
|
||||
@objc func loginWithPhone(phone: String,
|
||||
code: String,
|
||||
areaCode: String,
|
||||
completion: @escaping (AccountModel) -> Void,
|
||||
failure: @escaping (Int, String) -> Void) {
|
||||
|
||||
// 🔐 DES 加密手机号
|
||||
let encryptedPhone = encryptDES(phone)
|
||||
|
||||
Api.login(code: { [weak self] (data, code, msg) in
|
||||
self?.parseAndSaveAccount(
|
||||
data: data,
|
||||
code: Int64(code),
|
||||
completion: completion,
|
||||
failure: { errorCode, _ in
|
||||
failure(errorCode, msg ?? YMLocalizedString("error.login_failed"))
|
||||
})
|
||||
},
|
||||
phone: encryptedPhone,
|
||||
code: code,
|
||||
client_secret: clientSecret,
|
||||
version: version,
|
||||
client_id: clientId,
|
||||
grant_type: "password",
|
||||
phoneAreaCode: areaCode)
|
||||
}
|
||||
|
||||
// MARK: - Reset Password
|
||||
|
||||
/// 邮箱重置密码
|
||||
/// - Parameters:
|
||||
/// - email: 邮箱地址
|
||||
/// - code: 验证码
|
||||
/// - newPassword: 新密码
|
||||
/// - completion: 成功回调
|
||||
/// - failure: 失败回调
|
||||
@objc func resetEmailPassword(email: String,
|
||||
code: String,
|
||||
newPassword: String,
|
||||
completion: @escaping () -> Void,
|
||||
failure: @escaping (Int, String) -> Void) {
|
||||
|
||||
// 🔐 DES 加密邮箱和新密码
|
||||
let encryptedEmail = encryptDES(email)
|
||||
let encryptedPassword = encryptDES(newPassword)
|
||||
|
||||
Api.resetPassword(email: { (data, code, msg) in
|
||||
if code == 200 {
|
||||
completion()
|
||||
} else {
|
||||
failure(Int(code), msg ?? YMLocalizedString("error.reset_password_failed"))
|
||||
}
|
||||
}, email: encryptedEmail, newPwd: encryptedPassword, code: code)
|
||||
}
|
||||
|
||||
/// 手机号重置密码
|
||||
/// - Parameters:
|
||||
/// - phone: 手机号
|
||||
/// - code: 验证码
|
||||
/// - areaCode: 区号
|
||||
/// - newPassword: 新密码
|
||||
/// - completion: 成功回调
|
||||
/// - failure: 失败回调
|
||||
@objc func resetPhonePassword(phone: String,
|
||||
code: String,
|
||||
areaCode: String,
|
||||
newPassword: String,
|
||||
completion: @escaping () -> Void,
|
||||
failure: @escaping (Int, String) -> Void) {
|
||||
|
||||
// 🔐 DES 加密手机号和新密码
|
||||
let encryptedPhone = encryptDES(phone)
|
||||
let encryptedPassword = encryptDES(newPassword)
|
||||
|
||||
Api.resetPassword(phone: { (data, code, msg) in
|
||||
if code == 200 {
|
||||
completion()
|
||||
} else {
|
||||
failure(Int(code), msg ?? YMLocalizedString("error.reset_password_failed"))
|
||||
}
|
||||
}, phone: encryptedPhone, newPwd: encryptedPassword, smsCode: code, phoneAreaCode: areaCode)
|
||||
}
|
||||
|
||||
// MARK: - Phone Quick Login (保留接口)
|
||||
|
||||
/// 手机快速登录(保留接口但 UI 暂不暴露)
|
||||
/// - Parameters:
|
||||
/// - accessToken: 访问令牌
|
||||
/// - token: 令牌
|
||||
/// - completion: 成功回调 (AccountModel)
|
||||
/// - failure: 失败回调
|
||||
@objc func phoneQuickLogin(accessToken: String,
|
||||
token: String,
|
||||
completion: @escaping (AccountModel) -> Void,
|
||||
failure: @escaping (Int, String) -> Void) {
|
||||
|
||||
Api.phoneQuickLogin({ [weak self] (data, code, msg) in
|
||||
self?.parseAndSaveAccount(
|
||||
data: data,
|
||||
code: Int64(code),
|
||||
completion: completion,
|
||||
failure: { errorCode, _ in
|
||||
failure(errorCode, msg ?? YMLocalizedString("error.quick_login_failed"))
|
||||
})
|
||||
},
|
||||
accessToken: accessToken,
|
||||
token: token)
|
||||
}
|
||||
}
|
||||
|
||||
131
YuMi/E-P/NewLogin/Views/EPLoginButton.swift
Normal file
131
YuMi/E-P/NewLogin/Views/EPLoginButton.swift
Normal file
@@ -0,0 +1,131 @@
|
||||
//
|
||||
// EPLoginButton.swift
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-01-27.
|
||||
// 登录按钮组件 - 使用 StackView 实现 icon 左侧固定 + title 居中
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
/// 登录按钮点击代理
|
||||
protocol EPLoginButtonDelegate: AnyObject {
|
||||
func loginButtonDidTap(_ button: EPLoginButton)
|
||||
}
|
||||
|
||||
/// 登录按钮组件
|
||||
class EPLoginButton: UIControl {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
weak var delegate: EPLoginButtonDelegate?
|
||||
|
||||
private let stackView = UIStackView()
|
||||
private let iconImageView = UIImageView()
|
||||
private let titleLabel = UILabel()
|
||||
private let leftSpacer = UIView()
|
||||
private let rightSpacer = UIView()
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
private func setupUI() {
|
||||
backgroundColor = EPLoginConfig.Colors.background
|
||||
layer.cornerRadius = EPLoginConfig.Layout.cornerRadius
|
||||
|
||||
// StackView 配置
|
||||
stackView.axis = .horizontal
|
||||
stackView.alignment = .center
|
||||
stackView.distribution = .fill
|
||||
stackView.spacing = 0
|
||||
stackView.isUserInteractionEnabled = false
|
||||
addSubview(stackView)
|
||||
|
||||
// Icon
|
||||
iconImageView.contentMode = .scaleAspectFit
|
||||
|
||||
// Title
|
||||
titleLabel.font = .systemFont(ofSize: EPLoginConfig.Layout.inputFontSize, weight: .semibold)
|
||||
titleLabel.textColor = EPLoginConfig.Colors.text
|
||||
titleLabel.textAlignment = .center
|
||||
|
||||
// Spacers - 让 title 居中
|
||||
leftSpacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
rightSpacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
|
||||
// 布局顺序: [Leading 33] + [Icon] + [Flexible Spacer] + [Title] + [Flexible Spacer] + [Trailing 33]
|
||||
let leadingPadding = UIView()
|
||||
let trailingPadding = UIView()
|
||||
|
||||
stackView.addArrangedSubview(leadingPadding)
|
||||
stackView.addArrangedSubview(iconImageView)
|
||||
stackView.addArrangedSubview(leftSpacer)
|
||||
stackView.addArrangedSubview(titleLabel)
|
||||
stackView.addArrangedSubview(rightSpacer)
|
||||
stackView.addArrangedSubview(trailingPadding)
|
||||
|
||||
// 约束
|
||||
stackView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
|
||||
leadingPadding.snp.makeConstraints { make in
|
||||
make.width.equalTo(EPLoginConfig.Layout.loginButtonIconLeading)
|
||||
}
|
||||
|
||||
iconImageView.snp.makeConstraints { make in
|
||||
make.size.equalTo(EPLoginConfig.Layout.loginButtonIconSize)
|
||||
}
|
||||
|
||||
trailingPadding.snp.makeConstraints { make in
|
||||
make.width.equalTo(EPLoginConfig.Layout.loginButtonIconLeading)
|
||||
}
|
||||
|
||||
// 设置 leftSpacer 和 rightSpacer 宽度相等,实现 title 居中
|
||||
leftSpacer.snp.makeConstraints { make in
|
||||
make.width.equalTo(rightSpacer)
|
||||
}
|
||||
|
||||
// 添加点击事件
|
||||
addTarget(self, action: #selector(handleTap), for: .touchUpInside)
|
||||
}
|
||||
|
||||
// MARK: - Configuration
|
||||
|
||||
/// 配置按钮
|
||||
/// - Parameters:
|
||||
/// - icon: 图标名称
|
||||
/// - title: 标题文字
|
||||
func configure(icon: String, title: String) {
|
||||
iconImageView.image = kImage(icon)
|
||||
titleLabel.text = title
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@objc private func handleTap() {
|
||||
delegate?.loginButtonDidTap(self)
|
||||
}
|
||||
|
||||
// MARK: - Touch Feedback
|
||||
|
||||
override var isHighlighted: Bool {
|
||||
didSet {
|
||||
UIView.animate(withDuration: 0.1) {
|
||||
self.alpha = self.isHighlighted ? 0.7 : 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
322
YuMi/E-P/NewLogin/Views/EPLoginInputView.swift
Normal file
322
YuMi/E-P/NewLogin/Views/EPLoginInputView.swift
Normal file
@@ -0,0 +1,322 @@
|
||||
//
|
||||
// EPLoginInputView.swift
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-01-27.
|
||||
// 登录输入框组件 - 支持区号、验证码、密码切换等完整功能
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
/// 输入框配置
|
||||
struct EPLoginInputConfig {
|
||||
var showAreaCode: Bool = false
|
||||
var showCodeButton: Bool = false
|
||||
var isSecure: Bool = false
|
||||
var icon: String?
|
||||
var placeholder: String
|
||||
var keyboardType: UIKeyboardType = .default
|
||||
}
|
||||
|
||||
/// 输入框代理
|
||||
protocol EPLoginInputViewDelegate: AnyObject {
|
||||
func inputViewDidRequestCode(_ inputView: EPLoginInputView)
|
||||
func inputViewDidSelectArea(_ inputView: EPLoginInputView)
|
||||
}
|
||||
|
||||
/// 登录输入框组件
|
||||
class EPLoginInputView: UIView {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
weak var delegate: EPLoginInputViewDelegate?
|
||||
|
||||
/// 输入内容变化回调
|
||||
var onTextChanged: ((String) -> Void)?
|
||||
|
||||
private let stackView = UIStackView()
|
||||
|
||||
// 区号区域
|
||||
private let areaStackView = UIStackView()
|
||||
private let areaCodeButton = UIButton(type: .custom)
|
||||
private let areaArrowImageView = UIImageView()
|
||||
private let areaTapButton = UIButton(type: .custom)
|
||||
|
||||
// 输入框
|
||||
private let inputTextField = UITextField()
|
||||
private let iconImageView = UIImageView()
|
||||
|
||||
// 眼睛按钮(密码可见性切换)
|
||||
private let eyeButton = UIButton(type: .custom)
|
||||
|
||||
// 验证码按钮
|
||||
private let codeButton = UIButton(type: .custom)
|
||||
|
||||
// 倒计时
|
||||
private var timer: DispatchSourceTimer?
|
||||
private var countdownSeconds = 60
|
||||
private var isCountingDown = false
|
||||
|
||||
// 配置
|
||||
private var config: EPLoginInputConfig?
|
||||
|
||||
/// 获取输入内容
|
||||
var text: String {
|
||||
return inputTextField.text ?? ""
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
stopCountdown()
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
private func setupUI() {
|
||||
backgroundColor = EPLoginConfig.Colors.inputBackground
|
||||
layer.cornerRadius = EPLoginConfig.Layout.inputCornerRadius
|
||||
layer.borderWidth = EPLoginConfig.Layout.inputBorderWidth
|
||||
layer.borderColor = EPLoginConfig.Colors.inputBorder.cgColor
|
||||
|
||||
// Main StackView
|
||||
stackView.axis = .horizontal
|
||||
stackView.alignment = .center
|
||||
stackView.distribution = .fill
|
||||
stackView.spacing = 8
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(stackView)
|
||||
|
||||
setupAreaCodeView()
|
||||
setupInputTextField()
|
||||
setupEyeButton()
|
||||
setupCodeButton()
|
||||
|
||||
stackView.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(EPLoginConfig.Layout.inputHorizontalPadding)
|
||||
make.trailing.equalToSuperview().offset(-EPLoginConfig.Layout.inputHorizontalPadding)
|
||||
make.top.bottom.equalToSuperview()
|
||||
}
|
||||
|
||||
// 默认隐藏所有可选组件
|
||||
areaStackView.isHidden = true
|
||||
eyeButton.isHidden = true
|
||||
codeButton.isHidden = true
|
||||
iconImageView.isHidden = true
|
||||
}
|
||||
|
||||
private func setupAreaCodeView() {
|
||||
// 区号 StackView
|
||||
areaStackView.axis = .horizontal
|
||||
areaStackView.alignment = .center
|
||||
areaStackView.distribution = .fill
|
||||
areaStackView.spacing = 8
|
||||
areaStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// 区号按钮
|
||||
areaCodeButton.setTitle("+86", for: .normal)
|
||||
areaCodeButton.setTitleColor(EPLoginConfig.Colors.inputText, for: .normal)
|
||||
areaCodeButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
areaCodeButton.isUserInteractionEnabled = false
|
||||
areaCodeButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// 箭头图标
|
||||
areaArrowImageView.image = kImage("login_area_arrow")
|
||||
areaArrowImageView.contentMode = .scaleAspectFit
|
||||
areaArrowImageView.isUserInteractionEnabled = false
|
||||
areaArrowImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// 点击区域按钮
|
||||
areaTapButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
areaTapButton.addTarget(self, action: #selector(handleAreaTap), for: .touchUpInside)
|
||||
|
||||
areaStackView.addSubview(areaTapButton)
|
||||
areaStackView.addArrangedSubview(areaCodeButton)
|
||||
areaStackView.addArrangedSubview(areaArrowImageView)
|
||||
|
||||
stackView.addArrangedSubview(areaStackView)
|
||||
|
||||
areaTapButton.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
|
||||
areaCodeButton.snp.makeConstraints { make in
|
||||
make.width.lessThanOrEqualTo(60)
|
||||
}
|
||||
|
||||
areaArrowImageView.snp.makeConstraints { make in
|
||||
make.width.equalTo(12)
|
||||
make.height.equalTo(8)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupInputTextField() {
|
||||
// Icon (可选)
|
||||
iconImageView.contentMode = .scaleAspectFit
|
||||
iconImageView.tintColor = EPLoginConfig.Colors.icon
|
||||
iconImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
stackView.addArrangedSubview(iconImageView)
|
||||
|
||||
iconImageView.snp.makeConstraints { make in
|
||||
make.size.equalTo(EPLoginConfig.Layout.inputIconSize)
|
||||
}
|
||||
|
||||
// TextField
|
||||
inputTextField.textColor = EPLoginConfig.Colors.textLight
|
||||
inputTextField.font = .systemFont(ofSize: 14)
|
||||
inputTextField.tintColor = EPLoginConfig.Colors.textLight
|
||||
inputTextField.translatesAutoresizingMaskIntoConstraints = false
|
||||
inputTextField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
|
||||
stackView.addArrangedSubview(inputTextField)
|
||||
}
|
||||
|
||||
@objc private func textFieldDidChange() {
|
||||
onTextChanged?(inputTextField.text ?? "")
|
||||
}
|
||||
|
||||
private func setupEyeButton() {
|
||||
eyeButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
eyeButton.setImage(kImage(EPLoginConfig.Images.iconPasswordUnsee), for: .normal)
|
||||
eyeButton.setImage(kImage(EPLoginConfig.Images.iconPasswordSee), for: .selected)
|
||||
eyeButton.addTarget(self, action: #selector(handleEyeTap), for: .touchUpInside)
|
||||
stackView.addArrangedSubview(eyeButton)
|
||||
|
||||
eyeButton.snp.makeConstraints { make in
|
||||
make.size.equalTo(24)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupCodeButton() {
|
||||
codeButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
codeButton.setTitle(YMLocalizedString("XPLoginInputView0"), for: .normal)
|
||||
codeButton.setTitleColor(.white, for: .normal)
|
||||
codeButton.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
codeButton.titleLabel?.textAlignment = .center
|
||||
codeButton.titleLabel?.numberOfLines = 2
|
||||
codeButton.layer.cornerRadius = EPLoginConfig.Layout.codeButtonHeight / 2
|
||||
codeButton.backgroundColor = EPLoginConfig.Colors.codeButtonBackground
|
||||
codeButton.addTarget(self, action: #selector(handleCodeTap), for: .touchUpInside)
|
||||
stackView.addArrangedSubview(codeButton)
|
||||
|
||||
codeButton.snp.makeConstraints { make in
|
||||
make.width.equalTo(EPLoginConfig.Layout.codeButtonWidth)
|
||||
make.height.equalTo(EPLoginConfig.Layout.codeButtonHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Configuration
|
||||
|
||||
/// 配置输入框
|
||||
func configure(with config: EPLoginInputConfig) {
|
||||
self.config = config
|
||||
|
||||
// 区号
|
||||
areaStackView.isHidden = !config.showAreaCode
|
||||
|
||||
// Icon - 默认隐藏,不再使用
|
||||
iconImageView.isHidden = true
|
||||
|
||||
// Placeholder(60% 白色)
|
||||
inputTextField.attributedPlaceholder = NSAttributedString(
|
||||
string: config.placeholder,
|
||||
attributes: [NSAttributedString.Key.foregroundColor: UIColor.white.withAlphaComponent(0.6)]
|
||||
)
|
||||
|
||||
// 键盘类型
|
||||
inputTextField.keyboardType = config.keyboardType
|
||||
|
||||
// 密码模式
|
||||
inputTextField.isSecureTextEntry = config.isSecure
|
||||
eyeButton.isHidden = !config.isSecure
|
||||
|
||||
// 验证码按钮
|
||||
codeButton.isHidden = !config.showCodeButton
|
||||
}
|
||||
|
||||
/// 设置区号
|
||||
func setAreaCode(_ code: String) {
|
||||
areaCodeButton.setTitle(code, for: .normal)
|
||||
}
|
||||
|
||||
/// 清空输入
|
||||
func clearInput() {
|
||||
inputTextField.text = ""
|
||||
}
|
||||
|
||||
/// 弹出键盘(自动聚焦输入框)
|
||||
func displayKeyboard() {
|
||||
inputTextField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@objc private func handleAreaTap() {
|
||||
delegate?.inputViewDidSelectArea(self)
|
||||
}
|
||||
|
||||
@objc private func handleEyeTap() {
|
||||
eyeButton.isSelected.toggle()
|
||||
inputTextField.isSecureTextEntry = !eyeButton.isSelected
|
||||
}
|
||||
|
||||
@objc private func handleCodeTap() {
|
||||
guard !isCountingDown else { return }
|
||||
delegate?.inputViewDidRequestCode(self)
|
||||
}
|
||||
|
||||
// MARK: - Countdown
|
||||
|
||||
/// 开始倒计时
|
||||
func startCountdown() {
|
||||
guard !isCountingDown else { return }
|
||||
|
||||
isCountingDown = true
|
||||
countdownSeconds = 60
|
||||
codeButton.isEnabled = false
|
||||
codeButton.backgroundColor = EPLoginConfig.Colors.iconDisabled
|
||||
|
||||
let queue = DispatchQueue.main
|
||||
let timer = DispatchSource.makeTimerSource(queue: queue)
|
||||
timer.schedule(deadline: .now(), repeating: 1.0)
|
||||
|
||||
timer.setEventHandler { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.countdownSeconds -= 1
|
||||
|
||||
if self.countdownSeconds <= 0 {
|
||||
self.stopCountdown()
|
||||
self.codeButton.setTitle(YMLocalizedString("XPLoginInputView1"), for: .normal)
|
||||
} else {
|
||||
self.codeButton.setTitle("\(self.countdownSeconds)s", for: .normal)
|
||||
}
|
||||
}
|
||||
|
||||
timer.resume()
|
||||
self.timer = timer
|
||||
}
|
||||
|
||||
/// 停止倒计时
|
||||
func stopCountdown() {
|
||||
guard let timer = timer else { return }
|
||||
|
||||
timer.cancel()
|
||||
self.timer = nil
|
||||
isCountingDown = false
|
||||
|
||||
codeButton.isEnabled = true
|
||||
codeButton.backgroundColor = EPLoginConfig.Colors.codeButtonBackground
|
||||
codeButton.setTitle(YMLocalizedString("XPLoginInputView0"), for: .normal)
|
||||
}
|
||||
}
|
||||
|
||||
151
YuMi/E-P/NewLogin/Views/EPPolicyLabel.swift
Normal file
151
YuMi/E-P/NewLogin/Views/EPPolicyLabel.swift
Normal file
@@ -0,0 +1,151 @@
|
||||
//
|
||||
// EPPolicyLabel.swift
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-01-27.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class EPPolicyLabel: UILabel {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
var onUserAgreementTapped: (() -> Void)?
|
||||
var onPrivacyPolicyTapped: (() -> Void)?
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setup()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setup()
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
private func setup() {
|
||||
numberOfLines = 0
|
||||
isUserInteractionEnabled = true
|
||||
|
||||
// 使用 YMLocalizedString 获取文案
|
||||
let fullText = YMLocalizedString("XPLoginViewController6")
|
||||
let userAgreementText = YMLocalizedString("XPLoginViewController7")
|
||||
let privacyPolicyText = YMLocalizedString("XPLoginViewController9")
|
||||
|
||||
let attributedString = NSMutableAttributedString(string: fullText)
|
||||
attributedString.addAttribute(NSAttributedString.Key.foregroundColor,
|
||||
value: UIColor.darkGray,
|
||||
range: NSRange(location: 0, length: fullText.count))
|
||||
attributedString.addAttribute(NSAttributedString.Key.font,
|
||||
value: UIFont.systemFont(ofSize: 12),
|
||||
range: NSRange(location: 0, length: fullText.count))
|
||||
|
||||
// 高亮用户协议(蓝色)
|
||||
if let userRange = fullText.range(of: userAgreementText) {
|
||||
let nsRange = NSRange(userRange, in: fullText)
|
||||
attributedString.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.systemBlue, range: nsRange)
|
||||
attributedString.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: nsRange)
|
||||
}
|
||||
|
||||
// 高亮隐私政策(蓝色)
|
||||
if let privacyRange = fullText.range(of: privacyPolicyText) {
|
||||
let nsRange = NSRange(privacyRange, in: fullText)
|
||||
attributedString.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.systemBlue, range: nsRange)
|
||||
attributedString.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: nsRange)
|
||||
}
|
||||
|
||||
attributedText = attributedString
|
||||
|
||||
// 添加点击手势
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
|
||||
addGestureRecognizer(tapGesture)
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@objc private func handleTap(_ gesture: UITapGestureRecognizer) {
|
||||
guard let attributedText = self.attributedText else {
|
||||
print("[EPPolicyLabel] No attributed text")
|
||||
return
|
||||
}
|
||||
|
||||
let text = attributedText.string
|
||||
let userAgreementText = YMLocalizedString("XPLoginViewController7")
|
||||
let privacyPolicyText = YMLocalizedString("XPLoginViewController9")
|
||||
|
||||
print("[EPPolicyLabel] Tap detected, text: \(text)")
|
||||
|
||||
let layoutManager = NSLayoutManager()
|
||||
let textContainer = NSTextContainer(size: bounds.size)
|
||||
let textStorage = NSTextStorage(attributedString: attributedText)
|
||||
|
||||
layoutManager.addTextContainer(textContainer)
|
||||
textStorage.addLayoutManager(layoutManager)
|
||||
|
||||
textContainer.lineFragmentPadding = 0
|
||||
textContainer.maximumNumberOfLines = numberOfLines
|
||||
textContainer.lineBreakMode = lineBreakMode
|
||||
|
||||
let locationOfTouchInLabel = gesture.location(in: self)
|
||||
let textBoundingBox = layoutManager.usedRect(for: textContainer)
|
||||
|
||||
// 根据 textAlignment 计算偏移
|
||||
var textContainerOffset = CGPoint.zero
|
||||
switch textAlignment {
|
||||
case .left, .natural, .justified:
|
||||
textContainerOffset = CGPoint(x: 0, y: (bounds.height - textBoundingBox.height) / 2)
|
||||
case .center:
|
||||
textContainerOffset = CGPoint(x: (bounds.width - textBoundingBox.width) / 2,
|
||||
y: (bounds.height - textBoundingBox.height) / 2)
|
||||
case .right:
|
||||
textContainerOffset = CGPoint(x: bounds.width - textBoundingBox.width,
|
||||
y: (bounds.height - textBoundingBox.height) / 2)
|
||||
@unknown default:
|
||||
textContainerOffset = CGPoint(x: 0, y: (bounds.height - textBoundingBox.height) / 2)
|
||||
}
|
||||
|
||||
let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x,
|
||||
y: locationOfTouchInLabel.y - textContainerOffset.y)
|
||||
|
||||
// 确保点击在文本区域内
|
||||
guard textBoundingBox.contains(locationOfTouchInTextContainer) else {
|
||||
print("[EPPolicyLabel] Tap outside text bounds")
|
||||
return
|
||||
}
|
||||
|
||||
let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer,
|
||||
in: textContainer,
|
||||
fractionOfDistanceBetweenInsertionPoints: nil)
|
||||
|
||||
print("[EPPolicyLabel] Character index: \(indexOfCharacter)")
|
||||
|
||||
// 检查点击位置
|
||||
if let userRange = text.range(of: userAgreementText) {
|
||||
let nsRange = NSRange(userRange, in: text)
|
||||
print("[EPPolicyLabel] User agreement range: \(nsRange)")
|
||||
if NSLocationInRange(indexOfCharacter, nsRange) {
|
||||
print("[EPPolicyLabel] User agreement tapped!")
|
||||
onUserAgreementTapped?()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if let privacyRange = text.range(of: privacyPolicyText) {
|
||||
let nsRange = NSRange(privacyRange, in: text)
|
||||
print("[EPPolicyLabel] Privacy policy range: \(nsRange)")
|
||||
if NSLocationInRange(indexOfCharacter, nsRange) {
|
||||
print("[EPPolicyLabel] Privacy policy tapped!")
|
||||
onPrivacyPolicyTapped?()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
print("[EPPolicyLabel] No link tapped")
|
||||
}
|
||||
}
|
||||
|
||||
54
YuMi/E-P/NewMessage/EPBaseListViewController.swift
Normal file
54
YuMi/E-P/NewMessage/EPBaseListViewController.swift
Normal file
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// EPBaseListViewController.swift
|
||||
// YuMi
|
||||
//
|
||||
// A lightweight table-view base class used by EP Message subpages.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
class EPBaseListViewController<Cell: UITableViewCell>: UIViewController, UITableViewDataSource, UITableViewDelegate {
|
||||
let tableView = UITableView(frame: .zero, style: .plain)
|
||||
var itemsCount: Int = 0 { didSet { tableView.reloadData() } }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = UIColor(named: "ep.background.dark") ?? UIColor.black.withAlphaComponent(0.9)
|
||||
|
||||
tableView.backgroundColor = .clear
|
||||
tableView.separatorStyle = .none
|
||||
tableView.showsVerticalScrollIndicator = false
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
tableView.rowHeight = 72
|
||||
tableView.contentInsetAdjustmentBehavior = .never
|
||||
tableView.keyboardDismissMode = .onDrag
|
||||
tableView.register(Cell.self, forCellReuseIdentifier: "cell")
|
||||
|
||||
view.addSubview(tableView)
|
||||
tableView.snp.makeConstraints { make in
|
||||
make.edges.equalTo(view.safeAreaLayoutGuide)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSource
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return itemsCount
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! Cell
|
||||
cell.backgroundColor = .clear
|
||||
return cell
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
func simulateItems(_ count: Int) {
|
||||
itemsCount = count
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
126
YuMi/E-P/NewMessage/EPFriendFollowingFans.swift
Normal file
126
YuMi/E-P/NewMessage/EPFriendFollowingFans.swift
Normal file
@@ -0,0 +1,126 @@
|
||||
import UIKit
|
||||
|
||||
final class EPFriendListVC: EPBaseListViewController<EPUserBriefCell> {
|
||||
override func viewDidLoad() { super.viewDidLoad(); simulateItems(6) }
|
||||
}
|
||||
|
||||
final class EPFollowingListVC: EPBaseListViewController<EPUserBriefCell> {
|
||||
override func viewDidLoad() { super.viewDidLoad(); simulateItems(10) }
|
||||
}
|
||||
|
||||
final class EPFansListVC: EPBaseListViewController<EPUserBriefCell> {
|
||||
override func viewDidLoad() { super.viewDidLoad(); simulateItems(12) }
|
||||
}
|
||||
|
||||
final class EPUserBriefCell: UITableViewCell {
|
||||
private let avatar = UIImageView()
|
||||
private let nameLabel = UILabel()
|
||||
private let subtitleLabel = UILabel()
|
||||
private let followButton = EPFollowButton()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
setup()
|
||||
}
|
||||
required init?(coder: NSCoder) { super.init(coder: coder); setup() }
|
||||
|
||||
private func setup() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
|
||||
avatar.contentMode = .scaleAspectFill
|
||||
avatar.layer.cornerRadius = 24
|
||||
avatar.layer.masksToBounds = true
|
||||
avatar.image = UIImage(named: "pi_login_new_logo")
|
||||
|
||||
nameLabel.font = .systemFont(ofSize: 20, weight: .semibold)
|
||||
nameLabel.textColor = .white
|
||||
nameLabel.text = "Momoyy"
|
||||
|
||||
subtitleLabel.font = .systemFont(ofSize: 16)
|
||||
subtitleLabel.textColor = UIColor.white.withAlphaComponent(0.6)
|
||||
subtitleLabel.text = "Welcome to play"
|
||||
|
||||
contentView.addSubview(avatar)
|
||||
contentView.addSubview(nameLabel)
|
||||
contentView.addSubview(subtitleLabel)
|
||||
contentView.addSubview(followButton)
|
||||
|
||||
avatar.translatesAutoresizingMaskIntoConstraints = false
|
||||
nameLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
followButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
avatar.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
|
||||
avatar.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
avatar.widthAnchor.constraint(equalToConstant: 48),
|
||||
avatar.heightAnchor.constraint(equalToConstant: 48),
|
||||
|
||||
nameLabel.leadingAnchor.constraint(equalTo: avatar.trailingAnchor, constant: 12),
|
||||
nameLabel.topAnchor.constraint(equalTo: avatar.topAnchor, constant: -2),
|
||||
|
||||
subtitleLabel.leadingAnchor.constraint(equalTo: nameLabel.leadingAnchor),
|
||||
subtitleLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 6),
|
||||
|
||||
followButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
|
||||
followButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
followButton.widthAnchor.constraint(equalToConstant: 120),
|
||||
followButton.heightAnchor.constraint(equalToConstant: 40)
|
||||
])
|
||||
|
||||
followButton.setFollowed(false)
|
||||
}
|
||||
}
|
||||
|
||||
final class EPFollowButton: UIButton {
|
||||
private var isFollowedState: Bool = false
|
||||
|
||||
override init(frame: CGRect) { super.init(frame: frame); setup() }
|
||||
required init?(coder: NSCoder) { super.init(coder: coder); setup() }
|
||||
|
||||
private func setup() {
|
||||
titleLabel?.font = .systemFont(ofSize: 18, weight: .semibold)
|
||||
layer.cornerRadius = 20
|
||||
layer.masksToBounds = true
|
||||
addTarget(self, action: #selector(onTap), for: .touchUpInside)
|
||||
}
|
||||
|
||||
func setFollowed(_ followed: Bool) {
|
||||
isFollowedState = followed
|
||||
if followed {
|
||||
setTitle("Followed", for: .normal)
|
||||
backgroundColor = .clear
|
||||
layer.borderWidth = 1
|
||||
layer.borderColor = UIColor.systemPurple.withAlphaComponent(0.6).cgColor
|
||||
setTitleColor(UIColor.systemPurple.withAlphaComponent(0.9), for: .normal)
|
||||
} else {
|
||||
setTitle("Follow", for: .normal)
|
||||
layer.borderWidth = 0
|
||||
setTitleColor(.white, for: .normal)
|
||||
setGradientBackground()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func onTap() { setFollowed(!isFollowedState) }
|
||||
|
||||
private func setGradientBackground() {
|
||||
let gradient = CAGradientLayer()
|
||||
gradient.colors = [UIColor.systemPink.cgColor, UIColor.systemPurple.cgColor]
|
||||
gradient.startPoint = CGPoint(x: 0, y: 0.5)
|
||||
gradient.endPoint = CGPoint(x: 1, y: 0.5)
|
||||
gradient.frame = bounds
|
||||
gradient.cornerRadius = layer.cornerRadius
|
||||
layer.sublayers?.removeAll(where: { $0 is CAGradientLayer })
|
||||
layer.insertSublayer(gradient, at: 0)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
if !isFollowedState { setGradientBackground() }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
90
YuMi/E-P/NewMessage/EPMessageListVC.swift
Normal file
90
YuMi/E-P/NewMessage/EPMessageListVC.swift
Normal file
@@ -0,0 +1,90 @@
|
||||
import UIKit
|
||||
|
||||
final class EPMessageListVC: EPBaseListViewController<EPMessageCell> {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
simulateItems(8)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Cell
|
||||
final class EPMessageCell: UITableViewCell {
|
||||
private let avatar = UIImageView()
|
||||
private let nameLabel = UILabel()
|
||||
private let subtitleLabel = UILabel()
|
||||
private let timeLabel = UILabel()
|
||||
private let unreadView = UILabel()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
setup()
|
||||
}
|
||||
required init?(coder: NSCoder) { super.init(coder: coder); setup() }
|
||||
|
||||
private func setup() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
|
||||
avatar.contentMode = .scaleAspectFill
|
||||
avatar.layer.cornerRadius = 24
|
||||
avatar.layer.masksToBounds = true
|
||||
avatar.image = UIImage(named: "pi_login_new_logo")
|
||||
|
||||
nameLabel.font = .systemFont(ofSize: 20, weight: .semibold)
|
||||
nameLabel.textColor = .white
|
||||
nameLabel.text = "Momoyy"
|
||||
|
||||
subtitleLabel.font = .systemFont(ofSize: 16)
|
||||
subtitleLabel.textColor = UIColor.white.withAlphaComponent(0.6)
|
||||
subtitleLabel.text = "Nice to meet you"
|
||||
|
||||
timeLabel.font = .systemFont(ofSize: 14)
|
||||
timeLabel.textColor = UIColor.white.withAlphaComponent(0.5)
|
||||
timeLabel.text = "11:03"
|
||||
|
||||
unreadView.backgroundColor = UIColor.systemRed
|
||||
unreadView.textColor = .white
|
||||
unreadView.font = .systemFont(ofSize: 12, weight: .bold)
|
||||
unreadView.textAlignment = .center
|
||||
unreadView.layer.cornerRadius = 12
|
||||
unreadView.layer.masksToBounds = true
|
||||
unreadView.text = "99+"
|
||||
|
||||
contentView.addSubview(avatar)
|
||||
contentView.addSubview(nameLabel)
|
||||
contentView.addSubview(subtitleLabel)
|
||||
contentView.addSubview(timeLabel)
|
||||
contentView.addSubview(unreadView)
|
||||
|
||||
avatar.translatesAutoresizingMaskIntoConstraints = false
|
||||
nameLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
timeLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
unreadView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
avatar.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
|
||||
avatar.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
avatar.widthAnchor.constraint(equalToConstant: 48),
|
||||
avatar.heightAnchor.constraint(equalToConstant: 48),
|
||||
|
||||
nameLabel.leadingAnchor.constraint(equalTo: avatar.trailingAnchor, constant: 12),
|
||||
nameLabel.topAnchor.constraint(equalTo: avatar.topAnchor, constant: -2),
|
||||
|
||||
subtitleLabel.leadingAnchor.constraint(equalTo: nameLabel.leadingAnchor),
|
||||
subtitleLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 6),
|
||||
|
||||
timeLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
|
||||
timeLabel.topAnchor.constraint(equalTo: nameLabel.topAnchor),
|
||||
|
||||
unreadView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
|
||||
unreadView.centerYAnchor.constraint(equalTo: subtitleLabel.centerYAnchor),
|
||||
unreadView.widthAnchor.constraint(greaterThanOrEqualToConstant: 40),
|
||||
unreadView.heightAnchor.constraint(equalToConstant: 24)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
92
YuMi/E-P/NewMessage/EPMessageMainViewController.swift
Normal file
92
YuMi/E-P/NewMessage/EPMessageMainViewController.swift
Normal file
@@ -0,0 +1,92 @@
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
final class EPMessageMainViewController: UIViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
|
||||
// 外部回调:未读数变化
|
||||
var unreadCountDidChange: ((Int)->Void)?
|
||||
|
||||
private let segment = EPMessageSegmentView()
|
||||
private let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal)
|
||||
|
||||
private lazy var pages: [UIViewController] = {
|
||||
return [
|
||||
EPMessageListVC(),
|
||||
EPFriendListVC(),
|
||||
EPFollowingListVC(),
|
||||
EPFansListVC()
|
||||
]
|
||||
}()
|
||||
|
||||
private var currentIndex: Int = 0
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = UIColor.black.withAlphaComponent(0.92)
|
||||
title = YMLocalizedString("XPSessionMainViewController0")
|
||||
|
||||
setupSegment()
|
||||
setupPageVC()
|
||||
|
||||
// 模拟未读变化(后续接入桥接器)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
|
||||
self?.unreadCountDidChange?(12)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupSegment() {
|
||||
view.addSubview(segment)
|
||||
segment.snp.makeConstraints { make in
|
||||
make.top.equalTo(view.safeAreaLayoutGuide.snp.top).offset(8)
|
||||
make.leading.trailing.equalToSuperview().inset(20)
|
||||
make.height.equalTo(48)
|
||||
}
|
||||
segment.didSelect = { [weak self] index in
|
||||
self?.setPage(index: index, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupPageVC() {
|
||||
addChild(pageVC)
|
||||
view.addSubview(pageVC.view)
|
||||
pageVC.view.backgroundColor = .clear
|
||||
pageVC.view.snp.makeConstraints { make in
|
||||
make.top.equalTo(segment.snp.bottom).offset(8)
|
||||
make.leading.trailing.bottom.equalTo(view.safeAreaLayoutGuide)
|
||||
}
|
||||
pageVC.didMove(toParent: self)
|
||||
pageVC.dataSource = self
|
||||
pageVC.delegate = self
|
||||
pageVC.setViewControllers([pages[0]], direction: .forward, animated: false)
|
||||
}
|
||||
|
||||
private func setPage(index: Int, animated: Bool) {
|
||||
guard index != currentIndex, index >= 0, index < pages.count else { return }
|
||||
let direction: UIPageViewController.NavigationDirection = index > currentIndex ? .forward : .reverse
|
||||
pageVC.setViewControllers([pages[index]], direction: direction, animated: animated)
|
||||
currentIndex = index
|
||||
segment.select(index: index, animated: animated)
|
||||
title = segment.titles[index]
|
||||
}
|
||||
|
||||
// MARK: - UIPageViewControllerDataSource
|
||||
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
|
||||
guard let idx = pages.firstIndex(of: viewController), idx > 0 else { return nil }
|
||||
return pages[idx - 1]
|
||||
}
|
||||
|
||||
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
|
||||
guard let idx = pages.firstIndex(of: viewController), idx < pages.count - 1 else { return nil }
|
||||
return pages[idx + 1]
|
||||
}
|
||||
|
||||
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
|
||||
guard completed, let vc = pageViewController.viewControllers?.first, let idx = pages.firstIndex(of: vc) else { return }
|
||||
currentIndex = idx
|
||||
segment.select(index: idx, animated: true)
|
||||
title = segment.titles[idx]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
84
YuMi/E-P/NewMessage/EPMessageSegmentView.swift
Normal file
84
YuMi/E-P/NewMessage/EPMessageSegmentView.swift
Normal file
@@ -0,0 +1,84 @@
|
||||
// A simple segmented control with underline indicator for four tabs
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
final class EPMessageSegmentView: UIView {
|
||||
enum Segment: Int, CaseIterable { case message=0, friend, following, fans }
|
||||
|
||||
var titles: [String] = [
|
||||
YMLocalizedString("XPSessionMainViewController0"),
|
||||
YMLocalizedString("XPSessionMainViewController1"),
|
||||
YMLocalizedString("XPSessionMainViewController2"),
|
||||
YMLocalizedString("XPSessionMainViewController3")
|
||||
]
|
||||
|
||||
var didSelect: ((Int)->Void)?
|
||||
|
||||
private var buttons: [UIButton] = []
|
||||
private let indicator = UIView()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setup()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { super.init(coder: coder); setup() }
|
||||
|
||||
private func setup() {
|
||||
backgroundColor = .clear
|
||||
|
||||
let stack = UIStackView()
|
||||
stack.axis = .horizontal
|
||||
stack.alignment = .fill
|
||||
stack.distribution = .fillEqually
|
||||
stack.spacing = 0
|
||||
addSubview(stack)
|
||||
stack.snp.makeConstraints { $0.edges.equalToSuperview() }
|
||||
|
||||
for (idx, title) in titles.enumerated() {
|
||||
let b = UIButton(type: .custom)
|
||||
b.tag = idx
|
||||
b.setTitle(title, for: .normal)
|
||||
b.setTitleColor(UIColor.white.withAlphaComponent(0.6), for: .normal)
|
||||
b.setTitleColor(.white, for: .selected)
|
||||
b.titleLabel?.font = .systemFont(ofSize: 24, weight: idx == 0 ? .heavy : .regular)
|
||||
b.addTarget(self, action: #selector(onTap(_:)), for: .touchUpInside)
|
||||
buttons.append(b)
|
||||
stack.addArrangedSubview(b)
|
||||
}
|
||||
|
||||
indicator.backgroundColor = UIColor.systemPink
|
||||
addSubview(indicator)
|
||||
layoutIfNeeded()
|
||||
select(index: 0, animated: false)
|
||||
}
|
||||
|
||||
@objc private func onTap(_ sender: UIButton) {
|
||||
select(index: sender.tag, animated: true)
|
||||
didSelect?(sender.tag)
|
||||
}
|
||||
|
||||
func select(index: Int, animated: Bool) {
|
||||
guard index >= 0 && index < buttons.count else { return }
|
||||
for (i,b) in buttons.enumerated() {
|
||||
b.isSelected = (i == index)
|
||||
b.titleLabel?.font = .systemFont(ofSize: 24, weight: b.isSelected ? .heavy : .regular)
|
||||
}
|
||||
let target = buttons[index]
|
||||
let width = target.bounds.width
|
||||
let y = bounds.height - 4
|
||||
let frame = CGRect(x: CGFloat(index) * width + width*0.15, y: y, width: width*0.7, height: 3)
|
||||
|
||||
if animated {
|
||||
UIView.animate(withDuration: 0.2) {
|
||||
self.indicator.frame = frame
|
||||
}
|
||||
} else {
|
||||
indicator.frame = frame
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
162
YuMi/E-P/NewMine/Controllers/EPAboutUsViewController.swift
Normal file
162
YuMi/E-P/NewMine/Controllers/EPAboutUsViewController.swift
Normal file
@@ -0,0 +1,162 @@
|
||||
//
|
||||
// EPAboutUsViewController.swift
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-01-28.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
/// About Us 页面
|
||||
/// 展示应用图标和版本信息
|
||||
class EPAboutUsViewController: BaseViewController {
|
||||
|
||||
// MARK: - UI Components
|
||||
|
||||
private lazy var appIconImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
imageView.layer.cornerRadius = 20
|
||||
imageView.layer.masksToBounds = true
|
||||
// 获取应用图标
|
||||
if let iconName = Bundle.main.object(forInfoDictionaryKey: "CFBundleIconName") as? String {
|
||||
imageView.image = UIImage(named: iconName)
|
||||
} else if let icons = Bundle.main.object(forInfoDictionaryKey: "CFBundleIcons") as? [String: Any],
|
||||
let primaryIcon = icons["CFBundlePrimaryIcon"] as? [String: Any],
|
||||
let iconFiles = primaryIcon["CFBundleIconFiles"] as? [String],
|
||||
let lastIcon = iconFiles.last {
|
||||
imageView.image = UIImage(named: lastIcon)
|
||||
} else {
|
||||
// 使用占位图标
|
||||
imageView.image = UIImage(named: "pi_app_logo_new_bg")
|
||||
}
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var appNameLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String
|
||||
?? Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String
|
||||
?? "YuMi"
|
||||
label.textColor = .white
|
||||
label.font = .systemFont(ofSize: 24, weight: .bold)
|
||||
label.textAlignment = .center
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var versionLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.0.0"
|
||||
let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "1"
|
||||
label.text = "Version \(version) (\(build))"
|
||||
label.textColor = UIColor.white.withAlphaComponent(0.7)
|
||||
label.font = .systemFont(ofSize: 16)
|
||||
label.textAlignment = .center
|
||||
return label
|
||||
}()
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setupNavigationBar()
|
||||
setupUI()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
navigationController?.setNavigationBarHidden(false, animated: animated)
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
private func setupNavigationBar() {
|
||||
title = YMLocalizedString("EPEditSetting.AboutUs")
|
||||
|
||||
// 配置导航栏外观(iOS 13+)
|
||||
let appearance = UINavigationBarAppearance()
|
||||
appearance.configureWithOpaqueBackground()
|
||||
appearance.backgroundColor = UIColor(hex: "#0C0527")
|
||||
appearance.titleTextAttributes = [
|
||||
.foregroundColor: UIColor.white,
|
||||
.font: UIFont.systemFont(ofSize: 18, weight: .medium)
|
||||
]
|
||||
appearance.shadowColor = .clear // 移除底部分割线
|
||||
|
||||
navigationController?.navigationBar.standardAppearance = appearance
|
||||
navigationController?.navigationBar.scrollEdgeAppearance = appearance
|
||||
navigationController?.navigationBar.compactAppearance = appearance
|
||||
navigationController?.navigationBar.tintColor = .white // 返回按钮颜色
|
||||
|
||||
// 隐藏返回按钮文字,只保留箭头
|
||||
navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
view.backgroundColor = UIColor(hex: "#0C0527")
|
||||
|
||||
// 创建容器视图
|
||||
let containerView = UIView()
|
||||
view.addSubview(containerView)
|
||||
|
||||
// 添加 UI 组件到容器
|
||||
containerView.addSubview(appIconImageView)
|
||||
containerView.addSubview(appNameLabel)
|
||||
containerView.addSubview(versionLabel)
|
||||
|
||||
// 布局容器(垂直居中)
|
||||
containerView.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(view).offset(-50) // 稍微偏上
|
||||
make.leading.trailing.equalTo(view).inset(40)
|
||||
}
|
||||
|
||||
// 应用图标
|
||||
appIconImageView.snp.makeConstraints { make in
|
||||
make.top.equalTo(containerView)
|
||||
make.centerX.equalTo(containerView)
|
||||
make.size.equalTo(100)
|
||||
}
|
||||
|
||||
// 应用名称
|
||||
appNameLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(appIconImageView.snp.bottom).offset(24)
|
||||
make.leading.trailing.equalTo(containerView)
|
||||
}
|
||||
|
||||
// 版本号
|
||||
versionLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(appNameLabel.snp.bottom).offset(12)
|
||||
make.leading.trailing.equalTo(containerView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIColor Extension
|
||||
|
||||
private extension UIColor {
|
||||
convenience init(hex: String) {
|
||||
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
|
||||
var int: UInt64 = 0
|
||||
Scanner(string: hex).scanHexInt64(&int)
|
||||
let a, r, g, b: UInt64
|
||||
switch hex.count {
|
||||
case 3: // RGB (12-bit)
|
||||
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
|
||||
case 6: // RGB (24-bit)
|
||||
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
|
||||
case 8: // ARGB (32-bit)
|
||||
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
|
||||
default:
|
||||
(a, r, g, b) = (1, 1, 1, 0)
|
||||
}
|
||||
|
||||
self.init(
|
||||
red: CGFloat(r) / 255,
|
||||
green: CGFloat(g) / 255,
|
||||
blue: CGFloat(b) / 255,
|
||||
alpha: CGFloat(a) / 255
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
850
YuMi/E-P/NewMine/Controllers/EPEditSettingViewController.swift
Normal file
850
YuMi/E-P/NewMine/Controllers/EPEditSettingViewController.swift
Normal file
@@ -0,0 +1,850 @@
|
||||
//
|
||||
// EPEditSettingViewController.swift
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-01-27.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Photos
|
||||
import SnapKit
|
||||
import WebKit
|
||||
|
||||
/// 设置编辑页面
|
||||
/// 支持头像更新、昵称修改和退出登录功能
|
||||
class EPEditSettingViewController: BaseViewController {
|
||||
|
||||
// MARK: - UI Components
|
||||
private lazy var profileImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.layer.cornerRadius = 60 // 120/2 = 60
|
||||
imageView.layer.masksToBounds = true
|
||||
imageView.backgroundColor = .systemGray5
|
||||
imageView.isUserInteractionEnabled = true
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var cameraIconView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
imageView.image = UIImage(named: "icon_setting_camear")
|
||||
imageView.backgroundColor = UIColor(hex: "#0C0527")
|
||||
imageView.layer.cornerRadius = 15 // 30/2 = 15
|
||||
imageView.layer.masksToBounds = true
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var tableView: UITableView = {
|
||||
let tableView = UITableView(frame: .zero, style: .plain)
|
||||
tableView.backgroundColor = UIColor(hex: "#0C0527")
|
||||
tableView.separatorStyle = .none
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "SettingCell")
|
||||
tableView.isScrollEnabled = true // 启用内部滚动
|
||||
return tableView
|
||||
}()
|
||||
|
||||
private lazy var logoutButton: UIButton = {
|
||||
let button = UIButton(type: .system)
|
||||
button.setTitle(YMLocalizedString("EPEditSetting.Logout"), for: .normal)
|
||||
button.setTitleColor(.white, for: .normal)
|
||||
button.titleLabel?.font = .systemFont(ofSize: 17, weight: .semibold)
|
||||
button.layer.cornerRadius = 25
|
||||
button.addTarget(self, action: #selector(logoutButtonTapped), for: .touchUpInside)
|
||||
return button
|
||||
}()
|
||||
|
||||
// MARK: - Data
|
||||
|
||||
private var settingItems: [SettingItem] = []
|
||||
private var userInfo: UserInfoModel?
|
||||
private var apiHelper: EPMineAPIHelper = EPMineAPIHelper()
|
||||
private var hasAddedGradient = false
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setupNavigationBar()
|
||||
setupUI()
|
||||
setupData()
|
||||
loadUserInfo()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
navigationController?.setNavigationBarHidden(false, animated: animated)
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
// 恢复父页面的导航栏配置(透明)
|
||||
restoreParentNavigationBarStyle()
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
// 添加渐变背景到 Logout 按钮(只添加一次)
|
||||
if !hasAddedGradient && logoutButton.bounds.width > 0 {
|
||||
logoutButton.addGradientBackground(
|
||||
with: [
|
||||
UIColor(red: 0xF8/255.0, green: 0x54/255.0, blue: 0xFC/255.0, alpha: 1.0), // #F854FC
|
||||
UIColor(red: 0x50/255.0, green: 0x0F/255.0, blue: 0xFF/255.0, alpha: 1.0) // #500FFF
|
||||
],
|
||||
start: CGPoint(x: 0, y: 0.5),
|
||||
end: CGPoint(x: 1, y: 0.5),
|
||||
cornerRadius: 25
|
||||
)
|
||||
hasAddedGradient = true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
private func setupNavigationBar() {
|
||||
title = YMLocalizedString("EPEditSetting.Title")
|
||||
|
||||
// 配置导航栏外观(iOS 13+)
|
||||
let appearance = UINavigationBarAppearance()
|
||||
appearance.configureWithOpaqueBackground()
|
||||
appearance.backgroundColor = UIColor(hex: "#0C0527")
|
||||
appearance.titleTextAttributes = [
|
||||
.foregroundColor: UIColor.white,
|
||||
.font: UIFont.systemFont(ofSize: 18, weight: .medium)
|
||||
]
|
||||
appearance.shadowColor = .clear // 移除底部分割线
|
||||
|
||||
navigationController?.navigationBar.standardAppearance = appearance
|
||||
navigationController?.navigationBar.scrollEdgeAppearance = appearance
|
||||
navigationController?.navigationBar.compactAppearance = appearance
|
||||
navigationController?.navigationBar.tintColor = .white // 返回按钮颜色
|
||||
|
||||
// 隐藏返回按钮文字,只保留箭头
|
||||
navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
|
||||
|
||||
// 如果是从上一页 push 进来的,也要修改上一页的 backButtonTitle
|
||||
navigationController?.navigationBar.topItem?.backBarButtonItem = UIBarButtonItem(
|
||||
title: "",
|
||||
style: .plain,
|
||||
target: nil,
|
||||
action: nil
|
||||
)
|
||||
}
|
||||
|
||||
private func restoreParentNavigationBarStyle() {
|
||||
// 恢复透明导航栏(EPMineViewController 使用的是透明导航栏)
|
||||
let transparentAppearance = UINavigationBarAppearance()
|
||||
transparentAppearance.configureWithTransparentBackground()
|
||||
transparentAppearance.backgroundColor = .clear
|
||||
transparentAppearance.shadowColor = .clear
|
||||
|
||||
navigationController?.navigationBar.standardAppearance = transparentAppearance
|
||||
navigationController?.navigationBar.scrollEdgeAppearance = transparentAppearance
|
||||
navigationController?.navigationBar.compactAppearance = transparentAppearance
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
view.backgroundColor = UIColor(hex: "#0C0527")
|
||||
|
||||
// 设置头像布局
|
||||
view.addSubview(profileImageView)
|
||||
profileImageView.snp.makeConstraints { make in
|
||||
make.top.equalTo(view.safeAreaLayoutGuide.snp.top).offset(40)
|
||||
make.centerX.equalTo(view)
|
||||
make.size.equalTo(120)
|
||||
}
|
||||
|
||||
// 设置相机图标布局
|
||||
view.addSubview(cameraIconView)
|
||||
cameraIconView.snp.makeConstraints { make in
|
||||
make.bottom.equalTo(profileImageView.snp.bottom)
|
||||
make.trailing.equalTo(profileImageView.snp.trailing)
|
||||
make.size.equalTo(30)
|
||||
}
|
||||
|
||||
// 设置 Logout 按钮布局
|
||||
view.addSubview(logoutButton)
|
||||
logoutButton.snp.makeConstraints { make in
|
||||
make.leading.trailing.equalTo(view).inset(20)
|
||||
make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-40)
|
||||
make.height.equalTo(50)
|
||||
}
|
||||
|
||||
// 设置 TableView 布局
|
||||
view.addSubview(tableView)
|
||||
tableView.snp.makeConstraints { make in
|
||||
make.top.equalTo(profileImageView.snp.bottom).offset(40)
|
||||
make.leading.trailing.equalTo(view)
|
||||
make.bottom.equalTo(logoutButton.snp.top).offset(-20)
|
||||
}
|
||||
|
||||
// 添加头像点击手势
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(profileImageTapped))
|
||||
profileImageView.addGestureRecognizer(tapGesture)
|
||||
|
||||
// 添加相机图标点击手势
|
||||
let cameraTapGesture = UITapGestureRecognizer(target: self, action: #selector(profileImageTapped))
|
||||
cameraIconView.addGestureRecognizer(cameraTapGesture)
|
||||
}
|
||||
|
||||
|
||||
|
||||
private func setupData() {
|
||||
settingItems = [
|
||||
SettingItem(
|
||||
title: YMLocalizedString("EPEditSetting.PersonalInfo"),
|
||||
action: { [weak self] in self?.handleReservedAction("PersonalInfo") }
|
||||
),
|
||||
SettingItem(
|
||||
title: YMLocalizedString("EPEditSetting.Help"),
|
||||
action: { [weak self] in self?.handleReservedAction("Help") }
|
||||
),
|
||||
SettingItem(
|
||||
title: YMLocalizedString("EPEditSetting.ClearCache"),
|
||||
action: { [weak self] in self?.handleReservedAction("ClearCache") }
|
||||
),
|
||||
SettingItem(
|
||||
title: YMLocalizedString("EPEditSetting.AboutUs"),
|
||||
action: { [weak self] in self?.handleReservedAction("AboutUs") }
|
||||
)
|
||||
]
|
||||
NSLog("[EPEditSetting] setupData 完成,设置项数量: \(settingItems.count)")
|
||||
}
|
||||
|
||||
private func loadUserInfo() {
|
||||
// 如果已经有用户信息(从 EPMineViewController 传递),则不需要重新加载
|
||||
if userInfo != nil {
|
||||
updateProfileImage()
|
||||
tableView.reloadData()
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前用户信息
|
||||
guard let uid = AccountInfoStorage.instance().getUid(), !uid.isEmpty else {
|
||||
print("[EPEditSetting] 未登录,无法获取用户信息")
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 调用API获取用户详细信息
|
||||
// 这里暂时创建默认的UserInfoModel用于显示
|
||||
let tempUserInfo = UserInfoModel()
|
||||
tempUserInfo.nick = "User"
|
||||
tempUserInfo.avatar = ""
|
||||
userInfo = tempUserInfo
|
||||
|
||||
updateProfileImage()
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
private func updateProfileImage() {
|
||||
guard let avatarUrl = userInfo?.avatar, !avatarUrl.isEmpty else {
|
||||
profileImageView.image = UIImage(systemName: "person.circle.fill")
|
||||
return
|
||||
}
|
||||
|
||||
// 使用SDWebImage加载头像
|
||||
if let url = URL(string: avatarUrl) {
|
||||
profileImageView.sd_setImage(with: url, placeholderImage: UIImage(systemName: "person.circle.fill"))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@objc private func profileImageTapped() {
|
||||
showAvatarSelectionSheet()
|
||||
}
|
||||
|
||||
@objc private func openSettings() {
|
||||
// 预留设置按钮功能
|
||||
handleReservedAction("Settings")
|
||||
}
|
||||
|
||||
@objc private func logoutButtonTapped() {
|
||||
showLogoutConfirm()
|
||||
}
|
||||
|
||||
private func showAvatarSelectionSheet() {
|
||||
let alert = UIAlertController(title: YMLocalizedString("EPEditSetting.EditNickname"), message: nil, preferredStyle: .actionSheet)
|
||||
|
||||
// 拍照选项
|
||||
alert.addAction(UIAlertAction(title: YMLocalizedString("EPEditSetting.Camera"), style: .default) { [weak self] _ in
|
||||
self?.checkCameraPermissionAndPresent()
|
||||
})
|
||||
|
||||
// 相册选项
|
||||
alert.addAction(UIAlertAction(title: YMLocalizedString("EPEditSetting.PhotoLibrary"), style: .default) { [weak self] _ in
|
||||
self?.checkPhotoLibraryPermissionAndPresent()
|
||||
})
|
||||
|
||||
alert.addAction(UIAlertAction(title: YMLocalizedString("EPEditSetting.Cancel"), style: .cancel))
|
||||
|
||||
// iPad支持
|
||||
if let popover = alert.popoverPresentationController {
|
||||
popover.sourceView = profileImageView
|
||||
popover.sourceRect = profileImageView.bounds
|
||||
}
|
||||
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func checkCameraPermissionAndPresent() {
|
||||
YYUtility.checkCameraAvailable { [weak self] in
|
||||
self?.presentImagePicker(sourceType: .camera)
|
||||
} denied: { [weak self] in
|
||||
self?.showPermissionAlert(title: "Camera Access", message: "Please allow camera access in Settings")
|
||||
} restriction: { [weak self] in
|
||||
self?.showPermissionAlert(title: "Camera Restricted", message: "Camera access is restricted on this device")
|
||||
}
|
||||
}
|
||||
|
||||
private func checkPhotoLibraryPermissionAndPresent() {
|
||||
YYUtility.checkAssetsLibrayAvailable { [weak self] in
|
||||
self?.presentImagePicker(sourceType: .photoLibrary)
|
||||
} denied: { [weak self] in
|
||||
self?.showPermissionAlert(title: "Photo Library Access", message: "Please allow photo library access in Settings")
|
||||
} restriction: { [weak self] in
|
||||
self?.showPermissionAlert(title: "Photo Library Restricted", message: "Photo library access is restricted on this device")
|
||||
}
|
||||
}
|
||||
|
||||
private func presentImagePicker(sourceType: UIImagePickerController.SourceType) {
|
||||
let imagePicker = UIImagePickerController()
|
||||
imagePicker.delegate = self
|
||||
imagePicker.sourceType = sourceType
|
||||
imagePicker.allowsEditing = true
|
||||
present(imagePicker, animated: true)
|
||||
}
|
||||
|
||||
private func showPermissionAlert(title: String, message: String) {
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "Settings", style: .default) { _ in
|
||||
if let settingsURL = URL(string: UIApplication.openSettingsURLString) {
|
||||
UIApplication.shared.open(settingsURL)
|
||||
}
|
||||
})
|
||||
alert.addAction(UIAlertAction(title: YMLocalizedString("EPEditSetting.Cancel"), style: .cancel))
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func showNicknameEditAlert() {
|
||||
let alert = UIAlertController(
|
||||
title: YMLocalizedString("EPEditSetting.EditNickname"),
|
||||
message: nil,
|
||||
preferredStyle: .alert
|
||||
)
|
||||
|
||||
alert.addTextField { [weak self] textField in
|
||||
textField.text = self?.userInfo?.nick ?? ""
|
||||
textField.placeholder = YMLocalizedString("EPEditSetting.EnterNickname")
|
||||
}
|
||||
|
||||
alert.addAction(UIAlertAction(title: YMLocalizedString("EPEditSetting.Cancel"), style: .cancel))
|
||||
alert.addAction(UIAlertAction(title: YMLocalizedString("EPEditSetting.Confirm"), style: .default) { [weak self] _ in
|
||||
guard let newNickname = alert.textFields?.first?.text, !newNickname.isEmpty else { return }
|
||||
self?.updateNickname(newNickname)
|
||||
})
|
||||
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func updateNickname(_ newNickname: String) {
|
||||
// 显示加载状态
|
||||
showLoading()
|
||||
|
||||
// 调用 API 更新昵称
|
||||
apiHelper.updateNickname(withNick: newNickname,
|
||||
completion: { [weak self] in
|
||||
self?.hideHUD()
|
||||
|
||||
// 更新成功后才更新本地显示
|
||||
self?.userInfo?.nick = newNickname
|
||||
self?.tableView.reloadData()
|
||||
|
||||
// 显示成功提示
|
||||
self?.showSuccessToast(YMLocalizedString("XPMineUserInfoEditViewController13"))
|
||||
|
||||
print("[EPEditSetting] 昵称更新成功: \(newNickname)")
|
||||
},
|
||||
failure: { [weak self] (code: Int, msg: String?) in
|
||||
self?.hideHUD()
|
||||
|
||||
// 显示错误提示
|
||||
let errorMsg = msg ?? YMLocalizedString("setting.nickname_update_failed")
|
||||
self?.showErrorToast(errorMsg)
|
||||
|
||||
print("[EPEditSetting] 昵称更新失败: \(code) - \(errorMsg)")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func showLogoutConfirm() {
|
||||
let alert = UIAlertController(
|
||||
title: YMLocalizedString("EPEditSetting.LogoutConfirm"),
|
||||
message: nil,
|
||||
preferredStyle: .alert
|
||||
)
|
||||
|
||||
alert.addAction(UIAlertAction(title: YMLocalizedString("EPEditSetting.Cancel"), style: .cancel))
|
||||
alert.addAction(UIAlertAction(title: YMLocalizedString("EPEditSetting.Logout"), style: .destructive) { [weak self] _ in
|
||||
self?.performLogout()
|
||||
})
|
||||
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func performLogout() {
|
||||
guard let account = AccountInfoStorage.instance().accountModel else {
|
||||
print("[EPEditSetting] 账号信息不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 调用登出API
|
||||
Api.logoutCurrentAccount({ [weak self] (data, code, msg) in
|
||||
DispatchQueue.main.async {
|
||||
// 清除本地数据
|
||||
AccountInfoStorage.instance().saveAccountInfo(nil)
|
||||
AccountInfoStorage.instance().saveTicket(nil)
|
||||
|
||||
// 跳转登录页
|
||||
self?.navigateToLogin()
|
||||
}
|
||||
}, access_token: account.access_token)
|
||||
}
|
||||
|
||||
private func navigateToLogin() {
|
||||
let loginVC = EPLoginViewController()
|
||||
let nav = UINavigationController(rootViewController: loginVC)
|
||||
|
||||
if let window = UIApplication.shared.windows.first {
|
||||
window.rootViewController = nav
|
||||
window.makeKeyAndVisible()
|
||||
}
|
||||
|
||||
print("[EPEditSetting] 已跳转到登录页面")
|
||||
}
|
||||
|
||||
private func handleReservedAction(_ title: String) {
|
||||
print("[\(title)] - 功能触发")
|
||||
|
||||
// About Us 已实现
|
||||
if title == "AboutUs" {
|
||||
let aboutVC = EPAboutUsViewController()
|
||||
navigationController?.pushViewController(aboutVC, animated: true)
|
||||
return
|
||||
}
|
||||
|
||||
// Personal Info - 显示协议和隐私政策选项
|
||||
if title == "PersonalInfo" {
|
||||
showPolicyOptionsSheet()
|
||||
return
|
||||
}
|
||||
|
||||
// Help - 跳转到 FAQ 页面
|
||||
if title == "Help" {
|
||||
let faqUrl = getFAQURL()
|
||||
openPolicyInExternalBrowser(faqUrl)
|
||||
return
|
||||
}
|
||||
|
||||
// Clear Cache - 清理图片和网页缓存
|
||||
if title == "ClearCache" {
|
||||
showClearCacheConfirmation()
|
||||
return
|
||||
}
|
||||
|
||||
// 其他功能预留
|
||||
// TODO: Phase 2 implementation
|
||||
let alert = UIAlertController(title: "Coming Soon", message: "This feature will be available in the next update.", preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func showClearCacheConfirmation() {
|
||||
let alert = UIAlertController(
|
||||
title: YMLocalizedString("EPEditSetting.ClearCacheTitle"),
|
||||
message: YMLocalizedString("EPEditSetting.ClearCacheMessage"),
|
||||
preferredStyle: .alert
|
||||
)
|
||||
|
||||
alert.addAction(UIAlertAction(title: YMLocalizedString("EPEditSetting.Cancel"), style: .cancel))
|
||||
alert.addAction(UIAlertAction(title: YMLocalizedString("EPEditSetting.Confirm"), style: .destructive) { [weak self] _ in
|
||||
self?.performClearCache()
|
||||
})
|
||||
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func performClearCache() {
|
||||
print("[EPEditSetting] 开始清理缓存")
|
||||
|
||||
// 显示加载状态
|
||||
showLoading()
|
||||
|
||||
// 1. 清理 SDWebImage 图片缓存
|
||||
SDWebImageManager.shared.imageCache.clear?(with: .all) {
|
||||
print("[EPEditSetting] SDWebImage 缓存已清理")
|
||||
|
||||
// 2. 清理 WKWebsiteDataStore 网页缓存
|
||||
let websiteDataTypes = WKWebsiteDataStore.allWebsiteDataTypes()
|
||||
let dateFrom = Date(timeIntervalSince1970: 0)
|
||||
WKWebsiteDataStore.default().removeData(ofTypes: websiteDataTypes, modifiedSince: dateFrom) { [weak self] in
|
||||
print("[EPEditSetting] WKWebsiteDataStore 缓存已清理")
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self?.hideHUD()
|
||||
self?.showSuccessToast(YMLocalizedString("EPEditSetting.ClearCacheSuccess"))
|
||||
print("[EPEditSetting] 缓存清理完成")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func showPolicyOptionsSheet() {
|
||||
let alert = UIAlertController(
|
||||
title: nil,
|
||||
message: nil,
|
||||
preferredStyle: .actionSheet
|
||||
)
|
||||
|
||||
// 用户服务协议
|
||||
alert.addAction(UIAlertAction(
|
||||
title: YMLocalizedString("EPEditSetting.UserAgreement"),
|
||||
style: .default
|
||||
) { [weak self] _ in
|
||||
let url = self?.getUserAgreementURL() ?? ""
|
||||
self?.openPolicyInExternalBrowser(url)
|
||||
})
|
||||
|
||||
// 隐私政策
|
||||
alert.addAction(UIAlertAction(
|
||||
title: YMLocalizedString("EPEditSetting.PrivacyPolicy"),
|
||||
style: .default
|
||||
) { [weak self] _ in
|
||||
let url = self?.getPrivacyPolicyURL() ?? ""
|
||||
self?.openPolicyInExternalBrowser(url)
|
||||
})
|
||||
|
||||
// 取消
|
||||
alert.addAction(UIAlertAction(
|
||||
title: YMLocalizedString("EPEditSetting.Cancel"),
|
||||
style: .cancel
|
||||
))
|
||||
|
||||
// iPad 支持
|
||||
if let popover = alert.popoverPresentationController {
|
||||
popover.sourceView = view
|
||||
popover.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.midY, width: 0, height: 0)
|
||||
popover.permittedArrowDirections = []
|
||||
}
|
||||
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
/// 获取用户协议 URL
|
||||
private func getUserAgreementURL() -> String {
|
||||
// kUserProtocalURL 对应枚举值 4
|
||||
let url = URLWithType(URLType(rawValue: 4)!) as String
|
||||
print("[EPEditSetting] User agreement URL from URLWithType: \(url)")
|
||||
return url
|
||||
}
|
||||
|
||||
/// 获取隐私政策 URL
|
||||
private func getPrivacyPolicyURL() -> String {
|
||||
// kPrivacyURL 对应枚举值 0
|
||||
let url = URLWithType(URLType(rawValue: 0)!) as String
|
||||
print("[EPEditSetting] Privacy policy URL from URLWithType: \(url)")
|
||||
return url
|
||||
}
|
||||
|
||||
/// 获取 FAQ 帮助页面 URL
|
||||
private func getFAQURL() -> String {
|
||||
// kFAQURL 对应枚举值 6
|
||||
let url = URLWithType(URLType(rawValue: 6)!) as String
|
||||
print("[EPEditSetting] FAQ URL from URLWithType: \(url)")
|
||||
return url
|
||||
}
|
||||
|
||||
private func openPolicyInExternalBrowser(_ urlString: String) {
|
||||
print("[EPEditSetting] Original URL: \(urlString)")
|
||||
|
||||
// 如果不是完整 URL,拼接域名
|
||||
var fullUrl = urlString
|
||||
if !urlString.hasPrefix("http") && !urlString.hasPrefix("https") {
|
||||
let hostUrl = HttpRequestHelper.getHostUrl()
|
||||
fullUrl = "\(hostUrl)/\(urlString)"
|
||||
print("[EPEditSetting] Added host URL, full URL: \(fullUrl)")
|
||||
}
|
||||
|
||||
print("[EPEditSetting] Opening URL in external browser: \(fullUrl)")
|
||||
|
||||
guard let url = URL(string: fullUrl) else {
|
||||
print("[EPEditSetting] ❌ Invalid URL: \(fullUrl)")
|
||||
return
|
||||
}
|
||||
|
||||
print("[EPEditSetting] URL object created: \(url)")
|
||||
|
||||
// 在外部浏览器中打开
|
||||
if UIApplication.shared.canOpenURL(url) {
|
||||
print("[EPEditSetting] ✅ Can open URL, attempting to open...")
|
||||
UIApplication.shared.open(url, options: [:]) { success in
|
||||
print("[EPEditSetting] Open external browser: \(success ? "✅ Success" : "❌ Failed")")
|
||||
}
|
||||
} else {
|
||||
print("[EPEditSetting] ❌ Cannot open URL: \(fullUrl)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
|
||||
/// 更新用户信息(从 EPMineViewController 传递)
|
||||
@objc func updateWithUserInfo(_ userInfo: UserInfoModel) {
|
||||
self.userInfo = userInfo
|
||||
updateProfileImage()
|
||||
tableView.reloadData()
|
||||
NSLog("[EPEditSetting] 已更新用户信息: \(userInfo.nick)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSource & UITableViewDelegate
|
||||
|
||||
extension EPEditSettingViewController: UITableViewDataSource, UITableViewDelegate {
|
||||
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return 1 // 只有一个 section
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
let count = settingItems.count + 1 // +1 for nickname row
|
||||
NSLog("[EPEditSetting] TableView rows count: \(count), settingItems: \(settingItems.count)")
|
||||
return count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "SettingCell", for: indexPath)
|
||||
cell.backgroundColor = UIColor(hex: "#0C0527")
|
||||
cell.textLabel?.textColor = .white
|
||||
cell.selectionStyle = .none
|
||||
|
||||
// 清除之前的自定义视图
|
||||
cell.contentView.subviews.forEach { $0.removeFromSuperview() }
|
||||
|
||||
if indexPath.row == 0 {
|
||||
// 昵称行
|
||||
cell.textLabel?.text = YMLocalizedString("EPEditSetting.Nickname")
|
||||
|
||||
// 添加右箭头图标
|
||||
let arrowImageView = UIImageView()
|
||||
arrowImageView.image = UIImage(named: "icon_setting_right_arrow")
|
||||
arrowImageView.contentMode = .scaleAspectFit
|
||||
cell.contentView.addSubview(arrowImageView)
|
||||
arrowImageView.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().offset(-20)
|
||||
make.centerY.equalToSuperview()
|
||||
make.size.equalTo(22)
|
||||
}
|
||||
|
||||
// 添加用户昵称标签
|
||||
let nicknameLabel = UILabel()
|
||||
nicknameLabel.text = userInfo?.nick ?? YMLocalizedString("user.not_set")
|
||||
nicknameLabel.textColor = .lightGray
|
||||
nicknameLabel.font = UIFont.systemFont(ofSize: 16)
|
||||
cell.contentView.addSubview(nicknameLabel)
|
||||
nicknameLabel.snp.makeConstraints { make in
|
||||
make.trailing.equalTo(arrowImageView.snp.leading).offset(-12)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
|
||||
} else {
|
||||
// 其他设置项
|
||||
let item = settingItems[indexPath.row - 1]
|
||||
cell.textLabel?.text = item.title
|
||||
cell.textLabel?.textColor = .white
|
||||
|
||||
// 添加右箭头图标
|
||||
let arrowImageView = UIImageView()
|
||||
arrowImageView.image = UIImage(named: "icon_setting_right_arrow")
|
||||
arrowImageView.contentMode = .scaleAspectFit
|
||||
cell.contentView.addSubview(arrowImageView)
|
||||
arrowImageView.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().offset(-20)
|
||||
make.centerY.equalToSuperview()
|
||||
make.size.equalTo(22)
|
||||
}
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
return 60 // 所有行都是 60pt 高度
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
|
||||
if indexPath.row == 0 {
|
||||
// 昵称点击
|
||||
showNicknameEditAlert()
|
||||
} else {
|
||||
// 设置项点击
|
||||
let item = settingItems[indexPath.row - 1]
|
||||
item.action()
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||
return 0
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
|
||||
return 0
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIImagePickerControllerDelegate & UINavigationControllerDelegate
|
||||
|
||||
extension EPEditSettingViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
|
||||
|
||||
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
||||
picker.dismiss(animated: true)
|
||||
|
||||
guard let image = info[.editedImage] as? UIImage ?? info[.originalImage] as? UIImage else {
|
||||
print("[EPEditSetting] 未能获取选择的图片")
|
||||
return
|
||||
}
|
||||
|
||||
// 更新头像显示
|
||||
profileImageView.image = image
|
||||
|
||||
// 上传头像到腾讯云
|
||||
uploadAvatar(image)
|
||||
}
|
||||
|
||||
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
||||
picker.dismiss(animated: true)
|
||||
}
|
||||
|
||||
private func uploadAvatar(_ image: UIImage) {
|
||||
// 显示上传进度
|
||||
EPProgressHUD.showProgress(0, total: 1)
|
||||
|
||||
// 使用 EPSDKManager 统一上传接口(避免腾讯云 OCR 配置问题)
|
||||
EPSDKManager.shared.uploadImages([image],
|
||||
progress: { uploaded, total in
|
||||
EPProgressHUD.showProgress(uploaded, total: total)
|
||||
},
|
||||
success: { [weak self] resList in
|
||||
EPProgressHUD.dismiss()
|
||||
|
||||
guard !resList.isEmpty,
|
||||
let firstRes = resList.first,
|
||||
let avatarUrl = firstRes["resUrl"] as? String else {
|
||||
print("[EPEditSetting] 头像上传成功但无法获取URL")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
print("[EPEditSetting] 头像上传成功: \(avatarUrl)")
|
||||
|
||||
// 调用API更新头像
|
||||
self?.updateAvatarAPI(avatarUrl: avatarUrl)
|
||||
},
|
||||
failure: { [weak self] errorMsg in
|
||||
EPProgressHUD.dismiss()
|
||||
print("[EPEditSetting] 头像上传失败: \(errorMsg)")
|
||||
|
||||
// 显示错误提示
|
||||
DispatchQueue.main.async {
|
||||
let alert = UIAlertController(title: YMLocalizedString("common.upload_failed"), message: errorMsg, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: YMLocalizedString("common.confirm"), style: .default))
|
||||
self?.present(alert, animated: true)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func updateAvatarAPI(avatarUrl: String) {
|
||||
// 使用 API Helper 更新头像
|
||||
apiHelper.updateAvatar(withUrl: avatarUrl, completion: { [weak self] in
|
||||
print("[EPEditSetting] 头像更新成功")
|
||||
|
||||
// 更新本地用户信息
|
||||
self?.userInfo?.avatar = avatarUrl
|
||||
|
||||
// 通知父页面头像已更新
|
||||
self?.notifyParentAvatarUpdated(avatarUrl)
|
||||
|
||||
}, failure: { [weak self] (code: Int, msg: String?) in
|
||||
print("[EPEditSetting] 头像更新失败: \(code) - \(msg ?? "未知错误")")
|
||||
|
||||
// 显示错误提示
|
||||
DispatchQueue.main.async {
|
||||
let alert = UIAlertController(
|
||||
title: YMLocalizedString("common.update_failed"),
|
||||
message: msg ?? YMLocalizedString("setting.avatar_update_failed"),
|
||||
preferredStyle: .alert
|
||||
)
|
||||
alert.addAction(UIAlertAction(title: YMLocalizedString("common.confirm"), style: .default))
|
||||
self?.present(alert, animated: true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func notifyParentAvatarUpdated(_ avatarUrl: String) {
|
||||
// 发送通知给 EPMineViewController 更新头像
|
||||
let userInfo = ["avatarUrl": avatarUrl]
|
||||
NotificationCenter.default.post(name: NSNotification.Name("EPEditSettingAvatarUpdated"), object: nil, userInfo: userInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper Models
|
||||
|
||||
private struct SettingItem {
|
||||
let title: String
|
||||
let action: () -> Void
|
||||
|
||||
init(title: String, action: @escaping () -> Void) {
|
||||
self.title = title
|
||||
self.action = action
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIColor Extension
|
||||
|
||||
private extension UIColor {
|
||||
convenience init(hex: String) {
|
||||
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
|
||||
var int: UInt64 = 0
|
||||
Scanner(string: hex).scanHexInt64(&int)
|
||||
let a, r, g, b: UInt64
|
||||
switch hex.count {
|
||||
case 3: // RGB (12-bit)
|
||||
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
|
||||
case 6: // RGB (24-bit)
|
||||
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
|
||||
case 8: // ARGB (32-bit)
|
||||
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
|
||||
default:
|
||||
(a, r, g, b) = (1, 1, 1, 0)
|
||||
}
|
||||
|
||||
self.init(
|
||||
red: CGFloat(r) / 255,
|
||||
green: CGFloat(g) / 255,
|
||||
blue: CGFloat(b) / 255,
|
||||
alpha: CGFloat(a) / 255
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,19 @@
|
||||
//
|
||||
// NewMineViewController.h
|
||||
// EPMineViewController.h
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-10-09.
|
||||
// Copyright © 2025 YuMi. All rights reserved.
|
||||
//
|
||||
|
||||
#import "BaseViewController.h"
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// 新的个人中心页面控制器
|
||||
/// 采用纵向卡片式设计,完全不同于原 XPMineViewController
|
||||
@interface NewMineViewController : BaseViewController
|
||||
/// 注意:直接继承 UIViewController,不继承 BaseViewController(避免依赖链)
|
||||
@interface EPMineViewController : UIViewController
|
||||
|
||||
@end
|
||||
|
||||
220
YuMi/E-P/NewMine/Controllers/EPMineViewController.m
Normal file
220
YuMi/E-P/NewMine/Controllers/EPMineViewController.m
Normal file
@@ -0,0 +1,220 @@
|
||||
//
|
||||
// EPMineViewController.m
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-10-09.
|
||||
// Copyright © 2025 YuMi. All rights reserved.
|
||||
//
|
||||
|
||||
#import "EPMineViewController.h"
|
||||
#import "EPMineHeaderView.h"
|
||||
#import "EPMomentListView.h"
|
||||
#import "EPMineAPIHelper.h"
|
||||
#import "AccountInfoStorage.h"
|
||||
#import "UserInfoModel.h"
|
||||
#import <Masonry/Masonry.h>
|
||||
#import "YuMi-Swift.h" // 导入Swift桥接
|
||||
|
||||
@interface EPMineViewController ()
|
||||
|
||||
// MARK: - UI Components
|
||||
|
||||
/// 动态列表视图(复用 EPMomentListView)
|
||||
@property (nonatomic, strong) EPMomentListView *momentListView;
|
||||
|
||||
/// 顶部个人信息卡片
|
||||
@property (nonatomic, strong) EPMineHeaderView *headerView;
|
||||
|
||||
// MARK: - Data
|
||||
|
||||
/// 用户信息模型
|
||||
@property (nonatomic, strong) UserInfoModel *userInfo;
|
||||
|
||||
/// API Helper
|
||||
@property (nonatomic, strong) EPMineAPIHelper *apiHelper;
|
||||
|
||||
@end
|
||||
|
||||
@implementation EPMineViewController
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
[self setupUI];
|
||||
|
||||
NSLog(@"[EPMineViewController] viewDidLoad 完成");
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
[self.navigationController setNavigationBarHidden:YES animated:animated];
|
||||
// 每次显示时加载最新数据
|
||||
[self loadUserDetailInfo];
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
- (void)setupUI {
|
||||
UIImageView *bgImageView = [[UIImageView alloc] initWithImage:kImage(@"vc_bg")];
|
||||
bgImageView.contentMode = UIViewContentModeScaleAspectFill;
|
||||
bgImageView.clipsToBounds = YES;
|
||||
[self.view addSubview:bgImageView];
|
||||
[bgImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.mas_equalTo(self.view);
|
||||
}];
|
||||
|
||||
[self setupHeaderView];
|
||||
[self setupMomentListView];
|
||||
|
||||
NSLog(@"[EPMineViewController] UI 设置完成");
|
||||
}
|
||||
|
||||
- (void)setupHeaderView {
|
||||
self.headerView = [[EPMineHeaderView alloc] initWithFrame:CGRectZero];
|
||||
[self.view addSubview:self.headerView];
|
||||
|
||||
// 使用 Masonry 约束布局
|
||||
[self.headerView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.mas_equalTo(self.view);
|
||||
make.leading.mas_equalTo(self.view);
|
||||
make.trailing.mas_equalTo(self.view);
|
||||
make.height.mas_equalTo(kGetScaleWidth(260));
|
||||
}];
|
||||
|
||||
// 设置按钮点击回调
|
||||
__weak typeof(self) weakSelf = self;
|
||||
self.headerView.onSettingsButtonTapped = ^{
|
||||
__strong typeof(weakSelf) self = weakSelf;
|
||||
[self openSettings];
|
||||
};
|
||||
|
||||
// 监听头像更新事件
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(onAvatarUpdated:)
|
||||
name:@"EPEditSettingAvatarUpdated"
|
||||
object:nil];
|
||||
}
|
||||
|
||||
- (void)setupMomentListView {
|
||||
self.momentListView = [[EPMomentListView alloc] initWithFrame:CGRectZero];
|
||||
[self.view addSubview:self.momentListView];
|
||||
|
||||
[self.momentListView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.mas_equalTo(self.headerView.mas_bottom);
|
||||
make.bottom.mas_equalTo(self.view);
|
||||
make.leading.mas_equalTo(self.view);
|
||||
make.trailing.mas_equalTo(self.view);
|
||||
}];
|
||||
|
||||
// 初始化为空的本地模式,避免在数据加载前触发网络请求
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[self.momentListView loadWithDynamicInfo:@[] refreshCallback:^{
|
||||
__strong typeof(weakSelf) self = weakSelf;
|
||||
[self loadUserDetailInfo];
|
||||
}];
|
||||
}
|
||||
|
||||
// MARK: - Data Loading
|
||||
|
||||
- (void)loadUserDetailInfo {
|
||||
NSString *uid = [[AccountInfoStorage instance] getUid];
|
||||
if (!uid || uid.length == 0) {
|
||||
NSLog(@"[EPMineViewController] 用户未登录");
|
||||
return;
|
||||
}
|
||||
|
||||
@kWeakify(self);
|
||||
[self.apiHelper getUserDetailInfoWithUid:uid
|
||||
completion:^(UserInfoModel * _Nullable userInfo) {
|
||||
@kStrongify(self);
|
||||
if (!userInfo) {
|
||||
NSLog(@"[EPMineViewController] 加载用户信息失败");
|
||||
return;
|
||||
}
|
||||
|
||||
self.userInfo = userInfo;
|
||||
[self updateHeaderWithUserInfo:userInfo];
|
||||
|
||||
// 如果有动态信息,直接使用
|
||||
if (userInfo.dynamicInfo && userInfo.dynamicInfo.count > 0) {
|
||||
[self.momentListView loadWithDynamicInfo:userInfo.dynamicInfo refreshCallback:^{
|
||||
[self loadUserDetailInfo]; // 刷新时重新加载
|
||||
}];
|
||||
}
|
||||
} failure:^(NSInteger code, NSString * _Nullable msg) {
|
||||
NSLog(@"[EPMineViewController] 加载用户信息失败: %@", msg);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)updateHeaderWithUserInfo:(UserInfoModel *)userInfo {
|
||||
NSDictionary *userInfoDict = @{
|
||||
@"nickname": userInfo.nick ?: @"未设置昵称",
|
||||
@"uid": [NSString stringWithFormat:@"%ld", (long)userInfo.erbanNo],
|
||||
@"avatar": userInfo.avatar ?: @"",
|
||||
@"following": @(userInfo.followNum),
|
||||
@"followers": @(userInfo.fansNum)
|
||||
};
|
||||
|
||||
[self.headerView updateWithUserInfo:userInfoDict];
|
||||
}
|
||||
|
||||
// MARK: - Lazy Loading
|
||||
|
||||
- (EPMomentListView *)momentListView {
|
||||
if (!_momentListView) {
|
||||
_momentListView = [[EPMomentListView alloc] init];
|
||||
__weak typeof(self) weakSelf = self;
|
||||
_momentListView.onSelectMoment = ^(NSInteger index) {
|
||||
__strong typeof(weakSelf) self = weakSelf;
|
||||
NSLog(@"[EPMineViewController] 点击了第 %ld 条动态", (long)index);
|
||||
// TODO: 跳转到动态详情页
|
||||
};
|
||||
}
|
||||
return _momentListView;
|
||||
}
|
||||
|
||||
- (EPMineAPIHelper *)apiHelper {
|
||||
if (!_apiHelper) {
|
||||
_apiHelper = [[EPMineAPIHelper alloc] init];
|
||||
}
|
||||
return _apiHelper;
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
- (void)openSettings {
|
||||
// 隐藏返回按钮文字,只保留白色箭头
|
||||
self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@""
|
||||
style:UIBarButtonItemStylePlain
|
||||
target:nil
|
||||
action:nil];
|
||||
|
||||
EPEditSettingViewController *settingsVC = [[EPEditSettingViewController alloc] init];
|
||||
// 传递用户信息到设置页面
|
||||
if (self.userInfo) {
|
||||
[settingsVC updateWithUserInfo:self.userInfo];
|
||||
}
|
||||
[self.navigationController pushViewController:settingsVC animated:YES];
|
||||
NSLog(@"[EPMineViewController] 打开设置页面,已传递用户信息");
|
||||
}
|
||||
|
||||
- (void)onAvatarUpdated:(NSNotification *)notification {
|
||||
NSString *avatarUrl = notification.userInfo[@"avatarUrl"];
|
||||
if (avatarUrl && self.userInfo) {
|
||||
// 更新本地用户信息
|
||||
self.userInfo.avatar = avatarUrl;
|
||||
|
||||
// 更新 UI 显示
|
||||
[self updateHeaderWithUserInfo:self.userInfo];
|
||||
|
||||
NSLog(@"[EPMineViewController] 头像已更新: %@", avatarUrl);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
// 只移除头像更新通知的观察者,设置按钮现在使用 block 回调
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"EPEditSettingAvatarUpdated" object:nil];
|
||||
}
|
||||
|
||||
@end
|
||||
40
YuMi/E-P/NewMine/Services/EPMineAPIHelper.h
Normal file
40
YuMi/E-P/NewMine/Services/EPMineAPIHelper.h
Normal file
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// EPMineAPIHelper.h
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-10-10.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class UserInfoModel;
|
||||
|
||||
/// 封装用户信息相关 API
|
||||
@interface EPMineAPIHelper : NSObject
|
||||
|
||||
/// 获取用户基础信息
|
||||
- (void)getUserInfoWithUid:(NSString *)uid
|
||||
completion:(void (^)(UserInfoModel * _Nullable userInfo))completion
|
||||
failure:(void (^)(NSInteger code, NSString * _Nullable msg))failure;
|
||||
|
||||
/// 获取用户详细信息(包含 dynamicInfo)
|
||||
- (void)getUserDetailInfoWithUid:(NSString *)uid
|
||||
completion:(void (^)(UserInfoModel * _Nullable userInfo))completion
|
||||
failure:(void (^)(NSInteger code, NSString * _Nullable msg))failure;
|
||||
|
||||
/// 更新用户头像
|
||||
- (void)updateAvatarWithUrl:(NSString *)avatarUrl
|
||||
completion:(void (^)(void))completion
|
||||
failure:(void (^)(NSInteger code, NSString * _Nullable msg))failure;
|
||||
|
||||
/// 更新用户昵称
|
||||
- (void)updateNicknameWithNick:(NSString *)nickname
|
||||
completion:(void (^)(void))completion
|
||||
failure:(void (^)(NSInteger code, NSString * _Nullable msg))failure;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
77
YuMi/E-P/NewMine/Services/EPMineAPIHelper.m
Normal file
77
YuMi/E-P/NewMine/Services/EPMineAPIHelper.m
Normal file
@@ -0,0 +1,77 @@
|
||||
//
|
||||
// EPMineAPIHelper.m
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-10-10.
|
||||
//
|
||||
|
||||
#import "EPMineAPIHelper.h"
|
||||
#import "Api+Mine.h"
|
||||
#import "UserInfoModel.h"
|
||||
#import "BaseModel.h"
|
||||
#import "AccountInfoStorage.h"
|
||||
|
||||
@implementation EPMineAPIHelper
|
||||
|
||||
- (void)getUserInfoWithUid:(NSString *)uid
|
||||
completion:(void (^)(UserInfoModel * _Nullable userInfo))completion
|
||||
failure:(void (^)(NSInteger code, NSString * _Nullable msg))failure {
|
||||
[Api getUserInfo:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) {
|
||||
if (code == 200 && data.data) {
|
||||
UserInfoModel *userInfo = [UserInfoModel modelWithDictionary:data.data];
|
||||
if (completion) completion(userInfo);
|
||||
} else {
|
||||
if (failure) failure(code, msg);
|
||||
}
|
||||
} uid:uid];
|
||||
}
|
||||
|
||||
- (void)getUserDetailInfoWithUid:(NSString *)uid
|
||||
completion:(void (^)(UserInfoModel * _Nullable userInfo))completion
|
||||
failure:(void (^)(NSInteger code, NSString * _Nullable msg))failure {
|
||||
[Api userDetailInfoCompletion:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) {
|
||||
if (code == 200 && data.data) {
|
||||
UserInfoModel *userInfo = [UserInfoModel modelWithDictionary:data.data];
|
||||
if (completion) completion(userInfo);
|
||||
} else {
|
||||
if (failure) failure(code, msg);
|
||||
}
|
||||
} uid:uid page:@"1" pageSize:@"20"];
|
||||
}
|
||||
|
||||
- (void)updateAvatarWithUrl:(NSString *)avatarUrl
|
||||
completion:(void (^)(void))completion
|
||||
failure:(void (^)(NSInteger code, NSString * _Nullable msg))failure {
|
||||
[Api userV2UploadAvatar:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) {
|
||||
if (code == 200) {
|
||||
if (completion) completion();
|
||||
} else {
|
||||
if (failure) failure(code, msg);
|
||||
}
|
||||
} avatarUrl:avatarUrl needPay:@NO];
|
||||
}
|
||||
|
||||
- (void)updateNicknameWithNick:(NSString *)nickname
|
||||
completion:(void (^)(void))completion
|
||||
failure:(void (^)(NSInteger code, NSString * _Nullable msg))failure {
|
||||
NSString *uid = [[AccountInfoStorage instance] getUid];
|
||||
NSString *ticket = [[AccountInfoStorage instance] getTicket];
|
||||
|
||||
NSMutableDictionary *params = [NSMutableDictionary dictionary];
|
||||
if (nickname.length > 0) {
|
||||
[params setValue:nickname forKey:@"nick"];
|
||||
}
|
||||
[params setObject:uid forKey:@"uid"];
|
||||
[params setObject:ticket forKey:@"ticket"];
|
||||
|
||||
[Api completeUserInfo:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) {
|
||||
if (code == 200) {
|
||||
if (completion) completion();
|
||||
} else {
|
||||
if (failure) failure(code, msg);
|
||||
}
|
||||
} userInfo:params];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
26
YuMi/E-P/NewMine/Views/EPMineHeaderView.h
Normal file
26
YuMi/E-P/NewMine/Views/EPMineHeaderView.h
Normal file
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// EPMineHeaderView.h
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-10-09.
|
||||
// Copyright © 2025 YuMi. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// EP 系列个人主页头部视图
|
||||
/// 大圆形头像 + 渐变背景 + 用户信息展示
|
||||
@interface EPMineHeaderView : UIView
|
||||
|
||||
/// 设置按钮点击回调
|
||||
@property (nonatomic, copy, nullable) void(^onSettingsButtonTapped)(void);
|
||||
|
||||
/// 更新用户信息
|
||||
/// @param userInfoDict 用户信息字典
|
||||
- (void)updateWithUserInfo:(NSDictionary *)userInfoDict;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
242
YuMi/E-P/NewMine/Views/EPMineHeaderView.m
Normal file
242
YuMi/E-P/NewMine/Views/EPMineHeaderView.m
Normal file
@@ -0,0 +1,242 @@
|
||||
//
|
||||
// EPMineHeaderView.m
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-10-09.
|
||||
// Copyright © 2025 YuMi. All rights reserved.
|
||||
//
|
||||
|
||||
#import "EPMineHeaderView.h"
|
||||
#import <Masonry/Masonry.h>
|
||||
#import <SDWebImage/SDWebImage.h>
|
||||
#import "EPEmotionColorStorage.h"
|
||||
|
||||
@interface EPMineHeaderView ()
|
||||
|
||||
/// 头像视图
|
||||
@property (nonatomic, strong) UIImageView *avatarImageView;
|
||||
|
||||
/// 呼吸光晕层
|
||||
@property (nonatomic, strong) CALayer *glowLayer;
|
||||
|
||||
/// 昵称标签
|
||||
@property (nonatomic, strong) UILabel *nicknameLabel;
|
||||
|
||||
/// ID 标签
|
||||
@property (nonatomic, strong) UILabel *idLabel;
|
||||
|
||||
/// 设置按钮
|
||||
@property (nonatomic, strong) UIButton *settingsButton;
|
||||
|
||||
@end
|
||||
|
||||
@implementation EPMineHeaderView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
[self setupUI];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
// 更新光晕层 frame(跟随头像位置)
|
||||
if (self.glowLayer) {
|
||||
self.glowLayer.frame = CGRectInset(self.avatarImageView.frame, -8, -8);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setupUI {
|
||||
// 大圆形头像
|
||||
self.avatarImageView = [[UIImageView alloc] init];
|
||||
self.avatarImageView.layer.cornerRadius = 60;
|
||||
self.avatarImageView.layer.masksToBounds = NO; // 改为 NO 以显示阴影
|
||||
self.avatarImageView.layer.borderWidth = 0; // 移除边框
|
||||
self.avatarImageView.backgroundColor = [UIColor whiteColor];
|
||||
self.avatarImageView.contentMode = UIViewContentModeScaleAspectFill;
|
||||
|
||||
// 为了同时显示圆角和阴影,需要设置 clipsToBounds
|
||||
self.avatarImageView.clipsToBounds = YES;
|
||||
|
||||
[self addSubview:self.avatarImageView];
|
||||
|
||||
[self.avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.centerX.equalTo(self);
|
||||
make.top.equalTo(self).offset(60);
|
||||
make.size.mas_equalTo(CGSizeMake(120, 120));
|
||||
}];
|
||||
|
||||
// 昵称
|
||||
self.nicknameLabel = [[UILabel alloc] init];
|
||||
self.nicknameLabel.font = [UIFont boldSystemFontOfSize:24];
|
||||
self.nicknameLabel.textColor = [UIColor whiteColor];
|
||||
self.nicknameLabel.textAlignment = NSTextAlignmentCenter;
|
||||
[self addSubview:self.nicknameLabel];
|
||||
|
||||
[self.nicknameLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.centerX.equalTo(self);
|
||||
make.top.equalTo(self.avatarImageView.mas_bottom).offset(16);
|
||||
}];
|
||||
|
||||
// ID
|
||||
self.idLabel = [[UILabel alloc] init];
|
||||
self.idLabel.font = [UIFont systemFontOfSize:14];
|
||||
self.idLabel.textColor = [UIColor whiteColor];
|
||||
self.idLabel.alpha = 0.8;
|
||||
self.idLabel.textAlignment = NSTextAlignmentCenter;
|
||||
[self addSubview:self.idLabel];
|
||||
|
||||
[self.idLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.centerX.equalTo(self);
|
||||
make.top.equalTo(self.nicknameLabel.mas_bottom).offset(8);
|
||||
}];
|
||||
|
||||
// 设置按钮(右上角)
|
||||
self.settingsButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
[self.settingsButton setImage:[UIImage systemImageNamed:@"gearshape"] forState:UIControlStateNormal];
|
||||
self.settingsButton.tintColor = [UIColor whiteColor];
|
||||
self.settingsButton.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.2];
|
||||
self.settingsButton.layer.cornerRadius = 20;
|
||||
[self.settingsButton addTarget:self action:@selector(settingsButtonTapped) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self addSubview:self.settingsButton];
|
||||
|
||||
[self.settingsButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self).offset(50);
|
||||
make.trailing.equalTo(self).offset(-20);
|
||||
make.size.mas_equalTo(CGSizeMake(40, 40));
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)updateWithUserInfo:(NSDictionary *)userInfoDict {
|
||||
// 更新昵称
|
||||
NSString *nickname = userInfoDict[@"nickname"] ?: YMLocalizedString(@"user.nickname_not_set");
|
||||
self.nicknameLabel.text = nickname;
|
||||
|
||||
// 更新 ID
|
||||
NSString *uid = userInfoDict[@"uid"] ?: @"";
|
||||
self.idLabel.text = [NSString stringWithFormat:@"ID:%@", uid];
|
||||
|
||||
// 加载头像
|
||||
NSString *avatarURL = userInfoDict[@"avatar"];
|
||||
if (avatarURL && avatarURL.length > 0) {
|
||||
[self.avatarImageView sd_setImageWithURL:[NSURL URLWithString:avatarURL]
|
||||
placeholderImage:[UIImage imageNamed:@"default_avatar"]];
|
||||
} else {
|
||||
// 使用默认头像
|
||||
self.avatarImageView.image = [UIImage imageNamed:@"default_avatar"];
|
||||
}
|
||||
|
||||
// 应用用户专属情绪颜色
|
||||
[self applyUserSignatureColor];
|
||||
}
|
||||
|
||||
/// 应用用户专属情绪颜色到头像边框和阴影
|
||||
- (void)applyUserSignatureColor {
|
||||
NSString *signatureColor = [EPEmotionColorStorage userSignatureColor];
|
||||
|
||||
if (signatureColor) {
|
||||
// 有专属颜色,使用该颜色
|
||||
UIColor *color = [self colorFromHex:signatureColor];
|
||||
|
||||
// 取消边框
|
||||
self.avatarImageView.layer.borderWidth = 0;
|
||||
|
||||
// 设置阴影(使用情绪颜色)
|
||||
self.avatarImageView.layer.shadowColor = color.CGColor;
|
||||
self.avatarImageView.layer.shadowOffset = CGSizeMake(0, 4);
|
||||
self.avatarImageView.layer.shadowOpacity = 0.6;
|
||||
self.avatarImageView.layer.shadowRadius = 12;
|
||||
|
||||
NSLog(@"[EPMineHeaderView] 应用专属颜色: %@", signatureColor);
|
||||
|
||||
// 应用呼吸光晕动效 ⭐
|
||||
[self applyBreathingGlow];
|
||||
} else {
|
||||
// 没有专属颜色,保持无边框
|
||||
self.avatarImageView.layer.borderWidth = 0;
|
||||
|
||||
// 默认轻微阴影
|
||||
self.avatarImageView.layer.shadowColor = [UIColor blackColor].CGColor;
|
||||
self.avatarImageView.layer.shadowOffset = CGSizeMake(0, 2);
|
||||
self.avatarImageView.layer.shadowOpacity = 0.2;
|
||||
self.avatarImageView.layer.shadowRadius = 8;
|
||||
|
||||
// 移除光晕层
|
||||
if (self.glowLayer) {
|
||||
[self.glowLayer removeFromSuperlayer];
|
||||
self.glowLayer = nil;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 应用呼吸光晕动效
|
||||
- (void)applyBreathingGlow {
|
||||
NSString *signatureColor = [EPEmotionColorStorage userSignatureColor];
|
||||
if (!signatureColor) return;
|
||||
|
||||
UIColor *color = [self colorFromHex:signatureColor];
|
||||
|
||||
// 创建光晕层(如果不存在)
|
||||
if (!self.glowLayer) {
|
||||
self.glowLayer = [CALayer layer];
|
||||
self.glowLayer.frame = CGRectInset(self.avatarImageView.frame, -8, -8); // 比头像大 16pt
|
||||
self.glowLayer.cornerRadius = 68; // 头像 60 + 扩展 8
|
||||
self.glowLayer.backgroundColor = [color colorWithAlphaComponent:0.75].CGColor; // 大幅加深
|
||||
|
||||
// 插入到头像 layer 下方
|
||||
[self.layer insertSublayer:self.glowLayer below:self.avatarImageView.layer];
|
||||
} else {
|
||||
// 更新颜色
|
||||
self.glowLayer.backgroundColor = [color colorWithAlphaComponent:0.75].CGColor; // 大幅加深
|
||||
}
|
||||
|
||||
// 移除旧动画
|
||||
[self.glowLayer removeAllAnimations];
|
||||
|
||||
// 创建呼吸动画组
|
||||
CAAnimationGroup *breathingGroup = [CAAnimationGroup animation];
|
||||
breathingGroup.duration = 1.8; // 加速
|
||||
breathingGroup.repeatCount = HUGE_VALF; // 无限循环
|
||||
breathingGroup.autoreverses = YES;
|
||||
breathingGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
|
||||
|
||||
// 动画 1:透明度变化(呼吸亮度)
|
||||
CABasicAnimation *opacityAnim = [CABasicAnimation animationWithKeyPath:@"opacity"];
|
||||
opacityAnim.fromValue = @(0.65);
|
||||
opacityAnim.toValue = @(1.0); // 接近完全不透明,颜色饱和
|
||||
|
||||
// 动画 2:缩放变化(呼吸扩散)
|
||||
CABasicAnimation *scaleAnim = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
|
||||
scaleAnim.fromValue = @(1.0);
|
||||
scaleAnim.toValue = @(1.1);
|
||||
|
||||
breathingGroup.animations = @[opacityAnim, scaleAnim];
|
||||
|
||||
[self.glowLayer addAnimation:breathingGroup forKey:@"breathing"];
|
||||
|
||||
NSLog(@"[EPMineHeaderView] 启动呼吸光晕动效");
|
||||
}
|
||||
|
||||
/// Hex 转 UIColor
|
||||
- (UIColor *)colorFromHex:(NSString *)hexString {
|
||||
unsigned rgbValue = 0;
|
||||
NSScanner *scanner = [NSScanner scannerWithString:hexString];
|
||||
[scanner setScanLocation:1]; // 跳过 #
|
||||
[scanner scanHexInt:&rgbValue];
|
||||
return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16)/255.0
|
||||
green:((rgbValue & 0xFF00) >> 8)/255.0
|
||||
blue:(rgbValue & 0xFF)/255.0
|
||||
alpha:1.0];
|
||||
}
|
||||
|
||||
- (void)settingsButtonTapped {
|
||||
NSLog(@"[EPMineHeaderView] 设置按钮点击");
|
||||
// 使用 block 回调
|
||||
if (self.onSettingsButtonTapped) {
|
||||
self.onSettingsButtonTapped();
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// EPMomentPublishViewController.h
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-10-10.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// 发布成功通知
|
||||
extern NSString *const EPMomentPublishSuccessNotification;
|
||||
|
||||
/// EP 版:图文发布页面
|
||||
@interface EPMomentPublishViewController : UIViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
|
||||
435
YuMi/E-P/NewMoments/Controllers/EPMomentPublishViewController.m
Normal file
435
YuMi/E-P/NewMoments/Controllers/EPMomentPublishViewController.m
Normal file
@@ -0,0 +1,435 @@
|
||||
|
||||
|
||||
// Created by AI on 2025-10-10.
|
||||
|
||||
|
||||
// NOTE: 话题选择功能未实现
|
||||
|
||||
|
||||
#import "EPMomentPublishViewController.h"
|
||||
#import <Masonry/Masonry.h>
|
||||
#import <TZImagePickerController/TZImagePickerController.h>
|
||||
#import "DJDKMIMOMColor.h"
|
||||
#import "SZTextView.h"
|
||||
#import "YuMi-Swift.h"
|
||||
#import "EPEmotionColorPicker.h"
|
||||
#import "EPEmotionColorStorage.h"
|
||||
#import "UIView+GradientLayer.h"
|
||||
|
||||
|
||||
NSString *const EPMomentPublishSuccessNotification = @"EPMomentPublishSuccessNotification";
|
||||
|
||||
@interface EPMomentPublishViewController () <UICollectionViewDataSource, UICollectionViewDelegate, TZImagePickerControllerDelegate, UITextViewDelegate>
|
||||
|
||||
@property (nonatomic, strong) UIView *navView;
|
||||
@property (nonatomic, strong) UIButton *backButton;
|
||||
@property (nonatomic, strong) UILabel *titleLabel;
|
||||
@property (nonatomic, strong) UIButton *publishButton;
|
||||
|
||||
@property (nonatomic, strong) UIView *contentView;
|
||||
@property (nonatomic, strong) SZTextView *textView;
|
||||
@property (nonatomic, strong) UILabel *limitLabel;
|
||||
@property (nonatomic, strong) UIView *lineView;
|
||||
@property (nonatomic, strong) UIButton *emotionButton;
|
||||
@property (nonatomic, strong) UICollectionView *collectionView;
|
||||
@property (nonatomic, strong) NSMutableArray<UIImage *> *images;
|
||||
@property (nonatomic, strong) NSMutableArray *selectedAssets;
|
||||
@property (nonatomic, copy) NSString *selectedEmotionColor;
|
||||
|
||||
@property (nonatomic, assign) BOOL hasAddedGradient;
|
||||
|
||||
@end
|
||||
|
||||
@implementation EPMomentPublishViewController
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.view.backgroundColor = [UIColor colorWithRed:0x0C/255.0 green:0x05/255.0 blue:0x27/255.0 alpha:1.0];
|
||||
[self setupUI];
|
||||
|
||||
|
||||
[self loadUserSignatureColor];
|
||||
}
|
||||
|
||||
- (void)viewDidLayoutSubviews {
|
||||
[super viewDidLayoutSubviews];
|
||||
|
||||
|
||||
if (!self.hasAddedGradient && self.publishButton.bounds.size.width > 0) {
|
||||
|
||||
|
||||
[self.publishButton addGradientBackgroundWithColors:@[
|
||||
[UIColor colorWithRed:0xF8/255.0 green:0x54/255.0 blue:0xFC/255.0 alpha:1.0],
|
||||
[UIColor colorWithRed:0x50/255.0 green:0x0F/255.0 blue:0xFF/255.0 alpha:1.0]
|
||||
] startPoint:CGPointMake(0, 0.5) endPoint:CGPointMake(1, 0.5) cornerRadius:25];
|
||||
|
||||
self.hasAddedGradient = YES;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)loadUserSignatureColor {
|
||||
NSString *signatureColor = [EPEmotionColorStorage userSignatureColor];
|
||||
if (signatureColor) {
|
||||
self.selectedEmotionColor = signatureColor;
|
||||
[self updateEmotionButtonAppearance];
|
||||
NSLog(@"[Publish] 自动选中专属颜色: %@", signatureColor);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setupUI {
|
||||
[self.view addSubview:self.navView];
|
||||
[self.view addSubview:self.contentView];
|
||||
[self.navView addSubview:self.backButton];
|
||||
[self.navView addSubview:self.titleLabel];
|
||||
|
||||
[self.contentView addSubview:self.textView];
|
||||
[self.contentView addSubview:self.limitLabel];
|
||||
[self.contentView addSubview:self.lineView];
|
||||
[self.contentView addSubview:self.emotionButton];
|
||||
[self.contentView addSubview:self.collectionView];
|
||||
[self.contentView addSubview:self.publishButton];
|
||||
|
||||
[self.navView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.trailing.top.equalTo(self.view);
|
||||
make.height.mas_equalTo(kNavigationHeight);
|
||||
}];
|
||||
[self.backButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.equalTo(self.view).offset(10);
|
||||
make.top.mas_equalTo(statusbarHeight);
|
||||
make.size.mas_equalTo(CGSizeMake(44, 44));
|
||||
}];
|
||||
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.centerX.equalTo(self.navView);
|
||||
make.centerY.equalTo(self.backButton);
|
||||
}];
|
||||
|
||||
[self.contentView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.trailing.equalTo(self.view);
|
||||
make.top.equalTo(self.navView.mas_bottom);
|
||||
make.bottom.equalTo(self.view);
|
||||
}];
|
||||
[self.textView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.trailing.equalTo(self.contentView).inset(15);
|
||||
make.top.equalTo(self.contentView).offset(10);
|
||||
make.height.mas_equalTo(150);
|
||||
}];
|
||||
[self.limitLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.textView.mas_bottom).offset(5);
|
||||
make.trailing.equalTo(self.textView);
|
||||
}];
|
||||
[self.lineView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.limitLabel.mas_bottom).offset(10);
|
||||
make.leading.trailing.equalTo(self.textView);
|
||||
make.height.mas_equalTo(1);
|
||||
}];
|
||||
|
||||
|
||||
[self.emotionButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.trailing.equalTo(self.contentView).inset(15);
|
||||
make.top.equalTo(self.lineView.mas_bottom).offset(10);
|
||||
make.height.mas_equalTo(44);
|
||||
}];
|
||||
|
||||
|
||||
CGFloat itemW = (KScreenWidth - 15*2 - 10*2)/3.0;
|
||||
CGFloat collectionHeight = itemW * 3 + 10 * 2;
|
||||
|
||||
[self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.trailing.equalTo(self.contentView).inset(15);
|
||||
make.top.equalTo(self.emotionButton.mas_bottom).offset(10);
|
||||
make.height.mas_equalTo(collectionHeight);
|
||||
}];
|
||||
|
||||
|
||||
[self.publishButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.trailing.equalTo(self.view).inset(20);
|
||||
make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom).offset(-20);
|
||||
make.height.mas_equalTo(50);
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Actions
|
||||
|
||||
- (void)onBack {
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)onEmotionButtonTapped {
|
||||
EPEmotionColorPicker *picker = [[EPEmotionColorPicker alloc] init];
|
||||
|
||||
|
||||
picker.preselectedColor = self.selectedEmotionColor;
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
picker.onColorSelected = ^(NSString *hexColor) {
|
||||
__strong typeof(weakSelf) self = weakSelf;
|
||||
self.selectedEmotionColor = hexColor;
|
||||
[self updateEmotionButtonAppearance];
|
||||
};
|
||||
[picker showInView:self.view];
|
||||
}
|
||||
|
||||
- (void)updateEmotionButtonAppearance {
|
||||
if (self.selectedEmotionColor) {
|
||||
|
||||
UIColor *color = [self colorFromHex:self.selectedEmotionColor];
|
||||
|
||||
|
||||
UIView *colorDot = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 20, 20)];
|
||||
colorDot.backgroundColor = color;
|
||||
colorDot.layer.cornerRadius = 10;
|
||||
colorDot.layer.masksToBounds = YES;
|
||||
colorDot.layer.borderWidth = 2;
|
||||
colorDot.layer.borderColor = [UIColor whiteColor].CGColor;
|
||||
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(colorDot.bounds.size, NO, 0);
|
||||
[colorDot.layer renderInContext:UIGraphicsGetCurrentContext()];
|
||||
UIImage *colorDotImage = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
|
||||
[self.emotionButton setImage:colorDotImage forState:UIControlStateNormal];
|
||||
|
||||
|
||||
NSString *emotionName = [EPEmotionColorStorage emotionNameForColor:self.selectedEmotionColor];
|
||||
NSString *title = emotionName
|
||||
? [NSString stringWithFormat:@" Selected Emotion: %@", emotionName]
|
||||
: @" Emotion Selected";
|
||||
[self.emotionButton setTitle:title forState:UIControlStateNormal];
|
||||
} else {
|
||||
[self.emotionButton setImage:nil forState:UIControlStateNormal];
|
||||
[self.emotionButton setTitle:@"🎨 Add Emotion" forState:UIControlStateNormal];
|
||||
}
|
||||
}
|
||||
|
||||
- (UIColor *)colorFromHex:(NSString *)hexString {
|
||||
unsigned rgbValue = 0;
|
||||
NSScanner *scanner = [NSScanner scannerWithString:hexString];
|
||||
[scanner setScanLocation:1];
|
||||
[scanner scanHexInt:&rgbValue];
|
||||
return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16)/255.0
|
||||
green:((rgbValue & 0xFF00) >> 8)/255.0
|
||||
blue:(rgbValue & 0xFF)/255.0
|
||||
alpha:1.0];
|
||||
}
|
||||
|
||||
- (void)onPublish {
|
||||
[self.view endEditing:YES];
|
||||
|
||||
|
||||
if (self.textView.text.length == 0 && self.images.count == 0) {
|
||||
[EPProgressHUD showError:YMLocalizedString(@"publish.content_or_image_required")];
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
EPMomentAPISwiftHelper *apiHelper = [[EPMomentAPISwiftHelper alloc] init];
|
||||
|
||||
|
||||
NSString *emotionColorToSave = self.selectedEmotionColor;
|
||||
|
||||
if (self.images.count > 0) {
|
||||
|
||||
[[EPSDKManager shared] uploadImages:self.images
|
||||
progress:^(NSInteger uploaded, NSInteger total) {
|
||||
[EPProgressHUD showProgress:uploaded total:total];
|
||||
}
|
||||
success:^(NSArray<NSDictionary *> *resList) {
|
||||
[EPProgressHUD dismiss];
|
||||
[apiHelper publishMomentWithType:@"2"
|
||||
content:self.textView.text ?: @""
|
||||
resList:resList
|
||||
completion:^{
|
||||
|
||||
if (emotionColorToSave) {
|
||||
[self savePendingEmotionColor:emotionColorToSave];
|
||||
}
|
||||
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:EPMomentPublishSuccessNotification object:nil];
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
} failure:^(NSInteger code, NSString *msg) {
|
||||
// TODO: 显示错误 Toast
|
||||
NSLog(@"发布失败: %ld - %@", (long)code, msg);
|
||||
}];
|
||||
}
|
||||
failure:^(NSString *errorMsg) {
|
||||
[EPProgressHUD dismiss];
|
||||
// TODO: 显示错误 Toast
|
||||
NSLog(@"上传失败: %@", errorMsg);
|
||||
}];
|
||||
} else {
|
||||
|
||||
[apiHelper publishMomentWithType:@"0"
|
||||
content:self.textView.text
|
||||
resList:@[]
|
||||
completion:^{
|
||||
|
||||
if (emotionColorToSave) {
|
||||
[self savePendingEmotionColor:emotionColorToSave];
|
||||
}
|
||||
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:EPMomentPublishSuccessNotification object:nil];
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
} failure:^(NSInteger code, NSString *msg) {
|
||||
// TODO: 显示错误 Toast
|
||||
NSLog(@"发布失败: %ld - %@", (long)code, msg);
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)savePendingEmotionColor:(NSString *)color {
|
||||
[[NSUserDefaults standardUserDefaults] setObject:color forKey:@"EP_Pending_Emotion_Color"];
|
||||
[[NSUserDefaults standardUserDefaults] setObject:@([[NSDate date] timeIntervalSince1970]) forKey:@"EP_Pending_Emotion_Timestamp"];
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
}
|
||||
|
||||
#pragma mark - UICollectionView
|
||||
|
||||
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
|
||||
return self.images.count + 1;
|
||||
}
|
||||
|
||||
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"ep.publish.cell" forIndexPath:indexPath];
|
||||
cell.contentView.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.06];
|
||||
cell.contentView.layer.cornerRadius = 12;
|
||||
|
||||
for (UIView *sub in cell.contentView.subviews) { [sub removeFromSuperview]; }
|
||||
BOOL showAdd = (self.images.count < 9) && (indexPath.item == self.images.count);
|
||||
if (showAdd) {
|
||||
UIImageView *iv = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"icon_moment_addphoto"]];
|
||||
iv.contentMode = UIViewContentModeScaleAspectFill;
|
||||
iv.clipsToBounds = YES;
|
||||
[cell.contentView addSubview:iv];
|
||||
[iv mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(cell.contentView); }];
|
||||
} else {
|
||||
UIImageView *iv = [[UIImageView alloc] init];
|
||||
iv.contentMode = UIViewContentModeScaleAspectFill;
|
||||
iv.layer.masksToBounds = YES;
|
||||
[cell.contentView addSubview:iv];
|
||||
[iv mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(cell.contentView); }];
|
||||
NSInteger idx = MIN(indexPath.item, (NSInteger)self.images.count - 1);
|
||||
if (idx >= 0 && idx < self.images.count) iv.image = self.images[idx];
|
||||
}
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
if (indexPath.item == self.images.count) {
|
||||
TZImagePickerController *picker = [[TZImagePickerController alloc] initWithMaxImagesCount:9 delegate:self];
|
||||
picker.allowPickingVideo = NO;
|
||||
picker.allowTakeVideo = NO;
|
||||
picker.allowCameraLocation = NO; // 禁止请求定位权限
|
||||
picker.selectedAssets = self.selectedAssets;
|
||||
picker.maxImagesCount = 9;
|
||||
[self presentViewController:picker animated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - TZImagePickerControllerDelegate
|
||||
- (void)imagePickerController:(TZImagePickerController *)picker didFinishPickingPhotos:(NSArray<UIImage *> *)photos sourceAssets:(NSArray *)assets isSelectOriginalPhoto:(BOOL)isSelectOriginalPhoto infos:(NSArray<NSDictionary *> *)infos {
|
||||
|
||||
for (NSInteger i = 0; i < assets.count; i++) {
|
||||
id asset = assets[i];
|
||||
UIImage *img = [photos xpSafeObjectAtIndex:i] ?: photos[i];
|
||||
if (![self.selectedAssets containsObject:asset] && self.images.count < 9) {
|
||||
[self.selectedAssets addObject:asset];
|
||||
[self.images addObject:img];
|
||||
}
|
||||
}
|
||||
[self.collectionView reloadData];
|
||||
}
|
||||
|
||||
#pragma mark - UITextViewDelegate
|
||||
- (void)textViewDidChange:(UITextView *)textView {
|
||||
if (textView.text.length > 500) {
|
||||
textView.text = [textView.text substringToIndex:500];
|
||||
}
|
||||
self.limitLabel.text = [NSString stringWithFormat:@"%lu/500", (unsigned long)textView.text.length];
|
||||
}
|
||||
|
||||
#pragma mark - Lazy
|
||||
|
||||
- (UIView *)navView { if (!_navView) { _navView = [UIView new]; _navView.backgroundColor = [UIColor clearColor]; } return _navView; }
|
||||
- (UIButton *)backButton {
|
||||
if (!_backButton) {
|
||||
_backButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
|
||||
UIImage *backImage = [UIImage systemImageNamed:@"chevron.left"];
|
||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:20 weight:UIImageSymbolWeightMedium];
|
||||
backImage = [backImage imageByApplyingSymbolConfiguration:config];
|
||||
[_backButton setImage:backImage forState:UIControlStateNormal];
|
||||
[_backButton setTintColor:[UIColor whiteColor]];
|
||||
[_backButton addTarget:self action:@selector(onBack) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
return _backButton;
|
||||
}
|
||||
- (UILabel *)titleLabel {
|
||||
if (!_titleLabel) {
|
||||
_titleLabel = [UILabel new];
|
||||
_titleLabel.text = YMLocalizedString(@"publish.title");
|
||||
_titleLabel.textColor = [UIColor whiteColor];
|
||||
_titleLabel.font = [UIFont systemFontOfSize:17];
|
||||
}
|
||||
return _titleLabel;
|
||||
}
|
||||
- (UIButton *)publishButton {
|
||||
if (!_publishButton) {
|
||||
_publishButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
[_publishButton setTitle:YMLocalizedString(@"common.publish") forState:UIControlStateNormal];
|
||||
[_publishButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
||||
_publishButton.titleLabel.font = [UIFont systemFontOfSize:17 weight:UIFontWeightMedium];
|
||||
_publishButton.layer.cornerRadius = 25;
|
||||
_publishButton.layer.masksToBounds = NO;
|
||||
|
||||
[_publishButton addTarget:self action:@selector(onPublish) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
return _publishButton;
|
||||
}
|
||||
- (UIView *)contentView { if (!_contentView) { _contentView = [UIView new]; _contentView.backgroundColor = [UIColor clearColor]; } return _contentView; }
|
||||
- (SZTextView *)textView {
|
||||
if (!_textView) {
|
||||
_textView = [SZTextView new];
|
||||
_textView.placeholder = @"Enter Content";
|
||||
_textView.textColor = [UIColor whiteColor];
|
||||
_textView.placeholderTextColor = [[UIColor whiteColor] colorWithAlphaComponent:0.4];
|
||||
_textView.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.08];
|
||||
_textView.layer.cornerRadius = 12;
|
||||
_textView.layer.masksToBounds = YES;
|
||||
_textView.font = [UIFont systemFontOfSize:15];
|
||||
_textView.delegate = self;
|
||||
}
|
||||
return _textView;
|
||||
}
|
||||
- (UILabel *)limitLabel {
|
||||
if (!_limitLabel) {
|
||||
_limitLabel = [UILabel new];
|
||||
_limitLabel.text = @"0/500";
|
||||
_limitLabel.textColor = [[UIColor whiteColor] colorWithAlphaComponent:0.6];
|
||||
_limitLabel.font = [UIFont systemFontOfSize:12];
|
||||
}
|
||||
return _limitLabel;
|
||||
}
|
||||
- (UIView *)lineView { if (!_lineView) { _lineView = [UIView new]; _lineView.backgroundColor = [DJDKMIMOMColor dividerColor]; } return _lineView; }
|
||||
- (UIButton *)emotionButton {
|
||||
if (!_emotionButton) {
|
||||
_emotionButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
[_emotionButton setTitle:@"🎨 Add Emotion" forState:UIControlStateNormal];
|
||||
[_emotionButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
||||
_emotionButton.titleLabel.font = [UIFont systemFontOfSize:15];
|
||||
_emotionButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
|
||||
_emotionButton.contentEdgeInsets = UIEdgeInsetsMake(0, 15, 0, 0);
|
||||
_emotionButton.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.08];
|
||||
_emotionButton.layer.cornerRadius = 8;
|
||||
_emotionButton.layer.masksToBounds = YES;
|
||||
[_emotionButton addTarget:self action:@selector(onEmotionButtonTapped) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
return _emotionButton;
|
||||
}
|
||||
- (UICollectionView *)collectionView { if (!_collectionView) { UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; layout.minimumLineSpacing = 10; layout.minimumInteritemSpacing = 10; CGFloat itemW = (KScreenWidth - 15*2 - 10*2)/3.0; layout.itemSize = CGSizeMake(itemW, itemW); _collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; _collectionView.delegate = self; _collectionView.dataSource = self; _collectionView.backgroundColor = [UIColor clearColor]; [_collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"ep.publish.cell"]; } return _collectionView; }
|
||||
- (NSMutableArray<UIImage *> *)images { if (!_images) { _images = [NSMutableArray array]; } return _images; }
|
||||
- (NSMutableArray *)selectedAssets { if (!_selectedAssets) { _selectedAssets = [NSMutableArray array]; } return _selectedAssets; }
|
||||
|
||||
@end
|
||||
@@ -1,18 +1,19 @@
|
||||
//
|
||||
// NewMomentViewController.h
|
||||
// EPMomentViewController.h
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-10-09.
|
||||
// Copyright © 2025 YuMi. All rights reserved.
|
||||
//
|
||||
|
||||
#import "BaseViewController.h"
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// 新的动态页面控制器
|
||||
/// 采用卡片式布局,完全不同于原 XPMomentsViewController
|
||||
@interface NewMomentViewController : BaseViewController
|
||||
/// 注意:直接继承 UIViewController,不继承 BaseViewController(避免依赖链)
|
||||
@interface EPMomentViewController : UIViewController
|
||||
|
||||
@end
|
||||
|
||||
180
YuMi/E-P/NewMoments/Controllers/EPMomentViewController.m
Normal file
180
YuMi/E-P/NewMoments/Controllers/EPMomentViewController.m
Normal file
@@ -0,0 +1,180 @@
|
||||
|
||||
|
||||
// Created by AI on 2025-10-09.
|
||||
// Copyright © 2025 YuMi. All rights reserved.
|
||||
|
||||
|
||||
#import "EPMomentViewController.h"
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <Masonry/Masonry.h>
|
||||
#import "EPMomentCell.h"
|
||||
#import "EPMomentListView.h"
|
||||
#import "EPMomentPublishViewController.h"
|
||||
#import "YUMIMacroUitls.h"
|
||||
|
||||
@interface EPMomentViewController ()
|
||||
|
||||
// MARK: - UI Components
|
||||
|
||||
|
||||
@property (nonatomic, strong) EPMomentListView *listView;
|
||||
|
||||
|
||||
@property (nonatomic, strong) UIImageView *topIconImageView;
|
||||
|
||||
|
||||
@property (nonatomic, strong) UILabel *topTipLabel;
|
||||
|
||||
@end
|
||||
|
||||
@implementation EPMomentViewController
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.title = @"Enjoy your Life Time";
|
||||
|
||||
|
||||
[self.navigationController.navigationBar setTitleTextAttributes:@{
|
||||
NSForegroundColorAttributeName: [UIColor whiteColor]
|
||||
}];
|
||||
|
||||
[self setupUI];
|
||||
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(onMomentPublishSuccess:)
|
||||
name:EPMomentPublishSuccessNotification
|
||||
object:nil];
|
||||
|
||||
NSLog(@"[EPMomentViewController] 页面加载完成,UI 已设置");
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
[super viewDidAppear:animated];
|
||||
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
NSLog(@"[EPMomentViewController] 首次 viewDidAppear,延迟 0.3s 后开始加载数据");
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
NSLog(@"[EPMomentViewController] 触发首次数据加载");
|
||||
[self.listView reloadFirstPage];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
}
|
||||
|
||||
// MARK: - Setup UI
|
||||
|
||||
- (void)setupUI {
|
||||
UIImageView *bgImageView = [[UIImageView alloc] initWithImage:kImage(@"vc_bg")];
|
||||
bgImageView.contentMode = UIViewContentModeScaleAspectFill;
|
||||
bgImageView.clipsToBounds = YES;
|
||||
[self.view addSubview:bgImageView];
|
||||
[bgImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.mas_equalTo(self.view);
|
||||
}];
|
||||
|
||||
|
||||
[self.view addSubview:self.topIconImageView];
|
||||
[self.topIconImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.centerX.equalTo(self.view);
|
||||
make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop).offset(14);
|
||||
make.size.mas_equalTo(CGSizeMake(56, 41));
|
||||
}];
|
||||
|
||||
|
||||
[self.view addSubview:self.topTipLabel];
|
||||
[self.topTipLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.topIconImageView.mas_bottom).offset(14);
|
||||
make.leading.trailing.equalTo(self.view).inset(20);
|
||||
}];
|
||||
|
||||
|
||||
[self.view addSubview:self.listView];
|
||||
[self.listView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.trailing.bottom.equalTo(self.view);
|
||||
make.top.equalTo(self.topTipLabel.mas_bottom).offset(8);
|
||||
}];
|
||||
|
||||
|
||||
UIImage *addIcon = [UIImage imageNamed:@"icon_moment_add"];
|
||||
UIButton *publishButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
publishButton.contentMode = UIViewContentModeScaleAspectFit;
|
||||
[publishButton setImage:addIcon forState:UIControlStateNormal];
|
||||
publishButton.frame = CGRectMake(0, 0, 40, 40);
|
||||
[publishButton addTarget:self action:@selector(onPublishButtonTapped) forControlEvents:UIControlEventTouchUpInside];
|
||||
UIBarButtonItem *publishItem = [[UIBarButtonItem alloc] initWithCustomView:publishButton];
|
||||
self.navigationItem.rightBarButtonItem = publishItem;
|
||||
|
||||
NSLog(@"[EPMomentViewController] UI 设置完成");
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
- (void)onPublishButtonTapped {
|
||||
NSLog(@"[EPMomentViewController] 发布按钮点击");
|
||||
EPMomentPublishViewController *vc = [[EPMomentPublishViewController alloc] init];
|
||||
vc.modalPresentationStyle = UIModalPresentationFullScreen;
|
||||
[self.navigationController presentViewController:vc animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)showAlertWithMessage:(NSString *)message {
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:YMLocalizedString(@"common.tips")
|
||||
message:message
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:YMLocalizedString(@"common.confirm") style:UIAlertActionStyleDefault handler:nil]];
|
||||
[self presentViewController:alert animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)onMomentPublishSuccess:(NSNotification *)notification {
|
||||
NSLog(@"[EPMomentViewController] 收到发布成功通知,刷新列表");
|
||||
[self.listView reloadFirstPage];
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Lazy Loading
|
||||
|
||||
- (EPMomentListView *)listView {
|
||||
if (!_listView) {
|
||||
_listView = [[EPMomentListView alloc] initWithFrame:CGRectZero];
|
||||
|
||||
_listView.onSelectMoment = ^(NSInteger index) {
|
||||
|
||||
|
||||
};
|
||||
}
|
||||
return _listView;
|
||||
}
|
||||
|
||||
- (UIImageView *)topIconImageView {
|
||||
if (!_topIconImageView) {
|
||||
_topIconImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"icon_moment_Volume"]];
|
||||
_topIconImageView.contentMode = UIViewContentModeScaleAspectFit;
|
||||
}
|
||||
return _topIconImageView;
|
||||
}
|
||||
|
||||
- (UILabel *)topTipLabel {
|
||||
if (!_topTipLabel) {
|
||||
_topTipLabel = [UILabel new];
|
||||
_topTipLabel.numberOfLines = 0;
|
||||
_topTipLabel.textColor = [UIColor whiteColor];
|
||||
_topTipLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightRegular];
|
||||
_topTipLabel.text = @"In the quiet gallery of the heart, we learn to see the colors of emotion. And in the shared silence between souls, we begin to find the sound of resonance. This is more than an app—it's a space where your inner world is both a masterpiece and a melody.";
|
||||
}
|
||||
return _topTipLabel;
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
46
YuMi/E-P/NewMoments/Services/EPEmotionColorStorage.h
Normal file
46
YuMi/E-P/NewMoments/Services/EPEmotionColorStorage.h
Normal file
@@ -0,0 +1,46 @@
|
||||
|
||||
|
||||
// Created by AI on 2025-10-14.
|
||||
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface EPEmotionColorStorage : NSObject
|
||||
|
||||
|
||||
+ (void)saveColor:(NSString *)hexColor forDynamicId:(NSString *)dynamicId;
|
||||
|
||||
|
||||
+ (nullable NSString *)colorForDynamicId:(NSString *)dynamicId;
|
||||
|
||||
|
||||
+ (void)removeColorForDynamicId:(NSString *)dynamicId;
|
||||
|
||||
|
||||
+ (NSArray<NSString *> *)allEmotionColors;
|
||||
|
||||
|
||||
+ (NSString *)randomEmotionColor;
|
||||
|
||||
|
||||
+ (nullable NSString *)emotionNameForColor:(NSString *)hexColor;
|
||||
|
||||
#pragma mark - User Signature Color
|
||||
|
||||
|
||||
+ (void)saveUserSignatureColor:(NSString *)hexColor;
|
||||
|
||||
|
||||
+ (nullable NSString *)userSignatureColor;
|
||||
|
||||
|
||||
+ (BOOL)hasUserSignatureColor;
|
||||
|
||||
|
||||
+ (void)clearUserSignatureColor;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
115
YuMi/E-P/NewMoments/Services/EPEmotionColorStorage.m
Normal file
115
YuMi/E-P/NewMoments/Services/EPEmotionColorStorage.m
Normal file
@@ -0,0 +1,115 @@
|
||||
|
||||
|
||||
// Created by AI on 2025-10-14.
|
||||
|
||||
|
||||
#import "EPEmotionColorStorage.h"
|
||||
|
||||
static NSString *const kEmotionColorStorageKey = @"EP_Emotion_Colors";
|
||||
static NSString *const kUserSignatureColorKey = @"EP_User_Signature_Color";
|
||||
static NSString *const kUserSignatureTimestampKey = @"EP_User_Signature_Timestamp";
|
||||
|
||||
@implementation EPEmotionColorStorage
|
||||
|
||||
#pragma mark - Public Methods
|
||||
|
||||
+ (void)saveColor:(NSString *)hexColor forDynamicId:(NSString *)dynamicId {
|
||||
if (!hexColor || !dynamicId) return;
|
||||
|
||||
NSMutableDictionary *colorDict = [[self loadColorDictionary] mutableCopy];
|
||||
colorDict[dynamicId] = hexColor;
|
||||
|
||||
[[NSUserDefaults standardUserDefaults] setObject:colorDict forKey:kEmotionColorStorageKey];
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
}
|
||||
|
||||
+ (NSString *)colorForDynamicId:(NSString *)dynamicId {
|
||||
if (!dynamicId) return nil;
|
||||
|
||||
NSDictionary *colorDict = [self loadColorDictionary];
|
||||
return colorDict[dynamicId];
|
||||
}
|
||||
|
||||
+ (void)removeColorForDynamicId:(NSString *)dynamicId {
|
||||
if (!dynamicId) return;
|
||||
|
||||
NSMutableDictionary *colorDict = [[self loadColorDictionary] mutableCopy];
|
||||
[colorDict removeObjectForKey:dynamicId];
|
||||
|
||||
[[NSUserDefaults standardUserDefaults] setObject:colorDict forKey:kEmotionColorStorageKey];
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
}
|
||||
|
||||
+ (NSArray<NSString *> *)allEmotionColors {
|
||||
return @[
|
||||
@"#FFD700",
|
||||
@"#4A90E2",
|
||||
@"#E74C3C",
|
||||
@"#9B59B6",
|
||||
@"#FF9A3D",
|
||||
@"#2ECC71",
|
||||
@"#3498DB",
|
||||
@"#F39C12"
|
||||
];
|
||||
}
|
||||
|
||||
+ (NSString *)randomEmotionColor {
|
||||
NSArray *colors = [self allEmotionColors];
|
||||
uint32_t randomIndex = arc4random_uniform((uint32_t)colors.count);
|
||||
return colors[randomIndex];
|
||||
}
|
||||
|
||||
+ (NSString *)emotionNameForColor:(NSString *)hexColor {
|
||||
if (!hexColor || hexColor.length == 0) return nil;
|
||||
|
||||
NSArray<NSString *> *colors = [self allEmotionColors];
|
||||
NSArray<NSString *> *emotions = @[@"Joy", @"Sadness", @"Anger", @"Fear", @"Surprise", @"Disgust", @"Trust", @"Anticipation"];
|
||||
|
||||
|
||||
NSString *upperHex = [hexColor uppercaseString];
|
||||
for (NSInteger i = 0; i < colors.count; i++) {
|
||||
if ([[colors[i] uppercaseString] isEqualToString:upperHex]) {
|
||||
return emotions[i];
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
#pragma mark - Private Methods
|
||||
|
||||
+ (NSDictionary *)loadColorDictionary {
|
||||
NSDictionary *dict = [[NSUserDefaults standardUserDefaults] dictionaryForKey:kEmotionColorStorageKey];
|
||||
return dict ?: @{};
|
||||
}
|
||||
|
||||
#pragma mark - User Signature Color
|
||||
|
||||
+ (void)saveUserSignatureColor:(NSString *)hexColor {
|
||||
if (!hexColor) return;
|
||||
|
||||
[[NSUserDefaults standardUserDefaults] setObject:hexColor forKey:kUserSignatureColorKey];
|
||||
[[NSUserDefaults standardUserDefaults] setObject:@([[NSDate date] timeIntervalSince1970])
|
||||
forKey:kUserSignatureTimestampKey];
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
|
||||
NSLog(@"[EPEmotionColorStorage] 保存用户专属颜色: %@", hexColor);
|
||||
}
|
||||
|
||||
+ (NSString *)userSignatureColor {
|
||||
return [[NSUserDefaults standardUserDefaults] stringForKey:kUserSignatureColorKey];
|
||||
}
|
||||
|
||||
+ (BOOL)hasUserSignatureColor {
|
||||
return [self userSignatureColor] != nil;
|
||||
}
|
||||
|
||||
+ (void)clearUserSignatureColor {
|
||||
[[NSUserDefaults standardUserDefaults] removeObjectForKey:kUserSignatureColorKey];
|
||||
[[NSUserDefaults standardUserDefaults] removeObjectForKey:kUserSignatureTimestampKey];
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
|
||||
NSLog(@"[EPEmotionColorStorage] 清除用户专属颜色");
|
||||
}
|
||||
|
||||
@end
|
||||
95
YuMi/E-P/NewMoments/Services/EPMomentAPISwiftHelper.swift
Normal file
95
YuMi/E-P/NewMoments/Services/EPMomentAPISwiftHelper.swift
Normal file
@@ -0,0 +1,95 @@
|
||||
|
||||
|
||||
// Created by AI on 2025-10-11.
|
||||
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
@objc class EPMomentAPISwiftHelper: NSObject {
|
||||
|
||||
|
||||
@objc func fetchLatestMomentsWithNextID(
|
||||
_ nextID: String,
|
||||
completion: @escaping ([MomentsInfoModel], String) -> Void,
|
||||
failure: @escaping (Int, String) -> Void
|
||||
) {
|
||||
let pageSize = "20"
|
||||
let types = "0,2"
|
||||
|
||||
NSLog("[EPMomentAPISwiftHelper] 🔄 开始请求动态列表,nextID=\(nextID.isEmpty ? "(首页)" : nextID)")
|
||||
|
||||
Api.momentsLatestList({ (data, code, msg) in
|
||||
NSLog("[EPMomentAPISwiftHelper] 📥 收到响应,code=\(code)")
|
||||
|
||||
if code == 200, let dict = data?.data as? NSDictionary {
|
||||
NSLog("[EPMomentAPISwiftHelper] 📦 开始解析数据字典")
|
||||
|
||||
if let listInfo = MomentsListInfoModel.mj_object(withKeyValues: dict) {
|
||||
let dynamicList = listInfo.dynamicList
|
||||
let nextDynamicId = listInfo.nextDynamicId
|
||||
NSLog("[EPMomentAPISwiftHelper] ✅ 解析成功,dynamicList.count=\(dynamicList.count), nextDynamicId=\(nextDynamicId)")
|
||||
completion(dynamicList, nextDynamicId)
|
||||
} else {
|
||||
NSLog("[EPMomentAPISwiftHelper] ⚠️ 解析失败,返回空数组")
|
||||
completion([], "")
|
||||
}
|
||||
} else {
|
||||
NSLog("[EPMomentAPISwiftHelper] ❌ 请求失败,code=\(code), msg=\(msg ?? "无错误信息")")
|
||||
failure(Int(code), msg ?? YMLocalizedString("error.request_failed"))
|
||||
}
|
||||
}, dynamicId: nextID, pageSize: pageSize, types: types)
|
||||
}
|
||||
|
||||
|
||||
@objc func publishMoment(
|
||||
type: String,
|
||||
content: String,
|
||||
resList: [[String: Any]],
|
||||
completion: @escaping () -> Void,
|
||||
failure: @escaping (Int, String) -> Void
|
||||
) {
|
||||
guard let uid = AccountInfoStorage.instance().getUid() else {
|
||||
failure(-1, YMLocalizedString("error.not_logged_in"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// NOTE: 旧版本 XPMonentsPublishViewController 包含话题选择功能
|
||||
|
||||
|
||||
Api.momentsPublish({ (data, code, msg) in
|
||||
if code == 200 {
|
||||
completion()
|
||||
} else {
|
||||
failure(Int(code), msg ?? YMLocalizedString("error.publish_failed"))
|
||||
}
|
||||
}, uid: uid, type: type, worldId: "", content: content, resList: resList)
|
||||
}
|
||||
|
||||
|
||||
@objc func likeMoment(
|
||||
dynamicId: String,
|
||||
isLike: Bool,
|
||||
likedUid: String,
|
||||
worldId: Int,
|
||||
completion: @escaping () -> Void,
|
||||
failure: @escaping (Int, String) -> Void
|
||||
) {
|
||||
guard let uid = AccountInfoStorage.instance().getUid() else {
|
||||
failure(-1, YMLocalizedString("error.not_logged_in"))
|
||||
return
|
||||
}
|
||||
|
||||
let status = isLike ? "1" : "0"
|
||||
let worldIdStr = String(format: "%ld", worldId)
|
||||
|
||||
Api.momentsLike({ (data, code, msg) in
|
||||
if code == 200 {
|
||||
completion()
|
||||
} else {
|
||||
failure(Int(code), msg ?? YMLocalizedString("error.like_failed"))
|
||||
}
|
||||
}, dynamicId: dynamicId, uid: uid, status: status, likedUid: likedUid, worldId: worldIdStr)
|
||||
}
|
||||
}
|
||||
31
YuMi/E-P/NewMoments/Views/EPEmotionColorPicker.h
Normal file
31
YuMi/E-P/NewMoments/Views/EPEmotionColorPicker.h
Normal file
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// EPEmotionColorPicker.h
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-10-14.
|
||||
// 情绪色轮选择器 - 环形布局
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface EPEmotionColorPicker : UIView
|
||||
|
||||
/// 颜色选择回调
|
||||
@property (nonatomic, copy) void(^onColorSelected)(NSString *hexColor);
|
||||
|
||||
/// 预选中的颜色(用于标记默认选中状态)
|
||||
@property (nonatomic, copy) NSString *preselectedColor;
|
||||
|
||||
/// 在指定视图中显示选择器
|
||||
/// @param parentView 父视图(通常是 ViewController 的 view)
|
||||
- (void)showInView:(UIView *)parentView;
|
||||
|
||||
/// 关闭选择器
|
||||
- (void)dismiss;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
299
YuMi/E-P/NewMoments/Views/EPEmotionColorPicker.m
Normal file
299
YuMi/E-P/NewMoments/Views/EPEmotionColorPicker.m
Normal file
@@ -0,0 +1,299 @@
|
||||
|
||||
|
||||
// Created by AI on 2025-10-14.
|
||||
|
||||
|
||||
#import "EPEmotionColorPicker.h"
|
||||
#import "EPEmotionColorWheelView.h"
|
||||
#import "EPEmotionInfoView.h"
|
||||
#import <Masonry/Masonry.h>
|
||||
|
||||
@interface EPEmotionColorPicker ()
|
||||
|
||||
@property (nonatomic, strong) UIView *backgroundMask;
|
||||
@property (nonatomic, strong) UIView *containerView;
|
||||
@property (nonatomic, strong) UILabel *titleLabel;
|
||||
@property (nonatomic, strong) UIButton *infoButton;
|
||||
@property (nonatomic, strong) UIView *selectedColorView;
|
||||
@property (nonatomic, strong) UILabel *selectedColorLabel;
|
||||
@property (nonatomic, strong) UIButton *okButton;
|
||||
@property (nonatomic, strong) EPEmotionColorWheelView *colorWheelView;
|
||||
@property (nonatomic, copy) NSString *currentSelectedColor;
|
||||
@property (nonatomic, assign) NSInteger currentSelectedIndex;
|
||||
|
||||
@end
|
||||
|
||||
@implementation EPEmotionColorPicker
|
||||
|
||||
#pragma mark - Lifecycle
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
[self setupUI];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setupUI {
|
||||
self.backgroundColor = [UIColor clearColor];
|
||||
|
||||
|
||||
[self addSubview:self.backgroundMask];
|
||||
[self.backgroundMask mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(self);
|
||||
}];
|
||||
|
||||
|
||||
[self addSubview:self.containerView];
|
||||
[self.containerView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.trailing.bottom.equalTo(self);
|
||||
make.height.mas_equalTo(450);
|
||||
}];
|
||||
|
||||
|
||||
[self.containerView addSubview:self.titleLabel];
|
||||
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.containerView).offset(20);
|
||||
make.centerX.equalTo(self.containerView);
|
||||
}];
|
||||
|
||||
|
||||
[self.containerView addSubview:self.infoButton];
|
||||
[self.infoButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.equalTo(self.containerView).offset(16);
|
||||
make.centerY.equalTo(self.titleLabel);
|
||||
make.size.mas_equalTo(CGSizeMake(28, 28));
|
||||
}];
|
||||
|
||||
|
||||
[self.containerView addSubview:self.okButton];
|
||||
[self.okButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.trailing.equalTo(self.containerView).offset(-16);
|
||||
make.centerY.equalTo(self.titleLabel);
|
||||
make.size.mas_equalTo(CGSizeMake(60, 32));
|
||||
}];
|
||||
|
||||
|
||||
[self.containerView addSubview:self.selectedColorView];
|
||||
[self.selectedColorView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.titleLabel.mas_bottom).offset(20);
|
||||
make.centerX.equalTo(self.containerView);
|
||||
make.height.mas_equalTo(50);
|
||||
make.leading.trailing.equalTo(self.containerView).inset(20);
|
||||
}];
|
||||
|
||||
|
||||
[self.containerView addSubview:self.colorWheelView];
|
||||
[self.colorWheelView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.centerX.equalTo(self.containerView);
|
||||
make.top.equalTo(self.selectedColorView.mas_bottom).offset(20);
|
||||
make.size.mas_equalTo(CGSizeMake(280, 280));
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Actions
|
||||
|
||||
- (void)onBackgroundTapped {
|
||||
[self dismiss];
|
||||
}
|
||||
|
||||
- (void)onInfoButtonTapped {
|
||||
EPEmotionInfoView *infoView = [[EPEmotionInfoView alloc] init];
|
||||
[infoView showInView:self];
|
||||
}
|
||||
|
||||
- (void)onOkButtonTapped {
|
||||
if (self.currentSelectedColor && self.onColorSelected) {
|
||||
self.onColorSelected(self.currentSelectedColor);
|
||||
}
|
||||
[self dismiss];
|
||||
}
|
||||
|
||||
#pragma mark - Public Methods
|
||||
|
||||
- (void)showInView:(UIView *)parentView {
|
||||
[parentView addSubview:self];
|
||||
[self mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(parentView);
|
||||
}];
|
||||
|
||||
|
||||
self.backgroundMask.alpha = 0;
|
||||
self.containerView.transform = CGAffineTransformMakeTranslation(0, 450);
|
||||
|
||||
|
||||
[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
|
||||
self.backgroundMask.alpha = 1;
|
||||
self.containerView.transform = CGAffineTransformIdentity;
|
||||
} completion:nil];
|
||||
}
|
||||
|
||||
- (void)dismiss {
|
||||
[UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
|
||||
self.backgroundMask.alpha = 0;
|
||||
self.containerView.transform = CGAffineTransformMakeTranslation(0, 450);
|
||||
} completion:^(BOOL finished) {
|
||||
[self removeFromSuperview];
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Lazy Loading
|
||||
|
||||
- (UIView *)backgroundMask {
|
||||
if (!_backgroundMask) {
|
||||
_backgroundMask = [[UIView alloc] init];
|
||||
_backgroundMask.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.5];
|
||||
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onBackgroundTapped)];
|
||||
[_backgroundMask addGestureRecognizer:tap];
|
||||
}
|
||||
return _backgroundMask;
|
||||
}
|
||||
|
||||
- (UIView *)containerView {
|
||||
if (!_containerView) {
|
||||
_containerView = [[UIView alloc] init];
|
||||
_containerView.backgroundColor = [UIColor colorWithRed:0x0C/255.0 green:0x05/255.0 blue:0x27/255.0 alpha:1.0];
|
||||
_containerView.layer.cornerRadius = 20;
|
||||
_containerView.layer.maskedCorners = kCALayerMinXMinYCorner | kCALayerMaxXMinYCorner;
|
||||
_containerView.layer.masksToBounds = YES;
|
||||
}
|
||||
return _containerView;
|
||||
}
|
||||
|
||||
- (UILabel *)titleLabel {
|
||||
if (!_titleLabel) {
|
||||
_titleLabel = [[UILabel alloc] init];
|
||||
_titleLabel.text = @"Choose your emotion";
|
||||
_titleLabel.textColor = [UIColor whiteColor];
|
||||
_titleLabel.font = [UIFont systemFontOfSize:20 weight:UIFontWeightSemibold];
|
||||
_titleLabel.textAlignment = NSTextAlignmentCenter;
|
||||
}
|
||||
return _titleLabel;
|
||||
}
|
||||
|
||||
- (UIButton *)infoButton {
|
||||
if (!_infoButton) {
|
||||
_infoButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
|
||||
|
||||
UIImage *infoIcon = [UIImage systemImageNamed:@"info.circle"];
|
||||
[_infoButton setImage:infoIcon forState:UIControlStateNormal];
|
||||
_infoButton.tintColor = [[UIColor whiteColor] colorWithAlphaComponent:0.7];
|
||||
|
||||
|
||||
[_infoButton addTarget:self action:@selector(onInfoButtonTapped) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
return _infoButton;
|
||||
}
|
||||
|
||||
- (UIView *)selectedColorView {
|
||||
if (!_selectedColorView) {
|
||||
_selectedColorView = [[UIView alloc] init];
|
||||
_selectedColorView.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.1];
|
||||
_selectedColorView.layer.cornerRadius = 25;
|
||||
_selectedColorView.layer.masksToBounds = YES;
|
||||
_selectedColorView.hidden = YES;
|
||||
|
||||
|
||||
UIView *colorDot = [[UIView alloc] init];
|
||||
colorDot.tag = 100;
|
||||
colorDot.layer.cornerRadius = 12;
|
||||
colorDot.layer.masksToBounds = YES;
|
||||
[_selectedColorView addSubview:colorDot];
|
||||
[colorDot mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.equalTo(_selectedColorView).offset(15);
|
||||
make.centerY.equalTo(_selectedColorView);
|
||||
make.size.mas_equalTo(CGSizeMake(24, 24));
|
||||
}];
|
||||
|
||||
|
||||
[_selectedColorView addSubview:self.selectedColorLabel];
|
||||
[self.selectedColorLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.equalTo(colorDot.mas_trailing).offset(12);
|
||||
make.centerY.equalTo(_selectedColorView);
|
||||
make.trailing.equalTo(_selectedColorView).offset(-15);
|
||||
}];
|
||||
}
|
||||
return _selectedColorView;
|
||||
}
|
||||
|
||||
- (UILabel *)selectedColorLabel {
|
||||
if (!_selectedColorLabel) {
|
||||
_selectedColorLabel = [[UILabel alloc] init];
|
||||
_selectedColorLabel.textColor = [UIColor whiteColor];
|
||||
_selectedColorLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
|
||||
_selectedColorLabel.text = @"Select an emotion";
|
||||
}
|
||||
return _selectedColorLabel;
|
||||
}
|
||||
|
||||
- (UIButton *)okButton {
|
||||
if (!_okButton) {
|
||||
_okButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
[_okButton setTitle:@"OK" forState:UIControlStateNormal];
|
||||
[_okButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
||||
_okButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold];
|
||||
_okButton.backgroundColor = [UIColor colorWithRed:0x9B/255.0 green:0x59/255.0 blue:0xB6/255.0 alpha:1.0];
|
||||
_okButton.layer.cornerRadius = 16;
|
||||
_okButton.layer.masksToBounds = YES;
|
||||
_okButton.enabled = NO;
|
||||
_okButton.alpha = 0.5;
|
||||
[_okButton addTarget:self action:@selector(onOkButtonTapped) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
return _okButton;
|
||||
}
|
||||
|
||||
- (EPEmotionColorWheelView *)colorWheelView {
|
||||
if (!_colorWheelView) {
|
||||
_colorWheelView = [[EPEmotionColorWheelView alloc] init];
|
||||
_colorWheelView.radius = 100.0;
|
||||
_colorWheelView.buttonSize = 50.0;
|
||||
_colorWheelView.preselectedColor = self.preselectedColor;
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
_colorWheelView.onColorTapped = ^(NSString *hexColor, NSInteger index) {
|
||||
__strong typeof(weakSelf) self = weakSelf;
|
||||
|
||||
|
||||
self.currentSelectedColor = hexColor;
|
||||
self.currentSelectedIndex = index;
|
||||
|
||||
|
||||
[self updateSelectedColorDisplay:hexColor index:index];
|
||||
};
|
||||
}
|
||||
return _colorWheelView;
|
||||
}
|
||||
|
||||
|
||||
- (void)updateSelectedColorDisplay:(NSString *)hexColor index:(NSInteger)index {
|
||||
NSArray<NSString *> *emotions = @[@"Joy", @"Sadness", @"Anger", @"Fear", @"Surprise", @"Disgust", @"Trust", @"Anticipation"];
|
||||
|
||||
|
||||
self.selectedColorView.hidden = NO;
|
||||
|
||||
|
||||
UIView *colorDot = [self.selectedColorView viewWithTag:100];
|
||||
colorDot.backgroundColor = [self colorFromHex:hexColor];
|
||||
|
||||
|
||||
self.selectedColorLabel.text = emotions[index];
|
||||
|
||||
|
||||
self.okButton.enabled = YES;
|
||||
self.okButton.alpha = 1.0;
|
||||
}
|
||||
|
||||
|
||||
- (UIColor *)colorFromHex:(NSString *)hexString {
|
||||
unsigned rgbValue = 0;
|
||||
NSScanner *scanner = [NSScanner scannerWithString:hexString];
|
||||
[scanner setScanLocation:1];
|
||||
[scanner scanHexInt:&rgbValue];
|
||||
return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16)/255.0
|
||||
green:((rgbValue & 0xFF00) >> 8)/255.0
|
||||
blue:(rgbValue & 0xFF)/255.0
|
||||
alpha:1.0];
|
||||
}
|
||||
|
||||
@end
|
||||
42
YuMi/E-P/NewMoments/Views/EPEmotionColorWheelView.h
Normal file
42
YuMi/E-P/NewMoments/Views/EPEmotionColorWheelView.h
Normal file
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// EPEmotionColorWheelView.h
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-10-15.
|
||||
// 共享情绪色轮组件 - 纯渲染逻辑,不包含容器和外部交互
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface EPEmotionColorWheelView : UIView
|
||||
|
||||
#pragma mark - Configuration
|
||||
|
||||
/// 圆周半径(默认 80pt)
|
||||
@property (nonatomic, assign) CGFloat radius;
|
||||
|
||||
/// 按钮直径(默认 50pt)
|
||||
@property (nonatomic, assign) CGFloat buttonSize;
|
||||
|
||||
/// 预选中的颜色(Hex 格式,如 #FFD700)
|
||||
@property (nonatomic, copy, nullable) NSString *preselectedColor;
|
||||
|
||||
#pragma mark - Callbacks
|
||||
|
||||
/// 颜色点击回调
|
||||
/// @param hexColor 选中的颜色值
|
||||
/// @param index 颜色索引 (0-7)
|
||||
@property (nonatomic, copy) void(^onColorTapped)(NSString *hexColor, NSInteger index);
|
||||
|
||||
#pragma mark - Methods
|
||||
|
||||
/// 刷新色轮(支持动态更新预选中颜色)
|
||||
/// @param color 新的预选中颜色
|
||||
- (void)reloadWithPreselectedColor:(nullable NSString *)color;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
144
YuMi/E-P/NewMoments/Views/EPEmotionColorWheelView.m
Normal file
144
YuMi/E-P/NewMoments/Views/EPEmotionColorWheelView.m
Normal file
@@ -0,0 +1,144 @@
|
||||
|
||||
|
||||
// Created by AI on 2025-10-15.
|
||||
|
||||
|
||||
#import "EPEmotionColorWheelView.h"
|
||||
#import "EPEmotionColorStorage.h"
|
||||
|
||||
@interface EPEmotionColorWheelView ()
|
||||
|
||||
@property (nonatomic, strong) NSMutableArray<UIButton *> *colorButtons;
|
||||
@property (nonatomic, assign) NSInteger selectedIndex;
|
||||
|
||||
@end
|
||||
|
||||
@implementation EPEmotionColorWheelView
|
||||
|
||||
#pragma mark - Lifecycle
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
|
||||
_radius = 80.0;
|
||||
_buttonSize = 50.0;
|
||||
_colorButtons = [NSMutableArray array];
|
||||
|
||||
self.backgroundColor = [UIColor clearColor];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
|
||||
if (self.colorButtons.count == 0) {
|
||||
[self createColorButtons];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Public Methods
|
||||
|
||||
- (void)reloadWithPreselectedColor:(NSString *)color {
|
||||
self.preselectedColor = color;
|
||||
|
||||
|
||||
for (UIButton *btn in self.colorButtons) {
|
||||
[btn removeFromSuperview];
|
||||
}
|
||||
[self.colorButtons removeAllObjects];
|
||||
|
||||
|
||||
[self createColorButtons];
|
||||
}
|
||||
|
||||
#pragma mark - Private Methods
|
||||
|
||||
- (void)createColorButtons {
|
||||
NSArray<NSString *> *colors = [EPEmotionColorStorage allEmotionColors];
|
||||
NSArray<NSString *> *emotions = @[@"Joy", @"Sadness", @"Anger", @"Fear", @"Surprise", @"Disgust", @"Trust", @"Anticipation"];
|
||||
|
||||
CGFloat angleStep = M_PI * 2.0 / colors.count;
|
||||
CGFloat centerX = CGRectGetWidth(self.bounds) / 2.0;
|
||||
CGFloat centerY = CGRectGetHeight(self.bounds) / 2.0;
|
||||
|
||||
for (NSInteger i = 0; i < colors.count; i++) {
|
||||
|
||||
CGFloat angle = angleStep * i - M_PI_2;
|
||||
CGFloat x = centerX + self.radius * cos(angle) - self.buttonSize / 2.0;
|
||||
CGFloat y = centerY + self.radius * sin(angle) - self.buttonSize / 2.0;
|
||||
|
||||
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
button.frame = CGRectMake(x, y, self.buttonSize, self.buttonSize);
|
||||
button.backgroundColor = [self colorFromHex:colors[i]];
|
||||
button.layer.cornerRadius = self.buttonSize / 2.0;
|
||||
button.layer.masksToBounds = YES;
|
||||
button.layer.borderWidth = 3.0;
|
||||
button.layer.borderColor = [UIColor whiteColor].CGColor;
|
||||
button.tag = i;
|
||||
[button addTarget:self action:@selector(onButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
|
||||
if (self.preselectedColor && [colors[i] isEqualToString:self.preselectedColor]) {
|
||||
button.layer.borderWidth = 5.0;
|
||||
button.transform = CGAffineTransformMakeScale(1.1, 1.1);
|
||||
}
|
||||
|
||||
|
||||
button.layer.shadowColor = [self colorFromHex:colors[i]].CGColor;
|
||||
button.layer.shadowOffset = CGSizeMake(0, 2);
|
||||
button.layer.shadowOpacity = 0.6;
|
||||
button.layer.shadowRadius = 8;
|
||||
button.layer.masksToBounds = NO;
|
||||
|
||||
[self addSubview:button];
|
||||
[self.colorButtons addObject:button];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)onButtonTapped:(UIButton *)sender {
|
||||
NSInteger index = sender.tag;
|
||||
self.selectedIndex = index;
|
||||
|
||||
|
||||
[self updateSelectionState];
|
||||
|
||||
|
||||
NSArray<NSString *> *colors = [EPEmotionColorStorage allEmotionColors];
|
||||
NSString *selectedColor = colors[index];
|
||||
if (self.onColorTapped) {
|
||||
self.onColorTapped(selectedColor, index);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)updateSelectionState {
|
||||
for (NSInteger i = 0; i < self.colorButtons.count; i++) {
|
||||
UIButton *button = self.colorButtons[i];
|
||||
if (i == self.selectedIndex) {
|
||||
|
||||
button.layer.borderWidth = 5.0;
|
||||
button.transform = CGAffineTransformMakeScale(1.1, 1.1);
|
||||
} else {
|
||||
|
||||
button.layer.borderWidth = 3.0;
|
||||
button.transform = CGAffineTransformIdentity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Utilities
|
||||
|
||||
- (UIColor *)colorFromHex:(NSString *)hexString {
|
||||
unsigned rgbValue = 0;
|
||||
NSScanner *scanner = [NSScanner scannerWithString:hexString];
|
||||
[scanner setScanLocation:1];
|
||||
[scanner scanHexInt:&rgbValue];
|
||||
return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16)/255.0
|
||||
green:((rgbValue & 0xFF00) >> 8)/255.0
|
||||
blue:(rgbValue & 0xFF)/255.0
|
||||
alpha:1.0];
|
||||
}
|
||||
|
||||
@end
|
||||
25
YuMi/E-P/NewMoments/Views/EPEmotionInfoView.h
Normal file
25
YuMi/E-P/NewMoments/Views/EPEmotionInfoView.h
Normal file
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// EPEmotionInfoView.h
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-10-16.
|
||||
// 普拉奇克情绪轮说明视图
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface EPEmotionInfoView : UIView
|
||||
|
||||
/// 在指定视图中显示说明
|
||||
/// @param parentView 父视图
|
||||
- (void)showInView:(UIView *)parentView;
|
||||
|
||||
/// 关闭说明视图
|
||||
- (void)dismiss;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
210
YuMi/E-P/NewMoments/Views/EPEmotionInfoView.m
Normal file
210
YuMi/E-P/NewMoments/Views/EPEmotionInfoView.m
Normal file
@@ -0,0 +1,210 @@
|
||||
|
||||
|
||||
// Created by AI on 2025-10-16.
|
||||
|
||||
|
||||
#import "EPEmotionInfoView.h"
|
||||
#import <Masonry/Masonry.h>
|
||||
|
||||
@interface EPEmotionInfoView ()
|
||||
|
||||
@property (nonatomic, strong) UIView *backgroundMask;
|
||||
@property (nonatomic, strong) UIView *contentContainer;
|
||||
@property (nonatomic, strong) UILabel *titleLabel;
|
||||
@property (nonatomic, strong) UIScrollView *scrollView;
|
||||
@property (nonatomic, strong) UILabel *contentLabel;
|
||||
@property (nonatomic, strong) UIButton *closeButton;
|
||||
|
||||
@end
|
||||
|
||||
@implementation EPEmotionInfoView
|
||||
|
||||
#pragma mark - Lifecycle
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
[self setupUI];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setupUI {
|
||||
self.backgroundColor = [UIColor clearColor];
|
||||
|
||||
|
||||
[self addSubview:self.backgroundMask];
|
||||
[self.backgroundMask mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(self);
|
||||
}];
|
||||
|
||||
|
||||
[self addSubview:self.contentContainer];
|
||||
[self.contentContainer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.center.equalTo(self);
|
||||
make.leading.trailing.equalTo(self).inset(30);
|
||||
make.height.mas_lessThanOrEqualTo(500);
|
||||
}];
|
||||
|
||||
|
||||
[self.contentContainer addSubview:self.titleLabel];
|
||||
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.contentContainer).offset(24);
|
||||
make.leading.trailing.equalTo(self.contentContainer).inset(20);
|
||||
}];
|
||||
|
||||
|
||||
[self.contentContainer addSubview:self.scrollView];
|
||||
[self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.titleLabel.mas_bottom).offset(16);
|
||||
make.leading.trailing.equalTo(self.contentContainer).inset(20);
|
||||
make.height.mas_lessThanOrEqualTo(320);
|
||||
}];
|
||||
|
||||
|
||||
[self.scrollView addSubview:self.contentLabel];
|
||||
[self.contentLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(self.scrollView);
|
||||
make.width.equalTo(self.scrollView);
|
||||
}];
|
||||
|
||||
|
||||
[self.contentContainer addSubview:self.closeButton];
|
||||
[self.closeButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.scrollView.mas_bottom).offset(20);
|
||||
make.centerX.equalTo(self.contentContainer);
|
||||
make.leading.trailing.equalTo(self.contentContainer).inset(20);
|
||||
make.height.mas_equalTo(50);
|
||||
make.bottom.equalTo(self.contentContainer).offset(-24);
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Actions
|
||||
|
||||
- (void)onBackgroundTapped {
|
||||
[self dismiss];
|
||||
}
|
||||
|
||||
- (void)onCloseButtonTapped {
|
||||
[self dismiss];
|
||||
}
|
||||
|
||||
#pragma mark - Public Methods
|
||||
|
||||
- (void)showInView:(UIView *)parentView {
|
||||
[parentView addSubview:self];
|
||||
[self mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(parentView);
|
||||
}];
|
||||
|
||||
|
||||
self.backgroundMask.alpha = 0;
|
||||
self.contentContainer.alpha = 0;
|
||||
self.contentContainer.transform = CGAffineTransformMakeScale(0.9, 0.9);
|
||||
|
||||
|
||||
[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
|
||||
self.backgroundMask.alpha = 1;
|
||||
self.contentContainer.alpha = 1;
|
||||
self.contentContainer.transform = CGAffineTransformIdentity;
|
||||
} completion:nil];
|
||||
}
|
||||
|
||||
- (void)dismiss {
|
||||
[UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
|
||||
self.backgroundMask.alpha = 0;
|
||||
self.contentContainer.alpha = 0;
|
||||
self.contentContainer.transform = CGAffineTransformMakeScale(0.95, 0.95);
|
||||
} completion:^(BOOL finished) {
|
||||
[self removeFromSuperview];
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Lazy Loading
|
||||
|
||||
- (UIView *)backgroundMask {
|
||||
if (!_backgroundMask) {
|
||||
_backgroundMask = [[UIView alloc] init];
|
||||
_backgroundMask.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.6];
|
||||
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onBackgroundTapped)];
|
||||
[_backgroundMask addGestureRecognizer:tap];
|
||||
}
|
||||
return _backgroundMask;
|
||||
}
|
||||
|
||||
- (UIView *)contentContainer {
|
||||
if (!_contentContainer) {
|
||||
_contentContainer = [[UIView alloc] init];
|
||||
_contentContainer.backgroundColor = [UIColor colorWithRed:0x1a/255.0 green:0x1a/255.0 blue:0x2e/255.0 alpha:1.0];
|
||||
_contentContainer.layer.cornerRadius = 16;
|
||||
_contentContainer.layer.masksToBounds = YES;
|
||||
}
|
||||
return _contentContainer;
|
||||
}
|
||||
|
||||
- (UILabel *)titleLabel {
|
||||
if (!_titleLabel) {
|
||||
_titleLabel = [[UILabel alloc] init];
|
||||
_titleLabel.text = @"About Emotion Colors";
|
||||
_titleLabel.textColor = [UIColor whiteColor];
|
||||
_titleLabel.font = [UIFont systemFontOfSize:20 weight:UIFontWeightBold];
|
||||
_titleLabel.textAlignment = NSTextAlignmentCenter;
|
||||
}
|
||||
return _titleLabel;
|
||||
}
|
||||
|
||||
- (UIScrollView *)scrollView {
|
||||
if (!_scrollView) {
|
||||
_scrollView = [[UIScrollView alloc] init];
|
||||
_scrollView.showsVerticalScrollIndicator = YES;
|
||||
_scrollView.alwaysBounceVertical = YES;
|
||||
}
|
||||
return _scrollView;
|
||||
}
|
||||
|
||||
- (UILabel *)contentLabel {
|
||||
if (!_contentLabel) {
|
||||
_contentLabel = [[UILabel alloc] init];
|
||||
_contentLabel.numberOfLines = 0;
|
||||
_contentLabel.textColor = [[UIColor whiteColor] colorWithAlphaComponent:0.9];
|
||||
_contentLabel.font = [UIFont systemFontOfSize:15];
|
||||
|
||||
|
||||
NSString *content = @"Based on Plutchik's Wheel of Emotions, we use 8 core colors to represent fundamental human emotions:\n\n"
|
||||
"🟡 Joy (Gold)\n"
|
||||
"Represents happiness, delight, and cheerfulness. Like sunshine warming your heart.\n\n"
|
||||
"🔵 Sadness (Sky Blue)\n"
|
||||
"Reflects sorrow, melancholy, and contemplation. The quiet depth of blue skies.\n\n"
|
||||
"🔴 Anger (Coral Red)\n"
|
||||
"Expresses frustration, rage, and intensity. The fire of passionate emotions.\n\n"
|
||||
"🟣 Fear (Violet)\n"
|
||||
"Embodies anxiety, worry, and apprehension. The uncertainty of purple twilight.\n\n"
|
||||
"🟠 Surprise (Amber)\n"
|
||||
"Captures amazement, shock, and wonder. The spark of unexpected moments.\n\n"
|
||||
"🟢 Disgust (Emerald)\n"
|
||||
"Conveys aversion, distaste, and rejection. The instinctive green of caution.\n\n"
|
||||
"🔵 Trust (Bright Blue)\n"
|
||||
"Symbolizes confidence, faith, and security. The clarity of open skies.\n\n"
|
||||
"🟡 Anticipation (Amber)\n"
|
||||
"Represents expectation, hope, and eagerness. The warmth of looking forward.\n\n"
|
||||
"Each color helps you express your current emotional state in moments you share.";
|
||||
|
||||
_contentLabel.text = content;
|
||||
}
|
||||
return _contentLabel;
|
||||
}
|
||||
|
||||
- (UIButton *)closeButton {
|
||||
if (!_closeButton) {
|
||||
_closeButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
[_closeButton setTitle:@"Got it" forState:UIControlStateNormal];
|
||||
[_closeButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
||||
_closeButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold];
|
||||
_closeButton.backgroundColor = [UIColor colorWithRed:0x9B/255.0 green:0x59/255.0 blue:0xB6/255.0 alpha:1.0];
|
||||
_closeButton.layer.cornerRadius = 25;
|
||||
_closeButton.layer.masksToBounds = YES;
|
||||
[_closeButton addTarget:self action:@selector(onCloseButtonTapped) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
return _closeButton;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -8,15 +8,18 @@
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class MomentsInfoModel;
|
||||
@class SDPhotoBrowser;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// 新的动态 Cell(卡片式设计)
|
||||
/// 完全不同于原 XPMomentsCell 的列表式设计
|
||||
@interface NewMomentCell : UITableViewCell
|
||||
@interface EPMomentCell : UITableViewCell
|
||||
|
||||
/// 配置 Cell 数据
|
||||
/// @param data 动态数据字典
|
||||
- (void)configureWithData:(NSDictionary *)data;
|
||||
/// @param model 动态数据模型
|
||||
- (void)configureWithModel:(MomentsInfoModel *)model;
|
||||
|
||||
@end
|
||||
|
||||
556
YuMi/E-P/NewMoments/Views/EPMomentCell.m
Normal file
556
YuMi/E-P/NewMoments/Views/EPMomentCell.m
Normal file
@@ -0,0 +1,556 @@
|
||||
|
||||
|
||||
// Created by AI on 2025-10-09.
|
||||
// Copyright © 2025 YuMi. All rights reserved.
|
||||
|
||||
|
||||
#import "EPMomentCell.h"
|
||||
#import "MomentsInfoModel.h"
|
||||
#import "AccountInfoStorage.h"
|
||||
#import "NetImageView.h"
|
||||
#import "EPEmotionColorStorage.h"
|
||||
#import "SDPhotoBrowser.h"
|
||||
#import "YuMi-Swift.h"
|
||||
|
||||
@interface EPMomentCell () <SDPhotoBrowserDelegate>
|
||||
|
||||
// MARK: - UI Components
|
||||
|
||||
|
||||
@property (nonatomic, strong) UIView *cardView;
|
||||
|
||||
|
||||
@property (nonatomic, strong) UIView *colorBackgroundView;
|
||||
|
||||
|
||||
@property (nonatomic, strong) UIVisualEffectView *blurEffectView;
|
||||
|
||||
|
||||
@property (nonatomic, strong) NetImageView *avatarImageView;
|
||||
|
||||
|
||||
@property (nonatomic, strong) UILabel *nameLabel;
|
||||
|
||||
|
||||
@property (nonatomic, strong) UILabel *timeLabel;
|
||||
|
||||
|
||||
@property (nonatomic, strong) UILabel *contentLabel;
|
||||
|
||||
|
||||
@property (nonatomic, strong) UIView *imagesContainer;
|
||||
@property (nonatomic, strong) NSMutableArray<NetImageView *> *imageViews;
|
||||
|
||||
|
||||
@property (nonatomic, strong) UIView *actionBar;
|
||||
|
||||
|
||||
@property (nonatomic, strong) UIButton *likeButton;
|
||||
|
||||
|
||||
@property (nonatomic, strong) UIButton *commentButton;
|
||||
|
||||
|
||||
@property (nonatomic, strong) MomentsInfoModel *currentModel;
|
||||
|
||||
|
||||
@property (nonatomic, strong) EPMomentAPISwiftHelper *apiHelper;
|
||||
|
||||
@end
|
||||
|
||||
@implementation EPMomentCell
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
|
||||
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
|
||||
self.selectionStyle = UITableViewCellSelectionStyleNone;
|
||||
self.backgroundColor = [UIColor clearColor];
|
||||
[self setupUI];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
// MARK: - Setup UI
|
||||
|
||||
- (void)setupUI {
|
||||
|
||||
[self.contentView addSubview:self.cardView];
|
||||
[self.cardView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.trailing.equalTo(self.contentView).inset(15);
|
||||
make.top.equalTo(self.contentView).offset(8);
|
||||
make.bottom.equalTo(self.contentView).offset(-8).priority(UILayoutPriorityRequired - 1);
|
||||
}];
|
||||
|
||||
|
||||
[self.cardView addSubview:self.colorBackgroundView];
|
||||
[self.colorBackgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(self.cardView);
|
||||
}];
|
||||
|
||||
|
||||
[self.cardView addSubview:self.blurEffectView];
|
||||
[self.blurEffectView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(self.cardView);
|
||||
}];
|
||||
|
||||
|
||||
[self.blurEffectView.contentView addSubview:self.avatarImageView];
|
||||
[self.avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.equalTo(self.cardView).offset(15);
|
||||
make.top.equalTo(self.cardView).offset(15);
|
||||
make.size.mas_equalTo(CGSizeMake(40, 40));
|
||||
}];
|
||||
|
||||
|
||||
[self.blurEffectView.contentView addSubview:self.nameLabel];
|
||||
[self.nameLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.equalTo(self.avatarImageView.mas_trailing).offset(10);
|
||||
make.top.equalTo(self.avatarImageView);
|
||||
make.trailing.equalTo(self.cardView).offset(-15);
|
||||
}];
|
||||
|
||||
|
||||
[self.blurEffectView.contentView addSubview:self.timeLabel];
|
||||
[self.timeLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.equalTo(self.nameLabel);
|
||||
make.bottom.equalTo(self.avatarImageView);
|
||||
make.trailing.equalTo(self.cardView).offset(-15);
|
||||
}];
|
||||
|
||||
|
||||
[self.blurEffectView.contentView addSubview:self.contentLabel];
|
||||
[self.contentLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.trailing.equalTo(self.cardView).inset(15);
|
||||
make.top.equalTo(self.avatarImageView.mas_bottom).offset(12);
|
||||
}];
|
||||
|
||||
|
||||
[self.blurEffectView.contentView addSubview:self.imagesContainer];
|
||||
[self.imagesContainer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.trailing.equalTo(self.cardView).inset(15);
|
||||
make.top.equalTo(self.contentLabel.mas_bottom).offset(12);
|
||||
make.height.mas_equalTo(0);
|
||||
}];
|
||||
|
||||
|
||||
[self.blurEffectView.contentView addSubview:self.actionBar];
|
||||
[self.actionBar mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.trailing.equalTo(self.cardView);
|
||||
make.top.equalTo(self.imagesContainer.mas_bottom).offset(12);
|
||||
make.height.mas_equalTo(50);
|
||||
|
||||
make.bottom.equalTo(self.cardView).offset(-8).priority(UILayoutPriorityRequired - 2);
|
||||
}];
|
||||
|
||||
|
||||
[self.actionBar addSubview:self.likeButton];
|
||||
[self.likeButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.equalTo(self.actionBar);
|
||||
make.centerY.equalTo(self.actionBar);
|
||||
make.width.mas_greaterThanOrEqualTo(80);
|
||||
}];
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
|
||||
- (void)configureWithModel:(MomentsInfoModel *)model {
|
||||
self.currentModel = model;
|
||||
|
||||
|
||||
self.nameLabel.text = model.nick ?: YMLocalizedString(@"user.anonymous");
|
||||
|
||||
|
||||
self.timeLabel.text = [self formatTimestampToDate:model.publishTime];
|
||||
|
||||
|
||||
self.contentLabel.text = model.content ?: @"";
|
||||
|
||||
|
||||
[self renderImages:model.dynamicResList];
|
||||
|
||||
|
||||
NSInteger likeCnt = MAX(0, model.likeCount.integerValue);
|
||||
self.likeButton.selected = model.isLike;
|
||||
[self.likeButton setTitle:[NSString stringWithFormat:@" %ld", (long)likeCnt] forState:UIControlStateNormal];
|
||||
[self.likeButton setTitle:[NSString stringWithFormat:@" %ld", (long)likeCnt] forState:UIControlStateSelected];
|
||||
|
||||
self.avatarImageView.imageUrl = model.avatar;
|
||||
|
||||
|
||||
[self applyEmotionColorEffect:model.emotionColor];
|
||||
|
||||
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
||||
|
||||
- (void)applyEmotionColorEffect:(NSString *)emotionColorHex {
|
||||
|
||||
if (!emotionColorHex) {
|
||||
NSLog(@"[EPMomentCell] 警告:emotionColorHex 为 nil");
|
||||
return;
|
||||
}
|
||||
|
||||
UIColor *color = [self colorFromHex:emotionColorHex];
|
||||
|
||||
|
||||
self.cardView.layer.borderWidth = 0;
|
||||
|
||||
|
||||
self.colorBackgroundView.backgroundColor = [color colorWithAlphaComponent:0.5];
|
||||
|
||||
|
||||
self.cardView.layer.shadowColor = color.CGColor;
|
||||
self.cardView.layer.shadowOffset = CGSizeMake(0, 2);
|
||||
self.cardView.layer.shadowOpacity = 0.5;
|
||||
self.cardView.layer.shadowRadius = 16.0;
|
||||
}
|
||||
|
||||
|
||||
- (UIColor *)colorFromHex:(NSString *)hexString {
|
||||
unsigned rgbValue = 0;
|
||||
NSScanner *scanner = [NSScanner scannerWithString:hexString];
|
||||
[scanner setScanLocation:1];
|
||||
[scanner scanHexInt:&rgbValue];
|
||||
return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16)/255.0
|
||||
green:((rgbValue & 0xFF00) >> 8)/255.0
|
||||
blue:(rgbValue & 0xFF)/255.0
|
||||
alpha:1.0];
|
||||
}
|
||||
|
||||
// MARK: - Images Grid
|
||||
|
||||
- (void)renderImages:(NSArray *)resList {
|
||||
|
||||
for (UIView *iv in self.imageViews) { [iv removeFromSuperview]; }
|
||||
[self.imageViews removeAllObjects];
|
||||
if (resList.count == 0) {
|
||||
[self.imagesContainer mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.trailing.equalTo(self.cardView).inset(15);
|
||||
make.top.equalTo(self.contentLabel.mas_bottom).offset(0);
|
||||
make.height.mas_equalTo(0);
|
||||
}];
|
||||
|
||||
|
||||
[self.contentView setNeedsLayout];
|
||||
[self.contentView layoutIfNeeded];
|
||||
return;
|
||||
}
|
||||
NSInteger columns = 3;
|
||||
CGFloat spacing = 6.0;
|
||||
CGFloat totalWidth = [UIScreen mainScreen].bounds.size.width - 30 - 30;
|
||||
CGFloat itemW = floor((totalWidth - spacing * (columns - 1)) / columns);
|
||||
|
||||
for (NSInteger i = 0; i < resList.count && i < 9; i++) {
|
||||
NetImageConfig *config = [[NetImageConfig alloc] init];
|
||||
config.placeHolder = [UIImageConstant defaultBannerPlaceholder];
|
||||
NetImageView *iv = [[NetImageView alloc] initWithConfig:config];
|
||||
iv.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0];
|
||||
iv.layer.cornerRadius = 6;
|
||||
iv.layer.masksToBounds = YES;
|
||||
iv.contentMode = UIViewContentModeScaleAspectFill;
|
||||
iv.userInteractionEnabled = YES;
|
||||
iv.tag = i;
|
||||
|
||||
|
||||
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onImageTapped:)];
|
||||
[iv addGestureRecognizer:tap];
|
||||
|
||||
[self.imagesContainer addSubview:iv];
|
||||
[self.imageViews addObject:iv];
|
||||
NSInteger row = i / columns;
|
||||
NSInteger col = i % columns;
|
||||
[iv mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.equalTo(self.imagesContainer).offset((itemW + spacing) * col);
|
||||
make.top.equalTo(self.imagesContainer).offset((itemW + spacing) * row);
|
||||
make.size.mas_equalTo(CGSizeMake(itemW, itemW));
|
||||
}];
|
||||
|
||||
NSString *url = nil;
|
||||
id item = resList[i];
|
||||
if ([item isKindOfClass:[NSDictionary class]]) {
|
||||
url = [item valueForKey:@"resUrl"] ?: [item valueForKey:@"url"];
|
||||
} else if ([item respondsToSelector:@selector(resUrl)]) {
|
||||
url = [item valueForKey:@"resUrl"];
|
||||
}
|
||||
iv.imageUrl = url;
|
||||
}
|
||||
|
||||
NSInteger rows = ((MIN(resList.count, 9) - 1) / columns) + 1;
|
||||
CGFloat height = rows * itemW + (rows - 1) * spacing;
|
||||
[self.imagesContainer mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.trailing.equalTo(self.cardView).inset(15);
|
||||
make.top.equalTo(self.contentLabel.mas_bottom).offset(12);
|
||||
make.height.mas_equalTo(height);
|
||||
}];
|
||||
|
||||
|
||||
[self.contentView setNeedsLayout];
|
||||
[self.contentView layoutIfNeeded];
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)formatTimestampToDate:(NSString *)timestampString {
|
||||
if (!timestampString || timestampString.length == 0) {
|
||||
return @"";
|
||||
}
|
||||
|
||||
|
||||
NSTimeInterval timestamp = [timestampString doubleValue] / 1000.0;
|
||||
|
||||
if (timestamp <= 0) {
|
||||
return @"";
|
||||
}
|
||||
|
||||
NSDate *date = [NSDate dateWithTimeIntervalSince1970:timestamp];
|
||||
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
|
||||
formatter.dateFormat = @"MM/dd";
|
||||
|
||||
return [formatter stringFromDate:date];
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)formatTimeInterval:(NSInteger)timestamp {
|
||||
if (timestamp <= 0) return YMLocalizedString(@"time.just_now");
|
||||
|
||||
NSTimeInterval interval = [[NSDate date] timeIntervalSince1970] - timestamp / 1000.0;
|
||||
|
||||
if (interval < 60) {
|
||||
return YMLocalizedString(@"time.just_now");
|
||||
} else if (interval < 3600) {
|
||||
return [NSString stringWithFormat:YMLocalizedString(@"time.minutes_ago"), interval / 60];
|
||||
} else if (interval < 86400) {
|
||||
return [NSString stringWithFormat:YMLocalizedString(@"time.hours_ago"), interval / 3600];
|
||||
} else if (interval < 604800) {
|
||||
return [NSString stringWithFormat:YMLocalizedString(@"time.days_ago"), interval / 86400];
|
||||
} else {
|
||||
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
|
||||
formatter.dateFormat = @"yyyy-MM-dd";
|
||||
return [formatter stringFromDate:[NSDate dateWithTimeIntervalSince1970:timestamp / 1000.0]];
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
- (void)onLikeButtonTapped {
|
||||
if (!self.currentModel) return;
|
||||
|
||||
|
||||
if (self.currentModel.isLike) {
|
||||
[self performLikeAction:NO];
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (self.currentModel.status == 0) {
|
||||
NSLog(@"[EPMomentCell] 动态审核中,无法点赞");
|
||||
// TODO: 可选择显示提示 Toast
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
[self performLikeAction:YES];
|
||||
}
|
||||
|
||||
- (void)performLikeAction:(BOOL)isLike {
|
||||
NSLog(@"[EPMomentCell] %@ 动态: %@", isLike ? @"点赞" : @"取消点赞", self.currentModel.dynamicId);
|
||||
|
||||
NSString *dynamicId = self.currentModel.dynamicId;
|
||||
NSString *likedUid = self.currentModel.uid;
|
||||
long worldId = self.currentModel.worldId;
|
||||
|
||||
|
||||
@kWeakify(self);
|
||||
[self.apiHelper likeMomentWithDynamicId:dynamicId
|
||||
isLike:isLike
|
||||
likedUid:likedUid
|
||||
worldId:worldId
|
||||
completion:^{
|
||||
@kStrongify(self);
|
||||
|
||||
self.currentModel.isLike = isLike;
|
||||
NSInteger likeCount = [self.currentModel.likeCount integerValue];
|
||||
likeCount += isLike ? 1 : -1;
|
||||
likeCount = MAX(0, likeCount);
|
||||
self.currentModel.likeCount = @(likeCount).stringValue;
|
||||
|
||||
|
||||
self.likeButton.selected = self.currentModel.isLike;
|
||||
[self.likeButton setTitle:[NSString stringWithFormat:@" %ld", (long)likeCount] forState:UIControlStateNormal];
|
||||
[self.likeButton setTitle:[NSString stringWithFormat:@" %ld", (long)likeCount] forState:UIControlStateSelected];
|
||||
|
||||
NSLog(@"[EPMomentCell] %@ 成功", isLike ? @"点赞" : @"取消点赞");
|
||||
} failure:^(NSInteger code, NSString * _Nonnull msg) {
|
||||
NSLog(@"[EPMomentCell] %@ 失败 (code: %ld): %@", isLike ? @"点赞" : @"取消点赞", (long)code, msg);
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
- (void)onImageTapped:(UITapGestureRecognizer *)gesture {
|
||||
if (!self.currentModel || !self.currentModel.dynamicResList.count) return;
|
||||
|
||||
NSInteger index = gesture.view.tag;
|
||||
NSLog(@"[EPMomentCell] 点击图片索引: %ld", (long)index);
|
||||
|
||||
SDPhotoBrowser *browser = [[SDPhotoBrowser alloc] init];
|
||||
browser.sourceImagesContainerView = self.imagesContainer;
|
||||
browser.delegate = self;
|
||||
browser.imageCount = self.currentModel.dynamicResList.count;
|
||||
browser.currentImageIndex = index;
|
||||
[browser show];
|
||||
}
|
||||
|
||||
#pragma mark - SDPhotoBrowserDelegate
|
||||
|
||||
- (NSURL *)photoBrowser:(SDPhotoBrowser *)browser highQualityImageURLForIndex:(NSInteger)index {
|
||||
if (index >= 0 && index < self.currentModel.dynamicResList.count) {
|
||||
id item = self.currentModel.dynamicResList[index];
|
||||
NSString *url = nil;
|
||||
if ([item isKindOfClass:[NSDictionary class]]) {
|
||||
url = [item valueForKey:@"resUrl"] ?: [item valueForKey:@"url"];
|
||||
} else if ([item respondsToSelector:@selector(resUrl)]) {
|
||||
url = [item valueForKey:@"resUrl"];
|
||||
}
|
||||
if (url) {
|
||||
return [NSURL URLWithString:url];
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (UIImage *)photoBrowser:(SDPhotoBrowser *)browser placeholderImageForIndex:(NSInteger)index {
|
||||
return [UIImageConstant defaultBannerPlaceholder];
|
||||
}
|
||||
|
||||
// MARK: - Lazy Loading
|
||||
|
||||
- (UIView *)cardView {
|
||||
if (!_cardView) {
|
||||
_cardView = [[UIView alloc] init];
|
||||
_cardView.backgroundColor = [UIColor clearColor];
|
||||
_cardView.layer.cornerRadius = 12;
|
||||
|
||||
_cardView.layer.masksToBounds = NO;
|
||||
}
|
||||
return _cardView;
|
||||
}
|
||||
|
||||
- (UIView *)colorBackgroundView {
|
||||
if (!_colorBackgroundView) {
|
||||
_colorBackgroundView = [[UIView alloc] init];
|
||||
_colorBackgroundView.layer.cornerRadius = 12;
|
||||
_colorBackgroundView.layer.masksToBounds = YES;
|
||||
}
|
||||
return _colorBackgroundView;
|
||||
}
|
||||
|
||||
- (UIVisualEffectView *)blurEffectView {
|
||||
if (!_blurEffectView) {
|
||||
UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
|
||||
_blurEffectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
|
||||
_blurEffectView.layer.cornerRadius = 12;
|
||||
_blurEffectView.layer.masksToBounds = YES;
|
||||
}
|
||||
return _blurEffectView;
|
||||
}
|
||||
|
||||
- (UIImageView *)avatarImageView {
|
||||
if (!_avatarImageView) {
|
||||
NetImageConfig *config = [[NetImageConfig alloc] init];
|
||||
_avatarImageView = [[NetImageView alloc] initWithConfig:config];
|
||||
_avatarImageView.backgroundColor = [UIColor colorWithWhite:0.9 alpha:1.0];
|
||||
_avatarImageView.layer.cornerRadius = 20;
|
||||
_avatarImageView.layer.masksToBounds = YES;
|
||||
_avatarImageView.contentMode = UIViewContentModeScaleAspectFill;
|
||||
}
|
||||
return _avatarImageView;
|
||||
}
|
||||
|
||||
- (UILabel *)nameLabel {
|
||||
if (!_nameLabel) {
|
||||
_nameLabel = [[UILabel alloc] init];
|
||||
_nameLabel.font = [UIFont systemFontOfSize:15 weight:UIFontWeightMedium];
|
||||
_nameLabel.textColor = [UIColor whiteColor];
|
||||
}
|
||||
return _nameLabel;
|
||||
}
|
||||
|
||||
- (UILabel *)timeLabel {
|
||||
if (!_timeLabel) {
|
||||
_timeLabel = [[UILabel alloc] init];
|
||||
_timeLabel.font = [UIFont systemFontOfSize:12];
|
||||
_timeLabel.textColor = [UIColor colorWithWhite:1 alpha:0.6];
|
||||
}
|
||||
return _timeLabel;
|
||||
}
|
||||
|
||||
- (UILabel *)contentLabel {
|
||||
if (!_contentLabel) {
|
||||
_contentLabel = [[UILabel alloc] init];
|
||||
_contentLabel.font = [UIFont systemFontOfSize:15];
|
||||
_contentLabel.textColor = [UIColor whiteColor];
|
||||
_contentLabel.numberOfLines = 0;
|
||||
_contentLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
}
|
||||
return _contentLabel;
|
||||
}
|
||||
|
||||
- (UIView *)actionBar {
|
||||
if (!_actionBar) {
|
||||
_actionBar = [[UIView alloc] init];
|
||||
_actionBar.backgroundColor = [UIColor clearColor];
|
||||
}
|
||||
return _actionBar;
|
||||
}
|
||||
|
||||
- (UIButton *)likeButton {
|
||||
if (!_likeButton) {
|
||||
_likeButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
[_likeButton setImage:[UIImage imageNamed:@"monents_info_like_count_normal"] forState:UIControlStateNormal];
|
||||
[_likeButton setImage:[UIImage imageNamed:@"monents_info_like_count_select"] forState:UIControlStateSelected];
|
||||
[_likeButton setTitle:@" 0" forState:UIControlStateNormal];
|
||||
_likeButton.titleLabel.font = [UIFont systemFontOfSize:13];
|
||||
[_likeButton setTitleColor:[UIColor colorWithWhite:1 alpha:0.6] forState:UIControlStateNormal];
|
||||
[_likeButton setTitleColor:[UIColor colorWithWhite:1 alpha:1.0] forState:UIControlStateSelected];
|
||||
[_likeButton addTarget:self action:@selector(onLikeButtonTapped) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
return _likeButton;
|
||||
}
|
||||
|
||||
|
||||
- (UIButton *)commentButton {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (UIButton *)createActionButtonWithTitle:(NSString *)title {
|
||||
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
[button setTitle:title forState:UIControlStateNormal];
|
||||
button.titleLabel.font = [UIFont systemFontOfSize:13];
|
||||
[button setTitleColor:[UIColor colorWithWhite:0.5 alpha:1.0] forState:UIControlStateNormal];
|
||||
return button;
|
||||
}
|
||||
|
||||
- (UIView *)imagesContainer {
|
||||
if (!_imagesContainer) {
|
||||
_imagesContainer = [[UIView alloc] init];
|
||||
_imagesContainer.backgroundColor = [UIColor clearColor];
|
||||
}
|
||||
return _imagesContainer;
|
||||
}
|
||||
|
||||
- (NSMutableArray<NetImageView *> *)imageViews {
|
||||
if (!_imageViews) {
|
||||
_imageViews = [NSMutableArray array];
|
||||
}
|
||||
return _imageViews;
|
||||
}
|
||||
|
||||
- (EPMomentAPISwiftHelper *)apiHelper {
|
||||
if (!_apiHelper) {
|
||||
_apiHelper = [[EPMomentAPISwiftHelper alloc] init];
|
||||
}
|
||||
return _apiHelper;
|
||||
}
|
||||
|
||||
@end
|
||||
46
YuMi/E-P/NewMoments/Views/EPMomentListView.h
Normal file
46
YuMi/E-P/NewMoments/Views/EPMomentListView.h
Normal file
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// EPMomentListView.h
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-10-10.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class EPMomentAPISwiftHelper;
|
||||
@class MomentsInfoModel;
|
||||
|
||||
/// 推荐/我的动态列表数据源类型
|
||||
typedef NS_ENUM(NSInteger, EPMomentListSourceType) {
|
||||
EPMomentListSourceTypeRecommend = 0,
|
||||
EPMomentListSourceTypeMine = 1
|
||||
};
|
||||
|
||||
/// 承载 Moments 列表与分页刷新的视图
|
||||
@interface EPMomentListView : UIView
|
||||
|
||||
/// 当前数据源(外部可读)
|
||||
@property (nonatomic, strong, readonly) NSArray *rawList;
|
||||
|
||||
/// 列表类型:推荐 / 我的
|
||||
@property (nonatomic, assign) EPMomentListSourceType sourceType;
|
||||
|
||||
/// 外部可设置:当某一项被点击
|
||||
@property (nonatomic, copy) void (^onSelectMoment)(NSInteger index);
|
||||
|
||||
/// 重新加载(刷新到第一页)
|
||||
- (void)reloadFirstPage;
|
||||
|
||||
/// 使用本地数组模式显示动态(禁用分页加载)
|
||||
/// @param dynamicInfo 本地动态数组
|
||||
/// @param refreshCallback 下拉刷新回调(由外部重新获取数据)
|
||||
- (void)loadWithDynamicInfo:(NSArray<MomentsInfoModel *> *)dynamicInfo
|
||||
refreshCallback:(void(^)(void))refreshCallback;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
|
||||
289
YuMi/E-P/NewMoments/Views/EPMomentListView.m
Normal file
289
YuMi/E-P/NewMoments/Views/EPMomentListView.m
Normal file
@@ -0,0 +1,289 @@
|
||||
|
||||
|
||||
// Created by AI on 2025-10-10.
|
||||
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "EPMomentListView.h"
|
||||
#import "EPMomentCell.h"
|
||||
#import <MJRefresh/MJRefresh.h>
|
||||
#import "YuMi-Swift.h"
|
||||
#import "EPEmotionColorStorage.h"
|
||||
|
||||
|
||||
@interface EPMomentListView () <UITableViewDelegate, UITableViewDataSource>
|
||||
|
||||
@property (nonatomic, strong) UITableView *tableView;
|
||||
@property (nonatomic, strong) UIRefreshControl *refreshControl;
|
||||
@property (nonatomic, strong) NSMutableArray *mutableRawList;
|
||||
@property (nonatomic, strong) EPMomentAPISwiftHelper *api;
|
||||
@property (nonatomic, assign) BOOL isLoading;
|
||||
@property (nonatomic, copy) NSString *nextID;
|
||||
@property (nonatomic, assign) BOOL isLocalMode;
|
||||
@property (nonatomic, copy) void (^refreshCallback)(void);
|
||||
@end
|
||||
|
||||
@implementation EPMomentListView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.backgroundColor = [UIColor clearColor];
|
||||
_api = [[EPMomentAPISwiftHelper alloc] init];
|
||||
_mutableRawList = [NSMutableArray array];
|
||||
_sourceType = EPMomentListSourceTypeRecommend;
|
||||
_isLocalMode = NO;
|
||||
|
||||
[self addSubview:self.tableView];
|
||||
[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(self);
|
||||
}];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSArray<NSMutableDictionary *> *)rawList {
|
||||
return [self.mutableRawList copy];
|
||||
}
|
||||
|
||||
- (void)reloadFirstPage {
|
||||
NSLog(@"[EPMomentListView] 📄 开始刷新第一页,isLocalMode=%d", self.isLocalMode);
|
||||
|
||||
if (self.isLocalMode) {
|
||||
|
||||
if (self.refreshCallback) {
|
||||
self.refreshCallback();
|
||||
}
|
||||
[self.refreshControl endRefreshing];
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
self.nextID = @"";
|
||||
[self.mutableRawList removeAllObjects];
|
||||
[self.tableView reloadData];
|
||||
[self.tableView.mj_footer resetNoMoreData];
|
||||
[self requestNextPage];
|
||||
}
|
||||
|
||||
- (void)loadWithDynamicInfo:(NSArray<MomentsInfoModel *> *)dynamicInfo
|
||||
refreshCallback:(void (^)(void))refreshCallback {
|
||||
self.isLocalMode = YES;
|
||||
self.refreshCallback = refreshCallback;
|
||||
|
||||
[self.mutableRawList removeAllObjects];
|
||||
if (dynamicInfo.count > 0) {
|
||||
[self.mutableRawList addObjectsFromArray:dynamicInfo];
|
||||
}
|
||||
|
||||
|
||||
self.tableView.mj_footer.hidden = YES;
|
||||
|
||||
[self.tableView reloadData];
|
||||
[self.refreshControl endRefreshing];
|
||||
}
|
||||
|
||||
- (void)requestNextPage {
|
||||
if (self.isLoading) {
|
||||
NSLog(@"[EPMomentListView] ⚠️ 已有加载任务进行中,跳过本次请求");
|
||||
return;
|
||||
}
|
||||
|
||||
NSLog(@"[EPMomentListView] 🌐 发起网络请求,nextID=%@", self.nextID.length > 0 ? self.nextID : @"(首页)");
|
||||
self.isLoading = YES;
|
||||
|
||||
@kWeakify(self);
|
||||
[self.api fetchLatestMomentsWithNextID:self.nextID
|
||||
completion:^(NSArray<MomentsInfoModel *> * _Nonnull list, NSString * _Nonnull nextMomentID) {
|
||||
@kStrongify(self);
|
||||
NSLog(@"[EPMomentListView] ✅ 请求成功,获得 %lu 条数据", (unsigned long)list.count);
|
||||
[self endLoading];
|
||||
if (list.count > 0) {
|
||||
|
||||
[self processEmotionColors:list isFirstPage:(self.nextID.length == 0)];
|
||||
|
||||
self.nextID = nextMomentID;
|
||||
[self.mutableRawList addObjectsFromArray:list];
|
||||
[self removeEmptyState];
|
||||
[self.tableView reloadData];
|
||||
if (nextMomentID.length > 0) {
|
||||
[self.tableView.mj_footer endRefreshing];
|
||||
} else {
|
||||
[self.tableView.mj_footer endRefreshingWithNoMoreData];
|
||||
}
|
||||
} else {
|
||||
NSLog(@"[EPMomentListView] ⚠️ 返回数据为空");
|
||||
if (self.mutableRawList.count == 0) {
|
||||
[self showEmptyStateWithMessage:YMLocalizedString(@"common.no_data")];
|
||||
}
|
||||
[self.tableView.mj_footer endRefreshingWithNoMoreData];
|
||||
}
|
||||
} failure:^(NSInteger code, NSString * _Nonnull msg) {
|
||||
@kStrongify(self);
|
||||
NSLog(@"[EPMomentListView] ❌ 请求失败,code=%ld, msg=%@", (long)code, msg);
|
||||
[self endLoading];
|
||||
|
||||
|
||||
if (self.mutableRawList.count == 0) {
|
||||
[self showEmptyStateWithMessage:msg ?: YMLocalizedString(@"error.request_failed")];
|
||||
}
|
||||
[self.tableView.mj_footer endRefreshing];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)endLoading {
|
||||
self.isLoading = NO;
|
||||
[self.refreshControl endRefreshing];
|
||||
}
|
||||
|
||||
|
||||
- (void)showEmptyStateWithMessage:(NSString *)message {
|
||||
UILabel *emptyLabel = [[UILabel alloc] initWithFrame:CGRectZero];
|
||||
emptyLabel.text = [NSString stringWithFormat:@"%@\n\n%@", message, YMLocalizedString(@"common.pull_to_retry")];
|
||||
emptyLabel.textColor = [UIColor whiteColor];
|
||||
emptyLabel.textAlignment = NSTextAlignmentCenter;
|
||||
emptyLabel.numberOfLines = 0;
|
||||
emptyLabel.font = [UIFont systemFontOfSize:15];
|
||||
emptyLabel.tag = 9999;
|
||||
|
||||
|
||||
[[self.tableView viewWithTag:9999] removeFromSuperview];
|
||||
[self.tableView addSubview:emptyLabel];
|
||||
[emptyLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.center.equalTo(self.tableView);
|
||||
make.leading.trailing.equalTo(self.tableView).inset(40);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)removeEmptyState {
|
||||
[[self.tableView viewWithTag:9999] removeFromSuperview];
|
||||
}
|
||||
|
||||
|
||||
- (void)processEmotionColors:(NSArray<MomentsInfoModel *> *)list isFirstPage:(BOOL)isFirstPage {
|
||||
|
||||
NSString *pendingColor = [[NSUserDefaults standardUserDefaults] stringForKey:@"EP_Pending_Emotion_Color"];
|
||||
NSNumber *pendingTimestamp = [[NSUserDefaults standardUserDefaults] objectForKey:@"EP_Pending_Emotion_Timestamp"];
|
||||
|
||||
for (NSInteger i = 0; i < list.count; i++) {
|
||||
MomentsInfoModel *model = list[i];
|
||||
|
||||
|
||||
if (isFirstPage && i == 0 && pendingColor && pendingTimestamp) {
|
||||
|
||||
NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
|
||||
NSTimeInterval pending = pendingTimestamp.doubleValue;
|
||||
if ((now - pending) < 5.0) {
|
||||
model.emotionColor = pendingColor;
|
||||
|
||||
[EPEmotionColorStorage saveColor:pendingColor forDynamicId:model.dynamicId];
|
||||
|
||||
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"EP_Pending_Emotion_Color"];
|
||||
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"EP_Pending_Emotion_Timestamp"];
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
NSString *savedColor = [EPEmotionColorStorage colorForDynamicId:model.dynamicId];
|
||||
if (savedColor) {
|
||||
model.emotionColor = savedColor;
|
||||
} else {
|
||||
|
||||
NSString *randomColor = [EPEmotionColorStorage randomEmotionColor];
|
||||
model.emotionColor = randomColor;
|
||||
[EPEmotionColorStorage saveColor:randomColor forDynamicId:model.dynamicId];
|
||||
NSLog(@"[EPMomentListView] 为动态 %@ 分配随机颜色: %@", model.dynamicId, randomColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - UITableView
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
return self.mutableRawList.count;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
EPMomentCell *cell = [tableView dequeueReusableCellWithIdentifier:@"NewMomentCell" forIndexPath:indexPath];
|
||||
if (indexPath.row < self.mutableRawList.count) {
|
||||
MomentsInfoModel *model = [self.mutableRawList xpSafeObjectAtIndex:indexPath.row];
|
||||
[cell configureWithModel:model];
|
||||
}
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return UITableViewAutomaticDimension;
|
||||
}
|
||||
|
||||
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return 200;
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
[tableView deselectRowAtIndexPath:indexPath animated:YES];
|
||||
if (self.onSelectMoment) self.onSelectMoment(indexPath.row);
|
||||
}
|
||||
|
||||
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
|
||||
|
||||
if (self.isLocalMode) return;
|
||||
|
||||
CGFloat offsetY = scrollView.contentOffset.y;
|
||||
CGFloat contentHeight = scrollView.contentSize.height;
|
||||
CGFloat screenHeight = scrollView.frame.size.height;
|
||||
if (offsetY > contentHeight - screenHeight - 100 && !self.isLoading) {
|
||||
[self requestNextPage];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Lazy
|
||||
|
||||
- (UITableView *)tableView {
|
||||
if (!_tableView) {
|
||||
_tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
|
||||
_tableView.delegate = self;
|
||||
_tableView.dataSource = self;
|
||||
_tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
|
||||
_tableView.backgroundColor = [UIColor clearColor];
|
||||
_tableView.estimatedRowHeight = 200;
|
||||
_tableView.rowHeight = UITableViewAutomaticDimension;
|
||||
_tableView.showsVerticalScrollIndicator = NO;
|
||||
|
||||
_tableView.contentInset = UIEdgeInsetsMake(10, 0, 120, 0);
|
||||
_tableView.scrollIndicatorInsets = UIEdgeInsetsMake(10, 0, 120, 0);
|
||||
[_tableView registerClass:[EPMomentCell class] forCellReuseIdentifier:@"NewMomentCell"];
|
||||
_tableView.refreshControl = self.refreshControl;
|
||||
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
MJRefreshAutoNormalFooter *footer = [MJRefreshAutoNormalFooter footerWithRefreshingBlock:^{
|
||||
__strong typeof(weakSelf) self = weakSelf;
|
||||
if (!self.isLoading && self.nextID.length > 0) {
|
||||
[self requestNextPage];
|
||||
} else if (self.nextID.length == 0) {
|
||||
[self.tableView.mj_footer endRefreshingWithNoMoreData];
|
||||
} else {
|
||||
[self.tableView.mj_footer endRefreshing];
|
||||
}
|
||||
}];
|
||||
|
||||
footer.stateLabel.textColor = [UIColor whiteColor];
|
||||
footer.loadingView.color = [UIColor whiteColor];
|
||||
_tableView.mj_footer = footer;
|
||||
}
|
||||
return _tableView;
|
||||
}
|
||||
|
||||
- (UIRefreshControl *)refreshControl {
|
||||
if (!_refreshControl) {
|
||||
_refreshControl = [[UIRefreshControl alloc] init];
|
||||
_refreshControl.tintColor = [UIColor whiteColor];
|
||||
[_refreshControl addTarget:self action:@selector(reloadFirstPage) forControlEvents:UIControlEventValueChanged];
|
||||
}
|
||||
return _refreshControl;
|
||||
}
|
||||
|
||||
@end
|
||||
36
YuMi/E-P/NewMoments/Views/EPSignatureColorGuideView.h
Normal file
36
YuMi/E-P/NewMoments/Views/EPSignatureColorGuideView.h
Normal file
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// EPSignatureColorGuideView.h
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-10-15.
|
||||
// 用户专属情绪颜色首次引导页
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface EPSignatureColorGuideView : UIView
|
||||
|
||||
/// 颜色确认回调
|
||||
@property (nonatomic, copy) void(^onColorConfirmed)(NSString *hexColor);
|
||||
|
||||
/// Skip 按钮点击回调(仅 debug 模式且已有颜色时显示)
|
||||
@property (nonatomic, copy) void(^onSkipTapped)(void);
|
||||
|
||||
/// 在 window 中显示引导页(全屏模态)
|
||||
/// @param window 应用主 window
|
||||
- (void)showInWindow:(UIWindow *)window;
|
||||
|
||||
/// 在 window 中显示引导页(带 Skip 按钮)
|
||||
/// @param window 应用主 window
|
||||
/// @param showSkip 是否显示 Skip 按钮(用于 debug 模式)
|
||||
- (void)showInWindow:(UIWindow *)window showSkipButton:(BOOL)showSkip;
|
||||
|
||||
/// 关闭引导页
|
||||
- (void)dismiss;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
372
YuMi/E-P/NewMoments/Views/EPSignatureColorGuideView.m
Normal file
372
YuMi/E-P/NewMoments/Views/EPSignatureColorGuideView.m
Normal file
@@ -0,0 +1,372 @@
|
||||
|
||||
|
||||
// Created by AI on 2025-10-15.
|
||||
|
||||
|
||||
#import "EPSignatureColorGuideView.h"
|
||||
#import "EPEmotionColorWheelView.h"
|
||||
#import "EPEmotionInfoView.h"
|
||||
#import <Masonry/Masonry.h>
|
||||
|
||||
@interface EPSignatureColorGuideView ()
|
||||
|
||||
@property (nonatomic, strong) CAGradientLayer *gradientLayer;
|
||||
@property (nonatomic, strong) UIView *contentContainer;
|
||||
@property (nonatomic, strong) UILabel *titleLabel;
|
||||
@property (nonatomic, strong) UILabel *subtitleLabel;
|
||||
@property (nonatomic, strong) UIButton *infoButton;
|
||||
@property (nonatomic, strong) UIView *selectedColorView;
|
||||
@property (nonatomic, strong) UILabel *selectedColorLabel;
|
||||
@property (nonatomic, strong) EPEmotionColorWheelView *colorWheelView;
|
||||
@property (nonatomic, strong) UIButton *confirmButton;
|
||||
@property (nonatomic, strong) UIButton *skipButton;
|
||||
@property (nonatomic, copy) NSString *selectedColor;
|
||||
|
||||
@end
|
||||
|
||||
@implementation EPSignatureColorGuideView
|
||||
|
||||
#pragma mark - Lifecycle
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
[self setupUI];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setupUI {
|
||||
|
||||
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
|
||||
gradientLayer.colors = @[
|
||||
(id)[UIColor colorWithRed:0x1a/255.0 green:0x09/255.0 blue:0x33/255.0 alpha:1.0].CGColor,
|
||||
(id)[UIColor colorWithRed:0x0d/255.0 green:0x1b/255.0 blue:0x2a/255.0 alpha:1.0].CGColor
|
||||
];
|
||||
gradientLayer.startPoint = CGPointMake(0.5, 0);
|
||||
gradientLayer.endPoint = CGPointMake(0.5, 1);
|
||||
[self.layer insertSublayer:gradientLayer atIndex:0];
|
||||
self.gradientLayer = gradientLayer;
|
||||
|
||||
|
||||
[self addSubview:self.contentContainer];
|
||||
[self.contentContainer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self).offset(100);
|
||||
make.leading.trailing.equalTo(self).inset(30);
|
||||
make.bottom.lessThanOrEqualTo(self).offset(-30);
|
||||
}];
|
||||
|
||||
|
||||
[self.contentContainer addSubview:self.titleLabel];
|
||||
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.contentContainer);
|
||||
make.centerX.equalTo(self.contentContainer);
|
||||
}];
|
||||
|
||||
|
||||
[self.contentContainer addSubview:self.subtitleLabel];
|
||||
[self.subtitleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.titleLabel.mas_bottom).offset(8);
|
||||
make.centerX.equalTo(self.contentContainer);
|
||||
make.leading.trailing.equalTo(self.contentContainer).inset(20);
|
||||
}];
|
||||
|
||||
|
||||
[self addSubview:self.infoButton];
|
||||
[self.infoButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.equalTo(self).offset(20);
|
||||
make.top.equalTo(self).offset(60);
|
||||
make.size.mas_equalTo(CGSizeMake(36, 36));
|
||||
}];
|
||||
|
||||
|
||||
[self.contentContainer addSubview:self.selectedColorView];
|
||||
[self.selectedColorView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.subtitleLabel.mas_bottom).offset(20);
|
||||
make.centerX.equalTo(self.contentContainer);
|
||||
make.height.mas_equalTo(60);
|
||||
make.leading.trailing.equalTo(self.contentContainer).inset(40);
|
||||
}];
|
||||
|
||||
|
||||
[self.contentContainer addSubview:self.colorWheelView];
|
||||
[self.colorWheelView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.selectedColorView.mas_bottom).offset(20);
|
||||
make.centerX.equalTo(self.contentContainer);
|
||||
CGFloat wheelSize = MIN(300, [UIScreen mainScreen].bounds.size.width - 80);
|
||||
make.size.mas_equalTo(CGSizeMake(wheelSize, wheelSize));
|
||||
}];
|
||||
|
||||
|
||||
[self.contentContainer addSubview:self.confirmButton];
|
||||
[self.confirmButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.colorWheelView.mas_bottom).offset(30);
|
||||
make.leading.trailing.equalTo(self.contentContainer).inset(20);
|
||||
make.height.mas_equalTo(56);
|
||||
make.bottom.equalTo(self.contentContainer);
|
||||
}];
|
||||
|
||||
|
||||
[self addSubview:self.skipButton];
|
||||
[self.skipButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self).offset(60);
|
||||
make.trailing.equalTo(self).offset(-20);
|
||||
make.size.mas_equalTo(CGSizeMake(60, 36));
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
self.gradientLayer.frame = self.bounds;
|
||||
}
|
||||
|
||||
#pragma mark - Actions
|
||||
|
||||
- (void)onConfirmButtonTapped {
|
||||
if (!self.selectedColor) return;
|
||||
|
||||
|
||||
if (self.onColorConfirmed) {
|
||||
self.onColorConfirmed(self.selectedColor);
|
||||
}
|
||||
|
||||
|
||||
[self dismiss];
|
||||
}
|
||||
|
||||
- (void)onSkipButtonTapped {
|
||||
|
||||
if (self.onSkipTapped) {
|
||||
self.onSkipTapped();
|
||||
}
|
||||
|
||||
|
||||
[self dismiss];
|
||||
}
|
||||
|
||||
- (void)onInfoButtonTapped {
|
||||
EPEmotionInfoView *infoView = [[EPEmotionInfoView alloc] init];
|
||||
[infoView showInView:self];
|
||||
}
|
||||
|
||||
#pragma mark - Public Methods
|
||||
|
||||
- (void)showInWindow:(UIWindow *)window {
|
||||
[self showInWindow:window showSkipButton:NO];
|
||||
}
|
||||
|
||||
- (void)showInWindow:(UIWindow *)window showSkipButton:(BOOL)showSkip {
|
||||
[window addSubview:self];
|
||||
[self mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(window);
|
||||
}];
|
||||
|
||||
|
||||
self.skipButton.hidden = !showSkip;
|
||||
|
||||
|
||||
self.alpha = 0;
|
||||
self.contentContainer.transform = CGAffineTransformMakeScale(0.8, 0.8);
|
||||
|
||||
|
||||
[UIView animateWithDuration:0.4 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
|
||||
self.alpha = 1.0;
|
||||
self.contentContainer.transform = CGAffineTransformIdentity;
|
||||
} completion:nil];
|
||||
}
|
||||
|
||||
- (void)dismiss {
|
||||
[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
|
||||
self.alpha = 0;
|
||||
self.contentContainer.transform = CGAffineTransformMakeScale(0.95, 0.95);
|
||||
} completion:^(BOOL finished) {
|
||||
[self removeFromSuperview];
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Lazy Loading
|
||||
|
||||
- (UIView *)contentContainer {
|
||||
if (!_contentContainer) {
|
||||
_contentContainer = [[UIView alloc] init];
|
||||
_contentContainer.backgroundColor = [UIColor clearColor];
|
||||
}
|
||||
return _contentContainer;
|
||||
}
|
||||
|
||||
- (UILabel *)titleLabel {
|
||||
if (!_titleLabel) {
|
||||
_titleLabel = [[UILabel alloc] init];
|
||||
_titleLabel.text = @"Choose your signature emotion";
|
||||
_titleLabel.textColor = [UIColor whiteColor];
|
||||
_titleLabel.font = [UIFont systemFontOfSize:24 weight:UIFontWeightBold];
|
||||
_titleLabel.textAlignment = NSTextAlignmentCenter;
|
||||
}
|
||||
return _titleLabel;
|
||||
}
|
||||
|
||||
- (UILabel *)subtitleLabel {
|
||||
if (!_subtitleLabel) {
|
||||
_subtitleLabel = [[UILabel alloc] init];
|
||||
_subtitleLabel.text = @"This color represents your emotional identity";
|
||||
_subtitleLabel.textColor = [[UIColor whiteColor] colorWithAlphaComponent:0.7];
|
||||
_subtitleLabel.font = [UIFont systemFontOfSize:14];
|
||||
_subtitleLabel.textAlignment = NSTextAlignmentCenter;
|
||||
_subtitleLabel.numberOfLines = 0;
|
||||
}
|
||||
return _subtitleLabel;
|
||||
}
|
||||
|
||||
- (UIView *)selectedColorView {
|
||||
if (!_selectedColorView) {
|
||||
_selectedColorView = [[UIView alloc] init];
|
||||
_selectedColorView.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.15];
|
||||
_selectedColorView.layer.cornerRadius = 30;
|
||||
_selectedColorView.layer.masksToBounds = YES;
|
||||
_selectedColorView.hidden = YES;
|
||||
|
||||
|
||||
UIView *colorDot = [[UIView alloc] init];
|
||||
colorDot.tag = 100;
|
||||
colorDot.layer.cornerRadius = 16;
|
||||
colorDot.layer.masksToBounds = YES;
|
||||
[_selectedColorView addSubview:colorDot];
|
||||
[colorDot mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.equalTo(_selectedColorView).offset(20);
|
||||
make.centerY.equalTo(_selectedColorView);
|
||||
make.size.mas_equalTo(CGSizeMake(32, 32));
|
||||
}];
|
||||
|
||||
|
||||
[_selectedColorView addSubview:self.selectedColorLabel];
|
||||
[self.selectedColorLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.equalTo(colorDot.mas_trailing).offset(16);
|
||||
make.centerY.equalTo(_selectedColorView);
|
||||
make.trailing.equalTo(_selectedColorView).offset(-20);
|
||||
}];
|
||||
}
|
||||
return _selectedColorView;
|
||||
}
|
||||
|
||||
- (UILabel *)selectedColorLabel {
|
||||
if (!_selectedColorLabel) {
|
||||
_selectedColorLabel = [[UILabel alloc] init];
|
||||
_selectedColorLabel.textColor = [UIColor whiteColor];
|
||||
_selectedColorLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightMedium];
|
||||
_selectedColorLabel.text = @"Select your signature emotion";
|
||||
}
|
||||
return _selectedColorLabel;
|
||||
}
|
||||
|
||||
- (EPEmotionColorWheelView *)colorWheelView {
|
||||
if (!_colorWheelView) {
|
||||
_colorWheelView = [[EPEmotionColorWheelView alloc] init];
|
||||
CGFloat wheelSize = MIN(300, [UIScreen mainScreen].bounds.size.width - 80);
|
||||
_colorWheelView.radius = wheelSize / 3.0;
|
||||
_colorWheelView.buttonSize = 48.0;
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
_colorWheelView.onColorTapped = ^(NSString *hexColor, NSInteger index) {
|
||||
__strong typeof(weakSelf) self = weakSelf;
|
||||
|
||||
|
||||
self.selectedColor = hexColor;
|
||||
|
||||
|
||||
[self updateSelectedColorDisplay:hexColor index:index];
|
||||
|
||||
|
||||
self.confirmButton.enabled = YES;
|
||||
self.confirmButton.alpha = 1.0;
|
||||
};
|
||||
}
|
||||
return _colorWheelView;
|
||||
}
|
||||
|
||||
|
||||
- (void)updateSelectedColorDisplay:(NSString *)hexColor index:(NSInteger)index {
|
||||
NSArray<NSString *> *emotions = @[@"Joy", @"Sadness", @"Anger", @"Fear", @"Surprise", @"Disgust", @"Trust", @"Anticipation"];
|
||||
|
||||
|
||||
self.selectedColorView.hidden = NO;
|
||||
|
||||
|
||||
UIView *colorDot = [self.selectedColorView viewWithTag:100];
|
||||
colorDot.backgroundColor = [self colorFromHex:hexColor];
|
||||
|
||||
|
||||
self.selectedColorLabel.text = emotions[index];
|
||||
}
|
||||
|
||||
|
||||
- (UIColor *)colorFromHex:(NSString *)hexString {
|
||||
unsigned rgbValue = 0;
|
||||
NSScanner *scanner = [NSScanner scannerWithString:hexString];
|
||||
[scanner setScanLocation:1];
|
||||
[scanner scanHexInt:&rgbValue];
|
||||
return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16)/255.0
|
||||
green:((rgbValue & 0xFF00) >> 8)/255.0
|
||||
blue:(rgbValue & 0xFF)/255.0
|
||||
alpha:1.0];
|
||||
}
|
||||
|
||||
- (UIButton *)confirmButton {
|
||||
if (!_confirmButton) {
|
||||
_confirmButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
[_confirmButton setTitle:@"Confirm & Continue" forState:UIControlStateNormal];
|
||||
[_confirmButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
||||
_confirmButton.titleLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightSemibold];
|
||||
_confirmButton.layer.cornerRadius = 28;
|
||||
_confirmButton.layer.masksToBounds = YES;
|
||||
|
||||
|
||||
CAGradientLayer *gradient = [CAGradientLayer layer];
|
||||
gradient.colors = @[
|
||||
(id)[UIColor colorWithRed:0x9B/255.0 green:0x59/255.0 blue:0xB6/255.0 alpha:1.0].CGColor,
|
||||
(id)[UIColor colorWithRed:0x6C/255.0 green:0x34/255.0 blue:0x83/255.0 alpha:1.0].CGColor
|
||||
];
|
||||
gradient.startPoint = CGPointMake(0, 0);
|
||||
gradient.endPoint = CGPointMake(1, 0);
|
||||
gradient.frame = CGRectMake(0, 0, 1000, 56);
|
||||
[_confirmButton.layer insertSublayer:gradient atIndex:0];
|
||||
|
||||
[_confirmButton addTarget:self action:@selector(onConfirmButtonTapped) forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
|
||||
_confirmButton.enabled = NO;
|
||||
_confirmButton.alpha = 0.5;
|
||||
}
|
||||
return _confirmButton;
|
||||
}
|
||||
|
||||
- (UIButton *)infoButton {
|
||||
if (!_infoButton) {
|
||||
_infoButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
|
||||
|
||||
UIImage *infoIcon = [UIImage systemImageNamed:@"info.circle"];
|
||||
[_infoButton setImage:infoIcon forState:UIControlStateNormal];
|
||||
_infoButton.tintColor = [[UIColor whiteColor] colorWithAlphaComponent:0.8];
|
||||
|
||||
|
||||
[_infoButton addTarget:self action:@selector(onInfoButtonTapped) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
return _infoButton;
|
||||
}
|
||||
|
||||
- (UIButton *)skipButton {
|
||||
if (!_skipButton) {
|
||||
_skipButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
[_skipButton setTitle:@"Skip" forState:UIControlStateNormal];
|
||||
[_skipButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
||||
_skipButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
|
||||
_skipButton.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.2];
|
||||
_skipButton.layer.cornerRadius = 18;
|
||||
_skipButton.layer.masksToBounds = YES;
|
||||
[_skipButton addTarget:self action:@selector(onSkipButtonTapped) forControlEvents:UIControlEventTouchUpInside];
|
||||
_skipButton.hidden = YES;
|
||||
}
|
||||
return _skipButton;
|
||||
}
|
||||
|
||||
@end
|
||||
543
YuMi/E-P/NewTabBar/EPTabBarController.swift
Normal file
543
YuMi/E-P/NewTabBar/EPTabBarController.swift
Normal file
@@ -0,0 +1,543 @@
|
||||
|
||||
|
||||
// Created by AI on 2025-10-09.
|
||||
// Copyright © 2025 YuMi. All rights reserved.
|
||||
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
|
||||
@objc class EPTabBarController: UITabBarController {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
|
||||
private var isLoggedIn: Bool = false
|
||||
|
||||
|
||||
private var customTabBarView: UIView!
|
||||
|
||||
|
||||
private var tabBarBackgroundView: UIVisualEffectView!
|
||||
|
||||
|
||||
private var tabButtons: [UIButton] = []
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
|
||||
#if DEBUG
|
||||
APIConfig.testEncryption()
|
||||
#endif
|
||||
|
||||
|
||||
self.tabBar.isHidden = true
|
||||
|
||||
|
||||
self.delegate = self
|
||||
|
||||
|
||||
performAutoLogin()
|
||||
|
||||
setupCustomFloatingTabBar()
|
||||
setupInitialViewControllers()
|
||||
|
||||
NSLog("[EPTabBarController] 悬浮 TabBar 初始化完成")
|
||||
}
|
||||
|
||||
deinit {
|
||||
NSLog("[EPTabBarController] 已释放")
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
|
||||
private func setupCustomFloatingTabBar() {
|
||||
|
||||
customTabBarView = UIView()
|
||||
customTabBarView.translatesAutoresizingMaskIntoConstraints = false
|
||||
customTabBarView.backgroundColor = .clear
|
||||
view.addSubview(customTabBarView)
|
||||
|
||||
|
||||
let effect: UIVisualEffect
|
||||
if #available(iOS 26.0, *) {
|
||||
|
||||
effect = UIGlassEffect()
|
||||
} else {
|
||||
|
||||
effect = UIBlurEffect(style: .systemMaterial)
|
||||
}
|
||||
|
||||
tabBarBackgroundView = UIVisualEffectView(effect: effect)
|
||||
tabBarBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||
tabBarBackgroundView.layer.cornerRadius = 28
|
||||
tabBarBackgroundView.layer.masksToBounds = true
|
||||
|
||||
|
||||
tabBarBackgroundView.layer.borderWidth = 0.5
|
||||
tabBarBackgroundView.layer.borderColor = UIColor.white.withAlphaComponent(0.2).cgColor
|
||||
|
||||
customTabBarView.addSubview(tabBarBackgroundView)
|
||||
|
||||
|
||||
customTabBarView.snp.makeConstraints { make in
|
||||
make.leading.equalTo(view).offset(16)
|
||||
make.trailing.equalTo(view).offset(-16)
|
||||
make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-12)
|
||||
make.height.equalTo(64)
|
||||
}
|
||||
|
||||
tabBarBackgroundView.snp.makeConstraints { make in
|
||||
make.edges.equalTo(customTabBarView)
|
||||
}
|
||||
|
||||
|
||||
setupTabButtons()
|
||||
|
||||
NSLog("[EPTabBarController] 悬浮 TabBar 设置完成")
|
||||
}
|
||||
|
||||
|
||||
private func setupTabButtons() {
|
||||
let momentButton = createTabButton(
|
||||
normalImage: "tab_moment_off",
|
||||
selectedImage: "tab_moment_on",
|
||||
tag: 0
|
||||
)
|
||||
|
||||
let messageButton = createTabButton(
|
||||
normalImage: "tab_message_off",
|
||||
selectedImage: "tab_message_on",
|
||||
tag: 1
|
||||
)
|
||||
|
||||
let mineButton = createTabButton(
|
||||
normalImage: "tab_mine_off",
|
||||
selectedImage: "tab_mine_on",
|
||||
tag: 2
|
||||
)
|
||||
|
||||
tabButtons = [momentButton, messageButton, mineButton]
|
||||
|
||||
let stackView = UIStackView(arrangedSubviews: tabButtons)
|
||||
stackView.axis = .horizontal
|
||||
stackView.distribution = .fillEqually
|
||||
stackView.spacing = 20
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
tabBarBackgroundView.contentView.addSubview(stackView)
|
||||
|
||||
stackView.snp.makeConstraints { make in
|
||||
make.top.equalTo(tabBarBackgroundView).offset(8)
|
||||
make.leading.equalTo(tabBarBackgroundView).offset(20)
|
||||
make.trailing.equalTo(tabBarBackgroundView).offset(-20)
|
||||
make.bottom.equalTo(tabBarBackgroundView).offset(-8)
|
||||
}
|
||||
|
||||
|
||||
updateTabButtonStates(selectedIndex: 0)
|
||||
}
|
||||
|
||||
|
||||
private func createTabButton(normalImage: String, selectedImage: String, tag: Int) -> UIButton {
|
||||
let button = UIButton(type: .custom)
|
||||
button.tag = tag
|
||||
button.adjustsImageWhenHighlighted = false
|
||||
|
||||
|
||||
if let normalImg = UIImage(named: normalImage), let selectedImg = UIImage(named: selectedImage) {
|
||||
|
||||
button.setImage(normalImg, for: .normal)
|
||||
button.setImage(selectedImg, for: .selected)
|
||||
} else {
|
||||
|
||||
let fallbackIcons = ["sparkles", "person.circle"]
|
||||
let iconName = fallbackIcons[tag]
|
||||
let imageConfig = UIImage.SymbolConfiguration(pointSize: 24, weight: .medium)
|
||||
let normalIcon = UIImage(systemName: iconName, withConfiguration: imageConfig)
|
||||
|
||||
button.setImage(normalIcon, for: .normal)
|
||||
button.setImage(normalIcon, for: .selected)
|
||||
button.tintColor = .white.withAlphaComponent(0.6)
|
||||
}
|
||||
|
||||
|
||||
button.imageView?.contentMode = .scaleAspectFit
|
||||
|
||||
|
||||
button.setTitle(nil, for: .normal)
|
||||
button.setTitle(nil, for: .selected)
|
||||
|
||||
|
||||
button.imageView?.snp.makeConstraints { make in
|
||||
make.size.equalTo(28)
|
||||
}
|
||||
|
||||
button.addTarget(self, action: #selector(tabButtonTapped(_:)), for: .touchUpInside)
|
||||
return button
|
||||
}
|
||||
|
||||
|
||||
@objc private func tabButtonTapped(_ sender: UIButton) {
|
||||
let newIndex = sender.tag
|
||||
|
||||
|
||||
if newIndex == selectedIndex {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
updateTabButtonStates(selectedIndex: newIndex)
|
||||
|
||||
|
||||
UIView.performWithoutAnimation {
|
||||
selectedIndex = newIndex
|
||||
}
|
||||
|
||||
let tabNames = [YMLocalizedString("tab.moment"),
|
||||
YMLocalizedString("tab.message"),
|
||||
YMLocalizedString("tab.mine")]
|
||||
NSLog("[EPTabBarController] 选中 Tab: \(tabNames[newIndex])")
|
||||
}
|
||||
|
||||
|
||||
private func updateTabButtonStates(selectedIndex: Int) {
|
||||
|
||||
tabButtons.forEach { $0.isUserInteractionEnabled = false }
|
||||
|
||||
for (index, button) in tabButtons.enumerated() {
|
||||
let isSelected = (index == selectedIndex)
|
||||
|
||||
|
||||
button.isSelected = isSelected
|
||||
|
||||
|
||||
if button.currentImage?.isSymbolImage == true {
|
||||
button.tintColor = isSelected ? .white : .white.withAlphaComponent(0.6)
|
||||
}
|
||||
|
||||
|
||||
UIView.animate(withDuration: 0.2, delay: 0, options: [.curveEaseOut], animations: {
|
||||
button.transform = isSelected ? CGAffineTransform(scaleX: 1.1, y: 1.1) : .identity
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
|
||||
self.tabButtons.forEach { $0.isUserInteractionEnabled = true }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func setupInitialViewControllers() {
|
||||
// 三栏占位(Moment | Message | Mine)
|
||||
let momentVC = UIViewController()
|
||||
momentVC.view.backgroundColor = .systemBlue
|
||||
momentVC.title = "Moment"
|
||||
let momentNav = UINavigationController(rootViewController: momentVC)
|
||||
momentNav.tabBarItem = createTabBarItem(title: YMLocalizedString("tab.moment"), normalImage: "tab_moment_normal", selectedImage: "tab_moment_selected")
|
||||
|
||||
let messageVC = EPMessageMainViewController()
|
||||
messageVC.title = "Message"
|
||||
let messageNav = UINavigationController(rootViewController: messageVC)
|
||||
messageNav.tabBarItem = createTabBarItem(title: YMLocalizedString("tab.message"), normalImage: "tab_message_normal", selectedImage: "tab_message_selected")
|
||||
|
||||
// 角标绑定
|
||||
messageVC.unreadCountDidChange = { [weak self] c in
|
||||
let value: String? = c > 0 ? (c > 99 ? "99+" : "\(c)") : nil
|
||||
self?.viewControllers?[1].tabBarItem.badgeValue = value
|
||||
}
|
||||
|
||||
let mineVC = UIViewController()
|
||||
mineVC.view.backgroundColor = .systemGreen
|
||||
mineVC.title = "Mine"
|
||||
let mineNav = UINavigationController(rootViewController: mineVC)
|
||||
mineNav.tabBarItem = createTabBarItem(title: YMLocalizedString("tab.mine"), normalImage: "tab_mine_normal", selectedImage: "tab_mine_selected")
|
||||
|
||||
viewControllers = [momentNav, messageNav, mineNav]
|
||||
selectedIndex = 0
|
||||
|
||||
NSLog("[EPTabBarController] 初始 ViewControllers 设置完成")
|
||||
}
|
||||
|
||||
|
||||
private func createTabBarItem(title: String, normalImage: String, selectedImage: String) -> UITabBarItem {
|
||||
let item = UITabBarItem(
|
||||
title: title,
|
||||
image: UIImage(named: normalImage)?.withRenderingMode(.alwaysOriginal),
|
||||
selectedImage: UIImage(named: selectedImage)?.withRenderingMode(.alwaysOriginal)
|
||||
)
|
||||
return item
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
|
||||
|
||||
func refreshTabBar(isLogin: Bool) {
|
||||
isLoggedIn = isLogin
|
||||
|
||||
if isLogin {
|
||||
setupLoggedInViewControllers()
|
||||
} else {
|
||||
setupInitialViewControllers()
|
||||
}
|
||||
|
||||
NSLog("[EPTabBarController] TabBar 已刷新,登录状态: \(isLogin)")
|
||||
}
|
||||
|
||||
|
||||
private func setupLoggedInViewControllers() {
|
||||
|
||||
// 创建动态页
|
||||
let momentVC = EPMomentViewController()
|
||||
momentVC.title = YMLocalizedString("tab.moment")
|
||||
let momentNav = createTransparentNavigationController(
|
||||
rootViewController: momentVC,
|
||||
tabTitle: YMLocalizedString("tab.moment"),
|
||||
normalImage: "tab_moment_normal",
|
||||
selectedImage: "tab_moment_selected"
|
||||
)
|
||||
|
||||
// 创建消息页(Swift UIKit 容器)
|
||||
let messageVC = EPMessageMainViewController()
|
||||
let messageNav = createTransparentNavigationController(
|
||||
rootViewController: messageVC,
|
||||
tabTitle: YMLocalizedString("tab.message"),
|
||||
normalImage: "tab_message_normal",
|
||||
selectedImage: "tab_message_selected"
|
||||
)
|
||||
// 角标绑定
|
||||
messageVC.unreadCountDidChange = { [weak self] c in
|
||||
let value: String? = c > 0 ? (c > 99 ? "99+" : "\(c)") : nil
|
||||
self?.viewControllers?[1].tabBarItem.badgeValue = value
|
||||
}
|
||||
|
||||
// 创建我的页
|
||||
let mineVC = EPMineViewController()
|
||||
mineVC.title = YMLocalizedString("tab.mine")
|
||||
let mineNav = createTransparentNavigationController(
|
||||
rootViewController: mineVC,
|
||||
tabTitle: YMLocalizedString("tab.mine"),
|
||||
normalImage: "tab_mine_normal",
|
||||
selectedImage: "tab_mine_selected"
|
||||
)
|
||||
|
||||
viewControllers = [momentNav, messageNav, mineNav]
|
||||
NSLog("[EPTabBarController] 登录后 ViewControllers 创建完成 - Moment & Message & Mine")
|
||||
|
||||
selectedIndex = 0
|
||||
}
|
||||
|
||||
|
||||
private func createTransparentNavigationController(
|
||||
rootViewController: UIViewController,
|
||||
tabTitle: String,
|
||||
normalImage: String,
|
||||
selectedImage: String
|
||||
) -> UINavigationController {
|
||||
let nav = UINavigationController(rootViewController: rootViewController)
|
||||
nav.navigationBar.isTranslucent = true
|
||||
nav.navigationBar.setBackgroundImage(UIImage(), for: .default)
|
||||
nav.navigationBar.shadowImage = UIImage()
|
||||
nav.view.backgroundColor = .clear
|
||||
nav.tabBarItem = createTabBarItem(
|
||||
title: tabTitle,
|
||||
normalImage: normalImage,
|
||||
selectedImage: selectedImage
|
||||
)
|
||||
|
||||
|
||||
nav.delegate = self
|
||||
|
||||
return nav
|
||||
}
|
||||
|
||||
// MARK: - TabBar Visibility Control
|
||||
|
||||
|
||||
private func showCustomTabBar(animated: Bool = true) {
|
||||
guard customTabBarView.isHidden else { return }
|
||||
|
||||
if animated {
|
||||
customTabBarView.isHidden = false
|
||||
customTabBarView.alpha = 0
|
||||
customTabBarView.transform = CGAffineTransform(translationX: 0, y: 20)
|
||||
|
||||
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseOut) {
|
||||
self.customTabBarView.alpha = 1
|
||||
self.customTabBarView.transform = .identity
|
||||
}
|
||||
} else {
|
||||
customTabBarView.isHidden = false
|
||||
customTabBarView.alpha = 1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func hideCustomTabBar(animated: Bool = true) {
|
||||
guard !customTabBarView.isHidden else { return }
|
||||
|
||||
if animated {
|
||||
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseIn, animations: {
|
||||
self.customTabBarView.alpha = 0
|
||||
self.customTabBarView.transform = CGAffineTransform(translationX: 0, y: 20)
|
||||
}) { _ in
|
||||
self.customTabBarView.isHidden = true
|
||||
self.customTabBarView.transform = .identity
|
||||
}
|
||||
} else {
|
||||
customTabBarView.isHidden = true
|
||||
customTabBarView.alpha = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITabBarControllerDelegate
|
||||
|
||||
extension EPTabBarController: UITabBarControllerDelegate {
|
||||
|
||||
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
|
||||
NSLog("[EPTabBarController] 选中 Tab: \(item.title ?? "Unknown")")
|
||||
}
|
||||
|
||||
|
||||
func tabBarController(_ tabBarController: UITabBarController,
|
||||
animationControllerForTransitionFrom fromVC: UIViewController,
|
||||
to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
func tabBarController(_ tabBarController: UITabBarController,
|
||||
shouldSelect viewController: UIViewController) -> Bool {
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UINavigationControllerDelegate
|
||||
|
||||
extension EPTabBarController: UINavigationControllerDelegate {
|
||||
|
||||
func navigationController(_ navigationController: UINavigationController,
|
||||
willShow viewController: UIViewController,
|
||||
animated: Bool) {
|
||||
|
||||
|
||||
let isRootViewController = navigationController.viewControllers.count == 1
|
||||
|
||||
if isRootViewController {
|
||||
|
||||
showCustomTabBar(animated: animated)
|
||||
NSLog("[EPTabBarController] 显示 TabBar - 根页面")
|
||||
} else {
|
||||
|
||||
hideCustomTabBar(animated: animated)
|
||||
NSLog("[EPTabBarController] 隐藏 TabBar - 子页面 (层级: \(navigationController.viewControllers.count))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Auto Login & Ticket Validation
|
||||
|
||||
extension EPTabBarController {
|
||||
|
||||
|
||||
private func performAutoLogin() {
|
||||
|
||||
guard let accountModel = AccountInfoStorage.instance().getCurrentAccountInfo() else {
|
||||
NSLog("[EPTabBarController] ⚠️ 账号信息不存在,跳转到登录页")
|
||||
handleTokenInvalid()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let uid = accountModel.uid
|
||||
let accessToken = accountModel.access_token
|
||||
|
||||
guard !uid.isEmpty, !accessToken.isEmpty else {
|
||||
NSLog("[EPTabBarController] ⚠️ uid 或 access_token 为空,跳转到登录页")
|
||||
handleTokenInvalid()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let existingTicket = AccountInfoStorage.instance().getTicket() ?? ""
|
||||
if !existingTicket.isEmpty {
|
||||
NSLog("[EPTabBarController] ✅ Ticket 已存在,自动登录成功")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
NSLog("[EPTabBarController] 🔄 Ticket 不存在,正在请求...")
|
||||
let loginService = EPLoginService()
|
||||
|
||||
loginService.requestTicket(accessToken: accessToken) { ticket in
|
||||
NSLog("[EPTabBarController] ✅ Ticket 请求成功: \(ticket)")
|
||||
AccountInfoStorage.instance().saveTicket(ticket)
|
||||
} failure: { [weak self] code, msg in
|
||||
NSLog("[EPTabBarController] ❌ Ticket 请求失败 (\(code)): \(msg)")
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self?.handleTokenInvalid()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func handleTokenInvalid() {
|
||||
NSLog("[EPTabBarController] ⚠️ Token 失效,清空账号数据...")
|
||||
|
||||
|
||||
AccountInfoStorage.instance().saveAccountInfo(nil)
|
||||
AccountInfoStorage.instance().saveTicket("")
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let loginVC = EPLoginViewController()
|
||||
let nav = BaseNavigationController(rootViewController: loginVC)
|
||||
nav.modalPresentationStyle = .fullScreen
|
||||
|
||||
|
||||
if #available(iOS 13.0, *) {
|
||||
for scene in UIApplication.shared.connectedScenes {
|
||||
if let windowScene = scene as? UIWindowScene,
|
||||
windowScene.activationState == .foregroundActive,
|
||||
let window = windowScene.windows.first(where: { $0.isKeyWindow }) {
|
||||
window.rootViewController = nav
|
||||
window.makeKeyAndVisible()
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let window = UIApplication.shared.keyWindow {
|
||||
window.rootViewController = nav
|
||||
window.makeKeyAndVisible()
|
||||
}
|
||||
}
|
||||
|
||||
NSLog("[EPTabBarController] ✅ 已跳转到登录页")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - OC Compatibility
|
||||
|
||||
extension EPTabBarController {
|
||||
|
||||
|
||||
@objc static func create() -> EPTabBarController {
|
||||
return EPTabBarController()
|
||||
}
|
||||
|
||||
|
||||
@objc func refreshTabBarWithIsLogin(_ isLogin: Bool) {
|
||||
refreshTabBar(isLogin: isLogin)
|
||||
}
|
||||
}
|
||||
29
YuMi/E-P/SDKManager/NIMSDK/EPNIMConfig.h
Normal file
29
YuMi/E-P/SDKManager/NIMSDK/EPNIMConfig.h
Normal file
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// EPNIMConfig.h
|
||||
// YuMi
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// NIMSDK 配置模型(从 ClientConfig 派生)
|
||||
@interface EPNIMConfig : NSObject
|
||||
|
||||
@property (nonatomic, copy) NSString *appKey;
|
||||
@property (nonatomic, copy) NSString *apnsCername;
|
||||
@property (nonatomic, assign) BOOL shouldConsiderRevokedMessageUnreadCount;
|
||||
@property (nonatomic, assign) BOOL shouldSyncStickTopSessionInfos;
|
||||
@property (nonatomic, assign) BOOL enabledHttpsForInfo;
|
||||
@property (nonatomic, assign) BOOL enabledHttpsForMessage;
|
||||
@property (nonatomic, assign) NSInteger cdnTrackInterval;
|
||||
@property (nonatomic, assign) NSInteger chatroomMessageReceiveMinInterval;
|
||||
|
||||
/// 从 ClientConfig 创建配置;若缺失 nimKey 则返回 nil
|
||||
+ (instancetype _Nullable)configFromClientConfig;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
|
||||
39
YuMi/E-P/SDKManager/NIMSDK/EPNIMConfig.m
Normal file
39
YuMi/E-P/SDKManager/NIMSDK/EPNIMConfig.m
Normal file
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// EPNIMConfig.m
|
||||
// YuMi
|
||||
//
|
||||
|
||||
#import "EPNIMConfig.h"
|
||||
#import "ClientConfig.h"
|
||||
#import "YUMIConstant.h"
|
||||
|
||||
@implementation EPNIMConfig
|
||||
|
||||
+ (instancetype _Nullable)configFromClientConfig {
|
||||
ClientConfig *client = [ClientConfig shareConfig];
|
||||
if (client.configInfo == nil) {
|
||||
return nil;
|
||||
}
|
||||
NSString *nimKey = client.configInfo.nimKey;
|
||||
if (nimKey.length == 0) {
|
||||
return nil;
|
||||
}
|
||||
EPNIMConfig *cfg = [[EPNIMConfig alloc] init];
|
||||
cfg.appKey = nimKey;
|
||||
#ifdef DEBUG
|
||||
cfg.apnsCername = @"pikoDevelopPush";
|
||||
#else
|
||||
cfg.apnsCername = @"newPiko";
|
||||
#endif
|
||||
cfg.shouldConsiderRevokedMessageUnreadCount = YES;
|
||||
cfg.shouldSyncStickTopSessionInfos = YES;
|
||||
cfg.enabledHttpsForInfo = YES;
|
||||
cfg.enabledHttpsForMessage = YES;
|
||||
cfg.cdnTrackInterval = 0;
|
||||
cfg.chatroomMessageReceiveMinInterval = 50;
|
||||
return cfg;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
26
YuMi/E-P/SDKManager/NIMSDK/EPNIMManager.h
Normal file
26
YuMi/E-P/SDKManager/NIMSDK/EPNIMManager.h
Normal file
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// EPNIMManager.h
|
||||
// YuMi
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface EPNIMManager : NSObject
|
||||
|
||||
+ (instancetype)sharedManager;
|
||||
|
||||
- (void)initializeWithCompletion:(void(^ _Nullable)(NSError * _Nullable error))completion;
|
||||
|
||||
- (void)updateApnsToken:(NSData *)deviceToken;
|
||||
|
||||
- (NSInteger)allUnreadCount;
|
||||
|
||||
- (BOOL)isInitialized;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
|
||||
61
YuMi/E-P/SDKManager/NIMSDK/EPNIMManager.m
Normal file
61
YuMi/E-P/SDKManager/NIMSDK/EPNIMManager.m
Normal file
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// EPNIMManager.m
|
||||
// YuMi
|
||||
//
|
||||
|
||||
#import "EPNIMManager.h"
|
||||
#import "EPNIMConfig.h"
|
||||
#import <NIMSDK/NIMSDK.h>
|
||||
#import "CustomAttachmentDecoder.h"
|
||||
|
||||
@interface EPNIMManager ()
|
||||
@property (nonatomic, assign) BOOL initialized;
|
||||
@end
|
||||
|
||||
@implementation EPNIMManager
|
||||
|
||||
+ (instancetype)sharedManager {
|
||||
static EPNIMManager *s;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{ s = [EPNIMManager new]; });
|
||||
return s;
|
||||
}
|
||||
|
||||
- (void)initializeWithCompletion:(void(^ _Nullable)(NSError * _Nullable error))completion {
|
||||
if (self.initialized) {
|
||||
if (completion) completion(nil);
|
||||
return;
|
||||
}
|
||||
EPNIMConfig *cfg = [EPNIMConfig configFromClientConfig];
|
||||
if (!cfg) {
|
||||
if (completion) {
|
||||
completion([NSError errorWithDomain:@"EPNIM" code:-1001 userInfo:@{NSLocalizedDescriptionKey:@"ClientConfig not ready or nimKey missing"}]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
NIMSDKOption *option = [NIMSDKOption optionWithAppKey:cfg.appKey];
|
||||
option.apnsCername = cfg.apnsCername;
|
||||
[[NIMSDK sharedSDK] registerWithOption:option];
|
||||
[NIMCustomObject registerCustomDecoder:[[CustomAttachmentDecoder alloc] init]];
|
||||
[NIMSDKConfig sharedConfig].shouldConsiderRevokedMessageUnreadCount = cfg.shouldConsiderRevokedMessageUnreadCount;
|
||||
[[NIMSDKConfig sharedConfig] setShouldSyncStickTopSessionInfos:cfg.shouldSyncStickTopSessionInfos];
|
||||
[NIMSDKConfig sharedConfig].enabledHttpsForInfo = cfg.enabledHttpsForInfo;
|
||||
[NIMSDKConfig sharedConfig].enabledHttpsForMessage = cfg.enabledHttpsForMessage;
|
||||
[NIMSDKConfig sharedConfig].cdnTrackInterval = cfg.cdnTrackInterval;
|
||||
[NIMSDKConfig sharedConfig].chatroomMessageReceiveMinInterval = cfg.chatroomMessageReceiveMinInterval;
|
||||
self.initialized = YES;
|
||||
if (completion) completion(nil);
|
||||
}
|
||||
|
||||
- (void)updateApnsToken:(NSData *)deviceToken {
|
||||
if (!deviceToken) return;
|
||||
[[NIMSDK sharedSDK] updateApnsToken:deviceToken];
|
||||
}
|
||||
|
||||
- (NSInteger)allUnreadCount { return [NIMSDK sharedSDK].conversationManager.allUnreadCount; }
|
||||
|
||||
- (BOOL)isInitialized { return self.initialized; }
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
//
|
||||
// GlobalEventManager.h
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-10-09.
|
||||
// Copyright © 2025 YuMi. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// 全局事件管理器
|
||||
/// 负责处理原 TabBar 中的全局逻辑(SDK 代理、房间最小化、通知等)
|
||||
@interface GlobalEventManager : NSObject
|
||||
|
||||
/// 单例
|
||||
+ (instancetype)shared;
|
||||
|
||||
// MARK: - SDK Delegates Setup
|
||||
|
||||
/// 设置所有第三方 SDK 的代理
|
||||
- (void)setupSDKDelegates;
|
||||
|
||||
/// 移除所有代理(dealloc 时调用)
|
||||
- (void)removeAllDelegates;
|
||||
|
||||
// MARK: - Room Mini View
|
||||
|
||||
/// 设置房间最小化视图
|
||||
/// @param containerView 父视图(通常是 TabBar 的 view)
|
||||
- (void)setupRoomMiniViewOn:(UIView *)containerView;
|
||||
|
||||
/// 处理房间最小化通知
|
||||
/// @param userInfo 通知携带的数据
|
||||
- (void)handleRoomMini:(NSDictionary * _Nullable)userInfo;
|
||||
|
||||
/// 隐藏房间最小化视图
|
||||
- (void)hideRoomMiniView;
|
||||
|
||||
// MARK: - Global Notifications
|
||||
|
||||
/// 处理配置重载通知
|
||||
- (void)handleConfigReload;
|
||||
|
||||
/// 处理新用户充值通知
|
||||
- (void)handleNewUserRecharge;
|
||||
|
||||
/// 处理主播卡片通知
|
||||
/// @param notification 通知对象
|
||||
- (void)handleAnchorCard:(NSNotification * _Nullable)notification;
|
||||
|
||||
/// 处理语言切换通知
|
||||
/// @param notification 通知对象
|
||||
- (void)handleLanguageSwitch:(NSNotification * _Nullable)notification;
|
||||
|
||||
// MARK: - User Info
|
||||
|
||||
/// 获取用户信息成功后的处理
|
||||
/// @param userInfo 用户信息模型
|
||||
- (void)handleUserInfoSuccess:(id)userInfo;
|
||||
|
||||
// MARK: - Social Share
|
||||
|
||||
/// 注册社交分享回调
|
||||
- (void)registerSocialShareCallback;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,242 +0,0 @@
|
||||
//
|
||||
// GlobalEventManager.m
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-10-09.
|
||||
// Copyright © 2025 YuMi. All rights reserved.
|
||||
//
|
||||
|
||||
#import "GlobalEventManager.h"
|
||||
#import "XPMiniRoomView.h"
|
||||
#import "RoomBoomManager.h"
|
||||
#import "PublicRoomManager.h"
|
||||
#import "XPSkillCardPlayerManager.h"
|
||||
#import "SocialShareManager.h"
|
||||
#import "YUMIConstant.h"
|
||||
#import <NIMSDK/NIMSDK.h>
|
||||
|
||||
@interface GlobalEventManager () <NIMLoginManagerDelegate, NIMChatManagerDelegate, NIMSystemNotificationManagerDelegate, NIMBroadcastManagerDelegate>
|
||||
|
||||
// MARK: - Private Properties
|
||||
|
||||
/// 房间最小化视图
|
||||
@property (nonatomic, strong) XPMiniRoomView *miniRoomView;
|
||||
|
||||
/// 配置重载回调
|
||||
@property (nonatomic, copy) void(^configReloadCallback)(void);
|
||||
|
||||
/// 新用户充值回调
|
||||
@property (nonatomic, copy) void(^newUserRechargeCallback)(void);
|
||||
|
||||
@end
|
||||
|
||||
@implementation GlobalEventManager
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
+ (instancetype)shared {
|
||||
static GlobalEventManager *instance = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
instance = [[GlobalEventManager alloc] init];
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
[self setupNotificationObservers];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[self removeAllDelegates];
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
// MARK: - SDK Delegates Setup
|
||||
|
||||
- (void)setupSDKDelegates {
|
||||
// NIMSDK 代理设置
|
||||
[[NIMSDK sharedSDK].loginManager addDelegate:self];
|
||||
[[NIMSDK sharedSDK].chatManager addDelegate:self];
|
||||
[[NIMSDK sharedSDK].systemNotificationManager addDelegate:self];
|
||||
[[NIMSDK sharedSDK].broadcastManager addDelegate:self];
|
||||
|
||||
// RoomBoomManager 回调注册
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[[RoomBoomManager sharedManager] registerBoomBanner:^(id sth) {
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
if (!strongSelf) return;
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// 检查用户是否在房间中
|
||||
if ([XPSkillCardPlayerManager shareInstance].isInRoom) {
|
||||
NSLog(@"[GlobalEventManager] 收到 RoomBoom 通知");
|
||||
// TODO: 显示 Boom Banner
|
||||
// [RoomBoomBannerAnimation display:window with:sth tapToRoom:YES complete:^{}];
|
||||
}
|
||||
});
|
||||
} target:self];
|
||||
|
||||
NSLog(@"[GlobalEventManager] SDK 代理设置完成");
|
||||
}
|
||||
|
||||
- (void)removeAllDelegates {
|
||||
[[NIMSDK sharedSDK].loginManager removeDelegate:self];
|
||||
[[NIMSDK sharedSDK].chatManager removeDelegate:self];
|
||||
[[NIMSDK sharedSDK].systemNotificationManager removeDelegate:self];
|
||||
[[NIMSDK sharedSDK].broadcastManager removeDelegate:self];
|
||||
[[RoomBoomManager sharedManager] removeEventListenerForTarget:self];
|
||||
|
||||
NSLog(@"[GlobalEventManager] 所有代理已移除");
|
||||
}
|
||||
|
||||
// MARK: - Room Mini View
|
||||
|
||||
- (void)setupRoomMiniViewOn:(UIView *)containerView {
|
||||
if (!self.miniRoomView) {
|
||||
self.miniRoomView = [[XPMiniRoomView alloc] init];
|
||||
}
|
||||
[containerView addSubview:self.miniRoomView];
|
||||
NSLog(@"[GlobalEventManager] 房间最小化视图已添加");
|
||||
}
|
||||
|
||||
- (void)handleRoomMini:(NSDictionary *)userInfo {
|
||||
if (self.miniRoomView) {
|
||||
// TODO: 处理房间最小化逻辑
|
||||
NSLog(@"[GlobalEventManager] 处理房间最小化: %@", userInfo);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)hideRoomMiniView {
|
||||
if (self.miniRoomView) {
|
||||
[self.miniRoomView hiddenRoomMiniView];
|
||||
NSLog(@"[GlobalEventManager] 房间最小化视图已隐藏");
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Notification Observers
|
||||
|
||||
- (void)setupNotificationObservers {
|
||||
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
|
||||
|
||||
// 房间最小化通知
|
||||
[center addObserver:self
|
||||
selector:@selector(onRoomMiniNotification:)
|
||||
name:kRoomMiniNotificationKey
|
||||
object:nil];
|
||||
|
||||
// 配置重载通知
|
||||
[center addObserver:self
|
||||
selector:@selector(onConfigReloadNotification:)
|
||||
name:@"reloadAfterLoadConfig"
|
||||
object:nil];
|
||||
|
||||
// 语言切换通知
|
||||
[center addObserver:self
|
||||
selector:@selector(onLanguageSwitchNotification:)
|
||||
name:@"kSwitchLanguage"
|
||||
object:nil];
|
||||
|
||||
NSLog(@"[GlobalEventManager] 通知监听已设置");
|
||||
}
|
||||
|
||||
- (void)onRoomMiniNotification:(NSNotification *)notification {
|
||||
[self handleRoomMini:notification.userInfo];
|
||||
}
|
||||
|
||||
- (void)onConfigReloadNotification:(NSNotification *)notification {
|
||||
[self handleConfigReload];
|
||||
}
|
||||
|
||||
- (void)onLanguageSwitchNotification:(NSNotification *)notification {
|
||||
[self handleLanguageSwitch:notification];
|
||||
}
|
||||
|
||||
// MARK: - Global Notifications Handler
|
||||
|
||||
- (void)handleConfigReload {
|
||||
NSLog(@"[GlobalEventManager] 配置重载");
|
||||
if (self.configReloadCallback) {
|
||||
self.configReloadCallback();
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleNewUserRecharge {
|
||||
NSLog(@"[GlobalEventManager] 新用户充值");
|
||||
if (self.newUserRechargeCallback) {
|
||||
self.newUserRechargeCallback();
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleAnchorCard:(NSNotification *)notification {
|
||||
NSLog(@"[GlobalEventManager] 主播卡片通知: %@", notification.userInfo);
|
||||
// TODO: 实现主播卡片逻辑
|
||||
}
|
||||
|
||||
- (void)handleLanguageSwitch:(NSNotification *)notification {
|
||||
NSLog(@"[GlobalEventManager] 语言切换: %@", notification.userInfo);
|
||||
// TODO: 实现语言切换逻辑
|
||||
}
|
||||
|
||||
// MARK: - User Info
|
||||
|
||||
- (void)handleUserInfoSuccess:(id)userInfo {
|
||||
NSLog(@"[GlobalEventManager] 用户信息获取成功");
|
||||
|
||||
// 更新各个 Manager 的用户信息
|
||||
if ([userInfo respondsToSelector:@selector(uid)]) {
|
||||
[[PublicRoomManager sharedManager] initialize];
|
||||
[[PublicRoomManager sharedManager] updateUserInfo:userInfo];
|
||||
[[RoomBoomManager sharedManager] saveUserInfo:userInfo];
|
||||
[[XPSkillCardPlayerManager shareInstance] setUserInfoModel:userInfo];
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Social Share
|
||||
|
||||
- (void)registerSocialShareCallback {
|
||||
// 延迟 2 秒检查社交分享
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
[[SocialShareManager sharedManager] checkSocialShareItem];
|
||||
NSLog(@"[GlobalEventManager] 社交分享回调已注册");
|
||||
});
|
||||
}
|
||||
|
||||
// MARK: - NIMSDK Delegate Methods
|
||||
|
||||
#pragma mark - NIMLoginManagerDelegate
|
||||
|
||||
- (void)onLogin:(NIMLoginStep)step {
|
||||
NSLog(@"[GlobalEventManager] NIMSDK 登录步骤: %ld", (long)step);
|
||||
}
|
||||
|
||||
- (void)onKickout:(NIMKickReason)code clientType:(NIMLoginClientType)clientType {
|
||||
NSLog(@"[GlobalEventManager] NIMSDK 被踢出: reason=%ld, clientType=%ld", (long)code, (long)clientType);
|
||||
}
|
||||
|
||||
- (void)onAutoLoginFailed:(NSError *)error {
|
||||
NSLog(@"[GlobalEventManager] NIMSDK 自动登录失败: %@", error);
|
||||
}
|
||||
|
||||
#pragma mark - NIMChatManagerDelegate
|
||||
|
||||
- (void)onRecvMessages:(NSArray<NIMMessage *> *)messages {
|
||||
NSLog(@"[GlobalEventManager] 收到 %lu 条消息", (unsigned long)messages.count);
|
||||
}
|
||||
|
||||
#pragma mark - NIMSystemNotificationManagerDelegate
|
||||
|
||||
- (void)onReceiveSystemNotification:(NIMSystemNotification *)notification {
|
||||
NSLog(@"[GlobalEventManager] 收到系统通知: %@", notification.notificationId);
|
||||
}
|
||||
|
||||
#pragma mark - NIMBroadcastManagerDelegate
|
||||
|
||||
- (void)onReceiveBroadcastMessage:(NIMBroadcastMessage *)message {
|
||||
NSLog(@"[GlobalEventManager] 收到广播消息: %@", message.content);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -12,7 +12,7 @@
|
||||
@implementation YUMIHtmlUrl
|
||||
|
||||
NSString * const URLWithType(URLType type) {
|
||||
NSString * prefix = @"molistar";
|
||||
NSString * prefix = @"eparty";
|
||||
NSDictionary *newDic = @{
|
||||
@(kTreasureTicketBuyURL) : @"modules/act-treasureSnatching/index.html",///夺宝购买
|
||||
@(kTreasureRankListURL) : @"modules/act-treasureSnatching/list.html",///夺宝达人
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
//
|
||||
///一些宏
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "../Tools/Bundle/NSBundle+Localizable.h"
|
||||
|
||||
#ifndef YUMIMacroUitls_h
|
||||
@@ -23,7 +24,19 @@ isPhoneXSeries = [[UIApplication sharedApplication] delegate].window.safeAreaIns
|
||||
|
||||
#define KScreenWidth [[UIScreen mainScreen] bounds].size.width
|
||||
#define KScreenHeight [[UIScreen mainScreen] bounds].size.height
|
||||
#define statusbarHeight [[UIApplication sharedApplication] statusBarFrame].size.height
|
||||
|
||||
// 兼容 iOS 13+ 的状态栏高度获取
|
||||
#define statusbarHeight ({\
|
||||
CGFloat height = 0;\
|
||||
if (@available(iOS 13.0, *)) {\
|
||||
UIWindowScene *windowScene = (UIWindowScene *)[[[UIApplication sharedApplication] connectedScenes] allObjects].firstObject;\
|
||||
height = windowScene.statusBarManager.statusBarFrame.size.height;\
|
||||
} else {\
|
||||
height = [[UIApplication sharedApplication] statusBarFrame].size.height;\
|
||||
}\
|
||||
height;\
|
||||
})
|
||||
|
||||
#define kStatusBarHeight statusbarHeight
|
||||
#define kSafeAreaBottomHeight (iPhoneXSeries ? 34 : 0)
|
||||
#define kSafeAreaTopHeight (iPhoneXSeries ? 24 : 0)
|
||||
@@ -36,8 +49,37 @@ isPhoneXSeries = [[UIApplication sharedApplication] delegate].window.safeAreaIns
|
||||
#define kRoundValue(value) round(kScreenScale * value)
|
||||
#define kWeakify(o) try{}@finally{} __weak typeof(o) o##Weak = o;
|
||||
#define kStrongify(o) autoreleasepool{} __strong typeof(o) o = o##Weak;
|
||||
///keyWindow
|
||||
#define kWindow [UIApplication sharedApplication].keyWindow
|
||||
|
||||
/// 兼容 iOS 13+ 的 keyWindow 获取(Swift & ObjC 通用)
|
||||
/// 使用此函数统一获取 keyWindow,避免在各处重复实现
|
||||
/// 自动处理 iOS 13+ 的 UIWindowScene 和旧版本的兼容性
|
||||
static inline __attribute__((unused)) UIWindow * _Nullable kGetKeyWindow(void) {
|
||||
UIWindow *window = nil;
|
||||
if (@available(iOS 13.0, *)) {
|
||||
for (UIWindowScene *scene in [UIApplication sharedApplication].connectedScenes) {
|
||||
if (scene.activationState == UISceneActivationStateForegroundActive) {
|
||||
for (UIWindow *w in scene.windows) {
|
||||
if (w.isKeyWindow) {
|
||||
return w;
|
||||
}
|
||||
}
|
||||
if (scene.windows.firstObject) {
|
||||
return scene.windows.firstObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
window = [UIApplication sharedApplication].keyWindow;
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
return window;
|
||||
}
|
||||
|
||||
// 兼容旧代码:保留宏入口
|
||||
#define kWindow kGetKeyWindow()
|
||||
|
||||
#define kImage(image) [UIImage imageNamed:image]
|
||||
|
||||
///UIFont
|
||||
@@ -49,15 +91,15 @@ isPhoneXSeries = [[UIApplication sharedApplication] delegate].window.safeAreaIns
|
||||
#define kFontHeavy(font) [UIFont systemFontOfSize:kGetScaleWidth(font) weight:UIFontWeightHeavy]
|
||||
|
||||
///内置版本号
|
||||
#define PI_App_Version @"1.0.31"
|
||||
#define PI_App_Version @"1.0.0"
|
||||
///渠道
|
||||
#define PI_App_Source @"appstore"
|
||||
#define PI_Test_Flight @"TestFlight"
|
||||
#define ISTestFlight 0
|
||||
///正式环境
|
||||
#define API_HOST_URL @"https://api.hfighting.com"
|
||||
#define API_HOST_URL @"https://api.epartylive.com"
|
||||
///测试环境
|
||||
#define API_HOST_TEST_URL @"http://beta.api.pekolive.com" // http://beta.api.pekolive.com | http://beta.api.molistar.xyz
|
||||
#define API_HOST_TEST_URL @"http://beta.api.epartylive.com" // http://beta.api.epartylive.com | http://beta.api.pekolive.com | http://beta.api.molistar.xyz
|
||||
|
||||
#define API_Image_URL @"https://image.hfighting.com"
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
<key>FacebookClientToken</key>
|
||||
<string>189d1a90712cc61cedded4cf1372cb21</string>
|
||||
<key>FacebookDisplayName</key>
|
||||
<string>MoliStar</string>
|
||||
<string>E-Party</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
@@ -96,17 +96,17 @@
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>“MoliStar”需要您的同意,才可以访问进行拍照并上传您的图片,然后展示在您的个人主页上,便于他人查看</string>
|
||||
<string>"E-Party"需要您的同意,才可以访问进行拍照并上传您的图片,然后展示在您的个人主页上,便于他人查看</string>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>此App将可发现和连接到您所用网络上的设备。</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>“MoliStar”需要您的同意,才可以进行定位服务,推荐附近好友</string>
|
||||
<string>"E-Party"需要您的同意,才可以进行定位服务,推荐附近好友</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>“MoliStar”需要您的同意,才可以进行语音聊天</string>
|
||||
<string>"E-Party"需要您的同意,才可以进行语音聊天</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>“MoliStar”需要您的同意,才可以存储相片到相册</string>
|
||||
<string>"E-Party"需要您的同意,才可以存储相片到相册</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>“MoliStar”需要您的同意,才可以访问相册并选择您需要上传的图片,然后展示在您的个人主页上,便于他人查看</string>
|
||||
<string>"E-Party"需要您的同意,才可以访问相册并选择您需要上传的图片,然后展示在您的个人主页上,便于他人查看</string>
|
||||
<key>NSUserTrackingUsageDescription</key>
|
||||
<string>請允許我們獲取您的IDFA權限,可以為您提供個性化活動和服務。未經您的允許,您的信息將不作其他用途。</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
|
||||
@@ -1,235 +0,0 @@
|
||||
//
|
||||
// NewMineViewController.m
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-10-09.
|
||||
// Copyright © 2025 YuMi. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NewMineViewController.h"
|
||||
#import "NewMineHeaderView.h"
|
||||
#import <Masonry/Masonry.h>
|
||||
|
||||
@interface NewMineViewController () <UITableViewDelegate, UITableViewDataSource>
|
||||
|
||||
// MARK: - UI Components
|
||||
|
||||
/// 主列表
|
||||
@property (nonatomic, strong) UITableView *tableView;
|
||||
|
||||
/// 顶部个人信息卡片
|
||||
@property (nonatomic, strong) NewMineHeaderView *headerView;
|
||||
|
||||
/// 设置按钮
|
||||
@property (nonatomic, strong) UIButton *settingsButton;
|
||||
|
||||
// MARK: - Data
|
||||
|
||||
/// 菜单项数据源
|
||||
@property (nonatomic, strong) NSArray<NSDictionary *> *menuItems;
|
||||
|
||||
@end
|
||||
|
||||
@implementation NewMineViewController
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.title = @"我的";
|
||||
self.view.backgroundColor = [UIColor colorWithRed:0.96 green:0.96 blue:0.96 alpha:1.0]; // 浅灰背景
|
||||
|
||||
[self setupNavigationBar];
|
||||
[self setupUI];
|
||||
[self loadData];
|
||||
|
||||
NSLog(@"[NewMineViewController] 页面加载完成");
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
// 刷新用户信息
|
||||
[self refreshUserInfo];
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
- (void)setupNavigationBar {
|
||||
// 设置按钮(右上角)
|
||||
self.settingsButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
[self.settingsButton setTitle:@"⚙️" forState:UIControlStateNormal];
|
||||
self.settingsButton.titleLabel.font = [UIFont systemFontOfSize:24];
|
||||
[self.settingsButton addTarget:self action:@selector(onSettingsButtonTapped) forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
UIBarButtonItem *settingsBarButton = [[UIBarButtonItem alloc] initWithCustomView:self.settingsButton];
|
||||
self.navigationItem.rightBarButtonItem = settingsBarButton;
|
||||
}
|
||||
|
||||
- (void)setupUI {
|
||||
// TableView
|
||||
[self.view addSubview:self.tableView];
|
||||
[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(self.view);
|
||||
}];
|
||||
|
||||
// 设置头部视图
|
||||
self.tableView.tableHeaderView = self.headerView;
|
||||
|
||||
NSLog(@"[NewMineViewController] UI 设置完成");
|
||||
}
|
||||
|
||||
// MARK: - Data Loading
|
||||
|
||||
- (void)loadData {
|
||||
// 菜单项配置(完全不同的顺序和图标)
|
||||
self.menuItems = @[
|
||||
@{@"title": @"💎 我的钱包", @"type": @"wallet"},
|
||||
@{@"title": @"📊 数据统计", @"type": @"stats"},
|
||||
@{@"title": @"⭐️ 我的收藏", @"type": @"favorites"},
|
||||
@{@"title": @"📝 编辑资料", @"type": @"profile"},
|
||||
@{@"title": @"🔔 消息通知", @"type": @"notifications"},
|
||||
@{@"title": @"🎨 主题设置", @"type": @"theme"},
|
||||
@{@"title": @"🌐 语言切换", @"type": @"language"},
|
||||
@{@"title": @"ℹ️ 关于我们", @"type": @"about"},
|
||||
];
|
||||
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
- (void)refreshUserInfo {
|
||||
// TODO: 从接口获取用户信息
|
||||
// 暂时使用模拟数据
|
||||
NSDictionary *mockUserInfo = @{
|
||||
@"nickname": @"测试用户",
|
||||
@"avatar": @"",
|
||||
@"level": @(12),
|
||||
@"exp": @(3580),
|
||||
@"nextLevelExp": @(5000),
|
||||
@"followers": @(128),
|
||||
@"following": @(256),
|
||||
};
|
||||
|
||||
[self.headerView configureWithUserInfo:mockUserInfo];
|
||||
NSLog(@"[NewMineViewController] 用户信息已刷新");
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
- (void)onSettingsButtonTapped {
|
||||
NSLog(@"[NewMineViewController] 设置按钮点击");
|
||||
// TODO: 跳转到设置页面
|
||||
[self showAlertWithMessage:@"设置功能开发中"];
|
||||
}
|
||||
|
||||
- (void)onMenuItemTapped:(NSDictionary *)item {
|
||||
NSString *type = item[@"type"];
|
||||
NSString *title = item[@"title"];
|
||||
|
||||
NSLog(@"[NewMineViewController] 菜单项点击: %@", type);
|
||||
|
||||
if ([type isEqualToString:@"wallet"]) {
|
||||
// TODO: 跳转到钱包页面
|
||||
[self showAlertWithMessage:@"钱包功能开发中"];
|
||||
} else if ([type isEqualToString:@"stats"]) {
|
||||
// TODO: 跳转到数据统计页面
|
||||
[self showAlertWithMessage:@"数据统计功能开发中"];
|
||||
} else if ([type isEqualToString:@"favorites"]) {
|
||||
// TODO: 跳转到收藏页面
|
||||
[self showAlertWithMessage:@"收藏功能开发中"];
|
||||
} else if ([type isEqualToString:@"profile"]) {
|
||||
// TODO: 跳转到编辑资料页面
|
||||
[self showAlertWithMessage:@"编辑资料功能开发中"];
|
||||
} else {
|
||||
[self showAlertWithMessage:[NSString stringWithFormat:@"%@ 功能开发中", title]];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)showAlertWithMessage:(NSString *)message {
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提示"
|
||||
message:message
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]];
|
||||
[self presentViewController:alert animated:YES completion:nil];
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSource
|
||||
|
||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
|
||||
return 1;
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
return self.menuItems.count;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
static NSString *identifier = @"MenuCell";
|
||||
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
|
||||
|
||||
if (!cell) {
|
||||
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
|
||||
cell.backgroundColor = [UIColor whiteColor];
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
cell.textLabel.font = [UIFont systemFontOfSize:15];
|
||||
cell.textLabel.textColor = [UIColor colorWithWhite:0.2 alpha:1.0];
|
||||
}
|
||||
|
||||
if (indexPath.row < self.menuItems.count) {
|
||||
NSDictionary *item = self.menuItems[indexPath.row];
|
||||
cell.textLabel.text = item[@"title"];
|
||||
}
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
[tableView deselectRowAtIndexPath:indexPath animated:YES];
|
||||
|
||||
if (indexPath.row < self.menuItems.count) {
|
||||
NSDictionary *item = self.menuItems[indexPath.row];
|
||||
[self onMenuItemTapped:item];
|
||||
}
|
||||
}
|
||||
|
||||
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return 56; // 卡片式高度
|
||||
}
|
||||
|
||||
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
|
||||
return 15; // 上间距
|
||||
}
|
||||
|
||||
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
|
||||
UIView *header = [[UIView alloc] init];
|
||||
header.backgroundColor = [UIColor clearColor];
|
||||
return header;
|
||||
}
|
||||
|
||||
// MARK: - Lazy Loading
|
||||
|
||||
- (UITableView *)tableView {
|
||||
if (!_tableView) {
|
||||
_tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
|
||||
_tableView.delegate = self;
|
||||
_tableView.dataSource = self;
|
||||
_tableView.separatorStyle = UITableViewCellSeparatorStyleSingleLine;
|
||||
_tableView.separatorInset = UIEdgeInsetsMake(0, 15, 0, 15);
|
||||
_tableView.backgroundColor = self.view.backgroundColor;
|
||||
_tableView.showsVerticalScrollIndicator = NO;
|
||||
_tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
|
||||
}
|
||||
return _tableView;
|
||||
}
|
||||
|
||||
- (NewMineHeaderView *)headerView {
|
||||
if (!_headerView) {
|
||||
_headerView = [[NewMineHeaderView alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, 280)];
|
||||
}
|
||||
return _headerView;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,23 +0,0 @@
|
||||
//
|
||||
// NewMineHeaderView.h
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-10-09.
|
||||
// Copyright © 2025 YuMi. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// 新的个人中心头部视图
|
||||
/// 纵向卡片式设计 + 渐变背景
|
||||
@interface NewMineHeaderView : UIView
|
||||
|
||||
/// 配置用户信息
|
||||
/// @param userInfo 用户信息字典
|
||||
- (void)configureWithUserInfo:(NSDictionary *)userInfo;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,278 +0,0 @@
|
||||
//
|
||||
// NewMineHeaderView.m
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-10-09.
|
||||
// Copyright © 2025 YuMi. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NewMineHeaderView.h"
|
||||
#import <Masonry/Masonry.h>
|
||||
|
||||
@interface NewMineHeaderView ()
|
||||
|
||||
// MARK: - UI Components
|
||||
|
||||
/// 渐变背景层
|
||||
@property (nonatomic, strong) CAGradientLayer *gradientLayer;
|
||||
|
||||
/// 头像(大尺寸,圆角矩形)
|
||||
@property (nonatomic, strong) UIImageView *avatarImageView;
|
||||
|
||||
/// 昵称
|
||||
@property (nonatomic, strong) UILabel *nicknameLabel;
|
||||
|
||||
/// 等级标签
|
||||
@property (nonatomic, strong) UILabel *levelLabel;
|
||||
|
||||
/// 经验进度条容器
|
||||
@property (nonatomic, strong) UIView *progressContainer;
|
||||
|
||||
/// 经验进度条
|
||||
@property (nonatomic, strong) UIView *progressBar;
|
||||
|
||||
/// 经验文本
|
||||
@property (nonatomic, strong) UILabel *expLabel;
|
||||
|
||||
/// 统计容器
|
||||
@property (nonatomic, strong) UIView *statsContainer;
|
||||
|
||||
/// 关注数标签
|
||||
@property (nonatomic, strong) UILabel *followingLabel;
|
||||
|
||||
/// 粉丝数标签
|
||||
@property (nonatomic, strong) UILabel *followersLabel;
|
||||
|
||||
@end
|
||||
|
||||
@implementation NewMineHeaderView
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
[self setupUI];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
// 更新渐变层大小
|
||||
self.gradientLayer.frame = self.bounds;
|
||||
}
|
||||
|
||||
// MARK: - Setup UI
|
||||
|
||||
- (void)setupUI {
|
||||
// 渐变背景(新的配色方案)
|
||||
self.gradientLayer = [CAGradientLayer layer];
|
||||
self.gradientLayer.colors = @[
|
||||
(id)[UIColor colorWithRed:0.2 green:0.6 blue:0.86 alpha:1.0].CGColor, // 主色调
|
||||
(id)[UIColor colorWithRed:0.3 green:0.5 blue:0.9 alpha:1.0].CGColor, // 渐变色
|
||||
];
|
||||
self.gradientLayer.startPoint = CGPointMake(0, 0);
|
||||
self.gradientLayer.endPoint = CGPointMake(1, 1);
|
||||
[self.layer insertSublayer:self.gradientLayer atIndex:0];
|
||||
|
||||
// 头像(大尺寸,圆角矩形)
|
||||
[self addSubview:self.avatarImageView];
|
||||
[self.avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.centerX.equalTo(self);
|
||||
make.top.equalTo(self).offset(40);
|
||||
make.size.mas_equalTo(CGSizeMake(80, 80));
|
||||
}];
|
||||
|
||||
// 昵称
|
||||
[self addSubview:self.nicknameLabel];
|
||||
[self.nicknameLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.centerX.equalTo(self);
|
||||
make.top.equalTo(self.avatarImageView.mas_bottom).offset(15);
|
||||
}];
|
||||
|
||||
// 等级标签
|
||||
[self addSubview:self.levelLabel];
|
||||
[self.levelLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.centerX.equalTo(self);
|
||||
make.top.equalTo(self.nicknameLabel.mas_bottom).offset(8);
|
||||
}];
|
||||
|
||||
// 经验进度条容器
|
||||
[self addSubview:self.progressContainer];
|
||||
[self.progressContainer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self).offset(40);
|
||||
make.right.equalTo(self).offset(-40);
|
||||
make.top.equalTo(self.levelLabel.mas_bottom).offset(15);
|
||||
make.height.mas_equalTo(8);
|
||||
}];
|
||||
|
||||
// 经验进度条
|
||||
[self.progressContainer addSubview:self.progressBar];
|
||||
[self.progressBar mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.top.bottom.equalTo(self.progressContainer);
|
||||
make.width.equalTo(self.progressContainer).multipliedBy(0.7); // 默认 70%
|
||||
}];
|
||||
|
||||
// 经验文本
|
||||
[self addSubview:self.expLabel];
|
||||
[self.expLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.centerX.equalTo(self);
|
||||
make.top.equalTo(self.progressContainer.mas_bottom).offset(6);
|
||||
}];
|
||||
|
||||
// 统计容器
|
||||
[self addSubview:self.statsContainer];
|
||||
[self.statsContainer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.right.equalTo(self);
|
||||
make.top.equalTo(self.expLabel.mas_bottom).offset(20);
|
||||
make.height.mas_equalTo(60);
|
||||
make.bottom.equalTo(self).offset(-15);
|
||||
}];
|
||||
|
||||
// 关注数
|
||||
[self.statsContainer addSubview:self.followingLabel];
|
||||
[self.followingLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.right.equalTo(self.statsContainer.mas_centerX).offset(-20);
|
||||
make.centerY.equalTo(self.statsContainer);
|
||||
}];
|
||||
|
||||
// 粉丝数
|
||||
[self.statsContainer addSubview:self.followersLabel];
|
||||
[self.followersLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.statsContainer.mas_centerX).offset(20);
|
||||
make.centerY.equalTo(self.statsContainer);
|
||||
}];
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
|
||||
- (void)configureWithUserInfo:(NSDictionary *)userInfo {
|
||||
// 配置昵称
|
||||
self.nicknameLabel.text = userInfo[@"nickname"] ?: @"未设置昵称";
|
||||
|
||||
// 配置等级
|
||||
NSNumber *level = userInfo[@"level"];
|
||||
self.levelLabel.text = [NSString stringWithFormat:@"Lv.%@", level ?: @"0"];
|
||||
|
||||
// 配置经验
|
||||
NSNumber *exp = userInfo[@"exp"];
|
||||
NSNumber *nextLevelExp = userInfo[@"nextLevelExp"];
|
||||
CGFloat progress = [exp floatValue] / [nextLevelExp floatValue];
|
||||
[self.progressBar mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.top.bottom.equalTo(self.progressContainer);
|
||||
make.width.equalTo(self.progressContainer).multipliedBy(MAX(0.1, MIN(1.0, progress)));
|
||||
}];
|
||||
self.expLabel.text = [NSString stringWithFormat:@"%@ / %@", exp, nextLevelExp];
|
||||
|
||||
// 配置统计
|
||||
NSNumber *followers = userInfo[@"followers"];
|
||||
NSNumber *following = userInfo[@"following"];
|
||||
self.followingLabel.text = [NSString stringWithFormat:@"关注\n%@", following ?: @"0"];
|
||||
self.followersLabel.text = [NSString stringWithFormat:@"粉丝\n%@", followers ?: @"0"];
|
||||
|
||||
NSLog(@"[NewMineHeaderView] 用户信息已配置: %@", userInfo[@"nickname"]);
|
||||
}
|
||||
|
||||
// MARK: - Lazy Loading
|
||||
|
||||
- (UIImageView *)avatarImageView {
|
||||
if (!_avatarImageView) {
|
||||
_avatarImageView = [[UIImageView alloc] init];
|
||||
_avatarImageView.backgroundColor = [UIColor whiteColor];
|
||||
_avatarImageView.layer.cornerRadius = 16; // 圆角矩形
|
||||
_avatarImageView.layer.masksToBounds = YES;
|
||||
_avatarImageView.layer.borderWidth = 3;
|
||||
_avatarImageView.layer.borderColor = [UIColor whiteColor].CGColor;
|
||||
_avatarImageView.contentMode = UIViewContentModeScaleAspectFill;
|
||||
}
|
||||
return _avatarImageView;
|
||||
}
|
||||
|
||||
- (UILabel *)nicknameLabel {
|
||||
if (!_nicknameLabel) {
|
||||
_nicknameLabel = [[UILabel alloc] init];
|
||||
_nicknameLabel.font = [UIFont systemFontOfSize:20 weight:UIFontWeightBold];
|
||||
_nicknameLabel.textColor = [UIColor whiteColor];
|
||||
_nicknameLabel.textAlignment = NSTextAlignmentCenter;
|
||||
}
|
||||
return _nicknameLabel;
|
||||
}
|
||||
|
||||
- (UILabel *)levelLabel {
|
||||
if (!_levelLabel) {
|
||||
_levelLabel = [[UILabel alloc] init];
|
||||
_levelLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];
|
||||
_levelLabel.textColor = [UIColor colorWithWhite:1.0 alpha:0.9];
|
||||
_levelLabel.textAlignment = NSTextAlignmentCenter;
|
||||
_levelLabel.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.2];
|
||||
_levelLabel.layer.cornerRadius = 12;
|
||||
_levelLabel.layer.masksToBounds = YES;
|
||||
|
||||
// 添加内边距
|
||||
_levelLabel.text = @" Lv.0 ";
|
||||
}
|
||||
return _levelLabel;
|
||||
}
|
||||
|
||||
- (UIView *)progressContainer {
|
||||
if (!_progressContainer) {
|
||||
_progressContainer = [[UIView alloc] init];
|
||||
_progressContainer.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.3];
|
||||
_progressContainer.layer.cornerRadius = 4;
|
||||
_progressContainer.layer.masksToBounds = YES;
|
||||
}
|
||||
return _progressContainer;
|
||||
}
|
||||
|
||||
- (UIView *)progressBar {
|
||||
if (!_progressBar) {
|
||||
_progressBar = [[UIView alloc] init];
|
||||
_progressBar.backgroundColor = [UIColor whiteColor];
|
||||
_progressBar.layer.cornerRadius = 4;
|
||||
_progressBar.layer.masksToBounds = YES;
|
||||
}
|
||||
return _progressBar;
|
||||
}
|
||||
|
||||
- (UILabel *)expLabel {
|
||||
if (!_expLabel) {
|
||||
_expLabel = [[UILabel alloc] init];
|
||||
_expLabel.font = [UIFont systemFontOfSize:12];
|
||||
_expLabel.textColor = [UIColor colorWithWhite:1.0 alpha:0.8];
|
||||
_expLabel.textAlignment = NSTextAlignmentCenter;
|
||||
}
|
||||
return _expLabel;
|
||||
}
|
||||
|
||||
- (UIView *)statsContainer {
|
||||
if (!_statsContainer) {
|
||||
_statsContainer = [[UIView alloc] init];
|
||||
_statsContainer.backgroundColor = [UIColor clearColor];
|
||||
}
|
||||
return _statsContainer;
|
||||
}
|
||||
|
||||
- (UILabel *)followingLabel {
|
||||
if (!_followingLabel) {
|
||||
_followingLabel = [[UILabel alloc] init];
|
||||
_followingLabel.font = [UIFont systemFontOfSize:14];
|
||||
_followingLabel.textColor = [UIColor whiteColor];
|
||||
_followingLabel.textAlignment = NSTextAlignmentCenter;
|
||||
_followingLabel.numberOfLines = 2;
|
||||
}
|
||||
return _followingLabel;
|
||||
}
|
||||
|
||||
- (UILabel *)followersLabel {
|
||||
if (!_followersLabel) {
|
||||
_followersLabel = [[UILabel alloc] init];
|
||||
_followersLabel.font = [UIFont systemFontOfSize:14];
|
||||
_followersLabel.textColor = [UIColor whiteColor];
|
||||
_followersLabel.textAlignment = NSTextAlignmentCenter;
|
||||
_followersLabel.numberOfLines = 2;
|
||||
}
|
||||
return _followersLabel;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,240 +0,0 @@
|
||||
//
|
||||
// NewMomentViewController.m
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-10-09.
|
||||
// Copyright © 2025 YuMi. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NewMomentViewController.h"
|
||||
#import "NewMomentCell.h"
|
||||
#import <Masonry/Masonry.h>
|
||||
|
||||
@interface NewMomentViewController () <UITableViewDelegate, UITableViewDataSource>
|
||||
|
||||
// MARK: - UI Components
|
||||
|
||||
/// 主列表(卡片式布局)
|
||||
@property (nonatomic, strong) UITableView *tableView;
|
||||
|
||||
/// 刷新控件
|
||||
@property (nonatomic, strong) UIRefreshControl *refreshControl;
|
||||
|
||||
/// 发布按钮
|
||||
@property (nonatomic, strong) UIButton *publishButton;
|
||||
|
||||
// MARK: - Data
|
||||
|
||||
/// 动态数据源
|
||||
@property (nonatomic, strong) NSMutableArray *dataSource;
|
||||
|
||||
/// 当前页码
|
||||
@property (nonatomic, assign) NSInteger currentPage;
|
||||
|
||||
/// 是否正在加载
|
||||
@property (nonatomic, assign) BOOL isLoading;
|
||||
|
||||
@end
|
||||
|
||||
@implementation NewMomentViewController
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.title = @"动态";
|
||||
self.view.backgroundColor = [UIColor colorWithRed:0.96 green:0.96 blue:0.96 alpha:1.0]; // 浅灰背景
|
||||
|
||||
[self setupUI];
|
||||
[self loadData];
|
||||
|
||||
NSLog(@"[NewMomentViewController] 页面加载完成");
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
// 隐藏导航栏(如果需要沉浸式体验)
|
||||
// [self.navigationController setNavigationBarHidden:YES animated:animated];
|
||||
}
|
||||
|
||||
// MARK: - Setup UI
|
||||
|
||||
- (void)setupUI {
|
||||
// TableView
|
||||
[self.view addSubview:self.tableView];
|
||||
[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(self.view);
|
||||
}];
|
||||
|
||||
// 发布按钮(悬浮在右下角)
|
||||
[self.view addSubview:self.publishButton];
|
||||
[self.publishButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.right.equalTo(self.view).offset(-20);
|
||||
make.bottom.equalTo(self.view).offset(-100); // 避开 TabBar
|
||||
make.size.mas_equalTo(CGSizeMake(56, 56));
|
||||
}];
|
||||
|
||||
NSLog(@"[NewMomentViewController] UI 设置完成");
|
||||
}
|
||||
|
||||
// MARK: - Data Loading
|
||||
|
||||
- (void)loadData {
|
||||
if (self.isLoading) return;
|
||||
|
||||
self.isLoading = YES;
|
||||
NSLog(@"[NewMomentViewController] 开始加载数据,页码: %ld", (long)self.currentPage);
|
||||
|
||||
// TODO: 调用 API 加载动态数据
|
||||
// 暂时使用模拟数据
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
// 模拟数据
|
||||
for (int i = 0; i < 10; i++) {
|
||||
NSDictionary *mockData = @{
|
||||
@"id": @(self.currentPage * 10 + i),
|
||||
@"content": [NSString stringWithFormat:@"这是第 %ld 页的第 %d 条动态", (long)self.currentPage, i],
|
||||
@"images": @[],
|
||||
@"likeCount": @(arc4random() % 100),
|
||||
@"commentCount": @(arc4random() % 50),
|
||||
};
|
||||
[self.dataSource addObject:mockData];
|
||||
}
|
||||
|
||||
self.currentPage++;
|
||||
self.isLoading = NO;
|
||||
[self.tableView reloadData];
|
||||
[self.refreshControl endRefreshing];
|
||||
|
||||
NSLog(@"[NewMomentViewController] 数据加载完成,当前数据量: %lu", (unsigned long)self.dataSource.count);
|
||||
});
|
||||
}
|
||||
|
||||
- (void)onRefresh {
|
||||
self.currentPage = 0;
|
||||
[self.dataSource removeAllObjects];
|
||||
[self loadData];
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
- (void)onPublishButtonTapped {
|
||||
NSLog(@"[NewMomentViewController] 发布按钮点击");
|
||||
// TODO: 跳转到发布页面
|
||||
[self showAlertWithMessage:@"发布功能开发中"];
|
||||
}
|
||||
|
||||
- (void)showAlertWithMessage:(NSString *)message {
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提示"
|
||||
message:message
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]];
|
||||
[self presentViewController:alert animated:YES completion:nil];
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSource
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
return self.dataSource.count;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
NewMomentCell *cell = [tableView dequeueReusableCellWithIdentifier:@"NewMomentCell" forIndexPath:indexPath];
|
||||
|
||||
if (indexPath.row < self.dataSource.count) {
|
||||
NSDictionary *data = self.dataSource[indexPath.row];
|
||||
[cell configureWithData:data];
|
||||
}
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
[tableView deselectRowAtIndexPath:indexPath animated:YES];
|
||||
|
||||
NSLog(@"[NewMomentViewController] 点击动态: %ld", (long)indexPath.row);
|
||||
// TODO: 跳转到详情页
|
||||
[self showAlertWithMessage:[NSString stringWithFormat:@"点击了第 %ld 条动态", (long)indexPath.row]];
|
||||
}
|
||||
|
||||
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return UITableViewAutomaticDimension;
|
||||
}
|
||||
|
||||
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return 200;
|
||||
}
|
||||
|
||||
// 滚动到底部时加载更多
|
||||
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
|
||||
CGFloat offsetY = scrollView.contentOffset.y;
|
||||
CGFloat contentHeight = scrollView.contentSize.height;
|
||||
CGFloat screenHeight = scrollView.frame.size.height;
|
||||
|
||||
if (offsetY > contentHeight - screenHeight - 100 && !self.isLoading) {
|
||||
[self loadData];
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Lazy Loading
|
||||
|
||||
- (UITableView *)tableView {
|
||||
if (!_tableView) {
|
||||
_tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
|
||||
_tableView.delegate = self;
|
||||
_tableView.dataSource = self;
|
||||
_tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
|
||||
_tableView.backgroundColor = self.view.backgroundColor;
|
||||
_tableView.estimatedRowHeight = 200;
|
||||
_tableView.rowHeight = UITableViewAutomaticDimension;
|
||||
_tableView.showsVerticalScrollIndicator = NO;
|
||||
_tableView.contentInset = UIEdgeInsetsMake(10, 0, 10, 0);
|
||||
|
||||
// 注册 Cell
|
||||
[_tableView registerClass:[NewMomentCell class] forCellReuseIdentifier:@"NewMomentCell"];
|
||||
|
||||
// 添加下拉刷新
|
||||
_tableView.refreshControl = self.refreshControl;
|
||||
}
|
||||
return _tableView;
|
||||
}
|
||||
|
||||
- (UIRefreshControl *)refreshControl {
|
||||
if (!_refreshControl) {
|
||||
_refreshControl = [[UIRefreshControl alloc] init];
|
||||
[_refreshControl addTarget:self action:@selector(onRefresh) forControlEvents:UIControlEventValueChanged];
|
||||
}
|
||||
return _refreshControl;
|
||||
}
|
||||
|
||||
- (UIButton *)publishButton {
|
||||
if (!_publishButton) {
|
||||
_publishButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
_publishButton.backgroundColor = [UIColor colorWithRed:0.2 green:0.6 blue:0.86 alpha:1.0]; // 主色调
|
||||
_publishButton.layer.cornerRadius = 28;
|
||||
_publishButton.layer.shadowColor = [UIColor blackColor].CGColor;
|
||||
_publishButton.layer.shadowOffset = CGSizeMake(0, 2);
|
||||
_publishButton.layer.shadowOpacity = 0.3;
|
||||
_publishButton.layer.shadowRadius = 4;
|
||||
|
||||
// 设置图标(暂时使用文字)
|
||||
[_publishButton setTitle:@"+" forState:UIControlStateNormal];
|
||||
_publishButton.titleLabel.font = [UIFont systemFontOfSize:32 weight:UIFontWeightLight];
|
||||
[_publishButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
||||
|
||||
[_publishButton addTarget:self action:@selector(onPublishButtonTapped) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
return _publishButton;
|
||||
}
|
||||
|
||||
- (NSMutableArray *)dataSource {
|
||||
if (!_dataSource) {
|
||||
_dataSource = [NSMutableArray array];
|
||||
}
|
||||
return _dataSource;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,276 +0,0 @@
|
||||
//
|
||||
// NewMomentCell.m
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-10-09.
|
||||
// Copyright © 2025 YuMi. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NewMomentCell.h"
|
||||
#import <Masonry/Masonry.h>
|
||||
|
||||
@interface NewMomentCell ()
|
||||
|
||||
// MARK: - UI Components
|
||||
|
||||
/// 卡片容器
|
||||
@property (nonatomic, strong) UIView *cardView;
|
||||
|
||||
/// 头像
|
||||
@property (nonatomic, strong) UIImageView *avatarImageView;
|
||||
|
||||
/// 用户名
|
||||
@property (nonatomic, strong) UILabel *nameLabel;
|
||||
|
||||
/// 时间标签
|
||||
@property (nonatomic, strong) UILabel *timeLabel;
|
||||
|
||||
/// 内容标签
|
||||
@property (nonatomic, strong) UILabel *contentLabel;
|
||||
|
||||
/// 图片容器(可选)
|
||||
@property (nonatomic, strong) UIView *imagesContainer;
|
||||
|
||||
/// 底部操作栏
|
||||
@property (nonatomic, strong) UIView *actionBar;
|
||||
|
||||
/// 点赞按钮
|
||||
@property (nonatomic, strong) UIButton *likeButton;
|
||||
|
||||
/// 评论按钮
|
||||
@property (nonatomic, strong) UIButton *commentButton;
|
||||
|
||||
/// 分享按钮
|
||||
@property (nonatomic, strong) UIButton *shareButton;
|
||||
|
||||
@end
|
||||
|
||||
@implementation NewMomentCell
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
|
||||
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
|
||||
self.selectionStyle = UITableViewCellSelectionStyleNone;
|
||||
self.backgroundColor = [UIColor clearColor];
|
||||
[self setupUI];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
// MARK: - Setup UI
|
||||
|
||||
- (void)setupUI {
|
||||
// 卡片容器(圆角矩形 + 阴影)
|
||||
[self.contentView addSubview:self.cardView];
|
||||
[self.cardView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.contentView).offset(15);
|
||||
make.right.equalTo(self.contentView).offset(-15);
|
||||
make.top.equalTo(self.contentView).offset(8);
|
||||
make.bottom.equalTo(self.contentView).offset(-8);
|
||||
}];
|
||||
|
||||
// 头像(圆角矩形,不是圆形!)
|
||||
[self.cardView addSubview:self.avatarImageView];
|
||||
[self.avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.cardView).offset(15);
|
||||
make.top.equalTo(self.cardView).offset(15);
|
||||
make.size.mas_equalTo(CGSizeMake(40, 40));
|
||||
}];
|
||||
|
||||
// 用户名
|
||||
[self.cardView addSubview:self.nameLabel];
|
||||
[self.nameLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.avatarImageView.mas_right).offset(10);
|
||||
make.top.equalTo(self.avatarImageView);
|
||||
make.right.equalTo(self.cardView).offset(-15);
|
||||
}];
|
||||
|
||||
// 时间
|
||||
[self.cardView addSubview:self.timeLabel];
|
||||
[self.timeLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.nameLabel);
|
||||
make.bottom.equalTo(self.avatarImageView);
|
||||
make.right.equalTo(self.cardView).offset(-15);
|
||||
}];
|
||||
|
||||
// 内容
|
||||
[self.cardView addSubview:self.contentLabel];
|
||||
[self.contentLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.cardView).offset(15);
|
||||
make.right.equalTo(self.cardView).offset(-15);
|
||||
make.top.equalTo(self.avatarImageView.mas_bottom).offset(12);
|
||||
}];
|
||||
|
||||
// 底部操作栏
|
||||
[self.cardView addSubview:self.actionBar];
|
||||
[self.actionBar mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.right.equalTo(self.cardView);
|
||||
make.top.equalTo(self.contentLabel.mas_bottom).offset(15);
|
||||
make.height.mas_equalTo(50);
|
||||
make.bottom.equalTo(self.cardView).offset(-8);
|
||||
}];
|
||||
|
||||
// 点赞按钮
|
||||
[self.actionBar addSubview:self.likeButton];
|
||||
[self.likeButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.actionBar).offset(15);
|
||||
make.centerY.equalTo(self.actionBar);
|
||||
make.width.mas_greaterThanOrEqualTo(60);
|
||||
}];
|
||||
|
||||
// 评论按钮
|
||||
[self.actionBar addSubview:self.commentButton];
|
||||
[self.commentButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.centerX.equalTo(self.actionBar);
|
||||
make.centerY.equalTo(self.actionBar);
|
||||
make.width.mas_greaterThanOrEqualTo(60);
|
||||
}];
|
||||
|
||||
// 分享按钮
|
||||
[self.actionBar addSubview:self.shareButton];
|
||||
[self.shareButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.right.equalTo(self.actionBar).offset(-15);
|
||||
make.centerY.equalTo(self.actionBar);
|
||||
make.width.mas_greaterThanOrEqualTo(60);
|
||||
}];
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
|
||||
- (void)configureWithData:(NSDictionary *)data {
|
||||
// 配置用户名
|
||||
self.nameLabel.text = data[@"userName"] ?: @"匿名用户";
|
||||
|
||||
// 配置时间
|
||||
self.timeLabel.text = @"2小时前"; // TODO: 实际计算时间
|
||||
|
||||
// 配置内容
|
||||
self.contentLabel.text = data[@"content"] ?: @"";
|
||||
|
||||
// 配置点赞数
|
||||
NSNumber *likeCount = data[@"likeCount"];
|
||||
[self.likeButton setTitle:[NSString stringWithFormat:@"👍 %@", likeCount ?: @"0"] forState:UIControlStateNormal];
|
||||
|
||||
// 配置评论数
|
||||
NSNumber *commentCount = data[@"commentCount"];
|
||||
[self.commentButton setTitle:[NSString stringWithFormat:@"💬 %@", commentCount ?: @"0"] forState:UIControlStateNormal];
|
||||
|
||||
// 配置分享
|
||||
[self.shareButton setTitle:@"🔗 分享" forState:UIControlStateNormal];
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
- (void)onLikeButtonTapped {
|
||||
NSLog(@"[NewMomentCell] 点赞");
|
||||
// TODO: 实现点赞逻辑
|
||||
}
|
||||
|
||||
- (void)onCommentButtonTapped {
|
||||
NSLog(@"[NewMomentCell] 评论");
|
||||
// TODO: 实现评论逻辑
|
||||
}
|
||||
|
||||
- (void)onShareButtonTapped {
|
||||
NSLog(@"[NewMomentCell] 分享");
|
||||
// TODO: 实现分享逻辑
|
||||
}
|
||||
|
||||
// MARK: - Lazy Loading
|
||||
|
||||
- (UIView *)cardView {
|
||||
if (!_cardView) {
|
||||
_cardView = [[UIView alloc] init];
|
||||
_cardView.backgroundColor = [UIColor whiteColor];
|
||||
_cardView.layer.cornerRadius = 12; // 圆角
|
||||
_cardView.layer.shadowColor = [UIColor blackColor].CGColor;
|
||||
_cardView.layer.shadowOffset = CGSizeMake(0, 2);
|
||||
_cardView.layer.shadowOpacity = 0.1;
|
||||
_cardView.layer.shadowRadius = 8;
|
||||
_cardView.layer.masksToBounds = NO;
|
||||
}
|
||||
return _cardView;
|
||||
}
|
||||
|
||||
- (UIImageView *)avatarImageView {
|
||||
if (!_avatarImageView) {
|
||||
_avatarImageView = [[UIImageView alloc] init];
|
||||
_avatarImageView.backgroundColor = [UIColor colorWithWhite:0.9 alpha:1.0];
|
||||
_avatarImageView.layer.cornerRadius = 8; // 圆角矩形,不是圆形!
|
||||
_avatarImageView.layer.masksToBounds = YES;
|
||||
_avatarImageView.contentMode = UIViewContentModeScaleAspectFill;
|
||||
}
|
||||
return _avatarImageView;
|
||||
}
|
||||
|
||||
- (UILabel *)nameLabel {
|
||||
if (!_nameLabel) {
|
||||
_nameLabel = [[UILabel alloc] init];
|
||||
_nameLabel.font = [UIFont systemFontOfSize:15 weight:UIFontWeightMedium];
|
||||
_nameLabel.textColor = [UIColor colorWithWhite:0.2 alpha:1.0];
|
||||
}
|
||||
return _nameLabel;
|
||||
}
|
||||
|
||||
- (UILabel *)timeLabel {
|
||||
if (!_timeLabel) {
|
||||
_timeLabel = [[UILabel alloc] init];
|
||||
_timeLabel.font = [UIFont systemFontOfSize:12];
|
||||
_timeLabel.textColor = [UIColor colorWithWhite:0.6 alpha:1.0];
|
||||
}
|
||||
return _timeLabel;
|
||||
}
|
||||
|
||||
- (UILabel *)contentLabel {
|
||||
if (!_contentLabel) {
|
||||
_contentLabel = [[UILabel alloc] init];
|
||||
_contentLabel.font = [UIFont systemFontOfSize:15];
|
||||
_contentLabel.textColor = [UIColor colorWithWhite:0.3 alpha:1.0];
|
||||
_contentLabel.numberOfLines = 0;
|
||||
_contentLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
}
|
||||
return _contentLabel;
|
||||
}
|
||||
|
||||
- (UIView *)actionBar {
|
||||
if (!_actionBar) {
|
||||
_actionBar = [[UIView alloc] init];
|
||||
_actionBar.backgroundColor = [UIColor colorWithWhite:0.98 alpha:1.0];
|
||||
}
|
||||
return _actionBar;
|
||||
}
|
||||
|
||||
- (UIButton *)likeButton {
|
||||
if (!_likeButton) {
|
||||
_likeButton = [self createActionButtonWithTitle:@"👍 0"];
|
||||
[_likeButton addTarget:self action:@selector(onLikeButtonTapped) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
return _likeButton;
|
||||
}
|
||||
|
||||
- (UIButton *)commentButton {
|
||||
if (!_commentButton) {
|
||||
_commentButton = [self createActionButtonWithTitle:@"💬 0"];
|
||||
[_commentButton addTarget:self action:@selector(onCommentButtonTapped) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
return _commentButton;
|
||||
}
|
||||
|
||||
- (UIButton *)shareButton {
|
||||
if (!_shareButton) {
|
||||
_shareButton = [self createActionButtonWithTitle:@"🔗 分享"];
|
||||
[_shareButton addTarget:self action:@selector(onShareButtonTapped) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
return _shareButton;
|
||||
}
|
||||
|
||||
- (UIButton *)createActionButtonWithTitle:(NSString *)title {
|
||||
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
[button setTitle:title forState:UIControlStateNormal];
|
||||
button.titleLabel.font = [UIFont systemFontOfSize:13];
|
||||
[button setTitleColor:[UIColor colorWithWhite:0.5 alpha:1.0] forState:UIControlStateNormal];
|
||||
return button;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,192 +0,0 @@
|
||||
//
|
||||
// NewTabBarController.swift
|
||||
// YuMi
|
||||
//
|
||||
// Created by AI on 2025-10-09.
|
||||
// Copyright © 2025 YuMi. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// 新的 TabBar 控制器
|
||||
/// 只包含 Moment 和 Mine 两个 Tab
|
||||
class NewTabBarController: UITabBarController {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
/// 全局事件管理器
|
||||
private var globalEventManager: GlobalEventManager?
|
||||
|
||||
/// 是否已登录
|
||||
private var isLoggedIn: Bool = false
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
// 测试域名配置
|
||||
#if DEBUG
|
||||
APIConfig.testEncryption()
|
||||
#endif
|
||||
|
||||
setupTabBarAppearance()
|
||||
setupGlobalManagers()
|
||||
setupInitialViewControllers()
|
||||
|
||||
NSLog("[NewTabBarController] 初始化完成")
|
||||
}
|
||||
|
||||
deinit {
|
||||
globalEventManager?.removeAllDelegates()
|
||||
NSLog("[NewTabBarController] 已释放")
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
/// 设置 TabBar 外观
|
||||
private func setupTabBarAppearance() {
|
||||
// 自定义 TabBar 样式
|
||||
tabBar.tintColor = UIColor(red: 0.2, green: 0.6, blue: 0.86, alpha: 1.0) // 新主色调
|
||||
tabBar.unselectedItemTintColor = UIColor(white: 0.6, alpha: 1.0) // 新辅助色
|
||||
tabBar.backgroundColor = .white
|
||||
tabBar.isTranslucent = false
|
||||
|
||||
// 添加顶部分割线
|
||||
if #available(iOS 13.0, *) {
|
||||
let appearance = UITabBarAppearance()
|
||||
appearance.configureWithOpaqueBackground()
|
||||
appearance.backgroundColor = .white
|
||||
appearance.stackedLayoutAppearance.selected.iconColor = tabBar.tintColor
|
||||
appearance.stackedLayoutAppearance.selected.titleTextAttributes = [
|
||||
.foregroundColor: tabBar.tintColor ?? .blue,
|
||||
.font: UIFont.systemFont(ofSize: 10, weight: .medium)
|
||||
]
|
||||
appearance.stackedLayoutAppearance.normal.titleTextAttributes = [
|
||||
.foregroundColor: tabBar.unselectedItemTintColor ?? .gray,
|
||||
.font: UIFont.systemFont(ofSize: 10)
|
||||
]
|
||||
|
||||
tabBar.standardAppearance = appearance
|
||||
if #available(iOS 15.0, *) {
|
||||
tabBar.scrollEdgeAppearance = appearance
|
||||
}
|
||||
}
|
||||
|
||||
NSLog("[NewTabBarController] TabBar 外观设置完成")
|
||||
}
|
||||
|
||||
/// 设置全局管理器
|
||||
private func setupGlobalManagers() {
|
||||
globalEventManager = GlobalEventManager.shared()
|
||||
globalEventManager?.setupSDKDelegates()
|
||||
|
||||
// 设置房间最小化视图
|
||||
if let containerView = view {
|
||||
globalEventManager?.setupRoomMiniView(on: containerView)
|
||||
}
|
||||
|
||||
// 注册社交分享回调
|
||||
globalEventManager?.registerSocialShareCallback()
|
||||
|
||||
NSLog("[NewTabBarController] 全局管理器设置完成")
|
||||
}
|
||||
|
||||
/// 设置初始 ViewController(未登录状态)
|
||||
private func setupInitialViewControllers() {
|
||||
// TODO: 暂时使用空白页面占位
|
||||
let blankVC1 = UIViewController()
|
||||
blankVC1.view.backgroundColor = .white
|
||||
blankVC1.tabBarItem = createTabBarItem(
|
||||
title: "动态",
|
||||
normalImage: "tab_moment_normal",
|
||||
selectedImage: "tab_moment_selected"
|
||||
)
|
||||
|
||||
let blankVC2 = UIViewController()
|
||||
blankVC2.view.backgroundColor = .white
|
||||
blankVC2.tabBarItem = createTabBarItem(
|
||||
title: "我的",
|
||||
normalImage: "tab_mine_normal",
|
||||
selectedImage: "tab_mine_selected"
|
||||
)
|
||||
|
||||
viewControllers = [blankVC1, blankVC2]
|
||||
selectedIndex = 0
|
||||
|
||||
NSLog("[NewTabBarController] 初始 ViewControllers 设置完成")
|
||||
}
|
||||
|
||||
/// 创建 TabBarItem
|
||||
/// - Parameters:
|
||||
/// - title: 标题
|
||||
/// - normalImage: 未选中图标名称
|
||||
/// - selectedImage: 选中图标名称
|
||||
/// - Returns: UITabBarItem
|
||||
private func createTabBarItem(title: String, normalImage: String, selectedImage: String) -> UITabBarItem {
|
||||
let item = UITabBarItem(
|
||||
title: title,
|
||||
image: UIImage(named: normalImage)?.withRenderingMode(.alwaysOriginal),
|
||||
selectedImage: UIImage(named: selectedImage)?.withRenderingMode(.alwaysOriginal)
|
||||
)
|
||||
return item
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
|
||||
/// 登录成功后刷新 TabBar
|
||||
/// - Parameter isLogin: 是否已登录
|
||||
@objc func refreshTabBar(isLogin: Bool) {
|
||||
isLoggedIn = isLogin
|
||||
|
||||
if isLogin {
|
||||
setupLoggedInViewControllers()
|
||||
} else {
|
||||
setupInitialViewControllers()
|
||||
}
|
||||
|
||||
NSLog("[NewTabBarController] TabBar 已刷新,登录状态: \(isLogin)")
|
||||
}
|
||||
|
||||
/// 设置登录后的 ViewControllers
|
||||
private func setupLoggedInViewControllers() {
|
||||
// 创建真实的 ViewController(OC 类)
|
||||
let momentVC = NewMomentViewController()
|
||||
momentVC.tabBarItem = createTabBarItem(
|
||||
title: "动态",
|
||||
normalImage: "tab_moment_normal",
|
||||
selectedImage: "tab_moment_selected"
|
||||
)
|
||||
|
||||
let mineVC = NewMineViewController()
|
||||
mineVC.tabBarItem = createTabBarItem(
|
||||
title: "我的",
|
||||
normalImage: "tab_mine_normal",
|
||||
selectedImage: "tab_mine_selected"
|
||||
)
|
||||
|
||||
viewControllers = [momentVC, mineVC]
|
||||
selectedIndex = 0
|
||||
|
||||
NSLog("[NewTabBarController] 登录后 ViewControllers 设置完成 - Moment & Mine")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITabBarControllerDelegate
|
||||
|
||||
extension NewTabBarController: UITabBarControllerDelegate {
|
||||
|
||||
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
|
||||
NSLog("[NewTabBarController] 选中 Tab: \(item.title ?? "Unknown")")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - OC Compatibility
|
||||
|
||||
extension NewTabBarController {
|
||||
|
||||
/// OC 兼容:创建实例的工厂方法
|
||||
@objc static func create() -> NewTabBarController {
|
||||
return NewTabBarController()
|
||||
}
|
||||
}
|
||||
@@ -28,8 +28,8 @@
|
||||
/// @param phone 手机号
|
||||
/// @param password 验证码
|
||||
+ (void)loginWithPassword:(HttpRequestHelperCompletion)completion phone:(NSString *)phone password:(NSString *)password client_secret:(NSString *)client_secret version:(NSString *)version client_id:(NSString *)client_id grant_type:(NSString *)grant_type {
|
||||
NSString * fang = [NSString stringFromBase64String:@"b2F1dGgvdG9rZW4="];///oauth/token
|
||||
[self makeRequest:fang method:HttpRequestHelperMethodPOST completion:completion, __FUNCTION__,phone,password,client_secret,version, client_id, grant_type, nil];
|
||||
|
||||
[self makeRequest:@"oauth/token" method:HttpRequestHelperMethodPOST completion:completion, __FUNCTION__,phone,password,client_secret,version, client_id, grant_type, nil];
|
||||
}
|
||||
|
||||
/// 重置手机号登录密码
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#import "TurboModeStateManager.h"
|
||||
#import "FirstRechargeManager.h"
|
||||
#import "PublicRoomManager.h"
|
||||
///Swift
|
||||
#import "YuMi-Swift.h" // 引入 Swift 类(NewTabBarController)
|
||||
///Tool
|
||||
#import "XNDJTDDLoadingTool.h"
|
||||
#import "AccountInfoStorage.h"
|
||||
@@ -84,11 +86,15 @@
|
||||
|
||||
}
|
||||
+(void)jumpToHomeVCWithInviteCode:(NSString *)inviteCode{
|
||||
TabbarViewController *vc = [[TabbarViewController alloc] init];
|
||||
vc.isFormLogin = YES;
|
||||
vc.inviteCode = inviteCode;
|
||||
BaseNavigationController *bnc = [[BaseNavigationController alloc] initWithRootViewController:vc];
|
||||
kWindow.rootViewController = bnc;
|
||||
// ========== 白牌版本:使用新的 NewTabBarController ==========
|
||||
// 原代码已注释,改用 Swift 实现的 NewTabBarController
|
||||
|
||||
EPTabBarController *newTabBar = [EPTabBarController new];
|
||||
[newTabBar refreshTabBarWithIsLogin:YES];
|
||||
|
||||
// 设置为根控制器(不需要 NavigationController 包装)
|
||||
|
||||
kWindow.rootViewController = newTabBar;
|
||||
|
||||
// 登录成功并进入主页后,启动首充监控
|
||||
[[FirstRechargeManager sharedManager] startMonitoring];
|
||||
@@ -96,10 +102,22 @@
|
||||
// 初始化公共房间管理器
|
||||
[[PublicRoomManager sharedManager] initialize];
|
||||
|
||||
// 🔧 新增:启动 TurboModeStateManager
|
||||
// 🔧 启动 TurboModeStateManager
|
||||
NSString *userId = [[AccountInfoStorage instance] getUid];
|
||||
if (userId) {
|
||||
[[TurboModeStateManager sharedManager] startupWithCurrentUser:userId];
|
||||
}
|
||||
|
||||
NSLog(@"[PILoginManager] 已切换到白牌 TabBar:EPTabBarController");
|
||||
|
||||
// ========== 原代码(已注释) ==========
|
||||
/*
|
||||
TabbarViewController *vc = [[TabbarViewController alloc] init];
|
||||
vc.isFormLogin = YES;
|
||||
vc.inviteCode = inviteCode;
|
||||
BaseNavigationController *bnc = [[BaseNavigationController alloc] initWithRootViewController:vc];
|
||||
kWindow.rootViewController = bnc;
|
||||
*/
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -7,9 +7,6 @@
|
||||
|
||||
#import "BaseMvpPresenter.h"
|
||||
#import "YUMINNNN.h"
|
||||
#import <GoogleSignIn/GoogleSignIn.h>
|
||||
#import <GoogleSignIn/GIDGoogleUser.h>
|
||||
#import <GoogleSignIn/GoogleSignIn-umbrella.h>
|
||||
|
||||
@class FeedBackConfigModel;
|
||||
|
||||
@@ -19,18 +16,6 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
- (void)phoneQuickLogin:(NSString *)accessToken token:(NSString*) token;
|
||||
|
||||
|
||||
/// 第三方登录
|
||||
/// @param type 登录的类型
|
||||
- (void)thirdLoginWithType:(ThirdLoginType)type;
|
||||
|
||||
///第三方登录,谷歌登录
|
||||
-(void)thirdLoginByGoogleWithPresentingViewController:(UIViewController *)presentingViewController configuration:(GIDConfiguration *)configuration;
|
||||
///第三方登录,fb登录
|
||||
-(void)thirdLoginByFBWithPresentingViewController:(UIViewController *)presentingViewController;
|
||||
///第三方登录,line登录
|
||||
-(void)thirdLoginByLine:(UIViewController *)presentingViewController;
|
||||
|
||||
/// 获取手机的验证码
|
||||
/// @param phone 手机号
|
||||
/// @param type 类型
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#import "LoginPresenter.h"
|
||||
///Third
|
||||
#import <ReactiveObjC/ReactiveObjC.h>
|
||||
#import <ShareSDK/ShareSDK.h>
|
||||
///APi
|
||||
#import "Api+Login.h"
|
||||
///Tool
|
||||
@@ -56,75 +55,7 @@ static NSString *clinet_s = @"uyzjdhds";
|
||||
/// 第三方登录
|
||||
/// @param type 登录的类型
|
||||
- (void)thirdLoginWithType:(ThirdLoginType)type{
|
||||
|
||||
SSDKPlatformType platformType;
|
||||
switch (type) {
|
||||
case ThirdLoginType_FB:
|
||||
platformType = SSDKPlatformTypeFacebook;
|
||||
break;
|
||||
case ThirdLoginType_Line:
|
||||
platformType = SSDKPlatformTypeLine;
|
||||
break;
|
||||
case ThirdLoginType_Apple:
|
||||
platformType = SSDKPlatformTypeAppleAccount;
|
||||
break;
|
||||
case ThirdLoginType_Gmail:
|
||||
platformType = SSDKPlatformTypeGooglePlus;
|
||||
break;
|
||||
default:
|
||||
platformType = SSDKPlatformTypeAppleAccount;
|
||||
break;
|
||||
}
|
||||
NSDictionary * settings;
|
||||
if (type == SSDKPlatformTypeFacebook) {
|
||||
settings = @{@"isBrowser":@(YES)};
|
||||
}
|
||||
@kWeakify(self);
|
||||
[ShareSDK cancelAuthorize:platformType result:nil];
|
||||
[ShareSDK authorize:platformType
|
||||
settings:settings
|
||||
onStateChanged:^(SSDKResponseState state, SSDKUser *user, NSError *error) {
|
||||
@kStrongify(self);
|
||||
if (state == SSDKResponseStateSuccess) {///成功
|
||||
ThirdUserInfo * userInfo = [[ThirdUserInfo alloc] init];
|
||||
NSString * openid = @"";
|
||||
NSString * access_token = user.credential.token.length > 0 ? user.credential.token : @"";
|
||||
NSString * unionid = @"";
|
||||
if (platformType == SSDKPlatformTypeLine) {
|
||||
openid = user.credential.uid.length > 0 ? user.credential.uid : user.uid;
|
||||
unionid = user.credential.uid.length > 0 ? user.credential.uid : user.uid;
|
||||
userInfo.userName = user.nickname;
|
||||
userInfo.avatarUrl = user.icon;
|
||||
} else if (platformType == SSDKPlatformTypeFacebook) { //微信登录
|
||||
openid = user.credential.uid.length > 0 ? user.credential.uid : user.uid;;
|
||||
unionid = user.credential.uid.length > 0 ? user.credential.uid : user.uid;;
|
||||
userInfo.userName = user.nickname;
|
||||
userInfo.avatarUrl = user.icon;
|
||||
} else if (platformType == SSDKPlatformTypeAppleAccount) { //苹果登录
|
||||
// openid = user.credential.token;
|
||||
unionid = [user.credential rawData][@"user"];
|
||||
NSString * familyName = [user.credential rawData][@"fullName"][@"familyName"];
|
||||
NSString * givenName = [user.credential rawData][@"fullName"][@"givenName"];
|
||||
if (familyName.length > 0 && givenName.length> 0) {
|
||||
userInfo.userName = [NSString stringWithFormat:@"%@%@", familyName, givenName];
|
||||
}
|
||||
}
|
||||
if (unionid == nil) {
|
||||
unionid = @"";
|
||||
}
|
||||
openid = unionid;
|
||||
userInfo.openid = openid;
|
||||
userInfo.access_token = access_token;
|
||||
userInfo.unionid = unionid;
|
||||
///保存一下第三方的值
|
||||
[AccountInfoStorage instance].thirdUserInfo = userInfo;
|
||||
[self loginWithThirdPartWithType:type];
|
||||
} else if(state == SSDKResponseStateCancel) {///取消
|
||||
[[self getView] showErrorToast:YMLocalizedString(@"LoginPresenter0")];
|
||||
} else if (state == SSDKResponseStateFail) {///失败
|
||||
[[self getView] showErrorToast:YMLocalizedString(@"LoginPresenter1")];
|
||||
}
|
||||
}];
|
||||
// TODO: 后续补充 Apple Login
|
||||
}
|
||||
-(void)loginWithThirdPartWithType:(ThirdLoginType)type{
|
||||
[XNDJTDDLoadingTool showOnlyView:kWindow];
|
||||
@@ -154,68 +85,6 @@ static NSString *clinet_s = @"uyzjdhds";
|
||||
|
||||
|
||||
}
|
||||
-(void)thirdLoginByLine:(UIViewController *)presentingViewController {
|
||||
|
||||
}
|
||||
-(void)thirdLoginByFBWithPresentingViewController:(UIViewController *)presentingViewController {
|
||||
|
||||
}
|
||||
-(void)thirdLoginByGoogleWithPresentingViewController:(UIViewController *)presentingViewController configuration:(GIDConfiguration *)configuration{
|
||||
@kWeakify(self);
|
||||
[GIDSignIn sharedInstance].configuration = configuration;
|
||||
[GIDSignIn.sharedInstance signInWithPresentingViewController:presentingViewController
|
||||
completion:^(GIDSignInResult * _Nullable signInResult, NSError * _Nullable error) {
|
||||
@kStrongify(self);
|
||||
if (error) {
|
||||
if (error.code == kGIDSignInErrorCodeCanceled){
|
||||
[[self getView] showErrorToast:YMLocalizedString(@"LoginPresenter0")];
|
||||
}else{
|
||||
[[self getView] showErrorToast:YMLocalizedString(@"LoginPresenter1")];
|
||||
}
|
||||
} else {
|
||||
ThirdUserInfo * userInfo = [[ThirdUserInfo alloc] init];
|
||||
NSString * openid = signInResult.user.userID;
|
||||
NSString * access_token = signInResult.user.idToken.tokenString.length > 0 ? signInResult.user.idToken.tokenString : @"";
|
||||
NSString * unionid = signInResult.user.userID;
|
||||
userInfo.userName = signInResult.user.profile.name;
|
||||
userInfo.avatarUrl = [[signInResult.user.profile imageURLWithDimension:60] absoluteString];
|
||||
userInfo.openid = openid;
|
||||
userInfo.access_token = access_token;
|
||||
userInfo.unionid = unionid;
|
||||
///保存一下第三方的值
|
||||
[AccountInfoStorage instance].thirdUserInfo = userInfo;
|
||||
[self loginWithThirdGoogle];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
-(void)loginWithThirdGoogle{
|
||||
[XNDJTDDLoadingTool showOnlyView:kWindow];
|
||||
NSString * openid = [AccountInfoStorage instance].thirdUserInfo.openid;
|
||||
NSString * access_token = [AccountInfoStorage instance].thirdUserInfo.access_token;
|
||||
NSString * unionid = [AccountInfoStorage instance].thirdUserInfo.unionid;
|
||||
@kWeakify(self);
|
||||
[Api loginWithThirdPart:[self createHttpCompletion:^(BaseModel * _Nonnull data) {
|
||||
@kStrongify(self);
|
||||
[XNDJTDDLoadingTool hideOnlyView:kWindow];
|
||||
AccountModel * model = [AccountModel modelWithDictionary:data.data];
|
||||
if (model != nil) {
|
||||
[[AccountInfoStorage instance] saveAccountInfo:model];
|
||||
[[self getView] loginThirdPartSuccess];
|
||||
}
|
||||
}fail:^(NSInteger code, NSString * _Nullable msg) {
|
||||
@kStrongify(self);
|
||||
[XNDJTDDLoadingTool hideOnlyView:kWindow];
|
||||
if (msg.length == 0) {
|
||||
[[self getView] showErrorToast:YMLocalizedString(@"LoginPresenter1")];
|
||||
}
|
||||
} showLoading:YES errorToast:YES]
|
||||
openid:openid
|
||||
unionid:unionid
|
||||
access_token:access_token
|
||||
type:[NSString stringWithFormat:@"%lu", (unsigned long)ThirdLoginType_Gmail]];
|
||||
}
|
||||
|
||||
/// 获取手机的验证码
|
||||
/// @param phone 手机号
|
||||
/// @param type 类型
|
||||
|
||||
@@ -96,11 +96,6 @@
|
||||
[self initSubViewConstraints];
|
||||
[self initEvents];
|
||||
[self loadAllRegions];
|
||||
ClientConfig *config = [ClientConfig shareConfig];
|
||||
if (config.inviteCode.length > 0){
|
||||
self.inviteCode = config.inviteCode;
|
||||
config.inviteCode = @"";
|
||||
}
|
||||
|
||||
// 防止進入後還有 loading
|
||||
[XNDJTDDLoadingTool hideHUD];
|
||||
|
||||
@@ -20,9 +20,7 @@ NSString * const HadAgreePrivacy = @"HadAgreePrivacy";
|
||||
|
||||
typedef NS_ENUM(NSUInteger, LoginType) {
|
||||
LoginType_ID = 101,
|
||||
LoginType_Email = 102,
|
||||
LoginType_Google = 103,
|
||||
LoginType_Apple = 104
|
||||
LoginType_Email = 102
|
||||
};
|
||||
|
||||
@interface LoginViewController () <LoginProtocol>
|
||||
@@ -33,9 +31,6 @@ typedef NS_ENUM(NSUInteger, LoginType) {
|
||||
@property(nonatomic, strong) YYLabel *policyLabel;
|
||||
@property(nonatomic, strong) UIView *policyTips;
|
||||
|
||||
///谷歌登录配置
|
||||
@property (nonatomic,strong) GIDConfiguration *configuration;
|
||||
|
||||
@end
|
||||
|
||||
@implementation LoginViewController
|
||||
@@ -163,8 +158,6 @@ typedef NS_ENUM(NSUInteger, LoginType) {
|
||||
UIStackView *stackView = [[UIStackView alloc] initWithArrangedSubviews:@[
|
||||
idBUtton,
|
||||
[self entrcyButton:LoginType_Email tapSelector:@selector(didTapEntrcyButton:)],
|
||||
[self entrcyButton:LoginType_Google tapSelector:@selector(didTapEntrcyButton:)],
|
||||
[self entrcyButton:LoginType_Apple tapSelector:@selector(didTapEntrcyButton:)],
|
||||
]];
|
||||
stackView.axis = UILayoutConstraintAxisVertical;
|
||||
stackView.distribution = UIStackViewDistributionFillEqually;
|
||||
@@ -204,16 +197,6 @@ typedef NS_ENUM(NSUInteger, LoginType) {
|
||||
[icon setImage:kImage(@"login_page_mail")];
|
||||
}
|
||||
break;
|
||||
case LoginType_Google:{
|
||||
[button setTitle:YMLocalizedString(@"XPLoginViewController13") forState:UIControlStateNormal];
|
||||
[icon setImage:kImage(@"login_gmail")];
|
||||
}
|
||||
break;
|
||||
case LoginType_Apple:{
|
||||
[button setTitle:YMLocalizedString(@"XPLoginViewController12") forState:UIControlStateNormal];
|
||||
[icon setImage:kImage(@"mine_noble_center_apple")];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -258,29 +241,20 @@ typedef NS_ENUM(NSUInteger, LoginType) {
|
||||
return;
|
||||
}
|
||||
|
||||
LoginTypesViewController *vc = [[LoginTypesViewController alloc] init];
|
||||
|
||||
switch (sender.tag) {
|
||||
case LoginType_ID:{
|
||||
LoginTypesViewController *vc = [[LoginTypesViewController alloc] init];
|
||||
[self.navigationController pushViewController:vc animated:YES];
|
||||
case LoginType_ID:
|
||||
[vc updateLoginType:LoginDisplayType_id];
|
||||
}
|
||||
break;
|
||||
case LoginType_Email: {
|
||||
LoginTypesViewController *vc = [[LoginTypesViewController alloc] init];
|
||||
[self.navigationController pushViewController:vc animated:YES];
|
||||
case LoginType_Email:
|
||||
[vc updateLoginType:LoginDisplayType_email];
|
||||
}
|
||||
break;
|
||||
case LoginType_Google:
|
||||
[self.presenter thirdLoginByGoogleWithPresentingViewController:self
|
||||
configuration:self.configuration];
|
||||
break;
|
||||
case LoginType_Apple:
|
||||
[self.presenter thirdLoginWithType:ThirdLoginType_Apple];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
[self.navigationController pushViewController:vc animated:YES];
|
||||
}
|
||||
|
||||
- (void)didTapFeedback {
|
||||
@@ -412,18 +386,6 @@ typedef NS_ENUM(NSUInteger, LoginType) {
|
||||
return _policyTips;
|
||||
}
|
||||
|
||||
- (GIDConfiguration *)configuration{
|
||||
if (!_configuration){
|
||||
static dispatch_once_t onceToken;
|
||||
static NSString *decryptedNumber;
|
||||
dispatch_once(&onceToken, ^{
|
||||
decryptedNumber = [AESUtils aesDecrypt:@"ScLBu7ctIiyGCKPro3Jj6XMdsdCCpNT9L4wyjHEF+bguqubkXNSayFBGMKmoDwe1hjfAc958XSaBdMyEaFXLO38Bwq3xURYVNpgEM4b14zg="];
|
||||
});
|
||||
_configuration = [[GIDConfiguration alloc] initWithClientID:decryptedNumber];
|
||||
}
|
||||
return _configuration;
|
||||
}
|
||||
|
||||
- (UIImageView *)logoImageView {
|
||||
if (!_logoImageView) {
|
||||
_logoImageView = [[UIImageView alloc] initWithImage:kImage(@"login_page_logo")];
|
||||
|
||||
@@ -274,7 +274,7 @@
|
||||
- (UILabel *)titleLabel {
|
||||
if (!_titleLabel) {
|
||||
_titleLabel = [[UILabel alloc] init];
|
||||
_titleLabel.text = @"Welcome to MoliStar!";
|
||||
_titleLabel.text = @"Welcome to E-Party!";
|
||||
_titleLabel.font = kFontBold(28);
|
||||
_titleLabel.textColor = UIColorFromRGB(0x1F1B4F);
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
//
|
||||
// YMMineShareViewController.h
|
||||
// YUMI
|
||||
//
|
||||
// Created by YUMI on 2022/6/27.
|
||||
//
|
||||
|
||||
#import "BaseViewController.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
@class XPShareInfoModel;
|
||||
|
||||
typedef NS_ENUM(NSInteger, MineShareType) {
|
||||
///分享动态
|
||||
MineShareType_Monents = 1,
|
||||
};
|
||||
|
||||
@interface XPMineShareViewController : BaseViewController
|
||||
@property (nonatomic,strong) XPShareInfoModel *shareInfo;
|
||||
///分享的类型
|
||||
@property (nonatomic,assign) MineShareType shareType;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,232 +0,0 @@
|
||||
//
|
||||
// YMMineShareViewController.m
|
||||
// YUMI
|
||||
//
|
||||
// Created by YUMI on 2022/6/27.
|
||||
//
|
||||
|
||||
#import "XPMineShareViewController.h"
|
||||
///Third
|
||||
#import <Masonry/Masonry.h>
|
||||
#import <NIMSDK/NIMSDK.h>
|
||||
#import <JXCategoryView/JXCategoryView.h>
|
||||
#import <JXCategoryView/JXCategoryListContainerView.h>
|
||||
///Tool
|
||||
#import "DJDKMIMOMColor.h"
|
||||
#import "YUMIMacroUitls.h"
|
||||
#import "TTPopup.h"
|
||||
///Model
|
||||
#import "XPShareInfoModel.h"
|
||||
#import "FansInfoModel.h"
|
||||
#import "UserInfoModel.h"
|
||||
#import "AttachMentModel.h"
|
||||
#import "ContentShareMonentsModel.h"
|
||||
///View
|
||||
#import "SessionViewController.h"
|
||||
#import "SessionListViewController.h"
|
||||
#import "XPMineFriendViewController.h"
|
||||
#import "XPMineAttentionViewController.h"
|
||||
#import "XPMineFansViewController.h"
|
||||
@interface XPMineShareViewController ()<JXCategoryViewDelegate,JXCategoryListContainerViewDelegate, XPMineAttentionViewControllerDelegate, XPMineFansViewControllerDelegate, XPMineFriendViewControllerDelegate>
|
||||
///标题
|
||||
@property (nonatomic,strong) NSArray<NSString *> *titles;
|
||||
///滑块
|
||||
@property (nonatomic,strong) JXCategoryTitleView *titleView;
|
||||
@property (nonatomic, strong) JXCategoryListContainerView *listContainerView;
|
||||
///好友
|
||||
@property (nonatomic,strong) XPMineFriendViewController *friendVC;
|
||||
///关注
|
||||
@property (nonatomic,strong) XPMineAttentionViewController *attentionVC;
|
||||
///粉丝
|
||||
@property (nonatomic,strong) XPMineFansViewController *fansVC;
|
||||
///回话的id
|
||||
@property (nonatomic,copy) NSString *sessionId;
|
||||
@end
|
||||
|
||||
@implementation XPMineShareViewController
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
[self initSubViews];
|
||||
[self initSubViewConstraints];
|
||||
}
|
||||
|
||||
#pragma mark - Private Method
|
||||
- (void)initSubViews {
|
||||
self.title = YMLocalizedString(@"XPMineShareViewController0");
|
||||
[self.view addSubview:self.titleView];
|
||||
[self.view addSubview:self.listContainerView];
|
||||
}
|
||||
|
||||
- (void)initSubViewConstraints {
|
||||
[self.titleView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.trailing.top.mas_equalTo(self.view);
|
||||
make.height.mas_equalTo(50);
|
||||
}];
|
||||
|
||||
[self.listContainerView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.trailing.bottom.mas_equalTo(self.view);
|
||||
make.top.mas_equalTo(self.titleView.mas_bottom);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)sendCustomMessage:(AttachmentModel *)attachment {
|
||||
NIMMessage *message = [[NIMMessage alloc]init];
|
||||
NIMCustomObject *object = [[NIMCustomObject alloc] init];
|
||||
object.attachment = attachment;
|
||||
message.messageObject = object;
|
||||
NIMSessionType sessionType = NIMSessionTypeP2P;
|
||||
//构造会话
|
||||
NIMSession *session = [NIMSession session:self.sessionId type:sessionType];
|
||||
[[NIMSDK sharedSDK].chatManager sendMessage:message toSession:session error:nil];
|
||||
}
|
||||
|
||||
- (void)shareToUser:(NSString *)nick {
|
||||
NSString * title;
|
||||
AttachmentModel * attachment = [[AttachmentModel alloc] init];
|
||||
|
||||
// 添加类型安全检查
|
||||
if (![self.shareInfo isKindOfClass:[XPShareInfoModel class]]) {
|
||||
NSLog(@"警告:self.shareInfo不是XPShareInfoModel类型,而是%@类型", NSStringFromClass([self.shareInfo class]));
|
||||
return;
|
||||
}
|
||||
|
||||
switch (self.shareType) {
|
||||
case MineShareType_Monents:
|
||||
{
|
||||
title = [NSString stringWithFormat:YMLocalizedString(@"XPMineShareViewController1"), nick];
|
||||
attachment.first = CustomMessageType_Monents;
|
||||
attachment.second = Custom_Message_Sub_Monents_Share;
|
||||
ContentShareMonentsModel * shareInfo = [[ContentShareMonentsModel alloc] init];
|
||||
shareInfo.imageUrl = self.shareInfo.imageUrl;
|
||||
shareInfo.nick = self.shareInfo.nick;
|
||||
shareInfo.content = self.shareInfo.content;
|
||||
shareInfo.dynamicId= self.shareInfo.dynamicId;
|
||||
shareInfo.routerValue = self.shareInfo.dynamicId;
|
||||
shareInfo.routerType = 50;
|
||||
attachment.data = shareInfo.model2dictionary;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (title.length > 0) {
|
||||
[TTPopup alertWithMessage:title confirmHandler:^{
|
||||
[self sendCustomMessage:attachment];
|
||||
} cancelHandler:^{
|
||||
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - JXCategoryListContainerViewDelegate
|
||||
- (NSInteger)numberOfListsInlistContainerView:(JXCategoryListContainerView *)listContainerView {
|
||||
return self.titles.count;
|
||||
}
|
||||
|
||||
// 根据下标 index 返回对应遵守并实现 `JXCategoryListContentViewDelegate` 协议的列表实例
|
||||
- (id<JXCategoryListContentViewDelegate>)listContainerView:(JXCategoryListContainerView *)listContainerView initListForIndex:(NSInteger)index {
|
||||
if (index == 0) {
|
||||
return self.friendVC;
|
||||
} else if(index == 1) {
|
||||
return self.fansVC;
|
||||
} else {
|
||||
return self.attentionVC;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - XPMineAttentionViewControllerDelegate
|
||||
///点击了关注的某个人
|
||||
- (void)xPMineAttentionViewController:(XPMineAttentionViewController *)viewController didSelectItem:(FansInfoModel *)userInfo {
|
||||
self.sessionId = userInfo.uid;
|
||||
[self shareToUser:userInfo.nick];
|
||||
}
|
||||
|
||||
#pragma mark - XPMineFansViewControllerDelegate
|
||||
///点击了粉丝
|
||||
- (void)xPMineFansViewController:(XPMineFansViewController *)view didSelectItem:(FansInfoModel *)userInfo {
|
||||
self.sessionId = userInfo.uid;
|
||||
[self shareToUser:userInfo.nick];
|
||||
}
|
||||
|
||||
#pragma mark - XPMineFriendViewControllerDelegate
|
||||
///点击了好友
|
||||
- (void)xPMineFriendViewController:(XPMineFriendViewController *)viewController didSelectItem:(UserInfoModel *)userInfo {
|
||||
self.sessionId = [NSString stringWithFormat:@"%ld", userInfo.uid];
|
||||
[self shareToUser:userInfo.nick];
|
||||
}
|
||||
|
||||
#pragma mark - Getters And Setters
|
||||
|
||||
- (JXCategoryListContainerView *)listContainerView {
|
||||
if (!_listContainerView) {
|
||||
_listContainerView = [[JXCategoryListContainerView alloc] initWithType:JXCategoryListContainerType_ScrollView delegate:self];
|
||||
_listContainerView.listCellBackgroundColor = [UIColor clearColor];
|
||||
}
|
||||
return _listContainerView;
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)titles {
|
||||
if (!_titles) {
|
||||
_titles = @[YMLocalizedString(@"XPMonentsTooBarView3"),YMLocalizedString(@"XPMineContactViewController3"), YMLocalizedString(@"XPMineShareViewController4")];
|
||||
}
|
||||
return _titles;
|
||||
}
|
||||
|
||||
- (JXCategoryTitleView *)titleView {
|
||||
if (!_titleView) {
|
||||
_titleView = [[JXCategoryTitleView alloc] initWithFrame:CGRectZero];
|
||||
_titleView.backgroundColor =[UIColor clearColor];
|
||||
_titleView.titleColor = UIColorFromRGB(0x444444);
|
||||
_titleView.titleSelectedColor = [DJDKMIMOMColor mainTextColor];
|
||||
_titleView.titleFont = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold];
|
||||
_titleView.titleSelectedFont = [UIFont systemFontOfSize:18 weight:UIFontWeightHeavy];
|
||||
_titleView.titleLabelAnchorPointStyle = JXCategoryTitleLabelAnchorPointStyleCenter;
|
||||
_titleView.contentScrollViewClickTransitionAnimationEnabled = NO;
|
||||
_titleView.averageCellSpacingEnabled = NO;
|
||||
_titleView.defaultSelectedIndex = 0;
|
||||
_titleView.titles = self.titles;
|
||||
_titleView.delegate = self;
|
||||
_titleView.cellSpacing = 0;
|
||||
_titleView.cellWidth = (CGFloat)KScreenWidth/ 3.0;
|
||||
_titleView.listContainer = self.listContainerView;
|
||||
|
||||
JXCategoryIndicatorLineView *lineView = [[JXCategoryIndicatorLineView alloc] init];
|
||||
lineView.indicatorColor = [DJDKMIMOMColor appMainColor];
|
||||
lineView.indicatorWidth = 8.f;
|
||||
lineView.indicatorHeight = 4.f;
|
||||
lineView.indicatorCornerRadius = 2.f;
|
||||
_titleView.indicators = @[lineView];
|
||||
}
|
||||
return _titleView;
|
||||
}
|
||||
|
||||
- (XPMineAttentionViewController *)attentionVC {
|
||||
if (!_attentionVC) {
|
||||
_attentionVC = [[XPMineAttentionViewController alloc] init];
|
||||
_attentionVC.type = ContactUseingType_Share;
|
||||
_attentionVC.delegate = self;
|
||||
}
|
||||
return _attentionVC;
|
||||
}
|
||||
|
||||
- (XPMineFriendViewController *)friendVC {
|
||||
if (!_friendVC) {
|
||||
_friendVC = [[XPMineFriendViewController alloc] init];
|
||||
_friendVC.type = ContactUseingType_Share;
|
||||
_friendVC.delegate = self;
|
||||
}
|
||||
return _friendVC;
|
||||
}
|
||||
|
||||
- (XPMineFansViewController *)fansVC {
|
||||
if (!_fansVC) {
|
||||
_fansVC = [[XPMineFansViewController alloc] init];
|
||||
_fansVC.type = ContactUseingType_Share;
|
||||
_fansVC.delegate = self;
|
||||
}
|
||||
return _fansVC;
|
||||
}
|
||||
@end
|
||||
@@ -17,8 +17,7 @@
|
||||
/// @param pageSize 一页的个数
|
||||
/// @param types 类型 0,2
|
||||
+ (void)momentsRecommendList:(HttpRequestHelperCompletion)completion page:(NSString *)page pageSize:(NSString *)pageSize types:(NSString *)types {
|
||||
NSString * fang = [NSString stringFromBase64String:@"ZHluYW1pYy9zcXVhcmUvcmVjb21tZW5kRHluYW1pY3M="];///dynamic/square/recommendDynamics
|
||||
[self makeRequest:fang method:HttpRequestHelperMethodGET completion:completion, __FUNCTION__, page, pageSize, types, nil];
|
||||
[self makeRequest:@"dynamic/square/recommendDynamics" method:HttpRequestHelperMethodGET completion:completion, __FUNCTION__, page, pageSize, types, nil];
|
||||
}
|
||||
|
||||
/// 朋友圈动态最新列表
|
||||
@@ -27,8 +26,7 @@
|
||||
/// @param pageSize 一页的个数
|
||||
/// @param types 类型 0,2
|
||||
+ (void)momentsLatestList:(HttpRequestHelperCompletion)completion dynamicId:(NSString *)dynamicId pageSize:(NSString *)pageSize types:(NSString *)types {
|
||||
NSString * fang = [NSString stringFromBase64String:@"ZHluYW1pYy9zcXVhcmUvbGF0ZXN0RHluYW1pY3M="];///dynamic/square/latestDynamics
|
||||
[self makeRequest:fang method:HttpRequestHelperMethodGET completion:completion, __FUNCTION__, dynamicId, pageSize, types, nil];
|
||||
[self makeRequest:@"dynamic/square/latestDynamics" method:HttpRequestHelperMethodGET completion:completion, __FUNCTION__, dynamicId, pageSize, types, nil];
|
||||
}
|
||||
|
||||
/// 朋友圈动态关注列表
|
||||
@@ -93,8 +91,7 @@
|
||||
/// @param likedUid 点赞人的uid
|
||||
/// @param worldId 世界的id
|
||||
+ (void)momentsLike:(HttpRequestHelperCompletion)completion dynamicId:(NSString *)dynamicId uid:(NSString *)uid status:(NSString *)status likedUid:(NSString *)likedUid worldId:(NSString *)worldId {
|
||||
NSString * fang = [NSString stringFromBase64String:@"ZHluYW1pYy9saWtl"];///dynamic/like
|
||||
[self makeRequest:fang method:HttpRequestHelperMethodPOST completion:completion, __FUNCTION__, dynamicId, uid, status, likedUid, worldId, nil];
|
||||
[self makeRequest:@"dynamic/like" method:HttpRequestHelperMethodPOST completion:completion, __FUNCTION__, dynamicId, uid, status, likedUid, worldId, nil];
|
||||
}
|
||||
|
||||
/// 动态详情
|
||||
|
||||
@@ -77,6 +77,10 @@ typedef NS_ENUM(NSInteger, MonentsContentType) {
|
||||
@property (nonatomic, copy) NSString *worldName;
|
||||
///动态的id
|
||||
@property (nonatomic,copy) NSString *dynamicId;
|
||||
///审核状态(0=审核中,1=通过,2=拒绝)
|
||||
@property (nonatomic, assign) NSInteger status;
|
||||
///情绪颜色(本地标注,Hex格式如 #FF0000)
|
||||
@property (nonatomic, copy) NSString *emotionColor;
|
||||
///是否是折叠起来的
|
||||
@property (nonatomic,assign) BOOL isFold;
|
||||
///cell的高度
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user