Explorar el Código

feat: 补充 IM 订单消息逻辑

陈文艺 hace 3 meses
padre
commit
52ba358dbd
Se han modificado 23 ficheros con 454 adiciones y 2 borrados
  1. 1 0
      Lanu.xcodeproj/project.pbxproj
  2. 6 0
      Lanu/Assets.xcassets/IM/Order/Contents.json
  3. 22 0
      Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_accepted.imageset/Contents.json
  4. BIN
      Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_accepted.imageset/ic_im_chat_order_accepted@2x.png
  5. BIN
      Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_accepted.imageset/ic_im_chat_order_accepted@3x.png
  6. 22 0
      Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_cancel.imageset/Contents.json
  7. BIN
      Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_cancel.imageset/ic_im_chat_order_cancel@2x.png
  8. BIN
      Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_cancel.imageset/ic_im_chat_order_cancel@3x.png
  9. 22 0
      Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_complete.imageset/Contents.json
  10. BIN
      Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_complete.imageset/ic_im_chat_order_complete@2x.png
  11. BIN
      Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_complete.imageset/ic_im_chat_order_complete@3x.png
  12. 22 0
      Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_pending.imageset/Contents.json
  13. BIN
      Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_pending.imageset/ic_im_chat_order_pending@2x.png
  14. BIN
      Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_pending.imageset/ic_im_chat_order_pending@3x.png
  15. 22 0
      Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_processing.imageset/Contents.json
  16. BIN
      Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_processing.imageset/ic_im_chat_order_processing@2x.png
  17. BIN
      Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_processing.imageset/ic_im_chat_order_processing@3x.png
  18. 22 0
      Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_success.imageset/Contents.json
  19. BIN
      Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_success.imageset/ic_im_chat_order_success@2x.png
  20. BIN
      Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_success.imageset/ic_im_chat_order_success@3x.png
  21. 269 0
      Lanu/Views/IM/Chat/Cells/LNIMChatOrderMessageCell.swift
  22. 11 1
      Lanu/Views/IM/Chat/LNIMChatViewController.swift
  23. 35 1
      Lanu/Views/IM/Chat/ViewModel/LNIMChatViewModel.swift

+ 1 - 0
Lanu.xcodeproj/project.pbxproj

@@ -170,6 +170,7 @@
 				Views/Home/LNHomeViewController.swift,
 				Views/IM/Chat/Cells/LNIMChatBaseMessageCell.swift,
 				Views/IM/Chat/Cells/LNIMChatImageMessageCell.swift,
+				Views/IM/Chat/Cells/LNIMChatOrderMessageCell.swift,
 				Views/IM/Chat/Cells/LNIMChatSystemMessageCell.swift,
 				Views/IM/Chat/Cells/LNIMChatTextMessageCell.swift,
 				Views/IM/Chat/Cells/LNIMChatUnknownMessageCell.swift,

+ 6 - 0
Lanu/Assets.xcassets/IM/Order/Contents.json

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

+ 22 - 0
Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_accepted.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_accepted.imageset/ic_im_chat_order_accepted@2x.png


BIN
Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_accepted.imageset/ic_im_chat_order_accepted@3x.png


+ 22 - 0
Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_cancel.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_cancel.imageset/ic_im_chat_order_cancel@2x.png


BIN
Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_cancel.imageset/ic_im_chat_order_cancel@3x.png


+ 22 - 0
Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_complete.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_complete.imageset/ic_im_chat_order_complete@2x.png


BIN
Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_complete.imageset/ic_im_chat_order_complete@3x.png


+ 22 - 0
Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_pending.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_pending.imageset/ic_im_chat_order_pending@2x.png


BIN
Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_pending.imageset/ic_im_chat_order_pending@3x.png


+ 22 - 0
Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_processing.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_processing.imageset/ic_im_chat_order_processing@2x.png


BIN
Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_processing.imageset/ic_im_chat_order_processing@3x.png


+ 22 - 0
Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_success.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_success.imageset/ic_im_chat_order_success@2x.png


BIN
Lanu/Assets.xcassets/IM/Order/ic_im_chat_order_success.imageset/ic_im_chat_order_success@3x.png


+ 269 - 0
Lanu/Views/IM/Chat/Cells/LNIMChatOrderMessageCell.swift

@@ -0,0 +1,269 @@
+//
+//  LNIMChatOrderMessageCell.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/12/26.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNIMChatOrderMessageCell: UITableViewCell {
+    private let statusIc = UIImageView()
+    private let statusLabel = UILabel()
+    
+    private let tipsLabel = UILabel()
+    
+    private let gameIc = UIImageView()
+    private let billInfoLabel = UILabel()
+    
+    private let menuButton = UIButton()
+    
+    private var curItem: LNIMMessageData?
+    
+    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+        super.init(style: style, reuseIdentifier: reuseIdentifier)
+        
+        setupViews()
+    }
+    
+    func update(_ data: LNIMMessageData, viewModel: LNIMChatViewModel) {
+        guard let order: LNIMOrderMessage = data.decodeCustomMessage() else { return }
+        
+        let isCreator = !data.imMessage.isSelf
+        
+        tipsLabel.isHidden = false
+        menuButton.isHidden = true
+        
+        gameIc.sd_setImage(with: URL(string: order.categoryIcon))
+        billInfoLabel.text = "\(order.bizCategoryName)x\(order.purchaseQty)\(order.unit)"
+        
+        switch order.status {
+        case .created, .waitingForAccept:
+            if isCreator {
+                statusIc.image = .init(named: "ic_im_chat_order_success")
+                statusLabel.text = .init(key: "Order Success")
+                tipsLabel.text = .init(key: "Waiting for the playmate to accept the order. If the playmate does not reply within 1 hour")
+            } else {
+                statusIc.image = .init(named: "ic_im_chat_order_pending")
+                statusLabel.text = .init(key: "Pending Order")
+                tipsLabel.text = .init(key: "Note: %@", order.customerRemark)
+            }
+        case .completed:
+            statusIc.image = .init(named: "ic_im_chat_order_complete")
+            statusLabel.text = .init(key: "Order Complete")
+            
+            if isCreator {
+                tipsLabel.text = .init(key: "The playmate has accepted the order. Waiting for the playmate to start serving")
+            } else {
+                tipsLabel.text = .init(key: "The service has been completed and the payment has been sent to your wallet!")
+            }
+        case .refunded:
+            if isCreator {
+                statusIc.image = .init(named: "ic_im_chat_order_success")
+                statusLabel.text = .init(key: "")
+            } else {
+                statusIc.image = .init(named: "ic_im_chat_order_pending")
+                statusLabel.text = .init(key: "")
+            }
+        case .accepted:
+            statusIc.image = .init(named: "ic_im_chat_order_accepted")
+            statusLabel.text = .init(key: "Order Accepted")
+            
+            if isCreator {
+                tipsLabel.text = .init(key: "The playmate has accepted the order. Waiting for the playmate to start serving")
+            } else {
+                tipsLabel.isHidden = true
+                menuButton.isHidden = false
+                menuButton.setTitle(.init(key: "Start Service"), for: .normal)
+            }
+        case .rejected:
+            statusIc.image = .init(named: "ic_im_chat_order_cancel")
+            statusLabel.text = .init(key: "Order Rejected")
+            
+            if isCreator {
+                tipsLabel.text = .init(key: "The playmate has declined the order. Please place your order here or choose another playmate")
+            } else {
+                tipsLabel.text = .init(key: "You have terminated the order!")
+            }
+        case .servicing:
+            statusIc.image = .init(named: "ic_im_chat_order_processing")
+            statusLabel.text = .init(key: "In Order Service")
+            
+            if isCreator {
+                tipsLabel.text = "The playmate has accepted the order. Waiting for the playmate to start serving"
+            } else {
+                tipsLabel.text = "The playmate has accepted the order. Waiting for the playmate to start serving"
+                menuButton.isHidden = false
+                menuButton.setTitle(.init(key: "Service Completion"), for: .normal)
+            }
+        case .serviceDone:
+            statusIc.image = .init(named: "ic_im_chat_order_processing")
+            statusLabel.text = .init(key: "In Order Service")
+            
+            if isCreator {
+                tipsLabel.text = "The playmate has accepted the order. Waiting for the playmate to start serving"
+                menuButton.isHidden = false
+                menuButton.setTitle(.init(key: "Service Completion"), for: .normal)
+            } else {
+                tipsLabel.text = "The playmate has accepted the order. Waiting for the playmate to start serving"
+            }
+        case .cancelled:
+            statusIc.image = .init(named: "ic_im_chat_order_success")
+            statusLabel.text = .init(key: "Order Canceled")
+            
+            if isCreator {
+                tipsLabel.text = .init(key: "The playmate has declined the order. Please place your order here or choose another playmate")
+            } else {
+                tipsLabel.text = .init(key: "You did not respond to the order within the countdown, and the order will be automatically cancelled !")
+            }
+        }
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNIMChatOrderMessageCell {
+    private func setupViews() {
+        backgroundColor = .clear
+        
+        let container = UIView()
+        container.backgroundColor = .fill
+        container.layer.cornerRadius = 12
+        contentView.addSubview(container)
+        container.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview().inset(16)
+            make.verticalEdges.equalToSuperview()
+        }
+        
+        let mainStackView = UIStackView()
+        mainStackView.axis = .vertical
+        mainStackView.spacing = 12
+        container.addSubview(mainStackView)
+        mainStackView.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview().inset(12)
+            make.verticalEdges.equalToSuperview().inset(12)
+        }
+        
+        let infoStackView = UIStackView()
+        infoStackView.axis = .vertical
+        infoStackView.spacing = 6
+        infoStackView.onTap { [weak self] in
+            guard let self else { return }
+            guard let order: LNIMOrderMessage = curItem?.decodeCustomMessage() else { return }
+            pushToOrderDetail(orderId: order.orderId)
+        }
+        mainStackView.addArrangedSubview(infoStackView)
+        
+        infoStackView.addArrangedSubview(buildStatusView())
+        infoStackView.addArrangedSubview(buildTipsView())
+        infoStackView.addArrangedSubview(buildLine())
+        infoStackView.addArrangedSubview(buildBillInfoView())
+        
+        mainStackView.addArrangedSubview(buildMenu())
+    }
+    
+    private func buildStatusView() -> UIView {
+        let container = UIView()
+        container.snp.makeConstraints { make in
+            make.height.equalTo(24)
+        }
+        
+        container.addSubview(statusIc)
+        statusIc.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalToSuperview()
+            make.width.height.equalTo(24)
+        }
+        
+        statusLabel.font = .heading_h4
+        statusLabel.textColor = .text_5
+        container.addSubview(statusLabel)
+        statusLabel.snp.makeConstraints { make in
+            make.leading.equalTo(statusIc.snp.trailing).offset(6)
+            make.centerY.equalToSuperview()
+        }
+        
+        let arrow = UIImageView.arrowImageView(size: 16)
+        arrow.tintColor = .text_4
+        container.addSubview(arrow)
+        arrow.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview()
+        }
+        
+        return container
+    }
+    
+    private func buildTipsView() -> UIView {
+        tipsLabel.font = .body_xs
+        tipsLabel.textColor = .text_3
+        tipsLabel.numberOfLines = 0
+        
+        return tipsLabel
+    }
+    
+    private func buildLine() -> UIView {
+        let line = UIView()
+        line.backgroundColor = .fill_2
+        line.snp.makeConstraints { make in
+            make.height.equalTo(0.5)
+        }
+        
+        return line
+    }
+    
+    private func buildBillInfoView() -> UIView {
+        let container = UIView()
+        container.snp.makeConstraints { make in
+            make.height.equalTo(20)
+        }
+        
+        let backgroundIc = UIImageView()
+        backgroundIc.layer.cornerRadius = 10
+        backgroundIc.image = .primary_7
+        backgroundIc.clipsToBounds = true
+        container.addSubview(backgroundIc)
+        backgroundIc.snp.makeConstraints { make in
+            make.leading.equalToSuperview()
+            make.centerY.equalToSuperview()
+            make.width.height.equalTo(20)
+        }
+        
+        gameIc.backgroundColor = .fill
+        gameIc.layer.cornerRadius = 9
+        backgroundIc.addSubview(gameIc)
+        gameIc.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+            make.width.height.equalTo(18)
+        }
+        
+        billInfoLabel.font = .body_s
+        billInfoLabel.textColor = .text_4
+        container.addSubview(billInfoLabel)
+        billInfoLabel.snp.makeConstraints { make in
+            make.leading.equalTo(backgroundIc.snp.trailing).offset(6)
+            make.centerY.equalToSuperview()
+        }
+        
+        return container
+    }
+    
+    private func buildMenu() -> UIView {
+        menuButton.layer.cornerRadius = 20
+        menuButton.setBackgroundImage(.primary_8, for: .normal)
+        menuButton.clipsToBounds = true
+        menuButton.setTitleColor(.text_1, for: .normal)
+        menuButton.titleLabel?.font = .heading_h3
+        menuButton.snp.makeConstraints { make in
+            make.height.equalTo(40)
+        }
+        
+        return menuButton
+    }
+}
+

+ 11 - 1
Lanu/Views/IM/Chat/LNIMChatViewController.swift

@@ -142,7 +142,16 @@ extension LNIMChatViewController: UITableViewDataSource, UITableViewDelegate {
             let cell = tableView.dequeueReusableCell(withIdentifier: LNIMChatVoiceMessageCell.className, for: indexPath) as! LNIMChatVoiceMessageCell
             cell.update(data, viewModel: viewModel)
             return cell
-        case .none, .custom:
+        case .custom(let subType):
+            switch subType {
+            case .playmate_order:
+                let cell = tableView.dequeueReusableCell(withIdentifier: LNIMChatOrderMessageCell.className, for: indexPath) as! LNIMChatOrderMessageCell
+                cell.update(data, viewModel: viewModel)
+                return cell
+            default:
+                break
+            }
+        case .none:
             break
         }
         return tableView.dequeueReusableCell(withIdentifier: LNIMChatUnknownMessageCell.className, for: indexPath)
@@ -206,6 +215,7 @@ extension LNIMChatViewController {
         tableView.register(LNIMChatTextMessageCell.self, forCellReuseIdentifier: LNIMChatTextMessageCell.className)
         tableView.register(LNIMChatVoiceMessageCell.self, forCellReuseIdentifier: LNIMChatVoiceMessageCell.className)
         tableView.register(LNIMChatUnknownMessageCell.self, forCellReuseIdentifier: LNIMChatUnknownMessageCell.className)
+        tableView.register(LNIMChatOrderMessageCell.self, forCellReuseIdentifier: LNIMChatOrderMessageCell.className)
         tableView.dataSource = self
         tableView.delegate = self
         view.addSubview(tableView)

+ 35 - 1
Lanu/Views/IM/Chat/ViewModel/LNIMChatViewModel.swift

@@ -30,6 +30,7 @@ class LNIMChatViewModel: NSObject {
     private(set) var allMessage: [LNIMMessageData] = []
     private var lastData: Date? = nil
     private var lastMessage: V2TIMMessage?
+    private var orderMessageCache: [String: LNIMMessageData] = [:]
     
     // 配置
     @Published
@@ -214,6 +215,35 @@ extension LNIMChatViewModel {
                 datas.append(dateMessage)
             }
             let data = LNIMMessageData(imMessage: message)
+            if case .custom(.playmate_order) = data.type {
+                if let orderMessage: LNIMOrderMessage = data.decodeCustomMessage() {
+                    if let oldMessage = orderMessageCache[orderMessage.orderId] {
+                        // 存在旧的订单信息
+                        if (oldMessage.imMessage.timestamp?.timeIntervalSince1970 ?? 0)
+                            > (message.timestamp?.timeIntervalSince1970 ?? 0) {
+                            // 消息为旧的订单信息,忽略
+                            continue
+                        } else {
+                            // 订单的新消息
+                            if let index = allMessage.firstIndex(of: oldMessage) {
+                                // 移除旧的订单消息
+                                allMessage.remove(at: index)
+                                notifyMessageChanged(index: index, type: .delete, toBottom: false)
+                            }
+                            if let index = datas.firstIndex(of: oldMessage) {
+                                datas.remove(at: index)
+                            }
+                            
+                            // 生成新的订单消息
+                            orderMessageCache[orderMessage.orderId] = data
+                        }
+                    } else {
+                        orderMessageCache[orderMessage.orderId] = data
+                    }
+                } else {
+                    continue // 解析失败,忽略
+                }
+            }
             datas.append(data)
         }
         
@@ -235,7 +265,11 @@ extension LNIMChatViewModel {
 extension LNIMChatViewModel: V2TIMAdvancedMsgListener {
     func onRecvNewMessage(msg: V2TIMMessage!) {
         guard msg.userID == userId else { return }
-        allMessage.append(LNIMMessageData(imMessage: msg))
+        
+        let list = transUIMsgFromIMMsg(messages: [msg])
+        if list.isEmpty { return }
+        
+        allMessage.append(contentsOf: list)
         notifyMessageChanged(index: allMessage.count - 1, type: .insert, toBottom: true)
     }