Files
e-party-iOS/yana/Views/Components/OptimizedDynamicCardView.swift
edwinQQQ 57ba103996 feat: 新增用户ID显示组件和头像样式优化
- 创建UserIDDisplay组件,支持ID显示和复制功能,增强用户交互体验。
- 更新MeView中的头像样式,调整尺寸和边框,提升视觉效果。
- 修改OptimizedDynamicCardView以使用新组件,确保一致性和复用性。
- 新增icon_copy图标资源,支持复制功能的视觉反馈。
- 更新AppSettingView中的布局,优化用户界面体验。
2025-08-01 15:53:56 +08:00

332 lines
13 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
import ComposableArchitecture
import Foundation
// MARK: -
struct OptimizedDynamicCardView: View {
let moment: MomentsInfo
let allMoments: [MomentsInfo]
let currentIndex: Int
//
let onImageTap: (_ images: [String], _ index: Int) -> Void
//
let onLikeTap: (_ dynamicId: Int, _ uid: Int, _ likedUid: Int, _ worldId: Int) -> Void
//
let onCardTap: (() -> Void)?
//
let isDetailMode: Bool
// loading
let isLikeLoading: Bool
init(moment: MomentsInfo, allMoments: [MomentsInfo], currentIndex: Int, onImageTap: @escaping (_ images: [String], _ index: Int) -> Void, onLikeTap: @escaping (_ dynamicId: Int, _ uid: Int, _ likedUid: Int, _ worldId: Int) -> Void, onCardTap: (() -> Void)? = nil, isDetailMode: Bool = false, isLikeLoading: Bool = false) {
self.moment = moment
self.allMoments = allMoments
self.currentIndex = currentIndex
self.onImageTap = onImageTap
self.onLikeTap = onLikeTap
self.onCardTap = onCardTap
self.isDetailMode = isDetailMode
self.isLikeLoading = isLikeLoading
}
public var body: some View {
ZStack {
// -
if !isDetailMode {
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())
.allowsHitTesting(false) //
VStack(alignment: .leading, spacing: 2) {
Text(moment.nick)
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
.allowsHitTesting(false) //
UserIDDisplay(uid: moment.uid, fontSize: 12, textColor: .white.opacity(0.6))
.allowsHitTesting(false) //
}
Spacer()
// VIP
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)
.allowsHitTesting(false) //
}
//
if !moment.content.isEmpty {
Text(moment.content)
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.9))
.multilineTextAlignment(.leading)
.padding(.leading, 40 + 8) //
.allowsHitTesting(false) //
}
//
if let images = moment.dynamicResList, !images.isEmpty {
OptimizedImageGrid(images: images) { tappedIndex in
let urls = images.map { $0.resUrl ?? "" }
onImageTap(urls, tappedIndex)
}
.padding(.leading, 40 + 8)
.padding(.bottom, images.count == 2 ? 30 : 0) //
.allowsHitTesting(true) //
}
//
HStack(spacing: 20) {
// Like
Button(action: {
if !isLikeLoading {
onLikeTap(moment.dynamicId, moment.uid, moment.uid, moment.worldId)
}
}) {
HStack(spacing: 4) {
if isLikeLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: moment.isLike ? .red : .white.opacity(0.8)))
.scaleEffect(0.8)
} else {
Image(systemName: moment.isLike ? "heart.fill" : "heart")
.font(.system(size: 16))
}
Text("\(moment.likeCount)")
.font(.system(size: 14))
}
.foregroundColor(moment.isLike ? .red : .white.opacity(0.8))
}
.disabled(isLikeLoading)
.padding(.leading, 40 + 8) // +
.allowsHitTesting(true) // Like
Spacer()
}
.padding(.top, 8)
}
.padding(16)
// -
.contentShape(Rectangle())
.onTapGesture {
if !isDetailMode, let onCardTap = onCardTap {
onCardTap()
}
}
}
.onAppear {
preloadNearbyImages()
}
}
private func formatTime(_ 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)
if interval < 60 {
return "刚刚"
} else if interval < 3600 {
return "\(Int(interval / 60))分钟前"
} else if interval < 86400 {
return "\(Int(interval / 3600))小时前"
} else {
formatter.dateFormat = "MM-dd HH:mm"
return formatter.string(from: date)
}
}
//
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)
}
}
private func preloadNearbyImages() {
var urlsToPreload: [String] = []
let preloadRange = max(0, currentIndex - 2)...min(allMoments.count - 1, currentIndex + 2)
for index in preloadRange {
let moment = allMoments[index]
urlsToPreload.append(moment.avatar)
if let images = moment.dynamicResList {
urlsToPreload.append(contentsOf: images.compactMap { $0.resUrl })
}
}
ImageCacheManager.shared.preloadImages(urls: urlsToPreload)
}
// UIImageURL
}
// MARK: -
struct OptimizedImageGrid: View {
let images: [MomentsPicture]
let onImageTap: (Int) -> Void
init(images: [MomentsPicture], onImageTap: @escaping (Int) -> Void) {
self.images = images
self.onImageTap = onImageTap
}
public 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()
SquareImageView(image: images[0], size: imageSize) {
onImageTap(0)
}
Spacer()
}
case 2:
let imageSize: CGFloat = (availableWidth - spacing) / 2
HStack(spacing: spacing) {
SquareImageView(image: images[0], size: imageSize) {
onImageTap(0)
}
SquareImageView(image: images[1], size: imageSize) {
onImageTap(1)
}
}
case 3:
let imageSize: CGFloat = (availableWidth - spacing * 2) / 3
HStack(spacing: spacing) {
ForEach(Array(images.prefix(3).enumerated()), id: \ .element.id) { idx, image in
SquareImageView(image: image, size: imageSize) {
onImageTap(idx)
}
}
}
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) { idx, image in
SquareImageView(image: image, size: imageSize) {
onImageTap(idx)
}
}
}
}
}
}
.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 SquareImageView: View {
let image: MomentsPicture
let size: CGFloat
let onTap: (() -> Void)?
init(image: MomentsPicture, size: CGFloat, onTap: (() -> Void)? = nil) {
self.image = image
self.size = size
self.onTap = onTap
}
public var body: some View {
let safeSize = size.isFinite && size > 0 ? size : 100
Group {
if let onTap = onTap {
Button(action: onTap) {
imageContent
}
.buttonStyle(PlainButtonStyle())
} else {
imageContent
}
}
.frame(width: safeSize, height: safeSize)
.clipped()
.cornerRadius(8)
}
private var imageContent: some View {
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)
)
}
}
}