feat: 添加COSManagerAdapter以支持新的TCCos组件
- 新增COSManagerAdapter类,保持与现有COSManager相同的接口,内部使用新的TCCos组件。 - 实现获取、刷新Token及上传图片的功能,确保与腾讯云COS的兼容性。 - 在CreateFeedView中重构内容输入、图片选择和发布按钮逻辑,提升用户体验。 - 更新EditFeedView以优化视图结构和状态管理,确保功能正常运行。 - 在多个视图中添加键盘状态管理,改善用户交互体验。
This commit is contained in:
@@ -255,4 +255,4 @@ struct EditFeedFeature {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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<AnyObject>()
|
||||
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
|
||||
}
|
||||
}
|
8
yana/Utils/TCCos/COSManagerAdapter.swift
Normal file
8
yana/Utils/TCCos/COSManagerAdapter.swift
Normal file
@@ -0,0 +1,8 @@
|
||||
//
|
||||
// COSManagerAdapter.swift
|
||||
// yana
|
||||
//
|
||||
// Created by P on 2025/7/31.
|
||||
//
|
||||
|
||||
import Foundation
|
@@ -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 {
|
||||
|
@@ -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 {
|
||||
|
Reference in New Issue
Block a user