Răsfoiți Sursa

feat: 补充完善个人相关部分页面

陈文艺 3 luni în urmă
părinte
comite
a6ab802a14
100 a modificat fișierele cu 1072 adăugiri și 236 ștergeri
  1. 25 5
      Lanu.xcodeproj/project.pbxproj
  2. 6 0
      Lanu/Assets.xcassets/Profile/Mine/Contents.json
  3. 2 2
      Lanu/Assets.xcassets/Profile/Mine/ic_mine_home.imageset/Contents.json
  4. BIN
      Lanu/Assets.xcassets/Profile/Mine/ic_mine_home.imageset/ic_mine_home@2x.png
  5. BIN
      Lanu/Assets.xcassets/Profile/Mine/ic_mine_home.imageset/ic_mine_home@3x.png
  6. 2 2
      Lanu/Assets.xcassets/Profile/Mine/ic_profile_help.imageset/Contents.json
  7. BIN
      Lanu/Assets.xcassets/Profile/Mine/ic_profile_help.imageset/ic_profile_help@2x.png
  8. BIN
      Lanu/Assets.xcassets/Profile/Mine/ic_profile_help.imageset/ic_profile_help@3x.png
  9. 0 0
      Lanu/Assets.xcassets/Profile/Mine/ic_profile_order.imageset/Contents.json
  10. BIN
      Lanu/Assets.xcassets/Profile/Mine/ic_profile_order.imageset/ic_profile_order@2x.png
  11. BIN
      Lanu/Assets.xcassets/Profile/Mine/ic_profile_order.imageset/ic_profile_order@3x.png
  12. 22 0
      Lanu/Assets.xcassets/Profile/Mine/ic_profile_settings.imageset/Contents.json
  13. BIN
      Lanu/Assets.xcassets/Profile/Mine/ic_profile_settings.imageset/ic_profile_settings@2x.png
  14. BIN
      Lanu/Assets.xcassets/Profile/Mine/ic_profile_settings.imageset/ic_profile_settings@3x.png
  15. 6 0
      Lanu/Assets.xcassets/Profile/Settings/Contents.json
  16. 2 2
      Lanu/Assets.xcassets/Profile/Settings/ic_about.imageset/Contents.json
  17. BIN
      Lanu/Assets.xcassets/Profile/Settings/ic_about.imageset/ic_about@2x.png
  18. BIN
      Lanu/Assets.xcassets/Profile/Settings/ic_about.imageset/ic_about@3x.png
  19. 2 2
      Lanu/Assets.xcassets/Profile/Settings/ic_clean_cache.imageset/Contents.json
  20. BIN
      Lanu/Assets.xcassets/Profile/Settings/ic_clean_cache.imageset/ic_clean_cache@2x.png
  21. BIN
      Lanu/Assets.xcassets/Profile/Settings/ic_clean_cache.imageset/ic_clean_cache@3x.png
  22. 2 2
      Lanu/Assets.xcassets/Profile/Settings/ic_gami.imageset/Contents.json
  23. BIN
      Lanu/Assets.xcassets/Profile/Settings/ic_gami.imageset/ic_gami@2x.png
  24. BIN
      Lanu/Assets.xcassets/Profile/Settings/ic_gami.imageset/ic_gami@3x.png
  25. 22 0
      Lanu/Assets.xcassets/Profile/Settings/ic_logout.imageset/Contents.json
  26. BIN
      Lanu/Assets.xcassets/Profile/Settings/ic_logout.imageset/ic_logout@2x.png
  27. BIN
      Lanu/Assets.xcassets/Profile/Settings/ic_logout.imageset/ic_logout@3x.png
  28. BIN
      Lanu/Assets.xcassets/Profile/ic_order_all.imageset/ic_order_all@2x.png
  29. BIN
      Lanu/Assets.xcassets/Profile/ic_order_all.imageset/ic_order_all@3x.png
  30. BIN
      Lanu/Assets.xcassets/Profile/ic_order_done.imageset/ic_order_done@2x.png
  31. BIN
      Lanu/Assets.xcassets/Profile/ic_order_done.imageset/ic_order_done@3x.png
  32. BIN
      Lanu/Assets.xcassets/Profile/ic_order_pending.imageset/ic_order_pending@2x.png
  33. BIN
      Lanu/Assets.xcassets/Profile/ic_order_pending.imageset/ic_order_pending@3x.png
  34. BIN
      Lanu/Assets.xcassets/Profile/ic_order_refund.imageset/ic_order_refund@2x.png
  35. BIN
      Lanu/Assets.xcassets/Profile/ic_order_refund.imageset/ic_order_refund@3x.png
  36. BIN
      Lanu/Assets.xcassets/Profile/ic_profile_order.imageset/ic_profile_order@2x.png
  37. BIN
      Lanu/Assets.xcassets/Profile/ic_profile_order.imageset/ic_profile_order@3x.png
  38. BIN
      Lanu/Assets.xcassets/Profile/ic_profile_qr.imageset/ic_profile_qr@2x.png
  39. BIN
      Lanu/Assets.xcassets/Profile/ic_profile_qr.imageset/ic_profile_qr@3x.png
  40. 0 22
      Lanu/Assets.xcassets/Profile/ic_profile_qr_arrow.imageset/Contents.json
  41. BIN
      Lanu/Assets.xcassets/Profile/ic_profile_qr_arrow.imageset/ic_profile_qr_arrow@2x.png
  42. BIN
      Lanu/Assets.xcassets/Profile/ic_profile_qr_arrow.imageset/ic_profile_qr_arrow@3x.png
  43. 0 22
      Lanu/Assets.xcassets/Profile/ic_profile_share.imageset/Contents.json
  44. BIN
      Lanu/Assets.xcassets/Profile/ic_profile_share.imageset/ic_profile_share@2x.png
  45. BIN
      Lanu/Assets.xcassets/Profile/ic_profile_share.imageset/ic_profile_share@3x.png
  46. 0 22
      Lanu/Assets.xcassets/Profile/ic_profile_share_arrow.imageset/Contents.json
  47. BIN
      Lanu/Assets.xcassets/Profile/ic_profile_share_arrow.imageset/ic_profile_share_arrow@2x.png
  48. BIN
      Lanu/Assets.xcassets/Profile/ic_profile_share_arrow.imageset/ic_profile_share_arrow@3x.png
  49. 0 22
      Lanu/Assets.xcassets/Profile/ic_profile_wallet.imageset/Contents.json
  50. BIN
      Lanu/Assets.xcassets/Profile/ic_profile_wallet.imageset/ic_profile_wallet@2x.png
  51. BIN
      Lanu/Assets.xcassets/Profile/ic_profile_wallet.imageset/ic_profile_wallet@3x.png
  52. 6 0
      Lanu/Assets.xcassets/common/Coin/Contents.json
  53. 22 0
      Lanu/Assets.xcassets/common/Coin/ic_coin.imageset/Contents.json
  54. BIN
      Lanu/Assets.xcassets/common/Coin/ic_coin.imageset/ic_coin@2x.png
  55. BIN
      Lanu/Assets.xcassets/common/Coin/ic_coin.imageset/ic_coin@3x.png
  56. BIN
      Lanu/Assets.xcassets/common/Edit/ic_edit_fill_white.imageset/ic_avatar_edit@2x.png
  57. BIN
      Lanu/Assets.xcassets/common/Edit/ic_edit_fill_white.imageset/ic_avatar_edit@3x.png
  58. BIN
      Lanu/Assets.xcassets/common/Gender/ic_gender_female.imageset/ic_gender_female@2x.png
  59. BIN
      Lanu/Assets.xcassets/common/Gender/ic_gender_female.imageset/ic_gender_female@3x.png
  60. 2 2
      Lanu/Assets.xcassets/common/Gender/ic_gender_female_with_bg.imageset/Contents.json
  61. BIN
      Lanu/Assets.xcassets/common/Gender/ic_gender_female_with_bg.imageset/ic_gender_female@2x.png
  62. BIN
      Lanu/Assets.xcassets/common/Gender/ic_gender_female_with_bg.imageset/ic_gender_female@3x.png
  63. BIN
      Lanu/Assets.xcassets/common/Gender/ic_gender_male.imageset/ic_gender_male@2x.png
  64. BIN
      Lanu/Assets.xcassets/common/Gender/ic_gender_male.imageset/ic_gender_male@3x.png
  65. 22 0
      Lanu/Assets.xcassets/common/Gender/ic_gender_male_with_bg.imageset/Contents.json
  66. BIN
      Lanu/Assets.xcassets/common/Gender/ic_gender_male_with_bg.imageset/ic_gender_male@2x.png
  67. BIN
      Lanu/Assets.xcassets/common/Gender/ic_gender_male_with_bg.imageset/ic_gender_male@3x.png
  68. 22 0
      Lanu/Assets.xcassets/common/Voice/ic_voice_pause.imageset/Contents.json
  69. BIN
      Lanu/Assets.xcassets/common/Voice/ic_voice_pause.imageset/ic_voice_pause@2x.png
  70. BIN
      Lanu/Assets.xcassets/common/Voice/ic_voice_pause.imageset/ic_voice_pause@3x.png
  71. 22 0
      Lanu/Assets.xcassets/common/ic_qr_green.imageset/Contents.json
  72. BIN
      Lanu/Assets.xcassets/common/ic_qr_green.imageset/ic_qr_green@2x.png
  73. BIN
      Lanu/Assets.xcassets/common/ic_qr_green.imageset/ic_qr_green@3x.png
  74. 63 27
      Lanu/Common/LNPhotosPicker.swift
  75. 18 0
      Lanu/Common/Theme/UIImageView+Theme.swift
  76. 2 2
      Lanu/Common/Views/Gender/LNGenderView.swift
  77. 142 0
      Lanu/Common/Views/ImageUpload/LNFeedbackImageUploadView.swift
  78. 92 0
      Lanu/Common/Views/ImageUpload/LNMultiImagesUploadView.swift
  79. 3 3
      Lanu/Common/Views/LNBirthdayDatePickerPanel.swift
  80. 6 1
      Lanu/Common/Views/LNPopupView.swift
  81. 21 0
      Lanu/Manager/Account/LNCommonAlertView+Settings.swift
  82. 9 0
      Lanu/Manager/GameMate/LNGameMateManager.swift
  83. 7 6
      Lanu/Manager/Profile/LNProfileManager.swift
  84. 35 8
      Lanu/Manager/Profile/Network/LNHttpManager+Profile.swift
  85. 20 1
      Lanu/Manager/Profile/Network/LNProfileResponse.swift
  86. 6 8
      Lanu/Manager/Purchase/LNPurchaseManager.swift
  87. 0 14
      Lanu/Manager/Purchase/LNUserWalletInfo.swift
  88. 1 1
      Lanu/Manager/Relation/LNCommonAlertView+Relation.swift
  89. 67 26
      Lanu/Manager/Relation/LNRelationManager.swift
  90. 28 0
      Lanu/Manager/Relation/Network/LNHttpManager+Relation.swift
  91. 24 0
      Lanu/Manager/Relation/Network/LNRelationResponse.swift
  92. 1 3
      Lanu/Views/Game/MateList/LNGameMateListMenuView.swift
  93. 1 3
      Lanu/Views/IM/Chat/GameMate/LNIMChatGameMateSkillCell.swift
  94. 4 16
      Lanu/Views/IM/Chat/InputMenu/LNIMChatTextInputView.swift
  95. 2 1
      Lanu/Views/IM/Chat/LNIMChatViewController.swift
  96. 16 2
      Lanu/Views/IM/Chat/UserMenu/LNIMChatUserMenuView.swift
  97. 1 3
      Lanu/Views/IM/Notify/Cell/LNIMOfficialMessageCell.swift
  98. 5 6
      Lanu/Views/Login/Setup/LNBaseInfoSetupViewController.swift
  99. 160 0
      Lanu/Views/Profile/Edit/LNEditBioPanel.swift
  100. 151 0
      Lanu/Views/Profile/Edit/LNEditGenderPanel.swift

+ 25 - 5
Lanu.xcodeproj/project.pbxproj

@@ -58,14 +58,17 @@
 				"Common/Theme/UIColor+Theme.swift",
 				"Common/Theme/UIFont+Theme.swift",
 				"Common/Theme/UIImage+Theme.swift",
+				"Common/Theme/UIImageView+Theme.swift",
 				Common/Views/Base/LNNavigationController.swift,
 				Common/Views/Base/LNViewController.swift,
 				Common/Views/Gender/LNGenderView.swift,
 				Common/Views/ImagePreview/LNImagePreviewCell.swift,
 				Common/Views/ImagePreview/LNImagePreviewController.swift,
+				Common/Views/ImageUpload/LNFeedbackImageUploadView.swift,
+				Common/Views/ImageUpload/LNMultiImagesUploadView.swift,
 				Common/Views/LNAutoSizeTextView.swift,
+				Common/Views/LNBirthdayDatePickerPanel.swift,
 				Common/Views/LNCircleProgressView.swift,
-				Common/Views/LNDatePickerPanel.swift,
 				Common/Views/LNPopupView.swift,
 				Common/Views/LNSortedEditView.swift,
 				Common/Views/Menu/LNBottomSheetMenu.swift,
@@ -87,6 +90,7 @@
 				"GoogleService-Info-Release.plist",
 				Localizable.xcstrings,
 				Manager/Account/LNAccountManager.swift,
+				"Manager/Account/LNCommonAlertView+Settings.swift",
 				"Manager/Account/Network/LNHttpManager+Login.swift",
 				Manager/Account/Network/LNLoginResponse.swift,
 				Manager/Deeplink/LNDeeplinkManager.swift,
@@ -120,7 +124,6 @@
 				"Manager/Profile/Network/LNHttpManager+Profile.swift",
 				Manager/Profile/Network/LNProfileResponse.swift,
 				Manager/Purchase/LNPurchaseManager.swift,
-				Manager/Purchase/LNUserWalletInfo.swift,
 				"Manager/Relation/LNCommonAlertView+Relation.swift",
 				Manager/Relation/LNRelationManager.swift,
 				"Manager/Relation/Network/LNHttpManager+Relation.swift",
@@ -150,7 +153,6 @@
 				Views/Home/LNHomeTopMenuView.swift,
 				Views/Home/LNHomeTopTabView.swift,
 				Views/Home/LNHomeViewController.swift,
-				Views/Home/LNLanguageSettingPanel.swift,
 				Views/IM/Chat/Cells/LNIMChatBaseMessageCell.swift,
 				Views/IM/Chat/Cells/LNIMChatImageMessageCell.swift,
 				Views/IM/Chat/Cells/LNIMChatSystemMessageCell.swift,
@@ -191,8 +193,18 @@
 				Views/Order/OrderQR/LNOrderQRTabView.swift,
 				Views/Order/OrderQR/LNOrderShareImageGenerator.swift,
 				Views/Order/OrderQR/LNOrderSkillListPanel.swift,
+				Views/Profile/Edit/LNEditBioPanel.swift,
+				Views/Profile/Edit/LNEditGenderPanel.swift,
+				Views/Profile/Edit/LNEditInterestPanel.swift,
+				Views/Profile/Edit/LNEditNickNamePanel.swift,
+				Views/Profile/Edit/LNEditProfilePhotoWallView.swift,
+				Views/Profile/Edit/LNEditProfileUploadImageView.swift,
 				Views/Profile/Edit/LNEditProfileViewController.swift,
-				Views/Profile/LNMineViewController.swift,
+				Views/Profile/Mine/LNMineFunctionView.swift,
+				Views/Profile/Mine/LNMineQRCodeShareView.swift,
+				Views/Profile/Mine/LNMineUserInfoView.swift,
+				Views/Profile/Mine/LNMineViewController.swift,
+				Views/Profile/Mine/LNMineWalletInfoView.swift,
 				Views/Profile/Post/LNPostShareImageGenerator.swift,
 				Views/Profile/Post/LNPostShareSkillItemView.swift,
 				Views/Profile/Post/LNPostSkillSelectPanel.swift,
@@ -209,11 +221,19 @@
 				Views/Profile/Profile/LNProfileUserDetailView.swift,
 				Views/Profile/Profile/LNProfileUserInfoView.swift,
 				Views/Profile/Profile/LNProfileViewController.swift,
-				Views/Report/LNReportImageUploadView.swift,
+				Views/Profile/Profile/LNProfileVoiceBarView.swift,
+				Views/Profile/Relation/LNUserRelationItemCell.swift,
+				Views/Profile/Relation/LNUserRelationListView.swift,
+				Views/Profile/Relation/LNUserRelationNaviBarView.swift,
+				Views/Profile/Relation/LNUserRelationViewController.swift,
 				Views/Report/LNReportViewController.swift,
 				Views/Search/LNUserSearchHistoryView.swift,
 				Views/Search/LNUserSearchItemCell.swift,
 				Views/Search/LNUserSearchViewController.swift,
+				Views/Settings/LNAboutViewController.swift,
+				Views/Settings/LNHelpCenterViewController.swift,
+				Views/Settings/LNLanguageSettingPanel.swift,
+				Views/Settings/LNSettingsViewController.swift,
 				Views/Web/LNWebViewController.swift,
 			);
 			target = FBFE13BF2EBC39B000DCE6E9 /* Lanu */;

+ 6 - 0
Lanu/Assets.xcassets/Profile/Mine/Contents.json

@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 2 - 2
Lanu/Assets.xcassets/Profile/ic_order_all.imageset/Contents.json → Lanu/Assets.xcassets/Profile/Mine/ic_mine_home.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/Profile/Mine/ic_mine_home.imageset/ic_mine_home@2x.png


BIN
Lanu/Assets.xcassets/Profile/Mine/ic_mine_home.imageset/ic_mine_home@3x.png


+ 2 - 2
Lanu/Assets.xcassets/Profile/ic_order_refund.imageset/Contents.json → Lanu/Assets.xcassets/Profile/Mine/ic_profile_help.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/Profile/Mine/ic_profile_help.imageset/ic_profile_help@2x.png


BIN
Lanu/Assets.xcassets/Profile/Mine/ic_profile_help.imageset/ic_profile_help@3x.png


+ 0 - 0
Lanu/Assets.xcassets/Profile/ic_profile_order.imageset/Contents.json → Lanu/Assets.xcassets/Profile/Mine/ic_profile_order.imageset/Contents.json


BIN
Lanu/Assets.xcassets/Profile/Mine/ic_profile_order.imageset/ic_profile_order@2x.png


BIN
Lanu/Assets.xcassets/Profile/Mine/ic_profile_order.imageset/ic_profile_order@3x.png


+ 22 - 0
Lanu/Assets.xcassets/Profile/Mine/ic_profile_settings.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/Profile/Mine/ic_profile_settings.imageset/ic_profile_settings@2x.png


BIN
Lanu/Assets.xcassets/Profile/Mine/ic_profile_settings.imageset/ic_profile_settings@3x.png


+ 6 - 0
Lanu/Assets.xcassets/Profile/Settings/Contents.json

@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 2 - 2
Lanu/Assets.xcassets/Profile/ic_profile_qr.imageset/Contents.json → Lanu/Assets.xcassets/Profile/Settings/ic_about.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/Profile/Settings/ic_about.imageset/ic_about@2x.png


BIN
Lanu/Assets.xcassets/Profile/Settings/ic_about.imageset/ic_about@3x.png


+ 2 - 2
Lanu/Assets.xcassets/common/Edit/ic_edit_fill_white.imageset/Contents.json → Lanu/Assets.xcassets/Profile/Settings/ic_clean_cache.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/Profile/Settings/ic_clean_cache.imageset/ic_clean_cache@2x.png


BIN
Lanu/Assets.xcassets/Profile/Settings/ic_clean_cache.imageset/ic_clean_cache@3x.png


+ 2 - 2
Lanu/Assets.xcassets/Profile/ic_order_done.imageset/Contents.json → Lanu/Assets.xcassets/Profile/Settings/ic_gami.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/Profile/Settings/ic_gami.imageset/ic_gami@2x.png


BIN
Lanu/Assets.xcassets/Profile/Settings/ic_gami.imageset/ic_gami@3x.png


+ 22 - 0
Lanu/Assets.xcassets/Profile/Settings/ic_logout.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/Profile/Settings/ic_logout.imageset/ic_logout@2x.png


BIN
Lanu/Assets.xcassets/Profile/Settings/ic_logout.imageset/ic_logout@3x.png


BIN
Lanu/Assets.xcassets/Profile/ic_order_all.imageset/ic_order_all@2x.png


BIN
Lanu/Assets.xcassets/Profile/ic_order_all.imageset/ic_order_all@3x.png


BIN
Lanu/Assets.xcassets/Profile/ic_order_done.imageset/ic_order_done@2x.png


BIN
Lanu/Assets.xcassets/Profile/ic_order_done.imageset/ic_order_done@3x.png


BIN
Lanu/Assets.xcassets/Profile/ic_order_pending.imageset/ic_order_pending@2x.png


BIN
Lanu/Assets.xcassets/Profile/ic_order_pending.imageset/ic_order_pending@3x.png


BIN
Lanu/Assets.xcassets/Profile/ic_order_refund.imageset/ic_order_refund@2x.png


BIN
Lanu/Assets.xcassets/Profile/ic_order_refund.imageset/ic_order_refund@3x.png


BIN
Lanu/Assets.xcassets/Profile/ic_profile_order.imageset/ic_profile_order@2x.png


BIN
Lanu/Assets.xcassets/Profile/ic_profile_order.imageset/ic_profile_order@3x.png


BIN
Lanu/Assets.xcassets/Profile/ic_profile_qr.imageset/ic_profile_qr@2x.png


BIN
Lanu/Assets.xcassets/Profile/ic_profile_qr.imageset/ic_profile_qr@3x.png


+ 0 - 22
Lanu/Assets.xcassets/Profile/ic_profile_qr_arrow.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/Profile/ic_profile_qr_arrow.imageset/ic_profile_qr_arrow@2x.png


BIN
Lanu/Assets.xcassets/Profile/ic_profile_qr_arrow.imageset/ic_profile_qr_arrow@3x.png


+ 0 - 22
Lanu/Assets.xcassets/Profile/ic_profile_share.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/Profile/ic_profile_share.imageset/ic_profile_share@2x.png


BIN
Lanu/Assets.xcassets/Profile/ic_profile_share.imageset/ic_profile_share@3x.png


+ 0 - 22
Lanu/Assets.xcassets/Profile/ic_profile_share_arrow.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/Profile/ic_profile_share_arrow.imageset/ic_profile_share_arrow@2x.png


BIN
Lanu/Assets.xcassets/Profile/ic_profile_share_arrow.imageset/ic_profile_share_arrow@3x.png


+ 0 - 22
Lanu/Assets.xcassets/Profile/ic_profile_wallet.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/Profile/ic_profile_wallet.imageset/ic_profile_wallet@2x.png


BIN
Lanu/Assets.xcassets/Profile/ic_profile_wallet.imageset/ic_profile_wallet@3x.png


+ 6 - 0
Lanu/Assets.xcassets/common/Coin/Contents.json

@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 22 - 0
Lanu/Assets.xcassets/common/Coin/ic_coin.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/common/Coin/ic_coin.imageset/ic_coin@2x.png


BIN
Lanu/Assets.xcassets/common/Coin/ic_coin.imageset/ic_coin@3x.png


BIN
Lanu/Assets.xcassets/common/Edit/ic_edit_fill_white.imageset/ic_avatar_edit@2x.png


BIN
Lanu/Assets.xcassets/common/Edit/ic_edit_fill_white.imageset/ic_avatar_edit@3x.png


BIN
Lanu/Assets.xcassets/common/Gender/ic_gender_female.imageset/ic_gender_female@2x.png


BIN
Lanu/Assets.xcassets/common/Gender/ic_gender_female.imageset/ic_gender_female@3x.png


+ 2 - 2
Lanu/Assets.xcassets/Profile/ic_order_pending.imageset/Contents.json → Lanu/Assets.xcassets/common/Gender/ic_gender_female_with_bg.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/common/Gender/ic_gender_female_with_bg.imageset/ic_gender_female@2x.png


BIN
Lanu/Assets.xcassets/common/Gender/ic_gender_female_with_bg.imageset/ic_gender_female@3x.png


BIN
Lanu/Assets.xcassets/common/Gender/ic_gender_male.imageset/ic_gender_male@2x.png


BIN
Lanu/Assets.xcassets/common/Gender/ic_gender_male.imageset/ic_gender_male@3x.png


+ 22 - 0
Lanu/Assets.xcassets/common/Gender/ic_gender_male_with_bg.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/common/Gender/ic_gender_male_with_bg.imageset/ic_gender_male@2x.png


BIN
Lanu/Assets.xcassets/common/Gender/ic_gender_male_with_bg.imageset/ic_gender_male@3x.png


+ 22 - 0
Lanu/Assets.xcassets/common/Voice/ic_voice_pause.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/common/Voice/ic_voice_pause.imageset/ic_voice_pause@2x.png


BIN
Lanu/Assets.xcassets/common/Voice/ic_voice_pause.imageset/ic_voice_pause@3x.png


+ 22 - 0
Lanu/Assets.xcassets/common/ic_qr_green.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/common/ic_qr_green.imageset/ic_qr_green@2x.png


BIN
Lanu/Assets.xcassets/common/ic_qr_green.imageset/ic_qr_green@3x.png


+ 63 - 27
Lanu/Common/LNPhotosPicker.swift

@@ -23,13 +23,41 @@ enum LNImagePickerType: CaseIterable {
 }
 
 
+extension LNBottomSheetMenu {
+    static func showImageSelectMenu(view: UIView? = nil, handler: @escaping LNImagePickerHandler) {
+        let panel = LNBottomSheetMenu()
+        panel.update([
+            LNImagePickerType.camera.title,
+            LNImagePickerType.photo.title,
+            .init(key: "取消")
+        ]) { index, _ in
+            if index == 0 {
+                LNImagePicker.shared.takePictures(from: view, handler: handler)
+            } else if index == 1 {
+                LNImagePicker.shared.selectPhoto(from: view, handler: handler)
+            }
+        }
+        panel.showIn()
+    }
+}
+
+
+typealias LNImagePickerHandler = (UIImage?) -> Void
+private class LNImagePickerController: UIImagePickerController {
+    var handler: LNImagePickerHandler?
+}
+
+
 class LNImagePicker: NSObject {
-    private var handler: ((UIImage?, Data?) -> Void)?
+    static let shared = LNImagePicker()
+    
+    private override init() {
+        super.init()
+    }
 }
 
 extension LNImagePicker {
-    func selectPhoto(from view: UIView, handler: @escaping (UIImage?, Data?) -> Void) {
-        self.handler = handler
+    func selectPhoto(from view: UIView? = nil, handler: @escaping LNImagePickerHandler) {
         // 2. 申请相册权限(iOS 10+ 需授权)
         PHPhotoLibrary.requestAuthorization { [weak self, weak view] status in
             guard let self, let view else { return }
@@ -38,10 +66,10 @@ extension LNImagePicker {
                 
                 switch status {
                 case .authorized: // 已授权,打开相册
-                    self.openPhotoLibrary(view)
+                    self.openPhotoLibrary(view, handler: handler)
                 case .denied, .restricted: // 拒绝授权或受限制
 //                    self.showAlert(message: "请在「设置-隐私-照片」中允许访问相册")
-                    handler(nil, nil)
+                    handler(nil)
                     break
                 case .notDetermined: // 首次请求,系统会自动弹出授权框(无需额外处理)
                     break
@@ -54,67 +82,74 @@ extension LNImagePicker {
         }
     }
     
-    func takePictures(from view: UIView, handler: @escaping (UIImage?, Data?) -> Void) {
-        self.handler = handler
-        
+    func takePictures(from view: UIView? = nil, handler: @escaping LNImagePickerHandler) {
         let status = AVCaptureDevice.authorizationStatus(for: .video)
         
         switch status {
         case .authorized: // 已授权
-            self.openCamera(view)
+            self.openCamera(view, handler: handler)
         case .notDetermined: // 未决定,请求权限
             AVCaptureDevice.requestAccess(for: .video) { [weak self, weak view] granted in
                 guard let self, let view else { return }
                 DispatchQueue.main.async { [weak self, weak view] in
                     guard let self, let view else { return }
                     guard granted else {
-                        handler(nil, nil)
+                        handler(nil)
                         return
                     }
                     
-                    self.openCamera(view)
+                    self.openCamera(view, handler: handler)
                 }
             }
         case .denied, .restricted: // 拒绝/受限
-            handler(nil, nil)
+            handler(nil)
         @unknown default:
-            handler(nil, nil)
+            handler(nil)
         }
     }
 }
 
 extension LNImagePicker {
-    private func buildSelectPhotoPicker() -> UIImagePickerController {
-        let vc = UIImagePickerController()
+    private func buildSelectPhotoPicker(
+        handler: @escaping LNImagePickerHandler
+    ) -> UIImagePickerController {
+        let vc = LNImagePickerController()
         vc.delegate = self
         vc.sourceType = .photoLibrary
         vc.mediaTypes = [UTType.image.identifier]
+        vc.handler = handler
         
         return vc
     }
     
-    private func buildTakePicturesPicker() -> UIImagePickerController {
-        let vc = UIImagePickerController()
+    private func buildTakePicturesPicker(
+        handler: @escaping LNImagePickerHandler
+    ) -> UIImagePickerController {
+        let vc = LNImagePickerController()
         vc.delegate = self
         vc.sourceType = .camera
         vc.allowsEditing = true
+        vc.handler = handler
         
         return vc
     }
     
-    private func openPhotoLibrary(_ view: UIView) {
-        let picker = buildSelectPhotoPicker()
-        view.viewController?.present(picker, animated: true)
+    private func openPhotoLibrary(_ view: UIView?, handler: @escaping LNImagePickerHandler) {
+        let vc = view?.viewController ?? UIView.appKeyWindow?.rootViewController
+        let picker = buildSelectPhotoPicker(handler: handler)
+        vc?.present(picker, animated: true)
     }
     
-    private func openCamera(_ view: UIView) {
-        let picker = buildTakePicturesPicker()
-        view.viewController?.present(picker, animated: true)
+    private func openCamera(_ view: UIView?, handler: @escaping LNImagePickerHandler) {
+        let vc = view?.viewController ?? UIView.appKeyWindow?.rootViewController
+        let picker = buildTakePicturesPicker(handler: handler)
+        vc?.present(picker, animated: true)
     }
 }
 
 extension LNImagePicker: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
     func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
+        guard let vc = picker as? LNImagePickerController else { return }
         // 关闭相册
         picker.dismiss(animated: true)
         
@@ -128,7 +163,7 @@ extension LNImagePicker: UIImagePickerControllerDelegate, UINavigationController
             image = nil
         }
         guard let image else {
-            handler?(nil, nil)
+            vc.handler?(nil)
             return
         }
         
@@ -136,13 +171,14 @@ extension LNImagePicker: UIImagePickerControllerDelegate, UINavigationController
         image.draw(in: .init(origin: .zero, size: image.size))
         let convertToUpImage = UIGraphicsGetImageFromCurrentImageContext();
         UIGraphicsEndImageContext();
-        let data = convertToUpImage?.jpegData(compressionQuality: 0.75)
-        handler?(convertToUpImage, data)
+        vc.handler?(convertToUpImage)
     }
     
     // 取消选择
     func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
+        guard let vc = picker as? LNImagePickerController else { return }
+        
         picker.dismiss(animated: true)
-        handler?(nil, nil)
+        vc.handler?(nil)
     }
 }

+ 18 - 0
Lanu/Common/Theme/UIImageView+Theme.swift

@@ -0,0 +1,18 @@
+//
+//  UIImageView+Theme.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/12/19.
+//
+
+import Foundation
+
+
+extension UIImageView {
+    static func arrowImageView(size: CGFloat) -> UIImageView {
+        let config = UIImage.SymbolConfiguration(pointSize: size)
+        let arrow = UIImageView()
+        arrow.image = .init(systemName: "chevron.forward", withConfiguration: config)
+        return arrow
+    }
+}

+ 2 - 2
Lanu/Common/Views/Gender/LNGenderView.swift

@@ -23,8 +23,8 @@ class LNGenderView: UIView {
     func update(_ gender: LNUserGender, _ age: Int) {
         genderIc.image = switch gender {
         case .unknow: nil
-        case .male: .init(named: "ic_gender_male")
-        case .female: .init(named: "ic_gender_female")
+        case .male: .init(named: "ic_gender_male_with_bg")
+        case .female: .init(named: "ic_gender_female_with_bg")
         }
         
         if gender == .unknow {

+ 142 - 0
Lanu/Common/Views/ImageUpload/LNFeedbackImageUploadView.swift

@@ -0,0 +1,142 @@
+//
+//  LNFeedbackImageUploadView.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/12/22.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+protocol LNFeedbackImageUploadViewDelegate: NSObject {
+    func onFeedbackImageUploadViewClickSelectImage(view: LNFeedbackImageUploadView)
+    func onFeedbackImageUploadViewClickDelete(view: LNFeedbackImageUploadView)
+    func onFeedbackImageUploadView(view: LNFeedbackImageUploadView, didUploadImage url: String)
+}
+
+
+class LNFeedbackImageUploadView: UIView {
+    static let size: CGFloat = 100
+    
+    private let defaultIc = UIImageView()
+    
+    private let container = UIView()
+    
+    private let imageView = UIImageView()
+    
+    private let progressCover = UIView()
+    private let progressView = LNCircleProgressView()
+    
+    private let deleteButton = UIButton()
+    
+    private var curTask: String?
+    private(set) var imageUrl: String?
+    
+    var isDefault: Bool {
+        container.isHidden
+    }
+    
+    weak var delegate: LNFeedbackImageUploadViewDelegate?
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        snp.makeConstraints { make in
+            make.width.height.equalTo(Self.size)
+        }
+        
+        backgroundColor = .fill_2
+        layer.cornerRadius = 10
+        clipsToBounds = true
+        
+        var config = UIImage.SymbolConfiguration(pointSize: 17)
+        defaultIc.image = .init(systemName: "plus", withConfiguration: config)
+        defaultIc.tintColor = .text_3
+        addSubview(defaultIc)
+        defaultIc.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+        }
+        
+        container.isHidden = true
+        addSubview(container)
+        container.snp.makeConstraints { make in
+            make.directionalEdges.equalToSuperview()
+        }
+        
+        container.addSubview(imageView)
+        imageView.snp.makeConstraints { make in
+            make.directionalEdges.equalToSuperview()
+        }
+        
+        progressCover.isHidden = true
+        progressCover.backgroundColor = .black.withAlphaComponent(0.3)
+        container.addSubview(progressCover)
+        progressCover.snp.makeConstraints { make in
+            make.directionalEdges.equalToSuperview()
+        }
+        
+        progressCover.addSubview(progressView)
+        progressView.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+            make.width.height.equalTo(20)
+        }
+        
+        deleteButton.isHidden = true
+        config = UIImage.SymbolConfiguration(pointSize: 7)
+        deleteButton.setImage(.init(systemName: "xmark", withConfiguration: config), for: .normal)
+        deleteButton.tintColor = .white
+        deleteButton.backgroundColor = .black.withAlphaComponent(0.5)
+        deleteButton.layer.cornerRadius = 8
+        deleteButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            if let curTask {
+                LNFileUploader.shared.cancelUpload(taskID: curTask)
+            }
+            delegate?.onFeedbackImageUploadViewClickDelete(view: self)
+        }), for: .touchUpInside)
+        container.addSubview(deleteButton)
+        deleteButton.snp.makeConstraints { make in
+            make.top.equalToSuperview().offset(6)
+            make.trailing.equalToSuperview().offset(-6)
+            make.width.height.equalTo(16)
+        }
+        
+        onTap { [weak self] in
+            guard let self else { return }
+            guard !defaultIc.isHidden else { return }
+            delegate?.onFeedbackImageUploadViewClickSelectImage(view: self)
+        }
+    }
+    
+    func uploadImage(image: UIImage) {
+        defaultIc.isHidden = true
+        container.isHidden = false
+        deleteButton.isHidden = false
+        
+        imageView.image = image
+        progressCover.isHidden = false
+        curTask = LNFileUploader.shared.startUpload(
+            type: .feedback, fileData: image.jpegData(compressionQuality: 1.0)!,
+            suffix: "jpeg")
+        { [weak self] progress in
+            guard let self else { return }
+            progressView.setProgress(CGFloat(progress), animated: true)
+        } completionHandler: { [weak self] url, err in
+            guard let self else { return }
+            if let url, err == nil {
+                imageUrl = url
+                progressCover.isHidden = true
+                curTask = nil
+                delegate?.onFeedbackImageUploadView(view: self, didUploadImage: url)
+            } else {
+                
+            }
+        }
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}

+ 92 - 0
Lanu/Common/Views/ImageUpload/LNMultiImagesUploadView.swift

@@ -0,0 +1,92 @@
+//
+//  LNMultiImagesUploadView.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/12/12.
+//
+import Foundation
+import UIKit
+import SnapKit
+
+
+protocol LNMultiImagesUploadViewDelegate: NSObject {
+    func onMultiImagesUploadView(view: LNMultiImagesUploadView, imageUrlsChanged urls: [String])
+}
+
+
+class LNMultiImagesUploadView: UIView {
+    var maxPhoto = 6
+    private let photoView = LNMultiLineStackView()
+    
+    weak var delegate: LNMultiImagesUploadViewDelegate?
+    
+    var curFileUrls: [String] {
+        photoView.curItemViews.compactMap {
+            ($0 as? LNFeedbackImageUploadView)?.imageUrl
+        }
+    }
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setupViews()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNMultiImagesUploadView: LNFeedbackImageUploadViewDelegate {
+    func onFeedbackImageUploadView(view: LNFeedbackImageUploadView, didUploadImage url: String) {
+        delegate?.onMultiImagesUploadView(view: self, imageUrlsChanged: curFileUrls)
+    }
+    
+    func onFeedbackImageUploadViewClickSelectImage(view: LNFeedbackImageUploadView) {
+        let handler: (UIImage?) -> Void =
+        { [weak self, weak view] image in
+            guard let self else { return }
+            guard let view, let image else { return }
+            view.uploadImage(image: image)
+            if photoView.curItemViews.count < maxPhoto {
+                let nextView = buildReportImage()
+                photoView.append([nextView])
+            }
+        }
+        LNBottomSheetMenu.showImageSelectMenu(view: view, handler: handler)
+    }
+    
+    func onFeedbackImageUploadViewClickDelete(view: LNFeedbackImageUploadView) {
+        if photoView.curItemViews.count == maxPhoto,
+           (photoView.curItemViews.last as? LNFeedbackImageUploadView)?.isDefault != true {
+            let nextView = buildReportImage()
+            photoView.append([nextView])
+        }
+        photoView.remove([view])
+        
+        delegate?.onMultiImagesUploadView(view: self, imageUrlsChanged: curFileUrls)
+    }
+}
+
+extension LNMultiImagesUploadView {
+    private func buildReportImage() -> UIView {
+        let itemView = LNFeedbackImageUploadView()
+        itemView.delegate = self
+        
+        return itemView
+    }
+    
+    private func setupViews() {
+        photoView.columns = 3
+        photoView.spacing = 4
+        photoView.defaultSize = .init(width: LNFeedbackImageUploadView.size, height: LNFeedbackImageUploadView.size)
+        addSubview(photoView)
+        photoView.snp.makeConstraints { make in
+            make.directionalEdges.equalToSuperview()
+            make.height.equalTo(0).priority(.low)
+        }
+        
+        let defaultItem = buildReportImage()
+        photoView.update([defaultItem])
+    }
+}

+ 3 - 3
Lanu/Common/Views/LNDatePickerPanel.swift → Lanu/Common/Views/LNBirthdayDatePickerPanel.swift

@@ -1,5 +1,5 @@
 //
-//  LNDatePickerPanel.swift
+//  LNBirthdayDatePickerPanel.swift
 //  Lanu
 //
 //  Created by OneeChan on 2025/12/3.
@@ -9,7 +9,7 @@ import Foundation
 import UIKit
 import SnapKit
 
-class LNDatePickerPanel: LNPopupView {
+class LNBirthdayDatePickerPanel: LNPopupView {
     var handler: ((TimeInterval) -> Void)?
     
     private let datePicker = UIDatePicker()
@@ -29,7 +29,7 @@ class LNDatePickerPanel: LNPopupView {
     }
 }
 
-extension LNDatePickerPanel {
+extension LNBirthdayDatePickerPanel {
     private func setupViews() {
         let header = UIView()
         container.addSubview(header)

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

@@ -18,6 +18,7 @@ enum LNPopupViewHeight {
 class LNPopupView: UIView {
     let container = UIView()
     var containerHeight: LNPopupViewHeight = .auto
+    var touchOutsideToCancel = true
     
     override init(frame: CGRect) {
         super.init(frame: frame)
@@ -74,7 +75,11 @@ extension LNPopupView {
         let bg = UIView()
         bg.onTap { [weak self] in
             guard let self else { return }
-            self.dismiss()
+            if touchOutsideToCancel {
+                self.dismiss()
+            } else {
+                endEditing(true)
+            }
         }
         insertSubview(bg, at: 0)
         bg.snp.makeConstraints { make in

+ 21 - 0
Lanu/Manager/Account/LNCommonAlertView+Settings.swift

@@ -0,0 +1,21 @@
+//
+//  LNCommonAlertView+Settings.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/12/22.
+//
+
+import Foundation
+
+
+extension LNCommonAlertView {
+    static func showLogoutAlert() {
+        let panel = LNCommonAlertView()
+        panel.titleLabel.text = .init(key: "是否退出登陆?")
+        panel.setConfirm(.init(key: "确认")) {
+            LNAccountManager.shared.logout()
+        }
+        panel.setCancel(.init(key: "取消"))
+        panel.popup()
+    }
+}

+ 9 - 0
Lanu/Manager/GameMate/LNGameMateManager.swift

@@ -23,6 +23,15 @@ class LNGameMateManager {
     
     fileprivate var myGameMateInfo: LNGameMateInfoResponse?
     
+    func gameCategory(for code: String) -> LNGameCategoryItemVO? {
+        for type in curGameTypes {
+            if let item = type.children.first(where: { $0.code == code }) {
+                return item
+            }
+        }
+        return nil
+    }
+    
     private init() {
         LNEventDeliver.addObserver(self)
     }

+ 7 - 6
Lanu/Manager/Profile/LNProfileManager.swift

@@ -89,19 +89,20 @@ extension LNProfileManager {
             if let wallet = res.wallet {
                 LNPurchaseManager.shared.updateWalletInfo(wallet)
             }
+            if let relation = res.userFollowStat {
+                LNRelationManager.shared.updateMyRelationInfo(relation)
+            }
         }
     }
     
-    func modifyMyProfile(age: Int? = nil, avatar: String? = nil,
-                         nickname: String? = nil, gender: Int? = nil,
-                         voiceBar: String? = nil, queue: DispatchQueue = .main,
+    func modifyMyProfile(config: LNProfileUpdateConfig, queue: DispatchQueue = .main,
                          completion: @escaping (Bool) -> Void) {
-        LNHttpManager.shared.modifyMyProfile(age: age, avatar: avatar,
-                                             nickname: nickname, gender: gender,
-                                             voiceBar: voiceBar) { [weak self] err in
+        LNHttpManager.shared.modifyMyProfile(
+            config: config) { [weak self] err in
             guard let self else { return }
             if err == nil {
                 reloadMyProfile()
+                LNGameMateManager.shared.getUserGameMateInfo(uid: myUid) { _ in }
             }
             queue.asyncIfNotGlobal {
                 completion(err == nil)

+ 35 - 8
Lanu/Manager/Profile/Network/LNHttpManager+Profile.swift

@@ -13,31 +13,58 @@ let kNetPath_Profile_EditMyInfo = "/user/my/info/edit"
 
 let kNetPath_Profile_UsersInfo = "/user/get/infos"
 
+
+class LNProfileUpdateConfig {
+    var age: Int?
+    var avatar: String?
+    var nickName: String?
+    var gender: LNUserGender?
+    var voiceBar: String?
+    var voiceDuration: Double?
+    var birthday: String?
+    var interest: [String]?
+    var bio: String?
+    var photos: [String]?
+}
+
 extension LNHttpManager {
     func getMyProfile(completion: @escaping (LNMyProfileResponseVO?, LNHttpError?) -> Void) {
         post(path: kNetPath_Profile_MyInfo, completion: completion)
     }
     
-    func modifyMyProfile(age: Int? = nil, avatar: String? = nil,
-                         nickname: String? = nil, gender: Int? = nil,
-                         voiceBar: String? = nil,
+    func modifyMyProfile(config: LNProfileUpdateConfig,
                          completion: @escaping (LNHttpError?) -> Void) {
         var params: [String: Any] = [:]
-        if let age {
+        if let age = config.age {
             params["age"] = age
         }
-        if let avatar {
+        if let avatar = config.avatar {
             params["avatar"] = avatar
         }
-        if let nickname {
+        if let nickname = config.nickName {
             params["nickname"] = nickname
         }
-        if let gender {
+        if let gender = config.gender {
             params["gender"] = gender
         }
-        if let voiceBar {
+        if let voiceBar = config.voiceBar {
             params["voiceBar"] = voiceBar
         }
+        if let voidDuration = config.voiceDuration {
+            params["voiceBarDuration"] = voidDuration
+        }
+        if let birthday = config.birthday {
+            params["birthday"] = birthday
+        }
+        if let interest = config.interest {
+            params["interestCateGores"] = interest
+        }
+        if let intro = config.bio {
+            params["intro"] = intro
+        }
+        if let photos = config.photos {
+            params["photos"] = photos
+        }
         guard !params.isEmpty else {
             completion(nil)
             return

+ 20 - 1
Lanu/Manager/Profile/Network/LNProfileResponse.swift

@@ -12,10 +12,18 @@ enum LNUserGender: Int, Decodable {
     case unknow = 0
     case male = 1
     case female = 2
+    
+    var desc: String {
+        switch self {
+        case .unknow: .init(key: "保密")
+        case .male: .init(key: "男")
+        case .female: .init(key: "女")
+        }
+    }
 }
 
 @AutoCodable
-class LNUserProfileVO: Decodable {
+class LNUserProfileVO: Decodable, Copyable {
     var userNo: String = ""
     var avatar: String = ""
     var nickname: String = ""
@@ -35,12 +43,23 @@ class LNUserProfileVO: Decodable {
 class LNUserWalletVO: Decodable {
     var diamond: Int = 0
     var goldCoin: Int = 0
+    
+    init() { }
+}
+
+@AutoCodable
+class LNUserRelationVO: Decodable {
+    var fansCount: Int = 0
+    var followCount: Int = 0
+    
+    init() { }
 }
 
 @AutoCodable
 class LNMyProfileResponseVO: Decodable {
     var userProfile: LNUserProfileVO?
     var wallet: LNUserWalletVO?
+    var userFollowStat: LNUserRelationVO?
 }
 
 @AutoCodable

+ 6 - 8
Lanu/Manager/Purchase/LNPurchaseManager.swift

@@ -8,29 +8,27 @@
 import Foundation
 
 
-var myWalletInfo: LNUserWalletInfo? {
+var myWalletInfo: LNUserWalletVO {
     LNPurchaseManager.shared.myWalletInfo
 }
 
 
 protocol LNPurchaseManagerNotify {
-    func onUserWalletInfoChanged(info: LNUserWalletInfo)
+    func onUserWalletInfoChanged(info: LNUserWalletVO)
 }
 
 
 class LNPurchaseManager {
     static let shared = LNPurchaseManager()
     
-    fileprivate var myWalletInfo: LNUserWalletInfo?
+    private(set) var myWalletInfo: LNUserWalletVO = LNUserWalletVO()
     
     private init() { }
 }
 
 extension LNPurchaseManager {
     func updateWalletInfo(_ info: LNUserWalletVO) {
-        myWalletInfo = LNUserWalletInfo()
-        myWalletInfo?.diamond = info.diamond
-        myWalletInfo?.goldCoin = info.goldCoin
+        myWalletInfo = info
         
         notifyWalletInfoChanged()
     }
@@ -40,13 +38,13 @@ extension LNPurchaseManager: LNAccountManagerNotify {
     func onUserLogin() { }
     
     func onUserLogout() {
-        myWalletInfo = nil
+        myWalletInfo = LNUserWalletVO()
     }
 }
 
 extension LNPurchaseManager {
     private func notifyWalletInfoChanged() {
-        guard let info = myWalletInfo else { return }
+        let info = myWalletInfo
         LNEventDeliver.notifyEvent { ($0 as? LNPurchaseManagerNotify)?.onUserWalletInfoChanged(info: info) }
     }
 }

+ 0 - 14
Lanu/Manager/Purchase/LNUserWalletInfo.swift

@@ -1,14 +0,0 @@
-//
-//  LNUserWalletInfo.swift
-//  Lanu
-//
-//  Created by OneeChan on 2025/11/26.
-//
-
-import Foundation
-
-
-class LNUserWalletInfo {
-    var diamond: Int = 0
-    var goldCoin: Int = 0
-}

+ 1 - 1
Lanu/Manager/Relation/LNCommonAlertView+Relation.swift

@@ -25,7 +25,7 @@ extension LNCommonAlertView {
         panel.titleLabel.text = .init(key: "拉黑后将不会收到對方消息且不再推荐對方的动态,确定拉黑?")
         panel.titleLabel.numberOfLines = 0
         panel.setConfirm {
-            //        LNRelationManager.shared.operateFollow(uid: uid, follow: false, handler: nil)
+            LNRelationManager.shared.blackListUser(uid: uid, black: true, handler: nil)
         }
         panel.setCancel()
         panel.popup()

+ 67 - 26
Lanu/Manager/Relation/LNRelationManager.swift

@@ -10,34 +10,39 @@ import Foundation
 
 protocol LNRelationManagerNotify {
     func onUserRelationChanged(uid: String, follow: Bool)
+    func onMyRelationInfoChanged()
+}
+extension LNRelationManagerNotify {
+    func onUserRelationChanged(uid: String, follow: Bool) {}
+    func onMyRelationInfoChanged() {}
+}
+
+
+var myRelationInfo: LNUserRelationVO {
+    LNRelationManager.shared.myRelationInfo
+}
+
+
+struct LNUserRelationShip: OptionSet, Decodable {
+    let rawValue: UInt8
+    
+    static let followed = LNUserRelationShip(rawValue: 1<<0)
+    static let fans = LNUserRelationShip(rawValue: 1<<1)
 }
 
 
 class LNRelationManager {
     static var shared = LNRelationManager()
     
-    private var relationMap: [String: Bool] = [:]
-    private let lock = NSLock()
+    private(set) var myRelationInfo: LNUserRelationVO = LNUserRelationVO()
     
-    func hasFollow(uid: String) -> Bool {
-        lock.lock()
-        let follow = relationMap[uid] == true
-        lock.unlock()
-        
-        return follow
+    private init() {
+        LNEventDeliver.addObserver(self)
     }
     
-    func updateFollowState(uid: String, follow: Bool) {
-        lock.lock()
-        let changed = relationMap[uid] != follow
-        if changed {
-            relationMap[uid] = follow
-        }
-        lock.unlock()
-        
-        if changed {
-            notifyRelationChanged(uid: uid)
-        }
+    func updateMyRelationInfo(_ info: LNUserRelationVO) {
+        myRelationInfo = info
+        notifyUserRelationInfoChanged()
     }
     
     func operateFollow(uid: String, follow: Bool,
@@ -46,23 +51,59 @@ class LNRelationManager {
         LNHttpManager.shared.operateFollow(uid: uid, follow: follow) { [weak self] err in
             guard let self else { return }
             if err == nil {
-                lock.lock()
-                relationMap[uid] = follow
-                lock.unlock()
-                notifyRelationChanged(uid: uid)
+                notifyRelationChanged(uid: uid, follow: follow)
             }
             queue.asyncIfNotGlobal {
                 handler?(err == nil)
             }
         }
     }
+    
+    func getUserFollowList(next: String, queue: DispatchQueue = .main,
+                           handler: @escaping ([LNRelationUserVO], String?) -> Void) {
+        LNHttpManager.shared.getUserFollowList(size: 30, next: next) { res, err in
+            queue.asyncIfNotGlobal {
+                handler(res?.list ?? [], res?.next)
+            }
+        }
+    }
+    
+    func getUserFansList(next: String, queue: DispatchQueue = .main,
+                         handler: @escaping ([LNRelationUserVO], String?) -> Void) {
+        LNHttpManager.shared.getUserFansList(size: 30, next: next) { res, err in
+            queue.asyncIfNotGlobal {
+                handler(res?.list ?? [], res?.next)
+            }
+        }
+    }
 }
 
 extension LNRelationManager {
-    private func notifyRelationChanged(uid: String) {
-        let hasFollow = hasFollow(uid: uid)
+    func blackListUser(uid: String, black: Bool, queue: DispatchQueue = .main, handler: ((Bool) -> Void)?) {
+        LNHttpManager.shared.blackListUser(uid: uid, black: black) { err in
+            queue.asyncIfNotGlobal {
+                handler?(err == nil)
+            }
+        }
+    }
+}
+
+extension LNRelationManager: LNAccountManagerNotify {
+    func onUserLogout() {
+        myRelationInfo = LNUserRelationVO()
+    }
+}
+
+extension LNRelationManager {
+    private func notifyRelationChanged(uid: String, follow: Bool) {
+        LNEventDeliver.notifyEvent {
+            ($0 as? LNRelationManagerNotify)?.onUserRelationChanged(uid: uid, follow: follow)
+        }
+    }
+    
+    private func notifyUserRelationInfoChanged() {
         LNEventDeliver.notifyEvent {
-            ($0 as? LNRelationManagerNotify)?.onUserRelationChanged(uid: uid, follow: hasFollow)
+            ($0 as? LNRelationManagerNotify)?.onMyRelationInfoChanged()
         }
     }
 }

+ 28 - 0
Lanu/Manager/Relation/Network/LNHttpManager+Relation.swift

@@ -9,7 +9,10 @@ import Foundation
 
 
 let kNetPath_Relation_Follow = "/user/follow"
+let kNetPath_Relation_FollowList = "/user/relateion/myFollows"
+let kNetPath_Relation_FansList = "/user/relateion/fans"
 
+let kNetPath_Relation_BlackList = "/im/pullInBlacklist"
 
 extension LNHttpManager {
     func operateFollow(uid: String, follow: Bool, completion: @escaping (LNHttpError?) -> Void) {
@@ -18,4 +21,29 @@ extension LNHttpManager {
             "follow": follow
         ], completion: completion)
     }
+    
+    func blackListUser(uid: String, black: Bool, completion: @escaping (LNHttpError?) -> Void) {
+        post(path: kNetPath_Relation_BlackList, params: [
+            "userNo": uid,
+            "black": black
+        ], completion: completion)
+    }
+}
+
+extension LNHttpManager {
+    func getUserFollowList(size: Int, next: String,
+                           completion: @escaping (LNUserFollowListResponse?, LNHttpError?) -> Void) {
+        post(path: kNetPath_Relation_FollowList, params: [
+            "size": size,
+            "next": next
+        ], completion: completion)
+    }
+    
+    func getUserFansList(size: Int, next: String,
+                         completion: @escaping (LNUserFansListResponse?, LNHttpError?) -> Void) {
+        post(path: kNetPath_Relation_FansList, params: [
+            "size": size,
+            "next": next
+        ], completion: completion)
+    }
 }

+ 24 - 0
Lanu/Manager/Relation/Network/LNRelationResponse.swift

@@ -6,3 +6,27 @@
 //
 
 import Foundation
+import AutoCodable
+
+
+@AutoCodable
+class LNRelationUserVO: Decodable {
+    var userNo: String = ""
+    var relateion: LNUserRelationShip = LNUserRelationShip(rawValue: 0)
+    var avatar: String = ""
+    var nickname: String = ""
+    var gender: LNUserGender = .unknow
+    var age: Int = 0
+}
+
+@AutoCodable
+class LNUserFollowListResponse: Decodable {
+    var list: [LNRelationUserVO] = []
+    var next: String = ""
+}
+
+@AutoCodable
+class LNUserFansListResponse: Decodable {
+    var list: [LNRelationUserVO] = []
+    var next: String = ""
+}

+ 1 - 3
Lanu/Views/Game/MateList/LNGameMateListMenuView.swift

@@ -92,9 +92,7 @@ extension LNGameMateListMenuView {
             make.leading.equalToSuperview()
         }
         
-        let config = UIImage.SymbolConfiguration(pointSize: 14)
-        let arrow = UIImageView()
-        arrow.image = .init(systemName: "chevron.forward", withConfiguration: config)
+        let arrow = UIImageView.arrowImageView(size: 14)
         arrow.tintColor = .text_3
         find.addSubview(arrow)
         arrow.snp.makeConstraints { make in

+ 1 - 3
Lanu/Views/IM/Chat/GameMate/LNIMChatGameMateSkillCell.swift

@@ -192,9 +192,7 @@ extension LNIMChatGameMateSkillCell {
             make.top.equalToSuperview()
         }
         
-        let config = UIImage.SymbolConfiguration(pointSize: 10)
-        let arrow = UIImageView()
-        arrow.image = .init(systemName: "chevron.forward", withConfiguration: config)
+        let arrow = UIImageView.arrowImageView(size: 10)
         arrow.tintColor = .text_4
         infoView.addSubview(arrow)
         arrow.snp.makeConstraints { make in

+ 4 - 16
Lanu/Views/IM/Chat/InputMenu/LNIMChatTextInputView.swift

@@ -36,7 +36,6 @@ class LNIMChatTextInputView: UIView {
     private var hideCameraConstraint: Constraint?
     
     private let sendButton = UIButton()
-    private let imagePicker = LNImagePicker()
     
     weak var delegate: LNIMChatTextInputViewDelegate?
     weak var viewModel: LNIMChatViewModel?
@@ -59,24 +58,13 @@ class LNIMChatTextInputView: UIView {
 
 extension LNIMChatTextInputView {
     private func inputImage() {
-        let handler: (UIImage?, Data?) -> Void = { [weak self] image, data in
-            guard let self, let data else { return }
+        let handler: (UIImage?) -> Void = { [weak self] image in
+            guard let self else { return }
+            guard let data = image?.jpegData(compressionQuality: 1.0) else { return }
             viewModel?.sendImageMessage(imageData: data)
         }
         
-        let panel = LNBottomSheetMenu()
-        panel.update([
-            LNImagePickerType.camera.title,
-            LNImagePickerType.photo.title
-        ]) { [weak self] index, text in
-            guard let self else { return }
-            if index == 0 {
-                imagePicker.takePictures(from: self, handler: handler)
-            } else if index == 1 {
-                imagePicker.selectPhoto(from: self, handler: handler)
-            }
-        }
-        panel.showIn(self)
+        LNBottomSheetMenu.showImageSelectMenu(view: self, handler: handler)
     }
 }
 

+ 2 - 1
Lanu/Views/IM/Chat/LNIMChatViewController.swift

@@ -50,6 +50,8 @@ class LNIMChatViewController: LNViewController {
     override func viewWillDisappear(_ animated: Bool) {
         super.viewWillDisappear(animated)
         
+        view.endEditing(true)
+        
         viewModel.cleanUnread()
     }
 }
@@ -213,7 +215,6 @@ extension LNIMChatViewController {
             make.bottom.equalTo(bottomMenu.snp.top)
         }
         
-        
         skillView.viewModel = viewModel
         orderView.addSubview(skillView)
         skillView.snp.makeConstraints { make in

+ 16 - 2
Lanu/Views/IM/Chat/UserMenu/LNIMChatUserMenuView.swift

@@ -49,8 +49,8 @@ extension LNIMChatUserMenuView {
         let menus = [
             buildMute(),
             
-            buildLine(),
-            buildRemark(),
+//            buildLine(),
+//            buildRemark(),
             
             buildLine(),
             buildBlack(),
@@ -168,6 +168,13 @@ extension LNIMChatUserMenuView {
             make.trailing.equalToSuperview().offset(-16)
         }
         
+        container.onTap { [weak self] in
+            guard let self else { return }
+            guard let uid = viewModel?.userId else { return }
+            dismiss()
+            LNCommonAlertView.showBlackAlert(uid: uid)
+        }
+        
         return container
     }
     
@@ -204,6 +211,13 @@ extension LNIMChatUserMenuView {
             make.trailing.equalToSuperview().offset(-16)
         }
         
+        container.onTap { [weak self] in
+            guard let self else { return }
+            guard let uid = viewModel?.userId else { return }
+            dismiss()
+            pushToReport(uid: uid)
+        }
+        
         return container
     }
     

+ 1 - 3
Lanu/Views/IM/Notify/Cell/LNIMOfficialMessageCell.swift

@@ -127,9 +127,7 @@ extension LNIMOfficialMessageCell {
             make.leading.equalToSuperview().offset(12)
         }
         
-        let config = UIImage.SymbolConfiguration(pointSize: 14)
-        let arrow = UIImageView()
-        arrow.image = .init(systemName: "chevron.forward", withConfiguration: config)
+        let arrow = UIImageView.arrowImageView(size: 14)
         arrow.tintColor = .text_4
         jumpView.addSubview(arrow)
         arrow.snp.makeConstraints { make in

+ 5 - 6
Lanu/Views/Login/Setup/LNBaseInfoSetupViewController.swift

@@ -51,7 +51,6 @@ class LNBaseInfoSetupViewController: LNViewController {
     private let nextButton = UIButton()
     
     private var randomAvatars: [String] = []
-    private let imagePicker = LNImagePicker()
     private var curUrl: String? {
         didSet {
             checkNextButtonEnable()
@@ -84,8 +83,8 @@ extension LNBaseInfoSetupViewController {
     }
     
     private func changeAvatar() {
-        let handler: (UIImage?, Data?) -> Void = { [weak self] image, data in
-            guard let self, let data else { return }
+        let handler: (UIImage?) -> Void = { [weak self] image in
+            guard let self, let data = image?.jpegData(compressionQuality: 1.0) else { return }
             avatar.image = image
             LNFileUploader.shared.startUpload(
                 type: .avatar, fileData: data,
@@ -103,9 +102,9 @@ extension LNBaseInfoSetupViewController {
         ]) { [weak self] index, text in
             guard let self else { return }
             if index == 0 {
-                imagePicker.takePictures(from: view, handler: handler)
+                LNImagePicker.shared.takePictures(from: view, handler: handler)
             } else if index == 1 {
-                imagePicker.selectPhoto(from: view, handler: handler)
+                LNImagePicker.shared.selectPhoto(from: view, handler: handler)
             } else if index == 2 {
                 guard let item = randomAvatars.randomElement() else { return }
                 avatar.sd_setImage(with: URL(string: item))
@@ -332,7 +331,7 @@ extension LNBaseInfoSetupViewController {
         }
         container.onTap { [weak self] in
             guard let self else { return }
-            let panel = LNDatePickerPanel()
+            let panel = LNBirthdayDatePickerPanel()
             if let curDate {
                 panel.setDefault(curDate)
             }

+ 160 - 0
Lanu/Views/Profile/Edit/LNEditBioPanel.swift

@@ -0,0 +1,160 @@
+//
+//  LNEditBioPanel.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/12/19.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNEditBioPanel: LNPopupView {
+    private let textView = UITextView()
+    private let textLengthLabel = UILabel()
+    
+    private let maxInput = 100
+    private let placeholderLabel = UILabel()
+    
+    private let confirmButton = UIButton()
+    
+    var handler: ((String) -> Void)?
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setupViews()
+    }
+    
+    func update(_ bio: String) {
+        textView.text = bio
+        textViewDidChange(textView)
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNEditBioPanel: UITextViewDelegate {
+    func textViewDidChange(_ textView: UITextView) {
+        placeholderLabel.isHidden = !textView.text.isEmpty
+        
+        if textView.text.count > maxInput {
+            textView.text = String(textView.text.prefix(maxInput))
+        }
+        textLengthLabel.text = "\(textView.text.count)/\(maxInput)"
+        
+        if textView.text.isEmpty, confirmButton.isEnabled {
+            confirmButton.isEnabled = false
+            confirmButton.setBackgroundImage(nil, for: .normal)
+        } else if confirmButton.backgroundImage(for: .normal) == nil {
+            confirmButton.isEnabled = true
+            confirmButton.setBackgroundImage(.primary_8, for: .normal)
+        }
+    }
+}
+
+extension LNEditBioPanel {
+    private func setupViews() {
+        let titleLabel = UILabel()
+        titleLabel.font = .heading_h3
+        titleLabel.textColor = .text_5
+        titleLabel.text = .init(key: "个性签名")
+        container.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.top.equalToSuperview().offset(16)
+        }
+        
+        let holder = UIView()
+        holder.backgroundColor = .fill_1
+        holder.layer.cornerRadius = 8
+        container.addSubview(holder)
+        holder.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview().inset(16)
+            make.top.equalTo(titleLabel.snp.bottom).offset(16)
+            make.height.equalTo(119)
+        }
+        
+        textLengthLabel.text = "0/\(maxInput)"
+        textLengthLabel.font = .body_s
+        textLengthLabel.textColor = .text_3
+        holder.addSubview(textLengthLabel)
+        textLengthLabel.snp.makeConstraints { make in
+            make.trailing.equalToSuperview().offset(-12)
+            make.bottom.equalToSuperview().offset(-15)
+        }
+        
+        placeholderLabel.text = .init(key: "有趣的签名会极大提升你的魅力")
+        placeholderLabel.font = .body_m
+        placeholderLabel.textColor = .text_2
+        holder.addSubview(placeholderLabel)
+        placeholderLabel.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(16)
+            make.top.equalToSuperview().offset(19)
+        }
+        
+        textView.font = .body_m
+        textView.textColor = .text_5
+        textView.backgroundColor = .clear
+        textView.delegate = self
+        holder.addSubview(textView)
+        textView.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview().inset(12)
+            make.top.equalToSuperview().offset(12)
+            make.bottom.equalTo(textLengthLabel.snp.top)
+        }
+        
+        confirmButton.setTitle(.init(key: "Save"), for: .normal)
+        confirmButton.setTitleColor(.text_1, for: .normal)
+        confirmButton.titleLabel?.font = .heading_h3
+        confirmButton.setBackgroundImage(.primary_8, for: .normal)
+        confirmButton.layer.cornerRadius = 23.5
+        confirmButton.clipsToBounds = true
+        confirmButton.backgroundColor = .fill_4
+        confirmButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            guard let text = textView.text else { return }
+            endEditing(true)
+            dismiss()
+            handler?(text)
+        }), for: .touchUpInside)
+        container.addSubview(confirmButton)
+        confirmButton.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview().inset(12)
+            make.top.equalTo(holder.snp.bottom).offset(16)
+            make.bottom.equalToSuperview().offset(-safeBottomInset - 5)
+            make.height.equalTo(47)
+        }
+        
+        onTap { [weak self] in
+            guard let self else { return }
+            endEditing(true)
+        }
+    }
+}
+
+#if DEBUG
+
+import SwiftUI
+
+struct LNEditBioPanelPreview: UIViewRepresentable {
+    func makeUIView(context: Context) -> some UIView {
+        let container = UIView()
+        container.backgroundColor = .lightGray
+        
+        let view = LNEditBioPanel()
+        view.showIn(container)
+        
+        return container
+    }
+    
+    func updateUIView(_ uiView: UIViewType, context: Context) { }
+}
+
+#Preview(body: {
+    LNEditBioPanelPreview()
+})
+#endif // DEBUG

+ 151 - 0
Lanu/Views/Profile/Edit/LNEditGenderPanel.swift

@@ -0,0 +1,151 @@
+//
+//  LNEditGenderPanel.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/12/19.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNEditGenderPanel: LNPopupView {
+    private let maleButton = UIButton()
+    private let femaleButton = UIButton()
+    private let confirmButton = UIButton()
+    
+    var curGender: LNUserGender = .unknow {
+        didSet {
+            maleButton.backgroundColor = curGender == .male ? .fill_5 : .primary_1
+            femaleButton.backgroundColor = curGender == .female ? .fill_5 : .primary_1
+            if curGender == .unknow {
+                confirmButton.isEnabled = false
+                confirmButton.setBackgroundImage(nil, for: .normal)
+            } else if confirmButton.backgroundImage(for: .normal) == nil {
+                confirmButton.isEnabled = true
+                confirmButton.setBackgroundImage(.primary_8, for: .normal)
+            }
+        }
+    }
+    
+    var handler: ((LNUserGender) -> Void)?
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setupViews()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNEditGenderPanel {
+    private func setupViews() {
+        let titleLabel = UILabel()
+        titleLabel.font = .heading_h3
+        titleLabel.textColor = .text_5
+        titleLabel.text = .init(key: "性别")
+        container.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.top.equalToSuperview().offset(16)
+        }
+        
+        maleButton.layer.cornerRadius = 23
+        maleButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            curGender = .male
+        }), for: .touchUpInside)
+        container.addSubview(maleButton)
+        maleButton.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview().inset(16)
+            make.top.equalTo(titleLabel.snp.bottom).offset(36)
+            make.height.equalTo(46)
+        }
+        
+        let maleView = UIView()
+        maleView.isUserInteractionEnabled = false
+        maleButton.addSubview(maleView)
+        maleView.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+        }
+        
+        let maleIc = UIImageView()
+        maleIc.image = .init(named: "ic_gender_male")
+        maleView.addSubview(maleIc)
+        maleIc.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalToSuperview()
+        }
+        
+        let maleTitleLabel = UILabel()
+        maleTitleLabel.text = LNUserGender.male.desc
+        maleTitleLabel.font = .heading_h4
+        maleTitleLabel.textColor = .text_5
+        maleView.addSubview(maleTitleLabel)
+        maleTitleLabel.snp.makeConstraints { make in
+            make.verticalEdges.equalToSuperview()
+            make.trailing.equalToSuperview()
+        }
+        
+        femaleButton.layer.cornerRadius = 23
+        femaleButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            curGender = .female
+        }), for: .touchUpInside)
+        container.addSubview(femaleButton)
+        femaleButton.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview().inset(16)
+            make.top.equalTo(maleButton.snp.bottom).offset(12)
+            make.height.equalTo(46)
+        }
+        
+        let femaleView = UIView()
+        femaleView.isUserInteractionEnabled = false
+        femaleButton.addSubview(femaleView)
+        femaleView.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+        }
+        
+        let femaleIc = UIImageView()
+        femaleIc.image = .init(named: "ic_gender_female")
+        femaleView.addSubview(femaleIc)
+        femaleIc.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalToSuperview()
+        }
+        
+        let femaleTitleLabel = UILabel()
+        femaleTitleLabel.text = LNUserGender.female.desc
+        femaleTitleLabel.font = .heading_h4
+        femaleTitleLabel.textColor = .text_5
+        femaleView.addSubview(femaleTitleLabel)
+        femaleTitleLabel.snp.makeConstraints { make in
+            make.verticalEdges.equalToSuperview()
+            make.trailing.equalToSuperview()
+        }
+        
+        confirmButton.setTitle(.init(key: "Save"), for: .normal)
+        confirmButton.setTitleColor(.text_1, for: .normal)
+        confirmButton.titleLabel?.font = .heading_h3
+        confirmButton.setBackgroundImage(.primary_8, for: .normal)
+        confirmButton.layer.cornerRadius = 23.5
+        confirmButton.clipsToBounds = true
+        confirmButton.backgroundColor = .fill_4
+        confirmButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            dismiss()
+            handler?(curGender)
+        }), for: .touchUpInside)
+        container.addSubview(confirmButton)
+        confirmButton.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview().inset(12)
+            make.top.equalTo(femaleButton.snp.bottom).offset(16)
+            make.bottom.equalToSuperview().offset(-safeBottomInset - 5)
+            make.height.equalTo(47)
+        }
+    }
+}

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff