feat: 实现动态详情页及相关功能
- 在MePage和MomentListHomePage中新增动态点击事件,支持打开动态详情页。 - 创建MomentDetailPage视图,展示动态详细信息,包括用户信息、动态内容和互动按钮。 - 实现MomentDetailViewModel,管理动态详情页的状态和点赞逻辑。 - 更新MomentListItem组件,添加整体点击回调,提升用户交互体验。 - 优化背景视图组件,确保一致的视觉效果。
This commit is contained in:
@@ -17,7 +17,7 @@ struct MomentsListData: Codable, Equatable, Sendable {
|
||||
}
|
||||
|
||||
/// 动态信息结构
|
||||
public struct MomentsInfo: Codable, Equatable, Sendable {
|
||||
public struct MomentsInfo: Codable, Equatable, Sendable, Identifiable {
|
||||
let dynamicId: Int
|
||||
let uid: Int
|
||||
let nick: String
|
||||
@@ -51,6 +51,7 @@ public struct MomentsInfo: Codable, Equatable, Sendable {
|
||||
let isCustomWord: Bool?
|
||||
let labelList: [String]?
|
||||
// 计算属性
|
||||
public var id: Int { dynamicId } // Identifiable 协议要求
|
||||
var isSquareTop: Bool { (squareTop ?? 0) != 0 }
|
||||
var isTopicTop: Bool { (topicTop ?? 0) != 0 }
|
||||
var formattedPublishTime: Date {
|
||||
@@ -270,7 +271,8 @@ struct MyMomentInfo: Codable, Equatable, Sendable {
|
||||
likeCount: likeCount ?? 0,
|
||||
isLike: isLike ?? false,
|
||||
commentCount: commentCount ?? 0,
|
||||
publishTime: Int(publishTime / 1000),
|
||||
// 注意:UI 的 formatDisplayTime 期望毫秒,这里不做 /1000 转换
|
||||
publishTime: Int(publishTime),
|
||||
worldId: worldId ?? 0,
|
||||
status: status ?? 1,
|
||||
playCount: playCount,
|
||||
|
@@ -150,10 +150,9 @@ struct LiquidGlassBackground: View {
|
||||
// MARK: - 背景视图组件
|
||||
struct LoginBackgroundView: View {
|
||||
var body: some View {
|
||||
Color.blue
|
||||
// Image("bg")
|
||||
// .resizable()
|
||||
// .aspectRatio(contentMode: .fill)
|
||||
Image("bg")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
// .ignoresSafeArea(.all)
|
||||
}
|
||||
}
|
||||
|
@@ -98,7 +98,7 @@ struct CreateFeedPage: View {
|
||||
}
|
||||
.frame(height: 200)
|
||||
.padding(.horizontal, 20)
|
||||
.onChange(of: viewModel.content) { newValue in
|
||||
.onChange(of: viewModel.content) { _, newValue in
|
||||
// 限制最大字数
|
||||
if newValue.count > maxCharacters {
|
||||
viewModel.content = String(newValue.prefix(maxCharacters))
|
||||
|
@@ -9,10 +9,13 @@ struct MePage: View {
|
||||
@State private var previewItem: PreviewItem? = nil
|
||||
@State private var previewCurrentIndex: Int = 0
|
||||
|
||||
// 详情页状态
|
||||
@State private var selectedMoment: MomentsInfo? = nil
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// 背景
|
||||
MomentListBackgroundView()
|
||||
// MomentListBackgroundView()
|
||||
|
||||
VStack(spacing: 0) {
|
||||
// 顶部:大头像 + 姓名 + ID + 右上角设置
|
||||
@@ -73,6 +76,10 @@ struct MePage: View {
|
||||
onImageTap: { images, tappedIndex in
|
||||
previewCurrentIndex = tappedIndex
|
||||
previewItem = PreviewItem(images: images, index: tappedIndex)
|
||||
},
|
||||
onMomentTap: { tapped in
|
||||
selectedMoment = tapped
|
||||
debugInfoSync("➡️ MePage: 动态被点击 - ID: \(tapped.dynamicId)")
|
||||
}
|
||||
)
|
||||
.padding(.horizontal, 16)
|
||||
@@ -163,6 +170,16 @@ struct MePage: View {
|
||||
previewItem = nil
|
||||
}
|
||||
}
|
||||
// 详情页
|
||||
.sheet(item: $selectedMoment) { moment in
|
||||
MomentDetailPage(moment: moment) {
|
||||
selectedMoment = nil
|
||||
debugInfoSync("📱 MePage: 详情页已关闭")
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
.presentationDetents([.large])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
245
yana/MVVM/MomentDetailPage.swift
Normal file
245
yana/MVVM/MomentDetailPage.swift
Normal file
@@ -0,0 +1,245 @@
|
||||
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("关闭详情页")
|
||||
// }
|
||||
//}
|
@@ -21,13 +21,16 @@ struct MomentListHomePage: View {
|
||||
@State private var previewItem: PreviewItem? = nil
|
||||
@State private var previewCurrentIndex: Int = 0
|
||||
|
||||
// MARK: - 详情页状态
|
||||
@State private var selectedMoment: MomentsInfo? = nil
|
||||
|
||||
// MARK: - 创建动态发布页弹窗
|
||||
// 迁移到上层(MainPage)统一管理,避免与 TabView 全屏弹窗冲突
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// 背景
|
||||
MomentListBackgroundView()
|
||||
// MomentListBackgroundView()
|
||||
|
||||
VStack(alignment: .center, spacing: 0) {
|
||||
// 顶部标题居中 + 右上角添加按钮(垂直居中对齐)
|
||||
@@ -83,6 +86,13 @@ struct MomentListHomePage: View {
|
||||
debugInfoSync(" 动态索引: \(index)")
|
||||
debugInfoSync(" 图片索引: \(tappedIndex)")
|
||||
debugInfoSync(" 图片数量: \(images.count)")
|
||||
},
|
||||
onMomentTap: { tappedMoment in
|
||||
// 处理整体点击事件 - 打开详情页
|
||||
selectedMoment = tappedMoment
|
||||
debugInfoSync("➡️ MomentListHomePage: 动态被点击")
|
||||
debugInfoSync(" 动态ID: \(tappedMoment.dynamicId)")
|
||||
debugInfoSync(" 用户: \(tappedMoment.nick)")
|
||||
}
|
||||
)
|
||||
.padding(.leading, 16)
|
||||
@@ -178,6 +188,16 @@ struct MomentListHomePage: View {
|
||||
debugInfoSync("📸 MomentListHomePage: 图片预览已关闭")
|
||||
}
|
||||
}
|
||||
// MARK: - 详情页弹窗
|
||||
.sheet(item: $selectedMoment) { moment in
|
||||
MomentDetailPage(moment: moment) {
|
||||
selectedMoment = nil
|
||||
debugInfoSync("📱 MomentListHomePage: 详情页已关闭")
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
.presentationDetents([.large])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
// 发布页由上层统一控制
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import SwiftUI
|
||||
struct MomentListItem: View {
|
||||
let moment: MomentsInfo
|
||||
let onImageTap: (([String], Int)) -> Void // 新增:图片点击回调
|
||||
let onMomentTap: (MomentsInfo) -> Void // 新增:整体点击回调
|
||||
|
||||
// 新增:点赞相关状态
|
||||
@State private var isLikeLoading = false
|
||||
@@ -12,111 +13,134 @@ struct MomentListItem: View {
|
||||
|
||||
init(
|
||||
moment: MomentsInfo,
|
||||
onImageTap: @escaping (([String], Int)) -> Void = { (arg) in let (_, _) = arg; }
|
||||
onImageTap: @escaping (([String], Int)) -> Void = { (arg) in let (_, _) = arg; },
|
||||
onMomentTap: @escaping (MomentsInfo) -> Void = { _ in }
|
||||
) {
|
||||
self.moment = moment
|
||||
self.onImageTap = onImageTap
|
||||
self.onMomentTap = onMomentTap
|
||||
// 初始化本地状态
|
||||
self._localIsLike = State(initialValue: moment.isLike)
|
||||
self._localLikeCount = State(initialValue: moment.likeCount)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(Color.clear)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(Color.white.opacity(0.1), lineWidth: 1)
|
||||
)
|
||||
.shadow(color: Color(red: 0.43, green: 0.43, blue: 0.43, opacity: 0.34), radius: 10.7, x: 0, y: 1.9)
|
||||
|
||||
// 内容层
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
// 用户信息
|
||||
HStack(alignment: .top) {
|
||||
// 头像
|
||||
CachedAsyncImage(url: moment.avatar) { image in
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
} placeholder: {
|
||||
Circle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.overlay(
|
||||
Text(String(moment.nick.prefix(1)))
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
)
|
||||
}
|
||||
.frame(width: 40, height: 40)
|
||||
.clipShape(Circle())
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(moment.nick)
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
UserIDDisplay(uid: moment.uid, fontSize: 12, textColor: .white.opacity(0.6))
|
||||
}
|
||||
Spacer()
|
||||
// 时间
|
||||
Text(formatDisplayTime(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 !moment.content.isEmpty {
|
||||
Text(moment.content)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
.multilineTextAlignment(.leading)
|
||||
.padding(.leading, 40 + 8) // 与用户名左边对齐
|
||||
}
|
||||
|
||||
// 图片网格
|
||||
if let images = moment.dynamicResList, !images.isEmpty {
|
||||
MomentImageGrid(
|
||||
images: images,
|
||||
onImageTap: onImageTap
|
||||
let isReviewing = moment.status == 0
|
||||
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(Color.clear)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(Color.white.opacity(0.1), lineWidth: 1)
|
||||
)
|
||||
.padding(.leading, 40 + 8)
|
||||
.padding(.bottom, images.count == 2 ? 30 : 0) // 两张图片时增加底部间距
|
||||
}
|
||||
|
||||
// 互动按钮
|
||||
HStack(spacing: 20) {
|
||||
// Like 按钮与用户名左侧对齐
|
||||
Button(action: {
|
||||
if !isLikeLoading {
|
||||
handleLikeTap()
|
||||
.shadow(color: Color(red: 0.43, green: 0.43, blue: 0.43, opacity: 0.34), radius: 10.7, x: 0, y: 1.9)
|
||||
|
||||
// 内容层
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
// 用户信息
|
||||
HStack(alignment: .top) {
|
||||
// 头像
|
||||
CachedAsyncImage(url: moment.avatar) { image in
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
} placeholder: {
|
||||
Circle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.overlay(
|
||||
Text(String(moment.nick.prefix(1)))
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
)
|
||||
}
|
||||
}) {
|
||||
HStack(spacing: 4) {
|
||||
if isLikeLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: localIsLike ? .red : .white.opacity(0.8)))
|
||||
.scaleEffect(0.8)
|
||||
} else {
|
||||
Image(systemName: localIsLike ? "heart.fill" : "heart")
|
||||
.font(.system(size: 16))
|
||||
}
|
||||
Text("\(localLikeCount)")
|
||||
.font(.system(size: 14))
|
||||
.frame(width: 40, height: 40)
|
||||
.clipShape(Circle())
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(moment.nick)
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
UserIDDisplay(uid: moment.uid, fontSize: 12, textColor: .white.opacity(0.6))
|
||||
}
|
||||
.foregroundColor(localIsLike ? .red : .white.opacity(0.8))
|
||||
Spacer()
|
||||
// 时间
|
||||
Text(formatDisplayTime(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)
|
||||
}
|
||||
.disabled(isLikeLoading)
|
||||
.padding(.leading, 40 + 8) // 与用户名左侧对齐(头像宽度 + 间距)
|
||||
Spacer()
|
||||
|
||||
// 动态内容
|
||||
if !moment.content.isEmpty {
|
||||
Text(moment.content)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
.multilineTextAlignment(.leading)
|
||||
.padding(.leading, 40 + 8) // 与用户名左边对齐
|
||||
}
|
||||
|
||||
// 图片网格
|
||||
if let images = moment.dynamicResList, !images.isEmpty {
|
||||
MomentImageGrid(
|
||||
images: images,
|
||||
onImageTap: onImageTap
|
||||
)
|
||||
.padding(.leading, 40 + 8)
|
||||
.padding(.bottom, images.count == 2 ? 30 : 0) // 两张图片时增加底部间距
|
||||
}
|
||||
|
||||
// 互动按钮
|
||||
HStack(spacing: 20) {
|
||||
// Like 按钮与用户名左侧对齐
|
||||
Button(action: {
|
||||
if !isLikeLoading && !isReviewing {
|
||||
handleLikeTap()
|
||||
}
|
||||
}) {
|
||||
HStack(spacing: 4) {
|
||||
if isLikeLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: localIsLike ? .red : .white.opacity(0.8)))
|
||||
.scaleEffect(0.8)
|
||||
} else {
|
||||
Image(systemName: localIsLike ? "heart.fill" : "heart")
|
||||
.font(.system(size: 16))
|
||||
}
|
||||
Text("\(localLikeCount)")
|
||||
.font(.system(size: 14))
|
||||
}
|
||||
.foregroundColor(localIsLike ? .red : .white.opacity(0.8))
|
||||
}
|
||||
.disabled(isLikeLoading || isReviewing)
|
||||
.opacity(isReviewing ? 0.5 : 1.0)
|
||||
.padding(.leading, 40 + 8) // 与用户名左侧对齐(头像宽度 + 间距)
|
||||
|
||||
Spacer()
|
||||
|
||||
// 审核中状态角标 - 右侧对齐日期,垂直居中对齐点赞按钮
|
||||
if isReviewing {
|
||||
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(.top, 8)
|
||||
.padding(16)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
onMomentTap(moment)
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
}
|
||||
|
||||
|
114
yana/MVVM/ViewModel/MomentDetailViewModel.swift
Normal file
114
yana/MVVM/ViewModel/MomentDetailViewModel.swift
Normal file
@@ -0,0 +1,114 @@
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
// MARK: - MomentDetailViewModel
|
||||
|
||||
@MainActor
|
||||
final class MomentDetailViewModel: ObservableObject {
|
||||
// MARK: - Published Properties
|
||||
@Published var moment: MomentsInfo
|
||||
@Published var isLikeLoading = false
|
||||
@Published var localIsLike: Bool
|
||||
@Published var localLikeCount: Int
|
||||
@Published var showImagePreview = false
|
||||
@Published var images: [String] = []
|
||||
@Published var currentIndex: Int = 0
|
||||
|
||||
// MARK: - Private Properties
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
// MARK: - Initialization
|
||||
init(moment: MomentsInfo) {
|
||||
self.moment = moment
|
||||
self.localIsLike = moment.isLike
|
||||
self.localLikeCount = moment.likeCount
|
||||
self.images = moment.dynamicResList?.compactMap { $0.resUrl } ?? []
|
||||
|
||||
debugInfoSync("📱 MomentDetailViewModel: 初始化")
|
||||
debugInfoSync(" 动态ID: \(moment.dynamicId)")
|
||||
debugInfoSync(" 用户: \(moment.nick)")
|
||||
debugInfoSync(" 图片数量: \(images.count)")
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
func onImageTap(_ index: Int) {
|
||||
currentIndex = index
|
||||
showImagePreview = true
|
||||
debugInfoSync("📸 MomentDetailViewModel: 图片被点击,索引: \(index)")
|
||||
}
|
||||
|
||||
func like() {
|
||||
guard !isLikeLoading, moment.status != 0 else {
|
||||
debugInfoSync("⏸️ MomentDetailViewModel: 跳过点赞 - 正在加载: \(isLikeLoading), 审核中: \(moment.status == 0)")
|
||||
return
|
||||
}
|
||||
|
||||
isLikeLoading = true
|
||||
debugInfoSync("📡 MomentDetailViewModel: 开始点赞操作")
|
||||
|
||||
Task {
|
||||
do {
|
||||
// 获取当前用户ID
|
||||
guard let uidStr = await UserInfoManager.getCurrentUserId(),
|
||||
let uid = Int(uidStr) else {
|
||||
await MainActor.run {
|
||||
isLikeLoading = false
|
||||
}
|
||||
setAPILoadingErrorSync(UUID(), errorMessage: "无法获取用户信息,请重新登录")
|
||||
return
|
||||
}
|
||||
|
||||
// 确定请求参数
|
||||
let status = localIsLike ? 0 : 1 // 0: 取消点赞, 1: 点赞
|
||||
|
||||
// 创建 API 服务实例
|
||||
let api = LiveAPIService()
|
||||
|
||||
// 创建请求
|
||||
let request = LikeDynamicRequest(
|
||||
dynamicId: moment.dynamicId,
|
||||
uid: uid,
|
||||
status: status,
|
||||
likedUid: moment.uid,
|
||||
worldId: moment.worldId
|
||||
)
|
||||
|
||||
debugInfoSync("📡 MomentDetailViewModel: 发送点赞请求")
|
||||
debugInfoSync(" 动态ID: \(moment.dynamicId)")
|
||||
debugInfoSync(" 当前状态: \(localIsLike)")
|
||||
debugInfoSync(" 请求状态: \(status)")
|
||||
|
||||
// 发起请求
|
||||
let response: LikeDynamicResponse = try await api.request(request)
|
||||
|
||||
await MainActor.run {
|
||||
isLikeLoading = false
|
||||
// 处理响应
|
||||
if response.code == 200 {
|
||||
localIsLike.toggle()
|
||||
localLikeCount += localIsLike ? 1 : -1
|
||||
debugInfoSync("✅ MomentDetailViewModel: 点赞操作成功")
|
||||
debugInfoSync(" 动态ID: \(moment.dynamicId)")
|
||||
debugInfoSync(" 新状态: \(localIsLike)")
|
||||
debugInfoSync(" 新数量: \(localLikeCount)")
|
||||
} else {
|
||||
let errorMessage = response.message.isEmpty ? "点赞失败,请重试" : response.message
|
||||
setAPILoadingErrorSync(UUID(), errorMessage: errorMessage)
|
||||
debugErrorSync("❌ MomentDetailViewModel: 点赞操作失败")
|
||||
debugErrorSync(" 动态ID: \(moment.dynamicId)")
|
||||
debugErrorSync(" 错误: \(errorMessage)")
|
||||
}
|
||||
}
|
||||
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
isLikeLoading = false
|
||||
}
|
||||
setAPILoadingErrorSync(UUID(), errorMessage: error.localizedDescription)
|
||||
debugErrorSync("❌ MomentDetailViewModel: 点赞请求异常")
|
||||
debugErrorSync(" 动态ID: \(moment.dynamicId)")
|
||||
debugErrorSync(" 错误: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -90,10 +90,10 @@
|
||||
"createFeed.processingImages" = "Processing images...";
|
||||
"createFeed.publishing" = "Publishing...";
|
||||
"createFeed.publish" = "Publish";
|
||||
"createFeed.title" = "Image & Text Publish";
|
||||
"createFeed.title" = "Image & Text";
|
||||
|
||||
// MARK: - Edit Feed
|
||||
"editFeed.title" = "Image & Text Edit";
|
||||
"editFeed.title" = "Image & Text";
|
||||
"editFeed.publish" = "Publish";
|
||||
"editFeed.enterContent" = "Enter Content";
|
||||
|
||||
@@ -225,4 +225,4 @@
|
||||
"config.version" = "Version";
|
||||
"config.debug_mode" = "Debug Mode";
|
||||
"config.api_timeout" = "API Timeout";
|
||||
"config.max_retries" = "Max Retries";
|
||||
"config.max_retries" = "Max Retries";
|
||||
|
Reference in New Issue
Block a user