Browse Source

feat: 构建首页简单的逻辑,增加部分公共 UI 组件

陈文艺 4 months ago
parent
commit
6800731245
46 changed files with 1187 additions and 34 deletions
  1. 20 2
      Lanu.xcodeproj/project.pbxproj
  2. 2 4
      Lanu/AppDelegate.swift
  3. 6 0
      Lanu/Assets.xcassets/common/Contents.json
  4. 22 0
      Lanu/Assets.xcassets/common/ic_gender_female.imageset/Contents.json
  5. BIN
      Lanu/Assets.xcassets/common/ic_gender_female.imageset/ic_gender_female@2x.png
  6. BIN
      Lanu/Assets.xcassets/common/ic_gender_female.imageset/ic_gender_female@3x.png
  7. 22 0
      Lanu/Assets.xcassets/common/ic_gender_male.imageset/Contents.json
  8. BIN
      Lanu/Assets.xcassets/common/ic_gender_male.imageset/ic_gender_male@2x.png
  9. BIN
      Lanu/Assets.xcassets/common/ic_gender_male.imageset/ic_gender_male@3x.png
  10. 22 0
      Lanu/Assets.xcassets/common/ic_star.imageset/Contents.json
  11. BIN
      Lanu/Assets.xcassets/common/ic_star.imageset/ic_star@2x.png
  12. BIN
      Lanu/Assets.xcassets/common/ic_star.imageset/ic_star@3x.png
  13. 22 0
      Lanu/Assets.xcassets/common/ic_star_fill.imageset/Contents.json
  14. BIN
      Lanu/Assets.xcassets/common/ic_star_fill.imageset/ic_star_fill@2x.png
  15. BIN
      Lanu/Assets.xcassets/common/ic_star_fill.imageset/ic_star_fill@3x.png
  16. 6 0
      Lanu/Assets.xcassets/main/Contents.json
  17. 22 0
      Lanu/Assets.xcassets/main/ic_main_mine.imageset/Contents.json
  18. BIN
      Lanu/Assets.xcassets/main/ic_main_mine.imageset/ic_main_mine@2x.png
  19. BIN
      Lanu/Assets.xcassets/main/ic_main_mine.imageset/ic_main_mine@3x.png
  20. 22 0
      Lanu/Assets.xcassets/main/ic_main_tab_selected.imageset/Contents.json
  21. BIN
      Lanu/Assets.xcassets/main/ic_main_tab_selected.imageset/ic_main_tab_selected@2x.png
  22. BIN
      Lanu/Assets.xcassets/main/ic_main_tab_selected.imageset/ic_main_tab_selected@3x.png
  23. 22 0
      Lanu/Assets.xcassets/main/ic_main_top_bg.imageset/Contents.json
  24. BIN
      Lanu/Assets.xcassets/main/ic_main_top_bg.imageset/ic_main_top_bg@2x.png
  25. BIN
      Lanu/Assets.xcassets/main/ic_main_top_bg.imageset/ic_main_top_bg@3x.png
  26. 16 0
      Lanu/Common/Utils/Comparable+Extension.swift
  27. 32 0
      Lanu/Common/Utils/NSObject+Extension.swift
  28. 36 0
      Lanu/Common/Utils/UIView+Extension.swift
  29. 88 0
      Lanu/Common/Views/Gender/LNGenderView.swift
  30. 34 0
      Lanu/Common/Views/LNAutoSizeTextView.swift
  31. 143 0
      Lanu/Common/Views/LNPopupViewProtocol.swift
  32. 86 0
      Lanu/Common/Views/StarScore/LNFiveStarScoreView.swift
  33. 92 0
      Lanu/Common/Views/StarScore/LNStarScoreView.swift
  34. BIN
      Lanu/Files/Font/Poppins-Regular.ttf
  35. BIN
      Lanu/Files/Font/Poppins-SemiBold.ttf
  36. 5 0
      Lanu/Info.plist
  37. 73 1
      Lanu/Localizable.xcstrings
  38. 12 2
      Lanu/Manager/Account/LNAccountManager.swift
  39. 6 0
      Lanu/Manager/Account/Network/LNHttpManager+Login.swift
  40. 1 1
      Lanu/Manager/Profile/LNUserProfileInfo.swift
  41. 7 1
      Lanu/Manager/Profile/Network/LNProfileResponse.swift
  42. 29 7
      Lanu/Views/Login/LNLoginViewController.swift
  43. 37 0
      Lanu/Views/Login/LNPrivacyTextView.swift
  44. 75 0
      Lanu/Views/Main/LNMainTopMenuView.swift
  45. 135 0
      Lanu/Views/Main/LNMainTopTabView.swift
  46. 92 16
      Lanu/Views/Main/LNMainViewController.swift

+ 20 - 2
Lanu.xcodeproj/project.pbxproj

@@ -41,14 +41,23 @@
 				"Common/Theme/UIColor+Theme.swift",
 				"Common/Theme/UIFont+Theme.swift",
 				Common/Utils/AppUtils.swift,
+				"Common/Utils/Comparable+Extension.swift",
 				"Common/Utils/DispatchQueue+Extension.swift",
+				"Common/Utils/NSObject+Extension.swift",
 				"Common/Utils/String+Extension.swift",
 				"Common/Utils/UIColor+Extension.swift",
 				"Common/Utils/UIView+Extension.swift",
 				Common/Views/Base/LNNavigationController.swift,
 				Common/Views/Base/LNViewController.swift,
+				Common/Views/Gender/LNGenderView.swift,
+				Common/Views/LNAutoSizeTextView.swift,
+				Common/Views/LNPopupViewProtocol.swift,
+				Common/Views/StarScore/LNFiveStarScoreView.swift,
+				Common/Views/StarScore/LNStarScoreView.swift,
 				Config_Debug.xcconfig,
 				Config_Release.xcconfig,
+				"Files/Font/Poppins-Regular.ttf",
+				"Files/Font/Poppins-SemiBold.ttf",
 				"GoogleService-Info-Debug.plist",
 				"GoogleService-Info-Release.plist",
 				Localizable.xcstrings,
@@ -73,6 +82,9 @@
 				SceneDelegate.swift,
 				Views/IM/LNIMViewController.swift,
 				Views/Login/LNLoginViewController.swift,
+				Views/Login/LNPrivacyTextView.swift,
+				Views/Main/LNMainTopMenuView.swift,
+				Views/Main/LNMainTopTabView.swift,
 				Views/Main/LNMainViewController.swift,
 			);
 			target = FBFE13BF2EBC39B000DCE6E9 /* Lanu */;
@@ -90,8 +102,6 @@
 		};
 		FBB67E232EC48B440070E686 /* ThirdParty */ = {
 			isa = PBXFileSystemSynchronizedRootGroup;
-			exceptions = (
-			);
 			path = ThirdParty;
 			sourceTree = "<group>";
 		};
@@ -240,10 +250,14 @@
 			inputFileListPaths = (
 				"${PODS_ROOT}/Target Support Files/Pods-Lanu/Pods-Lanu-resources-${CONFIGURATION}-input-files.xcfilelist",
 			);
+			inputPaths = (
+			);
 			name = "[CP] Copy Pods Resources";
 			outputFileListPaths = (
 				"${PODS_ROOT}/Target Support Files/Pods-Lanu/Pods-Lanu-resources-${CONFIGURATION}-output-files.xcfilelist",
 			);
+			outputPaths = (
+			);
 			runOnlyForDeploymentPostprocessing = 0;
 			shellPath = /bin/sh;
 			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Lanu/Pods-Lanu-resources.sh\"\n";
@@ -279,10 +293,14 @@
 			inputFileListPaths = (
 				"${PODS_ROOT}/Target Support Files/Pods-Lanu/Pods-Lanu-frameworks-${CONFIGURATION}-input-files.xcfilelist",
 			);
+			inputPaths = (
+			);
 			name = "[CP] Embed Pods Frameworks";
 			outputFileListPaths = (
 				"${PODS_ROOT}/Target Support Files/Pods-Lanu/Pods-Lanu-frameworks-${CONFIGURATION}-output-files.xcfilelist",
 			);
+			outputPaths = (
+			);
 			runOnlyForDeploymentPostprocessing = 0;
 			shellPath = /bin/sh;
 			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Lanu/Pods-Lanu-frameworks.sh\"\n";

+ 2 - 4
Lanu/AppDelegate.swift

@@ -51,16 +51,14 @@ extension AppDelegate {
     }
     
     private func setupLogger() {
-        let formatter = LNLoggerFormater()
-        
 #if DEBUG // 只在 Debug 模式打印到终端
         let logger = DDOSLogger.sharedInstance
-        logger.logFormatter = formatter
+        logger.logFormatter = LNLoggerFormater()
         DDLog.add(logger)
 #endif
         
         let fileLogger = DDFileLogger()
-        fileLogger.logFormatter = formatter
+        fileLogger.logFormatter = LNLoggerFormater()
         fileLogger.rollingFrequency = 24 * 60 * 60 // 1 天轮转
         fileLogger.logFileManager.maximumNumberOfLogFiles = 7 // 最多保存 7 个文件
         fileLogger.maximumFileSize = 5 * 1024 * 1024 // 5M 最大限制

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

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

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

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

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


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


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

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

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


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


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

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

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


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


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

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

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


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


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

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

+ 22 - 0
Lanu/Assets.xcassets/main/ic_main_mine.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/main/ic_main_mine.imageset/ic_main_mine@2x.png


BIN
Lanu/Assets.xcassets/main/ic_main_mine.imageset/ic_main_mine@3x.png


+ 22 - 0
Lanu/Assets.xcassets/main/ic_main_tab_selected.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/main/ic_main_tab_selected.imageset/ic_main_tab_selected@2x.png


BIN
Lanu/Assets.xcassets/main/ic_main_tab_selected.imageset/ic_main_tab_selected@3x.png


+ 22 - 0
Lanu/Assets.xcassets/main/ic_main_top_bg.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/main/ic_main_top_bg.imageset/ic_main_top_bg@2x.png


BIN
Lanu/Assets.xcassets/main/ic_main_top_bg.imageset/ic_main_top_bg@3x.png


+ 16 - 0
Lanu/Common/Utils/Comparable+Extension.swift

@@ -0,0 +1,16 @@
+//
+//  Comparable+Extension.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/11/14.
+//
+
+import Foundation
+
+// 扩展数值类型,添加区间限制方法
+extension Comparable {
+    /// 将值限制在 [min, max] 区间内
+    func bounded(min: Self, max: Self) -> Self {
+        return Swift.min(max, Swift.max(min, self))
+    }
+}

+ 32 - 0
Lanu/Common/Utils/NSObject+Extension.swift

@@ -0,0 +1,32 @@
+//
+//  NSObject+Extension.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/11/14.
+//
+
+import Foundation
+import Combine
+
+// 定义关联对象的 key
+private var cancellableBagKey: Void?
+
+extension NSObject {
+    /// 用于存储 Combine 订阅的容器,自动与对象生命周期绑定
+    var bag: Set<AnyCancellable> {
+        get {
+            // 如果已存在,则直接返回
+            if let existingBag = objc_getAssociatedObject(self, &cancellableBagKey) as? Set<AnyCancellable> {
+                return existingBag
+            }
+            // 如果不存在,则初始化一个新的 Set 并关联
+            let newBag = Set<AnyCancellable>()
+            objc_setAssociatedObject(self, &cancellableBagKey, newBag, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+            return newBag
+        }
+        set {
+            // 设置新值时关联到对象
+            objc_setAssociatedObject(self, &cancellableBagKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+        }
+    }
+}

+ 36 - 0
Lanu/Common/Utils/UIView+Extension.swift

@@ -8,6 +8,42 @@
 import Foundation
 import UIKit
 
+
+extension UIView {
+    var navigationController: UINavigationController? {
+        viewController?.navigationController
+    }
+    
+    var viewController: UIViewController? {
+        var responder: UIResponder? = self
+        while responder != nil {
+            if let viewController = responder as? UIViewController {
+                return viewController
+            }
+            responder = responder?.next
+        }
+        return nil
+    }
+    
+    var appKeyWindow: UIWindow {
+        // 从当前活跃的场景中获取窗口
+        (UIApplication.shared.connectedScenes
+            .filter { $0.activationState == .foregroundActive }
+            .compactMap { $0 as? UIWindowScene }
+            .first?.windows
+            .filter { $0.isKeyWindow }
+            .first)!
+    }
+    
+    static var statusBarHeight: CGFloat = {
+        guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
+        else {
+            return 0
+        }
+        return windowScene.statusBarManager?.statusBarFrame.height ?? 0
+    }()
+}
+
 // MARK: 点击响应
 extension UIView {
     private class BlockTapGestureRecognizer: UITapGestureRecognizer {

+ 88 - 0
Lanu/Common/Views/Gender/LNGenderView.swift

@@ -0,0 +1,88 @@
+//
+//  LNGenderView.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/11/14.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNGenderView: UIView {
+    private let genderIc = UIImageView()
+    private let ageLabel = UILabel()
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setupViews()
+    }
+    
+    func update(_ gender: LNUserGender, _ age: Int) {
+        genderIc.image = switch gender {
+        case .unknow: nil
+        case .male: .init(named: "ic_gender_male")
+        case .female: .init(named: "ic_gender_female")
+        }
+        
+        if gender == .unknow {
+            ageLabel.text = nil
+            return
+        }
+        ageLabel.text = "\(age)"
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNGenderView {
+    private func setupViews() {
+        addSubview(genderIc)
+        genderIc.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+            make.width.equalTo(0).priority(.low)
+            make.height.equalTo(0).priority(.low)
+        }
+        
+        ageLabel.font = .systemFont(ofSize: 11)
+        ageLabel.textColor = .white
+        addSubview(ageLabel)
+        ageLabel.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.centerX.equalTo(snp.trailing).offset(-11)
+        }
+    }
+}
+
+#if DEBUG
+
+import SwiftUI
+
+struct LNGenderViewPreview: UIViewRepresentable {
+    func makeUIView(context: Context) -> some UIView {
+        let container = UIView()
+        container.backgroundColor = .lightGray
+        
+        let view = LNGenderView()
+        container.addSubview(view)
+        view.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+        }
+        
+        view.update(.female, 36)
+        
+        return container
+    }
+    
+    func updateUIView(_ uiView: UIViewType, context: Context) { }
+}
+
+#Preview(body: {
+    LNGenderViewPreview()
+})
+#endif
+

+ 34 - 0
Lanu/Common/Views/LNAutoSizeTextView.swift

@@ -0,0 +1,34 @@
+//
+//  LNAutoSizeTextView.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/11/14.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNAutoSizeTextView: UITextView {
+    override var contentSize: CGSize {
+        didSet {
+            guard contentSize.height > 0 else { return }
+            snp.updateConstraints { make in
+                make.height.equalTo(contentSize.height)
+            }
+        }
+    }
+    
+    override init(frame: CGRect, textContainer: NSTextContainer?) {
+        super.init(frame: frame, textContainer: textContainer)
+        
+        snp.makeConstraints { make in
+            make.height.equalTo(0)
+        }
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}

+ 143 - 0
Lanu/Common/Views/LNPopupViewProtocol.swift

@@ -0,0 +1,143 @@
+//
+//  LNPopupViewProtocol.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/11/14.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+enum LNPopupViewHeight {
+    case auto
+    case percent(CGFloat)
+    case height(CGFloat)
+}
+
+protocol LNPopupViewProtocol: UIView {
+    var container: UIView { get }
+    var containerHeight: LNPopupViewHeight { get }
+}
+
+extension LNPopupViewProtocol {
+    func showHIn(_ targetView: UIView) {
+        if superview != nil, superview != targetView {
+            removeFromSuperview()
+        }
+        targetView.addSubview(self)
+        self.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+        }
+        if container.superview == nil {
+            self.addSubview(container)
+        }
+        container.snp.remakeConstraints { make in
+            make.leading.equalTo(self.snp.trailing)
+            make.width.equalToSuperview()
+            make.bottom.equalToSuperview()
+            switch containerHeight {
+            case .percent(let percent):
+                make.height.equalToSuperview().multipliedBy(percent)
+            case .height(let height):
+                make.height.equalTo(height)
+            case .auto: break
+            }
+        }
+        layoutIfNeeded()
+        
+        container.snp.remakeConstraints { make in
+            make.leading.equalToSuperview()
+            make.width.equalToSuperview()
+            make.bottom.equalToSuperview()
+            switch containerHeight {
+            case .percent(let percent):
+                make.height.equalToSuperview().multipliedBy(percent)
+            case .height(let height):
+                make.height.equalTo(height)
+            case .auto: break
+            }
+        }
+        UIView.animate(withDuration: 0.2) {
+            self.layoutIfNeeded()
+        }
+    }
+    
+    func dismissH() {
+        container.snp.remakeConstraints { make in
+            make.leading.equalTo(self.snp.trailing)
+            make.width.equalToSuperview()
+            make.bottom.equalToSuperview()
+            switch containerHeight {
+            case .percent(let percent):
+                make.height.equalToSuperview().multipliedBy(percent)
+            case .height(let height):
+                make.height.equalTo(height)
+            case .auto: break
+            }
+        }
+        UIView.animate(withDuration: 0.2) {
+            self.layoutIfNeeded()
+        } completion: { _ in
+            self.removeFromSuperview()
+        }
+    }
+    
+    func showVIn(_ targetView: UIView) {
+        if superview != nil, superview != targetView {
+            removeFromSuperview()
+        }
+        targetView.addSubview(self)
+        self.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+        }
+        if container.superview == nil {
+            self.addSubview(container)
+        }
+        container.snp.remakeConstraints { make in
+            make.leading.trailing.equalToSuperview()
+            make.top.equalTo(self.snp.bottom)
+            switch containerHeight {
+            case .percent(let percent):
+                make.height.equalToSuperview().multipliedBy(percent)
+            case .height(let height):
+                make.height.equalTo(height)
+            case .auto: break
+            }
+        }
+        layoutIfNeeded()
+        
+        container.snp.remakeConstraints { make in
+            make.leading.trailing.bottom.equalToSuperview()
+            switch containerHeight {
+            case .percent(let percent):
+                make.height.equalToSuperview().multipliedBy(percent)
+            case .height(let height):
+                make.height.equalTo(height)
+            case .auto: break
+            }
+        }
+        UIView.animate(withDuration: 0.2) {
+            self.layoutIfNeeded()
+        }
+    }
+    
+    func dismissV() {
+        container.snp.remakeConstraints { make in
+            make.leading.trailing.equalToSuperview()
+            make.top.equalTo(self.snp.bottom)
+            switch containerHeight {
+            case .percent(let percent):
+                make.height.equalToSuperview().multipliedBy(percent)
+            case .height(let height):
+                make.height.equalTo(height)
+            case .auto: break
+            }
+        }
+        UIView.animate(withDuration: 0.2) {
+            self.layoutIfNeeded()
+        } completion: { _ in
+            self.removeFromSuperview()
+        }
+    }
+}

+ 86 - 0
Lanu/Common/Views/StarScore/LNFiveStarScoreView.swift

@@ -0,0 +1,86 @@
+//
+//  LNFiveStarScoreView.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/11/14.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNFiveStarScoreView: UIView {
+    private var starViews: [LNStarScoreView] = []
+    var score: Double = 0.0 {
+        didSet {
+            guard oldValue != score else { return }
+            let fixed = score.bounded(min: 0.0, max: 5.0)
+            for (index, view) in starViews.enumerated() {
+                if fixed - 1 > Double(index) {
+                    view.score = 1.0
+                } else {
+                    view.score = fixed - Double(index)
+                }
+            }
+        }
+    }
+    
+    private
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setupViews()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNFiveStarScoreView {
+    private func setupViews() {
+        let stackView = UIStackView()
+        stackView.axis = .horizontal
+        stackView.spacing = 4
+        stackView.distribution = .equalSpacing
+        addSubview(stackView)
+        stackView.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+        }
+        
+        for _ in 0..<5 {
+            let star = LNStarScoreView()
+            stackView.addArrangedSubview(star)
+            starViews.append(star)
+        }
+    }
+}
+
+#if DEBUG
+ 
+import SwiftUI
+
+struct LNFiveStarScoreViewPreview: UIViewRepresentable {
+    func makeUIView(context: Context) -> some UIView {
+        let container = UIView()
+        container.backgroundColor = .lightGray
+        
+        let view = LNFiveStarScoreView()
+        container.addSubview(view)
+        view.snp.makeConstraints { make in
+            make.leading.centerY.equalToSuperview()
+        }
+        view.score = 1.3
+        
+        return container
+    }
+    
+    func updateUIView(_ uiView: UIViewType, context: Context) { }
+}
+
+#Preview(body: {
+    LNFiveStarScoreViewPreview()
+})
+#endif
+

+ 92 - 0
Lanu/Common/Views/StarScore/LNStarScoreView.swift

@@ -0,0 +1,92 @@
+//
+//  LNStarScoreView.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/11/14.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+class LNStarScoreView: UIView {
+    private let fillView = UIView()
+    
+    var score: Double = 0.0 {
+        didSet {
+            guard oldValue != score else { return }
+            let fixed = score.bounded(min: 0.0, max: 1.0)
+            fillView.snp.remakeConstraints { make in
+                make.leading.top.bottom.equalToSuperview()
+                make.width.equalToSuperview().multipliedBy(fixed)
+            }
+        }
+    }
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setupViews()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNStarScoreView {
+    private func setupViews() {
+        let bg = UIImageView()
+        bg.image = .init(named: "ic_star")
+        addSubview(bg)
+        bg.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+            make.width.height.equalToSuperview()
+        }
+        
+        fillView.clipsToBounds = true
+        addSubview(fillView)
+        fillView.snp.makeConstraints { make in
+            make.leading.top.bottom.equalToSuperview()
+            make.width.equalToSuperview().multipliedBy(score)
+        }
+        
+        let fillIc = UIImageView()
+        fillIc.image = .init(named: "ic_star_fill")
+        fillIc.contentMode = .left
+        fillView.addSubview(fillIc)
+        fillIc.snp.makeConstraints { make in
+            make.leading.centerY.equalToSuperview()
+        }
+    }
+}
+
+
+#if DEBUG
+
+import SwiftUI
+
+struct LNStarScoreViewPreview: UIViewRepresentable {
+    func makeUIView(context: Context) -> some UIView {
+        let container = UIView()
+        container.backgroundColor = .lightGray
+        
+        let view = LNStarScoreView()
+        container.addSubview(view)
+        view.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+        }
+        
+        view.score = 0.8
+        
+        return container
+    }
+    
+    func updateUIView(_ uiView: UIViewType, context: Context) { }
+}
+
+#Preview(body: {
+    LNStarScoreViewPreview()
+})
+#endif
+

BIN
Lanu/Files/Font/Poppins-Regular.ttf


BIN
Lanu/Files/Font/Poppins-SemiBold.ttf


+ 5 - 0
Lanu/Info.plist

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

+ 73 - 1
Lanu/Localizable.xcstrings

@@ -1,7 +1,79 @@
 {
   "sourceLanguage" : "en",
   "strings" : {
-
+    "Activity" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Activity"
+          }
+        }
+      }
+    },
+    "login_bottom_privacy_text" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "By Continuing To Use, You Agree To Our User Agreement And Privacy Policy"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "By Continuing To Use, You Agree To Our User Agreement And Privacy Policy"
+          }
+        }
+      }
+    },
+    "Online_Play" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Online Play"
+          }
+        }
+      }
+    },
+    "Privacy_Policy" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Privacy Policy"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Privacy Policy"
+          }
+        }
+      }
+    },
+    "User_Agreement" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "User Agreement"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "User Agreement"
+          }
+        }
+      }
+    }
   },
   "version" : "1.1"
 }

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

@@ -43,8 +43,18 @@ class LNAccountManager {
     }
     
     func loginByToken(completion: ((Bool) -> Void)? = nil) {
-        completion?(true)
-        notifyUserLogin()
+        LNHttpManager.shared.refreshToken { [weak self] res, err in
+            guard let self else { return }
+            guard err == nil, let res else {
+                completion?(false)
+                return
+            }
+            self.token = res.token
+            self.uid = res.userProfile.id
+            completion?(true)
+            
+            self.notifyUserLogin()
+        }
     }
     
     func loginByGoogle(data: String, completion: ((Bool) -> Void)? = nil) {

+ 6 - 0
Lanu/Manager/Account/Network/LNHttpManager+Login.swift

@@ -11,6 +11,8 @@ import Foundation
 let kNetPath_Login_Google = "/user/login/google/enter"
 let kNetPath_Login_Email = "/user/login/email/enter"
 
+let kNetPath_Login_Refresh = "/user/renewalToken"
+
 let kNetPath_Logout = "/user/logout"
 
 extension LNHttpManager {
@@ -24,6 +26,10 @@ extension LNHttpManager {
     }
 #endif
     
+    func refreshToken(completion: @escaping (LNLoginResponseVO?, LNHttpError?) -> Void) {
+        post(path: kNetPath_Login_Refresh, completion: completion)
+    }
+    
     func logout(completion: @escaping (LNHttpError?) -> Void) {
         post(path: kNetPath_Logout, completion: completion)
     }

+ 1 - 1
Lanu/Manager/Profile/LNUserProfileInfo.swift

@@ -14,7 +14,7 @@ class LNUserProfileInfo {
     var avatar: String = ""
     var nickname: String = ""
     var age: Int = 0
-    var gender: Int = 0
+    var gender: LNUserGender = .unknow
     var intro: String = ""
     var playmate: Bool = false
     

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

@@ -8,6 +8,12 @@
 import Foundation
 import AutoCodable
 
+enum LNUserGender: Int, Decodable {
+    case unknow = 0
+    case male = 1
+    case female = 2
+}
+
 @AutoCodable
 class LNUserProfileVO: Decodable {
     var id: String = ""
@@ -15,7 +21,7 @@ class LNUserProfileVO: Decodable {
     var avatar: String = ""
     var nickname: String = ""
     var age: Int = 0
-    var gender: Int = 0
+    var gender: LNUserGender = .unknow
     var intro: String = ""
     var playmate: Bool = false
     

+ 29 - 7
Lanu/Views/Login/LNLoginViewController.swift

@@ -29,18 +29,21 @@ extension LNLoginViewController {
 extension LNLoginViewController {
     private func setupViews() {
         showNavigationBar = false
-        
-        let container = UIView()
+        view.backgroundColor = .init(hex: "#565656")
         
         let google = buildGoogleLogin()
-        container.addSubview(google)
+        view.addSubview(google)
         google.snp.makeConstraints { make in
-            make.edges.equalToSuperview()
+            make.centerX.equalToSuperview()
+            make.centerY.equalToSuperview().multipliedBy(1.5)
         }
         
-        view.addSubview(container)
-        container.snp.makeConstraints { make in
-            make.center.equalToSuperview()
+        let privacy = buildPrivacy()
+        view.addSubview(privacy)
+        privacy.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.width.equalToSuperview().offset(-50)
+            make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom).offset(-40)
         }
     }
     
@@ -58,6 +61,25 @@ extension LNLoginViewController {
         
         return loginButton
     }
+    
+    private func buildPrivacy() -> UIView {
+        let text: String = .init(key: "login_bottom_privacy_text")
+        let attrStr = NSMutableAttributedString(string: text)
+        let agreementRange = (text as NSString).range(of: .init(key: "User_Agreement"))
+        attrStr.addAttribute(.link, value: "https://cn.bing.com", range: agreementRange)
+        let privacyRange = (text as NSString).range(of: .init(key: "Privacy_Policy"))
+        attrStr.addAttribute(.link, value: "https://www.baidu.com", range: privacyRange)
+        
+        let privacyView = LNPrivacyTextView()
+        privacyView.attributedText = attrStr
+        privacyView.textAlignment = .center
+        privacyView.backgroundColor = .clear
+        privacyView.font = .systemFont(ofSize: 13.4)
+        privacyView.textColor = .init(hex: "#C7C7C7")
+        privacyView.linkTextAttributes = [.foregroundColor: UIColor.white]
+        
+        return privacyView
+    }
 }
 
 #if DEBUG

+ 37 - 0
Lanu/Views/Login/LNPrivacyTextView.swift

@@ -0,0 +1,37 @@
+//
+//  LNPrivacyTextView.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/11/14.
+//
+
+import Foundation
+import UIKit
+
+
+class LNPrivacyTextView: LNAutoSizeTextView {
+    override init(frame: CGRect, textContainer: NSTextContainer?) {
+        super.init(frame: frame, textContainer: textContainer)
+        
+        delegate = self
+        isEditable = false
+        showsVerticalScrollIndicator = false
+        showsHorizontalScrollIndicator = false
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+
+extension LNPrivacyTextView: UITextViewDelegate {
+    func textView(_ textView: UITextView, shouldInteractWith URL: URL,
+                  in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
+        true
+    }
+    
+    func textViewDidChangeSelection(_ textView: UITextView) {
+        textView.selectedTextRange = nil
+    }
+}

+ 75 - 0
Lanu/Views/Main/LNMainTopMenuView.swift

@@ -0,0 +1,75 @@
+//
+//  LNMainTopMenuView.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/11/14.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNMainTopMenuView: UIView {
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setupViews()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNMainTopMenuView {
+    private func setupViews() {
+        let logo = UIImageView()
+        logo.image = .init(named: "ic_star_fill")
+        addSubview(logo)
+        logo.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(16)
+            make.centerY.equalToSuperview()
+        }
+        
+        let button = UIButton()
+        button.setImage(.init(named: "ic_main_mine"), for: .normal)
+        button.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            
+        }), for: .touchUpInside)
+        addSubview(button)
+        button.snp.makeConstraints { make in
+            make.top.bottom.equalToSuperview()
+            make.trailing.equalToSuperview().offset(-16)
+            make.width.height.equalTo(34)
+        }
+    }
+}
+
+
+#if DEBUG
+
+import SwiftUI
+
+struct LNMainTopMenuViewPreview: UIViewRepresentable {
+    func makeUIView(context: Context) -> some UIView {
+        let container = UIView()
+        container.backgroundColor = .lightGray
+        
+        let view = LNMainTopMenuView()
+        container.addSubview(view)
+        view.snp.makeConstraints { make in
+            make.leading.trailing.top.equalToSuperview()
+        }
+        
+        return container
+    }
+    
+    func updateUIView(_ uiView: UIViewType, context: Context) { }
+}
+
+#Preview(body: {
+    LNMainTopMenuViewPreview()
+})
+#endif

+ 135 - 0
Lanu/Views/Main/LNMainTopTabView.swift

@@ -0,0 +1,135 @@
+//
+//  LNMainTopTabView.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/11/14.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+enum LNMainTopTabType {
+    case online
+    case activity
+    
+    var desc: String {
+        switch self {
+        case .online: .init(key: "Online_Play")
+        case .activity: .init(key: "Activity")
+        }
+    }
+}
+
+
+protocol LNMainTopTabViewDelegate: NSObject {
+    func mainTopTabView(view: LNMainTopTabView, didSelect: LNMainTopTabType)
+}
+
+
+class LNMainTopTabView: UIView {
+    private let titles: [LNMainTopTabType] = [.online, .activity]
+    private(set) var curType: LNMainTopTabType = .online
+    private let indicator = UIImageView()
+    private var tabItemViews: [UIButton] = []
+    
+    weak var delegate: LNMainTopTabViewDelegate?
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setupViews()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNMainTopTabView {
+    private func handleTabClick(type: LNMainTopTabType, tab: UIButton) {
+        tabItemViews.forEach {
+            let isSelected = tab == $0
+            $0.titleLabel?.font = isSelected ? .boldSystemFont(ofSize: 22) : .boldSystemFont(ofSize: 16)
+            $0.setTitleColor(isSelected ? .init(hex: "#1D2129") : .init(hex: "#4E5969"), for: .normal)
+        }
+        indicator.snp.remakeConstraints { make in
+            make.center.equalTo(tab)
+        }
+        UIView.animate(withDuration: 0.3) { [weak self] in
+            guard let self else { return }
+            self.layoutIfNeeded()
+        }
+        curType = type
+        delegate?.mainTopTabView(view: self, didSelect: type)
+    }
+}
+
+extension LNMainTopTabView {
+    private func setupViews() {
+        let scrollView = UIScrollView()
+        addSubview(scrollView)
+        scrollView.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+        }
+        
+        let fakeView = UIView()
+        scrollView.addSubview(fakeView)
+        fakeView.snp.makeConstraints { make in
+            make.leading.top.bottom.equalToSuperview()
+            make.height.equalToSuperview()
+        }
+        
+        let stackView = UIStackView()
+        stackView.axis = .horizontal
+        stackView.spacing = 14
+        stackView.distribution = .equalSpacing
+        scrollView.addSubview(stackView)
+        stackView.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+        }
+        
+        indicator.image = .init(named: "ic_main_tab_selected")
+        stackView.addSubview(indicator)
+        
+        for type in titles {
+            let tabItem = UIButton()
+            tabItem.setTitle(type.desc, for: .normal)
+            tabItem.addAction(UIAction(handler: { [weak self] _ in
+                guard let self else { return }
+                self.handleTabClick(type: type, tab: tabItem)
+            }), for: .touchUpInside)
+            stackView.addArrangedSubview(tabItem)
+            tabItemViews.append(tabItem)
+        }
+        handleTabClick(type: titles.first!, tab: tabItemViews.first!)
+    }
+}
+
+
+#if DEBUG
+
+import SwiftUI
+
+struct LNMainTopTabViewPreview: UIViewRepresentable {
+    func makeUIView(context: Context) -> some UIView {
+        let container = UIView()
+        container.backgroundColor = .lightGray
+        
+        let view = LNMainTopTabView()
+        container.addSubview(view)
+        view.snp.makeConstraints { make in
+            make.leading.trailing.top.equalToSuperview()
+        }
+        
+        return container
+    }
+    
+    func updateUIView(_ uiView: UIViewType, context: Context) { }
+}
+
+#Preview(body: {
+    LNMainTopTabViewPreview()
+})
+#endif
+

+ 92 - 16
Lanu/Views/Main/LNMainViewController.swift

@@ -14,26 +14,102 @@ class LNMainViewController: LNViewController {
     override func viewDidLoad() {
         super.viewDidLoad()
         
+        setupViews()
+    }
+}
+
+extension LNMainViewController {
+    private func setupViews() {
         showNavigationBar = false
+        view.backgroundColor = .init(hex: "#F1F2F5")
         
-        let title = UILabel()
-        title.text = "这个是首页"
-        view.addSubview(title)
-        title.snp.makeConstraints { make in
-            make.center.equalToSuperview()
+        let topCover = UIImageView()
+        topCover.image = .init(named: "ic_main_top_bg")
+        view.addSubview(topCover)
+        topCover.snp.makeConstraints { make in
+            make.top.leading.trailing.equalToSuperview()
+        }
+        
+        let menu = buildTopMenu()
+        view.addSubview(menu)
+        menu.snp.makeConstraints { make in
+            make.leading.trailing.equalToSuperview()
+            make.top.equalToSuperview().offset(UIView.statusBarHeight)
         }
         
-        let chat = UIButton()
-        chat.setTitle("Chat", for: .normal)
-        chat.setTitleColor(.black, for: .normal)
-        chat.addAction(UIAction(handler: { [weak self] _ in
-            guard let self else { return }
-            navigationController?.pushViewController(LNIMViewController(), animated: true)
-        }), for: .touchUpInside)
-        view.addSubview(chat)
-        chat.snp.makeConstraints { make in
-            make.centerX.equalToSuperview()
-            make.centerY.equalToSuperview().multipliedBy(0.5)
+        let tab = buildTopTab()
+        view.addSubview(tab)
+        tab.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(16)
+            make.trailing.equalToSuperview().offset(-16)
+            make.top.equalTo(menu.snp.bottom).offset(4)
         }
+        
+        let star = LNFiveStarScoreView()
+        star.score = 4.5
+        view.addSubview(star)
+        star.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+        }
+        
+//        let title = UILabel()
+//        title.text = "这个是首页"
+//        view.addSubview(title)
+//        title.snp.makeConstraints { make in
+//            make.center.equalToSuperview()
+//        }
+//        
+//        let chat = UIButton()
+//        chat.setTitle("Chat", for: .normal)
+//        chat.setTitleColor(.black, for: .normal)
+//        chat.addAction(UIAction(handler: { [weak self] _ in
+//            guard let self else { return }
+////
+////            V2TIMManager.sharedInstance().sendC2CTextMessage(text: "This is a test", to: "69145452e6acb84232465830") {
+////                var i = 0
+////            } fail: { code, err in
+////                var i = 0
+////            }
+//////            V2TIMManager.sharedInstance()?.getMessageManager()?.send(message, succ: {
+//////                print("消息发送成功")
+//////            }, fail: { code, msg in
+//////                print("消息发送失败,错误码:\(code),错误信息:\(msg ?? "无")")
+//////            })
+//        }), for: .touchUpInside)
+//        view.addSubview(chat)
+//        chat.snp.makeConstraints { make in
+//            make.centerX.equalToSuperview()
+//            make.centerY.equalToSuperview().multipliedBy(0.5)
+//        }
+    }
+    
+    private func buildTopMenu() -> UIView {
+        let menu = LNMainTopMenuView()
+        return menu
+    }
+    
+    private func buildTopTab() -> UIView {
+        let tab = LNMainTopTabView()
+        
+        return tab
+    }
+}
+
+#if DEBUG
+
+import SwiftUI
+
+struct LNMainViewControllerPreview: UIViewControllerRepresentable {
+    func makeUIViewController(context: Context) -> some UIViewController {
+        LNNavigationController(rootViewController: LNMainViewController())
+    }
+    
+    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
+        
     }
 }
+
+#Preview(body: {
+    LNMainViewControllerPreview()
+})
+#endif