对齐旅拍相册详情页 Android UI,并修复网格预览与布局问题。

重构相册详情为信息卡片、Tab 筛选、排序、批量删除与底部上传栏;修复网格重叠、禁用按钮蒙层,并支持点击预览大图。同步扩展素材列表 API 与 ViewModel 分页逻辑,并优化有线传图缩略图与传输性能。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-30 13:53:59 +08:00
parent c71b45bdfd
commit 5e620623eb
23 changed files with 2000 additions and 288 deletions

View File

@ -0,0 +1,34 @@
//
// MiddleTruncatedText.swift
// suixinkan
//
// Android MiddleEllipsisTextView
//
import SwiftUI
import UIKit
///
struct MiddleTruncatedText: UIViewRepresentable {
let text: String
var font: UIFont = .systemFont(ofSize: 11, weight: .regular)
var textColor: UIColor = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 1)
/// UILabel
func makeUIView(context: Context) -> UILabel {
let label = UILabel()
label.lineBreakMode = .byTruncatingMiddle
label.numberOfLines = 1
label.setContentHuggingPriority(.required, for: .vertical)
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
label.setContentCompressionResistancePriority(.required, for: .vertical)
return label
}
///
func updateUIView(_ uiView: UILabel, context: Context) {
uiView.text = text
uiView.font = font
uiView.textColor = textColor
}
}

View File

@ -0,0 +1,41 @@
//
// TravelAlbumDetailDesign.swift
// suixinkan
//
// Token Android TravelAlbumDetailScreen
//
import SwiftUI
///
enum TravelAlbumDetailDesign {
static let pageBackground = Color(hex: 0xF5F5F5)
static let text333 = Color(hex: 0x333333)
static let text999 = Color(hex: 0x999999)
static let lightBlue = Color(hex: 0xEAF4FF)
static let deleteRed = Color(hex: 0xE53935)
static let uploadedGreen = Color(hex: 0x34C759)
static let borderLight = Color(hex: 0xEEEEEE)
static let selectionInactiveBackground = Color(hex: 0xF4F4F4)
static let cardShadow = Color.black.opacity(0.08)
static let cardCornerRadius: CGFloat = 12
static let cardShadowRadius: CGFloat = 2
static let infoIconSize: CGFloat = 48
static let infoIconCornerRadius: CGFloat = 10
static let callButtonSize: CGFloat = 40
static let tabPillCornerRadius: CGFloat = 18
static let actionCircleSize: CGFloat = 32
static let gridHorizontalSpacing: CGFloat = 8
static let gridVerticalSpacing: CGFloat = 12
static let gridPadding: CGFloat = 12
static let thumbnailCornerRadius: CGFloat = 8
static let uploadedBadgeCornerRadius: CGFloat = 4
static let bottomButtonHeight: CGFloat = 48
static let bottomButtonCornerRadius: CGFloat = 10
static let contentHorizontalPadding: CGFloat = 16
static let sectionSpacing: CGFloat = 12
static let bottomBarHorizontalPadding: CGFloat = 20
static let bottomBarVerticalPadding: CGFloat = 12
static let selectionCheckSize: CGFloat = 20
}

View File

@ -0,0 +1,57 @@
//
// TravelAlbumDetailInfoCard.swift
// suixinkan
//
// Android AlbumInfoCard
//
import SwiftUI
///
struct TravelAlbumDetailInfoCard: View {
let album: TravelAlbumItem
let onCallClick: (String) -> Void
var body: some View {
HStack(spacing: 12) {
RoundedRectangle(cornerRadius: TravelAlbumDetailDesign.infoIconCornerRadius)
.fill(AppDesign.primary)
.frame(width: TravelAlbumDetailDesign.infoIconSize, height: TravelAlbumDetailDesign.infoIconSize)
.overlay {
Image(systemName: "list.clipboard.fill")
.font(.system(size: 28))
.foregroundStyle(.white)
}
VStack(alignment: .leading, spacing: 6) {
Text("手机号 \(OrderListUI.maskPhone(album.displayPhone))")
.font(.system(size: 14, weight: .medium))
.foregroundStyle(TravelAlbumDetailDesign.text333)
Text("创建时间 \(album.createdAt)")
.font(.system(size: 12))
.foregroundStyle(TravelAlbumDetailDesign.text999)
}
.frame(maxWidth: .infinity, alignment: .leading)
Button {
onCallClick(album.displayPhone)
} label: {
Circle()
.fill(TravelAlbumDetailDesign.lightBlue)
.frame(width: TravelAlbumDetailDesign.callButtonSize, height: TravelAlbumDetailDesign.callButtonSize)
.overlay {
Image(systemName: "phone.fill")
.font(.system(size: 20))
.foregroundStyle(AppDesign.primary)
}
}
.buttonStyle(.plain)
.accessibilityLabel("拨打电话")
}
.padding(.horizontal, 14)
.padding(.vertical, 14)
.background(Color.white)
.clipShape(RoundedRectangle(cornerRadius: TravelAlbumDetailDesign.cardCornerRadius))
.shadow(color: TravelAlbumDetailDesign.cardShadow, radius: TravelAlbumDetailDesign.cardShadowRadius, x: 0, y: 1)
}
}

View File

@ -0,0 +1,80 @@
//
// TravelAlbumDetailMaterialGridItem.swift
// suixinkan
//
// Android MaterialGridItem
//
import SwiftUI
///
struct TravelAlbumDetailMaterialGridItem: View {
let material: TravelAlbumMaterial
let isSelectionMode: Bool
let isSelected: Bool
let onSelect: () -> Void
let onPreview: () -> Void
var body: some View {
VStack(alignment: .leading, spacing: 4) {
thumbnailSection
MiddleTruncatedText(
text: material.fileName,
font: .systemFont(ofSize: 11),
textColor: UIColor(TravelAlbumDetailDesign.text333)
)
.frame(maxWidth: .infinity, minHeight: 14, maxHeight: 14, alignment: .leading)
Text(WiredTransferPhotoRecord.formatFileSize(Int64(material.fileSize)))
.font(.system(size: 10))
.foregroundStyle(TravelAlbumDetailDesign.text999)
.lineLimit(1)
}
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
.onTapGesture {
if isSelectionMode {
onSelect()
} else {
onPreview()
}
}
}
/// 1:1
private var thumbnailSection: some View {
Color.clear
.aspectRatio(1, contentMode: .fit)
.overlay {
RemoteImage(
urlString: material.thumbnailURLString,
contentMode: .fill
) {
Color(hex: 0xE5E7EB)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.clipShape(RoundedRectangle(cornerRadius: TravelAlbumDetailDesign.thumbnailCornerRadius))
.overlay(alignment: .topLeading) {
Text("已上传")
.font(.system(size: 10))
.foregroundStyle(.white)
.padding(.horizontal, 4)
.padding(.vertical, 2)
.background(TravelAlbumDetailDesign.uploadedGreen)
.clipShape(RoundedRectangle(cornerRadius: TravelAlbumDetailDesign.uploadedBadgeCornerRadius))
.padding(4)
}
.overlay(alignment: .topTrailing) {
if isSelectionMode {
Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
.font(.system(size: TravelAlbumDetailDesign.selectionCheckSize))
.foregroundStyle(isSelected ? AppDesign.primary : .white)
.padding(4)
.background(Color.black.opacity(0.4), in: Circle())
.padding(4)
}
}
}
}

View File

@ -0,0 +1,153 @@
//
// TravelAlbumDetailPhotoManageCard.swift
// suixinkan
//
// Android PhotoManageCard
//
import SwiftUI
///
struct TravelAlbumDetailPhotoManageCard: View {
let materials: [TravelAlbumMaterial]
let allPhotoCount: Int
let selectedTab: Int
let orderBy: Int
let isSelectionMode: Bool
let selectedMaterialIDs: Set<Int>
let onTabSelect: (Int) -> Void
let onOrderBySelect: (Int) -> Void
let onToggleSelectionMode: () -> Void
let onMaterialClick: (TravelAlbumMaterial) -> Void
let onMaterialPreview: (TravelAlbumMaterial) -> Void
let onLoadMore: (Int) -> Void
private let gridColumns = [
GridItem(.flexible(), spacing: TravelAlbumDetailDesign.gridHorizontalSpacing),
GridItem(.flexible(), spacing: TravelAlbumDetailDesign.gridHorizontalSpacing),
GridItem(.flexible())
]
var body: some View {
VStack(spacing: 0) {
tabToolbar
Divider().overlay(TravelAlbumDetailDesign.borderLight)
photoGrid
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.background(Color.white)
.clipShape(RoundedRectangle(cornerRadius: TravelAlbumDetailDesign.cardCornerRadius))
.shadow(color: TravelAlbumDetailDesign.cardShadow, radius: TravelAlbumDetailDesign.cardShadowRadius, x: 0, y: 1)
}
private var tabToolbar: some View {
HStack(spacing: 8) {
HStack(spacing: 8) {
tabPill(
text: "全部照片\(allPhotoCount)",
selected: selectedTab == TravelAlbumDetailViewModel.tabAll
) {
onTabSelect(TravelAlbumDetailViewModel.tabAll)
}
tabPill(
text: "已购照片",
selected: selectedTab == TravelAlbumDetailViewModel.tabPurchased
) {
onTabSelect(TravelAlbumDetailViewModel.tabPurchased)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
sortMenu
if selectedTab == TravelAlbumDetailViewModel.tabAll {
Button(action: onToggleSelectionMode) {
Circle()
.fill(isSelectionMode ? TravelAlbumDetailDesign.lightBlue : TravelAlbumDetailDesign.selectionInactiveBackground)
.frame(width: TravelAlbumDetailDesign.actionCircleSize, height: TravelAlbumDetailDesign.actionCircleSize)
.overlay {
Image(systemName: isSelectionMode ? "checkmark.circle.fill" : "circle")
.font(.system(size: 18))
.foregroundStyle(isSelectionMode ? AppDesign.primary : TravelAlbumDetailDesign.text999)
}
}
.buttonStyle(.plain)
.accessibilityLabel("选择")
}
}
.padding(.horizontal, 12)
.padding(.vertical, 12)
}
private var sortMenu: some View {
Menu {
ForEach(TravelAlbumDetailViewModel.sortOptions, id: \.value) { option in
Button {
onOrderBySelect(option.value)
} label: {
if orderBy == option.value {
Label(option.label, systemImage: "checkmark")
} else {
Text(option.label)
}
}
}
} label: {
Circle()
.fill(TravelAlbumDetailDesign.lightBlue)
.frame(width: TravelAlbumDetailDesign.actionCircleSize, height: TravelAlbumDetailDesign.actionCircleSize)
.overlay {
Image(systemName: "line.3.horizontal.decrease")
.font(.system(size: 16))
.foregroundStyle(AppDesign.primary)
}
}
.accessibilityLabel("筛选")
}
private var photoGrid: some View {
ScrollView {
if materials.isEmpty {
Text("暂无照片")
.font(.system(size: 14))
.foregroundStyle(TravelAlbumDetailDesign.text999)
.frame(maxWidth: .infinity, minHeight: 180)
} else {
LazyVGrid(
columns: gridColumns,
spacing: TravelAlbumDetailDesign.gridVerticalSpacing
) {
ForEach(Array(materials.enumerated()), id: \.element.id) { index, material in
TravelAlbumDetailMaterialGridItem(
material: material,
isSelectionMode: isSelectionMode,
isSelected: selectedMaterialIDs.contains(material.id),
onSelect: { onMaterialClick(material) },
onPreview: { onMaterialPreview(material) }
)
.onAppear {
onLoadMore(index)
}
}
}
.padding(TravelAlbumDetailDesign.gridPadding)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.clipped()
}
/// Tab
private func tabPill(text: String, selected: Bool, action: @escaping () -> Void) -> some View {
Button(action: action) {
Text(text)
.font(.system(size: 13, weight: selected ? .medium : .regular))
.foregroundStyle(selected ? Color.white : AppDesign.primary)
.padding(.horizontal, 14)
.padding(.vertical, 7)
.background(selected ? AppDesign.primary : TravelAlbumDetailDesign.lightBlue)
.clipShape(RoundedRectangle(cornerRadius: TravelAlbumDetailDesign.tabPillCornerRadius))
}
.buttonStyle(.plain)
}
}

View File

@ -7,7 +7,7 @@
import SwiftUI
///
/// Android TravelAlbumDetailScreen
struct TravelAlbumDetailView: View {
let albumID: Int
@ -16,9 +16,13 @@ struct TravelAlbumDetailView: View {
@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 showDeleteConfirm = false
@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(
@ -28,125 +32,186 @@ struct TravelAlbumDetailView: View {
}
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)
}
}
}
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)
}
.padding(.horizontal, AppMetrics.Spacing.pageHorizontal)
.padding(.vertical, AppMetrics.Spacing.medium)
}
.background(Color(hex: 0xF5F7FA))
.navigationTitle(viewModel.album?.name ?? "相册详情")
.background(TravelAlbumDetailDesign.pageBackground.ignoresSafeArea())
.navigationTitle("相册管理")
.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")
ToolbarItem(placement: .topBarTrailing) {
Menu {
Button("删除相册", role: .destructive) {
viewModel.showDeleteAlbumConfirm = true
}
.accessibilityLabel("有线传图")
}
Button(role: .destructive) {
showDeleteConfirm = true
} label: {
Image(systemName: "trash")
Image(systemName: "ellipsis")
.foregroundStyle(TravelAlbumDetailDesign.text333)
}
.accessibilityLabel("删除相册")
.accessibilityLabel("更多")
}
}
.confirmationDialog("确认删除该相册?", isPresented: $showDeleteConfirm, titleVisibility: .visible) {
.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.load(api: travelAlbumAPI, albumID: albumID)
await viewModel.refreshAll(api: travelAlbumAPI, albumID: albumID)
}
.refreshable {
await viewModel.load(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 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,
contentMode: .fill
) {
Color(hex: 0xE5E7EB)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
Menu {
Button(role: .destructive) {
Task {
await viewModel.deleteMaterial(api: travelAlbumAPI, materialID: material.id, albumID: albumID)
}
} label: {
Label("删除", systemImage: "trash")
private var contentView: some View {
VStack(spacing: TravelAlbumDetailDesign.sectionSpacing) {
if let album = viewModel.album {
TravelAlbumDetailInfoCard(album: album) { phone in
dialPhone(phone)
}
} label: {
Image(systemName: "ellipsis")
.padding(6)
.background(Color.black.opacity(0.35))
.foregroundStyle(.white)
.clipShape(Circle())
.padding(4)
}
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)
}
.aspectRatio(1, contentMode: .fit)
.frame(maxWidth: .infinity)
.clipShape(RoundedRectangle(cornerRadius: 8))
.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 {
@ -158,4 +223,20 @@ struct TravelAlbumDetailView: View {
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
}

View File

@ -53,7 +53,7 @@ struct WiredCameraTransferView: View {
cameraDeviceName: viewModel.cameraDeviceName,
transferModeOption: viewModel.transferModeOption,
tabTitles: tabTitles,
tabCounts: viewModel.photos.transferTabCounts,
tabCounts: viewModel.tabCounts,
selectedTabIndex: viewModel.selectedTabIndex,
onDeviceUsageClick: viewModel.onDeviceUsageClick,
onRefreshCameraFiles: {
@ -168,11 +168,11 @@ struct WiredCameraTransferView: View {
}
private var sidebarGroups: [WiredTransferDateGroup] {
visiblePhotos.buildSidebarGroups()
viewModel.sidebarGroups
}
private var photoSections: [WiredTransferPhotoSection] {
visiblePhotos.buildPhotoSections()
viewModel.photoSections
}
private var selectAllBar: some View {
@ -202,7 +202,16 @@ struct WiredCameraTransferView: View {
onToggleSidebar: viewModel.toggleSidebar,
onTimeSlotSelected: viewModel.selectTimeSlot,
onPhotoClick: openPhotoPreview,
onRetry: { id in Task { await viewModel.retryPhoto(id: id) } },
onRetry: { id in
Task {
await viewModel.retryPhoto(
id: id,
api: travelAlbumAPI,
ossService: ossUploadService,
scenicID: scenicID
)
}
},
onDelete: viewModel.deletePhoto,
onTogglePhotoSelection: viewModel.togglePhotoSelection
)

View File

@ -4,7 +4,6 @@
//
import SwiftUI
import UIKit
import Kingfisher
/// 线 file:// URL
@ -19,7 +18,8 @@ struct WiredTransferPhotoPreviewSheet: View {
self.imageSources = imageSources
self.startIndex = startIndex
self.onDismiss = onDismiss
_currentIndex = State(initialValue: startIndex)
let safeIndex = imageSources.isEmpty ? 0 : min(max(startIndex, 0), imageSources.count - 1)
_currentIndex = State(initialValue: safeIndex)
}
var body: some View {
@ -46,12 +46,7 @@ struct WiredTransferPhotoPreviewSheet: View {
@ViewBuilder
private func previewImage(_ source: String) -> some View {
if source.hasPrefix("file://"), let url = URL(string: source),
let uiImage = UIImage(contentsOfFile: url.path) {
Image(uiImage: uiImage)
.resizable()
.scaledToFit()
} else if let url = URL(string: source), !source.isEmpty {
if let url = URL(string: source), !source.isEmpty {
KFImage(url)
.resizable()
.scaledToFit()

View File

@ -3,6 +3,7 @@
// suixinkan
//
import Kingfisher
import SwiftUI
import UIKit
@ -23,36 +24,40 @@ struct WiredTransferPhotoRow: View {
var body: some View {
VStack(spacing: 0) {
HStack(alignment: .center, spacing: 8) {
if selectUploadMode {
Button(action: onToggleSelection) {
HStack(alignment: .center, spacing: 8) {
if selectUploadMode {
WiredTransferSelectIndicator(selected: selected, enabled: canSelect)
}
.buttonStyle(.plain)
.disabled(!canSelect)
}
thumbnailView
.frame(width: WiredTransferDesign.thumbnailSize, height: WiredTransferDesign.thumbnailSize)
.clipShape(RoundedRectangle(cornerRadius: WiredTransferDesign.thumbnailCornerRadius))
.opacity(rowOpacity)
.onTapGesture {
if !selectUploadMode { onPhotoClick() }
}
thumbnailView
.frame(width: WiredTransferDesign.thumbnailSize, height: WiredTransferDesign.thumbnailSize)
.clipShape(RoundedRectangle(cornerRadius: WiredTransferDesign.thumbnailCornerRadius))
.opacity(rowOpacity)
VStack(alignment: .leading, spacing: 3) {
HStack(spacing: 4) {
Text(photo.fileName)
.font(.system(size: 12, weight: .medium))
.foregroundStyle(WiredTransferDesign.text333.opacity(rowOpacity))
VStack(alignment: .leading, spacing: 3) {
HStack(spacing: 4) {
Text(photo.fileName)
.font(.system(size: 12, weight: .medium))
.foregroundStyle(WiredTransferDesign.text333.opacity(rowOpacity))
.lineLimit(1)
Spacer(minLength: 0)
WiredTransferStatusBadge(status: photo.status)
.opacity(rowOpacity)
}
Text(metaText)
.font(.system(size: 10))
.foregroundStyle(WiredTransferDesign.text999.opacity(rowOpacity))
.lineLimit(1)
Spacer(minLength: 0)
WiredTransferStatusBadge(status: photo.status)
.opacity(rowOpacity)
}
Text(metaText)
.font(.system(size: 10))
.foregroundStyle(WiredTransferDesign.text999.opacity(rowOpacity))
.lineLimit(1)
}
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
.onTapGesture {
if selectUploadMode {
if canSelect { onToggleSelection() }
} else {
onPhotoClick()
}
}
if !selectUploadMode {
@ -71,10 +76,6 @@ struct WiredTransferPhotoRow: View {
}
.padding(.horizontal, 10)
.padding(.vertical, 8)
.contentShape(Rectangle())
.onTapGesture {
if selectUploadMode, canSelect { onToggleSelection() }
}
if photo.status == .transferring || photo.status == .uploading {
ProgressView(value: Double(photo.progress), total: 100)
@ -93,18 +94,20 @@ struct WiredTransferPhotoRow: View {
@ViewBuilder
private var thumbnailView: some View {
if photo.thumbnailURL.hasPrefix("file://"), let url = URL(string: photo.thumbnailURL) {
if let uiImage = UIImage(contentsOfFile: url.path) {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fill)
} else {
thumbnailPlaceholder
}
} else if !photo.thumbnailURL.isEmpty {
RemoteImage(urlString: photo.thumbnailURL) {
thumbnailPlaceholder
}
if let url = URL(string: photo.thumbnailURL), !photo.thumbnailURL.isEmpty {
KFImage(url)
.placeholder { thumbnailPlaceholder }
.setProcessor(
DownsamplingImageProcessor(
size: CGSize(
width: WiredTransferDesign.thumbnailSize,
height: WiredTransferDesign.thumbnailSize
)
)
)
.scaleFactor(UIScreen.main.scale)
.resizable()
.aspectRatio(contentMode: .fill)
} else {
thumbnailPlaceholder
}