feat: 更新动态功能,新增我的动态视图及相关状态管理
- 在HomeFeature中添加MeDynamicFeature以管理用户动态状态。 - 在MainFeature中集成MeDynamicFeature,支持动态内容的加载与展示。 - 新增MeDynamicView以展示用户的动态列表,支持下拉刷新和上拉加载更多功能。 - 更新MeView以集成用户动态视图,提升用户体验。 - 在APIEndpoints中新增getMyDynamic端点以支持获取用户动态信息。 - 更新DynamicsModels以适应新的动态数据结构,确保数据解析的准确性。 - 在OptimizedDynamicCardView中优化图片处理逻辑,提升动态展示效果。 - 更新相关视图组件以支持动态内容的展示与交互,增强用户体验。
This commit is contained in:
@@ -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) {
|
||||
// 这里用预设store,实际项目可替换为真实store
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user