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: - 响应数据模型
|
// MARK: - 响应数据模型
|
||||||
|
|
||||||
/// 最新动态响应结构
|
/// 最新动态响应结构
|
||||||
struct MomentsLatestResponse: Codable, Equatable {
|
struct MomentsLatestResponse: Codable, Equatable, Sendable {
|
||||||
let code: Int
|
let code: Int
|
||||||
let message: String
|
let message: String
|
||||||
let data: MomentsListData?
|
let data: MomentsListData?
|
||||||
@@ -12,13 +12,13 @@ struct MomentsLatestResponse: Codable, Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 动态列表数据
|
/// 动态列表数据
|
||||||
struct MomentsListData: Codable, Equatable {
|
struct MomentsListData: Codable, Equatable, Sendable {
|
||||||
let dynamicList: [MomentsInfo]
|
let dynamicList: [MomentsInfo]
|
||||||
let nextDynamicId: Int
|
let nextDynamicId: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 动态信息结构
|
/// 动态信息结构
|
||||||
struct MomentsInfo: Codable, Equatable {
|
public struct MomentsInfo: Codable, Equatable, Sendable {
|
||||||
let dynamicId: Int
|
let dynamicId: Int
|
||||||
let uid: Int
|
let uid: Int
|
||||||
let nick: String
|
let nick: String
|
||||||
@@ -66,7 +66,7 @@ struct MomentsInfo: Codable, Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 动态图片信息
|
/// 动态图片信息
|
||||||
struct MomentsPicture: Codable, Equatable {
|
struct MomentsPicture: Codable, Equatable, Sendable {
|
||||||
let id: Int
|
let id: Int
|
||||||
let resUrl: String
|
let resUrl: String
|
||||||
let format: String
|
let format: String
|
||||||
@@ -76,7 +76,7 @@ struct MomentsPicture: Codable, Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 用户VIP信息 - 完整版本,所有字段都是可选的
|
/// 用户VIP信息 - 完整版本,所有字段都是可选的
|
||||||
struct UserVipInfo: Codable, Equatable {
|
struct UserVipInfo: Codable, Equatable, Sendable {
|
||||||
let vipLevel: Int?
|
let vipLevel: Int?
|
||||||
let vipName: String?
|
let vipName: String?
|
||||||
let vipIcon: String?
|
let vipIcon: String?
|
||||||
|
@@ -1,12 +1,16 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import ComposableArchitecture
|
import ComposableArchitecture
|
||||||
|
|
||||||
struct FeedListFeature: Reducer {
|
@Reducer
|
||||||
|
struct FeedListFeature {
|
||||||
|
@Dependency(\.apiService) var apiService
|
||||||
struct State: Equatable {
|
struct State: Equatable {
|
||||||
var feeds: [Feed] = [] // 预留 feed 内容
|
var feeds: [Feed] = [] // 预留 feed 内容
|
||||||
var isLoading: Bool = false
|
var isLoading: Bool = false
|
||||||
var error: String? = nil
|
var error: String? = nil
|
||||||
var isEditFeedPresented: Bool = false // 新增:控制 EditFeedView 弹窗
|
var isEditFeedPresented: Bool = false // 新增:控制 EditFeedView 弹窗
|
||||||
|
// 新增:动态内容
|
||||||
|
var moments: [MomentsInfo] = []
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Action: Equatable {
|
enum Action: Equatable {
|
||||||
@@ -15,13 +19,41 @@ struct FeedListFeature: Reducer {
|
|||||||
case loadMore
|
case loadMore
|
||||||
case editFeedButtonTapped // 新增:点击 add 按钮
|
case editFeedButtonTapped // 新增:点击 add 按钮
|
||||||
case editFeedDismissed // 新增:关闭编辑页
|
case editFeedDismissed // 新增:关闭编辑页
|
||||||
|
// 新增:动态内容相关
|
||||||
|
case fetchFeeds
|
||||||
|
case fetchFeedsResponse(TaskResult<MomentsLatestResponse>)
|
||||||
// 预留后续 Action
|
// 预留后续 Action
|
||||||
}
|
}
|
||||||
|
|
||||||
func reduce(into state: inout State, action: Action) -> Effect<Action> {
|
func reduce(into state: inout State, action: Action) -> Effect<Action> {
|
||||||
switch action {
|
switch action {
|
||||||
case .onAppear:
|
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
|
return .none
|
||||||
case .reload:
|
case .reload:
|
||||||
// 预留刷新逻辑
|
// 预留刷新逻辑
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import ComposableArchitecture
|
import ComposableArchitecture
|
||||||
|
import QCloudCOSXML
|
||||||
|
|
||||||
// MARK: - 腾讯云 COS 管理器
|
// MARK: - 腾讯云 COS 管理器
|
||||||
|
|
||||||
@@ -15,6 +16,26 @@ class COSManager: ObservableObject {
|
|||||||
|
|
||||||
private init() {}
|
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 管理
|
// MARK: - Token 管理
|
||||||
|
|
||||||
/// 当前缓存的 Token 信息
|
/// 当前缓存的 Token 信息
|
||||||
@@ -102,13 +123,83 @@ class COSManager: ObservableObject {
|
|||||||
|
|
||||||
/// 获取当前 Token 状态信息
|
/// 获取当前 Token 状态信息
|
||||||
func getTokenStatus() -> String {
|
func getTokenStatus() -> String {
|
||||||
if let cached = cachedToken, let expiration = tokenExpirationDate {
|
if let _ = cachedToken, let expiration = tokenExpirationDate {
|
||||||
let isExpired = Date() >= expiration
|
let isExpired = Date() >= expiration
|
||||||
return "Token 状态: \(isExpired ? "已过期" : "有效"), 过期时间: \(expiration)"
|
return "Token 状态: \(isExpired ? "已过期" : "有效"), 过期时间: \(expiration)"
|
||||||
} else {
|
} else {
|
||||||
return "Token 状态: 未缓存"
|
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: - 调试扩展
|
// MARK: - 调试扩展
|
||||||
@@ -134,4 +225,4 @@ extension COSManager {
|
|||||||
debugInfoSync("✅ 腾讯云 COS Token 测试完成\n")
|
debugInfoSync("✅ 腾讯云 COS Token 测试完成\n")
|
||||||
#endif
|
#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 SwiftUI
|
||||||
import ComposableArchitecture
|
import ComposableArchitecture
|
||||||
|
//import OptimizedDynamicCardView // 导入新组件
|
||||||
|
|
||||||
struct FeedListView: View {
|
struct FeedListView: View {
|
||||||
let store: StoreOf<FeedListFeature>
|
let store: StoreOf<FeedListFeature>
|
||||||
@@ -46,6 +47,35 @@ struct FeedListView: View {
|
|||||||
.foregroundColor(.white.opacity(0.9))
|
.foregroundColor(.white.opacity(0.9))
|
||||||
.padding(.horizontal, 30)
|
.padding(.horizontal, 30)
|
||||||
.padding(.bottom, 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()
|
Spacer()
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .top)
|
.frame(maxWidth: .infinity, alignment: .top)
|
||||||
|
@@ -172,460 +172,460 @@ struct FeedView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 优化的动态卡片组件
|
// MARK: - 优化的动态卡片组件
|
||||||
struct OptimizedDynamicCardView: View {
|
//struct OptimizedDynamicCardView: View {
|
||||||
let moment: MomentsInfo
|
// let moment: MomentsInfo
|
||||||
let allMoments: [MomentsInfo]
|
// let allMoments: [MomentsInfo]
|
||||||
let currentIndex: Int
|
// let currentIndex: Int
|
||||||
|
//
|
||||||
var body: some View {
|
// var body: some View {
|
||||||
WithPerceptionTracking{
|
// WithPerceptionTracking{
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
// VStack(alignment: .leading, spacing: 12) {
|
||||||
// 用户信息
|
// // 用户信息
|
||||||
HStack {
|
// HStack {
|
||||||
// 使用缓存的头像
|
// // 使用缓存的头像
|
||||||
CachedAsyncImage(url: moment.avatar) { image in
|
// CachedAsyncImage(url: moment.avatar) { image in
|
||||||
image
|
// image
|
||||||
.resizable()
|
// .resizable()
|
||||||
.aspectRatio(contentMode: .fill)
|
// .aspectRatio(contentMode: .fill)
|
||||||
} placeholder: {
|
// } placeholder: {
|
||||||
Circle()
|
// Circle()
|
||||||
.fill(Color.gray.opacity(0.3))
|
// .fill(Color.gray.opacity(0.3))
|
||||||
.overlay(
|
// .overlay(
|
||||||
Text(String(moment.nick.prefix(1)))
|
// Text(String(moment.nick.prefix(1)))
|
||||||
.font(.system(size: 16, weight: .medium))
|
// .font(.system(size: 16, weight: .medium))
|
||||||
.foregroundColor(.white)
|
// .foregroundColor(.white)
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
.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(formatTime(moment.publishTime))
|
// 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 标识
|
||||||
if let vipInfo = moment.userVipInfoVO, let vipLevel = vipInfo.vipLevel {
|
// if let vipInfo = moment.userVipInfoVO, let vipLevel = vipInfo.vipLevel {
|
||||||
Text("VIP\(vipLevel)")
|
// Text("VIP\(vipLevel)")
|
||||||
.font(.system(size: 10, weight: .bold))
|
// .font(.system(size: 10, weight: .bold))
|
||||||
.foregroundColor(.yellow)
|
// .foregroundColor(.yellow)
|
||||||
.padding(.horizontal, 6)
|
// .padding(.horizontal, 6)
|
||||||
.padding(.vertical, 2)
|
// .padding(.vertical, 2)
|
||||||
.background(Color.yellow.opacity(0.2))
|
// .background(Color.yellow.opacity(0.2))
|
||||||
.cornerRadius(4)
|
// .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)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// 优化的图片网格
|
// // 优化的图片网格
|
||||||
if let images = moment.dynamicResList, !images.isEmpty {
|
// if let images = moment.dynamicResList, !images.isEmpty {
|
||||||
OptimizedImageGrid(images: images)
|
// OptimizedImageGrid(images: images)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// 互动按钮
|
// // 互动按钮
|
||||||
HStack(spacing: 20) {
|
// HStack(spacing: 20) {
|
||||||
Button(action: {}) {
|
// Button(action: {}) {
|
||||||
HStack(spacing: 4) {
|
// HStack(spacing: 4) {
|
||||||
Image(systemName: "message")
|
// Image(systemName: "message")
|
||||||
.font(.system(size: 16))
|
// .font(.system(size: 16))
|
||||||
Text("\(moment.commentCount)")
|
// Text("\(moment.commentCount)")
|
||||||
.font(.system(size: 14))
|
// .font(.system(size: 14))
|
||||||
}
|
// }
|
||||||
.foregroundColor(.white.opacity(0.8))
|
// .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")
|
||||||
.font(.system(size: 16))
|
// .font(.system(size: 16))
|
||||||
Text("\(moment.likeCount)")
|
// Text("\(moment.likeCount)")
|
||||||
.font(.system(size: 14))
|
// .font(.system(size: 14))
|
||||||
}
|
// }
|
||||||
.foregroundColor(moment.isLike ? .red : .white.opacity(0.8))
|
// .foregroundColor(moment.isLike ? .red : .white.opacity(0.8))
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Spacer()
|
// Spacer()
|
||||||
}
|
// }
|
||||||
.padding(.top, 8)
|
// .padding(.top, 8)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
.padding(16)
|
// .padding(16)
|
||||||
.background(
|
// .background(
|
||||||
Color.white.opacity(0.1)
|
// Color.white.opacity(0.1)
|
||||||
.cornerRadius(12)
|
// .cornerRadius(12)
|
||||||
)
|
// )
|
||||||
.onAppear {
|
// .onAppear {
|
||||||
// 预加载相邻的图片
|
// // 预加载相邻的图片
|
||||||
preloadNearbyImages()
|
// preloadNearbyImages()
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private func formatTime(_ timestamp: Int) -> String {
|
// private func formatTime(_ timestamp: Int) -> String {
|
||||||
let date = Date(timeIntervalSince1970: TimeInterval(timestamp) / 1000.0)
|
// let date = Date(timeIntervalSince1970: TimeInterval(timestamp) / 1000.0)
|
||||||
let formatter = DateFormatter()
|
// let formatter = DateFormatter()
|
||||||
formatter.locale = Locale(identifier: "zh_CN")
|
// formatter.locale = Locale(identifier: "zh_CN")
|
||||||
|
//
|
||||||
let now = Date()
|
// let now = Date()
|
||||||
let interval = now.timeIntervalSince(date)
|
// let interval = now.timeIntervalSince(date)
|
||||||
|
//
|
||||||
if interval < 60 {
|
// if interval < 60 {
|
||||||
return "刚刚"
|
// return "刚刚"
|
||||||
} else if interval < 3600 {
|
// } else if interval < 3600 {
|
||||||
return "\(Int(interval / 60))分钟前"
|
// return "\(Int(interval / 60))分钟前"
|
||||||
} else if interval < 86400 {
|
// } else if interval < 86400 {
|
||||||
return "\(Int(interval / 3600))小时前"
|
// return "\(Int(interval / 3600))小时前"
|
||||||
} else {
|
// } else {
|
||||||
formatter.dateFormat = "MM-dd HH:mm"
|
// formatter.dateFormat = "MM-dd HH:mm"
|
||||||
return formatter.string(from: date)
|
// return formatter.string(from: date)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private func preloadNearbyImages() {
|
// private func preloadNearbyImages() {
|
||||||
var urlsToPreload: [String] = []
|
// var urlsToPreload: [String] = []
|
||||||
|
//
|
||||||
// 预加载前后2个动态的图片
|
// // 预加载前后2个动态的图片
|
||||||
let preloadRange = max(0, currentIndex - 2)...min(allMoments.count - 1, currentIndex + 2)
|
// let preloadRange = max(0, currentIndex - 2)...min(allMoments.count - 1, currentIndex + 2)
|
||||||
|
//
|
||||||
for index in preloadRange {
|
// for index in preloadRange {
|
||||||
let moment = allMoments[index]
|
// let moment = allMoments[index]
|
||||||
|
//
|
||||||
// 添加头像
|
// // 添加头像
|
||||||
urlsToPreload.append(moment.avatar)
|
// urlsToPreload.append(moment.avatar)
|
||||||
|
//
|
||||||
// 添加动态图片
|
// // 添加动态图片
|
||||||
if let images = moment.dynamicResList {
|
// if let images = moment.dynamicResList {
|
||||||
urlsToPreload.append(contentsOf: images.map { $0.resUrl })
|
// urlsToPreload.append(contentsOf: images.map { $0.resUrl })
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// 异步预加载
|
// // 异步预加载
|
||||||
ImageCacheManager.shared.preloadImages(urls: urlsToPreload)
|
// ImageCacheManager.shared.preloadImages(urls: urlsToPreload)
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|
||||||
// MARK: - 优化的图片网格
|
// MARK: - 优化的图片网格
|
||||||
struct OptimizedImageGrid: View {
|
//struct OptimizedImageGrid: View {
|
||||||
let images: [MomentsPicture]
|
// let images: [MomentsPicture]
|
||||||
|
//
|
||||||
var body: some View {
|
// var body: some View {
|
||||||
GeometryReader { geometry in
|
// GeometryReader { geometry in
|
||||||
let availableWidth = max(geometry.size.width, 1) // 防止为0或负数
|
// let availableWidth = max(geometry.size.width, 1) // 防止为0或负数
|
||||||
let spacing: CGFloat = 8
|
// let spacing: CGFloat = 8
|
||||||
|
//
|
||||||
// 保护:如果availableWidth不合理,直接返回空视图
|
// // 保护:如果availableWidth不合理,直接返回空视图
|
||||||
if availableWidth < 10 {
|
// if availableWidth < 10 {
|
||||||
Color.clear.frame(height: 1)
|
// Color.clear.frame(height: 1)
|
||||||
} else {
|
// } else {
|
||||||
switch images.count {
|
// switch images.count {
|
||||||
case 1:
|
// case 1:
|
||||||
// 单张图片:大正方形居中显示
|
// // 单张图片:大正方形居中显示
|
||||||
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)
|
||||||
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)
|
// SquareImageView(image: images[1], size: imageSize)
|
||||||
}
|
// }
|
||||||
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(images.prefix(3), id: \.id) { image in
|
||||||
SquareImageView(image: image, size: imageSize)
|
// SquareImageView(image: image, size: imageSize)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
default:
|
// default:
|
||||||
// 四张及以上:九宫格布局(最多9张)
|
// // 四张及以上:九宫格布局(最多9张)
|
||||||
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(images.prefix(9), id: \.id) { image in
|
||||||
SquareImageView(image: image, size: imageSize)
|
// SquareImageView(image: image, size: imageSize)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
.frame(height: calculateGridHeight())
|
// .frame(height: calculateGridHeight())
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private func calculateGridHeight() -> CGFloat {
|
// private func calculateGridHeight() -> CGFloat {
|
||||||
switch images.count {
|
// switch images.count {
|
||||||
case 1:
|
// case 1:
|
||||||
return 200 // 单张图片的最大高度
|
// return 200 // 单张图片的最大高度
|
||||||
case 2:
|
// case 2:
|
||||||
return 120 // 两张图片并排的高度
|
// return 120 // 两张图片并排的高度
|
||||||
case 3:
|
// case 3:
|
||||||
return 100 // 三张图片水平排列的高度
|
// return 100 // 三张图片水平排列的高度
|
||||||
case 4...6:
|
// case 4...6:
|
||||||
return 216 // 九宫格2行的高度 (实际图片大小 * 2 + 间距)
|
// return 216 // 九宫格2行的高度 (实际图片大小 * 2 + 间距)
|
||||||
default:
|
// default:
|
||||||
return 340 // 九宫格3行的高度 (实际图片大小 * 3 + 间距 + 额外空间)
|
// return 340 // 九宫格3行的高度 (实际图片大小 * 3 + 间距 + 额外空间)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|
||||||
// MARK: - 正方形图片视图组件
|
// MARK: - 正方形图片视图组件
|
||||||
struct SquareImageView: View {
|
//struct SquareImageView: View {
|
||||||
let image: MomentsPicture
|
// let image: MomentsPicture
|
||||||
let size: CGFloat
|
// let size: CGFloat
|
||||||
|
//
|
||||||
var body: some View {
|
// var body: some View {
|
||||||
let safeSize = size.isFinite && size > 0 ? size : 100 // 防止非有限或负数
|
// let safeSize = size.isFinite && size > 0 ? size : 100 // 防止非有限或负数
|
||||||
CachedAsyncImage(url: image.resUrl) { imageView in
|
// CachedAsyncImage(url: image.resUrl) { imageView in
|
||||||
imageView
|
// imageView
|
||||||
.resizable()
|
// .resizable()
|
||||||
.aspectRatio(contentMode: .fill)
|
// .aspectRatio(contentMode: .fill)
|
||||||
} placeholder: {
|
// } placeholder: {
|
||||||
Rectangle()
|
// Rectangle()
|
||||||
.fill(Color.gray.opacity(0.3))
|
// .fill(Color.gray.opacity(0.3))
|
||||||
.overlay(
|
// .overlay(
|
||||||
ProgressView()
|
// ProgressView()
|
||||||
.progressViewStyle(CircularProgressViewStyle(tint: .white.opacity(0.6)))
|
// .progressViewStyle(CircularProgressViewStyle(tint: .white.opacity(0.6)))
|
||||||
.scaleEffect(0.8)
|
// .scaleEffect(0.8)
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
.frame(width: safeSize, height: safeSize)
|
// .frame(width: safeSize, height: safeSize)
|
||||||
.clipped()
|
// .clipped()
|
||||||
.cornerRadius(8)
|
// .cornerRadius(8)
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|
||||||
// MARK: - 旧的真实动态卡片组件(保留备用)
|
// MARK: - 旧的真实动态卡片组件(保留备用)
|
||||||
struct RealDynamicCardView: View {
|
//struct RealDynamicCardView: View {
|
||||||
let moment: MomentsInfo
|
// let moment: MomentsInfo
|
||||||
|
//
|
||||||
var body: some View {
|
// var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
// VStack(alignment: .leading, spacing: 12) {
|
||||||
// 用户信息
|
// // 用户信息
|
||||||
HStack {
|
// HStack {
|
||||||
AsyncImage(url: URL(string: moment.avatar)) { image in
|
// AsyncImage(url: URL(string: moment.avatar)) { image in
|
||||||
image
|
// image
|
||||||
.resizable()
|
// .resizable()
|
||||||
.aspectRatio(contentMode: .fill)
|
// .aspectRatio(contentMode: .fill)
|
||||||
} placeholder: {
|
// } placeholder: {
|
||||||
Circle()
|
// Circle()
|
||||||
.fill(Color.gray.opacity(0.3))
|
// .fill(Color.gray.opacity(0.3))
|
||||||
.overlay(
|
// .overlay(
|
||||||
Text(String(moment.nick.prefix(1)))
|
// Text(String(moment.nick.prefix(1)))
|
||||||
.font(.system(size: 16, weight: .medium))
|
// .font(.system(size: 16, weight: .medium))
|
||||||
.foregroundColor(.white)
|
// .foregroundColor(.white)
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
.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(formatTime(moment.publishTime))
|
// 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 标识
|
||||||
if let vipInfo = moment.userVipInfoVO, let vipLevel = vipInfo.vipLevel {
|
// if let vipInfo = moment.userVipInfoVO, let vipLevel = vipInfo.vipLevel {
|
||||||
Text("VIP\(vipLevel)")
|
// Text("VIP\(vipLevel)")
|
||||||
.font(.system(size: 10, weight: .bold))
|
// .font(.system(size: 10, weight: .bold))
|
||||||
.foregroundColor(.yellow)
|
// .foregroundColor(.yellow)
|
||||||
.padding(.horizontal, 6)
|
// .padding(.horizontal, 6)
|
||||||
.padding(.vertical, 2)
|
// .padding(.vertical, 2)
|
||||||
.background(Color.yellow.opacity(0.2))
|
// .background(Color.yellow.opacity(0.2))
|
||||||
.cornerRadius(4)
|
// .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)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// 图片网格
|
// // 图片网格
|
||||||
if let images = moment.dynamicResList, !images.isEmpty {
|
// if let images = moment.dynamicResList, !images.isEmpty {
|
||||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: min(images.count, 3)), spacing: 8) {
|
// LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: min(images.count, 3)), spacing: 8) {
|
||||||
ForEach(images.prefix(9), id: \.id) { image in
|
// ForEach(images.prefix(9), id: \.id) { image in
|
||||||
AsyncImage(url: URL(string: image.resUrl)) { imageView in
|
// AsyncImage(url: URL(string: image.resUrl)) { imageView in
|
||||||
imageView
|
// imageView
|
||||||
.resizable()
|
// .resizable()
|
||||||
.aspectRatio(contentMode: .fill)
|
// .aspectRatio(contentMode: .fill)
|
||||||
} placeholder: {
|
// } placeholder: {
|
||||||
Rectangle()
|
// Rectangle()
|
||||||
.fill(Color.gray.opacity(0.3))
|
// .fill(Color.gray.opacity(0.3))
|
||||||
.overlay(
|
// .overlay(
|
||||||
ProgressView()
|
// ProgressView()
|
||||||
.progressViewStyle(CircularProgressViewStyle(tint: .white.opacity(0.6)))
|
// .progressViewStyle(CircularProgressViewStyle(tint: .white.opacity(0.6)))
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
.frame(height: 100)
|
// .frame(height: 100)
|
||||||
.clipped()
|
// .clipped()
|
||||||
.cornerRadius(8)
|
// .cornerRadius(8)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// 互动按钮
|
// // 互动按钮
|
||||||
HStack(spacing: 20) {
|
// HStack(spacing: 20) {
|
||||||
Button(action: {}) {
|
// Button(action: {}) {
|
||||||
HStack(spacing: 4) {
|
// HStack(spacing: 4) {
|
||||||
Image(systemName: "message")
|
// Image(systemName: "message")
|
||||||
.font(.system(size: 16))
|
// .font(.system(size: 16))
|
||||||
Text("\(moment.commentCount)")
|
// Text("\(moment.commentCount)")
|
||||||
.font(.system(size: 14))
|
// .font(.system(size: 14))
|
||||||
}
|
// }
|
||||||
.foregroundColor(.white.opacity(0.8))
|
// .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")
|
||||||
.font(.system(size: 16))
|
// .font(.system(size: 16))
|
||||||
Text("\(moment.likeCount)")
|
// Text("\(moment.likeCount)")
|
||||||
.font(.system(size: 14))
|
// .font(.system(size: 14))
|
||||||
}
|
// }
|
||||||
.foregroundColor(moment.isLike ? .red : .white.opacity(0.8))
|
// .foregroundColor(moment.isLike ? .red : .white.opacity(0.8))
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Spacer()
|
// Spacer()
|
||||||
}
|
// }
|
||||||
.padding(.top, 8)
|
// .padding(.top, 8)
|
||||||
}
|
// }
|
||||||
.padding(16)
|
// .padding(16)
|
||||||
.background(
|
// .background(
|
||||||
Color.white.opacity(0.1)
|
// Color.white.opacity(0.1)
|
||||||
.cornerRadius(12)
|
// .cornerRadius(12)
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private func formatTime(_ timestamp: Int) -> String {
|
// private func formatTime(_ timestamp: Int) -> String {
|
||||||
let date = Date(timeIntervalSince1970: TimeInterval(timestamp) / 1000.0)
|
// let date = Date(timeIntervalSince1970: TimeInterval(timestamp) / 1000.0)
|
||||||
let formatter = DateFormatter()
|
// let formatter = DateFormatter()
|
||||||
formatter.locale = Locale(identifier: "zh_CN")
|
// formatter.locale = Locale(identifier: "zh_CN")
|
||||||
|
//
|
||||||
let now = Date()
|
// let now = Date()
|
||||||
let interval = now.timeIntervalSince(date)
|
// let interval = now.timeIntervalSince(date)
|
||||||
|
//
|
||||||
if interval < 60 {
|
// if interval < 60 {
|
||||||
return "刚刚"
|
// return "刚刚"
|
||||||
} else if interval < 3600 {
|
// } else if interval < 3600 {
|
||||||
return "\(Int(interval / 60))分钟前"
|
// return "\(Int(interval / 60))分钟前"
|
||||||
} else if interval < 86400 {
|
// } else if interval < 86400 {
|
||||||
return "\(Int(interval / 3600))小时前"
|
// return "\(Int(interval / 3600))小时前"
|
||||||
} else {
|
// } else {
|
||||||
formatter.dateFormat = "MM-dd HH:mm"
|
// formatter.dateFormat = "MM-dd HH:mm"
|
||||||
return formatter.string(from: date)
|
// return formatter.string(from: date)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|
||||||
// MARK: - 旧的模拟卡片组件(保留备用)
|
// MARK: - 旧的模拟卡片组件(保留备用)
|
||||||
struct DynamicCardView: View {
|
//struct DynamicCardView: View {
|
||||||
let index: Int
|
// let index: Int
|
||||||
|
//
|
||||||
var body: some View {
|
// var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
// VStack(alignment: .leading, spacing: 12) {
|
||||||
// 用户信息
|
// // 用户信息
|
||||||
HStack {
|
// HStack {
|
||||||
Circle()
|
// Circle()
|
||||||
.fill(Color.gray.opacity(0.3))
|
// .fill(Color.gray.opacity(0.3))
|
||||||
.frame(width: 40, height: 40)
|
// .frame(width: 40, height: 40)
|
||||||
.overlay(
|
// .overlay(
|
||||||
Text("U\(index + 1)")
|
// Text("U\(index + 1)")
|
||||||
.font(.system(size: 16, weight: .medium))
|
// .font(.system(size: 16, weight: .medium))
|
||||||
.foregroundColor(.white)
|
// .foregroundColor(.white)
|
||||||
)
|
// )
|
||||||
|
//
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
// VStack(alignment: .leading, spacing: 2) {
|
||||||
Text("用户\(index + 1)")
|
// Text("用户\(index + 1)")
|
||||||
.font(.system(size: 16, weight: .medium))
|
// .font(.system(size: 16, weight: .medium))
|
||||||
.foregroundColor(.white)
|
// .foregroundColor(.white)
|
||||||
|
//
|
||||||
Text("2小时前")
|
// Text("2小时前")
|
||||||
.font(.system(size: 12))
|
// .font(.system(size: 12))
|
||||||
.foregroundColor(.white.opacity(0.6))
|
// .foregroundColor(.white.opacity(0.6))
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Spacer()
|
// Spacer()
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// 动态内容
|
// // 动态内容
|
||||||
Text("今天是美好的一天,分享一些生活中的小确幸。希望大家都能珍惜每一个当下的时刻。")
|
// Text("今天是美好的一天,分享一些生活中的小确幸。希望大家都能珍惜每一个当下的时刻。")
|
||||||
.font(.system(size: 14))
|
// .font(.system(size: 14))
|
||||||
.foregroundColor(.white.opacity(0.9))
|
// .foregroundColor(.white.opacity(0.9))
|
||||||
.multilineTextAlignment(.leading)
|
// .multilineTextAlignment(.leading)
|
||||||
|
//
|
||||||
// 图片网格
|
// // 图片网格
|
||||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 3), spacing: 8) {
|
// LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 3), spacing: 8) {
|
||||||
ForEach(0..<3) { imageIndex in
|
// ForEach(0..<3) { imageIndex in
|
||||||
Rectangle()
|
// Rectangle()
|
||||||
.fill(Color.gray.opacity(0.3))
|
// .fill(Color.gray.opacity(0.3))
|
||||||
.aspectRatio(1, contentMode: .fit)
|
// .aspectRatio(1, contentMode: .fit)
|
||||||
.overlay(
|
// .overlay(
|
||||||
Image(systemName: "photo")
|
// Image(systemName: "photo")
|
||||||
.foregroundColor(.white.opacity(0.6))
|
// .foregroundColor(.white.opacity(0.6))
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// 互动按钮
|
// // 互动按钮
|
||||||
HStack(spacing: 20) {
|
// HStack(spacing: 20) {
|
||||||
Button(action: {}) {
|
// Button(action: {}) {
|
||||||
HStack(spacing: 4) {
|
// HStack(spacing: 4) {
|
||||||
Image(systemName: "message")
|
// Image(systemName: "message")
|
||||||
.font(.system(size: 16))
|
// .font(.system(size: 16))
|
||||||
Text("354")
|
// Text("354")
|
||||||
.font(.system(size: 14))
|
// .font(.system(size: 14))
|
||||||
}
|
// }
|
||||||
.foregroundColor(.white.opacity(0.8))
|
// .foregroundColor(.white.opacity(0.8))
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Button(action: {}) {
|
// Button(action: {}) {
|
||||||
HStack(spacing: 4) {
|
// HStack(spacing: 4) {
|
||||||
Image(systemName: "heart")
|
// Image(systemName: "heart")
|
||||||
.font(.system(size: 16))
|
// .font(.system(size: 16))
|
||||||
Text("354")
|
// Text("354")
|
||||||
.font(.system(size: 14))
|
// .font(.system(size: 14))
|
||||||
}
|
// }
|
||||||
.foregroundColor(.white.opacity(0.8))
|
// .foregroundColor(.white.opacity(0.8))
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Spacer()
|
// Spacer()
|
||||||
}
|
// }
|
||||||
.padding(.top, 8)
|
// .padding(.top, 8)
|
||||||
}
|
// }
|
||||||
.padding(16)
|
// .padding(16)
|
||||||
.background(
|
// .background(
|
||||||
Color.white.opacity(0.1)
|
// Color.white.opacity(0.1)
|
||||||
.cornerRadius(12)
|
// .cornerRadius(12)
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|
||||||
//#Preview {
|
//#Preview {
|
||||||
// FeedView(
|
// FeedView(
|
||||||
|
@@ -27,6 +27,7 @@ struct MainView: View {
|
|||||||
))
|
))
|
||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
case .other:
|
case .other:
|
||||||
|
|
||||||
MeView(onLogout: {}) // 这里可根据需要传递实际登出回调
|
MeView(onLogout: {}) // 这里可根据需要传递实际登出回调
|
||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import ComposableArchitecture
|
||||||
|
|
||||||
struct MeView: View {
|
struct MeView: View {
|
||||||
@State private var showLogoutConfirmation = false
|
@State private var showLogoutConfirmation = false
|
||||||
let onLogout: () -> Void // 新增:登出回调
|
let onLogout: () -> Void // 新增:登出回调
|
||||||
|
@State private var showSetting = false // 新增:控制SettingView弹出
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
@@ -15,6 +17,12 @@ struct MeView: View {
|
|||||||
.font(.system(size: 22, weight: .semibold))
|
.font(.system(size: 22, weight: .semibold))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
Spacer()
|
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)
|
.padding(.top, geometry.safeAreaInsets.top + 20)
|
||||||
|
|
||||||
@@ -74,6 +82,7 @@ struct MeView: View {
|
|||||||
// 底部安全区域 - 为底部导航栏和安全区域留出空间
|
// 底部安全区域 - 为底部导航栏和安全区域留出空间
|
||||||
Color.clear.frame(height: geometry.safeAreaInsets.bottom + 100)
|
Color.clear.frame(height: geometry.safeAreaInsets.bottom + 100)
|
||||||
}
|
}
|
||||||
|
.padding(.top, 100)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ignoresSafeArea(.container, edges: .top)
|
.ignoresSafeArea(.container, edges: .top)
|
||||||
@@ -85,6 +94,10 @@ struct MeView: View {
|
|||||||
} message: {
|
} message: {
|
||||||
Text("确定要退出登录吗?")
|
Text("确定要退出登录吗?")
|
||||||
}
|
}
|
||||||
|
.sheet(isPresented: $showSetting) {
|
||||||
|
// 这里用预设store,实际项目可替换为真实store
|
||||||
|
SettingView(store: Store(initialState: SettingFeature.State()) { SettingFeature() })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 退出登录方法
|
// MARK: - 退出登录方法
|
||||||
@@ -132,6 +145,6 @@ struct MenuItemView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
//#Preview {
|
||||||
MeView(onLogout: {})
|
// MeView(onLogout: {})
|
||||||
}
|
//}
|
||||||
|
@@ -6,73 +6,75 @@ struct SettingView: View {
|
|||||||
@ObservedObject private var localizationManager = LocalizationManager.shared
|
@ObservedObject private var localizationManager = LocalizationManager.shared
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
WithPerceptionTracking {
|
NavigationStack {
|
||||||
GeometryReader { geometry in
|
WithPerceptionTracking {
|
||||||
ZStack {
|
GeometryReader { geometry in
|
||||||
// 背景图片 - 使用"bg"图片,全屏显示
|
ZStack {
|
||||||
Image("bg")
|
// 背景图片 - 使用"bg"图片,全屏显示
|
||||||
.resizable()
|
Image("bg")
|
||||||
.aspectRatio(contentMode: .fill)
|
.resizable()
|
||||||
.ignoresSafeArea(.all)
|
.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)
|
|
||||||
|
|
||||||
// 内容区域
|
VStack(spacing: 0) {
|
||||||
ScrollView {
|
// Navigation Bar
|
||||||
VStack(spacing: 24) {
|
HStack {
|
||||||
UserInfoCardView(userInfo: store.userInfo, accountModel: store.accountModel)
|
// 返回按钮
|
||||||
// .padding()
|
Button(action: {
|
||||||
.padding(.top, 32)
|
store.send(.dismissTapped)
|
||||||
|
}) {
|
||||||
SettingOptionsView(
|
Image(systemName: "chevron.left")
|
||||||
onLanguageTapped: {
|
.font(.title2)
|
||||||
// TODO: 实现语言设置
|
.foregroundColor(.white)
|
||||||
},
|
}
|
||||||
onAboutTapped: {
|
.padding(.leading, 16)
|
||||||
// TODO: 实现关于页面
|
|
||||||
}
|
Spacer()
|
||||||
)
|
|
||||||
|
// 标题
|
||||||
Spacer(minLength: 50)
|
Text(NSLocalizedString("setting.title", comment: "Settings"))
|
||||||
|
.font(.custom("PingFang SC-Semibold", size: 16))
|
||||||
LogoutButtonView {
|
.foregroundColor(.white)
|
||||||
store.send(.logoutTapped)
|
|
||||||
|
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 {
|
||||||
.onAppear {
|
store.send(.onAppear)
|
||||||
store.send(.onAppear)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,6 +125,61 @@ struct UserInfoCardView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Setting Options 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 {
|
struct SettingOptionsView: View {
|
||||||
let onLanguageTapped: () -> Void
|
let onLanguageTapped: () -> Void
|
||||||
let onAboutTapped: () -> Void
|
let onAboutTapped: () -> Void
|
||||||
@@ -141,6 +198,29 @@ struct SettingOptionsView: View {
|
|||||||
action: onAboutTapped
|
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 {
|
HStack {
|
||||||
Image(systemName: "app.badge")
|
Image(systemName: "app.badge")
|
||||||
.foregroundColor(.white.opacity(0.8))
|
.foregroundColor(.white.opacity(0.8))
|
||||||
@@ -223,4 +303,4 @@ struct SettingRowView: View {
|
|||||||
// SettingFeature()
|
// SettingFeature()
|
||||||
// }
|
// }
|
||||||
// )
|
// )
|
||||||
//}
|
//}
|
||||||
|
Reference in New Issue
Block a user