Просмотр исходного кода

feat: 动态评论取消多行输入

陈文艺 4 недель назад
Родитель
Сommit
1068ecbe22

+ 1 - 0
Lanu.xcodeproj/project.pbxproj

@@ -304,6 +304,7 @@
 				Views/Profile/Feed/LNFeedCommentCell.swift,
 				Views/Profile/Feed/LNFeedCommentInputPanel.swift,
 				Views/Profile/Feed/LNFeedCommentListPanel.swift,
+				Views/Profile/Feed/LNFeedCommentView.swift,
 				Views/Profile/Feed/LNFeedLikeView.swift,
 				Views/Profile/Feed/LNImageFeedDetailViewController.swift,
 				Views/Profile/Feed/LNProfileFeedItemCell.swift,

+ 34 - 0
Lanu/Common/Extension/Double+Extension.swift

@@ -45,4 +45,38 @@ extension Double {
     var timeCountDisplay: String {
         toDuration.timeCountDisplay
     }
+    
+    func formattedAsShortNumber(fractionDigits: Int = 1) -> String {
+        let absNum = abs(self)
+        
+        // 定义量级:(基数, 单位),按中文/英文区分
+        let units: [(Double, String)] = [
+            (1e12, "T"),  // 万亿
+            (1e9, "B"),   // 十亿
+            (1e6, "M"),   // 百万
+            (1e3, "K")    // 千
+        ]
+        
+        // 遍历量级,找到匹配的单位
+        for (threshold, unit) in units {
+            if absNum >= threshold {
+                let formattedNum = self / threshold
+                // 格式化小数,去掉无意义的尾缀(如 5.0 → 5)
+                let formatter = NumberFormatter()
+                formatter.maximumFractionDigits = fractionDigits
+                formatter.minimumFractionDigits = formattedNum.truncatingRemainder(dividingBy: 1) == 0 ? 0 : fractionDigits
+                
+                guard let str = formatter.string(from: NSNumber(value: formattedNum)) else {
+                    return "\(Int(formattedNum))\(unit)"
+                }
+                return "\(str)\(unit)"
+            }
+        }
+        
+        // 小于1000,直接显示原数字(无单位)
+        let formatter = NumberFormatter()
+        formatter.maximumFractionDigits = fractionDigits
+        formatter.minimumFractionDigits = 0
+        return formatter.string(from: NSNumber(value: self)) ?? "\(Int(self))"
+    }
 }

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

@@ -16,4 +16,8 @@ extension Int {
             String(format: "%02d:%02d", self / 60, self % 60)
         }
     }
+    
+    func formattedAsShortNumber(fractionDigits: Int = 1) -> String {
+        Double(self).formattedAsShortNumber(fractionDigits: fractionDigits)
+    }
 }

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

@@ -51,7 +51,7 @@ class LNPopupView: UIView {
         moveToShowupPosition()
         window?.endEditing(true)
         backgroundView.backgroundColor = .clear
-        UIView.animate(withDuration: 0.2) {
+        UIView.animate(withDuration: 0.25) {
             self.backgroundView.backgroundColor = .black.withAlphaComponent(0.4)
             self.layoutIfNeeded()
         }
@@ -60,7 +60,7 @@ class LNPopupView: UIView {
     func dismiss() {
         endEditing(true)
         moveToHiddenPosition()
-        UIView.animate(withDuration: 0.2) {
+        UIView.animate(withDuration: 0.25) {
             self.backgroundView.backgroundColor = .clear
             self.layoutIfNeeded()
         } completion: { [weak self] _ in

+ 12 - 10
Lanu/Common/Views/TextView/LNCommonTextView.swift

@@ -46,19 +46,21 @@ class LNCommonTextView: UIView {
 
 extension LNCommonTextView: UITextViewDelegate {
     func textViewDidChange(_ textView: UITextView) {
-        placeholderLabel.isHidden = !textView.text.isEmpty
+        delegate?.textViewDidChange?(textView)
+    }
+    
+    func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
+        let currentText = textView.text ?? ""
+        guard let swiftRange = Range(range, in: currentText) else {
+            return true
+        }
+        let newText = currentText.replacingCharacters(in: swiftRange, with: text)
         
-        if maxInput > 0 {
-            if textView.text.count > maxInput {
-                textView.text = String(textView.text.prefix(maxInput))
-            }
-            textLengthLabel.text = "\(textView.text.count)/\(maxInput)"
-            textLengthLabel.isHidden = false
-        } else {
-            textLengthLabel.isHidden = true
+        if maxInput > 0, newText.count > maxInput {
+            return false
         }
         
-        delegate?.textViewDidChange?(textView)
+        return delegate?.textView?(textView, shouldChangeTextIn: range, replacementText: text) ?? true
     }
 }
 

+ 23 - 0
Lanu/Localizable.xcstrings

@@ -7062,6 +7062,29 @@
         }
       }
     },
+    "A00309" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Show more"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Lebih banyak"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "展开"
+          }
+        }
+      }
+    },
     "B00001" : {
       "extractionState" : "manual",
       "localizations" : {

+ 12 - 17
Lanu/Views/Profile/Feed/LNFeedCommentCell.swift

@@ -14,7 +14,6 @@ class LNFeedCommentCell: UITableViewCell {
     private let avatar = UIImageView()
     private let nameLabel = UILabel()
     private let contentLabel = UILabel()
-    private let expandLabel = UILabel()
     private let timeLabel = UILabel()
     
     override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
@@ -46,32 +45,28 @@ extension LNFeedCommentCell {
             make.width.height.equalTo(42)
         }
         
-        nameLabel.font = .heading_h4
-        nameLabel.textColor = .text_5
-        contentView.addSubview(nameLabel)
-        nameLabel.snp.makeConstraints { make in
+        let stackView = UIStackView()
+        stackView.axis = .vertical
+        stackView.spacing = 4
+        contentView.addSubview(stackView)
+        stackView.snp.makeConstraints { make in
             make.top.equalToSuperview()
             make.trailing.equalToSuperview().offset(-16)
             make.leading.equalTo(avatar.snp.trailing).offset(10)
+            make.bottom.equalToSuperview().offset(-22)
         }
         
+        nameLabel.font = .heading_h4
+        nameLabel.textColor = .text_5
+        stackView.addArrangedSubview(nameLabel)
+        
         contentLabel.font = .body_m
         contentLabel.textColor = .text_5
         contentLabel.numberOfLines = 0
-        contentView.addSubview(contentLabel)
-        contentLabel.snp.makeConstraints { make in
-            make.leading.equalTo(avatar.snp.trailing).offset(10)
-            make.trailing.equalToSuperview().offset(-16)
-            make.top.equalTo(nameLabel.snp.bottom).offset(4)
-        }
+        stackView.addArrangedSubview(contentLabel)
         
         timeLabel.font = .body_xs
         timeLabel.textColor = .text_3
-        contentView.addSubview(timeLabel)
-        timeLabel.snp.makeConstraints { make in
-            make.leading.equalTo(avatar.snp.trailing).offset(10)
-            make.top.equalTo(contentLabel.snp.bottom).offset(4)
-            make.bottom.equalToSuperview().offset(-22)
-        }
+        stackView.addArrangedSubview(timeLabel)
     }
 }

+ 34 - 18
Lanu/Views/Profile/Feed/LNFeedCommentInputPanel.swift

@@ -11,7 +11,7 @@ import SnapKit
 
 
 class LNFeedCommentInputPanel: LNPopupView {
-    private let inputField = LNAutoSizeTextView()
+    private let textView = LNAutoSizeTextView()
     private var hideSend: Constraint?
     private let sendView = UIView()
     var handler: ((String) -> Void)?
@@ -25,7 +25,7 @@ class LNFeedCommentInputPanel: LNPopupView {
     override func popup(_ targetView: UIView? = nil) {
         super.popup(targetView)
         
-        inputField.becomeFirstResponder()
+        textView.becomeFirstResponder()
     }
     
     required init?(coder: NSCoder) {
@@ -33,8 +33,20 @@ class LNFeedCommentInputPanel: LNPopupView {
     }
 }
 
+extension LNFeedCommentInputPanel {
+    private func toSendComment() {
+        dismiss()
+        let cleanedText = (textView.text ?? "").replacingOccurrences(of: "\n", with: "").trimmingCharacters(in: .whitespacesAndNewlines)
+        guard !cleanedText.isEmpty else { return }
+        handler?(cleanedText)
+    }
+}
+
 extension LNFeedCommentInputPanel: UITextViewDelegate {
     func textViewDidChange(_ textView: UITextView) {
+        if textView.text.count > LNFeedManager.feedCommentMaxInput {
+            textView.text = String(textView.text.prefix(LNFeedManager.feedCommentMaxInput))
+        }
         let showSend = !textView.text.isEmpty
         hideSend?.update(priority: showSend ? .low : .high)
         UIView.animate(withDuration: 0.2) { [weak self] in
@@ -43,16 +55,21 @@ extension LNFeedCommentInputPanel: UITextViewDelegate {
         }
     }
     
-    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
-        let currentText = textField.text ?? ""
-                
-        guard let range = Range(range, in: currentText) else { return false }
-        let newText = currentText.replacingCharacters(in: range, with: string)
-        if newText.count < currentText.count {
+    func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
+        if text == "\n" {
+            toSendComment()
+            return false
+        }
+        let currentText = textView.text ?? ""
+        guard let swiftRange = Range(range, in: currentText) else {
             return true
         }
+        let newText = currentText.replacingCharacters(in: swiftRange, with: text)
         
-        return newText.count <= LNFeedManager.feedCommentMaxInput
+        if newText.count > LNFeedManager.feedCommentMaxInput {
+            return false
+        }
+        return true
     }
 }
 
@@ -82,12 +99,13 @@ extension LNFeedCommentInputPanel {
             hideSend = make.trailing.equalToSuperview().offset(-12).priority(.high).constraint
         }
         
-        inputField.backgroundColor = .clear
-        inputField.delegate = self
-        inputField.font = .body_m
-        inputField.textColor = .text_5
-        holder.addSubview(inputField)
-        inputField.snp.makeConstraints { make in
+        textView.backgroundColor = .clear
+        textView.delegate = self
+        textView.font = .body_m
+        textView.textColor = .text_5
+        textView.returnKeyType = .send
+        holder.addSubview(textView)
+        textView.snp.makeConstraints { make in
             make.horizontalEdges.equalToSuperview().inset(10)
             make.centerY.equalToSuperview()
             make.top.greaterThanOrEqualToSuperview()
@@ -110,9 +128,7 @@ extension LNFeedCommentInputPanel {
         sendButton.contentEdgeInsets = .init(top: 0, left: 16, bottom: 0, right: 16)
         sendButton.addAction(UIAction(handler: { [weak self] _ in
             guard let self else { return }
-            guard let text = inputField.text else { return }
-            dismiss()
-            handler?(text.trimmingCharacters(in: .whitespacesAndNewlines))
+            toSendComment()
         }), for: .touchUpInside)
         sendView.addSubview(sendButton)
         sendButton.snp.makeConstraints { make in

+ 71 - 0
Lanu/Views/Profile/Feed/LNFeedCommentView.swift

@@ -0,0 +1,71 @@
+//
+//  LNFeedCommentView.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/5.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNFeedCommentView: UIView, LNFeedManagerNotify {
+    let commentIc = UIImageView()
+    let commentLabel = UILabel()
+    
+    var uiColor: UIColor = .text_4 {
+        didSet {
+            commentLabel.textColor = uiColor
+            commentIc.image = .icProfileBio.withTintColor(uiColor, renderingMode: .alwaysOriginal)
+        }
+    }
+    
+    private var curId: String?
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        commentIc.isUserInteractionEnabled = false
+        commentIc.image = .icProfileBio.withTintColor(.text_4, renderingMode: .alwaysOriginal)
+        addSubview(commentIc)
+        commentIc.snp.makeConstraints { make in
+            make.leading.equalToSuperview()
+            make.verticalEdges.equalToSuperview()
+            make.width.height.equalTo(24)
+        }
+        
+        commentLabel.isUserInteractionEnabled = false
+        commentLabel.font = .heading_h5
+        commentLabel.textColor = .text_4
+        addSubview(commentLabel)
+        commentLabel.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.trailing.equalToSuperview()
+            make.leading.equalTo(commentIc.snp.trailing).offset(2)
+        }
+        
+        LNEventDeliver.addObserver(self)
+    }
+    
+    func update(id: String, count: Int) {
+        curId = id
+        
+        _updateUI(count: count)
+    }
+    
+    func onFeedCommentCountChanged(id: String, count: Int) {
+        guard id == curId else { return }
+        
+        _updateUI(count: count)
+    }
+    
+    private func _updateUI(count: Int) {
+        commentLabel.text = count == 0 ? .init(key: "A00302") : count.formattedAsShortNumber()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+

+ 1 - 1
Lanu/Views/Profile/Feed/LNFeedLikeView.swift

@@ -74,7 +74,7 @@ class LNFeedLikeView: UIView, LNFeedManagerNotify {
     
     private func _updateUI() {
         likeIc.image = isLiked ? .icLikeFilled : .icLikeEmpty.withTintColor(uiColor, renderingMode: .alwaysOriginal)
-        likeLabel.text = likeCount == 0 ? .init(key: "A00301") : "\(likeCount)"
+        likeLabel.text = likeCount == 0 ? .init(key: "A00301") : "\(likeCount.formattedAsShortNumber())"
     }
     
     required init?(coder: NSCoder) {

+ 5 - 24
Lanu/Views/Profile/Feed/LNImageFeedDetailViewController.swift

@@ -27,7 +27,7 @@ class LNImageFeedDetailViewController: LNViewController {
     private let tableView = UITableView(frame: .zero, style: .grouped)
     
     private let likeView = LNFeedLikeView()
-    private let commentLabel = UILabel()
+    private let commentView = LNFeedCommentView()
     
     private var curDetail: LNFeedDetailVO?
     private var nextTag: String?
@@ -53,7 +53,7 @@ class LNImageFeedDetailViewController: LNViewController {
             avatar.sd_setImage(with: URL(string: detail.avatar))
             nameLabel.text = detail.nickname
             likeView.update(id: id, liked: detail.liked, count: detail.likeCount)
-            commentLabel.text = detail.commentCount == 0 ? .init(key: "A00302") : "\(detail.commentCount)"
+            commentView.update(id: id, count: detail.commentCount)
         }
         nextTag = nil
         loadComment(id: id)
@@ -104,7 +104,6 @@ extension LNImageFeedDetailViewController {
                 tableView.reloadSections(.init(integer: 1), with: .automatic)
                 
                 curDetail.commentCount += 1
-                commentLabel.text = "\(curDetail.commentCount)"
                 LNFeedManager.shared.notifyFeedCommentChanged(id: curDetail.id, count: curDetail.commentCount)
             }
         }
@@ -295,31 +294,13 @@ extension LNImageFeedDetailViewController {
     }
     
     private func buildComment() -> UIView {
-        let container = UIView()
-        container.onTap { [weak self] in
+        commentView.uiColor = .text_4
+        commentView.onTap { [weak self] in
             guard let self else { return }
             toComment()
         }
         
-        let commentIc = UIImageView()
-        commentIc.image = .icProfileBio.withTintColor(.text_4, renderingMode: .alwaysOriginal)
-        container.addSubview(commentIc)
-        commentIc.snp.makeConstraints { make in
-            make.leading.equalToSuperview()
-            make.verticalEdges.equalToSuperview()
-            make.width.height.equalTo(24)
-        }
-        
-        commentLabel.font = .heading_h5
-        commentLabel.textColor = .text_4
-        container.addSubview(commentLabel)
-        commentLabel.snp.makeConstraints { make in
-            make.centerY.equalToSuperview()
-            make.trailing.equalToSuperview()
-            make.leading.equalTo(commentIc.snp.trailing).offset(2)
-        }
-        
-        return container
+        return commentView
     }
     
     private func buildLike() -> UIView {

+ 7 - 31
Lanu/Views/Profile/Feed/LNProfileFeedItemCell.swift

@@ -28,8 +28,7 @@ class LNProfileFeedItemCell: UITableViewCell {
     private let inputField = UITextField()
     
     private let likeView = LNFeedLikeView()
-    
-    private let commentLabel = UILabel()
+    private let commentView = LNFeedCommentView()
     
     private var curItem: LNFeedItemVO?
     
@@ -50,7 +49,7 @@ class LNProfileFeedItemCell: UITableViewCell {
         timeLabel.text = TimeInterval(item.createdAt / 1_000).tencentIMTimeDesc
         contentLabel.text = item.textContent
         likeView.update(id: item.id, liked: item.liked, count: item.likeCount)
-        commentLabel.text = item.commentCount == 0 ? .init(key: "A00302") : "\(item.commentCount)"
+        commentView.update(id: item.id, count: item.commentCount)
         videoView.stop()
         
         if item.medias.isEmpty { // 无媒体
@@ -179,15 +178,6 @@ extension LNProfileFeedItemCell {
     }
 }
 
-extension LNProfileFeedItemCell: LNFeedManagerNotify {
-    func onFeedCommentCountChanged(id: String, count: Int) {
-        guard let curItem else { return }
-        guard id == curItem.id else { return }
-        curItem.commentCount = count
-        commentLabel.text = "\(curItem.commentCount)"
-    }
-}
-
 extension LNProfileFeedItemCell: LNVideoPlayerViewDelegate {
     func onVideoProgressChanged(view: LNVideoPlayerView, cur: Float64, total: Float64) {
         let remain = total - cur
@@ -443,27 +433,13 @@ extension LNProfileFeedItemCell {
     }
     
     private func buildComment() -> UIView {
-        let container = UIView()
-        
-        let commentIc = UIImageView()
-        commentIc.image = .icProfileBio.withTintColor(.text_4, renderingMode: .alwaysOriginal)
-        container.addSubview(commentIc)
-        commentIc.snp.makeConstraints { make in
-            make.leading.equalToSuperview()
-            make.verticalEdges.equalToSuperview()
-            make.width.height.equalTo(24)
-        }
-        
-        commentLabel.font = .heading_h5
-        commentLabel.textColor = .text_4
-        container.addSubview(commentLabel)
-        commentLabel.snp.makeConstraints { make in
-            make.centerY.equalToSuperview()
-            make.trailing.equalToSuperview()
-            make.leading.equalTo(commentIc.snp.trailing).offset(2)
+        commentView.uiColor = .text_4
+        commentView.onTap { [weak self] in
+            guard let self else { return }
+            toComment()
         }
         
-        return container
+        return commentView
     }
     
     private func buildLike() -> UIView {

+ 7 - 35
Lanu/Views/Profile/Feed/LNVideoFeedDetailViewController.swift

@@ -31,8 +31,7 @@ class LNVideoFeedDetailViewController: LNViewController {
     private let inputField = UITextField()
     
     private let likeView = LNFeedLikeView()
-    
-    private let commentLabel = UILabel()
+    private let commentView = LNFeedCommentView()
     
     private let videoPlayer = LNVideoPlayerView()
     
@@ -58,7 +57,7 @@ class LNVideoFeedDetailViewController: LNViewController {
             avatar.sd_setImage(with: URL(string: detail.avatar))
             nameLabel.text = detail.nickname
             likeView.update(id: detail.id, liked: detail.liked, count: detail.likeCount)
-            commentLabel.text = detail.commentCount == 0 ? .init(key: "A00302") : "\(detail.commentCount)"
+            commentView.update(id: detail.id, count: detail.commentCount)
             contentLabel.text = detail.textContent
             timeLabel.text = TimeInterval(detail.createdAt / 1_000).tencentIMTimeDesc
             videoPlayer.loadVideo(video.url, coverUrl: video.videoCover)
@@ -77,7 +76,7 @@ extension LNVideoFeedDetailViewController {
                 guard let self else { return }
                 guard success else { return }
                 curDetail.commentCount += 1
-                commentLabel.text = "\(curDetail.commentCount)"
+                LNFeedManager.shared.notifyFeedCommentChanged(id: curDetail.id, count: curDetail.commentCount)
             }
         }
         panel.popup()
@@ -97,15 +96,6 @@ extension LNVideoFeedDetailViewController {
     }
 }
 
-extension LNVideoFeedDetailViewController: LNFeedManagerNotify {
-    func onFeedCommentCountChanged(id: String, count: Int) {
-        guard let curDetail else { return }
-        guard curDetail.id == id else { return }
-        
-        commentLabel.text = "\(count)"
-    }
-}
-
 extension LNVideoFeedDetailViewController: LNVideoPlayerViewDelegate {
     func onVideoDidLoad(view: LNVideoPlayerView) {
         seekBar.isUserInteractionEnabled = true
@@ -250,8 +240,8 @@ extension LNVideoFeedDetailViewController {
     }
     
     private func buildComment() -> UIView {
-        let container = UIView()
-        container.onTap { [weak self] in
+        commentView.uiColor = .text_1
+        commentView.onTap { [weak self] in
             guard let self else { return }
             guard let curDetail else { return }
             if curDetail.commentCount == 0 {
@@ -263,25 +253,7 @@ extension LNVideoFeedDetailViewController {
             }
         }
         
-        let commentIc = UIImageView()
-        commentIc.image = .icProfileBio.withTintColor(.text_1)
-        container.addSubview(commentIc)
-        commentIc.snp.makeConstraints { make in
-            make.leading.equalToSuperview()
-            make.verticalEdges.equalToSuperview()
-            make.width.height.equalTo(24)
-        }
-        
-        commentLabel.font = .heading_h5
-        commentLabel.textColor = .text_1
-        container.addSubview(commentLabel)
-        commentLabel.snp.makeConstraints { make in
-            make.centerY.equalToSuperview()
-            make.trailing.equalToSuperview()
-            make.leading.equalTo(commentIc.snp.trailing).offset(2)
-        }
-        
-        return container
+        return commentView
     }
     
     private func buildLike() -> UIView {
@@ -305,7 +277,7 @@ extension LNVideoFeedDetailViewController {
                     guard let self else { return }
                     guard success else { return }
                     curDetail.commentCount += 1
-                    commentLabel.text = "\(curDetail.commentCount)"
+                    LNFeedManager.shared.notifyFeedCommentChanged(id: curDetail.id, count: curDetail.commentCount)
                 }
             }
             panel.popup()

+ 1 - 0
Lanu/Views/Profile/Relation/LNUserRelationViewController.swift

@@ -49,6 +49,7 @@ class LNUserRelationViewController: LNViewController {
     }
     
     func locatedTo(_ tab: LNUserRelationTabType) {
+        view.layoutIfNeeded()
         curTabType = tab
     }
 }