Selaa lähdekoodia

feat: 补充空白页逻辑

陈文艺 3 kuukautta sitten
vanhempi
sitoutus
89d54b9ff1
35 muutettua tiedostoa jossa 373 lisäystä ja 92 poistoa
  1. 11 4
      Lanu.xcodeproj/project.pbxproj
  2. 6 0
      Lanu/Assets.xcassets/common/NoData/Contents.json
  3. 22 0
      Lanu/Assets.xcassets/common/NoData/ic_no_data_empty.imageset/Contents.json
  4. BIN
      Lanu/Assets.xcassets/common/NoData/ic_no_data_empty.imageset/ic_no_data_empty@2x.png
  5. BIN
      Lanu/Assets.xcassets/common/NoData/ic_no_data_empty.imageset/ic_no_data_empty@3x.png
  6. 22 0
      Lanu/Assets.xcassets/common/NoData/ic_no_data_network_error.imageset/Contents.json
  7. BIN
      Lanu/Assets.xcassets/common/NoData/ic_no_data_network_error.imageset/ic_no_data_network_error@2x.png
  8. BIN
      Lanu/Assets.xcassets/common/NoData/ic_no_data_network_error.imageset/ic_no_data_network_error@3x.png
  9. 22 0
      Lanu/Assets.xcassets/common/NoData/ic_no_data_order_empty.imageset/Contents.json
  10. BIN
      Lanu/Assets.xcassets/common/NoData/ic_no_data_order_empty.imageset/ic_no_data_order_empty@2x.png
  11. BIN
      Lanu/Assets.xcassets/common/NoData/ic_no_data_order_empty.imageset/ic_no_data_order_empty@3x.png
  12. 22 0
      Lanu/Assets.xcassets/common/NoData/ic_no_data_relation_empty.imageset/Contents.json
  13. BIN
      Lanu/Assets.xcassets/common/NoData/ic_no_data_relation_empty.imageset/ic_no_data_relation_empty@2x.png
  14. BIN
      Lanu/Assets.xcassets/common/NoData/ic_no_data_relation_empty.imageset/ic_no_data_relation_empty@3x.png
  15. 65 0
      Lanu/Common/Views/NoMoreView/LNNoMoreDataView.swift
  16. 5 2
      Lanu/Manager/GameMate/LNGameMateManager.swift
  17. 1 0
      Lanu/Manager/GameMate/Network/LNGameMateResponse.swift
  18. 2 2
      Lanu/Manager/GameMate/Network/LNHttpManager+GameMate.swift
  19. 1 1
      Lanu/Manager/Network/LNHttpManager.swift
  20. 1 1
      Lanu/Manager/Order/LNCommonAlertView+Order.swift
  21. 2 8
      Lanu/Manager/Order/LNOrderManager.swift
  22. 2 0
      Lanu/Manager/Profile/LNProfileManager.swift
  23. 0 4
      Lanu/Manager/Profile/Network/LNHttpManager+Profile.swift
  24. 4 4
      Lanu/Manager/Relation/LNRelationManager.swift
  25. 2 1
      Lanu/Views/Game/MateFilter/LNGameMateFilterPanel.swift
  26. 28 19
      Lanu/Views/Game/MateList/LNGameMateListView.swift
  27. 1 1
      Lanu/Views/Login/Setup/LNBaseInfoSetupViewController.swift
  28. 24 5
      Lanu/Views/Order/OrderList/LNOrderListViewController.swift
  29. 2 0
      Lanu/Views/Order/OrderRecords/LNOrderRecordListViewController.swift
  30. 6 1
      Lanu/Views/Profile/Profile/LNProfileInfosView.swift
  31. 14 0
      Lanu/Views/Profile/Profile/LNProfilePhotoWall.swift
  32. 4 0
      Lanu/Views/Profile/Profile/LNProfileViewController.swift
  33. 40 27
      Lanu/Views/Profile/Relation/LNUserRelationListView.swift
  34. 2 0
      Lanu/Views/Search/LNUserSearchItemCell.swift
  35. 62 12
      Lanu/Views/Search/LNUserSearchViewController.swift

+ 11 - 4
Lanu.xcodeproj/project.pbxproj

@@ -79,6 +79,7 @@
 				Common/Views/Loading/LNLoadingView.swift,
 				Common/Views/Menu/LNBottomSheetMenu.swift,
 				Common/Views/Menu/LNCommonAlertView.swift,
+				Common/Views/NoMoreView/LNNoMoreDataView.swift,
 				Common/Views/ScrollView/LNNestedScrollView.swift,
 				Common/Views/ScrollView/LNNestedTableView.swift,
 				Common/Views/StackView/LNAutoFillStackView.swift,
@@ -288,8 +289,6 @@
 		};
 		FBB67E232EC48B440070E686 /* ThirdParty */ = {
 			isa = PBXFileSystemSynchronizedRootGroup;
-			exceptions = (
-			);
 			path = ThirdParty;
 			sourceTree = "<group>";
 		};
@@ -442,10 +441,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";
@@ -481,10 +484,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";
@@ -511,7 +518,7 @@
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CLANG_ENABLE_MODULES = YES;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 6;
+				CURRENT_PROJECT_VERSION = 7;
 				DEVELOPMENT_TEAM = 5H8D98R72W;
 				ENABLE_USER_SCRIPT_SANDBOXING = NO;
 				GENERATE_INFOPLIST_FILE = YES;
@@ -553,7 +560,7 @@
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CLANG_ENABLE_MODULES = YES;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 6;
+				CURRENT_PROJECT_VERSION = 7;
 				DEVELOPMENT_TEAM = 5H8D98R72W;
 				ENABLE_USER_SCRIPT_SANDBOXING = NO;
 				GENERATE_INFOPLIST_FILE = YES;

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

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

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

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

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


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


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

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

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


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


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

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

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


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


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

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

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


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


+ 65 - 0
Lanu/Common/Views/NoMoreView/LNNoMoreDataView.swift

@@ -0,0 +1,65 @@
+//
+//  LNNoMoreDataView.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/12/29.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNNoMoreDataView: UIView {
+    private let imageView = UIImageView()
+    private let tipsLabel = UILabel()
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setupViews()
+    }
+    
+    func showNetworkError() {
+        imageView.image = .init(named: "ic_no_data_network_error")
+        tipsLabel.text = .init(key: "哎呀,网络连接失败")
+        isHidden = false
+    }
+    
+    func showNoData(icon: String = "ic_no_data_empty",
+                    tips: String = .init(key: "这里空空如也")) {
+        imageView.image = .init(named: icon)
+        tipsLabel.text = tips
+        isHidden = false
+    }
+    
+    func hide() {
+        isHidden = true
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNNoMoreDataView {
+    private func setupViews() {
+        isHidden = true
+        
+        addSubview(imageView)
+        imageView.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.top.equalToSuperview()
+            make.width.height.equalTo(0).priority(.low)
+        }
+        
+        tipsLabel.font = .body_s
+        tipsLabel.textColor = .text_2
+        addSubview(tipsLabel)
+        tipsLabel.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview()
+            make.top.equalTo(imageView.snp.bottom).offset(14)
+            make.bottom.equalToSuperview()
+        }
+    }
+}

+ 5 - 2
Lanu/Manager/GameMate/LNGameMateManager.swift

@@ -143,10 +143,13 @@ extension LNGameMateManager {
 
 extension LNGameMateManager {
     func searchGameMate(keyword: String, next: String, queue: DispatchQueue = .main,
-                        handler: @escaping ([LNGameMateSearchResultVO]) -> Void) {
+                        handler: @escaping ([LNGameMateSearchResultVO]?, String?) -> Void) {
         LNHttpManager.shared.searchGameMate(keyword: keyword, size: 30, next: next) { res, err in
             queue.asyncIfNotGlobal {
-                handler(res?.list ?? [])
+                if let err {
+                    showToast(err.errorDescription)
+                }
+                handler(res?.list, res?.next)
             }
         }
     }

+ 1 - 0
Lanu/Manager/GameMate/Network/LNGameMateResponse.swift

@@ -141,4 +141,5 @@ class LNGameMateSearchResultVO: Decodable {
 @AutoCodable
 class LNGameMateSearchResponse: Decodable {
     var list: [LNGameMateSearchResultVO] = []
+    var next: String = ""
 }

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

@@ -20,13 +20,13 @@ let kNetPath_GameMate_Search = "/playmate/search"
 
 enum LNGameMateAgeRange: Int, CaseIterable {
     case all = -1
-    case `15-20` = 0
+    case `15-25` = 0
     case `25-35` = 1
     case `>35` = 2
     
     var text: String {
         switch self {
-        case .`15-20`: "15-20"
+        case .`15-25`: "15-25"
         case .`25-35`: "25-35"
         case .`>35`: "35以上"
         case .all: "Unlimited"

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

@@ -37,7 +37,7 @@ enum LNHttpError: Error, LocalizedError {
         case .statusCode(let code):
             return "请求失败,状态码: \(code)"
         case .serverError(let code, let error):
-            return error
+            return "code: \(code) err: \(error)"
         }
     }
 }

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

@@ -13,7 +13,7 @@ extension LNCommonAlertView {
         let panel = LNCommonAlertView()
         panel.titleLabel.text = .init(key: "确认取消订单吗?")
         panel.setConfirm(.init(key: "再想一下")) { }
-        panel.setCancel(.init(key: "不再关注")) {
+        panel.setCancel(.init(key: "取消订单")) {
             LNOrderManager.shared.cancelOrder(orderId: orderId) { success in
                 completion?(success)
             }

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

@@ -27,16 +27,10 @@ enum LNOrderErrorCode: Int {
 // MARK: 订单信息
 extension LNOrderManager {
     func getList(size: Int, next: String? = nil, queue: DispatchQueue = .main,
-                 handler: @escaping ([LNOrderListItemVO]?, String) -> Void) {
+                 handler: @escaping ([LNOrderListItemVO]?, String?) -> Void) {
         LNHttpManager.shared.getOrderList(size: size, next: next ?? "") { res, err in
-            guard err == nil, let res else {
-                queue.asyncIfNotGlobal {
-                    handler(nil, "")
-                }
-                return
-            }
             queue.asyncIfNotGlobal {
-                handler(res.list, res.next)
+                handler(res?.list, res?.next)
             }
         }
     }

+ 2 - 0
Lanu/Manager/Profile/LNProfileManager.swift

@@ -111,6 +111,8 @@ extension LNProfileManager {
                 res.list.forEach {
                     self.updateUserInfo(info: $0)
                 }
+            } else {
+                showToast(err?.errorDescription)
             }
             queue.asyncIfNotGlobal {
                 handler(res?.list.first)

+ 0 - 4
Lanu/Manager/Profile/Network/LNHttpManager+Profile.swift

@@ -15,7 +15,6 @@ let kNetPath_Profile_UsersInfo = "/user/get/infos"
 
 
 class LNProfileUpdateConfig {
-    var age: Int?
     var avatar: String?
     var nickName: String?
     var gender: LNUserGender?
@@ -35,9 +34,6 @@ extension LNHttpManager {
     func modifyMyProfile(config: LNProfileUpdateConfig,
                          completion: @escaping (LNHttpError?) -> Void) {
         var params: [String: Any] = [:]
-        if let age = config.age {
-            params["age"] = age
-        }
         if let avatar = config.avatar {
             params["avatar"] = avatar
         }

+ 4 - 4
Lanu/Manager/Relation/LNRelationManager.swift

@@ -72,19 +72,19 @@ class LNRelationManager {
     }
     
     func getUserFollowList(next: String, queue: DispatchQueue = .main,
-                           handler: @escaping ([LNRelationUserVO], String?, Int?) -> Void) {
+                           handler: @escaping ([LNRelationUserVO]?, String?, Int?) -> Void) {
         LNHttpManager.shared.getUserFollowList(size: 30, next: next) { res, err in
             queue.asyncIfNotGlobal {
-                handler(res?.list ?? [], res?.next, res?.total)
+                handler(res?.list, res?.next, res?.total)
             }
         }
     }
     
     func getUserFansList(next: String, queue: DispatchQueue = .main,
-                         handler: @escaping ([LNRelationUserVO], String?, Int?) -> Void) {
+                         handler: @escaping ([LNRelationUserVO]?, String?, Int?) -> Void) {
         LNHttpManager.shared.getUserFansList(size: 30, next: next) { res, err in
             queue.asyncIfNotGlobal {
-                handler(res?.list ?? [], res?.next, res?.total)
+                handler(res?.list, res?.next, res?.total)
             }
         }
     }

+ 2 - 1
Lanu/Views/Game/MateFilter/LNGameMateFilterPanel.swift

@@ -152,6 +152,7 @@ extension LNGameMateFilterPanel {
         var itemViews: [LNGameMateFiliterItemView] = []
         let stackView = LNMultiLineStackView()
         stackView.itemSpacing = 8
+        stackView.itemDistribution = .fillEqually
         stackView.columns = 4
         stackView.spacing = 6
         container.addArrangedSubview(stackView)
@@ -188,7 +189,7 @@ private class LNGameMateFiliterItemView: UIView {
         titleLabel.textAlignment = .center
         addSubview(titleLabel)
         titleLabel.snp.makeConstraints { make in
-            make.directionalHorizontalEdges.equalToSuperview().inset(11)
+            make.directionalHorizontalEdges.equalToSuperview().inset(8)
             make.centerY.equalToSuperview()
         }
         

+ 28 - 19
Lanu/Views/Game/MateList/LNGameMateListView.swift

@@ -20,6 +20,7 @@ class LNGameMateListView: UIView {
     private var nextTag: String?
     private let pageSize = 30
     
+    private let emptyView = LNNoMoreDataView()
     private let tableView = UITableView()
     private var loading = false
     
@@ -53,7 +54,7 @@ class LNGameMateListView: UIView {
 }
 
 extension LNGameMateListView {
-    private func loadList(_ handler: ((Bool) -> Void)? = nil) {
+    private func loadList() {
         guard !loading else { return }
         loading = true
         let curNext = nextTag ?? ""
@@ -70,8 +71,24 @@ extension LNGameMateListView {
                 self.curMateList.append(contentsOf: list)
                 self.nextTag = nextTag
                 self.tableView.reloadData()
+                
+                if curMateList.isEmpty {
+                    emptyView.showNoData(tips: .init(key: "当前筛选条件下暂无陪玩师"))
+                } else {
+                    emptyView.hide()
+                }
+            } else {
+                if curMateList.isEmpty {
+                    emptyView.showNetworkError()
+                }
+            }
+            
+            self.tableView.mj_header?.endRefreshing()
+            if nextTag?.isEmpty != false {
+                tableView.mj_footer?.endRefreshingWithNoMoreData()
+            } else {
+                tableView.mj_footer?.endRefreshing()
             }
-            handler?(nextTag?.isEmpty == false)
         }
     }
 }
@@ -145,15 +162,7 @@ extension LNGameMateListView {
         let header = MJRefreshNormalHeader { [weak self] in
             guard let self else { return }
             self.nextTag = nil
-            self.loadList { [weak self] hasMore in
-                guard let self else { return }
-                self.tableView.mj_header?.endRefreshing()
-                if hasMore {
-                    self.tableView.mj_footer?.endRefreshing()
-                } else {
-                    self.tableView.mj_footer?.endRefreshingWithNoMoreData()
-                }
-            }
+            self.loadList()
         }
         header.lastUpdatedTimeLabel?.isHidden = true
         header.stateLabel?.isHidden = true
@@ -161,14 +170,7 @@ extension LNGameMateListView {
         
         let footer = MJRefreshAutoNormalFooter { [weak self] in
             guard let self else { return }
-            self.loadList { [weak self] hasMore in
-                guard let self else { return }
-                if hasMore {
-                    self.tableView.mj_footer?.endRefreshing()
-                } else {
-                    self.tableView.mj_footer?.endRefreshingWithNoMoreData()
-                }
-            }
+            self.loadList()
         }
         footer.setTitle("", for: .noMoreData)
         tableView.mj_footer = footer
@@ -178,6 +180,13 @@ extension LNGameMateListView {
         tableView.delegate = self
         tableView.separatorStyle = .none
         tableView.backgroundColor = .clear
+        
+        tableView.addSubview(emptyView)
+        emptyView.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.centerY.equalToSuperview().multipliedBy(0.6)
+        }
+        
         return tableView
     }
 }

+ 1 - 1
Lanu/Views/Login/Setup/LNBaseInfoSetupViewController.swift

@@ -174,7 +174,7 @@ extension LNBaseInfoSetupViewController {
         nextButton.addAction(UIAction(handler: { [weak self] _ in
             guard let self else { return }
             updateConfig.nickName = nameInputField.text ?? ""
-            updateConfig.age = Int(curDate) * 1_000
+            updateConfig.birthday = curDate.formattedFullDate("-")
             updateConfig.avatar = avatar.imageUrl ?? ""
             view.pushToInterestSetup(updateConfig)
         }), for: .touchUpInside)

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

@@ -21,6 +21,7 @@ extension UIView {
 
 class LNOrderListViewController: LNViewController {
     private var isLoading = false
+    private let emptyView = LNNoMoreDataView()
     private let tableView = UITableView()
     
     private var orders: [LNOrderListItemVO] = []
@@ -45,19 +46,31 @@ extension LNOrderListViewController {
         LNOrderManager.shared.getList(size: pageSize, next: nextTag) { [weak self] list, next in
             guard let self else { return }
             isLoading = false
-            self.tableView.mj_header?.endRefreshing()
-            if let list {
+            if let list, let next {
                 if nextTag?.isEmpty == false {
                     orders.append(contentsOf: list)
                 } else {
                     orders = list
                 }
+                nextTag = next
+                tableView.reloadData()
+                
+                if orders.isEmpty {
+                    emptyView.showNoData(icon: "ic_no_data_order_empty", tips: .init(key: "没有相关订单"))
+                } else {
+                    emptyView.hide()
+                }
+            } else {
+                if orders.isEmpty {
+                    emptyView.showNetworkError()
+                }
             }
-            self.nextTag = next
-            if next.isEmpty {
+            tableView.mj_header?.endRefreshing()
+            if next?.isEmpty != false {
                 tableView.mj_footer?.endRefreshingWithNoMoreData()
+            } else {
+                tableView.mj_footer?.endRefreshing()
             }
-            tableView.reloadData()
         }
     }
 }
@@ -115,6 +128,12 @@ extension LNOrderListViewController {
             make.top.equalToSuperview().offset(6)
             make.bottom.equalToSuperview()
         }
+        
+        tableView.addSubview(emptyView)
+        emptyView.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.centerY.equalToSuperview().multipliedBy(0.6)
+        }
     }
 }
 

+ 2 - 0
Lanu/Views/Order/OrderRecords/LNOrderRecordListViewController.swift

@@ -60,6 +60,8 @@ extension LNOrderRecordListViewController {
             self.nextTag = next
             if next.isEmpty {
                 tableView.mj_footer?.endRefreshingWithNoMoreData()
+            } else {
+                tableView.mj_footer?.endRefreshing()
             }
             tableView.reloadData()
         }

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

@@ -23,6 +23,11 @@ class LNProfileInfosView: UIView {
             updateViewHeight()
         }
     }
+    var minHeight: CGFloat = 0.0 {
+        didSet {
+            updateViewHeight()
+        }
+    }
     var progress: CGFloat = 0.0 {
         didSet {
             layer.cornerRadius = 20 * (1 - progress.bounded(min: 0, max: 1.0))
@@ -91,7 +96,7 @@ extension LNProfileInfosView: LNProfileUserDetailViewDelegate {
 extension LNProfileInfosView {
     private func updateViewHeight() {
         var height = max(detailView.contentHeight, photoWall.contentHeight) + tabView.bounds.height
-        height = min(height, maxHeight)
+        height = height.bounded(min: minHeight, max: maxHeight)
         heightConstraint?.update(offset: height)
         if height > 0 {
             heightConstraint?.update(priority: .high)

+ 14 - 0
Lanu/Views/Profile/Profile/LNProfilePhotoWall.swift

@@ -22,6 +22,7 @@ protocol LNProfilePhotoWallDelegate: NSObject {
 
 
 class LNProfilePhotoWall: UIView {
+    private let emptyView = LNNoMoreDataView()
     private let columnCount = 2
     private var allPhotos: [LNProfilePhotoWallMode] = []
     private var columns: [[LNProfilePhotoWallMode]] = []
@@ -52,6 +53,12 @@ class LNProfilePhotoWall: UIView {
     func update(_ urls: [String]) {
         allPhotos.removeAll()
         
+        if urls.isEmpty {
+            emptyView.showNoData()
+        } else {
+            emptyView.hide()
+        }
+        
         urls.forEach {
             let mode = LNProfilePhotoWallMode()
             mode.url = $0
@@ -166,6 +173,12 @@ extension LNProfilePhotoWall: UITableViewDataSource {
 
 extension LNProfilePhotoWall {
     private func setupViews() {
+        addSubview(emptyView)
+        emptyView.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.centerY.equalToSuperview().multipliedBy(0.6)
+        }
+        
         let stackView = UIStackView()
         stackView.axis = .horizontal
         stackView.spacing = 9
@@ -187,6 +200,7 @@ extension LNProfilePhotoWall {
             tableView.delegate = self
             tableView.allowsSelection = false
             tableView.contentInset = .init(top: 6, left: 0, bottom: 0, right: 0)
+            tableView.backgroundColor = .clear
             tableView.publisher(for: \.contentSize).removeDuplicates().sink
             { [weak self] newValue in
                 guard let self else { return }

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

@@ -62,6 +62,10 @@ class LNProfileViewController: LNViewController {
         infoView.maxHeight = scrollView.bounds.height
         - CGRectGetHeight(fakeNavBar.bounds)
         - (uid.isMyUid ? 0 : (view.safeBottomInset + 30))
+        
+        infoView.minHeight = scrollView.bounds.height
+        - cover.bounds.height + 52
+        - (uid.isMyUid ? 0 : (view.safeBottomInset + 30))
     }
     
     required init?(coder: NSCoder) {

+ 40 - 27
Lanu/Views/Profile/Relation/LNUserRelationListView.swift

@@ -17,6 +17,7 @@ protocol LNUserRelationListViewDelegate: NSObject {
 
 
 class LNUserRelationListView: UIView {
+    private let emptyView = LNNoMoreDataView()
     private let tableView = UITableView()
     let curType: LNUserRelationTabType
     
@@ -40,20 +41,41 @@ class LNUserRelationListView: UIView {
 }
 
 extension LNUserRelationListView {
-    private func loadList(handler: @escaping (Bool) -> Void) {
-        let completion: ([LNRelationUserVO], String?, Int?) -> Void = { [weak self] list, next, total in
+    private func loadList() {
+        let completion: ([LNRelationUserVO]?, String?, Int?) -> Void = { [weak self] list, next, total in
             guard let self else { return }
-            if nextTag.isEmpty {
-                curList = list
+            if let list, let next, let total {
+                if nextTag.isEmpty {
+                    curList = list
+                } else {
+                    curList.append(contentsOf: list)
+                }
+                tableView.reloadData()
+                
+                nextTag = next
+                delegate?.onUserRelationListViewTotalChanged(view: self, total: total)
+                
+                if curList.isEmpty {
+                    let text: String = switch curType {
+                    case .follow: .init(key: "暂无关注")
+                    case .fans: .init(key: "暂无粉丝")
+                    }
+                    emptyView.showNoData(icon: "ic_no_data_relation_empty", tips: text)
+                } else {
+                    emptyView.hide()
+                }
             } else {
-                curList.append(contentsOf: list)
+                if curList.isEmpty {
+                    emptyView.showNetworkError()
+                }
             }
-            tableView.reloadData()
-            
-            nextTag = nextTag
-            delegate?.onUserRelationListViewTotalChanged(view: self, total: total ?? 0)
             
-            handler(next?.isEmpty != true)
+            self.tableView.mj_header?.endRefreshing()
+            if next?.isEmpty != false {
+                tableView.mj_footer?.endRefreshingWithNoMoreData()
+            } else {
+                tableView.mj_footer?.endRefreshing()
+            }
         }
         
         switch curType {
@@ -92,15 +114,7 @@ extension LNUserRelationListView {
         let header = MJRefreshNormalHeader { [weak self] in
             guard let self else { return }
             self.nextTag = ""
-            self.loadList { [weak self] hasMore in
-                guard let self else { return }
-                self.tableView.mj_header?.endRefreshing()
-                if hasMore {
-                    self.tableView.mj_footer?.endRefreshing()
-                } else {
-                    self.tableView.mj_footer?.endRefreshingWithNoMoreData()
-                }
-            }
+            self.loadList()
         }
         header.lastUpdatedTimeLabel?.isHidden = true
         header.stateLabel?.isHidden = true
@@ -108,14 +122,7 @@ extension LNUserRelationListView {
         
         let footer = MJRefreshAutoNormalFooter { [weak self] in
             guard let self else { return }
-            self.loadList { [weak self] hasMore in
-                guard let self else { return }
-                if hasMore {
-                    self.tableView.mj_footer?.endRefreshing()
-                } else {
-                    self.tableView.mj_footer?.endRefreshingWithNoMoreData()
-                }
-            }
+            self.loadList()
         }
         footer.setTitle("", for: .noMoreData)
         tableView.mj_footer = footer
@@ -128,5 +135,11 @@ extension LNUserRelationListView {
         tableView.snp.makeConstraints { make in
             make.directionalEdges.equalToSuperview()
         }
+        
+        tableView.addSubview(emptyView)
+        emptyView.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.centerY.equalToSuperview().multipliedBy(0.6)
+        }
     }
 }

+ 2 - 0
Lanu/Views/Search/LNUserSearchItemCell.swift

@@ -34,6 +34,8 @@ class LNUserSearchItemCell: UITableViewCell {
         idLabel.text = "ID \(item.userNo)"
         fansCountLabel.text = .init(key: "粉丝数 %d", item.followCount)
         
+        followButton.isHidden = item.userNo.isMyUid
+        
         curItem = item
         
         updateFollowButton()

+ 62 - 12
Lanu/Views/Search/LNUserSearchViewController.swift

@@ -23,7 +23,11 @@ class LNUserSearchViewController: LNViewController {
     
     private let historyView = LNUserSearchHistoryView()
     
+    private let emptyView = LNNoMoreDataView()
     private let tableView = UITableView()
+    
+    private var curKeyword: String?
+    private var nextTag: String?
     private var curList: [LNGameMateSearchResultVO] = []
     
     override func viewDidLoad() {
@@ -35,18 +39,44 @@ class LNUserSearchViewController: LNViewController {
 }
 
 extension LNUserSearchViewController {
-    private func searchUser(_ keyword: String) {
+    private func searchUser() {
+        guard let curKeyword, !curKeyword.isEmpty else {
+            tableView.mj_header?.endRefreshing()
+            tableView.mj_footer?.endRefreshingWithNoMoreData()
+            return
+        }
         view.endEditing(true)
         
         historyView.isHidden = true
-        historyView.addRecord(keyword)
+        historyView.addRecord(curKeyword)
         
         tableView.isHidden = false
-        LNGameMateManager.shared.searchGameMate(keyword: keyword, next: "")
-        { [weak self] list in
+        LNGameMateManager.shared.searchGameMate(keyword: curKeyword, next: nextTag ?? "")
+        { [weak self] list, next in
             guard let self else { return }
+            guard let list else {
+                tableView.mj_header?.endRefreshing()
+                tableView.mj_footer?.endRefreshingWithNoMoreData()
+                
+                if curList.isEmpty {
+                    emptyView.showNetworkError()
+                }
+                return
+            }
             curList = list
             tableView.reloadData()
+            if list.isEmpty {
+                emptyView.showNoData(tips: .init(key: "暂无搜索结果"))
+            } else {
+                emptyView.hide()
+            }
+            
+            self.tableView.mj_header?.endRefreshing()
+            if next?.isEmpty != false {
+                tableView.mj_footer?.endRefreshingWithNoMoreData()
+            } else {
+                tableView.mj_footer?.endRefreshing()
+            }
         }
     }
 }
@@ -77,18 +107,20 @@ extension LNUserSearchViewController: UITableViewDataSource, UITableViewDelegate
 extension LNUserSearchViewController: LNUserSearchHistoryViewDelegate {
     func onUserSearchHistoryView(view: LNUserSearchHistoryView, didClick history: String) {
         searchInput.text = history
+        curKeyword = history
         
-        searchUser(history)
+        searchUser()
     }
 }
 
 extension LNUserSearchViewController: UITextFieldDelegate {
     func textFieldShouldReturn(_ textField: UITextField) -> Bool {
         textField.resignFirstResponder()
-        
         guard let text = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines),
               !text.isEmpty else { return true }
-        searchUser(text)
+        
+        curKeyword = text
+        searchUser()
         
         return true
     }
@@ -161,11 +193,7 @@ extension LNUserSearchViewController {
         search.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
         search.addAction(UIAction(handler: { [weak self] _ in
             guard let self else { return }
-            guard let text = searchInput.text else { return }
-            let fixed = text.trimmingCharacters(in: .whitespacesAndNewlines)
-            guard !fixed.isEmpty else { return }
-            
-            searchUser(fixed)
+            _ = textFieldShouldReturn(searchInput)
         }), for: .touchUpInside)
         container.addSubview(search)
         search.snp.makeConstraints { make in
@@ -225,6 +253,21 @@ extension LNUserSearchViewController {
     }
     
     private func buildList() -> UIView {
+        let header = MJRefreshNormalHeader { [weak self] in
+            guard let self else { return }
+            nextTag = nil
+            searchUser()
+        }
+        header.lastUpdatedTimeLabel?.isHidden = true
+        header.stateLabel?.isHidden = true
+        tableView.mj_header = header
+        
+        let footer = MJRefreshAutoNormalFooter { [weak self] in
+            guard let self else { return }
+            searchUser()
+        }
+        footer.setTitle("", for: .noMoreData)
+        tableView.mj_footer = footer
         tableView.isHidden = true
         tableView.dataSource = self
         tableView.delegate = self
@@ -236,6 +279,12 @@ extension LNUserSearchViewController {
             forCellReuseIdentifier: LNUserSearchItemCell.className
         )
         
+        tableView.addSubview(emptyView)
+        emptyView.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.centerY.equalToSuperview().multipliedBy(0.6)
+        }
+        
         return tableView
     }
     
@@ -250,6 +299,7 @@ extension LNUserSearchViewController {
 #if DEBUG
 
 import SwiftUI
+import MJRefresh
 
 struct LNIMSearchViewControllerPreview: UIViewControllerRepresentable {
     func makeUIViewController(context: Context) -> some UIViewController {