增强有线传图手动上传并发能力,并稳定列表行布局。
指定/勾选/批量上传支持恢复仅本地持久化的待传照片,手动上传与 pipeline 均按 3 路并发;pipeline 任务签名去重避免状态回退,照片行固定高度并预留进度条占位。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -417,6 +417,7 @@ final class WiredCameraTransferViewModel: ObservableObject {
|
||||
static let specifyUploadAllPending = "上传所有未上传的照片"
|
||||
static let specifyUploadTodayCaptured = "上传所有今日拍摄的照片"
|
||||
static let helpDocURL = URL(string: "https://sharesky.feishu.cn/wiki/CaAhwl3Gnin4Kqk0BVocK3SGnab?from=from_copylink")!
|
||||
private static let manualUploadConcurrency = 3
|
||||
|
||||
@Published private(set) var photos: [WiredTransferPhotoItem] = []
|
||||
@Published private(set) var visiblePhotos: [WiredTransferPhotoItem] = []
|
||||
@ -444,6 +445,7 @@ final class WiredCameraTransferViewModel: ObservableObject {
|
||||
private var userIDProvider: (() -> String)?
|
||||
private var sessionNewPhotoIDs: Set<String> = []
|
||||
private var persistedRecordsByID: [String: WiredTransferPhotoRecord] = [:]
|
||||
private var lastAppliedPipelineSignatures: [String: PipelineTaskSignature] = [:]
|
||||
private var lastProgressPersistDates: [String: Date] = [:]
|
||||
private var wasConnected = false
|
||||
private var disconnectAlertTask: Task<Void, Never>?
|
||||
@ -639,7 +641,7 @@ final class WiredCameraTransferViewModel: ObservableObject {
|
||||
errorMessage = "暂无未上传的照片"
|
||||
return
|
||||
}
|
||||
await pipeline.uploadAssets(withIDs: pendingIDs)
|
||||
await uploadPendingPhotoIDs(pendingIDs)
|
||||
}
|
||||
|
||||
/// 上传今日拍摄且未上传的照片。
|
||||
@ -650,13 +652,13 @@ final class WiredCameraTransferViewModel: ObservableObject {
|
||||
errorMessage = "暂无今日拍摄且未上传的照片"
|
||||
return
|
||||
}
|
||||
await pipeline.uploadAssets(withIDs: pendingIDs)
|
||||
await uploadPendingPhotoIDs(pendingIDs)
|
||||
}
|
||||
|
||||
/// 上传选中照片。
|
||||
func uploadSelected(api: any TravelAlbumServing, ossService: any OSSUploadServing, scenicID: Int) async {
|
||||
ensureUploadEnabled(api: api, ossService: ossService, scenicID: scenicID)
|
||||
await pipeline.uploadAssets(withIDs: Array(selectedPhotoIDs))
|
||||
await uploadPendingPhotoIDs(Array(selectedPhotoIDs))
|
||||
exitSelectUploadMode()
|
||||
}
|
||||
|
||||
@ -699,10 +701,57 @@ final class WiredCameraTransferViewModel: ObservableObject {
|
||||
await refreshPhotoFromPipelineTask(id: id)
|
||||
}
|
||||
|
||||
private func uploadPendingPhotoIDs(_ ids: [String]) async {
|
||||
let pendingIDs = ids.filter { id in
|
||||
photos.first(where: { $0.id == id })?.isNotUploaded == true
|
||||
}
|
||||
var currentIndex = 0
|
||||
while currentIndex < pendingIDs.count {
|
||||
let upperBound = min(currentIndex + Self.manualUploadConcurrency, pendingIDs.count)
|
||||
let batch = pendingIDs[currentIndex ..< upperBound]
|
||||
let uploadTasks = batch.map { id in
|
||||
Task { @MainActor in
|
||||
await self.uploadSinglePendingPhoto(id: id)
|
||||
}
|
||||
}
|
||||
for task in uploadTasks {
|
||||
await task.value
|
||||
}
|
||||
currentIndex = upperBound
|
||||
}
|
||||
}
|
||||
|
||||
private func uploadSinglePendingPhoto(id: String) async {
|
||||
updatePhotoStatus(id: id, status: .uploading, progress: 0, errorMessage: nil)
|
||||
|
||||
if pipeline.task(forAssetID: id) != nil {
|
||||
await pipeline.uploadAssets(withIDs: [id])
|
||||
await refreshPhotoFromPipelineTask(id: id)
|
||||
return
|
||||
}
|
||||
|
||||
guard let record = persistedRecord(for: id),
|
||||
let localURL = CameraDownloadStorage.resolveLocalURL(from: record.localPath),
|
||||
FileManager.default.fileExists(atPath: localURL.path) else {
|
||||
let message = "本地文件不存在"
|
||||
updatePhotoStatus(id: id, status: .failed, progress: 0, errorMessage: message)
|
||||
persistPhotoStatus(id: id, status: .failed, progress: 0, errorMessage: message)
|
||||
return
|
||||
}
|
||||
|
||||
await pipeline.retryUpload(
|
||||
assetID: id,
|
||||
filename: record.fileName,
|
||||
localPath: record.localPath
|
||||
)
|
||||
await refreshPhotoFromPipelineTask(id: id)
|
||||
}
|
||||
|
||||
/// 删除本地照片记录。
|
||||
func deletePhoto(id: String) {
|
||||
photoStore?.remove(albumID: context.albumId, photoID: id)
|
||||
persistedRecordsByID.removeValue(forKey: id)
|
||||
lastAppliedPipelineSignatures.removeValue(forKey: id)
|
||||
lastProgressPersistDates.removeValue(forKey: id)
|
||||
setPhotos(photos.filter { $0.id != id }, rebuildGroups: true)
|
||||
}
|
||||
@ -905,16 +954,24 @@ final class WiredCameraTransferViewModel: ObservableObject {
|
||||
for task in tasks {
|
||||
guard !enforceAlbumBinding || belongsToCurrentAlbum(task.assetID) else { continue }
|
||||
|
||||
let signature = PipelineTaskSignature(task: task)
|
||||
guard lastAppliedPipelineSignatures[task.assetID] != signature else { continue }
|
||||
|
||||
let status = mapPipelineStatus(task.status)
|
||||
let existing = existingByID[task.assetID] ?? existingByFileName[task.filename]
|
||||
if shouldIgnoreStalePipelineStatus(existing: existing?.status, incoming: status) {
|
||||
lastAppliedPipelineSignatures[task.assetID] = signature
|
||||
continue
|
||||
}
|
||||
let storedLocalPath = task.localPath ?? ""
|
||||
let localURL = task.localURL
|
||||
let fileSizeBytes = await CameraDownloadStorage.fileSizeBytes(for: localURL)
|
||||
|
||||
let existingRecord = persistedRecordsByID[task.assetID]
|
||||
let fileSizeBytes: Int64
|
||||
if let existingRecord, existingRecord.fileSizeBytes > 0 {
|
||||
fileSizeBytes = existingRecord.fileSizeBytes
|
||||
} else {
|
||||
fileSizeBytes = await CameraDownloadStorage.fileSizeBytes(for: localURL)
|
||||
}
|
||||
let capturedAt = await resolveCapturedAt(
|
||||
existing: existing,
|
||||
taskCapturedAt: task.capturedAt,
|
||||
@ -973,6 +1030,7 @@ final class WiredCameraTransferViewModel: ObservableObject {
|
||||
)
|
||||
persistRecord(record)
|
||||
}
|
||||
lastAppliedPipelineSignatures[task.assetID] = signature
|
||||
}
|
||||
}
|
||||
|
||||
@ -1091,3 +1149,19 @@ final class WiredCameraTransferViewModel: ObservableObject {
|
||||
return bound == context.albumId
|
||||
}
|
||||
}
|
||||
|
||||
private struct PipelineTaskSignature: Equatable {
|
||||
let status: CameraTransferStatus
|
||||
let progress: Int
|
||||
let localPath: String?
|
||||
let remoteURL: String?
|
||||
let errorMessage: String?
|
||||
|
||||
init(task: CameraTransferTask) {
|
||||
status = task.status
|
||||
progress = task.progress
|
||||
localPath = task.localPath
|
||||
remoteURL = task.remoteURL
|
||||
errorMessage = task.errorMessage
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user