Pārlūkot izejas kodu

feat: 增加个人页以及编辑个人页逻辑

陈文艺 4 mēneši atpakaļ
vecāks
revīzija
db4e25d036
78 mainītis faili ar 1461 papildinājumiem un 86 dzēšanām
  1. 4 0
      Lanu.xcodeproj/project.pbxproj
  2. 6 0
      Lanu/Assets.xcassets/Profile/Contents.json
  3. 22 0
      Lanu/Assets.xcassets/Profile/ic_order_all.imageset/Contents.json
  4. BIN
      Lanu/Assets.xcassets/Profile/ic_order_all.imageset/ic_order_all@2x.png
  5. BIN
      Lanu/Assets.xcassets/Profile/ic_order_all.imageset/ic_order_all@3x.png
  6. 22 0
      Lanu/Assets.xcassets/Profile/ic_order_done.imageset/Contents.json
  7. BIN
      Lanu/Assets.xcassets/Profile/ic_order_done.imageset/ic_order_done@2x.png
  8. BIN
      Lanu/Assets.xcassets/Profile/ic_order_done.imageset/ic_order_done@3x.png
  9. 22 0
      Lanu/Assets.xcassets/Profile/ic_order_pending.imageset/Contents.json
  10. BIN
      Lanu/Assets.xcassets/Profile/ic_order_pending.imageset/ic_order_pending@2x.png
  11. BIN
      Lanu/Assets.xcassets/Profile/ic_order_pending.imageset/ic_order_pending@3x.png
  12. 22 0
      Lanu/Assets.xcassets/Profile/ic_order_refund.imageset/Contents.json
  13. BIN
      Lanu/Assets.xcassets/Profile/ic_order_refund.imageset/ic_order_refund@2x.png
  14. BIN
      Lanu/Assets.xcassets/Profile/ic_order_refund.imageset/ic_order_refund@3x.png
  15. 22 0
      Lanu/Assets.xcassets/Profile/ic_profile_avatar_edit.imageset/Contents.json
  16. BIN
      Lanu/Assets.xcassets/Profile/ic_profile_avatar_edit.imageset/ic_avatar_edit@2x.png
  17. BIN
      Lanu/Assets.xcassets/Profile/ic_profile_avatar_edit.imageset/ic_avatar_edit@3x.png
  18. 22 0
      Lanu/Assets.xcassets/Profile/ic_profile_avatar_edit_gradient.imageset/Contents.json
  19. BIN
      Lanu/Assets.xcassets/Profile/ic_profile_avatar_edit_gradient.imageset/ic_profile_avatar_edit_gradient@2x.png
  20. BIN
      Lanu/Assets.xcassets/Profile/ic_profile_avatar_edit_gradient.imageset/ic_profile_avatar_edit_gradient@3x.png
  21. 22 0
      Lanu/Assets.xcassets/Profile/ic_profile_login_avatar.imageset/Contents.json
  22. BIN
      Lanu/Assets.xcassets/Profile/ic_profile_login_avatar.imageset/ic_profile_login_avatar@2x.png
  23. BIN
      Lanu/Assets.xcassets/Profile/ic_profile_login_avatar.imageset/ic_profile_login_avatar@3x.png
  24. 22 0
      Lanu/Assets.xcassets/Profile/ic_profile_order.imageset/Contents.json
  25. BIN
      Lanu/Assets.xcassets/Profile/ic_profile_order.imageset/ic_profile_order@2x.png
  26. BIN
      Lanu/Assets.xcassets/Profile/ic_profile_order.imageset/ic_profile_order@3x.png
  27. 22 0
      Lanu/Assets.xcassets/Profile/ic_profile_qr.imageset/Contents.json
  28. BIN
      Lanu/Assets.xcassets/Profile/ic_profile_qr.imageset/ic_profile_qr@2x.png
  29. BIN
      Lanu/Assets.xcassets/Profile/ic_profile_qr.imageset/ic_profile_qr@3x.png
  30. 22 0
      Lanu/Assets.xcassets/Profile/ic_profile_qr_arrow.imageset/Contents.json
  31. BIN
      Lanu/Assets.xcassets/Profile/ic_profile_qr_arrow.imageset/ic_profile_qr_arrow@2x.png
  32. BIN
      Lanu/Assets.xcassets/Profile/ic_profile_qr_arrow.imageset/ic_profile_qr_arrow@3x.png
  33. 22 0
      Lanu/Assets.xcassets/Profile/ic_profile_share.imageset/Contents.json
  34. BIN
      Lanu/Assets.xcassets/Profile/ic_profile_share.imageset/ic_profile_share@2x.png
  35. BIN
      Lanu/Assets.xcassets/Profile/ic_profile_share.imageset/ic_profile_share@3x.png
  36. 22 0
      Lanu/Assets.xcassets/Profile/ic_profile_share_arrow.imageset/Contents.json
  37. BIN
      Lanu/Assets.xcassets/Profile/ic_profile_share_arrow.imageset/ic_profile_share_arrow@2x.png
  38. BIN
      Lanu/Assets.xcassets/Profile/ic_profile_share_arrow.imageset/ic_profile_share_arrow@3x.png
  39. 22 0
      Lanu/Assets.xcassets/Profile/ic_profile_wallet.imageset/Contents.json
  40. BIN
      Lanu/Assets.xcassets/Profile/ic_profile_wallet.imageset/ic_profile_wallet@2x.png
  41. BIN
      Lanu/Assets.xcassets/Profile/ic_profile_wallet.imageset/ic_profile_wallet@3x.png
  42. 80 0
      Lanu/Common/LNPhotosPicker.swift
  43. 2 0
      Lanu/Info.plist
  44. 3 2
      Lanu/Manager/Account/LNAccountManager.swift
  45. 1 1
      Lanu/Manager/Account/Network/LNHttpManager+Login.swift
  46. 3 3
      Lanu/Manager/GameMate/LNGameMateManager.swift
  47. 6 6
      Lanu/Manager/GameMate/Network/LNGameMateResponse.swift
  48. 2 2
      Lanu/Manager/Order/LNOrderManager.swift
  49. 1 1
      Lanu/Manager/Order/Network/LNHttpManager+Order.swift
  50. 3 3
      Lanu/Manager/Order/Network/LNOrderResponse.swift
  51. 18 13
      Lanu/Manager/Profile/LNProfileManager.swift
  52. 2 2
      Lanu/Manager/Profile/Network/LNHttpManager+Profile.swift
  53. 8 3
      Lanu/Manager/Profile/Network/LNProfileResponse.swift
  54. 44 0
      Lanu/Manager/Purchase/LNPurchaseManager.swift
  55. 14 0
      Lanu/Manager/Purchase/LNUserWalletInfo.swift
  56. 0 1
      Lanu/SceneDelegate.swift
  57. 5 5
      Lanu/Views/Game/Category/LNGameCategoryListView.swift
  58. 2 2
      Lanu/Views/Game/Category/LNGameCategoryListViewController.swift
  59. 2 2
      Lanu/Views/Game/Category/LNGameCategoryTabItemView.swift
  60. 3 3
      Lanu/Views/Game/Category/LNGameCategoryTabView.swift
  61. 3 3
      Lanu/Views/Game/MateList/LNGameMateListCell.swift
  62. 2 2
      Lanu/Views/Game/MateList/LNGameMateListView.swift
  63. 2 2
      Lanu/Views/Main/GameTab/LNMainActivityTabItemView.swift
  64. 3 3
      Lanu/Views/Main/GameTab/LNMainActivityTabView.swift
  65. 2 2
      Lanu/Views/Main/GameTab/LNMainGameTabItemView.swift
  66. 3 3
      Lanu/Views/Main/GameTab/LNMainGameTabView.swift
  67. 5 5
      Lanu/Views/Main/LNMainGameMatePanel.swift
  68. 4 4
      Lanu/Views/Main/LNMainTopTabView.swift
  69. 1 1
      Lanu/Views/Main/LNMainViewController.swift
  70. 1 1
      Lanu/Views/Order/LNOrderDetailViewController.swift
  71. 6 6
      Lanu/Views/Order/OrderList/LNOrderListItemCell.swift
  72. 5 5
      Lanu/Views/Order/OrderList/LNOrderListViewController.swift
  73. 387 0
      Lanu/Views/Profile/LNEditProfileViewController.swift
  74. 496 0
      Lanu/Views/Profile/LNMineViewController.swift
  75. 43 0
      Lanu/Views/Profile/LNProfileViewController.swift
  76. 1 0
      Lanu/en.lproj/InfoPlist.strings
  77. 1 0
      Lanu/id.lproj/InfoPlist.strings
  78. 1 0
      Lanu/zh-Hans.lproj/InfoPlist.strings

+ 4 - 0
Lanu.xcodeproj/project.pbxproj

@@ -35,6 +35,7 @@
 				AppDelegate.swift,
 				Assets.xcassets,
 				Common/Config/LNAppConfig.swift,
+				Common/LNPhotosPicker.swift,
 				Common/Logger/LNLogger.swift,
 				Common/Logger/LNLoggerFormater.swift,
 				Common/Storage/LNUserDefaults.swift,
@@ -88,6 +89,7 @@
 				"Manager/Profile/Network/LNHttpManager+Profile.swift",
 				Manager/Profile/Network/LNProfileResponse.swift,
 				Manager/Purchase/LNPurchaseManager.swift,
+				Manager/Purchase/LNUserWalletInfo.swift,
 				SceneDelegate.swift,
 				Views/Game/Category/LNGameCategoryListView.swift,
 				Views/Game/Category/LNGameCategoryListViewController.swift,
@@ -114,7 +116,9 @@
 				Views/Order/LNOrderDetailViewController.swift,
 				Views/Order/OrderList/LNOrderListItemCell.swift,
 				Views/Order/OrderList/LNOrderListViewController.swift,
+				Views/Profile/LNEditProfileViewController.swift,
 				Views/Profile/LNMineViewController.swift,
+				Views/Profile/LNProfileViewController.swift,
 			);
 			target = FBFE13BF2EBC39B000DCE6E9 /* Lanu */;
 		};

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

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

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

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

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


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

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

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


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

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

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


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

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

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


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

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

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


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


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

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

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


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


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

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

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


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


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

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

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


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

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

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


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

@@ -0,0 +1,22 @@
+{
+  "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


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

@@ -0,0 +1,22 @@
+{
+  "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


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

@@ -0,0 +1,22 @@
+{
+  "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


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

@@ -0,0 +1,22 @@
+{
+  "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


+ 80 - 0
Lanu/Common/LNPhotosPicker.swift

@@ -0,0 +1,80 @@
+//
+//  LNPhotosPicker.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/11/26.
+//
+
+import Foundation
+import UIKit
+import Photos
+
+class LNImagePicker: NSObject {
+    private var handler: ((UIImage?) -> Void)?
+}
+
+extension LNImagePicker {
+    func selectPhoto(from view: UIView, handler: @escaping (UIImage?) -> Void) {
+        self.handler = handler
+        // 2. 申请相册权限(iOS 10+ 需授权)
+        PHPhotoLibrary.requestAuthorization { [weak self, weak view] status in
+            DispatchQueue.main.async {
+                guard let self, let view else { return }
+                
+                switch status {
+                case .authorized: // 已授权,打开相册
+                    self.openPhotoLibrary(view)
+                case .denied, .restricted: // 拒绝授权或受限制
+//                    self.showAlert(message: "请在「设置-隐私-照片」中允许访问相册")
+                    handler(nil)
+                    break
+                case .notDetermined: // 首次请求,系统会自动弹出授权框(无需额外处理)
+                    break
+                case .limited:
+                    break
+                @unknown default:
+                    break
+                }
+            }
+        }
+    }
+}
+
+extension LNImagePicker {
+    private func buildPicker() -> UIImagePickerController {
+        let vc = UIImagePickerController()
+        vc.delegate = self
+        vc.sourceType = .photoLibrary
+        vc.mediaTypes = [UTType.image.identifier]
+        
+        return vc
+    }
+    
+    private func openPhotoLibrary(_ view: UIView) {
+        let picker = buildPicker()
+        view.viewController?.present(picker, animated: true)
+    }
+}
+
+extension LNImagePicker: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
+    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
+        // 关闭相册
+        picker.dismiss(animated: true)
+        
+        // 获取选择的图片(editedImage:编辑后的图片,originalImage:原始图片)
+        if let editedImage = info[.editedImage] as? UIImage {
+            handler?(editedImage)
+        } else if let originalImage = info[.originalImage] as? UIImage {
+            handler?(originalImage)
+        } else {
+//            showAlert(message: "未能获取选择的照片")
+            handler?(nil)
+        }
+    }
+    
+    // 取消选择
+    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
+        picker.dismiss(animated: true)
+        handler?(nil)
+    }
+}

+ 2 - 0
Lanu/Info.plist

@@ -2,6 +2,8 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
+	<key>NSPhotoLibraryUsageDescription</key>
+	<string>need permission</string>
 	<key>UIAppFonts</key>
 	<array>
 		<string>Poppins-SemiBold.ttf</string>

+ 3 - 2
Lanu/Manager/Account/LNAccountManager.swift

@@ -50,8 +50,7 @@ class LNAccountManager {
                 self.clean()
                 return
             }
-            self.token = res.token
-            self.uid = res.userProfile.id
+            self.token = res
             completion?(true)
             
             self.notifyUserLogin()
@@ -63,6 +62,7 @@ class LNAccountManager {
             guard let self else { return }
             guard err == nil, let response else {
                 completion?(false)
+                self.clean()
                 return
             }
             self.token = response.token
@@ -79,6 +79,7 @@ class LNAccountManager {
             guard let self else { return }
             guard err == nil, let response else {
                 completion(false)
+                self.clean()
                 return
             }
             self.token = response.token

+ 1 - 1
Lanu/Manager/Account/Network/LNHttpManager+Login.swift

@@ -26,7 +26,7 @@ extension LNHttpManager {
     }
 #endif
     
-    func refreshToken(completion: @escaping (LNLoginResponseVO?, LNHttpError?) -> Void) {
+    func refreshToken(completion: @escaping (String?, LNHttpError?) -> Void) {
         post(path: kNetPath_Login_Refresh, completion: completion)
     }
     

+ 3 - 3
Lanu/Manager/GameMate/LNGameMateManager.swift

@@ -10,14 +10,14 @@ import Foundation
 
 class LNGameMateManager {
     static let shared = LNGameMateManager()
-    private(set) var curGameTypes: [LNGameTypeItemBo] = []
+    private(set) var curGameTypes: [LNGameTypeItemVO] = []
     
     private init() {}
 }
 
 extension LNGameMateManager {
     func getGameTypeList(queue: DispatchQueue = .main,
-                         handler: @escaping ([LNGameTypeItemBo]?) -> Void) {
+                         handler: @escaping ([LNGameTypeItemVO]?) -> Void) {
         LNHttpManager.shared.getGameCategories { res, err in
             guard err == nil, let res else {
                 queue.asyncIfNotGlobal {
@@ -37,7 +37,7 @@ extension LNGameMateManager {
     func getGameMateList(
         topCategory: String, category: String?, filter: LNGameMateFilter, size: Int,
         next: String?, queue: DispatchQueue = .main,
-        handler: @escaping ([LNGameMateListItemBo]?, String?) -> Void) {
+        handler: @escaping ([LNGameMateListItemVO]?, String?) -> Void) {
             LNHttpManager.shared.getGameMateList(
                 topCategory: topCategory, category: category,
                 filter: filter, size: size, next: next ?? "") { res, err in

+ 6 - 6
Lanu/Manager/GameMate/Network/LNGameMateResponse.swift

@@ -10,7 +10,7 @@ import AutoCodable
 
 
 @AutoCodable
-class LNGameMateListItemBo: Decodable {
+class LNGameMateListItemVO: Decodable {
     var id: String = ""
     var avatar: String = ""
     var nickname: String = ""
@@ -30,26 +30,26 @@ class LNGameMateListItemBo: Decodable {
 
 @AutoCodable
 class LNGameMateListResponse: Decodable {
-    var list: [LNGameMateListItemBo] = []
+    var list: [LNGameMateListItemVO] = []
     var next: String = ""
 }
 
 @AutoCodable
-class LNGameCategoryItemBo: Decodable {
+class LNGameCategoryItemVO: Decodable {
     var code: String = ""
     var name: String = ""
     var icon: String = ""
 }
 
 @AutoCodable
-class LNGameTypeItemBo: Decodable {
+class LNGameTypeItemVO: Decodable {
     var code: String = ""
     var name: String = ""
     var icon: String = ""
-    var children: [LNGameCategoryItemBo] = []
+    var children: [LNGameCategoryItemVO] = []
 }
 
 @AutoCodable
 class LNGameTypeListResponse: Decodable {
-    var list: [LNGameTypeItemBo] = []
+    var list: [LNGameTypeItemVO] = []
 }

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

@@ -16,7 +16,7 @@ class LNOrderManager {
 
 extension LNOrderManager {
     func getList(size: Int, next: String? = nil, queue: DispatchQueue = .main,
-                 handler: @escaping ([LNOrderListItemBo]?, String) -> Void) {
+                 handler: @escaping ([LNOrderListItemVO]?, String) -> Void) {
         LNHttpManager.shared.getOrderList(size: size, next: next ?? "") { res, err in
             guard err == nil, let res else {
                 queue.asyncIfNotGlobal {
@@ -34,7 +34,7 @@ extension LNOrderManager {
     }
     
     func getOrderDetail(orderId: String, queue: DispatchQueue = .main,
-                        handler: @escaping (LNOrderDetailBo?) -> Void) {
+                        handler: @escaping (LNOrderDetailVO?) -> Void) {
         LNHttpManager.shared.getOrderDetail(orderId: orderId) { res, err in
             guard err == nil, let res else {
                 queue.asyncIfNotGlobal {

+ 1 - 1
Lanu/Manager/Order/Network/LNHttpManager+Order.swift

@@ -20,7 +20,7 @@ extension LNHttpManager {
         post(path: kNetPath_Order_List, params: ["size": size, "next": next], completion: completion)
     }
     
-    func getOrderDetail(orderId: String, completion: @escaping (LNOrderDetailBo?, LNHttpError?) -> Void) {
+    func getOrderDetail(orderId: String, completion: @escaping (LNOrderDetailVO?, LNHttpError?) -> Void) {
         post(path: kNetPath_Order_Detail, params: ["id": orderId], completion: completion)
     }
 }

+ 3 - 3
Lanu/Manager/Order/Network/LNOrderResponse.swift

@@ -18,7 +18,7 @@ enum LNOrderStatus: Int, Decodable {
 
 
 @AutoCodable
-class LNOrderListItemBo: Decodable {
+class LNOrderListItemVO: Decodable {
     var orderId: String = ""
     var avatar: String = ""
     var nickname: String = ""
@@ -33,13 +33,13 @@ class LNOrderListItemBo: Decodable {
 
 @AutoCodable
 class LNOrderListResponse: Decodable {
-    var list: [LNOrderListItemBo] = []
+    var list: [LNOrderListItemVO] = []
     var next: String = ""
 }
 
 
 @AutoCodable
-class LNOrderDetailBo: Decodable {
+class LNOrderDetailVO: Decodable {
     var orderId: String = ""
     var avatar: String = ""
     var nickname: String = ""

+ 18 - 13
Lanu/Manager/Profile/LNProfileManager.swift

@@ -12,14 +12,14 @@ protocol LNProfileManagerNotify {
     func onUserInfoChanged(userInfo: LNUserProfileInfo)
 }
 
-var myUserInfo: LNUserProfileInfo {
+var myUserInfo: LNUserProfileInfo? {
     LNProfileManager.shared.myUserInfo
 }
 
 class LNProfileManager {
     static let shared = LNProfileManager()
     
-    private(set) var myUserInfo: LNUserProfileInfo = LNUserProfileInfo()
+    fileprivate var myUserInfo: LNUserProfileInfo?
     
     private let lock = NSLock()
     private var profileCached: [String: LNUserProfileInfo] = [:]
@@ -60,25 +60,30 @@ extension LNProfileManager {
             guard let self else { return }
             guard err == nil, let res else { return }
             
-            let info = LNUserProfileInfo(profile: res.userProfile)
-            self.updateUserInfo(info: info)
+            if let profile = res.userProfile {
+                let info = LNUserProfileInfo(profile: profile)
+                self.updateUserInfo(info: info)
+            }
+            if let wallet = res.wallet {
+                LNPurchaseManager.shared.updateWalletInfo(wallet)
+            }
         }
     }
     
     func modifyMyProfile(age: Int? = nil, avatar: String? = nil,
                          nickname: String? = nil, gender: Int? = nil,
-                         voiceBar: String? = nil,
-                         completion: @escaping (LNMyProfileResponseVO?) -> Void) {
+                         voiceBar: String? = nil, queue: DispatchQueue = .main,
+                         completion: @escaping (Bool) -> Void) {
         LNHttpManager.shared.modifyMyProfile(age: age, avatar: avatar,
                                              nickname: nickname, gender: gender,
-                                             voiceBar: voiceBar) { [weak self] res, err in
+                                             voiceBar: voiceBar) { [weak self] err in
             guard let self else { return }
-            guard err == nil, let res else {
-                completion(nil)
-                return
+            if err == nil {
+                reloadMyProfile()
+            }
+            queue.asyncIfNotGlobal {
+                completion(err == nil)
             }
-            let info = LNUserProfileInfo(profile: res.userProfile)
-            self.updateUserInfo(info: info)
         }
     }
 }
@@ -102,7 +107,7 @@ extension LNProfileManager: LNUserMainEvent {
     }
     
     func onUserLogout() {
-        myUserInfo = LNUserProfileInfo()
+        myUserInfo = nil
     }
 }
 

+ 2 - 2
Lanu/Manager/Profile/Network/LNHttpManager+Profile.swift

@@ -19,7 +19,7 @@ extension LNHttpManager {
     func modifyMyProfile(age: Int? = nil, avatar: String? = nil,
                          nickname: String? = nil, gender: Int? = nil,
                          voiceBar: String? = nil,
-                         completion: @escaping (LNMyProfileResponseVO?, LNHttpError?) -> Void) {
+                         completion: @escaping (LNHttpError?) -> Void) {
         var params: [String: Any] = [:]
         if let age {
             params["age"] = age
@@ -37,7 +37,7 @@ extension LNHttpManager {
             params["voiceBar"] = voiceBar
         }
         guard !params.isEmpty else {
-            completion(nil, nil)
+            completion(nil)
             return
         }
         post(path: kNetPath_Profile_EditMyInfo, params: params, completion: completion)

+ 8 - 3
Lanu/Manager/Profile/Network/LNProfileResponse.swift

@@ -24,11 +24,16 @@ class LNUserProfileVO: Decodable {
     var gender: LNUserGender = .unknow
     var intro: String = ""
     var playmate: Bool = false
-    
-    init() { }
+}
+
+@AutoCodable
+class LNUserWalletVO: Decodable {
+    var diamond: Int = 0
+    var totalConsume: Int = 0
 }
 
 @AutoCodable
 class LNMyProfileResponseVO: Decodable {
-    var userProfile: LNUserProfileVO = LNUserProfileVO()
+    var userProfile: LNUserProfileVO?
+    var wallet: LNUserWalletVO?
 }

+ 44 - 0
Lanu/Manager/Purchase/LNPurchaseManager.swift

@@ -6,3 +6,47 @@
 //
 
 import Foundation
+
+
+var myWalletInfo: LNUserWalletInfo? {
+    LNPurchaseManager.shared.myWalletInfo
+}
+
+
+protocol LNPurchaseManagerNotify {
+    func onUserWalletInfoChanged(info: LNUserWalletInfo)
+}
+
+
+class LNPurchaseManager {
+    static let shared = LNPurchaseManager()
+    
+    fileprivate var myWalletInfo: LNUserWalletInfo?
+    
+    private init() { }
+}
+
+extension LNPurchaseManager {
+    func updateWalletInfo(_ info: LNUserWalletVO) {
+        myWalletInfo = LNUserWalletInfo()
+        myWalletInfo?.diamond = info.diamond
+        myWalletInfo?.totalConsume = info.totalConsume
+        
+        notifyWalletInfoChanged()
+    }
+}
+
+extension LNPurchaseManager: LNUserMainEvent {
+    func onUserLogin() { }
+    
+    func onUserLogout() {
+        myWalletInfo = nil
+    }
+}
+
+extension LNPurchaseManager {
+    private func notifyWalletInfoChanged() {
+        guard let info = myWalletInfo else { return }
+        LNEventDeliver.notifyEvent { ($0 as? LNPurchaseManagerNotify)?.onUserWalletInfoChanged(info: info) }
+    }
+}

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

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

+ 0 - 1
Lanu/SceneDelegate.swift

@@ -88,7 +88,6 @@ extension SceneDelegate: LNUserMainEvent {
 
 extension SceneDelegate {
     private func autoLoginIfNeed() {
-        return
         guard LNAccountManager.shared.wasLogin,
               LNNetworkMonitor.curState == .available else { return }
         

+ 5 - 5
Lanu/Views/Game/Category/LNGameCategoryListView.swift

@@ -11,7 +11,7 @@ import SnapKit
 
 
 protocol LNGameCategoryListViewDelegate: NSObject {
-    func onCategoryListView(view: LNGameCategoryListView, didScrollTo category: LNGameTypeItemBo)
+    func onCategoryListView(view: LNGameCategoryListView, didScrollTo category: LNGameTypeItemVO)
 }
 
 
@@ -21,7 +21,7 @@ class LNGameCategoryListView: UIView {
     private let stackView = UIStackView()
     private var categoryViews: [UIView] = []
     
-    private var curCategories: [LNGameTypeItemBo] = []
+    private var curCategories: [LNGameTypeItemVO] = []
     
     weak var delegate: LNGameCategoryListViewDelegate?
     
@@ -31,7 +31,7 @@ class LNGameCategoryListView: UIView {
         setupViews()
     }
     
-    func update(categories: [LNGameTypeItemBo]) {
+    func update(categories: [LNGameTypeItemVO]) {
         let old = stackView.arrangedSubviews
         old.forEach {
             stackView.removeArrangedSubview($0)
@@ -47,7 +47,7 @@ class LNGameCategoryListView: UIView {
         curCategories = categories
     }
     
-    func scrollTo(category: LNGameTypeItemBo) {
+    func scrollTo(category: LNGameTypeItemVO) {
         guard let index = curCategories.firstIndex(where: { $0.code == category.code }),
               index < categoryViews.count else {
             return
@@ -115,7 +115,7 @@ extension LNGameCategoryListView {
         }
     }
     
-    private func buildCategoryPanel(category: LNGameTypeItemBo) -> UIView {
+    private func buildCategoryPanel(category: LNGameTypeItemVO) -> UIView {
         let container = UIStackView()
         container.axis = .vertical
         container.spacing = 10

+ 2 - 2
Lanu/Views/Game/Category/LNGameCategoryListViewController.swift

@@ -38,13 +38,13 @@ class LNGameCategoryListViewController: LNViewController {
 }
 
 extension LNGameCategoryListViewController: LNGameCategoryTabViewDelegate {
-    func onGameCategoryTabView(view: LNGameCategoryTabView, didSelect select: LNGameTypeItemBo) {
+    func onGameCategoryTabView(view: LNGameCategoryTabView, didSelect select: LNGameTypeItemVO) {
         listView.scrollTo(category: select)
     }
 }
 
 extension LNGameCategoryListViewController: LNGameCategoryListViewDelegate {
-    func onCategoryListView(view: LNGameCategoryListView, didScrollTo category: LNGameTypeItemBo) {
+    func onCategoryListView(view: LNGameCategoryListView, didScrollTo category: LNGameTypeItemVO) {
         tabView.select(category)
     }
 }

+ 2 - 2
Lanu/Views/Game/Category/LNGameCategoryTabItemView.swift

@@ -15,7 +15,7 @@ class LNGameCategoryTabItemView: UIView {
     private let nameLabel = UILabel()
     private let selectedIc = UIImageView()
     
-    private(set) var item: LNGameTypeItemBo?
+    private(set) var item: LNGameTypeItemVO?
     private(set) var isSelected: Bool = false
     
     override init(frame: CGRect) {
@@ -24,7 +24,7 @@ class LNGameCategoryTabItemView: UIView {
         setupViews()
     }
     
-    func update(_ item: LNGameTypeItemBo) {
+    func update(_ item: LNGameTypeItemVO) {
         nameLabel.text = item.name
         self.item = item
     }

+ 3 - 3
Lanu/Views/Game/Category/LNGameCategoryTabView.swift

@@ -11,7 +11,7 @@ import SnapKit
 
 
 protocol LNGameCategoryTabViewDelegate: NSObject {
-    func onGameCategoryTabView(view: LNGameCategoryTabView, didSelect category: LNGameTypeItemBo)
+    func onGameCategoryTabView(view: LNGameCategoryTabView, didSelect category: LNGameTypeItemVO)
 }
 
 
@@ -28,7 +28,7 @@ class LNGameCategoryTabView: UIView {
         setupViews()
     }
     
-    func update(_ categories: [LNGameTypeItemBo]) {
+    func update(_ categories: [LNGameTypeItemVO]) {
         itemViews.forEach {
             stackView.removeArrangedSubview($0)
             $0.removeFromSuperview()
@@ -52,7 +52,7 @@ class LNGameCategoryTabView: UIView {
         }
     }
     
-    func select(_ category: LNGameTypeItemBo) {
+    func select(_ category: LNGameTypeItemVO) {
         let view = itemViews.first { $0.item?.code == category.code }
         guard let view else { return }
         selectCategory(view)

+ 3 - 3
Lanu/Views/Game/MateList/LNGameMateListCell.swift

@@ -37,7 +37,7 @@ class LNGameMateListCell: UITableViewCell {
         setupViews()
     }
     
-    func update(_ item: LNGameMateListItemBo) {
+    func update(_ item: LNGameMateListItemVO) {
         avatar.sd_setImage(with: URL(string: item.avatar))
         priceLabel.text = "\(item.price)"
         nameLabel.text = item.nickname
@@ -94,8 +94,8 @@ extension LNGameMateListCell {
         container.snp.makeConstraints { make in
             make.leading.equalToSuperview().offset(16)
             make.trailing.equalToSuperview().offset(-16)
-            make.top.equalToSuperview().offset(8)
-            make.bottom.equalToSuperview().offset(-2)
+            make.top.equalToSuperview()
+            make.bottom.equalToSuperview().offset(-10)
         }
         
         let bg = UIImageView()

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

@@ -12,7 +12,7 @@ import Combine
 import MJRefresh
 
 class LNGameMateListView: UIView {
-    private(set) var curMateList: [LNGameMateListItemBo] = []
+    private(set) var curMateList: [LNGameMateListItemVO] = []
     
     private var topCategory: String = ""
     private var category: String?
@@ -127,7 +127,7 @@ extension LNGameMateListView {
         addSubview(listView)
         listView.snp.makeConstraints { make in
             make.leading.trailing.bottom.equalToSuperview()
-            make.top.equalTo(menu.snp.bottom)
+            make.top.equalTo(menu.snp.bottom).offset(10)
         }
     }
     

+ 2 - 2
Lanu/Views/Main/GameTab/LNMainActivityTabItemView.swift

@@ -15,7 +15,7 @@ class LNMainActivityTabItemView: UIView {
     private let nameLabel = UILabel()
     private let selectedIc = UIImageView()
     
-    private(set) var item: LNGameCategoryItemBo?
+    private(set) var item: LNGameCategoryItemVO?
     
     override init(frame: CGRect) {
         super.init(frame: frame)
@@ -23,7 +23,7 @@ class LNMainActivityTabItemView: UIView {
         setupViews()
     }
     
-    func update(_ item: LNGameCategoryItemBo) {
+    func update(_ item: LNGameCategoryItemVO) {
         cover.sd_setImage(with: URL(string: item.icon))
         nameLabel.text = item.name
         self.item = item

+ 3 - 3
Lanu/Views/Main/GameTab/LNMainActivityTabView.swift

@@ -11,13 +11,13 @@ import SnapKit
 
 
 protocol LNMainActivityTabViewDelegate: NSObject {
-    func mainActivityTabView(view: LNMainActivityTabView, didSelect category: LNGameCategoryItemBo?)
+    func mainActivityTabView(view: LNMainActivityTabView, didSelect category: LNGameCategoryItemVO?)
     func mainActivityTabViewClickMore(view: LNMainActivityTabView)
 }
 
 
 class LNMainActivityTabView: UIView {
-    private(set) var curGame: LNGameCategoryItemBo? {
+    private(set) var curGame: LNGameCategoryItemVO? {
         didSet {
             guard oldValue?.code != curGame?.code else { return }
             tabItemViews.forEach {
@@ -38,7 +38,7 @@ class LNMainActivityTabView: UIView {
         setupViews()
     }
     
-    func update(_ games: [LNGameCategoryItemBo]) {
+    func update(_ games: [LNGameCategoryItemVO]) {
         let itemViews = stackView.arrangedSubviews
         itemViews.forEach {
             stackView.removeArrangedSubview($0)

+ 2 - 2
Lanu/Views/Main/GameTab/LNMainGameTabItemView.swift

@@ -15,7 +15,7 @@ class LNMainGameTabItemView: UIView {
     private let selectedBorder = UIImageView()
     private let nameLabel = UILabel()
     
-    private(set) var item: LNGameCategoryItemBo?
+    private(set) var item: LNGameCategoryItemVO?
     
     override init(frame: CGRect) {
         super.init(frame: frame)
@@ -23,7 +23,7 @@ class LNMainGameTabItemView: UIView {
         setupViews()
     }
     
-    func update(_ item: LNGameCategoryItemBo) {
+    func update(_ item: LNGameCategoryItemVO) {
         cover.sd_setImage(with: URL(string: item.icon))
         nameLabel.text = item.name
         self.item = item

+ 3 - 3
Lanu/Views/Main/GameTab/LNMainGameTabView.swift

@@ -11,13 +11,13 @@ import SnapKit
 
 
 protocol LNMainGameTabViewDelegate: NSObject {
-    func mainGameTabView(view: LNMainGameTabView, didSelect category: LNGameCategoryItemBo?)
+    func mainGameTabView(view: LNMainGameTabView, didSelect category: LNGameCategoryItemVO?)
     func mainGameTabViewClickMore(view: LNMainGameTabView)
 }
 
 
 class LNMainGameTabView: UIView {
-    private(set) var curGame: LNGameCategoryItemBo? {
+    private(set) var curGame: LNGameCategoryItemVO? {
         didSet {
             guard oldValue?.code != curGame?.code else { return }
             tabItemViews.forEach {
@@ -39,7 +39,7 @@ class LNMainGameTabView: UIView {
         setupViews()
     }
     
-    func update(_ games: [LNGameCategoryItemBo]) {
+    func update(_ games: [LNGameCategoryItemVO]) {
         let itemViews = stackView.arrangedSubviews
         itemViews.forEach {
             stackView.removeArrangedSubview($0)

+ 5 - 5
Lanu/Views/Main/LNMainGameMatePanel.swift

@@ -11,10 +11,10 @@ import SnapKit
 
 
 class LNMainGameMatePanel: UIView {
-    private var curItem: LNGameTypeItemBo?
+    private var curItem: LNGameTypeItemVO?
     private let listView = LNGameMateListView()
     
-    func prepareFor(_ item: LNGameTypeItemBo) {
+    func prepareFor(_ item: LNGameTypeItemVO) {
         setupViews(item)
         curItem = item
     }
@@ -24,7 +24,7 @@ class LNMainGameMatePanel: UIView {
 }
 
 extension LNMainGameMatePanel: LNMainGameTabViewDelegate {
-    func mainGameTabView(view: LNMainGameTabView, didSelect category: LNGameCategoryItemBo?) {
+    func mainGameTabView(view: LNMainGameTabView, didSelect category: LNGameCategoryItemVO?) {
         guard let curItem else { return }
         listView.reloadList(newTopCategory: curItem.code, newCategory: category?.code)
     }
@@ -35,7 +35,7 @@ extension LNMainGameMatePanel: LNMainGameTabViewDelegate {
 }
 
 extension LNMainGameMatePanel: LNMainActivityTabViewDelegate {
-    func mainActivityTabView(view: LNMainActivityTabView, didSelect category: LNGameCategoryItemBo?) {
+    func mainActivityTabView(view: LNMainActivityTabView, didSelect category: LNGameCategoryItemVO?) {
         guard let curItem else { return }
         listView.reloadList(newTopCategory: curItem.code, newCategory: category?.code)
     }
@@ -46,7 +46,7 @@ extension LNMainGameMatePanel: LNMainActivityTabViewDelegate {
 }
 
 extension LNMainGameMatePanel {
-    private func setupViews(_ item: LNGameTypeItemBo) {
+    private func setupViews(_ item: LNGameTypeItemVO) {
         let tabView: UIView
         if item.code == "0005" {
             let gameTab = LNMainActivityTabView()

+ 4 - 4
Lanu/Views/Main/LNMainTopTabView.swift

@@ -10,12 +10,12 @@ import UIKit
 import SnapKit
 
 protocol LNMainTopTabViewDelegate: NSObject {
-    func mainTopTabView(view: LNMainTopTabView, didSelectAt index: Int, type: LNGameTypeItemBo)
+    func mainTopTabView(view: LNMainTopTabView, didSelectAt index: Int, type: LNGameTypeItemVO)
 }
 
 
 class LNMainTopTabView: UIView {
-    private var titles: [LNGameTypeItemBo] = []
+    private var titles: [LNGameTypeItemVO] = []
     
     private let indicator = UIImageView()
     private let scrollView = UIScrollView()
@@ -30,7 +30,7 @@ class LNMainTopTabView: UIView {
         setupViews()
     }
     
-    func update(_ tabs: [LNGameTypeItemBo]) {
+    func update(_ tabs: [LNGameTypeItemVO]) {
         let old = stackView.arrangedSubviews
         old.forEach {
             stackView.removeArrangedSubview($0)
@@ -79,7 +79,7 @@ class LNMainTopTabView: UIView {
 }
 
 extension LNMainTopTabView {
-    private func handleTabClick(type: LNGameTypeItemBo, tab: UIButton) {
+    private func handleTabClick(type: LNGameTypeItemVO, tab: UIButton) {
         guard let index = tabItemViews.firstIndex(of: tab) else { return }
         selectTab(at: index)
         delegate?.mainTopTabView(view: self, didSelectAt: index, type: type)

+ 1 - 1
Lanu/Views/Main/LNMainViewController.swift

@@ -26,7 +26,7 @@ class LNMainViewController: LNViewController {
 }
 
 extension LNMainViewController: LNMainTopTabViewDelegate {
-    func mainTopTabView(view: LNMainTopTabView, didSelectAt index: Int, type: LNGameTypeItemBo) {
+    func mainTopTabView(view: LNMainTopTabView, didSelectAt index: Int, type: LNGameTypeItemVO) {
         let offsetX: CGFloat = container.bounds.width * CGFloat(index)
         container.setContentOffset(.init(x: offsetX, y: 0), animated: true)
     }

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

@@ -66,7 +66,7 @@ extension LNOrderDetailViewController {
         }
     }
     
-    private func update(_ item: LNOrderDetailBo) {
+    private func update(_ item: LNOrderDetailVO) {
         bottomMenu.subviews.forEach { $0.removeFromSuperview() }
         scoreContainer.subviews.forEach { $0.removeFromSuperview() }
         

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

@@ -11,10 +11,10 @@ import SnapKit
 
 
 protocol LNOrderListItemCellDelegate: NSObject {
-    func onOrderListItemCell(cell: LNOrderListItemCell, clickFinish item: LNOrderListItemBo)
-    func onOrderListItemCell(cell: LNOrderListItemCell, clickDelete item: LNOrderListItemBo)
-    func onOrderListItemCell(cell: LNOrderListItemCell, clickRefund item: LNOrderListItemBo)
-    func onOrderListItemCell(cell: LNOrderListItemCell, clickComment item: LNOrderListItemBo)
+    func onOrderListItemCell(cell: LNOrderListItemCell, clickFinish item: LNOrderListItemVO)
+    func onOrderListItemCell(cell: LNOrderListItemCell, clickDelete item: LNOrderListItemVO)
+    func onOrderListItemCell(cell: LNOrderListItemCell, clickRefund item: LNOrderListItemVO)
+    func onOrderListItemCell(cell: LNOrderListItemCell, clickComment item: LNOrderListItemVO)
 }
 
 
@@ -32,7 +32,7 @@ class LNOrderListItemCell: UITableViewCell {
     private let operationStackView = UIStackView()
     private let totalLabel = UILabel()
 
-    private var curItem: LNOrderListItemBo?
+    private var curItem: LNOrderListItemVO?
     weak var delegate: LNOrderListItemCellDelegate?
     
     override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
@@ -41,7 +41,7 @@ class LNOrderListItemCell: UITableViewCell {
         setupViews()
     }
     
-    func update(_ item: LNOrderListItemBo) {
+    func update(_ item: LNOrderListItemVO) {
         stateContainer.subviews.forEach { $0.removeFromSuperview() }
         operationStackView.arrangedSubviews.forEach {
             operationStackView.removeArrangedSubview($0)

+ 5 - 5
Lanu/Views/Order/OrderList/LNOrderListViewController.swift

@@ -22,7 +22,7 @@ extension UIView {
 class LNOrderListViewController: LNViewController {
     private let tableView = UITableView()
     
-    private var orders: [LNOrderListItemBo] = []
+    private var orders: [LNOrderListItemVO] = []
     private var nextTag: String? = nil
     private let pageSize = 30
     
@@ -50,7 +50,7 @@ extension LNOrderListViewController {
 }
 
 extension LNOrderListViewController: LNOrderListItemCellDelegate {
-    func onOrderListItemCell(cell: LNOrderListItemCell, clickFinish item: LNOrderListItemBo) {
+    func onOrderListItemCell(cell: LNOrderListItemCell, clickFinish item: LNOrderListItemVO) {
         LNOrderManager.shared.finishOrder(orderId: item.orderId) { [weak self] success in
             guard let self else { return }
             if success {
@@ -60,7 +60,7 @@ extension LNOrderListViewController: LNOrderListItemCellDelegate {
         }
     }
     
-    func onOrderListItemCell(cell: LNOrderListItemCell, clickDelete item: LNOrderListItemBo) {
+    func onOrderListItemCell(cell: LNOrderListItemCell, clickDelete item: LNOrderListItemVO) {
         LNOrderManager.shared.deleteOrder(orderId: item.orderId) { [weak self] success in
             guard let self else { return }
             if success {
@@ -70,11 +70,11 @@ extension LNOrderListViewController: LNOrderListItemCellDelegate {
         }
     }
     
-    func onOrderListItemCell(cell: LNOrderListItemCell, clickRefund item: LNOrderListItemBo) {
+    func onOrderListItemCell(cell: LNOrderListItemCell, clickRefund item: LNOrderListItemVO) {
         
     }
     
-    func onOrderListItemCell(cell: LNOrderListItemCell, clickComment item: LNOrderListItemBo) {
+    func onOrderListItemCell(cell: LNOrderListItemCell, clickComment item: LNOrderListItemVO) {
         
     }
 }

+ 387 - 0
Lanu/Views/Profile/LNEditProfileViewController.swift

@@ -0,0 +1,387 @@
+//
+//  LNEditProfileViewController.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/11/26.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+extension UIView {
+    func pushToEditProfile() {
+        let vc = LNEditProfileViewController()
+        navigationController?.pushViewController(vc, animated: true)
+    }
+}
+
+
+class LNEditProfileViewController: LNViewController {
+    private let avatar = UIImageView()
+    private let nameInputField = UITextField()
+    private let ageInputField = UITextField()
+    
+    private let maleButton = LNEditProfileOptionalButton(title: .init(key: "Male"))
+    private let femaleButton = LNEditProfileOptionalButton(title: .init(key: "Female"))
+    
+    private let saveButton = UIButton()
+    
+    private let imagePicker = LNImagePicker()
+    private var curAvatar: String?
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        setupViews()
+        
+        checkSaveButton()
+    }
+}
+
+extension LNEditProfileViewController {
+    @objc
+    private func onInputFieldValueChanged(_ textfield: UITextField) {
+        checkSaveButton()
+    }
+}
+
+extension LNEditProfileViewController {
+    private func checkSaveButton() {
+        if nameInputField.text?.isEmpty == false,
+           ageInputField.text?.isEmpty == false,
+           (maleButton.isSelected || femaleButton.isSelected) {
+            saveButton.isEnabled = true
+            saveButton.setBackgroundImage(.primary_8, for: .normal)
+        } else {
+            saveButton.isEnabled = false
+            saveButton.setBackgroundImage(nil, for: .normal)
+        }
+    }
+    
+    private func setupViews() {
+        view.backgroundColor = .primary_1
+        view.onTap { [weak self] in
+            guard let self else { return }
+            self.view.endEditing(true)
+        }
+        
+        let scrollView = UIScrollView()
+        scrollView.showsVerticalScrollIndicator = false
+        scrollView.showsHorizontalScrollIndicator = false
+        view.addSubview(scrollView)
+        scrollView.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+        }
+        
+        let fakeView = UIView()
+        scrollView.addSubview(fakeView)
+        fakeView.snp.makeConstraints { make in
+            make.top.equalToSuperview()
+            make.horizontalEdges.equalToSuperview()
+            make.width.equalToSuperview()
+            make.height.equalTo(0)
+        }
+        
+        let avatar = buildAvatar()
+        scrollView.addSubview(avatar)
+        avatar.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.top.equalToSuperview().offset(20)
+        }
+        
+        let edit = buildEditView()
+        scrollView.addSubview(edit)
+        edit.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(16)
+            make.top.equalTo(avatar.snp.bottom).offset(20)
+        }
+        
+        saveButton.backgroundColor = .fill_4
+        saveButton.setTitle(.init(key: "保存"), for: .normal)
+        saveButton.layer.cornerRadius = 23.5
+        saveButton.clipsToBounds = true
+        saveButton.isEnabled = false
+        saveButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            let age: Int? = if let text = self.ageInputField.text {
+                Int(text)
+            } else {
+                nil
+            }
+            
+            let gender: LNUserGender? = if self.maleButton.isSelected {
+                .male
+            } else if self.femaleButton.isSelected {
+                .female
+            } else {
+                nil
+            }
+            
+            LNProfileManager.shared.modifyMyProfile(
+                age: age,
+                avatar: curAvatar,
+                nickname: nameInputField.text,
+                gender: gender?.rawValue
+            ) { [weak self] success in
+                guard let self else { return }
+                
+                if success {
+                    navigationController?.popViewController(animated: true)
+                }
+            }
+        }), for: .touchUpInside)
+        scrollView.addSubview(saveButton)
+        saveButton.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(16)
+            make.top.equalTo(edit.snp.bottom).offset(24)
+            make.height.equalTo(47)
+            make.bottom.equalToSuperview()
+        }
+    }
+    
+    private func buildAvatar() -> UIView {
+        if let url = myUserInfo?.avatar, !url.isEmpty {
+            avatar.sd_setImage(with: URL(string: url))
+        } else {
+            avatar.image = .init(named: "ic_profile_login_avatar")
+        }
+        avatar.isUserInteractionEnabled = true
+        avatar.layer.cornerRadius = 45
+        avatar.snp.makeConstraints { make in
+            make.width.height.equalTo(90)
+        }
+        
+        let edit = UIButton()
+        edit.setImage(.init(named: "ic_profile_avatar_edit_gradient"), for: .normal)
+        edit.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            self.imagePicker.selectPhoto(from: self.view) { [weak self] image in
+                guard let self else { return }
+            }
+        }), for: .touchUpInside)
+        avatar.addSubview(edit)
+        edit.snp.makeConstraints { make in
+            make.trailing.bottom.equalToSuperview()
+        }
+        
+        return avatar
+    }
+    
+    private func buildEditView() -> UIView {
+        let container = UIView()
+        container.backgroundColor = .fill
+        container.layer.cornerRadius = 16
+        
+        let stackView = UIStackView()
+        stackView.axis = .vertical
+        stackView.spacing = 24
+        container.addSubview(stackView)
+        stackView.snp.makeConstraints { make in
+            make.edges.equalToSuperview().inset(16)
+        }
+        
+        let itemViews: [UIView] = [
+            buildNameInput(),
+            buildGender(),
+            buildAgeInput()
+        ]
+        itemViews.forEach {
+            stackView.addArrangedSubview($0)
+        }
+        
+        return container
+    }
+    
+    private func buildNameInput() -> UIView {
+        let container = UIView()
+        
+        let titleLabel = UILabel()
+        titleLabel.text = .init(key: "昵称")
+        titleLabel.font = .heading_h4
+        titleLabel.textColor =  .text_4
+        container.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.top.horizontalEdges.equalToSuperview()
+        }
+        
+        let holder = UIView()
+        holder.backgroundColor = .primary_1
+        holder.layer.cornerRadius = 18
+        container.addSubview(holder)
+        holder.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.bottom.equalToSuperview()
+            make.top.equalTo(titleLabel.snp.bottom).offset(8)
+            make.height.equalTo(36)
+        }
+        
+        nameInputField.text = myUserInfo?.nickname
+        nameInputField.font = .heading_h4
+        nameInputField.textColor = .text_5
+        nameInputField.backgroundColor = .primary_1
+        nameInputField.layer.cornerRadius = 18
+        nameInputField.attributedPlaceholder = NSAttributedString(string: .init(key: "Please enter"), attributes: [.font: UIFont.body_m])
+        nameInputField.clearButtonMode = .whileEditing
+        nameInputField.addTarget(self, action: #selector(onInputFieldValueChanged(_:)), for: .valueChanged)
+        holder.addSubview(nameInputField)
+        nameInputField.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(12)
+            make.centerY.equalToSuperview()
+        }
+        
+        return container
+    }
+    
+    private func buildGender() -> UIView {
+        let container = UIView()
+        
+        let titleLabel = UILabel()
+        titleLabel.text = .init(key: "性别")
+        titleLabel.font = .heading_h4
+        titleLabel.textColor =  .text_4
+        container.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.top.horizontalEdges.equalToSuperview()
+        }
+        
+        maleButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            maleButton.isSelected = true
+            femaleButton.isSelected = false
+            self.checkSaveButton()
+        }), for: .touchUpInside)
+        container.addSubview(maleButton)
+        maleButton.snp.makeConstraints { make in
+            make.leading.bottom.equalToSuperview()
+            make.top.equalTo(titleLabel.snp.bottom).offset(8)
+        }
+        
+        container.addSubview(femaleButton)
+        femaleButton.snp.makeConstraints { make in
+            make.centerY.equalTo(maleButton)
+            make.leading.equalTo(maleButton.snp.trailing).offset(8)
+        }
+        femaleButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            maleButton.isSelected = false
+            femaleButton.isSelected = true
+            self.checkSaveButton()
+        }), for: .touchUpInside)
+        
+        maleButton.isSelected = myUserInfo?.gender == .male
+        femaleButton.isSelected = myUserInfo?.gender == .female
+        
+        return container
+    }
+    
+    private func buildAgeInput() -> UIView {
+        let container = UIView()
+        
+        let titleLabel = UILabel()
+        titleLabel.text = .init(key: "年龄")
+        titleLabel.font = .heading_h4
+        titleLabel.textColor =  .text_4
+        container.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.top.horizontalEdges.equalToSuperview()
+        }
+        
+        let holder = UIView()
+        holder.backgroundColor = .primary_1
+        holder.layer.cornerRadius = 18
+        container.addSubview(holder)
+        holder.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.bottom.equalToSuperview()
+            make.top.equalTo(titleLabel.snp.bottom).offset(8)
+            make.height.equalTo(36)
+        }
+        
+        if let age = myUserInfo?.age {
+            ageInputField.text = "\(age)"
+        }
+        ageInputField.font = .heading_h4
+        ageInputField.textColor = .text_5
+        ageInputField.backgroundColor = .primary_1
+        ageInputField.layer.cornerRadius = 18
+        ageInputField.attributedPlaceholder = NSAttributedString(string: .init(key: "Please enter"), attributes: [.font: UIFont.body_m])
+        ageInputField.clearButtonMode = .whileEditing
+        ageInputField.keyboardType = .numberPad
+        ageInputField.addTarget(self, action: #selector(onInputFieldValueChanged(_:)), for: .valueChanged)
+        holder.addSubview(ageInputField)
+        ageInputField.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(12)
+            make.centerY.equalToSuperview()
+        }
+        
+        return container
+    }
+}
+
+private class LNEditProfileOptionalButton: UIButton {
+    private let label = UILabel()
+    
+    override var isSelected: Bool {
+        didSet {
+            if isSelected {
+                setBackgroundImage(.primary_7, for: .normal)
+                
+                label.font = .heading_h4
+                label.textColor = .text_1
+            } else {
+                setBackgroundImage(nil, for: .normal)
+                
+                label.font = .body_m
+                label.textColor = .text_2
+            }
+        }
+    }
+    
+    init(title: String) {
+        super.init(frame: .zero)
+        
+        setupViews(title)
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    private func setupViews(_ title: String) {
+        layer.cornerRadius = 14
+        clipsToBounds = true
+        backgroundColor = .primary_1
+        snp.makeConstraints { make in
+            make.height.equalTo(28)
+        }
+        
+        label.text = title
+        label.font = .heading_h4
+        label.textColor = .text_1
+        addSubview(label)
+        label.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(28)
+            make.centerY.equalToSuperview()
+        }
+    }
+}
+
+
+#if DEBUG
+
+import SwiftUI
+
+struct LNEditProfileViewControllerPreview: UIViewControllerRepresentable {
+    func makeUIViewController(context: Context) -> some UIViewController {
+        LNNavigationController(rootViewController: LNEditProfileViewController())
+    }
+    
+    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { }
+}
+
+#Preview(body: {
+    LNEditProfileViewControllerPreview()
+})
+#endif

+ 496 - 0
Lanu/Views/Profile/LNMineViewController.swift

@@ -19,15 +19,68 @@ extension UIView {
 
 
 class LNMineViewController: LNViewController {
+    private let stackView = UIStackView()
+    
+    private let userView = UIView()
+    private let avatar = UIImageView()
+    private let userNameLabel = UILabel()
+    private let idLabel = UILabel()
+    private let toProfileView = UIView()
+    
+    private let loginView = UIView()
+    
+    private let diamondLabel = UILabel()
+    
     override func viewDidLoad() {
         super.viewDidLoad()
         
         setupViews()
+        updateUserContent()
+        updateWalletContent()
+        
+        LNEventDeliver.addObserver(self)
+    }
+}
+
+extension LNMineViewController: LNProfileManagerNotify, LNPurchaseManagerNotify {
+    func onUserInfoChanged(userInfo: LNUserProfileInfo) {
+        guard userInfo.id.isMyUid else { return }
+        updateUserContent()
+    }
+    
+    func onUserWalletInfoChanged(info: LNUserWalletInfo) {
+        updateWalletContent()
     }
 }
 
 extension LNMineViewController {
+    private func updateWalletContent() {
+        if let info = myWalletInfo {
+            diamondLabel.text = "\(info.diamond)"
+        } else {
+            diamondLabel.text = "0"
+        }
+    }
+    
+    private func updateUserContent() {
+        if let myUserInfo {
+            loginView.isHidden = true
+            userView.isHidden = false
+            
+            avatar.sd_setImage(with: URL(string: myUserInfo.avatar))
+            userNameLabel.text = myUserInfo.nickname
+            idLabel.text = myUserInfo.id
+        } else {
+            loginView.isHidden = false
+            userView.isHidden = true
+            
+            avatar.image = .init(named: "ic_profile_login_avatar")
+        }
+    }
+    
     private func setupViews() {
+        title = .init(key: "我的页")
+        view.backgroundColor = .primary_1
         
         let topCover = UIImageView()
         topCover.image = .init(named: "ic_main_top_bg")
@@ -35,6 +88,449 @@ extension LNMineViewController {
         topCover.snp.makeConstraints { make in
             make.top.leading.trailing.equalToSuperview()
         }
+        
+        stackView.axis = .vertical
+        stackView.spacing = 14
+        view.addSubview(stackView)
+        stackView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(16)
+            make.top.equalToSuperview().offset(20)
+        }
+        
+        let itemViews: [UIView] = [
+            buildBaseInfo(),
+            buildGameMate(),
+            buildWallet(),
+            buildOrder()
+        ]
+        itemViews.forEach {
+            stackView.addArrangedSubview($0)
+        }
+        
+        let logoutButton = buildLogout()
+        view.addSubview(logoutButton)
+        logoutButton.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom).offset(-50)
+        }
+    }
+    
+    private func buildBaseInfo() -> UIView {
+        let container = UIView()
+        
+        avatar.layer.borderColor = UIColor.fill.cgColor
+        avatar.layer.borderWidth = 2
+        avatar.layer.cornerRadius = 37.5
+        avatar.clipsToBounds = true
+        container.addSubview(avatar)
+        avatar.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(16)
+            make.verticalEdges.equalToSuperview()
+            make.width.height.equalTo(75)
+        }
+        
+        let editButton = UIButton()
+        editButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            self.view.pushToEditProfile()
+        }), for: .touchUpInside)
+        editButton.setImage(.init(named: "ic_profile_avatar_edit"), for: .normal)
+        container.addSubview(editButton)
+        editButton.snp.makeConstraints { make in
+            make.trailing.bottom.equalTo(avatar)
+        }
+        
+        let userView = buildUserView()
+        container.addSubview(userView)
+        userView.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalTo(avatar.snp.trailing).offset(16)
+            make.trailing.equalToSuperview()
+        }
+        
+        let loginView = buildLogin()
+        container.addSubview(loginView)
+        loginView.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalTo(avatar.snp.trailing).offset(16)
+            make.trailing.equalToSuperview()
+        }
+        
+        return container
+    }
+    
+    private func buildUserView() -> UIView {
+        let profileLabel = UILabel()
+        profileLabel.text = .init(key: "个人页")
+        profileLabel.font = .body_xs
+        profileLabel.textColor = .text_4
+        toProfileView.addSubview(profileLabel)
+        profileLabel.snp.makeConstraints { make in
+            make.leading.verticalEdges.equalToSuperview()
+        }
+        
+        let config = UIImage.SymbolConfiguration(pointSize: 8)
+        let arrow = UIImageView()
+        arrow.tintColor = .text_4
+        arrow.image = .init(systemName: "chevron.forward", withConfiguration: config)
+        arrow.contentMode = .center
+        toProfileView.addSubview(arrow)
+        arrow.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalTo(profileLabel.snp.trailing).offset(2)
+            make.trailing.equalToSuperview()
+            make.width.height.equalTo(14)
+        }
+        
+        toProfileView.onTap { [weak self] in
+            guard let self else { return }
+            self.view.pushToProfile(uid: myUid)
+        }
+        userView.addSubview(toProfileView)
+        toProfileView.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview()
+        }
+        
+        userNameLabel.font = .heading_h2
+        userNameLabel.textColor = .text_5
+        userView.addSubview(userNameLabel)
+        userNameLabel.snp.makeConstraints { make in
+            make.leading.top.equalToSuperview()
+            make.trailing.lessThanOrEqualTo(toProfileView.snp.leading).offset(-16)
+        }
+        
+        let idTitle = UILabel()
+        idTitle.text = "ID"
+        idTitle.font = .body_s
+        idTitle.textColor = .text_3
+        userView.addSubview(idTitle)
+        idTitle.snp.makeConstraints { make in
+            make.top.equalTo(userNameLabel.snp.bottom).offset(4)
+            make.leading.bottom.equalToSuperview()
+        }
+        
+        idLabel.font = .body_xs
+        idLabel.textColor = .text_4
+        userView.addSubview(idLabel)
+        idLabel.snp.makeConstraints { make in
+            make.centerY.equalTo(idTitle)
+            make.leading.equalTo(idTitle.snp.trailing).offset(4)
+        }
+        
+        let copyIdButton = UIButton()
+        copyIdButton.setImage(.init(named: ""), for: .normal)
+        userView.addSubview(copyIdButton)
+        copyIdButton.snp.makeConstraints { make in
+            make.centerY.equalTo(idTitle)
+            make.leading.equalTo(idLabel.snp.trailing).offset(4)
+            make.trailing.lessThanOrEqualTo(toProfileView.snp.leading).offset(-16)
+        }
+        
+        return userView
+    }
+    
+    private func buildGameMate() -> UIView {
+        let container = UIView()
+        
+        let qr = buildQRView()
+        container.addSubview(qr)
+        qr.snp.makeConstraints { make in
+            make.leading.equalToSuperview()
+            make.verticalEdges.equalToSuperview()
+        }
+        
+        let share = buildShare()
+        container.addSubview(share)
+        share.snp.makeConstraints { make in
+            make.trailing.equalToSuperview()
+            make.verticalEdges.equalToSuperview()
+            make.leading.equalTo(qr.snp.trailing).offset(11)
+            make.width.equalTo(qr)
+        }
+        
+        return container
+    }
+    
+    private func buildQRView() -> UIView {
+        let ic = UIImageView()
+        ic.image = .init(named: "ic_profile_qr")
+        
+        
+        let label = UILabel()
+        label.font = .heading_h3
+        label.textColor = .init(hex: "#01327B")
+        label.numberOfLines = 0
+        ic.addSubview(label)
+        label.snp.makeConstraints { make in
+            make.leading.top.equalToSuperview().inset(10)
+            make.trailing.equalToSuperview().offset(-50)
+        }
+        
+        let text: String = .init(key: "Generate QR code")
+        let attrStr = NSMutableAttributedString(string: text)
+        
+        attrStr.append(.init(string: " "))
+        
+        let image: UIImage = .init(named: "ic_profile_qr_arrow")!
+        let arrow = NSTextAttachment(image: image)
+        arrow.bounds = .init(x: 0,
+                             y: -(label.font.ascender - image.size.height) / 2,
+                             width: image.size.width,
+                             height: image.size.height)
+        attrStr.append(NSAttributedString(attachment: arrow))
+        label.attributedText = attrStr
+        
+        return ic
+    }
+    
+    private func buildShare() -> UIView {
+        let ic = UIImageView()
+        ic.image = .init(named: "ic_profile_share")
+        
+        let label = UILabel()
+        label.font = .heading_h3
+        label.textColor = .init(hex: "#0A625F")
+        label.numberOfLines = 0
+        ic.addSubview(label)
+        label.snp.makeConstraints { make in
+            make.leading.top.equalToSuperview().inset(10)
+            make.trailing.equalToSuperview().offset(-50)
+        }
+        
+        let text: String = .init(key: "Share with friends")
+        let attrStr = NSMutableAttributedString(string: text)
+        
+        attrStr.append(.init(string: " "))
+        
+        let image: UIImage = .init(named: "ic_profile_share_arrow")!
+        let arrow = NSTextAttachment(image: image)
+        arrow.bounds = .init(x: 0,
+                             y: -(label.font.ascender - image.size.height) / 2,
+                             width: image.size.width,
+                             height: image.size.height)
+        attrStr.append(NSAttributedString(attachment: arrow))
+        label.attributedText = attrStr
+        
+        return ic
+    }
+    
+    private func buildWallet() -> UIView {
+        let container = UIView()
+        container.backgroundColor = .fill
+        container.layer.cornerRadius = 12
+        
+        let walletIc = UIImageView()
+        walletIc.image = .init(named: "ic_profile_wallet")
+        container.addSubview(walletIc)
+        walletIc.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(16)
+            make.top.equalToSuperview().offset(16)
+            make.bottom.equalToSuperview().offset(-16)
+        }
+        
+        let title = UILabel()
+        title.font = .heading_h3
+        title.textColor = .text_5
+        title.text = .init(key: "My Wallet")
+        container.addSubview(title)
+        title.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalTo(walletIc.snp.trailing).offset(10)
+        }
+        
+        let config = UIImage.SymbolConfiguration(pointSize: 10)
+        let arrow = UIImageView()
+        arrow.tintColor = .text_4
+        arrow.image = .init(systemName: "chevron.forward", withConfiguration: config)
+        arrow.contentMode = .center
+        container.addSubview(arrow)
+        arrow.snp.makeConstraints { make in
+            make.trailing.equalToSuperview().offset(-16)
+            make.centerY.equalToSuperview()
+            make.width.height.equalTo(16)
+        }
+        
+        diamondLabel.font = .heading_h4
+        diamondLabel.textColor = .text_5
+        container.addSubview(diamondLabel)
+        diamondLabel.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.trailing.equalTo(arrow.snp.leading).offset(-3)
+        }
+        
+        let diamondIc = UIImageView()
+        diamondIc.image = .init(named: "ic_diamond")
+        container.addSubview(diamondIc)
+        diamondIc.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.trailing.equalTo(diamondLabel.snp.leading).offset(-3)
+        }
+        
+        return container
+    }
+    
+    private func buildOrder() -> UIView {
+        let container = UIView()
+        container.backgroundColor = .fill
+        container.layer.cornerRadius = 12
+        
+        let orderIc = UIImageView()
+        orderIc.image = .init(named: "ic_profile_order")
+        container.addSubview(orderIc)
+        orderIc.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(16)
+            make.top.equalToSuperview().offset(12)
+        }
+        
+        let title = UILabel()
+        title.font = .heading_h3
+        title.textColor = .text_5
+        title.text = .init(key: "My Order")
+        container.addSubview(title)
+        title.snp.makeConstraints { make in
+            make.centerY.equalTo(orderIc)
+            make.leading.equalTo(orderIc.snp.trailing).offset(10)
+        }
+        
+        let line = UIView()
+        line.backgroundColor = .fill_3
+        container.addSubview(line)
+        line.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(19)
+            make.height.equalTo(0.5)
+            make.top.equalTo(title.snp.bottom).offset(10)
+        }
+        
+        let stackView = UIStackView()
+        stackView.axis = .horizontal
+        stackView.distribution = .equalSpacing
+        container.addSubview(stackView)
+        stackView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(16)
+            make.top.equalTo(line.snp.bottom).offset(10)
+            make.bottom.equalToSuperview().offset(-12)
+        }
+        
+        let pending = buildOrderSubItemView(icName: "ic_order_pending", title: .init(key: "待付款"))
+        stackView.addArrangedSubview(pending)
+        pending.onTap {
+            
+        }
+        
+        let finish = buildOrderSubItemView(icName: "ic_order_done", title: .init(key: "已完成"))
+        stackView.addArrangedSubview(finish)
+        finish.onTap {
+            
+        }
+        
+        let refund = buildOrderSubItemView(icName: "ic_order_refund", title: .init(key: "退款"))
+        stackView.addArrangedSubview(refund)
+        refund.onTap {
+            
+        }
+        
+        let all = buildOrderSubItemView(icName: "ic_order_all", title: .init(key: "全部订单"))
+        stackView.addArrangedSubview(all)
+        all.onTap {
+            
+        }
+        
+        return container
+    }
+    
+    private func buildLogout() -> UIView {
+        let button = UIButton()
+        button.layer.cornerRadius = 15
+        button.layer.borderColor = UIColor.fill_4.cgColor
+        button.layer.borderWidth = 1
+        view.addSubview(button)
+        button.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom).offset(-50)
+        }
+        
+        let title = UILabel()
+        title.text = .init(key: "Logout")
+        title.font = .body_s
+        title.textColor = .text_4
+        button.addSubview(title)
+        title.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(30)
+            make.verticalEdges.equalToSuperview().inset(7)
+        }
+        
+        return button
+    }
+    
+    private func buildOrderSubItemView(icName: String, title: String) -> UIView {
+        let container = UIView()
+        
+        let ic = UIImageView()
+        ic.image = .init(named: icName)
+        container.addSubview(ic)
+        ic.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.top.equalToSuperview()
+        }
+        
+        let label = UILabel()
+        label.text = title
+        label.font = .body_s
+        label.textColor = .text_4
+        label.textAlignment = .center
+        container.addSubview(label)
+        label.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.bottom.equalToSuperview()
+            make.top.equalTo(ic.snp.bottom).offset(3)
+            make.width.greaterThanOrEqualTo(43)
+        }
+        
+        return container
+    }
+    
+    private func buildLogin() -> UIView {
+        let button = UIButton()
+        button.setBackgroundImage(.primary_7, for: .normal)
+        button.layer.cornerRadius = 8
+        button.clipsToBounds = true
+        loginView.addSubview(button)
+        button.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview().offset(-10)
+        }
+        
+        let login = UILabel()
+        login.text = .init(key: "login")
+        login.font = .body_s
+        login.textColor = .text_1
+        button.addSubview(login)
+        login.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(16)
+            make.verticalEdges.equalToSuperview().inset(7)
+        }
+        
+        let title = UILabel()
+        title.text = .init(key: "点击登陆")
+        title.font = .heading_h2
+        title.textColor = .text_5
+        loginView.addSubview(title)
+        title.snp.makeConstraints { make in
+            make.leading.top.equalToSuperview()
+        }
+        
+        let desc = UILabel()
+        desc.text = .init(key: "登陆更精彩")
+        desc.font = .body_xs
+        desc.textColor = .text_4
+        loginView.addSubview(desc)
+        desc.snp.makeConstraints { make in
+            make.leading.bottom.equalToSuperview()
+            make.top.equalTo(title.snp.bottom).offset(4)
+        }
+        
+        return loginView
     }
 }
 

+ 43 - 0
Lanu/Views/Profile/LNProfileViewController.swift

@@ -0,0 +1,43 @@
+//
+//  LNProfileViewController.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/11/26.
+//
+
+import Foundation
+import UIKit
+
+
+extension UIView {
+    func pushToProfile(uid: String) {
+        let vc = LNProfileViewController(uid: uid)
+        navigationController?.pushViewController(vc, animated: true)
+    }
+}
+
+
+class LNProfileViewController: LNViewController {
+    private let uid: String
+    
+    init(uid: String) {
+        self.uid = uid
+        super.init(nibName: nil, bundle: nil)
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        setupViews()
+    }
+}
+
+extension LNProfileViewController {
+    private func setupViews() {
+        
+    }
+}

+ 1 - 0
Lanu/en.lproj/InfoPlist.strings

@@ -7,3 +7,4 @@
 */
 
 "NSLocationWhenInUseUsageDescription" = "Your consent is required to open the location";
+"NSPhotoLibraryUsageDescription" = "need photo permission";

+ 1 - 0
Lanu/id.lproj/InfoPlist.strings

@@ -7,3 +7,4 @@
 */
 
 "NSLocationWhenInUseUsageDescription" = "Your consent is required to open the location";
+"NSPhotoLibraryUsageDescription" = "need photo permission";

+ 1 - 0
Lanu/zh-Hans.lproj/InfoPlist.strings

@@ -7,3 +7,4 @@
 */
 
 "NSLocationWhenInUseUsageDescription" = "需要您的同意才能打开位置";
+"NSPhotoLibraryUsageDescription" = "需要图片权限";