Files
suixinkan_ios_new/suixinkan/Features/TravelAlbum/Views/TravelAlbumDetailView.swift
汉秋 5e620623eb 对齐旅拍相册详情页 Android UI,并修复网格预览与布局问题。
重构相册详情为信息卡片、Tab 筛选、排序、批量删除与底部上传栏;修复网格重叠、禁用按钮蒙层,并支持点击预览大图。同步扩展素材列表 API 与 ViewModel 分页逻辑,并优化有线传图缩略图与传输性能。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 13:53:59 +08:00

243 lines
9.6 KiB
Swift
Raw 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.

//
// TravelAlbumDetailView.swift
// suixinkan
//
// Created by Codex on 2026/6/29.
//
import SwiftUI
/// Android TravelAlbumDetailScreen
struct TravelAlbumDetailView: View {
let albumID: Int
@EnvironmentObject private var accountContext: AccountContext
@Environment(\.travelAlbumAPI) private var travelAlbumAPI
@EnvironmentObject private var toastCenter: ToastCenter
@Environment(\.globalLoading) private var globalLoading
@Environment(\.dismiss) private var dismiss
@Environment(\.openURL) private var openURL
@StateObject private var viewModel = TravelAlbumDetailViewModel()
@State private var showMoreMenu = false
@State private var navigateToWiredTransfer = false
@State private var didReturnFromWiredTransfer = false
@State private var previewPayload: TravelAlbumMaterialPreviewPayload?
private var photoStore: WiredTransferPhotoStore {
WiredTransferPhotoStore(
accountPrefixProvider: { accountContext.accountCachePrefix ?? "guest_" },
userIDProvider: { accountContext.profile?.userId ?? "" }
)
}
var body: some View {
Group {
if viewModel.isLoading, viewModel.album == nil {
ProgressView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else if viewModel.album != nil {
contentView
} else {
Text("加载失败")
.font(.system(size: 14))
.foregroundStyle(TravelAlbumDetailDesign.text333)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
.background(TravelAlbumDetailDesign.pageBackground.ignoresSafeArea())
.navigationTitle("相册管理")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Menu {
Button("删除相册", role: .destructive) {
viewModel.showDeleteAlbumConfirm = true
}
} label: {
Image(systemName: "ellipsis")
.foregroundStyle(TravelAlbumDetailDesign.text333)
}
.accessibilityLabel("更多")
}
}
.safeAreaInset(edge: .bottom) {
bottomBar
}
.background(wiredTransferNavigationLink)
.sheet(item: $previewPayload) { payload in
WiredTransferPhotoPreviewSheet(
imageSources: payload.sources,
startIndex: payload.startIndex,
onDismiss: { previewPayload = nil }
)
}
.confirmationDialog("删除相册", isPresented: $viewModel.showDeleteAlbumConfirm, titleVisibility: .visible) {
Button("删除", role: .destructive) {
Task { await deleteAlbum() }
}
Button("取消", role: .cancel) {}
} message: {
Text("确定删除该相册吗?已有购买素材的相册无法删除。")
}
.confirmationDialog("删除素材", isPresented: $viewModel.showDeleteMaterialConfirm, titleVisibility: .visible) {
Button("删除", role: .destructive) {
Task { await deleteSelectedMaterials() }
}
Button("取消", role: .cancel) {}
} message: {
Text("确定删除选中的 \(viewModel.selectedMaterialIDs.count) 张素材吗?")
}
.task(id: albumID) {
globalLoading.show()
defer { globalLoading.hide() }
await viewModel.refreshAll(api: travelAlbumAPI, albumID: albumID)
}
.onChange(of: navigateToWiredTransfer) { isActive in
guard !isActive, didReturnFromWiredTransfer else { return }
didReturnFromWiredTransfer = false
Task { await viewModel.refreshAll(api: travelAlbumAPI, albumID: albumID) }
}
.onChange(of: viewModel.errorMessage) { message in
guard let message, !message.isEmpty else { return }
toastCenter.show(message)
viewModel.errorMessage = nil
}
}
private var contentView: some View {
VStack(spacing: TravelAlbumDetailDesign.sectionSpacing) {
if let album = viewModel.album {
TravelAlbumDetailInfoCard(album: album) { phone in
dialPhone(phone)
}
}
TravelAlbumDetailPhotoManageCard(
materials: viewModel.materials,
allPhotoCount: viewModel.allPhotoCount,
selectedTab: viewModel.selectedTab,
orderBy: viewModel.orderBy,
isSelectionMode: viewModel.isSelectionMode,
selectedMaterialIDs: viewModel.selectedMaterialIDs,
onTabSelect: { tab in
Task { await viewModel.selectTab(tab, api: travelAlbumAPI) }
},
onOrderBySelect: { order in
Task { await viewModel.setOrderBy(order, api: travelAlbumAPI) }
},
onToggleSelectionMode: {
viewModel.toggleSelectionMode()
},
onMaterialClick: { material in
viewModel.toggleMaterialSelection(material)
},
onMaterialPreview: { material in
openMaterialPreview(material)
},
onLoadMore: { index in
Task { await viewModel.loadMoreIfNeeded(lastVisibleIndex: index, api: travelAlbumAPI) }
}
)
.frame(maxHeight: .infinity)
}
.padding(.horizontal, TravelAlbumDetailDesign.contentHorizontalPadding)
.padding(.top, 12)
.padding(.bottom, 8)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
}
private var bottomBar: some View {
VStack(spacing: 8) {
if viewModel.isSelectionMode, !viewModel.selectedMaterialIDs.isEmpty {
Button {
viewModel.requestDeleteSelectedMaterials()
} label: {
Text("删除选中(\(viewModel.selectedMaterialIDs.count))")
.font(.system(size: 16))
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
.frame(height: TravelAlbumDetailDesign.bottomButtonHeight)
.background(TravelAlbumDetailDesign.deleteRed)
.clipShape(RoundedRectangle(cornerRadius: TravelAlbumDetailDesign.bottomButtonCornerRadius))
}
.buttonStyle(.plain)
}
Button {
didReturnFromWiredTransfer = true
navigateToWiredTransfer = true
} label: {
Text("上传照片")
.font(.system(size: 16, weight: .medium))
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
.frame(height: TravelAlbumDetailDesign.bottomButtonHeight)
.background(AppDesign.primary)
.clipShape(RoundedRectangle(cornerRadius: TravelAlbumDetailDesign.bottomButtonCornerRadius))
}
.buttonStyle(.plain)
}
.padding(.horizontal, TravelAlbumDetailDesign.bottomBarHorizontalPadding)
.padding(.vertical, TravelAlbumDetailDesign.bottomBarVerticalPadding)
.background(TravelAlbumDetailDesign.pageBackground)
}
@ViewBuilder
private var wiredTransferNavigationLink: some View {
if let context = viewModel.wiredTransferContext() {
NavigationLink(isActive: $navigateToWiredTransfer) {
WiredCameraTransferView(context: context)
} label: {
EmptyView()
}
.hidden()
}
}
///
private func dialPhone(_ phone: String) {
let trimmed = phone.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty, let url = URL(string: "tel:\(trimmed)") else { return }
openURL(url)
}
///
private func openMaterialPreview(_ material: TravelAlbumMaterial) {
let previewMaterials = viewModel.materials.filter { !$0.previewURLString.isEmpty }
guard !material.previewURLString.isEmpty else {
toastCenter.show("暂无法预览该照片")
return
}
let sources = previewMaterials.map(\.previewURLString)
let startIndex = previewMaterials.firstIndex(where: { $0.id == material.id }) ?? 0
previewPayload = TravelAlbumMaterialPreviewPayload(sources: sources, startIndex: startIndex)
}
private func deleteAlbum() async {
globalLoading.show()
defer { globalLoading.hide() }
let success = await viewModel.deleteAlbum(api: travelAlbumAPI, albumID: albumID, photoStore: photoStore)
if success {
toastCenter.show("相册已删除")
dismiss()
}
}
private func deleteSelectedMaterials() async {
globalLoading.show()
defer { globalLoading.hide() }
let success = await viewModel.confirmDeleteSelectedMaterials(api: travelAlbumAPI)
if success {
toastCenter.show("删除成功")
}
}
}
/// Sheet
private struct TravelAlbumMaterialPreviewPayload: Identifiable {
let id = UUID()
let sources: [String]
let startIndex: Int
}