feat: 更新素材管理和举报风险地图
This commit is contained in:
@ -0,0 +1,710 @@
|
||||
//
|
||||
// MaterialDetailViewController.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
import Kingfisher
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 素材详情页,对齐 Android `MaterialDetailNewScreen`。
|
||||
/// 素材详情页,展示轮播、信息、统计、审核与底部操作。
|
||||
final class MaterialDetailViewController: BaseViewController {
|
||||
private let viewModel: MaterialDetailViewModel
|
||||
private let api: any MaterialManagementServing
|
||||
|
||||
private let scrollView = UIScrollView()
|
||||
private let contentStack = UIStackView()
|
||||
private let carouselView = MaterialMediaCarouselView()
|
||||
private let infoSection = MaterialDetailInfoSection()
|
||||
private let uploaderSection = MaterialDetailUploaderSection()
|
||||
private let reviewSection = MaterialDetailReviewSection()
|
||||
private let bottomBar = UIView()
|
||||
private let deleteButton = UIButton(type: .system)
|
||||
private let editButton = UIButton(type: .system)
|
||||
|
||||
var onDeleted: ((Int) -> Void)?
|
||||
var onEdited: (() -> Void)?
|
||||
|
||||
/// 初始化素材详情页。
|
||||
init(
|
||||
materialId: Int,
|
||||
api: any MaterialManagementServing = NetworkServices.shared.materialManagementAPI
|
||||
) {
|
||||
viewModel = MaterialDetailViewModel(materialId: materialId)
|
||||
self.api = api
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func setupNavigationBar() {
|
||||
title = "素材详情"
|
||||
}
|
||||
|
||||
override func setupUI() {
|
||||
view.backgroundColor = UIColor(hex: 0xF4F4F4)
|
||||
contentStack.axis = .vertical
|
||||
contentStack.spacing = 12
|
||||
scrollView.backgroundColor = UIColor(hex: 0xF4F4F4)
|
||||
bottomBar.backgroundColor = .white
|
||||
|
||||
configureBottomButton(deleteButton, title: "删除", backgroundColor: UIColor(hex: 0xEFF6FF), titleColor: AppColor.primary)
|
||||
configureBottomButton(editButton, title: "编辑", backgroundColor: AppColor.primary, titleColor: .white)
|
||||
|
||||
view.addSubview(scrollView)
|
||||
scrollView.addSubview(contentStack)
|
||||
contentStack.addArrangedSubview(carouselView)
|
||||
contentStack.addArrangedSubview(infoSection)
|
||||
contentStack.addArrangedSubview(uploaderSection)
|
||||
contentStack.addArrangedSubview(reviewSection)
|
||||
view.addSubview(bottomBar)
|
||||
bottomBar.addSubview(deleteButton)
|
||||
bottomBar.addSubview(editButton)
|
||||
}
|
||||
|
||||
override func setupConstraints() {
|
||||
bottomBar.snp.makeConstraints { make in
|
||||
make.leading.trailing.bottom.equalToSuperview()
|
||||
}
|
||||
deleteButton.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(16)
|
||||
make.leading.equalToSuperview().offset(16)
|
||||
make.width.equalTo(100)
|
||||
make.height.equalTo(48)
|
||||
make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-16)
|
||||
}
|
||||
editButton.snp.makeConstraints { make in
|
||||
make.top.height.equalTo(deleteButton)
|
||||
make.leading.equalTo(deleteButton.snp.trailing).offset(12)
|
||||
make.trailing.equalToSuperview().inset(16)
|
||||
}
|
||||
scrollView.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalTo(view.safeAreaLayoutGuide)
|
||||
make.bottom.equalTo(bottomBar.snp.top)
|
||||
}
|
||||
contentStack.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
make.width.equalTo(scrollView)
|
||||
}
|
||||
carouselView.snp.makeConstraints { make in
|
||||
make.height.equalTo(carouselView.snp.width).multipliedBy(9.0 / 16.0)
|
||||
}
|
||||
}
|
||||
|
||||
override func bindActions() {
|
||||
viewModel.onStateChange = { [weak self] in
|
||||
Task { @MainActor in self?.applyViewModel() }
|
||||
}
|
||||
viewModel.onShowMessage = { [weak self] message in
|
||||
Task { @MainActor in self?.showToast(message) }
|
||||
}
|
||||
viewModel.onDeleted = { [weak self] id in
|
||||
Task { @MainActor in
|
||||
self?.onDeleted?(id)
|
||||
self?.navigationController?.popViewController(animated: true)
|
||||
}
|
||||
}
|
||||
deleteButton.addTarget(self, action: #selector(deleteTapped), for: .touchUpInside)
|
||||
editButton.addTarget(self, action: #selector(editTapped), for: .touchUpInside)
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
Task { await viewModel.load(api: api) }
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
carouselView.pause()
|
||||
}
|
||||
|
||||
private func configureBottomButton(_ button: UIButton, title: String, backgroundColor: UIColor, titleColor: UIColor) {
|
||||
button.setTitle(title, for: .normal)
|
||||
button.setTitleColor(titleColor, for: .normal)
|
||||
button.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
button.backgroundColor = backgroundColor
|
||||
button.layer.cornerRadius = 8
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func applyViewModel() {
|
||||
if (viewModel.isLoading || viewModel.isDeleting) && viewModel.detail == nil {
|
||||
showLoading()
|
||||
} else {
|
||||
hideLoading()
|
||||
}
|
||||
guard let detail = viewModel.detail else { return }
|
||||
carouselView.apply(media: detail.mediaList ?? [])
|
||||
infoSection.apply(detail: detail)
|
||||
uploaderSection.apply(detail: detail)
|
||||
reviewSection.apply(detail: detail)
|
||||
}
|
||||
|
||||
@objc private func deleteTapped() {
|
||||
let alert = UIAlertController(title: "确认删除", message: "确定删除该素材吗?", preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
alert.addAction(UIAlertAction(title: "删除", style: .destructive) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
Task {
|
||||
self.showLoading()
|
||||
await self.viewModel.delete(api: self.api)
|
||||
self.hideLoading()
|
||||
}
|
||||
})
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
@objc private func editTapped() {
|
||||
let controller = MaterialFormViewController(mode: .edit(id: viewModel.materialId))
|
||||
controller.onSubmitSuccess = { [weak self] in
|
||||
guard let self else { return }
|
||||
self.onEdited?()
|
||||
Task { await self.viewModel.load(api: self.api) }
|
||||
}
|
||||
navigationController?.pushViewController(controller, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
/// 素材详情媒体轮播,支持图片自动切换与视频播完切换。
|
||||
/// 素材详情顶部媒体轮播视图,支持图片与视频播放。
|
||||
private final class MaterialMediaCarouselView: UIView, UIScrollViewDelegate {
|
||||
private let scrollView = UIScrollView()
|
||||
private let pageControl = UIPageControl()
|
||||
private let placeholderLabel = UILabel()
|
||||
private var media: [MaterialDetailMediaItem] = []
|
||||
private var players: [Int: AVPlayer] = [:]
|
||||
private var timer: Timer?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = UIColor(hex: 0xF5F5F5)
|
||||
scrollView.isPagingEnabled = true
|
||||
scrollView.showsHorizontalScrollIndicator = false
|
||||
scrollView.delegate = self
|
||||
scrollView.bounces = false
|
||||
pageControl.currentPageIndicatorTintColor = .white
|
||||
pageControl.pageIndicatorTintColor = UIColor.white.withAlphaComponent(0.5)
|
||||
placeholderLabel.text = "暂无媒体"
|
||||
placeholderLabel.font = .systemFont(ofSize: 14)
|
||||
placeholderLabel.textColor = AppColor.textSecondary
|
||||
placeholderLabel.textAlignment = .center
|
||||
addSubview(scrollView)
|
||||
addSubview(pageControl)
|
||||
addSubview(placeholderLabel)
|
||||
scrollView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
pageControl.snp.makeConstraints { make in
|
||||
make.centerX.equalToSuperview()
|
||||
make.bottom.equalToSuperview().inset(8)
|
||||
}
|
||||
placeholderLabel.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
layoutPages()
|
||||
}
|
||||
|
||||
/// 应用媒体列表。
|
||||
func apply(media: [MaterialDetailMediaItem]) {
|
||||
stopTimer()
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
players.values.forEach { $0.pause() }
|
||||
players.removeAll()
|
||||
self.media = media
|
||||
scrollView.subviews.forEach { $0.removeFromSuperview() }
|
||||
pageControl.numberOfPages = media.count
|
||||
pageControl.currentPage = 0
|
||||
pageControl.isHidden = media.count <= 1
|
||||
placeholderLabel.isHidden = !media.isEmpty
|
||||
scrollView.isHidden = media.isEmpty
|
||||
guard !media.isEmpty else { return }
|
||||
|
||||
for (index, item) in media.enumerated() {
|
||||
let page = UIView()
|
||||
page.backgroundColor = UIColor(hex: 0xF5F5F5)
|
||||
scrollView.addSubview(page)
|
||||
if item.type == 2 {
|
||||
addVideo(urlText: item.displayURL, to: page, index: index)
|
||||
} else {
|
||||
addImage(urlText: item.displayURL, to: page)
|
||||
}
|
||||
}
|
||||
setNeedsLayout()
|
||||
layoutIfNeeded()
|
||||
playCurrentPageIfNeeded()
|
||||
}
|
||||
|
||||
/// 暂停当前轮播。
|
||||
func pause() {
|
||||
stopTimer()
|
||||
players.values.forEach { $0.pause() }
|
||||
}
|
||||
|
||||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
updateCurrentPage()
|
||||
}
|
||||
|
||||
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
|
||||
updateCurrentPage()
|
||||
}
|
||||
|
||||
private func addImage(urlText: String, to page: UIView) {
|
||||
let imageView = UIImageView()
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.clipsToBounds = true
|
||||
page.addSubview(imageView)
|
||||
imageView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
if let url = URL(string: urlText), !urlText.isEmpty {
|
||||
imageView.kf.setImage(with: url)
|
||||
}
|
||||
}
|
||||
|
||||
private func addVideo(urlText: String, to page: UIView, index: Int) {
|
||||
guard let url = URL(string: urlText), !urlText.isEmpty else {
|
||||
addImage(urlText: "", to: page)
|
||||
return
|
||||
}
|
||||
let player = AVPlayer(url: url)
|
||||
players[index] = player
|
||||
let playerView = MaterialPlayerLayerView()
|
||||
playerView.playerLayer.player = player
|
||||
page.addSubview(playerView)
|
||||
playerView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(videoDidEnd(_:)),
|
||||
name: .AVPlayerItemDidPlayToEndTime,
|
||||
object: player.currentItem
|
||||
)
|
||||
}
|
||||
|
||||
private func layoutPages() {
|
||||
let size = bounds.size
|
||||
guard size.width > 0, size.height > 0 else { return }
|
||||
for (index, page) in scrollView.subviews.enumerated() {
|
||||
page.frame = CGRect(x: CGFloat(index) * size.width, y: 0, width: size.width, height: size.height)
|
||||
}
|
||||
scrollView.contentSize = CGSize(width: size.width * CGFloat(media.count), height: size.height)
|
||||
scrollView.contentOffset = CGPoint(x: CGFloat(pageControl.currentPage) * size.width, y: 0)
|
||||
}
|
||||
|
||||
private func updateCurrentPage() {
|
||||
guard bounds.width > 0 else { return }
|
||||
let page = Int(round(scrollView.contentOffset.x / bounds.width))
|
||||
pageControl.currentPage = max(0, min(page, media.count - 1))
|
||||
playCurrentPageIfNeeded()
|
||||
}
|
||||
|
||||
private func playCurrentPageIfNeeded() {
|
||||
stopTimer()
|
||||
players.forEach { index, player in
|
||||
if index == pageControl.currentPage {
|
||||
player.seek(to: .zero)
|
||||
player.play()
|
||||
} else {
|
||||
player.pause()
|
||||
}
|
||||
}
|
||||
guard !media.isEmpty else { return }
|
||||
if media[pageControl.currentPage].type == 1 {
|
||||
startImageTimer()
|
||||
}
|
||||
}
|
||||
|
||||
private func startImageTimer() {
|
||||
guard media.count > 1 else { return }
|
||||
timer = Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { [weak self] _ in
|
||||
self?.advancePage()
|
||||
}
|
||||
}
|
||||
|
||||
private func stopTimer() {
|
||||
timer?.invalidate()
|
||||
timer = nil
|
||||
}
|
||||
|
||||
@objc private func videoDidEnd(_ notification: Notification) {
|
||||
advancePage()
|
||||
}
|
||||
|
||||
private func advancePage() {
|
||||
guard media.count > 1, bounds.width > 0 else { return }
|
||||
let nextPage = (pageControl.currentPage + 1) % media.count
|
||||
pageControl.currentPage = nextPage
|
||||
scrollView.setContentOffset(CGPoint(x: CGFloat(nextPage) * bounds.width, y: 0), animated: true)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in
|
||||
self?.playCurrentPageIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// AVPlayerLayer 容器。
|
||||
/// 承载 AVPlayerLayer 的视频播放容器。
|
||||
private final class MaterialPlayerLayerView: UIView {
|
||||
override class var layerClass: AnyClass { AVPlayerLayer.self }
|
||||
var playerLayer: AVPlayerLayer { layer as! AVPlayerLayer }
|
||||
}
|
||||
|
||||
/// 素材详情基础信息区。
|
||||
/// 素材详情的基础信息区块。
|
||||
private final class MaterialDetailInfoSection: UIView {
|
||||
private let titleLabel = UILabel()
|
||||
private let listingBadge = UILabel()
|
||||
private let tagStack = UIStackView()
|
||||
private let statsStack = UIStackView()
|
||||
private let downloadStat = MaterialDetailStatView(systemIconName: "arrow.down.circle")
|
||||
private let likeStat = MaterialDetailStatView(iconName: "sample_ic_like_count")
|
||||
private let collectStat = MaterialDetailStatView(iconName: "sample_ic_collect_count")
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .white
|
||||
titleLabel.font = .systemFont(ofSize: 20, weight: .bold)
|
||||
titleLabel.textColor = UIColor(hex: 0x2E3746)
|
||||
titleLabel.numberOfLines = 0
|
||||
listingBadge.font = .systemFont(ofSize: 12)
|
||||
listingBadge.layer.cornerRadius = 4
|
||||
listingBadge.clipsToBounds = true
|
||||
listingBadge.textAlignment = .center
|
||||
tagStack.axis = .horizontal
|
||||
tagStack.spacing = 8
|
||||
tagStack.alignment = .leading
|
||||
statsStack.axis = .horizontal
|
||||
statsStack.spacing = 16
|
||||
[downloadStat, likeStat, collectStat].forEach(statsStack.addArrangedSubview)
|
||||
|
||||
addSubview(titleLabel)
|
||||
addSubview(listingBadge)
|
||||
addSubview(tagStack)
|
||||
addSubview(statsStack)
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.top.leading.equalToSuperview().offset(16)
|
||||
make.trailing.lessThanOrEqualTo(listingBadge.snp.leading).offset(-12)
|
||||
}
|
||||
listingBadge.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().inset(16)
|
||||
make.centerY.equalTo(titleLabel)
|
||||
make.height.equalTo(24)
|
||||
make.width.greaterThanOrEqualTo(54)
|
||||
}
|
||||
tagStack.snp.makeConstraints { make in
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(12)
|
||||
make.leading.trailing.equalToSuperview().inset(16)
|
||||
}
|
||||
statsStack.snp.makeConstraints { make in
|
||||
make.top.equalTo(tagStack.snp.bottom).offset(12)
|
||||
make.leading.equalToSuperview().offset(16)
|
||||
make.trailing.lessThanOrEqualToSuperview().inset(16)
|
||||
make.bottom.equalToSuperview().inset(16)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func apply(detail: MaterialDetail) {
|
||||
titleLabel.text = detail.name ?? ""
|
||||
let listed = detail.listingStatus == 1
|
||||
listingBadge.text = listed ? "已上架" : "未上架"
|
||||
let color = listed ? UIColor(hex: 0x22C55E) : AppColor.textSecondary
|
||||
listingBadge.textColor = color
|
||||
listingBadge.backgroundColor = color.withAlphaComponent(0.1)
|
||||
tagStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
(detail.materialTag ?? []).forEach { tagStack.addArrangedSubview(makeTag($0.name)) }
|
||||
downloadStat.setCount(detail.downloadCount)
|
||||
likeStat.setCount(detail.likesCount)
|
||||
collectStat.setCount(detail.collectCount)
|
||||
}
|
||||
|
||||
private func makeTag(_ title: String) -> UILabel {
|
||||
let label = UILabel()
|
||||
label.text = title
|
||||
label.font = .systemFont(ofSize: 12)
|
||||
label.textColor = AppColor.primary
|
||||
label.textAlignment = .center
|
||||
label.backgroundColor = UIColor(hex: 0xEFF6FF)
|
||||
label.layer.cornerRadius = 4
|
||||
label.clipsToBounds = true
|
||||
label.snp.makeConstraints { make in
|
||||
make.height.equalTo(22)
|
||||
make.width.greaterThanOrEqualTo(40)
|
||||
}
|
||||
return label
|
||||
}
|
||||
}
|
||||
|
||||
/// 素材上传人信息区。
|
||||
/// 素材详情的上传人信息区块。
|
||||
private final class MaterialDetailUploaderSection: UIView {
|
||||
private let titleLabel = UILabel()
|
||||
private let avatarView = UIImageView()
|
||||
private let nameLabel = UILabel()
|
||||
private let tagStack = UIStackView()
|
||||
private let descriptionLabel = UILabel()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .white
|
||||
titleLabel.text = "上传人"
|
||||
titleLabel.font = .systemFont(ofSize: 16, weight: .bold)
|
||||
titleLabel.textColor = AppColor.textPrimary
|
||||
avatarView.contentMode = .scaleAspectFill
|
||||
avatarView.clipsToBounds = true
|
||||
avatarView.layer.cornerRadius = 24
|
||||
avatarView.backgroundColor = UIColor(hex: 0xF5F5F5)
|
||||
nameLabel.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
nameLabel.textColor = UIColor(hex: 0x002255)
|
||||
descriptionLabel.font = .systemFont(ofSize: 14)
|
||||
descriptionLabel.textColor = UIColor(hex: 0x7B8EAA)
|
||||
descriptionLabel.numberOfLines = 0
|
||||
tagStack.axis = .horizontal
|
||||
tagStack.spacing = 8
|
||||
|
||||
addSubview(titleLabel)
|
||||
addSubview(avatarView)
|
||||
addSubview(nameLabel)
|
||||
addSubview(tagStack)
|
||||
addSubview(descriptionLabel)
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalToSuperview().inset(16)
|
||||
}
|
||||
avatarView.snp.makeConstraints { make in
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(12)
|
||||
make.leading.equalToSuperview().offset(16)
|
||||
make.size.equalTo(48)
|
||||
make.bottom.lessThanOrEqualToSuperview().inset(16)
|
||||
}
|
||||
nameLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(avatarView)
|
||||
make.leading.equalTo(avatarView.snp.trailing).offset(12)
|
||||
}
|
||||
tagStack.snp.makeConstraints { make in
|
||||
make.leading.equalTo(nameLabel.snp.trailing).offset(8)
|
||||
make.centerY.equalTo(nameLabel)
|
||||
make.trailing.lessThanOrEqualToSuperview().inset(16)
|
||||
}
|
||||
descriptionLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(nameLabel.snp.bottom).offset(4)
|
||||
make.leading.equalTo(nameLabel)
|
||||
make.trailing.equalToSuperview().inset(16)
|
||||
make.bottom.equalToSuperview().inset(16)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func apply(detail: MaterialDetail) {
|
||||
nameLabel.text = detail.uploaderName ?? ""
|
||||
descriptionLabel.text = detail.uploaderDescription ?? ""
|
||||
tagStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
if detail.platformCertification {
|
||||
tagStack.addArrangedSubview(makeTag("平台认证"))
|
||||
}
|
||||
if detail.scenicCertification {
|
||||
tagStack.addArrangedSubview(makeTag("景区认证"))
|
||||
}
|
||||
if let urlText = detail.uploaderAvatar, let url = URL(string: urlText), !urlText.isEmpty {
|
||||
avatarView.kf.setImage(with: url, placeholder: UIImage(systemName: "person.crop.circle.fill"))
|
||||
} else {
|
||||
avatarView.image = UIImage(systemName: "person.crop.circle.fill")
|
||||
avatarView.tintColor = AppColor.textTertiary
|
||||
}
|
||||
}
|
||||
|
||||
private func makeTag(_ title: String) -> UILabel {
|
||||
let label = UILabel()
|
||||
label.text = title
|
||||
label.textColor = .white
|
||||
label.font = .systemFont(ofSize: 8)
|
||||
label.textAlignment = .center
|
||||
label.backgroundColor = AppColor.primary
|
||||
label.layer.cornerRadius = 4
|
||||
label.clipsToBounds = true
|
||||
label.snp.makeConstraints { make in
|
||||
make.height.equalTo(16)
|
||||
make.width.greaterThanOrEqualTo(44)
|
||||
}
|
||||
return label
|
||||
}
|
||||
}
|
||||
|
||||
/// 素材审核信息区。
|
||||
/// 素材详情的审核信息区块。
|
||||
private final class MaterialDetailReviewSection: UIView {
|
||||
private let titleLabel = UILabel()
|
||||
private let stack = UIStackView()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .white
|
||||
titleLabel.text = "审核信息"
|
||||
titleLabel.font = .systemFont(ofSize: 14, weight: .bold)
|
||||
titleLabel.textColor = .black
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 0
|
||||
addSubview(titleLabel)
|
||||
addSubview(stack)
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalToSuperview().inset(16)
|
||||
}
|
||||
stack.snp.makeConstraints { make in
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(12)
|
||||
make.leading.trailing.bottom.equalToSuperview().inset(16)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func apply(detail: MaterialDetail) {
|
||||
stack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
stack.addArrangedSubview(MaterialInfoRow(label: "审核人", value: detail.reviewer ?? "--"))
|
||||
stack.addArrangedSubview(separator())
|
||||
stack.addArrangedSubview(MaterialInfoRow(label: "审核时间", value: detail.reviewTime ?? "--"))
|
||||
stack.addArrangedSubview(separator())
|
||||
stack.addArrangedSubview(MaterialStatusRow(status: detail.status))
|
||||
stack.addArrangedSubview(separator())
|
||||
stack.addArrangedSubview(MaterialInfoRow(label: "审核备注", value: detail.reviewNotes ?? "--", valueColor: UIColor(hex: 0x4B5563), valueFont: .systemFont(ofSize: 12)))
|
||||
}
|
||||
|
||||
private func separator() -> UIView {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor(hex: 0xEEEEEE)
|
||||
view.snp.makeConstraints { make in
|
||||
make.height.equalTo(1)
|
||||
}
|
||||
return view
|
||||
}
|
||||
}
|
||||
|
||||
/// 详情统计图标与数字。
|
||||
/// 素材详情中的统计指标视图。
|
||||
private final class MaterialDetailStatView: UIView {
|
||||
private let iconView = UIImageView()
|
||||
private let countLabel = UILabel()
|
||||
|
||||
init(iconName: String? = nil, systemIconName: String? = nil) {
|
||||
super.init(frame: .zero)
|
||||
if let iconName {
|
||||
iconView.image = UIImage(named: iconName)
|
||||
} else if let systemIconName {
|
||||
iconView.image = UIImage(systemName: systemIconName)
|
||||
}
|
||||
iconView.tintColor = UIColor(hex: 0x6B7280)
|
||||
iconView.contentMode = .scaleAspectFit
|
||||
countLabel.font = .systemFont(ofSize: 14)
|
||||
countLabel.textColor = UIColor(hex: 0x6B7280)
|
||||
addSubview(iconView)
|
||||
addSubview(countLabel)
|
||||
iconView.snp.makeConstraints { make in
|
||||
make.leading.centerY.equalToSuperview()
|
||||
make.size.equalTo(16)
|
||||
}
|
||||
countLabel.snp.makeConstraints { make in
|
||||
make.leading.equalTo(iconView.snp.trailing).offset(4)
|
||||
make.trailing.centerY.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func setCount(_ count: Int) {
|
||||
countLabel.text = MaterialDisplayFormatter.formatCount(count)
|
||||
}
|
||||
}
|
||||
|
||||
/// 素材详情普通信息行。
|
||||
/// 标题和值组成的信息行。
|
||||
private final class MaterialInfoRow: UIView {
|
||||
private let label = UILabel()
|
||||
private let valueLabel = UILabel()
|
||||
|
||||
init(label: String, value: String, valueColor: UIColor = .black, valueFont: UIFont = .systemFont(ofSize: 14, weight: .medium)) {
|
||||
super.init(frame: .zero)
|
||||
self.label.text = label
|
||||
self.label.font = .systemFont(ofSize: 14)
|
||||
self.label.textColor = UIColor(hex: 0x2E3746)
|
||||
valueLabel.text = value
|
||||
valueLabel.font = valueFont
|
||||
valueLabel.textColor = valueColor
|
||||
valueLabel.textAlignment = .right
|
||||
valueLabel.numberOfLines = 1
|
||||
addSubview(self.label)
|
||||
addSubview(valueLabel)
|
||||
snp.makeConstraints { make in
|
||||
make.height.equalTo(44)
|
||||
}
|
||||
self.label.snp.makeConstraints { make in
|
||||
make.leading.centerY.equalToSuperview()
|
||||
}
|
||||
valueLabel.snp.makeConstraints { make in
|
||||
make.leading.greaterThanOrEqualTo(self.label.snp.trailing).offset(12)
|
||||
make.trailing.centerY.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
/// 素材审核状态行。
|
||||
/// 素材详情中展示上架状态的信息行。
|
||||
private final class MaterialStatusRow: UIView {
|
||||
private let label = UILabel()
|
||||
private let badge = MaterialStatusBadgeView()
|
||||
|
||||
init(status: Int) {
|
||||
super.init(frame: .zero)
|
||||
label.text = "审核状态"
|
||||
label.font = .systemFont(ofSize: 14)
|
||||
label.textColor = UIColor(hex: 0x2E3746)
|
||||
badge.apply(status: status)
|
||||
addSubview(label)
|
||||
addSubview(badge)
|
||||
snp.makeConstraints { make in
|
||||
make.height.equalTo(44)
|
||||
}
|
||||
label.snp.makeConstraints { make in
|
||||
make.leading.centerY.equalToSuperview()
|
||||
}
|
||||
badge.snp.makeConstraints { make in
|
||||
make.trailing.centerY.equalToSuperview()
|
||||
make.height.equalTo(24)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user