feat: 添加腾讯云COS Token管理功能及相关视图更新
- 在APIEndpoints.swift中新增tcToken端点以支持腾讯云COS Token获取。 - 在APIModels.swift中新增TcTokenRequest和TcTokenResponse模型,处理Token请求和响应。 - 在COSManager.swift中实现Token的获取、缓存和过期管理逻辑,提升API请求的安全性。 - 在LanguageSettingsView中添加调试功能,允许测试COS Token获取。 - 在多个视图中更新状态管理和导航逻辑,确保用户体验一致性。 - 在FeedFeature和HomeFeature中优化状态管理,简化视图逻辑。
This commit is contained in:
@@ -20,6 +20,7 @@ enum APIEndpoint: String, CaseIterable {
|
|||||||
case ticket = "/oauth/ticket"
|
case ticket = "/oauth/ticket"
|
||||||
case emailGetCode = "/email/getCode" // 新增:邮箱验证码获取端点
|
case emailGetCode = "/email/getCode" // 新增:邮箱验证码获取端点
|
||||||
case latestDynamics = "/dynamic/square/latestDynamics" // 新增:获取最新动态列表端点
|
case latestDynamics = "/dynamic/square/latestDynamics" // 新增:获取最新动态列表端点
|
||||||
|
case tcToken = "/tencent/cos/getToken" // 新增:腾讯云 COS Token 获取端点
|
||||||
// Web 页面路径
|
// Web 页面路径
|
||||||
case userAgreement = "/modules/rule/protocol.html"
|
case userAgreement = "/modules/rule/protocol.html"
|
||||||
case privacyPolicy = "/modules/rule/privacy-wap.html"
|
case privacyPolicy = "/modules/rule/privacy-wap.html"
|
||||||
|
@@ -661,3 +661,64 @@ struct APIResponse<T: Codable>: Codable {
|
|||||||
|
|
||||||
// 注意:String+MD5 扩展已移至 Utils/Extensions/String+MD5.swift
|
// 注意: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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -11,12 +11,11 @@ struct FeedFeature {
|
|||||||
var error: String?
|
var error: String?
|
||||||
var nextDynamicId: Int = 0
|
var nextDynamicId: Int = 0
|
||||||
|
|
||||||
// 是否已初始化
|
// 是否已初始化 - 用于防止重复初始化
|
||||||
var isInitialized = false
|
var isInitialized = false
|
||||||
|
|
||||||
// CreateFeedView 相关状态
|
// CreateFeedView 相关状态 - 简化为布尔值
|
||||||
var isShowingCreateFeed = false
|
var isCreateFeedPresented = false
|
||||||
var createFeedState: CreateFeedFeature.State? = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Action {
|
enum Action {
|
||||||
@@ -27,11 +26,10 @@ struct FeedFeature {
|
|||||||
case clearError
|
case clearError
|
||||||
case retryLoad
|
case retryLoad
|
||||||
|
|
||||||
// CreateFeedView 相关 Action
|
// CreateFeedView 相关 Action - 简化为布尔控制
|
||||||
case showCreateFeed
|
case showCreateFeed
|
||||||
case dismissCreateFeed
|
|
||||||
case createFeedCompleted
|
case createFeedCompleted
|
||||||
indirect case createFeed(CreateFeedFeature.Action)
|
case createFeedDismissed
|
||||||
}
|
}
|
||||||
|
|
||||||
@Dependency(\.apiService) var apiService
|
@Dependency(\.apiService) var apiService
|
||||||
@@ -40,15 +38,19 @@ struct FeedFeature {
|
|||||||
Reduce { state, action in
|
Reduce { state, action in
|
||||||
switch action {
|
switch action {
|
||||||
case .onAppear:
|
case .onAppear:
|
||||||
#if DEBUG
|
// 只在未初始化时才执行首次加载
|
||||||
return .none
|
guard !state.isInitialized else {
|
||||||
#endif
|
return .none
|
||||||
// 只在首次出现时触发加载
|
}
|
||||||
guard !state.isInitialized else { return .none }
|
|
||||||
state.isInitialized = true
|
|
||||||
return .send(.loadLatestMoments)
|
return .send(.loadLatestMoments)
|
||||||
|
|
||||||
case .loadLatestMoments:
|
case .loadLatestMoments:
|
||||||
|
// 添加重复请求防护
|
||||||
|
guard !state.isLoading else {
|
||||||
|
return .none
|
||||||
|
}
|
||||||
|
|
||||||
// 加载最新数据(下拉刷新)
|
// 加载最新数据(下拉刷新)
|
||||||
state.isLoading = true
|
state.isLoading = true
|
||||||
state.error = nil
|
state.error = nil
|
||||||
@@ -58,7 +60,6 @@ struct FeedFeature {
|
|||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
types: [.text, .picture]
|
types: [.text, .picture]
|
||||||
)
|
)
|
||||||
|
|
||||||
return .run { send in
|
return .run { send in
|
||||||
await send(.momentsResponse(TaskResult {
|
await send(.momentsResponse(TaskResult {
|
||||||
try await apiService.request(request)
|
try await apiService.request(request)
|
||||||
@@ -87,50 +88,38 @@ struct FeedFeature {
|
|||||||
case let .momentsResponse(.success(response)):
|
case let .momentsResponse(.success(response)):
|
||||||
state.isLoading = false
|
state.isLoading = false
|
||||||
|
|
||||||
// 添加调试日志
|
// 设置初始化状态
|
||||||
debugInfoSync("📱 FeedFeature: API 响应成功")
|
if !state.isInitialized {
|
||||||
debugInfoSync("📱 FeedFeature: response.code = \(response.code)")
|
state.isInitialized = true
|
||||||
debugInfoSync("📱 FeedFeature: response.message = \(response.message)")
|
}
|
||||||
debugInfoSync("📱 FeedFeature: response.data = \(response.data != nil ? "有数据" : "无数据")")
|
|
||||||
|
|
||||||
// 检查响应状态
|
// 检查响应状态
|
||||||
guard response.code == 200, let data = response.data else {
|
guard response.code == 200, let data = response.data else {
|
||||||
let errorMsg = response.message.isEmpty ? "获取动态失败" : response.message
|
let errorMsg = response.message.isEmpty ? "获取动态失败" : response.message
|
||||||
state.error = errorMsg
|
state.error = errorMsg
|
||||||
debugErrorSync("❌ FeedFeature: API 响应失败 - code: \(response.code), message: \(errorMsg)")
|
|
||||||
return .none
|
return .none
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加数据调试日志
|
|
||||||
debugInfoSync("📱 FeedFeature: data.dynamicList.count = \(data.dynamicList.count)")
|
|
||||||
debugInfoSync("📱 FeedFeature: data.nextDynamicId = \(data.nextDynamicId)")
|
|
||||||
|
|
||||||
// 判断是刷新还是加载更多
|
// 判断是刷新还是加载更多
|
||||||
let isRefresh = state.nextDynamicId == 0
|
let isRefresh = state.nextDynamicId == 0
|
||||||
debugInfoSync("📱 FeedFeature: isRefresh = \(isRefresh)")
|
|
||||||
|
|
||||||
if isRefresh {
|
if isRefresh {
|
||||||
// 刷新:替换所有数据
|
// 刷新:替换所有数据
|
||||||
state.moments = data.dynamicList
|
state.moments = data.dynamicList
|
||||||
debugInfoSync(" FeedFeature: 刷新数据,moments.count = \(state.moments.count)")
|
|
||||||
} else {
|
} else {
|
||||||
// 加载更多:追加到现有数据
|
// 加载更多:追加到现有数据
|
||||||
let oldCount = state.moments.count
|
|
||||||
state.moments.append(contentsOf: data.dynamicList)
|
state.moments.append(contentsOf: data.dynamicList)
|
||||||
debugInfoSync(" FeedFeature: 加载更多,moments.count: \(oldCount) -> \(state.moments.count)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新分页状态
|
// 更新分页状态
|
||||||
state.nextDynamicId = data.nextDynamicId
|
state.nextDynamicId = data.nextDynamicId
|
||||||
state.hasMoreData = !data.dynamicList.isEmpty
|
state.hasMoreData = !data.dynamicList.isEmpty
|
||||||
|
|
||||||
debugInfoSync("📱 FeedFeature: 更新完成 - nextDynamicId: \(state.nextDynamicId), hasMoreData: \(state.hasMoreData)")
|
|
||||||
return .none
|
return .none
|
||||||
|
|
||||||
case let .momentsResponse(.failure(error)):
|
case let .momentsResponse(.failure(error)):
|
||||||
state.isLoading = false
|
state.isLoading = false
|
||||||
state.error = error.localizedDescription
|
state.error = error.localizedDescription
|
||||||
debugErrorSync("❌ FeedFeature: API 请求失败 - \(error.localizedDescription)")
|
|
||||||
return .none
|
return .none
|
||||||
|
|
||||||
case .clearError:
|
case .clearError:
|
||||||
@@ -145,30 +134,20 @@ struct FeedFeature {
|
|||||||
return .send(.loadMoreMoments)
|
return .send(.loadMoreMoments)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateFeedView 相关 Action 处理 - 简化为布尔控制
|
||||||
case .showCreateFeed:
|
case .showCreateFeed:
|
||||||
state.isShowingCreateFeed = true
|
state.isCreateFeedPresented = true
|
||||||
// 初始化 createFeedState
|
|
||||||
state.createFeedState = CreateFeedFeature.State()
|
|
||||||
return .none
|
|
||||||
|
|
||||||
case .dismissCreateFeed:
|
|
||||||
state.isShowingCreateFeed = false
|
|
||||||
state.createFeedState = nil
|
|
||||||
return .none
|
return .none
|
||||||
|
|
||||||
case .createFeedCompleted:
|
case .createFeedCompleted:
|
||||||
state.isShowingCreateFeed = false
|
|
||||||
state.createFeedState = nil
|
|
||||||
// 发布完成后刷新动态列表
|
// 发布完成后刷新动态列表
|
||||||
|
state.isCreateFeedPresented = false
|
||||||
return .send(.loadLatestMoments)
|
return .send(.loadLatestMoments)
|
||||||
case .createFeed:
|
|
||||||
// 子模块 Action 由作用域 reducer 处理
|
case .createFeedDismissed:
|
||||||
|
state.isCreateFeedPresented = false
|
||||||
return .none
|
return .none
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 子模块作用域 reducer
|
|
||||||
self.ifLet(\.createFeedState, action: \.createFeed) {
|
|
||||||
CreateFeedFeature()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -42,21 +42,23 @@ struct HomeFeature {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some ReducerOf<Self> {
|
var body: some ReducerOf<Self> {
|
||||||
Scope(state: \ .settingState, action: \ .setting) {
|
Scope(state: \.settingState, action: \.setting) {
|
||||||
SettingFeature()
|
SettingFeature()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增:Feed Scope
|
// 新增:Feed Scope
|
||||||
Scope(state: \ .feedState, action: \ .feed) {
|
Scope(state: \.feedState, action: \.feed) {
|
||||||
FeedFeature()
|
FeedFeature()
|
||||||
}
|
}
|
||||||
|
|
||||||
Reduce { state, action in
|
Reduce { state, action in
|
||||||
switch action {
|
switch action {
|
||||||
case .onAppear:
|
case .onAppear:
|
||||||
#if DEBUG
|
// 只在未初始化时才执行首次加载
|
||||||
return .none
|
guard !state.isInitialized else {
|
||||||
#endif
|
return .none
|
||||||
|
}
|
||||||
|
|
||||||
state.isInitialized = true
|
state.isInitialized = true
|
||||||
return .concatenate(
|
return .concatenate(
|
||||||
.send(.loadUserInfo),
|
.send(.loadUserInfo),
|
||||||
@@ -106,8 +108,9 @@ struct HomeFeature {
|
|||||||
case .setting:
|
case .setting:
|
||||||
// 由子reducer处理
|
// 由子reducer处理
|
||||||
return .none
|
return .none
|
||||||
|
|
||||||
case .feed(_):
|
case .feed(_):
|
||||||
// FeedFeature 的 action 已由 Scope 自动处理,无需额外处理
|
// FeedFeature 的 action 由 Scope 自动处理
|
||||||
return .none
|
return .none
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -27,9 +27,8 @@ struct IDLoginFeature {
|
|||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
init() {
|
init() {
|
||||||
// 移除测试用的硬编码凭据
|
self.userID = "2356814"
|
||||||
self.userID = ""
|
self.password = "a123456"
|
||||||
self.password = ""
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
@@ -20,6 +20,9 @@ struct LoginFeature {
|
|||||||
var ticketError: String?
|
var ticketError: String?
|
||||||
var loginStep: LoginStep = .initial
|
var loginStep: LoginStep = .initial
|
||||||
|
|
||||||
|
// 新增:初始化状态管理 - 防止重复执行
|
||||||
|
var isInitialized = false
|
||||||
|
|
||||||
// 新增:任一登录方式完成时为 true
|
// 新增:任一登录方式完成时为 true
|
||||||
var isAnyLoginCompleted: Bool {
|
var isAnyLoginCompleted: Bool {
|
||||||
idLoginState.loginStep == .completed || emailLoginState.loginStep == .completed
|
idLoginState.loginStep == .completed || emailLoginState.loginStep == .completed
|
||||||
@@ -43,6 +46,7 @@ struct LoginFeature {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum Action {
|
enum Action {
|
||||||
|
case onAppear
|
||||||
case updateAccount(String)
|
case updateAccount(String)
|
||||||
case updatePassword(String)
|
case updatePassword(String)
|
||||||
case login
|
case login
|
||||||
@@ -75,6 +79,19 @@ struct LoginFeature {
|
|||||||
|
|
||||||
Reduce { state, action in
|
Reduce { state, action in
|
||||||
switch action {
|
switch action {
|
||||||
|
case .onAppear:
|
||||||
|
// 防止重复初始化
|
||||||
|
guard !state.isInitialized else {
|
||||||
|
debugInfoSync("🚀 LoginFeature: 已初始化,跳过重复执行")
|
||||||
|
return .none
|
||||||
|
}
|
||||||
|
|
||||||
|
state.isInitialized = true
|
||||||
|
debugInfoSync("🚀 LoginFeature: 首次初始化")
|
||||||
|
|
||||||
|
// 登录页面出现时的初始化逻辑
|
||||||
|
return .none
|
||||||
|
|
||||||
case let .updateAccount(account):
|
case let .updateAccount(account):
|
||||||
state.account = account
|
state.account = account
|
||||||
return .none
|
return .none
|
||||||
@@ -213,7 +230,9 @@ struct LoginFeature {
|
|||||||
state.accountModel = nil // 清除 AccountModel
|
state.accountModel = nil // 清除 AccountModel
|
||||||
state.loginStep = .initial
|
state.loginStep = .initial
|
||||||
// Effect 清除本地存储的认证信息
|
// Effect 清除本地存储的认证信息
|
||||||
return .run { _ in await UserInfoManager.clearAllAuthenticationData() }
|
return .run { _ in
|
||||||
|
await UserInfoManager.clearAllAuthenticationData()
|
||||||
|
}
|
||||||
|
|
||||||
case .idLogin:
|
case .idLogin:
|
||||||
// IDLogin动作由子feature处理
|
// IDLogin动作由子feature处理
|
||||||
|
@@ -9,6 +9,15 @@ struct SplashFeature {
|
|||||||
var shouldShowMainApp = false
|
var shouldShowMainApp = false
|
||||||
var authenticationStatus: UserInfoManager.AuthenticationStatus = .notFound
|
var authenticationStatus: UserInfoManager.AuthenticationStatus = .notFound
|
||||||
var isCheckingAuthentication = false
|
var isCheckingAuthentication = false
|
||||||
|
|
||||||
|
// 新增:导航目标
|
||||||
|
var navigationDestination: NavigationDestination?
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增:导航目标枚举
|
||||||
|
enum NavigationDestination: Equatable {
|
||||||
|
case login // 跳转到登录页面
|
||||||
|
case main // 跳转到主页面
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Action: Equatable {
|
enum Action: Equatable {
|
||||||
@@ -16,6 +25,10 @@ struct SplashFeature {
|
|||||||
case splashFinished
|
case splashFinished
|
||||||
case checkAuthentication
|
case checkAuthentication
|
||||||
case authenticationChecked(UserInfoManager.AuthenticationStatus)
|
case authenticationChecked(UserInfoManager.AuthenticationStatus)
|
||||||
|
|
||||||
|
// 新增:导航 actions
|
||||||
|
case navigateToLogin
|
||||||
|
case navigateToMain
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some ReducerOf<Self> {
|
var body: some ReducerOf<Self> {
|
||||||
@@ -26,6 +39,7 @@ struct SplashFeature {
|
|||||||
state.shouldShowMainApp = false
|
state.shouldShowMainApp = false
|
||||||
state.authenticationStatus = UserInfoManager.AuthenticationStatus.notFound
|
state.authenticationStatus = UserInfoManager.AuthenticationStatus.notFound
|
||||||
state.isCheckingAuthentication = false
|
state.isCheckingAuthentication = false
|
||||||
|
state.navigationDestination = nil
|
||||||
|
|
||||||
// 1秒延迟后显示主应用 (iOS 15.5+ 兼容)
|
// 1秒延迟后显示主应用 (iOS 15.5+ 兼容)
|
||||||
return .run { send in
|
return .run { send in
|
||||||
@@ -34,7 +48,6 @@ struct SplashFeature {
|
|||||||
}
|
}
|
||||||
case .splashFinished:
|
case .splashFinished:
|
||||||
state.isLoading = false
|
state.isLoading = false
|
||||||
state.shouldShowMainApp = true
|
|
||||||
|
|
||||||
// Splash 完成后,开始检查认证状态
|
// Splash 完成后,开始检查认证状态
|
||||||
return .send(.checkAuthentication)
|
return .send(.checkAuthentication)
|
||||||
@@ -49,20 +62,25 @@ struct SplashFeature {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case let .authenticationChecked(status):
|
case let .authenticationChecked(status):
|
||||||
#if DEBUG
|
|
||||||
debugInfoSync("🔑 需要手动登录")
|
|
||||||
return .none
|
|
||||||
#endif
|
|
||||||
state.isCheckingAuthentication = false
|
state.isCheckingAuthentication = false
|
||||||
state.authenticationStatus = status
|
state.authenticationStatus = status
|
||||||
|
|
||||||
// 根据认证状态发送相应的导航通知
|
// 根据认证状态决定导航目标
|
||||||
if status.canAutoLogin {
|
if status.canAutoLogin {
|
||||||
debugInfoSync("🎉 自动登录成功,进入主页")
|
debugInfoSync("🎉 自动登录成功,进入主页")
|
||||||
|
return .send(.navigateToMain)
|
||||||
} else {
|
} else {
|
||||||
debugInfoSync("🔑 需要手动登录")
|
debugInfoSync("🔑 需要手动登录")
|
||||||
|
return .send(.navigateToLogin)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case .navigateToLogin:
|
||||||
|
state.navigationDestination = .login
|
||||||
|
return .none
|
||||||
|
|
||||||
|
case .navigateToMain:
|
||||||
|
state.navigationDestination = .main
|
||||||
|
state.shouldShowMainApp = true
|
||||||
return .none
|
return .none
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
137
yana/Utils/COSManager.swift
Normal file
137
yana/Utils/COSManager.swift
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
@@ -6,170 +6,168 @@ struct CreateFeedView: View {
|
|||||||
let store: StoreOf<CreateFeedFeature>
|
let store: StoreOf<CreateFeedFeature>
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
WithPerceptionTracking {
|
NavigationStack {
|
||||||
NavigationStack {
|
GeometryReader { geometry in
|
||||||
GeometryReader { geometry in
|
ZStack {
|
||||||
ZStack {
|
// 背景渐变
|
||||||
// 背景渐变
|
LinearGradient(
|
||||||
LinearGradient(
|
gradient: Gradient(colors: [
|
||||||
gradient: Gradient(colors: [
|
Color(red: 0.1, green: 0.1, blue: 0.2),
|
||||||
Color(red: 0.1, green: 0.1, blue: 0.2),
|
Color(red: 0.2, green: 0.1, blue: 0.3)
|
||||||
Color(red: 0.2, green: 0.1, blue: 0.3)
|
]),
|
||||||
]),
|
startPoint: .topLeading,
|
||||||
startPoint: .topLeading,
|
endPoint: .bottomTrailing
|
||||||
endPoint: .bottomTrailing
|
)
|
||||||
)
|
.ignoresSafeArea()
|
||||||
.ignoresSafeArea()
|
|
||||||
|
ScrollView {
|
||||||
ScrollView {
|
VStack(spacing: 20) {
|
||||||
VStack(spacing: 20) {
|
// 内容输入区域
|
||||||
// 内容输入区域
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
// 文本输入框
|
||||||
// 文本输入框
|
ZStack(alignment: .topLeading) {
|
||||||
ZStack(alignment: .topLeading) {
|
RoundedRectangle(cornerRadius: 12)
|
||||||
RoundedRectangle(cornerRadius: 12)
|
.fill(Color.white.opacity(0.1))
|
||||||
.fill(Color.white.opacity(0.1))
|
.frame(minHeight: 120)
|
||||||
.frame(minHeight: 120)
|
|
||||||
|
if store.content.isEmpty {
|
||||||
if store.content.isEmpty {
|
Text("Enter Content")
|
||||||
Text("Enter Content")
|
.foregroundColor(.white.opacity(0.5))
|
||||||
.foregroundColor(.white.opacity(0.5))
|
.padding(.horizontal, 16)
|
||||||
.padding(.horizontal, 16)
|
.padding(.vertical, 12)
|
||||||
.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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 字符计数
|
TextEditor(text: .init(
|
||||||
HStack {
|
get: { store.content },
|
||||||
Spacer()
|
set: { store.send(.contentChanged($0)) }
|
||||||
Text("\(store.characterCount)/500")
|
))
|
||||||
.font(.system(size: 12))
|
.foregroundColor(.white)
|
||||||
.foregroundColor(
|
.background(Color.clear)
|
||||||
store.characterCount > 500 ? .red : .white.opacity(0.6)
|
.padding(.horizontal, 12)
|
||||||
)
|
.padding(.vertical, 8)
|
||||||
}
|
.scrollContentBackground(.hidden)
|
||||||
}
|
|
||||||
.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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 错误提示
|
// 字符计数
|
||||||
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 {
|
HStack {
|
||||||
if store.isLoading {
|
Spacer()
|
||||||
ProgressView()
|
Text("\(store.characterCount)/500")
|
||||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
.font(.system(size: 12))
|
||||||
.scaleEffect(0.8)
|
.foregroundColor(
|
||||||
Text("发布中...")
|
store.characterCount > 500 ? .red : .white.opacity(0.6)
|
||||||
.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)
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 20)
|
.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)
|
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))
|
.padding(.horizontal, 20)
|
||||||
.disabled(!store.canPublish || store.isLoading)
|
.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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,14 +5,12 @@ import Combine
|
|||||||
struct EMailLoginView: View {
|
struct EMailLoginView: View {
|
||||||
let store: StoreOf<EMailLoginFeature>
|
let store: StoreOf<EMailLoginFeature>
|
||||||
let onBack: () -> Void
|
let onBack: () -> Void
|
||||||
|
@Binding var showEmailLogin: Bool
|
||||||
|
|
||||||
// 使用本地@State管理UI状态
|
|
||||||
@State private var email: String = ""
|
@State private var email: String = ""
|
||||||
@State private var verificationCode: String = ""
|
@State private var verificationCode: String = ""
|
||||||
@State private var codeCountdown: Int = 0
|
@State private var codeCountdown: Int = 0
|
||||||
@State private var timerCancellable: AnyCancellable?
|
@State private var timerCancellable: AnyCancellable?
|
||||||
|
|
||||||
// 管理输入框焦点状态
|
|
||||||
@FocusState private var focusedField: Field?
|
@FocusState private var focusedField: Field?
|
||||||
|
|
||||||
enum Field {
|
enum Field {
|
||||||
@@ -20,12 +18,9 @@ struct EMailLoginView: View {
|
|||||||
case verificationCode
|
case verificationCode
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算登录按钮是否可用
|
|
||||||
private var isLoginButtonEnabled: Bool {
|
private var isLoginButtonEnabled: Bool {
|
||||||
return !store.isLoading && !email.isEmpty && !verificationCode.isEmpty
|
return !store.isLoading && !email.isEmpty && !verificationCode.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算获取验证码按钮文本
|
|
||||||
private var getCodeButtonText: String {
|
private var getCodeButtonText: String {
|
||||||
if store.isCodeLoading {
|
if store.isCodeLoading {
|
||||||
return ""
|
return ""
|
||||||
@@ -35,188 +30,38 @@ struct EMailLoginView: View {
|
|||||||
return NSLocalizedString("email_login.get_code", comment: "")
|
return NSLocalizedString("email_login.get_code", comment: "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算获取验证码按钮是否可用
|
|
||||||
private var isCodeButtonEnabled: Bool {
|
private var isCodeButtonEnabled: Bool {
|
||||||
return !store.isCodeLoading && codeCountdown == 0
|
return !store.isCodeLoading && codeCountdown == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
// WithViewStore(store, observe: { $0 }) { _ in
|
WithViewStore(store, observe: { $0.loginStep }) { viewStore in
|
||||||
GeometryReader { geometry in
|
LoginContentView(
|
||||||
WithPerceptionTracking {
|
store: store,
|
||||||
ZStack {
|
onBack: onBack,
|
||||||
// 背景图片
|
email: $email,
|
||||||
Image("bg")
|
verificationCode: $verificationCode,
|
||||||
.resizable()
|
codeCountdown: $codeCountdown,
|
||||||
.aspectRatio(contentMode: .fill)
|
focusedField: $focusedField,
|
||||||
.ignoresSafeArea(.all)
|
isLoginButtonEnabled: isLoginButtonEnabled,
|
||||||
|
getCodeButtonText: getCodeButtonText,
|
||||||
VStack(spacing: 0) {
|
isCodeButtonEnabled: isCodeButtonEnabled
|
||||||
// 顶部导航栏
|
)
|
||||||
HStack {
|
.onChange(of: viewStore.state) { newStep in
|
||||||
Button(action: {
|
debugInfoSync("🔄 EMailLoginView: loginStep 变化为 \(newStep)")
|
||||||
onBack()
|
if newStep == .completed {
|
||||||
}) {
|
debugInfoSync("✅ EMailLoginView: 登录成功,准备关闭自身")
|
||||||
Image(systemName: "chevron.left")
|
showEmailLogin = false
|
||||||
.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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// }
|
|
||||||
.onAppear {
|
.onAppear {
|
||||||
let _ = WithPerceptionTracking {
|
let _ = WithPerceptionTracking {
|
||||||
// 每次进入页面都重置状态
|
|
||||||
store.send(.resetState)
|
store.send(.resetState)
|
||||||
|
|
||||||
email = ""
|
email = ""
|
||||||
verificationCode = ""
|
verificationCode = ""
|
||||||
codeCountdown = 0
|
codeCountdown = 0
|
||||||
stopCountdown()
|
stopCountdown()
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
email = "exzero@126.com"
|
email = "exzero@126.com"
|
||||||
store.send(.emailChanged(email))
|
store.send(.emailChanged(email))
|
||||||
@@ -240,7 +85,6 @@ struct EMailLoginView: View {
|
|||||||
}
|
}
|
||||||
.onChange(of: store.isCodeLoading) { isCodeLoading in
|
.onChange(of: store.isCodeLoading) { isCodeLoading in
|
||||||
let _ = WithPerceptionTracking {
|
let _ = WithPerceptionTracking {
|
||||||
// 当API请求完成且成功时,自动将焦点切换到验证码输入框
|
|
||||||
if !isCodeLoading && store.errorMessage == nil {
|
if !isCodeLoading && store.errorMessage == nil {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||||
focusedField = .verificationCode
|
focusedField = .verificationCode
|
||||||
@@ -250,14 +94,9 @@ struct EMailLoginView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 倒计时管理
|
|
||||||
private func startCountdown() {
|
private func startCountdown() {
|
||||||
stopCountdown()
|
stopCountdown()
|
||||||
|
|
||||||
// 立即设置倒计时
|
|
||||||
codeCountdown = 60
|
codeCountdown = 60
|
||||||
|
|
||||||
// 使用 SwiftUI 原生的 Timer.publish 方式
|
|
||||||
timerCancellable = Timer.publish(every: 1.0, on: .main, in: .common)
|
timerCancellable = Timer.publish(every: 1.0, on: .main, in: .common)
|
||||||
.autoconnect()
|
.autoconnect()
|
||||||
.sink { _ in
|
.sink { _ in
|
||||||
@@ -268,13 +107,159 @@ struct EMailLoginView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func stopCountdown() {
|
private func stopCountdown() {
|
||||||
timerCancellable?.cancel()
|
timerCancellable?.cancel()
|
||||||
timerCancellable = nil
|
timerCancellable = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct LoginContentView: View {
|
||||||
|
let store: StoreOf<EMailLoginFeature>
|
||||||
|
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 {
|
//#Preview {
|
||||||
// EMailLoginView(
|
// EMailLoginView(
|
||||||
// store: Store(
|
// store: Store(
|
||||||
@@ -282,6 +267,7 @@ struct EMailLoginView: View {
|
|||||||
// ) {
|
// ) {
|
||||||
// EMailLoginFeature()
|
// EMailLoginFeature()
|
||||||
// },
|
// },
|
||||||
// onBack: {}
|
// onBack: {},
|
||||||
|
// showEmailLogin: .constant(true)
|
||||||
// )
|
// )
|
||||||
//}
|
//}
|
||||||
|
@@ -24,34 +24,32 @@ struct FeedTopBarView: View {
|
|||||||
struct FeedMomentsListView: View {
|
struct FeedMomentsListView: View {
|
||||||
let store: StoreOf<FeedFeature>
|
let store: StoreOf<FeedFeature>
|
||||||
var body: some View {
|
var body: some View {
|
||||||
WithPerceptionTracking {
|
LazyVStack(spacing: 16) {
|
||||||
LazyVStack(spacing: 16) {
|
if store.moments.isEmpty {
|
||||||
if store.moments.isEmpty {
|
VStack(spacing: 12) {
|
||||||
VStack(spacing: 12) {
|
Image(systemName: "heart.text.square")
|
||||||
Image(systemName: "heart.text.square")
|
.font(.system(size: 40))
|
||||||
.font(.system(size: 40))
|
.foregroundColor(.white.opacity(0.6))
|
||||||
.foregroundColor(.white.opacity(0.6))
|
Text("暂无动态内容")
|
||||||
Text("暂无动态内容")
|
.font(.system(size: 16))
|
||||||
.font(.system(size: 16))
|
.foregroundColor(.white.opacity(0.8))
|
||||||
.foregroundColor(.white.opacity(0.8))
|
if let error = store.error {
|
||||||
if let error = store.error {
|
Text("错误: \(error)")
|
||||||
Text("错误: \(error)")
|
.font(.system(size: 12))
|
||||||
.font(.system(size: 12))
|
.foregroundColor(.red.opacity(0.8))
|
||||||
.foregroundColor(.red.opacity(0.8))
|
.multilineTextAlignment(.center)
|
||||||
.multilineTextAlignment(.center)
|
.padding(.horizontal, 20)
|
||||||
.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(.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)
|
.padding(.horizontal, 16)
|
||||||
@@ -63,50 +61,52 @@ struct FeedView: View {
|
|||||||
let store: StoreOf<FeedFeature>
|
let store: StoreOf<FeedFeature>
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
WithPerceptionTracking {
|
GeometryReader { geometry in
|
||||||
GeometryReader { geometry in
|
ScrollView {
|
||||||
ScrollView {
|
VStack(spacing: 20) {
|
||||||
VStack(spacing: 20) {
|
FeedTopBarView(store: store)
|
||||||
FeedTopBarView(store: store)
|
Image(systemName: "heart.fill")
|
||||||
Image(systemName: "heart.fill")
|
.font(.system(size: 60))
|
||||||
.font(.system(size: 60))
|
.foregroundColor(.red)
|
||||||
.foregroundColor(.red)
|
.padding(.top, 40)
|
||||||
.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.")
|
||||||
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))
|
||||||
.font(.system(size: 16))
|
.multilineTextAlignment(.center)
|
||||||
.multilineTextAlignment(.center)
|
.foregroundColor(.white.opacity(0.9))
|
||||||
.foregroundColor(.white.opacity(0.9))
|
.padding(.horizontal, 30)
|
||||||
.padding(.horizontal, 30)
|
.padding(.top, 20)
|
||||||
.padding(.top, 20)
|
FeedMomentsListView(store: store)
|
||||||
FeedMomentsListView(store: store)
|
if store.isLoading {
|
||||||
if store.isLoading {
|
HStack {
|
||||||
HStack {
|
ProgressView()
|
||||||
ProgressView()
|
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
Text("加载中...")
|
||||||
Text("加载中...")
|
.font(.system(size: 14))
|
||||||
.font(.system(size: 14))
|
.foregroundColor(.white.opacity(0.8))
|
||||||
.foregroundColor(.white.opacity(0.8))
|
|
||||||
}
|
|
||||||
.padding(.top, 20)
|
|
||||||
}
|
}
|
||||||
Color.clear.frame(height: geometry.safeAreaInsets.bottom + 100)
|
.padding(.top, 20)
|
||||||
}
|
|
||||||
}
|
|
||||||
.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)
|
|
||||||
}
|
}
|
||||||
|
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 imageSize: CGFloat = (availableWidth - spacing * 2) / 3
|
||||||
let columns = Array(repeating: GridItem(.fixed(imageSize), spacing: spacing), count: 3)
|
let columns = Array(repeating: GridItem(.fixed(imageSize), spacing: spacing), count: 3)
|
||||||
|
|
||||||
WithPerceptionTracking {
|
LazyVGrid(columns: columns, spacing: spacing) {
|
||||||
LazyVGrid(columns: columns, spacing: spacing) {
|
ForEach(images.prefix(9), id: \.id) { image in
|
||||||
ForEach(images.prefix(9), id: \.id) { image in
|
SquareImageView(image: image, size: imageSize)
|
||||||
SquareImageView(image: image, size: imageSize)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -407,25 +405,23 @@ struct RealDynamicCardView: View {
|
|||||||
|
|
||||||
// 图片网格
|
// 图片网格
|
||||||
if let images = moment.dynamicResList, !images.isEmpty {
|
if let images = moment.dynamicResList, !images.isEmpty {
|
||||||
WithPerceptionTracking {
|
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: min(images.count, 3)), spacing: 8) {
|
||||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: min(images.count, 3)), spacing: 8) {
|
ForEach(images.prefix(9), id: \.id) { image in
|
||||||
ForEach(images.prefix(9), id: \.id) { image in
|
AsyncImage(url: URL(string: image.resUrl)) { imageView in
|
||||||
AsyncImage(url: URL(string: image.resUrl)) { imageView in
|
imageView
|
||||||
imageView
|
.resizable()
|
||||||
.resizable()
|
.aspectRatio(contentMode: .fill)
|
||||||
.aspectRatio(contentMode: .fill)
|
} placeholder: {
|
||||||
} placeholder: {
|
Rectangle()
|
||||||
Rectangle()
|
.fill(Color.gray.opacity(0.3))
|
||||||
.fill(Color.gray.opacity(0.3))
|
.overlay(
|
||||||
.overlay(
|
ProgressView()
|
||||||
ProgressView()
|
.progressViewStyle(CircularProgressViewStyle(tint: .white.opacity(0.6)))
|
||||||
.progressViewStyle(CircularProgressViewStyle(tint: .white.opacity(0.6)))
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
.frame(height: 100)
|
|
||||||
.clipped()
|
|
||||||
.cornerRadius(8)
|
|
||||||
}
|
}
|
||||||
|
.frame(height: 100)
|
||||||
|
.clipped()
|
||||||
|
.cornerRadius(8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -521,17 +517,15 @@ struct DynamicCardView: View {
|
|||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
|
|
||||||
// 图片网格
|
// 图片网格
|
||||||
WithPerceptionTracking {
|
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 3), spacing: 8) {
|
||||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 3), spacing: 8) {
|
ForEach(0..<3) { imageIndex in
|
||||||
ForEach(0..<3) { imageIndex in
|
Rectangle()
|
||||||
Rectangle()
|
.fill(Color.gray.opacity(0.3))
|
||||||
.fill(Color.gray.opacity(0.3))
|
.aspectRatio(1, contentMode: .fit)
|
||||||
.aspectRatio(1, contentMode: .fit)
|
.overlay(
|
||||||
.overlay(
|
Image(systemName: "photo")
|
||||||
Image(systemName: "photo")
|
.foregroundColor(.white.opacity(0.6))
|
||||||
.foregroundColor(.white.opacity(0.6))
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -569,10 +563,10 @@ struct DynamicCardView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
//#Preview {
|
||||||
FeedView(
|
// FeedView(
|
||||||
store: Store(initialState: FeedFeature.State()) {
|
// store: Store(initialState: FeedFeature.State()) {
|
||||||
FeedFeature()
|
// FeedFeature()
|
||||||
}
|
// }
|
||||||
)
|
// )
|
||||||
}
|
//}
|
||||||
|
@@ -8,59 +8,59 @@ struct HomeView: View {
|
|||||||
@State private var selectedTab: Tab = .feed
|
@State private var selectedTab: Tab = .feed
|
||||||
|
|
||||||
var body: some View {
|
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 {
|
ZStack {
|
||||||
// 使用 "bg" 图片作为背景 - 全屏显示
|
switch selectedTab {
|
||||||
Image("bg")
|
case .feed:
|
||||||
.resizable()
|
NavigationStack {
|
||||||
.aspectRatio(contentMode: .fill)
|
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
||||||
.clipped()
|
|
||||||
.ignoresSafeArea(.all)
|
|
||||||
|
|
||||||
// 主要内容区域 - 全屏显示
|
|
||||||
ZStack {
|
|
||||||
switch selectedTab {
|
|
||||||
case .feed:
|
|
||||||
FeedView(
|
FeedView(
|
||||||
store: store.scope(state: \.feedState, action: \.feed)
|
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)
|
.onAppear {
|
||||||
}
|
store.send(.onAppear)
|
||||||
.sheet(isPresented: Binding(
|
}
|
||||||
get: { store.isSettingPresented },
|
.sheet(isPresented: Binding(
|
||||||
set: { _ in store.send(.settingDismissed) }
|
get: { store.isSettingPresented },
|
||||||
)) {
|
set: { _ in store.send(.settingDismissed) }
|
||||||
SettingView(store: store.scope(state: \.settingState, action: \.setting))
|
)) {
|
||||||
}
|
SettingView(store: store.scope(state: \.settingState, action: \.setting))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
//#Preview {
|
||||||
HomeView(
|
// HomeView(
|
||||||
store: Store(
|
// store: Store(
|
||||||
initialState: HomeFeature.State()
|
// initialState: HomeFeature.State()
|
||||||
) {
|
// ) {
|
||||||
HomeFeature()
|
// HomeFeature()
|
||||||
}, onLogout: {}
|
// }, onLogout: {}
|
||||||
)
|
// )
|
||||||
}
|
//}
|
||||||
|
@@ -5,6 +5,7 @@ import Perception
|
|||||||
struct IDLoginView: View {
|
struct IDLoginView: View {
|
||||||
let store: StoreOf<IDLoginFeature>
|
let store: StoreOf<IDLoginFeature>
|
||||||
let onBack: () -> Void
|
let onBack: () -> Void
|
||||||
|
@Binding var showIDLogin: Bool // 新增:绑定父视图的显示状态
|
||||||
|
|
||||||
// 使用本地@State管理UI状态
|
// 使用本地@State管理UI状态
|
||||||
@State private var userID: String = ""
|
@State private var userID: String = ""
|
||||||
@@ -20,199 +21,209 @@ struct IDLoginView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { geometry in
|
WithViewStore(store, observe: { $0.loginStep }) { viewStore in
|
||||||
WithPerceptionTracking {
|
GeometryReader { geometry in
|
||||||
ZStack {
|
WithPerceptionTracking {
|
||||||
// 背景图片 - 使用与登录页面相同的"bg"
|
ZStack {
|
||||||
Image("bg")
|
// 背景图片 - 使用与登录页面相同的"bg"
|
||||||
.resizable()
|
Image("bg")
|
||||||
.aspectRatio(contentMode: .fill)
|
.resizable()
|
||||||
.ignoresSafeArea(.all)
|
.aspectRatio(contentMode: .fill)
|
||||||
|
.ignoresSafeArea(.all)
|
||||||
VStack(spacing: 0) {
|
|
||||||
// 顶部导航栏
|
VStack(spacing: 0) {
|
||||||
HStack {
|
// 顶部导航栏
|
||||||
Button(action: {
|
HStack {
|
||||||
onBack()
|
Button(action: {
|
||||||
}) {
|
onBack()
|
||||||
Image(systemName: "chevron.left")
|
}) {
|
||||||
.font(.system(size: 24, weight: .medium))
|
Image(systemName: "chevron.left")
|
||||||
.foregroundColor(.white)
|
.font(.system(size: 24, weight: .medium))
|
||||||
.frame(width: 44, height: 44)
|
.foregroundColor(.white)
|
||||||
|
.frame(width: 44, height: 44)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
}
|
}
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.padding(.top, 8)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
.frame(height: 60)
|
||||||
.padding(.horizontal, 16)
|
|
||||||
.padding(.top, 8)
|
// 标题
|
||||||
|
Text(NSLocalizedString("id_login.title", comment: ""))
|
||||||
Spacer()
|
.font(.system(size: 28, weight: .medium))
|
||||||
.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))
|
|
||||||
}
|
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.font(.system(size: 16))
|
.padding(.bottom, 80)
|
||||||
.padding(.horizontal, 24)
|
|
||||||
.keyboardType(.numberPad)
|
// 输入框区域
|
||||||
|
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()
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
.onAppear {
|
||||||
.navigationBarHidden(true)
|
let _ = WithPerceptionTracking {
|
||||||
// 使用与 LoginView 一致的 navigationDestination 方式
|
// 初始化时同步TCA状态到本地状态
|
||||||
.navigationDestination(isPresented: $showRecoverPassword) {
|
userID = store.userID
|
||||||
WithPerceptionTracking {
|
password = store.password
|
||||||
RecoverPasswordView(
|
isPasswordVisible = store.isPasswordVisible
|
||||||
store: Store(
|
|
||||||
initialState: RecoverPasswordFeature.State()
|
#if DEBUG
|
||||||
) {
|
// 移除测试用的硬编码凭据
|
||||||
RecoverPasswordFeature()
|
debugInfoSync("🐛 Debug模式: 已移除硬编码测试凭据")
|
||||||
},
|
#endif
|
||||||
onBack: {
|
}
|
||||||
showRecoverPassword = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.navigationBarHidden(true)
|
|
||||||
}
|
}
|
||||||
}
|
// 新增:监听登录状态,成功后自动关闭自身
|
||||||
.onAppear {
|
.onChange(of: viewStore.state) { newStep in
|
||||||
let _ = WithPerceptionTracking {
|
debugInfoSync("🔄 IDLoginView: loginStep 变化为 \(newStep)")
|
||||||
// 初始化时同步TCA状态到本地状态
|
if newStep == .completed {
|
||||||
userID = store.userID
|
debugInfoSync("✅ IDLoginView: 登录成功,准备关闭自身")
|
||||||
password = store.password
|
showIDLogin = false
|
||||||
isPasswordVisible = store.isPasswordVisible
|
}
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
// 移除测试用的硬编码凭据
|
|
||||||
debugInfoSync("🐛 Debug模式: 已移除硬编码测试凭据")
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -225,6 +236,7 @@ struct IDLoginView: View {
|
|||||||
// ) {
|
// ) {
|
||||||
// IDLoginFeature()
|
// IDLoginFeature()
|
||||||
// },
|
// },
|
||||||
// onBack: {}
|
// onBack: {},
|
||||||
|
// showIDLogin: .constant(true)
|
||||||
// )
|
// )
|
||||||
//}
|
//}
|
||||||
|
@@ -3,8 +3,12 @@ import ComposableArchitecture
|
|||||||
|
|
||||||
struct LanguageSettingsView: View {
|
struct LanguageSettingsView: View {
|
||||||
@ObservedObject private var localizationManager = LocalizationManager.shared
|
@ObservedObject private var localizationManager = LocalizationManager.shared
|
||||||
|
@StateObject private var cosManager = COSManager.shared
|
||||||
@Binding var isPresented: Bool
|
@Binding var isPresented: Bool
|
||||||
|
|
||||||
|
// 使用 TCA 的依赖注入获取 API 服务
|
||||||
|
@Dependency(\.apiService) private var apiService
|
||||||
|
|
||||||
init(isPresented: Binding<Bool> = .constant(true)) {
|
init(isPresented: Binding<Bool> = .constant(true)) {
|
||||||
self._isPresented = isPresented
|
self._isPresented = isPresented
|
||||||
}
|
}
|
||||||
@@ -43,10 +47,66 @@ struct LanguageSettingsView: View {
|
|||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.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")
|
.navigationTitle("语言设置 / Language")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.navigationBarBackButtonHidden(true)
|
.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
|
// MARK: - Preview
|
||||||
#Preview {
|
//#Preview {
|
||||||
LanguageSettingsView(isPresented: .constant(true))
|
// LanguageSettingsView(isPresented: .constant(true))
|
||||||
}
|
//}
|
||||||
|
@@ -133,7 +133,8 @@ struct LoginView: View {
|
|||||||
),
|
),
|
||||||
onBack: {
|
onBack: {
|
||||||
showIDLogin = false
|
showIDLogin = false
|
||||||
}
|
},
|
||||||
|
showIDLogin: $showIDLogin // 新增:传递Binding
|
||||||
)
|
)
|
||||||
.navigationBarHidden(true)
|
.navigationBarHidden(true)
|
||||||
}
|
}
|
||||||
@@ -147,7 +148,8 @@ struct LoginView: View {
|
|||||||
),
|
),
|
||||||
onBack: {
|
onBack: {
|
||||||
showEmailLogin = false
|
showEmailLogin = false
|
||||||
}
|
},
|
||||||
|
showEmailLogin: $showEmailLogin // 新增:传递Binding
|
||||||
)
|
)
|
||||||
.navigationBarHidden(true)
|
.navigationBarHidden(true)
|
||||||
}
|
}
|
||||||
@@ -169,10 +171,20 @@ struct LoginView: View {
|
|||||||
)
|
)
|
||||||
// 新增:监听登录成功,调用回调
|
// 新增:监听登录成功,调用回调
|
||||||
.onChange(of: viewStore.state) { completed in
|
.onChange(of: viewStore.state) { completed in
|
||||||
WithPerceptionTracking {
|
if completed {
|
||||||
if completed {
|
onLoginSuccess()
|
||||||
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,29 +6,40 @@ struct SplashView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
WithPerceptionTracking {
|
WithPerceptionTracking {
|
||||||
ZStack {
|
Group {
|
||||||
// 背景图片 - 全屏显示
|
// 根据导航目标显示不同页面
|
||||||
Image("bg")
|
if let navigationDestination = store.navigationDestination {
|
||||||
.resizable()
|
switch navigationDestination {
|
||||||
.aspectRatio(contentMode: .fill)
|
case .login:
|
||||||
.ignoresSafeArea(.all)
|
// 显示登录页面
|
||||||
|
LoginView(
|
||||||
VStack(spacing: 32) {
|
store: Store(
|
||||||
Spacer()
|
initialState: LoginFeature.State()
|
||||||
.frame(height: 200) // 与 storyboard 中的约束对应
|
) {
|
||||||
|
LoginFeature()
|
||||||
// Logo 图片 - 100x100
|
},
|
||||||
Image("logo")
|
onLoginSuccess: {
|
||||||
.resizable()
|
// 登录成功后导航到主页面
|
||||||
.aspectRatio(contentMode: .fit)
|
store.send(.navigateToMain)
|
||||||
.frame(width: 100, height: 100)
|
}
|
||||||
|
)
|
||||||
// 应用标题 - 白色,40pt字体
|
case .main:
|
||||||
Text("E-Parti")
|
// 显示主应用页面
|
||||||
.font(.system(size: 40, weight: .regular))
|
HomeView(
|
||||||
.foregroundColor(.white)
|
store: Store(
|
||||||
|
initialState: HomeFeature.State()
|
||||||
Spacer()
|
) {
|
||||||
|
HomeFeature()
|
||||||
|
},
|
||||||
|
onLogout: {
|
||||||
|
// 登出时重新导航到登录页面
|
||||||
|
store.send(.navigateToLogin)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 显示启动画面
|
||||||
|
splashContent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.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 {
|
//#Preview {
|
||||||
SplashView(
|
// SplashView(
|
||||||
store: Store(
|
// store: Store(
|
||||||
initialState: SplashFeature.State()
|
// initialState: SplashFeature.State()
|
||||||
) {
|
// ) {
|
||||||
SplashFeature()
|
// SplashFeature()
|
||||||
}
|
// }
|
||||||
)
|
// )
|
||||||
}
|
//}
|
||||||
|
@@ -20,13 +20,17 @@ struct yanaApp: App {
|
|||||||
// 不是在Previews环境中运行
|
// 不是在Previews环境中运行
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
debugInfoSync("🛠 原生URLSession测试开始")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
AppRootView()
|
SplashView(
|
||||||
|
store: Store(
|
||||||
|
initialState: SplashFeature.State()
|
||||||
|
) {
|
||||||
|
SplashFeature()
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user