Introduce travel album entry with Sony PTP tethering pipeline, profile space settings page, home routing updates, Launch Screen storyboard, and related tests/docs. Co-authored-by: Cursor <cursoragent@cursor.com>
157 lines
6.0 KiB
Swift
157 lines
6.0 KiB
Swift
//
|
||
// TravelAlbumDetailView.swift
|
||
// suixinkan
|
||
//
|
||
// Created by Codex on 2026/6/29.
|
||
//
|
||
|
||
import SwiftUI
|
||
|
||
/// 旅拍相册详情页,展示素材网格与管理操作。
|
||
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
|
||
|
||
@StateObject private var viewModel = TravelAlbumDetailViewModel()
|
||
@State private var showDeleteConfirm = false
|
||
|
||
private var photoStore: WiredTransferPhotoStore {
|
||
WiredTransferPhotoStore(
|
||
accountPrefixProvider: { accountContext.accountCachePrefix ?? "guest_" },
|
||
userIDProvider: { accountContext.profile?.userId ?? "" }
|
||
)
|
||
}
|
||
|
||
var body: some View {
|
||
ScrollView {
|
||
VStack(alignment: .leading, spacing: AppMetrics.Spacing.medium) {
|
||
if let album = viewModel.album {
|
||
albumHeader(album)
|
||
}
|
||
|
||
if viewModel.materials.isEmpty, !viewModel.isLoading {
|
||
AppContentUnavailableView("暂无素材", systemImage: "photo.on.rectangle")
|
||
.frame(maxWidth: .infinity, minHeight: 200)
|
||
} else {
|
||
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], spacing: 8) {
|
||
ForEach(viewModel.materials) { material in
|
||
materialCell(material)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.padding(.horizontal, AppMetrics.Spacing.pageHorizontal)
|
||
.padding(.vertical, AppMetrics.Spacing.medium)
|
||
}
|
||
.background(Color(hex: 0xF5F7FA))
|
||
.navigationTitle(viewModel.album?.name ?? "相册详情")
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.toolbar {
|
||
ToolbarItemGroup(placement: .topBarTrailing) {
|
||
if let album = viewModel.album {
|
||
NavigationLink {
|
||
WiredCameraTransferView(context: WiredTransferContext(
|
||
albumId: album.id,
|
||
albumName: album.name,
|
||
phone: album.displayPhone,
|
||
orderNumber: album.orderNumber
|
||
))
|
||
} label: {
|
||
Image(systemName: "cable.connector")
|
||
}
|
||
.accessibilityLabel("有线传图")
|
||
}
|
||
|
||
Button(role: .destructive) {
|
||
showDeleteConfirm = true
|
||
} label: {
|
||
Image(systemName: "trash")
|
||
}
|
||
.accessibilityLabel("删除相册")
|
||
}
|
||
}
|
||
.confirmationDialog("确认删除该相册?", isPresented: $showDeleteConfirm, titleVisibility: .visible) {
|
||
Button("删除", role: .destructive) {
|
||
Task { await deleteAlbum() }
|
||
}
|
||
Button("取消", role: .cancel) {}
|
||
}
|
||
.task(id: albumID) {
|
||
globalLoading.show()
|
||
defer { globalLoading.hide() }
|
||
await viewModel.load(api: travelAlbumAPI, albumID: albumID)
|
||
}
|
||
.refreshable {
|
||
await viewModel.load(api: travelAlbumAPI, albumID: albumID)
|
||
}
|
||
.onChange(of: viewModel.errorMessage) { message in
|
||
guard let message, !message.isEmpty else { return }
|
||
toastCenter.show(message)
|
||
}
|
||
}
|
||
|
||
private func albumHeader(_ album: TravelAlbumItem) -> some View {
|
||
VStack(alignment: .leading, spacing: 8) {
|
||
Text(album.type == 1 ? "先拍再买" : "买了再拍")
|
||
.font(.system(size: AppMetrics.FontSize.caption))
|
||
.foregroundStyle(AppDesign.primary)
|
||
if !album.displayPhone.isEmpty {
|
||
Label(album.displayPhone, systemImage: "phone.fill")
|
||
.font(.system(size: AppMetrics.FontSize.subheadline))
|
||
.foregroundStyle(AppDesign.textSecondary)
|
||
}
|
||
if !album.orderNumber.isEmpty {
|
||
Text("订单号:\(album.orderNumber)")
|
||
.font(.system(size: AppMetrics.FontSize.caption))
|
||
.foregroundStyle(AppDesign.textSecondary)
|
||
}
|
||
}
|
||
.frame(maxWidth: .infinity, alignment: .leading)
|
||
.padding(AppMetrics.Spacing.large)
|
||
.background(Color.white)
|
||
.clipShape(RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
|
||
}
|
||
|
||
private func materialCell(_ material: TravelAlbumMaterial) -> some View {
|
||
ZStack(alignment: .topTrailing) {
|
||
RemoteImage(urlString: material.coverURL.isEmpty ? material.fileURL : material.coverURL) {
|
||
Color(hex: 0xE5E7EB)
|
||
}
|
||
.aspectRatio(1, contentMode: .fill)
|
||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||
|
||
Menu {
|
||
Button(role: .destructive) {
|
||
Task {
|
||
await viewModel.deleteMaterial(api: travelAlbumAPI, materialID: material.id, albumID: albumID)
|
||
}
|
||
} label: {
|
||
Label("删除", systemImage: "trash")
|
||
}
|
||
} label: {
|
||
Image(systemName: "ellipsis")
|
||
.padding(6)
|
||
.background(Color.black.opacity(0.35))
|
||
.foregroundStyle(.white)
|
||
.clipShape(Circle())
|
||
.padding(4)
|
||
}
|
||
}
|
||
}
|
||
|
||
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()
|
||
}
|
||
}
|
||
}
|