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