feat: 更新动态点赞与加载状态管理以提升用户体验
- 在DetailFeature和FeedListFeature中增强点赞功能的状态管理,确保用户交互流畅。 - 新增API加载效果视图,提升用户在操作过程中的反馈体验。 - 更新视图组件以支持点赞加载状态,优化用户界面交互。 - 改进错误处理逻辑,确保在API请求失败时提供友好的错误提示。
This commit is contained in:
@@ -1,39 +1,48 @@
|
|||||||
---
|
---
|
||||||
description:
|
Description:
|
||||||
globs:
|
globs:
|
||||||
alwaysApply: true
|
alwaysApply: true
|
||||||
---
|
---
|
||||||
# CONTEXT
|
# Background
|
||||||
|
|
||||||
This project based on iOS 16.0+ & SwiftUI & TCA 1.20.2
|
This project is based on iOS 16.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.
|
- Use the latest versions of SwiftUI, Swift(6), and TCA(1.20.2), and be familiar with the latest 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.
|
|
||||||
|
|
||||||
## STYLE
|
- Provide careful, accurate answers that are well-reasoned and well-thought-out.
|
||||||
|
|
||||||
- Keep answers concise and direct, minimizing unnecessary wording.
|
- **Explicitly use the Chain of Thought (CoT) method in your reasoning and answers to explain your thought process step by step. **
|
||||||
- Emphasize code readability over performance optimization.
|
- Follow my instructions and complete the task meticulously.
|
||||||
- Maintain a professional and supportive tone, ensuring clarity of content.
|
|
||||||
|
|
||||||
## RESPONSE FORMAT
|
- Start by outlining your proposed approach with detailed steps or pseudocode.
|
||||||
|
|
||||||
- **Utilize the Chain-of-Thought (CoT) method to reason and respond, explaining your thought process step by step.**
|
- Once you have confirmed your plan, start writing code.
|
||||||
- The reply should include:
|
- After coding is done, no compilation check is required, remind me to check
|
||||||
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:
|
## Style
|
||||||
- Include all necessary imports and properly name key components.
|
|
||||||
- Fully implement all requested features, leaving no to-dos, placeholders, or omissions.
|
- Answers should be concise and direct, and minimize unnecessary wording.
|
||||||
3. **Concise Response**: Minimize unnecessary verbosity, focusing only on essential information.
|
- Emphasize code readability rather than performance optimization.
|
||||||
|
- Maintain a professional and supportive tone to ensure clarity.
|
||||||
- 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.
|
|
||||||
|
## Answer format
|
||||||
|
|
||||||
|
- **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.
|
@@ -36,7 +36,6 @@ struct DetailFeature {
|
|||||||
case showImagePreview([String], Int)
|
case showImagePreview([String], Int)
|
||||||
case hideImagePreview
|
case hideImagePreview
|
||||||
case imagePreviewDismissed
|
case imagePreviewDismissed
|
||||||
case onLikeSuccess(Int, Bool) // dynamicId, newLikeState
|
|
||||||
case dismissView
|
case dismissView
|
||||||
|
|
||||||
// 新增:当前用户ID相关actions
|
// 新增:当前用户ID相关actions
|
||||||
@@ -45,7 +44,9 @@ struct DetailFeature {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some ReducerOf<Self> {
|
var body: some ReducerOf<Self> {
|
||||||
Reduce { state, action in
|
Reduce {
|
||||||
|
state,
|
||||||
|
action in
|
||||||
switch action {
|
switch action {
|
||||||
case .onAppear:
|
case .onAppear:
|
||||||
// 如果还没有获取过当前用户ID,则开始获取
|
// 如果还没有获取过当前用户ID,则开始获取
|
||||||
@@ -69,9 +70,10 @@ struct DetailFeature {
|
|||||||
return .none
|
return .none
|
||||||
|
|
||||||
case let .likeDynamic(dynamicId, uid, likedUid, worldId):
|
case let .likeDynamic(dynamicId, uid, likedUid, worldId):
|
||||||
|
// 设置loading状态
|
||||||
state.isLikeLoading = true
|
state.isLikeLoading = true
|
||||||
|
|
||||||
let status = state.moment.isLike ? 0 : 1
|
let status = state.moment.isLike ? 0 : 1 // 0: 取消点赞, 1: 点赞
|
||||||
let request = LikeDynamicRequest(
|
let request = LikeDynamicRequest(
|
||||||
dynamicId: dynamicId,
|
dynamicId: dynamicId,
|
||||||
uid: uid,
|
uid: uid,
|
||||||
@@ -80,59 +82,71 @@ struct DetailFeature {
|
|||||||
worldId: worldId
|
worldId: worldId
|
||||||
)
|
)
|
||||||
|
|
||||||
return .run { send in
|
return .run { [apiService] send in
|
||||||
let result = await TaskResult {
|
do {
|
||||||
try await apiService.request(request)
|
let response: LikeDynamicResponse = try await apiService.request(request)
|
||||||
|
await send(.likeResponse(.success(response)))
|
||||||
|
} catch {
|
||||||
|
await send(.likeResponse(.failure(error)))
|
||||||
}
|
}
|
||||||
await send(.likeResponse(result))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case let .likeResponse(.success(response)):
|
case let .likeResponse(.success(response)):
|
||||||
state.isLikeLoading = false
|
if let data = response.data, let success = data.success, success {
|
||||||
// 点赞成功,通知父视图更新状态
|
// 根据API响应更新点赞状态
|
||||||
return .send(.onLikeSuccess(state.moment.dynamicId, !state.moment.isLike))
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
case let .onLikeSuccess(dynamicId, newLikeState):
|
// 移除loading状态
|
||||||
// 更新本地动态的点赞状态
|
state.isLikeLoading = false
|
||||||
// 由于MomentsInfo的isLike是let,我们需要重新创建moment对象
|
|
||||||
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: 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
|
|
||||||
return .none
|
return .none
|
||||||
|
|
||||||
case let .likeResponse(.failure(error)):
|
case let .likeResponse(.failure(error)):
|
||||||
|
// 移除loading状态
|
||||||
state.isLikeLoading = false
|
state.isLikeLoading = false
|
||||||
// 可以在这里处理错误,比如显示错误提示
|
// 通过APILoadingManager显示错误信息
|
||||||
|
setAPILoadingErrorSync(UUID(), errorMessage: error.localizedDescription)
|
||||||
return .none
|
return .none
|
||||||
|
|
||||||
case .deleteDynamic:
|
case .deleteDynamic:
|
||||||
@@ -179,4 +193,4 @@ struct DetailFeature {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -41,7 +41,7 @@ struct FeedListFeature {
|
|||||||
case detailDismissed
|
case detailDismissed
|
||||||
// 新增:点赞相关Action
|
// 新增:点赞相关Action
|
||||||
case likeDynamic(Int, Int, Int, Int) // dynamicId, uid, likedUid, worldId
|
case likeDynamic(Int, Int, Int, Int) // dynamicId, uid, likedUid, worldId
|
||||||
case likeResponse(TaskResult<LikeDynamicResponse>)
|
case likeResponse(TaskResult<LikeDynamicResponse>, dynamicId: Int)
|
||||||
// 预留后续 Action
|
// 预留后续 Action
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,15 +149,17 @@ struct FeedListFeature {
|
|||||||
state.selectedMoment = nil
|
state.selectedMoment = nil
|
||||||
return .none
|
return .none
|
||||||
case let .likeDynamic(dynamicId, uid, likedUid, worldId):
|
case let .likeDynamic(dynamicId, uid, likedUid, worldId):
|
||||||
// 找到对应的动态并更新点赞状态
|
|
||||||
guard let index = state.moments.firstIndex(where: { $0.dynamicId == dynamicId }) else {
|
|
||||||
return .none
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加loading状态
|
// 添加loading状态
|
||||||
state.likeLoadingDynamicIds.insert(dynamicId)
|
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 currentMoment = state.moments[index]
|
||||||
let status = currentMoment.isLike ? 0 : 1 // 0: 取消点赞, 1: 点赞
|
let status = currentMoment.isLike ? 0 : 1 // 0: 取消点赞, 1: 点赞
|
||||||
|
|
||||||
@@ -170,71 +172,74 @@ struct FeedListFeature {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return .run { [apiService] send in
|
return .run { [apiService] send in
|
||||||
let result = await TaskResult {
|
do {
|
||||||
try await apiService.request(request)
|
let response: LikeDynamicResponse = try await apiService.request(request)
|
||||||
|
await send(.likeResponse(.success(response), dynamicId: dynamicId))
|
||||||
|
} catch {
|
||||||
|
await send(.likeResponse(.failure(error), dynamicId: dynamicId))
|
||||||
}
|
}
|
||||||
await send(.likeResponse(result))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case let .likeResponse(.success(response)):
|
case let .likeResponse(.success(response), dynamicId):
|
||||||
// 移除loading状态
|
|
||||||
if let data = response.data, let success = data.success, success {
|
if let data = response.data, let success = data.success, success {
|
||||||
// 找到对应的动态并更新点赞状态
|
// 根据API响应更新点赞状态
|
||||||
// 注意:这里我们需要从loading状态中找到对应的dynamicId
|
if let index = state.moments.firstIndex(where: { $0.dynamicId == dynamicId }) {
|
||||||
let loadingDynamicIds = state.likeLoadingDynamicIds
|
// 根据当前状态推断新的点赞状态
|
||||||
for dynamicId in loadingDynamicIds {
|
let currentMoment = state.moments[index]
|
||||||
if let index = state.moments.firstIndex(where: { $0.dynamicId == dynamicId }) {
|
let newLikeState = !currentMoment.isLike // 切换点赞状态
|
||||||
let currentMoment = state.moments[index]
|
|
||||||
let newLikeState = !currentMoment.isLike
|
// 创建更新后的动态对象
|
||||||
let newLikeCount = data.likeCount ?? currentMoment.likeCount
|
let updatedMoment = MomentsInfo(
|
||||||
|
dynamicId: currentMoment.dynamicId,
|
||||||
// 创建更新后的动态对象
|
uid: currentMoment.uid,
|
||||||
let updatedMoment = MomentsInfo(
|
nick: currentMoment.nick,
|
||||||
dynamicId: currentMoment.dynamicId,
|
avatar: currentMoment.avatar,
|
||||||
uid: currentMoment.uid,
|
type: currentMoment.type,
|
||||||
nick: currentMoment.nick,
|
content: currentMoment.content,
|
||||||
avatar: currentMoment.avatar,
|
likeCount: data.likeCount ?? currentMoment.likeCount,
|
||||||
type: currentMoment.type,
|
isLike: newLikeState,
|
||||||
content: currentMoment.content,
|
commentCount: currentMoment.commentCount,
|
||||||
likeCount: newLikeCount,
|
publishTime: currentMoment.publishTime,
|
||||||
isLike: newLikeState,
|
worldId: currentMoment.worldId,
|
||||||
commentCount: currentMoment.commentCount,
|
status: currentMoment.status,
|
||||||
publishTime: currentMoment.publishTime,
|
playCount: currentMoment.playCount,
|
||||||
worldId: currentMoment.worldId,
|
dynamicResList: currentMoment.dynamicResList,
|
||||||
status: currentMoment.status,
|
gender: currentMoment.gender,
|
||||||
playCount: currentMoment.playCount,
|
squareTop: currentMoment.squareTop,
|
||||||
dynamicResList: currentMoment.dynamicResList,
|
topicTop: currentMoment.topicTop,
|
||||||
gender: currentMoment.gender,
|
newUser: currentMoment.newUser,
|
||||||
squareTop: currentMoment.squareTop,
|
defUser: currentMoment.defUser,
|
||||||
topicTop: currentMoment.topicTop,
|
scene: currentMoment.scene,
|
||||||
newUser: currentMoment.newUser,
|
userVipInfoVO: currentMoment.userVipInfoVO,
|
||||||
defUser: currentMoment.defUser,
|
headwearPic: currentMoment.headwearPic,
|
||||||
scene: currentMoment.scene,
|
headwearEffect: currentMoment.headwearEffect,
|
||||||
userVipInfoVO: currentMoment.userVipInfoVO,
|
headwearType: currentMoment.headwearType,
|
||||||
headwearPic: currentMoment.headwearPic,
|
headwearName: currentMoment.headwearName,
|
||||||
headwearEffect: currentMoment.headwearEffect,
|
headwearId: currentMoment.headwearId,
|
||||||
headwearType: currentMoment.headwearType,
|
experLevelPic: currentMoment.experLevelPic,
|
||||||
headwearName: currentMoment.headwearName,
|
charmLevelPic: currentMoment.charmLevelPic,
|
||||||
headwearId: currentMoment.headwearId,
|
isCustomWord: currentMoment.isCustomWord,
|
||||||
experLevelPic: currentMoment.experLevelPic,
|
labelList: currentMoment.labelList
|
||||||
charmLevelPic: currentMoment.charmLevelPic,
|
)
|
||||||
isCustomWord: currentMoment.isCustomWord,
|
state.moments[index] = updatedMoment
|
||||||
labelList: currentMoment.labelList
|
|
||||||
)
|
|
||||||
|
|
||||||
state.moments[index] = updatedMoment
|
|
||||||
break // 找到并更新后退出循环
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// 移除loading状态
|
||||||
|
state.likeLoadingDynamicIds.removeAll()
|
||||||
|
} else {
|
||||||
|
// API返回失败,通过APILoadingManager显示错误信息
|
||||||
|
let errorMessage = response.message.isEmpty ? "点赞失败,请重试" : response.message
|
||||||
|
setAPILoadingErrorSync(UUID(), errorMessage: errorMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移除所有loading状态
|
// 移除所有loading状态
|
||||||
state.likeLoadingDynamicIds.removeAll()
|
state.likeLoadingDynamicIds.removeAll()
|
||||||
return .none
|
return .none
|
||||||
|
|
||||||
case let .likeResponse(.failure(error)):
|
case let .likeResponse(.failure(error), dynamicId):
|
||||||
// 移除loading状态
|
// 移除loading状态
|
||||||
state.likeLoadingDynamicIds.removeAll()
|
state.likeLoadingDynamicIds.removeAll()
|
||||||
|
// 通过APILoadingManager显示错误信息
|
||||||
|
setAPILoadingErrorSync(UUID(), errorMessage: error.localizedDescription)
|
||||||
return .none
|
return .none
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -137,91 +137,91 @@ private struct SimpleErrorView: View {
|
|||||||
|
|
||||||
// MARK: - Preview
|
// MARK: - Preview
|
||||||
|
|
||||||
#if DEBUG
|
//#if DEBUG
|
||||||
struct APILoadingEffectView_Previews: PreviewProvider {
|
//struct APILoadingEffectView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
// static var previews: some View {
|
||||||
ZStack {
|
// ZStack {
|
||||||
// 模拟背景
|
// // 模拟背景
|
||||||
Rectangle()
|
// Rectangle()
|
||||||
.fill(Color.blue.opacity(0.3))
|
// .fill(Color.blue.opacity(0.3))
|
||||||
.ignoresSafeArea()
|
// .ignoresSafeArea()
|
||||||
|
//
|
||||||
VStack(spacing: 20) {
|
// VStack(spacing: 20) {
|
||||||
Text("背景内容")
|
// Text("背景内容")
|
||||||
.font(.title)
|
// .font(.title)
|
||||||
|
//
|
||||||
Button("测试按钮") {
|
// Button("测试按钮") {
|
||||||
debugInfoSync("按钮被点击了!")
|
// debugInfoSync("按钮被点击了!")
|
||||||
}
|
// }
|
||||||
.padding()
|
// .padding()
|
||||||
.background(Color.blue)
|
// .background(Color.blue)
|
||||||
.foregroundColor(.white)
|
// .foregroundColor(.white)
|
||||||
.cornerRadius(8)
|
// .cornerRadius(8)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// Loading Effect View
|
// // Loading Effect View
|
||||||
APILoadingEffectView()
|
// APILoadingEffectView()
|
||||||
}
|
// }
|
||||||
.previewDisplayName("API Loading Effect")
|
// .previewDisplayName("API Loading Effect")
|
||||||
.onAppear {
|
// .onAppear {
|
||||||
// 模拟不同状态的预览
|
// // 模拟不同状态的预览
|
||||||
Task {
|
// Task {
|
||||||
let manager = APILoadingManager.shared
|
// let manager = APILoadingManager.shared
|
||||||
|
//
|
||||||
// 添加 loading
|
// // 添加 loading
|
||||||
let id1 = manager.startLoading()
|
// let id1 = manager.startLoading()
|
||||||
|
//
|
||||||
// 2秒后添加错误
|
// // 2秒后添加错误
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
// DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||||
Task {
|
// Task {
|
||||||
manager.setError(id1, errorMessage: "网络连接失败,请检查网络设置")
|
// manager.setError(id1, errorMessage: "网络连接失败,请检查网络设置")
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
// MARK: - Preview Helpers
|
//// MARK: - Preview Helpers
|
||||||
|
//
|
||||||
/// 预览用的测试状态
|
///// 预览用的测试状态
|
||||||
private struct PreviewStateModifier: ViewModifier {
|
//private struct PreviewStateModifier: ViewModifier {
|
||||||
let showLoading: Bool
|
// let showLoading: Bool
|
||||||
let showError: Bool
|
// let showError: Bool
|
||||||
let errorMessage: String
|
// let errorMessage: String
|
||||||
|
//
|
||||||
func body(content: Content) -> some View {
|
// func body(content: Content) -> some View {
|
||||||
content
|
// content
|
||||||
.onAppear {
|
// .onAppear {
|
||||||
Task {
|
// Task {
|
||||||
let manager = APILoadingManager.shared
|
// let manager = APILoadingManager.shared
|
||||||
|
//
|
||||||
if showLoading {
|
// if showLoading {
|
||||||
let _ = manager.startLoading()
|
// let _ = manager.startLoading()
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if showError {
|
// if showError {
|
||||||
let id = manager.startLoading()
|
// let id = manager.startLoading()
|
||||||
try? await Task.sleep(nanoseconds: 1_000_000_000) // 1秒
|
// try? await Task.sleep(nanoseconds: 1_000_000_000) // 1秒
|
||||||
manager.setError(id, errorMessage: errorMessage)
|
// manager.setError(id, errorMessage: errorMessage)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
extension View {
|
//extension View {
|
||||||
/// 添加预览状态
|
// /// 添加预览状态
|
||||||
func previewLoadingState(
|
// func previewLoadingState(
|
||||||
showLoading: Bool = false,
|
// showLoading: Bool = false,
|
||||||
showError: Bool = false,
|
// showError: Bool = false,
|
||||||
errorMessage: String = "示例错误信息"
|
// errorMessage: String = "示例错误信息"
|
||||||
) -> some View {
|
// ) -> some View {
|
||||||
self.modifier(PreviewStateModifier(
|
// self.modifier(PreviewStateModifier(
|
||||||
showLoading: showLoading,
|
// showLoading: showLoading,
|
||||||
showError: showError,
|
// showError: showError,
|
||||||
errorMessage: errorMessage
|
// errorMessage: errorMessage
|
||||||
))
|
// ))
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
#endif
|
//#endif
|
||||||
|
@@ -134,4 +134,18 @@ extension APILoadingManager {
|
|||||||
return .failure(error)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,19 +11,19 @@ struct OptimizedDynamicCardView: View {
|
|||||||
let onImageTap: (_ images: [String], _ index: Int) -> Void
|
let onImageTap: (_ images: [String], _ index: Int) -> Void
|
||||||
// 新增:点赞回调
|
// 新增:点赞回调
|
||||||
let onLikeTap: (_ dynamicId: Int, _ uid: Int, _ likedUid: Int, _ worldId: Int) -> Void
|
let onLikeTap: (_ dynamicId: Int, _ uid: Int, _ likedUid: Int, _ worldId: Int) -> Void
|
||||||
// 新增:点赞loading状态
|
|
||||||
let isLikeLoading: Bool
|
|
||||||
// 新增:详情页模式,点击卡片不跳转
|
// 新增:详情页模式,点击卡片不跳转
|
||||||
let isDetailMode: Bool
|
let isDetailMode: Bool
|
||||||
|
// 新增:点赞loading状态
|
||||||
|
let isLikeLoading: Bool
|
||||||
|
|
||||||
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, isLikeLoading: Bool = false, isDetailMode: Bool = false) {
|
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, isDetailMode: Bool = false, isLikeLoading: Bool = false) {
|
||||||
self.moment = moment
|
self.moment = moment
|
||||||
self.allMoments = allMoments
|
self.allMoments = allMoments
|
||||||
self.currentIndex = currentIndex
|
self.currentIndex = currentIndex
|
||||||
self.onImageTap = onImageTap
|
self.onImageTap = onImageTap
|
||||||
self.onLikeTap = onLikeTap
|
self.onLikeTap = onLikeTap
|
||||||
self.isLikeLoading = isLikeLoading
|
|
||||||
self.isDetailMode = isDetailMode
|
self.isDetailMode = isDetailMode
|
||||||
|
self.isLikeLoading = isLikeLoading
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
@@ -101,7 +101,9 @@ struct OptimizedDynamicCardView: View {
|
|||||||
HStack(spacing: 20) {
|
HStack(spacing: 20) {
|
||||||
// Like 按钮与用户名左侧对齐
|
// Like 按钮与用户名左侧对齐
|
||||||
Button(action: {
|
Button(action: {
|
||||||
onLikeTap(moment.dynamicId, moment.uid, moment.uid, moment.worldId)
|
if !isLikeLoading {
|
||||||
|
onLikeTap(moment.dynamicId, moment.uid, moment.uid, moment.worldId)
|
||||||
|
}
|
||||||
}) {
|
}) {
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
if isLikeLoading {
|
if isLikeLoading {
|
||||||
|
@@ -52,8 +52,8 @@ struct DetailView: View {
|
|||||||
onLikeTap: { dynamicId, uid, likedUid, worldId in
|
onLikeTap: { dynamicId, uid, likedUid, worldId in
|
||||||
store.send(.likeDynamic(dynamicId, uid, likedUid, worldId))
|
store.send(.likeDynamic(dynamicId, uid, likedUid, worldId))
|
||||||
},
|
},
|
||||||
isLikeLoading: store.isLikeLoading,
|
isDetailMode: true, // 详情页模式,点击卡片不跳转
|
||||||
isDetailMode: true // 详情页模式,点击卡片不跳转
|
isLikeLoading: store.isLikeLoading
|
||||||
)
|
)
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.padding(.top, 16)
|
.padding(.top, 16)
|
||||||
|
@@ -254,6 +254,9 @@ private struct LoginContentView: View {
|
|||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加API Loading和错误处理视图
|
||||||
|
APILoadingEffectView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -95,8 +95,8 @@ struct MomentCardView: View {
|
|||||||
currentIndex: index,
|
currentIndex: index,
|
||||||
onImageTap: onImageTap,
|
onImageTap: onImageTap,
|
||||||
onLikeTap: onLikeTap,
|
onLikeTap: onLikeTap,
|
||||||
isLikeLoading: isLikeLoading,
|
isDetailMode: false,
|
||||||
isDetailMode: false
|
isLikeLoading: isLikeLoading
|
||||||
)
|
)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
onTap()
|
onTap()
|
||||||
@@ -244,6 +244,9 @@ struct FeedListView: View {
|
|||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .top)
|
.frame(maxWidth: .infinity, alignment: .top)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加API Loading和错误处理视图
|
||||||
|
APILoadingEffectView()
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewStore.send(.onAppear)
|
viewStore.send(.onAppear)
|
||||||
|
@@ -112,77 +112,75 @@ struct IDLoginView: View {
|
|||||||
Button(action: {
|
Button(action: {
|
||||||
isPasswordVisible.toggle()
|
isPasswordVisible.toggle()
|
||||||
}) {
|
}) {
|
||||||
Image(systemName: isPasswordVisible ? "eye.slash" : "eye")
|
Image(systemName: isPasswordVisible ? "eye.slash.fill" : "eye.fill")
|
||||||
.foregroundColor(.white.opacity(0.7))
|
.foregroundColor(.white.opacity(0.6))
|
||||||
.font(.system(size: 18))
|
.font(.system(size: 16))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 24)
|
.padding(.horizontal, 24)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.padding(.horizontal, 32)
|
// 忘记密码按钮
|
||||||
|
HStack {
|
||||||
// Forgot Password 链接
|
Spacer()
|
||||||
HStack {
|
Button(action: {
|
||||||
Spacer()
|
showRecoverPassword = true
|
||||||
Button(action: {
|
}) {
|
||||||
showRecoverPassword = true
|
Text(NSLocalizedString("id_login.forgot_password", comment: ""))
|
||||||
}) {
|
.font(.system(size: 14))
|
||||||
Text(NSLocalizedString("id_login.forgot_password", comment: ""))
|
.foregroundColor(.white.opacity(0.8))
|
||||||
.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)
|
.padding(.horizontal, 8)
|
||||||
}
|
|
||||||
.disabled(store.isLoading || userID.isEmpty || password.isEmpty)
|
// 登录按钮
|
||||||
.opacity(isLoginButtonEnabled ? 1.0 : 0.5) // 透明度50%当条件不满足时
|
Button(action: {
|
||||||
.padding(.horizontal, 32)
|
// 发送登录action时传递本地状态
|
||||||
|
store.send(.loginButtonTapped(userID: userID, password: password))
|
||||||
// 错误信息
|
}) {
|
||||||
if let errorMessage = store.errorMessage {
|
ZStack {
|
||||||
Text(errorMessage)
|
// 渐变背景
|
||||||
.font(.system(size: 14))
|
LinearGradient(
|
||||||
.foregroundColor(.red)
|
colors: [
|
||||||
.padding(.top, 16)
|
Color(red: 0.85, green: 0.37, blue: 1.0), // #D85EFF
|
||||||
.padding(.horizontal, 32)
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
// 添加API Loading和错误处理视图
|
||||||
|
APILoadingEffectView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -99,8 +99,7 @@ struct LoginView: View {
|
|||||||
.onPreferenceChange(ImageHeightPreferenceKey.self) { imageHeight in
|
.onPreferenceChange(ImageHeightPreferenceKey.self) { imageHeight in
|
||||||
topImageHeight = imageHeight
|
topImageHeight = imageHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
// 间距,使登录按钮区域顶部距离"top"图片底部40pt
|
|
||||||
Spacer()
|
Spacer()
|
||||||
.frame(height: 120)
|
.frame(height: 120)
|
||||||
|
|
||||||
@@ -118,6 +117,9 @@ struct LoginView: View {
|
|||||||
.padding(.bottom, 140)
|
.padding(.bottom, 140)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加API Loading和错误处理视图
|
||||||
|
APILoadingEffectView()
|
||||||
|
|
||||||
// 移除旧的 NavigationLink,改用 navigationDestination
|
// 移除旧的 NavigationLink,改用 navigationDestination
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -98,7 +98,9 @@ struct InternalMainView: View {
|
|||||||
send: { MainFeature.Action.selectTab(MainFeature.Tab(rawValue: $0.rawValue) ?? .feed) }
|
send: { MainFeature.Action.selectTab(MainFeature.Tab(rawValue: $0.rawValue) ?? .feed) }
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
.padding(.bottom, geometry.safeAreaInsets.bottom + 60)
|
|
||||||
|
// 添加API Loading和错误处理视图
|
||||||
|
APILoadingEffectView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,43 +6,48 @@ struct SplashView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
WithPerceptionTracking {
|
WithPerceptionTracking {
|
||||||
Group {
|
ZStack {
|
||||||
// 根据导航目标显示不同页面
|
Group {
|
||||||
if let navigationDestination = store.navigationDestination {
|
// 根据导航目标显示不同页面
|
||||||
switch navigationDestination {
|
if let navigationDestination = store.navigationDestination {
|
||||||
case .login:
|
switch navigationDestination {
|
||||||
// 显示登录页面
|
case .login:
|
||||||
LoginView(
|
// 显示登录页面
|
||||||
store: Store(
|
LoginView(
|
||||||
initialState: LoginFeature.State()
|
store: Store(
|
||||||
) {
|
initialState: LoginFeature.State()
|
||||||
LoginFeature()
|
) {
|
||||||
},
|
LoginFeature()
|
||||||
onLoginSuccess: {
|
},
|
||||||
// 登录成功后导航到主页面
|
onLoginSuccess: {
|
||||||
store.send(.navigateToMain)
|
// 登录成功后导航到主页面
|
||||||
}
|
store.send(.navigateToMain)
|
||||||
)
|
}
|
||||||
case .main:
|
)
|
||||||
// 显示主应用页面
|
case .main:
|
||||||
MainView(
|
// 显示主应用页面
|
||||||
store: Store(
|
MainView(
|
||||||
initialState: MainFeature.State()
|
store: Store(
|
||||||
) {
|
initialState: MainFeature.State()
|
||||||
MainFeature()
|
) {
|
||||||
},
|
MainFeature()
|
||||||
onLogout: {
|
},
|
||||||
store.send(.navigateToLogin)
|
onLogout: {
|
||||||
}
|
store.send(.navigateToLogin)
|
||||||
)
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 显示启动画面
|
||||||
|
splashContent
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// 显示启动画面
|
|
||||||
splashContent
|
|
||||||
}
|
}
|
||||||
}
|
.onAppear {
|
||||||
.onAppear {
|
store.send(.onAppear)
|
||||||
store.send(.onAppear)
|
}
|
||||||
|
|
||||||
|
// API Loading 效果视图 - 显示在所有内容之上
|
||||||
|
APILoadingEffectView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user