Files
e-party-iOS/yana/MVVM/MomentDetailPage.swift
edwinQQQ 327d4fd218 feat: 实现动态详情页及相关功能
- 在MePage和MomentListHomePage中新增动态点击事件,支持打开动态详情页。
- 创建MomentDetailPage视图,展示动态详细信息,包括用户信息、动态内容和互动按钮。
- 实现MomentDetailViewModel,管理动态详情页的状态和点赞逻辑。
- 更新MomentListItem组件,添加整体点击回调,提升用户交互体验。
- 优化背景视图组件,确保一致的视觉效果。
2025-09-26 16:49:18 +08:00

246 lines
10 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
// MARK: - MomentDetailPage
struct MomentDetailPage: View {
@StateObject private var viewModel: MomentDetailViewModel
let onClose: () -> Void
init(moment: MomentsInfo, onClose: @escaping () -> Void) {
_viewModel = StateObject(wrappedValue: MomentDetailViewModel(moment: moment))
self.onClose = onClose
}
var body: some View {
ZStack {
//
LoginBackgroundView()
.ignoresSafeArea()
VStack(spacing: 0) {
//
HStack {
Button {
onClose()
} label: {
Image(systemName: "chevron.left")
.font(.system(size: 20, weight: .medium))
.foregroundColor(.white)
.frame(width: 44, height: 44)
.background(Color.black.opacity(0.3))
.clipShape(Circle())
}
Spacer()
Text(LocalizedString("detail.title", comment: "Detail page title"))
.font(.system(size: 18, weight: .semibold))
.foregroundColor(.white)
.shadow(color: .black.opacity(0.5), radius: 2, x: 0, y: 1)
Spacer()
//
Color.clear
.frame(width: 44, height: 44)
}
.padding(.horizontal, 16)
.safeAreaPadding(.top, 60)
.padding(.bottom, 12)
.background(
LinearGradient(
gradient: Gradient(colors: [
Color.black.opacity(0.4),
Color.black.opacity(0.2),
Color.clear
]),
startPoint: .top,
endPoint: .bottom
)
)
//
ScrollView {
VStack(alignment: .leading, spacing: 12) {
//
HStack(alignment: .top) {
//
CachedAsyncImage(url: viewModel.moment.avatar) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
Circle()
.fill(Color.gray.opacity(0.3))
.overlay(
Text(String(viewModel.moment.nick.prefix(1)))
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
)
}
.frame(width: 44, height: 44)
.clipShape(Circle())
VStack(alignment: .leading, spacing: 2) {
Text(viewModel.moment.nick)
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
UserIDDisplay(uid: viewModel.moment.uid, fontSize: 12, textColor: .white.opacity(0.6))
}
Spacer()
//
Text(formatDisplayTime(viewModel.moment.publishTime))
.font(.system(size: 12, weight: .bold))
.foregroundColor(.white.opacity(0.8))
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.white.opacity(0.15))
.cornerRadius(4)
}
//
if !viewModel.moment.content.isEmpty {
Text(viewModel.moment.content)
.font(.system(size: 16))
.foregroundColor(.white.opacity(0.95))
.multilineTextAlignment(.leading)
}
//
if let images = viewModel.moment.dynamicResList, !images.isEmpty {
MomentImageGrid(
images: images,
onImageTap: { images, index in
viewModel.onImageTap(index)
}
)
}
//
HStack(spacing: 20) {
Button {
viewModel.like()
} label: {
HStack(spacing: 6) {
if viewModel.isLikeLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: viewModel.localIsLike ? .red : .white.opacity(0.8)))
.scaleEffect(0.8)
} else {
Image(systemName: viewModel.localIsLike ? "heart.fill" : "heart")
.font(.system(size: 18))
}
Text("\(viewModel.localLikeCount)")
.font(.system(size: 16))
}
}
.foregroundColor(viewModel.localIsLike ? .red : .white.opacity(0.9))
.disabled(viewModel.isLikeLoading || viewModel.moment.status == 0)
.opacity(viewModel.moment.status == 0 ? 0.5 : 1.0)
Spacer()
// -
if viewModel.moment.status == 0 {
Text("reviewing")
.font(.system(size: 12, weight: .semibold))
.foregroundColor(.white)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(Color.orange.opacity(0.85))
.clipShape(Capsule())
}
}
.padding(.top, 8)
}
.padding(.horizontal, 16)
.padding(.bottom, 16)
.safeAreaPadding(.top, 8)
}
}
}
.navigationBarHidden(true)
.fullScreenCover(isPresented: $viewModel.showImagePreview) {
ImagePreviewPager(
images: viewModel.images,
currentIndex: $viewModel.currentIndex
) {
viewModel.showImagePreview = false
}
}
.onAppear {
debugInfoSync("📱 MomentDetailPage: 显示详情页")
debugInfoSync(" 动态ID: \(viewModel.moment.dynamicId)")
debugInfoSync(" 用户: \(viewModel.moment.nick)")
debugInfoSync(" 审核状态: \(viewModel.moment.status)")
}
}
// MARK: -
private func formatDisplayTime(_ timestamp: Int) -> String {
let date = Date(timeIntervalSince1970: TimeInterval(timestamp) / 1000.0)
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "zh_CN")
let now = Date()
let interval = now.timeIntervalSince(date)
let calendar = Calendar.current
if calendar.isDateInToday(date) {
if interval < 60 {
return "刚刚"
} else if interval < 3600 {
return "\(Int(interval / 60))分钟前"
} else {
return "\(Int(interval / 3600))小时前"
}
} else {
formatter.dateFormat = "MM/dd"
return formatter.string(from: date)
}
}
}
//#Preview {
// let testMoment = MomentsInfo(
// dynamicId: 1,
// uid: 123456,
// nick: "",
// avatar: "",
// type: 0,
// content: " MomentDetailPage ",
// likeCount: 42,
// isLike: false,
// commentCount: 5,
// publishTime: Int(Date().timeIntervalSince1970 * 1000),
// worldId: 1,
// status: 0, //
// playCount: nil,
// dynamicResList: [
// MomentsPicture(id: 1, resUrl: "https://picsum.photos/300/300", format: "jpg", width: 300, height: 300, resDuration: nil),
// MomentsPicture(id: 2, resUrl: "https://picsum.photos/301/301", format: "jpg", width: 301, height: 301, resDuration: nil)
// ],
// gender: nil,
// squareTop: nil,
// topicTop: nil,
// newUser: nil,
// defUser: nil,
// scene: nil,
// userVipInfoVO: nil,
// headwearPic: nil,
// headwearEffect: nil,
// headwearType: nil,
// headwearName: nil,
// headwearId: nil,
// experLevelPic: nil,
// charmLevelPic: nil,
// isCustomWord: nil,
// labelList: nil
// )
//
// MomentDetailPage(moment: testMoment) {
// print("")
// }
//}