feat: 新增动态点赞与删除功能
- 在APIEndpoints中新增动态点赞和删除端点。 - 实现LikeDynamicRequest和DeleteDynamicRequest结构体,支持动态点赞和删除请求。 - 在DetailFeature中添加点赞和删除动态的逻辑,提升用户交互体验。 - 更新FeedListFeature以支持动态详情视图的展示,增强用户体验。 - 新增DetailView以展示动态详情,包含点赞和删除功能。
This commit is contained in:
@@ -25,6 +25,8 @@ enum APIEndpoint: String, CaseIterable {
|
||||
case getUserInfo = "/user/get" // 新增:获取用户信息端点
|
||||
case getMyDynamic = "/dynamic/getMyDynamic"
|
||||
case updateUser = "/user/v2/update" // 新增:用户信息更新端点
|
||||
case dynamicLike = "/dynamic/like" // 新增:动态点赞/取消点赞端点
|
||||
case deleteDynamic = "/dynamic/delete" // 新增:删除动态端点
|
||||
|
||||
// Web 页面路径
|
||||
case userAgreement = "/modules/rule/protocol.html"
|
||||
|
@@ -281,3 +281,101 @@ struct GetMyDynamicRequest: APIRequestProtocol {
|
||||
var shouldShowLoading: Bool { true }
|
||||
var shouldShowError: Bool { true }
|
||||
}
|
||||
|
||||
// MARK: - 动态点赞 API 请求与响应
|
||||
|
||||
/// 动态点赞响应结构
|
||||
struct LikeDynamicResponse: Codable, Equatable, Sendable {
|
||||
let code: Int
|
||||
let message: String
|
||||
let data: LikeDynamicData?
|
||||
let timestamp: Int?
|
||||
}
|
||||
|
||||
/// 动态点赞返回数据
|
||||
struct LikeDynamicData: Codable, Equatable, Sendable {
|
||||
let success: Bool?
|
||||
let likeCount: Int?
|
||||
}
|
||||
|
||||
/// 动态点赞请求
|
||||
struct LikeDynamicRequest: APIRequestProtocol {
|
||||
typealias Response = LikeDynamicResponse
|
||||
|
||||
let endpoint: String = APIEndpoint.dynamicLike.path
|
||||
let method: HTTPMethod = .POST
|
||||
|
||||
let dynamicId: Int
|
||||
let uid: Int
|
||||
let status: Int // 0: 取消点赞, 1: 点赞
|
||||
let likedUid: Int
|
||||
let worldId: Int
|
||||
|
||||
init(dynamicId: Int, uid: Int, status: Int, likedUid: Int, worldId: Int) {
|
||||
self.dynamicId = dynamicId
|
||||
self.uid = uid
|
||||
self.status = status
|
||||
self.likedUid = likedUid
|
||||
self.worldId = worldId
|
||||
}
|
||||
|
||||
var queryParameters: [String: String]? { nil }
|
||||
|
||||
var bodyParameters: [String: Any]? {
|
||||
return [
|
||||
"dynamicId": dynamicId,
|
||||
"uid": uid,
|
||||
"status": status,
|
||||
"likedUid": likedUid,
|
||||
"worldId": worldId
|
||||
]
|
||||
}
|
||||
|
||||
var includeBaseParameters: Bool { true }
|
||||
var shouldShowLoading: Bool { true }
|
||||
var shouldShowError: Bool { true }
|
||||
}
|
||||
|
||||
// MARK: - 删除动态 API 请求与响应
|
||||
|
||||
/// 删除动态响应结构
|
||||
struct DeleteDynamicResponse: Codable, Equatable, Sendable {
|
||||
let code: Int
|
||||
let message: String
|
||||
let data: DeleteDynamicData?
|
||||
let timestamp: Int?
|
||||
}
|
||||
|
||||
/// 删除动态返回数据
|
||||
struct DeleteDynamicData: Codable, Equatable, Sendable {
|
||||
let success: Bool?
|
||||
}
|
||||
|
||||
/// 删除动态请求
|
||||
struct DeleteDynamicRequest: APIRequestProtocol {
|
||||
typealias Response = DeleteDynamicResponse
|
||||
|
||||
let endpoint: String = APIEndpoint.deleteDynamic.path
|
||||
let method: HTTPMethod = .POST
|
||||
|
||||
let dynamicId: Int
|
||||
let uid: Int
|
||||
|
||||
init(dynamicId: Int, uid: Int) {
|
||||
self.dynamicId = dynamicId
|
||||
self.uid = uid
|
||||
}
|
||||
|
||||
var queryParameters: [String: String]? { nil }
|
||||
|
||||
var bodyParameters: [String: Any]? {
|
||||
return [
|
||||
"dynamicId": dynamicId,
|
||||
"uid": uid
|
||||
]
|
||||
}
|
||||
|
||||
var includeBaseParameters: Bool { true }
|
||||
var shouldShowLoading: Bool { true }
|
||||
var shouldShowError: Bool { true }
|
||||
}
|
||||
|
145
yana/Features/DetailFeature.swift
Normal file
145
yana/Features/DetailFeature.swift
Normal file
@@ -0,0 +1,145 @@
|
||||
import Foundation
|
||||
import ComposableArchitecture
|
||||
|
||||
@Reducer
|
||||
struct DetailFeature {
|
||||
@Dependency(\.apiService) var apiService
|
||||
|
||||
@ObservableState
|
||||
struct State: Equatable {
|
||||
var moment: MomentsInfo
|
||||
var isLikeLoading = false
|
||||
var isDeleteLoading = false
|
||||
var showImagePreview = false
|
||||
var selectedImageIndex = 0
|
||||
var selectedImages: [String] = []
|
||||
|
||||
init(moment: MomentsInfo) {
|
||||
self.moment = moment
|
||||
}
|
||||
}
|
||||
|
||||
enum Action: Equatable {
|
||||
case onAppear
|
||||
case likeDynamic(Int, Int, Int, Int) // dynamicId, uid, likedUid, worldId
|
||||
case likeResponse(TaskResult<LikeDynamicResponse>)
|
||||
case deleteDynamic
|
||||
case deleteResponse(TaskResult<DeleteDynamicResponse>)
|
||||
case showImagePreview([String], Int)
|
||||
case hideImagePreview
|
||||
case imagePreviewDismissed
|
||||
case onLikeSuccess(Int, Bool) // dynamicId, newLikeState
|
||||
}
|
||||
|
||||
var body: some ReducerOf<Self> {
|
||||
Reduce { state, action in
|
||||
switch action {
|
||||
case .onAppear:
|
||||
return .none
|
||||
|
||||
case let .likeDynamic(dynamicId, uid, likedUid, worldId):
|
||||
state.isLikeLoading = true
|
||||
|
||||
let status = state.moment.isLike ? 0 : 1
|
||||
let request = LikeDynamicRequest(
|
||||
dynamicId: dynamicId,
|
||||
uid: uid,
|
||||
status: status,
|
||||
likedUid: likedUid,
|
||||
worldId: worldId
|
||||
)
|
||||
|
||||
return .run { send in
|
||||
let result = await TaskResult {
|
||||
try await apiService.request(request)
|
||||
}
|
||||
await send(.likeResponse(result))
|
||||
}
|
||||
|
||||
case let .likeResponse(.success(response)):
|
||||
state.isLikeLoading = false
|
||||
// 点赞成功,通知父视图更新状态
|
||||
return .send(.onLikeSuccess(state.moment.dynamicId, !state.moment.isLike))
|
||||
|
||||
case let .onLikeSuccess(dynamicId, newLikeState):
|
||||
// 更新本地动态的点赞状态
|
||||
// 由于MomentsInfo的isLike是let,我们需要重新创建moment对象
|
||||
let updatedMoment = MomentsInfo(
|
||||
dynamicId: state.moment.dynamicId,
|
||||
uid: state.moment.uid,
|
||||
nick: state.moment.nick,
|
||||
avatar: state.moment.avatar,
|
||||
type: state.moment.type,
|
||||
content: state.moment.content,
|
||||
likeCount: state.moment.likeCount,
|
||||
isLike: newLikeState,
|
||||
commentCount: state.moment.commentCount,
|
||||
publishTime: state.moment.publishTime,
|
||||
worldId: state.moment.worldId,
|
||||
status: state.moment.status,
|
||||
playCount: state.moment.playCount,
|
||||
dynamicResList: state.moment.dynamicResList,
|
||||
gender: state.moment.gender,
|
||||
squareTop: state.moment.squareTop,
|
||||
topicTop: state.moment.topicTop,
|
||||
newUser: state.moment.newUser,
|
||||
defUser: state.moment.defUser,
|
||||
scene: state.moment.scene,
|
||||
userVipInfoVO: state.moment.userVipInfoVO,
|
||||
headwearPic: state.moment.headwearPic,
|
||||
headwearEffect: state.moment.headwearEffect,
|
||||
headwearType: state.moment.headwearType,
|
||||
headwearName: state.moment.headwearName,
|
||||
headwearId: state.moment.headwearId,
|
||||
experLevelPic: state.moment.experLevelPic,
|
||||
charmLevelPic: state.moment.charmLevelPic,
|
||||
isCustomWord: state.moment.isCustomWord,
|
||||
labelList: state.moment.labelList
|
||||
)
|
||||
state.moment = updatedMoment
|
||||
return .none
|
||||
|
||||
case let .likeResponse(.failure(error)):
|
||||
state.isLikeLoading = false
|
||||
// 可以在这里处理错误,比如显示错误提示
|
||||
return .none
|
||||
|
||||
case .deleteDynamic:
|
||||
state.isDeleteLoading = true
|
||||
|
||||
let request = DeleteDynamicRequest(dynamicId: state.moment.dynamicId, uid: state.moment.uid)
|
||||
|
||||
return .run { send in
|
||||
let result = await TaskResult {
|
||||
try await apiService.request(request)
|
||||
}
|
||||
await send(.deleteResponse(result))
|
||||
}
|
||||
|
||||
case let .deleteResponse(.success(response)):
|
||||
state.isDeleteLoading = false
|
||||
// 删除成功,返回上一页
|
||||
return .none
|
||||
|
||||
case let .deleteResponse(.failure(error)):
|
||||
state.isDeleteLoading = false
|
||||
// 可以在这里处理错误
|
||||
return .none
|
||||
|
||||
case let .showImagePreview(images, index):
|
||||
state.selectedImages = images
|
||||
state.selectedImageIndex = index
|
||||
state.showImagePreview = true
|
||||
return .none
|
||||
|
||||
case .hideImagePreview:
|
||||
state.showImagePreview = false
|
||||
return .none
|
||||
|
||||
case .imagePreviewDismissed:
|
||||
state.showImagePreview = false
|
||||
return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,111 +0,0 @@
|
||||
import Foundation
|
||||
import ComposableArchitecture
|
||||
|
||||
@Reducer
|
||||
struct FeedFeature {
|
||||
@ObservableState
|
||||
struct State: Equatable {
|
||||
var moments: [MomentsInfo] = []
|
||||
var isLoading = false
|
||||
var isRefreshing = false
|
||||
var hasMoreData = true
|
||||
var error: String?
|
||||
var nextDynamicId: Int = 0
|
||||
// CreateFeedView 相关状态
|
||||
var createFeedState = CreateFeedFeature.State()
|
||||
}
|
||||
|
||||
enum Action: Equatable {
|
||||
case onAppear
|
||||
case refresh
|
||||
case loadLatestMoments
|
||||
case loadMoreMoments
|
||||
case momentsResponse(TaskResult<MomentsLatestResponse>)
|
||||
case clearError
|
||||
case retryLoad
|
||||
// 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.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 }
|
||||
state.isLoading = 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 .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])
|
||||
return .run { send in
|
||||
await send(.momentsResponse(TaskResult { try await apiService.request(request) }))
|
||||
}
|
||||
case let .momentsResponse(.success(response)):
|
||||
state.isLoading = false
|
||||
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 || 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)
|
||||
}
|
||||
case .createFeedCompleted:
|
||||
return .send(.refresh)
|
||||
case .createFeedDismissed:
|
||||
return .none
|
||||
case .createFeed(.dismissView):
|
||||
return .send(.createFeedDismissed)
|
||||
case .createFeed:
|
||||
return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -18,6 +18,9 @@ struct FeedListFeature {
|
||||
var currentPage: Int = 1
|
||||
var hasMore: Bool = true
|
||||
var isLoadingMore: Bool = false
|
||||
// 新增:DetailView相关状态
|
||||
var showDetail: Bool = false
|
||||
var selectedMoment: MomentsInfo?
|
||||
}
|
||||
|
||||
enum Action: Equatable {
|
||||
@@ -31,6 +34,9 @@ struct FeedListFeature {
|
||||
// 新增:动态内容相关
|
||||
case fetchFeeds
|
||||
case fetchFeedsResponse(TaskResult<MomentsLatestResponse>)
|
||||
// 新增:DetailView相关Action
|
||||
case showDetail(MomentsInfo)
|
||||
case detailDismissed
|
||||
// 预留后续 Action
|
||||
}
|
||||
|
||||
@@ -129,6 +135,14 @@ struct FeedListFeature {
|
||||
case .testButtonTapped:
|
||||
debugInfoSync("[LOG] FeedListFeature testButtonTapped")
|
||||
return .none
|
||||
case let .showDetail(moment):
|
||||
state.selectedMoment = moment
|
||||
state.showDetail = true
|
||||
return .none
|
||||
case .detailDismissed:
|
||||
state.showDetail = false
|
||||
state.selectedMoment = nil
|
||||
return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,92 +0,0 @@
|
||||
import Foundation
|
||||
import ComposableArchitecture
|
||||
|
||||
struct HomeFeature: Reducer {
|
||||
enum Route: Equatable {
|
||||
case createFeed
|
||||
}
|
||||
|
||||
struct State: Equatable {
|
||||
var isInitialized = false
|
||||
var userInfo: UserInfo?
|
||||
var accountModel: AccountModel?
|
||||
var error: String?
|
||||
var feedState = FeedFeature.State()
|
||||
var meDynamic = MeDynamicFeature.State(uid: 0)
|
||||
var isLoggedOut = false
|
||||
var route: Route? = nil
|
||||
}
|
||||
|
||||
@CasePathable
|
||||
enum Action: Equatable {
|
||||
case onAppear
|
||||
case loadUserInfo
|
||||
case userInfoLoaded(UserInfo?)
|
||||
case loadAccountModel
|
||||
case accountModelLoaded(AccountModel?)
|
||||
case logoutTapped
|
||||
case logout
|
||||
case feed(FeedFeature.Action)
|
||||
case meDynamic(MeDynamicFeature.Action)
|
||||
case logoutCompleted
|
||||
case showCreateFeed
|
||||
case createFeedDismissed
|
||||
}
|
||||
|
||||
var body: some ReducerOf<Self> {
|
||||
Scope(state: \.feedState, action: \.feed) {
|
||||
FeedFeature()
|
||||
}
|
||||
Scope(state: \.meDynamic, action: \.meDynamic) {
|
||||
MeDynamicFeature()
|
||||
}
|
||||
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
|
||||
state.meDynamic.uid = userInfo?.uid ?? 0
|
||||
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 .feed:
|
||||
return .none
|
||||
case .meDynamic:
|
||||
return .none
|
||||
case .showCreateFeed:
|
||||
state.route = .createFeed
|
||||
return .none
|
||||
case .createFeedDismissed:
|
||||
state.route = nil
|
||||
return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -11,8 +11,6 @@ struct LoginFeature {
|
||||
var error: String?
|
||||
var idLoginState = IDLoginFeature.State()
|
||||
var emailLoginState = EMailLoginFeature.State() // 新增:邮箱登录状态
|
||||
// 新增:HomeFeature 状态
|
||||
var homeState = HomeFeature.State()
|
||||
|
||||
// 新增:Account Model 和 Ticket 相关状态
|
||||
var accountModel: AccountModel?
|
||||
@@ -54,7 +52,6 @@ struct LoginFeature {
|
||||
case idLogin(IDLoginFeature.Action)
|
||||
case emailLogin(EMailLoginFeature.Action) // 新增:邮箱登录action
|
||||
// 新增:HomeFeature action
|
||||
case home(HomeFeature.Action)
|
||||
// 新增:Ticket 相关 actions
|
||||
case requestTicket(accessToken: String)
|
||||
case ticketResponse(TaskResult<TicketResponse>)
|
||||
@@ -72,10 +69,6 @@ struct LoginFeature {
|
||||
Scope(state: \.emailLoginState, action: \.emailLogin) {
|
||||
EMailLoginFeature()
|
||||
}
|
||||
// 新增:HomeFeature 作用域
|
||||
Scope(state: \.homeState, action: \.home) {
|
||||
HomeFeature()
|
||||
}
|
||||
|
||||
Reduce { state, action in
|
||||
switch action {
|
||||
@@ -241,8 +234,6 @@ struct LoginFeature {
|
||||
case .emailLogin:
|
||||
// EmailLogin动作由子feature处理
|
||||
return .none
|
||||
case .home(_):
|
||||
return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -129,4 +129,7 @@
|
||||
"appSetting.checkUpdates" = "Check for Updates";
|
||||
"appSetting.logout" = "Log Out";
|
||||
"appSetting.aboutUs" = "About Us";
|
||||
"appSetting.logoutAccount" = "Log out of account";
|
||||
"appSetting.logoutAccount" = "Log out of account";
|
||||
|
||||
// MARK: - Detail
|
||||
"detail.title" = "Enjoy your life";
|
@@ -126,3 +126,6 @@
|
||||
"appSetting.logout" = "退出登录";
|
||||
"appSetting.aboutUs" = "关于我们";
|
||||
"appSetting.logoutAccount" = "退出账户";
|
||||
|
||||
// MARK: - Detail
|
||||
"detail.title" = "享受你的生活";
|
||||
|
@@ -9,12 +9,21 @@ struct OptimizedDynamicCardView: View {
|
||||
let currentIndex: Int
|
||||
// 新增:图片点击回调
|
||||
let onImageTap: (_ images: [String], _ index: Int) -> Void
|
||||
// 新增:点赞回调
|
||||
let onLikeTap: (_ dynamicId: Int, _ uid: Int, _ likedUid: Int, _ worldId: Int) -> Void
|
||||
// 新增:点赞loading状态
|
||||
let isLikeLoading: Bool
|
||||
// 新增:详情页模式,点击卡片不跳转
|
||||
let isDetailMode: Bool
|
||||
|
||||
init(moment: MomentsInfo, allMoments: [MomentsInfo], currentIndex: Int, onImageTap: @escaping (_ images: [String], _ index: Int) -> Void) {
|
||||
init(moment: MomentsInfo, allMoments: [MomentsInfo], currentIndex: Int, onImageTap: @escaping (_ images: [String], _ index: Int) -> Void, onLikeTap: @escaping (_ dynamicId: Int, _ uid: Int, _ likedUid: Int, _ worldId: Int) -> Void, isLikeLoading: Bool = false, isDetailMode: Bool = false) {
|
||||
self.moment = moment
|
||||
self.allMoments = allMoments
|
||||
self.currentIndex = currentIndex
|
||||
self.onImageTap = onImageTap
|
||||
self.onLikeTap = onLikeTap
|
||||
self.isLikeLoading = isLikeLoading
|
||||
self.isDetailMode = isDetailMode
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
@@ -27,6 +36,7 @@ struct OptimizedDynamicCardView: View {
|
||||
.stroke(Color.white.opacity(0.1), lineWidth: 1)
|
||||
)
|
||||
.shadow(color: Color(red: 0.43, green: 0.43, blue: 0.43, opacity: 0.34), radius: 10.7, x: 0, y: 1.9)
|
||||
|
||||
// 内容层
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
// 用户信息
|
||||
@@ -88,21 +98,36 @@ struct OptimizedDynamicCardView: View {
|
||||
// 互动按钮
|
||||
HStack(spacing: 20) {
|
||||
// Like 按钮左对齐
|
||||
Button(action: {}) {
|
||||
Button(action: {
|
||||
onLikeTap(moment.dynamicId, moment.uid, moment.uid, moment.worldId)
|
||||
}) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: moment.isLike ? "heart.fill" : "heart")
|
||||
.font(.system(size: 16))
|
||||
if isLikeLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: moment.isLike ? .red : .white.opacity(0.8)))
|
||||
.scaleEffect(0.8)
|
||||
} else {
|
||||
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))
|
||||
}
|
||||
.disabled(isLikeLoading)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top, 8)
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
.onTapGesture {
|
||||
// 只有在非详情页模式下才允许点击跳转
|
||||
if !isDetailMode {
|
||||
// 点击事件由父视图处理
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
preloadNearbyImages()
|
||||
}
|
||||
|
121
yana/Views/DetailView.swift
Normal file
121
yana/Views/DetailView.swift
Normal file
@@ -0,0 +1,121 @@
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
|
||||
struct DetailView: View {
|
||||
@State var store: StoreOf<DetailFeature>
|
||||
// @Environment(\.dismiss) private var dismiss
|
||||
let onLikeSuccess: ((Int, Bool) -> Void)?
|
||||
|
||||
init(store: StoreOf<DetailFeature>, onLikeSuccess: ((Int, Bool) -> Void)? = nil) {
|
||||
self.store = store
|
||||
self.onLikeSuccess = onLikeSuccess
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
// 使用OptimizedDynamicCardView显示动态内容
|
||||
OptimizedDynamicCardView(
|
||||
moment: store.moment,
|
||||
allMoments: [store.moment], // 详情页只有一个动态
|
||||
currentIndex: 0,
|
||||
onImageTap: { images, index in
|
||||
store.send(.showImagePreview(images, index))
|
||||
},
|
||||
onLikeTap: { dynamicId, uid, likedUid, worldId in
|
||||
store.send(.likeDynamic(dynamicId, uid, likedUid, worldId))
|
||||
},
|
||||
isLikeLoading: store.isLikeLoading,
|
||||
isDetailMode: true // 详情页模式,点击卡片不跳转
|
||||
)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 16)
|
||||
}
|
||||
}
|
||||
.navigationTitle(NSLocalizedString("detail.title", comment: "Detail page title"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
// 只有当动态的uid与当前登录用户uid相同时才显示删除按钮
|
||||
if isCurrentUserDynamic {
|
||||
Button(action: {
|
||||
store.send(.deleteDynamic)
|
||||
}) {
|
||||
if store.isDeleteLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .red))
|
||||
.scaleEffect(0.8)
|
||||
} else {
|
||||
Image(systemName: "trash")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
.disabled(store.isDeleteLoading)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
store.send(.onAppear)
|
||||
}
|
||||
// .onReceive(store.publisher(for: \.moment)) { moment in
|
||||
// // 监听动态状态变化
|
||||
// }
|
||||
.fullScreenCover(isPresented: Binding(
|
||||
get: { store.showImagePreview },
|
||||
set: { _ in store.send(.hideImagePreview) }
|
||||
)) {
|
||||
ImagePreviewPager(
|
||||
images: store.selectedImages,
|
||||
currentIndex: Binding(
|
||||
get: { store.selectedImageIndex },
|
||||
set: { newIndex in
|
||||
store.send(.showImagePreview(store.selectedImages, newIndex))
|
||||
}
|
||||
),
|
||||
onClose: {
|
||||
store.send(.imagePreviewDismissed)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否为当前用户的动态
|
||||
private var isCurrentUserDynamic: Bool {
|
||||
// 暂时返回false,避免异步调用问题
|
||||
// TODO: 实现异步获取用户ID的逻辑
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
//#Preview {
|
||||
// DetailView(
|
||||
// store: Store(
|
||||
// initialState: DetailFeature.State(
|
||||
// moment: MomentsInfo(
|
||||
// dynamicId: 1,
|
||||
// uid: 123,
|
||||
// nick: "Test User",
|
||||
// avatar: "https://example.com/avatar.jpg",
|
||||
// type: 1,
|
||||
// content: "This is a test dynamic content",
|
||||
// publishTime: Int(Date().timeIntervalSince1970 * 1000),
|
||||
// likeCount: 10,
|
||||
// isLike: false,
|
||||
// worldId: 1,
|
||||
// dynamicResList: [
|
||||
// MomentsPicture(
|
||||
// id: 1,
|
||||
// resUrl: "https://example.com/image1.jpg",
|
||||
// format: "jpg",
|
||||
// width: 800,
|
||||
// height: 600,
|
||||
// resDuration: nil
|
||||
// )
|
||||
// ]
|
||||
// )
|
||||
// )
|
||||
// ) {
|
||||
// DetailFeature()
|
||||
// }
|
||||
// )
|
||||
//}
|
@@ -1,6 +1,196 @@
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
|
||||
// MARK: - BackgroundView
|
||||
struct BackgroundView: View {
|
||||
var body: some View {
|
||||
Image("bg")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.clipped()
|
||||
.ignoresSafeArea(.all)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - TopBarView
|
||||
struct TopBarView: View {
|
||||
let onEditTapped: () -> Void
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
HStack {
|
||||
Spacer(minLength: 0)
|
||||
Text(NSLocalizedString("feedList.title", comment: "Enjoy your Life Time"))
|
||||
.font(.system(size: 22, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
HStack {
|
||||
Spacer(minLength: 0)
|
||||
Button(action: onEditTapped) {
|
||||
Image("add icon")
|
||||
.resizable()
|
||||
.frame(width: 40, height: 40)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - LoadingView
|
||||
private struct LoadingView: View {
|
||||
var body: some View {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.padding(.top, 20)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ErrorView
|
||||
struct ErrorView: View {
|
||||
let error: String
|
||||
|
||||
var body: some View {
|
||||
Text(error)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.red)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 20)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - EmptyView
|
||||
struct EmptyView: View {
|
||||
var body: some View {
|
||||
Text(NSLocalizedString("feedList.empty", comment: "暂无动态"))
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
.padding(.top, 20)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MomentCardView
|
||||
struct MomentCardView: View {
|
||||
let moment: MomentsInfo
|
||||
let allMoments: [MomentsInfo]
|
||||
let index: Int
|
||||
let onImageTap: ([String], Int) -> Void
|
||||
let onTap: () -> Void
|
||||
let onLoadMore: () -> Void
|
||||
let isLastItem: Bool
|
||||
let hasMore: Bool
|
||||
let isLoadingMore: Bool
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
OptimizedDynamicCardView(
|
||||
moment: moment,
|
||||
allMoments: allMoments,
|
||||
currentIndex: index,
|
||||
onImageTap: onImageTap,
|
||||
onLikeTap: { _, _,_,_ in }
|
||||
)
|
||||
.onTapGesture {
|
||||
onTap()
|
||||
}
|
||||
|
||||
// 上拉加载更多触发点
|
||||
if isLastItem && hasMore && !isLoadingMore {
|
||||
Color.clear
|
||||
.frame(height: 1)
|
||||
.onAppear {
|
||||
onLoadMore()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MomentsListView
|
||||
struct MomentsListView: View {
|
||||
let moments: [MomentsInfo]
|
||||
let hasMore: Bool
|
||||
let isLoadingMore: Bool
|
||||
let onImageTap: ([String], Int) -> Void
|
||||
let onMomentTap: (MomentsInfo) -> Void
|
||||
let onLoadMore: () -> Void
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 16) {
|
||||
ForEach(Array(moments.enumerated()), id: \.element.dynamicId) { index, moment in
|
||||
MomentCardView(
|
||||
moment: moment,
|
||||
allMoments: moments,
|
||||
index: index,
|
||||
onImageTap: onImageTap,
|
||||
onTap: {
|
||||
onMomentTap(moment)
|
||||
},
|
||||
onLoadMore: onLoadMore,
|
||||
isLastItem: index == moments.count - 1,
|
||||
hasMore: hasMore,
|
||||
isLoadingMore: isLoadingMore
|
||||
)
|
||||
}
|
||||
|
||||
// 加载更多指示器
|
||||
if isLoadingMore {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
|
||||
// 新增底部间距
|
||||
Color.clear.frame(height: 120)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
.refreshable {
|
||||
onLoadMore()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - FeedListContentView
|
||||
struct FeedListContentView: View {
|
||||
let store: StoreOf<FeedListFeature>
|
||||
@Binding var previewItem: PreviewItem?
|
||||
|
||||
var body: some View {
|
||||
WithViewStore(self.store, observe: { $0 }) { viewStore in
|
||||
if viewStore.isLoading {
|
||||
LoadingView()
|
||||
} else if let error = viewStore.error {
|
||||
ErrorView(error: error)
|
||||
} else if viewStore.moments.isEmpty {
|
||||
EmptyView()
|
||||
} else {
|
||||
MomentsListView(
|
||||
moments: viewStore.moments,
|
||||
hasMore: viewStore.hasMore,
|
||||
isLoadingMore: viewStore.isLoadingMore,
|
||||
onImageTap: { images, tappedIndex in
|
||||
previewItem = PreviewItem(images: images, index: tappedIndex)
|
||||
},
|
||||
onMomentTap: { moment in
|
||||
viewStore.send(.showDetail(moment))
|
||||
},
|
||||
onLoadMore: {
|
||||
viewStore.send(.loadMore)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FeedListView: View {
|
||||
let store: StoreOf<FeedListFeature>
|
||||
// 新增:图片预览状态
|
||||
@@ -10,137 +200,86 @@ struct FeedListView: View {
|
||||
WithViewStore(self.store, observe: { $0 }) { viewStore in
|
||||
WithPerceptionTracking {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
// 背景图片
|
||||
Image("bg")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.clipped()
|
||||
.ignoresSafeArea(.all)
|
||||
VStack(alignment: .center, spacing: 0) {
|
||||
// 顶部栏
|
||||
ZStack {
|
||||
HStack {
|
||||
Spacer(minLength: 0)
|
||||
Text(NSLocalizedString("feedList.title", comment: "Enjoy your Life Time"))
|
||||
.font(.system(size: 22, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
HStack {
|
||||
Spacer(minLength: 0)
|
||||
Button(action: {
|
||||
viewStore.send(.editFeedButtonTapped)
|
||||
}) {
|
||||
Image("add icon")
|
||||
.resizable()
|
||||
.frame(width: 40, height: 40)
|
||||
}
|
||||
}
|
||||
}
|
||||
ZStack {
|
||||
// 背景
|
||||
BackgroundView()
|
||||
|
||||
.padding(.horizontal, 20)
|
||||
// 其他内容
|
||||
Image("Volume")
|
||||
.frame(width: 56, height: 41)
|
||||
.padding(.top, 16)
|
||||
Text(NSLocalizedString("feedList.slogan", comment: "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(.leading)
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
.padding(.horizontal, 30)
|
||||
.padding(.bottom, 30)
|
||||
// 新增:动态内容列表
|
||||
if viewStore.isLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.padding(.top, 20)
|
||||
} else if let error = viewStore.error {
|
||||
Text(error)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.red)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 20)
|
||||
} else if viewStore.moments.isEmpty {
|
||||
Text(NSLocalizedString("feedList.empty", comment: "暂无动态"))
|
||||
VStack(alignment: .center, spacing: 0) {
|
||||
// 顶部栏
|
||||
TopBarView {
|
||||
viewStore.send(.editFeedButtonTapped)
|
||||
}
|
||||
|
||||
// 其他内容
|
||||
Image("Volume")
|
||||
.frame(width: 56, height: 41)
|
||||
.padding(.top, 16)
|
||||
|
||||
Text(NSLocalizedString("feedList.slogan", comment: "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))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
.padding(.top, 20)
|
||||
} else {
|
||||
ScrollView {
|
||||
WithPerceptionTracking {
|
||||
LazyVStack(spacing: 16) {
|
||||
ForEach(Array(viewStore.moments.enumerated()), id: \ .element.dynamicId) { index, moment in
|
||||
OptimizedDynamicCardView(
|
||||
moment: moment,
|
||||
allMoments: viewStore.moments,
|
||||
currentIndex: index,
|
||||
onImageTap: { images, tappedIndex in
|
||||
previewItem = PreviewItem(images: images, index: tappedIndex)
|
||||
}
|
||||
)
|
||||
// 上拉加载更多触发点
|
||||
if index == viewStore.moments.count - 1, viewStore.hasMore, !viewStore.isLoadingMore {
|
||||
Color.clear
|
||||
.frame(height: 1)
|
||||
.onAppear {
|
||||
viewStore.send(.loadMore)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 加载更多指示器
|
||||
if viewStore.isLoadingMore {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
// 新增底部间距
|
||||
Color.clear.frame(height: 120)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
}
|
||||
.refreshable {
|
||||
viewStore.send(.reload)
|
||||
}
|
||||
.multilineTextAlignment(.leading)
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
.padding(.horizontal, 30)
|
||||
.padding(.bottom, 30)
|
||||
|
||||
// 动态内容列表
|
||||
FeedListContentView(
|
||||
store: store,
|
||||
previewItem: $previewItem
|
||||
)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
Spacer()
|
||||
.frame(maxWidth: .infinity, alignment: .top)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .top)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewStore.send(.onAppear)
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("reloadFeedList"))) { _ in
|
||||
viewStore.send(.reload)
|
||||
}
|
||||
.sheet(isPresented: viewStore.binding(
|
||||
get: \.isEditFeedPresented,
|
||||
send: { $0 ? .editFeedButtonTapped : .editFeedDismissed }
|
||||
)) {
|
||||
EditFeedView(
|
||||
onDismiss: {
|
||||
viewStore.send(.editFeedDismissed)
|
||||
},
|
||||
store: Store(
|
||||
initialState: EditFeedFeature.State()
|
||||
) {
|
||||
EditFeedFeature()
|
||||
.onAppear {
|
||||
viewStore.send(.onAppear)
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("reloadFeedList"))) { _ in
|
||||
viewStore.send(.reload)
|
||||
}
|
||||
.sheet(isPresented: viewStore.binding(
|
||||
get: \.isEditFeedPresented,
|
||||
send: { $0 ? .editFeedButtonTapped : .editFeedDismissed }
|
||||
)) {
|
||||
EditFeedView(
|
||||
onDismiss: {
|
||||
viewStore.send(.editFeedDismissed)
|
||||
},
|
||||
store: Store(
|
||||
initialState: EditFeedFeature.State()
|
||||
) {
|
||||
EditFeedFeature()
|
||||
}
|
||||
)
|
||||
}
|
||||
// 新增:图片预览弹窗
|
||||
.fullScreenCover(item: $previewItem) { item in
|
||||
ImagePreviewPager(images: item.images, currentIndex: .constant(item.index)) {
|
||||
previewItem = nil
|
||||
}
|
||||
}
|
||||
// 新增:DetailView导航
|
||||
.navigationDestination(isPresented: viewStore.binding(
|
||||
get: \.showDetail,
|
||||
send: { isPresented in
|
||||
if !isPresented {
|
||||
return .detailDismissed
|
||||
}
|
||||
return .detailDismissed
|
||||
}
|
||||
)) {
|
||||
if let selectedMoment = viewStore.selectedMoment {
|
||||
DetailView(
|
||||
store: Store(
|
||||
initialState: DetailFeature.State(moment: selectedMoment)
|
||||
) {
|
||||
DetailFeature()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
// 新增:图片预览弹窗
|
||||
.fullScreenCover(item: $previewItem) { item in
|
||||
ImagePreviewPager(images: item.images, currentIndex: .constant(item.index)) {
|
||||
previewItem = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,636 +0,0 @@
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
|
||||
struct FeedTopBarView: View {
|
||||
let store: StoreOf<FeedFeature>
|
||||
let onShowCreateFeed: () -> Void
|
||||
var body: some View {
|
||||
WithPerceptionTracking {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text(NSLocalizedString("feed.title", comment: "Enjoy your Life Time"))
|
||||
.font(.system(size: 22, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
Spacer()
|
||||
Button(action: {
|
||||
// showEditFeed = true // 显示编辑界面
|
||||
}) {
|
||||
Image("add icon")
|
||||
.frame(width: 36, height: 36)
|
||||
}
|
||||
}
|
||||
.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) {
|
||||
Image(systemName: "heart.text.square")
|
||||
.font(.system(size: 40))
|
||||
.foregroundColor(.white.opacity(0.6))
|
||||
Text(NSLocalizedString("feed.empty", comment: "No moments yet"))
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
if let error = store.error {
|
||||
Text(String(format: NSLocalizedString("feed.error", comment: "Error: %@"), 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(NSLocalizedString("feed.retry", comment: "Retry"))
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载更多指示器
|
||||
if store.isLoading && !store.moments.isEmpty {
|
||||
HStack {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
Text(NSLocalizedString("feed.loadingMore", comment: "Loading more..."))
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
.padding(.top, 20)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 20) // 调整顶部间距
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FeedView: View {
|
||||
let store: StoreOf<FeedFeature>
|
||||
let onShowCreateFeed: () -> Void
|
||||
@State private var showEditFeed = false
|
||||
|
||||
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: $showEditFeed) {
|
||||
EditFeedView(
|
||||
onDismiss: {
|
||||
showEditFeed = false
|
||||
},
|
||||
store: Store(
|
||||
initialState: EditFeedFeature.State()
|
||||
) {
|
||||
EditFeedFeature()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 优化的动态卡片组件
|
||||
//struct OptimizedDynamicCardView: View {
|
||||
// let moment: MomentsInfo
|
||||
// let allMoments: [MomentsInfo]
|
||||
// let currentIndex: Int
|
||||
//
|
||||
// var body: some View {
|
||||
// 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())
|
||||
//
|
||||
// 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))
|
||||
// }
|
||||
//
|
||||
// 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))
|
||||
// }
|
||||
// .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(16)
|
||||
// .background(
|
||||
// Color.white.opacity(0.1)
|
||||
// .cornerRadius(12)
|
||||
// )
|
||||
// .onAppear {
|
||||
// // 预加载相邻的图片
|
||||
// preloadNearbyImages()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func formatTime(_ timestamp: Int) -> String {
|
||||
// let date = Date(timeIntervalSince1970: TimeInterval(timestamp) / 1000.0)
|
||||
// let formatter = DateFormatter()
|
||||
// formatter.locale = Locale(identifier: "zh_CN")
|
||||
//
|
||||
// let now = Date()
|
||||
// let interval = now.timeIntervalSince(date)
|
||||
//
|
||||
// if interval < 60 {
|
||||
// return "刚刚"
|
||||
// } else if interval < 3600 {
|
||||
// return "\(Int(interval / 60))分钟前"
|
||||
// } else if interval < 86400 {
|
||||
// return "\(Int(interval / 3600))小时前"
|
||||
// } else {
|
||||
// formatter.dateFormat = "MM-dd HH:mm"
|
||||
// return formatter.string(from: date)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func preloadNearbyImages() {
|
||||
// var urlsToPreload: [String] = []
|
||||
//
|
||||
// // 预加载前后2个动态的图片
|
||||
// let preloadRange = max(0, currentIndex - 2)...min(allMoments.count - 1, currentIndex + 2)
|
||||
//
|
||||
// for index in preloadRange {
|
||||
// let moment = allMoments[index]
|
||||
//
|
||||
// // 添加头像
|
||||
// urlsToPreload.append(moment.avatar)
|
||||
//
|
||||
// // 添加动态图片
|
||||
// if let images = moment.dynamicResList {
|
||||
// urlsToPreload.append(contentsOf: images.map { $0.resUrl })
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // 异步预加载
|
||||
// ImageCacheManager.shared.preloadImages(urls: urlsToPreload)
|
||||
// }
|
||||
//}
|
||||
|
||||
// MARK: - 优化的图片网格
|
||||
//struct OptimizedImageGrid: View {
|
||||
// let images: [MomentsPicture]
|
||||
//
|
||||
// var body: some View {
|
||||
// GeometryReader { geometry in
|
||||
// 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:
|
||||
// // 单张图片:大正方形居中显示
|
||||
// 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)
|
||||
// }
|
||||
// }
|
||||
// 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)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .frame(height: calculateGridHeight())
|
||||
// }
|
||||
//
|
||||
// private func calculateGridHeight() -> CGFloat {
|
||||
// switch images.count {
|
||||
// case 1:
|
||||
// return 200 // 单张图片的最大高度
|
||||
// case 2:
|
||||
// return 120 // 两张图片并排的高度
|
||||
// case 3:
|
||||
// return 100 // 三张图片水平排列的高度
|
||||
// case 4...6:
|
||||
// return 216 // 九宫格2行的高度 (实际图片大小 * 2 + 间距)
|
||||
// default:
|
||||
// return 340 // 九宫格3行的高度 (实际图片大小 * 3 + 间距 + 额外空间)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
// MARK: - 正方形图片视图组件
|
||||
//struct SquareImageView: View {
|
||||
// let image: MomentsPicture
|
||||
// let size: CGFloat
|
||||
//
|
||||
// var body: some View {
|
||||
// let safeSize = size.isFinite && size > 0 ? size : 100 // 防止非有限或负数
|
||||
// CachedAsyncImage(url: image.resUrl) { imageView in
|
||||
// imageView
|
||||
// .resizable()
|
||||
// .aspectRatio(contentMode: .fill)
|
||||
// } placeholder: {
|
||||
// Rectangle()
|
||||
// .fill(Color.gray.opacity(0.3))
|
||||
// .overlay(
|
||||
// ProgressView()
|
||||
// .progressViewStyle(CircularProgressViewStyle(tint: .white.opacity(0.6)))
|
||||
// .scaleEffect(0.8)
|
||||
// )
|
||||
// }
|
||||
// .frame(width: safeSize, height: safeSize)
|
||||
// .clipped()
|
||||
// .cornerRadius(8)
|
||||
// }
|
||||
//}
|
||||
|
||||
// MARK: - 旧的真实动态卡片组件(保留备用)
|
||||
//struct RealDynamicCardView: View {
|
||||
// let moment: MomentsInfo
|
||||
//
|
||||
// var body: some View {
|
||||
// VStack(alignment: .leading, spacing: 12) {
|
||||
// // 用户信息
|
||||
// HStack {
|
||||
// AsyncImage(url: URL(string: 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)
|
||||
//
|
||||
// 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 {
|
||||
// LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: min(images.count, 3)), spacing: 8) {
|
||||
// ForEach(images.prefix(9), id: \.id) { image in
|
||||
// AsyncImage(url: URL(string: image.resUrl)) { imageView in
|
||||
// imageView
|
||||
// .resizable()
|
||||
// .aspectRatio(contentMode: .fill)
|
||||
// } placeholder: {
|
||||
// Rectangle()
|
||||
// .fill(Color.gray.opacity(0.3))
|
||||
// .overlay(
|
||||
// ProgressView()
|
||||
// .progressViewStyle(CircularProgressViewStyle(tint: .white.opacity(0.6)))
|
||||
// )
|
||||
// }
|
||||
// .frame(height: 100)
|
||||
// .clipped()
|
||||
// .cornerRadius(8)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // 互动按钮
|
||||
// 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(16)
|
||||
// .background(
|
||||
// Color.white.opacity(0.1)
|
||||
// .cornerRadius(12)
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// private func formatTime(_ timestamp: Int) -> String {
|
||||
// let date = Date(timeIntervalSince1970: TimeInterval(timestamp) / 1000.0)
|
||||
// let formatter = DateFormatter()
|
||||
// formatter.locale = Locale(identifier: "zh_CN")
|
||||
//
|
||||
// let now = Date()
|
||||
// let interval = now.timeIntervalSince(date)
|
||||
//
|
||||
// if interval < 60 {
|
||||
// return "刚刚"
|
||||
// } else if interval < 3600 {
|
||||
// return "\(Int(interval / 60))分钟前"
|
||||
// } else if interval < 86400 {
|
||||
// return "\(Int(interval / 3600))小时前"
|
||||
// } else {
|
||||
// formatter.dateFormat = "MM-dd HH:mm"
|
||||
// return formatter.string(from: date)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
// MARK: - 旧的模拟卡片组件(保留备用)
|
||||
//struct DynamicCardView: View {
|
||||
// let index: Int
|
||||
//
|
||||
// var body: some View {
|
||||
// VStack(alignment: .leading, spacing: 12) {
|
||||
// // 用户信息
|
||||
// HStack {
|
||||
// Circle()
|
||||
// .fill(Color.gray.opacity(0.3))
|
||||
// .frame(width: 40, height: 40)
|
||||
// .overlay(
|
||||
// Text("U\(index + 1)")
|
||||
// .font(.system(size: 16, weight: .medium))
|
||||
// .foregroundColor(.white)
|
||||
// )
|
||||
//
|
||||
// VStack(alignment: .leading, spacing: 2) {
|
||||
// Text("用户\(index + 1)")
|
||||
// .font(.system(size: 16, weight: .medium))
|
||||
// .foregroundColor(.white)
|
||||
//
|
||||
// Text("2小时前")
|
||||
// .font(.system(size: 12))
|
||||
// .foregroundColor(.white.opacity(0.6))
|
||||
// }
|
||||
//
|
||||
// Spacer()
|
||||
// }
|
||||
//
|
||||
// // 动态内容
|
||||
// Text("今天是美好的一天,分享一些生活中的小确幸。希望大家都能珍惜每一个当下的时刻。")
|
||||
// .font(.system(size: 14))
|
||||
// .foregroundColor(.white.opacity(0.9))
|
||||
// .multilineTextAlignment(.leading)
|
||||
//
|
||||
// // 图片网格
|
||||
// LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 3), spacing: 8) {
|
||||
// ForEach(0..<3) { imageIndex in
|
||||
// Rectangle()
|
||||
// .fill(Color.gray.opacity(0.3))
|
||||
// .aspectRatio(1, contentMode: .fit)
|
||||
// .overlay(
|
||||
// Image(systemName: "photo")
|
||||
// .foregroundColor(.white.opacity(0.6))
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // 互动按钮
|
||||
// HStack(spacing: 20) {
|
||||
// Button(action: {}) {
|
||||
// HStack(spacing: 4) {
|
||||
// Image(systemName: "message")
|
||||
// .font(.system(size: 16))
|
||||
// Text("354")
|
||||
// .font(.system(size: 14))
|
||||
// }
|
||||
// .foregroundColor(.white.opacity(0.8))
|
||||
// }
|
||||
//
|
||||
// Button(action: {}) {
|
||||
// HStack(spacing: 4) {
|
||||
// Image(systemName: "heart")
|
||||
// .font(.system(size: 16))
|
||||
// Text("354")
|
||||
// .font(.system(size: 14))
|
||||
// }
|
||||
// .foregroundColor(.white.opacity(0.8))
|
||||
// }
|
||||
//
|
||||
// Spacer()
|
||||
// }
|
||||
// .padding(.top, 8)
|
||||
// }
|
||||
// .padding(16)
|
||||
// .background(
|
||||
// Color.white.opacity(0.1)
|
||||
// .cornerRadius(12)
|
||||
// )
|
||||
// }
|
||||
//}
|
||||
|
||||
//#Preview {
|
||||
// FeedView(
|
||||
// store: Store(initialState: FeedFeature.State()) {
|
||||
// FeedFeature()
|
||||
// }
|
||||
// )
|
||||
//}
|
@@ -1,88 +0,0 @@
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
|
||||
struct HomeView: View {
|
||||
let store: StoreOf<HomeFeature>
|
||||
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" 图片作为背景 - 全屏显示
|
||||
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
|
||||
),
|
||||
onShowCreateFeed: {
|
||||
store.send(.showCreateFeed)
|
||||
}
|
||||
)
|
||||
.transition(.opacity)
|
||||
case .me:
|
||||
Spacer()
|
||||
// MeView(
|
||||
// meDynamicStore: store.scope(
|
||||
// state: \.meDynamic,
|
||||
// action: \.meDynamic
|
||||
// ),
|
||||
// onLogout: onLogout
|
||||
// )
|
||||
// .transition(.opacity)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
|
||||
// 底部导航栏 - 悬浮在最上层
|
||||
VStack {
|
||||
Spacer()
|
||||
BottomTabView(selectedTab: $selectedTab)
|
||||
}
|
||||
.padding(.bottom, geometry.safeAreaInsets.bottom + 100)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
store.send(.onAppear)
|
||||
}
|
||||
.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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#Preview {
|
||||
// HomeView(
|
||||
// store: Store(
|
||||
// initialState: HomeFeature.State()
|
||||
// ) {
|
||||
// HomeFeature()
|
||||
// }, onLogout: {}
|
||||
// )
|
||||
//}
|
@@ -154,7 +154,6 @@ struct LoginView: View {
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
}
|
||||
// 移除:HomeView 的 navigationDestination
|
||||
}
|
||||
.sheet(isPresented: $showLanguageSettings) {
|
||||
WithPerceptionTracking {
|
||||
|
@@ -7,145 +7,161 @@ struct MeView: View {
|
||||
@State private var previewItem: PreviewItem? = nil
|
||||
|
||||
var body: some View {
|
||||
WithPerceptionTracking {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
Image("bg")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.clipped()
|
||||
.ignoresSafeArea(.all)
|
||||
// 顶部栏,右上角设置按钮
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
WithViewStore(self.store, observe: { $0 }) { viewStore in
|
||||
Button(action: {
|
||||
viewStore.send(.settingButtonTapped)
|
||||
}) {
|
||||
Image(systemName: "gearshape")
|
||||
.font(.system(size: 33, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.padding(.trailing, 16)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
VStack(spacing: 16) {
|
||||
// 用户信息区域
|
||||
WithViewStore(self.store, observe: { $0 }) { viewStore in
|
||||
if viewStore.isLoadingUserInfo {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.frame(height: 130)
|
||||
} else if let error = viewStore.userInfoError {
|
||||
Text(error)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.red)
|
||||
.frame(height: 130)
|
||||
} else if let userInfo = viewStore.userInfo {
|
||||
VStack(spacing: 8) {
|
||||
if let avatarUrl = userInfo.avatar, !avatarUrl.isEmpty {
|
||||
AsyncImage(url: URL(string: avatarUrl)) { image in
|
||||
image.resizable().aspectRatio(contentMode: .fill).clipShape(Circle())
|
||||
} placeholder: {
|
||||
Image(systemName: "person.fill").font(.system(size: 40)).foregroundColor(.white)
|
||||
}
|
||||
.frame(width: 90, height: 90)
|
||||
} else {
|
||||
Image(systemName: "person.fill").font(.system(size: 40)).foregroundColor(.white)
|
||||
.frame(width: 90, height: 90)
|
||||
}
|
||||
Text(userInfo.nick ?? "用户昵称")
|
||||
.font(.system(size: 18, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
Text("ID: \(userInfo.uid ?? 0)")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
}
|
||||
.padding(.top, 0)
|
||||
.frame(height: 130)
|
||||
} else {
|
||||
Spacer().frame(height: 130)
|
||||
}
|
||||
}
|
||||
// 动态内容区域
|
||||
WithViewStore(self.store, observe: { $0 }) { viewStore in
|
||||
if viewStore.isLoadingMoments && viewStore.moments.isEmpty {
|
||||
ProgressView("加载中...")
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else if let error = viewStore.momentsError {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.font(.system(size: 40))
|
||||
.foregroundColor(.yellow)
|
||||
Text(error)
|
||||
.foregroundColor(.red)
|
||||
Button("重试") {
|
||||
viewStore.send(.onAppear)
|
||||
}
|
||||
}
|
||||
WithViewStore(self.store, observe: { $0 }) { viewStore in
|
||||
WithPerceptionTracking {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
// 背景图片
|
||||
Image("bg")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else if viewStore.moments.isEmpty {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "tray")
|
||||
.font(.system(size: 40))
|
||||
.foregroundColor(.gray)
|
||||
Text("暂无动态")
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else {
|
||||
ScrollView {
|
||||
WithPerceptionTracking {
|
||||
LazyVStack(spacing: 12) {
|
||||
ForEach(viewStore.moments.indices, id: \ .self) { index in
|
||||
let moment = viewStore.moments[index]
|
||||
OptimizedDynamicCardView(
|
||||
moment: moment,
|
||||
allMoments: viewStore.moments,
|
||||
currentIndex: index,
|
||||
onImageTap: { images, tappedIndex in
|
||||
previewItem = PreviewItem(images: images, index: tappedIndex)
|
||||
}
|
||||
)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
if viewStore.hasMore {
|
||||
ProgressView()
|
||||
.onAppear {
|
||||
viewStore.send(.loadMore)
|
||||
}
|
||||
}
|
||||
// 新增底部间距
|
||||
Color.clear.frame(height: 120)
|
||||
}
|
||||
.padding(.top, 8)
|
||||
.clipped()
|
||||
.ignoresSafeArea(.all)
|
||||
|
||||
// 顶部栏,右上角设置按钮
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(action: {
|
||||
viewStore.send(.settingButtonTapped)
|
||||
}) {
|
||||
Image(systemName: "gearshape")
|
||||
.font(.system(size: 33, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.padding(.trailing, 16)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
.refreshable {
|
||||
viewStore.send(.refresh)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
|
||||
// 主要内容区域
|
||||
VStack(spacing: 16) {
|
||||
// 用户信息区域
|
||||
userInfoSection(viewStore: viewStore)
|
||||
|
||||
// 动态内容区域
|
||||
momentsSection(viewStore: viewStore)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .top)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .top)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
ViewStore(self.store, observe: { $0 }).send(.onAppear)
|
||||
}
|
||||
// 新增:图片预览弹窗
|
||||
.fullScreenCover(item: $previewItem) { item in
|
||||
ImagePreviewPager(images: item.images, currentIndex: .constant(item.index)) {
|
||||
previewItem = nil
|
||||
.onAppear {
|
||||
viewStore.send(.onAppear)
|
||||
}
|
||||
// 新增:图片预览弹窗
|
||||
.fullScreenCover(item: $previewItem) { item in
|
||||
ImagePreviewPager(images: item.images, currentIndex: .constant(item.index)) {
|
||||
previewItem = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 用户信息区域
|
||||
@ViewBuilder
|
||||
private func userInfoSection(viewStore: ViewStoreOf<MeFeature>) -> some View {
|
||||
if viewStore.isLoadingUserInfo {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.frame(height: 130)
|
||||
} else if let error = viewStore.userInfoError {
|
||||
Text(error)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.red)
|
||||
.frame(height: 130)
|
||||
} else if let userInfo = viewStore.userInfo {
|
||||
VStack(spacing: 8) {
|
||||
if let avatarUrl = userInfo.avatar, !avatarUrl.isEmpty {
|
||||
AsyncImage(url: URL(string: avatarUrl)) { image in
|
||||
image.resizable().aspectRatio(contentMode: .fill).clipShape(Circle())
|
||||
} placeholder: {
|
||||
Image(systemName: "person.fill").font(.system(size: 40)).foregroundColor(.white)
|
||||
}
|
||||
.frame(width: 90, height: 90)
|
||||
} else {
|
||||
Image(systemName: "person.fill").font(.system(size: 40)).foregroundColor(.white)
|
||||
.frame(width: 90, height: 90)
|
||||
}
|
||||
Text(userInfo.nick ?? "用户昵称")
|
||||
.font(.system(size: 18, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
Text("ID: \(userInfo.uid ?? 0)")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
}
|
||||
.padding(.top, 0)
|
||||
.frame(height: 130)
|
||||
} else {
|
||||
Spacer().frame(height: 130)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 动态内容区域
|
||||
@ViewBuilder
|
||||
private func momentsSection(viewStore: ViewStoreOf<MeFeature>) -> some View {
|
||||
if viewStore.isLoadingMoments && viewStore.moments.isEmpty {
|
||||
ProgressView("加载中...")
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else if let error = viewStore.momentsError {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.font(.system(size: 40))
|
||||
.foregroundColor(.yellow)
|
||||
Text(error)
|
||||
.foregroundColor(.red)
|
||||
Button("重试") {
|
||||
viewStore.send(.onAppear)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else if viewStore.moments.isEmpty {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "tray")
|
||||
.font(.system(size: 40))
|
||||
.foregroundColor(.gray)
|
||||
Text("暂无动态")
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else {
|
||||
ScrollView {
|
||||
WithPerceptionTracking {
|
||||
LazyVStack(spacing: 12) {
|
||||
ForEach(Array(viewStore.moments.enumerated()), id: \.element.dynamicId) { index, moment in
|
||||
OptimizedDynamicCardView(
|
||||
moment: moment,
|
||||
allMoments: viewStore.moments,
|
||||
currentIndex: index,
|
||||
onImageTap: { images, tappedIndex in
|
||||
previewItem = PreviewItem(images: images, index: tappedIndex)
|
||||
},
|
||||
onLikeTap: { _, _, _, _ in
|
||||
// 暂时不处理点赞,后续可以添加点赞功能
|
||||
}
|
||||
)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
if viewStore.hasMore {
|
||||
ProgressView()
|
||||
.onAppear {
|
||||
viewStore.send(.loadMore)
|
||||
}
|
||||
}
|
||||
// 新增底部间距
|
||||
Color.clear.frame(height: 120)
|
||||
}
|
||||
.padding(.top, 8)
|
||||
}
|
||||
}
|
||||
.refreshable {
|
||||
viewStore.send(.refresh)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user