feat: 暂提
This commit is contained in:
parent
2cc7e5fb01
commit
45b8d211af
@ -302,7 +302,7 @@ struct MainUploadArea: View {
|
|||||||
.frame(height: 50)
|
.frame(height: 50)
|
||||||
// 主显示区域
|
// 主显示区域
|
||||||
if let mediaToDisplay = selectedMedia ?? uploadManager.selectedMedia.first {
|
if let mediaToDisplay = selectedMedia ?? uploadManager.selectedMedia.first {
|
||||||
MediaPreview(media: mediaToDisplay, uploadManager: uploadManager)
|
MediaPreview(media: mediaToDisplay)
|
||||||
.id(mediaToDisplay.id)
|
.id(mediaToDisplay.id)
|
||||||
.frame(width: 225, height: 225)
|
.frame(width: 225, height: 225)
|
||||||
.overlay(
|
.overlay(
|
||||||
@ -374,120 +374,106 @@ struct MainUploadArea: View {
|
|||||||
/// - Returns: 媒体项视图
|
/// - Returns: 媒体项视图
|
||||||
private func mediaItemView(for media: MediaType, at index: Int) -> some View {
|
private func mediaItemView(for media: MediaType, at index: Int) -> some View {
|
||||||
ZStack(alignment: .topTrailing) {
|
ZStack(alignment: .topTrailing) {
|
||||||
VStack(spacing: 4) {
|
// 媒体预览 - 始终使用本地资源
|
||||||
// 媒体预览
|
MediaPreview(media: media)
|
||||||
MediaPreview(media: media, uploadManager: uploadManager)
|
.frame(width: 58, height: 58)
|
||||||
.frame(width: 58, height: 58)
|
.cornerRadius(8)
|
||||||
.cornerRadius(8)
|
.shadow(radius: 1)
|
||||||
.shadow(radius: 1)
|
.overlay(
|
||||||
.overlay(
|
// 左上角序号
|
||||||
// 左上角序号
|
ZStack(alignment: .topLeading) {
|
||||||
ZStack(alignment: .topLeading) {
|
Path { path in
|
||||||
// 左上角序号
|
let radius: CGFloat = 4
|
||||||
ZStack(alignment: .topLeading) {
|
let width: CGFloat = 14
|
||||||
Path { path in
|
let height: CGFloat = 10
|
||||||
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)
|
|
||||||
|
|
||||||
// 右下角视频时长
|
// 从左上角开始(带圆角)
|
||||||
if case .video(let url, _) = media, let videoURL = url as? URL {
|
path.move(to: CGPoint(x: 0, y: radius))
|
||||||
VStack {
|
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()
|
Spacer()
|
||||||
HStack {
|
Text(getVideoDuration(url: videoURL))
|
||||||
Spacer()
|
.font(.system(size: 8, weight: .bold))
|
||||||
Text(getVideoDuration(url: videoURL))
|
.foregroundColor(.black)
|
||||||
.font(.system(size: 8, weight: .bold))
|
.padding(.horizontal, 4)
|
||||||
.foregroundColor(.black)
|
.frame(height: 10)
|
||||||
.padding(.horizontal, 4)
|
.background(Color(hex: "BEBEBE").opacity(0.6))
|
||||||
.frame(height: 10)
|
.cornerRadius(2)
|
||||||
.background(Color(hex: "BEBEBE").opacity(0.6))
|
|
||||||
.cornerRadius(2)
|
|
||||||
}
|
|
||||||
.padding([.trailing, .bottom], 0)
|
|
||||||
}
|
}
|
||||||
} else {
|
.padding([.trailing, .bottom], 0)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
}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:
|
alignment: .topLeading
|
||||||
// .onTapGesture {
|
)
|
||||||
// print("Tapped media at index: \(index)") // 添加日志
|
.onTapGesture {
|
||||||
// withAnimation {
|
print("点击了媒体项,索引: \(index)")
|
||||||
// selectedIndices = [index]
|
withAnimation {
|
||||||
// selectedMedia = media
|
selectedMedia = media
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// .contentShape(Rectangle()) // 确保整个区域都可点击
|
|
||||||
// 在 mediaItemView 中,更新 onTapGesture:
|
|
||||||
.onTapGesture {
|
|
||||||
print("点击了媒体项,索引: \(index)")
|
|
||||||
withAnimation {
|
|
||||||
selectedMedia = media // 直接更新选中的媒体
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.contentShape(Rectangle()) // 确保整个区域都可点击
|
}
|
||||||
}
|
.contentShape(Rectangle())
|
||||||
|
|
||||||
// 右上角关闭按钮
|
// 右上角关闭按钮
|
||||||
Button(action: {
|
Button(action: {
|
||||||
// 使用公共API移除媒体
|
|
||||||
uploadManager.removeMedia(id: media.id)
|
uploadManager.removeMedia(id: media.id)
|
||||||
|
|
||||||
// 重置选中的媒体
|
|
||||||
if selectedMedia == media {
|
if selectedMedia == media {
|
||||||
selectedMedia = nil
|
selectedMedia = nil
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
Image(systemName: "xmark")
|
Image(systemName: "xmark")
|
||||||
.font(.system(size: 10, weight: .bold))
|
.font(.system(size: 8, weight: .bold))
|
||||||
.foregroundColor(.black)
|
.foregroundColor(.black)
|
||||||
.frame(width: 12, height: 12)
|
.frame(width: 12, height: 12)
|
||||||
.background(
|
.background(
|
||||||
@ -496,51 +482,12 @@ struct MainUploadArea: View {
|
|||||||
.frame(width: 12, height: 12)
|
.frame(width: 12, height: 12)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.offset(x: 6, y: -6) // 调整位置,确保完全可见
|
.offset(x: 6, y: -6)
|
||||||
}
|
}
|
||||||
.padding(.horizontal,4)
|
.padding(.horizontal, 4)
|
||||||
.contentShape(Rectangle())
|
.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 {
|
private var addMoreButton: some View {
|
||||||
Button(action: { showMediaPicker = true }) {
|
Button(action: { showMediaPicker = true }) {
|
||||||
@ -599,102 +546,23 @@ struct UploadPromptView: View {
|
|||||||
// MARK: - 媒体预览视图
|
// MARK: - 媒体预览视图
|
||||||
|
|
||||||
/// 媒体预览视图
|
/// 媒体预览视图
|
||||||
/// 显示图片或视频的预览图
|
/// 显示图片或视频的预览图,始终使用本地资源
|
||||||
struct MediaPreview: View {
|
struct MediaPreview: View {
|
||||||
// MARK: - 属性
|
// MARK: - 属性
|
||||||
|
|
||||||
/// 图片处理队列
|
|
||||||
private let imageProcessingQueue = DispatchQueue(
|
|
||||||
label: "com.yourapp.imageprocessing",
|
|
||||||
qos: .userInitiated,
|
|
||||||
attributes: .concurrent
|
|
||||||
)
|
|
||||||
|
|
||||||
/// 媒体类型
|
/// 媒体类型
|
||||||
let media: MediaType
|
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: - 计算属性
|
// MARK: - 计算属性
|
||||||
|
|
||||||
/// 上传进度
|
/// 获取要显示的图片
|
||||||
private var uploadProgress: Double {
|
private var displayImage: UIImage? {
|
||||||
guard let index = uploadManager.selectedMedia.firstIndex(where: { $0.id == media.id }) else {
|
switch media {
|
||||||
return 0
|
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: - 视图主体
|
// MARK: - 视图主体
|
||||||
@ -702,33 +570,14 @@ struct MediaPreview: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
// 1. 显示图片或视频缩略图
|
// 1. 显示图片或视频缩略图
|
||||||
if let image = image {
|
if let image = displayImage {
|
||||||
ZStack(alignment: .bottomTrailing) {
|
Image(uiImage: image)
|
||||||
loadedImageView(image)
|
.resizable()
|
||||||
}
|
.scaledToFill()
|
||||||
|
.transition(.opacity.animation(.easeInOut(duration: 0.2)))
|
||||||
// 上传进度条(仅当正在上传且进度小于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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// 2. 加载中的占位图
|
// 2. 加载中的占位图
|
||||||
placeholderView
|
Color.gray.opacity(0.1)
|
||||||
.onAppear {
|
|
||||||
loadImage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 显示错误状态(如果有)
|
|
||||||
if case .failure(let error) = loadState, image == nil {
|
|
||||||
errorView(error: error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.aspectRatio(1, contentMode: .fill)
|
.aspectRatio(1, contentMode: .fill)
|
||||||
@ -739,100 +588,6 @@ struct MediaPreview: View {
|
|||||||
.stroke(Color.themePrimary.opacity(0.3), lineWidth: 1)
|
.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 {
|
private func getVideoDuration(url: URL) -> String {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user