瀏覽代碼

feat:补充 deeplink 功能

陈文艺 2 月之前
父節點
當前提交
4e5dc998d6
共有 34 個文件被更改,包括 592 次插入232 次删除
  1. 1 0
      Lanu.xcodeproj/project.pbxproj
  2. 0 7
      Lanu/AppDelegate.swift
  3. 7 0
      Lanu/Common/Config/String+Urls.swift
  4. 3 1
      Lanu/Common/Extension/UIImage+Extension.swift
  5. 15 9
      Lanu/Common/Extension/UIView+Extension.swift
  6. 74 19
      Lanu/Common/LNPhotosPicker.swift
  7. 3 0
      Lanu/Manager/Account/LNAccountManager.swift
  8. 72 12
      Lanu/Manager/Deeplink/LNDeeplinkManager.swift
  9. 27 0
      Lanu/Manager/Deeplink/LNDeeplinkParams.swift
  10. 111 69
      Lanu/Manager/Order/LNOrderManager.swift
  11. 19 0
      Lanu/Manager/Order/Network/LNHttpManager+Order.swift
  12. 18 1
      Lanu/Manager/Order/Network/LNOrderResponse.swift
  13. 7 0
      Lanu/SceneDelegate.swift
  14. 5 5
      Lanu/Views/Game/MateFilter/LNGameMateFilterPanel.swift
  15. 2 0
      Lanu/Views/Game/Skill/LNSkillBottomMenuView.swift
  16. 1 0
      Lanu/Views/Game/Skill/LNSkillDetailViewController.swift
  17. 2 2
      Lanu/Views/Login/Setup/LNBaseInfoSetupViewController.swift
  18. 0 1
      Lanu/Views/Main/LNMainViewController.swift
  19. 35 12
      Lanu/Views/Order/Create/LNCreateOrderPanel.swift
  20. 60 24
      Lanu/Views/Order/Create/LNCreateOrderViewController.swift
  21. 10 2
      Lanu/Views/Order/Detail/LNOrderDetailCardView.swift
  22. 46 6
      Lanu/Views/Order/Detail/LNOrderDetailViewController.swift
  23. 0 20
      Lanu/Views/Order/OrderList/LNOrderListItemCell.swift
  24. 8 0
      Lanu/Views/Order/OrderList/LNOrderListViewController.swift
  25. 3 1
      Lanu/Views/Order/OrderQR/LNOrderQRCodeShowView.swift
  26. 6 29
      Lanu/Views/Order/OrderRecords/LNOrderRecordCell.swift
  27. 8 0
      Lanu/Views/Order/OrderRecords/LNOrderRecordListViewController.swift
  28. 1 1
      Lanu/Views/Profile/Edit/LNEditProfilePhotoWallView.swift
  29. 4 4
      Lanu/Views/Profile/Edit/LNEditProfileViewController.swift
  30. 1 1
      Lanu/Views/Profile/Post/LNSharePostViewController.swift
  31. 2 1
      Lanu/Views/Profile/Profile/LNProfilePhotoWall.swift
  32. 7 0
      Lanu/Views/Search/LNUserSearchViewController.swift
  33. 8 0
      Lanu/Views/Wallet/LNWalletViewController.swift
  34. 26 5
      Lanu/Views/Web/LNWebViewController.swift

+ 1 - 0
Lanu.xcodeproj/project.pbxproj

@@ -109,6 +109,7 @@
 				Manager/Config/Network/LNConfigResponse.swift,
 				"Manager/Config/Network/LNHttpManager+Config.swift",
 				Manager/Deeplink/LNDeeplinkManager.swift,
+				Manager/Deeplink/LNDeeplinkParams.swift,
 				Manager/GameMate/LNGameMateManager.swift,
 				Manager/GameMate/Network/LNGameMateResponse.swift,
 				"Manager/GameMate/Network/LNHttpManager+GameMate.swift",

+ 0 - 7
Lanu/AppDelegate.swift

@@ -51,13 +51,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
         // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
         // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
     }
-    
-    func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
-        LNDeeplinkManager.shared.handleDeepLink(url)
-        return true
-    }
-    
-    
 }
 
 extension AppDelegate {

+ 7 - 0
Lanu/Common/Config/String+Urls.swift

@@ -29,4 +29,11 @@ extension String {
     static var walletHistoryUrl: String = {
         "\(webUrlHost)/wallet/record"
     }()
+    
+    static var orderQRShareUrl: String = {
+        "\(webUrlHost)/user/category"
+    }()
+    static var profileShareUrl: String = {
+        "\(webUrlHost)/user/profile"
+    }()
 }

+ 3 - 1
Lanu/Common/Extension/UIImage+Extension.swift

@@ -57,7 +57,9 @@ extension UIImage {
                         // 创建保存请求
                         PHAssetChangeRequest.creationRequestForAsset(from: self)
                     }) { success, error in
-                        completion?(success, error)
+                        DispatchQueue.main.async {
+                            completion?(success, error)
+                        }
                     }
                     
                 case .denied, .restricted: // 权限拒绝/受限

+ 15 - 9
Lanu/Common/Extension/UIView+Extension.swift

@@ -26,8 +26,9 @@ extension UIView {
             }
             responder = responder?.next
         }
-        
-        if let window {
+        if let window = self as? UIWindow {
+            return window.rootViewController
+        } else if let window {
             return window.rootViewController
         }
         return nil
@@ -38,13 +39,18 @@ extension UIView {
     }
     
     static var appKeyWindow: UIWindow? {
-        // 从当前活跃的场景中获取窗口
-        (UIApplication.shared.connectedScenes
-            .filter { $0.activationState == .foregroundActive }
-            .compactMap { $0 as? UIWindowScene }
-            .first?.windows
-            .filter { $0.isKeyWindow }
-            .first)
+        let scenes = UIApplication.shared.connectedScenes
+        if scenes.isEmpty { return nil }
+        
+        var activeScene = scenes.filter { $0.activationState == .foregroundActive }
+        if activeScene.isEmpty {
+            activeScene.insert(scenes.first!)
+        }
+        
+        let windowScenes = scenes.compactMap { $0 as? UIWindowScene }
+        if windowScenes.isEmpty { return nil }
+        
+        return windowScenes.first?.windows.filter { $0.isKeyWindow }.first
     }
     
     static var statusBarHeight: CGFloat = {

+ 74 - 19
Lanu/Common/LNPhotosPicker.swift

@@ -24,7 +24,9 @@ enum LNImagePickerType: CaseIterable {
 
 
 extension LNBottomSheetMenu {
-    static func showImageSelectMenu(view: UIView? = nil, handler: @escaping LNImagePickerHandler) {
+    static func showImageSelectMenu(view: UIView? = nil,
+                                    source: LNImagePickerSource = .none,
+                                    handler: @escaping LNImagePickerHandler) {
         let panel = LNBottomSheetMenu()
         panel.update([
             LNImagePickerType.camera.title,
@@ -32,9 +34,9 @@ extension LNBottomSheetMenu {
             .init(key: "取消")
         ]) { index, _ in
             if index == 0 {
-                LNImagePicker.shared.takePictures(from: view, handler: handler)
+                LNImagePicker.shared.takePictures(from: view, source: source, handler: handler)
             } else if index == 1 {
-                LNImagePicker.shared.selectPhoto(from: view, handler: handler)
+                LNImagePicker.shared.selectPhoto(from: view, source: source, handler: handler)
             }
         }
         panel.showIn()
@@ -45,6 +47,21 @@ extension LNBottomSheetMenu {
 typealias LNImagePickerHandler = (UIImage?) -> Void
 private class LNImagePickerController: UIImagePickerController {
     var handler: LNImagePickerHandler?
+    var source: LNImagePickerSource = .none
+}
+
+enum LNImagePickerSource {
+    case none
+    case avatar
+    case photoWall
+    
+    var maxSize: CGSize {
+        switch self {
+        case .none: .zero
+        case .avatar: .init(width: 300, height: 300)
+        case .photoWall: .init(width: 750, height: 1334)
+        }
+    }
 }
 
 
@@ -57,7 +74,9 @@ class LNImagePicker: NSObject {
 }
 
 extension LNImagePicker {
-    func selectPhoto(from view: UIView? = nil, handler: @escaping LNImagePickerHandler) {
+    func selectPhoto(from view: UIView? = nil,
+                     source: LNImagePickerSource,
+                     handler: @escaping LNImagePickerHandler) {
         // 2. 申请相册权限(iOS 10+ 需授权)
         PHPhotoLibrary.requestAuthorization { [weak self, weak view] status in
             guard let self, let view else { return }
@@ -66,7 +85,7 @@ extension LNImagePicker {
                 
                 switch status {
                 case .authorized: // 已授权,打开相册
-                    self.openPhotoLibrary(view, handler: handler)
+                    self.openPhotoLibrary(view, source: source, handler: handler)
                 case .denied, .restricted: // 拒绝授权或受限制
 //                    self.showAlert(message: "请在「设置-隐私-照片」中允许访问相册")
                     handler(nil)
@@ -82,12 +101,14 @@ extension LNImagePicker {
         }
     }
     
-    func takePictures(from view: UIView? = nil, handler: @escaping LNImagePickerHandler) {
+    func takePictures(from view: UIView? = nil,
+                      source: LNImagePickerSource,
+                      handler: @escaping LNImagePickerHandler) {
         let status = AVCaptureDevice.authorizationStatus(for: .video)
         
         switch status {
         case .authorized: // 已授权
-            self.openCamera(view, handler: handler)
+            self.openCamera(view, source: source, handler: handler)
         case .notDetermined: // 未决定,请求权限
             AVCaptureDevice.requestAccess(for: .video) { [weak self, weak view] granted in
                 guard let self, let view else { return }
@@ -98,7 +119,7 @@ extension LNImagePicker {
                         return
                     }
                     
-                    self.openCamera(view, handler: handler)
+                    self.openCamera(view, source: source, handler: handler)
                 }
             }
         case .denied, .restricted: // 拒绝/受限
@@ -111,7 +132,8 @@ extension LNImagePicker {
 
 extension LNImagePicker {
     private func buildSelectPhotoPicker(
-        handler: @escaping LNImagePickerHandler
+        handler: @escaping LNImagePickerHandler,
+        source: LNImagePickerSource
     ) -> UIImagePickerController {
         let vc = LNImagePickerController()
         vc.delegate = self
@@ -119,31 +141,38 @@ extension LNImagePicker {
         vc.mediaTypes = [UTType.image.identifier]
         vc.allowsEditing = false
         vc.handler = handler
+        vc.source = source
         
         return vc
     }
     
     private func buildTakePicturesPicker(
-        handler: @escaping LNImagePickerHandler
+        handler: @escaping LNImagePickerHandler,
+        source: LNImagePickerSource
     ) -> UIImagePickerController {
         let vc = LNImagePickerController()
         vc.delegate = self
         vc.sourceType = .camera
         vc.allowsEditing = false
         vc.handler = handler
+        vc.source = source
         
         return vc
     }
     
-    private func openPhotoLibrary(_ view: UIView?, handler: @escaping LNImagePickerHandler) {
+    private func openPhotoLibrary(_ view: UIView?,
+                                  source: LNImagePickerSource,
+                                  handler: @escaping LNImagePickerHandler) {
         let vc = view?.viewController ?? UIView.appKeyWindow?.rootViewController
-        let picker = buildSelectPhotoPicker(handler: handler)
+        let picker = buildSelectPhotoPicker(handler: handler, source: source)
         vc?.present(picker, animated: true)
     }
     
-    private func openCamera(_ view: UIView?, handler: @escaping LNImagePickerHandler) {
+    private func openCamera(_ view: UIView?,
+                            source: LNImagePickerSource,
+                            handler: @escaping LNImagePickerHandler) {
         let vc = view?.viewController ?? UIView.appKeyWindow?.rootViewController
-        let picker = buildTakePicturesPicker(handler: handler)
+        let picker = buildTakePicturesPicker(handler: handler, source: source)
         vc?.present(picker, animated: true)
     }
 }
@@ -168,11 +197,37 @@ extension LNImagePicker: UIImagePickerControllerDelegate, UINavigationController
             return
         }
         
-        UIGraphicsBeginImageContext(CGSizeMake(image.size.width, image.size.height));
-        image.draw(in: .init(origin: .zero, size: image.size))
-        let convertToUpImage = UIGraphicsGetImageFromCurrentImageContext();
-        UIGraphicsEndImageContext();
-        vc.handler?(convertToUpImage)
+        // 压缩图片
+        let maxSize: CGSize = vc.source.maxSize
+        if maxSize == .zero {
+            UIGraphicsBeginImageContext(CGSizeMake(image.size.width, image.size.height));
+            image.draw(in: .init(origin: .zero, size: image.size))
+            let convertToUpImage = UIGraphicsGetImageFromCurrentImageContext();
+            UIGraphicsEndImageContext();
+            vc.handler?(convertToUpImage)
+            return
+        }
+        let originalSize = image.size
+        
+        if originalSize.width <= maxSize.width
+            && originalSize.height <= maxSize.height {
+            vc.handler?(image)
+            return
+        }
+        
+        let widthRatio = maxSize.width / originalSize.width
+        let heightRatio = maxSize.height / originalSize.height
+        let scaleRatio = min(widthRatio, heightRatio)
+        
+        let newSize = CGSize(width: originalSize.width * scaleRatio,
+                             height: originalSize.height * scaleRatio)
+        
+        let renderer = UIGraphicsImageRenderer(size: newSize)
+        let resizedImage = renderer.image { _ in
+            image.draw(in: CGRect(origin: .zero, size: newSize))
+        }
+        
+        vc.handler?(resizedImage)
     }
     
     // 取消选择

+ 3 - 0
Lanu/Manager/Account/LNAccountManager.swift

@@ -58,6 +58,7 @@ class LNAccountManager {
                 if case .serverError = err {
                     self.clean()
                 }
+                showToast(err?.errorDescription)
                 completion?(false)
                 return
             }
@@ -72,6 +73,7 @@ class LNAccountManager {
         LNHttpManager.shared.loginByGoogle(data: data) { [weak self] response, err in
             guard let self else { return }
             guard err == nil, let response else {
+                showToast(err?.errorDescription)
                 completion?(false)
                 self.clean()
                 return
@@ -99,6 +101,7 @@ class LNAccountManager {
         LNHttpManager.shared.loginByEmail(email: email) { [weak self] response, err in
             guard let self else { return }
             guard err == nil, let response else {
+                showToast(err?.errorDescription)
                 completion(false)
                 self.clean()
                 return

+ 72 - 12
Lanu/Manager/Deeplink/LNDeeplinkManager.swift

@@ -7,7 +7,6 @@
 
 import Foundation
 
-
 struct LNDeeplinkUrls {
     static let appScheme = "gami"
     
@@ -16,6 +15,14 @@ struct LNDeeplinkUrls {
         case userCancellation
         case page
     }
+    
+    enum app: String {
+        case profile
+        
+        enum qrcode: String {
+            case order
+        }
+    }
 }
 
 extension RawRepresentable where RawValue == String {
@@ -40,33 +47,86 @@ extension RawRepresentable where RawValue == String {
 
 class LNDeeplinkManager {
     static let shared = LNDeeplinkManager()
-    typealias LNDeeplinkHandler = ([String: Any]?) -> Void
+    typealias LNDeeplinkHandler<T: Decodable> = (T?) -> Void
     
     private let lock = NSLock()
-    private var routerMap: [String: LNDeeplinkHandler] = [:]
+    private var routerMap: [String: (Data?) -> Void] = [:]
     
     private init() {
         DispatchQueue.global().async { [weak self] in
             guard let self else { return }
-            lock.lock()
             setupWebDeeplink()
-            lock.unlock()
+            setupProfileDepplink()
+            setupOrderDeeplink()
         }
     }
     
     func handleDeepLink(_ url: URL) {
-//        lock.lock()
-//        let handler = routerMap[url]
-//        lock.unlock()
-//        
-//        handler?(params)
+        DispatchQueue.global().async { [weak self] in
+            guard let self else { return }
+            
+            guard url.scheme?.lowercased() == LNDeeplinkUrls.appScheme else {
+                return
+            }
+            let fullPath = if let host = url.host {
+                "\(host)\(url.path)"
+            } else {
+                url.path
+            }
+            
+            let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
+            let params: [String: Any] = components?.queryItems?.reduce(into: [String: Any]())
+            { partialResult, item in
+                partialResult[item.name] = item.value
+            } ?? [:]
+            
+            let jsonData = try? JSONSerialization.data(withJSONObject: params)
+            
+            lock.lock()
+            let handler = routerMap[fullPath]
+            lock.unlock()
+            
+            handler?(jsonData)
+        }
+    }
+    
+    func register<T: Decodable>(url: String, handler: @escaping LNDeeplinkHandler<T>) {
+        lock.lock()
+        routerMap[url] = { data in
+            let model: T?
+            if let data {
+                let decoder = JSONDecoder()
+                model = try? decoder.decode(T.self, from: data)
+            } else {
+                model = nil
+            }
+            DispatchQueue.main.async {
+                handler(model)
+            }
+        }
+        lock.unlock()
     }
 }
 
 extension LNDeeplinkManager {
     private func setupWebDeeplink() {
-        routerMap[LNDeeplinkUrls.web.page.deeplinkPath] = { params in
-            
+        register(url: LNDeeplinkUrls.web.page.deeplinkPath) { (param: LNWebPageDeeplinkParams?) in
+            guard let param, !param.url.isEmpty else { return }
+            UIView.appKeyWindow?.pushToWebView(param)
+        }
+    }
+    
+    private func setupProfileDepplink() {
+        register(url: LNDeeplinkUrls.app.profile.deeplinkPath) { (param: LNProfileDeeplinkParams?) in
+            guard let param, !param.uid.isEmpty else { return }
+            UIView.appKeyWindow?.pushToProfile(uid: param.uid)
+        }
+    }
+    
+    private func setupOrderDeeplink() {
+        register(url: LNDeeplinkUrls.app.qrcode.order.deeplinkPath) { (param: LNORCodeCreateOrderParams?) in
+            guard let param, !param.qrcode.isEmpty else { return }
+            UIView.appKeyWindow?.pushToCreateOrder(param.qrcode)
         }
     }
 }

+ 27 - 0
Lanu/Manager/Deeplink/LNDeeplinkParams.swift

@@ -0,0 +1,27 @@
+//
+//  LNDeeplinkParams.swift
+//  Lanu
+//
+//  Created by OneeChan on 2026/1/6.
+//
+
+import Foundation
+import AutoCodable
+
+
+@AutoCodable
+class LNWebPageDeeplinkParams: Decodable {
+    var url: String = ""
+    var safeArea: Bool = false
+    var header: Bool = false
+}
+
+@AutoCodable
+class LNProfileDeeplinkParams: Decodable {
+    var uid: String = ""
+}
+
+@AutoCodable
+class LNORCodeCreateOrderParams: Decodable {
+    var qrcode: String = ""
+}

+ 111 - 69
Lanu/Manager/Order/LNOrderManager.swift

@@ -108,14 +108,17 @@ extension LNOrderManager {
                      queue: DispatchQueue = .main,
                      handler: @escaping (Bool) -> Void) {
         LNHttpManager.shared.cancelOrder(orderId: orderId) { [weak self] err in
-            queue.asyncIfNotGlobal {
-                handler(err == nil)
-            }
             guard let self else { return }
-            if err == nil {
-                notifyOrderInfoChanged(orderId: orderId)
-            } else {
-                showToast(err?.errorDescription)
+            
+            queue.asyncIfNotGlobal { [weak self] in
+                handler(err == nil)
+                
+                guard let self else { return }
+                if err == nil {
+                    notifyOrderInfoChanged(orderId: orderId)
+                } else {
+                    showToast(err?.errorDescription)
+                }
             }
         }
     }
@@ -123,14 +126,16 @@ extension LNOrderManager {
     func finishOrder(orderId: String, queue: DispatchQueue = .main,
                      handler: @escaping (Bool) -> Void) {
         LNHttpManager.shared.finishOrder(orderId: orderId) { [weak self] err in
-            queue.asyncIfNotGlobal {
-                handler(err == nil)
-            }
             guard let self else { return }
-            if err == nil {
-                notifyOrderInfoChanged(orderId: orderId)
-            } else {
-                showToast(err?.errorDescription)
+            queue.asyncIfNotGlobal { [weak self] in
+                handler(err == nil)
+                
+                guard let self else { return }
+                if err == nil {
+                    notifyOrderInfoChanged(orderId: orderId)
+                } else {
+                    showToast(err?.errorDescription)
+                }
             }
         }
     }
@@ -140,14 +145,16 @@ extension LNOrderManager {
                      handler: @escaping (Bool) -> Void) {
         LNHttpManager.shared.refundOrder(orderId: orderId, reason: reason, attachments: attachments)
         { [weak self] err in
-            queue.asyncIfNotGlobal {
-                handler(err == nil)
-            }
             guard let self else { return }
-            if err == nil {
-                notifyOrderInfoChanged(orderId: orderId)
-            } else {
-                showToast(err?.errorDescription)
+            queue.asyncIfNotGlobal { [weak self] in
+                handler(err == nil)
+                
+                guard let self else { return }
+                if err == nil {
+                    notifyOrderInfoChanged(orderId: orderId)
+                } else {
+                    showToast(err?.errorDescription)
+                }
             }
         }
     }
@@ -158,14 +165,15 @@ extension LNOrderManager {
         LNHttpManager.shared.commentOrder(
             orderId: orderId, star: star, comment: comment)
         { [weak self] err in
-            queue.asyncIfNotGlobal {
-                handler(err == nil)
-            }
             guard let self else { return }
-            if err == nil {
-                notifyOrderInfoChanged(orderId: orderId)
-            } else {
-                showToast(err?.errorDescription)
+            queue.asyncIfNotGlobal { [weak self] in
+                handler(err == nil)
+                guard let self else { return }
+                if err == nil {
+                    notifyOrderInfoChanged(orderId: orderId)
+                } else {
+                    showToast(err?.errorDescription)
+                }
             }
         }
     }
@@ -173,14 +181,16 @@ extension LNOrderManager {
     func rejectOrder(orderId: String, queue: DispatchQueue = .main,
                      handler: @escaping (Bool) -> Void) {
         LNHttpManager.shared.rejectOrder(orderId: orderId) { [weak self] err in
-            queue.asyncIfNotGlobal {
-                handler(err == nil)
-            }
             guard let self else { return }
-            if err == nil {
-                notifyOrderInfoChanged(orderId: orderId)
-            } else {
-                showToast(err?.errorDescription)
+            queue.asyncIfNotGlobal { [weak self] in
+                handler(err == nil)
+                
+                guard let self else { return }
+                if err == nil {
+                    notifyOrderInfoChanged(orderId: orderId)
+                } else {
+                    showToast(err?.errorDescription)
+                }
             }
         }
     }
@@ -188,14 +198,15 @@ extension LNOrderManager {
     func acceptOrder(orderId: String, queue: DispatchQueue = .main,
                      handler: @escaping (Bool) -> Void) {
         LNHttpManager.shared.acceptOrder(orderId: orderId) { [weak self] err in
-            queue.asyncIfNotGlobal {
-                handler(err == nil)
-            }
             guard let self else { return }
-            if err == nil {
-                notifyOrderInfoChanged(orderId: orderId)
-            } else {
-                showToast(err?.errorDescription)
+            queue.asyncIfNotGlobal { [weak self] in
+                handler(err == nil)
+                guard let self else { return }
+                if err == nil {
+                    notifyOrderInfoChanged(orderId: orderId)
+                } else {
+                    showToast(err?.errorDescription)
+                }
             }
         }
     }
@@ -203,14 +214,15 @@ extension LNOrderManager {
     func startOrderService(orderId: String, queue: DispatchQueue = .main,
                            handler: @escaping (Bool) -> Void) {
         LNHttpManager.shared.startOrderService(orderId: orderId) { [weak self] err in
-            queue.asyncIfNotGlobal {
-                handler(err == nil)
-            }
             guard let self else { return }
-            if err == nil {
-                notifyOrderInfoChanged(orderId: orderId)
-            } else {
-                showToast(err?.errorDescription)
+            queue.asyncIfNotGlobal { [weak self] in
+                handler(err == nil)
+                guard let self else { return }
+                if err == nil {
+                    notifyOrderInfoChanged(orderId: orderId)
+                } else {
+                    showToast(err?.errorDescription)
+                }
             }
         }
     }
@@ -218,14 +230,15 @@ extension LNOrderManager {
     func finishOrderService(orderId: String, queue: DispatchQueue = .main,
                             handler: @escaping (Bool) -> Void) {
         LNHttpManager.shared.finishOrderService(orderId: orderId) { [weak self] err in
-            queue.asyncIfNotGlobal {
-                handler(err == nil)
-            }
             guard let self else { return }
-            if err == nil {
-                notifyOrderInfoChanged(orderId: orderId)
-            } else {
-                showToast(err?.errorDescription)
+            queue.asyncIfNotGlobal { [weak self] in
+                handler(err == nil)
+                guard let self else { return }
+                if err == nil {
+                    notifyOrderInfoChanged(orderId: orderId)
+                } else {
+                    showToast(err?.errorDescription)
+                }
             }
         }
     }
@@ -236,14 +249,16 @@ extension LNOrderManager {
     {
         LNHttpManager.shared.protestOrder(orderId: orderId, reason: reason, attachments: attachments)
         { [weak self] err in
-            queue.asyncIfNotGlobal {
-                handler(err == nil)
-            }
             guard let self else { return }
-            if err == nil {
-                notifyOrderInfoChanged(orderId: orderId)
-            } else {
-                showToast(err?.errorDescription)
+            queue.asyncIfNotGlobal { [weak self] in
+                handler(err == nil)
+                
+                guard let self else { return }
+                if err == nil {
+                    notifyOrderInfoChanged(orderId: orderId)
+                } else {
+                    showToast(err?.errorDescription)
+                }
             }
         }
     }
@@ -255,14 +270,41 @@ extension LNOrderManager {
     func createOrderQR(skillId: String,
                        count: Int, type: LNOrderSource,
                        queue: DispatchQueue = .main,
-                       completion: @escaping (String?) -> Void) {
+                       handler: @escaping (String?) -> Void) {
         LNHttpManager.shared.createOrderQR(skillId: skillId, count: count, type: type) { data, err in
             queue.asyncIfNotGlobal {
-                guard let data, err == nil else {
-                    completion(nil)
-                    return
-                }
-                completion(data.qrCode)
+                handler(data?.qrCode)
+            }
+            if let err {
+                showToast(err.errorDescription)
+            }
+        }
+    }
+    
+    func getQRDetail(data: String,
+                     queue: DispatchQueue = .main,
+                     handler: @escaping (LNQRCodeDetailResponse?) -> Void) {
+        LNHttpManager.shared.getQRDetail(data: data) { res, err in
+            queue.asyncIfNotGlobal {
+                handler(res)
+            }
+            
+            if let err {
+                showToast(err.errorDescription)
+            }
+        }
+    }
+    
+    func createQRCodeOrder(data: String, count: Int, extra: String,
+                           queue: DispatchQueue = .main,
+                           handler: @escaping (String?) -> Void) {
+        LNHttpManager.shared.createQROrder(data: data, count: count, extra: extra) { res, err in
+            queue.asyncIfNotGlobal {
+                handler(res?.orderNo)
+            }
+            
+            if let err {
+                showToast(err.errorDescription)
             }
         }
     }

+ 19 - 0
Lanu/Manager/Order/Network/LNHttpManager+Order.swift

@@ -21,6 +21,8 @@ private let kNetPath_Order_Cancel = "/skill/order/canecl"
 private let kNetPath_Order_Handler = "/playmate/order/handler"
 
 private let kNetPath_Order_QR_Create = "/skill/create/order/qrcode"
+private let kNetPath_Order_QR_Detail = "/skill/view/order/qrcode"
+private let kNetPath_Order_QR_Order = "/skill/order/qr/payment"
 
 private let kNetPath_Order_Records = "/playmate/order/list"
 private let kNetPath_Order_Protest = "/playmate/submit/refundVoucher"
@@ -96,6 +98,23 @@ extension LNHttpManager {
         ], completion: completion)
     }
     
+    func getQRDetail(data: String, completion: @escaping (LNQRCodeDetailResponse?, LNHttpError?) -> Void) {
+        post(path: kNetPath_Order_QR_Detail, params: [
+            "id": data
+        ], completion: completion)
+    }
+    
+    func createQROrder(data: String, count: Int, extra: String,
+                       completion: @escaping (LNCreateOrderResponse?, LNHttpError?) -> Void) {
+        post(path: kNetPath_Order_QR_Order, params: [
+            "qrCode": data,
+            "purchaseQty": count,
+            "remark": extra
+        ], completion: completion)
+    }
+}
+
+extension LNHttpManager {
     func cancelOrder(orderId: String, completion: @escaping (LNHttpError?) -> Void) {
         post(path: kNetPath_Order_Cancel, params: [
             "id": orderId

+ 18 - 1
Lanu/Manager/Order/Network/LNOrderResponse.swift

@@ -21,7 +21,7 @@ enum LNOrderStatus: Int, Decodable {
     case cancelled = 8
 }
 
-enum LNOrderSource: Int {
+enum LNOrderSource: Int, Decodable {
     case normal = 1
     case custom = 2
 }
@@ -144,3 +144,20 @@ class LNUnfinishedOrderListResponse: Decodable {
     var list: [LNUnfinishedOrderVO] = []
     var next: String = ""
 }
+
+@AutoCodable
+class LNQRCodeDetailResponse: Decodable {
+    var qrCode: String = ""
+    var avatar: String = ""
+    var nickname: String = ""
+    var bizCategoryName: String = ""
+    var bizCategoryIcon: String = ""
+    var price: Double = 0
+    var unit: String = ""
+    var purchaseQty: Int = 0
+    var goldCoinAmount: Double = 0
+    var type: LNOrderSource = .normal
+    var star: Double = 0
+    var currencyAmount: Double = 0
+    var sellerUserNo: String = ""
+}

+ 7 - 0
Lanu/SceneDelegate.swift

@@ -62,6 +62,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
         // Use this method to save data, release shared resources, and store enough scene-specific state information
         // to restore the scene back to its current state.
     }
+    
+    func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
+        if let urlContext = URLContexts.first {
+            let url = urlContext.url
+            LNDeeplinkManager.shared.handleDeepLink(url)
+        }
+    }
 }
 
 extension SceneDelegate: LNNetworkMonitorNotify {

+ 5 - 5
Lanu/Views/Game/MateFilter/LNGameMateFilterPanel.swift

@@ -149,7 +149,7 @@ extension LNGameMateFilterPanel {
         titleLabel.textColor = .text_5
         container.addArrangedSubview(titleLabel)
         
-        var itemViews: [LNGameMateFiliterItemView] = []
+        var itemViews: [LNGameMateFilterItemView] = []
         let stackView = LNMultiLineStackView()
         stackView.itemSpacing = 8
         stackView.itemDistribution = .fillEqually
@@ -157,10 +157,10 @@ extension LNGameMateFilterPanel {
         stackView.spacing = 6
         container.addArrangedSubview(stackView)
         for (index, type) in filters.enumerated() {
-            let itemView = LNGameMateFiliterItemView()
+            let itemView = LNGameMateFilterItemView()
             itemView.titleLabel.text = type
-            itemView.onTap { [weak self] in
-                guard self != nil else { return }
+            itemView.onTap { [weak self, weak itemView] in
+                guard self != nil, let itemView else { return }
                 itemViews.forEach {
                     $0.showHighLight($0 == itemView)
                 }
@@ -175,7 +175,7 @@ extension LNGameMateFilterPanel {
     }
 }
 
-private class LNGameMateFiliterItemView: UIView {
+private class LNGameMateFilterItemView: UIView {
     let titleLabel = UILabel()
     
     override init(frame: CGRect) {

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

@@ -40,6 +40,8 @@ class LNSkillBottomMenuView: UIView {
 
 extension LNSkillBottomMenuView {
     private func setupViews() {
+        isHidden = true
+        
         let bottomGradient = CAGradientLayer()
         bottomGradient.colors = [
             UIColor.white.withAlphaComponent(0).cgColor,

+ 1 - 0
Lanu/Views/Game/Skill/LNSkillDetailViewController.swift

@@ -9,6 +9,7 @@ import Foundation
 import UIKit
 import SnapKit
 import Combine
+import AutoCodable
 
 
 extension UIView {

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

@@ -88,9 +88,9 @@ extension LNBaseInfoSetupViewController {
         panel.update(titles) { [weak self] index, text in
             guard let self else { return }
             if index == 0 {
-                LNImagePicker.shared.takePictures(from: view, handler: handler)
+                LNImagePicker.shared.takePictures(from: view, source: .avatar, handler: handler)
             } else if index == 1 {
-                LNImagePicker.shared.selectPhoto(from: view, handler: handler)
+                LNImagePicker.shared.selectPhoto(from: view, source: .avatar, handler: handler)
             } else if index == 2 {
                 guard let item = randomProfile?.avatars.randomElement() else { return }
                 avatar.loadImage(url: item)

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

@@ -40,7 +40,6 @@ class LNMainViewController: UITabBarController {
         }
         
         selectedViewController = message // 触发消息界面加载逻辑
-        
         selectedViewController = home
         
         delegate = self

+ 35 - 12
Lanu/Views/Order/Create/LNCreateOrderPanel.swift

@@ -32,7 +32,7 @@ class LNCreateOrderPanel: LNPopupView {
     private let curCoinLabel = UILabel()
     
     private var skillId: String?
-    private var userId: String?
+    private var qrCode: String?
     private var price: Double = 0.0
     private var curCount = 1 {
         didSet {
@@ -73,7 +73,6 @@ class LNCreateOrderPanel: LNPopupView {
         
         skillId = detail.id
         price = detail.price
-        userId = detail.userNo
         curCount = count
         
         extraInput.text = extra
@@ -87,10 +86,22 @@ class LNCreateOrderPanel: LNPopupView {
         
         skillId = skill.id
         price = skill.price
-        userId = user.userNo
         curCount = 1
     }
     
+    func update(_ detail: LNQRCodeDetailResponse, count: Int, extra: String) {
+        gameNameLabel.text = detail.bizCategoryName
+        avatar.sd_setImage(with: URL(string: detail.avatar))
+        nameLabel.text = detail.nickname
+        priceLabel.text = "\(detail.price.toDisplay)/\(detail.unit)"
+        
+        qrCode = detail.qrCode
+        price = detail.price
+        curCount = count
+        
+        extraInput.text = extra
+    }
+    
     required init?(coder: NSCoder) {
         fatalError("init(coder:) has not been implemented")
     }
@@ -144,16 +155,28 @@ extension LNCreateOrderPanel {
         confirmButton.clipsToBounds = true
         confirmButton.addAction(UIAction(handler: { [weak self] _ in
             guard let self else { return }
-            guard let skillId else { return }
+            let extra = extraInput.text ?? ""
             
-            LNOrderManager.shared.createOrder(
-                skillId: skillId, count: curCount, remark: extraInput.text ?? "")
-            { [weak self] orderNo in
-                guard let self else { return }
-                guard let orderNo else { return }
-                
-                dismiss()
-                completionHandler?(orderNo)
+            if let skillId {
+                LNOrderManager.shared.createOrder(
+                    skillId: skillId, count: curCount, remark: extra)
+                { [weak self] orderNo in
+                    guard let self else { return }
+                    guard let orderNo else { return }
+                    
+                    dismiss()
+                    completionHandler?(orderNo)
+                }
+            } else if let qrCode {
+                LNOrderManager.shared.createQRCodeOrder(
+                    data: qrCode, count: curCount, extra: extra)
+                { [weak self] orderNo in
+                    guard let self else { return }
+                    guard let orderNo else { return }
+                    
+                    dismiss()
+                    completionHandler?(orderNo)
+                }
             }
         }), for: .touchUpInside)
         container.addSubview(confirmButton)

+ 60 - 24
Lanu/Views/Order/Create/LNCreateOrderViewController.swift

@@ -11,15 +11,15 @@ import SnapKit
 
 
 extension UIView {
-    func pushToCreateOrder(_ skillId: String) {
+    func pushToCreateOrder(_ skillDetail: LNGameMateSkillDetailVO) {
         let vc = LNCreateOrderViewController()
-        vc.loadSkill(skillId)
+        vc.loadSkill(skillDetail)
         navigationController?.pushViewController(vc, animated: true)
     }
     
-    func pushToCreateOrder(_ skillDetail: LNGameMateSkillDetailVO) {
+    func pushToCreateOrder(_ qrCode: String) {
         let vc = LNCreateOrderViewController()
-        vc.loadSkill(skillDetail)
+        vc.loadQRData(qrCode)
         navigationController?.pushViewController(vc, animated: true)
     }
 }
@@ -27,6 +27,7 @@ extension UIView {
 
 class LNCreateOrderViewController: LNViewController {
     private var skill: LNGameMateSkillDetailVO?
+    private var qrDetail: LNQRCodeDetailResponse?
     
     private let avatar = UIImageView()
     private let nameLabel = UILabel()
@@ -64,19 +65,19 @@ class LNCreateOrderViewController: LNViewController {
         setupViews()
     }
     
-    func loadSkill(_ skillId: String) {
-        LNGameMateManager.shared.getSkillDetail(skillId: skillId) { [weak self] detail in
-            guard let self else { return }
-            guard let detail else { return }
-            skill = detail
-            updateContent(detail)
-        }
-    }
-    
     func loadSkill(_ detail: LNGameMateSkillDetailVO) {
         skill = detail
         updateContent(detail)
     }
+    
+    func loadQRData(_ data: String) {
+        LNOrderManager.shared.getQRDetail(data: data) { [weak self] res in
+            guard let self else { return }
+            guard let res else { return }
+            qrDetail = res
+            updateContent(res)
+        }
+    }
 }
 
 extension LNCreateOrderViewController: UITextFieldDelegate {
@@ -95,7 +96,7 @@ extension LNCreateOrderViewController {
         avatar.sd_setImage(with: URL(string: detail.avatar))
         nameLabel.text = detail.nickname
         starLabel.text = "\(detail.star)"
-        orderCountLabel.text = "(\(detail.orderCount))"
+//        orderCountLabel.text = "(\(detail.orderCount))"
         
         let skill = LNGameMateManager.shared.gameCategory(for: detail.categoryCode)
         skillIc.sd_setImage(with: URL(string: skill?.icon ?? ""))
@@ -107,14 +108,41 @@ extension LNCreateOrderViewController {
         orderButton.isEnabled = true
     }
     
+    private func updateContent(_ detail: LNQRCodeDetailResponse) {
+        avatar.sd_setImage(with: URL(string: detail.avatar))
+        nameLabel.text = detail.nickname
+        starLabel.text = "\(detail.star)"
+//        orderCountLabel.text = "(\(detail.purchaseQty))"
+        
+        skillIc.sd_setImage(with: URL(string: detail.bizCategoryIcon))
+        skillNameLabel.text = detail.bizCategoryName
+        
+        skillPriceLabel.text = "\(detail.price.toDisplay)"
+        curCount = detail.purchaseQty
+        if detail.type != .normal {
+            minuButton.isEnabled = false
+            addButton.isEnabled = false
+        }
+        
+        orderButton.isEnabled = !detail.sellerUserNo.isMyUid
+    }
+    
     private func updateCost() {
-        guard let skill else { return }
-        let cost = skill.price * Double(curCount)
-        let text: String = .init(key: "%@/ %d %@", cost.toDisplay, curCount, skill.unit)
-        let attrStr = NSMutableAttributedString(string: text)
-        let range = (text as NSString).range(of: "\(cost.toDisplay)")
-        attrStr.addAttribute(.font, value: UIFont.heading_h2, range: range)
-        costLabel.attributedText = attrStr
+        if let skill {
+            let cost = skill.price * Double(curCount)
+            let text: String = .init(key: "%@/ %d %@", cost.toDisplay, curCount, skill.unit)
+            let attrStr = NSMutableAttributedString(string: text)
+            let range = (text as NSString).range(of: "\(cost.toDisplay)")
+            attrStr.addAttribute(.font, value: UIFont.heading_h2, range: range)
+            costLabel.attributedText = attrStr
+        } else if let qrDetail {
+            let cost = qrDetail.price * Double(curCount)
+            let text: String = .init(key: "%@/ %d %@", cost.toDisplay, curCount, qrDetail.unit)
+            let attrStr = NSMutableAttributedString(string: text)
+            let range = (text as NSString).range(of: "\(cost.toDisplay)")
+            attrStr.addAttribute(.font, value: UIFont.heading_h2, range: range)
+            costLabel.attributedText = attrStr
+        }
     }
     
     private func setupViews() {
@@ -183,6 +211,7 @@ extension LNCreateOrderViewController {
             make.leading.equalTo(star.snp.trailing).offset(4)
         }
         
+        orderCountLabel.isHidden = true
         orderCountLabel.font = .body_xs
         orderCountLabel.textColor = .text_5
         starView.addSubview(orderCountLabel)
@@ -438,13 +467,20 @@ extension LNCreateOrderViewController {
         orderButton.titleLabel?.font = .heading_h3
         orderButton.addAction(UIAction(handler: { [weak self] _ in
             guard let self else { return }
-            guard let skill else { return }
             let panel = LNCreateOrderPanel()
-            panel.update(skill, count: curCount, extra: extraInput.text ?? "")
+            if let skill {
+                panel.update(skill, count: curCount, extra: extraInput.text ?? "")
+            } else if let qrDetail {
+                panel.update(qrDetail, count: curCount, extra: extraInput.text ?? "")
+            } else { return }
             panel.completionHandler = { [weak self] orderId in
                 guard let self else { return }
                 
-                view.pushToChat(uid: skill.userNo)
+                if let skill {
+                    view.pushToChat(uid: skill.userNo)
+                } else if let qrDetail {
+                    view.pushToChat(uid: qrDetail.sellerUserNo)
+                }
                 navigationController?.viewControllers.removeAll { $0 is LNCreateOrderViewController }
             }
             panel.showIn()

+ 10 - 2
Lanu/Views/Order/Detail/LNOrderDetailCardView.swift

@@ -25,6 +25,8 @@ class LNOrderDetailCardView: UIView {
     private let gameLabel = UILabel()
     private let priceLabel = UILabel()
     private let countLabel = UILabel()
+    private var extraView: UIView?
+    private let extraLabel = UILabel()
     private let orderIdLabel = UILabel()
     private let timeLabel = UILabel()
     private let totalLabel = UILabel()
@@ -42,6 +44,8 @@ class LNOrderDetailCardView: UIView {
         gameLabel.text = item.orderInfo.bizCategoryName
         priceLabel.text = "\(item.orderInfo.price.toDisplay)"
         countLabel.text = "\(item.orderInfo.unit)x\(item.orderInfo.purchaseQty)"
+        extraLabel.text = item.orderInfo.customerRemark
+        extraView?.isHidden = item.orderInfo.customerRemark.isEmpty
         orderIdLabel.text = item.orderInfo.orderId
         timeLabel.text = (TimeInterval(item.orderInfo.createTime) / 1000.0).formattedFullDateWithTime()
         totalLabel.text = "\((item.orderInfo.price * Double(item.orderInfo.purchaseQty)).toDisplay)"
@@ -85,6 +89,12 @@ extension LNOrderDetailCardView {
         let countView = buildDetailInfo(title: .init(key: "数量"), contentView: countLabel)
         stackView.addArrangedSubview(countView)
         
+        extraLabel.font = curType == .normal ? .body_s : .body_xs
+        extraLabel.textColor = .text_5
+        let extraView = buildDetailInfo(title: .init(key: "备注"), contentView: extraLabel)
+        stackView.addArrangedSubview(extraView)
+        self.extraView = extraView
+        
         orderIdLabel.font = curType == .normal ? .body_s : .body_xs
         orderIdLabel.textColor = .text_5
         let orderView = buildDetailInfo(title: .init(key: "订单编号"), contentView: orderIdLabel)
@@ -95,9 +105,7 @@ extension LNOrderDetailCardView {
         let timeView = buildDetailInfo(title: .init(key: "购买时间"), contentView: timeLabel)
         
         stackView.addArrangedSubview(timeView)
-        
         stackView.addArrangedSubview(buildLine())
-        
         stackView.addArrangedSubview(buildTotal())
     }
     

+ 46 - 6
Lanu/Views/Order/Detail/LNOrderDetailViewController.swift

@@ -26,6 +26,8 @@ class LNOrderDetailViewController: LNViewController {
     private let starView = LNFiveStarScoreView()
     private let stateIc = UIImageView()
     private let stateLabel = UILabel()
+    
+    private var timer: Timer?
     private let countDownLabel = UILabel()
     
     private let detailView = LNOrderDetailCardView()
@@ -74,7 +76,47 @@ extension LNOrderDetailViewController {
             update(detail)
         }
     }
-    
+}
+
+extension LNOrderDetailViewController {
+    private func startCountDown() {
+        stopCountDown()
+            
+        updateRemain()
+        let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
+            guard let self else { return }
+            if updateRemain() {
+                stopCountDown()
+                reloadDetail()
+            }
+        }
+        RunLoop.current.add(timer, forMode: .common)
+        self.timer = timer
+    }
+        
+    private func stopCountDown() {
+        timer?.invalidate()
+        timer = nil
+    }
+        
+    @discardableResult
+    private func updateRemain() -> Bool {
+        guard let curDetail else { return true }
+        let remain = 3600 - (Int(curTime) - curDetail.orderInfo.createTime / 1_000)
+        let countDownText = String(format: "%02d:%02d", remain/60, remain%60)
+        
+        let text: String = .init(key: "Automatic cancellation after %@", countDownText)
+        let attr = NSMutableAttributedString(string: text)
+        let range = (text as NSString).range(of: countDownText)
+        attr.addAttributes([.font: UIFont.heading_h4,
+                            .foregroundColor: UIColor.primary_5], range: range)
+        countDownLabel.attributedText = attr
+        
+        return remain <= 0
+    }
+}
+
+extension LNOrderDetailViewController {
     private func update(_ item: LNOrderDetailResponse) {
         starView.isHidden = true
         countDownLabel.isHidden = true
@@ -82,6 +124,7 @@ extension LNOrderDetailViewController {
         commentButton.isHidden = true
         completeView.isHidden = true
         refundButton.isHidden = true
+        stopCountDown()
         
         detailView.update(item)
         refundView.update(item)
@@ -102,6 +145,7 @@ extension LNOrderDetailViewController {
             stateLabel.text = .init(key: "Pending Acceptance")
             countDownLabel.isHidden = false
             cancelButton.isHidden = false
+            startCountDown()
         case .completed:
             backgroundIc.image = .init(named: "ic_order_normal_bg")
             stateIc.image = .init(named: "ic_order_status_complete")
@@ -340,11 +384,7 @@ extension LNOrderDetailViewController {
         completeButton.clipsToBounds = true
         completeButton.addAction(UIAction(handler: { [weak self] _ in
             guard let self else { return }
-            LNOrderManager.shared.finishOrder(orderId: orderId) { [weak self] success in
-                guard let self else { return }
-                guard success else { return }
-                reloadDetail()
-            }
+            LNOrderManager.shared.finishOrder(orderId: orderId) { _ in }
         }), for: .touchUpInside)
         completeView.addSubview(completeButton)
         completeButton.snp.makeConstraints { make in

+ 0 - 20
Lanu/Views/Order/OrderList/LNOrderListItemCell.swift

@@ -31,8 +31,6 @@ class LNOrderListItemCell: UITableViewCell {
         super.init(style: style, reuseIdentifier: reuseIdentifier)
         
         setupViews()
-        
-        LNEventDeliver.addObserver(self)
     }
     
     func update(_ item: LNOrderListItemVO) {
@@ -56,13 +54,6 @@ class LNOrderListItemCell: UITableViewCell {
     }
 }
 
-extension LNOrderListItemCell: LNOrderManagerNotify {
-    func onOrderInfoChanged(orderId: String) {
-        guard orderId == curItem?.orderId else { return }
-        updateByStateChanged()
-    }
-}
-
 extension LNOrderListItemCell {
     @objc
     private func handleOperationButtonClick() {
@@ -73,10 +64,6 @@ extension LNOrderListItemCell {
                 guard let self else { return }
                 guard success else { return }
                 orderItem.status = .cancelled
-                
-                if orderItem.orderId == curItem?.orderId {
-                    updateByStateChanged()
-                }
             }
             break
         case .completed: // 可以评价
@@ -85,9 +72,6 @@ extension LNOrderListItemCell {
             panel.handler = { [weak self] star, comment in
                 guard let self else { return }
                 orderItem.star = star
-                if orderItem.orderId == curItem?.orderId {
-                    updateByStateChanged()
-                }
             }
             panel.showIn()
             break
@@ -96,10 +80,6 @@ extension LNOrderListItemCell {
                 guard let self else { return }
                 guard success else { return }
                 orderItem.status = .completed
-                
-                if orderItem.orderId == curItem?.orderId {
-                    updateByStateChanged()
-                }
             }
             break
         case .refunded, .accepted, .rejected, .cancelled, .servicing:

+ 8 - 0
Lanu/Views/Order/OrderList/LNOrderListViewController.swift

@@ -34,6 +34,8 @@ class LNOrderListViewController: LNViewController {
         setupViews()
         
         tableView.mj_header?.beginRefreshing()
+        
+        LNEventDeliver.addObserver(self)
     }
 }
 
@@ -95,6 +97,12 @@ extension LNOrderListViewController: UITableViewDelegate, UITableViewDataSource
     }
 }
 
+extension LNOrderListViewController: LNOrderManagerNotify {
+    func onOrderInfoChanged(orderId: String) {
+        tableView.reloadData()
+    }
+}
+
 extension LNOrderListViewController {
     private func setupViews() {
         title = .init(key: "我的订单")

+ 3 - 1
Lanu/Views/Order/OrderQR/LNOrderQRCodeShowView.swift

@@ -62,9 +62,11 @@ class LNOrderQRCodeShowView: UIView {
             count: count,
             type: type) { [weak self] qrCode in
                 guard let self else { return }
+                guard let qrCode else { return }
                 guard skill.id == self.curSkill?.id,
                       self.count == count else { return }
-                self.qrCodeIc.image = qrCode?.toQRCode(size: qrCodeSize)
+                let url: String = .orderQRShareUrl + "?id=\(skill.id)&code=\(qrCode)"
+                self.qrCodeIc.image = url.toQRCode(size: qrCodeSize)
                 self.qrCodeData = qrCode
                 self.saveButton.isEnabled = true
                 self.copyButton.isEnabled = true

+ 6 - 29
Lanu/Views/Order/OrderRecords/LNOrderRecordCell.swift

@@ -55,13 +55,10 @@ class LNOrderRecordCell: UITableViewCell {
         
         avatar.sd_setImage(with: URL(string: item.avatar))
         nameLabel.text = item.nickname
-        switch item.gender {
-        case .unknow:
-            genderView.image = .init(named: "ic_gender_male")
-        case .male:
-            genderView.image = .init(named: "ic_gender_female")
-        case .female:
-            genderView.image = nil
+        genderView.image = switch item.gender {
+        case .unknow: nil
+        case .male: .init(named: "ic_gender_male")
+        case .female: .init(named: "ic_gender_female")
         }
         requestLabel.text = item.customerRemark
         requestLine.isHidden = item.customerRemark.isEmpty
@@ -332,10 +329,6 @@ extension LNOrderRecordCell {
                 guard let self else { return }
                 guard success else { return }
                 curItem.status = .rejected
-                
-                if self.curItem?.orderId == curItem.orderId {
-                    updateByStateChanged()
-                }
             }
         }), for: .touchUpInside)
         rejectButton.snp.makeConstraints { make in
@@ -357,10 +350,6 @@ extension LNOrderRecordCell {
                 guard let self else { return }
                 guard success else { return }
                 curItem.status = .accepted
-                
-                if self.curItem?.orderId == curItem.orderId {
-                    updateByStateChanged()
-                }
             }
         }), for: .touchUpInside)
         acceptButton.snp.makeConstraints { make in
@@ -382,10 +371,6 @@ extension LNOrderRecordCell {
                 guard let self else { return }
                 guard success else { return }
                 curItem.status = .servicing
-                
-                if self.curItem?.orderId == curItem.orderId {
-                    updateByStateChanged()
-                }
             }
         }), for: .touchUpInside)
         startButton.snp.makeConstraints { make in
@@ -407,10 +392,6 @@ extension LNOrderRecordCell {
                 guard let self else { return }
                 guard success else { return }
                 curItem.status = .serviceDone
-                
-                if self.curItem?.orderId == curItem.orderId {
-                    updateByStateChanged()
-                }
             }
         }), for: .touchUpInside)
         completeButton.snp.makeConstraints { make in
@@ -431,10 +412,6 @@ extension LNOrderRecordCell {
             pushToProtest(curItem.orderId) { [weak self] in
                 guard let self else { return }
                 curItem.status = .serviceDone
-                
-                if self.curItem?.orderId == curItem.orderId {
-                    updateByStateChanged()
-                }
             }
         }), for: .touchUpInside)
         commitButton.snp.makeConstraints { make in
@@ -508,20 +485,20 @@ extension LNOrderRecordCell {
         container.addSubview(coin)
         coin.snp.makeConstraints { make in
             make.centerY.equalTo(priceLabel)
-            make.leading.greaterThanOrEqualToSuperview()
+            make.leading.equalToSuperview()
             make.trailing.equalTo(priceLabel.snp.leading)
             make.width.height.equalTo(16)
         }
         
         countLabel.font = .body_m
         countLabel.textColor = .text_5
+        countLabel.textAlignment = .right
         countLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
         countLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
         container.addSubview(countLabel)
         countLabel.snp.makeConstraints { make in
             make.trailing.bottom.equalToSuperview()
             make.top.equalTo(priceLabel.snp.bottom)
-            make.leading.equalToSuperview()
         }
         
         return container

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

@@ -31,6 +31,8 @@ class LNOrderRecordListViewController: LNViewController {
         super.viewDidLoad()
         
         setupViews()
+        
+        LNEventDeliver.addObserver(self)
     }
     
     override func viewDidAppear(_ animated: Bool) {
@@ -82,6 +84,12 @@ extension LNOrderRecordListViewController: UITableViewDataSource {
     }
 }
 
+extension LNOrderRecordListViewController: LNOrderManagerNotify {
+    func onOrderInfoChanged(orderId: String) {
+        tableView.reloadData()
+    }
+}
+
 extension LNOrderRecordListViewController {
     private func setupViews() {
         title = .init(key: "接单记录")

+ 1 - 1
Lanu/Views/Profile/Edit/LNEditProfilePhotoWallView.swift

@@ -54,7 +54,7 @@ extension LNEditProfilePhotoWallView: LNUploadImageViewDelegate {
 
 extension LNEditProfilePhotoWallView {
     private func handlePhotoClick() {
-        LNBottomSheetMenu.showImageSelectMenu(view: self) { [weak self] image in
+        LNBottomSheetMenu.showImageSelectMenu(view: self, source: .photoWall) { [weak self] image in
             guard let self else { return }
             guard let image else { return }
             let firstView = photoWall.first { $0.image == nil }

+ 4 - 4
Lanu/Views/Profile/Edit/LNEditProfileViewController.swift

@@ -117,7 +117,7 @@ extension LNEditProfileViewController {
 extension LNEditProfileViewController {
     private func checkSaveButton() {
         let enable = profilePhoto.imageUrl != myUserInfo.avatar
-        || photoWall.curPhotos != myGameMateInfo?.photos
+        || photoWall.curPhotos != myUserInfo.photos
         || curName != myUserInfo.nickname
         || curGender != myUserInfo.gender
         || curBirthday != Double(myUserInfo.birthday / 1_000)
@@ -214,7 +214,7 @@ extension LNEditProfileViewController {
                 config.avatar = profilePhoto.imageUrl
             }
             let photos = photoWall.curPhotos
-            if photos != myGameMateInfo?.photos {
+            if photos != myUserInfo.photos {
                 config.photos = photos
             }
             if curName != myUserInfo.nickname {
@@ -274,7 +274,7 @@ extension LNEditProfileViewController {
         profilePhoto.delegate = self
         profilePhoto.onTap { [weak self] in
             guard let self else { return }
-            LNBottomSheetMenu.showImageSelectMenu(view: view) { [weak self] image in
+            LNBottomSheetMenu.showImageSelectMenu(view: view, source: .avatar) { [weak self] image in
                 guard let self else { return }
                 guard let image else { return }
                 profilePhoto.uploadImage(image: image)
@@ -292,7 +292,7 @@ extension LNEditProfileViewController {
     
     private func buildPhotoWall() -> UIView {
         photoWall.delegate = self
-        photoWall.loadImages(myGameMateInfo?.photos ?? [])
+        photoWall.loadImages(myUserInfo.photos)
         
         return photoWall
     }

+ 1 - 1
Lanu/Views/Profile/Post/LNSharePostViewController.swift

@@ -142,7 +142,7 @@ extension LNSharePostViewController {
             generator.setInfo(info: info)
             generator.setAlbum(image: album)
             generator.setSkills(skills: selectedSkills)
-            generator.setShareQRCode(url: "http://localhost:3000/user/category")
+            generator.setShareQRCode(url: .profileShareUrl + "?id=\(myUid)&share=app")
             let shareImage = generator.generate()
             shareImage.saveToLibrary { [weak self] success, err in
                 guard let self else { return }

+ 2 - 1
Lanu/Views/Profile/Profile/LNProfilePhotoWall.swift

@@ -68,7 +68,7 @@ class LNProfilePhotoWall: UIView {
             { [weak self] image, _, _, _, _, _ in
                 guard let self else { return }
                 guard let image else { return }
-                mode.ratio = image.size.height / image.size.width
+                mode.ratio = min(image.size.height / image.size.width, 3.0 / 2.0) // 最大宽高比 3:2
                 self.resortModes()
             }
         }
@@ -225,6 +225,7 @@ private class LNProfilePhotoWallCell: UITableViewCell {
     override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
         super.init(style: style, reuseIdentifier: reuseIdentifier)
         
+        photo.contentMode = .scaleAspectFill
         photo.layer.cornerRadius = 12
         photo.clipsToBounds = true
         photo.isUserInteractionEnabled = true

+ 7 - 0
Lanu/Views/Search/LNUserSearchViewController.swift

@@ -115,6 +115,9 @@ extension LNUserSearchViewController: LNUserSearchHistoryViewDelegate {
     func onUserSearchHistoryView(view: LNUserSearchHistoryView, didClick history: String) {
         searchInput.text = history
         curKeyword = history
+        nextTag = nil
+        curList.removeAll()
+        tableView.reloadData()
         
         searchUser()
     }
@@ -127,6 +130,10 @@ extension LNUserSearchViewController: UITextFieldDelegate {
               !text.isEmpty else { return true }
         
         curKeyword = text
+        nextTag = nil
+        curList.removeAll()
+        tableView.reloadData()
+        
         searchUser()
         
         return true

+ 8 - 0
Lanu/Views/Wallet/LNWalletViewController.swift

@@ -181,6 +181,10 @@ extension LNWalletViewController {
         let container = UIView()
         container.backgroundColor = .init(hex: "#FFC4000D")
         container.layer.cornerRadius = 12
+        container.onTap { [weak self] in
+            guard let self else { return }
+            view.pushToCoinView()
+        }
         
         let coin = UIImageView.coinImageView()
         container.addSubview(coin)
@@ -264,6 +268,10 @@ extension LNWalletViewController {
         let container = UIView()
         container.backgroundColor = .init(hex: "#008FFF0D")
         container.layer.cornerRadius = 12
+        container.onTap { [weak self] in
+            guard let self else { return }
+            view.pushToDiamondView()
+        }
         
         let diamond = UIImageView.diamondImageView()
         container.addSubview(diamond)

+ 26 - 5
Lanu/Views/Web/LNWebViewController.swift

@@ -10,26 +10,39 @@ import UIKit
 import SnapKit
 import WebKit
 import Combine
+import AutoCodable
 
 
 class LNJumpWebViewConfig {
-    let url: String
-    let customTitle: String
+    var url: String
+    var customTitle: String = ""
     var showNavigationBar = true
+    var setSafeArea = true
     
     init(url: String, title: String = "", showNavigationBar: Bool = true) {
         self.url = url
         customTitle = title
         self.showNavigationBar = showNavigationBar
     }
+    
+    init(param: LNWebPageDeeplinkParams) {
+        url = param.url
+        showNavigationBar = param.header
+        setSafeArea = param.safeArea
+    }
 }
 
-
 extension UIView {
     func pushToWebView(_ config: LNJumpWebViewConfig) {
         let vc = LNWebViewController(config: config)
         navigationController?.pushViewController(vc, animated: true)
     }
+    
+    func pushToWebView(_ param: LNWebPageDeeplinkParams) {
+        let config = LNJumpWebViewConfig(param: param)
+        let vc = LNWebViewController(config: config)
+        navigationController?.pushViewController(vc, animated: true)
+    }
 }
 
 
@@ -104,8 +117,16 @@ extension LNWebViewController {
         view.addSubview(webView)
         webView.snp.makeConstraints { make in
             make.directionalHorizontalEdges.equalToSuperview()
-            make.top.equalToSuperview().offset(showNavigationBar ? 0 : UIView.statusBarHeight)
-            make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom)
+            if config.showNavigationBar || !config.setSafeArea {
+                make.top.equalToSuperview()
+            } else if config.setSafeArea {
+                make.top.equalToSuperview().offset(UIView.statusBarHeight)
+            }
+            if config.setSafeArea {
+                make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom)
+            } else {
+                make.bottom.equalToSuperview()
+            }
         }
     }