Files
suixinkan_ios_new/suixinkan/Features/TravelAlbum/Views/TravelAlbumEntryView.swift
汉秋 e91a46c315 修复有线传图新拍照片不显示,并优化旅拍相册列表交互。
未绑定相册的新拍照片被错误过滤导致列表始终为空;同时调整入口卡片点击区域与详情素材网格布局。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 10:20:26 +08:00

222 lines
9.3 KiB
Swift
Raw Permalink 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.

//
// TravelAlbumEntryView.swift
// suixinkan
//
// Created by Codex on 2026/6/29.
//
import SwiftUI
///
struct TravelAlbumEntryView: View {
@EnvironmentObject private var accountContext: AccountContext
@Environment(\.travelAlbumAPI) private var travelAlbumAPI
@EnvironmentObject private var toastCenter: ToastCenter
@Environment(\.globalLoading) private var globalLoading
@StateObject private var viewModel = TravelAlbumEntryViewModel()
var body: some View {
ScrollView {
VStack(spacing: AppMetrics.Spacing.medium) {
createHeroCard
if viewModel.isLoading, viewModel.albums.isEmpty {
ProgressView()
.frame(maxWidth: .infinity, minHeight: 200)
} else if viewModel.albums.isEmpty {
AppContentUnavailableView("暂无相册任务", systemImage: "photo.on.rectangle.angled", description: Text("点击“新建相册任务”开始创建任务"))
.frame(maxWidth: .infinity, minHeight: 240)
} else {
LazyVStack(spacing: AppMetrics.Spacing.medium) {
ForEach(viewModel.albums) { album in
TravelAlbumCard(
album: album,
onOpenDetail: { TravelAlbumSession.currentAlbum = album },
onOpenWiredTransfer: { TravelAlbumSession.currentAlbum = album },
onOpenCode: {
Task {
await viewModel.openAlbumCode(api: travelAlbumAPI, album: album)
}
}
)
}
}
}
}
.padding(.horizontal, AppMetrics.Spacing.pageHorizontal)
.padding(.vertical, AppMetrics.Spacing.medium)
}
.background(Color(hex: 0xF5F7FA))
.navigationTitle("新增相册")
.navigationBarTitleDisplayMode(.inline)
.refreshable {
await reload(showLoading: false)
}
.task {
await reload(showLoading: viewModel.albums.isEmpty)
}
.sheet(isPresented: $viewModel.showCreateSheet) {
CreateTravelAlbumSheet(
orders: viewModel.bindableOrders,
isCreating: viewModel.isCreating,
onConfirm: { mode, freeCount, singlePrice, packagePrice, order in
Task {
let success = await viewModel.confirmCreateTask(
api: travelAlbumAPI,
mode: mode,
freeCount: freeCount,
singlePrice: singlePrice,
packagePrice: packagePrice,
order: order,
scenicSelected: accountContext.currentScenic != nil
)
if success {
toastCenter.show("任务创建成功")
} else if let message = viewModel.errorMessage {
toastCenter.show(message)
}
}
}
)
.presentationDetents([.large])
}
.sheet(item: $viewModel.codeSheet) { state in
TravelAlbumCodeSheet(state: state)
}
.onChange(of: viewModel.errorMessage) { message in
guard let message, !message.isEmpty, !viewModel.showCreateSheet else { return }
toastCenter.show(message)
}
}
private var createHeroCard: some View {
Button {
Task {
await viewModel.openCreateSheet(
api: travelAlbumAPI,
scenicSelected: accountContext.currentScenic != nil
)
}
} label: {
HStack(spacing: AppMetrics.Spacing.medium) {
Image(systemName: "plus.circle.fill")
.font(.system(size: 28))
.foregroundStyle(AppDesign.primary)
VStack(alignment: .leading, spacing: 4) {
Text(viewModel.isCreating ? "新建中..." : "新建相册任务")
.font(.system(size: AppMetrics.FontSize.title3, weight: .semibold))
.foregroundStyle(AppDesign.textPrimary)
Text("创建旅拍相册并开始有线传图")
.font(.system(size: AppMetrics.FontSize.subheadline))
.foregroundStyle(AppDesign.textSecondary)
}
Spacer()
Image(systemName: "chevron.right")
.foregroundStyle(AppDesign.placeholder)
}
.padding(AppMetrics.Spacing.large)
.background(Color.white)
.clipShape(RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
}
.buttonStyle(.plain)
.disabled(viewModel.isCreating)
}
private func reload(showLoading: Bool) async {
if showLoading { globalLoading.show() }
defer { if showLoading { globalLoading.hide() } }
await viewModel.loadAlbums(api: travelAlbumAPI)
}
}
private struct TravelAlbumCard: View {
let album: TravelAlbumItem
let onOpenDetail: () -> Void
let onOpenWiredTransfer: () -> Void
let onOpenCode: () -> Void
var body: some View {
NavigationLink {
TravelAlbumDetailView(albumID: album.id)
.onAppear { onOpenDetail() }
} label: {
VStack(alignment: .leading, spacing: AppMetrics.Spacing.small) {
HStack(alignment: .top, spacing: AppMetrics.Spacing.medium) {
RemoteImage(urlString: album.coverURL) {
Color(hex: 0xE5E7EB)
.overlay { Image(systemName: "photo").foregroundStyle(AppDesign.placeholder) }
}
.frame(width: 72, height: 72)
.clipShape(RoundedRectangle(cornerRadius: 8))
VStack(alignment: .leading, spacing: 6) {
Text(album.name)
.font(.system(size: AppMetrics.FontSize.title3, weight: .semibold))
.foregroundStyle(AppDesign.textPrimary)
.lineLimit(1)
if !album.displayPhone.isEmpty {
Label(album.displayPhone, systemImage: "phone.fill")
.font(.system(size: AppMetrics.FontSize.caption))
.foregroundStyle(AppDesign.textSecondary)
}
Text(album.type == 1 ? "先拍再买" : "买了再拍")
.font(.system(size: AppMetrics.FontSize.caption))
.foregroundStyle(AppDesign.primary)
}
Spacer()
Image(systemName: "chevron.right")
.font(.system(size: AppMetrics.FontSize.caption, weight: .semibold))
.foregroundStyle(AppDesign.placeholder)
}
//
HStack(spacing: AppMetrics.Spacing.small) {
actionChip(title: "拍传", systemImage: "cable.connector")
actionChip(title: "相册码", systemImage: "qrcode")
}
.hidden()
}
.padding(AppMetrics.Spacing.large)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.white)
.clipShape(RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.overlay(alignment: .bottomLeading) {
HStack(spacing: AppMetrics.Spacing.small) {
NavigationLink {
WiredCameraTransferView(context: WiredTransferContext(
albumId: album.id,
albumName: album.name,
phone: album.displayPhone,
orderNumber: album.orderNumber
))
.onAppear { onOpenWiredTransfer() }
} label: {
actionChip(title: "拍传", systemImage: "cable.connector")
}
Button(action: onOpenCode) {
actionChip(title: "相册码", systemImage: "qrcode")
}
}
.padding(.horizontal, AppMetrics.Spacing.large)
.padding(.bottom, AppMetrics.Spacing.large)
}
}
private func actionChip(title: String, systemImage: String) -> some View {
HStack(spacing: 4) {
Image(systemName: systemImage)
Text(title)
}
.font(.system(size: AppMetrics.FontSize.caption, weight: .medium))
.foregroundStyle(AppDesign.primary)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(AppDesign.primary.opacity(0.08))
.clipShape(Capsule())
}
}