diff --git a/yana/APIs/DynamicsModels.swift b/yana/APIs/DynamicsModels.swift index ce57f30..b591de7 100644 --- a/yana/APIs/DynamicsModels.swift +++ b/yana/APIs/DynamicsModels.swift @@ -242,28 +242,39 @@ struct PublishFeedData: Codable, Equatable { /// 我的动态信息结构 - 专门用于 /dynamic/getMyDynamic 接口 struct MyMomentInfo: Codable, Equatable, Sendable { - let content: String + // 服务器可能返回的完整字段(均用可选兼容不同版本) + let dynamicId: Int? let uid: Int - let publishTime: Int64 + let nick: String? + let avatar: String? let type: Int + let content: String + let likeCount: Int? + let isLike: Bool? + let commentCount: Int? + let publishTime: Int64 + let worldId: Int? + let status: Int? + let playCount: Int? + let dynamicResList: [MomentsPicture]? // 资源列表(图片/视频) // 转换为 MomentsInfo 的辅助方法 func toMomentsInfo() -> MomentsInfo { return MomentsInfo( - dynamicId: 0, // 我的动态接口没有返回 dynamicId + dynamicId: dynamicId ?? 0, uid: uid, - nick: "", // 需要从用户信息中获取 - avatar: "", // 需要从用户信息中获取 + nick: nick ?? "", + avatar: avatar ?? "", type: type, content: content, - likeCount: 0, // 我的动态接口没有返回点赞数 - isLike: false, // 我的动态接口没有返回点赞状态 - commentCount: 0, // 我的动态接口没有返回评论数 - publishTime: Int(publishTime / 1000), // 转换为秒 - worldId: 0, // 我的动态接口没有返回 worldId - status: 1, // 默认状态 - playCount: nil, - dynamicResList: nil, + likeCount: likeCount ?? 0, + isLike: isLike ?? false, + commentCount: commentCount ?? 0, + publishTime: Int(publishTime / 1000), + worldId: worldId ?? 0, + status: status ?? 1, + playCount: playCount, + dynamicResList: dynamicResList, gender: nil, squareTop: nil, topicTop: nil, diff --git a/yana/MVVM/CommonComponents.swift b/yana/MVVM/CommonComponents.swift index 5f42de5..15751a6 100644 --- a/yana/MVVM/CommonComponents.swift +++ b/yana/MVVM/CommonComponents.swift @@ -112,10 +112,11 @@ struct LiquidGlassBackground: View { // MARK: - 背景视图组件 struct LoginBackgroundView: View { var body: some View { - Image("bg") - .resizable() - .aspectRatio(contentMode: .fill) - .ignoresSafeArea(.all) + Color.blue +// Image("bg") +// .resizable() +// .aspectRatio(contentMode: .fill) +// .ignoresSafeArea(.all) } } diff --git a/yana/MVVM/MainPage.swift b/yana/MVVM/MainPage.swift index 638a285..e9de2c0 100644 --- a/yana/MVVM/MainPage.swift +++ b/yana/MVVM/MainPage.swift @@ -46,8 +46,9 @@ struct MainPage: View { .padding(.horizontal, 24) .padding(.bottom, 100) } - } + }.ignoresSafeArea(.all) } + .toolbar(.hidden) } .onAppear { viewModel.onLogout = onLogout diff --git a/yana/MVVM/MePage.swift b/yana/MVVM/MePage.swift index abdddcd..7201148 100644 --- a/yana/MVVM/MePage.swift +++ b/yana/MVVM/MePage.swift @@ -3,31 +3,146 @@ import SwiftUI struct MePage: View { let onLogout: () -> Void @State private var isShowingSettings: Bool = false + @StateObject private var viewModel = MePageViewModel() + + // 图片预览状态 + @State private var previewItem: PreviewItem? = nil + @State private var previewCurrentIndex: Int = 0 var body: some View { - ZStack(alignment: .topTrailing) { - VStack { - Text("Me View") - .font(.title) - .foregroundColor(.white) - - Text("This is a simplified MeView") - .font(.body) - .foregroundColor(.white.opacity(0.8)) - } + ZStack { + // 背景 + MomentListBackgroundView() - Button(action: { - isShowingSettings = true - }) { - Image(systemName: "gearshape") - .font(.system(size: 24, weight: .medium)) - .foregroundColor(.white) - .frame(width: 40, height: 40) - .background(Color.black.opacity(0.3)) - .clipShape(Circle()) + VStack(spacing: 0) { + // 顶部:大头像 + 姓名 + ID + 右上角设置 + ZStack(alignment: .topTrailing) { + VStack(spacing: 12) { + AsyncImage(url: URL(string: viewModel.avatarURL)) { image in + image.resizable().scaledToFill() + } placeholder: { + Image(systemName: "person.circle.fill") + .resizable() + .scaledToFill() + .foregroundColor(.gray) + } + .frame(width: 132, height: 132) + .clipShape(Circle()) + .overlay(Circle().stroke(Color.white, lineWidth: 3)) + .shadow(color: .black.opacity(0.25), radius: 10, x: 0, y: 6) + + Text(viewModel.nickname.isEmpty ? "未知用户" : viewModel.nickname) + .font(.system(size: 34, weight: .semibold)) + .foregroundColor(.white) + .lineLimit(1) + .minimumScaleFactor(0.6) + + if viewModel.userId > 0 { + HStack(spacing: 6) { + Text("ID:\(viewModel.userId)") + .font(.system(size: 16)) + .foregroundColor(.white.opacity(0.8)) + Image(systemName: "doc.on.doc") + .foregroundColor(.white.opacity(0.8)) + } + } + } + .frame(maxWidth: .infinity) + .padding(.top, 24) + + Button(action: { isShowingSettings = true }) { + Image(systemName: "gearshape") + .font(.system(size: 24, weight: .medium)) + .foregroundColor(.white) + .frame(width: 40, height: 40) + .background(Color.black.opacity(0.3)) + .clipShape(Circle()) + } + .padding(.trailing, 16) + .padding(.top, 8) + } + .padding(.bottom, 8) + + // 下部:只显示当前用户的动态列表 + if !viewModel.moments.isEmpty { + ScrollView { + LazyVStack(spacing: 16) { + ForEach(Array(viewModel.moments.enumerated()), id: \.offset) { index, moment in + MomentListItem( + moment: moment, + onImageTap: { images, tappedIndex in + previewCurrentIndex = tappedIndex + previewItem = PreviewItem(images: images, index: tappedIndex) + } + ) + .padding(.horizontal, 16) + .onAppear { + if index == viewModel.moments.count - 3 { + viewModel.loadMoreData() + } + } + } + if viewModel.isLoadingMore { + HStack { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .scaleEffect(0.8) + Text("加载更多...") + .font(.system(size: 14)) + .foregroundColor(.white.opacity(0.8)) + } + .padding(.vertical, 20) + } + if !viewModel.hasMore && !viewModel.moments.isEmpty { + Text("没有更多数据了") + .font(.system(size: 14)) + .foregroundColor(.white.opacity(0.6)) + .padding(.vertical, 20) + } + } + .padding(.bottom, 160) + } + .refreshable { await viewModel.refreshData() } + } else if viewModel.isLoading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .padding(.top, 20) + } else if let error = viewModel.errorMessage { + VStack(spacing: 16) { + Text(error) + .font(.system(size: 14)) + .foregroundColor(.red) + .multilineTextAlignment(.center) + .padding(.horizontal, 20) + Button(action: { Task { await viewModel.refreshData() } }) { + Text("重试") + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.white) + .padding(.horizontal, 20) + .padding(.vertical, 8) + .background(Color.white.opacity(0.2)) + .cornerRadius(8) + } + } + .padding(.top, 20) + } else { + VStack(spacing: 12) { + Image(systemName: "doc.text") + .font(.system(size: 32)) + .foregroundColor(.white.opacity(0.5)) + Text("暂无动态") + .font(.system(size: 16, weight: .medium)) + .foregroundColor(.white.opacity(0.7)) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + Spacer() } - .padding(.trailing, 16) - .padding(.top, 8) + .safeAreaPadding(.top, 8) + } + .onAppear { viewModel.onAppear() } + .onReceive(NotificationCenter.default.publisher(for: .init("CreateFeedPublished"))) { _ in + Task { await viewModel.refreshData() } } .sheet(isPresented: $isShowingSettings) { SettingPage( @@ -39,6 +154,15 @@ struct MePage: View { ) .navigationBarHidden(true) } + // 图片预览 + .sheet(item: $previewItem) { item in + ImagePreviewPager( + images: item.images as [String], + currentIndex: $previewCurrentIndex + ) { + previewItem = nil + } + } } } diff --git a/yana/MVVM/SplashPage.swift b/yana/MVVM/SplashPage.swift index 9beb0dd..61c5f42 100644 --- a/yana/MVVM/SplashPage.swift +++ b/yana/MVVM/SplashPage.swift @@ -4,7 +4,7 @@ struct SplashPage: View { @State private var showLogin = false @State private var showMain = false @State private var hasCheckedAuth = false - private let splashTransitionAnimation: Animation = .easeInOut(duration: 0.25) + private let splashTransitionAnimation: Animation = .easeInOut(duration: 0.5) var body: some View { Group { @@ -56,7 +56,6 @@ struct SplashPage: View { } } } - .ignoresSafeArea() } } diff --git a/yana/MVVM/ViewModel/MePageViewModel.swift b/yana/MVVM/ViewModel/MePageViewModel.swift new file mode 100644 index 0000000..1563f07 --- /dev/null +++ b/yana/MVVM/ViewModel/MePageViewModel.swift @@ -0,0 +1,88 @@ +import Foundation +import SwiftUI + +@MainActor +final class MePageViewModel: ObservableObject { + @Published var userId: Int = 0 + @Published var nickname: String = "" + @Published var avatarURL: String = "" + + @Published var moments: [MomentsInfo] = [] + @Published var isLoading: Bool = false + @Published var isLoadingMore: Bool = false + @Published var errorMessage: String? = nil + @Published var hasMore: Bool = true + + private var page: Int = 1 + private let pageSize: Int = 20 + + func onAppear() { + Task { @MainActor in + await loadCurrentUser() + // 仅首次或空列表时加载,避免每次 Tab 切换重复请求 + if moments.isEmpty { + await refreshData() + } + } + } + + func refreshData() async { + page = 1 + hasMore = true + errorMessage = nil + isLoading = true + moments.removeAll() + defer { isLoading = false } + await fetchMyMoments(page: page) + } + + func loadMoreData() { + guard !isLoadingMore, hasMore else { return } + isLoadingMore = true + Task { @MainActor in + defer { isLoadingMore = false } + page += 1 + await fetchMyMoments(page: page) + } + } + + private func loadCurrentUser() async { + // 从缓存/Keychain 获取当前登录用户信息 + if let account = await UserInfoManager.getAccountModel() { + if let uidString = account.uid, let uid = Int(uidString) { + userId = uid + } + // 优先从缓存的 UserInfo 获取更完整的信息 + if let info = await UserInfoManager.getUserInfo() { + nickname = info.nick ?? nickname + avatarURL = info.avatar ?? avatarURL + } + } + // 兜底 + if nickname.isEmpty { nickname = "未知用户" } + } + + private func fetchMyMoments(page: Int) async { + guard userId > 0 else { + errorMessage = "未登录或用户ID无效" + return + } + let api: any APIServiceProtocol & Sendable = LiveAPIService() + let request = GetMyDynamicRequest(fromUid: userId, uid: userId, page: page, pageSize: pageSize) + do { + let response = try await api.request(request) + if let list = response.data { + let items = list.map { $0.toMomentsInfo() } + if items.isEmpty { hasMore = false } + moments.append(contentsOf: items) + } else { + hasMore = false + } + } catch { + errorMessage = error.localizedDescription + } + } +} + + + diff --git a/yana/yanaApp.swift b/yana/yanaApp.swift index 6a0a6af..8866e23 100644 --- a/yana/yanaApp.swift +++ b/yana/yanaApp.swift @@ -24,6 +24,7 @@ struct yanaApp: App { var body: some Scene { WindowGroup { SplashPage() + .ignoresSafeArea(.all) } } }