feat: 实现DetailView头像点击功能并优化MeView
- 在DetailView中添加头像点击功能,支持展示非当前用户的主页。 - 更新OptimizedDynamicCardView以支持头像点击回调。 - 修改DetailFeature以管理用户主页显示状态。 - 在MeView中添加关闭按钮支持,优化用户体验。 - 确保其他页面的兼容性,未影响现有功能。
This commit is contained in:
68
issues/DetailView头像点击功能.md
Normal file
68
issues/DetailView头像点击功能.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# DetailView头像点击功能实现
|
||||
|
||||
## 需求分析
|
||||
在DetailView中点击OptimizedDynamicCardView的头像时,如果是非当前用户的动态,则present一个MeView并传入该动态的uid作为displayUID。
|
||||
|
||||
## 实施计划
|
||||
|
||||
### 修改文件
|
||||
1. **OptimizedDynamicCardView.swift**:添加头像点击回调参数
|
||||
2. **DetailFeature.swift**:添加显示用户主页的状态管理
|
||||
3. **DetailView.swift**:添加MeView的present逻辑
|
||||
4. **MeView.swift**:更新OptimizedDynamicCardView调用,添加关闭按钮支持
|
||||
5. **FeedListView.swift**:更新OptimizedDynamicCardView调用
|
||||
6. **MainView.swift**:更新MeView调用
|
||||
|
||||
### 核心功能设计
|
||||
1. **OptimizedDynamicCardView**:
|
||||
- 添加`onAvatarTap: (() -> Void)?`参数
|
||||
- 在头像上添加点击手势
|
||||
- 移除头像的`allowsHitTesting(false)`
|
||||
|
||||
2. **DetailFeature**:
|
||||
- 添加`showUserProfile: Bool`状态
|
||||
- 添加`targetUserId: Int`状态
|
||||
- 添加`showUserProfile(Int)`和`hideUserProfile` Action
|
||||
|
||||
3. **DetailView**:
|
||||
- 在OptimizedDynamicCardView中添加头像点击回调
|
||||
- 判断是否为当前用户动态
|
||||
- 使用sheet替代fullScreenCover,支持下拉关闭
|
||||
- 添加presentationDetents和presentationDragIndicator
|
||||
|
||||
4. **MeView**:
|
||||
- 添加`showCloseButton: Bool`参数
|
||||
- 在present时显示关闭按钮
|
||||
- 在MainView中不显示关闭按钮
|
||||
|
||||
### 实施步骤
|
||||
1. ✅ 修改OptimizedDynamicCardView添加头像点击回调
|
||||
2. ✅ 修改DetailFeature添加用户主页状态管理
|
||||
3. ✅ 修改DetailView添加MeView present逻辑
|
||||
4. ✅ 更新其他使用OptimizedDynamicCardView的地方
|
||||
5. ✅ 改进present方式,使用sheet替代fullScreenCover
|
||||
6. ✅ 添加MeView关闭按钮支持
|
||||
|
||||
### 功能特点
|
||||
- **智能判断**:只有点击非当前用户的头像才会显示用户主页
|
||||
- **复用MeView**:利用之前实现的displayUID功能
|
||||
- **用户体验**:使用sheet支持下拉关闭,更符合iOS设计规范
|
||||
- **关闭按钮**:在present时提供明确的关闭方式
|
||||
- **向后兼容**:其他页面的OptimizedDynamicCardView不受影响
|
||||
|
||||
## 完成状态
|
||||
- [x] OptimizedDynamicCardView头像点击功能
|
||||
- [x] DetailFeature状态管理
|
||||
- [x] DetailView MeView present逻辑
|
||||
- [x] 其他页面兼容性更新
|
||||
- [x] 改进present方式(sheet替代fullScreenCover)
|
||||
- [x] MeView关闭按钮支持
|
||||
|
||||
## 测试要点
|
||||
1. 在DetailView中点击当前用户头像,不触发任何操作
|
||||
2. 在DetailView中点击其他用户头像,正确显示该用户的主页
|
||||
3. 用户主页支持下拉关闭
|
||||
4. 用户主页显示关闭按钮,点击可关闭
|
||||
5. MainView中的MeView不显示关闭按钮
|
||||
6. 其他页面的OptimizedDynamicCardView正常工作
|
||||
7. MeView正确显示指定用户的信息
|
@@ -17,10 +17,11 @@
|
||||
|
||||
### 核心组件设计
|
||||
1. **UserIDDisplay组件**:
|
||||
- 参数:uid (Int), fontSize (CGFloat), textColor (Color)
|
||||
- 功能:显示"ID: xxx",右侧复制图标,点击复制ID
|
||||
- 参数:uid (Int), fontSize (CGFloat), textColor (Color), isDisplayCopy (Bool)
|
||||
- 功能:显示"ID: xxx",可选的复制图标,点击复制ID
|
||||
- 样式:数字不使用逗号分割
|
||||
- 反馈:点击后显示"已复制"提示
|
||||
- 配置:isDisplayCopy控制是否显示复制图标和启用复制功能
|
||||
|
||||
2. **头像样式调整**:
|
||||
- 尺寸:130x130
|
||||
@@ -44,11 +45,12 @@
|
||||
- [x] OptimizedDynamicCardView组件更新
|
||||
- [x] 复制功能实现
|
||||
- [x] 视觉反馈实现
|
||||
- [x] 复制图标显示控制功能
|
||||
|
||||
## 测试要点
|
||||
1. 头像尺寸和边框显示正确
|
||||
2. ID显示格式正确(无逗号分割)
|
||||
3. 复制图标显示正确
|
||||
3. 复制图标显示控制正确(MeView显示,其他页面不显示)
|
||||
4. 点击复制功能正常
|
||||
5. 复制成功反馈显示
|
||||
6. 组件在不同场景下复用正常
|
@@ -22,6 +22,10 @@ struct DetailFeature {
|
||||
// 新增:是否需要关闭DetailView
|
||||
var shouldDismiss = false
|
||||
|
||||
// 新增:显示用户主页相关状态
|
||||
var showUserProfile = false
|
||||
var targetUserId: Int = 0
|
||||
|
||||
init(moment: MomentsInfo) {
|
||||
self.moment = moment
|
||||
}
|
||||
@@ -41,6 +45,10 @@ struct DetailFeature {
|
||||
// 新增:当前用户ID相关actions
|
||||
case loadCurrentUserId
|
||||
case currentUserIdLoaded(String?)
|
||||
|
||||
// 新增:用户主页相关actions
|
||||
case showUserProfile(Int)
|
||||
case hideUserProfile
|
||||
}
|
||||
|
||||
var body: some ReducerOf<Self> {
|
||||
@@ -190,6 +198,15 @@ struct DetailFeature {
|
||||
debugInfoSync("🔍 DetailFeature: 请求关闭DetailView")
|
||||
state.shouldDismiss = true
|
||||
return .none
|
||||
|
||||
case let .showUserProfile(userId):
|
||||
state.targetUserId = userId
|
||||
state.showUserProfile = true
|
||||
return .none
|
||||
|
||||
case .hideUserProfile:
|
||||
state.showUserProfile = false
|
||||
return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@ struct MainFeature {
|
||||
struct State: Equatable {
|
||||
var selectedTab: Tab = .feed
|
||||
var feedList: FeedListFeature.State = .init()
|
||||
var me: MeFeature.State = .init()
|
||||
var me: MeFeature.State
|
||||
var accountModel: AccountModel? = nil
|
||||
// 新增:导航路径和设置页面 State
|
||||
var navigationPath: [Destination] = []
|
||||
@@ -20,8 +20,10 @@ struct MainFeature {
|
||||
// 新增:登出标志
|
||||
var isLoggedOut: Bool = false
|
||||
|
||||
init() {
|
||||
// 默认初始化
|
||||
init(accountModel: AccountModel? = nil) {
|
||||
self.accountModel = accountModel
|
||||
let uid = accountModel?.uid.flatMap { Int($0) } ?? 0
|
||||
self.me = MeFeature.State(displayUID: uid > 0 ? uid : nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,9 +66,9 @@ struct MainFeature {
|
||||
state.selectedTab = tab
|
||||
state.navigationPath = []
|
||||
if tab == .other, let uidStr = state.accountModel?.uid, let uid = Int(uidStr), uid > 0 {
|
||||
if state.me.uid != uid {
|
||||
state.me.uid = uid
|
||||
state.me.isFirstLoad = true // 仅当用户切换时才重置首次加载
|
||||
if state.me.displayUID != uid {
|
||||
state.me.displayUID = uid
|
||||
state.me.isFirstLoad = true
|
||||
}
|
||||
return .send(.me(.onAppear))
|
||||
}
|
||||
@@ -86,8 +88,8 @@ struct MainFeature {
|
||||
state.accountModel = accountModel
|
||||
// 如果当前选中的是 MeView 标签页,且有有效的 uid,则触发数据加载
|
||||
if state.selectedTab == .other, let uidStr = accountModel?.uid, let uid = Int(uidStr), uid > 0 {
|
||||
if state.me.uid != uid {
|
||||
state.me.uid = uid
|
||||
if state.me.displayUID != uid {
|
||||
state.me.displayUID = uid
|
||||
state.me.isFirstLoad = true
|
||||
}
|
||||
return .send(.me(.onAppear))
|
||||
|
@@ -19,12 +19,24 @@ struct MeFeature {
|
||||
var page: Int = 1
|
||||
var pageSize: Int = 20
|
||||
var uid: Int = 0
|
||||
// 新增:显示指定用户ID,如果为nil则显示当前登录用户
|
||||
var displayUID: Int?
|
||||
// 新增:DetailView相关状态
|
||||
var showDetail: Bool = false
|
||||
var selectedMoment: MomentsInfo?
|
||||
|
||||
init() {
|
||||
// 默认初始化
|
||||
init(displayUID: Int? = nil) {
|
||||
self.displayUID = displayUID
|
||||
}
|
||||
|
||||
// 获取实际要显示的用户ID
|
||||
var effectiveUID: Int {
|
||||
return displayUID ?? uid
|
||||
}
|
||||
|
||||
// 判断是否显示其他用户
|
||||
var isDisplayingOtherUser: Bool {
|
||||
return displayUID != nil && displayUID != uid
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,18 +60,18 @@ struct MeFeature {
|
||||
state.isFirstLoad = false
|
||||
return .send(.refresh)
|
||||
case .refresh:
|
||||
guard state.uid > 0 else { return .none }
|
||||
guard state.effectiveUID > 0 else { return .none }
|
||||
state.isRefreshing = true
|
||||
state.page = 1
|
||||
state.hasMore = true
|
||||
return .merge(
|
||||
fetchUserInfo(uid: state.uid),
|
||||
fetchMoments(uid: state.uid, page: 1, pageSize: state.pageSize)
|
||||
fetchUserInfo(uid: state.effectiveUID),
|
||||
fetchMoments(uid: state.effectiveUID, page: 1, pageSize: state.pageSize)
|
||||
)
|
||||
case .loadMore:
|
||||
guard state.uid > 0, state.hasMore, !state.isLoadingMore else { return .none }
|
||||
guard state.effectiveUID > 0, state.hasMore, !state.isLoadingMore else { return .none }
|
||||
state.isLoadingMore = true
|
||||
return fetchMoments(uid: state.uid, page: state.page + 1, pageSize: state.pageSize)
|
||||
return fetchMoments(uid: state.effectiveUID, page: state.page + 1, pageSize: state.pageSize)
|
||||
case let .userInfoResponse(result):
|
||||
state.isLoadingUserInfo = false
|
||||
state.isRefreshing = false
|
||||
@@ -106,15 +118,11 @@ struct MeFeature {
|
||||
|
||||
private func fetchUserInfo(uid: Int) -> Effect<Action> {
|
||||
.run { send in
|
||||
// do {
|
||||
if let userInfo = await UserInfoManager.fetchUserInfoFromServer(uid: String(uid), apiService: apiService) {
|
||||
await send(.userInfoResponse(.success(userInfo)))
|
||||
} else {
|
||||
await send(.userInfoResponse(.failure(.noData)))
|
||||
}
|
||||
// } catch {
|
||||
// await send(.userInfoResponse(.failure(error as? APIError ?? .unknown(error.localizedDescription))))
|
||||
// }
|
||||
if let userInfo = await UserInfoManager.fetchUserInfoFromServer(uid: String(uid), apiService: apiService) {
|
||||
await send(.userInfoResponse(.success(userInfo)))
|
||||
} else {
|
||||
await send(.userInfoResponse(.failure(.noData)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -13,18 +13,21 @@ struct OptimizedDynamicCardView: View {
|
||||
let onLikeTap: (_ dynamicId: Int, _ uid: Int, _ likedUid: Int, _ worldId: Int) -> Void
|
||||
// 新增:卡片点击回调
|
||||
let onCardTap: (() -> Void)?
|
||||
// 新增:头像点击回调
|
||||
let onAvatarTap: (() -> Void)?
|
||||
// 新增:详情页模式,点击卡片不跳转
|
||||
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, onCardTap: (() -> Void)? = nil, isDetailMode: Bool = false, isLikeLoading: 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, onCardTap: (() -> Void)? = nil, onAvatarTap: (() -> Void)? = nil, isDetailMode: Bool = false, isLikeLoading: Bool = false) {
|
||||
self.moment = moment
|
||||
self.allMoments = allMoments
|
||||
self.currentIndex = currentIndex
|
||||
self.onImageTap = onImageTap
|
||||
self.onLikeTap = onLikeTap
|
||||
self.onCardTap = onCardTap
|
||||
self.onAvatarTap = onAvatarTap
|
||||
self.isDetailMode = isDetailMode
|
||||
self.isLikeLoading = isLikeLoading
|
||||
}
|
||||
@@ -62,7 +65,11 @@ struct OptimizedDynamicCardView: View {
|
||||
}
|
||||
.frame(width: 40, height: 40)
|
||||
.clipShape(Circle())
|
||||
.allowsHitTesting(false) // 不拦截点击事件
|
||||
.onTapGesture {
|
||||
if let onAvatarTap = onAvatarTap {
|
||||
onAvatarTap()
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(moment.nick)
|
||||
|
@@ -4,13 +4,15 @@ struct UserIDDisplay: View {
|
||||
let uid: Int
|
||||
let fontSize: CGFloat
|
||||
let textColor: Color
|
||||
let isDisplayCopy: Bool
|
||||
|
||||
@State private var showCopiedFeedback: Bool = false
|
||||
|
||||
init(uid: Int, fontSize: CGFloat = 14, textColor: Color = .white.opacity(0.7)) {
|
||||
init(uid: Int, fontSize: CGFloat = 14, textColor: Color = .white.opacity(0.7), isDisplayCopy: Bool = false) {
|
||||
self.uid = uid
|
||||
self.fontSize = fontSize
|
||||
self.textColor = textColor
|
||||
self.isDisplayCopy = isDisplayCopy
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -19,17 +21,21 @@ struct UserIDDisplay: View {
|
||||
.font(.system(size: fontSize))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Image("icon_copy")
|
||||
.resizable()
|
||||
.frame(width: 14, height: 14)
|
||||
.foregroundColor(textColor)
|
||||
if isDisplayCopy {
|
||||
Image("icon_copy")
|
||||
.resizable()
|
||||
.frame(width: 14, height: 14)
|
||||
.foregroundColor(textColor)
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
copyToClipboard()
|
||||
if isDisplayCopy {
|
||||
copyToClipboard()
|
||||
}
|
||||
}
|
||||
.overlay(
|
||||
Group {
|
||||
if showCopiedFeedback {
|
||||
if isDisplayCopy && showCopiedFeedback {
|
||||
Text("已复制")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.white)
|
||||
@@ -61,8 +67,8 @@ struct UserIDDisplay: View {
|
||||
|
||||
#Preview {
|
||||
VStack(spacing: 20) {
|
||||
UserIDDisplay(uid: 123456789)
|
||||
UserIDDisplay(uid: 987654321, fontSize: 16, textColor: .black)
|
||||
UserIDDisplay(uid: 123456789, isDisplayCopy: true)
|
||||
UserIDDisplay(uid: 987654321, fontSize: 16, textColor: .black, isDisplayCopy: false)
|
||||
}
|
||||
.padding()
|
||||
}
|
@@ -51,6 +51,12 @@ struct DetailView: View {
|
||||
store.send(.likeDynamic(dynamicId, uid, likedUid, worldId))
|
||||
},
|
||||
onCardTap: nil, // 详情页不需要卡片点击
|
||||
onAvatarTap: {
|
||||
// 如果点击的是非当前用户的头像,则显示用户主页
|
||||
if !isCurrentUserDynamic {
|
||||
store.send(.showUserProfile(store.moment.uid))
|
||||
}
|
||||
},
|
||||
isDetailMode: true, // 详情页模式,点击卡片不跳转
|
||||
isLikeLoading: store.isLikeLoading
|
||||
)
|
||||
@@ -91,7 +97,23 @@ struct DetailView: View {
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: Binding(
|
||||
get: { store.showUserProfile },
|
||||
set: { _ in store.send(.hideUserProfile) }
|
||||
)) {
|
||||
WithPerceptionTracking {
|
||||
let meStore = Store(
|
||||
initialState: MeFeature.State(displayUID: store.targetUserId)
|
||||
) {
|
||||
MeFeature()
|
||||
}
|
||||
|
||||
MeView(store: meStore, showCloseButton: true)
|
||||
.presentationDetents([.large])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否为当前用户的动态
|
||||
|
@@ -96,6 +96,7 @@ struct MomentCardView: View {
|
||||
onImageTap: onImageTap,
|
||||
onLikeTap: onLikeTap,
|
||||
onCardTap: onTap,
|
||||
onAvatarTap: nil, // FeedListView中暂时不需要头像点击功能
|
||||
isDetailMode: false,
|
||||
isLikeLoading: isLikeLoading
|
||||
)
|
||||
|
@@ -124,7 +124,8 @@ struct MainContentView: View {
|
||||
store: store.scope(
|
||||
state: \.me,
|
||||
action: \.me
|
||||
)
|
||||
),
|
||||
showCloseButton: false // MainView中不需要关闭按钮
|
||||
)
|
||||
} else {
|
||||
EmptyView()
|
||||
|
@@ -6,6 +6,15 @@ struct MeView: View {
|
||||
// 新增:图片预览状态
|
||||
@State private var previewItem: PreviewItem? = nil
|
||||
@State private var previewCurrentIndex: Int = 0
|
||||
// 新增:是否显示关闭按钮
|
||||
let showCloseButton: Bool
|
||||
// 新增:dismiss环境变量
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
init(store: StoreOf<MeFeature>, showCloseButton: Bool = false) {
|
||||
self.store = store
|
||||
self.showCloseButton = showCloseButton
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
WithPerceptionTracking {
|
||||
@@ -22,16 +31,35 @@ struct MeView: View {
|
||||
// 顶部栏,右上角设置按钮
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(action: {
|
||||
store.send(.settingButtonTapped)
|
||||
}) {
|
||||
Image(systemName: "gearshape")
|
||||
.font(.system(size: 33, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
// 新增:关闭按钮(仅在present时显示)
|
||||
if showCloseButton {
|
||||
Button(action: {
|
||||
dismiss()
|
||||
}) {
|
||||
Image(systemName: "xmark")
|
||||
.font(.system(size: 20, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.frame(width: 44, height: 44)
|
||||
.background(Color.black.opacity(0.3))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.padding(.leading, 16)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if !store.isDisplayingOtherUser {
|
||||
Button(action: {
|
||||
store.send(.settingButtonTapped)
|
||||
}) {
|
||||
Image(systemName: "gearshape")
|
||||
.font(.system(size: 33, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.padding(.trailing, 16)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
.padding(.trailing, 16)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
@@ -111,7 +139,7 @@ struct MeView: View {
|
||||
.foregroundColor(.white)
|
||||
|
||||
// 用户ID
|
||||
UserIDDisplay(uid: store.userInfo?.uid ?? 0)
|
||||
UserIDDisplay(uid: store.userInfo?.uid ?? 0, isDisplayCopy: true)
|
||||
}
|
||||
.padding(.horizontal, 32)
|
||||
}
|
||||
@@ -184,7 +212,8 @@ struct MeView: View {
|
||||
},
|
||||
onCardTap: {
|
||||
store.send(.showDetail(moment))
|
||||
}
|
||||
},
|
||||
onAvatarTap: nil // MeView中不需要头像点击功能
|
||||
)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
|
Reference in New Issue
Block a user