Explorar el Código

feat: 增加手机号登录逻辑

陈文艺 hace 2 meses
padre
commit
6d183c3359
Se han modificado 76 ficheros con 2276 adiciones y 437 borrados
  1. 22 17
      Lanu.xcodeproj/project.pbxproj
  2. 78 0
      Lanu.xcodeproj/xcshareddata/xcschemes/Lanu_Debug.xcscheme
  3. 78 0
      Lanu.xcodeproj/xcshareddata/xcschemes/Lanu_Release.xcscheme
  4. 2 1
      Lanu.xcworkspace/xcshareddata/swiftpm/Package.resolved
  5. BIN
      Lanu/Assets.xcassets/Login/google.imageset/google@2x.png
  6. BIN
      Lanu/Assets.xcassets/Login/google.imageset/google@3x.png
  7. 2 2
      Lanu/Assets.xcassets/Login/ic_google.imageset/Contents.json
  8. BIN
      Lanu/Assets.xcassets/Login/ic_google.imageset/ic_google@2x.png
  9. BIN
      Lanu/Assets.xcassets/Login/ic_google.imageset/ic_google@3x.png
  10. 22 0
      Lanu/Assets.xcassets/Login/ic_login_phone.imageset/Contents.json
  11. BIN
      Lanu/Assets.xcassets/Login/ic_login_phone.imageset/ic_login_phone@2x.png
  12. BIN
      Lanu/Assets.xcassets/Login/ic_login_phone.imageset/ic_login_phone@3x.png
  13. 22 0
      Lanu/Assets.xcassets/Login/ic_login_phone_bg.imageset/Contents.json
  14. BIN
      Lanu/Assets.xcassets/Login/ic_login_phone_bg.imageset/ic_login_phone_bg@2x.png
  15. BIN
      Lanu/Assets.xcassets/Login/ic_login_phone_bg.imageset/ic_login_phone_bg@3x.png
  16. 1 1
      Lanu/Common/Extension/Date+Extension.swift
  17. 40 0
      Lanu/Common/Extension/String+Extension.swift
  18. 20 0
      Lanu/Common/Extension/UIButton+Theme.swift
  19. 7 0
      Lanu/Common/Extension/UIView+Extension.swift
  20. 1 1
      Lanu/Common/LNPhotosPicker.swift
  21. 25 0
      Lanu/Common/Theme/UIImage+Theme.swift
  22. 70 0
      Lanu/Common/Views/Base/LNFakeNaviBar.swift
  23. 3 1
      Lanu/Common/Views/Base/LNNavigationController.swift
  24. 3 0
      Lanu/Common/Views/Base/LNViewController.swift
  25. 12 1
      Lanu/Common/Views/ImagePreview/LNImagePreviewCell.swift
  26. 5 25
      Lanu/Common/Views/ImagePreview/LNImagePreviewController.swift
  27. 1 1
      Lanu/Common/Views/LNPopupView.swift
  28. 690 0
      Lanu/Localizable.xcstrings
  29. 93 12
      Lanu/Manager/Account/LNAccountManager.swift
  30. 25 2
      Lanu/Manager/Account/Network/LNHttpManager+Login.swift
  31. 12 1
      Lanu/Manager/Config/LNConfigManager.swift
  32. 15 0
      Lanu/Manager/Config/Network/LNConfigResponse.swift
  33. 5 0
      Lanu/Manager/Config/Network/LNHttpManager+Config.swift
  34. 1 1
      Lanu/Manager/Network/LNNetworkConfig.swift
  35. 5 5
      Lanu/Manager/Network/Upload/LNFileUploader.swift
  36. 1 1
      Lanu/Manager/Order/LNOrderManager.swift
  37. 1 1
      Lanu/Views/Game/MateList/LNGameMateListView.swift
  38. 1 1
      Lanu/Views/Game/Skill/LNSkillNaviBarView.swift
  39. 1 1
      Lanu/Views/IM/Chat/GameMate/LNIMChatGameMateSkillView.swift
  40. 4 1
      Lanu/Views/IM/Chat/InputMenu/LNIMChatInputMenuView.swift
  41. 1 1
      Lanu/Views/IM/Chat/LNIMChatTopMenuView.swift
  42. 44 7
      Lanu/Views/Login/LNLoginPanel.swift
  43. 0 166
      Lanu/Views/Login/LNLoginViewController.swift
  44. 140 0
      Lanu/Views/Login/Phone/LNCaptchaInputView.swift
  45. 272 0
      Lanu/Views/Login/Phone/LNCountrySelectPanel.swift
  46. 186 0
      Lanu/Views/Login/Phone/LNLoginCaptchaInputViewController.swift
  47. 295 0
      Lanu/Views/Login/Phone/LNLoginPhoneInputViewController.swift
  48. 3 28
      Lanu/Views/Login/Setup/LNBaseInfoSetupViewController.swift
  49. 3 25
      Lanu/Views/Login/Setup/LNGenderSetupViewController.swift
  50. 2 27
      Lanu/Views/Login/Setup/LNInterestSetupViewController.swift
  51. 1 1
      Lanu/Views/Order/Create/LNCreateOrderFromSkillListPanel.swift
  52. 1 1
      Lanu/Views/Order/Create/LNCreateOrderPanel.swift
  53. 1 1
      Lanu/Views/Order/Create/LNCreateOrderViewController.swift
  54. 1 1
      Lanu/Views/Order/Detail/LNOrderDetailViewController.swift
  55. 1 1
      Lanu/Views/Order/LNOrderCommentPanel.swift
  56. 1 1
      Lanu/Views/Order/OrderList/LNOrderListItemCell.swift
  57. 2 2
      Lanu/Views/Order/OrderQR/LNOrderGenerateQRCodePanel.swift
  58. 1 1
      Lanu/Views/Order/OrderQR/LNOrderSkillListPanel.swift
  59. 1 1
      Lanu/Views/Order/OrderRecords/LNOrderRecordCell.swift
  60. 1 1
      Lanu/Views/Profile/Edit/LNEditBioPanel.swift
  61. 5 0
      Lanu/Views/Profile/Edit/LNEditNickNamePanel.swift
  62. 15 12
      Lanu/Views/Profile/Edit/LNEditProfileViewController.swift
  63. 9 9
      Lanu/Views/Profile/Edit/LNEditVoicePanel.swift
  64. 1 1
      Lanu/Views/Profile/Mine/LNMineQRCodeShareView.swift
  65. 1 0
      Lanu/Views/Profile/Mine/LNMineUserInfoView.swift
  66. 1 1
      Lanu/Views/Profile/Post/LNPostShareViewController.swift
  67. 1 1
      Lanu/Views/Profile/Post/LNPostSkillSelectPanel.swift
  68. 1 1
      Lanu/Views/Profile/Profile/LNProfileNaviBarView.swift
  69. 1 1
      Lanu/Views/Profile/Profile/LNProfileScoreFloatingView.swift
  70. 1 1
      Lanu/Views/Profile/Profile/LNProfileStaringPanel.swift
  71. 4 31
      Lanu/Views/Search/LNUserSearchViewController.swift
  72. 1 1
      Lanu/Views/Settings/LNLanguageSettingPanel.swift
  73. 1 1
      Lanu/Views/Settings/LNSettingsViewController.swift
  74. 1 1
      Lanu/Views/Wallet/Coin/LNCoinViewController.swift
  75. 1 1
      Lanu/Views/Wallet/Diamond/LNDiamondViewController.swift
  76. 7 34
      Lanu/Views/Wallet/LNWalletViewController.swift

+ 22 - 17
Lanu.xcodeproj/project.pbxproj

@@ -8,13 +8,13 @@
 
 /* Begin PBXBuildFile section */
 		314B1B286681A79A6D153299 /* Pods_Gami.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C4E0C09312B6CA8A283AD62F /* Pods_Gami.framework */; };
-		FB31D8282F17353D0075F690 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = FB31D8272F17353D0075F690 /* SnapKit */; };
 		FB696C172EC96C0F00FAD639 /* MJRefresh in Frameworks */ = {isa = PBXBuildFile; productRef = FB696C162EC96C0F00FAD639 /* MJRefresh */; };
 		FB9CD1192EC1EEA10033B14B /* FirebaseCore in Frameworks */ = {isa = PBXBuildFile; productRef = FB9CD1182EC1EEA10033B14B /* FirebaseCore */; };
 		FB9CD11B2EC1EEA10033B14B /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = FB9CD11A2EC1EEA10033B14B /* FirebaseCrashlytics */; };
 		FB9CD11E2EC1EEF30033B14B /* GoogleSignIn in Frameworks */ = {isa = PBXBuildFile; productRef = FB9CD11D2EC1EEF30033B14B /* GoogleSignIn */; };
 		FB9EAE7B2F011ACD00E77B7C /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FB9EAE7A2F011ACD00E77B7C /* StoreKit.framework */; };
 		FB9FCD262EF25D6B00DDAAC9 /* SDWebImage in Frameworks */ = {isa = PBXBuildFile; productRef = FB9FCD252EF25D6B00DDAAC9 /* SDWebImage */; };
+		FBA06B692F18F7D300DDD745 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = FBA06B682F18F7D300DDD745 /* SnapKit */; };
 		FBECA9C42EC1C5250013A5E6 /* AutoCodable in Frameworks */ = {isa = PBXBuildFile; productRef = FBECA9C32EC1C5250013A5E6 /* AutoCodable */; };
 		FBECA9CA2EC1C8240013A5E6 /* CocoaLumberjackSwift in Frameworks */ = {isa = PBXBuildFile; productRef = FBECA9C92EC1C8240013A5E6 /* CocoaLumberjackSwift */; };
 		FBECAA1D2EC1C8860013A5E6 /* CocoaLumberjackSwiftLogBackend in Frameworks */ = {isa = PBXBuildFile; productRef = FBECAA1C2EC1C8860013A5E6 /* CocoaLumberjackSwiftLogBackend */; };
@@ -50,6 +50,7 @@
 				"Common/Extension/NSObject+Extension.swift",
 				"Common/Extension/String+Extension.swift",
 				"Common/Extension/TimeInterval+Extension.swift",
+				"Common/Extension/UIButton+Theme.swift",
 				"Common/Extension/UIColor+Extension.swift",
 				"Common/Extension/UIImage+Extension.swift",
 				"Common/Extension/UITableView+Extension.swift",
@@ -65,6 +66,7 @@
 				"Common/Theme/UIFont+Theme.swift",
 				"Common/Theme/UIImage+Theme.swift",
 				"Common/Theme/UIImageView+Theme.swift",
+				Common/Views/Base/LNFakeNaviBar.swift,
 				Common/Views/Base/LNNavigationController.swift,
 				Common/Views/Base/LNViewController.swift,
 				Common/Views/Gender/LNGenderView.swift,
@@ -203,8 +205,11 @@
 				Views/IM/Notify/Cell/LNIMOfficialMessageCell.swift,
 				Views/IM/Notify/LNIMOfficialMessageViewController.swift,
 				Views/Login/LNLoginPanel.swift,
-				Views/Login/LNLoginViewController.swift,
 				Views/Login/LNPrivacyTextView.swift,
+				Views/Login/Phone/LNCaptchaInputView.swift,
+				Views/Login/Phone/LNCountrySelectPanel.swift,
+				Views/Login/Phone/LNLoginCaptchaInputViewController.swift,
+				Views/Login/Phone/LNLoginPhoneInputViewController.swift,
 				Views/Login/Setup/LNBaseInfoSetupViewController.swift,
 				Views/Login/Setup/LNGenderSetupViewController.swift,
 				Views/Login/Setup/LNInterestSetupViewController.swift,
@@ -320,7 +325,7 @@
 				FB9CD11E2EC1EEF30033B14B /* GoogleSignIn in Frameworks */,
 				FB9EAE7B2F011ACD00E77B7C /* StoreKit.framework in Frameworks */,
 				314B1B286681A79A6D153299 /* Pods_Gami.framework in Frameworks */,
-				FB31D8282F17353D0075F690 /* SnapKit in Frameworks */,
+				FBA06B692F18F7D300DDD745 /* SnapKit in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -423,7 +428,7 @@
 				FB9CD11C2EC1EEF30033B14B /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */,
 				FB696C152EC96C0F00FAD639 /* XCRemoteSwiftPackageReference "MJRefresh" */,
 				FB9FCD242EF25D6B00DDAAC9 /* XCRemoteSwiftPackageReference "SDWebImage" */,
-				FB31D8242F1734DB0075F690 /* XCRemoteSwiftPackageReference "SnapKit" */,
+				FBA06B672F18F7D300DDD745 /* XCRemoteSwiftPackageReference "SnapKit" */,
 			);
 			preferredProjectObjectVersion = 77;
 			productRefGroup = FBFE13C12EBC39B000DCE6E9 /* Products */;
@@ -773,14 +778,6 @@
 /* End XCConfigurationList section */
 
 /* Begin XCRemoteSwiftPackageReference section */
-		FB31D8242F1734DB0075F690 /* XCRemoteSwiftPackageReference "SnapKit" */ = {
-			isa = XCRemoteSwiftPackageReference;
-			repositoryURL = "http://8.134.139.102:10880/chenwenyi/SnapKit.git";
-			requirement = {
-				kind = revision;
-				revision = 89a54dd7b8cd88f882a03fbd2553badbe9729940;
-			};
-		};
 		FB696C152EC96C0F00FAD639 /* XCRemoteSwiftPackageReference "MJRefresh" */ = {
 			isa = XCRemoteSwiftPackageReference;
 			repositoryURL = "http://8.134.139.102:10880/chenwenyi/MJRefresh.git";
@@ -813,6 +810,14 @@
 				minimumVersion = 5.21.3;
 			};
 		};
+		FBA06B672F18F7D300DDD745 /* XCRemoteSwiftPackageReference "SnapKit" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "http://8.134.139.102:10880/chenwenyi/SnapKit.git";
+			requirement = {
+				kind = upToNextMajorVersion;
+				minimumVersion = 5.7.2;
+			};
+		};
 		FBECA9C22EC1C5250013A5E6 /* XCRemoteSwiftPackageReference "AutoCodable" */ = {
 			isa = XCRemoteSwiftPackageReference;
 			repositoryURL = "http://8.134.139.102:10880/chenwenyi/AutoCodable.git";
@@ -840,11 +845,6 @@
 /* End XCRemoteSwiftPackageReference section */
 
 /* Begin XCSwiftPackageProductDependency section */
-		FB31D8272F17353D0075F690 /* SnapKit */ = {
-			isa = XCSwiftPackageProductDependency;
-			package = FB31D8242F1734DB0075F690 /* XCRemoteSwiftPackageReference "SnapKit" */;
-			productName = SnapKit;
-		};
 		FB696C162EC96C0F00FAD639 /* MJRefresh */ = {
 			isa = XCSwiftPackageProductDependency;
 			package = FB696C152EC96C0F00FAD639 /* XCRemoteSwiftPackageReference "MJRefresh" */;
@@ -870,6 +870,11 @@
 			package = FB9FCD242EF25D6B00DDAAC9 /* XCRemoteSwiftPackageReference "SDWebImage" */;
 			productName = SDWebImage;
 		};
+		FBA06B682F18F7D300DDD745 /* SnapKit */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = FBA06B672F18F7D300DDD745 /* XCRemoteSwiftPackageReference "SnapKit" */;
+			productName = SnapKit;
+		};
 		FBECA9C32EC1C5250013A5E6 /* AutoCodable */ = {
 			isa = XCSwiftPackageProductDependency;
 			package = FBECA9C22EC1C5250013A5E6 /* XCRemoteSwiftPackageReference "AutoCodable" */;

+ 78 - 0
Lanu.xcodeproj/xcshareddata/xcschemes/Lanu_Debug.xcscheme

@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "2600"
+   version = "1.7">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES"
+      buildArchitectures = "Automatic">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "FBFE13BF2EBC39B000DCE6E9"
+               BuildableName = "Gami.app"
+               BlueprintName = "Gami"
+               ReferencedContainer = "container:Lanu.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      shouldAutocreateTestPlan = "YES">
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "FBFE13BF2EBC39B000DCE6E9"
+            BuildableName = "Gami.app"
+            BlueprintName = "Gami"
+            ReferencedContainer = "container:Lanu.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Debug"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "FBFE13BF2EBC39B000DCE6E9"
+            BuildableName = "Gami.app"
+            BlueprintName = "Gami"
+            ReferencedContainer = "container:Lanu.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Debug"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 78 - 0
Lanu.xcodeproj/xcshareddata/xcschemes/Lanu_Release.xcscheme

@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "2600"
+   version = "1.7">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES"
+      buildArchitectures = "Automatic">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "FBFE13BF2EBC39B000DCE6E9"
+               BuildableName = "Gami.app"
+               BlueprintName = "Gami"
+               ReferencedContainer = "container:Lanu.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      shouldAutocreateTestPlan = "YES">
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Release"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "FBFE13BF2EBC39B000DCE6E9"
+            BuildableName = "Gami.app"
+            BlueprintName = "Gami"
+            ReferencedContainer = "container:Lanu.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "FBFE13BF2EBC39B000DCE6E9"
+            BuildableName = "Gami.app"
+            BlueprintName = "Gami"
+            ReferencedContainer = "container:Lanu.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Release">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 2 - 1
Lanu.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -186,7 +186,8 @@
       "kind" : "remoteSourceControl",
       "location" : "http://8.134.139.102:10880/chenwenyi/SnapKit.git",
       "state" : {
-        "revision" : "89a54dd7b8cd88f882a03fbd2553badbe9729940"
+        "revision" : "0456911aa90276968dbcee48f011629aef6f2f4e",
+        "version" : "5.7.2"
       }
     },
     {

BIN
Lanu/Assets.xcassets/Login/google.imageset/google@2x.png


BIN
Lanu/Assets.xcassets/Login/google.imageset/google@3x.png


+ 2 - 2
Lanu/Assets.xcassets/Login/google.imageset/Contents.json → Lanu/Assets.xcassets/Login/ic_google.imageset/Contents.json

@@ -5,12 +5,12 @@
       "scale" : "1x"
     },
     {
-      "filename" : "google@2x.png",
+      "filename" : "ic_google@2x.png",
       "idiom" : "universal",
       "scale" : "2x"
     },
     {
-      "filename" : "google@3x.png",
+      "filename" : "ic_google@3x.png",
       "idiom" : "universal",
       "scale" : "3x"
     }

BIN
Lanu/Assets.xcassets/Login/ic_google.imageset/ic_google@2x.png


BIN
Lanu/Assets.xcassets/Login/ic_google.imageset/ic_google@3x.png


+ 22 - 0
Lanu/Assets.xcassets/Login/ic_login_phone.imageset/Contents.json

@@ -0,0 +1,22 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "filename" : "ic_login_phone@2x.png",
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "filename" : "ic_login_phone@3x.png",
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
Lanu/Assets.xcassets/Login/ic_login_phone.imageset/ic_login_phone@2x.png


BIN
Lanu/Assets.xcassets/Login/ic_login_phone.imageset/ic_login_phone@3x.png


+ 22 - 0
Lanu/Assets.xcassets/Login/ic_login_phone_bg.imageset/Contents.json

@@ -0,0 +1,22 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "filename" : "ic_login_phone_bg@2x.png",
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "filename" : "ic_login_phone_bg@3x.png",
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
Lanu/Assets.xcassets/Login/ic_login_phone_bg.imageset/ic_login_phone_bg@2x.png


BIN
Lanu/Assets.xcassets/Login/ic_login_phone_bg.imageset/ic_login_phone_bg@3x.png


+ 1 - 1
Lanu/Common/Extension/Date+Extension.swift

@@ -65,7 +65,7 @@ extension Date {
     func formattedFullDateWithTime(_ separator: String = "/") -> String {
         let formatter = DateFormatter()
         formatter.locale = curLocal
-        formatter.dateFormat = "yyyy\(separator)MM\(separator)dd HH:mm"
+        formatter.dateFormat = "dd\(separator)MM\(separator)yyyy HH:mm"
         return formatter.string(from: self)
     }
     

+ 40 - 0
Lanu/Common/Extension/String+Extension.swift

@@ -90,3 +90,43 @@ extension String {
         return newPath
     }
 }
+
+// MARK: 拼音
+extension String {
+    var classificationFirstLetter: String {
+        // 1. 空字符串直接返回"#"
+        guard !trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
+            return "#"
+        }
+        
+        // 2. 统一预处理:转为可变字符串(支持CFStringTransform修改)
+        let mutableString = NSMutableString(string: self) as CFMutableString
+        
+        // 3. 步骤1:中文转带声调拼音(对英/印尼语无影响,可安全执行)
+        CFStringTransform(mutableString, nil, kCFStringTransformMandarinLatin, false)
+        
+        // 4. 步骤2:去除所有重音符号(关键:处理印尼语重音+中文拼音声调)
+        CFStringTransform(mutableString, nil, kCFStringTransformStripDiacritics, false)
+        
+        // 5. 步骤3:提取处理后的字符串首字符(忽略空白字符)
+        let processedString = mutableString as String
+        guard let firstChar = processedString
+            .trimmingCharacters(in: .whitespacesAndNewlines)
+            .first else {
+            return "#"
+        }
+        let firstLetter = String(firstChar).uppercased()
+        
+        // 6. 步骤4:判断是否为A-Z字母,统一返回结果
+        let isValidLetter = firstLetter.range(of: "^[A-Z]$", options: .regularExpression) != nil
+        return isValidLetter ? firstLetter : "#"
+    }
+    
+    var normalizedFullString: String {
+        guard !isEmpty else { return "" }
+        let mutableString = NSMutableString(string: self) as CFMutableString
+        CFStringTransform(mutableString, nil, kCFStringTransformMandarinLatin, false)
+        CFStringTransform(mutableString, nil, kCFStringTransformStripDiacritics, false)
+        return (mutableString as String).uppercased()
+    }
+}

+ 20 - 0
Lanu/Common/Extension/UIButton+Theme.swift

@@ -0,0 +1,20 @@
+//
+//  UIButton+Theme.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/1/15.
+//
+
+import Foundation
+
+
+extension UIButton {
+    static func backButton() -> UIButton {
+        let backButton = UIButton()
+        let buttonImage: UIImage? = .init(systemName: "chevron.backward")?.withRenderingMode(.alwaysTemplate)
+        backButton.setImage(buttonImage, for: .normal)
+        backButton.tintColor = .text_4
+        
+        return backButton
+    }
+}

+ 7 - 0
Lanu/Common/Extension/UIView+Extension.swift

@@ -61,6 +61,13 @@ extension UIView {
         return windowScene.statusBarManager?.statusBarFrame.height ?? 0
     }()
     
+    static var navigationBarHeight: CGFloat = {
+        guard let navigationController = UIView.appKeyWindow?.rootViewController as? UINavigationController else {
+            return 44
+        }
+        return navigationController.navigationBar.bounds.height
+    }()
+    
     var safeBottomInset: CGFloat {
         Self.appKeyWindow?.safeAreaInsets.bottom ?? 0
     }

+ 1 - 1
Lanu/Common/LNPhotosPicker.swift

@@ -39,7 +39,7 @@ extension LNBottomSheetMenu {
                 LNImagePicker.shared.selectPhoto(from: view, source: source, handler: handler)
             }
         }
-        panel.showIn()
+        panel.popup()
     }
 }
 

+ 25 - 0
Lanu/Common/Theme/UIImage+Theme.swift

@@ -7,7 +7,23 @@
 
 import Foundation
 
+private var customColorImage: [UIColor: UIImage] = [:]
+
 extension UIImage {
+    static func image(for color: UIColor) -> UIImage {
+        if let cache = customColorImage[color] {
+            return cache
+        }
+        let size = CGSize(width: 10, height: 10)
+        let renderer = UIGraphicsImageRenderer(size: size)
+        let newImage = renderer.image { context in
+            color.setFill()
+            context.fill(CGRect(origin: .zero, size: size))
+        }
+        customColorImage[color] = newImage
+        return newImage
+    }
+    
     static let primary_3: UIImage? = {
         let size = CGSize(width: 10, height: 10)
         let renderer = UIGraphicsImageRenderer(size: size)
@@ -17,6 +33,15 @@ extension UIImage {
         }
     }()
     
+    static let fill: UIImage? = {
+        let size = CGSize(width: 10, height: 10)
+        let renderer = UIGraphicsImageRenderer(size: size)
+        return renderer.image { context in
+            UIColor.fill.setFill()
+            context.fill(CGRect(origin: .zero, size: size))
+        }
+    }()
+    
     static let primary_6: UIImage? = {
         var view = UIView()
         view.frame = CGRect(x: 0, y: 0, width: 10, height: 10)

+ 70 - 0
Lanu/Common/Views/Base/LNFakeNaviBar.swift

@@ -0,0 +1,70 @@
+//
+//  LNFakeNaviBar.swift
+//  Lanu
+//
+//  Created by OneeChan on 2026/1/15.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNFakeNaviBar: UIView {
+    private(set) var backButton: UIButton?
+    let actionView = UIView()
+    
+    private var rightMenuView: UIView?
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        clipsToBounds = false
+        
+        addSubview(actionView)
+        actionView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.bottom.equalToSuperview()
+            make.height.equalTo(UIView.navigationBarHeight)
+            make.top.equalToSuperview().offset(UIView.statusBarHeight)
+        }
+    }
+    
+    @discardableResult
+    func showBackButton(_ handler: (() -> Void)? = nil) -> UIButton {
+        let backButton = UIButton.backButton()
+        backButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            if let handler {
+                handler()
+            } else {
+                navigationController?.popViewController(animated: true)
+            }
+        }), for: .touchUpInside)
+        actionView.addSubview(backButton)
+        backButton.snp.makeConstraints { make in
+            make.leading.equalToSuperview()
+            make.centerY.equalToSuperview()
+            make.width.height.equalTo(44)
+        }
+        self.backButton = backButton
+        
+        return backButton
+    }
+    
+    func setRightMenu(_ view: UIView) {
+        rightMenuView?.removeFromSuperview()
+        
+        actionView.addSubview(view)
+        view.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview().offset(-16)
+            make.width.height.equalTo(44)
+        }
+        rightMenuView = view
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}

+ 3 - 1
Lanu/Common/Views/Base/LNNavigationController.swift

@@ -11,7 +11,9 @@ import UIKit
 class LNNavigationController: UINavigationController {
     private let whileListVC: [UIViewController.Type] = [
         LNMainViewController.self,
-        LNWebViewController.self
+        LNWebViewController.self,
+        LNLoginPhoneInputViewController.self,
+        LNLoginCaptchaInputViewController.self
     ]
     
     override func viewDidLoad() {

+ 3 - 0
Lanu/Common/Views/Base/LNViewController.swift

@@ -57,6 +57,9 @@ class LNViewController: UIViewController {
             let appearance = UINavigationBarAppearance()
             appearance.backgroundColor = navigationBarColor // 导航栏背景色
             appearance.shadowColor = .clear // 去除底部阴影线
+            if navigationBarColor == .clear {
+                appearance.configureWithTransparentBackground()
+            }
             
             // 2. 应用外观设置(iOS 15+ 需要设置 scrollEdgeAppearance 和 standardAppearance)
             navBar.scrollEdgeAppearance = appearance // 滚动到顶部时的外观(如列表顶部)

+ 12 - 1
Lanu/Common/Views/ImagePreview/LNImagePreviewCell.swift

@@ -246,7 +246,18 @@ extension LNImagePreviewCell {
         
         contentView.onTap { [weak self] in
             guard let self else { return }
-            delegate?.onImagePreviewCellDragToDismiss(cell: self)
+            var frame = contentView.frame
+            frame.origin.y = bounds.height
+            
+            UIView.animate(withDuration: 0.25) { [weak self] in
+                guard let self else { return }
+                contentView.frame = frame
+                
+                superview?.backgroundColor = .clear
+            } completion: { [weak self] _ in
+                guard let self else { return }
+                delegate?.onImagePreviewCellDragToDismiss(cell: self)
+            }
         }
     }
 }

+ 5 - 25
Lanu/Common/Views/ImagePreview/LNImagePreviewController.swift

@@ -30,7 +30,7 @@ class LNImagePreviewController: LNViewController {
     
     private var collectionView: UICollectionView?
     
-    private let fakeBar = UIView()
+    private let fakeBar = LNFakeNaviBar()
     private let titleLabel = UILabel()
     
     func loadImages(urls: [String], targetIndex: Int) {
@@ -119,36 +119,16 @@ extension LNImagePreviewController {
     }
     
     private func buildFakeNavBar() -> UIView {
-        fakeBar.snp.makeConstraints { make in
-            make.height.equalTo((navigationController?.navigationBar.bounds.height ?? 44) + UIView.statusBarHeight)
-        }
-        
-        let barView = UIView()
-        fakeBar.addSubview(barView)
-        barView.snp.makeConstraints { make in
-            make.horizontalEdges.equalToSuperview()
-            make.bottom.equalToSuperview()
-            make.height.equalTo(navigationController?.navigationBar.bounds.height ?? 44)
-        }
-        
-        let closeButton = UIButton(type: .system)
-        closeButton.setImage(UIImage(systemName: "chevron.backward"), for: .normal)
-        closeButton.tintColor = .white
-        closeButton.addAction(UIAction(handler: { [weak self] _ in
+        fakeBar.showBackButton { [weak self] in
             guard let self else { return }
             dismiss(animated: true)
-        }), for: .touchUpInside)
-        closeButton.translatesAutoresizingMaskIntoConstraints = false
-        barView.addSubview(closeButton)
-        closeButton.snp.makeConstraints { make in
-            make.leading.equalToSuperview().offset(16)
-            make.centerY.equalToSuperview()
-            make.width.height.equalTo(24)
         }
+        fakeBar.backButton?.setImage(.init(systemName: "chevron.backward")?.withRenderingMode(.alwaysTemplate), for: .normal)
+        fakeBar.backButton?.tintColor = .fill
         
         titleLabel.font = .body_l
         titleLabel.textColor = .text_1
-        barView.addSubview(titleLabel)
+        fakeBar.actionView.addSubview(titleLabel)
         titleLabel.snp.makeConstraints { make in
             make.center.equalToSuperview()
         }

+ 1 - 1
Lanu/Common/Views/LNPopupView.swift

@@ -27,7 +27,7 @@ class LNPopupView: UIView {
         setupViews()
     }
     
-    func showIn(_ targetView: UIView? = nil) {
+    func popup(_ targetView: UIView? = nil) {
         if let window = targetView as? UIWindow {
             window.addSubview(self)
         } else if let view = targetView?.viewController?.view {

+ 690 - 0
Lanu/Localizable.xcstrings

@@ -6555,6 +6555,696 @@
           }
         }
       }
+    },
+    "B00001" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Introduce yourself with your voice"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Kenalalkan dengan suara"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "用声音介绍你自己"
+          }
+        }
+      }
+    },
+    "B00002" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Displaying"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Sedang Ditampilkan"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "展示中"
+          }
+        }
+      }
+    },
+    "B00003" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Under Review"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Sedang Diverifikasi"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "审核中"
+          }
+        }
+      }
+    },
+    "B00004" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Talent Show"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Pameran Bakat"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "才艺展示"
+          }
+        }
+      }
+    },
+    "B00005" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Voice"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Suara"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "声音"
+          }
+        }
+      }
+    },
+    "B00006" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Sound Settings"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Pengaturan Suara"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "设置声音"
+          }
+        }
+      }
+    },
+    "B00007" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Tap to Start Recording"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Klik untuk Memulai Perekaman"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "点击开始录音"
+          }
+        }
+      }
+    },
+    "B00008" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Tap to Stop Recording"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Klik Akhiri Rekam"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "点击结束录音"
+          }
+        }
+      }
+    },
+    "B00009" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Recording time must be at least 3 seconds."
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Waktu rekam minimal 3 detik."
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "录音时间不能低于3秒哦"
+          }
+        }
+      }
+    },
+    "B00010" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Re-record"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Ulang"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "重录"
+          }
+        }
+      }
+    },
+    "B00011" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Tap to Preview"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Klik Preview"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "点击试听"
+          }
+        }
+      }
+    },
+    "B00012" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Audio Under Review"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Audio Sedang Diverifikasi"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "音频审核中"
+          }
+        }
+      }
+    },
+    "B00013" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Please wait patiently"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Silakan tunggu dengan sabar"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "请耐心等候"
+          }
+        }
+      }
+    },
+    "B00014" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "File does not exist"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Berkas tidak ditemukan"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "文件不存在"
+          }
+        }
+      }
+    },
+    "B00015" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Upload Cancelled"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Unggahan Dibatalkan"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "上传已取消"
+          }
+        }
+      }
+    },
+    "B00016" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Upload Failed"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Unggahan Gagal"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "上传失败"
+          }
+        }
+      }
+    },
+    "B00017" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "File is uploading. Do not upload again."
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Berkas sedang diunggah, jangan unggah ulang."
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "文件正在上传中,请勿重复上传"
+          }
+        }
+      }
+    },
+    "B00018" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Are you sure you want to abandon the recording?"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Apakah kamu yakin ingin meninggalkan perekaman?"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "确定放弃录制吗?"
+          }
+        }
+      }
+    },
+    "B00019" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Mobile Login"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Masuk Telepon"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "手机登录"
+          }
+        }
+      }
+    },
+    "B00020" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Log in\nStart your exclusive gaming journey"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Masuk\nMulai perjalanan permainan eksklusifmu"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "登录后\n开启你的专属游戏之旅"
+          }
+        }
+      }
+    },
+    "B00021" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Enter Phone Number"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Nomor ponsel"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "手机号码"
+          }
+        }
+      }
+    },
+    "B00022" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "To use this feature normally, please go to Settings > Privacy > Microphone and enable Gami access."
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Untuk pakai fitur ini, buka Pengaturan > Privasi > Mikrofon, aktifkan izin Gami."
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "如想正常使用, 请打开设置-隐私-麦克风, 打开Gami使用权限"
+          }
+        }
+      }
+    },
+    "B00023" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Verification Code"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Dapatkan Kode Verif"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "获取验证码"
+          }
+        }
+      }
+    },
+    "B00024" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Select Country or Region"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Pilih Negara atau Wilayah"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "选择国家或地区"
+          }
+        }
+      }
+    },
+    "B00025" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Enter Verification Code"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Masukkan Kode Verif"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "输入验证码"
+          }
+        }
+      }
+    },
+    "B00026" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Verification code sent to"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Kode verif dikirim ke"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "验证码已发送至"
+          }
+        }
+      }
+    },
+    "B00027" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Resend Verification Code"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Kirim Ulang Kode Verifikasi"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "重新发送验证码"
+          }
+        }
+      }
+    },
+    "B00028" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Haven't received the sign in code?"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Belum menerima kode verifikasi login?"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "未收到登录验证码?"
+          }
+        }
+      }
+    },
+    "B00029" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Resend"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Kirim Ulang"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "重新发送"
+          }
+        }
+      }
+    },
+    "B00030" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Verification code sent"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Kode terkirim"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "验证码已发送"
+          }
+        }
+      }
     }
   },
   "version" : "1.1"

+ 93 - 12
Lanu/Manager/Account/LNAccountManager.swift

@@ -12,10 +12,14 @@ import GoogleSignIn
 protocol LNAccountManagerNotify {
     func onUserLogin()
     func onUserLogout()
+    
+    func onCaptchaCoolDownChanged(time: Int)
 }
 extension LNAccountManagerNotify {
     func onUserLogin() {}
     func onUserLogout() {}
+    
+    func onCaptchaCoolDownChanged(time: Int) { }
 }
 
 extension String {
@@ -46,6 +50,13 @@ class LNAccountManager {
         !token.isEmpty && !uid.isEmpty
     }
     
+    private let captchaCoolDown = 60
+    private var captchaRemain = 0
+    private var captchaTimer: Timer?
+    var canSendCaptcha: Bool {
+        captchaRemain == 0
+    }
+    
     private init() {
         let clientID = if LNAppConfig.shared.curEnv == .test {
             "981655295954-noc65ii1gfgpq3mrc0r75t7gq66v57bj.apps.googleusercontent.com"
@@ -55,8 +66,10 @@ class LNAccountManager {
         
         GIDSignIn.sharedInstance.configuration = GIDConfiguration(clientID: clientID)
     }
-    
-    func loginByToken(completion: ((Bool) -> Void)? = nil) {
+}
+ 
+extension LNAccountManager {
+    func loginByToken(handler: ((Bool) -> Void)? = nil) {
         LNHttpManager.shared.refreshToken { [weak self] res, err in
             guard let self else { return }
             guard err == nil, let res else {
@@ -64,45 +77,64 @@ class LNAccountManager {
                     self.clean()
                 }
                 showToast(err?.errorDesc)
-                completion?(false)
+                handler?(false)
                 return
             }
             self.token = res.token
-            completion?(true)
+            handler?(true)
             
             self.notifyUserLogin()
         }
     }
     
-    func loginByGoogle(data: String, completion: ((Bool) -> Void)? = nil) {
+    func loginByGoogle(data: String, handler: ((Bool) -> Void)? = nil) {
         LNHttpManager.shared.loginByGoogle(token: data) { [weak self] response, err in
             guard let self else { return }
             guard err == nil, let response else {
                 showToast(err?.errorDesc)
-                completion?(false)
+                handler?(false)
                 self.clean()
                 return
             }
             self.token = response.token
             self.uid = response.userProfile.userNo
-            completion?(true)
+            handler?(true)
             
             self.notifyUserLogin()
         }
     }
     
-    func loginByApple(data: String, completion: ((Bool) -> Void)? = nil) {
+    func loginByApple(data: String, handler: ((Bool) -> Void)? = nil) {
         LNHttpManager.shared.loginByApple(token: data) { [weak self] response, err in
             guard let self else { return }
             guard err == nil, let response else {
                 showToast(err?.errorDesc)
-                completion?(false)
+                handler?(false)
+                self.clean()
+                return
+            }
+            self.token = response.token
+            self.uid = response.userProfile.userNo
+            handler?(true)
+            
+            self.notifyUserLogin()
+        }
+    }
+    
+    func loginByPhone(code: String, num: String, captcha: String,
+                      handler: ((Bool) -> Void)? = nil) {
+        LNHttpManager.shared.loginByPhone(code: code, num: num, captcha: captcha)
+        { [weak self] response, err in
+            guard let self else { return }
+            guard err == nil, let response else {
+                showToast(err?.errorDesc)
+                handler?(false)
                 self.clean()
                 return
             }
             self.token = response.token
             self.uid = response.userProfile.userNo
-            completion?(true)
+            handler?(true)
             
             self.notifyUserLogin()
         }
@@ -138,6 +170,49 @@ class LNAccountManager {
 #endif
 }
 
+extension LNAccountManager {
+    func getLoginCaptcha(code: String, phone: String,
+                         queue: DispatchQueue = .main,
+                         handler: @escaping (Bool) -> Void) {
+        LNHttpManager.shared.getLoginCaptcha(code: code, phone: phone) { [weak self] err in
+            queue.asyncIfNotGlobal {
+                handler(err == nil)
+            }
+            guard let self else { return }
+            if let err {
+                showToast(err.errorDesc)
+            } else {
+                captchaRemain = captchaCoolDown
+                notifyCaptchaTime(time: captchaRemain)
+                startCaptchaTimer()
+            }
+        }
+    }
+    
+    private func startCaptchaTimer() {
+        DispatchQueue.main.async { [weak self] in
+            guard let self else { return }
+            stopCaptchaTimer()
+            
+            let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
+                guard let self else { return }
+                captchaRemain -= 1
+                notifyCaptchaTime(time: captchaRemain)
+                if captchaRemain == 0 {
+                    stopCaptchaTimer()
+                }
+            }
+            RunLoop.main.add(timer, forMode: .common)
+            captchaTimer = timer
+        }
+    }
+    
+    private func stopCaptchaTimer() {
+        captchaTimer?.invalidate()
+        captchaTimer = nil
+    }
+}
+
 extension LNAccountManager {
     func clean() {
         let wasLogin = !token.isEmpty
@@ -151,11 +226,17 @@ extension LNAccountManager {
 }
 
 extension LNAccountManager {
-    func notifyUserLogin() {
+    private func notifyUserLogin() {
         LNEventDeliver.notifyEvent { ($0 as? LNAccountManagerNotify)?.onUserLogin() }
     }
 
-    func notifyUserLogout() {
+    private func notifyUserLogout() {
         LNEventDeliver.notifyEvent { ($0 as? LNAccountManagerNotify)?.onUserLogout() }
     }
+    
+    private func notifyCaptchaTime(time: Int) {
+        LNEventDeliver.notifyEvent {
+            ($0 as? LNAccountManagerNotify)?.onCaptchaCoolDownChanged(time: time)
+        }
+    }
 }

+ 25 - 2
Lanu/Manager/Account/Network/LNHttpManager+Login.swift

@@ -11,20 +11,34 @@ import Foundation
 private let kNetPath_Login_Google = "/user/login/google/enter"
 private let kNetPath_Login_Email = "/user/login/email/enter"
 private let kNetPath_Login_Apple = "/user/login/apple/enter"
+private let kNetPath_Login_Phone = "/user/login/mobile/enter"
 
 private let kNetPath_Login_Refresh = "/user/renewalToken"
 
 private let kNetPath_Logout = "/user/logout"
 
+private let kNetPath_Login_Captcha = "/user/login/mobile/sendCode"
+
 extension LNHttpManager {
-    func loginByGoogle(token: String, completion: @escaping (LNLoginResponse?, LNHttpError?) -> Void) {
-        post(path: kNetPath_Login_Google, params: ["data": token], completion: completion)
+    func loginByPhone(code: String, num: String, captcha: String,
+                      completion: @escaping (LNLoginResponse?, LNHttpError?) -> Void) {
+        post(path: kNetPath_Login_Phone, params: [
+            "mobile": [
+                "code": code,
+                "num": num
+            ],
+            "code": captcha
+        ], completion: completion)
     }
     
     func loginByApple(token: String, completion: @escaping (LNLoginResponse?, LNHttpError?) -> Void) {
         post(path: kNetPath_Login_Apple, params: ["data": token], completion: completion)
     }
     
+    func loginByGoogle(token: String, completion: @escaping (LNLoginResponse?, LNHttpError?) -> Void) {
+        post(path: kNetPath_Login_Google, params: ["data": token], completion: completion)
+    }
+    
 #if DEBUG
     func loginByEmail(email: String, completion: @escaping (LNLoginResponse?, LNHttpError?) -> Void) {
         post(path: kNetPath_Login_Email, params: ["email": email], completion: completion)
@@ -39,3 +53,12 @@ extension LNHttpManager {
         post(path: kNetPath_Logout, completion: completion)
     }
 }
+
+extension LNHttpManager {
+    func getLoginCaptcha(code: String, phone: String, completion: @escaping (LNHttpError?) -> Void) {
+        post(path: kNetPath_Login_Captcha, params: [
+            "code": code,
+            "num": phone
+        ], completion: completion)
+    }
+}

+ 12 - 1
Lanu/Manager/Config/LNConfigManager.swift

@@ -17,7 +17,7 @@ class LNConfigManager {
     
     func reloadCommonConfig() {
         LNHttpManager.shared.getCommonConfig { [weak self] res, err in
-            guard let self else { return }
+            guard self != nil else { return }
             guard let res else { return }
             
             let config = LNCurrencyExchangeConfig()
@@ -29,6 +29,17 @@ class LNConfigManager {
             LNGameMateManager.shared.availableArea = res.commonAreaConsts
         }
     }
+    
+    func getCountryCodeList(queue: DispatchQueue = .main, handler: @escaping ([LNCountryCodeVO]?) -> Void) {
+        LNHttpManager.shared.getCountryCodeList { res, err in
+            queue.asyncIfNotGlobal {
+                handler(res?.list)
+            }
+            if let err {
+                showToast(err.errorDesc)
+            }
+        }
+    }
 }
 
 

+ 15 - 0
Lanu/Manager/Config/Network/LNConfigResponse.swift

@@ -28,3 +28,18 @@ class LNConfigResponse: Decodable {
     var commonCoinExchangeConsts: [LNCurrenyExchangeConstsVO] = []
     var commonAreaConsts: [LNCommonAreaConsts] = []
 }
+
+@AutoCodable
+class LNCountryCodeVO: Decodable {
+    var name: String = ""
+    var icon: String = ""
+    var code: String = ""
+    
+    init() { }
+}
+
+
+@AutoCodable
+class LNCountryCodeListResponse: Decodable {
+    var list: [LNCountryCodeVO] = []
+}

+ 5 - 0
Lanu/Manager/Config/Network/LNHttpManager+Config.swift

@@ -9,10 +9,15 @@ import Foundation
 
 
 private let kNetPath_Config_Common = "/base/consts/config"
+private let kNetPath_Config_Country = "/base/country/mobile"
 
 
 extension LNHttpManager {
     func getCommonConfig(completion: @escaping (LNConfigResponse?, LNHttpError?) -> Void) {
         post(path: kNetPath_Config_Common, completion: completion)
     }
+    
+    func getCountryCodeList(completion: @escaping (LNCountryCodeListResponse?, LNHttpError?) -> Void) {
+        post(path: kNetPath_Config_Country, completion: completion)
+    }
 }

+ 1 - 1
Lanu/Manager/Network/LNNetworkConfig.swift

@@ -24,6 +24,6 @@ extension LNBottomSheetMenu {
             
             exit(0)
         }
-        panel.showIn()
+        panel.popup()
     }
 }

+ 5 - 5
Lanu/Manager/Network/Upload/LNFileUploader.swift

@@ -52,12 +52,12 @@ class LNFileUploader: NSObject {
         let taskId = "\(type.rawValue)-\(fileURL.absoluteString.md5)"
         
         guard uploadTasks[taskId] == nil else {
-            completionHandler?(nil, .init(key: "A00302"))
+            completionHandler?(nil, .init(key: "B00017"))
             return nil
         }
         
         guard FileManager.default.fileExists(atPath: fileURL.path) else {
-            completionHandler?(nil, .init(key: "A00299"))
+            completionHandler?(nil, .init(key: "B00014"))
             return nil
         }
         
@@ -104,7 +104,7 @@ class LNFileUploader: NSObject {
         let taskId = "\(type.rawValue)-\(fileData.md5)"
         
         guard uploadTasks[taskId] == nil else {
-            completionHandler?(nil, .init(key: "A00302"))
+            completionHandler?(nil, .init(key: "B00017"))
             return nil
         }
         
@@ -180,7 +180,7 @@ extension LNFileUploader: URLSessionTaskDelegate, URLSessionDataDelegate {
         DispatchQueue.main.async {
             if let error = error {
                 if (error as NSError).code == NSURLErrorCancelled {
-                    completionHandler(nil, .init(key: "A00300"))
+                    completionHandler(nil, .init(key: "B00015"))
                 } else {
                     completionHandler(nil, error.localizedDescription)
                 }
@@ -189,7 +189,7 @@ extension LNFileUploader: URLSessionTaskDelegate, URLSessionDataDelegate {
                     completionHandler(uploadTask.fileUrl, nil)
                 } else {
                     let statusCode = (task.response as? HTTPURLResponse)?.statusCode ?? -4
-                    completionHandler(nil, .init(key: "A00301"))
+                    completionHandler(nil, .init(key: "B00016"))
                 }
             }
         }

+ 1 - 1
Lanu/Manager/Order/LNOrderManager.swift

@@ -106,7 +106,7 @@ extension LNOrderManager {
                     if code == LNOrderErrorCode.NotEnoughMoney.rawValue {
                         let panel = LNPurchasePanel()
                         panel.update(.coin)
-                        panel.showIn()
+                        panel.popup()
                         showToast(err)
                     } else {
                         showToast(err)

+ 1 - 1
Lanu/Views/Game/MateList/LNGameMateListView.swift

@@ -119,7 +119,7 @@ extension LNGameMateListView: LNGameMateListMenuViewDelegate {
     func menuViewDidClickFind(view: LNGameMateListMenuView) {
         let filterPanel = LNGameFilterPanel()
         filterPanel.delegate = self
-        filterPanel.showIn()
+        filterPanel.popup()
     }
 }
 

+ 1 - 1
Lanu/Views/Game/Skill/LNSkillNaviBarView.swift

@@ -89,7 +89,7 @@ extension LNSkillNaviBarView {
                 }
             }
         }
-        panel.showIn()
+        panel.popup()
     }
     
     private func setupViews() {

+ 1 - 1
Lanu/Views/IM/Chat/GameMate/LNIMChatGameMateSkillView.swift

@@ -72,7 +72,7 @@ extension LNIMChatGameMateSkillView: LNIMChatGameMateSkillCellDelegate {
         let panel = LNCreateOrderPanel()
         panel.update(skill, user: userInfo)
         panel.editable = true
-        panel.showIn()
+        panel.popup()
     }
 }
 

+ 4 - 1
Lanu/Views/IM/Chat/InputMenu/LNIMChatInputMenuView.swift

@@ -44,7 +44,10 @@ extension LNIMChatInputMenuView: LNIMChatTextInputViewDelegate {
         
         LNVoiceRecorder.shared.requestMicrophonePermission { [weak self] granted in
             guard let self else { return }
-            guard granted else { return }
+            guard granted else {
+                showToast(.init(key: "B00022"))
+                return
+            }
             
             textInput.snp.remakeConstraints { make in
                 make.horizontalEdges.equalToSuperview()

+ 1 - 1
Lanu/Views/IM/Chat/LNIMChatTopMenuView.swift

@@ -189,7 +189,7 @@ extension LNIMChatTopMenuView {
             guard let self else { return }
             let panel = LNIMChatUserMenuView()
             panel.viewModel = viewModel
-            panel.showIn(self)
+            panel.popup(self)
         }), for: .touchUpInside)
         stackView.addArrangedSubview(more)
     }

+ 44 - 7
Lanu/Views/Login/LNLoginPanel.swift

@@ -30,7 +30,7 @@ class LNLoginPanel: LNPopupView {
         guard curLoginPanel == nil else { return }
             
         let panel = LNLoginPanel()
-        panel.showIn(container)
+        panel.popup(container)
         curLoginPanel = panel
     }
     
@@ -94,6 +94,8 @@ extension LNLoginPanel {
         let email = buildEmail()
         stackView.addArrangedSubview(email)
 #endif
+        let phone = buildPhoneLogin()
+        stackView.addArrangedSubview(phone)
         
         let apple = buildAppleLogin()
         stackView.addArrangedSubview(apple)
@@ -121,10 +123,45 @@ extension LNLoginPanel {
         }
     }
     
+    private func buildPhoneLogin() -> UIView {
+        let button = UIButton()
+        button.setBackgroundImage(.primary_3, for: .normal)
+        button.layer.cornerRadius = 24
+        button.clipsToBounds = true
+        button.snp.makeConstraints { make in
+            make.height.equalTo(48)
+        }
+        
+        let ic = UIImageView()
+        ic.image = .icLoginPhone
+        button.addSubview(ic)
+        ic.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalToSuperview().offset(12)
+        }
+        
+        let title = UILabel()
+        title.font = .heading_h3
+        title.textColor = .text_1
+        title.text = .init(key: "B00019")
+        button.addSubview(title)
+        title.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+        }
+        
+        button.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            pushToLoginPhone()
+        }), for: .touchUpInside)
+        
+        return button
+    }
+    
     private func buildAppleLogin() -> UIView {
         let button = UIButton()
-        button.backgroundColor = .init(hex: "#0B0B0A")
+        button.setBackgroundImage(UIImage.image(for: .init(hex: "#0B0B0A")), for: .normal)
         button.layer.cornerRadius = 24
+        button.clipsToBounds = true
         button.snp.makeConstraints { make in
             make.height.equalTo(48)
         }
@@ -162,19 +199,19 @@ extension LNLoginPanel {
     
     private func buildGoogleLogin() -> UIView {
         let button = UIButton()
-        button.backgroundColor = .fill
+        button.setBackgroundImage(.fill, for: .normal)
         button.layer.cornerRadius = 24
+        button.clipsToBounds = true
         button.snp.makeConstraints { make in
             make.height.equalTo(48)
         }
         
         let ic = UIImageView()
-        ic.image = .google
+        ic.image = .icGoogle
         button.addSubview(ic)
         ic.snp.makeConstraints { make in
             make.centerY.equalToSuperview()
             make.leading.equalToSuperview().offset(22)
-            make.width.height.equalTo(20)
         }
         
         let title = UILabel()
@@ -214,7 +251,7 @@ extension LNLoginPanel {
         attrStr.addAttributes([
             .link: String.privacyUrl,
             .underlineStyle: NSUnderlineStyle.single.rawValue,
-            .underlineColor: UIColor.text_2
+            .underlineColor: UIColor.text_2,
         ], range: privacyRange)
         
         let privacyView = LNPrivacyTextView()
@@ -291,7 +328,7 @@ struct LNLoginPanelPreview: UIViewRepresentable {
         container.backgroundColor = .lightGray
         
         let view = LNLoginPanel()
-        view.showIn(container)
+        view.popup(container)
         
         return container
     }

+ 0 - 166
Lanu/Views/Login/LNLoginViewController.swift

@@ -1,166 +0,0 @@
-//
-//  LNLoginViewController.swift
-//  Lanu
-//
-//  Created by OneeChan on 2025/11/11.
-//
-
-import Foundation
-import UIKit
-import SnapKit
-import GoogleSignIn
-
-class LNLoginViewController: LNViewController {
-    override func viewDidLoad() {
-        super.viewDidLoad()
-        
-        setupViews()
-    }
-}
-
-extension LNLoginViewController {
-    private func setupViews() {
-        showNavigationBar = false
-        
-        let bg = UIImageView()
-        bg.image = .icLoginBg
-        view.addSubview(bg)
-        bg.snp.makeConstraints { make in
-            make.edges.equalToSuperview()
-        }
-        
-#if DEBUG
-        let email = buildEmail()
-        view.addSubview(email)
-        email.snp.makeConstraints { make in
-            make.centerX.equalToSuperview()
-            make.centerY.equalToSuperview().multipliedBy(1.3)
-        }
-#endif
-        
-        let google = buildGoogleLogin()
-        view.addSubview(google)
-        google.snp.makeConstraints { make in
-            make.horizontalEdges.equalToSuperview().inset(38)
-            make.centerY.equalToSuperview().multipliedBy(1.5)
-        }
-        
-        let privacy = buildPrivacy()
-        view.addSubview(privacy)
-        privacy.snp.makeConstraints { make in
-            make.centerX.equalToSuperview()
-            make.width.equalToSuperview().offset(-50)
-            make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom).offset(-40)
-        }
-    }
-    
-    private func buildGoogleLogin() -> UIView {
-        let button = UIButton()
-        button.backgroundColor = .fill
-        button.layer.cornerRadius = 24
-        button.snp.makeConstraints { make in
-            make.height.equalTo(48)
-        }
-        
-        let ic = UIImageView()
-        ic.image = .google
-        button.addSubview(ic)
-        ic.snp.makeConstraints { make in
-            make.centerY.equalToSuperview()
-            make.leading.equalToSuperview().offset(22)
-            make.width.height.equalTo(20)
-        }
-        
-        let title = UILabel()
-        title.font = .heading_h3
-        title.textColor = .text_5
-        title.text = .init(key: "A00115")
-        button.addSubview(title)
-        title.snp.makeConstraints { make in
-            make.center.equalToSuperview()
-        }
-        
-        button.addAction(UIAction(handler: { [weak self] _ in
-            guard let self else { return }
-            GIDSignIn.sharedInstance.signIn(withPresenting: self) { [weak self] result, err in
-                guard self != nil else { return }
-                guard err == nil, let result else { return }
-                guard let token = result.user.idToken?.tokenString else { return }
-                LNAccountManager.shared.loginByGoogle(data: token)
-            }
-        }), for: .touchUpInside)
-        
-        return button
-    }
-    
-    private func buildPrivacy() -> UIView {
-        let text: String = .init(key: "A00116")
-        let attrStr = NSMutableAttributedString(string: text)
-        let agreementRange = (text as NSString).range(of: .init(key: "A00117"))
-        attrStr.addAttribute(.link, value: String.serviceUrl, range: agreementRange)
-        let privacyRange = (text as NSString).range(of: .init(key: "A00118"))
-        attrStr.addAttribute(.link, value: String.privacyUrl, range: privacyRange)
-        
-        let privacyView = LNPrivacyTextView()
-        privacyView.attributedText = attrStr
-        privacyView.textAlignment = .center
-        privacyView.backgroundColor = .clear
-        privacyView.font = .systemFont(ofSize: 13.4)
-        privacyView.textColor = .text_4
-        privacyView.linkTextAttributes = [.foregroundColor: UIColor.text_5]
-        
-        return privacyView
-    }
-    
-#if DEBUG
-    private func buildEmail() -> UIView {
-        let container = UIView()
-        
-        let input = UITextField()
-        container.addSubview(input)
-        input.snp.makeConstraints { make in
-            make.leading.verticalEdges.equalToSuperview()
-            make.width.equalTo(150)
-        }
-        
-        let login = UIButton()
-        login.setImage(.init(systemName: "arrow.forward"), for: .normal)
-        login.addAction(UIAction(handler: { [weak self] _ in
-            guard self != nil else { return }
-            guard let text = input.text, !text.isEmpty else { return }
-            LNAccountManager.shared.loginByEmail(email: text) { _ in }
-        }), for: .touchUpInside)
-        container.addSubview(login)
-        login.snp.makeConstraints { make in
-            make.centerY.trailing.equalToSuperview()
-            make.leading.equalTo(input.snp.trailing).offset(5)
-        }
-        
-        view.onTap { [weak self] in
-            guard let self else { return }
-            self.view.endEditing(true)
-        }
-        
-        return container
-    }
-#endif
-}
-
-#if DEBUG
-
-import SwiftUI
-
-struct LNLoginViewControllerPreview: UIViewControllerRepresentable {
-    func makeUIViewController(context: Context) -> some UIViewController {
-        LNNavigationController(rootViewController: LNLoginViewController())
-    }
-    
-    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
-        
-    }
-}
-
-#Preview(body: {
-    LNLoginViewControllerPreview()
-})
-#endif

+ 140 - 0
Lanu/Views/Login/Phone/LNCaptchaInputView.swift

@@ -0,0 +1,140 @@
+//
+//  LNCaptchaInputView.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/1/16.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+protocol LNCaptchaInputViewDelegate: NSObject {
+    func onCaptchaInputChange(view: LNCaptchaInputView)
+}
+
+
+class LNCaptchaInputView: UIView {
+    private let captchaCount: Int = 4
+    private let stackView = UIStackView()
+    private var inputViews: [UITextField] = []
+    
+    weak var delegate: LNCaptchaInputViewDelegate?
+    
+    var hasDone: Bool {
+        inputViews.first { $0.text?.isEmpty != false } == nil
+    }
+    var curInput: String {
+        inputViews.map { $0.text ?? "" }.joined()
+    }
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setupViews()
+        buildCaptchaInput()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNCaptchaInputView: UITextFieldDelegate, LNTextFieldDelegate {
+    func onDeleteBackward(_ textField: UITextField, oldText: String?) {
+        if let oldText, !oldText.isEmpty { return }
+        guard let index = inputViews.firstIndex(of: textField) else { return }
+        if index == 0 { return }
+        
+        inputViews[index - 1].text = nil
+        inputViews[index - 1].becomeFirstResponder()
+    }
+    
+    func textFieldDidChangeSelection(_ textField: UITextField) {
+        guard textField.text != nil else { return }
+        
+        let endPosition = textField.endOfDocument
+        
+        if textField.selectedTextRange?.start != endPosition {
+            textField.selectedTextRange = textField.textRange(from: endPosition, to: endPosition)
+        }
+    }
+}
+
+extension LNCaptchaInputView {
+    private func setupViews() {
+        stackView.axis = .horizontal
+        stackView.distribution = .equalCentering
+        addSubview(stackView)
+        stackView.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+        }
+    }
+    
+    private func buildCaptchaInput() {
+        let allViews = stackView.arrangedSubviews
+        allViews.forEach {
+            stackView.removeArrangedSubview($0)
+            $0.removeFromSuperview()
+        }
+        inputViews.removeAll()
+        
+        for _ in 0..<captchaCount {
+            let container = UIView()
+            container.layer.cornerRadius = 10
+            container.backgroundColor = .fill
+            container.snp.makeConstraints { make in
+                make.width.equalTo(64)
+            }
+            
+            let input = LNTextField()
+            input.delegate = self
+            input.exDelegate = self
+            input.font = .systemFont(ofSize: 32, weight: .semibold)
+            input.textColor = .text_5
+            input.keyboardType = .numberPad
+            input.textAlignment = .center
+            input.addAction(UIAction(handler: { [weak self, weak input] _ in
+                guard let self, let input else { return }
+                
+                guard let text = input.text, !text.isEmpty else { return }
+                input.text = "\(Int(text.prefix(1)) ?? 0)"
+                guard let index = inputViews.firstIndex(of: input) else { return }
+                if index < inputViews.count - 1 {
+                    inputViews[index + 1].text = nil
+                    inputViews[index + 1].becomeFirstResponder()
+                } else {
+                    input.resignFirstResponder()
+                }
+                delegate?.onCaptchaInputChange(view: self)
+            }), for: .editingChanged)
+            container.addSubview(input)
+            input.snp.makeConstraints { make in
+                make.horizontalEdges.equalToSuperview()
+                make.centerY.equalToSuperview()
+            }
+            
+            stackView.addArrangedSubview(container)
+            inputViews.append(input)
+        }
+        
+        inputViews.first?.becomeFirstResponder()
+    }
+}
+
+private protocol LNTextFieldDelegate: NSObject {
+    func onDeleteBackward(_ textField: UITextField, oldText: String?)
+}
+
+private class LNTextField: UITextField {
+    weak var exDelegate: LNTextFieldDelegate?
+    
+    override func deleteBackward() {
+        let oldText = text
+        
+        super.deleteBackward()
+        
+        exDelegate?.onDeleteBackward(self, oldText: oldText)
+    }
+}

+ 272 - 0
Lanu/Views/Login/Phone/LNCountrySelectPanel.swift

@@ -0,0 +1,272 @@
+//
+//  LNCountrySelectPanel.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/1/15.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNCountrySelectPanel: LNPopupView {
+    private let tableView = UITableView(frame: .zero, style: .grouped)
+    private var sections: [(code: String, list: [LNCountryCodeVO])] = []
+    private let sectionIndex = UIStackView()
+    
+    var handler: ((LNCountryCodeVO) -> Void)?
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setupViews()
+    }
+    
+    func update(_ sections: [(code: String, list: [LNCountryCodeVO])]) {
+        self.sections = sections
+        
+        tableView.reloadData()
+        
+        sectionIndex.arrangedSubviews.forEach {
+            sectionIndex.removeArrangedSubview($0)
+            $0.removeFromSuperview()
+        }
+        
+        for (index, section) in sections.enumerated() {
+            let item = buildSectionIndexItem(section.code)
+            sectionIndex.addArrangedSubview(item)
+            
+            item.onTap { [weak self] in
+                guard let self else { return }
+                let indexPath = IndexPath(row: 0, section: index)
+                tableView.scrollToRow(at: indexPath, at: .top, animated: false)
+            }
+        }
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNCountrySelectPanel: UITableViewDataSource, UITableViewDelegate {
+    func numberOfSections(in tableView: UITableView) -> Int {
+        sections.count
+    }
+    
+    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+        sections[section].list.count
+    }
+    
+    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
+        56
+    }
+    
+    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+        let cell = tableView.dequeueReusableCell(withIdentifier: LNCountryCodeCell.className, for: indexPath) as! LNCountryCodeCell
+        
+        let item = sections[indexPath.section].list[indexPath.row]
+        cell.update(item)
+        
+        return cell
+    }
+    
+    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
+        30
+    }
+    
+    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
+        let container = UIView()
+        
+        let titleLabel = UILabel()
+        titleLabel.font = .body_l
+        titleLabel.textColor = .text_3
+        titleLabel.text = sections[section].code
+        container.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalToSuperview().offset(16)
+        }
+        
+        return container
+    }
+    
+    func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
+        0
+    }
+    
+    func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
+        nil
+    }
+    
+    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        tableView.deselectRow(at: indexPath, animated: false)
+        let item = sections[indexPath.section].list[indexPath.row]
+        dismiss()
+        handler?(item)
+    }
+}
+
+extension LNCountrySelectPanel {
+    private func setupViews() {
+        let titleView = buildTitleView()
+        container.addSubview(titleView)
+        titleView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalToSuperview()
+        }
+        
+        let listView = buildListView()
+        container.addSubview(listView)
+        listView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalTo(titleView.snp.bottom)
+            make.bottom.equalToSuperview()
+        }
+        
+        let sectionIndex = buildSectionIndex()
+        container.addSubview(sectionIndex)
+        sectionIndex.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview()
+        }
+        
+        let closeButton = UIButton()
+        closeButton.setImage(.init(systemName: "xmark")?.withRenderingMode(.alwaysTemplate), for: .normal)
+        closeButton.tintColor = .text_2
+        closeButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            dismiss()
+        }), for: .touchUpInside)
+        container.addSubview(closeButton)
+        closeButton.snp.makeConstraints { make in
+            make.top.equalToSuperview().offset(13)
+            make.trailing.equalToSuperview().offset(-13)
+            make.width.height.equalTo(24)
+        }
+    }
+    
+    private func buildTitleView() -> UIView {
+        let container = UIView()
+        container.snp.makeConstraints { make in
+            make.height.equalTo(50)
+        }
+        
+        let titleLabel = UILabel()
+        titleLabel.text = .init(key: "B00024")
+        titleLabel.font = .heading_h3
+        titleLabel.textColor = .text_5
+        container.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+        }
+        
+        return container
+    }
+    
+    private func buildListView() -> UIView {
+        tableView.dataSource = self
+        tableView.delegate = self
+        tableView.separatorStyle = .none
+        tableView.backgroundColor = .fill
+        tableView.register(LNCountryCodeCell.self, forCellReuseIdentifier: LNCountryCodeCell.className)
+        tableView.contentInset = .init(top: 0, left: 0, bottom: -commonBottomInset, right: 0)
+        
+        return tableView
+    }
+    
+    private func buildSectionIndex() -> UIView {
+        sectionIndex.axis = .vertical
+        sectionIndex.snp.makeConstraints { make in
+            make.width.equalTo(32)
+            make.height.equalTo(0).priority(.low)
+        }
+        
+        return sectionIndex
+    }
+    
+    private func buildSectionIndexItem(_ title: String) -> UIView {
+        let container = UIView()
+        container.backgroundColor = .fill
+        container.snp.makeConstraints { make in
+            make.width.equalTo(32)
+            make.height.equalTo(30)
+        }
+        
+        let label = UILabel()
+        label.text = title
+        label.font = .body_l
+        label.textColor = .text_3
+        container.addSubview(label)
+        label.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+        }
+        
+        return container
+    }
+}
+
+private class LNCountryCodeCell: UITableViewCell {
+    private let icon = UIImageView()
+    private let nameLabel = UILabel()
+    
+    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+        super.init(style: style, reuseIdentifier: reuseIdentifier)
+        
+        setupViews()
+    }
+    
+    func update(_ item: LNCountryCodeVO) {
+        icon.sd_setImage(with: URL(string: item.icon))
+        nameLabel.text = "\(item.name) \(item.code)"
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    private func setupViews() {
+        icon.layer.cornerRadius = 12
+        icon.clipsToBounds = true
+        contentView.addSubview(icon)
+        icon.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(16)
+            make.centerY.equalToSuperview()
+            make.width.height.equalTo(24)
+        }
+        
+        nameLabel.font = .body_l
+        nameLabel.textColor = .text_5
+        contentView.addSubview(nameLabel)
+        nameLabel.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalTo(icon.snp.trailing).offset(12)
+        }
+    }
+}
+
+#if DEBUG
+
+import SwiftUI
+
+struct LNCountrySelectPanelPreview: UIViewRepresentable {
+    func makeUIView(context: Context) -> some UIView {
+        let container = UIView()
+        container.backgroundColor = .lightGray
+        
+        let view = LNCountrySelectPanel()
+        view.containerHeight = .percent(0.7)
+        view.popup(container)
+        
+        return container
+    }
+    
+    func updateUIView(_ uiView: UIViewType, context: Context) { }
+}
+
+#Preview(body: {
+    LNCountrySelectPanelPreview()
+})
+#endif
+

+ 186 - 0
Lanu/Views/Login/Phone/LNLoginCaptchaInputViewController.swift

@@ -0,0 +1,186 @@
+//
+//  LNLoginCaptchaInputViewController.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/1/16.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+extension UIView {
+    func pushToLoginCaptcha(code: String, phone: String) {
+        let vc = LNLoginCaptchaInputViewController(code: code, phone: phone)
+        navigationController?.pushViewController(vc, animated: true)
+    }
+}
+
+
+class LNLoginCaptchaInputViewController: LNViewController {
+    private let code: String
+    private let phone: String
+    
+    private let captchaInput = LNCaptchaInputView()
+    private let tipsView = LNAutoSizeTextView()
+    
+    init(code: String, phone: String) {
+        self.code = code
+        self.phone = phone
+        
+        super.init(nibName: nil, bundle: nil)
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        setupViews()
+        
+        LNEventDeliver.addObserver(self)
+    }
+}
+
+extension LNLoginCaptchaInputViewController: LNAccountManagerNotify {
+    func onCaptchaCoolDownChanged(time: Int) {
+        if time == 0 {
+            let text = NSMutableAttributedString(string: .init(key: "B00028"), attributes: [
+                .foregroundColor: UIColor.text_5
+            ])
+            let resend = NSMutableAttributedString(string: .init(key: "B00029"), attributes: [
+                .font: UIFont.heading_h4,
+                .link: "Gami:resend",
+            ])
+            text.append(.init(string: " "))
+            text.append(resend)
+            tipsView.attributedText = text
+        } else {
+            let text: String = .init(key: "B00027") + " (\(time)s)"
+            tipsView.attributedText = .init(string: text, attributes: [
+                .foregroundColor: UIColor.text_3
+            ])
+        }
+    }
+    
+    func onUserLogin() {
+        navigationController?.popToRootViewController(animated: true)
+    }
+}
+
+extension LNLoginCaptchaInputViewController: UITextViewDelegate {
+    func textView(_ textView: UITextView, shouldInteractWith URL: URL,
+                  in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
+        showLoading()
+        LNAccountManager.shared.getLoginCaptcha(code: code, phone: phone) { [weak self] success in
+            dismissLoading()
+            guard self != nil else { return }
+            guard success else { return }
+            showToast(.init(key: "B00030"))
+        }
+        
+        return false
+    }
+    
+    func textViewDidChangeSelection(_ textView: UITextView) {
+        textView.selectedTextRange = nil
+    }
+}
+
+extension LNLoginCaptchaInputViewController: LNCaptchaInputViewDelegate {
+    func onCaptchaInputChange(view: LNCaptchaInputView) {
+        guard view.hasDone else { return }
+        
+        LNAccountManager.shared.loginByPhone(code: code, num: phone, captcha: view.curInput)
+    }
+}
+
+extension LNLoginCaptchaInputViewController {
+    private func setupViews() {
+        showNavigationBar = false
+        
+        let cover = UIImageView(image: .icLoginProfileBg)
+        view.addSubview(cover)
+        cover.snp.makeConstraints { make in
+            make.top.horizontalEdges.equalToSuperview()
+        }
+        
+        let fakeNav = LNFakeNaviBar()
+        fakeNav.showBackButton()
+        view.addSubview(fakeNav)
+        fakeNav.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalToSuperview()
+        }
+        
+        let textView = buildTextView()
+        view.addSubview(textView)
+        textView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(22)
+            make.top.equalTo(fakeNav.snp.bottom).offset(20)
+        }
+        
+        captchaInput.delegate = self
+        view.addSubview(captchaInput)
+        captchaInput.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(22)
+            make.top.equalTo(textView.snp.bottom).offset(24)
+            make.height.equalTo(64)
+        }
+        
+        let tipsView = buildCoolDown()
+        view.addSubview(tipsView)
+        tipsView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(22 - 4)
+            make.top.equalTo(captchaInput.snp.bottom).offset(24 - 7) // 7 是 UITextView 自身的内容缩进
+        }
+        
+        view.onTap { [weak self] in
+            guard let self else { return }
+            view.endEditing(true)
+        }
+    }
+    
+    private func buildTextView() -> UIView {
+        let container = UIView()
+        
+        let titleLabel = UILabel()
+        titleLabel.text = .init(key: "B00025")
+        titleLabel.font = .heading_h1
+        titleLabel.textColor = .text_5
+        container.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalToSuperview()
+        }
+        
+        let descLabel = UILabel()
+        descLabel.text = .init(key: "B00026") + "\n\(code) \(phone)"
+        descLabel.font = .body_m
+        descLabel.textColor = .text_4
+        descLabel.numberOfLines = 0
+        container.addSubview(descLabel)
+        descLabel.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalTo(titleLabel.snp.bottom).offset(13)
+            make.bottom.equalToSuperview()
+        }
+        
+        return container
+    }
+    
+    private func buildCoolDown() -> UIView {
+        tipsView.font = .body_m
+        tipsView.isEditable = false
+        tipsView.delegate = self
+        tipsView.showsVerticalScrollIndicator = false
+        tipsView.showsHorizontalScrollIndicator = false
+        tipsView.backgroundColor = .clear
+        tipsView.linkTextAttributes = [.foregroundColor: UIColor.text_6]
+        
+        return tipsView
+    }
+}

+ 295 - 0
Lanu/Views/Login/Phone/LNLoginPhoneInputViewController.swift

@@ -0,0 +1,295 @@
+//
+//  LNLoginPhoneInputViewController.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/1/15.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+extension UIView {
+    func pushToLoginPhone() {
+        let vc = LNLoginPhoneInputViewController()
+        navigationController?.pushViewController(vc, animated: true)
+    }
+}
+
+
+class LNLoginPhoneInputViewController: LNViewController {
+    private let container = UIView()
+    
+    private let countryIcon = UIImageView()
+    private let countryCodeLabel = UILabel()
+    private let phoneInputView = UITextField()
+    
+    private let confirmButton = UIButton()
+    
+    private var sections: [(code: String, list: [LNCountryCodeVO])] = []
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        loadCountryCodeList()
+        
+        setupViews()
+        
+        LNEventDeliver.addObserver(self)
+    }
+}
+
+extension LNLoginPhoneInputViewController {
+    private func loadCountryCodeList() {
+        LNConfigManager.shared.getCountryCodeList { [weak self] list in
+            guard let self else { return }
+            guard let list else { return }
+            
+            sortList(list)
+        }
+    }
+    
+    private func sortList(_ list: [LNCountryCodeVO]) {
+        sections.removeAll()
+        
+        var map: [String: [LNCountryCodeVO]] = [:]
+        for item in list {
+            let first = item.name.classificationFirstLetter
+            var section = map[first] ?? []
+            section.append(item)
+            map[first] = section
+        }
+        
+        let keys = map.keys.sorted()
+        for key in keys {
+            sections.append((key, map[key]) as! (code: String, list: [LNCountryCodeVO]))
+        }
+        
+        let item: LNCountryCodeVO? = list.first {
+            switch LNAppConfig.shared.curLang {
+            case .chiness: $0.code == "CN"
+            case .english: $0.code == "EN"
+            case .indonesian: $0.code == "ID"
+            }
+        }
+        if let item {
+            countryIcon.sd_setImage(with: URL(string: item.icon))
+            countryCodeLabel.text = item.code
+        }
+    }
+}
+
+extension LNLoginPhoneInputViewController: LNAccountManagerNotify {
+    func onCaptchaCoolDownChanged(time: Int) {
+        let text: String = if time == 0 {
+            .init(key: "B00023")
+        } else {
+            .init(key: "B00023") + " (\(time)s)"
+        }
+        confirmButton.setTitle(text, for: .normal)
+        checkConfirmButton()
+    }
+}
+
+extension LNLoginPhoneInputViewController {
+    private func checkConfirmButton() {
+        let text = phoneInputView.text ?? ""
+        confirmButton.isEnabled = !text.isEmpty && LNAccountManager.shared.canSendCaptcha
+    }
+    
+    private func setupViews() {
+        showNavigationBar = false
+        
+        let cover = UIImageView(image: .icLoginPhoneBg)
+        view.addSubview(cover)
+        cover.snp.makeConstraints { make in
+            make.top.horizontalEdges.equalToSuperview()
+        }
+        
+        let fakeNav = LNFakeNaviBar()
+        fakeNav.showBackButton()
+        view.addSubview(fakeNav)
+        fakeNav.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalToSuperview()
+        }
+        
+        let paragraphStyle = NSMutableParagraphStyle()
+        paragraphStyle.lineHeightMultiple = 0.8
+        
+        let titleLabel = UILabel()
+        titleLabel.attributedText = .init(string: .init(key: "B00020"), attributes: [
+            .paragraphStyle: paragraphStyle,
+        ])
+        titleLabel.font = .heading_h1
+        titleLabel.textColor = .text_5
+        titleLabel.numberOfLines = 0
+        titleLabel.clipsToBounds = false
+        titleLabel.layer.masksToBounds = false
+        view.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(24)
+            make.bottom.equalTo(cover.snp.bottom).offset(-42)
+        }
+        
+        container.backgroundColor = .fill
+        container.layer.cornerRadius = 20
+        container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
+        view.addSubview(container)
+        container.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalTo(titleLabel.snp.bottom).offset(12)
+            make.bottom.equalToSuperview()
+        }
+        
+        let phoneInput = buildPhoneInput()
+        container.addSubview(phoneInput)
+        phoneInput.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(22)
+            make.top.equalToSuperview().offset(26)
+        }
+        
+        let confirm = buildConfirmButton()
+        container.addSubview(confirm)
+        confirm.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(22)
+            make.top.equalTo(phoneInput.snp.bottom).offset(24)
+        }
+        
+        view.onTap { [weak self] in
+            guard let self else { return }
+            view.endEditing(true)
+        }
+    }
+    
+    private func buildPhoneInput() -> UIView {
+        let container = UIView()
+        container.backgroundColor = .fill_2
+        container.layer.cornerRadius = 26
+        container.snp.makeConstraints { make in
+            make.height.equalTo(52)
+        }
+        
+        let countryView = UIView()
+        countryView.onTap { [weak self] in
+            guard let self else { return }
+            guard !sections.isEmpty else { return }
+            
+            let panel = LNCountrySelectPanel()
+            panel.containerHeight = .height(self.container.bounds.height)
+            panel.update(sections)
+            panel.handler = { [weak self] item in
+                guard let self else { return }
+                countryIcon.sd_setImage(with: URL(string: item.icon))
+                countryCodeLabel.text = item.code
+            }
+            panel.popup()
+        }
+        container.addSubview(countryView)
+        countryView.snp.makeConstraints { make in
+            make.verticalEdges.equalToSuperview()
+            make.leading.equalToSuperview()
+        }
+        
+        countryIcon.layer.cornerRadius = 12
+        countryIcon.clipsToBounds = true
+        countryView.addSubview(countryIcon)
+        countryIcon.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalToSuperview().offset(16)
+            make.width.height.equalTo(24)
+        }
+        
+        countryCodeLabel.font = .heading_h2
+        countryCodeLabel.textColor = .text_5
+        countryCodeLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
+        countryCodeLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
+        countryView.addSubview(countryCodeLabel)
+        countryCodeLabel.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalTo(countryIcon.snp.trailing).offset(4)
+        }
+        
+        let config = UIImage.SymbolConfiguration(pointSize: 10)
+        let arrow = UIImageView()
+        arrow.image = .init(systemName: "chevron.down", withConfiguration: config)?.withRenderingMode(.alwaysTemplate)
+        arrow.tintColor = .text_5
+        countryView.addSubview(arrow)
+        arrow.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalTo(countryCodeLabel.snp.trailing).offset(4)
+            make.trailing.equalToSuperview()
+            make.width.equalTo(12)
+        }
+        
+        phoneInputView.font = .heading_h2
+        phoneInputView.textColor = .text_5
+        phoneInputView.attributedPlaceholder = .init(string: .init(key: "B00021"), attributes: [
+            .font: UIFont.body_l,
+            .foregroundColor: UIColor.text_2
+        ])
+        phoneInputView.clearButtonMode = .always
+        phoneInputView.keyboardType = .numberPad
+        phoneInputView.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            checkConfirmButton()
+        }), for: .editingChanged)
+        container.addSubview(phoneInputView)
+        phoneInputView.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalTo(countryView.snp.trailing).offset(12)
+            make.trailing.equalToSuperview().offset(-12)
+        }
+        
+        return container
+    }
+    
+    private func buildConfirmButton() -> UIView {
+        confirmButton.setBackgroundImage(.primary_8, for: .normal)
+        confirmButton.layer.cornerRadius = 23.5
+        confirmButton.clipsToBounds = true
+        confirmButton.setTitle(.init(key: "B00023"), for: .normal)
+        confirmButton.setTitleColor(.text_1, for: .normal)
+        confirmButton.titleLabel?.font = .heading_h3
+        confirmButton.isEnabled = false
+        confirmButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            let code = countryCodeLabel.text
+            let phone = phoneInputView.text
+            guard let code, let phone else { return }
+            showLoading()
+            LNAccountManager.shared.getLoginCaptcha(code: code, phone: phone)
+            { [weak self] success in
+                dismissLoading()
+                guard let self else { return }
+                guard success else { return }
+                
+                view.pushToLoginCaptcha(code: code, phone: phone)
+            }
+        }), for: .touchUpInside)
+        confirmButton.snp.makeConstraints { make in
+            make.height.equalTo(47)
+        }
+        
+        return confirmButton
+    }
+}
+
+#if DEBUG
+
+import SwiftUI
+
+struct LNLoginPhoneInputViewControllerPreview: UIViewControllerRepresentable {
+    func makeUIViewController(context: Context) -> some UIViewController {
+        LNLoginPhoneInputViewController()
+    }
+    
+    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
+        
+    }
+}
+
+#Preview(body: {
+    LNLoginPhoneInputViewControllerPreview()
+})
+#endif

+ 3 - 28
Lanu/Views/Login/Setup/LNBaseInfoSetupViewController.swift

@@ -34,7 +34,7 @@ enum LNAvatarModifyType: CaseIterable {
 class LNBaseInfoSetupViewController: LNViewController {
     private let updateConfig: LNProfileUpdateConfig
     
-    private let fakeNavBar = UIView()
+    private let fakeNavBar = LNFakeNaviBar()
     
     private let avatar = LNUploadImageView()
     private let nameInputField = UITextField()
@@ -98,7 +98,7 @@ extension LNBaseInfoSetupViewController {
                 checkNextButtonEnable()
             }
         }
-        panel.showIn(view)
+        panel.popup(view)
     }
     
     private func loadRandomProfile() {
@@ -410,32 +410,7 @@ extension LNBaseInfoSetupViewController {
 //    }
     
     private func buildFakeNavBar() -> UIView {
-        fakeNavBar.snp.makeConstraints { make in
-            make.height.equalTo(44 + UIView.statusBarHeight)
-        }
-        
-        let container = UIView()
-        fakeNavBar.addSubview(container)
-        container.snp.makeConstraints { make in
-            make.horizontalEdges.equalToSuperview()
-            make.bottom.equalToSuperview()
-            make.height.equalTo(44)
-        }
-        
-        let backButton = UIButton()
-        backButton.addAction(UIAction(handler: { [weak self] _ in
-            guard let self else { return }
-            navigationController?.popViewController(animated: true)
-        }), for: .touchUpInside)
-        let buttonImage: UIImage? = .init(systemName: "chevron.backward")?.withRenderingMode(.alwaysTemplate)
-        backButton.setImage(buttonImage, for: .normal)
-        backButton.tintColor = .text_4
-        container.addSubview(backButton)
-        backButton.snp.makeConstraints { make in
-            make.centerY.equalToSuperview()
-            make.leading.equalToSuperview().offset(16)
-            make.width.height.equalTo(24)
-        }
+        fakeNavBar.showBackButton()
         
         return fakeNavBar
     }

+ 3 - 25
Lanu/Views/Login/Setup/LNGenderSetupViewController.swift

@@ -19,7 +19,7 @@ extension UIView {
 
 
 class LNGenderSetupViewController: LNViewController {
-    private let fakeNavBar = UIView()
+    private let fakeNavBar = LNFakeNaviBar()
     
     private let maleIc = UIImageView()
     private let maleBg = UIImageView()
@@ -235,32 +235,10 @@ extension LNGenderSetupViewController {
     }
     
     private func buildFakeNavBar() -> UIView {
-        fakeNavBar.snp.makeConstraints { make in
-            make.height.equalTo(44 + UIView.statusBarHeight)
-        }
-        
-        let container = UIView()
-        fakeNavBar.addSubview(container)
-        container.snp.makeConstraints { make in
-            make.horizontalEdges.equalToSuperview()
-            make.bottom.equalToSuperview()
-            make.height.equalTo(44)
-        }
-        
-        let backButton = UIButton()
-        backButton.addAction(UIAction(handler: { [weak self] _ in
+        fakeNavBar.showBackButton { [weak self] in
             guard let self else { return }
             navigationController?.popViewController(animated: true)
             LNAccountManager.shared.logout()
-        }), for: .touchUpInside)
-        let buttonImage: UIImage? = .init(systemName: "chevron.backward")?.withRenderingMode(.alwaysTemplate)
-        backButton.setImage(buttonImage, for: .normal)
-        backButton.tintColor = .text_4
-        container.addSubview(backButton)
-        backButton.snp.makeConstraints { make in
-            make.centerY.equalToSuperview()
-            make.leading.equalToSuperview().offset(16)
-            make.width.height.equalTo(24)
         }
         
         let skipButton = UIButton()
@@ -273,7 +251,7 @@ extension LNGenderSetupViewController {
             let config = LNProfileUpdateConfig()
             view.pushToBaseInfoSetup(config)
         }), for: .touchUpInside)
-        container.addSubview(skipButton)
+        fakeNavBar.actionView.addSubview(skipButton)
         skipButton.snp.makeConstraints { make in
             make.centerY.equalToSuperview()
             make.trailing.equalToSuperview().offset(-16)

+ 2 - 27
Lanu/Views/Login/Setup/LNInterestSetupViewController.swift

@@ -21,7 +21,7 @@ extension UIView {
 class LNInterestSetupViewController: LNViewController {
     private let updateConfig: LNProfileUpdateConfig
     
-    private let fakeNavBar = UIView()
+    private let fakeNavBar = LNFakeNaviBar()
     
     private let horizentalSpace = 22.0
     private let lineSpace = 8.0
@@ -170,32 +170,7 @@ extension LNInterestSetupViewController {
     }
     
     private func buildFakeNavBar() -> UIView {
-        fakeNavBar.snp.makeConstraints { make in
-            make.height.equalTo(44 + UIView.statusBarHeight)
-        }
-        
-        let container = UIView()
-        fakeNavBar.addSubview(container)
-        container.snp.makeConstraints { make in
-            make.horizontalEdges.equalToSuperview()
-            make.bottom.equalToSuperview()
-            make.height.equalTo(44)
-        }
-        
-        let backButton = UIButton()
-        backButton.addAction(UIAction(handler: { [weak self] _ in
-            guard let self else { return }
-            navigationController?.popViewController(animated: true)
-        }), for: .touchUpInside)
-        let buttonImage: UIImage? = .init(systemName: "chevron.backward")?.withRenderingMode(.alwaysTemplate)
-        backButton.setImage(buttonImage, for: .normal)
-        backButton.tintColor = .text_4
-        container.addSubview(backButton)
-        backButton.snp.makeConstraints { make in
-            make.centerY.equalToSuperview()
-            make.leading.equalToSuperview().offset(16)
-            make.width.height.equalTo(24)
-        }
+        fakeNavBar.showBackButton()
         
         return fakeNavBar
     }

+ 1 - 1
Lanu/Views/Order/Create/LNCreateOrderFromSkillListPanel.swift

@@ -320,7 +320,7 @@ struct LNProfileOrderPanelPreview: UIViewRepresentable {
         container.backgroundColor = .lightGray
         
         let view = LNCreateOrderFromSkillListPanel()
-        view.showIn(container)
+        view.popup(container)
         
         return container
     }

+ 1 - 1
Lanu/Views/Order/Create/LNCreateOrderPanel.swift

@@ -498,7 +498,7 @@ struct LNCreateOrderPanelPreview: UIViewRepresentable {
         container.backgroundColor = .lightGray
         
         let view = LNCreateOrderPanel()
-        view.showIn(container)
+        view.popup(container)
         
         return container
     }

+ 1 - 1
Lanu/Views/Order/Create/LNCreateOrderViewController.swift

@@ -483,7 +483,7 @@ extension LNCreateOrderViewController {
                 }
                 navigationController?.viewControllers.removeAll { $0 is LNCreateOrderViewController }
             }
-            panel.showIn()
+            panel.popup()
         }), for: .touchUpInside)
         container.addSubview(orderButton)
         orderButton.snp.makeConstraints { make in

+ 1 - 1
Lanu/Views/Order/Detail/LNOrderDetailViewController.swift

@@ -343,7 +343,7 @@ extension LNOrderDetailViewController {
             guard let curDetail else { return }
             let panel = LNOrderCommentPanel()
             panel.update(curDetail.orderInfo.avatar, orderId: orderId)
-            panel.showIn()
+            panel.popup()
         }), for: .touchUpInside)
         stackView.addArrangedSubview(commentButton)
         commentButton.snp.makeConstraints { make in

+ 1 - 1
Lanu/Views/Order/LNOrderCommentPanel.swift

@@ -155,7 +155,7 @@ struct LNOrderCommentPanelPreview: UIViewRepresentable {
         container.backgroundColor = .lightGray
         
         let view = LNOrderCommentPanel()
-        view.showIn(container)
+        view.popup(container)
         
         return container
     }

+ 1 - 1
Lanu/Views/Order/OrderList/LNOrderListItemCell.swift

@@ -71,7 +71,7 @@ extension LNOrderListItemCell {
             panel.handler = { star, comment in
                 orderItem.star = star
             }
-            panel.showIn()
+            panel.popup()
             break
         case .serviceDone: // 可以点击完成
             LNCommonAlertView.showFinishOrderAlert(orderId: orderItem.orderId) { success in

+ 2 - 2
Lanu/Views/Order/OrderQR/LNOrderGenerateQRCodePanel.swift

@@ -139,7 +139,7 @@ extension LNOrderGenerateQRCodePanel {
                 guard let self else { return }
                 self.curSkill = skill
             }
-            panel.showIn()
+            panel.popup()
         }
         
         return container
@@ -162,7 +162,7 @@ struct LNOrderQRPanelPreview: UIViewRepresentable {
         container.backgroundColor = .lightGray
         
         let view = LNOrderGenerateQRCodePanel()
-        view.showIn(container)
+        view.popup(container)
         
         return container
     }

+ 1 - 1
Lanu/Views/Order/OrderQR/LNOrderSkillListPanel.swift

@@ -105,7 +105,7 @@ struct LNOrderSkillListPanelPreview: UIViewRepresentable {
         let view = LNOrderSkillListPanel(curSkillId: "") { skill in
             
         }
-        view.showIn(container)
+        view.popup(container)
         
         return container
     }

+ 1 - 1
Lanu/Views/Order/OrderRecords/LNOrderRecordCell.swift

@@ -46,7 +46,7 @@ class LNOrderRecordCell: UITableViewCell {
     
     func update(_ item: LNOrderRecordItemVO) {
         gameIc.sd_setImage(with: URL(string: item.categoryIcon))
-        dateLabel.text = TimeInterval(item.createTime / 1_000).formattedFullDate()
+        dateLabel.text = TimeInterval(item.createTime / 1_000).formattedFullDateWithTime()
         gameNameLabel.text = item.bizCategoryName
         orderIdLabel.text = item.orderId
         

+ 1 - 1
Lanu/Views/Profile/Edit/LNEditBioPanel.swift

@@ -97,7 +97,7 @@ struct LNEditBioPanelPreview: UIViewRepresentable {
         container.backgroundColor = .lightGray
         
         let view = LNEditBioPanel()
-        view.showIn(container)
+        view.popup(container)
         
         return container
     }

+ 5 - 0
Lanu/Views/Profile/Edit/LNEditNickNamePanel.swift

@@ -41,6 +41,11 @@ extension LNEditNickNamePanel: UITextFieldDelegate {
         
         return newText.count <= maxInput
     }
+    
+    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
+        textField.resignFirstResponder()
+        return true
+    }
 }
 
 extension LNEditNickNamePanel {

+ 15 - 12
Lanu/Views/Profile/Edit/LNEditProfileViewController.swift

@@ -81,10 +81,10 @@ class LNEditProfileViewController: LNViewController {
         didSet {
             if curVoice.isEmpty {
                 voiceLabel.text = nil
-                voiceLabel.attributedText = .init(string: .init(key: "A00286"), attributes: [.foregroundColor: UIColor.text_6])
+                voiceLabel.attributedText = .init(string: .init(key: "B00001"), attributes: [.foregroundColor: UIColor.text_6])
             } else {
                 voiceLabel.attributedText = nil
-                voiceLabel.text = true ? .init(key: "A00287") : .init(key: "A00288") // 是否通过
+                voiceLabel.text = true ? .init(key: "B00002") : .init(key: "B00003") // 是否通过
             }
         }
     }
@@ -211,7 +211,7 @@ extension LNEditProfileViewController {
                 config.gender = curGender
             }
             if curBirthday != Double(myUserInfo.birthday / 1_000) {
-                config.birthday = curBirthday.formattedFullDate("-")
+                config.birthday = curBirthday.formattedFullDate("-", normal: true)
             }
             if curBio != myUserInfo.intro {
                 config.bio = curBio
@@ -317,7 +317,7 @@ extension LNEditProfileViewController {
                 curName = name
                 checkSaveButton()
             }
-            panel.showIn()
+            panel.popup()
         }
         stackView.addArrangedSubview(nameView)
         
@@ -331,7 +331,7 @@ extension LNEditProfileViewController {
                 curGender = gender
                 checkSaveButton()
             }
-            panel.showIn()
+            panel.popup()
         }
         stackView.addArrangedSubview(genderView)
         
@@ -345,7 +345,7 @@ extension LNEditProfileViewController {
                 curBirthday = date
                 checkSaveButton()
             }
-            panel.showIn()
+            panel.popup()
         }
         stackView.addArrangedSubview(birthdayView)
         
@@ -359,7 +359,7 @@ extension LNEditProfileViewController {
                 curBio = bio
                 checkSaveButton()
             }
-            panel.showIn()
+            panel.popup()
         }
         stackView.addArrangedSubview(bioView)
         
@@ -374,7 +374,7 @@ extension LNEditProfileViewController {
                 })
                 checkSaveButton()
             }
-            panel.showIn()
+            panel.popup()
         }
         stackView.addArrangedSubview(interestView)
         
@@ -394,7 +394,7 @@ extension LNEditProfileViewController {
         let titleLabel = UILabel()
         titleLabel.font = .heading_h4
         titleLabel.textColor = .text_5
-        titleLabel.text = .init(key: "A00289")
+        titleLabel.text = .init(key: "B00004")
         container.addSubview(titleLabel)
         titleLabel.snp.makeConstraints { make in
             make.horizontalEdges.equalToSuperview()
@@ -411,14 +411,17 @@ extension LNEditProfileViewController {
             make.bottom.equalToSuperview()
         }
         
-        let voiceView = buildInfoLine(title: .init(key: "A00290"), label: voiceLabel)
+        let voiceView = buildInfoLine(title: .init(key: "B00005"), label: voiceLabel)
         voiceView.onTap { [weak self] in
             guard let self else { return }
             LNVoiceRecorder.shared.requestMicrophonePermission { [weak self] success in
                 guard let self else { return }
-                guard success else { return }
+                guard success else {
+                    showToast(.init(key: "B00022"))
+                    return
+                }
                 let panel = LNEditVoicePanel()
-                panel.showIn()
+                panel.popup()
             }
         }
         stackView.addArrangedSubview(voiceView)

+ 9 - 9
Lanu/Views/Profile/Edit/LNEditVoicePanel.swift

@@ -61,7 +61,7 @@ class LNEditVoicePanel: LNPopupView {
                 stopRecord()
             } else if curState == .edit {
                 let alert = LNCommonAlertView()
-                alert.titleLabel.text = .init(key: "A00303")
+                alert.titleLabel.text = .init(key: "B00018")
                 alert.setConfirm { [weak self] in
                     guard let self else { return }
                     dismiss()
@@ -99,7 +99,7 @@ extension LNEditVoicePanel {
             return
         }
         if duration < minDuration {
-            showToast(.init(key: "A00294"))
+            showToast(.init(key: "B00009"))
             return
         }
         curUrl = url
@@ -155,7 +155,7 @@ extension LNEditVoicePanel: LNVoiceRecorderNotify {
     func onRecordTaskRecording(taskId: String) {
         guard recordTaskId == taskId else { return }
         
-        recordText.text = .init(key: "A00293")
+        recordText.text = .init(key: "B00008")
         recordButton.setImage(.icVoiceEditStop, for: .normal)
     }
     
@@ -178,7 +178,7 @@ extension LNEditVoicePanel {
     private func resetRecord() {
         recordDurationLabel.text = "00:00"
         recordButton.setImage(.icVoiceEditStart, for: .normal)
-        recordText.text = .init(key: "A00292")
+        recordText.text = .init(key: "B00007")
     }
     
     private func setupViews() {
@@ -231,7 +231,7 @@ extension LNEditVoicePanel {
         }
         
         let titleLabel = UILabel()
-        titleLabel.text = .init(key: "A00291")
+        titleLabel.text = .init(key: "B00006")
         container.addSubview(titleLabel)
         titleLabel.snp.makeConstraints { make in
             make.center.equalToSuperview()
@@ -330,7 +330,7 @@ extension LNEditVoicePanel {
         }
         
         let remakeLabel = UILabel()
-        remakeLabel.text = .init(key: "A00295")
+        remakeLabel.text = .init(key: "B00010")
         remakeLabel.font = .body_m
         remakeLabel.textColor = .text_4
         remakeLabel.textAlignment = .center
@@ -359,7 +359,7 @@ extension LNEditVoicePanel {
         }
         
         let playLabel = UILabel()
-        playLabel.text = .init(key: "A00296")
+        playLabel.text = .init(key: "B00011")
         playLabel.font = .body_m
         playLabel.textColor = .text_4
         playLabel.textAlignment = .center
@@ -420,7 +420,7 @@ extension LNEditVoicePanel {
         }
         
         let titleLabel = UILabel()
-        titleLabel.text = .init(key: "A00297")
+        titleLabel.text = .init(key: "B00012")
         titleLabel.textColor = .text_5
         titleLabel.font = .heading_h4
         reviewView.addSubview(titleLabel)
@@ -430,7 +430,7 @@ extension LNEditVoicePanel {
         }
         
         let descLabel = UILabel()
-        descLabel.text = .init(key: "A00298")
+        descLabel.text = .init(key: "B00013")
         descLabel.font = .body_m
         descLabel.textColor = .text_5
         reviewView.addSubview(descLabel)

+ 1 - 1
Lanu/Views/Profile/Mine/LNMineQRCodeShareView.swift

@@ -80,7 +80,7 @@ extension LNMineQRCodeShareView {
         
         onTap {
             let panel = LNOrderGenerateQRCodePanel()
-            panel.showIn()
+            panel.popup()
         }
     }
 }

+ 1 - 0
Lanu/Views/Profile/Mine/LNMineUserInfoView.swift

@@ -101,6 +101,7 @@ extension LNMineUserInfoView {
         avatar.layer.borderColor = UIColor.fill.cgColor
         avatar.backgroundColor = .fill
         avatar.clipsToBounds = true
+        avatar.contentMode = .scaleAspectFill
         avatar.isUserInteractionEnabled = true
         avatar.onTap { [weak self] in
             guard let self else { return }

+ 1 - 1
Lanu/Views/Profile/Post/LNPostShareViewController.swift

@@ -81,7 +81,7 @@ extension LNPostShareViewController {
             selectedSkills = selections
             updateSkillsView()
         }
-        panel.showIn(view)
+        panel.popup(view)
     }
     
     private func setupViews() {

+ 1 - 1
Lanu/Views/Profile/Post/LNPostSkillSelectPanel.swift

@@ -174,7 +174,7 @@ struct LNPostSkillSelectPanelPreview: UIViewRepresentable {
         container.backgroundColor = .lightGray
         
         let view = LNPostSkillSelectPanel()
-        view.showIn(container)
+        view.popup(container)
         
         return container
     }

+ 1 - 1
Lanu/Views/Profile/Profile/LNProfileNaviBarView.swift

@@ -118,7 +118,7 @@ extension LNProfileNaviBarView {
                 }
             }
         }
-        panel.showIn()
+        panel.popup()
     }
     
     private func setupViews() {

+ 1 - 1
Lanu/Views/Profile/Profile/LNProfileScoreFloatingView.swift

@@ -129,7 +129,7 @@ extension LNProfileScoreFloatingView {
                     }
                 }
             }
-            panel.showIn()
+            panel.popup()
         }
     }
 }

+ 1 - 1
Lanu/Views/Profile/Profile/LNProfileStaringPanel.swift

@@ -106,7 +106,7 @@ struct LNProfileStaringPanelPreview: UIViewRepresentable {
         container.backgroundColor = .lightGray
         
         let view = LNProfileStaringPanel()
-        view.showIn(container)
+        view.popup(container)
         
         return container
     }

+ 4 - 31
Lanu/Views/Search/LNUserSearchViewController.swift

@@ -171,33 +171,8 @@ extension LNUserSearchViewController {
     }
     
     private func buildFakeNavBar() -> UIView {
-        let fakeNavBar = UIView()
-        fakeNavBar.snp.makeConstraints { make in
-            make.height.equalTo(44 + UIView.statusBarHeight)
-        }
-        
-        let container = UIView()
-        fakeNavBar.addSubview(container)
-        container.snp.makeConstraints { make in
-            make.horizontalEdges.equalToSuperview()
-            make.bottom.equalToSuperview()
-            make.height.equalTo(44)
-        }
-        
-        let backButton = UIButton()
-        backButton.addAction(UIAction(handler: { [weak self] _ in
-            guard let self else { return }
-            self.navigationController?.popViewController(animated: true)
-        }), for: .touchUpInside)
-        let buttonImage: UIImage? = .init(systemName: "chevron.backward")?.withRenderingMode(.alwaysTemplate)
-        backButton.setImage(buttonImage, for: .normal)
-        backButton.tintColor = .text_4
-        container.addSubview(backButton)
-        backButton.snp.makeConstraints { make in
-            make.centerY.equalToSuperview()
-            make.leading.equalToSuperview().offset(16)
-            make.width.height.equalTo(24)
-        }
+        let fakeNavBar = LNFakeNaviBar()
+        let backButton = fakeNavBar.showBackButton()
         
         let search = UIButton()
         search.setTitle(.init(key: "A00245"), for: .normal)
@@ -209,14 +184,14 @@ extension LNUserSearchViewController {
             guard let self else { return }
             _ = textFieldShouldReturn(searchInput)
         }), for: .touchUpInside)
-        container.addSubview(search)
+        fakeNavBar.actionView.addSubview(search)
         search.snp.makeConstraints { make in
             make.trailing.equalToSuperview().offset(-16)
             make.centerY.equalToSuperview()
         }
         
         let input = buildSearchInput()
-        container.addSubview(input)
+        fakeNavBar.actionView.addSubview(input)
         input.snp.makeConstraints { make in
             make.leading.equalTo(backButton.snp.trailing).offset(12)
             make.centerY.equalToSuperview()
@@ -315,7 +290,6 @@ extension LNUserSearchViewController {
 #if DEBUG
 
 import SwiftUI
-import MJRefresh
 
 struct LNIMSearchViewControllerPreview: UIViewControllerRepresentable {
     func makeUIViewController(context: Context) -> some UIViewController {
@@ -331,4 +305,3 @@ struct LNIMSearchViewControllerPreview: UIViewControllerRepresentable {
     LNIMSearchViewControllerPreview()
 })
 #endif
-

+ 1 - 1
Lanu/Views/Settings/LNLanguageSettingPanel.swift

@@ -106,7 +106,7 @@ struct LNLanguageSettingPanelPreview: UIViewRepresentable {
         container.backgroundColor = .lightGray
         
         let view = LNLanguageSettingPanel()
-        view.showIn(container)
+        view.popup(container)
         
         return container
     }

+ 1 - 1
Lanu/Views/Settings/LNSettingsViewController.swift

@@ -57,7 +57,7 @@ extension LNSettingsViewController {
         curLanguageLabel.text = LNAppConfig.shared.curLang.text
         language.onTap {
             let panel = LNLanguageSettingPanel()
-            panel.showIn()
+            panel.popup()
         }
         stackView.addArrangedSubview(language)
         

+ 1 - 1
Lanu/Views/Wallet/Coin/LNCoinViewController.swift

@@ -128,7 +128,7 @@ extension LNCoinViewController {
         let jumpButton = UIButton()
         jumpButton.addAction(UIAction(handler: { _ in
             let panel = LNExchangePanel(exchangeType: .coinToDiamond)
-            panel.showIn()
+            panel.popup()
         }), for: .touchUpInside)
         bg.addSubview(jumpButton)
         jumpButton.snp.makeConstraints { make in

+ 1 - 1
Lanu/Views/Wallet/Diamond/LNDiamondViewController.swift

@@ -128,7 +128,7 @@ extension LNDiamondViewController {
         let jumpButton = UIButton()
         jumpButton.addAction(UIAction(handler: { _ in
             let panel = LNExchangePanel(exchangeType: .diamondToCoin)
-            panel.showIn()
+            panel.popup()
         }), for: .touchUpInside)
         bg.addSubview(jumpButton)
         jumpButton.snp.makeConstraints { make in

+ 7 - 34
Lanu/Views/Wallet/LNWalletViewController.swift

@@ -26,7 +26,6 @@ class LNWalletViewController: LNViewController {
     override func viewDidLoad() {
         super.viewDidLoad()
         
-        showNavigationBar = false
         setupViews()
         
         updateWalletInfo()
@@ -49,6 +48,7 @@ extension LNWalletViewController {
     }
     
     private func setupViews() {
+        showNavigationBar = false
         view.backgroundColor = .primary_1
         
         let topCover = UIImageView()
@@ -82,39 +82,17 @@ extension LNWalletViewController {
     }
     
     private func buildFakeNavBar() -> UIView {
-        let fakeBar = UIView()
-        fakeBar.snp.makeConstraints { make in
-            make.height.equalTo((navigationController?.navigationBar.bounds.height ?? 44) + UIView.statusBarHeight)
-        }
+        let fakeBar = LNFakeNaviBar()
         
-        let barView = UIView()
-        fakeBar.addSubview(barView)
-        barView.snp.makeConstraints { make in
-            make.horizontalEdges.equalToSuperview()
-            make.bottom.equalToSuperview()
-            make.height.equalTo(navigationController?.navigationBar.bounds.height ?? 44)
-        }
-        
-        let closeButton = UIButton(type: .system)
-        closeButton.setImage(UIImage(systemName: "chevron.backward"), for: .normal)
-        closeButton.tintColor = .text_5
-        closeButton.addAction(UIAction(handler: { [weak self] _ in
-            guard let self else { return }
-            navigationController?.popViewController(animated: true)
-        }), for: .touchUpInside)
-        closeButton.translatesAutoresizingMaskIntoConstraints = false
-        barView.addSubview(closeButton)
-        closeButton.snp.makeConstraints { make in
-            make.leading.equalToSuperview().offset(16)
-            make.centerY.equalToSuperview()
-            make.width.height.equalTo(24)
-        }
+        fakeBar.showBackButton()
+        fakeBar.backButton?.setImage(.init(systemName: "chevron.backward"), for: .normal)
+        fakeBar.backButton?.tintColor = .text_5
         
         let titleLabel = UILabel()
         titleLabel.text = .init(key: "A00215")
         titleLabel.font = .heading_h2
         titleLabel.textColor = .text_5
-        barView.addSubview(titleLabel)
+        fakeBar.actionView.addSubview(titleLabel)
         titleLabel.snp.makeConstraints { make in
             make.center.equalToSuperview()
         }
@@ -125,12 +103,7 @@ extension LNWalletViewController {
             guard let self else { return }
             view.pushToWebView(.init(url: .walletHistoryUrl, showNavigationBar: false))
         }), for: .touchUpInside)
-        barView.addSubview(history)
-        history.snp.makeConstraints { make in
-            make.trailing.equalToSuperview().offset(-16)
-            make.centerY.equalToSuperview()
-            make.width.height.equalTo(30)
-        }
+        fakeBar.setRightMenu(history)
         
         return fakeBar
     }