Bladeren bron

feat: 补充钱包功能

陈文艺 3 maanden geleden
bovenliggende
commit
47eb09662c
40 gewijzigde bestanden met toevoegingen van 2193 en 29 verwijderingen
  1. 10 2
      Lanu.xcodeproj/project.pbxproj
  2. 6 0
      Lanu/Assets.xcassets/Wallet/Contents.json
  3. 22 0
      Lanu/Assets.xcassets/Wallet/ic_bean_bg.imageset/Contents.json
  4. BIN
      Lanu/Assets.xcassets/Wallet/ic_bean_bg.imageset/ic_bean_bg@2x.png
  5. BIN
      Lanu/Assets.xcassets/Wallet/ic_bean_bg.imageset/ic_bean_bg@3x.png
  6. 22 0
      Lanu/Assets.xcassets/Wallet/ic_coin_bg.imageset/Contents.json
  7. BIN
      Lanu/Assets.xcassets/Wallet/ic_coin_bg.imageset/ic_coin_bg@2x.png
  8. BIN
      Lanu/Assets.xcassets/Wallet/ic_coin_bg.imageset/ic_coin_bg@3x.png
  9. 22 0
      Lanu/Assets.xcassets/Wallet/ic_diamond_bg.imageset/Contents.json
  10. BIN
      Lanu/Assets.xcassets/Wallet/ic_diamond_bg.imageset/ic_diamond_bg@2x.png
  11. BIN
      Lanu/Assets.xcassets/Wallet/ic_diamond_bg.imageset/ic_diamond_bg@3x.png
  12. 22 0
      Lanu/Assets.xcassets/Wallet/ic_wallet_history.imageset/Contents.json
  13. BIN
      Lanu/Assets.xcassets/Wallet/ic_wallet_history.imageset/ic_wallet_history@2x.png
  14. BIN
      Lanu/Assets.xcassets/Wallet/ic_wallet_history.imageset/ic_wallet_history@3x.png
  15. 6 0
      Lanu/Assets.xcassets/common/Bean/Contents.json
  16. 22 0
      Lanu/Assets.xcassets/common/Bean/ic_bean.imageset/Contents.json
  17. BIN
      Lanu/Assets.xcassets/common/Bean/ic_bean.imageset/ic_bean@2x.png
  18. BIN
      Lanu/Assets.xcassets/common/Bean/ic_bean.imageset/ic_bean@3x.png
  19. 7 0
      Lanu/Common/Theme/UIImageView+Theme.swift
  20. 23 0
      Lanu/Manager/Config/LNConfigManager.swift
  21. 23 0
      Lanu/Manager/Config/Network/LNConfigResponse.swift
  22. 18 0
      Lanu/Manager/Config/Network/LNHttpManager+Config.swift
  23. 2 0
      Lanu/Manager/Profile/Network/LNProfileResponse.swift
  24. 98 0
      Lanu/Manager/Purchase/LNPurchaseManager.swift
  25. 28 2
      Lanu/Manager/Purchase/LNUserWalletInfo.swift
  26. 20 1
      Lanu/Manager/Purchase/Network/LNHttpManager+Purchase.swift
  27. 26 2
      Lanu/Manager/Purchase/Network/LNPurchaseResponse.swift
  28. 11 10
      Lanu/Views/Login/Setup/LNBaseInfoSetupViewController.swift
  29. 3 3
      Lanu/Views/Login/Setup/LNGenderSetupViewController.swift
  30. 15 5
      Lanu/Views/Login/Setup/LNInterestSetupViewController.swift
  31. 0 2
      Lanu/Views/Main/LNMainViewController.swift
  32. 7 0
      Lanu/Views/Profile/Mine/LNMineWalletInfoView.swift
  33. 2 1
      Lanu/Views/Profile/Profile/LNProfileInfosView.swift
  34. 5 1
      Lanu/Views/Profile/Profile/LNProfilePhotoWall.swift
  35. 1 0
      Lanu/Views/Profile/Profile/LNProfileUserDetailView.swift
  36. 273 0
      Lanu/Views/Wallet/Coin/LNCoinToDiamondPanel.swift
  37. 380 0
      Lanu/Views/Wallet/Coin/LNCoinViewController.swift
  38. 272 0
      Lanu/Views/Wallet/Diamond/LNDiamondToCoinPanel.swift
  39. 381 0
      Lanu/Views/Wallet/Diamond/LNDiamondViewController.swift
  40. 466 0
      Lanu/Views/Wallet/LNWalletViewController.swift

+ 10 - 2
Lanu.xcodeproj/project.pbxproj

@@ -95,6 +95,9 @@
 				"Manager/Account/LNCommonAlertView+Settings.swift",
 				"Manager/Account/Network/LNHttpManager+Login.swift",
 				Manager/Account/Network/LNLoginResponse.swift,
+				Manager/Config/LNConfigManager.swift,
+				Manager/Config/Network/LNConfigResponse.swift,
+				"Manager/Config/Network/LNHttpManager+Config.swift",
 				Manager/Deeplink/LNDeeplinkManager.swift,
 				Manager/GameMate/LNGameMateManager.swift,
 				Manager/GameMate/Network/LNGameMateResponse.swift,
@@ -250,6 +253,11 @@
 				Views/Settings/LNHelpCenterViewController.swift,
 				Views/Settings/LNLanguageSettingPanel.swift,
 				Views/Settings/LNSettingsViewController.swift,
+				Views/Wallet/Coin/LNCoinToDiamondPanel.swift,
+				Views/Wallet/Coin/LNCoinViewController.swift,
+				Views/Wallet/Diamond/LNDiamondToCoinPanel.swift,
+				Views/Wallet/Diamond/LNDiamondViewController.swift,
+				Views/Wallet/LNWalletViewController.swift,
 				Views/Web/LNWebViewController.swift,
 			);
 			target = FBFE13BF2EBC39B000DCE6E9 /* Lanu */;
@@ -499,7 +507,7 @@
 				ENABLE_USER_SCRIPT_SANDBOXING = NO;
 				GENERATE_INFOPLIST_FILE = YES;
 				INFOPLIST_FILE = Lanu/Info.plist;
-				INFOPLIST_KEY_CFBundleDisplayName = Gami_Debug;
+				INFOPLIST_KEY_CFBundleDisplayName = "Gami(Debug)";
 				INFOPLIST_KEY_NSCameraUsageDescription = "need permission";
 				INFOPLIST_KEY_NSMicrophoneUsageDescription = "need permission";
 				INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "need permission";
@@ -540,7 +548,7 @@
 				ENABLE_USER_SCRIPT_SANDBOXING = NO;
 				GENERATE_INFOPLIST_FILE = YES;
 				INFOPLIST_FILE = Lanu/Info.plist;
-				INFOPLIST_KEY_CFBundleDisplayName = Gami_Debug;
+				INFOPLIST_KEY_CFBundleDisplayName = "Gami(Debug)";
 				INFOPLIST_KEY_NSCameraUsageDescription = "need permission";
 				INFOPLIST_KEY_NSMicrophoneUsageDescription = "need permission";
 				INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "need permission";

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

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

+ 22 - 0
Lanu/Assets.xcassets/Wallet/ic_bean_bg.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/Wallet/ic_bean_bg.imageset/ic_bean_bg@2x.png


BIN
Lanu/Assets.xcassets/Wallet/ic_bean_bg.imageset/ic_bean_bg@3x.png


+ 22 - 0
Lanu/Assets.xcassets/Wallet/ic_coin_bg.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/Wallet/ic_coin_bg.imageset/ic_coin_bg@2x.png


BIN
Lanu/Assets.xcassets/Wallet/ic_coin_bg.imageset/ic_coin_bg@3x.png


+ 22 - 0
Lanu/Assets.xcassets/Wallet/ic_diamond_bg.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/Wallet/ic_diamond_bg.imageset/ic_diamond_bg@2x.png


BIN
Lanu/Assets.xcassets/Wallet/ic_diamond_bg.imageset/ic_diamond_bg@3x.png


+ 22 - 0
Lanu/Assets.xcassets/Wallet/ic_wallet_history.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/Wallet/ic_wallet_history.imageset/ic_wallet_history@2x.png


BIN
Lanu/Assets.xcassets/Wallet/ic_wallet_history.imageset/ic_wallet_history@3x.png


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

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

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

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

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


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


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

@@ -29,4 +29,11 @@ extension UIImageView {
         
         return diamond
     }
+    
+    static func beanImageView() -> UIImageView {
+        let bean = UIImageView()
+        bean.image = .init(named: "ic_bean")
+        
+        return bean
+    }
 }

+ 23 - 0
Lanu/Manager/Config/LNConfigManager.swift

@@ -0,0 +1,23 @@
+//
+//  LNConfigManager.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/12/25.
+//
+
+import Foundation
+
+
+class LNConfigManager {
+    static let shared = LNConfigManager()
+    
+    private init() { }
+    
+    func getCommonConfig(queue: DispatchQueue = .main, handler: @escaping (LNConfigResponse?) -> Void) {
+        LNHttpManager.shared.getCommonConfig { res, err in
+            queue.asyncIfNotGlobal {
+                handler(res)
+            }
+        }
+    }
+}

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

@@ -0,0 +1,23 @@
+//
+//  LNConfigResponse.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/12/25.
+//
+
+import Foundation
+import AutoCodable
+
+
+@AutoCodable
+class LNCurrenyExchangeConstsVO: Decodable {
+    var IDR: Double?
+    var goldCoin: Double?
+    var bean: Double?
+    var diamond: Double?
+}
+
+@AutoCodable
+class LNConfigResponse: Decodable {
+    var commonCoinExchangeConsts: [LNCurrenyExchangeConstsVO] = []
+}

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

@@ -0,0 +1,18 @@
+//
+//  LNHttpManager+Config.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/12/25.
+//
+
+import Foundation
+
+
+private let kNetPath_Config_Common = "/base/consts/config"
+
+
+extension LNHttpManager {
+    func getCommonConfig(completion: @escaping (LNConfigResponse?, LNHttpError?) -> Void) {
+        post(path: kNetPath_Config_Common, completion: completion)
+    }
+}

+ 2 - 0
Lanu/Manager/Profile/Network/LNProfileResponse.swift

@@ -44,6 +44,8 @@ class LNUserProfileVO: Decodable, Copyable {
     
     var isAvailable: Bool {
         gender != .unknow
+        && !avatar.isEmpty
+        && !nickname.isEmpty
     }
     
     init() { }

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

@@ -23,6 +23,25 @@ class LNPurchaseManager {
     
     private(set) var myWalletInfo: LNUserWalletInfo = LNUserWalletInfo()
     
+    private let lock = NSLock()
+    private var goodsCache: [LNCurrencyType: [LNPurchaseGoodVO]] = [:]
+    
+    private var exchangeConfig: LNCurrencyExchangeConfig?
+    var coinToDiamondRatio: Decimal? {
+        guard let exchangeConfig else {
+            getExchangeConfig()
+            return nil
+        }
+        return Decimal(exchangeConfig.diamond / exchangeConfig.coin)
+    }
+    var diamondToCoinRatio: Decimal? {
+        guard let exchangeConfig else {
+            getExchangeConfig()
+            return nil
+        }
+        return Decimal(exchangeConfig.coin / exchangeConfig.diamond)
+    }
+    
     private init() {
         LNEventDeliver.addObserver(self)
     }
@@ -37,11 +56,90 @@ class LNPurchaseManager {
             notifyWalletInfoChanged()
         }
     }
+    
+    func loadGoodsList(currencyType: LNCurrencyType,
+                       queue: DispatchQueue = .main,
+                       handler: @escaping ([LNPurchaseGoodVO]?) -> Void)
+    {
+        lock.lock()
+        let cache = goodsCache[currencyType]
+        lock.unlock()
+        
+        var cacheAvailable = false
+        if let cache, !cache.isEmpty {
+            // 如果有缓存,先返回缓存,并且触发更新(但是不回调)
+            cacheAvailable = true
+            handler(cache)
+        }
+        LNHttpManager.shared.getGoodsList(currencyType: currencyType) { [weak self] res, err in
+            guard let self else { return }
+            if let res, err == nil {
+                lock.lock()
+                goodsCache[currencyType] = res.items
+                lock.unlock()
+            }
+            if !cacheAvailable {
+                // 如果前面没有缓存,则在更新时触发回调
+                queue.asyncIfNotGlobal {
+                    handler(res?.items)
+                }
+            }
+        }
+    }
+    
+    func exchangeCoinToDiamond(
+        amount: Double, queue: DispatchQueue = .main,
+        handler: @escaping (Bool) -> Void)
+    {
+        LNHttpManager.shared.exchangeCurrency(from: .coin, to: .diamond, amount: amount) { [weak self] err in
+            guard let self else { return }
+            queue.asyncIfNotGlobal {
+                handler(err == nil)
+            }
+            if err == nil {
+                reloadWalletInfo()
+            }
+        }
+    }
+
+    func exchangeDiamondToCoin(
+        amount: Double, queue: DispatchQueue = .main,
+        handler: @escaping (Bool) -> Void)
+    {
+        LNHttpManager.shared.exchangeCurrency(from: .diamond, to: .coin, amount: amount) { [weak self] err in
+            guard let self else { return }
+            queue.asyncIfNotGlobal {
+                handler(err == nil)
+            }
+            if err == nil {
+                reloadWalletInfo()
+            }
+        }
+    }
+    
+    private func getExchangeConfig() {
+        LNConfigManager.shared.getCommonConfig(queue: .global()) { [weak self] res in
+            guard let self else { return }
+            guard let res else { return }
+            let config = LNCurrencyExchangeConfig()
+            for item in res.commonCoinExchangeConsts {
+                config.update(item)
+            }
+            exchangeConfig = config
+        }
+    }
 }
 
 extension LNPurchaseManager: LNAccountManagerNotify {
     func onUserLogin() {
         reloadWalletInfo()
+        
+        // 拉取新的商品列表
+        loadGoodsList(currencyType: .coin) { _ in }
+        loadGoodsList(currencyType: .diamond) { _ in }
+        
+        // 拉取转换配置
+        getExchangeConfig()
     }
     
     func onUserLogout() {

+ 28 - 2
Lanu/Manager/Purchase/LNUserWalletInfo.swift

@@ -9,6 +9,32 @@ import Foundation
 
 
 class LNUserWalletInfo {
-    var diamond: Int = 0
-    var coin: Int = 0
+    var diamond: Double = 0
+    var coin: Double = 0
+}
+
+class LNCurrencyExchangeConfig {
+    var idr: Double = 0
+    var coin: Double = 0
+    var diamond: Double = 0
+    var bean: Double = 0
+    
+    func update(_ config: LNCurrenyExchangeConstsVO) {
+        guard let IDR = config.IDR else { return }
+        var scale = 1.0
+        if idr == 0 {
+            idr = IDR
+        } else {
+            scale = IDR / idr
+        }
+        if let goldCoin = config.goldCoin {
+            coin = goldCoin / scale
+        }
+        if let diamond = config.diamond {
+            self.diamond = diamond / scale
+        }
+        if let bean = config.bean {
+            self.bean = bean / scale
+        }
+    }
 }

+ 20 - 1
Lanu/Manager/Purchase/Network/LNHttpManager+Purchase.swift

@@ -10,11 +10,30 @@ import UIKit
 import SnapKit
 
 
-let kNetPath_Purchase_Wallet = "/user/my/wallet"
+private let kNetPath_Purchase_Wallet = "/user/my/wallet"
+
+private let kNetPath_Purchase_Goods = "/wallet/recharge/goods"
+private let kNetPath_Purchase_Exchange = "/wallet/exchange"
 
 
 extension LNHttpManager {
     func getWalletInfo(completion: @escaping (LNUserWalletInfoResponse?, LNHttpError?) -> Void) {
         post(path: kNetPath_Purchase_Wallet, completion: completion)
     }
+    
+    func getGoodsList(currencyType: LNCurrencyType, completion: @escaping (LNPUrchaseGoodsListResponse?, LNHttpError?) -> Void) {
+        post(path: kNetPath_Purchase_Goods, params: [
+            "walletType": currencyType.rawValue,
+            "rechargePlatform": LNPurchasePlatform.official.rawValue
+        ], completion: completion)
+    }
+    
+    func exchangeCurrency(from: LNCurrencyType, to: LNCurrencyType,
+                          amount: Double, completion: @escaping (LNHttpError?) -> Void) {
+        post(path: kNetPath_Purchase_Exchange, params: [
+            "fromWalletType": from.rawValue,
+            "toWalletType": to.rawValue,
+            "amount": amount
+        ], completion: completion)
+    }
 }

+ 26 - 2
Lanu/Manager/Purchase/Network/LNPurchaseResponse.swift

@@ -9,8 +9,32 @@ import Foundation
 import AutoCodable
 
 
+
+enum LNCurrencyType: Int, Decodable {
+    case coin = 0
+    case diamond = 1
+}
+
+enum LNPurchasePlatform: Int, Decodable {
+    case official = 0
+    case android = 1
+    case web = 2
+}
+
 @AutoCodable
 class LNUserWalletInfoResponse: Decodable {
-    var diamond: Int = 0
-    var goldCoin: Int = 0
+    var diamond: Double = 0
+    var goldCoin: Double = 0
+}
+
+@AutoCodable
+class LNPurchaseGoodVO: Decodable {
+    var id: String = ""
+    var coinRechargeAmount: Double = 0
+    var amount: Double = 0
+}
+
+@AutoCodable
+class LNPUrchaseGoodsListResponse: Decodable {
+    var items: [LNPurchaseGoodVO] = []
 }

+ 11 - 10
Lanu/Views/Login/Setup/LNBaseInfoSetupViewController.swift

@@ -11,8 +11,8 @@ import SnapKit
 
 
 extension UIView {
-    func pushToBaseInfoSetup(_ userInfo: LNUserProfileVO) {
-        let vc = LNBaseInfoSetupViewController(userInfo: userInfo)
+    func pushToBaseInfoSetup(_ config: LNProfileUpdateConfig) {
+        let vc = LNBaseInfoSetupViewController(config: config)
         navigationController?.pushViewController(vc, animated: true)
     }
 }
@@ -32,7 +32,7 @@ enum LNAvatarModifyType: CaseIterable {
 }
 
 class LNBaseInfoSetupViewController: LNViewController {
-    private let userInfo: LNUserProfileVO
+    private let updateConfig: LNProfileUpdateConfig
     
     private let fakeNavBar = UIView()
     
@@ -55,8 +55,8 @@ class LNBaseInfoSetupViewController: LNViewController {
         }
     }
     
-    init(userInfo: LNUserProfileVO) {
-        self.userInfo = userInfo
+    init(config: LNProfileUpdateConfig) {
+        updateConfig = config
         super.init(nibName: nil, bundle: nil)
     }
     
@@ -177,10 +177,10 @@ extension LNBaseInfoSetupViewController {
         nextButton.isEnabled = false
         nextButton.addAction(UIAction(handler: { [weak self] _ in
             guard let self else { return }
-            userInfo.nickname = nameInputField.text ?? ""
-            userInfo.age = Int(curDate)
-            userInfo.avatar = curUrl ?? ""
-            view.pushToInterestSetup(userInfo)
+            updateConfig.nickName = nameInputField.text ?? ""
+            updateConfig.age = Int(curDate) * 1_000
+            updateConfig.avatar = curUrl ?? ""
+            view.pushToInterestSetup(updateConfig)
         }), for: .touchUpInside)
         view.addSubview(nextButton)
         nextButton.snp.makeConstraints { make in
@@ -347,6 +347,7 @@ extension LNBaseInfoSetupViewController {
             make.trailing.equalToSuperview().offset(-16)
         }
         
+        birthDayLabel.text = curDate.formattedFullDate("-")
         birthDayLabel.font = .body_l
         birthDayLabel.textColor = .text_5
         container.addSubview(birthDayLabel)
@@ -396,7 +397,7 @@ import SwiftUI
 
 struct LNBaseInfoSetupViewControllerPreview: UIViewControllerRepresentable {
     func makeUIViewController(context: Context) -> some UIViewController {
-        LNBaseInfoSetupViewController(userInfo: LNUserProfileVO())
+        LNBaseInfoSetupViewController(config: LNProfileUpdateConfig())
     }
     
     func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {

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

@@ -100,9 +100,9 @@ extension LNGenderSetupViewController {
         nextButton.isEnabled = false
         nextButton.addAction(UIAction(handler: { [weak self] _ in
             guard let self else { return }
-            let info = LNUserProfileVO()
-            info.gender = self.curGender
-            self.view.pushToBaseInfoSetup(info)
+            let config = LNProfileUpdateConfig()
+            config.gender = self.curGender
+            self.view.pushToBaseInfoSetup(config)
         }), for: .touchUpInside)
         view.addSubview(nextButton)
         nextButton.snp.makeConstraints { make in

+ 15 - 5
Lanu/Views/Login/Setup/LNInterestSetupViewController.swift

@@ -11,15 +11,15 @@ import SnapKit
 
 
 extension UIView {
-    func pushToInterestSetup(_ userInfo: LNUserProfileVO) {
-        let vc = LNInterestSetupViewController(userInfo: userInfo)
+    func pushToInterestSetup(_ config: LNProfileUpdateConfig) {
+        let vc = LNInterestSetupViewController(config: config)
         navigationController?.pushViewController(vc, animated: true)
     }
 }
 
 
 class LNInterestSetupViewController: LNViewController {
-    private let userInfo: LNUserProfileVO
+    private let updateConfig: LNProfileUpdateConfig
     
     private let fakeNavBar = UIView()
     
@@ -37,8 +37,8 @@ class LNInterestSetupViewController: LNViewController {
     
     private var games: [LNGameTypeItemVO] = LNGameMateManager.shared.curGameTypes
     
-    init(userInfo: LNUserProfileVO) {
-        self.userInfo = userInfo
+    init(config: LNProfileUpdateConfig) {
+        updateConfig = config
         
         collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
         
@@ -246,6 +246,16 @@ extension LNInterestSetupViewController {
         nextButton.isEnabled = false
         nextButton.addAction(UIAction(handler: { [weak self] _ in
             guard let self else { return }
+            guard let indexs = collectionView.indexPathsForSelectedItems else { return }
+            let interests = indexs.compactMap {
+                self.games[$0.row].code
+            }
+            updateConfig.interest = interests
+            LNProfileManager.shared.modifyMyProfile(config: updateConfig) { [weak self] success in
+                guard let self else { return }
+                guard success else { return }
+                navigationController?.popToRootViewController(animated: true)
+            }
         }), for: .touchUpInside)
         container.addSubview(nextButton)
         nextButton.snp.makeConstraints { make in

+ 0 - 2
Lanu/Views/Main/LNMainViewController.swift

@@ -53,10 +53,8 @@ extension LNMainViewController: LNProfileManagerNotify {
     func onUserInfoChanged(userInfo: LNUserProfileVO) {
         guard userInfo.userNo.isMyUid else { return }
         
-#if !DEBUG
         if !userInfo.isAvailable {
             view.pushToGenderSetup()
         }
-#endif
     }
 }

+ 7 - 0
Lanu/Views/Profile/Mine/LNMineWalletInfoView.swift

@@ -41,6 +41,11 @@ extension LNMineWalletInfoView {
         layer.cornerRadius = 12
         backgroundColor = .fill
         
+        onTap { [weak self] in
+            guard let self else { return }
+            pushToWallet()
+        }
+        
         let titleLabel = UILabel()
         titleLabel.font = .heading_h3
         titleLabel.textColor = .text_5
@@ -148,5 +153,7 @@ extension LNMineWalletInfoView {
             make.bottom.equalToSuperview()
             make.trailing.lessThanOrEqualToSuperview()
         }
+        
+        subviews.forEach { $0.isUserInteractionEnabled = false }
     }
 }

+ 2 - 1
Lanu/Views/Profile/Profile/LNProfileInfosView.swift

@@ -91,6 +91,7 @@ extension LNProfileInfosView: LNProfileUserDetailViewDelegate {
 extension LNProfileInfosView {
     private func updateViewHeight() {
         var height = max(detailView.contentHeight, photoWall.contentHeight) + tabView.bounds.height
+        print("-----test \(detailView.contentHeight) \(photoWall.contentHeight) \(maxHeight)")
         height = min(height, maxHeight)
         heightConstraint?.update(offset: height)
         if height > 0 {
@@ -151,7 +152,7 @@ extension LNProfileInfosView {
     }
     
     private func buildIntroduce() -> UIView {
-
+        detailView.delegate = self
         return detailView
     }
     

+ 5 - 1
Lanu/Views/Profile/Profile/LNProfilePhotoWall.swift

@@ -190,7 +190,11 @@ extension LNProfilePhotoWall {
             tableView.publisher(for: \.contentSize).removeDuplicates().sink
             { [weak self] newValue in
                 guard let self else { return }
-                contentHeight = tableViews.max(by: { $0.contentSize.height > $1.contentSize.height })?.contentSize.height ?? 0
+                let maxHeightItem = tableViews.max(by: {
+                    ($0.contentSize.height + $0.contentInset.top) >
+                    ($1.contentSize.height + $1.contentInset.top)
+                })
+                contentHeight = (maxHeightItem?.contentInset.top ?? 0) + (maxHeightItem?.contentSize.height ?? 0)
             }.store(in: &bag)
             stackView.addArrangedSubview(tableView)
             tableViews.append(tableView)

+ 1 - 0
Lanu/Views/Profile/Profile/LNProfileUserDetailView.swift

@@ -68,6 +68,7 @@ extension LNProfileUserDetailView {
         { [weak self] newValue in
             guard let self else { return }
             contentHeight = newValue.height
+            print("-----test \(newValue)")
         }.store(in: &bag)
         
         let fakeView = UIView()

+ 273 - 0
Lanu/Views/Wallet/Coin/LNCoinToDiamondPanel.swift

@@ -0,0 +1,273 @@
+//
+//  LNCoinToDiamondPanel.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/12/25.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNCoinToDiamondPanel: LNPopupView {
+    private let coinLabel = UILabel()
+    private let coinInput = UITextField()
+    private let diamondLabel = UILabel()
+    
+    private let exchangeButton = UIButton()
+    private let exchangeRatio = LNPurchaseManager.shared.coinToDiamondRatio ?? 1
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setupViews()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNCoinToDiamondPanel {
+    private func checkExchangeButton() {
+        let text = coinInput.text ?? ""
+        if text.isEmpty {
+            coinInput.attributedPlaceholder = .init(string: .init(key: "Please fill in"),
+                                                    attributes: [.font: UIFont.heading_h3,
+                                                                 .foregroundColor: UIColor.text_2])
+        } else {
+            coinInput.attributedPlaceholder = nil
+        }
+        
+        let value = Int(text) ?? 0
+        if value > 0 {
+            if !exchangeButton.isEnabled {
+                exchangeButton.isEnabled = true
+                exchangeButton.setBackgroundImage(.primary_8, for: .normal)
+            }
+            
+            diamondLabel.text = "\(exchangeRatio * Decimal(value)))"
+        } else if value <= 0 {
+            if exchangeButton.isEnabled {
+                exchangeButton.isEnabled = false
+                exchangeButton.setBackgroundImage(nil, for: .normal)
+            }
+            
+            diamondLabel.text = "0"
+        }
+    }
+    
+    private func setupViews() {
+        let titleView = buildTitleView()
+        container.addSubview(titleView)
+        titleView.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview()
+            make.top.equalToSuperview()
+        }
+        
+        let coinView = buildCoinView()
+        container.addSubview(coinView)
+        coinView.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview()
+            make.top.equalTo(titleView.snp.bottom).offset(4)
+        }
+        
+        let exchangeView = UIView()
+        exchangeView.backgroundColor = .fill_1
+        exchangeView.layer.cornerRadius = 12
+        container.addSubview(exchangeView)
+        exchangeView.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview().inset(16)
+            make.top.equalTo(coinView.snp.bottom).offset(4)
+        }
+        
+        let coinExchange = buildExchangeCoinView()
+        exchangeView.addSubview(coinExchange)
+        coinExchange.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview()
+            make.top.equalToSuperview()
+        }
+        
+        let line = UIView()
+        line.backgroundColor = .fill_3
+        exchangeView.addSubview(line)
+        line.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview().inset(10)
+            make.top.equalTo(coinExchange.snp.bottom).offset(4)
+        }
+        
+        let diamondView = buildDiamondView()
+        exchangeView.addSubview(diamondView)
+        diamondView.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview()
+            make.bottom.equalToSuperview()
+            make.top.equalTo(line.snp.bottom).offset(4)
+        }
+        
+        exchangeButton.setTitle(.init(key: "With Draw"), for: .normal)
+        exchangeButton.setTitleColor(.text_1, for: .normal)
+        exchangeButton.titleLabel?.font = .heading_h3
+        exchangeButton.setBackgroundImage(.primary_8, for: .normal)
+        exchangeButton.backgroundColor = .text_2
+        exchangeButton.layer.cornerRadius = 23.5
+        exchangeButton.clipsToBounds = true
+        exchangeButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            guard let amout = Double(coinInput.text ?? "") else { return }
+            guard amout > 0 else { return }
+            LNPurchaseManager.shared.exchangeCoinToDiamond(amount: amout) { [weak self] success in
+                guard let self else { return }
+                guard success else { return }
+                dismiss()
+                showToast(.init(key: "兑换成功!"))
+            }
+        }), for: .touchUpInside)
+        container.addSubview(exchangeButton)
+        exchangeButton.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview().inset(16)
+            make.height.equalTo(47)
+            make.top.equalTo(exchangeView.snp.bottom).offset(24)
+            make.bottom.equalToSuperview().offset(-safeBottomInset - 10)
+        }
+    }
+    
+    private func buildTitleView() -> UIView {
+        let titleView = UIView()
+        titleView.snp.makeConstraints { make in
+            make.height.equalTo(50)
+        }
+        
+        let titleLabel = UILabel()
+        titleLabel.text = .init(key: "Convert To Diamonds")
+        titleLabel.font = .heading_h3
+        titleLabel.textColor = .text_5
+        titleView.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(16)
+            make.centerY.equalToSuperview()
+        }
+        
+        return titleView
+    }
+    
+    private func buildCoinView() -> UIView {
+        let container = UIView()
+        container.snp.makeConstraints { make in
+            make.height.equalTo(36)
+        }
+        
+        let exchange = UIButton()
+        exchange.setTitle(.init(key: "全部兑换"), for: .normal)
+        exchange.setTitleColor(.text_6, for: .normal)
+        exchange.titleLabel?.font = .body_xs
+        exchange.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            coinInput.text = "\(Int(myWalletInfo.coin))"
+            checkExchangeButton()
+        }), for: .touchUpInside)
+        container.addSubview(exchange)
+        exchange.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview().offset(-16)
+        }
+        
+        let coin = UIImageView.coinImageView()
+        container.addSubview(coin)
+        coin.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(16)
+            make.centerY.equalToSuperview()
+            make.width.height.equalTo(24)
+        }
+        
+        coinLabel.text = "\(myWalletInfo.coin)"
+        coinLabel.font = .heading_h1
+        coinLabel.textColor = .text_5
+        container.addSubview(coinLabel)
+        coinLabel.snp.makeConstraints { make in
+            make.leading.equalTo(coin.snp.trailing).offset(2)
+            make.centerY.equalToSuperview()
+        }
+        
+        return container
+    }
+    
+    private func buildExchangeCoinView() -> UIView {
+        let container = UIView()
+        container.snp.makeConstraints { make in
+            make.height.equalTo(67)
+        }
+        
+        let titleLabel = UILabel()
+        titleLabel.text = .init(key: "From Coins")
+        titleLabel.font = .heading_h4
+        titleLabel.textColor = .text_5
+        container.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(12)
+            make.centerY.equalToSuperview()
+        }
+        
+        coinInput.font = .heading_h2
+        coinInput.textColor = .text_5
+        coinInput.attributedPlaceholder = .init(string: .init(key: "Please fill in"),
+                                                attributes: [.font: UIFont.heading_h3,
+                                                             .foregroundColor: UIColor.text_2])
+        coinInput.keyboardType = .numberPad
+        coinInput.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            checkExchangeButton()
+        }), for: .editingChanged)
+        container.addSubview(coinInput)
+        coinInput.snp.makeConstraints { make in
+            make.trailing.equalToSuperview().offset(-12)
+            make.centerY.equalToSuperview()
+        }
+        
+        let coin = UIImageView.coinImageView()
+        container.addSubview(coin)
+        coin.snp.makeConstraints { make in
+            make.trailing.equalTo(coinInput.snp.leading).offset(-2)
+            make.centerY.equalToSuperview()
+            make.width.height.equalTo(20)
+        }
+        
+        return container
+    }
+    
+    private func buildDiamondView() -> UIView {
+        let container = UIView()
+        container.snp.makeConstraints { make in
+            make.height.equalTo(67)
+        }
+        
+        let titleLabel = UILabel()
+        titleLabel.text = .init(key: "To Diamonds")
+        titleLabel.font = .heading_h4
+        titleLabel.textColor = .text_5
+        container.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(12)
+            make.centerY.equalToSuperview()
+        }
+        
+        diamondLabel.text = "0"
+        diamondLabel.font = .heading_h2
+        diamondLabel.textColor = .text_5
+        container.addSubview(diamondLabel)
+        diamondLabel.snp.makeConstraints { make in
+            make.trailing.equalToSuperview().offset(-12)
+            make.centerY.equalToSuperview()
+        }
+        
+        let diamond = UIImageView.diamondImageView()
+        container.addSubview(diamond)
+        diamond.snp.makeConstraints { make in
+            make.trailing.equalTo(diamondLabel.snp.leading).offset(-2)
+            make.centerY.equalToSuperview()
+            make.width.height.equalTo(20)
+        }
+        
+        return container
+    }
+}

+ 380 - 0
Lanu/Views/Wallet/Coin/LNCoinViewController.swift

@@ -0,0 +1,380 @@
+//
+//  LNCoinViewController.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/12/24.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+extension UIView {
+    func pushToCoinView() {
+        let vc = LNCoinViewController()
+        navigationController?.pushViewController(vc, animated: true)
+    }
+}
+
+
+class LNCoinViewController: LNViewController {
+    private let valueLabel = UILabel()
+    private let rechargeView = LNMultiLineStackView()
+    private var itemViews: [LNCoinRechargeItemView] = []
+    
+    private let rechargeButton = UIButton()
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        setupViews()
+        
+        loadGoodsList()
+        onUserWalletInfoChanged(info: myWalletInfo)
+        
+        // 获取转换比例,如果没有,触发更新
+        _ = LNPurchaseManager.shared.coinToDiamondRatio
+        
+        LNEventDeliver.addObserver(self)
+    }
+}
+
+extension LNCoinViewController {
+    private func loadGoodsList() {
+        LNPurchaseManager.shared.loadGoodsList(currencyType: .coin) { [weak self] list in
+            guard let self else { return }
+            guard let list else { return }
+            
+            itemViews.removeAll()
+            list.forEach {
+                let itemView = LNCoinRechargeItemView()
+                itemView.coinLabel.text = "\($0.coinRechargeAmount)"
+                itemView.moneyLabel.text = "IDR \($0.amount)"
+                itemView.goodId = $0.id
+                itemView.onTap { [weak self, weak itemView] in
+                    guard let self, let itemView else { return }
+                    updateSelection(newSelection: itemView)
+                }
+                
+                self.itemViews.append(itemView)
+            }
+            rechargeView.update(itemViews)
+            
+            updateSelection(newSelection: nil)
+        }
+    }
+}
+
+extension LNCoinViewController: LNPurchaseManagerNotify {
+    func onUserWalletInfoChanged(info: LNUserWalletInfo) {
+        valueLabel.text = "\(info.coin)"
+    }
+}
+
+extension LNCoinViewController {
+    private func updateSelection(newSelection: LNCoinRechargeItemView?) {
+        itemViews.forEach {
+            $0.isSelected = newSelection == $0
+        }
+        
+        if rechargeButton.isEnabled, newSelection == nil {
+            rechargeButton.isEnabled = false
+            rechargeButton.setBackgroundImage(nil, for: .normal)
+        } else if !rechargeButton.isEnabled, newSelection != nil {
+            rechargeButton.isEnabled = true
+            rechargeButton.setBackgroundImage(.primary_8, for: .normal)
+        }
+    }
+    
+    private func setupViews() {
+        navigationBarColor = .primary_1
+        view.backgroundColor = .primary_1
+        title = .init(key: "My Wallet")
+        
+        let bg = buildValueView()
+        view.addSubview(bg)
+        bg.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview().inset(15)
+            make.top.equalToSuperview().offset(8)
+        }
+        
+        let set = buildRechargeSet()
+        view.addSubview(set)
+        set.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview()
+            make.bottom.equalToSuperview()
+            make.top.equalTo(bg.snp.bottom).offset(-31)
+        }
+    }
+    
+    private func buildValueView() -> UIView {
+        let bg = UIImageView()
+        bg.image = .init(named: "ic_coin_bg")
+        bg.isUserInteractionEnabled = true
+        
+        let titleLabel = UILabel()
+        titleLabel.text = .init(key: "您的余额")
+        titleLabel.font = .heading_h4
+        titleLabel.textColor = .text_5
+        bg.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(30)
+            make.top.equalToSuperview().offset(38)
+        }
+        
+        let valueView = UIView()
+        bg.addSubview(valueView)
+        valueView.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(30)
+            make.top.equalTo(titleLabel.snp.bottom).offset(6)
+        }
+        
+        let coin = UIImageView.coinImageView()
+        valueView.addSubview(coin)
+        coin.snp.makeConstraints { make in
+            make.leading.equalToSuperview()
+            make.centerY.equalToSuperview()
+            make.width.height.equalTo(24)
+        }
+        
+        valueLabel.text = "0"
+        valueLabel.font = .heading_h1
+        valueLabel.textColor = .text_5
+        valueView.addSubview(valueLabel)
+        valueLabel.snp.makeConstraints { make in
+            make.verticalEdges.equalToSuperview()
+            make.trailing.equalToSuperview()
+            make.leading.equalTo(coin.snp.trailing).offset(2)
+        }
+        
+        let jumpButton = UIButton()
+        jumpButton.addAction(UIAction(handler: { _ in
+            guard LNPurchaseManager.shared.coinToDiamondRatio != nil else { return }
+            let panel = LNCoinToDiamondPanel()
+            panel.showIn()
+        }), for: .touchUpInside)
+        bg.addSubview(jumpButton)
+        jumpButton.snp.makeConstraints { make in
+            make.top.equalToSuperview()
+            make.trailing.equalToSuperview().offset(-3)
+        }
+        
+        let contentView = UIView()
+        contentView.isUserInteractionEnabled = false
+        jumpButton.addSubview(contentView)
+        contentView.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+            make.leading.greaterThanOrEqualToSuperview().offset(8)
+        }
+        
+        let jumpTitleLabel = UILabel()
+        jumpTitleLabel.text = .init(key: "Convert To Diamonds")
+        jumpTitleLabel.font = .heading_h4
+        jumpTitleLabel.textColor = .text_1
+        contentView.addSubview(jumpTitleLabel)
+        jumpTitleLabel.snp.makeConstraints { make in
+            make.leading.verticalEdges.equalToSuperview()
+        }
+        
+        let arrow = UIImageView.arrowImageView(size: 16)
+        arrow.tintColor = .white
+        contentView.addSubview(arrow)
+        arrow.snp.makeConstraints { make in
+            make.leading.equalTo(jumpTitleLabel.snp.trailing).offset(2)
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview()
+        }
+        
+        return bg
+    }
+    
+    private func buildRechargeSet() -> UIView {
+        let container = UIView()
+        container.backgroundColor = .fill
+        container.layer.cornerRadius = 20
+        container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
+        
+        let scrollView = UIScrollView()
+        scrollView.showsVerticalScrollIndicator = false
+        scrollView.showsHorizontalScrollIndicator = false
+        container.addSubview(scrollView)
+        scrollView.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview().inset(16)
+            make.top.equalToSuperview().offset(20)
+            make.bottom.equalToSuperview().offset(-view.safeBottomInset - 10)
+        }
+        
+        let fakeView = UIView()
+        scrollView.addSubview(fakeView)
+        fakeView.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview()
+            make.top.width.equalToSuperview()
+            make.height.equalTo(0)
+        }
+        
+        rechargeView.columns = 3
+        rechargeView.spacing = 16
+        rechargeView.defaultSize = LNCoinRechargeItemView.size
+        scrollView.addSubview(rechargeView)
+        rechargeView.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview()
+            make.top.equalToSuperview()
+        }
+        
+        rechargeButton.backgroundColor = .text_2
+        rechargeButton.layer.cornerRadius = 23.5
+        rechargeButton.setBackgroundImage(.primary_8, for: .normal)
+        rechargeButton.setTitle(.init(key: "充值"), for: .normal)
+        rechargeButton.setTitleColor(.text_1, for: .normal)
+        rechargeButton.titleLabel?.font = .heading_h3
+        rechargeButton.clipsToBounds = true
+        scrollView.addSubview(rechargeButton)
+        rechargeButton.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview()
+            make.top.equalTo(rechargeView.snp.bottom).offset(20)
+            make.height.equalTo(47)
+        }
+        
+        let introduce = buildDescView()
+        scrollView.addSubview(introduce)
+        introduce.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview()
+            make.top.equalTo(rechargeButton.snp.bottom).offset(20)
+            make.bottom.equalToSuperview()
+        }
+        
+        return container
+    }
+    
+    private func buildDescView() -> UIView {
+        let container = UIView()
+        
+        let titleLabel = UILabel()
+        titleLabel.text = .init(key: "Recharge Instructions")
+        titleLabel.font = .heading_h5
+        titleLabel.textColor = .text_5
+        container.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.leading.top.equalToSuperview()
+        }
+        
+        let text: String = .init(key: "1. After the recharge is successful, there will definitelybe a delay in the account, please wait;\n2.lf you encounter any problems with the rechargeplease click Feedback")
+        let attrStr = NSMutableAttributedString(string: text)
+        let agreementRange = (text as NSString).range(of: .init(key: "Feedback"))
+        attrStr.addAttribute(.link, value: String.serviceUrl, range: agreementRange)
+        
+        let textView = LNAutoSizeTextView()
+        textView.backgroundColor = .clear
+        textView.attributedText = attrStr
+        textView.font = .body_s
+        textView.textColor = .text_4
+        textView.linkTextAttributes = [.foregroundColor: UIColor.primary_5]
+        container.addSubview(textView)
+        textView.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview().inset(-4)
+            make.top.equalTo(titleLabel.snp.bottom).offset(-7)
+            make.bottom.equalToSuperview()
+        }
+        
+        return container
+    }
+}
+
+private class LNCoinRechargeItemView: UIView {
+    static let size: CGSize = .init(width: 107, height: 76)
+    let coinLabel = UILabel()
+    let moneyLabel = UILabel()
+    var goodId: String = ""
+    
+    var isSelected: Bool = false {
+        didSet {
+            bg.isHidden = !isSelected
+        }
+    }
+    
+    private let bg = UIImageView()
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        backgroundColor = .fill_1
+        layer.cornerRadius = 12
+        clipsToBounds = true
+        snp.makeConstraints { make in
+            make.size.equalTo(Self.size)
+        }
+        
+        bg.image = .primary_7
+        addSubview(bg)
+        bg.snp.makeConstraints { make in
+            make.directionalEdges.equalToSuperview()
+        }
+        
+        let cover = UIView()
+        cover.backgroundColor = .fill_1
+        cover.layer.cornerRadius = 11
+        bg.addSubview(cover)
+        cover.snp.makeConstraints { make in
+            make.directionalEdges.equalToSuperview().inset(1)
+        }
+        
+        let container = UIView()
+        addSubview(container)
+        container.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+        }
+        
+        let coinView = UIView()
+        container.addSubview(coinView)
+        coinView.snp.makeConstraints { make in
+            make.centerX.top.equalToSuperview()
+        }
+        
+        let coin = UIImageView.coinImageView()
+        coinView.addSubview(coin)
+        coin.snp.makeConstraints { make in
+            make.leading.centerY.equalToSuperview()
+            make.width.height.equalTo(18)
+        }
+        
+        coinLabel.font = .heading_h2
+        coinLabel.textColor = .text_5
+        coinView.addSubview(coinLabel)
+        coinLabel.snp.makeConstraints { make in
+            make.verticalEdges.equalToSuperview()
+            make.trailing.equalToSuperview()
+            make.leading.equalTo(coin.snp.trailing).offset(2)
+        }
+        
+        moneyLabel.font = .body_s
+        moneyLabel.textColor = .text_4
+        container.addSubview(moneyLabel)
+        moneyLabel.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview()
+            make.top.equalTo(coinView.snp.bottom).offset(4)
+            make.bottom.equalToSuperview()
+        }
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+#if DEBUG
+
+import SwiftUI
+
+struct LNCoinViewControllerPreview: UIViewControllerRepresentable {
+    func makeUIViewController(context: Context) -> some UIViewController {
+        LNCoinViewController()
+    }
+    
+    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { }
+}
+
+#Preview(body: {
+    LNCoinViewControllerPreview()
+})
+#endif

+ 272 - 0
Lanu/Views/Wallet/Diamond/LNDiamondToCoinPanel.swift

@@ -0,0 +1,272 @@
+//
+//  LNDiamondToCoinPanel.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/12/25.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNDiamondToCoinPanel: LNPopupView {
+    private let diamondLabel = UILabel()
+    private let diamondInput = UITextField()
+    private let coinLabel = UILabel()
+    
+    private let exchangeButton = UIButton()
+    private let exchangeRatio = LNPurchaseManager.shared.diamondToCoinRatio ?? 1
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setupViews()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNDiamondToCoinPanel {
+    private func checkExchangeButton() {
+        let text = diamondInput.text ?? ""
+        if text.isEmpty {
+            diamondInput.attributedPlaceholder = .init(string: .init(key: "Please fill in"),
+                                                    attributes: [.font: UIFont.heading_h3,
+                                                                 .foregroundColor: UIColor.text_2])
+        } else {
+            diamondInput.attributedPlaceholder = nil
+        }
+        
+        let value = Int(text) ?? 0
+        let coinValue = exchangeRatio * Decimal(value)
+        if coinValue >= 1.0 {
+            if !exchangeButton.isEnabled {
+                exchangeButton.isEnabled = true
+                exchangeButton.setBackgroundImage(.primary_8, for: .normal)
+            }
+        } else {
+            if exchangeButton.isEnabled {
+                exchangeButton.isEnabled = false
+                exchangeButton.setBackgroundImage(nil, for: .normal)
+            }
+        }
+        coinLabel.text = "\(coinValue)"
+    }
+    
+    private func setupViews() {
+        let titleView = buildTitleView()
+        container.addSubview(titleView)
+        titleView.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview()
+            make.top.equalToSuperview()
+        }
+        
+        let diamondView = buildDiamond()
+        container.addSubview(diamondView)
+        diamondView.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview()
+            make.top.equalTo(titleView.snp.bottom).offset(4)
+        }
+        
+        let exchangeView = UIView()
+        exchangeView.backgroundColor = .fill_1
+        exchangeView.layer.cornerRadius = 12
+        container.addSubview(exchangeView)
+        exchangeView.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview().inset(16)
+            make.top.equalTo(diamondView.snp.bottom).offset(4)
+        }
+        
+        let diamondExchange = buildExchangeDiamondView()
+        exchangeView.addSubview(diamondExchange)
+        diamondExchange.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview()
+            make.top.equalToSuperview()
+        }
+        
+        let line = UIView()
+        line.backgroundColor = .fill_3
+        exchangeView.addSubview(line)
+        line.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview().inset(10)
+            make.top.equalTo(diamondExchange.snp.bottom).offset(4)
+        }
+        
+        let coinView = buildCoinView()
+        exchangeView.addSubview(coinView)
+        coinView.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview()
+            make.bottom.equalToSuperview()
+            make.top.equalTo(line.snp.bottom).offset(4)
+        }
+        
+        exchangeButton.setTitle(.init(key: "With Draw"), for: .normal)
+        exchangeButton.setTitleColor(.text_1, for: .normal)
+        exchangeButton.titleLabel?.font = .heading_h3
+        exchangeButton.setBackgroundImage(.primary_8, for: .normal)
+        exchangeButton.backgroundColor = .text_2
+        exchangeButton.layer.cornerRadius = 23.5
+        exchangeButton.clipsToBounds = true
+        exchangeButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            guard let amout = Double(diamondInput.text ?? "") else { return }
+            guard amout > 0 else { return }
+            LNPurchaseManager.shared.exchangeDiamondToCoin(amount: amout) { [weak self] success in
+                guard let self else { return }
+                guard success else { return }
+                dismiss()
+                showToast(.init(key: "兑换成功!"))
+            }
+        }), for: .touchUpInside)
+        container.addSubview(exchangeButton)
+        exchangeButton.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview().inset(16)
+            make.height.equalTo(47)
+            make.top.equalTo(exchangeView.snp.bottom).offset(24)
+            make.bottom.equalToSuperview().offset(-safeBottomInset - 10)
+        }
+    }
+    
+    private func buildTitleView() -> UIView {
+        let titleView = UIView()
+        titleView.snp.makeConstraints { make in
+            make.height.equalTo(50)
+        }
+        
+        let titleLabel = UILabel()
+        titleLabel.text = .init(key: "Convert To Coins")
+        titleLabel.font = .heading_h3
+        titleLabel.textColor = .text_5
+        titleView.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(16)
+            make.centerY.equalToSuperview()
+        }
+        
+        return titleView
+    }
+    
+    private func buildDiamond() -> UIView {
+        let container = UIView()
+        container.snp.makeConstraints { make in
+            make.height.equalTo(36)
+        }
+        
+        let exchange = UIButton()
+        exchange.setTitle(.init(key: "全部兑换"), for: .normal)
+        exchange.setTitleColor(.text_6, for: .normal)
+        exchange.titleLabel?.font = .body_xs
+        exchange.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            diamondInput.text = "\(Int(myWalletInfo.diamond))"
+            checkExchangeButton()
+        }), for: .touchUpInside)
+        container.addSubview(exchange)
+        exchange.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview().offset(-16)
+        }
+        
+        let diamond = UIImageView.diamondImageView()
+        container.addSubview(diamond)
+        diamond.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(16)
+            make.centerY.equalToSuperview()
+            make.width.height.equalTo(24)
+        }
+        
+        diamondLabel.text = "\(myWalletInfo.diamond)"
+        diamondLabel.font = .heading_h1
+        diamondLabel.textColor = .text_5
+        container.addSubview(diamondLabel)
+        diamondLabel.snp.makeConstraints { make in
+            make.leading.equalTo(diamond.snp.trailing).offset(2)
+            make.centerY.equalToSuperview()
+        }
+        
+        return container
+    }
+    
+    private func buildExchangeDiamondView() -> UIView {
+        let container = UIView()
+        container.snp.makeConstraints { make in
+            make.height.equalTo(67)
+        }
+        
+        let titleLabel = UILabel()
+        titleLabel.text = .init(key: "From Diamonds")
+        titleLabel.font = .heading_h4
+        titleLabel.textColor = .text_5
+        container.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(12)
+            make.centerY.equalToSuperview()
+        }
+        
+        diamondInput.font = .heading_h2
+        diamondInput.textColor = .text_5
+        diamondInput.attributedPlaceholder = .init(string: .init(key: "Please fill in"),
+                                                attributes: [.font: UIFont.heading_h3,
+                                                             .foregroundColor: UIColor.text_2])
+        diamondInput.keyboardType = .numberPad
+        diamondInput.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            checkExchangeButton()
+        }), for: .editingChanged)
+        container.addSubview(diamondInput)
+        diamondInput.snp.makeConstraints { make in
+            make.trailing.equalToSuperview().offset(-12)
+            make.centerY.equalToSuperview()
+        }
+        
+        let diamond = UIImageView.diamondImageView()
+        container.addSubview(diamond)
+        diamond.snp.makeConstraints { make in
+            make.trailing.equalTo(diamondInput.snp.leading).offset(-2)
+            make.centerY.equalToSuperview()
+            make.width.height.equalTo(20)
+        }
+        
+        return container
+    }
+    
+    private func buildCoinView() -> UIView {
+        let container = UIView()
+        container.snp.makeConstraints { make in
+            make.height.equalTo(67)
+        }
+        
+        let titleLabel = UILabel()
+        titleLabel.text = .init(key: "To Coins")
+        titleLabel.font = .heading_h4
+        titleLabel.textColor = .text_5
+        container.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(12)
+            make.centerY.equalToSuperview()
+        }
+        
+        coinLabel.text = "0"
+        coinLabel.font = .heading_h2
+        coinLabel.textColor = .text_5
+        container.addSubview(coinLabel)
+        coinLabel.snp.makeConstraints { make in
+            make.trailing.equalToSuperview().offset(-12)
+            make.centerY.equalToSuperview()
+        }
+        
+        let coin = UIImageView.coinImageView()
+        container.addSubview(coin)
+        coin.snp.makeConstraints { make in
+            make.trailing.equalTo(coinLabel.snp.leading).offset(-2)
+            make.centerY.equalToSuperview()
+            make.width.height.equalTo(20)
+        }
+        
+        return container
+    }
+}
+

+ 381 - 0
Lanu/Views/Wallet/Diamond/LNDiamondViewController.swift

@@ -0,0 +1,381 @@
+//
+//  LNDiamondViewController.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/12/25.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+extension UIView {
+    func pushToDiamondView() {
+        let vc = LNDiamondViewController()
+        navigationController?.pushViewController(vc, animated: true)
+    }
+}
+
+
+class LNDiamondViewController: LNViewController {
+    private let valueLabel = UILabel()
+    private let rechargeView = LNMultiLineStackView()
+    private var itemViews: [LNDiamondRechargeItemView] = []
+    
+    private let rechargeButton = UIButton()
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        setupViews()
+        
+        loadGoodsList()
+        onUserWalletInfoChanged(info: myWalletInfo)
+        
+        // 获取转换比例,如果没有,触发更新
+        _ = LNPurchaseManager.shared.diamondToCoinRatio
+        
+        LNEventDeliver.addObserver(self)
+    }
+}
+
+extension LNDiamondViewController {
+    private func loadGoodsList() {
+        LNPurchaseManager.shared.loadGoodsList(currencyType: .diamond) { [weak self] list in
+            guard let self else { return }
+            guard let list else { return }
+            
+            itemViews.removeAll()
+            list.forEach {
+                let itemView = LNDiamondRechargeItemView()
+                itemView.diamondLabel.text = "\($0.coinRechargeAmount)"
+                itemView.moneyLabel.text = "IDR \($0.amount)"
+                itemView.goodId = $0.id
+                itemView.onTap { [weak self, weak itemView] in
+                    guard let self, let itemView else { return }
+                    updateSelection(newSelection: itemView)
+                }
+                
+                self.itemViews.append(itemView)
+            }
+            rechargeView.update(itemViews)
+            
+            updateSelection(newSelection: nil)
+        }
+    }
+}
+
+extension LNDiamondViewController: LNPurchaseManagerNotify {
+    func onUserWalletInfoChanged(info: LNUserWalletInfo) {
+        valueLabel.text = "\(info.diamond)"
+    }
+}
+
+extension LNDiamondViewController {
+    private func updateSelection(newSelection: LNDiamondRechargeItemView?) {
+        itemViews.forEach {
+            $0.isSelected = newSelection == $0
+        }
+        
+        if rechargeButton.isEnabled, newSelection == nil {
+            rechargeButton.isEnabled = false
+            rechargeButton.setBackgroundImage(nil, for: .normal)
+        } else if !rechargeButton.isEnabled, newSelection != nil {
+            rechargeButton.isEnabled = true
+            rechargeButton.setBackgroundImage(.primary_8, for: .normal)
+        }
+    }
+    
+    private func setupViews() {
+        navigationBarColor = .primary_1
+        view.backgroundColor = .primary_1
+        title = .init(key: "My Wallet")
+        
+        let bg = buildValueView()
+        view.addSubview(bg)
+        bg.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview().inset(15)
+            make.top.equalToSuperview().offset(8)
+        }
+        
+        let set = buildRechargeSet()
+        view.addSubview(set)
+        set.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview()
+            make.bottom.equalToSuperview()
+            make.top.equalTo(bg.snp.bottom).offset(-31)
+        }
+    }
+    
+    private func buildValueView() -> UIView {
+        let bg = UIImageView()
+        bg.image = .init(named: "ic_diamond_bg")
+        bg.isUserInteractionEnabled = true
+        
+        let titleLabel = UILabel()
+        titleLabel.text = .init(key: "您的余额")
+        titleLabel.font = .heading_h4
+        titleLabel.textColor = .text_5
+        bg.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(30)
+            make.top.equalToSuperview().offset(38)
+        }
+        
+        let valueView = UIView()
+        bg.addSubview(valueView)
+        valueView.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(30)
+            make.top.equalTo(titleLabel.snp.bottom).offset(6)
+        }
+        
+        let diamond = UIImageView.diamondImageView()
+        valueView.addSubview(diamond)
+        diamond.snp.makeConstraints { make in
+            make.leading.equalToSuperview()
+            make.centerY.equalToSuperview()
+            make.width.height.equalTo(24)
+        }
+        
+        valueLabel.text = "0"
+        valueLabel.font = .heading_h1
+        valueLabel.textColor = .text_5
+        valueView.addSubview(valueLabel)
+        valueLabel.snp.makeConstraints { make in
+            make.verticalEdges.equalToSuperview()
+            make.trailing.equalToSuperview()
+            make.leading.equalTo(diamond.snp.trailing).offset(2)
+        }
+        
+        let jumpButton = UIButton()
+        jumpButton.addAction(UIAction(handler: { _ in
+            guard LNPurchaseManager.shared.diamondToCoinRatio != nil else { return }
+            let panel = LNDiamondToCoinPanel()
+            panel.showIn()
+        }), for: .touchUpInside)
+        bg.addSubview(jumpButton)
+        jumpButton.snp.makeConstraints { make in
+            make.top.equalToSuperview()
+            make.trailing.equalToSuperview().offset(-3)
+        }
+        
+        let contentView = UIView()
+        contentView.isUserInteractionEnabled = false
+        jumpButton.addSubview(contentView)
+        contentView.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+            make.leading.greaterThanOrEqualToSuperview().offset(8)
+        }
+        
+        let jumpTitleLabel = UILabel()
+        jumpTitleLabel.text = .init(key: "Convert To Coins")
+        jumpTitleLabel.font = .heading_h4
+        jumpTitleLabel.textColor = .text_1
+        contentView.addSubview(jumpTitleLabel)
+        jumpTitleLabel.snp.makeConstraints { make in
+            make.leading.verticalEdges.equalToSuperview()
+        }
+        
+        let arrow = UIImageView.arrowImageView(size: 16)
+        arrow.tintColor = .white
+        contentView.addSubview(arrow)
+        arrow.snp.makeConstraints { make in
+            make.leading.equalTo(jumpTitleLabel.snp.trailing).offset(2)
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview()
+        }
+        
+        return bg
+    }
+    
+    private func buildRechargeSet() -> UIView {
+        let container = UIView()
+        container.backgroundColor = .fill
+        container.layer.cornerRadius = 20
+        container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
+        
+        let scrollView = UIScrollView()
+        scrollView.showsVerticalScrollIndicator = false
+        scrollView.showsHorizontalScrollIndicator = false
+        container.addSubview(scrollView)
+        scrollView.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview().inset(16)
+            make.top.equalToSuperview().offset(20)
+            make.bottom.equalToSuperview().offset(-view.safeBottomInset - 10)
+        }
+        
+        let fakeView = UIView()
+        scrollView.addSubview(fakeView)
+        fakeView.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview()
+            make.top.width.equalToSuperview()
+            make.height.equalTo(0)
+        }
+        
+        rechargeView.columns = 3
+        rechargeView.spacing = 16
+        rechargeView.defaultSize = LNDiamondRechargeItemView.size
+        scrollView.addSubview(rechargeView)
+        rechargeView.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview()
+            make.top.equalToSuperview()
+        }
+        
+        rechargeButton.backgroundColor = .text_2
+        rechargeButton.layer.cornerRadius = 23.5
+        rechargeButton.setBackgroundImage(.primary_8, for: .normal)
+        rechargeButton.setTitle(.init(key: "充值"), for: .normal)
+        rechargeButton.setTitleColor(.text_1, for: .normal)
+        rechargeButton.titleLabel?.font = .heading_h3
+        rechargeButton.clipsToBounds = true
+        scrollView.addSubview(rechargeButton)
+        rechargeButton.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview()
+            make.top.equalTo(rechargeView.snp.bottom).offset(20)
+            make.height.equalTo(47)
+        }
+        
+        let introduce = buildDescView()
+        scrollView.addSubview(introduce)
+        introduce.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview()
+            make.top.equalTo(rechargeButton.snp.bottom).offset(20)
+            make.bottom.equalToSuperview()
+        }
+        
+        return container
+    }
+    
+    private func buildDescView() -> UIView {
+        let container = UIView()
+        
+        let titleLabel = UILabel()
+        titleLabel.text = .init(key: "Recharge Instructions")
+        titleLabel.font = .heading_h5
+        titleLabel.textColor = .text_5
+        container.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.leading.top.equalToSuperview()
+        }
+        
+        let text: String = .init(key: "1. After the recharge is successful, there will definitelybe a delay in the account, please wait;\n2.lf you encounter any problems with the rechargeplease click Feedback")
+        let attrStr = NSMutableAttributedString(string: text)
+        let agreementRange = (text as NSString).range(of: .init(key: "Feedback"))
+        attrStr.addAttribute(.link, value: String.serviceUrl, range: agreementRange)
+        
+        let textView = LNAutoSizeTextView()
+        textView.backgroundColor = .clear
+        textView.attributedText = attrStr
+        textView.font = .body_s
+        textView.textColor = .text_4
+        textView.linkTextAttributes = [.foregroundColor: UIColor.primary_5]
+        container.addSubview(textView)
+        textView.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview().inset(-4)
+            make.top.equalTo(titleLabel.snp.bottom).offset(-7)
+            make.bottom.equalToSuperview()
+        }
+        
+        return container
+    }
+}
+
+private class LNDiamondRechargeItemView: UIView {
+    static let size: CGSize = .init(width: 107, height: 76)
+    let diamondLabel = UILabel()
+    let moneyLabel = UILabel()
+    var goodId: String = ""
+    
+    var isSelected: Bool = false {
+        didSet {
+            bg.isHidden = !isSelected
+        }
+    }
+    
+    private let bg = UIImageView()
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        backgroundColor = .fill_1
+        layer.cornerRadius = 12
+        clipsToBounds = true
+        snp.makeConstraints { make in
+            make.size.equalTo(Self.size)
+        }
+        
+        bg.image = .primary_7
+        addSubview(bg)
+        bg.snp.makeConstraints { make in
+            make.directionalEdges.equalToSuperview()
+        }
+        
+        let cover = UIView()
+        cover.backgroundColor = .fill_1
+        cover.layer.cornerRadius = 11
+        bg.addSubview(cover)
+        cover.snp.makeConstraints { make in
+            make.directionalEdges.equalToSuperview().inset(1)
+        }
+        
+        let container = UIView()
+        addSubview(container)
+        container.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+        }
+        
+        let diamondView = UIView()
+        container.addSubview(diamondView)
+        diamondView.snp.makeConstraints { make in
+            make.centerX.top.equalToSuperview()
+        }
+        
+        let diamond = UIImageView.diamondImageView()
+        diamondView.addSubview(diamond)
+        diamond.snp.makeConstraints { make in
+            make.leading.centerY.equalToSuperview()
+            make.width.height.equalTo(18)
+        }
+        
+        diamondLabel.font = .heading_h2
+        diamondLabel.textColor = .text_5
+        diamondView.addSubview(diamondLabel)
+        diamondLabel.snp.makeConstraints { make in
+            make.verticalEdges.equalToSuperview()
+            make.trailing.equalToSuperview()
+            make.leading.equalTo(diamond.snp.trailing).offset(2)
+        }
+        
+        moneyLabel.font = .body_s
+        moneyLabel.textColor = .text_4
+        container.addSubview(moneyLabel)
+        moneyLabel.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview()
+            make.top.equalTo(diamondView.snp.bottom).offset(4)
+            make.bottom.equalToSuperview()
+        }
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+#if DEBUG
+
+import SwiftUI
+
+struct LNDiamondViewControllerPreview: UIViewControllerRepresentable {
+    func makeUIViewController(context: Context) -> some UIViewController {
+        LNDiamondViewController()
+    }
+    
+    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { }
+}
+
+#Preview(body: {
+    LNDiamondViewControllerPreview()
+})
+#endif
+

+ 466 - 0
Lanu/Views/Wallet/LNWalletViewController.swift

@@ -0,0 +1,466 @@
+//
+//  LNWalletViewController.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/12/24.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+extension UIView {
+    func pushToWallet() {
+        let vc = LNWalletViewController()
+        navigationController?.pushViewController(vc, animated: true)
+    }
+}
+
+
+class LNWalletViewController: LNViewController {
+    private let coinLabel = UILabel()
+    private let diamondLabel = UILabel()
+    private let beanLabel = UILabel()
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        showNavigationBar = false
+        setupViews()
+        
+        updateWalletInfo()
+    }
+}
+
+extension LNWalletViewController {
+    private func updateWalletInfo() {
+        diamondLabel.text = "\(myWalletInfo.diamond)"
+        coinLabel.text = "\(myWalletInfo.coin)"
+    }
+    
+    private func setupViews() {
+        view.backgroundColor = .primary_1
+        
+        let topCover = UIImageView()
+        topCover.image = .init(named: "ic_home_top_bg")
+        view.addSubview(topCover)
+        topCover.snp.makeConstraints { make in
+            make.top.leading.trailing.equalToSuperview()
+        }
+        
+        let naviBar = buildFakeNavBar()
+        view.addSubview(naviBar)
+        naviBar.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview()
+            make.top.equalToSuperview()
+        }
+        
+        let stackView = UIStackView()
+        stackView.axis = .vertical
+        stackView.spacing = 16
+        view.addSubview(stackView)
+        stackView.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview().inset(16)
+            make.top.equalTo(naviBar.snp.bottom).offset(16)
+        }
+        
+        stackView.addArrangedSubview(buildWallet())
+        
+        if myUserInfo.playmate {
+            stackView.addArrangedSubview(buildIncome())
+        }
+    }
+    
+    private func buildFakeNavBar() -> UIView {
+        let fakeBar = UIView()
+        fakeBar.snp.makeConstraints { make in
+            make.height.equalTo((navigationController?.navigationBar.bounds.height ?? 44) + UIView.statusBarHeight)
+        }
+        
+        let barView = UIView()
+        fakeBar.addSubview(barView)
+        barView.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview()
+            make.bottom.equalToSuperview()
+            make.height.equalTo(navigationController?.navigationBar.bounds.height ?? 44)
+        }
+        
+        let closeButton = UIButton(type: .system)
+        closeButton.setImage(UIImage(systemName: "chevron.backward"), for: .normal)
+        closeButton.tintColor = .text_5
+        closeButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            navigationController?.popViewController(animated: true)
+        }), for: .touchUpInside)
+        closeButton.translatesAutoresizingMaskIntoConstraints = false
+        barView.addSubview(closeButton)
+        closeButton.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(16)
+            make.centerY.equalToSuperview()
+            make.width.height.equalTo(24)
+        }
+        
+        let titleLabel = UILabel()
+        titleLabel.text = .init(key: "My Wallet")
+        titleLabel.font = .heading_h2
+        titleLabel.textColor = .text_5
+        barView.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+        }
+        
+        let history = UIButton()
+        history.setImage(.init(named: "ic_wallet_history"), for: .normal)
+        history.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+        }), for: .touchUpInside)
+        barView.addSubview(history)
+        history.snp.makeConstraints { make in
+            make.trailing.equalToSuperview().offset(-16)
+            make.centerY.equalToSuperview()
+            make.width.height.equalTo(30)
+        }
+        
+        return fakeBar
+    }
+    
+    private func buildWallet() -> UIView {
+        let container = UIView()
+        container.layer.cornerRadius = 12
+        container.backgroundColor = .fill
+        
+        let titleView = UIView()
+        container.addSubview(titleView)
+        titleView.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview()
+            make.top.equalToSuperview()
+            make.height.equalTo(40)
+        }
+        
+        let titleLabel = UILabel()
+        titleLabel.text = .init(key: "My Balance")
+        titleLabel.font = .heading_h3
+        titleLabel.textColor = .text_5
+        titleView.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(16)
+            make.centerY.equalToSuperview()
+        }
+        
+        let coinView = buildCoinView()
+        container.addSubview(coinView)
+        coinView.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview().inset(16)
+            make.top.equalTo(titleLabel.snp.bottom)
+            make.height.equalTo(64)
+        }
+        
+        let diamondView = buildDiamondView()
+        container.addSubview(diamondView)
+        diamondView.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview().inset(16)
+            make.top.equalTo(coinView.snp.bottom).offset(10)
+            make.height.equalTo(64)
+            make.bottom.equalToSuperview().offset(-12)
+        }
+        
+        return container
+    }
+    
+    private func buildCoinView() -> UIView {
+        let container = UIView()
+        container.backgroundColor = .init(hex: "#FFC4000D")
+        container.layer.cornerRadius = 12
+        
+        let coin = UIImageView.coinImageView()
+        container.addSubview(coin)
+        coin.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(14)
+            make.centerY.equalToSuperview()
+            make.width.height.equalTo(42)
+        }
+        
+        let textView = UIView()
+        container.addSubview(textView)
+        textView.snp.makeConstraints { make in
+            make.leading.equalTo(coin.snp.trailing).offset(8)
+            make.centerY.equalToSuperview()
+        }
+        
+        coinLabel.text = "0"
+        coinLabel.font = .heading_h1_5
+        coinLabel.textColor = .text_5
+        textView.addSubview(coinLabel)
+        coinLabel.snp.makeConstraints { make in
+            make.leading.top.equalToSuperview()
+            make.trailing.equalToSuperview()
+        }
+        
+        let descLabel = UILabel()
+        descLabel.text = .init(key: "Coin")
+        descLabel.font = .heading_h5
+        descLabel.textColor = .text_5
+        textView.addSubview(descLabel)
+        descLabel.snp.makeConstraints { make in
+            make.leading.trailing.bottom.equalToSuperview()
+            make.top.equalTo(coinLabel.snp.bottom).offset(-5)
+        }
+        
+        let jumpButton = UIButton()
+        jumpButton.setBackgroundImage(.primary_8, for: .normal)
+        jumpButton.layer.cornerRadius = 12
+        jumpButton.clipsToBounds = true
+        jumpButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            view.pushToCoinView()
+        }), for: .touchUpInside)
+        container.addSubview(jumpButton)
+        jumpButton.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview().offset(-14)
+            make.height.equalTo(24)
+        }
+        
+        let contentView = UIView()
+        contentView.isUserInteractionEnabled = false
+        jumpButton.addSubview(contentView)
+        contentView.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+            make.leading.greaterThanOrEqualToSuperview().offset(8)
+        }
+        
+        let jumpTitleLabel = UILabel()
+        jumpTitleLabel.text = .init(key: "Top-up")
+        jumpTitleLabel.font = .heading_h5
+        jumpTitleLabel.textColor = .text_1
+        contentView.addSubview(jumpTitleLabel)
+        jumpTitleLabel.snp.makeConstraints { make in
+            make.leading.verticalEdges.equalToSuperview()
+        }
+        
+        let arrow = UIImageView.arrowImageView(size: 10)
+        arrow.tintColor = .white
+        contentView.addSubview(arrow)
+        arrow.snp.makeConstraints { make in
+            make.leading.equalTo(jumpTitleLabel.snp.trailing).offset(2)
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview()
+        }
+        
+        return container
+    }
+    
+    private func buildDiamondView() -> UIView {
+        let container = UIView()
+        container.backgroundColor = .init(hex: "#008FFF0D")
+        container.layer.cornerRadius = 12
+        
+        let diamond = UIImageView.diamondImageView()
+        container.addSubview(diamond)
+        diamond.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(14)
+            make.centerY.equalToSuperview()
+            make.width.height.equalTo(42)
+        }
+        
+        let textView = UIView()
+        container.addSubview(textView)
+        textView.snp.makeConstraints { make in
+            make.leading.equalTo(diamond.snp.trailing).offset(8)
+            make.centerY.equalToSuperview()
+        }
+        
+        diamondLabel.text = "0"
+        diamondLabel.font = .heading_h1_5
+        diamondLabel.textColor = .text_5
+        textView.addSubview(diamondLabel)
+        diamondLabel.snp.makeConstraints { make in
+            make.leading.top.equalToSuperview()
+            make.trailing.equalToSuperview()
+        }
+        
+        let descLabel = UILabel()
+        descLabel.text = .init(key: "Diamond")
+        descLabel.font = .heading_h5
+        descLabel.textColor = .text_5
+        textView.addSubview(descLabel)
+        descLabel.snp.makeConstraints { make in
+            make.leading.trailing.bottom.equalToSuperview()
+            make.top.equalTo(diamondLabel.snp.bottom).offset(-5)
+        }
+        
+        let jumpButton = UIButton()
+        jumpButton.setBackgroundImage(.primary_8, for: .normal)
+        jumpButton.layer.cornerRadius = 12
+        jumpButton.clipsToBounds = true
+        jumpButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            view.pushToDiamondView()
+        }), for: .touchUpInside)
+        container.addSubview(jumpButton)
+        jumpButton.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview().offset(-14)
+            make.height.equalTo(24)
+        }
+        
+        let contentView = UIView()
+        contentView.isUserInteractionEnabled = false
+        jumpButton.addSubview(contentView)
+        contentView.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+            make.leading.greaterThanOrEqualToSuperview().offset(8)
+        }
+        
+        let jumpTitleLabel = UILabel()
+        jumpTitleLabel.text = .init(key: "Top-up")
+        jumpTitleLabel.font = .heading_h5
+        jumpTitleLabel.textColor = .text_1
+        contentView.addSubview(jumpTitleLabel)
+        jumpTitleLabel.snp.makeConstraints { make in
+            make.leading.verticalEdges.equalToSuperview()
+        }
+        
+        let arrow = UIImageView.arrowImageView(size: 10)
+        arrow.tintColor = .white
+        contentView.addSubview(arrow)
+        arrow.snp.makeConstraints { make in
+            make.leading.equalTo(jumpTitleLabel.snp.trailing).offset(2)
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview()
+        }
+        
+        return container
+    }
+    
+    private func buildIncome() -> UIView {
+        let container = UIView()
+        container.layer.cornerRadius = 12
+        container.backgroundColor = .fill
+        
+        let titleView = UIView()
+        container.addSubview(titleView)
+        titleView.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview()
+            make.top.equalToSuperview()
+            make.height.equalTo(40)
+        }
+        
+        let titleLabel = UILabel()
+        titleLabel.text = .init(key: "My Income")
+        titleLabel.font = .heading_h3
+        titleLabel.textColor = .text_5
+        titleView.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(16)
+            make.centerY.equalToSuperview()
+        }
+        
+        let beanView = buildBeanView()
+        container.addSubview(beanView)
+        beanView.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview().inset(16)
+            make.top.equalTo(titleLabel.snp.bottom)
+            make.height.equalTo(64)
+            make.bottom.equalToSuperview().offset(-10)
+        }
+        
+        return container
+    }
+    
+    private func buildBeanView() -> UIView {
+        let container = UIView()
+        container.backgroundColor = .init(hex: "#FF73000D")
+        container.layer.cornerRadius = 12
+        
+        let bean = UIImageView.beanImageView()
+        container.addSubview(bean)
+        bean.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(14)
+            make.centerY.equalToSuperview()
+            make.width.height.equalTo(42)
+        }
+        
+        let textView = UIView()
+        container.addSubview(textView)
+        textView.snp.makeConstraints { make in
+            make.leading.equalTo(bean.snp.trailing).offset(8)
+            make.centerY.equalToSuperview()
+        }
+        
+        beanLabel.text = "0"
+        beanLabel.font = .heading_h1_5
+        beanLabel.textColor = .text_5
+        textView.addSubview(beanLabel)
+        beanLabel.snp.makeConstraints { make in
+            make.leading.bottom.equalToSuperview()
+            make.trailing.equalToSuperview()
+        }
+        
+        let descLabel = UILabel()
+        descLabel.text = .init(key: "Beans")
+        descLabel.font = .heading_h5
+        descLabel.textColor = .text_5
+        textView.addSubview(descLabel)
+        descLabel.snp.makeConstraints { make in
+            make.leading.trailing.top.equalToSuperview()
+            make.bottom.equalTo(beanLabel.snp.top)
+        }
+        
+        let jumpButton = UIButton()
+        jumpButton.setBackgroundImage(.primary_8, for: .normal)
+        jumpButton.layer.cornerRadius = 12
+        jumpButton.clipsToBounds = true
+        container.addSubview(jumpButton)
+        jumpButton.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview().offset(-14)
+            make.height.equalTo(24)
+        }
+        
+        let contentView = UIView()
+        jumpButton.addSubview(contentView)
+        contentView.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+            make.leading.greaterThanOrEqualToSuperview().offset(8)
+        }
+        
+        let jumpTitleLabel = UILabel()
+        jumpTitleLabel.text = .init(key: "Details")
+        jumpTitleLabel.font = .heading_h5
+        jumpTitleLabel.textColor = .text_1
+        contentView.addSubview(jumpTitleLabel)
+        jumpTitleLabel.snp.makeConstraints { make in
+            make.leading.verticalEdges.equalToSuperview()
+        }
+        
+        let arrow = UIImageView.arrowImageView(size: 10)
+        arrow.tintColor = .white
+        contentView.addSubview(arrow)
+        arrow.snp.makeConstraints { make in
+            make.leading.equalTo(jumpTitleLabel.snp.trailing).offset(2)
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview()
+        }
+        
+        return container
+    }
+}
+
+#if DEBUG
+
+import SwiftUI
+
+struct LNWalletViewControllerPreview: UIViewControllerRepresentable {
+    func makeUIViewController(context: Context) -> some UIViewController {
+        LNWalletViewController()
+    }
+    
+    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { }
+}
+
+#Preview(body: {
+    LNWalletViewControllerPreview()
+})
+#endif