diff --git a/Visual_Novel_iOS/Assets.xcassets/Base/header_check_icon.imageset/Contents.json b/Visual_Novel_iOS/Assets.xcassets/Base/header_check_icon.imageset/Contents.json new file mode 100644 index 0000000..49a3220 --- /dev/null +++ b/Visual_Novel_iOS/Assets.xcassets/Base/header_check_icon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "header_check_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "header_check_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Visual_Novel_iOS/Assets.xcassets/Base/header_check_icon.imageset/header_check_icon@2x.png b/Visual_Novel_iOS/Assets.xcassets/Base/header_check_icon.imageset/header_check_icon@2x.png new file mode 100644 index 0000000..2406883 Binary files /dev/null and b/Visual_Novel_iOS/Assets.xcassets/Base/header_check_icon.imageset/header_check_icon@2x.png differ diff --git a/Visual_Novel_iOS/Assets.xcassets/Base/header_check_icon.imageset/header_check_icon@3x.png b/Visual_Novel_iOS/Assets.xcassets/Base/header_check_icon.imageset/header_check_icon@3x.png new file mode 100644 index 0000000..9662fe5 Binary files /dev/null and b/Visual_Novel_iOS/Assets.xcassets/Base/header_check_icon.imageset/header_check_icon@3x.png differ diff --git a/Visual_Novel_iOS/Assets.xcassets/Base/header_discord_icon.imageset/Contents.json b/Visual_Novel_iOS/Assets.xcassets/Base/header_discord_icon.imageset/Contents.json new file mode 100644 index 0000000..6e37dda --- /dev/null +++ b/Visual_Novel_iOS/Assets.xcassets/Base/header_discord_icon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "header_discord_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "header_discord_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Visual_Novel_iOS/Assets.xcassets/Base/header_discord_icon.imageset/header_discord_icon@2x.png b/Visual_Novel_iOS/Assets.xcassets/Base/header_discord_icon.imageset/header_discord_icon@2x.png new file mode 100644 index 0000000..b404fe4 Binary files /dev/null and b/Visual_Novel_iOS/Assets.xcassets/Base/header_discord_icon.imageset/header_discord_icon@2x.png differ diff --git a/Visual_Novel_iOS/Assets.xcassets/Base/header_discord_icon.imageset/header_discord_icon@3x.png b/Visual_Novel_iOS/Assets.xcassets/Base/header_discord_icon.imageset/header_discord_icon@3x.png new file mode 100644 index 0000000..211c1c3 Binary files /dev/null and b/Visual_Novel_iOS/Assets.xcassets/Base/header_discord_icon.imageset/header_discord_icon@3x.png differ diff --git a/Visual_Novel_iOS/Assets.xcassets/Base/header_money_icon.imageset/Contents.json b/Visual_Novel_iOS/Assets.xcassets/Base/header_money_icon.imageset/Contents.json new file mode 100644 index 0000000..adf53fc --- /dev/null +++ b/Visual_Novel_iOS/Assets.xcassets/Base/header_money_icon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "header_money_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "header_money_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Visual_Novel_iOS/Assets.xcassets/Base/header_money_icon.imageset/header_money_icon@2x.png b/Visual_Novel_iOS/Assets.xcassets/Base/header_money_icon.imageset/header_money_icon@2x.png new file mode 100644 index 0000000..eca6c37 Binary files /dev/null and b/Visual_Novel_iOS/Assets.xcassets/Base/header_money_icon.imageset/header_money_icon@2x.png differ diff --git a/Visual_Novel_iOS/Assets.xcassets/Base/header_money_icon.imageset/header_money_icon@3x.png b/Visual_Novel_iOS/Assets.xcassets/Base/header_money_icon.imageset/header_money_icon@3x.png new file mode 100644 index 0000000..ee8784a Binary files /dev/null and b/Visual_Novel_iOS/Assets.xcassets/Base/header_money_icon.imageset/header_money_icon@3x.png differ diff --git a/Visual_Novel_iOS/Assets.xcassets/Base/header_not_subscriber_icon.imageset/Contents.json b/Visual_Novel_iOS/Assets.xcassets/Base/header_not_subscriber_icon.imageset/Contents.json new file mode 100644 index 0000000..da2c3bf --- /dev/null +++ b/Visual_Novel_iOS/Assets.xcassets/Base/header_not_subscriber_icon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "header_not_subscriber_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "header_not_subscriber_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Visual_Novel_iOS/Assets.xcassets/Base/header_not_subscriber_icon.imageset/header_not_subscriber_icon@2x.png b/Visual_Novel_iOS/Assets.xcassets/Base/header_not_subscriber_icon.imageset/header_not_subscriber_icon@2x.png new file mode 100644 index 0000000..77ae68a Binary files /dev/null and b/Visual_Novel_iOS/Assets.xcassets/Base/header_not_subscriber_icon.imageset/header_not_subscriber_icon@2x.png differ diff --git a/Visual_Novel_iOS/Assets.xcassets/Base/header_not_subscriber_icon.imageset/header_not_subscriber_icon@3x.png b/Visual_Novel_iOS/Assets.xcassets/Base/header_not_subscriber_icon.imageset/header_not_subscriber_icon@3x.png new file mode 100644 index 0000000..fbe5793 Binary files /dev/null and b/Visual_Novel_iOS/Assets.xcassets/Base/header_not_subscriber_icon.imageset/header_not_subscriber_icon@3x.png differ diff --git a/Visual_Novel_iOS/Assets.xcassets/Base/header_search_icon.imageset/Contents.json b/Visual_Novel_iOS/Assets.xcassets/Base/header_search_icon.imageset/Contents.json new file mode 100644 index 0000000..0a82be4 --- /dev/null +++ b/Visual_Novel_iOS/Assets.xcassets/Base/header_search_icon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "header_search_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "header_search_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Visual_Novel_iOS/Assets.xcassets/Base/header_search_icon.imageset/header_search_icon@2x.png b/Visual_Novel_iOS/Assets.xcassets/Base/header_search_icon.imageset/header_search_icon@2x.png new file mode 100644 index 0000000..3e37da0 Binary files /dev/null and b/Visual_Novel_iOS/Assets.xcassets/Base/header_search_icon.imageset/header_search_icon@2x.png differ diff --git a/Visual_Novel_iOS/Assets.xcassets/Base/header_search_icon.imageset/header_search_icon@3x.png b/Visual_Novel_iOS/Assets.xcassets/Base/header_search_icon.imageset/header_search_icon@3x.png new file mode 100644 index 0000000..dd188e7 Binary files /dev/null and b/Visual_Novel_iOS/Assets.xcassets/Base/header_search_icon.imageset/header_search_icon@3x.png differ diff --git a/Visual_Novel_iOS/Assets.xcassets/Base/header_subscriber_icon.imageset/Contents.json b/Visual_Novel_iOS/Assets.xcassets/Base/header_subscriber_icon.imageset/Contents.json new file mode 100644 index 0000000..8bc6b9d --- /dev/null +++ b/Visual_Novel_iOS/Assets.xcassets/Base/header_subscriber_icon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "header_subscriber_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "header_subscriber_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Visual_Novel_iOS/Assets.xcassets/Base/header_subscriber_icon.imageset/header_subscriber_icon@2x.png b/Visual_Novel_iOS/Assets.xcassets/Base/header_subscriber_icon.imageset/header_subscriber_icon@2x.png new file mode 100644 index 0000000..c022c78 Binary files /dev/null and b/Visual_Novel_iOS/Assets.xcassets/Base/header_subscriber_icon.imageset/header_subscriber_icon@2x.png differ diff --git a/Visual_Novel_iOS/Assets.xcassets/Base/header_subscriber_icon.imageset/header_subscriber_icon@3x.png b/Visual_Novel_iOS/Assets.xcassets/Base/header_subscriber_icon.imageset/header_subscriber_icon@3x.png new file mode 100644 index 0000000..2b3d04b Binary files /dev/null and b/Visual_Novel_iOS/Assets.xcassets/Base/header_subscriber_icon.imageset/header_subscriber_icon@3x.png differ diff --git a/Visual_Novel_iOS/Assets.xcassets/Role/role_book.imageset/Contents.json b/Visual_Novel_iOS/Assets.xcassets/Role/role_book.imageset/Contents.json new file mode 100644 index 0000000..ec9c882 --- /dev/null +++ b/Visual_Novel_iOS/Assets.xcassets/Role/role_book.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "role_book@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "role_book@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Visual_Novel_iOS/Assets.xcassets/Role/role_book.imageset/role_book@2x.png b/Visual_Novel_iOS/Assets.xcassets/Role/role_book.imageset/role_book@2x.png new file mode 100644 index 0000000..6c4d20f Binary files /dev/null and b/Visual_Novel_iOS/Assets.xcassets/Role/role_book.imageset/role_book@2x.png differ diff --git a/Visual_Novel_iOS/Assets.xcassets/Role/role_book.imageset/role_book@3x.png b/Visual_Novel_iOS/Assets.xcassets/Role/role_book.imageset/role_book@3x.png new file mode 100644 index 0000000..d86c573 Binary files /dev/null and b/Visual_Novel_iOS/Assets.xcassets/Role/role_book.imageset/role_book@3x.png differ diff --git a/Visual_Novel_iOS/Assets.xcassets/Role/role_bottom_bg.imageset/Contents.json b/Visual_Novel_iOS/Assets.xcassets/Role/role_bottom_bg.imageset/Contents.json new file mode 100644 index 0000000..eed0941 --- /dev/null +++ b/Visual_Novel_iOS/Assets.xcassets/Role/role_bottom_bg.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "role_bottom_bg@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "role_bottom_bg@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Visual_Novel_iOS/Assets.xcassets/Role/role_bottom_bg.imageset/role_bottom_bg@2x.png b/Visual_Novel_iOS/Assets.xcassets/Role/role_bottom_bg.imageset/role_bottom_bg@2x.png new file mode 100644 index 0000000..995a2ad Binary files /dev/null and b/Visual_Novel_iOS/Assets.xcassets/Role/role_bottom_bg.imageset/role_bottom_bg@2x.png differ diff --git a/Visual_Novel_iOS/Assets.xcassets/Role/role_bottom_bg.imageset/role_bottom_bg@3x.png b/Visual_Novel_iOS/Assets.xcassets/Role/role_bottom_bg.imageset/role_bottom_bg@3x.png new file mode 100644 index 0000000..ad87af9 Binary files /dev/null and b/Visual_Novel_iOS/Assets.xcassets/Role/role_bottom_bg.imageset/role_bottom_bg@3x.png differ diff --git a/Visual_Novel_iOS/Assets.xcassets/Role/role_close_icon.imageset/Contents.json b/Visual_Novel_iOS/Assets.xcassets/Role/role_close_icon.imageset/Contents.json new file mode 100644 index 0000000..8f5eb15 --- /dev/null +++ b/Visual_Novel_iOS/Assets.xcassets/Role/role_close_icon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "role_close_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "role_close_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Visual_Novel_iOS/Assets.xcassets/Role/role_close_icon.imageset/role_close_icon@2x.png b/Visual_Novel_iOS/Assets.xcassets/Role/role_close_icon.imageset/role_close_icon@2x.png new file mode 100644 index 0000000..61a709d Binary files /dev/null and b/Visual_Novel_iOS/Assets.xcassets/Role/role_close_icon.imageset/role_close_icon@2x.png differ diff --git a/Visual_Novel_iOS/Assets.xcassets/Role/role_close_icon.imageset/role_close_icon@3x.png b/Visual_Novel_iOS/Assets.xcassets/Role/role_close_icon.imageset/role_close_icon@3x.png new file mode 100644 index 0000000..267e1c7 Binary files /dev/null and b/Visual_Novel_iOS/Assets.xcassets/Role/role_close_icon.imageset/role_close_icon@3x.png differ diff --git a/Visual_Novel_iOS/Assets.xcassets/Role/role_from.imageset/Contents.json b/Visual_Novel_iOS/Assets.xcassets/Role/role_from.imageset/Contents.json new file mode 100644 index 0000000..8783e33 --- /dev/null +++ b/Visual_Novel_iOS/Assets.xcassets/Role/role_from.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "role_from@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "role_from@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Visual_Novel_iOS/Assets.xcassets/Role/role_from.imageset/role_from@2x.png b/Visual_Novel_iOS/Assets.xcassets/Role/role_from.imageset/role_from@2x.png new file mode 100644 index 0000000..e07e92a Binary files /dev/null and b/Visual_Novel_iOS/Assets.xcassets/Role/role_from.imageset/role_from@2x.png differ diff --git a/Visual_Novel_iOS/Assets.xcassets/Role/role_from.imageset/role_from@3x.png b/Visual_Novel_iOS/Assets.xcassets/Role/role_from.imageset/role_from@3x.png new file mode 100644 index 0000000..53e914e Binary files /dev/null and b/Visual_Novel_iOS/Assets.xcassets/Role/role_from.imageset/role_from@3x.png differ diff --git a/Visual_Novel_iOS/Assets.xcassets/Role/role_open_icon.imageset/Contents.json b/Visual_Novel_iOS/Assets.xcassets/Role/role_open_icon.imageset/Contents.json new file mode 100644 index 0000000..ab77c73 --- /dev/null +++ b/Visual_Novel_iOS/Assets.xcassets/Role/role_open_icon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "role_open_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "role_open_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Visual_Novel_iOS/Assets.xcassets/Role/role_open_icon.imageset/role_open_icon@2x.png b/Visual_Novel_iOS/Assets.xcassets/Role/role_open_icon.imageset/role_open_icon@2x.png new file mode 100644 index 0000000..cb4403a Binary files /dev/null and b/Visual_Novel_iOS/Assets.xcassets/Role/role_open_icon.imageset/role_open_icon@2x.png differ diff --git a/Visual_Novel_iOS/Assets.xcassets/Role/role_open_icon.imageset/role_open_icon@3x.png b/Visual_Novel_iOS/Assets.xcassets/Role/role_open_icon.imageset/role_open_icon@3x.png new file mode 100644 index 0000000..4493646 Binary files /dev/null and b/Visual_Novel_iOS/Assets.xcassets/Role/role_open_icon.imageset/role_open_icon@3x.png differ diff --git a/Visual_Novel_iOS/Assets.xcassets/Role/role_star.imageset/Contents.json b/Visual_Novel_iOS/Assets.xcassets/Role/role_star.imageset/Contents.json new file mode 100644 index 0000000..4d4d179 --- /dev/null +++ b/Visual_Novel_iOS/Assets.xcassets/Role/role_star.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "role_star@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "role_star@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Visual_Novel_iOS/Assets.xcassets/Role/role_star.imageset/role_star@2x.png b/Visual_Novel_iOS/Assets.xcassets/Role/role_star.imageset/role_star@2x.png new file mode 100644 index 0000000..3c2bc2a Binary files /dev/null and b/Visual_Novel_iOS/Assets.xcassets/Role/role_star.imageset/role_star@2x.png differ diff --git a/Visual_Novel_iOS/Assets.xcassets/Role/role_star.imageset/role_star@3x.png b/Visual_Novel_iOS/Assets.xcassets/Role/role_star.imageset/role_star@3x.png new file mode 100644 index 0000000..26224a4 Binary files /dev/null and b/Visual_Novel_iOS/Assets.xcassets/Role/role_star.imageset/role_star@3x.png differ diff --git a/Visual_Novel_iOS/Assets.xcassets/Role/role_top_bg.imageset/Contents.json b/Visual_Novel_iOS/Assets.xcassets/Role/role_top_bg.imageset/Contents.json new file mode 100644 index 0000000..fd725a6 --- /dev/null +++ b/Visual_Novel_iOS/Assets.xcassets/Role/role_top_bg.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "role_top_bg@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "role_top_bg@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Visual_Novel_iOS/Assets.xcassets/Role/role_top_bg.imageset/role_top_bg@2x.png b/Visual_Novel_iOS/Assets.xcassets/Role/role_top_bg.imageset/role_top_bg@2x.png new file mode 100644 index 0000000..ea88471 Binary files /dev/null and b/Visual_Novel_iOS/Assets.xcassets/Role/role_top_bg.imageset/role_top_bg@2x.png differ diff --git a/Visual_Novel_iOS/Assets.xcassets/Role/role_top_bg.imageset/role_top_bg@3x.png b/Visual_Novel_iOS/Assets.xcassets/Role/role_top_bg.imageset/role_top_bg@3x.png new file mode 100644 index 0000000..c9a93d4 Binary files /dev/null and b/Visual_Novel_iOS/Assets.xcassets/Role/role_top_bg.imageset/role_top_bg@3x.png differ diff --git a/Visual_Novel_iOS/Assets.xcassets/Role/role_video.imageset/Contents.json b/Visual_Novel_iOS/Assets.xcassets/Role/role_video.imageset/Contents.json new file mode 100644 index 0000000..1452c8f --- /dev/null +++ b/Visual_Novel_iOS/Assets.xcassets/Role/role_video.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "role_video@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "role_video@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Visual_Novel_iOS/Assets.xcassets/Role/role_video.imageset/role_video@2x.png b/Visual_Novel_iOS/Assets.xcassets/Role/role_video.imageset/role_video@2x.png new file mode 100644 index 0000000..2fe42fd Binary files /dev/null and b/Visual_Novel_iOS/Assets.xcassets/Role/role_video.imageset/role_video@2x.png differ diff --git a/Visual_Novel_iOS/Assets.xcassets/Role/role_video.imageset/role_video@3x.png b/Visual_Novel_iOS/Assets.xcassets/Role/role_video.imageset/role_video@3x.png new file mode 100644 index 0000000..7230fb8 Binary files /dev/null and b/Visual_Novel_iOS/Assets.xcassets/Role/role_video.imageset/role_video@3x.png differ diff --git a/Visual_Novel_iOS/Src/Components/Base/CLContainerVC.swift b/Visual_Novel_iOS/Src/Components/Base/CLContainerVC.swift new file mode 100644 index 0000000..68d5a14 --- /dev/null +++ b/Visual_Novel_iOS/Src/Components/Base/CLContainerVC.swift @@ -0,0 +1,74 @@ +// +// CLContainerVC.swift +// Visual_Novel_iOS +// +// Created by mh on 2025/10/15. +// + +import Foundation + +// tabbar的四个rootvc都用共同的header 做一个包容的vc +final class CLContainerVC: UIViewController { + private let header = HeaderView() // 头像栏 + var nav = CLNavigationController() // 真正堆栈 + + init(rootvc: UIViewController) { + super.init(nibName: nil, bundle: nil) + nav = CLNavigationController(rootViewController: rootvc) + addChild(nav) + // nav.setViewControllers([rootViewController], animated: false) + } + required init?(coder: NSCoder) { fatalError() } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .systemBackground + [header, nav.view].forEach { v in +// v.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(v) + } + + header.snp.makeConstraints { make in + make.left.right.top.equalToSuperview() + make.height.equalTo(88) + } + + nav.view.snp.makeConstraints { make in + make.left.right.equalToSuperview() + make.top.equalTo(header.snp.bottom) + make.bottom.equalToSuperview() + } + } +} + +final class HeaderView: UIView { + + lazy var avatar: UIImageView = { + let imageview = UIImageView() + imageview.backgroundColor = .blue + return imageview + }() + + override init(frame: CGRect) { + super.init(frame: frame) + self.backgroundColor = .red + setup() + } + + required init?(coder: NSCoder) { fatalError() } + + func render(_ model: AnyObject) { +// avatar.image = model.avatar + } + + private func setup() { + /* 圆角、阴影、布局代码略 */ + addSubview(avatar) + avatar.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.left.equalToSuperview().offset(30) + make.width.height.equalTo(44) + } + } +} + diff --git a/Visual_Novel_iOS/Src/Components/Base/CLTabRootController.swift b/Visual_Novel_iOS/Src/Components/Base/CLTabRootController.swift index 57fbcb8..6cfd11f 100644 --- a/Visual_Novel_iOS/Src/Components/Base/CLTabRootController.swift +++ b/Visual_Novel_iOS/Src/Components/Base/CLTabRootController.swift @@ -8,7 +8,7 @@ import UIKit class CLTabRootController: CLViewController{ - private var bgTopIv: UIImageView! + var bgTopIv: UIImageView! override func viewDidLoad() { super.viewDidLoad() diff --git a/Visual_Novel_iOS/Src/Components/UI/BaseTopView/CLTopHeaderView.swift b/Visual_Novel_iOS/Src/Components/UI/BaseTopView/CLTopHeaderView.swift new file mode 100644 index 0000000..3beed59 --- /dev/null +++ b/Visual_Novel_iOS/Src/Components/UI/BaseTopView/CLTopHeaderView.swift @@ -0,0 +1,189 @@ +// +// CLTopHeaderView.swift +// Visual_Novel_iOS +// +// Created by mh on 2025/10/15. +// + +import Foundation +import Combine + +/// 所有可能跳转的目标页面 +enum JumpTarget: CaseIterable { + case search + case check // 签到 + case discord +// case web(url: String) // 关联值可以带参数 +} + + +/// tabbar上rootvc顶部头像等公共区域 +class CLTopHeaderView: UIView { + + private let subject = PassthroughSubject() + var jumpPublisher: AnyPublisher { + subject.eraseToAnyPublisher() + } + + lazy var avatarContainerView: UIView = { + let view = UIView() + return view + }() + + lazy var avatar: UIImageView = { + let imgView = UIImageView() + imgView.contentMode = .scaleAspectFill + imgView.cornerRadius = 18.0 + imgView.backgroundColor = .purple + return imgView + }() + + lazy var vipImgView: UIImageView = { + let imgview = UIImageView(image: UIImage(named: "header_not_subscriber_icon")) + imgview.contentMode = .scaleAspectFill + return imgview + }() + + lazy var containerView: UIView = { + let view = UIView() + view.backgroundColor = .white + view.cornerRadius = 18.0 + return view + }() + + lazy var starImgView: UIImageView = { + let imgView = UIImageView(image: UIImage(named: "header_money_icon")) + imgView.contentMode = .scaleAspectFill + return imgView + }() + + lazy var moneyLab: UILabel = { + let lab = UILabel() + lab.text = "150" + lab.font = UIFont.boldSystemFont(ofSize: 14) + return lab + }() + + lazy var searchImgView: UIImageView = { + let imgView = UIImageView(image: UIImage(named: "header_search_icon")) + imgView.contentMode = .scaleAspectFill + imgView.isUserInteractionEnabled = true + let tap = UITapGestureRecognizer(target: self, action: #selector(searchImgViewClicked)) + imgView.addGestureRecognizer(tap) + return imgView + }() + + lazy var calendarImgView: UIImageView = { + let imgView = UIImageView(image: UIImage(named: "header_check_icon")) + imgView.contentMode = .scaleAspectFill + imgView.isUserInteractionEnabled = true + let tap = UITapGestureRecognizer(target: self, action: #selector(calendarImgViewClicked)) + imgView.addGestureRecognizer(tap) + return imgView + }() + + lazy var discordImgView: UIImageView = { + let imgView = UIImageView(image: UIImage(named: "header_discord_icon")) + imgView.contentMode = .scaleAspectFill + imgView.isUserInteractionEnabled = true + let tap = UITapGestureRecognizer(target: self, action: #selector(discordImgViewClicked)) + imgView.addGestureRecognizer(tap) + return imgView + }() + + lazy var avatarStackView: UIStackView = { + let stackView = UIStackView(arrangedSubviews: [avatarContainerView, containerView]) + stackView.axis = .horizontal + stackView.spacing = 15.0 + stackView.alignment = .fill + stackView.distribution = .fill + return stackView + }() + + lazy var toolStackView: UIStackView = { + let stackView = UIStackView(arrangedSubviews: [searchImgView, calendarImgView, discordImgView]) + stackView.axis = .horizontal + stackView.spacing = 15.0 + stackView.alignment = .fill + stackView.distribution = .fill + return stackView + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + setupSubviews() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Action + @objc func searchImgViewClicked() { + print("111111") +// UIWindow.getTopViewController()?.navigationController?.pushViewController(TestEntrancesController(), animated: true) + + subject.send(.search) + } + + @objc func calendarImgViewClicked() { + print("2222") + subject.send(.check) + } + + @objc func discordImgViewClicked() { + print("333") + subject.send(.discord) + } + + // MARK: Subviews + func setupSubviews() { + + addSubview(avatarStackView) + addSubview(toolStackView) + + avatarContainerView.addSubview(avatar) + avatarContainerView.addSubview(vipImgView) + + containerView.addSubview(starImgView) + containerView.addSubview(moneyLab) + + avatarStackView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(20) + make.top.equalToSuperview().offset(UIDevice().statusBarHeight + 4.0) + } + + toolStackView.snp.makeConstraints { make in + make.right.equalToSuperview().inset(20) + make.centerY.equalTo(avatarStackView.snp.centerY) + } + + avatarContainerView.snp.makeConstraints { make in + make.width.height.equalTo(36) + } + + avatar.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + vipImgView.snp.makeConstraints { make in + make.right.bottom.equalToSuperview() + } + + containerView.snp.makeConstraints { make in + make.height.equalTo(36) + } + + starImgView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(6.5) + make.centerY.equalToSuperview() + } + + moneyLab.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.left.equalTo(starImgView.snp.right).offset(5.5) + make.right.equalToSuperview().inset(12.5) + } + } +} diff --git a/Visual_Novel_iOS/Src/Modules/Roles/RolesRootPageController.swift b/Visual_Novel_iOS/Src/Modules/Roles/RolesRootPageController.swift new file mode 100644 index 0000000..951fa79 --- /dev/null +++ b/Visual_Novel_iOS/Src/Modules/Roles/RolesRootPageController.swift @@ -0,0 +1,61 @@ +// +// RolesRootPageController.swift +// Visual_Novel_iOS +// +// Created by mh on 2025/10/14. +// + +import UIKit +import Combine + + +class RolesRootPageController: CLTabRootController { + + private var cancellables = Set() + + override func viewDidLoad() { + super.viewDidLoad() + + let baseImageView = UIImageView(image: R.image.base_bg()) + baseImageView.contentMode = .scaleAspectFill + self.view.insertSubview(baseImageView, at: 0) + baseImageView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + view.backgroundColor = .clear + bgTopIv.isHidden = true + + setupViews() + setupEvent() + } + + private func setupViews() { + navigationView.bgView.alpha = 0 + navigationView.setupBgViewToStatusBarHeight() + navigationView.isUserInteractionEnabled = false + } + + private func setupEvent() { + self.container.jumpPublisher + .receive(on: RunLoop.main) + .sink { [weak self] target in + self?.handleJump(target) + } + .store(in: &cancellables) + } + + // 点击顶部 实现跳转 + private func handleJump(_ target: JumpTarget) { + switch target { + case .search: + let vc = TestEntrancesController() + navigationController?.pushViewController(vc, animated: true) + case .check: + let vc = TestEntrancesController() + navigationController?.pushViewController(vc, animated: true) + case .discord: + let vc = TestEntrancesController() + navigationController?.pushViewController(vc, animated: true) + } + } +} diff --git a/Visual_Novel_iOS/Src/Modules/Roles/View/CLRoleCollectionCell.swift b/Visual_Novel_iOS/Src/Modules/Roles/View/CLRoleCollectionCell.swift new file mode 100644 index 0000000..668f81d --- /dev/null +++ b/Visual_Novel_iOS/Src/Modules/Roles/View/CLRoleCollectionCell.swift @@ -0,0 +1,186 @@ +// +// CLRoleCollectionCell.swift +// Visual_Novel_iOS +// +// Created by mh on 2025/10/15. +// + +import UIKit + +class CLRoleCollectionCell: UICollectionViewCell { + + let cellWidth = (UIScreen.width - 30.0) / 2.0 + + lazy var bookBgImgView: UIImageView = { + let imgView = UIImageView(image: UIImage(named: "role_book")) + imgView.contentMode = .scaleAspectFill + return imgView + }() + + lazy var fromImgView: UIImageView = { + let imgView = UIImageView(image: UIImage(named: "role_from")) + imgView.contentMode = .scaleAspectFill + return imgView + }() + + lazy var coverImgView: UIImageView = { + let imgView = UIImageView() + imgView.contentMode = .scaleAspectFill + imgView.backgroundColor = .red + imgView.cornerRadius = 25.0 + return imgView + }() + + lazy var topShadowImgView: UIImageView = { + let image = UIImage(named: "role_top_bg") + let stretchedImage = image?.stretchableImage(horizontalRatio: 0.5, verticalRatio: 0.5) + let imgView = UIImageView(image: stretchedImage) + return imgView + }() + + lazy var bottomShadowImgView: UIImageView = { + let image = UIImage(named: "role_bottom_bg") + let stretchedImage = image?.stretchableImage(horizontalRatio: 0.5, verticalRatio: 0.5) + let imgView = UIImageView(image: stretchedImage) + return imgView + }() + + lazy var starImgView: UIImageView = { + let imgView = UIImageView(image: UIImage(named: "role_star")) + imgView.contentMode = .scaleAspectFill + return imgView + }() + + lazy var sourceLab: UILabel = { + let lab = UILabel() + lab.font = UIFont.boldSystemFont(ofSize: 15) + lab.text = "9.5" + lab.textColor = UIColor(hex: "#FFE100") + return lab + }() + + lazy var nameLab: UILabel = { + let lab = UILabel() + lab.font = UIFont.boldSystemFont(ofSize: 14) + lab.text = "Character · 18" + lab.textColor = UIColor(hex: "#FFFFFF") + return lab + }() + + lazy var tagLab: UILabel = { + let lab = UILabel() + lab.font = UIFont.boldSystemFont(ofSize: 10) + lab.text = "# Sining ng Pagpapatalim / #xianxia / #swordsmanship" + lab.textColor = .white + lab.numberOfLines = 0 + return lab + }() + + lazy var descLab: UILabel = { + let lab = UILabel() + lab.numberOfLines = 3 + lab.font = UIFont.systemFont(ofSize: 12) + lab.text = "Once a prodigy, Lin Feng had his cultivation shattered and was cast out Once a prodigy, Lin Feng had his cultivation shattered and was cast out" + lab.textColor = UIColor(hex: "#666666") + return lab + }() + + lazy var remindLab: UILabel = { + let lab = UILabel() + lab.font = UIFont.boldSystemFont(ofSize: 10) + lab.text = "[The Lsat Oracle of Kael]" + lab.textColor = UIColor(hex: "#999999") + return lab + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + self.contentView.clipsToBounds = true + setupViews() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: data + func setupData(desc: String) { + descLab.text = desc + } + + // MARK: subviews + private func setupViews() { + contentView.addSubview(coverImgView) + + contentView.addSubview(topShadowImgView) + contentView.addSubview(bottomShadowImgView) + + contentView.addSubview(bookBgImgView) + contentView.addSubview(fromImgView) + + contentView.addSubview(starImgView) + contentView.addSubview(sourceLab) + + contentView.addSubview(nameLab) + contentView.addSubview(tagLab) + + contentView.addSubview(descLab) + contentView.addSubview(remindLab) + + coverImgView.snp.makeConstraints { make in + make.width.equalTo(cellWidth) + make.height.equalTo(coverImgView.snp.width).multipliedBy(4.0 / 3.0) + make.top.left.right.equalToSuperview() + } + + topShadowImgView.snp.makeConstraints { make in + make.left.right.top.equalToSuperview() + make.bottom.equalTo(sourceLab.snp.bottom).offset(18) + } + + bottomShadowImgView.snp.makeConstraints { make in + make.left.right.equalToSuperview() + make.bottom.equalTo(coverImgView.snp.bottom) + make.top.equalTo(nameLab.snp.top).offset(-25) + } + + bookBgImgView.snp.makeConstraints { make in + make.top.left.equalToSuperview() + } + + fromImgView.snp.makeConstraints { make in + make.top.left.equalToSuperview() + } + + sourceLab.snp.makeConstraints { make in + make.top.equalToSuperview().offset(12) + make.right.equalToSuperview().inset(18) + } + + starImgView.snp.makeConstraints { make in + make.centerY.equalTo(sourceLab.snp.centerY) + make.right.equalTo(sourceLab.snp.left).offset(-5) + } + + tagLab.snp.makeConstraints { make in + make.right.left.equalToSuperview().inset(10) + make.bottom.equalTo(coverImgView.snp.bottom).offset(-10) + } + + nameLab.snp.makeConstraints { make in + make.left.right.equalToSuperview().inset(10) + make.bottom.equalTo(tagLab.snp.top).offset(-10) + } + + descLab.snp.makeConstraints { make in + make.left.right.equalToSuperview().inset(10) + make.top.equalTo(coverImgView.snp.bottom).offset(10) + } + + remindLab.snp.makeConstraints { make in + make.left.right.equalToSuperview().inset(10) + make.top.equalTo(descLab.snp.bottom).offset(5) + } + } +} diff --git a/Visual_Novel_iOS/Src/Modules/Roles/View/CLWaterfallLayout.swift b/Visual_Novel_iOS/Src/Modules/Roles/View/CLWaterfallLayout.swift new file mode 100644 index 0000000..53babbe --- /dev/null +++ b/Visual_Novel_iOS/Src/Modules/Roles/View/CLWaterfallLayout.swift @@ -0,0 +1,78 @@ +// +// CLWaterfallLayout.swift +// Visual_Novel_iOS +// +// Created by mh on 2025/10/15. +// + +import UIKit + +final class CLWaterfallLayout: UICollectionViewLayout { + // MARK: 可外部配置 + var columnCount: Int = 2 + var columnSpacing: CGFloat = 15 + var interItemSpacing: CGFloat = 10 + var sectionInset: UIEdgeInsets = .init(top: 0, left: 10, bottom: 10, right: 10) + var delegate: WaterfallLayoutDelegate? = nil + + // MARK: 私有缓存 + private var cache: [UICollectionViewLayoutAttributes] = [] + private var contentHeight: CGFloat = 0 + private var colHeight: [CGFloat] = [] + + override var collectionViewContentSize: CGSize { + CGSize(width: collectionView!.bounds.width, height: contentHeight) + } + + override func prepare() { + super.prepare() + guard cache.isEmpty, let cv = collectionView else { return } + + contentHeight = sectionInset.top + colHeight = .init(repeating: sectionInset.top, count: columnCount) + + let itemCount = cv.numberOfItems(inSection: 0) + let totalWidth = cv.bounds.width - sectionInset.left - sectionInset.right + let cellWidth = (totalWidth - CGFloat(columnCount - 1) * columnSpacing) / CGFloat(columnCount) + + for idx in 0.. [UICollectionViewLayoutAttributes]? { + cache.filter { $0.frame.intersects(rect) } + } + + override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { + cache[indexPath.item] + } + + override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { + newBounds.width != collectionView?.bounds.width + } +} + +// MARK: - 代理协议(动态高度) +protocol WaterfallLayoutDelegate: AnyObject { + func collectionView(_ collectionView: UICollectionView, + layout: CLWaterfallLayout, + heightForItemAt indexPath: IndexPath) -> CGFloat +} + diff --git a/Visual_Novel_iOS/Src/Modules/Roles/View/RolesRootPageView.swift b/Visual_Novel_iOS/Src/Modules/Roles/View/RolesRootPageView.swift new file mode 100644 index 0000000..553da7e --- /dev/null +++ b/Visual_Novel_iOS/Src/Modules/Roles/View/RolesRootPageView.swift @@ -0,0 +1,133 @@ +// +// RolesRootPageView.swift +// Visual_Novel_iOS +// +// Created by mh on 2025/10/14. +// + +import JXPagingView +import JXSegmentedView +import UIKit +import Combine + +class RolesRootPageView: CLContainer { + + let itemWidth: CGFloat = (UIScreen.width - 30.0) / 2.0 + var jumpPublisher: AnyPublisher { topView.jumpPublisher } + + let data: [String] = [ + "Once a prodigy, Lin Feng had his cultivation shattered and was cast out Once a prodigy, Lin Feng had his cultivation shattered and was cast", + "Once a prodigy, Lin Feng had his cultivation", + "Once a prodigy, Lin Feng had his cultivation shattered and was cast out Once a prodigy", + "Once a prodigy", + "Once a prodigy, Lin Feng had his cultivation shattered and was cast out Once a prodigy, Lin Feng had his cultivation shattered and was cast Once a prodigy, Lin Feng had his cultivation shattered and was cast out Once a prodigy, Lin Feng had his cultivation shattered and was cast", + "Once a prodigy, Lin Feng had his cultivation shattered and was cast out Once a prodigy", + "Once a prodigy, Lin Feng had his cultivation shattered and was cast out Once a prodigy", + "Once a prodigy", + "Once a prodigy, Lin Feng had his cultivation shattered and was cast out Once a prodigy, Lin Feng had his cultivation shattered and was cast", + "Once a prodigy" + ] + + 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]" + ] + + lazy var topView: CLTopHeaderView = { + let view = CLTopHeaderView() + return view + }() + + lazy var collectionView: UICollectionView = { + let layout = CLWaterfallLayout() + layout.columnCount = 2 + layout.delegate = self + + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.delegate = self + collectionView.dataSource = self + collectionView.backgroundColor = .clear + collectionView.showsVerticalScrollIndicator = false + collectionView.showsHorizontalScrollIndicator = false + collectionView.register(CLRoleCollectionCell.self, forCellWithReuseIdentifier: "CLRoleCollectionCell") + return collectionView + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + setupViews() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupViews() { + addSubview(self.topView) + addSubview(collectionView) + + topView.snp.makeConstraints { make in + make.left.right.top.equalToSuperview() + make.height.equalTo(UIDevice().navHeight) + } + + collectionView.snp.makeConstraints { make in + make.bottom.left.right.equalToSuperview() + make.top.equalTo(topView.snp.bottom).offset(20) + } + } +} + +extension RolesRootPageView: UICollectionViewDelegate, UICollectionViewDataSource, WaterfallLayoutDelegate { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return data.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell: CLRoleCollectionCell = collectionView.dequeueReusableCell(withReuseIdentifier: "CLRoleCollectionCell", for: indexPath) as! CLRoleCollectionCell + + cell.setupData(desc: data[indexPath.item]) + + return cell + } + + func collectionView(_ collectionView: UICollectionView, + layout: CLWaterfallLayout, + heightForItemAt indexPath: IndexPath) -> CGFloat { + let model = data[indexPath.item] + + let coverH = itemWidth * (4.0 / 3.0) + // 文字高度 + let maxLines = 3 + let font = UIFont.systemFont(ofSize: 12) + let lineHeight = font.lineHeight // 15 pt 字体 ≈ 17.4 pt + let maxHeight = lineHeight * CGFloat(maxLines) + + // 真实文本高度(不会超过 maxHeight) + let textSize = model.boundingRect( + with: CGSize(width: itemWidth - 20, height: maxHeight), // 高度封顶 + options: [.usesLineFragmentOrigin, .usesFontLeading], + attributes: [.font: font], + context: nil).size + + // 取最小值,保证不足 3 行时按实际算,超过 3 行时截断在 3 行高 + let textH = min(textSize.height, maxHeight) + + let remindH = remind[indexPath.item].boundingRect( + with: CGSize(width: itemWidth - 20.0, height: .greatestFiniteMagnitude), + options: .usesLineFragmentOrigin, + attributes: [.font: UIFont.boldSystemFont(ofSize: 10)], + context: nil).height + + return coverH + 10.0 + textH + 5.0 + remindH + } +} diff --git a/Visual_Novel_iOS/Src/Modules/Tab/TabBarController.swift b/Visual_Novel_iOS/Src/Modules/Tab/TabBarController.swift index d56d77d..eb9e87e 100755 --- a/Visual_Novel_iOS/Src/Modules/Tab/TabBarController.swift +++ b/Visual_Novel_iOS/Src/Modules/Tab/TabBarController.swift @@ -63,7 +63,8 @@ extension TabBarController { private func configViewControllers() { let home = CLNavigationController(rootViewController: HomePageRootController()) let friend = CLNavigationController(rootViewController: FriendsRootHomeController()) - let discover = CLNavigationController(rootViewController: DiscoverRootPageController()) +// let discover = CLNavigationController(rootViewController: DiscoverRootPageController()) + let discover = CLNavigationController(rootViewController: RolesRootPageController()) let me = CLNavigationController(rootViewController: MeRootPageController()) viewControllers = [home, friend, discover, me] diff --git a/Visual_Novel_iOS/Src/Utils/Extensions/UIDeviceExt.swift b/Visual_Novel_iOS/Src/Utils/Extensions/UIDeviceExt.swift index c83173d..bb9d645 100755 --- a/Visual_Novel_iOS/Src/Utils/Extensions/UIDeviceExt.swift +++ b/Visual_Novel_iOS/Src/Utils/Extensions/UIDeviceExt.swift @@ -60,4 +60,26 @@ extension UIDevice { return UIApplication.shared.windows.first?.safeAreaInsets.bottom ?? 0 > 0 } } + + // 状态栏 高度 + var statusBarHeight: CGFloat { + if #available(iOS 13.0, *) { + let windowScene = UIApplication.shared.connectedScenes + .compactMap { $0 as? UIWindowScene } + .first + return windowScene?.statusBarManager?.statusBarFrame.size.height ?? 0 + } else { + return UIApplication.shared.statusBarFrame.size.height + } + } + + // 导航栏 高度 + var navBarHeight: CGFloat { + return UINavigationController().navigationBar.frame.height + } + + // 状态栏 + 导航栏 高度 + var navHeight: CGFloat { + return navBarHeight + statusBarHeight + } }