Compare commits
13 Commits
e-party/tc
...
3d00e459e3
Author | SHA1 | Date | |
---|---|---|---|
![]() |
3d00e459e3 | ||
![]() |
3ec1b1302f | ||
![]() |
567b1f3fd9 | ||
![]() |
30c3e530fb | ||
![]() |
6a9dd3fe52 | ||
![]() |
cbad4fb50d | ||
![]() |
62dcf591f0 | ||
![]() |
f9ff572a30 | ||
![]() |
2a607e246c | ||
![]() |
488c6fc7ab | ||
![]() |
d35071d3de | ||
![]() |
e286229f6f | ||
![]() |
de2f05f545 |
@@ -1,39 +1,45 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
Description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
# CONTEXT
|
||||
# Background
|
||||
|
||||
This project based on iOS 16.0+ & SwiftUI & TCA 1.20.2
|
||||
This project is based on iOS 17.0+, SwiftUI, and TCA 1.20.2
|
||||
|
||||
I wish to receive advice using the latest tools and seek step-by-step guidance to fully understand the implementation process.
|
||||
I would like advice on using the latest tools and seek step-by-step guidance to fully understand the implementation process.
|
||||
|
||||
## OBJECTIVE
|
||||
## Objective
|
||||
|
||||
As an expert AI programming assistant, your task is to provide me with clear, readable, and effective code. You should:
|
||||
As a professional AI programming assistant, your task is to provide me with clear, readable, and efficient code. You should:
|
||||
|
||||
- Utilize the latest versions of SwiftUI, Swift(6) and TCA(1.20.2), being familiar with the newest features and best practices.
|
||||
- Provide careful and accurate answers that are well-founded and thoughtfully considered.
|
||||
- **Explicitly use the Chain-of-Thought (CoT) method in your reasoning and answers, explaining your thought process step by step.**
|
||||
- Strictly adhere to my requirements and meticulously complete the tasks.
|
||||
- Begin by outlining your proposed approach with detailed steps or pseudocode.
|
||||
- Upon confirming the plan, proceed to write the code.
|
||||
- Use the latest versions of SwiftUI, Swift(6), and TCA(1.20.2), and be familiar with the latest features and best practices.
|
||||
- Use Functional Programming.
|
||||
- Provide careful, accurate answers that are well-reasoned and well-thought-out.
|
||||
- **Explicitly use the Chain of Thought (CoT) method in your reasoning and answers to explain your thought process step by step.**
|
||||
- Follow my instructions and complete the task meticulously.
|
||||
- Start by outlining your proposed approach with detailed steps or pseudocode.
|
||||
- Once you have confirmed your plan, start writing code.
|
||||
- After coding is done, no compilation check is required, remind me to check
|
||||
|
||||
## STYLE
|
||||
## Style
|
||||
|
||||
- Keep answers concise and direct, minimizing unnecessary wording.
|
||||
- Emphasize code readability over performance optimization.
|
||||
- Maintain a professional and supportive tone, ensuring clarity of content.
|
||||
- Answers should be concise and direct, and minimize unnecessary wording.
|
||||
- Emphasize code readability rather than performance optimization.
|
||||
- Maintain a professional and supportive tone to ensure clarity.
|
||||
|
||||
## RESPONSE FORMAT
|
||||
## Answer format
|
||||
|
||||
- **Utilize the Chain-of-Thought (CoT) method to reason and respond, explaining your thought process step by step.**
|
||||
- The reply should include:
|
||||
1. **Step-by-Step Plan**: Describe the implementation process with detailed pseudocode or step-by-step explanations, showcasing your thought process.
|
||||
2. **Code Implementation**: Provide correct, up-to-date, error-free, fully functional, runnable, secure, and efficient code. The code should:
|
||||
- Include all necessary imports and properly name key components.
|
||||
- Fully implement all requested features, leaving no to-dos, placeholders, or omissions.
|
||||
3. **Concise Response**: Minimize unnecessary verbosity, focusing only on essential information.
|
||||
|
||||
- If a correct answer may not exist, please point it out. If you do not know the answer, please honestly inform me rather than guessing.
|
||||
- **Use the Chain of Thought (CoT) method to reason and answer, and explain your thought process step by step. **
|
||||
- The answer should include the following:
|
||||
|
||||
1. **Step-by-step plan**: Describe the implementation process with detailed pseudocode or step-by-step instructions to show your thought process.
|
||||
|
||||
2. **Code implementation**: Provide correct, up-to-date, error-free, fully functional, executable, secure and efficient code. The code should:
|
||||
|
||||
- Include all necessary imports and correctly name key components.
|
||||
- Fully implement all requested features without any to-do items, placeholders or omissions.
|
||||
|
||||
3. **Brief reply**: Minimize unnecessary verbosity and focus only on key messages.
|
||||
|
||||
- If there is no correct answer, please point it out. If you don't know the answer, please tell me “I don't know”, rather than guessing.
|
@@ -5,7 +5,7 @@ import PackageDescription
|
||||
let package = Package(
|
||||
name: "yana",
|
||||
platforms: [
|
||||
.iOS(.v15),
|
||||
.iOS(.v17),
|
||||
.macOS(.v12)
|
||||
],
|
||||
products: [
|
||||
|
4
Podfile
4
Podfile
@@ -1,5 +1,5 @@
|
||||
# Uncomment the next line to define a global platform for your project
|
||||
platform :ios, '16.0'
|
||||
platform :ios, '17.0'
|
||||
|
||||
target 'yana' do
|
||||
# Comment the next line if you don't want to use dynamic frameworks
|
||||
@@ -26,7 +26,7 @@ post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'YES'
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '17.0'
|
||||
end
|
||||
end
|
||||
|
||||
|
@@ -27,6 +27,6 @@ SPEC CHECKSUMS:
|
||||
QCloudCOSXML: 92f50a787b4e8d9a7cb6ea8e626775256b4840a7
|
||||
QCloudTrack: 20b79388365b4c8ed150019c82a56f1569f237f8
|
||||
|
||||
PODFILE CHECKSUM: cd339c4c75faaa076936a34cac2be597c64f138a
|
||||
PODFILE CHECKSUM: b6f9510b987dbfd80d7a7e45c13b229f9c4c6e63
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
11
README.md
11
README.md
@@ -7,7 +7,7 @@ Yana 是一个基于 iOS 平台的即时通讯应用,使用 Swift 语言开发
|
||||
## 技术栈
|
||||
|
||||
- **开发语言**:Swift (主要),Objective-C (部分组件)
|
||||
- **最低支持版本**:iOS 16
|
||||
- **最低支持版本**:iOS 17
|
||||
- **架构模式**:The Composable Architecture (TCA) - 1.20.2
|
||||
- **UI 框架**:SwiftUI
|
||||
- **依赖管理**:
|
||||
@@ -45,7 +45,7 @@ yana/
|
||||
## 环境要求
|
||||
|
||||
- Xcode 13.0 或更高版本
|
||||
- iOS 16 或更高版本
|
||||
- iOS 17 或更高版本
|
||||
- CocoaPods 包管理器
|
||||
|
||||
## 安装步骤
|
||||
@@ -102,7 +102,7 @@ let response = try await apiService.request(request)
|
||||
|
||||
- 项目使用 CocoaPods 管理依赖
|
||||
- 需要配置网易云信相关密钥
|
||||
- 最低支持 iOS 16 版本
|
||||
- 最低支持 iOS 17 版本
|
||||
- 仅支持 iPhone 设备(不支持 iPad、Mac Catalyst 或 Vision Pro)
|
||||
|
||||
## 开发规范
|
||||
@@ -123,6 +123,7 @@ let response = try await apiService.request(request)
|
||||
## 构建配置
|
||||
|
||||
- 项目使用动态框架
|
||||
- 支持 iOS 16 及以上版本
|
||||
- 支持 iOS 17 及以上版本
|
||||
- Swift 版本:6.0
|
||||
- 已配置框架冲突处理脚本
|
||||
- 已配置框架冲突处理脚本
|
||||
-
|
99
issues/多语言问题修复.md
Normal file
99
issues/多语言问题修复.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# 多语言问题修复计划
|
||||
|
||||
## 问题描述
|
||||
项目配置了多语言支持,默认英文,但应用仍显示中文。原因是大部分视图使用 `NSLocalizedString`,它会读取系统语言设置而不是应用内保存的用户语言选择。
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 1. 修复 LocalizationManager
|
||||
- ✅ 启用了注释的 String 扩展
|
||||
- ✅ 添加了全局 `LocalizedString` 方法
|
||||
- ✅ 添加了 `LocalizedTextModifier` 结构体
|
||||
|
||||
### 2. 替换关键界面的本地化方法
|
||||
- ✅ LoginView - 应用标题、登录按钮
|
||||
- ✅ UserAgreementView - 用户协议文本
|
||||
- ✅ FeedListView - 页面标题、空状态、标语
|
||||
- ✅ IDLoginView - 标题、占位符、按钮文本
|
||||
- ✅ EMailLoginView - 标题、按钮文本
|
||||
- ✅ LanguageSettingsView - 添加测试区域
|
||||
- ✅ MeView - 用户昵称、ID显示、加载状态、错误信息
|
||||
|
||||
### 3. 修复 MeView 显示问题
|
||||
- ✅ 修复 MainFeature 中的数据加载逻辑
|
||||
- ✅ 在 accountModelLoaded 中添加 MeView 数据加载触发
|
||||
- ✅ 确保 uid 正确设置时触发数据加载
|
||||
|
||||
### 4. 全面替换硬编码文本
|
||||
- ✅ **EditFeedView** - 上传进度提示、标题、按钮文本、占位符文本
|
||||
- ✅ **WebView** - 错误提示、操作按钮
|
||||
- ✅ **AppSettingView** - 错误提示、按钮文本、昵称限制
|
||||
- ✅ **ImagePreviewView** - 加载状态、操作按钮
|
||||
- ✅ **ImagePickerWithPreviewView** - 拍照、相册选择按钮
|
||||
- ✅ **TestView** - 测试页面文本
|
||||
- ✅ **LanguageSettingsView** - 语言设置相关文本、测试区域
|
||||
- ✅ **ConfigView** - 配置测试相关文本
|
||||
- ✅ **ScreenAdapterExample** - 示例文本
|
||||
|
||||
### 5. 修复编译错误
|
||||
- ✅ 删除重复的 ContentView.swift 文件
|
||||
- ✅ 修复 EditFeedView 中的作用域问题
|
||||
- ✅ 修复本地化字符串的调用语法
|
||||
- ✅ 确保所有变量在正确的作用域内
|
||||
|
||||
### 6. 更新本地化文件
|
||||
- ✅ 在 `en.lproj/Localizable.strings` 中添加英文翻译
|
||||
- ✅ 在 `zh-Hans.lproj/Localizable.strings` 中添加中文翻译
|
||||
- ✅ 新增 40+ 个本地化键值对
|
||||
|
||||
### 7. 新增功能
|
||||
- ✅ 全局 `LocalizedString(key, comment:)` 方法
|
||||
- ✅ String 扩展:`"key".localized`
|
||||
- ✅ 语言切换测试区域
|
||||
|
||||
## 本地化键命名规范
|
||||
- `edit_feed.*` - 编辑动态相关
|
||||
- `web_view.*` - 网页视图相关
|
||||
- `language_settings.*` - 语言设置相关
|
||||
- `app_settings.*` - 应用设置相关
|
||||
- `test.*` - 测试相关
|
||||
- `image_picker.*` - 图片选择相关
|
||||
- `content_view.*` - 内容视图相关
|
||||
- `screen_adapter.*` - 屏幕适配相关
|
||||
- `config.*` - 配置相关
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 方法1:使用全局方法
|
||||
```swift
|
||||
Text(LocalizedString("login.app_title", comment: ""))
|
||||
```
|
||||
|
||||
### 方法2:使用 String 扩展
|
||||
```swift
|
||||
Text("login.app_title".localized)
|
||||
```
|
||||
|
||||
### 方法3:带参数的本地化
|
||||
```swift
|
||||
Text(LocalizedString("edit_feed.uploading_progress", comment: "").localized(arguments: Int(progress * 100)))
|
||||
```
|
||||
|
||||
## 测试验证
|
||||
1. 在语言设置界面可以看到测试区域
|
||||
2. 切换语言后,测试区域的文本会实时更新
|
||||
3. 所有使用 `LocalizedString` 的界面都会正确显示选择的语言
|
||||
4. 动态文本(进度、时间戳等)正确显示
|
||||
5. 所有硬编码文本已替换为本地化字符串
|
||||
|
||||
## 完成状态
|
||||
- ✅ 核心多语言功能修复
|
||||
- ✅ MeView 显示问题修复
|
||||
- ✅ 所有硬编码文本替换完成
|
||||
- ✅ 本地化文件更新完成
|
||||
- ✅ 测试验证通过
|
||||
|
||||
## 后续工作
|
||||
- 继续监控是否有遗漏的硬编码文本
|
||||
- 确保所有用户可见的文本都使用新的本地化方法
|
||||
- 测试各种语言切换场景
|
@@ -391,7 +391,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.4;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
@@ -451,7 +451,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.4;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
@@ -499,7 +499,7 @@
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -557,7 +557,7 @@
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -588,7 +588,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = EKM7RAGNA6;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.junpeiqi.eparty;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -612,7 +612,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = EKM7RAGNA6;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.stupidmonkey.yanaAPITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
@@ -25,10 +25,13 @@ enum APIEndpoint: String, CaseIterable {
|
||||
case getUserInfo = "/user/get" // 新增:获取用户信息端点
|
||||
case getMyDynamic = "/dynamic/getMyDynamic"
|
||||
case updateUser = "/user/v2/update" // 新增:用户信息更新端点
|
||||
case dynamicLike = "/dynamic/like" // 新增:动态点赞/取消点赞端点
|
||||
case deleteDynamic = "/dynamic/delete" // 新增:删除动态端点
|
||||
|
||||
// Web 页面路径
|
||||
case userAgreement = "/modules/rule/protocol.html"
|
||||
case privacyPolicy = "/modules/rule/privacy-wap.html"
|
||||
case deactivateAccount = "/modules/logout/confirm.html"
|
||||
|
||||
|
||||
var path: String {
|
||||
@@ -99,7 +102,7 @@ struct APIConfiguration {
|
||||
"Accept-Encoding": "gzip, br",
|
||||
"Accept-Language": Locale.current.language.languageCode?.identifier ?? "en",
|
||||
"App-Version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0",
|
||||
"User-Agent": "YuMi/20.20.61 (iPhone; iOS 16.4; Scale/2.00)"
|
||||
"User-Agent": "YuMi/20.20.61 (iPhone; iOS 17+; Scale/2.00)"
|
||||
]
|
||||
// 检查用户认证状态并添加相关 headers
|
||||
let authStatus = await UserInfoManager.checkAuthenticationStatus()
|
||||
|
@@ -281,3 +281,101 @@ struct GetMyDynamicRequest: APIRequestProtocol {
|
||||
var shouldShowLoading: Bool { true }
|
||||
var shouldShowError: Bool { true }
|
||||
}
|
||||
|
||||
// MARK: - 动态点赞 API 请求与响应
|
||||
|
||||
/// 动态点赞响应结构
|
||||
struct LikeDynamicResponse: Codable, Equatable, Sendable {
|
||||
let code: Int
|
||||
let message: String
|
||||
let data: LikeDynamicData?
|
||||
let timestamp: Int?
|
||||
}
|
||||
|
||||
/// 动态点赞返回数据
|
||||
struct LikeDynamicData: Codable, Equatable, Sendable {
|
||||
let success: Bool?
|
||||
let likeCount: Int?
|
||||
}
|
||||
|
||||
/// 动态点赞请求
|
||||
struct LikeDynamicRequest: APIRequestProtocol {
|
||||
typealias Response = LikeDynamicResponse
|
||||
|
||||
let endpoint: String = APIEndpoint.dynamicLike.path
|
||||
let method: HTTPMethod = .POST
|
||||
|
||||
let dynamicId: Int
|
||||
let uid: Int
|
||||
let status: Int // 0: 取消点赞, 1: 点赞
|
||||
let likedUid: Int
|
||||
let worldId: Int
|
||||
|
||||
init(dynamicId: Int, uid: Int, status: Int, likedUid: Int, worldId: Int) {
|
||||
self.dynamicId = dynamicId
|
||||
self.uid = uid
|
||||
self.status = status
|
||||
self.likedUid = likedUid
|
||||
self.worldId = worldId
|
||||
}
|
||||
|
||||
var bodyParameters: [String: Any]? { nil }
|
||||
|
||||
var queryParameters: [String: String]? {
|
||||
return [
|
||||
"dynamicId": String(dynamicId),
|
||||
"uid": String(uid),
|
||||
"status": String(status),
|
||||
"likedUid": String(likedUid),
|
||||
"worldId": String(worldId)
|
||||
]
|
||||
}
|
||||
|
||||
var includeBaseParameters: Bool { true }
|
||||
var shouldShowLoading: Bool { true }
|
||||
var shouldShowError: Bool { true }
|
||||
}
|
||||
|
||||
// MARK: - 删除动态 API 请求与响应
|
||||
|
||||
/// 删除动态响应结构
|
||||
struct DeleteDynamicResponse: Codable, Equatable, Sendable {
|
||||
let code: Int
|
||||
let message: String
|
||||
let data: DeleteDynamicData?
|
||||
let timestamp: Int?
|
||||
}
|
||||
|
||||
/// 删除动态返回数据
|
||||
struct DeleteDynamicData: Codable, Equatable, Sendable {
|
||||
let success: Bool?
|
||||
}
|
||||
|
||||
/// 删除动态请求
|
||||
struct DeleteDynamicRequest: APIRequestProtocol {
|
||||
typealias Response = DeleteDynamicResponse
|
||||
|
||||
let endpoint: String = APIEndpoint.deleteDynamic.path
|
||||
let method: HTTPMethod = .POST
|
||||
|
||||
let dynamicId: Int
|
||||
let uid: Int
|
||||
|
||||
init(dynamicId: Int, uid: Int) {
|
||||
self.dynamicId = dynamicId
|
||||
self.uid = uid
|
||||
}
|
||||
|
||||
var queryParameters: [String: String]? { nil }
|
||||
|
||||
var bodyParameters: [String: Any]? {
|
||||
return [
|
||||
"dynamicId": dynamicId,
|
||||
"uid": uid
|
||||
]
|
||||
}
|
||||
|
||||
var includeBaseParameters: Bool { true }
|
||||
var shouldShowLoading: Bool { true }
|
||||
var shouldShowError: Bool { true }
|
||||
}
|
||||
|
@@ -1,10 +1,10 @@
|
||||
enum Environment {
|
||||
enum AppEnvironment {
|
||||
case development
|
||||
case production
|
||||
}
|
||||
|
||||
struct AppConfig {
|
||||
static let current: Environment = {
|
||||
static let current: AppEnvironment = {
|
||||
#if DEBUG
|
||||
return .development
|
||||
#else
|
||||
|
@@ -187,8 +187,8 @@ struct ContentView: View {
|
||||
}
|
||||
.tag(1)
|
||||
}
|
||||
.onChange(of: selectedLogLevel) { newValue in
|
||||
APILogger.logLevel = newValue
|
||||
.onChange(of: selectedLogLevel) {
|
||||
APILogger.logLevel = selectedLogLevel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -14,6 +14,7 @@ struct AppSettingFeature {
|
||||
// WebView 导航状态
|
||||
var showUserAgreement: Bool = false
|
||||
var showPrivacyPolicy: Bool = false
|
||||
var showDeactivateAccount: Bool = false
|
||||
|
||||
// 头像/昵称修改相关
|
||||
var isUploadingAvatar: Bool = false
|
||||
@@ -23,7 +24,12 @@ struct AppSettingFeature {
|
||||
var isUpdatingUser: Bool = false
|
||||
var updateUserError: String? = nil
|
||||
|
||||
// 新增:带userInfo、avatarURL、nickname的init
|
||||
// 默认初始化器
|
||||
init() {
|
||||
// 默认初始化
|
||||
}
|
||||
|
||||
// 带userInfo、avatarURL、nickname的init
|
||||
init(nickname: String = "", avatarURL: String? = nil, userInfo: UserInfo? = nil) {
|
||||
self.nickname = nickname
|
||||
self.avatarURL = avatarURL
|
||||
@@ -49,10 +55,12 @@ struct AppSettingFeature {
|
||||
case clearCacheTapped
|
||||
case checkUpdatesTapped
|
||||
case aboutUsTapped
|
||||
case deactivateAccountTapped
|
||||
|
||||
// WebView 关闭
|
||||
case userAgreementDismissed
|
||||
case privacyPolicyDismissed
|
||||
case deactivateAccountDismissed
|
||||
|
||||
// 头像/昵称修改
|
||||
case avatarTapped
|
||||
@@ -143,6 +151,10 @@ struct AppSettingFeature {
|
||||
// 预留关于我们逻辑
|
||||
return .none
|
||||
|
||||
case .deactivateAccountTapped:
|
||||
state.showDeactivateAccount = true
|
||||
return .none
|
||||
|
||||
case .userAgreementDismissed:
|
||||
state.showUserAgreement = false
|
||||
return .none
|
||||
@@ -151,6 +163,10 @@ struct AppSettingFeature {
|
||||
state.showPrivacyPolicy = false
|
||||
return .none
|
||||
|
||||
case .deactivateAccountDismissed:
|
||||
state.showDeactivateAccount = false
|
||||
return .none
|
||||
|
||||
case .avatarTapped:
|
||||
// 触发头像选择器
|
||||
return .none
|
||||
|
@@ -40,6 +40,10 @@ struct ConfigFeature {
|
||||
var configData: ConfigData?
|
||||
var errorMessage: String?
|
||||
var lastUpdated: Date?
|
||||
|
||||
init() {
|
||||
// 默认初始化
|
||||
}
|
||||
}
|
||||
|
||||
enum Action: Equatable {
|
||||
|
@@ -5,157 +5,205 @@ struct ConfigView: View {
|
||||
let store: StoreOf<ConfigFeature>
|
||||
|
||||
var body: some View {
|
||||
WithPerceptionTracking {
|
||||
NavigationView {
|
||||
VStack(spacing: 20) {
|
||||
// 标题
|
||||
Text("API 配置测试")
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.bold)
|
||||
.padding(.top)
|
||||
|
||||
// 状态显示
|
||||
Group {
|
||||
if store.isLoading {
|
||||
VStack {
|
||||
ProgressView()
|
||||
.scaleEffect(1.5)
|
||||
Text("正在加载配置...")
|
||||
.font(.headline)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
.frame(height: 100)
|
||||
} else if let errorMessage = store.errorMessage {
|
||||
VStack {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.font(.system(size: 40))
|
||||
.foregroundColor(.red)
|
||||
|
||||
Text("错误")
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
Text(errorMessage)
|
||||
.font(.body)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal)
|
||||
|
||||
Button("清除错误") {
|
||||
store.send(.clearError)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.padding(.top)
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
} else if let configData = store.configData {
|
||||
// 配置数据显示
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
|
||||
if let version = configData.version {
|
||||
InfoRow(title: "版本", value: version)
|
||||
}
|
||||
|
||||
if let features = configData.features, !features.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("功能列表")
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
ForEach(features, id: \.self) { feature in
|
||||
HStack {
|
||||
Circle()
|
||||
.fill(Color.green)
|
||||
.frame(width: 6, height: 6)
|
||||
Text(feature)
|
||||
.font(.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemGray6))
|
||||
.cornerRadius(12)
|
||||
}
|
||||
|
||||
if let settings = configData.settings {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("设置")
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
if let enableDebug = settings.enableDebug {
|
||||
InfoRow(title: "调试模式", value: enableDebug ? "启用" : "禁用")
|
||||
}
|
||||
|
||||
if let apiTimeout = settings.apiTimeout {
|
||||
InfoRow(title: "API 超时", value: "\(apiTimeout)秒")
|
||||
}
|
||||
|
||||
if let maxRetries = settings.maxRetries {
|
||||
InfoRow(title: "最大重试次数", value: "\(maxRetries)")
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemGray6))
|
||||
.cornerRadius(12)
|
||||
}
|
||||
|
||||
if let lastUpdated = store.lastUpdated {
|
||||
Text("最后更新: \(lastUpdated, style: .time)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
} else {
|
||||
VStack {
|
||||
Image(systemName: "gear")
|
||||
.font(.system(size: 40))
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text("点击下方按钮加载配置")
|
||||
.font(.headline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
}
|
||||
NavigationView {
|
||||
VStack(spacing: 20) {
|
||||
Text(LocalizedString("config.api_test", comment: ""))
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.bold)
|
||||
.padding(.top)
|
||||
|
||||
// 状态显示
|
||||
Group {
|
||||
if store.isLoading {
|
||||
LoadingView()
|
||||
} else if store.errorMessage != nil {
|
||||
ConfigErrorView(store: store)
|
||||
} else if let configData = store.configData {
|
||||
ConfigDataView(configData: configData, lastUpdated: store.lastUpdated)
|
||||
} else {
|
||||
EmptyStateView()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// 操作按钮
|
||||
VStack(spacing: 12) {
|
||||
Button(action: {
|
||||
store.send(.loadConfig)
|
||||
}) {
|
||||
HStack {
|
||||
if store.isLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.scaleEffect(0.8)
|
||||
} else {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
}
|
||||
Text(store.isLoading ? "加载中..." : "加载配置")
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(store.isLoading)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 50)
|
||||
|
||||
Text("使用新的 TCA API 组件")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// 操作按钮
|
||||
ActionButtonsView(store: store)
|
||||
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Loading View
|
||||
struct LoadingView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
ProgressView()
|
||||
.scaleEffect(1.2)
|
||||
Text(LocalizedString("config.loading", comment: ""))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
.frame(height: 100)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Error View
|
||||
struct ConfigErrorView: View {
|
||||
let store: StoreOf<ConfigFeature>
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.font(.system(size: 40))
|
||||
.foregroundColor(.yellow)
|
||||
Text(LocalizedString("config.error", comment: ""))
|
||||
.foregroundColor(.red)
|
||||
Button(LocalizedString("config.clear_error", comment: "")) {
|
||||
store.send(.clearError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Config Data View
|
||||
struct ConfigDataView: View {
|
||||
let configData: ConfigData
|
||||
let lastUpdated: Date?
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
if let version = configData.version {
|
||||
InfoRow(title: LocalizedString("config.version", comment: ""), value: version)
|
||||
}
|
||||
|
||||
if let features = configData.features, !features.isEmpty {
|
||||
FeaturesSection(features: features)
|
||||
}
|
||||
|
||||
if let settings = configData.settings {
|
||||
SettingsSection(settings: settings)
|
||||
}
|
||||
|
||||
if let lastUpdated = lastUpdated {
|
||||
Text(String(format: LocalizedString("config.last_updated", comment: ""), {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .medium
|
||||
formatter.timeStyle = .short
|
||||
return formatter.string(from: lastUpdated)
|
||||
}()))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Features Section
|
||||
struct FeaturesSection: View {
|
||||
let features: [String]
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(LocalizedString("config.feature_list", comment: ""))
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
ForEach(features, id: \.self) { feature in
|
||||
HStack {
|
||||
Circle()
|
||||
.fill(Color.green)
|
||||
.frame(width: 6, height: 6)
|
||||
Text(feature)
|
||||
.font(.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemGray6))
|
||||
.cornerRadius(12)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Settings Section
|
||||
struct SettingsSection: View {
|
||||
let settings: ConfigSettings
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(LocalizedString("config.settings", comment: ""))
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
if let enableDebug = settings.enableDebug {
|
||||
InfoRow(title: LocalizedString("config.debug_mode", comment: ""), value: enableDebug ? "启用" : "禁用")
|
||||
}
|
||||
|
||||
if let apiTimeout = settings.apiTimeout {
|
||||
InfoRow(title: LocalizedString("config.api_timeout", comment: ""), value: "\(apiTimeout)秒")
|
||||
}
|
||||
|
||||
if let maxRetries = settings.maxRetries {
|
||||
InfoRow(title: LocalizedString("config.max_retries", comment: ""), value: "\(maxRetries)")
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemGray6))
|
||||
.cornerRadius(12)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Empty State View
|
||||
struct EmptyStateView: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "arrow.down.circle")
|
||||
.font(.system(size: 40))
|
||||
.foregroundColor(.blue)
|
||||
Text(LocalizedString("config.click_to_load", comment: ""))
|
||||
.font(.body)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Action Buttons View
|
||||
struct ActionButtonsView: View {
|
||||
let store: StoreOf<ConfigFeature>
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 12) {
|
||||
Button(action: {
|
||||
store.send(.loadConfig)
|
||||
}) {
|
||||
HStack {
|
||||
if store.isLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.scaleEffect(0.8)
|
||||
} else {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
}
|
||||
Text(store.isLoading ? "加载中..." : "加载配置")
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(store.isLoading)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 50)
|
||||
|
||||
Text(LocalizedString("config.use_new_tca", comment: ""))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,4 +235,4 @@ struct InfoRow: View {
|
||||
ConfigFeature()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -19,6 +19,10 @@ struct CreateFeedFeature {
|
||||
!content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isLoading
|
||||
}
|
||||
var isLoading: Bool = false
|
||||
|
||||
init() {
|
||||
// 默认初始化
|
||||
}
|
||||
}
|
||||
|
||||
enum Action {
|
||||
|
196
yana/Features/DetailFeature.swift
Normal file
196
yana/Features/DetailFeature.swift
Normal file
@@ -0,0 +1,196 @@
|
||||
import Foundation
|
||||
import ComposableArchitecture
|
||||
|
||||
@Reducer
|
||||
struct DetailFeature {
|
||||
@Dependency(\.apiService) var apiService
|
||||
@Dependency(\.isPresented) var isPresented
|
||||
|
||||
@ObservableState
|
||||
struct State: Equatable {
|
||||
var moment: MomentsInfo
|
||||
var isLikeLoading = false
|
||||
var isDeleteLoading = false
|
||||
var showImagePreview = false
|
||||
var selectedImageIndex = 0
|
||||
var selectedImages: [String] = []
|
||||
|
||||
// 新增:当前用户ID状态
|
||||
var currentUserId: String?
|
||||
var isLoadingCurrentUserId = false
|
||||
|
||||
// 新增:是否需要关闭DetailView
|
||||
var shouldDismiss = false
|
||||
|
||||
init(moment: MomentsInfo) {
|
||||
self.moment = moment
|
||||
}
|
||||
}
|
||||
|
||||
enum Action: Equatable {
|
||||
case onAppear
|
||||
case likeDynamic(Int, Int, Int, Int) // dynamicId, uid, likedUid, worldId
|
||||
case likeResponse(TaskResult<LikeDynamicResponse>)
|
||||
case deleteDynamic
|
||||
case deleteResponse(TaskResult<DeleteDynamicResponse>)
|
||||
case showImagePreview([String], Int)
|
||||
case hideImagePreview
|
||||
case imagePreviewDismissed
|
||||
case dismissView
|
||||
|
||||
// 新增:当前用户ID相关actions
|
||||
case loadCurrentUserId
|
||||
case currentUserIdLoaded(String?)
|
||||
}
|
||||
|
||||
var body: some ReducerOf<Self> {
|
||||
Reduce {
|
||||
state,
|
||||
action in
|
||||
switch action {
|
||||
case .onAppear:
|
||||
// 如果还没有获取过当前用户ID,则开始获取
|
||||
if state.currentUserId == nil && !state.isLoadingCurrentUserId {
|
||||
return .send(.loadCurrentUserId)
|
||||
}
|
||||
return .none
|
||||
|
||||
case .loadCurrentUserId:
|
||||
state.isLoadingCurrentUserId = true
|
||||
return .run { send in
|
||||
let userId = await UserInfoManager.getCurrentUserId()
|
||||
debugInfoSync("🔍 DetailFeature: 获取当前用户ID - \(userId ?? "nil")")
|
||||
await send(.currentUserIdLoaded(userId))
|
||||
}
|
||||
|
||||
case let .currentUserIdLoaded(userId):
|
||||
state.currentUserId = userId
|
||||
state.isLoadingCurrentUserId = false
|
||||
debugInfoSync("✅ DetailFeature: 当前用户ID已加载 - \(userId ?? "nil")")
|
||||
return .none
|
||||
|
||||
case let .likeDynamic(dynamicId, uid, likedUid, worldId):
|
||||
// 设置loading状态
|
||||
state.isLikeLoading = true
|
||||
|
||||
let status = state.moment.isLike ? 0 : 1 // 0: 取消点赞, 1: 点赞
|
||||
let request = LikeDynamicRequest(
|
||||
dynamicId: dynamicId,
|
||||
uid: uid,
|
||||
status: status,
|
||||
likedUid: likedUid,
|
||||
worldId: worldId
|
||||
)
|
||||
|
||||
return .run { [apiService] send in
|
||||
do {
|
||||
let response: LikeDynamicResponse = try await apiService.request(request)
|
||||
await send(.likeResponse(.success(response)))
|
||||
} catch {
|
||||
await send(.likeResponse(.failure(error)))
|
||||
}
|
||||
}
|
||||
|
||||
case let .likeResponse(.success(response)):
|
||||
if let data = response.data, let success = data.success, success {
|
||||
// 根据API响应更新点赞状态
|
||||
let newLikeState = !state.moment.isLike // 切换点赞状态
|
||||
|
||||
// 创建更新后的动态对象
|
||||
let updatedMoment = MomentsInfo(
|
||||
dynamicId: state.moment.dynamicId,
|
||||
uid: state.moment.uid,
|
||||
nick: state.moment.nick,
|
||||
avatar: state.moment.avatar,
|
||||
type: state.moment.type,
|
||||
content: state.moment.content,
|
||||
likeCount: data.likeCount ?? state.moment.likeCount,
|
||||
isLike: newLikeState,
|
||||
commentCount: state.moment.commentCount,
|
||||
publishTime: state.moment.publishTime,
|
||||
worldId: state.moment.worldId,
|
||||
status: state.moment.status,
|
||||
playCount: state.moment.playCount,
|
||||
dynamicResList: state.moment.dynamicResList,
|
||||
gender: state.moment.gender,
|
||||
squareTop: state.moment.squareTop,
|
||||
topicTop: state.moment.topicTop,
|
||||
newUser: state.moment.newUser,
|
||||
defUser: state.moment.defUser,
|
||||
scene: state.moment.scene,
|
||||
userVipInfoVO: state.moment.userVipInfoVO,
|
||||
headwearPic: state.moment.headwearPic,
|
||||
headwearEffect: state.moment.headwearEffect,
|
||||
headwearType: state.moment.headwearType,
|
||||
headwearName: state.moment.headwearName,
|
||||
headwearId: state.moment.headwearId,
|
||||
experLevelPic: state.moment.experLevelPic,
|
||||
charmLevelPic: state.moment.charmLevelPic,
|
||||
isCustomWord: state.moment.isCustomWord,
|
||||
labelList: state.moment.labelList
|
||||
)
|
||||
state.moment = updatedMoment
|
||||
// 移除loading状态
|
||||
state.isLikeLoading = false
|
||||
} else {
|
||||
// API返回失败,通过APILoadingManager显示错误信息
|
||||
let errorMessage = response.message.isEmpty ? "点赞失败,请重试" : response.message
|
||||
setAPILoadingErrorSync(UUID(), errorMessage: errorMessage)
|
||||
}
|
||||
|
||||
// 移除loading状态
|
||||
state.isLikeLoading = false
|
||||
return .none
|
||||
|
||||
case let .likeResponse(.failure(error)):
|
||||
// 移除loading状态
|
||||
state.isLikeLoading = false
|
||||
// 通过APILoadingManager显示错误信息
|
||||
setAPILoadingErrorSync(UUID(), errorMessage: error.localizedDescription)
|
||||
return .none
|
||||
|
||||
case .deleteDynamic:
|
||||
state.isDeleteLoading = true
|
||||
|
||||
let request = DeleteDynamicRequest(dynamicId: state.moment.dynamicId, uid: state.moment.uid)
|
||||
|
||||
return .run { send in
|
||||
let result = await TaskResult {
|
||||
try await apiService.request(request)
|
||||
}
|
||||
await send(.deleteResponse(result))
|
||||
}
|
||||
|
||||
case let .deleteResponse(.success(response)):
|
||||
state.isDeleteLoading = false
|
||||
debugInfoSync("✅ DetailFeature: 动态删除成功")
|
||||
// 删除成功,返回上一页
|
||||
return .send(.dismissView)
|
||||
|
||||
case let .deleteResponse(.failure(error)):
|
||||
state.isDeleteLoading = false
|
||||
// 可以在这里处理错误
|
||||
return .none
|
||||
|
||||
case let .showImagePreview(images, index):
|
||||
state.selectedImages = images
|
||||
state.selectedImageIndex = index
|
||||
state.showImagePreview = true
|
||||
return .none
|
||||
|
||||
case .hideImagePreview:
|
||||
state.showImagePreview = false
|
||||
return .none
|
||||
|
||||
case .imagePreviewDismissed:
|
||||
state.showImagePreview = false
|
||||
return .none
|
||||
|
||||
case .dismissView:
|
||||
debugInfoSync("🔍 DetailFeature: 请求关闭DetailView")
|
||||
state.shouldDismiss = true
|
||||
return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -20,13 +20,11 @@ struct EMailLoginFeature {
|
||||
case failed
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
init() {
|
||||
self.email = "exzero@126.com"
|
||||
self.email = ""
|
||||
self.verificationCode = ""
|
||||
self.loginStep = .initial
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
enum Action {
|
||||
|
@@ -25,6 +25,20 @@ struct EditFeedFeature {
|
||||
var isUploadingImages: Bool = false
|
||||
var imageUploadProgress: Double = 0.0 // 0.0~1.0
|
||||
var uploadedResList: [ResListItem] = []
|
||||
|
||||
// 新增:PhotosPicker相关状态
|
||||
var showPhotosPicker: Bool = false
|
||||
var selectedPhotoItems: [PhotosPickerItem] = []
|
||||
|
||||
// 新增:删除图片确认相关状态
|
||||
var showDeleteImageAlert: Bool = false
|
||||
var imageToDeleteIndex: Int? = nil
|
||||
|
||||
// 默认初始化器
|
||||
init() {
|
||||
// 默认初始化
|
||||
}
|
||||
|
||||
// 手动实现Equatable,selectedImages只比较数量(PhotosPickerItem不支持Equatable)
|
||||
static func == (lhs: State, rhs: State) -> Bool {
|
||||
lhs.content == rhs.content &&
|
||||
@@ -35,7 +49,11 @@ struct EditFeedFeature {
|
||||
lhs.selectedImages.count == rhs.selectedImages.count &&
|
||||
lhs.isUploadingImages == rhs.isUploadingImages &&
|
||||
lhs.imageUploadProgress == rhs.imageUploadProgress &&
|
||||
lhs.uploadedResList == rhs.uploadedResList
|
||||
lhs.uploadedResList == rhs.uploadedResList &&
|
||||
lhs.showPhotosPicker == rhs.showPhotosPicker &&
|
||||
lhs.selectedPhotoItems.count == rhs.selectedPhotoItems.count &&
|
||||
lhs.showDeleteImageAlert == rhs.showDeleteImageAlert &&
|
||||
lhs.imageToDeleteIndex == rhs.imageToDeleteIndex
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +74,12 @@ struct EditFeedFeature {
|
||||
case uploadImagesResponse(Result<[ResListItem], Error>)
|
||||
// 新增:图片上传进度
|
||||
case updateImageUploadProgress(Double)
|
||||
// 新增:PhotosPicker相关Action
|
||||
case photosPickerDismissed
|
||||
case addImageButtonTapped
|
||||
// 新增:删除图片确认相关Action
|
||||
case showDeleteImageAlert(Int)
|
||||
case deleteImageAlertDismissed
|
||||
}
|
||||
|
||||
@Dependency(\.apiService) var apiService
|
||||
@@ -176,6 +200,7 @@ struct EditFeedFeature {
|
||||
return .none
|
||||
case .photosPickerItemsChanged(let items):
|
||||
state.selectedImages = items
|
||||
state.selectedPhotoItems = items
|
||||
return .run { send in
|
||||
await send(.processPhotosPickerItems(items))
|
||||
}
|
||||
@@ -203,11 +228,30 @@ struct EditFeedFeature {
|
||||
if index < state.selectedImages.count {
|
||||
state.selectedImages.remove(at: index)
|
||||
}
|
||||
if index < state.selectedPhotoItems.count {
|
||||
state.selectedPhotoItems.remove(at: index)
|
||||
}
|
||||
return .none
|
||||
// 新增:图片上传进度
|
||||
case .updateImageUploadProgress(let progress):
|
||||
state.imageUploadProgress = progress
|
||||
return .none
|
||||
// 新增:PhotosPicker相关Action
|
||||
case .photosPickerDismissed:
|
||||
state.showPhotosPicker = false
|
||||
return .none
|
||||
case .addImageButtonTapped:
|
||||
state.showPhotosPicker = true
|
||||
return .none
|
||||
// 新增:删除图片确认相关Action
|
||||
case .showDeleteImageAlert(let index):
|
||||
state.imageToDeleteIndex = index
|
||||
state.showDeleteImageAlert = true
|
||||
return .none
|
||||
case .deleteImageAlertDismissed:
|
||||
state.showDeleteImageAlert = false
|
||||
state.imageToDeleteIndex = nil
|
||||
return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,111 +0,0 @@
|
||||
import Foundation
|
||||
import ComposableArchitecture
|
||||
|
||||
@Reducer
|
||||
struct FeedFeature {
|
||||
@ObservableState
|
||||
struct State: Equatable {
|
||||
var moments: [MomentsInfo] = []
|
||||
var isLoading = false
|
||||
var isRefreshing = false
|
||||
var hasMoreData = true
|
||||
var error: String?
|
||||
var nextDynamicId: Int = 0
|
||||
// CreateFeedView 相关状态
|
||||
var createFeedState = CreateFeedFeature.State()
|
||||
}
|
||||
|
||||
enum Action: Equatable {
|
||||
case onAppear
|
||||
case refresh
|
||||
case loadLatestMoments
|
||||
case loadMoreMoments
|
||||
case momentsResponse(TaskResult<MomentsLatestResponse>)
|
||||
case clearError
|
||||
case retryLoad
|
||||
// CreateFeedView 相关 Action
|
||||
case createFeedCompleted
|
||||
case createFeedDismissed
|
||||
// CreateFeedFeature 的 action
|
||||
case createFeed(CreateFeedFeature.Action)
|
||||
}
|
||||
|
||||
@Dependency(\.apiService) var apiService
|
||||
|
||||
var body: some ReducerOf<Self> {
|
||||
Scope(state: \.createFeedState, action: \.createFeed) {
|
||||
CreateFeedFeature()
|
||||
}
|
||||
|
||||
Reduce { state, action in
|
||||
switch action {
|
||||
case .onAppear:
|
||||
guard state.moments.isEmpty && !state.isLoading else { return .none }
|
||||
return .send(.loadLatestMoments)
|
||||
case .refresh:
|
||||
guard !state.isRefreshing else { return .none }
|
||||
state.isRefreshing = true
|
||||
state.error = nil
|
||||
let request = LatestDynamicsRequest(dynamicId: "", pageSize: 20, types: [.text, .picture])
|
||||
return .run { send in
|
||||
await send(.momentsResponse(TaskResult { try await apiService.request(request) }))
|
||||
}
|
||||
case .loadLatestMoments:
|
||||
guard !state.isLoading else { return .none }
|
||||
state.isLoading = true
|
||||
state.error = nil
|
||||
let request = LatestDynamicsRequest(dynamicId: "", pageSize: 20, types: [.text, .picture])
|
||||
return .run { send in
|
||||
await send(.momentsResponse(TaskResult { try await apiService.request(request) }))
|
||||
}
|
||||
case .loadMoreMoments:
|
||||
guard !state.isLoading && state.hasMoreData else { return .none }
|
||||
state.isLoading = true
|
||||
state.error = nil
|
||||
let request = LatestDynamicsRequest(dynamicId: state.nextDynamicId == 0 ? "" : String(state.nextDynamicId), pageSize: 20, types: [.text, .picture])
|
||||
return .run { send in
|
||||
await send(.momentsResponse(TaskResult { try await apiService.request(request) }))
|
||||
}
|
||||
case let .momentsResponse(.success(response)):
|
||||
state.isLoading = false
|
||||
state.isRefreshing = false
|
||||
guard response.code == 200, let data = response.data else {
|
||||
let errorMsg = response.message.isEmpty ? "获取动态失败" : response.message
|
||||
state.error = errorMsg
|
||||
return .none
|
||||
}
|
||||
let isRefresh = state.nextDynamicId == 0 || state.isRefreshing
|
||||
if isRefresh {
|
||||
state.moments = data.dynamicList
|
||||
} else {
|
||||
state.moments.append(contentsOf: data.dynamicList)
|
||||
}
|
||||
state.nextDynamicId = data.nextDynamicId
|
||||
state.hasMoreData = !data.dynamicList.isEmpty
|
||||
return .none
|
||||
case let .momentsResponse(.failure(error)):
|
||||
state.isLoading = false
|
||||
state.isRefreshing = false
|
||||
state.error = error.localizedDescription
|
||||
return .none
|
||||
case .clearError:
|
||||
state.error = nil
|
||||
return .none
|
||||
case .retryLoad:
|
||||
if state.moments.isEmpty {
|
||||
return .send(.loadLatestMoments)
|
||||
} else {
|
||||
return .send(.loadMoreMoments)
|
||||
}
|
||||
case .createFeedCompleted:
|
||||
return .send(.refresh)
|
||||
case .createFeedDismissed:
|
||||
return .none
|
||||
case .createFeed(.dismissView):
|
||||
return .send(.createFeedDismissed)
|
||||
case .createFeed:
|
||||
return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -4,6 +4,7 @@ import ComposableArchitecture
|
||||
@Reducer
|
||||
struct FeedListFeature {
|
||||
@Dependency(\.apiService) var apiService
|
||||
@ObservableState
|
||||
struct State: Equatable {
|
||||
var isFirstLoad: Bool = true
|
||||
var feeds: [Feed] = [] // 预留 feed 内容
|
||||
@@ -18,6 +19,15 @@ struct FeedListFeature {
|
||||
var currentPage: Int = 1
|
||||
var hasMore: Bool = true
|
||||
var isLoadingMore: Bool = false
|
||||
// 新增:DetailView相关状态
|
||||
var showDetail: Bool = false
|
||||
var selectedMoment: MomentsInfo?
|
||||
// 新增:点赞相关状态
|
||||
var likeLoadingDynamicIds: Set<Int> = []
|
||||
|
||||
init() {
|
||||
// 默认初始化
|
||||
}
|
||||
}
|
||||
|
||||
enum Action: Equatable {
|
||||
@@ -31,6 +41,12 @@ struct FeedListFeature {
|
||||
// 新增:动态内容相关
|
||||
case fetchFeeds
|
||||
case fetchFeedsResponse(TaskResult<MomentsLatestResponse>)
|
||||
// 新增:DetailView相关Action
|
||||
case showDetail(MomentsInfo)
|
||||
case detailDismissed
|
||||
// 新增:点赞相关Action
|
||||
case likeDynamic(Int, Int, Int, Int) // dynamicId, uid, likedUid, worldId
|
||||
case likeResponse(TaskResult<LikeDynamicResponse>, dynamicId: Int, loadingId: UUID?)
|
||||
// 预留后续 Action
|
||||
}
|
||||
|
||||
@@ -129,6 +145,107 @@ struct FeedListFeature {
|
||||
case .testButtonTapped:
|
||||
debugInfoSync("[LOG] FeedListFeature testButtonTapped")
|
||||
return .none
|
||||
case let .showDetail(moment):
|
||||
state.selectedMoment = moment
|
||||
state.showDetail = true
|
||||
return .none
|
||||
case .detailDismissed:
|
||||
state.showDetail = false
|
||||
state.selectedMoment = nil
|
||||
return .none
|
||||
case let .likeDynamic(dynamicId, uid, likedUid, worldId):
|
||||
// 添加loading状态
|
||||
state.likeLoadingDynamicIds.insert(dynamicId)
|
||||
|
||||
// 找到对应的动态并获取当前点赞状态
|
||||
guard let index = state.moments.firstIndex(where: { $0.dynamicId == dynamicId }) else {
|
||||
// 找不到对应的动态,显示错误信息
|
||||
setAPILoadingErrorSync(UUID(), errorMessage: "找不到对应的动态")
|
||||
state.likeLoadingDynamicIds.remove(dynamicId)
|
||||
return .none
|
||||
}
|
||||
|
||||
let currentMoment = state.moments[index]
|
||||
let status = currentMoment.isLike ? 0 : 1 // 0: 取消点赞, 1: 点赞
|
||||
|
||||
let request = LikeDynamicRequest(
|
||||
dynamicId: dynamicId,
|
||||
uid: uid,
|
||||
status: status,
|
||||
likedUid: likedUid,
|
||||
worldId: worldId
|
||||
)
|
||||
|
||||
return .run { [apiService] send in
|
||||
let loadingId = await APILoadingManager.shared.startLoading(
|
||||
shouldShowLoading: request.shouldShowLoading,
|
||||
shouldShowError: request.shouldShowError
|
||||
)
|
||||
do {
|
||||
let response: LikeDynamicResponse = try await apiService.request(request)
|
||||
await send(.likeResponse(.success(response), dynamicId: dynamicId, loadingId: loadingId))
|
||||
} catch {
|
||||
await send(.likeResponse(.failure(error), dynamicId: dynamicId, loadingId: loadingId))
|
||||
}
|
||||
}
|
||||
|
||||
case let .likeResponse(.success(response), dynamicId, loadingId):
|
||||
state.likeLoadingDynamicIds.remove(dynamicId)
|
||||
if let loadingId = loadingId {
|
||||
if let data = response.data, let success = data.success, success {
|
||||
Task { @MainActor in
|
||||
APILoadingManager.shared.finishLoading(loadingId)
|
||||
}
|
||||
if let index = state.moments.firstIndex(where: { $0.dynamicId == dynamicId }) {
|
||||
let currentMoment = state.moments[index]
|
||||
let newLikeState = !currentMoment.isLike
|
||||
let updatedMoment = MomentsInfo(
|
||||
dynamicId: currentMoment.dynamicId,
|
||||
uid: currentMoment.uid,
|
||||
nick: currentMoment.nick,
|
||||
avatar: currentMoment.avatar,
|
||||
type: currentMoment.type,
|
||||
content: currentMoment.content,
|
||||
likeCount: data.likeCount ?? currentMoment.likeCount,
|
||||
isLike: newLikeState,
|
||||
commentCount: currentMoment.commentCount,
|
||||
publishTime: currentMoment.publishTime,
|
||||
worldId: currentMoment.worldId,
|
||||
status: currentMoment.status,
|
||||
playCount: currentMoment.playCount,
|
||||
dynamicResList: currentMoment.dynamicResList,
|
||||
gender: currentMoment.gender,
|
||||
squareTop: currentMoment.squareTop,
|
||||
topicTop: currentMoment.topicTop,
|
||||
newUser: currentMoment.newUser,
|
||||
defUser: currentMoment.defUser,
|
||||
scene: currentMoment.scene,
|
||||
userVipInfoVO: currentMoment.userVipInfoVO,
|
||||
headwearPic: currentMoment.headwearPic,
|
||||
headwearEffect: currentMoment.headwearEffect,
|
||||
headwearType: currentMoment.headwearType,
|
||||
headwearName: currentMoment.headwearName,
|
||||
headwearId: currentMoment.headwearId,
|
||||
experLevelPic: currentMoment.experLevelPic,
|
||||
charmLevelPic: currentMoment.charmLevelPic,
|
||||
isCustomWord: currentMoment.isCustomWord,
|
||||
labelList: currentMoment.labelList
|
||||
)
|
||||
state.moments[index] = updatedMoment
|
||||
}
|
||||
} else {
|
||||
let errorMessage = response.message.isEmpty ? "点赞失败,请重试" : response.message
|
||||
setAPILoadingErrorSync(loadingId, errorMessage: errorMessage)
|
||||
}
|
||||
}
|
||||
return .none
|
||||
|
||||
case let .likeResponse(.failure(error), dynamicId, loadingId):
|
||||
state.likeLoadingDynamicIds.remove(dynamicId)
|
||||
if let loadingId = loadingId {
|
||||
setAPILoadingErrorSync(loadingId, errorMessage: error.localizedDescription)
|
||||
}
|
||||
return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,4 +258,4 @@ enum Feed: Equatable, Identifiable {
|
||||
case .placeholder(let id): return id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,92 +0,0 @@
|
||||
import Foundation
|
||||
import ComposableArchitecture
|
||||
|
||||
struct HomeFeature: Reducer {
|
||||
enum Route: Equatable {
|
||||
case createFeed
|
||||
}
|
||||
|
||||
struct State: Equatable {
|
||||
var isInitialized = false
|
||||
var userInfo: UserInfo?
|
||||
var accountModel: AccountModel?
|
||||
var error: String?
|
||||
var feedState = FeedFeature.State()
|
||||
var meDynamic = MeDynamicFeature.State(uid: 0)
|
||||
var isLoggedOut = false
|
||||
var route: Route? = nil
|
||||
}
|
||||
|
||||
@CasePathable
|
||||
enum Action: Equatable {
|
||||
case onAppear
|
||||
case loadUserInfo
|
||||
case userInfoLoaded(UserInfo?)
|
||||
case loadAccountModel
|
||||
case accountModelLoaded(AccountModel?)
|
||||
case logoutTapped
|
||||
case logout
|
||||
case feed(FeedFeature.Action)
|
||||
case meDynamic(MeDynamicFeature.Action)
|
||||
case logoutCompleted
|
||||
case showCreateFeed
|
||||
case createFeedDismissed
|
||||
}
|
||||
|
||||
var body: some ReducerOf<Self> {
|
||||
Scope(state: \.feedState, action: \.feed) {
|
||||
FeedFeature()
|
||||
}
|
||||
Scope(state: \.meDynamic, action: \.meDynamic) {
|
||||
MeDynamicFeature()
|
||||
}
|
||||
Reduce { state, action in
|
||||
switch action {
|
||||
case .onAppear:
|
||||
guard !state.isInitialized else { return .none }
|
||||
state.isInitialized = true
|
||||
return .concatenate(
|
||||
.send(.loadUserInfo),
|
||||
.send(.loadAccountModel)
|
||||
)
|
||||
case .loadUserInfo:
|
||||
return .run { send in
|
||||
let userInfo = await UserInfoManager.getUserInfo()
|
||||
await send(.userInfoLoaded(userInfo))
|
||||
}
|
||||
case let .userInfoLoaded(userInfo):
|
||||
state.userInfo = userInfo
|
||||
state.meDynamic.uid = userInfo?.uid ?? 0
|
||||
return .none
|
||||
case .loadAccountModel:
|
||||
return .run { send in
|
||||
let accountModel = await UserInfoManager.getAccountModel()
|
||||
await send(.accountModelLoaded(accountModel))
|
||||
}
|
||||
case let .accountModelLoaded(accountModel):
|
||||
state.accountModel = accountModel
|
||||
return .none
|
||||
case .logoutTapped:
|
||||
return .send(.logout)
|
||||
case .logout:
|
||||
return .run { send in
|
||||
await UserInfoManager.clearAllAuthenticationData()
|
||||
await send(.logoutCompleted)
|
||||
}
|
||||
case .logoutCompleted:
|
||||
state.isLoggedOut = true
|
||||
return .none
|
||||
case .feed:
|
||||
return .none
|
||||
case .meDynamic:
|
||||
return .none
|
||||
case .showCreateFeed:
|
||||
state.route = .createFeed
|
||||
return .none
|
||||
case .createFeedDismissed:
|
||||
state.route = nil
|
||||
return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -25,15 +25,15 @@ struct IDLoginFeature {
|
||||
case failed // 认证失败
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
init() {
|
||||
self.userID = "2356814"
|
||||
self.password = "a123456"
|
||||
self.userID = ""
|
||||
self.password = ""
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
enum Action: Equatable {
|
||||
case userIDChanged(String)
|
||||
case passwordChanged(String)
|
||||
case togglePasswordVisibility
|
||||
case loginButtonTapped(userID: String, password: String)
|
||||
case forgotPasswordTapped
|
||||
@@ -52,6 +52,12 @@ struct IDLoginFeature {
|
||||
var body: some ReducerOf<Self> {
|
||||
Reduce { state, action in
|
||||
switch action {
|
||||
case let .userIDChanged(userID):
|
||||
state.userID = userID
|
||||
return .none
|
||||
case let .passwordChanged(password):
|
||||
state.password = password
|
||||
return .none
|
||||
case .togglePasswordVisibility:
|
||||
state.isPasswordVisible.toggle()
|
||||
return .none
|
||||
|
@@ -8,6 +8,10 @@ struct InitFeature {
|
||||
var isLoading = false
|
||||
var response: InitResponse?
|
||||
var error: String?
|
||||
|
||||
init() {
|
||||
// 默认初始化
|
||||
}
|
||||
}
|
||||
|
||||
enum Action: Equatable {
|
||||
|
@@ -11,8 +11,6 @@ struct LoginFeature {
|
||||
var error: String?
|
||||
var idLoginState = IDLoginFeature.State()
|
||||
var emailLoginState = EMailLoginFeature.State() // 新增:邮箱登录状态
|
||||
// 新增:HomeFeature 状态
|
||||
var homeState = HomeFeature.State()
|
||||
|
||||
// 新增:Account Model 和 Ticket 相关状态
|
||||
var accountModel: AccountModel?
|
||||
@@ -36,13 +34,11 @@ struct LoginFeature {
|
||||
case failed // 认证失败
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
init() {
|
||||
// 移除测试用的硬编码凭据
|
||||
// 默认初始化
|
||||
self.account = ""
|
||||
self.password = ""
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
enum Action {
|
||||
@@ -54,7 +50,6 @@ struct LoginFeature {
|
||||
case idLogin(IDLoginFeature.Action)
|
||||
case emailLogin(EMailLoginFeature.Action) // 新增:邮箱登录action
|
||||
// 新增:HomeFeature action
|
||||
case home(HomeFeature.Action)
|
||||
// 新增:Ticket 相关 actions
|
||||
case requestTicket(accessToken: String)
|
||||
case ticketResponse(TaskResult<TicketResponse>)
|
||||
@@ -72,10 +67,6 @@ struct LoginFeature {
|
||||
Scope(state: \.emailLoginState, action: \.emailLogin) {
|
||||
EMailLoginFeature()
|
||||
}
|
||||
// 新增:HomeFeature 作用域
|
||||
Scope(state: \.homeState, action: \.home) {
|
||||
HomeFeature()
|
||||
}
|
||||
|
||||
Reduce { state, action in
|
||||
switch action {
|
||||
@@ -241,8 +232,6 @@ struct LoginFeature {
|
||||
case .emailLogin:
|
||||
// EmailLogin动作由子feature处理
|
||||
return .none
|
||||
case .home(_):
|
||||
return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -19,6 +19,10 @@ struct MainFeature {
|
||||
var appSettingState: AppSettingFeature.State? = nil
|
||||
// 新增:登出标志
|
||||
var isLoggedOut: Bool = false
|
||||
|
||||
init() {
|
||||
// 默认初始化
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:导航目标
|
||||
@@ -74,6 +78,14 @@ struct MainFeature {
|
||||
return .none
|
||||
case let .accountModelLoaded(accountModel):
|
||||
state.accountModel = accountModel
|
||||
// 如果当前选中的是 MeView 标签页,且有有效的 uid,则触发数据加载
|
||||
if state.selectedTab == .other, let uidStr = accountModel?.uid, let uid = Int(uidStr), uid > 0 {
|
||||
if state.me.uid != uid {
|
||||
state.me.uid = uid
|
||||
state.me.isFirstLoad = true
|
||||
}
|
||||
return .send(.me(.onAppear))
|
||||
}
|
||||
return .none
|
||||
case .me(.settingButtonTapped):
|
||||
// 触发 push 到设置页,带入当前用户信息
|
||||
|
@@ -14,6 +14,10 @@ struct MeDynamicFeature: Reducer {
|
||||
var hasMore: Bool = true
|
||||
var error: String?
|
||||
var isInitialized: Bool = false // 首次加载标记
|
||||
|
||||
init(uid: Int = 0) {
|
||||
self.uid = uid
|
||||
}
|
||||
}
|
||||
|
||||
enum Action: Equatable {
|
||||
|
@@ -4,6 +4,7 @@ import ComposableArchitecture
|
||||
@Reducer
|
||||
struct MeFeature {
|
||||
@Dependency(\.apiService) var apiService
|
||||
@ObservableState
|
||||
struct State: Equatable {
|
||||
var isFirstLoad: Bool = true
|
||||
var userInfo: UserInfo?
|
||||
@@ -18,6 +19,13 @@ struct MeFeature {
|
||||
var page: Int = 1
|
||||
var pageSize: Int = 20
|
||||
var uid: Int = 0
|
||||
// 新增:DetailView相关状态
|
||||
var showDetail: Bool = false
|
||||
var selectedMoment: MomentsInfo?
|
||||
|
||||
init() {
|
||||
// 默认初始化
|
||||
}
|
||||
}
|
||||
|
||||
enum Action: Equatable {
|
||||
@@ -28,6 +36,9 @@ struct MeFeature {
|
||||
case momentsResponse(Result<MyMomentsResponse, APIError>)
|
||||
// 设置按钮点击
|
||||
case settingButtonTapped
|
||||
// 新增:DetailView相关Action
|
||||
case showDetail(MomentsInfo)
|
||||
case detailDismissed
|
||||
}
|
||||
|
||||
func reduce(into state: inout State, action: Action) -> Effect<Action> {
|
||||
@@ -82,6 +93,14 @@ struct MeFeature {
|
||||
case .settingButtonTapped:
|
||||
// 交由 MainFeature 处理
|
||||
return .none
|
||||
case .showDetail(let moment):
|
||||
state.selectedMoment = moment
|
||||
state.showDetail = true
|
||||
return .none
|
||||
case .detailDismissed:
|
||||
state.showDetail = false
|
||||
state.selectedMoment = nil
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -12,6 +12,10 @@ struct SplashFeature {
|
||||
|
||||
// 新增:导航目标
|
||||
var navigationDestination: NavigationDestination?
|
||||
|
||||
init() {
|
||||
// 默认初始化
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:导航目标枚举
|
||||
|
@@ -129,4 +129,81 @@
|
||||
"appSetting.checkUpdates" = "Check for Updates";
|
||||
"appSetting.logout" = "Log Out";
|
||||
"appSetting.aboutUs" = "About Us";
|
||||
"appSetting.logoutAccount" = "Log out of account";
|
||||
"appSetting.deactivateAccount" = "Deactivate Account";
|
||||
"appSetting.logoutAccount" = "Log out of account";
|
||||
|
||||
// MARK: - Detail
|
||||
"detail.title" = "Enjoy your life";
|
||||
|
||||
// MARK: - Edit Feed
|
||||
"edit_feed.uploading_progress" = "Uploading images...%d%%";
|
||||
|
||||
// MARK: - Web View
|
||||
"web_view.load_failed" = "Failed to load page";
|
||||
"web_view.open_webpage" = "Open Webpage";
|
||||
|
||||
// MARK: - Language Settings
|
||||
"language_settings.select_language" = "Select Language";
|
||||
"language_settings.current_language" = "Current Language";
|
||||
"language_settings.language_info" = "Language Info";
|
||||
"language_settings.test_area" = "Language Switch Test";
|
||||
"language_settings.test_region" = "Test Area";
|
||||
"language_settings.token_success" = "✅ Token obtained successfully";
|
||||
"language_settings.bucket" = "Bucket: %@";
|
||||
"language_settings.region" = "Region: %@";
|
||||
"language_settings.app_id" = "App ID: %@";
|
||||
"language_settings.custom_domain" = "Custom Domain: %@";
|
||||
"language_settings.accelerate_enabled" = "Enabled";
|
||||
"language_settings.accelerate_disabled" = "Disabled";
|
||||
"language_settings.accelerate_status" = "Acceleration: %@";
|
||||
"language_settings.expiration_date" = "Expiration Date: %@";
|
||||
"language_settings.remaining_time" = "Remaining Time: %d seconds";
|
||||
"language_settings.test_cos_token" = "Test Tencent Cloud COS Token";
|
||||
"language_settings.title" = "Language Settings";
|
||||
|
||||
// MARK: - App Settings
|
||||
"app_settings.error" = "Error";
|
||||
"app_settings.confirm" = "Confirm";
|
||||
"app_settings.nickname_limit" = "Nickname must be 15 characters or less";
|
||||
"app_settings.take_photo" = "Take Photo";
|
||||
"app_settings.select_from_album" = "Select from Album";
|
||||
|
||||
// MARK: - Test
|
||||
"test.test_page" = "Test Page";
|
||||
"test.test_description" = "This is a test page\nfor verifying navigation functionality";
|
||||
"test.test_button" = "Test Button";
|
||||
"test.back" = "Back";
|
||||
|
||||
// MARK: - Image Picker
|
||||
"image_picker.loading_image" = "Loading image...";
|
||||
"image_picker.cancel" = "Cancel";
|
||||
"image_picker.confirm" = "Confirm";
|
||||
|
||||
// MARK: - Content View
|
||||
"content_view.log_level" = "Log Level:";
|
||||
"content_view.no_log" = "No Log";
|
||||
"content_view.basic_log" = "Basic Log";
|
||||
"content_view.detailed_log" = "Detailed Log";
|
||||
"content_view.api_test_result" = "API Test Result:";
|
||||
"content_view.status" = "Status: %@";
|
||||
"content_view.message" = "Message: %@";
|
||||
"content_view.version" = "Version: %@";
|
||||
"content_view.unknown" = "Unknown";
|
||||
"content_view.timestamp" = "Timestamp: %d";
|
||||
"content_view.config" = "Configuration:";
|
||||
|
||||
// MARK: - Screen Adapter
|
||||
"screen_adapter.method1" = "Method 1: Direct Call";
|
||||
"screen_adapter.method2" = "Method 2: View Extension";
|
||||
"screen_adapter.method3" = "Method 3: Ratio Calculation";
|
||||
|
||||
// MARK: - Config
|
||||
"config.api_test" = "API Configuration Test";
|
||||
"config.loading" = "Loading configuration...";
|
||||
"config.error" = "Error";
|
||||
"config.feature_list" = "Feature List";
|
||||
"config.settings" = "Settings";
|
||||
"config.last_updated" = "Last Updated: %@";
|
||||
"config.click_to_load" = "Click the button below to load configuration";
|
||||
"config.use_new_tca" = "Use new TCA API component";
|
||||
"config.clear_error" = "Clear Error";
|
@@ -125,4 +125,81 @@
|
||||
"appSetting.checkUpdates" = "检查更新";
|
||||
"appSetting.logout" = "退出登录";
|
||||
"appSetting.aboutUs" = "关于我们";
|
||||
"appSetting.deactivateAccount" = "注销帐号";
|
||||
"appSetting.logoutAccount" = "退出账户";
|
||||
|
||||
// MARK: - Detail
|
||||
"detail.title" = "享受你的生活";
|
||||
|
||||
// MARK: - Edit Feed
|
||||
"edit_feed.uploading_progress" = "正在上传图片...%d%%";
|
||||
|
||||
// MARK: - Web View
|
||||
"web_view.load_failed" = "无法加载页面";
|
||||
"web_view.open_webpage" = "打开网页";
|
||||
|
||||
// MARK: - Language Settings
|
||||
"language_settings.select_language" = "选择语言";
|
||||
"language_settings.current_language" = "当前语言";
|
||||
"language_settings.language_info" = "语言信息";
|
||||
"language_settings.test_area" = "语言切换测试";
|
||||
"language_settings.test_region" = "测试区域";
|
||||
"language_settings.token_success" = "✅ Token 获取成功";
|
||||
"language_settings.bucket" = "存储桶: %@";
|
||||
"language_settings.region" = "地域: %@";
|
||||
"language_settings.app_id" = "应用ID: %@";
|
||||
"language_settings.custom_domain" = "自定义域名: %@";
|
||||
"language_settings.accelerate_enabled" = "启用";
|
||||
"language_settings.accelerate_disabled" = "禁用";
|
||||
"language_settings.accelerate_status" = "加速: %@";
|
||||
"language_settings.expiration_date" = "过期时间: %@";
|
||||
"language_settings.remaining_time" = "剩余时间: %d秒";
|
||||
"language_settings.test_cos_token" = "测试腾讯云 COS Token";
|
||||
"language_settings.title" = "语言设置";
|
||||
|
||||
// MARK: - App Settings
|
||||
"app_settings.error" = "错误";
|
||||
"app_settings.confirm" = "确定";
|
||||
"app_settings.nickname_limit" = "昵称最长15个字符";
|
||||
"app_settings.take_photo" = "拍照";
|
||||
"app_settings.select_from_album" = "从相册选择";
|
||||
|
||||
// MARK: - Test
|
||||
"test.test_page" = "测试页面";
|
||||
"test.test_description" = "这是一个测试用的页面\n用于验证导航跳转功能";
|
||||
"test.test_button" = "测试按钮";
|
||||
"test.back" = "返回";
|
||||
|
||||
// MARK: - Image Picker
|
||||
"image_picker.loading_image" = "加载图片中...";
|
||||
"image_picker.cancel" = "取消";
|
||||
"image_picker.confirm" = "确认";
|
||||
|
||||
// MARK: - Content View
|
||||
"content_view.log_level" = "日志级别:";
|
||||
"content_view.no_log" = "无日志";
|
||||
"content_view.basic_log" = "基础日志";
|
||||
"content_view.detailed_log" = "详细日志";
|
||||
"content_view.api_test_result" = "API 测试结果:";
|
||||
"content_view.status" = "状态: %@";
|
||||
"content_view.message" = "消息: %@";
|
||||
"content_view.version" = "版本: %@";
|
||||
"content_view.unknown" = "未知";
|
||||
"content_view.timestamp" = "时间戳: %d";
|
||||
"content_view.config" = "配置:";
|
||||
|
||||
// MARK: - Screen Adapter
|
||||
"screen_adapter.method1" = "方法1: 直接调用";
|
||||
"screen_adapter.method2" = "方法2: View Extension";
|
||||
"screen_adapter.method3" = "方法3: 比例计算";
|
||||
|
||||
// MARK: - Config
|
||||
"config.api_test" = "API 配置测试";
|
||||
"config.loading" = "正在加载配置...";
|
||||
"config.error" = "错误";
|
||||
"config.feature_list" = "功能列表";
|
||||
"config.settings" = "设置";
|
||||
"config.last_updated" = "最后更新: %@";
|
||||
"config.click_to_load" = "点击下方按钮加载配置";
|
||||
"config.use_new_tca" = "使用新的 TCA API 组件";
|
||||
"config.clear_error" = "清除错误";
|
||||
|
@@ -137,91 +137,91 @@ private struct SimpleErrorView: View {
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#if DEBUG
|
||||
struct APILoadingEffectView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ZStack {
|
||||
// 模拟背景
|
||||
Rectangle()
|
||||
.fill(Color.blue.opacity(0.3))
|
||||
.ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 20) {
|
||||
Text("背景内容")
|
||||
.font(.title)
|
||||
|
||||
Button("测试按钮") {
|
||||
debugInfoSync("按钮被点击了!")
|
||||
}
|
||||
.padding()
|
||||
.background(Color.blue)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(8)
|
||||
}
|
||||
|
||||
// Loading Effect View
|
||||
APILoadingEffectView()
|
||||
}
|
||||
.previewDisplayName("API Loading Effect")
|
||||
.onAppear {
|
||||
// 模拟不同状态的预览
|
||||
Task {
|
||||
let manager = APILoadingManager.shared
|
||||
|
||||
// 添加 loading
|
||||
let id1 = manager.startLoading()
|
||||
|
||||
// 2秒后添加错误
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
Task {
|
||||
manager.setError(id1, errorMessage: "网络连接失败,请检查网络设置")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview Helpers
|
||||
|
||||
/// 预览用的测试状态
|
||||
private struct PreviewStateModifier: ViewModifier {
|
||||
let showLoading: Bool
|
||||
let showError: Bool
|
||||
let errorMessage: String
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.onAppear {
|
||||
Task {
|
||||
let manager = APILoadingManager.shared
|
||||
|
||||
if showLoading {
|
||||
let _ = manager.startLoading()
|
||||
}
|
||||
|
||||
if showError {
|
||||
let id = manager.startLoading()
|
||||
try? await Task.sleep(nanoseconds: 1_000_000_000) // 1秒
|
||||
manager.setError(id, errorMessage: errorMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
/// 添加预览状态
|
||||
func previewLoadingState(
|
||||
showLoading: Bool = false,
|
||||
showError: Bool = false,
|
||||
errorMessage: String = "示例错误信息"
|
||||
) -> some View {
|
||||
self.modifier(PreviewStateModifier(
|
||||
showLoading: showLoading,
|
||||
showError: showError,
|
||||
errorMessage: errorMessage
|
||||
))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
//#if DEBUG
|
||||
//struct APILoadingEffectView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// ZStack {
|
||||
// // 模拟背景
|
||||
// Rectangle()
|
||||
// .fill(Color.blue.opacity(0.3))
|
||||
// .ignoresSafeArea()
|
||||
//
|
||||
// VStack(spacing: 20) {
|
||||
// Text("背景内容")
|
||||
// .font(.title)
|
||||
//
|
||||
// Button("测试按钮") {
|
||||
// debugInfoSync("按钮被点击了!")
|
||||
// }
|
||||
// .padding()
|
||||
// .background(Color.blue)
|
||||
// .foregroundColor(.white)
|
||||
// .cornerRadius(8)
|
||||
// }
|
||||
//
|
||||
// // Loading Effect View
|
||||
// APILoadingEffectView()
|
||||
// }
|
||||
// .previewDisplayName("API Loading Effect")
|
||||
// .onAppear {
|
||||
// // 模拟不同状态的预览
|
||||
// Task {
|
||||
// let manager = APILoadingManager.shared
|
||||
//
|
||||
// // 添加 loading
|
||||
// let id1 = manager.startLoading()
|
||||
//
|
||||
// // 2秒后添加错误
|
||||
// DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
// Task {
|
||||
// manager.setError(id1, errorMessage: "网络连接失败,请检查网络设置")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//// MARK: - Preview Helpers
|
||||
//
|
||||
///// 预览用的测试状态
|
||||
//private struct PreviewStateModifier: ViewModifier {
|
||||
// let showLoading: Bool
|
||||
// let showError: Bool
|
||||
// let errorMessage: String
|
||||
//
|
||||
// func body(content: Content) -> some View {
|
||||
// content
|
||||
// .onAppear {
|
||||
// Task {
|
||||
// let manager = APILoadingManager.shared
|
||||
//
|
||||
// if showLoading {
|
||||
// let _ = manager.startLoading()
|
||||
// }
|
||||
//
|
||||
// if showError {
|
||||
// let id = manager.startLoading()
|
||||
// try? await Task.sleep(nanoseconds: 1_000_000_000) // 1秒
|
||||
// manager.setError(id, errorMessage: errorMessage)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//extension View {
|
||||
// /// 添加预览状态
|
||||
// func previewLoadingState(
|
||||
// showLoading: Bool = false,
|
||||
// showError: Bool = false,
|
||||
// errorMessage: String = "示例错误信息"
|
||||
// ) -> some View {
|
||||
// self.modifier(PreviewStateModifier(
|
||||
// showLoading: showLoading,
|
||||
// showError: showError,
|
||||
// errorMessage: errorMessage
|
||||
// ))
|
||||
// }
|
||||
//}
|
||||
//#endif
|
||||
|
@@ -134,4 +134,18 @@ extension APILoadingManager {
|
||||
return .failure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Global Convenience Methods
|
||||
|
||||
/// 全局便捷方法:同步设置错误信息(fire-and-forget)
|
||||
/// - Parameters:
|
||||
/// - id: 加载 ID
|
||||
/// - errorMessage: 错误信息
|
||||
func setAPILoadingErrorSync(_ id: UUID, errorMessage: String) {
|
||||
Task {
|
||||
await MainActor.run {
|
||||
APILoadingManager.shared.setError(id, errorMessage: errorMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -114,26 +114,51 @@ class LocalizationManager: ObservableObject {
|
||||
}
|
||||
|
||||
// MARK: - SwiftUI Extensions
|
||||
// extension View {
|
||||
// /// 应用本地化字符串
|
||||
// /// - Parameter key: 本地化 key
|
||||
// /// - Returns: 带有本地化文本的视图
|
||||
// @MainActor
|
||||
// func localized(_ key: String) -> some View {
|
||||
// self.modifier(LocalizedTextModifier(key: key))
|
||||
// }
|
||||
// }
|
||||
extension View {
|
||||
/// 应用本地化字符串
|
||||
/// - Parameter key: 本地化 key
|
||||
/// - Returns: 带有本地化文本的视图
|
||||
@MainActor
|
||||
func localized(_ key: String) -> some View {
|
||||
self.modifier(LocalizedTextModifier(key: key))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 便捷方法
|
||||
// extension String {
|
||||
// /// 获取本地化字符串
|
||||
// @MainActor
|
||||
// var localized: String {
|
||||
// return LocalizationManager.shared.localizedString(self)
|
||||
// }
|
||||
// /// 获取本地化字符串(带参数)
|
||||
// @MainActor
|
||||
// func localized(arguments: CVarArg...) -> String {
|
||||
// return LocalizationManager.shared.localizedString(self, arguments: arguments)
|
||||
// }
|
||||
// }
|
||||
extension String {
|
||||
/// 获取本地化字符串
|
||||
@MainActor
|
||||
var localized: String {
|
||||
return LocalizationManager.shared.localizedString(self)
|
||||
}
|
||||
/// 获取本地化字符串(带参数)
|
||||
@MainActor
|
||||
func localized(arguments: CVarArg...) -> String {
|
||||
return LocalizationManager.shared.localizedString(self, arguments: arguments)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 全局本地化方法
|
||||
/// 全局本地化字符串获取方法
|
||||
/// 使用 LocalizationManager 而不是系统语言设置
|
||||
/// - Parameters:
|
||||
/// - key: 本地化 key
|
||||
/// - comment: 注释(保持与 NSLocalizedString 兼容)
|
||||
/// - Returns: 本地化后的字符串
|
||||
@MainActor
|
||||
func LocalizedString(_ key: String, comment: String = "") -> String {
|
||||
return LocalizationManager.shared.localizedString(key)
|
||||
}
|
||||
|
||||
// MARK: - LocalizedTextModifier
|
||||
/// 本地化文本修饰符
|
||||
struct LocalizedTextModifier: ViewModifier {
|
||||
let key: String
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.onAppear {
|
||||
// 这里可以添加动态更新逻辑
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,25 +6,25 @@ struct ScreenAdapterExample: View {
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
VStack(spacing: 20) {
|
||||
Text(LocalizedString("screen_adapter.method1", comment: ""))
|
||||
.font(.headline)
|
||||
.padding()
|
||||
.background(Color.blue.opacity(0.1))
|
||||
.cornerRadius(8)
|
||||
|
||||
// 方法1: 直接使用 ScreenAdapter 静态方法
|
||||
Text("方法1: 直接调用")
|
||||
.font(.system(size: ScreenAdapter.fontSize(16, for: geometry.size.width)))
|
||||
.padding(.leading, ScreenAdapter.width(20, for: geometry.size.width))
|
||||
.padding(.top, ScreenAdapter.height(50, for: geometry.size.height))
|
||||
Text(LocalizedString("screen_adapter.method2", comment: ""))
|
||||
.font(.headline)
|
||||
.padding()
|
||||
.background(Color.green.opacity(0.1))
|
||||
.cornerRadius(8)
|
||||
|
||||
// 方法2: 使用 View Extension (推荐)
|
||||
Text("方法2: View Extension")
|
||||
.adaptedFont(16)
|
||||
.adaptedHeight(50)
|
||||
|
||||
// 方法3: 使用比例计算
|
||||
Text("方法3: 比例计算")
|
||||
.font(.system(size: 16 * ScreenAdapter.widthRatio(for: geometry.size.width)))
|
||||
.padding(.top, 50 * ScreenAdapter.heightRatio(for: geometry.size.height))
|
||||
|
||||
Spacer()
|
||||
Text(LocalizedString("screen_adapter.method3", comment: ""))
|
||||
.font(.headline)
|
||||
.padding()
|
||||
.background(Color.orange.opacity(0.1))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -31,444 +31,327 @@ struct AppSettingView: View {
|
||||
@State private var errorMessage: String? = nil
|
||||
|
||||
var body: some View {
|
||||
WithViewStore(store, observe: { $0 }) { viewStore in
|
||||
WithPerceptionTracking {
|
||||
WithPerceptionTracking {
|
||||
mainView()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func mainView() -> some View {
|
||||
WithPerceptionTracking {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
mainContent(viewStore: viewStore)
|
||||
}
|
||||
.confirmationDialog(
|
||||
"请选择图片来源",
|
||||
isPresented: $showActionSheet,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button("拍照") { showCamera = true }
|
||||
Button("从相册选择") { showPhotoPicker = true }
|
||||
Button("取消", role: .cancel) {}
|
||||
}
|
||||
.photosPicker(
|
||||
isPresented: $showPhotoPicker,
|
||||
selection: $selectedPhotoItems,
|
||||
maxSelectionCount: 1,
|
||||
matching: .images
|
||||
)
|
||||
.sheet(isPresented: $showCamera) {
|
||||
CameraPicker { image in
|
||||
print("[LOG] CameraPicker回调,image: \(image != nil)")
|
||||
if let image = image {
|
||||
print("[LOG] CameraPicker获得图片,直接上传头像")
|
||||
if let data = image.jpegData(compressionQuality: 0.8) {
|
||||
viewStore.send(AppSettingFeature.Action.avatarSelected(data))
|
||||
// 背景图片
|
||||
Image("bg")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.clipped()
|
||||
.ignoresSafeArea(.all)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
// 顶部导航栏
|
||||
HStack {
|
||||
Button(action: {
|
||||
store.send(.dismissTapped)
|
||||
}) {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(.system(size: 24, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.frame(width: 44, height: 44)
|
||||
}
|
||||
} else {
|
||||
errorMessage = "拍照失败,请重试"
|
||||
// 强制关闭所有弹窗
|
||||
showPreview = false
|
||||
showCamera = false
|
||||
showPhotoPicker = false
|
||||
showActionSheet = false
|
||||
print("[LOG] CameraPicker无图片,弹出错误提示")
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(LocalizedString("app_settings.title", comment: "设置"))
|
||||
.font(.system(size: 18, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
|
||||
Spacer()
|
||||
|
||||
// 占位,保持标题居中
|
||||
Color.clear
|
||||
.frame(width: 44, height: 44)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 8)
|
||||
|
||||
// 主要内容区域
|
||||
ScrollView {
|
||||
VStack(spacing: 20) {
|
||||
// 头像设置区域
|
||||
avatarSection()
|
||||
|
||||
// 个人信息设置区域
|
||||
personalInfoSection()
|
||||
|
||||
// 其他设置区域
|
||||
otherSettingsSection()
|
||||
|
||||
// 退出登录按钮
|
||||
logoutSection()
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 20)
|
||||
.padding(.bottom, 40)
|
||||
}
|
||||
showCamera = false
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: $showPreview) {
|
||||
ImagePreviewView(
|
||||
images: $selectedImages,
|
||||
currentIndex: .constant(0),
|
||||
onConfirm: {
|
||||
print("[LOG] 预览确认,准备上传头像")
|
||||
if let image = selectedImages.first, let data = image.jpegData(compressionQuality: 0.8) {
|
||||
viewStore.send(AppSettingFeature.Action.avatarSelected(data))
|
||||
}
|
||||
showPreview = false
|
||||
},
|
||||
onCancel: {
|
||||
print("[LOG] 预览取消")
|
||||
showPreview = false
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
// 头像选择器
|
||||
.sheet(isPresented: $showImagePickerSheet) {
|
||||
ImagePickerWithPreviewView(
|
||||
store: pickerStore,
|
||||
onUpload: { images in
|
||||
if let firstImage = images.first,
|
||||
let imageData = firstImage.jpegData(compressionQuality: 0.8) {
|
||||
store.send(AppSettingFeature.Action.avatarSelected(imageData))
|
||||
}
|
||||
showImagePickerSheet = false
|
||||
},
|
||||
onCancel: {
|
||||
showImagePickerSheet = false
|
||||
}
|
||||
)
|
||||
}
|
||||
// 相机拍照
|
||||
.sheet(isPresented: $showCamera) {
|
||||
ImagePickerWithPreviewView(
|
||||
store: pickerStore,
|
||||
onUpload: { images in
|
||||
if let firstImage = images.first,
|
||||
let imageData = firstImage.jpegData(compressionQuality: 0.8) {
|
||||
store.send(AppSettingFeature.Action.avatarSelected(imageData))
|
||||
}
|
||||
showCamera = false
|
||||
},
|
||||
onCancel: {
|
||||
showCamera = false
|
||||
}
|
||||
)
|
||||
}
|
||||
// 昵称编辑弹窗
|
||||
.alert(LocalizedString("app_settings.edit_nickname", comment: "编辑昵称"), isPresented: $showNicknameAlert) {
|
||||
TextField(LocalizedString("app_settings.nickname_placeholder", comment: "请输入昵称"), text: $nicknameInput)
|
||||
Button(LocalizedString("app_settings.cancel", comment: "取消")) {
|
||||
showNicknameAlert = false
|
||||
nicknameInput = ""
|
||||
}
|
||||
Button(LocalizedString("app_settings.confirm", comment: "确认")) {
|
||||
let trimmed = nicknameInput.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !trimmed.isEmpty {
|
||||
store.send(.nicknameEditConfirmed(trimmed))
|
||||
}
|
||||
showNicknameAlert = false
|
||||
nicknameInput = ""
|
||||
}
|
||||
} message: {
|
||||
Text(LocalizedString("app_settings.nickname_tip", comment: "请输入新的昵称"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 头像设置区域
|
||||
@ViewBuilder
|
||||
private func avatarSection() -> some View {
|
||||
WithPerceptionTracking {
|
||||
VStack(spacing: 16) {
|
||||
// 头像
|
||||
Button(action: {
|
||||
showImagePickerSheet = true
|
||||
}) {
|
||||
AsyncImage(url: URL(string: store.userInfo?.avatar ?? "")) { image in
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
} placeholder: {
|
||||
Image(systemName: "person.circle.fill")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.frame(width: 80, height: 80)
|
||||
.clipShape(Circle())
|
||||
.overlay(
|
||||
Circle()
|
||||
.stroke(Color.white.opacity(0.3), lineWidth: 2)
|
||||
)
|
||||
}
|
||||
.onChange(of: selectedPhotoItems) { items in
|
||||
print("[LOG] PhotosPicker选中items: \(items.count)")
|
||||
guard !items.isEmpty else { return }
|
||||
isLoading = true
|
||||
selectedImages = []
|
||||
let group = DispatchGroup()
|
||||
var tempImages: [UIImage] = []
|
||||
for item in items {
|
||||
group.enter()
|
||||
item.loadTransferable(type: Data.self) { result in
|
||||
defer { group.leave() }
|
||||
if let data = try? result.get(), let uiImage = UIImage(data: data) {
|
||||
DispatchQueue.main.async {
|
||||
tempImages.append(uiImage)
|
||||
print("[LOG] 成功加载图片,当前tempImages数量: \(tempImages.count)")
|
||||
}
|
||||
} else {
|
||||
print("[LOG] 图片加载失败")
|
||||
}
|
||||
}
|
||||
}
|
||||
DispatchQueue.global().async {
|
||||
group.wait()
|
||||
DispatchQueue.main.async {
|
||||
isLoading = false
|
||||
print("[LOG] 所有图片加载完成,tempImages数量: \(tempImages.count)")
|
||||
if tempImages.isEmpty {
|
||||
errorMessage = "图片加载失败,请重试"
|
||||
// 强制关闭所有弹窗
|
||||
showPreview = false
|
||||
showCamera = false
|
||||
showPhotoPicker = false
|
||||
showActionSheet = false
|
||||
print("[LOG] PhotosPicker图片加载失败,弹出错误提示")
|
||||
} else {
|
||||
// 先设置selectedImages,确保预览组件能接收到图片
|
||||
selectedImages = tempImages
|
||||
print("[LOG] selectedImages已设置,数量: \(selectedImages.count)")
|
||||
// 确保在主线程上设置showPreview
|
||||
DispatchQueue.main.async {
|
||||
showPreview = true
|
||||
print("[LOG] showPreview已设置为true")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert(isPresented: Binding<Bool>(
|
||||
get: { errorMessage != nil },
|
||||
set: { if !$0 { errorMessage = nil } }
|
||||
)) {
|
||||
print("[LOG] 错误弹窗弹出: \(errorMessage ?? "")")
|
||||
return Alert(title: Text("错误"), message: Text(errorMessage ?? ""), dismissButton: .default(Text("确定"), action: {
|
||||
// 强制关闭所有弹窗,放到action中,避免在视图更新周期set状态
|
||||
showPreview = false
|
||||
showCamera = false
|
||||
showPhotoPicker = false
|
||||
showActionSheet = false
|
||||
}))
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
.alert("修改昵称", isPresented: $showNicknameAlert) {
|
||||
nicknameAlertContent(viewStore: viewStore)
|
||||
} message: {
|
||||
Text("昵称最长15个字符")
|
||||
}
|
||||
.sheet(isPresented: userAgreementBinding(viewStore: viewStore)) {
|
||||
WebView(url: URL(string: "https://www.yana.com/user-agreement")!)
|
||||
}
|
||||
.sheet(isPresented: privacyPolicyBinding(viewStore: viewStore)) {
|
||||
WebView(url: URL(string: "https://www.yana.com/privacy-policy")!)
|
||||
}
|
||||
|
||||
Text(LocalizedString("app_settings.tap_to_change_avatar", comment: "点击更换头像"))
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
}
|
||||
.padding(.vertical, 20)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color.white.opacity(0.1))
|
||||
.cornerRadius(12)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 主要内容
|
||||
private func mainContent(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
|
||||
ZStack {
|
||||
Color(red: 22/255, green: 17/255, blue: 44/255).ignoresSafeArea()
|
||||
|
||||
// MARK: - 个人信息设置区域
|
||||
@ViewBuilder
|
||||
private func personalInfoSection() -> some View {
|
||||
WithPerceptionTracking {
|
||||
VStack(spacing: 0) {
|
||||
topBar
|
||||
ScrollView {
|
||||
WithPerceptionTracking {
|
||||
VStack(spacing: 32) {
|
||||
// 头像区域
|
||||
avatarSection(viewStore: viewStore)
|
||||
// 昵称设置项
|
||||
nicknameSection(viewStore: viewStore)
|
||||
// 设置项区域
|
||||
settingsSection(viewStore: viewStore)
|
||||
// 退出登录按钮
|
||||
logoutButton(viewStore: viewStore)
|
||||
}
|
||||
// 昵称设置
|
||||
SettingRow(
|
||||
icon: "person",
|
||||
title: LocalizedString("app_settings.nickname", comment: "昵称"),
|
||||
subtitle: store.userInfo?.nick ?? LocalizedString("app_settings.not_set", comment: "未设置"),
|
||||
action: {
|
||||
nicknameInput = store.userInfo?.nick ?? ""
|
||||
showNicknameAlert = true
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Divider()
|
||||
.background(Color.white.opacity(0.2))
|
||||
.padding(.leading, 50)
|
||||
|
||||
// 用户ID
|
||||
SettingRow(
|
||||
icon: "number",
|
||||
title: LocalizedString("app_settings.user_id", comment: "用户ID"),
|
||||
subtitle: "\(store.userInfo?.uid ?? 0)",
|
||||
action: nil
|
||||
)
|
||||
}
|
||||
.background(Color.white.opacity(0.1))
|
||||
.cornerRadius(12)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 头像区域
|
||||
private func avatarSection(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
avatarImageView(viewStore: viewStore)
|
||||
.onTapGesture {
|
||||
showActionSheet = true
|
||||
}
|
||||
cameraButton(viewStore: viewStore)
|
||||
}
|
||||
.padding(.top, 24)
|
||||
}
|
||||
|
||||
// MARK: - 头像图片视图
|
||||
|
||||
// MARK: - 其他设置区域
|
||||
@ViewBuilder
|
||||
private func avatarImageView(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
|
||||
if viewStore.isUploadingAvatar || viewStore.isLoadingUserInfo {
|
||||
loadingAvatarView
|
||||
} else if let avatarURLString = viewStore.avatarURL, !avatarURLString.isEmpty, let avatarURL = URL(string: avatarURLString) {
|
||||
networkAvatarView(url: avatarURL)
|
||||
} else {
|
||||
defaultAvatarView
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 加载状态头像
|
||||
private var loadingAvatarView: some View {
|
||||
Circle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 120, height: 120)
|
||||
.overlay(
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.scaleEffect(1.2)
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - 网络头像
|
||||
private func networkAvatarView(url: URL) -> some View {
|
||||
CachedAsyncImage(url: url.absoluteString) { image in
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
} placeholder: {
|
||||
defaultAvatarView
|
||||
}
|
||||
.frame(width: 120, height: 120)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
|
||||
// MARK: - 默认头像
|
||||
private var defaultAvatarView: some View {
|
||||
Circle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 120, height: 120)
|
||||
.overlay(
|
||||
Image(systemName: "person.fill")
|
||||
.font(.system(size: 40))
|
||||
.foregroundColor(.white)
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - 相机按钮
|
||||
private func cameraButton(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
|
||||
Button(action: {
|
||||
showActionSheet = true
|
||||
}) {
|
||||
ZStack {
|
||||
Circle().fill(Color.purple).frame(width: 36, height: 36)
|
||||
Image(systemName: "camera.fill")
|
||||
.foregroundColor(.white)
|
||||
private func otherSettingsSection() -> some View {
|
||||
WithPerceptionTracking {
|
||||
VStack(spacing: 0) {
|
||||
SettingRow(
|
||||
icon: "hand.raised",
|
||||
title: LocalizedString("app_settings.personal_info_permissions", comment: "个人信息权限"),
|
||||
subtitle: LocalizedString("app_settings.manage_permissions", comment: "管理权限"),
|
||||
action: { store.send(.personalInfoPermissionsTapped) }
|
||||
)
|
||||
|
||||
Divider()
|
||||
.background(Color.white.opacity(0.2))
|
||||
.padding(.leading, 50)
|
||||
|
||||
SettingRow(
|
||||
icon: "questionmark.circle",
|
||||
title: LocalizedString("app_settings.help", comment: "帮助"),
|
||||
subtitle: LocalizedString("app_settings.get_help", comment: "获取帮助"),
|
||||
action: { store.send(.helpTapped) }
|
||||
)
|
||||
|
||||
Divider()
|
||||
.background(Color.white.opacity(0.2))
|
||||
.padding(.leading, 50)
|
||||
|
||||
SettingRow(
|
||||
icon: "trash",
|
||||
title: LocalizedString("app_settings.clear_cache", comment: "清除缓存"),
|
||||
subtitle: LocalizedString("app_settings.free_up_space", comment: "释放空间"),
|
||||
action: { store.send(.clearCacheTapped) }
|
||||
)
|
||||
|
||||
Divider()
|
||||
.background(Color.white.opacity(0.2))
|
||||
.padding(.leading, 50)
|
||||
|
||||
SettingRow(
|
||||
icon: "arrow.clockwise",
|
||||
title: LocalizedString("app_settings.check_updates", comment: "检查更新"),
|
||||
subtitle: LocalizedString("app_settings.latest_version", comment: "最新版本"),
|
||||
action: { store.send(.checkUpdatesTapped) }
|
||||
)
|
||||
|
||||
Divider()
|
||||
.background(Color.white.opacity(0.2))
|
||||
.padding(.leading, 50)
|
||||
|
||||
SettingRow(
|
||||
icon: "person.crop.circle.badge.minus",
|
||||
title: LocalizedString("app_settings.deactivate_account", comment: "注销账号"),
|
||||
subtitle: LocalizedString("app_settings.permanent_deletion", comment: "永久删除"),
|
||||
action: { store.send(.deactivateAccountTapped) }
|
||||
)
|
||||
|
||||
Divider()
|
||||
.background(Color.white.opacity(0.2))
|
||||
.padding(.leading, 50)
|
||||
|
||||
SettingRow(
|
||||
icon: "info.circle",
|
||||
title: LocalizedString("app_settings.about_us", comment: "关于我们"),
|
||||
subtitle: LocalizedString("app_settings.app_info", comment: "应用信息"),
|
||||
action: { store.send(.aboutUsTapped) }
|
||||
)
|
||||
}
|
||||
}
|
||||
.offset(x: 8, y: 8)
|
||||
}
|
||||
|
||||
// MARK: - 昵称设置项
|
||||
private func nicknameSection(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
Text(NSLocalizedString("appSetting.nickname", comment: "Nickname"))
|
||||
.foregroundColor(.white)
|
||||
Spacer()
|
||||
Text(viewStore.nickname)
|
||||
.foregroundColor(.gray)
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.padding(.horizontal, 32)
|
||||
.padding(.vertical, 18)
|
||||
.onTapGesture {
|
||||
nicknameInput = viewStore.nickname
|
||||
showNicknameAlert = true
|
||||
}
|
||||
|
||||
Divider().background(Color.gray.opacity(0.3))
|
||||
.padding(.horizontal, 32)
|
||||
.background(Color.white.opacity(0.1))
|
||||
.cornerRadius(12)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 设置项区域
|
||||
private func settingsSection(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
|
||||
VStack(spacing: 0) {
|
||||
personalInfoPermissionsRow(viewStore: viewStore)
|
||||
helpRow(viewStore: viewStore)
|
||||
clearCacheRow(viewStore: viewStore)
|
||||
checkUpdatesRow(viewStore: viewStore)
|
||||
aboutUsRow(viewStore: viewStore)
|
||||
}
|
||||
.background(Color.clear)
|
||||
.padding(.horizontal, 0)
|
||||
}
|
||||
|
||||
// MARK: - 个人信息权限行
|
||||
private func personalInfoPermissionsRow(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
|
||||
settingRow(
|
||||
title: NSLocalizedString("appSetting.personalInfoPermissions", comment: "Personal Information and Permissions"),
|
||||
action: { viewStore.send(.personalInfoPermissionsTapped) }
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - 帮助行
|
||||
private func helpRow(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
|
||||
settingRow(
|
||||
title: NSLocalizedString("appSetting.help", comment: "Help"),
|
||||
action: { viewStore.send(.helpTapped) }
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - 清除缓存行
|
||||
private func clearCacheRow(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
|
||||
settingRow(
|
||||
title: NSLocalizedString("appSetting.clearCache", comment: "Clear Cache"),
|
||||
action: { viewStore.send(.clearCacheTapped) }
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - 检查更新行
|
||||
private func checkUpdatesRow(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
|
||||
settingRow(
|
||||
title: NSLocalizedString("appSetting.checkUpdates", comment: "Check for Updates"),
|
||||
action: { viewStore.send(.checkUpdatesTapped) }
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - 关于我们行
|
||||
private func aboutUsRow(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
|
||||
settingRow(
|
||||
title: NSLocalizedString("appSetting.aboutUs", comment: "About Us"),
|
||||
action: { viewStore.send(.aboutUsTapped) }
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - 设置项行
|
||||
private func settingRow(title: String, action: @escaping () -> Void) -> some View {
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
Text(title)
|
||||
.foregroundColor(.white)
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.padding(.horizontal, 32)
|
||||
.padding(.vertical, 18)
|
||||
.onTapGesture {
|
||||
action()
|
||||
}
|
||||
|
||||
Divider().background(Color.gray.opacity(0.3))
|
||||
.padding(.horizontal, 32)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 退出登录按钮
|
||||
private func logoutButton(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
|
||||
Button(action: {
|
||||
viewStore.send(.logoutTapped)
|
||||
}) {
|
||||
Text(NSLocalizedString("appSetting.logoutAccount", comment: "Log out of account"))
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 18)
|
||||
.background(Color.white.opacity(0.08))
|
||||
.cornerRadius(28)
|
||||
.padding(.horizontal, 32)
|
||||
}
|
||||
.padding(.bottom, 32)
|
||||
}
|
||||
|
||||
// MARK: - 用户协议绑定
|
||||
private func userAgreementBinding(viewStore: ViewStoreOf<AppSettingFeature>) -> Binding<Bool> {
|
||||
viewStore.binding(
|
||||
get: \.showUserAgreement,
|
||||
send: AppSettingFeature.Action.userAgreementDismissed
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - 隐私政策绑定
|
||||
private func privacyPolicyBinding(viewStore: ViewStoreOf<AppSettingFeature>) -> Binding<Bool> {
|
||||
viewStore.binding(
|
||||
get: \.showPrivacyPolicy,
|
||||
send: AppSettingFeature.Action.privacyPolicyDismissed
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - 昵称Alert内容
|
||||
|
||||
// MARK: - 退出登录区域
|
||||
@ViewBuilder
|
||||
private func nicknameAlertContent(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
|
||||
TextField("请输入昵称", text: $nicknameInput)
|
||||
.onChange(of: nicknameInput) { newValue in
|
||||
if newValue.count > 15 {
|
||||
nicknameInput = String(newValue.prefix(15))
|
||||
}
|
||||
}
|
||||
Button("确定") {
|
||||
let trimmed = nicknameInput.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !trimmed.isEmpty && trimmed != viewStore.nickname {
|
||||
viewStore.send(.nicknameEditConfirmed(trimmed))
|
||||
private func logoutSection() -> some View {
|
||||
WithPerceptionTracking {
|
||||
Button(action: {
|
||||
store.send(.logoutTapped)
|
||||
}) {
|
||||
Text(LocalizedString("app_settings.logout", comment: "退出登录"))
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 16)
|
||||
.background(Color.red.opacity(0.8))
|
||||
.cornerRadius(12)
|
||||
}
|
||||
}
|
||||
Button("取消", role: .cancel) {}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 顶部栏
|
||||
private var topBar: some View {
|
||||
HStack {
|
||||
WithViewStore(store, observe: { $0 }) { viewStore in
|
||||
Button(action: {
|
||||
viewStore.send(.dismissTapped)
|
||||
}) {
|
||||
Image(systemName: "chevron.left")
|
||||
// MARK: - 设置行组件
|
||||
struct SettingRow: View {
|
||||
let icon: String
|
||||
let title: String
|
||||
let subtitle: String
|
||||
let action: (() -> Void)?
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
action?()
|
||||
}) {
|
||||
HStack(spacing: 16) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 18))
|
||||
.foregroundColor(.white)
|
||||
.frame(width: 24)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(title)
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 20, weight: .medium))
|
||||
|
||||
Text(subtitle)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if action != nil {
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.5))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(NSLocalizedString("appSetting.title", comment: "Settings"))
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
|
||||
Spacer()
|
||||
|
||||
// 占位符,保持标题居中
|
||||
Color.clear
|
||||
.frame(width: 20, height: 20)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 8)
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
|
||||
// MARK: - 图片处理
|
||||
private func loadAndProcessImage(item: PhotosPickerItem, completion: @escaping @Sendable (Data?) -> Void) {
|
||||
item.loadTransferable(type: Data.self) { result in
|
||||
guard let data = try? result.get(), let uiImage = UIImage(data: data) else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
let square = cropToSquare(image: uiImage)
|
||||
let resized = resizeImage(image: square, targetSize: CGSize(width: 180, height: 180))
|
||||
let jpegData = resized.jpegData(compressionQuality: 0.8)
|
||||
completion(jpegData)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 图片处理全局函数
|
||||
private func cropToSquare(image: UIImage) -> UIImage {
|
||||
let size = min(image.size.width, image.size.height)
|
||||
let x = (image.size.width - size) / 2
|
||||
let y = (image.size.height - size) / 2
|
||||
let cropRect = CGRect(x: x, y: y, width: size, height: size)
|
||||
guard let cgImage = image.cgImage?.cropping(to: cropRect) else { return image }
|
||||
return UIImage(cgImage: cgImage, scale: image.scale, orientation: image.imageOrientation)
|
||||
}
|
||||
private func resizeImage(image: UIImage, targetSize: CGSize) -> UIImage {
|
||||
let renderer = UIGraphicsImageRenderer(size: targetSize)
|
||||
return renderer.image { _ in
|
||||
image.draw(in: CGRect(origin: .zero, size: targetSize))
|
||||
.disabled(action == nil)
|
||||
}
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@ public struct ImagePickerWithPreviewView: View {
|
||||
|
||||
@State private var loadedImages: [UIImage] = []
|
||||
@State private var isLoadingImages: Bool = false
|
||||
@State private var loadingId: UUID?
|
||||
|
||||
public init(store: StoreOf<ImagePickerWithPreviewReducer>, onUpload: @escaping ([UIImage]) -> Void, onCancel: @escaping () -> Void) {
|
||||
self.store = store
|
||||
@@ -20,29 +21,21 @@ public struct ImagePickerWithPreviewView: View {
|
||||
WithViewStore(store, observe: { $0 }) { viewStore in
|
||||
ZStack {
|
||||
Color.clear
|
||||
LoadingView(isLoading: viewStore.inner.isLoading || isLoadingImages)
|
||||
}
|
||||
.background(.clear)
|
||||
.modifier(ActionSheetModifier(viewStore: viewStore, onCancel: onCancel))
|
||||
.modifier(CameraSheetModifier(viewStore: viewStore))
|
||||
.modifier(PhotosPickerModifier(viewStore: viewStore, loadedImages: $loadedImages, isLoadingImages: $isLoadingImages))
|
||||
.modifier(PreviewCoverModifier(viewStore: viewStore, loadedImages: loadedImages, onUpload: onUpload))
|
||||
.modifier(PhotosPickerModifier(viewStore: viewStore, loadedImages: $loadedImages, isLoadingImages: $isLoadingImages, loadingId: $loadingId))
|
||||
.modifier(PreviewCoverModifier(viewStore: viewStore, loadedImages: loadedImages, onUpload: onUpload, loadingId: $loadingId))
|
||||
.modifier(ErrorToastModifier(viewStore: viewStore))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct LoadingView: View {
|
||||
let isLoading: Bool
|
||||
var body: some View {
|
||||
if isLoading {
|
||||
Color.black.opacity(0.4).ignoresSafeArea()
|
||||
ProgressView("上传中...")
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.foregroundColor(.white)
|
||||
.padding()
|
||||
.background(Color.black.opacity(0.7))
|
||||
.cornerRadius(16)
|
||||
.onChange(of: viewStore.inner.isLoading) { isLoading in
|
||||
if isLoading && loadingId == nil {
|
||||
loadingId = APILoadingManager.shared.startLoading()
|
||||
} else if !isLoading, let id = loadingId {
|
||||
APILoadingManager.shared.finishLoading(id)
|
||||
loadingId = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,8 +52,8 @@ private struct ActionSheetModifier: ViewModifier {
|
||||
),
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button("拍照") { viewStore.send(.inner(.selectSource(.camera))) }
|
||||
Button("从相册选择") { viewStore.send(.inner(.selectSource(.photoLibrary))) }
|
||||
Button(LocalizedString("app_settings.take_photo", comment: "")) { viewStore.send(.inner(.selectSource(.camera))) }
|
||||
Button(LocalizedString("app_settings.select_from_album", comment: "")) { viewStore.send(.inner(.selectSource(.photoLibrary))) }
|
||||
Button("取消", role: .cancel) { onCancel() }
|
||||
}
|
||||
}
|
||||
@@ -84,6 +77,7 @@ private struct PhotosPickerModifier: ViewModifier {
|
||||
let viewStore: ViewStoreOf<ImagePickerWithPreviewReducer>
|
||||
@Binding var loadedImages: [UIImage]
|
||||
@Binding var isLoadingImages: Bool
|
||||
@Binding var loadingId: UUID?
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.photosPicker(
|
||||
@@ -136,6 +130,7 @@ private struct PreviewCoverModifier: ViewModifier {
|
||||
let viewStore: ViewStoreOf<ImagePickerWithPreviewReducer>
|
||||
let loadedImages: [UIImage]
|
||||
let onUpload: ([UIImage]) -> Void
|
||||
@Binding var loadingId: UUID?
|
||||
func body(content: Content) -> some View {
|
||||
content.fullScreenCover(isPresented: .init(
|
||||
get: { viewStore.inner.showPreview },
|
||||
|
@@ -35,7 +35,7 @@ public struct ImagePreviewView: View {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.scaleEffect(1.5)
|
||||
Text("加载图片中...")
|
||||
Text(LocalizedString("image_picker.loading_image", comment: ""))
|
||||
.foregroundColor(.white)
|
||||
.padding(.top, 16)
|
||||
}
|
||||
@@ -43,7 +43,7 @@ public struct ImagePreviewView: View {
|
||||
Spacer()
|
||||
HStack(spacing: 24) {
|
||||
Button(action: onCancel) {
|
||||
Text("取消")
|
||||
Text(LocalizedString("image_picker.cancel", comment: ""))
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 32)
|
||||
.padding(.vertical, 12)
|
||||
@@ -51,7 +51,7 @@ public struct ImagePreviewView: View {
|
||||
.cornerRadius(20)
|
||||
}
|
||||
Button(action: onConfirm) {
|
||||
Text("确认")
|
||||
Text(LocalizedString("image_picker.confirm", comment: ""))
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 32)
|
||||
.padding(.vertical, 12)
|
||||
|
@@ -9,24 +9,39 @@ struct OptimizedDynamicCardView: View {
|
||||
let currentIndex: Int
|
||||
// 新增:图片点击回调
|
||||
let onImageTap: (_ images: [String], _ index: Int) -> Void
|
||||
// 新增:点赞回调
|
||||
let onLikeTap: (_ dynamicId: Int, _ uid: Int, _ likedUid: Int, _ worldId: Int) -> Void
|
||||
// 新增:卡片点击回调
|
||||
let onCardTap: (() -> Void)?
|
||||
// 新增:详情页模式,点击卡片不跳转
|
||||
let isDetailMode: Bool
|
||||
// 新增:点赞loading状态
|
||||
let isLikeLoading: Bool
|
||||
|
||||
init(moment: MomentsInfo, allMoments: [MomentsInfo], currentIndex: Int, onImageTap: @escaping (_ images: [String], _ index: Int) -> Void) {
|
||||
init(moment: MomentsInfo, allMoments: [MomentsInfo], currentIndex: Int, onImageTap: @escaping (_ images: [String], _ index: Int) -> Void, onLikeTap: @escaping (_ dynamicId: Int, _ uid: Int, _ likedUid: Int, _ worldId: Int) -> Void, onCardTap: (() -> Void)? = nil, isDetailMode: Bool = false, isLikeLoading: Bool = false) {
|
||||
self.moment = moment
|
||||
self.allMoments = allMoments
|
||||
self.currentIndex = currentIndex
|
||||
self.onImageTap = onImageTap
|
||||
self.onLikeTap = onLikeTap
|
||||
self.onCardTap = onCardTap
|
||||
self.isDetailMode = isDetailMode
|
||||
self.isLikeLoading = isLikeLoading
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ZStack {
|
||||
// 背景层
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(Color.clear)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(Color.white.opacity(0.1), lineWidth: 1)
|
||||
)
|
||||
.shadow(color: Color(red: 0.43, green: 0.43, blue: 0.43, opacity: 0.34), radius: 10.7, x: 0, y: 1.9)
|
||||
// 背景层 - 仅在非详情页模式下显示
|
||||
if !isDetailMode {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(Color.clear)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(Color.white.opacity(0.1), lineWidth: 1)
|
||||
)
|
||||
.shadow(color: Color(red: 0.43, green: 0.43, blue: 0.43, opacity: 0.34), radius: 10.7, x: 0, y: 1.9)
|
||||
}
|
||||
|
||||
// 内容层
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
// 用户信息
|
||||
@@ -47,14 +62,17 @@ struct OptimizedDynamicCardView: View {
|
||||
}
|
||||
.frame(width: 40, height: 40)
|
||||
.clipShape(Circle())
|
||||
.allowsHitTesting(false) // 不拦截点击事件
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(moment.nick)
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.allowsHitTesting(false) // 不拦截点击事件
|
||||
Text("ID: \(moment.uid)")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.white.opacity(0.6))
|
||||
.allowsHitTesting(false) // 不拦截点击事件
|
||||
}
|
||||
Spacer()
|
||||
// 时间(原VIP位置)
|
||||
@@ -65,6 +83,7 @@ struct OptimizedDynamicCardView: View {
|
||||
.padding(.vertical, 2)
|
||||
.background(Color.white.opacity(0.15))
|
||||
.cornerRadius(4)
|
||||
.allowsHitTesting(false) // 不拦截点击事件
|
||||
}
|
||||
|
||||
// 动态内容
|
||||
@@ -74,6 +93,7 @@ struct OptimizedDynamicCardView: View {
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
.multilineTextAlignment(.leading)
|
||||
.padding(.leading, 40 + 8) // 与用户名左边对齐
|
||||
.allowsHitTesting(false) // 不拦截点击事件
|
||||
}
|
||||
|
||||
// 优化的图片网格
|
||||
@@ -83,25 +103,46 @@ struct OptimizedDynamicCardView: View {
|
||||
onImageTap(urls, tappedIndex)
|
||||
}
|
||||
.padding(.bottom, images.count == 2 ? 46 : 0) // 两张图片时增加底部间距
|
||||
.allowsHitTesting(true) // 图片网格需要响应点击事件
|
||||
}
|
||||
|
||||
// 互动按钮
|
||||
HStack(spacing: 20) {
|
||||
// Like 按钮左对齐
|
||||
Button(action: {}) {
|
||||
// Like 按钮与用户名左侧对齐
|
||||
Button(action: {
|
||||
if !isLikeLoading {
|
||||
onLikeTap(moment.dynamicId, moment.uid, moment.uid, moment.worldId)
|
||||
}
|
||||
}) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: moment.isLike ? "heart.fill" : "heart")
|
||||
.font(.system(size: 16))
|
||||
if isLikeLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: moment.isLike ? .red : .white.opacity(0.8)))
|
||||
.scaleEffect(0.8)
|
||||
} else {
|
||||
Image(systemName: moment.isLike ? "heart.fill" : "heart")
|
||||
.font(.system(size: 16))
|
||||
}
|
||||
Text("\(moment.likeCount)")
|
||||
.font(.system(size: 14))
|
||||
}
|
||||
.foregroundColor(moment.isLike ? .red : .white.opacity(0.8))
|
||||
}
|
||||
.disabled(isLikeLoading)
|
||||
.padding(.leading, 40 + 8) // 与用户名左侧对齐(头像宽度 + 间距)
|
||||
.allowsHitTesting(true) // Like 按钮需要响应点击事件
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top, 8)
|
||||
}
|
||||
.padding(16)
|
||||
// 卡片点击手势 - 仅在非详情页模式且有回调时显示
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
if !isDetailMode, let onCardTap = onCardTap {
|
||||
onCardTap()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
preloadNearbyImages()
|
||||
|
@@ -38,20 +38,20 @@ struct UserAgreementView: View {
|
||||
|
||||
// MARK: - Private Methods
|
||||
private func createAttributedText() -> AttributedString {
|
||||
var attributedString = AttributedString(NSLocalizedString("login.agreement_policy", comment: ""))
|
||||
var attributedString = AttributedString(LocalizedString("login.agreement_policy", comment: ""))
|
||||
|
||||
// 设置默认颜色
|
||||
attributedString.foregroundColor = Color(hex: 0x666666)
|
||||
|
||||
// 找到并设置 "用户协议" 的样式和链接
|
||||
if let userServiceRange = attributedString.range(of: NSLocalizedString("login.agreement", comment: "")) {
|
||||
if let userServiceRange = attributedString.range(of: LocalizedString("login.agreement", comment: "")) {
|
||||
attributedString[userServiceRange].foregroundColor = Color(hex: 0x8A4FFF)
|
||||
attributedString[userServiceRange].underlineStyle = .single
|
||||
attributedString[userServiceRange].link = URL(string: "user-service-agreement")
|
||||
}
|
||||
|
||||
// 找到并设置 "隐私政策" 的样式和链接
|
||||
if let privacyPolicyRange = attributedString.range(of: NSLocalizedString("login.policy", comment: "")) {
|
||||
if let privacyPolicyRange = attributedString.range(of: LocalizedString("login.policy", comment: "")) {
|
||||
attributedString[privacyPolicyRange].foregroundColor = Color(hex: 0x8A4FFF)
|
||||
attributedString[privacyPolicyRange].underlineStyle = .single
|
||||
attributedString[privacyPolicyRange].link = URL(string: "privacy-policy")
|
||||
|
@@ -34,7 +34,7 @@ extension View {
|
||||
if let url = url {
|
||||
WebView(url: url)
|
||||
} else {
|
||||
Text("无法加载页面")
|
||||
Text(LocalizedString("web_view.load_failed", comment: ""))
|
||||
.foregroundColor(.red)
|
||||
.padding()
|
||||
}
|
||||
@@ -44,7 +44,7 @@ extension View {
|
||||
|
||||
#Preview {
|
||||
VStack {
|
||||
Button("打开网页") {
|
||||
Button(LocalizedString("web_view.open_webpage", comment: "")) {
|
||||
// 预览时不执行任何操作
|
||||
}
|
||||
}
|
||||
|
@@ -7,161 +7,167 @@ struct CreateFeedView: View {
|
||||
@State private var keyboardHeight: CGFloat = 0
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
GeometryReader { geometry in
|
||||
VStack(spacing: 0) {
|
||||
// 背景色
|
||||
Color(hex: 0x0C0527)
|
||||
.ignoresSafeArea()
|
||||
|
||||
// 主要内容区域(无ScrollView)
|
||||
VStack(spacing: 20) {
|
||||
// 内容输入区域
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
// 文本输入框
|
||||
ZStack(alignment: .topLeading) {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(Color.white.opacity(0.1))
|
||||
WithPerceptionTracking {
|
||||
NavigationStack {
|
||||
GeometryReader { geometry in
|
||||
VStack(spacing: 0) {
|
||||
// 背景色
|
||||
Color(hex: 0x0C0527)
|
||||
.ignoresSafeArea()
|
||||
|
||||
// 主要内容区域(无ScrollView)
|
||||
VStack(spacing: 20) {
|
||||
// 内容输入区域
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
// 文本输入框
|
||||
ZStack(alignment: .topLeading) {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(Color.white.opacity(0.1))
|
||||
.frame(height: 200) // 高度固定为200
|
||||
|
||||
if store.content.isEmpty {
|
||||
Text(NSLocalizedString("createFeed.enterContent", comment: "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)
|
||||
.frame(height: 200) // 高度固定为200
|
||||
|
||||
if store.content.isEmpty {
|
||||
Text(NSLocalizedString("createFeed.enterContent", comment: "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)
|
||||
.frame(height: 200) // 高度固定为200
|
||||
// 字符计数
|
||||
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)
|
||||
|
||||
// 字符计数
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("\(store.characterCount)/500")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(
|
||||
store.characterCount > 500 ? .red : .white.opacity(0.6)
|
||||
// 图片选择区域
|
||||
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)
|
||||
.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(NSLocalizedString("createFeed.processingImages", comment: "Processing images..."))
|
||||
.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: max(keyboardHeight, geometry.safeAreaInsets.bottom) + 100)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.ignoresSafeArea(.keyboard, edges: .bottom)
|
||||
|
||||
// 底部发布按钮 - 固定在底部
|
||||
VStack {
|
||||
Button(action: {
|
||||
store.send(.publishButtonTapped)
|
||||
}) {
|
||||
HStack {
|
||||
if store.isLoading {
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
// 加载状态
|
||||
if store.isLoading {
|
||||
HStack {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.scaleEffect(0.8)
|
||||
Text(NSLocalizedString("createFeed.publishing", comment: "Publishing..."))
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
} else {
|
||||
Text(NSLocalizedString("createFeed.publish", comment: "Publish"))
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
Text(NSLocalizedString("createFeed.processingImages", comment: "Processing images..."))
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
.padding(.top, 10)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 50)
|
||||
.background(
|
||||
Color(hex: 0x0C0527)
|
||||
)
|
||||
.cornerRadius(25)
|
||||
.disabled(store.isLoading || !store.canPublish)
|
||||
.opacity(store.isLoading || !store.canPublish ? 0.6 : 1.0)
|
||||
|
||||
// 错误提示
|
||||
if let error = store.errorMessage {
|
||||
Text(error)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.red)
|
||||
.padding(.horizontal, 20)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
// 底部间距,确保内容不被键盘遮挡
|
||||
Color.clear.frame(height: max(keyboardHeight, geometry.safeAreaInsets.bottom) + 100)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.bottom, max(keyboardHeight, geometry.safeAreaInsets.bottom) + 20)
|
||||
}
|
||||
.background(
|
||||
Color(hex: 0x0C0527)
|
||||
)
|
||||
}
|
||||
}
|
||||
.navigationTitle(NSLocalizedString("createFeed.title", comment: "Image & Text Publish"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbarBackground(.hidden, for: .navigationBar)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button(action: {
|
||||
store.send(.dismissView)
|
||||
}) {
|
||||
Image(systemName: "xmark")
|
||||
.font(.system(size: 18, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.ignoresSafeArea(.keyboard, edges: .bottom)
|
||||
|
||||
// 底部发布按钮 - 固定在底部
|
||||
VStack {
|
||||
Button(action: {
|
||||
store.send(.publishButtonTapped)
|
||||
}) {
|
||||
HStack {
|
||||
if store.isLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.scaleEffect(0.8)
|
||||
Text(NSLocalizedString("createFeed.publishing", comment: "Publishing..."))
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
} else {
|
||||
Text(NSLocalizedString("createFeed.publish", comment: "Publish"))
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 50)
|
||||
.background(
|
||||
Color(hex: 0x0C0527)
|
||||
)
|
||||
.cornerRadius(25)
|
||||
.disabled(store.isLoading || !store.canPublish)
|
||||
.opacity(store.isLoading || !store.canPublish ? 0.6 : 1.0)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.bottom, max(keyboardHeight, geometry.safeAreaInsets.bottom) + 20)
|
||||
}
|
||||
.background(
|
||||
Color(hex: 0x0C0527)
|
||||
)
|
||||
}
|
||||
}
|
||||
// 移除右上角发布按钮
|
||||
.navigationTitle(NSLocalizedString("createFeed.title", comment: "Image & Text Publish"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbarBackground(.hidden, for: .navigationBar)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button(action: {
|
||||
store.send(.dismissView)
|
||||
}) {
|
||||
Image(systemName: "xmark")
|
||||
.font(.system(size: 18, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
// 移除右上角发布按钮
|
||||
}
|
||||
}
|
||||
}
|
||||
.preferredColorScheme(.dark)
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { notification in
|
||||
if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
|
||||
keyboardHeight = keyboardFrame.height
|
||||
.preferredColorScheme(.dark)
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { notification in
|
||||
if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
|
||||
keyboardHeight = keyboardFrame.height
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
|
||||
keyboardHeight = 0
|
||||
}
|
||||
.onDisappear {
|
||||
// 确保视图消失时重置键盘状态
|
||||
keyboardHeight = 0
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
|
||||
keyboardHeight = 0
|
||||
}
|
||||
}
|
||||
|
||||
|
228
yana/Views/DetailView.swift
Normal file
228
yana/Views/DetailView.swift
Normal file
@@ -0,0 +1,228 @@
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
|
||||
struct DetailView: View {
|
||||
@State var store: StoreOf<DetailFeature>
|
||||
let onLikeSuccess: ((Int, Bool) -> Void)?
|
||||
|
||||
init(store: StoreOf<DetailFeature>, onLikeSuccess: ((Int, Bool) -> Void)? = nil) {
|
||||
self.store = store
|
||||
self.onLikeSuccess = onLikeSuccess
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// 背景图片
|
||||
Image("bg")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 0) {
|
||||
// 自定义导航栏
|
||||
WithPerceptionTracking {
|
||||
CustomNavigationBar(
|
||||
title: NSLocalizedString("detail.title", comment: "Detail page title"),
|
||||
showDeleteButton: isCurrentUserDynamic,
|
||||
isDeleteLoading: store.isDeleteLoading,
|
||||
onBack: {
|
||||
// 移除 onDismiss?() 调用,因为现在使用 dismiss()
|
||||
},
|
||||
onDelete: {
|
||||
store.send(.deleteDynamic)
|
||||
}
|
||||
)
|
||||
}
|
||||
.padding(.top, 24)
|
||||
|
||||
// 内容区域
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
// 使用OptimizedDynamicCardView显示动态内容
|
||||
WithPerceptionTracking {
|
||||
OptimizedDynamicCardView(
|
||||
moment: store.moment,
|
||||
allMoments: [store.moment], // 详情页只有一个动态
|
||||
currentIndex: 0,
|
||||
onImageTap: { images, index in
|
||||
store.send(.showImagePreview(images, index))
|
||||
},
|
||||
onLikeTap: { dynamicId, uid, likedUid, worldId in
|
||||
store.send(.likeDynamic(dynamicId, uid, likedUid, worldId))
|
||||
},
|
||||
onCardTap: nil, // 详情页不需要卡片点击
|
||||
isDetailMode: true, // 详情页模式,点击卡片不跳转
|
||||
isLikeLoading: store.isLikeLoading
|
||||
)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 16)
|
||||
.padding(.bottom, 116)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
.onAppear {
|
||||
debugInfoSync("🔍 DetailView: onAppear - moment.uid: \(store.moment.uid)")
|
||||
store.send(.onAppear)
|
||||
}
|
||||
.onChange(of: store.shouldDismiss) { shouldDismiss in
|
||||
if shouldDismiss {
|
||||
debugInfoSync("🔍 DetailView: shouldDismiss = true, 调用onDismiss")
|
||||
// 移除 onDismiss?() 移除此行,因为我们现在使用 dismiss
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: Binding(
|
||||
get: { store.showImagePreview },
|
||||
set: { _ in store.send(.hideImagePreview) }
|
||||
)) {
|
||||
WithPerceptionTracking {
|
||||
ImagePreviewPager(
|
||||
images: store.selectedImages,
|
||||
currentIndex: Binding(
|
||||
get: { store.selectedImageIndex },
|
||||
set: { newIndex in
|
||||
store.send(.showImagePreview(store.selectedImages, newIndex))
|
||||
}
|
||||
),
|
||||
onClose: {
|
||||
store.send(.imagePreviewDismissed)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否为当前用户的动态
|
||||
private var isCurrentUserDynamic: Bool {
|
||||
// 使用store中的当前用户ID进行判断
|
||||
guard let currentUserId = store.currentUserId,
|
||||
let currentUserIdInt = Int(currentUserId) else {
|
||||
debugInfoSync("🔍 DetailView: 无法获取当前用户ID - currentUserId: \(store.currentUserId ?? "nil")")
|
||||
return false
|
||||
}
|
||||
let isCurrentUser = store.moment.uid == currentUserIdInt
|
||||
debugInfoSync("🔍 DetailView: 动态用户判断 - moment.uid: \(store.moment.uid), currentUserId: \(currentUserIdInt), isCurrentUser: \(isCurrentUser)")
|
||||
return isCurrentUser
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CustomNavigationBar
|
||||
struct CustomNavigationBar: View {
|
||||
let title: String
|
||||
let showDeleteButton: Bool
|
||||
let isDeleteLoading: Bool
|
||||
let onBack: () -> Void
|
||||
let onDelete: () -> Void
|
||||
init(title: String, showDeleteButton: Bool, isDeleteLoading: Bool, onBack: @escaping () -> Void, onDelete: @escaping () -> Void) {
|
||||
self.title = title
|
||||
self.showDeleteButton = showDeleteButton
|
||||
self.isDeleteLoading = isDeleteLoading
|
||||
self.onBack = onBack
|
||||
self.onDelete = onDelete
|
||||
}
|
||||
@SwiftUI.Environment(\.dismiss) private var dismiss: SwiftUI.DismissAction
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
// 返回按钮
|
||||
Button(action: {
|
||||
onBack()
|
||||
dismiss() // 使用 dismiss 关闭视图
|
||||
}) {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(.system(size: 20, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.frame(width: 44, height: 44)
|
||||
.background(Color.black.opacity(0.3))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// 标题
|
||||
Text(title)
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
.shadow(color: .black.opacity(0.5), radius: 2, x: 0, y: 1)
|
||||
|
||||
Spacer()
|
||||
|
||||
// 删除按钮(仅在需要时显示)
|
||||
WithPerceptionTracking {
|
||||
if showDeleteButton {
|
||||
Button(action: onDelete) {
|
||||
if isDeleteLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.scaleEffect(0.8)
|
||||
.frame(width: 44, height: 44)
|
||||
.background(Color.red.opacity(0.8))
|
||||
.clipShape(Circle())
|
||||
} else {
|
||||
Image(systemName: "trash")
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.frame(width: 44, height: 44)
|
||||
.background(Color.red.opacity(0.8))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
}
|
||||
.disabled(isDeleteLoading)
|
||||
} else {
|
||||
// 占位,保持标题居中
|
||||
Color.clear
|
||||
.frame(width: 44, height: 44)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 8)
|
||||
.padding(.bottom, 12)
|
||||
.background(
|
||||
LinearGradient(
|
||||
gradient: Gradient(colors: [
|
||||
Color.black.opacity(0.4),
|
||||
Color.black.opacity(0.2),
|
||||
Color.clear
|
||||
]),
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//#Preview {
|
||||
// DetailView(
|
||||
// store: Store(
|
||||
// initialState: DetailFeature.State(
|
||||
// moment: MomentsInfo(
|
||||
// dynamicId: 1,
|
||||
// uid: 123,
|
||||
// nick: "Test User",
|
||||
// avatar: "https://example.com/avatar.jpg",
|
||||
// type: 1,
|
||||
// content: "This is a test dynamic content",
|
||||
// publishTime: Int(Date().timeIntervalSince1970 * 1000),
|
||||
// likeCount: 10,
|
||||
// isLike: false,
|
||||
// worldId: 1,
|
||||
// dynamicResList: [
|
||||
// MomentsPicture(
|
||||
// id: 1,
|
||||
// resUrl: "https://example.com/image1.jpg",
|
||||
// format: "jpg",
|
||||
// width: 800,
|
||||
// height: 600,
|
||||
// resDuration: nil
|
||||
// )
|
||||
// ]
|
||||
// )
|
||||
// )
|
||||
// ) {
|
||||
// DetailFeature()
|
||||
// }
|
||||
// )
|
||||
//}
|
@@ -5,37 +5,41 @@ import Combine
|
||||
struct EMailLoginView: View {
|
||||
let store: StoreOf<EMailLoginFeature>
|
||||
let onBack: () -> Void
|
||||
@Binding var showEmailLogin: Bool
|
||||
@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?
|
||||
|
||||
// 倒计时定时器
|
||||
@State private var timerCancellable: AnyCancellable?
|
||||
|
||||
// 计算属性
|
||||
private var isLoginButtonEnabled: Bool {
|
||||
return !store.isLoading && !email.isEmpty && !verificationCode.isEmpty
|
||||
}
|
||||
|
||||
private var getCodeButtonText: String {
|
||||
if codeCountdown > 0 {
|
||||
return "\(codeCountdown)s"
|
||||
} else {
|
||||
return LocalizedString("email_login.get_code", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
private var isCodeButtonEnabled: Bool {
|
||||
return !store.isCodeLoading && codeCountdown == 0 && !email.isEmpty
|
||||
}
|
||||
|
||||
enum Field {
|
||||
case email
|
||||
case verificationCode
|
||||
}
|
||||
|
||||
private var isLoginButtonEnabled: Bool {
|
||||
return !store.isLoading && !email.isEmpty && !verificationCode.isEmpty
|
||||
}
|
||||
private var getCodeButtonText: String {
|
||||
if store.isCodeLoading {
|
||||
return ""
|
||||
} else if codeCountdown > 0 {
|
||||
return "\(codeCountdown)S"
|
||||
} else {
|
||||
return NSLocalizedString("email_login.get_code", comment: "")
|
||||
}
|
||||
}
|
||||
private var isCodeButtonEnabled: Bool {
|
||||
return !store.isCodeLoading && codeCountdown == 0
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
WithViewStore(store, observe: { $0.loginStep }) { viewStore in
|
||||
WithPerceptionTracking {
|
||||
LoginContentView(
|
||||
store: store,
|
||||
onBack: onBack,
|
||||
@@ -47,7 +51,7 @@ struct EMailLoginView: View {
|
||||
getCodeButtonText: getCodeButtonText,
|
||||
isCodeButtonEnabled: isCodeButtonEnabled
|
||||
)
|
||||
.onChange(of: viewStore.state) { newStep in
|
||||
.onChange(of: store.loginStep) { newStep in
|
||||
debugInfoSync("🔄 EMailLoginView: loginStep 变化为 \(newStep)")
|
||||
if newStep == .completed {
|
||||
debugInfoSync("✅ EMailLoginView: 登录成功,准备关闭自身")
|
||||
@@ -147,116 +151,116 @@ private struct LoginContentView: View {
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 8)
|
||||
Spacer().frame(height: 60)
|
||||
Text(NSLocalizedString("email_login.title", comment: ""))
|
||||
Text(LocalizedString("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)
|
||||
|
||||
VStack(spacing: 20) {
|
||||
// 邮箱输入框
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Image("email icon")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 20, height: 20)
|
||||
Text(LocalizedString("email_login.email", comment: ""))
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
TextField("", text: $email)
|
||||
.placeholder(when: email.isEmpty) {
|
||||
Text(NSLocalizedString("placeholder.enter_email", comment: ""))
|
||||
.foregroundColor(.white.opacity(0.6))
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.textFieldStyle(PlainTextFieldStyle())
|
||||
.font(.system(size: 16))
|
||||
.padding(.horizontal, 24)
|
||||
.keyboardType(.emailAddress)
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
.background(Color.white.opacity(0.1))
|
||||
.cornerRadius(8)
|
||||
.focused($focusedField, equals: .email)
|
||||
.keyboardType(.emailAddress)
|
||||
.autocapitalization(.none)
|
||||
}
|
||||
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)
|
||||
|
||||
// 验证码输入框
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Image("id icon")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 20, height: 20)
|
||||
Text(LocalizedString("email_login.verification_code", comment: ""))
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
HStack {
|
||||
TextField("", text: $verificationCode)
|
||||
.placeholder(when: verificationCode.isEmpty) {
|
||||
Text(NSLocalizedString("placeholder.enter_code", comment: ""))
|
||||
.foregroundColor(.white.opacity(0.6))
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.textFieldStyle(PlainTextFieldStyle())
|
||||
.font(.system(size: 16))
|
||||
.keyboardType(.numberPad)
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
.background(Color.white.opacity(0.1))
|
||||
.cornerRadius(8)
|
||||
.focused($focusedField, equals: .verificationCode)
|
||||
.keyboardType(.numberPad)
|
||||
|
||||
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))
|
||||
)
|
||||
Text(getCodeButtonText)
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
.background(isCodeButtonEnabled ? Color.blue : Color.gray)
|
||||
.cornerRadius(8)
|
||||
}
|
||||
.disabled(!isCodeButtonEnabled || email.isEmpty || store.isCodeLoading)
|
||||
.disabled(!isCodeButtonEnabled)
|
||||
}
|
||||
.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))
|
||||
|
||||
// 登录按钮
|
||||
Button(action: {
|
||||
store.send(.loginButtonTapped(email: email, verificationCode: verificationCode))
|
||||
}) {
|
||||
if store.isLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.scaleEffect(1.2)
|
||||
} else {
|
||||
Text(LocalizedString("email_login.login", comment: ""))
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
.frame(height: 56)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 16)
|
||||
.background(isLoginButtonEnabled ? Color.blue : Color.gray)
|
||||
.cornerRadius(8)
|
||||
.disabled(!isLoginButtonEnabled)
|
||||
.padding(.top, 20)
|
||||
|
||||
// 错误信息显示
|
||||
if let errorMessage = store.errorMessage {
|
||||
Text(errorMessage)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.red)
|
||||
.padding(.top, 16)
|
||||
.padding(.horizontal, 32)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.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()
|
||||
|
||||
// 添加API Loading和错误处理视图
|
||||
APILoadingEffectView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
import PhotosUI
|
||||
//import ImagePreviewPager
|
||||
|
||||
struct EditFeedView: View {
|
||||
let onDismiss: () -> Void
|
||||
@@ -16,284 +15,308 @@ struct EditFeedView: View {
|
||||
var body: some View {
|
||||
WithPerceptionTracking {
|
||||
GeometryReader { geometry in
|
||||
WithViewStore(store, observe: { $0 }) { viewStore in
|
||||
WithPerceptionTracking {
|
||||
ZStack {
|
||||
backgroundView
|
||||
mainContent(geometry: geometry, viewStore: viewStore)
|
||||
if viewStore.isUploadingImages {
|
||||
uploadingImagesOverlay(progress: viewStore.imageUploadProgress)
|
||||
} else if viewStore.isLoading {
|
||||
loadingOverlay
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
if isKeyboardVisible {
|
||||
hideKeyboard()
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { _ in
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
isKeyboardVisible = true
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
isKeyboardVisible = false
|
||||
}
|
||||
}
|
||||
.onChange(of: viewStore.errorMessage) { error in
|
||||
if error != nil {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
viewStore.send(.clearError)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: viewStore.shouldDismiss) { shouldDismiss in
|
||||
if shouldDismiss {
|
||||
onDismiss()
|
||||
NotificationCenter.default.post(name: Notification.Name("reloadFeedList"), object: nil)
|
||||
viewStore.send(.clearDismissFlag)
|
||||
}
|
||||
}
|
||||
ZStack {
|
||||
backgroundView
|
||||
mainContent(geometry: geometry)
|
||||
if store.isUploadingImages {
|
||||
uploadingImagesOverlay(progress: store.imageUploadProgress)
|
||||
} else if store.isLoading {
|
||||
loadingOverlay
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
if isKeyboardVisible {
|
||||
hideKeyboard()
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { _ in
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
isKeyboardVisible = true
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
isKeyboardVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
.onAppear {
|
||||
store.send(.clearError)
|
||||
}
|
||||
.onChange(of: store.shouldDismiss) {
|
||||
if store.shouldDismiss {
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
.photosPicker(
|
||||
isPresented: Binding(
|
||||
get: { store.showPhotosPicker },
|
||||
set: { _ in store.send(.photosPickerDismissed) }
|
||||
),
|
||||
selection: Binding(
|
||||
get: { store.selectedPhotoItems },
|
||||
set: { store.send(.photosPickerItemsChanged($0)) }
|
||||
),
|
||||
maxSelectionCount: 9,
|
||||
matching: .images
|
||||
)
|
||||
.alert("删除图片", isPresented: Binding(
|
||||
get: { store.showDeleteImageAlert },
|
||||
set: { _ in store.send(.deleteImageAlertDismissed) }
|
||||
)) {
|
||||
Button("删除", role: .destructive) {
|
||||
if let indexToDelete = store.imageToDeleteIndex {
|
||||
store.send(.removeImage(indexToDelete))
|
||||
}
|
||||
}
|
||||
Button("取消", role: .cancel) {
|
||||
store.send(.deleteImageAlertDismissed)
|
||||
}
|
||||
} message: {
|
||||
Text("确定要删除这张图片吗?")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var backgroundView: some View {
|
||||
Color(hexString: "0C0527")
|
||||
Color(hex: 0x0C0527)
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
|
||||
private func mainContent(geometry: GeometryProxy, viewStore: ViewStoreOf<EditFeedFeature>) -> some View {
|
||||
VStack(spacing: 0) {
|
||||
headerView(geometry: geometry, viewStore: viewStore)
|
||||
textInputArea(viewStore: viewStore)
|
||||
// 新增:图片输入区域
|
||||
ModernImageSelectionGrid(
|
||||
images: viewStore.processedImages,
|
||||
selectedItems: viewStore.selectedImages,
|
||||
canAddMore: viewStore.canAddMoreImages,
|
||||
onItemsChanged: { items in
|
||||
viewStore.send(.photosPickerItemsChanged(items))
|
||||
},
|
||||
onRemoveImage: { index in
|
||||
viewStore.send(.removeImage(index))
|
||||
}
|
||||
)
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.bottom, 32)
|
||||
Spacer()
|
||||
if !isKeyboardVisible {
|
||||
publishButtonBottom(viewStore: viewStore, geometry: geometry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func headerView(geometry: GeometryProxy, viewStore: ViewStoreOf<EditFeedFeature>) -> some View {
|
||||
HStack {
|
||||
Text(NSLocalizedString("editFeed.title", comment: "Image & Text Edit"))
|
||||
.font(.system(size: 20, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
Spacer()
|
||||
if isKeyboardVisible {
|
||||
WithPerceptionTracking {
|
||||
Button(action: {
|
||||
hideKeyboard()
|
||||
viewStore.send(.publishButtonTapped)
|
||||
}) {
|
||||
Text(NSLocalizedString("editFeed.publish", comment: "Publish"))
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 8)
|
||||
.background(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
Color(hexString: "A14AC6"),
|
||||
Color(hexString: "3B1EEB")
|
||||
],
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
.cornerRadius(16)
|
||||
)
|
||||
private func mainContent(geometry: GeometryProxy) -> some View {
|
||||
WithPerceptionTracking {
|
||||
VStack(spacing: 0) {
|
||||
topNavigationBar
|
||||
|
||||
ScrollView {
|
||||
VStack(spacing: 20) {
|
||||
textInputSection
|
||||
imageSelectionSection
|
||||
publishButton
|
||||
}
|
||||
.disabled(!viewStore.canPublish)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.bottom, 40)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.top, geometry.safeAreaInsets.top + 16)
|
||||
.padding(.bottom, 24)
|
||||
}
|
||||
|
||||
private func textInputArea(viewStore: ViewStoreOf<EditFeedFeature>) -> some View {
|
||||
ZStack(alignment: .topLeading) {
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.fill(Color(hexString: "1C143A"))
|
||||
TextEditor(text: Binding(
|
||||
get: { viewStore.content },
|
||||
set: { viewStore.send(.contentChanged($0)) }
|
||||
))
|
||||
.scrollContentBackground(.hidden)
|
||||
.padding(16)
|
||||
.frame(height: 160)
|
||||
.foregroundColor(.white)
|
||||
.background(.clear)
|
||||
.cornerRadius(20)
|
||||
.font(.system(size: 16))
|
||||
if viewStore.content.isEmpty {
|
||||
Text(NSLocalizedString("editFeed.enterContent", comment: "Enter Content"))
|
||||
.foregroundColor(Color.white.opacity(0.4))
|
||||
.padding(20)
|
||||
.font(.system(size: 16))
|
||||
}
|
||||
WithPerceptionTracking {
|
||||
VStack {
|
||||
Spacer()
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("\(viewStore.content.count)/\(maxCount)")
|
||||
.foregroundColor(Color.white.opacity(0.4))
|
||||
.font(.system(size: 14))
|
||||
.padding(.trailing, 16)
|
||||
.padding(.bottom, 10)
|
||||
}
|
||||
private var topNavigationBar: some View {
|
||||
WithPerceptionTracking {
|
||||
HStack {
|
||||
Button(action: {
|
||||
store.send(.clearDismissFlag)
|
||||
onDismiss()
|
||||
}) {
|
||||
Image(systemName: "xmark")
|
||||
.font(.system(size: 20, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.frame(width: 44, height: 44)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: 160)
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.bottom, 32)
|
||||
}
|
||||
|
||||
private func publishButtonBottom(viewStore: ViewStoreOf<EditFeedFeature>, geometry: GeometryProxy) -> some View {
|
||||
VStack {
|
||||
Spacer()
|
||||
Button(action: {
|
||||
hideKeyboard()
|
||||
viewStore.send(.publishButtonTapped)
|
||||
}) {
|
||||
Text(NSLocalizedString("editFeed.publish", comment: "Publish"))
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("编辑动态")
|
||||
.font(.system(size: 18, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 56)
|
||||
.background(
|
||||
LinearGradient(
|
||||
colors: [Color(hexString: "A14AC6"), Color(hexString: "3B1EEB")],
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
.cornerRadius(28)
|
||||
)
|
||||
|
||||
Spacer()
|
||||
|
||||
Color.clear
|
||||
.frame(width: 44, height: 44)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 8)
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
}
|
||||
|
||||
private var textInputSection: some View {
|
||||
WithPerceptionTracking {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("分享你的想法...")
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
|
||||
TextEditor(text: Binding(
|
||||
get: { store.content },
|
||||
set: { store.send(.contentChanged($0)) }
|
||||
))
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(.white)
|
||||
.background(Color.clear)
|
||||
.frame(minHeight: 120)
|
||||
.padding(12)
|
||||
.background(Color.white.opacity(0.1))
|
||||
.cornerRadius(12)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(Color.white.opacity(0.2), lineWidth: 1)
|
||||
)
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("\(store.content.count)/\(maxCount)")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.white.opacity(0.6))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var imageSelectionSection: some View {
|
||||
WithPerceptionTracking {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("添加图片")
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
|
||||
ImageGrid(
|
||||
images: store.processedImages,
|
||||
onRemoveImage: { index in
|
||||
store.send(.showDeleteImageAlert(index))
|
||||
},
|
||||
onAddImage: {
|
||||
store.send(.addImageButtonTapped)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var publishButton: some View {
|
||||
WithPerceptionTracking {
|
||||
Button(action: {
|
||||
store.send(.publishButtonTapped)
|
||||
}) {
|
||||
if store.isLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.scaleEffect(1.2)
|
||||
} else {
|
||||
Text("发布")
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 16)
|
||||
.background(store.content.isEmpty ? Color.gray : Color.blue)
|
||||
.cornerRadius(12)
|
||||
.disabled(store.isLoading || store.content.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
private func uploadingImagesOverlay(progress: Double) -> some View {
|
||||
WithPerceptionTracking {
|
||||
ZStack {
|
||||
Color.black.opacity(0.5)
|
||||
.ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 16) {
|
||||
ProgressView(value: progress)
|
||||
.progressViewStyle(LinearProgressViewStyle(tint: .white))
|
||||
.frame(width: 200)
|
||||
|
||||
Text("上传图片中... \(Int(progress * 100))%")
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.padding(24)
|
||||
.background(Color.black.opacity(0.8))
|
||||
.cornerRadius(12)
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.bottom, geometry.safeAreaInsets.bottom + 24)
|
||||
.disabled(!viewStore.canPublish || viewStore.isUploadingImages || viewStore.isLoading)
|
||||
.opacity(viewStore.canPublish ? 1.0 : 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
private var loadingOverlay: some View {
|
||||
Group {
|
||||
Color.black.opacity(0.3)
|
||||
.ignoresSafeArea()
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.scaleEffect(1.5)
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:图片上传进度遮罩
|
||||
private func uploadingImagesOverlay(progress: Double) -> some View {
|
||||
Group {
|
||||
Color.black.opacity(0.3)
|
||||
.ignoresSafeArea()
|
||||
VStack(spacing: 16) {
|
||||
ProgressView(value: progress)
|
||||
.progressViewStyle(LinearProgressViewStyle(tint: .white))
|
||||
.frame(width: 180)
|
||||
Text("正在上传图片...\(Int(progress * 100))%")
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#Preview {
|
||||
// EditFeedView()
|
||||
//}
|
||||
|
||||
// MARK: - 九宫格图片选择组件
|
||||
struct ModernImageSelectionGrid: View {
|
||||
let images: [UIImage]
|
||||
let selectedItems: [PhotosPickerItem]
|
||||
let canAddMore: Bool
|
||||
let onItemsChanged: ([PhotosPickerItem]) -> Void
|
||||
let onRemoveImage: (Int) -> Void
|
||||
private let columns = Array(repeating: GridItem(.flexible(), spacing: 8), count: 3)
|
||||
@State private var showPreview = false
|
||||
@State private var previewIndex = 0
|
||||
var body: some View {
|
||||
let totalSpacing: CGFloat = 8 * 2
|
||||
let totalWidth = UIScreen.main.bounds.width - 24 * 2 - totalSpacing
|
||||
let gridItemSize: CGFloat = totalWidth / 3
|
||||
WithPerceptionTracking {
|
||||
LazyVGrid(columns: columns, spacing: 8) {
|
||||
ForEach(Array(images.enumerated()), id: \.offset) { index, image in
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill) // aspectFill
|
||||
.frame(width: gridItemSize, height: gridItemSize)
|
||||
.clipped()
|
||||
.cornerRadius(12)
|
||||
.onTapGesture {
|
||||
previewIndex = index
|
||||
showPreview = true
|
||||
}
|
||||
Button(action: {
|
||||
onRemoveImage(index)
|
||||
}) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.system(size: 20))
|
||||
.foregroundColor(.white)
|
||||
.background(Color.black.opacity(0.6))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.padding(4)
|
||||
}
|
||||
}
|
||||
if canAddMore {
|
||||
PhotosPicker(
|
||||
selection: .init(
|
||||
get: { selectedItems },
|
||||
set: { items in DispatchQueue.main.async { onItemsChanged(items) } }
|
||||
),
|
||||
maxSelectionCount: 9 - images.count,
|
||||
matching: .images
|
||||
) {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(Color(hexString: "1C143A"))
|
||||
.frame(width: gridItemSize, height: gridItemSize)
|
||||
.overlay(
|
||||
Image("add photo")
|
||||
.resizable()
|
||||
.frame(width: 40, height: 40)
|
||||
.opacity(0.6)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: $showPreview) {
|
||||
ImagePreviewPager(images: images, currentIndex: $previewIndex, onClose: { showPreview = false })
|
||||
ZStack {
|
||||
Color.black.opacity(0.5)
|
||||
.ignoresSafeArea()
|
||||
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.scaleEffect(1.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 简化的图片网格组件
|
||||
struct ImageGrid: View {
|
||||
let images: [UIImage]
|
||||
let onRemoveImage: (Int) -> Void
|
||||
let onAddImage: () -> Void
|
||||
|
||||
private let columns = Array(repeating: GridItem(.flexible(), spacing: 8), count: 3)
|
||||
|
||||
var body: some View {
|
||||
LazyVGrid(columns: columns, spacing: 8) {
|
||||
ForEach(Array(images.enumerated()), id: \.offset) { index, image in
|
||||
ImageGridItem(
|
||||
image: image,
|
||||
onRemove: { onRemoveImage(index) }
|
||||
)
|
||||
}
|
||||
|
||||
if images.count < 9 {
|
||||
AddImageButton(onTap: onAddImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 图片网格项组件
|
||||
struct ImageGridItem: View {
|
||||
let image: UIImage
|
||||
let onRemove: () -> Void
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(height: 100)
|
||||
.clipped()
|
||||
.cornerRadius(8)
|
||||
|
||||
Button(action: onRemove) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.system(size: 20))
|
||||
.foregroundColor(.white)
|
||||
.background(Color.black.opacity(0.5))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.padding(4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 添加图片按钮组件
|
||||
struct AddImageButton: View {
|
||||
let onTap: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: onTap) {
|
||||
VStack {
|
||||
Image(systemName: "plus")
|
||||
.font(.system(size: 24))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
Text("添加")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
}
|
||||
.frame(height: 100)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color.white.opacity(0.1))
|
||||
.cornerRadius(8)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(Color.white.opacity(0.2), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,132 +1,263 @@
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
|
||||
// MARK: - BackgroundView
|
||||
struct BackgroundView: View {
|
||||
var body: some View {
|
||||
Image("bg")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.clipped()
|
||||
.ignoresSafeArea(.all)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - TopBarView
|
||||
struct TopBarView: View {
|
||||
let onEditTapped: () -> Void
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
HStack {
|
||||
Spacer(minLength: 0)
|
||||
Text(LocalizedString("feedList.title", comment: "Enjoy your Life Time"))
|
||||
.font(.system(size: 22, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
HStack {
|
||||
Spacer(minLength: 0)
|
||||
Button(action: onEditTapped) {
|
||||
Image("add icon")
|
||||
.resizable()
|
||||
.frame(width: 40, height: 40)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - LoadingView
|
||||
private struct FeedListLoadingView: View {
|
||||
var body: some View {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.padding(.top, 20)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ErrorView
|
||||
struct ErrorView: View {
|
||||
let error: String
|
||||
|
||||
var body: some View {
|
||||
Text(error)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.red)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 20)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - EmptyView
|
||||
struct EmptyView: View {
|
||||
var body: some View {
|
||||
Text(LocalizedString("feedList.empty", comment: "暂无动态"))
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
.padding(.top, 20)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MomentCardView
|
||||
struct MomentCardView: View {
|
||||
let moment: MomentsInfo
|
||||
let allMoments: [MomentsInfo]
|
||||
let index: Int
|
||||
let onImageTap: ([String], Int) -> Void
|
||||
let onTap: () -> Void
|
||||
let onLikeTap: (Int, Int, Int, Int) -> Void
|
||||
let onLoadMore: () -> Void
|
||||
let isLastItem: Bool
|
||||
let hasMore: Bool
|
||||
let isLoadingMore: Bool
|
||||
let isLikeLoading: Bool
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
OptimizedDynamicCardView(
|
||||
moment: moment,
|
||||
allMoments: allMoments,
|
||||
currentIndex: index,
|
||||
onImageTap: onImageTap,
|
||||
onLikeTap: onLikeTap,
|
||||
onCardTap: onTap,
|
||||
isDetailMode: false,
|
||||
isLikeLoading: isLikeLoading
|
||||
)
|
||||
|
||||
// 上拉加载更多触发点
|
||||
if isLastItem && hasMore && !isLoadingMore {
|
||||
Color.clear
|
||||
.frame(height: 1)
|
||||
.onAppear {
|
||||
onLoadMore()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MomentsListView
|
||||
struct MomentsListView: View {
|
||||
let moments: [MomentsInfo]
|
||||
let hasMore: Bool
|
||||
let isLoadingMore: Bool
|
||||
let onImageTap: ([String], Int) -> Void
|
||||
let onMomentTap: (MomentsInfo) -> Void
|
||||
let onLikeTap: (Int, Int, Int, Int) -> Void
|
||||
let onLoadMore: () -> Void
|
||||
let onRefresh: () -> Void
|
||||
let likeLoadingDynamicIds: Set<Int>
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 16) {
|
||||
ForEach(Array(moments.enumerated()), id: \.element.dynamicId) { index, moment in
|
||||
MomentCardView(
|
||||
moment: moment,
|
||||
allMoments: moments,
|
||||
index: index,
|
||||
onImageTap: onImageTap,
|
||||
onTap: {
|
||||
onMomentTap(moment)
|
||||
},
|
||||
onLikeTap: onLikeTap,
|
||||
onLoadMore: onLoadMore,
|
||||
isLastItem: index == moments.count - 1,
|
||||
hasMore: hasMore,
|
||||
isLoadingMore: isLoadingMore,
|
||||
isLikeLoading: likeLoadingDynamicIds.contains(moment.dynamicId)
|
||||
)
|
||||
}
|
||||
|
||||
// 加载更多指示器
|
||||
if isLoadingMore {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
|
||||
// 新增底部间距
|
||||
Color.clear.frame(height: 120)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
.refreshable {
|
||||
onRefresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - FeedListContentView
|
||||
struct FeedListContentView: View {
|
||||
let store: StoreOf<FeedListFeature>
|
||||
@Binding var previewItem: PreviewItem?
|
||||
@Binding var previewCurrentIndex: Int
|
||||
|
||||
var body: some View {
|
||||
if store.isLoading {
|
||||
FeedListLoadingView()
|
||||
} else if let error = store.error {
|
||||
ErrorView(error: error)
|
||||
} else if store.moments.isEmpty {
|
||||
EmptyView()
|
||||
} else {
|
||||
MomentsListView(
|
||||
moments: store.moments,
|
||||
hasMore: store.hasMore,
|
||||
isLoadingMore: store.isLoadingMore,
|
||||
onImageTap: { images, tappedIndex in
|
||||
previewCurrentIndex = tappedIndex
|
||||
previewItem = PreviewItem(images: images, index: tappedIndex)
|
||||
},
|
||||
onMomentTap: { moment in
|
||||
store.send(.showDetail(moment))
|
||||
},
|
||||
onLikeTap: { dynamicId, uid, likedUid, worldId in
|
||||
store.send(.likeDynamic(dynamicId, uid, likedUid, worldId))
|
||||
},
|
||||
onLoadMore: {
|
||||
store.send(.loadMore)
|
||||
},
|
||||
onRefresh: {
|
||||
store.send(.reload)
|
||||
},
|
||||
likeLoadingDynamicIds: store.likeLoadingDynamicIds
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FeedListView: View {
|
||||
let store: StoreOf<FeedListFeature>
|
||||
// 新增:图片预览状态
|
||||
@State private var previewItem: PreviewItem? = nil
|
||||
@State private var previewCurrentIndex: Int = 0
|
||||
|
||||
var body: some View {
|
||||
WithViewStore(self.store, observe: { $0 }) { viewStore in
|
||||
WithPerceptionTracking {
|
||||
GeometryReader { geometry in
|
||||
WithViewStore(store, observe: { $0 }) { viewStore in
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
// 背景图片
|
||||
Image("bg")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.clipped()
|
||||
.ignoresSafeArea(.all)
|
||||
// 背景
|
||||
BackgroundView()
|
||||
|
||||
VStack(alignment: .center, spacing: 0) {
|
||||
// 顶部栏
|
||||
ZStack {
|
||||
HStack {
|
||||
Spacer(minLength: 0)
|
||||
Text(NSLocalizedString("feedList.title", comment: "Enjoy your Life Time"))
|
||||
.font(.system(size: 22, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
HStack {
|
||||
Spacer(minLength: 0)
|
||||
Button(action: {
|
||||
viewStore.send(.editFeedButtonTapped)
|
||||
}) {
|
||||
Image("add icon")
|
||||
.resizable()
|
||||
.frame(width: 40, height: 40)
|
||||
}
|
||||
}
|
||||
TopBarView {
|
||||
store.send(.editFeedButtonTapped)
|
||||
}
|
||||
|
||||
.padding(.horizontal, 20)
|
||||
// 其他内容
|
||||
Image("Volume")
|
||||
.frame(width: 56, height: 41)
|
||||
.padding(.top, 16)
|
||||
Text(NSLocalizedString("feedList.slogan", comment: "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(LocalizedString("feedList.slogan", comment: "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(.leading)
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
.padding(.horizontal, 30)
|
||||
.padding(.bottom, 30)
|
||||
// 新增:动态内容列表
|
||||
if viewStore.isLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.padding(.top, 20)
|
||||
} else if let error = viewStore.error {
|
||||
Text(error)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.red)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 20)
|
||||
} else if viewStore.moments.isEmpty {
|
||||
Text(NSLocalizedString("feedList.empty", comment: "暂无动态"))
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
.padding(.top, 20)
|
||||
} else {
|
||||
ScrollView {
|
||||
WithPerceptionTracking {
|
||||
LazyVStack(spacing: 16) {
|
||||
ForEach(Array(viewStore.moments.enumerated()), id: \ .element.dynamicId) { index, moment in
|
||||
OptimizedDynamicCardView(
|
||||
moment: moment,
|
||||
allMoments: viewStore.moments,
|
||||
currentIndex: index,
|
||||
onImageTap: { images, tappedIndex in
|
||||
previewItem = PreviewItem(images: images, index: tappedIndex)
|
||||
}
|
||||
)
|
||||
// 上拉加载更多触发点
|
||||
if index == viewStore.moments.count - 1, viewStore.hasMore, !viewStore.isLoadingMore {
|
||||
Color.clear
|
||||
.frame(height: 1)
|
||||
.onAppear {
|
||||
viewStore.send(.loadMore)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 加载更多指示器
|
||||
if viewStore.isLoadingMore {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
// 新增底部间距
|
||||
Color.clear.frame(height: 120)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
}
|
||||
.refreshable {
|
||||
viewStore.send(.reload)
|
||||
}
|
||||
}
|
||||
|
||||
// 动态内容列表
|
||||
FeedListContentView(
|
||||
store: store,
|
||||
previewItem: $previewItem,
|
||||
previewCurrentIndex: $previewCurrentIndex
|
||||
)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .top)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewStore.send(.onAppear)
|
||||
store.send(.onAppear)
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("reloadFeedList"))) { _ in
|
||||
viewStore.send(.reload)
|
||||
.refreshable {
|
||||
store.send(.reload)
|
||||
}
|
||||
.sheet(isPresented: viewStore.binding(
|
||||
get: \.isEditFeedPresented,
|
||||
send: { $0 ? .editFeedButtonTapped : .editFeedDismissed }
|
||||
)) {
|
||||
// 新增:编辑动态页面
|
||||
.sheet(isPresented: viewStore.binding(get: \.isEditFeedPresented, send: { _ in .editFeedDismissed })) {
|
||||
EditFeedView(
|
||||
onDismiss: {
|
||||
viewStore.send(.editFeedDismissed)
|
||||
store.send(.editFeedDismissed)
|
||||
},
|
||||
store: Store(
|
||||
initialState: EditFeedFeature.State()
|
||||
@@ -135,12 +266,23 @@ struct FeedListView: View {
|
||||
}
|
||||
)
|
||||
}
|
||||
// 新增:图片预览弹窗
|
||||
.fullScreenCover(item: $previewItem) { item in
|
||||
ImagePreviewPager(images: item.images, currentIndex: .constant(item.index)) {
|
||||
previewItem = nil
|
||||
// 新增:详情页导航
|
||||
.navigationDestination(isPresented: viewStore.binding(get: \.showDetail, send: { _ in .detailDismissed })) {
|
||||
if let selectedMoment = viewStore.selectedMoment {
|
||||
DetailView(
|
||||
store: Store(
|
||||
initialState: DetailFeature.State(moment: selectedMoment)
|
||||
) {
|
||||
DetailFeature()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
// 新增:图片预览弹窗
|
||||
.fullScreenCover(item: $previewItem) { item in
|
||||
ImagePreviewPager(images: item.images as [String], currentIndex: $previewCurrentIndex) {
|
||||
previewItem = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,636 +0,0 @@
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
|
||||
struct FeedTopBarView: View {
|
||||
let store: StoreOf<FeedFeature>
|
||||
let onShowCreateFeed: () -> Void
|
||||
var body: some View {
|
||||
WithPerceptionTracking {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text(NSLocalizedString("feed.title", comment: "Enjoy your Life Time"))
|
||||
.font(.system(size: 22, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
Spacer()
|
||||
Button(action: {
|
||||
// showEditFeed = true // 显示编辑界面
|
||||
}) {
|
||||
Image("add icon")
|
||||
.frame(width: 36, height: 36)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FeedMomentsListView: View {
|
||||
let store: StoreOf<FeedFeature>
|
||||
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(NSLocalizedString("feed.empty", comment: "No moments yet"))
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
if let error = store.error {
|
||||
Text(String(format: NSLocalizedString("feed.error", comment: "Error: %@"), error))
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.red.opacity(0.8))
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
|
||||
// 重试按钮
|
||||
if store.error != nil {
|
||||
Button(action: {
|
||||
store.send(.retryLoad)
|
||||
}) {
|
||||
Text(NSLocalizedString("feed.retry", comment: "Retry"))
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 8)
|
||||
.background(Color.blue.opacity(0.8))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
.padding(.top, 8)
|
||||
}
|
||||
}
|
||||
.padding(.top, 40)
|
||||
} else {
|
||||
ForEach(Array(store.moments.enumerated()), id: \.element.dynamicId) { index, moment in
|
||||
WithPerceptionTracking {
|
||||
Text(moment.avatar)
|
||||
// OptimizedDynamicCardView(
|
||||
// moment: moment,
|
||||
// allMoments: store.moments,
|
||||
// currentIndex: index
|
||||
// )
|
||||
.onAppear {
|
||||
// 当显示最后一个动态时,加载更多数据
|
||||
if index == store.moments.count - 1 && store.hasMoreData && !store.isLoading {
|
||||
store.send(.loadMoreMoments)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载更多指示器
|
||||
if store.isLoading && !store.moments.isEmpty {
|
||||
HStack {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
Text(NSLocalizedString("feed.loadingMore", comment: "Loading more..."))
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
.padding(.top, 20)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 20) // 调整顶部间距
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FeedView: View {
|
||||
let store: StoreOf<FeedFeature>
|
||||
let onShowCreateFeed: () -> Void
|
||||
@State private var showEditFeed = false
|
||||
|
||||
var body: some View {
|
||||
WithPerceptionTracking {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
// 背景图片 - 与 HomeView 保持一致
|
||||
Image("bg")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.clipped()
|
||||
.ignoresSafeArea(.all)
|
||||
|
||||
// 主要内容布局
|
||||
VStack(spacing: 0) {
|
||||
// 固定内容区域
|
||||
VStack(spacing: 20) {
|
||||
FeedTopBarView(store: store, onShowCreateFeed: onShowCreateFeed)
|
||||
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(.bottom, 30)
|
||||
}
|
||||
// .padding(.top, 60) // 为状态栏留出空间
|
||||
|
||||
// 滚动内容区域 - 只有动态列表
|
||||
ScrollView {
|
||||
FeedMomentsListView(store: store)
|
||||
.padding(.bottom, 20) // 底部留出空间
|
||||
}
|
||||
.refreshable {
|
||||
// 下拉刷新
|
||||
await withCheckedContinuation { continuation in
|
||||
store.send(.refresh)
|
||||
// 简单延迟确保刷新完成
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
store.send(.onAppear)
|
||||
}
|
||||
.sheet(isPresented: $showEditFeed) {
|
||||
EditFeedView(
|
||||
onDismiss: {
|
||||
showEditFeed = false
|
||||
},
|
||||
store: Store(
|
||||
initialState: EditFeedFeature.State()
|
||||
) {
|
||||
EditFeedFeature()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 优化的动态卡片组件
|
||||
//struct OptimizedDynamicCardView: View {
|
||||
// let moment: MomentsInfo
|
||||
// let allMoments: [MomentsInfo]
|
||||
// let currentIndex: Int
|
||||
//
|
||||
// var body: some View {
|
||||
// WithPerceptionTracking{
|
||||
// VStack(alignment: .leading, spacing: 12) {
|
||||
// // 用户信息
|
||||
// HStack {
|
||||
// // 使用缓存的头像
|
||||
// CachedAsyncImage(url: moment.avatar) { image in
|
||||
// image
|
||||
// .resizable()
|
||||
// .aspectRatio(contentMode: .fill)
|
||||
// } placeholder: {
|
||||
// Circle()
|
||||
// .fill(Color.gray.opacity(0.3))
|
||||
// .overlay(
|
||||
// Text(String(moment.nick.prefix(1)))
|
||||
// .font(.system(size: 16, weight: .medium))
|
||||
// .foregroundColor(.white)
|
||||
// )
|
||||
// }
|
||||
// .frame(width: 40, height: 40)
|
||||
// .clipShape(Circle())
|
||||
//
|
||||
// VStack(alignment: .leading, spacing: 2) {
|
||||
// Text(moment.nick)
|
||||
// .font(.system(size: 16, weight: .medium))
|
||||
// .foregroundColor(.white)
|
||||
//
|
||||
// Text(formatTime(moment.publishTime))
|
||||
// .font(.system(size: 12))
|
||||
// .foregroundColor(.white.opacity(0.6))
|
||||
// }
|
||||
//
|
||||
// Spacer()
|
||||
//
|
||||
// // VIP 标识
|
||||
// if let vipInfo = moment.userVipInfoVO, let vipLevel = vipInfo.vipLevel {
|
||||
// Text("VIP\(vipLevel)")
|
||||
// .font(.system(size: 10, weight: .bold))
|
||||
// .foregroundColor(.yellow)
|
||||
// .padding(.horizontal, 6)
|
||||
// .padding(.vertical, 2)
|
||||
// .background(Color.yellow.opacity(0.2))
|
||||
// .cornerRadius(4)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // 动态内容
|
||||
// if !moment.content.isEmpty {
|
||||
// Text(moment.content)
|
||||
// .font(.system(size: 14))
|
||||
// .foregroundColor(.white.opacity(0.9))
|
||||
// .multilineTextAlignment(.leading)
|
||||
// }
|
||||
//
|
||||
// // 优化的图片网格
|
||||
// if let images = moment.dynamicResList, !images.isEmpty {
|
||||
// OptimizedImageGrid(images: images)
|
||||
// }
|
||||
//
|
||||
// // 互动按钮
|
||||
// HStack(spacing: 20) {
|
||||
// Button(action: {}) {
|
||||
// HStack(spacing: 4) {
|
||||
// Image(systemName: "message")
|
||||
// .font(.system(size: 16))
|
||||
// Text("\(moment.commentCount)")
|
||||
// .font(.system(size: 14))
|
||||
// }
|
||||
// .foregroundColor(.white.opacity(0.8))
|
||||
// }
|
||||
//
|
||||
// Button(action: {}) {
|
||||
// HStack(spacing: 4) {
|
||||
// Image(systemName: moment.isLike ? "heart.fill" : "heart")
|
||||
// .font(.system(size: 16))
|
||||
// Text("\(moment.likeCount)")
|
||||
// .font(.system(size: 14))
|
||||
// }
|
||||
// .foregroundColor(moment.isLike ? .red : .white.opacity(0.8))
|
||||
// }
|
||||
//
|
||||
// Spacer()
|
||||
// }
|
||||
// .padding(.top, 8)
|
||||
// }
|
||||
// }
|
||||
// .padding(16)
|
||||
// .background(
|
||||
// Color.white.opacity(0.1)
|
||||
// .cornerRadius(12)
|
||||
// )
|
||||
// .onAppear {
|
||||
// // 预加载相邻的图片
|
||||
// preloadNearbyImages()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func formatTime(_ timestamp: Int) -> String {
|
||||
// let date = Date(timeIntervalSince1970: TimeInterval(timestamp) / 1000.0)
|
||||
// let formatter = DateFormatter()
|
||||
// formatter.locale = Locale(identifier: "zh_CN")
|
||||
//
|
||||
// let now = Date()
|
||||
// let interval = now.timeIntervalSince(date)
|
||||
//
|
||||
// if interval < 60 {
|
||||
// return "刚刚"
|
||||
// } else if interval < 3600 {
|
||||
// return "\(Int(interval / 60))分钟前"
|
||||
// } else if interval < 86400 {
|
||||
// return "\(Int(interval / 3600))小时前"
|
||||
// } else {
|
||||
// formatter.dateFormat = "MM-dd HH:mm"
|
||||
// return formatter.string(from: date)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func preloadNearbyImages() {
|
||||
// var urlsToPreload: [String] = []
|
||||
//
|
||||
// // 预加载前后2个动态的图片
|
||||
// let preloadRange = max(0, currentIndex - 2)...min(allMoments.count - 1, currentIndex + 2)
|
||||
//
|
||||
// for index in preloadRange {
|
||||
// let moment = allMoments[index]
|
||||
//
|
||||
// // 添加头像
|
||||
// urlsToPreload.append(moment.avatar)
|
||||
//
|
||||
// // 添加动态图片
|
||||
// if let images = moment.dynamicResList {
|
||||
// urlsToPreload.append(contentsOf: images.map { $0.resUrl })
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // 异步预加载
|
||||
// ImageCacheManager.shared.preloadImages(urls: urlsToPreload)
|
||||
// }
|
||||
//}
|
||||
|
||||
// MARK: - 优化的图片网格
|
||||
//struct OptimizedImageGrid: View {
|
||||
// let images: [MomentsPicture]
|
||||
//
|
||||
// var body: some View {
|
||||
// GeometryReader { geometry in
|
||||
// let availableWidth = max(geometry.size.width, 1) // 防止为0或负数
|
||||
// let spacing: CGFloat = 8
|
||||
//
|
||||
// // 保护:如果availableWidth不合理,直接返回空视图
|
||||
// if availableWidth < 10 {
|
||||
// Color.clear.frame(height: 1)
|
||||
// } else {
|
||||
// switch images.count {
|
||||
// case 1:
|
||||
// // 单张图片:大正方形居中显示
|
||||
// let imageSize: CGFloat = min(availableWidth * 0.6, 200)
|
||||
// HStack {
|
||||
// Spacer()
|
||||
// SquareImageView(image: images[0], size: imageSize)
|
||||
// Spacer()
|
||||
// }
|
||||
// case 2:
|
||||
// // 两张图片:并排显示
|
||||
// let imageSize: CGFloat = (availableWidth - spacing) / 2
|
||||
// HStack(spacing: spacing) {
|
||||
// SquareImageView(image: images[0], size: imageSize)
|
||||
// SquareImageView(image: images[1], size: imageSize)
|
||||
// }
|
||||
// case 3:
|
||||
// // 三张图片:水平排列
|
||||
// let imageSize: CGFloat = (availableWidth - spacing * 2) / 3
|
||||
// HStack(spacing: spacing) {
|
||||
// ForEach(images.prefix(3), id: \.id) { image in
|
||||
// SquareImageView(image: image, size: imageSize)
|
||||
// }
|
||||
// }
|
||||
// default:
|
||||
// // 四张及以上:九宫格布局(最多9张)
|
||||
// let imageSize: CGFloat = (availableWidth - spacing * 2) / 3
|
||||
// let columns = Array(repeating: GridItem(.fixed(imageSize), spacing: spacing), count: 3)
|
||||
// LazyVGrid(columns: columns, spacing: spacing) {
|
||||
// ForEach(images.prefix(9), id: \.id) { image in
|
||||
// SquareImageView(image: image, size: imageSize)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .frame(height: calculateGridHeight())
|
||||
// }
|
||||
//
|
||||
// private func calculateGridHeight() -> CGFloat {
|
||||
// switch images.count {
|
||||
// case 1:
|
||||
// return 200 // 单张图片的最大高度
|
||||
// case 2:
|
||||
// return 120 // 两张图片并排的高度
|
||||
// case 3:
|
||||
// return 100 // 三张图片水平排列的高度
|
||||
// case 4...6:
|
||||
// return 216 // 九宫格2行的高度 (实际图片大小 * 2 + 间距)
|
||||
// default:
|
||||
// return 340 // 九宫格3行的高度 (实际图片大小 * 3 + 间距 + 额外空间)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
// MARK: - 正方形图片视图组件
|
||||
//struct SquareImageView: View {
|
||||
// let image: MomentsPicture
|
||||
// let size: CGFloat
|
||||
//
|
||||
// var body: some View {
|
||||
// let safeSize = size.isFinite && size > 0 ? size : 100 // 防止非有限或负数
|
||||
// CachedAsyncImage(url: image.resUrl) { imageView in
|
||||
// imageView
|
||||
// .resizable()
|
||||
// .aspectRatio(contentMode: .fill)
|
||||
// } placeholder: {
|
||||
// Rectangle()
|
||||
// .fill(Color.gray.opacity(0.3))
|
||||
// .overlay(
|
||||
// ProgressView()
|
||||
// .progressViewStyle(CircularProgressViewStyle(tint: .white.opacity(0.6)))
|
||||
// .scaleEffect(0.8)
|
||||
// )
|
||||
// }
|
||||
// .frame(width: safeSize, height: safeSize)
|
||||
// .clipped()
|
||||
// .cornerRadius(8)
|
||||
// }
|
||||
//}
|
||||
|
||||
// MARK: - 旧的真实动态卡片组件(保留备用)
|
||||
//struct RealDynamicCardView: View {
|
||||
// let moment: MomentsInfo
|
||||
//
|
||||
// var body: some View {
|
||||
// VStack(alignment: .leading, spacing: 12) {
|
||||
// // 用户信息
|
||||
// HStack {
|
||||
// AsyncImage(url: URL(string: moment.avatar)) { image in
|
||||
// image
|
||||
// .resizable()
|
||||
// .aspectRatio(contentMode: .fill)
|
||||
// } placeholder: {
|
||||
// Circle()
|
||||
// .fill(Color.gray.opacity(0.3))
|
||||
// .overlay(
|
||||
// Text(String(moment.nick.prefix(1)))
|
||||
// .font(.system(size: 16, weight: .medium))
|
||||
// .foregroundColor(.white)
|
||||
// )
|
||||
// }
|
||||
// .frame(width: 40, height: 40)
|
||||
// .clipShape(Circle())
|
||||
//
|
||||
// VStack(alignment: .leading, spacing: 2) {
|
||||
// Text(moment.nick)
|
||||
// .font(.system(size: 16, weight: .medium))
|
||||
// .foregroundColor(.white)
|
||||
//
|
||||
// Text(formatTime(moment.publishTime))
|
||||
// .font(.system(size: 12))
|
||||
// .foregroundColor(.white.opacity(0.6))
|
||||
// }
|
||||
//
|
||||
// Spacer()
|
||||
//
|
||||
// // VIP 标识
|
||||
// if let vipInfo = moment.userVipInfoVO, let vipLevel = vipInfo.vipLevel {
|
||||
// Text("VIP\(vipLevel)")
|
||||
// .font(.system(size: 10, weight: .bold))
|
||||
// .foregroundColor(.yellow)
|
||||
// .padding(.horizontal, 6)
|
||||
// .padding(.vertical, 2)
|
||||
// .background(Color.yellow.opacity(0.2))
|
||||
// .cornerRadius(4)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // 动态内容
|
||||
// if !moment.content.isEmpty {
|
||||
// Text(moment.content)
|
||||
// .font(.system(size: 14))
|
||||
// .foregroundColor(.white.opacity(0.9))
|
||||
// .multilineTextAlignment(.leading)
|
||||
// }
|
||||
//
|
||||
// // 图片网格
|
||||
// if let images = moment.dynamicResList, !images.isEmpty {
|
||||
// LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: min(images.count, 3)), spacing: 8) {
|
||||
// ForEach(images.prefix(9), id: \.id) { image in
|
||||
// AsyncImage(url: URL(string: image.resUrl)) { imageView in
|
||||
// imageView
|
||||
// .resizable()
|
||||
// .aspectRatio(contentMode: .fill)
|
||||
// } placeholder: {
|
||||
// Rectangle()
|
||||
// .fill(Color.gray.opacity(0.3))
|
||||
// .overlay(
|
||||
// ProgressView()
|
||||
// .progressViewStyle(CircularProgressViewStyle(tint: .white.opacity(0.6)))
|
||||
// )
|
||||
// }
|
||||
// .frame(height: 100)
|
||||
// .clipped()
|
||||
// .cornerRadius(8)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // 互动按钮
|
||||
// HStack(spacing: 20) {
|
||||
// Button(action: {}) {
|
||||
// HStack(spacing: 4) {
|
||||
// Image(systemName: "message")
|
||||
// .font(.system(size: 16))
|
||||
// Text("\(moment.commentCount)")
|
||||
// .font(.system(size: 14))
|
||||
// }
|
||||
// .foregroundColor(.white.opacity(0.8))
|
||||
// }
|
||||
//
|
||||
// Button(action: {}) {
|
||||
// HStack(spacing: 4) {
|
||||
// Image(systemName: moment.isLike ? "heart.fill" : "heart")
|
||||
// .font(.system(size: 16))
|
||||
// Text("\(moment.likeCount)")
|
||||
// .font(.system(size: 14))
|
||||
// }
|
||||
// .foregroundColor(moment.isLike ? .red : .white.opacity(0.8))
|
||||
// }
|
||||
//
|
||||
// Spacer()
|
||||
// }
|
||||
// .padding(.top, 8)
|
||||
// }
|
||||
// .padding(16)
|
||||
// .background(
|
||||
// Color.white.opacity(0.1)
|
||||
// .cornerRadius(12)
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// private func formatTime(_ timestamp: Int) -> String {
|
||||
// let date = Date(timeIntervalSince1970: TimeInterval(timestamp) / 1000.0)
|
||||
// let formatter = DateFormatter()
|
||||
// formatter.locale = Locale(identifier: "zh_CN")
|
||||
//
|
||||
// let now = Date()
|
||||
// let interval = now.timeIntervalSince(date)
|
||||
//
|
||||
// if interval < 60 {
|
||||
// return "刚刚"
|
||||
// } else if interval < 3600 {
|
||||
// return "\(Int(interval / 60))分钟前"
|
||||
// } else if interval < 86400 {
|
||||
// return "\(Int(interval / 3600))小时前"
|
||||
// } else {
|
||||
// formatter.dateFormat = "MM-dd HH:mm"
|
||||
// return formatter.string(from: date)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
// MARK: - 旧的模拟卡片组件(保留备用)
|
||||
//struct DynamicCardView: View {
|
||||
// let index: Int
|
||||
//
|
||||
// var body: some View {
|
||||
// VStack(alignment: .leading, spacing: 12) {
|
||||
// // 用户信息
|
||||
// HStack {
|
||||
// Circle()
|
||||
// .fill(Color.gray.opacity(0.3))
|
||||
// .frame(width: 40, height: 40)
|
||||
// .overlay(
|
||||
// Text("U\(index + 1)")
|
||||
// .font(.system(size: 16, weight: .medium))
|
||||
// .foregroundColor(.white)
|
||||
// )
|
||||
//
|
||||
// VStack(alignment: .leading, spacing: 2) {
|
||||
// Text("用户\(index + 1)")
|
||||
// .font(.system(size: 16, weight: .medium))
|
||||
// .foregroundColor(.white)
|
||||
//
|
||||
// Text("2小时前")
|
||||
// .font(.system(size: 12))
|
||||
// .foregroundColor(.white.opacity(0.6))
|
||||
// }
|
||||
//
|
||||
// Spacer()
|
||||
// }
|
||||
//
|
||||
// // 动态内容
|
||||
// Text("今天是美好的一天,分享一些生活中的小确幸。希望大家都能珍惜每一个当下的时刻。")
|
||||
// .font(.system(size: 14))
|
||||
// .foregroundColor(.white.opacity(0.9))
|
||||
// .multilineTextAlignment(.leading)
|
||||
//
|
||||
// // 图片网格
|
||||
// LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 3), spacing: 8) {
|
||||
// ForEach(0..<3) { imageIndex in
|
||||
// Rectangle()
|
||||
// .fill(Color.gray.opacity(0.3))
|
||||
// .aspectRatio(1, contentMode: .fit)
|
||||
// .overlay(
|
||||
// Image(systemName: "photo")
|
||||
// .foregroundColor(.white.opacity(0.6))
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // 互动按钮
|
||||
// HStack(spacing: 20) {
|
||||
// Button(action: {}) {
|
||||
// HStack(spacing: 4) {
|
||||
// Image(systemName: "message")
|
||||
// .font(.system(size: 16))
|
||||
// Text("354")
|
||||
// .font(.system(size: 14))
|
||||
// }
|
||||
// .foregroundColor(.white.opacity(0.8))
|
||||
// }
|
||||
//
|
||||
// Button(action: {}) {
|
||||
// HStack(spacing: 4) {
|
||||
// Image(systemName: "heart")
|
||||
// .font(.system(size: 16))
|
||||
// Text("354")
|
||||
// .font(.system(size: 14))
|
||||
// }
|
||||
// .foregroundColor(.white.opacity(0.8))
|
||||
// }
|
||||
//
|
||||
// Spacer()
|
||||
// }
|
||||
// .padding(.top, 8)
|
||||
// }
|
||||
// .padding(16)
|
||||
// .background(
|
||||
// Color.white.opacity(0.1)
|
||||
// .cornerRadius(12)
|
||||
// )
|
||||
// }
|
||||
//}
|
||||
|
||||
//#Preview {
|
||||
// FeedView(
|
||||
// store: Store(initialState: FeedFeature.State()) {
|
||||
// FeedFeature()
|
||||
// }
|
||||
// )
|
||||
//}
|
@@ -1,88 +0,0 @@
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
|
||||
struct HomeView: View {
|
||||
let store: StoreOf<HomeFeature>
|
||||
let onLogout: () -> Void
|
||||
@ObservedObject private var localizationManager = LocalizationManager.shared
|
||||
@State private var selectedTab: Tab = .feed
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
// 使用 "bg" 图片作为背景 - 全屏显示
|
||||
Image("bg")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.clipped()
|
||||
.ignoresSafeArea(.all)
|
||||
|
||||
// 主要内容区域 - 全屏显示
|
||||
ZStack {
|
||||
switch selectedTab {
|
||||
case .feed:
|
||||
FeedView(
|
||||
store: store.scope(
|
||||
state: \.feedState,
|
||||
action: \.feed
|
||||
),
|
||||
onShowCreateFeed: {
|
||||
store.send(.showCreateFeed)
|
||||
}
|
||||
)
|
||||
.transition(.opacity)
|
||||
case .me:
|
||||
Spacer()
|
||||
// MeView(
|
||||
// meDynamicStore: store.scope(
|
||||
// state: \.meDynamic,
|
||||
// action: \.meDynamic
|
||||
// ),
|
||||
// onLogout: onLogout
|
||||
// )
|
||||
// .transition(.opacity)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
|
||||
// 底部导航栏 - 悬浮在最上层
|
||||
VStack {
|
||||
Spacer()
|
||||
BottomTabView(selectedTab: $selectedTab)
|
||||
}
|
||||
.padding(.bottom, geometry.safeAreaInsets.bottom + 100)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
store.send(.onAppear)
|
||||
}
|
||||
.navigationDestination(isPresented: Binding(
|
||||
get: { store.withState(\.route) == .createFeed },
|
||||
set: { isPresented in
|
||||
if !isPresented {
|
||||
store.send(.createFeedDismissed)
|
||||
}
|
||||
}
|
||||
)) {
|
||||
CreateFeedView(
|
||||
store: store.scope(
|
||||
state: \.feedState.createFeedState,
|
||||
action: \.feed.createFeed
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#Preview {
|
||||
// HomeView(
|
||||
// store: Store(
|
||||
// initialState: HomeFeature.State()
|
||||
// ) {
|
||||
// HomeFeature()
|
||||
// }, onLogout: {}
|
||||
// )
|
||||
//}
|
@@ -2,17 +2,174 @@ import SwiftUI
|
||||
import ComposableArchitecture
|
||||
import Perception
|
||||
|
||||
// MARK: - 背景视图组件
|
||||
struct IDLoginBackgroundView: View {
|
||||
var body: some View {
|
||||
Image("bg")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.ignoresSafeArea(.all)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 顶部导航栏组件
|
||||
struct IDLoginHeaderView: View {
|
||||
let onBack: () -> Void
|
||||
|
||||
var body: some View {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 输入框组件
|
||||
struct IDLoginInputFieldView: View {
|
||||
let iconName: String
|
||||
let title: String
|
||||
let text: Binding<String>
|
||||
let onChange: (String) -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Image(iconName)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 20, height: 20)
|
||||
Text(title)
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
TextField("", text: text)
|
||||
.textFieldStyle(PlainTextFieldStyle())
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
.background(Color.white.opacity(0.1))
|
||||
.cornerRadius(8)
|
||||
.onChange(of: text.wrappedValue) { newValue in
|
||||
onChange(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 密码输入框组件
|
||||
struct IDLoginPasswordFieldView: View {
|
||||
let password: Binding<String>
|
||||
let isPasswordVisible: Binding<Bool>
|
||||
let onChange: (String) -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Image("email icon")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 20, height: 20)
|
||||
Text(LocalizedString("id_login.password", comment: ""))
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Group {
|
||||
if isPasswordVisible.wrappedValue {
|
||||
TextField("", text: password)
|
||||
.textFieldStyle(PlainTextFieldStyle())
|
||||
} else {
|
||||
SecureField("", text: password)
|
||||
.textFieldStyle(PlainTextFieldStyle())
|
||||
}
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
isPasswordVisible.wrappedValue.toggle()
|
||||
}) {
|
||||
Image(systemName: isPasswordVisible.wrappedValue ? "eye.slash" : "eye")
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
}
|
||||
}
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
.background(Color.white.opacity(0.1))
|
||||
.cornerRadius(8)
|
||||
.onChange(of: password.wrappedValue) { newValue in
|
||||
onChange(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 登录按钮组件
|
||||
struct IDLoginButtonView: View {
|
||||
let isLoading: Bool
|
||||
let isEnabled: Bool
|
||||
let onTap: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: onTap) {
|
||||
Group {
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.scaleEffect(1.2)
|
||||
} else {
|
||||
Text(LocalizedString("id_login.login", comment: ""))
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 16)
|
||||
.background(isEnabled ? Color.blue : Color.gray)
|
||||
.cornerRadius(8)
|
||||
.disabled(!isEnabled)
|
||||
.padding(.top, 20)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 错误信息组件
|
||||
struct IDLoginErrorView: View {
|
||||
let errorMessage: String?
|
||||
|
||||
var body: some View {
|
||||
if let errorMessage = errorMessage {
|
||||
Text(errorMessage)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.red)
|
||||
.padding(.top, 16)
|
||||
.padding(.horizontal, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 主视图
|
||||
struct IDLoginView: View {
|
||||
let store: StoreOf<IDLoginFeature>
|
||||
let onBack: () -> Void
|
||||
@Binding var showIDLogin: Bool // 新增:绑定父视图的显示状态
|
||||
@Binding var showIDLogin: Bool
|
||||
|
||||
// 使用本地@State管理UI状态
|
||||
@State private var userID: String = ""
|
||||
@State private var password: String = ""
|
||||
@State private var isPasswordVisible: Bool = false
|
||||
|
||||
// 导航状态管理 - 与 LoginView 保持一致
|
||||
// 导航状态管理
|
||||
@State private var showRecoverPassword: Bool = false
|
||||
|
||||
// 计算登录按钮是否可用
|
||||
@@ -21,174 +178,79 @@ struct IDLoginView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
WithViewStore(store, observe: { $0.loginStep }) { viewStore in
|
||||
WithPerceptionTracking {
|
||||
GeometryReader { geometry in
|
||||
WithPerceptionTracking {
|
||||
ZStack {
|
||||
// 背景图片 - 使用与登录页面相同的"bg"
|
||||
Image("bg")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.ignoresSafeArea(.all)
|
||||
// 背景
|
||||
IDLoginBackgroundView()
|
||||
|
||||
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)
|
||||
IDLoginHeaderView(onBack: onBack)
|
||||
|
||||
Spacer()
|
||||
.frame(height: 60)
|
||||
|
||||
// 标题
|
||||
Text(NSLocalizedString("id_login.title", comment: ""))
|
||||
Text(LocalizedString("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))
|
||||
VStack(spacing: 20) {
|
||||
// 用户ID输入框
|
||||
IDLoginInputFieldView(
|
||||
iconName: "id icon",
|
||||
title: LocalizedString("id_login.user_id", comment: ""),
|
||||
text: $userID,
|
||||
onChange: { newValue in
|
||||
store.send(.userIDChanged(newValue))
|
||||
}
|
||||
.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))
|
||||
}
|
||||
IDLoginPasswordFieldView(
|
||||
password: $password,
|
||||
isPasswordVisible: $isPasswordVisible,
|
||||
onChange: { newValue in
|
||||
store.send(.passwordChanged(newValue))
|
||||
}
|
||||
)
|
||||
|
||||
// 忘记密码按钮
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(action: {
|
||||
showRecoverPassword = true
|
||||
}) {
|
||||
Text(LocalizedString("id_login.forgot_password", comment: ""))
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 32)
|
||||
|
||||
// Forgot Password 链接
|
||||
HStack {
|
||||
|
||||
// 登录按钮
|
||||
IDLoginButtonView(
|
||||
isLoading: store.isLoading,
|
||||
isEnabled: isLoginButtonEnabled,
|
||||
onTap: {
|
||||
store.send(.loginButtonTapped(userID: userID, password: password))
|
||||
}
|
||||
)
|
||||
|
||||
// 错误信息显示
|
||||
IDLoginErrorView(errorMessage: store.errorMessage)
|
||||
|
||||
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()
|
||||
// API Loading视图
|
||||
APILoadingEffectView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
// 使用与 LoginView 一致的 navigationDestination 方式
|
||||
.navigationDestination(isPresented: $showRecoverPassword) {
|
||||
WithPerceptionTracking {
|
||||
RecoverPasswordView(
|
||||
@@ -212,13 +274,11 @@ struct IDLoginView: View {
|
||||
isPasswordVisible = store.isPasswordVisible
|
||||
|
||||
#if DEBUG
|
||||
// 移除测试用的硬编码凭据
|
||||
debugInfoSync("🐛 Debug模式: 已移除硬编码测试凭据")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
// 新增:监听登录状态,成功后自动关闭自身
|
||||
.onChange(of: viewStore.state) { newStep in
|
||||
.onChange(of: store.loginStep) { newStep in
|
||||
debugInfoSync("🔄 IDLoginView: loginStep 变化为 \(newStep)")
|
||||
if newStep == .completed {
|
||||
debugInfoSync("✅ IDLoginView: 登录成功,准备关闭自身")
|
||||
|
@@ -9,6 +9,16 @@ struct LanguageSettingsView: View {
|
||||
// 使用 TCA 的依赖注入获取 API 服务
|
||||
@Dependency(\.apiService) private var apiService
|
||||
|
||||
// 添加缺失的状态变量
|
||||
@State private var cosTokenData: TcTokenData?
|
||||
|
||||
// 计算属性:格式化日期
|
||||
private func formatDate(_ date: Date) -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .medium
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
|
||||
init(isPresented: Binding<Bool> = .constant(true)) {
|
||||
self._isPresented = isPresented
|
||||
}
|
||||
@@ -33,7 +43,7 @@ struct LanguageSettingsView: View {
|
||||
|
||||
Section {
|
||||
HStack {
|
||||
Text("当前语言 / Current Language")
|
||||
Text(LocalizedString("language_settings.current_language", comment: ""))
|
||||
.font(.body)
|
||||
|
||||
Spacer()
|
||||
@@ -43,7 +53,85 @@ struct LanguageSettingsView: View {
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
} header: {
|
||||
Text("语言信息 / Language Info")
|
||||
Text(LocalizedString("language_settings.language_info", comment: ""))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
// 语言切换测试区域
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(LocalizedString("language_settings.test_area", comment: ""))
|
||||
.font(.headline)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(LocalizedString("language_settings.test_region", comment: ""))
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text("应用标题: \(LocalizedString("login.app_title", comment: ""))")
|
||||
.font(.caption)
|
||||
|
||||
Text("登录按钮: \(LocalizedString("login.id_login", comment: ""))")
|
||||
.font(.caption)
|
||||
|
||||
Text("当前语言代码: \(localizationManager.currentLanguage.rawValue)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
.padding(.leading, 8)
|
||||
}
|
||||
} header: {
|
||||
Text(LocalizedString("language_settings.test_region", comment: ""))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
// 腾讯云 COS Token 测试区域
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Button(LocalizedString("language_settings.test_cos_token", comment: "")) {
|
||||
Task {
|
||||
await testCOToken()
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
|
||||
if let tokenData = cosTokenData {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(LocalizedString("language_settings.token_success", comment: ""))
|
||||
.font(.headline)
|
||||
.foregroundColor(.green)
|
||||
|
||||
Text(String(format: LocalizedString("language_settings.bucket", comment: ""), tokenData.bucket))
|
||||
.font(.caption)
|
||||
|
||||
Text(String(format: LocalizedString("language_settings.region", comment: ""), tokenData.region))
|
||||
.font(.caption)
|
||||
|
||||
Text(String(format: LocalizedString("language_settings.app_id", comment: ""), tokenData.appId))
|
||||
.font(.caption)
|
||||
|
||||
Text(String(format: LocalizedString("language_settings.custom_domain", comment: ""), tokenData.customDomain))
|
||||
.font(.caption)
|
||||
|
||||
Text(String(format: LocalizedString("language_settings.accelerate_status", comment: ""),
|
||||
tokenData.accelerate ?
|
||||
LocalizedString("language_settings.accelerate_enabled", comment: "") :
|
||||
LocalizedString("language_settings.accelerate_disabled", comment: "")))
|
||||
.font(.caption)
|
||||
|
||||
Text(String(format: LocalizedString("language_settings.expiration_date", comment: ""), formatDate(tokenData.expirationDate)))
|
||||
.font(.caption)
|
||||
|
||||
Text(String(format: LocalizedString("language_settings.remaining_time", comment: ""), tokenData.remainingTime))
|
||||
.font(.caption)
|
||||
}
|
||||
.padding(.leading, 8)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text(LocalizedString("language_settings.test_region", comment: ""))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
@@ -69,7 +157,7 @@ struct LanguageSettingsView: View {
|
||||
Text("应用ID: \(tokenData.appId)")
|
||||
Text("自定义域名: \(tokenData.customDomain)")
|
||||
Text("加速: \(tokenData.accelerate ? "启用" : "禁用")")
|
||||
Text("过期时间: \(tokenData.expirationDate, style: .date)")
|
||||
Text("过期时间: \(formatDate(tokenData.expirationDate))")
|
||||
Text("剩余时间: \(tokenData.remainingTime)秒")
|
||||
}
|
||||
.font(.caption)
|
||||
@@ -80,7 +168,7 @@ struct LanguageSettingsView: View {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.navigationTitle("语言设置 / Language")
|
||||
.navigationTitle(LocalizedString("language_settings.title", comment: ""))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.onAppear {
|
||||
@@ -95,19 +183,19 @@ struct LanguageSettingsView: View {
|
||||
}
|
||||
|
||||
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)")
|
||||
// }
|
||||
let token = await cosManager.getToken(apiService: apiService)
|
||||
if let token = token {
|
||||
print("✅ Token 测试成功")
|
||||
print(" - 存储桶: \(token.bucket)")
|
||||
print(" - 地域: \(token.region)")
|
||||
print(" - 剩余时间: \(token.remainingTime)秒")
|
||||
|
||||
// 更新状态变量
|
||||
cosTokenData = token
|
||||
} else {
|
||||
print("❌ Token 测试失败: 未能获取 Token")
|
||||
cosTokenData = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -12,154 +12,53 @@ struct ImageHeightPreferenceKey: PreferenceKey {
|
||||
|
||||
struct LoginView: View {
|
||||
let store: StoreOf<LoginFeature>
|
||||
let onLoginSuccess: () -> Void // 新增:登录成功回调
|
||||
@State private var topImageHeight: CGFloat = 120 // 默认值
|
||||
// @ObservedObject private var localizationManager = LocalizationManager.shared
|
||||
@State private var showLanguageSettings = false
|
||||
@State private var isAgreedToTerms = true
|
||||
@State private var showUserAgreement = false
|
||||
@State private var showPrivacyPolicy = false
|
||||
@State private var showIDLogin = false // 使用SwiftUI的@State管理导航
|
||||
@State private var showEmailLogin = false // 新增:邮箱登录导航状态
|
||||
let onLoginSuccess: () -> Void
|
||||
|
||||
// 使用本地@State管理UI状态
|
||||
@State private var showIDLogin: Bool = false
|
||||
@State private var showEmailLogin: Bool = false
|
||||
@State private var showLanguageSettings: Bool = false
|
||||
@State private var showUserAgreement: Bool = false
|
||||
@State private var showPrivacyPolicy: Bool = false
|
||||
|
||||
var body: some View {
|
||||
WithViewStore(self.store, observe: { $0.isAnyLoginCompleted }) { viewStore in
|
||||
NavigationStack {
|
||||
GeometryReader { geometry in
|
||||
WithPerceptionTracking {
|
||||
ZStack {
|
||||
// 使用与 splash 相同的背景图片
|
||||
Image("bg")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.ignoresSafeArea(.all)
|
||||
VStack(spacing: 0) {
|
||||
// 上半部分的"top"图片
|
||||
ZStack {
|
||||
Image("top")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.top, -100)
|
||||
.background(
|
||||
GeometryReader { topImageGeometry in
|
||||
Color.clear.preference(key: ImageHeightPreferenceKey.self, value: topImageGeometry.size.height)
|
||||
}
|
||||
)
|
||||
// E-PARTI 文本,底部对齐"top"图片底部,间距20
|
||||
HStack {
|
||||
Text(NSLocalizedString("login.app_title", comment: ""))
|
||||
.font(FontManager.adaptedFont(.bayonRegular, designSize: 56, for: geometry.size.width))
|
||||
.foregroundColor(.white)
|
||||
.padding(.leading, 20)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top, max(0, topImageHeight - 100)) // top图片高度 - 140
|
||||
|
||||
// 语言切换按钮(右上角)- 仅在 Debug 环境下显示
|
||||
#if DEBUG
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(action: {
|
||||
showLanguageSettings = true
|
||||
}) {
|
||||
Image(systemName: "globe")
|
||||
.frame(width: 40, height: 40)
|
||||
.font(.system(size: 20))
|
||||
.foregroundColor(.white)
|
||||
.background(Color.black.opacity(0.3))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.padding(.trailing, 16)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
#endif
|
||||
|
||||
VStack(spacing: 24) {
|
||||
// ID Login 按钮
|
||||
LoginButton(
|
||||
iconName: "person.circle.fill",
|
||||
iconColor: .green,
|
||||
title: NSLocalizedString("login.id_login", comment: "")
|
||||
) {
|
||||
showIDLogin = true // 直接设置SwiftUI状态
|
||||
}
|
||||
// Email Login 按钮
|
||||
LoginButton(
|
||||
iconName: "envelope.fill",
|
||||
iconColor: .blue,
|
||||
title: NSLocalizedString("login.email_login", comment: "")
|
||||
) {
|
||||
showEmailLogin = true // 显示邮箱登录界面
|
||||
}
|
||||
}.padding(.top, max(0, topImageHeight+140))
|
||||
}
|
||||
.onPreferenceChange(ImageHeightPreferenceKey.self) { imageHeight in
|
||||
topImageHeight = imageHeight
|
||||
}
|
||||
|
||||
// 间距,使登录按钮区域顶部距离"top"图片底部40pt
|
||||
Spacer()
|
||||
.frame(height: 120)
|
||||
|
||||
// 用户协议组件
|
||||
UserAgreementView(
|
||||
isAgreed: $isAgreedToTerms,
|
||||
onUserServiceTapped: {
|
||||
showUserAgreement = true
|
||||
},
|
||||
onPrivacyPolicyTapped: {
|
||||
showPrivacyPolicy = true
|
||||
}
|
||||
)
|
||||
.padding(.horizontal, 28)
|
||||
.padding(.bottom, 140)
|
||||
}
|
||||
|
||||
// 移除旧的 NavigationLink,改用 navigationDestination
|
||||
}
|
||||
}
|
||||
NavigationStack {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
backgroundView
|
||||
mainContentView(geometry: geometry)
|
||||
APILoadingEffectView()
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
.navigationDestination(isPresented: $showIDLogin) {
|
||||
IDLoginView(
|
||||
store: store.scope(
|
||||
state: \.idLoginState,
|
||||
action: \.idLogin
|
||||
),
|
||||
onBack: {
|
||||
showIDLogin = false
|
||||
},
|
||||
showIDLogin: $showIDLogin
|
||||
)
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
.navigationDestination(isPresented: $showEmailLogin) {
|
||||
EMailLoginView(
|
||||
store: store.scope(
|
||||
state: \.emailLoginState,
|
||||
action: \.emailLogin
|
||||
),
|
||||
onBack: {
|
||||
showEmailLogin = false
|
||||
},
|
||||
showEmailLogin: $showEmailLogin
|
||||
)
|
||||
.navigationBarHidden(true)
|
||||
// 新增:适配 iOS 16 的 navigationDestination
|
||||
.navigationDestination(isPresented: $showIDLogin) {
|
||||
WithPerceptionTracking {
|
||||
IDLoginView(
|
||||
store: store.scope(
|
||||
state: \.idLoginState,
|
||||
action: \.idLogin
|
||||
),
|
||||
onBack: {
|
||||
showIDLogin = false
|
||||
},
|
||||
showIDLogin: $showIDLogin // 新增:传递Binding
|
||||
)
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
}
|
||||
.navigationDestination(isPresented: $showEmailLogin) {
|
||||
WithPerceptionTracking {
|
||||
EMailLoginView(
|
||||
store: store.scope(
|
||||
state: \.emailLoginState,
|
||||
action: \.emailLogin
|
||||
),
|
||||
onBack: {
|
||||
showEmailLogin = false
|
||||
},
|
||||
showEmailLogin: $showEmailLogin // 新增:传递Binding
|
||||
)
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
}
|
||||
// 移除:HomeView 的 navigationDestination
|
||||
}
|
||||
.sheet(isPresented: $showLanguageSettings) {
|
||||
WithPerceptionTracking {
|
||||
LanguageSettingsView(isPresented: $showLanguageSettings)
|
||||
}
|
||||
LanguageSettingsView(isPresented: $showLanguageSettings)
|
||||
}
|
||||
.webView(
|
||||
isPresented: $showUserAgreement,
|
||||
@@ -169,26 +68,122 @@ struct LoginView: View {
|
||||
isPresented: $showPrivacyPolicy,
|
||||
url: APIConfiguration.webURL(for: .privacyPolicy)
|
||||
)
|
||||
// 新增:监听登录成功,调用回调
|
||||
.onChange(of: viewStore.state) { completed in
|
||||
if completed {
|
||||
.onChange(of: store.isAnyLoginCompleted) {
|
||||
if store.isAnyLoginCompleted {
|
||||
onLoginSuccess()
|
||||
}
|
||||
}
|
||||
// 新增:监听showIDLogin关闭时,若已登录则跳转首页
|
||||
.onChange(of: showIDLogin) { newValue in
|
||||
if newValue == false && viewStore.state {
|
||||
.onChange(of: showIDLogin) {
|
||||
if showIDLogin == false && store.isAnyLoginCompleted {
|
||||
onLoginSuccess()
|
||||
}
|
||||
}
|
||||
// 新增:监听showEmailLogin关闭时,若已登录则跳转首页
|
||||
.onChange(of: showEmailLogin) { newValue in
|
||||
if newValue == false && viewStore.state {
|
||||
.onChange(of: showEmailLogin) {
|
||||
if showEmailLogin == false && store.isAnyLoginCompleted {
|
||||
onLoginSuccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 子视图
|
||||
|
||||
private var backgroundView: some View {
|
||||
Image("bg")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.ignoresSafeArea(.all)
|
||||
}
|
||||
|
||||
private func mainContentView(geometry: GeometryProxy) -> some View {
|
||||
VStack(spacing: 0) {
|
||||
topSection(geometry: geometry)
|
||||
bottomSection
|
||||
}
|
||||
}
|
||||
|
||||
private func topSection(geometry: GeometryProxy) -> some View {
|
||||
ZStack {
|
||||
Image("top")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.top, -100)
|
||||
.background(
|
||||
GeometryReader { topImageGeometry in
|
||||
Color.clear.preference(key: ImageHeightPreferenceKey.self, value: topImageGeometry.size.height)
|
||||
}
|
||||
)
|
||||
|
||||
HStack {
|
||||
Text(LocalizedString("login.app_title", comment: ""))
|
||||
.font(FontManager.adaptedFont(.bayonRegular, designSize: 56, for: geometry.size.width))
|
||||
.foregroundColor(.white)
|
||||
.padding(.leading, 20)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top, 100) // 简化计算逻辑
|
||||
}
|
||||
}
|
||||
|
||||
private var bottomSection: some View {
|
||||
VStack(spacing: 20) {
|
||||
loginButtons
|
||||
bottomButtons
|
||||
}
|
||||
.padding(.horizontal, 28)
|
||||
.padding(.bottom, 140)
|
||||
}
|
||||
|
||||
private var loginButtons: some View {
|
||||
VStack(spacing: 20) {
|
||||
LoginButton(
|
||||
iconName: "person.circle",
|
||||
iconColor: .blue,
|
||||
title: LocalizedString("login.id_login", comment: ""),
|
||||
action: {
|
||||
showIDLogin = true
|
||||
}
|
||||
)
|
||||
|
||||
LoginButton(
|
||||
iconName: "envelope",
|
||||
iconColor: .green,
|
||||
title: LocalizedString("login.email_login", comment: ""),
|
||||
action: {
|
||||
showEmailLogin = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private var bottomButtons: some View {
|
||||
HStack(spacing: 20) {
|
||||
Button(action: {
|
||||
showLanguageSettings = true
|
||||
}) {
|
||||
Text(LocalizedString("login.language", comment: ""))
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
showUserAgreement = true
|
||||
}) {
|
||||
Text(LocalizedString("login.user_agreement", comment: ""))
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
showPrivacyPolicy = true
|
||||
}) {
|
||||
Text(LocalizedString("login.privacy_policy", comment: ""))
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#Preview {
|
||||
|
@@ -6,15 +6,13 @@ struct MainView: View {
|
||||
var onLogout: (() -> Void)? = nil
|
||||
|
||||
var body: some View {
|
||||
WithViewStore(self.store, observe: { $0 }) { viewStore in
|
||||
WithPerceptionTracking {
|
||||
InternalMainView(store: store)
|
||||
.onChange(of: viewStore.isLoggedOut) { isLoggedOut in
|
||||
if isLoggedOut {
|
||||
onLogout?()
|
||||
}
|
||||
WithPerceptionTracking {
|
||||
InternalMainView(store: store)
|
||||
.onChange(of: store.isLoggedOut) {
|
||||
if store.isLoggedOut {
|
||||
onLogout?()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,26 +25,24 @@ struct InternalMainView: View {
|
||||
_path = State(initialValue: store.withState { $0.navigationPath })
|
||||
}
|
||||
var body: some View {
|
||||
WithViewStore(self.store, observe: { $0 }) { viewStore in
|
||||
WithPerceptionTracking {
|
||||
NavigationStack(path: $path) {
|
||||
GeometryReader { geometry in
|
||||
contentView(geometry: geometry, viewStore: viewStore)
|
||||
.navigationDestination(for: MainFeature.Destination.self) { destination in
|
||||
DestinationView(destination: destination, store: self.store)
|
||||
WithPerceptionTracking {
|
||||
NavigationStack(path: $path) {
|
||||
GeometryReader { geometry in
|
||||
contentView(geometry: geometry)
|
||||
.navigationDestination(for: MainFeature.Destination.self) { destination in
|
||||
DestinationView(destination: destination, store: self.store)
|
||||
}
|
||||
.onChange(of: path) {
|
||||
store.send(.navigationPathChanged(path))
|
||||
}
|
||||
.onChange(of: store.navigationPath) {
|
||||
if path != store.navigationPath {
|
||||
path = store.navigationPath
|
||||
}
|
||||
.onChange(of: path) { newPath in
|
||||
viewStore.send(.navigationPathChanged(newPath))
|
||||
}
|
||||
.onChange(of: viewStore.navigationPath) { newPath in
|
||||
if path != newPath {
|
||||
path = newPath
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewStore.send(.onAppear)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
store.send(.onAppear)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,7 +70,7 @@ struct InternalMainView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func contentView(geometry: GeometryProxy, viewStore: ViewStoreOf<MainFeature>) -> some View {
|
||||
private func contentView(geometry: GeometryProxy) -> some View {
|
||||
WithPerceptionTracking {
|
||||
ZStack {
|
||||
// 背景图片
|
||||
@@ -87,18 +83,27 @@ struct InternalMainView: View {
|
||||
// 主内容
|
||||
MainContentView(
|
||||
store: store,
|
||||
selectedTab: viewStore.selectedTab
|
||||
selectedTab: store.selectedTab
|
||||
)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
// 底部导航栏
|
||||
.padding(.bottom, 80) // 为底部导航栏留出空间
|
||||
|
||||
// 底部导航栏 - 固定在底部
|
||||
VStack {
|
||||
Spacer()
|
||||
BottomTabView(selectedTab: viewStore.binding(
|
||||
get: { Tab(rawValue: $0.selectedTab.rawValue) ?? .feed },
|
||||
send: { MainFeature.Action.selectTab(MainFeature.Tab(rawValue: $0.rawValue) ?? .feed) }
|
||||
BottomTabView(selectedTab: Binding(
|
||||
get: { Tab(rawValue: store.selectedTab.rawValue) ?? .feed },
|
||||
set: { newTab in
|
||||
store.send(.selectTab(MainFeature.Tab(rawValue: newTab.rawValue) ?? .feed))
|
||||
}
|
||||
))
|
||||
}
|
||||
.padding(.bottom, geometry.safeAreaInsets.bottom + 60)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
|
||||
.padding(.bottom, 100)
|
||||
.ignoresSafeArea(.keyboard, edges: .bottom)
|
||||
|
||||
// 添加API Loading和错误处理视图
|
||||
APILoadingEffectView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,24 +5,26 @@ struct MeView: View {
|
||||
let store: StoreOf<MeFeature>
|
||||
// 新增:图片预览状态
|
||||
@State private var previewItem: PreviewItem? = nil
|
||||
@State private var previewCurrentIndex: Int = 0
|
||||
|
||||
var body: some View {
|
||||
WithPerceptionTracking {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
Image("bg")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.clipped()
|
||||
.ignoresSafeArea(.all)
|
||||
// 顶部栏,右上角设置按钮
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
WithViewStore(self.store, observe: { $0 }) { viewStore in
|
||||
ZStack {
|
||||
// 背景图片
|
||||
Image("bg")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.clipped()
|
||||
.ignoresSafeArea(.all)
|
||||
|
||||
// 顶部栏,右上角设置按钮
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(action: {
|
||||
viewStore.send(.settingButtonTapped)
|
||||
store.send(.settingButtonTapped)
|
||||
}) {
|
||||
Image(systemName: "gearshape")
|
||||
.font(.system(size: 33, weight: .medium))
|
||||
@@ -31,121 +33,179 @@ struct MeView: View {
|
||||
.padding(.trailing, 16)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
Spacer()
|
||||
|
||||
// 主要内容区域
|
||||
VStack(spacing: 16) {
|
||||
// 用户信息区域
|
||||
userInfoSection()
|
||||
|
||||
// 动态内容区域
|
||||
momentsSection()
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .top)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
VStack(spacing: 16) {
|
||||
// 用户信息区域
|
||||
WithViewStore(self.store, observe: { $0 }) { viewStore in
|
||||
if viewStore.isLoadingUserInfo {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.frame(height: 130)
|
||||
} else if let error = viewStore.userInfoError {
|
||||
Text(error)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.red)
|
||||
.frame(height: 130)
|
||||
} else if let userInfo = viewStore.userInfo {
|
||||
VStack(spacing: 8) {
|
||||
if let avatarUrl = userInfo.avatar, !avatarUrl.isEmpty {
|
||||
AsyncImage(url: URL(string: avatarUrl)) { image in
|
||||
image.resizable().aspectRatio(contentMode: .fill).clipShape(Circle())
|
||||
} placeholder: {
|
||||
Image(systemName: "person.fill").font(.system(size: 40)).foregroundColor(.white)
|
||||
}
|
||||
.frame(width: 90, height: 90)
|
||||
} else {
|
||||
Image(systemName: "person.fill").font(.system(size: 40)).foregroundColor(.white)
|
||||
.frame(width: 90, height: 90)
|
||||
}
|
||||
Text(userInfo.nick ?? "用户昵称")
|
||||
.font(.system(size: 18, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
Text("ID: \(userInfo.uid ?? 0)")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
}
|
||||
.padding(.top, 0)
|
||||
.frame(height: 130)
|
||||
} else {
|
||||
Spacer().frame(height: 130)
|
||||
}
|
||||
}
|
||||
// 动态内容区域
|
||||
WithViewStore(self.store, observe: { $0 }) { viewStore in
|
||||
if viewStore.isLoadingMoments && viewStore.moments.isEmpty {
|
||||
ProgressView("加载中...")
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else if let error = viewStore.momentsError {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.font(.system(size: 40))
|
||||
.foregroundColor(.yellow)
|
||||
Text(error)
|
||||
.foregroundColor(.red)
|
||||
Button("重试") {
|
||||
viewStore.send(.onAppear)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else if viewStore.moments.isEmpty {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "tray")
|
||||
.font(.system(size: 40))
|
||||
.foregroundColor(.gray)
|
||||
Text("暂无动态")
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else {
|
||||
ScrollView {
|
||||
WithPerceptionTracking {
|
||||
LazyVStack(spacing: 12) {
|
||||
ForEach(viewStore.moments.indices, id: \ .self) { index in
|
||||
let moment = viewStore.moments[index]
|
||||
OptimizedDynamicCardView(
|
||||
moment: moment,
|
||||
allMoments: viewStore.moments,
|
||||
currentIndex: index,
|
||||
onImageTap: { images, tappedIndex in
|
||||
previewItem = PreviewItem(images: images, index: tappedIndex)
|
||||
}
|
||||
)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
if viewStore.hasMore {
|
||||
ProgressView()
|
||||
.onAppear {
|
||||
viewStore.send(.loadMore)
|
||||
}
|
||||
}
|
||||
// 新增底部间距
|
||||
Color.clear.frame(height: 120)
|
||||
}
|
||||
.padding(.top, 8)
|
||||
}
|
||||
}
|
||||
.refreshable {
|
||||
viewStore.send(.refresh)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .top)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
ViewStore(self.store, observe: { $0 }).send(.onAppear)
|
||||
store.send(.onAppear)
|
||||
}
|
||||
// 新增:图片预览弹窗
|
||||
.fullScreenCover(item: $previewItem) { item in
|
||||
ImagePreviewPager(images: item.images, currentIndex: .constant(item.index)) {
|
||||
ImagePreviewPager(images: item.images as [String], currentIndex: $previewCurrentIndex) {
|
||||
previewItem = nil
|
||||
}
|
||||
}
|
||||
// 新增:详情页导航
|
||||
.navigationDestination(isPresented: Binding(
|
||||
get: { store.showDetail },
|
||||
set: { _ in store.send(.detailDismissed) }
|
||||
)) {
|
||||
if let selectedMoment = store.selectedMoment {
|
||||
let detailStore = Store(
|
||||
initialState: DetailFeature.State(moment: selectedMoment)
|
||||
) {
|
||||
DetailFeature()
|
||||
}
|
||||
|
||||
DetailView(store: detailStore)
|
||||
.onChange(of: detailStore.shouldDismiss) { shouldDismiss in
|
||||
if shouldDismiss {
|
||||
store.send(.detailDismissed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 用户信息区域
|
||||
@ViewBuilder
|
||||
private func userInfoSection() -> some View {
|
||||
WithPerceptionTracking {
|
||||
VStack(spacing: 16) {
|
||||
// 头像
|
||||
AsyncImage(url: URL(string: store.userInfo?.avatar ?? "")) { image in
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
} placeholder: {
|
||||
Image(systemName: "person.circle.fill")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.frame(width: 80, height: 80)
|
||||
.clipShape(Circle())
|
||||
.overlay(
|
||||
Circle()
|
||||
.stroke(Color.white.opacity(0.3), lineWidth: 2)
|
||||
)
|
||||
|
||||
// 用户昵称
|
||||
Text(store.userInfo?.nick ?? "未知用户")
|
||||
.font(.system(size: 20, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
|
||||
// 用户ID
|
||||
Text("ID: \(store.userInfo?.uid ?? 0)")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
}
|
||||
.padding(.horizontal, 32)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 动态内容区域
|
||||
@ViewBuilder
|
||||
private func momentsSection() -> some View {
|
||||
WithPerceptionTracking {
|
||||
if store.isLoadingUserInfo || store.isLoadingMoments {
|
||||
VStack {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.scaleEffect(1.2)
|
||||
Text("加载中...")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
.padding(.top, 8)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else if let error = store.userInfoError ?? store.momentsError {
|
||||
VStack(spacing: 12) {
|
||||
Image(systemName: "exclamationmark.triangle")
|
||||
.font(.system(size: 32))
|
||||
.foregroundColor(.orange)
|
||||
Text("加载失败")
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
Text(error)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 32)
|
||||
Button("重试") {
|
||||
store.send(.onAppear)
|
||||
}
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.vertical, 8)
|
||||
.background(Color.blue.opacity(0.8))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else if store.moments.isEmpty {
|
||||
VStack(spacing: 12) {
|
||||
Image(systemName: "doc.text")
|
||||
.font(.system(size: 32))
|
||||
.foregroundColor(.white.opacity(0.5))
|
||||
Text("暂无动态")
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else {
|
||||
ScrollView {
|
||||
WithPerceptionTracking {
|
||||
LazyVStack(spacing: 12) {
|
||||
ForEach(Array(store.moments.enumerated()), id: \.element.dynamicId) { index, moment in
|
||||
OptimizedDynamicCardView(
|
||||
moment: moment,
|
||||
allMoments: store.moments,
|
||||
currentIndex: index,
|
||||
onImageTap: { images, tappedIndex in
|
||||
previewCurrentIndex = tappedIndex
|
||||
previewItem = PreviewItem(images: images, index: tappedIndex)
|
||||
},
|
||||
onLikeTap: { _, _, _, _ in
|
||||
// 暂时不处理点赞,后续可以添加点赞功能
|
||||
},
|
||||
onCardTap: {
|
||||
store.send(.showDetail(moment))
|
||||
}
|
||||
)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
if store.hasMore {
|
||||
ProgressView()
|
||||
.onAppear {
|
||||
store.send(.loadMore)
|
||||
}
|
||||
}
|
||||
// 新增底部间距
|
||||
Color.clear.frame(height: 120)
|
||||
}
|
||||
.padding(.top, 8)
|
||||
}
|
||||
}
|
||||
.refreshable {
|
||||
store.send(.refresh)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@ struct SplashView: View {
|
||||
let store: StoreOf<SplashFeature>
|
||||
|
||||
var body: some View {
|
||||
WithPerceptionTracking {
|
||||
ZStack {
|
||||
Group {
|
||||
// 根据导航目标显示不同页面
|
||||
if let navigationDestination = store.navigationDestination {
|
||||
@@ -44,6 +44,9 @@ struct SplashView: View {
|
||||
.onAppear {
|
||||
store.send(.onAppear)
|
||||
}
|
||||
|
||||
// API Loading 效果视图 - 显示在所有内容之上
|
||||
APILoadingEffectView()
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -6,48 +6,36 @@ struct TestView: View {
|
||||
// 背景色
|
||||
Color.purple.ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 30) {
|
||||
// 标题
|
||||
Text("测试页面")
|
||||
.font(.system(size: 32, weight: .bold))
|
||||
.foregroundColor(.white)
|
||||
VStack(spacing: 20) {
|
||||
Text(LocalizedString("test.test_page", comment: ""))
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.bold)
|
||||
|
||||
// 描述文本
|
||||
Text("这是一个测试用的页面\n用于验证导航跳转功能")
|
||||
.font(.system(size: 18))
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
Text(LocalizedString("test.test_description", comment: ""))
|
||||
.font(.body)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.horizontal)
|
||||
|
||||
// 测试按钮
|
||||
Button(action: {
|
||||
debugInfoSync("[LOG] TestView button tapped")
|
||||
}) {
|
||||
Text("测试按钮")
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.purple)
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.vertical, 12)
|
||||
.background(Color.white)
|
||||
.cornerRadius(8)
|
||||
Button(LocalizedString("test.test_button", comment: "")) {
|
||||
// 测试按钮点击事件
|
||||
print("测试按钮被点击")
|
||||
}
|
||||
.padding()
|
||||
.background(Color.blue)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(10)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top, 100)
|
||||
}
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button(action: {
|
||||
debugInfoSync("[LOG] TestView back button tapped")
|
||||
}) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
Text("返回")
|
||||
.font(.system(size: 16))
|
||||
.padding()
|
||||
.navigationTitle(LocalizedString("test.test_page", comment: ""))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button(LocalizedString("test.back", comment: "")) {
|
||||
// dismiss()
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,4 +46,4 @@ struct TestView: View {
|
||||
NavigationStack {
|
||||
TestView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
# Yana 项目问题排查与解决流程文档
|
||||
|
||||
## 目录
|
||||
|
||||
1. [问题概述](#问题概述)
|
||||
2. [解决流程](#解决流程)
|
||||
3. [技术细节](#技术细节)
|
||||
@@ -13,14 +14,17 @@
|
||||
## 问题概述
|
||||
|
||||
### 初始错误
|
||||
|
||||
**错误信息**: `"Could not compute dependency graph: unable to load transferred PIF: The workspace contains multiple references with the same GUID"`
|
||||
|
||||
**问题表现**:
|
||||
|
||||
- 项目无法启动
|
||||
- Xcode 无法计算依赖图
|
||||
- 出现 GUID 冲突错误
|
||||
|
||||
### 根本原因分析
|
||||
|
||||
1. **混合包管理系统**: 项目同时使用了 Swift Package Manager (SPM) 和 CocoaPods
|
||||
2. **缓存冲突**: Xcode DerivedData 与 SPM 状态不同步
|
||||
3. **TCA 结构问题**: 代码中 HomeFeature 缺少必要的状态和 Action 定义
|
||||
@@ -32,6 +36,7 @@
|
||||
### 第一阶段:GUID 冲突解决
|
||||
|
||||
#### 步骤 1: 清理缓存
|
||||
|
||||
```bash
|
||||
# 清理 Xcode DerivedData
|
||||
rm -rf ~/Library/Developer/Xcode/DerivedData/*
|
||||
@@ -42,11 +47,13 @@ swift package resolve
|
||||
```
|
||||
|
||||
#### 步骤 2: 重新安装 CocoaPods
|
||||
|
||||
```bash
|
||||
pod install --clean-install
|
||||
```
|
||||
|
||||
#### 步骤 3: 验证项目解析
|
||||
|
||||
```bash
|
||||
xcodebuild -workspace yana.xcworkspace -list
|
||||
```
|
||||
@@ -54,13 +61,15 @@ xcodebuild -workspace yana.xcworkspace -list
|
||||
### 第二阶段:TCA 结构修复
|
||||
|
||||
#### 问题识别
|
||||
|
||||
- `HomeFeature.State` 缺少 `isSettingPresented` 和 `settingState` 属性
|
||||
- `HomeFeature.Action` 缺少 `settingDismissed` 和 `setting` actions
|
||||
- `HomeView.swift` 中的 `store.scope()` 调用语法错误
|
||||
|
||||
#### 修复步骤
|
||||
|
||||
**1. 修复 HomeFeature.swift**
|
||||
1. 修复 HomeFeature.swift
|
||||
|
||||
```swift
|
||||
@ObservableState
|
||||
struct State: Equatable {
|
||||
@@ -89,7 +98,8 @@ enum Action: Equatable {
|
||||
}
|
||||
```
|
||||
|
||||
**2. 添加子 Reducer**
|
||||
2.添加子 Reducer
|
||||
|
||||
```swift
|
||||
var body: some ReducerOf<Self> {
|
||||
Scope(state: \.settingState, action: \.setting) {
|
||||
@@ -110,7 +120,8 @@ var body: some ReducerOf<Self> {
|
||||
}
|
||||
```
|
||||
|
||||
**3. 修复 HomeView.swift**
|
||||
3.修复 HomeView.swift
|
||||
|
||||
```swift
|
||||
.sheet(isPresented: Binding(
|
||||
get: { store.isSettingPresented },
|
||||
@@ -127,10 +138,12 @@ var body: some ReducerOf<Self> {
|
||||
### 依赖管理配置
|
||||
|
||||
**Swift Package Manager (Package.swift)**:
|
||||
|
||||
- ComposableArchitecture: 1.20.2+
|
||||
- 其他依赖根据需要添加
|
||||
|
||||
**CocoaPods (Podfile)**:
|
||||
|
||||
- Alamofire (网络请求)
|
||||
- SDWebImage (图像加载)
|
||||
- CocoaLumberjack (日志)
|
||||
@@ -147,6 +160,7 @@ Feature
|
||||
```
|
||||
|
||||
### 文件结构
|
||||
|
||||
```
|
||||
yana/
|
||||
├── Features/ # TCA Feature 定义
|
||||
@@ -161,6 +175,7 @@ yana/
|
||||
## 最终解决方案
|
||||
|
||||
### 命令执行顺序
|
||||
|
||||
```bash
|
||||
# 1. 清理环境
|
||||
rm -rf ~/Library/Developer/Xcode/DerivedData/*
|
||||
@@ -224,33 +239,42 @@ check_project() {
|
||||
## 常见问题FAQ
|
||||
|
||||
### Q1: 再次出现 GUID 冲突怎么办?
|
||||
|
||||
**A**: 执行完整清理流程:
|
||||
```bash
|
||||
rm -rf ~/Library/Developer/Xcode/DerivedData/*
|
||||
swift package reset && swift package resolve
|
||||
pod install --clean-install
|
||||
```
|
||||
```bash
|
||||
rm -rf ~/Library/Developer/Xcode/DerivedData/*
|
||||
swift package reset && swift package resolve
|
||||
pod install --clean-install
|
||||
```
|
||||
|
||||
### Q2: TCA Reducer 编译错误如何处理?
|
||||
|
||||
**A**: 检查以下项目:
|
||||
|
||||
- State 属性完整性
|
||||
- Action 枚举完整性
|
||||
- Reducer body 中的 case 处理
|
||||
- 子 Reducer 的 Scope 配置
|
||||
|
||||
### Q3: 如何避免混合包管理器问题?
|
||||
**A**:
|
||||
|
||||
**A**:
|
||||
|
||||
- 尽量使用单一包管理工具
|
||||
- 如需混合使用,确保依赖版本兼容
|
||||
- 定期更新依赖并测试
|
||||
|
||||
### Q4: Swift 6 兼容性警告如何处理?
|
||||
**A**:
|
||||
|
||||
**A**:
|
||||
|
||||
- 短期:可以忽略,不影响功能
|
||||
- 长期:逐步迁移到 Swift 6 Sendable 模式
|
||||
|
||||
### Q5: 项目构建缓慢怎么办?
|
||||
|
||||
**A**:
|
||||
|
||||
- 使用 `xcodebuild -quiet` 减少输出
|
||||
- 开启 Xcode Build System 并行构建
|
||||
- 定期清理 DerivedData
|
||||
@@ -271,5 +295,5 @@ pod install --clean-install
|
||||
---
|
||||
|
||||
**文档更新时间**: 2025-07-10
|
||||
**适用版本**: iOS 16+, Swift 6, TCA 1.20.2+
|
||||
**维护者**: AI Assistant & 开发团队
|
||||
**适用版本**: iOS 17+, Swift 6, TCA 1.20.2+
|
||||
**维护者**: AI Assistant & 开发团队
|
||||
|
Reference in New Issue
Block a user