feat: add travel album module

This commit is contained in:
2026-07-07 17:23:31 +08:00
parent a80c181bd7
commit 00bda390e8
16 changed files with 3115 additions and 0 deletions

View File

@ -0,0 +1,311 @@
//
// WiredCameraTransferViewController.swift
// suixinkan
//
import SnapKit
import UIKit
/// 线 Android UI OTG
final class WiredCameraTransferViewController: BaseViewController {
private let viewModel: WiredCameraTransferViewModel
private let headerView = UIView()
private let backButton = UIButton(type: .system)
private let titleLabel = UILabel()
private let phoneLabel = UILabel()
private let statusCard = UIView()
private let storagePanel = UIView()
private let storageTitleLabel = UILabel()
private let storageValueLabel = UILabel()
private let statusLabel = UILabel()
private let refreshButton = UIButton(type: .system)
private let helpLabel = UILabel()
private let chipsStack = UIStackView()
private let statsCard = UIStackView()
private let emptyLabel = UILabel()
private let bottomBar = UIView()
private let batchButton = UIButton(type: .system)
private let specifyButton = UIButton(type: .system)
init(viewModel: WiredCameraTransferViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func setupNavigationBar() {
navigationController?.setNavigationBarHidden(true, animated: false)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.setNavigationBarHidden(false, animated: animated)
}
override func setupUI() {
view.backgroundColor = AppColor.pageBackground
headerView.backgroundColor = AppColor.primary
backButton.setImage(UIImage(systemName: "chevron.left"), for: .normal)
backButton.tintColor = .white
titleLabel.text = viewModel.albumTitle
titleLabel.font = .systemFont(ofSize: 20, weight: .bold)
titleLabel.textColor = .white
phoneLabel.text = viewModel.headerPhone
phoneLabel.font = .systemFont(ofSize: 13)
phoneLabel.textColor = UIColor.white.withAlphaComponent(0.92)
statusCard.backgroundColor = .white
statusCard.layer.cornerRadius = 12
statusCard.layer.shadowColor = UIColor.black.withAlphaComponent(0.10).cgColor
statusCard.layer.shadowOpacity = 1
statusCard.layer.shadowRadius = 8
statusCard.layer.shadowOffset = CGSize(width: 0, height: 3)
storagePanel.layer.cornerRadius = 12
storagePanel.layer.borderColor = AppColor.border.cgColor
storagePanel.layer.borderWidth = 3
storageTitleLabel.text = viewModel.deviceModelName
storageTitleLabel.font = .systemFont(ofSize: 11, weight: .medium)
storageTitleLabel.textColor = AppColor.textPrimary
storageTitleLabel.textAlignment = .center
storageValueLabel.text = viewModel.availableStorageText
storageValueLabel.font = .systemFont(ofSize: 10)
storageValueLabel.textColor = AppColor.primary
storageValueLabel.textAlignment = .center
statusLabel.text = viewModel.cameraStatusText
statusLabel.font = .systemFont(ofSize: 11, weight: .medium)
statusLabel.textColor = AppColor.danger
statusLabel.backgroundColor = AppColor.danger.withAlphaComponent(0.10)
statusLabel.layer.cornerRadius = 4
statusLabel.clipsToBounds = true
statusLabel.textAlignment = .center
refreshButton.setTitle(viewModel.actionButtonText, for: .normal)
refreshButton.setTitleColor(.white, for: .normal)
refreshButton.titleLabel?.font = .systemFont(ofSize: 12)
refreshButton.backgroundColor = AppColor.danger
refreshButton.layer.cornerRadius = 8
helpLabel.attributedText = helpText()
helpLabel.font = .systemFont(ofSize: 10)
helpLabel.numberOfLines = 2
chipsStack.axis = .horizontal
chipsStack.spacing = 6
[viewModel.retouchOption, viewModel.photoFormatOption, viewModel.transferModeOption].forEach {
chipsStack.addArrangedSubview(makeChip($0))
}
statsCard.axis = .horizontal
statsCard.distribution = .fillEqually
["全部", "已上传", "失败"].enumerated().forEach { index, title in
statsCard.addArrangedSubview(makeStat(title: title, count: viewModel.tabCounts[index], selected: index == 0, danger: index == 2))
}
emptyLabel.text = "暂无照片"
emptyLabel.font = .systemFont(ofSize: 14)
emptyLabel.textColor = AppColor.textTertiary
emptyLabel.textAlignment = .center
bottomBar.backgroundColor = .white
configureBottomButton(batchButton, title: "批量上传", filled: true)
configureBottomButton(specifyButton, title: "指定上传", filled: false)
view.addSubview(headerView)
headerView.addSubview(backButton)
headerView.addSubview(titleLabel)
headerView.addSubview(phoneLabel)
view.addSubview(statusCard)
statusCard.addSubview(storagePanel)
storagePanel.addSubview(storageTitleLabel)
storagePanel.addSubview(storageValueLabel)
statusCard.addSubview(statusLabel)
statusCard.addSubview(refreshButton)
statusCard.addSubview(helpLabel)
statusCard.addSubview(chipsStack)
statusCard.addSubview(statsCard)
view.addSubview(emptyLabel)
view.addSubview(bottomBar)
bottomBar.addSubview(batchButton)
bottomBar.addSubview(specifyButton)
}
override func setupConstraints() {
headerView.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
make.height.equalTo(118)
}
backButton.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(8)
make.bottom.equalToSuperview().offset(-28)
make.size.equalTo(44)
}
titleLabel.snp.makeConstraints { make in
make.leading.equalTo(backButton.snp.trailing).offset(4)
make.trailing.equalToSuperview().offset(-16)
make.top.equalTo(backButton).offset(4)
}
phoneLabel.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(2)
make.leading.trailing.equalTo(titleLabel)
}
statusCard.snp.makeConstraints { make in
make.top.equalTo(headerView.snp.bottom).offset(-10)
make.leading.trailing.equalToSuperview().inset(16)
}
storagePanel.snp.makeConstraints { make in
make.top.leading.equalToSuperview().offset(14)
make.width.greaterThanOrEqualTo(76)
make.height.equalTo(76)
}
storageTitleLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(10)
make.leading.trailing.equalToSuperview().inset(8)
}
storageValueLabel.snp.makeConstraints { make in
make.top.equalTo(storageTitleLabel.snp.bottom).offset(6)
make.leading.trailing.equalTo(storageTitleLabel)
}
statusLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(16)
make.leading.equalTo(storagePanel.snp.trailing).offset(10)
make.height.equalTo(22)
make.width.greaterThanOrEqualTo(84)
}
refreshButton.snp.makeConstraints { make in
make.centerY.equalTo(statusLabel)
make.trailing.equalToSuperview().offset(-14)
make.height.equalTo(34)
make.width.greaterThanOrEqualTo(72)
}
helpLabel.snp.makeConstraints { make in
make.top.equalTo(statusLabel.snp.bottom).offset(4)
make.leading.equalTo(statusLabel)
make.trailing.equalTo(refreshButton)
}
chipsStack.snp.makeConstraints { make in
make.top.equalTo(storagePanel.snp.bottom).offset(12)
make.leading.trailing.equalToSuperview().inset(14)
make.height.equalTo(34)
}
statsCard.snp.makeConstraints { make in
make.top.equalTo(chipsStack.snp.bottom).offset(14)
make.leading.trailing.bottom.equalToSuperview()
make.height.equalTo(68)
}
bottomBar.snp.makeConstraints { make in
make.leading.trailing.bottom.equalToSuperview()
}
batchButton.snp.makeConstraints { make in
make.top.equalToSuperview().offset(12)
make.leading.equalToSuperview().offset(16)
make.height.equalTo(44)
make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-12)
}
specifyButton.snp.makeConstraints { make in
make.top.height.width.equalTo(batchButton)
make.leading.equalTo(batchButton.snp.trailing).offset(12)
make.trailing.equalToSuperview().offset(-16)
}
emptyLabel.snp.makeConstraints { make in
make.top.equalTo(statusCard.snp.bottom)
make.leading.trailing.equalToSuperview()
make.bottom.equalTo(bottomBar.snp.top)
}
}
override func bindActions() {
viewModel.onShowMessage = { [weak self] message in
Task { @MainActor in self?.showToast(message) }
}
backButton.addTarget(self, action: #selector(backTapped), for: .touchUpInside)
refreshButton.addTarget(self, action: #selector(uiOnlyTapped), for: .touchUpInside)
batchButton.addTarget(self, action: #selector(uiOnlyTapped), for: .touchUpInside)
specifyButton.addTarget(self, action: #selector(uiOnlyTapped), for: .touchUpInside)
}
private func makeChip(_ text: String) -> UIView {
let label = UILabel()
label.text = text
label.font = .systemFont(ofSize: 11)
label.textColor = AppColor.textTertiary
label.backgroundColor = AppColor.pageBackground
label.layer.cornerRadius = 8
label.layer.borderWidth = 1
label.layer.borderColor = AppColor.border.cgColor
label.clipsToBounds = true
label.textAlignment = .center
return label
}
private func makeStat(title: String, count: Int, selected: Bool, danger: Bool) -> UIView {
let container = UIView()
let countLabel = UILabel()
let titleLabel = UILabel()
let underline = UIView()
countLabel.text = "\(count)"
countLabel.font = .systemFont(ofSize: 18, weight: .semibold)
countLabel.textColor = selected ? AppColor.primary : (danger ? AppColor.danger : AppColor.textPrimary)
countLabel.textAlignment = .center
titleLabel.text = title
titleLabel.font = .systemFont(ofSize: 13)
titleLabel.textColor = selected ? AppColor.primary : AppColor.textSecondary
titleLabel.textAlignment = .center
underline.backgroundColor = selected ? AppColor.primary : .clear
underline.layer.cornerRadius = 1
container.addSubview(countLabel)
container.addSubview(titleLabel)
container.addSubview(underline)
countLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(8)
make.leading.trailing.equalToSuperview()
}
titleLabel.snp.makeConstraints { make in
make.top.equalTo(countLabel.snp.bottom).offset(2)
make.leading.trailing.equalToSuperview()
}
underline.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(4)
make.centerX.equalToSuperview()
make.width.equalTo(24)
make.height.equalTo(2)
}
return container
}
private func configureBottomButton(_ button: UIButton, title: String, filled: Bool) {
button.setTitle(title, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 15)
button.layer.cornerRadius = 10
if filled {
button.backgroundColor = AppColor.primary
button.setTitleColor(.white, for: .normal)
} else {
button.backgroundColor = .white
button.setTitleColor(AppColor.primary, for: .normal)
button.layer.borderWidth = 1
button.layer.borderColor = AppColor.primary.cgColor
}
}
private func helpText() -> NSAttributedString {
let text = NSMutableAttributedString(
string: "未连接,查看",
attributes: [.foregroundColor: AppColor.textSecondary]
)
text.append(NSAttributedString(string: "《帮助文档》", attributes: [.foregroundColor: AppColor.primary]))
return text
}
@objc private func backTapped() {
navigationController?.popViewController(animated: true)
}
@objc private func uiOnlyTapped() {
viewModel.showUIOnlyMessage()
}
}