Jelajahi Sumber

feat: 房间的基础功能完善

陈文艺 2 minggu lalu
induk
melakukan
640ca679dc
61 mengubah file dengan 3528 tambahan dan 375 penghapusan
  1. 24 8
      Lanu.xcodeproj/project.pbxproj
  2. 1 0
      Lanu/AppDelegate.swift
  3. 22 0
      Lanu/Assets.xcassets/Seat/ic_seat_lock.imageset/Contents.json
  4. TEMPAT SAMPAH
      Lanu/Assets.xcassets/Seat/ic_seat_lock.imageset/ic_seat_lock@2x.png
  5. TEMPAT SAMPAH
      Lanu/Assets.xcassets/Seat/ic_seat_lock.imageset/ic_seat_lock@3x.png
  6. 22 0
      Lanu/Assets.xcassets/Setting/ic_settings_room.imageset/Contents.json
  7. TEMPAT SAMPAH
      Lanu/Assets.xcassets/Setting/ic_settings_room.imageset/ic_settings_room@2x.png
  8. TEMPAT SAMPAH
      Lanu/Assets.xcassets/Setting/ic_settings_room.imageset/ic_settings_room@3x.png
  9. 21 18
      Lanu/Common/Views/LNCommonInputPanel.swift
  10. 0 1
      Lanu/Common/Views/LNOnlineView.swift
  11. 1 1
      Lanu/Common/Views/Menu/LNBottomSheetMenu.swift
  12. 460 0
      Lanu/Localizable.xcstrings
  13. 2 11
      Lanu/Manager/IM/LNIMManager.swift
  14. 83 30
      Lanu/Manager/Room/LNRoomManager.swift
  15. 0 15
      Lanu/Manager/Room/Network/LNRoomResponse.swift
  16. 0 7
      Lanu/Views/Game/Skill/LNSkillCommentsView.swift
  17. 16 6
      Lanu/Views/Game/Skill/LNSkillDetailViewController.swift
  18. 2 1
      Lanu/Views/Profile/Feed/LNFeedCommentListPanel.swift
  19. 2 1
      Lanu/Views/Profile/Feed/LNImageFeedDetailViewController.swift
  20. 2 1
      Lanu/Views/Profile/Feed/LNProfileFeedItemCell.swift
  21. 3 14
      Lanu/Views/Profile/Feed/LNVideoFeedDetailViewController.swift
  22. 0 0
      Lanu/Views/Profile/LNCommonAlertView+Profile.swift
  23. 0 0
      Lanu/Views/Profile/LNCommonAlertView+Relation.swift
  24. 24 18
      Lanu/Views/Profile/Profile/LNProfileViewController.swift
  25. 61 0
      Lanu/Views/Room/Bottom/Input/LNRoomMessageInputView.swift
  26. 174 0
      Lanu/Views/Room/Bottom/Join/ApplyList/LNRoomSeatApplyListCell.swift
  27. 248 0
      Lanu/Views/Room/Bottom/Join/ApplyList/LNRoomSeatApplyListPanel.swift
  28. 107 0
      Lanu/Views/Room/Bottom/Join/LNRoomApplySeatPanel.swift
  29. 175 0
      Lanu/Views/Room/Bottom/Join/LNRoomJoinMenuView.swift
  30. 0 28
      Lanu/Views/Room/Bottom/LNRoomApplySeatPanel.swift
  31. 10 19
      Lanu/Views/Room/Bottom/LNRoomBottomMenuView.swift
  32. 0 93
      Lanu/Views/Room/Bottom/LNRoomJoinMenuView.swift
  33. 33 23
      Lanu/Views/Room/Create/LNCreateRoomPanel.swift
  34. 23 0
      Lanu/Views/Room/LNCommonAlertView+Room.swift
  35. 0 8
      Lanu/Views/Room/LNRoomBottomMenuView.swift
  36. 0 28
      Lanu/Views/Room/LNRoomMessageView.swift
  37. 114 0
      Lanu/Views/Room/LNRoomSheetMenu.swift
  38. 0 8
      Lanu/Views/Room/LNRoomTopMenuView.swift
  39. 10 1
      Lanu/Views/Room/LNRoomViewController.swift
  40. 6 4
      Lanu/Views/Room/Message/LNRoomChatMessageCell.swift
  41. 18 8
      Lanu/Views/Room/Message/LNRoomMessageView.swift
  42. 1 1
      Lanu/Views/Room/Message/LNRoomSystemMessageCell.swift
  43. 114 0
      Lanu/Views/Room/Profile/LNRoomProfileBottomMenu.swift
  44. 176 0
      Lanu/Views/Room/Profile/LNRoomProfileCardPanel.swift
  45. 153 0
      Lanu/Views/Room/Profile/LNRoomProfileSkillView.swift
  46. 46 2
      Lanu/Views/Room/Seats/Guest/LNRoomGuestSeatView.swift
  47. 47 2
      Lanu/Views/Room/Seats/Host/LNRoomHostSeatView.swift
  48. 156 0
      Lanu/Views/Room/Seats/LNRoomSeatSpeakingView.swift
  49. 150 0
      Lanu/Views/Room/Seats/LNRoomSeatViewProtocol.swift
  50. 6 3
      Lanu/Views/Room/Seats/LNRoomSeatsView.swift
  51. 52 3
      Lanu/Views/Room/Seats/Playmate/LNRoomPlaymateSeatView.swift
  52. 276 0
      Lanu/Views/Room/Settings/LNRoomInfoEditPanel.swift
  53. 140 0
      Lanu/Views/Room/Settings/LNRoomSettingMenuPanel.swift
  54. 22 7
      Lanu/Views/Room/Top/LNRoomTopMenuView.swift
  55. 29 0
      Lanu/Views/Room/ViewModel/LNRoomInfo.swift
  56. 27 0
      Lanu/Views/Room/ViewModel/LNRoomMessageItem.swift
  57. 61 0
      Lanu/Views/Room/ViewModel/LNRoomSeatApplyItem.swift
  58. 40 0
      Lanu/Views/Room/ViewModel/LNRoomSeatItem.swift
  59. 366 3
      Lanu/Views/Room/ViewModel/LNRoomViewModel.swift
  60. 1 1
      Podfile
  61. 1 1
      Podfile.lock

+ 24 - 8
Lanu.xcodeproj/project.pbxproj

@@ -80,6 +80,7 @@
 				Common/Views/LNAutoSizeTextView.swift,
 				Common/Views/LNCaptchaInputView.swift,
 				Common/Views/LNCircleProgressView.swift,
+				Common/Views/LNCommonInputPanel.swift,
 				Common/Views/LNCountrySelectPanel.swift,
 				Common/Views/LNGenderView.swift,
 				Common/Views/LNNestedScrollView.swift,
@@ -303,7 +304,6 @@
 				Views/Order/OrderRecords/LNOrderRecordListViewController.swift,
 				Views/Order/Refund/LNOrderProtestViewController.swift,
 				Views/Order/Refund/LNOrderRefundViewController.swift,
-				"Views/Profile/Edit/LNCommonAlertView+Profile.swift",
 				Views/Profile/Edit/LNEditBioPanel.swift,
 				Views/Profile/Edit/LNEditGenderPanel.swift,
 				Views/Profile/Edit/LNEditInterestPanel.swift,
@@ -313,7 +313,6 @@
 				Views/Profile/Edit/LNEditVoicePanel.swift,
 				Views/Profile/Feed/LNCreateFeedViewController.swift,
 				Views/Profile/Feed/LNFeedCommentCell.swift,
-				Views/Profile/Feed/LNFeedCommentInputPanel.swift,
 				Views/Profile/Feed/LNFeedCommentListPanel.swift,
 				Views/Profile/Feed/LNFeedCommentView.swift,
 				Views/Profile/Feed/LNFeedLikeView.swift,
@@ -321,6 +320,8 @@
 				Views/Profile/Feed/LNProfileFeedItemCell.swift,
 				Views/Profile/Feed/LNProfileFeedProvider.swift,
 				Views/Profile/Feed/LNVideoFeedDetailViewController.swift,
+				"Views/Profile/LNCommonAlertView+Profile.swift",
+				"Views/Profile/LNCommonAlertView+Relation.swift",
 				Views/Profile/Mine/LNMineFunctionView.swift,
 				Views/Profile/Mine/LNMineOrderRecordView.swift,
 				Views/Profile/Mine/LNMineQRCodeShareView.swift,
@@ -344,26 +345,41 @@
 				Views/Profile/Profile/LNProfileUserInfoView.swift,
 				Views/Profile/Profile/LNProfileViewController.swift,
 				Views/Profile/Profile/LNProfileVoiceBarView.swift,
-				"Views/Profile/Relation/LNCommonAlertView+Relation.swift",
 				Views/Profile/Relation/LNUserRelationItemCell.swift,
 				Views/Profile/Relation/LNUserRelationListView.swift,
 				Views/Profile/Relation/LNUserRelationViewController.swift,
 				Views/Report/LNReportViewController.swift,
-				Views/Room/Bottom/LNRoomApplySeatPanel.swift,
+				Views/Room/Bottom/Input/LNRoomMessageInputView.swift,
+				Views/Room/Bottom/Join/ApplyList/LNRoomSeatApplyListCell.swift,
+				Views/Room/Bottom/Join/ApplyList/LNRoomSeatApplyListPanel.swift,
+				Views/Room/Bottom/Join/LNRoomApplySeatPanel.swift,
+				Views/Room/Bottom/Join/LNRoomJoinMenuView.swift,
 				Views/Room/Bottom/LNRoomBottomMenuView.swift,
-				Views/Room/Bottom/LNRoomJoinMenuView.swift,
 				Views/Room/Create/LNCreateRoomPanel.swift,
 				Views/Room/Create/LNRoomNameInputPanel.swift,
+				"Views/Room/LNCommonAlertView+Room.swift",
+				Views/Room/LNRoomSheetMenu.swift,
 				Views/Room/LNRoomViewController.swift,
 				Views/Room/Message/LNRoomChatMessageCell.swift,
 				Views/Room/Message/LNRoomMessageView.swift,
 				Views/Room/Message/LNRoomSystemMessageCell.swift,
 				Views/Room/Message/LNRoomUnknownMessageCell.swift,
-				Views/Room/Seats/LNRoomGuestSeatView.swift,
-				Views/Room/Seats/LNRoomHostSeatView.swift,
-				Views/Room/Seats/LNRoomPlaymateSeatView.swift,
+				Views/Room/Profile/LNRoomProfileBottomMenu.swift,
+				Views/Room/Profile/LNRoomProfileCardPanel.swift,
+				Views/Room/Profile/LNRoomProfileSkillView.swift,
+				Views/Room/Seats/Guest/LNRoomGuestSeatView.swift,
+				Views/Room/Seats/Host/LNRoomHostSeatView.swift,
+				Views/Room/Seats/LNRoomSeatSpeakingView.swift,
 				Views/Room/Seats/LNRoomSeatsView.swift,
+				Views/Room/Seats/LNRoomSeatViewProtocol.swift,
+				Views/Room/Seats/Playmate/LNRoomPlaymateSeatView.swift,
+				Views/Room/Settings/LNRoomInfoEditPanel.swift,
+				Views/Room/Settings/LNRoomSettingMenuPanel.swift,
 				Views/Room/Top/LNRoomTopMenuView.swift,
+				Views/Room/ViewModel/LNRoomInfo.swift,
+				Views/Room/ViewModel/LNRoomMessageItem.swift,
+				Views/Room/ViewModel/LNRoomSeatApplyItem.swift,
+				Views/Room/ViewModel/LNRoomSeatItem.swift,
 				Views/Room/ViewModel/LNRoomViewModel.swift,
 				Views/Search/LNUserSearchHistoryView.swift,
 				Views/Search/LNUserSearchItemCell.swift,

+ 1 - 0
Lanu/AppDelegate.swift

@@ -31,6 +31,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
         _ = LNKeyboardManager.shared
         _ = LNConfigManager.shared
         _ = LNOrderManager.shared
+        _ = LNRoomManager.shared
         
         LNEventDeliver.notifyAppLaunchFinished()
         

+ 22 - 0
Lanu/Assets.xcassets/Seat/ic_seat_lock.imageset/Contents.json

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

TEMPAT SAMPAH
Lanu/Assets.xcassets/Seat/ic_seat_lock.imageset/ic_seat_lock@2x.png


TEMPAT SAMPAH
Lanu/Assets.xcassets/Seat/ic_seat_lock.imageset/ic_seat_lock@3x.png


+ 22 - 0
Lanu/Assets.xcassets/Setting/ic_settings_room.imageset/Contents.json

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

TEMPAT SAMPAH
Lanu/Assets.xcassets/Setting/ic_settings_room.imageset/ic_settings_room@2x.png


TEMPAT SAMPAH
Lanu/Assets.xcassets/Setting/ic_settings_room.imageset/ic_settings_room@3x.png


+ 21 - 18
Lanu/Views/Profile/Feed/LNFeedCommentInputPanel.swift → Lanu/Common/Views/LNCommonInputPanel.swift

@@ -1,5 +1,5 @@
 //
-//  LNFeedCommentInputPanel.swift
+//  LNCommonInputPanel.swift
 //  Gami
 //
 //  Created by OneeChan on 2026/3/3.
@@ -10,7 +10,8 @@ import UIKit
 import SnapKit
 
 
-class LNFeedCommentInputPanel: LNPopupView {
+class LNCommonInputPanel: LNPopupView {
+    var maxInput = 0
     private let textView = LNAutoSizeTextView()
     private var hideSend: Constraint?
     private let sendView = UIView()
@@ -33,8 +34,8 @@ class LNFeedCommentInputPanel: LNPopupView {
     }
 }
 
-extension LNFeedCommentInputPanel {
-    private func toSendComment() {
+extension LNCommonInputPanel {
+    private func toSendText() {
         dismiss()
         let cleanedText = (textView.text ?? "").replacingOccurrences(of: "\n", with: "").trimmingCharacters(in: .whitespacesAndNewlines)
         guard !cleanedText.isEmpty else { return }
@@ -42,10 +43,10 @@ extension LNFeedCommentInputPanel {
     }
 }
 
-extension LNFeedCommentInputPanel: UITextViewDelegate {
+extension LNCommonInputPanel: UITextViewDelegate {
     func textViewDidChange(_ textView: UITextView) {
-        if textView.text.count > LNFeedManager.feedCommentMaxInput {
-            textView.text = String(textView.text.prefix(LNFeedManager.feedCommentMaxInput))
+        if maxInput > 0, textView.text.count > maxInput {
+            textView.text = String(textView.text.prefix(maxInput))
         }
         let showSend = !textView.text.isEmpty
         hideSend?.update(priority: showSend ? .low : .high)
@@ -57,23 +58,25 @@ extension LNFeedCommentInputPanel: UITextViewDelegate {
     
     func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
         if text == "\n" {
-            toSendComment()
+            toSendText()
             return false
         }
-        let currentText = textView.text ?? ""
-        guard let swiftRange = Range(range, in: currentText) else {
-            return true
-        }
-        let newText = currentText.replacingCharacters(in: swiftRange, with: text)
-        
-        if newText.count > LNFeedManager.feedCommentMaxInput {
-            return false
+        if maxInput > 0 {
+            let currentText = textView.text ?? ""
+            guard let swiftRange = Range(range, in: currentText) else {
+                return true
+            }
+            let newText = currentText.replacingCharacters(in: swiftRange, with: text)
+            
+            if newText.count > maxInput {
+                return false
+            }
         }
         return true
     }
 }
 
-extension LNFeedCommentInputPanel {
+extension LNCommonInputPanel {
     private func setupViews() {
         container.layer.cornerRadius = 0
         ignoreKeyboardToDismiss = true
@@ -128,7 +131,7 @@ extension LNFeedCommentInputPanel {
         sendButton.contentEdgeInsets = .init(top: 0, left: 16, bottom: 0, right: 16)
         sendButton.addAction(UIAction(handler: { [weak self] _ in
             guard let self else { return }
-            toSendComment()
+            toSendText()
         }), for: .touchUpInside)
         sendView.addSubview(sendButton)
         sendButton.snp.makeConstraints { make in

+ 0 - 1
Lanu/Common/Views/LNOnlineView.swift

@@ -31,7 +31,6 @@ class LNOnlineView: UIView {
             reset()
         }
     }
-    private var animator: UIViewPropertyAnimator?
     
     private let borderLayer = CAShapeLayer()
     

+ 1 - 1
Lanu/Common/Views/Menu/LNBottomSheetMenu.swift

@@ -10,7 +10,7 @@ import UIKit
 import SnapKit
 
 class LNBottomSheetMenu: LNPopupView {
-    private let stackView = UIStackView()
+    let stackView = UIStackView()
     
     override init(frame: CGRect) {
         super.init(frame: frame)

+ 460 - 0
Lanu/Localizable.xcstrings

@@ -7545,6 +7545,466 @@
         }
       }
     },
+    "A00330" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Join Mic"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Mic Aktif"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "上麦互动"
+          }
+        }
+      }
+    },
+    "A00331" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Guest Seat"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Tempat Tamu"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "嘉宾位"
+          }
+        }
+      }
+    },
+    "A00332" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Playmate Seat"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Posisi pendamping"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "陪玩师位"
+          }
+        }
+      }
+    },
+    "A00333" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Applying to join mic"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Sedang mengajukan bergabung mic"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "在申请上麦"
+          }
+        }
+      }
+    },
+    "A00334" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%d people"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%d orang"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "%d 人"
+          }
+        }
+      }
+    },
+    "A00335" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Clear All Application"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Hapus Semua Pengajuan"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "清空所有申请"
+          }
+        }
+      }
+    },
+    "A00336" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Send Gift"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Kirim Hadiah"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "送礼物"
+          }
+        }
+      }
+    },
+    "A00337" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Room Settings"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Pengaturan Room"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "房间设置"
+          }
+        }
+      }
+    },
+    "A00338" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Close"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Tutup"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "关闭"
+          }
+        }
+      }
+    },
+    "A00339" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Tips"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Tips"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "提示"
+          }
+        }
+      }
+    },
+    "A00340" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Closing the room will remove all members."
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Menutup ruangan akan menghapus semua anggota."
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "关闭房间将清空房间人员哦~"
+          }
+        }
+      }
+    },
+    "A00341" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Invite User to Mic"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Undang Pengguna ke Mic"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "邀请用户上麦"
+          }
+        }
+      }
+    },
+    "A00342" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Lock Seat"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Kunci Kursi"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "关闭座位"
+          }
+        }
+      }
+    },
+    "A00343" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Mute Seat"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Matikan Mic Kursi"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "禁麦座位"
+          }
+        }
+      }
+    },
+    "A00344" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "View Profile"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Lihat Profil"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "查看资料"
+          }
+        }
+      }
+    },
+    "A00345" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Kick Off Mic"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Keluarkan dari Mic"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "踢下麦"
+          }
+        }
+      }
+    },
+    "A00346" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Leave Mic"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Turun Mic"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "下麦围观"
+          }
+        }
+      }
+    },
+    "A00347" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Apply to Be a Playmate"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Ajukan Jadi Pendamping"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "申请成为陪玩师"
+          }
+        }
+      }
+    },
+    "A00348" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Unlock Seat"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Buka Kunci Kursi"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "打开麦位"
+          }
+        }
+      }
+    },
+    "A00349" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Unmute Seat"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Hidupkan Mic Kursi"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "开麦座位"
+          }
+        }
+      }
+    },
     "B00001" : {
       "extractionState" : "manual",
       "localizations" : {

+ 2 - 11
Lanu/Manager/IM/LNIMManager.swift

@@ -8,7 +8,6 @@
 import Foundation
 import RTCRoomEngine
 import AVFAudio
-import AtomicXCore
 
 
 protocol LNIMManagerNotify {
@@ -73,7 +72,7 @@ class LNIMVoiceCallInfo {
 class LNIMManager: NSObject {
     private static var appId: Int32 {
         if LNAppConfig.shared.curEnv == .test {
-            80000468
+            20030346
         } else {
             80000456
         }
@@ -558,14 +557,7 @@ extension LNIMManager: LNAccountManagerNotify {
                 Log.e("TUICallEngine init failed: \(err ?? "")")
             }
             
-            LoginStore.shared.login(sdkAppID: Self.appId, userID: myUid, userSig: token) { error in
-                switch error {
-                  case .success(let info):
-                    Log.i("LoginStore", "login success")
-                  case .failure(let error):
-                    Log.i("LoginStore", "login failed code:\(error.code), message:\(error.message)")
-                }
-            }
+            LNRoomManager.shared.login(appId: Self.appId, token: token)
         }
     }
     
@@ -573,7 +565,6 @@ extension LNIMManager: LNAccountManagerNotify {
         V2TIMManager.sharedInstance().logout(succ: nil)
         TIMPushManager.unRegisterPush { } fail: { _, _ in }
         TUICallEngine.destroyInstance()
-        LoginStore.shared.logout(completion: nil)
         
         Self.shared = LNIMManager()
     }

+ 83 - 30
Lanu/Manager/Room/LNRoomManager.swift

@@ -9,69 +9,122 @@ import Foundation
 import AtomicXCore
 
 
-protocol LNRoomManagerNotify {
-    func onSeatInfoChanged()
-    func onSpeakingUserChanged()
-}
-
-
 class LNRoomManager {
     static let shared = LNRoomManager()
     static let RoomNameMinInput = 2
     static let RoomNameMaxInput = 20
     static let MicNum = 10
     
-    private let liveListStore = LiveListStore.shared
+    let liveListStore = LiveListStore.shared
+    
+    init () {
+        LNEventDeliver.addObserver(self)
+    }
+    
+    func login(appId: Int32, token: String) {
+        LoginStore.shared.login(sdkAppID: appId, userID: myUid, userSig: token) { error in
+            switch error {
+              case .success(let info):
+                Log.i("LoginStore", "login success \(info)")
+                let info = UserProfile(userID: myUid, nickname: myUserInfo.nickname, avatarURL: myUserInfo.avatar)
+                LoginStore.shared.setSelfInfo(userProfile: info, completion: nil)
+              case .failure(let error):
+                Log.i("LoginStore", "login failed code:\(error.code), message:\(error.message)")
+            }
+        }
+    }
 }
 
 // MARK: 直播间管理
 extension LNRoomManager {
     func createRoom(roomName: String, cover: String, handler: @escaping (String?) -> Void) {
         showLoading()
-        leaveRoom() // 自动退出上一个直播间
-        closeRoom() // 关闭之前的直播间
         
-        LNHttpManager.shared.createRoom { [weak self] id, err in
-            if let err {
-                dismissLoading()
-                showToast(err.errorDesc)
+        DeviceStore.shared.openLocalMicrophone { result in
+            if case .failure(let errorInfo) = result {
+                showToast(errorInfo.message)
                 handler(nil)
                 return
             }
-            guard let id else {
-                handler(nil)
-                dismissLoading()
-                return
-            }
-            
-            guard let self else { return }
-            DeviceStore.shared.openLocalMicrophone(completion: nil)
+            //        LNHttpManager.shared.createRoom { [weak self] id, err in
+            //            if let err {
+            //                dismissLoading()
+            //                showToast(err.errorDesc)
+            //                handler(nil)
+            //                return
+            //            }
+            //            guard let id else {
+            //                handler(nil)
+            //                dismissLoading()
+            //                return
+            //            }
+            //
+            //            guard let self else { return }
             
             var liveInfo = LiveInfo()
-            liveInfo.liveID = id
+            liveInfo.liveID = myUid
             liveInfo.liveName = roomName
             liveInfo.coverURL = cover
             liveInfo.seatTemplate = .audioSalon(seatCount: Self.MicNum)
             liveInfo.seatMode = .apply
-            liveListStore.createLive(liveInfo) { [weak self] result in
+            self.liveListStore.createLive(liveInfo) { result in
                 dismissLoading()
-                guard let self = self else { return }
                 switch result {
-                case .success(let liveInfo):
-                    handler(id)
+                case .success:
+                    handler(myUid)
                 case .failure(let errorInfo):
                     handler(nil)
                     showToast(errorInfo.message)
                 }
             }
+            //        }
         }
     }
     
-    func closeRoom() {
-        liveListStore.endLive(completion: nil)
+    func closeRoom(handler: @escaping (Bool) -> Void) {
+        liveListStore.endLive { result in
+            switch result {
+            case .success:
+                handler(true)
+            case .failure(let errorInfo):
+                handler(false)
+                showToast(errorInfo.message)
+            }
+        }
+    }
+    
+    func leaveRoom(handler: @escaping (Bool) -> Void) {
+        liveListStore.leaveLive { result in
+            switch result {
+            case .success:
+                handler(true)
+            case .failure(let errorInfo):
+                handler(false)
+                showToast(errorInfo.message)
+            }
+        }
     }
     
-    func leaveRoom() {
-        liveListStore.leaveLive(completion: nil)
+    func fetchMyRoomInfo(handler: @escaping (LNRoomInfo?) -> Void) {
+        liveListStore.fetchLiveInfo(liveID: myUid) { result in
+            switch result {
+            case .success(let info):
+                let roomInfo = LNRoomInfo()
+                roomInfo.update(info)
+                handler(roomInfo)
+            case .failure(let errorInfo):
+                if errorInfo.code == 100004 { // 房间不存在,返回成功
+                    handler(LNRoomInfo())
+                } else {
+                    showToast(errorInfo.message)
+                }
+            }
+        }
+    }
+}
+
+extension LNRoomManager: LNAccountManagerNotify {
+    func onUserLogout() {
+        LoginStore.shared.logout(completion: nil)
     }
 }

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

@@ -8,18 +8,3 @@
 import Foundation
 import AutoCodable
 
-
-enum LNRoomMessageType: Int, Decodable {
-    case unknown = -1
-    case system = 0
-    case chat = 1
-}
-
-@AutoCodable
-class LNRoomMessageVO: Decodable {
-    var type: LNRoomMessageType = .unknown
-    var userNo: String = ""
-    var avatar: String = ""
-    var nickname: String = ""
-    var text: String = ""
-}

+ 0 - 7
Lanu/Views/Game/Skill/LNSkillCommentsView.swift

@@ -234,12 +234,5 @@ extension LNSkillCommentItemView {
             make.top.equalTo(commentLabel.snp.bottom).offset(4)
             make.bottom.equalToSuperview().offset(-18)
         }
-        
-        onTap { [weak self] in
-            guard let self else { return }
-            guard let comment else { return }
-            
-            pushToProfile(uid: comment.userNo)
-        }
     }
 }

+ 16 - 6
Lanu/Views/Game/Skill/LNSkillDetailViewController.swift

@@ -62,10 +62,8 @@ class LNSkillDetailViewController: LNViewController {
         
         if detail?.userNo.isMyUid == true {
             loadSkillInfo()
-        } else if let uid = detail?.userNo {
-            stayTimer = LNDelayTask.perform(delay: 3) {
-                LNGameMateManager.shared.triggerAutoReplayIfNeed(uid: uid)
-            }
+        } else {
+            triggerAutoReplay()
         }
     }
     
@@ -80,8 +78,18 @@ class LNSkillDetailViewController: LNViewController {
     }
 }
 
-extension LNSkillDetailViewController {
-    private func loadSkillInfo() {
+private extension LNSkillDetailViewController {
+    func triggerAutoReplay() {
+        guard let uid = detail?.userNo else { return }
+        if !uid.isMyUid {
+            stayTimer = LNDelayTask.perform(delay: 3) { [weak self] in
+                guard let self else { return }
+                LNGameMateManager.shared.triggerAutoReplayIfNeed(uid: uid)
+            }
+        }
+    }
+    
+    func loadSkillInfo() {
         LNGameMateManager.shared.getSkillDetail(skillId: skillId) { [weak self] info in
             guard let self else { return }
             guard let info else { return }
@@ -109,6 +117,8 @@ extension LNSkillDetailViewController {
             titleLabel.text = info.nickname
             followButton.isHidden = info.follow || info.userNo.isMyUid
             moreButton.isHidden = info.userNo.isMyUid
+            
+            triggerAutoReplay()
         }
     }
 }

+ 2 - 1
Lanu/Views/Profile/Feed/LNFeedCommentListPanel.swift

@@ -141,7 +141,8 @@ extension LNFeedCommentListPanel {
         container.onTap { [weak self] in
             guard let self else { return }
             guard let feedId else { return }
-            let panel = LNFeedCommentInputPanel()
+            let panel = LNCommonInputPanel()
+            panel.maxInput = LNFeedManager.feedCommentMaxInput
             panel.handler = { [weak self] comment in
                 guard let self else { return }
                 LNFeedManager.shared.sendFeedComment(id: feedId, content: comment) { [weak self] success in

+ 2 - 1
Lanu/Views/Profile/Feed/LNImageFeedDetailViewController.swift

@@ -88,7 +88,8 @@ extension LNImageFeedDetailViewController {
             return
         }
         
-        let panel = LNFeedCommentInputPanel()
+        let panel = LNCommonInputPanel()
+        panel.maxInput = LNFeedManager.feedCommentMaxInput
         panel.handler = { [weak self] comment in
             guard let self else { return }
             LNFeedManager.shared.sendFeedComment(id: curDetail.id, content: comment) { [weak self] success in

+ 2 - 1
Lanu/Views/Profile/Feed/LNProfileFeedItemCell.swift

@@ -165,7 +165,8 @@ class LNProfileFeedItemCell: UITableViewCell {
 extension LNProfileFeedItemCell {
     private func toComment() {
         guard let curItem else { return }
-        let panel = LNFeedCommentInputPanel()
+        let panel = LNCommonInputPanel()
+        panel.maxInput = LNFeedManager.feedCommentMaxInput
         panel.handler = { comment in
             LNFeedManager.shared.sendFeedComment(id: curItem.id, content: comment)
             { success in

+ 3 - 14
Lanu/Views/Profile/Feed/LNVideoFeedDetailViewController.swift

@@ -68,7 +68,8 @@ class LNVideoFeedDetailViewController: LNViewController {
 extension LNVideoFeedDetailViewController {
     private func toComment() {
         guard let curDetail else { return }
-        let panel = LNFeedCommentInputPanel()
+        let panel = LNCommonInputPanel()
+        panel.maxInput = LNFeedManager.feedCommentMaxInput
         panel.handler = { comment in
             LNFeedManager.shared.sendFeedComment(id: curDetail.id, content: comment)
             { success in
@@ -266,19 +267,7 @@ extension LNVideoFeedDetailViewController {
         container.layer.cornerRadius = 19
         container.onTap { [weak self] in
             guard let self else { return }
-            guard let curDetail else { return }
-            let panel = LNFeedCommentInputPanel()
-            panel.handler = { [weak self] comment in
-                guard let self else { return }
-                LNFeedManager.shared.sendFeedComment(id: curDetail.id, content: comment)
-                { [weak self] success in
-                    guard let self else { return }
-                    guard success else { return }
-                    curDetail.commentCount += 1
-                    LNFeedManager.shared.notifyFeedCommentChanged(id: curDetail.id, count: curDetail.commentCount)
-                }
-            }
-            panel.popup()
+            toComment()
         }
         container.snp.makeConstraints { make in
             make.height.equalTo(38)

+ 0 - 0
Lanu/Views/Profile/Edit/LNCommonAlertView+Profile.swift → Lanu/Views/Profile/LNCommonAlertView+Profile.swift


+ 0 - 0
Lanu/Views/Profile/Relation/LNCommonAlertView+Relation.swift → Lanu/Views/Profile/LNCommonAlertView+Relation.swift


+ 24 - 18
Lanu/Views/Profile/Profile/LNProfileViewController.swift

@@ -88,12 +88,7 @@ class LNProfileViewController: LNViewController {
     override func viewDidAppear(_ animated: Bool) {
         super.viewDidAppear(animated)
         
-        if !uid.isMyUid {
-            stayTimer = LNDelayTask.perform(delay: 3) { [weak self] in
-                guard let self else { return }
-                LNGameMateManager.shared.triggerAutoReplayIfNeed(uid: uid)
-            }
-        }
+        triggerAutoReplay()
     }
     
     required init?(coder: NSCoder) {
@@ -129,8 +124,19 @@ extension LNProfileViewController: LNProfileManagerNotify, LNRelationManagerNoti
     }
 }
 
-extension LNProfileViewController {
-    private func updateContent() {
+private extension LNProfileViewController {
+    func triggerAutoReplay() {
+        if !uid.isMyUid {
+            stayTimer = LNDelayTask.perform(delay: 3) { [weak self] in
+                guard let self else { return }
+                LNGameMateManager.shared.triggerAutoReplayIfNeed(uid: uid)
+            }
+        }
+    }
+}
+
+private extension LNProfileViewController {
+    func updateContent() {
         guard let detail else { return }
         hasFollow = detail.follow
         
@@ -148,7 +154,7 @@ extension LNProfileViewController {
         scoreView.update(detail)
     }
     
-    private func updateProgress(_ progress: Double) {
+    func updateProgress(_ progress: Double) {
         navigationBarColor = .white.withAlphaComponent(progress)
         
         avatar.alpha = progress
@@ -169,7 +175,7 @@ extension LNProfileViewController {
         editLabel.isHidden = progress > 0.35
     }
     
-    private func showMoreMenu() {
+    func showMoreMenu() {
         let panel = LNBottomSheetMenu()
         var menu: [String] = [
             .init(key: "A00043"),
@@ -208,7 +214,7 @@ extension LNProfileViewController {
         panel.popup()
     }
     
-    private func setupViews() {
+    func setupViews() {
         setupNavBar()
         
         scrollView.contentInsetAdjustmentBehavior = .never
@@ -280,7 +286,7 @@ extension LNProfileViewController {
         infoView.outerScrollView = scrollView
     }
     
-    private func buildCover() -> UIView {
+    func buildCover() -> UIView {
         let coverGradient = CAGradientLayer()
         coverGradient.colors = [UIColor.black.withAlphaComponent(0).cgColor, UIColor.black.cgColor]
         coverGradient.locations = [0, 1]
@@ -313,22 +319,22 @@ extension LNProfileViewController {
         return cover
     }
     
-    private func buildUserInfoView() -> UIView {
+    func buildUserInfoView() -> UIView {
         
         return userInfoView
     }
     
-    private func buildExtraInfoView() -> UIView {
+    func buildExtraInfoView() -> UIView {
         infoView.outerScrollView = scrollView
         
         return infoView
     }
     
-    private func buildBottomMenu() -> UIView {
+    func buildBottomMenu() -> UIView {
         return bottomMenu
     }
     
-    private func setupNavBar() {
+    func setupNavBar() {
         navigationBarColor = .clear
         
         let menu = buildButtonMenu()
@@ -366,7 +372,7 @@ extension LNProfileViewController {
         updateProgress(0.0)
     }
     
-    private func buildButtonMenu() -> UIView {
+    func buildButtonMenu() -> UIView {
         let stackView = UIStackView()
         stackView.axis = .horizontal
         stackView.spacing = 14
@@ -401,7 +407,7 @@ extension LNProfileViewController {
         return stackView
     }
     
-    private func buildEditButton() -> UIView {
+    func buildEditButton() -> UIView {
         editButton.isHidden = true
         editButton.layer.cornerRadius = 16
         editButton.addAction(UIAction(handler: { [weak self] _ in

+ 61 - 0
Lanu/Views/Room/Bottom/Input/LNRoomMessageInputView.swift

@@ -0,0 +1,61 @@
+//
+//  LNRoomMessageInputView.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/11.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNRoomMessageInputView: UIView {
+    private weak var roomSession: LNRoomViewModel?
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setupViews()
+    }
+    
+    func update(_ room: LNRoomViewModel?) {
+        roomSession = room
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNRoomMessageInputView {
+    private func setupViews() {
+        onTap { [weak self] in
+            guard let self else { return }
+            let panel = LNCommonInputPanel()
+            panel.handler = { [weak self] message in
+                guard let self else { return }
+                guard let roomSession else { return }
+                roomSession.sendMessage(text: message) { success in }
+            }
+            panel.popup()
+        }
+        
+        backgroundColor = .fill.withAlphaComponent(0.15)
+        layer.cornerRadius = 15
+        snp.makeConstraints { make in
+            make.width.equalTo(140)
+            make.height.equalTo(30)
+        }
+        
+        let titleLabel = UILabel()
+        titleLabel.text = .init(key: "A00325")
+        titleLabel.font = .body_s
+        titleLabel.textColor = .text_1
+        addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(10)
+            make.centerY.equalToSuperview()
+        }
+    }
+}

+ 174 - 0
Lanu/Views/Room/Bottom/Join/ApplyList/LNRoomSeatApplyListCell.swift

@@ -0,0 +1,174 @@
+//
+//  LNRoomSeatApplyListCell.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/13.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNRoomSeatApplyListCell: UITableViewCell {
+    private let indexLabel = UILabel()
+    private let avatarView = UIImageView()
+    private let nameLabel = UILabel()
+    private let genderView = UIImageView()
+    private let timeLabel = UILabel()
+    private let acceptButton = UIButton()
+    
+    private weak var roomSession: LNRoomViewModel?
+    private var curItem: LNRoomSeatApplyItem?
+    
+    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+        super.init(style: style, reuseIdentifier: reuseIdentifier)
+        
+        setupViews()
+    }
+    
+    func update(room: LNRoomViewModel?, item: LNRoomSeatApplyItem, index: Int) {
+        indexLabel.text = "\(index)"
+        avatarView.showAvatar(item.avatar)
+        nameLabel.text = item.name
+        timeLabel.text = item.relativeTimeText
+        
+        genderView.image = switch item.gender {
+        case .unknow: nil
+        case .male: .icGenderMale
+        case .female: .icGenderFemale
+        }
+        genderView.isHidden = item.gender == .unknow
+        
+        roomSession = room
+        curItem = item
+        
+        updateAcceptButton()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+private extension LNRoomSeatApplyListCell {
+    func updateAcceptButton() {
+        if curItem?.hasAccept == true {
+            acceptButton.setBackgroundImage(nil, for: .normal)
+            acceptButton.setTitleColor(.fill_7, for: .normal)
+            acceptButton.titleLabel?.font = .body_xs
+        } else {
+            acceptButton.setBackgroundImage(.primary_8, for: .normal)
+            acceptButton.setTitleColor(.text_1, for: .normal)
+            acceptButton.titleLabel?.font = .heading_h5
+        }
+    }
+    
+    func setupViews() {
+        backgroundColor = .clear
+        contentView.backgroundColor = .clear
+        selectionStyle = .none
+        
+        contentView.snp.makeConstraints { make in
+            make.height.equalTo(63)
+        }
+        
+        indexLabel.font = .heading_h3
+        indexLabel.textColor = UIColor.text_2.withAlphaComponent(0.7)
+        indexLabel.textAlignment = .center
+        contentView.addSubview(indexLabel)
+        indexLabel.snp.makeConstraints { make in
+            make.leading.equalToSuperview()
+            make.centerY.equalToSuperview()
+            make.width.equalTo(10)
+        }
+        
+        avatarView.layer.cornerRadius = 16
+        avatarView.layer.borderWidth = 0.5
+        avatarView.layer.borderColor = UIColor.fill.cgColor
+        avatarView.clipsToBounds = true
+        avatarView.contentMode = .scaleAspectFill
+        contentView.addSubview(avatarView)
+        avatarView.snp.makeConstraints { make in
+            make.leading.equalTo(indexLabel.snp.trailing).offset(12)
+            make.centerY.equalToSuperview()
+            make.width.height.equalTo(32)
+        }
+        
+        acceptButton.backgroundColor = .white.withAlphaComponent(0.2)
+        acceptButton.setBackgroundImage(.primary_8, for: .normal)
+        acceptButton.setTitle(.init(key: "A00083"), for: .normal)
+        acceptButton.setTitleColor(.text_1, for: .normal)
+        acceptButton.titleLabel?.font = .heading_h5
+        acceptButton.layer.cornerRadius = 11
+        acceptButton.clipsToBounds = true
+        acceptButton.contentEdgeInsets = .init(top: 0, left: 7, bottom: 0, right: 7)
+        acceptButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            guard let curItem else { return }
+            roomSession?.acceptSeatApply(uid: curItem.userNo) { [weak self] success in
+                guard let self else { return }
+                guard success else { return }
+                curItem.hasAccept = true
+                updateAcceptButton()
+            }
+        }), for: .touchUpInside)
+        contentView.addSubview(acceptButton)
+        acceptButton.snp.makeConstraints { make in
+            make.trailing.equalToSuperview()
+            make.centerY.equalToSuperview()
+            make.width.greaterThanOrEqualTo(56)
+            make.height.equalTo(22)
+        }
+        
+        let infoView = UIView()
+        contentView.addSubview(infoView)
+        infoView.snp.makeConstraints { make in
+            make.leading.equalTo(avatarView.snp.trailing).offset(12)
+            make.trailing.lessThanOrEqualTo(acceptButton.snp.leading).offset(-12)
+            make.centerY.equalToSuperview()
+        }
+        
+        let nameRow = UIView()
+        infoView.addSubview(nameRow)
+        nameRow.snp.makeConstraints { make in
+            make.top.leading.trailing.equalToSuperview()
+        }
+        
+        nameLabel.font = .heading_h4
+        nameLabel.textColor = .text_1
+        nameRow.addSubview(nameLabel)
+        nameLabel.snp.makeConstraints { make in
+            make.leading.top.bottom.equalToSuperview()
+        }
+        
+        genderView.contentMode = .scaleAspectFit
+        nameRow.addSubview(genderView)
+        genderView.snp.makeConstraints { make in
+            make.leading.equalTo(nameLabel.snp.trailing).offset(2)
+            make.centerY.equalTo(nameLabel)
+            make.trailing.lessThanOrEqualToSuperview()
+            make.width.height.equalTo(14)
+        }
+        
+        timeLabel.font = .body_s
+        timeLabel.textColor = .text_2
+        infoView.addSubview(timeLabel)
+        timeLabel.snp.makeConstraints { make in
+            make.leading.equalToSuperview()
+            make.top.equalTo(nameRow.snp.bottom).offset(4)
+            make.bottom.equalToSuperview()
+            make.trailing.lessThanOrEqualToSuperview()
+        }
+        
+        let separator = UIView()
+        separator.backgroundColor = .fill.withAlphaComponent(0.08)
+        contentView.addSubview(separator)
+        separator.snp.makeConstraints { make in
+            make.leading.equalTo(avatarView.snp.leading)
+            make.trailing.equalToSuperview()
+            make.bottom.equalToSuperview()
+            make.height.equalTo(0.5)
+        }
+    }
+}

+ 248 - 0
Lanu/Views/Room/Bottom/Join/ApplyList/LNRoomSeatApplyListPanel.swift

@@ -0,0 +1,248 @@
+//
+//  LNRoomSeatApplyListPanel.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/13.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+enum LNRoomSeatApplyTabType: Int, CaseIterable {
+    case guest
+    case playmate
+}
+
+
+class LNRoomSeatApplyListPanel: LNPopupView {
+    private let tabBackgroundView = UIView()
+    private let guestTabButton = UIButton()
+    private let playmateTabButton = UIButton()
+    private let tabSelectionView = UIView()
+    
+    private let countLabel = UILabel()
+    private let countDescLabel = UILabel()
+    private let filterButton = UIButton()
+    
+    private let tableView = UITableView(frame: .zero, style: .plain)
+    
+    private var guestItems: [LNRoomSeatApplyItem] = []
+    private var playmateItems: [LNRoomSeatApplyItem] = []
+    private var playmateCategoryTitle: String = "所有品类"
+    
+    private weak var roomSession: LNRoomViewModel?
+    
+    private var curTab: LNRoomSeatApplyTabType = .guest {
+        didSet {
+            guard oldValue != curTab else { return }
+            reloadData()
+        }
+    }
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        containerHeight = .percent(0.62)
+        
+        setupViews()
+    }
+    
+    func update(_ room: LNRoomViewModel?) {
+        roomSession = room
+        
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNRoomSeatApplyListPanel: UITableViewDataSource, UITableViewDelegate {
+    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+        currentItems.count
+    }
+    
+    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+        let cell = tableView.dequeueReusableCell(
+            withIdentifier: LNRoomSeatApplyListCell.className,
+            for: indexPath
+        ) as! LNRoomSeatApplyListCell
+        let item = currentItems[indexPath.row]
+        cell.update(room: roomSession, item: item, index: indexPath.row + 1)
+        return cell
+    }
+    
+    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
+        64
+    }
+}
+
+extension LNRoomSeatApplyListPanel {
+    private var currentItems: [LNRoomSeatApplyItem] {
+        switch curTab {
+        case .guest: guestItems
+        case .playmate: playmateItems
+        }
+    }
+    
+    private func reloadData() {
+        updateTabs()
+        countLabel.text = .init(key: "A00334", currentItems.count)
+        filterButton.isHidden = curTab != .playmate
+        filterButton.setTitle(playmateCategoryTitle, for: .normal)
+        tableView.reloadData()
+    }
+    
+    private func updateTabs() {
+        let normalColor = UIColor.text_2
+        let selectedColor = UIColor.text_5
+        let selectedButton = curTab == .guest ? guestTabButton : playmateTabButton
+        
+        guestTabButton.setTitleColor(curTab == .guest ? selectedColor : normalColor, for: .normal)
+        playmateTabButton.setTitleColor(curTab == .playmate ? selectedColor : normalColor, for: .normal)
+        
+        tabSelectionView.snp.remakeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalTo(selectedButton.snp.leading).offset(3)
+            make.trailing.equalTo(selectedButton.snp.trailing).offset(-3)
+            make.height.equalTo(26)
+        }
+        
+        UIView.animate(withDuration: 0.25) { [weak self] in
+            self?.tabBackgroundView.layoutIfNeeded()
+        }
+    }
+    
+    private func setupViews() {
+        container.backgroundColor = .fill_7
+        
+        let tabView = buildTabView()
+        container.addSubview(tabView)
+        tabView.snp.makeConstraints { make in
+            make.top.equalToSuperview().offset(16)
+            make.horizontalEdges.equalToSuperview().inset(33)
+            make.height.equalTo(32)
+        }
+        
+        let countView = buildCountView()
+        container.addSubview(countView)
+        countView.snp.makeConstraints { make in
+            make.top.equalTo(tabView.snp.bottom).offset(16)
+            make.horizontalEdges.equalToSuperview().inset(16)
+            make.height.equalTo(30)
+        }
+        
+        let clearButton = UIButton()
+        clearButton.layer.cornerRadius = 23.5
+        clearButton.layer.borderColor = .fill_7
+        clearButton.layer.borderWidth = 1
+        clearButton.setTitle(.init(key: "A00335"), for: .normal)
+        clearButton.setTitleColor(.text_2, for: .normal)
+        clearButton.titleLabel?.font = .heading_h3
+        clearButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            
+        }), for: .touchUpInside)
+        container.addSubview(clearButton)
+        clearButton.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(16)
+            make.bottom.equalToSuperview().offset(commonBottomInset)
+            make.height.equalTo(47)
+        }
+        
+        tableView.backgroundColor = .clear
+        tableView.separatorStyle = .none
+        tableView.showsVerticalScrollIndicator = false
+        tableView.showsHorizontalScrollIndicator = false
+        tableView.rowHeight = 64
+        tableView.dataSource = self
+        tableView.delegate = self
+        tableView.register(LNRoomSeatApplyListCell.self, forCellReuseIdentifier: LNRoomSeatApplyListCell.className)
+        container.addSubview(tableView)
+        tableView.snp.makeConstraints { make in
+            make.top.equalTo(countView.snp.bottom).offset(8)
+            make.horizontalEdges.equalToSuperview().inset(16)
+            make.bottom.equalTo(clearButton.snp.top)
+        }
+    }
+    
+    private func buildTabView() -> UIView {
+        tabBackgroundView.backgroundColor = .fill.withAlphaComponent(0.2)
+        tabBackgroundView.layer.cornerRadius = 16
+        tabBackgroundView.clipsToBounds = true
+        
+        tabSelectionView.backgroundColor = .primary_1
+        tabSelectionView.layer.cornerRadius = 13
+        tabSelectionView.isUserInteractionEnabled = false
+        tabBackgroundView.addSubview(tabSelectionView)
+        
+        let stackView = UIStackView()
+        stackView.axis = .horizontal
+        stackView.distribution = .fillEqually
+        tabBackgroundView.addSubview(stackView)
+        stackView.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+        }
+        
+        guestTabButton.setTitle(.init(key: "A00331"), for: .normal)
+        guestTabButton.titleLabel?.font = .heading_h4
+        guestTabButton.addAction(UIAction(handler: { [weak self] _ in
+            self?.curTab = .guest
+        }), for: .touchUpInside)
+        stackView.addArrangedSubview(guestTabButton)
+        
+        playmateTabButton.setTitle(.init(key: "A00332"), for: .normal)
+        playmateTabButton.titleLabel?.font = .heading_h4
+        playmateTabButton.addAction(UIAction(handler: { [weak self] _ in
+            self?.curTab = .playmate
+        }), for: .touchUpInside)
+        stackView.addArrangedSubview(playmateTabButton)
+        
+        return tabBackgroundView
+    }
+    
+    private func buildCountView() -> UIView {
+        let container = UIView()
+        
+        let stackView = UIStackView()
+        stackView.axis = .horizontal
+        stackView.spacing = 5
+        stackView.alignment = .center
+        container.addSubview(stackView)
+        stackView.snp.makeConstraints { make in
+            make.leading.centerY.equalToSuperview()
+        }
+        
+        countLabel.font = .body_m
+        countLabel.textColor = .text_6
+        stackView.addArrangedSubview(countLabel)
+        
+        countDescLabel.font = .body_m
+        countDescLabel.textColor = .text_1
+        countDescLabel.text = .init(key: "A00333")
+        stackView.addArrangedSubview(countDescLabel)
+        
+        filterButton.backgroundColor = .fill.withAlphaComponent(0.15)
+        filterButton.layer.cornerRadius = 15
+        filterButton.clipsToBounds = true
+        filterButton.titleLabel?.font = .body_s
+        filterButton.setTitleColor(.text_1, for: .normal)
+        filterButton.setImage(.init(systemName: "chevron.down"), for: .normal)
+        filterButton.tintColor = .text_1
+        filterButton.semanticContentAttribute = .forceRightToLeft
+        filterButton.imageEdgeInsets = .init(top: 0, left: 4, bottom: 0, right: -4)
+        filterButton.contentEdgeInsets = .init(top: 0, left: 12, bottom: 0, right: 12)
+        filterButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+        }), for: .touchUpInside)
+        container.addSubview(filterButton)
+        filterButton.snp.makeConstraints { make in
+            make.centerY.trailing.equalToSuperview()
+            make.height.equalTo(30)
+        }
+        
+        return container
+    }
+}

+ 107 - 0
Lanu/Views/Room/Bottom/Join/LNRoomApplySeatPanel.swift

@@ -0,0 +1,107 @@
+//
+//  LNRoomApplySeatPanel.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/11.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNRoomApplySeatPanel: LNPopupView {
+    private let titleLabel = UILabel()
+    private let avatarView = UIImageView()
+    private let nameLabel = UILabel()
+    private let applyButton = UIButton()
+    
+    private weak var roomSession: LNRoomViewModel?
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setupViews()
+    }
+    
+    func update(_ room: LNRoomViewModel?) {
+        roomSession = room
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNRoomApplySeatPanel {
+    private func setupViews() {
+        container.backgroundColor = .fill_7
+        
+        let header = buildHeader()
+        container.addSubview(header)
+        header.snp.makeConstraints { make in
+            make.top.horizontalEdges.equalToSuperview()
+            make.height.equalTo(52)
+        }
+        
+        avatarView.sd_setImage(with: URL(string: myUserInfo.avatar))
+        avatarView.clipsToBounds = true
+        avatarView.contentMode = .scaleAspectFill
+        avatarView.layer.cornerRadius = 38
+        avatarView.layer.borderWidth = 1
+        avatarView.layer.borderColor = .fill
+        container.addSubview(avatarView)
+        avatarView.snp.makeConstraints { make in
+            make.top.equalTo(header.snp.bottom).offset(20)
+            make.centerX.equalToSuperview()
+            make.width.height.equalTo(76)
+        }
+        
+        nameLabel.text = myUserInfo.nickname
+        nameLabel.font = .heading_h2
+        nameLabel.textColor = .text_1
+        nameLabel.textAlignment = .center
+        container.addSubview(nameLabel)
+        nameLabel.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalTo(avatarView.snp.bottom).offset(13)
+        }
+        
+        applyButton.setTitle(.init(key: "A00322"), for: .normal)
+        applyButton.setTitleColor(.text_1, for: .normal)
+        applyButton.titleLabel?.font = .heading_h3
+        applyButton.setBackgroundImage(.primary_8, for: .normal)
+        applyButton.layer.cornerRadius = 23.5
+        applyButton.clipsToBounds = true
+        applyButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            guard let roomSession else { return }
+            roomSession.applySeat { [weak self] success in
+                guard let self else { return }
+                guard success else { return }
+                dismiss()
+            }
+        }), for: .touchUpInside)
+        container.addSubview(applyButton)
+        applyButton.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(16)
+            make.top.equalTo(nameLabel.snp.bottom).offset(46)
+            make.height.equalTo(47)
+            make.bottom.equalToSuperview().offset(commonBottomInset)
+        }
+    }
+    
+    private func buildHeader() -> UIView {
+        let header = UIView()
+        
+        titleLabel.font = .heading_h3
+        titleLabel.textColor = .text_1
+        titleLabel.text = .init(key: "A00330")
+        header.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+        }
+        
+        return header
+    }
+}

+ 175 - 0
Lanu/Views/Room/Bottom/Join/LNRoomJoinMenuView.swift

@@ -0,0 +1,175 @@
+//
+//  LNRoomJoinMenuView.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/9.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+private enum LNRoomJoinMenuState {
+    case normal
+    case appling
+    case reviewing
+}
+
+
+class LNRoomJoinMenuView: UIView {
+    private let joinView = UIStackView()
+    private let bgImageView = UIImageView()
+    private let seatIc = UIImageView()
+    private let titleLabel = UILabel()
+    private let redCountView = UIView()
+    private let redCountLabel = UILabel()
+    
+    private var curState: LNRoomJoinMenuState = .normal {
+        didSet {
+            switch curState {
+            case .normal:
+                toBeOffMic()
+            case .appling:
+                toBePending()
+            case .reviewing:
+                toBeRequests()
+            }
+        }
+    }
+    
+    private weak var roomSession: LNRoomViewModel?
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setupViews()
+        toBeOffMic()
+        
+        LNEventDeliver.addObserver(self)
+    }
+    
+    func update(_ room: LNRoomViewModel?) {
+        roomSession = room
+        
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNRoomJoinMenuView: LNRoomViewModelNotify {
+    func onRoomSeatApplyChanged() {
+        guard let roomSession else { return }
+        let applies = roomSession.curSeatApplications
+        
+        if applies.contains(where: { $0.userNo == myUid }) {
+            curState = .appling
+        } else {
+            curState = .normal
+        }
+        
+        if applies.isEmpty {
+            redCountView.isHidden = true
+        } else {
+            redCountLabel.text = "\(applies.count)"
+        }
+    }
+}
+
+extension LNRoomJoinMenuView {
+    private func setupViews() {
+        backgroundColor = .fill.withAlphaComponent(0.15)
+        layer.cornerRadius = 15
+        onTap { [weak self] in
+            guard let self else { return }
+            guard let roomSession else { return }
+            switch curState {
+            case .normal:
+                let panel = LNRoomApplySeatPanel()
+                panel.update(roomSession)
+                panel.popup()
+            case .appling:
+                break
+            case .reviewing:
+                let panel = LNRoomSeatApplyListPanel()
+                panel.update(roomSession)
+                panel.popup()
+                break
+            }
+        }
+        snp.makeConstraints { make in
+            make.width.greaterThanOrEqualTo(69)
+        }
+        
+        bgImageView.image = .primary_8
+        bgImageView.layer.cornerRadius = 15
+        bgImageView.clipsToBounds = true
+        addSubview(bgImageView)
+        bgImageView.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+        }
+        
+        joinView.isUserInteractionEnabled = false
+        joinView.spacing = 4
+        addSubview(joinView)
+        joinView.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.leading.greaterThanOrEqualToSuperview().offset(12)
+            make.centerY.equalToSuperview()
+        }
+        
+        seatIc.image = .icSeat
+        joinView.addArrangedSubview(seatIc)
+        seatIc.snp.makeConstraints { make in
+            make.width.height.equalTo(16)
+        }
+        
+        titleLabel.font = .heading_h5
+        titleLabel.textColor = .text_1
+        joinView.addArrangedSubview(titleLabel)
+        
+        redCountView.backgroundColor = .fill_6
+        redCountView.layer.cornerRadius = 7
+        addSubview(redCountView)
+        redCountView.snp.makeConstraints { make in
+            make.trailing.equalToSuperview()
+            make.centerY.equalTo(snp.top)
+            make.width.greaterThanOrEqualTo(15)
+        }
+        
+        redCountLabel.text = "9"
+        redCountLabel.font = .body_xs
+        redCountLabel.textColor = .text_1
+        redCountView.addSubview(redCountLabel)
+        redCountLabel.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(4)
+            make.verticalEdges.equalToSuperview()
+        }
+    }
+    
+    private func toBeOffMic() {
+        redCountView.isHidden = true
+        
+        bgImageView.isHidden = false
+        seatIc.isHidden = false
+        titleLabel.text = .init(key: "A00322")
+    }
+    
+    private func toBePending() {
+        redCountView.isHidden = false
+        
+        bgImageView.isHidden = true
+        seatIc.isHidden = true
+        titleLabel.text = .init(key: "A00323")
+    }
+    
+    private func toBeRequests() {
+        redCountView.isHidden = false
+        
+        bgImageView.isHidden = true
+        seatIc.isHidden = true
+        titleLabel.text = .init(key: "A00324")
+    }
+}

+ 0 - 28
Lanu/Views/Room/Bottom/LNRoomApplySeatPanel.swift

@@ -1,28 +0,0 @@
-//
-//  LNRoomApplySeatPanel.swift
-//  Gami
-//
-//  Created by OneeChan on 2026/3/11.
-//
-
-import Foundation
-import UIKit
-
-
-class LNRoomApplySeatPanel: LNPopupView {
-    override init(frame: CGRect) {
-        super.init(frame: frame)
-        
-        setupViews()
-    }
-    
-    required init?(coder: NSCoder) {
-        fatalError("init(coder:) has not been implemented")
-    }
-}
-
-extension LNRoomApplySeatPanel {
-    private func setupViews() {
-        
-    }
-}

+ 10 - 19
Lanu/Views/Room/Bottom/LNRoomBottomMenuView.swift

@@ -11,7 +11,7 @@ import SnapKit
 
 
 class LNRoomBottomMenuView: UIView {
-    private let messageInput = UIView()
+    private let messageInput = LNRoomMessageInputView()
     
     private let micButton = UIButton()
     private let giftButton = UIButton()
@@ -26,9 +26,11 @@ class LNRoomBottomMenuView: UIView {
         setupViews()
     }
     
-    func update(_ room: LNRoomViewModel) {
+    func update(_ room: LNRoomViewModel?) {
         roomSession = room
         
+        joinButton.update(room)
+        messageInput.update(room)
     }
     
     required init?(coder: NSCoder) {
@@ -69,6 +71,12 @@ extension LNRoomBottomMenuView {
         stackView.addArrangedSubview(giftButton)
         
         menuButton.setImage(.icMoreWithBg, for: .normal)
+        menuButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            let panel = LNRoomSettingMenuPanel()
+            panel.update(roomSession)
+            panel.popup()
+        }), for: .touchUpInside)
         stackView.addArrangedSubview(menuButton)
         
         stackView.addArrangedSubview(joinButton)
@@ -77,23 +85,6 @@ extension LNRoomBottomMenuView {
     }
     
     private func buildMessageInput() -> UIView {
-        messageInput.backgroundColor = .fill.withAlphaComponent(0.15)
-        messageInput.layer.cornerRadius = 15
-        messageInput.snp.makeConstraints { make in
-            make.width.equalTo(140)
-            make.height.equalTo(30)
-        }
-        
-        let titleLabel = UILabel()
-        titleLabel.text = .init(key: "A00325")
-        titleLabel.font = .body_s
-        titleLabel.textColor = .text_1
-        messageInput.addSubview(titleLabel)
-        titleLabel.snp.makeConstraints { make in
-            make.horizontalEdges.equalToSuperview().inset(10)
-            make.centerY.equalToSuperview()
-        }
-        
         return messageInput
     }
 }

+ 0 - 93
Lanu/Views/Room/Bottom/LNRoomJoinMenuView.swift

@@ -1,93 +0,0 @@
-//
-//  LNRoomJoinMenuView.swift
-//  Gami
-//
-//  Created by OneeChan on 2026/3/9.
-//
-
-import Foundation
-import UIKit
-import SnapKit
-
-
-class LNRoomJoinMenuView: UIView {
-    private let button = UIButton()
-    private let redCountView = UIView()
-    private let redCountLabel = UILabel()
-    
-    private weak var roomSession: LNRoomViewModel?
-    
-    override init(frame: CGRect) {
-        super.init(frame: frame)
-        
-        setupViews()
-        toBeOffMic()
-    }
-    
-    func update(_ room: LNRoomViewModel) {
-        roomSession = room
-        
-    }
-    
-    required init?(coder: NSCoder) {
-        fatalError("init(coder:) has not been implemented")
-    }
-}
-
-extension LNRoomJoinMenuView {
-    private func setupViews() {
-        button.backgroundColor = .fill.withAlphaComponent(0.15)
-        button.setTitleColor(.text_1, for: .normal)
-        button.titleLabel?.font = .heading_h5
-        button.contentEdgeInsets = .init(top: 0, left: 12, bottom: 0, right: 12)
-        button.imageEdgeInsets = .init(top: 0, left: 0, bottom: 0, right: 4)
-        button.layer.cornerRadius = 15
-        button.clipsToBounds = true
-        addSubview(button)
-        button.snp.makeConstraints { make in
-            make.edges.equalToSuperview()
-        }
-        
-        redCountView.backgroundColor = .fill_6
-        redCountView.layer.cornerRadius = 7
-        addSubview(redCountView)
-        redCountView.snp.makeConstraints { make in
-            make.trailing.equalToSuperview()
-            make.centerY.equalTo(snp.top)
-            make.width.greaterThanOrEqualTo(15)
-        }
-        
-        redCountLabel.text = "9"
-        redCountLabel.font = .body_xs
-        redCountLabel.textColor = .text_1
-        redCountView.addSubview(redCountLabel)
-        redCountLabel.snp.makeConstraints { make in
-            make.horizontalEdges.equalToSuperview().inset(4)
-            make.verticalEdges.equalToSuperview()
-        }
-    }
-    
-    private func toBeOffMic() {
-        redCountView.isHidden = true
-        
-        button.setBackgroundImage(.primary_8, for: .normal)
-        button.setImage(.icSeat, for: .normal)
-        button.setTitle(.init(key: "A00322"), for: .normal)
-    }
-    
-    private func toBePending() {
-        redCountView.isHidden = false
-        
-        button.setBackgroundImage(nil, for: .normal)
-        button.setImage(nil, for: .normal)
-        button.setTitle(.init(key: "A00323"), for: .normal)
-    }
-    
-    private func toBeRequests() {
-        redCountView.isHidden = false
-        
-        button.setBackgroundImage(nil, for: .normal)
-        button.setImage(nil, for: .normal)
-        button.setTitle(.init(key: "A00324"), for: .normal)
-    }
-}

+ 33 - 23
Lanu/Views/Room/Create/LNCreateRoomPanel.swift

@@ -13,8 +13,25 @@ import SnapKit
 class LNCreateRoomPanel: LNPopupView {
     private let cover = LNImageUploadView()
     private let coverLabel = UILabel()
+    
     private let nameLabel = UILabel()
     private let nameCountLabel = UILabel()
+    private var curName: String = "" {
+        didSet {
+            if !curName.isEmpty {
+                nameLabel.textColor = .text_5
+                nameLabel.text = curName
+                nameCountLabel.text = "\(curName.count)/\(LNRoomManager.RoomNameMaxInput)"
+            } else {
+                nameLabel.textColor = .text_2
+                nameLabel.text = .init(key: "A00316")
+                nameCountLabel.text = "0/\(LNRoomManager.RoomNameMaxInput)"
+            }
+            
+            checkCreate()
+        }
+    }
+    
     private let muteSwitch = UISwitch()
     private let createButton = UIButton()
     
@@ -23,7 +40,12 @@ class LNCreateRoomPanel: LNPopupView {
         
         setupViews()
         
-        updateName(name: "")
+        LNRoomManager.shared.fetchMyRoomInfo { [weak self] roomInfo in
+            guard let self else { return }
+            guard let roomInfo else { return }
+            cover.loadImage(url: roomInfo.coverURL)
+            curName = roomInfo.liveName
+        }
     }
     
     required init?(coder: NSCoder) {
@@ -39,23 +61,8 @@ extension LNCreateRoomPanel: LNImageUploadViewDelegate {
 }
 
 extension LNCreateRoomPanel {
-    private func updateName(name: String?) {
-        if let name, !name.isEmpty {
-            nameLabel.textColor = .text_5
-            nameLabel.text = name
-            nameCountLabel.text = "\(name.count)/\(LNRoomManager.RoomNameMaxInput)"
-        } else {
-            nameLabel.textColor = .text_2
-            nameLabel.text = .init(key: "A00316")
-            nameCountLabel.text = "0/\(LNRoomManager.RoomNameMaxInput)"
-        }
-        
-        checkCreate()
-    }
-    
     private func checkCreate() {
-//        createButton.isEnabled = nameLabel.textColor != .text_2 && cover.imageUrl?.isEmpty == false
-        createButton.isEnabled = true
+        createButton.isEnabled = !curName.isEmpty && cover.imageUrl?.isEmpty == false
     }
     
     private func setupViews() {
@@ -89,8 +96,13 @@ extension LNCreateRoomPanel {
         createButton.titleEdgeInsets = .init(top: 0, left: 4, bottom: 0, right: 0)
         createButton.addAction(UIAction(handler: { [weak self] _ in
             guard let self else { return }
-            dismiss()
-            pushToRoom("")
+            LNRoomManager.shared.createRoom(roomName: curName, cover: cover.imageUrl ?? "")
+            { [weak self] id in
+                guard let self else { return }
+                guard let id else { return }
+                dismiss()
+                pushToRoom(id)
+            }
         }), for: .touchUpInside)
         stackView.addArrangedSubview(createButton)
         createButton.snp.makeConstraints { make in
@@ -204,11 +216,9 @@ extension LNCreateRoomPanel {
             let panel = LNRoomNameInputPanel()
             panel.handler = { [weak self] name in
                 guard let self else { return }
-                updateName(name: name)
-            }
-            if nameLabel.textColor == .text_5 {
-                panel.update(nameLabel.text)
+                curName = name
             }
+            panel.update(curName)
             panel.popup()
         }
         container.addSubview(holder)

+ 23 - 0
Lanu/Views/Room/LNCommonAlertView+Room.swift

@@ -0,0 +1,23 @@
+//
+//  LNCommonAlertView+Room.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/16.
+//
+
+import Foundation
+
+
+extension LNCommonAlertView {
+    static func showCloseRoomAlert(_ room: LNRoomViewModel) {
+        let alert = LNCommonAlertView()
+        alert.titleLabel.text = .init(key: "A00339")
+        alert.messageLabel.text = .init(key: "A00340")
+        alert.showConfirm(.init(key: "A00022")) { }
+        alert.showCancel(.init(key: "A00223")) {
+            room.closeRoom()
+        }
+        
+        alert.popup()
+    }
+}

+ 0 - 8
Lanu/Views/Room/LNRoomBottomMenuView.swift

@@ -1,8 +0,0 @@
-//
-//  LNRoomBottomMenuView.swift
-//  Gami
-//
-//  Created by OneeChan on 2026/3/9.
-//
-
-import Foundation

+ 0 - 28
Lanu/Views/Room/LNRoomMessageView.swift

@@ -1,28 +0,0 @@
-//
-//  LNRoomPublicBoardView.swift
-//  Gami
-//
-//  Created by OneeChan on 2026/3/9.
-//
-
-import Foundation
-import UIKit
-
-
-class LNRoomMessageView: UIView {
-    override init(frame: CGRect) {
-        super.init(frame: frame)
-        
-        setupViews()
-    }
-    
-    required init?(coder: NSCoder) {
-        fatalError("init(coder:) has not been implemented")
-    }
-}
-
-extension LNRoomMessageView {
-    private func setupViews() {
-        
-    }
-}

+ 114 - 0
Lanu/Views/Room/LNRoomSheetMenu.swift

@@ -0,0 +1,114 @@
+//
+//  LNRoomSheetMenu.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/16.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNRoomSheetMenu: LNBottomSheetMenu {
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        container.backgroundColor = .fill_7
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    static func buildMenuItem(title: String, handler: @escaping () -> Void) -> UIView {
+        let container = UIView()
+        container.onTap(handler)
+        
+        let titleLabel = UILabel()
+        titleLabel.font = .heading_h4
+        titleLabel.textColor = .text_1
+        titleLabel.text = title
+        container.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.verticalEdges.equalToSuperview().inset(15)
+        }
+        
+        return container
+    }
+    
+    func update(title: String?, views: [UIView]) {
+        let header = UIView()
+        if let title {
+            let titleLabel = UILabel()
+            titleLabel.font = .heading_h5
+            titleLabel.textColor = .text_2
+            titleLabel.text = title
+            header.addSubview(titleLabel)
+            titleLabel.snp.makeConstraints { make in
+                make.centerX.equalToSuperview()
+                make.top.equalToSuperview().offset(17)
+                make.bottom.equalToSuperview().offset(-5)
+            }
+        } else {
+            header.snp.makeConstraints { make in
+                make.height.equalTo(15)
+            }
+        }
+        
+        stackView.addArrangedSubview(header)
+        stackView.addArrangedSubview(buildMenus(views: views))
+        stackView.addArrangedSubview(buildSeperator())
+        stackView.addArrangedSubview(buildCancel())
+    }
+    
+    private func buildMenus(views: [UIView]) -> UIView {
+        let container = UIView()
+        
+        let stackView = UIStackView()
+        stackView.axis = .vertical
+        container.addSubview(stackView)
+        stackView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalToSuperview()
+            make.bottom.equalToSuperview().offset(-15)
+        }
+        
+        for view in views {
+            stackView.addArrangedSubview(view)
+        }
+        
+        return container
+    }
+    
+    private func buildSeperator() -> UIView {
+        let view = UIView()
+        view.backgroundColor = .black.withAlphaComponent(0.8)
+        view.snp.makeConstraints { make in
+            make.height.equalTo(3)
+        }
+        
+        return view
+    }
+    
+    private func buildCancel() -> UIView {
+        let container = UIView()
+        container.onTap { [weak self] in
+            guard let self else { return }
+            dismiss()
+        }
+        
+        let cancel = UILabel()
+        cancel.text = .init(key: "A00003")
+        cancel.textColor = .text_2
+        cancel.font = .heading_h4
+        container.addSubview(cancel)
+        cancel.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.verticalEdges.equalToSuperview().inset(20)
+        }
+        
+        return container
+    }
+}

+ 0 - 8
Lanu/Views/Room/LNRoomTopMenuView.swift

@@ -1,8 +0,0 @@
-//
-//  LNRoomTopMenuView.swift
-//  Gami
-//
-//  Created by OneeChan on 2026/3/9.
-//
-
-import Foundation

+ 10 - 1
Lanu/Views/Room/LNRoomViewController.swift

@@ -29,6 +29,8 @@ class LNRoomViewController: LNViewController {
     init(_ id: String) {
         viewModel = LNRoomViewModel(roomId: id)
         super.init(nibName: nil, bundle: nil)
+        
+        LNEventDeliver.addObserver(self)
     }
     
     @MainActor required init?(coder: NSCoder) {
@@ -42,6 +44,12 @@ class LNRoomViewController: LNViewController {
     }
 }
 
+extension LNRoomViewController: LNRoomViewModelNotify {
+    func onRoomClosed() {
+        navigationController?.popViewController(animated: true)
+    }
+}
+
 extension LNRoomViewController {
     private func setupViews() {
         enableDragBack = false
@@ -77,7 +85,8 @@ extension LNRoomViewController {
         messageView.update(viewModel)
         view.addSubview(messageView)
         messageView.snp.makeConstraints { make in
-            make.horizontalEdges.equalToSuperview()
+            make.leading.equalToSuperview().offset(10)
+            make.trailing.equalToSuperview().offset(-85)
             make.bottom.equalTo(bottomMenuView.snp.top).offset(-5)
             make.top.equalTo(seatsView.snp.bottom).offset(22)
         }

+ 6 - 4
Lanu/Views/Room/Message/LNRoomChatMessageCell.swift

@@ -21,8 +21,8 @@ class LNRoomChatMessageCell: UITableViewCell {
         setupViews()
     }
     
-    func update(_ message: LNRoomMessageVO) {
-        
+    func update(_ message: LNRoomMessageItem) {
+        contentLabel.text = message.text
     }
     
     required init?(coder: NSCoder) {
@@ -32,6 +32,8 @@ class LNRoomChatMessageCell: UITableViewCell {
 
 extension LNRoomChatMessageCell {
     private func setupViews() {
+        backgroundColor = .clear
+        
         avatarView.layer.cornerRadius = 13
         avatarView.layer.borderColor = .fill
         avatarView.layer.borderWidth = 0.5
@@ -45,11 +47,11 @@ extension LNRoomChatMessageCell {
         }
         
         let bodyView = UIView()
-        addSubview(bodyView)
+        contentView.addSubview(bodyView)
         bodyView.snp.makeConstraints { make in
             make.leading.equalTo(avatarView.snp.trailing).offset(10)
             make.verticalEdges.equalToSuperview()
-            make.trailing.equalToSuperview()
+            make.trailing.lessThanOrEqualToSuperview()
         }
         
         nameLabel.font = .body_xs

+ 18 - 8
Lanu/Views/Room/Message/LNRoomMessageView.swift

@@ -8,21 +8,23 @@
 import Foundation
 import UIKit
 import SnapKit
+import Combine
 
 
 class LNRoomMessageView: UIView {
     private let tableView = UITableView()
-    
     private weak var roomSession: LNRoomViewModel?
-    private var items: [LNRoomMessageVO] = []
+    
+    private var items: [LNRoomMessageItem] = []
     
     override init(frame: CGRect) {
         super.init(frame: frame)
         
         setupViews()
+        LNEventDeliver.addObserver(self)
     }
     
-    func update(_ room: LNRoomViewModel) {
+    func update(_ room: LNRoomViewModel?) {
         roomSession = room
     }
     
@@ -31,6 +33,14 @@ class LNRoomMessageView: UIView {
     }
 }
 
+extension LNRoomMessageView: LNRoomViewModelNotify {
+    func onRoomMessageChanged() {
+        items = roomSession?.curMessage ?? []
+        tableView.reloadData()
+        tableView.scrollToBottom()
+    }
+}
+
 extension LNRoomMessageView: UITableViewDataSource {
     func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
         items.count
@@ -40,15 +50,15 @@ extension LNRoomMessageView: UITableViewDataSource {
         let message = items[indexPath.row]
         
         switch message.type {
-        case .system:
-            let cell = tableView.dequeueReusableCell(withIdentifier: LNRoomSystemMessageCell.className, for: indexPath) as! LNRoomSystemMessageCell
-            cell.update(message)
-            return cell
         case .chat:
             let cell = tableView.dequeueReusableCell(withIdentifier: LNRoomChatMessageCell.className, for: indexPath) as! LNRoomChatMessageCell
             cell.update(message)
             return cell
-        case .unknown:
+        case .system:
+            let cell = tableView.dequeueReusableCell(withIdentifier: LNRoomSystemMessageCell.className, for: indexPath) as! LNRoomSystemMessageCell
+            cell.update(message)
+            return cell
+        default:
             break
         }
         

+ 1 - 1
Lanu/Views/Room/Message/LNRoomSystemMessageCell.swift

@@ -19,7 +19,7 @@ class LNRoomSystemMessageCell: UITableViewCell {
         setupViews()
     }
     
-    func update(_ message: LNRoomMessageVO) {
+    func update(_ message: LNRoomMessageItem) {
         
     }
     

+ 114 - 0
Lanu/Views/Room/Profile/LNRoomProfileBottomMenu.swift

@@ -0,0 +1,114 @@
+//
+//  LNRoomProfileBottomMenu.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/16.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNRoomProfileBottomMenu: UIView {
+    private let follow = UIButton()
+    private let chat = UIButton()
+    private let gift = UIButton()
+    
+    private var curUid: String?
+    private var isFollow: Bool = false {
+        didSet {
+            if isFollow {
+                follow.setTitle(.init(key: "A00026"), for: .normal)
+                follow.setTitleColor(.text_1.withAlphaComponent(0.2), for: .normal)
+                follow.titleLabel?.font = .body_xl
+            } else {
+                follow.setTitle(.init(key: "A00225"), for: .normal)
+                follow.setTitleColor(.text_1, for: .normal)
+                follow.titleLabel?.font = .body_xl
+            }
+        }
+    }
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        let stackView = UIStackView()
+        stackView.axis = .horizontal
+        stackView.alignment = .fill
+        stackView.distribution = .fillEqually
+        addSubview(stackView)
+        stackView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalToSuperview().offset(9)
+            make.height.equalTo(48)
+            make.bottom.equalToSuperview()
+        }
+        
+        follow.setTitle(.init(key: "A00225"), for: .normal)
+        follow.setTitleColor(.text_1, for: .normal)
+        follow.titleLabel?.font = .body_xl
+        follow.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            guard let curUid else { return }
+            if isFollow {
+                LNCommonAlertView.showUnfollowAlert(uid: curUid)
+            } else {
+                LNRelationManager.shared.operateFollow(uid: curUid, follow: true, handler: nil)
+            }
+        }), for: .touchUpInside)
+        stackView.addArrangedSubview(follow)
+        addSeperator(follow)
+        
+        chat.setTitle(.init(key: "A00042"), for: .normal)
+        chat.setTitleColor(.text_1, for: .normal)
+        chat.titleLabel?.font = .body_xl
+        chat.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            guard let curUid else { return }
+            pushToChat(uid: curUid)
+        }), for: .touchUpInside)
+        stackView.addArrangedSubview(chat)
+        addSeperator(chat)
+        
+        gift.setTitle(.init(key: "A00336"), for: .normal)
+        gift.setTitleColor(.text_6, for: .normal)
+        gift.titleLabel?.font = .body_xl
+        stackView.addArrangedSubview(gift)
+        
+        LNEventDeliver.addObserver(self)
+    }
+    
+    func update(_ uid: String) {
+        curUid = uid
+        
+        if uid.isMyUid {
+            isHidden = true
+            return
+        }
+        LNRelationManager.shared.getRelationWithUser(uid: uid, handler: nil)
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    private func addSeperator(_ view: UIView) {
+        let sep = UIView()
+        sep.backgroundColor = .text_3
+        view.addSubview(sep)
+        sep.snp.makeConstraints { make in
+            make.width.equalTo(1)
+            make.height.equalTo(12)
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview()
+        }
+    }
+}
+
+extension LNRoomProfileBottomMenu: LNRelationManagerNotify {
+    func onUserRelationChanged(uid: String, relation: LNUserRelationShip) {
+        guard uid == curUid else { return }
+        isFollow = relation.contains(.followed)
+    }
+}

+ 176 - 0
Lanu/Views/Room/Profile/LNRoomProfileCardPanel.swift

@@ -0,0 +1,176 @@
+//
+//  LNRoomProfileCardPanel.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/16.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNRoomProfileCardPanel: LNPopupView {
+    private let reportButton = UIButton(type: .system)
+    
+    private let avatarView = UIImageView()
+    
+    private let nameLabel = UILabel()
+    private let genderView = LNGenderView()
+    private let userIdLabel = UILabel()
+    
+    private let skillSection = LNRoomProfileSkillView()
+    private let actionsSection = LNRoomProfileBottomMenu()
+    
+    private var curDetail: LNUserProfileVO?
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setupViews()
+    }
+    
+    func load(_ uid: String) {
+        reportButton.isHidden = uid.isMyUid
+        skillSection.isHidden = true
+        actionsSection.update(uid)
+        
+        LNProfileManager.shared.getUserProfile(uid: uid) { [weak self] detail in
+            guard let self else { return }
+            guard let detail else {
+                dismiss()
+                return
+            }
+            
+            avatarView.sd_setImage(with: URL(string: detail.avatar))
+            nameLabel.text = detail.nickname
+            genderView.update(detail.gender, detail.age)
+            userIdLabel.text = "ID \(detail.userNo)"
+            
+            skillSection.isHidden = uid.isMyUid || detail.skills.isEmpty
+            skillSection.update(detail, detail.skills)
+            
+            curDetail = detail
+        }
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+private extension LNRoomProfileCardPanel {
+    func setupViews() {
+        container.backgroundColor = .fill_7
+        
+        let topMenu = buildTopMenu()
+        container.addSubview(topMenu)
+        topMenu.snp.makeConstraints { make in
+            make.top.equalToSuperview()
+            make.horizontalEdges.equalToSuperview()
+        }
+        
+        avatarView.layer.cornerRadius = 38
+        avatarView.layer.borderWidth = 1
+        avatarView.layer.borderColor = UIColor.fill.cgColor
+        avatarView.clipsToBounds = true
+        avatarView.contentMode = .scaleAspectFill
+        avatarView.backgroundColor = UIColor.fill.withAlphaComponent(0.12)
+        avatarView.onTap { [weak self] in
+            guard let self else { return }
+            guard let curDetail else { return }
+            dismiss()
+            pushToProfile(uid: curDetail.userNo)
+        }
+        container.addSubview(avatarView)
+        avatarView.snp.makeConstraints { make in
+            make.top.equalToSuperview().offset(-17)
+            make.centerX.equalToSuperview()
+            make.width.height.equalTo(76)
+        }
+        
+        let stackView = UIStackView()
+        stackView.axis = .vertical
+        stackView.distribution = .fill
+        container.addSubview(stackView)
+        stackView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalTo(avatarView.snp.bottom).offset(17)
+            make.bottom.equalToSuperview().offset(commonBottomInset)
+        }
+        
+        stackView.addArrangedSubview(buildUserInfo())
+        stackView.addArrangedSubview(buildSkillSection())
+        stackView.addArrangedSubview(buildActionSection())
+    }
+    
+    func buildTopMenu() -> UIView {
+        let container = UIView()
+        container.snp.makeConstraints { make in
+            make.height.equalTo(56)
+        }
+        
+        reportButton.tintColor = UIColor.text_2.withAlphaComponent(0.6)
+        reportButton.setImage(UIImage(systemName: "exclamationmark.triangle"), for: .normal)
+        reportButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            guard let curDetail else { return }
+            pushToReport(uid: curDetail.userNo)
+        }), for: .touchUpInside)
+        container.addSubview(reportButton)
+        reportButton.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(16)
+            make.top.equalToSuperview().offset(16)
+            make.width.height.equalTo(24)
+        }
+        
+        return container
+    }
+    
+    func buildUserInfo() -> UIView {
+        let container = UIView()
+        
+        let nameView = UIView()
+        container.addSubview(nameView)
+        nameView.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.leading.greaterThanOrEqualToSuperview().offset(16)
+            make.top.equalToSuperview().offset(3)
+        }
+        
+        nameLabel.font = .heading_h2
+        nameLabel.textColor = .text_1
+        nameView.addSubview(nameLabel)
+        nameLabel.snp.makeConstraints { make in
+            make.leading.equalToSuperview()
+            make.verticalEdges.equalToSuperview()
+        }
+        
+        nameView.addSubview(genderView)
+        genderView.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalTo(nameLabel.snp.trailing).offset(4)
+            make.trailing.equalToSuperview()
+        }
+        
+        userIdLabel.font = .body_s
+        userIdLabel.textColor = UIColor.text_1.withAlphaComponent(0.5)
+        userIdLabel.textAlignment = .center
+        container.addSubview(userIdLabel)
+        userIdLabel.snp.makeConstraints { make in
+            make.top.equalTo(nameView.snp.bottom).offset(4)
+            make.centerX.equalToSuperview()
+            make.bottom.equalToSuperview().offset(-10)
+        }
+        
+        return container
+    }
+    
+    func buildSkillSection() -> UIView {
+        return skillSection
+    }
+    
+    func buildActionSection() -> UIView {
+        return actionsSection
+    }
+}

+ 153 - 0
Lanu/Views/Room/Profile/LNRoomProfileSkillView.swift

@@ -0,0 +1,153 @@
+//
+//  LNRoomProfileSkillView.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/16.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNRoomProfileSkillView: UIView {
+    private let stackView = UIStackView()
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        let scrollView = UIScrollView()
+        scrollView.showsHorizontalScrollIndicator = false
+        scrollView.alwaysBounceHorizontal = true
+        scrollView.contentInset = .init(top: 0, left: 12, bottom: 0, right: 0)
+        addSubview(scrollView)
+        scrollView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.verticalEdges.equalToSuperview().inset(10)
+        }
+        
+        stackView.axis = .horizontal
+        stackView.spacing = 6
+        scrollView.addSubview(stackView)
+        stackView.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+            make.height.equalToSuperview()
+        }
+    }
+    
+    func update(_ userInfo: LNUserProfileVO, _ skills: [LNGameMateSkillVO]) {
+        for skill in skills {
+            let itemView = LNRoomProfileSkillItemView()
+            itemView.update(userInfo, skill)
+            stackView.addArrangedSubview(itemView)
+        }
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+
+private class LNRoomProfileSkillItemView: UIView {
+    private let iconView = UIImageView()
+    private let titleLabel = UILabel()
+    private let coinIconView = UIImageView.coinImageView()
+    private let priceLabel = UILabel()
+    
+    private var skill: LNGameMateSkillVO?
+    private var userInfo: LNUserProfileVO?
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setupViews()
+    }
+    
+    func update(_ user: LNUserProfileVO, _ item: LNGameMateSkillVO) {
+        titleLabel.text = item.name
+        priceLabel.text = item.price.toDisplay
+        iconView.sd_setImage(with: URL(string: item.icon))
+        
+        skill = item
+        userInfo = user
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+private extension LNRoomProfileSkillItemView {
+    func setupViews() {
+        backgroundColor = UIColor.fill.withAlphaComponent(0.1)
+        layer.cornerRadius = 12
+        clipsToBounds = true
+        onTap { [weak self] in
+            guard let self else { return }
+            guard let skill, let userInfo else { return }
+            
+            let panel = LNCreateOrderPanel()
+            panel.update(skill, user: userInfo)
+            panel.editable = true
+            panel.popup()
+        }
+        snp.makeConstraints { make in
+            make.width.equalTo(169)
+            make.height.equalTo(68)
+        }
+        
+        iconView.backgroundColor = UIColor.fill.withAlphaComponent(0.2)
+        iconView.layer.borderWidth = 0.5
+        iconView.layer.borderColor = UIColor.fill.withAlphaComponent(0.3).cgColor
+        iconView.layer.cornerRadius = 20
+        iconView.clipsToBounds = true
+        iconView.contentMode = .scaleAspectFill
+        addSubview(iconView)
+        iconView.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalToSuperview().offset(10)
+            make.height.width.equalTo(40)
+        }
+        
+        let infoView = UIView()
+        addSubview(infoView)
+        infoView.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalTo(iconView.snp.trailing).offset(5)
+            make.trailing.equalToSuperview().offset(-10)
+        }
+        
+        titleLabel.font = .heading_h4
+        titleLabel.textColor = .text_1
+        titleLabel.lineBreakMode = .byTruncatingTail
+        infoView.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalToSuperview()
+        }
+        
+        let priceView = UIView()
+        infoView.addSubview(priceView)
+        priceView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.bottom.equalToSuperview()
+            make.top.equalTo(titleLabel.snp.bottom).offset(4)
+        }
+        
+        priceView.addSubview(coinIconView)
+        coinIconView.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalToSuperview()
+        }
+        
+        priceLabel.font = .body_s
+        priceLabel.textColor = .text_2
+        priceView.addSubview(priceLabel)
+        priceLabel.snp.makeConstraints { make in
+            make.leading.equalTo(coinIconView.snp.trailing).offset(4)
+            make.verticalEdges.equalToSuperview()
+            make.trailing.lessThanOrEqualToSuperview().offset(-10)
+        }
+    }
+}

+ 46 - 2
Lanu/Views/Room/Seats/LNRoomGuestSeatView.swift → Lanu/Views/Room/Seats/Guest/LNRoomGuestSeatView.swift

@@ -10,19 +10,36 @@ import UIKit
 import SnapKit
 
 
-class LNRoomGuestSeatView: UIView {
-    private let seatNum: Int = 1002
+class LNRoomGuestSeatView: UIView, LNRoomSeatViewProtocol {
+    private let seatNum: Int = 1
     private let nameLabel = UILabel()
     private let emptyIc = UIImageView()
     
     private let userView = UIView()
     private let userAvatar = UIImageView()
     private let muteIc = UIImageView()
+    private let speakingView = LNRoomSeatSpeakingView()
+    
+    private weak var roomSession: LNRoomViewModel?
+    private var curSeat: LNRoomSeatItem? {
+        roomSession?.seatsInfo.first { $0.index == seatNum }
+    }
+    private var isSpeaking: Bool {
+        roomSession?.speakingUser.contains(curSeat?.uid ?? "") == true
+    }
     
     override init(frame: CGRect) {
         super.init(frame: frame)
         
         setupViews()
+        
+        LNEventDeliver.addObserver(self)
+    }
+    
+    func update(_ room: LNRoomViewModel?) {
+        roomSession = room
+        onRoomSeatsChanged()
+        speakingView.update(seatNum, room: room)
     }
     
     required init?(coder: NSCoder) {
@@ -30,8 +47,30 @@ class LNRoomGuestSeatView: UIView {
     }
 }
 
+extension LNRoomGuestSeatView: LNRoomViewModelNotify {
+    func onRoomSeatsChanged() {
+        if let curSeat {
+            muteIc.isHidden = !curSeat.isMute
+            userView.isHidden = false
+            userAvatar.sd_setImage(with: URL(string: curSeat.avatar))
+            nameLabel.text = curSeat.nickname.isEmpty ? " " : curSeat.nickname
+        } else {
+            muteIc.isHidden = true
+            userView.isHidden = true
+            nameLabel.text = .init(key: "A00328")
+        }
+    }
+}
+
 extension LNRoomGuestSeatView {
     private func setupViews() {
+        onTap { [weak self] in
+            guard let self else { return }
+            guard let roomSession,
+                  let curSeat else { return }
+            handlerClick(roomSession, curSeat)
+        }
+        
         snp.makeConstraints { make in
             make.width.equalTo(76)
             make.height.equalTo(68)
@@ -72,6 +111,11 @@ extension LNRoomGuestSeatView {
             make.width.height.equalTo(46)
         }
         
+        userView.addSubview(speakingView)
+        speakingView.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+        }
+        
         let gradientBg = UIImageView()
         gradientBg.layer.cornerRadius = 23
         gradientBg.image = .primary_7

+ 47 - 2
Lanu/Views/Room/Seats/LNRoomHostSeatView.swift → Lanu/Views/Room/Seats/Host/LNRoomHostSeatView.swift

@@ -10,19 +10,36 @@ import UIKit
 import SnapKit
 
 
-class LNRoomHostSeatView: UIView {
-    private let seatNum: Int = 1001
+class LNRoomHostSeatView: UIView, LNRoomSeatViewProtocol {
+    private let seatNum: Int = 0
     private let nameLabel = UILabel()
     private let emptyIc = UIImageView()
     
     private let userView = UIView()
     private let userAvatar = UIImageView()
     private let muteIc = UIImageView()
+    private let speakingView = LNRoomSeatSpeakingView()
+    
+    private weak var roomSession: LNRoomViewModel?
+    private var curSeat: LNRoomSeatItem? {
+        roomSession?.seatsInfo.first { $0.index == seatNum }
+    }
+    private var isSpeaking: Bool {
+        roomSession?.speakingUser.contains(curSeat?.uid ?? "") == true
+    }
     
     override init(frame: CGRect) {
         super.init(frame: frame)
         
         setupViews()
+        
+        LNEventDeliver.addObserver(self)
+    }
+    
+    func update(_ room: LNRoomViewModel?) {
+        roomSession = room
+        onRoomSeatsChanged()
+        speakingView.update(seatNum, room: room)
     }
     
     required init?(coder: NSCoder) {
@@ -30,8 +47,29 @@ class LNRoomHostSeatView: UIView {
     }
 }
 
+extension LNRoomHostSeatView: LNRoomViewModelNotify {
+    func onRoomSeatsChanged() {
+        if let curSeat {
+            muteIc.isHidden = !curSeat.isMute
+            userView.isHidden = false
+            userAvatar.sd_setImage(with: URL(string: curSeat.avatar))
+            nameLabel.text = curSeat.nickname.isEmpty ? " " : curSeat.nickname
+        } else {
+            muteIc.isHidden = true
+            userView.isHidden = true
+            nameLabel.text = .init(key: "A00328")
+        }
+    }
+}
+
 extension LNRoomHostSeatView {
     private func setupViews() {
+        onTap { [weak self] in
+            guard let self else { return }
+            guard let roomSession,
+                  let curSeat else { return }
+            handlerClick(roomSession, curSeat)
+        }
         snp.makeConstraints { make in
             make.width.equalTo(76)
             make.height.equalTo(68)
@@ -72,6 +110,11 @@ extension LNRoomHostSeatView {
             make.width.height.equalTo(46)
         }
         
+        userView.addSubview(speakingView)
+        speakingView.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+        }
+        
         let gradientBg = UIImageView()
         gradientBg.layer.cornerRadius = 23
         gradientBg.image = .primary_7
@@ -89,6 +132,8 @@ extension LNRoomHostSeatView {
             make.width.height.equalTo(44)
         }
         
+        muteIc.layer.cornerRadius = 6.3
+        muteIc.backgroundColor = .black.withAlphaComponent(0.7)
         muteIc.image = .icMicOn
         userView.addSubview(muteIc)
         muteIc.snp.makeConstraints { make in

+ 156 - 0
Lanu/Views/Room/Seats/LNRoomSeatSpeakingView.swift

@@ -0,0 +1,156 @@
+//
+//  LNRoomSeatSpeakingView.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/17.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNRoomSeatSpeakingView: UIView {
+    private var borderColor: UIColor = .fill
+    private var borderWidth: CGFloat = 1
+    private var fillColor: UIColor = .fill.withAlphaComponent(0.5)
+    private var duration: Double = 2
+    private var offset = 10.0
+    
+    private let waveCount = 2
+    private var borderLayers: [CAShapeLayer] = []
+    
+    private weak var roomSession: LNRoomViewModel?
+    private var curIndex = -1
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        clipsToBounds = false
+        isUserInteractionEnabled = false
+        
+        for _ in 0..<waveCount {
+            let borderLayer = CAShapeLayer()
+            borderLayer.borderColor = borderColor.cgColor
+            borderLayer.borderWidth = borderWidth
+            borderLayer.backgroundColor = fillColor.cgColor
+            layer.addSublayer(borderLayer)
+            borderLayers.append(borderLayer)
+        }
+        
+        LNEventDeliver.addObserver(self)
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    func update(_ index: Int, room: LNRoomViewModel?) {
+        roomSession = room
+        curIndex = index
+    }
+    
+    override func layoutSubviews() {
+        super.layoutSubviews()
+        
+        if borderLayers.first?.bounds.height != bounds.height {
+            rebuild()
+        }
+    }
+}
+
+extension LNRoomSeatSpeakingView: LNRoomViewModelNotify {
+    func onRoomSeatsChanged() {
+        check()
+    }
+    
+    func onRoomSpeakingUsersChanged() {
+        check()
+    }
+    
+    private func check() {
+        guard curIndex != -1,
+              let seat = roomSession?.seatsInfo.first(where: { $0.index == curIndex }) else {
+            dismiss()
+            return
+        }
+        
+        if roomSession?.speakingUser.contains(seat.uid) == true {
+            show()
+        } else {
+            dismiss()
+        }
+    }
+}
+ 
+extension LNRoomSeatSpeakingView {
+    private func startAnimate(layer: CAShapeLayer, index: Int) {
+        let scaleAnim = CABasicAnimation(keyPath: "transform.scale")
+        scaleAnim.fromValue = 1.0
+        scaleAnim.toValue = 1.0 + offset / CGFloat(bounds.width / 2)
+        scaleAnim.duration = duration
+        
+        let opacityAnim = CABasicAnimation(keyPath: "opacity")
+        opacityAnim.fromValue = 1.0
+        opacityAnim.toValue = 0.0
+        opacityAnim.duration = duration
+        
+        let animGroup = CAAnimationGroup()
+        animGroup.animations = [scaleAnim, opacityAnim]
+        animGroup.duration = duration
+        animGroup.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
+        animGroup.isRemovedOnCompletion = false
+        animGroup.fillMode = .forwards
+        animGroup.repeatCount = .infinity
+        animGroup.beginTime = CACurrentMediaTime() + Double(index) * duration * 0.5
+        layer.add(animGroup, forKey: "scaleAndFadeGroup")
+    }
+    
+    private func rebuild() {
+        for (index, layer) in borderLayers.enumerated() {
+            layer.frame = bounds
+            layer.cornerRadius = bounds.height * 0.5
+            layer.removeAllAnimations()
+            startAnimate(layer: layer, index: index)
+        }
+    }
+    
+    private func show() {
+        UIView.animate(withDuration: 0.25) {
+            self.alpha = 1.0
+        }
+    }
+    
+    private func dismiss() {
+        UIView.animate(withDuration: 0.25) {
+            self.alpha = 0.0
+        }
+    }
+}
+
+#if DEBUG
+
+import SwiftUI
+
+struct LNRoomSeatSpeakingViewPreview: UIViewRepresentable {
+    func makeUIView(context: Context) -> some UIView {
+        let container = UIView()
+        container.backgroundColor = .lightGray
+        
+        let view = LNRoomSeatSpeakingView()
+        container.addSubview(view)
+        view.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+            make.width.height.equalTo(50)
+        }
+        
+        return container
+    }
+    
+    func updateUIView(_ uiView: UIViewType, context: Context) { }
+}
+
+#Preview(body: {
+    LNRoomSeatSpeakingViewPreview()
+})
+#endif

+ 150 - 0
Lanu/Views/Room/Seats/LNRoomSeatViewProtocol.swift

@@ -0,0 +1,150 @@
+//
+//  LNRoomSeatViewProtocol.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/16.
+//
+
+import Foundation
+
+
+protocol LNRoomSeatViewProtocol {
+}
+
+extension LNRoomSeatViewProtocol {
+    func handlerClick(_ room: LNRoomViewModel, _ seat: LNRoomSeatItem) {
+        let sheet = LNRoomSheetMenu()
+        
+        let invite = LNRoomSheetMenu.buildMenuItem(title: .init(key: "A00341"), handler: { [weak sheet] in
+            guard let sheet else { return }
+            sheet.dismiss()
+        })
+        
+        let closeMic = LNRoomSheetMenu.buildMenuItem(title: .init(key: "A00342"), handler: { [weak sheet] in
+            guard let sheet else { return }
+            room.lockSeat(num: seat.index) { [weak sheet] success in
+                guard let sheet else { return }
+                guard success else { return }
+                sheet.dismiss()
+            }
+        })
+        
+        let openMic = LNRoomSheetMenu.buildMenuItem(title: .init(key: "A00348"), handler: { [weak sheet] in
+            guard let sheet else { return }
+            room.unlockSeat(num: seat.index) { [weak sheet] success in
+                guard let sheet else { return }
+                guard success else { return }
+                sheet.dismiss()
+            }
+        })
+        
+        let muteMic = LNRoomSheetMenu.buildMenuItem(title: .init(key: "A00343"), handler: { [weak sheet] in
+            guard let sheet else { return }
+            sheet.dismiss()
+        })
+        
+        let unmuteMic = LNRoomSheetMenu.buildMenuItem(title: .init(key: "A00349"), handler: { [weak sheet] in
+            guard let sheet else { return }
+            sheet.dismiss()
+        })
+        
+        let profile = LNRoomSheetMenu.buildMenuItem(title: .init(key: "A00344"), handler: { [weak sheet] in
+            guard let sheet else { return }
+            sheet.dismiss()
+            
+            let panel = LNRoomProfileCardPanel()
+            panel.load(seat.uid)
+            panel.popup()
+        })
+        
+        let kickOffMic = LNRoomSheetMenu.buildMenuItem(title: .init(key: "A00345"), handler: { [weak sheet] in
+            guard let sheet else { return }
+            room.kickUserOffSeat(uid: seat.uid) { [weak sheet] success in
+                guard let sheet else { return }
+                guard success else { return }
+                sheet.dismiss()
+            }
+        })
+        
+        let offMic = LNRoomSheetMenu.buildMenuItem(title: .init(key: "A00346"), handler: { [weak sheet] in
+            guard let sheet else { return }
+            room.offSeat { [weak sheet] success in
+                guard let sheet else { return }
+                guard success else { return }
+                sheet.dismiss()
+            }
+        })
+        
+        let toBePlaymate = LNRoomSheetMenu.buildMenuItem(title: .init(key: "A00347"), handler: { [weak sheet] in
+            guard let sheet else { return }
+            showLoading()
+            LNGameMateManager.shared.getJoinGameMateInfo { [weak sheet] res in
+                dismissLoading()
+                guard let sheet else { return }
+                guard let res else { return }
+                sheet.dismiss()
+                
+                if res.step3Complete || res.underReview {
+                    sheet.pushToJoinUsReview(true)
+                    return
+                }
+                let config = LNJumpWebViewConfig(url: .joinUsUrl)
+                sheet.pushToWebView(config)
+            }
+        })
+        
+        if seat.uid.isMyUid { // 自己
+            sheet.update(title: nil, views: [
+                offMic, profile
+            ])
+        } else if true { // 管理员
+            if seat.uid.isEmpty { // 空麦位
+                if seat.isLocked {
+                    sheet.update(title: .init(key: "A00326", seat.index), views: [
+                        openMic, seat.isMute ? unmuteMic : muteMic
+                    ])
+                } else {
+                    sheet.update(title: .init(key: "A00326", seat.index), views: [
+                        invite, closeMic, seat.isMute ? unmuteMic : muteMic
+                    ])
+                }
+            } else { // 有人的麦位
+                sheet.update(title: .init(key: "A00326", seat.index), views: [
+                    profile, kickOffMic, closeMic, seat.isMute ? unmuteMic : muteMic
+                ])
+            }
+        } else if myUserInfo.playmate { // 陪玩师
+            if seat.index == 0 { // 主持人麦位
+                showToast(.init(key: "123")) // TODO: cwy
+                return
+            } else if seat.uid.isEmpty { // 空麦位
+                room.applySeat { _ in } // 直接申请上麦
+                return
+            } else {
+                let panel = LNRoomProfileCardPanel()
+                panel.load(seat.uid)
+                panel.popup()
+                return
+            }
+        } else { // 普通用户
+            if !seat.uid.isEmpty { // 麦上有人
+                let panel = LNRoomProfileCardPanel()
+                panel.load(seat.uid)
+                panel.popup()
+                return
+            } else if seat.index == 0 { // 主持人麦位
+                showToast(.init(key: "123")) // TODO: cwy
+                return
+            } else if seat.index == 1 { // 嘉宾位
+                room.applySeat { _ in } // 直接申请上麦
+                return
+            } else {
+                sheet.update(title: nil, views: [
+                    toBePlaymate
+                ])
+            }
+        }
+        
+        sheet.popup()
+    }
+}

+ 6 - 3
Lanu/Views/Room/Seats/LNRoomSeatsView.swift

@@ -14,7 +14,6 @@ class LNRoomSeatsView: UIView {
     private let hostSeat = LNRoomHostSeatView()
     private let guestSeat = LNRoomGuestSeatView()
     private var playMateSeats: [LNRoomPlaymateSeatView] = []
-    private let seatsView = LNMultiLineStackView()
     
     private weak var roomSession: LNRoomViewModel?
     
@@ -24,9 +23,12 @@ class LNRoomSeatsView: UIView {
         setupViews()
     }
     
-    func update(_ room: LNRoomViewModel) {
+    func update(_ room: LNRoomViewModel?) {
         roomSession = room
         
+        hostSeat.update(room)
+        guestSeat.update(room)
+        playMateSeats.forEach { $0.update(room) }
     }
     
     required init?(coder: NSCoder) {
@@ -108,12 +110,13 @@ extension LNRoomSeatsView {
     }
     
     private func buildSeatsView() -> UIView {
+        let seatsView = LNMultiLineStackView()
         seatsView.columns = 4
         seatsView.spacing = 20
         seatsView.itemDistribution = .equalSpacing
         
         for i in 0..<8 {
-            let seat = LNRoomPlaymateSeatView(seatNum: i)
+            let seat = LNRoomPlaymateSeatView(seatNum: i + 2)
             seat.update(roomSession)
             playMateSeats.append(seat)
         }

+ 52 - 3
Lanu/Views/Room/Seats/LNRoomPlaymateSeatView.swift → Lanu/Views/Room/Seats/Playmate/LNRoomPlaymateSeatView.swift

@@ -10,27 +10,36 @@ import UIKit
 import SnapKit
 
 
-class LNRoomPlaymateSeatView: UIView {
-    let seatNum: Int
+class LNRoomPlaymateSeatView: UIView, LNRoomSeatViewProtocol {
+    private let seatNum: Int
     private let nameLabel = UILabel()
     private let emptyIc = UIImageView()
     
     private let userView = UIView()
     private let userAvatar = UIImageView()
     private let muteIc = UIImageView()
+    private let speakingView = LNRoomSeatSpeakingView()
     
     private weak var roomSession: LNRoomViewModel?
+    private var curSeat: LNRoomSeatItem? {
+        roomSession?.seatsInfo.first { $0.index == seatNum }
+    }
+    private var isSpeaking: Bool {
+        roomSession?.speakingUser.contains(curSeat?.uid ?? "") == true
+    }
     
     init(seatNum: Int) {
         self.seatNum = seatNum
         super.init(frame: .zero)
         
         setupViews()
+        LNEventDeliver.addObserver(self)
     }
     
     func update(_ room: LNRoomViewModel?) {
         roomSession = room
-        
+        onRoomSeatsChanged()
+        speakingView.update(seatNum, room: room)
     }
     
     required init?(coder: NSCoder) {
@@ -38,8 +47,43 @@ class LNRoomPlaymateSeatView: UIView {
     }
 }
 
+extension LNRoomPlaymateSeatView: LNRoomViewModelNotify {
+    func onRoomSeatsChanged() {
+        guard let curSeat else { return }
+        
+        if curSeat.isLocked {
+            userView.isHidden = true
+            emptyIc.image = .icSeatLock
+        } else if curSeat.uid.isEmpty {
+            userView.isHidden = true
+            emptyIc.image = .icSeatNormal
+            nameLabel.text = .init(key: "A00326", seatNum + 1)
+        } else {
+            userView.isHidden = false
+            userAvatar.sd_setImage(with: URL(string: curSeat.avatar))
+            nameLabel.text = curSeat.nickname
+        }
+        
+        muteIc.isHidden = !curSeat.isMute
+    }
+    
+    func onRoomSpeakingUsersChanged() {
+        guard let curSeat else {
+            speakingView.isHidden = true
+            return
+        }
+        speakingView.isHidden = roomSession?.speakingUser.contains(where: { $0 == curSeat.uid }) != false
+    }
+}
+
 extension LNRoomPlaymateSeatView {
     private func setupViews() {
+        onTap { [weak self] in
+            guard let self else { return }
+            guard let roomSession,
+                  let curSeat else { return }
+            handlerClick(roomSession, curSeat)
+        }
         snp.makeConstraints { make in
             make.width.equalTo(76)
             make.height.equalTo(68)
@@ -82,6 +126,11 @@ extension LNRoomPlaymateSeatView {
             make.width.height.equalTo(46)
         }
         
+        userView.addSubview(speakingView)
+        speakingView.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+        }
+        
         let gradientBg = UIImageView()
         gradientBg.layer.cornerRadius = 23
         gradientBg.image = .primary_7

+ 276 - 0
Lanu/Views/Room/Settings/LNRoomInfoEditPanel.swift

@@ -0,0 +1,276 @@
+//
+//  LNRoomInfoEditPanel.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/16.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNRoomInfoEditPanel: LNPopupView {
+    private let cover = LNImageUploadView()
+    private let coverLabel = UILabel()
+    
+    private let nameLabel = UILabel()
+    private let nameCountLabel = UILabel()
+    private var curName: String = "" {
+        didSet {
+            if !curName.isEmpty {
+                nameLabel.textColor = .text_1
+                nameLabel.text = curName
+                nameCountLabel.text = "\(curName.count)/\(LNRoomManager.RoomNameMaxInput)"
+            } else {
+                nameLabel.textColor = .text_2
+                nameLabel.text = .init(key: "A00316")
+                nameCountLabel.text = "0/\(LNRoomManager.RoomNameMaxInput)"
+            }
+            
+            checkCreate()
+        }
+    }
+    
+    private let muteSwitch = UISwitch()
+    private let saveButton = UIButton()
+    
+    private weak var roomSession: LNRoomViewModel?
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setupViews()
+    }
+    
+    func update(_ room: LNRoomViewModel?) {
+        roomSession = room
+        
+        if let room {
+            cover.loadImage(url: room.roomInfo.coverURL)
+            curName = room.roomInfo.liveName
+            muteSwitch.isOn = true
+        }
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNRoomInfoEditPanel: LNImageUploadViewDelegate {
+    func onImageUploadView(view: LNImageUploadView, didUploadImage url: String) {
+        checkCreate()
+    }
+}
+
+extension LNRoomInfoEditPanel {
+    private func checkCreate() {
+        saveButton.isEnabled = !curName.isEmpty && cover.imageUrl?.isEmpty == false
+    }
+    
+    private func setupViews() {
+        container.backgroundColor = .fill_7
+        
+        let header = buildHeader()
+        container.addSubview(header)
+        header.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalToSuperview()
+        }
+        
+        let stackView = UIStackView()
+        stackView.axis = .vertical
+        stackView.spacing = 16
+        container.addSubview(stackView)
+        stackView.snp.makeConstraints { make in
+            make.top.equalTo(header.snp.bottom)
+            make.horizontalEdges.equalToSuperview().inset(16)
+            make.bottom.equalToSuperview().offset(commonBottomInset)
+        }
+        
+        stackView.addArrangedSubview(buildCover())
+        stackView.addArrangedSubview(buildTextInfo())
+        stackView.addArrangedSubview(buildSettings())
+        
+        saveButton.setBackgroundImage(.primary_8, for: .normal)
+        saveButton.setTitle(.init(key: "A00185"), for: .normal)
+        saveButton.layer.cornerRadius = 23.5
+        saveButton.clipsToBounds = true
+        saveButton.titleLabel?.font = .heading_h3
+        saveButton.titleEdgeInsets = .init(top: 0, left: 4, bottom: 0, right: 0)
+        saveButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            guard let roomSession else { return }
+            guard let imageUrl = cover.imageUrl else { return }
+            
+            roomSession.updateRoomInfo(name: curName, cover: imageUrl) { [weak self] success in
+                guard let self else { return }
+                guard success else { return }
+                dismiss()
+            }
+        }), for: .touchUpInside)
+        stackView.addArrangedSubview(saveButton)
+        saveButton.snp.makeConstraints { make in
+            make.height.equalTo(47)
+        }
+    }
+    
+    private func buildHeader() -> UIView {
+        let container = UIView()
+        container.snp.makeConstraints { make in
+            make.height.equalTo(52)
+        }
+        
+        let titleLabel = UILabel()
+        titleLabel.text = .init(key: "A00211")
+        titleLabel.font = .heading_h3
+        titleLabel.textColor = .text_1
+        container.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+        }
+        
+        return container
+    }
+    
+    private func buildCover() -> UIView {
+        let container = UIView()
+        
+        let holder = UIView()
+        holder.backgroundColor = .fill_2
+        holder.layer.cornerRadius = 12
+        holder.clipsToBounds = true
+        holder.onTap { [weak self] in
+            guard let self else { return }
+            LNBottomSheetMenu.showImageSelectMenu { [weak self] image, _ in
+                guard let self else { return }
+                if let image {
+                    cover.uploadImage(image: image)
+                }
+            }
+        }
+        container.addSubview(holder)
+        holder.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.verticalEdges.equalToSuperview()
+            make.width.height.equalTo(105)
+        }
+        
+        let config = UIImage.SymbolConfiguration(pointSize: 17)
+        let plus = UIImageView()
+        plus.image = .init(systemName: "plus", withConfiguration: config)
+        plus.tintColor = .text_3
+        holder.addSubview(plus)
+        plus.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+        }
+        
+        cover.delegate = self
+        cover.showClearButton = false
+        holder.addSubview(cover)
+        cover.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+        }
+        
+        let bottom = UIView()
+        bottom.backgroundColor = .black.withAlphaComponent(0.3)
+        holder.addSubview(bottom)
+        bottom.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.bottom.equalToSuperview()
+            make.height.equalTo(22)
+        }
+        
+        coverLabel.text = .init(key: "A00226")
+        coverLabel.font = .body_s
+        coverLabel.textColor = .text_1
+        bottom.addSubview(coverLabel)
+        coverLabel.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+        }
+        
+        return container
+    }
+    
+    private func buildTextInfo() -> UIView {
+        let container = UIView()
+        
+        let titleLabel = UILabel()
+        titleLabel.font = .heading_h5
+        titleLabel.textColor = .text_2
+        titleLabel.text = .init(key: "A00315")
+        container.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.leading.equalToSuperview()
+            make.top.equalToSuperview()
+        }
+        
+        nameCountLabel.font = .body_s
+        nameCountLabel.textColor = .text_2
+        container.addSubview(nameCountLabel)
+        nameCountLabel.snp.makeConstraints { make in
+            make.centerY.equalTo(titleLabel)
+            make.trailing.equalToSuperview()
+        }
+        
+        let holder = UIView()
+        holder.layer.cornerRadius = 8
+        holder.backgroundColor = .fill.withAlphaComponent(0.1)
+        holder.onTap { [weak self] in
+            guard let self else { return }
+            let panel = LNRoomNameInputPanel()
+            panel.handler = { [weak self] name in
+                guard let self else { return }
+                curName = name
+            }
+            panel.update(curName)
+            panel.popup()
+        }
+        container.addSubview(holder)
+        holder.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalTo(titleLabel.snp.bottom).offset(8)
+            make.bottom.equalToSuperview()
+            make.height.equalTo(46)
+        }
+        
+        nameLabel.text = .init(key: "A00316")
+        nameLabel.font = .heading_h4
+        nameLabel.textColor = .text_1
+        holder.addSubview(nameLabel)
+        nameLabel.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(12)
+            make.centerY.equalToSuperview()
+        }
+        
+        return container
+    }
+    
+    private func buildSettings() -> UIView {
+        let container = UIView()
+        
+        let titleLabel = UILabel()
+        titleLabel.font = .heading_h4
+        titleLabel.textColor = .text_1
+        titleLabel.text = .init(key: "A00317")
+        container.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.leading.equalToSuperview()
+            make.centerY.equalToSuperview()
+        }
+        
+        let scaleX: CGFloat = 40.0 / 51.0
+        let scaleY: CGFloat = 24.5 / 31.0
+        muteSwitch.onTintColor = .primary_5
+        muteSwitch.transform = CGAffineTransform(scaleX: scaleX, y: scaleY)
+        container.addSubview(muteSwitch)
+        muteSwitch.snp.makeConstraints { make in
+            make.verticalEdges.equalToSuperview()
+            make.trailing.equalToSuperview()
+            make.leading.greaterThanOrEqualTo(titleLabel.snp.trailing).offset(12)
+        }
+        
+        return container
+    }
+}

+ 140 - 0
Lanu/Views/Room/Settings/LNRoomSettingMenuPanel.swift

@@ -0,0 +1,140 @@
+//
+//  LNRoomSettingMenuPanel.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/16.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNRoomSettingMenuPanel: LNPopupView {
+    private weak var roomSession: LNRoomViewModel?
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setupViews()
+    }
+    
+    func update(_ room: LNRoomViewModel?) {
+        roomSession = room
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNRoomSettingMenuPanel {
+    private func setupViews() {
+        container.backgroundColor = .fill_7
+        
+        let header = buildHeader()
+        container.addSubview(header)
+        header.snp.makeConstraints { make in
+            make.horizontalEdges.top.equalToSuperview()
+            make.height.equalTo(52)
+        }
+        
+        let stackView = UIStackView()
+        stackView.axis = .horizontal
+        stackView.alignment = .top
+        stackView.spacing = 2
+        stackView.distribution = .fillEqually
+        container.addSubview(stackView)
+        stackView.snp.makeConstraints { make in
+            make.top.equalTo(header.snp.bottom)
+            make.leading.equalToSuperview().offset(10)
+            make.trailing.lessThanOrEqualToSuperview().offset(-10)
+            make.bottom.equalToSuperview().offset(commonBottomInset)
+            make.height.equalTo(85)
+        }
+        
+        stackView.addArrangedSubview(buildActionItem(
+            icon: .icSettingsRoom,
+            title: .init(key: "B00095")
+        ) { [weak self] in
+            guard let self else { return }
+            guard let roomSession else { return }
+            dismiss()
+            
+            let panel = LNRoomInfoEditPanel()
+            panel.update(roomSession)
+            panel.popup()
+        })
+        
+        stackView.addArrangedSubview(buildActionItem(
+            icon: .icShutdown,
+            title: .init(key: "A00338")
+        ) { [weak self] in
+            guard let self else { return }
+            guard let roomSession else { return }
+            
+            dismiss()
+            
+            LNCommonAlertView.showCloseRoomAlert(roomSession)
+        })
+    }
+    
+    private func buildHeader() -> UIView {
+        let view = UIView()
+        
+        let titleLabel = UILabel()
+        titleLabel.text = .init(key: "A00337")
+        titleLabel.font = .heading_h3
+        titleLabel.textColor = .text_1
+        view.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(16)
+            make.centerY.equalToSuperview()
+        }
+        
+        return view
+    }
+    
+    private func buildActionItem(icon: UIImage, title: String, handler: @escaping () -> Void) -> UIView {
+        let view = UIView()
+        view.snp.makeConstraints { make in
+            make.width.equalTo(70)
+            make.height.equalTo(85)
+        }
+        view.onTap(handler)
+        
+        let buttonHolder = UIView()
+        buttonHolder.backgroundColor = UIColor.primary_1.withAlphaComponent(0.24)
+        buttonHolder.layer.cornerRadius = 24
+        buttonHolder.isUserInteractionEnabled = false
+        view.addSubview(buttonHolder)
+        buttonHolder.snp.makeConstraints { make in
+            make.top.equalToSuperview().offset(1)
+            make.centerX.equalToSuperview()
+            make.width.height.equalTo(48)
+        }
+        
+        let iconView = UIImageView()
+        iconView.image = icon
+        iconView.tintColor = .primary_1
+        buttonHolder.addSubview(iconView)
+        iconView.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+            make.width.height.equalTo(28)
+        }
+        
+        let titleLabel = UILabel()
+        titleLabel.text = title
+        titleLabel.font = .body_s
+        titleLabel.textColor = .text_2
+        titleLabel.textAlignment = .center
+        titleLabel.numberOfLines = 2
+        view.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.top.equalTo(buttonHolder.snp.bottom).offset(6)
+            make.horizontalEdges.equalToSuperview()
+        }
+        
+        return view
+    }
+}

+ 22 - 7
Lanu/Views/Room/Top/LNRoomTopMenuView.swift

@@ -21,14 +21,15 @@ class LNRoomTopMenuView: UIView {
         super.init(frame: frame)
         
         setupViews()
+        LNEventDeliver.addObserver(self)
     }
     
-    func update(_ room: LNRoomViewModel) {
+    func update(_ room: LNRoomViewModel?) {
         roomSession = room
         
-        hostAvatar.sd_setImage(with: URL(string: room.hostAvatar))
-        roomNameLabel.text = room.roomName
-        roomIdLabel.text = room.roomId
+        if let room {
+            update(room.roomInfo)
+        }
     }
     
     required init?(coder: NSCoder) {
@@ -36,14 +37,26 @@ class LNRoomTopMenuView: UIView {
     }
 }
 
+extension LNRoomTopMenuView: LNRoomViewModelNotify {
+    func onRoomInfoChanged() {
+        guard let roomInfo = roomSession?.roomInfo else { return }
+        update(roomInfo)
+    }
+}
+
 extension LNRoomTopMenuView {
+    private func update(_ roomInfo: LNRoomInfo) {
+        hostAvatar.sd_setImage(with: URL(string: roomInfo.coverURL))
+        roomNameLabel.text = roomInfo.liveName
+        roomIdLabel.text = roomInfo.liveID
+    }
+    
     private func setupViews() {
         let closeButton = UIButton()
         closeButton.setImage(.icShutdown, for: .normal)
         closeButton.addAction(UIAction(handler: { [weak self] _ in
             guard let self else { return }
-            
-            navigationController?.popViewController(animated: true)
+            roomSession?.closeRoom()
         }), for: .touchUpInside)
         addSubview(closeButton)
         closeButton.snp.makeConstraints { make in
@@ -62,6 +75,7 @@ extension LNRoomTopMenuView {
     private func buildRoomInfo() -> UIView {
         let container = UIView()
         
+        hostAvatar.contentMode = .scaleAspectFill
         hostAvatar.layer.cornerRadius = 15
         hostAvatar.clipsToBounds = true
         container.addSubview(hostAvatar)
@@ -76,6 +90,7 @@ extension LNRoomTopMenuView {
         textView.snp.makeConstraints { make in
             make.centerY.equalToSuperview()
             make.leading.equalTo(hostAvatar.snp.trailing).offset(8)
+            make.trailing.equalToSuperview()
         }
         
         roomNameLabel.font = .heading_h5
@@ -86,7 +101,7 @@ extension LNRoomTopMenuView {
             make.top.equalToSuperview()
         }
         
-        roomIdLabel.font = .body_m
+        roomIdLabel.font = .systemFont(ofSize: 8)
         roomIdLabel.textColor = .text_1
         textView.addSubview(roomIdLabel)
         roomIdLabel.snp.makeConstraints { make in

+ 29 - 0
Lanu/Views/Room/ViewModel/LNRoomInfo.swift

@@ -0,0 +1,29 @@
+//
+//  LNRoomInfo.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/13.
+//
+
+import Foundation
+import AtomicXCore
+
+
+class LNRoomInfo {
+    var liveID: String = ""
+    var liveName: String = ""
+    var coverURL: String = ""
+    
+    @discardableResult
+    func update(_ info: LiveInfo) -> Bool {
+        if info.liveID != liveID
+            || info.liveName != liveName
+            || info.coverURL != coverURL {
+            liveID = info.liveID
+            liveName = info.liveName
+            coverURL = info.coverURL
+            return true
+        }
+        return false
+    }
+}

+ 27 - 0
Lanu/Views/Room/ViewModel/LNRoomMessageItem.swift

@@ -0,0 +1,27 @@
+//
+//  LNRoomMessageItem.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/12.
+//
+
+import Foundation
+
+
+enum LNRoomMessageItemType {
+    case unknown
+    case chat
+    case system
+}
+
+class LNRoomMessageItem {
+    let type: LNRoomMessageItemType
+    let sender: String
+    let text: String
+    
+    init(type: LNRoomMessageItemType, sender: String, text: String) {
+        self.type = type
+        self.sender = sender
+        self.text = text
+    }
+}

+ 61 - 0
Lanu/Views/Room/ViewModel/LNRoomSeatApplyItem.swift

@@ -0,0 +1,61 @@
+//
+//  LNRoomSeatApplyItem.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/13.
+//
+
+import Foundation
+
+
+class LNRoomSeatApplyItem {
+    var userNo: String = ""
+    var avatar: String = ""
+    var name: String = ""
+    var gender: LNUserGender = .unknow
+    var time: Int64 = 0
+    var category: String = ""
+    
+    var hasAccept = false
+    
+    convenience init(
+        userNo: String,
+        avatar: String,
+        name: String,
+        gender: LNUserGender,
+        time: Int64,
+        category: String = ""
+    ) {
+        self.init()
+        self.userNo = userNo
+        self.avatar = avatar
+        self.name = name
+        self.gender = gender
+        self.time = time
+        self.category = category
+    }
+    
+    var relativeTimeText: String {
+        guard time > 0 else { return "just now" }
+        
+        let now = Int64(Date().timeIntervalSince1970 * 1_000)
+        let diff = max(0, now - time) / 1_000
+        
+        if diff < 60 {
+            return "just now"
+        }
+        
+        let minute = diff / 60
+        if minute < 60 {
+            return minute == 1 ? "1 minute" : "\(minute) minutes"
+        }
+        
+        let hour = minute / 60
+        if hour < 24 {
+            return hour == 1 ? "1 hour" : "\(hour) hours"
+        }
+        
+        let day = hour / 24
+        return day == 1 ? "1 day" : "\(day) days"
+    }
+}

+ 40 - 0
Lanu/Views/Room/ViewModel/LNRoomSeatItem.swift

@@ -0,0 +1,40 @@
+//
+//  LNRoomSeatItem.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/12.
+//
+
+import Foundation
+import AtomicXCore
+
+class LNRoomSeatItem {
+    let index: Int
+    var avatar: String = ""
+    var nickname: String = ""
+    var isLocked: Bool = false
+    var uid: String = ""
+    var isMute: Bool = false
+    
+    init(index: Int) {
+        self.index = index
+    }
+    
+    @discardableResult
+    func update(_ item: SeatInfo) -> Bool {
+        if avatar != item.userInfo.avatarURL
+            || nickname != item.userInfo.userName
+            || isLocked != item.isLocked
+            || uid != item.userInfo.userID
+            || isMute != !item.userInfo.allowOpenMicrophone {
+            avatar = item.userInfo.avatarURL
+            nickname = item.userInfo.userName
+            isLocked = item.isLocked
+            uid = item.userInfo.userID
+            isMute = !item.userInfo.allowOpenMicrophone
+            
+            return true
+        }
+        return false
+    }
+}

+ 366 - 3
Lanu/Views/Room/ViewModel/LNRoomViewModel.swift

@@ -7,16 +7,379 @@
 
 import Foundation
 import AtomicXCore
+import Combine
+
+
+protocol LNRoomViewModelNotify {
+    func onRoomMessageChanged()
+    func onRoomSeatsChanged()
+    func onRoomSpeakingUsersChanged()
+    func onRoomSeatApplyChanged()
+    func onRoomInfoChanged()
+    
+    func onRoomClosed()
+}
+extension LNRoomViewModelNotify {
+    func onRoomMessageChanged() { }
+    func onRoomSeatsChanged() { }
+    func onRoomSpeakingUsersChanged() { }
+    func onRoomSeatApplyChanged() { }
+    func onRoomInfoChanged() { }
+    
+    func onRoomClosed() { }
+}
 
 
 class LNRoomViewModel: NSObject {
     let roomId: String
-    let seatStore: LiveSeatStore
-    private(set) var hostAvatar = ""
-    private(set) var roomName = ""
+    private let seatStore: LiveSeatStore
+    private let guestStore: CoGuestStore
+    private let messageStore: BarrageStore
+    private(set) var seatsInfo: [LNRoomSeatItem] = []
+    private(set) var roomInfo = LNRoomInfo()
+    private(set) var speakingUser: [String] = []
+    
+    private let maxMessageCount = 300
+    private let messageRefreshInterval: TimeInterval = 1
+    private var messageRefreshTask: String?
     
     init(roomId: String) {
         self.roomId = roomId
+        
         seatStore = LiveSeatStore.create(liveID: roomId)
+        guestStore = CoGuestStore.create(liveID: roomId)
+        messageStore = BarrageStore.create(liveID: roomId)
+        
+        super.init()
+        
+        setupSeatObservers()
+        setupApplyObservers()
+        setupMessageObservers()
+        setupRoomInfoObserver()
+    }
+    
+    func closeRoom() {
+        LNRoomManager.shared.closeRoom { success in
+            LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomClosed() }
+        }
+    }
+}
+
+// MARK: 麦位管理 - 普通用户
+extension LNRoomViewModel {
+    var isOnMic: Bool {
+        seatsInfo.contains { $0.uid == myUid }
+    }
+    
+    private func setupSeatObservers() {
+        seatStore.state.subscribe().receive(on: DispatchQueue.main).sink { [weak self] state in
+            guard let self else { return }
+            let seats = state.seatList
+            var hasChanged = seats.count != seatsInfo.count
+            var newSeats: [LNRoomSeatItem] = []
+            for seat in seats {
+                let item = seatsInfo.first(where: { $0.index == seat.index }) ?? LNRoomSeatItem(index: seat.index)
+                hasChanged = hasChanged || item.update(seat)
+                newSeats.append(item)
+            }
+            if hasChanged {
+                seatsInfo = newSeats
+                LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomSeatsChanged() }
+            }
+            
+            let speakings = state.speakingUsers.filter { $0.value > 0 }.map { $0.key }.sorted { $1 > $0 }
+            if speakings != speakingUser {
+                speakingUser = speakings
+                LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomSpeakingUsersChanged() }
+            }
+        }.store(in: &cancellables)
+    }
+    
+    // 申请上麦
+    func applySeat(handler: @escaping (Bool) -> Void) {
+        guestStore.applyForSeat(timeout: 0, extraInfo: nil) { result in
+            switch result {
+            case .success:
+                handler(true)
+            case .failure(let err):
+                showToast(err.localizedDescription)
+                handler(false)
+            }
+        }
+    }
+    
+    // 取消上麦申请
+    func cancelSeatApply(handler: @escaping (Bool) -> Void) {
+        guestStore.cancelApplication { result in
+            switch result {
+            case .success:
+                handler(true)
+            case .failure(let err):
+                showToast(err.localizedDescription)
+                handler(false)
+            }
+        }
+    }
+    
+    // 主动下麦
+    func offSeat(handler: @escaping (Bool) -> Void) {
+        guestStore.disConnect { result in
+            switch result {
+            case .success:
+                handler(true)
+            case .failure(let err):
+                showToast(err.localizedDescription)
+                handler(false)
+            }
+        }
+    }
+    
+    // 接受邀请
+    func acceptSeatInvite(uid: String, handler: @escaping (Bool) -> Void) {
+        guestStore.acceptInvitation(inviterID: uid) { result in
+            switch result {
+            case .success:
+                handler(true)
+            case .failure(let err):
+                showToast(err.localizedDescription)
+                handler(false)
+            }
+        }
+    }
+    
+    // 拒绝邀请
+    func rejectSeatInvite(uid: String, handler: @escaping (Bool) -> Void) {
+        guestStore.rejectInvitation(inviterID: uid) { result in
+            switch result {
+            case .success:
+                handler(true)
+            case .failure(let err):
+                showToast(err.localizedDescription)
+                handler(false)
+            }
+        }
+    }
+}
+
+
+// MARK: 麦位管理 - 管理员
+extension LNRoomViewModel {
+    var curSeatApplications: [LNRoomSeatApplyItem] {
+        guestStore.state.value.applicants.map {
+            LNRoomSeatApplyItem(userNo: $0.userID, avatar: $0.avatarURL,
+                                name: $0.userName, gender: .unknow,
+                                time: 0, category: "")
+        }
+    }
+    
+    private func setupApplyObservers() {
+        guestStore.state.subscribe().receive(on: DispatchQueue.main).sink { [weak self] state in
+            guard let self else { return }
+            LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomSeatApplyChanged() }
+        }.store(in: &cancellables)
+    }
+    
+    // 踢人下麦
+    func kickUserOffSeat(uid: String, handler: @escaping (Bool) -> Void) {
+        seatStore.kickUserOutOfSeat(userID: uid) { result in
+            switch result {
+            case .success:
+                handler(true)
+            case .failure(let err):
+                showToast(err.localizedDescription)
+                handler(false)
+            }
+        }
+    }
+    
+    // 接受上麦申请
+    func acceptSeatApply(uid: String, handler: @escaping (Bool) -> Void) {
+        guestStore.acceptApplication(userID: uid) { result in
+            switch result {
+            case .success:
+                handler(true)
+            case .failure(let err):
+                showToast(err.localizedDescription)
+                handler(false)
+            }
+        }
+    }
+    
+    // 拒绝上麦申请
+    func rejectSeatApply(uid: String, handler: @escaping (Bool) -> Void) {
+        guestStore.rejectApplication(userID: uid) { result in
+            switch result {
+            case .success:
+                handler(true)
+            case .failure(let err):
+                showToast(err.localizedDescription)
+                handler(false)
+            }
+        }
+    }
+    
+    // 邀请上麦
+    func inviteUserToSeat(uid: String, handler: @escaping (Bool) -> Void) {
+        guestStore.inviteToSeat(userID: uid, timeout: 0, extraInfo: nil) { result in
+            switch result {
+            case .success:
+                handler(true)
+            case .failure(let err):
+                showToast(err.localizedDescription)
+                handler(false)
+            }
+        }
+    }
+    
+    // 关闭麦位
+    func lockSeat(num: Int, handler: @escaping (Bool) -> Void) {
+        seatStore.lockSeat(seatIndex: num) { result in
+            switch result {
+            case .success:
+                handler(true)
+            case .failure(let err):
+                showToast(err.localizedDescription)
+                handler(false)
+            }
+        }
+    }
+    
+    // 解锁麦位
+    func unlockSeat(num: Int, handler: @escaping (Bool) -> Void) {
+        seatStore.unlockSeat(seatIndex: num) { result in
+            switch result {
+            case .success:
+                handler(true)
+            case .failure(let err):
+                showToast(err.localizedDescription)
+                handler(false)
+            }
+        }
+    }
+}
+
+// MARK: 麦克风管理 - 普通用户
+extension LNRoomViewModel {
+    // 关闭自己麦克风
+    func muteMySeat() {
+        seatStore.muteMicrophone()
+    }
+    
+    // 打开自己麦克风
+    func unmuteMySeat(handler: @escaping (Bool) -> Void) {
+        seatStore.unmuteMicrophone { result in
+            switch result {
+            case .success:
+                handler(true)
+            case .failure(let err):
+                showToast(err.localizedDescription)
+                handler(false)
+            }
+        }
+    }
+}
+
+// MARK: 麦克风管理 - 管理员
+extension LNRoomViewModel {
+    // 禁止某人的麦克风
+    func muteSeat(uid: String, handler: @escaping (Bool) -> Void) {
+        seatStore.closeRemoteMicrophone(userID: uid) { result in
+            switch result {
+            case .success:
+                handler(true)
+            case .failure(let err):
+                showToast(err.localizedDescription)
+                handler(false)
+            }
+        }
+    }
+    
+    // 解锁某人麦克风
+    func unmuteSeat(uid: String, handler: @escaping (Bool) -> Void) {
+        seatStore.openRemoteMicrophone(userID: uid, policy: .unlockOnly) { result in
+            switch result {
+            case .success:
+                handler(true)
+            case .failure(let err):
+                showToast(err.localizedDescription)
+                handler(false)
+            }
+        }
+    }
+}
+
+// MARK: 公屏
+private extension BarrageType {
+    var toMessageType: LNRoomMessageItemType {
+        switch self {
+        case .text:
+                .chat
+        case .custom:
+                .system
+        default:
+                .unknown
+        }
+    }
+}
+extension LNRoomViewModel {
+    var curMessage: [LNRoomMessageItem] {
+        messageStore.state.value.messageList.suffix(maxMessageCount).map {
+            LNRoomMessageItem(type: $0.messageType.toMessageType, sender: $0.sender.userID, text: $0.textContent)
+        }
+    }
+    
+    private func setupMessageObservers() {
+        messageStore.state.subscribe().receive(on: DispatchQueue.main).sink { [weak self] state in
+            guard let self else { return }
+            guard messageRefreshTask == nil else {
+                return
+            }
+            messageRefreshTask = LNDelayTask.perform(delay: self.messageRefreshInterval, task: { [weak self] in
+                guard let self else { return }
+                
+                messageRefreshTask = nil
+                LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomMessageChanged() }
+            })
+        }.store(in: &cancellables)
+    }
+    
+    func sendMessage(text: String, handler: @escaping (Bool) -> Void) {
+        messageStore.sendTextMessage(text: text, extensionInfo: nil) { result in
+            switch result {
+            case .success:
+                handler(true)
+            case .failure(let err):
+                showToast(err.localizedDescription)
+                handler(false)
+            }
+        }
+    }
+}
+
+// MARK: 房间信息
+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 roomInfo.update(state.currentLive) {
+                LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomInfoChanged() }
+            }
+        }.store(in: &cancellables)
+    }
+    
+    func updateRoomInfo(name: String, cover: String, handler: @escaping (Bool) -> Void) {
+        var info = LiveInfo()
+        info.liveID = roomInfo.liveID
+        info.liveName = name
+        info.coverURL = cover
+        LNRoomManager.shared.liveListStore.updateLiveInfo(info, modifyFlag: [.liveName, .coverURL]) { result in
+            switch result {
+            case .success:
+                handler(true)
+            case .failure(let err):
+                showToast(err.localizedDescription)
+                handler(false)
+            }
+        }
     }
 }

+ 1 - 1
Podfile

@@ -12,7 +12,7 @@ target 'Gami' do
   pod 'TUIChat', :path => "./ThirdParty/TUIKit/TUIChat" # IM 消息
   pod 'TIMPush' # 离线推送
 #  pod 'RTCRoomEngine' # 语音通话 被移到了 RTCRoomEngine
-  pod 'AtomicXCore' # 直播间功能
+  pod 'AtomicXCore' # 直播间功能(会包含 RTCRoomEngine)
   
   pod 'DoraemonKit', :configurations => ['Debug']
   

+ 1 - 1
Podfile.lock

@@ -83,6 +83,6 @@ SPEC CHECKSUMS:
   TXIMSDK_Plus_iOS_XCFramework: 3b435eae84c639f35ae8dc9c8b92c399a8b0a67f
   TXLiteAVSDK_Professional: 985619fe6e60bff462b6f496559ae0eb2a128960
 
-PODFILE CHECKSUM: 6c4b5268291b664ec8d8aa020ea0be82cbc60def
+PODFILE CHECKSUM: 6cf7c50b420510a1e0bc25866532ae09ec08e15b
 
 COCOAPODS: 1.16.2