feat: 预览图片细节
This commit is contained in:
parent
2b5ab92068
commit
9a432f65ac
@ -14,6 +14,7 @@ struct MediaUploadView: View {
|
||||
@State private var selectedMedia: MediaType? = nil
|
||||
/// 当前选中的媒体索引集合
|
||||
@State private var selectedIndices: Set<Int> = []
|
||||
@State private var mediaPickerSelection: [MediaType] = [] // 添加这个状态变量
|
||||
|
||||
// MARK: - 视图主体
|
||||
|
||||
@ -48,9 +49,6 @@ struct MediaUploadView: View {
|
||||
// 媒体选择器
|
||||
mediaPickerView
|
||||
}
|
||||
.onChange(of: uploadManager.selectedMedia) { [oldMedia = uploadManager.selectedMedia] newMedia in
|
||||
handleMediaChange(newMedia, oldMedia: oldMedia)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 子视图
|
||||
@ -132,14 +130,51 @@ struct MediaUploadView: View {
|
||||
/// 媒体选择器视图
|
||||
private var mediaPickerView: some View {
|
||||
MediaPicker(
|
||||
selectedMedia: $uploadManager.selectedMedia,
|
||||
imageSelectionLimit: 20,
|
||||
videoSelectionLimit: 5,
|
||||
selectedMedia: Binding(
|
||||
get: { mediaPickerSelection },
|
||||
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,
|
||||
onUploadProgress: { index, progress in
|
||||
print("文件 \(index) 上传进度: \(progress * 100)%")
|
||||
}
|
||||
)
|
||||
.onAppear {
|
||||
// 重置选择状态当选择器出现时
|
||||
mediaPickerSelection = []
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 私有方法
|
||||
@ -151,7 +186,7 @@ struct MediaUploadView: View {
|
||||
|
||||
// 如果有选中的媒体,开始上传
|
||||
if !uploadManager.selectedMedia.isEmpty {
|
||||
uploadManager.startUpload()
|
||||
// 不需要在这里开始上传,因为handleMediaChange会处理
|
||||
}
|
||||
}
|
||||
|
||||
@ -169,9 +204,7 @@ struct MediaUploadView: View {
|
||||
}
|
||||
|
||||
// 在后台线程处理媒体变化
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let startTime = Date()
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async { [self] in
|
||||
// 找出新增的媒体(在newMedia中但不在oldMedia中的项)
|
||||
let newItems = newMedia.filter { newItem in
|
||||
!oldMedia.contains { $0.id == newItem.id }
|
||||
@ -183,28 +216,26 @@ struct MediaUploadView: View {
|
||||
if !newItems.isEmpty {
|
||||
print("准备添加\(newItems.count)个新项...")
|
||||
|
||||
// 回到主线程更新UI状态
|
||||
DispatchQueue.main.async {
|
||||
// 在主线程更新UI
|
||||
DispatchQueue.main.async { [self] in
|
||||
// 创建新的数组,包含原有媒体和新媒体
|
||||
var updatedMedia = uploadManager.selectedMedia
|
||||
updatedMedia.append(contentsOf: newItems)
|
||||
|
||||
// 更新选中的媒体
|
||||
uploadManager.selectedMedia = updatedMedia
|
||||
|
||||
// 如果当前没有选中的媒体,则选中第一个新增的媒体
|
||||
if self.selectedIndices.isEmpty && !newItems.isEmpty {
|
||||
self.selectedIndices = [self.uploadManager.selectedMedia.count] // 选择第一个新增项的索引
|
||||
self.selectedMedia = newItems.first
|
||||
if selectedIndices.isEmpty && !newItems.isEmpty {
|
||||
selectedIndices = [oldMedia.count] // 选择第一个新增项的索引
|
||||
selectedMedia = newItems.first
|
||||
}
|
||||
|
||||
// 开始上传新添加的媒体
|
||||
self.uploadManager.startUpload()
|
||||
print("媒体添加完成,总数量: \(self.uploadManager.selectedMedia.count)")
|
||||
}
|
||||
} else if newMedia.isEmpty {
|
||||
// 清空选择
|
||||
DispatchQueue.main.async {
|
||||
self.selectedIndices = []
|
||||
self.selectedMedia = nil
|
||||
print("媒体已清空,重置选择状态")
|
||||
uploadManager.startUpload()
|
||||
print("媒体添加完成,总数量: \(uploadManager.selectedMedia.count)")
|
||||
}
|
||||
}
|
||||
|
||||
print("媒体变化处理完成,总耗时: \(String(format: "%.3f", Date().timeIntervalSince(startTime)))s")
|
||||
}
|
||||
}
|
||||
|
||||
@ -264,11 +295,6 @@ struct MainUploadArea: View {
|
||||
Group {
|
||||
if !uploadManager.selectedMedia.isEmpty {
|
||||
VStack(spacing: 8) {
|
||||
// 已选择文件数量
|
||||
Text("已选择 \(uploadManager.selectedMedia.count) 个文件")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.gray)
|
||||
|
||||
// 横向滚动的缩略图列表
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack(spacing: 10) {
|
||||
@ -295,22 +321,86 @@ struct MainUploadArea: View {
|
||||
/// - index: 索引
|
||||
/// - Returns: 媒体项视图
|
||||
private func mediaItemView(for media: MediaType, at index: Int) -> some View {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
VStack(spacing: 4) {
|
||||
// 媒体预览
|
||||
MediaPreview(media: media, uploadManager: uploadManager)
|
||||
.frame(width: 80, height: 80)
|
||||
.cornerRadius(8)
|
||||
.shadow(radius: 1)
|
||||
.overlay(
|
||||
// 左上角序号
|
||||
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)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
|
||||
/// 上传状态视图
|
||||
@ -320,18 +410,18 @@ struct MainUploadArea: View {
|
||||
private func uploadStatusView(for index: Int) -> some View {
|
||||
if let status = uploadManager.uploadStatus["\(index)"] {
|
||||
switch status {
|
||||
// case .uploading(let progress):
|
||||
// // 上传中,显示进度条
|
||||
// VStack(alignment: .center, spacing: 2) {
|
||||
// Text("\(Int(progress * 100))%")
|
||||
// .font(.caption2)
|
||||
// .foregroundColor(.gray)
|
||||
// ProgressView(value: progress, total: 1.0)
|
||||
// .progressViewStyle(LinearProgressViewStyle())
|
||||
// .frame(height: 3)
|
||||
// .tint(Color.themePrimary)
|
||||
// }
|
||||
// .frame(width: 60)
|
||||
case .uploading(let progress):
|
||||
// 上传中,显示进度条
|
||||
VStack(alignment: .center, spacing: 2) {
|
||||
Text("\(Int(progress * 100))%")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.gray)
|
||||
ProgressView(value: progress, total: 1.0)
|
||||
.progressViewStyle(LinearProgressViewStyle())
|
||||
.frame(height: 3)
|
||||
.tint(Color.themePrimary)
|
||||
}
|
||||
.frame(width: 60)
|
||||
|
||||
case .completed:
|
||||
// 上传完成,显示完成图标
|
||||
@ -439,6 +529,37 @@ struct MediaPreview: View {
|
||||
/// 图片缓存
|
||||
private struct ImageCache {
|
||||
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: - 计算属性
|
||||
@ -481,11 +602,6 @@ struct MediaPreview: View {
|
||||
if case .video = media {
|
||||
playButton
|
||||
}
|
||||
|
||||
// // 上传进度指示器(仅在上传时显示)
|
||||
// if isUploading || uploadProgress > 0 {
|
||||
// loadingOverlay
|
||||
// }
|
||||
} else if case .failure(let error) = loadState {
|
||||
// 加载失败状态
|
||||
errorView(error: error)
|
||||
@ -561,46 +677,6 @@ struct MediaPreview: View {
|
||||
.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: - 私有方法
|
||||
|
||||
/// 加载图片
|
||||
@ -608,9 +684,9 @@ struct MediaPreview: View {
|
||||
let cacheKey = "\(media.id)" as NSString
|
||||
|
||||
// 检查缓存
|
||||
if let cachedImage = ImageCache.shared.object(forKey: cacheKey) {
|
||||
self.image = cachedImage
|
||||
self.loadState = .success(cachedImage)
|
||||
if let cachedImage = ImageCache.getImage(forKey: cacheKey as String) {
|
||||
image = cachedImage
|
||||
loadState = .success(cachedImage)
|
||||
return
|
||||
}
|
||||
|
||||
@ -619,7 +695,7 @@ struct MediaPreview: View {
|
||||
do {
|
||||
let imageToCache: UIImage
|
||||
|
||||
switch self.media {
|
||||
switch media {
|
||||
case .image(let uiImage):
|
||||
imageToCache = uiImage
|
||||
|
||||
@ -635,20 +711,20 @@ struct MediaPreview: View {
|
||||
}
|
||||
|
||||
// 缓存图片
|
||||
ImageCache.shared.setObject(imageToCache, forKey: cacheKey)
|
||||
ImageCache.setImage(imageToCache, forKey: cacheKey as String)
|
||||
|
||||
// 更新UI
|
||||
DispatchQueue.main.async {
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
self.image = imageToCache
|
||||
self.loadState = .success(imageToCache)
|
||||
image = imageToCache
|
||||
loadState = .success(imageToCache)
|
||||
}
|
||||
}
|
||||
|
||||
} catch {
|
||||
print("图片加载失败: \(error.localizedDescription)")
|
||||
DispatchQueue.main.async {
|
||||
self.loadState = .failure(error)
|
||||
loadState = .failure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user