diff --git a/issues/DetailView头像点击功能.md b/issues/DetailView头像点击功能.md new file mode 100644 index 0000000..be4271b --- /dev/null +++ b/issues/DetailView头像点击功能.md @@ -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正确显示指定用户的信息 \ No newline at end of file diff --git a/issues/MeView头像和ID显示优化.md b/issues/MeView头像和ID显示优化.md index dd4f755..8eb6018 100644 --- a/issues/MeView头像和ID显示优化.md +++ b/issues/MeView头像和ID显示优化.md @@ -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. 组件在不同场景下复用正常 \ No newline at end of file diff --git a/yana/Features/DetailFeature.swift b/yana/Features/DetailFeature.swift index 3144a0f..b73da3a 100644 --- a/yana/Features/DetailFeature.swift +++ b/yana/Features/DetailFeature.swift @@ -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 { @@ -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 } } } diff --git a/yana/Features/MainFeature.swift b/yana/Features/MainFeature.swift index 65f954e..673ceff 100644 --- a/yana/Features/MainFeature.swift +++ b/yana/Features/MainFeature.swift @@ -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)) diff --git a/yana/Features/MeFeature.swift b/yana/Features/MeFeature.swift index 833b673..1e6f2f0 100644 --- a/yana/Features/MeFeature.swift +++ b/yana/Features/MeFeature.swift @@ -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 { .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))) + } } } diff --git a/yana/Views/Components/OptimizedDynamicCardView.swift b/yana/Views/Components/OptimizedDynamicCardView.swift index 66139b9..bd0b20c 100644 --- a/yana/Views/Components/OptimizedDynamicCardView.swift +++ b/yana/Views/Components/OptimizedDynamicCardView.swift @@ -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) diff --git a/yana/Views/Components/UserIDDisplay.swift b/yana/Views/Components/UserIDDisplay.swift index a386fbe..4c6c832 100644 --- a/yana/Views/Components/UserIDDisplay.swift +++ b/yana/Views/Components/UserIDDisplay.swift @@ -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() } \ No newline at end of file diff --git a/yana/Views/DetailView.swift b/yana/Views/DetailView.swift index 6d1684f..54c1e12 100644 --- a/yana/Views/DetailView.swift +++ b/yana/Views/DetailView.swift @@ -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) + } + } } // 判断是否为当前用户的动态 diff --git a/yana/Views/FeedListView.swift b/yana/Views/FeedListView.swift index b132dbf..ee8a65f 100644 --- a/yana/Views/FeedListView.swift +++ b/yana/Views/FeedListView.swift @@ -96,6 +96,7 @@ struct MomentCardView: View { onImageTap: onImageTap, onLikeTap: onLikeTap, onCardTap: onTap, + onAvatarTap: nil, // FeedListView中暂时不需要头像点击功能 isDetailMode: false, isLikeLoading: isLikeLoading ) diff --git a/yana/Views/MainView.swift b/yana/Views/MainView.swift index 2e50e57..7f6a6a5 100644 --- a/yana/Views/MainView.swift +++ b/yana/Views/MainView.swift @@ -124,7 +124,8 @@ struct MainContentView: View { store: store.scope( state: \.me, action: \.me - ) + ), + showCloseButton: false // MainView中不需要关闭按钮 ) } else { EmptyView() diff --git a/yana/Views/MeView.swift b/yana/Views/MeView.swift index f21a0c7..e410ec7 100644 --- a/yana/Views/MeView.swift +++ b/yana/Views/MeView.swift @@ -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, 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) }