457 lines
18 KiB
Swift
457 lines
18 KiB
Swift
//
|
||
// 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 = NetworkServices.shared.travelAlbumAPI) {
|
||
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 = "新增相册"
|
||
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 = 132
|
||
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) }
|
||
cell.onShare = { [weak self] in self?.showShareSheet() }
|
||
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 showShareSheet() {
|
||
let sheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||
sheet.addAction(UIAlertAction(title: "微信好友", style: .default) { [weak self] _ in
|
||
self?.showToast("已选择微信好友")
|
||
})
|
||
sheet.addAction(UIAlertAction(title: "QQ好友", style: .default) { [weak self] _ in
|
||
self?.showToast("已选择QQ好友")
|
||
})
|
||
sheet.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||
present(sheet, 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 = 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.tintColor = AppColor.primary
|
||
plusView.contentMode = .scaleAspectFit
|
||
plusView.backgroundColor = .white
|
||
plusView.layer.cornerRadius = 17
|
||
|
||
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)
|
||
addSubview(titleLabel)
|
||
addSubview(subtitleLabel)
|
||
addSubview(arrowLabel)
|
||
plusView.snp.makeConstraints { make in
|
||
make.leading.equalToSuperview().offset(28)
|
||
make.centerY.equalToSuperview()
|
||
make.size.equalTo(34)
|
||
}
|
||
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)?
|
||
var onShare: (() -> 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 shareButton = UIButton(type: .system)
|
||
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
|
||
timeLabel.font = .systemFont(ofSize: 12)
|
||
timeLabel.textColor = AppColor.textTertiary
|
||
phoneLabel.font = .systemFont(ofSize: 12)
|
||
phoneLabel.textColor = AppColor.textTertiary
|
||
|
||
shareButton.setImage(UIImage(systemName: "square.and.arrow.up"), for: .normal)
|
||
shareButton.tintColor = AppColor.primary
|
||
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(shareButton)
|
||
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(10)
|
||
make.size.equalTo(86)
|
||
}
|
||
iconView.snp.makeConstraints { make in
|
||
make.center.equalToSuperview()
|
||
make.size.equalTo(42)
|
||
}
|
||
shareButton.snp.makeConstraints { make in
|
||
make.top.equalToSuperview().offset(12)
|
||
make.trailing.equalTo(codeButton.snp.leading).offset(-8)
|
||
make.size.equalTo(26)
|
||
}
|
||
codeButton.snp.makeConstraints { make in
|
||
make.top.equalTo(shareButton)
|
||
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(shareButton.snp.leading).offset(-8)
|
||
}
|
||
timeLabel.snp.makeConstraints { make in
|
||
make.top.equalTo(nameLabel.snp.bottom).offset(7)
|
||
make.leading.trailing.equalTo(nameLabel)
|
||
}
|
||
phoneLabel.snp.makeConstraints { make in
|
||
make.top.equalTo(timeLabel.snp.bottom).offset(5)
|
||
make.leading.trailing.equalTo(nameLabel)
|
||
}
|
||
uploadButton.snp.makeConstraints { make in
|
||
make.top.equalTo(phoneLabel.snp.bottom).offset(9)
|
||
make.trailing.equalToSuperview().offset(-10)
|
||
make.bottom.equalToSuperview().offset(-10)
|
||
make.height.equalTo(34)
|
||
make.width.greaterThanOrEqualTo(104)
|
||
}
|
||
|
||
uploadButton.addTarget(self, action: #selector(uploadTapped), for: .touchUpInside)
|
||
shareButton.addTarget(self, action: #selector(shareTapped), 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 shareTapped() { onShare?() }
|
||
@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")
|
||
}
|
||
}
|