Files
suixinkan_uikit/suixinkan/UI/MaterialManagement/MaterialDetailViewController.swift
汉秋 005349f8e6 模块化 AppStore 并完善素材管理与个人空间设置。
将会话、权限、位置、收款与排队存储拆分为独立模块,同步更新各 ViewModel 与单元测试,并补充素材管理 UI 与个人空间设置交互。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 11:01:08 +08:00

808 lines
30 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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 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")
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")
}
}