Files
suixinkan_uikit/suixinkan/UI/SampleManagement/SampleDetailViewController.swift
汉秋 6336feff27 完善景区排队设置页与全局按钮配置迁移。
重构排队设置与变更日志交互,补充 Overlay 与资源,并将多页面主操作按钮统一到 UIButton Configuration,同步更新相关单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 16:21:53 +08:00

826 lines
30 KiB
Swift
Raw Permalink 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.

//
// SampleDetailViewController.swift
// suixinkan
//
import AVFoundation
import Kingfisher
import SnapKit
import UIKit
/// Android `SampleDetailScreen`
final class SampleDetailViewController: SampleBaseViewController {
private let viewModel: SampleDetailViewModel
private let api: any SampleManagementServing
private let scrollView = UIScrollView()
private let contentStack = UIStackView()
private let carouselView = SampleMediaCarouselView()
private let sampleInfoSection = SampleDetailSampleInfoSection()
private let uploaderSection = SampleDetailUploaderSection()
private let scenicSection = SampleDetailImageInfoSection(title: "打卡地点")
private let projectSection = SampleDetailImageInfoSection(title: "样片绑定套餐")
private let reviewSection = SampleDetailReviewSection()
private let bottomSpacer = UIView()
///
init(
sampleId: Int,
api: (any SampleManagementServing)? = nil
) {
viewModel = SampleDetailViewModel(sampleId: sampleId)
self.api = api ?? NetworkServices.shared.sampleManagementAPI
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func setupNavigationBar() {
super.setupNavigationBar()
setSampleTitle("样片详情")
}
override func setupUI() {
super.setupUI()
view.backgroundColor = SampleManagementUITokens.pageBackground
contentStack.axis = .vertical
contentStack.spacing = 0
scrollView.backgroundColor = SampleManagementUITokens.pageBackground
view.addSubview(scrollView)
scrollView.addSubview(contentStack)
contentStack.addArrangedSubview(carouselView)
contentStack.addArrangedSubview(sampleInfoSection)
contentStack.addArrangedSubview(uploaderSection)
contentStack.addArrangedSubview(scenicSection)
contentStack.addArrangedSubview(projectSection)
contentStack.addArrangedSubview(reviewSection)
contentStack.addArrangedSubview(bottomSpacer)
contentStack.setCustomSpacing(12, after: sampleInfoSection)
contentStack.setCustomSpacing(12, after: uploaderSection)
contentStack.setCustomSpacing(12, after: scenicSection)
contentStack.setCustomSpacing(12, after: projectSection)
bottomSpacer.snp.makeConstraints { make in make.height.equalTo(16) }
}
override func setupConstraints() {
scrollView.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide)
make.leading.trailing.bottom.equalTo(view.safeAreaLayoutGuide)
}
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) }
}
}
override func viewDidLoad() {
super.viewDidLoad()
Task { await viewModel.load(api: api) }
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
carouselView.pause()
}
private func applyViewModel() {
if viewModel.isLoading && viewModel.detail == nil {
showLoading()
} else {
hideLoading()
}
guard let detail = viewModel.detail else { return }
carouselView.apply(media: detail.mediaList ?? [], fallbackType: detail.type ?? 1)
sampleInfoSection.apply(detail: detail)
uploaderSection.apply(detail: detail)
scenicSection.applyScenic(detail: detail)
projectSection.applyProject(detail: detail)
reviewSection.apply(detail: detail)
}
}
///
private final class SampleMediaCarouselView: UIView, UIScrollViewDelegate {
private let scrollView = UIScrollView()
private let indicatorStack = UIStackView()
private let placeholderLabel = UILabel()
private var media: [SampleDetailMediaItem] = []
private var renderedMediaIndices: [Int] = []
private var currentRenderedPage = 0
private var fallbackType = 1
private var pageViews: [UIView] = []
private var players: [Int: AVPlayer] = [:]
private var playerViews: [Int: SamplePlayerLayerView] = [:]
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
indicatorStack.axis = .horizontal
indicatorStack.spacing = 6
indicatorStack.alignment = .center
placeholderLabel.text = "暂无媒体"
placeholderLabel.font = .systemFont(ofSize: 14)
placeholderLabel.textColor = AppColor.textSecondary
placeholderLabel.textAlignment = .center
addSubview(scrollView)
addSubview(indicatorStack)
addSubview(placeholderLabel)
scrollView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
indicatorStack.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.bottom.equalToSuperview().inset(12)
make.height.equalTo(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: [SampleDetailMediaItem], fallbackType: Int) {
stopTimer()
NotificationCenter.default.removeObserver(self)
players.values.forEach { $0.pause() }
players.removeAll()
playerViews.removeAll()
pageViews.removeAll()
self.media = media
self.fallbackType = fallbackType
renderedMediaIndices = media.count > 1
? [media.count - 1] + Array(media.indices) + [0]
: Array(media.indices)
currentRenderedPage = media.count > 1 ? 1 : 0
scrollView.subviews.forEach { $0.removeFromSuperview() }
rebuildIndicators(currentIndex: 0)
indicatorStack.isHidden = media.count <= 1
placeholderLabel.isHidden = !media.isEmpty
scrollView.isHidden = media.isEmpty
guard !media.isEmpty else { return }
for (renderedIndex, mediaIndex) in renderedMediaIndices.enumerated() {
let item = media[mediaIndex]
let page = UIView()
page.backgroundColor = UIColor(hex: 0xF5F5F5)
page.tag = renderedIndex
scrollView.addSubview(page)
pageViews.append(page)
if item.resolvedType(fallback: fallbackType) == 2 {
addVideo(urlText: item.displayURL, to: page, index: renderedIndex)
} 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()
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
stopTimer()
}
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, placeholder: UIImage(named: "sample_img_loading")) { result in
if case .failure = result {
imageView.image = UIImage(named: "sample_img_error")
}
}
} else {
imageView.image = UIImage(named: "sample_img_error")
}
}
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 = SamplePlayerLayerView()
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 pageViews.enumerated() {
page.frame = CGRect(x: CGFloat(index) * size.width, y: 0, width: size.width, height: size.height)
}
scrollView.contentSize = CGSize(width: size.width * CGFloat(renderedMediaIndices.count), height: size.height)
scrollView.contentOffset = CGPoint(x: CGFloat(currentRenderedPage) * size.width, y: 0)
}
private func updateCurrentPage() {
guard bounds.width > 0, !renderedMediaIndices.isEmpty else { return }
let page = Int(round(scrollView.contentOffset.x / bounds.width))
currentRenderedPage = max(0, min(page, renderedMediaIndices.count - 1))
if media.count > 1, currentRenderedPage == 0 {
currentRenderedPage = media.count
scrollView.setContentOffset(CGPoint(x: CGFloat(currentRenderedPage) * bounds.width, y: 0), animated: false)
} else if media.count > 1, currentRenderedPage == renderedMediaIndices.count - 1 {
currentRenderedPage = 1
scrollView.setContentOffset(CGPoint(x: bounds.width, y: 0), animated: false)
}
rebuildIndicators(currentIndex: currentMediaIndex)
playCurrentPageIfNeeded()
}
private func playCurrentPageIfNeeded() {
stopTimer()
players.forEach { index, player in
if index == currentRenderedPage {
player.seek(to: .zero)
player.play()
playerViews[index]?.setPlaying(true)
} else {
player.pause()
playerViews[index]?.setPlaying(false)
}
}
guard !media.isEmpty else { return }
if media[currentMediaIndex].resolvedType(fallback: fallbackType) == 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 let item = notification.object as? AVPlayerItem,
players[currentRenderedPage]?.currentItem === item else { return }
advancePage()
}
private func advancePage() {
guard media.count > 1, bounds.width > 0 else { return }
let nextPage = min(currentRenderedPage + 1, renderedMediaIndices.count - 1)
currentRenderedPage = nextPage
scrollView.setContentOffset(CGPoint(x: CGFloat(nextPage) * bounds.width, y: 0), animated: true)
if !scrollView.isDragging && !scrollView.isDecelerating {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in
self?.playCurrentPageIfNeeded()
}
}
}
private var currentMediaIndex: Int {
guard renderedMediaIndices.indices.contains(currentRenderedPage) else { return 0 }
return renderedMediaIndices[currentRenderedPage]
}
private func rebuildIndicators(currentIndex: Int) {
indicatorStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
guard media.count > 1 else { return }
for index in media.indices {
let dot = UIView()
let size: CGFloat = index == currentIndex ? 8 : 6
dot.backgroundColor = index == currentIndex ? .white : UIColor.white.withAlphaComponent(0.5)
dot.layer.cornerRadius = size / 2
dot.snp.makeConstraints { make in make.size.equalTo(size) }
indicatorStack.addArrangedSubview(dot)
}
}
}
/// AVPlayerLayer
private final class SamplePlayerLayerView: UIView {
private let controlButton = UIButton(type: .system)
private weak var player: AVPlayer?
private var hideControlWorkItem: DispatchWorkItem?
override class var layerClass: AnyClass { AVPlayerLayer.self }
var playerLayer: AVPlayerLayer { layer as! AVPlayerLayer }
override init(frame: CGRect) {
super.init(frame: frame)
controlButton.tintColor = .white
controlButton.backgroundColor = UIColor.black.withAlphaComponent(0.45)
controlButton.layer.cornerRadius = 22
controlButton.isHidden = true
controlButton.accessibilityLabel = "播放或暂停视频"
addSubview(controlButton)
controlButton.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(44)
}
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(showControls)))
controlButton.addTarget(self, action: #selector(togglePlayback), for: .touchUpInside)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
///
func configure(player: AVPlayer) {
self.player = player
playerLayer.player = player
updateControlImage()
}
///
func setPlaying(_ playing: Bool) {
if !playing { controlButton.isHidden = true }
updateControlImage()
}
@objc private func showControls() {
controlButton.isHidden = false
updateControlImage()
hideControlWorkItem?.cancel()
let workItem = DispatchWorkItem { [weak self] in self?.controlButton.isHidden = true }
hideControlWorkItem = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: workItem)
}
@objc private func togglePlayback() {
guard let player else { return }
if player.rate == 0 {
player.play()
} else {
player.pause()
}
showControls()
}
private func updateControlImage() {
let name = player?.rate == 0 ? "play.fill" : "pause.fill"
controlButton.setImage(UIImage(systemName: name), for: .normal)
}
}
///
private final class SampleDetailSampleInfoSection: UIView {
private let titleLabel = UILabel()
private let listingBadge = UILabel()
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
addSubview(titleLabel)
addSubview(listingBadge)
titleLabel.snp.makeConstraints { make in
make.top.bottom.equalToSuperview().inset(16)
make.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)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(detail: SampleDetail) {
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)
}
}
///
private final class SampleDetailUploaderSection: 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: 14, weight: .bold)
titleLabel.textColor = .black
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: SampleDetail) {
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 SampleDetailImageInfoSection: UIView {
private let sectionTitleLabel = UILabel()
private let imageView = UIImageView()
private let titleLabel = UILabel()
private let firstLineLabel = UILabel()
private let secondLineLabel = UILabel()
init(title: String) {
super.init(frame: .zero)
backgroundColor = .white
sectionTitleLabel.text = title
sectionTitleLabel.font = .systemFont(ofSize: 14, weight: .bold)
sectionTitleLabel.textColor = .black
imageView.backgroundColor = UIColor(hex: 0xF5F5F5)
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.layer.cornerRadius = 12
titleLabel.font = .systemFont(ofSize: title == "打卡地点" ? 14 : 16, weight: .bold)
titleLabel.textColor = .black
[firstLineLabel, secondLineLabel].forEach { label in
label.font = .systemFont(ofSize: 12)
label.textColor = UIColor(hex: 0x4B5563)
label.numberOfLines = 1
}
addSubview(sectionTitleLabel)
addSubview(imageView)
addSubview(titleLabel)
addSubview(firstLineLabel)
addSubview(secondLineLabel)
sectionTitleLabel.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview().inset(16)
}
imageView.snp.makeConstraints { make in
make.top.equalTo(sectionTitleLabel.snp.bottom).offset(12)
make.leading.equalToSuperview().offset(16)
make.height.equalTo(100)
make.width.equalTo(179)
make.bottom.equalToSuperview().inset(16)
}
titleLabel.snp.makeConstraints { make in
make.top.equalTo(imageView).offset(2)
make.leading.equalTo(imageView.snp.trailing).offset(12)
make.trailing.equalToSuperview().inset(16)
}
firstLineLabel.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(12)
make.leading.trailing.equalTo(titleLabel)
}
secondLineLabel.snp.makeConstraints { make in
make.top.equalTo(firstLineLabel.snp.bottom).offset(7)
make.leading.trailing.equalTo(titleLabel)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func applyScenic(detail: SampleDetail) {
titleLabel.text = detail.scenicName ?? ""
firstLineLabel.attributedText = coloredValue(prefix: "无人机: ", value: "\(detail.scenicPlanesNum)")
secondLineLabel.attributedText = coloredValue(prefix: "摄影师: ", value: "\(detail.scenicPhotographerNum)")
let shouldShowCounts = detail.scenicPlanesNum > 0 || detail.scenicPhotographerNum > 0
firstLineLabel.isHidden = !shouldShowCounts
secondLineLabel.isHidden = !shouldShowCounts
setImage(detail.scenicCoverImg)
}
func applyProject(detail: SampleDetail) {
titleLabel.text = detail.projectName ?? ""
firstLineLabel.isHidden = detail.projectPrice == nil
secondLineLabel.isHidden = detail.projectPriceDeposit == nil
if let price = detail.projectPrice {
firstLineLabel.attributedText = coloredValue(prefix: "价格: ", value: "\(price)")
} else {
firstLineLabel.text = ""
}
if let deposit = detail.projectPriceDeposit {
secondLineLabel.attributedText = coloredValue(prefix: "定金: ", value: "\(deposit)")
} else {
secondLineLabel.text = ""
}
setImage(detail.projectCover)
}
private func setImage(_ urlText: String?) {
if let urlText, let url = URL(string: urlText), !urlText.isEmpty {
imageView.kf.setImage(with: url, placeholder: UIImage(named: "sample_img_loading")) { [weak imageView] result in
if case .failure = result {
imageView?.image = UIImage(named: "sample_img_error")
}
}
} else {
imageView.image = UIImage(named: "sample_img_error")
}
}
private func coloredValue(prefix: String, value: String) -> NSAttributedString {
let text = NSMutableAttributedString(
string: prefix,
attributes: [.foregroundColor: UIColor(hex: 0x4B5563), .font: UIFont.systemFont(ofSize: 12)]
)
text.append(
NSAttributedString(
string: value,
attributes: [.foregroundColor: AppColor.primary, .font: UIFont.systemFont(ofSize: 12)]
)
)
return text
}
}
///
private final class SampleDetailReviewSection: UIView {
private let stack = UIStackView()
private let titleLabel = UILabel()
private let reviewerRow = SampleDetailInfoRow()
private let reviewTimeRow = SampleDetailInfoRow()
private let statusRow = SampleDetailStatusRow()
private let noteRow = SampleDetailInfoRow()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
stack.axis = .vertical
stack.spacing = 0
titleLabel.text = "审核信息"
titleLabel.font = .systemFont(ofSize: 14, weight: .bold)
titleLabel.textColor = .black
addSubview(stack)
stack.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(16)
}
stack.addArrangedSubview(titleLabel)
[reviewerRow, reviewTimeRow, statusRow, noteRow].forEach { row in
stack.addArrangedSubview(row)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(detail: SampleDetail) {
reviewerRow.apply(label: "审核人", value: detail.reviewer?.nonEmpty ?? "--")
reviewTimeRow.apply(label: "审核时间", value: detail.reviewTime?.nonEmpty ?? "--")
statusRow.apply(status: detail.status)
noteRow.apply(label: "审核备注", value: detail.reviewNotes?.nonEmpty ?? "--", valueColor: UIColor(hex: 0x4B5563), valueFont: .systemFont(ofSize: 12))
noteRow.setSeparatorHidden(true)
}
}
///
private final class SampleDetailInfoRow: UIView {
private let label = UILabel()
private let valueLabel = UILabel()
private let separator = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
label.font = .systemFont(ofSize: 14)
label.textColor = UIColor(hex: 0x2E3746)
valueLabel.font = .systemFont(ofSize: 14, weight: .medium)
valueLabel.textColor = .black
valueLabel.textAlignment = .right
valueLabel.numberOfLines = 1
separator.backgroundColor = UIColor(hex: 0xEEEEEE)
addSubview(label)
addSubview(valueLabel)
addSubview(separator)
snp.makeConstraints { make in
make.height.equalTo(44)
}
label.snp.makeConstraints { make in
make.leading.centerY.equalToSuperview()
}
valueLabel.snp.makeConstraints { make in
make.trailing.centerY.equalToSuperview()
make.leading.greaterThanOrEqualTo(label.snp.trailing).offset(12)
}
separator.snp.makeConstraints { make in
make.leading.trailing.bottom.equalToSuperview()
make.height.equalTo(1)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(label: String, value: String, valueColor: UIColor = .black, valueFont: UIFont = .systemFont(ofSize: 14, weight: .medium)) {
self.label.text = label
valueLabel.text = value
valueLabel.textColor = valueColor
valueLabel.font = valueFont
}
/// 线
func setSeparatorHidden(_ hidden: Bool) {
separator.isHidden = hidden
}
}
///
private final class SampleDetailStatusRow: UIView {
private let label = UILabel()
private let badge = SampleStatusBadgeView()
private let separator = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
label.text = "审核状态"
label.font = .systemFont(ofSize: 14)
label.textColor = UIColor(hex: 0x2E3746)
separator.backgroundColor = UIColor(hex: 0xEEEEEE)
addSubview(label)
addSubview(badge)
addSubview(separator)
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)
}
separator.snp.makeConstraints { make in
make.leading.trailing.bottom.equalToSuperview()
make.height.equalTo(1)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(status: Int) {
badge.apply(status: status)
}
}
private extension String {
var nonEmpty: String? { isEmpty ? nil : self }
}