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:
2026-06-30 09:41:05 +08:00
parent 5692134efc
commit d2fe5d71e4
48 changed files with 6477 additions and 11 deletions

View File

@ -0,0 +1,158 @@
//
// CreateTravelAlbumSheet.swift
// suixinkan
//
// Created by Codex on 2026/6/29.
//
import SwiftUI
///
struct CreateTravelAlbumSheet: View {
let orders: [TravelAlbumAvailableOrder]
let isCreating: Bool
let onConfirm: (Int, String, String, String, TravelAlbumAvailableOrder?) -> Void
@Environment(\.dismiss) private var dismiss
@State private var mode = TravelAlbumEntryViewModel.modePreShoot
@State private var freeCount = ""
@State private var singlePrice = ""
@State private var packagePrice = ""
@State private var selectedOrder: TravelAlbumAvailableOrder?
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: AppMetrics.Spacing.medium) {
modeCard(
title: "先拍再买",
description: "拍完后分享给用户,用户在小程序上选择性购买",
selected: mode == TravelAlbumEntryViewModel.modePreShoot
) {
mode = TravelAlbumEntryViewModel.modePreShoot
}
modeCard(
title: "买了再拍",
description: "用户已在小程序下单,绑定订单后用户可直接选片",
selected: mode == TravelAlbumEntryViewModel.modePreOrder
) {
mode = TravelAlbumEntryViewModel.modePreOrder
}
if mode == TravelAlbumEntryViewModel.modePreShoot {
preShootFields
} else {
preOrderFields
}
}
.padding(AppMetrics.Spacing.pageHorizontal)
.padding(.vertical, AppMetrics.Spacing.medium)
}
.background(Color(hex: 0xF5F7FA))
.navigationTitle("新建相册任务")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("取消") { dismiss() }
}
ToolbarItem(placement: .confirmationAction) {
Button(isCreating ? "创建中" : "确认创建") {
onConfirm(mode, freeCount, singlePrice, packagePrice, selectedOrder)
}
.disabled(isCreating)
}
}
}
}
private var preShootFields: some View {
VStack(spacing: AppMetrics.Spacing.small) {
fieldRow(title: "免费张数", placeholder: "请输入免费张数", text: $freeCount, keyboard: .numberPad)
fieldRow(title: "单张价格(元)", placeholder: "请输入单张照片价格", text: $singlePrice, keyboard: .decimalPad)
fieldRow(title: "打包价(元)", placeholder: "可选", text: $packagePrice, keyboard: .decimalPad)
}
}
private var preOrderFields: some View {
VStack(alignment: .leading, spacing: AppMetrics.Spacing.small) {
Text("选择绑定订单")
.font(.system(size: AppMetrics.FontSize.subheadline, weight: .medium))
.foregroundStyle(AppDesign.textSecondary)
if orders.isEmpty {
Text("暂无可绑定订单")
.foregroundStyle(AppDesign.placeholder)
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(Color.white)
.clipShape(RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
} else {
ForEach(orders) { order in
Button {
selectedOrder = order
} label: {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(order.projectName)
.font(.system(size: AppMetrics.FontSize.body, weight: .medium))
.foregroundStyle(AppDesign.textPrimary)
Text("\(order.orderNumber) · \(order.userPhone)")
.font(.system(size: AppMetrics.FontSize.caption))
.foregroundStyle(AppDesign.textSecondary)
}
Spacer()
Image(systemName: selectedOrder?.orderNumber == order.orderNumber ? "checkmark.circle.fill" : "circle")
.foregroundStyle(selectedOrder?.orderNumber == order.orderNumber ? AppDesign.primary : AppDesign.placeholder)
}
.padding(AppMetrics.Spacing.medium)
.background(Color.white)
.clipShape(RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
}
.buttonStyle(.plain)
}
}
}
}
private func modeCard(title: String, description: String, selected: Bool, action: @escaping () -> Void) -> some View {
Button(action: action) {
HStack(alignment: .top, spacing: AppMetrics.Spacing.medium) {
Image(systemName: selected ? "largecircle.fill.circle" : "circle")
.foregroundStyle(selected ? AppDesign.primary : AppDesign.placeholder)
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.system(size: AppMetrics.FontSize.body, weight: .semibold))
.foregroundStyle(AppDesign.textPrimary)
Text(description)
.font(.system(size: AppMetrics.FontSize.caption))
.foregroundStyle(AppDesign.textSecondary)
.multilineTextAlignment(.leading)
}
Spacer()
}
.padding(AppMetrics.Spacing.large)
.background(Color.white)
.overlay(
RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card)
.stroke(selected ? AppDesign.primary : Color.clear, lineWidth: 1.5)
)
.clipShape(RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
}
.buttonStyle(.plain)
}
private func fieldRow(title: String, placeholder: String, text: Binding<String>, keyboard: UIKeyboardType) -> some View {
VStack(alignment: .leading, spacing: 6) {
Text(title)
.font(.system(size: AppMetrics.FontSize.caption))
.foregroundStyle(AppDesign.textSecondary)
TextField(placeholder, text: text)
.keyboardType(keyboard)
.padding(AppMetrics.Spacing.medium)
.background(Color.white)
.clipShape(RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
}
}
}