Add TravelAlbum, ProfileSpace, and wired camera transfer modules.
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>
This commit is contained in:
207
suixinkan/Features/TravelAlbum/Views/TravelAlbumEntryView.swift
Normal file
207
suixinkan/Features/TravelAlbum/Views/TravelAlbumEntryView.swift
Normal file
@ -0,0 +1,207 @@
|
||||
//
|
||||
// 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 {
|
||||
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()
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
NavigationLink {
|
||||
TravelAlbumDetailView(albumID: album.id)
|
||||
.onAppear { onOpenDetail() }
|
||||
} label: {
|
||||
actionChip(title: "详情", systemImage: "info.circle")
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(AppMetrics.Spacing.large)
|
||||
.background(Color.white)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user