重构相册详情为信息卡片、Tab 筛选、排序、批量删除与底部上传栏;修复网格重叠、禁用按钮蒙层,并支持点击预览大图。同步扩展素材列表 API 与 ViewModel 分页逻辑,并优化有线传图缩略图与传输性能。 Co-authored-by: Cursor <cursoragent@cursor.com>
243 lines
9.6 KiB
Swift
243 lines
9.6 KiB
Swift
//
|
||
// 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
|
||
}
|