Files
suixinkan_uikit/suixinkan/UI/TravelAlbum/TravelAlbumEntryViewController.swift

445 lines
17 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.

//
// TravelAlbumEntryViewController.swift
// suixinkan
//
import Kingfisher
import SnapKit
import UIKit
/// Android `TravelAlbumEntryScreen`
final class TravelAlbumEntryViewController: BaseViewController {
private let viewModel = TravelAlbumEntryViewModel()
private let api: any TravelAlbumServing
private let heroCard = TravelAlbumHeroCard()
private let titleLabel = UILabel()
private let tableView = UITableView(frame: .zero, style: .plain)
private let emptyView = TravelAlbumEmptyView()
private var dataSource: UITableViewDiffableDataSource<Int, TravelAlbum>!
init(api: (any TravelAlbumServing)? = nil) {
self.api = api ?? NetworkServices.shared.travelAlbumAPI
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func setupNavigationBar() {
title = "新增相册"
navigationController?.setNavigationBarHidden(false, animated: false)
}
override func setupUI() {
view.backgroundColor = .white
titleLabel.font = .systemFont(ofSize: 18, weight: .bold)
titleLabel.textColor = .black
tableView.backgroundColor = .white
tableView.separatorStyle = .none
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 148
tableView.delegate = self
tableView.register(TravelAlbumTaskCell.self, forCellReuseIdentifier: TravelAlbumTaskCell.reuseIdentifier)
dataSource = UITableViewDiffableDataSource<Int, TravelAlbum>(tableView: tableView) {
[weak self] tableView, indexPath, album in
let cell = tableView.dequeueReusableCell(
withIdentifier: TravelAlbumTaskCell.reuseIdentifier,
for: indexPath
) as! TravelAlbumTaskCell
cell.apply(album: album)
cell.onShootUpload = { [weak self] in self?.pushWiredTransfer(album: album) }
cell.onAlbumCode = { [weak self] in self?.loadAlbumCode(album) }
return cell
}
view.addSubview(heroCard)
view.addSubview(titleLabel)
view.addSubview(tableView)
view.addSubview(emptyView)
}
override func setupConstraints() {
heroCard.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide).offset(20)
make.leading.trailing.equalToSuperview().inset(20)
make.height.equalTo(108)
}
titleLabel.snp.makeConstraints { make in
make.top.equalTo(heroCard.snp.bottom).offset(28)
make.leading.trailing.equalToSuperview().inset(20)
}
tableView.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(8)
make.leading.trailing.bottom.equalToSuperview()
}
emptyView.snp.makeConstraints { make in
make.top.equalTo(heroCard.snp.bottom)
make.leading.trailing.bottom.equalToSuperview()
}
}
override func bindActions() {
heroCard.addTarget(self, action: #selector(createTapped), for: .touchUpInside)
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.loadAlbums(api: api) }
}
@MainActor
private func applyViewModel() {
heroCard.isLoading = viewModel.isCreating
titleLabel.text = "我的任务(\(viewModel.albumTotal)"
titleLabel.isHidden = viewModel.albums.isEmpty
tableView.isHidden = viewModel.albums.isEmpty
emptyView.isHidden = !viewModel.albums.isEmpty || viewModel.isLoading
var snapshot = NSDiffableDataSourceSnapshot<Int, TravelAlbum>()
snapshot.appendSections([0])
snapshot.appendItems(viewModel.albums)
dataSource.apply(snapshot, animatingDifferences: true)
if viewModel.isLoading && viewModel.albums.isEmpty {
showLoading()
} else {
hideLoading()
}
}
@objc private func createTapped() {
Task {
await viewModel.openCreateSheet(api: api)
await MainActor.run {
guard viewModel.isCreateSheetVisible, presentedViewController == nil else { return }
let sheet = CreateTravelAlbumSheetViewController(viewModel: viewModel, api: api)
sheet.onDismissed = { [weak self] in self?.viewModel.dismissCreateSheet() }
present(sheet, animated: true)
}
}
}
private func loadAlbumCode(_ album: TravelAlbum) {
Task {
await viewModel.openAlbumCode(album: album, api: api)
await MainActor.run { self.presentAlbumCodeIfNeeded() }
}
}
@MainActor
private func presentAlbumCodeIfNeeded() {
guard let state = viewModel.albumCodeState else { return }
let controller = TravelAlbumCodeDialogViewController(state: state)
controller.onClose = { [weak self] in self?.viewModel.dismissAlbumCode() }
controller.onDownload = { [weak self, weak controller] in
guard let self, let url = self.viewModel.albumCodeState?.qrImageUrl, !url.isEmpty else { return }
self.viewModel.setDownloadingQRCode(true)
controller?.updateDownloading(true)
Task {
do {
try await ScenicQueueQRCodeSaver.saveImage(from: url)
await MainActor.run { self.showToast("已保存到相册") }
} catch {
await MainActor.run { self.showToast(error.localizedDescription) }
}
await MainActor.run {
self.viewModel.setDownloadingQRCode(false)
controller?.updateDownloading(false)
}
}
}
present(controller, animated: true)
}
private func pushWiredTransfer(album: TravelAlbum) {
let controller = WiredCameraTransferViewController(
viewModel: WiredCameraTransferViewModel(
albumId: album.id,
albumTitle: album.name,
headerPhone: album.displayPhone
)
)
navigationController?.pushViewController(controller, animated: true)
}
}
extension TravelAlbumEntryViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
guard let album = dataSource.itemIdentifier(for: indexPath) else { return }
navigationController?.pushViewController(TravelAlbumDetailViewController(albumId: album.id), animated: true)
}
}
///
private final class TravelAlbumHeroCard: UIControl {
private let gradientLayer = CAGradientLayer()
private let plusView = UIView()
private let plusImageView = UIImageView(image: UIImage(systemName: "plus"))
private let titleLabel = UILabel()
private let subtitleLabel = UILabel()
private let arrowLabel = UILabel()
var isLoading = false {
didSet { titleLabel.text = isLoading ? "新建中..." : "新建相册任务" }
}
override init(frame: CGRect) {
super.init(frame: frame)
layer.cornerRadius = 10
clipsToBounds = true
gradientLayer.colors = [UIColor(hex: 0x007BFF).cgColor, UIColor(hex: 0x3A91FF).cgColor]
gradientLayer.startPoint = CGPoint(x: 0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
layer.insertSublayer(gradientLayer, at: 0)
plusView.backgroundColor = .white
plusView.layer.cornerRadius = 17
plusView.clipsToBounds = true
plusImageView.tintColor = AppColor.primary
plusImageView.contentMode = .scaleAspectFit
titleLabel.text = "新建相册任务"
titleLabel.font = .systemFont(ofSize: 19, weight: .medium)
titleLabel.textColor = .white
subtitleLabel.text = "创建一个新的相册拍摄任务"
subtitleLabel.font = .systemFont(ofSize: 14)
subtitleLabel.textColor = .white
arrowLabel.text = ""
arrowLabel.textColor = .white
arrowLabel.font = .systemFont(ofSize: 36, weight: .light)
addSubview(plusView)
plusView.addSubview(plusImageView)
addSubview(titleLabel)
addSubview(subtitleLabel)
addSubview(arrowLabel)
plusView.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(28)
make.centerY.equalToSuperview()
make.size.equalTo(34)
}
plusImageView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(26)
}
titleLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(30)
make.leading.equalTo(plusView.snp.trailing).offset(16)
make.trailing.lessThanOrEqualTo(arrowLabel.snp.leading).offset(-8)
}
subtitleLabel.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(8)
make.leading.equalTo(titleLabel)
make.trailing.lessThanOrEqualTo(arrowLabel.snp.leading).offset(-8)
}
arrowLabel.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-18)
make.centerY.equalToSuperview()
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
gradientLayer.frame = bounds
}
}
/// cell
private final class TravelAlbumTaskCell: UITableViewCell {
static let reuseIdentifier = "TravelAlbumTaskCell"
var onShootUpload: (() -> Void)?
var onAlbumCode: (() -> Void)?
private let cardView = UIView()
private let coverView = UIImageView()
private let iconView = UIImageView(image: UIImage(systemName: "checklist"))
private let nameLabel = UILabel()
private let timeLabel = UILabel()
private let phoneLabel = UILabel()
private let codeButton = UIButton(type: .system)
private let uploadButton = UIButton(type: .system)
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
contentView.backgroundColor = .white
cardView.backgroundColor = .white
cardView.layer.cornerRadius = 10
cardView.layer.shadowColor = UIColor(hex: 0x2F6BFF).withAlphaComponent(0.10).cgColor
cardView.layer.shadowOpacity = 1
cardView.layer.shadowRadius = 10
cardView.layer.shadowOffset = CGSize(width: 0, height: 4)
coverView.backgroundColor = UIColor(hex: 0xEAF2FF)
coverView.layer.cornerRadius = 8
coverView.clipsToBounds = true
coverView.contentMode = .scaleAspectFill
iconView.tintColor = AppColor.primary
iconView.contentMode = .scaleAspectFit
nameLabel.font = .systemFont(ofSize: 16, weight: .bold)
nameLabel.textColor = AppColor.textPrimary
nameLabel.numberOfLines = 1
timeLabel.font = .systemFont(ofSize: 12)
timeLabel.textColor = AppColor.textTertiary
timeLabel.numberOfLines = 1
phoneLabel.font = .systemFont(ofSize: 12)
phoneLabel.textColor = AppColor.textTertiary
phoneLabel.numberOfLines = 1
phoneLabel.setContentCompressionResistancePriority(.required, for: .vertical)
codeButton.setImage(UIImage(systemName: "qrcode"), for: .normal)
codeButton.tintColor = .black
uploadButton.setImage(UIImage(systemName: "camera.fill"), for: .normal)
uploadButton.setTitle(" 拍摄上传", for: .normal)
uploadButton.tintColor = AppColor.primary
uploadButton.setTitleColor(AppColor.primary, for: .normal)
uploadButton.titleLabel?.font = .systemFont(ofSize: 13, weight: .bold)
uploadButton.backgroundColor = UIColor(hex: 0xEAF4FF)
uploadButton.layer.cornerRadius = 8
contentView.addSubview(cardView)
cardView.addSubview(coverView)
coverView.addSubview(iconView)
cardView.addSubview(nameLabel)
cardView.addSubview(timeLabel)
cardView.addSubview(phoneLabel)
cardView.addSubview(codeButton)
cardView.addSubview(uploadButton)
cardView.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 6, left: 20, bottom: 6, right: 20))
}
coverView.snp.makeConstraints { make in
make.leading.top.bottom.equalToSuperview().inset(12)
make.size.equalTo(112)
}
iconView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(42)
}
codeButton.snp.makeConstraints { make in
make.top.equalToSuperview().offset(12)
make.trailing.equalToSuperview().offset(-10)
make.size.equalTo(26)
}
nameLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(14)
make.leading.equalTo(coverView.snp.trailing).offset(12)
make.trailing.equalTo(codeButton.snp.leading).offset(-8)
}
timeLabel.snp.makeConstraints { make in
make.top.equalTo(nameLabel.snp.bottom).offset(7)
make.leading.equalTo(nameLabel)
make.trailing.equalToSuperview().offset(-12)
}
phoneLabel.snp.makeConstraints { make in
make.top.equalTo(timeLabel.snp.bottom).offset(5)
make.leading.equalTo(nameLabel)
make.trailing.equalToSuperview().offset(-12)
}
uploadButton.snp.makeConstraints { make in
make.top.equalTo(phoneLabel.snp.bottom).offset(9)
make.trailing.equalToSuperview().offset(-10)
make.bottom.equalToSuperview().offset(-12)
make.height.equalTo(34)
make.width.greaterThanOrEqualTo(104)
}
uploadButton.addTarget(self, action: #selector(uploadTapped), for: .touchUpInside)
codeButton.addTarget(self, action: #selector(codeTapped), for: .touchUpInside)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(album: TravelAlbum) {
nameLabel.text = album.name.isEmpty ? "未命名任务" : album.name
timeLabel.text = "创建时间:\(TravelAlbumDisplayFormatter.shortTaskTime(album.createdAt))"
phoneLabel.text = "手机号:\(album.displayPhone.isEmpty ? "--" : album.displayPhone)"
if let url = URL(string: album.coverUrl), !album.coverUrl.isEmpty {
iconView.isHidden = true
coverView.kf.setImage(with: url)
} else {
coverView.image = nil
iconView.isHidden = false
}
}
@objc private func uploadTapped() { onShootUpload?() }
@objc private func codeTapped() { onAlbumCode?() }
}
///
private final class TravelAlbumEmptyView: UIView {
private let imageView = UIImageView(image: UIImage(systemName: "photo.on.rectangle.angled"))
private let titleLabel = UILabel()
private let subtitleLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
isHidden = true
imageView.tintColor = AppColor.primary
imageView.contentMode = .scaleAspectFit
titleLabel.text = "暂无任务"
titleLabel.font = .systemFont(ofSize: 18, weight: .bold)
titleLabel.textColor = .black
titleLabel.textAlignment = .center
subtitleLabel.text = "点击“新建相册任务”开始创建任务"
subtitleLabel.font = .systemFont(ofSize: 14)
subtitleLabel.textColor = AppColor.textTertiary
subtitleLabel.textAlignment = .center
let backdrop = UIView()
backdrop.backgroundColor = UIColor(hex: 0xF2F7FF)
backdrop.layer.cornerRadius = 54
addSubview(backdrop)
addSubview(imageView)
addSubview(titleLabel)
addSubview(subtitleLabel)
backdrop.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.centerY.equalToSuperview().offset(-70)
make.width.equalToSuperview().multipliedBy(0.58)
make.height.equalTo(108)
}
imageView.snp.makeConstraints { make in
make.center.equalTo(backdrop)
make.size.equalTo(82)
}
titleLabel.snp.makeConstraints { make in
make.top.equalTo(backdrop.snp.bottom).offset(22)
make.leading.trailing.equalToSuperview().inset(20)
}
subtitleLabel.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(10)
make.leading.trailing.equalTo(titleLabel)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}