feat: 实现MomentListItem点赞功能及状态管理
- 在MomentListItem中新增点赞功能,用户点击按钮可触发点赞请求。 - 使用MVVM+Combine架构管理点赞状态,确保UI与状态同步。 - 添加加载状态和错误处理,提升用户体验和交互反馈。 - 更新相关视图以支持新的点赞逻辑,优化代码可读性和维护性。
This commit is contained in:
27
Debug/API response log.txt
Normal file
27
Debug/API response log.txt
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
✅ [API Response] [11:19:32.208] ===================
|
||||||
|
⏱️ Duration: 0.258s
|
||||||
|
📊 Status Code: 200
|
||||||
|
🔗 URL: https://api.epartylive.com/dynamic/like?uid=7&likedUid=563&status=1&worldId=-1&dynamicId=8
|
||||||
|
📏 Data Size: 0 KB
|
||||||
|
📋 Response Headers:
|
||||||
|
Alt-Svc: h3=":443"; ma=2592000, h3-29=":443"; ma=2592000, h3-27=":443"; ma=2592000, h3-Q050=":443"; ma=2592000, h3-Q046=":443"; ma=2592000, h3-Q043=":443"; ma=2592000, h3-Q039=":443"; ma=2592000, quic=":443"; ma=2592000; v="39,43,46"
|
||||||
|
Content-Length: 58
|
||||||
|
Content-Type: application/json
|
||||||
|
Date: Thu, 07 Aug 2025 03:19:34 GMT
|
||||||
|
Server: TencentEdgeOne
|
||||||
|
Vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers
|
||||||
|
eo-cache-status: MISS
|
||||||
|
eo-log-uuid: 6089645366037004798
|
||||||
|
📦 Response Data:
|
||||||
|
{
|
||||||
|
"message" : "success",
|
||||||
|
"timestamp" : 1754536774238,
|
||||||
|
"code" : 200
|
||||||
|
}
|
||||||
|
=====================================
|
||||||
|
🎯 [Decoded Response] [11:19:32.210] Type: LikeDynamicResponse
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
[error] ❌ MomentListItem: 点赞操作失败
|
||||||
|
[error] 动态ID: 8
|
||||||
|
[error] 错误: success
|
51
Debug/debug info.txt
Normal file
51
Debug/debug info.txt
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
warning: (arm64) /Users/edwinqqq/Library/Developer/Xcode/DerivedData/yana-fuvanhpzisxarwhiosnkkltamhjw/Build/Products/Debug-iphoneos/yana.app/yana empty dSYM file detected, dSYM was created with an executable with no debug info.
|
||||||
|
[info] 🔐 Keychain 读取成功: AppLanguage
|
||||||
|
[info] 🔍 Loading items updated: 0 items
|
||||||
|
[info] 🔐 Keychain 读取成功: account_model
|
||||||
|
[info] 🔍 认证检查:认证有效 - uid: 563, ticket: eyJhbGciOi...
|
||||||
|
[info] 🎉 自动登录成功,开始获取用户信息
|
||||||
|
[info] 🔍 认证检查:认证有效 - uid: 563, ticket: eyJhbGciOi...
|
||||||
|
[info] 🔐 Keychain 读取成功: user_info
|
||||||
|
[info] 📱 APP启动:使用现有用户信息缓存
|
||||||
|
[info] ✅ 用户信息获取成功,进入主页
|
||||||
|
[info] 🏗️ MainFeature 初始化
|
||||||
|
[info] accountModel.uid: nil
|
||||||
|
[info] 转换后的uid: 0
|
||||||
|
[info] 🔍 尝试从Keychain获取AccountModel
|
||||||
|
[info] ✅ 从Keychain获取到AccountModel: 563
|
||||||
|
[info] meState.uid: 0
|
||||||
|
[info] meState.displayUID: -1
|
||||||
|
[info] meState.effectiveUID: 0
|
||||||
|
[info] 🔍 BottomTabView get: MainFeature.Tab.feed → BottomTabView.Tab.feed
|
||||||
|
[info] 📱 MainContentView selectedTab: feed
|
||||||
|
[info] 与store.selectedTab一致: true
|
||||||
|
[info] 📱 FeedListContentView 状态:
|
||||||
|
[info] isLoading: false
|
||||||
|
[info] error: nil
|
||||||
|
[info] moments.count: 0
|
||||||
|
[info] hasMore: true
|
||||||
|
[info] 🔍 BottomTabView get: MainFeature.Tab.feed → BottomTabView.Tab.feed
|
||||||
|
[info] 🔍 Loading items updated: 0 items
|
||||||
|
[info] 🚀 MainView onAppear
|
||||||
|
[info] 当前selectedTab: feed
|
||||||
|
[info] 📦 MainFeature: AccountModel已加载
|
||||||
|
[info] uid: 563
|
||||||
|
[info] 🔄 更新MeFeature状态,uid: 563
|
||||||
|
[info] ✅ FeedListFeature: 认证信息已准备好,开始获取动态
|
||||||
|
[info] 🏗️ MainFeature 初始化
|
||||||
|
[info] accountModel.uid: nil
|
||||||
|
[info] 转换后的uid: 0
|
||||||
|
[info] 🔍 尝试从Keychain获取AccountModel
|
||||||
|
[info] meState.uid: 0
|
||||||
|
[info] meState.displayUID: -1
|
||||||
|
[info] meState.effectiveUID: 0
|
||||||
|
[info] ✅ 从Keychain获取到AccountModel: 563
|
||||||
|
[info] 🔍 BottomTabView get: MainFeature.Tab.feed → BottomTabView.Tab.feed
|
||||||
|
[info] 📱 MainContentView selectedTab: feed
|
||||||
|
[info] 与store.selectedTab一致: true
|
||||||
|
[info] 📱 FeedListContentView 状态:
|
||||||
|
[info] isLoading: false
|
||||||
|
[info] error: nil
|
||||||
|
[info] moments.count: 0
|
||||||
|
[info] hasMore: true
|
||||||
|
[info] 🔍 BottomTabView get: MainFeature.Tab.feed → BottomTabView.Tab.feed
|
225
issues/MomentListItem点赞功能实现.md
Normal file
225
issues/MomentListItem点赞功能实现.md
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
# MomentListItem 点赞功能实现 (MVVM+Combine)
|
||||||
|
|
||||||
|
## 需求分析
|
||||||
|
1. 用户可以点击 like 按钮
|
||||||
|
2. 点击 like 按钮时,触发 LikeDynamicRequest 请求
|
||||||
|
3. 当 moment.isLike 为 true 时,请求的 status 参数传 0(取消点赞)
|
||||||
|
4. 当 moment.isLike 为 false 时,请求的 status 参数传 1(点赞)
|
||||||
|
5. 请求成功后,更新 MomentListItem 的 like 状态
|
||||||
|
|
||||||
|
## 架构选择
|
||||||
|
**使用 MVVM+Combine 架构**,参考 MomentListHomeViewModel 的实现模式:
|
||||||
|
- 不使用 TCA 框架
|
||||||
|
- 使用 @State 管理本地状态
|
||||||
|
- 使用 LiveAPIService 直接发起 API 请求
|
||||||
|
- 使用 Task 和 async/await 处理异步操作
|
||||||
|
|
||||||
|
## 实施计划
|
||||||
|
|
||||||
|
### 文件结构
|
||||||
|
- ✅ 修改:`yana/MVVM/View/MomentListItem.swift`
|
||||||
|
|
||||||
|
### 核心组件设计
|
||||||
|
1. **状态管理**:
|
||||||
|
- `@State private var isLikeLoading = false` - 点赞加载状态
|
||||||
|
- `@State private var localIsLike: Bool` - 本地点赞状态
|
||||||
|
- `@State private var localLikeCount: Int` - 本地点赞数量
|
||||||
|
|
||||||
|
2. **API 请求**:
|
||||||
|
- 使用 `LiveAPIService()` 直接创建服务实例
|
||||||
|
- 使用 `UserInfoManager.getCurrentUserId()` 获取当前用户ID
|
||||||
|
- 使用 `LikeDynamicRequest` 创建请求
|
||||||
|
|
||||||
|
3. **点赞处理逻辑**:
|
||||||
|
- `handleLikeTap()` - 处理点赞按钮点击
|
||||||
|
- `performLikeRequest()` - 执行点赞 API 请求
|
||||||
|
|
||||||
|
### 实施步骤
|
||||||
|
1. ✅ 移除 TCA 相关导入和依赖
|
||||||
|
2. ✅ 添加 @State 状态变量
|
||||||
|
3. ✅ 实现点赞按钮的点击处理
|
||||||
|
4. ✅ 实现 API 请求逻辑(参考 MomentListHomeViewModel)
|
||||||
|
5. ✅ 更新 UI 显示状态
|
||||||
|
6. ✅ 添加错误处理和加载状态
|
||||||
|
|
||||||
|
### 技术要点
|
||||||
|
- 使用 `LiveAPIService()` 直接创建服务实例
|
||||||
|
- 使用 `UserInfoManager.getCurrentUserId()` 获取当前用户ID
|
||||||
|
- 使用 `APILoadingManager` 显示错误信息
|
||||||
|
- 使用 `debugInfoSync` 和 `debugErrorSync` 记录日志
|
||||||
|
- 使用 `MainActor.run` 确保 UI 更新在主线程
|
||||||
|
|
||||||
|
## 实现细节
|
||||||
|
|
||||||
|
### 状态初始化
|
||||||
|
```swift
|
||||||
|
init(moment: MomentsInfo, onImageTap: @escaping (([String], Int)) -> Void = { (arg) in let (_, _) = arg; }) {
|
||||||
|
self.moment = moment
|
||||||
|
self.onImageTap = onImageTap
|
||||||
|
// 初始化本地状态
|
||||||
|
self._localIsLike = State(initialValue: moment.isLike)
|
||||||
|
self._localLikeCount = State(initialValue: moment.likeCount)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 点赞按钮 UI
|
||||||
|
```swift
|
||||||
|
Button(action: {
|
||||||
|
if !isLikeLoading {
|
||||||
|
handleLikeTap()
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
if isLikeLoading {
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(CircularProgressViewStyle(tint: localIsLike ? .red : .white.opacity(0.8)))
|
||||||
|
.scaleEffect(0.8)
|
||||||
|
} else {
|
||||||
|
Image(systemName: localIsLike ? "heart.fill" : "heart")
|
||||||
|
.font(.system(size: 16))
|
||||||
|
}
|
||||||
|
Text("\(localLikeCount)")
|
||||||
|
.font(.system(size: 14))
|
||||||
|
}
|
||||||
|
.foregroundColor(localIsLike ? .red : .white.opacity(0.8))
|
||||||
|
}
|
||||||
|
.disabled(isLikeLoading)
|
||||||
|
```
|
||||||
|
|
||||||
|
### API 请求逻辑
|
||||||
|
```swift
|
||||||
|
private func performLikeRequest() async {
|
||||||
|
// 设置加载状态
|
||||||
|
await MainActor.run {
|
||||||
|
isLikeLoading = true
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
// 获取当前用户ID
|
||||||
|
guard let currentUserId = await UserInfoManager.getCurrentUserId(),
|
||||||
|
let currentUserIdInt = Int(currentUserId) else {
|
||||||
|
await MainActor.run {
|
||||||
|
isLikeLoading = false
|
||||||
|
}
|
||||||
|
setAPILoadingErrorSync(UUID(), errorMessage: "无法获取用户信息,请重新登录")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确定请求参数
|
||||||
|
let status = localIsLike ? 0 : 1 // 0: 取消点赞, 1: 点赞
|
||||||
|
|
||||||
|
// 创建 API 服务实例
|
||||||
|
let apiService = LiveAPIService()
|
||||||
|
|
||||||
|
// 创建请求
|
||||||
|
let request = LikeDynamicRequest(
|
||||||
|
dynamicId: moment.dynamicId,
|
||||||
|
uid: moment.uid,
|
||||||
|
status: status,
|
||||||
|
likedUid: currentUserIdInt,
|
||||||
|
worldId: moment.worldId
|
||||||
|
)
|
||||||
|
|
||||||
|
debugInfoSync("📡 MomentListItem: 发送点赞请求")
|
||||||
|
debugInfoSync(" 动态ID: \(moment.dynamicId)")
|
||||||
|
debugInfoSync(" 当前状态: \(localIsLike)")
|
||||||
|
debugInfoSync(" 请求状态: \(status)")
|
||||||
|
|
||||||
|
// 发起请求
|
||||||
|
let response: LikeDynamicResponse = try await apiService.request(request)
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
isLikeLoading = false
|
||||||
|
|
||||||
|
// 处理响应
|
||||||
|
if let data = response.data, let success = data.success, success {
|
||||||
|
// 更新本地状态
|
||||||
|
localIsLike = !localIsLike
|
||||||
|
localLikeCount = data.likeCount ?? localLikeCount
|
||||||
|
debugInfoSync("✅ MomentListItem: 点赞操作成功")
|
||||||
|
debugInfoSync(" 动态ID: \(moment.dynamicId)")
|
||||||
|
debugInfoSync(" 新状态: \(localIsLike)")
|
||||||
|
debugInfoSync(" 新数量: \(localLikeCount)")
|
||||||
|
} else {
|
||||||
|
// 显示错误信息
|
||||||
|
let errorMessage = response.message.isEmpty ? "点赞失败,请重试" : response.message
|
||||||
|
setAPILoadingErrorSync(UUID(), errorMessage: errorMessage)
|
||||||
|
debugErrorSync("❌ MomentListItem: 点赞操作失败")
|
||||||
|
debugErrorSync(" 动态ID: \(moment.dynamicId)")
|
||||||
|
debugErrorSync(" 错误: \(errorMessage)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
await MainActor.run {
|
||||||
|
isLikeLoading = false
|
||||||
|
}
|
||||||
|
setAPILoadingErrorSync(UUID(), errorMessage: error.localizedDescription)
|
||||||
|
debugErrorSync("❌ MomentListItem: 点赞请求异常")
|
||||||
|
debugErrorSync(" 动态ID: \(moment.dynamicId)")
|
||||||
|
debugErrorSync(" 错误: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 架构对比
|
||||||
|
|
||||||
|
### 与 TCA 架构的区别
|
||||||
|
| 方面 | TCA 架构 | MVVM+Combine 架构 |
|
||||||
|
|------|----------|-------------------|
|
||||||
|
| 依赖注入 | @Dependency(\.apiService) | LiveAPIService() |
|
||||||
|
| 状态管理 | @ObservableState | @State |
|
||||||
|
| 异步处理 | Effect.task | Task + async/await |
|
||||||
|
| 错误处理 | 通过 Effect 处理 | 直接 try-catch |
|
||||||
|
| 复杂度 | 较高 | 较低 |
|
||||||
|
|
||||||
|
### 与 MomentListHomeViewModel 的一致性
|
||||||
|
- ✅ 使用相同的 API 服务创建方式
|
||||||
|
- ✅ 使用相同的错误处理模式
|
||||||
|
- ✅ 使用相同的日志记录方式
|
||||||
|
- ✅ 使用相同的用户验证逻辑
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
### 交互体验
|
||||||
|
- **即时反馈**:点击后立即显示加载状态
|
||||||
|
- **状态切换**:成功后在点赞/取消点赞状态间切换
|
||||||
|
- **数量更新**:实时更新点赞数量显示
|
||||||
|
- **错误处理**:网络错误或服务器错误时显示友好提示
|
||||||
|
|
||||||
|
### 状态管理
|
||||||
|
- **本地状态**:使用 `@State` 管理本地点赞状态,避免影响其他组件
|
||||||
|
- **加载状态**:防止重复点击,提供视觉反馈
|
||||||
|
- **错误恢复**:请求失败时保持原有状态
|
||||||
|
|
||||||
|
### 安全性
|
||||||
|
- **用户验证**:确保用户已登录才能点赞
|
||||||
|
- **参数验证**:正确传递点赞状态参数
|
||||||
|
- **错误边界**:完善的错误处理机制
|
||||||
|
|
||||||
|
## 测试要点
|
||||||
|
1. 点赞状态切换正确(true → false, false → true)
|
||||||
|
2. 点赞数量实时更新
|
||||||
|
3. 加载状态显示正常
|
||||||
|
4. 网络错误处理正确
|
||||||
|
5. 用户未登录时的错误提示
|
||||||
|
6. 重复点击防护
|
||||||
|
7. 与其他组件的状态同步
|
||||||
|
|
||||||
|
## 完成状态
|
||||||
|
- [x] 移除 TCA 相关代码
|
||||||
|
- [x] 实现 MVVM+Combine 架构
|
||||||
|
- [x] 实现状态管理
|
||||||
|
- [x] 实现点赞按钮 UI
|
||||||
|
- [x] 实现 API 请求逻辑
|
||||||
|
- [x] 实现错误处理
|
||||||
|
- [x] 实现加载状态
|
||||||
|
- [x] 添加日志记录
|
||||||
|
- [x] 代码审查和优化
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
1. 本实现使用本地状态管理,不会影响其他使用相同动态数据的组件
|
||||||
|
2. 如果需要全局状态同步,建议在父组件中实现状态管理
|
||||||
|
3. 点赞操作是幂等的,重复请求不会产生副作用
|
||||||
|
4. 错误处理使用全局的 APILoadingManager,确保用户体验一致
|
||||||
|
5. 架构选择符合项目要求,不使用 TCA 框架
|
@@ -40,7 +40,8 @@ struct MomentListHomePage: View {
|
|||||||
.padding(.top, 16)
|
.padding(.top, 16)
|
||||||
|
|
||||||
// 标语
|
// 标语
|
||||||
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."))
|
Text(LocalizedString("feedList.slogan",
|
||||||
|
comment: ""))
|
||||||
.font(.system(size: 16))
|
.font(.system(size: 16))
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
.foregroundColor(.white.opacity(0.9))
|
.foregroundColor(.white.opacity(0.9))
|
||||||
@@ -64,7 +65,8 @@ struct MomentListHomePage: View {
|
|||||||
debugInfoSync(" 图片数量: \(images.count)")
|
debugInfoSync(" 图片数量: \(images.count)")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.padding(.horizontal, 16)
|
.padding(.leading, 16)
|
||||||
|
.padding(.trailing, 32)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
// 当显示倒数第三个项目时,开始加载更多
|
// 当显示倒数第三个项目时,开始加载更多
|
||||||
if index == viewModel.moments.count - 3 {
|
if index == viewModel.moments.count - 3 {
|
||||||
@@ -94,7 +96,7 @@ struct MomentListHomePage: View {
|
|||||||
.padding(.vertical, 20)
|
.padding(.vertical, 20)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.bottom, 100) // 为底部导航栏留出空间
|
.padding(.bottom, 160) // 为底部导航栏留出空间
|
||||||
}
|
}
|
||||||
.refreshable {
|
.refreshable {
|
||||||
// 下拉刷新
|
// 下拉刷新
|
||||||
|
@@ -5,12 +5,20 @@ struct MomentListItem: View {
|
|||||||
let moment: MomentsInfo
|
let moment: MomentsInfo
|
||||||
let onImageTap: (([String], Int)) -> Void // 新增:图片点击回调
|
let onImageTap: (([String], Int)) -> Void // 新增:图片点击回调
|
||||||
|
|
||||||
|
// 新增:点赞相关状态
|
||||||
|
@State private var isLikeLoading = false
|
||||||
|
@State private var localIsLike: Bool
|
||||||
|
@State private var localLikeCount: Int
|
||||||
|
|
||||||
init(
|
init(
|
||||||
moment: MomentsInfo,
|
moment: MomentsInfo,
|
||||||
onImageTap: @escaping (([String], Int)) -> Void = { (arg) in let (_, _) = arg; }
|
onImageTap: @escaping (([String], Int)) -> Void = { (arg) in let (_, _) = arg; }
|
||||||
) {
|
) {
|
||||||
self.moment = moment
|
self.moment = moment
|
||||||
self.onImageTap = onImageTap
|
self.onImageTap = onImageTap
|
||||||
|
// 初始化本地状态
|
||||||
|
self._localIsLike = State(initialValue: moment.isLike)
|
||||||
|
self._localLikeCount = State(initialValue: moment.likeCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -84,13 +92,26 @@ struct MomentListItem: View {
|
|||||||
// 互动按钮
|
// 互动按钮
|
||||||
HStack(spacing: 20) {
|
HStack(spacing: 20) {
|
||||||
// Like 按钮与用户名左侧对齐
|
// Like 按钮与用户名左侧对齐
|
||||||
HStack(spacing: 4) {
|
Button(action: {
|
||||||
Image(systemName: moment.isLike ? "heart.fill" : "heart")
|
if !isLikeLoading {
|
||||||
.font(.system(size: 16))
|
handleLikeTap()
|
||||||
Text("\(moment.likeCount)")
|
}
|
||||||
.font(.system(size: 14))
|
}) {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
if isLikeLoading {
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(CircularProgressViewStyle(tint: localIsLike ? .red : .white.opacity(0.8)))
|
||||||
|
.scaleEffect(0.8)
|
||||||
|
} else {
|
||||||
|
Image(systemName: localIsLike ? "heart.fill" : "heart")
|
||||||
|
.font(.system(size: 16))
|
||||||
|
}
|
||||||
|
Text("\(localLikeCount)")
|
||||||
|
.font(.system(size: 14))
|
||||||
|
}
|
||||||
|
.foregroundColor(localIsLike ? .red : .white.opacity(0.8))
|
||||||
}
|
}
|
||||||
.foregroundColor(moment.isLike ? .red : .white.opacity(0.8))
|
.disabled(isLikeLoading)
|
||||||
.padding(.leading, 40 + 8) // 与用户名左侧对齐(头像宽度 + 间距)
|
.padding(.leading, 40 + 8) // 与用户名左侧对齐(头像宽度 + 间距)
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
@@ -100,6 +121,83 @@ struct MomentListItem: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - 点赞处理逻辑
|
||||||
|
private func handleLikeTap() {
|
||||||
|
Task {
|
||||||
|
await performLikeRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func performLikeRequest() async {
|
||||||
|
// 设置加载状态
|
||||||
|
await MainActor.run {
|
||||||
|
isLikeLoading = true
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
// 获取当前用户ID
|
||||||
|
guard let currentUserId = await UserInfoManager.getCurrentUserId(),
|
||||||
|
let currentUserIdInt = Int(currentUserId) else {
|
||||||
|
await MainActor.run {
|
||||||
|
isLikeLoading = false
|
||||||
|
}
|
||||||
|
setAPILoadingErrorSync(UUID(), errorMessage: "无法获取用户信息,请重新登录")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确定请求参数
|
||||||
|
let status = localIsLike ? 0 : 1 // 0: 取消点赞, 1: 点赞
|
||||||
|
|
||||||
|
// 创建 API 服务实例
|
||||||
|
let apiService = LiveAPIService()
|
||||||
|
|
||||||
|
// 创建请求
|
||||||
|
let request = LikeDynamicRequest(
|
||||||
|
dynamicId: moment.dynamicId,
|
||||||
|
uid: currentUserIdInt,
|
||||||
|
status: status,
|
||||||
|
likedUid: moment.uid,
|
||||||
|
worldId: moment.worldId
|
||||||
|
)
|
||||||
|
|
||||||
|
debugInfoSync("📡 MomentListItem: 发送点赞请求")
|
||||||
|
debugInfoSync(" 动态ID: \(moment.dynamicId)")
|
||||||
|
debugInfoSync(" 当前状态: \(localIsLike)")
|
||||||
|
debugInfoSync(" 请求状态: \(status)")
|
||||||
|
|
||||||
|
// 发起请求
|
||||||
|
let response: LikeDynamicResponse = try await apiService.request(request)
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
isLikeLoading = false
|
||||||
|
// 处理响应, 只需要判断 code
|
||||||
|
if response.code == 200 {
|
||||||
|
localIsLike = !localIsLike
|
||||||
|
localLikeCount = localIsLike ? localLikeCount+1 : localLikeCount-1
|
||||||
|
debugInfoSync("✅ MomentListItem: 点赞操作成功")
|
||||||
|
debugInfoSync(" 动态ID: \(moment.dynamicId)")
|
||||||
|
debugInfoSync(" 新状态: \(localIsLike)")
|
||||||
|
debugInfoSync(" 新数量: \(localLikeCount)")
|
||||||
|
} else {
|
||||||
|
let errorMessage = response.message.isEmpty ? "点赞失败,请重试" : response.message
|
||||||
|
setAPILoadingErrorSync(UUID(), errorMessage: errorMessage)
|
||||||
|
debugErrorSync("❌ MomentListItem: 点赞操作失败")
|
||||||
|
debugErrorSync(" 动态ID: \(moment.dynamicId)")
|
||||||
|
debugErrorSync(" 错误: \(errorMessage)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
await MainActor.run {
|
||||||
|
isLikeLoading = false
|
||||||
|
}
|
||||||
|
setAPILoadingErrorSync(UUID(), errorMessage: error.localizedDescription)
|
||||||
|
debugErrorSync("❌ MomentListItem: 点赞请求异常")
|
||||||
|
debugErrorSync(" 动态ID: \(moment.dynamicId)")
|
||||||
|
debugErrorSync(" 错误: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - 时间显示逻辑
|
// MARK: - 时间显示逻辑
|
||||||
private func formatDisplayTime(_ timestamp: Int) -> String {
|
private func formatDisplayTime(_ timestamp: Int) -> String {
|
||||||
let date = Date(timeIntervalSince1970: TimeInterval(timestamp) / 1000.0)
|
let date = Date(timeIntervalSince1970: TimeInterval(timestamp) / 1000.0)
|
||||||
@@ -251,50 +349,50 @@ struct MomentSquareImageView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
//#Preview {
|
||||||
// 创建测试数据
|
// // 创建测试数据
|
||||||
let testMoment = MomentsInfo(
|
// let testMoment = MomentsInfo(
|
||||||
dynamicId: 1,
|
// dynamicId: 1,
|
||||||
uid: 123456,
|
// uid: 123456,
|
||||||
nick: "测试用户",
|
// nick: "测试用户",
|
||||||
avatar: "",
|
// avatar: "",
|
||||||
type: 0,
|
// type: 0,
|
||||||
content: "这是一条测试动态内容,用来测试 MomentListItem 的显示效果。",
|
// content: "这是一条测试动态内容,用来测试 MomentListItem 的显示效果。",
|
||||||
likeCount: 42,
|
// likeCount: 42,
|
||||||
isLike: false,
|
// isLike: false,
|
||||||
commentCount: 5,
|
// commentCount: 5,
|
||||||
publishTime: Int(Date().timeIntervalSince1970 * 1000),
|
// publishTime: Int(Date().timeIntervalSince1970 * 1000),
|
||||||
worldId: 1,
|
// worldId: 1,
|
||||||
status: 1,
|
// status: 1,
|
||||||
playCount: nil,
|
// playCount: nil,
|
||||||
dynamicResList: [
|
// dynamicResList: [
|
||||||
MomentsPicture(id: 1, resUrl: "https://picsum.photos/300/300", format: "jpg", width: 300, height: 300, resDuration: nil),
|
// MomentsPicture(id: 1, resUrl: "https://picsum.photos/300/300", format: "jpg", width: 300, height: 300, resDuration: nil),
|
||||||
MomentsPicture(id: 2, resUrl: "https://picsum.photos/301/301", format: "jpg", width: 301, height: 301, resDuration: nil)
|
// MomentsPicture(id: 2, resUrl: "https://picsum.photos/301/301", format: "jpg", width: 301, height: 301, resDuration: nil)
|
||||||
],
|
// ],
|
||||||
gender: nil,
|
// gender: nil,
|
||||||
squareTop: nil,
|
// squareTop: nil,
|
||||||
topicTop: nil,
|
// topicTop: nil,
|
||||||
newUser: nil,
|
// newUser: nil,
|
||||||
defUser: nil,
|
// defUser: nil,
|
||||||
scene: nil,
|
// scene: nil,
|
||||||
userVipInfoVO: nil,
|
// userVipInfoVO: nil,
|
||||||
headwearPic: nil,
|
// headwearPic: nil,
|
||||||
headwearEffect: nil,
|
// headwearEffect: nil,
|
||||||
headwearType: nil,
|
// headwearType: nil,
|
||||||
headwearName: nil,
|
// headwearName: nil,
|
||||||
headwearId: nil,
|
// headwearId: nil,
|
||||||
experLevelPic: nil,
|
// experLevelPic: nil,
|
||||||
charmLevelPic: nil,
|
// charmLevelPic: nil,
|
||||||
isCustomWord: nil,
|
// isCustomWord: nil,
|
||||||
labelList: nil
|
// labelList: nil
|
||||||
)
|
// )
|
||||||
|
//
|
||||||
MomentListItem(
|
// MomentListItem(
|
||||||
moment: testMoment,
|
// moment: testMoment,
|
||||||
onImageTap: { images, index in
|
// onImageTap: { images, index in
|
||||||
print("图片被点击: 索引 \(index), 图片数量 \(images.count)")
|
// print("图片被点击: 索引 \(index), 图片数量 \(images.count)")
|
||||||
}
|
// }
|
||||||
)
|
// )
|
||||||
.padding()
|
// .padding()
|
||||||
.background(Color.black)
|
// .background(Color.black)
|
||||||
}
|
//}
|
||||||
|
299
项目问题排查与解决流程.md
299
项目问题排查与解决流程.md
@@ -1,299 +0,0 @@
|
|||||||
# Yana 项目问题排查与解决流程文档
|
|
||||||
|
|
||||||
## 目录
|
|
||||||
|
|
||||||
1. [问题概述](#问题概述)
|
|
||||||
2. [解决流程](#解决流程)
|
|
||||||
3. [技术细节](#技术细节)
|
|
||||||
4. [最终解决方案](#最终解决方案)
|
|
||||||
5. [预防措施](#预防措施)
|
|
||||||
6. [常见问题FAQ](#常见问题faq)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 问题概述
|
|
||||||
|
|
||||||
### 初始错误
|
|
||||||
|
|
||||||
**错误信息**: `"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 定义
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 解决流程
|
|
||||||
|
|
||||||
### 第一阶段:GUID 冲突解决
|
|
||||||
|
|
||||||
#### 步骤 1: 清理缓存
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 清理 Xcode DerivedData
|
|
||||||
rm -rf ~/Library/Developer/Xcode/DerivedData/*
|
|
||||||
|
|
||||||
# 重置 Swift Package Manager
|
|
||||||
swift package reset
|
|
||||||
swift package resolve
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 步骤 2: 重新安装 CocoaPods
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pod install --clean-install
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 步骤 3: 验证项目解析
|
|
||||||
|
|
||||||
```bash
|
|
||||||
xcodebuild -workspace yana.xcworkspace -list
|
|
||||||
```
|
|
||||||
|
|
||||||
### 第二阶段:TCA 结构修复
|
|
||||||
|
|
||||||
#### 问题识别
|
|
||||||
|
|
||||||
- `HomeFeature.State` 缺少 `isSettingPresented` 和 `settingState` 属性
|
|
||||||
- `HomeFeature.Action` 缺少 `settingDismissed` 和 `setting` actions
|
|
||||||
- `HomeView.swift` 中的 `store.scope()` 调用语法错误
|
|
||||||
|
|
||||||
#### 修复步骤
|
|
||||||
|
|
||||||
1. 修复 HomeFeature.swift
|
|
||||||
|
|
||||||
```swift
|
|
||||||
@ObservableState
|
|
||||||
struct State: Equatable {
|
|
||||||
var isInitialized = false
|
|
||||||
var userInfo: UserInfo?
|
|
||||||
var accountModel: AccountModel?
|
|
||||||
var error: String?
|
|
||||||
|
|
||||||
// 添加设置页面相关状态
|
|
||||||
var isSettingPresented = false
|
|
||||||
var settingState = SettingFeature.State()
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Action: Equatable {
|
|
||||||
case onAppear
|
|
||||||
case loadUserInfo
|
|
||||||
case userInfoLoaded(UserInfo?)
|
|
||||||
case loadAccountModel
|
|
||||||
case accountModelLoaded(AccountModel?)
|
|
||||||
case logoutTapped
|
|
||||||
case logout
|
|
||||||
|
|
||||||
// 添加设置页面相关actions
|
|
||||||
case settingDismissed
|
|
||||||
case setting(SettingFeature.Action)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2.添加子 Reducer
|
|
||||||
|
|
||||||
```swift
|
|
||||||
var body: some ReducerOf<Self> {
|
|
||||||
Scope(state: \.settingState, action: \.setting) {
|
|
||||||
SettingFeature()
|
|
||||||
}
|
|
||||||
|
|
||||||
Reduce { state, action in
|
|
||||||
// ... existing cases ...
|
|
||||||
|
|
||||||
case .settingDismissed:
|
|
||||||
state.isSettingPresented = false
|
|
||||||
return .none
|
|
||||||
|
|
||||||
case .setting:
|
|
||||||
// 由子reducer处理
|
|
||||||
return .none
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3.修复 HomeView.swift
|
|
||||||
|
|
||||||
```swift
|
|
||||||
.sheet(isPresented: Binding(
|
|
||||||
get: { store.isSettingPresented },
|
|
||||||
set: { _ in store.send(.settingDismissed) }
|
|
||||||
)) {
|
|
||||||
SettingView(store: store.scope(state: \.settingState, action: \.setting))
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 技术细节
|
|
||||||
|
|
||||||
### 依赖管理配置
|
|
||||||
|
|
||||||
**Swift Package Manager (Package.swift)**:
|
|
||||||
|
|
||||||
- ComposableArchitecture: 1.20.2+
|
|
||||||
- 其他依赖根据需要添加
|
|
||||||
|
|
||||||
**CocoaPods (Podfile)**:
|
|
||||||
|
|
||||||
- Alamofire (网络请求)
|
|
||||||
- SDWebImage (图像加载)
|
|
||||||
- CocoaLumberjack (日志)
|
|
||||||
- 其他 UI 相关库
|
|
||||||
|
|
||||||
### TCA 架构模式
|
|
||||||
|
|
||||||
```
|
|
||||||
Feature
|
|
||||||
├── State (数据状态)
|
|
||||||
├── Action (用户操作)
|
|
||||||
├── Reducer (状态转换逻辑)
|
|
||||||
└── Dependencies (外部依赖)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 文件结构
|
|
||||||
|
|
||||||
```
|
|
||||||
yana/
|
|
||||||
├── Features/ # TCA Feature 定义
|
|
||||||
├── Views/ # SwiftUI 视图
|
|
||||||
├── APIs/ # 网络 API 层
|
|
||||||
├── Utils/ # 工具类
|
|
||||||
└── Managers/ # 管理器类
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 最终解决方案
|
|
||||||
|
|
||||||
### 命令执行顺序
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. 清理环境
|
|
||||||
rm -rf ~/Library/Developer/Xcode/DerivedData/*
|
|
||||||
swift package reset
|
|
||||||
|
|
||||||
# 2. 重新解析依赖
|
|
||||||
swift package resolve
|
|
||||||
pod install --clean-install
|
|
||||||
|
|
||||||
# 3. 验证项目
|
|
||||||
xcodebuild -workspace yana.xcworkspace -scheme yana -configuration Debug build
|
|
||||||
```
|
|
||||||
|
|
||||||
### 关键代码修改
|
|
||||||
|
|
||||||
1. **HomeFeature.swift**: 添加设置相关状态管理
|
|
||||||
2. **HomeView.swift**: 修复 TCA store 绑定语法
|
|
||||||
3. **SettingFeature.swift**: 确保 Action 完整性
|
|
||||||
|
|
||||||
### 构建结果
|
|
||||||
|
|
||||||
✅ **编译成功**: Exit code 0
|
|
||||||
⚠️ **警告信息**: 仅 Swift 6 兼容性警告,不影响运行
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 预防措施
|
|
||||||
|
|
||||||
### 开发规范
|
|
||||||
|
|
||||||
1. **统一包管理**: 优先使用一种包管理工具
|
|
||||||
2. **定期清理**: 定期清理 DerivedData 避免缓存问题
|
|
||||||
3. **代码审查**: 确保 TCA Feature 结构完整
|
|
||||||
4. **版本控制**: 及时提交关键配置文件
|
|
||||||
|
|
||||||
### 监控指标
|
|
||||||
|
|
||||||
- [ ] 项目编译时间 < 30s
|
|
||||||
- [ ] 无编译错误
|
|
||||||
- [ ] 依赖解析正常
|
|
||||||
- [ ] TCA 结构完整
|
|
||||||
|
|
||||||
### 工具使用
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 项目健康检查脚本
|
|
||||||
check_project() {
|
|
||||||
echo "🔍 检查项目状态..."
|
|
||||||
xcodebuild -workspace yana.xcworkspace -list > /dev/null 2>&1
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
echo "✅ 项目解析正常"
|
|
||||||
else
|
|
||||||
echo "❌ 项目解析失败,需要执行清理流程"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 常见问题FAQ
|
|
||||||
|
|
||||||
### Q1: 再次出现 GUID 冲突怎么办?
|
|
||||||
|
|
||||||
**A**: 执行完整清理流程:
|
|
||||||
```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**:
|
|
||||||
|
|
||||||
- 尽量使用单一包管理工具
|
|
||||||
- 如需混合使用,确保依赖版本兼容
|
|
||||||
- 定期更新依赖并测试
|
|
||||||
|
|
||||||
### Q4: Swift 6 兼容性警告如何处理?
|
|
||||||
|
|
||||||
**A**:
|
|
||||||
|
|
||||||
- 短期:可以忽略,不影响功能
|
|
||||||
- 长期:逐步迁移到 Swift 6 Sendable 模式
|
|
||||||
|
|
||||||
### Q5: 项目构建缓慢怎么办?
|
|
||||||
|
|
||||||
**A**:
|
|
||||||
|
|
||||||
- 使用 `xcodebuild -quiet` 减少输出
|
|
||||||
- 开启 Xcode Build System 并行构建
|
|
||||||
- 定期清理 DerivedData
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 总结
|
|
||||||
|
|
||||||
本次问题解决涉及以下关键技术点:
|
|
||||||
|
|
||||||
1. **Xcode 项目配置管理**
|
|
||||||
2. **Swift Package Manager 与 CocoaPods 共存**
|
|
||||||
3. **TCA (The Composable Architecture) 最佳实践**
|
|
||||||
4. **iOS 开发环境故障排除**
|
|
||||||
|
|
||||||
通过系统性的排查和修复,项目现已恢复正常运行状态。建议团队建立定期维护机制,避免类似问题再次发生。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**文档更新时间**: 2025-07-10
|
|
||||||
**适用版本**: iOS 17+, Swift 6, TCA 1.20.2+
|
|
||||||
**维护者**: AI Assistant & 开发团队
|
|
Reference in New Issue
Block a user