feat: 更新视图组件及数据模型
- 在yanaApp中为SplashPage添加忽略安全区域的设置,确保全屏显示。 - 在DynamicsModels中更新MyMomentInfo结构,添加可选字段以兼容不同版本的服务器返回数据。 - 在CommonComponents中将LoginBackgroundView的背景图替换为蓝色,简化视图。 - 在MainPage中为内容添加忽略安全区域的设置,提升布局一致性。 - 在MePage中新增MePageViewModel,优化用户信息管理逻辑,支持动态列表的加载和错误处理。 - 在SplashPage中调整过渡动画时长,提升用户体验。
This commit is contained in:
@@ -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,
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -46,8 +46,9 @@ struct MainPage: View {
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.bottom, 100)
|
||||
}
|
||||
}
|
||||
}.ignoresSafeArea(.all)
|
||||
}
|
||||
.toolbar(.hidden)
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.onLogout = onLogout
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
88
yana/MVVM/ViewModel/MePageViewModel.swift
Normal file
88
yana/MVVM/ViewModel/MePageViewModel.swift
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@@ -24,6 +24,7 @@ struct yanaApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
SplashPage()
|
||||
.ignoresSafeArea(.all)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user