diff --git a/yana/APIs/DynamicsModels.swift b/yana/APIs/DynamicsModels.swift index c11cf2d..3bf85a4 100644 --- a/yana/APIs/DynamicsModels.swift +++ b/yana/APIs/DynamicsModels.swift @@ -4,7 +4,7 @@ import ComposableArchitecture // MARK: - 响应数据模型 /// 最新动态响应结构 -struct MomentsLatestResponse: Codable, Equatable { +struct MomentsLatestResponse: Codable, Equatable, Sendable { let code: Int let message: String let data: MomentsListData? @@ -12,13 +12,13 @@ struct MomentsLatestResponse: Codable, Equatable { } /// 动态列表数据 -struct MomentsListData: Codable, Equatable { +struct MomentsListData: Codable, Equatable, Sendable { let dynamicList: [MomentsInfo] let nextDynamicId: Int } /// 动态信息结构 -struct MomentsInfo: Codable, Equatable { +public struct MomentsInfo: Codable, Equatable, Sendable { let dynamicId: Int let uid: Int let nick: String @@ -66,7 +66,7 @@ struct MomentsInfo: Codable, Equatable { } /// 动态图片信息 -struct MomentsPicture: Codable, Equatable { +struct MomentsPicture: Codable, Equatable, Sendable { let id: Int let resUrl: String let format: String @@ -76,7 +76,7 @@ struct MomentsPicture: Codable, Equatable { } /// 用户VIP信息 - 完整版本,所有字段都是可选的 -struct UserVipInfo: Codable, Equatable { +struct UserVipInfo: Codable, Equatable, Sendable { let vipLevel: Int? let vipName: String? let vipIcon: String? diff --git a/yana/Features/FeedListFeature.swift b/yana/Features/FeedListFeature.swift index b25d8a5..579d7c4 100644 --- a/yana/Features/FeedListFeature.swift +++ b/yana/Features/FeedListFeature.swift @@ -1,12 +1,16 @@ import Foundation import ComposableArchitecture -struct FeedListFeature: Reducer { +@Reducer +struct FeedListFeature { + @Dependency(\.apiService) var apiService struct State: Equatable { var feeds: [Feed] = [] // 预留 feed 内容 var isLoading: Bool = false var error: String? = nil var isEditFeedPresented: Bool = false // 新增:控制 EditFeedView 弹窗 + // 新增:动态内容 + var moments: [MomentsInfo] = [] } enum Action: Equatable { @@ -15,13 +19,41 @@ struct FeedListFeature: Reducer { case loadMore case editFeedButtonTapped // 新增:点击 add 按钮 case editFeedDismissed // 新增:关闭编辑页 + // 新增:动态内容相关 + case fetchFeeds + case fetchFeedsResponse(TaskResult) // 预留后续 Action } func reduce(into state: inout State, action: Action) -> Effect { switch action { case .onAppear: - // 预留数据加载逻辑 + // 页面展示时自动请求 feed 数据 + return .send(.fetchFeeds) + case .fetchFeeds: + state.isLoading = true + state.error = nil + // 发起 API 请求 + return .run { [apiService] send in + await send(.fetchFeedsResponse(TaskResult { + let request = LatestDynamicsRequest(dynamicId: "", pageSize: 20, types: [.text, .picture]) + return try await apiService.request(request) + })) + } + case let .fetchFeedsResponse(.success(response)): + state.isLoading = false + if let list = response.data?.dynamicList { + state.moments = list + state.error = nil + } else { + state.moments = [] + state.error = response.message + } + return .none + case let .fetchFeedsResponse(.failure(error)): + state.isLoading = false + state.moments = [] + state.error = error.localizedDescription return .none case .reload: // 预留刷新逻辑 diff --git a/yana/Utils/COSManager.swift b/yana/Utils/COSManager.swift index 1980ec8..7fe12c2 100644 --- a/yana/Utils/COSManager.swift +++ b/yana/Utils/COSManager.swift @@ -1,5 +1,6 @@ import Foundation import ComposableArchitecture +import QCloudCOSXML // MARK: - 腾讯云 COS 管理器 @@ -15,6 +16,26 @@ class COSManager: ObservableObject { 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 信息 @@ -102,13 +123,83 @@ class COSManager: ObservableObject { /// 获取当前 Token 状态信息 func getTokenStatus() -> String { - if let cached = cachedToken, let expiration = tokenExpirationDate { + 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 cloudURL = "https://\(domain)/\(key)" + debugInfoSync("✅ 图片上传成功: \(cloudURL)") + continuation.resume(returning: cloudURL) + } + } + QCloudCOSTransferMangerService.defaultCOSTransferManager().uploadObject(request) + } + } } // MARK: - 调试扩展 @@ -134,4 +225,4 @@ extension COSManager { debugInfoSync("✅ 腾讯云 COS Token 测试完成\n") #endif } -} +} diff --git a/yana/Views/Components/OptimizedDynamicCardView.swift b/yana/Views/Components/OptimizedDynamicCardView.swift new file mode 100644 index 0000000..2550675 --- /dev/null +++ b/yana/Views/Components/OptimizedDynamicCardView.swift @@ -0,0 +1,240 @@ +import SwiftUI +import ComposableArchitecture +import Foundation + +// MARK: - 优化的动态卡片组件 +struct OptimizedDynamicCardView: View { + let moment: MomentsInfo + let allMoments: [MomentsInfo] + let currentIndex: Int + + init(moment: MomentsInfo, allMoments: [MomentsInfo], currentIndex: Int) { + self.moment = moment + self.allMoments = allMoments + self.currentIndex = currentIndex + } + + public var body: some View { + VStack(alignment: .leading, spacing: 12) { + // 用户信息 + HStack { + // 头像 + CachedAsyncImage(url: moment.avatar) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + Circle() + .fill(Color.gray.opacity(0.3)) + .overlay( + Text(String(moment.nick.prefix(1))) + .font(.system(size: 16, weight: .medium)) + .foregroundColor(.white) + ) + } + .frame(width: 40, height: 40) + .clipShape(Circle()) + + VStack(alignment: .leading, spacing: 2) { + Text(moment.nick) + .font(.system(size: 16, weight: .medium)) + .foregroundColor(.white) + + Text(formatTime(moment.publishTime)) + .font(.system(size: 12)) + .foregroundColor(.white.opacity(0.6)) + } + + Spacer() + + // VIP 标识 + if let vipInfo = moment.userVipInfoVO, let vipLevel = vipInfo.vipLevel { + Text("VIP\(vipLevel)") + .font(.system(size: 10, weight: .bold)) + .foregroundColor(.yellow) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(Color.yellow.opacity(0.2)) + .cornerRadius(4) + } + } + + // 动态内容 + if !moment.content.isEmpty { + Text(moment.content) + .font(.system(size: 14)) + .foregroundColor(.white.opacity(0.9)) + .multilineTextAlignment(.leading) + } + + // 优化的图片网格 + if let images = moment.dynamicResList, !images.isEmpty { + OptimizedImageGrid(images: images) + } + + // 互动按钮 + HStack(spacing: 20) { + Button(action: {}) { + HStack(spacing: 4) { + Image(systemName: "message") + .font(.system(size: 16)) + Text("\(moment.commentCount)") + .font(.system(size: 14)) + } + .foregroundColor(.white.opacity(0.8)) + } + + Button(action: {}) { + HStack(spacing: 4) { + Image(systemName: moment.isLike ? "heart.fill" : "heart") + .font(.system(size: 16)) + Text("\(moment.likeCount)") + .font(.system(size: 14)) + } + .foregroundColor(moment.isLike ? .red : .white.opacity(0.8)) + } + + Spacer() + } + .padding(.top, 8) + } + .padding(16) + .background( + Color.white.opacity(0.1) + .cornerRadius(12) + ) + .onAppear { + preloadNearbyImages() + } + } + + private func formatTime(_ timestamp: Int) -> String { + let date = Date(timeIntervalSince1970: TimeInterval(timestamp) / 1000.0) + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "zh_CN") + + let now = Date() + let interval = now.timeIntervalSince(date) + + if interval < 60 { + return "刚刚" + } else if interval < 3600 { + return "\(Int(interval / 60))分钟前" + } else if interval < 86400 { + return "\(Int(interval / 3600))小时前" + } else { + formatter.dateFormat = "MM-dd HH:mm" + return formatter.string(from: date) + } + } + + private func preloadNearbyImages() { + var urlsToPreload: [String] = [] + let preloadRange = max(0, currentIndex - 2)...min(allMoments.count - 1, currentIndex + 2) + for index in preloadRange { + let moment = allMoments[index] + urlsToPreload.append(moment.avatar) + if let images = moment.dynamicResList { + urlsToPreload.append(contentsOf: images.map { $0.resUrl }) + } + } + ImageCacheManager.shared.preloadImages(urls: urlsToPreload) + } +} + +// MARK: - 优化的图片网格 +struct OptimizedImageGrid: View { + let images: [MomentsPicture] + + init(images: [MomentsPicture]) { + self.images = images + } + + public var body: some View { + GeometryReader { geometry in + let availableWidth = max(geometry.size.width, 1) + let spacing: CGFloat = 8 + if availableWidth < 10 { + Color.clear.frame(height: 1) + } else { + switch images.count { + case 1: + let imageSize: CGFloat = min(availableWidth * 0.6, 200) + HStack { + Spacer() + SquareImageView(image: images[0], size: imageSize) + Spacer() + } + case 2: + let imageSize: CGFloat = (availableWidth - spacing) / 2 + HStack(spacing: spacing) { + SquareImageView(image: images[0], size: imageSize) + SquareImageView(image: images[1], size: imageSize) + } + case 3: + let imageSize: CGFloat = (availableWidth - spacing * 2) / 3 + HStack(spacing: spacing) { + ForEach(images.prefix(3), id: \ .id) { image in + SquareImageView(image: image, size: imageSize) + } + } + default: + let imageSize: CGFloat = (availableWidth - spacing * 2) / 3 + let columns = Array(repeating: GridItem(.fixed(imageSize), spacing: spacing), count: 3) + LazyVGrid(columns: columns, spacing: spacing) { + ForEach(images.prefix(9), id: \ .id) { image in + SquareImageView(image: image, size: imageSize) + } + } + } + } + } + .frame(height: calculateGridHeight()) + } + + private func calculateGridHeight() -> CGFloat { + switch images.count { + case 1: + return 200 + case 2: + return 120 + case 3: + return 100 + case 4...6: + return 216 + default: + return 340 + } + } +} + +// MARK: - 正方形图片视图组件 +struct SquareImageView: View { + let image: MomentsPicture + let size: CGFloat + + init(image: MomentsPicture, size: CGFloat) { + self.image = image + self.size = size + } + + public var body: some View { + let safeSize = size.isFinite && size > 0 ? size : 100 + CachedAsyncImage(url: image.resUrl) { imageView in + imageView + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + Rectangle() + .fill(Color.gray.opacity(0.3)) + .overlay( + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white.opacity(0.6))) + .scaleEffect(0.8) + ) + } + .frame(width: safeSize, height: safeSize) + .clipped() + .cornerRadius(8) + } +} \ No newline at end of file diff --git a/yana/Views/FeedListView.swift b/yana/Views/FeedListView.swift index 711d609..6494e2b 100644 --- a/yana/Views/FeedListView.swift +++ b/yana/Views/FeedListView.swift @@ -1,5 +1,6 @@ import SwiftUI import ComposableArchitecture +//import OptimizedDynamicCardView // 导入新组件 struct FeedListView: View { let store: StoreOf @@ -46,6 +47,35 @@ struct FeedListView: View { .foregroundColor(.white.opacity(0.9)) .padding(.horizontal, 30) .padding(.bottom, 30) + // 新增:动态内容列表 + if viewStore.isLoading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .padding(.top, 20) + } else if let error = viewStore.error { + Text(error) + .font(.system(size: 14)) + .foregroundColor(.red) + .multilineTextAlignment(.center) + .padding(.horizontal, 20) + .padding(.top, 20) + } else if viewStore.moments.isEmpty { + Text(NSLocalizedString("feedList.empty", comment: "暂无动态")) + .font(.system(size: 16)) + .foregroundColor(.white.opacity(0.7)) + .padding(.top, 20) + } else { + ScrollView { + LazyVStack(spacing: 16) { + ForEach(Array(viewStore.moments.enumerated()), id: \ .element.dynamicId) { index, moment in + OptimizedDynamicCardView(moment: moment, allMoments: viewStore.moments, currentIndex: index) + } + } + .padding(.horizontal, 16) + .padding(.top, 10) + .padding(.bottom, 20) + } + } Spacer() } .frame(maxWidth: .infinity, alignment: .top) diff --git a/yana/Views/FeedView.swift b/yana/Views/FeedView.swift index 0431ce8..7c3e3e6 100644 --- a/yana/Views/FeedView.swift +++ b/yana/Views/FeedView.swift @@ -172,460 +172,460 @@ struct FeedView: View { } // MARK: - 优化的动态卡片组件 -struct OptimizedDynamicCardView: View { - let moment: MomentsInfo - let allMoments: [MomentsInfo] - let currentIndex: Int - - var body: some View { - WithPerceptionTracking{ - VStack(alignment: .leading, spacing: 12) { - // 用户信息 - HStack { - // 使用缓存的头像 - CachedAsyncImage(url: moment.avatar) { image in - image - .resizable() - .aspectRatio(contentMode: .fill) - } placeholder: { - Circle() - .fill(Color.gray.opacity(0.3)) - .overlay( - Text(String(moment.nick.prefix(1))) - .font(.system(size: 16, weight: .medium)) - .foregroundColor(.white) - ) - } - .frame(width: 40, height: 40) - .clipShape(Circle()) - - VStack(alignment: .leading, spacing: 2) { - Text(moment.nick) - .font(.system(size: 16, weight: .medium)) - .foregroundColor(.white) - - Text(formatTime(moment.publishTime)) - .font(.system(size: 12)) - .foregroundColor(.white.opacity(0.6)) - } - - Spacer() - - // VIP 标识 - if let vipInfo = moment.userVipInfoVO, let vipLevel = vipInfo.vipLevel { - Text("VIP\(vipLevel)") - .font(.system(size: 10, weight: .bold)) - .foregroundColor(.yellow) - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(Color.yellow.opacity(0.2)) - .cornerRadius(4) - } - } - - // 动态内容 - if !moment.content.isEmpty { - Text(moment.content) - .font(.system(size: 14)) - .foregroundColor(.white.opacity(0.9)) - .multilineTextAlignment(.leading) - } - - // 优化的图片网格 - if let images = moment.dynamicResList, !images.isEmpty { - OptimizedImageGrid(images: images) - } - - // 互动按钮 - HStack(spacing: 20) { - Button(action: {}) { - HStack(spacing: 4) { - Image(systemName: "message") - .font(.system(size: 16)) - Text("\(moment.commentCount)") - .font(.system(size: 14)) - } - .foregroundColor(.white.opacity(0.8)) - } - - Button(action: {}) { - HStack(spacing: 4) { - Image(systemName: moment.isLike ? "heart.fill" : "heart") - .font(.system(size: 16)) - Text("\(moment.likeCount)") - .font(.system(size: 14)) - } - .foregroundColor(moment.isLike ? .red : .white.opacity(0.8)) - } - - Spacer() - } - .padding(.top, 8) - } - } - .padding(16) - .background( - Color.white.opacity(0.1) - .cornerRadius(12) - ) - .onAppear { - // 预加载相邻的图片 - preloadNearbyImages() - } - } - - private func formatTime(_ timestamp: Int) -> String { - let date = Date(timeIntervalSince1970: TimeInterval(timestamp) / 1000.0) - let formatter = DateFormatter() - formatter.locale = Locale(identifier: "zh_CN") - - let now = Date() - let interval = now.timeIntervalSince(date) - - if interval < 60 { - return "刚刚" - } else if interval < 3600 { - return "\(Int(interval / 60))分钟前" - } else if interval < 86400 { - return "\(Int(interval / 3600))小时前" - } else { - formatter.dateFormat = "MM-dd HH:mm" - return formatter.string(from: date) - } - } - - private func preloadNearbyImages() { - var urlsToPreload: [String] = [] - - // 预加载前后2个动态的图片 - let preloadRange = max(0, currentIndex - 2)...min(allMoments.count - 1, currentIndex + 2) - - for index in preloadRange { - let moment = allMoments[index] - - // 添加头像 - urlsToPreload.append(moment.avatar) - - // 添加动态图片 - if let images = moment.dynamicResList { - urlsToPreload.append(contentsOf: images.map { $0.resUrl }) - } - } - - // 异步预加载 - ImageCacheManager.shared.preloadImages(urls: urlsToPreload) - } -} +//struct OptimizedDynamicCardView: View { +// let moment: MomentsInfo +// let allMoments: [MomentsInfo] +// let currentIndex: Int +// +// var body: some View { +// WithPerceptionTracking{ +// VStack(alignment: .leading, spacing: 12) { +// // 用户信息 +// HStack { +// // 使用缓存的头像 +// CachedAsyncImage(url: moment.avatar) { image in +// image +// .resizable() +// .aspectRatio(contentMode: .fill) +// } placeholder: { +// Circle() +// .fill(Color.gray.opacity(0.3)) +// .overlay( +// Text(String(moment.nick.prefix(1))) +// .font(.system(size: 16, weight: .medium)) +// .foregroundColor(.white) +// ) +// } +// .frame(width: 40, height: 40) +// .clipShape(Circle()) +// +// VStack(alignment: .leading, spacing: 2) { +// Text(moment.nick) +// .font(.system(size: 16, weight: .medium)) +// .foregroundColor(.white) +// +// Text(formatTime(moment.publishTime)) +// .font(.system(size: 12)) +// .foregroundColor(.white.opacity(0.6)) +// } +// +// Spacer() +// +// // VIP 标识 +// if let vipInfo = moment.userVipInfoVO, let vipLevel = vipInfo.vipLevel { +// Text("VIP\(vipLevel)") +// .font(.system(size: 10, weight: .bold)) +// .foregroundColor(.yellow) +// .padding(.horizontal, 6) +// .padding(.vertical, 2) +// .background(Color.yellow.opacity(0.2)) +// .cornerRadius(4) +// } +// } +// +// // 动态内容 +// if !moment.content.isEmpty { +// Text(moment.content) +// .font(.system(size: 14)) +// .foregroundColor(.white.opacity(0.9)) +// .multilineTextAlignment(.leading) +// } +// +// // 优化的图片网格 +// if let images = moment.dynamicResList, !images.isEmpty { +// OptimizedImageGrid(images: images) +// } +// +// // 互动按钮 +// HStack(spacing: 20) { +// Button(action: {}) { +// HStack(spacing: 4) { +// Image(systemName: "message") +// .font(.system(size: 16)) +// Text("\(moment.commentCount)") +// .font(.system(size: 14)) +// } +// .foregroundColor(.white.opacity(0.8)) +// } +// +// Button(action: {}) { +// HStack(spacing: 4) { +// Image(systemName: moment.isLike ? "heart.fill" : "heart") +// .font(.system(size: 16)) +// Text("\(moment.likeCount)") +// .font(.system(size: 14)) +// } +// .foregroundColor(moment.isLike ? .red : .white.opacity(0.8)) +// } +// +// Spacer() +// } +// .padding(.top, 8) +// } +// } +// .padding(16) +// .background( +// Color.white.opacity(0.1) +// .cornerRadius(12) +// ) +// .onAppear { +// // 预加载相邻的图片 +// preloadNearbyImages() +// } +// } +// +// private func formatTime(_ timestamp: Int) -> String { +// let date = Date(timeIntervalSince1970: TimeInterval(timestamp) / 1000.0) +// let formatter = DateFormatter() +// formatter.locale = Locale(identifier: "zh_CN") +// +// let now = Date() +// let interval = now.timeIntervalSince(date) +// +// if interval < 60 { +// return "刚刚" +// } else if interval < 3600 { +// return "\(Int(interval / 60))分钟前" +// } else if interval < 86400 { +// return "\(Int(interval / 3600))小时前" +// } else { +// formatter.dateFormat = "MM-dd HH:mm" +// return formatter.string(from: date) +// } +// } +// +// private func preloadNearbyImages() { +// var urlsToPreload: [String] = [] +// +// // 预加载前后2个动态的图片 +// let preloadRange = max(0, currentIndex - 2)...min(allMoments.count - 1, currentIndex + 2) +// +// for index in preloadRange { +// let moment = allMoments[index] +// +// // 添加头像 +// urlsToPreload.append(moment.avatar) +// +// // 添加动态图片 +// if let images = moment.dynamicResList { +// urlsToPreload.append(contentsOf: images.map { $0.resUrl }) +// } +// } +// +// // 异步预加载 +// ImageCacheManager.shared.preloadImages(urls: urlsToPreload) +// } +//} // MARK: - 优化的图片网格 -struct OptimizedImageGrid: View { - let images: [MomentsPicture] - - var body: some View { - GeometryReader { geometry in - let availableWidth = max(geometry.size.width, 1) // 防止为0或负数 - let spacing: CGFloat = 8 - - // 保护:如果availableWidth不合理,直接返回空视图 - if availableWidth < 10 { - Color.clear.frame(height: 1) - } else { - switch images.count { - case 1: - // 单张图片:大正方形居中显示 - let imageSize: CGFloat = min(availableWidth * 0.6, 200) - HStack { - Spacer() - SquareImageView(image: images[0], size: imageSize) - Spacer() - } - case 2: - // 两张图片:并排显示 - let imageSize: CGFloat = (availableWidth - spacing) / 2 - HStack(spacing: spacing) { - SquareImageView(image: images[0], size: imageSize) - SquareImageView(image: images[1], size: imageSize) - } - case 3: - // 三张图片:水平排列 - let imageSize: CGFloat = (availableWidth - spacing * 2) / 3 - HStack(spacing: spacing) { - ForEach(images.prefix(3), id: \.id) { image in - SquareImageView(image: image, size: imageSize) - } - } - default: - // 四张及以上:九宫格布局(最多9张) - let imageSize: CGFloat = (availableWidth - spacing * 2) / 3 - let columns = Array(repeating: GridItem(.fixed(imageSize), spacing: spacing), count: 3) - LazyVGrid(columns: columns, spacing: spacing) { - ForEach(images.prefix(9), id: \.id) { image in - SquareImageView(image: image, size: imageSize) - } - } - } - } - } - .frame(height: calculateGridHeight()) - } - - private func calculateGridHeight() -> CGFloat { - switch images.count { - case 1: - return 200 // 单张图片的最大高度 - case 2: - return 120 // 两张图片并排的高度 - case 3: - return 100 // 三张图片水平排列的高度 - case 4...6: - return 216 // 九宫格2行的高度 (实际图片大小 * 2 + 间距) - default: - return 340 // 九宫格3行的高度 (实际图片大小 * 3 + 间距 + 额外空间) - } - } -} +//struct OptimizedImageGrid: View { +// let images: [MomentsPicture] +// +// var body: some View { +// GeometryReader { geometry in +// let availableWidth = max(geometry.size.width, 1) // 防止为0或负数 +// let spacing: CGFloat = 8 +// +// // 保护:如果availableWidth不合理,直接返回空视图 +// if availableWidth < 10 { +// Color.clear.frame(height: 1) +// } else { +// switch images.count { +// case 1: +// // 单张图片:大正方形居中显示 +// let imageSize: CGFloat = min(availableWidth * 0.6, 200) +// HStack { +// Spacer() +// SquareImageView(image: images[0], size: imageSize) +// Spacer() +// } +// case 2: +// // 两张图片:并排显示 +// let imageSize: CGFloat = (availableWidth - spacing) / 2 +// HStack(spacing: spacing) { +// SquareImageView(image: images[0], size: imageSize) +// SquareImageView(image: images[1], size: imageSize) +// } +// case 3: +// // 三张图片:水平排列 +// let imageSize: CGFloat = (availableWidth - spacing * 2) / 3 +// HStack(spacing: spacing) { +// ForEach(images.prefix(3), id: \.id) { image in +// SquareImageView(image: image, size: imageSize) +// } +// } +// default: +// // 四张及以上:九宫格布局(最多9张) +// let imageSize: CGFloat = (availableWidth - spacing * 2) / 3 +// let columns = Array(repeating: GridItem(.fixed(imageSize), spacing: spacing), count: 3) +// LazyVGrid(columns: columns, spacing: spacing) { +// ForEach(images.prefix(9), id: \.id) { image in +// SquareImageView(image: image, size: imageSize) +// } +// } +// } +// } +// } +// .frame(height: calculateGridHeight()) +// } +// +// private func calculateGridHeight() -> CGFloat { +// switch images.count { +// case 1: +// return 200 // 单张图片的最大高度 +// case 2: +// return 120 // 两张图片并排的高度 +// case 3: +// return 100 // 三张图片水平排列的高度 +// case 4...6: +// return 216 // 九宫格2行的高度 (实际图片大小 * 2 + 间距) +// default: +// return 340 // 九宫格3行的高度 (实际图片大小 * 3 + 间距 + 额外空间) +// } +// } +//} // MARK: - 正方形图片视图组件 -struct SquareImageView: View { - let image: MomentsPicture - let size: CGFloat - - var body: some View { - let safeSize = size.isFinite && size > 0 ? size : 100 // 防止非有限或负数 - CachedAsyncImage(url: image.resUrl) { imageView in - imageView - .resizable() - .aspectRatio(contentMode: .fill) - } placeholder: { - Rectangle() - .fill(Color.gray.opacity(0.3)) - .overlay( - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white.opacity(0.6))) - .scaleEffect(0.8) - ) - } - .frame(width: safeSize, height: safeSize) - .clipped() - .cornerRadius(8) - } -} +//struct SquareImageView: View { +// let image: MomentsPicture +// let size: CGFloat +// +// var body: some View { +// let safeSize = size.isFinite && size > 0 ? size : 100 // 防止非有限或负数 +// CachedAsyncImage(url: image.resUrl) { imageView in +// imageView +// .resizable() +// .aspectRatio(contentMode: .fill) +// } placeholder: { +// Rectangle() +// .fill(Color.gray.opacity(0.3)) +// .overlay( +// ProgressView() +// .progressViewStyle(CircularProgressViewStyle(tint: .white.opacity(0.6))) +// .scaleEffect(0.8) +// ) +// } +// .frame(width: safeSize, height: safeSize) +// .clipped() +// .cornerRadius(8) +// } +//} // MARK: - 旧的真实动态卡片组件(保留备用) -struct RealDynamicCardView: View { - let moment: MomentsInfo - - var body: some View { - VStack(alignment: .leading, spacing: 12) { - // 用户信息 - HStack { - AsyncImage(url: URL(string: moment.avatar)) { image in - image - .resizable() - .aspectRatio(contentMode: .fill) - } placeholder: { - Circle() - .fill(Color.gray.opacity(0.3)) - .overlay( - Text(String(moment.nick.prefix(1))) - .font(.system(size: 16, weight: .medium)) - .foregroundColor(.white) - ) - } - .frame(width: 40, height: 40) - .clipShape(Circle()) - - VStack(alignment: .leading, spacing: 2) { - Text(moment.nick) - .font(.system(size: 16, weight: .medium)) - .foregroundColor(.white) - - Text(formatTime(moment.publishTime)) - .font(.system(size: 12)) - .foregroundColor(.white.opacity(0.6)) - } - - Spacer() - - // VIP 标识 - if let vipInfo = moment.userVipInfoVO, let vipLevel = vipInfo.vipLevel { - Text("VIP\(vipLevel)") - .font(.system(size: 10, weight: .bold)) - .foregroundColor(.yellow) - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(Color.yellow.opacity(0.2)) - .cornerRadius(4) - } - } - - // 动态内容 - if !moment.content.isEmpty { - Text(moment.content) - .font(.system(size: 14)) - .foregroundColor(.white.opacity(0.9)) - .multilineTextAlignment(.leading) - } - - // 图片网格 - if let images = moment.dynamicResList, !images.isEmpty { - LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: min(images.count, 3)), spacing: 8) { - ForEach(images.prefix(9), id: \.id) { image in - AsyncImage(url: URL(string: image.resUrl)) { imageView in - imageView - .resizable() - .aspectRatio(contentMode: .fill) - } placeholder: { - Rectangle() - .fill(Color.gray.opacity(0.3)) - .overlay( - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white.opacity(0.6))) - ) - } - .frame(height: 100) - .clipped() - .cornerRadius(8) - } - } - } - - // 互动按钮 - HStack(spacing: 20) { - Button(action: {}) { - HStack(spacing: 4) { - Image(systemName: "message") - .font(.system(size: 16)) - Text("\(moment.commentCount)") - .font(.system(size: 14)) - } - .foregroundColor(.white.opacity(0.8)) - } - - Button(action: {}) { - HStack(spacing: 4) { - Image(systemName: moment.isLike ? "heart.fill" : "heart") - .font(.system(size: 16)) - Text("\(moment.likeCount)") - .font(.system(size: 14)) - } - .foregroundColor(moment.isLike ? .red : .white.opacity(0.8)) - } - - Spacer() - } - .padding(.top, 8) - } - .padding(16) - .background( - Color.white.opacity(0.1) - .cornerRadius(12) - ) - } - - private func formatTime(_ timestamp: Int) -> String { - let date = Date(timeIntervalSince1970: TimeInterval(timestamp) / 1000.0) - let formatter = DateFormatter() - formatter.locale = Locale(identifier: "zh_CN") - - let now = Date() - let interval = now.timeIntervalSince(date) - - if interval < 60 { - return "刚刚" - } else if interval < 3600 { - return "\(Int(interval / 60))分钟前" - } else if interval < 86400 { - return "\(Int(interval / 3600))小时前" - } else { - formatter.dateFormat = "MM-dd HH:mm" - return formatter.string(from: date) - } - } -} +//struct RealDynamicCardView: View { +// let moment: MomentsInfo +// +// var body: some View { +// VStack(alignment: .leading, spacing: 12) { +// // 用户信息 +// HStack { +// AsyncImage(url: URL(string: moment.avatar)) { image in +// image +// .resizable() +// .aspectRatio(contentMode: .fill) +// } placeholder: { +// Circle() +// .fill(Color.gray.opacity(0.3)) +// .overlay( +// Text(String(moment.nick.prefix(1))) +// .font(.system(size: 16, weight: .medium)) +// .foregroundColor(.white) +// ) +// } +// .frame(width: 40, height: 40) +// .clipShape(Circle()) +// +// VStack(alignment: .leading, spacing: 2) { +// Text(moment.nick) +// .font(.system(size: 16, weight: .medium)) +// .foregroundColor(.white) +// +// Text(formatTime(moment.publishTime)) +// .font(.system(size: 12)) +// .foregroundColor(.white.opacity(0.6)) +// } +// +// Spacer() +// +// // VIP 标识 +// if let vipInfo = moment.userVipInfoVO, let vipLevel = vipInfo.vipLevel { +// Text("VIP\(vipLevel)") +// .font(.system(size: 10, weight: .bold)) +// .foregroundColor(.yellow) +// .padding(.horizontal, 6) +// .padding(.vertical, 2) +// .background(Color.yellow.opacity(0.2)) +// .cornerRadius(4) +// } +// } +// +// // 动态内容 +// if !moment.content.isEmpty { +// Text(moment.content) +// .font(.system(size: 14)) +// .foregroundColor(.white.opacity(0.9)) +// .multilineTextAlignment(.leading) +// } +// +// // 图片网格 +// if let images = moment.dynamicResList, !images.isEmpty { +// LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: min(images.count, 3)), spacing: 8) { +// ForEach(images.prefix(9), id: \.id) { image in +// AsyncImage(url: URL(string: image.resUrl)) { imageView in +// imageView +// .resizable() +// .aspectRatio(contentMode: .fill) +// } placeholder: { +// Rectangle() +// .fill(Color.gray.opacity(0.3)) +// .overlay( +// ProgressView() +// .progressViewStyle(CircularProgressViewStyle(tint: .white.opacity(0.6))) +// ) +// } +// .frame(height: 100) +// .clipped() +// .cornerRadius(8) +// } +// } +// } +// +// // 互动按钮 +// HStack(spacing: 20) { +// Button(action: {}) { +// HStack(spacing: 4) { +// Image(systemName: "message") +// .font(.system(size: 16)) +// Text("\(moment.commentCount)") +// .font(.system(size: 14)) +// } +// .foregroundColor(.white.opacity(0.8)) +// } +// +// Button(action: {}) { +// HStack(spacing: 4) { +// Image(systemName: moment.isLike ? "heart.fill" : "heart") +// .font(.system(size: 16)) +// Text("\(moment.likeCount)") +// .font(.system(size: 14)) +// } +// .foregroundColor(moment.isLike ? .red : .white.opacity(0.8)) +// } +// +// Spacer() +// } +// .padding(.top, 8) +// } +// .padding(16) +// .background( +// Color.white.opacity(0.1) +// .cornerRadius(12) +// ) +// } +// +// private func formatTime(_ timestamp: Int) -> String { +// let date = Date(timeIntervalSince1970: TimeInterval(timestamp) / 1000.0) +// let formatter = DateFormatter() +// formatter.locale = Locale(identifier: "zh_CN") +// +// let now = Date() +// let interval = now.timeIntervalSince(date) +// +// if interval < 60 { +// return "刚刚" +// } else if interval < 3600 { +// return "\(Int(interval / 60))分钟前" +// } else if interval < 86400 { +// return "\(Int(interval / 3600))小时前" +// } else { +// formatter.dateFormat = "MM-dd HH:mm" +// return formatter.string(from: date) +// } +// } +//} // MARK: - 旧的模拟卡片组件(保留备用) -struct DynamicCardView: View { - let index: Int - - var body: some View { - VStack(alignment: .leading, spacing: 12) { - // 用户信息 - HStack { - Circle() - .fill(Color.gray.opacity(0.3)) - .frame(width: 40, height: 40) - .overlay( - Text("U\(index + 1)") - .font(.system(size: 16, weight: .medium)) - .foregroundColor(.white) - ) - - VStack(alignment: .leading, spacing: 2) { - Text("用户\(index + 1)") - .font(.system(size: 16, weight: .medium)) - .foregroundColor(.white) - - Text("2小时前") - .font(.system(size: 12)) - .foregroundColor(.white.opacity(0.6)) - } - - Spacer() - } - - // 动态内容 - Text("今天是美好的一天,分享一些生活中的小确幸。希望大家都能珍惜每一个当下的时刻。") - .font(.system(size: 14)) - .foregroundColor(.white.opacity(0.9)) - .multilineTextAlignment(.leading) - - // 图片网格 - LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 3), spacing: 8) { - ForEach(0..<3) { imageIndex in - Rectangle() - .fill(Color.gray.opacity(0.3)) - .aspectRatio(1, contentMode: .fit) - .overlay( - Image(systemName: "photo") - .foregroundColor(.white.opacity(0.6)) - ) - } - } - - // 互动按钮 - HStack(spacing: 20) { - Button(action: {}) { - HStack(spacing: 4) { - Image(systemName: "message") - .font(.system(size: 16)) - Text("354") - .font(.system(size: 14)) - } - .foregroundColor(.white.opacity(0.8)) - } - - Button(action: {}) { - HStack(spacing: 4) { - Image(systemName: "heart") - .font(.system(size: 16)) - Text("354") - .font(.system(size: 14)) - } - .foregroundColor(.white.opacity(0.8)) - } - - Spacer() - } - .padding(.top, 8) - } - .padding(16) - .background( - Color.white.opacity(0.1) - .cornerRadius(12) - ) - } -} +//struct DynamicCardView: View { +// let index: Int +// +// var body: some View { +// VStack(alignment: .leading, spacing: 12) { +// // 用户信息 +// HStack { +// Circle() +// .fill(Color.gray.opacity(0.3)) +// .frame(width: 40, height: 40) +// .overlay( +// Text("U\(index + 1)") +// .font(.system(size: 16, weight: .medium)) +// .foregroundColor(.white) +// ) +// +// VStack(alignment: .leading, spacing: 2) { +// Text("用户\(index + 1)") +// .font(.system(size: 16, weight: .medium)) +// .foregroundColor(.white) +// +// Text("2小时前") +// .font(.system(size: 12)) +// .foregroundColor(.white.opacity(0.6)) +// } +// +// Spacer() +// } +// +// // 动态内容 +// Text("今天是美好的一天,分享一些生活中的小确幸。希望大家都能珍惜每一个当下的时刻。") +// .font(.system(size: 14)) +// .foregroundColor(.white.opacity(0.9)) +// .multilineTextAlignment(.leading) +// +// // 图片网格 +// LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 3), spacing: 8) { +// ForEach(0..<3) { imageIndex in +// Rectangle() +// .fill(Color.gray.opacity(0.3)) +// .aspectRatio(1, contentMode: .fit) +// .overlay( +// Image(systemName: "photo") +// .foregroundColor(.white.opacity(0.6)) +// ) +// } +// } +// +// // 互动按钮 +// HStack(spacing: 20) { +// Button(action: {}) { +// HStack(spacing: 4) { +// Image(systemName: "message") +// .font(.system(size: 16)) +// Text("354") +// .font(.system(size: 14)) +// } +// .foregroundColor(.white.opacity(0.8)) +// } +// +// Button(action: {}) { +// HStack(spacing: 4) { +// Image(systemName: "heart") +// .font(.system(size: 16)) +// Text("354") +// .font(.system(size: 14)) +// } +// .foregroundColor(.white.opacity(0.8)) +// } +// +// Spacer() +// } +// .padding(.top, 8) +// } +// .padding(16) +// .background( +// Color.white.opacity(0.1) +// .cornerRadius(12) +// ) +// } +//} //#Preview { // FeedView( diff --git a/yana/Views/MainView.swift b/yana/Views/MainView.swift index da54a53..aabf791 100644 --- a/yana/Views/MainView.swift +++ b/yana/Views/MainView.swift @@ -27,6 +27,7 @@ struct MainView: View { )) .transition(.opacity) case .other: + MeView(onLogout: {}) // 这里可根据需要传递实际登出回调 .transition(.opacity) } diff --git a/yana/Views/MeView.swift b/yana/Views/MeView.swift index b5bc99c..41cfdad 100644 --- a/yana/Views/MeView.swift +++ b/yana/Views/MeView.swift @@ -1,8 +1,10 @@ import SwiftUI +import ComposableArchitecture struct MeView: View { @State private var showLogoutConfirmation = false let onLogout: () -> Void // 新增:登出回调 + @State private var showSetting = false // 新增:控制SettingView弹出 var body: some View { GeometryReader { geometry in @@ -15,6 +17,12 @@ struct MeView: View { .font(.system(size: 22, weight: .semibold)) .foregroundColor(.white) Spacer() + Button(action: { showSetting = true }) { + Image(systemName: "gearshape") + .font(.system(size: 22, weight: .regular)) + .foregroundColor(.white) + } + .padding(.trailing, 8) } .padding(.top, geometry.safeAreaInsets.top + 20) @@ -74,6 +82,7 @@ struct MeView: View { // 底部安全区域 - 为底部导航栏和安全区域留出空间 Color.clear.frame(height: geometry.safeAreaInsets.bottom + 100) } + .padding(.top, 100) } } .ignoresSafeArea(.container, edges: .top) @@ -85,6 +94,10 @@ struct MeView: View { } message: { Text("确定要退出登录吗?") } + .sheet(isPresented: $showSetting) { + // 这里用预设store,实际项目可替换为真实store + SettingView(store: Store(initialState: SettingFeature.State()) { SettingFeature() }) + } } // MARK: - 退出登录方法 @@ -132,6 +145,6 @@ struct MenuItemView: View { } } -#Preview { - MeView(onLogout: {}) -} +//#Preview { +// MeView(onLogout: {}) +//} diff --git a/yana/Views/SettingView.swift b/yana/Views/SettingView.swift index 4ab447f..c5f4cb2 100644 --- a/yana/Views/SettingView.swift +++ b/yana/Views/SettingView.swift @@ -6,73 +6,75 @@ struct SettingView: View { @ObservedObject private var localizationManager = LocalizationManager.shared var body: some View { - WithPerceptionTracking { - GeometryReader { geometry in - ZStack { - // 背景图片 - 使用"bg"图片,全屏显示 - Image("bg") - .resizable() - .aspectRatio(contentMode: .fill) - .ignoresSafeArea(.all) - - VStack(spacing: 0) { - // Navigation Bar - HStack { - // 返回按钮 - Button(action: { - store.send(.dismissTapped) - }) { - Image(systemName: "chevron.left") - .font(.title2) - .foregroundColor(.white) - } - .padding(.leading, 16) - - Spacer() - - // 标题 - Text(NSLocalizedString("setting.title", comment: "Settings")) - .font(.custom("PingFang SC-Semibold", size: 16)) - .foregroundColor(.white) - - Spacer() - - // 占位符,保持标题居中 - Color.clear - .frame(width: 44, height: 44) - } - .padding(.top, 8) - .padding(.horizontal) + NavigationStack { + WithPerceptionTracking { + GeometryReader { geometry in + ZStack { + // 背景图片 - 使用"bg"图片,全屏显示 + Image("bg") + .resizable() + .aspectRatio(contentMode: .fill) + .ignoresSafeArea(.all) - // 内容区域 - ScrollView { - VStack(spacing: 24) { - UserInfoCardView(userInfo: store.userInfo, accountModel: store.accountModel) -// .padding() - .padding(.top, 32) - - SettingOptionsView( - onLanguageTapped: { - // TODO: 实现语言设置 - }, - onAboutTapped: { - // TODO: 实现关于页面 - } - ) - - Spacer(minLength: 50) - - LogoutButtonView { - store.send(.logoutTapped) + VStack(spacing: 0) { + // Navigation Bar + HStack { + // 返回按钮 + Button(action: { + store.send(.dismissTapped) + }) { + Image(systemName: "chevron.left") + .font(.title2) + .foregroundColor(.white) + } + .padding(.leading, 16) + + Spacer() + + // 标题 + Text(NSLocalizedString("setting.title", comment: "Settings")) + .font(.custom("PingFang SC-Semibold", size: 16)) + .foregroundColor(.white) + + Spacer() + + // 占位符,保持标题居中 + Color.clear + .frame(width: 44, height: 44) + } + .padding(.top, 8) + .padding(.horizontal) + + // 内容区域 + ScrollView { + VStack(spacing: 24) { + UserInfoCardView(userInfo: store.userInfo, accountModel: store.accountModel) +// .padding() + .padding(.top, 32) + + SettingOptionsView( + onLanguageTapped: { + // TODO: 实现语言设置 + }, + onAboutTapped: { + // TODO: 实现关于页面 + } + ) + + Spacer(minLength: 50) + + LogoutButtonView { + store.send(.logoutTapped) + } + .padding(.bottom, 50) } - .padding(.bottom, 50) } } } } - } - .onAppear { - store.send(.onAppear) + .onAppear { + store.send(.onAppear) + } } } } @@ -123,6 +125,61 @@ struct UserInfoCardView: View { } // MARK: - Setting Options View +// Add this new view for testing COS upload +struct TestCOSUploadView: View { + @State private var imageURL: String = "https://img.toto.im/mw600/66b3de17ly1i3mpcw0k7yj20hs0md0tf.jpg.webp" + @State private var uploadResult: String = "" + @State private var isUploading: Bool = false + @Dependency(\.apiService) private var apiService + + var body: some View { + VStack(spacing: 20) { + TextField("Enter image URL", text: $imageURL) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .padding() + + Button(action: { + Task { + await uploadImageFromURL() + } + }) { + Text(isUploading ? "Uploading..." : "Upload to COS") + .padding() + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(8) + } + .disabled(isUploading || imageURL.isEmpty) + + Text(uploadResult) + .multilineTextAlignment(.center) + .padding() + } + .navigationTitle("Test COS Upload") + } + + private func uploadImageFromURL() async { + guard let url = URL(string: imageURL) else { + uploadResult = "Invalid URL" + return + } + isUploading = true + uploadResult = "" + do { + let (data, _) = try await URLSession.shared.data(from: url) + if let cloudURL = await COSManager.shared.uploadImage(data, apiService: apiService) { + uploadResult = "Upload successful! Cloud URL: \(cloudURL)" + } else { + uploadResult = "Upload failed" + } + } catch { + uploadResult = "Download failed: \(error.localizedDescription)" + } + isUploading = false + } +} + +// Modify SettingOptionsView to add the test row struct SettingOptionsView: View { let onLanguageTapped: () -> Void let onAboutTapped: () -> Void @@ -141,6 +198,29 @@ struct SettingOptionsView: View { action: onAboutTapped ) + #if DEBUG + NavigationLink(destination: TestCOSUploadView()) { + HStack { + Image(systemName: "cloud.upload") + .foregroundColor(.white.opacity(0.8)) + .frame(width: 24) + + Text("Test COS Upload") + .foregroundColor(.white) + + Spacer() + + Image(systemName: "chevron.right") + .foregroundColor(.white.opacity(0.6)) + .font(.caption) + } + .padding(.horizontal, 20) + .padding(.vertical, 16) + .background(Color.black.opacity(0.2)) + .cornerRadius(12) + } + #endif + HStack { Image(systemName: "app.badge") .foregroundColor(.white.opacity(0.8)) @@ -223,4 +303,4 @@ struct SettingRowView: View { // SettingFeature() // } // ) -//} +//}