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