// // 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! 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 = 148 tableView.delegate = self tableView.register(TravelAlbumTaskCell.self, forCellReuseIdentifier: TravelAlbumTaskCell.reuseIdentifier) dataSource = UITableViewDiffableDataSource(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() 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 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) 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(12) make.size.equalTo(112) } 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.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) 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") } }