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 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()
|
|
||||||
|
|
||||||
// 找出新增的媒体(在newMedia中但不在oldMedia中的项)
|
// 找出新增的媒体(在newMedia中但不在oldMedia中的项)
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user