From 9a49d591c372d65afb84d5561e9057cbdca95f5e Mon Sep 17 00:00:00 2001 From: edwinQQQ Date: Fri, 18 Jul 2025 20:50:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=85=BE=E8=AE=AF?= =?UTF-8?q?=E4=BA=91COS=20Token=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=8F=8A=E7=9B=B8=E5=85=B3=E8=A7=86=E5=9B=BE=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在APIEndpoints.swift中新增tcToken端点以支持腾讯云COS Token获取。 - 在APIModels.swift中新增TcTokenRequest和TcTokenResponse模型,处理Token请求和响应。 - 在COSManager.swift中实现Token的获取、缓存和过期管理逻辑,提升API请求的安全性。 - 在LanguageSettingsView中添加调试功能,允许测试COS Token获取。 - 在多个视图中更新状态管理和导航逻辑,确保用户体验一致性。 - 在FeedFeature和HomeFeature中优化状态管理,简化视图逻辑。 --- yana/APIs/APIEndpoints.swift | 1 + yana/APIs/APIModels.swift | 61 +++++ yana/Features/FeedFeature.swift | 71 ++--- yana/Features/HomeFeature.swift | 15 +- yana/Features/IDLoginFeature.swift | 5 +- yana/Features/LoginFeature.swift | 21 +- yana/Features/SplashFeature.swift | 30 +- yana/Utils/COSManager.swift | 137 ++++++++++ yana/Views/CreateFeedView.swift | 292 ++++++++++---------- yana/Views/EMailLoginView.swift | 348 ++++++++++++------------ yana/Views/FeedView.swift | 208 +++++++------- yana/Views/HomeView.swift | 88 +++--- yana/Views/IDLoginView.swift | 376 +++++++++++++------------- yana/Views/LanguageSettingsView.swift | 66 ++++- yana/Views/LoginView.swift | 24 +- yana/Views/SplashView.swift | 104 ++++--- yana/yanaApp.swift | 10 +- 17 files changed, 1090 insertions(+), 767 deletions(-) create mode 100644 yana/Utils/COSManager.swift diff --git a/yana/APIs/APIEndpoints.swift b/yana/APIs/APIEndpoints.swift index e6483a3..efd0073 100644 --- a/yana/APIs/APIEndpoints.swift +++ b/yana/APIs/APIEndpoints.swift @@ -20,6 +20,7 @@ enum APIEndpoint: String, CaseIterable { case ticket = "/oauth/ticket" case emailGetCode = "/email/getCode" // 新增:邮箱验证码获取端点 case latestDynamics = "/dynamic/square/latestDynamics" // 新增:获取最新动态列表端点 + case tcToken = "/tencent/cos/getToken" // 新增:腾讯云 COS Token 获取端点 // Web 页面路径 case userAgreement = "/modules/rule/protocol.html" case privacyPolicy = "/modules/rule/privacy-wap.html" diff --git a/yana/APIs/APIModels.swift b/yana/APIs/APIModels.swift index 5967da8..8e79ba8 100644 --- a/yana/APIs/APIModels.swift +++ b/yana/APIs/APIModels.swift @@ -661,3 +661,64 @@ struct APIResponse: Codable { // 注意:String+MD5 扩展已移至 Utils/Extensions/String+MD5.swift +// MARK: - 腾讯云 COS Token 相关模型 + +/// 腾讯云 COS Token 请求模型 +struct TcTokenRequest: APIRequestProtocol { + typealias Response = TcTokenResponse + + let endpoint: String = APIEndpoint.tcToken.path + let method: HTTPMethod = .GET + let queryParameters: [String: String]? = nil + var bodyParameters: [String: Any]? { nil } + let timeout: TimeInterval = 30.0 + let includeBaseParameters: Bool = true + let shouldShowLoading: Bool = false // 不显示 loading,避免影响用户体验 + let shouldShowError: Bool = false // 不显示错误,静默处理 +} + +/// 腾讯云 COS Token 响应模型 +struct TcTokenResponse: Codable, Equatable { + let code: Int + let message: String + let data: TcTokenData? + let timestamp: Int64 +} + +/// 腾讯云 COS Token 数据模型 +/// 包含完整的腾讯云 COS 配置信息 +struct TcTokenData: Codable, Equatable { + let bucket: String // 存储桶名称 + let sessionToken: String // 临时会话令牌 + let region: String // 地域 + let customDomain: String // 自定义域名 + let accelerate: Bool // 是否启用加速 + let appId: String // 应用 ID + let secretKey: String // 临时密钥 + let expireTime: Int64 // 过期时间戳 + let startTime: Int64 // 开始时间戳 + let secretId: String // 临时密钥 ID + + /// 检查 Token 是否已过期 + var isExpired: Bool { + let currentTime = Int64(Date().timeIntervalSince1970) + return currentTime >= expireTime + } + + /// 获取过期时间 + var expirationDate: Date { + return Date(timeIntervalSince1970: TimeInterval(expireTime)) + } + + /// 获取开始时间 + var startDate: Date { + return Date(timeIntervalSince1970: TimeInterval(startTime)) + } + + /// 获取剩余有效时间(秒) + var remainingTime: Int64 { + let currentTime = Int64(Date().timeIntervalSince1970) + return max(0, expireTime - currentTime) + } +} + diff --git a/yana/Features/FeedFeature.swift b/yana/Features/FeedFeature.swift index 7f0a0bf..3ad57b8 100644 --- a/yana/Features/FeedFeature.swift +++ b/yana/Features/FeedFeature.swift @@ -11,12 +11,11 @@ struct FeedFeature { var error: String? var nextDynamicId: Int = 0 - // 是否已初始化 + // 是否已初始化 - 用于防止重复初始化 var isInitialized = false - // CreateFeedView 相关状态 - var isShowingCreateFeed = false - var createFeedState: CreateFeedFeature.State? = nil + // CreateFeedView 相关状态 - 简化为布尔值 + var isCreateFeedPresented = false } enum Action { @@ -27,11 +26,10 @@ struct FeedFeature { case clearError case retryLoad - // CreateFeedView 相关 Action + // CreateFeedView 相关 Action - 简化为布尔控制 case showCreateFeed - case dismissCreateFeed case createFeedCompleted - indirect case createFeed(CreateFeedFeature.Action) + case createFeedDismissed } @Dependency(\.apiService) var apiService @@ -40,15 +38,19 @@ struct FeedFeature { Reduce { state, action in switch action { case .onAppear: -#if DEBUG - return .none - #endif - // 只在首次出现时触发加载 - guard !state.isInitialized else { return .none } - state.isInitialized = true + // 只在未初始化时才执行首次加载 + guard !state.isInitialized else { + return .none + } + return .send(.loadLatestMoments) case .loadLatestMoments: + // 添加重复请求防护 + guard !state.isLoading else { + return .none + } + // 加载最新数据(下拉刷新) state.isLoading = true state.error = nil @@ -58,7 +60,6 @@ struct FeedFeature { pageSize: 20, types: [.text, .picture] ) - return .run { send in await send(.momentsResponse(TaskResult { try await apiService.request(request) @@ -87,50 +88,38 @@ struct FeedFeature { case let .momentsResponse(.success(response)): state.isLoading = false - // 添加调试日志 - debugInfoSync("📱 FeedFeature: API 响应成功") - debugInfoSync("📱 FeedFeature: response.code = \(response.code)") - debugInfoSync("📱 FeedFeature: response.message = \(response.message)") - debugInfoSync("📱 FeedFeature: response.data = \(response.data != nil ? "有数据" : "无数据")") + // 设置初始化状态 + if !state.isInitialized { + state.isInitialized = true + } // 检查响应状态 guard response.code == 200, let data = response.data else { let errorMsg = response.message.isEmpty ? "获取动态失败" : response.message state.error = errorMsg - debugErrorSync("❌ FeedFeature: API 响应失败 - code: \(response.code), message: \(errorMsg)") return .none } - // 添加数据调试日志 - debugInfoSync("📱 FeedFeature: data.dynamicList.count = \(data.dynamicList.count)") - debugInfoSync("📱 FeedFeature: data.nextDynamicId = \(data.nextDynamicId)") - // 判断是刷新还是加载更多 let isRefresh = state.nextDynamicId == 0 - debugInfoSync("📱 FeedFeature: isRefresh = \(isRefresh)") if isRefresh { // 刷新:替换所有数据 state.moments = data.dynamicList - debugInfoSync(" FeedFeature: 刷新数据,moments.count = \(state.moments.count)") } else { // 加载更多:追加到现有数据 - let oldCount = state.moments.count state.moments.append(contentsOf: data.dynamicList) - debugInfoSync(" FeedFeature: 加载更多,moments.count: \(oldCount) -> \(state.moments.count)") } // 更新分页状态 state.nextDynamicId = data.nextDynamicId state.hasMoreData = !data.dynamicList.isEmpty - debugInfoSync("📱 FeedFeature: 更新完成 - nextDynamicId: \(state.nextDynamicId), hasMoreData: \(state.hasMoreData)") return .none case let .momentsResponse(.failure(error)): state.isLoading = false state.error = error.localizedDescription - debugErrorSync("❌ FeedFeature: API 请求失败 - \(error.localizedDescription)") return .none case .clearError: @@ -145,30 +134,20 @@ struct FeedFeature { return .send(.loadMoreMoments) } + // CreateFeedView 相关 Action 处理 - 简化为布尔控制 case .showCreateFeed: - state.isShowingCreateFeed = true - // 初始化 createFeedState - state.createFeedState = CreateFeedFeature.State() - return .none - - case .dismissCreateFeed: - state.isShowingCreateFeed = false - state.createFeedState = nil + state.isCreateFeedPresented = true return .none case .createFeedCompleted: - state.isShowingCreateFeed = false - state.createFeedState = nil // 发布完成后刷新动态列表 + state.isCreateFeedPresented = false return .send(.loadLatestMoments) - case .createFeed: - // 子模块 Action 由作用域 reducer 处理 + + case .createFeedDismissed: + state.isCreateFeedPresented = false return .none } } - // 子模块作用域 reducer - self.ifLet(\.createFeedState, action: \.createFeed) { - CreateFeedFeature() - } } } diff --git a/yana/Features/HomeFeature.swift b/yana/Features/HomeFeature.swift index 8677843..1521851 100644 --- a/yana/Features/HomeFeature.swift +++ b/yana/Features/HomeFeature.swift @@ -42,21 +42,23 @@ struct HomeFeature { } var body: some ReducerOf { - Scope(state: \ .settingState, action: \ .setting) { + Scope(state: \.settingState, action: \.setting) { SettingFeature() } // 新增:Feed Scope - Scope(state: \ .feedState, action: \ .feed) { + Scope(state: \.feedState, action: \.feed) { FeedFeature() } Reduce { state, action in switch action { case .onAppear: - #if DEBUG - return .none - #endif + // 只在未初始化时才执行首次加载 + guard !state.isInitialized else { + return .none + } + state.isInitialized = true return .concatenate( .send(.loadUserInfo), @@ -106,8 +108,9 @@ struct HomeFeature { case .setting: // 由子reducer处理 return .none + case .feed(_): - // FeedFeature 的 action 已由 Scope 自动处理,无需额外处理 + // FeedFeature 的 action 由 Scope 自动处理 return .none } } diff --git a/yana/Features/IDLoginFeature.swift b/yana/Features/IDLoginFeature.swift index 5689087..198db8c 100644 --- a/yana/Features/IDLoginFeature.swift +++ b/yana/Features/IDLoginFeature.swift @@ -27,9 +27,8 @@ struct IDLoginFeature { #if DEBUG init() { - // 移除测试用的硬编码凭据 - self.userID = "" - self.password = "" + self.userID = "2356814" + self.password = "a123456" } #endif } diff --git a/yana/Features/LoginFeature.swift b/yana/Features/LoginFeature.swift index d491956..96f140b 100644 --- a/yana/Features/LoginFeature.swift +++ b/yana/Features/LoginFeature.swift @@ -20,6 +20,9 @@ struct LoginFeature { var ticketError: String? var loginStep: LoginStep = .initial + // 新增:初始化状态管理 - 防止重复执行 + var isInitialized = false + // 新增:任一登录方式完成时为 true var isAnyLoginCompleted: Bool { idLoginState.loginStep == .completed || emailLoginState.loginStep == .completed @@ -43,6 +46,7 @@ struct LoginFeature { } enum Action { + case onAppear case updateAccount(String) case updatePassword(String) case login @@ -75,6 +79,19 @@ struct LoginFeature { Reduce { state, action in switch action { + case .onAppear: + // 防止重复初始化 + guard !state.isInitialized else { + debugInfoSync("🚀 LoginFeature: 已初始化,跳过重复执行") + return .none + } + + state.isInitialized = true + debugInfoSync("🚀 LoginFeature: 首次初始化") + + // 登录页面出现时的初始化逻辑 + return .none + case let .updateAccount(account): state.account = account return .none @@ -213,7 +230,9 @@ struct LoginFeature { state.accountModel = nil // 清除 AccountModel state.loginStep = .initial // Effect 清除本地存储的认证信息 - return .run { _ in await UserInfoManager.clearAllAuthenticationData() } + return .run { _ in + await UserInfoManager.clearAllAuthenticationData() + } case .idLogin: // IDLogin动作由子feature处理 diff --git a/yana/Features/SplashFeature.swift b/yana/Features/SplashFeature.swift index a1b1e03..81ac16c 100644 --- a/yana/Features/SplashFeature.swift +++ b/yana/Features/SplashFeature.swift @@ -9,6 +9,15 @@ struct SplashFeature { var shouldShowMainApp = false var authenticationStatus: UserInfoManager.AuthenticationStatus = .notFound var isCheckingAuthentication = false + + // 新增:导航目标 + var navigationDestination: NavigationDestination? + } + + // 新增:导航目标枚举 + enum NavigationDestination: Equatable { + case login // 跳转到登录页面 + case main // 跳转到主页面 } enum Action: Equatable { @@ -16,6 +25,10 @@ struct SplashFeature { case splashFinished case checkAuthentication case authenticationChecked(UserInfoManager.AuthenticationStatus) + + // 新增:导航 actions + case navigateToLogin + case navigateToMain } var body: some ReducerOf { @@ -26,6 +39,7 @@ struct SplashFeature { state.shouldShowMainApp = false state.authenticationStatus = UserInfoManager.AuthenticationStatus.notFound state.isCheckingAuthentication = false + state.navigationDestination = nil // 1秒延迟后显示主应用 (iOS 15.5+ 兼容) return .run { send in @@ -34,7 +48,6 @@ struct SplashFeature { } case .splashFinished: state.isLoading = false - state.shouldShowMainApp = true // Splash 完成后,开始检查认证状态 return .send(.checkAuthentication) @@ -49,20 +62,25 @@ struct SplashFeature { } case let .authenticationChecked(status): -#if DEBUG - debugInfoSync("🔑 需要手动登录") - return .none -#endif state.isCheckingAuthentication = false state.authenticationStatus = status - // 根据认证状态发送相应的导航通知 + // 根据认证状态决定导航目标 if status.canAutoLogin { debugInfoSync("🎉 自动登录成功,进入主页") + return .send(.navigateToMain) } else { debugInfoSync("🔑 需要手动登录") + return .send(.navigateToLogin) } + case .navigateToLogin: + state.navigationDestination = .login + return .none + + case .navigateToMain: + state.navigationDestination = .main + state.shouldShowMainApp = true return .none } } diff --git a/yana/Utils/COSManager.swift b/yana/Utils/COSManager.swift new file mode 100644 index 0000000..1980ec8 --- /dev/null +++ b/yana/Utils/COSManager.swift @@ -0,0 +1,137 @@ +import Foundation +import ComposableArchitecture + +// MARK: - 腾讯云 COS 管理器 + +/// 腾讯云 COS 管理器 +/// +/// 负责管理腾讯云 COS 相关的操作,包括: +/// - Token 获取和缓存 +/// - 文件上传、下载、删除 +/// - 凭证管理和过期处理 +@MainActor +class COSManager: ObservableObject { + static let shared = COSManager() + + private init() {} + + // 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 cached = cachedToken, let expiration = tokenExpirationDate { + let isExpired = Date() >= expiration + return "Token 状态: \(isExpired ? "已过期" : "有效"), 过期时间: \(expiration)" + } else { + return "Token 状态: 未缓存" + } + } +} + +// 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/Views/CreateFeedView.swift b/yana/Views/CreateFeedView.swift index 2517b8d..7b0a7b9 100644 --- a/yana/Views/CreateFeedView.swift +++ b/yana/Views/CreateFeedView.swift @@ -6,170 +6,168 @@ struct CreateFeedView: View { let store: StoreOf var body: some View { - WithPerceptionTracking { - NavigationStack { - GeometryReader { geometry in - ZStack { - // 背景渐变 - LinearGradient( - gradient: Gradient(colors: [ - Color(red: 0.1, green: 0.1, blue: 0.2), - Color(red: 0.2, green: 0.1, blue: 0.3) - ]), - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - .ignoresSafeArea() - - ScrollView { - VStack(spacing: 20) { - // 内容输入区域 - VStack(alignment: .leading, spacing: 12) { - // 文本输入框 - ZStack(alignment: .topLeading) { - RoundedRectangle(cornerRadius: 12) - .fill(Color.white.opacity(0.1)) - .frame(minHeight: 120) - - if store.content.isEmpty { - Text("Enter Content") - .foregroundColor(.white.opacity(0.5)) - .padding(.horizontal, 16) - .padding(.vertical, 12) - } - - TextEditor(text: .init( - get: { store.content }, - set: { store.send(.contentChanged($0)) } - )) - .foregroundColor(.white) - .background(Color.clear) - .padding(.horizontal, 12) - .padding(.vertical, 8) - .scrollContentBackground(.hidden) + NavigationStack { + GeometryReader { geometry in + ZStack { + // 背景渐变 + LinearGradient( + gradient: Gradient(colors: [ + Color(red: 0.1, green: 0.1, blue: 0.2), + Color(red: 0.2, green: 0.1, blue: 0.3) + ]), + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + .ignoresSafeArea() + + ScrollView { + VStack(spacing: 20) { + // 内容输入区域 + VStack(alignment: .leading, spacing: 12) { + // 文本输入框 + ZStack(alignment: .topLeading) { + RoundedRectangle(cornerRadius: 12) + .fill(Color.white.opacity(0.1)) + .frame(minHeight: 120) + + if store.content.isEmpty { + Text("Enter Content") + .foregroundColor(.white.opacity(0.5)) + .padding(.horizontal, 16) + .padding(.vertical, 12) } - // 字符计数 - HStack { - Spacer() - Text("\(store.characterCount)/500") - .font(.system(size: 12)) - .foregroundColor( - store.characterCount > 500 ? .red : .white.opacity(0.6) - ) - } - } - .padding(.horizontal, 20) - .padding(.top, 20) - - // 图片选择区域 - VStack(alignment: .leading, spacing: 12) { - if !store.processedImages.isEmpty || store.canAddMoreImages { - ModernImageSelectionGrid( - images: store.processedImages, - selectedItems: store.selectedImages, - canAddMore: store.canAddMoreImages, - onItemsChanged: { items in - store.send(.photosPickerItemsChanged(items)) - }, - onRemoveImage: { index in - store.send(.removeImage(index)) - } - ) - } - } - .padding(.horizontal, 20) - - // 加载状态 - if store.isLoading { - HStack { - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - Text("处理图片中...") - .font(.system(size: 14)) - .foregroundColor(.white.opacity(0.8)) - } - .padding(.top, 10) + TextEditor(text: .init( + get: { store.content }, + set: { store.send(.contentChanged($0)) } + )) + .foregroundColor(.white) + .background(Color.clear) + .padding(.horizontal, 12) + .padding(.vertical, 8) + .scrollContentBackground(.hidden) } - // 错误提示 - if let error = store.errorMessage { - Text(error) - .font(.system(size: 14)) - .foregroundColor(.red) - .padding(.horizontal, 20) - .multilineTextAlignment(.center) - } - - // 底部安全区域 - Color.clear.frame(height: geometry.safeAreaInsets.bottom + 100) - } - } - - // 底部发布按钮 - VStack { - Spacer() - - Button(action: { - store.send(.publishButtonTapped) - }) { + // 字符计数 HStack { - if store.isLoading { - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - .scaleEffect(0.8) - Text("发布中...") - .font(.system(size: 16, weight: .medium)) - .foregroundColor(.white) - } else { - Text("发布") - .font(.system(size: 16, weight: .medium)) - .foregroundColor(.white) - } + Spacer() + Text("\(store.characterCount)/500") + .font(.system(size: 12)) + .foregroundColor( + store.characterCount > 500 ? .red : .white.opacity(0.6) + ) } - .frame(maxWidth: .infinity) - .frame(height: 50) - .background( - LinearGradient( - gradient: Gradient(colors: [ - Color.purple, - Color.blue - ]), - startPoint: .leading, - endPoint: .trailing - ) - ) - .cornerRadius(25) - .disabled(store.isLoading || !store.canPublish) - .opacity(store.isLoading || !store.canPublish ? 0.6 : 1.0) } .padding(.horizontal, 20) - .padding(.bottom, geometry.safeAreaInsets.bottom + 20) + .padding(.top, 20) + + // 图片选择区域 + VStack(alignment: .leading, spacing: 12) { + if !store.processedImages.isEmpty || store.canAddMoreImages { + ModernImageSelectionGrid( + images: store.processedImages, + selectedItems: store.selectedImages, + canAddMore: store.canAddMoreImages, + onItemsChanged: { items in + store.send(.photosPickerItemsChanged(items)) + }, + onRemoveImage: { index in + store.send(.removeImage(index)) + } + ) + } + } + .padding(.horizontal, 20) + + // 加载状态 + if store.isLoading { + HStack { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + Text("处理图片中...") + .font(.system(size: 14)) + .foregroundColor(.white.opacity(0.8)) + } + .padding(.top, 10) + } + + // 错误提示 + if let error = store.errorMessage { + Text(error) + .font(.system(size: 14)) + .foregroundColor(.red) + .padding(.horizontal, 20) + .multilineTextAlignment(.center) + } + + // 底部安全区域 + Color.clear.frame(height: geometry.safeAreaInsets.bottom + 100) } } - } - .navigationTitle("图文发布") - .navigationBarTitleDisplayMode(.inline) - .toolbarBackground(.hidden, for: .navigationBar) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button("取消") { - store.send(.dismissView) - } - .foregroundColor(.white) - } - ToolbarItem(placement: .navigationBarTrailing) { - Button("发布") { + // 底部发布按钮 + VStack { + Spacer() + + Button(action: { store.send(.publishButtonTapped) + }) { + HStack { + if store.isLoading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .scaleEffect(0.8) + Text("发布中...") + .font(.system(size: 16, weight: .medium)) + .foregroundColor(.white) + } else { + Text("发布") + .font(.system(size: 16, weight: .medium)) + .foregroundColor(.white) + } + } + .frame(maxWidth: .infinity) + .frame(height: 50) + .background( + LinearGradient( + gradient: Gradient(colors: [ + Color.purple, + Color.blue + ]), + startPoint: .leading, + endPoint: .trailing + ) + ) + .cornerRadius(25) + .disabled(store.isLoading || !store.canPublish) + .opacity(store.isLoading || !store.canPublish ? 0.6 : 1.0) } - .foregroundColor(store.canPublish ? .white : .white.opacity(0.5)) - .disabled(!store.canPublish || store.isLoading) + .padding(.horizontal, 20) + .padding(.bottom, geometry.safeAreaInsets.bottom + 20) } } } - .preferredColorScheme(.dark) + .navigationTitle("图文发布") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.hidden, for: .navigationBar) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("取消") { + store.send(.dismissView) + } + .foregroundColor(.white) + } + + ToolbarItem(placement: .navigationBarTrailing) { + Button("发布") { + store.send(.publishButtonTapped) + } + .foregroundColor(store.canPublish ? .white : .white.opacity(0.5)) + .disabled(!store.canPublish || store.isLoading) + } + } } + .preferredColorScheme(.dark) } } diff --git a/yana/Views/EMailLoginView.swift b/yana/Views/EMailLoginView.swift index 00a922b..fc22118 100644 --- a/yana/Views/EMailLoginView.swift +++ b/yana/Views/EMailLoginView.swift @@ -5,14 +5,12 @@ import Combine struct EMailLoginView: View { let store: StoreOf let onBack: () -> Void + @Binding var showEmailLogin: Bool - // 使用本地@State管理UI状态 @State private var email: String = "" @State private var verificationCode: String = "" @State private var codeCountdown: Int = 0 @State private var timerCancellable: AnyCancellable? - - // 管理输入框焦点状态 @FocusState private var focusedField: Field? enum Field { @@ -20,12 +18,9 @@ struct EMailLoginView: View { case verificationCode } - // 计算登录按钮是否可用 private var isLoginButtonEnabled: Bool { return !store.isLoading && !email.isEmpty && !verificationCode.isEmpty } - - // 计算获取验证码按钮文本 private var getCodeButtonText: String { if store.isCodeLoading { return "" @@ -35,188 +30,38 @@ struct EMailLoginView: View { return NSLocalizedString("email_login.get_code", comment: "") } } - - // 计算获取验证码按钮是否可用 private var isCodeButtonEnabled: Bool { return !store.isCodeLoading && codeCountdown == 0 } var body: some View { -// WithViewStore(store, observe: { $0 }) { _ in - GeometryReader { geometry in - WithPerceptionTracking { - ZStack { - // 背景图片 - Image("bg") - .resizable() - .aspectRatio(contentMode: .fill) - .ignoresSafeArea(.all) - - VStack(spacing: 0) { - // 顶部导航栏 - HStack { - Button(action: { - onBack() - }) { - Image(systemName: "chevron.left") - .font(.system(size: 24, weight: .medium)) - .foregroundColor(.white) - .frame(width: 44, height: 44) - } - - Spacer() - } - .padding(.horizontal, 16) - .padding(.top, 8) - - Spacer() - .frame(height: 60) - - // 标题 - Text(NSLocalizedString("email_login.title", comment: "")) - .font(.system(size: 28, weight: .medium)) - .foregroundColor(.white) - .padding(.bottom, 80) - - // 输入框区域 - VStack(spacing: 24) { - // 邮箱输入框 - ZStack { - RoundedRectangle(cornerRadius: 25) - .fill(Color.white.opacity(0.1)) - .overlay( - RoundedRectangle(cornerRadius: 25) - .stroke(Color.white.opacity(0.3), lineWidth: 1) - ) - .frame(height: 56) - - TextField("", text: $email) - .placeholder(when: email.isEmpty) { - Text(NSLocalizedString("placeholder.enter_email", comment: "")) - .foregroundColor(.white.opacity(0.6)) - } - .foregroundColor(.white) - .font(.system(size: 16)) - .padding(.horizontal, 24) - .keyboardType(.emailAddress) - .autocapitalization(.none) - .disableAutocorrection(true) - .focused($focusedField, equals: .email) - } - - // 验证码输入框 - ZStack { - RoundedRectangle(cornerRadius: 25) - .fill(Color.white.opacity(0.1)) - .overlay( - RoundedRectangle(cornerRadius: 25) - .stroke(Color.white.opacity(0.3), lineWidth: 1) - ) - .frame(height: 56) - - HStack { - TextField("", text: $verificationCode) - .placeholder(when: verificationCode.isEmpty) { - Text(NSLocalizedString("placeholder.enter_verification_code", comment: "")) - .foregroundColor(.white.opacity(0.6)) - } - .foregroundColor(.white) - .font(.system(size: 16)) - .keyboardType(.numberPad) - .focused($focusedField, equals: .verificationCode) - - // 获取验证码按钮 - Button(action: { - // 发送API请求 - store.send(.getVerificationCodeTapped) - // 立即开始倒计时 - startCountdown() - }) { - ZStack { - if store.isCodeLoading { - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - .scaleEffect(0.7) - } else { - Text(getCodeButtonText) - .font(.system(size: 14, weight: .medium)) - .foregroundColor(.white) - } - } - .frame(width: 60, height: 36) - .background( - RoundedRectangle(cornerRadius: 18) - .fill(Color.white.opacity(isCodeButtonEnabled && !email.isEmpty ? 0.2 : 0.1)) - ) - } - .disabled(!isCodeButtonEnabled || email.isEmpty || store.isCodeLoading) - } - .padding(.horizontal, 24) - } - } - .padding(.horizontal, 32) - - Spacer() - .frame(height: 60) - - // 登录按钮 - Button(action: { - store.send(.loginButtonTapped(email: email, verificationCode: verificationCode)) - }) { - ZStack { - // 渐变背景 - LinearGradient( - colors: [ - Color(red: 0.85, green: 0.37, blue: 1.0), // #D85EFF - Color(red: 0.54, green: 0.31, blue: 1.0) // #8A4FFF - ], - startPoint: .leading, - endPoint: .trailing - ) - .clipShape(RoundedRectangle(cornerRadius: 28)) - - HStack { - if store.isLoading { - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - .scaleEffect(0.8) - } - Text(store.isLoading ? NSLocalizedString("email_login.logging_in", comment: "") : NSLocalizedString("email_login.login_button", comment: "")) - .font(.system(size: 18, weight: .semibold)) - .foregroundColor(.white) - } - } - .frame(height: 56) - } - .disabled(store.isLoading || email.isEmpty || verificationCode.isEmpty) - .opacity(isLoginButtonEnabled ? 1.0 : 0.5) - .padding(.horizontal, 32) - - // 错误信息 - if let errorMessage = store.errorMessage { - Text(errorMessage) - .font(.system(size: 14)) - .foregroundColor(.red) - .padding(.top, 16) - .padding(.horizontal, 32) - } - - Spacer() - } + WithViewStore(store, observe: { $0.loginStep }) { viewStore in + LoginContentView( + store: store, + onBack: onBack, + email: $email, + verificationCode: $verificationCode, + codeCountdown: $codeCountdown, + focusedField: $focusedField, + isLoginButtonEnabled: isLoginButtonEnabled, + getCodeButtonText: getCodeButtonText, + isCodeButtonEnabled: isCodeButtonEnabled + ) + .onChange(of: viewStore.state) { newStep in + debugInfoSync("🔄 EMailLoginView: loginStep 变化为 \(newStep)") + if newStep == .completed { + debugInfoSync("✅ EMailLoginView: 登录成功,准备关闭自身") + showEmailLogin = false } } } -// } .onAppear { let _ = WithPerceptionTracking { - // 每次进入页面都重置状态 store.send(.resetState) - email = "" verificationCode = "" codeCountdown = 0 stopCountdown() - #if DEBUG email = "exzero@126.com" store.send(.emailChanged(email)) @@ -240,7 +85,6 @@ struct EMailLoginView: View { } .onChange(of: store.isCodeLoading) { isCodeLoading in let _ = WithPerceptionTracking { - // 当API请求完成且成功时,自动将焦点切换到验证码输入框 if !isCodeLoading && store.errorMessage == nil { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { focusedField = .verificationCode @@ -250,14 +94,9 @@ struct EMailLoginView: View { } } - // MARK: - 倒计时管理 private func startCountdown() { stopCountdown() - - // 立即设置倒计时 codeCountdown = 60 - - // 使用 SwiftUI 原生的 Timer.publish 方式 timerCancellable = Timer.publish(every: 1.0, on: .main, in: .common) .autoconnect() .sink { _ in @@ -268,13 +107,159 @@ struct EMailLoginView: View { } } } - private func stopCountdown() { timerCancellable?.cancel() timerCancellable = nil } } +private struct LoginContentView: View { + let store: StoreOf + let onBack: () -> Void + @Binding var email: String + @Binding var verificationCode: String + @Binding var codeCountdown: Int + @FocusState.Binding var focusedField: EMailLoginView.Field? + let isLoginButtonEnabled: Bool + let getCodeButtonText: String + let isCodeButtonEnabled: Bool + + var body: some View { + GeometryReader { geometry in + WithPerceptionTracking { + ZStack { + Image("bg") + .resizable() + .aspectRatio(contentMode: .fill) + .ignoresSafeArea(.all) + VStack(spacing: 0) { + HStack { + Button(action: { + onBack() + }) { + Image(systemName: "chevron.left") + .font(.system(size: 24, weight: .medium)) + .foregroundColor(.white) + .frame(width: 44, height: 44) + } + Spacer() + } + .padding(.horizontal, 16) + .padding(.top, 8) + Spacer().frame(height: 60) + Text(NSLocalizedString("email_login.title", comment: "")) + .font(.system(size: 28, weight: .medium)) + .foregroundColor(.white) + .padding(.bottom, 80) + VStack(spacing: 24) { + ZStack { + RoundedRectangle(cornerRadius: 25) + .fill(Color.white.opacity(0.1)) + .overlay( + RoundedRectangle(cornerRadius: 25) + .stroke(Color.white.opacity(0.3), lineWidth: 1) + ) + .frame(height: 56) + TextField("", text: $email) + .placeholder(when: email.isEmpty) { + Text(NSLocalizedString("placeholder.enter_email", comment: "")) + .foregroundColor(.white.opacity(0.6)) + } + .foregroundColor(.white) + .font(.system(size: 16)) + .padding(.horizontal, 24) + .keyboardType(.emailAddress) + .focused($focusedField, equals: .email) + } + ZStack { + RoundedRectangle(cornerRadius: 25) + .fill(Color.white.opacity(0.1)) + .overlay( + RoundedRectangle(cornerRadius: 25) + .stroke(Color.white.opacity(0.3), lineWidth: 1) + ) + .frame(height: 56) + HStack { + TextField("", text: $verificationCode) + .placeholder(when: verificationCode.isEmpty) { + Text(NSLocalizedString("placeholder.enter_code", comment: "")) + .foregroundColor(.white.opacity(0.6)) + } + .foregroundColor(.white) + .font(.system(size: 16)) + .keyboardType(.numberPad) + .focused($focusedField, equals: .verificationCode) + Button(action: { + store.send(.getVerificationCodeTapped) + }) { + ZStack { + if store.isCodeLoading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .scaleEffect(0.7) + } else { + Text(getCodeButtonText) + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.white) + } + } + .frame(width: 60, height: 36) + .background( + RoundedRectangle(cornerRadius: 18) + .fill(Color.white.opacity(isCodeButtonEnabled && !email.isEmpty ? 0.2 : 0.1)) + ) + } + .disabled(!isCodeButtonEnabled || email.isEmpty || store.isCodeLoading) + } + .padding(.horizontal, 24) + } + } + .padding(.horizontal, 32) + Spacer().frame(height: 60) + Button(action: { + store.send(.loginButtonTapped(email: email, verificationCode: verificationCode)) + }) { + ZStack { + LinearGradient( + colors: [ + Color(red: 0.85, green: 0.37, blue: 1.0), + Color(red: 0.54, green: 0.31, blue: 1.0) + ], + startPoint: .leading, + endPoint: .trailing + ) + .clipShape(RoundedRectangle(cornerRadius: 28)) + HStack { + if store.isLoading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .scaleEffect(0.8) + } + Text(store.isLoading ? NSLocalizedString("email_login.logging_in", comment: "") : NSLocalizedString("email_login.login_button", comment: "")) + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(.white) + } + } + .frame(height: 56) + } + .disabled(store.isLoading || email.isEmpty || verificationCode.isEmpty) + .opacity(isLoginButtonEnabled ? 1.0 : 0.5) + .padding(.horizontal, 32) + if let errorMessage = store.errorMessage { + Text(errorMessage) + .font(.system(size: 14)) + .foregroundColor(.red) + .padding(.top, 16) + .padding(.horizontal, 32) + } + Spacer() + } + } + } + } + } +} + //#Preview { // EMailLoginView( // store: Store( @@ -282,6 +267,7 @@ struct EMailLoginView: View { // ) { // EMailLoginFeature() // }, -// onBack: {} +// onBack: {}, +// showEmailLogin: .constant(true) // ) //} diff --git a/yana/Views/FeedView.swift b/yana/Views/FeedView.swift index 2ffb0fa..c100bba 100644 --- a/yana/Views/FeedView.swift +++ b/yana/Views/FeedView.swift @@ -24,34 +24,32 @@ struct FeedTopBarView: View { struct FeedMomentsListView: View { let store: StoreOf var body: some View { - WithPerceptionTracking { - LazyVStack(spacing: 16) { - if store.moments.isEmpty { - VStack(spacing: 12) { - Image(systemName: "heart.text.square") - .font(.system(size: 40)) - .foregroundColor(.white.opacity(0.6)) - Text("暂无动态内容") - .font(.system(size: 16)) - .foregroundColor(.white.opacity(0.8)) - if let error = store.error { - Text("错误: \(error)") - .font(.system(size: 12)) - .foregroundColor(.red.opacity(0.8)) - .multilineTextAlignment(.center) - .padding(.horizontal, 20) - } - } - .padding(.top, 40) - } else { - ForEach(Array(store.moments.enumerated()), id: \.element.dynamicId) { index, moment in - OptimizedDynamicCardView( - moment: moment, - allMoments: store.moments, - currentIndex: index - ) + LazyVStack(spacing: 16) { + if store.moments.isEmpty { + VStack(spacing: 12) { + Image(systemName: "heart.text.square") + .font(.system(size: 40)) + .foregroundColor(.white.opacity(0.6)) + Text("暂无动态内容") + .font(.system(size: 16)) + .foregroundColor(.white.opacity(0.8)) + if let error = store.error { + Text("错误: \(error)") + .font(.system(size: 12)) + .foregroundColor(.red.opacity(0.8)) + .multilineTextAlignment(.center) + .padding(.horizontal, 20) } } + .padding(.top, 40) + } else { + ForEach(Array(store.moments.enumerated()), id: \.element.dynamicId) { index, moment in + OptimizedDynamicCardView( + moment: moment, + allMoments: store.moments, + currentIndex: index + ) + } } } .padding(.horizontal, 16) @@ -63,50 +61,52 @@ struct FeedView: View { let store: StoreOf var body: some View { - WithPerceptionTracking { - GeometryReader { geometry in - ScrollView { - VStack(spacing: 20) { - FeedTopBarView(store: store) - Image(systemName: "heart.fill") - .font(.system(size: 60)) - .foregroundColor(.red) - .padding(.top, 40) - Text("The disease is like a cruel ruler,\nand time is our most precious treasure.\nEvery moment we live is a victory\nagainst the inevitable.") - .font(.system(size: 16)) - .multilineTextAlignment(.center) - .foregroundColor(.white.opacity(0.9)) - .padding(.horizontal, 30) - .padding(.top, 20) - FeedMomentsListView(store: store) - if store.isLoading { - HStack { - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - Text("加载中...") - .font(.system(size: 14)) - .foregroundColor(.white.opacity(0.8)) - } - .padding(.top, 20) + GeometryReader { geometry in + ScrollView { + VStack(spacing: 20) { + FeedTopBarView(store: store) + Image(systemName: "heart.fill") + .font(.system(size: 60)) + .foregroundColor(.red) + .padding(.top, 40) + Text("The disease is like a cruel ruler,\nand time is our most precious treasure.\nEvery moment we live is a victory\nagainst the inevitable.") + .font(.system(size: 16)) + .multilineTextAlignment(.center) + .foregroundColor(.white.opacity(0.9)) + .padding(.horizontal, 30) + .padding(.top, 20) + FeedMomentsListView(store: store) + if store.isLoading { + HStack { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + Text("加载中...") + .font(.system(size: 14)) + .foregroundColor(.white.opacity(0.8)) } - Color.clear.frame(height: geometry.safeAreaInsets.bottom + 100) - } - } - .refreshable { - store.send(.loadLatestMoments) - } - .onAppear { -// store.send(.onAppear) - } - .sheet(isPresented: .init( - get: { store.isShowingCreateFeed }, - set: { _ in store.send(.dismissCreateFeed) } - )) { - if let createFeedStore = store.scope(state: \.createFeedState, action: \.createFeed) { - CreateFeedView(store: createFeedStore) + .padding(.top, 20) } + Color.clear.frame(height: geometry.safeAreaInsets.bottom + 100) } } + .refreshable { + store.send(.loadLatestMoments) + } + .onAppear { + store.send(.onAppear) + } + } + .sheet(isPresented: Binding( + get: { store.isCreateFeedPresented }, + set: { _ in store.send(.createFeedDismissed) } + )) { + CreateFeedView( + store: Store( + initialState: CreateFeedFeature.State() + ) { + CreateFeedFeature() + } + ) } } } @@ -296,11 +296,9 @@ struct OptimizedImageGrid: View { let imageSize: CGFloat = (availableWidth - spacing * 2) / 3 let columns = Array(repeating: GridItem(.fixed(imageSize), spacing: spacing), count: 3) - WithPerceptionTracking { - LazyVGrid(columns: columns, spacing: spacing) { - ForEach(images.prefix(9), id: \.id) { image in - SquareImageView(image: image, size: imageSize) - } + LazyVGrid(columns: columns, spacing: spacing) { + ForEach(images.prefix(9), id: \.id) { image in + SquareImageView(image: image, size: imageSize) } } } @@ -407,25 +405,23 @@ struct RealDynamicCardView: View { // 图片网格 if let images = moment.dynamicResList, !images.isEmpty { - WithPerceptionTracking { - 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) + 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) } } } @@ -521,17 +517,15 @@ struct DynamicCardView: View { .multilineTextAlignment(.leading) // 图片网格 - WithPerceptionTracking { - 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)) - ) - } + 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)) + ) } } @@ -569,10 +563,10 @@ struct DynamicCardView: View { } } -#Preview { - FeedView( - store: Store(initialState: FeedFeature.State()) { - FeedFeature() - } - ) -} +//#Preview { +// FeedView( +// store: Store(initialState: FeedFeature.State()) { +// FeedFeature() +// } +// ) +//} diff --git a/yana/Views/HomeView.swift b/yana/Views/HomeView.swift index bcde1ee..b0cab32 100644 --- a/yana/Views/HomeView.swift +++ b/yana/Views/HomeView.swift @@ -8,59 +8,59 @@ struct HomeView: View { @State private var selectedTab: Tab = .feed var body: some View { - WithPerceptionTracking { - GeometryReader { geometry in + GeometryReader { geometry in + ZStack { + // 使用 "bg" 图片作为背景 - 全屏显示 + Image("bg") + .resizable() + .aspectRatio(contentMode: .fill) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .clipped() + .ignoresSafeArea(.all) + + // 主要内容区域 - 全屏显示 ZStack { - // 使用 "bg" 图片作为背景 - 全屏显示 - Image("bg") - .resizable() - .aspectRatio(contentMode: .fill) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .clipped() - .ignoresSafeArea(.all) - - // 主要内容区域 - 全屏显示 - ZStack { - switch selectedTab { - case .feed: + switch selectedTab { + case .feed: + NavigationStack { FeedView( store: store.scope(state: \.feedState, action: \.feed) ) - .transition(.opacity) - case .me: - MeView(onLogout: onLogout) - .transition(.opacity) } + .transition(.opacity) + case .me: + MeView(onLogout: onLogout) + .transition(.opacity) } - .frame(maxWidth: .infinity, maxHeight: .infinity) - - // 底部导航栏 - 悬浮在最上层 - VStack { - Spacer() - BottomTabView(selectedTab: $selectedTab) - } - .padding(.bottom, geometry.safeAreaInsets.bottom + 100) } + .frame(maxWidth: .infinity, maxHeight: .infinity) + + // 底部导航栏 - 悬浮在最上层 + VStack { + Spacer() + BottomTabView(selectedTab: $selectedTab) + } + .padding(.bottom, geometry.safeAreaInsets.bottom + 100) } - .onAppear { - store.send(.onAppear) - } - .sheet(isPresented: Binding( - get: { store.isSettingPresented }, - set: { _ in store.send(.settingDismissed) } - )) { - SettingView(store: store.scope(state: \.settingState, action: \.setting)) - } + } + .onAppear { + store.send(.onAppear) + } + .sheet(isPresented: Binding( + get: { store.isSettingPresented }, + set: { _ in store.send(.settingDismissed) } + )) { + SettingView(store: store.scope(state: \.settingState, action: \.setting)) } } } -#Preview { - HomeView( - store: Store( - initialState: HomeFeature.State() - ) { - HomeFeature() - }, onLogout: {} - ) -} +//#Preview { +// HomeView( +// store: Store( +// initialState: HomeFeature.State() +// ) { +// HomeFeature() +// }, onLogout: {} +// ) +//} diff --git a/yana/Views/IDLoginView.swift b/yana/Views/IDLoginView.swift index f6831e7..38d9800 100644 --- a/yana/Views/IDLoginView.swift +++ b/yana/Views/IDLoginView.swift @@ -5,6 +5,7 @@ import Perception struct IDLoginView: View { let store: StoreOf let onBack: () -> Void + @Binding var showIDLogin: Bool // 新增:绑定父视图的显示状态 // 使用本地@State管理UI状态 @State private var userID: String = "" @@ -20,199 +21,209 @@ struct IDLoginView: View { } var body: some View { - GeometryReader { geometry in - WithPerceptionTracking { - ZStack { - // 背景图片 - 使用与登录页面相同的"bg" - Image("bg") - .resizable() - .aspectRatio(contentMode: .fill) - .ignoresSafeArea(.all) - - VStack(spacing: 0) { - // 顶部导航栏 - HStack { - Button(action: { - onBack() - }) { - Image(systemName: "chevron.left") - .font(.system(size: 24, weight: .medium)) - .foregroundColor(.white) - .frame(width: 44, height: 44) + WithViewStore(store, observe: { $0.loginStep }) { viewStore in + GeometryReader { geometry in + WithPerceptionTracking { + ZStack { + // 背景图片 - 使用与登录页面相同的"bg" + Image("bg") + .resizable() + .aspectRatio(contentMode: .fill) + .ignoresSafeArea(.all) + + VStack(spacing: 0) { + // 顶部导航栏 + HStack { + Button(action: { + onBack() + }) { + Image(systemName: "chevron.left") + .font(.system(size: 24, weight: .medium)) + .foregroundColor(.white) + .frame(width: 44, height: 44) + } + + Spacer() } + .padding(.horizontal, 16) + .padding(.top, 8) Spacer() - } - .padding(.horizontal, 16) - .padding(.top, 8) - - Spacer() - .frame(height: 60) - - // 标题 - Text(NSLocalizedString("id_login.title", comment: "")) - .font(.system(size: 28, weight: .medium)) - .foregroundColor(.white) - .padding(.bottom, 80) - - // 输入框区域 - VStack(spacing: 24) { - // ID 输入框 - ZStack { - RoundedRectangle(cornerRadius: 25) - .fill(Color.white.opacity(0.1)) - .overlay( - RoundedRectangle(cornerRadius: 25) - .stroke(Color.white.opacity(0.3), lineWidth: 1) - ) - .frame(height: 56) - - TextField("", text: $userID) // 使用SwiftUI的绑定 - .placeholder(when: userID.isEmpty) { - Text(NSLocalizedString("placeholder.enter_id", comment: "")) - .foregroundColor(.white.opacity(0.6)) - } + .frame(height: 60) + + // 标题 + Text(NSLocalizedString("id_login.title", comment: "")) + .font(.system(size: 28, weight: .medium)) .foregroundColor(.white) - .font(.system(size: 16)) - .padding(.horizontal, 24) - .keyboardType(.numberPad) + .padding(.bottom, 80) + + // 输入框区域 + VStack(spacing: 24) { + // ID 输入框 + ZStack { + RoundedRectangle(cornerRadius: 25) + .fill(Color.white.opacity(0.1)) + .overlay( + RoundedRectangle(cornerRadius: 25) + .stroke(Color.white.opacity(0.3), lineWidth: 1) + ) + .frame(height: 56) + + TextField("", text: $userID) // 使用SwiftUI的绑定 + .placeholder(when: userID.isEmpty) { + Text(NSLocalizedString("placeholder.enter_id", comment: "")) + .foregroundColor(.white.opacity(0.6)) + } + .foregroundColor(.white) + .font(.system(size: 16)) + .padding(.horizontal, 24) + .keyboardType(.numberPad) + } + + // 密码输入框 + ZStack { + RoundedRectangle(cornerRadius: 25) + .fill(Color.white.opacity(0.1)) + .overlay( + RoundedRectangle(cornerRadius: 25) + .stroke(Color.white.opacity(0.3), lineWidth: 1) + ) + .frame(height: 56) + + HStack { + if isPasswordVisible { + TextField("", text: $password) // 使用SwiftUI的绑定 + .placeholder(when: password.isEmpty) { + Text(NSLocalizedString("placeholder.enter_password", comment: "")) + .foregroundColor(.white.opacity(0.6)) + } + .foregroundColor(.white) + .font(.system(size: 16)) + } else { + SecureField("", text: $password) // 使用SwiftUI的绑定 + .placeholder(when: password.isEmpty) { + Text(NSLocalizedString("placeholder.enter_password", comment: "")) + .foregroundColor(.white.opacity(0.6)) + } + .foregroundColor(.white) + .font(.system(size: 16)) + } + + Button(action: { + isPasswordVisible.toggle() + }) { + Image(systemName: isPasswordVisible ? "eye.slash" : "eye") + .foregroundColor(.white.opacity(0.7)) + .font(.system(size: 18)) + } + } + .padding(.horizontal, 24) + } + } + .padding(.horizontal, 32) + + // Forgot Password 链接 + HStack { + Spacer() + Button(action: { + showRecoverPassword = true + }) { + Text(NSLocalizedString("id_login.forgot_password", comment: "")) + .font(.system(size: 14)) + .foregroundColor(.white.opacity(0.8)) + } + } + .padding(.horizontal, 32) + .padding(.top, 16) + + Spacer() + .frame(height: 60) + + // 登录按钮 + Button(action: { + // 发送登录action时传递本地状态 + store.send(.loginButtonTapped(userID: userID, password: password)) + }) { + ZStack { + // 渐变背景 + LinearGradient( + colors: [ + Color(red: 0.85, green: 0.37, blue: 1.0), // #D85EFF + Color(red: 0.54, green: 0.31, blue: 1.0) // #8A4FFF + ], + startPoint: .leading, + endPoint: .trailing + ) + .clipShape(RoundedRectangle(cornerRadius: 28)) + + HStack { + if store.isLoading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .scaleEffect(0.8) + } + Text(store.isLoading ? NSLocalizedString("id_login.logging_in", comment: "") : NSLocalizedString("id_login.login_button", comment: "")) + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(.white) + } + } + .frame(height: 56) + } + .disabled(store.isLoading || userID.isEmpty || password.isEmpty) + .opacity(isLoginButtonEnabled ? 1.0 : 0.5) // 透明度50%当条件不满足时 + .padding(.horizontal, 32) + + // 错误信息 + if let errorMessage = store.errorMessage { + Text(errorMessage) + .font(.system(size: 14)) + .foregroundColor(.red) + .padding(.top, 16) + .padding(.horizontal, 32) } - // 密码输入框 - ZStack { - RoundedRectangle(cornerRadius: 25) - .fill(Color.white.opacity(0.1)) - .overlay( - RoundedRectangle(cornerRadius: 25) - .stroke(Color.white.opacity(0.3), lineWidth: 1) - ) - .frame(height: 56) - - HStack { - if isPasswordVisible { - TextField("", text: $password) // 使用SwiftUI的绑定 - .placeholder(when: password.isEmpty) { - Text(NSLocalizedString("placeholder.enter_password", comment: "")) - .foregroundColor(.white.opacity(0.6)) - } - .foregroundColor(.white) - .font(.system(size: 16)) - } else { - SecureField("", text: $password) // 使用SwiftUI的绑定 - .placeholder(when: password.isEmpty) { - Text(NSLocalizedString("placeholder.enter_password", comment: "")) - .foregroundColor(.white.opacity(0.6)) - } - .foregroundColor(.white) - .font(.system(size: 16)) - } - - Button(action: { - isPasswordVisible.toggle() - }) { - Image(systemName: isPasswordVisible ? "eye.slash" : "eye") - .foregroundColor(.white.opacity(0.7)) - .font(.system(size: 18)) - } - } - .padding(.horizontal, 24) - } - } - .padding(.horizontal, 32) - - // Forgot Password 链接 - HStack { Spacer() - Button(action: { - showRecoverPassword = true - }) { - Text(NSLocalizedString("id_login.forgot_password", comment: "")) - .font(.system(size: 14)) - .foregroundColor(.white.opacity(0.8)) - } } - .padding(.horizontal, 32) - .padding(.top, 16) - - Spacer() - .frame(height: 60) - - // 登录按钮 - Button(action: { - // 发送登录action时传递本地状态 - store.send(.loginButtonTapped(userID: userID, password: password)) - }) { - ZStack { - // 渐变背景 - LinearGradient( - colors: [ - Color(red: 0.85, green: 0.37, blue: 1.0), // #D85EFF - Color(red: 0.54, green: 0.31, blue: 1.0) // #8A4FFF - ], - startPoint: .leading, - endPoint: .trailing - ) - .clipShape(RoundedRectangle(cornerRadius: 28)) - - HStack { - if store.isLoading { - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - .scaleEffect(0.8) - } - Text(store.isLoading ? NSLocalizedString("id_login.logging_in", comment: "") : NSLocalizedString("id_login.login_button", comment: "")) - .font(.system(size: 18, weight: .semibold)) - .foregroundColor(.white) - } - } - .frame(height: 56) - } - .disabled(store.isLoading || userID.isEmpty || password.isEmpty) - .opacity(isLoginButtonEnabled ? 1.0 : 0.5) // 透明度50%当条件不满足时 - .padding(.horizontal, 32) - - // 错误信息 - if let errorMessage = store.errorMessage { - Text(errorMessage) - .font(.system(size: 14)) - .foregroundColor(.red) - .padding(.top, 16) - .padding(.horizontal, 32) - } - - Spacer() + } } } + .navigationBarHidden(true) + // 使用与 LoginView 一致的 navigationDestination 方式 + .navigationDestination(isPresented: $showRecoverPassword) { + WithPerceptionTracking { + RecoverPasswordView( + store: Store( + initialState: RecoverPasswordFeature.State() + ) { + RecoverPasswordFeature() + }, + onBack: { + showRecoverPassword = false + } + ) + .navigationBarHidden(true) + } } - } - .navigationBarHidden(true) - // 使用与 LoginView 一致的 navigationDestination 方式 - .navigationDestination(isPresented: $showRecoverPassword) { - WithPerceptionTracking { - RecoverPasswordView( - store: Store( - initialState: RecoverPasswordFeature.State() - ) { - RecoverPasswordFeature() - }, - onBack: { - showRecoverPassword = false - } - ) - .navigationBarHidden(true) + .onAppear { + let _ = WithPerceptionTracking { + // 初始化时同步TCA状态到本地状态 + userID = store.userID + password = store.password + isPasswordVisible = store.isPasswordVisible + + #if DEBUG + // 移除测试用的硬编码凭据 + debugInfoSync("🐛 Debug模式: 已移除硬编码测试凭据") + #endif + } } - } - .onAppear { - let _ = WithPerceptionTracking { - // 初始化时同步TCA状态到本地状态 - userID = store.userID - password = store.password - isPasswordVisible = store.isPasswordVisible - - #if DEBUG - // 移除测试用的硬编码凭据 - debugInfoSync("🐛 Debug模式: 已移除硬编码测试凭据") - #endif + // 新增:监听登录状态,成功后自动关闭自身 + .onChange(of: viewStore.state) { newStep in + debugInfoSync("🔄 IDLoginView: loginStep 变化为 \(newStep)") + if newStep == .completed { + debugInfoSync("✅ IDLoginView: 登录成功,准备关闭自身") + showIDLogin = false + } } } } @@ -225,6 +236,7 @@ struct IDLoginView: View { // ) { // IDLoginFeature() // }, -// onBack: {} +// onBack: {}, +// showIDLogin: .constant(true) // ) //} diff --git a/yana/Views/LanguageSettingsView.swift b/yana/Views/LanguageSettingsView.swift index b90fb02..a0adf3c 100644 --- a/yana/Views/LanguageSettingsView.swift +++ b/yana/Views/LanguageSettingsView.swift @@ -3,8 +3,12 @@ import ComposableArchitecture struct LanguageSettingsView: View { @ObservedObject private var localizationManager = LocalizationManager.shared + @StateObject private var cosManager = COSManager.shared @Binding var isPresented: Bool + // 使用 TCA 的依赖注入获取 API 服务 + @Dependency(\.apiService) private var apiService + init(isPresented: Binding = .constant(true)) { self._isPresented = isPresented } @@ -43,10 +47,66 @@ struct LanguageSettingsView: View { .font(.caption) .foregroundColor(.secondary) } + + #if DEBUG + Section("调试功能") { + Button("测试腾讯云 COS Token") { + Task { + await testCOToken() + } + } + .foregroundColor(.blue) + + if let tokenData = cosManager.token { + VStack(alignment: .leading, spacing: 8) { + Text("✅ Token 获取成功") + .font(.headline) + .foregroundColor(.green) + + Group { + Text("存储桶: \(tokenData.bucket)") + Text("地域: \(tokenData.region)") + Text("应用ID: \(tokenData.appId)") + Text("自定义域名: \(tokenData.customDomain)") + Text("加速: \(tokenData.accelerate ? "启用" : "禁用")") + Text("过期时间: \(tokenData.expirationDate, style: .date)") + Text("剩余时间: \(tokenData.remainingTime)秒") + } + .font(.caption) + .foregroundColor(.secondary) + } + .padding(.vertical, 4) + } + } + #endif } .navigationTitle("语言设置 / Language") .navigationBarTitleDisplayMode(.inline) .navigationBarBackButtonHidden(true) + .onAppear { + #if DEBUG + // 调试环境下,页面显示时自动调用 tcToken API + Task { + await cosManager.testTokenRetrieval(apiService: apiService) + } + #endif + } + } + } + + private func testCOToken() async { + do { + let token = await cosManager.getToken(apiService: apiService) + if let token = token { + print("✅ Token 测试成功") + print(" - 存储桶: \(token.bucket)") + print(" - 地域: \(token.region)") + print(" - 剩余时间: \(token.remainingTime)秒") + } else { + print("❌ Token 测试失败: 未能获取 Token") + } + } catch { + print("❌ Token 测试异常: \(error.localizedDescription)") } } } @@ -84,6 +144,6 @@ struct LanguageRow: View { } // MARK: - Preview -#Preview { - LanguageSettingsView(isPresented: .constant(true)) -} \ No newline at end of file +//#Preview { +// LanguageSettingsView(isPresented: .constant(true)) +//} diff --git a/yana/Views/LoginView.swift b/yana/Views/LoginView.swift index 5b19874..bbb9ada 100644 --- a/yana/Views/LoginView.swift +++ b/yana/Views/LoginView.swift @@ -133,7 +133,8 @@ struct LoginView: View { ), onBack: { showIDLogin = false - } + }, + showIDLogin: $showIDLogin // 新增:传递Binding ) .navigationBarHidden(true) } @@ -147,7 +148,8 @@ struct LoginView: View { ), onBack: { showEmailLogin = false - } + }, + showEmailLogin: $showEmailLogin // 新增:传递Binding ) .navigationBarHidden(true) } @@ -169,10 +171,20 @@ struct LoginView: View { ) // 新增:监听登录成功,调用回调 .onChange(of: viewStore.state) { completed in - WithPerceptionTracking { - if completed { - onLoginSuccess() - } + if completed { + onLoginSuccess() + } + } + // 新增:监听showIDLogin关闭时,若已登录则跳转首页 + .onChange(of: showIDLogin) { newValue in + if newValue == false && viewStore.state { + onLoginSuccess() + } + } + // 新增:监听showEmailLogin关闭时,若已登录则跳转首页 + .onChange(of: showEmailLogin) { newValue in + if newValue == false && viewStore.state { + onLoginSuccess() } } } diff --git a/yana/Views/SplashView.swift b/yana/Views/SplashView.swift index 8f62245..84d9f5c 100644 --- a/yana/Views/SplashView.swift +++ b/yana/Views/SplashView.swift @@ -6,29 +6,40 @@ struct SplashView: View { var body: some View { WithPerceptionTracking { - ZStack { - // 背景图片 - 全屏显示 - Image("bg") - .resizable() - .aspectRatio(contentMode: .fill) - .ignoresSafeArea(.all) - - VStack(spacing: 32) { - Spacer() - .frame(height: 200) // 与 storyboard 中的约束对应 - - // Logo 图片 - 100x100 - Image("logo") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 100, height: 100) - - // 应用标题 - 白色,40pt字体 - Text("E-Parti") - .font(.system(size: 40, weight: .regular)) - .foregroundColor(.white) - - Spacer() + Group { + // 根据导航目标显示不同页面 + if let navigationDestination = store.navigationDestination { + switch navigationDestination { + case .login: + // 显示登录页面 + LoginView( + store: Store( + initialState: LoginFeature.State() + ) { + LoginFeature() + }, + onLoginSuccess: { + // 登录成功后导航到主页面 + store.send(.navigateToMain) + } + ) + case .main: + // 显示主应用页面 + HomeView( + store: Store( + initialState: HomeFeature.State() + ) { + HomeFeature() + }, + onLogout: { + // 登出时重新导航到登录页面 + store.send(.navigateToLogin) + } + ) + } + } else { + // 显示启动画面 + splashContent } } .onAppear { @@ -36,14 +47,43 @@ struct SplashView: View { } } } + + // 启动画面内容 + private var splashContent: some View { + ZStack { + // 背景图片 - 全屏显示 + Image("bg") + .resizable() + .aspectRatio(contentMode: .fill) + .ignoresSafeArea(.all) + + VStack(spacing: 32) { + Spacer() + .frame(height: 200) // 与 storyboard 中的约束对应 + + // Logo 图片 - 100x100 + Image("logo") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 100, height: 100) + + // 应用标题 - 白色,40pt字体 + Text("E-Parti") + .font(.system(size: 40, weight: .regular)) + .foregroundColor(.white) + + Spacer() + } + } + } } -#Preview { - SplashView( - store: Store( - initialState: SplashFeature.State() - ) { - SplashFeature() - } - ) -} \ No newline at end of file +//#Preview { +// SplashView( +// store: Store( +// initialState: SplashFeature.State() +// ) { +// SplashFeature() +// } +// ) +//} diff --git a/yana/yanaApp.swift b/yana/yanaApp.swift index fecd363..8af2bca 100644 --- a/yana/yanaApp.swift +++ b/yana/yanaApp.swift @@ -20,13 +20,17 @@ struct yanaApp: App { // 不是在Previews环境中运行 } #endif - - debugInfoSync("🛠 原生URLSession测试开始") } var body: some Scene { WindowGroup { - AppRootView() + SplashView( + store: Store( + initialState: SplashFeature.State() + ) { + SplashFeature() + } + ) } } }