feat: 更新动态请求与详情视图以增强用户交互体验

- 修改LikeDynamicRequest结构体,调整queryParameters和bodyParameters的定义,确保请求参数正确传递。
- 在DetailFeature中新增当前用户ID的加载逻辑,提升动态详情的交互性。
- 更新FeedListFeature以支持点赞功能的状态管理,增强用户体验。
- 在DetailView中实现关闭回调,优化动态详情视图的用户交互。
- 改进OptimizedDynamicCardView以支持点赞按钮的交互逻辑,提升界面友好性。
This commit is contained in:
edwinQQQ
2025-07-28 16:05:11 +08:00
parent de2f05f545
commit e286229f6f
6 changed files with 320 additions and 87 deletions

View File

@@ -319,15 +319,15 @@ struct LikeDynamicRequest: APIRequestProtocol {
self.worldId = worldId
}
var queryParameters: [String: String]? { nil }
var bodyParameters: [String: Any]? { nil }
var bodyParameters: [String: Any]? {
var queryParameters: [String: String]? {
return [
"dynamicId": dynamicId,
"uid": uid,
"status": status,
"likedUid": likedUid,
"worldId": worldId
"dynamicId": String(dynamicId),
"uid": String(uid),
"status": String(status),
"likedUid": String(likedUid),
"worldId": String(worldId)
]
}

View File

@@ -4,6 +4,7 @@ import ComposableArchitecture
@Reducer
struct DetailFeature {
@Dependency(\.apiService) var apiService
@Dependency(\.isPresented) var isPresented
@ObservableState
struct State: Equatable {
@@ -14,6 +15,13 @@ struct DetailFeature {
var selectedImageIndex = 0
var selectedImages: [String] = []
// ID
var currentUserId: String?
var isLoadingCurrentUserId = false
// DetailView
var shouldDismiss = false
init(moment: MomentsInfo) {
self.moment = moment
}
@@ -29,12 +37,35 @@ struct DetailFeature {
case hideImagePreview
case imagePreviewDismissed
case onLikeSuccess(Int, Bool) // dynamicId, newLikeState
case dismissView
// IDactions
case loadCurrentUserId
case currentUserIdLoaded(String?)
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .onAppear:
// ID
if state.currentUserId == nil && !state.isLoadingCurrentUserId {
return .send(.loadCurrentUserId)
}
return .none
case .loadCurrentUserId:
state.isLoadingCurrentUserId = true
return .run { send in
let userId = await UserInfoManager.getCurrentUserId()
debugInfoSync("🔍 DetailFeature: 获取当前用户ID - \(userId ?? "nil")")
await send(.currentUserIdLoaded(userId))
}
case let .currentUserIdLoaded(userId):
state.currentUserId = userId
state.isLoadingCurrentUserId = false
debugInfoSync("✅ DetailFeature: 当前用户ID已加载 - \(userId ?? "nil")")
return .none
case let .likeDynamic(dynamicId, uid, likedUid, worldId):
@@ -118,8 +149,9 @@ struct DetailFeature {
case let .deleteResponse(.success(response)):
state.isDeleteLoading = false
debugInfoSync("✅ DetailFeature: 动态删除成功")
//
return .none
return .send(.dismissView)
case let .deleteResponse(.failure(error)):
state.isDeleteLoading = false
@@ -139,6 +171,11 @@ struct DetailFeature {
case .imagePreviewDismissed:
state.showImagePreview = false
return .none
case .dismissView:
debugInfoSync("🔍 DetailFeature: 请求关闭DetailView")
state.shouldDismiss = true
return .none
}
}
}

View File

@@ -21,6 +21,8 @@ struct FeedListFeature {
// DetailView
var showDetail: Bool = false
var selectedMoment: MomentsInfo?
//
var likeLoadingDynamicIds: Set<Int> = []
}
enum Action: Equatable {
@@ -37,6 +39,9 @@ struct FeedListFeature {
// DetailViewAction
case showDetail(MomentsInfo)
case detailDismissed
// Action
case likeDynamic(Int, Int, Int, Int) // dynamicId, uid, likedUid, worldId
case likeResponse(TaskResult<LikeDynamicResponse>)
// Action
}
@@ -143,6 +148,94 @@ struct FeedListFeature {
state.showDetail = false
state.selectedMoment = nil
return .none
case let .likeDynamic(dynamicId, uid, likedUid, worldId):
//
guard let index = state.moments.firstIndex(where: { $0.dynamicId == dynamicId }) else {
return .none
}
// loading
state.likeLoadingDynamicIds.insert(dynamicId)
//
let currentMoment = state.moments[index]
let status = currentMoment.isLike ? 0 : 1 // 0: , 1:
let request = LikeDynamicRequest(
dynamicId: dynamicId,
uid: uid,
status: status,
likedUid: likedUid,
worldId: worldId
)
return .run { [apiService] send in
let result = await TaskResult {
try await apiService.request(request)
}
await send(.likeResponse(result))
}
case let .likeResponse(.success(response)):
// loading
if let data = response.data, let success = data.success, success {
//
// loadingdynamicId
let loadingDynamicIds = state.likeLoadingDynamicIds
for dynamicId in loadingDynamicIds {
if let index = state.moments.firstIndex(where: { $0.dynamicId == dynamicId }) {
let currentMoment = state.moments[index]
let newLikeState = !currentMoment.isLike
let newLikeCount = data.likeCount ?? currentMoment.likeCount
//
let updatedMoment = MomentsInfo(
dynamicId: currentMoment.dynamicId,
uid: currentMoment.uid,
nick: currentMoment.nick,
avatar: currentMoment.avatar,
type: currentMoment.type,
content: currentMoment.content,
likeCount: newLikeCount,
isLike: newLikeState,
commentCount: currentMoment.commentCount,
publishTime: currentMoment.publishTime,
worldId: currentMoment.worldId,
status: currentMoment.status,
playCount: currentMoment.playCount,
dynamicResList: currentMoment.dynamicResList,
gender: currentMoment.gender,
squareTop: currentMoment.squareTop,
topicTop: currentMoment.topicTop,
newUser: currentMoment.newUser,
defUser: currentMoment.defUser,
scene: currentMoment.scene,
userVipInfoVO: currentMoment.userVipInfoVO,
headwearPic: currentMoment.headwearPic,
headwearEffect: currentMoment.headwearEffect,
headwearType: currentMoment.headwearType,
headwearName: currentMoment.headwearName,
headwearId: currentMoment.headwearId,
experLevelPic: currentMoment.experLevelPic,
charmLevelPic: currentMoment.charmLevelPic,
isCustomWord: currentMoment.isCustomWord,
labelList: currentMoment.labelList
)
state.moments[index] = updatedMoment
break // 退
}
}
}
// loading
state.likeLoadingDynamicIds.removeAll()
return .none
case let .likeResponse(.failure(error)):
// loading
state.likeLoadingDynamicIds.removeAll()
return .none
}
}
}

View File

@@ -28,14 +28,16 @@ struct OptimizedDynamicCardView: View {
public var body: some View {
ZStack {
//
RoundedRectangle(cornerRadius: 12)
.fill(Color.clear)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color.white.opacity(0.1), lineWidth: 1)
)
.shadow(color: Color(red: 0.43, green: 0.43, blue: 0.43, opacity: 0.34), radius: 10.7, x: 0, y: 1.9)
// -
if !isDetailMode {
RoundedRectangle(cornerRadius: 12)
.fill(Color.clear)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color.white.opacity(0.1), lineWidth: 1)
)
.shadow(color: Color(red: 0.43, green: 0.43, blue: 0.43, opacity: 0.34), radius: 10.7, x: 0, y: 1.9)
}
//
VStack(alignment: .leading, spacing: 12) {
@@ -97,7 +99,7 @@ struct OptimizedDynamicCardView: View {
//
HStack(spacing: 20) {
// Like
// Like
Button(action: {
onLikeTap(moment.dynamicId, moment.uid, moment.uid, moment.worldId)
}) {
@@ -116,18 +118,13 @@ struct OptimizedDynamicCardView: View {
.foregroundColor(moment.isLike ? .red : .white.opacity(0.8))
}
.disabled(isLikeLoading)
.padding(.leading, 40 + 8) // +
Spacer()
}
.padding(.top, 8)
}
.padding(16)
}
.onTapGesture {
//
if !isDetailMode {
//
}
}
.onAppear {
preloadNearbyImages()
}

View File

@@ -3,87 +3,183 @@ import ComposableArchitecture
struct DetailView: View {
@State var store: StoreOf<DetailFeature>
// @Environment(\.dismiss) private var dismiss
let onLikeSuccess: ((Int, Bool) -> Void)?
let onDismiss: (() -> Void)? //
init(store: StoreOf<DetailFeature>, onLikeSuccess: ((Int, Bool) -> Void)? = nil) {
init(store: StoreOf<DetailFeature>, onLikeSuccess: ((Int, Bool) -> Void)? = nil, onDismiss: (() -> Void)? = nil) {
self.store = store
self.onLikeSuccess = onLikeSuccess
self.onDismiss = onDismiss
}
var body: some View {
ScrollView {
VStack(spacing: 0) {
// 使OptimizedDynamicCardView
OptimizedDynamicCardView(
moment: store.moment,
allMoments: [store.moment], //
currentIndex: 0,
onImageTap: { images, index in
store.send(.showImagePreview(images, index))
ZStack {
//
Image("bg")
.resizable()
.aspectRatio(contentMode: .fill)
.ignoresSafeArea()
VStack(spacing: 0) {
//
WithPerceptionTracking {
CustomNavigationBar(
title: NSLocalizedString("detail.title", comment: "Detail page title"),
showDeleteButton: isCurrentUserDynamic,
isDeleteLoading: store.isDeleteLoading,
onBack: {
onDismiss?() //
},
onLikeTap: { dynamicId, uid, likedUid, worldId in
store.send(.likeDynamic(dynamicId, uid, likedUid, worldId))
},
isLikeLoading: store.isLikeLoading,
isDetailMode: true //
)
.padding(.horizontal, 16)
.padding(.top, 16)
}
}
.navigationTitle(NSLocalizedString("detail.title", comment: "Detail page title"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
// uiduid
if isCurrentUserDynamic {
Button(action: {
onDelete: {
store.send(.deleteDynamic)
}) {
if store.isDeleteLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .red))
.scaleEffect(0.8)
} else {
Image(systemName: "trash")
.foregroundColor(.red)
}
}
.disabled(store.isDeleteLoading)
)
}
.padding(.top, 44)
//
ScrollView {
VStack(spacing: 0) {
// 使OptimizedDynamicCardView
WithPerceptionTracking {
OptimizedDynamicCardView(
moment: store.moment,
allMoments: [store.moment], //
currentIndex: 0,
onImageTap: { images, index in
store.send(.showImagePreview(images, index))
},
onLikeTap: { dynamicId, uid, likedUid, worldId in
store.send(.likeDynamic(dynamicId, uid, likedUid, worldId))
},
isLikeLoading: store.isLikeLoading,
isDetailMode: true //
)
.padding(.horizontal, 16)
.padding(.top, 16)
}
}
}
}
}
.navigationBarHidden(true)
.onAppear {
debugInfoSync("🔍 DetailView: onAppear - moment.uid: \(store.moment.uid)")
store.send(.onAppear)
}
// .onReceive(store.publisher(for: \.moment)) { moment in
// //
// }
.onChange(of: store.shouldDismiss) { shouldDismiss in
if shouldDismiss {
debugInfoSync("🔍 DetailView: shouldDismiss = true, 调用onDismiss")
onDismiss?()
}
}
.fullScreenCover(isPresented: Binding(
get: { store.showImagePreview },
set: { _ in store.send(.hideImagePreview) }
)) {
ImagePreviewPager(
images: store.selectedImages,
currentIndex: Binding(
get: { store.selectedImageIndex },
set: { newIndex in
store.send(.showImagePreview(store.selectedImages, newIndex))
WithPerceptionTracking {
ImagePreviewPager(
images: store.selectedImages,
currentIndex: Binding(
get: { store.selectedImageIndex },
set: { newIndex in
store.send(.showImagePreview(store.selectedImages, newIndex))
}
),
onClose: {
store.send(.imagePreviewDismissed)
}
),
onClose: {
store.send(.imagePreviewDismissed)
}
)
)
}
}
}
//
private var isCurrentUserDynamic: Bool {
// false
// TODO: ID
return false
// 使storeID
guard let currentUserId = store.currentUserId,
let currentUserIdInt = Int(currentUserId) else {
debugInfoSync("🔍 DetailView: 无法获取当前用户ID - currentUserId: \(store.currentUserId ?? "nil")")
return false
}
let isCurrentUser = store.moment.uid == currentUserIdInt
debugInfoSync("🔍 DetailView: 动态用户判断 - moment.uid: \(store.moment.uid), currentUserId: \(currentUserIdInt), isCurrentUser: \(isCurrentUser)")
return isCurrentUser
}
}
// MARK: - CustomNavigationBar
struct CustomNavigationBar: View {
let title: String
let showDeleteButton: Bool
let isDeleteLoading: Bool
let onBack: () -> Void
let onDelete: () -> Void
var body: some View {
HStack {
//
Button(action: onBack) {
Image(systemName: "chevron.left")
.font(.system(size: 20, weight: .medium))
.foregroundColor(.white)
.frame(width: 44, height: 44)
.background(Color.black.opacity(0.3))
.clipShape(Circle())
}
Spacer()
//
Text(title)
.font(.system(size: 18, weight: .semibold))
.foregroundColor(.white)
.shadow(color: .black.opacity(0.5), radius: 2, x: 0, y: 1)
Spacer()
//
WithPerceptionTracking {
if showDeleteButton {
Button(action: onDelete) {
if isDeleteLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.8)
.frame(width: 44, height: 44)
.background(Color.red.opacity(0.8))
.clipShape(Circle())
} else {
Image(systemName: "trash")
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
.frame(width: 44, height: 44)
.background(Color.red.opacity(0.8))
.clipShape(Circle())
}
}
.disabled(isDeleteLoading)
} else {
//
Color.clear
.frame(width: 44, height: 44)
}
}
}
.padding(.horizontal, 16)
.padding(.top, 8)
.padding(.bottom, 12)
.background(
LinearGradient(
gradient: Gradient(colors: [
Color.black.opacity(0.4),
Color.black.opacity(0.2),
Color.clear
]),
startPoint: .top,
endPoint: .bottom
)
)
}
}

View File

@@ -80,10 +80,12 @@ struct MomentCardView: View {
let index: Int
let onImageTap: ([String], Int) -> Void
let onTap: () -> Void
let onLikeTap: (Int, Int, Int, Int) -> Void
let onLoadMore: () -> Void
let isLastItem: Bool
let hasMore: Bool
let isLoadingMore: Bool
let isLikeLoading: Bool
var body: some View {
VStack(spacing: 16) {
@@ -92,7 +94,9 @@ struct MomentCardView: View {
allMoments: allMoments,
currentIndex: index,
onImageTap: onImageTap,
onLikeTap: { _, _,_,_ in }
onLikeTap: onLikeTap,
isLikeLoading: isLikeLoading,
isDetailMode: false
)
.onTapGesture {
onTap()
@@ -117,7 +121,9 @@ struct MomentsListView: View {
let isLoadingMore: Bool
let onImageTap: ([String], Int) -> Void
let onMomentTap: (MomentsInfo) -> Void
let onLikeTap: (Int, Int, Int, Int) -> Void
let onLoadMore: () -> Void
let likeLoadingDynamicIds: Set<Int>
var body: some View {
ScrollView {
@@ -131,10 +137,12 @@ struct MomentsListView: View {
onTap: {
onMomentTap(moment)
},
onLikeTap: onLikeTap,
onLoadMore: onLoadMore,
isLastItem: index == moments.count - 1,
hasMore: hasMore,
isLoadingMore: isLoadingMore
isLoadingMore: isLoadingMore,
isLikeLoading: likeLoadingDynamicIds.contains(moment.dynamicId)
)
}
@@ -182,9 +190,13 @@ struct FeedListContentView: View {
onMomentTap: { moment in
viewStore.send(.showDetail(moment))
},
onLikeTap: { dynamicId, uid, likedUid, worldId in
viewStore.send(.likeDynamic(dynamicId, uid, likedUid, worldId))
},
onLoadMore: {
viewStore.send(.loadMore)
}
},
likeLoadingDynamicIds: viewStore.likeLoadingDynamicIds
)
}
}
@@ -263,12 +275,7 @@ struct FeedListView: View {
// DetailView
.navigationDestination(isPresented: viewStore.binding(
get: \.showDetail,
send: { isPresented in
if !isPresented {
return .detailDismissed
}
return .detailDismissed
}
send: { _ in .detailDismissed }
)) {
if let selectedMoment = viewStore.selectedMoment {
DetailView(
@@ -276,6 +283,9 @@ struct FeedListView: View {
initialState: DetailFeature.State(moment: selectedMoment)
) {
DetailFeature()
},
onDismiss: {
viewStore.send(.detailDismissed)
}
)
}