فهرست منبع

feat: 陪玩中心逻辑

陈文艺 2 ماه پیش
والد
کامیت
409d545d09
73فایلهای تغییر یافته به همراه4521 افزوده شده و 424 حذف شده
  1. 22 8
      Lanu.xcodeproj/project.pbxproj
  2. 22 0
      Lanu/Assets.xcassets/Profile/Skill/ic_skill_edit.imageset/Contents.json
  3. BIN
      Lanu/Assets.xcassets/Profile/Skill/ic_skill_edit.imageset/ic_skill_edit@2x.png
  4. BIN
      Lanu/Assets.xcassets/Profile/Skill/ic_skill_edit.imageset/ic_skill_edit@3x.png
  5. 6 0
      Lanu/Assets.xcassets/common/Wallet/Contents.json
  6. 0 0
      Lanu/Assets.xcassets/common/Wallet/ic_wallet.imageset/Contents.json
  7. BIN
      Lanu/Assets.xcassets/common/Wallet/ic_wallet.imageset/ic_wallet@2x.png
  8. BIN
      Lanu/Assets.xcassets/common/Wallet/ic_wallet.imageset/ic_wallet@3x.png
  9. 22 0
      Lanu/Assets.xcassets/common/Wallet/ic_wallet_with_bg.imageset/Contents.json
  10. 0 0
      Lanu/Assets.xcassets/common/Wallet/ic_wallet_with_bg.imageset/ic_wallet@2x.png
  11. 0 0
      Lanu/Assets.xcassets/common/Wallet/ic_wallet_with_bg.imageset/ic_wallet@3x.png
  12. 22 0
      Lanu/Assets.xcassets/common/ic_settings.imageset/Contents.json
  13. BIN
      Lanu/Assets.xcassets/common/ic_settings.imageset/ic_settings@2x.png
  14. BIN
      Lanu/Assets.xcassets/common/ic_settings.imageset/ic_settings@3x.png
  15. 22 0
      Lanu/Assets.xcassets/common/ic_tag.imageset/Contents.json
  16. BIN
      Lanu/Assets.xcassets/common/ic_tag.imageset/ic_tag@2x.png
  17. BIN
      Lanu/Assets.xcassets/common/ic_tag.imageset/ic_tag@3x.png
  18. 0 4
      Lanu/Common/Extension/NSObject+Extension.swift
  19. 62 0
      Lanu/Common/Extension/UIScrollView+Extension.swift
  20. 19 2
      Lanu/Common/Views/Base/LNViewController.swift
  21. 1 0
      Lanu/Common/Views/LNCircleProgressView.swift
  22. 0 0
      Lanu/Common/Views/LNGenderView.swift
  23. 2 4
      Lanu/Common/Views/LNPopupView.swift
  24. 1 0
      Lanu/Common/Views/Selection/LNDatePickerPanel.swift
  25. 226 0
      Lanu/Common/Views/Selection/LNHourRangePickerPanel.swift
  26. 5 12
      Lanu/Common/Views/Selection/LNSingleSelectionPanel.swift
  27. 650 6
      Lanu/Localizable.xcstrings
  28. 206 40
      Lanu/Manager/GameMate/LNGameMateManager.swift
  29. 135 23
      Lanu/Manager/GameMate/Network/LNGameMateResponse.swift
  30. 141 20
      Lanu/Manager/GameMate/Network/LNHttpManager+GameMate.swift
  31. 0 1
      Lanu/Manager/Profile/LNProfileManager.swift
  32. 6 1
      Lanu/Views/Game/Category/LNGameCategoryListView.swift
  33. 8 1
      Lanu/Views/Game/Category/LNGameCategoryListViewController.swift
  34. 2 39
      Lanu/Views/Game/Join/Input/BaseInfo/LNJoinUsInputInfoView.swift
  35. 2 2
      Lanu/Views/Game/Join/Input/Example/LNJoinUsPhotoExamplePanel.swift
  36. 2 2
      Lanu/Views/Game/Join/Input/Example/LNJoinUsVoiceExamplePanel.swift
  37. 1 0
      Lanu/Views/Game/Join/Input/LNJoinUsHeaderView.swift
  38. 20 43
      Lanu/Views/Game/Join/Input/LNJoinUsViewController.swift
  39. 21 57
      Lanu/Views/Game/Join/Input/SkillInfo/LNJoinUsSkillFieldsEditView.swift
  40. 11 35
      Lanu/Views/Game/Join/Review/LNJoinUsReviewViewController.swift
  41. 7 7
      Lanu/Views/Game/MateFilter/LNGameFilterPanel.swift
  42. 23 2
      Lanu/Views/Game/MateFilter/LNGameMateFilterPanel.swift
  43. 0 4
      Lanu/Views/Game/MateList/LNGameMateListView.swift
  44. 487 0
      Lanu/Views/Game/OrderCenter/LNGameMateCenterViewController.swift
  45. 291 0
      Lanu/Views/Game/OrderCenter/LNOrderAcceptSettingsViewController.swift
  46. 191 0
      Lanu/Views/Game/OrderCenter/Skill/LNSkillCreateViewController.swift
  47. 98 0
      Lanu/Views/Game/OrderCenter/Skill/LNSkillCreatedReviewViewController.swift
  48. 189 0
      Lanu/Views/Game/OrderCenter/Skill/LNSkillEditViewController.swift
  49. 134 0
      Lanu/Views/Game/OrderCenter/Skill/LNSkillFieldsEditView.swift
  50. 260 0
      Lanu/Views/Game/OrderCenter/Skill/LNSkillManagerViewController.swift
  51. 138 0
      Lanu/Views/Game/OrderCenter/Visitors/LNVisitorItemCell.swift
  52. 147 0
      Lanu/Views/Game/OrderCenter/Visitors/LNVisitorsViewController.swift
  53. 19 10
      Lanu/Views/Game/Skill/Edit/LNSkillFieldBaseEditView.swift
  54. 7 5
      Lanu/Views/Game/Skill/Edit/LNSkillFieldMultiLineEditView.swift
  55. 11 7
      Lanu/Views/Game/Skill/Edit/LNSkillFieldPhotoEditView.swift
  56. 187 0
      Lanu/Views/Game/Skill/Edit/LNSkillFieldPriceEditView.swift
  57. 21 6
      Lanu/Views/Game/Skill/Edit/LNSkillFieldSelectionEditView.swift
  58. 9 7
      Lanu/Views/Game/Skill/Edit/LNSkillFieldSingleLineEditView.swift
  59. 9 8
      Lanu/Views/Game/Skill/Edit/LNSkillFieldVoiceEditView.swift
  60. 171 27
      Lanu/Views/Game/Skill/LNSkillBottomMenuView.swift
  61. 26 17
      Lanu/Views/Game/Skill/LNSkillDetailViewController.swift
  62. 133 0
      Lanu/Views/Game/Skill/LNSkillSettingMenu.swift
  63. 2 0
      Lanu/Views/Game/Skill/LNSkillVoiceBarView.swift
  64. 8 2
      Lanu/Views/Home/LNHomeGameMatePanel.swift
  65. 1 1
      Lanu/Views/Order/Create/LNCreateOrderPanel.swift
  66. 1 1
      Lanu/Views/Order/OrderQR/LNOrderGenerateQRCodePanel.swift
  67. 1 1
      Lanu/Views/Order/OrderQR/LNOrderShareImageGenerator.swift
  68. 1 1
      Lanu/Views/Order/OrderQR/LNOrderSkillListPanel.swift
  69. 268 0
      Lanu/Views/Profile/Mine/LNMineOrderRecordView.swift
  70. 32 3
      Lanu/Views/Profile/Mine/LNMineViewController.swift
  71. 3 3
      Lanu/Views/Profile/Post/LNPostShareImageGenerator.swift
  72. 8 11
      Lanu/Views/Profile/Post/LNPostShareViewController.swift
  73. 1 1
      Lanu/Views/Wallet/LNPurchasePanel.swift

+ 22 - 8
Lanu.xcodeproj/project.pbxproj

@@ -53,6 +53,7 @@
 				"Common/Extension/UIButton+Theme.swift",
 				"Common/Extension/UIColor+Extension.swift",
 				"Common/Extension/UIImage+Extension.swift",
+				"Common/Extension/UIScrollView+Extension.swift",
 				"Common/Extension/UITableView+Extension.swift",
 				"Common/Extension/UIView+Extension.swift",
 				"Common/Extension/URL+Extension.swift",
@@ -69,7 +70,6 @@
 				Common/Views/Base/LNFakeNaviBar.swift,
 				Common/Views/Base/LNNavigationController.swift,
 				Common/Views/Base/LNViewController.swift,
-				Common/Views/Gender/LNGenderView.swift,
 				Common/Views/ImagePreview/LNImagePreviewCell.swift,
 				Common/Views/ImagePreview/LNImagePreviewController.swift,
 				Common/Views/ImageUpload/LNFeedbackImageUploadView.swift,
@@ -79,6 +79,7 @@
 				Common/Views/LNCaptchaInputView.swift,
 				Common/Views/LNCircleProgressView.swift,
 				Common/Views/LNCountrySelectPanel.swift,
+				Common/Views/LNGenderView.swift,
 				Common/Views/LNOnlineView.swift,
 				Common/Views/LNPopupView.swift,
 				Common/Views/LNSortedEditView.swift,
@@ -90,6 +91,7 @@
 				Common/Views/ScrollView/LNNestedScrollView.swift,
 				Common/Views/ScrollView/LNNestedTableView.swift,
 				Common/Views/Selection/LNDatePickerPanel.swift,
+				Common/Views/Selection/LNHourRangePickerPanel.swift,
 				Common/Views/Selection/LNMultiSelectionPanel.swift,
 				Common/Views/Selection/LNSingleSelectionPanel.swift,
 				Common/Views/StackView/LNAutoFillStackView.swift,
@@ -169,14 +171,8 @@
 				Views/Game/Join/Input/LNJoinUsHeaderView.swift,
 				Views/Game/Join/Input/LNJoinUsInputFieldGroupView.swift,
 				Views/Game/Join/Input/LNJoinUsViewController.swift,
-				Views/Game/Join/Input/SkillInfo/InputViews/LNJoinUsMultiLineTextInputView.swift,
-				Views/Game/Join/Input/SkillInfo/InputViews/LNJoinUsPhotoInputView.swift,
-				Views/Game/Join/Input/SkillInfo/InputViews/LNJoinUsSelectionInputView.swift,
-				Views/Game/Join/Input/SkillInfo/InputViews/LNJoinUsSingleLineTextInputView.swift,
-				Views/Game/Join/Input/SkillInfo/InputViews/LNJoinUsSkillInfoInputFieldView.swift,
-				Views/Game/Join/Input/SkillInfo/InputViews/LNJoinUsVoiceInputView.swift,
-				Views/Game/Join/Input/SkillInfo/LNJoinUsInputSkillInfoView.swift,
 				Views/Game/Join/Input/SkillInfo/LNJoinUsSelectSkillView.swift,
+				Views/Game/Join/Input/SkillInfo/LNJoinUsSkillFieldsEditView.swift,
 				Views/Game/Join/Review/LNJoinUsReviewViewController.swift,
 				Views/Game/MateFilter/LNGameCategoryFilterPanel.swift,
 				Views/Game/MateFilter/LNGameFilterPanel.swift,
@@ -185,10 +181,27 @@
 				Views/Game/MateList/LNGameMateListMenuView.swift,
 				Views/Game/MateList/LNGameMateListView.swift,
 				Views/Game/MateList/LNGameMateListViewController.swift,
+				Views/Game/OrderCenter/LNGameMateCenterViewController.swift,
+				Views/Game/OrderCenter/LNOrderAcceptSettingsViewController.swift,
+				Views/Game/OrderCenter/Skill/LNSkillCreatedReviewViewController.swift,
+				Views/Game/OrderCenter/Skill/LNSkillCreateViewController.swift,
+				Views/Game/OrderCenter/Skill/LNSkillEditViewController.swift,
+				Views/Game/OrderCenter/Skill/LNSkillFieldsEditView.swift,
+				Views/Game/OrderCenter/Skill/LNSkillManagerViewController.swift,
+				Views/Game/OrderCenter/Visitors/LNVisitorItemCell.swift,
+				Views/Game/OrderCenter/Visitors/LNVisitorsViewController.swift,
+				Views/Game/Skill/Edit/LNSkillFieldBaseEditView.swift,
+				Views/Game/Skill/Edit/LNSkillFieldMultiLineEditView.swift,
+				Views/Game/Skill/Edit/LNSkillFieldPhotoEditView.swift,
+				Views/Game/Skill/Edit/LNSkillFieldPriceEditView.swift,
+				Views/Game/Skill/Edit/LNSkillFieldSelectionEditView.swift,
+				Views/Game/Skill/Edit/LNSkillFieldSingleLineEditView.swift,
+				Views/Game/Skill/Edit/LNSkillFieldVoiceEditView.swift,
 				Views/Game/Skill/LNSkillBottomMenuView.swift,
 				Views/Game/Skill/LNSkillDetailViewController.swift,
 				Views/Game/Skill/LNSkillNaviBarView.swift,
 				Views/Game/Skill/LNSkillPhotosView.swift,
+				Views/Game/Skill/LNSkillSettingMenu.swift,
 				Views/Game/Skill/LNSkillTagView.swift,
 				Views/Game/Skill/LNSkillUserInfoView.swift,
 				Views/Game/Skill/LNSkillVoiceBarView.swift,
@@ -267,6 +280,7 @@
 				Views/Profile/Edit/LNEditProfileViewController.swift,
 				Views/Profile/Edit/LNEditVoicePanel.swift,
 				Views/Profile/Mine/LNMineFunctionView.swift,
+				Views/Profile/Mine/LNMineOrderRecordView.swift,
 				Views/Profile/Mine/LNMineQRCodeShareView.swift,
 				Views/Profile/Mine/LNMineUserInfoView.swift,
 				Views/Profile/Mine/LNMineViewController.swift,

+ 22 - 0
Lanu/Assets.xcassets/Profile/Skill/ic_skill_edit.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/Profile/Skill/ic_skill_edit.imageset/ic_skill_edit@2x.png


BIN
Lanu/Assets.xcassets/Profile/Skill/ic_skill_edit.imageset/ic_skill_edit@3x.png


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

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

+ 0 - 0
Lanu/Assets.xcassets/common/ic_wallet.imageset/Contents.json → Lanu/Assets.xcassets/common/Wallet/ic_wallet.imageset/Contents.json


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


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


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

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

+ 0 - 0
Lanu/Assets.xcassets/common/ic_wallet.imageset/ic_wallet@2x.png → Lanu/Assets.xcassets/common/Wallet/ic_wallet_with_bg.imageset/ic_wallet@2x.png


+ 0 - 0
Lanu/Assets.xcassets/common/ic_wallet.imageset/ic_wallet@3x.png → Lanu/Assets.xcassets/common/Wallet/ic_wallet_with_bg.imageset/ic_wallet@3x.png


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

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

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


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


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

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

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


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


+ 0 - 4
Lanu/Common/Extension/NSObject+Extension.swift

@@ -9,7 +9,6 @@ import Foundation
 import Combine
 
 
-// 1. 定义私有容器类(引用类型,解决桥接问题)
 private final class CancellableBagContainer {
     /// 线程安全的订阅容器
     var cancellables: Set<AnyCancellable> = []
@@ -17,10 +16,7 @@ private final class CancellableBagContainer {
     let lock = NSLock()
 }
 
-// 2. 关联对象Key(私有静态变量)
 private var cancellableBagKey: UInt8 = 0
-
-// 3. 安全的NSObject扩展
 extension NSObject {
     /// 用于存储Combine订阅的容器,自动与对象生命周期绑定
     var bag: Set<AnyCancellable> {

+ 62 - 0
Lanu/Common/Extension/UIScrollView+Extension.swift

@@ -0,0 +1,62 @@
+//
+//  UIScrollView+Extension.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/1/25.
+//
+
+import Foundation
+import UIKit
+
+private var scrollViewOriginContentInsetKey: UInt8 = 0
+
+extension UIScrollView: LNKeyboardNotify {
+    private var originContentInset: UIEdgeInsets {
+        get {
+            objc_getAssociatedObject(self, &scrollViewOriginContentInsetKey) as? UIEdgeInsets ?? .zero
+        }
+        set {
+            objc_setAssociatedObject(self, &scrollViewOriginContentInsetKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+        }
+    }
+    
+    func adjustKeyoard() {
+        LNEventDeliver.addObserver(self)
+    }
+    
+    func onKeybaordWillShow(curInput: UIView?, keyboardHeight: CGFloat) {
+        guard let curInput, curInput.isDescendant(of: self) else { return }
+        
+        originContentInset = contentInset
+        
+        let offset = 20 + keyboardHeight
+        
+        let editViewY = curInput.convert(curInput.bounds, to: nil).maxY
+        let distance = UIScreen.main.bounds.height - editViewY
+        if distance > offset {
+            return
+        }
+        
+        var remain = offset - distance
+        let curOffset = contentOffset
+        let availableOffset = contentSize.height + contentInset.bottom - curOffset.y - bounds.height
+        if availableOffset > remain {
+            setContentOffset(.init(x: curOffset.x, y: curOffset.y + remain), animated: true)
+            return
+        }
+        remain = remain - availableOffset
+        
+        
+        var curInset = contentInset
+        curInset.bottom += remain
+        contentInset = curInset
+        
+        setContentOffset(.init(x: curOffset.x, y: curOffset.y + availableOffset + remain), animated: true)
+    }
+    
+    func onKeybaordWillHide(curInput: UIView?) {
+        guard let curInput, curInput.isDescendant(of: self) else { return }
+        
+        contentInset = originContentInset
+    }
+}

+ 19 - 2
Lanu/Common/Views/Base/LNViewController.swift

@@ -7,6 +7,7 @@
 
 import Foundation
 import UIKit
+import SnapKit
 
 
 class LNViewController: UIViewController {
@@ -15,6 +16,8 @@ class LNViewController: UIViewController {
     var navigationBarColor: UIColor = .white
     var customBack: (() -> Void)?
     
+    private let fakeView = UIView()
+    
     override func viewDidLoad() {
         super.viewDidLoad()
         
@@ -36,6 +39,13 @@ class LNViewController: UIViewController {
             }
         }), for: .touchUpInside)
         navigationItem.leftBarButtonItem = UIBarButtonItem(customView: button)
+        
+        view.addSubview(fakeView)
+        fakeView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.bottom.equalTo(view.snp.top)
+            make.height.equalTo(UIView.navigationBarHeight + UIView.statusBarHeight)
+        }
     }
     
     func setRightButton(_ view: UIView?) {
@@ -46,6 +56,12 @@ class LNViewController: UIViewController {
         }
     }
     
+    override func viewDidAppear(_ animated: Bool) {
+        super.viewDidAppear(animated)
+        
+        fakeView.backgroundColor = view.backgroundColor
+    }
+    
     override func viewWillAppear(_ animated: Bool) {
         super.viewWillAppear(animated)
         
@@ -53,15 +69,16 @@ class LNViewController: UIViewController {
         
         if showNavigationBar,
            let navBar = navigationController?.navigationBar {
-            // 1. 配置外观(iOS 15+ 必须用 UINavigationBarAppearance)
             let appearance = UINavigationBarAppearance()
             appearance.backgroundColor = navigationBarColor // 导航栏背景色
             appearance.shadowColor = .clear // 去除底部阴影线
             if navigationBarColor == .clear {
                 appearance.configureWithTransparentBackground()
+                navBar.backgroundColor = .clear
+            } else {
+                navBar.backgroundColor = navigationBarColor
             }
             
-            // 2. 应用外观设置(iOS 15+ 需要设置 scrollEdgeAppearance 和 standardAppearance)
             navBar.scrollEdgeAppearance = appearance // 滚动到顶部时的外观(如列表顶部)
             navBar.standardAppearance = appearance   // 常规状态的外观
             navBar.compactAppearance = appearance

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

@@ -35,6 +35,7 @@ class LNCircleProgressView: UIView {
     func setProgress(_ progress: CGFloat, animated: Bool = true) {
         let targetProgress = max(0, min(1, progress))
         
+        progressLayer.removeAllAnimations()
         if animated {
             // 创建进度动画
             let animation = CABasicAnimation(keyPath: "strokeEnd")

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


+ 2 - 4
Lanu/Common/Views/LNPopupView.swift

@@ -39,11 +39,8 @@ class LNPopupView: UIView {
         }
         guard let parentView else { return }
         
+        frame = parentView.bounds
         parentView.addSubview(self)
-        snp.makeConstraints { make in
-            make.edges.equalToSuperview()
-        }
-        moveToHiddenPosition()
         layoutIfNeeded()
         
         moveToShowupPosition()
@@ -98,6 +95,7 @@ extension LNPopupView {
         container.layer.cornerRadius = 20
         container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
         addSubview(container)
+        moveToHiddenPosition()
         
         let dismissView = UIView()
         container.addSubview(dismissView)

+ 1 - 0
Lanu/Common/Views/Selection/LNDatePickerPanel.swift

@@ -52,6 +52,7 @@ extension LNDatePickerPanel {
         confirm.setTitleColor(.text_1, for: .normal)
         confirm.setBackgroundImage(.primary_8, for: .normal)
         confirm.layer.cornerRadius = 23.5
+        confirm.titleLabel?.font = .heading_h3
         confirm.clipsToBounds = true
         confirm.addAction(UIAction(handler: { [weak self] _ in
             guard let self else { return }

+ 226 - 0
Lanu/Common/Views/Selection/LNHourRangePickerPanel.swift

@@ -0,0 +1,226 @@
+//
+//  LNHourRangePickerPanel.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/1/23.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNHourRangePickerPanel: LNPopupView {
+    private let titleLabel = UILabel()
+    private let descLabel = UILabel()
+    
+    private let fromPicker = UIPickerView()
+    private let toPicker = UIPickerView()
+    
+    var isRelative: Bool = true
+    var handler: ((Int, Int) -> Void)?
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setupViews()
+    }
+    
+    func setTitles(_ title: String, desc: String? = nil) {
+        titleLabel.text = title
+        descLabel.text = desc
+        descLabel.isHidden = desc?.isEmpty != false
+    }
+    
+    func setDefault(from: Int, to: Int) {
+        fromPicker.selectRow(from, inComponent: 0, animated: false)
+        let toIndex = if isRelative {
+            to - from - 1
+        } else {
+            to
+        }
+        toPicker.selectRow(toIndex, inComponent: 0, animated: false)
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNHourRangePickerPanel: UIPickerViewDataSource, UIPickerViewDelegate {
+    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
+        if pickerView == toPicker, isRelative {
+            23
+        } else {
+            24
+        }
+    }
+    
+    func numberOfComponents(in pickerView: UIPickerView) -> Int {
+        1
+    }
+    
+    func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
+        let itemView = view as? LNHourRangeItemView ?? LNHourRangeItemView()
+        
+        if pickerView == fromPicker {
+            itemView.titleLabel.text = String(format: "%02d:00", row)
+        } else if pickerView == toPicker {
+            if isRelative {
+                let from = fromPicker.selectedRow(inComponent: 0)
+                itemView.titleLabel.text = String(format: "%02d:00", (row + from + 1) % 24)
+            } else {
+                itemView.titleLabel.text = String(format: "%02d:00", row)
+            }
+        }
+        
+        return itemView
+    }
+    
+    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
+        if pickerView == fromPicker, isRelative {
+            toPicker.reloadComponent(0)
+        }
+    }
+    
+    func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat {
+        52
+    }
+}
+
+extension LNHourRangePickerPanel {
+    private func setupViews() {
+        let header = UIView()
+        container.addSubview(header)
+        header.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalToSuperview()
+        }
+        
+        let stackView = UIStackView()
+        stackView.axis = .vertical
+        stackView.spacing = 4
+        header.addSubview(stackView)
+        stackView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(22)
+            make.verticalEdges.equalToSuperview().inset(9)
+        }
+        
+        titleLabel.font = .heading_h3
+        titleLabel.textColor = .text_5
+        titleLabel.textAlignment = .center
+        stackView.addArrangedSubview(titleLabel)
+        
+        descLabel.font = .body_s
+        descLabel.textColor = .text_4
+        descLabel.textAlignment = .center
+        descLabel.numberOfLines = 0
+        stackView.addArrangedSubview(descLabel)
+        
+        let pickerView = UIView()
+        container.addSubview(pickerView)
+        pickerView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(20)
+            make.top.equalTo(header.snp.bottom)
+            make.height.equalTo(150)
+        }
+        
+        let textView = UIView()
+        pickerView.addSubview(textView)
+        textView.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+        }
+        
+        let toLabel = UILabel()
+        toLabel.font = .body_l
+        toLabel.textColor = .text_5
+        toLabel.text = .init(key: "B00081")
+        toLabel.textAlignment = .center
+        toLabel.setContentHuggingPriority(.required, for: .horizontal)
+        toLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
+        textView.addSubview(toLabel)
+        toLabel.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(12)
+            make.verticalEdges.equalToSuperview()
+        }
+        
+        fromPicker.delegate = self
+        fromPicker.dataSource = self
+        pickerView.addSubview(fromPicker)
+        fromPicker.snp.makeConstraints { make in
+            make.verticalEdges.equalToSuperview()
+            make.leading.equalToSuperview()
+            make.trailing.equalTo(textView.snp.leading)
+        }
+        
+        toPicker.delegate = self
+        toPicker.dataSource = self
+        pickerView.addSubview(toPicker)
+        toPicker.snp.makeConstraints { make in
+            make.verticalEdges.equalToSuperview()
+            make.trailing.equalToSuperview()
+            make.leading.equalTo(textView.snp.trailing)
+            make.width.equalTo(fromPicker)
+        }
+        
+        let confirm = UIButton()
+        confirm.setTitle(.init(key: "A00002"), for: .normal)
+        confirm.setTitleColor(.text_1, for: .normal)
+        confirm.setBackgroundImage(.primary_8, for: .normal)
+        confirm.layer.cornerRadius = 23.5
+        confirm.titleLabel?.font = .heading_h3
+        confirm.clipsToBounds = true
+        confirm.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            dismiss()
+            let from = fromPicker.selectedRow(inComponent: 0)
+            var to = toPicker.selectedRow(inComponent: 0)
+            if isRelative {
+                to += from + 1
+            }
+            handler?(from, to)
+        }), for: .touchUpInside)
+        container.addSubview(confirm)
+        confirm.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(22)
+            make.bottom.equalToSuperview().offset(commonBottomInset)
+            make.height.equalTo(47)
+            make.top.equalTo(pickerView.snp.bottom).offset(4)
+        }
+        
+        DispatchQueue.main.async { [weak self] in
+            guard let self else { return }
+            fromPicker.subviews.forEach {
+                if $0.subviews.isEmpty {
+                    $0.backgroundColor = .clear
+                }
+            }
+            toPicker.subviews.forEach {
+                if $0.subviews.isEmpty {
+                    $0.backgroundColor = .clear
+                }
+            }
+        }
+    }
+}
+
+
+private class LNHourRangeItemView: UIView {
+    let titleLabel = UILabel()
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        titleLabel.font = .body_xl
+        titleLabel.textColor = .text_5
+        titleLabel.textAlignment = .center
+        addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+        }
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}

+ 5 - 12
Lanu/Common/Views/Selection/LNSingleSelectionPanel.swift

@@ -83,22 +83,15 @@ extension LNSingleSelectionPanel {
             make.top.equalToSuperview()
         }
         
-        let pickerContainer = UIView()
-        container.addSubview(pickerContainer)
-        pickerContainer.snp.makeConstraints { make in
+        pickerView.dataSource = self
+        pickerView.delegate = self
+        container.addSubview(pickerView)
+        pickerView.snp.makeConstraints { make in
             make.horizontalEdges.equalToSuperview()
             make.top.equalTo(headerView.snp.bottom)
             make.height.equalTo(240)
         }
         
-        pickerView.dataSource = self
-        pickerView.delegate = self
-        pickerContainer.addSubview(pickerView)
-        pickerContainer.publisher(for: \.bounds).removeDuplicates().sink { [weak self] newValue in
-            guard let self else { return }
-            pickerView.frame = newValue
-        }.store(in: &bag)
-        
         confirmButton.setTitle(.init(key: "A00223"), for: .normal)
         confirmButton.setTitleColor(.text_1, for: .normal)
         confirmButton.titleLabel?.font = .heading_h3
@@ -114,7 +107,7 @@ extension LNSingleSelectionPanel {
         container.addSubview(confirmButton)
         confirmButton.snp.makeConstraints { make in
             make.horizontalEdges.equalToSuperview().inset(16)
-            make.top.equalTo(pickerContainer.snp.bottom).offset(10)
+            make.top.equalTo(pickerView.snp.bottom).offset(10)
             make.bottom.equalToSuperview().offset(commonBottomInset)
             make.height.equalTo(47)
         }

+ 650 - 6
Lanu/Localizable.xcstrings

@@ -8149,19 +8149,19 @@
         "en" : {
           "stringUnit" : {
             "state" : "translated",
-            "value" : "%@ minimum value: %d"
+            "value" : "%1$@ minimum value: %2$@"
           }
         },
         "id" : {
           "stringUnit" : {
             "state" : "translated",
-            "value" : "%@ minimal harus diisi %d"
+            "value" : "%1$@ minimal harus diisi %2$@"
           }
         },
         "zh-Hans" : {
           "stringUnit" : {
             "state" : "translated",
-            "value" : "%@最小需要填入%d"
+            "value" : "%1$@最小需要填入%2$@"
           }
         }
       }
@@ -8172,19 +8172,19 @@
         "en" : {
           "stringUnit" : {
             "state" : "translated",
-            "value" : "%@ maximum value: %d"
+            "value" : "%1$@ maximum value: %2$@"
           }
         },
         "id" : {
           "stringUnit" : {
             "state" : "translated",
-            "value" : "%@ maksimal harus diisi %d"
+            "value" : "%1$@ maksimal harus diisi %2$@"
           }
         },
         "zh-Hans" : {
           "stringUnit" : {
             "state" : "translated",
-            "value" : "%@最大只能填入%d"
+            "value" : "%1$@最大只能填入%2$@"
           }
         }
       }
@@ -8234,6 +8234,650 @@
           }
         }
       }
+    },
+    "B00071" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Accept Orders"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Terima Pesanan"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "接单中心"
+          }
+        }
+      }
+    },
+    "B00072" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "This Week's Income"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Pendapatan Minggu"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "本周收入"
+          }
+        }
+      }
+    },
+    "B00073" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Today's Exposure"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Eksposur Hari"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "今日曝光"
+          }
+        }
+      }
+    },
+    "B00074" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Recent Visitors"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Pengunjung Baru"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "近期访客"
+          }
+        }
+      }
+    },
+    "B00075" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "My Skills"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Keahlian Saya"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "我的技能"
+          }
+        }
+      }
+    },
+    "B00076" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Skill Management"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Manajemen Keahlian"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "技能管理"
+          }
+        }
+      }
+    },
+    "B00077" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Apply for Skills"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Ajukan Keahlian"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "申请技能"
+          }
+        }
+      }
+    },
+    "B00078" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Order Accept Settings"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Pengaturan Terima Pesanan"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "接单设置"
+          }
+        }
+      }
+    },
+    "B00079" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Order Accept Time"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Waktu Terima Pesanan"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "接单时段"
+          }
+        }
+      }
+    },
+    "B00080" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Order Accept Cycle"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Siklus Terima"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "接单周期"
+          }
+        }
+      }
+    },
+    "B00081" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "To"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Hingga"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "至"
+          }
+        }
+      }
+    },
+    "B00082" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Select Order Accept Time"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Pilih Waktu Terima Pesanan"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "选择接单时段"
+          }
+        }
+      }
+    },
+    "B00083" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Failure to accept orders during set hours will result in negative impacts"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Tidak terima pesanan di jam yang ditetapkan akan ada dampak negatif"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "所设置时间内不接单将产生负面影响"
+          }
+        }
+      }
+    },
+    "B00084" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Next Day"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Hari Berikutnya"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "次日"
+          }
+        }
+      }
+    },
+    "B00085" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Set Order Accept Cycle"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Atur Siklus Terima Pesanan"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "设置接单周期"
+          }
+        }
+      }
+    },
+    "B00086" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Select at least one"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Pilih setidaknya satu"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "至少选择一项"
+          }
+        }
+      }
+    },
+    "B00087" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Opennig..."
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Buka..."
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "营业中..."
+          }
+        }
+      }
+    },
+    "B00088" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Continuously recommended to more users"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Direkomendasikan terus ke lebih banyak pengguna"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "持续推荐给更多用户"
+          }
+        }
+      }
+    },
+    "B00089" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Open For Business"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Buka Untuk Bisnis"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "开启营业"
+          }
+        }
+      }
+    },
+    "B00090" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Activate the status of grabbing orders"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Aktifkan status ambil pesanan"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "开启抢单状态"
+          }
+        }
+      }
+    },
+    "B00091" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Recent Visitors"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Pengunjung Terbaru"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "最近访客"
+          }
+        }
+      }
+    },
+    "B00092" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Visited you"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Mengunjungi mu"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "访问了你"
+          }
+        }
+      }
+    },
+    "B00093" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Skill Info"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Info Keterampilan"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "技能信息"
+          }
+        }
+      }
+    },
+    "B00094" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Please select"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Silakan pilih"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "请选择"
+          }
+        }
+      }
+    },
+    "B00095" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Setting"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Atur"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "设置"
+          }
+        }
+      }
+    },
+    "B00096" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Set as Default Main Skill"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Atur jadi Skill Utama Default"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "设为默认主技能"
+          }
+        }
+      }
+    },
+    "B00097" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Enter Unit Price"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Masukkan Harga"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "输入单价"
+          }
+        }
+      }
+    },
+    "B00098" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "No optional items available, this item cannot be edited"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Tidak ada item pilihan, item ini tidak dapat diedit"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "可选项目为空,该项目不可编辑"
+          }
+        }
+      }
     }
   },
   "version" : "1.1"

+ 206 - 40
Lanu/Manager/GameMate/LNGameMateManager.swift

@@ -9,24 +9,24 @@ import Foundation
 
 
 protocol LNGameMateManagerNotify {
-    func onUserGameMateInfoChanged(info: LNGameMateInfoResponse)
+    func onGameMateManagerInfoChanged()
     func onGameTypesChanged()
 }
 extension LNGameMateManagerNotify {
-    func onUserGameMateInfoChanged(info: LNGameMateInfoResponse) { }
+    func onGameMateManagerInfoChanged() { }
     func onGameTypesChanged() { }
 }
 
-var myGameMateInfo: LNGameMateInfoResponse? {
+var myGameMateInfo: LNGameMateManagerInfo? {
     LNGameMateManager.shared.myGameMateInfo
 }
 
-
 class LNGameMateManager {
     static let shared = LNGameMateManager()
     private(set) var curGameTypes: [LNGameTypeItemVO] = []
+    private(set) var gameFilterMap: [String: [LNSkillFilterField]] = [:]
     
-    fileprivate var myGameMateInfo: LNGameMateInfoResponse?
+    private(set) var myGameMateInfo: LNGameMateManagerInfo?
     
     static let gameSelectionMaxCount = 5
     
@@ -39,6 +39,10 @@ class LNGameMateManager {
         return nil
     }
     
+    func gameFilter(for code: String) -> [LNSkillFilterField] {
+        gameFilterMap[code] ?? []
+    }
+    
     private init() {
         LNEventDeliver.addObserver(self)
     }
@@ -47,42 +51,19 @@ class LNGameMateManager {
 extension LNGameMateManager {
     func getUserSkills(uid: String, queue: DispatchQueue = .main,
                        handler: @escaping ([LNGameMateSkillVO]?) -> Void) {
-        LNHttpManager.shared.getUserSkills(uid: uid) { [weak self] res, err in
+        LNHttpManager.shared.getUserSkills(uid: uid) { res, err in
             guard err == nil, let res else {
                 queue.asyncIfNotGlobal {
                     handler(nil)
                 }
                 return
             }
-            if let self, uid.isMyUid, let info = myGameMateInfo {
-                info.skills = res.list
-                notifyUserGameMateInfoChanged()
-            }
             queue.asyncIfNotGlobal {
                 handler(res.list)
             }
         }
     }
     
-    func getUserGameMateInfo(uid: String, queue: DispatchQueue = .main,
-                             handler: @escaping (LNGameMateInfoResponse?) -> Void) {
-        LNHttpManager.shared.getGameMateInfo(uid: uid) { [weak self] res, err in
-            guard err == nil, let res else {
-                queue.asyncIfNotGlobal {
-                    handler(nil)
-                }
-                return
-            }
-            if let self, uid.isMyUid {
-                myGameMateInfo = res
-                notifyUserGameMateInfoChanged()
-            }
-            queue.asyncIfNotGlobal {
-                handler(res)
-            }
-        }
-    }
-    
     func getSkillDetail(skillId: String, queue: DispatchQueue = .main,
                         handler: @escaping (LNGameMateSkillDetailVO?) -> Void) {
         LNHttpManager.shared.getSkillDetail(skillId: skillId) { res, err in
@@ -119,6 +100,8 @@ extension LNGameMateManager {
                 }
                 return
             }
+            reloadGameFilterConfig()
+            
             curGameTypes = res.list
             queue.asyncIfNotGlobal {
                 handler(res.list)
@@ -126,6 +109,17 @@ extension LNGameMateManager {
             notifyGameTypesChanged()
         }
     }
+    
+    func reloadGameFilterConfig() {
+        LNHttpManager.shared.getGameFilterConfig { [weak self] list, err in
+            guard let self else { return }
+            guard let list else { return }
+            gameFilterMap.removeAll()
+            for item in list.list {
+                gameFilterMap[item.code] = item.filterFields
+            }
+        }
+    }
 }
 
 extension LNGameMateManager {
@@ -187,8 +181,82 @@ extension LNGameMateManager {
         }
     }
     
-    func getSkillField(id: String, queue: DispatchQueue = .main, handler: @escaping (LNJoinUsSkillInfoVO?) -> Void) {
-        LNHttpManager.shared.getJoinGameMateSkillField(id: id) { res, err in
+    func getCreateSkillFields(id: String, queue: DispatchQueue = .main, handler: @escaping (LNCreateSkillInputFieldsVO?) -> Void) {
+        LNHttpManager.shared.getCreateSkillFields(id: id) { res, err in
+            queue.asyncIfNotGlobal {
+                if let res {
+                    guard !res.fields.isEmpty else {
+                        showToast(.init(key: "B00060"))
+                        handler(nil)
+                        return
+                    }
+                    guard res.isAvailable else {
+                        showToast(.init(key: "B00058"))
+                        handler(nil)
+                        return
+                    }
+                }
+                handler(res)
+            }
+            if let err {
+                showToast(err.errorDesc)
+            }
+        }
+    }
+    
+    func createSkill(info: LNCreateSkillFieldsInfo,
+                     queue: DispatchQueue = .main,
+                     handler: @escaping (Bool) -> Void)
+    {
+        LNHttpManager.shared.createSkill(info: info) { err in
+            queue.asyncIfNotGlobal {
+                handler(err == nil)
+            }
+        }
+    }
+}
+
+// MARK: 陪玩师管理
+extension LNGameMateManager {
+    func getGameMateManagerInfo(
+        queue: DispatchQueue = .main,
+        handler: ((LNGameMateManagerInfo?) -> Void)? = nil)
+    {
+        LNHttpManager.shared.getGameMateManagerInfo { [weak self] info, err in
+            queue.asyncIfNotGlobal {
+                handler?(info)
+            }
+            if let err {
+                showToast(err.errorDesc)
+            } else {
+                guard let self else { return }
+                myGameMateInfo = info
+                notifyUserGameMateManagerInfoChanged()
+            }
+        }
+    }
+    
+    func enableGameMate(open: Bool,
+                        queue: DispatchQueue = .main,
+                        handler: @escaping (Bool) -> Void) {
+        LNHttpManager.shared.enableGameMate(open: open) { [weak self] err in
+            queue.asyncIfNotGlobal {
+                handler(err == nil)
+            }
+            if let err {
+                showToast(err.errorDesc)
+            } else {
+                guard let self else { return }
+                getGameMateManagerInfo()
+            }
+        }
+    }
+    
+    func getOrderAccetpConfig(
+        queue: DispatchQueue = .main,
+        handler: @escaping (LNOrderAcceptConfig?) -> Void)
+    {
+        LNHttpManager.shared.getOrderAccetpConfig { res, err in
             queue.asyncIfNotGlobal {
                 handler(res)
             }
@@ -198,32 +266,130 @@ extension LNGameMateManager {
         }
     }
     
-    func setJoinGameMateSkillInfos(info: LNJoinUsInputInfo,
-                                   queue: DispatchQueue = .main,
-                                   handler: @escaping (Bool) -> Void)
+    func setOrderAcceptConfig(
+        config: LNOrderAcceptConfig,
+        queue: DispatchQueue = .main,
+        handler: @escaping (Bool) -> Void)
     {
-        LNHttpManager.shared.setJoinGameMateSkillInfos(info: info) { err in
+        LNHttpManager.shared.setOrderAcceptConfig(config: config) { err in
             queue.asyncIfNotGlobal {
                 handler(err == nil)
             }
+            if let err {
+                showToast(err.errorDesc)
+            }
+        }
+    }
+    
+    func getMySkillList(
+        queue: DispatchQueue = .main,
+        handler: @escaping (LNMySkillListResponse?) -> Void)
+    {
+        LNHttpManager.shared.getMySkillList { res, err in
+            queue.asyncIfNotGlobal {
+                handler(res)
+            }
+            if let err {
+                showToast(err.errorDesc)
+            }
+        }
+    }
+    
+    func getSkillSwitchInfo(id: String,
+                            queue: DispatchQueue = .main,
+                            handler: @escaping (LNSkillSwitchResponse?) -> Void) {
+        LNHttpManager.shared.getSkillSwitchInfo(id: id) { res, err in
+            queue.asyncIfNotGlobal {
+                handler(res)
+            }
+            if let err {
+                showToast(err.errorDesc)
+            }
+        }
+    }
+    
+    func enableSkill(skillId: String, open: Bool,
+                     queue: DispatchQueue = .main,
+                     handler: @escaping (Bool) -> Void) {
+        LNHttpManager.shared.enableSkill(skillId: skillId, open: open) { err in
+            queue.asyncIfNotGlobal {
+                handler(err == nil)
+            }
+            if let err {
+                showToast(err.errorDesc)
+            } else {
+                LNProfileManager.shared.reloadMyProfile()
+            }
+        }
+    }
+    
+    func setAsMainSkill(skillId: String, asMain: Bool,
+                        queue: DispatchQueue = .main,
+                        handler: @escaping (Bool) -> Void) {
+        LNHttpManager.shared.setAsMainSkill(skillId: skillId, asMain: asMain) { err in
+            queue.asyncIfNotGlobal {
+                handler(err == nil)
+            }
+            if let err {
+                showToast(err.errorDesc)
+            } else {
+                LNProfileManager.shared.reloadMyProfile()
+            }
+        }
+    }
+    
+    func getVisitorsList(next: String? = nil,
+                         queue: DispatchQueue = .main,
+                         handler: @escaping ([LNVisitorItemVO]?, String?) -> Void) {
+        LNHttpManager.shared.getVisitorsList(next: next ?? "", size: 30) { res, err in
+            queue.asyncIfNotGlobal {
+                handler(res?.list, res?.next)
+            }
+            if let err {
+                showToast(err.errorDesc)
+            }
+        }
+    }
+    
+    func getSkillEditFields(id: String,
+                            queue: DispatchQueue = .main,
+                            handler: @escaping (LNSkillEditFieldsResponse?) -> Void) {
+        LNHttpManager.shared.getSkillEditFields(id: id) { res, err in
+            queue.asyncIfNotGlobal {
+                handler(res)
+            }
+            if let err {
+                showToast(err.errorDesc)
+            }
+        }
+    }
+    
+    func commitSkillEdit(info: LNEditSkillFieldsInfo,
+                         queue: DispatchQueue = .main,
+                         handler: @escaping (Bool) -> Void) {
+        LNHttpManager.shared.commitSkillEdit(info: info) { err in
+            queue.asyncIfNotGlobal {
+                handler(err == nil)
+            }
+            if let err {
+                showToast(err.errorDesc)
+            }
         }
     }
 }
 
 extension LNGameMateManager: LNAccountManagerNotify {
     func onUserLogout() {
-        myGameMateInfo = nil
     }
     
     func onUserLogin() {
-        getUserGameMateInfo(uid: myUid) { _ in }
+        getGameMateManagerInfo()
     }
 }
 
 extension LNGameMateManager {
-    private func notifyUserGameMateInfoChanged() {
-        guard let curInfo = myGameMateInfo else { return }
-        LNEventDeliver.notifyEvent { ($0 as? LNGameMateManagerNotify)?.onUserGameMateInfoChanged(info: curInfo) }
+    private func notifyUserGameMateManagerInfoChanged() {
+        LNEventDeliver.notifyEvent { ($0 as? LNGameMateManagerNotify)?.onGameMateManagerInfoChanged() }
     }
     
     private func notifyGameTypesChanged() {

+ 135 - 23
Lanu/Manager/GameMate/Network/LNGameMateResponse.swift

@@ -165,36 +165,36 @@ class LNJoinUsBaseInfoVO: Decodable {
 }
 
 @AutoCodable
-class LNJoinUsFieldValidateLenLimite: Decodable {
+class LNSkillFieldValidateLenLimit: Decodable {
     var min: Int = 0
     var max: Int = 0
 }
 
 @AutoCodable
-class LNJoinUsFieldValidateNumLimite: Decodable {
+class LNSkillFieldValidateNumLimit: Decodable {
     var min: Double = 0
     var max: Double = 0
 }
 
 @AutoCodable
-class LNJoinUsFieldValidate: Decodable {
+class LNSkillFieldValidate: Decodable {
     var required: Bool = false
-    var size: LNJoinUsFieldValidateLenLimite?
-    var numLimit: LNJoinUsFieldValidateNumLimite?
-    var arraySize: LNJoinUsFieldValidateLenLimite?
+    var size: LNSkillFieldValidateLenLimit?
+    var numLimit: LNSkillFieldValidateNumLimit?
+    var arraySize: LNSkillFieldValidateLenLimit?
     var regex: String = ""
     
     init() { }
 }
 
-enum LNJoinUsExampleType: Int, Decodable {
+enum LNSkillFieldExampleType: Int, Decodable {
     case photo = 0
     case voice = 1
 }
 
 @AutoCodable
-class LNJoinUsFieldExample: Decodable {
-    var type: LNJoinUsExampleType = .photo
+class LNSkillFieldExample: Decodable {
+    var type: LNSkillFieldExampleType = .photo
     var value: String = ""
     var title: String = ""
     var desc: String = ""
@@ -202,14 +202,14 @@ class LNJoinUsFieldExample: Decodable {
     init() { }
 }
 
-enum LNJoinUsFieldValueType: Int, Decodable {
+enum LNSkillFieldValueType: Int, Decodable {
     case unknown = -1
     case string = 0
     case int = 1
     case double = 2
 }
 
-enum LNJoinUsFieldInputType: Int, Decodable {
+enum LNSkillFieldInputType: Int, Decodable {
     case unknown = -1
     case singleLineText = 0
     case multiLineText = 1
@@ -221,7 +221,7 @@ enum LNJoinUsFieldInputType: Int, Decodable {
     case voice = 7
 }
 
-class LNJoinUsFieldValue: Decodable {
+class LNSkillFieldValue: Decodable {
     private var container: (any SingleValueDecodingContainer)?
     var value: (any Encodable)?
     
@@ -278,19 +278,24 @@ class LNJoinUsFieldValue: Decodable {
 }
 
 @AutoCodable
-class LNJoinUsFieldConstants: Decodable {
+class LNSkillFieldConstants: Decodable {
     var value: String = ""
     var key: String = ""
 }
 
+enum LNSkillEditStaticFieldCode: String {
+    case fieldUnitPrice
+    case fieldUnit
+}
+
 @AutoCodable
-class LNJoinUsField: Decodable {
+class LNSkillEditField: Decodable {
     var fieldCode: String = ""
-    var value: LNJoinUsFieldValue = LNJoinUsFieldValue()
+    var value: LNSkillFieldValue = LNSkillFieldValue()
     var fieldName: String = ""
     var fieldDesc: String = ""
-    var valueClassType: LNJoinUsFieldValueType = .string
-    var type: LNJoinUsFieldInputType = .singleLineText
+    var valueClassType: LNSkillFieldValueType = .string
+    var type: LNSkillFieldInputType = .singleLineText
     var array: Bool = false {
         didSet {
             if array {
@@ -319,15 +324,15 @@ class LNJoinUsField: Decodable {
         }
     }
     var duration: Int = 0
-    var constants: [LNJoinUsFieldConstants] = []
-    var validate: LNJoinUsFieldValidate = LNJoinUsFieldValidate()
-    var example: LNJoinUsFieldExample?
+    var constants: [LNSkillFieldConstants] = []
+    var validate: LNSkillFieldValidate = LNSkillFieldValidate()
+    var example: LNSkillFieldExample?
 }
 
 @AutoCodable
-class LNJoinUsSkillInfoVO: Decodable {
+class LNCreateSkillInputFieldsVO: Decodable {
     var bizCategoryCode: String?
-    var fields: [LNJoinUsField] = []
+    var fields: [LNSkillEditField] = []
     
     var isAvailable: Bool {
         for field in fields {
@@ -348,5 +353,112 @@ class LNJoinUsInputInfosResponse: Decodable {
     var step3Complete: Bool = false
     var setp1: LNJoinUsBindPhoneInfoVO?
     var setp2: LNJoinUsBaseInfoVO?
-    var setp3: LNJoinUsSkillInfoVO?
+    var setp3: LNCreateSkillInputFieldsVO?
+}
+
+enum LNOrderAcceptWeekDay: Int, Decodable, CaseIterable {
+    case sun = 1
+    case mon = 2
+    case tue = 3
+    case wed = 4
+    case thu = 5
+    case fri = 6
+    case sat = 7
+    
+    var text: String {
+        let formatter = DateFormatter()
+        formatter.locale = curLocal
+        return formatter.weekdaySymbols[rawValue - 1]
+    }
+}
+
+@AutoCodable
+class LNOrderAcceptConfig: Decodable {
+    var timeRange: String = ""
+    var weekNums: [LNOrderAcceptWeekDay] = []
+    
+    init() { }
+}
+
+@AutoCodable
+class LNMySkillItemVO: Decodable {
+    var id: String = ""
+    var bizCategoryName: String = ""
+    var categoryIcon: String = ""
+    var price: Double = 0.0
+    var unit: String = ""
+    var open: Bool = false
+    var bizCategoryCode: String = ""
+}
+
+@AutoCodable
+class LNMySkillListResponse: Decodable {
+    var list: [LNMySkillItemVO] = []
+}
+
+@AutoCodable
+class LNGameMateManagerInfo: Decodable {
+    var weekBeanIncome: Double = 0
+    var exposureCountDay: Int = 0
+    var visitorCount: Int = 0
+    var playmateOpen: Bool = false
+}
+
+@AutoCodable
+class LNVisitorItemVO: Decodable {
+    var userNO: String = ""
+    var nickname: String = ""
+    var avatar: String = ""
+    var gender: LNUserGender = .unknow
+    var age: Int = 0
+    var online: Bool = false
+    var visitTime: Int = 0
+    var intro: String = ""
+    
+    init() { }
+}
+
+@AutoCodable
+class LNVisitorsListResponse: Decodable {
+    var list: [LNVisitorItemVO] = []
+    var next: String = ""
+}
+
+@AutoCodable
+class LNSkillEditFieldsResponse: Decodable {
+    var bizCategoryName: String = ""
+    var fields: [LNSkillEditField] = []
+    
+    var skillId = ""
+}
+
+@AutoCodable
+class LNSkillSwitchResponse: Decodable {
+    var open: Bool = false
+    var mainSkill: Bool = false
+}
+
+@AutoCodable
+class LNSkillFilterConstants: Decodable {
+    var key: String = ""
+    var value: String = ""
+    var icon: String = ""
+}
+
+@AutoCodable
+class LNSkillFilterField: Decodable {
+    var name: String = ""
+    var fieldCode: String = ""
+    var constants: [LNSkillFieldConstants] = []
+}
+
+@AutoCodable
+class LNSkillFilterConfig: Decodable {
+    var code: String = ""
+    var filterFields: [LNSkillFilterField] = []
+}
+
+@AutoCodable
+class LNSkillFilterConfigList: Decodable {
+    var list: [LNSkillFilterConfig] = []
 }

+ 141 - 20
Lanu/Manager/GameMate/Network/LNHttpManager+GameMate.swift

@@ -8,6 +8,7 @@
 import Foundation
 
 private let kNetPath_GameMate_Category = "/biz/categorys"
+private let kNetPath_GameMate_Filter = "/biz/categorys/filter/condition"
 
 private let kNetPath_GameMate_List = "/skill/list"
 
@@ -18,10 +19,22 @@ private let kNetPath_GameMate_Score = "/user/playmate/charmStar"
 
 private let kNetPath_GameMate_Search = "/playmate/search"
 
-private let kNetPath_GameMate_Join_Infos = "/playmate/curInfo"
-private let kNetPath_GameMate_Join_Improve_BaseInfo = "/playmate/improve/info"
-private let kNetPath_GameMate_Join_Skill_Field = "/playmate/setp3/info/get"
-private let kNetPath_GameMate_Join_Apply = "/playmate/apply"
+private let kNetPath_GameMate_Join_Infos = "/playmate/apply/curInfo"
+private let kNetPath_GameMate_Join_Improve_BaseInfo = "/playmate/apply/improve/info"
+private let kNetPath_GameMate_Join_Skill_Field = "/playmate/apply/setp3/info/get"
+private let kNetPath_GameMate_Join_Apply = "/playmate/apply/submit"
+
+private let kNetPath_GameMate_Settings_Info = "/playmate/business/info"
+private let kNetPath_GameMate_Settings_Switch = "/playmate/business/switch"
+private let kNetPath_GameMate_Settings_Time = "/playmate/getAcceptSet"
+private let kNetPath_GameMate_Settings_Time_Set = "/playmate/acceptSet"
+private let kNetPath_GameMate_Settings_Skill_List = "/playmate/skill/list"
+
+private let kNetPath_GameMate_Visitors = "/playmate/visitors"
+
+private let kNetPath_GameMate_Skill_Switch = "/skill/switch/get"
+private let kNetPath_GameMate_Skill_Fields = "/skill/editInfo/get"
+private let kNetPath_GameMate_Skill_Edit = "/skill/edit"
 
 enum LNGameMateAgeRange: Int, CaseIterable {
     case all = -1
@@ -82,17 +95,7 @@ class LNGameMateFilter {
     var sortByStar: LNSortedType = .none
     var sortByPrice: LNSortedType = .none
     var priceRange: LNGameMatePriceRange = .all
-}
-
-class LNJoinUsInputFieldInfo {
-    var fieldCode: String = ""
-    var value: (any Encodable)?
-    var duration: Int?
-}
-
-class LNJoinUsInputInfo {
-    var bizCategoryCode = ""
-    var fields: [LNJoinUsInputFieldInfo] = []
+    var categoryFilters: [(fieldCode: String, filterValues: [String])] = []
 }
 
 // MARK: 获取大品类类型
@@ -100,6 +103,10 @@ extension LNHttpManager {
     func getGameCategories(completion: @escaping(LNGameTypeListResponse?, LNHttpError?) -> Void) {
         post(path: kNetPath_GameMate_Category, completion: completion)
     }
+    
+    func getGameFilterConfig(completion: @escaping (LNSkillFilterConfigList?, LNHttpError?) -> Void) {
+        post(path: kNetPath_GameMate_Filter, completion: completion)
+    }
 }
 
 // MARK: 获取陪玩师列表
@@ -107,8 +114,9 @@ extension LNHttpManager {
     func getGameMateList(
         topCategory: String, category: String?,
         filter: LNGameMateFilter, size: Int, next: String,
-        completion: @escaping (LNGameMateListResponse?, LNHttpError?) -> Void) {
-        let params: [String: Any] = [
+        completion: @escaping (LNGameMateListResponse?, LNHttpError?) -> Void)
+    {
+        var params: [String: Any] = [
             "firstCode": topCategory,
             "code": category ?? "",
             "ageRange": filter.ageRange.rawValue,
@@ -121,7 +129,17 @@ extension LNHttpManager {
                 "size": size,
                 "next": next
             ]
-         ]
+        ]
+        var filters: [[String: any Encodable]] = []
+        for item in filter.categoryFilters {
+            filters.append([
+                "fieldCode": item.fieldCode,
+                "filterValues": item.filterValues
+            ])
+        }
+        if !filters.isEmpty {
+            params["categoryFilters"] = filters
+        }
         post(path: kNetPath_GameMate_List,
              params: params,
              completion: completion)
@@ -163,6 +181,17 @@ extension LNHttpManager {
 }
 
 // MARK: 陪玩师申请
+class LNCreateSkillFieldInfo {
+    var fieldCode: String = ""
+    var value: (any Encodable)?
+    var duration: Int?
+}
+
+class LNCreateSkillFieldsInfo {
+    var bizCategoryCode = ""
+    var fields: [LNCreateSkillFieldInfo] = []
+}
+
 extension LNHttpManager {
     func getJoinGameMateInfo(completion: @escaping (LNJoinUsInputInfosResponse?, LNHttpError?) -> Void) {
         post(path: kNetPath_GameMate_Join_Infos, completion: completion)
@@ -182,13 +211,13 @@ extension LNHttpManager {
         ], completion: completion)
     }
     
-    func getJoinGameMateSkillField(id: String, completion: @escaping (LNJoinUsSkillInfoVO?, LNHttpError?) -> Void) {
+    func getCreateSkillFields(id: String, completion: @escaping (LNCreateSkillInputFieldsVO?, LNHttpError?) -> Void) {
         post(path: kNetPath_GameMate_Join_Skill_Field, params: [
             "id": id
         ], completion: completion)
     }
     
-    func setJoinGameMateSkillInfos(info: LNJoinUsInputInfo, completion: @escaping (LNHttpError?) -> Void) {
+    func createSkill(info: LNCreateSkillFieldsInfo, completion: @escaping (LNHttpError?) -> Void) {
         var fields: [[String: Any]] = []
         info.fields.forEach {
             var field: [String: Any] = [
@@ -207,3 +236,95 @@ extension LNHttpManager {
         ], completion: completion)
     }
 }
+
+// MARK: 陪玩师管理
+class LNEditSkillFieldsInfo {
+    var skillId: String = ""
+    var apply: Bool = false
+    var fields: [LNCreateSkillFieldInfo] = []
+}
+
+extension LNHttpManager {
+    func getGameMateManagerInfo(completion: @escaping (LNGameMateManagerInfo?, LNHttpError?) -> Void) {
+        post(path: kNetPath_GameMate_Settings_Info, completion: completion)
+    }
+    
+    func enableGameMate(open: Bool, completion: @escaping (LNHttpError?) -> Void) {
+        post(path: kNetPath_GameMate_Settings_Switch, params: [
+            "open": open
+        ], completion: completion)
+    }
+    
+    func getOrderAccetpConfig(completion: @escaping (LNOrderAcceptConfig?, LNHttpError?) -> Void) {
+        post(path: kNetPath_GameMate_Settings_Time, completion: completion)
+    }
+    
+    func setOrderAcceptConfig(config: LNOrderAcceptConfig, completion: @escaping (LNHttpError?) -> Void) {
+        post(path: kNetPath_GameMate_Settings_Time_Set, params: [
+            "timeRange": config.timeRange,
+            "weekNums": config.weekNums.compactMap({ $0.rawValue })
+        ], completion: completion)
+    }
+    
+    func getMySkillList(onlyOpen: Bool = false, completion: @escaping (LNMySkillListResponse?, LNHttpError?) -> Void) {
+        var params: [String: Any] = [:]
+        if onlyOpen {
+            params["open"] = true
+        }
+        post(path: kNetPath_GameMate_Settings_Skill_List, params: params, completion: completion)
+    }
+    
+    func enableSkill(skillId: String, open: Bool, completion: @escaping (LNHttpError?) -> Void) {
+        post(path: kNetPath_GameMate_Skill_Switch, params: [
+            "skillId": skillId,
+            "open": open
+        ], completion: completion)
+    }
+    
+    func getVisitorsList(next: String, size: Int, completion: @escaping (LNVisitorsListResponse?, LNHttpError?) -> Void) {
+        post(path: kNetPath_GameMate_Visitors, params: [
+            "size": size,
+            "next": next
+        ], completion: completion)
+    }
+    
+    func getSkillEditFields(id: String, completion: @escaping (LNSkillEditFieldsResponse?, LNHttpError?) -> Void) {
+        post(path: kNetPath_GameMate_Skill_Fields, params: [
+            "id": id
+        ], completion: completion)
+    }
+    
+    func commitSkillEdit(info: LNEditSkillFieldsInfo, completion: @escaping (LNHttpError?) -> Void) {
+        var fields: [[String: Any]] = []
+        info.fields.forEach {
+            var field: [String: Any] = [
+                "fieldCode": $0.fieldCode,
+                "value": $0.value as Any
+            ]
+            if let duration = $0.duration {
+                field["duration"] = duration
+            }
+            
+            fields.append(field)
+        }
+        
+        post(path: kNetPath_GameMate_Skill_Edit, params: [
+            "skillId": info.skillId,
+            "apply": info.apply,
+            "fields": fields
+        ], completion: completion)
+    }
+    
+    func getSkillSwitchInfo(id: String, completion: @escaping (LNSkillSwitchResponse?, LNHttpError?) -> Void) {
+        post(path: kNetPath_GameMate_Skill_Switch, params: [
+            "id": id
+        ], completion: completion)
+    }
+    
+    func setAsMainSkill(skillId: String, asMain: Bool, completion: @escaping (LNHttpError?) -> Void) {
+        post(path: kNetPath_GameMate_Skill_Switch, params: [
+            "skillId": skillId,
+            "mainSkill": asMain
+        ], completion: completion)
+    }
+}

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

@@ -112,7 +112,6 @@ extension LNProfileManager {
                 showToast(err.errorDesc)
             } else {
                 reloadMyProfile()
-                LNGameMateManager.shared.getUserGameMateInfo(uid: myUid) { _ in }
             }
             queue.asyncIfNotGlobal {
                 completion(err == nil)

+ 6 - 1
Lanu/Views/Game/Category/LNGameCategoryListView.swift

@@ -12,6 +12,11 @@ import SnapKit
 
 protocol LNGameCategoryListViewDelegate: NSObject {
     func onCategoryListView(view: LNGameCategoryListView, didScrollTo category: LNGameTypeItemVO)
+    func onCategoryListView(view: LNGameCategoryListView, didSelect topCategory: LNGameTypeItemVO, category: LNGameCategoryItemVO)
+}
+extension LNGameCategoryListViewDelegate {
+    func onCategoryListView(view: LNGameCategoryListView, didScrollTo category: LNGameTypeItemVO) { }
+    func onCategoryListView(view: LNGameCategoryListView, didSelect topCategory: LNGameTypeItemVO, category: LNGameCategoryItemVO) { }
 }
 
 
@@ -130,7 +135,7 @@ extension LNGameCategoryListView: UICollectionViewDataSource, UICollectionViewDe
     func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
         let category = categories[indexPath.section]
         let game = category.children[indexPath.row]
-        pushToGameMateList(topCategory: category, category: game, filter: LNGameMateFilter())
+        delegate?.onCategoryListView(view: self, didSelect: category, category: game)
     }
 }
 

+ 8 - 1
Lanu/Views/Game/Category/LNGameCategoryListViewController.swift

@@ -11,8 +11,9 @@ import SnapKit
 
 
 extension UIView {
-    func pushToGameCategoryList() {
+    func pushToGameCategoryList(handler: @escaping ((LNGameTypeItemVO, LNGameCategoryItemVO) -> Void)) {
         let vc = LNGameCategoryListViewController()
+        vc.handler = handler
         navigationController?.pushViewController(vc, animated: true)
     }
 }
@@ -22,6 +23,8 @@ class LNGameCategoryListViewController: LNViewController {
     private let tabView = LNGameCategoryTabView()
     private let listView = LNGameCategoryListView()
     
+    var handler: ((LNGameTypeItemVO, LNGameCategoryItemVO) -> Void)?
+    
     override func viewDidLoad() {
         super.viewDidLoad()
         
@@ -47,6 +50,10 @@ extension LNGameCategoryListViewController: LNGameCategoryListViewDelegate {
     func onCategoryListView(view: LNGameCategoryListView, didScrollTo category: LNGameTypeItemVO) {
         tabView.select(category)
     }
+    
+    func onCategoryListView(view: LNGameCategoryListView, didSelect topCategory: LNGameTypeItemVO, category: LNGameCategoryItemVO) {
+        handler?(topCategory, category)
+    }
 }
 
 extension LNGameCategoryListViewController {

+ 2 - 39
Lanu/Views/Game/Join/Input/BaseInfo/LNJoinUsInputInfoView.swift

@@ -17,9 +17,6 @@ protocol LNJoinUsInputInfoViewDelegate: NSObject {
 
 class LNJoinUsInputInfoView: UIView {
     private let scrollView = UIScrollView()
-    private var scrollViewBottomInset: CGFloat {
-        -commonBottomInset + 32 + 47
-    }
     
     private let avatar = LNUploadImageView()
     
@@ -59,8 +56,6 @@ class LNJoinUsInputInfoView: UIView {
         super.init(frame: frame)
         
         setupViews()
-        
-        LNEventDeliver.addObserver(self)
     }
     
     func update(_ info: LNJoinUsBaseInfoVO) {
@@ -93,39 +88,6 @@ class LNJoinUsInputInfoView: UIView {
     }
 }
 
-extension LNJoinUsInputInfoView: LNKeyboardNotify {
-    func onKeybaordWillShow(curInput: UIView?, keyboardHeight: CGFloat) {
-        guard let curInput, curInput.isDescendant(of: self) else { return }
-        
-        let offset = 20 + keyboardHeight
-        
-        let editViewY = curInput.convert(curInput.bounds, to: nil).maxY
-        let distance = UIScreen.main.bounds.height - editViewY
-        if distance > offset {
-            return
-        }
-        
-        var remain = offset - distance
-        let curOffset = scrollView.contentOffset
-        let availableOffset = scrollView.contentSize.height + scrollView.contentInset.bottom - curOffset.y - scrollView.bounds.height
-        if availableOffset > remain {
-            scrollView.setContentOffset(.init(x: curOffset.x, y: curOffset.y + remain), animated: true)
-            return
-        }
-        remain = remain - availableOffset
-        
-        var curInset = scrollView.contentInset
-        curInset.bottom += remain
-        scrollView.contentInset = curInset
-        
-        scrollView.setContentOffset(.init(x: curOffset.x, y: curOffset.y + availableOffset + remain), animated: true)
-    }
-    
-    func onKeybaordWillHide(curInput: UIView?) {
-        scrollView.contentInset = .init(top: 0, left: 0, bottom: scrollViewBottomInset, right: 0)
-    }
-}
-
 extension LNJoinUsInputInfoView: LNUploadImageViewDelegate {
     func onUploadImageView(view: LNUploadImageView, didUploadImage url: String) {
         checkSaveButton()
@@ -178,7 +140,8 @@ extension LNJoinUsInputInfoView {
         
         scrollView.showsVerticalScrollIndicator = false
         scrollView.showsHorizontalScrollIndicator = false
-        scrollView.contentInset = .init(top: 0, left: 0, bottom: scrollViewBottomInset, right: 0)
+        scrollView.contentInset = .init(top: 0, left: 0, bottom: -commonBottomInset + 32 + 47, right: 0)
+        scrollView.adjustKeyoard()
         addSubview(scrollView)
         scrollView.snp.makeConstraints { make in
             make.horizontalEdges.equalToSuperview().inset(16)

+ 2 - 2
Lanu/Views/Game/Join/Input/Example/LNJoinUsPhotoExamplePanel.swift

@@ -16,7 +16,7 @@ class LNJoinUsPhotoExamplePanel: LNPopupView {
     
     private let imageView = UIImageView()
     
-    private var curItem: LNJoinUsFieldExample?
+    private var curItem: LNSkillFieldExample?
     
     override init(frame: CGRect) {
         super.init(frame: frame)
@@ -24,7 +24,7 @@ class LNJoinUsPhotoExamplePanel: LNPopupView {
         setupViews()
     }
     
-    func update(_ example: LNJoinUsFieldExample) {
+    func update(_ example: LNSkillFieldExample) {
         titleLabel.text = example.title
         descLabel.text = example.desc
         

+ 2 - 2
Lanu/Views/Game/Join/Input/Example/LNJoinUsVoiceExamplePanel.swift

@@ -18,7 +18,7 @@ class LNJoinUsVoiceExamplePanel: LNPopupView {
     private let voiceWaveView = LNVoiceWaveView()
     private let playDurationLabel = UILabel()
     
-    private var curItem: LNJoinUsFieldExample?
+    private var curItem: LNSkillFieldExample?
     
     override init(frame: CGRect) {
         super.init(frame: frame)
@@ -28,7 +28,7 @@ class LNJoinUsVoiceExamplePanel: LNPopupView {
         LNEventDeliver.addObserver(self)
     }
     
-    func update(_ example: LNJoinUsFieldExample) {
+    func update(_ example: LNSkillFieldExample) {
         curItem = example
         
         updateDuration()

+ 1 - 0
Lanu/Views/Game/Join/Input/LNJoinUsHeaderView.swift

@@ -52,6 +52,7 @@ extension LNJoinUsHeaderView {
     private func setupViews() {
         let image = UIImage.icJoinUsHeaderBg
         let bg = UIImageView()
+        bg.backgroundColor = .primary_1
         bg.image = image
         addSubview(bg)
         bg.snp.makeConstraints { make in

+ 20 - 43
Lanu/Views/Game/Join/Input/LNJoinUsViewController.swift

@@ -13,7 +13,12 @@ import SnapKit
 extension UIView {
     func pushToJoinUs() {
         let vc = LNJoinUsViewController()
+        let topVC = navigationController?.viewControllers.last
         navigationController?.pushViewController(vc, animated: true)
+        if topVC is LNWebViewController,
+           let count = navigationController?.viewControllers.count {
+            navigationController?.viewControllers.remove(at: count - 2)
+        }
     }
 }
 
@@ -70,12 +75,11 @@ class LNJoinUsViewController: LNViewController {
     private let inputCaptchaView = LNJoinUsInputCaptchaView()
     private let inputInfoView = LNJoinUsInputInfoView()
     private let selectSkillView = LNJoinUsSelectSkillView()
-    private let inputSkillView = LNJoinUsInputSkillInfoView()
+    private let inputSkillView = LNJoinUsSkillFieldsEditView()
     
     override func viewDidLoad() {
         super.viewDidLoad()
         
-        enableDragBack = false
         setupViews()
         
         curStep = .none
@@ -126,19 +130,11 @@ extension LNJoinUsViewController:
     LNJoinUsSelectSkillViewDelegate {
     func joinUsSelectSkillView(view: LNJoinUsSelectSkillView, didSelect skill: LNGameCategoryItemVO) {
         showLoading()
-        LNGameMateManager.shared.getSkillField(id: skill.code) { [weak self] info in
+        LNGameMateManager.shared.getCreateSkillFields(id: skill.code) { [weak self] info in
             dismissLoading()
             
             guard let self else { return }
             guard let info else { return }
-            if info.fields.isEmpty {
-                showToast(.init(key: "B00060"))
-                return
-            }
-            guard info.isAvailable else {
-                showToast(.init(key: "B00058"))
-                return
-            }
             curStep = .skillInfoInput
             
             inputSkillView.update(skill, info: info)
@@ -161,12 +157,23 @@ extension LNJoinUsViewController:
 
 extension LNJoinUsViewController {
     private func setupViews() {
-        showNavigationBar = false
+        navigationBarColor = .clear
+        view.backgroundColor = .primary_1
+        customBack = { [weak self] in
+            guard let self else { return }
+            if let back = curStep.backStep {
+                curStep = back
+            } else {
+                navigationController?.popViewController(animated: true)
+            }
+        }
+        
+        title = .init(key: "B00031")
         
         view.addSubview(headerView)
         headerView.snp.makeConstraints { make in
             make.horizontalEdges.equalToSuperview()
-            make.top.equalToSuperview()
+            make.top.equalToSuperview().offset(-(UIView.navigationBarHeight + UIView.statusBarHeight))
         }
         
         let container = UIView()
@@ -205,36 +212,6 @@ extension LNJoinUsViewController {
         inputSkillView.snp.makeConstraints { make in
             make.edges.equalToSuperview()
         }
-        
-        let navBar = buildFakeNavi()
-        view.addSubview(navBar)
-        navBar.snp.makeConstraints { make in
-            make.horizontalEdges.equalToSuperview()
-            make.top.equalToSuperview()
-        }
-    }
-    
-    private func buildFakeNavi() -> UIView {
-        let navBar = LNFakeNaviBar()
-        navBar.showBackButton { [weak self] in
-            guard let self else { return }
-            if let back = curStep.backStep {
-                curStep = back
-            } else {
-                navigationController?.popToRootViewController(animated: true)
-            }
-        }
-        
-        let titleLabel = UILabel()
-        titleLabel.text = .init(key: "B00031")
-        titleLabel.font = .heading_h2
-        titleLabel.textColor = .text_5
-        navBar.actionView.addSubview(titleLabel)
-        titleLabel.snp.makeConstraints { make in
-            make.center.equalToSuperview()
-        }
-        
-        return navBar
     }
 }
 

+ 21 - 57
Lanu/Views/Game/Join/Input/SkillInfo/LNJoinUsInputSkillInfoView.swift → Lanu/Views/Game/Join/Input/SkillInfo/LNJoinUsSkillFieldsEditView.swift

@@ -1,5 +1,5 @@
 //
-//  LNJoinUsInputSkillInfoView.swift
+//  LNJoinUsSkillFieldsEditView.swift
 //  Gami
 //
 //  Created by OneeChan on 2026/1/19.
@@ -10,11 +10,7 @@ import UIKit
 import SnapKit
 
 
-class LNJoinUsInputSkillInfoView: UIView {
-    private var scrollViewBottomInset: CGFloat {
-        -commonBottomInset + 32
-    }
-    
+class LNJoinUsSkillFieldsEditView: UIView {
     private let titleLabel = UILabel()
     
     private let scrollView = UIScrollView()
@@ -22,17 +18,16 @@ class LNJoinUsInputSkillInfoView: UIView {
     
     private let applyButton = UIButton()
     
-    private var fieldViews: [LNJoinUsSkillInfoInputFieldView] = []
-    private var field: LNJoinUsSkillInfoVO?
+    private var fieldViews: [LNSkillFieldBaseEditView] = []
+    private var field: LNCreateSkillInputFieldsVO?
     
     override init(frame: CGRect) {
         super.init(frame: frame)
         
         setupViews()
-        LNEventDeliver.addObserver(self)
     }
     
-    func update(_ skill: LNGameCategoryItemVO, info: LNJoinUsSkillInfoVO) {
+    func update(_ skill: LNGameCategoryItemVO, info: LNCreateSkillInputFieldsVO) {
         stackView.arrangedSubviews.forEach {
             stackView.removeArrangedSubview($0)
             $0.removeFromSuperview()
@@ -44,17 +39,17 @@ class LNJoinUsInputSkillInfoView: UIView {
         }
         
         for field in info.fields {
-            let view: LNJoinUsSkillInfoInputFieldView? = switch field.type {
+            let view: LNSkillFieldBaseEditView? = switch field.type {
             case .singleLineText, .number:
-                LNJoinUsSingleLineTextInputView()
+                LNSkillFieldSingleLineEditView()
             case .multiLineText:
-                LNJoinUsMultiLineTextInputView()
+                LNSkillFieldMultiLineEditView()
             case .singleSelection, .multiSelection:
-                LNJoinUsSelectionInputView()
+                LNSkillFieldSelectionEditView()
             case .voice:
-                LNJoinUsVoiceInputView()
+                LNSkillFieldVoiceEditView()
             case .photo:
-                LNJoinUsPhotoInputView()
+                LNSkillFieldPhotoEditView()
             default:
                 nil
             }
@@ -74,7 +69,7 @@ class LNJoinUsInputSkillInfoView: UIView {
     }
 }
 
-extension LNJoinUsInputSkillInfoView {
+extension LNJoinUsSkillFieldsEditView {
     private func commit() {
         guard let field else { return }
         
@@ -110,10 +105,11 @@ extension LNJoinUsInputSkillInfoView {
                 dismissLoading()
                 return
             }
-            let info = LNJoinUsInputInfo()
+            guard let self else { return }
+            let info = LNCreateSkillFieldsInfo()
             info.bizCategoryCode = field.bizCategoryCode!
             for item in field.fields {
-                let config = LNJoinUsInputFieldInfo()
+                let config = LNCreateSkillFieldInfo()
                 config.fieldCode = item.fieldCode
                 config.value = item.value.value
                 if item.type == .voice {
@@ -121,7 +117,7 @@ extension LNJoinUsInputSkillInfoView {
                 }
                 info.fields.append(config)
             }
-            LNGameMateManager.shared.setJoinGameMateSkillInfos(info: info) { [weak self] success in
+            LNGameMateManager.shared.createSkill(info: info) { [weak self] success in
                 dismissLoading()
                 guard let self else { return }
                 pushToJoinUsReview()
@@ -130,46 +126,13 @@ extension LNJoinUsInputSkillInfoView {
     }
 }
 
-extension LNJoinUsInputSkillInfoView: LNJoinUsInputFieldViewDelegate {
-    func joinUsInputFieldViewInputChanged(view: LNJoinUsSkillInfoInputFieldView) {
+extension LNJoinUsSkillFieldsEditView: LNSkillFieldBaseEditViewDelegate {
+    func onSkillFieldBaseEditViewInputChanged(view: LNSkillFieldBaseEditView) {
         checkSaveButton()
     }
 }
 
-extension LNJoinUsInputSkillInfoView: LNKeyboardNotify {
-    func onKeybaordWillShow(curInput: UIView?, keyboardHeight: CGFloat) {
-        guard let curInput, curInput.isDescendant(of: self) else { return }
-        
-        let offset = 20 + keyboardHeight
-        
-        let editViewY = curInput.convert(curInput.bounds, to: nil).maxY
-        let distance = UIScreen.main.bounds.height - editViewY
-        if distance > offset {
-            return
-        }
-        
-        var remain = offset - distance
-        let curOffset = scrollView.contentOffset
-        let availableOffset = scrollView.contentSize.height + scrollView.contentInset.bottom - curOffset.y - scrollView.bounds.height
-        if availableOffset > remain {
-            scrollView.setContentOffset(.init(x: curOffset.x, y: curOffset.y + remain), animated: true)
-            return
-        }
-        remain = remain - availableOffset
-        
-        var curInset = scrollView.contentInset
-        curInset.bottom += remain
-        scrollView.contentInset = curInset
-        
-        scrollView.setContentOffset(.init(x: curOffset.x, y: curOffset.y + availableOffset + remain), animated: true)
-    }
-    
-    func onKeybaordWillHide(curInput: UIView?) {
-        scrollView.contentInset = .init(top: 0, left: 0, bottom: scrollViewBottomInset, right: 0)
-    }
-}
-
-extension LNJoinUsInputSkillInfoView {
+extension LNJoinUsSkillFieldsEditView {
     private func checkSaveButton() {
         let allInput = fieldViews.first { !$0.hasInput() } == nil
         
@@ -194,7 +157,8 @@ extension LNJoinUsInputSkillInfoView {
         
         scrollView.showsVerticalScrollIndicator = false
         scrollView.showsHorizontalScrollIndicator = false
-        scrollView.contentInset = .init(top: 0, left: 0, bottom: scrollViewBottomInset, right: 0)
+        scrollView.contentInset = .init(top: 0, left: 0, bottom: -commonBottomInset + 32, right: 0)
+        scrollView.adjustKeyoard()
         addSubview(scrollView)
         scrollView.snp.makeConstraints { make in
             make.horizontalEdges.equalToSuperview().inset(16)

+ 11 - 35
Lanu/Views/Game/Join/Review/LNJoinUsReviewViewController.swift

@@ -13,7 +13,12 @@ import SnapKit
 extension UIView {
     func pushToJoinUsReview(_ animated: Bool = true) {
         let vc = LNJoinUsReviewViewController()
+        let topVC = navigationController?.viewControllers.last
         navigationController?.pushViewController(vc, animated: animated)
+        if topVC is LNJoinUsReviewViewController,
+           let count = navigationController?.viewControllers.count {
+            navigationController?.viewControllers.remove(at: count - 2)
+        }
     }
 }
 
@@ -22,36 +27,26 @@ class LNJoinUsReviewViewController: LNViewController {
     override func viewDidLoad() {
         super.viewDidLoad()
         
-        enableDragBack = false
         setupViews()
     }
 }
 
 extension LNJoinUsReviewViewController {
     private func setupViews() {
-        showNavigationBar = false
-        
-        let image = UIImage.icJoinUsReviewBg
-        let bg = UIImageView()
-        bg.image = image
-        view.addSubview(bg)
-        bg.snp.makeConstraints { make in
-            make.horizontalEdges.top.equalToSuperview()
+        customBack = { [weak self] in
+            guard let self else { return }
+            navigationController?.popToRootViewController(animated: true)
         }
         
+        title = .init(key: "B00031")
+        view.backgroundColor = .primary_1
+        
         let infoView = buildInfoView()
         view.addSubview(infoView)
         infoView.snp.makeConstraints { make in
             make.horizontalEdges.equalToSuperview()
             make.centerY.equalToSuperview().multipliedBy(0.75)
         }
-        
-        let header = buildFakeNavi()
-        view.addSubview(header)
-        header.snp.makeConstraints { make in
-            make.horizontalEdges.equalToSuperview()
-            make.top.equalToSuperview()
-        }
     }
     
     private func buildInfoView() -> UIView {
@@ -92,23 +87,4 @@ extension LNJoinUsReviewViewController {
         
         return container
     }
-    
-    private func buildFakeNavi() -> UIView {
-        let navBar = LNFakeNaviBar()
-        navBar.showBackButton { [weak self] in
-            guard let self else { return }
-            navigationController?.popToRootViewController(animated: true)
-        }
-        
-        let titleLabel = UILabel()
-        titleLabel.text = .init(key: "B00031")
-        titleLabel.font = .heading_h2
-        titleLabel.textColor = .text_5
-        navBar.actionView.addSubview(titleLabel)
-        titleLabel.snp.makeConstraints { make in
-            make.center.equalToSuperview()
-        }
-        
-        return navBar
-    }
 }

+ 7 - 7
Lanu/Views/Game/MateFilter/LNGameFilterPanel.swift

@@ -8,6 +8,7 @@
 import Foundation
 import UIKit
 import SnapKit
+import Combine
 
 
 protocol LNGameFilterPanelDelegate: NSObject {
@@ -37,12 +38,6 @@ class LNGameFilterPanel: LNPopupView {
         setupViews()
     }
     
-    override func layoutSubviews() {
-        super.layoutSubviews()
-        
-        gradientLayer.frame = container.bounds
-    }
-    
     required init?(coder: NSCoder) {
         fatalError("init(coder:) has not been implemented")
     }
@@ -53,7 +48,7 @@ extension LNGameFilterPanel: LNGameCategoryFilterPanelDelegate {
         curGameType = gameType
         curGameCategory = game
         
-        filterPanel.update(filter: LNGameMateFilter())
+        filterPanel.update(game: game, filter: LNGameMateFilter())
         scrollView.setContentOffset(.init(x: scrollView.bounds.width, y: 0), animated: true)
         backButton.isHidden = false
     }
@@ -80,6 +75,11 @@ extension LNGameFilterPanel {
         gradientLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
         container.layer.addSublayer(gradientLayer)
         
+        container.publisher(for: \.bounds).removeDuplicates().sink { [weak self] newValue in
+            guard let self else { return }
+            gradientLayer.frame = container.bounds
+        }.store(in: &bag)
+        
         let titleLabel = UILabel()
         titleLabel.font = .heading_h3
         titleLabel.textColor = .text_5

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

@@ -19,6 +19,7 @@ class LNGameMateFilterPanel: UIView {
     private let columns = 4
     private let stackView = UIStackView()
     private var curFilter: LNGameMateFilter?
+    private var curGame: LNGameCategoryItemVO?
     
     weak var delegate: LNGameMateFilterPanelDelegate?
     
@@ -28,7 +29,7 @@ class LNGameMateFilterPanel: UIView {
         setupViews()
     }
     
-    func update(filter: LNGameMateFilter) {
+    func update(game: LNGameCategoryItemVO, filter: LNGameMateFilter) {
         let old = stackView.arrangedSubviews
         old.forEach {
             stackView.removeArrangedSubview($0)
@@ -68,7 +69,26 @@ class LNGameMateFilterPanel: UIView {
         }
         stackView.addArrangedSubview(price)
         
+        for item in LNGameMateManager.shared.gameFilter(for: game.code) {
+            let curSelect = filter.categoryFilters.first { $0.fieldCode == item.fieldCode }?.filterValues.first
+            let index = item.constants.firstIndex { $0.value == curSelect } ?? 0
+            var filters = item.constants.map({ $0.value })
+            filters.insert(.init(key: "A00008"), at: 0) // 手动插入"无限制"选项
+            let filterSection = buildFilterSection(title: item.name, filters: filters, selected: index)
+            { [weak self] index in
+                guard let self else { return }
+                
+                curFilter?.categoryFilters.removeAll { $0.fieldCode == item.fieldCode }
+                if index != 0 { // 0 为"无限制",不需要填充值
+                    curFilter?.categoryFilters.append((item.fieldCode, [item.constants[index - 1].key]))
+                }
+            }
+            stackView.addArrangedSubview(filterSection)
+        }
+        
+        
         curFilter = filter
+        curGame = game
     }
     
     required init?(coder: NSCoder) {
@@ -133,7 +153,8 @@ extension LNGameMateFilterPanel {
         resetButton.layer.borderColor = UIColor.fill_4.cgColor
         resetButton.addAction(UIAction(handler: { [weak self] _ in
             guard let self else { return }
-            update(filter: LNGameMateFilter())
+            guard let curGame else { return }
+            update(game: curGame, filter: LNGameMateFilter())
         }), for: .touchUpInside)
         addSubview(resetButton)
         resetButton.snp.makeConstraints { make in

+ 0 - 4
Lanu/Views/Game/MateList/LNGameMateListView.swift

@@ -37,10 +37,6 @@ class LNGameMateListView: UIView {
     }
     
     func reloadList(newTopCategory: String, newCategory: String?, filter: LNGameMateFilter) {
-        if category == newCategory,
-            newTopCategory == topCategory {
-            return
-        }
         topCategory = newTopCategory
         category = newCategory
         self.filter = filter

+ 487 - 0
Lanu/Views/Game/OrderCenter/LNGameMateCenterViewController.swift

@@ -0,0 +1,487 @@
+//
+//  LNGameMateCenterViewController.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/1/22.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+
+extension UIView {
+    func pushToMateCenter() {
+        let vc = LNGameMateCenterViewController()
+        navigationController?.pushViewController(vc, animated: true)
+    }
+}
+
+
+class LNGameMateCenterViewController: LNViewController {
+    private let incomeLabel = UILabel()
+    private let exposureLabel = UILabel()
+    private let visitorLabel = UILabel()
+    
+    private let statusButton = UIButton()
+    private let statusLabel = UILabel()
+    private let statusDescLabel = UILabel()
+    
+    private let skillStackView = UIStackView()
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        setupViews()
+        
+        LNEventDeliver.addObserver(self)
+        
+        onUserInfoChanged(userInfo: myUserInfo)
+        onGameMateManagerInfoChanged()
+    }
+}
+
+extension LNGameMateCenterViewController: LNProfileManagerNotify, LNGameMateManagerNotify {
+    func onUserInfoChanged(userInfo: LNUserProfileVO) {
+        guard userInfo.userNo.isMyUid else { return }
+        
+        skillStackView.arrangedSubviews.forEach {
+            skillStackView.removeArrangedSubview($0)
+            $0.removeFromSuperview()
+        }
+        skillStackView.superview?.isHidden = userInfo.skills.isEmpty
+        
+        for skill in userInfo.skills {
+            let view = buildSkillItem(skill)
+            skillStackView.addArrangedSubview(view)
+        }
+    }
+    
+    func onGameMateManagerInfoChanged() {
+        guard let myGameMateInfo else { return }
+        
+        incomeLabel.text = myGameMateInfo.weekBeanIncome.toDisplay
+        exposureLabel.text = "\(myGameMateInfo.exposureCountDay)"
+        visitorLabel.text = "\(myGameMateInfo.visitorCount)"
+        
+        if myGameMateInfo.playmateOpen {
+            statusButton.setBackgroundImage(.primary_7, for: .normal)
+            statusLabel.text = .init(key: "B00087")
+            statusDescLabel.text = .init(key: "B00088")
+        } else {
+            statusButton.setBackgroundImage(.primary_8, for: .normal)
+            statusLabel.text = .init(key: "B00089")
+            statusDescLabel.text = .init(key: "B00090")
+        }
+    }
+}
+
+extension LNGameMateCenterViewController {
+    private func setupViews() {
+        navigationBarColor = .clear
+        view.backgroundColor = .primary_1
+        title = .init(key: "B00071")
+        
+        let topCover = UIImageView()
+        topCover.image = .icHomeTopBg
+        topCover.backgroundColor = .primary_1
+        view.addSubview(topCover)
+        topCover.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalToSuperview().offset(-(UIView.navigationBarHeight + UIView.statusBarHeight))
+        }
+        
+        let scrollView = UIScrollView()
+        scrollView.showsVerticalScrollIndicator = false
+        scrollView.showsHorizontalScrollIndicator = false
+        scrollView.contentInset = .init(top: 0, left: 0, bottom: -view.commonBottomInset, right: 0)
+        view.addSubview(scrollView)
+        scrollView.snp.makeConstraints { make in
+            make.verticalEdges.equalToSuperview()
+            make.horizontalEdges.equalToSuperview().inset(16)
+        }
+        
+        let stackView = UIStackView()
+        stackView.axis = .vertical
+        stackView.spacing = 12
+        scrollView.addSubview(stackView)
+        stackView.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+            make.width.equalToSuperview()
+        }
+        
+        stackView.addArrangedSubview(buildRecordInfos())
+        stackView.addArrangedSubview(buildSkillList())
+        stackView.addArrangedSubview(buildOrderRecord())
+    }
+    
+    private func buildRecordInfos() -> UIView {
+        let container = UIView()
+        container.layer.cornerRadius = 12
+        container.backgroundColor = .fill
+        
+        let stackView = UIStackView()
+        stackView.axis = .vertical
+        stackView.spacing = 20
+        container.addSubview(stackView)
+        stackView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(10)
+            make.verticalEdges.equalToSuperview().inset(20)
+        }
+        
+        stackView.addArrangedSubview(buildIncome())
+        stackView.addArrangedSubview(buildDataView())
+        stackView.addArrangedSubview(buildOpenButton())
+        
+        return container
+    }
+    
+    private func buildIncome() -> UIView {
+        let container = UIView()
+        
+        let incomeView = UIView()
+        container.addSubview(incomeView)
+        incomeView.snp.makeConstraints { make in
+            make.top.equalToSuperview()
+            make.centerX.equalToSuperview()
+        }
+        
+        let beanIc = UIImageView.beanImageView()
+        incomeView.addSubview(beanIc)
+        beanIc.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalToSuperview()
+            make.width.height.equalTo(28)
+        }
+        
+        incomeLabel.text = "0"
+        incomeLabel.font = .systemFont(ofSize: 30, weight: .semibold)
+        incomeLabel.textColor = .text_5
+        incomeView.addSubview(incomeLabel)
+        incomeLabel.snp.makeConstraints { make in
+            make.verticalEdges.equalToSuperview()
+            make.leading.equalTo(beanIc.snp.trailing)
+            make.trailing.equalToSuperview()
+        }
+        
+        let descView = UIView()
+        container.addSubview(descView)
+        descView.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.top.equalTo(incomeView.snp.bottom).offset(4)
+            make.bottom.equalToSuperview()
+        }
+        
+        let descLabel = UILabel()
+        descLabel.text = .init(key: "B00073")
+        descLabel.font = .body_m
+        descLabel.textColor = .text_5
+        descView.addSubview(descLabel)
+        descLabel.snp.makeConstraints { make in
+            make.leading.equalToSuperview()
+            make.verticalEdges.equalToSuperview()
+        }
+        
+        let arrow = UIImageView.arrowImageView(size: 12)
+        arrow.tintColor = .text_4
+        descView.addSubview(arrow)
+        arrow.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview()
+            make.leading.equalTo(descLabel.snp.trailing).offset(4)
+        }
+        
+        return container
+    }
+    
+    private func buildDataView() -> UIView {
+        let container = UIView()
+        container.onTap { [weak self] in
+            guard let self else { return }
+            view.pushToVisitors()
+        }
+        
+        let stackView = UIStackView()
+        stackView.axis = .horizontal
+        stackView.distribution = .fillEqually
+        container.addSubview(stackView)
+        stackView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(5)
+            make.verticalEdges.equalToSuperview()
+        }
+        
+        stackView.addArrangedSubview(buildDataItemView(.init(key: "B00073"), contentLabel: exposureLabel))
+        stackView.addArrangedSubview(buildDataItemView(.init(key: "B00074"), contentLabel: visitorLabel))
+        
+        let line = buildLine()
+        stackView.addSubview(line)
+        line.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+        }
+        
+        return container
+    }
+    
+    private func buildDataItemView(_ title: String, contentLabel: UILabel) -> UIView {
+        let container = UIView()
+        
+        contentLabel.text = "0"
+        contentLabel.font = .heading_h2
+        contentLabel.textColor = .text_5
+        container.addSubview(contentLabel)
+        contentLabel.snp.makeConstraints { make in
+            make.top.equalToSuperview()
+            make.centerX.equalToSuperview()
+        }
+        
+        let descView = UIView()
+        container.addSubview(descView)
+        descView.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.top.equalTo(contentLabel.snp.bottom).offset(6)
+            make.bottom.equalToSuperview()
+        }
+        
+        
+        let descLabel = UILabel()
+        descLabel.text = title
+        descLabel.font = .body_s
+        descLabel.textColor = .text_4
+        descView.addSubview(descLabel)
+        descLabel.snp.makeConstraints { make in
+            make.leading.equalToSuperview()
+            make.verticalEdges.equalToSuperview()
+        }
+        
+        let arrow = UIImageView.arrowImageView(size: 10)
+        arrow.tintColor = .text_4
+        descView.addSubview(arrow)
+        arrow.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview()
+            make.leading.equalTo(descLabel.snp.trailing).offset(4)
+        }
+        
+        return container
+    }
+    
+    private func buildOpenButton() -> UIView {
+        statusButton.layer.cornerRadius = 26
+        statusButton.clipsToBounds = true
+        statusButton.setBackgroundImage(.primary_8, for: .normal)
+        statusButton.addAction(UIAction(handler: { _ in
+            showLoading()
+            let isOpen = myGameMateInfo?.playmateOpen == true
+            LNGameMateManager.shared.enableGameMate(open: !isOpen) { success in
+                dismissLoading()
+            }
+        }), for: .touchUpInside)
+        statusButton.snp.makeConstraints { make in
+            make.height.equalTo(56)
+        }
+        
+        let container = UIView()
+        container.isUserInteractionEnabled = false
+        statusButton.addSubview(container)
+        container.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+            make.leading.greaterThanOrEqualToSuperview().offset(16)
+        }
+        
+        statusLabel.font = .heading_h3
+        statusLabel.textColor = .text_1
+        statusLabel.textAlignment = .center
+        container.addSubview(statusLabel)
+        statusLabel.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalToSuperview()
+        }
+        
+        statusDescLabel.font = .body_xs
+        statusDescLabel.textColor = .primary_1
+        container.addSubview(statusDescLabel)
+        statusDescLabel.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.bottom.equalToSuperview()
+            make.top.equalTo(statusLabel.snp.bottom)
+        }
+        
+        return statusButton
+    }
+    
+    private func buildLine() -> UIView {
+        let line = UIView()
+        line.backgroundColor = .init(hex: "#D9D9D9")
+        line.snp.makeConstraints { make in
+            make.width.equalTo(1)
+            make.height.equalTo(37)
+        }
+    
+        return line
+    }
+    
+    private func buildSkillList() -> UIView {
+        let container = UIView()
+        container.layer.cornerRadius = 12
+        container.backgroundColor = .fill
+        
+        let stackView = UIStackView()
+        stackView.axis = .vertical
+        container.addSubview(stackView)
+        stackView.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+        }
+        
+        let header = buildSkillHeader()
+        stackView.addArrangedSubview(header)
+        
+        let skillContainer = UIView()
+        stackView.addArrangedSubview(skillContainer)
+        
+        skillStackView.axis = .vertical
+        skillStackView.spacing = 8
+        skillContainer.addSubview(skillStackView)
+        skillStackView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(12)
+            make.top.equalToSuperview().offset(4)
+            make.bottom.equalToSuperview().offset(-12)
+        }
+        
+        return container
+    }
+    
+    private func buildSkillHeader() -> UIView {
+        let container = UIView()
+        container.onTap { [weak self] in
+            guard let self else { return }
+            view.pushToSkillManager()
+        }
+        container.snp.makeConstraints { make in
+            make.height.equalTo(40)
+        }
+        
+        let titleLabel = UILabel()
+        titleLabel.text = .init(key: "B00075")
+        titleLabel.font = .heading_h3
+        titleLabel.textColor = .text_5
+        container.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalToSuperview().offset(16)
+        }
+        
+        let arrow = UIImageView.arrowImageView(size: 12)
+        arrow.tintColor = .text_4
+        container.addSubview(arrow)
+        arrow.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview().offset(-16)
+        }
+        
+        return container
+    }
+    
+    private func buildSkillItem(_ skill: LNGameMateSkillVO) -> UIView {
+        let container = UIView()
+        container.layer.cornerRadius = 12
+        container.layer.borderColor = .fill_4
+        container.layer.borderWidth = 1
+        container.snp.makeConstraints { make in
+            make.height.equalTo(54)
+        }
+        
+        let ic = UIImageView()
+        ic.sd_setImage(with: URL(string: skill.icon))
+        container.addSubview(ic)
+        ic.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalToSuperview().offset(10)
+            make.width.height.equalTo(50)
+        }
+        
+        let priceView = UIView()
+        container.addSubview(priceView)
+        priceView.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview().offset(-10)
+        }
+        
+        let coin = UIImageView.coinImageView()
+        priceView.addSubview(coin)
+        coin.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalToSuperview()
+            make.width.height.equalTo(16)
+        }
+        
+        let priceLabel = UILabel()
+        priceLabel.text = skill.price.toDisplay
+        priceLabel.font = .heading_h4
+        priceLabel.textColor = .text_5
+        priceView.addSubview(priceLabel)
+        priceLabel.snp.makeConstraints { make in
+            make.verticalEdges.equalToSuperview()
+            make.leading.equalTo(coin.snp.trailing).offset(1)
+        }
+        
+        let unitLabel = UILabel()
+        unitLabel.text = "/1 \(skill.unit)"
+        unitLabel.font = .body_s
+        unitLabel.textColor = .text_4
+        priceView.addSubview(unitLabel)
+        unitLabel.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalTo(priceLabel.snp.trailing).offset(1)
+            make.trailing.equalToSuperview()
+        }
+        
+        let nameLabel = UILabel()
+        nameLabel.text = skill.name
+        nameLabel.font = .heading_h4
+        nameLabel.textColor = .text_5
+        nameLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
+        nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+        container.addSubview(nameLabel)
+        nameLabel.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalTo(ic.snp.trailing).offset(4)
+            make.trailing.lessThanOrEqualTo(priceView.snp.leading).offset(-20)
+        }
+        
+        return container
+    }
+    
+    private func buildOrderRecord() -> UIView {
+        let container = UIView()
+        container.layer.cornerRadius = 12
+        container.backgroundColor = .fill
+        container.onTap { [weak self] in
+            guard let self else { return }
+            view.pushToOrderRecord()
+        }
+        container.snp.makeConstraints { make in
+            make.height.equalTo(40)
+        }
+        
+        let titleLabel = UILabel()
+        titleLabel.text = .init(key: "A00209")
+        titleLabel.font = .heading_h3
+        titleLabel.textColor = .text_5
+        container.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalToSuperview().offset(16)
+        }
+        
+        let arrow = UIImageView.arrowImageView(size: 12)
+        arrow.tintColor = .text_4
+        container.addSubview(arrow)
+        arrow.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview().offset(-16)
+        }
+        
+        return container
+    }
+}

+ 291 - 0
Lanu/Views/Game/OrderCenter/LNOrderAcceptSettingsViewController.swift

@@ -0,0 +1,291 @@
+//
+//  LNOrderAcceptSettingsViewController.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/1/23.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+extension UIView {
+    func pushToOrderAcceptSettings() {
+        let vc = LNOrderAcceptSettingsViewController()
+        navigationController?.pushViewController(vc, animated: true)
+    }
+}
+
+
+class LNOrderAcceptSettingsViewController: LNViewController {
+    private let timeLabel = UILabel()
+    private var selectTime: (from: Int, to: Int) = (0, 1) {
+        didSet {
+            let fromTime = String(format: "%02d:00", selectTime.from)
+            let toTime: String = if selectTime.to > 23 {
+                .init(key: "B00084") + String(format: "%02d:00", selectTime.to - 24)
+            } else {
+                String(format: "%02d:00", selectTime.to)
+            }
+            timeLabel.text = fromTime + "~" + toTime
+        }
+    }
+    
+    private let dateLabel = UILabel()
+    private var selectDate: [LNOrderAcceptWeekDay] = [] {
+        didSet {
+            dateLabel.text = selectedDateDesc()
+        }
+    }
+    
+    private var config: LNOrderAcceptConfig?
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        setupViews()
+        
+        loadConfig()
+    }
+}
+
+extension LNOrderAcceptSettingsViewController {
+    private func loadConfig() {
+        showLoading()
+        LNGameMateManager.shared.getOrderAccetpConfig { [weak self] config in
+            dismissLoading()
+            guard let self else { return }
+            guard let config else {
+                navigationController?.popViewController(animated: true)
+                return
+            }
+            
+            let components = config.timeRange.components(separatedBy: "-")
+            if components.count == 2 {
+                let from = extractHourFromTimeString(components[0])
+                let to = extractHourFromTimeString(components[1])
+                selectTime = (from, to)
+            } else {
+                selectTime = (0, 1)
+            }
+            
+            selectDate = config.weekNums
+            self.config = config
+        }
+    }
+    
+    private func saveConfig() {
+        let timeRange = String(format: "%02d:00", selectTime.from) + "-" + String(format: "%02d:00", selectTime.to)
+        if let config,
+            config.weekNums == selectDate,
+            config.timeRange == timeRange {
+            navigationController?.popViewController(animated: true)
+            return
+        }
+        
+        showLoading()
+        let newConfig = LNOrderAcceptConfig()
+        newConfig.weekNums = selectDate
+        newConfig.timeRange = timeRange
+        LNGameMateManager.shared.setOrderAcceptConfig(config: newConfig) { [weak self] success in
+            dismissLoading()
+            guard let self else { return }
+            if success {
+                navigationController?.popViewController(animated: true)
+            }
+        }
+    }
+    
+    private func extractHourFromTimeString(_ timeString: String) -> Int {
+        let components = timeString.components(separatedBy: ":")
+        
+        guard components.count == 2 else {
+            return 0
+        }
+        
+        let hourString = components[0]
+        guard let hour = Int(hourString) else {
+            return 0
+        }
+        
+        guard hour >= 0 && hour <= 23 else {
+            return 0
+        }
+        
+        return hour
+    }
+    
+    private func selectedDateDesc() -> String {
+        guard !selectDate.isEmpty else { return "" }
+
+        var mergedSegments: [String] = []
+        var start = selectDate[0]
+        var end = selectDate[0]
+        
+        for day in selectDate {
+            if day == selectDate[0] {
+                continue
+            }
+            // 如果当前星期与上一个连续,更新结束值
+            if day.rawValue == end.rawValue + 1 {
+                end = day
+            } else {
+                // 不连续则保存当前段,并重置起始/结束
+                mergedSegments.append(mergeSegment(start: start, end: end))
+                start = day
+                end = day
+            }
+        }
+        mergedSegments.append(mergeSegment(start: start, end: end))
+        
+        return mergedSegments.joined(separator: ",")
+    }
+    
+    private func mergeSegment(start: LNOrderAcceptWeekDay, end: LNOrderAcceptWeekDay) -> String {
+        let formatter = DateFormatter()
+        formatter.locale = curLocal
+        let symbols = formatter.weekdaySymbols!
+        
+        var segments: [String] = []
+        
+        segments.append(symbols[start.rawValue - 1])
+        if start != end {
+            segments.append(.init(key: "B00081"))
+            segments.append(symbols[end.rawValue - 1])
+        }
+        return segments.joined(separator: LNAppConfig.shared.curLang == .chiness ? "" : " ")
+    }
+}
+
+extension LNOrderAcceptSettingsViewController {
+    private func setupViews() {
+        title = .init(key: "B00078")
+        view.backgroundColor = .primary_1
+        customBack = { [weak self] in
+            guard let self else { return }
+            saveConfig()
+        }
+        
+        let stackView = UIStackView()
+        stackView.axis = .vertical
+        stackView.spacing = 10
+        view.addSubview(stackView)
+        stackView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(16)
+            make.top.equalToSuperview().offset(12)
+        }
+        
+        stackView.addArrangedSubview(buildTime())
+        stackView.addArrangedSubview(buildDate())
+    }
+    
+    private func buildTime() -> UIView {
+        let container = UIView()
+        container.layer.cornerRadius = 12
+        container.backgroundColor = .fill
+        container.onTap { [weak self] in
+            guard let self else { return }
+            let panel = LNHourRangePickerPanel()
+            panel.setTitles(.init(key: "B00082"), desc: .init(key: "B00083"))
+            panel.setDefault(from: selectTime.from, to: selectTime.to)
+            panel.handler = { [weak self] from, to in
+                guard let self else { return }
+                selectTime = (from, to)
+            }
+            panel.popup()
+        }
+        container.snp.makeConstraints { make in
+            make.height.equalTo(60)
+        }
+        
+        let arrow = UIImageView.arrowImageView(size: 14)
+        arrow.tintColor = .text_4
+        container.addSubview(arrow)
+        arrow.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview().offset(-12)
+        }
+        
+        timeLabel.font = .body_s
+        timeLabel.textColor = .text_4
+        container.addSubview(timeLabel)
+        timeLabel.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.trailing.equalTo(arrow.snp.leading).offset(-2)
+        }
+        
+        let titleLabel = UILabel()
+        titleLabel.font = .heading_h4
+        titleLabel.textColor = .text_5
+        titleLabel.text = .init(key: "B00079")
+        titleLabel.setContentHuggingPriority(.required, for: .horizontal)
+        titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
+        container.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalToSuperview().offset(12)
+            make.trailing.lessThanOrEqualTo(timeLabel.snp.leading).offset(-12)
+        }
+        
+        return container
+    }
+    
+    private func buildDate() -> UIView {
+        let container = UIView()
+        container.layer.cornerRadius = 12
+        container.backgroundColor = .fill
+        container.onTap { [weak self] in
+            guard let self else { return }
+            let formatter = DateFormatter()
+            formatter.locale = curLocal
+            
+            let panel = LNMultiSelectionPanel()
+            panel.setTitles(.init(key: "B00085"), desc: .init(key: "B00086"))
+            panel.update(formatter.weekdaySymbols, curSelected: selectDate.map({ $0.text }))
+            panel.handler = { [weak self] indexs in
+                guard let self else { return }
+                var dates: [LNOrderAcceptWeekDay] = []
+                for index in indexs {
+                    dates.append(LNOrderAcceptWeekDay.allCases[index])
+                }
+                selectDate = dates
+            }
+            panel.popup()
+        }
+        container.snp.makeConstraints { make in
+            make.height.equalTo(60)
+        }
+        
+        let arrow = UIImageView.arrowImageView(size: 14)
+        arrow.tintColor = .text_4
+        container.addSubview(arrow)
+        arrow.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview().offset(-12)
+        }
+        
+        dateLabel.font = .body_s
+        dateLabel.textColor = .text_4
+        container.addSubview(dateLabel)
+        dateLabel.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.trailing.equalTo(arrow.snp.leading).offset(-2)
+        }
+        
+        let titleLabel = UILabel()
+        titleLabel.font = .heading_h4
+        titleLabel.textColor = .text_5
+        titleLabel.text = .init(key: "B00080")
+        titleLabel.setContentHuggingPriority(.required, for: .horizontal)
+        titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
+        container.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalToSuperview().offset(12)
+            make.trailing.lessThanOrEqualTo(dateLabel.snp.leading).offset(-12)
+        }
+        
+        return container
+    }
+}

+ 191 - 0
Lanu/Views/Game/OrderCenter/Skill/LNSkillCreateViewController.swift

@@ -0,0 +1,191 @@
+//
+//  LNSkillCreateViewController.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/1/25.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+import Combine
+
+
+extension UIView {
+    func pushToSkillCreate(skill: LNGameCategoryItemVO, info: LNCreateSkillInputFieldsVO) {
+        let vc = LNSkillCreateViewController(skill: skill, info: info)
+        navigationController?.pushViewController(vc, animated: true)
+    }
+}
+
+
+class LNSkillCreateViewController: LNViewController {
+    private let skill: LNGameCategoryItemVO
+    private let scrollView = UIScrollView()
+    private let info: LNCreateSkillInputFieldsVO
+    private let editPanel = LNSkillFieldsEditView()
+    private let confirmButton = UIButton()
+    
+    init(skill: LNGameCategoryItemVO, info: LNCreateSkillInputFieldsVO) {
+        self.skill = skill
+        self.info = info
+        
+        super.init(nibName: nil, bundle: nil)
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        setupViews()
+        checkConfirmButton()
+    }
+}
+
+extension LNSkillCreateViewController {
+    private func commit() {
+        guard editPanel.checkAvailable else {
+            return
+        }
+        showLoading()
+        DispatchQueue.global().async { [weak self] in
+            guard let self else {
+                dismissLoading()
+                return
+            }
+            let voices = info.fields.filter({ $0.type == .voice })
+            let group = DispatchGroup()
+            var uploadDone = true
+            for voice in voices {
+                if let path = voice.value.stringValue,
+                   !path.starts(with: "http") {
+                    group.enter()
+                    LNFileUploader.shared.startUpload(type: .voice, fileURL: URL(fileURLWithPath: path), progressHandler: nil)
+                    { url, err in
+                        if let err {
+                            showToast(err)
+                            uploadDone = false
+                        } else if let url {
+                            voice.value.value = url
+                        } else {
+                            uploadDone = false
+                        }
+                        group.leave()
+                    }
+                }
+            }
+            group.wait()
+            
+            if !uploadDone {
+                dismissLoading()
+                return
+            }
+            let input = LNCreateSkillFieldsInfo()
+            input.bizCategoryCode = skill.code
+            for item in info.fields {
+                let config = LNCreateSkillFieldInfo()
+                config.fieldCode = item.fieldCode
+                config.value = item.value.value
+                if item.type == .voice {
+                    config.duration = item.duration
+                }
+                input.fields.append(config)
+            }
+            LNGameMateManager.shared.createSkill(info: input) { [weak self] success in
+                dismissLoading()
+                guard let self else { return }
+                view.pushToSkillCreatedReview()
+            }
+        }
+    }
+}
+
+extension LNSkillCreateViewController: LNSkillFieldBaseEditViewDelegate {
+    func onSkillFieldBaseEditViewInputChanged(view: LNSkillFieldBaseEditView) {
+        checkConfirmButton()
+    }
+}
+
+extension LNSkillCreateViewController {
+    private func checkConfirmButton() {
+        let allInput = editPanel.hasAllInput
+        
+        if allInput != confirmButton.isEnabled {
+            confirmButton.isEnabled = allInput
+            confirmButton.setBackgroundImage(allInput ? .primary_8 : nil, for: .normal)
+        }
+    }
+    
+    private func setupViews() {
+        title = .init(key: "B00093")
+        view.backgroundColor = .primary_1
+        view.onTap { [weak self] in
+            guard let self else { return }
+            view.endEditing(true)
+        }
+        
+        scrollView.showsHorizontalScrollIndicator = false
+        scrollView.showsVerticalScrollIndicator = false
+        scrollView.contentInset = .init(top: 0, left: 0, bottom: -view.commonBottomInset + 32, right: 0)
+        scrollView.adjustKeyoard()
+        view.addSubview(scrollView)
+        scrollView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(16)
+            make.top.equalToSuperview().offset(12)
+            make.bottom.equalToSuperview()
+        }
+        
+        scrollView.addSubview(editPanel)
+        editPanel.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+            make.width.equalToSuperview()
+        }
+        
+        editPanel.delegate = self
+        editPanel.update(skill.name, fields: info.fields)
+        
+        let bottomMenu = UIView()
+        view.addSubview(bottomMenu)
+        bottomMenu.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.bottom.equalToSuperview()
+        }
+        
+        let bottomGradient = CAGradientLayer()
+        bottomGradient.colors = [
+            UIColor.white.withAlphaComponent(0).cgColor,
+            UIColor.white.cgColor,
+            UIColor.white.cgColor
+        ]
+        bottomGradient.locations = [0, 0.5, 1]
+        bottomGradient.startPoint = .init(x: 0, y: 0)
+        bottomGradient.endPoint = .init(x: 0, y: 1)
+        bottomMenu.layer.addSublayer(bottomGradient)
+        bottomMenu.publisher(for: \.bounds).removeDuplicates().sink { [weak bottomGradient] newValue in
+            guard let bottomGradient else { return }
+            bottomGradient.frame = newValue
+        }.store(in: &bag)
+        
+        confirmButton.setTitle(.init(key: "A00002"), for: .normal)
+        confirmButton.setTitleColor(.text_1, for: .normal)
+        confirmButton.titleLabel?.font = .heading_h3
+        confirmButton.layer.cornerRadius = 23.5
+        confirmButton.backgroundColor = .fill_4
+        confirmButton.clipsToBounds = true
+        confirmButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            commit()
+        }), for: .touchUpInside)
+        bottomMenu.addSubview(confirmButton)
+        confirmButton.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(16)
+            make.bottom.equalToSuperview().offset(view.commonBottomInset)
+            make.top.equalToSuperview()
+            make.height.equalTo(47)
+        }
+    }
+}
+

+ 98 - 0
Lanu/Views/Game/OrderCenter/Skill/LNSkillCreatedReviewViewController.swift

@@ -0,0 +1,98 @@
+//
+//  LNSkillCreatedReviewViewController.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/1/26.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+extension UIView {
+    func pushToSkillCreatedReview(_ animated: Bool = true) {
+        let vc = LNJoinUsReviewViewController()
+        
+        let index = navigationController?.viewControllers.lastIndex (where: { $0 is LNSkillManagerViewController })
+        if let index, var viewControllers = navigationController?.viewControllers {
+            viewControllers = Array(viewControllers[0...index])
+            viewControllers.append(vc)
+            navigationController?.setViewControllers(viewControllers, animated: true)
+        } else {
+            navigationController?.pushViewController(vc, animated: animated)
+        }
+    }
+}
+
+
+class LNSkillCreatedReviewViewController: LNViewController {
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        setupViews()
+    }
+}
+
+extension LNSkillCreatedReviewViewController {
+    private func setupViews() {
+        navigationBarColor = .clear
+        
+        title = .init(key: "B00031")
+        
+        let bg = UIImageView()
+        bg.image = .icJoinUsReviewBg
+        bg.backgroundColor = .white
+        view.addSubview(bg)
+        bg.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalToSuperview().offset(-(UIView.navigationBarHeight + UIView.statusBarHeight))
+        }
+        
+        let infoView = buildInfoView()
+        view.addSubview(infoView)
+        infoView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.centerY.equalToSuperview().multipliedBy(0.75)
+        }
+    }
+    
+    private func buildInfoView() -> UIView {
+        let container = UIView()
+        
+        let icon = UIImageView()
+        icon.image = .icJoinUsReview
+        container.addSubview(icon)
+        icon.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.top.equalToSuperview()
+        }
+        
+        let titleLabel = UILabel()
+        titleLabel.text = .init(key: "B00061")
+        titleLabel.font = .heading_h3
+        titleLabel.textColor = .text_5
+        titleLabel.numberOfLines = 0
+        titleLabel.textAlignment = .center
+        container.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(32)
+            make.top.equalTo(icon.snp.bottom).offset(10)
+        }
+        
+        let descLabel = UILabel()
+        descLabel.text = .init(key: "B00062")
+        descLabel.font = .body_s
+        descLabel.textColor = .text_5
+        descLabel.numberOfLines = 0
+        descLabel.textAlignment = .center
+        container.addSubview(descLabel)
+        descLabel.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(32)
+            make.bottom.equalToSuperview()
+            make.top.equalTo(titleLabel.snp.bottom).offset(4)
+        }
+        
+        return container
+    }
+}

+ 189 - 0
Lanu/Views/Game/OrderCenter/Skill/LNSkillEditViewController.swift

@@ -0,0 +1,189 @@
+//
+//  LNSkillEditViewController.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/1/25.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+import Combine
+
+
+extension UIView {
+    func pushToSkillEdit(_ info: LNSkillEditFieldsResponse) {
+        let vc = LNSkillEditViewController(info: info)
+        navigationController?.pushViewController(vc, animated: true)
+    }
+}
+
+
+class LNSkillEditViewController: LNViewController {
+    private let scrollView = UIScrollView()
+    private let info: LNSkillEditFieldsResponse
+    private let editPanel = LNSkillFieldsEditView()
+    private let confirmButton = UIButton()
+    
+    init(info: LNSkillEditFieldsResponse) {
+        self.info = info
+        
+        super.init(nibName: nil, bundle: nil)
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        setupViews()
+        checkConfirmButton()
+    }
+}
+
+extension LNSkillEditViewController {
+    private func commit() {
+        guard editPanel.checkAvailable else {
+            return
+        }
+        showLoading()
+        DispatchQueue.global().async { [weak self] in
+            guard let self else {
+                dismissLoading()
+                return
+            }
+            let voices = info.fields.filter({ $0.type == .voice })
+            let group = DispatchGroup()
+            var uploadDone = true
+            for voice in voices {
+                if let path = voice.value.stringValue,
+                   !path.starts(with: "http") {
+                    group.enter()
+                    LNFileUploader.shared.startUpload(type: .voice, fileURL: URL(fileURLWithPath: path), progressHandler: nil)
+                    { url, err in
+                        if let err {
+                            showToast(err)
+                            uploadDone = false
+                        } else if let url {
+                            voice.value.value = url
+                        } else {
+                            uploadDone = false
+                        }
+                        group.leave()
+                    }
+                }
+            }
+            group.wait()
+            
+            if !uploadDone {
+                dismissLoading()
+                return
+            }
+            let input = LNEditSkillFieldsInfo()
+            input.skillId = info.skillId
+            input.apply = editPanel.needReview
+            for item in info.fields {
+                let config = LNCreateSkillFieldInfo()
+                config.fieldCode = item.fieldCode
+                config.value = item.value.value
+                if item.type == .voice {
+                    config.duration = item.duration
+                }
+                input.fields.append(config)
+            }
+            LNGameMateManager.shared.commitSkillEdit(info: input) { [weak self] success in
+                dismissLoading()
+                guard let self else { return }
+                navigationController?.popViewController(animated: true)
+            }
+        }
+    }
+}
+
+extension LNSkillEditViewController: LNSkillFieldBaseEditViewDelegate {
+    func onSkillFieldBaseEditViewInputChanged(view: LNSkillFieldBaseEditView) {
+        checkConfirmButton()
+    }
+}
+
+extension LNSkillEditViewController {
+    private func checkConfirmButton() {
+        let allInput = editPanel.hasAllInput
+        
+        if allInput != confirmButton.isEnabled {
+            confirmButton.isEnabled = allInput
+            confirmButton.setBackgroundImage(allInput ? .primary_8 : nil, for: .normal)
+        }
+    }
+    
+    private func setupViews() {
+        title = .init(key: "B00093")
+        view.backgroundColor = .primary_1
+        view.onTap { [weak self] in
+            guard let self else { return }
+            view.endEditing(true)
+        }
+        
+        scrollView.showsHorizontalScrollIndicator = false
+        scrollView.showsVerticalScrollIndicator = false
+        scrollView.contentInset = .init(top: 0, left: 0, bottom: -view.commonBottomInset + 32, right: 0)
+        scrollView.adjustKeyoard()
+        view.addSubview(scrollView)
+        scrollView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(16)
+            make.top.equalToSuperview().offset(12)
+            make.bottom.equalToSuperview()
+        }
+        
+        scrollView.addSubview(editPanel)
+        editPanel.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+            make.width.equalToSuperview()
+        }
+        
+        editPanel.delegate = self
+        editPanel.update(info.bizCategoryName, fields: info.fields)
+        
+        let bottomMenu = UIView()
+        view.addSubview(bottomMenu)
+        bottomMenu.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.bottom.equalToSuperview()
+        }
+        
+        let bottomGradient = CAGradientLayer()
+        bottomGradient.colors = [
+            UIColor.white.withAlphaComponent(0).cgColor,
+            UIColor.white.cgColor,
+            UIColor.white.cgColor
+        ]
+        bottomGradient.locations = [0, 0.5, 1]
+        bottomGradient.startPoint = .init(x: 0, y: 0)
+        bottomGradient.endPoint = .init(x: 0, y: 1)
+        bottomMenu.layer.addSublayer(bottomGradient)
+        bottomMenu.publisher(for: \.bounds).removeDuplicates().sink { [weak bottomGradient] newValue in
+            guard let bottomGradient else { return }
+            bottomGradient.frame = newValue
+        }.store(in: &bag)
+        
+        confirmButton.setTitle(.init(key: "A00002"), for: .normal)
+        confirmButton.setTitleColor(.text_1, for: .normal)
+        confirmButton.titleLabel?.font = .heading_h3
+        confirmButton.layer.cornerRadius = 23.5
+        confirmButton.backgroundColor = .fill_4
+        confirmButton.clipsToBounds = true
+        confirmButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            commit()
+        }), for: .touchUpInside)
+        bottomMenu.addSubview(confirmButton)
+        confirmButton.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(16)
+            make.bottom.equalToSuperview().offset(view.commonBottomInset)
+            make.top.equalToSuperview()
+            make.height.equalTo(47)
+        }
+    }
+}

+ 134 - 0
Lanu/Views/Game/OrderCenter/Skill/LNSkillFieldsEditView.swift

@@ -0,0 +1,134 @@
+//
+//  LNSkillFieldsEditView.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/1/25.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNSkillFieldsEditView: UIView {
+    private let stackView = UIStackView()
+    
+    private let titleLabel = UILabel()
+    private var fieldViews: [LNSkillFieldBaseEditView] = []
+    
+    weak var delegate: LNSkillFieldBaseEditViewDelegate?
+    
+    var hasAllInput: Bool {
+        fieldViews.first { !$0.hasInput() } == nil
+    }
+    
+    var checkAvailable: Bool {
+        fieldViews.first { !$0.checkAvailable() } == nil
+    }
+    
+    var needReview: Bool {
+        fieldViews.first { $0.needReview } != nil
+    }
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setupViews()
+    }
+    
+    func update(_ name: String, fields: [LNSkillEditField]) {
+        stackView.arrangedSubviews.forEach {
+            stackView.removeArrangedSubview($0)
+            $0.removeFromSuperview()
+        }
+        
+        titleLabel.text = name
+        
+        for field in fields {
+            let view: LNSkillFieldBaseEditView? =
+            if field.fieldCode == LNSkillEditStaticFieldCode.fieldUnitPrice.rawValue {
+                LNSkillFieldPriceEditView()
+            } else {
+                switch field.type {
+                case .singleLineText, .number:
+                    LNSkillFieldSingleLineEditView()
+                case .multiLineText:
+                    LNSkillFieldMultiLineEditView()
+                case .singleSelection, .multiSelection:
+                    LNSkillFieldSelectionEditView()
+                case .voice:
+                    LNSkillFieldVoiceEditView()
+                case .photo:
+                    LNSkillFieldPhotoEditView()
+                default:
+                    nil
+                }
+            }
+            if let view {
+                view.delegate = self
+                view.update(field)
+                stackView.addArrangedSubview(view)
+                fieldViews.append(view)
+            }
+        }
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNSkillFieldsEditView: LNSkillFieldBaseEditViewDelegate {
+    func onSkillFieldBaseEditViewInputChanged(view: LNSkillFieldBaseEditView) {
+        delegate?.onSkillFieldBaseEditViewInputChanged(view: view)
+    }
+}
+
+extension LNSkillFieldsEditView {
+    private func setupViews() {
+        layer.cornerRadius = 12
+        layer.backgroundColor = .fill
+        
+        let header = buildHeader()
+        addSubview(header)
+        header.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalToSuperview()
+        }
+        
+        stackView.axis = .vertical
+        stackView.spacing = 20
+        addSubview(stackView)
+        stackView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(16)
+            make.top.equalTo(header.snp.bottom).offset(10)
+            make.bottom.equalToSuperview().offset(-10)
+        }
+    }
+    
+    private func buildHeader() -> UIView {
+        let container = UIView()
+        container.snp.makeConstraints { make in
+            make.height.equalTo(46)
+        }
+        
+        titleLabel.font = .heading_h2
+        titleLabel.textColor = .text_5
+        container.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(16)
+            make.centerY.equalToSuperview()
+        }
+        
+        let line = UIView()
+        line.backgroundColor = .fill_2
+        container.addSubview(line)
+        line.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().offset(16)
+            make.bottom.equalToSuperview()
+            make.height.equalTo(1)
+        }
+        
+        return container
+    }
+}

+ 260 - 0
Lanu/Views/Game/OrderCenter/Skill/LNSkillManagerViewController.swift

@@ -0,0 +1,260 @@
+//
+//  LNSkillManagerViewController.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/1/23.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+extension UIView {
+    func pushToSkillManager() {
+        let vc = LNSkillManagerViewController()
+        navigationController?.pushViewController(vc, animated: true)
+    }
+}
+
+
+class LNSkillManagerViewController: LNViewController {
+    private let stackView = UIStackView()
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        setupViews()
+        
+        loadList()
+    }
+}
+
+extension LNSkillManagerViewController {
+    private func loadList() {
+        showLoading()
+        LNGameMateManager.shared.getMySkillList { [weak self] res in
+            dismissLoading()
+            guard let self else { return }
+            guard let res else {
+                navigationController?.popViewController(animated: true)
+                return
+            }
+            
+            stackView.arrangedSubviews.forEach {
+                self.stackView.removeArrangedSubview($0)
+                $0.removeFromSuperview()
+            }
+            
+            for skill in res.list {
+                let view = buildSkillItem(skill)
+                stackView.addArrangedSubview(view)
+            }
+        }
+    }
+}
+
+extension LNSkillManagerViewController {
+    private func setupViews() {
+        title = .init(key: "B00076")
+        view.backgroundColor = .primary_1
+        
+        let settingButton = UIButton()
+        settingButton.setImage(.icSettings, for: .normal)
+        settingButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            view.pushToOrderAcceptSettings()
+        }), for: .touchUpInside)
+        setRightButton(settingButton)
+        
+        let scrollView = UIScrollView()
+        scrollView.showsVerticalScrollIndicator = false
+        scrollView.showsHorizontalScrollIndicator = false
+        scrollView.contentInset = .init(top: 12, left: 0, bottom: -view.commonBottomInset + 50, right: 0)
+        view.addSubview(scrollView)
+        scrollView.snp.makeConstraints { make in
+            make.verticalEdges.equalToSuperview()
+            make.horizontalEdges.equalToSuperview().inset(16)
+        }
+        
+        stackView.axis = .vertical
+        stackView.spacing = 10
+        scrollView.addSubview(stackView)
+        stackView.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+            make.width.equalToSuperview()
+        }
+        
+        let createSkillButton = UIButton()
+        createSkillButton.setTitle(.init(key: "B00077"), for: .normal)
+        createSkillButton.setTitleColor(.text_1, for: .normal)
+        createSkillButton.titleLabel?.font = .heading_h3
+        createSkillButton.layer.cornerRadius = 23.5
+        createSkillButton.backgroundColor = .fill_4
+        createSkillButton.clipsToBounds = true
+        createSkillButton.setBackgroundImage(.primary_8, for: .normal)
+        createSkillButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            view.pushToGameCategoryList { [weak self] category, game in
+                guard let self else { return }
+                showLoading()
+                LNGameMateManager.shared.getCreateSkillFields(id: game.code) { [weak self] res in
+                    dismissLoading()
+                    guard let self else { return }
+                    guard let res else { return }
+                    view.pushToSkillCreate(skill: game, info: res)
+                }
+            }
+        }), for: .touchUpInside)
+        view.addSubview(createSkillButton)
+        createSkillButton.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(16)
+            make.bottom.equalToSuperview().offset(view.commonBottomInset)
+            make.height.equalTo(47)
+        }
+    }
+    
+    private func buildSkillItem(_ skill: LNMySkillItemVO) -> UIView {
+        let container = UIView()
+        container.layer.cornerRadius = 12
+        container.backgroundColor = .fill
+        container.onTap { [weak self] in
+            guard let self else { return }
+            showLoading()
+            LNGameMateManager.shared.getSkillEditFields(id: skill.id) { [weak self] info in
+                dismissLoading()
+                guard let self else { return }
+                guard let info, !info.fields.isEmpty else { return }
+                
+                info.skillId = skill.id
+                view.pushToSkillEdit(info)
+            }
+        }
+        
+        let infoView = UIView()
+        infoView.isUserInteractionEnabled = false
+        container.addSubview(infoView)
+        infoView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalToSuperview()
+            make.height.equalTo(64)
+        }
+        
+        let icon = UIImageView()
+        icon.sd_setImage(with: URL(string: skill.categoryIcon))
+        infoView.addSubview(icon)
+        icon.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalToSuperview().offset(12)
+            make.width.height.equalTo(50)
+        }
+        
+        let arrow = UIImageView.arrowImageView(size: 16)
+        arrow.tintColor = .text_4
+        infoView.addSubview(arrow)
+        arrow.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview().offset(-12)
+        }
+        
+        let textView = UIView()
+        infoView.addSubview(textView)
+        textView.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalTo(icon.snp.trailing).offset(4)
+            make.trailing.lessThanOrEqualTo(arrow.snp.leading).offset(-4)
+        }
+        
+        let nameLabel = UILabel()
+        nameLabel.text = skill.bizCategoryName
+        nameLabel.font = .heading_h4
+        nameLabel.textColor = .text_5
+        textView.addSubview(nameLabel)
+        nameLabel.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalToSuperview()
+        }
+        
+        let priceView = UIView()
+        textView.addSubview(priceView)
+        priceView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.bottom.equalToSuperview()
+            make.top.equalTo(nameLabel.snp.bottom).offset(4)
+        }
+        
+        let coin = UIImageView.coinImageView()
+        priceView.addSubview(coin)
+        coin.snp.makeConstraints { make in
+            make.leading.equalToSuperview()
+            make.centerY.equalToSuperview()
+            make.width.height.equalTo(14)
+        }
+        
+        let priceLabel = UILabel()
+        priceLabel.text = skill.price.toDisplay
+        priceLabel.font = .heading_h5
+        priceLabel.textColor = .text_5
+        priceView.addSubview(priceLabel)
+        priceLabel.snp.makeConstraints { make in
+            make.verticalEdges.equalToSuperview()
+            make.leading.equalTo(coin.snp.trailing).offset(1)
+        }
+        
+        let unitLabel = UILabel()
+        unitLabel.text = "/1 \(skill.unit)"
+        unitLabel.font = .body_s
+        unitLabel.textColor = .text_4
+        priceView.addSubview(unitLabel)
+        unitLabel.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalTo(priceLabel.snp.trailing).offset(1)
+            make.trailing.lessThanOrEqualToSuperview()
+        }
+        
+        let statusView = UIView()
+        container.addSubview(statusView)
+        statusView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalTo(infoView.snp.bottom).offset(2)
+            make.bottom.equalToSuperview().offset(-8)
+            make.height.equalTo(25)
+        }
+        
+        let titleLabel = UILabel()
+        titleLabel.text = .init(key: "A00169")
+        titleLabel.font = .heading_h5
+        titleLabel.textColor = .text_5
+        statusView.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(18)
+            make.centerY.equalToSuperview()
+        }
+        
+        let scaleX: CGFloat = 40.0 / 51.0
+        let scaleY: CGFloat = 24.5 / 31.0
+        let switchView = UISwitch()
+        switchView.isOn = skill.open
+        switchView.onTintColor = .primary_5
+        switchView.transform = CGAffineTransform(scaleX: scaleX, y: scaleY)
+        switchView.addAction(UIAction(handler: { [weak switchView] _ in
+            guard let switchView else { return }
+            showLoading()
+            LNGameMateManager.shared.enableSkill(skillId: skill.id, open: switchView.isOn)
+            { [weak switchView] success in
+                dismissLoading()
+                guard let switchView else { return }
+                if !success {
+                    switchView.isOn.toggle()
+                }
+            }
+        }), for: .valueChanged)
+        statusView.addSubview(switchView)
+        switchView.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview().offset(-12)
+        }
+        
+        return container
+    }
+}

+ 138 - 0
Lanu/Views/Game/OrderCenter/Visitors/LNVisitorItemCell.swift

@@ -0,0 +1,138 @@
+//
+//  LNVisitorItemCell.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/12/12.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+class LNVisitorItemCell: UITableViewCell {
+    private let avatar = UIImageView()
+    private let onlineView = LNOnlineView()
+    private let nameLabel = UILabel()
+    private let genderView = LNGenderView()
+    private let visitTimeLabel = UILabel()
+    private let descLabel = UILabel()
+    
+    private var curItem: LNVisitorItemVO?
+    
+    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+        super.init(style: style, reuseIdentifier: reuseIdentifier)
+        
+        setupViews()
+        LNEventDeliver.addObserver(self)
+    }
+    
+    func update(_ item: LNVisitorItemVO) {
+        avatar.sd_setImage(with: URL(string: item.avatar))
+        onlineView.isHidden = !item.online
+        nameLabel.text = item.nickname
+        
+//        visitTimeLabel.text = Double(item.visitTime).tencentIMTimeDesc
+//        visitTimeLabel.isHidden = item.visitTime == 0
+        visitTimeLabel.isHidden = true
+        
+        genderView.update(item.gender, item.age)
+        
+        curItem = item
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNVisitorItemCell {
+    private func setupViews() {
+        backgroundColor = .clear
+        
+        avatar.layer.cornerRadius = 20
+        avatar.clipsToBounds = true
+        contentView.addSubview(avatar)
+        avatar.snp.makeConstraints { make in
+            make.width.height.equalTo(40)
+            make.leading.equalToSuperview().offset(21)
+            make.top.equalToSuperview().offset(5)
+            make.bottom.equalToSuperview().offset(-25)
+        }
+        
+        contentView.addSubview(onlineView)
+        onlineView.snp.makeConstraints { make in
+            make.edges.equalTo(avatar).inset(-2)
+        }
+        
+        let tips = buildTipsView()
+        contentView.addSubview(tips)
+        tips.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview().offset(-16)
+        }
+        
+        let infoView = buildInfoView()
+        contentView.addSubview(infoView)
+        infoView.snp.makeConstraints { make in
+            make.leading.equalTo(avatar.snp.trailing).offset(13)
+            make.centerY.equalTo(avatar)
+            make.trailing.equalTo(tips.snp.leading).offset(-12)
+        }
+        
+        contentView.onTap { [weak self] in
+            guard let self else { return }
+            guard let curItem else { return }
+            pushToProfile(uid: curItem.userNO)
+        }
+    }
+    
+    private func buildInfoView() -> UIView {
+        let stackView = UIStackView()
+        stackView.axis = .vertical
+        stackView.spacing = 4
+        
+        stackView.addArrangedSubview(buildNameView())
+        stackView.addArrangedSubview(buildDescView())
+        
+        return stackView
+    }
+    
+    private func buildNameView() -> UIView {
+        let container = UIView()
+        
+        nameLabel.font = .heading_h4
+        nameLabel.textColor = .text_5
+        container.addSubview(nameLabel)
+        nameLabel.snp.makeConstraints { make in
+            make.leading.equalToSuperview()
+            make.verticalEdges.equalToSuperview()
+        }
+        
+        container.addSubview(genderView)
+        genderView.snp.makeConstraints { make in
+            make.leading.equalTo(nameLabel.snp.trailing).offset(4)
+            make.centerY.equalToSuperview()
+            make.trailing.lessThanOrEqualToSuperview()
+        }
+        
+        return container
+    }
+    
+    private func buildDescView() -> UIView {
+        visitTimeLabel.isHidden = true
+        visitTimeLabel.font = .body_xs
+        visitTimeLabel.textColor = .text_3
+        
+        return visitTimeLabel
+    }
+    
+    private func buildTipsView() -> UIView {
+        descLabel.text = .init(key: "B00092")
+        descLabel.font = .body_xs
+        descLabel.textColor = .text_4
+        descLabel.setContentHuggingPriority(.required, for: .horizontal)
+        descLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
+        
+        return descLabel
+    }
+}

+ 147 - 0
Lanu/Views/Game/OrderCenter/Visitors/LNVisitorsViewController.swift

@@ -0,0 +1,147 @@
+//
+//  LNVisitorsViewController.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/1/25.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+import Combine
+import MJRefresh
+
+
+extension UIView {
+    func pushToVisitors() {
+        let vc = LNVisitorsViewController()
+        navigationController?.pushViewController(vc, animated: true)
+    }
+}
+
+
+class LNVisitorsViewController: LNViewController {
+    private let permissionView = LNIMNotificationPermissionView()
+    private let tableView = UITableView()
+    
+    private var visitors: [LNVisitorItemVO] = []
+    private var nextTag: String?
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        setupViews()
+        tableView.mj_header?.beginRefreshing()
+    }
+}
+
+extension LNVisitorsViewController {
+    private func loadList() {
+        LNGameMateManager.shared.getVisitorsList(next: nextTag)
+        { [weak self] list, next in
+            guard let self else { return }
+            guard let list else {
+                tableView.mj_header?.endRefreshing()
+                tableView.mj_footer?.endRefreshingWithNoMoreData()
+                return
+            }
+            
+            if nextTag?.isEmpty != false {
+                visitors = list
+            } else {
+                visitors.append(contentsOf: list)
+            }
+            nextTag = next
+            
+            tableView.reloadData()
+            
+            self.tableView.mj_header?.endRefreshing()
+            if next?.isEmpty != false {
+                tableView.mj_footer?.endRefreshingWithNoMoreData()
+            } else {
+                tableView.mj_footer?.endRefreshing()
+            }
+        }
+    }
+}
+
+extension LNVisitorsViewController: UITableViewDataSource, UITableViewDelegate {
+    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+        visitors.count
+    }
+    
+    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+        let cell = tableView.dequeueReusableCell(withIdentifier: LNVisitorItemCell.className, for: indexPath) as! LNVisitorItemCell
+        
+        let item = visitors[indexPath.row]
+        cell.update(item)
+        
+        return cell
+    }
+}
+
+extension LNVisitorsViewController {
+    private func setupViews() {
+        title = .init(key: "B00091")
+        view.backgroundColor = .primary_1
+        
+        let stackView = UIStackView()
+        stackView.axis = .vertical
+        view.addSubview(stackView)
+        stackView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalToSuperview().offset(10)
+            make.bottom.equalToSuperview()
+        }
+        
+        stackView.addArrangedSubview(buildPermission())
+        stackView.addArrangedSubview(buildList())
+    }
+    
+    private func buildPermission() -> UIView {
+        let container = UIView()
+        
+        permissionView.publisher(for: \.isHidden).removeDuplicates().sink
+        { [weak container] newValue in
+            guard let container else { return }
+            container.isHidden = newValue
+        }.store(in: &bag)
+        container.addSubview(permissionView)
+        permissionView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.bottom.equalToSuperview().offset(-10)
+            make.top.equalToSuperview()
+        }
+        
+        return container
+    }
+    
+    private func buildList() -> UIView {
+        let header = MJRefreshNormalHeader { [weak self] in
+            guard let self else { return }
+            nextTag = nil
+            loadList()
+        }
+        header.lastUpdatedTimeLabel?.isHidden = true
+        header.stateLabel?.isHidden = true
+        tableView.mj_header = header
+        
+        let footer = MJRefreshAutoNormalFooter { [weak self] in
+            guard let self else { return }
+            loadList()
+        }
+        footer.setTitle("", for: .noMoreData)
+        footer.setTitle(.init(key: "A00046"), for: .idle)
+        
+        tableView.delegate = self
+        tableView.dataSource = self
+        tableView.allowsSelection = false
+        tableView.showsVerticalScrollIndicator = false
+        tableView.separatorStyle = .none
+        tableView.backgroundColor = .clear
+        tableView.contentInset = .init(top: 0, left: 0, bottom: -view.commonBottomInset, right: 0)
+        tableView.register(LNVisitorItemCell.self, forCellReuseIdentifier: LNVisitorItemCell.className)
+        
+        return tableView
+    }
+}

+ 19 - 10
Lanu/Views/Game/Join/Input/SkillInfo/InputViews/LNJoinUsSkillInfoInputFieldView.swift → Lanu/Views/Game/Skill/Edit/LNSkillFieldBaseEditView.swift

@@ -1,5 +1,5 @@
 //
-//  LNJoinUsSkillInfoInputFieldView.swift
+//  LNSkillFieldBaseEditView.swift
 //  Gami
 //
 //  Created by OneeChan on 2026/1/21.
@@ -10,17 +10,18 @@ import UIKit
 import SnapKit
 
 
-protocol LNJoinUsInputFieldViewDelegate: NSObject {
-    func joinUsInputFieldViewInputChanged(view: LNJoinUsSkillInfoInputFieldView)
+protocol LNSkillFieldBaseEditViewDelegate: NSObject {
+    func onSkillFieldBaseEditViewInputChanged(view: LNSkillFieldBaseEditView)
 }
 
 
-class LNJoinUsSkillInfoInputFieldView: LNJoinUsInputFieldGroupView {
-    private(set) var field: LNJoinUsField?
+class LNSkillFieldBaseEditView: LNJoinUsInputFieldGroupView {
+    private(set) var field: LNSkillEditField?
+    var needReview: Bool = false
     
-    weak var delegate: LNJoinUsInputFieldViewDelegate?
+    weak var delegate: LNSkillFieldBaseEditViewDelegate?
     
-    func update(_ field: LNJoinUsField) {
+    func update(_ field: LNSkillEditField) {
         titleLabel.text = field.fieldName
         
         descLabel.text = field.fieldDesc
@@ -149,16 +150,24 @@ class LNJoinUsSkillInfoInputFieldView: LNJoinUsInputFieldGroupView {
             }
             if let number = field.value.intValue {
                 if Double(number) < limit.min {
-                    showToast(.init(key: "B00067", field.fieldName, limit.min))
+                    showToast(.init(key: "B00067", field.fieldName, limit.min.toDisplay))
                     return false
                 } else if Double(number) > limit.max {
-                    showToast(.init(key: "B00068", field.fieldName, limit.max))
+                    showToast(.init(key: "B00068", field.fieldName, limit.max.toDisplay))
                     return false
                 } else {
                     return true
                 }
             } else if let number = field.value.doubleValue {
-                return number >= limit.min && number <= limit.max
+                if number < limit.min {
+                    showToast(.init(key: "B00067", field.fieldName, limit.min.toDisplay))
+                    return false
+                } else if number > limit.max {
+                    showToast(.init(key: "B00068", field.fieldName, limit.max.toDisplay))
+                    return false
+                } else {
+                    return true
+                }
             } else {
                 return false
             }

+ 7 - 5
Lanu/Views/Game/Join/Input/SkillInfo/InputViews/LNJoinUsMultiLineTextInputView.swift → Lanu/Views/Game/Skill/Edit/LNSkillFieldMultiLineEditView.swift

@@ -10,7 +10,7 @@ import UIKit
 import SnapKit
 
 
-class LNJoinUsMultiLineTextInputView: LNJoinUsSkillInfoInputFieldView {
+class LNSkillFieldMultiLineEditView: LNSkillFieldBaseEditView {
     private let inputField = LNCommonTextView()
     
     override init(frame: CGRect) {
@@ -19,7 +19,7 @@ class LNJoinUsMultiLineTextInputView: LNJoinUsSkillInfoInputFieldView {
         setupViews()
     }
     
-    override func update(_ field: LNJoinUsField) {
+    override func update(_ field: LNSkillEditField) {
         super.update(field)
         
         if let size = field.validate.size {
@@ -28,6 +28,7 @@ class LNJoinUsMultiLineTextInputView: LNJoinUsSkillInfoInputFieldView {
         if let text = field.value.stringValue {
             inputField.setText(text)
         }
+        needReview = false
     }
     
     required init(coder: NSCoder) {
@@ -35,15 +36,16 @@ class LNJoinUsMultiLineTextInputView: LNJoinUsSkillInfoInputFieldView {
     }
 }
 
-extension LNJoinUsMultiLineTextInputView: UITextViewDelegate {
+extension LNSkillFieldMultiLineEditView: UITextViewDelegate {
     func textViewDidChange(_ textView: UITextView) {
         field?.value.value = textView.text
+        needReview = true
         
-        delegate?.joinUsInputFieldViewInputChanged(view: self)
+        delegate?.onSkillFieldBaseEditViewInputChanged(view: self)
     }
 }
 
-extension LNJoinUsMultiLineTextInputView {
+extension LNSkillFieldMultiLineEditView {
     private func setupViews() {
         inputField.delegate = self
         container.addSubview(inputField)

+ 11 - 7
Lanu/Views/Game/Join/Input/SkillInfo/InputViews/LNJoinUsPhotoInputView.swift → Lanu/Views/Game/Skill/Edit/LNSkillFieldPhotoEditView.swift

@@ -1,5 +1,5 @@
 //
-//  LNJoinUsPhotoInputView.swift
+//  LNSkillFieldPhotoEditView.swift
 //  Gami
 //
 //  Created by OneeChan on 2026/1/21.
@@ -10,7 +10,7 @@ import UIKit
 import SnapKit
 
 
-class LNJoinUsPhotoInputView: LNJoinUsSkillInfoInputFieldView {
+class LNSkillFieldPhotoEditView: LNSkillFieldBaseEditView {
     private var maxCount = -1
     private let stackView = UIStackView()
     private let addImageButton = UIButton()
@@ -21,7 +21,7 @@ class LNJoinUsPhotoInputView: LNJoinUsSkillInfoInputFieldView {
         setupViews()
     }
     
-    override func update(_ field: LNJoinUsField) {
+    override func update(_ field: LNSkillEditField) {
         super.update(field)
         
         if let limit = field.validate.arraySize {
@@ -48,12 +48,14 @@ class LNJoinUsPhotoInputView: LNJoinUsSkillInfoInputFieldView {
     }
 }
 
-extension LNJoinUsPhotoInputView: LNUploadImageViewDelegate {
+extension LNSkillFieldPhotoEditView: LNUploadImageViewDelegate {
     func onUploadImageView(view: LNUploadImageView, didUploadImage url: String) {
         guard let field else { return }
         let imageViews = stackView.arrangedSubviews.filter { $0 is LNUploadImageView } as! [LNUploadImageView]
         field.value.value = imageViews.compactMap({ $0.imageUrl })
-        delegate?.joinUsInputFieldViewInputChanged(view: self)
+        delegate?.onSkillFieldBaseEditViewInputChanged(view: self)
+        
+        needReview = true
     }
     
     func onUploadImageViewDidClickDelete(view: LNUploadImageView) {
@@ -64,12 +66,14 @@ extension LNJoinUsPhotoInputView: LNUploadImageViewDelegate {
         let imageViews = stackView.arrangedSubviews.filter { $0 is LNUploadImageView } as! [LNUploadImageView]
         field.value.value = imageViews.compactMap({ $0.imageUrl })
         
-        delegate?.joinUsInputFieldViewInputChanged(view: self)
+        delegate?.onSkillFieldBaseEditViewInputChanged(view: self)
         updateImageCount()
+        
+        needReview = true
     }
 }
 
-extension LNJoinUsPhotoInputView {
+extension LNSkillFieldPhotoEditView {
     private func updateImageCount() {
         guard maxCount > 0 else { return }
         let imageViews = stackView.arrangedSubviews.filter { $0 is LNUploadImageView }

+ 187 - 0
Lanu/Views/Game/Skill/Edit/LNSkillFieldPriceEditView.swift

@@ -0,0 +1,187 @@
+//
+//  LNSkillFieldPriceEditView.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/1/25.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNSkillFieldPriceEditView: LNSkillFieldBaseEditView {
+    private let curValueLabel = UILabel()
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setupViews()
+    }
+    
+    override func update(_ field: LNSkillEditField) {
+        super.update(field)
+        
+        if let text = field.value.doubleValue {
+            curValueLabel.text = text.toDisplay
+        } else {
+            curValueLabel.text = "0"
+        }
+    }
+    
+    required init(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNSkillFieldPriceEditView {
+    private func setupViews() {
+        container.backgroundColor = .fill_2
+        container.layer.cornerRadius = 19
+        container.onTap { [weak self] in
+            guard let self else { return }
+            guard let field else { return }
+            let panel = LNSkillPriceEditPanel()
+            panel.titleLabel.text = field.fieldName
+            panel.inputField.text = field.value.doubleValue?.toDisplay
+            panel.handler = { [weak self] price in
+                guard let self else { return }
+                curValueLabel.text = price.toDisplay
+                field.value.value = price
+                
+                delegate?.onSkillFieldBaseEditViewInputChanged(view: self)
+            }
+            panel.popup()
+        }
+        container.snp.makeConstraints { make in
+            make.height.equalTo(38)
+        }
+        
+        let arrow = UIImageView.arrowImageView(size: 15)
+        arrow.tintColor = .text_3
+        container.addSubview(arrow)
+        arrow.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview().offset(-16)
+        }
+        
+        let coin = UIImageView.coinImageView()
+        container.addSubview(coin)
+        coin.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalToSuperview().offset(16)
+            make.width.height.equalTo(18)
+        }
+        
+        curValueLabel.font = .heading_h4
+        curValueLabel.textColor = .text_5
+        container.addSubview(curValueLabel)
+        curValueLabel.snp.makeConstraints { make in
+            make.leading.equalTo(coin.snp.trailing).offset(1)
+            make.centerY.equalToSuperview()
+        }
+    }
+}
+
+private class LNSkillPriceEditPanel: LNPopupView {
+    let titleLabel = UILabel()
+    let inputField = UITextField()
+    private let confirmButton = UIButton()
+    
+    var handler: ((Double) -> Void)?
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        let headerView = UIView()
+        headerView.isUserInteractionEnabled = false
+        container.addSubview(headerView)
+        headerView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalToSuperview()
+            make.height.equalTo(50)
+        }
+        
+        titleLabel.font = .heading_h3
+        titleLabel.textColor = .text_5
+        titleLabel.textAlignment = .center
+        headerView.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.horizontalEdges.equalToSuperview().inset(16)
+        }
+        
+        let inputView = UIView()
+        container.addSubview(inputView)
+        inputView.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.top.equalTo(headerView.snp.bottom).offset(12)
+            make.leading.greaterThanOrEqualToSuperview().offset(32)
+        }
+        
+        let coin = UIImageView.coinImageView()
+        inputView.addSubview(coin)
+        coin.snp.makeConstraints { make in
+            make.leading.equalToSuperview()
+            make.centerY.equalToSuperview()
+            make.width.height.equalTo(24)
+        }
+        
+        inputField.font = .heading_h1
+        inputField.textColor = .text_5
+        inputField.placeholder = .init(key: "B00097")
+        inputField.keyboardType = .decimalPad
+        inputField.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            let inputAvailable = if let text = inputField.text, Double(text) != nil {
+                true
+            } else {
+                false
+            }
+            if inputAvailable != confirmButton.isEnabled {
+                confirmButton.isEnabled = inputAvailable
+                confirmButton.setBackgroundImage(inputAvailable ? .primary_8 : nil, for: .normal)
+            }
+        }), for: .editingChanged)
+        inputView.addSubview(inputField)
+        inputField.snp.makeConstraints { make in
+            make.verticalEdges.equalToSuperview()
+            make.leading.equalTo(coin.snp.trailing).offset(2)
+            make.trailing.equalToSuperview()
+            make.width.greaterThanOrEqualTo(180)
+        }
+        
+        let line = UIView()
+        line.backgroundColor = .fill_2
+        inputView.addSubview(line)
+        line.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.bottom.equalToSuperview()
+            make.height.equalTo(1)
+        }
+        
+        confirmButton.setTitle(.init(key: "A00002"), for: .normal)
+        confirmButton.setTitleColor(.text_1, for: .normal)
+        confirmButton.titleLabel?.font = .heading_h3
+        confirmButton.layer.cornerRadius = 23.5
+        confirmButton.clipsToBounds = true
+        confirmButton.isEnabled = false
+        confirmButton.backgroundColor = .fill_4
+        confirmButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            dismiss()
+            handler?(Double(inputField.text ?? "0") ?? 0)
+        }), for: .touchUpInside)
+        container.addSubview(confirmButton)
+        confirmButton.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(16)
+            make.top.equalTo(inputView.snp.bottom).offset(60)
+            make.bottom.equalToSuperview().offset(commonBottomInset)
+            make.height.equalTo(47)
+        }
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}

+ 21 - 6
Lanu/Views/Game/Join/Input/SkillInfo/InputViews/LNJoinUsSelectionInputView.swift → Lanu/Views/Game/Skill/Edit/LNSkillFieldSelectionEditView.swift

@@ -1,5 +1,5 @@
 //
-//  LNJoinUsSelectionInputView.swift
+//  LNSkillFieldSelectionEditView.swift
 //  Gami
 //
 //  Created by OneeChan on 2026/1/20.
@@ -10,7 +10,7 @@ import UIKit
 import SnapKit
 
 
-class LNJoinUsSelectionInputView: LNJoinUsSkillInfoInputFieldView {
+class LNSkillFieldSelectionEditView: LNSkillFieldBaseEditView {
     private let curValueLabel = UILabel()
     
     override init(frame: CGRect) {
@@ -19,7 +19,7 @@ class LNJoinUsSelectionInputView: LNJoinUsSkillInfoInputFieldView {
         setupViews()
     }
     
-    override func update(_ field: LNJoinUsField) {
+    override func update(_ field: LNSkillEditField) {
         super.update(field)
         
         if case .multiSelection = field.type,
@@ -36,6 +36,12 @@ class LNJoinUsSelectionInputView: LNJoinUsSkillInfoInputFieldView {
             }
             curValueLabel.text = values.joined(separator: ",")
         }
+        if curValueLabel.text?.isEmpty != false {
+            curValueLabel.attributedText = .init(string: .init(key: "B00094"), attributes: [
+                .font: UIFont.body_m,
+                .foregroundColor: UIColor.text_3
+            ])
+        }
     }
     
     required init(coder: NSCoder) {
@@ -43,13 +49,18 @@ class LNJoinUsSelectionInputView: LNJoinUsSkillInfoInputFieldView {
     }
 }
 
-extension LNJoinUsSelectionInputView {
+extension LNSkillFieldSelectionEditView {
     private func setupViews() {
         container.backgroundColor = .fill_2
         container.layer.cornerRadius = 19
         container.onTap { [weak self] in
             guard let self else { return }
             guard let field else { return }
+            guard !field.constants.isEmpty else {
+                showToast(.init(key: "B00098"))
+                return
+            }
+            
             switch field.type {
 //            case .date:
 //                let panel = LNDatePickerPanel()
@@ -76,9 +87,11 @@ extension LNJoinUsSelectionInputView {
                 
                 panel.handler = { [weak self] index in
                     guard let self else { return }
+                    curValueLabel.attributedText = nil
                     curValueLabel.text = field.constants[index].value
                     field.value.value = field.constants[index].key
-                    delegate?.joinUsInputFieldViewInputChanged(view: self)
+                    needReview = field.fieldCode != LNSkillEditStaticFieldCode.fieldUnit.rawValue
+                    delegate?.onSkillFieldBaseEditViewInputChanged(view: self)
                 }
                 panel.popup()
             case .multiSelection:
@@ -106,9 +119,11 @@ extension LNJoinUsSelectionInputView {
                         texts.append(field.constants[index].value)
                         keys.append(field.constants[index].key)
                     }
+                    curValueLabel.attributedText = nil
                     curValueLabel.text = texts.joined(separator: ",")
                     field.value.value = keys
-                    delegate?.joinUsInputFieldViewInputChanged(view: self)
+                    needReview = true
+                    delegate?.onSkillFieldBaseEditViewInputChanged(view: self)
                 }
                 panel.popup()
             default: break

+ 9 - 7
Lanu/Views/Game/Join/Input/SkillInfo/InputViews/LNJoinUsSingleLineTextInputView.swift → Lanu/Views/Game/Skill/Edit/LNSkillFieldSingleLineEditView.swift

@@ -1,5 +1,5 @@
 //
-//  LNJoinUsSingleLineTextInputView.swift
+//  LNSkillFieldSingleLineEditView.swift
 //  Gami
 //
 //  Created by OneeChan on 2026/1/20.
@@ -10,7 +10,7 @@ import UIKit
 import SnapKit
 
 
-class LNJoinUsSingleLineTextInputView: LNJoinUsSkillInfoInputFieldView {
+class LNSkillFieldSingleLineEditView: LNSkillFieldBaseEditView {
     private let countLabel = UILabel()
     private let inputField = UITextField()
     
@@ -20,7 +20,7 @@ class LNJoinUsSingleLineTextInputView: LNJoinUsSkillInfoInputFieldView {
         setupViews()
     }
     
-    override func update(_ field: LNJoinUsField) {
+    override func update(_ field: LNSkillEditField) {
         super.update(field)
         
         inputField.text = field.value.stringValue
@@ -33,6 +33,7 @@ class LNJoinUsSingleLineTextInputView: LNJoinUsSkillInfoInputFieldView {
         case .double: .decimalPad
         case .unknown: .default
         }
+        needReview = false
     }
     
     required init(coder: NSCoder) {
@@ -40,7 +41,7 @@ class LNJoinUsSingleLineTextInputView: LNJoinUsSkillInfoInputFieldView {
     }
 }
 
-extension LNJoinUsSingleLineTextInputView: UITextFieldDelegate {
+extension LNSkillFieldSingleLineEditView: UITextFieldDelegate {
     func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
         guard let size = field?.validate.size else { return true }
         let currentText = textField.text ?? ""
@@ -60,7 +61,7 @@ extension LNJoinUsSingleLineTextInputView: UITextFieldDelegate {
     }
 }
 
-extension LNJoinUsSingleLineTextInputView {
+extension LNSkillFieldSingleLineEditView {
     private func setupViews() {
         container.backgroundColor = .fill_2
         container.layer.cornerRadius = 19
@@ -80,7 +81,7 @@ extension LNJoinUsSingleLineTextInputView {
         
         inputField.font = .body_m
         inputField.textColor = .text_5
-        inputField.placeholder = .init(key: "B00043")
+        inputField.placeholder = .init(key: "A00006")
         inputField.delegate = self
         inputField.returnKeyType = .done
         inputField.addAction(UIAction(handler: { [weak self] _ in
@@ -98,7 +99,8 @@ extension LNJoinUsSingleLineTextInputView {
                 case .unknown: break
                 }
             }
-            delegate?.joinUsInputFieldViewInputChanged(view: self)
+            needReview = true
+            delegate?.onSkillFieldBaseEditViewInputChanged(view: self)
         }), for: .editingChanged)
         stackView.addArrangedSubview(inputField)
         

+ 9 - 8
Lanu/Views/Game/Join/Input/SkillInfo/InputViews/LNJoinUsVoiceInputView.swift → Lanu/Views/Game/Skill/Edit/LNSkillFieldVoiceEditView.swift

@@ -1,5 +1,5 @@
 //
-//  LNJoinUsVoiceInputView.swift
+//  LNSkillFieldVoiceEditView.swift
 //  Gami
 //
 //  Created by OneeChan on 2026/1/21.
@@ -10,7 +10,7 @@ import UIKit
 import SnapKit
 
 
-class LNJoinUsVoiceInputView: LNJoinUsSkillInfoInputFieldView {
+class LNSkillFieldVoiceEditView: LNSkillFieldBaseEditView {
     private var minDuration: Double?
     private var maxDuration: Double?
     
@@ -34,7 +34,7 @@ class LNJoinUsVoiceInputView: LNJoinUsSkillInfoInputFieldView {
         LNVoiceRecorder.shared.prepare()
     }
     
-    override func update(_ field: LNJoinUsField) {
+    override func update(_ field: LNSkillEditField) {
         super.update(field)
         
         if let limit = field.validate.size {
@@ -54,7 +54,7 @@ class LNJoinUsVoiceInputView: LNJoinUsSkillInfoInputFieldView {
     }
 }
 
-extension LNJoinUsVoiceInputView {
+extension LNSkillFieldVoiceEditView {
     private func handleRecordResult(url: URL?, duration: Double) {
         guard let url else {
             return
@@ -71,11 +71,12 @@ extension LNJoinUsVoiceInputView {
         resetRecord()
         displayView.isHidden = false
         
-        delegate?.joinUsInputFieldViewInputChanged(view: self)
+        needReview = true
+        delegate?.onSkillFieldBaseEditViewInputChanged(view: self)
     }
 }
 
-extension LNJoinUsVoiceInputView: LNVoicePlayerNotify {
+extension LNSkillFieldVoiceEditView: LNVoicePlayerNotify {
     func onAudioUpdateDuration(path: String, cur: TimeInterval, total: TimeInterval) {
         guard field?.value.stringValue == path else { return }
         
@@ -100,7 +101,7 @@ extension LNJoinUsVoiceInputView: LNVoicePlayerNotify {
     }
 }
 
-extension LNJoinUsVoiceInputView: LNVoiceRecorderNotify {
+extension LNSkillFieldVoiceEditView: LNVoiceRecorderNotify {
     func onRecordTaskDurationChanged(taskId: String, duration: Double, volumeRatio: Double) {
         guard recordTaskId == taskId else { return }
         
@@ -128,7 +129,7 @@ extension LNJoinUsVoiceInputView: LNVoiceRecorderNotify {
     }
 }
 
-extension LNJoinUsVoiceInputView {
+extension LNSkillFieldVoiceEditView {
     private func resetRecord() {
         recordDurationLabel.text = "00:00"
         recordButton.setImage(.icVoiceEditStart, for: .normal)

+ 171 - 27
Lanu/Views/Game/Skill/LNSkillBottomMenuView.swift

@@ -17,22 +17,47 @@ class LNSkillBottomMenuView: UIView {
     
     private var curDetail: LNGameMateSkillDetailVO?
     
-    override init(frame: CGRect) {
-        super.init(frame: frame)
-        
-        setupViews()
-    }
-    
     func update(_ detail: LNGameMateSkillDetailVO) {
         priceLabel.text = detail.price.toDisplay
         unitLabel.text = "/\(detail.unit)"
         
+        if curDetail == nil {
+            if detail.userNo.isMyUid {
+                let editView = buildEditView()
+                addSubview(editView)
+                editView.snp.makeConstraints { make in
+                    make.leading.equalToSuperview().inset(16)
+                    make.bottom.equalToSuperview().offset(-4)
+                    make.top.equalToSuperview().offset(16)
+                }
+                
+                let settingView = buildSettingsView()
+                addSubview(settingView)
+                settingView.snp.makeConstraints { make in
+                    make.leading.equalTo(editView.snp.trailing).offset(10)
+                    make.centerY.equalTo(editView)
+                    make.trailing.equalToSuperview().offset(-16)
+                }
+            } else {
+                let orderView = buildOrderView()
+                addSubview(orderView)
+                orderView.snp.makeConstraints { make in
+                    make.leading.equalToSuperview().inset(16)
+                    make.bottom.equalToSuperview().offset(-4)
+                    make.top.equalToSuperview().offset(16)
+                }
+                
+                let chatView = buildChatView()
+                addSubview(chatView)
+                chatView.snp.makeConstraints { make in
+                    make.leading.equalTo(orderView.snp.trailing).offset(10)
+                    make.centerY.equalTo(orderView)
+                    make.trailing.equalToSuperview().offset(-16)
+                }
+            }
+        }
+        
         curDetail = detail
-        isHidden = detail.userNo.isMyUid
-    }
-    
-    required init?(coder: NSCoder) {
-        fatalError("init(coder:) has not been implemented")
     }
 }
 
@@ -54,22 +79,6 @@ extension LNSkillBottomMenuView {
             guard let bottomGradient else { return }
             bottomGradient.frame = newValue
         }.store(in: &bag)
-        
-        let orderView = buildOrderView()
-        addSubview(orderView)
-        orderView.snp.makeConstraints { make in
-            make.leading.equalToSuperview().inset(16)
-            make.bottom.equalToSuperview().offset(-4)
-            make.top.equalToSuperview().offset(16)
-        }
-        
-        let chatView = buildChatView()
-        addSubview(chatView)
-        chatView.snp.makeConstraints { make in
-            make.leading.equalTo(orderView.snp.trailing).offset(10)
-            make.centerY.equalTo(orderView)
-            make.trailing.equalToSuperview().offset(-16)
-        }
     }
     
     private func buildOrderView() -> UIView {
@@ -188,4 +197,139 @@ extension LNSkillBottomMenuView {
         
         return button
     }
+    
+    private func buildEditView() -> UIView {
+        let container = UIView()
+        container.backgroundColor = .init(hex: "#ECF6FF")
+        container.layer.cornerRadius = 23.5
+        container.snp.makeConstraints { make in
+            make.height.equalTo(47)
+        }
+        
+        let editButton = UIButton()
+        editButton.setBackgroundImage(.icSkillEdit, for: .normal)
+        editButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            guard let curDetail else { return }
+            showLoading()
+            LNGameMateManager.shared.getSkillEditFields(id: curDetail.id) { [weak self] info in
+                dismissLoading()
+                guard let self else { return }
+                guard let info, !info.fields.isEmpty else { return }
+                
+                info.skillId = curDetail.id
+                pushToSkillEdit(info)
+            }
+        }), for: .touchUpInside)
+        container.addSubview(editButton)
+        editButton.snp.makeConstraints { make in
+            make.trailing.equalToSuperview()
+            make.verticalEdges.equalToSuperview()
+        }
+        
+        let editView = UIView()
+        editView.isUserInteractionEnabled = false
+        editButton.addSubview(editView)
+        editView.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.centerX.equalToSuperview().offset(4)
+        }
+        
+        let editIc = UIImageView()
+        editIc.image = .icEditClear.withRenderingMode(.alwaysTemplate)
+        editIc.tintColor = .fill
+        editView.addSubview(editIc)
+        editIc.snp.makeConstraints { make in
+            make.verticalEdges.equalToSuperview()
+            make.leading.equalToSuperview()
+            make.width.height.equalTo(24)
+        }
+        
+        let titleLabel = UILabel()
+        titleLabel.font = .heading_h3
+        titleLabel.textColor = .text_1
+        titleLabel.text = .init(key: "A00226")
+        titleLabel.isUserInteractionEnabled = false
+        editView.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview()
+            make.leading.equalTo(editIc.snp.trailing).offset(2)
+        }
+        
+        let descView = UIView()
+        container.addSubview(descView)
+        descView.snp.makeConstraints { make in
+            make.leading.verticalEdges.equalToSuperview()
+            make.trailing.equalTo(editButton.snp.leading)
+        }
+        
+        let priceView = UIView()
+        descView.addSubview(priceView)
+        priceView.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+        }
+        
+        let coin = UIImageView.coinImageView()
+        priceView.addSubview(coin)
+        coin.snp.makeConstraints { make in
+            make.leading.centerY.equalToSuperview()
+            make.width.height.equalTo(20)
+        }
+        
+        priceLabel.font = .heading_h2
+        priceLabel.textColor = .text_5
+        priceView.addSubview(priceLabel)
+        priceLabel.snp.makeConstraints { make in
+            make.verticalEdges.equalToSuperview()
+            make.leading.equalTo(coin.snp.trailing).offset(4)
+        }
+        
+        unitLabel.font = .body_s
+        unitLabel.textColor = .text_5
+        priceView.addSubview(unitLabel)
+        unitLabel.snp.makeConstraints { make in
+            make.leading.equalTo(priceLabel.snp.trailing)
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview()
+        }
+        
+        return container
+    }
+    
+    private func buildSettingsView() -> UIView {
+        let button = UIButton()
+        button.setBackgroundImage(.primary_3, for: .normal)
+        button.layer.cornerRadius = 23.5
+        button.clipsToBounds = true
+        button.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            guard let curDetail else { return }
+            showLoading()
+            LNGameMateManager.shared.getSkillSwitchInfo(id: curDetail.id) { [weak self] res in
+                dismissLoading()
+                guard let self else { return }
+                guard let res else { return }
+                let panel = LNSkillSettingMenu(skill: curDetail, switchInfo: res)
+                panel.popup()
+            }
+        }), for: .touchUpInside)
+        button.snp.makeConstraints { make in
+            make.height.equalTo(47)
+            make.width.greaterThanOrEqualTo(90)
+        }
+        
+        let settingLabel = UILabel()
+        settingLabel.text = .init(key: "B00095")
+        settingLabel.font = .heading_h3
+        settingLabel.textColor = .text_1
+        settingLabel.textAlignment = .center
+        button.addSubview(settingLabel)
+        settingLabel.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+            make.leading.equalToSuperview().offset(16)
+        }
+        
+        return button
+    }
 }

+ 26 - 17
Lanu/Views/Game/Skill/LNSkillDetailViewController.swift

@@ -47,18 +47,39 @@ class LNSkillDetailViewController: LNViewController {
         
         setupViews()
         
+        loadSkillInfo()
+    }
+    
+    override func viewDidDisappear(_ animated: Bool) {
+        super.viewDidDisappear(animated)
+        
+        if detail?.userNo.isMyUid == true {
+            loadSkillInfo()
+        }
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNSkillDetailViewController {
+    private func loadSkillInfo() {
         LNGameMateManager.shared.getSkillDetail(skillId: skillId) { [weak self] info in
             guard let self else { return }
             guard let info else { return }
             self.detail = info
-            updateContent()
+            
+            cover.sd_setImage(with: URL(string: info.cover.isEmpty ? info.avatar : info.cover))
+            userInfoView.update(info)
+            gameNameLabel.text = info.categoryName
+            descLabel.text = info.summary
+            tagView.update(info.labels)
+            photosView.update(info)
+            bottomMenu.update(info)
             fakeNavBar.update(info)
         }
     }
-    
-    required init?(coder: NSCoder) {
-        fatalError("init(coder:) has not been implemented")
-    }
 }
 
 extension LNSkillDetailViewController: UIScrollViewDelegate {
@@ -76,18 +97,6 @@ extension LNSkillDetailViewController: UIScrollViewDelegate {
 }
 
 extension LNSkillDetailViewController {
-    private func updateContent() {
-        guard let detail else { return }
-        
-        cover.sd_setImage(with: URL(string: detail.cover.isEmpty ? detail.avatar : detail.cover))
-        userInfoView.update(detail)
-        gameNameLabel.text = detail.categoryName
-        descLabel.text = detail.summary
-        tagView.update(detail.labels)
-        photosView.update(detail)
-        bottomMenu.update(detail)
-    }
-    
     private func setupViews() {
         let stackView = UIStackView()
         stackView.axis = .vertical

+ 133 - 0
Lanu/Views/Game/Skill/LNSkillSettingMenu.swift

@@ -0,0 +1,133 @@
+//
+//  LNSkillSettingMenu.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/1/25.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNSkillSettingMenu: LNPopupView {
+    private let skill: LNGameMateSkillDetailVO
+    private let openSwitch = UISwitch()
+    private let mainSkillSwitch = UISwitch()
+    
+    init(skill: LNGameMateSkillDetailVO, switchInfo: LNSkillSwitchResponse) {
+        self.skill = skill
+        super.init(frame: .zero)
+        
+        setupViews()
+        
+        openSwitch.isOn = switchInfo.open
+        mainSkillSwitch.isOn = switchInfo.mainSkill
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNSkillSettingMenu {
+    private func setupViews() {
+        let stackView = UIStackView()
+        stackView.axis = .vertical
+        stackView.spacing = 0
+        
+        container.addSubview(stackView)
+        stackView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalToSuperview().offset(10)
+            make.bottom.equalToSuperview().offset(commonBottomInset)
+        }
+        
+        let scaleX: CGFloat = 40.0 / 51.0
+        let scaleY: CGFloat = 24.5 / 31.0
+        openSwitch.onTintColor = .primary_5
+        openSwitch.transform = CGAffineTransform(scaleX: scaleX, y: scaleY)
+        openSwitch.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            showLoading()
+            LNGameMateManager.shared.enableSkill(skillId: skill.id, open: openSwitch.isOn)
+            { [weak self] success in
+                dismissLoading()
+                guard let self else { return }
+                if !success {
+                    openSwitch.isOn.toggle()
+                }
+            }
+        }), for: .valueChanged)
+        stackView.addArrangedSubview(buildMenuItem(.icWallet, title: .init(key: "A00169"), contentView: openSwitch))
+        stackView.addArrangedSubview(buildLine())
+        
+        mainSkillSwitch.onTintColor = .primary_5
+        mainSkillSwitch.onTintColor = .primary_5
+        mainSkillSwitch.transform = CGAffineTransform(scaleX: scaleX, y: scaleY)
+        mainSkillSwitch.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            showLoading()
+            LNGameMateManager.shared.setAsMainSkill(skillId: skill.id, asMain: mainSkillSwitch.isOn)
+            { [weak self] success in
+                dismissLoading()
+                guard let self else { return }
+                if !success {
+                    mainSkillSwitch.isOn.toggle()
+                }
+            }
+        }), for: .valueChanged)
+        stackView.addArrangedSubview(buildMenuItem(.icTag, title: .init(key: "B00096"), contentView: mainSkillSwitch))
+        stackView.addArrangedSubview(buildLine())
+    }
+    
+    private func buildMenuItem(_ icon: UIImage, title: String, contentView: UIView) -> UIView {
+        let container = UIView()
+        container.snp.makeConstraints { make in
+            make.height.equalTo(50)
+        }
+        
+        let ic = UIImageView()
+        ic.image = icon.withRenderingMode(.alwaysTemplate)
+        ic.tintColor = .text_5
+        container.addSubview(ic)
+        ic.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(16)
+            make.centerY.equalToSuperview()
+            make.width.height.equalTo(18)
+        }
+        
+        let titleLabel = UILabel()
+        titleLabel.text = title
+        titleLabel.font = .body_m
+        titleLabel.textColor = .text_5
+        container.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.leading.equalTo(ic.snp.trailing).offset(10)
+            make.centerY.equalToSuperview()
+        }
+        
+        container.addSubview(contentView)
+        contentView.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview().offset(-16)
+        }
+        
+        return container
+    }
+    
+    private func buildLine() -> UIView {
+        let container = UIView()
+        
+        let line = UIView()
+        line.backgroundColor = .fill_2
+        container.addSubview(line)
+        line.snp.makeConstraints { make in
+            make.verticalEdges.equalToSuperview()
+            make.height.equalTo(1)
+            make.horizontalEdges.equalToSuperview().inset(16)
+        }
+        
+        return container
+    }
+}

+ 2 - 0
Lanu/Views/Game/Skill/LNSkillVoiceBarView.swift

@@ -100,6 +100,7 @@ extension LNSkillVoiceBarView {
         playStateIc.snp.makeConstraints { make in
             make.leading.equalToSuperview().offset(8)
             make.centerY.equalToSuperview()
+            make.width.height.equalTo(22)
         }
         
         voiceWaveView.build()
@@ -114,6 +115,7 @@ extension LNSkillVoiceBarView {
         
         durationLabel.font = .heading_h5
         durationLabel.textColor = .text_1
+        durationLabel.textAlignment = .center
         addSubview(durationLabel)
         durationLabel.snp.makeConstraints { make in
             make.leading.equalTo(voiceWaveView.snp.trailing).offset(4)

+ 8 - 2
Lanu/Views/Home/LNHomeGameMatePanel.swift

@@ -30,7 +30,10 @@ extension LNHomeGameMatePanel: LNHomeGameTabViewDelegate {
     }
     
     func homeGameTabViewClickMore(view: LNHomeGameTabView) {
-        pushToGameCategoryList()
+        pushToGameCategoryList { [weak self] category, game in
+            guard let self else { return }
+            pushToGameMateList(topCategory: category, category: game, filter: LNGameMateFilter())
+        }
     }
 }
 
@@ -41,7 +44,10 @@ extension LNHomeGameMatePanel: LNHomeActivityTabViewDelegate {
     }
     
     func homeActivityTabViewClickMore(view: LNHomeActivityTabView) {
-        pushToGameCategoryList()
+        pushToGameCategoryList { [weak self] category, game in
+            guard let self else { return }
+            pushToGameMateList(topCategory: category, category: game, filter: LNGameMateFilter())
+        }
     }
 }
 

+ 1 - 1
Lanu/Views/Order/Create/LNCreateOrderPanel.swift

@@ -438,7 +438,7 @@ extension LNCreateOrderPanel {
         }
         
         let icon = UIImageView()
-        icon.image = .icWallet
+        icon.image = .icWalletWithBg
         container.addSubview(icon)
         icon.snp.makeConstraints { make in
             make.centerY.equalToSuperview()

+ 1 - 1
Lanu/Views/Order/OrderQR/LNOrderGenerateQRCodePanel.swift

@@ -36,7 +36,7 @@ class LNOrderGenerateQRCodePanel: LNPopupView {
         DispatchQueue.main.async { [weak self] in
             guard let self else { return }
             tabView.curType = .normal
-            curSkill = myGameMateInfo?.skills.first
+            curSkill = myUserInfo.skills.first
         }
     }
     

+ 1 - 1
Lanu/Views/Order/OrderQR/LNOrderShareImageGenerator.swift

@@ -31,7 +31,7 @@ class LNOrderShareImageGenerator {
         avatar.sd_setImage(with: URL(string: myUserInfo.avatar))
         skillIc.sd_setImage(with: URL(string: skill.icon))
         skillNameLabel.text = skill.name
-        scoreView.score = myGameMateInfo?.star ?? 0
+        scoreView.score = myUserInfo.star
         qrCodeView.image = image
         
         countLabel.text = .init(key: "A00162", count, skill.unit)

+ 1 - 1
Lanu/Views/Order/OrderQR/LNOrderSkillListPanel.swift

@@ -40,7 +40,7 @@ extension LNOrderSkillListPanel {
         }
         
         var itemViews: [UIView] = []
-        myGameMateInfo?.skills.forEach {
+        myUserInfo.skills.forEach {
             itemViews.append(buildSkillItem($0))
         }
         itemViews.forEach {

+ 268 - 0
Lanu/Views/Profile/Mine/LNMineOrderRecordView.swift

@@ -0,0 +1,268 @@
+//
+//  LNMineOrderRecordView.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/1/22.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNMineOrderRecordView: UIView {
+    private let incomeLabel = UILabel()
+    private let exposureLabel = UILabel()
+    private let visitorLabel = UILabel()
+    
+    private let statusButton = UIButton()
+    private let statusLabel = UILabel()
+    private let statusDescLabel = UILabel()
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setupViews()
+        
+        LNEventDeliver.addObserver(self)
+        
+        onGameMateManagerInfoChanged()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNMineOrderRecordView: LNGameMateManagerNotify {
+    func onGameMateManagerInfoChanged() {
+        guard let myGameMateInfo else { return }
+        
+        incomeLabel.text = myGameMateInfo.weekBeanIncome.toDisplay
+        exposureLabel.text = "\(myGameMateInfo.exposureCountDay)"
+        visitorLabel.text = "\(myGameMateInfo.visitorCount)"
+        
+        if myGameMateInfo.playmateOpen {
+            statusButton.setBackgroundImage(.primary_7, for: .normal)
+            statusLabel.text = .init(key: "B00087")
+            statusDescLabel.text = .init(key: "B00088")
+        } else {
+            statusButton.setBackgroundImage(.primary_8, for: .normal)
+            statusLabel.text = .init(key: "B00089")
+            statusDescLabel.text = .init(key: "B00090")
+        }
+    }
+}
+
+extension LNMineOrderRecordView {
+    private func setupViews() {
+        layer.cornerRadius = 12
+        backgroundColor = .fill
+        onTap { [weak self] in
+            guard let self else { return }
+            pushToMateCenter()
+        }
+        
+        let header = buildHeaderView()
+        addSubview(header)
+        header.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalToSuperview()
+        }
+        
+        let recordView = buildRecordInfo()
+        addSubview(recordView)
+        recordView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(24)
+            make.top.equalTo(header.snp.bottom)
+        }
+        
+        let openButton = buildOpenButton()
+        addSubview(openButton)
+        openButton.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(12)
+            make.top.equalTo(recordView.snp.bottom).offset(12)
+            make.bottom.equalToSuperview().offset(-12)
+        }
+    }
+    
+    private func buildHeaderView() -> UIView {
+        let container = UIView()
+        container.isUserInteractionEnabled = false
+        container.snp.makeConstraints { make in
+            make.height.equalTo(40)
+        }
+        
+        let titleLabel = UILabel()
+        titleLabel.text = .init(key: "B00071")
+        titleLabel.font = .heading_h3
+        titleLabel.textColor = .text_5
+        container.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalToSuperview().offset(16)
+        }
+        
+        let arrow = UIImageView.arrowImageView(size: 16)
+        arrow.tintColor = .text_4
+        container.addSubview(arrow)
+        arrow.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview().offset(-16)
+        }
+        
+        return container
+    }
+    
+    private func buildRecordInfo() -> UIView {
+        let stackView = UIStackView()
+        stackView.axis = .horizontal
+        stackView.distribution = .equalSpacing
+        stackView.spacing = 28
+        stackView.alignment = .center
+        stackView.isUserInteractionEnabled = false
+        stackView.snp.makeConstraints { make in
+            make.height.equalTo(56)
+        }
+        
+        stackView.addArrangedSubview(buildInCome())
+        stackView.addArrangedSubview(buildLine())
+        stackView.addArrangedSubview(buildDataItemView(.init(key: "B00073"), contentLabel: exposureLabel))
+        stackView.addArrangedSubview(buildDataItemView(.init(key: "B00074"), contentLabel: visitorLabel))
+        
+        return stackView
+    }
+    
+    private func buildInCome() -> UIView {
+        let container = UIView()
+        container.snp.makeConstraints { make in
+            make.width.greaterThanOrEqualTo(120)
+        }
+        
+        let beanView = UIView()
+        container.addSubview(beanView)
+        beanView.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.top.equalToSuperview()
+        }
+        
+        let beanIc = UIImageView.beanImageView()
+        beanView.addSubview(beanIc)
+        beanIc.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalToSuperview()
+            make.width.height.equalTo(20)
+        }
+        
+        incomeLabel.text = "0"
+        incomeLabel.font = .heading_h1
+        incomeLabel.textColor = .text_5
+        beanView.addSubview(incomeLabel)
+        incomeLabel.snp.makeConstraints { make in
+            make.verticalEdges.equalToSuperview()
+            make.trailing.equalToSuperview()
+            make.leading.equalTo(beanIc.snp.trailing)
+        }
+        
+        let desclabel = UILabel()
+        desclabel.text = .init(key: "B00072")
+        desclabel.font = .body_s
+        desclabel.textColor = .text_5
+        desclabel.textAlignment = .center
+        desclabel.numberOfLines = 2
+        container.addSubview(desclabel)
+        desclabel.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.bottom.equalToSuperview()
+            make.top.equalTo(beanView.snp.bottom).offset(6)
+        }
+        
+        return container
+    }
+    
+    private func buildDataItemView(_ title: String, contentLabel: UILabel) -> UIView {
+        let container = UIView()
+        container.snp.makeConstraints { make in
+            make.width.lessThanOrEqualTo(60)
+        }
+        
+        contentLabel.text = "0"
+        contentLabel.font = .heading_h2
+        contentLabel.textColor = .text_5
+        container.addSubview(contentLabel)
+        contentLabel.snp.makeConstraints { make in
+            make.top.equalToSuperview()
+            make.centerX.equalToSuperview()
+        }
+        
+        let descLabel = UILabel()
+        descLabel.text = title
+        descLabel.font = .body_s
+        descLabel.textColor = .text_4
+        descLabel.numberOfLines = 2
+        descLabel.textAlignment = .center
+        container.addSubview(descLabel)
+        descLabel.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.bottom.equalToSuperview()
+            make.top.equalTo(contentLabel.snp.bottom).offset(2)
+        }
+        
+        return container
+    }
+    
+    private func buildOpenButton() -> UIView {
+        statusButton.layer.cornerRadius = 26
+        statusButton.clipsToBounds = true
+        statusButton.setBackgroundImage(.primary_8, for: .normal)
+        statusButton.addAction(UIAction(handler: { _ in
+            showLoading()
+            let isOpen = myGameMateInfo?.playmateOpen == true
+            LNGameMateManager.shared.enableGameMate(open: !isOpen) { success in
+                dismissLoading()
+            }
+        }), for: .touchUpInside)
+        statusButton.snp.makeConstraints { make in
+            make.height.equalTo(56)
+        }
+        
+        let container = UIView()
+        container.isUserInteractionEnabled = false
+        statusButton.addSubview(container)
+        container.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+            make.leading.greaterThanOrEqualToSuperview().offset(16)
+        }
+        
+        statusLabel.font = .heading_h3
+        statusLabel.textColor = .text_1
+        statusLabel.textAlignment = .center
+        container.addSubview(statusLabel)
+        statusLabel.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalToSuperview()
+        }
+        
+        statusDescLabel.font = .body_xs
+        statusDescLabel.textColor = .primary_1
+        container.addSubview(statusDescLabel)
+        statusDescLabel.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.bottom.equalToSuperview()
+            make.top.equalTo(statusLabel.snp.bottom)
+        }
+        
+        return statusButton
+    }
+    
+    private func buildLine() -> UIView {
+        let line = UIView()
+        line.backgroundColor = .init(hex: "#D9D9D9")
+        line.snp.makeConstraints { make in
+            make.width.equalTo(1)
+            make.height.equalTo(37)
+        }
+    
+        return line
+    }
+}

+ 32 - 3
Lanu/Views/Profile/Mine/LNMineViewController.swift

@@ -21,6 +21,7 @@ extension UIView {
 class LNMineViewController: UIViewController {
     private let stackView = UIStackView()
     private let qrCodeView = LNMineQRCodeShareView()
+    private let orderRecordView = LNMineOrderRecordView()
     
     override func viewDidLoad() {
         super.viewDidLoad()
@@ -35,6 +36,12 @@ class LNMineViewController: UIViewController {
         super.viewWillAppear(animated)
         
         LNRelationManager.shared.reloadRelationInfoIfNeed()
+        LNProfileManager.shared.reloadMyProfile()
+        LNPurchaseManager.shared.reloadWalletInfo()
+        
+        if myUserInfo.playmate {
+            LNGameMateManager.shared.getGameMateManagerInfo()
+        }
     }
 }
 
@@ -43,6 +50,7 @@ extension LNMineViewController: LNProfileManagerNotify {
         guard userInfo.userNo.isMyUid else { return }
         
         qrCodeView.isHidden = !userInfo.playmate
+        orderRecordView.isHidden = !userInfo.playmate
     }
 }
 
@@ -57,17 +65,38 @@ extension LNMineViewController {
             make.top.leading.trailing.equalToSuperview()
         }
         
+        let fakeNavBar = LNFakeNaviBar()
+        fakeNavBar.isUserInteractionEnabled = false // 因为是空白
+        view.addSubview(fakeNavBar)
+        fakeNavBar.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalToSuperview()
+        }
+        
+        let scrollView = UIScrollView()
+        scrollView.showsVerticalScrollIndicator = false
+        scrollView.showsHorizontalScrollIndicator = false
+        scrollView.contentInset = .init(top: 0, left: 0, bottom: -view.commonBottomInset + 70, right: 0)
+        scrollView.clipsToBounds = false
+        view.addSubview(scrollView)
+        scrollView.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(16)
+            make.bottom.equalToSuperview()
+            make.top.equalTo(fakeNavBar.snp.bottom)
+        }
+        
         stackView.axis = .vertical
         stackView.spacing = 16
-        view.addSubview(stackView)
+        scrollView.addSubview(stackView)
         stackView.snp.makeConstraints { make in
-            make.horizontalEdges.equalToSuperview().inset(16)
-            make.top.equalToSuperview().offset(44 + UIView.statusBarHeight)
+            make.edges.equalToSuperview()
+            make.width.equalToSuperview()
         }
         
         stackView.addArrangedSubview(LNMineUserInfoView())
         stackView.addArrangedSubview(qrCodeView)
         stackView.addArrangedSubview(LNMineWalletInfoView())
+        stackView.addArrangedSubview(orderRecordView)
         stackView.addArrangedSubview(LNMineFunctionView())
     }
 }

+ 3 - 3
Lanu/Views/Profile/Post/LNPostShareImageGenerator.swift

@@ -30,11 +30,11 @@ class LNPostShareImageGenerator {
         setupViews()
     }
     
-    func setInfo(info: LNGameMateInfoResponse) {
+    func setInfo() {
         nameLabel.text = myUserInfo.nickname
         genderView.update(myUserInfo.gender, myUserInfo.age)
-        starLabel.text = "\(myGameMateInfo?.star ?? 0)"
-        bioLabel.text = info.intro
+        starLabel.text = "\(myUserInfo.star)"
+        bioLabel.text = myUserInfo.intro
     }
     
     func setAlbum(image: UIImage) {

+ 8 - 11
Lanu/Views/Profile/Post/LNPostShareViewController.swift

@@ -33,14 +33,14 @@ class LNPostShareViewController: LNViewController {
         prefetchSkillIcon()
         loadLastOrder()
         
-        selectedSkills = Array(myGameMateInfo?.skills.prefix(3) ?? [])
+        selectedSkills = Array(myUserInfo.skills.prefix(3))
         updateSkillsView()
     }
 }
 
 extension LNPostShareViewController {
     private func prefetchSkillIcon() {
-        myGameMateInfo?.skills.forEach {
+        myUserInfo.skills.forEach {
             SDWebImageManager.shared.loadImage(with: URL(string: $0.icon),
                                                progress: nil)
             { _, _, _, _, _, _ in }
@@ -72,10 +72,9 @@ extension LNPostShareViewController {
     }
     
     private func showSkillSelectPanel() {
-        guard let skills = myGameMateInfo?.skills,
-              !skills.isEmpty else { return }
+        guard !myUserInfo.skills.isEmpty else { return }
         let panel = LNPostSkillSelectPanel()
-        panel.setSkills(skills: skills, selecteds: selectedSkills)
+        panel.setSkills(skills: myUserInfo.skills, selecteds: selectedSkills)
         panel.handler = { [weak self] selections in
             guard let self else { return }
             selectedSkills = selections
@@ -145,11 +144,9 @@ extension LNPostShareViewController {
         share.clipsToBounds = true
         share.addAction(UIAction(handler: { [weak self] _ in
             guard let self else { return }
-            guard let info = myGameMateInfo else { return }
             guard let album = postView.image else { return }
             
             let generator = LNPostShareImageGenerator()
-            generator.setInfo(info: info)
             generator.setOrderInfo(desc: lastDesc)
             generator.setAlbum(image: album)
             generator.setSkills(skills: selectedSkills)
@@ -199,7 +196,7 @@ extension LNPostShareViewController {
         bio.font = .body_m
         bio.textColor = .text_4
         bio.numberOfLines = 0
-        bio.text = myGameMateInfo?.intro ?? ""
+        bio.text = myUserInfo.intro
         container.addSubview(bio)
         bio.snp.makeConstraints { make in
             make.horizontalEdges.equalToSuperview().inset(22)
@@ -251,7 +248,7 @@ extension LNPostShareViewController {
         }
         
         let starLabel = UILabel()
-        starLabel.text = "\(myGameMateInfo?.star ?? 0)"
+        starLabel.text = "\(myUserInfo.star)"
         starLabel.font = .heading_h2
         starLabel.textColor = .text_5
         container.addSubview(starLabel)
@@ -354,7 +351,7 @@ extension LNPostShareViewController {
         }
         
         var albumItemViews: [LNSharePostAlbumItemView] = []
-        myGameMateInfo?.photos.forEach { url in
+        myUserInfo.photos.forEach { url in
             let album = LNSharePostAlbumItemView()
             album.imageView.sd_setImage(with: URL(string: url))
             album.onTap { [weak self, weak album] in
@@ -369,7 +366,7 @@ extension LNPostShareViewController {
             albumItemViews.append(album)
         }
         albumItemViews.first?.isSelected = true
-        postView.sd_setImage(with: URL(string: myGameMateInfo?.photos.first ?? ""))
+        postView.sd_setImage(with: URL(string: myUserInfo.photos.first ?? ""))
         
         return scrollView
     }

+ 1 - 1
Lanu/Views/Wallet/LNPurchasePanel.swift

@@ -138,7 +138,7 @@ extension LNPurchasePanel {
             make.height.equalTo(44)
         }
         
-        let ic = UIImageView(image: .icWallet)
+        let ic = UIImageView(image: .icWalletWithBg)
         container.addSubview(ic)
         ic.snp.makeConstraints { make in
             make.leading.equalToSuperview().offset(12)