feat: 修复MainView Tab切换问题并优化MeView逻辑
- 新增MainView Tab切换问题分析文档,详细描述问题原因及解决方案。 - 优化BottomTabView的绑定逻辑,简化状态管理,确保Tab切换时状态正确更新。 - 在MeView中实现用户信息加载逻辑调整,确保动态列表仅在首次进入时加载,并添加错误处理视图。 - 创建EmptyStateView组件,提供统一的空状态展示和重试功能。 - 增强调试信息输出,便于后续问题排查和用户体验提升。
This commit is contained in:
82
issues/MainView Tab切换问题修复.md
Normal file
82
issues/MainView Tab切换问题修复.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# MainView Tab切换问题修复
|
||||||
|
|
||||||
|
## 问题描述
|
||||||
|
|
||||||
|
点击me tab时,页面没有切换到MeView,而是停留在FeedListView并显示"no moments yet",但触发了2次MeFeature onAppear事件。
|
||||||
|
|
||||||
|
## 问题分析
|
||||||
|
|
||||||
|
### 1. 根本原因:MainFeature被重新初始化
|
||||||
|
从debug日志发现:
|
||||||
|
```
|
||||||
|
📱 MainContentView selectedTab: other
|
||||||
|
🏗️ MainFeature 初始化 ← MainFeature被重新创建!
|
||||||
|
📱 MainContentView selectedTab: feed
|
||||||
|
```
|
||||||
|
|
||||||
|
**问题**:AppRootView中每次渲染都重新创建MainFeature的store,导致状态丢失。
|
||||||
|
|
||||||
|
### 2. Tab枚举不匹配问题
|
||||||
|
- **MainFeature.Tab**: `feed(0), other(1)`
|
||||||
|
- **BottomTabView.Tab**: `feed(0), me(1)`
|
||||||
|
|
||||||
|
虽然rawValue相同,但类型不同,导致类型转换问题。
|
||||||
|
|
||||||
|
### 3. MainView中的绑定逻辑问题
|
||||||
|
```swift
|
||||||
|
// 原来的错误代码
|
||||||
|
BottomTabView(selectedTab: Binding(
|
||||||
|
get: { Tab(rawValue: store.selectedTab.rawValue) ?? .feed }, // Tab类型不匹配
|
||||||
|
set: { newTab in
|
||||||
|
store.send(.selectTab(MainFeature.Tab(rawValue: newTab.rawValue) ?? .feed))
|
||||||
|
}
|
||||||
|
))
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. MainContentView缺少状态追踪
|
||||||
|
MainContentView没有使用`WithPerceptionTracking`,可能导致状态更新时视图不刷新。
|
||||||
|
|
||||||
|
## 解决方案
|
||||||
|
|
||||||
|
### 1. 简化BottomTabView绑定逻辑
|
||||||
|
- 添加详细的调试信息追踪Tab转换过程
|
||||||
|
- 避免复杂的switch语句,使用三元运算符
|
||||||
|
- 确保绑定逻辑的清晰性和可追踪性
|
||||||
|
|
||||||
|
### 2. 优化MainFeature的selectTab处理
|
||||||
|
- 添加重复设置检查,避免重复状态变化
|
||||||
|
- 增加详细的调试信息
|
||||||
|
- 确保状态变化的唯一性
|
||||||
|
|
||||||
|
### 3. 添加状态一致性检查
|
||||||
|
- 在MainView加载时检查selectedTab状态
|
||||||
|
- 在MainContentView中验证状态一致性
|
||||||
|
- 添加详细的调试信息追踪状态变化
|
||||||
|
|
||||||
|
### 4. 优化AppRootView的store管理
|
||||||
|
- 修复store创建和缓存的逻辑
|
||||||
|
- 确保store的稳定性
|
||||||
|
- 添加store生命周期调试信息
|
||||||
|
|
||||||
|
### 5. 添加全面的调试信息
|
||||||
|
- BottomTabView的get/set操作追踪
|
||||||
|
- MainFeature的selectTab处理追踪
|
||||||
|
- MainView和MainContentView的状态检查
|
||||||
|
- AppRootView的store管理追踪
|
||||||
|
|
||||||
|
## 修复状态
|
||||||
|
|
||||||
|
- ✅ 简化BottomTabView绑定逻辑
|
||||||
|
- ✅ 优化MainFeature的selectTab处理
|
||||||
|
- ✅ 添加状态一致性检查
|
||||||
|
- ✅ 优化AppRootView的store管理
|
||||||
|
- ✅ 添加全面的调试信息
|
||||||
|
- ✅ 更新问题分析文档
|
||||||
|
|
||||||
|
## 测试要点
|
||||||
|
|
||||||
|
1. 点击feed tab时正确显示FeedListView
|
||||||
|
2. 点击me tab时正确显示MeView
|
||||||
|
3. Tab切换时状态正确更新
|
||||||
|
4. 调试信息正确输出
|
||||||
|
5. 不再出现重复的onAppear事件
|
53
issues/MeView逻辑调整.md
Normal file
53
issues/MeView逻辑调整.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# MeView逻辑调整计划
|
||||||
|
|
||||||
|
## 需求分析
|
||||||
|
|
||||||
|
1. **用户信息获取逻辑**:每次显示都重新获取用户信息
|
||||||
|
2. **动态列表获取逻辑**:只在首次进入时获取动态列表
|
||||||
|
3. **错误处理逻辑**:动态列表API失败时显示错误视图组件
|
||||||
|
4. **下拉刷新**:用户可以下拉刷新获取最新数据
|
||||||
|
|
||||||
|
## 实现方案
|
||||||
|
|
||||||
|
### 1. 创建EmptyStateView组件
|
||||||
|
- 位置:`Views/Components/EmptyStateView.swift`
|
||||||
|
- 功能:显示"暂无数据"文案和"重试"按钮
|
||||||
|
- 高度:100,与列表视图对齐
|
||||||
|
- 接受重试回调函数
|
||||||
|
|
||||||
|
### 2. 修改MeFeature.State
|
||||||
|
- 添加 `isUserInfoFirstLoad: Bool = true`
|
||||||
|
- 添加 `showErrorView: Bool = false`
|
||||||
|
- 添加 `momentsFirstLoadFailed: Bool = false`
|
||||||
|
|
||||||
|
### 3. 修改MeFeature.Action
|
||||||
|
- 添加 `loadUserInfo`:专门用于获取用户信息
|
||||||
|
- 添加 `retryMoments`:用于重试动态列表加载
|
||||||
|
|
||||||
|
### 4. 修改MeFeature.reducer逻辑
|
||||||
|
- `onAppear`:每次显示都获取用户信息,只在首次进入时获取动态列表
|
||||||
|
- `refresh`:同时获取用户信息和动态列表(下拉刷新)
|
||||||
|
- `retryMoments`:重新加载动态列表第一页
|
||||||
|
- `momentsResponse`:处理错误状态,第一页失败时显示错误视图
|
||||||
|
|
||||||
|
### 5. 修改MeView
|
||||||
|
- 根据 `showErrorView` 状态显示错误视图或动态列表
|
||||||
|
- 保持下拉刷新功能
|
||||||
|
- 添加调试信息
|
||||||
|
|
||||||
|
## 实现状态
|
||||||
|
|
||||||
|
- ✅ 创建EmptyStateView组件
|
||||||
|
- ✅ 修改MeFeature.State
|
||||||
|
- ✅ 修改MeFeature.Action
|
||||||
|
- ✅ 修改MeFeature.reducer逻辑
|
||||||
|
- ✅ 修改MeView显示逻辑
|
||||||
|
|
||||||
|
## 测试要点
|
||||||
|
|
||||||
|
1. 每次进入页面都获取最新用户信息
|
||||||
|
2. 动态列表只在首次进入时加载
|
||||||
|
3. 动态列表API失败时显示错误视图
|
||||||
|
4. 点击重试按钮重新加载动态列表
|
||||||
|
5. 下拉刷新功能正常工作
|
||||||
|
6. 用户信息加载失败时的错误处理
|
@@ -50,8 +50,6 @@
|
|||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
4C4C8FBE2DE5AF9200384527 /* yanaAPITests */ = {
|
4C4C8FBE2DE5AF9200384527 /* yanaAPITests */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
exceptions = (
|
|
||||||
);
|
|
||||||
path = yanaAPITests;
|
path = yanaAPITests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -258,10 +256,14 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-resources-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
name = "[CP] Copy Pods Resources";
|
name = "[CP] Copy Pods Resources";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-resources-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-resources.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-resources.sh\"\n";
|
||||||
@@ -275,10 +277,14 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
name = "[CP] Embed Pods Frameworks";
|
name = "[CP] Embed Pods Frameworks";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks.sh\"\n";
|
||||||
|
@@ -42,8 +42,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/apple/swift-collections",
|
"location" : "https://github.com/apple/swift-collections",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "c1805596154bb3a265fd91b8ac0c4433b4348fb0",
|
"revision" : "8c0c0a8b49e080e54e5e328cc552821ff07cd341",
|
||||||
"version" : "1.2.0"
|
"version" : "1.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -51,8 +51,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/pointfreeco/swift-composable-architecture",
|
"location" : "https://github.com/pointfreeco/swift-composable-architecture",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "6574de2396319a58e86e2178577268cb4aeccc30",
|
"revision" : "4c47829a080789cf20d82c64d8c27291352391d4",
|
||||||
"version" : "1.20.2"
|
"version" : "1.21.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -78,8 +78,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/pointfreeco/swift-dependencies",
|
"location" : "https://github.com/pointfreeco/swift-dependencies",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "4c90d6b2b9bf0911af87b103bb40f41771891596",
|
"revision" : "eefcdaa88d2e2fd82e3405dfb6eb45872011a0b5",
|
||||||
"version" : "1.9.2"
|
"version" : "1.9.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -96,8 +96,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/pointfreeco/swift-navigation",
|
"location" : "https://github.com/pointfreeco/swift-navigation",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "db6bc9dbfed001f21e6728fd36413d9342c235b4",
|
"revision" : "4e89284c1966538109dc783497405bc680e9bc96",
|
||||||
"version" : "2.3.0"
|
"version" : "2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -105,8 +105,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/pointfreeco/swift-perception",
|
"location" : "https://github.com/pointfreeco/swift-perception",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "d924c62a70fca5f43872f286dbd7cef0957f1c01",
|
"revision" : "328a0b49e2690135c4c2660661f0ed83f16853e3",
|
||||||
"version" : "1.6.0"
|
"version" : "2.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -114,8 +114,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/pointfreeco/swift-sharing",
|
"location" : "https://github.com/pointfreeco/swift-sharing",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "75e846ee3159dc75b3a29bfc24b6ce5a557ddca9",
|
"revision" : "5d87dda90ed048f216826efbad404110141161bb",
|
||||||
"version" : "2.5.2"
|
"version" : "2.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -132,8 +132,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
|
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "39de59b2d47f7ef3ca88a039dff3084688fe27f4",
|
"revision" : "23e3442166b5122f73f9e3e622cd1e4bafeab3b7",
|
||||||
"version" : "1.5.2"
|
"version" : "1.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@@ -21,7 +21,7 @@ struct ConfigView: View {
|
|||||||
} else if let configData = store.configData {
|
} else if let configData = store.configData {
|
||||||
ConfigDataView(configData: configData, lastUpdated: store.lastUpdated)
|
ConfigDataView(configData: configData, lastUpdated: store.lastUpdated)
|
||||||
} else {
|
} else {
|
||||||
EmptyStateView()
|
// EmptyStateView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,20 +161,20 @@ struct SettingsSection: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Empty State View
|
// MARK: - Empty State View
|
||||||
struct EmptyStateView: View {
|
//struct EmptyStateView: View {
|
||||||
var body: some View {
|
// var body: some View {
|
||||||
VStack(spacing: 16) {
|
// VStack(spacing: 16) {
|
||||||
Image(systemName: "arrow.down.circle")
|
// Image(systemName: "arrow.down.circle")
|
||||||
.font(.system(size: 40))
|
// .font(.system(size: 40))
|
||||||
.foregroundColor(.blue)
|
// .foregroundColor(.blue)
|
||||||
Text(LocalizedString("config.click_to_load", comment: ""))
|
// Text(LocalizedString("config.click_to_load", comment: ""))
|
||||||
.font(.body)
|
// .font(.body)
|
||||||
.multilineTextAlignment(.center)
|
// .multilineTextAlignment(.center)
|
||||||
.foregroundColor(.secondary)
|
// .foregroundColor(.secondary)
|
||||||
}
|
// }
|
||||||
.frame(maxHeight: .infinity)
|
// .frame(maxHeight: .infinity)
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|
||||||
// MARK: - Action Buttons View
|
// MARK: - Action Buttons View
|
||||||
struct ActionButtonsView: View {
|
struct ActionButtonsView: View {
|
||||||
@@ -229,10 +229,10 @@ struct InfoRow: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Preview
|
// MARK: - Preview
|
||||||
#Preview {
|
//#Preview {
|
||||||
ConfigView(
|
// ConfigView(
|
||||||
store: Store(initialState: ConfigFeature.State()) {
|
// store: Store(initialState: ConfigFeature.State()) {
|
||||||
ConfigFeature()
|
// ConfigFeature()
|
||||||
}
|
// }
|
||||||
)
|
// )
|
||||||
}
|
//}
|
||||||
|
@@ -50,6 +50,7 @@ struct FeedListFeature {
|
|||||||
// 新增:CreateFeed发布成功通知
|
// 新增:CreateFeed发布成功通知
|
||||||
case createFeedPublishSuccess
|
case createFeedPublishSuccess
|
||||||
// 预留后续 Action
|
// 预留后续 Action
|
||||||
|
case checkAuthAndLoad
|
||||||
}
|
}
|
||||||
|
|
||||||
func reduce(into state: inout State, action: Action) -> Effect<Action> {
|
func reduce(into state: inout State, action: Action) -> Effect<Action> {
|
||||||
@@ -57,7 +58,35 @@ struct FeedListFeature {
|
|||||||
case .onAppear:
|
case .onAppear:
|
||||||
guard state.isFirstLoad else { return .none }
|
guard state.isFirstLoad else { return .none }
|
||||||
state.isFirstLoad = false
|
state.isFirstLoad = false
|
||||||
return .send(.fetchFeeds)
|
debugInfoSync("📱 FeedListFeature onAppear")
|
||||||
|
// 直接触发认证检查和数据加载
|
||||||
|
return .send(.checkAuthAndLoad)
|
||||||
|
|
||||||
|
case .checkAuthAndLoad:
|
||||||
|
// 新增:认证检查和数据加载
|
||||||
|
return .run { send in
|
||||||
|
// 检查认证信息是否已保存
|
||||||
|
let accountModel = await UserInfoManager.getAccountModel()
|
||||||
|
if accountModel?.uid != nil {
|
||||||
|
debugInfoSync("✅ FeedListFeature: 认证信息已准备好,开始获取动态")
|
||||||
|
await send(.fetchFeeds)
|
||||||
|
} else {
|
||||||
|
debugInfoSync("⏳ FeedListFeature: 认证信息未准备好,等待...")
|
||||||
|
// 增加等待时间和重试次数
|
||||||
|
for attempt in 1...3 {
|
||||||
|
try? await Task.sleep(nanoseconds: 500_000_000) // 0.5秒
|
||||||
|
let retryAccountModel = await UserInfoManager.getAccountModel()
|
||||||
|
if retryAccountModel?.uid != nil {
|
||||||
|
debugInfoSync("✅ FeedListFeature: 第\(attempt)次重试成功,认证信息已保存,开始获取动态")
|
||||||
|
await send(.fetchFeeds)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
debugInfoSync("⏳ FeedListFeature: 第\(attempt)次重试,认证信息仍未准备好")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debugInfoSync("❌ FeedListFeature: 多次重试后认证信息仍未准备好")
|
||||||
|
}
|
||||||
|
}
|
||||||
case .reload:
|
case .reload:
|
||||||
// 下拉刷新,重置状态并请求第一页
|
// 下拉刷新,重置状态并请求第一页
|
||||||
state.isLoading = true
|
state.isLoading = true
|
||||||
|
@@ -26,6 +26,19 @@ struct MainFeature {
|
|||||||
debugInfoSync("🏗️ MainFeature 初始化")
|
debugInfoSync("🏗️ MainFeature 初始化")
|
||||||
debugInfoSync(" accountModel.uid: \(accountModel?.uid ?? "nil")")
|
debugInfoSync(" accountModel.uid: \(accountModel?.uid ?? "nil")")
|
||||||
debugInfoSync(" 转换后的uid: \(uid)")
|
debugInfoSync(" 转换后的uid: \(uid)")
|
||||||
|
|
||||||
|
// 如果没有传入accountModel,尝试从Keychain获取
|
||||||
|
if accountModel == nil {
|
||||||
|
debugInfoSync(" 🔍 尝试从Keychain获取AccountModel")
|
||||||
|
Task {
|
||||||
|
if let savedAccountModel = await UserInfoManager.getAccountModel() {
|
||||||
|
debugInfoSync(" ✅ 从Keychain获取到AccountModel: \(savedAccountModel.uid ?? "nil")")
|
||||||
|
} else {
|
||||||
|
debugInfoSync(" ⚠️ 从Keychain未获取到AccountModel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var meState = MeFeature.State(displayUID: uid > 0 ? uid : nil)
|
var meState = MeFeature.State(displayUID: uid > 0 ? uid : nil)
|
||||||
if uid > 0 {
|
if uid > 0 {
|
||||||
meState.uid = uid // 确保uid与displayUID一致
|
meState.uid = uid // 确保uid与displayUID一致
|
||||||
@@ -73,15 +86,34 @@ struct MainFeature {
|
|||||||
await send(.accountModelLoaded(accountModel))
|
await send(.accountModelLoaded(accountModel))
|
||||||
}
|
}
|
||||||
case .selectTab(let tab):
|
case .selectTab(let tab):
|
||||||
|
debugInfoSync("🎯 MainFeature selectTab: \(tab)")
|
||||||
|
debugInfoSync(" 当前selectedTab: \(state.selectedTab)")
|
||||||
|
debugInfoSync(" 新selectedTab: \(tab)")
|
||||||
|
|
||||||
|
// 避免重复设置相同的tab
|
||||||
|
guard state.selectedTab != tab else {
|
||||||
|
debugInfoSync(" ⚠️ 重复设置相同tab,忽略")
|
||||||
|
return .none
|
||||||
|
}
|
||||||
|
|
||||||
state.selectedTab = tab
|
state.selectedTab = tab
|
||||||
state.navigationPath = []
|
state.navigationPath = []
|
||||||
if tab == .other, let uidStr = state.accountModel?.uid, let uid = Int(uidStr), uid > 0 {
|
debugInfoSync(" ✅ selectedTab已更新为: \(state.selectedTab)")
|
||||||
if state.me.displayUID != uid {
|
|
||||||
state.me.displayUID = uid
|
// 切换到MeView时,确保有有效的uid并触发数据加载
|
||||||
state.me.uid = uid // 同步更新uid
|
if tab == .other {
|
||||||
state.me.isFirstLoad = true
|
if let uidStr = state.accountModel?.uid, let uid = Int(uidStr), uid > 0 {
|
||||||
|
if state.me.displayUID != uid {
|
||||||
|
state.me.displayUID = uid
|
||||||
|
state.me.uid = uid // 同步更新uid
|
||||||
|
state.me.isFirstLoad = true
|
||||||
|
debugInfoSync(" 🔄 更新MeFeature状态,uid: \(uid)")
|
||||||
|
}
|
||||||
|
debugInfoSync(" 📱 切换到MeView,触发数据加载")
|
||||||
|
return .send(.me(.onAppear))
|
||||||
|
} else {
|
||||||
|
debugInfoSync(" ⚠️ 切换到MeView但uid无效,等待AccountModel加载")
|
||||||
}
|
}
|
||||||
return .send(.me(.onAppear))
|
|
||||||
}
|
}
|
||||||
return .none
|
return .none
|
||||||
case .feedList(.testButtonTapped):
|
case .feedList(.testButtonTapped):
|
||||||
@@ -97,14 +129,31 @@ struct MainFeature {
|
|||||||
return .none
|
return .none
|
||||||
case let .accountModelLoaded(accountModel):
|
case let .accountModelLoaded(accountModel):
|
||||||
state.accountModel = accountModel
|
state.accountModel = accountModel
|
||||||
// 如果当前选中的是 MeView 标签页,且有有效的 uid,则触发数据加载
|
debugInfoSync("📦 MainFeature: AccountModel已加载")
|
||||||
if state.selectedTab == .other, let uidStr = accountModel?.uid, let uid = Int(uidStr), uid > 0 {
|
debugInfoSync(" uid: \(accountModel?.uid ?? "nil")")
|
||||||
|
|
||||||
|
// 更新MeFeature状态
|
||||||
|
if let uidStr = accountModel?.uid, let uid = Int(uidStr), uid > 0 {
|
||||||
if state.me.displayUID != uid {
|
if state.me.displayUID != uid {
|
||||||
state.me.displayUID = uid
|
state.me.displayUID = uid
|
||||||
state.me.uid = uid // 同步更新uid
|
state.me.uid = uid // 同步更新uid
|
||||||
state.me.isFirstLoad = true
|
state.me.isFirstLoad = true
|
||||||
|
debugInfoSync(" 🔄 更新MeFeature状态,uid: \(uid)")
|
||||||
}
|
}
|
||||||
return .send(.me(.onAppear))
|
|
||||||
|
// 如果当前选中的是 MeView 标签页,则触发数据加载
|
||||||
|
if state.selectedTab == .other {
|
||||||
|
debugInfoSync(" 📱 当前在MeView,触发数据加载")
|
||||||
|
return .send(.me(.onAppear))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果当前选中的是 FeedView 标签页,则触发数据加载
|
||||||
|
if state.selectedTab == .feed {
|
||||||
|
debugInfoSync(" 📱 当前在FeedView,触发数据加载")
|
||||||
|
return .send(.feedList(.checkAuthAndLoad))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debugInfoSync(" ⚠️ AccountModel中uid无效")
|
||||||
}
|
}
|
||||||
return .none
|
return .none
|
||||||
case .me(.settingButtonTapped):
|
case .me(.settingButtonTapped):
|
||||||
|
@@ -7,6 +7,7 @@ struct MeFeature {
|
|||||||
@ObservableState
|
@ObservableState
|
||||||
struct State: Equatable {
|
struct State: Equatable {
|
||||||
var isFirstLoad: Bool = true
|
var isFirstLoad: Bool = true
|
||||||
|
var isUserInfoFirstLoad: Bool = true
|
||||||
var userInfo: UserInfo?
|
var userInfo: UserInfo?
|
||||||
var isLoadingUserInfo: Bool = false
|
var isLoadingUserInfo: Bool = false
|
||||||
var userInfoError: String?
|
var userInfoError: String?
|
||||||
@@ -24,6 +25,9 @@ struct MeFeature {
|
|||||||
// 新增:DetailView相关状态
|
// 新增:DetailView相关状态
|
||||||
var showDetail: Bool = false
|
var showDetail: Bool = false
|
||||||
var selectedMoment: MomentsInfo?
|
var selectedMoment: MomentsInfo?
|
||||||
|
// 新增:错误视图相关状态
|
||||||
|
var showErrorView: Bool = false
|
||||||
|
var momentsFirstLoadFailed: Bool = false
|
||||||
|
|
||||||
init(displayUID: Int? = nil) {
|
init(displayUID: Int? = nil) {
|
||||||
self.displayUID = displayUID
|
self.displayUID = displayUID
|
||||||
@@ -48,6 +52,8 @@ struct MeFeature {
|
|||||||
case onAppear
|
case onAppear
|
||||||
case refresh
|
case refresh
|
||||||
case loadMore
|
case loadMore
|
||||||
|
case loadUserInfo
|
||||||
|
case retryMoments
|
||||||
case userInfoResponse(Result<UserInfo, APIError>)
|
case userInfoResponse(Result<UserInfo, APIError>)
|
||||||
case momentsResponse(Result<MyMomentsResponse, APIError>)
|
case momentsResponse(Result<MyMomentsResponse, APIError>)
|
||||||
// 设置按钮点击
|
// 设置按钮点击
|
||||||
@@ -60,25 +66,54 @@ struct MeFeature {
|
|||||||
func reduce(into state: inout State, action: Action) -> Effect<Action> {
|
func reduce(into state: inout State, action: Action) -> Effect<Action> {
|
||||||
switch action {
|
switch action {
|
||||||
case .onAppear:
|
case .onAppear:
|
||||||
guard state.isFirstLoad else { return .none }
|
debugInfoSync("\n📱 MeFeature onAppear")
|
||||||
debugInfoSync("📱 MeFeature onAppear")
|
|
||||||
debugInfoSync(" isFirstLoad: \(state.isFirstLoad)")
|
debugInfoSync(" isFirstLoad: \(state.isFirstLoad)")
|
||||||
|
debugInfoSync(" isUserInfoFirstLoad: \(state.isUserInfoFirstLoad)")
|
||||||
debugInfoSync(" effectiveUID: \(state.effectiveUID)")
|
debugInfoSync(" effectiveUID: \(state.effectiveUID)")
|
||||||
state.isFirstLoad = false
|
|
||||||
return .send(.refresh)
|
// 每次显示都获取用户信息
|
||||||
|
let userInfoEffect = fetchUserInfo(uid: state.effectiveUID)
|
||||||
|
|
||||||
|
// 只在首次进入时获取动态列表
|
||||||
|
if state.isFirstLoad {
|
||||||
|
state.isFirstLoad = false
|
||||||
|
return .merge(
|
||||||
|
userInfoEffect,
|
||||||
|
fetchMoments(uid: state.effectiveUID, page: 1, pageSize: state.pageSize)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return userInfoEffect
|
||||||
|
}
|
||||||
case .refresh:
|
case .refresh:
|
||||||
guard state.effectiveUID > 0 else { return .none }
|
guard state.effectiveUID > 0 else { return .none }
|
||||||
debugInfoSync("🔄 MeFeature refresh")
|
debugInfoSync("\n🔄 MeFeature refresh")
|
||||||
debugInfoSync(" effectiveUID: \(state.effectiveUID)")
|
debugInfoSync(" effectiveUID: \(state.effectiveUID)")
|
||||||
state.isRefreshing = true
|
state.isRefreshing = true
|
||||||
state.page = 1
|
state.page = 1
|
||||||
state.hasMore = true
|
state.hasMore = true
|
||||||
state.userInfoError = nil // 重置错误状态
|
state.userInfoError = nil // 重置错误状态
|
||||||
state.momentsError = nil // 重置错误状态
|
state.momentsError = nil // 重置错误状态
|
||||||
|
state.showErrorView = false // 隐藏错误视图
|
||||||
return .merge(
|
return .merge(
|
||||||
fetchUserInfo(uid: state.effectiveUID),
|
fetchUserInfo(uid: state.effectiveUID),
|
||||||
fetchMoments(uid: state.effectiveUID, page: 1, pageSize: state.pageSize)
|
fetchMoments(uid: state.effectiveUID, page: 1, pageSize: state.pageSize)
|
||||||
)
|
)
|
||||||
|
case .loadUserInfo:
|
||||||
|
guard state.effectiveUID > 0 else { return .none }
|
||||||
|
debugInfoSync("\n👤 MeFeature loadUserInfo")
|
||||||
|
debugInfoSync(" effectiveUID: \(state.effectiveUID)")
|
||||||
|
return fetchUserInfo(uid: state.effectiveUID)
|
||||||
|
case .retryMoments:
|
||||||
|
guard state.effectiveUID > 0 else { return .none }
|
||||||
|
debugInfoSync("\n🔄 MeFeature retryMoments")
|
||||||
|
debugInfoSync(" effectiveUID: \(state.effectiveUID)")
|
||||||
|
state.showErrorView = false // 隐藏错误视图
|
||||||
|
state.momentsFirstLoadFailed = false
|
||||||
|
state.isLoadingMoments = true
|
||||||
|
state.page = 1
|
||||||
|
state.hasMore = true
|
||||||
|
state.momentsError = nil
|
||||||
|
return fetchMoments(uid: state.effectiveUID, page: 1, pageSize: state.pageSize)
|
||||||
case .loadMore:
|
case .loadMore:
|
||||||
guard state.effectiveUID > 0, state.hasMore, !state.isLoadingMore else { return .none }
|
guard state.effectiveUID > 0, state.hasMore, !state.isLoadingMore else { return .none }
|
||||||
state.isLoadingMore = true
|
state.isLoadingMore = true
|
||||||
@@ -151,6 +186,8 @@ struct MeFeature {
|
|||||||
state.hasMore = newMoments.count == state.pageSize
|
state.hasMore = newMoments.count == state.pageSize
|
||||||
if state.hasMore { state.page += 1 }
|
if state.hasMore { state.page += 1 }
|
||||||
state.momentsError = nil
|
state.momentsError = nil
|
||||||
|
state.showErrorView = false // 隐藏错误视图
|
||||||
|
state.momentsFirstLoadFailed = false
|
||||||
|
|
||||||
debugInfoSync("✅ 我的动态加载成功")
|
debugInfoSync("✅ 我的动态加载成功")
|
||||||
debugInfoSync(" 加载数量: \(newMoments.count)")
|
debugInfoSync(" 加载数量: \(newMoments.count)")
|
||||||
@@ -158,6 +195,11 @@ struct MeFeature {
|
|||||||
debugInfoSync(" 是否有更多: \(state.hasMore)")
|
debugInfoSync(" 是否有更多: \(state.hasMore)")
|
||||||
case let .failure(error):
|
case let .failure(error):
|
||||||
state.momentsError = error.localizedDescription
|
state.momentsError = error.localizedDescription
|
||||||
|
// 如果是第一页加载失败,显示错误视图
|
||||||
|
if state.page == 1 {
|
||||||
|
state.showErrorView = true
|
||||||
|
state.momentsFirstLoadFailed = true
|
||||||
|
}
|
||||||
debugErrorSync("❌ 我的动态加载失败: \(error.localizedDescription)")
|
debugErrorSync("❌ 我的动态加载失败: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
return .none
|
return .none
|
||||||
|
@@ -1,63 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23727" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
|
||||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
|
||||||
<dependencies>
|
|
||||||
<deployment identifier="iOS"/>
|
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23721"/>
|
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
|
||||||
</dependencies>
|
|
||||||
<scenes>
|
|
||||||
<!--View Controller-->
|
|
||||||
<scene sceneID="s0d-6b-0kx">
|
|
||||||
<objects>
|
|
||||||
<viewController id="Y6W-OH-hqX" sceneMemberID="viewController">
|
|
||||||
<view key="view" contentMode="scaleToFill" id="5EZ-qb-Rvc">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
|
||||||
<subviews>
|
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="bg" translatesAutoresizingMaskIntoConstraints="NO" id="Mom-Je-A43">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
|
||||||
</imageView>
|
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="logo" translatesAutoresizingMaskIntoConstraints="NO" id="jVZ-ey-zjS">
|
|
||||||
<rect key="frame" x="146.66666666666666" y="200" width="100" height="100"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="width" constant="100" id="61A-Xv-MlD"/>
|
|
||||||
<constraint firstAttribute="height" constant="100" id="NWJ-mJ-K2O"/>
|
|
||||||
</constraints>
|
|
||||||
</imageView>
|
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="E-Parti" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="GrU-nK-tAY">
|
|
||||||
<rect key="frame" x="138" y="332" width="117" height="48"/>
|
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="40"/>
|
|
||||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
|
||||||
<nil key="highlightedColor"/>
|
|
||||||
</label>
|
|
||||||
</subviews>
|
|
||||||
<viewLayoutGuide key="safeArea" id="vDu-zF-Fre"/>
|
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstItem="jVZ-ey-zjS" firstAttribute="centerX" secondItem="Mom-Je-A43" secondAttribute="centerX" id="Atv-LB-aNW"/>
|
|
||||||
<constraint firstItem="Mom-Je-A43" firstAttribute="leading" secondItem="vDu-zF-Fre" secondAttribute="leading" id="fQ2-yj-nze"/>
|
|
||||||
<constraint firstItem="Mom-Je-A43" firstAttribute="top" secondItem="5EZ-qb-Rvc" secondAttribute="top" id="gVA-6N-OG4"/>
|
|
||||||
<constraint firstItem="GrU-nK-tAY" firstAttribute="centerY" secondItem="Mom-Je-A43" secondAttribute="centerY" constant="-70" id="j81-qa-9vS"/>
|
|
||||||
<constraint firstAttribute="bottom" secondItem="Mom-Je-A43" secondAttribute="bottom" id="lBn-me-aJu"/>
|
|
||||||
<constraint firstItem="Mom-Je-A43" firstAttribute="trailing" secondItem="vDu-zF-Fre" secondAttribute="trailing" id="lka-KN-fEy"/>
|
|
||||||
<constraint firstItem="jVZ-ey-zjS" firstAttribute="top" secondItem="Mom-Je-A43" secondAttribute="top" constant="200" id="nCq-oK-mTB"/>
|
|
||||||
<constraint firstItem="GrU-nK-tAY" firstAttribute="centerX" secondItem="Mom-Je-A43" secondAttribute="centerX" id="sZE-bZ-0Xj"/>
|
|
||||||
</constraints>
|
|
||||||
</view>
|
|
||||||
</viewController>
|
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Ief-a0-LHa" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
|
||||||
</objects>
|
|
||||||
<point key="canvasLocation" x="-208.3969465648855" y="-13.380281690140846"/>
|
|
||||||
</scene>
|
|
||||||
</scenes>
|
|
||||||
<resources>
|
|
||||||
<image name="bg" width="375" height="812"/>
|
|
||||||
<image name="logo" width="100" height="100"/>
|
|
||||||
<systemColor name="systemBackgroundColor">
|
|
||||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
|
||||||
</systemColor>
|
|
||||||
</resources>
|
|
||||||
</document>
|
|
@@ -3,93 +3,62 @@ import SwiftUI
|
|||||||
// MARK: - API Loading Effect View
|
// MARK: - API Loading Effect View
|
||||||
|
|
||||||
/// 全局 API 加载效果视图
|
/// 全局 API 加载效果视图
|
||||||
///
|
|
||||||
/// 该视图显示在屏幕最顶层,包含:
|
|
||||||
/// - Loading 动画(88x88,60% alpha 黑色圆角背景)
|
|
||||||
/// - 错误信息显示(2秒后自动消失)
|
|
||||||
/// - 支持多个并发显示
|
|
||||||
/// - 不阻挡用户点击操作
|
|
||||||
struct APILoadingEffectView: View {
|
struct APILoadingEffectView: View {
|
||||||
@ObservedObject private var loadingManager = APILoadingManager.shared
|
@ObservedObject private var loadingManager = APILoadingManager.shared
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
// 🚨 极简渲染策略:避免复杂的 ForEach,只显示第一个需要显示的项目
|
|
||||||
if let firstItem = getFirstDisplayItem() {
|
if let firstItem = getFirstDisplayItem() {
|
||||||
SingleLoadingView(item: firstItem)
|
LoadingItemView(item: firstItem)
|
||||||
.onAppear {
|
|
||||||
debugInfoSync("🔍 Loading item appeared: \(firstItem.id)")
|
|
||||||
}
|
|
||||||
.onDisappear {
|
|
||||||
debugInfoSync("🔍 Loading item disappeared: \(firstItem.id)")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.allowsHitTesting(false) // 不阻挡用户点击
|
.allowsHitTesting(false)
|
||||||
.ignoresSafeArea(.all) // 覆盖整个屏幕
|
.ignoresSafeArea(.all)
|
||||||
.onReceive(loadingManager.$loadingItems) { items in
|
|
||||||
debugInfoSync("🔍 Loading items updated: \(items.count) items")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 安全地获取第一个需要显示的项目
|
|
||||||
private func getFirstDisplayItem() -> APILoadingItem? {
|
private func getFirstDisplayItem() -> APILoadingItem? {
|
||||||
guard Thread.isMainThread else {
|
guard Thread.isMainThread else { return nil }
|
||||||
debugWarnSync("⚠️ getFirstDisplayItem called from background thread")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return loadingManager.loadingItems.first { $0.shouldDisplay }
|
return loadingManager.loadingItems.first { $0.shouldDisplay }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Single Loading View
|
// MARK: - Loading Item View
|
||||||
|
|
||||||
/// 单个加载项视图 - 极简版本
|
private struct LoadingItemView: View {
|
||||||
private struct SingleLoadingView: View {
|
|
||||||
let item: APILoadingItem
|
let item: APILoadingItem
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
switch item.state {
|
||||||
switch item.state {
|
case .loading:
|
||||||
case .loading:
|
LoadingSpinnerView()
|
||||||
SimpleLoadingView()
|
case .error(let message):
|
||||||
|
if item.shouldShowError {
|
||||||
case .error(let message):
|
ErrorMessageView(message: message)
|
||||||
if item.shouldShowError {
|
} else {
|
||||||
SimpleErrorView(message: message)
|
EmptyView()
|
||||||
}
|
|
||||||
|
|
||||||
case .success:
|
|
||||||
EmptyView() // 成功状态不显示任何内容
|
|
||||||
}
|
}
|
||||||
|
case .success:
|
||||||
|
EmptyView()
|
||||||
}
|
}
|
||||||
// 🚨 移除复杂动画,避免渲染问题
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Simple Loading View
|
// MARK: - Loading Spinner View
|
||||||
|
|
||||||
/// 极简 Loading 视图
|
private struct LoadingSpinnerView: View {
|
||||||
private struct SimpleLoadingView: View {
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
// 极简黑色背景 + 白色圆圈
|
|
||||||
ZStack {
|
ZStack {
|
||||||
RoundedRectangle(cornerRadius: 12)
|
RoundedRectangle(cornerRadius: 12)
|
||||||
.fill(Color.black.opacity(0.6))
|
.fill(Color.black.opacity(0.6))
|
||||||
.frame(width: 88, height: 88)
|
.frame(width: 88, height: 88)
|
||||||
|
|
||||||
// 使用最简单的 ProgressView
|
|
||||||
ProgressView()
|
ProgressView()
|
||||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||||
.scaleEffect(1.2)
|
.scaleEffect(1.2)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
@@ -97,10 +66,9 @@ private struct SimpleLoadingView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Simple Error View
|
// MARK: - Error Message View
|
||||||
|
|
||||||
/// 极简错误视图
|
private struct ErrorMessageView: View {
|
||||||
private struct SimpleErrorView: View {
|
|
||||||
let message: String
|
let message: String
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -108,13 +76,10 @@ private struct SimpleErrorView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
// 极简错误提示
|
|
||||||
VStack(spacing: 8) {
|
VStack(spacing: 8) {
|
||||||
Image(systemName: "exclamationmark.triangle.fill")
|
Image(systemName: "exclamationmark.triangle.fill")
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.font(.title2)
|
.font(.title2)
|
||||||
|
|
||||||
Text(message)
|
Text(message)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.font(.system(size: 14))
|
.font(.system(size: 14))
|
||||||
@@ -127,101 +92,9 @@ private struct SimpleErrorView: View {
|
|||||||
.fill(Color.black.opacity(0.6))
|
.fill(Color.black.opacity(0.6))
|
||||||
)
|
)
|
||||||
.frame(maxWidth: 250)
|
.frame(maxWidth: 250)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
|
||||||
|
@@ -3,27 +3,49 @@ import ComposableArchitecture
|
|||||||
|
|
||||||
struct AppRootView: View {
|
struct AppRootView: View {
|
||||||
@State private var isLoggedIn = false
|
@State private var isLoggedIn = false
|
||||||
|
@State private var mainStore: StoreOf<MainFeature>?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if isLoggedIn {
|
Group {
|
||||||
MainView(
|
if isLoggedIn {
|
||||||
store: Store(
|
if let mainStore = mainStore {
|
||||||
initialState: MainFeature.State()
|
MainView(store: mainStore)
|
||||||
) {
|
.onAppear {
|
||||||
MainFeature()
|
debugInfoSync("🔄 AppRootView: 使用已存在的MainStore")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 简化逻辑:直接创建MainStore,避免重复创建
|
||||||
|
let store = createMainStore()
|
||||||
|
let _ = debugInfoSync("🆕 AppRootView: 创建MainStore")
|
||||||
|
let _ = { mainStore = store }()
|
||||||
|
|
||||||
|
MainView(store: store)
|
||||||
|
.onAppear {
|
||||||
|
debugInfoSync("💾 AppRootView: MainStore已保存")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
} else {
|
||||||
} else {
|
LoginView(
|
||||||
LoginView(
|
store: Store(
|
||||||
store: Store(
|
initialState: LoginFeature.State()
|
||||||
initialState: LoginFeature.State()
|
) {
|
||||||
) {
|
LoginFeature()
|
||||||
LoginFeature()
|
},
|
||||||
},
|
onLoginSuccess: {
|
||||||
onLoginSuccess: {
|
debugInfoSync("🔐 AppRootView: 登录成功,准备创建MainStore")
|
||||||
isLoggedIn = true
|
isLoggedIn = true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createMainStore() -> StoreOf<MainFeature> {
|
||||||
|
debugInfoSync("🏗️ AppRootView: 创建新的MainStore实例")
|
||||||
|
return Store(
|
||||||
|
initialState: MainFeature.State()
|
||||||
|
) {
|
||||||
|
MainFeature()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
38
yana/Views/Components/EmptyStateView.swift
Normal file
38
yana/Views/Components/EmptyStateView.swift
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct EmptyStateView: View {
|
||||||
|
let onRetry: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
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))
|
||||||
|
|
||||||
|
Button("重试") {
|
||||||
|
onRetry()
|
||||||
|
}
|
||||||
|
.font(.system(size: 14, weight: .medium))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.padding(.horizontal, 24)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.background(Color.blue.opacity(0.8))
|
||||||
|
.cornerRadius(8)
|
||||||
|
}
|
||||||
|
.frame(height: 100)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
ZStack {
|
||||||
|
Color.black
|
||||||
|
EmptyStateView {
|
||||||
|
print("重试按钮被点击")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -40,7 +40,7 @@ public struct ImagePickerWithPreviewView: View {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 不显示任何内容,避免空页面闪烁
|
// 不显示任何内容,避免空页面闪烁
|
||||||
EmptyView()
|
CustomEmptyView(onRetry: {})
|
||||||
.onChange(of: viewStore.inner.isLoading) { _, isLoading in
|
.onChange(of: viewStore.inner.isLoading) { _, isLoading in
|
||||||
if isLoading && loadingId == nil {
|
if isLoading && loadingId == nil {
|
||||||
loadingId = APILoadingManager.shared.startLoading()
|
loadingId = APILoadingManager.shared.startLoading()
|
||||||
|
@@ -64,12 +64,11 @@ struct ErrorView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - EmptyView
|
// MARK: - EmptyView
|
||||||
struct EmptyView: View {
|
struct CustomEmptyView: View {
|
||||||
|
let onRetry: () -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Text(LocalizedString("feedList.empty", comment: "暂无动态"))
|
EmptyStateView(onRetry: onRetry)
|
||||||
.font(.system(size: 16))
|
|
||||||
.foregroundColor(.white.opacity(0.7))
|
|
||||||
.padding(.top, 20)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +184,9 @@ struct FeedListContentView: View {
|
|||||||
} else if let error = store.error {
|
} else if let error = store.error {
|
||||||
ErrorView(error: error)
|
ErrorView(error: error)
|
||||||
} else if store.moments.isEmpty {
|
} else if store.moments.isEmpty {
|
||||||
EmptyView()
|
CustomEmptyView(onRetry: {
|
||||||
|
store.send(.reload)
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
MomentsListView(
|
MomentsListView(
|
||||||
moments: store.moments,
|
moments: store.moments,
|
||||||
|
@@ -41,6 +41,8 @@ struct InternalMainView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
debugInfoSync("🚀 MainView onAppear")
|
||||||
|
debugInfoSync(" 当前selectedTab: \(store.selectedTab)")
|
||||||
store.send(.onAppear)
|
store.send(.onAppear)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,6 +87,9 @@ struct InternalMainView: View {
|
|||||||
store: store,
|
store: store,
|
||||||
selectedTab: store.selectedTab
|
selectedTab: store.selectedTab
|
||||||
)
|
)
|
||||||
|
.onChange(of: store.selectedTab) { _, newTab in
|
||||||
|
debugInfoSync("🔄 MainView selectedTab changed: \(newTab)")
|
||||||
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.padding(.bottom, 80) // 为底部导航栏留出空间
|
.padding(.bottom, 80) // 为底部导航栏留出空间
|
||||||
|
|
||||||
@@ -92,9 +97,17 @@ struct InternalMainView: View {
|
|||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
BottomTabView(selectedTab: Binding(
|
BottomTabView(selectedTab: Binding(
|
||||||
get: { Tab(rawValue: store.selectedTab.rawValue) ?? .feed },
|
get: {
|
||||||
|
// 将MainFeature.Tab转换为BottomTabView.Tab
|
||||||
|
let currentTab = store.selectedTab == .feed ? Tab.feed : Tab.me
|
||||||
|
debugInfoSync("🔍 BottomTabView get: MainFeature.Tab.\(store.selectedTab) → BottomTabView.Tab.\(currentTab)")
|
||||||
|
return currentTab
|
||||||
|
},
|
||||||
set: { newTab in
|
set: { newTab in
|
||||||
store.send(.selectTab(MainFeature.Tab(rawValue: newTab.rawValue) ?? .feed))
|
// 将BottomTabView.Tab转换为MainFeature.Tab
|
||||||
|
let mainTab: MainFeature.Tab = newTab == .feed ? .feed : .other
|
||||||
|
debugInfoSync("🔍 BottomTabView set: BottomTabView.Tab.\(newTab) → MainFeature.Tab.\(mainTab)")
|
||||||
|
store.send(.selectTab(mainTab))
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -113,22 +126,26 @@ struct MainContentView: View {
|
|||||||
let store: StoreOf<MainFeature>
|
let store: StoreOf<MainFeature>
|
||||||
let selectedTab: MainFeature.Tab
|
let selectedTab: MainFeature.Tab
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
WithPerceptionTracking {
|
||||||
if selectedTab == .feed {
|
let _ = debugInfoSync("📱 MainContentView selectedTab: \(selectedTab)")
|
||||||
FeedListView(store: store.scope(
|
let _ = debugInfoSync(" 与store.selectedTab一致: \(selectedTab == store.selectedTab)")
|
||||||
state: \.feedList,
|
Group {
|
||||||
action: \.feedList
|
if selectedTab == .feed {
|
||||||
))
|
FeedListView(store: store.scope(
|
||||||
} else if selectedTab == .other {
|
state: \.feedList,
|
||||||
MeView(
|
action: \.feedList
|
||||||
store: store.scope(
|
))
|
||||||
state: \.me,
|
} else if selectedTab == .other {
|
||||||
action: \.me
|
MeView(
|
||||||
),
|
store: store.scope(
|
||||||
showCloseButton: false // MainView中不需要关闭按钮
|
state: \.me,
|
||||||
)
|
action: \.me
|
||||||
} else {
|
),
|
||||||
EmptyView()
|
showCloseButton: false // MainView中不需要关闭按钮
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
CustomEmptyView(onRetry: {})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -84,6 +84,7 @@ struct MeView: View {
|
|||||||
debugInfoSync(" 动态数量: \(store.moments.count)")
|
debugInfoSync(" 动态数量: \(store.moments.count)")
|
||||||
debugInfoSync(" 用户信息错误: \(store.userInfoError ?? "nil")")
|
debugInfoSync(" 用户信息错误: \(store.userInfoError ?? "nil")")
|
||||||
debugInfoSync(" 动态错误: \(store.momentsError ?? "nil")")
|
debugInfoSync(" 动态错误: \(store.momentsError ?? "nil")")
|
||||||
|
debugInfoSync(" 显示错误视图: \(store.showErrorView)")
|
||||||
store.send(.onAppear)
|
store.send(.onAppear)
|
||||||
}
|
}
|
||||||
// 新增:图片预览弹窗
|
// 新增:图片预览弹窗
|
||||||
@@ -97,19 +98,19 @@ struct MeView: View {
|
|||||||
get: { store.showDetail },
|
get: { store.showDetail },
|
||||||
set: { _ in store.send(.detailDismissed) }
|
set: { _ in store.send(.detailDismissed) }
|
||||||
)) {
|
)) {
|
||||||
if let selectedMoment = store.selectedMoment {
|
if let selectedMoment = store.selectedMoment {
|
||||||
let detailStore = Store(
|
let detailStore = Store(
|
||||||
initialState: DetailFeature.State(moment: selectedMoment)
|
initialState: DetailFeature.State(moment: selectedMoment)
|
||||||
) {
|
) {
|
||||||
DetailFeature()
|
DetailFeature()
|
||||||
}
|
}
|
||||||
|
|
||||||
DetailView(store: detailStore)
|
DetailView(store: detailStore)
|
||||||
.onChange(of: detailStore.shouldDismiss) { _, shouldDismiss in
|
.onChange(of: detailStore.shouldDismiss) { _, shouldDismiss in
|
||||||
if shouldDismiss {
|
if shouldDismiss {
|
||||||
store.send(.detailDismissed)
|
store.send(.detailDismissed)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,12 +166,12 @@ struct MeView: View {
|
|||||||
.padding(.top, 8)
|
.padding(.top, 8)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
} else if let error = store.userInfoError ?? store.momentsError {
|
} else if let error = store.userInfoError {
|
||||||
VStack(spacing: 12) {
|
VStack(spacing: 12) {
|
||||||
Image(systemName: "exclamationmark.triangle")
|
Image(systemName: "exclamationmark.triangle")
|
||||||
.font(.system(size: 32))
|
.font(.system(size: 32))
|
||||||
.foregroundColor(.orange)
|
.foregroundColor(.orange)
|
||||||
Text("加载失败")
|
Text("用户信息加载失败")
|
||||||
.font(.system(size: 16, weight: .medium))
|
.font(.system(size: 16, weight: .medium))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
Text(error)
|
Text(error)
|
||||||
@@ -179,7 +180,7 @@ struct MeView: View {
|
|||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.padding(.horizontal, 32)
|
.padding(.horizontal, 32)
|
||||||
Button("重试") {
|
Button("重试") {
|
||||||
store.send(.refresh)
|
store.send(.loadUserInfo)
|
||||||
}
|
}
|
||||||
.font(.system(size: 14, weight: .medium))
|
.font(.system(size: 14, weight: .medium))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
@@ -189,6 +190,12 @@ struct MeView: View {
|
|||||||
.cornerRadius(8)
|
.cornerRadius(8)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
} else if store.showErrorView {
|
||||||
|
// 显示错误视图组件
|
||||||
|
EmptyStateView {
|
||||||
|
store.send(.retryMoments)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
} else if store.moments.isEmpty {
|
} else if store.moments.isEmpty {
|
||||||
VStack(spacing: 12) {
|
VStack(spacing: 12) {
|
||||||
Image(systemName: "doc.text")
|
Image(systemName: "doc.text")
|
||||||
@@ -213,6 +220,9 @@ struct MeView: View {
|
|||||||
Text("调试: momentsError = \(store.momentsError ?? "nil")")
|
Text("调试: momentsError = \(store.momentsError ?? "nil")")
|
||||||
.font(.system(size: 12))
|
.font(.system(size: 12))
|
||||||
.foregroundColor(.yellow)
|
.foregroundColor(.yellow)
|
||||||
|
Text("调试: showErrorView = \(store.showErrorView)")
|
||||||
|
.font(.system(size: 12))
|
||||||
|
.foregroundColor(.yellow)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
} else {
|
} else {
|
||||||
|
Reference in New Issue
Block a user