From ba991598beebc5f2a8337a4332876e13605f0436 Mon Sep 17 00:00:00 2001 From: edwinQQQ Date: Mon, 21 Jul 2025 19:10:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0CreateFeed=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=8F=8A=E7=9B=B8=E5=85=B3=E8=A7=86=E5=9B=BE=E7=BB=84?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在CreateFeedFeature中新增isPresented依赖,确保在适当的上下文中执行视图关闭操作。 - 在FeedFeature中优化状态管理,简化CreateFeedView的呈现逻辑。 - 新增FeedListFeature和MainFeature,整合FeedListView和底部导航功能,提升用户体验。 - 更新HomeView和SplashView以集成MainView,确保应用结构一致性。 - 在多个视图中调整状态管理和导航逻辑,增强可维护性和用户体验。 --- yana/Features/CreateFeedFeature.swift | 8 +- yana/Features/FeedFeature.swift | 110 ++----- yana/Features/FeedListFeature.swift | 50 +++ yana/Features/HomeFeature.swift | 156 +++++----- yana/Features/MainFeature.swift | 35 +++ yana/Views/AppRootView.swift | 9 +- yana/Views/CreateFeedView.swift | 316 ++++++++++--------- yana/Views/EditFeedView.swift | 19 ++ yana/Views/FeedListView.swift | 67 ++++ yana/Views/FeedView.swift | 433 ++++++++++++++------------ yana/Views/HomeView.swift | 95 +++--- yana/Views/MainView.swift | 49 +++ yana/Views/SplashView.swift | 10 +- 13 files changed, 804 insertions(+), 553 deletions(-) create mode 100644 yana/Features/FeedListFeature.swift create mode 100644 yana/Features/MainFeature.swift create mode 100644 yana/Views/EditFeedView.swift create mode 100644 yana/Views/FeedListView.swift create mode 100644 yana/Views/MainView.swift diff --git a/yana/Features/CreateFeedFeature.swift b/yana/Features/CreateFeedFeature.swift index 3197ede..079ed6d 100644 --- a/yana/Features/CreateFeedFeature.swift +++ b/yana/Features/CreateFeedFeature.swift @@ -35,6 +35,7 @@ struct CreateFeedFeature { @Dependency(\.apiService) var apiService @Dependency(\.dismiss) var dismiss + @Dependency(\.isPresented) var isPresented var body: some ReducerOf { Reduce { state, action in @@ -108,6 +109,11 @@ struct CreateFeedFeature { state.errorMessage = nil return .none case .dismissView: + // 检查是否在presentation context中 + guard isPresented else { + // 如果不在presentation context中,不执行dismiss + return .none + } return .run { _ in await dismiss() } @@ -176,4 +182,4 @@ struct PublishDynamicResponse: Codable { struct PublishDynamicData: Codable { let dynamicId: Int let publishTime: Int -} \ No newline at end of file +} diff --git a/yana/Features/FeedFeature.swift b/yana/Features/FeedFeature.swift index 3ad57b8..b68f101 100644 --- a/yana/Features/FeedFeature.swift +++ b/yana/Features/FeedFeature.swift @@ -7,145 +7,103 @@ struct FeedFeature { struct State: Equatable { var moments: [MomentsInfo] = [] var isLoading = false + var isRefreshing = false var hasMoreData = true var error: String? var nextDynamicId: Int = 0 - - // 是否已初始化 - 用于防止重复初始化 - var isInitialized = false - - // CreateFeedView 相关状态 - 简化为布尔值 - var isCreateFeedPresented = false + // CreateFeedView 相关状态 + var createFeedState = CreateFeedFeature.State() } - enum Action { + enum Action: Equatable { case onAppear + case refresh case loadLatestMoments case loadMoreMoments case momentsResponse(TaskResult) case clearError case retryLoad - - // CreateFeedView 相关 Action - 简化为布尔控制 - case showCreateFeed + // CreateFeedView 相关 Action case createFeedCompleted case createFeedDismissed + // CreateFeedFeature 的 action + case createFeed(CreateFeedFeature.Action) } @Dependency(\.apiService) var apiService var body: some ReducerOf { + Scope(state: \.createFeedState, action: \.createFeed) { + CreateFeedFeature() + } + Reduce { state, action in switch action { case .onAppear: - // 只在未初始化时才执行首次加载 - guard !state.isInitialized else { - return .none - } - + guard state.moments.isEmpty && !state.isLoading else { return .none } return .send(.loadLatestMoments) - + case .refresh: + guard !state.isRefreshing else { return .none } + state.isRefreshing = true + state.error = nil + let request = LatestDynamicsRequest(dynamicId: "", pageSize: 20, types: [.text, .picture]) + return .run { send in + await send(.momentsResponse(TaskResult { try await apiService.request(request) })) + } case .loadLatestMoments: - // 添加重复请求防护 - guard !state.isLoading else { - return .none - } - - // 加载最新数据(下拉刷新) + guard !state.isLoading else { return .none } state.isLoading = true state.error = nil - - let request = LatestDynamicsRequest( - dynamicId: "", // 首次加载传空字符串 - pageSize: 20, - types: [.text, .picture] - ) + let request = LatestDynamicsRequest(dynamicId: "", pageSize: 20, types: [.text, .picture]) return .run { send in - await send(.momentsResponse(TaskResult { - try await apiService.request(request) - })) + await send(.momentsResponse(TaskResult { try await apiService.request(request) })) } - case .loadMoreMoments: - // 加载更多数据(分页加载) guard !state.isLoading && state.hasMoreData else { return .none } - state.isLoading = true state.error = nil - - let request = LatestDynamicsRequest( - dynamicId: state.nextDynamicId == 0 ? "" : String(state.nextDynamicId), - pageSize: 20, - types: [.text, .picture] - ) - + let request = LatestDynamicsRequest(dynamicId: state.nextDynamicId == 0 ? "" : String(state.nextDynamicId), pageSize: 20, types: [.text, .picture]) return .run { send in - await send(.momentsResponse(TaskResult { - try await apiService.request(request) - })) + await send(.momentsResponse(TaskResult { try await apiService.request(request) })) } - case let .momentsResponse(.success(response)): state.isLoading = false - - // 设置初始化状态 - if !state.isInitialized { - state.isInitialized = true - } - - // 检查响应状态 + state.isRefreshing = false guard response.code == 200, let data = response.data else { let errorMsg = response.message.isEmpty ? "获取动态失败" : response.message state.error = errorMsg return .none } - - // 判断是刷新还是加载更多 - let isRefresh = state.nextDynamicId == 0 - + let isRefresh = state.nextDynamicId == 0 || state.isRefreshing if isRefresh { - // 刷新:替换所有数据 state.moments = data.dynamicList } else { - // 加载更多:追加到现有数据 state.moments.append(contentsOf: data.dynamicList) } - - // 更新分页状态 state.nextDynamicId = data.nextDynamicId state.hasMoreData = !data.dynamicList.isEmpty - return .none - case let .momentsResponse(.failure(error)): state.isLoading = false + state.isRefreshing = false state.error = error.localizedDescription return .none - case .clearError: state.error = nil return .none - case .retryLoad: - // 重试加载 if state.moments.isEmpty { return .send(.loadLatestMoments) } else { return .send(.loadMoreMoments) } - - // CreateFeedView 相关 Action 处理 - 简化为布尔控制 - case .showCreateFeed: - state.isCreateFeedPresented = true - return .none - case .createFeedCompleted: - // 发布完成后刷新动态列表 - state.isCreateFeedPresented = false - return .send(.loadLatestMoments) - + return .send(.refresh) case .createFeedDismissed: - state.isCreateFeedPresented = false + return .none + case .createFeed(.dismissView): + return .send(.createFeedDismissed) + case .createFeed: return .none } } diff --git a/yana/Features/FeedListFeature.swift b/yana/Features/FeedListFeature.swift new file mode 100644 index 0000000..b25d8a5 --- /dev/null +++ b/yana/Features/FeedListFeature.swift @@ -0,0 +1,50 @@ +import Foundation +import ComposableArchitecture + +struct FeedListFeature: Reducer { + struct State: Equatable { + var feeds: [Feed] = [] // 预留 feed 内容 + var isLoading: Bool = false + var error: String? = nil + var isEditFeedPresented: Bool = false // 新增:控制 EditFeedView 弹窗 + } + + enum Action: Equatable { + case onAppear + case reload + case loadMore + case editFeedButtonTapped // 新增:点击 add 按钮 + case editFeedDismissed // 新增:关闭编辑页 + // 预留后续 Action + } + + func reduce(into state: inout State, action: Action) -> Effect { + switch action { + case .onAppear: + // 预留数据加载逻辑 + return .none + case .reload: + // 预留刷新逻辑 + return .none + case .loadMore: + // 预留分页加载逻辑 + return .none + case .editFeedButtonTapped: + state.isEditFeedPresented = true + return .none + case .editFeedDismissed: + state.isEditFeedPresented = false + return .none + } + } +} + +// Feed 数据模型占位,后续可替换为真实模型 +enum Feed: Equatable, Identifiable { + case placeholder(id: UUID = UUID()) + var id: UUID { + switch self { + case .placeholder(let id): return id + } + } +} \ No newline at end of file diff --git a/yana/Features/HomeFeature.swift b/yana/Features/HomeFeature.swift index 1521851..1cfa4b9 100644 --- a/yana/Features/HomeFeature.swift +++ b/yana/Features/HomeFeature.swift @@ -3,8 +3,12 @@ import ComposableArchitecture @Reducer struct HomeFeature { + enum Route: Equatable { + case createFeed + } + @ObservableState - struct State: Equatable { + struct State: Equatable, Sendable { var isInitialized = false var userInfo: UserInfo? var accountModel: AccountModel? @@ -19,9 +23,13 @@ struct HomeFeature { // 新增:登出状态 var isLoggedOut = false + + // 新增:路由状态 + var route: Route? = nil } - enum Action { + @CasePathable + enum Action: Equatable { case onAppear case loadUserInfo case userInfoLoaded(UserInfo?) @@ -39,85 +47,81 @@ struct HomeFeature { // 新增:登出完成 case logoutCompleted + + // 新增:路由 actions + case showCreateFeed + case createFeedDismissed } - var body: some ReducerOf { - Scope(state: \.settingState, action: \.setting) { - SettingFeature() - } - - // 新增:Feed Scope - Scope(state: \.feedState, action: \.feed) { - FeedFeature() - } - - Reduce { state, action in - switch action { - case .onAppear: - // 只在未初始化时才执行首次加载 - guard !state.isInitialized else { - return .none - } - - state.isInitialized = true - return .concatenate( - .send(.loadUserInfo), - .send(.loadAccountModel) - ) - - case .loadUserInfo: - // 从本地存储加载用户信息 - return .run { send in - let userInfo = await UserInfoManager.getUserInfo() - await send(.userInfoLoaded(userInfo)) - } - - case let .userInfoLoaded(userInfo): - state.userInfo = userInfo - return .none - - case .loadAccountModel: - // 从本地存储加载账户信息 - return .run { send in - let accountModel = await UserInfoManager.getAccountModel() - await send(.accountModelLoaded(accountModel)) - } - - case let .accountModelLoaded(accountModel): - state.accountModel = accountModel - return .none - - case .logoutTapped: - return .send(.logout) - - case .logout: - // 清除所有认证数据并设置登出状态 - return .run { send in - await UserInfoManager.clearAllAuthenticationData() - await send(.logoutCompleted) - } - - case .logoutCompleted: - state.isLoggedOut = true - return .none - - case .settingDismissed: - state.isSettingPresented = false - return .none - - case .setting: - // 由子reducer处理 - return .none - - case .feed(_): - // FeedFeature 的 action 由 Scope 自动处理 - return .none - } - } + var body: some Reducer { +// Reducer.combine([ +// Reducer { state, action in +// switch action { +// case .onAppear: +// guard !state.isInitialized else { +// return Effect.none +// } +// state.isInitialized = true +// return .concatenate( +// .send(.loadUserInfo), +// .send(.loadAccountModel) +// ) +// case .loadUserInfo: +// return .run { send in +// let userInfo = await UserInfoManager.getUserInfo() +// await send(.userInfoLoaded(userInfo)) +// } +// case let .userInfoLoaded(userInfo): +// state.userInfo = userInfo +// return Effect.none +// case .loadAccountModel: +// return .run { send in +// let accountModel = await UserInfoManager.getAccountModel() +// await send(.accountModelLoaded(accountModel)) +// } +// case let .accountModelLoaded(accountModel): +// state.accountModel = accountModel +// return Effect.none +// case .logoutTapped: +// return .send(.logout) +// case .logout: +// return .run { send in +// await UserInfoManager.clearAllAuthenticationData() +// await send(.logoutCompleted) +// } +// case .logoutCompleted: +// state.isLoggedOut = true +// return Effect.none +// case .settingDismissed: +// state.isSettingPresented = false +// return Effect.none +// case .setting: +// return Effect.none +// case .showCreateFeed: +// state.route = .createFeed +// return Effect.none +// case .createFeedDismissed: +// state.route = nil +// return Effect.none +// case .feed: +// return Effect.none +// } +// }, +// Scope( +// state: \State.settingState, +// action: /Action.setting, +// child: SettingFeature() +// ), +// Scope( +// state: \State.feedState, +// action: /Action.feed, +// child: FeedFeature() +// ) +// ]) } } // 移除:未使用的通知名称定义 // extension Notification.Name { // static let homeLogout = Notification.Name("homeLogout") -// } +// } diff --git a/yana/Features/MainFeature.swift b/yana/Features/MainFeature.swift new file mode 100644 index 0000000..bf9a1d3 --- /dev/null +++ b/yana/Features/MainFeature.swift @@ -0,0 +1,35 @@ +import Foundation +import ComposableArchitecture +import CasePaths + +struct MainFeature: Reducer { + enum Tab: Int, Equatable, CaseIterable { + case feed, other + } + + struct State: Equatable { + var selectedTab: Tab = .feed + var feedList: FeedListFeature.State = .init() + } + + @CasePathable + enum Action: Equatable { + case selectTab(Tab) + case feedList(FeedListFeature.Action) + } + + var body: some ReducerOf { + Scope(state: \.feedList, action: \.feedList) { + FeedListFeature() + } + Reduce { state, action in + switch action { + case .selectTab(let tab): + state.selectedTab = tab + return .none + case .feedList: + return .none + } + } + } +} diff --git a/yana/Views/AppRootView.swift b/yana/Views/AppRootView.swift index 912b0ce..d4d24bd 100644 --- a/yana/Views/AppRootView.swift +++ b/yana/Views/AppRootView.swift @@ -6,14 +6,11 @@ struct AppRootView: View { var body: some View { if isLoggedIn { - HomeView( + MainView( store: Store( - initialState: HomeFeature.State() + initialState: MainFeature.State() ) { - HomeFeature() - }, - onLogout: { - isLoggedIn = false + MainFeature() } ) } else { diff --git a/yana/Views/CreateFeedView.swift b/yana/Views/CreateFeedView.swift index 7b0a7b9..2210eed 100644 --- a/yana/Views/CreateFeedView.swift +++ b/yana/Views/CreateFeedView.swift @@ -4,111 +4,105 @@ import PhotosUI struct CreateFeedView: View { let store: StoreOf + @State private var keyboardHeight: CGFloat = 0 var body: some View { NavigationStack { GeometryReader { geometry in - ZStack { - // 背景渐变 - LinearGradient( - gradient: Gradient(colors: [ - Color(red: 0.1, green: 0.1, blue: 0.2), - Color(red: 0.2, green: 0.1, blue: 0.3) - ]), - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - .ignoresSafeArea() + VStack(spacing: 0) { + // 背景色 + Color(hex: 0x0C0527) + .ignoresSafeArea() - ScrollView { - VStack(spacing: 20) { - // 内容输入区域 - VStack(alignment: .leading, spacing: 12) { - // 文本输入框 - ZStack(alignment: .topLeading) { - RoundedRectangle(cornerRadius: 12) - .fill(Color.white.opacity(0.1)) - .frame(minHeight: 120) - - if store.content.isEmpty { - Text("Enter Content") - .foregroundColor(.white.opacity(0.5)) - .padding(.horizontal, 16) - .padding(.vertical, 12) - } - - TextEditor(text: .init( - get: { store.content }, - set: { store.send(.contentChanged($0)) } - )) - .foregroundColor(.white) - .background(Color.clear) - .padding(.horizontal, 12) - .padding(.vertical, 8) - .scrollContentBackground(.hidden) + // 主要内容区域(无ScrollView) + VStack(spacing: 20) { + // 内容输入区域 + VStack(alignment: .leading, spacing: 12) { + // 文本输入框 + ZStack(alignment: .topLeading) { + RoundedRectangle(cornerRadius: 12) + .fill(Color.white.opacity(0.1)) + .frame(height: 200) // 高度固定为200 + + if store.content.isEmpty { + Text("Enter Content") + .foregroundColor(.white.opacity(0.5)) + .padding(.horizontal, 16) + .padding(.vertical, 12) } - // 字符计数 - HStack { - Spacer() - Text("\(store.characterCount)/500") - .font(.system(size: 12)) - .foregroundColor( - store.characterCount > 500 ? .red : .white.opacity(0.6) - ) - } + TextEditor(text: .init( + get: { store.content }, + set: { store.send(.contentChanged($0)) } + )) + .foregroundColor(.white) + .background(Color.clear) + .padding(.horizontal, 12) + .padding(.vertical, 8) + .scrollContentBackground(.hidden) + .frame(height: 200) // 高度固定为200 } - .padding(.horizontal, 20) - .padding(.top, 20) - // 图片选择区域 - VStack(alignment: .leading, spacing: 12) { - if !store.processedImages.isEmpty || store.canAddMoreImages { - ModernImageSelectionGrid( - images: store.processedImages, - selectedItems: store.selectedImages, - canAddMore: store.canAddMoreImages, - onItemsChanged: { items in - store.send(.photosPickerItemsChanged(items)) - }, - onRemoveImage: { index in - store.send(.removeImage(index)) - } + // 字符计数 + HStack { + Spacer() + Text("\(store.characterCount)/500") + .font(.system(size: 12)) + .foregroundColor( + store.characterCount > 500 ? .red : .white.opacity(0.6) ) - } } - .padding(.horizontal, 20) - - // 加载状态 - if store.isLoading { - HStack { - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - Text("处理图片中...") - .font(.system(size: 14)) - .foregroundColor(.white.opacity(0.8)) - } - .padding(.top, 10) - } - - // 错误提示 - if let error = store.errorMessage { - Text(error) - .font(.system(size: 14)) - .foregroundColor(.red) - .padding(.horizontal, 20) - .multilineTextAlignment(.center) - } - - // 底部安全区域 - Color.clear.frame(height: geometry.safeAreaInsets.bottom + 100) } - } - - // 底部发布按钮 - VStack { - Spacer() + .padding(.horizontal, 20) + .padding(.top, 20) + // 图片选择区域 + VStack(alignment: .leading, spacing: 12) { + if !store.processedImages.isEmpty || store.canAddMoreImages { + ModernImageSelectionGrid( + images: store.processedImages, + selectedItems: store.selectedImages, + canAddMore: store.canAddMoreImages, + onItemsChanged: { items in + store.send(.photosPickerItemsChanged(items)) + }, + onRemoveImage: { index in + store.send(.removeImage(index)) + } + ) + } + } + .padding(.horizontal, 20) + + // 加载状态 + if store.isLoading { + HStack { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + Text("处理图片中...") + .font(.system(size: 14)) + .foregroundColor(.white.opacity(0.8)) + } + .padding(.top, 10) + } + + // 错误提示 + if let error = store.errorMessage { + Text(error) + .font(.system(size: 14)) + .foregroundColor(.red) + .padding(.horizontal, 20) + .multilineTextAlignment(.center) + } + + // 底部间距,确保内容不被键盘遮挡 + Color.clear.frame(height: max(keyboardHeight, geometry.safeAreaInsets.bottom) + 100) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .ignoresSafeArea(.keyboard, edges: .bottom) + + // 底部发布按钮 - 固定在底部 + VStack { Button(action: { store.send(.publishButtonTapped) }) { @@ -129,46 +123,48 @@ struct CreateFeedView: View { .frame(maxWidth: .infinity) .frame(height: 50) .background( - LinearGradient( - gradient: Gradient(colors: [ - Color.purple, - Color.blue - ]), - startPoint: .leading, - endPoint: .trailing - ) + Color(hex: 0x0C0527) ) .cornerRadius(25) .disabled(store.isLoading || !store.canPublish) .opacity(store.isLoading || !store.canPublish ? 0.6 : 1.0) } .padding(.horizontal, 20) - .padding(.bottom, geometry.safeAreaInsets.bottom + 20) + .padding(.bottom, max(keyboardHeight, geometry.safeAreaInsets.bottom) + 20) } + .background( + Color(hex: 0x0C0527) + ) } } .navigationTitle("图文发布") .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.hidden, for: .navigationBar) + .navigationBarBackButtonHidden(true) .toolbar { ToolbarItem(placement: .navigationBarLeading) { - Button("取消") { + Button(action: { store.send(.dismissView) + }) { + Image(systemName: "xmark") + .font(.system(size: 18, weight: .medium)) + .foregroundColor(.white) } - .foregroundColor(.white) - } - - ToolbarItem(placement: .navigationBarTrailing) { - Button("发布") { - store.send(.publishButtonTapped) - } - .foregroundColor(store.canPublish ? .white : .white.opacity(0.5)) - .disabled(!store.canPublish || store.isLoading) } + // 移除右上角发布按钮 } } .preferredColorScheme(.dark) + .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { notification in + if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect { + keyboardHeight = keyboardFrame.height + } + } + .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in + keyboardHeight = 0 + } } + } // MARK: - iOS 16+ 图片选择网格组件 @@ -182,49 +178,51 @@ struct ModernImageSelectionGrid: View { private let columns = Array(repeating: GridItem(.flexible(), spacing: 8), count: 3) var body: some View { - LazyVGrid(columns: columns, spacing: 8) { - // 显示已选择的图片 - ForEach(Array(images.enumerated()), id: \.offset) { index, image in - ZStack(alignment: .topTrailing) { - Image(uiImage: image) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(height: 100) - .clipped() - .cornerRadius(8) - - // 删除按钮 - Button(action: { - onRemoveImage(index) - }) { - Image(systemName: "xmark.circle.fill") - .font(.system(size: 20)) - .foregroundColor(.white) - .background(Color.black.opacity(0.6)) - .clipShape(Circle()) + WithPerceptionTracking { + LazyVGrid(columns: columns, spacing: 8) { + // 显示已选择的图片 + ForEach(Array(images.enumerated()), id: \.offset) { index, image in + ZStack(alignment: .topTrailing) { + Image(uiImage: image) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(height: 100) + .clipped() + .cornerRadius(8) + + // 删除按钮 + Button(action: { + onRemoveImage(index) + }) { + Image(systemName: "xmark.circle.fill") + .font(.system(size: 20)) + .foregroundColor(.white) + .background(Color.black.opacity(0.6)) + .clipShape(Circle()) + } + .padding(4) } - .padding(4) } - } - - // 添加图片按钮 - if canAddMore { - PhotosPicker( - selection: .init( - get: { selectedItems }, - set: onItemsChanged - ), - maxSelectionCount: 9, - matching: .images - ) { - RoundedRectangle(cornerRadius: 8) - .fill(Color.white.opacity(0.1)) - .frame(height: 100) - .overlay( - Image(systemName: "plus") - .font(.system(size: 40)) - .foregroundColor(.white.opacity(0.6)) - ) + + // 添加图片按钮 + if canAddMore { + PhotosPicker( + selection: .init( + get: { selectedItems }, + set: onItemsChanged + ), + maxSelectionCount: 9, + matching: .images + ) { + RoundedRectangle(cornerRadius: 8) + .fill(Color.white.opacity(0.1)) + .frame(height: 100) + .overlay( + Image(systemName: "plus") + .font(.system(size: 40)) + .foregroundColor(.white.opacity(0.6)) + ) + } } } } @@ -232,10 +230,10 @@ struct ModernImageSelectionGrid: View { } // MARK: - 预览 -#Preview { - CreateFeedView( - store: Store(initialState: CreateFeedFeature.State()) { - CreateFeedFeature() - } - ) -} \ No newline at end of file +//#Preview { +// CreateFeedView( +// store: Store(initialState: CreateFeedFeature.State()) { +// CreateFeedFeature() +// } +// ) +//} diff --git a/yana/Views/EditFeedView.swift b/yana/Views/EditFeedView.swift new file mode 100644 index 0000000..f2637da --- /dev/null +++ b/yana/Views/EditFeedView.swift @@ -0,0 +1,19 @@ +import SwiftUI + +struct EditFeedView: View { + var body: some View { + VStack(spacing: 20) { + Text("编辑动态") + .font(.title) + .bold() + Text("这里是 EditFeedView 占位内容") + .foregroundColor(.gray) + Spacer() + } + .padding() + } +} + +#Preview { + EditFeedView() +} \ No newline at end of file diff --git a/yana/Views/FeedListView.swift b/yana/Views/FeedListView.swift new file mode 100644 index 0000000..9315380 --- /dev/null +++ b/yana/Views/FeedListView.swift @@ -0,0 +1,67 @@ +import SwiftUI +import ComposableArchitecture + +struct FeedListView: View { + let store: StoreOf + + @State private var isEditFeedSheetPresented = false // 本地状态用于 sheet + + var body: some View { + WithPerceptionTracking { + GeometryReader { geometry in + ZStack { + // 背景图片 + Image("bg") + .resizable() + .aspectRatio(contentMode: .fill) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .clipped() + .ignoresSafeArea(.all) + VStack(alignment: .center, spacing: 0) { + // 顶部栏 + HStack { + Spacer(minLength: 0) + Spacer(minLength: 0) + Text("Enjoy your Life Time") + .font(.system(size: 22, weight: .semibold)) + .foregroundColor(.white) + .frame(maxWidth: .infinity, alignment: .center) + Spacer(minLength: 0) + Button(action: { + store.send(.editFeedButtonTapped) + }) { + Image("add icon") + .resizable() + .frame(width: 36, height: 36) + } + } + .padding(.horizontal, 20) + .padding(.top, geometry.safeAreaInsets.top) + // 其他内容 + Image(systemName: "heart.fill") + .font(.system(size: 60)) + .foregroundColor(.red) + .padding(.top, 40) + Text("The disease is like a cruel ruler,\nand time is our most precious treasure.\nEvery moment we live is a victory\nagainst the inevitable.") + .font(.system(size: 16)) + .multilineTextAlignment(.center) + .foregroundColor(.white.opacity(0.9)) + .padding(.horizontal, 30) + .padding(.bottom, 30) + Spacer() + } + .frame(maxWidth: .infinity, alignment: .top) + } + } + .onAppear { + store.send(.onAppear) + } +// .sheet(isPresented: store.binding( +// get: \.isEditFeedPresented, +// send: { $0 ? .editFeedButtonTapped : .editFeedDismissed } +// )) { +// EditFeedView() +// } + } + } +} diff --git a/yana/Views/FeedView.swift b/yana/Views/FeedView.swift index c100bba..1ff9611 100644 --- a/yana/Views/FeedView.swift +++ b/yana/Views/FeedView.swift @@ -3,111 +3,158 @@ import ComposableArchitecture struct FeedTopBarView: View { let store: StoreOf + let onShowCreateFeed: () -> Void var body: some View { - HStack { - Spacer() - Text("Enjoy your Life Time") - .font(.system(size: 22, weight: .semibold)) - .foregroundColor(.white) - Spacer() - Button(action: { - store.send(.showCreateFeed) - }) { - Image("add icon") - .frame(width: 36, height: 36) + WithPerceptionTracking { + HStack { + Spacer() + Text("Enjoy your Life Time") + .font(.system(size: 22, weight: .semibold)) + .foregroundColor(.white) + Spacer() + Button(action: { + onShowCreateFeed() // 只调用回调 + }) { + Image("add icon") + .frame(width: 36, height: 36) + } } + .padding(.horizontal, 20) } - .padding(.horizontal, 20) } } struct FeedMomentsListView: View { let store: StoreOf var body: some View { - LazyVStack(spacing: 16) { - if store.moments.isEmpty { - VStack(spacing: 12) { - Image(systemName: "heart.text.square") - .font(.system(size: 40)) - .foregroundColor(.white.opacity(0.6)) - Text("暂无动态内容") - .font(.system(size: 16)) - .foregroundColor(.white.opacity(0.8)) - if let error = store.error { - Text("错误: \(error)") - .font(.system(size: 12)) - .foregroundColor(.red.opacity(0.8)) - .multilineTextAlignment(.center) - .padding(.horizontal, 20) + WithPerceptionTracking { + LazyVStack(spacing: 16) { + if store.moments.isEmpty { + VStack(spacing: 12) { + Image(systemName: "heart.text.square") + .font(.system(size: 40)) + .foregroundColor(.white.opacity(0.6)) + Text("暂无动态内容") + .font(.system(size: 16)) + .foregroundColor(.white.opacity(0.8)) + if let error = store.error { + Text("错误: \(error)") + .font(.system(size: 12)) + .foregroundColor(.red.opacity(0.8)) + .multilineTextAlignment(.center) + .padding(.horizontal, 20) + } + + // 重试按钮 + if store.error != nil { + Button(action: { + store.send(.retryLoad) + }) { + Text("重试") + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.white) + .padding(.horizontal, 20) + .padding(.vertical, 8) + .background(Color.blue.opacity(0.8)) + .cornerRadius(8) + } + .padding(.top, 8) + } + } + .padding(.top, 40) + } else { + ForEach(Array(store.moments.enumerated()), id: \.element.dynamicId) { index, moment in + WithPerceptionTracking { + Text(moment.avatar) +// OptimizedDynamicCardView( +// moment: moment, +// allMoments: store.moments, +// currentIndex: index +// ) + .onAppear { + // 当显示最后一个动态时,加载更多数据 + if index == store.moments.count - 1 && store.hasMoreData && !store.isLoading { + store.send(.loadMoreMoments) + } + } + } } - } - .padding(.top, 40) - } else { - ForEach(Array(store.moments.enumerated()), id: \.element.dynamicId) { index, moment in - OptimizedDynamicCardView( - moment: moment, - allMoments: store.moments, - currentIndex: index - ) - } - } - } - .padding(.horizontal, 16) - .padding(.top, 30) - } -} - -struct FeedView: View { - let store: StoreOf - var body: some View { - GeometryReader { geometry in - ScrollView { - VStack(spacing: 20) { - FeedTopBarView(store: store) - Image(systemName: "heart.fill") - .font(.system(size: 60)) - .foregroundColor(.red) - .padding(.top, 40) - Text("The disease is like a cruel ruler,\nand time is our most precious treasure.\nEvery moment we live is a victory\nagainst the inevitable.") - .font(.system(size: 16)) - .multilineTextAlignment(.center) - .foregroundColor(.white.opacity(0.9)) - .padding(.horizontal, 30) - .padding(.top, 20) - FeedMomentsListView(store: store) - if store.isLoading { + // 加载更多指示器 + if store.isLoading && !store.moments.isEmpty { HStack { ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: .white)) - Text("加载中...") + Text("加载更多...") .font(.system(size: 14)) .foregroundColor(.white.opacity(0.8)) } .padding(.top, 20) } - Color.clear.frame(height: geometry.safeAreaInsets.bottom + 100) } } - .refreshable { - store.send(.loadLatestMoments) + .padding(.horizontal, 16) + .padding(.top, 20) // 调整顶部间距 + } + } +} + +struct FeedView: View { + let store: StoreOf + let onShowCreateFeed: () -> Void + + var body: some View { + WithPerceptionTracking { + GeometryReader { geometry in + ZStack { + // 背景图片 - 与 HomeView 保持一致 + Image("bg") + .resizable() + .aspectRatio(contentMode: .fill) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .clipped() + .ignoresSafeArea(.all) + + // 主要内容布局 + VStack(spacing: 0) { + // 固定内容区域 + VStack(spacing: 20) { + FeedTopBarView(store: store, onShowCreateFeed: onShowCreateFeed) + Image(systemName: "heart.fill") + .font(.system(size: 60)) + .foregroundColor(.red) + .padding(.top, 40) + Text("The disease is like a cruel ruler,\nand time is our most precious treasure.\nEvery moment we live is a victory\nagainst the inevitable.") + .font(.system(size: 16)) + .multilineTextAlignment(.center) + .foregroundColor(.white.opacity(0.9)) + .padding(.horizontal, 30) + .padding(.bottom, 30) + } +// .padding(.top, 60) // 为状态栏留出空间 + + // 滚动内容区域 - 只有动态列表 + ScrollView { + FeedMomentsListView(store: store) + .padding(.bottom, 20) // 底部留出空间 + } + .refreshable { + // 下拉刷新 + await withCheckedContinuation { continuation in + store.send(.refresh) + // 简单延迟确保刷新完成 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + continuation.resume() + } + } + } + } + } } .onAppear { store.send(.onAppear) } } - .sheet(isPresented: Binding( - get: { store.isCreateFeedPresented }, - set: { _ in store.send(.createFeedDismissed) } - )) { - CreateFeedView( - store: Store( - initialState: CreateFeedFeature.State() - ) { - CreateFeedFeature() - } - ) - } } } @@ -118,88 +165,90 @@ struct OptimizedDynamicCardView: View { let currentIndex: Int var body: some View { - VStack(alignment: .leading, spacing: 12) { - // 用户信息 - HStack { - // 使用缓存的头像 - CachedAsyncImage(url: moment.avatar) { image in - image - .resizable() - .aspectRatio(contentMode: .fill) - } placeholder: { - Circle() - .fill(Color.gray.opacity(0.3)) - .overlay( - Text(String(moment.nick.prefix(1))) - .font(.system(size: 16, weight: .medium)) - .foregroundColor(.white) - ) - } - .frame(width: 40, height: 40) - .clipShape(Circle()) - - VStack(alignment: .leading, spacing: 2) { - Text(moment.nick) - .font(.system(size: 16, weight: .medium)) - .foregroundColor(.white) + WithPerceptionTracking{ + VStack(alignment: .leading, spacing: 12) { + // 用户信息 + HStack { + // 使用缓存的头像 + CachedAsyncImage(url: moment.avatar) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + Circle() + .fill(Color.gray.opacity(0.3)) + .overlay( + Text(String(moment.nick.prefix(1))) + .font(.system(size: 16, weight: .medium)) + .foregroundColor(.white) + ) + } + .frame(width: 40, height: 40) + .clipShape(Circle()) - Text(formatTime(moment.publishTime)) - .font(.system(size: 12)) - .foregroundColor(.white.opacity(0.6)) - } - - Spacer() - - // VIP 标识 - if let vipInfo = moment.userVipInfoVO, let vipLevel = vipInfo.vipLevel { - Text("VIP\(vipLevel)") - .font(.system(size: 10, weight: .bold)) - .foregroundColor(.yellow) - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(Color.yellow.opacity(0.2)) - .cornerRadius(4) - } - } - - // 动态内容 - if !moment.content.isEmpty { - Text(moment.content) - .font(.system(size: 14)) - .foregroundColor(.white.opacity(0.9)) - .multilineTextAlignment(.leading) - } - - // 优化的图片网格 - if let images = moment.dynamicResList, !images.isEmpty { - OptimizedImageGrid(images: images) - } - - // 互动按钮 - HStack(spacing: 20) { - Button(action: {}) { - HStack(spacing: 4) { - Image(systemName: "message") - .font(.system(size: 16)) - Text("\(moment.commentCount)") - .font(.system(size: 14)) + VStack(alignment: .leading, spacing: 2) { + Text(moment.nick) + .font(.system(size: 16, weight: .medium)) + .foregroundColor(.white) + + Text(formatTime(moment.publishTime)) + .font(.system(size: 12)) + .foregroundColor(.white.opacity(0.6)) } - .foregroundColor(.white.opacity(0.8)) - } - - Button(action: {}) { - HStack(spacing: 4) { - Image(systemName: moment.isLike ? "heart.fill" : "heart") - .font(.system(size: 16)) - Text("\(moment.likeCount)") - .font(.system(size: 14)) + + Spacer() + + // VIP 标识 + if let vipInfo = moment.userVipInfoVO, let vipLevel = vipInfo.vipLevel { + Text("VIP\(vipLevel)") + .font(.system(size: 10, weight: .bold)) + .foregroundColor(.yellow) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(Color.yellow.opacity(0.2)) + .cornerRadius(4) } - .foregroundColor(moment.isLike ? .red : .white.opacity(0.8)) } - Spacer() + // 动态内容 + if !moment.content.isEmpty { + Text(moment.content) + .font(.system(size: 14)) + .foregroundColor(.white.opacity(0.9)) + .multilineTextAlignment(.leading) + } + + // 优化的图片网格 + if let images = moment.dynamicResList, !images.isEmpty { + OptimizedImageGrid(images: images) + } + + // 互动按钮 + HStack(spacing: 20) { + Button(action: {}) { + HStack(spacing: 4) { + Image(systemName: "message") + .font(.system(size: 16)) + Text("\(moment.commentCount)") + .font(.system(size: 14)) + } + .foregroundColor(.white.opacity(0.8)) + } + + Button(action: {}) { + HStack(spacing: 4) { + Image(systemName: moment.isLike ? "heart.fill" : "heart") + .font(.system(size: 16)) + Text("\(moment.likeCount)") + .font(.system(size: 14)) + } + .foregroundColor(moment.isLike ? .red : .white.opacity(0.8)) + } + + Spacer() + } + .padding(.top, 8) } - .padding(.top, 8) } .padding(16) .background( @@ -261,44 +310,45 @@ struct OptimizedImageGrid: View { var body: some View { GeometryReader { geometry in - let availableWidth = geometry.size.width + let availableWidth = max(geometry.size.width, 1) // 防止为0或负数 let spacing: CGFloat = 8 - switch images.count { - case 1: - // 单张图片:大正方形居中显示 - let imageSize: CGFloat = min(availableWidth * 0.6, 200) - HStack { - Spacer() - SquareImageView(image: images[0], size: imageSize) - Spacer() - } - - case 2: - // 两张图片:并排显示 - let imageSize: CGFloat = (availableWidth - spacing) / 2 - HStack(spacing: spacing) { - SquareImageView(image: images[0], size: imageSize) - SquareImageView(image: images[1], size: imageSize) - } - - case 3: - // 三张图片:水平排列 - let imageSize: CGFloat = (availableWidth - spacing * 2) / 3 - HStack(spacing: spacing) { - ForEach(images.prefix(3), id: \.id) { image in - SquareImageView(image: image, size: imageSize) + // 保护:如果availableWidth不合理,直接返回空视图 + if availableWidth < 10 { + Color.clear.frame(height: 1) + } else { + switch images.count { + case 1: + // 单张图片:大正方形居中显示 + let imageSize: CGFloat = min(availableWidth * 0.6, 200) + HStack { + Spacer() + SquareImageView(image: images[0], size: imageSize) + Spacer() } - } - - default: - // 四张及以上:九宫格布局(最多9张) - let imageSize: CGFloat = (availableWidth - spacing * 2) / 3 - let columns = Array(repeating: GridItem(.fixed(imageSize), spacing: spacing), count: 3) - - LazyVGrid(columns: columns, spacing: spacing) { - ForEach(images.prefix(9), id: \.id) { image in - SquareImageView(image: image, size: imageSize) + case 2: + // 两张图片:并排显示 + let imageSize: CGFloat = (availableWidth - spacing) / 2 + HStack(spacing: spacing) { + SquareImageView(image: images[0], size: imageSize) + SquareImageView(image: images[1], size: imageSize) + } + case 3: + // 三张图片:水平排列 + let imageSize: CGFloat = (availableWidth - spacing * 2) / 3 + HStack(spacing: spacing) { + ForEach(images.prefix(3), id: \.id) { image in + SquareImageView(image: image, size: imageSize) + } + } + default: + // 四张及以上:九宫格布局(最多9张) + let imageSize: CGFloat = (availableWidth - spacing * 2) / 3 + let columns = Array(repeating: GridItem(.fixed(imageSize), spacing: spacing), count: 3) + LazyVGrid(columns: columns, spacing: spacing) { + ForEach(images.prefix(9), id: \.id) { image in + SquareImageView(image: image, size: imageSize) + } } } } @@ -328,6 +378,7 @@ struct SquareImageView: View { let size: CGFloat var body: some View { + let safeSize = size.isFinite && size > 0 ? size : 100 // 防止非有限或负数 CachedAsyncImage(url: image.resUrl) { imageView in imageView .resizable() @@ -341,7 +392,7 @@ struct SquareImageView: View { .scaleEffect(0.8) ) } - .frame(width: size, height: size) + .frame(width: safeSize, height: safeSize) .clipped() .cornerRadius(8) } diff --git a/yana/Views/HomeView.swift b/yana/Views/HomeView.swift index b0cab32..54a1389 100644 --- a/yana/Views/HomeView.swift +++ b/yana/Views/HomeView.swift @@ -3,55 +3,76 @@ import ComposableArchitecture struct HomeView: View { let store: StoreOf - let onLogout: () -> Void // 新增:登出回调 + let onLogout: () -> Void @ObservedObject private var localizationManager = LocalizationManager.shared @State private var selectedTab: Tab = .feed var body: some View { - GeometryReader { geometry in - ZStack { - // 使用 "bg" 图片作为背景 - 全屏显示 - Image("bg") - .resizable() - .aspectRatio(contentMode: .fill) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .clipped() - .ignoresSafeArea(.all) - - // 主要内容区域 - 全屏显示 + NavigationStack { + GeometryReader { geometry in ZStack { - switch selectedTab { - case .feed: - NavigationStack { + // 使用 "bg" 图片作为背景 - 全屏显示 + Image("bg") + .resizable() + .aspectRatio(contentMode: .fill) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .clipped() + .ignoresSafeArea(.all) + + // 主要内容区域 - 全屏显示 + ZStack { + switch selectedTab { + case .feed: FeedView( - store: store.scope(state: \.feedState, action: \.feed) + store: store.scope( + state: \.feedState, + action: \.feed + ), + onShowCreateFeed: { + store.send(.showCreateFeed) + } ) - } - .transition(.opacity) - case .me: - MeView(onLogout: onLogout) .transition(.opacity) + case .me: + MeView(onLogout: onLogout) + .transition(.opacity) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + + // 底部导航栏 - 悬浮在最上层 + VStack { + Spacer() + BottomTabView(selectedTab: $selectedTab) + } + .padding(.bottom, geometry.safeAreaInsets.bottom + 100) + } + } + .onAppear { + store.send(.onAppear) + } + .sheet(isPresented: Binding( + get: { store.withState(\.isSettingPresented) }, + set: { _ in store.send(.settingDismissed) } + )) { + SettingView(store: store.scope(state: \.settingState, action: \.setting)) + } + .navigationDestination(isPresented: Binding( + get: { store.withState(\.route) == .createFeed }, + set: { isPresented in + if !isPresented { + store.send(.createFeedDismissed) } } - .frame(maxWidth: .infinity, maxHeight: .infinity) - - // 底部导航栏 - 悬浮在最上层 - VStack { - Spacer() - BottomTabView(selectedTab: $selectedTab) - } - .padding(.bottom, geometry.safeAreaInsets.bottom + 100) + )) { + CreateFeedView( + store: store.scope( + state: \.feedState.createFeedState, + action: \.feed.createFeed + ) + ) } } - .onAppear { - store.send(.onAppear) - } - .sheet(isPresented: Binding( - get: { store.isSettingPresented }, - set: { _ in store.send(.settingDismissed) } - )) { - SettingView(store: store.scope(state: \.settingState, action: \.setting)) - } } } diff --git a/yana/Views/MainView.swift b/yana/Views/MainView.swift new file mode 100644 index 0000000..da54a53 --- /dev/null +++ b/yana/Views/MainView.swift @@ -0,0 +1,49 @@ +import SwiftUI +import ComposableArchitecture +//import Components // 如果 BottomTabView 在 Components 命名空间,否则移除 + +struct MainView: View { + let store: StoreOf + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + NavigationStack { + GeometryReader { geometry in + ZStack { + // 背景图片 + Image("bg") + .resizable() + .aspectRatio(contentMode: .fill) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .clipped() + .ignoresSafeArea(.all) + // 主内容 + ZStack { + switch viewStore.selectedTab { + case .feed: + FeedListView(store: store.scope( + state: \.feedList, + action: \.feedList + )) + .transition(.opacity) + case .other: + MeView(onLogout: {}) // 这里可根据需要传递实际登出回调 + .transition(.opacity) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + // 底部导航栏 + VStack { + Spacer() + BottomTabView(selectedTab: viewStore.binding( + get: { Tab(rawValue: $0.selectedTab.rawValue) ?? .feed }, + send: { MainFeature.Action.selectTab(MainFeature.Tab(rawValue: $0.rawValue) ?? .feed) } + )) + } + .padding(.bottom, geometry.safeAreaInsets.bottom + 60) + } + } + } + } + } +} diff --git a/yana/Views/SplashView.swift b/yana/Views/SplashView.swift index 84d9f5c..9ef4769 100644 --- a/yana/Views/SplashView.swift +++ b/yana/Views/SplashView.swift @@ -25,15 +25,11 @@ struct SplashView: View { ) case .main: // 显示主应用页面 - HomeView( + MainView( store: Store( - initialState: HomeFeature.State() + initialState: MainFeature.State() ) { - HomeFeature() - }, - onLogout: { - // 登出时重新导航到登录页面 - store.send(.navigateToLogin) + MainFeature() } ) }