// // 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 = 0 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(makeVerticalSpacer(height: 12)) contentStack.addArrangedSubview(uploaderSection) contentStack.addArrangedSubview(makeVerticalSpacer(height: 12)) contentStack.addArrangedSubview(reviewSection) contentStack.addArrangedSubview(makeVerticalSpacer(height: 16)) 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 } private func makeVerticalSpacer(height: CGFloat) -> UIView { let spacer = UIView() spacer.backgroundColor = .clear spacer.snp.makeConstraints { make in make.height.equalTo(height) } return spacer } @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 dialog = MaterialDeleteDialogViewController(itemName: viewModel.detail?.name ?? "") { [weak self] in guard let self else { return } Task { self.showLoading() await self.viewModel.delete(api: self.api) self.hideLoading() } } present(dialog, 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) } } /// 素材详情无限轮播的页码映射规则。 struct MaterialCarouselIndexMapper { /// 初始渲染页;多媒体时跳过头部复制页。 static func initialRenderedPage(mediaCount: Int) -> Int { mediaCount > 1 ? 1 : 0 } /// 将头尾复制页归一到对应的真实渲染页。 static func normalizedRenderedPage(_ page: Int, mediaCount: Int) -> Int { guard mediaCount > 1 else { return 0 } if page <= 0 { return mediaCount } if page >= mediaCount + 1 { return 1 } return page } /// 将渲染页映射到圆点使用的真实素材索引。 static func logicalPage(renderedPage: Int, mediaCount: Int) -> Int { guard mediaCount > 1 else { return 0 } return normalizedRenderedPage(renderedPage, mediaCount: mediaCount) - 1 } } /// 素材详情媒体轮播,支持图片自动切换与视频播完切换。 /// 素材详情顶部媒体轮播视图,支持图片与视频播放。 private final class MaterialMediaCarouselView: UIView, UIScrollViewDelegate { private let scrollView = UIScrollView() private let pageControl = UIPageControl() private let placeholderLabel = UILabel() private var media: [MaterialDetailMediaItem] = [] private var renderedMedia: [MaterialDetailMediaItem] = [] private var players: [Int: AVPlayer] = [:] private var playerViews: [Int: MaterialPlayerLayerView] = [:] private var timer: Timer? private var renderedPage = 0 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() playerViews.removeAll() self.media = media renderedMedia = media.count > 1 ? [media.last!] + media + [media.first!] : 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 } renderedPage = MaterialCarouselIndexMapper.initialRenderedPage(mediaCount: media.count) for (index, item) in renderedMedia.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() scrollView.contentOffset = CGPoint(x: CGFloat(renderedPage) * bounds.width, y: 0) 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.configure(player: player) playerViews[index] = playerView 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(renderedMedia.count), height: size.height) scrollView.contentOffset = CGPoint(x: CGFloat(renderedPage) * size.width, y: 0) } private func updateCurrentPage() { guard bounds.width > 0 else { return } let rawPage = Int(round(scrollView.contentOffset.x / bounds.width)) let page = MaterialCarouselIndexMapper.normalizedRenderedPage(rawPage, mediaCount: media.count) if media.count > 1 { if page != rawPage { scrollView.setContentOffset(CGPoint(x: CGFloat(page) * bounds.width, y: 0), animated: false) } pageControl.currentPage = MaterialCarouselIndexMapper.logicalPage(renderedPage: page, mediaCount: media.count) } else { pageControl.currentPage = 0 } renderedPage = page playCurrentPageIfNeeded() } private func playCurrentPageIfNeeded() { stopTimer() players.forEach { index, player in if index == renderedPage { player.seek(to: .zero) player.play() playerViews[index]?.setPlaying(true) } else { player.pause() playerViews[index]?.setPlaying(false) } } guard renderedMedia.indices.contains(renderedPage) else { return } if renderedMedia[renderedPage].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) { guard players[renderedPage]?.currentItem === notification.object as? AVPlayerItem else { return } advancePage() } private func advancePage() { guard media.count > 1, bounds.width > 0 else { return } let nextPage = renderedPage + 1 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 let controlButton = UIButton(type: .system) private weak var player: AVPlayer? private var hideWorkItem: DispatchWorkItem? override init(frame: CGRect) { super.init(frame: frame) controlButton.tintColor = .white controlButton.backgroundColor = UIColor.black.withAlphaComponent(0.5) controlButton.layer.cornerRadius = 24 controlButton.addTarget(self, action: #selector(controlTapped), for: .touchUpInside) addSubview(controlButton) addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(showControls))) controlButton.snp.makeConstraints { make in make.center.equalToSuperview(); make.size.equalTo(48) } } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func configure(player: AVPlayer) { self.player = player playerLayer.player = player playerLayer.videoGravity = .resizeAspect setPlaying(false) } func setPlaying(_ playing: Bool) { controlButton.setImage(UIImage(systemName: playing ? "pause.fill" : "play.fill"), for: .normal) if playing { scheduleHide() } else { hideWorkItem?.cancel() controlButton.isHidden = false } } @objc private func controlTapped() { guard let player else { return } if player.timeControlStatus == .playing { player.pause() setPlaying(false) } else { player.play() setPlaying(true) } } @objc private func showControls() { controlButton.isHidden = false if player?.timeControlStatus == .playing { scheduleHide() } } private func scheduleHide() { hideWorkItem?.cancel() controlButton.isHidden = false let work = DispatchWorkItem { [weak self] in self?.controlButton.isHidden = true } hideWorkItem = work DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: work) } } /// 素材详情基础信息区。 /// 素材详情的基础信息区块。 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(iconName: "material_ic_download_count") private let likeStat = MaterialDetailStatView(iconName: "sample_ic_like_count") private let collectStat = MaterialDetailStatView(iconName: "sample_ic_collect_count") private var statsTopConstraint: Constraint? 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 = .center 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 statsTopConstraint = make.top.equalTo(tagStack.snp.bottom).offset(12).constraint 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() } let tags = detail.materialTag ?? [] tags.forEach { tagStack.addArrangedSubview(makeTag($0.name)) } if !tags.isEmpty { tagStack.addArrangedSubview(UIView()) } tagStack.isHidden = tags.isEmpty statsTopConstraint?.update(offset: tags.isEmpty ? 0 : 12) downloadStat.setCount(detail.downloadCount) likeStat.setCount(detail.likesCount) collectStat.setCount(detail.collectCount) } private func makeTag(_ title: String) -> UILabel { let label = MaterialDetailTagLabel(horizontalInset: 6) 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.setContentHuggingPriority(.required, for: .horizontal) label.setContentCompressionResistancePriority(.required, for: .horizontal) label.snp.makeConstraints { make in make.height.equalTo(22) } 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 = MaterialDetailTagLabel(horizontalInset: 6) 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) } return label } } /// 为详情页标签文本提供固定水平内边距,使背景宽度随内容自适应。 private final class MaterialDetailTagLabel: UILabel { private let horizontalInset: CGFloat init(horizontalInset: CGFloat) { self.horizontalInset = horizontalInset super.init(frame: .zero) } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func drawText(in rect: CGRect) { super.drawText(in: rect.insetBy(dx: horizontalInset, dy: 0)) } override var intrinsicContentSize: CGSize { let size = super.intrinsicContentSize return CGSize(width: size.width + horizontalInset * 2, height: size.height) } } /// 素材审核信息区。 /// 素材详情的审核信息区块。 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") } }