feat: 更新CreateFeed功能及相关视图组件
- 在CreateFeedFeature中新增isPresented依赖,确保在适当的上下文中执行视图关闭操作。 - 在FeedFeature中优化状态管理,简化CreateFeedView的呈现逻辑。 - 新增FeedListFeature和MainFeature,整合FeedListView和底部导航功能,提升用户体验。 - 更新HomeView和SplashView以集成MainView,确保应用结构一致性。 - 在多个视图中调整状态管理和导航逻辑,增强可维护性和用户体验。
This commit is contained in:
@@ -35,6 +35,7 @@ struct CreateFeedFeature {
|
||||
|
||||
@Dependency(\.apiService) var apiService
|
||||
@Dependency(\.dismiss) var dismiss
|
||||
@Dependency(\.isPresented) var isPresented
|
||||
|
||||
var body: some ReducerOf<Self> {
|
||||
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()
|
||||
}
|
||||
|
@@ -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<MomentsLatestResponse>)
|
||||
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<Self> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
50
yana/Features/FeedListFeature.swift
Normal file
50
yana/Features/FeedListFeature.swift
Normal file
@@ -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<Action> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@@ -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,81 +47,77 @@ struct HomeFeature {
|
||||
|
||||
// 新增:登出完成
|
||||
case logoutCompleted
|
||||
|
||||
// 新增:路由 actions
|
||||
case showCreateFeed
|
||||
case createFeedDismissed
|
||||
}
|
||||
|
||||
var body: some ReducerOf<Self> {
|
||||
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<State, Action> {
|
||||
// Reducer<State, Action>.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()
|
||||
// )
|
||||
// ])
|
||||
}
|
||||
}
|
||||
|
||||
|
35
yana/Features/MainFeature.swift
Normal file
35
yana/Features/MainFeature.swift
Normal file
@@ -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<Self> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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 {
|
||||
|
@@ -4,23 +4,17 @@ import PhotosUI
|
||||
|
||||
struct CreateFeedView: View {
|
||||
let store: StoreOf<CreateFeedFeature>
|
||||
@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
|
||||
)
|
||||
VStack(spacing: 0) {
|
||||
// 背景色
|
||||
Color(hex: 0x0C0527)
|
||||
.ignoresSafeArea()
|
||||
|
||||
ScrollView {
|
||||
// 主要内容区域(无ScrollView)
|
||||
VStack(spacing: 20) {
|
||||
// 内容输入区域
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
@@ -28,7 +22,7 @@ struct CreateFeedView: View {
|
||||
ZStack(alignment: .topLeading) {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(Color.white.opacity(0.1))
|
||||
.frame(minHeight: 120)
|
||||
.frame(height: 200) // 高度固定为200
|
||||
|
||||
if store.content.isEmpty {
|
||||
Text("Enter Content")
|
||||
@@ -46,6 +40,7 @@ struct CreateFeedView: View {
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 8)
|
||||
.scrollContentBackground(.hidden)
|
||||
.frame(height: 200) // 高度固定为200
|
||||
}
|
||||
|
||||
// 字符计数
|
||||
@@ -100,15 +95,14 @@ struct CreateFeedView: View {
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
// 底部安全区域
|
||||
Color.clear.frame(height: geometry.safeAreaInsets.bottom + 100)
|
||||
}
|
||||
// 底部间距,确保内容不被键盘遮挡
|
||||
Color.clear.frame(height: max(keyboardHeight, geometry.safeAreaInsets.bottom) + 100)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.ignoresSafeArea(.keyboard, edges: .bottom)
|
||||
|
||||
// 底部发布按钮
|
||||
// 底部发布按钮 - 固定在底部
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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,6 +178,7 @@ struct ModernImageSelectionGrid: View {
|
||||
private let columns = Array(repeating: GridItem(.flexible(), spacing: 8), count: 3)
|
||||
|
||||
var body: some View {
|
||||
WithPerceptionTracking {
|
||||
LazyVGrid(columns: columns, spacing: 8) {
|
||||
// 显示已选择的图片
|
||||
ForEach(Array(images.enumerated()), id: \.offset) { index, image in
|
||||
@@ -229,13 +226,14 @@ struct ModernImageSelectionGrid: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 预览
|
||||
#Preview {
|
||||
CreateFeedView(
|
||||
store: Store(initialState: CreateFeedFeature.State()) {
|
||||
CreateFeedFeature()
|
||||
}
|
||||
)
|
||||
}
|
||||
//#Preview {
|
||||
// CreateFeedView(
|
||||
// store: Store(initialState: CreateFeedFeature.State()) {
|
||||
// CreateFeedFeature()
|
||||
// }
|
||||
// )
|
||||
//}
|
||||
|
19
yana/Views/EditFeedView.swift
Normal file
19
yana/Views/EditFeedView.swift
Normal file
@@ -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()
|
||||
}
|
67
yana/Views/FeedListView.swift
Normal file
67
yana/Views/FeedListView.swift
Normal file
@@ -0,0 +1,67 @@
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
|
||||
struct FeedListView: View {
|
||||
let store: StoreOf<FeedListFeature>
|
||||
|
||||
@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()
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
@@ -3,7 +3,9 @@ import ComposableArchitecture
|
||||
|
||||
struct FeedTopBarView: View {
|
||||
let store: StoreOf<FeedFeature>
|
||||
let onShowCreateFeed: () -> Void
|
||||
var body: some View {
|
||||
WithPerceptionTracking {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("Enjoy your Life Time")
|
||||
@@ -11,7 +13,7 @@ struct FeedTopBarView: View {
|
||||
.foregroundColor(.white)
|
||||
Spacer()
|
||||
Button(action: {
|
||||
store.send(.showCreateFeed)
|
||||
onShowCreateFeed() // 只调用回调
|
||||
}) {
|
||||
Image("add icon")
|
||||
.frame(width: 36, height: 36)
|
||||
@@ -19,11 +21,13 @@ struct FeedTopBarView: View {
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FeedMomentsListView: View {
|
||||
let store: StoreOf<FeedFeature>
|
||||
var body: some View {
|
||||
WithPerceptionTracking {
|
||||
LazyVStack(spacing: 16) {
|
||||
if store.moments.isEmpty {
|
||||
VStack(spacing: 12) {
|
||||
@@ -40,31 +44,82 @@ struct FeedMomentsListView: View {
|
||||
.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
|
||||
OptimizedDynamicCardView(
|
||||
moment: moment,
|
||||
allMoments: store.moments,
|
||||
currentIndex: index
|
||||
)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载更多指示器
|
||||
if store.isLoading && !store.moments.isEmpty {
|
||||
HStack {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
Text("加载更多...")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
.padding(.top, 20)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 30)
|
||||
.padding(.top, 20) // 调整顶部间距
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FeedView: View {
|
||||
let store: StoreOf<FeedFeature>
|
||||
let onShowCreateFeed: () -> Void
|
||||
|
||||
var body: some View {
|
||||
WithPerceptionTracking {
|
||||
GeometryReader { geometry in
|
||||
ScrollView {
|
||||
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)
|
||||
FeedTopBarView(store: store, onShowCreateFeed: onShowCreateFeed)
|
||||
Image(systemName: "heart.fill")
|
||||
.font(.system(size: 60))
|
||||
.foregroundColor(.red)
|
||||
@@ -74,40 +129,32 @@ struct FeedView: View {
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
.padding(.horizontal, 30)
|
||||
.padding(.top, 20)
|
||||
.padding(.bottom, 30)
|
||||
}
|
||||
// .padding(.top, 60) // 为状态栏留出空间
|
||||
|
||||
// 滚动内容区域 - 只有动态列表
|
||||
ScrollView {
|
||||
FeedMomentsListView(store: store)
|
||||
if store.isLoading {
|
||||
HStack {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
Text("加载中...")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
.padding(.top, 20)
|
||||
}
|
||||
Color.clear.frame(height: geometry.safeAreaInsets.bottom + 100)
|
||||
}
|
||||
.padding(.bottom, 20) // 底部留出空间
|
||||
}
|
||||
.refreshable {
|
||||
store.send(.loadLatestMoments)
|
||||
// 下拉刷新
|
||||
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,6 +165,7 @@ struct OptimizedDynamicCardView: View {
|
||||
let currentIndex: Int
|
||||
|
||||
var body: some View {
|
||||
WithPerceptionTracking{
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
// 用户信息
|
||||
HStack {
|
||||
@@ -201,6 +249,7 @@ struct OptimizedDynamicCardView: View {
|
||||
}
|
||||
.padding(.top, 8)
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
.background(
|
||||
Color.white.opacity(0.1)
|
||||
@@ -261,9 +310,13 @@ 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
|
||||
|
||||
// 保护:如果availableWidth不合理,直接返回空视图
|
||||
if availableWidth < 10 {
|
||||
Color.clear.frame(height: 1)
|
||||
} else {
|
||||
switch images.count {
|
||||
case 1:
|
||||
// 单张图片:大正方形居中显示
|
||||
@@ -273,7 +326,6 @@ struct OptimizedImageGrid: View {
|
||||
SquareImageView(image: images[0], size: imageSize)
|
||||
Spacer()
|
||||
}
|
||||
|
||||
case 2:
|
||||
// 两张图片:并排显示
|
||||
let imageSize: CGFloat = (availableWidth - spacing) / 2
|
||||
@@ -281,7 +333,6 @@ struct OptimizedImageGrid: View {
|
||||
SquareImageView(image: images[0], size: imageSize)
|
||||
SquareImageView(image: images[1], size: imageSize)
|
||||
}
|
||||
|
||||
case 3:
|
||||
// 三张图片:水平排列
|
||||
let imageSize: CGFloat = (availableWidth - spacing * 2) / 3
|
||||
@@ -290,12 +341,10 @@ struct OptimizedImageGrid: View {
|
||||
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)
|
||||
@@ -303,6 +352,7 @@ struct OptimizedImageGrid: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: calculateGridHeight())
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
@@ -3,11 +3,12 @@ import ComposableArchitecture
|
||||
|
||||
struct HomeView: View {
|
||||
let store: StoreOf<HomeFeature>
|
||||
let onLogout: () -> Void // 新增:登出回调
|
||||
let onLogout: () -> Void
|
||||
@ObservedObject private var localizationManager = LocalizationManager.shared
|
||||
@State private var selectedTab: Tab = .feed
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
// 使用 "bg" 图片作为背景 - 全屏显示
|
||||
@@ -22,11 +23,15 @@ struct HomeView: View {
|
||||
ZStack {
|
||||
switch selectedTab {
|
||||
case .feed:
|
||||
NavigationStack {
|
||||
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)
|
||||
@@ -47,11 +52,27 @@ struct HomeView: View {
|
||||
store.send(.onAppear)
|
||||
}
|
||||
.sheet(isPresented: Binding(
|
||||
get: { store.isSettingPresented },
|
||||
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)
|
||||
}
|
||||
}
|
||||
)) {
|
||||
CreateFeedView(
|
||||
store: store.scope(
|
||||
state: \.feedState.createFeedState,
|
||||
action: \.feed.createFeed
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
49
yana/Views/MainView.swift
Normal file
49
yana/Views/MainView.swift
Normal file
@@ -0,0 +1,49 @@
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
//import Components // 如果 BottomTabView 在 Components 命名空间,否则移除
|
||||
|
||||
struct MainView: View {
|
||||
let store: StoreOf<MainFeature>
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
Reference in New Issue
Block a user