Browse Source

feat: 补充下单引导界面逻辑

陈文艺 2 weeks ago
parent
commit
037f91c6fb

+ 1 - 0
Lanu.xcodeproj/project.pbxproj

@@ -367,6 +367,7 @@
 				Views/Room/Join/Manage/LNRoomManageSeatListView.swift,
 				Views/Room/Join/Manage/LNRoomManageSeatTabView.swift,
 				"Views/Room/LNCommonAlertView+Room.swift",
+				Views/Room/LNRoomOrderGuideView.swift,
 				Views/Room/LNRoomSheetMenu.swift,
 				Views/Room/LNRoomViewController.swift,
 				Views/Room/Message/LNRoomChatMessageCell.swift,

+ 2 - 2
Lanu/Common/Views/LNCommonInputPanel.swift

@@ -23,8 +23,8 @@ class LNCommonInputPanel: LNPopupView {
         setupViews()
     }
     
-    override func popup(_ targetView: UIView? = nil) {
-        super.popup(targetView)
+    override func popup(_ targetView: UIView? = nil, animated: Bool = true) {
+        super.popup(targetView, animated: animated)
         
         textView.becomeFirstResponder()
     }

+ 21 - 12
Lanu/Common/Views/LNPopupView.swift

@@ -42,7 +42,7 @@ class LNPopupView: UIView {
         setupViews()
     }
     
-    func popup(_ targetView: UIView? = nil) {
+    func popup(_ targetView: UIView? = nil, animated: Bool = true) {
         let parentView: UIView? = if let window = targetView as? UIWindow {
             window
         } else if let view = targetView?.viewController?.view {
@@ -63,22 +63,31 @@ class LNPopupView: UIView {
         
         moveToShowupPosition()
         window?.endEditing(true)
-        backgroundView.backgroundColor = .clear
-        UIView.animate(withDuration: 0.25) {
-            self.backgroundView.backgroundColor = .black.withAlphaComponent(0.4)
-            self.layoutIfNeeded()
+        if animated {
+            backgroundView.backgroundColor = .clear
+            UIView.animate(withDuration: 0.25) {
+                self.backgroundView.backgroundColor = .black.withAlphaComponent(0.4)
+                self.layoutIfNeeded()
+            }
+        } else {
+            backgroundView.backgroundColor = .black.withAlphaComponent(0.4)
+            layoutIfNeeded()
         }
     }
     
-    func dismiss() {
+    func dismiss(animated: Bool = true) {
         endEditing(true)
         moveToHiddenPosition()
-        UIView.animate(withDuration: 0.25) {
-            self.backgroundView.backgroundColor = .clear
-            self.layoutIfNeeded()
-        } completion: { [weak self] _ in
-            guard let self else { return }
-            self.removeFromSuperview()
+        if animated {
+            UIView.animate(withDuration: 0.25) {
+                self.backgroundView.backgroundColor = .clear
+                self.layoutIfNeeded()
+            } completion: { [weak self] _ in
+                guard let self else { return }
+                self.removeFromSuperview()
+            }
+        } else {
+            removeFromSuperview()
         }
     }
     

+ 299 - 0
Lanu/Localizable.xcstrings

@@ -8534,6 +8534,305 @@
         }
       }
     },
+    "A00373" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Next"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Lanjut"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "下一步"
+          }
+        }
+      }
+    },
+    "A00374" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Step 1: Request to Join Mic"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Langkah 1: Minta Naik Mic"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "步骤1:申请上麦"
+          }
+        }
+      }
+    },
+    "A00375" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Tap [Join Mic] and wait for the host's invitation."
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Klik tombol [Naik Mic] dan tunggu undangan host."
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "点击【上麦互动】按钮,等待主持人邀请。"
+          }
+        }
+      }
+    },
+    "A00376" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Step 2: Share Your Needs"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Langkah 2: Sampaikan Kebutuhan"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "步骤2:说明需求"
+          }
+        }
+      }
+    },
+    "A00378" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Share your preferred game, gender, price, or other requirements."
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Sampaikan preferensi game, gender, harga, atau kondisi lainnya."
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "说明你偏好的游戏、性别、价格或其他条件。"
+          }
+        }
+      }
+    },
+    "A00379" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "game"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "game"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "游戏"
+          }
+        }
+      }
+    },
+    "A00380" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "gender"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "gender"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "性别"
+          }
+        }
+      }
+    },
+    "A00381" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Step 3: Playmates"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Langkah 3: Teman Main"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "步骤3:陪玩师"
+          }
+        }
+      }
+    },
+    "A00382" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Up to 8 playmates can introduce themselves one by one."
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "8 Teman Main perkenalan satu per satu."
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "最多 8 位陪玩师会依次进行介绍。"
+          }
+        }
+      }
+    },
+    "A00383" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Step 4: Place an Order"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Langkah 4: Pesan"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "步骤4:下单"
+          }
+        }
+      }
+    },
+    "A00384" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Choose a playmate and place your order."
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Pilih 1 teman main lalu pesan."
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "选择一位陪玩师并下单。"
+          }
+        }
+      }
+    },
+    "A00385" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "price"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "harga"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "价格"
+          }
+        }
+      }
+    },
+    "A00386" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Leave"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Keluar"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "离开"
+          }
+        }
+      }
+    },
     "B00001" : {
       "extractionState" : "manual",
       "localizations" : {

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

@@ -91,6 +91,8 @@ class LNGameMateSkillVO: Decodable, Equatable {
         && lhs.unit == rhs.unit
         && lhs.cover == rhs.cover
     }
+    
+    init() { }
 }
 
 @AutoCodable

+ 30 - 12
Lanu/Views/Room/Bottom/Join/LNRoomJoinMenuView.swift

@@ -14,6 +14,7 @@ private enum LNRoomJoinMenuState {
     case normal
     case appling
     case reviewing
+    case onMic
 }
 
 
@@ -28,12 +29,10 @@ class LNRoomJoinMenuView: UIView {
     private var curState: LNRoomJoinMenuState = .normal {
         didSet {
             switch curState {
-            case .normal:
-                toBeOffMic()
-            case .appling:
-                toBePending()
-            case .reviewing:
-                toBeRequests()
+            case .normal: toBeOffMic()
+            case .appling: toBePending()
+            case .reviewing: toBeRequests()
+            case .onMic: toBeOnMic()
             }
         }
     }
@@ -76,12 +75,18 @@ extension LNRoomJoinMenuView: LNRoomViewModelNotify {
     private func checkState() {
         guard let roomSession else { return }
         
-        if roomSession.mySeatInfo?.index == .host {
-            curState = .reviewing
-        } else if roomSession.waitingForSeat {
-            curState = .appling
-        } else {
-            curState = .normal
+        if roomSession.mySeatInfo == nil { // 还没上麦
+            if roomSession.waitingForSeat {
+                curState = .appling
+            } else {
+                curState = .normal
+            }
+        } else { // 在麦上
+            if roomSession.mySeatInfo?.index == .host {
+                curState = .reviewing
+            } else {
+                curState = .onMic
+            }
         }
         
         if roomSession.seatApplyCount == 0 {
@@ -94,6 +99,8 @@ extension LNRoomJoinMenuView: LNRoomViewModelNotify {
 
 extension LNRoomJoinMenuView {
     private func setupViews() {
+        LNRoomOrderGuideView.joinView = self
+
         backgroundColor = .fill.withAlphaComponent(0.15)
         layer.cornerRadius = 15
         onTap { [weak self] in
@@ -114,6 +121,9 @@ extension LNRoomJoinMenuView {
                 panel.update(roomSession)
                 panel.popup()
                 break
+            case .onMic:
+                roomSession.leaveSeat { _ in }
+                break
             }
         }
         snp.makeConstraints { make in
@@ -188,4 +198,12 @@ extension LNRoomJoinMenuView {
         seatIc.isHidden = true
         titleLabel.text = .init(key: "A00324")
     }
+    
+    private func toBeOnMic() {
+        redCountView.isHidden = false
+        
+        bgImageView.isHidden = true
+        seatIc.isHidden = true
+        titleLabel.text = .init(key: "A00386")
+    }
 }

+ 2 - 2
Lanu/Views/Room/Create/LNRoomNameInputPanel.swift

@@ -25,8 +25,8 @@ class LNRoomNameInputPanel: LNPopupView {
         nameInputField.text = name
     }
     
-    override func popup(_ targetView: UIView? = nil) {
-        super.popup(targetView)
+    override func popup(_ targetView: UIView? = nil, animated: Bool = true) {
+        super.popup(targetView, animated: animated)
         
         nameInputField.becomeFirstResponder()
     }

+ 367 - 0
Lanu/Views/Room/LNRoomOrderGuideView.swift

@@ -0,0 +1,367 @@
+//
+//  LNRoomOrderGuideView.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/22.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNRoomOrderGuideView: UIView {
+    static weak var joinView: UIView?
+    static weak var mainSeatView: UIView?
+    static weak var playmateView: UIView?
+    static weak var orderCardView: UIView?
+    private static weak var currentGuide: LNRoomOrderGuideView?
+
+    private enum Step: Int, CaseIterable {
+        case join
+        case demand
+        case playmate
+        case order
+
+        var cardSize: CGSize {
+            switch self {
+            case .join, .playmate, .order:
+                .init(width: 314, height: self == .join ? 136 : 120)
+            case .demand:
+                .init(width: 320, height: 136)
+            }
+        }
+
+        var focusInset: UIEdgeInsets {
+            switch self {
+            case .join:
+                .init(top: -4, left: -4, bottom: -4, right: -4)
+            case .demand:
+                .init(top: -8, left: -4, bottom: -8, right: -4)
+            case .playmate:
+                .init(top: 0, left: -4, bottom: 0, right: -4)
+            case .order:
+                .init(top: -2, left: -2, bottom: -2, right: -2)
+            }
+        }
+
+        var focusCornerRadius: CGFloat {
+            switch self {
+            case .join:
+                15
+            case .demand, .playmate:
+                12
+            case .order:
+                12
+            }
+        }
+
+        var progressText: String {
+            "(\(rawValue + 1)/\(Self.allCases.count))"
+        }
+    }
+
+    private let dimLayer = CAShapeLayer()
+    private let lineLayer = CAShapeLayer()
+
+    private let dotView = UIView()
+    private let dotInnerView = UIView()
+
+    private let cardView = UIView()
+    private let cardGlowView = UIImageView(image: .primary_6)
+    private let titleLabel = UILabel()
+    private let descLabel = UILabel()
+    private let nextButton = UIButton(type: .custom)
+    private let nextTitleLabel = UILabel()
+    private let progressLabel = UILabel()
+    
+    private weak var profileCard: LNRoomProfileCardPanel?
+
+    private var currentStep: Step = .join {
+        didSet {
+            updateTexts()
+            setNeedsLayout()
+        }
+    }
+
+    static func show(_ container: UIView) {
+        guard currentGuide == nil else { return }
+
+        let guide = LNRoomOrderGuideView()
+        currentGuide = guide
+        container.addSubview(guide)
+        guide.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+        }
+    }
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+
+        setupViews()
+        updateTexts()
+    }
+
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    override func layoutSubviews() {
+        super.layoutSubviews()
+
+        dimLayer.frame = bounds
+        lineLayer.frame = bounds
+        updateLayout()
+    }
+}
+
+private extension LNRoomOrderGuideView {
+    var currentAnchorView: UIView? {
+        switch currentStep {
+        case .join:
+            Self.joinView
+        case .demand:
+            Self.mainSeatView
+        case .playmate:
+            Self.playmateView
+        case .order:
+            Self.orderCardView
+        }
+    }
+
+    func setupViews() {
+        backgroundColor = .clear
+
+        dimLayer.fillColor = UIColor.black.withAlphaComponent(0.8).cgColor
+        dimLayer.fillRule = .evenOdd
+        layer.addSublayer(dimLayer)
+
+        lineLayer.strokeColor = UIColor.fill.cgColor
+        lineLayer.lineWidth = 1
+        lineLayer.fillColor = UIColor.clear.cgColor
+        layer.addSublayer(lineLayer)
+
+        dotView.backgroundColor = UIColor.fill.withAlphaComponent(0.2)
+        dotView.layer.cornerRadius = 7.5
+        addSubview(dotView)
+
+        dotInnerView.backgroundColor = .fill
+        dotInnerView.layer.cornerRadius = 3.5
+        dotView.addSubview(dotInnerView)
+        dotInnerView.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+            make.width.height.equalTo(7)
+        }
+
+        cardView.backgroundColor = .fill
+        cardView.layer.cornerRadius = 20
+        cardView.clipsToBounds = true
+        addSubview(cardView)
+        cardView.frame = .init(x: 0, y: 0, width: 50, height: 50)
+
+        cardGlowView.alpha = 0.6
+        cardGlowView.contentMode = .scaleToFill
+        cardView.addSubview(cardGlowView)
+        cardGlowView.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+        }
+
+        titleLabel.font = .heading_h3
+        titleLabel.textColor = .text_5
+        titleLabel.numberOfLines = 0
+        cardView.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.top.equalToSuperview().offset(20)
+            make.leading.equalToSuperview().offset(15)
+            make.trailing.equalToSuperview().offset(-15)
+        }
+
+        descLabel.font = .body_m
+        descLabel.textColor = .text_4
+        descLabel.numberOfLines = 0
+        cardView.addSubview(descLabel)
+        descLabel.snp.makeConstraints { make in
+            make.top.equalTo(titleLabel.snp.bottom).offset(8)
+            make.leading.equalToSuperview().offset(15)
+            make.trailing.equalToSuperview().offset(-15)
+        }
+
+        nextButton.setBackgroundImage(.primary_7, for: .normal)
+        nextButton.layer.cornerRadius = 16.5
+        nextButton.clipsToBounds = true
+        nextButton.addAction(UIAction(handler: { [weak self] _ in
+            self?.handleNext()
+        }), for: .touchUpInside)
+        cardView.addSubview(nextButton)
+        nextButton.snp.makeConstraints { make in
+            make.trailing.equalToSuperview().offset(-15)
+            make.bottom.equalToSuperview().offset(-13)
+            make.height.equalTo(33)
+        }
+
+        let buttonStack = UIStackView()
+        buttonStack.axis = .horizontal
+        buttonStack.spacing = 2
+        buttonStack.alignment = .center
+        buttonStack.isUserInteractionEnabled = false
+        nextButton.addSubview(buttonStack)
+        buttonStack.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(12)
+            make.trailing.equalToSuperview().offset(-12)
+            make.centerY.equalToSuperview()
+        }
+
+        nextTitleLabel.font = .heading_h3
+        nextTitleLabel.textColor = .text_1
+        buttonStack.addArrangedSubview(nextTitleLabel)
+
+        progressLabel.font = .body_xs
+        progressLabel.textColor = .text_1
+        buttonStack.addArrangedSubview(progressLabel)
+    }
+
+    func updateTexts() {
+        nextTitleLabel.text = .init(key: "A00373")
+        progressLabel.text = currentStep.progressText
+
+        switch currentStep {
+        case .join:
+            titleLabel.text = .init(key: "A00374")
+            descLabel.attributedText = nil
+            descLabel.text = .init(key: "A00375")
+        case .demand:
+            titleLabel.text = .init(key: "A00376")
+            descLabel.attributedText = highlightedDemandText()
+        case .playmate:
+            titleLabel.text = .init(key: "A00381")
+            descLabel.attributedText = nil
+            descLabel.text = .init(key: "A00382")
+        case .order:
+            titleLabel.text = .init(key: "A00383")
+            descLabel.attributedText = nil
+            descLabel.text = .init(key: "A00384")
+        }
+    }
+
+    func highlightedDemandText() -> NSAttributedString {
+        let text = String(key: "A00378")
+        let attributed = NSMutableAttributedString(
+            string: text,
+            attributes: [
+                .font: UIFont.body_m,
+                .foregroundColor: UIColor.text_4
+            ]
+        )
+        [String(key: "A00379"), String(key: "A00380"), String(key: "A00385")].forEach { target in
+            let nsText = text as NSString
+            let range = nsText.range(of: target)
+            if range.location != NSNotFound {
+                attributed.addAttributes([
+                    .foregroundColor: UIColor.text_6
+                ], range: range)
+            }
+        }
+        return attributed
+    }
+
+    func updateLayout() {
+        guard let focusRect = currentFocusRect() else {
+            dimLayer.path = UIBezierPath(rect: bounds).cgPath
+            lineLayer.path = nil
+            dotView.isHidden = true
+            cardView.isHidden = true
+            return
+        }
+
+        cardView.isHidden = false
+        dotView.isHidden = false
+
+        let cardFrame = currentCardFrame(for: focusRect)
+        cardView.frame = cardFrame
+
+        let maskPath = UIBezierPath(rect: bounds)
+        maskPath.append(UIBezierPath(roundedRect: focusRect, cornerRadius: currentStep.focusCornerRadius))
+        dimLayer.path = maskPath.cgPath
+
+        let isCardAbove = cardFrame.maxY <= focusRect.minY
+        let dotCenter = CGPoint(
+            x: focusRect.midX,
+            y: isCardAbove ? focusRect.minY - 7.5 : focusRect.maxY + 7.5
+        )
+        dotView.bounds = CGRect(x: 0, y: 0, width: 15, height: 15)
+        dotView.center = dotCenter
+
+        let path = UIBezierPath()
+        path.move(to: CGPoint(x: dotCenter.x, y: isCardAbove ? cardFrame.maxY : cardFrame.minY))
+        path.addLine(to: dotCenter)
+        lineLayer.path = path.cgPath
+    }
+
+    func currentFocusRect() -> CGRect? {
+        guard let anchor = currentAnchorView,
+              anchor.window != nil else {
+            return nil
+        }
+
+        let rect = anchor.convert(anchor.bounds, to: self)
+        let inset = currentStep.focusInset
+        let focusedRect = rect.inset(by: inset)
+        let visibleRect = focusedRect.intersection(bounds)
+        guard !visibleRect.isNull, !visibleRect.isEmpty else { return nil }
+        return visibleRect
+    }
+
+    func currentCardFrame(for focusRect: CGRect) -> CGRect {
+        let size = currentStep.cardSize
+        switch currentStep {
+        case .join:
+            let x = bounds.width - 12 - size.width
+            let y = focusRect.minY - 56 - size.height
+            return .init(x: x, y: max(100, y), width: size.width, height: size.height)
+        case .demand:
+            let x = bounds.width - 12 - size.width
+            return .init(x: x, y: focusRect.maxY + 61, width: size.width, height: size.height)
+        case .playmate:
+            return .init(x: (bounds.width - size.width) / 2, y: focusRect.maxY + 59, width: size.width, height: size.height)
+        case .order:
+            let y = focusRect.minY - 66 - size.height
+            return .init(x: 10, y: max(100, y), width: size.width, height: size.height)
+        }
+    }
+
+    func handleNext() {
+        switch currentStep {
+        case .join:
+            currentStep = .demand
+        case .demand:
+            currentStep = .playmate
+        case .playmate:
+            showOrderStepIfPossible()
+        case .order:
+            dismiss()
+            profileCard?.dismiss(animated: false)
+        }
+    }
+
+    func showOrderStepIfPossible() {
+        let panel = LNRoomProfileCardPanel()
+        panel.toBeExample()
+        panel.popup(superview, animated: false)
+        superview?.bringSubviewToFront(self)
+        profileCard = panel
+        
+        currentStep = .order
+    }
+
+    func dismiss() {
+        if Self.currentGuide === self {
+            Self.currentGuide = nil
+        }
+
+        UIView.animate(withDuration: 0.2, animations: {
+            self.alpha = 0
+        }, completion: { [weak self] _ in
+            self?.removeFromSuperview()
+        })
+    }
+}

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

@@ -54,6 +54,33 @@ class LNRoomProfileCardPanel: LNPopupView {
         }
     }
     
+    func toBeExample() {
+        avatarView.sd_setImage(with: URL(string: myUserInfo.avatar))
+        nameLabel.text = "Super Beautiful Girl"
+        genderView.update(.female, 18)
+        userIdLabel.text = "ID 12345678"
+        
+        var skills: [LNGameMateSkillVO] = []
+        for type in LNGameMateManager.shared.curGameTypes {
+            for skill in type.children {
+                let item = LNGameMateSkillVO()
+                item.icon = skill.icon
+                item.name = skill.name
+                item.cover = skill.icon
+                item.price = 1
+                item.unit = "Match"
+                skills.append(item)
+                if skills.count >= 3 {
+                    break
+                }
+            }
+            if skills.count >= 3 {
+                break
+            }
+        }
+        skillSection.update(myUserInfo, skills)
+    }
+    
     required init?(coder: NSCoder) {
         fatalError("init(coder:) has not been implemented")
     }

+ 9 - 1
Lanu/Views/Room/Profile/LNRoomProfileSkillView.swift

@@ -36,10 +36,18 @@ class LNRoomProfileSkillView: UIView {
     }
     
     func update(_ userInfo: LNUserProfileVO, _ skills: [LNGameMateSkillVO]) {
-        for skill in skills {
+        stackView.arrangedSubviews.forEach {
+            stackView.removeArrangedSubview($0)
+            $0.removeFromSuperview()
+        }
+
+        for (index, skill) in skills.enumerated() {
             let itemView = LNRoomProfileSkillItemView()
             itemView.update(userInfo, skill)
             stackView.addArrangedSubview(itemView)
+            if index == 0 {
+                LNRoomOrderGuideView.orderCardView = itemView
+            }
         }
     }
     

+ 8 - 0
Lanu/Views/Room/Seats/LNRoomSeatsView.swift

@@ -58,12 +58,18 @@ extension LNRoomSeatsView {
         let stackView = UIStackView()
         stackView.distribution = .equalSpacing
         stackView.axis = .horizontal
+        LNRoomOrderGuideView.mainSeatView = stackView
         
         // 主持人位置
         stackView.addArrangedSubview(hostSeat)
         
         // 中间介绍界面
         let orderView = UIView()
+        orderView.onTap { [weak self] in
+            guard let self else { return }
+            guard let view = viewController?.view else { return }
+            LNRoomOrderGuideView.show(view)
+        }
         stackView.addArrangedSubview(orderView)
         
         let orderTag = UIImageView()
@@ -76,6 +82,7 @@ extension LNRoomSeatsView {
         }
         
         let textView = UIView()
+        textView.isUserInteractionEnabled = false
         textView.alpha = 0.8
         orderTag.addSubview(textView)
         textView.snp.makeConstraints { make in
@@ -114,6 +121,7 @@ extension LNRoomSeatsView {
         seatsView.columns = 4
         seatsView.spacing = 20
         seatsView.itemDistribution = .equalSpacing
+        LNRoomOrderGuideView.playmateView = seatsView
         
         for seat in LNRoomSeatNum.allCases {
             if case .mic1 ... .mic8 = seat {

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

@@ -132,6 +132,9 @@ extension LNRoomViewModel {
             }
             if hasChanged {
                 seatsInfo = newSeats
+                if waitingForSeat, mySeatInfo != nil {
+                    waitingForSeat = false
+                }
                 LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomSeatsChanged() }
             }