feat: 更新图片预览功能,支持本地与远程图片展示
- 在ImagePreviewPager中引入ImagePreviewSource枚举,支持本地和远程图片的统一处理。 - 优化OptimizedDynamicCardView,新增图片预览状态管理,集成全屏图片预览功能。 - 更新OptimizedImageGrid以支持图片点击事件,触发预览弹窗,提升用户体验。
This commit is contained in:
@@ -8,6 +8,11 @@ struct OptimizedDynamicCardView: View {
|
||||
let allMoments: [MomentsInfo]
|
||||
let currentIndex: Int
|
||||
|
||||
// 预览相关状态
|
||||
@State private var showPreview = false
|
||||
@State private var previewImageUrls: [String] = []
|
||||
@State private var previewIndex: Int = 0
|
||||
|
||||
init(moment: MomentsInfo, allMoments: [MomentsInfo], currentIndex: Int) {
|
||||
self.moment = moment
|
||||
self.allMoments = allMoments
|
||||
@@ -17,7 +22,7 @@ struct OptimizedDynamicCardView: View {
|
||||
public var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
// 用户信息
|
||||
HStack {
|
||||
HStack(alignment: .top) {
|
||||
// 头像
|
||||
CachedAsyncImage(url: moment.avatar) { image in
|
||||
image
|
||||
@@ -34,56 +39,48 @@ struct OptimizedDynamicCardView: View {
|
||||
}
|
||||
.frame(width: 40, height: 40)
|
||||
.clipShape(Circle())
|
||||
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(moment.nick)
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text(formatTime(moment.publishTime))
|
||||
Text("ID: \(moment.uid)")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.white.opacity(0.6))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// VIP 标识
|
||||
if let vipInfo = moment.userVipInfoVO, let vipLevel = vipInfo.vipLevel {
|
||||
Text("VIP\(vipLevel)")
|
||||
.font(.system(size: 10, weight: .bold))
|
||||
.foregroundColor(.yellow)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(Color.yellow.opacity(0.2))
|
||||
.cornerRadius(4)
|
||||
}
|
||||
// 时间(原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)
|
||||
}
|
||||
|
||||
|
||||
// 动态内容
|
||||
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 {
|
||||
OptimizedImageGrid(images: images)
|
||||
OptimizedImageGrid(images: images) { tappedIndex in
|
||||
previewImageUrls = images.map { $0.resUrl }
|
||||
previewIndex = tappedIndex
|
||||
showPreview = true
|
||||
}
|
||||
.padding(.bottom, images.count == 2 ? 16 : 0) // 两张图片时增加底部间距
|
||||
}
|
||||
|
||||
|
||||
// 互动按钮
|
||||
HStack(spacing: 20) {
|
||||
Button(action: {}) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "message")
|
||||
.font(.system(size: 16))
|
||||
Text("\(moment.commentCount)")
|
||||
.font(.system(size: 14))
|
||||
}
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
|
||||
// Like 按钮左对齐
|
||||
Button(action: {}) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: moment.isLike ? "heart.fill" : "heart")
|
||||
@@ -93,7 +90,6 @@ struct OptimizedDynamicCardView: View {
|
||||
}
|
||||
.foregroundColor(moment.isLike ? .red : .white.opacity(0.8))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top, 8)
|
||||
@@ -106,6 +102,13 @@ struct OptimizedDynamicCardView: View {
|
||||
.onAppear {
|
||||
preloadNearbyImages()
|
||||
}
|
||||
// 图片预览弹窗
|
||||
.fullScreenCover(isPresented: $showPreview) {
|
||||
ImagePreviewPager(images: previewImageUrls, currentIndex: $previewIndex) {
|
||||
showPreview = false
|
||||
previewImageUrls = []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func formatTime(_ timestamp: Int) -> String {
|
||||
@@ -128,6 +131,28 @@ struct OptimizedDynamicCardView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:时间显示逻辑
|
||||
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)
|
||||
@@ -140,14 +165,18 @@ struct OptimizedDynamicCardView: View {
|
||||
}
|
||||
ImageCacheManager.shared.preloadImages(urls: urlsToPreload)
|
||||
}
|
||||
|
||||
// 移除批量下载UIImage逻辑,直接用URL数组
|
||||
}
|
||||
|
||||
// MARK: - 优化的图片网格
|
||||
struct OptimizedImageGrid: View {
|
||||
let images: [MomentsPicture]
|
||||
let onImageTap: (Int) -> Void
|
||||
|
||||
init(images: [MomentsPicture]) {
|
||||
init(images: [MomentsPicture], onImageTap: @escaping (Int) -> Void) {
|
||||
self.images = images
|
||||
self.onImageTap = onImageTap
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
@@ -162,28 +191,38 @@ struct OptimizedImageGrid: View {
|
||||
let imageSize: CGFloat = min(availableWidth * 0.6, 200)
|
||||
HStack {
|
||||
Spacer()
|
||||
SquareImageView(image: images[0], size: imageSize)
|
||||
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)
|
||||
SquareImageView(image: images[1], size: imageSize)
|
||||
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(images.prefix(3), id: \ .id) { image in
|
||||
SquareImageView(image: image, size: imageSize)
|
||||
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(images.prefix(9), id: \ .id) { image in
|
||||
SquareImageView(image: image, size: imageSize)
|
||||
ForEach(Array(images.prefix(9).enumerated()), id: \ .element.id) { idx, image in
|
||||
SquareImageView(image: image, size: imageSize) {
|
||||
onImageTap(idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -212,14 +251,32 @@ struct OptimizedImageGrid: View {
|
||||
struct SquareImageView: View {
|
||||
let image: MomentsPicture
|
||||
let size: CGFloat
|
||||
let onTap: (() -> Void)?
|
||||
|
||||
init(image: MomentsPicture, size: CGFloat) {
|
||||
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()
|
||||
@@ -233,8 +290,5 @@ struct SquareImageView: View {
|
||||
.scaleEffect(0.8)
|
||||
)
|
||||
}
|
||||
.frame(width: safeSize, height: safeSize)
|
||||
.clipped()
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,20 +1,78 @@
|
||||
import SwiftUI
|
||||
|
||||
enum ImagePreviewSource: Identifiable, Equatable {
|
||||
case local(UIImage)
|
||||
case remote(String)
|
||||
var id: String {
|
||||
switch self {
|
||||
case .local(let img):
|
||||
return String(describing: img.hashValue)
|
||||
case .remote(let url):
|
||||
return url
|
||||
}
|
||||
}
|
||||
static func == (lhs: ImagePreviewSource, rhs: ImagePreviewSource) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (.local(l), .local(r)):
|
||||
return l.pngData() == r.pngData()
|
||||
case let (.remote(l), .remote(r)):
|
||||
return l == r
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ImagePreviewPager: View {
|
||||
let images: [UIImage]
|
||||
let images: [ImagePreviewSource]
|
||||
@Binding var currentIndex: Int
|
||||
let onClose: () -> Void
|
||||
|
||||
// 本地图片构造器
|
||||
init(images: [UIImage], currentIndex: Binding<Int>, onClose: @escaping () -> Void) {
|
||||
self.images = images.map { .local($0) }
|
||||
self._currentIndex = currentIndex
|
||||
self.onClose = onClose
|
||||
}
|
||||
// 远程图片构造器
|
||||
init(images: [String], currentIndex: Binding<Int>, onClose: @escaping () -> Void) {
|
||||
self.images = images.map { .remote($0) }
|
||||
self._currentIndex = currentIndex
|
||||
self.onClose = onClose
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Color.black.ignoresSafeArea()
|
||||
TabView(selection: $currentIndex) {
|
||||
ForEach(images.indices, id: \Int.self) { idx in
|
||||
Image(uiImage: images[idx])
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Color.black)
|
||||
.tag(idx)
|
||||
ForEach(Array(images.enumerated()), id: \ .element.id) { idx, source in
|
||||
Group {
|
||||
switch source {
|
||||
case .local(let img):
|
||||
Image(uiImage: img)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Color.black)
|
||||
case .remote(let urlStr):
|
||||
CachedAsyncImage(url: urlStr) { image in
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Color.black)
|
||||
} placeholder: {
|
||||
Rectangle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.overlay(
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white.opacity(0.6)))
|
||||
.scaleEffect(0.8)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.tag(idx)
|
||||
}
|
||||
}
|
||||
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
|
||||
|
Reference in New Issue
Block a user