feat: 暂提

This commit is contained in:
jinyaqiu 2025-08-28 16:18:50 +08:00
parent 2cc7e5fb01
commit 45b8d211af

View File

@ -302,7 +302,7 @@ struct MainUploadArea: View {
.frame(height: 50)
//
if let mediaToDisplay = selectedMedia ?? uploadManager.selectedMedia.first {
MediaPreview(media: mediaToDisplay, uploadManager: uploadManager)
MediaPreview(media: mediaToDisplay)
.id(mediaToDisplay.id)
.frame(width: 225, height: 225)
.overlay(
@ -374,120 +374,106 @@ struct MainUploadArea: View {
/// - 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: 58, height: 58)
.cornerRadius(8)
.shadow(radius: 1)
.overlay(
//
ZStack(alignment: .topLeading) {
//
ZStack(alignment: .topLeading) {
Path { path in
let radius: CGFloat = 4
let width: CGFloat = 14
let height: CGFloat = 10
//
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(hex: "BEBEBE").opacity(0.6))
Text("\(index + 1)")
.font(.system(size: 8, weight: .bold))
.foregroundColor(.black)
.frame(width: 14, height: 10)
.offset(y: -1)
}
.frame(width: 14, height: 10, alignment: .topLeading)
.padding([.top, .leading], 2)
// - 使
MediaPreview(media: media)
.frame(width: 58, height: 58)
.cornerRadius(8)
.shadow(radius: 1)
.overlay(
//
ZStack(alignment: .topLeading) {
Path { path in
let radius: CGFloat = 4
let width: CGFloat = 14
let height: CGFloat = 10
//
if case .video(let url, _) = media, let videoURL = url as? URL {
VStack {
//
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(hex: "BEBEBE").opacity(0.6))
.frame(width: 14, height: 10)
.overlay(
Text("\(index + 1)")
.font(.system(size: 8, weight: .bold))
.foregroundColor(.black)
.frame(width: 14, height: 10)
.offset(y: -1),
alignment: .topLeading
)
.padding([.top, .leading], 2)
//
if case .video(let url, _) = media, let videoURL = url as? URL {
VStack {
Spacer()
HStack {
Spacer()
HStack {
Spacer()
Text(getVideoDuration(url: videoURL))
.font(.system(size: 8, weight: .bold))
.foregroundColor(.black)
.padding(.horizontal, 4)
.frame(height: 10)
.background(Color(hex: "BEBEBE").opacity(0.6))
.cornerRadius(2)
}
.padding([.trailing, .bottom], 0)
Text(getVideoDuration(url: videoURL))
.font(.system(size: 8, weight: .bold))
.foregroundColor(.black)
.padding(.horizontal, 4)
.frame(height: 10)
.background(Color(hex: "BEBEBE").opacity(0.6))
.cornerRadius(2)
}
} else {
VStack {
Spacer()
HStack {
Spacer()
Text("")
.font(.system(size: 8, weight: .bold))
.foregroundColor(.black)
.padding(.horizontal, 4)
.frame(height: 10)
.background(Color(hex: "BEBEBE").opacity(0.6))
.cornerRadius(2)
}
.padding([.trailing, .bottom], 0)
}
.opacity(0)
.padding([.trailing, .bottom], 0)
}
}else{
//
VStack {
Spacer()
HStack {
Spacer()
Text("占位")
.font(.system(size: 8, weight: .bold))
.foregroundColor(.black)
.padding(.horizontal, 4)
.frame(height: 10)
.background(Color(hex: "BEBEBE").opacity(0.6))
.cornerRadius(2)
}
.padding([.trailing, .bottom], 0)
}
.opacity(0)
}
)
// mediaItemView onTapGesture
// .onTapGesture {
// print("Tapped media at index: \(index)") //
// withAnimation {
// selectedIndices = [index]
// selectedMedia = media
// }
// }
// .contentShape(Rectangle()) //
// mediaItemView onTapGesture
.onTapGesture {
print("点击了媒体项,索引: \(index)")
withAnimation {
selectedMedia = media //
}
},
alignment: .topLeading
)
.onTapGesture {
print("点击了媒体项,索引: \(index)")
withAnimation {
selectedMedia = media
}
.contentShape(Rectangle()) //
}
}
.contentShape(Rectangle())
//
Button(action: {
// 使API
uploadManager.removeMedia(id: media.id)
//
if selectedMedia == media {
selectedMedia = nil
}
}) {
Image(systemName: "xmark")
.font(.system(size: 10, weight: .bold))
.font(.system(size: 8, weight: .bold))
.foregroundColor(.black)
.frame(width: 12, height: 12)
.background(
@ -496,51 +482,12 @@ struct MainUploadArea: View {
.frame(width: 12, height: 12)
)
}
.offset(x: 6, y: -6) //
.offset(x: 6, y: -6)
}
.padding(.horizontal,4)
.padding(.horizontal, 4)
.contentShape(Rectangle())
}
///
/// - Parameter index:
/// - Returns:
@ViewBuilder
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)
.padding(.horizontal, 4)
.padding(.bottom, 2)
}
.frame(width: 60)
case .completed:
//
Image(systemName: "checkmark.circle.fill")
.font(.caption)
.foregroundColor(.green)
case .failed:
//
Image(systemName: "exclamationmark.triangle.fill")
.font(.caption)
.foregroundColor(.red)
default:
EmptyView()
}
}
}
///
private var addMoreButton: some View {
Button(action: { showMediaPicker = true }) {
@ -599,102 +546,23 @@ struct UploadPromptView: View {
// MARK: -
///
///
/// 使
struct MediaPreview: View {
// MARK: -
///
private let imageProcessingQueue = DispatchQueue(
label: "com.yourapp.imageprocessing",
qos: .userInitiated,
attributes: .concurrent
)
///
let media: MediaType
///
@ObservedObject var uploadManager: MediaUploadManager
// MARK: -
///
@State private var image: UIImage?
///
enum LoadState {
///
case success(UIImage)
///
case failure(Error)
}
///
@State private var loadState: LoadState?
// MARK: -
///
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: -
///
private var uploadProgress: Double {
guard let index = uploadManager.selectedMedia.firstIndex(where: { $0.id == media.id }) else {
return 0
///
private var displayImage: UIImage? {
switch media {
case .image(let uiImage):
return uiImage
case .video(_, let thumbnail):
return thumbnail
}
if case .uploading(let progress) = uploadManager.uploadStatus["\(index)"] {
return progress
} else if case .completed = uploadManager.uploadStatus["\(index)"] {
return 1.0
}
return 0
}
///
private var isUploading: Bool {
guard let index = uploadManager.selectedMedia.firstIndex(where: { $0.id == media.id }) else {
return false
}
if case .uploading = uploadManager.uploadStatus["\(index)"] {
return true
}
return false
}
// MARK: -
@ -702,33 +570,14 @@ struct MediaPreview: View {
var body: some View {
ZStack {
// 1.
if let image = image {
ZStack(alignment: .bottomTrailing) {
loadedImageView(image)
}
// 100%
if isUploading && uploadProgress < 1.0 {
VStack {
Spacer()
ProgressView(value: uploadProgress, total: 1.0)
.progressViewStyle(LinearProgressViewStyle(tint: .white))
.frame(height: 2)
.padding(.horizontal, 4)
.padding(.bottom, 2)
}
}
if let image = displayImage {
Image(uiImage: image)
.resizable()
.scaledToFill()
.transition(.opacity.animation(.easeInOut(duration: 0.2)))
} else {
// 2.
placeholderView
.onAppear {
loadImage()
}
}
// 3.
if case .failure(let error) = loadState, image == nil {
errorView(error: error)
Color.gray.opacity(0.1)
}
}
.aspectRatio(1, contentMode: .fill)
@ -739,100 +588,6 @@ struct MediaPreview: View {
.stroke(Color.themePrimary.opacity(0.3), lineWidth: 1)
)
}
//
private var placeholderView: some View {
Color.gray.opacity(0.1)
}
//
private func loadedImageView(_ image: UIImage) -> some View {
Image(uiImage: image)
.resizable()
.scaledToFill()
.transition(.opacity.animation(.easeInOut(duration: 0.2)))
}
///
private func errorView(error: Error) -> some View {
VStack(spacing: 8) {
Image(systemName: "exclamationmark.triangle")
.font(.system(size: 20))
.foregroundColor(.orange)
Text("加载失败")
.font(.caption2)
.foregroundColor(.secondary)
Button(action: {
loadState = nil
loadImage()
}) {
Image(systemName: "arrow.clockwise")
.font(.caption)
.padding(4)
.background(Color.gray.opacity(0.2))
.clipShape(Circle())
}
.padding(.top, 4)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
}
// MARK: -
///
private func loadImage() {
let cacheKey = "\(media.id)" as NSString
//
if let cachedImage = ImageCache.getImage(forKey: cacheKey as String) {
image = cachedImage
loadState = .success(cachedImage)
return
}
// 使
imageProcessingQueue.async {
do {
let imageToCache: UIImage
switch media {
case .image(let uiImage):
imageToCache = uiImage
case .video(_, let thumbnail):
guard let thumbnail = thumbnail else {
throw NSError(
domain: "com.yourapp.media",
code: -1,
userInfo: [NSLocalizedDescriptionKey: "视频缩略图加载失败"]
)
}
imageToCache = thumbnail
}
//
ImageCache.setImage(imageToCache, forKey: cacheKey as String)
// UI
DispatchQueue.main.async {
withAnimation(.easeInOut(duration: 0.2)) {
image = imageToCache
loadState = .success(imageToCache)
}
}
} catch {
print("图片加载失败: \(error.localizedDescription)")
DispatchQueue.main.async {
loadState = .failure(error)
}
}
}
}
}
private func getVideoDuration(url: URL) -> String {