diff --git a/yana/Features/MainFeature.swift b/yana/Features/MainFeature.swift index 691f7ee..00b1afd 100644 --- a/yana/Features/MainFeature.swift +++ b/yana/Features/MainFeature.swift @@ -10,7 +10,7 @@ struct MainFeature: Reducer { struct State: Equatable { var selectedTab: Tab = .feed var feedList: FeedListFeature.State = .init() - var meDynamic: MeDynamicFeature.State = .init(uid: 0) + var me: MeFeature.State = .init() var accountModel: AccountModel? = nil } @@ -19,7 +19,7 @@ struct MainFeature: Reducer { case onAppear case selectTab(Tab) case feedList(FeedListFeature.Action) - case meDynamic(MeDynamicFeature.Action) + case me(MeFeature.Action) case accountModelLoaded(AccountModel?) } @@ -27,8 +27,8 @@ struct MainFeature: Reducer { Scope(state: \.feedList, action: \.feedList) { FeedListFeature() } - Scope(state: \.meDynamic, action: \.meDynamic) { - MeDynamicFeature() + Scope(state: \.me, action: \.me) { + MeFeature() } Reduce { state, action in switch action { @@ -40,20 +40,16 @@ struct MainFeature: Reducer { case .selectTab(let tab): state.selectedTab = tab if tab == .other, let uidStr = state.accountModel?.uid, let uid = Int(uidStr), uid > 0 { - state.meDynamic = MeDynamicFeature.State(uid: uid) - return .send(.meDynamic(.onAppear)) + state.me = MeFeature.State(uid: uid) + return .send(.me(.onAppear)) } return .none case .feedList: return .none case let .accountModelLoaded(accountModel): state.accountModel = accountModel - if let uidStr = accountModel?.uid, let uid = Int(uidStr), uid > 0 { - state.meDynamic = MeDynamicFeature.State(uid: uid) - return .send(.meDynamic(.onAppear)) - } return .none - default: + case .me: return .none } } diff --git a/yana/Features/MeFeature.swift b/yana/Features/MeFeature.swift new file mode 100644 index 0000000..c653f0c --- /dev/null +++ b/yana/Features/MeFeature.swift @@ -0,0 +1,115 @@ +import Foundation +import ComposableArchitecture + +@Reducer +struct MeFeature { + @Dependency(\.apiService) var apiService + struct State: Equatable { + var userInfo: UserInfo? + var isLoadingUserInfo: Bool = false + var userInfoError: String? + var moments: [MomentsInfo] = [] + var isLoadingMoments: Bool = false + var momentsError: String? + var hasMore: Bool = true + var isLoadingMore: Bool = false + var isRefreshing: Bool = false + var page: Int = 1 + var pageSize: Int = 20 + var uid: Int = 0 + } + + enum Action: Equatable { + case onAppear + case refresh + case loadMore + case userInfoResponse(Result) + case momentsResponse(Result) + } + + func reduce(into state: inout State, action: Action) -> Effect { + switch action { + case .onAppear: + guard state.uid > 0 else { return .none } + state.isLoadingUserInfo = true + state.isLoadingMoments = true + state.userInfoError = nil + state.momentsError = nil + state.page = 1 + state.hasMore = true + return .merge( + fetchUserInfo(uid: state.uid), + fetchMoments(uid: state.uid, page: 1, pageSize: state.pageSize) + ) + case .refresh: + guard state.uid > 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) + ) + case .loadMore: + guard state.uid > 0, state.hasMore, !state.isLoadingMore else { return .none } + state.isLoadingMore = true + return fetchMoments(uid: state.uid, page: state.page + 1, pageSize: state.pageSize) + case let .userInfoResponse(result): + state.isLoadingUserInfo = false + state.isRefreshing = false + switch result { + case let .success(userInfo): + state.userInfo = userInfo + state.userInfoError = nil + case let .failure(error): + state.userInfoError = error.localizedDescription + } + return .none + case let .momentsResponse(result): + state.isLoadingMoments = false + state.isLoadingMore = false + state.isRefreshing = false + switch result { + case let .success(resp): + let newMoments = resp.data ?? [] + if state.page == 1 { + state.moments = newMoments + } else { + state.moments += newMoments + } + state.hasMore = newMoments.count == state.pageSize + if state.hasMore { state.page += 1 } + state.momentsError = nil + case let .failure(error): + state.momentsError = error.localizedDescription + } + return .none + } + } + + private func fetchUserInfo(uid: Int) -> Effect { + .run { send in + do { + if let userInfo = try 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)))) + } + } + } + + private func fetchMoments(uid: Int, page: Int, pageSize: Int) -> Effect { + .run { send in + do { + let req = GetMyDynamicRequest(fromUid: uid, uid: uid, page: page, pageSize: pageSize) + let resp = try await apiService.request(req) + await send(.momentsResponse(.success(resp))) + } catch { + await send(.momentsResponse(.failure(error as? APIError ?? .unknown(error.localizedDescription)))) + } + } + } +} \ No newline at end of file diff --git a/yana/Views/MainView.swift b/yana/Views/MainView.swift index 22c3085..81ddec8 100644 --- a/yana/Views/MainView.swift +++ b/yana/Views/MainView.swift @@ -26,17 +26,13 @@ struct MainView: View { )) .transition(.opacity) case .other: - if let accountModel = viewStore.accountModel { - MeView( - meDynamicStore: store.scope( - state: \.meDynamic, - action: \.meDynamic - ), - accountModel: accountModel, - onLogout: {} + MeView( + store: store.scope( + state: \.me, + action: \.me ) - .transition(.opacity) - } + ) + .transition(.opacity) } } .frame(maxWidth: .infinity, maxHeight: .infinity) diff --git a/yana/Views/MeDynamicView.swift b/yana/Views/MeDynamicView.swift deleted file mode 100644 index 9794a96..0000000 --- a/yana/Views/MeDynamicView.swift +++ /dev/null @@ -1,75 +0,0 @@ -import SwiftUI -import ComposableArchitecture - -struct MeDynamicView: View { - let store: StoreOf - // uid 由外部传入 - - @State private var showDeleteAlert = false - @State private var selectedMoment: MomentsInfo? - - var body: some View { - WithViewStore(self.store, observe: { $0 }) { viewStore in - Group { - if viewStore.isLoading && viewStore.dynamics.isEmpty { - ProgressView("加载中...") - .frame(maxWidth: .infinity, maxHeight: .infinity) - } else if let error = viewStore.error { - VStack(spacing: 16) { - Image(systemName: "exclamationmark.triangle.fill") - .font(.system(size: 40)) - .foregroundColor(.yellow) - Text(error) - .foregroundColor(.red) - Button("重试") { - viewStore.send(.onAppear) - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - } else if viewStore.dynamics.isEmpty { - VStack(spacing: 16) { - Image(systemName: "tray") - .font(.system(size: 40)) - .foregroundColor(.gray) - Text("暂无动态") - .foregroundColor(.white.opacity(0.8)) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - } else { - ScrollView { - LazyVStack(spacing: 12) { - ForEach(Array(viewStore.dynamics.enumerated()), id: \.element.dynamicId) { index, moment in - OptimizedDynamicCardView(moment: moment, allMoments: viewStore.dynamics, currentIndex: index) - .padding(.horizontal, 12) - .onLongPressGesture { - if viewStore.uid == moment.uid { - selectedMoment = moment - showDeleteAlert = true - } - } - } - if viewStore.hasMore { - ProgressView() - .onAppear { - viewStore.send(.loadMore) - } - } - } - .padding(.top, 8) - } - .refreshable { - viewStore.send(.refresh) - } - } - } - .alert("确认删除该动态?", isPresented: $showDeleteAlert, presenting: selectedMoment) { moment in - Button("删除", role: .destructive) { - // TODO: 后续可在此触发删除Action,如 viewStore.send(.delete(moment.dynamicId)) - } - Button("取消", role: .cancel) {} - } message: { _ in - Text("此操作不可恢复") - } - } - } -} diff --git a/yana/Views/MeView.swift b/yana/Views/MeView.swift index afd36c7..d54e005 100644 --- a/yana/Views/MeView.swift +++ b/yana/Views/MeView.swift @@ -2,232 +2,106 @@ import SwiftUI import ComposableArchitecture struct MeView: View { - let meDynamicStore: StoreOf - let accountModel: AccountModel - @State private var showLogoutConfirmation = false - @State private var showSetting = false - @State private var userInfo: UserInfo? - @State private var isLoadingUserInfo = true - @State private var errorMessage: String? - @State private var hasLoaded = false - - let onLogout: () -> Void + let store: StoreOf var body: some View { - GeometryReader { geometry in - ZStack { - // 背景图片 - 使用现有的"bg"图片 - Image("bg") - .resizable() - .aspectRatio(contentMode: .fill) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .clipped() - .ignoresSafeArea(.all) - - VStack(spacing: 0) { - // 用户信息区域 - 固定位置 - UserProfileSection( - userInfo: userInfo, - isLoading: isLoadingUserInfo, - errorMessage: errorMessage, - onSettingTapped: { showSetting = true } - ) - // 动态内容区域 - MeDynamicView(store: meDynamicStore) + WithViewStore(self.store, observe: { $0 }) { viewStore in + GeometryReader { geometry in + ZStack { + Image("bg") + .resizable() + .aspectRatio(contentMode: .fill) .frame(maxWidth: .infinity, maxHeight: .infinity) - } - } - } - .onAppear { - if !hasLoaded { - loadUserInfo() - hasLoaded = true - } - } - .alert("确认退出", isPresented: $showLogoutConfirmation) { - Button("取消", role: .cancel) { } - Button("退出", role: .destructive) { - Task { await performLogout() } - } - } message: { - Text("确定要退出登录吗?") - } - .sheet(isPresented: $showSetting) { - SettingView(store: Store(initialState: SettingFeature.State()) { SettingFeature() }) - } - } - - // MARK: - 加载用户信息 - private func loadUserInfo() { - Task { - isLoadingUserInfo = true - errorMessage = nil - - debugInfoSync("📱 MeView: 开始加载用户信息") - - // 获取当前用户ID - guard let currentUserId = await UserInfoManager.getCurrentUserId() else { - debugErrorSync("❌ MeView: 无法获取当前用户ID") - await MainActor.run { - errorMessage = "用户未登录" - isLoadingUserInfo = false - } - return - } - - debugInfoSync("📱 MeView: 当前用户ID: \(currentUserId)") - - // 创建APIService实例 - let apiService: APIServiceProtocol = LiveAPIService() - - // 先尝试从本地缓存获取 - if let cachedUserInfo = await UserInfoManager.getUserInfo() { - debugInfoSync("📱 MeView: 使用本地缓存的用户信息") - await MainActor.run { - self.userInfo = cachedUserInfo - self.isLoadingUserInfo = false - } - } - - // 然后从服务器获取最新数据 - debugInfoSync("🌐 MeView: 从服务器获取最新用户信息") - let freshUserInfo = await UserInfoManager.fetchUserInfoFromServer( - uid: currentUserId, - apiService: apiService - ) - - await MainActor.run { - if let freshUserInfo = freshUserInfo { - debugInfoSync("✅ MeView: 成功获取最新用户信息") - debugInfoSync(" 用户名: \(freshUserInfo.nick ?? freshUserInfo.nick ?? "未知")") - debugInfoSync(" 用户ID: \(String(freshUserInfo.uid ?? 0))") - self.userInfo = freshUserInfo - self.errorMessage = nil - } else { - debugErrorSync("❌ MeView: 无法从服务器获取用户信息") - if self.userInfo == nil { - self.errorMessage = "无法获取用户信息" - } - } - self.isLoadingUserInfo = false - } - } - } - - // MARK: - 退出登录方法 - private func performLogout() async { - debugInfoSync("🔓 开始执行退出登录...") - await UserInfoManager.clearAllAuthenticationData() - onLogout() - debugInfoSync("✅ 退出登录完成") - } - - // MARK: - 设置按钮点击 - private func onSettingTapped() { - showSetting = true - } - - // MARK: - 复制用户ID - private func copyUserId() { - if let userId = userInfo?.userId { - UIPasteboard.general.string = userId - debugInfoSync("📋 MeView: 用户ID已复制到剪贴板: \(userId)") - } - } -} - -// MARK: - 用户信息区域组件 -struct UserProfileSection: View { - let userInfo: UserInfo? - let isLoading: Bool - let errorMessage: String? - let onSettingTapped: () -> Void - - var body: some View { - VStack(spacing: 16) { - // 顶部栏:设置按钮 - HStack { - Spacer() - Button(action: onSettingTapped) { - Image(systemName: "gearshape") - .font(.system(size: 22, weight: .regular)) - .foregroundColor(.white) - } - .padding(.trailing, 20) - } - - // 用户头像 - if isLoading { - Circle() - .fill(Color.white.opacity(0.2)) - .frame(width: 130, height: 130) - .overlay( - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - ) - } else { - Circle() - .fill(Color.white.opacity(0.2)) - .frame(width: 130, height: 130) - .overlay( - Group { - if let avatarUrl = userInfo?.avatar, !avatarUrl.isEmpty { - AsyncImage(url: URL(string: avatarUrl)) { image in - image - .resizable() - .aspectRatio(contentMode: .fill) - .clipShape(Circle()) - } placeholder: { - Image(systemName: "person.fill") - .font(.system(size: 40)) - .foregroundColor(.white) + .clipped() + .ignoresSafeArea(.all) + VStack(spacing: 0) { + // 用户信息区域 + if viewStore.isLoadingUserInfo { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .frame(height: 130) + } else if let error = viewStore.userInfoError { + Text(error) + .font(.system(size: 14)) + .foregroundColor(.red) + .frame(height: 130) + } else if let userInfo = viewStore.userInfo { + VStack(spacing: 8) { + if let avatarUrl = userInfo.avatar, !avatarUrl.isEmpty { + AsyncImage(url: URL(string: avatarUrl)) { image in + image.resizable().aspectRatio(contentMode: .fill).clipShape(Circle()) + } placeholder: { + Image(systemName: "person.fill").font(.system(size: 40)).foregroundColor(.white) + } + .frame(width: 90, height: 90) + } else { + Image(systemName: "person.fill").font(.system(size: 40)).foregroundColor(.white) + .frame(width: 90, height: 90) } - } else { - Image(systemName: "person.fill") - .font(.system(size: 40)) + Text(userInfo.nick ?? "用户昵称") + .font(.system(size: 18, weight: .medium)) .foregroundColor(.white) + Text("ID: \(userInfo.uid ?? 0)") + .font(.system(size: 14)) + .foregroundColor(.white.opacity(0.7)) + } + .frame(height: 130) + } else { + Spacer().frame(height: 130) + } + // 动态内容区域 + if viewStore.isLoadingMoments && viewStore.moments.isEmpty { + ProgressView("加载中...") + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else if let error = viewStore.momentsError { + VStack(spacing: 16) { + Image(systemName: "exclamationmark.triangle.fill") + .font(.system(size: 40)) + .foregroundColor(.yellow) + Text(error) + .foregroundColor(.red) + Button("重试") { + viewStore.send(.onAppear) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else if viewStore.moments.isEmpty { + VStack(spacing: 16) { + Image(systemName: "tray") + .font(.system(size: 40)) + .foregroundColor(.gray) + Text("暂无动态") + .foregroundColor(.white.opacity(0.8)) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else { + ScrollView { + LazyVStack(spacing: 12) { + ForEach(Array(viewStore.moments.enumerated()), id: \ .element.dynamicId) { index, moment in + OptimizedDynamicCardView(moment: moment, allMoments: viewStore.moments, currentIndex: index) + .padding(.horizontal, 12) + } + if viewStore.hasMore { + ProgressView() + .onAppear { + viewStore.send(.loadMore) + } + } + } + .padding(.top, 8) + } + .refreshable { + viewStore.send(.refresh) } } - ) - } - - // 用户名称 - if let errorMessage = errorMessage { - Text(errorMessage) - .font(.system(size: 14)) - .foregroundColor(.red) - .multilineTextAlignment(.center) - } else { - Text(userInfo?.nick ?? userInfo?.nick ?? "用户昵称") - .font(.system(size: 18, weight: .medium)) - .foregroundColor(.white) - } - - // 用户ID - HStack(spacing: 8) { - Text("ID: \(userInfo?.uid ?? 0)") - .font(.system(size: 14)) - .foregroundColor(.white.opacity(0.7)) - - if userInfo?.userId != nil { - Button(action: { - // 复制用户ID到剪贴板 - if let userId = userInfo?.userId { - UIPasteboard.general.string = userId - } - }) { - Image(systemName: "doc.on.doc") - .font(.system(size: 12)) - .foregroundColor(.white.opacity(0.6)) + Spacer() } + .frame(maxWidth: .infinity, alignment: .top) } } + .onAppear { + viewStore.send(.onAppear) + } } - .padding(.horizontal, 20) - .padding(.bottom, 20) } } - -//#Preview { -// MeView(onLogout: {}) -//}