feat: 预览图片细节

This commit is contained in:
jinyaqiu 2025-08-25 20:10:53 +08:00
parent 2b5ab92068
commit 9a432f65ac

View File

@ -14,6 +14,7 @@ struct MediaUploadView: View {
@State private var selectedMedia: MediaType? = nil @State private var selectedMedia: MediaType? = nil
/// ///
@State private var selectedIndices: Set<Int> = [] @State private var selectedIndices: Set<Int> = []
@State private var mediaPickerSelection: [MediaType] = [] //
// MARK: - // MARK: -
@ -48,9 +49,6 @@ struct MediaUploadView: View {
// //
mediaPickerView mediaPickerView
} }
.onChange(of: uploadManager.selectedMedia) { [oldMedia = uploadManager.selectedMedia] newMedia in
handleMediaChange(newMedia, oldMedia: oldMedia)
}
} }
// MARK: - // MARK: -
@ -132,14 +130,51 @@ struct MediaUploadView: View {
/// ///
private var mediaPickerView: some View { private var mediaPickerView: some View {
MediaPicker( MediaPicker(
selectedMedia: $uploadManager.selectedMedia, selectedMedia: Binding(
imageSelectionLimit: 20, get: { mediaPickerSelection },
videoSelectionLimit: 5, set: { newSelections in
//
let newItems = newSelections.filter { newItem in
!uploadManager.selectedMedia.contains { $0.id == newItem.id }
}
if !newItems.isEmpty {
//
let newMedia = newItems + uploadManager.selectedMedia
uploadManager.selectedMedia = newMedia
//
if selectedIndices.isEmpty {
selectedIndices = [0] //
selectedMedia = newItems.first
}
//
uploadManager.startUpload()
}
//
mediaPickerSelection = []
}
),
imageSelectionLimit: max(0, 20 - uploadManager.selectedMedia.filter {
if case .image = $0 { return true }
return false
}.count),
videoSelectionLimit: max(0, 5 - uploadManager.selectedMedia.filter {
if case .video = $0 { return true }
return false
}.count),
selectionMode: .multiple,
onDismiss: handleMediaPickerDismiss, onDismiss: handleMediaPickerDismiss,
onUploadProgress: { index, progress in onUploadProgress: { index, progress in
print("文件 \(index) 上传进度: \(progress * 100)%") print("文件 \(index) 上传进度: \(progress * 100)%")
} }
) )
.onAppear {
//
mediaPickerSelection = []
}
} }
// MARK: - // MARK: -
@ -151,7 +186,7 @@ struct MediaUploadView: View {
// //
if !uploadManager.selectedMedia.isEmpty { if !uploadManager.selectedMedia.isEmpty {
uploadManager.startUpload() // handleMediaChange
} }
} }
@ -169,9 +204,7 @@ struct MediaUploadView: View {
} }
// 线 // 线
DispatchQueue.global(qos: .userInitiated).async { DispatchQueue.global(qos: .userInitiated).async { [self] in
let startTime = Date()
// newMediaoldMedia // newMediaoldMedia
let newItems = newMedia.filter { newItem in let newItems = newMedia.filter { newItem in
!oldMedia.contains { $0.id == newItem.id } !oldMedia.contains { $0.id == newItem.id }
@ -183,28 +216,26 @@ struct MediaUploadView: View {
if !newItems.isEmpty { if !newItems.isEmpty {
print("准备添加\(newItems.count)个新项...") print("准备添加\(newItems.count)个新项...")
// 线UI // 线UI
DispatchQueue.main.async { DispatchQueue.main.async { [self] in
//
var updatedMedia = uploadManager.selectedMedia
updatedMedia.append(contentsOf: newItems)
//
uploadManager.selectedMedia = updatedMedia
// //
if self.selectedIndices.isEmpty && !newItems.isEmpty { if selectedIndices.isEmpty && !newItems.isEmpty {
self.selectedIndices = [self.uploadManager.selectedMedia.count] // selectedIndices = [oldMedia.count] //
self.selectedMedia = newItems.first selectedMedia = newItems.first
} }
// //
self.uploadManager.startUpload() uploadManager.startUpload()
print("媒体添加完成,总数量: \(self.uploadManager.selectedMedia.count)") print("媒体添加完成,总数量: \(uploadManager.selectedMedia.count)")
}
} else if newMedia.isEmpty {
//
DispatchQueue.main.async {
self.selectedIndices = []
self.selectedMedia = nil
print("媒体已清空,重置选择状态")
} }
} }
print("媒体变化处理完成,总耗时: \(String(format: "%.3f", Date().timeIntervalSince(startTime)))s")
} }
} }
@ -264,11 +295,6 @@ struct MainUploadArea: View {
Group { Group {
if !uploadManager.selectedMedia.isEmpty { if !uploadManager.selectedMedia.isEmpty {
VStack(spacing: 8) { VStack(spacing: 8) {
//
Text("已选择 \(uploadManager.selectedMedia.count) 个文件")
.font(.subheadline)
.foregroundColor(.gray)
// //
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 10) { LazyHStack(spacing: 10) {
@ -295,22 +321,86 @@ struct MainUploadArea: View {
/// - index: /// - index:
/// - Returns: /// - Returns:
private func mediaItemView(for media: MediaType, at index: Int) -> some View { private func mediaItemView(for media: MediaType, at index: Int) -> some View {
VStack(spacing: 4) { ZStack(alignment: .topTrailing) {
// VStack(spacing: 4) {
MediaPreview(media: media, uploadManager: uploadManager) //
.frame(width: 80, height: 80) MediaPreview(media: media, uploadManager: uploadManager)
.cornerRadius(8) .frame(width: 80, height: 80)
.shadow(radius: 1) .cornerRadius(8)
.onTapGesture { .shadow(radius: 1)
// .overlay(
selectedIndices = [index] //
selectedMedia = media ZStack {
} Path { path in
let radius: CGFloat = 4
let width: CGFloat = 28
let height: CGFloat = 18
//
path.move(to: CGPoint(x: 0, y: radius))
path.addQuadCurve(to: CGPoint(x: radius, y: 0),
control: CGPoint(x: 0, y: 0))
//
path.addLine(to: CGPoint(x: width, y: 0))
//
path.addLine(to: CGPoint(x: width, y: height - radius))
//
path.addQuadCurve(to: CGPoint(x: width - radius, y: height),
control: CGPoint(x: width, y: height))
//
path.addLine(to: CGPoint(x: 0, y: height))
//
path.closeSubpath()
}
.fill(Color.white.opacity(0.5))
// Text
Text("\(index + 1)")
.font(.system(size: 12, weight: .bold))
.foregroundColor(.black)
.frame(width: 28, height: 18)
.offset(x: 0, y: -1)
}
.frame(width: 28, height: 18)
.offset(x: 0, y: 0)
.padding([.top, .leading], 0),
alignment: .topLeading
)
.onTapGesture {
//
selectedIndices = [index]
selectedMedia = media
}
}
// //
uploadStatusView(for: index) Button(action: {
//
uploadManager.selectedMedia.remove(at: index)
//
if selectedIndices.contains(index) {
selectedIndices = []
selectedMedia = nil
}
}) {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 20))
.foregroundColor(.white.opacity(0.5))
.background(
Circle()
.fill(Color.black.opacity(0.5))
.frame(width: 20, height: 20)
)
}
.offset(x: 6, y: -6) //
} }
.padding(4) .padding(4)
.contentShape(Rectangle())
} }
/// ///
@ -320,18 +410,18 @@ struct MainUploadArea: View {
private func uploadStatusView(for index: Int) -> some View { private func uploadStatusView(for index: Int) -> some View {
if let status = uploadManager.uploadStatus["\(index)"] { if let status = uploadManager.uploadStatus["\(index)"] {
switch status { switch status {
// case .uploading(let progress): case .uploading(let progress):
// // //
// VStack(alignment: .center, spacing: 2) { VStack(alignment: .center, spacing: 2) {
// Text("\(Int(progress * 100))%") Text("\(Int(progress * 100))%")
// .font(.caption2) .font(.caption2)
// .foregroundColor(.gray) .foregroundColor(.gray)
// ProgressView(value: progress, total: 1.0) ProgressView(value: progress, total: 1.0)
// .progressViewStyle(LinearProgressViewStyle()) .progressViewStyle(LinearProgressViewStyle())
// .frame(height: 3) .frame(height: 3)
// .tint(Color.themePrimary) .tint(Color.themePrimary)
// } }
// .frame(width: 60) .frame(width: 60)
case .completed: case .completed:
// //
@ -439,6 +529,37 @@ struct MediaPreview: View {
/// ///
private struct ImageCache { private struct ImageCache {
static let shared = NSCache<NSString, UIImage>() static let shared = NSCache<NSString, UIImage>()
private static var cacheKeys = Set<String>()
static func setImage(_ image: UIImage, forKey key: String) {
shared.setObject(image, forKey: key as NSString)
cacheKeys.insert(key)
}
static func getImage(forKey key: String) -> UIImage? {
return shared.object(forKey: key as NSString)
}
static func clearCache() {
cacheKeys.forEach { key in
shared.removeObject(forKey: key as NSString)
}
cacheKeys.removeAll()
}
}
// MARK: -
init(media: MediaType, uploadManager: MediaUploadManager) {
self.media = media
self.uploadManager = uploadManager
//
let cacheKey = "\(media.id)" as NSString
if let cachedImage = ImageCache.shared.object(forKey: cacheKey) {
self._image = State(initialValue: cachedImage)
self._loadState = State(initialValue: .success(cachedImage))
}
} }
// MARK: - // MARK: -
@ -481,11 +602,6 @@ struct MediaPreview: View {
if case .video = media { if case .video = media {
playButton playButton
} }
// //
// if isUploading || uploadProgress > 0 {
// loadingOverlay
// }
} else if case .failure(let error) = loadState { } else if case .failure(let error) = loadState {
// //
errorView(error: error) errorView(error: error)
@ -561,46 +677,6 @@ struct MediaPreview: View {
.cornerRadius(8) .cornerRadius(8)
} }
///
private var loadingOverlay: some View {
ZStack {
//
Color.black.opacity(0.3)
//
VStack {
Spacer()
//
ZStack {
Circle()
.stroke(
Color.white.opacity(0.3),
lineWidth: 4
)
Circle()
.trim(from: 0.0, to: uploadProgress)
.stroke(
Color.white,
style: StrokeStyle(
lineWidth: 4,
lineCap: .round
)
)
.rotationEffect(.degrees(-90))
Text("\(Int(uploadProgress * 100))%")
.font(.system(size: 12, weight: .bold))
.foregroundColor(.white)
}
.frame(width: 40, height: 40)
.padding(.bottom, 8)
}
}
.cornerRadius(8)
}
// MARK: - // MARK: -
/// ///
@ -608,9 +684,9 @@ struct MediaPreview: View {
let cacheKey = "\(media.id)" as NSString let cacheKey = "\(media.id)" as NSString
// //
if let cachedImage = ImageCache.shared.object(forKey: cacheKey) { if let cachedImage = ImageCache.getImage(forKey: cacheKey as String) {
self.image = cachedImage image = cachedImage
self.loadState = .success(cachedImage) loadState = .success(cachedImage)
return return
} }
@ -619,7 +695,7 @@ struct MediaPreview: View {
do { do {
let imageToCache: UIImage let imageToCache: UIImage
switch self.media { switch media {
case .image(let uiImage): case .image(let uiImage):
imageToCache = uiImage imageToCache = uiImage
@ -635,20 +711,20 @@ struct MediaPreview: View {
} }
// //
ImageCache.shared.setObject(imageToCache, forKey: cacheKey) ImageCache.setImage(imageToCache, forKey: cacheKey as String)
// UI // UI
DispatchQueue.main.async { DispatchQueue.main.async {
withAnimation(.easeInOut(duration: 0.2)) { withAnimation(.easeInOut(duration: 0.2)) {
self.image = imageToCache image = imageToCache
self.loadState = .success(imageToCache) loadState = .success(imageToCache)
} }
} }
} catch { } catch {
print("图片加载失败: \(error.localizedDescription)") print("图片加载失败: \(error.localizedDescription)")
DispatchQueue.main.async { DispatchQueue.main.async {
self.loadState = .failure(error) loadState = .failure(error)
} }
} }
} }