Files
e-party-iOS/yana/MVVM/View/MomentListItem.swift
edwinQQQ 6b960f53b4 feat: 更新Splash视图及登录模型逻辑
- 将SplashV2替换为SplashPage,优化应用启动流程。
- 在LoginModels中将用户ID参数更改为加密后的ID,增强安全性。
- 更新AppConfig中的API基础URL,确保与生产环境一致。
- 在CommonComponents中添加底部Tab栏图标映射逻辑,提升用户体验。
- 新增NineGridImagePicker组件,支持多图选择功能,优化CreateFeedPage的图片选择逻辑。
- 移除冗余的BottomTabView组件,简化视图结构,提升代码整洁性。
- 在MainPage中整合创建动态页面的逻辑,优化导航体验。
- 新增MePage视图,提供用户信息管理功能,增强应用的可用性。
2025-09-26 10:53:00 +08:00

398 lines
15 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: - 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)
//}