// // 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 }