feat: 更新动态相关数据模型及视图组件
- 在DynamicsModels.swift中为动态响应结构和列表数据添加Sendable协议,提升并发安全性。 - 在FeedListFeature.swift中实现动态内容的加载逻辑,集成API请求以获取最新动态。 - 在FeedListView.swift中新增动态内容列表展示,优化用户交互体验。 - 在MeView.swift中添加设置按钮,支持弹出设置视图。 - 在SettingView.swift中新增COS上传测试功能,允许用户测试图片上传至腾讯云COS。 - 在OptimizedDynamicCardView.swift中实现优化的动态卡片组件,提升动态展示效果。
This commit is contained in:
@@ -4,7 +4,7 @@ import ComposableArchitecture
|
||||
// MARK: - 响应数据模型
|
||||
|
||||
/// 最新动态响应结构
|
||||
struct MomentsLatestResponse: Codable, Equatable {
|
||||
struct MomentsLatestResponse: Codable, Equatable, Sendable {
|
||||
let code: Int
|
||||
let message: String
|
||||
let data: MomentsListData?
|
||||
@@ -12,13 +12,13 @@ struct MomentsLatestResponse: Codable, Equatable {
|
||||
}
|
||||
|
||||
/// 动态列表数据
|
||||
struct MomentsListData: Codable, Equatable {
|
||||
struct MomentsListData: Codable, Equatable, Sendable {
|
||||
let dynamicList: [MomentsInfo]
|
||||
let nextDynamicId: Int
|
||||
}
|
||||
|
||||
/// 动态信息结构
|
||||
struct MomentsInfo: Codable, Equatable {
|
||||
public struct MomentsInfo: Codable, Equatable, Sendable {
|
||||
let dynamicId: Int
|
||||
let uid: Int
|
||||
let nick: String
|
||||
@@ -66,7 +66,7 @@ struct MomentsInfo: Codable, Equatable {
|
||||
}
|
||||
|
||||
/// 动态图片信息
|
||||
struct MomentsPicture: Codable, Equatable {
|
||||
struct MomentsPicture: Codable, Equatable, Sendable {
|
||||
let id: Int
|
||||
let resUrl: String
|
||||
let format: String
|
||||
@@ -76,7 +76,7 @@ struct MomentsPicture: Codable, Equatable {
|
||||
}
|
||||
|
||||
/// 用户VIP信息 - 完整版本,所有字段都是可选的
|
||||
struct UserVipInfo: Codable, Equatable {
|
||||
struct UserVipInfo: Codable, Equatable, Sendable {
|
||||
let vipLevel: Int?
|
||||
let vipName: String?
|
||||
let vipIcon: String?
|
||||
|
@@ -1,12 +1,16 @@
|
||||
import Foundation
|
||||
import ComposableArchitecture
|
||||
|
||||
struct FeedListFeature: Reducer {
|
||||
@Reducer
|
||||
struct FeedListFeature {
|
||||
@Dependency(\.apiService) var apiService
|
||||
struct State: Equatable {
|
||||
var feeds: [Feed] = [] // 预留 feed 内容
|
||||
var isLoading: Bool = false
|
||||
var error: String? = nil
|
||||
var isEditFeedPresented: Bool = false // 新增:控制 EditFeedView 弹窗
|
||||
// 新增:动态内容
|
||||
var moments: [MomentsInfo] = []
|
||||
}
|
||||
|
||||
enum Action: Equatable {
|
||||
@@ -15,13 +19,41 @@ struct FeedListFeature: Reducer {
|
||||
case loadMore
|
||||
case editFeedButtonTapped // 新增:点击 add 按钮
|
||||
case editFeedDismissed // 新增:关闭编辑页
|
||||
// 新增:动态内容相关
|
||||
case fetchFeeds
|
||||
case fetchFeedsResponse(TaskResult<MomentsLatestResponse>)
|
||||
// 预留后续 Action
|
||||
}
|
||||
|
||||
func reduce(into state: inout State, action: Action) -> Effect<Action> {
|
||||
switch action {
|
||||
case .onAppear:
|
||||
// 预留数据加载逻辑
|
||||
// 页面展示时自动请求 feed 数据
|
||||
return .send(.fetchFeeds)
|
||||
case .fetchFeeds:
|
||||
state.isLoading = true
|
||||
state.error = nil
|
||||
// 发起 API 请求
|
||||
return .run { [apiService] send in
|
||||
await send(.fetchFeedsResponse(TaskResult {
|
||||
let request = LatestDynamicsRequest(dynamicId: "", pageSize: 20, types: [.text, .picture])
|
||||
return try await apiService.request(request)
|
||||
}))
|
||||
}
|
||||
case let .fetchFeedsResponse(.success(response)):
|
||||
state.isLoading = false
|
||||
if let list = response.data?.dynamicList {
|
||||
state.moments = list
|
||||
state.error = nil
|
||||
} else {
|
||||
state.moments = []
|
||||
state.error = response.message
|
||||
}
|
||||
return .none
|
||||
case let .fetchFeedsResponse(.failure(error)):
|
||||
state.isLoading = false
|
||||
state.moments = []
|
||||
state.error = error.localizedDescription
|
||||
return .none
|
||||
case .reload:
|
||||
// 预留刷新逻辑
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import ComposableArchitecture
|
||||
import QCloudCOSXML
|
||||
|
||||
// MARK: - 腾讯云 COS 管理器
|
||||
|
||||
@@ -15,6 +16,26 @@ class COSManager: ObservableObject {
|
||||
|
||||
private init() {}
|
||||
|
||||
// 幂等初始化标记
|
||||
private static var isCOSInitialized = false
|
||||
|
||||
// 幂等初始化方法
|
||||
private func ensureCOSInitialized(tokenData: TcTokenData) {
|
||||
guard !Self.isCOSInitialized else { return }
|
||||
let configuration = QCloudServiceConfiguration()
|
||||
let endpoint = QCloudCOSXMLEndPoint()
|
||||
endpoint.regionName = tokenData.region
|
||||
endpoint.useHTTPS = true
|
||||
if tokenData.accelerate {
|
||||
endpoint.suffix = "cos.accelerate.myqcloud.com"
|
||||
}
|
||||
configuration.endpoint = endpoint
|
||||
QCloudCOSXMLService.registerDefaultCOSXML(with: configuration)
|
||||
QCloudCOSTransferMangerService.registerDefaultCOSTransferManger(with: configuration)
|
||||
Self.isCOSInitialized = true
|
||||
debugInfoSync("✅ COS服务已初始化,region: \(tokenData.region)")
|
||||
}
|
||||
|
||||
// MARK: - Token 管理
|
||||
|
||||
/// 当前缓存的 Token 信息
|
||||
@@ -102,13 +123,83 @@ class COSManager: ObservableObject {
|
||||
|
||||
/// 获取当前 Token 状态信息
|
||||
func getTokenStatus() -> String {
|
||||
if let cached = cachedToken, let expiration = tokenExpirationDate {
|
||||
if let _ = cachedToken, let expiration = tokenExpirationDate {
|
||||
let isExpired = Date() >= expiration
|
||||
return "Token 状态: \(isExpired ? "已过期" : "有效"), 过期时间: \(expiration)"
|
||||
} else {
|
||||
return "Token 状态: 未缓存"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 上传功能
|
||||
|
||||
/// 上传图片到腾讯云 COS
|
||||
/// - Parameters:
|
||||
/// - imageData: 图片数据
|
||||
/// - apiService: API 服务实例
|
||||
/// - Returns: 上传成功的云地址,如果失败返回 nil
|
||||
func uploadImage(_ imageData: Data, apiService: any APIServiceProtocol & Sendable) async -> String? {
|
||||
guard let tokenData = await getToken(apiService: apiService) else {
|
||||
debugInfoSync("❌ 无法获取 COS Token")
|
||||
return nil
|
||||
}
|
||||
// 上传前确保COS服务已初始化
|
||||
ensureCOSInitialized(tokenData: tokenData)
|
||||
|
||||
// 初始化 COS 配置
|
||||
let credential = QCloudCredential()
|
||||
credential.secretID = tokenData.secretId
|
||||
// 打印secretKey原始内容,去除首尾空白
|
||||
let rawSecretKey = tokenData.secretKey.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
debugInfoSync("secretKey原始内容: [\(rawSecretKey)]")
|
||||
credential.secretKey = rawSecretKey
|
||||
credential.token = tokenData.sessionToken
|
||||
credential.startDate = tokenData.startDate
|
||||
credential.expirationDate = tokenData.expirationDate
|
||||
|
||||
let request = QCloudCOSXMLUploadObjectRequest<AnyObject>()
|
||||
request.bucket = tokenData.bucket
|
||||
request.regionName = tokenData.region
|
||||
request.credential = credential
|
||||
|
||||
// 生成唯一 key
|
||||
let fileExtension = "jpg" // 假设为 JPG,可根据实际调整
|
||||
let key = "images/\(UUID().uuidString).\(fileExtension)"
|
||||
request.object = key
|
||||
request.body = imageData as AnyObject
|
||||
|
||||
//监听上传进度
|
||||
request.sendProcessBlock = { (bytesSent, totalBytesSent,
|
||||
totalBytesExpectedToSend) in
|
||||
debugInfoSync("\(bytesSent), \(totalBytesSent), \(totalBytesExpectedToSend)")
|
||||
// bytesSent 本次要发送的字节数(一个大文件可能要分多次发送)
|
||||
// totalBytesSent 已发送的字节数
|
||||
// totalBytesExpectedToSend 本次上传要发送的总字节数(即一个文件大小)
|
||||
};
|
||||
|
||||
// 设置加速
|
||||
if tokenData.accelerate {
|
||||
request.enableQuic = true
|
||||
// endpoint 增加 "cos.accelerate.myqcloud.com"
|
||||
}
|
||||
|
||||
// 使用 async/await 包装上传回调
|
||||
return await withCheckedContinuation { continuation in
|
||||
request.setFinish { result, error in
|
||||
if let error = error {
|
||||
debugInfoSync("❌ 图片上传失败: \(error.localizedDescription)")
|
||||
continuation.resume(returning: " ?????????? ")
|
||||
} else {
|
||||
// 构建云地址
|
||||
let domain = tokenData.customDomain.isEmpty ? "\(tokenData.bucket).cos.\(tokenData.region).myqcloud.com" : tokenData.customDomain
|
||||
let cloudURL = "https://\(domain)/\(key)"
|
||||
debugInfoSync("✅ 图片上传成功: \(cloudURL)")
|
||||
continuation.resume(returning: cloudURL)
|
||||
}
|
||||
}
|
||||
QCloudCOSTransferMangerService.defaultCOSTransferManager().uploadObject(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 调试扩展
|
||||
@@ -134,4 +225,4 @@ extension COSManager {
|
||||
debugInfoSync("✅ 腾讯云 COS Token 测试完成\n")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
240
yana/Views/Components/OptimizedDynamicCardView.swift
Normal file
240
yana/Views/Components/OptimizedDynamicCardView.swift
Normal file
@@ -0,0 +1,240 @@
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
import Foundation
|
||||
|
||||
// MARK: - 优化的动态卡片组件
|
||||
struct OptimizedDynamicCardView: View {
|
||||
let moment: MomentsInfo
|
||||
let allMoments: [MomentsInfo]
|
||||
let currentIndex: Int
|
||||
|
||||
init(moment: MomentsInfo, allMoments: [MomentsInfo], currentIndex: Int) {
|
||||
self.moment = moment
|
||||
self.allMoments = allMoments
|
||||
self.currentIndex = currentIndex
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
// 用户信息
|
||||
HStack {
|
||||
// 头像
|
||||
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)
|
||||
|
||||
Text(formatTime(moment.publishTime))
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
||||
// 动态内容
|
||||
if !moment.content.isEmpty {
|
||||
Text(moment.content)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
.multilineTextAlignment(.leading)
|
||||
}
|
||||
|
||||
// 优化的图片网格
|
||||
if let images = moment.dynamicResList, !images.isEmpty {
|
||||
OptimizedImageGrid(images: images)
|
||||
}
|
||||
|
||||
// 互动按钮
|
||||
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))
|
||||
}
|
||||
|
||||
Button(action: {}) {
|
||||
HStack(spacing: 4) {
|
||||
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))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top, 8)
|
||||
}
|
||||
.padding(16)
|
||||
.background(
|
||||
Color.white.opacity(0.1)
|
||||
.cornerRadius(12)
|
||||
)
|
||||
.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 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.map { $0.resUrl })
|
||||
}
|
||||
}
|
||||
ImageCacheManager.shared.preloadImages(urls: urlsToPreload)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 优化的图片网格
|
||||
struct OptimizedImageGrid: View {
|
||||
let images: [MomentsPicture]
|
||||
|
||||
init(images: [MomentsPicture]) {
|
||||
self.images = images
|
||||
}
|
||||
|
||||
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)
|
||||
Spacer()
|
||||
}
|
||||
case 2:
|
||||
let imageSize: CGFloat = (availableWidth - spacing) / 2
|
||||
HStack(spacing: spacing) {
|
||||
SquareImageView(image: images[0], size: imageSize)
|
||||
SquareImageView(image: images[1], size: imageSize)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.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
|
||||
|
||||
init(image: MomentsPicture, size: CGFloat) {
|
||||
self.image = image
|
||||
self.size = size
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
let safeSize = size.isFinite && size > 0 ? size : 100
|
||||
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)
|
||||
}
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
//import OptimizedDynamicCardView // 导入新组件
|
||||
|
||||
struct FeedListView: View {
|
||||
let store: StoreOf<FeedListFeature>
|
||||
@@ -46,6 +47,35 @@ struct FeedListView: View {
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
.padding(.horizontal, 30)
|
||||
.padding(.bottom, 30)
|
||||
// 新增:动态内容列表
|
||||
if viewStore.isLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.padding(.top, 20)
|
||||
} else if let error = viewStore.error {
|
||||
Text(error)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.red)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 20)
|
||||
} else if viewStore.moments.isEmpty {
|
||||
Text(NSLocalizedString("feedList.empty", comment: "暂无动态"))
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
.padding(.top, 20)
|
||||
} else {
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 16) {
|
||||
ForEach(Array(viewStore.moments.enumerated()), id: \ .element.dynamicId) { index, moment in
|
||||
OptimizedDynamicCardView(moment: moment, allMoments: viewStore.moments, currentIndex: index)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .top)
|
||||
|
@@ -172,460 +172,460 @@ struct FeedView: View {
|
||||
}
|
||||
|
||||
// MARK: - 优化的动态卡片组件
|
||||
struct OptimizedDynamicCardView: View {
|
||||
let moment: MomentsInfo
|
||||
let allMoments: [MomentsInfo]
|
||||
let currentIndex: Int
|
||||
|
||||
var body: some View {
|
||||
WithPerceptionTracking{
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
// 用户信息
|
||||
HStack {
|
||||
// 使用缓存的头像
|
||||
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)
|
||||
|
||||
Text(formatTime(moment.publishTime))
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
||||
// 动态内容
|
||||
if !moment.content.isEmpty {
|
||||
Text(moment.content)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
.multilineTextAlignment(.leading)
|
||||
}
|
||||
|
||||
// 优化的图片网格
|
||||
if let images = moment.dynamicResList, !images.isEmpty {
|
||||
OptimizedImageGrid(images: images)
|
||||
}
|
||||
|
||||
// 互动按钮
|
||||
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))
|
||||
}
|
||||
|
||||
Button(action: {}) {
|
||||
HStack(spacing: 4) {
|
||||
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))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top, 8)
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
.background(
|
||||
Color.white.opacity(0.1)
|
||||
.cornerRadius(12)
|
||||
)
|
||||
.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 preloadNearbyImages() {
|
||||
var urlsToPreload: [String] = []
|
||||
|
||||
// 预加载前后2个动态的图片
|
||||
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.map { $0.resUrl })
|
||||
}
|
||||
}
|
||||
|
||||
// 异步预加载
|
||||
ImageCacheManager.shared.preloadImages(urls: urlsToPreload)
|
||||
}
|
||||
}
|
||||
//struct OptimizedDynamicCardView: View {
|
||||
// let moment: MomentsInfo
|
||||
// let allMoments: [MomentsInfo]
|
||||
// let currentIndex: Int
|
||||
//
|
||||
// var body: some View {
|
||||
// WithPerceptionTracking{
|
||||
// VStack(alignment: .leading, spacing: 12) {
|
||||
// // 用户信息
|
||||
// HStack {
|
||||
// // 使用缓存的头像
|
||||
// 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)
|
||||
//
|
||||
// Text(formatTime(moment.publishTime))
|
||||
// .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)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // 动态内容
|
||||
// if !moment.content.isEmpty {
|
||||
// Text(moment.content)
|
||||
// .font(.system(size: 14))
|
||||
// .foregroundColor(.white.opacity(0.9))
|
||||
// .multilineTextAlignment(.leading)
|
||||
// }
|
||||
//
|
||||
// // 优化的图片网格
|
||||
// if let images = moment.dynamicResList, !images.isEmpty {
|
||||
// OptimizedImageGrid(images: images)
|
||||
// }
|
||||
//
|
||||
// // 互动按钮
|
||||
// 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))
|
||||
// }
|
||||
//
|
||||
// Button(action: {}) {
|
||||
// HStack(spacing: 4) {
|
||||
// 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))
|
||||
// }
|
||||
//
|
||||
// Spacer()
|
||||
// }
|
||||
// .padding(.top, 8)
|
||||
// }
|
||||
// }
|
||||
// .padding(16)
|
||||
// .background(
|
||||
// Color.white.opacity(0.1)
|
||||
// .cornerRadius(12)
|
||||
// )
|
||||
// .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 preloadNearbyImages() {
|
||||
// var urlsToPreload: [String] = []
|
||||
//
|
||||
// // 预加载前后2个动态的图片
|
||||
// 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.map { $0.resUrl })
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // 异步预加载
|
||||
// ImageCacheManager.shared.preloadImages(urls: urlsToPreload)
|
||||
// }
|
||||
//}
|
||||
|
||||
// MARK: - 优化的图片网格
|
||||
struct OptimizedImageGrid: View {
|
||||
let images: [MomentsPicture]
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
let availableWidth = max(geometry.size.width, 1) // 防止为0或负数
|
||||
let spacing: CGFloat = 8
|
||||
|
||||
// 保护:如果availableWidth不合理,直接返回空视图
|
||||
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)
|
||||
Spacer()
|
||||
}
|
||||
case 2:
|
||||
// 两张图片:并排显示
|
||||
let imageSize: CGFloat = (availableWidth - spacing) / 2
|
||||
HStack(spacing: spacing) {
|
||||
SquareImageView(image: images[0], size: imageSize)
|
||||
SquareImageView(image: images[1], size: imageSize)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
default:
|
||||
// 四张及以上:九宫格布局(最多9张)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.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 // 九宫格2行的高度 (实际图片大小 * 2 + 间距)
|
||||
default:
|
||||
return 340 // 九宫格3行的高度 (实际图片大小 * 3 + 间距 + 额外空间)
|
||||
}
|
||||
}
|
||||
}
|
||||
//struct OptimizedImageGrid: View {
|
||||
// let images: [MomentsPicture]
|
||||
//
|
||||
// var body: some View {
|
||||
// GeometryReader { geometry in
|
||||
// let availableWidth = max(geometry.size.width, 1) // 防止为0或负数
|
||||
// let spacing: CGFloat = 8
|
||||
//
|
||||
// // 保护:如果availableWidth不合理,直接返回空视图
|
||||
// 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)
|
||||
// Spacer()
|
||||
// }
|
||||
// case 2:
|
||||
// // 两张图片:并排显示
|
||||
// let imageSize: CGFloat = (availableWidth - spacing) / 2
|
||||
// HStack(spacing: spacing) {
|
||||
// SquareImageView(image: images[0], size: imageSize)
|
||||
// SquareImageView(image: images[1], size: imageSize)
|
||||
// }
|
||||
// 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)
|
||||
// }
|
||||
// }
|
||||
// default:
|
||||
// // 四张及以上:九宫格布局(最多9张)
|
||||
// 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)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .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 // 九宫格2行的高度 (实际图片大小 * 2 + 间距)
|
||||
// default:
|
||||
// return 340 // 九宫格3行的高度 (实际图片大小 * 3 + 间距 + 额外空间)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
// MARK: - 正方形图片视图组件
|
||||
struct SquareImageView: View {
|
||||
let image: MomentsPicture
|
||||
let size: CGFloat
|
||||
|
||||
var body: some View {
|
||||
let safeSize = size.isFinite && size > 0 ? size : 100 // 防止非有限或负数
|
||||
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)
|
||||
}
|
||||
}
|
||||
//struct SquareImageView: View {
|
||||
// let image: MomentsPicture
|
||||
// let size: CGFloat
|
||||
//
|
||||
// var body: some View {
|
||||
// let safeSize = size.isFinite && size > 0 ? size : 100 // 防止非有限或负数
|
||||
// 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)
|
||||
// }
|
||||
//}
|
||||
|
||||
// MARK: - 旧的真实动态卡片组件(保留备用)
|
||||
struct RealDynamicCardView: View {
|
||||
let moment: MomentsInfo
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
// 用户信息
|
||||
HStack {
|
||||
AsyncImage(url: URL(string: 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)
|
||||
|
||||
Text(formatTime(moment.publishTime))
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
||||
// 动态内容
|
||||
if !moment.content.isEmpty {
|
||||
Text(moment.content)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
.multilineTextAlignment(.leading)
|
||||
}
|
||||
|
||||
// 图片网格
|
||||
if let images = moment.dynamicResList, !images.isEmpty {
|
||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: min(images.count, 3)), spacing: 8) {
|
||||
ForEach(images.prefix(9), id: \.id) { image in
|
||||
AsyncImage(url: URL(string: 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)))
|
||||
)
|
||||
}
|
||||
.frame(height: 100)
|
||||
.clipped()
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 互动按钮
|
||||
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))
|
||||
}
|
||||
|
||||
Button(action: {}) {
|
||||
HStack(spacing: 4) {
|
||||
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))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top, 8)
|
||||
}
|
||||
.padding(16)
|
||||
.background(
|
||||
Color.white.opacity(0.1)
|
||||
.cornerRadius(12)
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
//struct RealDynamicCardView: View {
|
||||
// let moment: MomentsInfo
|
||||
//
|
||||
// var body: some View {
|
||||
// VStack(alignment: .leading, spacing: 12) {
|
||||
// // 用户信息
|
||||
// HStack {
|
||||
// AsyncImage(url: URL(string: 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)
|
||||
//
|
||||
// Text(formatTime(moment.publishTime))
|
||||
// .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)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // 动态内容
|
||||
// if !moment.content.isEmpty {
|
||||
// Text(moment.content)
|
||||
// .font(.system(size: 14))
|
||||
// .foregroundColor(.white.opacity(0.9))
|
||||
// .multilineTextAlignment(.leading)
|
||||
// }
|
||||
//
|
||||
// // 图片网格
|
||||
// if let images = moment.dynamicResList, !images.isEmpty {
|
||||
// LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: min(images.count, 3)), spacing: 8) {
|
||||
// ForEach(images.prefix(9), id: \.id) { image in
|
||||
// AsyncImage(url: URL(string: 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)))
|
||||
// )
|
||||
// }
|
||||
// .frame(height: 100)
|
||||
// .clipped()
|
||||
// .cornerRadius(8)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // 互动按钮
|
||||
// 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))
|
||||
// }
|
||||
//
|
||||
// Button(action: {}) {
|
||||
// HStack(spacing: 4) {
|
||||
// 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))
|
||||
// }
|
||||
//
|
||||
// Spacer()
|
||||
// }
|
||||
// .padding(.top, 8)
|
||||
// }
|
||||
// .padding(16)
|
||||
// .background(
|
||||
// Color.white.opacity(0.1)
|
||||
// .cornerRadius(12)
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// 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)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
// MARK: - 旧的模拟卡片组件(保留备用)
|
||||
struct DynamicCardView: View {
|
||||
let index: Int
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
// 用户信息
|
||||
HStack {
|
||||
Circle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 40, height: 40)
|
||||
.overlay(
|
||||
Text("U\(index + 1)")
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("用户\(index + 1)")
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text("2小时前")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.white.opacity(0.6))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
// 动态内容
|
||||
Text("今天是美好的一天,分享一些生活中的小确幸。希望大家都能珍惜每一个当下的时刻。")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
.multilineTextAlignment(.leading)
|
||||
|
||||
// 图片网格
|
||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 3), spacing: 8) {
|
||||
ForEach(0..<3) { imageIndex in
|
||||
Rectangle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.overlay(
|
||||
Image(systemName: "photo")
|
||||
.foregroundColor(.white.opacity(0.6))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 互动按钮
|
||||
HStack(spacing: 20) {
|
||||
Button(action: {}) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "message")
|
||||
.font(.system(size: 16))
|
||||
Text("354")
|
||||
.font(.system(size: 14))
|
||||
}
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
|
||||
Button(action: {}) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "heart")
|
||||
.font(.system(size: 16))
|
||||
Text("354")
|
||||
.font(.system(size: 14))
|
||||
}
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top, 8)
|
||||
}
|
||||
.padding(16)
|
||||
.background(
|
||||
Color.white.opacity(0.1)
|
||||
.cornerRadius(12)
|
||||
)
|
||||
}
|
||||
}
|
||||
//struct DynamicCardView: View {
|
||||
// let index: Int
|
||||
//
|
||||
// var body: some View {
|
||||
// VStack(alignment: .leading, spacing: 12) {
|
||||
// // 用户信息
|
||||
// HStack {
|
||||
// Circle()
|
||||
// .fill(Color.gray.opacity(0.3))
|
||||
// .frame(width: 40, height: 40)
|
||||
// .overlay(
|
||||
// Text("U\(index + 1)")
|
||||
// .font(.system(size: 16, weight: .medium))
|
||||
// .foregroundColor(.white)
|
||||
// )
|
||||
//
|
||||
// VStack(alignment: .leading, spacing: 2) {
|
||||
// Text("用户\(index + 1)")
|
||||
// .font(.system(size: 16, weight: .medium))
|
||||
// .foregroundColor(.white)
|
||||
//
|
||||
// Text("2小时前")
|
||||
// .font(.system(size: 12))
|
||||
// .foregroundColor(.white.opacity(0.6))
|
||||
// }
|
||||
//
|
||||
// Spacer()
|
||||
// }
|
||||
//
|
||||
// // 动态内容
|
||||
// Text("今天是美好的一天,分享一些生活中的小确幸。希望大家都能珍惜每一个当下的时刻。")
|
||||
// .font(.system(size: 14))
|
||||
// .foregroundColor(.white.opacity(0.9))
|
||||
// .multilineTextAlignment(.leading)
|
||||
//
|
||||
// // 图片网格
|
||||
// LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 3), spacing: 8) {
|
||||
// ForEach(0..<3) { imageIndex in
|
||||
// Rectangle()
|
||||
// .fill(Color.gray.opacity(0.3))
|
||||
// .aspectRatio(1, contentMode: .fit)
|
||||
// .overlay(
|
||||
// Image(systemName: "photo")
|
||||
// .foregroundColor(.white.opacity(0.6))
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // 互动按钮
|
||||
// HStack(spacing: 20) {
|
||||
// Button(action: {}) {
|
||||
// HStack(spacing: 4) {
|
||||
// Image(systemName: "message")
|
||||
// .font(.system(size: 16))
|
||||
// Text("354")
|
||||
// .font(.system(size: 14))
|
||||
// }
|
||||
// .foregroundColor(.white.opacity(0.8))
|
||||
// }
|
||||
//
|
||||
// Button(action: {}) {
|
||||
// HStack(spacing: 4) {
|
||||
// Image(systemName: "heart")
|
||||
// .font(.system(size: 16))
|
||||
// Text("354")
|
||||
// .font(.system(size: 14))
|
||||
// }
|
||||
// .foregroundColor(.white.opacity(0.8))
|
||||
// }
|
||||
//
|
||||
// Spacer()
|
||||
// }
|
||||
// .padding(.top, 8)
|
||||
// }
|
||||
// .padding(16)
|
||||
// .background(
|
||||
// Color.white.opacity(0.1)
|
||||
// .cornerRadius(12)
|
||||
// )
|
||||
// }
|
||||
//}
|
||||
|
||||
//#Preview {
|
||||
// FeedView(
|
||||
|
@@ -27,6 +27,7 @@ struct MainView: View {
|
||||
))
|
||||
.transition(.opacity)
|
||||
case .other:
|
||||
|
||||
MeView(onLogout: {}) // 这里可根据需要传递实际登出回调
|
||||
.transition(.opacity)
|
||||
}
|
||||
|
@@ -1,8 +1,10 @@
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
|
||||
struct MeView: View {
|
||||
@State private var showLogoutConfirmation = false
|
||||
let onLogout: () -> Void // 新增:登出回调
|
||||
@State private var showSetting = false // 新增:控制SettingView弹出
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
@@ -15,6 +17,12 @@ struct MeView: View {
|
||||
.font(.system(size: 22, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
Spacer()
|
||||
Button(action: { showSetting = true }) {
|
||||
Image(systemName: "gearshape")
|
||||
.font(.system(size: 22, weight: .regular))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.padding(.trailing, 8)
|
||||
}
|
||||
.padding(.top, geometry.safeAreaInsets.top + 20)
|
||||
|
||||
@@ -74,6 +82,7 @@ struct MeView: View {
|
||||
// 底部安全区域 - 为底部导航栏和安全区域留出空间
|
||||
Color.clear.frame(height: geometry.safeAreaInsets.bottom + 100)
|
||||
}
|
||||
.padding(.top, 100)
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea(.container, edges: .top)
|
||||
@@ -85,6 +94,10 @@ struct MeView: View {
|
||||
} message: {
|
||||
Text("确定要退出登录吗?")
|
||||
}
|
||||
.sheet(isPresented: $showSetting) {
|
||||
// 这里用预设store,实际项目可替换为真实store
|
||||
SettingView(store: Store(initialState: SettingFeature.State()) { SettingFeature() })
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 退出登录方法
|
||||
@@ -132,6 +145,6 @@ struct MenuItemView: View {
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
MeView(onLogout: {})
|
||||
}
|
||||
//#Preview {
|
||||
// MeView(onLogout: {})
|
||||
//}
|
||||
|
@@ -6,73 +6,75 @@ struct SettingView: View {
|
||||
@ObservedObject private var localizationManager = LocalizationManager.shared
|
||||
|
||||
var body: some View {
|
||||
WithPerceptionTracking {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
// 背景图片 - 使用"bg"图片,全屏显示
|
||||
Image("bg")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.ignoresSafeArea(.all)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
// Navigation Bar
|
||||
HStack {
|
||||
// 返回按钮
|
||||
Button(action: {
|
||||
store.send(.dismissTapped)
|
||||
}) {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(.title2)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.padding(.leading, 16)
|
||||
|
||||
Spacer()
|
||||
|
||||
// 标题
|
||||
Text(NSLocalizedString("setting.title", comment: "Settings"))
|
||||
.font(.custom("PingFang SC-Semibold", size: 16))
|
||||
.foregroundColor(.white)
|
||||
|
||||
Spacer()
|
||||
|
||||
// 占位符,保持标题居中
|
||||
Color.clear
|
||||
.frame(width: 44, height: 44)
|
||||
}
|
||||
.padding(.top, 8)
|
||||
.padding(.horizontal)
|
||||
NavigationStack {
|
||||
WithPerceptionTracking {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
// 背景图片 - 使用"bg"图片,全屏显示
|
||||
Image("bg")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.ignoresSafeArea(.all)
|
||||
|
||||
// 内容区域
|
||||
ScrollView {
|
||||
VStack(spacing: 24) {
|
||||
UserInfoCardView(userInfo: store.userInfo, accountModel: store.accountModel)
|
||||
// .padding()
|
||||
.padding(.top, 32)
|
||||
|
||||
SettingOptionsView(
|
||||
onLanguageTapped: {
|
||||
// TODO: 实现语言设置
|
||||
},
|
||||
onAboutTapped: {
|
||||
// TODO: 实现关于页面
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(minLength: 50)
|
||||
|
||||
LogoutButtonView {
|
||||
store.send(.logoutTapped)
|
||||
VStack(spacing: 0) {
|
||||
// Navigation Bar
|
||||
HStack {
|
||||
// 返回按钮
|
||||
Button(action: {
|
||||
store.send(.dismissTapped)
|
||||
}) {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(.title2)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.padding(.leading, 16)
|
||||
|
||||
Spacer()
|
||||
|
||||
// 标题
|
||||
Text(NSLocalizedString("setting.title", comment: "Settings"))
|
||||
.font(.custom("PingFang SC-Semibold", size: 16))
|
||||
.foregroundColor(.white)
|
||||
|
||||
Spacer()
|
||||
|
||||
// 占位符,保持标题居中
|
||||
Color.clear
|
||||
.frame(width: 44, height: 44)
|
||||
}
|
||||
.padding(.top, 8)
|
||||
.padding(.horizontal)
|
||||
|
||||
// 内容区域
|
||||
ScrollView {
|
||||
VStack(spacing: 24) {
|
||||
UserInfoCardView(userInfo: store.userInfo, accountModel: store.accountModel)
|
||||
// .padding()
|
||||
.padding(.top, 32)
|
||||
|
||||
SettingOptionsView(
|
||||
onLanguageTapped: {
|
||||
// TODO: 实现语言设置
|
||||
},
|
||||
onAboutTapped: {
|
||||
// TODO: 实现关于页面
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(minLength: 50)
|
||||
|
||||
LogoutButtonView {
|
||||
store.send(.logoutTapped)
|
||||
}
|
||||
.padding(.bottom, 50)
|
||||
}
|
||||
.padding(.bottom, 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
store.send(.onAppear)
|
||||
.onAppear {
|
||||
store.send(.onAppear)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,6 +125,61 @@ struct UserInfoCardView: View {
|
||||
}
|
||||
|
||||
// MARK: - Setting Options View
|
||||
// Add this new view for testing COS upload
|
||||
struct TestCOSUploadView: View {
|
||||
@State private var imageURL: String = "https://img.toto.im/mw600/66b3de17ly1i3mpcw0k7yj20hs0md0tf.jpg.webp"
|
||||
@State private var uploadResult: String = ""
|
||||
@State private var isUploading: Bool = false
|
||||
@Dependency(\.apiService) private var apiService
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
TextField("Enter image URL", text: $imageURL)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.padding()
|
||||
|
||||
Button(action: {
|
||||
Task {
|
||||
await uploadImageFromURL()
|
||||
}
|
||||
}) {
|
||||
Text(isUploading ? "Uploading..." : "Upload to COS")
|
||||
.padding()
|
||||
.background(Color.blue)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(8)
|
||||
}
|
||||
.disabled(isUploading || imageURL.isEmpty)
|
||||
|
||||
Text(uploadResult)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding()
|
||||
}
|
||||
.navigationTitle("Test COS Upload")
|
||||
}
|
||||
|
||||
private func uploadImageFromURL() async {
|
||||
guard let url = URL(string: imageURL) else {
|
||||
uploadResult = "Invalid URL"
|
||||
return
|
||||
}
|
||||
isUploading = true
|
||||
uploadResult = ""
|
||||
do {
|
||||
let (data, _) = try await URLSession.shared.data(from: url)
|
||||
if let cloudURL = await COSManager.shared.uploadImage(data, apiService: apiService) {
|
||||
uploadResult = "Upload successful! Cloud URL: \(cloudURL)"
|
||||
} else {
|
||||
uploadResult = "Upload failed"
|
||||
}
|
||||
} catch {
|
||||
uploadResult = "Download failed: \(error.localizedDescription)"
|
||||
}
|
||||
isUploading = false
|
||||
}
|
||||
}
|
||||
|
||||
// Modify SettingOptionsView to add the test row
|
||||
struct SettingOptionsView: View {
|
||||
let onLanguageTapped: () -> Void
|
||||
let onAboutTapped: () -> Void
|
||||
@@ -141,6 +198,29 @@ struct SettingOptionsView: View {
|
||||
action: onAboutTapped
|
||||
)
|
||||
|
||||
#if DEBUG
|
||||
NavigationLink(destination: TestCOSUploadView()) {
|
||||
HStack {
|
||||
Image(systemName: "cloud.upload")
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
.frame(width: 24)
|
||||
|
||||
Text("Test COS Upload")
|
||||
.foregroundColor(.white)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundColor(.white.opacity(0.6))
|
||||
.font(.caption)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 16)
|
||||
.background(Color.black.opacity(0.2))
|
||||
.cornerRadius(12)
|
||||
}
|
||||
#endif
|
||||
|
||||
HStack {
|
||||
Image(systemName: "app.badge")
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
@@ -223,4 +303,4 @@ struct SettingRowView: View {
|
||||
// SettingFeature()
|
||||
// }
|
||||
// )
|
||||
//}
|
||||
//}
|
||||
|
Reference in New Issue
Block a user