Bladeren bron

feat: 增加首页房间列表的 Tab,增加 roomType 的过滤

陈文艺 1 week geleden
bovenliggende
commit
1c8dadedec

+ 2 - 0
Lanu.xcodeproj/project.pbxproj

@@ -369,6 +369,7 @@
 				Views/Room/Join/Manage/LNRoomManageSeatListView.swift,
 				Views/Room/Join/Manage/LNRoomManageSeatTabView.swift,
 				"Views/Room/LNCommonAlertView+Room.swift",
+				Views/Room/LNRoomListViewController.swift,
 				Views/Room/LNRoomOrderGuideView.swift,
 				Views/Room/LNRoomSheetMenu.swift,
 				Views/Room/LNRoomViewController.swift,
@@ -386,6 +387,7 @@
 				Views/Room/Settings/LNRoomInfoEditPanel.swift,
 				Views/Room/Settings/LNRoomSettingMenuPanel.swift,
 				Views/Room/Top/LNRoomTopMenuView.swift,
+				"Views/Room/ViewModel/LiveInfo+Extension.swift",
 				Views/Room/ViewModel/LNRoomViewModel.swift,
 				Views/Room/ViewModel/Message/LNRoomChatMessageItem.swift,
 				Views/Room/ViewModel/Message/LNRoomSystemMessageItem.swift,

+ 22 - 0
Lanu/Assets.xcassets/Main/ic_main_room_default.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/Main/ic_main_room_default.imageset/ic_main_room_default@2x.png


BIN
Lanu/Assets.xcassets/Main/ic_main_room_default.imageset/ic_main_room_default@3x.png


+ 22 - 0
Lanu/Assets.xcassets/Main/ic_main_room_selected.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/Main/ic_main_room_selected.imageset/ic_main_room_selected@2x.png


BIN
Lanu/Assets.xcassets/Main/ic_main_room_selected.imageset/ic_main_room_selected@3x.png


+ 23 - 0
Lanu/Localizable.xcstrings

@@ -8879,6 +8879,29 @@
         }
       }
     },
+    "A00389" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "The current room type is not supported. Please update to the latest version."
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Tipe ruangan saat ini tidak didukung. Silakan perbarui ke versi terbaru."
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "当前房间类型暂不支持,请更新至最新版本"
+          }
+        }
+      }
+    },
     "B00001" : {
       "extractionState" : "manual",
       "localizations" : {

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

@@ -154,18 +154,6 @@ extension LNGameMateManager {
             }
         }
     }
-    
-    func searchRoom(keyword: String, next: String?, queue: DispatchQueue = .main,
-                    handler: @escaping (LNSearchRoomResponse?) -> Void) {
-        LNHttpManager.shared.searchRoom(keyword: keyword, size: 30, next: next ?? "") { res, err in
-            queue.asyncIfNotGlobal {
-                handler(res)
-            }
-            if let err {
-                showToast(err.errorDesc)
-            }
-        }
-    }
 }
 
 // MARK: 陪玩师类别

+ 0 - 12
Lanu/Manager/GameMate/Network/LNHttpManager+GameMate.swift

@@ -20,7 +20,6 @@ private let kNetPath_GameMate_Skill_Comment = "/skill/goods/comments"
 
 private let kNetPath_GameMate_Search = "/playmate/search"
 private let kNetPath_GameMate_Mixed_Search = "/search/mixed/list"
-private let kNetPath_GameMate_Room_Search = "/live/room/list"
 
 private let kNetPath_GameMate_Join_Infos = "/playmate/apply/curInfo"
 private let kNetPath_GameMate_Join_Improve_BaseInfo = "/playmate/apply/improve/info"
@@ -193,17 +192,6 @@ extension LNHttpManager {
         ], completion: completion)
     }
     
-    func searchRoom(keyword: String, size: Int,
-                    next: String, completion: @escaping (LNSearchRoomResponse?, LNHttpError?) -> Void) {
-        post(path: kNetPath_GameMate_Room_Search, params: [
-            "keyword": keyword,
-            "page": [
-                "size": size,
-                "next": next
-            ]
-        ], completion: completion)
-    }
-    
     func scoreGameMate(uid: String, score: Int, completion: @escaping (LNHttpError?) -> Void) {
         post(path: kNetPath_GameMate_Score, params: [
             "userNo": uid,

+ 1 - 1
Lanu/Manager/IM/LNIMManager.swift

@@ -73,7 +73,7 @@ class LNIMVoiceCallInfo {
 class LNIMManager: NSObject {
     private static var appId: Int32 {
         if LNAppConfig.shared.curEnv == .test {
-            20030346
+            20034873
         } else {
             80000456
         }

+ 23 - 0
Lanu/Manager/Room/LNRoomManager.swift

@@ -144,6 +144,29 @@ extension LNRoomManager {
             }
         }
     }
+    
+    func searchRoom(keyword: String, next: String?, queue: DispatchQueue = .main,
+                    handler: @escaping (LNSearchRoomResponse?) -> Void) {
+        LNHttpManager.shared.searchRoom(keyword: keyword, size: 30, next: next ?? "") { res, err in
+            queue.asyncIfNotGlobal {
+                handler(res)
+            }
+            if let err {
+                showToast(err.errorDesc)
+            }
+        }
+    }
+    
+    func getRoomList(next: String?, queue: DispatchQueue = .main, handler: @escaping (LNSearchRoomResponse?) -> Void) {
+        LNHttpManager.shared.getRoomList(size: 30, next: next ?? "") { res, err in
+            queue.asyncIfNotGlobal {
+                handler(res)
+            }
+            if let err {
+                showToast(err.errorDesc)
+            }
+        }
+    }
 }
 
 // MARK: 用户

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

@@ -28,6 +28,9 @@ private let kNetPath_Room_Random = "/live/room/random"
 
 private let kNetPath_Room_Cur = "/live/view/watching"
 
+private let kNetPath_Room_Search = "/live/room/search"
+private let kNetPath_Room_List = "/live/room/list"
+
 
 // MARK: 房间管理
 extension LNHttpManager {
@@ -166,6 +169,30 @@ extension LNHttpManager {
     }
     
     func getRandomRoom(completion: @escaping (LNRandomRoomResponse?, LNHttpError?) -> Void) {
-        post(path: kNetPath_Room_Random, completion: completion)
+        post(path: kNetPath_Room_Random, params: [
+            "roomTypes": LNRoomType.availableType.map({ $0.rawValue }),
+        ], completion: completion)
+    }
+    
+    func searchRoom(keyword: String, size: Int,
+                    next: String, completion: @escaping (LNSearchRoomResponse?, LNHttpError?) -> Void) {
+        post(path: kNetPath_Room_Search, params: [
+            "keyword": keyword,
+            "roomTypes": LNRoomType.availableType.map({ $0.rawValue }),
+            "page": [
+                "size": size,
+                "next": next
+            ]
+        ], completion: completion)
+    }
+    
+    func getRoomList(size: Int, next: String, completion: @escaping (LNSearchRoomResponse?, LNHttpError?) -> Void) {
+        post(path: kNetPath_Room_List, params: [
+            "roomTypes": LNRoomType.availableType.map({ $0.rawValue }),
+            "page": [
+                "size": size,
+                "next": next
+            ]
+        ], completion: completion)
     }
 }

+ 5 - 0
Lanu/Manager/Room/Network/LNRoomResponse.swift

@@ -17,7 +17,12 @@ enum LNRoomApplySeatType: Int, Decodable {
 
 
 enum LNRoomType: Int, Decodable {
+    case unknown = 0
     case playmate = 1
+    
+    static var availableType: [LNRoomType] {
+        [.playmate]
+    }
 }
 
 @AutoCodable

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

@@ -31,6 +31,10 @@ class LNMainViewController: UITabBarController {
         home.tabBarItem.image = .icMainHomeDefault.withRenderingMode(.alwaysOriginal)
         home.tabBarItem.selectedImage = .icMainHomeSelected.withRenderingMode(.alwaysOriginal)
         
+        let room = LNRoomListViewController()
+        room.tabBarItem.image = .icMainRoomDefault.withRenderingMode(.alwaysOriginal)
+        room.tabBarItem.selectedImage = .icMainRoomSelected.withRenderingMode(.alwaysOriginal)
+        
         let message = LNIMConversationListController()
         message.tabBarItem.image = .icMainChatDefault.withRenderingMode(.alwaysOriginal)
         message.tabBarItem.selectedImage = .icMainChatSelected.withRenderingMode(.alwaysOriginal)
@@ -39,7 +43,7 @@ class LNMainViewController: UITabBarController {
         mine.tabBarItem.image = .icMainMineDefault.withRenderingMode(.alwaysOriginal)
         mine.tabBarItem.selectedImage = .icMainMineSelected.withRenderingMode(.alwaysOriginal)
         
-        viewControllers = [home, message, mine]
+        viewControllers = [home, room, message, mine]
         
         if let mainTabBar = tabBar as? LNMainTabBar {
             mainTabBar.setupViews()

+ 183 - 0
Lanu/Views/Room/LNRoomListViewController.swift

@@ -0,0 +1,183 @@
+//
+//  LNRoomListViewController.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/26.
+//
+
+import Foundation
+import UIKit
+import MJRefresh
+import SnapKit
+
+
+class LNRoomListViewController: UIViewController {
+    private let emptyView = LNNoMoreDataView()
+    private var items: [LNRoomItemVO] = []
+    private var nextTag: String? = nil
+    private let collectionView: UICollectionView
+    
+    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
+        let width = (UIScreen.main.bounds.width - 16 * 2 - 13) * 0.5
+        
+        let layout = UICollectionViewFlowLayout()
+        layout.scrollDirection = .vertical
+        layout.minimumLineSpacing = 16
+        layout.minimumInteritemSpacing = 13
+        layout.itemSize = .init(width: width, height: width * 204 / 165)
+        self.collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
+        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
+    }
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        setupViews()
+        
+        nextTag = nil
+        items.removeAll()
+        collectionView.reloadData()
+        emptyView.hide()
+        collectionView.mj_footer?.resetNoMoreData()
+        collectionView.mj_header?.beginRefreshing()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNRoomListViewController {
+    private func loadList() {
+        LNRoomManager.shared.getRoomList(next: nextTag) { [weak self] res in
+            guard let self else { return }
+            guard let list = res?.list else {
+                collectionView.mj_header?.endRefreshing()
+                collectionView.mj_footer?.endRefreshingWithNoMoreData()
+                
+                if items.isEmpty {
+                    emptyView.showNetworkError()
+                }
+                return
+            }
+            
+            if nextTag?.isEmpty != false {
+                items = list
+            } else {
+                items.append(contentsOf: list)
+            }
+            nextTag = res?.next
+            
+            collectionView.reloadData()
+            if items.isEmpty {
+                emptyView.showNoData(tips: .init(key: "A00244"))
+            } else {
+                emptyView.hide()
+            }
+            
+            collectionView.mj_header?.endRefreshing()
+            if res?.next.isEmpty != false {
+                collectionView.mj_footer?.endRefreshingWithNoMoreData()
+            } else {
+                collectionView.mj_footer?.endRefreshing()
+            }
+        }
+    }
+}
+
+extension LNRoomListViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
+    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
+        items.count
+    }
+    
+    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
+        let cell = collectionView.dequeueReusableCell(
+            withReuseIdentifier: LNRoomListCardCell.className,
+            for: indexPath
+        ) as! LNRoomListCardCell
+        cell.update(items[indexPath.item])
+        return cell
+    }
+}
+
+extension LNRoomListViewController {
+    private func setupViews() {
+        view.backgroundColor = .primary_1
+        
+        let fakeNavBar = LNFakeNaviBar()
+        fakeNavBar.isUserInteractionEnabled = false // 因为是空白
+        view.addSubview(fakeNavBar)
+        fakeNavBar.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalToSuperview()
+        }
+        
+        let titleLabel = UILabel()
+        titleLabel.text = .init(key: "A00370")
+        titleLabel.font = .heading_h3
+        titleLabel.textColor = .text_5
+        fakeNavBar.actionView.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+        }
+        
+        let header = MJRefreshNormalHeader { [weak self] in
+            guard let self else { return }
+            nextTag = nil
+            loadList()
+        }
+        header.lastUpdatedTimeLabel?.isHidden = true
+        header.stateLabel?.isHidden = true
+        collectionView.mj_header = header
+        
+        let footer = MJRefreshAutoNormalFooter { [weak self] in
+            guard let self else { return }
+            loadList()
+        }
+        footer.setTitle("", for: .noMoreData)
+        footer.setTitle(.init(key: "A00046"), for: .idle)
+        collectionView.mj_footer = footer
+        
+        collectionView.backgroundColor = .clear
+        collectionView.showsVerticalScrollIndicator = false
+        collectionView.showsHorizontalScrollIndicator = false
+        collectionView.allowsSelection = false
+        collectionView.dataSource = self
+        collectionView.delegate = self
+        collectionView.register(LNRoomListCardCell.self, forCellWithReuseIdentifier: LNRoomListCardCell.className)
+        collectionView.contentInset = .init(top: 6, left: 0, bottom: -view.commonBottomInset, right: 0)
+        view.addSubview(collectionView)
+        collectionView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(16)
+            make.top.equalTo(fakeNavBar.snp.bottom)
+            make.bottom.equalToSuperview()
+        }
+        
+        collectionView.addSubview(emptyView)
+        emptyView.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.centerY.equalToSuperview().multipliedBy(0.6)
+        }
+    }
+}
+
+private final class LNRoomListCardCell: UICollectionViewCell {
+    private let cardView = LNUserSearchRoomCardView()
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        contentView.addSubview(cardView)
+        cardView.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+        }
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    func update(_ item: LNRoomItemVO) {
+        cardView.update(item)
+    }
+}

+ 7 - 8
Lanu/Views/Room/ViewModel/LNRoomViewModel.swift

@@ -79,7 +79,6 @@ class LNRoomViewModel: NSObject {
     private(set) var roomInfo = LNRoomInfo()
     private(set) var speakingUser: [String] = []
     private(set) var lastmessage: Barrage? = nil
-    private let seatApplyCountKey = "mic_apply_num"
     private(set) var seatApplyCount: Int = 0
     private(set) var waitingForSeat: LNApplyingSeatType = .none {
         didSet {
@@ -489,18 +488,18 @@ extension LNRoomViewModel {
     private func setupRoomInfoObserver() {
         LNRoomManager.shared.liveListStore.state.subscribe().receive(on: DispatchQueue.main).sink { [weak self] state in
             guard let self else { return }
+            if state.currentLive.roomType == .unknown {
+                showToast(.init(key: "A00389"))
+                leaveRoom()
+                return
+            }
             if roomInfo.update(state.currentLive) {
                 LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomInfoChanged() }
             }
             
             let oldCount = seatApplyCount
-            if let value = state.currentLive.metaData[seatApplyCountKey],
-                let count = Int(value) {
-                if count != seatApplyCount {
-                    seatApplyCount = count
-                }
-            } else if seatApplyCount != 0 {
-                seatApplyCount = 0
+            if state.currentLive.applySeatCount != seatApplyCount {
+                seatApplyCount = state.currentLive.applySeatCount
             }
             if oldCount != seatApplyCount {
                 LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomSeatApplyChanged() }

+ 37 - 0
Lanu/Views/Room/ViewModel/LiveInfo+Extension.swift

@@ -0,0 +1,37 @@
+//
+//  LiveInfo+Extension.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/26.
+//
+
+import Foundation
+import AtomicXCore
+
+
+private let seatApplyCountKey = "mic_apply_num"
+private let roomTypeKey = "room_type"
+private let state_mic_mute = "state_mic_mute"
+
+extension LiveInfo {
+    var roomType: LNRoomType {
+        guard let value = metaData[roomTypeKey],
+              let iValue = Int(value),
+              let type = LNRoomType(rawValue: iValue) else {
+            return .unknown
+        }
+        return type
+    }
+    
+    var applySeatCount: Int {
+        guard let value = metaData[seatApplyCountKey],
+              let count = Int(value) else {
+            return 0
+        }
+        return count
+    }
+    
+    var forbidAudio: Bool {
+        metaData[state_mic_mute] == "1"
+    }
+}

+ 5 - 6
Lanu/Views/Room/ViewModel/RoomInfo/LNRoomInfo.swift

@@ -9,29 +9,28 @@ import Foundation
 import AtomicXCore
 
 
-private let state_mic_mute = "state_mic_mute"
-
-
 class LNRoomInfo {
     var liveID: String = ""
     var liveName: String = ""
     var coverURL: String = ""
     var forbidAudio: Bool = false
     var owner: String = ""
+    var roomType: LNRoomType = .unknown
     
     @discardableResult
     func update(_ info: LiveInfo) -> Bool {
-        let forbid = info.metaData[state_mic_mute] == "1"
         if info.liveID != liveID
             || info.liveName != liveName
             || info.coverURL != coverURL
             || info.liveOwner.userID != owner
-            || forbid != forbidAudio {
+            || info.forbidAudio != forbidAudio
+            || info.roomType != roomType {
             liveID = info.liveID
             liveName = info.liveName
             coverURL = info.coverURL
             owner = info.liveOwner.userID
-            forbidAudio = forbid
+            roomType = info.roomType
+            forbidAudio = info.forbidAudio
             return true
         }
         return false

+ 2 - 2
Lanu/Views/Search/LNUserSearchRoomListView.swift

@@ -58,7 +58,7 @@ extension LNUserSearchRoomListView {
         
         collectionView.isHidden = false
         
-        LNGameMateManager.shared.searchRoom(keyword: curKeyword, next: nextTag) { [weak self] res in
+        LNRoomManager.shared.searchRoom(keyword: curKeyword, next: nextTag) { [weak self] res in
             guard let self else { return }
             guard let list = res?.list else {
                 collectionView.mj_header?.endRefreshing()
@@ -111,7 +111,7 @@ extension LNUserSearchRoomListView: UICollectionViewDataSource, UICollectionView
 
 extension LNUserSearchRoomListView {
     private func setupViews() {
-        backgroundColor = .white
+        backgroundColor = .primary_1
         
         let header = MJRefreshNormalHeader { [weak self] in
             guard let self else { return }