feat: 多张图片上传
This commit is contained in:
parent
5ae03a4193
commit
bcf7543fca
Binary file not shown.
57
wake/Models/UploadModels.swift
Normal file
57
wake/Models/UploadModels.swift
Normal file
@ -0,0 +1,57 @@
|
||||
import SwiftUI
|
||||
|
||||
/// 上传状态
|
||||
public enum UploadStatus: Equatable {
|
||||
case idle
|
||||
case uploading(progress: Double)
|
||||
case success
|
||||
case failure(Error)
|
||||
|
||||
public var isUploading: Bool {
|
||||
if case .uploading = self { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
public var progress: Double {
|
||||
if case let .uploading(progress) = self { return progress }
|
||||
return 0
|
||||
}
|
||||
|
||||
public static func == (lhs: UploadStatus, rhs: UploadStatus) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.idle, .idle):
|
||||
return true
|
||||
case let (.uploading(lhsProgress), .uploading(rhsProgress)):
|
||||
// 使用近似比较来处理浮点数的精度问题
|
||||
return abs(lhsProgress - rhsProgress) < 0.001
|
||||
case (.success, .success):
|
||||
return true
|
||||
case (.failure, .failure):
|
||||
// 对于错误类型,我们简单地认为它们不相等,因为比较 Error 对象比较复杂
|
||||
// 如果需要更精确的比较,可以在这里添加具体实现
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 上传结果
|
||||
public struct UploadResult: Identifiable, Equatable {
|
||||
public let id = UUID()
|
||||
public var fileId: String
|
||||
public var previewFileId: String
|
||||
public let image: UIImage
|
||||
public var status: UploadStatus = .idle
|
||||
|
||||
public init(fileId: String = "", previewFileId: String = "", image: UIImage, status: UploadStatus = .idle) {
|
||||
self.fileId = fileId
|
||||
self.previewFileId = previewFileId
|
||||
self.image = image
|
||||
self.status = status
|
||||
}
|
||||
|
||||
public static func == (lhs: UploadResult, rhs: UploadResult) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
}
|
||||
259
wake/View/Components/Upload/MultiImageUploader.swift
Normal file
259
wake/View/Components/Upload/MultiImageUploader.swift
Normal file
@ -0,0 +1,259 @@
|
||||
import SwiftUI
|
||||
import PhotosUI
|
||||
import os.log
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
public struct MultiImageUploader: View {
|
||||
@State private var selectedImages: [UIImage] = []
|
||||
@State private var uploadResults: [UploadResult] = []
|
||||
@State private var isUploading = false
|
||||
@State private var showingImagePicker = false
|
||||
@State private var uploadProgress: [UUID: Double] = [:] // 跟踪每个上传任务的进度
|
||||
|
||||
private let maxSelection: Int
|
||||
private let onUploadComplete: ([UploadResult]) -> Void
|
||||
private let uploadService = ImageUploadService.shared
|
||||
private let logger = Logger(subsystem: "com.yourapp.uploader", category: "MultiImageUploader")
|
||||
|
||||
public init(
|
||||
maxSelection: Int = 10,
|
||||
onUploadComplete: @escaping ([UploadResult]) -> Void
|
||||
) {
|
||||
self.maxSelection = maxSelection
|
||||
self.onUploadComplete = onUploadComplete
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
// 上传按钮
|
||||
Button(action: {
|
||||
showingImagePicker = true
|
||||
}) {
|
||||
Label("选择图片", systemImage: "photo.on.rectangle")
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color.blue)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.sheet(isPresented: $showingImagePicker) {
|
||||
ImagePicker(images: $selectedImages, selectionLimit: maxSelection) {
|
||||
// 当选择完成时,开始上传
|
||||
if !selectedImages.isEmpty {
|
||||
uploadImages()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 上传进度和图片网格
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 16) {
|
||||
ForEach($uploadResults) { $result in
|
||||
VStack(spacing: 8) {
|
||||
// 图片预览
|
||||
Image(uiImage: result.image)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(height: 200)
|
||||
.frame(maxWidth: .infinity)
|
||||
.clipped()
|
||||
.cornerRadius(8)
|
||||
|
||||
// 上传进度条
|
||||
if case .uploading = result.status {
|
||||
ProgressView(value: uploadProgress[result.id] ?? 0, total: 1.0)
|
||||
.progressViewStyle(LinearProgressViewStyle())
|
||||
.padding(.horizontal)
|
||||
|
||||
Text("上传中: \(Int((uploadProgress[result.id] ?? 0) * 100))%")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
} else if case .success = result.status {
|
||||
HStack {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.green)
|
||||
Text("上传成功")
|
||||
.font(.caption)
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
} else if case .failure = result.status {
|
||||
HStack {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.foregroundColor(.red)
|
||||
Text("上传失败")
|
||||
.font(.caption)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemBackground))
|
||||
.cornerRadius(12)
|
||||
.shadow(radius: 2)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
// 上传按钮
|
||||
if !selectedImages.isEmpty && !isUploading {
|
||||
Button(action: uploadImages) {
|
||||
Text("开始上传 (\(selectedImages.count)张)")
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color.blue)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom)
|
||||
}
|
||||
}
|
||||
.background(Color(.systemGroupedBackground))
|
||||
}
|
||||
|
||||
private func uploadImages() {
|
||||
guard !isUploading else { return }
|
||||
|
||||
isUploading = true
|
||||
let group = DispatchGroup()
|
||||
var results = selectedImages.map { image in
|
||||
UploadResult(fileId: "", previewFileId: "", image: image, status: .uploading(progress: 0))
|
||||
}
|
||||
|
||||
// 更新UI显示上传中的状态
|
||||
uploadResults = results
|
||||
|
||||
// 创建并发的DispatchQueue
|
||||
let uploadQueue = DispatchQueue(label: "com.wake.uploadQueue", attributes: .concurrent)
|
||||
let semaphore = DispatchSemaphore(value: 3) // 限制并发数为3
|
||||
|
||||
for (index, image) in selectedImages.enumerated() {
|
||||
semaphore.wait()
|
||||
group.enter()
|
||||
|
||||
uploadQueue.async {
|
||||
let resultId = results[index].id
|
||||
|
||||
// 更新状态为上传中
|
||||
DispatchQueue.main.async {
|
||||
if let idx = results.firstIndex(where: { $0.id == resultId }) {
|
||||
results[idx].status = .uploading(progress: 0)
|
||||
uploadProgress[resultId] = 0
|
||||
uploadResults = results
|
||||
}
|
||||
}
|
||||
|
||||
// 上传原图
|
||||
uploadService.uploadImage(image, progress: { progress in
|
||||
DispatchQueue.main.async {
|
||||
uploadProgress[resultId] = progress.progress
|
||||
if let idx = results.firstIndex(where: { $0.id == resultId }) {
|
||||
results[idx].status = .uploading(progress: progress.progress)
|
||||
uploadResults = results
|
||||
}
|
||||
}
|
||||
}) { uploadResult in
|
||||
defer {
|
||||
semaphore.signal()
|
||||
group.leave()
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if let idx = results.firstIndex(where: { $0.id == resultId }) {
|
||||
switch uploadResult {
|
||||
case .success(let uploadResult):
|
||||
// 上传成功,更新结果
|
||||
results[idx].fileId = uploadResult.fileId
|
||||
// 使用空字符串作为 previewFileId,因为 ImageUploaderGetID.UploadResult 没有这个属性
|
||||
results[idx].previewFileId = ""
|
||||
results[idx].status = .success
|
||||
uploadResults = results
|
||||
|
||||
logger.info("图片上传成功: \(uploadResult.fileId)")
|
||||
|
||||
case .failure(let error):
|
||||
// 上传失败
|
||||
results[idx].status = .failure(error)
|
||||
uploadResults = results
|
||||
logger.error("图片上传失败: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 所有上传任务完成后的处理
|
||||
group.notify(queue: .main) {
|
||||
self.isUploading = false
|
||||
let successCount = results.filter { $0.status == .success }.count
|
||||
logger.info("上传完成,成功: \(successCount)/\(results.count)")
|
||||
self.onUploadComplete(results)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 图片选择器
|
||||
@available(iOS 14.0, *)
|
||||
struct ImagePicker: UIViewControllerRepresentable {
|
||||
@Binding var images: [UIImage]
|
||||
var selectionLimit: Int = 10
|
||||
var onDismiss: (() -> Void)?
|
||||
|
||||
func makeUIViewController(context: Context) -> PHPickerViewController {
|
||||
var config = PHPickerConfiguration(photoLibrary: .shared())
|
||||
config.filter = .images
|
||||
config.selectionLimit = selectionLimit
|
||||
config.preferredAssetRepresentationMode = .current
|
||||
|
||||
let picker = PHPickerViewController(configuration: config)
|
||||
picker.delegate = context.coordinator
|
||||
return picker
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(self)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, PHPickerViewControllerDelegate {
|
||||
let parent: ImagePicker
|
||||
|
||||
init(_ parent: ImagePicker) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||||
picker.dismiss(animated: true)
|
||||
|
||||
let group = DispatchGroup()
|
||||
var newImages: [UIImage] = []
|
||||
|
||||
for result in results {
|
||||
group.enter()
|
||||
let itemProvider = result.itemProvider
|
||||
|
||||
if itemProvider.canLoadObject(ofClass: UIImage.self) {
|
||||
itemProvider.loadObject(ofClass: UIImage.self) { [weak self] (image, error) in
|
||||
if let image = image as? UIImage {
|
||||
newImages.append(image)
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
} else {
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: .main) {
|
||||
self.parent.images = newImages
|
||||
self.parent.onDismiss?()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
284
wake/View/Examples/ImageUploadExampleView.swift
Normal file
284
wake/View/Examples/ImageUploadExampleView.swift
Normal file
@ -0,0 +1,284 @@
|
||||
import SwiftUI
|
||||
import os.log
|
||||
|
||||
/// 多图上传示例视图
|
||||
/// 展示如何使用 MultiImageUploader 组件实现多图上传功能
|
||||
@available(iOS 16.0, *)
|
||||
struct MultiImageUploadExampleView: View {
|
||||
// MARK: - 状态属性
|
||||
|
||||
@State private var uploadResults: [UploadResult] = []
|
||||
@State private var isShowingUploader = false
|
||||
@State private var showUploadAlert = false
|
||||
@State private var alertMessage = ""
|
||||
@State private var isUploading = false
|
||||
|
||||
private let logger = Logger(subsystem: "com.yourapp.uploader", category: "MultiImageUploadExample")
|
||||
|
||||
// MARK: - 视图主体
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
VStack(spacing: 16) {
|
||||
// 上传按钮和状态
|
||||
uploadButton
|
||||
|
||||
// 上传统计信息
|
||||
if !uploadResults.isEmpty {
|
||||
uploadStatsView
|
||||
}
|
||||
|
||||
// 上传进度列表
|
||||
uploadProgressList
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.navigationTitle("多图上传示例")
|
||||
.toolbar {
|
||||
// 清空按钮
|
||||
if !uploadResults.isEmpty && !isUploading {
|
||||
Button("清空") {
|
||||
withAnimation {
|
||||
uploadResults.removeAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $isShowingUploader) {
|
||||
// 多图上传组件
|
||||
MultiImageUploader(
|
||||
maxSelection: 10
|
||||
) { results in
|
||||
processUploadResults(results)
|
||||
}
|
||||
}
|
||||
.alert("上传结果", isPresented: $showUploadAlert) {
|
||||
Button("确定", role: .cancel) { }
|
||||
} message: {
|
||||
Text(alertMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 子视图
|
||||
|
||||
/// 上传按钮
|
||||
private var uploadButton: some View {
|
||||
Button(action: { isShowingUploader = true }) {
|
||||
Label("选择并上传图片", systemImage: "photo.on.rectangle.angled")
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color.blue)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.top)
|
||||
}
|
||||
|
||||
/// 上传统计信息
|
||||
private var uploadStatsView: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
let successCount = uploadResults.filter { $0.status == .success }.count
|
||||
let inProgressCount = uploadResults.filter {
|
||||
if case .uploading = $0.status { return true }
|
||||
return false
|
||||
}.count
|
||||
let failedCount = uploadResults.count - successCount - inProgressCount
|
||||
|
||||
Text("上传统计")
|
||||
.font(.headline)
|
||||
|
||||
HStack {
|
||||
StatView(
|
||||
value: "\(uploadResults.count)",
|
||||
label: "总数量",
|
||||
color: .blue
|
||||
)
|
||||
|
||||
StatView(
|
||||
value: "\(successCount)",
|
||||
label: "成功",
|
||||
color: .green
|
||||
)
|
||||
|
||||
StatView(
|
||||
value: "\(inProgressCount)",
|
||||
label: "上传中",
|
||||
color: .orange
|
||||
)
|
||||
|
||||
StatView(
|
||||
value: "\(failedCount)",
|
||||
label: "失败",
|
||||
color: .red
|
||||
)
|
||||
}
|
||||
|
||||
// 总进度条
|
||||
if inProgressCount > 0 {
|
||||
let progress = uploadResults.reduce(0.0) { result, uploadResult in
|
||||
if case .uploading(let progress) = uploadResult.status {
|
||||
return result + progress
|
||||
} else if uploadResult.status == .success {
|
||||
return result + 1.0
|
||||
}
|
||||
return result
|
||||
} / Double(uploadResults.count)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("总进度: \(Int(progress * 100))%")
|
||||
.font(.subheadline)
|
||||
ProgressView(value: progress)
|
||||
.progressViewStyle(LinearProgressViewStyle(tint: .blue))
|
||||
}
|
||||
.padding(.top, 8)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemGray6))
|
||||
.cornerRadius(10)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
/// 上传进度列表
|
||||
private var uploadProgressList: some View {
|
||||
List {
|
||||
ForEach($uploadResults) { $result in
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
// 图片缩略图和状态
|
||||
HStack {
|
||||
// 图片缩略图
|
||||
Image(uiImage: result.image)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 60, height: 60)
|
||||
.clipped()
|
||||
.cornerRadius(8)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(borderColor(for: result.status), lineWidth: 1)
|
||||
)
|
||||
|
||||
// 状态和进度
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("图片 \(result.id.uuidString.prefix(8))...")
|
||||
.font(.subheadline)
|
||||
|
||||
switch result.status {
|
||||
case .uploading(let progress):
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("上传中: \(Int(progress * 100))%")
|
||||
.font(.caption)
|
||||
ProgressView(value: progress)
|
||||
.progressViewStyle(LinearProgressViewStyle())
|
||||
}
|
||||
case .success:
|
||||
HStack {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.green)
|
||||
Text("上传成功")
|
||||
.font(.caption)
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
case .failure(let error):
|
||||
HStack {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.foregroundColor(.red)
|
||||
Text("上传失败: \(error.localizedDescription)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
case .idle:
|
||||
Text("等待上传...")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
.listRowSeparator(.hidden)
|
||||
}
|
||||
}
|
||||
.listStyle(PlainListStyle())
|
||||
.animation(.easeInOut, value: uploadResults)
|
||||
}
|
||||
|
||||
// MARK: - 辅助方法
|
||||
|
||||
/// 获取状态对应的边框颜色
|
||||
private func borderColor(for status: UploadStatus) -> Color {
|
||||
switch status {
|
||||
case .success: return .green
|
||||
case .failure: return .red
|
||||
case .uploading: return .blue
|
||||
case .idle: return .gray
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理上传结果
|
||||
private func processUploadResults(_ results: [UploadResult]) {
|
||||
// 更新状态
|
||||
isUploading = results.contains { result in
|
||||
if case .uploading = result.status { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
// 更新结果
|
||||
withAnimation {
|
||||
uploadResults = results
|
||||
isShowingUploader = false
|
||||
}
|
||||
|
||||
// 检查是否全部完成
|
||||
let allFinished = !results.contains { result in
|
||||
if case .uploading = result.status { return true }
|
||||
if case .idle = result.status { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
if allFinished {
|
||||
let successCount = results.filter { $0.status == .success }.count
|
||||
let totalCount = results.count
|
||||
alertMessage = "上传完成\n成功: \(successCount)/\(totalCount)"
|
||||
showUploadAlert = true
|
||||
isUploading = false
|
||||
|
||||
logger.info("上传完成,成功: \(successCount)/\(totalCount)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 子视图
|
||||
|
||||
/// 统计信息视图
|
||||
private struct StatView: View {
|
||||
let value: String
|
||||
let label: String
|
||||
let color: Color
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text(value)
|
||||
.font(.title3.bold())
|
||||
.foregroundColor(color)
|
||||
Text(label)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 预览
|
||||
@available(iOS 16.0, *)
|
||||
struct MultiImageUploadExampleView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MultiImageUploadExampleView()
|
||||
}
|
||||
}
|
||||
@ -44,7 +44,9 @@ struct WakeApp: App {
|
||||
// 根据登录状态显示不同视图
|
||||
if authState.isAuthenticated {
|
||||
// 已登录:显示userInfo页面
|
||||
UserInfo()
|
||||
// UserInfo()
|
||||
// .environmentObject(authState)
|
||||
MultiImageUploadExampleView()
|
||||
.environmentObject(authState)
|
||||
} else {
|
||||
// 未登录:显示登录界面
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user