diff --git a/yana/Features/EditFeedFeature.swift b/yana/Features/EditFeedFeature.swift index 8779301..4e79910 100644 --- a/yana/Features/EditFeedFeature.swift +++ b/yana/Features/EditFeedFeature.swift @@ -255,4 +255,4 @@ struct EditFeedFeature { } } } -} \ No newline at end of file +} diff --git a/yana/Utils/COSManager.swift b/yana/Utils/COSManager.swift deleted file mode 100644 index 30191a1..0000000 --- a/yana/Utils/COSManager.swift +++ /dev/null @@ -1,241 +0,0 @@ -import Foundation -import QCloudCOSXML - -// MARK: - 腾讯云 COS 管理器 - -/// 腾讯云 COS 管理器 -/// -/// 负责管理腾讯云 COS 相关的操作,包括: -/// - Token 获取和缓存 -/// - 文件上传、下载、删除 -/// - 凭证管理和过期处理 -@MainActor -class COSManager: ObservableObject { - static let shared = COSManager() - - private init() {} - - // 幂等初始化标记 - private static var isCOSInitialized = false - - // 幂等初始化方法 - private func ensureCOSInitialized(tokenData: TcTokenData) { - guard !Self.isCOSInitialized else { return } - let configuration = QCloudServiceConfiguration() - let endpoint = QCloudCOSXMLEndPoint() - endpoint.regionName = tokenData.region - endpoint.useHTTPS = true - if tokenData.accelerate { - endpoint.suffix = "cos.accelerate.myqcloud.com" - } - configuration.endpoint = endpoint - QCloudCOSXMLService.registerDefaultCOSXML(with: configuration) - QCloudCOSTransferMangerService.registerDefaultCOSTransferManger(with: configuration) - Self.isCOSInitialized = true - debugInfoSync("✅ COS服务已初始化,region: \(tokenData.region)") - } - - // MARK: - Token 管理 - - /// 当前缓存的 Token 信息 - private var cachedToken: TcTokenData? - private var tokenExpirationDate: Date? - - /// 获取腾讯云 COS Token - /// - Parameter apiService: API 服务实例 - /// - Returns: Token 数据,如果获取失败返回 nil - func getToken(apiService: any APIServiceProtocol & Sendable) async -> TcTokenData? { - // 检查缓存是否有效 - if let cached = cachedToken, let expiration = tokenExpirationDate, Date() < expiration { - debugInfoSync("🔐 使用缓存的 COS Token") - return cached - } - - // 清除过期缓存 - clearCachedToken() - - // 请求新的 Token - debugInfoSync("🔐 开始请求腾讯云 COS Token...") - - do { - let request = TcTokenRequest() - let response: TcTokenResponse = try await apiService.request(request) - - guard response.code == 200, let tokenData = response.data else { - debugInfoSync("❌ COS Token 请求失败: \(response.message)") - return nil - } - - // 缓存 Token 和过期时间 - cachedToken = tokenData - tokenExpirationDate = tokenData.expirationDate - - debugInfoSync("✅ COS Token 获取成功") - debugInfoSync(" - 存储桶: \(tokenData.bucket)") - debugInfoSync(" - 地域: \(tokenData.region)") - debugInfoSync(" - 过期时间: \(tokenData.expirationDate)") - debugInfoSync(" - 剩余时间: \(tokenData.remainingTime)秒") - - return tokenData - - } catch { - debugInfoSync("❌ COS Token 请求异常: \(error.localizedDescription)") - return nil - } - } - - /// 缓存 Token 信息 - /// - Parameter tokenData: Token 数据 - private func cacheToken(_ tokenData: TcTokenData) async { - cachedToken = tokenData - - // 解析过期时间(假设 expiration 是 ISO 8601 格式) - if let expirationDate = ISO8601DateFormatter().date(from: String(tokenData.expireTime)) { - // 提前 5 分钟过期,确保安全 - tokenExpirationDate = expirationDate.addingTimeInterval(-300) - } else { - // 如果解析失败,设置默认过期时间(1小时) - tokenExpirationDate = Date().addingTimeInterval(3600) - } - - debugInfoSync("💾 COS Token 已缓存,过期时间: \(tokenExpirationDate?.description ?? "未知")") - } - - /// 清除缓存的 Token - private func clearCachedToken() { - cachedToken = nil - tokenExpirationDate = nil - debugInfoSync("🗑️ 清除缓存的 COS Token") - } - - /// 强制刷新 Token - func refreshToken(apiService: any APIServiceProtocol & Sendable) async -> TcTokenData? { - clearCachedToken() - return await getToken(apiService: apiService) - } - - // MARK: - 只读属性 - /// 外部安全访问 Token - var token: TcTokenData? { cachedToken } - - // MARK: - 调试信息 - - /// 获取当前 Token 状态信息 - func getTokenStatus() -> String { - if let _ = cachedToken, let expiration = tokenExpirationDate { - let isExpired = Date() >= expiration - return "Token 状态: \(isExpired ? "已过期" : "有效"), 过期时间: \(expiration)" - } else { - return "Token 状态: 未缓存" - } - } - - // MARK: - 上传功能 - - /// 上传图片到腾讯云 COS - /// - Parameters: - /// - imageData: 图片数据 - /// - apiService: API 服务实例 - /// - Returns: 上传成功的云地址,如果失败返回 nil - func uploadImage(_ imageData: Data, apiService: any APIServiceProtocol & Sendable) async -> String? { - guard let tokenData = await getToken(apiService: apiService) else { - debugInfoSync("❌ 无法获取 COS Token") - return nil - } - // 上传前确保COS服务已初始化 - ensureCOSInitialized(tokenData: tokenData) - - // 初始化 COS 配置 - let credential = QCloudCredential() - credential.secretID = tokenData.secretId - // 打印secretKey原始内容,去除首尾空白 - let rawSecretKey = tokenData.secretKey.trimmingCharacters(in: .whitespacesAndNewlines) - debugInfoSync("secretKey原始内容: [\(rawSecretKey)]") - credential.secretKey = rawSecretKey - credential.token = tokenData.sessionToken - credential.startDate = tokenData.startDate - credential.expirationDate = tokenData.expirationDate - - let request = QCloudCOSXMLUploadObjectRequest() - request.bucket = tokenData.bucket - request.regionName = tokenData.region - request.credential = credential - - // 生成唯一 key - let fileExtension = "jpg" // 假设为 JPG,可根据实际调整 - let key = "images/\(UUID().uuidString).\(fileExtension)" - request.object = key - request.body = imageData as AnyObject - - //监听上传进度 - request.sendProcessBlock = { (bytesSent, totalBytesSent, - totalBytesExpectedToSend) in - debugInfoSync("\(bytesSent), \(totalBytesSent), \(totalBytesExpectedToSend)") - // bytesSent 本次要发送的字节数(一个大文件可能要分多次发送) - // totalBytesSent 已发送的字节数 - // totalBytesExpectedToSend 本次上传要发送的总字节数(即一个文件大小) - }; - - // 设置加速 - if tokenData.accelerate { - request.enableQuic = true - // endpoint 增加 "cos.accelerate.myqcloud.com" - } - - // 使用 async/await 包装上传回调 - return await withCheckedContinuation { continuation in - request.setFinish { result, error in - if let error = error { - debugInfoSync("❌ 图片上传失败: \(error.localizedDescription)") - continuation.resume(returning: " ?????????? ") - } else { - // 构建云地址 - let domain = tokenData.customDomain.isEmpty ? "\(tokenData.bucket).cos.\(tokenData.region).myqcloud.com" : tokenData.customDomain - let prefix = domain.hasPrefix("http") ? "" : "https://" - let cloudURL = "\(prefix)\(domain)/\(key)" - debugInfoSync("✅ 图片上传成功: \(cloudURL)") - continuation.resume(returning: cloudURL) - } - } - QCloudCOSTransferMangerService.defaultCOSTransferManager().uploadObject(request) - } - } - - /// 上传 UIImage 到腾讯云 COS,自动压缩为 JPEG(0.8) - /// - Parameters: - /// - image: UIImage 实例 - /// - apiService: API 服务实例 - /// - Returns: 上传成功的云地址,如果失败返回 nil - func uploadUIImage(_ image: UIImage, apiService: any APIServiceProtocol & Sendable) async -> String? { - guard let data = image.jpegData(compressionQuality: 0.8) else { - debugInfoSync("❌ 图片压缩失败,无法生成 JPEG 数据") - return nil - } - return await uploadImage(data, apiService: apiService) - } -} - -// MARK: - 调试扩展 - -extension COSManager { - /// 测试 Token 获取功能(仅用于调试) - func testTokenRetrieval(apiService: any APIServiceProtocol & Sendable) async { - #if DEBUG - debugInfoSync("\n🧪 开始测试腾讯云 COS Token 获取功能") - - let token = await getToken(apiService: apiService) - if let tokenData = token { - debugInfoSync("✅ Token 获取成功") - debugInfoSync(" bucket: \(tokenData.bucket)") - debugInfoSync(" Expiration: \(tokenData.expireTime)") - debugInfoSync(" Token: \(tokenData.sessionToken.prefix(20))...") - debugInfoSync(" SecretId: \(tokenData.secretId.prefix(20))...") - } else { - debugInfoSync("❌ Token 获取失败") - } - - debugInfoSync("📊 Token 状态: \(getTokenStatus())") - debugInfoSync("✅ 腾讯云 COS Token 测试完成\n") - #endif - } -} diff --git a/yana/Utils/TCCos/COSManagerAdapter.swift b/yana/Utils/TCCos/COSManagerAdapter.swift new file mode 100644 index 0000000..1c85220 --- /dev/null +++ b/yana/Utils/TCCos/COSManagerAdapter.swift @@ -0,0 +1,8 @@ +// +// COSManagerAdapter.swift +// yana +// +// Created by P on 2025/7/31. +// + +import Foundation diff --git a/yana/Views/CreateFeedView.swift b/yana/Views/CreateFeedView.swift index 62220d7..e764278 100644 --- a/yana/Views/CreateFeedView.swift +++ b/yana/Views/CreateFeedView.swift @@ -174,66 +174,64 @@ struct CreateFeedView: View { } // MARK: - iOS 16+ 图片选择网格组件 -//struct ModernImageSelectionGrid: View { -// let images: [UIImage] -// let selectedItems: [PhotosPickerItem] -// let canAddMore: Bool -// let onItemsChanged: ([PhotosPickerItem]) -> Void -// let onRemoveImage: (Int) -> Void -// -// private let columns = Array(repeating: GridItem(.flexible(), spacing: 8), count: 3) -// -// var body: some View { -// WithPerceptionTracking { -// LazyVGrid(columns: columns, spacing: 8) { -// // 显示已选择的图片 -// ForEach(Array(images.enumerated()), id: \.offset) { index, image in -// ZStack(alignment: .topTrailing) { -// Image(uiImage: image) -// .resizable() -// .aspectRatio(contentMode: .fill) -// .frame(height: 100) -// .clipped() -// .cornerRadius(8) -// -// // 删除按钮 -// Button(action: { -// onRemoveImage(index) -// }) { -// Image(systemName: "xmark.circle.fill") -// .font(.system(size: 20)) -// .foregroundColor(.white) -// .background(Color.black.opacity(0.6)) -// .clipShape(Circle()) -// } -// .padding(4) -// } -// } -// -// // 添加图片按钮 -// if canAddMore { -// PhotosPicker( -// selection: .init( -// get: { selectedItems }, -// set: onItemsChanged -// ), -// maxSelectionCount: 9, -// matching: .images -// ) { -// RoundedRectangle(cornerRadius: 8) -// .fill(Color.white.opacity(0.1)) -// .frame(height: 100) -// .overlay( -// Image(systemName: "plus") -// .font(.system(size: 40)) -// .foregroundColor(.white.opacity(0.6)) -// ) -// } -// } -// } -// } -// } -//} +struct ModernImageSelectionGrid: View { + let images: [UIImage] + let selectedItems: [PhotosPickerItem] + let canAddMore: Bool + let onItemsChanged: ([PhotosPickerItem]) -> Void + let onRemoveImage: (Int) -> Void + + private let columns = Array(repeating: GridItem(.flexible(), spacing: 8), count: 3) + + var body: some View { + LazyVGrid(columns: columns, spacing: 8) { + // 显示已选择的图片 + ForEach(Array(images.enumerated()), id: \.offset) { index, image in + ZStack(alignment: .topTrailing) { + Image(uiImage: image) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(height: 100) + .clipped() + .cornerRadius(8) + + // 删除按钮 + Button(action: { + onRemoveImage(index) + }) { + Image(systemName: "xmark.circle.fill") + .font(.system(size: 20)) + .foregroundColor(.white) + .background(Color.black.opacity(0.6)) + .clipShape(Circle()) + } + .padding(4) + } + } + + // 添加图片按钮 + if canAddMore { + PhotosPicker( + selection: .init( + get: { selectedItems }, + set: onItemsChanged + ), + maxSelectionCount: 9, + matching: .images + ) { + RoundedRectangle(cornerRadius: 8) + .fill(Color.white.opacity(0.1)) + .frame(height: 100) + .overlay( + Image(systemName: "plus") + .font(.system(size: 40)) + .foregroundColor(.white.opacity(0.6)) + ) + } + } + } + } +} // MARK: - 预览 //#Preview { diff --git a/yana/Views/EditFeedView.swift b/yana/Views/EditFeedView.swift index 4f723e8..f03e8dc 100644 --- a/yana/Views/EditFeedView.swift +++ b/yana/Views/EditFeedView.swift @@ -13,70 +13,68 @@ struct EditFeedView: View { } var body: some View { - WithPerceptionTracking { - GeometryReader { geometry in - ZStack { - backgroundView - mainContent(geometry: geometry) - if store.isUploadingImages { - uploadingImagesOverlay(progress: store.imageUploadProgress) - } else if store.isLoading { - loadingOverlay - } - } - .contentShape(Rectangle()) - .onTapGesture { - if isKeyboardVisible { - hideKeyboard() - } - } - .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { _ in - withAnimation(.easeInOut(duration: 0.3)) { - isKeyboardVisible = true - } - } - .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in - withAnimation(.easeInOut(duration: 0.3)) { - isKeyboardVisible = false - } + GeometryReader { geometry in + ZStack { + backgroundView + mainContent(geometry: geometry) + if store.isUploadingImages { + uploadingImagesOverlay(progress: store.imageUploadProgress) + } else if store.isLoading { + loadingOverlay } } - .navigationBarHidden(true) - .onAppear { - store.send(.clearError) - } - .onChange(of: store.shouldDismiss) { - if store.shouldDismiss { - onDismiss() + .contentShape(Rectangle()) + .onTapGesture { + if isKeyboardVisible { + hideKeyboard() } } - .photosPicker( - isPresented: Binding( - get: { store.showPhotosPicker }, - set: { _ in store.send(.photosPickerDismissed) } - ), - selection: Binding( - get: { store.selectedPhotoItems }, - set: { store.send(.photosPickerItemsChanged($0)) } - ), - maxSelectionCount: 9, - matching: .images - ) - .alert("删除图片", isPresented: Binding( - get: { store.showDeleteImageAlert }, - set: { _ in store.send(.deleteImageAlertDismissed) } - )) { - Button("删除", role: .destructive) { - if let indexToDelete = store.imageToDeleteIndex { - store.send(.removeImage(indexToDelete)) - } + .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { _ in + withAnimation(.easeInOut(duration: 0.3)) { + isKeyboardVisible = true } - Button("取消", role: .cancel) { - store.send(.deleteImageAlertDismissed) - } - } message: { - Text("确定要删除这张图片吗?") } + .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in + withAnimation(.easeInOut(duration: 0.3)) { + isKeyboardVisible = false + } + } + } + .navigationBarHidden(true) + .onAppear { + store.send(.clearError) + } + .onChange(of: store.shouldDismiss) { + if store.shouldDismiss { + onDismiss() + } + } + .photosPicker( + isPresented: Binding( + get: { store.showPhotosPicker }, + set: { _ in store.send(.photosPickerDismissed) } + ), + selection: Binding( + get: { store.selectedPhotoItems }, + set: { store.send(.photosPickerItemsChanged($0)) } + ), + maxSelectionCount: 9, + matching: .images + ) + .alert("删除图片", isPresented: Binding( + get: { store.showDeleteImageAlert }, + set: { _ in store.send(.deleteImageAlertDismissed) } + )) { + Button("删除", role: .destructive) { + if let indexToDelete = store.imageToDeleteIndex { + store.send(.removeImage(indexToDelete)) + } + } + Button("取消", role: .cancel) { + store.send(.deleteImageAlertDismissed) + } + } message: { + Text("确定要删除这张图片吗?") } } @@ -86,127 +84,117 @@ struct EditFeedView: View { } private func mainContent(geometry: GeometryProxy) -> some View { - WithPerceptionTracking { - VStack(spacing: 0) { - topNavigationBar - - ScrollView { - VStack(spacing: 20) { - textInputSection - imageSelectionSection - publishButton - } - .padding(.horizontal, 20) - .padding(.bottom, 40) + VStack(spacing: 0) { + topNavigationBar + + ScrollView { + VStack(spacing: 20) { + textInputSection + imageSelectionSection + publishButton } + .padding(.horizontal, 20) + .padding(.bottom, 40) } } } private var topNavigationBar: some View { - WithPerceptionTracking { - HStack { - Button(action: { - store.send(.clearDismissFlag) - onDismiss() - }) { - Image(systemName: "xmark") - .font(.system(size: 20, weight: .medium)) - .foregroundColor(.white) - .frame(width: 44, height: 44) - } - - Spacer() - - Text("编辑动态") - .font(.system(size: 18, weight: .medium)) + HStack { + Button(action: { + store.send(.clearDismissFlag) + onDismiss() + }) { + Image(systemName: "xmark") + .font(.system(size: 20, weight: .medium)) .foregroundColor(.white) - - Spacer() - - Color.clear .frame(width: 44, height: 44) } - .padding(.horizontal, 16) - .padding(.top, 8) - .padding(.bottom, 16) + + Spacer() + + Text("编辑动态") + .font(.system(size: 18, weight: .medium)) + .foregroundColor(.white) + + Spacer() + + Color.clear + .frame(width: 44, height: 44) } + .padding(.horizontal, 16) + .padding(.top, 8) + .padding(.bottom, 16) } private var textInputSection: some View { - WithPerceptionTracking { - VStack(alignment: .leading, spacing: 12) { - Text("分享你的想法...") - .font(.system(size: 16, weight: .medium)) - .foregroundColor(.white) - - TextEditor(text: Binding( - get: { store.content }, - set: { store.send(.contentChanged($0)) } - )) - .font(.system(size: 16)) + VStack(alignment: .leading, spacing: 12) { + Text("分享你的想法...") + .font(.system(size: 16, weight: .medium)) .foregroundColor(.white) - .background(Color.clear) - .frame(minHeight: 120) - .padding(12) - .background(Color.white.opacity(0.1)) - .cornerRadius(12) - .overlay( - RoundedRectangle(cornerRadius: 12) - .stroke(Color.white.opacity(0.2), lineWidth: 1) - ) - - HStack { - Spacer() - Text("\(store.content.count)/\(maxCount)") - .font(.system(size: 12)) - .foregroundColor(.white.opacity(0.6)) - } + + TextEditor(text: Binding( + get: { store.content }, + set: { store.send(.contentChanged($0)) } + )) + .font(.system(size: 16)) + .foregroundColor(.white) + .background(Color.clear) + .frame(minHeight: 120) + .padding(12) + .background(Color.white.opacity(0.1)) + .cornerRadius(12) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color.white.opacity(0.2), lineWidth: 1) + ) + + HStack { + Spacer() + Text("\(store.content.count)/\(maxCount)") + .font(.system(size: 12)) + .foregroundColor(.white.opacity(0.6)) } } } private var imageSelectionSection: some View { - WithPerceptionTracking { - VStack(alignment: .leading, spacing: 12) { - Text("添加图片") - .font(.system(size: 16, weight: .medium)) - .foregroundColor(.white) - - ImageGrid( - images: store.processedImages, - onRemoveImage: { index in - store.send(.showDeleteImageAlert(index)) - }, - onAddImage: { - store.send(.addImageButtonTapped) - } - ) - } + VStack(alignment: .leading, spacing: 12) { + Text("添加图片") + .font(.system(size: 16, weight: .medium)) + .foregroundColor(.white) + + ImageGrid( + images: store.processedImages, + onRemoveImage: { index in + store.send(.showDeleteImageAlert(index)) + }, + onAddImage: { + store.send(.addImageButtonTapped) + } + ) } } private var publishButton: some View { - WithPerceptionTracking { - Button(action: { - store.send(.publishButtonTapped) - }) { - if store.isLoading { - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - .scaleEffect(1.2) - } else { - Text("发布") - .font(.system(size: 16, weight: .medium)) - .foregroundColor(.white) - } + Button(action: { + store.send(.publishButtonTapped) + }) { + if store.isLoading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .scaleEffect(1.2) + } else { + Text("发布") + .font(.system(size: 16, weight: .medium)) + .foregroundColor(.white) } - .frame(maxWidth: .infinity) - .padding(.vertical, 16) - .background(store.content.isEmpty ? Color.gray : Color.blue) - .cornerRadius(12) - .disabled(store.isLoading || store.content.isEmpty) } + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + .background(store.content.isEmpty ? Color.gray : Color.blue) + .cornerRadius(12) + .disabled(store.isLoading || store.content.isEmpty) } private func uploadingImagesOverlay(progress: Double) -> some View {