
- 在MomentListItem中新增点赞功能,用户点击按钮可触发点赞请求。 - 使用MVVM+Combine架构管理点赞状态,确保UI与状态同步。 - 添加加载状态和错误处理,提升用户体验和交互反馈。 - 更新相关视图以支持新的点赞逻辑,优化代码可读性和维护性。
399 lines
15 KiB
Swift
399 lines
15 KiB
Swift
import SwiftUI
|
||
|
||
// MARK: - MomentListItem
|
||
struct MomentListItem: View {
|
||
let moment: MomentsInfo
|
||
let onImageTap: (([String], Int)) -> Void // 新增:图片点击回调
|
||
|
||
// 新增:点赞相关状态
|
||
@State private var isLikeLoading = false
|
||
@State private var localIsLike: Bool
|
||
@State private var localLikeCount: Int
|
||
|
||
init(
|
||
moment: MomentsInfo,
|
||
onImageTap: @escaping (([String], Int)) -> Void = { (arg) in let (_, _) = arg; }
|
||
) {
|
||
self.moment = moment
|
||
self.onImageTap = onImageTap
|
||
// 初始化本地状态
|
||
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
|
||
)
|
||
.padding(.leading, 40 + 8)
|
||
.padding(.bottom, images.count == 2 ? 30 : 0) // 两张图片时增加底部间距
|
||
}
|
||
|
||
// 互动按钮
|
||
HStack(spacing: 20) {
|
||
// Like 按钮与用户名左侧对齐
|
||
Button(action: {
|
||
if !isLikeLoading {
|
||
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)
|
||
.padding(.leading, 40 + 8) // 与用户名左侧对齐(头像宽度 + 间距)
|
||
Spacer()
|
||
}
|
||
.padding(.top, 8)
|
||
}
|
||
.padding(16)
|
||
}
|
||
}
|
||
|
||
// MARK: - 点赞处理逻辑
|
||
private func handleLikeTap() {
|
||
Task {
|
||
await performLikeRequest()
|
||
}
|
||
}
|
||
|
||
private func performLikeRequest() async {
|
||
// 设置加载状态
|
||
await MainActor.run {
|
||
isLikeLoading = true
|
||
}
|
||
|
||
do {
|
||
// 获取当前用户ID
|
||
guard let currentUserId = await UserInfoManager.getCurrentUserId(),
|
||
let currentUserIdInt = Int(currentUserId) else {
|
||
await MainActor.run {
|
||
isLikeLoading = false
|
||
}
|
||
setAPILoadingErrorSync(UUID(), errorMessage: "无法获取用户信息,请重新登录")
|
||
return
|
||
}
|
||
|
||
// 确定请求参数
|
||
let status = localIsLike ? 0 : 1 // 0: 取消点赞, 1: 点赞
|
||
|
||
// 创建 API 服务实例
|
||
let apiService = LiveAPIService()
|
||
|
||
// 创建请求
|
||
let request = LikeDynamicRequest(
|
||
dynamicId: moment.dynamicId,
|
||
uid: currentUserIdInt,
|
||
status: status,
|
||
likedUid: moment.uid,
|
||
worldId: moment.worldId
|
||
)
|
||
|
||
debugInfoSync("📡 MomentListItem: 发送点赞请求")
|
||
debugInfoSync(" 动态ID: \(moment.dynamicId)")
|
||
debugInfoSync(" 当前状态: \(localIsLike)")
|
||
debugInfoSync(" 请求状态: \(status)")
|
||
|
||
// 发起请求
|
||
let response: LikeDynamicResponse = try await apiService.request(request)
|
||
|
||
await MainActor.run {
|
||
isLikeLoading = false
|
||
// 处理响应, 只需要判断 code
|
||
if response.code == 200 {
|
||
localIsLike = !localIsLike
|
||
localLikeCount = localIsLike ? localLikeCount+1 : localLikeCount-1
|
||
debugInfoSync("✅ MomentListItem: 点赞操作成功")
|
||
debugInfoSync(" 动态ID: \(moment.dynamicId)")
|
||
debugInfoSync(" 新状态: \(localIsLike)")
|
||
debugInfoSync(" 新数量: \(localLikeCount)")
|
||
} else {
|
||
let errorMessage = response.message.isEmpty ? "点赞失败,请重试" : response.message
|
||
setAPILoadingErrorSync(UUID(), errorMessage: errorMessage)
|
||
debugErrorSync("❌ MomentListItem: 点赞操作失败")
|
||
debugErrorSync(" 动态ID: \(moment.dynamicId)")
|
||
debugErrorSync(" 错误: \(errorMessage)")
|
||
}
|
||
}
|
||
|
||
} catch {
|
||
await MainActor.run {
|
||
isLikeLoading = false
|
||
}
|
||
setAPILoadingErrorSync(UUID(), errorMessage: error.localizedDescription)
|
||
debugErrorSync("❌ MomentListItem: 点赞请求异常")
|
||
debugErrorSync(" 动态ID: \(moment.dynamicId)")
|
||
debugErrorSync(" 错误: \(error.localizedDescription)")
|
||
}
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 图片网格组件
|
||
struct MomentImageGrid: View {
|
||
let images: [MomentsPicture]
|
||
let onImageTap: (([String], Int)) -> Void // 新增:图片点击回调
|
||
|
||
var body: some View {
|
||
GeometryReader { geometry in
|
||
let availableWidth = max(geometry.size.width, 1)
|
||
let spacing: CGFloat = 8
|
||
if availableWidth < 10 {
|
||
Color.clear.frame(height: 1)
|
||
} else {
|
||
switch images.count {
|
||
case 1:
|
||
let imageSize: CGFloat = min(availableWidth * 0.6, 200)
|
||
HStack {
|
||
Spacer()
|
||
MomentSquareImageView(
|
||
image: images[0],
|
||
size: imageSize,
|
||
onTap: {
|
||
let imageUrls = images.compactMap { $0.resUrl }
|
||
onImageTap((imageUrls, 0))
|
||
}
|
||
)
|
||
Spacer()
|
||
}
|
||
case 2:
|
||
let imageSize: CGFloat = (availableWidth - spacing) / 2
|
||
HStack(spacing: spacing) {
|
||
MomentSquareImageView(
|
||
image: images[0],
|
||
size: imageSize,
|
||
onTap: {
|
||
let imageUrls = images.compactMap { $0.resUrl }
|
||
onImageTap((imageUrls, 0))
|
||
}
|
||
)
|
||
MomentSquareImageView(
|
||
image: images[1],
|
||
size: imageSize,
|
||
onTap: {
|
||
let imageUrls = images.compactMap { $0.resUrl }
|
||
onImageTap((imageUrls, 1))
|
||
}
|
||
)
|
||
}
|
||
case 3:
|
||
let imageSize: CGFloat = (availableWidth - spacing * 2) / 3
|
||
HStack(spacing: spacing) {
|
||
ForEach(Array(images.prefix(3).enumerated()), id: \.element.id) { index, image in
|
||
MomentSquareImageView(
|
||
image: image,
|
||
size: imageSize,
|
||
onTap: {
|
||
let imageUrls = images.compactMap { $0.resUrl }
|
||
onImageTap((imageUrls, index))
|
||
}
|
||
)
|
||
}
|
||
}
|
||
default:
|
||
let imageSize: CGFloat = (availableWidth - spacing * 2) / 3
|
||
let columns = Array(repeating: GridItem(.fixed(imageSize), spacing: spacing), count: 3)
|
||
LazyVGrid(columns: columns, spacing: spacing) {
|
||
ForEach(Array(images.prefix(9).enumerated()), id: \.element.id) { index, image in
|
||
MomentSquareImageView(
|
||
image: image,
|
||
size: imageSize,
|
||
onTap: {
|
||
let imageUrls = images.compactMap { $0.resUrl }
|
||
onImageTap((imageUrls, index))
|
||
}
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.frame(height: calculateGridHeight())
|
||
}
|
||
|
||
private func calculateGridHeight() -> CGFloat {
|
||
switch images.count {
|
||
case 1:
|
||
return 200
|
||
case 2:
|
||
return 120
|
||
case 3:
|
||
return 100
|
||
case 4...6:
|
||
return 216
|
||
default:
|
||
return 340
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 正方形图片视图组件
|
||
struct MomentSquareImageView: View {
|
||
let image: MomentsPicture
|
||
let size: CGFloat
|
||
let onTap: () -> Void // 新增:点击回调
|
||
|
||
var body: some View {
|
||
let safeSize = size.isFinite && size > 0 ? size : 100
|
||
Button(action: onTap) {
|
||
CachedAsyncImage(url: image.resUrl ?? "") { imageView in
|
||
imageView
|
||
.resizable()
|
||
.aspectRatio(contentMode: .fill)
|
||
} placeholder: {
|
||
Rectangle()
|
||
.fill(Color.gray.opacity(0.3))
|
||
.overlay(
|
||
ProgressView()
|
||
.progressViewStyle(CircularProgressViewStyle(tint: .white.opacity(0.6)))
|
||
.scaleEffect(0.8)
|
||
)
|
||
}
|
||
.frame(width: safeSize, height: safeSize)
|
||
.clipped()
|
||
.cornerRadius(8)
|
||
}
|
||
.buttonStyle(PlainButtonStyle()) // 使用PlainButtonStyle避免默认的按钮样式
|
||
}
|
||
}
|
||
|
||
//#Preview {
|
||
// // 创建测试数据
|
||
// let testMoment = MomentsInfo(
|
||
// dynamicId: 1,
|
||
// uid: 123456,
|
||
// nick: "测试用户",
|
||
// avatar: "",
|
||
// type: 0,
|
||
// content: "这是一条测试动态内容,用来测试 MomentListItem 的显示效果。",
|
||
// likeCount: 42,
|
||
// isLike: false,
|
||
// commentCount: 5,
|
||
// publishTime: Int(Date().timeIntervalSince1970 * 1000),
|
||
// worldId: 1,
|
||
// status: 1,
|
||
// 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
|
||
// )
|
||
//
|
||
// MomentListItem(
|
||
// moment: testMoment,
|
||
// onImageTap: { images, index in
|
||
// print("图片被点击: 索引 \(index), 图片数量 \(images.count)")
|
||
// }
|
||
// )
|
||
// .padding()
|
||
// .background(Color.black)
|
||
//}
|