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(\.apiService) var apiService
|
||||||
@Dependency(\.dismiss) var dismiss
|
@Dependency(\.dismiss) var dismiss
|
||||||
|
@Dependency(\.isPresented) var isPresented
|
||||||
|
|
||||||
var body: some ReducerOf<Self> {
|
var body: some ReducerOf<Self> {
|
||||||
Reduce { state, action in
|
Reduce { state, action in
|
||||||
@@ -108,6 +109,11 @@ struct CreateFeedFeature {
|
|||||||
state.errorMessage = nil
|
state.errorMessage = nil
|
||||||
return .none
|
return .none
|
||||||
case .dismissView:
|
case .dismissView:
|
||||||
|
// 检查是否在presentation context中
|
||||||
|
guard isPresented else {
|
||||||
|
// 如果不在presentation context中,不执行dismiss
|
||||||
|
return .none
|
||||||
|
}
|
||||||
return .run { _ in
|
return .run { _ in
|
||||||
await dismiss()
|
await dismiss()
|
||||||
}
|
}
|
||||||
|
@@ -7,145 +7,103 @@ struct FeedFeature {
|
|||||||
struct State: Equatable {
|
struct State: Equatable {
|
||||||
var moments: [MomentsInfo] = []
|
var moments: [MomentsInfo] = []
|
||||||
var isLoading = false
|
var isLoading = false
|
||||||
|
var isRefreshing = false
|
||||||
var hasMoreData = true
|
var hasMoreData = true
|
||||||
var error: String?
|
var error: String?
|
||||||
var nextDynamicId: Int = 0
|
var nextDynamicId: Int = 0
|
||||||
|
// CreateFeedView 相关状态
|
||||||
// 是否已初始化 - 用于防止重复初始化
|
var createFeedState = CreateFeedFeature.State()
|
||||||
var isInitialized = false
|
|
||||||
|
|
||||||
// CreateFeedView 相关状态 - 简化为布尔值
|
|
||||||
var isCreateFeedPresented = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Action {
|
enum Action: Equatable {
|
||||||
case onAppear
|
case onAppear
|
||||||
|
case refresh
|
||||||
case loadLatestMoments
|
case loadLatestMoments
|
||||||
case loadMoreMoments
|
case loadMoreMoments
|
||||||
case momentsResponse(TaskResult<MomentsLatestResponse>)
|
case momentsResponse(TaskResult<MomentsLatestResponse>)
|
||||||
case clearError
|
case clearError
|
||||||
case retryLoad
|
case retryLoad
|
||||||
|
// CreateFeedView 相关 Action
|
||||||
// CreateFeedView 相关 Action - 简化为布尔控制
|
|
||||||
case showCreateFeed
|
|
||||||
case createFeedCompleted
|
case createFeedCompleted
|
||||||
case createFeedDismissed
|
case createFeedDismissed
|
||||||
|
// CreateFeedFeature 的 action
|
||||||
|
case createFeed(CreateFeedFeature.Action)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Dependency(\.apiService) var apiService
|
@Dependency(\.apiService) var apiService
|
||||||
|
|
||||||
var body: some ReducerOf<Self> {
|
var body: some ReducerOf<Self> {
|
||||||
|
Scope(state: \.createFeedState, action: \.createFeed) {
|
||||||
|
CreateFeedFeature()
|
||||||
|
}
|
||||||
|
|
||||||
Reduce { state, action in
|
Reduce { state, action in
|
||||||
switch action {
|
switch action {
|
||||||
case .onAppear:
|
case .onAppear:
|
||||||
// 只在未初始化时才执行首次加载
|
guard state.moments.isEmpty && !state.isLoading else { return .none }
|
||||||
guard !state.isInitialized else {
|
|
||||||
return .none
|
|
||||||
}
|
|
||||||
|
|
||||||
return .send(.loadLatestMoments)
|
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:
|
case .loadLatestMoments:
|
||||||
// 添加重复请求防护
|
guard !state.isLoading else { return .none }
|
||||||
guard !state.isLoading else {
|
|
||||||
return .none
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载最新数据(下拉刷新)
|
|
||||||
state.isLoading = true
|
state.isLoading = true
|
||||||
state.error = nil
|
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
|
return .run { send in
|
||||||
await send(.momentsResponse(TaskResult {
|
await send(.momentsResponse(TaskResult { try await apiService.request(request) }))
|
||||||
try await apiService.request(request)
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case .loadMoreMoments:
|
case .loadMoreMoments:
|
||||||
// 加载更多数据(分页加载)
|
|
||||||
guard !state.isLoading && state.hasMoreData else { return .none }
|
guard !state.isLoading && state.hasMoreData else { return .none }
|
||||||
|
|
||||||
state.isLoading = true
|
state.isLoading = true
|
||||||
state.error = nil
|
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
|
return .run { send in
|
||||||
await send(.momentsResponse(TaskResult {
|
await send(.momentsResponse(TaskResult { try await apiService.request(request) }))
|
||||||
try await apiService.request(request)
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case let .momentsResponse(.success(response)):
|
case let .momentsResponse(.success(response)):
|
||||||
state.isLoading = false
|
state.isLoading = false
|
||||||
|
state.isRefreshing = false
|
||||||
// 设置初始化状态
|
|
||||||
if !state.isInitialized {
|
|
||||||
state.isInitialized = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查响应状态
|
|
||||||
guard response.code == 200, let data = response.data else {
|
guard response.code == 200, let data = response.data else {
|
||||||
let errorMsg = response.message.isEmpty ? "获取动态失败" : response.message
|
let errorMsg = response.message.isEmpty ? "获取动态失败" : response.message
|
||||||
state.error = errorMsg
|
state.error = errorMsg
|
||||||
return .none
|
return .none
|
||||||
}
|
}
|
||||||
|
let isRefresh = state.nextDynamicId == 0 || state.isRefreshing
|
||||||
// 判断是刷新还是加载更多
|
|
||||||
let isRefresh = state.nextDynamicId == 0
|
|
||||||
|
|
||||||
if isRefresh {
|
if isRefresh {
|
||||||
// 刷新:替换所有数据
|
|
||||||
state.moments = data.dynamicList
|
state.moments = data.dynamicList
|
||||||
} else {
|
} else {
|
||||||
// 加载更多:追加到现有数据
|
|
||||||
state.moments.append(contentsOf: data.dynamicList)
|
state.moments.append(contentsOf: data.dynamicList)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新分页状态
|
|
||||||
state.nextDynamicId = data.nextDynamicId
|
state.nextDynamicId = data.nextDynamicId
|
||||||
state.hasMoreData = !data.dynamicList.isEmpty
|
state.hasMoreData = !data.dynamicList.isEmpty
|
||||||
|
|
||||||
return .none
|
return .none
|
||||||
|
|
||||||
case let .momentsResponse(.failure(error)):
|
case let .momentsResponse(.failure(error)):
|
||||||
state.isLoading = false
|
state.isLoading = false
|
||||||
|
state.isRefreshing = false
|
||||||
state.error = error.localizedDescription
|
state.error = error.localizedDescription
|
||||||
return .none
|
return .none
|
||||||
|
|
||||||
case .clearError:
|
case .clearError:
|
||||||
state.error = nil
|
state.error = nil
|
||||||
return .none
|
return .none
|
||||||
|
|
||||||
case .retryLoad:
|
case .retryLoad:
|
||||||
// 重试加载
|
|
||||||
if state.moments.isEmpty {
|
if state.moments.isEmpty {
|
||||||
return .send(.loadLatestMoments)
|
return .send(.loadLatestMoments)
|
||||||
} else {
|
} else {
|
||||||
return .send(.loadMoreMoments)
|
return .send(.loadMoreMoments)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateFeedView 相关 Action 处理 - 简化为布尔控制
|
|
||||||
case .showCreateFeed:
|
|
||||||
state.isCreateFeedPresented = true
|
|
||||||
return .none
|
|
||||||
|
|
||||||
case .createFeedCompleted:
|
case .createFeedCompleted:
|
||||||
// 发布完成后刷新动态列表
|
return .send(.refresh)
|
||||||
state.isCreateFeedPresented = false
|
|
||||||
return .send(.loadLatestMoments)
|
|
||||||
|
|
||||||
case .createFeedDismissed:
|
case .createFeedDismissed:
|
||||||
state.isCreateFeedPresented = false
|
return .none
|
||||||
|
case .createFeed(.dismissView):
|
||||||
|
return .send(.createFeedDismissed)
|
||||||
|
case .createFeed:
|
||||||
return .none
|
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
|
@Reducer
|
||||||
struct HomeFeature {
|
struct HomeFeature {
|
||||||
|
enum Route: Equatable {
|
||||||
|
case createFeed
|
||||||
|
}
|
||||||
|
|
||||||
@ObservableState
|
@ObservableState
|
||||||
struct State: Equatable {
|
struct State: Equatable, Sendable {
|
||||||
var isInitialized = false
|
var isInitialized = false
|
||||||
var userInfo: UserInfo?
|
var userInfo: UserInfo?
|
||||||
var accountModel: AccountModel?
|
var accountModel: AccountModel?
|
||||||
@@ -19,9 +23,13 @@ struct HomeFeature {
|
|||||||
|
|
||||||
// 新增:登出状态
|
// 新增:登出状态
|
||||||
var isLoggedOut = false
|
var isLoggedOut = false
|
||||||
|
|
||||||
|
// 新增:路由状态
|
||||||
|
var route: Route? = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Action {
|
@CasePathable
|
||||||
|
enum Action: Equatable {
|
||||||
case onAppear
|
case onAppear
|
||||||
case loadUserInfo
|
case loadUserInfo
|
||||||
case userInfoLoaded(UserInfo?)
|
case userInfoLoaded(UserInfo?)
|
||||||
@@ -39,81 +47,77 @@ struct HomeFeature {
|
|||||||
|
|
||||||
// 新增:登出完成
|
// 新增:登出完成
|
||||||
case logoutCompleted
|
case logoutCompleted
|
||||||
|
|
||||||
|
// 新增:路由 actions
|
||||||
|
case showCreateFeed
|
||||||
|
case createFeedDismissed
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some ReducerOf<Self> {
|
var body: some Reducer<State, Action> {
|
||||||
Scope(state: \.settingState, action: \.setting) {
|
// Reducer<State, Action>.combine([
|
||||||
SettingFeature()
|
// Reducer { state, action in
|
||||||
}
|
// switch action {
|
||||||
|
// case .onAppear:
|
||||||
// 新增:Feed Scope
|
// guard !state.isInitialized else {
|
||||||
Scope(state: \.feedState, action: \.feed) {
|
// return Effect.none
|
||||||
FeedFeature()
|
// }
|
||||||
}
|
// state.isInitialized = true
|
||||||
|
// return .concatenate(
|
||||||
Reduce { state, action in
|
// .send(.loadUserInfo),
|
||||||
switch action {
|
// .send(.loadAccountModel)
|
||||||
case .onAppear:
|
// )
|
||||||
// 只在未初始化时才执行首次加载
|
// case .loadUserInfo:
|
||||||
guard !state.isInitialized else {
|
// return .run { send in
|
||||||
return .none
|
// let userInfo = await UserInfoManager.getUserInfo()
|
||||||
}
|
// await send(.userInfoLoaded(userInfo))
|
||||||
|
// }
|
||||||
state.isInitialized = true
|
// case let .userInfoLoaded(userInfo):
|
||||||
return .concatenate(
|
// state.userInfo = userInfo
|
||||||
.send(.loadUserInfo),
|
// return Effect.none
|
||||||
.send(.loadAccountModel)
|
// case .loadAccountModel:
|
||||||
)
|
// return .run { send in
|
||||||
|
// let accountModel = await UserInfoManager.getAccountModel()
|
||||||
case .loadUserInfo:
|
// await send(.accountModelLoaded(accountModel))
|
||||||
// 从本地存储加载用户信息
|
// }
|
||||||
return .run { send in
|
// case let .accountModelLoaded(accountModel):
|
||||||
let userInfo = await UserInfoManager.getUserInfo()
|
// state.accountModel = accountModel
|
||||||
await send(.userInfoLoaded(userInfo))
|
// return Effect.none
|
||||||
}
|
// case .logoutTapped:
|
||||||
|
// return .send(.logout)
|
||||||
case let .userInfoLoaded(userInfo):
|
// case .logout:
|
||||||
state.userInfo = userInfo
|
// return .run { send in
|
||||||
return .none
|
// await UserInfoManager.clearAllAuthenticationData()
|
||||||
|
// await send(.logoutCompleted)
|
||||||
case .loadAccountModel:
|
// }
|
||||||
// 从本地存储加载账户信息
|
// case .logoutCompleted:
|
||||||
return .run { send in
|
// state.isLoggedOut = true
|
||||||
let accountModel = await UserInfoManager.getAccountModel()
|
// return Effect.none
|
||||||
await send(.accountModelLoaded(accountModel))
|
// case .settingDismissed:
|
||||||
}
|
// state.isSettingPresented = false
|
||||||
|
// return Effect.none
|
||||||
case let .accountModelLoaded(accountModel):
|
// case .setting:
|
||||||
state.accountModel = accountModel
|
// return Effect.none
|
||||||
return .none
|
// case .showCreateFeed:
|
||||||
|
// state.route = .createFeed
|
||||||
case .logoutTapped:
|
// return Effect.none
|
||||||
return .send(.logout)
|
// case .createFeedDismissed:
|
||||||
|
// state.route = nil
|
||||||
case .logout:
|
// return Effect.none
|
||||||
// 清除所有认证数据并设置登出状态
|
// case .feed:
|
||||||
return .run { send in
|
// return Effect.none
|
||||||
await UserInfoManager.clearAllAuthenticationData()
|
// }
|
||||||
await send(.logoutCompleted)
|
// },
|
||||||
}
|
// Scope(
|
||||||
|
// state: \State.settingState,
|
||||||
case .logoutCompleted:
|
// action: /Action.setting,
|
||||||
state.isLoggedOut = true
|
// child: SettingFeature()
|
||||||
return .none
|
// ),
|
||||||
|
// Scope(
|
||||||
case .settingDismissed:
|
// state: \State.feedState,
|
||||||
state.isSettingPresented = false
|
// action: /Action.feed,
|
||||||
return .none
|
// child: FeedFeature()
|
||||||
|
// )
|
||||||
case .setting:
|
// ])
|
||||||
// 由子reducer处理
|
|
||||||
return .none
|
|
||||||
|
|
||||||
case .feed(_):
|
|
||||||
// FeedFeature 的 action 由 Scope 自动处理
|
|
||||||
return .none
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
var body: some View {
|
||||||
if isLoggedIn {
|
if isLoggedIn {
|
||||||
HomeView(
|
MainView(
|
||||||
store: Store(
|
store: Store(
|
||||||
initialState: HomeFeature.State()
|
initialState: MainFeature.State()
|
||||||
) {
|
) {
|
||||||
HomeFeature()
|
MainFeature()
|
||||||
},
|
|
||||||
onLogout: {
|
|
||||||
isLoggedIn = false
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@@ -4,23 +4,17 @@ import PhotosUI
|
|||||||
|
|
||||||
struct CreateFeedView: View {
|
struct CreateFeedView: View {
|
||||||
let store: StoreOf<CreateFeedFeature>
|
let store: StoreOf<CreateFeedFeature>
|
||||||
|
@State private var keyboardHeight: CGFloat = 0
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
ZStack {
|
VStack(spacing: 0) {
|
||||||
// 背景渐变
|
// 背景色
|
||||||
LinearGradient(
|
Color(hex: 0x0C0527)
|
||||||
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()
|
.ignoresSafeArea()
|
||||||
|
|
||||||
ScrollView {
|
// 主要内容区域(无ScrollView)
|
||||||
VStack(spacing: 20) {
|
VStack(spacing: 20) {
|
||||||
// 内容输入区域
|
// 内容输入区域
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
@@ -28,7 +22,7 @@ struct CreateFeedView: View {
|
|||||||
ZStack(alignment: .topLeading) {
|
ZStack(alignment: .topLeading) {
|
||||||
RoundedRectangle(cornerRadius: 12)
|
RoundedRectangle(cornerRadius: 12)
|
||||||
.fill(Color.white.opacity(0.1))
|
.fill(Color.white.opacity(0.1))
|
||||||
.frame(minHeight: 120)
|
.frame(height: 200) // 高度固定为200
|
||||||
|
|
||||||
if store.content.isEmpty {
|
if store.content.isEmpty {
|
||||||
Text("Enter Content")
|
Text("Enter Content")
|
||||||
@@ -46,6 +40,7 @@ struct CreateFeedView: View {
|
|||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
|
.frame(height: 200) // 高度固定为200
|
||||||
}
|
}
|
||||||
|
|
||||||
// 字符计数
|
// 字符计数
|
||||||
@@ -100,15 +95,14 @@ struct CreateFeedView: View {
|
|||||||
.multilineTextAlignment(.center)
|
.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 {
|
VStack {
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
store.send(.publishButtonTapped)
|
store.send(.publishButtonTapped)
|
||||||
}) {
|
}) {
|
||||||
@@ -129,46 +123,48 @@ struct CreateFeedView: View {
|
|||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.frame(height: 50)
|
.frame(height: 50)
|
||||||
.background(
|
.background(
|
||||||
LinearGradient(
|
Color(hex: 0x0C0527)
|
||||||
gradient: Gradient(colors: [
|
|
||||||
Color.purple,
|
|
||||||
Color.blue
|
|
||||||
]),
|
|
||||||
startPoint: .leading,
|
|
||||||
endPoint: .trailing
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.cornerRadius(25)
|
.cornerRadius(25)
|
||||||
.disabled(store.isLoading || !store.canPublish)
|
.disabled(store.isLoading || !store.canPublish)
|
||||||
.opacity(store.isLoading || !store.canPublish ? 0.6 : 1.0)
|
.opacity(store.isLoading || !store.canPublish ? 0.6 : 1.0)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 20)
|
.padding(.horizontal, 20)
|
||||||
.padding(.bottom, geometry.safeAreaInsets.bottom + 20)
|
.padding(.bottom, max(keyboardHeight, geometry.safeAreaInsets.bottom) + 20)
|
||||||
}
|
}
|
||||||
|
.background(
|
||||||
|
Color(hex: 0x0C0527)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("图文发布")
|
.navigationTitle("图文发布")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbarBackground(.hidden, for: .navigationBar)
|
.toolbarBackground(.hidden, for: .navigationBar)
|
||||||
|
.navigationBarBackButtonHidden(true)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
Button("取消") {
|
Button(action: {
|
||||||
store.send(.dismissView)
|
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)
|
.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+ 图片选择网格组件
|
// MARK: - iOS 16+ 图片选择网格组件
|
||||||
@@ -182,6 +178,7 @@ struct ModernImageSelectionGrid: View {
|
|||||||
private let columns = Array(repeating: GridItem(.flexible(), spacing: 8), count: 3)
|
private let columns = Array(repeating: GridItem(.flexible(), spacing: 8), count: 3)
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
WithPerceptionTracking {
|
||||||
LazyVGrid(columns: columns, spacing: 8) {
|
LazyVGrid(columns: columns, spacing: 8) {
|
||||||
// 显示已选择的图片
|
// 显示已选择的图片
|
||||||
ForEach(Array(images.enumerated()), id: \.offset) { index, image in
|
ForEach(Array(images.enumerated()), id: \.offset) { index, image in
|
||||||
@@ -229,13 +226,14 @@ struct ModernImageSelectionGrid: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 预览
|
// MARK: - 预览
|
||||||
#Preview {
|
//#Preview {
|
||||||
CreateFeedView(
|
// CreateFeedView(
|
||||||
store: Store(initialState: CreateFeedFeature.State()) {
|
// store: Store(initialState: CreateFeedFeature.State()) {
|
||||||
CreateFeedFeature()
|
// 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 {
|
struct FeedTopBarView: View {
|
||||||
let store: StoreOf<FeedFeature>
|
let store: StoreOf<FeedFeature>
|
||||||
|
let onShowCreateFeed: () -> Void
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
WithPerceptionTracking {
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
Text("Enjoy your Life Time")
|
Text("Enjoy your Life Time")
|
||||||
@@ -11,7 +13,7 @@ struct FeedTopBarView: View {
|
|||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
Spacer()
|
Spacer()
|
||||||
Button(action: {
|
Button(action: {
|
||||||
store.send(.showCreateFeed)
|
onShowCreateFeed() // 只调用回调
|
||||||
}) {
|
}) {
|
||||||
Image("add icon")
|
Image("add icon")
|
||||||
.frame(width: 36, height: 36)
|
.frame(width: 36, height: 36)
|
||||||
@@ -19,11 +21,13 @@ struct FeedTopBarView: View {
|
|||||||
}
|
}
|
||||||
.padding(.horizontal, 20)
|
.padding(.horizontal, 20)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct FeedMomentsListView: View {
|
struct FeedMomentsListView: View {
|
||||||
let store: StoreOf<FeedFeature>
|
let store: StoreOf<FeedFeature>
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
WithPerceptionTracking {
|
||||||
LazyVStack(spacing: 16) {
|
LazyVStack(spacing: 16) {
|
||||||
if store.moments.isEmpty {
|
if store.moments.isEmpty {
|
||||||
VStack(spacing: 12) {
|
VStack(spacing: 12) {
|
||||||
@@ -40,31 +44,82 @@ struct FeedMomentsListView: View {
|
|||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.padding(.horizontal, 20)
|
.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)
|
.padding(.top, 40)
|
||||||
} else {
|
} else {
|
||||||
ForEach(Array(store.moments.enumerated()), id: \.element.dynamicId) { index, moment in
|
ForEach(Array(store.moments.enumerated()), id: \.element.dynamicId) { index, moment in
|
||||||
OptimizedDynamicCardView(
|
WithPerceptionTracking {
|
||||||
moment: moment,
|
Text(moment.avatar)
|
||||||
allMoments: store.moments,
|
// OptimizedDynamicCardView(
|
||||||
currentIndex: index
|
// 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(.horizontal, 16)
|
||||||
.padding(.top, 30)
|
.padding(.top, 20) // 调整顶部间距
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct FeedView: View {
|
struct FeedView: View {
|
||||||
let store: StoreOf<FeedFeature>
|
let store: StoreOf<FeedFeature>
|
||||||
|
let onShowCreateFeed: () -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
WithPerceptionTracking {
|
||||||
GeometryReader { geometry in
|
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) {
|
VStack(spacing: 20) {
|
||||||
FeedTopBarView(store: store)
|
FeedTopBarView(store: store, onShowCreateFeed: onShowCreateFeed)
|
||||||
Image(systemName: "heart.fill")
|
Image(systemName: "heart.fill")
|
||||||
.font(.system(size: 60))
|
.font(.system(size: 60))
|
||||||
.foregroundColor(.red)
|
.foregroundColor(.red)
|
||||||
@@ -74,40 +129,32 @@ struct FeedView: View {
|
|||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.foregroundColor(.white.opacity(0.9))
|
.foregroundColor(.white.opacity(0.9))
|
||||||
.padding(.horizontal, 30)
|
.padding(.horizontal, 30)
|
||||||
.padding(.top, 20)
|
.padding(.bottom, 30)
|
||||||
|
}
|
||||||
|
// .padding(.top, 60) // 为状态栏留出空间
|
||||||
|
|
||||||
|
// 滚动内容区域 - 只有动态列表
|
||||||
|
ScrollView {
|
||||||
FeedMomentsListView(store: store)
|
FeedMomentsListView(store: store)
|
||||||
if store.isLoading {
|
.padding(.bottom, 20) // 底部留出空间
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.refreshable {
|
.refreshable {
|
||||||
store.send(.loadLatestMoments)
|
// 下拉刷新
|
||||||
|
await withCheckedContinuation { continuation in
|
||||||
|
store.send(.refresh)
|
||||||
|
// 简单延迟确保刷新完成
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
|
continuation.resume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
store.send(.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
|
let currentIndex: Int
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
WithPerceptionTracking{
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
// 用户信息
|
// 用户信息
|
||||||
HStack {
|
HStack {
|
||||||
@@ -201,6 +249,7 @@ struct OptimizedDynamicCardView: View {
|
|||||||
}
|
}
|
||||||
.padding(.top, 8)
|
.padding(.top, 8)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.padding(16)
|
.padding(16)
|
||||||
.background(
|
.background(
|
||||||
Color.white.opacity(0.1)
|
Color.white.opacity(0.1)
|
||||||
@@ -261,9 +310,13 @@ struct OptimizedImageGrid: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
let availableWidth = geometry.size.width
|
let availableWidth = max(geometry.size.width, 1) // 防止为0或负数
|
||||||
let spacing: CGFloat = 8
|
let spacing: CGFloat = 8
|
||||||
|
|
||||||
|
// 保护:如果availableWidth不合理,直接返回空视图
|
||||||
|
if availableWidth < 10 {
|
||||||
|
Color.clear.frame(height: 1)
|
||||||
|
} else {
|
||||||
switch images.count {
|
switch images.count {
|
||||||
case 1:
|
case 1:
|
||||||
// 单张图片:大正方形居中显示
|
// 单张图片:大正方形居中显示
|
||||||
@@ -273,7 +326,6 @@ struct OptimizedImageGrid: View {
|
|||||||
SquareImageView(image: images[0], size: imageSize)
|
SquareImageView(image: images[0], size: imageSize)
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
// 两张图片:并排显示
|
// 两张图片:并排显示
|
||||||
let imageSize: CGFloat = (availableWidth - spacing) / 2
|
let imageSize: CGFloat = (availableWidth - spacing) / 2
|
||||||
@@ -281,7 +333,6 @@ struct OptimizedImageGrid: View {
|
|||||||
SquareImageView(image: images[0], size: imageSize)
|
SquareImageView(image: images[0], size: imageSize)
|
||||||
SquareImageView(image: images[1], size: imageSize)
|
SquareImageView(image: images[1], size: imageSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
case 3:
|
case 3:
|
||||||
// 三张图片:水平排列
|
// 三张图片:水平排列
|
||||||
let imageSize: CGFloat = (availableWidth - spacing * 2) / 3
|
let imageSize: CGFloat = (availableWidth - spacing * 2) / 3
|
||||||
@@ -290,12 +341,10 @@ struct OptimizedImageGrid: View {
|
|||||||
SquareImageView(image: image, size: imageSize)
|
SquareImageView(image: image, size: imageSize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// 四张及以上:九宫格布局(最多9张)
|
// 四张及以上:九宫格布局(最多9张)
|
||||||
let imageSize: CGFloat = (availableWidth - spacing * 2) / 3
|
let imageSize: CGFloat = (availableWidth - spacing * 2) / 3
|
||||||
let columns = Array(repeating: GridItem(.fixed(imageSize), spacing: spacing), count: 3)
|
let columns = Array(repeating: GridItem(.fixed(imageSize), spacing: spacing), count: 3)
|
||||||
|
|
||||||
LazyVGrid(columns: columns, spacing: spacing) {
|
LazyVGrid(columns: columns, spacing: spacing) {
|
||||||
ForEach(images.prefix(9), id: \.id) { image in
|
ForEach(images.prefix(9), id: \.id) { image in
|
||||||
SquareImageView(image: image, size: imageSize)
|
SquareImageView(image: image, size: imageSize)
|
||||||
@@ -303,6 +352,7 @@ struct OptimizedImageGrid: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.frame(height: calculateGridHeight())
|
.frame(height: calculateGridHeight())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,6 +378,7 @@ struct SquareImageView: View {
|
|||||||
let size: CGFloat
|
let size: CGFloat
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
let safeSize = size.isFinite && size > 0 ? size : 100 // 防止非有限或负数
|
||||||
CachedAsyncImage(url: image.resUrl) { imageView in
|
CachedAsyncImage(url: image.resUrl) { imageView in
|
||||||
imageView
|
imageView
|
||||||
.resizable()
|
.resizable()
|
||||||
@@ -341,7 +392,7 @@ struct SquareImageView: View {
|
|||||||
.scaleEffect(0.8)
|
.scaleEffect(0.8)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.frame(width: size, height: size)
|
.frame(width: safeSize, height: safeSize)
|
||||||
.clipped()
|
.clipped()
|
||||||
.cornerRadius(8)
|
.cornerRadius(8)
|
||||||
}
|
}
|
||||||
|
@@ -3,11 +3,12 @@ import ComposableArchitecture
|
|||||||
|
|
||||||
struct HomeView: View {
|
struct HomeView: View {
|
||||||
let store: StoreOf<HomeFeature>
|
let store: StoreOf<HomeFeature>
|
||||||
let onLogout: () -> Void // 新增:登出回调
|
let onLogout: () -> Void
|
||||||
@ObservedObject private var localizationManager = LocalizationManager.shared
|
@ObservedObject private var localizationManager = LocalizationManager.shared
|
||||||
@State private var selectedTab: Tab = .feed
|
@State private var selectedTab: Tab = .feed
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
ZStack {
|
ZStack {
|
||||||
// 使用 "bg" 图片作为背景 - 全屏显示
|
// 使用 "bg" 图片作为背景 - 全屏显示
|
||||||
@@ -22,11 +23,15 @@ struct HomeView: View {
|
|||||||
ZStack {
|
ZStack {
|
||||||
switch selectedTab {
|
switch selectedTab {
|
||||||
case .feed:
|
case .feed:
|
||||||
NavigationStack {
|
|
||||||
FeedView(
|
FeedView(
|
||||||
store: store.scope(state: \.feedState, action: \.feed)
|
store: store.scope(
|
||||||
)
|
state: \.feedState,
|
||||||
|
action: \.feed
|
||||||
|
),
|
||||||
|
onShowCreateFeed: {
|
||||||
|
store.send(.showCreateFeed)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
case .me:
|
case .me:
|
||||||
MeView(onLogout: onLogout)
|
MeView(onLogout: onLogout)
|
||||||
@@ -47,11 +52,27 @@ struct HomeView: View {
|
|||||||
store.send(.onAppear)
|
store.send(.onAppear)
|
||||||
}
|
}
|
||||||
.sheet(isPresented: Binding(
|
.sheet(isPresented: Binding(
|
||||||
get: { store.isSettingPresented },
|
get: { store.withState(\.isSettingPresented) },
|
||||||
set: { _ in store.send(.settingDismissed) }
|
set: { _ in store.send(.settingDismissed) }
|
||||||
)) {
|
)) {
|
||||||
SettingView(store: store.scope(state: \.settingState, action: \.setting))
|
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:
|
case .main:
|
||||||
// 显示主应用页面
|
// 显示主应用页面
|
||||||
HomeView(
|
MainView(
|
||||||
store: Store(
|
store: Store(
|
||||||
initialState: HomeFeature.State()
|
initialState: MainFeature.State()
|
||||||
) {
|
) {
|
||||||
HomeFeature()
|
MainFeature()
|
||||||
},
|
|
||||||
onLogout: {
|
|
||||||
// 登出时重新导航到登录页面
|
|
||||||
store.send(.navigateToLogin)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user