feat: redesign travel album management
This commit is contained in:
@@ -7,15 +7,15 @@ import Kingfisher
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 相册详情管理页,对齐 Android `TravelAlbumDetailScreen`。
|
||||
/// 相册管理页,展示相册摘要、素材筛选排序、分页与批量操作。
|
||||
final class TravelAlbumDetailViewController: BaseViewController {
|
||||
private let viewModel: TravelAlbumDetailViewModel
|
||||
private let api: any TravelAlbumServing
|
||||
|
||||
private let scrollContainer = UIView()
|
||||
private let contentView = UIView()
|
||||
private let infoCard = TravelAlbumInfoCard()
|
||||
private let manageCard = UIView()
|
||||
private let tabStack = UIStackView()
|
||||
private let sectionTitleLabel = UILabel()
|
||||
private let segmentedControl = UIView()
|
||||
private let allTabButton = UIButton(type: .system)
|
||||
private let purchasedTabButton = UIButton(type: .system)
|
||||
private let sortButton = UIButton(type: .system)
|
||||
@@ -23,10 +23,9 @@ final class TravelAlbumDetailViewController: BaseViewController {
|
||||
private var collectionView: UICollectionView!
|
||||
private var dataSource: UICollectionViewDiffableDataSource<Int, TravelAlbumMaterial>!
|
||||
private let bottomBar = UIView()
|
||||
private let bottomActionStack = UIStackView()
|
||||
private let bottomDivider = UIView()
|
||||
private let deleteSelectedButton = UIButton(type: .system)
|
||||
private let uploadButton = UIButton(type: .system)
|
||||
private var isDeleteSelectedButtonVisible = false
|
||||
|
||||
init(albumId: Int, api: (any TravelAlbumServing)? = nil) {
|
||||
viewModel = TravelAlbumDetailViewModel(albumId: albumId)
|
||||
@@ -40,7 +39,20 @@ final class TravelAlbumDetailViewController: BaseViewController {
|
||||
}
|
||||
|
||||
override func setupNavigationBar() {
|
||||
title = "相册管理"
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = "相册管理"
|
||||
titleLabel.textColor = TravelAlbumDetailStyle.textPrimary
|
||||
titleLabel.font = .systemFont(ofSize: 20, weight: .semibold)
|
||||
navigationItem.titleView = titleLabel
|
||||
|
||||
let appearance = UINavigationBarAppearance()
|
||||
appearance.configureWithOpaqueBackground()
|
||||
appearance.backgroundColor = .white
|
||||
appearance.shadowColor = .clear
|
||||
navigationItem.standardAppearance = appearance
|
||||
navigationItem.scrollEdgeAppearance = appearance
|
||||
navigationItem.compactAppearance = appearance
|
||||
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||
image: UIImage(systemName: "ellipsis"),
|
||||
menu: UIMenu(children: [
|
||||
@@ -49,29 +61,35 @@ final class TravelAlbumDetailViewController: BaseViewController {
|
||||
},
|
||||
])
|
||||
)
|
||||
navigationItem.rightBarButtonItem?.accessibilityLabel = "更多操作"
|
||||
}
|
||||
|
||||
override func setupUI() {
|
||||
view.backgroundColor = AppColor.pageBackground
|
||||
view.backgroundColor = TravelAlbumDetailStyle.pageBackground
|
||||
|
||||
manageCard.backgroundColor = .white
|
||||
manageCard.layer.cornerRadius = 12
|
||||
manageCard.layer.shadowColor = UIColor.black.withAlphaComponent(0.08).cgColor
|
||||
manageCard.layer.shadowOpacity = 1
|
||||
manageCard.layer.shadowRadius = 6
|
||||
manageCard.layer.shadowOffset = CGSize(width: 0, height: 2)
|
||||
sectionTitleLabel.text = "照片"
|
||||
sectionTitleLabel.textColor = TravelAlbumDetailStyle.textPrimary
|
||||
sectionTitleLabel.font = .systemFont(ofSize: 20, weight: .semibold)
|
||||
|
||||
tabStack.axis = .horizontal
|
||||
tabStack.spacing = 8
|
||||
configurePillButton(allTabButton)
|
||||
configurePillButton(purchasedTabButton)
|
||||
configureIconButton(sortButton, image: UIImage(systemName: "line.3.horizontal.decrease.circle.fill"))
|
||||
configureIconButton(selectButton, image: UIImage(systemName: "circle"))
|
||||
segmentedControl.backgroundColor = .white
|
||||
segmentedControl.layer.cornerRadius = 9
|
||||
segmentedControl.layer.borderWidth = 1
|
||||
segmentedControl.layer.borderColor = TravelAlbumDetailStyle.border.cgColor
|
||||
segmentedControl.clipsToBounds = true
|
||||
configureTabButtonBase(allTabButton)
|
||||
configureTabButtonBase(purchasedTabButton)
|
||||
configureSortButton()
|
||||
configureSelectButton()
|
||||
|
||||
collectionView = UICollectionView(frame: .zero, collectionViewLayout: makeLayout())
|
||||
collectionView.backgroundColor = .white
|
||||
collectionView.backgroundColor = .clear
|
||||
collectionView.alwaysBounceVertical = true
|
||||
collectionView.showsVerticalScrollIndicator = false
|
||||
collectionView.delegate = self
|
||||
collectionView.register(TravelAlbumMaterialCell.self, forCellWithReuseIdentifier: TravelAlbumMaterialCell.reuseIdentifier)
|
||||
collectionView.register(
|
||||
TravelAlbumMaterialCell.self,
|
||||
forCellWithReuseIdentifier: TravelAlbumMaterialCell.reuseIdentifier
|
||||
)
|
||||
dataSource = UICollectionViewDiffableDataSource<Int, TravelAlbumMaterial>(collectionView: collectionView) {
|
||||
[weak self] collectionView, indexPath, material in
|
||||
let cell = collectionView.dequeueReusableCell(
|
||||
@@ -86,74 +104,93 @@ final class TravelAlbumDetailViewController: BaseViewController {
|
||||
return cell
|
||||
}
|
||||
|
||||
bottomBar.backgroundColor = AppColor.pageBackground
|
||||
bottomActionStack.axis = .vertical
|
||||
bottomActionStack.spacing = 8
|
||||
configureBottomButton(deleteSelectedButton, title: "删除选中(0)", color: UIColor(hex: 0xE53935))
|
||||
configureBottomButton(uploadButton, title: "上传照片", color: AppColor.primary)
|
||||
bottomBar.backgroundColor = .white
|
||||
bottomDivider.backgroundColor = TravelAlbumDetailStyle.border.withAlphaComponent(0.65)
|
||||
configureBottomButton(uploadButton, title: "上传照片", color: TravelAlbumDetailStyle.primary)
|
||||
var uploadConfiguration = uploadButton.configuration
|
||||
uploadConfiguration?.image = UIImage(systemName: "plus.circle.fill")
|
||||
uploadConfiguration?.imagePadding = 8
|
||||
uploadButton.configuration = uploadConfiguration
|
||||
uploadButton.accessibilityLabel = "上传照片"
|
||||
configureBottomButton(deleteSelectedButton, title: "删除选中(0)", color: TravelAlbumDetailStyle.danger)
|
||||
deleteSelectedButton.isHidden = true
|
||||
deleteSelectedButton.alpha = 0
|
||||
|
||||
view.addSubview(scrollContainer)
|
||||
scrollContainer.addSubview(infoCard)
|
||||
scrollContainer.addSubview(manageCard)
|
||||
manageCard.addSubview(tabStack)
|
||||
tabStack.addArrangedSubview(allTabButton)
|
||||
tabStack.addArrangedSubview(purchasedTabButton)
|
||||
manageCard.addSubview(sortButton)
|
||||
manageCard.addSubview(selectButton)
|
||||
manageCard.addSubview(collectionView)
|
||||
view.addSubview(contentView)
|
||||
contentView.addSubview(infoCard)
|
||||
contentView.addSubview(sectionTitleLabel)
|
||||
contentView.addSubview(segmentedControl)
|
||||
segmentedControl.addSubview(allTabButton)
|
||||
segmentedControl.addSubview(purchasedTabButton)
|
||||
contentView.addSubview(sortButton)
|
||||
contentView.addSubview(selectButton)
|
||||
contentView.addSubview(collectionView)
|
||||
view.addSubview(bottomBar)
|
||||
bottomBar.addSubview(bottomActionStack)
|
||||
bottomActionStack.addArrangedSubview(deleteSelectedButton)
|
||||
bottomActionStack.addArrangedSubview(uploadButton)
|
||||
bottomBar.addSubview(bottomDivider)
|
||||
bottomBar.addSubview(uploadButton)
|
||||
bottomBar.addSubview(deleteSelectedButton)
|
||||
}
|
||||
|
||||
override func setupConstraints() {
|
||||
bottomBar.snp.makeConstraints { make in
|
||||
make.leading.trailing.bottom.equalToSuperview()
|
||||
}
|
||||
bottomActionStack.snp.makeConstraints { make in
|
||||
bottomDivider.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalToSuperview()
|
||||
make.height.equalTo(0.5)
|
||||
}
|
||||
uploadButton.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(12)
|
||||
make.leading.trailing.equalToSuperview().inset(20)
|
||||
make.leading.trailing.equalToSuperview().inset(18)
|
||||
make.height.equalTo(48)
|
||||
make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-12)
|
||||
}
|
||||
deleteSelectedButton.snp.makeConstraints { make in
|
||||
make.height.equalTo(48).priority(.high)
|
||||
make.edges.equalTo(uploadButton)
|
||||
}
|
||||
uploadButton.snp.makeConstraints { make in
|
||||
make.height.equalTo(48).priority(.high)
|
||||
}
|
||||
scrollContainer.snp.makeConstraints { make in
|
||||
|
||||
contentView.snp.makeConstraints { make in
|
||||
make.top.equalTo(view.safeAreaLayoutGuide).offset(12)
|
||||
make.leading.trailing.equalToSuperview().inset(16)
|
||||
make.bottom.equalTo(bottomBar.snp.top).offset(-8)
|
||||
make.leading.trailing.equalToSuperview().inset(18)
|
||||
make.bottom.equalTo(bottomBar.snp.top)
|
||||
}
|
||||
infoCard.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalToSuperview()
|
||||
make.height.greaterThanOrEqualTo(76)
|
||||
make.height.equalTo(116)
|
||||
}
|
||||
manageCard.snp.makeConstraints { make in
|
||||
make.top.equalTo(infoCard.snp.bottom).offset(12)
|
||||
make.leading.trailing.bottom.equalToSuperview()
|
||||
sectionTitleLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(infoCard.snp.bottom).offset(20)
|
||||
make.leading.equalToSuperview()
|
||||
}
|
||||
tabStack.snp.makeConstraints { make in
|
||||
make.top.leading.equalToSuperview().offset(12)
|
||||
segmentedControl.snp.makeConstraints { make in
|
||||
make.top.equalTo(sectionTitleLabel.snp.bottom).offset(14)
|
||||
make.leading.equalToSuperview()
|
||||
make.width.equalTo(182)
|
||||
make.height.equalTo(32)
|
||||
}
|
||||
allTabButton.snp.makeConstraints { make in
|
||||
make.top.bottom.leading.equalToSuperview()
|
||||
make.width.equalToSuperview().multipliedBy(0.5)
|
||||
}
|
||||
purchasedTabButton.snp.makeConstraints { make in
|
||||
make.top.bottom.trailing.equalToSuperview()
|
||||
make.leading.equalTo(allTabButton.snp.trailing)
|
||||
}
|
||||
selectButton.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(segmentedControl)
|
||||
make.trailing.equalToSuperview()
|
||||
make.width.equalTo(48)
|
||||
make.height.equalTo(32)
|
||||
}
|
||||
sortButton.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(tabStack)
|
||||
make.centerY.equalTo(segmentedControl)
|
||||
make.trailing.equalTo(selectButton.snp.leading).offset(-8)
|
||||
make.size.equalTo(32)
|
||||
}
|
||||
selectButton.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(tabStack)
|
||||
make.trailing.equalToSuperview().offset(-12)
|
||||
make.size.equalTo(32)
|
||||
make.width.equalTo(74)
|
||||
make.height.equalTo(32)
|
||||
make.leading.greaterThanOrEqualTo(segmentedControl.snp.trailing).offset(8)
|
||||
}
|
||||
collectionView.snp.makeConstraints { make in
|
||||
make.top.equalTo(tabStack.snp.bottom).offset(12)
|
||||
make.leading.trailing.bottom.equalToSuperview().inset(12)
|
||||
make.top.equalTo(segmentedControl.snp.bottom).offset(14)
|
||||
make.leading.trailing.bottom.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,7 +198,6 @@ final class TravelAlbumDetailViewController: BaseViewController {
|
||||
infoCard.onCall = { [weak self] phone in self?.call(phone) }
|
||||
allTabButton.addTarget(self, action: #selector(allTabTapped), for: .touchUpInside)
|
||||
purchasedTabButton.addTarget(self, action: #selector(purchasedTabTapped), for: .touchUpInside)
|
||||
sortButton.addTarget(self, action: #selector(sortTapped), for: .touchUpInside)
|
||||
selectButton.addTarget(self, action: #selector(selectTapped), for: .touchUpInside)
|
||||
deleteSelectedButton.addTarget(self, action: #selector(deleteSelectedTapped), for: .touchUpInside)
|
||||
uploadButton.addTarget(self, action: #selector(uploadTapped), for: .touchUpInside)
|
||||
@@ -188,15 +224,36 @@ final class TravelAlbumDetailViewController: BaseViewController {
|
||||
@MainActor
|
||||
private func applyViewModel() {
|
||||
if let album = viewModel.album {
|
||||
infoCard.apply(album: album)
|
||||
infoCard.apply(album: album, coverURL: viewModel.displayCoverURL)
|
||||
}
|
||||
configureTabButton(allTabButton, title: "全部照片\(viewModel.allPhotoCount)", selected: viewModel.selectedTab == .all)
|
||||
configureTabButton(purchasedTabButton, title: "已购照片", selected: viewModel.selectedTab == .purchased)
|
||||
selectButton.isHidden = viewModel.selectedTab != .all
|
||||
selectButton.setImage(UIImage(systemName: viewModel.isSelectionMode ? "checkmark.circle.fill" : "circle"), for: .normal)
|
||||
selectButton.tintColor = viewModel.isSelectionMode ? AppColor.primary : AppColor.textTertiary
|
||||
deleteSelectedButton.setTitle("删除选中(\(viewModel.selectedMaterialIds.count))", for: .normal)
|
||||
setDeleteSelectedButtonVisible(viewModel.isSelectionMode && !viewModel.selectedMaterialIds.isEmpty)
|
||||
configureTabButton(allTabButton, title: "全部 \(viewModel.allPhotoCount)", selected: viewModel.selectedTab == .all)
|
||||
configureTabButton(
|
||||
purchasedTabButton,
|
||||
title: "已购 \(viewModel.purchasedPhotoCount)",
|
||||
selected: viewModel.selectedTab == .purchased
|
||||
)
|
||||
|
||||
var sortConfiguration = sortButton.configuration
|
||||
sortConfiguration?.title = viewModel.sortOption.compactTitle
|
||||
sortButton.configuration = sortConfiguration
|
||||
sortButton.accessibilityValue = viewModel.sortOption.title
|
||||
sortButton.menu = makeSortMenu()
|
||||
|
||||
let canSelectMaterials = viewModel.selectedTab == .all
|
||||
selectButton.isHidden = false
|
||||
selectButton.isEnabled = canSelectMaterials
|
||||
selectButton.alpha = canSelectMaterials ? 1 : 0.45
|
||||
selectButton.setTitle(viewModel.isSelectionMode ? "完成" : "选择", for: .normal)
|
||||
selectButton.accessibilityValue = canSelectMaterials
|
||||
? (viewModel.isSelectionMode ? "选择模式已开启" : "选择模式已关闭")
|
||||
: "已购照片不可删除"
|
||||
|
||||
let selectedCount = viewModel.selectedMaterialIds.count
|
||||
deleteSelectedButton.setTitle("删除选中(\(selectedCount))", for: .normal)
|
||||
deleteSelectedButton.isEnabled = selectedCount > 0
|
||||
deleteSelectedButton.alpha = selectedCount > 0 ? 1 : 0.45
|
||||
deleteSelectedButton.isHidden = !viewModel.isSelectionMode
|
||||
uploadButton.isHidden = viewModel.isSelectionMode
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Int, TravelAlbumMaterial>()
|
||||
snapshot.appendSections([0])
|
||||
@@ -213,69 +270,95 @@ final class TravelAlbumDetailViewController: BaseViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private func configurePillButton(_ button: UIButton) {
|
||||
private func configureTabButtonBase(_ button: UIButton) {
|
||||
button.titleLabel?.font = .systemFont(ofSize: 13, weight: .medium)
|
||||
button.layer.cornerRadius = 16
|
||||
button.setConfigurationContentInsets(
|
||||
NSDirectionalEdgeInsets(top: 7, leading: 14, bottom: 7, trailing: 14)
|
||||
)
|
||||
button.accessibilityTraits.insert(.button)
|
||||
}
|
||||
|
||||
private func configureTabButton(_ button: UIButton, title: String, selected: Bool) {
|
||||
button.setTitle(title, for: .normal)
|
||||
button.backgroundColor = selected ? AppColor.primary : UIColor(hex: 0xEAF4FF)
|
||||
button.setTitleColor(selected ? .white : AppColor.primary, for: .normal)
|
||||
button.backgroundColor = selected ? TravelAlbumDetailStyle.primary : .white
|
||||
button.setTitleColor(selected ? .white : TravelAlbumDetailStyle.textSecondary, for: .normal)
|
||||
button.accessibilityTraits = selected ? [.button, .selected] : [.button]
|
||||
}
|
||||
|
||||
private func configureIconButton(_ button: UIButton, image: UIImage?) {
|
||||
button.setImage(image, for: .normal)
|
||||
button.backgroundColor = UIColor(hex: 0xEAF4FF)
|
||||
button.tintColor = AppColor.primary
|
||||
button.layer.cornerRadius = 16
|
||||
private func configureSortButton() {
|
||||
var configuration = UIButton.Configuration.plain()
|
||||
configuration.image = UIImage(named: "travel_album_sort_icon")
|
||||
configuration.imagePadding = 4
|
||||
configuration.title = viewModel.sortOption.compactTitle
|
||||
configuration.baseForegroundColor = TravelAlbumDetailStyle.textPrimary
|
||||
configuration.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 7, bottom: 0, trailing: 7)
|
||||
configuration.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { attributes in
|
||||
var attributes = attributes
|
||||
attributes.font = .systemFont(ofSize: 11, weight: .medium)
|
||||
return attributes
|
||||
}
|
||||
sortButton.configuration = configuration
|
||||
sortButton.backgroundColor = .white
|
||||
sortButton.layer.cornerRadius = 9
|
||||
sortButton.layer.borderWidth = 1
|
||||
sortButton.layer.borderColor = TravelAlbumDetailStyle.border.cgColor
|
||||
sortButton.showsMenuAsPrimaryAction = true
|
||||
sortButton.accessibilityLabel = "排序"
|
||||
}
|
||||
|
||||
private func configureSelectButton() {
|
||||
selectButton.setTitle("选择", for: .normal)
|
||||
selectButton.setTitleColor(TravelAlbumDetailStyle.primary, for: .normal)
|
||||
selectButton.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
selectButton.backgroundColor = .white
|
||||
selectButton.layer.cornerRadius = 9
|
||||
selectButton.layer.borderWidth = 1
|
||||
selectButton.layer.borderColor = TravelAlbumDetailStyle.primary.withAlphaComponent(0.45).cgColor
|
||||
selectButton.accessibilityLabel = "选择照片"
|
||||
}
|
||||
|
||||
private func configureBottomButton(_ button: UIButton, title: String, color: UIColor) {
|
||||
button.setTitle(title, for: .normal)
|
||||
button.setTitleColor(.white, for: .normal)
|
||||
button.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
button.backgroundColor = color
|
||||
button.layer.cornerRadius = 10
|
||||
var configuration = UIButton.Configuration.filled()
|
||||
configuration.title = title
|
||||
configuration.baseBackgroundColor = color
|
||||
configuration.baseForegroundColor = .white
|
||||
configuration.cornerStyle = .fixed
|
||||
configuration.background.cornerRadius = 14
|
||||
configuration.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { attributes in
|
||||
var attributes = attributes
|
||||
attributes.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||
return attributes
|
||||
}
|
||||
button.configuration = configuration
|
||||
}
|
||||
|
||||
private func setDeleteSelectedButtonVisible(_ visible: Bool) {
|
||||
guard visible != isDeleteSelectedButtonVisible else { return }
|
||||
isDeleteSelectedButtonVisible = visible
|
||||
|
||||
let changes = {
|
||||
self.deleteSelectedButton.isHidden = !visible
|
||||
self.deleteSelectedButton.alpha = visible ? 1 : 0
|
||||
self.view.layoutIfNeeded()
|
||||
private func makeSortMenu() -> UIMenu {
|
||||
let actions = TravelAlbumDetailViewModel.SortOption.allCases.map { option in
|
||||
UIAction(
|
||||
title: option.title,
|
||||
state: option == viewModel.sortOption ? .on : .off
|
||||
) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
Task { await self.viewModel.setSortOption(option, api: self.api) }
|
||||
}
|
||||
}
|
||||
guard view.window != nil else {
|
||||
changes()
|
||||
return
|
||||
}
|
||||
|
||||
view.layoutIfNeeded()
|
||||
UIView.animate(
|
||||
withDuration: 0.25,
|
||||
delay: 0,
|
||||
options: [.curveEaseInOut, .beginFromCurrentState],
|
||||
animations: changes
|
||||
)
|
||||
return UIMenu(title: "排序方式", options: .singleSelection, children: actions)
|
||||
}
|
||||
|
||||
private func makeLayout() -> UICollectionViewCompositionalLayout {
|
||||
UICollectionViewCompositionalLayout { _, environment in
|
||||
let spacing: CGFloat = 8
|
||||
let width = (environment.container.effectiveContentSize.width - spacing * 2) / 3
|
||||
let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(width), heightDimension: .absolute(width + 38))
|
||||
let spacing: CGFloat = 10
|
||||
let width = floor((environment.container.effectiveContentSize.width - spacing * 2) / 3)
|
||||
let itemSize = NSCollectionLayoutSize(
|
||||
widthDimension: .absolute(width),
|
||||
heightDimension: .absolute(width + 40)
|
||||
)
|
||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(width + 38))
|
||||
let groupSize = NSCollectionLayoutSize(
|
||||
widthDimension: .fractionalWidth(1),
|
||||
heightDimension: .absolute(width + 40)
|
||||
)
|
||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, repeatingSubitem: item, count: 3)
|
||||
group.interItemSpacing = .fixed(spacing)
|
||||
let section = NSCollectionLayoutSection(group: group)
|
||||
section.interGroupSpacing = 12
|
||||
section.interGroupSpacing = 14
|
||||
return section
|
||||
}
|
||||
}
|
||||
@@ -303,24 +386,13 @@ final class TravelAlbumDetailViewController: BaseViewController {
|
||||
Task { await viewModel.selectTab(.purchased, api: api) }
|
||||
}
|
||||
|
||||
@objc private func sortTapped() {
|
||||
let alert = UIAlertController(title: "排序", message: nil, preferredStyle: .actionSheet)
|
||||
TravelAlbumDetailViewModel.SortOption.allCases.forEach { option in
|
||||
alert.addAction(UIAlertAction(title: option.title, style: .default) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
Task { await self.viewModel.setSortOption(option, api: self.api) }
|
||||
})
|
||||
}
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
@objc private func selectTapped() {
|
||||
viewModel.toggleSelectionMode()
|
||||
}
|
||||
|
||||
@objc private func deleteSelectedTapped() {
|
||||
let count = viewModel.selectedMaterialIds.count
|
||||
guard count > 0 else { return }
|
||||
let alert = UIAlertController(title: "删除素材", message: "确定删除选中的 \(count) 张素材吗?", preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
alert.addAction(UIAlertAction(title: "删除", style: .destructive) { [weak self] _ in
|
||||
@@ -355,66 +427,100 @@ extension TravelAlbumDetailViewController: UICollectionViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
/// 旅拍相册详情信息卡。
|
||||
/// 相册管理页专用视觉常量,避免污染全局主题。
|
||||
private enum TravelAlbumDetailStyle {
|
||||
static let primary = UIColor(hex: 0x1677FF)
|
||||
static let pageBackground = UIColor(hex: 0xF8FAFD)
|
||||
static let textPrimary = UIColor(hex: 0x172033)
|
||||
static let textSecondary = UIColor(hex: 0x7F8A9E)
|
||||
static let border = UIColor(hex: 0xDCE4EF)
|
||||
static let danger = UIColor(hex: 0xE53935)
|
||||
}
|
||||
|
||||
/// 旅拍相册摘要卡,展示封面、名称、用户手机号与创建时间。
|
||||
private final class TravelAlbumInfoCard: UIView {
|
||||
var onCall: ((String) -> Void)?
|
||||
private var phone = ""
|
||||
|
||||
private let iconView = UIImageView(image: UIImage(systemName: "checklist"))
|
||||
private let backgroundImageView = UIImageView(image: UIImage(named: "travel_album_header_background"))
|
||||
private let coverImageView = UIImageView()
|
||||
private let coverPhotoIconView = UIImageView(image: UIImage(named: "travel_album_cover_photo_icon"))
|
||||
private let nameLabel = UILabel()
|
||||
private let phoneLabel = UILabel()
|
||||
private let timeLabel = UILabel()
|
||||
private let callButton = UIButton(type: .system)
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .white
|
||||
layer.cornerRadius = 12
|
||||
layer.shadowColor = UIColor.black.withAlphaComponent(0.08).cgColor
|
||||
layer.shadowOpacity = 1
|
||||
layer.shadowRadius = 6
|
||||
layer.shadowOffset = CGSize(width: 0, height: 2)
|
||||
clipsToBounds = true
|
||||
layer.cornerRadius = 16
|
||||
|
||||
backgroundImageView.contentMode = .scaleAspectFill
|
||||
coverImageView.contentMode = .scaleAspectFill
|
||||
coverImageView.clipsToBounds = true
|
||||
coverImageView.layer.cornerRadius = 12
|
||||
coverImageView.backgroundColor = UIColor(hex: 0xDCEEFF)
|
||||
coverPhotoIconView.contentMode = .scaleAspectFit
|
||||
coverPhotoIconView.layer.shadowColor = UIColor.black.withAlphaComponent(0.3).cgColor
|
||||
coverPhotoIconView.layer.shadowOpacity = 1
|
||||
coverPhotoIconView.layer.shadowRadius = 2
|
||||
coverPhotoIconView.layer.shadowOffset = .zero
|
||||
|
||||
nameLabel.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||
nameLabel.textColor = TravelAlbumDetailStyle.textPrimary
|
||||
nameLabel.lineBreakMode = .byTruncatingTail
|
||||
phoneLabel.font = .systemFont(ofSize: 13, weight: .medium)
|
||||
phoneLabel.textColor = TravelAlbumDetailStyle.textPrimary
|
||||
timeLabel.font = .systemFont(ofSize: 11)
|
||||
timeLabel.textColor = TravelAlbumDetailStyle.textSecondary
|
||||
timeLabel.adjustsFontSizeToFitWidth = true
|
||||
timeLabel.minimumScaleFactor = 0.85
|
||||
|
||||
let iconBox = UIView()
|
||||
iconBox.backgroundColor = AppColor.primary
|
||||
iconBox.layer.cornerRadius = 10
|
||||
iconView.tintColor = .white
|
||||
iconView.contentMode = .scaleAspectFit
|
||||
phoneLabel.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
phoneLabel.textColor = AppColor.textPrimary
|
||||
timeLabel.font = .systemFont(ofSize: 12)
|
||||
timeLabel.textColor = AppColor.textTertiary
|
||||
callButton.setImage(UIImage(systemName: "phone.fill"), for: .normal)
|
||||
callButton.tintColor = AppColor.primary
|
||||
callButton.backgroundColor = UIColor(hex: 0xEAF4FF)
|
||||
callButton.layer.cornerRadius = 20
|
||||
callButton.tintColor = TravelAlbumDetailStyle.primary
|
||||
callButton.backgroundColor = .white.withAlphaComponent(0.9)
|
||||
callButton.layer.cornerRadius = 22
|
||||
callButton.layer.shadowColor = TravelAlbumDetailStyle.primary.withAlphaComponent(0.18).cgColor
|
||||
callButton.layer.shadowOpacity = 1
|
||||
callButton.layer.shadowRadius = 6
|
||||
callButton.layer.shadowOffset = CGSize(width: 0, height: 2)
|
||||
callButton.accessibilityLabel = "拨打相册用户电话"
|
||||
|
||||
addSubview(iconBox)
|
||||
iconBox.addSubview(iconView)
|
||||
addSubview(backgroundImageView)
|
||||
addSubview(coverImageView)
|
||||
coverImageView.addSubview(coverPhotoIconView)
|
||||
addSubview(nameLabel)
|
||||
addSubview(phoneLabel)
|
||||
addSubview(timeLabel)
|
||||
addSubview(callButton)
|
||||
iconBox.snp.makeConstraints { make in
|
||||
make.leading.top.bottom.equalToSuperview().inset(14)
|
||||
make.size.equalTo(48)
|
||||
|
||||
backgroundImageView.snp.makeConstraints { $0.edges.equalToSuperview() }
|
||||
coverImageView.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(16)
|
||||
make.top.bottom.equalToSuperview().inset(20)
|
||||
make.width.equalTo(68)
|
||||
}
|
||||
iconView.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
make.size.equalTo(28)
|
||||
coverPhotoIconView.snp.makeConstraints { make in
|
||||
make.trailing.bottom.equalToSuperview().inset(5)
|
||||
make.size.equalTo(22)
|
||||
}
|
||||
callButton.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().offset(-16)
|
||||
make.centerY.equalToSuperview()
|
||||
make.size.equalTo(44)
|
||||
}
|
||||
nameLabel.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(21)
|
||||
make.leading.equalTo(coverImageView.snp.trailing).offset(12)
|
||||
make.trailing.equalTo(callButton.snp.leading).offset(-10)
|
||||
}
|
||||
phoneLabel.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(16)
|
||||
make.leading.equalTo(iconBox.snp.trailing).offset(12)
|
||||
make.trailing.equalTo(callButton.snp.leading).offset(-12)
|
||||
make.top.equalTo(nameLabel.snp.bottom).offset(8)
|
||||
make.leading.trailing.equalTo(nameLabel)
|
||||
}
|
||||
timeLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(phoneLabel.snp.bottom).offset(6)
|
||||
make.leading.trailing.equalTo(phoneLabel)
|
||||
make.bottom.equalToSuperview().offset(-16)
|
||||
}
|
||||
callButton.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().offset(-14)
|
||||
make.centerY.equalToSuperview()
|
||||
make.size.equalTo(40)
|
||||
make.leading.trailing.equalTo(nameLabel)
|
||||
}
|
||||
callButton.addTarget(self, action: #selector(callTapped), for: .touchUpInside)
|
||||
}
|
||||
@@ -424,10 +530,21 @@ private final class TravelAlbumInfoCard: UIView {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func apply(album: TravelAlbum) {
|
||||
func apply(album: TravelAlbum, coverURL: String) {
|
||||
phone = album.displayPhone
|
||||
phoneLabel.text = "手机号 \(TravelAlbumDisplayFormatter.maskPhone(album.displayPhone))"
|
||||
timeLabel.text = "创建时间 \(album.createdAt)"
|
||||
nameLabel.text = album.name.isEmpty ? "旅拍相册" : album.name
|
||||
phoneLabel.text = TravelAlbumDisplayFormatter.maskPhone(album.displayPhone)
|
||||
timeLabel.text = "创建于 \(TravelAlbumDisplayFormatter.creationTimeText(album.createdAt))"
|
||||
if let url = URL(string: coverURL), !coverURL.isEmpty {
|
||||
coverImageView.kf.setImage(with: url, placeholder: UIImage(systemName: "photo.fill"))
|
||||
coverImageView.tintColor = TravelAlbumDetailStyle.primary.withAlphaComponent(0.45)
|
||||
} else {
|
||||
coverImageView.image = UIImage(systemName: "photo.fill")
|
||||
coverImageView.tintColor = TravelAlbumDetailStyle.primary.withAlphaComponent(0.35)
|
||||
coverImageView.contentMode = .center
|
||||
}
|
||||
callButton.isEnabled = !phone.isEmpty
|
||||
callButton.alpha = phone.isEmpty ? 0.45 : 1
|
||||
}
|
||||
|
||||
@objc private func callTapped() {
|
||||
@@ -435,12 +552,11 @@ private final class TravelAlbumInfoCard: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
/// 旅拍相册素材网格 cell。
|
||||
/// 旅拍相册素材网格单元,展示正方形缩略图、文件名、大小和选择状态。
|
||||
private final class TravelAlbumMaterialCell: UICollectionViewCell {
|
||||
static let reuseIdentifier = "TravelAlbumMaterialCell"
|
||||
|
||||
private let imageView = UIImageView()
|
||||
private let statusLabel = UILabel()
|
||||
private let checkImageView = UIImageView()
|
||||
private let nameLabel = UILabel()
|
||||
private let sizeLabel = UILabel()
|
||||
@@ -449,25 +565,18 @@ private final class TravelAlbumMaterialCell: UICollectionViewCell {
|
||||
super.init(frame: frame)
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.clipsToBounds = true
|
||||
imageView.layer.cornerRadius = 8
|
||||
statusLabel.text = "已上传"
|
||||
statusLabel.font = .systemFont(ofSize: 10)
|
||||
statusLabel.textColor = .white
|
||||
statusLabel.backgroundColor = UIColor(hex: 0x34C759)
|
||||
statusLabel.layer.cornerRadius = 4
|
||||
statusLabel.clipsToBounds = true
|
||||
statusLabel.textAlignment = .center
|
||||
imageView.layer.cornerRadius = 12
|
||||
imageView.backgroundColor = UIColor(hex: 0xEAF4FF)
|
||||
checkImageView.tintColor = .white
|
||||
checkImageView.backgroundColor = UIColor.black.withAlphaComponent(0.35)
|
||||
checkImageView.layer.cornerRadius = 10
|
||||
nameLabel.font = .systemFont(ofSize: 11)
|
||||
nameLabel.textColor = AppColor.textPrimary
|
||||
checkImageView.layer.cornerRadius = 11
|
||||
nameLabel.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
nameLabel.textColor = TravelAlbumDetailStyle.textPrimary
|
||||
nameLabel.lineBreakMode = .byTruncatingMiddle
|
||||
sizeLabel.font = .systemFont(ofSize: 10)
|
||||
sizeLabel.textColor = AppColor.textTertiary
|
||||
sizeLabel.textColor = TravelAlbumDetailStyle.textSecondary
|
||||
|
||||
contentView.addSubview(imageView)
|
||||
imageView.addSubview(statusLabel)
|
||||
imageView.addSubview(checkImageView)
|
||||
contentView.addSubview(nameLabel)
|
||||
contentView.addSubview(sizeLabel)
|
||||
@@ -475,17 +584,12 @@ private final class TravelAlbumMaterialCell: UICollectionViewCell {
|
||||
make.top.leading.trailing.equalToSuperview()
|
||||
make.height.equalTo(imageView.snp.width)
|
||||
}
|
||||
statusLabel.snp.makeConstraints { make in
|
||||
make.top.leading.equalToSuperview().offset(4)
|
||||
make.height.equalTo(18)
|
||||
make.width.equalTo(48)
|
||||
}
|
||||
checkImageView.snp.makeConstraints { make in
|
||||
make.top.trailing.equalToSuperview().inset(4)
|
||||
make.size.equalTo(20)
|
||||
make.top.trailing.equalToSuperview().inset(6)
|
||||
make.size.equalTo(22)
|
||||
}
|
||||
nameLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(imageView.snp.bottom).offset(4)
|
||||
make.top.equalTo(imageView.snp.bottom).offset(6)
|
||||
make.leading.trailing.equalToSuperview()
|
||||
}
|
||||
sizeLabel.snp.makeConstraints { make in
|
||||
@@ -501,17 +605,20 @@ private final class TravelAlbumMaterialCell: UICollectionViewCell {
|
||||
|
||||
func apply(material: TravelAlbumMaterial, selectionMode: Bool, selected: Bool) {
|
||||
let urlString = material.coverUrl.isEmpty ? material.fileUrl : material.coverUrl
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
if let url = URL(string: urlString), !urlString.isEmpty {
|
||||
imageView.kf.setImage(with: url, placeholder: UIImage(systemName: "photo"))
|
||||
imageView.kf.setImage(with: url, placeholder: UIImage(systemName: "photo.fill"))
|
||||
} else {
|
||||
imageView.image = UIImage(systemName: "photo")
|
||||
imageView.tintColor = AppColor.primary
|
||||
imageView.backgroundColor = UIColor(hex: 0xEAF2FF)
|
||||
imageView.image = UIImage(systemName: "photo.fill")
|
||||
imageView.tintColor = TravelAlbumDetailStyle.primary.withAlphaComponent(0.35)
|
||||
imageView.contentMode = .center
|
||||
}
|
||||
checkImageView.isHidden = !selectionMode
|
||||
checkImageView.image = UIImage(systemName: selected ? "checkmark.circle.fill" : "circle")
|
||||
checkImageView.tintColor = selected ? AppColor.primary : .white
|
||||
nameLabel.text = material.fileName
|
||||
checkImageView.tintColor = selected ? TravelAlbumDetailStyle.primary : .white
|
||||
nameLabel.text = material.fileName.isEmpty ? "未命名照片" : material.fileName
|
||||
sizeLabel.text = TravelAlbumDisplayFormatter.fileSizeText(material.fileSize)
|
||||
accessibilityLabel = "\(nameLabel.text ?? "照片"),\(sizeLabel.text ?? "")"
|
||||
accessibilityValue = selectionMode ? (selected ? "已选择" : "未选择") : nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user