diff --git a/Visual_Novel_iOS/Src/Components/UI/Header/TopHeaderManager.swift b/Visual_Novel_iOS/Src/Components/UI/Header/TopHeaderManager.swift index 42e6b74..8864a20 100644 --- a/Visual_Novel_iOS/Src/Components/UI/Header/TopHeaderManager.swift +++ b/Visual_Novel_iOS/Src/Components/UI/Header/TopHeaderManager.swift @@ -18,42 +18,58 @@ final class TopHeaderManager { return view }() - private var hostingView: UIView? - private var topConstraint: Constraint? - private var heightConstraint: Constraint? - - private init() {} - - var jumpPublisher: AnyPublisher { - headerView.jumpPublisher - } - - func attachIfNeeded(to viewController: UIViewController) { - guard hostingView !== viewController.view else { return } - detach() - let container = viewController.view! - container.addSubview(headerView) - headerView.snp.makeConstraints { make in - topConstraint = make.top.equalToSuperview().constraint - make.leading.trailing.equalToSuperview() - heightConstraint = make.height.equalTo(UIDevice().navHeight).constraint + // 为传入的视图创建并附加一个新的 CLTopHeaderView。 + // 若视图已拥有 CLTopHeaderView,则直接返回已有实例,避免重复添加。 + @discardableResult + func buildAndAttach(to superView: UIView) -> CLTopHeaderView { + if let existing = superView.subviews.first(where: { $0 is CLTopHeaderView }) as? CLTopHeaderView { + return existing } - hostingView = container - headerView.isHidden = false + let header = CLTopHeaderView() + superView.addSubview(header) + header.snp.makeConstraints { make in + make.left.right.top.equalToSuperview() + make.height.equalTo(UIDevice().navHeight) + } + return header } - func show() { - headerView.isHidden = false - } - - func hide() { - headerView.isHidden = true - } - - func detach() { - headerView.removeFromSuperview() - hostingView = nil - } +// private var hostingView: UIView? +// private var topConstraint: Constraint? +// private var heightConstraint: Constraint? +// +// private init() {} +// +// var jumpPublisher: AnyPublisher { +// headerView.jumpPublisher +// } +// +// func attachIfNeeded(to viewController: UIViewController) { +// guard hostingView !== viewController.view else { return } +// detach() +// let container = viewController.view! +// container.addSubview(headerView) +// headerView.snp.makeConstraints { make in +// topConstraint = make.top.equalToSuperview().constraint +// make.leading.trailing.equalToSuperview() +// heightConstraint = make.height.equalTo(UIDevice().navHeight).constraint +// } +// hostingView = container +// headerView.isHidden = false +// } +// +// func show() { +// headerView.isHidden = false +// } +// +// func hide() { +// headerView.isHidden = true +// } +// +// func detach() { +// headerView.removeFromSuperview() +// hostingView = nil +// } } diff --git a/Visual_Novel_iOS/Src/Modules/Chat/Setting/Model/ChatRoleViewModel.swift b/Visual_Novel_iOS/Src/Modules/Chat/Setting/Model/ChatRoleViewModel.swift index 71fc0b5..c9f6bad 100644 --- a/Visual_Novel_iOS/Src/Modules/Chat/Setting/Model/ChatRoleViewModel.swift +++ b/Visual_Novel_iOS/Src/Modules/Chat/Setting/Model/ChatRoleViewModel.swift @@ -27,17 +27,27 @@ struct RoleListModel: Codable { var rows: [RoleItem] = [] } +enum RoleSourceType: Int, Codable { + case novel + case video + case other +} + struct RoleItem: Codable { - var id: String = "" - var name: String = "" - var coverImage: String = "" + var id: String? + var name: String? + var coverImage: String? + // 来源封面 + var sourceCoverImage: String? + var sourceName: String? // 角色头像 - var headPortrait: String = "" + var headPortrait: String? var score: Double? - var description: String = "" - var updateTime: String = "" - var sourceId: String = "" - var sourceType: Int = 0 + var description: String? + var updateTime: String? + var sourceId: String? + // 0:小说;1:漫剧;2:用户自定义 + var sourceType: RoleSourceType? var commonCount: Int? var tags: [tagItem] = [] } diff --git a/Visual_Novel_iOS/Src/Modules/Friend/FriendsRootHomeController.swift b/Visual_Novel_iOS/Src/Modules/Friend/FriendsRootHomeController.swift index eec0576..ae4e9a8 100644 --- a/Visual_Novel_iOS/Src/Modules/Friend/FriendsRootHomeController.swift +++ b/Visual_Novel_iOS/Src/Modules/Friend/FriendsRootHomeController.swift @@ -28,6 +28,9 @@ class FriendsRootHomeController: CLTabRootController { // view.showEmpty(text: "Friends Coming soon") + // 每个 Tab 子控制器拥有自己的 Header,布局一致 + _ = TopHeaderManager.shared.buildAndAttach(to: view) + setupViews() setupDats() setupEvents() diff --git a/Visual_Novel_iOS/Src/Modules/Home/HomePageRootController.swift b/Visual_Novel_iOS/Src/Modules/Home/HomePageRootController.swift index 230c8f2..475d8eb 100644 --- a/Visual_Novel_iOS/Src/Modules/Home/HomePageRootController.swift +++ b/Visual_Novel_iOS/Src/Modules/Home/HomePageRootController.swift @@ -23,6 +23,8 @@ class HomePageRootController: CLTabRootController { //container.viewModel = viewModel container.setupViewModel(vm: viewModel) + // 每个 Tab 子控制器拥有自己的 Header,布局一致 + _ = TopHeaderManager.shared.buildAndAttach(to: view) // Do any additional setup after loading the view. // view.showEmpty(text: "首页 Coming soon") diff --git a/Visual_Novel_iOS/Src/Modules/Home/View/HomePageRootView.swift b/Visual_Novel_iOS/Src/Modules/Home/View/HomePageRootView.swift index 1adf78f..b3a9cc5 100644 --- a/Visual_Novel_iOS/Src/Modules/Home/View/HomePageRootView.swift +++ b/Visual_Novel_iOS/Src/Modules/Home/View/HomePageRootView.swift @@ -34,7 +34,8 @@ class HomePageRootView: UIView { } private func setupViews() { - +// TopHeaderManager.shared.addFromSuperView(self) + emptyView = { let v = UIView() v.showEmpty(text: "暂无卡片") diff --git a/Visual_Novel_iOS/Src/Modules/Me/MeRootPageController.swift b/Visual_Novel_iOS/Src/Modules/Me/MeRootPageController.swift index fcbccb7..80b0b6d 100644 --- a/Visual_Novel_iOS/Src/Modules/Me/MeRootPageController.swift +++ b/Visual_Novel_iOS/Src/Modules/Me/MeRootPageController.swift @@ -17,6 +17,9 @@ class MeRootPageController: CLTabRootController { override func viewDidLoad() { super.viewDidLoad() + // 每个 Tab 子控制器拥有自己的 Header,布局一致 + _ = TopHeaderManager.shared.buildAndAttach(to: view) + setupViews() setupDats() setupEvents() diff --git a/Visual_Novel_iOS/Src/Modules/Roles/View/CLRoleCollectionCell.swift b/Visual_Novel_iOS/Src/Modules/Roles/View/CLRoleCollectionCell.swift index f883f33..6c33f33 100644 --- a/Visual_Novel_iOS/Src/Modules/Roles/View/CLRoleCollectionCell.swift +++ b/Visual_Novel_iOS/Src/Modules/Roles/View/CLRoleCollectionCell.swift @@ -20,7 +20,6 @@ class CLRoleCollectionCell: UICollectionViewCell { lazy var bookImgView: UIImageView = { let imgView = UIImageView() - imgView.backgroundColor = .blue imgView.cornerRadius = 7 return imgView }() @@ -135,12 +134,13 @@ class CLRoleCollectionCell: UICollectionViewCell { func setupData(item: RoleItem) { descLab.text = item.description nameLab.text = item.name - coverImgView.sd_setImage(with: URL(string: item.coverImage), placeholderImage: nil) - tagLab.text = item.tags.compactMap { $0.name }.joined(separator: "/") - // 0:小说;1:漫剧;2:用户自定义 - self.bookBgImgView.isHidden = item.sourceType != 0 - self.playImgView.isHidden = item.sourceType != 1 -// coverImgView. + coverImgView.sd_setImage(with: URL(string: item.coverImage ?? ""), placeholderImage: nil) + bookImgView.sd_setImage(with: URL(string: item.sourceCoverImage ?? ""), placeholderImage: nil) + tagLab.text = item.tags.compactMap { "#\($0.name)" }.joined(separator: "/") + sourceLab.text = item.score?.truncateString(places: 1) + self.bookBgImgView.isHidden = item.sourceType != .novel + self.playImgView.isHidden = item.sourceType != .video + self.remindLab.text = item.sourceName } // MARK: subviews diff --git a/Visual_Novel_iOS/Src/Modules/Roles/View/RolesRootPageView.swift b/Visual_Novel_iOS/Src/Modules/Roles/View/RolesRootPageView.swift index b51b3a8..b42946b 100644 --- a/Visual_Novel_iOS/Src/Modules/Roles/View/RolesRootPageView.swift +++ b/Visual_Novel_iOS/Src/Modules/Roles/View/RolesRootPageView.swift @@ -13,7 +13,7 @@ import Combine class RolesRootPageView: CLContainer { let itemWidth: CGFloat = (UIScreen.width - 30.0) / 2.0 - var jumpPublisher: AnyPublisher { topView.jumpPublisher } + var jumpPublisher: AnyPublisher { TopHeaderManager.shared.buildAndAttach(to: self).jumpPublisher } // private lazy var pagingView = JXPagingListRefreshView(delegate: self) // lazy var headerView: @@ -25,33 +25,10 @@ class RolesRootPageView: CLContainer { var data: [RoleItem] = [] var hasMoreData: Bool = true // 是否还有更多数据 - let remind: [String] = [ - "[The Lsat Oracle of Kael]", - "[The Lsat Oracle of Kael]", - "[The Lsat Oracle of Kael]", - "[The Lsat Oracle of Kael]", - "[The Lsat Oracle of Kael]", - "[The Lsat Oracle of Kael]", - "[The Lsat Oracle of Kael]", - "[The Lsat Oracle of Kael]", - "[The Lsat Oracle of Kael]", - "[The Lsat Oracle of Kael]", - "[The Lsat Oracle of Kael]", - "[The Lsat Oracle of Kael]", - "[The Lsat Oracle of Kael]", - "[The Lsat Oracle of Kael]", - "[The Lsat Oracle of Kael]", - "[The Lsat Oracle of Kael]", - "[The Lsat Oracle of Kael]", - "[The Lsat Oracle of Kael]", - "[The Lsat Oracle of Kael]", - "[The Lsat Oracle of Kael]" - ] - - lazy var topView: CLTopHeaderView = { - let view = CLTopHeaderView() - return view - }() +// lazy var topView: CLTopHeaderView = { +// let view = CLTopHeaderView() +// return view +// }() lazy var tagsView: CLRoleTagsView = { let tagsView = CLRoleTagsView() @@ -85,19 +62,13 @@ class RolesRootPageView: CLContainer { } private func setupViews() { - addSubview(self.topView) - // addSubview(tagsChooseView) +// TopHeaderManager.shared.addFromSuperView(self) addSubview(tagsView) addSubview(collectionView) - topView.snp.makeConstraints { make in - make.left.right.top.equalToSuperview() - make.height.equalTo(UIDevice().navHeight) - } - tagsView.snp.makeConstraints { make in make.right.left.equalToSuperview() - make.top.equalTo(topView.snp.bottom).offset(0) + make.top.equalTo(TopHeaderManager.shared.buildAndAttach(to: self).snp.bottom).offset(0) } collectionView.snp.makeConstraints { make in @@ -206,7 +177,7 @@ extension RolesRootPageView: UICollectionViewDelegate, UICollectionViewDataSourc let maxHeight = lineHeight * CGFloat(maxLines) // 真实文本高度(不会超过 maxHeight) - let textSize = model.name.boundingRect( + let textSize = (model.name ?? "").boundingRect( with: CGSize(width: itemWidth - 20, height: maxHeight), // 高度封顶 options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [.font: font], @@ -216,8 +187,7 @@ extension RolesRootPageView: UICollectionViewDelegate, UICollectionViewDataSourc let textH = min(textSize.height, maxHeight) // 防止 remind 数组越界,使用模运算或默认值 - let remindIndex = indexPath.item % remind.count - let remindText = remind[remindIndex] + let remindText = model.sourceName ?? "" let remindH = remindText.boundingRect( with: CGSize(width: itemWidth - 20.0, height: .greatestFiniteMagnitude), options: .usesLineFragmentOrigin, diff --git a/Visual_Novel_iOS/Src/Utils/Extensions/DoubleExt.swift b/Visual_Novel_iOS/Src/Utils/Extensions/DoubleExt.swift new file mode 100644 index 0000000..858be70 --- /dev/null +++ b/Visual_Novel_iOS/Src/Utils/Extensions/DoubleExt.swift @@ -0,0 +1,22 @@ +// +// DoubleExt.swift +// Visual_Novel_iOS +// +// Created by mh on 2025/11/11. +// + +import Foundation + +extension Double { + /// 截断保留小数位数(向下取整) + func truncate(places: Int) -> Double { + let divisor = pow(10.0, Double(places)) + return floor(self * divisor) / divisor + } + + /// 截断保留小数位数并格式化为字符串 + func truncateString(places: Int) -> String { + let truncated = truncate(places: places) + return String(format: "%.\(places)f", truncated) + } +}