Przeglądaj źródła

Merge remote-tracking branch 'origin/feat/profile_comment' into dev

* origin/feat/profile_comment:
  feat: 强制升级弹窗增加背景图
  feat: 调整强制更新弹窗的文本样式,以及升级按钮文本
  feat: 强制升级弹窗文案对其方式改为自然对齐
  feat: 调整个人页在房挂件的位置
  feat: 移除用户信息中的点赞数显示
  feat: 个人页封面增加连击点赞效果
  feat: 增加个人页点赞接口逻辑
  feat: 增加个人页点赞功能
  feat: 移除废弃接口
  feat: 移除原用户缓存逻辑
  feat: 移除个人页评分逻辑,增加点赞功能
  feat: 评论 id 进行隐藏
陈文艺 2 dni temu
rodzic
commit
7786e4706f

+ 2 - 10
Lanu.xcodeproj/project.pbxproj

@@ -346,8 +346,6 @@
 				Views/Profile/Profile/LNProfileInfosView.swift,
 				Views/Profile/Profile/LNProfileInRoomView.swift,
 				Views/Profile/Profile/LNProfilePhotoWall.swift,
-				Views/Profile/Profile/LNProfileScoreFloatingView.swift,
-				Views/Profile/Profile/LNProfileStaringPanel.swift,
 				Views/Profile/Profile/LNProfileTabView.swift,
 				Views/Profile/Profile/LNProfileUserInfoView.swift,
 				Views/Profile/Profile/LNProfileViewController.swift,
@@ -445,6 +443,8 @@
 		};
 		FBB67E232EC48B440070E686 /* ThirdParty */ = {
 			isa = PBXFileSystemSynchronizedRootGroup;
+			exceptions = (
+			);
 			path = ThirdParty;
 			sourceTree = "<group>";
 		};
@@ -597,14 +597,10 @@
 			inputFileListPaths = (
 				"${PODS_ROOT}/Target Support Files/Pods-Gami/Pods-Gami-resources-${CONFIGURATION}-input-files.xcfilelist",
 			);
-			inputPaths = (
-			);
 			name = "[CP] Copy Pods Resources";
 			outputFileListPaths = (
 				"${PODS_ROOT}/Target Support Files/Pods-Gami/Pods-Gami-resources-${CONFIGURATION}-output-files.xcfilelist",
 			);
-			outputPaths = (
-			);
 			runOnlyForDeploymentPostprocessing = 0;
 			shellPath = /bin/sh;
 			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Gami/Pods-Gami-resources.sh\"\n";
@@ -640,14 +636,10 @@
 			inputFileListPaths = (
 				"${PODS_ROOT}/Target Support Files/Pods-Gami/Pods-Gami-frameworks-${CONFIGURATION}-input-files.xcfilelist",
 			);
-			inputPaths = (
-			);
 			name = "[CP] Embed Pods Frameworks";
 			outputFileListPaths = (
 				"${PODS_ROOT}/Target Support Files/Pods-Gami/Pods-Gami-frameworks-${CONFIGURATION}-output-files.xcfilelist",
 			);
-			outputPaths = (
-			);
 			runOnlyForDeploymentPostprocessing = 0;
 			shellPath = /bin/sh;
 			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Gami/Pods-Gami-frameworks.sh\"\n";

+ 29 - 0
Lanu/Common/Extension/String+Extension.swift

@@ -158,3 +158,32 @@ extension String {
         return .init(width: width!, height: height!)
     }
 }
+
+extension String {
+    /// 字符串脱敏:展示前后字符,中间用 * 隐藏
+    /// - Parameters:
+    ///   - prefixCount: 开头保留的字符数量(默认 1)
+    ///   - suffixCount: 结尾保留的字符数量(默认 1)
+    ///   - maskSymbol: 隐藏符号(默认 *)
+    /// - Returns: 脱敏后的字符串
+    func hideMiddle(prefixCount: Int = 1, suffixCount: Int = 1, maskSymbol: String = "*") -> String {
+        // 空字符串直接返回
+        guard !isEmpty else { return self }
+        
+        let totalCount = count
+        // 总长度 ≤ 前后保留长度之和,直接返回原字符串
+        guard totalCount > prefixCount + suffixCount else { return self }
+        
+        // 截取开头
+        let prefix = prefix(prefixCount)
+        // 截取结尾
+        let suffix = suffix(suffixCount)
+        // 计算需要隐藏的字符数量
+        let maskLength = totalCount - prefixCount - suffixCount
+        // 生成对应数量的隐藏符号
+        let mask = String(repeating: maskSymbol, count: maskLength)
+        
+        // 拼接结果
+        return prefix + mask + suffix
+    }
+}

+ 97 - 0
Lanu/Common/Extension/UIView+Extension.swift

@@ -9,6 +9,60 @@ import Foundation
 import UIKit
 
 
+enum LNTapComboEvent {
+    case single(UITapGestureRecognizer)
+    case combo(UITapGestureRecognizer, count: Int)
+}
+
+
+private class LNTapComboHandler {
+    typealias Handler = (LNTapComboEvent) -> Void
+    
+    private let interval: TimeInterval
+    private let handler: Handler
+    private var tapCount = 0
+    private weak var firstGesture: UITapGestureRecognizer?
+    private var pendingWorkItem: DispatchWorkItem?
+    
+    init(interval: TimeInterval = 0.25, handler: @escaping Handler) {
+        self.interval = interval
+        self.handler = handler
+    }
+    
+    func registerTap(_ gesture: UITapGestureRecognizer) {
+        tapCount += 1
+        if tapCount == 1 {
+            firstGesture = gesture
+        } else {
+            handler(.combo(gesture, count: tapCount))
+        }
+        
+        pendingWorkItem?.cancel()
+        let workItem = DispatchWorkItem { [weak self] in
+            self?.finishCurrentRound()
+        }
+        pendingWorkItem = workItem
+        DispatchQueue.main.asyncAfter(deadline: .now() + interval, execute: workItem)
+    }
+    
+    func reset() {
+        pendingWorkItem?.cancel()
+        pendingWorkItem = nil
+        tapCount = 0
+        firstGesture = nil
+    }
+    
+    private func finishCurrentRound() {
+        defer { reset() }
+        guard tapCount == 1, let firstGesture else { return }
+        handler(.single(firstGesture))
+    }
+    
+    deinit {
+        pendingWorkItem?.cancel()
+    }
+}
+
 extension UIView {
     var navigationController: UINavigationController? {
         let vc = viewController
@@ -114,5 +168,48 @@ extension UIView {
     func onLongPress(_ block: @escaping () -> Void) {
         let tap = BlockLongPressGestureRecognizer(action: block)
         addGestureRecognizer(tap)
+        isUserInteractionEnabled = true
+    }
+    
+    private class DoubleTapGestureRecognizer: UITapGestureRecognizer {
+        private var actionBlock: ((UITapGestureRecognizer) -> Void)?
+        
+        init(action block: ((UITapGestureRecognizer) -> Void)?) {
+            super.init(target: nil, action: nil)
+            numberOfTapsRequired = 2
+            self.actionBlock = block
+            // 设置目标和动作
+            addTarget(self, action: #selector(handleTap(_:)))
+        }
+        
+        @objc private func handleTap(_ gesture: BlockTapGestureRecognizer) {
+            actionBlock?(self)
+        }
+    }
+    
+    func onDoubleTap(_ block: @escaping (UITapGestureRecognizer) -> Void) {
+        let doubleTap = DoubleTapGestureRecognizer(action: block)
+        addGestureRecognizer(doubleTap)
+        isUserInteractionEnabled = true
+    }
+    
+    private final class ComboTapGestureRecognizer: UITapGestureRecognizer {
+        private var comboHandler: LNTapComboHandler?
+        
+        init(interval: TimeInterval, handler: @escaping LNTapComboHandler.Handler) {
+            super.init(target: nil, action: nil)
+            self.comboHandler = LNTapComboHandler(interval: interval, handler: handler)
+            addTarget(self, action: #selector(handleTap))
+        }
+        
+        @objc private func handleTap() {
+            comboHandler?.registerTap(self)
+        }
+    }
+    
+    func onTapCombo(interval: TimeInterval = 0.25, _ block: @escaping (LNTapComboEvent) -> Void) {
+        let comboTap = ComboTapGestureRecognizer(interval: interval, handler: block)
+        addGestureRecognizer(comboTap)
+        isUserInteractionEnabled = true
     }
 }

+ 2 - 1
Lanu/Common/Views/Menu/LNCommonAlertView.swift

@@ -24,7 +24,6 @@ extension LNCommonAlertView {
 
 class LNCommonAlertView: UIView {
     private let background = UIView()
-    private let container = UIView()
     private let closeButton = UIButton()
     var showCloseButton = true {
         didSet {
@@ -39,6 +38,7 @@ class LNCommonAlertView: UIView {
     private let messageView = UIStackView()
     private let buttonViews = UIStackView()
     
+    let container = UIView()
     let titleLabel = UILabel()
     let messageLabel = UILabel()
     
@@ -151,6 +151,7 @@ extension LNCommonAlertView {
         
         container.backgroundColor = .fill
         container.layer.cornerRadius = 20
+        container.clipsToBounds = true
         addSubview(container)
         container.snp.makeConstraints { make in
             make.center.equalToSuperview()

+ 26 - 3
Lanu/Localizable.xcstrings

@@ -5343,19 +5343,19 @@
         "en" : {
           "stringUnit" : {
             "state" : "translated",
-            "value" : "%d Fans"
+            "value" : "%@ Fans"
           }
         },
         "id" : {
           "stringUnit" : {
             "state" : "translated",
-            "value" : "%d Penggemar"
+            "value" : "%@ Penggemar"
           }
         },
         "zh-Hans" : {
           "stringUnit" : {
             "state" : "translated",
-            "value" : "%d 粉丝"
+            "value" : "%@ 粉丝"
           }
         }
       }
@@ -12145,6 +12145,29 @@
         }
       }
     },
+    "B00139" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Update Now"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Perbarui Sekarang"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "立即更新"
+          }
+        }
+      }
+    },
     "C00001" : {
       "extractionState" : "manual",
       "localizations" : {

+ 0 - 9
Lanu/Manager/GameMate/LNGameMateManager.swift

@@ -89,15 +89,6 @@ extension LNGameMateManager {
         }
     }
     
-    func scoreGameMate(uid: String, score: Int, queue: DispatchQueue = .main,
-                       handler: @escaping (Bool) -> Void) {
-        LNHttpManager.shared.scoreGameMate(uid: uid, score: score) { err in
-            queue.asyncIfNotGlobal {
-                handler(err == nil)
-            }
-        }
-    }
-    
     func getSkillCommentList(id: String, next: String? = nil,
                              queue: DispatchQueue = .main,
                              handler: @escaping (LNSkillCommentListResponse?) -> Void) {

+ 0 - 8
Lanu/Manager/GameMate/Network/LNHttpManager+GameMate.swift

@@ -15,7 +15,6 @@ private let kNetPath_GameMate_List = "/skill/list"
 private let kNetPath_GameMate_Skills = "/skill/user/goods"
 private let kNetPath_GameMate_Info = "/user/playmate/info"
 private let kNetPath_GameMate_Skill_Detail = "/skill/detail"
-private let kNetPath_GameMate_Score = "/user/playmate/charmStar"
 private let kNetPath_GameMate_Skill_Comment = "/skill/goods/comments"
 
 private let kNetPath_GameMate_Search = "/playmate/search"
@@ -192,13 +191,6 @@ extension LNHttpManager {
         ], completion: completion)
     }
     
-    func scoreGameMate(uid: String, score: Int, completion: @escaping (LNHttpError?) -> Void) {
-        post(path: kNetPath_GameMate_Score, params: [
-            "userNo": uid,
-            "star": score
-        ], completion: completion)
-    }
-    
     func getSkillCommentList(id: String, size: Int, next: String,
                              completion: @escaping (LNSkillCommentListResponse?, LNHttpError?) -> Void) {
         post(path: kNetPath_GameMate_Skill_Comment, params: [

+ 10 - 96
Lanu/Manager/Profile/LNProfileManager.swift

@@ -43,11 +43,6 @@ class LNProfileManager {
     
     private var randomProfile: LNRandomProfileResponse?
     
-    private static let profileCacheLimit = 300
-    private var profileCache: [String: LNProfileUserInfo] = [:]
-    private var profileCacheOrder: [String] = []
-    private let profileCacheLock = NSLock()
-    
     private let captchaCoolDown = 60
     private var captchaRemain = 0
     private var captchaTimer: Timer?
@@ -131,16 +126,7 @@ extension LNProfileManager {
             guard let self else { return }
             if let res, err == nil {
                 res.list.forEach {
-                    if $0.userNo.isMyUid {
-                        if self.myUserInfo.update($0) {
-                            self.notifyUserInfoChanged(newInfo: $0)
-                        }
-                    } else {
-                        self.updateProfileCache(
-                            uid: $0.userNo,
-                            name: $0.nickname,
-                            avatar: $0.avatar
-                        )
+                    if !$0.userNo.isMyUid || self.myUserInfo.update($0) {
                         self.notifyUserInfoChanged(newInfo: $0)
                     }
                 }
@@ -169,92 +155,20 @@ extension LNProfileManager {
         }
     }
     
-//    func getUserOnlineState(uids: [String], queue: DispatchQueue = .main, handler: @escaping ([String: Bool]) -> Void) {
-//        LNHttpManager.shared.getUserOnlineState(uids: uids) { res, err in
-//            queue.asyncIfNotGlobal {
-//                handler(res?.list.reduce(into: [String: Bool](), { partialResult, state in
-//                    partialResult[state.userNo] = state.online
-//                }) ?? [:])
-//            }
-//        }
-//    }
-}
-
-extension LNProfileManager {
-    func getCachedProfileUserInfo(uid: String) -> LNProfileUserInfo? {
-        profileCacheLock.lock()
-        defer { profileCacheLock.unlock() }
-        guard let info = profileCache[uid] else {
-            return nil
-        }
-        touchProfileCacheKey(uid)
-        return info
-    }
-    
-    func getCachedProfileUserInfo(uid: String,
-                                  fetchIfNeeded: Bool = true,
-                                  queue: DispatchQueue = .main,
-                                  handler: @escaping (LNProfileUserInfo?) -> Void) {
-        if let info = getCachedProfileUserInfo(uid: uid) {
+    func getUserLikeInfo(uid: String, queue: DispatchQueue = .main,
+                         handler: @escaping (LNUserLikeInfoResponse?) -> Void) {
+        LNHttpManager.shared.getUserLikeInfo(uid: uid) { res, err in
             queue.asyncIfNotGlobal {
-                handler(info)
+                handler(res)
             }
-            return
-        }
-        
-        guard fetchIfNeeded else {
-            queue.asyncIfNotGlobal {
-                handler(nil)
-            }
-            return
         }
-        
-        getUserProfileDetail(uid: uid, queue: queue) { [weak self] profile in
-            guard let self, let profile else {
-                handler(nil)
-                return
-            }
-            
-            self.updateProfileCache(
-                uid: profile.userNo,
-                name: profile.nickname,
-                avatar: profile.avatar
-            )
-            handler(self.getCachedProfileUserInfo(uid: uid))
-        }
-    }
-    
-    func updateProfileCache(uid: String,
-                            name: String,
-                            avatar: String) {
-        guard !uid.isEmpty else {
-            return
-        }
-        
-        profileCacheLock.lock()
-        defer { profileCacheLock.unlock() }
-        
-        let info = profileCache[uid] ?? LNProfileUserInfo()
-        info.uid = uid
-        info.name = name
-        info.avatar = avatar
-        
-        profileCache[uid] = info
-        touchProfileCacheKey(uid)
-        trimProfileCacheIfNeeded()
     }
     
-    private func touchProfileCacheKey(_ uid: String) {
-        if let index = profileCacheOrder.firstIndex(of: uid) {
-            profileCacheOrder.remove(at: index)
-        }
-        profileCacheOrder.append(uid)
-    }
-    
-    private func trimProfileCacheIfNeeded() {
-        while profileCacheOrder.count > Self.profileCacheLimit {
-            let expiredUid = profileCacheOrder.removeFirst()
-            profileCache.removeValue(forKey: expiredUid)
+    func likeUser(uid: String, like: Bool, queue: DispatchQueue = .main, handler: @escaping (Bool) -> Void) {
+        LNHttpManager.shared.likeUser(uid: uid, like: like) { err in
+            queue.asyncIfNotGlobal {
+                handler(err == nil)
+            }
         }
     }
 }

+ 12 - 5
Lanu/Manager/Profile/Network/LNHttpManager+Profile.swift

@@ -17,7 +17,8 @@ private let kNetPath_Profile_UsersInfo = "/user/get/infos"
 
 private let kNetPath_Profile_Random = "/user/info/random/profiles"
 
-private let kNetPath_Profile_Online = "/user/getUsersOnlineState"
+private let kNetPath_Profile_Like_Info = "/user/like/info"
+private let kNetPath_Profile_Like = "/user/like/submit"
 
 private let kNetPath_Profile_Bind_Captcha = "/user/bind/mobile/sendCode"
 private let kNetPath_Profile_Bind_Phone = "/user/bind/mobile"
@@ -93,11 +94,17 @@ extension LNHttpManager {
         post(path: kNetPath_Profile_Random, completion: completion)
     }
     
-    func getUserOnlineState(uids: [String], completion: @escaping (LNUserOnlineStateResponse?, LNHttpError?) -> Void) {
-        post(path: kNetPath_Profile_Online, params: [
-            "userNos": uids
+    func getUserLikeInfo(uid: String, completion: @escaping (LNUserLikeInfoResponse?, LNHttpError?) -> Void) {
+        post(path: kNetPath_Profile_Like_Info, params: [
+            "id": uid
+        ], completion: completion)
+    }
+    
+    func likeUser(uid: String, like: Bool, completion: @escaping (LNHttpError?) -> Void) {
+        post(path: kNetPath_Profile_Like, params: [
+            "userNo": uid,
+            "like": like
         ], completion: completion)
-        
     }
 }
 

+ 6 - 0
Lanu/Manager/Profile/Network/LNProfileResponse.swift

@@ -167,3 +167,9 @@ class LNUserOnlineStateVO: Decodable {
 class LNUserOnlineStateResponse: Decodable {
     var list: [LNUserOnlineStateVO] = []
 }
+
+@AutoCodable
+class LNUserLikeInfoResponse: Decodable {
+    var count: Int = 0
+    var liked: Bool = false
+}

+ 13 - 3
Lanu/SceneDelegate.swift

@@ -7,6 +7,7 @@
 
 import UIKit
 import Combine
+import SnapKit
 
 #if DEBUG
 import DoraemonKit
@@ -118,12 +119,21 @@ extension SceneDelegate: LNAccountManagerNotify {
         }
         
         LNConfigManager.shared.getUpdateInfo { res in
-            if let res, res.platform == 2, res.minVersion >= curAppVersion {
+            if let res, res.platform == 2, res.minVersion > curAppVersion {
+                let cover = UIImageView(image: .icHomeTopBg)
+                
                 let alert = LNCommonAlertView()
+                alert.container.insertSubview(cover, at: 0)
+                cover.snp.makeConstraints { make in
+                    make.horizontalEdges.equalToSuperview()
+                    make.top.equalToSuperview()
+                }
+                alert.touchOutsideCancel = false
                 alert.showCloseButton = false
                 alert.titleLabel.text = .init(key: "B00058")
                 alert.messageLabel.text = res.tip
-                alert.showConfirm(autoDismiss: false) {
+                alert.messageLabel.textAlignment = .natural
+                alert.showConfirm(.init(key: "B00139"), autoDismiss: false) {
                     LNAppConfig.shared.jumpToAppStore()
                 }
                 alert.popup()
@@ -132,7 +142,7 @@ extension SceneDelegate: LNAccountManagerNotify {
                     guard hasNew else { return }
                     let alert = LNCommonAlertView()
                     alert.titleLabel.text = .init(key: "A00392")
-                    alert.showConfirm {
+                    alert.showConfirm(.init(key: "B00139")) {
                         LNAppConfig.shared.jumpToAppStore()
                     }
                     alert.showCancel()

+ 1 - 1
Lanu/Views/Game/Skill/LNSkillCommentsView.swift

@@ -175,7 +175,7 @@ class LNSkillCommentItemView: UIView {
     
     func update(_ comment: LNSkillCommentVO) {
         avatar.showAvatar(comment.avatar)
-        nameLabel.text = comment.nickname
+        nameLabel.text = comment.nickname.hideMiddle()
         starView.score = comment.star
         commentLabel.text = comment.comment
         timeLabel.text = TimeInterval(comment.time / 1_000).tencentIMTimeDesc

+ 0 - 161
Lanu/Views/Profile/Profile/LNProfileScoreFloatingView.swift

@@ -1,161 +0,0 @@
-//
-//  LNProfileScoreFloatingView.swift
-//  Lanu
-//
-//  Created by OneeChan on 2025/12/16.
-//
-
-import Foundation
-import UIKit
-import SnapKit
-
-
-class LNProfileScoreFloatingView: UIView {
-    private var curDetail: LNUserProfileVO?
-    private let background = UIImageView()
-    
-    override init(frame: CGRect) {
-        super.init(frame: frame)
-        
-        setupViews()
-        setupGesture()
-    }
-    
-    func update(_ detail: LNUserProfileVO) {
-        curDetail = detail
-        isHidden = !detail.playmate || detail.rated || detail.userNo.isMyUid
-    }
-    
-    override func layoutSubviews() {
-        super.layoutSubviews()
-        background.layer.cornerRadius = background.bounds.height * 0.5
-    }
-    
-    required init?(coder: NSCoder) {
-        fatalError("init(coder:) has not been implemented")
-    }
-}
-
-extension LNProfileScoreFloatingView {
-    @objc
-    private func handlePan(_ ges: UIPanGestureRecognizer) {
-        let location = ges.location(in: superview)
-        
-        switch ges.state {
-        case .began:
-            break
-        case .changed:
-            center = location
-            break
-        default:
-            updatePosition(animated: true)
-            break
-        }
-    }
-}
-
-extension LNProfileScoreFloatingView {
-    private func updatePosition(animated: Bool) {
-        guard let superview else { return }
-        let movement = { [weak self] in
-            guard let self else { return }
-            
-            let y = center.y.bounded(min: 160, max: superview.bounds.height - 160)
-//            if center.x > superview.bounds.width * 0.5 {
-                center = .init(x: superview.bounds.width - bounds.width * 0.5 - 8, y: y)
-//            } else {
-//                center = .init(x: bounds.width * 0.5 + 8, y: y)
-//            }
-        }
-        if animated {
-            UIView.animate(withDuration: 0.25, animations: movement)
-        } else {
-            movement()
-        }
-    }
-    
-    private func setupViews() {
-        let ic = UIImageView()
-        ic.image = .icProfileFillScore
-        addSubview(ic)
-        ic.snp.makeConstraints { make in
-            make.centerX.equalToSuperview()
-            make.top.equalToSuperview()
-            make.leading.greaterThanOrEqualToSuperview()
-        }
-        
-        background.image = .primary_7
-        background.layer.cornerRadius = 9.5
-        background.clipsToBounds = true
-        addSubview(background)
-        background.snp.makeConstraints { make in
-            make.horizontalEdges.equalToSuperview()
-            make.bottom.equalToSuperview()
-            make.top.equalTo(ic.snp.bottom).offset(-12)
-            make.width.lessThanOrEqualTo(74)
-        }
-        
-        let label = UILabel()
-        label.text = .init(key: "A00227")
-        label.font = .init(name: UIFont.boldFontName, size: 11)
-        label.textColor = .text_1
-        label.textAlignment = .center
-        label.numberOfLines = 2
-        background.addSubview(label)
-        label.snp.makeConstraints { make in
-            make.verticalEdges.equalToSuperview().inset(2)
-            make.horizontalEdges.equalToSuperview().inset(5.5)
-        }
-    }
-    
-    private func setupGesture() {
-        let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
-        addGestureRecognizer(pan)
-        
-        onTap { [weak self] in
-            guard let self else { return }
-            guard let curDetail else { return }
-            
-            let panel = LNProfileStaringPanel()
-            panel.updateAvatar(url: curDetail.avatar)
-            panel.handler = { [weak self] score in
-                guard let self else { return }
-                LNGameMateManager.shared.scoreGameMate(uid: curDetail.userNo, score: Int(score))
-                { [weak self] success in
-                    guard let self else { return }
-                    if success {
-                        showToast(.init(key: "A00228"))
-                        isHidden = true
-                    }
-                }
-            }
-            panel.popup()
-        }
-    }
-}
-
-#if DEBUG
-
-import SwiftUI
-
-struct LNProfileScoreFloatingViewPreview: UIViewRepresentable {
-    func makeUIView(context: Context) -> some UIView {
-        let container = UIView()
-        container.backgroundColor = .lightGray
-        
-        let view = LNProfileScoreFloatingView()
-        container.addSubview(view)
-        view.snp.makeConstraints { make in
-            make.center.equalToSuperview()
-        }
-        
-        return container
-    }
-    
-    func updateUIView(_ uiView: UIViewType, context: Context) { }
-}
-
-#Preview(body: {
-    LNProfileScoreFloatingViewPreview()
-})
-#endif

+ 0 - 120
Lanu/Views/Profile/Profile/LNProfileStaringPanel.swift

@@ -1,120 +0,0 @@
-//
-//  LNProfileStaringPanel.swift
-//  Lanu
-//
-//  Created by OneeChan on 2025/12/2.
-//
-
-import Foundation
-import UIKit
-import SnapKit
-
-
-class LNProfileStaringPanel: LNPopupView {
-    var handler: ((Double) -> Void)? = nil
-    
-    private let avatar = UIImageView()
-    private let titleLabel = UILabel()
-    private let scoreView = LNFiveStarScoreView()
-    private let confirmButton = UIButton()
-    
-    override init(frame: CGRect) {
-        super.init(frame: frame)
-        
-        setupViews()
-    }
-    
-    func updateAvatar(url: String) {
-        avatar.sd_setImage(with: URL(string: url))
-    }
-    
-    required init?(coder: NSCoder) {
-        fatalError("init(coder:) has not been implemented")
-    }
-}
-
-extension LNProfileStaringPanel: LNFiveStarScoreViewDelegate {
-    func onFiveStarScoreView(view: LNFiveStarScoreView, scoreChanged newScore: Double) {
-        let bg = newScore == 0 ? nil : UIImage.primary_8
-        confirmButton.setBackgroundImage(bg, for: .normal)
-    }
-}
-
-extension LNProfileStaringPanel {
-    private func setupViews() {
-        avatar.backgroundColor = .fill
-        avatar.layer.cornerRadius = 30
-        avatar.layer.borderColor = UIColor.fill.cgColor
-        avatar.layer.borderWidth = 2
-        avatar.clipsToBounds = true
-        container.addSubview(avatar)
-        avatar.snp.makeConstraints { make in
-            make.centerX.equalToSuperview()
-            make.top.equalToSuperview().offset(-16)
-            make.width.height.equalTo(60)
-        }
-        
-        titleLabel.font = .heading_h3
-        titleLabel.textColor = .text_5
-        titleLabel.text = .init(key: "A00194")
-        titleLabel.textAlignment = .center
-        container.addSubview(titleLabel)
-        titleLabel.snp.makeConstraints { make in
-            make.horizontalEdges.equalToSuperview().inset(16)
-            make.top.equalTo(avatar.snp.bottom).offset(11)
-        }
-        
-        scoreView.startType = .whiteBorder
-        scoreView.icSize = 30
-        scoreView.editable = true
-        scoreView.spacing = 12
-        scoreView.delegate = self
-        container.addSubview(scoreView)
-        scoreView.snp.makeConstraints { make in
-            make.centerX.equalToSuperview()
-            make.top.equalTo(titleLabel.snp.bottom).offset(17)
-        }
-        
-        confirmButton.setTitle(.init(key: "A00183"), for: .normal)
-        confirmButton.backgroundColor = .text_2
-        confirmButton.setTitleColor(.text_1, for: .normal)
-        confirmButton.titleLabel?.font = .heading_h3
-        confirmButton.layer.cornerRadius = 23.5
-        confirmButton.clipsToBounds = true
-        confirmButton.addAction(UIAction(handler: { [weak self] _ in
-            guard let self else { return }
-            dismiss()
-            handler?(scoreView.score)
-        }), for: .touchUpInside)
-        container.addSubview(confirmButton)
-        confirmButton.snp.makeConstraints { make in
-            make.horizontalEdges.equalToSuperview().inset(12)
-            make.top.equalTo(scoreView.snp.bottom).offset(25)
-            make.bottom.equalToSuperview().offset(commonBottomInset)
-            make.height.equalTo(47)
-        }
-    }
-}
-
-#if DEBUG
-
-import SwiftUI
-
-struct LNProfileStaringPanelPreview: UIViewRepresentable {
-    func makeUIView(context: Context) -> some UIView {
-        let container = UIView()
-        container.backgroundColor = .lightGray
-        
-        let view = LNProfileStaringPanel()
-        view.popup(container)
-        
-        return container
-    }
-    
-    func updateUIView(_ uiView: UIViewType, context: Context) { }
-}
-
-#Preview(body: {
-    LNProfileStaringPanelPreview()
-})
-#endif

+ 159 - 20
Lanu/Views/Profile/Profile/LNProfileUserInfoView.swift

@@ -18,8 +18,12 @@ class LNProfileUserInfoView: UIView {
     private let followCountLabel = UILabel()
     
     private let scoreLabel = UILabel()
+    private let likeIc = UIImageView()
+    private let likeContainer = UIView()
     
     private var curDetail: LNUserProfileVO?
+    private var likeInfo: LNUserLikeInfoResponse?
+    private var isOperating = false
     
     override init(frame: CGRect) {
         super.init(frame: frame)
@@ -31,12 +35,29 @@ class LNProfileUserInfoView: UIView {
         userNameLabel.text = detail.nickname
         genderView.update(detail.gender, detail.age)
         idLabel.text = "ID \(detail.userNo)"
-        followCountLabel.text = .init(key: "A00234", detail.fansCount)
         
-        scoreLabel.text = "\(detail.star)"
-        scoreLabel.superview?.isHidden = !detail.playmate
+        followCountLabel.text = .init(key: "A00234", detail.fansCount.formattedAsShortNumber())
         
         curDetail = detail
+        
+        loadLikeInfo()
+    }
+    
+    func toLikeUser(onlyLike: Bool = false) {
+        guard let curDetail, let likeInfo else { return }
+        if onlyLike, likeInfo.liked { return }
+        guard !isOperating else { return }
+        let willLike = !likeInfo.liked
+        isOperating = true
+        LNProfileManager.shared.likeUser(uid: curDetail.userNo, like: willLike)
+        { [weak self] success in
+            guard let self else { return }
+            isOperating = false
+            guard success else { return }
+            likeInfo.count += (likeInfo.liked ? -1 : 1)
+            likeInfo.liked.toggle()
+            updateLikeView(animateLike: willLike)
+        }
     }
     
     required init?(coder: NSCoder) {
@@ -44,12 +65,124 @@ class LNProfileUserInfoView: UIView {
     }
 }
 
+extension LNProfileUserInfoView {
+    private func loadLikeInfo() {
+        guard let uid = curDetail?.userNo else { return }
+        LNProfileManager.shared.getUserLikeInfo(uid: uid) { [weak self] info in
+            guard let self else { return }
+            guard let info else { return }
+            likeInfo = info
+            updateLikeView()
+        }
+    }
+    
+    private func updateLikeView(animateLike: Bool = false) {
+        guard let likeInfo else { return }
+        likeIc.superview?.isHidden = false
+        scoreLabel.text = likeInfo.count.formattedAsShortNumber()
+        scoreLabel.isHidden = likeInfo.count == 0
+        if likeInfo.liked {
+            likeIc.image = .icLikeFilled
+        } else {
+            likeIc.image = .icLikeEmpty.withTintColor(.text_1, renderingMode: .alwaysOriginal)
+        }
+        
+        if animateLike {
+            playLikeAnimation()
+        }
+    }
+    
+    private func playLikeAnimation() {
+        likeIc.layer.removeAllAnimations()
+        likeContainer.layer.removeAnimation(forKey: "likeRing")
+        likeContainer.layer.removeAnimation(forKey: "likeBurst")
+        
+        likeIc.transform = CGAffineTransform(scaleX: 0.72, y: 0.72)
+        UIView.animate(withDuration: 0.18, delay: 0, usingSpringWithDamping: 0.52, initialSpringVelocity: 0.6) {
+            self.likeIc.transform = CGAffineTransform(scaleX: 1.26, y: 1.26)
+        } completion: { _ in
+            UIView.animate(withDuration: 0.16) {
+                self.likeIc.transform = .identity
+            }
+        }
+        
+        let center = CGPoint(x: likeIc.center.x, y: likeIc.center.y)
+        let ringLayer = CAShapeLayer()
+        ringLayer.path = UIBezierPath(ovalIn: CGRect(x: center.x - 10, y: center.y - 10, width: 20, height: 20)).cgPath
+        ringLayer.fillColor = UIColor.clear.cgColor
+        ringLayer.strokeColor = UIColor.fill_6.withAlphaComponent(0.35).cgColor
+        ringLayer.lineWidth = 2
+        likeContainer.layer.addSublayer(ringLayer)
+        
+        let ringScale = CABasicAnimation(keyPath: "transform.scale")
+        ringScale.fromValue = 0.3
+        ringScale.toValue = 1.9
+        
+        let ringOpacity = CABasicAnimation(keyPath: "opacity")
+        ringOpacity.fromValue = 0.9
+        ringOpacity.toValue = 0
+        
+        let ringGroup = CAAnimationGroup()
+        ringGroup.animations = [ringScale, ringOpacity]
+        ringGroup.duration = 0.42
+        ringGroup.timingFunction = CAMediaTimingFunction(name: .easeOut)
+        ringGroup.isRemovedOnCompletion = false
+        ringGroup.fillMode = .forwards
+        
+        CATransaction.begin()
+        CATransaction.setCompletionBlock {
+            ringLayer.removeFromSuperlayer()
+        }
+        ringLayer.add(ringGroup, forKey: "likeRing")
+        CATransaction.commit()
+        
+        let burstLayer = CAReplicatorLayer()
+        burstLayer.frame = likeContainer.bounds
+        burstLayer.instanceCount = 6
+        burstLayer.instanceTransform = CATransform3DMakeRotation(.pi * 2 / 6, 0, 0, 1)
+        burstLayer.instanceDelay = 0.015
+        likeContainer.layer.addSublayer(burstLayer)
+        
+        let dotLayer = CALayer()
+        dotLayer.backgroundColor = UIColor.fill_6.cgColor
+        dotLayer.frame = CGRect(x: center.x - 1.5, y: center.y - 17, width: 3, height: 7)
+        dotLayer.cornerRadius = 1.5
+        burstLayer.addSublayer(dotLayer)
+        
+        let burstMove = CABasicAnimation(keyPath: "transform.translation.y")
+        burstMove.fromValue = 0
+        burstMove.toValue = -9
+        
+        let burstScale = CABasicAnimation(keyPath: "transform.scale")
+        burstScale.fromValue = 0.6
+        burstScale.toValue = 1
+        
+        let burstOpacity = CABasicAnimation(keyPath: "opacity")
+        burstOpacity.fromValue = 1
+        burstOpacity.toValue = 0
+        
+        let burstGroup = CAAnimationGroup()
+        burstGroup.animations = [burstMove, burstScale, burstOpacity]
+        burstGroup.duration = 0.32
+        burstGroup.timingFunction = CAMediaTimingFunction(name: .easeOut)
+        burstGroup.isRemovedOnCompletion = false
+        burstGroup.fillMode = .forwards
+        
+        CATransaction.begin()
+        CATransaction.setCompletionBlock {
+            burstLayer.removeFromSuperlayer()
+        }
+        dotLayer.add(burstGroup, forKey: "likeBurst")
+        CATransaction.commit()
+    }
+}
+
 extension LNProfileUserInfoView {
     private func setupViews() {
         let star = buildStarView()
         addSubview(star)
         star.snp.makeConstraints { make in
-            make.trailing.equalToSuperview().offset(-16)
+            make.trailing.equalToSuperview().offset(-18)
             make.bottom.equalToSuperview().offset(-10)
         }
         
@@ -117,31 +250,37 @@ extension LNProfileUserInfoView {
         followCountLabel.textColor = .text_2
         fansView.addSubview(followCountLabel)
         followCountLabel.snp.makeConstraints { make in
-            make.edges.equalToSuperview()
+            make.leading.equalToSuperview()
+            make.verticalEdges.equalToSuperview()
+            make.trailing.lessThanOrEqualToSuperview()
         }
     }
     
     private func buildStarView() -> UIView {
-        let container = UIView()
+        likeContainer.isHidden = true
+        likeContainer.onTap { [weak self] in
+            guard let self else { return }
+            toLikeUser()
+        }
         
-        let star = LNStarScoreView()
-        star.icSize = 18
-        star.score = 1.0
-        container.addSubview(star)
-        star.snp.makeConstraints { make in
-            make.leading.equalToSuperview()
-            make.verticalEdges.equalToSuperview()
+        likeIc.image = .icLikeEmpty.withTintColor(.text_1, renderingMode: .alwaysOriginal)
+        likeContainer.addSubview(likeIc)
+        likeIc.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalToSuperview()
+            make.width.height.equalTo(36)
         }
         
-        scoreLabel.font = .heading_h2
-        scoreLabel.textColor = .text_1
-        container.addSubview(scoreLabel)
+        scoreLabel.text = "0"
+        scoreLabel.font = .body_s
+        scoreLabel.textColor = .text_2
+        likeContainer.addSubview(scoreLabel)
         scoreLabel.snp.makeConstraints { make in
-            make.centerY.equalToSuperview()
-            make.leading.equalTo(star.snp.trailing).offset(4)
-            make.trailing.equalToSuperview()
+            make.centerX.equalToSuperview()
+            make.top.equalTo(likeIc.snp.bottom).offset(2)
+            make.bottom.equalToSuperview()
         }
         
-        return container
+        return likeContainer
     }
 }

+ 51 - 13
Lanu/Views/Profile/Profile/LNProfileViewController.swift

@@ -42,10 +42,11 @@ class LNProfileViewController: LNViewController {
     private let userInfoView = LNProfileUserInfoView()
     private let infoView = LNProfileInfosView()
     private let bottomMenu = LNProfileBottomMenu()
-    private let scoreView = LNProfileScoreFloatingView()
     
     private var detail: LNUserProfileVO?
     private var stayTimer: String?
+    private let likeAnimationTravel: CGFloat = 150
+    private let likeAnimationSize: CGFloat = 80
     
     init(uid: String, scene: LNCommonSceneType) {
         self.scene = scene
@@ -155,7 +156,6 @@ private extension LNProfileViewController {
         voiceBar.update(detail.voiceBar)
         infoView.update(detail)
         bottomMenu.update(detail, scene: scene)
-        scoreView.update(detail)
     }
     
     func updateProgress(_ progress: Double) {
@@ -218,6 +218,42 @@ private extension LNProfileViewController {
         panel.popup()
     }
     
+    func showLikeAnimation(at point: CGPoint) {
+        view.layoutIfNeeded()
+        cover.layoutIfNeeded()
+        
+        let imageView = UIImageView(image: .icLikeFilled)
+        let rotation = CGFloat.random(in: -.pi / 6 ... .pi / 6)
+        imageView.contentMode = .scaleAspectFit
+        imageView.alpha = 0
+        imageView.bounds = CGRect(x: 0, y: 0, width: likeAnimationSize, height: likeAnimationSize)
+        imageView.center = cover.convert(point, to: view)
+        imageView.transform = CGAffineTransform(scaleX: 0.3, y: 0.3)
+            .rotated(by: rotation)
+        
+        view.addSubview(imageView)
+        
+        UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseOut]) {
+            imageView.alpha = 1
+            imageView.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
+                .rotated(by: rotation)
+        } completion: { _ in
+            UIView.animate(withDuration: 0.18, delay: 0, options: [.curveEaseInOut]) {
+                imageView.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
+                    .rotated(by: rotation)
+            }
+        }
+        
+        UIView.animate(withDuration: 0.5, delay: 0.45, options: [.curveEaseOut]) {
+            imageView.center.y -= self.likeAnimationTravel
+            imageView.alpha = 0
+            imageView.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
+                .rotated(by: rotation)
+        } completion: { _ in
+            imageView.removeFromSuperview()
+        }
+    }
+    
     func setupViews() {
         setupNavBar()
         
@@ -273,10 +309,11 @@ private extension LNProfileViewController {
             make.bottom.equalTo(userInfoView.snp.top).offset(-8)
         }
         
+        inRoomView.isHidden = true
         scrollView.addSubview(inRoomView)
         inRoomView.snp.makeConstraints { make in
             make.trailing.equalToSuperview()
-            make.bottom.equalTo(userInfoView.snp.bottom).offset(-40)
+            make.bottom.equalTo(userInfoView.snp.bottom).offset(-65)
         }
         
         let menu = buildBottomMenu()
@@ -286,13 +323,6 @@ private extension LNProfileViewController {
             make.bottom.equalToSuperview()
         }
         
-        scoreView.isHidden = true
-        view.addSubview(scoreView)
-        scoreView.snp.makeConstraints { make in
-            make.centerY.equalToSuperview()
-            make.trailing.equalToSuperview().offset(-8)
-        }
-        
         infoView.outerScrollView = scrollView
     }
     
@@ -314,11 +344,19 @@ private extension LNProfileViewController {
         }.store(in: &cancellables)
         
         cover.contentMode = .scaleAspectFill
-        cover.onTap { [weak self] in
+        cover.onTapCombo { [weak self] event in
             guard let self else { return }
             guard let detail else { return }
-            guard !detail.avatar.isEmpty else { return }
-            view.presentImagePreview([detail.avatar], 0)
+            
+            switch event {
+            case .single:
+                guard !detail.avatar.isEmpty else { return }
+                view.presentImagePreview([detail.avatar], 0)
+            case .combo(let gesture, _):
+                let point = gesture.location(in: cover)
+                showLikeAnimation(at: point)
+                userInfoView.toLikeUser(onlyLike: true)
+            }
         }
         cover.snp.makeConstraints { make in
             make.height.equalTo(cover.snp.width).multipliedBy(363.0/375.0)

+ 3 - 4
Lanu/Views/Room/Base/Views/Gift/LNRoomGiftHeaderView.swift

@@ -285,11 +285,10 @@ private class LNRoomGiftSpecifiedUserView: UIView {
             return
         }
         
-        LNProfileManager.shared.getCachedProfileUserInfo(uid: uid, fetchIfNeeded: true)
-        { [weak self] info in
+        LNProfileManager.shared.getUserProfileDetail(uid: uid) { [weak self] info in
             guard let self else { return }
-            guard let info, info.uid == curUid else { return }
-            nameLabel.text = info.name
+            guard let info, info.userNo == curUid else { return }
+            nameLabel.text = info.nickname
             avatar.sd_setImage(with: URL(string: info.avatar))
         }
     }

+ 2 - 10
Lanu/Views/Room/OrderRoom/Seats/LNOrderRoomUserSeatView.swift

@@ -226,16 +226,8 @@ extension LNOrderRoomUserSeatView: LNRoomViewModelNotify {
         } else {
             userView.isHidden = false
             
-            if !curSeat.nickname.isEmpty {
-                userAvatar.sd_setImage(with: URL(string: curSeat.avatar))
-                nameLabel.text = curSeat.nickname
-            } else {
-                LNProfileManager.shared.getCachedProfileUserInfo(uid: curSeat.uid) { [weak self] info in
-                    guard let self, let info, info.uid == curSeat.uid else { return }
-                    userAvatar.sd_setImage(with: URL(string: info.avatar))
-                    nameLabel.text = info.name
-                }
-            }
+            userAvatar.sd_setImage(with: URL(string: curSeat.avatar))
+            nameLabel.text = curSeat.nickname
         }
         
         muteIc.isHidden = !curSeat.isMute

+ 0 - 7
Lanu/Views/Room/OrderRoom/ViewModel/LNOrderRoomViewModel.swift

@@ -45,9 +45,6 @@ extension LNOrderRoomViewModel {
             runOnMain {
                 handler(res)
             }
-            res?.list.forEach {
-                LNProfileManager.shared.updateProfileCache(uid: $0.user.userNo, name: $0.user.nickname, avatar: $0.user.avatar)
-            }
             if let err {
                 showToast(err.errorDesc)
             }
@@ -78,10 +75,6 @@ extension LNOrderRoomViewModel {
                 handler(res)
             }
             
-            res?.list.forEach {
-                LNProfileManager.shared.updateProfileCache(uid: $0.user.userNo, name: $0.user.nickname, avatar: $0.user.avatar)
-            }
-            
             if let err {
                 showToast(err.errorDesc)
             }