feat: 更新动态功能,新增我的动态视图及相关状态管理

- 在HomeFeature中添加MeDynamicFeature以管理用户动态状态。
- 在MainFeature中集成MeDynamicFeature,支持动态内容的加载与展示。
- 新增MeDynamicView以展示用户的动态列表,支持下拉刷新和上拉加载更多功能。
- 更新MeView以集成用户动态视图,提升用户体验。
- 在APIEndpoints中新增getMyDynamic端点以支持获取用户动态信息。
- 更新DynamicsModels以适应新的动态数据结构,确保数据解析的准确性。
- 在OptimizedDynamicCardView中优化图片处理逻辑,提升动态展示效果。
- 更新相关视图组件以支持动态内容的展示与交互,增强用户体验。
This commit is contained in:
edwinQQQ
2025-07-23 19:17:49 +08:00
parent 3a68270ca9
commit 8b09653c4c
15 changed files with 1097 additions and 359 deletions

View File

@@ -2,90 +2,48 @@ import SwiftUI
import ComposableArchitecture
struct MeView: View {
let meDynamicStore: StoreOf<MeDynamicFeature>
let accountModel: AccountModel
@State private var showLogoutConfirmation = false
let onLogout: () -> Void //
@State private var showSetting = false // SettingView
@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
var body: some View {
GeometryReader { geometry in
ScrollView {
VStack(spacing: 20) {
//
HStack {
Spacer()
Text("我的")
.font(.system(size: 22, weight: .semibold))
.foregroundColor(.white)
Spacer()
Button(action: { showSetting = true }) {
Image(systemName: "gearshape")
.font(.system(size: 22, weight: .regular))
.foregroundColor(.white)
}
.padding(.trailing, 8)
}
.padding(.top, geometry.safeAreaInsets.top + 20)
//
VStack(spacing: 16) {
Circle()
.fill(Color.white.opacity(0.2))
.frame(width: 80, height: 80)
.overlay(
Image(systemName: "person.fill")
.font(.system(size: 40))
.foregroundColor(.white)
)
Text("用户昵称")
.font(.system(size: 18, weight: .medium))
.foregroundColor(.white)
Text("ID: 123456789")
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.7))
}
.padding(.top, 30)
//
VStack(spacing: 12) {
MenuItemView(icon: "gearshape", title: "设置", action: {})
MenuItemView(icon: "person.circle", title: "个人信息", action: {})
MenuItemView(icon: "heart", title: "我的收藏", action: {})
MenuItemView(icon: "clock", title: "浏览历史", action: {})
MenuItemView(icon: "questionmark.circle", title: "帮助与反馈", action: {})
}
.padding(.horizontal, 20)
.padding(.top, 40)
// 退
Button(action: {
showLogoutConfirmation = true
}) {
HStack {
Image(systemName: "rectangle.portrait.and.arrow.right")
.font(.system(size: 16))
Text("退出登录")
.font(.system(size: 16, weight: .medium))
}
.foregroundColor(.red)
.frame(maxWidth: .infinity)
.frame(height: 50)
.background(
Color.white.opacity(0.1)
.cornerRadius(12)
)
}
.padding(.horizontal, 20)
.padding(.top, 30)
// -
Color.clear.frame(height: geometry.safeAreaInsets.bottom + 100)
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)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.padding(.top, 100)
}
}
.ignoresSafeArea(.container, edges: .top)
.onAppear {
if !hasLoaded {
loadUserInfo()
hasLoaded = true
}
}
.alert("确认退出", isPresented: $showLogoutConfirmation) {
Button("取消", role: .cancel) { }
Button("退出", role: .destructive) {
@@ -95,53 +53,178 @@ struct MeView: View {
Text("确定要退出登录吗?")
}
.sheet(isPresented: $showSetting) {
// storestore
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("🔓 开始执行退出登录...")
// keychain
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 MenuItemView: View {
let icon: String
let title: String
let action: () -> Void
// MARK: -
struct UserProfileSection: View {
let userInfo: UserInfo?
let isLoading: Bool
let errorMessage: String?
let onSettingTapped: () -> Void
var body: some View {
Button(action: action) {
HStack(spacing: 16) {
Image(systemName: icon)
.font(.system(size: 20))
.foregroundColor(.white)
.frame(width: 24)
Text(title)
.font(.system(size: 16))
.foregroundColor(.white)
.frame(maxWidth: .infinity, alignment: .leading)
Image(systemName: "chevron.right")
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.6))
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)
}
} else {
Image(systemName: "person.fill")
.font(.system(size: 40))
.foregroundColor(.white)
}
}
)
}
//
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))
}
}
}
.padding(.horizontal, 20)
.frame(height: 56)
.background(
Color.white.opacity(0.1)
.cornerRadius(12)
)
}
.buttonStyle(PlainButtonStyle())
.padding(.horizontal, 20)
.padding(.bottom, 20)
}
}